From 82177a50da735cc0443ac10fa490d69368403d71 Mon Sep 17 00:00:00 2001 From: dotnet-bot Date: Wed, 11 Mar 2015 19:07:59 -0700 Subject: [PATCH] Initial code commit. Adding sources from Visual Studio 2015 release branch. --- .gitattributes | 80 + .gitignore | 183 + BuildAndCopy.cmd | 59 + MSBuildLocalSystemDependencies.txt | 13 + README.md | 47 + RebuildWithLocalMSBuild.cmd | 24 + build.cmd | 30 + build.proj | 10 + dir.props | 69 + dir.targets | 61 + dir.traversal.targets | 36 + src/.nuget/NuGet.Config | 14 + src/.nuget/packages.config | 4 + src/Framework/AssemblyInfo.cs | 52 + src/Framework/BuildEngineResult.cs | 67 + src/Framework/BuildErrorEventArgs.cs | 394 + src/Framework/BuildEventArgs.cs | 354 + src/Framework/BuildEventContext.cs | 298 + src/Framework/BuildFinishedEventArgs.cs | 133 + src/Framework/BuildMessageEventArgs.cs | 485 + src/Framework/BuildStartedEventArgs.cs | 113 + src/Framework/BuildStatusEventArgs.cs | 91 + src/Framework/BuildWarningEventArgs.cs | 354 + .../CriticalBuildMessageEventArgs.cs | 138 + src/Framework/CustomBuildEventArgs.cs | 88 + src/Framework/Event args classes.cd | 102 + src/Framework/EventContext.cs | 115 + .../ExternalProjectFinishedEventArgs.cs | 102 + .../ExternalProjectStartedEventArgs.cs | 104 + .../Microsoft.Build.Framework.Suppressions.cs | 13 + src/Framework/IBuildEngine.cs | 105 + src/Framework/IBuildEngine2.cs | 86 + src/Framework/IBuildEngine3.cs | 60 + src/Framework/IBuildEngine4.cs | 90 + src/Framework/ICancelableTask.cs | 24 + src/Framework/IEventRedirector.cs | 16 + src/Framework/IEventSource.cs | 155 + src/Framework/IForwardingLogger.cs | 33 + src/Framework/IGeneratedTask.cs | 46 + src/Framework/ILogger.cs | 100 + src/Framework/INodeLogger.cs | 18 + src/Framework/ITask.cs | 44 + src/Framework/ITaskFactory.cs | 75 + src/Framework/ITaskFactory2.cs | 67 + src/Framework/ITaskHost.cs | 22 + src/Framework/ITaskItem.cs | 94 + src/Framework/ITaskItem2.cs | 61 + src/Framework/LazyFormattedBuildEventArgs.cs | 296 + .../LoadInSeparateAppDomainAttribute.cs | 23 + src/Framework/LoggerException.cs | 138 + .../Microsoft.Build.Framework.csproj | 172 + src/Framework/OutputAttribute.cs | 25 + src/Framework/ProjectFinishedEventArgs.cs | 144 + src/Framework/ProjectStartedEventArgs.cs | 551 + src/Framework/RequiredAttribute.cs | 22 + src/Framework/RequiredRuntimeAttribute.cs | 40 + src/Framework/RunInMTAAttribute.cs | 28 + src/Framework/RunInSTAAttribute.cs | 28 + src/Framework/TargetFinishedEventArgs.cs | 265 + src/Framework/TargetStartedEventArgs.cs | 238 + src/Framework/TaskCommandLineEventArgs.cs | 97 + src/Framework/TaskFinishedEventArgs.cs | 224 + src/Framework/TaskPropertyInfo.cs | 54 + src/Framework/TaskStartedEventArgs.cs | 204 + src/Framework/UnitTests/Attribute_Tests.cs | 78 + .../UnitTests/BuildErrorEventArgs_Tests.cs | 55 + .../UnitTests/BuildFinishedEventArgs_Tests.cs | 56 + .../UnitTests/BuildMessageEventArgs_Tests.cs | 62 + .../UnitTests/BuildStartedEventArgs_Tests.cs | 64 + .../UnitTests/BuildWarningEventArgs_Tests.cs | 65 + .../CriticalBuildMessageEventArgs_Tests.cs | 50 + .../CustomEventArgSerialization_Tests.cs | 981 + src/Framework/UnitTests/EventArgs_Tests.cs | 179 + .../ExternalProjectFinishedEventArgs_Tests.cs | 54 + .../ExternalProjectStartedEventArgs_Tests.cs | 53 + .../UnitTests/LoggerException_Tests.cs | 75 + ...Microsoft.Build.Framework.UnitTests.csproj | 59 + .../ProjectFinishedEventArgs_Tests.cs | 54 + .../ProjectStartedEventArgs_Tests.cs | 97 + .../TargetFinishedEventArgs_Tests.cs | 56 + .../UnitTests/TargetStartedEventArgs_Tests.cs | 54 + .../TaskCommandLineEventArgs_Tests.cs | 48 + .../UnitTests/TaskFinishedEventArgs_Tests.cs | 54 + .../UnitTests/TaskStartedEventArgs_Tests.cs | 54 + src/Framework/XamlTypes/Argument.cs | 95 + src/Framework/XamlTypes/BaseProperty.cs | 447 + src/Framework/XamlTypes/BoolProperty.cs | 37 + src/Framework/XamlTypes/Category.cs | 161 + src/Framework/XamlTypes/CategorySchema.cs | 19 + src/Framework/XamlTypes/ContentType.cs | 170 + src/Framework/XamlTypes/DataSource.cs | 187 + .../XamlTypes/DynamicEnumProperty.cs | 52 + src/Framework/XamlTypes/EnumProperty.cs | 61 + src/Framework/XamlTypes/EnumValue.cs | 180 + src/Framework/XamlTypes/FileExtension.cs | 66 + src/Framework/XamlTypes/IProjectSchemaNode.cs | 28 + src/Framework/XamlTypes/IntProperty.cs | 61 + src/Framework/XamlTypes/ItemType.cs | 105 + src/Framework/XamlTypes/NameValuePair.cs | 51 + .../XamlTypes/ProjectSchemaDefinitions.cs | 73 + src/Framework/XamlTypes/Rule.cs | 617 + src/Framework/XamlTypes/RuleBag.cs | 97 + src/Framework/XamlTypes/RuleSchema.cs | 19 + src/Framework/XamlTypes/StringListProperty.cs | 74 + src/Framework/XamlTypes/StringProperty.cs | 34 + src/Framework/XamlTypes/ValueEditor.cs | 102 + src/Framework/native.rc | 5 + src/MSBuild.sln | 166 + src/Packages.dgml | 20 + src/Shared/AssemblyFoldersEx.cs | 474 + src/Shared/AssemblyLoadInfo.cs | 204 + src/Shared/AssemblyNameComparer.cs | 106 + src/Shared/AssemblyNameExtension.cs | 914 + .../AssemblyNameReverseVersionComparer.cs | 80 + src/Shared/AwaitExtensions.cs | 204 + src/Shared/BuildEventFileInfo.cs | 177 + src/Shared/CanonicalError.cs | 415 + src/Shared/CollectionHelpers.cs | 56 + src/Shared/CommunicationsUtilities.cs | 654 + src/Shared/Constants.cs | 107 + src/Shared/ConversionUtilities.cs | 146 + src/Shared/CopyOnWriteDictionary.cs | 656 + src/Shared/EncodingUtilities.cs | 54 + src/Shared/ErrorUtilities.cs | 758 + src/Shared/EscapingUtilities.cs | 297 + src/Shared/EventArgsFormatting.cs | 362 + src/Shared/ExceptionHandling.cs | 334 + src/Shared/ExtensionFoldersRegistryKey.cs | 72 + src/Shared/FileDelegates.cs | 59 + src/Shared/FileMatcher.cs | 1300 ++ src/Shared/FileUtilities.cs | 934 + src/Shared/FileUtilitiesRegex.cs | 29 + src/Shared/FrameworkLocationHelper.cs | 1580 ++ .../Microsoft.Build.Shared.Suppressions.cs | 156 + src/Shared/HybridDictionary.cs | 953 + src/Shared/IElementLocation.cs | 65 + src/Shared/IKeyed.cs | 26 + src/Shared/INodeEndpoint.cs | 113 + src/Shared/INodePacket.cs | 199 + src/Shared/INodePacketFactory.cs | 60 + src/Shared/INodePacketHandler.cs | 26 + src/Shared/INodePacketTranslatable.cs | 28 + src/Shared/INodePacketTranslator.cs | 301 + src/Shared/InprocTrackingNativeMethods.cs | 286 + src/Shared/InternalErrorException.cs | 133 + src/Shared/InterningBinaryReader.cs | 237 + .../LanguageParser/CSharptokenCharReader.cs | 139 + .../LanguageParser/CSharptokenEnumerator.cs | 328 + src/Shared/LanguageParser/CSharptokenizer.cs | 66 + .../LanguageParser/StreamMappedString.cs | 416 + .../VisualBasictokenCharReader.cs | 261 + .../VisualBasictokenEnumerator.cs | 335 + .../LanguageParser/VisualBasictokenizer.cs | 63 + src/Shared/LanguageParser/token.cs | 91 + src/Shared/LanguageParser/tokenChar.cs | 180 + src/Shared/LanguageParser/tokenCharReader.cs | 361 + src/Shared/LanguageParser/tokenEnumerator.cs | 85 + src/Shared/LoadedType.cs | 164 + src/Shared/LogMessagePacketBase.cs | 891 + src/Shared/MSBuildNameIgnoreCaseComparer.cs | 455 + src/Shared/MetadataConversionUtilities.cs | 70 + src/Shared/Modifiers.cs | 616 + src/Shared/NGen.cs | 55 + src/Shared/NativeMethodsShared.cs | 930 + src/Shared/NodeBuildComplete.cs | 89 + src/Shared/NodeEndpointOutOfProcBase.cs | 584 + src/Shared/NodeEngineShutdownReason.cs | 40 + src/Shared/NodePacketFactory.cs | 121 + src/Shared/NodePacketTranslator.cs | 1024 + src/Shared/NodeShutdown.cs | 128 + src/Shared/OpportunisticIntern.cs | 1162 ++ src/Shared/OutOfProcTaskHostTaskResult.cs | 142 + src/Shared/Pair.cs | 65 + src/Shared/ProjectErrorUtilities.cs | 439 + src/Shared/ProjectFileErrorUtilities.cs | 153 + src/Shared/ProjectWriter.cs | 177 + src/Shared/PropertyParser.cs | 263 + src/Shared/QuotingUtilities.cs | 238 + src/Shared/README.txt | 1 + src/Shared/ReadOnlyCollection.cs | 201 + src/Shared/ReadOnlyEmptyCollection.cs | 148 + src/Shared/ReadOnlyEmptyDictionary.cs | 290 + src/Shared/ReadOnlyEmptyList.cs | 196 + src/Shared/RegisteredTaskObjectCacheBase.cs | 180 + src/Shared/RegistryDelegates.cs | 35 + src/Shared/RegistryHelper.cs | 78 + src/Shared/ResourceUtilities.cs | 316 + src/Shared/ReuseableStringBuilder.cs | 342 + src/Shared/Shared Code.doc | Bin 0 -> 37376 bytes src/Shared/StringBuilderCache.cs | 84 + src/Shared/Strings.shared.resx | 271 + src/Shared/StrongNameHelpers.cs | 719 + src/Shared/TaskEngineAssemblyResolver.cs | 132 + src/Shared/TaskHostConfiguration.cs | 341 + src/Shared/TaskHostTaskCancelled.cs | 56 + src/Shared/TaskHostTaskComplete.cs | 235 + src/Shared/TaskLoader.cs | 176 + src/Shared/TaskLoggingHelper.cs | 1451 ++ src/Shared/TaskLoggingHelperExtension.cs | 141 + src/Shared/TaskParameter.cs | 752 + src/Shared/TaskParameterTypeVerifier.cs | 76 + src/Shared/TempFileUtilities.cs | 111 + src/Shared/ThreadPoolExtensions.cs | 62 + src/Shared/ThreadingUtilities.cs | 53 + src/Shared/ToolsetElement.cs | 562 + src/Shared/Tracing.cs | 158 + src/Shared/TypeLoader.cs | 393 + src/Shared/UnitTests/App.config | 51 + src/Shared/UnitTests/AssemblyNameEx_Tests.cs | 679 + .../UnitTests/BuildEventArgsExtension.cs | 449 + .../UnitTests/CopyOnWriteDictionary_Tests.cs | 252 + src/Shared/UnitTests/ErrorUtilities_Tests.cs | 101 + .../UnitTests/EscapingUtilities_Tests.cs | 96 + src/Shared/UnitTests/FileMatcher_Tests.cs | 1313 ++ src/Shared/UnitTests/FileUtilities_Tests.cs | 732 + .../UnitTests/HybridDictionary_Tests.cs | 331 + src/Shared/UnitTests/MockEngine.cs | 457 + src/Shared/UnitTests/MockLogger.cs | 502 + .../UnitTests/NativeMethodsShared_Tests.cs | 188 + src/Shared/UnitTests/ObjectModelHelpers.cs | 1339 ++ .../UnitTests/ResourceUtilities_Tests.cs | 86 + src/Shared/UnitTests/StreamHelpers.cs | 30 + src/Shared/UnitTests/TaskParameter_Tests.cs | 428 + src/Shared/UnitTests/TypeLoader_Tests.cs | 151 + src/Shared/UnitTests/XmakeAttributes_Tests.cs | 125 + src/Shared/UnitTests/XmlUtilities_Tests.cs | 65 + src/Shared/VersionUtilities.cs | 129 + src/Shared/VisualStudioConstants.cs | 33 + src/Shared/XMakeAttributes.cs | 459 + src/Shared/XMakeElements.cs | 85 + src/Shared/XamlUtilities.cs | 435 + src/Shared/XmlUtilities.cs | 244 + src/Utilities/ApiContract.cs | 106 + src/Utilities/AppDomainIsolatedTask.cs | 176 + src/Utilities/AssemblyFoldersExInfo.cs | 80 + src/Utilities/AssemblyInfo.cs | 21 + src/Utilities/AssemblyResources.cs | 103 + src/Utilities/CommandLineBuilder.cs | 774 + src/Utilities/ExtensionSDK.cs | 198 + .../Microsoft.Build.Utilities.Suppressions.cs | 24 + src/Utilities/Logger.cs | 111 + .../Microsoft.Build.Utilities.csproj | 233 + src/Utilities/MuxLogger.cs | 1315 ++ src/Utilities/PlatformManifest.cs | 264 + src/Utilities/ProcessorArchitecture.cs | 138 + src/Utilities/SDKManifest.cs | 771 + src/Utilities/SDKType.cs | 35 + src/Utilities/Strings.resx | 300 + src/Utilities/TargetPlatformSDK.cs | 239 + src/Utilities/Task.cs | 204 + src/Utilities/TaskItem.cs | 495 + src/Utilities/ToolLocationHelper.cs | 3614 ++++ src/Utilities/ToolTask.cs | 1806 ++ .../CanonicalTrackedFilesHelper.cs | 120 + .../CanonicalTrackedInputFiles.cs | 1168 ++ .../CanonicalTrackedOutputFiles.cs | 889 + .../DependencyTableCache.cs | 307 + .../TrackedDependencies/FileTracker.cs | 932 + .../TrackedDependencies/FlatTrackingData.cs | 973 + .../TrackedDependencies/NativeMethods.cs | 56 + .../TrackedDependencies.cs | 104 + src/Utilities/UnitTests/ApiContract_Tests.cs | 59 + .../UnitTests/CanonicalError_Tests.cs | 398 + .../UnitTests/CommandLineBuilder_Tests.cs | 443 + .../UnitTests/EventArgsFormatting_Tests.cs | 205 + .../GetTargetPlatformReferences_Tests.cs | 288 + src/Utilities/UnitTests/Logger_Tests.cs | 111 + ...Microsoft.Build.Utilities.UnitTests.csproj | 83 + src/Utilities/UnitTests/MockEngine.cs | 269 + src/Utilities/UnitTests/MockLogger.cs | 353 + src/Utilities/UnitTests/MockTask.cs | 42 + src/Utilities/UnitTests/MuxLogger_Tests.cs | 428 + .../UnitTests/PlatformManifest_Tests.cs | 307 + .../UnitTests/ProcessorArchitecture_Tests.cs | 115 + src/Utilities/UnitTests/TaskItem_Tests.cs | 462 + .../UnitTests/TaskLoggingHelper_Tests.cs | 297 + .../UnitTests/ToolLocationHelper_Tests.cs | 4363 ++++ src/Utilities/UnitTests/ToolTask_Tests.cs | 592 + .../TrackedDependencies/FileTrackerTests.cs | 2626 +++ .../TrackedDependencies/MockEngine.cs | 235 + .../TrackedDependencies/TestFiles/one.cpp | 1 + .../TrackedDependencies/TestFiles/one.h | 1 + .../TrackedDependencies/TestFiles/one.tst | 1 + .../TrackedDependencies/TestFiles/one1.h | 1 + .../TrackedDependencies/TestFiles/one2.h | 1 + .../TrackedDependencies/TestFiles/one3.h | 1 + .../TrackedDependencies/TestFiles/two.cpp | 1 + .../TrackedDependencies/TestFiles/two1.h | 1 + .../TrackedDependencies/TestFiles/two2.h | 1 + .../TrackedDependencies/TestFiles/two3.h | 1 + .../TrackedDependenciesTests.cs | 3729 ++++ src/Utilities/UnitTests/strings.resx | 60 + src/Utilities/native.rc | 5 + src/XMakeBuildEngine/AllEngine/allengine.proj | 19 + .../allengineandconversion.proj | 22 + .../ApexTests/ApexTests.csproj | 79 + .../ApexTests/AssemblyInfo.cs | 18 + .../BuildManagerContainerConfiguration.cs | 203 + .../BuildManagerContainerGenerator.cs | 145 + .../BuildManager/BuildManagerTestExtension.cs | 440 + .../BuildManager/BuildManagerVerifier.cs | 24 + .../BuildRequestConfigurationTestExtension.cs | 63 + .../BuildRequestConfigurationVerifier.cs | 91 + .../BuildResult/BuildResultTestExtension.cs | 42 + .../BuildResult/BuildResultVerifier.cs | 59 + .../BuildSubmissionTestExtension.cs | 111 + .../BuildSubmission/BuildSubmissionTests.cs | 425 + .../BuildSubmissionVerifier.cs | 68 + .../ConfigurationCacheTestExtension.cs | 68 + .../ConfigurationCacheVerifier.cs | 47 + .../Helpers/AsyncBuildRequestStatus.cs | 93 + .../LifeTimeManagmentServiceTestExtension.cs | 55 + .../LifeTimeManagmentServiceVerifier.cs | 22 + .../ApexTests/Helpers/SimpleTaskHelper.cs | 124 + .../ApexTests/Helpers/TestExtensionHelper.cs | 47 + .../ResultsCache/ResultsCacheTestExtension.cs | 54 + .../ResultsCache/ResultsCacheVerifier.cs | 33 + src/XMakeBuildEngine/AssemblyInfo.cs | 35 + .../BackEnd/BuildManager/BuildManager.cs | 1911 ++ .../BackEnd/BuildManager/BuildParameters.cs | 1086 + .../BackEnd/BuildManager/BuildRequestData.cs | 274 + .../BackEnd/BuildManager/BuildSubmission.cs | 275 + .../BuildManager/LegacyThreadingData.cs | 210 + .../BuildComponentFactoryCollection.cs | 248 + .../BuildRequestConfigurationResponse.cs | 116 + .../BuildRequestEngine/BuildRequestEngine.cs | 1413 ++ .../BuildRequestEngine/BuildRequestEntry.cs | 633 + .../FullyQualifiedBuildRequest.cs | 201 + .../BuildRequestEngine/IBuildRequestEngine.cs | 176 + .../BackEnd/Components/Caching/ConfigCache.cs | 362 + .../Components/Caching/IConfigCache.cs | 100 + .../Components/Caching/IPropertyCache.cs | 19 + .../Caching/IRegisteredTaskObjectCache.cs | 41 + .../Components/Caching/IResultsCache.cs | 76 + .../Caching/RegisteredTaskObjectCache.cs | 79 + .../Components/Caching/ResultsCache.cs | 357 + .../Caching/ResultsCacheResponse.cs | 63 + .../Components/Communications/INodeManager.cs | 59 + .../Communications/INodeProvider.cs | 93 + .../Communications/LogMessagePacket.cs | 79 + .../Communications/NodeEndpointInProc.cs | 497 + .../Communications/NodeEndpointOutOfProc.cs | 104 + .../NodeFailedToLaunchException.cs | 87 + .../Components/Communications/NodeInfo.cs | 108 + .../Components/Communications/NodeManager.cs | 370 + .../NodePacketTranslatorExtensions.cs | 70 + .../Communications/NodeProviderInProc.cs | 455 + .../Communications/NodeProviderOutOfProc.cs | 230 + .../NodeProviderOutOfProcBase.cs | 752 + .../NodeProviderOutOfProcTaskHost.cs | 628 + .../Communications/TaskHostNodeManager.cs | 190 + .../BackEnd/Components/IBuildComponent.cs | 32 + .../BackEnd/Components/IBuildComponentHost.cs | 195 + .../Components/Logging/BaseLoggingContext.cs | 312 + .../Logging/BuildEventArgTransportSink.cs | 127 + .../Logging/CentralForwardingLogger.cs | 124 + .../Logging/EventRedirectorToSink.cs | 65 + .../Components/Logging/EventSourceSink.cs | 846 + .../Logging/ForwardingLoggerRecord.cs | 54 + .../Components/Logging/ILoggingService.cs | 457 + .../Components/Logging/LoggingService.cs | 1363 ++ .../Logging/LoggingServiceFactory.cs | 62 + .../Logging/LoggingServiceLogMethods.cs | 812 + .../Components/Logging/NodeLoggingContext.cs | 110 + .../Logging/ProjectLoggingContext.cs | 272 + .../Logging/TargetLoggingContext.cs | 173 + .../Components/Logging/TaskLoggingContext.cs | 145 + .../RequestBuilder/BatchingEngine.cs | 432 + .../Components/RequestBuilder/FullTracking.cs | 166 + .../RequestBuilder/IRequestBuilder.cs | 90 + .../RequestBuilder/IRequestBuilderCallback.cs | 63 + .../RequestBuilder/ITargetBuilder.cs | 35 + .../RequestBuilder/ITargetBuilderCallback.cs | 42 + .../Components/RequestBuilder/ITaskBuilder.cs | 58 + .../RequestBuilder/IntrinsicTask.cs | 113 + .../IntrinsicTasks/CallTarget.cs | 206 + .../IntrinsicTasks/IntrinsicTaskFactory.cs | 102 + .../IntrinsicTasks/ItemGroupIntrinsicTask.cs | 631 + .../IntrinsicTasks/ItemGroupLoggingHelper.cs | 150 + .../RequestBuilder/IntrinsicTasks/MSBuild.cs | 929 + .../PropertyGroupIntrinsicTask.cs | 123 + .../Components/RequestBuilder/ItemBucket.cs | 207 + .../Components/RequestBuilder/Lookup.cs | 1513 ++ .../RequestBuilder/RequestBuilder.cs | 1231 ++ .../RequestBuilder/TargetBuilder.cs | 732 + .../Components/RequestBuilder/TargetEntry.cs | 921 + .../RequestBuilder/TargetSpecification.cs | 55 + .../RequestBuilder/TargetUpToDateChecker.cs | 1413 ++ .../Components/RequestBuilder/TaskBuilder.cs | 1119 ++ .../Components/RequestBuilder/TaskHost.cs | 898 + .../Components/Scheduler/IScheduler.cs | 77 + .../Scheduler/SchedulableRequest.cs | 693 + .../Components/Scheduler/ScheduleResponse.cs | 268 + .../Scheduler/ScheduleTimeRecord.cs | 72 + .../BackEnd/Components/Scheduler/Scheduler.cs | 2419 +++ .../SchedulerCircularDependencyException.cs | 57 + .../Components/Scheduler/SchedulingData.cs | 778 + .../Components/Scheduler/SchedulingPlan.cs | 733 + src/XMakeBuildEngine/BackEnd/Node/INode.cs | 38 + .../BackEnd/Node/InProcNode.cs | 549 + .../BackEnd/Node/NativeMethods.cs | 234 + .../BackEnd/Node/NodeConfiguration.cs | 166 + .../BackEnd/Node/OutOfProcNode.cs | 796 + .../BackEnd/Shared/BuildAbortedException.cs | 102 + .../BackEnd/Shared/BuildRequest.cs | 354 + .../BackEnd/Shared/BuildRequestBlocker.cs | 242 + .../Shared/BuildRequestConfiguration.cs | 1012 + .../BackEnd/Shared/BuildRequestUnblocker.cs | 130 + .../BackEnd/Shared/BuildResult.cs | 670 + .../Shared/CircularDependencyException.cs | 42 + .../BackEnd/Shared/ConfigurationMetadata.cs | 151 + .../BackEnd/Shared/IBuildResults.cs | 79 + .../BackEnd/Shared/ITargetResult.cs | 69 + .../BackEnd/Shared/TargetResult.cs | 577 + .../BackEnd/Shared/WorkUnitResult.cs | 190 + .../AddInParts/ITaskExecutionHost.cs | 112 + .../TaskExecutionHost/TaskExecutionHost.cs | 1649 ++ .../Collections/ConcurrentQueueExtensions.cs | 30 + .../Collections/ConcurrentStackExtensions.cs | 41 + .../Collections/ConvertingEnumerable.cs | 133 + .../Collections/CopyOnReadEnumerable.cs | 107 + .../CopyOnWritePropertyDictionary.cs | 537 + .../Collections/FilteringEnumerable.cs | 140 + .../Collections/HashTableUtility.cs | 62 + .../Collections/IDeepCloneable.cs | 23 + .../Collections/IImmutable.cs | 19 + src/XMakeBuildEngine/Collections/IValued.cs | 26 + .../Collections/ItemDictionary.cs | 522 + .../Collections/LookasideStringInterner.cs | 131 + .../Collections/MultiDictionary.cs | 329 + .../OrdinalIgnoreCaseKeyedComparer.cs | 61 + .../Collections/OrdinalKeyedComparer.cs | 61 + .../Collections/PropertyDictionary.cs | 546 + .../ReadOnlyConvertingCollection.cs | 161 + .../ReadOnlyConvertingDictionary.cs | 225 + .../RetrievableEntryHashSet/BitHelper.cs | 153 + .../RetrievableEntryHashSet/HashHelpers.cs | 99 + .../RetrievableEntryHashSet/HashSet.cs | 1822 ++ .../HashSetDebugView.cs | 39 + .../Originals/BitHelper.cs | 138 + .../Originals/HashHelpers.cs | 86 + .../Originals/HashSet.cs | 1461 ++ .../Originals/HashSetDebugView.cs | 32 + src/XMakeBuildEngine/Collections/Triple.cs | 70 + .../Collections/WeakDictionary.cs | 307 + .../Collections/WeakReference.cs | 66 + .../Collections/WeakValueDictionary.cs | 253 + .../Construction/ProjectChooseElement.cs | 141 + .../Construction/ProjectElement.cs | 499 + .../Construction/ProjectElementContainer.cs | 647 + .../Construction/ProjectExtensionsElement.cs | 201 + .../Construction/ProjectImportElement.cs | 99 + .../Construction/ProjectImportGroupElement.cs | 108 + .../ProjectItemDefinitionElement.cs | 114 + .../ProjectItemDefinitionGroupElement.cs | 96 + .../Construction/ProjectItemElement.cs | 435 + .../Construction/ProjectItemGroupElement.cs | 190 + .../Construction/ProjectMetadataElement.cs | 119 + .../Construction/ProjectOnErrorElement.cs | 104 + .../Construction/ProjectOtherwiseElement.cs | 141 + .../Construction/ProjectOutputElement.cs | 198 + .../Construction/ProjectPropertyElement.cs | 129 + .../ProjectPropertyGroupElement.cs | 135 + .../Construction/ProjectRootElement.cs | 2032 ++ .../Construction/ProjectTargetElement.cs | 493 + .../Construction/ProjectTaskElement.cs | 433 + .../ProjectUsingTaskBodyElement.cs | 173 + .../Construction/ProjectUsingTaskElement.cs | 299 + .../ProjectUsingTaskParameterElement.cs | 222 + .../Construction/ProjectWhenElement.cs | 117 + .../ProjectConfigurationInSolution.cs | 98 + .../Solution/ProjectInSolution.cs | 460 + .../SolutionConfigurationInSolution.cs | 90 + .../Construction/Solution/SolutionFile.cs | 1601 ++ .../Solution/SolutionProjectGenerator.cs | 2281 +++ .../UsingTaskParameterGroupElement.cs | 153 + .../Debugger/DebuggerLocalType.cs | 60 + .../Debugger/DebuggerManager.cs | 908 + .../Definition/BuiltInMetadata.cs | 126 + src/XMakeBuildEngine/Definition/Project.cs | 3145 +++ .../Definition/ProjectCollection.cs | 2416 +++ .../ProjectCollectionChangedEventArgs.cs | 84 + .../Definition/ProjectItem.cs | 1058 + .../Definition/ProjectItemDefinition.cs | 251 + .../Definition/ProjectMetadata.cs | 290 + .../Definition/ProjectProperty.cs | 629 + .../Definition/ResolvedImport.cs | 80 + src/XMakeBuildEngine/Definition/SubToolset.cs | 101 + src/XMakeBuildEngine/Definition/Toolset.cs | 1072 + .../Definition/ToolsetConfigurationReader.cs | 223 + .../Definition/ToolsetPropertyDefinition.cs | 99 + .../Definition/ToolsetReader.cs | 533 + .../Definition/ToolsetRegistryReader.cs | 354 + .../ElementLocation/ElementLocation.cs | 380 + .../ElementLocation/RegistryLocation.cs | 102 + .../XmlAttributeWithLocation.cs | 98 + .../XmlDocumentWithLocation.cs | 404 + .../ElementLocation/XmlElementWithLocation.cs | 155 + .../ElementLocation/XmlNameTableThreadSafe.cs | 76 + .../Errors/InternalLoggerException.cs | 242 + .../Errors/InvalidProjectFileException.cs | 376 + .../InvalidToolsetDefinitionException.cs | 164 + .../Errors/RegistryException.cs | 79 + .../Evaluation/ConditionEvaluator.cs | 458 + .../Conditionals/AndExpressionNode.cs | 60 + .../Conditionals/CharacterUtilities.cs | 37 + .../Conditionals/EqualExpressionNode.cs | 42 + .../FunctionCallExpressionNode.cs | 170 + .../Conditionals/GenericExpressionNode.cs | 100 + .../Conditionals/GreaterThanExpressionNode.cs | 65 + .../GreaterThanOrEqualExpressionNode.cs | 65 + .../Evaluation/Conditionals/IItem.cs | 37 + .../Conditionals/LessThanExpressionNode.cs | 65 + .../LessThanOrEqualExpressionNode.cs | 65 + .../MultipleComparisonExpressionNode.cs | 127 + .../Conditionals/NotEqualExpressionNode.cs | 42 + .../Conditionals/NotExpressionNode.cs | 48 + .../NumericComparisonExpressionNode.cs | 86 + .../Conditionals/NumericExpressionNode.cs | 107 + .../Conditionals/OperandExpressionNode.cs | 32 + .../Conditionals/OperatorExpressionNode.cs | 163 + .../Conditionals/OrExpressionNode.cs | 60 + .../Evaluation/Conditionals/Parser.cs | 405 + .../Evaluation/Conditionals/Scanner.cs | 716 + .../Conditionals/StringExpressionNode.cs | 145 + .../Evaluation/Conditionals/Token.cs | 159 + src/XMakeBuildEngine/Evaluation/Evaluator.cs | 2252 +++ .../Evaluation/EvaluatorMetadataTable.cs | 162 + src/XMakeBuildEngine/Evaluation/Expander.cs | 3553 ++++ .../Evaluation/ExpressionShredder.cs | 946 + .../Evaluation/IEvaluatorData.cs | 294 + src/XMakeBuildEngine/Evaluation/IItem.cs | 62 + .../Evaluation/IItemDefinition.cs | 32 + .../Evaluation/IItemFactory.cs | 77 + .../Evaluation/IItemProvider.cs | 30 + .../Evaluation/IMetadataTable.cs | 40 + src/XMakeBuildEngine/Evaluation/IMetadatum.cs | 22 + .../Evaluation/IProjectMetadataParent.cs | 32 + src/XMakeBuildEngine/Evaluation/IProperty.cs | 44 + .../Evaluation/IPropertyProvider.cs | 33 + .../Evaluation/IToolsetProvider.cs | 35 + .../Evaluation/IntrinsicFunctions.cs | 431 + .../Evaluation/ItemsAndMetadataPair.cs | 78 + .../Evaluation/MetadataReference.cs | 49 + .../Evaluation/Preprocessor.cs | 230 + .../Evaluation/ProjectChangedEventArgs.cs | 38 + .../Evaluation/ProjectParser.cs | 846 + .../Evaluation/ProjectRootElementCache.cs | 643 + .../Evaluation/ProjectStringCache.cs | 302 + .../Evaluation/ProjectXmlChangedEventArgs.cs | 61 + .../Evaluation/StringMetadataTable.cs | 86 + .../Evaluation/ToolsetProvider.cs | 121 + .../Microsoft.Build.Suppressions.cs | 27 + src/XMakeBuildEngine/Instance/HostServices.cs | 325 + .../Instance/ProjectInstance.cs | 2399 +++ .../Instance/ProjectItemDefinitionInstance.cs | 224 + .../Instance/ProjectItemGroupTaskInstance.cs | 134 + .../ProjectItemGroupTaskItemInstance.cs | 379 + .../ProjectItemGroupTaskMetadataInstance.cs | 137 + .../Instance/ProjectItemInstance.cs | 2143 ++ .../Instance/ProjectMetadataInstance.cs | 236 + .../Instance/ProjectOnErrorInstance.cs | 115 + .../ProjectPropertyGroupTaskInstance.cs | 134 + ...rojectPropertyGroupTaskPropertyInstance.cs | 135 + .../Instance/ProjectPropertyInstance.cs | 377 + .../Instance/ProjectTargetInstance.cs | 513 + .../Instance/ProjectTargetInstanceChild.cs | 56 + .../Instance/ProjectTaskInstance.cs | 327 + .../Instance/ProjectTaskInstanceChild.cs | 52 + .../Instance/ProjectTaskOutputItemInstance.cs | 136 + .../ProjectTaskOutputPropertyInstance.cs | 133 + .../Instance/ReflectableTaskPropertyInfo.cs | 76 + .../TaskFactories/AssemblyTaskFactory.cs | 653 + .../AssemblyTaskFactoryInstance.cs | 213 + .../Instance/TaskFactories/TaskHostTask.cs | 595 + .../Instance/TaskFactoryLoggingHost.cs | 351 + .../Instance/TaskFactoryWrapper.cs | 308 + src/XMakeBuildEngine/Instance/TaskRegistry.cs | 1632 ++ .../Logging/BaseConsoleLogger.cs | 1277 ++ src/XMakeBuildEngine/Logging/ConsoleLogger.cs | 542 + .../ConfigurableForwardingLogger.cs | 541 + .../DistributedFileLogger.cs | 219 + src/XMakeBuildEngine/Logging/FileLogger.cs | 264 + src/XMakeBuildEngine/Logging/LogFormatter.cs | 60 + .../Logging/LoggerDescription.cs | 420 + .../Logging/NullCentralLogger.cs | 74 + .../ParallelLogger/ParallelConsoleLogger.cs | 1709 ++ .../ParallelLogger/ParallelLoggerHelpers.cs | 709 + .../Logging/SerialConsoleLogger.cs | 970 + src/XMakeBuildEngine/Microsoft.Build.csproj | 664 + .../Resources/AssemblyResources.cs | 120 + src/XMakeBuildEngine/Resources/Constants.cs | 350 + src/XMakeBuildEngine/Resources/Strings.resx | 1559 ++ .../BackEnd/AssemblyTaskFactory_Tests.cs | 697 + .../UnitTests/BackEnd/BatchingEngine_Tests.cs | 472 + .../BuildEventArgTransportSink_Tests.cs | 156 + .../UnitTests/BackEnd/BuildManager_Tests.cs | 3632 ++++ ...BuildRequestConfigurationResponse_Tests.cs | 107 + .../BuildRequestConfiguration_Tests.cs | 421 + .../BackEnd/BuildRequestEngine_Tests.cs | 612 + .../BackEnd/BuildRequestEntry_Tests.cs | 293 + .../UnitTests/BackEnd/BuildRequest_Tests.cs | 148 + .../UnitTests/BackEnd/BuildResult_Tests.cs | 363 + .../BackEnd/CentralForwardingLogger_Tests.cs | 188 + .../BackEnd/ConfigurationMetadata_Tests.cs | 158 + .../UnitTests/BackEnd/CustomTaskHelper.cs | 79 + .../BackEnd/EventRedirectorToSink_Tests.cs | 97 + .../BackEnd/EventSourceSink_Tests.cs | 984 + .../FullyQualifiedBuildRequest_Tests.cs | 72 + .../UnitTests/BackEnd/ITestTaskHost.cs | 30 + .../UnitTests/BackEnd/IntegrationTests.cs | 10 + .../UnitTests/BackEnd/IntrinsicTask_Tests.cs | 3538 ++++ .../UnitTests/BackEnd/LoggingContext_Tests.cs | 59 + .../BackEnd/LoggingServiceFactory_Tests.cs | 44 + .../UnitTests/BackEnd/LoggingService_Tests.cs | 1141 ++ .../BackEnd/LoggingServicesLogMethod_Tests.cs | 1655 ++ .../UnitTests/BackEnd/Lookup_Tests.cs | 1460 ++ .../UnitTests/BackEnd/MSBuild_Tests.cs | 1658 ++ .../UnitTests/BackEnd/MockHost.cs | 228 + .../UnitTests/BackEnd/MockLoggingService.cs | 470 + .../UnitTests/BackEnd/MockTaskBuilder.cs | 162 + .../BackEnd/NodeEndpointInProc_Tests.cs | 463 + .../BackEnd/NodePacketTranslator_Tests.cs | 705 + .../UnitTests/BackEnd/NodePackets_Tests.cs | 314 + .../UnitTests/BackEnd/OnError_Tests.cs | 790 + .../UnitTests/BackEnd/RequestBuilder_Tests.cs | 416 + .../UnitTests/BackEnd/ResultsCache_Tests.cs | 278 + .../UnitTests/BackEnd/Scheduler_Tests.cs | 804 + .../UnitTests/BackEnd/TargetBuilder_Tests.cs | 1609 ++ .../UnitTests/BackEnd/TargetEntry_Tests.cs | 1336 ++ .../UnitTests/BackEnd/TargetResult_Tests.cs | 155 + .../BackEnd/TargetUpToDateChecker_Tests.cs | 912 + .../UnitTests/BackEnd/TaskBuilderTestTask.cs | 582 + .../UnitTests/BackEnd/TaskBuilder_Tests.cs | 1346 ++ .../BackEnd/TaskExecutionHost_Tests.cs | 1470 ++ .../BackEnd/TaskHostConfiguration_Tests.cs | 281 + .../BackEnd/TaskHostTaskCancelled_Tests.cs | 47 + .../BackEnd/TaskHostTaskComplete_Tests.cs | 189 + .../UnitTests/BackEnd/TaskHost_Tests.cs | 1306 ++ .../UnitTests/BackEnd/TaskItemComparer.cs | 85 + .../UnitTests/BackEnd/TaskRegistry_Tests.cs | 2271 +++ .../UnitTests/BackEnd/TranslationHelpers.cs | 114 + .../Collections/CopyOnReadEnumerable_Tests.cs | 80 + .../Collections/FilteringEnumerable_Tests.cs | 69 + .../LookasideStringInterner_Tests.cs | 112 + .../MSBuildNameIgnoreCaseComparer_Tests.cs | 370 + .../Collections/MultiDictionary_Tests.cs | 176 + .../Collections/OMcollections_tests.cs | 379 + .../Collections/WeakDictionary_Tests.cs | 296 + .../Collections/WeakValueDictionary_Tests.cs | 278 + .../ConfigureableForwardingLogger_Tests.cs | 243 + .../UnitTests/ConsoleLogger_Tests.cs | 2589 +++ .../Construction/ElementLocation_Tests.cs | 415 + .../Construction/SolutionFile_Tests.cs | 1748 ++ .../SolutionProjectGenerator_Tests.cs | 1989 ++ .../XmlReaderWithoutLocation_Tests.cs | 182 + .../Definition/ItemDefinitionGroup_Tests.cs | 1860 ++ .../UnitTests/Definition/ProjectHelpers.cs | 43 + .../UnitTests/Definition/ProjectItem_Tests.cs | 105 + .../Definition/Project_Internal_Tests.cs | 191 + .../Definition/ToolsVersion_Tests.cs | 1000 + .../ToolsetConfigurationReaderTestHelper.cs | 88 + .../ToolsetConfigurationReader_Tests.cs | 485 + .../Definition/ToolsetReader_Tests.cs | 2701 +++ .../Definition/ToolsetRegistryReader_Tests.cs | 597 + .../UnitTests/Definition/Toolset_Tests.cs | 520 + .../UnitTests/DualQueue_Tests.cs | 99 + .../UnitTests/EscapingInProjects_Tests.cs | 1876 ++ .../UnitTests/Evaluation/Evaluator_Tests.cs | 4165 ++++ .../UnitTests/Evaluation/Expander_Tests.cs | 3131 +++ .../Evaluation/ExpressionShredder_Tests.cs | 1297 ++ .../Evaluation/Preprocessor_Tests.cs | 833 + .../ProjectRootElementCache_Tests.cs | 224 + .../Evaluation/ProjectStringCache_Tests.cs | 433 + .../UnitTests/EventArgsFormatting_Tests.cs | 143 + .../UnitTests/ExpressionTree_Tests.cs | 1003 + .../UnitTests/FileLogger_Tests.cs | 447 + .../UnitTests/HashTableUtility_Tests.cs | 73 + .../UnitTests/Instance/HostServices_Tests.cs | 447 + .../ProjectInstance_Internal_Tests.cs | 642 + .../ProjectMetadataInstance_Internal_Tests.cs | 67 + .../ProjectPropertyInstance_Internal_Tests.cs | 106 + .../UnitTests/Instance/TaskItem_Tests.cs | 298 + .../InvalidProjectFileException_Tests.cs | 89 + .../UnitTests/LogFormatter_Tests.cs | 69 + .../UnitTests/LoggerDescription_Tests.cs | 55 + .../UnitTests/LoggerException_Tests.cs | 56 + .../Microsoft.Build.Engine.UnitTests.csproj | 215 + .../UnitTests/MockElementLocation.cs | 65 + src/XMakeBuildEngine/UnitTests/MockTask.cs | 480 + .../UnitTests/OpportunisticIntern_Tests.cs | 169 + .../UnitTests/Parser_Tests.cs | 520 + .../QaTests/BuildRequestEngine_Tests.cs | 310 + .../UnitTests/QaTests/ITestDataProvider.cs | 60 + .../UnitTests/QaTests/Integration_Tests.cs | 297 + .../UnitTests/QaTests/MockHost.cs | 450 + .../UnitTests/QaTests/MockLoggingService.cs | 448 + .../UnitTests/QaTests/MockRequestBuilder.cs | 321 + .../UnitTests/QaTests/MockResultsCache.cs | 177 + .../UnitTests/QaTests/MockTargetBuilder.cs | 206 + .../UnitTests/QaTests/MockTaskBuilder.cs | 1 + .../UnitTests/QaTests/ProjectDefinition.cs | 301 + .../UnitTests/QaTests/RequestBuilder_Tests.cs | 421 + .../UnitTests/QaTests/RequestDefinition.cs | 757 + .../UnitTests/QaTests/TargetBuilder_Tests.cs | 816 + .../UnitTests/QaTests/TargetDefinition.cs | 245 + .../UnitTests/QaTests/TaskBuilder_Tests.cs | 260 + .../UnitTests/QaTests/TaskDefinition.cs | 288 + .../UnitTests/QaTests/TestDataProvider.cs | 539 + .../UnitTests/QaTests/common_Tests.cs | 678 + .../UnitTests/Scanner_Tests.cs | 559 + .../UnitTests/TargetsFile_Test.cs | 2760 +++ .../UnitTests/TestUtilities.cs | 55 + .../UnitTests/Utilities_Tests.cs | 368 + .../UnitTestsPublicOM/App.config | 28 + .../UnitTestsPublicOM/AssemblyResources.cs | 18 + .../Construction/ConstructionEditing_Tests.cs | 2661 +++ .../ElementLocationPublic_Tests.cs | 214 + .../ProjectChooseElement_Tests.cs | 284 + .../Construction/ProjectElement_Tests.cs | 66 + .../ProjectExtensionsElement_Tests.cs | 217 + .../ProjectImportElement_Tests.cs | 285 + .../ProjectImportGroupElement_Tests.cs | 397 + .../ProjectItemDefinitionElement_Tests.cs | 146 + ...ProjectItemDefinitionGroupElement_Tests.cs | 137 + .../Construction/ProjectItemElement_Tests.cs | 662 + .../ProjectItemGroupElement_tests.cs | 116 + .../ProjectMetadataElement_Tests.cs | 255 + .../ProjectOnErrorElement_Tests.cs | 307 + .../ProjectOutputElement_Tests.cs | 314 + .../ProjectPropertyElement_Tests.cs | 281 + .../ProjectPropertyGroupElement_Tests.cs | 131 + .../Construction/ProjectRootElement_Tests.cs | 1264 ++ .../ProjectTargetElement_Tests.cs | 351 + .../Construction/ProjectTaskElement_Tests.cs | 324 + .../ProjectUsingTaskElement_Tests.cs | 523 + .../Construction/SolutionFile_Tests.cs | 944 + .../UsingTaskBodyElement_Tests.cs | 150 + .../UsingTaskParameterElement_Tests.cs | 242 + .../UsingTaskParameterGroup_Tests.cs | 150 + .../Definition/DefinitionEditing_Tests.cs | 2342 +++ ...gElementsReferencedByOrReferences_Tests.cs | 352 + .../Definition/ProjectCollection_Tests.cs | 1443 ++ .../Definition/ProjectItemDefinition_Tests.cs | 648 + .../Definition/ProjectItem_Tests.cs | 1499 ++ .../Definition/ProjectMetadata_Tests.cs | 525 + .../Definition/ProjectProperty_Tests.cs | 289 + .../Definition/Project_Tests.cs | 2454 +++ .../Definition/ProtectImports_Tests.cs | 663 + .../Instance/ProjectInstance_Tests.cs | 973 + .../Instance/ProjectItemInstance_Tests.cs | 870 + .../Instance/ProjectMetadataInstance_Tests.cs | 47 + .../Instance/ProjectOnErrorInstance_Tests.cs | 57 + .../Instance/ProjectPropertyInstance_Tests.cs | 114 + .../Instance/ProjectTargetInstance_Tests.cs | 156 + .../Instance/ProjectTaskInstance_Tests.cs | 86 + .../ProjectTaskOutputItemInstance_Tests.cs | 61 + ...ProjectTaskOutputPropertyInstance_Tests.cs | 61 + .../LazyFormattedEventArgs_Tests.cs | 114 + ...Microsoft.Build.Engine.OM.UnitTests.csproj | 195 + .../Utilities/EngineFileUtilities.cs | 140 + .../Utilities/RegistryKeyWrapper.cs | 273 + src/XMakeBuildEngine/Utilities/Utilities.cs | 583 + .../Xml/ProjectXmlUtilities.cs | 254 + .../allmajorprojects/allmajorprojects.proj | 25 + src/XMakeBuildEngine/native.rc | 5 + src/XMakeBuildEngine/packages.config | 4 + src/XMakeCommandLine/AssemblyInfo.cs | 19 + src/XMakeCommandLine/AssemblyResources.cs | 41 + .../CommandLineSwitchException.cs | 153 + src/XMakeCommandLine/CommandLineSwitches.cs | 921 + .../DistributedLoggerRecord.cs | 57 + .../FxCopExclusions/MsBuild.Suppressions.cs | 48 + .../InitializationException.cs | 177 + src/XMakeCommandLine/LogMessagePacket.cs | 54 + src/XMakeCommandLine/MSBuild.csproj | 202 + src/XMakeCommandLine/MSBuild.exe.manifest | 28 + src/XMakeCommandLine/MSBuild.ico | Bin 0 -> 10134 bytes .../MSBuildTaskHost/AssemblyResources.cs | 38 + .../MSBuildTaskHost.Suppressions.cs | 190 + .../MSBuildTaskHost/MSBuildTaskHost.csproj | 240 + .../MSBuildTaskHost/OutOfProcTaskHost.cs | 128 + .../MSBuildTaskHost/TypeLoader.cs | 392 + .../MSBuildTaskHost/app.config | 24 + .../MSBuildTaskHost/MSBuildTaskHost/native.rc | 5 + .../MSBuildTaskHost/dirs.proj | 18 + .../Microsoft.Build.CommonTypes.xsd | 5058 +++++ src/XMakeCommandLine/Microsoft.Build.Core.xsd | 722 + src/XMakeCommandLine/Microsoft.Build.xsd | 50 + .../NodeEndpointOutOfProcTaskHost.cs | 52 + .../OutOfProcTaskAppDomainWrapper.cs | 50 + .../OutOfProcTaskAppDomainWrapperBase.cs | 414 + src/XMakeCommandLine/OutOfProcTaskHostNode.cs | 1117 ++ .../ProjectSchemaValidationHandler.cs | 304 + src/XMakeCommandLine/Strings.resx | 949 + .../CommandLineSwitchException_Tests.cs | 61 + .../UnitTests/CommandLineSwitches_Tests.cs | 1163 ++ .../InitializationException_Tests.cs | 61 + ...crosoft.Build.CommandLine.UnitTests.csproj | 95 + .../ProjectSchemaValidationHandler_Tests.cs | 371 + src/XMakeCommandLine/UnitTests/XMake_Tests.cs | 1776 ++ src/XMakeCommandLine/XMake.cs | 2998 +++ src/XMakeCommandLine/app.config | 48 + .../clr2/OutOfProcTaskAppDomainWrapper.cs | 32 + src/XMakeCommandLine/msbuild.suitebin.config | 31 + src/XMakeCommandLine/msbuild_rascal.manifest | 9 + src/XMakeCommandLine/native.rc | 12 + src/XMakeCommandLine/xclpupdate.bat | 36 + src/XMakeTasks/Al.cs | 400 + src/XMakeTasks/AppConfig/AppConfig.cs | 92 + .../AppConfig/AppConfigException.cs | 75 + src/XMakeTasks/AppConfig/BindingRedirect.cs | 116 + src/XMakeTasks/AppConfig/DependentAssembly.cs | 137 + src/XMakeTasks/AppConfig/RuntimeSection.cs | 59 + .../AppDomainIsolatedTaskExtension.cs | 55 + src/XMakeTasks/AspNetCompiler.cs | 375 + .../AssemblyFoldersExResolver.cs | 369 + .../AssemblyFoldersResolver.cs | 82 + .../AssemblyDependency/AssemblyInformation.cs | 459 + .../AssemblyNameReference.cs | 57 + ...lyNameReferenceAscendingVersionComparer.cs | 50 + .../AssemblyDependency/AssemblyResolution.cs | 207 + .../AssemblyResolutionConstants.cs | 60 + .../BadImageReferenceException.cs | 46 + .../CandidateAssemblyFilesResolver.cs | 116 + .../AssemblyDependency/ConflictLossReason.cs | 48 + .../AssemblyDependency/CopyLocalState.cs | 113 + .../DependencyResolutionException.cs | 46 + .../AssemblyDependency/DirectoryResolver.cs | 73 + .../AssemblyDependency/DisposableBase.cs | 88 + .../FrameworkPathResolver.cs | 127 + .../AssemblyDependency/GacResolver.cs | 95 + .../GenerateBindingRedirects.cs | 369 + .../AssemblyDependency/GlobalAssemblyCache.cs | 383 + .../AssemblyDependency/HintPathResolver.cs | 73 + .../AssemblyDependency/InstalledAssemblies.cs | 168 + .../InvalidReferenceAssemblyNameException.cs | 56 + .../AssemblyDependency/NoMatchReason.cs | 58 + .../AssemblyDependency/RawFilenameResolver.cs | 89 + .../AssemblyDependency/Reference.cs | 1392 ++ .../ReferenceResolutionException.cs | 46 + .../AssemblyDependency/ReferenceTable.cs | 3073 +++ .../ResolutionSearchLocation.cs | 66 + .../ResolveAssemblyReference.cs | 2920 +++ src/XMakeTasks/AssemblyDependency/Resolver.cs | 409 + .../TaskItemSpecFilenameComparer.cs | 67 + .../AssemblyDependency/UnificationReason.cs | 37 + .../AssemblyDependency/UnificationVersion.cs | 19 + .../AssemblyDependency/UnifiedAssemblyName.cs | 69 + ...rorOnTargetArchitectureMismatchBehavior.cs | 44 + src/XMakeTasks/AssemblyFolder.cs | 161 + src/XMakeTasks/AssemblyInfo.cs | 24 + src/XMakeTasks/AssemblyRegistrationCache.cs | 77 + src/XMakeTasks/AssemblyRemapping.cs | 88 + src/XMakeTasks/AssemblyResources.cs | 67 + src/XMakeTasks/AssignCulture.cs | 220 + src/XMakeTasks/AssignLinkMetadata.cs | 87 + src/XMakeTasks/AssignProjectConfiguration.cs | 572 + src/XMakeTasks/AssignTargetPath.cs | 148 + src/XMakeTasks/AxImp.cs | 174 + src/XMakeTasks/AxReference.cs | 120 + src/XMakeTasks/AxTlbBaseReference.cs | 396 + src/XMakeTasks/AxTlbBaseTask.cs | 288 + .../BootstrapperUtil/BootstrapperBuilder.cs | 2220 +++ .../BootstrapperUtil/BuildMessage.cs | 88 + .../BootstrapperUtil/BuildResults.cs | 98 + .../BootstrapperUtil/BuildSettings.cs | 163 + src/XMakeTasks/BootstrapperUtil/Interfaces.cs | 435 + .../BootstrapperUtil/NativeMethods.cs | 21 + src/XMakeTasks/BootstrapperUtil/Package.cs | 63 + .../BootstrapperUtil/PackageCollection.cs | 60 + src/XMakeTasks/BootstrapperUtil/Product.cs | 262 + .../BootstrapperUtil/ProductBuilder.cs | 49 + .../ProductBuilderCollection.cs | 55 + .../BootstrapperUtil/ProductCollection.cs | 82 + .../BootstrapperUtil/ResourceUpdater.cs | 179 + src/XMakeTasks/BootstrapperUtil/Util.cs | 155 + .../productvalidationresults.cs | 49 + .../BootstrapperUtil/xmltoconfig.xsl | 72 + .../BootstrapperUtil/xmlvalidationresults.cs | 93 + src/XMakeTasks/BuildCacheDisposeWrapper.cs | 69 + src/XMakeTasks/CSharpParserUtilities.cs | 123 + src/XMakeTasks/CallTarget.cs | 160 + src/XMakeTasks/CodeTaskFactory.cs | 968 + src/XMakeTasks/CodeTaskFactoryInstance.cs | 154 + src/XMakeTasks/ComDependencyWalker.cs | 456 + src/XMakeTasks/ComReference.cs | 607 + src/XMakeTasks/ComReferenceInfo.cs | 245 + src/XMakeTasks/ComReferenceItemAttributes.cs | 21 + .../ComReferenceResolutionException.cs | 48 + src/XMakeTasks/ComReferenceTypes.cs | 53 + src/XMakeTasks/ComReferenceWrapperInfo.cs | 33 + src/XMakeTasks/CombinePath.cs | 117 + src/XMakeTasks/CommandLineBuilderExtension.cs | 310 + src/XMakeTasks/ConvertToAbsolutePath.cs | 102 + src/XMakeTasks/Copy.cs | 746 + .../CreateCSharpManifestResourceName.cs | 213 + src/XMakeTasks/CreateItem.cs | 250 + src/XMakeTasks/CreateManifestResourceName.cs | 392 + src/XMakeTasks/CreateProperty.cs | 92 + .../CreateVisualBasicManifestResourceName.cs | 208 + src/XMakeTasks/Csc.cs | 834 + src/XMakeTasks/Culture.cs | 100 + src/XMakeTasks/CultureStringUtilities.cs | 67 + .../DataDriven/DataDrivenToolTask.cs | 859 + src/XMakeTasks/DefaultTasks.bat | 23 + src/XMakeTasks/Delegate.cs | 130 + src/XMakeTasks/Delete.cs | 154 + src/XMakeTasks/Dependencies.cs | 70 + src/XMakeTasks/DependencyFile.cs | 108 + src/XMakeTasks/Error.cs | 106 + src/XMakeTasks/ErrorFromResources.cs | 159 + src/XMakeTasks/Exec.cs | 649 + src/XMakeTasks/ExtractedClassName.cs | 36 + src/XMakeTasks/FileIO/ReadLinesFromFile.cs | 111 + src/XMakeTasks/FileIO/WriteLinesToFile.cs | 168 + src/XMakeTasks/FileState.cs | 316 + src/XMakeTasks/FindAppConfigFile.cs | 161 + src/XMakeTasks/FindInList.cs | 166 + .../FindInvalidProjectReferences.cs | 162 + src/XMakeTasks/FormatUrl.cs | 43 + src/XMakeTasks/FormatVersion.cs | 91 + .../Microsoft.Build.Tasks.Suppressions.cs | 587 + src/XMakeTasks/GenerateApplicationManifest.cs | 482 + src/XMakeTasks/GenerateBootstrapper.cs | 266 + src/XMakeTasks/GenerateDeploymentManifest.cs | 293 + src/XMakeTasks/GenerateManifestBase.cs | 579 + src/XMakeTasks/GenerateResource.cs | 3775 ++++ src/XMakeTasks/GenerateTrustInfo.cs | 149 + src/XMakeTasks/GetAssemblyIdentity.cs | 94 + src/XMakeTasks/GetFrameworkPath.cs | 209 + src/XMakeTasks/GetFrameworkSDKPath.cs | 289 + src/XMakeTasks/GetInstalledSDKLocations.cs | 228 + src/XMakeTasks/GetReferenceAssemblyPaths.cs | 295 + src/XMakeTasks/GetSDKReferenceFiles.cs | 1434 ++ src/XMakeTasks/IAnalyzerHostObject.cs | 23 + src/XMakeTasks/IComReferenceResolver.cs | 51 + src/XMakeTasks/ICscHostObject.cs | 71 + src/XMakeTasks/ICscHostObject2.cs | 25 + src/XMakeTasks/ICscHostObject3.cs | 25 + src/XMakeTasks/ICscHostObject4.cs | 27 + src/XMakeTasks/IVbcHostObject.cs | 70 + src/XMakeTasks/IVbcHostObject2.cs | 24 + src/XMakeTasks/IVbcHostObject3.cs | 22 + src/XMakeTasks/IVbcHostObject4.cs | 22 + src/XMakeTasks/IVbcHostObject5.cs | 30 + src/XMakeTasks/IVbcHostObjectFreeThreaded.cs | 28 + src/XMakeTasks/InstalledSDKResolver.cs | 102 + .../InvalidParameterValueException.cs | 81 + src/XMakeTasks/LC.cs | 170 + src/XMakeTasks/ListOperators/FindUnderPath.cs | 151 + .../ListOperators/RemoveDuplicates.cs | 69 + src/XMakeTasks/MSBuild.cs | 879 + src/XMakeTasks/MSBuildTasks.h | 26 + src/XMakeTasks/MakeDir.cs | 110 + src/XMakeTasks/ManagedCompiler.cs | 701 + .../ManifestUtil/ApplicationIdentity.cs | 78 + .../ManifestUtil/ApplicationManifest.cs | 885 + .../ManifestUtil/AssemblyIdentity.cs | 608 + .../ManifestUtil/AssemblyManifest.cs | 49 + .../ManifestUtil/AssemblyReference.cs | 175 + .../AssemblyReferenceCollection.cs | 176 + src/XMakeTasks/ManifestUtil/BaseReference.cs | 205 + src/XMakeTasks/ManifestUtil/ComImporter.cs | 234 + .../ManifestUtil/CompatibleFramework.cs | 82 + .../CompatibleFrameworkCollection.cs | 53 + src/XMakeTasks/ManifestUtil/Constants.cs | 32 + src/XMakeTasks/ManifestUtil/ConvertUtil.cs | 37 + src/XMakeTasks/ManifestUtil/DeployManifest.cs | 780 + .../ManifestUtil/EmbeddedManifestReader.cs | 68 + .../ManifestUtil/FileAssociation.cs | 99 + .../ManifestUtil/FileAssociationCollection.cs | 53 + src/XMakeTasks/ManifestUtil/FileReference.cs | 518 + .../ManifestUtil/FileReferenceCollection.cs | 110 + src/XMakeTasks/ManifestUtil/Manifest.cs | 689 + .../ManifestUtil/ManifestFormatter.cs | 86 + src/XMakeTasks/ManifestUtil/ManifestReader.cs | 261 + src/XMakeTasks/ManifestUtil/ManifestWriter.cs | 192 + src/XMakeTasks/ManifestUtil/MetadataReader.cs | 153 + src/XMakeTasks/ManifestUtil/NativeMethods.cs | 46 + src/XMakeTasks/ManifestUtil/OutputMessage.cs | 177 + src/XMakeTasks/ManifestUtil/PathUtil.cs | 155 + .../RSAPKCS1SHA256SignatureDescription.cs | 42 + src/XMakeTasks/ManifestUtil/SecurityUtil.cs | 812 + src/XMakeTasks/ManifestUtil/Strings.resx | 170 + src/XMakeTasks/ManifestUtil/TrustInfo.cs | 733 + src/XMakeTasks/ManifestUtil/Util.cs | 526 + src/XMakeTasks/ManifestUtil/XPaths.cs | 113 + src/XMakeTasks/ManifestUtil/XmlNamespaces.cs | 29 + src/XMakeTasks/ManifestUtil/XmlUtil.cs | 178 + src/XMakeTasks/ManifestUtil/manifest.xml | 9 + src/XMakeTasks/ManifestUtil/mansign.cs | 1554 ++ src/XMakeTasks/ManifestUtil/mansign2.cs | 1734 ++ src/XMakeTasks/ManifestUtil/merge.xsl | 128 + src/XMakeTasks/ManifestUtil/read2.xsl | 568 + src/XMakeTasks/ManifestUtil/trustinfo2.xsl | 27 + src/XMakeTasks/ManifestUtil/write2.xsl | 416 + src/XMakeTasks/ManifestUtil/write3.xsl | 416 + src/XMakeTasks/Message.cs | 169 + src/XMakeTasks/Microsoft.Build.Tasks.csproj | 916 + .../Microsoft.CSharp.CurrentVersion.targets | 496 + src/XMakeTasks/Microsoft.CSharp.targets | 185 + .../Microsoft.Common.CurrentVersion.targets | 5462 +++++ src/XMakeTasks/Microsoft.Common.overridetasks | 24 + src/XMakeTasks/Microsoft.Common.props | 92 + src/XMakeTasks/Microsoft.Common.targets | 132 + src/XMakeTasks/Microsoft.Common.tasks | 78 + .../Microsoft.Data.Entity.Shim.targets | 14 + ...icrosoft.NETFramework.CurrentVersion.props | 141 + ...rosoft.NETFramework.CurrentVersion.targets | 121 + src/XMakeTasks/Microsoft.NETFramework.props | 46 + src/XMakeTasks/Microsoft.NETFramework.targets | 79 + .../Microsoft.ServiceModel.Shim.targets | 14 + ...crosoft.VisualBasic.CurrentVersion.targets | 494 + src/XMakeTasks/Microsoft.VisualBasic.targets | 185 + ...osoft.VisualStudioVersion.v11.Common.props | 20 + ...osoft.VisualStudioVersion.v12.Common.props | 20 + ...osoft.VisualStudioVersion.v14.Common.props | 20 + src/XMakeTasks/Microsoft.WinFx.Shim.targets | 14 + ...osoft.WorkflowBuildExtensions.Shim.targets | 27 + src/XMakeTasks/Microsoft.Xaml.Shim.targets | 21 + src/XMakeTasks/Move.cs | 304 + src/XMakeTasks/NativeMethods.cs | 1318 ++ src/XMakeTasks/ParserState.cs | 166 + src/XMakeTasks/PiaReference.cs | 125 + src/XMakeTasks/RCWForCurrentContext.cs | 129 + src/XMakeTasks/RedistList.cs | 1224 ++ src/XMakeTasks/RegisterAssembly.cs | 399 + src/XMakeTasks/RemoveDir.cs | 187 + .../RequiresFramework35SP1Assembly.cs | 240 + src/XMakeTasks/ResGen.cs | 648 + src/XMakeTasks/ResGenDependencies.cs | 375 + src/XMakeTasks/ResolveCodeAnalysisRuleSet.cs | 168 + src/XMakeTasks/ResolveComReference.cs | 1871 ++ src/XMakeTasks/ResolveComReferenceCache.cs | 101 + src/XMakeTasks/ResolveKeySource.cs | 339 + src/XMakeTasks/ResolveManifestFiles.cs | 924 + src/XMakeTasks/ResolveNativeReference.cs | 406 + .../ResolveNonMSBuildProjectOutput.cs | 225 + src/XMakeTasks/ResolveProjectBase.cs | 309 + src/XMakeTasks/ResolveSDKReference.cs | 1799 ++ src/XMakeTasks/SGen.cs | 404 + src/XMakeTasks/SdkToolsPathUtility.cs | 167 + src/XMakeTasks/SignFile.cs | 116 + src/XMakeTasks/StateFileBase.cs | 148 + src/XMakeTasks/Strings.resx | 2683 +++ src/XMakeTasks/StrongNameException.cs | 45 + src/XMakeTasks/StrongNameUtils.cs | 292 + .../StronglyTypedResourceBuilder.cs | 680 + src/XMakeTasks/System.Design.cs | 172 + src/XMakeTasks/SystemState.cs | 550 + src/XMakeTasks/TaskExtension.cs | 56 + src/XMakeTasks/TlbImp.cs | 361 + src/XMakeTasks/TlbReference.cs | 467 + src/XMakeTasks/ToolTaskExtension.cs | 160 + src/XMakeTasks/Touch.cs | 325 + src/XMakeTasks/UnitTests/Al_Tests.cs | 608 + src/XMakeTasks/UnitTests/AppConfig_Tests.cs | 228 + .../UnitTests/AspNetCompiler_Tests.cs | 298 + .../UnitTests/AssemblyIdentity_Tests.cs | 48 + .../UnitTests/AssemblyNameEx_Tests.cs | 594 + src/XMakeTasks/UnitTests/AssemblyRefs.cs | 94 + .../AssemblyRegistrationCache_Tests.cs | 31 + .../UnitTests/AssignCulture_Tests.cs | 222 + .../UnitTests/AssignLinkMetadata_Tests.cs | 184 + .../AssignProjectConfiguration_Tests.cs | 412 + .../UnitTests/AssignTargetPath_Tests.cs | 85 + src/XMakeTasks/UnitTests/AxImp_Tests.cs | 196 + .../UnitTests/AxTlbBaseTask_Tests.cs | 355 + .../UnitTests/CSharpParserUtilitites_Tests.cs | 385 + .../UnitTests/CSharpTokenizer_Tests.cs | 459 + src/XMakeTasks/UnitTests/CallTarget_Tests.cs | 211 + .../UnitTests/CodeTaskFactoryTests.cs | 1045 + .../UnitTests/ComReferenceWalker_Tests.cs | 373 + .../UnitTests/ComReference_Tests.cs | 54 + src/XMakeTasks/UnitTests/CombinePath_Tests.cs | 187 + .../CommandLineBuilderExtension_Tests.cs | 99 + .../UnitTests/CommandLineGenerator_Tests.cs | 210 + .../UnitTests/CommandLine_Support.cs | 295 + .../UnitTests/ConvertToAbsolutePath_Tests.cs | 124 + src/XMakeTasks/UnitTests/Copy_Tests.cs | 2150 ++ .../CreateCSharpManifestResourceName_Tests.cs | 721 + src/XMakeTasks/UnitTests/CreateItem_Tests.cs | 222 + .../UnitTests/CreateProperty_Tests.cs | 112 + ...teVisualBasicManifestResourceName_Tests.cs | 667 + src/XMakeTasks/UnitTests/Csc_Tests.cs | 1097 + src/XMakeTasks/UnitTests/Culture_Tests.cs | 60 + src/XMakeTasks/UnitTests/Delete_Tests.cs | 43 + .../UnitTests/DependentAssembly_Tests.cs | 33 + .../UnitTests/ErrorWarningMessage_Tests.cs | 353 + src/XMakeTasks/UnitTests/Exec_Tests.cs | 587 + src/XMakeTasks/UnitTests/FileStateTests.cs | 470 + .../UnitTests/FindAppConfigFile_Tests.cs | 100 + src/XMakeTasks/UnitTests/FindInList_Tests.cs | 138 + .../FindInvalidProjectReferences_Tests.cs | 73 + .../UnitTests/FindUnderPath_Tests.cs | 133 + .../GenerateResourceOutOfProc_Tests.cs | 2949 +++ .../UnitTests/GenerateResource_Tests.cs | 3371 ++++ .../GetInstalledSDKLocations_Tests.cs | 380 + .../UnitTests/GetReferencePaths_Tests.cs | 269 + .../UnitTests/GetSDKReference_Tests.cs | 1404 ++ src/XMakeTasks/UnitTests/LC_Tests.cs | 44 + src/XMakeTasks/UnitTests/MSBuild_Tests.cs | 1353 ++ src/XMakeTasks/UnitTests/MakeDir_Tests.cs | 209 + .../UnitTests/ManagedCompiler_Tests.cs | 360 + .../UnitTests/ManifestWriter_Tests.cs | 36 + .../Microsoft.Build.Tasks.UnitTests.csproj | 176 + src/XMakeTasks/UnitTests/MockCscHostObject.cs | 151 + .../UnitTests/MockFaultInjectionHelper.cs | 34 + src/XMakeTasks/UnitTests/MockTypeInfo.cs | 499 + src/XMakeTasks/UnitTests/MockTypeLib.cs | 253 + .../UnitTests/MockUnmanagedMemoryHelper.cs | 115 + src/XMakeTasks/UnitTests/MockVbcHostObject.cs | 176 + src/XMakeTasks/UnitTests/Move_Tests.cs | 877 + .../UnitTests/PropertyParser_Tests.cs | 224 + .../UnitTests/ReadLinesFromFile_Tests.cs | 303 + src/XMakeTasks/UnitTests/RemoveDir_Tests.cs | 43 + .../UnitTests/RemoveDuplicates_Tests.cs | 99 + .../UnitTests/ResGenDependencies_Tests.cs | 81 + src/XMakeTasks/UnitTests/ResGen_Tests.cs | 588 + .../ResolveAssemblyReference_Tests.cs | 16523 ++++++++++++++++ .../ResolveCodeAnalysisRuleSet_Tests.cs | 319 + .../UnitTests/ResolveComReference_Tests.cs | 839 + .../ResolveNonMSBuildProjectOutput_Tests.cs | 348 + .../UnitTests/ResolveSDKReference_Tests.cs | 4324 ++++ .../UnitTests/ResolveVCProjectOutput_Tests.cs | 2129 ++ src/XMakeTasks/UnitTests/SGen_Tests.cs | 182 + src/XMakeTasks/UnitTests/SampleResx | 131 + .../UnitTests/SdkToolsPathUtility_Tests.cs | 364 + src/XMakeTasks/UnitTests/StreamHelpers.cs | 44 + .../UnitTests/StreamMappedString_Tests.cs | 341 + src/XMakeTasks/UnitTests/TlbImp_Tests.cs | 269 + .../UnitTests/ToolTaskExtension_Tests.cs | 105 + src/XMakeTasks/UnitTests/Touch_Tests.cs | 348 + src/XMakeTasks/UnitTests/TrustInfo_Tests.cs | 41 + src/XMakeTasks/UnitTests/Vbc_Tests.cs | 1270 ++ .../VisualBasicParserUtilitites_Tests.cs | 130 + .../UnitTests/VisualBasicTokenizer_Tests.cs | 313 + src/XMakeTasks/UnitTests/WinMDExp_Tests.cs | 109 + .../UnitTests/WriteCodeFragment_Tests.cs | 532 + .../UnitTests/XamlDataDrivenToolTask_Tests.cs | 422 + .../UnitTests/XamlTaskFactory_Tests.cs | 792 + src/XMakeTasks/UnitTests/XamlTestHelpers.cs | 242 + src/XMakeTasks/UnitTests/XmlPeek_Tests.cs | 326 + src/XMakeTasks/UnitTests/XmlPoke_Tests.cs | 351 + .../UnitTests/XslTransformation_Tests.cs | 1235 ++ src/XMakeTasks/UnregisterAssembly.cs | 318 + src/XMakeTasks/UpdateManifest.cs | 69 + src/XMakeTasks/Vbc.cs | 1139 ++ src/XMakeTasks/VisualBasicParserUtilities.cs | 124 + src/XMakeTasks/Warning.cs | 97 + src/XMakeTasks/WinMDExp.cs | 336 + src/XMakeTasks/Workflow.Shim.Targets | 14 + .../Workflow.VisualBasic.Shim.Targets | 14 + src/XMakeTasks/WriteCodeFragment.cs | 280 + .../AssemblyIdentityTest.cs | 125 + .../XMakeTasksUnitTests/AuthoringTests.txt | 136 + .../Properties/AssemblyInfo.cs | 35 + .../XMakeTasksUnitTests/UtilTest.cs | 96 + .../XMakeTasksUnitTests.csproj | 102 + .../XamlRules/CSharp.BrowseObject.xaml | 88 + .../XamlRules/CSharp.ProjectItemsSchema.xaml | 16 + src/XMakeTasks/XamlRules/CSharp.xaml | 84 + src/XMakeTasks/XamlRules/Content.xaml | 80 + .../XamlRules/Debugger_General.xaml | 28 + .../XamlRules/EmbeddedResource.xaml | 95 + src/XMakeTasks/XamlRules/Folder.xaml | 20 + .../XamlRules/General.BrowseObject.xaml | 55 + src/XMakeTasks/XamlRules/General.xaml | 93 + src/XMakeTasks/XamlRules/General_File.xaml | 94 + src/XMakeTasks/XamlRules/None.xaml | 93 + .../XamlRules/ProjectItemsSchema.xaml | 139 + .../XamlRules/ResolvedAssemblyReference.xaml | 122 + .../XamlRules/ResolvedCOMReference.xaml | 124 + .../XamlRules/ResolvedProjectReference.xaml | 119 + src/XMakeTasks/XamlRules/Scc.xaml | 16 + src/XMakeTasks/XamlRules/SpecialFolder.xaml | 24 + src/XMakeTasks/XamlRules/SubProject.xaml | 16 + .../XamlRules/VisualBasic.BrowseObject.xaml | 88 + .../VisualBasic.ProjectItemsSchema.xaml | 16 + src/XMakeTasks/XamlRules/VisualBasic.xaml | 84 + .../XamlRules/assemblyreference.xaml | 43 + src/XMakeTasks/XamlRules/comreference.xaml | 20 + .../XamlRules/projectreference.xaml | 46 + .../XamlTaskFactory/CommandLineGenerator.cs | 774 + .../XamlTaskFactory/CommandLineToolSwitch.cs | 692 + src/XMakeTasks/XamlTaskFactory/Property.cs | 825 + .../XamlTaskFactory/RelationsParser.cs | 900 + .../XamlTaskFactory/TaskGenerator.cs | 1374 ++ src/XMakeTasks/XamlTaskFactory/TaskParser.cs | 630 + .../XamlTaskFactory/XamlDataDrivenToolTask.cs | 579 + .../XamlTaskFactory/XamlTaskFactory.cs | 252 + src/XMakeTasks/XmlPeek.cs | 416 + src/XMakeTasks/XmlPoke.cs | 283 + src/XMakeTasks/XslTransformation.cs | 651 + src/XMakeTasks/native.rc | 4 + .../stronglytypedresourcebuilder.cs | 702 + .../system.design/system.design.txt | 27 + src/dir.props | 3 + src/dir.targets | 19 + src/dirs.proj | 25 + 1201 files changed, 467049 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 BuildAndCopy.cmd create mode 100644 MSBuildLocalSystemDependencies.txt create mode 100644 README.md create mode 100644 RebuildWithLocalMSBuild.cmd create mode 100644 build.cmd create mode 100644 build.proj create mode 100644 dir.props create mode 100644 dir.targets create mode 100644 dir.traversal.targets create mode 100644 src/.nuget/NuGet.Config create mode 100644 src/.nuget/packages.config create mode 100644 src/Framework/AssemblyInfo.cs create mode 100644 src/Framework/BuildEngineResult.cs create mode 100644 src/Framework/BuildErrorEventArgs.cs create mode 100644 src/Framework/BuildEventArgs.cs create mode 100644 src/Framework/BuildEventContext.cs create mode 100644 src/Framework/BuildFinishedEventArgs.cs create mode 100644 src/Framework/BuildMessageEventArgs.cs create mode 100644 src/Framework/BuildStartedEventArgs.cs create mode 100644 src/Framework/BuildStatusEventArgs.cs create mode 100644 src/Framework/BuildWarningEventArgs.cs create mode 100644 src/Framework/CriticalBuildMessageEventArgs.cs create mode 100644 src/Framework/CustomBuildEventArgs.cs create mode 100644 src/Framework/Event args classes.cd create mode 100644 src/Framework/EventContext.cs create mode 100644 src/Framework/ExternalProjectFinishedEventArgs.cs create mode 100644 src/Framework/ExternalProjectStartedEventArgs.cs create mode 100644 src/Framework/FxCopExclusions/Microsoft.Build.Framework.Suppressions.cs create mode 100644 src/Framework/IBuildEngine.cs create mode 100644 src/Framework/IBuildEngine2.cs create mode 100644 src/Framework/IBuildEngine3.cs create mode 100644 src/Framework/IBuildEngine4.cs create mode 100644 src/Framework/ICancelableTask.cs create mode 100644 src/Framework/IEventRedirector.cs create mode 100644 src/Framework/IEventSource.cs create mode 100644 src/Framework/IForwardingLogger.cs create mode 100644 src/Framework/IGeneratedTask.cs create mode 100644 src/Framework/ILogger.cs create mode 100644 src/Framework/INodeLogger.cs create mode 100644 src/Framework/ITask.cs create mode 100644 src/Framework/ITaskFactory.cs create mode 100644 src/Framework/ITaskFactory2.cs create mode 100644 src/Framework/ITaskHost.cs create mode 100644 src/Framework/ITaskItem.cs create mode 100644 src/Framework/ITaskItem2.cs create mode 100644 src/Framework/LazyFormattedBuildEventArgs.cs create mode 100644 src/Framework/LoadInSeparateAppDomainAttribute.cs create mode 100644 src/Framework/LoggerException.cs create mode 100644 src/Framework/Microsoft.Build.Framework.csproj create mode 100644 src/Framework/OutputAttribute.cs create mode 100644 src/Framework/ProjectFinishedEventArgs.cs create mode 100644 src/Framework/ProjectStartedEventArgs.cs create mode 100644 src/Framework/RequiredAttribute.cs create mode 100644 src/Framework/RequiredRuntimeAttribute.cs create mode 100644 src/Framework/RunInMTAAttribute.cs create mode 100644 src/Framework/RunInSTAAttribute.cs create mode 100644 src/Framework/TargetFinishedEventArgs.cs create mode 100644 src/Framework/TargetStartedEventArgs.cs create mode 100644 src/Framework/TaskCommandLineEventArgs.cs create mode 100644 src/Framework/TaskFinishedEventArgs.cs create mode 100644 src/Framework/TaskPropertyInfo.cs create mode 100644 src/Framework/TaskStartedEventArgs.cs create mode 100644 src/Framework/UnitTests/Attribute_Tests.cs create mode 100644 src/Framework/UnitTests/BuildErrorEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/BuildFinishedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/BuildMessageEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/BuildStartedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/BuildWarningEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/CriticalBuildMessageEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/CustomEventArgSerialization_Tests.cs create mode 100644 src/Framework/UnitTests/EventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/ExternalProjectFinishedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/ExternalProjectStartedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/LoggerException_Tests.cs create mode 100644 src/Framework/UnitTests/Microsoft.Build.Framework.UnitTests.csproj create mode 100644 src/Framework/UnitTests/ProjectFinishedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/ProjectStartedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/TargetFinishedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/TargetStartedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/TaskCommandLineEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/TaskFinishedEventArgs_Tests.cs create mode 100644 src/Framework/UnitTests/TaskStartedEventArgs_Tests.cs create mode 100644 src/Framework/XamlTypes/Argument.cs create mode 100644 src/Framework/XamlTypes/BaseProperty.cs create mode 100644 src/Framework/XamlTypes/BoolProperty.cs create mode 100644 src/Framework/XamlTypes/Category.cs create mode 100644 src/Framework/XamlTypes/CategorySchema.cs create mode 100644 src/Framework/XamlTypes/ContentType.cs create mode 100644 src/Framework/XamlTypes/DataSource.cs create mode 100644 src/Framework/XamlTypes/DynamicEnumProperty.cs create mode 100644 src/Framework/XamlTypes/EnumProperty.cs create mode 100644 src/Framework/XamlTypes/EnumValue.cs create mode 100644 src/Framework/XamlTypes/FileExtension.cs create mode 100644 src/Framework/XamlTypes/IProjectSchemaNode.cs create mode 100644 src/Framework/XamlTypes/IntProperty.cs create mode 100644 src/Framework/XamlTypes/ItemType.cs create mode 100644 src/Framework/XamlTypes/NameValuePair.cs create mode 100644 src/Framework/XamlTypes/ProjectSchemaDefinitions.cs create mode 100644 src/Framework/XamlTypes/Rule.cs create mode 100644 src/Framework/XamlTypes/RuleBag.cs create mode 100644 src/Framework/XamlTypes/RuleSchema.cs create mode 100644 src/Framework/XamlTypes/StringListProperty.cs create mode 100644 src/Framework/XamlTypes/StringProperty.cs create mode 100644 src/Framework/XamlTypes/ValueEditor.cs create mode 100644 src/Framework/native.rc create mode 100644 src/MSBuild.sln create mode 100644 src/Packages.dgml create mode 100644 src/Shared/AssemblyFoldersEx.cs create mode 100644 src/Shared/AssemblyLoadInfo.cs create mode 100644 src/Shared/AssemblyNameComparer.cs create mode 100644 src/Shared/AssemblyNameExtension.cs create mode 100644 src/Shared/AssemblyNameReverseVersionComparer.cs create mode 100644 src/Shared/AwaitExtensions.cs create mode 100644 src/Shared/BuildEventFileInfo.cs create mode 100644 src/Shared/CanonicalError.cs create mode 100644 src/Shared/CollectionHelpers.cs create mode 100644 src/Shared/CommunicationsUtilities.cs create mode 100644 src/Shared/Constants.cs create mode 100644 src/Shared/ConversionUtilities.cs create mode 100644 src/Shared/CopyOnWriteDictionary.cs create mode 100644 src/Shared/EncodingUtilities.cs create mode 100644 src/Shared/ErrorUtilities.cs create mode 100644 src/Shared/EscapingUtilities.cs create mode 100644 src/Shared/EventArgsFormatting.cs create mode 100644 src/Shared/ExceptionHandling.cs create mode 100644 src/Shared/ExtensionFoldersRegistryKey.cs create mode 100644 src/Shared/FileDelegates.cs create mode 100644 src/Shared/FileMatcher.cs create mode 100644 src/Shared/FileUtilities.cs create mode 100644 src/Shared/FileUtilitiesRegex.cs create mode 100644 src/Shared/FrameworkLocationHelper.cs create mode 100644 src/Shared/FxCopExclusions/Microsoft.Build.Shared.Suppressions.cs create mode 100644 src/Shared/HybridDictionary.cs create mode 100644 src/Shared/IElementLocation.cs create mode 100644 src/Shared/IKeyed.cs create mode 100644 src/Shared/INodeEndpoint.cs create mode 100644 src/Shared/INodePacket.cs create mode 100644 src/Shared/INodePacketFactory.cs create mode 100644 src/Shared/INodePacketHandler.cs create mode 100644 src/Shared/INodePacketTranslatable.cs create mode 100644 src/Shared/INodePacketTranslator.cs create mode 100644 src/Shared/InprocTrackingNativeMethods.cs create mode 100644 src/Shared/InternalErrorException.cs create mode 100644 src/Shared/InterningBinaryReader.cs create mode 100644 src/Shared/LanguageParser/CSharptokenCharReader.cs create mode 100644 src/Shared/LanguageParser/CSharptokenEnumerator.cs create mode 100644 src/Shared/LanguageParser/CSharptokenizer.cs create mode 100644 src/Shared/LanguageParser/StreamMappedString.cs create mode 100644 src/Shared/LanguageParser/VisualBasictokenCharReader.cs create mode 100644 src/Shared/LanguageParser/VisualBasictokenEnumerator.cs create mode 100644 src/Shared/LanguageParser/VisualBasictokenizer.cs create mode 100644 src/Shared/LanguageParser/token.cs create mode 100644 src/Shared/LanguageParser/tokenChar.cs create mode 100644 src/Shared/LanguageParser/tokenCharReader.cs create mode 100644 src/Shared/LanguageParser/tokenEnumerator.cs create mode 100644 src/Shared/LoadedType.cs create mode 100644 src/Shared/LogMessagePacketBase.cs create mode 100644 src/Shared/MSBuildNameIgnoreCaseComparer.cs create mode 100644 src/Shared/MetadataConversionUtilities.cs create mode 100644 src/Shared/Modifiers.cs create mode 100644 src/Shared/NGen.cs create mode 100644 src/Shared/NativeMethodsShared.cs create mode 100644 src/Shared/NodeBuildComplete.cs create mode 100644 src/Shared/NodeEndpointOutOfProcBase.cs create mode 100644 src/Shared/NodeEngineShutdownReason.cs create mode 100644 src/Shared/NodePacketFactory.cs create mode 100644 src/Shared/NodePacketTranslator.cs create mode 100644 src/Shared/NodeShutdown.cs create mode 100644 src/Shared/OpportunisticIntern.cs create mode 100644 src/Shared/OutOfProcTaskHostTaskResult.cs create mode 100644 src/Shared/Pair.cs create mode 100644 src/Shared/ProjectErrorUtilities.cs create mode 100644 src/Shared/ProjectFileErrorUtilities.cs create mode 100644 src/Shared/ProjectWriter.cs create mode 100644 src/Shared/PropertyParser.cs create mode 100644 src/Shared/QuotingUtilities.cs create mode 100644 src/Shared/README.txt create mode 100644 src/Shared/ReadOnlyCollection.cs create mode 100644 src/Shared/ReadOnlyEmptyCollection.cs create mode 100644 src/Shared/ReadOnlyEmptyDictionary.cs create mode 100644 src/Shared/ReadOnlyEmptyList.cs create mode 100644 src/Shared/RegisteredTaskObjectCacheBase.cs create mode 100644 src/Shared/RegistryDelegates.cs create mode 100644 src/Shared/RegistryHelper.cs create mode 100644 src/Shared/ResourceUtilities.cs create mode 100644 src/Shared/ReuseableStringBuilder.cs create mode 100644 src/Shared/Shared Code.doc create mode 100644 src/Shared/StringBuilderCache.cs create mode 100644 src/Shared/Strings.shared.resx create mode 100644 src/Shared/StrongNameHelpers.cs create mode 100644 src/Shared/TaskEngineAssemblyResolver.cs create mode 100644 src/Shared/TaskHostConfiguration.cs create mode 100644 src/Shared/TaskHostTaskCancelled.cs create mode 100644 src/Shared/TaskHostTaskComplete.cs create mode 100644 src/Shared/TaskLoader.cs create mode 100644 src/Shared/TaskLoggingHelper.cs create mode 100644 src/Shared/TaskLoggingHelperExtension.cs create mode 100644 src/Shared/TaskParameter.cs create mode 100644 src/Shared/TaskParameterTypeVerifier.cs create mode 100644 src/Shared/TempFileUtilities.cs create mode 100644 src/Shared/ThreadPoolExtensions.cs create mode 100644 src/Shared/ThreadingUtilities.cs create mode 100644 src/Shared/ToolsetElement.cs create mode 100644 src/Shared/Tracing.cs create mode 100644 src/Shared/TypeLoader.cs create mode 100644 src/Shared/UnitTests/App.config create mode 100644 src/Shared/UnitTests/AssemblyNameEx_Tests.cs create mode 100644 src/Shared/UnitTests/BuildEventArgsExtension.cs create mode 100644 src/Shared/UnitTests/CopyOnWriteDictionary_Tests.cs create mode 100644 src/Shared/UnitTests/ErrorUtilities_Tests.cs create mode 100644 src/Shared/UnitTests/EscapingUtilities_Tests.cs create mode 100644 src/Shared/UnitTests/FileMatcher_Tests.cs create mode 100644 src/Shared/UnitTests/FileUtilities_Tests.cs create mode 100644 src/Shared/UnitTests/HybridDictionary_Tests.cs create mode 100644 src/Shared/UnitTests/MockEngine.cs create mode 100644 src/Shared/UnitTests/MockLogger.cs create mode 100644 src/Shared/UnitTests/NativeMethodsShared_Tests.cs create mode 100644 src/Shared/UnitTests/ObjectModelHelpers.cs create mode 100644 src/Shared/UnitTests/ResourceUtilities_Tests.cs create mode 100644 src/Shared/UnitTests/StreamHelpers.cs create mode 100644 src/Shared/UnitTests/TaskParameter_Tests.cs create mode 100644 src/Shared/UnitTests/TypeLoader_Tests.cs create mode 100644 src/Shared/UnitTests/XmakeAttributes_Tests.cs create mode 100644 src/Shared/UnitTests/XmlUtilities_Tests.cs create mode 100644 src/Shared/VersionUtilities.cs create mode 100644 src/Shared/VisualStudioConstants.cs create mode 100644 src/Shared/XMakeAttributes.cs create mode 100644 src/Shared/XMakeElements.cs create mode 100644 src/Shared/XamlUtilities.cs create mode 100644 src/Shared/XmlUtilities.cs create mode 100644 src/Utilities/ApiContract.cs create mode 100644 src/Utilities/AppDomainIsolatedTask.cs create mode 100644 src/Utilities/AssemblyFoldersExInfo.cs create mode 100644 src/Utilities/AssemblyInfo.cs create mode 100644 src/Utilities/AssemblyResources.cs create mode 100644 src/Utilities/CommandLineBuilder.cs create mode 100644 src/Utilities/ExtensionSDK.cs create mode 100644 src/Utilities/FxCopExclusions/Microsoft.Build.Utilities.Suppressions.cs create mode 100644 src/Utilities/Logger.cs create mode 100644 src/Utilities/Microsoft.Build.Utilities.csproj create mode 100644 src/Utilities/MuxLogger.cs create mode 100644 src/Utilities/PlatformManifest.cs create mode 100644 src/Utilities/ProcessorArchitecture.cs create mode 100644 src/Utilities/SDKManifest.cs create mode 100644 src/Utilities/SDKType.cs create mode 100644 src/Utilities/Strings.resx create mode 100644 src/Utilities/TargetPlatformSDK.cs create mode 100644 src/Utilities/Task.cs create mode 100644 src/Utilities/TaskItem.cs create mode 100644 src/Utilities/ToolLocationHelper.cs create mode 100644 src/Utilities/ToolTask.cs create mode 100644 src/Utilities/TrackedDependencies/CanonicalTrackedFilesHelper.cs create mode 100644 src/Utilities/TrackedDependencies/CanonicalTrackedInputFiles.cs create mode 100644 src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs create mode 100644 src/Utilities/TrackedDependencies/DependencyTableCache.cs create mode 100644 src/Utilities/TrackedDependencies/FileTracker.cs create mode 100644 src/Utilities/TrackedDependencies/FlatTrackingData.cs create mode 100644 src/Utilities/TrackedDependencies/NativeMethods.cs create mode 100644 src/Utilities/TrackedDependencies/TrackedDependencies.cs create mode 100644 src/Utilities/UnitTests/ApiContract_Tests.cs create mode 100644 src/Utilities/UnitTests/CanonicalError_Tests.cs create mode 100644 src/Utilities/UnitTests/CommandLineBuilder_Tests.cs create mode 100644 src/Utilities/UnitTests/EventArgsFormatting_Tests.cs create mode 100644 src/Utilities/UnitTests/GetTargetPlatformReferences_Tests.cs create mode 100644 src/Utilities/UnitTests/Logger_Tests.cs create mode 100644 src/Utilities/UnitTests/Microsoft.Build.Utilities.UnitTests.csproj create mode 100644 src/Utilities/UnitTests/MockEngine.cs create mode 100644 src/Utilities/UnitTests/MockLogger.cs create mode 100644 src/Utilities/UnitTests/MockTask.cs create mode 100644 src/Utilities/UnitTests/MuxLogger_Tests.cs create mode 100644 src/Utilities/UnitTests/PlatformManifest_Tests.cs create mode 100644 src/Utilities/UnitTests/ProcessorArchitecture_Tests.cs create mode 100644 src/Utilities/UnitTests/TaskItem_Tests.cs create mode 100644 src/Utilities/UnitTests/TaskLoggingHelper_Tests.cs create mode 100644 src/Utilities/UnitTests/ToolLocationHelper_Tests.cs create mode 100644 src/Utilities/UnitTests/ToolTask_Tests.cs create mode 100644 src/Utilities/UnitTests/TrackedDependencies/FileTrackerTests.cs create mode 100644 src/Utilities/UnitTests/TrackedDependencies/MockEngine.cs create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.cpp create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.tst create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/one1.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/one2.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/one3.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/two.cpp create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/two1.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/two2.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TestFiles/two3.h create mode 100644 src/Utilities/UnitTests/TrackedDependencies/TrackedDependenciesTests.cs create mode 100644 src/Utilities/UnitTests/strings.resx create mode 100644 src/Utilities/native.rc create mode 100644 src/XMakeBuildEngine/AllEngine/allengine.proj create mode 100644 src/XMakeBuildEngine/AllEngineAndConversion/allengineandconversion.proj create mode 100644 src/XMakeBuildEngine/ApexTests/ApexTests.csproj create mode 100644 src/XMakeBuildEngine/ApexTests/AssemblyInfo.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerConfiguration.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerGenerator.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerVerifier.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationVerifier.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultVerifier.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTests.cs create mode 100644 src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionVerifier.cs create mode 100644 src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheVerifier.cs create mode 100644 src/XMakeBuildEngine/ApexTests/Helpers/AsyncBuildRequestStatus.cs create mode 100644 src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceVerifier.cs create mode 100644 src/XMakeBuildEngine/ApexTests/Helpers/SimpleTaskHelper.cs create mode 100644 src/XMakeBuildEngine/ApexTests/Helpers/TestExtensionHelper.cs create mode 100644 src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheTestExtension.cs create mode 100644 src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheVerifier.cs create mode 100644 src/XMakeBuildEngine/AssemblyInfo.cs create mode 100644 src/XMakeBuildEngine/BackEnd/BuildManager/BuildManager.cs create mode 100644 src/XMakeBuildEngine/BackEnd/BuildManager/BuildParameters.cs create mode 100644 src/XMakeBuildEngine/BackEnd/BuildManager/BuildRequestData.cs create mode 100644 src/XMakeBuildEngine/BackEnd/BuildManager/BuildSubmission.cs create mode 100644 src/XMakeBuildEngine/BackEnd/BuildManager/LegacyThreadingData.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/BuildComponentFactoryCollection.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestConfigurationResponse.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/IBuildRequestEngine.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/ConfigCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/IConfigCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/IPropertyCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/IRegisteredTaskObjectCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/IResultsCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCache.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCacheResponse.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/INodeManager.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/INodeProvider.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/LogMessagePacket.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointInProc.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeFailedToLaunchException.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeInfo.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeManager.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodePacketTranslatorExtensions.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderInProc.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProc.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Communications/TaskHostNodeManager.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/IBuildComponent.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/IBuildComponentHost.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/BaseLoggingContext.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/BuildEventArgTransportSink.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/CentralForwardingLogger.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/EventRedirectorToSink.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/EventSourceSink.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/ForwardingLoggerRecord.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/ILoggingService.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingService.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceFactory.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceLogMethods.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/NodeLoggingContext.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/ProjectLoggingContext.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/TargetLoggingContext.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Logging/TaskLoggingContext.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/BatchingEngine.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/FullTracking.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilder.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilderCallback.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilder.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilderCallback.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITaskBuilder.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTask.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ItemBucket.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/Lookup.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/RequestBuilder.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetBuilder.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetEntry.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetSpecification.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskBuilder.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskHost.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/IScheduler.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulableRequest.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleResponse.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/Scheduler.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulerCircularDependencyException.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingData.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingPlan.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Node/INode.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Node/InProcNode.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Node/NativeMethods.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Node/NodeConfiguration.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Node/OutOfProcNode.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/BuildAbortedException.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/BuildRequest.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/BuildRequestBlocker.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/BuildRequestConfiguration.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/BuildRequestUnblocker.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/BuildResult.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/CircularDependencyException.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/ConfigurationMetadata.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/IBuildResults.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/ITargetResult.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/TargetResult.cs create mode 100644 src/XMakeBuildEngine/BackEnd/Shared/WorkUnitResult.cs create mode 100644 src/XMakeBuildEngine/BackEnd/TaskExecutionHost/AddInParts/ITaskExecutionHost.cs create mode 100644 src/XMakeBuildEngine/BackEnd/TaskExecutionHost/TaskExecutionHost.cs create mode 100644 src/XMakeBuildEngine/Collections/ConcurrentQueueExtensions.cs create mode 100644 src/XMakeBuildEngine/Collections/ConcurrentStackExtensions.cs create mode 100644 src/XMakeBuildEngine/Collections/ConvertingEnumerable.cs create mode 100644 src/XMakeBuildEngine/Collections/CopyOnReadEnumerable.cs create mode 100644 src/XMakeBuildEngine/Collections/CopyOnWritePropertyDictionary.cs create mode 100644 src/XMakeBuildEngine/Collections/FilteringEnumerable.cs create mode 100644 src/XMakeBuildEngine/Collections/HashTableUtility.cs create mode 100644 src/XMakeBuildEngine/Collections/IDeepCloneable.cs create mode 100644 src/XMakeBuildEngine/Collections/IImmutable.cs create mode 100644 src/XMakeBuildEngine/Collections/IValued.cs create mode 100644 src/XMakeBuildEngine/Collections/ItemDictionary.cs create mode 100644 src/XMakeBuildEngine/Collections/LookasideStringInterner.cs create mode 100644 src/XMakeBuildEngine/Collections/MultiDictionary.cs create mode 100644 src/XMakeBuildEngine/Collections/OrdinalIgnoreCaseKeyedComparer.cs create mode 100644 src/XMakeBuildEngine/Collections/OrdinalKeyedComparer.cs create mode 100644 src/XMakeBuildEngine/Collections/PropertyDictionary.cs create mode 100644 src/XMakeBuildEngine/Collections/ReadOnlyConvertingCollection.cs create mode 100644 src/XMakeBuildEngine/Collections/ReadOnlyConvertingDictionary.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/BitHelper.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashHelpers.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSet.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSetDebugView.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/BitHelper.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashHelpers.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSet.cs create mode 100644 src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSetDebugView.cs create mode 100644 src/XMakeBuildEngine/Collections/Triple.cs create mode 100644 src/XMakeBuildEngine/Collections/WeakDictionary.cs create mode 100644 src/XMakeBuildEngine/Collections/WeakReference.cs create mode 100644 src/XMakeBuildEngine/Collections/WeakValueDictionary.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectChooseElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectElementContainer.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectExtensionsElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectImportElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectImportGroupElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectItemDefinitionElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectItemDefinitionGroupElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectItemElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectItemGroupElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectMetadataElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectOnErrorElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectOtherwiseElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectOutputElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectPropertyElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectPropertyGroupElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectRootElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectTargetElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectTaskElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectUsingTaskBodyElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectUsingTaskElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectUsingTaskParameterElement.cs create mode 100644 src/XMakeBuildEngine/Construction/ProjectWhenElement.cs create mode 100644 src/XMakeBuildEngine/Construction/Solution/ProjectConfigurationInSolution.cs create mode 100644 src/XMakeBuildEngine/Construction/Solution/ProjectInSolution.cs create mode 100644 src/XMakeBuildEngine/Construction/Solution/SolutionConfigurationInSolution.cs create mode 100644 src/XMakeBuildEngine/Construction/Solution/SolutionFile.cs create mode 100644 src/XMakeBuildEngine/Construction/Solution/SolutionProjectGenerator.cs create mode 100644 src/XMakeBuildEngine/Construction/UsingTaskParameterGroupElement.cs create mode 100644 src/XMakeBuildEngine/Debugger/DebuggerLocalType.cs create mode 100644 src/XMakeBuildEngine/Debugger/DebuggerManager.cs create mode 100644 src/XMakeBuildEngine/Definition/BuiltInMetadata.cs create mode 100644 src/XMakeBuildEngine/Definition/Project.cs create mode 100644 src/XMakeBuildEngine/Definition/ProjectCollection.cs create mode 100644 src/XMakeBuildEngine/Definition/ProjectCollectionChangedEventArgs.cs create mode 100644 src/XMakeBuildEngine/Definition/ProjectItem.cs create mode 100644 src/XMakeBuildEngine/Definition/ProjectItemDefinition.cs create mode 100644 src/XMakeBuildEngine/Definition/ProjectMetadata.cs create mode 100644 src/XMakeBuildEngine/Definition/ProjectProperty.cs create mode 100644 src/XMakeBuildEngine/Definition/ResolvedImport.cs create mode 100644 src/XMakeBuildEngine/Definition/SubToolset.cs create mode 100644 src/XMakeBuildEngine/Definition/Toolset.cs create mode 100644 src/XMakeBuildEngine/Definition/ToolsetConfigurationReader.cs create mode 100644 src/XMakeBuildEngine/Definition/ToolsetPropertyDefinition.cs create mode 100644 src/XMakeBuildEngine/Definition/ToolsetReader.cs create mode 100644 src/XMakeBuildEngine/Definition/ToolsetRegistryReader.cs create mode 100644 src/XMakeBuildEngine/ElementLocation/ElementLocation.cs create mode 100644 src/XMakeBuildEngine/ElementLocation/RegistryLocation.cs create mode 100644 src/XMakeBuildEngine/ElementLocation/XmlAttributeWithLocation.cs create mode 100644 src/XMakeBuildEngine/ElementLocation/XmlDocumentWithLocation.cs create mode 100644 src/XMakeBuildEngine/ElementLocation/XmlElementWithLocation.cs create mode 100644 src/XMakeBuildEngine/ElementLocation/XmlNameTableThreadSafe.cs create mode 100644 src/XMakeBuildEngine/Errors/InternalLoggerException.cs create mode 100644 src/XMakeBuildEngine/Errors/InvalidProjectFileException.cs create mode 100644 src/XMakeBuildEngine/Errors/InvalidToolsetDefinitionException.cs create mode 100644 src/XMakeBuildEngine/Errors/RegistryException.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ConditionEvaluator.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/AndExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/CharacterUtilities.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/EqualExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/FunctionCallExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/GenericExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanOrEqualExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/IItem.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/LessThanExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/LessThanOrEqualExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/MultipleComparisonExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/NotEqualExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/NotExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/NumericComparisonExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/NumericExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/OperandExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/OperatorExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/OrExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/Parser.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/Scanner.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/StringExpressionNode.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Conditionals/Token.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Evaluator.cs create mode 100644 src/XMakeBuildEngine/Evaluation/EvaluatorMetadataTable.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Expander.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ExpressionShredder.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IEvaluatorData.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IItem.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IItemDefinition.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IItemFactory.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IItemProvider.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IMetadataTable.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IMetadatum.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IProjectMetadataParent.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IProperty.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IPropertyProvider.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IToolsetProvider.cs create mode 100644 src/XMakeBuildEngine/Evaluation/IntrinsicFunctions.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ItemsAndMetadataPair.cs create mode 100644 src/XMakeBuildEngine/Evaluation/MetadataReference.cs create mode 100644 src/XMakeBuildEngine/Evaluation/Preprocessor.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ProjectChangedEventArgs.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ProjectParser.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ProjectStringCache.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ProjectXmlChangedEventArgs.cs create mode 100644 src/XMakeBuildEngine/Evaluation/StringMetadataTable.cs create mode 100644 src/XMakeBuildEngine/Evaluation/ToolsetProvider.cs create mode 100644 src/XMakeBuildEngine/FxCopExclusions/Microsoft.Build.Suppressions.cs create mode 100644 src/XMakeBuildEngine/Instance/HostServices.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectItemDefinitionInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectItemGroupTaskInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectItemGroupTaskItemInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectItemGroupTaskMetadataInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectItemInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectMetadataInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectOnErrorInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskPropertyInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectPropertyInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectTargetInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectTargetInstanceChild.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectTaskInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectTaskInstanceChild.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectTaskOutputItemInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ProjectTaskOutputPropertyInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/ReflectableTaskPropertyInfo.cs create mode 100644 src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactory.cs create mode 100644 src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactoryInstance.cs create mode 100644 src/XMakeBuildEngine/Instance/TaskFactories/TaskHostTask.cs create mode 100644 src/XMakeBuildEngine/Instance/TaskFactoryLoggingHost.cs create mode 100644 src/XMakeBuildEngine/Instance/TaskFactoryWrapper.cs create mode 100644 src/XMakeBuildEngine/Instance/TaskRegistry.cs create mode 100644 src/XMakeBuildEngine/Logging/BaseConsoleLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/ConsoleLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/DistributedLoggers/DistributedFileLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/FileLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/LogFormatter.cs create mode 100644 src/XMakeBuildEngine/Logging/LoggerDescription.cs create mode 100644 src/XMakeBuildEngine/Logging/NullCentralLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/ParallelLogger/ParallelConsoleLogger.cs create mode 100644 src/XMakeBuildEngine/Logging/ParallelLogger/ParallelLoggerHelpers.cs create mode 100644 src/XMakeBuildEngine/Logging/SerialConsoleLogger.cs create mode 100644 src/XMakeBuildEngine/Microsoft.Build.csproj create mode 100644 src/XMakeBuildEngine/Resources/AssemblyResources.cs create mode 100644 src/XMakeBuildEngine/Resources/Constants.cs create mode 100644 src/XMakeBuildEngine/Resources/Strings.resx create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BatchingEngine_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildEventArgTransportSink_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildManager_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfigurationResponse_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEngine_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEntry_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequest_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/BuildResult_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/CentralForwardingLogger_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/ConfigurationMetadata_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/CustomTaskHelper.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/EventRedirectorToSink_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/EventSourceSink_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/FullyQualifiedBuildRequest_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/ITestTaskHost.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/IntegrationTests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/IntrinsicTask_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/LoggingContext_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServiceFactory_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/LoggingService_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/Lookup_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/MSBuild_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/MockHost.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/MockLoggingService.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/MockTaskBuilder.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/NodeEndpointInProc_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/NodePacketTranslator_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/NodePackets_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/OnError_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/RequestBuilder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/ResultsCache_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/Scheduler_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TargetBuilder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TargetEntry_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TargetResult_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilderTestTask.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskExecutionHost_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostConfiguration_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskCancelled_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskHost_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskItemComparer.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TaskRegistry_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/BackEnd/TranslationHelpers.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/CopyOnReadEnumerable_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/FilteringEnumerable_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/LookasideStringInterner_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/MSBuildNameIgnoreCaseComparer_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/MultiDictionary_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/OMcollections_tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/WeakDictionary_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Collections/WeakValueDictionary_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/ConfigureableForwardingLogger_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/ConsoleLogger_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Construction/ElementLocation_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Construction/SolutionFile_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Construction/SolutionProjectGenerator_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ItemDefinitionGroup_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ProjectHelpers.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ProjectItem_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/Project_Internal_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ToolsVersion_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReaderTestHelper.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReader_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ToolsetReader_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/ToolsetRegistryReader_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Definition/Toolset_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/DualQueue_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/EscapingInProjects_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Evaluation/Evaluator_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Evaluation/Expander_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Evaluation/ExpressionShredder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Evaluation/Preprocessor_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Evaluation/ProjectStringCache_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/EventArgsFormatting_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/ExpressionTree_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/FileLogger_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/HashTableUtility_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Instance/HostServices_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Instance/ProjectInstance_Internal_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Instance/ProjectMetadataInstance_Internal_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Instance/ProjectPropertyInstance_Internal_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Instance/TaskItem_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/InvalidProjectFileException_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/LogFormatter_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/LoggerDescription_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/LoggerException_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Microsoft.Build.Engine.UnitTests.csproj create mode 100644 src/XMakeBuildEngine/UnitTests/MockElementLocation.cs create mode 100644 src/XMakeBuildEngine/UnitTests/MockTask.cs create mode 100644 src/XMakeBuildEngine/UnitTests/OpportunisticIntern_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Parser_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/BuildRequestEngine_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/ITestDataProvider.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/Integration_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/MockHost.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/MockLoggingService.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/MockRequestBuilder.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/MockResultsCache.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/MockTargetBuilder.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/MockTaskBuilder.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/ProjectDefinition.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/RequestBuilder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/RequestDefinition.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/TargetBuilder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/TargetDefinition.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/TaskBuilder_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/TaskDefinition.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/TestDataProvider.cs create mode 100644 src/XMakeBuildEngine/UnitTests/QaTests/common_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Scanner_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTests/TargetsFile_Test.cs create mode 100644 src/XMakeBuildEngine/UnitTests/TestUtilities.cs create mode 100644 src/XMakeBuildEngine/UnitTests/Utilities_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/App.config create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/AssemblyResources.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ConstructionEditing_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ElementLocationPublic_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectChooseElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectExtensionsElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportGroupElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionGroupElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemGroupElement_tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectMetadataElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOnErrorElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOutputElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyGroupElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectRootElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTargetElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTaskElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectUsingTaskElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/SolutionFile_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskBodyElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterElement_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterGroup_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/DefinitionEditing_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/EditingElementsReferencedByOrReferences_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectCollection_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItemDefinition_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItem_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectMetadata_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectProperty_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/Project_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProtectImports_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectItemInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectMetadataInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectOnErrorInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectPropertyInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTargetInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputItemInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputPropertyInstance_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/LazyFormattedEventArgs_Tests.cs create mode 100644 src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj create mode 100644 src/XMakeBuildEngine/Utilities/EngineFileUtilities.cs create mode 100644 src/XMakeBuildEngine/Utilities/RegistryKeyWrapper.cs create mode 100644 src/XMakeBuildEngine/Utilities/Utilities.cs create mode 100644 src/XMakeBuildEngine/Xml/ProjectXmlUtilities.cs create mode 100644 src/XMakeBuildEngine/allmajorprojects/allmajorprojects.proj create mode 100644 src/XMakeBuildEngine/native.rc create mode 100644 src/XMakeBuildEngine/packages.config create mode 100644 src/XMakeCommandLine/AssemblyInfo.cs create mode 100644 src/XMakeCommandLine/AssemblyResources.cs create mode 100644 src/XMakeCommandLine/CommandLineSwitchException.cs create mode 100644 src/XMakeCommandLine/CommandLineSwitches.cs create mode 100644 src/XMakeCommandLine/DistributedLoggerRecord.cs create mode 100644 src/XMakeCommandLine/FxCopExclusions/MsBuild.Suppressions.cs create mode 100644 src/XMakeCommandLine/InitializationException.cs create mode 100644 src/XMakeCommandLine/LogMessagePacket.cs create mode 100644 src/XMakeCommandLine/MSBuild.csproj create mode 100644 src/XMakeCommandLine/MSBuild.exe.manifest create mode 100644 src/XMakeCommandLine/MSBuild.ico create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/AssemblyResources.cs create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/FxCopExclusions/MSBuildTaskHost.Suppressions.cs create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/MSBuildTaskHost.csproj create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/OutOfProcTaskHost.cs create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/TypeLoader.cs create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/app.config create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/native.rc create mode 100644 src/XMakeCommandLine/MSBuildTaskHost/dirs.proj create mode 100644 src/XMakeCommandLine/Microsoft.Build.CommonTypes.xsd create mode 100644 src/XMakeCommandLine/Microsoft.Build.Core.xsd create mode 100644 src/XMakeCommandLine/Microsoft.Build.xsd create mode 100644 src/XMakeCommandLine/NodeEndpointOutOfProcTaskHost.cs create mode 100644 src/XMakeCommandLine/OutOfProcTaskAppDomainWrapper.cs create mode 100644 src/XMakeCommandLine/OutOfProcTaskAppDomainWrapperBase.cs create mode 100644 src/XMakeCommandLine/OutOfProcTaskHostNode.cs create mode 100644 src/XMakeCommandLine/ProjectSchemaValidationHandler.cs create mode 100644 src/XMakeCommandLine/Strings.resx create mode 100644 src/XMakeCommandLine/UnitTests/CommandLineSwitchException_Tests.cs create mode 100644 src/XMakeCommandLine/UnitTests/CommandLineSwitches_Tests.cs create mode 100644 src/XMakeCommandLine/UnitTests/InitializationException_Tests.cs create mode 100644 src/XMakeCommandLine/UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj create mode 100644 src/XMakeCommandLine/UnitTests/ProjectSchemaValidationHandler_Tests.cs create mode 100644 src/XMakeCommandLine/UnitTests/XMake_Tests.cs create mode 100644 src/XMakeCommandLine/XMake.cs create mode 100644 src/XMakeCommandLine/app.config create mode 100644 src/XMakeCommandLine/clr2/OutOfProcTaskAppDomainWrapper.cs create mode 100644 src/XMakeCommandLine/msbuild.suitebin.config create mode 100644 src/XMakeCommandLine/msbuild_rascal.manifest create mode 100644 src/XMakeCommandLine/native.rc create mode 100644 src/XMakeCommandLine/xclpupdate.bat create mode 100644 src/XMakeTasks/Al.cs create mode 100644 src/XMakeTasks/AppConfig/AppConfig.cs create mode 100644 src/XMakeTasks/AppConfig/AppConfigException.cs create mode 100644 src/XMakeTasks/AppConfig/BindingRedirect.cs create mode 100644 src/XMakeTasks/AppConfig/DependentAssembly.cs create mode 100644 src/XMakeTasks/AppConfig/RuntimeSection.cs create mode 100644 src/XMakeTasks/AppDomainIsolatedTaskExtension.cs create mode 100644 src/XMakeTasks/AspNetCompiler.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyFoldersExResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyFoldersResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyInformation.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyNameReference.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyNameReferenceAscendingVersionComparer.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyResolution.cs create mode 100644 src/XMakeTasks/AssemblyDependency/AssemblyResolutionConstants.cs create mode 100644 src/XMakeTasks/AssemblyDependency/BadImageReferenceException.cs create mode 100644 src/XMakeTasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/ConflictLossReason.cs create mode 100644 src/XMakeTasks/AssemblyDependency/CopyLocalState.cs create mode 100644 src/XMakeTasks/AssemblyDependency/DependencyResolutionException.cs create mode 100644 src/XMakeTasks/AssemblyDependency/DirectoryResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/DisposableBase.cs create mode 100644 src/XMakeTasks/AssemblyDependency/FrameworkPathResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/GacResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/GenerateBindingRedirects.cs create mode 100644 src/XMakeTasks/AssemblyDependency/GlobalAssemblyCache.cs create mode 100644 src/XMakeTasks/AssemblyDependency/HintPathResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/InstalledAssemblies.cs create mode 100644 src/XMakeTasks/AssemblyDependency/InvalidReferenceAssemblyNameException.cs create mode 100644 src/XMakeTasks/AssemblyDependency/NoMatchReason.cs create mode 100644 src/XMakeTasks/AssemblyDependency/RawFilenameResolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/Reference.cs create mode 100644 src/XMakeTasks/AssemblyDependency/ReferenceResolutionException.cs create mode 100644 src/XMakeTasks/AssemblyDependency/ReferenceTable.cs create mode 100644 src/XMakeTasks/AssemblyDependency/ResolutionSearchLocation.cs create mode 100644 src/XMakeTasks/AssemblyDependency/ResolveAssemblyReference.cs create mode 100644 src/XMakeTasks/AssemblyDependency/Resolver.cs create mode 100644 src/XMakeTasks/AssemblyDependency/TaskItemSpecFilenameComparer.cs create mode 100644 src/XMakeTasks/AssemblyDependency/UnificationReason.cs create mode 100644 src/XMakeTasks/AssemblyDependency/UnificationVersion.cs create mode 100644 src/XMakeTasks/AssemblyDependency/UnifiedAssemblyName.cs create mode 100644 src/XMakeTasks/AssemblyDependency/WarnOrErrorOnTargetArchitectureMismatchBehavior.cs create mode 100644 src/XMakeTasks/AssemblyFolder.cs create mode 100644 src/XMakeTasks/AssemblyInfo.cs create mode 100644 src/XMakeTasks/AssemblyRegistrationCache.cs create mode 100644 src/XMakeTasks/AssemblyRemapping.cs create mode 100644 src/XMakeTasks/AssemblyResources.cs create mode 100644 src/XMakeTasks/AssignCulture.cs create mode 100644 src/XMakeTasks/AssignLinkMetadata.cs create mode 100644 src/XMakeTasks/AssignProjectConfiguration.cs create mode 100644 src/XMakeTasks/AssignTargetPath.cs create mode 100644 src/XMakeTasks/AxImp.cs create mode 100644 src/XMakeTasks/AxReference.cs create mode 100644 src/XMakeTasks/AxTlbBaseReference.cs create mode 100644 src/XMakeTasks/AxTlbBaseTask.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/BootstrapperBuilder.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/BuildMessage.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/BuildResults.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/BuildSettings.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/Interfaces.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/NativeMethods.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/Package.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/PackageCollection.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/Product.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/ProductBuilder.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/ProductBuilderCollection.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/ProductCollection.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/ResourceUpdater.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/Util.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/productvalidationresults.cs create mode 100644 src/XMakeTasks/BootstrapperUtil/xmltoconfig.xsl create mode 100644 src/XMakeTasks/BootstrapperUtil/xmlvalidationresults.cs create mode 100644 src/XMakeTasks/BuildCacheDisposeWrapper.cs create mode 100644 src/XMakeTasks/CSharpParserUtilities.cs create mode 100644 src/XMakeTasks/CallTarget.cs create mode 100644 src/XMakeTasks/CodeTaskFactory.cs create mode 100644 src/XMakeTasks/CodeTaskFactoryInstance.cs create mode 100644 src/XMakeTasks/ComDependencyWalker.cs create mode 100644 src/XMakeTasks/ComReference.cs create mode 100644 src/XMakeTasks/ComReferenceInfo.cs create mode 100644 src/XMakeTasks/ComReferenceItemAttributes.cs create mode 100644 src/XMakeTasks/ComReferenceResolutionException.cs create mode 100644 src/XMakeTasks/ComReferenceTypes.cs create mode 100644 src/XMakeTasks/ComReferenceWrapperInfo.cs create mode 100644 src/XMakeTasks/CombinePath.cs create mode 100644 src/XMakeTasks/CommandLineBuilderExtension.cs create mode 100644 src/XMakeTasks/ConvertToAbsolutePath.cs create mode 100644 src/XMakeTasks/Copy.cs create mode 100644 src/XMakeTasks/CreateCSharpManifestResourceName.cs create mode 100644 src/XMakeTasks/CreateItem.cs create mode 100644 src/XMakeTasks/CreateManifestResourceName.cs create mode 100644 src/XMakeTasks/CreateProperty.cs create mode 100644 src/XMakeTasks/CreateVisualBasicManifestResourceName.cs create mode 100644 src/XMakeTasks/Csc.cs create mode 100644 src/XMakeTasks/Culture.cs create mode 100644 src/XMakeTasks/CultureStringUtilities.cs create mode 100644 src/XMakeTasks/DataDriven/DataDrivenToolTask.cs create mode 100644 src/XMakeTasks/DefaultTasks.bat create mode 100644 src/XMakeTasks/Delegate.cs create mode 100644 src/XMakeTasks/Delete.cs create mode 100644 src/XMakeTasks/Dependencies.cs create mode 100644 src/XMakeTasks/DependencyFile.cs create mode 100644 src/XMakeTasks/Error.cs create mode 100644 src/XMakeTasks/ErrorFromResources.cs create mode 100644 src/XMakeTasks/Exec.cs create mode 100644 src/XMakeTasks/ExtractedClassName.cs create mode 100644 src/XMakeTasks/FileIO/ReadLinesFromFile.cs create mode 100644 src/XMakeTasks/FileIO/WriteLinesToFile.cs create mode 100644 src/XMakeTasks/FileState.cs create mode 100644 src/XMakeTasks/FindAppConfigFile.cs create mode 100644 src/XMakeTasks/FindInList.cs create mode 100644 src/XMakeTasks/FindInvalidProjectReferences.cs create mode 100644 src/XMakeTasks/FormatUrl.cs create mode 100644 src/XMakeTasks/FormatVersion.cs create mode 100644 src/XMakeTasks/FxCopExclusions/Microsoft.Build.Tasks.Suppressions.cs create mode 100644 src/XMakeTasks/GenerateApplicationManifest.cs create mode 100644 src/XMakeTasks/GenerateBootstrapper.cs create mode 100644 src/XMakeTasks/GenerateDeploymentManifest.cs create mode 100644 src/XMakeTasks/GenerateManifestBase.cs create mode 100644 src/XMakeTasks/GenerateResource.cs create mode 100644 src/XMakeTasks/GenerateTrustInfo.cs create mode 100644 src/XMakeTasks/GetAssemblyIdentity.cs create mode 100644 src/XMakeTasks/GetFrameworkPath.cs create mode 100644 src/XMakeTasks/GetFrameworkSDKPath.cs create mode 100644 src/XMakeTasks/GetInstalledSDKLocations.cs create mode 100644 src/XMakeTasks/GetReferenceAssemblyPaths.cs create mode 100644 src/XMakeTasks/GetSDKReferenceFiles.cs create mode 100644 src/XMakeTasks/IAnalyzerHostObject.cs create mode 100644 src/XMakeTasks/IComReferenceResolver.cs create mode 100644 src/XMakeTasks/ICscHostObject.cs create mode 100644 src/XMakeTasks/ICscHostObject2.cs create mode 100644 src/XMakeTasks/ICscHostObject3.cs create mode 100644 src/XMakeTasks/ICscHostObject4.cs create mode 100644 src/XMakeTasks/IVbcHostObject.cs create mode 100644 src/XMakeTasks/IVbcHostObject2.cs create mode 100644 src/XMakeTasks/IVbcHostObject3.cs create mode 100644 src/XMakeTasks/IVbcHostObject4.cs create mode 100644 src/XMakeTasks/IVbcHostObject5.cs create mode 100644 src/XMakeTasks/IVbcHostObjectFreeThreaded.cs create mode 100644 src/XMakeTasks/InstalledSDKResolver.cs create mode 100644 src/XMakeTasks/InvalidParameterValueException.cs create mode 100644 src/XMakeTasks/LC.cs create mode 100644 src/XMakeTasks/ListOperators/FindUnderPath.cs create mode 100644 src/XMakeTasks/ListOperators/RemoveDuplicates.cs create mode 100644 src/XMakeTasks/MSBuild.cs create mode 100644 src/XMakeTasks/MSBuildTasks.h create mode 100644 src/XMakeTasks/MakeDir.cs create mode 100644 src/XMakeTasks/ManagedCompiler.cs create mode 100644 src/XMakeTasks/ManifestUtil/ApplicationIdentity.cs create mode 100644 src/XMakeTasks/ManifestUtil/ApplicationManifest.cs create mode 100644 src/XMakeTasks/ManifestUtil/AssemblyIdentity.cs create mode 100644 src/XMakeTasks/ManifestUtil/AssemblyManifest.cs create mode 100644 src/XMakeTasks/ManifestUtil/AssemblyReference.cs create mode 100644 src/XMakeTasks/ManifestUtil/AssemblyReferenceCollection.cs create mode 100644 src/XMakeTasks/ManifestUtil/BaseReference.cs create mode 100644 src/XMakeTasks/ManifestUtil/ComImporter.cs create mode 100644 src/XMakeTasks/ManifestUtil/CompatibleFramework.cs create mode 100644 src/XMakeTasks/ManifestUtil/CompatibleFrameworkCollection.cs create mode 100644 src/XMakeTasks/ManifestUtil/Constants.cs create mode 100644 src/XMakeTasks/ManifestUtil/ConvertUtil.cs create mode 100644 src/XMakeTasks/ManifestUtil/DeployManifest.cs create mode 100644 src/XMakeTasks/ManifestUtil/EmbeddedManifestReader.cs create mode 100644 src/XMakeTasks/ManifestUtil/FileAssociation.cs create mode 100644 src/XMakeTasks/ManifestUtil/FileAssociationCollection.cs create mode 100644 src/XMakeTasks/ManifestUtil/FileReference.cs create mode 100644 src/XMakeTasks/ManifestUtil/FileReferenceCollection.cs create mode 100644 src/XMakeTasks/ManifestUtil/Manifest.cs create mode 100644 src/XMakeTasks/ManifestUtil/ManifestFormatter.cs create mode 100644 src/XMakeTasks/ManifestUtil/ManifestReader.cs create mode 100644 src/XMakeTasks/ManifestUtil/ManifestWriter.cs create mode 100644 src/XMakeTasks/ManifestUtil/MetadataReader.cs create mode 100644 src/XMakeTasks/ManifestUtil/NativeMethods.cs create mode 100644 src/XMakeTasks/ManifestUtil/OutputMessage.cs create mode 100644 src/XMakeTasks/ManifestUtil/PathUtil.cs create mode 100644 src/XMakeTasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs create mode 100644 src/XMakeTasks/ManifestUtil/SecurityUtil.cs create mode 100644 src/XMakeTasks/ManifestUtil/Strings.resx create mode 100644 src/XMakeTasks/ManifestUtil/TrustInfo.cs create mode 100644 src/XMakeTasks/ManifestUtil/Util.cs create mode 100644 src/XMakeTasks/ManifestUtil/XPaths.cs create mode 100644 src/XMakeTasks/ManifestUtil/XmlNamespaces.cs create mode 100644 src/XMakeTasks/ManifestUtil/XmlUtil.cs create mode 100644 src/XMakeTasks/ManifestUtil/manifest.xml create mode 100644 src/XMakeTasks/ManifestUtil/mansign.cs create mode 100644 src/XMakeTasks/ManifestUtil/mansign2.cs create mode 100644 src/XMakeTasks/ManifestUtil/merge.xsl create mode 100644 src/XMakeTasks/ManifestUtil/read2.xsl create mode 100644 src/XMakeTasks/ManifestUtil/trustinfo2.xsl create mode 100644 src/XMakeTasks/ManifestUtil/write2.xsl create mode 100644 src/XMakeTasks/ManifestUtil/write3.xsl create mode 100644 src/XMakeTasks/Message.cs create mode 100644 src/XMakeTasks/Microsoft.Build.Tasks.csproj create mode 100644 src/XMakeTasks/Microsoft.CSharp.CurrentVersion.targets create mode 100644 src/XMakeTasks/Microsoft.CSharp.targets create mode 100644 src/XMakeTasks/Microsoft.Common.CurrentVersion.targets create mode 100644 src/XMakeTasks/Microsoft.Common.overridetasks create mode 100644 src/XMakeTasks/Microsoft.Common.props create mode 100644 src/XMakeTasks/Microsoft.Common.targets create mode 100644 src/XMakeTasks/Microsoft.Common.tasks create mode 100644 src/XMakeTasks/Microsoft.Data.Entity.Shim.targets create mode 100644 src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.props create mode 100644 src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.targets create mode 100644 src/XMakeTasks/Microsoft.NETFramework.props create mode 100644 src/XMakeTasks/Microsoft.NETFramework.targets create mode 100644 src/XMakeTasks/Microsoft.ServiceModel.Shim.targets create mode 100644 src/XMakeTasks/Microsoft.VisualBasic.CurrentVersion.targets create mode 100644 src/XMakeTasks/Microsoft.VisualBasic.targets create mode 100644 src/XMakeTasks/Microsoft.VisualStudioVersion.v11.Common.props create mode 100644 src/XMakeTasks/Microsoft.VisualStudioVersion.v12.Common.props create mode 100644 src/XMakeTasks/Microsoft.VisualStudioVersion.v14.Common.props create mode 100644 src/XMakeTasks/Microsoft.WinFx.Shim.targets create mode 100644 src/XMakeTasks/Microsoft.WorkflowBuildExtensions.Shim.targets create mode 100644 src/XMakeTasks/Microsoft.Xaml.Shim.targets create mode 100644 src/XMakeTasks/Move.cs create mode 100644 src/XMakeTasks/NativeMethods.cs create mode 100644 src/XMakeTasks/ParserState.cs create mode 100644 src/XMakeTasks/PiaReference.cs create mode 100644 src/XMakeTasks/RCWForCurrentContext.cs create mode 100644 src/XMakeTasks/RedistList.cs create mode 100644 src/XMakeTasks/RegisterAssembly.cs create mode 100644 src/XMakeTasks/RemoveDir.cs create mode 100644 src/XMakeTasks/RequiresFramework35SP1Assembly.cs create mode 100644 src/XMakeTasks/ResGen.cs create mode 100644 src/XMakeTasks/ResGenDependencies.cs create mode 100644 src/XMakeTasks/ResolveCodeAnalysisRuleSet.cs create mode 100644 src/XMakeTasks/ResolveComReference.cs create mode 100644 src/XMakeTasks/ResolveComReferenceCache.cs create mode 100644 src/XMakeTasks/ResolveKeySource.cs create mode 100644 src/XMakeTasks/ResolveManifestFiles.cs create mode 100644 src/XMakeTasks/ResolveNativeReference.cs create mode 100644 src/XMakeTasks/ResolveNonMSBuildProjectOutput.cs create mode 100644 src/XMakeTasks/ResolveProjectBase.cs create mode 100644 src/XMakeTasks/ResolveSDKReference.cs create mode 100644 src/XMakeTasks/SGen.cs create mode 100644 src/XMakeTasks/SdkToolsPathUtility.cs create mode 100644 src/XMakeTasks/SignFile.cs create mode 100644 src/XMakeTasks/StateFileBase.cs create mode 100644 src/XMakeTasks/Strings.resx create mode 100644 src/XMakeTasks/StrongNameException.cs create mode 100644 src/XMakeTasks/StrongNameUtils.cs create mode 100644 src/XMakeTasks/StronglyTypedResourceBuilder.cs create mode 100644 src/XMakeTasks/System.Design.cs create mode 100644 src/XMakeTasks/SystemState.cs create mode 100644 src/XMakeTasks/TaskExtension.cs create mode 100644 src/XMakeTasks/TlbImp.cs create mode 100644 src/XMakeTasks/TlbReference.cs create mode 100644 src/XMakeTasks/ToolTaskExtension.cs create mode 100644 src/XMakeTasks/Touch.cs create mode 100644 src/XMakeTasks/UnitTests/Al_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AppConfig_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AspNetCompiler_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssemblyIdentity_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssemblyNameEx_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssemblyRefs.cs create mode 100644 src/XMakeTasks/UnitTests/AssemblyRegistrationCache_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssignCulture_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssignLinkMetadata_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssignProjectConfiguration_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AssignTargetPath_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AxImp_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/AxTlbBaseTask_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CSharpParserUtilitites_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CSharpTokenizer_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CallTarget_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CodeTaskFactoryTests.cs create mode 100644 src/XMakeTasks/UnitTests/ComReferenceWalker_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ComReference_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CombinePath_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CommandLineBuilderExtension_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CommandLineGenerator_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CommandLine_Support.cs create mode 100644 src/XMakeTasks/UnitTests/ConvertToAbsolutePath_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Copy_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CreateCSharpManifestResourceName_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CreateItem_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CreateProperty_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/CreateVisualBasicManifestResourceName_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Csc_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Culture_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Delete_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/DependentAssembly_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ErrorWarningMessage_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Exec_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/FileStateTests.cs create mode 100644 src/XMakeTasks/UnitTests/FindAppConfigFile_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/FindInList_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/FindInvalidProjectReferences_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/FindUnderPath_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/GenerateResourceOutOfProc_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/GenerateResource_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/GetInstalledSDKLocations_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/GetReferencePaths_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/GetSDKReference_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/LC_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/MSBuild_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/MakeDir_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ManagedCompiler_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ManifestWriter_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Microsoft.Build.Tasks.UnitTests.csproj create mode 100644 src/XMakeTasks/UnitTests/MockCscHostObject.cs create mode 100644 src/XMakeTasks/UnitTests/MockFaultInjectionHelper.cs create mode 100644 src/XMakeTasks/UnitTests/MockTypeInfo.cs create mode 100644 src/XMakeTasks/UnitTests/MockTypeLib.cs create mode 100644 src/XMakeTasks/UnitTests/MockUnmanagedMemoryHelper.cs create mode 100644 src/XMakeTasks/UnitTests/MockVbcHostObject.cs create mode 100644 src/XMakeTasks/UnitTests/Move_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/PropertyParser_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ReadLinesFromFile_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/RemoveDir_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/RemoveDuplicates_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResGenDependencies_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResGen_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResolveAssemblyReference_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResolveCodeAnalysisRuleSet_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResolveComReference_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResolveNonMSBuildProjectOutput_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResolveSDKReference_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ResolveVCProjectOutput_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/SGen_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/SampleResx create mode 100644 src/XMakeTasks/UnitTests/SdkToolsPathUtility_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/StreamHelpers.cs create mode 100644 src/XMakeTasks/UnitTests/StreamMappedString_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/TlbImp_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/ToolTaskExtension_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Touch_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/TrustInfo_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/Vbc_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/VisualBasicParserUtilitites_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/VisualBasicTokenizer_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/WinMDExp_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/WriteCodeFragment_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/XamlDataDrivenToolTask_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/XamlTaskFactory_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/XamlTestHelpers.cs create mode 100644 src/XMakeTasks/UnitTests/XmlPeek_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/XmlPoke_Tests.cs create mode 100644 src/XMakeTasks/UnitTests/XslTransformation_Tests.cs create mode 100644 src/XMakeTasks/UnregisterAssembly.cs create mode 100644 src/XMakeTasks/UpdateManifest.cs create mode 100644 src/XMakeTasks/Vbc.cs create mode 100644 src/XMakeTasks/VisualBasicParserUtilities.cs create mode 100644 src/XMakeTasks/Warning.cs create mode 100644 src/XMakeTasks/WinMDExp.cs create mode 100644 src/XMakeTasks/Workflow.Shim.Targets create mode 100644 src/XMakeTasks/Workflow.VisualBasic.Shim.Targets create mode 100644 src/XMakeTasks/WriteCodeFragment.cs create mode 100644 src/XMakeTasks/XMakeTasksUnitTests/AssemblyIdentityTest.cs create mode 100644 src/XMakeTasks/XMakeTasksUnitTests/AuthoringTests.txt create mode 100644 src/XMakeTasks/XMakeTasksUnitTests/Properties/AssemblyInfo.cs create mode 100644 src/XMakeTasks/XMakeTasksUnitTests/UtilTest.cs create mode 100644 src/XMakeTasks/XMakeTasksUnitTests/XMakeTasksUnitTests.csproj create mode 100644 src/XMakeTasks/XamlRules/CSharp.BrowseObject.xaml create mode 100644 src/XMakeTasks/XamlRules/CSharp.ProjectItemsSchema.xaml create mode 100644 src/XMakeTasks/XamlRules/CSharp.xaml create mode 100644 src/XMakeTasks/XamlRules/Content.xaml create mode 100644 src/XMakeTasks/XamlRules/Debugger_General.xaml create mode 100644 src/XMakeTasks/XamlRules/EmbeddedResource.xaml create mode 100644 src/XMakeTasks/XamlRules/Folder.xaml create mode 100644 src/XMakeTasks/XamlRules/General.BrowseObject.xaml create mode 100644 src/XMakeTasks/XamlRules/General.xaml create mode 100644 src/XMakeTasks/XamlRules/General_File.xaml create mode 100644 src/XMakeTasks/XamlRules/None.xaml create mode 100644 src/XMakeTasks/XamlRules/ProjectItemsSchema.xaml create mode 100644 src/XMakeTasks/XamlRules/ResolvedAssemblyReference.xaml create mode 100644 src/XMakeTasks/XamlRules/ResolvedCOMReference.xaml create mode 100644 src/XMakeTasks/XamlRules/ResolvedProjectReference.xaml create mode 100644 src/XMakeTasks/XamlRules/Scc.xaml create mode 100644 src/XMakeTasks/XamlRules/SpecialFolder.xaml create mode 100644 src/XMakeTasks/XamlRules/SubProject.xaml create mode 100644 src/XMakeTasks/XamlRules/VisualBasic.BrowseObject.xaml create mode 100644 src/XMakeTasks/XamlRules/VisualBasic.ProjectItemsSchema.xaml create mode 100644 src/XMakeTasks/XamlRules/VisualBasic.xaml create mode 100644 src/XMakeTasks/XamlRules/assemblyreference.xaml create mode 100644 src/XMakeTasks/XamlRules/comreference.xaml create mode 100644 src/XMakeTasks/XamlRules/projectreference.xaml create mode 100644 src/XMakeTasks/XamlTaskFactory/CommandLineGenerator.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/CommandLineToolSwitch.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/Property.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/RelationsParser.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/TaskGenerator.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/TaskParser.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/XamlDataDrivenToolTask.cs create mode 100644 src/XMakeTasks/XamlTaskFactory/XamlTaskFactory.cs create mode 100644 src/XMakeTasks/XmlPeek.cs create mode 100644 src/XMakeTasks/XmlPoke.cs create mode 100644 src/XMakeTasks/XslTransformation.cs create mode 100644 src/XMakeTasks/native.rc create mode 100644 src/XMakeTasks/system.design/stronglytypedresourcebuilder.cs create mode 100644 src/XMakeTasks/system.design/system.design.txt create mode 100644 src/dir.props create mode 100644 src/dir.targets create mode 100644 src/dirs.proj diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..7bf42d03b17 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,80 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### + +[attr]wintext text eol=crlf + +* text=auto + +# Default +*.* wintext + +# Build Specifications +*.sln wintext +*.props wintext +*.proj wintext +*.targets wintext +*.tasks wintext +*.overridetasks wintext +*.csproj wintext +*.rptproj wintext +*.vcxproj wintext +*.filters wintext +*.cd wintext + +# Sources (Managed) +*.cs wintext +*.vb wintext +*.resx wintext +*.xaml wintext +*.config wintext +*.manifest wintext +*.txt wintext +*.tst wintext + +# Sources (Web) +*.html wintext +*.css wintext +*.js wintext + +# Sources (Native) +*.h wintext +*.cpp wintext +*.rc wintext + +# Sources (Xml) +*.xml wintext +*.xsl wintext +*.xsd wintext + +# Sources (Script) +*.cmd wintext +*.bat wintext +*.rsp wintext +*.sample wintext + + +# Sources (Binary) +*.doc binary +*.ico binary +*.appx binary + +# Configuration +*.gitattributes wintext +*.gitignore wintext + +#Binaries +*.exe binary +*.dll binary +*.obj binary +*.pdb binary +*.zip binary +*.chm binary +*.nupkg binary +*.lex binary +*.ocx binary +*.mui binary + +#Images (Binary) +*.png binary +*.jpg binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..fd5204b5f64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# If using the old MSBuild-Integrated Package Restore, uncomment this: +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ diff --git a/BuildAndCopy.cmd b/BuildAndCopy.cmd new file mode 100644 index 00000000000..7b7306acbef --- /dev/null +++ b/BuildAndCopy.cmd @@ -0,0 +1,59 @@ +:: Usage: +:: BuildAndCopy +:: - Where to copy the build output +:: - true to have MSBuild target Microsoft.Build.Framework +:: +:: Example: BuildAndCopy.cmd bin\MSBuild false + +@echo off +setlocal + +set MSBuild14Path=%ProgramFiles(x86)%\MSBuild\14.0\Bin +set DebugBuildOutputPath=%~dp0bin\Windows_NT\Debug +set OutputPath=%~dp0bin\MSBuild + +:: Check prerequisites +if not defined VS140COMNTOOLS ( + echo Error: This script should be run from a Visual Studio 2015 Command Prompt. + echo Please see https://github.com/Microsoft/msbuild/wiki/Building-Testing-and-Debugging for build instructions. + exit /b 1 +) + +if not "%1"=="" ( + set OutputPath=%1 + if "%2"=="true" ( + set AdditionalBuildCommand= /p:TargetRetailBuildFramework=true + ) +) + +echo ** Creating a build package +echo ** Output Path: %OutputPath% +echo ** Additional Build Parameters:%AdditionalBuildCommand% +echo. +:: Build MSBuild +call "%~dp0build.cmd" /t:Rebuild %AdditionalBuildCommand% + +:: Make a copy of our build +echo ** ROBOCOPY bin\Windows_NT\Debug -^> %OutputPath% +robocopy "%DebugBuildOutputPath%" "%OutputPath%" *.* /S /NFL /NDL /NJH /NJS /nc /ns /np +echo. + +:: This is a bit hacky, but we need to copy certain dependencies. +:: The files needed are defined in MSBuildLocalSystemDependencies.txt (one per line) +:: Note: Files may be in use if the compiler is still running (this is generally ok) +echo ** Copying required dependencies from MSBuild 14.0 +for /F "tokens=*" %%A in (MSBuildLocalSystemDependencies.txt) do ( + robocopy "%MSBuild14Path%" "%OutputPath%" %%A /NFL /NDL /NJH /NJS /nc /ns /np>nul +) +echo. + +:: Delete the copy of Microsoft.Build.Framework.dll we built so there are no conflicts +if "%2"=="true" ( + echo ** Deleting Microsoft.Build.Framework.dll we built in favor of the Retail version. + del %OutputPath%\Microsoft.Build.Framework.* +) + +echo. +echo ** Packaging complete. +set MSBUILDCUSTOMPATH="%OutputPath%\MSBuild.exe" +echo ** MSBuild = %MSBUILDCUSTOMPATH% \ No newline at end of file diff --git a/MSBuildLocalSystemDependencies.txt b/MSBuildLocalSystemDependencies.txt new file mode 100644 index 00000000000..4bea7e0c2e3 --- /dev/null +++ b/MSBuildLocalSystemDependencies.txt @@ -0,0 +1,13 @@ +csc2.exe +Microsoft.CodeAnalysis.CSharp.Desktop.dll +Microsoft.CodeAnalysis.CSharp.dll +Microsoft.CodeAnalysis.dll +Microsoft.CodeAnalysis.Desktop.dll +Microsoft.CodeAnalysis.VisualBasic.Desktop.dll +Microsoft.CodeAnalysis.VisualBasic.dll +System.Collections.Immutable.dll +System.Reflection.Metadata.dll +Tracker.exe +vbc2.exe +VBCSCompiler.exe +VBCSCompiler.exe.config \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000000..586436e7ebe --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Microsoft.Build (MSBuild) +The Microsoft Build Engine is a platform for building applications. This engine, which is also known as MSBuild, provides an XML schema for a project file that controls how the build platform processes and builds software. Visual Studio uses MSBuild, but it doesn't depend on Visual Studio. By invoking msbuild.exe on your project or solution file, you can orchestrate and build products in environments where Visual Studio isn't installed. + +For more information on MSBuild, see the [MSDN documentation](https://msdn.microsoft.com/en-us/library/dd393574(v=vs.120).aspx). + +### Source code + +* Clone the sources: `git clone https://github.com/Microsoft/msbuild.git` + +### Building +For the full supported experience, you will need to have Visual Studio 2015 Preview or later. You can open the solution in Visual Studio 2013, but you will encounter issues building with the provided scripts. + +To get started on **Visual Studio 2015 Preview**: + +1. Set up a box with Visual Studio 2015 Preview. Either +[install Visual Studio 2015 Preview](http://www.visualstudio.com/en-us/downloads/visual-studio-2015-downloads-vs), +or grab a [prebuilt Azure VM image](http://blogs.msdn.com/b/visualstudioalm/archive/2014/06/04/visual-studio-14-ctp-now-available-in-the-virtual-machine-azure-gallery.aspx). +2. Clone the source code (see above). +3. Open src/MSBuild.sln solution in Visual Studio 2015. + +## How to Engage, Contribute and Provide Feedback +Before you contribute, please read through the contributing and developer guides to get an idea of what kinds of pull requsts we will or won't accept. + +* [Contributing Guide](https://github.com/Microsoft/msbuild/wiki/Contributing-Code) +* [Developer Guide](https://github.com/Microsoft/msbuild/wiki/Building-Testing-and-Debugging) + +Want to get more familiar with what's going on in the code? +* [Pull requests](https://github.com/Microsoft/msbuild/pulls): [Open](https://github.com/Microsoft/msbuild/pulls?q=is%3Aopen+is%3Apr)/[Closed](https://github.com/Microsoft/msbuild/pulls?q=is%3Apr+is%3Aclosed) +* [Issue Guide](https://github.com/Microsoft/msbuild/wiki/Issue-Guide) + +You are also encouraged to start a discussion by filing an issue or creating a gist. + +## MSBuild Components + +* **MSBuild**. [Microsoft.Build.CommandLine](https://msdn.microsoft.com/en-us/library/dd393574(v=vs.120).aspx) is the entrypoint for the Microsoft Build Engine (MSBuild.exe). + +* **Microsoft.Build**. The [Microsoft.Build](https://msdn.microsoft.com/en-us/library/gg145008(v=vs.120).aspx) namespaces contain types that provide programmatic access to, and control of, the MSBuild engine. + +* **Microsoft.Build.Framework**. The [Microsoft.Build.Framework](https://msdn.microsoft.com/en-us/library/microsoft.build.framework(v=vs.120).aspx) namespace contains the types that define how tasks and loggers interact with the MSBuild engine. For additional information on this component, see our [Microsoft.Build.Framework wiki page](https://github.com/Microsoft/msbuild/wiki/Microsoft.Build.Framework). + +* **Microsoft.Build.Tasks**. The [Microsoft.Build.Tasks](https://msdn.microsoft.com/en-us/library/microsoft.build.tasks(v=vs.120).aspx) namespace contains the implementation of all tasks shipping with MSBuild. + +* **Microsoft.Build.Utilities**. The [Microsoft.Build.Utilities](https://msdn.microsoft.com/en-us/library/microsoft.build.utilities(v=vs.120).aspx) namespace provides helper classes that you can use to create your own MSBuild loggers and tasks. + +## License + +MSBuild is licensed under the [MIT license](LICENSE). diff --git a/RebuildWithLocalMSBuild.cmd b/RebuildWithLocalMSBuild.cmd new file mode 100644 index 00000000000..bea6dbf2852 --- /dev/null +++ b/RebuildWithLocalMSBuild.cmd @@ -0,0 +1,24 @@ +:: This script will: +:: 1) Rebuild MSBuild source tree. +:: 2) Create a copy of the build output in bin\MSBuild +:: 3) Build the source tree again with the MSBuild.exe in step 2. + +@echo off +setlocal + +set MSBuildTempPath=%~dp0bin\MSBuild + +:: Check prerequisites +if not defined VS140COMNTOOLS ( + echo Error: This script should be run from a Visual Studio 2015 Command Prompt. + echo Please see https://github.com/Microsoft/msbuild/wiki/Developer-Guide for build instructions. + exit /b 1 +) + +:: Build and copy output to bin\MSBuild +:: Set TargetRetailBuildFramework to false so that we can target our own version of Microsoft.Build.Framework +call "%~dp0BuildAndCopy.cmd" "%MSBuildTempPath%" false + +:: Rebuild +set MSBUILDCUSTOMPATH=%MSBuildTempPath%\MSBuild.exe +"%~dp0build.cmd" /t:Rebuild \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 00000000000..a601c4c43ee --- /dev/null +++ b/build.cmd @@ -0,0 +1,30 @@ +@echo off +setlocal + +:: Check prerequisites +if not "%VisualStudioVersion%" == "14.0" ( + echo Error: build.cmd should be run from a Visual Studio 2015 Command Prompt. + echo Please see https://github.com/Microsoft/msbuild/wiki/Building-Testing-and-Debugging for build instructions. + exit /b 1 +) + +:: Check for a custom MSBuild path. If not defined, default to the one in your path. +if not defined MSBUILDCUSTOMPATH ( + set MSBUILDCUSTOMPATH=MSBuild.exe +) + + +echo ** MSBuild Path: %MSBUILDCUSTOMPATH% +echo ** Building all sources + +:: Call MSBuild +echo ** "%MSBUILDCUSTOMPATH%" "%~dp0build.proj" /maxcpucount /verbosity:minimal /nodeReuse:false /fileloggerparameters:Verbosity=diag;LogFile="%~dp0msbuild.log" %* +"%MSBUILDCUSTOMPATH%" "%~dp0build.proj" /maxcpucount /verbosity:minimal /nodeReuse:false /fileloggerparameters:Verbosity=diag;LogFile="%~dp0msbuild.log" %* +set BUILDERRORLEVEL=%ERRORLEVEL% +echo. + +:: Pull the build summary from the log file +findstr /ir /c:".*Warning(s)" /c:".*Error(s)" /c:"Time Elapsed.*" "%~dp0msbuild.log" +echo ** Build completed. Exit code: %BUILDERRORLEVEL% + +exit /b %BUILDERRORLEVEL% diff --git a/build.proj b/build.proj new file mode 100644 index 00000000000..30bf40137af --- /dev/null +++ b/build.proj @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/dir.props b/dir.props new file mode 100644 index 00000000000..1d5e393ffec --- /dev/null +++ b/dir.props @@ -0,0 +1,69 @@ + + + v4.5.1 + + + + + $(MSBuildThisFileDirectory) + $(ProjectDir)src\ + $(ProjectDir)bin\ + $(BinDir)tests\ + $(ProjectDir)packages\ + $(PackagesDir)Microsoft.DotNet.BuildTools.$(BuildToolsVersion)\lib\ + + + + + $(PackagesDir)NuGet.exe + $(SourceDir).nuget\NuGet.Config + -ConfigFile "$(NuGetConfigFile)" + + "$(NuGetToolPath)" + $(NugetRestoreCommand) install + $(NugetRestoreCommand) -OutputDirectory "$(PackagesDir.TrimEnd('\'))" + $(NugetRestoreCommand) $(NuGetConfigCommandLine) + + + + + Debug + AnyCPU + + + + + true + false + full + $(DefineConstants);DEBUG;TRACE;STANDALONEBUILD + + + true + true + pdbonly + $(DefineConstants);TRACE;STANDALONEBUILD + + + + + $(SourceDir)Common\src + $(SourceDir)Common\tests + + + + + $(ProjectDir)bin\ + $(BaseOutputPath)$(OS)\$(Configuration)\ + $(BaseOutputPath)$(Platform)\$(OS)\$(Configuration)\ + $(BaseOutputPathWithConfig)\ + + $(BaseOutputPath)obj\ + $(BaseIntermediateOutputPath)$(MSBuildProjectName)\$(OS)\$(Configuration)\ + $(BaseIntermediateOutputPath)$(MSBuildProjectName)\$(Platform)\$(OS)\$(Configuration)\ + + $(TestWorkingDir)$(MSBuildProjectName)\$(OS)\$(Configuration)\ + $(TestWorkingDir)$(MSBuildProjectName)\$(Platform)\$(OS)\$(Configuration)\ + + + diff --git a/dir.targets b/dir.targets new file mode 100644 index 00000000000..99bf2b1913a --- /dev/null +++ b/dir.targets @@ -0,0 +1,61 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dir.traversal.targets b/dir.traversal.targets new file mode 100644 index 00000000000..4298bcd7bd5 --- /dev/null +++ b/dir.traversal.targets @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + BuildAllProjects; + $(TraversalBuildDependsOn); + + + + CleanAllProjects; + $(TraversalCleanDependsOn); + + + + + + + + + + \ No newline at end of file diff --git a/src/.nuget/NuGet.Config b/src/.nuget/NuGet.Config new file mode 100644 index 00000000000..cbdb3d5e5c5 --- /dev/null +++ b/src/.nuget/NuGet.Config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/.nuget/packages.config b/src/.nuget/packages.config new file mode 100644 index 00000000000..33a50d6e422 --- /dev/null +++ b/src/.nuget/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/Framework/AssemblyInfo.cs b/src/Framework/AssemblyInfo.cs new file mode 100644 index 00000000000..f6fce6d4471 --- /dev/null +++ b/src/Framework/AssemblyInfo.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Assembly info. +//----------------------------------------------------------------------- + +using System.Reflection; +using System.Security.Permissions; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Windows.Markup; + +// A combination of RequestMinimum and RequestOptional causes the permissions granted to +// the assembly to only be the permission requested (like a PermitOnly). More generally +// the equation for the PermissionSet granted at load time is: +// +// Granted = (MaxGrant intersect (ReqMin union ReqOpt)) - ReqRefuse +// +// Where, +// MaxGrant -- the permissions granted by policy. +// ReqMin -- the permissions that RequestMinimum is specified for. +// ReqOpt -- the permissions that RequestOptional is specified for. +// ReqRefuse -- the permissions that Request refuse is specified for. +// +// Note that if ReqOpt is the empty set, then it is consider to be "FullTrust" and this +// equation becomes: +// +// Granted = MaxGrant - ReqRefuse +// +// Regardless of whether ReqMin is empty or not. +#pragma warning disable 618 +[assembly: SecurityPermission(SecurityAction.RequestMinimum, Flags = SecurityPermissionFlag.Execution)] +#pragma warning restore 618 +[assembly: InternalsVisibleTo("Microsoft.Build.Framework.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Framework.Whidbey.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +#if STANDALONEBUILD +[assembly: AssemblyVersion("14.1.0.0")] +[assembly: InternalsVisibleTo("Microsoft.Build.Framework.UnitTests")] +[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.UnitTests")] +#endif + +// This is the assembly-level GUID, and the GUID for the TypeLib associated with +// this assembly. We should specify this explicitly, as opposed to letting +// tlbexp just pick whatever it wants. +[assembly: GuidAttribute("D8A9BA71-4724-481d-9CA7-0DA23A1D615C")] + +[assembly: XmlnsDefinition("http://schemas.microsoft.com/build/2009/properties", "Microsoft.Build.Framework.XamlTypes")] + +// This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, +// so that we don't run into known security issues with loading libraries from unsafe locations +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/Framework/BuildEngineResult.cs b/src/Framework/BuildEngineResult.cs new file mode 100644 index 00000000000..74c6883d67c --- /dev/null +++ b/src/Framework/BuildEngineResult.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Structure which includes the success or failures of the IBuildEngine build and the target outputs. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// This structure is used to return the result of the build and the target outputs. + /// + [Serializable] + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Would require a public API change -- currently we're trying to keep our surface area static.")] + public struct BuildEngineResult + { + /// + /// Did the build pass or fail + /// + private bool _buildResult; + + /// + /// Target outputs by project + /// + private List> _targetOutputsPerProject; + + /// + /// The constructor takes the result of the build and a list of the target outputs per project + /// + public BuildEngineResult(bool result, List> targetOutputsPerProject) + { + _buildResult = result; + _targetOutputsPerProject = targetOutputsPerProject; + if (_targetOutputsPerProject == null) + { + _targetOutputsPerProject = new List>(); + } + } + + /// + /// Did the build pass or fail. True means the build succeeded, False means the build failed. + /// + public bool Result + { + get + { + return _buildResult; + } + } + + /// + /// Outputs of the targets per project. + /// + public IList> TargetOutputsPerProject + { + get + { + return _targetOutputsPerProject; + } + } + } +} diff --git a/src/Framework/BuildErrorEventArgs.cs b/src/Framework/BuildErrorEventArgs.cs new file mode 100644 index 00000000000..ab3b8a3645f --- /dev/null +++ b/src/Framework/BuildErrorEventArgs.cs @@ -0,0 +1,394 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for any build event. +//----------------------------------------------------------------------- + +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for error events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class BuildErrorEventArgs : LazyFormattedBuildEventArgs + { + /// + /// Subcategory of the error + /// + private string _subcategory; + + /// + /// Error code + /// + private string _code; + + /// + /// File name + /// + private string _file; + + /// + /// The project which issued the event + /// + private string _projectFile; + + /// + /// Line number + /// + private int _lineNumber; + + /// + /// Column number + /// + private int _columnNumber; + + /// + /// End line number + /// + private int _endLineNumber; + + /// + /// End column number + /// + private int _endColumnNumber; + + /// + /// This constructor allows all event data to be initialized + /// + /// event sub-category + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + public BuildErrorEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, DateTime.UtcNow) + { + } + + /// + /// This constructor which allows a timestamp to be set + /// + /// event sub-category + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// Timestamp when event was created + public BuildErrorEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constructor which allows a timestamp to be set + /// + /// event sub-category + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// Timestamp when event was created + /// message arguments + public BuildErrorEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, senderName, eventTimestamp, messageArgs) + { + _subcategory = subcategory; + _code = code; + _file = file; + _lineNumber = lineNumber; + _columnNumber = columnNumber; + _endLineNumber = endLineNumber; + _endColumnNumber = endColumnNumber; + } + + /// + /// Default constructor + /// + protected BuildErrorEventArgs() + : base() + { + // do nothing + } + + /// + /// The custom sub-type of the event. + /// + public string Subcategory + { + get + { + return _subcategory; + } + } + + /// + /// Code associated with event. + /// + public string Code + { + get + { + return _code; + } + } + + /// + /// File associated with event. + /// + public string File + { + get + { + return _file; + } + } + + /// + /// The project file which issued this event. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + + set + { + _projectFile = value; + } + } + + /// + /// Line number of interest in associated file. + /// + public int LineNumber + { + get + { + return _lineNumber; + } + } + + /// + /// Column number of interest in associated file. + /// + public int ColumnNumber + { + get + { + return _columnNumber; + } + } + + /// + /// Ending line number of interest in associated file. + /// + public int EndLineNumber + { + get + { + return _endLineNumber; + } + } + + /// + /// Ending column number of interest in associated file. + /// + public int EndColumnNumber + { + get + { + return _endColumnNumber; + } + } + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + #region SubCategory + if (_subcategory == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_subcategory); + } + #endregion + #region Code + if (_code == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_code); + } + #endregion + #region File + if (_file == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_file); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + writer.Write((Int32)_lineNumber); + writer.Write((Int32)_columnNumber); + writer.Write((Int32)_endLineNumber); + writer.Write((Int32)_endColumnNumber); + } + + /// + /// Deserializes to a stream through a binary writer + /// + /// Binary reader which the object will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + #region SubCategory + if (reader.ReadByte() == 0) + { + _subcategory = null; + } + else + { + _subcategory = reader.ReadString(); + } + #endregion + #region Code + if (reader.ReadByte() == 0) + { + _code = null; + } + else + { + _code = reader.ReadString(); + } + #endregion + #region File + if (reader.ReadByte() == 0) + { + _file = null; + } + else + { + _file = reader.ReadString(); + } + #endregion + #region ProjectFile + if (version > 20) + { + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + } + else + { + _projectFile = null; + } + #endregion + _lineNumber = reader.ReadInt32(); + _columnNumber = reader.ReadInt32(); + _endLineNumber = reader.ReadInt32(); + _endColumnNumber = reader.ReadInt32(); + } + #endregion + + } +} diff --git a/src/Framework/BuildEventArgs.cs b/src/Framework/BuildEventArgs.cs new file mode 100644 index 00000000000..c72e5a19158 --- /dev/null +++ b/src/Framework/BuildEventArgs.cs @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for any build event. +//----------------------------------------------------------------------- + +using System; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// This class encapsulates the default data associated with build events. + /// It is intended to be extended/sub-classed. + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public abstract class BuildEventArgs : EventArgs + { + /// + /// Message + /// + private string _message; + + /// + /// Help keyword + /// + private string _helpKeyword; + + /// + /// Sender name + /// + private string _senderName; + + /// + /// Timestamp + /// + private DateTime _timestamp; + + /// + /// Thread id + /// + private int _threadId; + + /// + /// Build event context + /// + [OptionalField(VersionAdded = 2)] + private BuildEventContext _buildEventContext; + + /// + /// Default constructor + /// + protected BuildEventArgs() + : this(null, null, null, DateTime.UtcNow) + { + } + + /// + /// This constructor allows all event data to be initialized + /// + /// text message + /// help keyword + /// name of event sender + protected BuildEventArgs(string message, string helpKeyword, string senderName) + : this(message, helpKeyword, senderName, DateTime.UtcNow) + { + } + + /// + /// This constructor allows all event data to be initialized while providing a custom timestamp. + /// + /// text message + /// help keyword + /// name of event sender + /// TimeStamp of when the event was created + protected BuildEventArgs(string message, string helpKeyword, string senderName, DateTime eventTimestamp) + { + _message = message; + _helpKeyword = helpKeyword; + _senderName = senderName; + _timestamp = eventTimestamp; + _threadId = System.Threading.Thread.CurrentThread.GetHashCode(); + } + + /// + /// The time when event was raised. + /// + public DateTime Timestamp + { + get + { + // Rather than storing dates in Local time all the time, we store in UTC type, and only + // convert to Local when the user requests access to this field. This lets us avoid the + // expensive conversion to Local time unless it's absolutely necessary. + if (_timestamp.Kind == DateTimeKind.Utc) + { + _timestamp = _timestamp.ToLocalTime(); + } + + return _timestamp; + } + } + + /// + /// The thread that raised event. + /// + public int ThreadId + { + get + { + return _threadId; + } + } + + /// + /// Text of event. + /// + public virtual string Message + { + get + { + return _message; + } + + protected set + { + _message = value; + } + } + + /// + /// Custom help keyword associated with event. + /// + public string HelpKeyword + { + get + { + return _helpKeyword; + } + } + + /// + /// Name of the object sending this event. + /// + public string SenderName + { + get + { + return _senderName; + } + } + + /// + /// Event contextual information for the build event argument + /// + public BuildEventContext BuildEventContext + { + get + { + return _buildEventContext; + } + + set + { + _buildEventContext = value; + } + } + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal virtual void WriteToStream(BinaryWriter writer) + { + #region Message + if (_message == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_message); + } + #endregion + #region HelpKeyword + if (_helpKeyword == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_helpKeyword); + } + #endregion + #region SenderName + if (_senderName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_senderName); + } + #endregion + #region TimeStamp + writer.Write((Int64)_timestamp.Ticks); + writer.Write((Int32)_timestamp.Kind); + #endregion + writer.Write((Int32)_threadId); + #region BuildEventContext + if (_buildEventContext == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write((Int32)_buildEventContext.NodeId); + writer.Write((Int32)_buildEventContext.ProjectContextId); + writer.Write((Int32)_buildEventContext.TargetId); + writer.Write((Int32)_buildEventContext.TaskId); + writer.Write((Int32)_buildEventContext.SubmissionId); + writer.Write((Int32)_buildEventContext.ProjectInstanceId); + } + #endregion + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal virtual void CreateFromStream(BinaryReader reader, int version) + { + #region Message + if (reader.ReadByte() == 0) + { + _message = null; + } + else + { + _message = reader.ReadString(); + } + #endregion + #region HelpKeyword + if (reader.ReadByte() == 0) + { + _helpKeyword = null; + } + else + { + _helpKeyword = reader.ReadString(); + } + #endregion + #region SenderName + if (reader.ReadByte() == 0) + { + _senderName = null; + } + else + { + _senderName = reader.ReadString(); + } + #endregion + #region TimeStamp + long timestampTicks = reader.ReadInt64(); + if (version > 20) + { + DateTimeKind kind = (DateTimeKind)reader.ReadInt32(); + _timestamp = new DateTime(timestampTicks, kind); + } + else + { + _timestamp = new DateTime(timestampTicks); + } + #endregion + _threadId = reader.ReadInt32(); + #region BuildEventContext + if (reader.ReadByte() == 0) + { + _buildEventContext = null; + } + else + { + int nodeId = reader.ReadInt32(); + int projectContextId = reader.ReadInt32(); + int targetId = reader.ReadInt32(); + int taskId = reader.ReadInt32(); + + if (version > 20) + { + int submissionId = reader.ReadInt32(); + int projectInstanceId = reader.ReadInt32(); + _buildEventContext = new BuildEventContext(submissionId, nodeId, projectInstanceId, projectContextId, targetId, taskId); + } + else + { + _buildEventContext = new BuildEventContext(nodeId, targetId, projectContextId, taskId); + } + } + #endregion + } + #endregion + #region SetSerializationDefaults + + /// + /// Run before the object has been deserialized + /// UNDONE (Logging.) Can this and the next function go away, and instead return a BuildEventContext.Invalid from + /// the property if the buildEventContext field is null? + /// + [OnDeserializing] + private void SetBuildEventContextDefaultBeforeSerialization(StreamingContext sc) + { + // Dont want to create a new one here as default all the time as that would be a lot of + // possibly useless allocations + _buildEventContext = null; + } + + /// + /// Run after the object has been deserialized + /// + [OnDeserialized] + private void SetBuildEventContextDefaultAfterSerialization(StreamingContext sc) + { + if (_buildEventContext == null) + { + _buildEventContext = new BuildEventContext + ( + BuildEventContext.InvalidNodeId, + BuildEventContext.InvalidTargetId, + BuildEventContext.InvalidProjectContextId, + BuildEventContext.InvalidTaskId + ); + } + } + #endregion + } +} + diff --git a/src/Framework/BuildEventContext.cs b/src/Framework/BuildEventContext.cs new file mode 100644 index 00000000000..9862e88fa33 --- /dev/null +++ b/src/Framework/BuildEventContext.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Globalization; + +namespace Microsoft.Build.Framework +{ + /// + /// Will provide location information for an event, this is especially + /// needed in a multi processor environment + /// + [Serializable] + public class BuildEventContext + { + #region Data + + /// + /// Node event was in + /// + private int _nodeId; + + /// + /// Target event was in + /// + private int _targetId; + + /// + ///The node-unique project request context the event was in + /// + private int _projectContextId; + + /// + /// Id of the task the event was caused from + /// + private int _taskId; + + /// + /// The id of the project instance to which this event refers. + /// + private int _projectInstanceId; + + /// + /// The id of the submission. + /// + private int _submissionId; + + #endregion + + #region Constructor + + /// + /// This is the original constructor. No one should ever use this except internally for backward compatibility. + /// + public BuildEventContext + ( + int nodeId, + int targetId, + int projectContextId, + int taskId + ) + : this(InvalidSubmissionId, nodeId, InvalidProjectInstanceId, projectContextId, targetId, taskId) + { + // UNDONE: This is obsolete. + } + + /// + /// Constructs a BuildEventContext with a specified project instance id. + /// + public BuildEventContext + ( + int nodeId, + int projectInstanceId, + int projectContextId, + int targetId, + int taskId + ) + : this(InvalidSubmissionId, nodeId, projectInstanceId, projectContextId, targetId, taskId) + { + } + + /// + /// Constructs a BuildEventContext with a specific submission id + /// + public BuildEventContext + ( + int submissionId, + int nodeId, + int projectInstanceId, + int projectContextId, + int targetId, + int taskId + ) + { + _submissionId = submissionId; + _nodeId = nodeId; + _targetId = targetId; + _projectContextId = projectContextId; + _taskId = taskId; + _projectInstanceId = projectInstanceId; + } + + #endregion + + #region Properties + + /// + /// Returns a default invalid BuildEventContext + /// + public static BuildEventContext Invalid + { + get + { + return new BuildEventContext(BuildEventContext.InvalidNodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + } + } + + /// + /// NodeId where event took place + /// + public int NodeId + { + get + { + return _nodeId; + } + } + + /// + /// Id of the target the event was in when the event was fired + /// + public int TargetId + { + get + { + return _targetId; + } + } + + /// + /// Retrieves the Project Context id. + /// + public int ProjectContextId + { + get + { + return _projectContextId; + } + } + + /// + /// Retrieves the task id. + /// + public int TaskId + { + get + { + return _taskId; + } + } + + /// + /// Retrieves the project instance id. + /// + public int ProjectInstanceId + { + get + { + return _projectInstanceId; + } + } + + /// + /// Retrieves the Submission id. + /// + public int SubmissionId + { + get + { + return _submissionId; + } + } + + /// + /// Retrieves the BuildRequest id. Note that this is not the same as the global request id on a BuildRequest or BuildResult. + /// + public long BuildRequestId + { + get + { + return ((long)_nodeId << 32) + _projectContextId; + } + } + + #endregion + + #region Constants + public const int InvalidProjectContextId = -2; + public const int InvalidTaskId = -1; + public const int InvalidTargetId = -1; + public const int InvalidNodeId = -2; + public const int InvalidProjectInstanceId = -1; + public const int InvalidSubmissionId = -1; + #endregion + + #region Equals + + /// + /// Retrieves a hash code for this BuildEventContext. + /// + /// + public override int GetHashCode() + { + return (ProjectContextId + (NodeId << 24)); + } + + /// + /// Compare a BuildEventContext with this BuildEventContext. + /// A build event context is compared in the following way. + /// + /// 1. If the obect references are the same the contexts are equivilant + /// 2. If the object type is the same and the Id values in the context are the same, the contexts are equivilant + /// + /// + /// + public override bool Equals(object obj) + { + // If the references are the same no need to do any more comparing + if (object.ReferenceEquals(this, obj)) + { + return true; + } + + if (object.ReferenceEquals(obj, null)) + { + return false; + } + + // The types do not match, they cannot be the same + if (this.GetType() != obj.GetType()) + { + return false; + } + + return InternalEquals((BuildEventContext)obj); + } + /// + /// Override == so the equals comparison using this operator will be the same as + /// .Equals + /// + /// Left hand side operand + /// Right hand side operand + /// True if the object values are identical, false if they are not identical + public static bool operator ==(BuildEventContext left, BuildEventContext right) + { + if (Object.ReferenceEquals(left, right)) + { + return true; + } + + if (Object.ReferenceEquals(left, null)) + { + return false; + } + + return left.Equals(right); + } + + /// + /// Override != so the equals comparison using this operator will be the same as + /// ! Equals + /// + /// Left hand side operand + /// Right hand side operand + /// True if the object values are not identical, false if they are identical + public static bool operator !=(BuildEventContext left, BuildEventContext right) + { + return !(left == right); + } + + /// + /// Verify the fields are identical + /// + /// BuildEventContext to compare to this instance + /// True if the value fields are the same, false if otherwise + private bool InternalEquals(BuildEventContext buildEventContext) + { + return ((_nodeId == buildEventContext.NodeId) + && (_projectContextId == buildEventContext.ProjectContextId) + && (_targetId == buildEventContext.TargetId) + && (_taskId == buildEventContext.TaskId)); + } + #endregion + + } +} diff --git a/src/Framework/BuildFinishedEventArgs.cs b/src/Framework/BuildFinishedEventArgs.cs new file mode 100644 index 00000000000..7f9e9a7a121 --- /dev/null +++ b/src/Framework/BuildFinishedEventArgs.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for any build event. +//----------------------------------------------------------------------- + +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// This class represents the event arguments for build finished events. + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class BuildFinishedEventArgs : BuildStatusEventArgs + { + /// + /// Whether the build succeeded + /// + private bool _succeeded; + + /// + /// Default constructor + /// + protected BuildFinishedEventArgs() + : base() + { + // do nothing + } + + /// + /// Constructor to initialize all paramterers. + /// Sender field cannot be set here and is assumed to be "MSBuild" + /// + /// text message + /// help keyword + /// True indicates a successful build + public BuildFinishedEventArgs + ( + string message, + string helpKeyword, + bool succeeded + ) + : this(message, helpKeyword, succeeded, DateTime.UtcNow) + { + } + + /// + /// Constructor which allows the timestamp to be set + /// + /// text message + /// help keyword + /// True indicates a successful build + /// Timestamp when the event was created + public BuildFinishedEventArgs + ( + string message, + string helpKeyword, + bool succeeded, + DateTime eventTimestamp + ) + : this(message, helpKeyword, succeeded, eventTimestamp, null) + { + // do nothing + } + + /// + /// Constructor which allows the timestamp to be set + /// + /// text message + /// help keyword + /// True indicates a successful build + /// Timestamp when the event was created + /// message arguments + public BuildFinishedEventArgs + ( + string message, + string helpKeyword, + bool succeeded, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp, messageArgs) + { + _succeeded = succeeded; + } + + + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + writer.Write(_succeeded); + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + _succeeded = reader.ReadBoolean(); + } + #endregion + /// + /// Succeeded is true if the build succeeded; false otherwise. + /// + public bool Succeeded + { + get + { + return _succeeded; + } + } + } +} diff --git a/src/Framework/BuildMessageEventArgs.cs b/src/Framework/BuildMessageEventArgs.cs new file mode 100644 index 00000000000..f662b21c56d --- /dev/null +++ b/src/Framework/BuildMessageEventArgs.cs @@ -0,0 +1,485 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for any build event. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Framework +{ + /// + /// This enumeration provides three levels of importance for messages. + /// + /// + /// + [Serializable] + public enum MessageImportance + { + /// + /// High importance, appears in less verbose logs + /// + High, + + /// + /// Normal importance + /// + Normal, + + /// + /// Low importance, appears in more verbose logs + /// + Low + } + + /// + /// Arguments for message events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class BuildMessageEventArgs : LazyFormattedBuildEventArgs + { + /// + /// Default constructor + /// + protected BuildMessageEventArgs() + : base() + { + // do nothing + } + + /// + /// This constuctor allows all event data to be initialized + /// + /// text message + /// help keyword + /// name of event sender + /// importance of the message + public BuildMessageEventArgs + ( + string message, + string helpKeyword, + string senderName, + MessageImportance importance + ) + : this(message, helpKeyword, senderName, importance, DateTime.UtcNow) + { + } + + /// + /// This constuctor allows a timestamp to be set + /// + /// text message + /// help keyword + /// name of event sender + /// importance of the message + /// Timestamp when event was created + public BuildMessageEventArgs + ( + string message, + string helpKeyword, + string senderName, + MessageImportance importance, + DateTime eventTimestamp + ) + : this(message, helpKeyword, senderName, importance, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constuctor allows a timestamp to be set + /// + /// text message + /// help keyword + /// name of event sender + /// importance of the message + /// Timestamp when event was created + /// message arguments + public BuildMessageEventArgs + ( + string message, + string helpKeyword, + string senderName, + MessageImportance importance, + DateTime eventTimestamp, + params object[] messageArgs + ) + : this(null, null, null, 0, 0, 0, 0, message, helpKeyword, senderName, importance, eventTimestamp, messageArgs) + { + // do nothing + } + + /// + /// This constructor allows all event data to be initialized + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// importance of the message + public BuildMessageEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + MessageImportance importance + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, importance, DateTime.UtcNow) + { + // do nothing + } + + /// + /// This constructor allows timestamp to be set + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// importance of the message + /// custom timestamp for the event + public BuildMessageEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + MessageImportance importance, + DateTime eventTimestamp + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, importance, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constructor allows timestamp to be set + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// importance of the message + /// custom timestamp for the event + /// message arguments + public BuildMessageEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + MessageImportance importance, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, senderName, eventTimestamp, messageArgs) + { + _importance = importance; + _subcategory = subcategory; + _code = code; + _file = file; + _lineNumber = lineNumber; + _columnNumber = columnNumber; + _endLineNumber = endLineNumber; + _endColumnNumber = endColumnNumber; + } + + private MessageImportance _importance; + + [OptionalField(VersionAdded = 2)] + private string _subcategory; + + [OptionalField(VersionAdded = 2)] + private string _code; + + [OptionalField(VersionAdded = 2)] + private string _file; + + [OptionalField(VersionAdded = 2)] + private string _projectFile; + + [OptionalField(VersionAdded = 2)] + private int _lineNumber; + + [OptionalField(VersionAdded = 2)] + private int _columnNumber; + + [OptionalField(VersionAdded = 2)] + private int _endLineNumber; + + [OptionalField(VersionAdded = 2)] + private int _endColumnNumber; + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + writer.Write((Int32)_importance); + #region SubCategory + if (_subcategory == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_subcategory); + } + #endregion + #region Code + if (_code == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_code); + } + #endregion + #region File + if (_file == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_file); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + writer.Write((Int32)_lineNumber); + writer.Write((Int32)_columnNumber); + writer.Write((Int32)_endLineNumber); + writer.Write((Int32)_endColumnNumber); + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + _importance = (MessageImportance)reader.ReadInt32(); + + //The data in the stream beyond this point are new to 4.0 + if (version > 20) + { + #region SubCategory + if (reader.ReadByte() == 0) + { + _subcategory = null; + } + else + { + _subcategory = reader.ReadString(); + } + #endregion + #region Code + if (reader.ReadByte() == 0) + { + _code = null; + } + else + { + _code = reader.ReadString(); + } + #endregion + #region File + if (reader.ReadByte() == 0) + { + _file = null; + } + else + { + _file = reader.ReadString(); + } + #endregion + #region ProjectFile + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + #endregion + _lineNumber = reader.ReadInt32(); + _columnNumber = reader.ReadInt32(); + _endLineNumber = reader.ReadInt32(); + _endColumnNumber = reader.ReadInt32(); + } + } + #endregion + + /// + /// Importance of the message + /// + public MessageImportance Importance + { + get + { + return _importance; + } + } + + /// + /// The custom sub-type of the event. + /// + public string Subcategory + { + get + { + return _subcategory; + } + } + + /// + /// Code associated with event. + /// + public string Code + { + get + { + return _code; + } + } + + /// + /// File associated with event. + /// + public string File + { + get + { + return _file; + } + } + + /// + /// Line number of interest in associated file. + /// + public int LineNumber + { + get + { + return _lineNumber; + } + } + + /// + /// Column number of interest in associated file. + /// + public int ColumnNumber + { + get + { + return _columnNumber; + } + } + + /// + /// Ending line number of interest in associated file. + /// + public int EndLineNumber + { + get + { + return _endLineNumber; + } + } + + /// + /// Ending column number of interest in associated file. + /// + public int EndColumnNumber + { + get + { + return _endColumnNumber; + } + } + + /// + /// The project which was building when the message was issued. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + + set + { + _projectFile = value; + } + } + } +} diff --git a/src/Framework/BuildStartedEventArgs.cs b/src/Framework/BuildStartedEventArgs.cs new file mode 100644 index 00000000000..4d35a73a846 --- /dev/null +++ b/src/Framework/BuildStartedEventArgs.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for build started events. + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class BuildStartedEventArgs : BuildStatusEventArgs + { + private IDictionary _environmentOnBuildStart; + + /// + /// Default constructor + /// + protected BuildStartedEventArgs() + : base() + { + // do nothing + } + + /// + /// Constructor to initialize all parameters. + /// Sender field cannot be set here and is assumed to be "MSBuild" + /// + /// text message + /// help keyword + public BuildStartedEventArgs + ( + string message, + string helpKeyword + ) + : this(message, helpKeyword, DateTime.UtcNow) + { + // do nothing + } + + /// + /// Constructor to initialize all parameters. + /// Sender field cannot be set here and is assumed to be "MSBuild" + /// + /// text message + /// help keyword + /// A dictionary which lists the environment of the build when the build is started. + public BuildStartedEventArgs + ( + string message, + string helpKeyword, + IDictionary environmentOfBuild + ) + : this(message, helpKeyword, DateTime.UtcNow) + { + _environmentOnBuildStart = environmentOfBuild; + } + + /// + /// Constructor to allow timestamp to be set + /// + /// text message + /// help keyword + /// Timestamp when the event was created + public BuildStartedEventArgs + ( + string message, + string helpKeyword, + DateTime eventTimestamp + ) + : this(message, helpKeyword, eventTimestamp, null) + { + // do nothing + } + + /// + /// Constructor to allow timestamp to be set + /// + /// text message + /// help keyword + /// Timestamp when the event was created + /// message args + public BuildStartedEventArgs + ( + string message, + string helpKeyword, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp, messageArgs) + { + // do nothing + } + + /// + /// The environment which is used at the start of the build + /// + public IDictionary BuildEnvironment + { + get { return _environmentOnBuildStart; } + } + } +} diff --git a/src/Framework/BuildStatusEventArgs.cs b/src/Framework/BuildStatusEventArgs.cs new file mode 100644 index 00000000000..90184947227 --- /dev/null +++ b/src/Framework/BuildStatusEventArgs.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// Base class for build status events. This class is meant + /// to be extended. + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public abstract class BuildStatusEventArgs : LazyFormattedBuildEventArgs + { + /// + /// Default constructor + /// + protected BuildStatusEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// + /// text message + /// help keyword + /// name of event sender + protected BuildStatusEventArgs + ( + string message, + string helpKeyword, + string senderName + ) + : this(message, helpKeyword, senderName, DateTime.UtcNow) + { + // do nothing + } + + + /// + /// This constructor allows timestamp to be set + /// + /// text message + /// help keyword + /// name of event sender + /// Timestamp when event was created + protected BuildStatusEventArgs + ( + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp + ) + : this(message, helpKeyword, senderName, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constructor allows timestamp to be set + /// + /// text message + /// help keyword + /// name of event sender + /// Timestamp when event was created + protected BuildStatusEventArgs + ( + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, senderName, eventTimestamp, messageArgs) + { + // do nothing + } + } +} + diff --git a/src/Framework/BuildWarningEventArgs.cs b/src/Framework/BuildWarningEventArgs.cs new file mode 100644 index 00000000000..fe835d029da --- /dev/null +++ b/src/Framework/BuildWarningEventArgs.cs @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for warning events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class BuildWarningEventArgs : LazyFormattedBuildEventArgs + { + /// + /// Default constructor + /// + protected BuildWarningEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows all event data to be initialized + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + public BuildWarningEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, DateTime.UtcNow) + { + } + + /// + /// This constructor allows timestamp to be set + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// custom timestamp for the event + public BuildWarningEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constructor allows timestamp to be set + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// custom timestamp for the event + /// message arguments + public BuildWarningEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, senderName, eventTimestamp, messageArgs) + { + _subcategory = subcategory; + _code = code; + _file = file; + _lineNumber = lineNumber; + _columnNumber = columnNumber; + _endLineNumber = endLineNumber; + _endColumnNumber = endColumnNumber; + } + + private string _subcategory; + private string _code; + private string _file; + private string _projectFile; + private int _lineNumber; + private int _columnNumber; + private int _endLineNumber; + private int _endColumnNumber; + + #region CustomSerializationToStream + /// + /// Serializes the Errorevent to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + #region SubCategory + if (_subcategory == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_subcategory); + } + #endregion + #region Code + if (_code == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_code); + } + #endregion + #region File + if (_file == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_file); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + writer.Write((Int32)_lineNumber); + writer.Write((Int32)_columnNumber); + writer.Write((Int32)_endLineNumber); + writer.Write((Int32)_endColumnNumber); + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + #region SubCatetory + if (reader.ReadByte() == 0) + { + _subcategory = null; + } + else + { + _subcategory = reader.ReadString(); + } + #endregion + #region Code + if (reader.ReadByte() == 0) + { + _code = null; + } + else + { + _code = reader.ReadString(); + } + #endregion + #region File + if (reader.ReadByte() == 0) + { + _file = null; + } + else + { + _file = reader.ReadString(); + } + #endregion + #region ProjectFile + if (version > 20) + { + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + } + #endregion + _lineNumber = reader.ReadInt32(); + _columnNumber = reader.ReadInt32(); + _endLineNumber = reader.ReadInt32(); + _endColumnNumber = reader.ReadInt32(); + } + #endregion + /// + /// The custom sub-type of the event. + /// + public string Subcategory + { + get + { + return _subcategory; + } + } + + /// + /// Code associated with event. + /// + public string Code + { + get + { + return _code; + } + } + + /// + /// File associated with event. + /// + public string File + { + get + { + return _file; + } + } + + /// + /// Line number of interest in associated file. + /// + public int LineNumber + { + get + { + return _lineNumber; + } + } + + /// + /// Column number of interest in associated file. + /// + public int ColumnNumber + { + get + { + return _columnNumber; + } + } + + /// + /// Ending line number of interest in associated file. + /// + public int EndLineNumber + { + get + { + return _endLineNumber; + } + } + + /// + /// Ending column number of interest in associated file. + /// + public int EndColumnNumber + { + get + { + return _endColumnNumber; + } + } + + /// + /// The project which was building when the message was issued. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + + set + { + _projectFile = value; + } + } + } +} diff --git a/src/Framework/CriticalBuildMessageEventArgs.cs b/src/Framework/CriticalBuildMessageEventArgs.cs new file mode 100644 index 00000000000..80f64af2de3 --- /dev/null +++ b/src/Framework/CriticalBuildMessageEventArgs.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for critical build message event. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for critical message events. These always have High importance. + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class CriticalBuildMessageEventArgs : BuildMessageEventArgs + { + /// + /// This constructor allows all event data to be initialized + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + public CriticalBuildMessageEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, DateTime.UtcNow) + { + // do nothing + } + + /// + /// This constructor allows timestamp to be set + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// custom timestamp for the event + public CriticalBuildMessageEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp + ) + : this(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constructor allows timestamp to be set + /// + /// event subcategory + /// event code + /// file associated with the event + /// line number (0 if not applicable) + /// column number (0 if not applicable) + /// end line number (0 if not applicable) + /// end column number (0 if not applicable) + /// text message + /// help keyword + /// name of event sender + /// custom timestamp for the event + /// message arguments + public CriticalBuildMessageEventArgs + ( + string subcategory, + string code, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp, + params object[] messageArgs + ) + //// Force importance to High. + : base(subcategory, code, file, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, helpKeyword, senderName, MessageImportance.High, eventTimestamp, messageArgs) + { + // do nothing + } + + /// + /// Default constructor + /// + protected CriticalBuildMessageEventArgs() + : base() + { + // do nothing + } + } +} diff --git a/src/Framework/CustomBuildEventArgs.cs b/src/Framework/CustomBuildEventArgs.cs new file mode 100644 index 00000000000..c676c75f8f1 --- /dev/null +++ b/src/Framework/CustomBuildEventArgs.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for custom build events. + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public abstract class CustomBuildEventArgs : LazyFormattedBuildEventArgs + { + /// + /// Default constructor + /// + protected CustomBuildEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// + /// text message + /// help keyword + /// name of sender + protected CustomBuildEventArgs + ( + string message, + string helpKeyword, + string senderName + ) + : this(message, helpKeyword, senderName, DateTime.UtcNow) + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized including timestamp. + /// + /// text message + /// help keyword + /// name of sender + /// Timestamp when event was created + protected CustomBuildEventArgs + ( + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp + ) + : this(message, helpKeyword, senderName, eventTimestamp, null) + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized including timestamp. + /// + /// text message + /// help keyword + /// name of sender + /// Timestamp when event was created + /// Message arguments + protected CustomBuildEventArgs + ( + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, senderName, eventTimestamp, messageArgs) + { + // do nothing + } + } +} diff --git a/src/Framework/Event args classes.cd b/src/Framework/Event args classes.cd new file mode 100644 index 00000000000..705d0aa3b6f --- /dev/null +++ b/src/Framework/Event args classes.cd @@ -0,0 +1,102 @@ + + + + + + + BuildErrorEventArgs.cs + AAAAAAAAgAAAAAAAAAAoAAAArAAEAAgAFAAAABAABAA= + + + + + + BuildEventArgs.cs + AAAAAAAAAAAgAAAAAAAGAAAABgAAAAAAACAAICAgACA= + + + + + + BuildFinishedEventArgs.cs + AAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA= + + + + + + BuildMessageEventArgs.cs + AAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAA= + + + + + + BuildStartedEventArgs.cs + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + + + + BuildStatusEventArgs.cs + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + + + + BuildWarningEventArgs.cs + AAAAAAAAgAAAAAAAAAAoAAAArAAEAAgAFAAAABAABAA= + + + + + + CustomBuildEventArgs.cs + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + + + + ProjectFinishedEventArgs.cs + AAAAgAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAIA= + + + + + + ProjectStartedEventArgs.cs + AAAAABAAABAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAA= + + + + + + TargetFinishedEventArgs.cs + AAAAoAAAADAAAAAQAAAAAAAAAAAAAAAgAAAAIAAAAIA= + + + + + + TargetStartedEventArgs.cs + AAAAIAAAADAAAAAQAAAAAAAAAAAAAAAgAAAAIAAAAAA= + + + + + + TaskFinishedEventArgs.cs + AAAAggAAABIEAAAQBAAAAAAAAAAAAAAAAAAAAAAAAIA= + + + + + + TaskStartedEventArgs.cs + AAAAAgAAABIEAAAQBAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + \ No newline at end of file diff --git a/src/Framework/EventContext.cs b/src/Framework/EventContext.cs new file mode 100644 index 00000000000..a392f2ade44 --- /dev/null +++ b/src/Framework/EventContext.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Globalization; + +namespace Microsoft.Build.Framework +{ + /// + /// Will provide location information for an event, this is especially + /// needed in a multi processor environment + /// + [Serializable] + public class BuildEventContext + { + #region Constructor + public BuildEventContext + ( + int nodeId, + int targetId, + int projectContextId, + int taskId + ) + { + this.nodeId = nodeId; + this.targetId = targetId; + this.projectContextId = projectContextId; + this.taskId = taskId; + } + #endregion + + #region Properties + /// + /// NodeId where event took Place + /// + public int NodeId + { + get + { + return nodeId; + } + } + + /// + /// TargetName of the target the event was in when the event was fired + /// + public int TargetId + { + get + { + return targetId; + } + } + + public int ProjectContextId + { + get + { + return projectContextId; + } + } + + public int TaskId + { + get + { + return this.taskId; + } + } + #endregion + + #region Constants + public const int InvalidProjectContextId = -2; + public const int InvalidTaskId = -1; + public const int InvalidTargetId = -1; + public const int InvalidNodeId = -2; + #endregion + + public override int GetHashCode() + { + return (ProjectContextId + (NodeId << 24)); + } + + public override bool Equals(object obj) + { + // If the references are the same no need to do any comparing + if (base.Equals(obj)) + { + return true; + } + + BuildEventContext contextToCompare = obj as BuildEventContext; + + if (contextToCompare == null) + { + return false; + } + + return (this.nodeId == contextToCompare.NodeId) + && (this.projectContextId == contextToCompare.ProjectContextId) + && (this.targetId == contextToCompare.TargetId) + && (this.taskId == contextToCompare.TaskId); + } + + #region Data + // Node event was in + private int nodeId; + // Target event was in + private int targetId; + //ProjectContext the event was in + private int projectContextId; + // Id of the task the event was caused from + private int taskId; + #endregion + } +} diff --git a/src/Framework/ExternalProjectFinishedEventArgs.cs b/src/Framework/ExternalProjectFinishedEventArgs.cs new file mode 100644 index 00000000000..cc4bc90edf8 --- /dev/null +++ b/src/Framework/ExternalProjectFinishedEventArgs.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for external project finished events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class ExternalProjectFinishedEventArgs : CustomBuildEventArgs + { + /// + /// Default constructor + /// + protected ExternalProjectFinishedEventArgs() + : base() + { + // nothing to do here, move along. + } + + /// + /// Useful constructor + /// + /// text message + /// help keyword + /// name of the object sending this event + /// project name + /// true indicates project built successfully + public ExternalProjectFinishedEventArgs + ( + string message, + string helpKeyword, + string senderName, + string projectFile, + bool succeeded + ) + : this(message, helpKeyword, senderName, projectFile, succeeded, DateTime.UtcNow) + { + } + + /// + /// Useful constructor including the ability to set the timestamp + /// + /// text message + /// help keyword + /// name of the object sending this event + /// project name + /// true indicates project built successfully + /// Timestamp when event was created + public ExternalProjectFinishedEventArgs + ( + string message, + string helpKeyword, + string senderName, + string projectFile, + bool succeeded, + DateTime eventTimestamp + ) + : base(message, helpKeyword, senderName, eventTimestamp) + { + _projectFile = projectFile; + _succeeded = succeeded; + } + + private string _projectFile; + + /// + /// Project name + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + private bool _succeeded; + + /// + /// True if project built successfully, false otherwise + /// + public bool Succeeded + { + get + { + return _succeeded; + } + } + } +} diff --git a/src/Framework/ExternalProjectStartedEventArgs.cs b/src/Framework/ExternalProjectStartedEventArgs.cs new file mode 100644 index 00000000000..73129568817 --- /dev/null +++ b/src/Framework/ExternalProjectStartedEventArgs.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for external project started events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class ExternalProjectStartedEventArgs : CustomBuildEventArgs + { + /// + /// Default constructor + /// + protected ExternalProjectStartedEventArgs() + : base() + { + // nothing to do here, move along. + } + + /// + /// Useful constructor + /// + /// text message + /// help keyword + /// name of the object sending this event + /// project name + /// targets we are going to build (empty indicates default targets) + public ExternalProjectStartedEventArgs + ( + string message, + string helpKeyword, + string senderName, + string projectFile, + string targetNames + ) + : this(message, helpKeyword, senderName, projectFile, targetNames, DateTime.UtcNow) + { + } + + /// + /// Useful constructor, including the ability to set the timestamp of the event + /// + /// text message + /// help keyword + /// name of the object sending this event + /// project name + /// targets we are going to build (empty indicates default targets) + /// Timestamp when the event was created + public ExternalProjectStartedEventArgs + ( + string message, + string helpKeyword, + string senderName, + string projectFile, + string targetNames, + DateTime eventTimestamp + ) + : base(message, helpKeyword, senderName, eventTimestamp) + { + _projectFile = projectFile; + _targetNames = targetNames; + } + + private string _projectFile; + + /// + /// Project name + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + private string _targetNames; + + /// + /// Targets that we will build in the project. This may mean different things for different project types, + /// our tasks will put something like Rebuild, Clean, etc. here. This may be null if the project is being + /// built with the default target. + /// + public string TargetNames + { + get + { + return _targetNames; + } + } + } +} diff --git a/src/Framework/FxCopExclusions/Microsoft.Build.Framework.Suppressions.cs b/src/Framework/FxCopExclusions/Microsoft.Build.Framework.Suppressions.cs new file mode 100644 index 00000000000..8f3195f091a --- /dev/null +++ b/src/Framework/FxCopExclusions/Microsoft.Build.Framework.Suppressions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// FxCop Suppression file +// To Use: +// Add add module level suppressions to this file to have them suppressed in the assembly + +using System.Diagnostics.CodeAnalysis; + +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames")] +[module: SuppressMessage("Microsoft.MSInternal", "CA905:SystemAndMicrosoftNamespacesRequireApproval", Scope="namespace", Target="Microsoft.Build.CommandLine", Justification="This is an approved namespace.")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="STA", Scope="type", Target="Microsoft.Build.Framework.RunInSTAAttribute", Justification="Not worth breaking custormers because of case.")] +#endif diff --git a/src/Framework/IBuildEngine.cs b/src/Framework/IBuildEngine.cs new file mode 100644 index 00000000000..1925f5927bc --- /dev/null +++ b/src/Framework/IBuildEngine.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface exposes functionality on the build engine + /// that is required for task authoring. + /// + public interface IBuildEngine + { + /// + /// Allows tasks to raise error events to all registered loggers. + /// The build engine may perform some filtering or + /// pre-processing on the events, before dispatching them. + /// + /// Details of event to raise. + void LogErrorEvent(BuildErrorEventArgs e); + + /// + /// Allows tasks to raise warning events to all registered loggers. + /// The build engine may perform some filtering or + /// pre-processing on the events, before dispatching them. + /// + /// Details of event to raise. + void LogWarningEvent(BuildWarningEventArgs e); + + /// + /// Allows tasks to raise message events to all registered loggers. + /// The build engine may perform some filtering or + /// pre-processing on the events, before dispatching them. + /// + /// Details of event to raise. + void LogMessageEvent(BuildMessageEventArgs e); + + /// + /// Allows tasks to raise custom events to all registered loggers. + /// The build engine may perform some filtering or + /// pre-processing on the events, before dispatching them. + /// + /// Details of event to raise. + void LogCustomEvent(CustomBuildEventArgs e); + + /// + /// Returns true if the ContinueOnError flag was set to true for this particular task + /// in the project file. + /// + bool ContinueOnError + { + get; + } + + /// + /// Retrieves the line number of the task node withing the project file that called it. + /// + int LineNumberOfTaskNode + { + get; + } + + /// + /// Retrieves the line number of the task node withing the project file that called it. + /// + int ColumnNumberOfTaskNode + { + get; + } + + /// + /// Returns the full path to the project file that contained the call to this task. + /// + string ProjectFileOfTaskNode + { + get; + } + + /// + /// This method allows tasks to initiate a build on a + /// particular project file. If the build is successful, the outputs + /// (if any) of the specified targets are returned. + /// + /// + /// 1) it is acceptable to pass null for both targetNames and targetOutputs + /// 2) if no targets are specified, the default targets are built + /// 3) target outputs are returned as ITaskItem arrays indexed by target name + /// + /// The project to build. + /// The targets in the project to build (can be null). + /// A hash table of additional global properties to apply + /// to the child project (can be null). The key and value should both be strings. + /// The outputs of each specified target (can be null). + /// true, if build was successful + bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs + ); + } +} diff --git a/src/Framework/IBuildEngine2.cs b/src/Framework/IBuildEngine2.cs new file mode 100644 index 00000000000..ddd10a08608 --- /dev/null +++ b/src/Framework/IBuildEngine2.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface extends IBuildEngine to provide a method allowing building + /// project files in parallel. + /// + public interface IBuildEngine2 : IBuildEngine + { + /// + /// This property allows a task to query whether or not the system is running in single process mode or multi process mode. + /// Single process mode (IsRunningMultipleNodes = false) is where the engine is initialized with the number of cpus = 1 and the engine is not a child engine. + /// The engine is in multi process mode (IsRunningMultipleNodes = true) when the engine is initialized with a number of cpus > 1 or the engine is a child engine. + /// + bool IsRunningMultipleNodes + { + get; + } + + /// + /// This method allows tasks to initiate a build on a + /// particular project file. If the build is successful, the outputs + /// (if any) of the specified targets are returned. + /// + /// + /// 1) it is acceptable to pass null for both targetNames and targetOutputs + /// 2) if no targets are specified, the default targets are built + /// 3) target outputs are returned as ITaskItem arrays indexed by target name + /// + /// The project to build. + /// The targets in the project to build (can be null). + /// A hash table of additional global properties to apply + /// to the child project (can be null). The key and value should both be strings. + /// The outputs of each specified target (can be null). + /// A tools version recognized by the Engine that will be used during this build (can be null). + /// true, if build was successful + bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs, + string toolsVersion + ); + + /// + /// This method allows tasks to initiate a build on a + /// particular project file. If the build is successful, the outputs + /// (if any) of the specified targets are returned. + /// + /// + /// 1) it is acceptable to pass null for both targetNames and targetOutputs + /// 2) if no targets are specified, the default targets are built + /// 3) target outputs are returned as ITaskItem arrays indexed by target name + /// + /// The project to build. + /// The targets in the project to build (can be null). + /// An array of hashtables of additional global properties to apply + /// to the child project (array entries can be null). + /// The key and value in the hashtable should both be strings. + /// The outputs of each specified target (can be null). + /// A tools version recognized by the Engine that will be used during this build (can be null). + /// If true the operation will only be run if the cache doesn't + /// already contain the result. After the operation the result is + /// stored in the cache + /// If true the project will be unloaded once the + /// operation is completed + /// true, if build was successful + bool BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IDictionary[] targetOutputsPerProject, + string[] toolsVersion, + bool useResultsCache, + bool unloadProjectsOnCompletion + ); + } +} diff --git a/src/Framework/IBuildEngine3.cs b/src/Framework/IBuildEngine3.cs new file mode 100644 index 00000000000..b502af85136 --- /dev/null +++ b/src/Framework/IBuildEngine3.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Interface for tasks to communicate with the MSBuild engine. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface extends IBuildEngine to provide a method allowing building + /// project files in parallel. + /// + public interface IBuildEngine3 : IBuildEngine2 + { + /// + /// This method allows tasks to initiate a build on a + /// particular project file. If the build is successful, the outputs + /// (if any) of the specified targets are returned. + /// + /// + /// 1) it is acceptable to pass null for both targetNames and targetOutputs + /// 2) if no targets are specified, the default targets are built + /// + /// + /// The project to build. + /// The targets in the project to build (can be null). + /// An array of hashtables of additional global properties to apply + /// to the child project (array entries can be null). + /// The key and value in the hashtable should both be strings. + /// A list of global properties which should be removed. + /// A tools version recognized by the Engine that will be used during this build (can be null). + /// Should the target outputs be returned in the BuildEngineResult + /// Returns a structure containing the success or failure of the build and the target outputs by project. + BuildEngineResult BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IList[] removeGlobalProperties, + string[] toolsVersion, + bool returnTargetOutputs + ); + + /// + /// Informs the system that this task has a long-running out-of-process component and other work can be done in the + /// build while that work completes. + /// + void Yield(); + + /// + /// Waits to reacquire control after yielding. + /// + void Reacquire(); + } +} diff --git a/src/Framework/IBuildEngine4.cs b/src/Framework/IBuildEngine4.cs new file mode 100644 index 00000000000..7e75ba0f3a1 --- /dev/null +++ b/src/Framework/IBuildEngine4.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for tasks to communicate with the MSBuild engine. +//----------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Framework +{ + /// + /// Defines the lifetime of a registered task object. + /// + public enum RegisteredTaskObjectLifetime + { + /// + /// The registered object will be disposed when the build ends. + /// + Build, + + /// + /// The registered object will be disposed when the AppDomain is unloaded. + /// + /// + /// The AppDomain to which this refers is the one in which MSBuild was launched, + /// not the one in which the Task was launched. + /// + AppDomain, + } + + /// + /// This interface extends IBuildEngine to provide a mechanism allowing tasks to + /// share data between task invocations. + /// + public interface IBuildEngine4 : IBuildEngine3 + { + /// + /// Registers an object with the system that will be disposed of at some specified time + /// in the future. + /// + /// The key used to retrieve the object. + /// The object to be held for later disposal. + /// The lifetime of the object. + /// The object may be disposed earlier that the requested time if + /// MSBuild needs to reclaim memory. + /// + /// + /// This method may be called by tasks which need to maintain state across task invocations, + /// such as to cache data which may be expensive to generate but which is known not to change during the + /// build. It is strongly recommended that be set to true if the + /// object will retain any significant amount of data, as this gives MSBuild the most flexibility to + /// manage limited process memory resources. + /// + /// + /// The thread on which the object is disposed may be arbitrary - however it is guaranteed not to + /// be disposed while the task is executing, even if is set + /// to true. + /// + /// + /// If the object implements IDisposable, IDisposable.Dispose will be invoked on the object before + /// discarding it. + /// + /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "obj", Justification = "Shipped this way in Dev11 Beta, which is go-live")] + void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection); + + /// + /// Retrieves a previously registered task object stored with the specified key. + /// + /// The key used to retrieve the object. + /// The lifetime of the object. + /// + /// The registered object, or null is there is no object registered under that key or the object + /// has been discarded through early collection. + /// + object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime); + + /// + /// Unregisters a previously-registered task object. + /// + /// The key used to retrieve the object. + /// The lifetime of the object. + /// + /// The registered object, or null is there is no object registered under that key or the object + /// has been discarded through early collection. + /// + object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime); + } +} diff --git a/src/Framework/ICancelableTask.cs b/src/Framework/ICancelableTask.cs new file mode 100644 index 00000000000..ccbe7c0c012 --- /dev/null +++ b/src/Framework/ICancelableTask.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for tasks which can be cancelled. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Framework +{ + /// + /// Interface for tasks which can be cancelled. + /// + public interface ICancelableTask : ITask + { + /// + /// Instructs the task to exit as soon as possible, or to immediately exit if Execute is invoked after this method. + /// + /// + /// Cancel() may be called at any time after the task has been instantiated, even before is called. + /// Cancel calls may come in from any thread. The implementation of this method should not block indefinitely. + /// + void Cancel(); + } +} diff --git a/src/Framework/IEventRedirector.cs b/src/Framework/IEventRedirector.cs new file mode 100644 index 00000000000..9be0bce4c63 --- /dev/null +++ b/src/Framework/IEventRedirector.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Framework +{ + /// + /// This interface is used to forward events to another loggers + /// + public interface IEventRedirector + { + /// + /// This method is called by the node loggers to forward the events to central logger + /// + void ForwardEvent(BuildEventArgs buildEvent); + } +} diff --git a/src/Framework/IEventSource.cs b/src/Framework/IEventSource.cs new file mode 100644 index 00000000000..07943a2cba5 --- /dev/null +++ b/src/Framework/IEventSource.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// Type of handler for MessageRaised events + /// + public delegate void BuildMessageEventHandler(object sender, BuildMessageEventArgs e); + + /// + /// Type of handler for ErrorRaised events + /// + public delegate void BuildErrorEventHandler(object sender, BuildErrorEventArgs e); + + /// + /// Type of handler for WarningRaised events + /// + public delegate void BuildWarningEventHandler(object sender, BuildWarningEventArgs e); + + /// + /// Type of handler for CustomEventRaised events + /// + public delegate void CustomBuildEventHandler(object sender, CustomBuildEventArgs e); + + /// + /// Type of handler for BuildStartedEvent events + /// + public delegate void BuildStartedEventHandler(object sender, BuildStartedEventArgs e); + + /// + /// Type of handler for BuildFinishedEvent events + /// + public delegate void BuildFinishedEventHandler(object sender, BuildFinishedEventArgs e); + + /// + /// Type of handler for ProjectStarted events + /// + public delegate void ProjectStartedEventHandler(object sender, ProjectStartedEventArgs e); + + /// + /// Type of handler for ProjectFinished events + /// + public delegate void ProjectFinishedEventHandler(object sender, ProjectFinishedEventArgs e); + + /// + /// Type of handler for TargetStarted events + /// + public delegate void TargetStartedEventHandler(object sender, TargetStartedEventArgs e); + + /// + /// Type of handler for TargetFinished events + /// + public delegate void TargetFinishedEventHandler(object sender, TargetFinishedEventArgs e); + + /// + /// Type of handler for TaskStarted events + /// + public delegate void TaskStartedEventHandler(object sender, TaskStartedEventArgs e); + + /// + /// Type of handler for TaskFinished events + /// + public delegate void TaskFinishedEventHandler(object sender, TaskFinishedEventArgs e); + + /// + /// Type of handler for BuildStatus events + /// + public delegate void BuildStatusEventHandler(object sender, BuildStatusEventArgs e); + + /// + /// Type of handler for AnyEventRaised events + /// + public delegate void AnyEventHandler(object sender, BuildEventArgs e); + + /// + /// This interface defines the events raised by the build engine. + /// Loggers use this interface to subscribe to the events they + /// are interested in receiving. + /// + public interface IEventSource + { + /// + /// this event is raised to log a message + /// + event BuildMessageEventHandler MessageRaised; + + /// + /// this event is raised to log an error + /// + event BuildErrorEventHandler ErrorRaised; + + /// + /// this event is raised to log a warning + /// + event BuildWarningEventHandler WarningRaised; + + /// + /// this event is raised to log the start of a build + /// + event BuildStartedEventHandler BuildStarted; + + /// + /// this event is raised to log the end of a build + /// + event BuildFinishedEventHandler BuildFinished; + + /// + /// this event is raised to log the start of a project build + /// + event ProjectStartedEventHandler ProjectStarted; + + /// + /// this event is raised to log the end of a project build + /// + event ProjectFinishedEventHandler ProjectFinished; + + /// + /// this event is raised to log the start of a target build + /// + event TargetStartedEventHandler TargetStarted; + + /// + /// this event is raised to log the end of a target build + /// + event TargetFinishedEventHandler TargetFinished; + + /// + /// this event is raised to log the start of task execution + /// + event TaskStartedEventHandler TaskStarted; + + /// + /// this event is raised to log the end of task execution + /// + event TaskFinishedEventHandler TaskFinished; + + /// + /// this event is raised to log custom events + /// + event CustomBuildEventHandler CustomEventRaised; + + /// + /// this event is raised to log any build status event + /// + event BuildStatusEventHandler StatusEventRaised; + + /// + /// this event is raised to log any build event + /// + event AnyEventHandler AnyEventRaised; + } +} diff --git a/src/Framework/IForwardingLogger.cs b/src/Framework/IForwardingLogger.cs new file mode 100644 index 00000000000..801e5b36012 --- /dev/null +++ b/src/Framework/IForwardingLogger.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Framework +{ + /// + /// This interface extends the ILogger interface to provide a property which can be used to forward events + /// to a logger running in a different process. It can also be used create filtering loggers. + /// + public interface IForwardingLogger : INodeLogger + { + /// + /// This property is set by the build engine to allow a node loggers to forward messages to the + /// central logger + /// + IEventRedirector BuildEventRedirector + { + get; + + set; + } + + /// + /// This property is set by the build engine or node to inform the forwarding logger which node it is running on + /// + int NodeId + { + get; + + set; + } + } +} diff --git a/src/Framework/IGeneratedTask.cs b/src/Framework/IGeneratedTask.cs new file mode 100644 index 00000000000..1cd0293df45 --- /dev/null +++ b/src/Framework/IGeneratedTask.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An interface implemented by tasks that are generated by ITaskFactory instances. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// An interface implemented by tasks that are generated by ITaskFactory instances. + /// + public interface IGeneratedTask : ITask + { + /// + /// Sets a value on a property of this task instance. + /// + /// The property to set. + /// The value to set. The caller is responsible to type-coerce this value to match the property's . + /// + /// All exceptions from this method will be caught in the taskExecution host and logged as a fatal task error + /// + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Property", Justification = "Public API that has shipped")] + void SetPropertyValue(TaskPropertyInfo property, object value); + + /// + /// Gets the property value. + /// + /// The property to get. + /// + /// The value of the property, the value's type will match the type given by . + /// + /// + /// MSBuild calls this method after executing the task to get output parameters. + /// All exceptions from this method will be caught in the taskExecution host and logged as a fatal task error + /// + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Property", Justification = "Public API that has shipped")] + object GetPropertyValue(TaskPropertyInfo property); + } +} diff --git a/src/Framework/ILogger.cs b/src/Framework/ILogger.cs new file mode 100644 index 00000000000..8996641f649 --- /dev/null +++ b/src/Framework/ILogger.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// Enumeration of the levels of detail of an event log. + /// + /// + /// The level of detail (i.e. verbosity) of an event log is entirely controlled by the logger generating the log -- a logger + /// will be directed to keep its verbosity at a certain level, based on user preferences, but a logger is free to choose the + /// events it logs for each verbosity level. + /// + /// LOGGING GUIDELINES FOR EACH VERBOSITY LEVEL: + /// 1) Quiet -- only display a summary at the end of build + /// 2) Minimal -- only display errors, warnings, high importance events and a build summary + /// 3) Normal -- display all errors, warnings, high importance events, some status events, and a build summary + /// 4) Detailed -- display all errors, warnings, high and normal importance events, all status events, and a build summary + /// 5) Diagnostic -- display all events, and a build summary + /// + /// WARNING: VS Automation code for the Tools/Options MSBuild build verbosity setting will be broken + /// by changes to this enum (not to mention existing MSBuild clients and vsproject code). + /// Please make sure to talk to automation devs before changing it. + /// + [ComVisible(true)] + public enum LoggerVerbosity + { + /// + /// The most minimal output + /// + Quiet, + + /// + /// Relatively little output + /// + Minimal, + + /// + /// Standard output. This should be the default if verbosity level is not set + /// + Normal, + + /// + /// Relatively verbose, but not exhaustive + /// + Detailed, + + /// + /// The most verbose and informative verbosity + /// + Diagnostic + } + + /// + /// This interface defines a "logger" in the build system. A logger subscribes to build system events. All logger classes must + /// implement this interface to be recognized by the build engine. + /// + [ComVisible(true)] + public interface ILogger + { + /// + /// The verbosity level directs the amount of detail that appears in a logger's event log. Though this is only a + /// recommendation based on user preferences, and a logger is free to choose the exact events it logs, it is still + /// important that the guidelines for each level be followed, for a good user experience. + /// + /// The verbosity level. + LoggerVerbosity Verbosity + { + get; + + set; + } + + /// + /// This property holds the user-specified parameters to the logger. If parameters are not provided, a logger should revert + /// to defaults. If a logger does not take parameters, it can ignore this property. + /// + /// The parameter string (can be null). + string Parameters + { + get; + + set; + } + + /// + /// Called by the build engine to allow loggers to subscribe to the events they desire. + /// + /// The events available to loggers. + void Initialize(IEventSource eventSource); + + /// + /// Called by the build engine to allow loggers to release any resources they may have allocated at initialization time, + /// or during the build. + /// + void Shutdown(); + } +} diff --git a/src/Framework/INodeLogger.cs b/src/Framework/INodeLogger.cs new file mode 100644 index 00000000000..eb36e26a224 --- /dev/null +++ b/src/Framework/INodeLogger.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface defines a "parallel aware logger" in the build system. A parallel aware logger + /// will accept a cpu count and be aware that any cpu count greater than 1 means the events will + /// be received from the logger from each cpu as the events are logged. + /// + [ComVisible(true)] + public interface INodeLogger : ILogger + { + void Initialize(IEventSource eventSource, int nodeCount); + } +} diff --git a/src/Framework/ITask.cs b/src/Framework/ITask.cs new file mode 100644 index 00000000000..28f1e6989ca --- /dev/null +++ b/src/Framework/ITask.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface defines a "task" in the build system. A task is an atomic unit of build operation. All task classes must + /// implement this interface to be recognized by the build engine. + /// + public interface ITask + { + /// + /// This property is set by the build engine to allow a task to call back into it. + /// + /// The interface on the build engine available to tasks. + IBuildEngine BuildEngine + { + get; + + set; + } + + /// + /// The build engine sets this property if the host IDE has associated a host object with this particular task. + /// + /// The host object instance (can be null). + ITaskHost HostObject + { + get; + + set; + } + + /// + /// This method is called by the build engine to begin task execution. A task uses the return value to indicate + /// whether it was successful. If a task throws an exception out of this method, the engine will automatically + /// assume that the task has failed. + /// + /// true, if successful + bool Execute(); + } +} diff --git a/src/Framework/ITaskFactory.cs b/src/Framework/ITaskFactory.cs new file mode 100644 index 00000000000..626148c31f8 --- /dev/null +++ b/src/Framework/ITaskFactory.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Task factory Instance which will instantiate and execute tasks +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// Interface that a task factory Instance should implement + /// + public interface ITaskFactory + { + /// + /// Gets the name of the factory. + /// + /// The name of the factory. + string FactoryName { get; } + + /// + /// Gets the type of the task this factory will instantiate. + /// + Type TaskType { get; } + + /// + /// Initializes this factory for instantiating tasks with a particular inline task block. + /// + /// Name of the task. + /// The parameter group. + /// The task body. + /// The task factory logging host. + /// A value indicating whether initialization was successful. + /// + /// MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the factory can be asked + /// whether or not task names can be created by the factory. + /// + /// The taskFactoryLoggingHost will log messages in the context of the target where the task is first used. + /// + /// + bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost); + + /// + /// Get the descriptions for all the task's parameters. + /// + /// A non-null array of property descriptions. + TaskPropertyInfo[] GetTaskParameters(); + + /// + /// Create an instance of the task to be used. + /// + /// + /// The task factory logging host will log messages in the context of the task. + /// + /// + /// The generated task, or null if the task failed to be created. + /// + ITask CreateTask(IBuildEngine taskFactoryLoggingHost); + + /// + /// Cleans up any context or state that may have been built up for a given task. + /// + /// The task to clean up. + /// + /// For many factories, this method is a no-op. But some factories may have built up + /// an AppDomain as part of an individual task instance, and this is their opportunity + /// to shutdown the AppDomain. + /// + void CleanupTask(ITask task); + } +} diff --git a/src/Framework/ITaskFactory2.cs b/src/Framework/ITaskFactory2.cs new file mode 100644 index 00000000000..f824d238d4d --- /dev/null +++ b/src/Framework/ITaskFactory2.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Task factory Instance which will instantiate and execute tasks +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.Framework +{ + /// + /// Interface that a task factory Instance should implement if it wants to be able to + /// use new UsingTask parameters such as Runtime and Architecture. + /// + public interface ITaskFactory2 : ITaskFactory + { + /// + /// Initializes this factory for instantiating tasks with a particular inline task block and a set of UsingTask parameters. MSBuild + /// provides an implementation of this interface, TaskHostFactory, that uses "Runtime", with values "CLR2", "CLR4", "CurrentRuntime", + /// and "*" (Any); and "Architecture", with values "x86", "x64", "CurrentArchitecture", and "*" (Any). An implementer of ITaskFactory2 + /// can choose to use these pre-defined Runtime and Architecture values, or can specify new values for these parameters. + /// + /// Name of the task. + /// Special parameters that the task factory can use to modify how it executes tasks, + /// such as Runtime and Architecture. The key is the name of the parameter and the value is the parameter's value. This + /// is the set of parameters that was set on the UsingTask using e.g. the UsingTask Runtime and Architecture parameters. + /// The parameter group. + /// The task body. + /// The task factory logging host. + /// A value indicating whether initialization was successful. + /// + /// MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the + /// factory can be asked whether or not task names can be created by the factory. If a task factory implements ITaskFactory2, + /// this Initialize method will be called in place of ITaskFactory.Initialize. + /// + /// The taskFactoryLoggingHost will log messages in the context of the target where the task is first used. + /// + /// + bool Initialize(string taskName, IDictionary factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost); + + /// + /// Create an instance of the task to be used, with an optional set of "special" parameters set on the individual task invocation using + /// the MSBuildRuntime and MSBuildArchitecture default task parameters. MSBuild provides an implementation of this interface, + /// TaskHostFactory, that uses "MSBuildRuntime", with values "CLR2", "CLR4", "CurrentRuntime", and "*" (Any); and "MSBuildArchitecture", + /// with values "x86", "x64", "CurrentArchitecture", and "*" (Any). An implementer of ITaskFactory2 can choose to use these pre-defined + /// MSBuildRuntime and MSBuildArchitecture values, or can specify new values for these parameters. + /// + /// + /// The task factory logging host will log messages in the context of the task. + /// + /// + /// Special parameters that the task factory can use to modify how it executes tasks, such as Runtime and Architecture. + /// The key is the name of the parameter and the value is the parameter's value. This is the set of parameters that was + /// set to the task invocation itself, via e.g. the special MSBuildRuntime and MSBuildArchitecture parameters. + /// + /// + /// If a task factory implements ITaskFactory2, MSBuild will call this method instead of ITaskFactory.CreateTask. + /// + /// + /// The generated task, or null if the task failed to be created. + /// + ITask CreateTask(IBuildEngine taskFactoryLoggingHost, IDictionary taskIdentityParameters); + } +} \ No newline at end of file diff --git a/src/Framework/ITaskHost.cs b/src/Framework/ITaskHost.cs new file mode 100644 index 00000000000..112ea50e0e2 --- /dev/null +++ b/src/Framework/ITaskHost.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Framework +{ + /// + /// This empty interface is used to pass host objects from an IDE to individual + /// tasks. Depending on the task itself and what kinds parameters and functionality + /// it exposes, the task should define its own interface that inherits from this one, + /// and then use that interface to communicate with the host. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("9049A481-D0E9-414f-8F92-D4F67A0359A6")] + [SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Justification = "This empty interface is used to pass host objects from an IDE to individual")] + public interface ITaskHost + { + } +} diff --git a/src/Framework/ITaskItem.cs b/src/Framework/ITaskItem.cs new file mode 100644 index 00000000000..31a909036e2 --- /dev/null +++ b/src/Framework/ITaskItem.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface defines a project item that can be consumed and emitted by tasks. + /// + [ComVisible(true)] + [Guid("8661674F-2148-4F71-A92A-49875511C528")] + public interface ITaskItem + { + /// + /// Gets or sets the item "specification" e.g. for disk-based items this would be the file path. + /// + /// + /// This should be named "EvaluatedInclude" but that would be a breaking change to this interface. + /// + /// The item-spec string. + string ItemSpec + { + get; + + set; + } + + /// + /// Gets the names of all the metadata on the item. + /// Includes the built-in metadata like "FullPath". + /// + /// The list of metadata names. + ICollection MetadataNames + { + get; + } + + /// + /// Gets the number of pieces of metadata on the item. Includes + /// both custom and built-in metadata. + /// + /// Count of pieces of metadata. + int MetadataCount + { + get; + } + + /// + /// Allows the values of metadata on the item to be queried. + /// + /// The name of the metadata to retrieve. + /// The value of the specified metadata. + string GetMetadata(string metadataName); + + /// + /// Allows a piece of custom metadata to be set on the item. + /// + /// The name of the metadata to set. + /// The metadata value. + void SetMetadata(string metadataName, string metadataValue); + + /// + /// Allows the removal of custom metadata set on the item. + /// + /// The name of the metadata to remove. + void RemoveMetadata(string metadataName); + + /// + /// Allows custom metadata on the item to be copied to another item. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should NOT copy over the item-spec + /// 2) if a particular piece of metadata already exists on the destination item, it should NOT be overwritten + /// 3) if there are pieces of metadata on the item that make no semantic sense on the destination item, they should NOT be copied + /// + /// The item to copy metadata to. + void CopyMetadataTo(ITaskItem destinationItem); + + /// + /// Get the collection of custom metadata. This does not include built-in metadata. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should return a clone of the metadata + /// 2) writing to this dictionary should not be reflected in the underlying item. + /// + /// Dictionary of cloned metadata + IDictionary CloneCustomMetadata(); + } +} diff --git a/src/Framework/ITaskItem2.cs b/src/Framework/ITaskItem2.cs new file mode 100644 index 00000000000..ff0c4249cc2 --- /dev/null +++ b/src/Framework/ITaskItem2.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An improvement to ITaskItem that makes it possible for implementations to avoid losing escaping information. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework +{ + /// + /// This interface adds escaping support to the ITaskItem interface. + /// + [ComVisible(true)] + [Guid("ac6d5a59-f877-461b-88e3-b2f06fce0cb9")] + public interface ITaskItem2 : ITaskItem + { + /// + /// Gets or sets the item include value e.g. for disk-based items this would be the file path. + /// + /// + /// Taking the opportunity to fix the property name, although this doesn't + /// make it obvious it's an improvement on ItemSpec. + /// + string EvaluatedIncludeEscaped + { + get; + set; + } + + /// + /// Allows the values of metadata on the item to be queried. + /// + /// + /// Taking the opportunity to fix the property name, although this doesn't + /// make it obvious it's an improvement on GetMetadata. + /// + string GetMetadataValueEscaped(string metadataName); + + /// + /// Allows a piece of custom metadata to be set on the item. Assumes that the value passed + /// in is unescaped, and escapes the value as necessary in order to maintain its value. + /// + /// + /// Taking the opportunity to fix the property name, although this doesn't + /// make it obvious it's an improvement on SetMetadata. + /// + void SetMetadataValueLiteral(string metadataName, string metadataValue); + + /// + /// ITaskItem2 implementation which returns a clone of the metadata on this object. + /// Values returned are in their original escaped form. + /// + /// The cloned metadata, with values' escaping preserved. + IDictionary CloneCustomMetadataEscaped(); + } +} diff --git a/src/Framework/LazyFormattedBuildEventArgs.cs b/src/Framework/LazyFormattedBuildEventArgs.cs new file mode 100644 index 00000000000..b2eb15efe00 --- /dev/null +++ b/src/Framework/LazyFormattedBuildEventArgs.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for any build event. +//----------------------------------------------------------------------- + +using System; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Framework +{ + /// + /// Stores strings for parts of a message delaying the formatting until it needs to be shown + /// + [Serializable] + public class LazyFormattedBuildEventArgs : BuildEventArgs + { + /// + /// Stores the message arguments. + /// + private object[] _arguments; + + /// + /// Stores the original culture for String.Format. + /// + private CultureInfo _originalCulture; + + /// + /// Lock object. + /// + [NonSerialized] + private Object _locker; + + /// + /// This constuctor allows all event data to be initialized. + /// + /// text message. + /// help keyword. + /// name of event sender. + public LazyFormattedBuildEventArgs + ( + string message, + string helpKeyword, + string senderName + ) + : this(message, helpKeyword, senderName, DateTime.Now, null) + { + } + + /// + /// This constuctor that allows message arguments that are lazily formatted. + /// + /// text message. + /// help keyword. + /// name of event sender. + /// Timestamp when event was created. + /// Message arguments. + public LazyFormattedBuildEventArgs + ( + string message, + string helpKeyword, + string senderName, + DateTime eventTimestamp, + params object[] messageArgs + ) + : base(message, helpKeyword, senderName, eventTimestamp) + { + _arguments = messageArgs; + _originalCulture = CultureInfo.CurrentCulture; + _locker = new Object(); + } + + /// + /// Default constructor. + /// + protected LazyFormattedBuildEventArgs() + : base() + { + _locker = new Object(); + } + + /// + /// Gets the formatted message. + /// + public override string Message + { + get + { + lock (_locker) + { + if (_arguments != null && _arguments.Length > 0) + { + base.Message = FormatString(_originalCulture, base.Message, _arguments); + _arguments = null; + } + } + + return base.Message; + } + } + + /// + /// Serializes to a stream through a binary writer. + /// + /// Binary writer which is attached to the stream the event will be serialized into. + internal override void WriteToStream(BinaryWriter writer) + { + // Locking is needed here as this is invoked on the serialization thread, + // whereas a local logger (a distributed logger) may concurrently invoke this.Message + // which will trigger formatting and thus the exception below + lock (_locker) + { + bool hasArguments = _arguments != null; + base.WriteToStream(writer); + + if (hasArguments && _arguments == null) + { + throw new InvalidOperationException("BuildEventArgs has formatted message while serializing!"); + } + + if (_arguments != null) + { + writer.Write(_arguments.Length); + + foreach (object argument in _arguments) + { + // Arguments may be ints, etc, so explicitly convert + // Convert.ToString returns String.Empty when it cannot convert, rather than throwing + writer.Write(Convert.ToString(argument, CultureInfo.CurrentCulture)); + } + } + else + { + writer.Write((Int32)(-1)); + } + + writer.Write(_originalCulture != null ? _originalCulture.LCID : 0); + } + } + + /// + /// Deserializes from a stream through a binary reader. + /// + /// Binary reader which is attached to the stream the event will be deserialized from. + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, Int32 version) + { + base.CreateFromStream(reader, version); + + if (version > 20) + { + string[] messageArgs = null; + int numArguments = reader.ReadInt32(); + + if (numArguments >= 0) + { + messageArgs = new string[numArguments]; + + for (int numRead = 0; numRead < numArguments; numRead++) + { + messageArgs[numRead] = reader.ReadString(); + } + } + + _arguments = messageArgs; + + int originalCultureId = reader.ReadInt32(); + if (originalCultureId != 0) + { + if (originalCultureId == CultureInfo.CurrentCulture.LCID) + { + _originalCulture = CultureInfo.CurrentCulture; + } + else + { + _originalCulture = new CultureInfo(originalCultureId); + } + } + } + } + + /// + /// Formats the given string using the variable arguments passed in. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// This method is thread-safe. + /// The culture info for formatting the message. + /// The string to format. + /// Optional arguments for formatting the given string. + /// The formatted string. + private static string FormatString(CultureInfo culture, string unformatted, params object[] args) + { + // Based on the one in Shared/ResourceUtilities. + string formatted = unformatted; + + // NOTE: String.Format() does not allow a null arguments array + if ((args != null) && (args.Length > 0)) + { +#if DEBUG && !BUILDING_DF_LKG + +#if VALIDATERESOURCESTRINGS + // The code below reveals many places in our codebase where + // we're not using all of the data given to us to format + // strings -- but there are too many to presently fix. + // Rather than toss away the code, we should later build it + // and fix each offending resource (or the code processing + // the resource). + + // String.Format() will throw a FormatException if args does + // not have enough elements to match each format parameter. + // However, it provides no feedback in the case when args contains + // more elements than necessary to replace each format + // parameter. We'd like to know if we're providing too much + // data in cases like these, so we'll fail if this code runs. + + // We create an array with one fewer element + object[] trimmedArgs = new object[args.Length - 1]; + Array.Copy(args, 0, trimmedArgs, 0, args.Length - 1); + + bool caughtFormatException = false; + try + { + // This will throw if there aren't enough elements in trimmedArgs... + String.Format(CultureInfo.CurrentCulture, unformatted, trimmedArgs); + } + catch (FormatException) + { + caughtFormatException = true; + } + + // If we didn't catch an exception above, then some of the elements + // of args were unnecessary when formatting unformatted... + Debug.Assert + ( + caughtFormatException, + String.Format("The provided format string '{0}' had fewer format parameters than the number of format args, '{1}'.", unformatted, args.Length) + ); +#endif + // If you accidentally pass some random type in that can't be converted to a string, + // FormatResourceString calls ToString() which returns the full name of the type! + foreach (object param in args) + { + // Check against a list of types that we know have + // overridden ToString() usefully. If you want to pass + // another one, add it here. + if (param != null && param.ToString() == param.GetType().FullName) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Invalid type for message formatting argument, was {0}", param.GetType().FullName)); + } + } +#endif + // Format the string, using the variable arguments passed in. + // NOTE: all String methods are thread-safe + try + { + formatted = String.Format(culture, unformatted, args); + } + catch (FormatException ex) + { + // User task may have logged something with too many format parameters + // We don't have resources in this assembly, and we generally log stack for task failures so they can be fixed by the owner + // However, we don't want to crash the logger and stop the build. + // Error will look like this (it's OK to not localize subcategory). It's not too bad, although there's no file. + // + // Task "Crash" + // (16,14): error : "This message logged from a task {1} has too few formatting parameters." + // at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, Object[] args) + // at System.String.Format(IFormatProvider provider, String format, Object[] args) + // at Microsoft.Build.Framework.LazyFormattedBuildEventArgs.FormatString(CultureInfo culture, String unformatted, Object[] args) in d:\W8T_Refactor\src\vsproject\xmake\Framework\LazyFormattedBuildEventArgs.cs:line 263 + // Done executing task "Crash". + // + // T + formatted = String.Format(CultureInfo.CurrentCulture, "\"{0}\"\n{1}", unformatted, ex.ToString()); + } + } + + return formatted; + } + + /// + /// Deserialization does not call any constructors, not even + /// the parameterless constructor. Therefore since we do not serialize + /// this field, we must populate it here. + /// + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + _locker = new Object(); + } + } +} diff --git a/src/Framework/LoadInSeparateAppDomainAttribute.cs b/src/Framework/LoadInSeparateAppDomainAttribute.cs new file mode 100644 index 00000000000..c464b35a413 --- /dev/null +++ b/src/Framework/LoadInSeparateAppDomainAttribute.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// This attribute is used to mark tasks that need to be run in their own app domains. The build engine will create a new app + /// domain each time it needs to run such a task, and immediately unload it when the task is finished. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class LoadInSeparateAppDomainAttribute : Attribute + { + /// + /// Default constructor. + /// + public LoadInSeparateAppDomainAttribute() + { + // do nothing + } + } +} diff --git a/src/Framework/LoggerException.cs b/src/Framework/LoggerException.cs new file mode 100644 index 00000000000..df9e9c6df70 --- /dev/null +++ b/src/Framework/LoggerException.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; // for SecurityPermissionAttribute + +namespace Microsoft.Build.Framework +{ + /// + /// Exception that should be thrown by a logger when it cannot continue. + /// Allows a logger to force the build to stop in an explicit way, when, for example, it + /// receives invalid parameters, or cannot write to disk. + /// + /// + /// WARNING: marking a type [Serializable] without implementing ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both forward and backward compatibility + /// + [Serializable] + public class LoggerException : Exception + { + /// + /// Default constructor. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use the rich constructor instead. + /// + public LoggerException() + { + // do nothing + // if message is null, the base class provides a default string. + } + + /// + /// Creates an instance of this exception using the specified error message. + /// + /// Message string + public LoggerException(string message) + : base(message, null) + { + // We do no verification of these parameters. + // if message is null, the base class provides a default string. + } + + /// + /// Creates an instance of this exception using the specified error message and inner exception. + /// + /// Message string + /// Inner exception. Can be null + public LoggerException(string message, Exception innerException) + : base(message, innerException) + { + // We do no verification of these parameters. Any can be null; + // if message is null, the base class provides a default string. + } + + /// + /// Creates an instance of this exception using rich error information. + /// + /// Message string + /// Inner exception. Can be null + /// Error code + /// Help keyword for host IDE. Can be null + public LoggerException(string message, Exception innerException, string errorCode, string helpKeyword) + : this(message, innerException) + { + // We do no verification of these parameters. Any can be null. + _errorCode = errorCode; + _helpKeyword = helpKeyword; + } + + #region Serialization (update when adding new class members) + + /// + /// Protected constructor used for (de)serialization. + /// If we ever add new members to this class, we'll need to update this. + /// + /// Serialization info + /// Streaming context + protected LoggerException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _errorCode = info.GetString("errorCode"); + _helpKeyword = info.GetString("helpKeyword"); + } + + /// + /// ISerializable method which we must override since Exception implements this interface + /// If we ever add new members to this class, we'll need to update this. + /// + /// Serialization info + /// Streaming context + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + override public void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("errorCode", _errorCode); + info.AddValue("helpKeyword", _helpKeyword); + } + + #endregion + + #region Properties + + /// + /// Gets the error code associated with this exception's message (not the inner exception). + /// + /// The error code string. + public string ErrorCode + { + get + { + return _errorCode; + } + } + + /// + /// Gets the F1-help keyword associated with this error, for the host IDE. + /// + /// The keyword string. + public string HelpKeyword + { + get + { + return _helpKeyword; + } + } + + #endregion + + // the error code for this exception's message (not the inner exception) + private string _errorCode; + // the F1-help keyword for the host IDE + private string _helpKeyword; + } +} diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj new file mode 100644 index 00000000000..282a16c68da --- /dev/null +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -0,0 +1,172 @@ + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Library + Properties + Microsoft.Build.Framework + Microsoft.Build.Framework + + + + + + + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + true + + + true + + + true + + + true + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Framework/OutputAttribute.cs b/src/Framework/OutputAttribute.cs new file mode 100644 index 00000000000..84125a0eb1b --- /dev/null +++ b/src/Framework/OutputAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// This attribute is used by task writers to designate certain task parameters as "outputs". The build engine will only allow + /// task parameters (i.e. the task class' .NET properties) that are marked with this attribute to output data from a task. Project + /// authors can only use parameters marked with this attribute in a task's <Output> tag. All task parameters, including those + /// marked with this attribute, may be treated as inputs to a task by the build engine. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class OutputAttribute : Attribute + { + /// + /// Default constructor. + /// + public OutputAttribute() + { + // do nothing + } + } +} diff --git a/src/Framework/ProjectFinishedEventArgs.cs b/src/Framework/ProjectFinishedEventArgs.cs new file mode 100644 index 00000000000..f87733911cc --- /dev/null +++ b/src/Framework/ProjectFinishedEventArgs.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for project finished events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class ProjectFinishedEventArgs : BuildStatusEventArgs + { + /// + /// Default constructor + /// + protected ProjectFinishedEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// name of the project + /// true indicates project built successfully + public ProjectFinishedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + bool succeeded + ) + : this(message, helpKeyword, projectFile, succeeded, DateTime.UtcNow) + { + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". This constructor allows the timestamp to be set as well + /// + /// text message + /// help keyword + /// name of the project + /// true indicates project built successfully + /// Timestamp when the event was created + public ProjectFinishedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + bool succeeded, + DateTime eventTimestamp + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp) + { + _projectFile = projectFile; + _succeeded = succeeded; + } + + private string _projectFile; + private bool _succeeded; + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + + writer.Write(_succeeded); + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + + _succeeded = reader.ReadBoolean(); + } + #endregion + + /// + /// Project name + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + /// + /// True if project built successfully, false otherwise + /// + public bool Succeeded + { + get + { + return _succeeded; + } + } + } +} diff --git a/src/Framework/ProjectStartedEventArgs.cs b/src/Framework/ProjectStartedEventArgs.cs new file mode 100644 index 00000000000..404ff7df3b1 --- /dev/null +++ b/src/Framework/ProjectStartedEventArgs.cs @@ -0,0 +1,551 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; +using System.Collections; +using System.IO; +using System.Runtime.Serialization; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguements for project started events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class ProjectStartedEventArgs : BuildStatusEventArgs + { + #region Constants + public const int InvalidProjectId = -1; + #endregion + + /// + /// Default constructor + /// + protected ProjectStartedEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// project name + /// targets we are going to build (empty indicates default targets) + /// list of properties + /// list of items + public ProjectStartedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + string targetNames, + IEnumerable properties, + IEnumerable items + ) + : this(message, helpKeyword, projectFile, targetNames, properties, items, DateTime.UtcNow) + { + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// project id + /// text message + /// help keyword + /// project name + /// targets we are going to build (empty indicates default targets) + /// list of properties + /// list of items + /// event context info for the parent project + public ProjectStartedEventArgs + ( + int projectId, + string message, + string helpKeyword, + string projectFile, + string targetNames, + IEnumerable properties, + IEnumerable items, + BuildEventContext parentBuildEventContext + ) + : this(projectId, message, helpKeyword, projectFile, targetNames, properties, items, parentBuildEventContext, DateTime.UtcNow) + { + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// project id + /// text message + /// help keyword + /// project name + /// targets we are going to build (empty indicates default targets) + /// list of properties + /// list of items + /// event context info for the parent project + public ProjectStartedEventArgs + ( + int projectId, + string message, + string helpKeyword, + string projectFile, + string targetNames, + IEnumerable properties, + IEnumerable items, + BuildEventContext parentBuildEventContext, + IDictionary globalProperties, + string toolsVersion + ) + : this(projectId, message, helpKeyword, projectFile, targetNames, properties, items, parentBuildEventContext) + { + this.GlobalProperties = globalProperties; + this.ToolsVersion = toolsVersion; + } + + /// + /// This constructor allows event data to be initialized. Also the timestamp can be set + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// project name + /// targets we are going to build (empty indicates default targets) + /// list of properties + /// list of items + public ProjectStartedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + string targetNames, + IEnumerable properties, + IEnumerable items, + DateTime eventTimestamp + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp) + { + _projectFile = projectFile; + + if (targetNames == null) + { + _targetNames = String.Empty; + } + else + { + _targetNames = targetNames; + } + + _properties = properties; + _items = items; + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// project id + /// text message + /// help keyword + /// project name + /// targets we are going to build (empty indicates default targets) + /// list of properties + /// list of items + /// event context info for the parent project + public ProjectStartedEventArgs + ( + int projectId, + string message, + string helpKeyword, + string projectFile, + string targetNames, + IEnumerable properties, + IEnumerable items, + BuildEventContext parentBuildEventContext, + DateTime eventTimestamp + ) + : this(message, helpKeyword, projectFile, targetNames, properties, items, eventTimestamp) + { + _parentProjectBuildEventContext = parentBuildEventContext; + _projectId = projectId; + } + + // ProjectId is only contained in the project started event. + // This number indicated the instance id of the project and can be + // used when debugging to determine if two projects with the same name + // are the same project instance or different instances + [OptionalField(VersionAdded = 2)] + private int _projectId; + + public int ProjectId + { + get + { + return _projectId; + } + } + + [OptionalField(VersionAdded = 2)] + private BuildEventContext _parentProjectBuildEventContext; + + /// + /// Event context information, where the event was fired from in terms of the build location + /// + public BuildEventContext ParentProjectBuildEventContext + { + get + { + return _parentProjectBuildEventContext; + } + } + + /// + /// The name of the project file + /// + private string _projectFile; + + /// + /// Project name + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + /// + /// Targets that we will build in the project + /// + private string _targetNames; + + /// + /// Targets that we will build in the project + /// + public string TargetNames + { + get + { + return _targetNames; + } + } + + /// + /// Gets the set of global properties used to evaluate this project. + /// + [OptionalField(VersionAdded = 2)] + private IDictionary _globalProperties; + + /// + /// Gets the set of global properties used to evaluate this project. + /// + public IDictionary GlobalProperties + { + get + { + return _globalProperties; + } + + internal set + { + _globalProperties = value; + } + } + + [OptionalField(VersionAdded = 2)] + private string _toolsVersion; + + /// + /// Gets the tools version used to evaluate this project. + /// + public string ToolsVersion + { + get + { + return _toolsVersion; + } + + internal set + { + _toolsVersion = value; + } + } + + // IEnumerable is not a serializable type. That is okay because + // (a) this event will not be thrown by tasks, so it should not generally cross AppDomain boundaries + // (b) this event still makes sense when this field is "null" + [NonSerialized] + private IEnumerable _properties; + + /// + /// List of properties in this project. This is a live, read-only list. + /// + public IEnumerable Properties + { + get + { + // UNDONE: (Serialization.) Rather than storing the properties directly in this class, we could + // grab them from the BuildRequestConfiguration associated with this projectId (which is now poorly + // named because it is actually the BuildRequestConfigurationId.) For central loggers in the + // multi-proc case, this could pull up just the global properties used to start the project. For + // distributed loggers in the multi-proc case and all loggers in the single-proc case, this could pull + // up the live list of properties from the loaded project, which is stored in the configuration as well. + // By doing this, we no longer need to transmit properties using this message because they've already + // been transmitted as part of the BuildRequestConfiguration. + return _properties; + } + } + + // IEnumerable is not a serializable type. That is okay because + // (a) this event will not be thrown by tasks, so it should not generally cross AppFomain boundaries + // (b) this event still makes sense when this field is "null" + [NonSerialized] + private IEnumerable _items; + + /// + /// List of items in this project. This is a live, read-only list. + /// + public IEnumerable Items + { + get + { + // UNDONE: (Serialization.) Currently there is a "bug" in the old OM in that items are not transported to + // the central logger in the multi-proc case. No one uses this though, so it's probably no big deal. In + // the new OM, this list of items could come directly from the BuildRequestConfiguration, which has access + // to the loaded project. For distributed loggers in the multi-proc case and all loggers in the single-proc + // case, this access is to the live list. For the central logger in the multi-proc case, the main node + // has likely not loaded this project, and therefore the live items would not be available to them, which is + // the same as the current functionality. + return _items; + } + } + + #region CustomSerializationToStream + + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + writer.Write((Int32)_projectId); + #region ParentProjectBuildEventContext + if (_parentProjectBuildEventContext == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write((Int32)_parentProjectBuildEventContext.NodeId); + writer.Write((Int32)_parentProjectBuildEventContext.ProjectContextId); + writer.Write((Int32)_parentProjectBuildEventContext.TargetId); + writer.Write((Int32)_parentProjectBuildEventContext.TaskId); + writer.Write((Int32)_parentProjectBuildEventContext.SubmissionId); + writer.Write((Int32)_parentProjectBuildEventContext.ProjectInstanceId); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + + #region TargetNames + // TargetNames cannot be null as per line 61 in the constructor + writer.Write(_targetNames); + #endregion + + #region Properties + + Dictionary propertyList = GeneratePropertyList(); + + // If no properties were added to the property list + // then we have nothing to create when it is deserialized + // This can happen if properties is null or if none of the + // five properties were found in the property object. + if ((propertyList == null || propertyList.Count == 0)) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + + // Write how many properties we are going to write into the stream + writer.Write((Int32)propertyList.Count); + + // Write the actual property name value pairs into the stream + foreach (KeyValuePair propertyPair in propertyList) + { + writer.Write(propertyPair.Key); + writer.Write(propertyPair.Value); + } + } + + #endregion + } + + /// + /// Generates a list of KeyValuePairs from the properties enumerator. + /// For each of these properties add them to a list to return to the caller. + /// + /// Null if properties is null, or a list containing one or more of the properties in the properties enumerator + private Dictionary GeneratePropertyList() + { + if (_properties == null) + { + return null; + } + + Dictionary propertyList = new Dictionary(); + + // Loop through the properties and add them to the keyvalue pair list + foreach (DictionaryEntry property in _properties) + { + object propertyKey = property.Key; + object propertyValue = property.Value; + + // Make sure property keys and values are not null before casting. + // property key and value will always be a string, if this is not the case + // the a cast exception is the correct course of action. + if (property.Key != null && property.Value != null) + { + propertyList.Add((string)property.Key, (string)property.Value); + } + } + return propertyList; + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + _projectId = reader.ReadInt32(); + #region ParentProjectBuildEventContext + if (reader.ReadByte() == 0) + { + _parentProjectBuildEventContext = null; + } + else + { + int nodeId = reader.ReadInt32(); + int projectContextId = reader.ReadInt32(); + int targetId = reader.ReadInt32(); + int taskId = reader.ReadInt32(); + + if (version > 20) + { + int submissionId = reader.ReadInt32(); + int projectInstanceId = reader.ReadInt32(); + _parentProjectBuildEventContext = new BuildEventContext(submissionId, nodeId, projectInstanceId, projectContextId, targetId, taskId); + } + else + { + _parentProjectBuildEventContext = new BuildEventContext(nodeId, targetId, projectContextId, taskId); + } + } + #endregion + #region ProjectFile + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + #endregion + #region TargetNames + // TargetNames cannot be null as per line 61 in the constructor + _targetNames = reader.ReadString(); + #endregion + #region Properties + + // Check to see if properties was null + if (reader.ReadByte() == 0) + { + _properties = null; + } + else + { + // Get number of properties put on the stream + int numberOfProperties = reader.ReadInt32(); + + // We need to use a dictionaryEntry as that is what the old behavior was + ArrayList dictionaryList = new ArrayList(numberOfProperties); + + // Read off each of the key value pairs and put them into the dictionaryList + for (int i = 0; i < numberOfProperties; i++) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + + if (key != null && value != null) + { + DictionaryEntry entry = new DictionaryEntry(key, value); + dictionaryList.Add(entry); + } + } + + _properties = dictionaryList; + } + + #endregion + } + #endregion + + #region SerializationSection + [OnDeserializing] // Will happen before the object is deserialized + private void SetDefaultsBeforeSerialization(StreamingContext sc) + { + _projectId = InvalidProjectId; + // Dont want to set the default before deserialization is completed to a new event context because + // that would most likely be a lot of wasted allocations + _parentProjectBuildEventContext = null; + } + + [OnDeserialized] + private void SetDefaultsAfterSerialization(StreamingContext sc) + { + if (_parentProjectBuildEventContext == null) + { + _parentProjectBuildEventContext = new BuildEventContext + ( + BuildEventContext.InvalidNodeId, + BuildEventContext.InvalidTargetId, + BuildEventContext.InvalidProjectContextId, + BuildEventContext.InvalidTaskId + ); + } + } + #endregion + } +} diff --git a/src/Framework/RequiredAttribute.cs b/src/Framework/RequiredAttribute.cs new file mode 100644 index 00000000000..5971f488767 --- /dev/null +++ b/src/Framework/RequiredAttribute.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// This class defines the attribute that a task writer can apply to a task's property to declare the property to be a + /// required property. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class RequiredAttribute : Attribute + { + /// + /// Default constructor. + /// + public RequiredAttribute() + { + } + } +} diff --git a/src/Framework/RequiredRuntimeAttribute.cs b/src/Framework/RequiredRuntimeAttribute.cs new file mode 100644 index 00000000000..0e343be56af --- /dev/null +++ b/src/Framework/RequiredRuntimeAttribute.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// When marked with the RequiredRuntimeAttribute, a task indicates that it has stricter + /// runtime requirements than a regular task - this tells MSBuild that it will need to potentially + /// launch a separate process for that task if the current runtime does not match the version requirement. + /// This attribute is currently non-functional since there is only one version of the CLR that is + /// capable of running MSBuild v2.0 or v3.5 - the runtime v2.0 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class RequiredRuntimeAttribute : Attribute + { + /// + /// Constructor taking a version, such as "v2.0". + /// + public RequiredRuntimeAttribute(string runtimeVersion) + { + _runtimeVersion = runtimeVersion; + } + + private string _runtimeVersion; + + /// + /// Returns the runtime version the attribute was constructed with, + /// e.g., "v2.0" + /// + public string RuntimeVersion + { + get + { + return _runtimeVersion; + } + } + } +} diff --git a/src/Framework/RunInMTAAttribute.cs b/src/Framework/RunInMTAAttribute.cs new file mode 100644 index 00000000000..88452643cfc --- /dev/null +++ b/src/Framework/RunInMTAAttribute.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Attribute which tells MSBuild the task may run in any thread. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Framework +{ + /// + /// This attribute is used to mark a task class as explicitly not being required to run in the STA for COM. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "MTA", Justification = "It is cased correctly.")] + public sealed class RunInMTAAttribute : Attribute + { + /// + /// Default constructor. + /// + public RunInMTAAttribute() + { + // do nothing + } + } +} \ No newline at end of file diff --git a/src/Framework/RunInSTAAttribute.cs b/src/Framework/RunInSTAAttribute.cs new file mode 100644 index 00000000000..c9c75f102cb --- /dev/null +++ b/src/Framework/RunInSTAAttribute.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Attribute which tells MSBuild the task must run in an STA thread. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Framework +{ + /// + /// This attribute is used to mark a task class as being required to run in a Single Threaded Apartment for COM. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "STA", Justification = "It is cased correctly.")] + public sealed class RunInSTAAttribute : Attribute + { + /// + /// Default constructor. + /// + public RunInSTAAttribute() + { + // do nothing + } + } +} \ No newline at end of file diff --git a/src/Framework/TargetFinishedEventArgs.cs b/src/Framework/TargetFinishedEventArgs.cs new file mode 100644 index 00000000000..580cfc52fae --- /dev/null +++ b/src/Framework/TargetFinishedEventArgs.cs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; +using System.IO; +using System.Collections; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for target finished events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class TargetFinishedEventArgs : BuildStatusEventArgs + { + /// + /// Default constructor + /// + protected TargetFinishedEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// target name + /// project file + /// file in which the target is defined + /// true if target built successfully + public TargetFinishedEventArgs + ( + string message, + string helpKeyword, + string targetName, + string projectFile, + string targetFile, + bool succeeded + ) + : this(message, helpKeyword, targetName, projectFile, targetFile, succeeded, DateTime.UtcNow, null) + { + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// target name + /// project file + /// file in which the target is defined + /// true if target built successfully + /// Target output items for the target. If batching will be null for everything except for the last target in the batch + public TargetFinishedEventArgs + ( + string message, + string helpKeyword, + string targetName, + string projectFile, + string targetFile, + bool succeeded, + IEnumerable targetOutputs + ) + : this(message, helpKeyword, targetName, projectFile, targetFile, succeeded, DateTime.UtcNow, targetOutputs) + { + } + + /// + /// This constructor allows event data to be initialized including the timestamp when the event was created. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// target name + /// project file + /// file in which the target is defined + /// true if target built successfully + /// Timestamp when the event was created + public TargetFinishedEventArgs + ( + string message, + string helpKeyword, + string targetName, + string projectFile, + string targetFile, + bool succeeded, + DateTime eventTimestamp, + IEnumerable targetOutputs + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp) + { + _targetName = targetName; + _succeeded = succeeded; + _projectFile = projectFile; + _targetFile = targetFile; + _targetOutputs = targetOutputs; + } + + private string _projectFile; + private string _targetFile; + private string _targetName; + private bool _succeeded; + private IEnumerable _targetOutputs; + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + #region TargetFile + if (_targetFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_targetFile); + } + #endregion TargetFile + #region TargetName + if (_targetName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_targetName); + } + #endregion + writer.Write(_succeeded); + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + #region ProjectFile + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + #endregion + #region TargetFile + if (reader.ReadByte() == 0) + { + _targetFile = null; + } + else + { + _targetFile = reader.ReadString(); + } + #endregion + #region TargetName + if (reader.ReadByte() == 0) + { + _targetName = null; + } + else + { + _targetName = reader.ReadString(); + } + #endregion + _succeeded = reader.ReadBoolean(); + } + #endregion + + /// + /// Target name + /// + public string TargetName + { + get + { + return _targetName; + } + } + + /// + /// True if target built successfully, false otherwise + /// + public bool Succeeded + { + get + { + return _succeeded; + } + } + + /// + /// Project file associated with event. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + /// + /// File where this target was declared. + /// + public string TargetFile + { + get + { + return _targetFile; + } + } + + /// + /// Target outputs + /// + public IEnumerable TargetOutputs + { + get + { + return _targetOutputs; + } + + set + { + _targetOutputs = value; + } + } + } +} diff --git a/src/Framework/TargetStartedEventArgs.cs b/src/Framework/TargetStartedEventArgs.cs new file mode 100644 index 00000000000..377c4cc8c99 --- /dev/null +++ b/src/Framework/TargetStartedEventArgs.cs @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for target started events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class TargetStartedEventArgs : BuildStatusEventArgs + { + /// + /// Default constructor + /// + protected TargetStartedEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// target name + /// project file + /// file in which the target is defined + public TargetStartedEventArgs + ( + string message, + string helpKeyword, + string targetName, + string projectFile, + string targetFile + ) + : this(message, helpKeyword, targetName, projectFile, targetFile, String.Empty, DateTime.UtcNow) + { + } + + /// + /// This constructor allows event data to be initialized including the timestamp when the event was created. + /// + /// text message + /// help keyword + /// target name + /// project file + /// file in which the target is defined + /// Timestamp when the event was created + public TargetStartedEventArgs + ( + string message, + string helpKeyword, + string targetName, + string projectFile, + string targetFile, + string parentTarget, + DateTime eventTimestamp + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp) + { + _targetName = targetName; + _projectFile = projectFile; + _targetFile = targetFile; + _parentTarget = parentTarget; + } + + private string _targetName; + private string _projectFile; + private string _targetFile; + private string _parentTarget; + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + #region TargetName + if (_targetName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_targetName); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + #region TargetFile + if (_targetFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_targetFile); + } + #endregion + #region ParentTarget + if (_parentTarget == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_parentTarget); + } + #endregion + } + + /// + /// Deserializes from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + #region TargetName + if (reader.ReadByte() == 0) + { + _targetName = null; + } + else + { + _targetName = reader.ReadString(); + } + #endregion + #region ProjectFile + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + #endregion + #region TargetFile + if (reader.ReadByte() == 0) + { + _targetFile = null; + } + else + { + _targetFile = reader.ReadString(); + } + #endregion + #region ParentTarget + if (version > 20) + { + if (reader.ReadByte() == 0) + { + _parentTarget = null; + } + else + { + _parentTarget = reader.ReadString(); + } + } + #endregion + } + #endregion + + /// + /// target name + /// + public string TargetName + { + get + { + return _targetName; + } + } + + /// + /// Target which caused this target to build + /// + public string ParentTarget + { + get + { + return _parentTarget; + } + } + + /// + /// Project file associated with event. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + /// + /// File where this target was declared. + /// + public string TargetFile + { + get + { + return _targetFile; + } + } + } +} diff --git a/src/Framework/TaskCommandLineEventArgs.cs b/src/Framework/TaskCommandLineEventArgs.cs new file mode 100644 index 00000000000..63f1edada11 --- /dev/null +++ b/src/Framework/TaskCommandLineEventArgs.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Framework +{ + /// + /// This class is used by tasks to log their command lines. This class extends + /// so that command lines can be logged as + /// messages. Logging a command line is only relevant for tasks that wrap an + /// underlying executable/tool, or emulate a shell command. Tasks that have + /// no command line equivalent should not raise this extended message event. + /// + /// + /// WARNING: marking a type [Serializable] without implementing ISerializable + /// imposes a serialization contract -- it is a promise to never change the + /// type's fields i.e. the type is immutable; adding new fields in the next + /// version of the type without following certain special FX guidelines, can + /// break both forward and backward compatibility + /// + [Serializable] + public class TaskCommandLineEventArgs : BuildMessageEventArgs + { + /// + /// Default (family) constructor. + /// + protected TaskCommandLineEventArgs() + : base() + { + // do nothing + } + + /// + /// Creates an instance of this class for the given task command line. + /// + /// The command line used by a task to launch + /// its underlying tool/executable. + /// The name of the task raising this event. + /// Importance of command line -- controls whether + /// the command line will be displayed by less verbose loggers. + public TaskCommandLineEventArgs + ( + string commandLine, + string taskName, + MessageImportance importance + ) + : this(commandLine, taskName, importance, DateTime.UtcNow) + { + // do nothing + } + + + /// + /// Creates an instance of this class for the given task command line. This constructor allows the timestamp to be set + /// + /// The command line used by a task to launch + /// its underlying tool/executable. + /// The name of the task raising this event. + /// Importance of command line -- controls whether + /// the command line will be displayed by less verbose loggers. + /// Timestamp when the event was created + public TaskCommandLineEventArgs + ( + string commandLine, + string taskName, + MessageImportance importance, + DateTime eventTimestamp + ) + : base(commandLine, null, taskName, importance, eventTimestamp) + { + // do nothing + } + + /// + /// Gets the task command line associated with this event. + /// + public string CommandLine + { + get + { + return Message; + } + } + + /// + /// Gets the name of the task that raised this event. + /// + public string TaskName + { + get + { + return SenderName; + } + } + } +} diff --git a/src/Framework/TaskFinishedEventArgs.cs b/src/Framework/TaskFinishedEventArgs.cs new file mode 100644 index 00000000000..e0c32e440f5 --- /dev/null +++ b/src/Framework/TaskFinishedEventArgs.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for target finished event. +//----------------------------------------------------------------------- + +using System.Runtime.InteropServices; +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for task finished events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class TaskFinishedEventArgs : BuildStatusEventArgs + { + /// + /// Default constructor + /// + protected TaskFinishedEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// project file + /// file in which the task is defined + /// task name + /// true indicates task succeed + public TaskFinishedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + string taskFile, + string taskName, + bool succeeded + ) + : this(message, helpKeyword, projectFile, taskFile, taskName, succeeded, DateTime.UtcNow) + { + } + + /// + /// This constructor allows event data to be initialized and the timestamp to be set + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// project file + /// file in which the task is defined + /// task name + /// true indicates task succeed + /// Timestamp when event was created + public TaskFinishedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + string taskFile, + string taskName, + bool succeeded, + DateTime eventTimestamp + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp) + { + _taskName = taskName; + _taskFile = taskFile; + _succeeded = succeeded; + _projectFile = projectFile; + } + + private string _taskName; + private string _projectFile; + private string _taskFile; + private bool _succeeded; + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + #region TaskName + if (_taskName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_taskName); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + #region TaskFile + if (_taskFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_taskFile); + } + #endregion + writer.Write(_succeeded); + } + + /// + /// Deserializes the Errorevent from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + #region TaskName + if (reader.ReadByte() == 0) + { + _taskName = null; + } + else + { + _taskName = reader.ReadString(); + } + #endregion + #region ProjectFile + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + #endregion + #region TaskFile + if (reader.ReadByte() == 0) + { + _taskFile = null; + } + else + { + _taskFile = reader.ReadString(); + } + #endregion + _succeeded = reader.ReadBoolean(); + } + #endregion + + /// + /// Task Name + /// + public string TaskName + { + get + { + return _taskName; + } + } + + /// + /// True if target built successfully, false otherwise + /// + public bool Succeeded + { + get + { + return _succeeded; + } + } + + /// + /// Project file associated with event. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + /// + /// MSBuild file where this task was defined. + /// + public string TaskFile + { + get + { + return _taskFile; + } + } + } +} diff --git a/src/Framework/TaskPropertyInfo.cs b/src/Framework/TaskPropertyInfo.cs new file mode 100644 index 00000000000..8858ed0b164 --- /dev/null +++ b/src/Framework/TaskPropertyInfo.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Stores the parameter information which will be passed to inline tasks to use as a set of parameters to create +//----------------------------------------------------------------------- + +using System; +using System.Reflection; + +namespace Microsoft.Build.Framework +{ + /// + /// Class which represents the parameter information from the using task as a strongly typed class. + /// + [Serializable] + public class TaskPropertyInfo + { + /// + /// Encapsulates a list of parameters declared in the UsingTask + /// + /// Name of the parameter + /// The actual type of the parameter + /// True if the parameter is both an output and and input parameter. False if the parameter is only an input parameter + /// True if the parameter must be supplied to each invocation of the task. + public TaskPropertyInfo(string name, Type typeOfParameter, bool output, bool required) + { + Name = name; + PropertyType = typeOfParameter; + Output = output; + Required = required; + } + + /// + /// The type of the property + /// + public Type PropertyType { get; private set; } + + /// + /// Name of the property + /// + public string Name { get; private set; } + + /// + /// This task parameter is an output parameter (analogous to [Output] attribute) + /// + public bool Output { get; private set; } + + /// + /// This task parameter is required (analogous to the [Required] attribute) + /// + public bool Required { get; private set; } + } +} diff --git a/src/Framework/TaskStartedEventArgs.cs b/src/Framework/TaskStartedEventArgs.cs new file mode 100644 index 00000000000..d0216d4c639 --- /dev/null +++ b/src/Framework/TaskStartedEventArgs.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Event args for task started event. +//----------------------------------------------------------------------- + +using System; +using System.IO; + +namespace Microsoft.Build.Framework +{ + /// + /// Arguments for task started events + /// + /// + /// WARNING: marking a type [Serializable] without implementing + /// ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is + /// immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both + /// forward and backward compatibility + /// + [Serializable] + public class TaskStartedEventArgs : BuildStatusEventArgs + { + /// + /// Default constructor + /// + protected TaskStartedEventArgs() + : base() + { + // do nothing + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// project file + /// file in which the task is defined + /// task name + public TaskStartedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + string taskFile, + string taskName + ) + : this(message, helpKeyword, projectFile, taskFile, taskName, DateTime.UtcNow) + { + } + + /// + /// This constructor allows event data to be initialized. + /// Sender is assumed to be "MSBuild". + /// + /// text message + /// help keyword + /// project file + /// file in which the task is defined + /// task name + /// Timestamp when event was created + public TaskStartedEventArgs + ( + string message, + string helpKeyword, + string projectFile, + string taskFile, + string taskName, + DateTime eventTimestamp + ) + : base(message, helpKeyword, "MSBuild", eventTimestamp) + { + _taskName = taskName; + _projectFile = projectFile; + _taskFile = taskFile; + } + + private string _taskName; + private string _projectFile; + private string _taskFile; + + #region CustomSerializationToStream + /// + /// Serializes to a stream through a binary writer + /// + /// Binary writer which is attached to the stream the event will be serialized into + internal override void WriteToStream(BinaryWriter writer) + { + base.WriteToStream(writer); + #region TaskName + if (_taskName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_taskName); + } + #endregion + #region ProjectFile + if (_projectFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_projectFile); + } + #endregion + #region TaskFile + if (_taskFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_taskFile); + } + #endregion + } + + /// + /// Deserializes the Errorevent from a stream through a binary reader + /// + /// Binary reader which is attached to the stream the event will be deserialized from + /// The version of the runtime the message packet was created from + internal override void CreateFromStream(BinaryReader reader, int version) + { + base.CreateFromStream(reader, version); + #region TaskName + if (reader.ReadByte() == 0) + { + _taskName = null; + } + else + { + _taskName = reader.ReadString(); + } + #endregion + #region ProjectFile + if (reader.ReadByte() == 0) + { + _projectFile = null; + } + else + { + _projectFile = reader.ReadString(); + } + #endregion + #region TaskFile + if (reader.ReadByte() == 0) + { + _taskFile = null; + } + else + { + _taskFile = reader.ReadString(); + } + #endregion + } + #endregion + + /// + /// Task name. + /// + public string TaskName + { + get + { + return _taskName; + } + } + + /// + /// Project file associated with event. + /// + public string ProjectFile + { + get + { + return _projectFile; + } + } + + /// + /// MSBuild file where this task was defined. + /// + public string TaskFile + { + get + { + return _taskFile; + } + } + } +} diff --git a/src/Framework/UnitTests/Attribute_Tests.cs b/src/Framework/UnitTests/Attribute_Tests.cs new file mode 100644 index 00000000000..04cb8bbef5c --- /dev/null +++ b/src/Framework/UnitTests/Attribute_Tests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class AttributeTests + { + /// + /// Test RequiredRuntimeAttribute + /// + [TestMethod] + public void RequiredRuntimeAttribute() + { + RequiredRuntimeAttribute attribute = + (RequiredRuntimeAttribute)Attribute.GetCustomAttribute(typeof(X), typeof(RequiredRuntimeAttribute)); + + Assert.AreEqual("v5", attribute.RuntimeVersion); + } + + [TestMethod] + public void OutputAttribute() + { + OutputAttribute attribute = + (OutputAttribute)Attribute.GetCustomAttribute(typeof(X).GetMember("TestValue2", BindingFlags.NonPublic | BindingFlags.Static)[0], typeof(OutputAttribute)); + Assert.IsNotNull(attribute); + } + + [TestMethod] + public void RequiredAttribute() + { + RequiredAttribute attribute = + (RequiredAttribute)Attribute.GetCustomAttribute(typeof(X).GetMember("TestValue", BindingFlags.NonPublic | BindingFlags.Static)[0], typeof(RequiredAttribute)); + Assert.IsNotNull(attribute); + } + } + + /// + /// Sample class with RequiredRuntimeAttribute on it + /// + [RequiredRuntime("v5")] + internal static class X + { + [Required] + internal static bool TestValue + { + get + { + return true; + } + } + + [Output] + internal static bool TestValue2 + { + get + { + return true; + } + } + } +} + + + + + + diff --git a/src/Framework/UnitTests/BuildErrorEventArgs_Tests.cs b/src/Framework/UnitTests/BuildErrorEventArgs_Tests.cs new file mode 100644 index 00000000000..8e69880cf3b --- /dev/null +++ b/src/Framework/UnitTests/BuildErrorEventArgs_Tests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for BuildErrorEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the BuildErrorEventArg class. + /// + [TestClass] + public class BuildErrorEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private BuildErrorEventArgs _baseErrorEvent = new BuildErrorEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "sender"); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + BuildErrorEventArgs beea = new BuildErrorEventArgs2(); + beea = new BuildErrorEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "sender"); + beea = new BuildErrorEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "sender", DateTime.Now); + beea = new BuildErrorEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "{0}", "HelpKeyword", "sender", DateTime.Now, "Messsage"); + beea = new BuildErrorEventArgs(null, null, null, 1, 2, 3, 4, null, null, null); + beea = new BuildErrorEventArgs(null, null, null, 1, 2, 3, 4, null, null, null, DateTime.Now); + beea = new BuildErrorEventArgs(null, null, null, 1, 2, 3, 4, null, null, null, DateTime.Now, null); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path. + /// + private class BuildErrorEventArgs2 : BuildErrorEventArgs + { + /// + /// Test Constructor + /// + public BuildErrorEventArgs2() : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/BuildFinishedEventArgs_Tests.cs b/src/Framework/UnitTests/BuildFinishedEventArgs_Tests.cs new file mode 100644 index 00000000000..d5a836d4a87 --- /dev/null +++ b/src/Framework/UnitTests/BuildFinishedEventArgs_Tests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for BuildFinishedEventArgs_Tests +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the BuildFinishedEventArg class. + /// + [TestClass] + public class BuildFinishedEventArgs_Tests + { + /// + /// Default buildFinished event to use in tests. + /// + private BuildFinishedEventArgs _baseFinishedEvent = new BuildFinishedEventArgs("Message", "HelpKeyword", true); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + BuildFinishedEventArgs buildFinishedEvent = new BuildFinishedEventArgs2(); + buildFinishedEvent = new BuildFinishedEventArgs("Message", "HelpKeyword", true); + buildFinishedEvent = new BuildFinishedEventArgs("Message", "HelpKeyword", true, new DateTime()); + buildFinishedEvent = new BuildFinishedEventArgs("{0}", "HelpKeyword", true, new DateTime(), "Message"); + buildFinishedEvent = new BuildFinishedEventArgs(null, null, true); + buildFinishedEvent = new BuildFinishedEventArgs(null, null, true, new DateTime()); + buildFinishedEvent = new BuildFinishedEventArgs(null, null, true, new DateTime(), null); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class BuildFinishedEventArgs2 : BuildFinishedEventArgs + { + /// + /// Test constructor + /// + public BuildFinishedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/BuildMessageEventArgs_Tests.cs b/src/Framework/UnitTests/BuildMessageEventArgs_Tests.cs new file mode 100644 index 00000000000..333f6e36b9b --- /dev/null +++ b/src/Framework/UnitTests/BuildMessageEventArgs_Tests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for BuildMessageEventArgs_Tests +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the BuildMessageEventArgs class. + /// + [TestClass] + public class BuildMessageEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private BuildMessageEventArgs _baseMessageEvent = new BuildMessageEventArgs("Message", "HelpKeyword", "Sender", MessageImportance.Low); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + BuildMessageEventArgs bmea = new BuildMessageEventArgs2(); + bmea = new BuildMessageEventArgs("Message", "HelpKeyword", "Sender", MessageImportance.Low); + bmea = new BuildMessageEventArgs("Message", "HelpKeyword", "Sender", MessageImportance.Low, DateTime.Now); + bmea = new BuildMessageEventArgs("{0}", "HelpKeyword", "Sender", MessageImportance.Low, DateTime.Now, "Message"); + bmea = new BuildMessageEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "Sender", MessageImportance.Low); + bmea = new BuildMessageEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "Sender", MessageImportance.Low, DateTime.Now); + bmea = new BuildMessageEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "{0}", "HelpKeyword", "Sender", MessageImportance.Low, DateTime.Now, "Message"); + bmea = new BuildMessageEventArgs(null, null, null, MessageImportance.Low); + bmea = new BuildMessageEventArgs(null, null, null, MessageImportance.Low, DateTime.Now); + bmea = new BuildMessageEventArgs(null, null, null, MessageImportance.Low, DateTime.Now, null); + bmea = new BuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, MessageImportance.Low); + bmea = new BuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, MessageImportance.Low, DateTime.Now); + bmea = new BuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, MessageImportance.Low, DateTime.Now, null); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class BuildMessageEventArgs2 : BuildMessageEventArgs + { + /// + /// Default constructor + /// + public BuildMessageEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/BuildStartedEventArgs_Tests.cs b/src/Framework/UnitTests/BuildStartedEventArgs_Tests.cs new file mode 100644 index 00000000000..ba6e9899102 --- /dev/null +++ b/src/Framework/UnitTests/BuildStartedEventArgs_Tests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for BuildStartedEventArgs +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the BuildStartedEventArgs class. + /// + [TestClass] + public class BuildStartedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private BuildStartedEventArgs _baseStartedEvent = new BuildStartedEventArgs("Message", "HelpKeyword"); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + BuildStartedEventArgs bsea = new BuildStartedEventArgs2(); + bsea = new BuildStartedEventArgs("Message", "HelpKeyword"); + bsea = new BuildStartedEventArgs("Message", "HelpKeyword", DateTime.Now); + bsea = new BuildStartedEventArgs("{0}", "HelpKeyword", DateTime.Now, "Message"); + bsea = new BuildStartedEventArgs(null, null); + bsea = new BuildStartedEventArgs(null, null, DateTime.Now); + bsea = new BuildStartedEventArgs(null, null, DateTime.Now, null); + } + + /// + /// Trivially exercise getHashCode. + /// + [TestMethod] + public void TestGetHashCode() + { + _baseStartedEvent.GetHashCode(); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class BuildStartedEventArgs2 : BuildStartedEventArgs + { + /// + /// Default constructor + /// + public BuildStartedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/BuildWarningEventArgs_Tests.cs b/src/Framework/UnitTests/BuildWarningEventArgs_Tests.cs new file mode 100644 index 00000000000..97276c23007 --- /dev/null +++ b/src/Framework/UnitTests/BuildWarningEventArgs_Tests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for BuildWarningEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the BuildWarningEventArgs class. + /// + [TestClass] + public class BuildWarningEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private BuildWarningEventArgs _baseWarningEvent = new BuildWarningEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "sender"); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + BuildWarningEventArgs buildWarningEvent = new BuildWarningEventArgs2(); + buildWarningEvent = new BuildWarningEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "sender"); + buildWarningEvent = new BuildWarningEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "sender", DateTime.Now); + buildWarningEvent = new BuildWarningEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "{0}", "HelpKeyword", "sender", DateTime.Now, "Message"); + buildWarningEvent = new BuildWarningEventArgs(null, null, null, 1, 2, 3, 4, null, null, null); + buildWarningEvent = new BuildWarningEventArgs(null, null, null, 1, 2, 3, 4, null, null, null, DateTime.Now); + buildWarningEvent = new BuildWarningEventArgs(null, null, null, 1, 2, 3, 4, null, null, null, DateTime.Now, null); + } + + /// + /// Trivially exercise getHashCode. + /// + [TestMethod] + public void TestGetHashCode() + { + _baseWarningEvent.GetHashCode(); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class BuildWarningEventArgs2 : BuildWarningEventArgs + { + /// + /// Default constructor + /// + public BuildWarningEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/CriticalBuildMessageEventArgs_Tests.cs b/src/Framework/UnitTests/CriticalBuildMessageEventArgs_Tests.cs new file mode 100644 index 00000000000..520ffbd535f --- /dev/null +++ b/src/Framework/UnitTests/CriticalBuildMessageEventArgs_Tests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for CriticalBuildMessageEventArgs_Tests +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the CriticalBuildMessageEventArgs class. + /// + [TestClass] + public class CriticalBuildMessageEventArgs_Tests + { + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + CriticalBuildMessageEventArgs cbmea = new CriticalBuildMessageEventArgs2(); + cbmea = new CriticalBuildMessageEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "Sender"); + cbmea = new CriticalBuildMessageEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "Sender", DateTime.Now); + cbmea = new CriticalBuildMessageEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "{0}", "HelpKeyword", "Sender", DateTime.Now, "Message"); + cbmea = new CriticalBuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null); + cbmea = new CriticalBuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, DateTime.Now); + cbmea = new CriticalBuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, DateTime.Now, null); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class CriticalBuildMessageEventArgs2 : CriticalBuildMessageEventArgs + { + /// + /// Default constructor + /// + public CriticalBuildMessageEventArgs2() + : base() + { + } + } + } +} diff --git a/src/Framework/UnitTests/CustomEventArgSerialization_Tests.cs b/src/Framework/UnitTests/CustomEventArgSerialization_Tests.cs new file mode 100644 index 00000000000..57f4bfe1e07 --- /dev/null +++ b/src/Framework/UnitTests/CustomEventArgSerialization_Tests.cs @@ -0,0 +1,981 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class CustomEventArgSerialization_Tests + { + // Generic build class to test custom serialization of abstract class BuildEventArgs + internal class GenericBuildEventArg : BuildEventArgs + { + internal GenericBuildEventArg + ( + string message, + string helpKeyword, + string senderName + ) + : base(message, helpKeyword, senderName) + { + //Do Nothing + } + } + + // Stream, writer and reader where the events will be serialized and deserialized from + private MemoryStream _stream; + private BinaryWriter _writer; + private BinaryReader _reader; + + private int _eventArgVersion = (Environment.Version.Major * 10) + Environment.Version.Minor; + + [TestInitialize] + public void SetUp() + { + _stream = new MemoryStream(); + _writer = new BinaryWriter(_stream); + _reader = new BinaryReader(_stream); + } + + [TestCleanup] + public void TearDown() + { + // Close will close the writer/reader and the underlying stream + _writer.Close(); + _reader.Close(); + _reader = null; + _stream = null; + _writer = null; + } + + [TestMethod] + public void TestGenericBuildEventArgs() + { + // Test using reasonable messages + GenericBuildEventArg genericEvent = new GenericBuildEventArg("Message", "HelpKeyword", "SenderName"); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + // Get position of stream after write so it can be compared to the position after read + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + GenericBuildEventArg newGenericEvent = new GenericBuildEventArg(null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + + // Test using empty strings + _stream.Position = 0; + genericEvent = new GenericBuildEventArg(string.Empty, string.Empty, string.Empty); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new GenericBuildEventArg(null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + + // Test using null strings + _stream.Position = 0; + genericEvent = new GenericBuildEventArg(null, null, null); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new GenericBuildEventArg(null, null, null); + newGenericEvent.BuildEventContext = new BuildEventContext(1, 3, 4, 5); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + } + + /// + /// Compares two BuildEventArgs + /// + private static void VerifyGenericEventArg(BuildEventArgs genericEvent, BuildEventArgs newGenericEvent) + { + Assert.AreEqual(genericEvent.BuildEventContext, newGenericEvent.BuildEventContext, "Expected Event Context to Match"); + Assert.IsTrue(string.Compare(genericEvent.HelpKeyword, newGenericEvent.HelpKeyword, StringComparison.OrdinalIgnoreCase) == 0, "Expected Help Keywords to Match"); + Assert.IsTrue(string.Compare(genericEvent.Message, newGenericEvent.Message, StringComparison.OrdinalIgnoreCase) == 0, "Expected Message to Match"); + Assert.IsTrue(string.Compare(genericEvent.SenderName, newGenericEvent.SenderName, StringComparison.OrdinalIgnoreCase) == 0, "Expected Sender Name to Match"); + Assert.AreEqual(genericEvent.ThreadId, newGenericEvent.ThreadId, "Expected ThreadId to Match"); + Assert.AreEqual(genericEvent.Timestamp, newGenericEvent.Timestamp, "Expected TimeStamp to Match"); + } + + [TestMethod] + public void TestBuildErrorEventArgs() + { + // Test using reasonable messages + BuildErrorEventArgs genericEvent = new BuildErrorEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "SenderName"); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + BuildErrorEventArgs newGenericEvent = new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyBuildErrorEventArgs(genericEvent, newGenericEvent); + + // Test using empty strings + _stream.Position = 0; + genericEvent = new BuildErrorEventArgs(string.Empty, string.Empty, string.Empty, 1, 2, 3, 4, string.Empty, string.Empty, string.Empty); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyBuildErrorEventArgs(genericEvent, newGenericEvent); + + // Test using null strings + _stream.Position = 0; + genericEvent = new BuildErrorEventArgs(null, null, null, 1, 2, 3, 4, null, null, null); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildErrorEventArgs("Something", "SomeThing", "SomeThing", -1, -1, -1, -1, "Something", "SomeThing", "Something"); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyBuildErrorEventArgs(genericEvent, newGenericEvent); + } + + /// + /// Compare two BuildEventArgs + /// + private static void VerifyBuildErrorEventArgs(BuildErrorEventArgs genericEvent, BuildErrorEventArgs newGenericEvent) + { + Assert.IsTrue(string.Compare(genericEvent.Code, newGenericEvent.Code, StringComparison.OrdinalIgnoreCase) == 0, "Expected Code to Match"); + Assert.IsTrue(string.Compare(genericEvent.File, newGenericEvent.File, StringComparison.OrdinalIgnoreCase) == 0, "Expected File to Match"); + Assert.AreEqual(genericEvent.ColumnNumber, newGenericEvent.ColumnNumber, "Expected ColumnNumber to Match"); + Assert.AreEqual(genericEvent.EndColumnNumber, newGenericEvent.EndColumnNumber, "Expected EndColumnNumber to Match"); + Assert.AreEqual(genericEvent.EndLineNumber, newGenericEvent.EndLineNumber, "Expected EndLineNumber to Match"); + } + + + [TestMethod] + public void TestBuildFinishedEventArgs() + { + // Test using reasonable messages + BuildFinishedEventArgs genericEvent = new BuildFinishedEventArgs("Message", "HelpKeyword", true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + + // Deserialize and Verify + _stream.Position = 0; + BuildFinishedEventArgs newGenericEvent = new BuildFinishedEventArgs(null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeeded to Match"); + + // Test using empty strings + _stream.Position = 0; + genericEvent = new BuildFinishedEventArgs(string.Empty, string.Empty, true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildFinishedEventArgs(null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + + // Test using null strings + _stream.Position = 0; + genericEvent = new BuildFinishedEventArgs(null, null, true); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildFinishedEventArgs("Something", "Something", false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + } + + [TestMethod] + public void TestBuildMessageEventArgs() + { + // Test using reasonable messages + BuildMessageEventArgs genericEvent = new BuildMessageEventArgs("Message", "HelpKeyword", "SenderName", MessageImportance.High); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + BuildMessageEventArgs newGenericEvent = new BuildMessageEventArgs(null, null, null, MessageImportance.Low); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.AreEqual(genericEvent.Importance, newGenericEvent.Importance, "Expected Message Importance to Match"); + + // Test empty strings + _stream.Position = 0; + // Make sure empty strings are passed correctly + genericEvent = new BuildMessageEventArgs(string.Empty, string.Empty, string.Empty, MessageImportance.Low); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildMessageEventArgs(null, null, null, MessageImportance.Low); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.AreEqual(genericEvent.Importance, newGenericEvent.Importance, "Expected Message Importance to Match"); + + // Test null strings + _stream.Position = 0; + // Make sure null string are passed correctly + genericEvent = new BuildMessageEventArgs(null, null, null, MessageImportance.Low); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildMessageEventArgs("Something", "Something", "Something", MessageImportance.Low); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.AreEqual(genericEvent.Importance, newGenericEvent.Importance, "Expected Message Importance to Match"); + } + + private void VerifyMessageEventArg(BuildMessageEventArgs messageEvent, BuildMessageEventArgs newMessageEvent) + { + VerifyGenericEventArg(messageEvent, newMessageEvent); + + Assert.AreEqual(messageEvent.Importance, newMessageEvent.Importance, "Expected Message Importance to Match"); + Assert.AreEqual(messageEvent.Subcategory, newMessageEvent.Subcategory, "Expected message Subcategory to match"); + Assert.AreEqual(messageEvent.Code, newMessageEvent.Code, "Expected message Code to match"); + Assert.AreEqual(messageEvent.File, newMessageEvent.File, "Expected message File to match"); + Assert.AreEqual(messageEvent.LineNumber, newMessageEvent.LineNumber, "Expected message LineNumber to match"); + Assert.AreEqual(messageEvent.ColumnNumber, newMessageEvent.ColumnNumber, "Expected message ColumnNumber to match"); + Assert.AreEqual(messageEvent.EndLineNumber, newMessageEvent.EndLineNumber, "Expected message EndLineNumber to match"); + Assert.AreEqual(messageEvent.EndColumnNumber, newMessageEvent.EndColumnNumber, "Expected message EndColumnNumber to match"); + } + + [TestMethod] + public void TestBuildMessageEventArgsWithFileInfo() + { + // Test using reasonable messages + BuildMessageEventArgs messageEvent = new BuildMessageEventArgs("SubCategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "SenderName", MessageImportance.High); + messageEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + messageEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + BuildMessageEventArgs newMessageEvent = new BuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, MessageImportance.Low); + newMessageEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyMessageEventArg(messageEvent, newMessageEvent); + + // Test empty strings + _stream.Position = 0; + // Make sure empty strings are passed correctly + messageEvent = new BuildMessageEventArgs(string.Empty, string.Empty, string.Empty, 1, 2, 3, 4, string.Empty, string.Empty, string.Empty, MessageImportance.Low); + messageEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + messageEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newMessageEvent = new BuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null, MessageImportance.Low); + newMessageEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyMessageEventArg(messageEvent, newMessageEvent); + + // Test null strings + _stream.Position = 0; + // Make sure null string are passed correctly + messageEvent = new BuildMessageEventArgs(null, null, null, 1, 2, 3, 4, null, null, null, MessageImportance.Low); + messageEvent.BuildEventContext = null; + + // Serialize + messageEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newMessageEvent = new BuildMessageEventArgs("Something", "Something", "Something", 0, 0, 0, 0, "Something", "Something", "Something", MessageImportance.Low); + newMessageEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyMessageEventArg(messageEvent, newMessageEvent); + } + + [TestMethod] + public void TestCriticalBuildMessageEventArgs() + { + // Test using reasonable messages + CriticalBuildMessageEventArgs criticalMessageEvent = new CriticalBuildMessageEventArgs("SubCategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "SenderName"); + criticalMessageEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + criticalMessageEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + CriticalBuildMessageEventArgs newCriticalMessageEvent = new CriticalBuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null); + newCriticalMessageEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyMessageEventArg(criticalMessageEvent, newCriticalMessageEvent); + + // Test empty strings + _stream.Position = 0; + // Make sure empty strings are passed correctly + criticalMessageEvent = new CriticalBuildMessageEventArgs(string.Empty, string.Empty, string.Empty, 1, 2, 3, 4, string.Empty, string.Empty, string.Empty); + criticalMessageEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + criticalMessageEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newCriticalMessageEvent = new CriticalBuildMessageEventArgs(null, null, null, 0, 0, 0, 0, null, null, null); + newCriticalMessageEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyMessageEventArg(criticalMessageEvent, newCriticalMessageEvent); + + // Test null strings + _stream.Position = 0; + // Make sure null string are passed correctly + criticalMessageEvent = new CriticalBuildMessageEventArgs(null, null, null, 1, 2, 3, 4, null, null, null); + criticalMessageEvent.BuildEventContext = null; + + // Serialize + criticalMessageEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newCriticalMessageEvent = new CriticalBuildMessageEventArgs("Something", "Something", "Something", 0, 0, 0, 0, "Something", "Something", "Something"); + newCriticalMessageEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyMessageEventArg(criticalMessageEvent, newCriticalMessageEvent); + } + + [TestMethod] + public void TestBuildWarningEventArgs() + { + // Test with reasonable messages + BuildWarningEventArgs genericEvent = new BuildWarningEventArgs("Subcategory", "Code", "File", 1, 2, 3, 4, "Message", "HelpKeyword", "SenderName"); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + //Deserialize and Verify + _stream.Position = 0; + BuildWarningEventArgs newGenericEvent = new BuildWarningEventArgs(null, null, null, -1, -1, -1, -1, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyBuildWarningEventArgs(genericEvent, newGenericEvent); + + // Test with empty strings + _stream.Position = 0; + genericEvent = new BuildWarningEventArgs(string.Empty, string.Empty, string.Empty, 1, 2, 3, 4, string.Empty, string.Empty, string.Empty); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + //Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildWarningEventArgs(null, null, null, -1, -1, -1, -1, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyBuildWarningEventArgs(genericEvent, newGenericEvent); + + // Test with null strings + _stream.Position = 0; + genericEvent = new BuildWarningEventArgs(null, null, null, 1, 2, 3, 4, null, null, null); + genericEvent.BuildEventContext = null; + + //Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + //Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new BuildWarningEventArgs("Something", "SomeThing", "SomeThing", -1, -1, -1, -1, "Something", "SomeThing", "Something"); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyBuildWarningEventArgs(genericEvent, newGenericEvent); + } + + /// + /// Compares two build warning events + /// + private static void VerifyBuildWarningEventArgs(BuildWarningEventArgs genericEvent, BuildWarningEventArgs newGenericEvent) + { + Assert.IsTrue(string.Compare(genericEvent.Subcategory, newGenericEvent.Subcategory, StringComparison.OrdinalIgnoreCase) == 0, "Expected SubCategory to Match"); + Assert.IsTrue(string.Compare(genericEvent.Code, newGenericEvent.Code, StringComparison.OrdinalIgnoreCase) == 0, "Expected Code to Match"); + Assert.IsTrue(string.Compare(genericEvent.File, newGenericEvent.File, StringComparison.OrdinalIgnoreCase) == 0, "Expected File to Match"); + Assert.AreEqual(genericEvent.ColumnNumber, newGenericEvent.ColumnNumber, "Expected ColumnNumber to Match"); + Assert.AreEqual(genericEvent.EndColumnNumber, newGenericEvent.EndColumnNumber, "Expected EndColumnNumber to Match"); + Assert.AreEqual(genericEvent.EndLineNumber, newGenericEvent.EndLineNumber, "Expected EndLineNumber to Match"); + } + + [TestMethod] + public void TestProjectFinishedEventArgs() + { + // Test with reasonable values + ProjectFinishedEventArgs genericEvent = new ProjectFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + ProjectFinishedEventArgs newGenericEvent = new ProjectFinishedEventArgs(null, null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + + // Test with empty strings + _stream.Position = 0; + genericEvent = new ProjectFinishedEventArgs(string.Empty, string.Empty, string.Empty, true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new ProjectFinishedEventArgs(null, null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + + // Test with null strings + _stream.Position = 0; + // Make sure null string are passed correctly + genericEvent = new ProjectFinishedEventArgs(null, null, null, true); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new ProjectFinishedEventArgs("Something", "Something", "Something", false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + } + + [TestMethod] + public void TestProjectStartedPropertySerialization() + { + // Create a list of test properties which should make it through serialization + List propertyList = new List(); + propertyList.Add(new DictionaryEntry("TeamBuildOutDir", "c:\\outdir")); + propertyList.Add(new DictionaryEntry("Configuration", "BuildConfiguration")); + propertyList.Add(new DictionaryEntry("Platform", "System Platform")); + propertyList.Add(new DictionaryEntry("OutDir", "myOutDir")); + propertyList.Add(new DictionaryEntry("WorkSpaceName", " MyWorkspace")); + propertyList.Add(new DictionaryEntry("WorkSpaceOwner", "The workspace owner")); + propertyList.Add(new DictionaryEntry("IAmBlank", string.Empty)); + + ProjectStartedEventArgs genericEvent = new ProjectStartedEventArgs(8, "Message", "HelpKeyword", "ProjectFile", null, propertyList, null, new BuildEventContext(7, 8, 9, 10)); + genericEvent.BuildEventContext = new BuildEventContext(7, 8, 9, 10); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + ProjectStartedEventArgs newGenericEvent = new ProjectStartedEventArgs(-1, null, null, null, null, null, null, null); + + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + Assert.IsNotNull(newGenericEvent.Properties, "Expected Properties to not be null"); + + // Create a list of all of the dictionaryEntries which were deserialized + List entryList = new List(); + foreach (DictionaryEntry entry in newGenericEvent.Properties) + { + entryList.Add(entry); + } + + // Verify that each of the items in propertyList is inside of the deserialized entryList. + AssertDictionaryEntry(entryList, propertyList); + } + + /// + /// Compare the BuildProperties in propertyList with the Name Value pairs in the entryList. + /// We need to make sure that each of the BuildProperties passed into the serializer come out correctly + /// + /// List of DictionaryEntries which were deserialized + /// List of BuildProperties which were serialized + private void AssertDictionaryEntry(List entryList, List propertyList) + { + // make sure that there are the same number of elements in both lists as a quick initial check + Assert.AreEqual(propertyList.Count, entryList.Count); + + // Go through each of the properties which were serialized and make sure we find the exact same + // name and value in the deserialized version. + foreach (DictionaryEntry property in propertyList) + { + bool found = false; + foreach (DictionaryEntry entry in entryList) + { + string key = (string)entry.Key; + string value = (string)entry.Value; + if (key.Equals((string)property.Key, StringComparison.OrdinalIgnoreCase)) + { + if (value.Equals((string)property.Value, StringComparison.OrdinalIgnoreCase)) + { + found = true; + } + } + } + + Assert.IsTrue(found, "Expected to find Key:" + property.Key + " Value:" + property.Value); + } + } + + [TestMethod] + public void TestProjectStartedEventArgs() + { + // Test with reasonable values + ProjectStartedEventArgs genericEvent = new ProjectStartedEventArgs(8, "Message", "HelpKeyword", "ProjectFile", null, null, null, new BuildEventContext(7, 8, 9, 10)); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + ProjectStartedEventArgs newGenericEvent = new ProjectStartedEventArgs(-1, null, null, null, null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyProjectStartedEvent(genericEvent, newGenericEvent); + + // Test with empty strings + _stream.Position = 0; + genericEvent = new ProjectStartedEventArgs(-1, string.Empty, string.Empty, string.Empty, string.Empty, null, null, null); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new ProjectStartedEventArgs(-1, null, null, null, null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream end positions should be equal"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyProjectStartedEvent(genericEvent, newGenericEvent); + + // Test with null strings + _stream.Position = 0; + genericEvent = new ProjectStartedEventArgs(-1, null, null, null, null, null, null, null); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new ProjectStartedEventArgs(4, "Something", "Something", "Something", null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyProjectStartedEvent(genericEvent, newGenericEvent); + } + + /// + /// Compare two project started events + /// + private static void VerifyProjectStartedEvent(ProjectStartedEventArgs genericEvent, ProjectStartedEventArgs newGenericEvent) + { + Assert.AreEqual(genericEvent.Items, newGenericEvent.Items, "Expected Properties to match"); + Assert.AreEqual(genericEvent.Properties, newGenericEvent.Properties, "Expected Properties to match"); + Assert.AreEqual(genericEvent.ParentProjectBuildEventContext, newGenericEvent.ParentProjectBuildEventContext, "Expected ParentEvent Contextes to match"); + Assert.AreEqual(genericEvent.ProjectId, newGenericEvent.ProjectId, "Expected ProjectId to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TargetNames, newGenericEvent.TargetNames, StringComparison.OrdinalIgnoreCase) == 0, "Expected TargetNames to Match"); + } + + [TestMethod] + public void TestTargetStartedEventArgs() + { + // Test using reasonable values + TargetStartedEventArgs genericEvent = new TargetStartedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile", "ParentTargetStartedEvent", DateTime.UtcNow); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + TargetStartedEventArgs newGenericEvent = new TargetStartedEventArgs(null, null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTargetStarted(genericEvent, newGenericEvent); + + //Test using Empty strings + _stream.Position = 0; + // Make sure empty strings are passed correctly + genericEvent = new TargetStartedEventArgs(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, DateTime.Now); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TargetStartedEventArgs(null, null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTargetStarted(genericEvent, newGenericEvent); + + // Test using null strings + _stream.Position = 0; + // Make sure null string are passed correctly + genericEvent = new TargetStartedEventArgs(null, null, null, null, null, null, DateTime.Now); + genericEvent.BuildEventContext = null; + //Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + //Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TargetStartedEventArgs("Something", "Something", "Something", "Something", "Something", "Something", DateTime.Now); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTargetStarted(genericEvent, newGenericEvent); + } + + /// + /// Compare two targetStarted events + /// + private static void VerifyTargetStarted(TargetStartedEventArgs genericEvent, TargetStartedEventArgs newGenericEvent) + { + Assert.IsTrue(string.Compare(genericEvent.TargetFile, newGenericEvent.TargetFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected TargetFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TargetName, newGenericEvent.TargetName, StringComparison.OrdinalIgnoreCase) == 0, "Expected TargetName to Match"); + Assert.IsTrue(string.Compare(genericEvent.ParentTarget, newGenericEvent.ParentTarget, StringComparison.OrdinalIgnoreCase) == 0, "Expected ParentTarget to Match"); + } + + [TestMethod] + public void TestTargetFinishedEventArgs() + { + // Test using reasonable values + TargetFinishedEventArgs genericEvent = new TargetFinishedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile", true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + TargetFinishedEventArgs newGenericEvent = new TargetFinishedEventArgs(null, null, null, null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTargetFinished(genericEvent, newGenericEvent); + + // Test using empty strings + _stream.Position = 0; + genericEvent = new TargetFinishedEventArgs(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TargetFinishedEventArgs(null, null, null, null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTargetFinished(genericEvent, newGenericEvent); + + // Test using null strings + _stream.Position = 0; + // Make sure null string are passed correctly + genericEvent = new TargetFinishedEventArgs(null, null, null, null, null, true); + genericEvent.BuildEventContext = null; + //Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + //Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TargetFinishedEventArgs("Something", "Something", "Something", "Something", "Something", false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTargetFinished(genericEvent, newGenericEvent); + } + + /// + /// Compare two TargetFinished events + /// + private static void VerifyTargetFinished(TargetFinishedEventArgs genericEvent, TargetFinishedEventArgs newGenericEvent) + { + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TargetFile, newGenericEvent.TargetFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected TargetFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TargetName, newGenericEvent.TargetName, StringComparison.OrdinalIgnoreCase) == 0, "Expected TargetName to Match"); + } + + [TestMethod] + public void TestTaskStartedEventArgs() + { + // Test using reasonable values + TaskStartedEventArgs genericEvent = new TaskStartedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName"); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + TaskStartedEventArgs newGenericEvent = new TaskStartedEventArgs(null, null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTaskStarted(genericEvent, newGenericEvent); + + _stream.Position = 0; + // Make sure empty strings are passed correctly + genericEvent = new TaskStartedEventArgs(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TaskStartedEventArgs(null, null, null, null, null); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTaskStarted(genericEvent, newGenericEvent); + + // Test using null strings + _stream.Position = 0; + // Make sure null string are passed correctly + genericEvent = new TaskStartedEventArgs(null, null, null, null, null); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TaskStartedEventArgs("Something", "Something", "Something", "Something", "Something"); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTaskStarted(genericEvent, newGenericEvent); + } + + /// + /// Compare two TaskStarted events + /// + private static void VerifyTaskStarted(TaskStartedEventArgs genericEvent, TaskStartedEventArgs newGenericEvent) + { + Assert.IsTrue(string.Compare(genericEvent.TaskFile, newGenericEvent.TaskFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected TaskFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TaskName, newGenericEvent.TaskName, StringComparison.OrdinalIgnoreCase) == 0, "Expected TaskName to Match"); + } + + [TestMethod] + public void TestTaskFinishedEventArgs() + { + // Test using reasonable values + TaskFinishedEventArgs genericEvent = new TaskFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName", true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + long streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + TaskFinishedEventArgs newGenericEvent = new TaskFinishedEventArgs(null, null, null, null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + long streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTaskFinished(genericEvent, newGenericEvent); + + //Test using empty strings + _stream.Position = 0; + // Make sure empty strings are passed correctly + genericEvent = new TaskFinishedEventArgs(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, true); + genericEvent.BuildEventContext = new BuildEventContext(5, 4, 3, 2); + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TaskFinishedEventArgs(null, null, null, null, null, false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTaskFinished(genericEvent, newGenericEvent); + + //Test using null strings + _stream.Position = 0; + // Make sure null string are passed correctly + genericEvent = new TaskFinishedEventArgs(null, null, null, null, null, true); + genericEvent.BuildEventContext = null; + + // Serialize + genericEvent.WriteToStream(_writer); + streamWriteEndPosition = _stream.Position; + + // Deserialize and Verify + _stream.Position = 0; + newGenericEvent = new TaskFinishedEventArgs("Something", "Something", "Something", "Something", "Something", false); + newGenericEvent.CreateFromStream(_reader, _eventArgVersion); + streamReadEndPosition = _stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream End Positions Should Match"); + VerifyGenericEventArg(genericEvent, newGenericEvent); + VerifyTaskFinished(genericEvent, newGenericEvent); + } + + /// + /// Compare two task finished events + /// + private static void VerifyTaskFinished(TaskFinishedEventArgs genericEvent, TaskFinishedEventArgs newGenericEvent) + { + Assert.IsTrue(genericEvent.Succeeded == newGenericEvent.Succeeded, "Expected Succeded to Match"); + Assert.IsTrue(string.Compare(genericEvent.ProjectFile, newGenericEvent.ProjectFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected ProjectFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TaskFile, newGenericEvent.TaskFile, StringComparison.OrdinalIgnoreCase) == 0, "Expected TaskFile to Match"); + Assert.IsTrue(string.Compare(genericEvent.TaskName, newGenericEvent.TaskName, StringComparison.OrdinalIgnoreCase) == 0, "Expected TaskName to Match"); + } + } +} diff --git a/src/Framework/UnitTests/EventArgs_Tests.cs b/src/Framework/UnitTests/EventArgs_Tests.cs new file mode 100644 index 00000000000..d73c24061b2 --- /dev/null +++ b/src/Framework/UnitTests/EventArgs_Tests.cs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for EventArgsTests +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Unit test the base class BuildEventArgs + /// + [TestClass] + public class EventArgs_Tests + { + #region BaseClass Equals Tests + + /// + /// Base instance of a BuildEventArgs some default data, this is used during the tests + /// to verify the equals operators. + /// + private static GenericBuildEventArgs s_baseGenericEvent = null; + + /// + /// Setup the text, this method is run ONCE for the entire text fixture + /// + [ClassInitialize] + public static void Setup(TestContext context) + { + s_baseGenericEvent = new GenericBuildEventArgs("Message", "HelpKeyword", "senderName"); + s_baseGenericEvent.BuildEventContext = new BuildEventContext(9, 8, 7, 6); + } + + /// + /// Trivially exercise getHashCode. + /// + [TestMethod] + public void TestGetHashCode() + { + s_baseGenericEvent.GetHashCode(); + } + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + GenericBuildEventArgs genericEventTest = new GenericBuildEventArgs(); + } + #endregion + + /// + /// Verify a whidby project started event can be deserialized, the whidby event is stored in a serialized base64 string. + /// + [TestMethod] + [Ignore] + // Ignore: Type in serialized string targets MSBuild retail public key, will not de-serialize + public void TestDeserialization() + { + string base64OldProjectStarted = "AAEAAAD/////AQAAAAAAAAAMAgAAAFxNaWNyb3NvZnQuQnVpbGQuRnJhbWV3b3JrLCBWZXJzaW9uPTIuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49YjAzZjVmN2YxMWQ1MGEzYQUBAAAAMU1pY3Jvc29mdC5CdWlsZC5GcmFtZXdvcmsuUHJvamVjdFN0YXJ0ZWRFdmVudEFyZ3MHAAAAC3Byb2plY3RGaWxlC3RhcmdldE5hbWVzFkJ1aWxkRXZlbnRBcmdzK21lc3NhZ2UaQnVpbGRFdmVudEFyZ3MraGVscEtleXdvcmQZQnVpbGRFdmVudEFyZ3Mrc2VuZGVyTmFtZRhCdWlsZEV2ZW50QXJncyt0aW1lc3RhbXAXQnVpbGRFdmVudEFyZ3MrdGhyZWFkSWQBAQEBAQAADQgCAAAABgMAAAALcHJvamVjdEZpbGUGBAAAAAt0YXJnZXROYW1lcwYFAAAAB21lc3NhZ2UGBgAAAAtoZWxwS2V5d29yZAYHAAAAB01TQnVpbGQBl5vjTYvIiAsAAAAL"; + BinaryFormatter bf = new BinaryFormatter(); + MemoryStream ms = new MemoryStream(); + byte[] binaryObject = Convert.FromBase64String(base64OldProjectStarted); + ms.Write(binaryObject, 0, binaryObject.Length); + ms.Position = 0; + ProjectStartedEventArgs pse = (ProjectStartedEventArgs)bf.Deserialize(ms); + Assert.IsTrue(string.Compare(pse.Message, "message", StringComparison.OrdinalIgnoreCase) == 0); + Assert.IsTrue(string.Compare(pse.ProjectFile, "projectFile", StringComparison.OrdinalIgnoreCase) == 0); + Assert.AreEqual(pse.ProjectId, -1); + Assert.IsTrue(string.Compare(pse.TargetNames, "targetNames", StringComparison.OrdinalIgnoreCase) == 0); + Assert.AreEqual(pse.BuildEventContext, BuildEventContext.Invalid); + Assert.AreEqual(pse.ParentProjectBuildEventContext, BuildEventContext.Invalid); + } + + /// + /// Verify the BuildEventContext is exercised + /// + [TestMethod] + public void ExerciseBuildEventContext() + { + BuildEventContext parentBuildEventContext = new BuildEventContext(0, 0, 0, 0); + BuildEventContext currentBuildEventContext = new BuildEventContext(0, 2, 1, 1); + + BuildEventContext currentBuildEventContextNode = new BuildEventContext(1, 0, 0, 0); + BuildEventContext currentBuildEventContextTarget = new BuildEventContext(0, 1, 0, 0); + BuildEventContext currentBuildEventContextPci = new BuildEventContext(0, 0, 1, 0); + BuildEventContext currentBuildEventContextTask = new BuildEventContext(0, 0, 0, 1); + BuildEventContext allDifferent = new BuildEventContext(1, 1, 1, 1); + BuildEventContext allSame = new BuildEventContext(0, 0, 0, 0); + + ProjectStartedEventArgs startedEvent = new ProjectStartedEventArgs(-1, "Message", "HELP", "File", "Targets", null, null, parentBuildEventContext); + startedEvent.BuildEventContext = currentBuildEventContext; + Assert.IsTrue(parentBuildEventContext.GetHashCode() == 0); + + // Node is different + Assert.IsFalse(parentBuildEventContext.Equals(currentBuildEventContextNode)); + + // Target is different + Assert.IsFalse(parentBuildEventContext.Equals(currentBuildEventContextTarget)); + + // PCI is different + Assert.IsFalse(parentBuildEventContext.Equals(currentBuildEventContextPci)); + + // Task is different + Assert.IsFalse(parentBuildEventContext.Equals(currentBuildEventContextTask)); + + // All fields are different + Assert.IsFalse(parentBuildEventContext.Equals(allDifferent)); + + // All fields are same + Assert.IsTrue(parentBuildEventContext.Equals(allSame)); + + // Compare with null + Assert.IsFalse(parentBuildEventContext.Equals(null)); + + // Compare with self + Assert.IsTrue(currentBuildEventContext.Equals(currentBuildEventContext)); + Assert.IsFalse(currentBuildEventContext.Equals(new object())); + Assert.IsNotNull(startedEvent.BuildEventContext); + + Assert.AreEqual(0, startedEvent.ParentProjectBuildEventContext.NodeId); + Assert.AreEqual(0, startedEvent.ParentProjectBuildEventContext.TargetId); + Assert.AreEqual(0, startedEvent.ParentProjectBuildEventContext.ProjectContextId); + Assert.AreEqual(0, startedEvent.ParentProjectBuildEventContext.TaskId); + Assert.AreEqual(0, startedEvent.BuildEventContext.NodeId); + Assert.AreEqual(2, startedEvent.BuildEventContext.TargetId); + Assert.AreEqual(1, startedEvent.BuildEventContext.ProjectContextId); + Assert.AreEqual(1, startedEvent.BuildEventContext.TaskId); + } + + /// + /// A generic buildEvent arg to test the equals method + /// + internal class GenericBuildEventArgs : BuildEventArgs + { + /// + /// Default constructor + /// + public GenericBuildEventArgs() + : base() + { + } + + /// + /// This constructor allows all event data to be initialized + /// + /// text message + /// help keyword + /// name of event sender + public GenericBuildEventArgs(string message, string helpKeyword, string senderName) + : base(message, helpKeyword, senderName) + { + } + + /// + /// This constructor allows all data including timeStamps to be initialized + /// + /// text message + /// help keyword + /// name of event sender + /// TimeStamp of when the event was created + public GenericBuildEventArgs(string message, string helpKeyword, string senderName, DateTime eventTimeStamp) + : base(message, helpKeyword, senderName, eventTimeStamp) + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/ExternalProjectFinishedEventArgs_Tests.cs b/src/Framework/UnitTests/ExternalProjectFinishedEventArgs_Tests.cs new file mode 100644 index 00000000000..4ccc37d7ed0 --- /dev/null +++ b/src/Framework/UnitTests/ExternalProjectFinishedEventArgs_Tests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for ExternalProjectFinishedEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the ExternalProjectFinishedEventArgs class. + /// + [TestClass] + public class ExternalProjectFinishedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private ExternalProjectFinishedEventArgs _baseExternalProjectFinishedEvent = new ExternalProjectFinishedEventArgs("Message", "HelpKeyword", "Sender", "ProjectFile", true); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + ExternalProjectFinishedEventArgs externalProjectFinishedEvent = new ExternalProjectFinishedEventArgs2(); + externalProjectFinishedEvent = new ExternalProjectFinishedEventArgs("Message", "HelpKeyword", "Sender", "ProjectFile", true); + externalProjectFinishedEvent = new ExternalProjectFinishedEventArgs("Message", "HelpKeyword", "Sender", "ProjectFile", true, DateTime.Now); + externalProjectFinishedEvent = new ExternalProjectFinishedEventArgs(null, null, null, null, true); + externalProjectFinishedEvent = new ExternalProjectFinishedEventArgs(null, null, null, null, true, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class ExternalProjectFinishedEventArgs2 : ExternalProjectFinishedEventArgs + { + /// + /// Default constructor + /// + public ExternalProjectFinishedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/ExternalProjectStartedEventArgs_Tests.cs b/src/Framework/UnitTests/ExternalProjectStartedEventArgs_Tests.cs new file mode 100644 index 00000000000..1a425773f77 --- /dev/null +++ b/src/Framework/UnitTests/ExternalProjectStartedEventArgs_Tests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for ExternalProjectStartedEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the ExternalProjectStartedEventArgs class. + /// + [TestClass] + public class ExternalProjectStartedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private ExternalProjectStartedEventArgs _baseExternalProjectStartedEvent = new ExternalProjectStartedEventArgs("Message", "HelpKeyword", "Sender", "ProjectFile", "TargetNames"); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + ExternalProjectStartedEventArgs externalProjectStartedEvent = new ExternalProjectStartedEventArgs2(); + externalProjectStartedEvent = new ExternalProjectStartedEventArgs("Message", "HelpKeyword", "Sender", "ProjectFile", "TargetNames"); + externalProjectStartedEvent = new ExternalProjectStartedEventArgs("Message", "HelpKeyword", "Sender", "ProjectFile", "TargetNames", DateTime.Now); + externalProjectStartedEvent = new ExternalProjectStartedEventArgs(null, null, null, null, null); + externalProjectStartedEvent = new ExternalProjectStartedEventArgs(null, null, null, null, null, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class ExternalProjectStartedEventArgs2 : ExternalProjectStartedEventArgs + { + /// + /// Default constructor + /// + public ExternalProjectStartedEventArgs2() : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/LoggerException_Tests.cs b/src/Framework/UnitTests/LoggerException_Tests.cs new file mode 100644 index 00000000000..bbc1fcd7c8a --- /dev/null +++ b/src/Framework/UnitTests/LoggerException_Tests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class LoggerExceptionTests + { + /// + /// Verify I implemented ISerializable correctly + /// + [TestMethod] + public void SerializeDeserialize() + { + LoggerException e = new LoggerException("message", + new Exception("innerException"), + "errorCode", + "helpKeyword"); + + using (MemoryStream memstr = new MemoryStream()) + { + BinaryFormatter frm = new BinaryFormatter(); + + frm.Serialize(memstr, e); + memstr.Position = 0; + + LoggerException e2 = (LoggerException)frm.Deserialize(memstr); + + Assert.AreEqual(e.ErrorCode, e2.ErrorCode); + Assert.AreEqual(e.HelpKeyword, e2.HelpKeyword); + Assert.AreEqual(e.Message, e2.Message); + Assert.AreEqual(e.InnerException.Message, e2.InnerException.Message); + } + } + + /// + /// Verify I implemented ISerializable correctly, using other ctor + /// + [TestMethod] + public void SerializeDeserialize2() + { + LoggerException e = new LoggerException("message"); + + using (MemoryStream memstr = new MemoryStream()) + { + BinaryFormatter frm = new BinaryFormatter(); + + frm.Serialize(memstr, e); + memstr.Position = 0; + + LoggerException e2 = (LoggerException)frm.Deserialize(memstr); + + Assert.AreEqual(null, e2.ErrorCode); + Assert.AreEqual(null, e2.HelpKeyword); + Assert.AreEqual(e.Message, e2.Message); + Assert.AreEqual(null, e2.InnerException); + } + } + } +} + + + + + diff --git a/src/Framework/UnitTests/Microsoft.Build.Framework.UnitTests.csproj b/src/Framework/UnitTests/Microsoft.Build.Framework.UnitTests.csproj new file mode 100644 index 00000000000..f4b5e433e4a --- /dev/null +++ b/src/Framework/UnitTests/Microsoft.Build.Framework.UnitTests.csproj @@ -0,0 +1,59 @@ + + + + + Debug + AnyCPU + {BE53C18D-7E4E-4C57-A359-1D662C384511} + Library + Microsoft.Build.Framework.UnitTests + Microsoft.Build.Framework.UnitTests + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + {571f09db-a81a-4444-945c-6f7b530054cd} + Microsoft.Build.Framework + + + + + App.config + Designer + + + + + + + \ No newline at end of file diff --git a/src/Framework/UnitTests/ProjectFinishedEventArgs_Tests.cs b/src/Framework/UnitTests/ProjectFinishedEventArgs_Tests.cs new file mode 100644 index 00000000000..b56e6a7d658 --- /dev/null +++ b/src/Framework/UnitTests/ProjectFinishedEventArgs_Tests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for ProjectFinishedEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the ProjectFinishedEventArgs class. + /// + [TestClass] + public class ProjectFinishedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private ProjectFinishedEventArgs _baseProjectFinishedEvent = new ProjectFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", true); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + ProjectFinishedEventArgs projectFinishedEvent = new ProjectFinishedEventArgs2(); + projectFinishedEvent = new ProjectFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", true); + projectFinishedEvent = new ProjectFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", true, DateTime.Now); + projectFinishedEvent = new ProjectFinishedEventArgs(null, null, null, true); + projectFinishedEvent = new ProjectFinishedEventArgs(null, null, null, true, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class ProjectFinishedEventArgs2 : ProjectFinishedEventArgs + { + /// + /// Default constructor + /// + public ProjectFinishedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/ProjectStartedEventArgs_Tests.cs b/src/Framework/UnitTests/ProjectStartedEventArgs_Tests.cs new file mode 100644 index 00000000000..8b51fbe1d0a --- /dev/null +++ b/src/Framework/UnitTests/ProjectStartedEventArgs_Tests.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for ProjectStartedEventArgs +//----------------------------------------------------------------------- + +using System; +using System.Collections; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the ProjectStartedEventArgs class. + /// + [TestClass] + public class ProjectStartedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private static ProjectStartedEventArgs s_baseProjectStartedEvent; + + /// + /// Setup for text fixture, this is run ONCE for the entire test fixture + /// + [ClassInitialize] + public static void FixtureSetup(TestContext context) + { + BuildEventContext parentBuildEventContext = new BuildEventContext(2, 3, 4, 5); + s_baseProjectStartedEvent = new ProjectStartedEventArgs(1, "Message", "HelpKeyword", "ProjecFile", "TargetNames", null, null, parentBuildEventContext); + } + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + ProjectStartedEventArgs projectStartedEvent = new ProjectStartedEventArgs2(); + Assert.IsNotNull(projectStartedEvent); + + projectStartedEvent = new ProjectStartedEventArgs("Message", "HelpKeyword", "ProjecFile", "TargetNames", null, null); + projectStartedEvent = new ProjectStartedEventArgs("Message", "HelpKeyword", "ProjecFile", "TargetNames", null, null, DateTime.Now); + projectStartedEvent = new ProjectStartedEventArgs(1, "Message", "HelpKeyword", "ProjecFile", "TargetNames", null, null, null); + projectStartedEvent = new ProjectStartedEventArgs(1, "Message", "HelpKeyword", "ProjecFile", "TargetNames", null, null, null, DateTime.Now); + projectStartedEvent = new ProjectStartedEventArgs(null, null, null, null, null, null); + projectStartedEvent = new ProjectStartedEventArgs(null, null, null, null, null, null, DateTime.Now); + projectStartedEvent = new ProjectStartedEventArgs(1, null, null, null, null, null, null, null); + projectStartedEvent = new ProjectStartedEventArgs(1, null, null, null, null, null, null, null, DateTime.Now); + } + + /// + /// Verify different Items and properties are not taken into account in the equals comparison. They should + /// not be considered as part of the equals evaluation + /// + [TestMethod] + public void ItemsAndPropertiesDifferentEquals() + { + ArrayList itemsList = new ArrayList(); + ArrayList propertiesList = new ArrayList(); + ProjectStartedEventArgs differentItemsAndProperties = new ProjectStartedEventArgs + ( + s_baseProjectStartedEvent.ProjectId, + s_baseProjectStartedEvent.Message, + s_baseProjectStartedEvent.HelpKeyword, + s_baseProjectStartedEvent.ProjectFile, + s_baseProjectStartedEvent.TargetNames, + propertiesList, + itemsList, + s_baseProjectStartedEvent.ParentProjectBuildEventContext, + s_baseProjectStartedEvent.Timestamp + ); + + Assert.IsFalse(propertiesList == s_baseProjectStartedEvent.Properties); + Assert.IsFalse(itemsList == s_baseProjectStartedEvent.Items); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class ProjectStartedEventArgs2 : ProjectStartedEventArgs + { + /// + /// Default constructor + /// + public ProjectStartedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/TargetFinishedEventArgs_Tests.cs b/src/Framework/UnitTests/TargetFinishedEventArgs_Tests.cs new file mode 100644 index 00000000000..436141b91ee --- /dev/null +++ b/src/Framework/UnitTests/TargetFinishedEventArgs_Tests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for TargetFinishedEventArgs +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the TargetFinishedEventArgs class. + /// + [TestClass] + public class TargetFinishedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private TargetFinishedEventArgs _baseTargetFinishedEvent = new TargetFinishedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile", true); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + List outputs = new List(); + TargetFinishedEventArgs targetFinishedEvent = new TargetFinishedEventArgs2(); + targetFinishedEvent = new TargetFinishedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile", true); + targetFinishedEvent = new TargetFinishedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile", true, DateTime.Now, outputs); + targetFinishedEvent = new TargetFinishedEventArgs(null, null, null, null, null, true); + targetFinishedEvent = new TargetFinishedEventArgs(null, null, null, null, null, true, DateTime.Now, null); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class TargetFinishedEventArgs2 : TargetFinishedEventArgs + { + /// + /// Default constructor + /// + public TargetFinishedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/TargetStartedEventArgs_Tests.cs b/src/Framework/UnitTests/TargetStartedEventArgs_Tests.cs new file mode 100644 index 00000000000..c25d3ecbdbb --- /dev/null +++ b/src/Framework/UnitTests/TargetStartedEventArgs_Tests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// NodePackets which are used for node communication +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the TargetStartedEventArgs class. + /// + [TestClass] + public class TargetStartedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private TargetStartedEventArgs _baseTargetStartedEvent = new TargetStartedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile"); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + TargetStartedEventArgs targetStartedEvent = new TargetStartedEventArgs2(); + targetStartedEvent = new TargetStartedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile"); + targetStartedEvent = new TargetStartedEventArgs("Message", "HelpKeyword", "TargetName", "ProjectFile", "TargetFile", "ParentTarget", DateTime.Now); + targetStartedEvent = new TargetStartedEventArgs(null, null, null, null, null); + targetStartedEvent = new TargetStartedEventArgs(null, null, null, null, null, null, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class TargetStartedEventArgs2 : TargetStartedEventArgs + { + /// + /// Default constructor + /// + public TargetStartedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/TaskCommandLineEventArgs_Tests.cs b/src/Framework/UnitTests/TaskCommandLineEventArgs_Tests.cs new file mode 100644 index 00000000000..a9712397f9f --- /dev/null +++ b/src/Framework/UnitTests/TaskCommandLineEventArgs_Tests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for TaskCommandLineEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the TaskCommandLineEventArgs class. + /// + [TestClass] + public class TaskCommandLineEventArgs_Tests + { + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + TaskCommandLineEventArgs taskCommandLineEvent = new TaskCommandLineEventArgs2(); + taskCommandLineEvent = new TaskCommandLineEventArgs("Commandline", "taskName", MessageImportance.High); + taskCommandLineEvent = new TaskCommandLineEventArgs("Commandline", "taskName", MessageImportance.High, DateTime.Now); + taskCommandLineEvent = new TaskCommandLineEventArgs(null, null, MessageImportance.High); + taskCommandLineEvent = new TaskCommandLineEventArgs(null, null, MessageImportance.High, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class TaskCommandLineEventArgs2 : TaskCommandLineEventArgs + { + /// + /// Default constructor + /// + public TaskCommandLineEventArgs2() : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/TaskFinishedEventArgs_Tests.cs b/src/Framework/UnitTests/TaskFinishedEventArgs_Tests.cs new file mode 100644 index 00000000000..c0b65d64956 --- /dev/null +++ b/src/Framework/UnitTests/TaskFinishedEventArgs_Tests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for TaskFinishedEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the TaskFinishedEventArgs class. + /// + [TestClass] + public class TaskFinishedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private TaskFinishedEventArgs _baseTaskFinishedEvent = new TaskFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName", true); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + TaskFinishedEventArgs targetFinishedEvent = new TaskFinishedEventArgs2(); + targetFinishedEvent = new TaskFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName", true); + targetFinishedEvent = new TaskFinishedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName", true, DateTime.Now); + targetFinishedEvent = new TaskFinishedEventArgs(null, null, null, null, null, true); + targetFinishedEvent = new TaskFinishedEventArgs(null, null, null, null, null, true, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class TaskFinishedEventArgs2 : TaskFinishedEventArgs + { + /// + /// Default constructor + /// + public TaskFinishedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/UnitTests/TaskStartedEventArgs_Tests.cs b/src/Framework/UnitTests/TaskStartedEventArgs_Tests.cs new file mode 100644 index 00000000000..6af7ad56c35 --- /dev/null +++ b/src/Framework/UnitTests/TaskStartedEventArgs_Tests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for TaskStartedEventArgs +//----------------------------------------------------------------------- + +using System; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Verify the functioning of the TaskStartedEventArgs class. + /// + [TestClass] + public class TaskStartedEventArgs_Tests + { + /// + /// Default event to use in tests. + /// + private TaskStartedEventArgs _baseTaskStartedEvent = new TaskStartedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName"); + + /// + /// Trivially exercise event args default ctors to boost Frameworks code coverage + /// + [TestMethod] + public void EventArgsCtors() + { + TaskStartedEventArgs taskStartedEvent = new TaskStartedEventArgs2(); + taskStartedEvent = new TaskStartedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName"); + taskStartedEvent = new TaskStartedEventArgs("Message", "HelpKeyword", "ProjectFile", "TaskFile", "TaskName", DateTime.Now); + taskStartedEvent = new TaskStartedEventArgs(null, null, null, null, null); + taskStartedEvent = new TaskStartedEventArgs(null, null, null, null, null, DateTime.Now); + } + + /// + /// Create a derrived class so that we can test the default constructor in order to increase code coverage and + /// verify this code path does not cause any exceptions. + /// + private class TaskStartedEventArgs2 : TaskStartedEventArgs + { + /// + /// Default constructor + /// + public TaskStartedEventArgs2() + : base() + { + } + } + } +} \ No newline at end of file diff --git a/src/Framework/XamlTypes/Argument.cs b/src/Framework/XamlTypes/Argument.cs new file mode 100644 index 00000000000..4dbb99b11cd --- /dev/null +++ b/src/Framework/XamlTypes/Argument.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents an argument to a property. +//----------------------------------------------------------------------- + +using System; +using System.ComponentModel; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents an argument to a . + /// + /// + /// Functionally, it is simply a reference to another . Those who manually + /// instantiate this class should remember to call before setting the first + /// property and after setting the last property of the object. + /// + public sealed class Argument : ISupportInitialize + { + #region Constructor + + /// + /// Default constructor needed for XAML deserialization. + /// + public Argument() + { + Separator = String.Empty; + } + + #endregion + + #region Properties + + /// + /// Name of the this argument refers to. + /// + /// + /// Its value must point to a valid . This field is mandatory and culture invariant. + /// + public string Property + { + get; + set; + } + + /// + /// Tells if the pointed to by must be defined for the definition + /// of the owning this argument to make sense. + /// + /// + /// This field is optional and is set to false by default. + /// + public bool IsRequired + { + get; + set; + } + + /// + /// The string used to separate this argument value from the parent switch in the command line. + /// + /// + /// This field is optional and culture invariant. + /// + public string Separator + { + get; + set; + } + + #endregion + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/BaseProperty.cs b/src/Framework/XamlTypes/BaseProperty.cs new file mode 100644 index 00000000000..7e06498d73c --- /dev/null +++ b/src/Framework/XamlTypes/BaseProperty.cs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a Rule property. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; +using System.Xml; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents a property. + /// + /// + /// This represents schema information (name, allowed values, etc) of a property. + /// Since this is just schema information, there is no field like "Value" used to get/set the value of this + /// property. + /// Those who manually instantiate this class should remember to call before + /// setting the first property and after setting the last property of the object. + /// + /// + /// This partial class contains all properties which are public and hence settable in XAML. Those properties that + /// are internal are defined in another partial class below. + /// + [ContentProperty("Arguments")] + public abstract partial class BaseProperty : ISupportInitialize + { + #region Fields + + /// + /// See DisplayName property. + /// + private string _displayName; + + #endregion + + #region Constructor + + /// + /// Default constructor. Needed for deserializtion from a persisted format. + /// + protected BaseProperty() + { + // Initialize collection properties in this class. This is required for + // proper deserialization. + Metadata = new List(); + Arguments = new List(); + ValueEditors = new List(); + + // The default value of Visible. + Visible = true; + + // The default value of IncludeInCommandLine. + IncludeInCommandLine = true; + + SwitchPrefix = String.Empty; + Separator = String.Empty; + Category = "General"; + Subcategory = String.Empty; + + HelpContext = -1; + HelpFile = String.Empty; + HelpUrl = String.Empty; + } + + #endregion + + #region Properties + + /// + /// The name of this . + /// + /// + /// This field is mandatory and culture invariant. The value of this field cannot be set to the empty string. + /// + public string Name + { + get; + set; + } + + /// + /// The name that could be used by a prospective UI client to display this . + /// + /// + /// This field is optional and is culture sensitive. When this property is not set, it is assigned the same + /// value as the property (and hence, would not be localized). + /// + [Localizable(true)] + public string DisplayName + { + get + { + return _displayName ?? Name; + } + + set + { + _displayName = value; + } + } + + /// + /// Description of this for use by a prospective UI client. + /// + /// + /// This field is optional and is culture sensitive. + /// + [Localizable(true)] + public string Description + { + get; + set; + } + + /// + /// The keyword that is used to open the help page for this property. + /// + /// + /// This form of specifying help takes precedence over + /// and + . + /// This field is optional and is culture insensitive. + /// + [Localizable(false)] + public string F1Keyword + { + get; + set; + } + + /// + /// The URL of the help page for this property that will be opened when the user hits F1. + /// + /// + /// This property is higher in priority that + + /// (i.e., these two properties are ignored if + /// is specified), but lower in priority than . + /// This field is optional and is culture insensitive. + /// + /// ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.VisualStudio.v80.en/dv_vstoc/html/06ddebea-2c83-4a45-bb48-6264c797ed93.htm + [Localizable(false)] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public string HelpUrl + { + get; + set; + } + + /// + /// The help file to use when the user hits F1. Must specify along with this. + /// + /// + /// This property goes along with . . This + /// form of specifying the help page for a property takes lower precedence than both + /// and . + /// This field is optional and is culture insensitive. + /// + [Localizable(false)] + public string HelpFile + { + get; + set; + } + + /// + /// The help context to use when the user hits F1. Must specify along with this. + /// + /// + /// This property uses the property to display the help context of the specified + /// help file. This field is optional. This + /// form of specifying the help page for a property takes lower precedence than both + /// and . + /// + public int HelpContext + { + get; + set; + } + + /// + /// The name of the category to which this property belongs to. + /// + /// + /// + /// If the value of this field does not correspond to the Name + /// property of a element defined in + /// the containing , a default with this name + /// is auto-generated and added to the containing class. + /// + /// + /// This field is optional and is culture invariant. + /// + /// + /// When this field is not specified, this property is added to a + /// auto-generated category called General (localized). This field cannot be set to the + /// empty string. + /// + /// + public string Category + { + get; + set; + } + + /// + /// The sub category to which this property belongs to. + /// + public string Subcategory + { + get; + set; + } + + /// + /// Tells if this property is a read-only property. + /// + /// + /// This field is optional and its default value is "false". + /// + public bool ReadOnly + { + get; + set; + } + + /// + /// A value indicating whether this property allows multiple values to be supplied/selected simultaneously. + /// + public bool MultipleValuesAllowed + { + get; + set; + } + + /// + /// The switch representation of this property for the case when this property represents a tool parameter. + /// + /// + /// This field is optional and culture invariant. + /// + /// + /// For the VC++ CL task, WholeProgramOptimization is a boolean parameter. It's switch is GL. + /// + public string Switch + { + get; + set; + } + + /// + /// The prefix for the switch representation of this property for the case when this property represents a tool parameter. + /// + /// + /// The value specified here overrides the value specified for the parent 's . + /// This field is optional and culture invariant. + /// + /// + /// For the VC++ CL task, WholeProgramOptimization is a boolean parameter. It's switch is GL and its + /// switch prefix (inherited from the parent since it is not overriden by WholeProgramOptimization) + /// is /. Thus the complete switch in the command line for this property would be /GL + /// + public string SwitchPrefix + { + get; + set; + } + + /// + /// The token used to separate a switch from its value. + /// + /// + /// The value specified here overrides the value specified for the parent 's . + /// This field is optional and culture invariant. + /// + /// + /// Example: Consider /D:WIN32. In this switch and value representation, ":" is the separator since its separates the switch D + /// from its value WIN32. + /// + public string Separator + { + get; + set; + } + + /// + /// A hint to the UI client telling it whether to display this property or not. + /// + /// + /// This field is optional and has the default value of "true". + /// + public bool Visible + { + get; + set; + } + + /// + /// A hint to the command line constructor whether to include this property in the command line or not. + /// + /// + /// Some properties are used only by the targets and don't want to be included in the command line. + /// Others (like task parameters) are included in the command line in the form of the switch/value they emit. + /// This field is optional and has the default value of true. + /// + public bool IncludeInCommandLine + { + get; + set; + } + + /// + /// Indicates whether this property is required to have a value set. + /// + public bool IsRequired + { + get; + set; + } + + /// + /// Specifies the default value for this property. + /// + /// + /// This field is optional and whether, for a , + /// it is culture sensitive or not depends on the semantics of it. + /// + [Localizable(true)] + public string Default + { + get; + set; + } + + /// + /// The data source where the current value of this property is stored. + /// + /// + /// If defined, it overrides the + /// property on the containing . This field is mandatory only if the parent + /// does not have the data source initialized. The getter for this property returns + /// only the set directly on this instance. + /// + public DataSource DataSource + { + get; + set; + } + + /// + /// Additional attributes of this . + /// + /// + /// This can be used as a grab bag of additional metadata of this property that are not + /// captured by the primary fields. You will need a custom UI to interpret the additional + /// metadata since the shipped UI formats can't obviously know about it. + /// This field is optional. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Metadata + { + get; + set; + } + + /// + /// List of arguments for this property. + /// + /// + /// This field is optional. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Arguments + { + get; + set; + } + + /// + /// List of value editors for this property. + /// + /// + /// This field is optional. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List ValueEditors + { + get; + set; + } + + #endregion + } + + /// + /// Represents a property. + /// + /// + /// This represents schema information (name, allowed values, etc) of a property. + /// Since this is just schema information, there is no field like "Value" used to get/set the value of this + /// property. + /// Those who manually instantiate this class should remember to call before + /// setting the first property and after setting the last property of the object. + /// + /// + /// This partial class contains members that are auto-generated, internal, etc. Whereas the + /// other partial class contains public properties that can be set in XAML. + /// + public abstract partial class BaseProperty : ISupportInitialize + { + #region Properties + + /// + /// The containing this . + /// + public Rule ContainingRule + { + get; + internal set; + } + + #endregion // Properties + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public virtual void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public virtual void EndInit() + { + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/BoolProperty.cs b/src/Framework/XamlTypes/BoolProperty.cs new file mode 100644 index 00000000000..14e53423940 --- /dev/null +++ b/src/Framework/XamlTypes/BoolProperty.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the schame of a boolean property. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents the schame of a boolean property. + /// + public sealed class BoolProperty : BaseProperty + { + #region Properties + + /// + /// Represents the logical negation of a boolean switch. + /// + /// + /// + /// For the VC++ CL task, WholeProgramOptimization is a boolean parameter. It's switch is GL. To + /// disable whole program optimization, you need to pass the ReverseSwitch, which is GL-. + /// + /// + /// This field is optional. + /// + /// + public string ReverseSwitch + { + get; + set; + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/Category.cs b/src/Framework/XamlTypes/Category.cs new file mode 100644 index 00000000000..d44b828eaf1 --- /dev/null +++ b/src/Framework/XamlTypes/Category.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//-------------------------------------------------------------------------------- +// +// Represents a category to which a property can belong to. +//-------------------------------------------------------------------------------- + +using System; +using System.ComponentModel; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents a category to which a can belong to. + /// + /// + /// Those who manually + /// instantiate this class should remember to call before setting the first + /// property and after setting the last property of the object. + /// + /// + /// This partial class contains all properties which are public and hence settable in XAML. Those properties that + /// are internal are defined in another partial class below. + /// + public sealed partial class Category : CategorySchema, ISupportInitialize + { + #region Fields + + /// + /// See DisplayName property. + /// + private string _displayName; + + #endregion + + #region Properties + + /// + /// The name of this . + /// + /// + /// This field is mandatory and culture invariant. + /// This field cannot be set to the empty string. + /// + public string Name + { + get; + set; + } + + /// + /// The name that could be used by a prospective UI client to display this . + /// + /// + /// This field is optional and is culture sensitive. When this property is not set, it is assigned the same + /// value as the property (and hence, would not be localized). + /// + [Localizable(true)] + public string DisplayName + { + get + { + return _displayName ?? Name; + } + + set + { + _displayName = value; + } + } + + /// + /// Description of this . + /// + /// + /// This field is optional and is culture sensitive. + /// + [Localizable(true)] + public string Description + { + get; + set; + } + + /// + /// Subtype of this . Is either Grid (default) or CommandLine. + /// + /// + /// It helps the UI display this category in an appropriate form. E.g. non command line category + /// properties are normally displayed in the form of a property grid. + /// + public string Subtype + { + get; + set; + } + + /// + /// Help information for this . + /// + /// + /// Maybe used to specify a help URL. This field + /// is optional and is culture sensitive. + /// + [Localizable(true)] + public string HelpString + { + get; + set; + } + + #endregion + } + + /// + /// Represents a category to which a can belong to. + /// + /// + /// Those who manually + /// instantiate this class should remember to call before setting the first + /// property and after setting the last property of the object. + /// + /// + /// This partial class contains members that are auto-generated, internal, etc. Whereas the + /// other partial class contains public properties that can be set in XAML. + /// + public sealed partial class Category : CategorySchema, ISupportInitialize + { + // This partial class contains members that are auto-generated, internal, etc. + #region Constructor + + /// + /// Default constructor. Called during deserialization. + /// + public Category() + { + Subtype = "Grid"; + } + + #endregion // Constructor + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/CategorySchema.cs b/src/Framework/XamlTypes/CategorySchema.cs new file mode 100644 index 00000000000..fc60cfd68f2 --- /dev/null +++ b/src/Framework/XamlTypes/CategorySchema.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Base type for categories in the property page schema data model. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// The CategorySchema provides a strongly typed identity handle to the underlying schema data model. + /// + public abstract class CategorySchema + { + } +} diff --git a/src/Framework/XamlTypes/ContentType.cs b/src/Framework/XamlTypes/ContentType.cs new file mode 100644 index 00000000000..dbf178b591e --- /dev/null +++ b/src/Framework/XamlTypes/ContentType.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// serialization class for Content type data. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Used to deserialize the content type information + /// + [ContentProperty("Metadata")] + public sealed class ContentType : ISupportInitialize, IProjectSchemaNode + { + /// + /// metadata hash + /// + private Lazy> _metadata; + + /// + /// Constructor + /// + public ContentType() + { + this.Metadata = new List(); + + // We must use ExecutionAndPublication thread safety here because the initializer is a destructive operation. + _metadata = new Lazy>(this.InitializeMetadata, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); + } + + /// + /// serializes IContentType.Name + /// + public string Name + { + get; + set; + } + + /// + /// serializes IContentType.DisplayName + /// + [Localizable(true)] + public string DisplayName + { + get; + set; + } + + /// + /// serializes IContentType.ItemType + /// + public string ItemType + { + get; + set; + } + + /// + /// serializes IContentType.DefaultContentTypeForItemType + /// + public bool DefaultContentTypeForItemType + { + get; + set; + } + + /// + /// This property was never used for anything. It should have been removed before we shipped MSBuild 4.0. + /// + [Obsolete("Unused. Use ItemType property instead.", true)] + public string ItemGroupName + { + get; + set; + } + + /// + /// serializes content type's metadata. Accessable via IContentType.GetMetadata() + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Metadata + { + get; + set; + } + + /// + /// Access metadata in convenient way + /// + public string GetMetadata(string metadataName) + { + if (String.IsNullOrEmpty(metadataName)) + { + throw new ArgumentNullException("metadataName"); + } + + string value; + _metadata.Value.TryGetValue(metadataName, out value); + return value; + } + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + } + + #endregion + + #region IProjectSchemaNode Members + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjectTypes() + { + yield return typeof(ContentType); + } + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjects(Type type) + { + if (type == typeof(ContentType)) + { + yield return this; + } + } + + #endregion + + /// + /// Lazily initializes the metadata dictionary. + /// + /// The new dictionary. + /// + /// This is a destructive operation. It clears the NameValuePair list field. + /// + private Dictionary InitializeMetadata() + { + var metadata = new Dictionary(this.Metadata.Count, StringComparer.OrdinalIgnoreCase); + foreach (NameValuePair pair in this.Metadata) + { + metadata.Add(pair.Name, pair.Value); + } + + return metadata; + } + } +} diff --git a/src/Framework/XamlTypes/DataSource.cs b/src/Framework/XamlTypes/DataSource.cs new file mode 100644 index 00000000000..3892cfa1e3b --- /dev/null +++ b/src/Framework/XamlTypes/DataSource.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the data source for a Rule property. +//----------------------------------------------------------------------- + +using System; +using System.ComponentModel; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Indicates where the default value for some property may be found. + /// + public enum DefaultValueSourceLocation + { + /// + /// The default value for a property is set at the top of the project file (usually via an import of a .props file). + /// + BeforeContext, + + /// + /// The default value for a property is set at the bottom of the project file (usually via an import of a .targets file, + /// where the property definition is conditional on whether the property has not already been defined.) + /// + AfterContext, + } + + /// + /// Represents the location and grouping for a . + /// + /// + /// Those who manually + /// instantiate this class should remember to call before setting the first + /// property and after setting the last property of the object. + /// + public sealed class DataSource : ISupportInitialize + { + #region Constructor + + /// + /// Default constructor. Needed for proper XAML deserialization. + /// + public DataSource() + { + // Set the default value for this property. + HasConfigurationCondition = true; + Label = String.Empty; + SourceOfDefaultValue = DefaultValueSourceLocation.BeforeContext; + } + + #endregion + + #region Properties + + /// + /// The storage location for this data source. + /// + /// + /// This field is mandatory and is culture invariant. Current accepted values are ProjectFile + /// and UserFile. ProjectFile causes the property value to be writted to and read from + /// the project manifest file or the property sheet (depending on which node in the solution explorer/property manager + /// window is used to spawn the property pages UI). UserFile causes the property value to be written to and + /// read from the .user file. + /// + public string Persistence + { + get; + set; + } + + /// + /// Gets or sets the actual MSBuild property name used to read/write the value of this property. + /// Applicable only to objects attached to properties. + /// + /// The MSBuild property name to use; or null to use the as the MSBuild property name. + /// + /// The persisted name will usually be the same as the property name as it appears in the + /// and the value of this property can therefore be left at is default of null. + /// Since property names must be unique but need not be unique in the persisted store (due to other differences + /// in the data source such as item type) there may be times when Rule property names must be changed to be + /// unique in the XAML file, but without changing how the property is persisted in the MSBuild file. + /// It is in those cases where this property becomes useful. + /// It may also be useful in specialized build environments where property names must differ from the + /// normally used name in order to maintain compatibility with the project system. + /// + public string PersistedName + { + get; + set; + } + + /// + /// The label of the MSBuild property group/item definition group to which + /// a property/item definition metadata belongs to. Default value is the + /// empty string. + /// + /// A VC++ property that exists in the project manifest + /// in the MSBuild property group with label Globals would have this + /// same value for this field. + public string Label + { + get; + set; + } + + /// + /// If a is an item definition metadata or item metadata, this field + /// specified the item type of the item definition or the item, respectively. For common properties + /// this field must not be set. + /// + public string ItemType + { + get; + set; + } + + /// + /// Indicates if a property is configuration-dependent as indicated by the presence of a configuration + /// condition attached to the property definition at its persistence location. + /// + /// + /// This field is optional and has the default value of true. + /// + public bool HasConfigurationCondition + { + get; + set; + } + + /// + /// The data type of the source. Generally one of Item, ItemDefinition, Property, + /// or TargetResults (when is non-empty). + /// Among other things this governs how the data is treated during build. + /// + /// + /// A value of Item for this property indicates that this property is actually + /// an item array - the list of all items with the item type specified by . + /// + public string SourceType + { + get; + set; + } + + /// + /// Gets or sets the semicolon-delimited list of MSBuild targets that must be executed before reading + /// the read-only properties or items described by this . + /// + public string MSBuildTarget + { + get; + set; + } + + /// + /// Gets or sets a value indicating where the default value for this property can be found. + /// + public DefaultValueSourceLocation SourceOfDefaultValue + { + get; + set; + } + + #endregion // Properties + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/DynamicEnumProperty.cs b/src/Framework/XamlTypes/DynamicEnumProperty.cs new file mode 100644 index 00000000000..ad01fdb1221 --- /dev/null +++ b/src/Framework/XamlTypes/DynamicEnumProperty.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the schema of a dynamic enumeration property. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents the schema of an enumeration property. + /// + /// This class inherits the property from the class. + /// That property does not make sense for this property. Use the property on the + /// instead to mark the default value for this property. + public sealed class DynamicEnumProperty : BaseProperty + { + #region Constructor + + /// + /// constructor + /// + public DynamicEnumProperty() + { + // Initialize collection properties in this class. This is required for + // proper deserialization. + ProviderSettings = new List(); + } + + #endregion + + #region Properties + + /// + /// The provider that produces the list of possible values for this property. Must be set. + /// + public string EnumProvider { get; set; } + + /// + /// A provider-specific set of options to pass to the provider. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Concrete collection types required for XAML deserialization")] + public List ProviderSettings { get; set; } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/EnumProperty.cs b/src/Framework/XamlTypes/EnumProperty.cs new file mode 100644 index 00000000000..e92823d2160 --- /dev/null +++ b/src/Framework/XamlTypes/EnumProperty.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the schema of an enumeration property. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents the schema of an enumeration property. + /// + /// This class inherits the property from the class. + /// That property does not make sense for this property. Use the property on the + /// instead to mark the default value for this property. + [ContentProperty("AdmissibleValues")] + public sealed class EnumProperty : BaseProperty + { + #region Constructor + + /// + /// constructor + /// + public EnumProperty() + { + AdmissibleValues = new List(); + } + + #endregion + + #region Properties + + /// + /// The list of possible values for this property. Must have at least one value. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List AdmissibleValues + { + get; + set; + } + + #endregion + + #region ISupportInitialize Methods + + /// + /// See ISupportInitialize. + /// + public override void EndInit() + { + base.EndInit(); + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/EnumValue.cs b/src/Framework/XamlTypes/EnumValue.cs new file mode 100644 index 00000000000..fa792ffaa3e --- /dev/null +++ b/src/Framework/XamlTypes/EnumValue.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents an admissible value of an EnumProperty. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents an admissible value of an . + /// + [ContentProperty("Arguments")] + public sealed class EnumValue + { + #region Fields + + /// + /// See DisplayName property. + /// + private string _displayName; + + #endregion + + #region Constructor + + /// + /// Default constructor needed for XAML deserialization. + /// + public EnumValue() + { + Arguments = new List(); + Metadata = new List(); + + SwitchPrefix = String.Empty; + } + + #endregion // Constructor + + #region Properties + + /// + /// The name of this . + /// + /// + /// This field is mandatory and culture invariant. + /// + public string Name + { + get; + set; + } + + /// + /// The name that could be used by a prospective UI client to display this . + /// + /// + /// This field is optional and is culture sensitive. When this property is not set, it is assigned the same + /// value as the property (and hence, would not be localized). + /// + [Localizable(true)] + public string DisplayName + { + get + { + return _displayName ?? Name; + } + + set + { + _displayName = value; + } + } + + /// + /// Description of this for use by a prospective UI client. + /// + /// + /// This field is optional and is culture sensitive. + /// + [Localizable(true)] + public string Description + { + get; + set; + } + + /// + /// Help information for this . + /// + /// + /// Maybe used to specify a help URL. This field + /// is optional and is culture sensitive. + /// + [Localizable(true)] + public string HelpString + { + get; + set; + } + + /// + /// The switch representation of this property for the case when the parent represents a tool parameter. + /// + /// + /// This field is optional and culture invariant. + /// + /// The VC compiler has an named Optimizationused to specify the desired optimization type. All the + /// admissible values for this property have switches, e.g. Disabled (switch = Od), "MinimumSize" (switch = O1), + /// etc. + public string Switch + { + get; + set; + } + + /// + /// The prefix for the switch representation of this value for the case when the parent represents a tool parameter. + /// + /// + /// This field is optional and culture invariant. + /// + public string SwitchPrefix + { + get; + set; + } + + /// + /// Tells if this is the default value for the associated + /// . + /// + /// + /// This field is optional and the default value for this + /// field is "false". + /// + public bool IsDefault + { + get; + set; + } + + /// + /// Additional attributes of this . + /// + /// + /// This can be used as a grab bag of additional metadata of this value that are not + /// captured by the primary fields. You will need a custom UI to interpret the additional + /// metadata since the shipped UI formats can't obviously know about it. + /// This field is optional. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Metadata + { + get; + set; + } + + /// + /// List of arguments for this . + /// + /// + /// This field is optional. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Arguments + { + get; + set; + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/FileExtension.cs b/src/Framework/XamlTypes/FileExtension.cs new file mode 100644 index 00000000000..1ceb119ba55 --- /dev/null +++ b/src/Framework/XamlTypes/FileExtension.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// serialization class for FileExtension type data. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// simple class that deserialize extension to content type data + /// + public sealed class FileExtension : IProjectSchemaNode + { + /// + /// Constructor + /// + public FileExtension() + { + } + + /// + /// file extension + /// + public string Name + { + get; + set; + } + + /// + /// coresponding content type + /// + public string ContentType + { + get; + set; + } + + #region IProjectSchemaNode Members + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjectTypes() + { + yield return typeof(FileExtension); + } + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjects(Type type) + { + if (type == typeof(FileExtension)) + { + yield return this; + } + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/IProjectSchemaNode.cs b/src/Framework/XamlTypes/IProjectSchemaNode.cs new file mode 100644 index 00000000000..c646ed8e46b --- /dev/null +++ b/src/Framework/XamlTypes/IProjectSchemaNode.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// IProjectSchemaNode. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Interface that we expect all root classes from project schema XAML files to implement + /// + public interface IProjectSchemaNode + { + /// + /// Return all types of static data for data driven features this node contains + /// + IEnumerable GetSchemaObjectTypes(); + + /// + /// Returns all instances of static data with Type "type". Null or Empty list if there is no objects from asked type provided by this node + /// + IEnumerable GetSchemaObjects(Type type); + } +} diff --git a/src/Framework/XamlTypes/IntProperty.cs b/src/Framework/XamlTypes/IntProperty.cs new file mode 100644 index 00000000000..fc3db93cd39 --- /dev/null +++ b/src/Framework/XamlTypes/IntProperty.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the schema of an integer property. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represent the schema of an integer property. + /// + public sealed class IntProperty : BaseProperty + { + #region Properties + + /// + /// Minimum allowed value for this property. + /// + /// + /// This field is optional. + /// It returns null when this property is not set. The value of this + /// property must be less than or equal to the + /// property (assuming that the latter is defined). + /// + public int? MinValue + { + get; + set; + } + + /// + /// Maximum allowed value for this property. + /// + /// + /// This field is optional. + /// It returns null when this property is not set. The value of this + /// property must be greater than or equal to the + /// property (assuming that the latter is defined). + /// + public int? MaxValue + { + get; + set; + } + + #endregion + + #region ISupportInitialize Methods + + /// + /// See ISupportInitialize. + /// + public override void EndInit() + { + base.EndInit(); + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/ItemType.cs b/src/Framework/XamlTypes/ItemType.cs new file mode 100644 index 00000000000..a6b84c0a35c --- /dev/null +++ b/src/Framework/XamlTypes/ItemType.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// serialization class for Content type data. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Used to deserialize the item type information + /// + public sealed class ItemType : ISupportInitialize, IProjectSchemaNode + { + /// + /// Constructor + /// + public ItemType() + { + // by default it is included in up-to-date check + UpToDateCheckInput = true; + } + + /// + /// serializes IItemType.Name + /// + public string Name + { + get; + set; + } + + /// + /// serializes IItemType.DisplayName + /// + [Localizable(true)] + public string DisplayName + { + get; + set; + } + + /// + /// serializes IItemType.ItemType + /// + public string DefaultContentType + { + get; + set; + } + + /// + /// serializes IItemType.UpToDateCheckInput + /// + public bool UpToDateCheckInput + { + get; + set; + } + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + } + + #endregion + + #region IProjectSchemaNode Members + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjectTypes() + { + yield return typeof(ItemType); + } + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjects(Type type) + { + if (type == typeof(ItemType)) + { + yield return this; + } + } + #endregion + } +} diff --git a/src/Framework/XamlTypes/NameValuePair.cs b/src/Framework/XamlTypes/NameValuePair.cs new file mode 100644 index 00000000000..69ae212f911 --- /dev/null +++ b/src/Framework/XamlTypes/NameValuePair.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a name-value pair. +//----------------------------------------------------------------------- + +using System.ComponentModel; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents a name-value pair. The name cannot be null or empty. + /// + public class NameValuePair + { + #region Constructor + + /// + /// Default constructor needed for + /// + public NameValuePair() + { + } + + #endregion // Constructor + + #region Properties + + /// + /// The name. + /// + public string Name + { + get; + set; + } + + /// + /// The value. + /// + [Localizable(true)] + public string Value + { + get; + set; + } + + #endregion // Properties + } +} diff --git a/src/Framework/XamlTypes/ProjectSchemaDefinitions.cs b/src/Framework/XamlTypes/ProjectSchemaDefinitions.cs new file mode 100644 index 00000000000..e5d12a873b4 --- /dev/null +++ b/src/Framework/XamlTypes/ProjectSchemaDefinitions.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Universal Root node for the Data driven project schema XAML. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Serializatrion class for node for the Data driven project schema XAML + /// + [ContentProperty("Nodes")] + public sealed class ProjectSchemaDefinitions : IProjectSchemaNode + { + /// + /// Constructor + /// + public ProjectSchemaDefinitions() + { + Nodes = new List(); + } + + /// + /// Collection of any schema node + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Nodes + { + get; + set; + } + + #region IProjectSchemaNode Members + /// + /// see IProjectSchemaNode + /// + [SuppressMessage("Microsoft.Usage", "CA2301:EmbeddableTypesInContainersRule", MessageId = "allTypes", Justification = "All object types come from within this assembly, so there will not be any type equivalence problems")] + public IEnumerable GetSchemaObjectTypes() + { + Dictionary allTypes = new Dictionary(); + foreach (IProjectSchemaNode node in Nodes) + { + foreach (Type t in node.GetSchemaObjectTypes()) + { + allTypes[t] = true; + } + } + + return allTypes.Keys; + } + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjects(Type type) + { + foreach (IProjectSchemaNode node in Nodes) + { + foreach (object o in node.GetSchemaObjects(type)) + { + yield return o; + } + } + } + #endregion + } +} diff --git a/src/Framework/XamlTypes/Rule.cs b/src/Framework/XamlTypes/Rule.cs new file mode 100644 index 00000000000..c1b850cb965 --- /dev/null +++ b/src/Framework/XamlTypes/Rule.cs @@ -0,0 +1,617 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Rule class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + // DEV11DESIGNISSUE (pavana): Once the de-serialization is complete, seal the type so + // that the object becomes immutable (e.g. we can set a flag in EndInit() and throw + // in all property setter if this flag is set. + + /// + /// Methods for overriding one rule with another. + /// + public enum RuleOverrideMode + { + /// + /// A subsequent definition for a rule (with the same name) entirely overrides a previous definition. + /// + Replace, + + /// + /// A subsequent definition for a rule (with the same name) adds properties to a previous definition. + /// + Extend, + } + + /// + /// Used to represent the schema information for a Tool, a Custom Build Rule, a PropertyPage, etc. + /// + /// + /// + /// Normally represented on disk as XAML, only one instance of this class is maintained per XAML + /// file per project engine (solution). + /// + /// Those who manually instantiate this class should remember to call before + /// setting the first property and after setting the last property of the object. + /// + /// + /// + /// This partial class contains all properties which are public and hence settable in XAML. Those properties that + /// are internal are defined in another partial class below. + /// + [ContentProperty("Properties")] + [DebuggerDisplay("Rule: {Name}")] + public sealed partial class Rule : RuleSchema, ISupportInitialize, IProjectSchemaNode + { + #region Fields + + /// + /// See DisplayName property. + /// + private string _displayName; + + #endregion // Fields + + #region Constructor + + /// + /// Default constructor. Needed for deserialization from a persisted format. + /// + public Rule() + { + // Initialize collection properties in this class. This is required for + // proper deserialization. + Properties = new List(); + Categories = new List(); + Metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Set defaults. + SupportsFileBatching = false; + ShowOnlyRuleProperties = true; + SwitchPrefix = String.Empty; + Separator = String.Empty; + OverrideMode = RuleOverrideMode.Replace; + PropertyPagesHidden = false; + } + + #endregion // Constructor + + #region Properties + + /// + /// The name of this . + /// + /// + /// This field is mandatory and culture invariant. The value of this field cannot be set to the empty string. + /// + public string Name + { + get; + set; + } + + /// + /// The name that could be used by a prospective UI client to display this . + /// + /// + /// This field is optional and is culture sensitive. When this property is not set, it is assigned the same + /// value as the property (and hence, would not be localized). + /// + [Localizable(true)] + public string DisplayName + { + get + { + return _displayName ?? Name; + } + + set + { + _displayName = value; + } + } + + /// + /// The name of the tool executable when this rule represents a tool. + /// + public string ToolName + { + get; + set; + } + + /// + /// Description of this for use by a prospective UI client. + /// + /// + /// This field is optional and is culture sensitive. + /// + [Localizable(true)] + public string Description + { + get; + set; + } + + /// + /// Help information for this . + /// + /// + /// Maybe used to specify a help URL. This field + /// is optional and is culture sensitive. + /// + [Localizable(true)] + public string HelpString + { + get; + set; + } + + /// + /// The prefix to use for all property switches in this for the case when this property represent a tool. + /// + /// + /// The value specified can be overriden by the value specified by a child 's . + /// This field is optional and culture invariant. + /// + /// + /// For the VC++ CL task, WholeProgramOptimization is a boolean parameter. It's switch is GL and its + /// switch prefix (inherited from the parent since it is not overriden by WholeProgramOptimization) + /// is /. Thus the complete switch in the command line for this property would be /GL + /// + public string SwitchPrefix + { + get; + set; + } + + /// + /// The token used to separate a property switch from its value. + /// + /// + /// The value specified here is overriden by the value specified by the child 's . + /// This field is optional and culture invariant. + /// + /// + /// Example: Consider /D:WIN32. In this switch and value representation, ":" is the separator since its separates the switch D + /// from its value WIN32. + /// + public string Separator + { + get; + set; + } + + /// + /// The UI renderer template used to display this Rule. + /// + /// + /// The value used to set + /// this field can be anything as long as it is recognized by the intended renderer. + /// This field is required only if this Rule is meant to be displayed as a property page. + /// + public string PageTemplate + { + get; + set; + } + + /// + /// The for all the properties in this . This is overriden by any + /// data source defined locally for a property. + /// + /// + /// This field need not be specified only if all individual properties have data source defined locally. + /// + public DataSource DataSource + { + get; + set; + } + + /// + /// This is a suggestion to a prospective UI client on the relative location of this compared to all other Rules in the system. + /// + public int Order + { + get; + set; + } + + /// + /// This is used to specify whether multiple files need to be batched on one command line invocation. + /// + /// + /// This field is optional. + /// + public bool SupportsFileBatching + { + get; + set; + } + + /// + /// Indicates whether to hide the command line category or not. Default value is true. + /// + /// + /// This field is optional. + /// + public bool ShowOnlyRuleProperties + { + get; + set; + } + + /// + /// When this represents a Build Customization, this field represents the file extension to associate. + /// + /// + /// This field is optional. + /// + public string FileExtension + { + get; + set; + } + + /// + /// When this represents a Build Customization, this field represents the message to be displayed before executing a Build Customization during the build. + /// + /// + /// This field is optional. + /// + public string ExecutionDescription + { + get; + set; + } + + /// + /// When this represents a Build Customization, this field represents the command line template that is going to be used by a Build Customization task to invoke the tool. + /// + /// + /// This field is optional. + /// + public string CommandLine + { + get; + set; + } + + /// + /// When this represents a Build Customization, this field defines the semicolon separated list of additional inputs that are going to be evaluated + /// for the Build Customization target. + /// + /// + /// This field is optional. + /// + public string AdditionalInputs + { + get; + set; + } + + /// + /// When this represents a Build Customization, this field defines the semicolon separated list of outputs that are going to be evaluated + /// for the Build Customization target. + /// + /// + /// This field is optional. + /// + public string Outputs + { + get; + set; + } + + /// + /// Gets or sets the method to use when multiple rules with the same name appear in the project + /// to reconcile the rules into one instance. + /// + public RuleOverrideMode OverrideMode + { + get; + set; + } + + /// + /// This list of properties in this . Atleast one property should be specified. + /// + /// The list returned by this property should not be modified. + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Properties + { + get; + set; + } + + /// + /// The list of s that properties in this belong to. + /// + /// + /// This field is optional. Note that this field returns only the categories that were explicitly defined and do + /// not contain any auto-generated categories. When a contained in this + /// declares its category to be something that is not present in this list, then we auto-generate a + /// with that name and add it to the internal list of categories. That auto-generated category will not be returned + /// by this field. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Categories + { + get; + set; + } + + /// + /// Gets or sets arbitrary metadata that may be set on a rule. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Shipped this way in Dev11 Beta, which is go-live")] + public Dictionary Metadata { get; set; } + + /// + /// Gets or sets a value indicating if property pages for this rule should be hidden or not. + /// + public bool PropertyPagesHidden { get; set; } + + #endregion + } + + /// + /// Used to represent the schema information for a Tool, a Custom Build Rule, a PropertyPage, etc. + /// + /// + /// + /// Normally represented on disk as XAML, only one instance of this class is maintained per XAML + /// file per project engine (solution). + /// + /// Those who manually instantiate this class should remember to call before + /// setting the first property and after setting the last property of the object. + /// + /// + /// + /// This partial class contains members that are auto-generated, internal, etc. Whereas the + /// other partial class contains public properties that can be set in XAML. + /// + public sealed partial class Rule : RuleSchema, ISupportInitialize, IProjectSchemaNode + { + #region Fields + + /// + /// Thread synchronization. + /// + private object _syncObject = new object(); + + /// + /// See the property. + /// + private List _evaluatedCategories; + + /// + /// Ordered dictionary of category names and the properties contained in them. + /// The order of the categories is exactly the same as that specified in the XAML file. + /// + private OrderedDictionary _categoryNamePropertyListMap; + + /// + /// A lookup cache of property names to properties. + /// + private ReadOnlyDictionary _propertiesByNameMap; + + #endregion + + #region Properties + + /// + /// This property returns the union of XAML specified s and auto-generated + /// s. The latter are created from the missing categories that are being referred to by the + /// properties in this Rule. The auto-generated s only have their name set. + /// + public List EvaluatedCategories + { + get + { + // check-lock-check pattern DOESN'T work here because two fields get initialized within this lazy initialization method. + lock (_syncObject) + { + if (null == _evaluatedCategories) + { + CreateCategoryNamePropertyListMap(); + } + + return _evaluatedCategories; + } + } + } + + #endregion // Properties + + #region Public Methods + + /// + /// Returns all properties partitioned into categories. The return value is never + /// null. + /// The returned list may contain auto-generated categories. Note that if a + /// (or its derived classes) refer to a property that is not specified, then an new + /// Category is generated for the same. If not category is specified for the property, then + /// the property is placed in the "General" category. + /// The list of categories is exactly as specified in the Xaml file. The auto-generated + /// categories come (in no strict order) after the specified categories. + /// + /// A dictionary whose keys are the names and + /// the value is the list of properties in that category. + public OrderedDictionary GetPropertiesByCategory() + { + // check-lock-check pattern DOESN'T work here because two fields get initialized within this lazy initialization method. + lock (_syncObject) + { + if (null == _categoryNamePropertyListMap) + { + CreateCategoryNamePropertyListMap(); + } + + return _categoryNamePropertyListMap; + } + } + + /// + /// Returns the list of properties in a . Returns null if this + /// doesn't contain this category. + /// + public IList GetPropertiesInCategory(string categoryName) + { + // check-lock-check pattern DOESN'T work here because two fields get initialized within this lazy initialization method. + lock (_syncObject) + { + if (null == _categoryNamePropertyListMap) + { + CreateCategoryNamePropertyListMap(); + } + + return _categoryNamePropertyListMap[categoryName] as IList; + } + } + + /// + /// Returns a property with a given name. + /// + /// The property, or null if one with a matching name could not be found. + public BaseProperty GetProperty(string propertyName) + { + if (_propertiesByNameMap == null) + { + lock (_syncObject) + { + if (_propertiesByNameMap == null) + { + var map = new Dictionary(this.Properties.Count, StringComparer.OrdinalIgnoreCase); + foreach (var property in this.Properties) + { + map[property.Name] = property; + } + + _propertiesByNameMap = new ReadOnlyDictionary(map); + } + } + } + + BaseProperty result; + _propertiesByNameMap.TryGetValue(propertyName, out result); + return result; + } + + #endregion // Public Methods + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + Initialize(); + } + + #endregion + + #region IProjectSchemaNode Members + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjectTypes() + { + yield return typeof(Rule); + } + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjects(Type type) + { + if (type == typeof(Rule)) + { + yield return this; + } + } + #endregion + + #region Private Methods + + /// + /// Initializes this class after Xaml loading is done. + /// + private void Initialize() + { + if (null != Properties) + { + // Set parent pointers on all containing properties. + foreach (BaseProperty property in Properties) + { + property.ContainingRule = this; + } + } + } + + /// + /// Creates a map containing all the evaluated category names and the list of + /// properties belonging to that category. + /// + private void CreateCategoryNamePropertyListMap() + { + lock (_syncObject) + { + _evaluatedCategories = new List(); + + if (null != Categories) + { + _evaluatedCategories.AddRange(Categories); + } + + _categoryNamePropertyListMap = new OrderedDictionary(); + + foreach (Category category in Categories) + { + _categoryNamePropertyListMap.Add(category.Name, new List()); + } + + foreach (BaseProperty property in Properties) + { + // If a property refers to a category which does not have an entry in the Xaml file, + // create a category object ourselves. + if (!_categoryNamePropertyListMap.Contains(property.Category)) + { + Category category = new Category(); + category.Name = property.Category; + + _evaluatedCategories.Add(category); + _categoryNamePropertyListMap.Add(category.Name, new List()); + } + + List propertiesInTheSameCategory = _categoryNamePropertyListMap[property.Category] as List; + propertiesInTheSameCategory.Add(property); + } + } + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/RuleBag.cs b/src/Framework/XamlTypes/RuleBag.cs new file mode 100644 index 00000000000..a8ac2cb9398 --- /dev/null +++ b/src/Framework/XamlTypes/RuleBag.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class which is a container of Rule instances. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// This is a simple container for instances. + /// + /// + /// Note that we only deal in terms of s + /// as far as property pages are concerned. The is only used as a + /// container for more than one . The containing s are + /// immediately stripped off after loading of the xaml file. + /// + [ContentProperty("Rules")] + public sealed class RuleBag : ISupportInitialize, IProjectSchemaNode + { + #region Constructor + + /// + /// Default constructor needed for XAML deserialization. + /// + public RuleBag() + { + Rules = new List(); + } + + #endregion + + #region Properties + + /// + /// The collection of instances this instance contains. + /// Must have atleast one . + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Rules + { + get; + set; + } + + #endregion + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize Members. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize Members. + /// + public void EndInit() + { + } + + #endregion + + #region IProjectSchemaNode Members + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjectTypes() + { + yield return typeof(Rule); + } + + /// + /// see IProjectSchemaNode + /// + public IEnumerable GetSchemaObjects(Type type) + { + if (type == typeof(Rule)) + { + foreach (Rule r in Rules) + { + yield return r; + } + } + } + #endregion + } +} diff --git a/src/Framework/XamlTypes/RuleSchema.cs b/src/Framework/XamlTypes/RuleSchema.cs new file mode 100644 index 00000000000..0078fb81c95 --- /dev/null +++ b/src/Framework/XamlTypes/RuleSchema.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Base type for rules in the property page schema data model. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// The RuleSchema provides a strongly typed identity handle to the underlying schema data model. + /// + public abstract class RuleSchema + { + } +} diff --git a/src/Framework/XamlTypes/StringListProperty.cs b/src/Framework/XamlTypes/StringListProperty.cs new file mode 100644 index 00000000000..3673affd546 --- /dev/null +++ b/src/Framework/XamlTypes/StringListProperty.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the schema of a list-of-strings property. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.ComponentModel; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents the schema of a list-of-strings property. + /// + /// + /// Note, this represents + /// a list of strings, not a list of s. + /// + public sealed class StringListProperty : BaseProperty + { + #region Constructor + + /// + /// Default constructor. Needed for property XAML deserialization. + /// + public StringListProperty() + { + RendererValueSeparator = ";"; + } + + #endregion + + #region Properties + + /// + /// The separator to use in delineating individual values of this string list property + /// + /// + /// For Val1;Val2;Val3, if CommandLineValueSeparator is specified as, say ,, + /// the command line looks like this: /p:val1,val2,val3 + /// If not specified, the command line looks like this: /p:val1 /p:val2 /p:val3 + /// This field is optional. + /// + public string CommandLineValueSeparator + { + get; + set; + } + + /// + /// Please don't use. This is planned to be deprecated. + /// + public string RendererValueSeparator + { + get; + set; + } + + /// + /// Qualifies this string property to give it a more specific classification. + /// + /// + /// Similar to the property. + /// + public string Subtype + { + get; + set; + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/StringProperty.cs b/src/Framework/XamlTypes/StringProperty.cs new file mode 100644 index 00000000000..09fc743a768 --- /dev/null +++ b/src/Framework/XamlTypes/StringProperty.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the schema of a string property. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents the schema of a string property. + /// + public sealed class StringProperty : BaseProperty + { + #region Properties + + /// + /// Qualifies this string property to give it a more specific classification. + /// + /// + /// The value this field is set to, must be understood by the consumer of this field + /// (normally a UI renderer). + /// + /// The value of this property can be set to, say, "File", "Folder", "CarModel" etc. to specify + /// if this is a file path, folder path, car model name etc. + public string Subtype + { + get; + set; + } + + #endregion + } +} diff --git a/src/Framework/XamlTypes/ValueEditor.cs b/src/Framework/XamlTypes/ValueEditor.cs new file mode 100644 index 00000000000..430d602b897 --- /dev/null +++ b/src/Framework/XamlTypes/ValueEditor.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a property value editor. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Markup; + +namespace Microsoft.Build.Framework.XamlTypes +{ + /// + /// Represents a value editor + /// + [ContentProperty("Metadata")] + public sealed class ValueEditor : ISupportInitialize + { + #region Fields + + /// + /// See DisplayName property. + /// + private string _displayName; + + #endregion + + #region Constructor + + /// + /// Default constructor needed for XAML deserialization. + /// + public ValueEditor() + { + Metadata = new List(); + } + + #endregion // Constructor + + #region Properties + + /// + /// The name of this . This field is mandatory and culture invariant. + /// + public string EditorType + { + get; + set; + } + + /// + /// The UI display name for the editor + /// + [Localizable(true)] + public string DisplayName + { + get + { + return _displayName ?? String.Empty; + } + + set + { + _displayName = value; + } + } + + /// + /// Additional attributes of the editor that are not generic enough to be made + /// properties on this class. This field is optional. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This has shipped in Framework, which is especially important to keep binary compatible, so we can't change it now")] + public List Metadata + { + get; + set; + } + + #endregion // Properties + + #region ISupportInitialize Members + + /// + /// See ISupportInitialize. + /// + public void BeginInit() + { + } + + /// + /// See ISupportInitialize. + /// + public void EndInit() + { + } + + #endregion + } +} diff --git a/src/Framework/native.rc b/src/Framework/native.rc new file mode 100644 index 00000000000..4b1150f3d62 --- /dev/null +++ b/src/Framework/native.rc @@ -0,0 +1,5 @@ +// From Dev12 on we want the versioning strings to be the VS ones instead of .NET Framework ones. +#include +#include + +#include \ No newline at end of file diff --git a/src/MSBuild.sln b/src/MSBuild.sln new file mode 100644 index 00000000000..999d72984bb --- /dev/null +++ b/src/MSBuild.sln @@ -0,0 +1,166 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.22512.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Framework", "Framework\Microsoft.Build.Framework.csproj", "{571F09DB-A81A-4444-945C-6F7B530054CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Utilities", "Utilities\Microsoft.Build.Utilities.csproj", "{828566EE-6F6A-4EF4-98B0-513F7DF9C628}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build", "XMakeBuildEngine\Microsoft.Build.csproj", "{16CD7635-7CF4-4C62-A77B-CF87D0F09A58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSBuild", "XMakeCommandLine\MSBuild.csproj", "{23C9FD0E-70C5-4F1F-B08A-D2774240FB51}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Tasks", "XMakeTasks\Microsoft.Build.Tasks.csproj", "{59A73FE0-D3B7-4299-9063-3A587D429AF4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Framework.UnitTests", "Framework\UnitTests\Microsoft.Build.Framework.UnitTests.csproj", "{BE53C18D-7E4E-4C57-A359-1D662C384511}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.CommandLine.UnitTests", "XMakeCommandLine\UnitTests\Microsoft.Build.CommandLine.UnitTests.csproj", "{C79756DC-CC78-45D6-AE11-8BB35F201CE4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Utilities.UnitTests", "Utilities\UnitTests\Microsoft.Build.Utilities.UnitTests.csproj", "{31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Tasks.UnitTests", "XMakeTasks\UnitTests\Microsoft.Build.Tasks.UnitTests.csproj", "{32126DCE-7484-4E4B-85DA-12378C0F2FC7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.Engine.UnitTests", "XMakeBuildEngine\UnitTests\Microsoft.Build.Engine.UnitTests.csproj", "{D06D5D07-9DFB-4896-B11F-0A8C44F8F971}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4900B3B8-4310-4D5B-B1F7-2FDF9199765F}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\packages.config = .nuget\packages.config + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {571F09DB-A81A-4444-945C-6F7B530054CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Debug|x64.Build.0 = Debug|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Debug|x86.Build.0 = Debug|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Release|Any CPU.Build.0 = Release|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Release|x64.ActiveCfg = Release|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Release|x64.Build.0 = Release|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Release|x86.ActiveCfg = Release|Any CPU + {571F09DB-A81A-4444-945C-6F7B530054CD}.Release|x86.Build.0 = Release|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Debug|Any CPU.Build.0 = Debug|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Debug|x64.ActiveCfg = Debug|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Debug|x64.Build.0 = Debug|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Debug|x86.ActiveCfg = Debug|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Debug|x86.Build.0 = Debug|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Release|Any CPU.ActiveCfg = Release|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Release|Any CPU.Build.0 = Release|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Release|x64.ActiveCfg = Release|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Release|x64.Build.0 = Release|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Release|x86.ActiveCfg = Release|Any CPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628}.Release|x86.Build.0 = Release|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Debug|x64.ActiveCfg = Debug|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Debug|x64.Build.0 = Debug|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Debug|x86.ActiveCfg = Debug|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Debug|x86.Build.0 = Debug|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Release|Any CPU.Build.0 = Release|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Release|x64.ActiveCfg = Release|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Release|x64.Build.0 = Release|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Release|x86.ActiveCfg = Release|Any CPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58}.Release|x86.Build.0 = Release|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Debug|x64.ActiveCfg = Debug|x64 + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Debug|x64.Build.0 = Debug|x64 + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Debug|x86.ActiveCfg = Debug|x86 + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Debug|x86.Build.0 = Debug|x86 + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Release|Any CPU.Build.0 = Release|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Release|x64.ActiveCfg = Release|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Release|x64.Build.0 = Release|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Release|x86.ActiveCfg = Release|Any CPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51}.Release|x86.Build.0 = Release|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Debug|x64.ActiveCfg = Debug|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Debug|x64.Build.0 = Debug|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Debug|x86.ActiveCfg = Debug|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Debug|x86.Build.0 = Debug|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Release|Any CPU.Build.0 = Release|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Release|x64.ActiveCfg = Release|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Release|x64.Build.0 = Release|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Release|x86.ActiveCfg = Release|Any CPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4}.Release|x86.Build.0 = Release|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Debug|x64.Build.0 = Debug|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Debug|x86.Build.0 = Debug|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Release|Any CPU.Build.0 = Release|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Release|x64.ActiveCfg = Release|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Release|x64.Build.0 = Release|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Release|x86.ActiveCfg = Release|Any CPU + {BE53C18D-7E4E-4C57-A359-1D662C384511}.Release|x86.Build.0 = Release|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Debug|x64.ActiveCfg = Debug|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Debug|x64.Build.0 = Debug|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Debug|x86.ActiveCfg = Debug|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Debug|x86.Build.0 = Debug|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Release|Any CPU.Build.0 = Release|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Release|x64.ActiveCfg = Release|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Release|x64.Build.0 = Release|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Release|x86.ActiveCfg = Release|Any CPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4}.Release|x86.Build.0 = Release|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Debug|x64.ActiveCfg = Debug|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Debug|x64.Build.0 = Debug|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Debug|x86.ActiveCfg = Debug|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Debug|x86.Build.0 = Debug|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Release|Any CPU.Build.0 = Release|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Release|x64.ActiveCfg = Release|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Release|x64.Build.0 = Release|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Release|x86.ActiveCfg = Release|Any CPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE}.Release|x86.Build.0 = Release|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Debug|x64.ActiveCfg = Debug|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Debug|x64.Build.0 = Debug|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Debug|x86.ActiveCfg = Debug|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Debug|x86.Build.0 = Debug|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Release|Any CPU.Build.0 = Release|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Release|x64.ActiveCfg = Release|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Release|x64.Build.0 = Release|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Release|x86.ActiveCfg = Release|Any CPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7}.Release|x86.Build.0 = Release|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Debug|x64.ActiveCfg = Debug|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Debug|x64.Build.0 = Debug|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Debug|x86.ActiveCfg = Debug|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Debug|x86.Build.0 = Debug|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Release|Any CPU.Build.0 = Release|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Release|x64.ActiveCfg = Release|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Release|x64.Build.0 = Release|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Release|x86.ActiveCfg = Release|Any CPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Packages.dgml b/src/Packages.dgml new file mode 100644 index 00000000000..5a754cb5bb3 --- /dev/null +++ b/src/Packages.dgml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Shared/AssemblyFoldersEx.cs b/src/Shared/AssemblyFoldersEx.cs new file mode 100644 index 00000000000..9753c6e5894 --- /dev/null +++ b/src/Shared/AssemblyFoldersEx.cs @@ -0,0 +1,474 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Win32; +using System.Collections; +using System.Globalization; +using System.Diagnostics; +using System.Reflection; +using System.Collections.Generic; +using Microsoft.Build.Utilities; +using ProcessorArchitecture = System.Reflection.ProcessorArchitecture; + +namespace Microsoft.Build.Shared +{ + /// + /// Implements the rules for finding component directories using the AssemblyFoldersEx scheme. + /// + /// This is the normal schema: + /// + /// [HKLM | HKCU]\SOFTWARE\MICROSOFT\.NetFramework\ + /// v1.0.3705 + /// AssemblyFoldersEx + /// Infragistics.GridControl.1.0: + /// @Default = c:\program files\infragistics\grid control\1.0\bin + /// @Description = Infragistics Grid Control for .NET version 1.0 + /// 9466 + /// @Default = c:\program files\infragistics\grid control\1.0sp1\bin + /// @Description = SP1 for Infragistics Grid Control for .NET version 1.0 + /// + /// + /// The root registry path is the following: + /// + /// [HKLM | HKCU]\{AssemblyFoldersBase}\{RuntimeVersion}\{AssemblyFoldersSuffix} + /// + /// Where: + /// + /// {AssemblyFoldersBase} = Software\Microsoft\[.NetFramework | .NetCompactFramework] + /// {RuntimeVersion} = the runtime version property from the project file + /// {AssemblyFoldersSuffix} = [ PocketPC | SmartPhone | WindowsCE]\AssemblyFoldersEx + /// + /// + internal class AssemblyFoldersEx : IEnumerable + { + /// + /// The list of directory names found from the registry. + /// + private List _directoryNames = new List(); + + /// + /// Construct. + /// + /// Like Software\Microsoft\[.NetFramework | .NetCompactFramework] + /// The runtime version property from the project file. + /// Like [ PocketPC | SmartPhone | WindowsCE]\AssemblyFoldersEx + /// Used to find registry subkey names. + /// Used to find registry key default values. + internal AssemblyFoldersEx + ( + string registryKeyRoot, + string targetRuntimeVersion, + string registryKeySuffix, + string osVersion, + string platform, + GetRegistrySubKeyNames getRegistrySubKeyNames, + GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, + ProcessorArchitecture targetProcessorArchitecture, + OpenBaseKey openBaseKey + ) + { + bool is64bitOS = Environment.Is64BitOperatingSystem; + bool targeting64bit = targetProcessorArchitecture == ProcessorArchitecture.Amd64 || targetProcessorArchitecture == ProcessorArchitecture.IA64; + + // The registry lookup should be as follows: + /* 64 bit OS: + * Targeting 64 bit: + * First, look in 64 bit registry location + * Second, look in 32 bit registry location + * Targeting X86 or MSIL: + * First, look in the 32 bit hive + * Second, look in 64 bit hive + * + * 32 bit OS: + * 32 bit process: + * Targeting 64 bit, or X86, or MSIL: + * Look in the default registy which is the 32 bit hive + */ + + // Under WOW64 the HKEY_CURRENT_USER\SOFTWARE key is shared. This means the values are the same in the 64 bit and 32 bit views. This means we only need to get one view of this key. + FindDirectories(RegistryView.Default, RegistryHive.CurrentUser, registryKeyRoot, targetRuntimeVersion, registryKeySuffix, osVersion, platform, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, openBaseKey); + + if (is64bitOS) + { + // Under WOW64 the HKEY_LOCAL_MACHINE\SOFTWARE key is redirected. This means the values can be different in the 64 bit and 32 bit views. This means we only need to get look at both keys. + + if (targeting64bit) + { + FindDirectories(RegistryView.Registry64, RegistryHive.LocalMachine, registryKeyRoot, targetRuntimeVersion, registryKeySuffix, osVersion, platform, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, openBaseKey); + FindDirectories(RegistryView.Registry32, RegistryHive.LocalMachine, registryKeyRoot, targetRuntimeVersion, registryKeySuffix, osVersion, platform, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, openBaseKey); + } + else + { + FindDirectories(RegistryView.Registry32, RegistryHive.LocalMachine, registryKeyRoot, targetRuntimeVersion, registryKeySuffix, osVersion, platform, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, openBaseKey); + FindDirectories(RegistryView.Registry64, RegistryHive.LocalMachine, registryKeyRoot, targetRuntimeVersion, registryKeySuffix, osVersion, platform, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, openBaseKey); + } + } + else + { + FindDirectories(RegistryView.Default, RegistryHive.LocalMachine, registryKeyRoot, targetRuntimeVersion, registryKeySuffix, osVersion, platform, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, openBaseKey); + } + } + + /// + /// Finds directories for a specific registry key. + /// + /// Base to look for directories under. + /// Like Software\Microsoft\[.NetFramework | .NetCompactFramework] + /// The runtime version property from the project file. + /// Like [ PocketPC | SmartPhone | WindowsCE]\AssemblyFoldersEx + /// Used to find registry subkey names. + /// Used to find registry key default values. + private void FindDirectories + ( + RegistryView view, + RegistryHive hive, + string registryKeyRoot, + string targetRuntimeVersion, + string registryKeySuffix, + string osVersion, + string platform, + GetRegistrySubKeyNames getRegistrySubKeyNames, + GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, + OpenBaseKey openBaseKey + ) + { + // Open the hive for a given view + using (RegistryKey baseKey = openBaseKey(hive, view)) + { + IEnumerable versions = getRegistrySubKeyNames(baseKey, registryKeyRoot); + + // No versions found. + if (versions == null) + { + return; + } + + List versionStrings = GatherVersionStrings(targetRuntimeVersion, versions); + + // Loop the versions, looking for component keys. + List componentKeys = new List(); + + foreach (ExtensionFoldersRegistryKey versionString in versionStrings) + { + // Make like SOFTWARE\MICROSOFT\.NetFramework\v2.0.x86chk\AssemblyFoldersEx + string fullVersionKey = registryKeyRoot + @"\" + versionString.RegistryKey + @"\" + registryKeySuffix; + IEnumerable components = getRegistrySubKeyNames(baseKey, fullVersionKey); + + if (components != null) + { + // Sort the components in reverse alphabetical order so values with higher alphabetical names are earlier in the array. + // This is to try and get newer versioned components based on the fact they should have higher versioned names. + List sortedComponents = new List(); + + foreach (string component in components) + { + sortedComponents.Add(component); + } + + // The reason we sort here rather than on the component keys is that we do not want to sort using the FullVersionKey + // the versions have already been sorted (with things that look like raw drops being tacked onto the bottom of the list after sorting) + // By sorting the versions again we will get these raw drop numbers possibly being somewhere other than at the bottom and thereby cause the resolver + // to find the assembly in the wrong location. + sortedComponents.Sort(ReverseStringGenericComparer.Comparer); + + foreach (string component in sortedComponents) + { + // ComponentKeys are like SOFTWARE\MICROSOFT\.NetFramework\v1.0.x86chk\AssemblyFoldersEx\Infragistics.GridControl.1.0 + componentKeys.Add(new ExtensionFoldersRegistryKey(fullVersionKey + @"\" + component, versionString.TargetFrameworkVersion)); + } + } + } + + // Loop the component keys, looking for servicing keys. + List directoryKeys = new List(); + + foreach (ExtensionFoldersRegistryKey componentKey in componentKeys) + { + IEnumerable servicingKeys = getRegistrySubKeyNames(baseKey, componentKey.RegistryKey); + + if (servicingKeys != null) + { + List fullServicingKeys = new List(); + + foreach (string servicingKey in servicingKeys) + { + // ServicingKeys are like SOFTWARE\MICROSOFT\.NetFramework\v1.0.3705\AssemblyFoldersEx\Infragistics.GridControl.1.0\9120 + fullServicingKeys.Add(componentKey.RegistryKey + @"\" + servicingKey); + } + + // Alphabetize to put them in version order. + fullServicingKeys.Sort(ReverseStringGenericComparer.Comparer); + foreach (string key in fullServicingKeys) + { + directoryKeys.Add(new ExtensionFoldersRegistryKey(key, componentKey.TargetFrameworkVersion)); + } + + directoryKeys.Add(componentKey); + } + } + + // Now, we have a properly ordered collection of registry keys, each of which + // should point to a default value with a file path. Get those files paths. + foreach (ExtensionFoldersRegistryKey directoryKey in directoryKeys) + { + if (!(String.IsNullOrEmpty(platform) && String.IsNullOrEmpty(osVersion))) + { + using (RegistryKey keyPlatform = baseKey.OpenSubKey(directoryKey.RegistryKey, false)) + { + if (keyPlatform != null && keyPlatform.ValueCount > 0) + { + if (platform != null && platform.Length > 0) + { + string platformValue = keyPlatform.GetValue("Platform", null) as string; + + if (!String.IsNullOrEmpty(platformValue) && !MatchingPlatformExists(platform, platformValue)) + { + continue; + } + } + + if (osVersion != null && osVersion.Length > 0) + { + Version ver = VersionUtilities.ConvertToVersion(osVersion); + + if (!IsVersionInsideRange(ver, keyPlatform)) + { + continue; + } + } + } + } + } + + string directoryName = getRegistrySubKeyDefaultValue(baseKey, directoryKey.RegistryKey); + + if (null != directoryName) + { + _directoryNames.Add(new AssemblyFoldersExInfo(hive, view, directoryKey.RegistryKey, directoryName, directoryKey.TargetFrameworkVersion)); + } + } + } + } + + private bool MatchingPlatformExists(string platform, string platformValue) + { + bool match = false; + + if (platformValue != null && platformValue.Length > 0) + { + string[] platforms = platformValue.Split(';'); + foreach (string p in platforms) + { + if (String.Compare(p, platform, StringComparison.OrdinalIgnoreCase) == 0) + { + match = true; + break; + } + } + } + + return match; + } + + private bool IsVersionInsideRange(Version v, RegistryKey keyPlatform) + { + bool insideRange = true; + + if (v != null) + { + string minVersionAsString = keyPlatform.GetValue("MinOSVersion", null) as string; + Version minVersion = minVersionAsString == null ? null : VersionUtilities.ConvertToVersion(minVersionAsString); + if (minVersion != null && minVersion > v) + { + // Filter keys with MinOSVersion > OSVersion + insideRange = false; + } + + string maxVersionAsString = keyPlatform.GetValue("MaxOSVersion", null) as string; + Version maxVersion = maxVersionAsString == null ? null : VersionUtilities.ConvertToVersion(maxVersionAsString); + if (maxVersion != null && maxVersion < v) + { + // Filter keys with MaxOSVersion < OSVersion + insideRange = false; + } + } + + return insideRange; + } + + /// + /// The algorithm for gathering versions from the registry is as follows: + /// 1) targetRuntimeVersion is the target framework version you are targeting + /// 2) versions is a string list from reading the registry, this list is in what ever order the registry returns + /// the keys to us in, this is usually alphabetical. + /// + /// We will go through each version string and do the following: + /// 1) Check to see if the string is a version + /// If the string is not a version we will check to see if the string starts with the framework we are targeting, + /// if it does we will add it to a list which will be added at the end + /// of the versions list, if not it gets ignored. We do this to stay compatible to what we have been doing since whidby. + /// + /// If the string is a version + /// We check to see if the version is a valid target framework version. Meaning. It has a Maj.Minor version and may have + /// build, Build is less than or equal to 255 and there is no revisison. The reason the build number needs to be less thatn 255 is because + /// 255 is the largest build number for a target framework version that visual studio 2010 supports. The build number is supposed to + /// represent a service pack on the 4.0 framework. + /// + /// If the string is a valid target framework version we check to see we already have a dictionary entry and if not we + /// add one. + /// If the string is not a valid target framework then we will ignore the part of the version which makes it invalid + /// (either the build or the revision, or both) and see where that version would fit in the dictionary as a key and + /// then put the original version string into the list for that entry. + /// + /// Since the dictionary is sorted in reverse order to generate the list to return we do the following: + /// Go through the list of dictionary entries + /// For each entry sort the list in reverse alphabeticl order and add the entries in their internal list to the listToreturn. + /// + /// This way we have a reverse sorted list of all of the version keys. + /// + internal static List GatherVersionStrings(string targetRuntimeVersion, IEnumerable versions) + { + List additionalToleratedKeys = new List(); + Version targetVersion = VersionUtilities.ConvertToVersion(targetRuntimeVersion); + List versionStrings = new List(); + + // This dictionary will contain a set of target framework versions and a list of strings read from the registry which are supposed to be treated like the + // target framework version stored as the key. + // For example: + // If the target framework version is 4.0 but the registry string is v4.0.2116 then we want to treat v4.0.2116 as if it was v4.0 during the sort, + // but when reading out of the registry + // we need to know the original value so we can open the correct key. + // + // The reason there needs to be a list for each target framework version is that there could be multiple keys in the registry which should be treated + // like v4.0 for sorting. + // for example lets say we had the following entries in the registry: + // 4.0.2116 and 4.0.2116.87 both of these are supposed to be treated like v4.0 because they are not valid target framework versions but + // are valid version numbers and should be searched when we are targeting 4.0. + SortedDictionary> targetFrameworkVersionToRegistryVersions = new SortedDictionary>(ReverseVersionGenericComparer.Comparer); + + // Loop over versions from registry. + foreach (string version in versions) + { + if ((version.Length > 0) && (String.Compare(version.Substring(0, 1), "v", StringComparison.OrdinalIgnoreCase) == 0)) + { + Version candidateVersion = VersionUtilities.ConvertToVersion(version); + + if (candidateVersion == null) + { + // If it wasn't a true version number, we may still want to tolerate it because raw drops have + // the form 'v2.0.x86chk' + if (String.Compare(version, 0, targetRuntimeVersion, 0, targetRuntimeVersion.Length, StringComparison.OrdinalIgnoreCase) == 0) + { + additionalToleratedKeys.Add(version); + } + } + else + { + // To be added to our dictionary our candidate version from the registry must be a valid target framework version which is less than or equal + // to the target version. Therefore if the candidate version is not a valid target framework version we will pretend it is and sort it in its correct form. + + Version replacementVersion = null; + if (candidateVersion.Build > 255) + { + // Pretend the candidate version is really Maj.Minor ignore the build and revision + replacementVersion = new Version(candidateVersion.Major, candidateVersion.Minor); + } + else if (candidateVersion.Revision != -1) + { + // Pretend the version is Maj.Minor.Build ignore the revision + replacementVersion = new Version(candidateVersion.Major, candidateVersion.Minor, candidateVersion.Build); + } + else + { + // Was not replaced just use as is since it is a good version + replacementVersion = candidateVersion; + } + + // If the target version is null then we need to do a partial version match + bool addToListDueToPartialNameMatch = false; + if (targetVersion == null) + { + if (String.Compare(version, 0, targetRuntimeVersion, 0, targetRuntimeVersion.Length, StringComparison.OrdinalIgnoreCase) == 0) + { + addToListDueToPartialNameMatch = true; + } + } + + // If we have a target framework version as a version object is the version we are going to add to our dictionary in the correct range. + bool replacementVersionWithinRange = (targetVersion != null && targetVersion >= replacementVersion); + + // Add the version to our dictionary if we are within the correct range or we had no target framework version but partially matched on the version string. + if (replacementVersion != null && (replacementVersionWithinRange || addToListDueToPartialNameMatch)) + { + AddCandidateVersion(targetFrameworkVersionToRegistryVersions, version, replacementVersion); + } + } + } + } + + // Go through the target framework versions in reverse version order + foreach (KeyValuePair> entry in targetFrameworkVersionToRegistryVersions) + { + List frameworkList = entry.Value; + + // Sort the list in reverse alphabetical order since these are the version strings from the registry + frameworkList.Sort(ReverseStringGenericComparer.Comparer); + + foreach (string s in frameworkList) + { + // The string in this case already contains the v + versionStrings.Add(new ExtensionFoldersRegistryKey(s, entry.Key)); + } + } + + // The additional tolerated keys are added onto the end of the versions list in what ever order they came from the + // registry in. + foreach (string key in additionalToleratedKeys) + { + versionStrings.Add(new ExtensionFoldersRegistryKey(key, targetVersion ?? new Version(0, 0))); + } + + return versionStrings; + } + + /// + /// Given a candidate version we need to add it to the dictionary of targetFrameworkToRegistry versions. This involves determining if we need to add it to + /// an existing entry or create a new one. + /// + private static void AddCandidateVersion(SortedDictionary> targetFrameworkVersionToRegistryVersions, string version, Version candidateVersion) + { + List listOfFrameworks = null; + if (targetFrameworkVersionToRegistryVersions.TryGetValue(candidateVersion, out listOfFrameworks)) + { + listOfFrameworks.Add(version); + } + else + { + // The version is not in our dictionary yet, lets add it + // We need a new list since one has not been added yet + listOfFrameworks = new List(); + // Make sure we add ourselves to the list + listOfFrameworks.Add(version); + + targetFrameworkVersionToRegistryVersions.Add(candidateVersion, listOfFrameworks); + } + } + + /// + /// Get Enumerator + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _directoryNames.GetEnumerator(); + } + + /// + /// Get enumerator + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + } +} diff --git a/src/Shared/AssemblyLoadInfo.cs b/src/Shared/AssemblyLoadInfo.cs new file mode 100644 index 00000000000..ea4edacfd95 --- /dev/null +++ b/src/Shared/AssemblyLoadInfo.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps location info for an assembly +//----------------------------------------------------------------------- + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.Shared +{ + /// + /// This class packages information about how to load a given assembly -- an assembly can be loaded by either its assembly + /// name (strong or weak), or its filename/path. + /// + /// + /// Uses factory to instantiate correct private class to save space: only one field is ever used of the two. + /// + internal abstract class AssemblyLoadInfo : INodePacketTranslatable + { + /// + /// This constructor initializes the assembly information. + /// + internal static AssemblyLoadInfo Create(string assemblyName, string assemblyFile) + { + ErrorUtilities.VerifyThrow(((assemblyName != null) && (assemblyName.Length > 0)) || ((assemblyFile != null) && (assemblyFile.Length > 0)), + "We must have either the assembly name or the assembly file/path."); + ErrorUtilities.VerifyThrow((assemblyName == null) || (assemblyFile == null), + "We must not have both the assembly name and the assembly file/path."); + + if (assemblyName != null) + { + return new AssemblyLoadInfoWithName(assemblyName); + } + else + { + return new AssemblyLoadInfoWithFile(assemblyFile); + } + } + + /// + /// Gets the assembly's identity denoted by its strong/weak name. + /// + public abstract string AssemblyName + { + get; + } + + /// + /// Gets the path to the assembly file. + /// + public abstract string AssemblyFile + { + get; + } + + /// + /// Get the assembly location + /// + internal abstract string AssemblyLocation + { + get; + } + + /// + /// Computes a hashcode for this assembly info, so this object can be used as a key into + /// a hash table. + /// + public override int GetHashCode() + { + return AssemblyLocation.GetHashCode(); + } + + /// + /// Determines if two AssemblyLoadInfos are effectively the same. + /// + public override bool Equals(Object obj) + { + if (obj == null) + { + return false; + } + + AssemblyLoadInfo otherAssemblyInfo = obj as AssemblyLoadInfo; + + if (otherAssemblyInfo == null) + { + return false; + } + + return ((this.AssemblyName == otherAssemblyInfo.AssemblyName) && (this.AssemblyFile == otherAssemblyInfo.AssemblyFile)); + } + + public void Translate(INodePacketTranslator translator) + { + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + string assemblyName = AssemblyName; + string assemblyFile = AssemblyFile; + translator.Translate(ref assemblyName); + translator.Translate(ref assemblyFile); + } + + static public AssemblyLoadInfo FactoryForTranslation(INodePacketTranslator translator) + { + string assemblyName = null; + string assemblyFile = null; + translator.Translate(ref assemblyName); + translator.Translate(ref assemblyFile); + + return Create(assemblyName, assemblyFile); + } + + /// + /// Assembly represented by name + /// + private sealed class AssemblyLoadInfoWithName : AssemblyLoadInfo + { + /// + /// Assembly name + /// + private string _assemblyName; + + /// + /// Constructor + /// + internal AssemblyLoadInfoWithName(string assemblyName) + { + _assemblyName = assemblyName; + } + + /// + /// Gets the assembly's identity denoted by its strong/weak name. + /// + public override string AssemblyName + { + get { return _assemblyName; } + } + + /// + /// Gets the path to the assembly file. + /// + public override string AssemblyFile + { + get { return null; } + } + + /// + /// Get the assembly location + /// + internal override string AssemblyLocation + { + get { return _assemblyName; } + } + } + + /// + /// Assembly info that uses a file path + /// + private sealed class AssemblyLoadInfoWithFile : AssemblyLoadInfo + { + /// + /// Path to assembly + /// + private string _assemblyFile; + + /// + /// Constructor + /// + internal AssemblyLoadInfoWithFile(string assemblyFile) + { + ErrorUtilities.VerifyThrow(Path.IsPathRooted(assemblyFile), "Assembly file path should be rooted"); + + _assemblyFile = assemblyFile; + } + + /// + /// Gets the assembly's identity denoted by its strong/weak name. + /// + public override string AssemblyName + { + get { return null; } + } + + /// + /// Gets the path to the assembly file. + /// + public override string AssemblyFile + { + get { return _assemblyFile; } + } + + /// + /// Get the assembly location + /// + internal override string AssemblyLocation + { + get { return _assemblyFile; } + } + } + } +} diff --git a/src/Shared/AssemblyNameComparer.cs b/src/Shared/AssemblyNameComparer.cs new file mode 100644 index 00000000000..ecd7f35c7d7 --- /dev/null +++ b/src/Shared/AssemblyNameComparer.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Compare two AssemblyNameExtensions to each other +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.Shared +{ + /// + /// IKeyComparer implementation that compares AssemblyNames for using in Hashtables. + /// + [Serializable] + sealed internal class AssemblyNameComparer : IComparer, IEqualityComparer, IEqualityComparer + { + /// + /// Comparer for two assembly name extensions + /// + internal readonly static IComparer Comparer = new AssemblyNameComparer(false); + + /// + /// Comparer for two assembly name extensions + /// + internal readonly static IComparer ComparerConsiderRetargetable = new AssemblyNameComparer(true); + + /// + /// Comparer for two assembly name extensions + /// + internal readonly static IEqualityComparer GenericComparer = Comparer as IEqualityComparer; + + /// + /// Comparer for two assembly name extensions + /// + internal readonly static IEqualityComparer GenericComparerConsiderRetargetable = ComparerConsiderRetargetable as IEqualityComparer; + + /// + /// Should the comparer consider the retargetable flag when doing comparisons + /// + private bool _considerRetargetableFlag; + + /// + /// Private construct so there's only one instance. + /// + private AssemblyNameComparer(bool considerRetargetableFlag) + { + _considerRetargetableFlag = considerRetargetableFlag; + } + + /// + /// Compare o1 and o2 as AssemblyNames. + /// + public int Compare(object o1, object o2) + { + AssemblyNameExtension a1 = (AssemblyNameExtension)o1; + AssemblyNameExtension a2 = (AssemblyNameExtension)o2; + + int result = a1.CompareTo(a2, _considerRetargetableFlag); + return result; + } + + /// + /// Treat o1 and o2 as AssemblyNames. Are they equal? + /// + new public bool Equals(object o1, object o2) + { + AssemblyNameExtension a1 = (AssemblyNameExtension)o1; + AssemblyNameExtension a2 = (AssemblyNameExtension)o2; + return Equals(a1, a2); + } + + /// + /// Get a hashcode for AssemblyName. + /// + public int GetHashCode(object o) + { + AssemblyNameExtension a = (AssemblyNameExtension)o; + return GetHashCode(a); + } + + #region IEqualityComparer Members + + /// + /// Determine if the assembly name extensions are equal + /// + public bool Equals(AssemblyNameExtension x, AssemblyNameExtension y) + { + bool result = x.Equals(y, _considerRetargetableFlag); + return result; + } + + /// + /// Get a hashcode for AssemblyName. + /// + public int GetHashCode(AssemblyNameExtension obj) + { + int result = obj.GetHashCode(); + return result; + } + + #endregion + } +} diff --git a/src/Shared/AssemblyNameExtension.cs b/src/Shared/AssemblyNameExtension.cs new file mode 100644 index 00000000000..a3d39877ae1 --- /dev/null +++ b/src/Shared/AssemblyNameExtension.cs @@ -0,0 +1,914 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Shared +{ + /// + /// Specifies the parts of the assembly name to partially match + /// + [FlagsAttribute] + internal enum PartialComparisonFlags : int + { + /// + /// Compare SimpleName A.PartialCompare(B,SimpleName) match the simple name on A and B if the simple name on A is not null. + /// + SimpleName = 1, // 0000 0000 0000 0001 + + /// + /// Compare Version A.PartialCompare(B, Version) match the Version on A and B if the Version on A is not null. + /// + Version = 2, // 0000 0000 0000 0010 + + /// + /// Compare Culture A.PartialCompare(B, Culture) match the Culture on A and B if the Culture on A is not null. + /// + Culture = 4, // 0000 0000 0000 0100 + + /// + /// Compare PublicKeyToken A.PartialCompare(B, PublicKeyToken) match the PublicKeyToken on A and B if the PublicKeyToken on A is not null. + /// + PublicKeyToken = 8, // 0000 0000 0000 1000 + + /// + /// When doing a comparison A.PartialComapare(B, Default) compare all fields of A which are not null with B. + /// + Default = 15, // 0000 0000 0000 1111 + } + + /// + /// A replacement for AssemblyName that optimizes calls to FullName which is expensive. + /// The assembly name is represented internally by an AssemblyName and a string, conversion + /// between the two is done lazily on demand. + /// + [Serializable] + sealed internal class AssemblyNameExtension + { + private AssemblyName _asAssemblyName = null; + private string _asString = null; + private bool _isSimpleName = false; + private bool _hasProcessorArchitectureInFusionName; + private bool _immutable; + + /// + /// Set of assemblyNameExtensions that THIS assemblyname was remapped from. + /// + private HashSet _remappedFrom; + + static private AssemblyNameExtension s_unnamedAssembly = new AssemblyNameExtension(); + + /// + /// Construct an unnamed assembly. + /// Private because we want only one of these. + /// + private AssemblyNameExtension() + { + InitializeRemappedFrom(); + } + + /// + /// Construct with AssemblyName. + /// + /// + internal AssemblyNameExtension(AssemblyName assemblyName) : this() + { + _asAssemblyName = assemblyName; + } + + /// + /// Construct with string. + /// + /// + internal AssemblyNameExtension(string assemblyName) : this() + { + _asString = assemblyName; + } + + /// + /// Construct from a string, but immediately construct a real AssemblyName. + /// This will cause an exception to be thrown up front if the assembly name + /// isn't well formed. + /// + /// + /// The string version of the assembly name. + /// + /// + /// Used when the assembly name comes from a user-controlled source like a project file or config file. + /// Does extra checking on the assembly name and will throw exceptions if something is invalid. + /// + internal AssemblyNameExtension(string assemblyName, bool validate) : this() + { + _asString = assemblyName; + + if (validate) + { + // This will throw... + CreateAssemblyName(); + } + } + + /// + /// To be used as a delegate. Gets the AssemblyName of the given file. + /// + /// + /// + internal static AssemblyNameExtension GetAssemblyNameEx(string path) + { + AssemblyName assemblyName = null; + + try + { + assemblyName = AssemblyName.GetAssemblyName(path); + } + catch (System.IO.FileLoadException) + { + // Its pretty hard to get here, you need an assembly that contains a valid reference + // to a dependent assembly that, in turn, throws a FileLoadException during GetAssemblyName. + // Still it happened once, with an older version of the CLR. + + // ...falling through and relying on the targetAssemblyName==null behavior below... + } + catch (System.IO.FileNotFoundException) + { + // Its pretty hard to get here, also since we do a file existence check right before calling this method so it can only happen if the file got deleted between that check and this call. + } + + if (assemblyName == null) + { + return null; + } + return new AssemblyNameExtension(assemblyName); + } + + /// + /// Run after the object has been deserialized + /// + [OnDeserialized] + private void SetRemappedFromDefaultAfterSerialization(StreamingContext sc) + { + InitializeRemappedFrom(); + } + + /// + /// Initialize the remapped from structure. + /// + private void InitializeRemappedFrom() + { + if (_remappedFrom == null) + { + _remappedFrom = new HashSet(AssemblyNameComparer.GenericComparerConsiderRetargetable); + } + } + + /// + /// Assume there is a string version, create the AssemblyName version. + /// + private void CreateAssemblyName() + { + if (_asAssemblyName == null) + { + _asAssemblyName = GetAssemblyNameFromDisplayName(_asString); + + if (_asAssemblyName != null) + { + _hasProcessorArchitectureInFusionName = _asString.IndexOf("ProcessorArchitecture", StringComparison.OrdinalIgnoreCase) != -1; + _isSimpleName = ((Version == null) && (CultureInfo == null) && (GetPublicKeyToken() == null) && (!_hasProcessorArchitectureInFusionName)); + } + } + } + + /// + /// Assume there is a string version, create the AssemblyName version. + /// + private void CreateFullName() + { + if (_asString == null) + { + _asString = _asAssemblyName.FullName; + } + } + + /// + /// The base name of the assembly. + /// + /// + internal string Name + { + get + { + // Is there a string? + CreateAssemblyName(); + return _asAssemblyName.Name; + } + } + + /// + /// Gets the backing AssemblyName, this can be None. + /// + internal ProcessorArchitecture ProcessorArchitecture + { + get + { + if (_asAssemblyName != null) + { + return _asAssemblyName.ProcessorArchitecture; + } + else + { + return ProcessorArchitecture.None; + } + } + } + + /// + /// The assembly's version number. + /// + /// + internal Version Version + { + get + { + // Is there a string? + CreateAssemblyName(); + return _asAssemblyName.Version; + } + } + + /// + /// Is the assembly a complex name or a simple name. A simple name is where only the name is set + /// a complex name is where the version, culture or publickeytoken is also set + /// + internal bool IsSimpleName + { + get + { + CreateAssemblyName(); + return _isSimpleName; + } + } + + /// + /// Does the fullName have the processor architecture defined + /// + internal bool HasProcessorArchitectureInFusionName + { + get + { + CreateAssemblyName(); + return _hasProcessorArchitectureInFusionName; + } + } + + /// + /// Replace the current version with a new version. + /// + /// + internal void ReplaceVersion(Version version) + { + ErrorUtilities.VerifyThrow(!_immutable, "Object is immutable cannot replace the version"); + CreateAssemblyName(); + if (_asAssemblyName.Version != version) + { + _asAssemblyName.Version = version; + + // String would now be invalid. + _asString = null; + } + } + + /// + /// The assembly's Culture + /// + /// + internal CultureInfo CultureInfo + { + get + { + // Is there a string? + CreateAssemblyName(); + return _asAssemblyName.CultureInfo; + } + } + + /// + /// The assembly's retargetable bit + /// + /// + internal bool Retargetable + { + get + { + // Is there a string? + CreateAssemblyName(); + // Cannot use the HasFlag method on the Flags enum because this class needs to work with 3.5 + return ((_asAssemblyName.Flags & AssemblyNameFlags.Retargetable) == AssemblyNameFlags.Retargetable); + } + } + + /// + /// The full name of the original extension we were before being remapped. + /// + internal IEnumerable RemappedFromEnumerator + { + get + { + InitializeRemappedFrom(); + return _remappedFrom; + } + } + + /// + /// Add an assemblyNameExtension which represents an assembly name which was mapped to THIS assemblyName. + /// + internal void AddRemappedAssemblyName(AssemblyNameExtension extensionToAdd) + { + ErrorUtilities.VerifyThrow(extensionToAdd.Immutable, "ExtensionToAdd is not immutable"); + InitializeRemappedFrom(); + _remappedFrom.Add(extensionToAdd); + } + + /// + /// As an AssemblyName + /// + /// + internal AssemblyName AssemblyName + { + get + { + // Is there a string? + CreateAssemblyName(); + return _asAssemblyName; + } + } + + /// + /// The assembly's full name. + /// + /// + internal string FullName + { + get + { + // Is there a string? + CreateFullName(); + return _asString; + } + } + + /// + /// Get the assembly's public key token. + /// + /// + internal byte[] GetPublicKeyToken() + { + // Is there a string? + CreateAssemblyName(); + return _asAssemblyName.GetPublicKeyToken(); + } + + + /// + /// A special "unnamed" instance of AssemblyNameExtension. + /// + /// + internal static AssemblyNameExtension UnnamedAssembly + { + get + { + return s_unnamedAssembly; + } + } + + /// + /// Compare one assembly name to another. + /// + /// + /// + internal int CompareTo(AssemblyNameExtension that) + { + return CompareTo(that, false); + } + + /// + /// Compare one assembly name to another. + /// + /// + /// + internal int CompareTo(AssemblyNameExtension that, bool considerRetargetableFlag) + { + // Are they identical? + if (this.Equals(that, considerRetargetableFlag)) + { + return 0; + } + + // Are the base names not identical? + int result = CompareBaseNameTo(that); + if (result != 0) + { + return result; + } + + // We would like to compare the version numerically rather than alphabetically (because for example version 10.0.0. should be below 9 not between 1 and 2) + if (this.Version != that.Version) + { + if (this.Version == null) + { + // This is therefore less than that. Since this is null and that is not null + return -1; + } + else + { + // Will not return 0 as the this != that check above takes care of the case where they are equal. + result = this.Version.CompareTo(that.Version); + return result; + } + } + + // We need some final collating order for these, alphabetical by FullName seems as good as any. + return String.Compare(this.FullName, that.FullName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Get a hash code for this assembly name. + /// + /// + new internal int GetHashCode() + { + // Ok, so this isn't a great hashing algorithm. However, basenames with different + // versions or PKTs are relatively uncommon and so collisions should be low. + // Hashing on FullName is wrong because the order of tuple fields is undefined. + int hash = StringComparer.OrdinalIgnoreCase.GetHashCode(this.Name); + return hash; + } + + /// + /// Compare two base names as quickly as possible. + /// + /// + /// + internal int CompareBaseNameTo(AssemblyNameExtension that) + { + int result = CompareBaseNameToImpl(that); +#if DEBUG + // Now, compare to the real value to make sure the result was accurate. + AssemblyName a1 = _asAssemblyName; + AssemblyName a2 = that._asAssemblyName; + if (a1 == null) + { + a1 = new AssemblyName(_asString); + } + if (a2 == null) + { + a2 = new AssemblyName(that._asString); + } + + int baselineResult = String.Compare(a1.Name, a2.Name, StringComparison.OrdinalIgnoreCase); + ErrorUtilities.VerifyThrow(result == baselineResult, "Optimized version of CompareBaseNameTo didn't return the same result as the baseline."); +#endif + return result; + } + + /// + /// An implementation of compare that compares two base + /// names as quickly as possible. + /// + /// + /// + private int CompareBaseNameToImpl(AssemblyNameExtension that) + { + // Pointer compare, if identical then base names are + // equal. + if (this == that) + { + return 0; + } + // Do both have assembly names? + if (_asAssemblyName != null && that._asAssemblyName != null) + { + // Pointer compare. + if (_asAssemblyName == that._asAssemblyName) + { + return 0; + } + + // Base name compare. + return String.Compare(_asAssemblyName.Name, that._asAssemblyName.Name, StringComparison.OrdinalIgnoreCase); + } + + // Do both have strings? + if (_asString != null && that._asString != null) + { + // If we have two random-case strings, then we need to compare case sensitively. + return CompareBaseNamesStringWise(_asString, that._asString); + } + + // Fall back to comparing by name. This is the slow path. + return String.Compare(this.Name, that.Name, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compare two basenames. + /// + /// + /// + /// + private static int CompareBaseNamesStringWise(string asString1, string asString2) + { + // Identical strings just match. + if (asString1 == asString2) + { + return 0; + } + + // Get the lengths of base names to compare. + int baseLenThis = asString1.IndexOf(','); + int baseLenThat = asString2.IndexOf(','); + if (baseLenThis == -1) + { + baseLenThis = asString1.Length; + } + if (baseLenThat == -1) + { + baseLenThat = asString2.Length; + } + + // If the lengths are the same then we can compare without copying. + if (baseLenThis == baseLenThat) + { + return String.Compare(asString1, 0, asString2, 0, baseLenThis, StringComparison.OrdinalIgnoreCase); + } + + // Lengths are different, so string copy is required. + string nameThis = asString1.Substring(0, baseLenThis); + string nameThat = asString2.Substring(0, baseLenThat); + return String.Compare(nameThis, nameThat, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Clone this assemblyNameExtension + /// + internal AssemblyNameExtension Clone() + { + AssemblyNameExtension newExtension = new AssemblyNameExtension(); + + if (_asAssemblyName != null) + { + newExtension._asAssemblyName = (AssemblyName)_asAssemblyName.Clone(); + } + + newExtension._asString = _asString; + newExtension._isSimpleName = _isSimpleName; + newExtension._hasProcessorArchitectureInFusionName = _hasProcessorArchitectureInFusionName; + newExtension._remappedFrom = _remappedFrom; + + // We are cloning so we can now party on the object even if the parent was immutable + newExtension._immutable = false; + + return newExtension; + } + + /// + /// Clone the object but mark and mark the cloned object as immutable + /// + /// + internal AssemblyNameExtension CloneImmutable() + { + AssemblyNameExtension clonedExtension = Clone(); + clonedExtension.MarkImmutable(); + return clonedExtension; + } + + /// + /// Is this object immutable + /// + public bool Immutable + { + get + { + return _immutable; + } + } + + /// + /// Mark this object as immutable + /// + internal void MarkImmutable() + { + _immutable = true; + } + + /// + /// Compare two assembly names for equality. + /// + /// + /// + internal bool Equals(AssemblyNameExtension that) + { + return EqualsImpl(that, false, false); + } + + /// + /// Compare two assembly names for equality ignoring version. + /// + /// + /// + internal bool EqualsIgnoreVersion(AssemblyNameExtension that) + { + return EqualsImpl(that, true, false); + } + + /// + /// Compare two assembly names and consider the retargetable flag during the comparison + /// + internal bool Equals(AssemblyNameExtension that, bool considerRetargetableFlag) + { + return EqualsImpl(that, false, considerRetargetableFlag); + } + + /// + /// Compare two assembly names for equality. + /// + /// + /// + private bool EqualsImpl(AssemblyNameExtension that, bool ignoreVersion, bool considerRetargetableFlag) + { + // Pointer compare. + if (Object.ReferenceEquals(this, that)) + { + return true; + } + + // If that is null then this and that are not equal. Also, this would cause a crash on the next line. + if (Object.ReferenceEquals(that, null)) + { + return false; + } + + // Do both have assembly names? + if (_asAssemblyName != null && that._asAssemblyName != null) + { + // Pointer compare. + if (Object.ReferenceEquals(_asAssemblyName, that._asAssemblyName)) + { + return true; + } + } + + // Do both have strings that equal each-other? + if (_asString != null && that._asString != null) + { + if (_asString == that._asString) + { + return true; + } + + // If they weren't identical then they might still differ only by + // case. So we can't assume that they don't match. So fall through... + } + + // Do the names match? + if (0 != String.Compare(Name, that.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!ignoreVersion && (this.Version != that.Version)) + { + return false; + } + + if (!CompareCulture(that)) + { + return false; + } + + if (!ComparePublicKeyToken(that)) + { + return false; + } + + if (considerRetargetableFlag && this.Retargetable != that.Retargetable) + { + return false; + } + + return true; + } + + /// + /// Allows the comparison of the culture. + /// + internal bool CompareCulture(AssemblyNameExtension that) + { + // Do the Cultures match? + CultureInfo aCulture = CultureInfo; + CultureInfo bCulture = that.CultureInfo; + if (aCulture == null) + { + aCulture = CultureInfo.InvariantCulture; + } + if (bCulture == null) + { + bCulture = CultureInfo.InvariantCulture; + } + if (aCulture.LCID != bCulture.LCID) + { + return false; + } + + return true; + } + + /// + /// Allows the comparison of just the PublicKeyToken + /// + internal bool ComparePublicKeyToken(AssemblyNameExtension that) + { + // Do the PKTs match? + byte[] aPKT = GetPublicKeyToken(); + byte[] bPKT = that.GetPublicKeyToken(); + return ComparePublicKeyTokens(aPKT, bPKT); + } + + /// + /// Compare two public key tokens. + /// + private static bool ComparePublicKeyTokens(byte[] aPKT, byte[] bPKT) + { + // Some assemblies (real case was interop assembly) may have null PKTs. + if (aPKT == null) + { + aPKT = new byte[0]; + } + if (bPKT == null) + { + bPKT = new byte[0]; + } + + if (aPKT.Length != bPKT.Length) + { + return false; + } + for (int i = 0; i < aPKT.Length; ++i) + { + if (aPKT[i] != bPKT[i]) + { + return false; + } + } + return true; + } + + /// + /// Only the unnamed assembly has both null assemblyname and null string. + /// + /// + internal bool IsUnnamedAssembly + { + get + { + return _asAssemblyName == null && _asString == null; + } + } + + /// + /// Given a display name, construct an assembly name. + /// + /// The display name. + /// The assembly name. + private static AssemblyName GetAssemblyNameFromDisplayName(string displayName) + { + AssemblyName assemblyName = new AssemblyName(displayName); + return assemblyName; + } + + /// + /// Return a string that has AssemblyName special characters escaped. + /// Those characters are Equals(=), Comma(,), Quote("), Apostrophe('), Backslash(\). + /// + /// + /// WARNING! This method is not meant as a general purpose escaping method for assembly names. + /// Use only if you really know that this does what you need. + /// + /// + /// + internal static string EscapeDisplayNameCharacters(string displayName) + { + StringBuilder sb = new StringBuilder(displayName); + sb = sb.Replace("\\", "\\\\"); + sb = sb.Replace("=", "\\="); + sb = sb.Replace(",", "\\,"); + sb = sb.Replace("\"", "\\\""); + sb = sb.Replace("'", "\\'"); + return sb.ToString(); + } + + /// + /// Convert to a string for display. + /// + /// + override public string ToString() + { + CreateFullName(); + return _asString; + } + + /// + /// Compare the fields of this with that if they are not null. + /// + internal bool PartialNameCompare(AssemblyNameExtension that) + { + return PartialNameCompare(that, PartialComparisonFlags.Default, false /* do not consider retargetable flag*/); + } + + /// + /// Compare the fields of this with that if they are not null. + /// + internal bool PartialNameCompare(AssemblyNameExtension that, bool considerRetargetableFlag) + { + return PartialNameCompare(that, PartialComparisonFlags.Default, considerRetargetableFlag); + } + + /// + /// Do a partial comparison between two assembly name extensions. + /// Compare the fields of A and B on the following conditions: + /// 1) A.Field has a non null value + /// 2) The field has been selected in the comparison flags or the default comparison flags are passed in. + /// + /// If A.Field is null then we will not compare A.Field and B.Field even when the comparison flag is set for that field unless skipNullFields is false. + /// + internal bool PartialNameCompare(AssemblyNameExtension that, PartialComparisonFlags comparisonFlags) + { + return PartialNameCompare(that, comparisonFlags, false /* do not consider retargetable flag*/); + } + + /// + /// Do a partial comparison between two assembly name extensions. + /// Compare the fields of A and B on the following conditions: + /// 1) A.Field has a non null value + /// 2) The field has been selected in the comparison flags or the default comparison flags are passed in. + /// + /// If A.Field is null then we will not compare A.Field and B.Field even when the comparison flag is set for that field unless skipNullFields is false. + /// + internal bool PartialNameCompare(AssemblyNameExtension that, PartialComparisonFlags comparisonFlags, bool considerRetargetableFlag) + { + // Pointer compare. + if (Object.ReferenceEquals(this, that)) + { + return true; + } + + // If that is null then this and that are not equal. Also, this would cause a crash on the next line. + if (Object.ReferenceEquals(that, null)) + { + return false; + } + + // Do both have assembly names? + if (_asAssemblyName != null && that._asAssemblyName != null) + { + // Pointer compare. + if (Object.ReferenceEquals(_asAssemblyName, that._asAssemblyName)) + { + return true; + } + } + + // Do the names match? + if ((comparisonFlags & PartialComparisonFlags.SimpleName) != 0 && Name != null && !String.Equals(Name, that.Name, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if ((comparisonFlags & PartialComparisonFlags.Version) != 0 && Version != null && this.Version != that.Version) + { + return false; + } + + if ((comparisonFlags & PartialComparisonFlags.Culture) != 0 && CultureInfo != null && (that.CultureInfo == null || !CompareCulture(that))) + { + return false; + } + + if ((comparisonFlags & PartialComparisonFlags.PublicKeyToken) != 0 && GetPublicKeyToken() != null && !ComparePublicKeyToken(that)) + { + return false; + } + + if (considerRetargetableFlag && (Retargetable != that.Retargetable)) + { + return false; + } + return true; + } + } +} diff --git a/src/Shared/AssemblyNameReverseVersionComparer.cs b/src/Shared/AssemblyNameReverseVersionComparer.cs new file mode 100644 index 00000000000..3c1043219da --- /dev/null +++ b/src/Shared/AssemblyNameReverseVersionComparer.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Compare the version numbers only for an AssemblyNameExtension and make sure they are in reverse order. This assumes the names are the same. +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.Shared +{ + /// + /// Compare the version numbers only for an AssemblyNameExtension and make sure they are in reverse order. This assumes the names are the same. + /// + sealed internal class AssemblyNameReverseVersionComparer : IComparer + { + /// + /// A static instance of the comparer for use in a sort method + /// + internal readonly static IComparer GenericComparer = new AssemblyNameReverseVersionComparer(); + + /// + /// Compare x and y by version only. + /// + /// Change the return value to sort the values in reverse order. + /// + /// If x is greater than y return -1 indicating x is less than y. + /// If x is less than y return 1 indicating x is greater than y. + /// If x and y are equal return 0. + /// + public int Compare(AssemblyNameExtension x, AssemblyNameExtension y) + { + if (x != null || y != null) + { + if (y == null) + { + // y should be lower than x in the sort. We need to indicate x is less than y in this case. + return -1; + } + else if (x == null) + { + // y should be higher than x in the sort. We need to indicate x is greater than y in this case.. + return 1; + } + } + else + { + // They are both null + return 0; + } + + // We would like to compare the version numerically rather than alphabetically (because for example version 10.0.0. should above 9 not between 1 and 2) + if (x.Version != y.Version) + { + if (y.Version == null) + { + // y should be lower than x in the sort. We need to indicate x is less than y in this case. + return -1; + } + else if (x.Version == null) + { + // y should be higher than x in the sort. We need to indicate x is greater than y in this case.. + return 1; + } + else + { + // Will not return 0 as the this != that check above takes care of the case where they are equal. + // If x is greater than y we want it to return -1, if x is less than y we want 1 to be returned. + int result = y.Version.CompareTo(x.Version); + return result; + } + } + + return 0; + } + } +} \ No newline at end of file diff --git a/src/Shared/AwaitExtensions.cs b/src/Shared/AwaitExtensions.cs new file mode 100644 index 00000000000..4d7ecbbf7f5 --- /dev/null +++ b/src/Shared/AwaitExtensions.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper methods for dealing with 'await'able objects. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Build.Shared +{ + /// + /// Class defining extension methods for awaitable objects. + /// + internal static class AwaitExtensions + { + /// + /// Synchronizes access to the staScheduler field. + /// + private static Object s_staSchedulerSync = new Object(); + + /// + /// The singleton STA scheduler object. + /// + private static TaskScheduler s_staScheduler; + + /// + /// Gets the STA scheduler. + /// + internal static TaskScheduler OneSTAThreadPerTaskSchedulerInstance + { + get + { + if (s_staScheduler == null) + { + lock (s_staSchedulerSync) + { + if (s_staScheduler == null) + { + s_staScheduler = new OneSTAThreadPerTaskScheduler(); + } + } + } + + return s_staScheduler; + } + } + + /// + /// Provides await functionality for ordinary s. + /// + /// The handle to wait on. + /// The awaiter. + internal static TaskAwaiter GetAwaiter(this WaitHandle handle) + { + ErrorUtilities.VerifyThrowArgumentNull(handle, "handle"); + return handle.ToTask().GetAwaiter(); + } + + /// + /// Provides await functionality for an array of ordinary s. + /// + /// The handles to wait on. + /// The awaiter. + internal static TaskAwaiter GetAwaiter(this WaitHandle[] handles) + { + ErrorUtilities.VerifyThrowArgumentNull(handles, "handle"); + return handles.ToTask().GetAwaiter(); + } + + /// + /// Creates a TPL Task that is marked as completed when a is signaled. + /// + /// The handle whose signal triggers the task to be completed. Do not use a here. + /// The timeout (in milliseconds) after which the task will fault with a if the handle is not signaled by that time. + /// A Task that is completed after the handle is signaled. + /// + /// There is a (brief) time delay between when the handle is signaled and when the task is marked as completed. + /// + internal static Task ToTask(this WaitHandle handle, int timeout = Timeout.Infinite) + { + return ToTask(new WaitHandle[1] { handle }, timeout); + } + + /// + /// Creates a TPL Task that is marked as completed when any in the array is signaled. + /// + /// The handles whose signals triggers the task to be completed. Do not use a here. + /// The timeout (in milliseconds) after which the task will return a value of WaitTimeout. + /// A Task that is completed after any handle is signaled. + /// + /// There is a (brief) time delay between when the handles are signaled and when the task is marked as completed. + /// + internal static Task ToTask(this WaitHandle[] handles, int timeout = Timeout.Infinite) + { + ErrorUtilities.VerifyThrowArgumentNull(handles, "handle"); + + var tcs = new TaskCompletionSource(); + int signalledHandle = WaitHandle.WaitAny(handles, 0); + if (signalledHandle != WaitHandle.WaitTimeout) + { + // An optimization for if the handle is already signaled + // to return a completed task. + tcs.SetResult(signalledHandle); + } + else + { + var localVariableInitLock = new object(); + var culture = CultureInfo.CurrentCulture; + var uiCulture = CultureInfo.CurrentUICulture; + lock (localVariableInitLock) + { + RegisteredWaitHandle[] callbackHandles = new RegisteredWaitHandle[handles.Length]; + for (int i = 0; i < handles.Length; i++) + { + callbackHandles[i] = ThreadPool.RegisterWaitForSingleObject( + handles[i], + (state, timedOut) => + { + int handleIndex = (int)state; + if (timedOut) + { + tcs.TrySetResult(WaitHandle.WaitTimeout); + } + else + { + tcs.TrySetResult(handleIndex); + } + + // We take a lock here to make sure the outer method has completed setting the local variable callbackHandles contents. + lock (localVariableInitLock) + { + foreach (var handle in callbackHandles) + { + handle.Unregister(null); + } + } + }, + state: i, + millisecondsTimeOutInterval: timeout, + executeOnlyOnce: true); + } + } + } + + return tcs.Task; + } + + /// + /// A class which acts as a task scheduler and ensures each scheduled task gets its + /// own STA thread. + /// + private class OneSTAThreadPerTaskScheduler : TaskScheduler + { + /// + /// The current queue of tasks. + /// + private ConcurrentQueue _queuedTasks = new ConcurrentQueue(); + + /// + /// Returns the list of queued tasks. + /// + protected override System.Collections.Generic.IEnumerable GetScheduledTasks() + { + return _queuedTasks; + } + + /// + /// Queues a task to the scheduler. + /// + protected override void QueueTask(Task task) + { + _queuedTasks.Enqueue(task); + + ParameterizedThreadStart threadStart = new ParameterizedThreadStart((_) => + { + Task t; + if (_queuedTasks.TryDequeue(out t)) + { + base.TryExecuteTask(t); + } + }); + + Thread thread = new Thread(threadStart); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(task); + } + + /// + /// Tries to execute the task immediately. This method will always return false for the STA scheduler. + /// + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // We don't get STA threads back here, so just deny the inline execution. + return false; + } + } + } +} diff --git a/src/Shared/BuildEventFileInfo.cs b/src/Shared/BuildEventFileInfo.cs new file mode 100644 index 00000000000..d31ea94965f --- /dev/null +++ b/src/Shared/BuildEventFileInfo.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Xml.Schema; + +namespace Microsoft.Build.Shared +{ + /// + /// This class encapsulates information about a file that is associated with a build event. + /// + internal sealed class BuildEventFileInfo + { + #region Constructors + + /// + /// Private default constructor disallows parameterless instantiation. + /// + private BuildEventFileInfo() + { + // do nothing + } + + /// + /// Creates an instance of this class using the given filename/path. + /// Filename may be an empty string, if there is truly no file associated. + /// This overload may also be used if there is a file but truly no line/column, + /// for example when failing to load a project file. + /// + /// IF AN IELEMENTLOCATION IS AVAILABLE, USE THE OVERLOAD ACCEPTING THAT INSTEAD. + /// + /// + internal BuildEventFileInfo(string file) + : this(file, 0, 0, 0, 0) + { + // do nothing + } + + /// + /// Creates an instance of this class using the given location. + /// This does not provide end-line or end-column information. + /// This is the preferred overload. + /// + internal BuildEventFileInfo(IElementLocation location) + : this(location.File, location.Line, location.Column) + { + // do nothing + } + + /// + /// Creates an instance of this class using the given filename/path and a line/column of interest in the file. + /// + /// IF AN IELEMENTLOCATION IS AVAILABLE, USE THE OVERLOAD ACCEPTING THAT INSTEAD. + /// + /// + /// Set to zero if not available. + /// Set to zero if not available. + internal BuildEventFileInfo(string file, int line, int column) + : this(file, line, column, 0, 0) + { + // do nothing + } + + /// + /// Creates an instance of this class using the given filename/path and a range of lines/columns of interest in the file. + /// + /// IF AN IELEMENTLOCATION IS AVAILABLE, USE THE OVERLOAD ACCEPTING THAT INSTEAD. + /// + /// + /// Set to zero if not available. + /// Set to zero if not available. + /// Set to zero if not available. + /// Set to zero if not available. + internal BuildEventFileInfo(string file, int line, int column, int endLine, int endColumn) + { + // Projects that don't have a filename when the are built should use an empty string instead. + _file = (file == null) ? String.Empty : file; + _line = line; + _column = column; + _endLine = endLine; + _endColumn = endColumn; + } + + /// + /// Creates an instance of this class using the information in the given XmlException. + /// + /// + internal BuildEventFileInfo(XmlException e) + { + ErrorUtilities.VerifyThrow(e != null, "Need exception context."); + + _file = (e.SourceUri.Length == 0) ? String.Empty : new Uri(e.SourceUri).LocalPath; + _line = e.LineNumber; + _column = e.LinePosition; + _endLine = 0; + _endColumn = 0; + } + + #endregion + + #region Properties + + /// + /// Gets the filename/path to be associated with some build event. + /// + /// The filename/path string. + internal string File + { + get + { + return _file; + } + } + + /// + /// Gets the line number of interest in the file. + /// + /// Line number, or zero if not available. + internal int Line + { + get + { + return _line; + } + } + + /// + /// Gets the column number of interest in the file. + /// + /// Column number, or zero if not available. + internal int Column + { + get + { + return _column; + } + } + + /// + /// Gets the last line number of a range of interesting lines in the file. + /// + /// Last line number, or zero if not available. + internal int EndLine + { + get + { + return _endLine; + } + } + + /// + /// Gets the last column number of a range of interesting columns in the file. + /// + /// Last column number, or zero if not available. + internal int EndColumn + { + get + { + return _endColumn; + } + } + + #endregion + + // the filename/path + private string _file; + // the line number of interest in the file + private int _line; + // the column number of interest in the file + private int _column; + // the last line in a range of interesting lines in the file + private int _endLine; + // the last column in a range of interesting columns in the file + private int _endColumn; + } +} diff --git a/src/Shared/CanonicalError.cs b/src/Shared/CanonicalError.cs new file mode 100644 index 00000000000..18a948f830b --- /dev/null +++ b/src/Shared/CanonicalError.cs @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Shared +{ + /// + /// Functions for dealing with the specially formatted errors returned by + /// build tools. + /// + /// + /// Various tools produce and consume CanonicalErrors in various formats. + /// + /// DEVENV Format When Clicking on Items in the Output Window + /// (taken from env\msenv\core\findutil.cpp ParseLocation function) + /// + /// v:\dir\file.ext (loc) : msg + /// \\server\share\dir\file.ext(loc):msg + /// url + /// + /// loc: + /// (line) + /// (line-line) + /// (line,col) + /// (line,col-col) + /// (line,col,len) + /// (line,col,line,col) + /// + /// DevDiv Build Process + /// (taken from tools\devdiv2.def) + /// + /// To echo warnings and errors to the build console, the + /// "description block" must be recognized by build. To do this, + /// add a $(ECHO_COMPILING_COMMAND) or $(ECHO_PROCESSING_COMMAND) + /// to the first line of the description block, e.g. + /// + /// $(ECHO_COMPILING_CMD) Resgen_$< + /// + /// Errors must have the format: + /// + /// <text> : error [num]: <msg> + /// + /// Warnings must have the format: + /// + /// <text> : warning [num]: <msg> + /// + internal static class CanonicalError + { + // Defines the main pattern for matching messages. + static private Regex s_originCategoryCodeTextExpression = new Regex + ( + // Beginning of line and any amount of whitespace. + @"^\s*" + // Match a [optional project number prefix 'ddd>'], single letter + colon + remaining filename, or + // string with no colon followed by a colon. + + @"(((?(((\d+>)?[a-zA-Z]?:[^:]*)|([^:]*))):)" + // Origin may also be empty. In this case there's no trailing colon. + + "|())" + // Match the empty string or a string without a colon that ends with a space + + "(?(()|([^:]*? )))" + // Match 'error' or 'warning'. + + @"(?(error|warning))" + // Match anything starting with a space that's not a colon/space, followed by a colon. + // Error code is optional in which case "error"/"warning" can be followed immediately by a colon. + + @"( \s*(?[^: ]*))?\s*:" + // Whatever's left on this line, including colons. + + "(?.*)$", + RegexOptions.IgnoreCase + ); + + // Matches and extracts filename and location from an 'origin' element. + static private Regex s_filenameLocationFromOrigin = new Regex + ( + "^" // Beginning of line + + @"(\d+>)?" // Optional ddd> project number prefix + + "(?.*)" // Match anything. + + @"\(" // Find a parenthesis. + + @"(?[\,,0-9,-]*)" // Match any combination of numbers and ',' and '-' + + @"\)\s*" // Find the closing paren then any amount of spaces. + + "$", // End-of-line + RegexOptions.IgnoreCase + ); + + // Matches location that is a simple number. + static private Regex s_lineFromLocation = new Regex // Example: line + ( + "^" // Beginning of line + + "(?[0-9]*)" // Match any number. + + "$", // End-of-line + RegexOptions.IgnoreCase + ); + + // Matches location that is a range of lines. + static private Regex s_lineLineFromLocation = new Regex // Example: line-line + ( + "^" // Beginning of line + + "(?[0-9]*)" // Match any number. + + "-" // Dash + + "(?[0-9]*)" // Match any number. + + "$", // End-of-line + RegexOptions.IgnoreCase + ); + + // Matches location that is a line and column + static private Regex s_lineColFromLocation = new Regex // Example: line,col + ( + "^" // Beginning of line + + "(?[0-9]*)" // Match any number. + + "," // Comma + + "(?[0-9]*)" // Match any number. + + "$", // End-of-line + RegexOptions.IgnoreCase + ); + + // Matches location that is a line and column-range + static private Regex s_lineColColFromLocation = new Regex // Example: line,col-col + ( + "^" // Beginning of line + + "(?[0-9]*)" // Match any number. + + "," // Comma + + "(?[0-9]*)" // Match any number. + + "-" // Dash + + "(?[0-9]*)" // Match any number. + + "$", // End-of-line + RegexOptions.IgnoreCase + ); + + // Matches location that is line,col,line,col + static private Regex s_lineColLineColFromLocation = new Regex // Example: line,col,line,col + ( + "^" // Beginning of line + + "(?[0-9]*)" // Match any number. + + "," // Comma + + "(?[0-9]*)" // Match any number. + + "," // Dash + + "(?[0-9]*)" // Match any number. + + "," // Dash + + "(?[0-9]*)" // Match any number. + + "$", // End-of-line + RegexOptions.IgnoreCase + ); + + /// + /// Represents the parts of a decomposed canonical message. + /// + internal sealed class Parts + { + /// + /// Defines the error category\severity level. + /// + internal enum Category + { + Warning, + Error + } + + /// + /// Value used for unspecified line and column numbers, which are 1-relative. + /// + internal const int numberNotSpecified = 0; + + /// + /// Initializes a new instance of the class. + /// + internal Parts() + { + } + + /// + /// Name of the file or tool (not localized) + /// + internal string origin; + + /// + /// The line number. + /// + internal int line = Parts.numberNotSpecified; + + /// + /// The column number. + /// + internal int column = Parts.numberNotSpecified; + + /// + /// The ending line number. + /// + internal int endLine = Parts.numberNotSpecified; + + /// + /// The ending column number. + /// + internal int endColumn = Parts.numberNotSpecified; + + /// + /// The category/severity level + /// + internal Category category; + + /// + /// The sub category (localized) + /// + internal string subcategory; + + /// + /// The error code (not localized) + /// + internal string code; + + /// + /// The error message text (localized) + /// + internal string text; + +#if NEVER + internal new string ToString() + { + return String.Format + ( + "Origin='{0}'\n" + +"Filename='{1}'\n" + +"Line='{2}'\n" + +"Column='{3}'\n" + +"EndLine='{4}'\n" + +"EndColumn='{5}'\n" + +"Category='{6}'\n" + +"Subcategory='{7}'\n" + +"Text='{8}'\n" + , origin, line, column, endLine, endColumn, category.ToString(), subcategory, code, text + ); + + } +#endif + } + + /// + /// A small custom int conversion method that treats invalid entries as missing (0). This is done to work around tools + /// that don't fully conform to the canonical message format - we still want to salvage what we can from the message. + /// + /// + /// 'value' converted to int or 0 if it can't be parsed or is negative + private static int ConvertToIntWithDefault(string value) + { + int result; + bool success = int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + + if (!success || (result < 0)) + { + result = CanonicalError.Parts.numberNotSpecified; + } + + return result; + } + + /// + /// Decompose an error or warning message into constituent parts. If the message isn't in the canonical form, return null. + /// + /// This method is thread-safe, because the Regex class is thread-safe (per MSDN). + /// + /// Decomposed canonical message, or null. + internal static Parts Parse(string message) + { + // An unusually long string causes pathologically slow Regex back-tracking. + // To avoid that, only scan the first 400 characters. That's enough for + // the longest possible prefix: MAX_PATH, plus a huge subcategory string, and an error location. + // After the regex is done, we can append the overflow. + string messageOverflow = String.Empty; + if (message.Length > 400) + { + messageOverflow = message.Substring(400); + message = message.Substring(0, 400); + } + + // If a tool has a large amount of output that isn't an error or warning (eg., "dir /s %hugetree%") + // the regex below is slow. It's faster to pre-scan for "warning" and "error" + // and bail out if neither are present. + if (message.IndexOf("warning", StringComparison.OrdinalIgnoreCase) == -1 && + message.IndexOf("error", StringComparison.OrdinalIgnoreCase) == -1) + { + return null; + } + + Parts parsedMessage = new Parts(); + + // First, split the message into three parts--Origin, Category, Code, Text. + // Example, + // Main.cs(17,20):Command line warning CS0168: The variable 'foo' is declared but never used + // -------------- ------------ ------- ------ ---------------------------------------------- + // Origin SubCategory Cat. Code Text + // + // To accomodate absolute filenames in Origin, tolerate a colon in the second position + // as long as its preceded by a letter. + // + // Localization Note: + // Even in foreign-language versions of tools, the category field needs to be in English. + // Also, if origin is a tool name, then that needs to be in English. + // + // Here's an example from the Japanese version of CL.EXE: + // cl : ???? ??? warning D4024 : ?????????? 'AssemblyInfo.cs' ?????????????????? ??????????? + // + // Here's an example from the Japanese version of LINK.EXE: + // AssemblyInfo.cpp : fatal error LNK1106: ???????????? ??????????????: 0x6580 ?????????? + // + Match match = s_originCategoryCodeTextExpression.Match(message); + + if (!match.Success) + { + // If no match here, then this message is not an error or warning. + return null; + } + + string origin = match.Groups["ORIGIN"].Value.Trim(); + string category = match.Groups["CATEGORY"].Value.Trim(); + parsedMessage.code = match.Groups["CODE"].Value.Trim(); + parsedMessage.text = (match.Groups["TEXT"].Value + messageOverflow).Trim(); + parsedMessage.subcategory = match.Groups["SUBCATEGORY"].Value.Trim(); + + // Next, see if category is something that is recognized. + if (0 == String.Compare(category, "error", StringComparison.OrdinalIgnoreCase)) + { + parsedMessage.category = Parts.Category.Error; + } + else if (0 == String.Compare(category, "warning", StringComparison.OrdinalIgnoreCase)) + { + parsedMessage.category = Parts.Category.Warning; + } + else + { + // Not an error\warning message. + return null; + } + + // Origin is not a simple file, but it still could be of the form, + // foo.cpp(location) + match = s_filenameLocationFromOrigin.Match(origin); + + if (match.Success) + { + // The origin is in the form, + // foo.cpp(location) + // Assume the filename exists, but don't verify it. What else could it be? + string location = match.Groups["LOCATION"].Value.Trim(); + parsedMessage.origin = match.Groups["FILENAME"].Value.Trim(); + + // Now, take apart the location. It can be one of these: + // loc: + // (line) + // (line-line) + // (line,col) + // (line,col-col) + // (line,col,len) + // (line,col,line,col) + if (location.Length > 0) + { + match = s_lineFromLocation.Match(location); + if (match.Success) + { + parsedMessage.line = ConvertToIntWithDefault(match.Groups["LINE"].Value.Trim()); + } + else + { + match = s_lineLineFromLocation.Match(location); + if (match.Success) + { + parsedMessage.line = ConvertToIntWithDefault(match.Groups["LINE"].Value.Trim()); + parsedMessage.endLine = ConvertToIntWithDefault(match.Groups["ENDLINE"].Value.Trim()); + } + else + { + match = s_lineColFromLocation.Match(location); + if (match.Success) + { + parsedMessage.line = ConvertToIntWithDefault(match.Groups["LINE"].Value.Trim()); + parsedMessage.column = ConvertToIntWithDefault(match.Groups["COLUMN"].Value.Trim()); + } + else + { + match = s_lineColColFromLocation.Match(location); + if (match.Success) + { + parsedMessage.line = ConvertToIntWithDefault(match.Groups["LINE"].Value.Trim()); + parsedMessage.column = ConvertToIntWithDefault(match.Groups["COLUMN"].Value.Trim()); + parsedMessage.endColumn = ConvertToIntWithDefault(match.Groups["ENDCOLUMN"].Value.Trim()); + } + else + { + match = s_lineColLineColFromLocation.Match(location); + if (match.Success) + { + parsedMessage.line = ConvertToIntWithDefault(match.Groups["LINE"].Value.Trim()); + parsedMessage.column = ConvertToIntWithDefault(match.Groups["COLUMN"].Value.Trim()); + parsedMessage.endLine = ConvertToIntWithDefault(match.Groups["ENDLINE"].Value.Trim()); + parsedMessage.endColumn = ConvertToIntWithDefault(match.Groups["ENDCOLUMN"].Value.Trim()); + } + } + } + } + } + } + } + else + { + // The origin does not fit the filename(location) pattern. + parsedMessage.origin = origin; + } + + return parsedMessage; + } + } +} diff --git a/src/Shared/CollectionHelpers.cs b/src/Shared/CollectionHelpers.cs new file mode 100644 index 00000000000..891e3925c4e --- /dev/null +++ b/src/Shared/CollectionHelpers.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Utilities for collections +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Shared +{ + /// + /// Utilities for collections + /// + internal static class CollectionHelpers + { + /// + /// Returns a new list containing the input list + /// contents, except for nulls + /// + /// Type of list elements + internal static List RemoveNulls(List inputs) + { + List inputsWithoutNulls = new List(inputs.Count); + + foreach (T entry in inputs) + { + if (entry != null) + { + inputsWithoutNulls.Add(entry); + } + } + + // Avoid possibly having two identical lists floating around + return (inputsWithoutNulls.Count == inputs.Count) ? inputs : inputsWithoutNulls; + } + + /// + /// Extension method -- combines a TryGet with a check to see that the value is equal. + /// + internal static bool ContainsValueAndIsEqual(this Dictionary dictionary, string key, string value, StringComparison comparer) + { + string valueFromDictionary = null; + if (dictionary.TryGetValue(key, out valueFromDictionary)) + { + return String.Equals(value, valueFromDictionary, comparer); + } + + return false; + } + } +} diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs new file mode 100644 index 00000000000..7982232165e --- /dev/null +++ b/src/Shared/CommunicationsUtilities.cs @@ -0,0 +1,654 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Shared utility methods primarily relating to communication +// between nodes. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Pipes; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Globalization; +using System.Xml.Serialization; +using System.Security; +using System.Security.Policy; +using System.Security.Permissions; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Internal +{ + /// + /// Enumeration of all possible (currently supported) types of task host context. + /// + internal enum TaskHostContext + { + /// + /// 32-bit Intel process, using the 2.0 CLR. + /// + X32CLR2, + + /// + /// 64-bit Intel process, using the 2.0 CLR. + /// + X64CLR2, + + /// + /// 32-bit Intel process, using the 4.0 CLR. + /// + X32CLR4, + + /// + /// 64-bit Intel process, using the 4.0 CLR. + /// + X64CLR4, + + /// + /// Invalid task host context + /// + Invalid + } + + /// + /// This class contains utility methods for the MSBuild engine. + /// + static internal class CommunicationsUtilities + { + /// + /// The timeout to connect to a node. + /// + private const int DefaultNodeConnectionTimeout = 900 * 1000; // 15 minutes; enough time that a dev will typically do another build in this time + + /// + /// Flag if we have already calculated the FileVersion hashcode + /// + private static bool s_fileVersionChecked; + + /// + /// A hashcode calculated from the fileversion + /// + private static int s_fileVersionHash; + + /// + /// Whether to trace communications + /// + private static bool s_trace = String.Equals(Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM"), "1", StringComparison.Ordinal); + + /// + /// Place to dump trace + /// + private static string s_debugDumpPath; + + /// + /// Ticks at last time logged + /// + private static long s_lastLoggedTicks = DateTime.UtcNow.Ticks; + + /// + /// Delegate to debug the communication utilities. + /// + internal delegate void LogDebugCommunications(string format, params object[] stuff); + + /// + /// Gets or sets the node connection timeout. + /// + static internal int NodeConnectionTimeout + { + get { return GetIntegerVariableOrDefault("MSBUILDNODECONNECTIONTIMEOUT", DefaultNodeConnectionTimeout); } + } + + /// + /// Looks up the file version and caches the hashcode + /// This file version hashcode is used in calculating the handshake + /// + private static int FileVersionHash + { + get + { + if (!s_fileVersionChecked) + { + // We only hash in any complus_installroot value, not a file version. + // This is because in general msbuildtaskhost.exe does not load any assembly that + // the parent process loads, so they can't compare the version of a particular assembly. + // They can't compare their own versions, because if one of them is serviced, they + // won't match any more. The only known incompatibility is between a razzle and non-razzle + // parent and child. COMPLUS_Version can (and typically will) differ legitimately between + // them, so just check COMPLUS_InstallRoot. + string complusInstallRoot = Environment.GetEnvironmentVariable("COMPLUS_INSTALLROOT"); + + // We should also check the file version when COMPLUS_INSTALLROOT is null, because the protocol can change between releases. + // If we don't check, we'll run into issues + string taskhostexe = FileUtilities.ExecutingAssemblyPath; + string majorVersion = FileVersionInfo.GetVersionInfo(taskhostexe).FileMajorPart.ToString(); + + s_fileVersionHash = GetHandshakeHashCode(complusInstallRoot ?? majorVersion); + s_fileVersionChecked = true; + } + + return s_fileVersionHash; + } + } + + /// + /// Get environment block + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static unsafe extern char* GetEnvironmentStrings(); + + /// + /// Free environment block + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static unsafe extern bool FreeEnvironmentStrings(char* pStrings); + + /// + /// Move a block of chars + /// + [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] + internal static unsafe extern void CopyMemory(char* destination, char* source, uint length); + + /// + /// Retrieve the environment block. + /// Copied from the BCL implementation to eliminate some expensive security asserts. + /// + internal unsafe static char[] GetEnvironmentCharArray() + { + char[] block = null; + char* pStrings = null; + + try + { + pStrings = GetEnvironmentStrings(); + if (pStrings == null) + { + throw new OutOfMemoryException(); + } + + // Format for GetEnvironmentStrings is: + // [=HiddenVar=value\0]* [Variable=value\0]* \0 + // See the description of Environment Blocks in MSDN's + // CreateProcess page (null-terminated array of null-terminated strings). + + // Search for terminating \0\0 (two unicode \0's). + char* p = pStrings; + while (!(*p == '\0' && *(p + 1) == '\0')) + { + p++; + } + + uint chars = (uint)(p - pStrings + 1); + uint bytes = chars * sizeof(char); + + block = new char[chars]; + + fixed (char* pBlock = block) + { + CopyMemory(pBlock, pStrings, bytes); + } + } + finally + { + if (pStrings != null) + { + FreeEnvironmentStrings(pStrings); + } + } + + return block; + } + + /// + /// Copied from the BCL implementation to eliminate some expensive security asserts. + /// Returns key value pairs of environment variables in a new dictionary + /// with a case-insensitive key comparer. + /// + internal static Dictionary GetEnvironmentVariables() + { + char[] block = GetEnvironmentCharArray(); + + Dictionary table = new Dictionary(200, StringComparer.OrdinalIgnoreCase); // Razzle has 150 environment variables + + // Copy strings out, parsing into pairs and inserting into the table. + // The first few environment variable entries start with an '='! + // The current working directory of every drive (except for those drives + // you haven't cd'ed into in your DOS window) are stored in the + // environment block (as =C:=pwd) and the program's exit code is + // as well (=ExitCode=00000000) Skip all that start with =. + // Read docs about Environment Blocks on MSDN's CreateProcess page. + + // Format for GetEnvironmentStrings is: + // (=HiddenVar=value\0 | Variable=value\0)* \0 + // See the description of Environment Blocks in MSDN's + // CreateProcess page (null-terminated array of null-terminated strings). + // Note the =HiddenVar's aren't always at the beginning. + for (int i = 0; i < block.Length; i++) + { + int startKey = i; + + // Skip to key + // On some old OS, the environment block can be corrupted. + // Someline will not have '=', so we need to check for '\0'. + while (block[i] != '=' && block[i] != '\0') + { + i++; + } + + if (block[i] == '\0') + { + continue; + } + + // Skip over environment variables starting with '=' + if (i - startKey == 0) + { + while (block[i] != 0) + { + i++; + } + + continue; + } + + string key = new string(block, startKey, i - startKey); + i++; + + // skip over '=' + int startValue = i; + + while (block[i] != 0) + { + // Read to end of this entry + i++; + } + + string value = new string(block, startValue, i - startValue); + + // skip over 0 handled by for loop's i++ + table[key] = value; + } + + return table; + } + + /// + /// Updates the environment to match the provided dictionary. + /// + internal static void SetEnvironment(IDictionary newEnvironment) + { + if (newEnvironment != null) + { + // First, empty out any new variables + foreach (KeyValuePair entry in CommunicationsUtilities.GetEnvironmentVariables()) + { + if (!newEnvironment.ContainsKey(entry.Key)) + { + Environment.SetEnvironmentVariable(entry.Key, null); + } + } + + // Then, make sure the old ones have their old values. + foreach (KeyValuePair entry in newEnvironment) + { + Environment.SetEnvironmentVariable(entry.Key, entry.Value); + } + } + } + + /// + /// Given a base handshake, generates the real handshake based on e.g. elevation level. + /// Client handshake required for comparison purposes only. Returns the update handshake. + /// + internal static long GenerateHostHandshakeFromBase(long baseHandshake, long clientHandshake) + { + // If we are running in elevated privs, we will only accept a handshake from an elevated process as well. + WindowsPrincipal principal = new WindowsPrincipal(WindowsIdentity.GetCurrent()); + + // Both the client and the host will calculate this separately, and the idea is that if they come out the same + // then we can be sufficiently confident that the other side has the same elevation level as us. This is complementary + // to the username check which is also done on connection. + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + unchecked + { + baseHandshake = baseHandshake ^ 0x5c5c5c5c5c5c5c5c + Process.GetCurrentProcess().SessionId; + } + + if ((baseHandshake & 0x00FFFFFFFFFFFFFF) == clientHandshake) + { + baseHandshake = ~baseHandshake; + } + } + + // Mask out the first byte. That's because old + // builds used a single, non zero initial byte, + // and we don't want to risk communicating with them + return baseHandshake & 0x00FFFFFFFFFFFFFF; + } + + /// + /// Magic number sent by the host to the client during the handshake. + /// Derived from the binary timestamp to avoid mixing binary versions. + /// + internal static long GetTaskHostHostHandshake(TaskHostContext hostContext) + { + long baseHandshake = GenerateHostHandshakeFromBase(GetBaseHandshakeForContext(hostContext), GetTaskHostClientHandshake(hostContext)); + return baseHandshake; + } + + /// + /// Magic number sent by the client to the host during the handshake. + /// Munged version of the host handshake. + /// + internal static long GetTaskHostClientHandshake(TaskHostContext hostContext) + { + // Mask out the first byte. That's because old + // builds used a single, non zero initial byte, + // and we don't want to risk communicating with them + long clientHandshake = ((GetBaseHandshakeForContext(hostContext) ^ Int64.MaxValue) & 0x00FFFFFFFFFFFFFF); + return clientHandshake; + } + + /// + /// Extension method to write a series of bytes to a stream + /// + internal static void WriteLongForHandshake(this PipeStream stream, long value) + { + byte[] bytes = BitConverter.GetBytes(value); + + // We want to read the long and send it from left to right (this means big endian) + // if we are little endian we need to reverse the array to keep the left to right reading + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + ErrorUtilities.VerifyThrow(bytes.Length == 8, "Long should be 8 bytes"); + + stream.Write(bytes, 0, bytes.Length); + } + + /// + /// Extension method to read a series of bytes from a stream + /// + internal static long ReadLongForHandshake(this PipeStream stream) + { + return stream.ReadLongForHandshake((byte[])null, 0); + } + + /// + /// Extension method to read a series of bytes from a stream. + /// If specified, leading byte matches one in the supplied array if any, returns rejection byte and throws IOException. + /// + internal static long ReadLongForHandshake(this PipeStream stream, byte[] leadingBytesToReject, byte rejectionByteToReturn) + { + byte[] bytes = new byte[8]; + + for (int i = 0; i < bytes.Length; i++) + { + int read = stream.ReadByte(); + + if (read == -1) + { + // We've unexpectly reached end of stream. + // We are now in a bad state, disconnect on our end + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Unexpected end of stream while reading for handshake")); + } + + if (i == 0 && leadingBytesToReject != null) + { + foreach (byte reject in leadingBytesToReject) + { + if (read == reject) + { + stream.WriteByte(rejectionByteToReturn); // disconnect the host + + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Client: rejected old host. Received byte {0} but this matched a byte to reject.", bytes[i])); // disconnect and quit + } + } + } + + bytes[i] = Convert.ToByte(read); + } + + long result; + + try + { + // We want to read the long and send it from left to right (this means big endian) + // If we are little endian the stream has already been reversed by the sender, we need to reverse it again to get the original number + if (BitConverter.IsLittleEndian) + { + Array.Reverse(bytes); + } + + result = BitConverter.ToInt64(bytes, 0 /* start index */); + } + catch (ArgumentException ex) + { + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to convert the handshake to big-endian. {0}", ex.Message)); + } + + return result; + } + + /// + /// Given the appropriate information, return the equivalent TaskHostContext. + /// + internal static TaskHostContext GetTaskHostContext(IDictionary taskHostParameters) + { + ErrorUtilities.VerifyThrow(taskHostParameters.ContainsKey(XMakeAttributes.runtime), "Should always have an explicit runtime when we call this method."); + ErrorUtilities.VerifyThrow(taskHostParameters.ContainsKey(XMakeAttributes.architecture), "Should always have an explicit architecture when we call this method."); + + string runtime = taskHostParameters[XMakeAttributes.runtime]; + string architecture = taskHostParameters[XMakeAttributes.architecture]; + + bool is64BitProcess = false; + int clrVersion = 0; + + if (architecture.Equals(XMakeAttributes.MSBuildArchitectureValues.x64, StringComparison.OrdinalIgnoreCase)) + { + is64BitProcess = true; + } + else if (architecture.Equals(XMakeAttributes.MSBuildArchitectureValues.x86, StringComparison.OrdinalIgnoreCase)) + { + is64BitProcess = false; + } + else + { + ErrorUtilities.ThrowInternalError("Should always have an explicit architecture when calling this method"); + } + + if (runtime.Equals(XMakeAttributes.MSBuildRuntimeValues.clr4, StringComparison.OrdinalIgnoreCase)) + { + clrVersion = 4; + } + else if (runtime.Equals(XMakeAttributes.MSBuildRuntimeValues.clr2, StringComparison.OrdinalIgnoreCase)) + { + clrVersion = 2; + } + else + { + ErrorUtilities.ThrowInternalError("Should always have an explicit runtime when calling this method"); + } + + TaskHostContext hostContext = GetTaskHostContext(is64BitProcess, clrVersion); + return hostContext; + } + + /// + /// Given the appropriate information, return the equivalent TaskHostContext. + /// + internal static TaskHostContext GetTaskHostContext(bool is64BitProcess, int clrVersion) + { + TaskHostContext hostContext = TaskHostContext.Invalid; + switch (clrVersion) + { + case 2: + hostContext = is64BitProcess ? TaskHostContext.X64CLR2 : TaskHostContext.X32CLR2; + break; + case 4: + hostContext = is64BitProcess ? TaskHostContext.X64CLR4 : TaskHostContext.X32CLR4; + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + hostContext = TaskHostContext.Invalid; + break; + } + + return hostContext; + } + + /// + /// Returns the TaskHostContext corresponding to this process + /// + internal static TaskHostContext GetCurrentTaskHostContext() + { + // We know that whichever assembly is executing this code -- whether it's MSBuildTaskHost.exe or + // Microsoft.Build.dll -- is of the version of the CLR that this process is running. So grab + // the version of mscorlib currently in use and call that good enough. + Version mscorlibVersion = typeof(bool).Assembly.GetName().Version; + + string currentMSBuildArchitecture = XMakeAttributes.GetCurrentMSBuildArchitecture(); + TaskHostContext hostContext = GetTaskHostContext(currentMSBuildArchitecture.Equals(XMakeAttributes.MSBuildArchitectureValues.x64), mscorlibVersion.Major); + + return hostContext; + } + + /// + /// Gets the value of an integer environment variable, or returns the default if none is set or it cannot be converted. + /// + internal static int GetIntegerVariableOrDefault(string environmentVariable, int defaultValue) + { + string environmentValue = Environment.GetEnvironmentVariable(environmentVariable); + if (String.IsNullOrEmpty(environmentValue)) + { + return defaultValue; + } + + int localDefaultValue; + if (Int32.TryParse(environmentValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out localDefaultValue)) + { + defaultValue = localDefaultValue; + } + + return defaultValue; + } + + /// + /// Writes trace information to a log file + /// + internal static void Trace(string format, params object[] args) + { + Trace(/* nodeId */ -1, format, args); + } + + /// + /// Writes trace information to a log file + /// + internal static void Trace(int nodeId, string format, params object[] args) + { + if (s_trace) + { + if (s_debugDumpPath == null) + { + s_debugDumpPath = Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH"); + + if (String.IsNullOrEmpty(s_debugDumpPath)) + { + s_debugDumpPath = Path.GetTempPath(); + } + else + { + Directory.CreateDirectory(s_debugDumpPath); + } + } + + try + { + string fileName = @"MSBuild_CommTrace_PID_{0}"; + if (nodeId != -1) + { + fileName += "_node_" + nodeId; + } + + fileName += ".txt"; + + using (StreamWriter file = new StreamWriter(String.Format(CultureInfo.CurrentCulture, Path.Combine(s_debugDumpPath, fileName), Process.GetCurrentProcess().Id, nodeId), true)) + { + string message = String.Format(CultureInfo.CurrentCulture, format, args); + long now = DateTime.UtcNow.Ticks; + float millisecondsSinceLastLog = (float)((now - s_lastLoggedTicks) / 10000L); + s_lastLoggedTicks = now; + file.WriteLine("{0} (TID {1}) {2,15} +{3,10}ms: {4}", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId, now, millisecondsSinceLastLog, message); + } + } + catch (IOException) + { + // Ignore + } + } + } + + /// + /// Add the task host context to this handshake, to make sure that task hosts with different contexts + /// will have different handshakes. Shift it into the upper 32-bits to avoid running into the + /// session ID. + /// + /// TaskHostContext + /// Base Handshake + private static long GetBaseHandshakeForContext(TaskHostContext hostContext) + { + long baseHandshake = ((long)hostContext << 40) | ((long)FileVersionHash << 8); + return baseHandshake; + } + + /// + /// Gets a hash code for this string. If strings A and B are such that A.Equals(B), then + /// they will return the same hash code. + /// This is as implemented in CLR String.GetHashCode() [ndp\clr\src\BCL\system\String.cs] + /// but stripped out architecture specific defines + /// that causes the hashcode to be different and this causes problem in cross-architecture handshaking + /// + private static int GetHandshakeHashCode(string fileVersion) + { + unsafe + { + fixed (char* src = fileVersion) + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + + int* pint = (int*)src; + int len = fileVersion.Length; + while (len > 0) + { + hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; + if (len <= 2) + { + break; + } + + hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1]; + pint += 2; + len -= 4; + } + + return hash1 + (hash2 * 1566083941); + } + } + } + } +} diff --git a/src/Shared/Constants.cs b/src/Shared/Constants.cs new file mode 100644 index 00000000000..1a4e7fe1018 --- /dev/null +++ b/src/Shared/Constants.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; + +namespace Microsoft.Build.Shared +{ + /// + /// Constants that we want to be shareable across all our assemblies. + /// + internal static class MSBuildConstants + { + /// + /// The name of the property that indicates the the tools path + /// + internal const string ToolsPath = "MSBuildToolsPath"; + + /// + /// The most current Visual Studio Version known to this version of MSBuild. + /// +#if STANDALONEBUILD + internal const string CurrentVisualStudioVersion = "14.1"; +#else + internal const string CurrentVisualStudioVersion = Microsoft.VisualStudio.Internal.BrandNames.VSGeneralVersion; +#endif + + /// + /// The most current ToolsVersion known to this version of MSBuild. + /// + internal const string CurrentToolsVersion = CurrentVisualStudioVersion; + + /// + /// The most current ToolsVersion known to this version of MSBuild as a Version object. + /// + internal static Version CurrentToolsVersionAsVersion = new Version(CurrentToolsVersion); + + /// + /// The most current VSGeneralAssemblyVersion known to this version of MSBuild. + /// +#if STANDALONEBUILD + internal const string CurrentAssemblyVersion = "14.1.0.0"; +#else + internal const string CurrentAssemblyVersion = Microsoft.VisualStudio.Internal.BrandNames.VSGeneralAssemblyVersion; +#endif + + /// + /// Current version of this MSBuild Engine assembly in the form, e.g, "12.0" + /// + internal static string CurrentProductVersion + { + get + { +#if STANDALONEBUILD + return "14.1"; +#else + Version thisAssemblyVersion = new Version(ThisAssembly.Version); + // "12.0.0.0" --> "12.0" + return thisAssemblyVersion.Major + "." + thisAssemblyVersion.Minor; +#endif + } + } + } + + /// + /// Constants naming well-known item metadata. + /// + internal static class ItemMetadataNames + { + internal const string fusionName = "FusionName"; + internal const string hintPath = "HintPath"; + internal const string assemblyFolderKey = "AssemblyFolderKey"; + internal const string alias = "Alias"; + internal const string aliases = "Aliases"; + internal const string parentFile = "ParentFile"; + internal const string privateMetadata = "Private"; + internal const string copyLocal = "CopyLocal"; + internal const string isRedistRoot = "IsRedistRoot"; + internal const string redist = "Redist"; + internal const string resolvedFrom = "ResolvedFrom"; + internal const string destinationSubDirectory = "DestinationSubDirectory"; + internal const string specificVersion = "SpecificVersion"; + internal const string link = "Link"; + internal const string subType = "SubType"; + internal const string executableExtension = "ExecutableExtension"; + internal const string embedInteropTypes = "EmbedInteropTypes"; + internal const string targetPath = "TargetPath"; + internal const string dependentUpon = "DependentUpon"; + internal const string msbuildSourceProjectFile = "MSBuildSourceProjectFile"; + internal const string msbuildSourceTargetName = "MSBuildSourceTargetName"; + internal const string isPrimary = "IsPrimary"; + internal const string targetFramework = "RequiredTargetFramework"; + internal const string frameworkDirectory = "FrameworkDirectory"; + internal const string version = "Version"; + internal const string imageRuntime = "ImageRuntime"; + internal const string winMDFile = "WinMDFile"; + internal const string winMDFileType = "WinMDFileType"; + internal const string msbuildReferenceSourceTarget = "ReferenceSourceTarget"; + internal const string msbuildReferenceGrouping = "ReferenceGrouping"; + internal const string msbuildReferenceGroupingDisplayName = "ReferenceGroupingDisplayName"; + internal const string msbuildReferenceFromSDK = "ReferenceFromSDK"; + internal const string winmdImplmentationFile = "Implementation"; + internal const string projectReferenceOriginalItemSpec = "ProjectReferenceOriginalItemSpec"; + internal const string IgnoreVersionForFrameworkReference = "IgnoreVersionForFrameworkReference"; + internal const string frameworkFile = "FrameworkFile"; + } +} diff --git a/src/Shared/ConversionUtilities.cs b/src/Shared/ConversionUtilities.cs new file mode 100644 index 00000000000..44324960ae9 --- /dev/null +++ b/src/Shared/ConversionUtilities.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; + +using error = Microsoft.Build.Shared.ErrorUtilities; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains only static methods, which are useful throughout many + /// of the MSBuild classes and don't really belong in any specific class. + /// + internal static class ConversionUtilities + { + /// + /// Converts a string to a bool. We consider "true/false", "on/off", and + /// "yes/no" to be valid boolean representations in the XML. + /// + /// The string to convert. + /// Boolean true or false, corresponding to the string. + internal static bool ConvertStringToBool(string parameterValue) + { + if (ValidBooleanTrue(parameterValue)) + { + return true; + } + else if (ValidBooleanFalse(parameterValue)) + { + return false; + } + else + { + // Unsupported boolean representation. + error.VerifyThrowArgument(false, "Shared.CannotConvertStringToBool", parameterValue); + return false; + } + } + + /// + /// Returns true if the string can be successfully converted to a bool, + /// such as "on" or "yes" + /// + internal static bool CanConvertStringToBool(string parameterValue) + { + return (ValidBooleanTrue(parameterValue) || ValidBooleanFalse(parameterValue)); + } + + /// + /// Returns true if the string represents a valid MSBuild boolean true value, + /// such as "on", "!false", "yes" + /// + private static bool ValidBooleanTrue(string parameterValue) + { + return ((String.Compare(parameterValue, "true", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "on", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "yes", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "!false", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "!off", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "!no", StringComparison.OrdinalIgnoreCase) == 0)); + } + + /// + /// Returns true if the string represents a valid MSBuild boolean false value, + /// such as "!on" "off" "no" "!true" + /// + private static bool ValidBooleanFalse(string parameterValue) + { + return ((String.Compare(parameterValue, "false", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "off", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "no", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "!true", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "!on", StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(parameterValue, "!yes", StringComparison.OrdinalIgnoreCase) == 0)); + } + + /// + /// Converts a string like "123.456" into a double. Leading sign is allowed. + /// + internal static double ConvertDecimalToDouble(string number) + { + return Double.Parse(number, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture.NumberFormat); + } + + /// + /// Converts a hex string like "0xABC" into a double. + /// + internal static double ConvertHexToDouble(string number) + { + return (double)Int32.Parse(number.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture.NumberFormat); + } + + /// + /// Converts a string like "123.456" or "0xABC" into a double. + /// Tries decimal conversion first. + /// + internal static double ConvertDecimalOrHexToDouble(string number) + { + if (ConversionUtilities.ValidDecimalNumber(number)) + { + return ConversionUtilities.ConvertDecimalToDouble(number); + } + else if (ConversionUtilities.ValidHexNumber(number)) + { + return ConversionUtilities.ConvertHexToDouble(number); + } + else + { + ErrorUtilities.VerifyThrow(false, "Cannot numeric evaluate"); + return 0.0D; + } + } + + /// + /// Returns true if the string is a valid hex number, like "0xABC" + /// + private static bool ValidHexNumber(string number) + { + bool canConvert = false; + if (number.Length >= 3 && number[0] == '0' && (number[1] == 'x' || number[1] == 'X')) + { + int value; + canConvert = Int32.TryParse(number.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture.NumberFormat, out value); + } + return canConvert; + } + + /// + /// Returns true if the string is a valid decimal number, like "-123.456" + /// + private static bool ValidDecimalNumber(string number) + { + double value; + return Double.TryParse(number, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture.NumberFormat, out value); + } + + /// + /// Returns true if the string is a valid decimal or hex number + /// + internal static bool ValidDecimalOrHexNumber(string number) + { + return ValidDecimalNumber(number) || ValidHexNumber(number); + } + } +} diff --git a/src/Shared/CopyOnWriteDictionary.cs b/src/Shared/CopyOnWriteDictionary.cs new file mode 100644 index 00000000000..893635cf96a --- /dev/null +++ b/src/Shared/CopyOnWriteDictionary.cs @@ -0,0 +1,656 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dictionary that has copy-on-write semantics. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Collections +{ + /// + /// A dictionary that has copy-on-write semantics. + /// KEYS AND VALUES MUST BE IMMUTABLE OR COPY-ON-WRITE FOR THIS TO WORK. + /// + /// The key type. + /// The value type. + /// + /// This dictionary works by having a backing dictionary which is ref-counted for each + /// COWDictionary which references it. When a write operation is performed on any + /// COWDictionary, we check the reference count on the backing dictionary. If it is + /// greater than 1, it means any changes we make to it would be visible to other readers. + /// Therefore, we clone the backing dictionary and decrement the reference count on the + /// original. From there on we use the cloned dictionary, which now has a reference count + /// of 1. + /// + /// Thread safety: for all users, this class is as thread safe as the underlying Dictionary implementation, that is, + /// safe for concurrent readers or one writer from EACH user. It achieves this by locking itself and cloning before + /// any write, if it is being shared - i.e., stopping sharing before any writes occur. + /// + /// + /// This class must be serializable as it is used for metadata passed to tasks, which may + /// be run in a separate appdomain. + /// + [Serializable] + internal class CopyOnWriteDictionary : IDictionary, IDictionary where V : class + { +#if DEBUG + /// + /// When set forces immediate copy + /// + private static readonly bool s_forceWrite = (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDFORCECOWCOPY"))); +#endif + /// + /// The equality comparer to use when the dictionary is created. + /// + private readonly IEqualityComparer _keyComparer; + + /// + /// The default capacity. + /// + private readonly int _capacity; + + /// + /// A special single dummy instance that always appears empty. + /// + private static CopyOnWriteDictionary s_dummy = new CopyOnWriteDictionary(); + + /// + /// The backing dictionary. + /// Lazily created. + /// + private CopyOnWriteBackingDictionary _backing; + + /// + /// Constructor. Consider supplying a comparer instead. + /// + internal CopyOnWriteDictionary() + { + } + + /// + /// Constructor taking an initial capacity + /// + internal CopyOnWriteDictionary(int capacity) + : this(capacity, null) + { + } + + /// + /// Constructor taking a specified comparer for the keys + /// + internal CopyOnWriteDictionary(IEqualityComparer keyComparer) + : this(0, keyComparer) + { + } + + /// + /// Constructor taking a specified comparer for the keys and an initial capacity + /// + internal CopyOnWriteDictionary(int capacity, IEqualityComparer keyComparer) + { + _capacity = capacity; + _keyComparer = keyComparer; + } + + /// + /// Serialization constructor, for crossing appdomain boundaries + /// + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "info", Justification = "Not needed")] + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "context", Justification = "Not needed")] + protected CopyOnWriteDictionary(SerializationInfo info, StreamingContext context) + { + } + + /// + /// Cloning constructor. Defers the actual clone. + /// + private CopyOnWriteDictionary(CopyOnWriteDictionary that) + { + _keyComparer = that._keyComparer; + _backing = that._backing; + if (_backing != null) + { + lock (((ICollection)_backing).SyncRoot) + { + _backing.AddRef(); + } + } + } + + /// + /// Returns the collection of keys in the dictionary. + /// + public ICollection Keys + { + get + { + return ReadOperation.Keys; + } + } + + /// + /// Returns the collection of values in the dictionary. + /// + public ICollection Values + { + get { return ReadOperation.Values; } + } + + /// + /// Returns the number of items in the collection. + /// + public int Count + { + get { return ReadOperation.Count; } + } + + /// + /// Returns true if the collection is read-only. + /// + public bool IsReadOnly + { + get { return ((IDictionary)ReadOperation).IsReadOnly; } + } + + /// + /// IDictionary implementation + /// + bool IDictionary.IsFixedSize + { + get { return false; } + } + + /// + /// IDictionary implementation + /// + bool IDictionary.IsReadOnly + { + get { return IsReadOnly; } + } + + /// + /// IDictionary implementation + /// + ICollection IDictionary.Keys + { + get { return (ICollection)Keys; } + } + + /// + /// IDictionary implementation + /// + ICollection IDictionary.Values + { + get { return (ICollection)Values; } + } + + /// + /// IDictionary implementation + /// + int ICollection.Count + { + get { return Count; } + } + + /// + /// IDictionary implementation + /// + bool ICollection.IsSynchronized + { + get { return false; } + } + + /// + /// IDictionary implementation + /// + object ICollection.SyncRoot + { + get { return this; } + } + + /// + /// A special single dummy instance that always appears empty. + /// + internal static CopyOnWriteDictionary Dummy + { + get { return s_dummy; } + } + + /// + /// Whether this is a dummy instance that always appears empty. + /// + internal bool IsDummy + { + get + { + if (Object.ReferenceEquals(this, Dummy)) + { + ErrorUtilities.VerifyThrow(_backing == null || _backing.Count == 0, "count"); // check count without recursion + return true; + } + + return false; + } + } + + /// + /// Comparer used for keys + /// + internal IEqualityComparer Comparer + { + get { return _keyComparer; } + } + + /// + /// Gets the backing dictionary for reading. + /// + private CopyOnWriteBackingDictionary ReadOperation + { + get + { + ErrorUtilities.VerifyThrow(!IsDummy || _backing == null || _backing.Count == 0, "count"); // check count without recursion +#if DEBUG + if (s_forceWrite) + { + if (!IsDummy) + { + return WriteOperation; + } + } +#endif + if (_backing == null) + { + return CopyOnWriteBackingDictionary.ReadOnlyEmptyInstance; + } + + return _backing; + } + } + + /// + /// Gets the backing dictionary for writing. + /// + private CopyOnWriteBackingDictionary WriteOperation + { + get + { + ErrorUtilities.VerifyThrow(!IsDummy, "dummy"); + + if (_backing == null) + { + _backing = new CopyOnWriteBackingDictionary(_capacity, _keyComparer); + } + else + { + lock (((ICollection)_backing).SyncRoot) + { + _backing = _backing.CloneForWriteIfNecessary(); + } + } + + return _backing; + } + } + + /// + /// Accesses the value for the specified key. + /// + public V this[K key] + { + get + { + return ReadOperation[key]; + } + + set + { + if (!IsDummy) + { + if (ReadOperation.HasNoClones) + { + WriteOperation[key] = value; + } + else + { + // Try to avoid a clone if it already is present with the same value + V existingValue = default(V); + if (!ReadOperation.TryGetValue(key, out existingValue) || !EqualityComparer.Default.Equals(existingValue, value)) + { + WriteOperation[key] = value; + } + } + } + } + } + + /// + /// IDictionary implementation + /// + object IDictionary.this[object key] + { + get + { + if (!ContainsKey((K)key)) + { + return null; + } + + return this[(K)key]; + } + + set + { + this[(K)key] = (V)value; + } + } + + /// + /// Adds a value to the dictionary. + /// + public void Add(K key, V value) + { + if (!IsDummy) + { + WriteOperation.Add(key, value); + } + } + + /// + /// Returns true if the dictionary contains the specified key. + /// + public bool ContainsKey(K key) + { + return ReadOperation.ContainsKey(key); + } + + /// + /// Removes the entry for the specified key from the dictionary. + /// + public bool Remove(K key) + { + // Avoid a clone if it's not present + if (ReadOperation.HasNoClones || ReadOperation.ContainsKey(key)) + { + if (!IsDummy) + { + return WriteOperation.Remove(key); + } + } + + return false; + } + + /// + /// Attempts to find the value for the specified key in the dictionary. + /// + public bool TryGetValue(K key, out V value) + { + return ReadOperation.TryGetValue(key, out value); + } + + /// + /// Adds an item to the collection. + /// + public void Add(KeyValuePair item) + { + if (!IsDummy) + { + ((IDictionary)WriteOperation).Add(item); + } + } + + /// + /// Clears the collection. + /// + public void Clear() + { + if (ReadOperation.Count > 0) + { + if (!IsDummy) + { + WriteOperation.Clear(); + } + } + } + + /// + /// Returns true ff the collection contains the specified item. + /// + public bool Contains(KeyValuePair item) + { + return ((IDictionary)ReadOperation).Contains(item); + } + + /// + /// Copies all of the elements of the collection to the specified array. + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)ReadOperation).CopyTo(array, arrayIndex); + } + + /// + /// Remove an item from the dictionary. + /// + public bool Remove(KeyValuePair item) + { + // If it doesn't already contain the key, avoid copying the dictionary. + if (ReadOperation.HasNoClones || ReadOperation.ContainsKey(item.Key)) + { + if (!IsDummy) + { + return ((IDictionary)WriteOperation).Remove(item); + } + } + + return false; + } + + /// + /// Implementation of generic IEnumerable.GetEnumerator() + /// + public IEnumerator> GetEnumerator() + { + return ReadOperation.GetEnumerator(); + } + + /// + /// Implementation of IEnumerable.GetEnumerator() + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + + /// + /// IDictionary implementation. + /// + void IDictionary.Add(object key, object value) + { + Add((K)key, (V)value); + } + + /// + /// IDictionary implementation. + /// + void IDictionary.Clear() + { + Clear(); + } + + /// + /// IDictionary implementation. + /// + bool IDictionary.Contains(object key) + { + return ContainsKey((K)key); + } + + /// + /// IDictionary implementation. + /// + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return ((IDictionary)ReadOperation).GetEnumerator(); + } + + /// + /// IDictionary implementation. + /// + void IDictionary.Remove(object key) + { + Remove((K)key); + } + + /// + /// IDictionary implementation. + /// + void ICollection.CopyTo(Array array, int index) + { + int i = 0; + foreach (KeyValuePair entry in this) + { + array.SetValue(new DictionaryEntry(entry.Key, entry.Value), index + i); + i++; + } + } + + /// + /// Clone, with the actual clone deferred + /// + internal CopyOnWriteDictionary Clone() + { + return new CopyOnWriteDictionary(this); + } + + /// + /// Returns true if these dictionaries have the same backing. + /// + internal bool HasSameBacking(CopyOnWriteDictionary other) + { + return Object.ReferenceEquals(other._backing, _backing); + } + + /// + /// A dictionary which is reference counted to allow several references for read operations, but knows when to clone for + /// write operations. + /// + /// The key type. + /// The value type. + [Serializable] + private class CopyOnWriteBackingDictionary : HybridDictionary where V1 : class + { + /// + /// An empty dictionary + /// + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Error in code analysis.")] + private static readonly CopyOnWriteBackingDictionary s_readOnlyEmptyDictionary = new CopyOnWriteBackingDictionary(); + + /// + /// The reference count. + /// + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Error in code analysis.")] + [NonSerialized] + private int _refCount = 1; + + /// + /// Constructor. + /// + public CopyOnWriteBackingDictionary(int capacity, IEqualityComparer comparer) + : base(capacity, comparer) + { + // Tracing.Record("New COWBD"); + } + + /// + /// Serialization constructor, for crossing appdomain boundaries + /// + protected CopyOnWriteBackingDictionary(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Empty constructor. + /// + private CopyOnWriteBackingDictionary() + : base() + { + } + + /// + /// Cloning constructor. + /// + private CopyOnWriteBackingDictionary(CopyOnWriteBackingDictionary that) + : base(that, that.Comparer) + { + // Tracing.Record("New COWBD-clone"); + } + + /// + /// Returns a read-only empty instance. + /// + public static CopyOnWriteBackingDictionary ReadOnlyEmptyInstance + { + get + { + return s_readOnlyEmptyDictionary; + } + } + + /// + /// Returns true if this collection has no clones. + /// + public bool HasNoClones + { + get + { + ErrorUtilities.VerifyThrow(_refCount >= 1, "refCount should not be less than 1."); + return _refCount == 1; + } + } + + /// + /// Clones backing dictionary if necessary for a write operation. + /// + public CopyOnWriteBackingDictionary CloneForWriteIfNecessary() + { + if (!HasNoClones) + { + _refCount--; + return new CopyOnWriteBackingDictionary(this); + } + + return this; + } + + /// + /// Adds a reader-reference to this backing dictionary. + /// + public int AddRef() + { + return ++_refCount; + } + + /// + /// Deserialization does not call any constructors, not even + /// the parameterless constructor. Therefore since we do not serialize + /// this field, we must populate it here. + /// + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + _refCount = 1; + } + } + } +} diff --git a/src/Shared/EncodingUtilities.cs b/src/Shared/EncodingUtilities.cs new file mode 100644 index 00000000000..b4bc885eda7 --- /dev/null +++ b/src/Shared/EncodingUtilities.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for dealing with console encodings. + /// + internal static class EncodingUtilities + { + /// + /// Get the current system locale code page, OEM version. OEM code pages are used for console-based input/output + /// for historical reasons. + /// + static internal Encoding CurrentSystemOemEncoding + { + get + { + // if we already have it, no need to do it again + if (s_currentOemEncoding != null) + return s_currentOemEncoding; + + // fall back to default ANSI encoding if we have problems + s_currentOemEncoding = Encoding.Default; + + try + { + // get the current OEM code page + s_currentOemEncoding = Encoding.GetEncoding(NativeMethodsShared.GetOEMCP()); + } + // theoretically, GetEncoding may throw an ArgumentException or a NotSupportedException. This should never + // really happen, since the code page we pass in has just been returned from the "underlying platform", + // so it really should support it. If it ever happens, we'll just fall back to the default encoding. + // No point in showing any errors to the users, since they most likely wouldn't be actionable. + catch (ArgumentException ex) + { + Debug.Assert(false, "GetEncoding(default OEM encoding) threw an ArgumentException in EncodingUtilities.CurrentSystemOemEncoding! Please log a bug against MSBuild.", ex.Message); + } + catch (NotSupportedException ex) + { + Debug.Assert(false, "GetEncoding(default OEM encoding) threw a NotSupportedException in EncodingUtilities.CurrentSystemOemEncoding! Please log a bug against MSBuild.", ex.Message); + } + + return s_currentOemEncoding; + } + } + + static private Encoding s_currentOemEncoding; + } +} diff --git a/src/Shared/ErrorUtilities.cs b/src/Shared/ErrorUtilities.cs new file mode 100644 index 00000000000..925178abc6e --- /dev/null +++ b/src/Shared/ErrorUtilities.cs @@ -0,0 +1,758 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Configuration; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Threading; + +#if BUILDINGAPPXTASKS +namespace Microsoft.Build.AppxPackage.Shared +#else +namespace Microsoft.Build.Shared +#endif +{ + /// + /// This class contains methods that are useful for error checking and validation. + /// + internal static class ErrorUtilities + { + /// + /// Emergency escape hatch. If a customer hits a bug in the shipped product causing an internal exception, + /// and fortuitously it happens that ignoring the VerifyThrow allows execution to continue in a reasonable way, + /// then we can give them this undocumented environment variable as an immediate workaround. + /// + private static readonly bool s_throwExceptions = String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDDONOTTHROWINTERNAL")); + private static readonly bool s_enableMSBuildDebugTracing = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDENABLEDEBUGTRACING")); + + #region DebugTracing + public static void DebugTraceMessage(string category, string formatstring, params object[] parameters) + { + if (s_enableMSBuildDebugTracing) + { + if (parameters != null) + { + Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, formatstring, parameters), category); + } + else + { + Trace.WriteLine(formatstring, category); + } + } + } + #endregion + +#if !BUILDINGAPPXTASKS + #region VerifyThrow -- for internal errors + + /// + /// Throws InternalErrorException. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + internal static void ThrowInternalError(string message, params object[] args) + { + if (s_throwExceptions) + { + throw new InternalErrorException(ResourceUtilities.FormatString(message, args)); + } + } + + /// + /// Throws InternalErrorException. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + internal static void ThrowInternalError(string message, Exception innerException, params object[] args) + { + if (s_throwExceptions) + { + throw new InternalErrorException(ResourceUtilities.FormatString(message, args), innerException); + } + } + + /// + /// Throws InternalErrorException. + /// Indicates the code path followed should not have been possible. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + internal static void ThrowInternalErrorUnreachable() + { + if (s_throwExceptions) + { + throw new InternalErrorException("Unreachable?"); + } + } + + /// + /// Throws InternalErrorException. + /// Indicates the code path followed should not have been possible. + /// This is only for situations that would mean that there is a bug in MSBuild itself. + /// + internal static void ThrowIfTypeDoesNotImplementToString(object param) + { +#if DEBUG + // Check it has a real implementation of ToString() + if (String.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal)) + { + ErrorUtilities.ThrowInternalError("This type does not implement ToString() properly {0}", param.GetType().FullName); + } +#endif + } + + /// + /// Helper to throw an InternalErrorException when the specified parameter is null. + /// This should be used ONLY if this would indicate a bug in MSBuild rather than + /// anything caused by user action. + /// + /// The value of the argument. + /// Parameter that should not be null + internal static void VerifyThrowInternalNull(object parameter, string parameterName) + { + if (parameter == null) + { + ThrowInternalError("{0} unexpectedly null", parameterName); + } + } + + /// + /// Helper to throw an InternalErrorException when a lock on the specified object is not already held. + /// This should be used ONLY if this would indicate a bug in MSBuild rather than + /// anything caused by user action. + /// + /// The object that should already have been used as a lock. + internal static void VerifyThrowInternalLockHeld(object locker) + { +#if !CLR2COMPATIBILITY + if (!Monitor.IsEntered(locker)) + { + ThrowInternalError("Lock should already have been taken"); + } +#endif + } + + /// + /// Helper to throw an InternalErrorException when the specified parameter is null or zero length. + /// This should be used ONLY if this would indicate a bug in MSBuild rather than + /// anything caused by user action. + /// + /// The value of the argument. + /// Parameter that should not be null or zero length + internal static void VerifyThrowInternalLength(string parameterValue, string parameterName) + { + VerifyThrowInternalNull(parameterValue, parameterName); + + if (parameterValue.Length == 0) + { + ThrowInternalError("{0} unexpectedly empty", parameterName); + } + } + + /// + /// Helper to throw an InternalErrorException when the specified parameter is not a rooted path. + /// This should be used ONLY if this would indicate a bug in MSBuild rather than + /// anything caused by user action. + /// + /// Parameter that should be a rooted path + internal static void VerifyThrowInternalRooted(string value) + { + if (!Path.IsPathRooted(value)) + { + ThrowInternalError("{0} unexpectedly not a rooted path", value); + } + } + + /// + /// This method should be used in places where one would normally put + /// an "assert". It should be used to validate that our assumptions are + /// true, where false would indicate that there must be a bug in our + /// code somewhere. This should not be used to throw errors based on bad + /// user input or anything that the user did wrong. + /// + /// + /// + internal static void VerifyThrow + ( + bool condition, + string unformattedMessage + ) + { + if (!condition) + { + // PERF NOTE: explicitly passing null for the arguments array + // prevents memory allocation + ThrowInternalError(unformattedMessage, null, null); + } + } + + /// + /// Overload for one string format argument. + /// + /// + /// + /// + internal static void VerifyThrow + ( + bool condition, + string unformattedMessage, + object arg0 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInternalError() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInternalError(unformattedMessage, arg0); + } + } + + /// + /// Overload for two string format arguments. + /// + /// + /// + /// + /// + internal static void VerifyThrow + ( + bool condition, + string unformattedMessage, + object arg0, + object arg1 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInternalError() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInternalError(unformattedMessage, arg0, arg1); + } + } + + /// + /// Overload for three string format arguments. + /// + /// + /// + /// + /// + /// + internal static void VerifyThrow + ( + bool condition, + string unformattedMessage, + object arg0, + object arg1, + object arg2 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInternalError() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInternalError(unformattedMessage, arg0, arg1, arg2); + } + } + + /// + /// Overload for four string format arguments. + /// + /// + /// + /// + /// + /// + /// + internal static void VerifyThrow + ( + bool condition, + string unformattedMessage, + object arg0, + object arg1, + object arg2, + object arg3 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInternalError() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInternalError(unformattedMessage, arg0, arg1, arg2, arg3); + } + } + + #endregion + + #region VerifyThrowInvalidOperation + + /// + /// Throws an InvalidOperationException with the specified resource string + /// + /// Resource to use in the exception + /// Formatting args. + internal static void ThrowInvalidOperation(string resourceName, params object[] args) + { +#if DEBUG + ResourceUtilities.VerifyResourceStringExists(resourceName); +#endif + if (s_throwExceptions) + { + throw new InvalidOperationException(ResourceUtilities.FormatResourceString(resourceName, args)); + } + } + + /// + /// Throws an InvalidOperationException if the given condition is false. + /// + /// + /// + internal static void VerifyThrowInvalidOperation + ( + bool condition, + string resourceName + ) + { + if (!condition) + { + // PERF NOTE: explicitly passing null for the arguments array + // prevents memory allocation + ThrowInvalidOperation(resourceName, null); + } + } + + /// + /// Overload for one string format argument. + /// + /// + /// + /// + internal static void VerifyThrowInvalidOperation + ( + bool condition, + string resourceName, + object arg0 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidOperation() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidOperation(resourceName, arg0); + } + } + + /// + /// Overload for two string format arguments. + /// + /// + /// + /// + /// + internal static void VerifyThrowInvalidOperation + ( + bool condition, + string resourceName, + object arg0, + object arg1 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidOperation() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidOperation(resourceName, arg0, arg1); + } + } + + /// + /// Overload for three string format arguments. + /// + /// + /// + /// + /// + /// + internal static void VerifyThrowInvalidOperation + ( + bool condition, + string resourceName, + object arg0, + object arg1, + object arg2 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidOperation() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidOperation(resourceName, arg0, arg1, arg2); + } + } + + #endregion + + #region VerifyThrowArgument + + /// + /// Throws an ArgumentException that can include an inner exception. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments + /// is expensive, because memory is allocated for the array of arguments -- do + /// not call this method repeatedly in performance-critical scenarios + /// + internal static void ThrowArgument + ( + string resourceName, + params object[] args + ) + { + ThrowArgument(null, resourceName, args); + } + + /// + /// Throws an ArgumentException that can include an inner exception. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments + /// is expensive, because memory is allocated for the array of arguments -- do + /// not call this method repeatedly in performance-critical scenarios + /// + /// + /// This method is thread-safe. + /// + /// Can be null. + /// + /// + private static void ThrowArgument + ( + Exception innerException, + string resourceName, + params object[] args + ) + { +#if DEBUG + ResourceUtilities.VerifyResourceStringExists(resourceName); +#endif + if (s_throwExceptions) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString(resourceName, args), innerException); + } + } + + /// + /// Throws an ArgumentException if the given condition is false. + /// + /// This method is thread-safe. + /// + /// + internal static void VerifyThrowArgument + ( + bool condition, + string resourceName + ) + { + VerifyThrowArgument(condition, null, resourceName); + } + + /// + /// Overload for one string format argument. + /// + /// This method is thread-safe. + /// + /// + /// + internal static void VerifyThrowArgument + ( + bool condition, + string resourceName, + object arg0 + ) + { + VerifyThrowArgument(condition, null, resourceName, arg0); + } + + /// + /// Overload for two string format arguments. + /// + /// This method is thread-safe. + /// + /// + /// + /// + internal static void VerifyThrowArgument + ( + bool condition, + string resourceName, + object arg0, + object arg1 + ) + { + VerifyThrowArgument(condition, null, resourceName, arg0, arg1); + } + + /// + /// Overload for three string format arguments. + /// + /// This method is thread-safe. + internal static void VerifyThrowArgument + ( + bool condition, + string resourceName, + object arg0, + object arg1, + object arg2 + ) + { + VerifyThrowArgument(condition, null, resourceName, arg0, arg1, arg2); + } + + /// + /// Overload for four string format arguments. + /// + /// This method is thread-safe. + internal static void VerifyThrowArgument + ( + bool condition, + string resourceName, + object arg0, + object arg1, + object arg2, + object arg3 + ) + { + VerifyThrowArgument(condition, null, resourceName, arg0, arg1, arg2, arg3); + } + + /// + /// Throws an ArgumentException that includes an inner exception, if + /// the given condition is false. + /// + /// This method is thread-safe. + /// + /// Can be null. + /// + internal static void VerifyThrowArgument + ( + bool condition, + Exception innerException, + string resourceName + ) + { + if (!condition) + { + // PERF NOTE: explicitly passing null for the arguments array + // prevents memory allocation + ThrowArgument(innerException, resourceName, null); + } + } + + /// + /// Overload for one string format argument. + /// + /// This method is thread-safe. + /// + /// + /// + /// + internal static void VerifyThrowArgument + ( + bool condition, + Exception innerException, + string resourceName, + object arg0 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowArgument() method, because that method always allocates + // memory for its variable array of arguments + if (!condition) + { + ThrowArgument(innerException, resourceName, arg0); + } + } + + /// + /// Overload for two string format arguments. + /// + /// This method is thread-safe. + /// + /// + /// + /// + /// + internal static void VerifyThrowArgument + ( + bool condition, + Exception innerException, + string resourceName, + object arg0, + object arg1 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowArgument() method, because that method always allocates + // memory for its variable array of arguments + if (!condition) + { + ThrowArgument(innerException, resourceName, arg0, arg1); + } + } + + /// + /// Overload for three string format arguments. + /// + /// This method is thread-safe. + internal static void VerifyThrowArgument + ( + bool condition, + Exception innerException, + string resourceName, + object arg0, + object arg1, + object arg2 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowArgument() method, because that method always allocates + // memory for its variable array of arguments + if (!condition) + { + ThrowArgument(innerException, resourceName, arg0, arg1, arg2); + } + } + + /// + /// Overload for four string format arguments. + /// + /// This method is thread-safe. + internal static void VerifyThrowArgument + ( + bool condition, + Exception innerException, + string resourceName, + object arg0, + object arg1, + object arg2, + object arg3 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowArgument() method, because that method always allocates + // memory for its variable array of arguments + if (!condition) + { + ThrowArgument(innerException, resourceName, arg0, arg1, arg2, arg3); + } + } + + #endregion + + #region VerifyThrowArgumentXXX + + /// + /// Throws an argument out of range exception. + /// + internal static void ThrowArgumentOutOfRange(string parameterName) + { + if (s_throwExceptions) + { + throw new ArgumentOutOfRangeException(parameterName); + } + } + + /// + /// Throws an ArgumentOutOfRangeException using the given parameter name + /// if the condition is false. + /// + internal static void VerifyThrowArgumentOutOfRange(bool condition, string parameterName) + { + if (!condition) + { + ThrowArgumentOutOfRange(parameterName); + } + } + + /// + /// Throws an ArgumentNullException if the given string parameter is null + /// and ArgumentException if it has zero length. + /// + /// + /// + internal static void VerifyThrowArgumentLength(string parameter, string parameterName) + { + VerifyThrowArgumentNull(parameter, parameterName); + + if (parameter.Length == 0 && s_throwExceptions) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("Shared.ParameterCannotHaveZeroLength", parameterName)); + } + } + + /// + /// Throws an ArgumentException if the string has zero length, unless it is + /// null, in which case no exception is thrown. + /// + internal static void VerifyThrowArgumentLengthIfNotNull(string parameter, string parameterName) + { + if (parameter != null && parameter.Length == 0 && s_throwExceptions) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("Shared.ParameterCannotHaveZeroLength", parameterName)); + } + } + + /// + /// Throws an ArgumentNullException if the given parameter is null. + /// + /// This method is thread-safe. + /// + /// + internal static void VerifyThrowArgumentNull(object parameter, string parameterName) + { + VerifyThrowArgumentNull(parameter, parameterName, "Shared.ParameterCannotBeNull"); + } + + /// + /// Throws an ArgumentNullException if the given parameter is null. + /// + /// This method is thread-safe. + internal static void VerifyThrowArgumentNull(object parameter, string parameterName, string resourceName) + { + if (parameter == null && s_throwExceptions) + { + // Most ArgumentNullException overloads append its own rather clunky multi-line message. + // So use the one overload that doesn't. + throw new ArgumentNullException( + ResourceUtilities.FormatResourceString(resourceName, parameterName), + (Exception)null); + } + } + + /// + /// Verifies the given arrays are not null and have the same length + /// + /// + /// + /// + /// + internal static void VerifyThrowArgumentArraysSameLength(Array parameter1, Array parameter2, string parameter1Name, string parameter2Name) + { + VerifyThrowArgumentNull(parameter1, parameter1Name); + VerifyThrowArgumentNull(parameter2, parameter2Name); + + if (parameter1.Length != parameter2.Length && s_throwExceptions) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("Shared.ParametersMustHaveTheSameLength", parameter1Name, parameter2Name)); + } + } + + #endregion +#endif + } +} diff --git a/src/Shared/EscapingUtilities.cs b/src/Shared/EscapingUtilities.cs new file mode 100644 index 00000000000..84dbd8d106e --- /dev/null +++ b/src/Shared/EscapingUtilities.cs @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Microsoft.Build.Shared +{ + /// + /// This class implements static methods to assist with unescaping of %XX codes + /// in the MSBuild file format. + /// + /// + /// PERF: since we escape and unescape relatively frequently, it may be worth caching + /// the last N strings that were (un)escaped + /// + static internal class EscapingUtilities + { + /// + /// Optional cache of escaped strings for use when needing to escape in performance-critical scenarios with significant + /// expected string reuse. + /// + private static Dictionary s_unescapedToEscapedStrings = new Dictionary(StringComparer.Ordinal); + + /// + /// Replaces all instances of %XX in the input string with the character represented + /// by the hexadecimal number XX. + /// + /// The string to unescape. + /// unescaped string + internal static string UnescapeAll + ( + string escapedString + ) + { + bool throwAwayBool; + return UnescapeAll(escapedString, out throwAwayBool); + } + + /// + /// Replaces all instances of %XX in the input string with the character represented + /// by the hexadecimal number XX. + /// + /// The string to unescape. + /// Whether any replacements were made. + /// unescaped string + internal static string UnescapeAll + ( + string escapedString, + out bool escapingWasNecessary + ) + { + escapingWasNecessary = false; + + // If the string doesn't contain anything, then by definition it doesn't + // need unescaping. + if (String.IsNullOrEmpty(escapedString)) + { + return escapedString; + } + + // If there are no percent signs, just return the original string immediately. + // Don't even instantiate the StringBuilder. + int indexOfPercent = escapedString.IndexOf('%'); + if (indexOfPercent == -1) + { + return escapedString; + } + + // This is where we're going to build up the final string to return to the caller. + StringBuilder unescapedString = StringBuilderCache.Acquire(escapedString.Length); + + int currentPosition = 0; + + // Loop until there are no more percent signs in the input string. + while (indexOfPercent != -1) + { + // There must be two hex characters following the percent sign + // for us to even consider doing anything with this. + if ( + (indexOfPercent <= (escapedString.Length - 3)) && + Uri.IsHexDigit(escapedString[indexOfPercent + 1]) && + Uri.IsHexDigit(escapedString[indexOfPercent + 2]) + ) + { + // First copy all the characters up to the current percent sign into + // the destination. + unescapedString.Append(escapedString, currentPosition, indexOfPercent - currentPosition); + + // Convert the %XX to an actual real character. + string hexString = escapedString.Substring(indexOfPercent + 1, 2); + char unescapedCharacter = (char)int.Parse(hexString, System.Globalization.NumberStyles.HexNumber, + CultureInfo.InvariantCulture); + + // if the unescaped character is not on the exception list, append it + unescapedString.Append(unescapedCharacter); + + // Advance the current pointer to reflect the fact that the destination string + // is up to date with everything up to and including this escape code we just found. + currentPosition = indexOfPercent + 3; + + escapingWasNecessary = true; + } + + // Find the next percent sign. + indexOfPercent = escapedString.IndexOf('%', indexOfPercent + 1); + } + + // Okay, there are no more percent signs in the input string, so just copy the remaining + // characters into the destination. + unescapedString.Append(escapedString, currentPosition, escapedString.Length - currentPosition); + + return StringBuilderCache.GetStringAndRelease(unescapedString); + } + + + /// + /// Adds instances of %XX in the input string where the char char to be escaped appears + /// XX is the hex value of the ASCII code for the char. Interns and caches the result. + /// + /// + /// NOTE: Only recommended for use in scenarios where there's expected to be significant + /// repetition of the escaped string. Cache currently grows unbounded. + /// + internal static string EscapeWithCaching(string unescapedString) + { + return EscapeWithOptionalCaching(unescapedString, cache: true); + } + + /// + /// Adds instances of %XX in the input string where the char char to be escaped appears + /// XX is the hex value of the ASCII code for the char. + /// + /// The string to escape. + /// escaped string + internal static string Escape(string unescapedString) + { + return EscapeWithOptionalCaching(unescapedString, cache: false); + } + + /// + /// Adds instances of %XX in the input string where the char char to be escaped appears + /// XX is the hex value of the ASCII code for the char. Caches if requested. + /// + /// The string to escape. + /// + /// True if the cache should be checked, and if the resultant string + /// should be cached. + /// + private static string EscapeWithOptionalCaching(string unescapedString, bool cache) + { + // If there are no special chars, just return the original string immediately. + // Don't even instantiate the StringBuilder. + if (String.IsNullOrEmpty(unescapedString) || !ContainsReservedCharacters(unescapedString)) + { + return unescapedString; + } + + // next, if we're caching, check to see if it's already there. + if (cache) + { + string cachedEscapedString = null; + lock (s_unescapedToEscapedStrings) + { + if (s_unescapedToEscapedStrings.TryGetValue(unescapedString, out cachedEscapedString)) + { + return cachedEscapedString; + } + } + } + + // This is where we're going to build up the final string to return to the caller. + StringBuilder escapedStringBuilder = StringBuilderCache.Acquire(unescapedString.Length * 2); + + AppendEscapedString(escapedStringBuilder, unescapedString); + + if (!cache) + { + return StringBuilderCache.GetStringAndRelease(escapedStringBuilder); + } + + string escapedString = OpportunisticIntern.StringBuilderToString(escapedStringBuilder); + StringBuilderCache.Release(escapedStringBuilder); + + lock (s_unescapedToEscapedStrings) + { + s_unescapedToEscapedStrings[unescapedString] = escapedString; + } + + return escapedString; + } + + /// + /// Before trying to actually escape the string, it can be useful to call this method to determine + /// if escaping is necessary at all. This can save lots of calls to copy around item metadata + /// that is really the same whether escaped or not. + /// + /// + /// + private static bool ContainsReservedCharacters + ( + string unescapedString + ) + { + return (-1 != unescapedString.IndexOfAny(s_charsToEscape)); + } + + /// + /// Determines whether the string contains the escaped form of '*' or '?'. + /// + /// + /// + internal static bool ContainsEscapedWildcards + ( + string escapedString + ) + { + if (-1 != escapedString.IndexOf('%')) + { + // It has a '%' sign. We have promise. + if ( + (-1 != escapedString.IndexOf("%2", StringComparison.Ordinal)) || + (-1 != escapedString.IndexOf("%3", StringComparison.Ordinal)) + ) + { + // It has either a '%2' or a '%3'. This is looking very promising. + return + ( + (-1 != escapedString.IndexOf("%2a", StringComparison.Ordinal)) || + (-1 != escapedString.IndexOf("%2A", StringComparison.Ordinal)) || + (-1 != escapedString.IndexOf("%3f", StringComparison.Ordinal)) || + (-1 != escapedString.IndexOf("%3F", StringComparison.Ordinal)) + ); + } + } + return false; + } + + /// + /// Convert the given integer into its hexadecimal representation. + /// + /// The number to convert, which must be non-negative and less than 16 + /// The character which is the hexadecimal representation of . + private static char HexDigitChar(int x) + { + return (char)(x + (x < 10 ? '0' : ('a' - 10))); + } + + /// + /// Append the escaped version of the given character to a . + /// + /// The to which to append. + /// The character to escape. + private static void AppendEscapedChar(StringBuilder sb, char ch) + { + // Append the escaped version which is a percent sign followed by two hexadecimal digits + sb.Append('%'); + sb.Append(HexDigitChar(ch / 0x10)); + sb.Append(HexDigitChar(ch & 0x0F)); + } + + /// + /// Append the escaped version of the given string to a . + /// + /// The to which to append. + /// The unescaped string. + private static void AppendEscapedString(StringBuilder sb, string unescapedString) + { + // Replace each unescaped special character with an escape sequence one + for (int idx = 0; ;) + { + int nextIdx = unescapedString.IndexOfAny(s_charsToEscape, idx); + if (nextIdx == -1) + { + sb.Append(unescapedString, idx, unescapedString.Length - idx); + break; + } + + sb.Append(unescapedString, idx, nextIdx - idx); + AppendEscapedChar(sb, unescapedString[nextIdx]); + idx = nextIdx + 1; + } + } + + /// + /// Special characters that need escaping. + /// It's VERY important that the percent character is the FIRST on the list - since it's both a character + /// we escape and use in escape sequences, we can unintentionally escape other escape sequences if we + /// don't process it first. Of course we'll have a similar problem if we ever decide to escape hex digits + /// (that would require rewriting the algorithm) but since it seems unlikely that we ever do, this should + /// be good enough to avoid complicating the algorithm at this point. + /// + private static readonly char[] s_charsToEscape = { '%', '*', '?', '@', '$', '(', ')', ';', '\'' }; + } +} diff --git a/src/Shared/EventArgsFormatting.cs b/src/Shared/EventArgsFormatting.cs new file mode 100644 index 00000000000..29b7a194c7c --- /dev/null +++ b/src/Shared/EventArgsFormatting.cs @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Shared +{ + /// + /// Offers a default string format for Error and Warning events + /// + internal static class EventArgsFormatting + { + /// + /// Escape the carriage Return from a string + /// + /// + /// String with carriage returns escaped as \\r + internal static string EscapeCarriageReturn(string stringWithCarriageReturn) + { + if (!string.IsNullOrEmpty(stringWithCarriageReturn)) + { + return stringWithCarriageReturn.Replace("\r", "\\r"); + } + // If the string is null or empty or then we just return the string + return stringWithCarriageReturn; + } + + /// + /// Format the error event message and all the other event data into + /// a single string. + /// + /// Error to format + /// The formatted message string. + internal static string FormatEventMessage(BuildErrorEventArgs e) + { + return FormatEventMessage(e, false); + } + + /// + /// Format the error event message and all the other event data into + /// a single string. + /// + /// Error to format + /// The formatted message string. + internal static string FormatEventMessage(BuildErrorEventArgs e, bool removeCarriageReturn) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + // "error" should not be localized + return FormatEventMessage("error", e.Subcategory, removeCarriageReturn ? EscapeCarriageReturn(e.Message) : e.Message, + e.Code, e.File, null, e.LineNumber, e.EndLineNumber, + e.ColumnNumber, e.EndColumnNumber, e.ThreadId); + } + + /// + /// Format the error event message and all the other event data into + /// a single string. + /// + /// Error to format + /// The formatted message string. + internal static string FormatEventMessage(BuildErrorEventArgs e, bool removeCarriageReturn, bool showProjectFile) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + // "error" should not be localized + return FormatEventMessage("error", e.Subcategory, removeCarriageReturn ? EscapeCarriageReturn(e.Message) : e.Message, + e.Code, e.File, showProjectFile ? e.ProjectFile : null, e.LineNumber, e.EndLineNumber, + e.ColumnNumber, e.EndColumnNumber, e.ThreadId); + } + + /// + /// Format the warning message and all the other event data into a + /// single string. + /// + /// Warning to format + /// The formatted message string. + internal static string FormatEventMessage(BuildWarningEventArgs e) + { + return FormatEventMessage(e, false); + } + + /// + /// Format the warning message and all the other event data into a + /// single string. + /// + /// Warning to format + /// The formatted message string. + internal static string FormatEventMessage(BuildWarningEventArgs e, bool removeCarriageReturn) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + // "warning" should not be localized + return FormatEventMessage("warning", e.Subcategory, removeCarriageReturn ? EscapeCarriageReturn(e.Message) : e.Message, + e.Code, e.File, null, e.LineNumber, e.EndLineNumber, + e.ColumnNumber, e.EndColumnNumber, e.ThreadId); + } + + /// + /// Format the warning message and all the other event data into a + /// single string. + /// + /// Warning to format + /// The formatted message string. + internal static string FormatEventMessage(BuildWarningEventArgs e, bool removeCarriageReturn, bool showProjectFile) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + // "warning" should not be localized + return FormatEventMessage("warning", e.Subcategory, removeCarriageReturn ? EscapeCarriageReturn(e.Message) : e.Message, + e.Code, e.File, showProjectFile ? e.ProjectFile : null, e.LineNumber, e.EndLineNumber, + e.ColumnNumber, e.EndColumnNumber, e.ThreadId); + } + + /// + /// Format the message and all the other event data into a + /// single string. + /// + /// Message to format + /// The formatted message string. + internal static string FormatEventMessage(BuildMessageEventArgs e) + { + return FormatEventMessage(e, false); + } + + /// + /// Format the message and all the other event data into a + /// single string. + /// + /// Message to format + /// Escape CR or leave as is + /// The formatted message string. + internal static string FormatEventMessage(BuildMessageEventArgs e, bool removeCarriageReturn) + { + return FormatEventMessage(e, removeCarriageReturn, false); + } + + /// + /// Format the message and all the other event data into a + /// single string. + /// + /// Message to format + /// Escape CR or leave as is + /// Show project file or not + /// The formatted message string. + internal static string FormatEventMessage(BuildMessageEventArgs e, bool removeCarriageReturn, bool showProjectFile) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + // "message" should not be localized + return FormatEventMessage("message", e.Subcategory, removeCarriageReturn ? EscapeCarriageReturn(e.Message) : e.Message, + e.Code, e.File, showProjectFile ? e.ProjectFile : null, e.LineNumber, e.EndLineNumber, e.ColumnNumber, e.EndColumnNumber, e.ThreadId); + } + + /// + /// Format the event message and all the other event data into a + /// single string. + /// + /// category ("error" or "warning") + /// subcategory + /// event message + /// error or warning code number + /// file name + /// line number (0 if n/a) + /// end line number (0 if n/a) + /// column number (0 if n/a) + /// end column number (0 if n/a) + /// thread id + /// The formatted message string. + internal static string FormatEventMessage + ( + string category, + string subcategory, + string message, + string code, + string file, + int lineNumber, + int endLineNumber, + int columnNumber, + int endColumnNumber, + int threadId + ) + { + return FormatEventMessage(category, subcategory, message, code, file, null, lineNumber, endLineNumber, columnNumber, endColumnNumber, threadId); + } + + /// + /// Format the event message and all the other event data into a + /// single string. + /// + /// category ("error" or "warning") + /// subcategory + /// event message + /// error or warning code number + /// file name + /// the project file name + /// line number (0 if n/a) + /// end line number (0 if n/a) + /// column number (0 if n/a) + /// end column number (0 if n/a) + /// thread id + /// The formatted message string. + internal static string FormatEventMessage + ( + string category, + string subcategory, + string message, + string code, + string file, + string projectFile, + int lineNumber, + int endLineNumber, + int columnNumber, + int endColumnNumber, + int threadId + ) + { + StringBuilder format = new StringBuilder(); + + // Uncomment these lines to show show the processor, if present. + /* + if (threadId != 0) + { + format.Append("{0}>"); + } + */ + + if ((file == null) || (file.Length == 0)) + { + format.Append("MSBUILD : "); // Should not be localized. + } + else + { + format.Append("{1}"); + + if (lineNumber == 0) + { + format.Append(" : "); + } + else + { + if (columnNumber == 0) + { + if (endLineNumber == 0) + { + format.Append("({2}): "); + } + else + { + format.Append("({2}-{7}): "); + } + } + else + { + if (endLineNumber == 0) + { + if (endColumnNumber == 0) + { + format.Append("({2},{3}): "); + } + else + { + format.Append("({2},{3}-{8}): "); + } + } + else + { + if (endColumnNumber == 0) + { + format.Append("({2}-{7},{3}): "); + } + else + { + format.Append("({2},{3},{7},{8}): "); + } + } + } + } + } + + if ((subcategory != null) && (subcategory.Length != 0)) + { + format.Append("{9} "); + } + + // The category as a string (should not be localized) + format.Append("{4} "); + + // Put a code in, if available and necessary. + if (code == null) + { + format.Append(": "); + } + else + { + format.Append("{5}: "); + } + + // Put the message in, if available. + if (message != null) + { + format.Append("{6}"); + } + + // If the project file was specified, tack that onto the very end. + if (projectFile != null && !String.Equals(projectFile, file)) + { + format.Append(" [{10}]"); + } + + // A null message is allowed and is to be treated as a blank line. + if (null == message) + { + message = String.Empty; + } + + string finalFormat = format.ToString(); + + // If there are multiple lines, show each line as a separate message. + string[] lines = SplitStringOnNewLines(message); + StringBuilder formattedMessage = new StringBuilder(); + + for (int i = 0; i < lines.Length; i++) + { + formattedMessage.Append(String.Format( + CultureInfo.CurrentCulture, finalFormat, + threadId, file, + lineNumber, columnNumber, category, code, + lines[i], endLineNumber, endColumnNumber, + subcategory, projectFile)); + + if (i < (lines.Length - 1)) + { + formattedMessage.AppendLine(); + } + } + + return formattedMessage.ToString(); + } + + + /// + /// Splits strings on 'newLines' with tolerance for Everett and Dogfood builds. + /// + /// String to split. + private static string[] SplitStringOnNewLines(string s) + { + string[] subStrings = s.Split(s_newLines, StringSplitOptions.None); + return subStrings; + } + + /// + /// The kinds of newline breaks we expect. + /// + /// Currently we're not supporting "\r". + private static readonly string[] s_newLines = { "\r\n", "\n" }; + } +} diff --git a/src/Shared/ExceptionHandling.cs b/src/Shared/ExceptionHandling.cs new file mode 100644 index 00000000000..fe4a556dfe0 --- /dev/null +++ b/src/Shared/ExceptionHandling.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Utility methods for classifying and handling exceptions. +//----------------------------------------------------------------------- + +#if BUILDINGAPPXTASKS +namespace Microsoft.Build.AppxPackage.Shared + + +#else +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Security; +using System.Threading; +using System.Xml; +using System.Xml.Schema; + + +namespace Microsoft.Build.Shared +#endif +{ + /// + /// Utility methods for classifying and handling exceptions. + /// + internal static class ExceptionHandling + { +#if !BUILDINGAPPXTASKS + /// + /// The filename that exceptions will be dumped to + /// + private static string s_dumpFileName; +#endif + /// + /// If the given exception is "ignorable under some circumstances" return false. + /// Otherwise it's "really bad", and return true. + /// This makes it possible to catch(Exception ex) without catching disasters. + /// + /// The exception to check. + /// True if exception is critical. + internal static bool IsCriticalException(Exception e) + { + if (e is StackOverflowException + || e is OutOfMemoryException + || e is ThreadAbortException + || e is ThreadInterruptedException +#if !BUILDINGAPPXTASKS + || e is InternalErrorException +#endif + || e is AccessViolationException) + { + // Ideally we would include NullReferenceException, because it should only ever be thrown by CLR (use ArgumentNullException for arguments) + // but we should handle it if tasks and loggers throw it. + + // ExecutionEngineException has been deprecated by the CLR + return true; + } + +#if !CLR2COMPATIBILITY + // Check if any critical exceptions + var aggregateException = e as AggregateException; + + if (aggregateException != null) + { + // If the aggregate exception contains a critical exception it is considered a critical exception + if (aggregateException.InnerExceptions.Any(innerException => IsCriticalException(innerException))) + { + return true; + } + } +#endif + + return false; + } + + /// + /// If the given exception is file IO related or expected return false. + /// Otherwise, return true. + /// + /// The exception to check. + /// True if exception is not IO related or expected otherwise. + internal static bool NotExpectedException(Exception e) + { + // These all derive from IOException + // DirectoryNotFoundException + // DriveNotFoundException + // EndOfStreamException + // FileLoadException + // FileNotFoundException + // PathTooLongException + // PipeException + if (e is UnauthorizedAccessException + || e is NotSupportedException + || (e is ArgumentException && !(e is ArgumentNullException)) + || e is SecurityException + || e is IOException) + { + return false; + } + + return true; + } + + /// + /// Clearer named version of NotExpectedException. + /// + /// The exception to check. + /// True if exception is IO related. + internal static bool IsIoRelatedException(Exception e) + { + return !NotExpectedException(e); + } + + /// Checks if the exception is an XML one. + /// Exception to check. + /// True if exception is related to XML parsing. + internal static bool IsXmlException(Exception e) + { + return e is XmlSyntaxException + || e is XmlException + || e is XmlSchemaException + || e is UriFormatException; // XmlTextReader for example uses this under the covers + } + + /// Extracts line and column numbers from the exception if it is XML-related one. + /// XML-related exception. + /// Line and column numbers if available, (0,0) if not. + /// This function works around the fact that XmlException and XmlSchemaException are not directly related. + internal static LineAndColumn GetXmlLineAndColumn(Exception e) + { + var line = 0; + var column = 0; + + var xmlException = e as XmlException; + if (xmlException != null) + { + line = xmlException.LineNumber; + column = xmlException.LinePosition; + } + else + { + var schemaException = e as XmlSchemaException; + if (schemaException != null) + { + line = schemaException.LineNumber; + column = schemaException.LinePosition; + } + } + + return new LineAndColumn + { + Line = line, + Column = column + }; + } + +#if !BUILDINGAPPXTASKS + + /// + /// If the given exception is file IO related or Xml related return false. + /// Otherwise, return true. + /// + /// The exception to check. + internal static bool NotExpectedIoOrXmlException(Exception e) + { + if + ( + IsXmlException(e) + || !NotExpectedException(e) + ) + { + return false; + } + + return true; + } + + /// + /// If the given exception is reflection-related return false. + /// Otherwise, return true. + /// + /// The exception to check. + internal static bool NotExpectedReflectionException(Exception e) + { + // We are explicitly not handling TargetInvocationException. Those are just wrappers around + // exceptions thrown by the called code (such as a task or logger) which callers will typically + // want to treat differently. + if + ( + e is TypeLoadException // thrown when the common language runtime cannot find the assembly, the type within the assembly, or cannot load the type + || e is MethodAccessException // thrown when a class member is not found or access to the member is not permitted + || e is MissingMethodException // thrown when code in a dependent assembly attempts to access a missing method in an assembly that was modified + || e is MemberAccessException // thrown when a class member is not found or access to the member is not permitted + || e is BadImageFormatException // thrown when the file image of a DLL or an executable program is invalid + || e is ReflectionTypeLoadException // thrown by the Module.GetTypes method if any of the classes in a module cannot be loaded + || e is CustomAttributeFormatException // thrown if a custom attribute on a data type is formatted incorrectly + || e is TargetParameterCountException // thrown when the number of parameters for an invocation does not match the number expected + || e is InvalidCastException + || e is AmbiguousMatchException // thrown when binding to a member results in more than one member matching the binding criteria + || e is InvalidFilterCriteriaException // thrown in FindMembers when the filter criteria is not valid for the type of filter you are using + || e is TargetException // thrown when an attempt is made to invoke a non-static method on a null object. This may occur because the caller does not + // have access to the member, or because the target does not define the member, and so on. + || e is MissingFieldException // thrown when code in a dependent assembly attempts to access a missing field in an assembly that was modified. + || !NotExpectedException(e) // Reflection can throw IO exceptions if the assembly cannot be opened + + ) + { + return false; + } + + return true; + } + + /// + /// Serialization has been observed to throw TypeLoadException as + /// well as SerializationException and IO exceptions. (Obviously + /// it has to do reflection but it ought to be wrapping the exceptions.) + /// + internal static bool NotExpectedSerializationException(Exception e) + { + if + ( + e is SerializationException + || !NotExpectedReflectionException(e) + ) + { + return false; + } + + return true; + } + + /// + /// Returns false if this is a known exception thrown by the registry API. + /// + internal static bool NotExpectedRegistryException(Exception e) + { + if (e is SecurityException + || e is UnauthorizedAccessException + || e is IOException + || e is ObjectDisposedException + || e is ArgumentException) + { + return false; + } + + return true; + } + + /// + /// Returns false if this is a known exception thrown by function evaluation + /// + internal static bool NotExpectedFunctionException(Exception e) + { + if (e is InvalidCastException + || e is ArgumentNullException + || e is FormatException + || e is InvalidOperationException + || !NotExpectedReflectionException(e)) + { + return false; + } + + return true; + } + + /// + /// Dump any unhandled exceptions to a file so they can be diagnosed + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "It is called by the CLR")] + internal static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) + { + Exception ex = (Exception)e.ExceptionObject; + DumpExceptionToFile(ex); + } + + /// + /// Dump the exception information to a file + /// + [MethodImpl(MethodImplOptions.Synchronized)] + internal static void DumpExceptionToFile(Exception ex) + { + if (s_dumpFileName == null) + { + Guid guid = Guid.NewGuid(); + string tempPath = Path.GetTempPath(); + + // For some reason we get Watson buckets because GetTempPath gives us a folder here that doesn't exist. + // Either because %TMP% is misdefined, or because they deleted the temp folder during the build. + if (!Directory.Exists(tempPath)) + { + // If this throws, no sense catching it, we can't log it now, and we're here + // because we're a child node with no console to log to, so die + Directory.CreateDirectory(tempPath); + } + + s_dumpFileName = Path.Combine(tempPath, "MSBuild_" + guid.ToString() + ".failure.txt"); + + using (StreamWriter writer = new StreamWriter(s_dumpFileName, true /*append*/)) + { + writer.WriteLine("UNHANDLED EXCEPTIONS FROM PROCESS {0}:", Process.GetCurrentProcess().Id); + writer.WriteLine("====================="); + } + } + + using (StreamWriter writer = new StreamWriter(s_dumpFileName, true /*append*/)) + { + // "G" format is, e.g., 6/15/2008 9:15:07 PM + writer.WriteLine(DateTime.Now.ToString("G", CultureInfo.CurrentCulture)); + writer.WriteLine(ex.ToString()); + writer.WriteLine("==================="); + } + } +#endif + + /// Line and column pair. + internal struct LineAndColumn + { + /// Gets or sets line number. + internal int Line { get; set; } + + /// Gets or sets column position. + internal int Column { get; set; } + } + } +} diff --git a/src/Shared/ExtensionFoldersRegistryKey.cs b/src/Shared/ExtensionFoldersRegistryKey.cs new file mode 100644 index 00000000000..54bfeb7d0a9 --- /dev/null +++ b/src/Shared/ExtensionFoldersRegistryKey.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Contains a registry key string and some version information associated with it +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Shared; +using Microsoft.Win32; + +namespace Microsoft.Build.Shared +{ + /// + /// Contains information about entries in the AssemblyFoldersEx registry keys. + /// + internal class ExtensionFoldersRegistryKey + { + /// + /// Constructor + /// + internal ExtensionFoldersRegistryKey(string registryKey, Version targetFrameworkVersion) + { + ErrorUtilities.VerifyThrowArgumentNull(registryKey, "registryKey"); + ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkVersion, "targetFrameworkVersion"); + + RegistryKey = registryKey; + ComponentVersion = null; + TargetFrameworkVersion = targetFrameworkVersion; + } + + /// + /// Constructor + /// + internal ExtensionFoldersRegistryKey(string registryKey, Version componentVersion, Version targetFrameworkVersion) + { + ErrorUtilities.VerifyThrowArgumentNull(registryKey, "registryKey"); + ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkVersion, "targetFrameworkVersion"); + + RegistryKey = registryKey; + ComponentVersion = componentVersion; + TargetFrameworkVersion = targetFrameworkVersion; + } + + /// + /// The registry key to the component + /// + internal string RegistryKey + { + get; + private set; + } + + /// + /// Target framework version for the registry key + /// + internal Version ComponentVersion + { + get; + private set; + } + + /// + /// Target framework version for the registry key + /// + internal Version TargetFrameworkVersion + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/src/Shared/FileDelegates.cs b/src/Shared/FileDelegates.cs new file mode 100644 index 00000000000..5631a190a83 --- /dev/null +++ b/src/Shared/FileDelegates.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// delegate for System.IO.Directory.GetFiles, used for testing + /// + /// Directory path to start search for files in + /// pattern of files to match + /// string array of files which match search pattern + internal delegate string[] DirectoryGetFiles(string path, string searchPattern); + + /// + /// delegate for Directory.GetDirectories. + /// + /// The path to get directories for. + /// The pattern to search for. + /// An array of directories. + internal delegate string[] GetDirectories(string path, string pattern); + + /// + /// Delegate for System.IO.Directory.Exists + /// + /// Directory path to check if it exists + /// true if directory exists + internal delegate bool DirectoryExists(string path); + + /// + /// File exists delegate + /// + /// The path to check for existence. + /// 'true' if the file exists. + internal delegate bool FileExists(string path); + + /// + /// File.Copy delegate + /// + /// + /// + internal delegate void FileCopy(string source, string destination); + + /// + /// File.Delete delegate + /// + /// + internal delegate void FileDelete(string path); + + /// + /// File create delegate + /// + /// The path to create. + internal delegate FileStream FileCreate(string path); +} \ No newline at end of file diff --git a/src/Shared/FileMatcher.cs b/src/Shared/FileMatcher.cs new file mode 100644 index 00000000000..ce3316350b0 --- /dev/null +++ b/src/Shared/FileMatcher.cs @@ -0,0 +1,1300 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Functions for matching file names with patterns. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// Functions for matching file names with patterns. + /// + internal static class FileMatcher + { + private const string recursiveDirectoryMatch = "**"; + private const string dotdot = ".."; + private static readonly string s_directorySeparator = new string(Path.DirectorySeparatorChar, 1); + private static readonly string s_altDirectorySeparator = new string(Path.AltDirectorySeparatorChar, 1); + + private static readonly char[] s_wildcardCharacters = { '*', '?' }; + private static readonly char[] s_wildcardAndSemicolonCharacters = { '*', '?', ';' }; + internal static readonly char[] directorySeparatorCharacters = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + private static readonly GetFileSystemEntries s_defaultGetFileSystemEntries = new GetFileSystemEntries(GetAccessibleFileSystemEntries); + private static readonly DirectoryExists s_defaultDirectoryExists = new DirectoryExists(Directory.Exists); + + /// + /// Cache of the list of invalid path characters, because this method returns a clone (for security reasons) + /// which can cause significant transient allocations + /// + private static readonly char[] s_invalidPathChars = Path.GetInvalidPathChars(); + + /// + /// The type of entity that GetFileSystemEntries should return. + /// + internal enum FileSystemEntity + { + Files, + Directories, + FilesAndDirectories + }; + + /// + /// Delegate defines the GetFileSystemEntries signature that GetLongPathName uses + /// to enumerate directories on the file system. + /// + /// Files, Directories, or Files and Directories + /// The path to search. + /// The file pattern. + /// + /// + /// The array of filesystem entries. + internal delegate string[] GetFileSystemEntries(FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory); + + /// + /// Determines whether the given path has any wild card characters. + /// + /// + /// + internal static bool HasWildcards(string filespec) + { + return -1 != filespec.IndexOfAny(s_wildcardCharacters); + } + + /// + /// Determines whether the given path has any wild card characters or any semicolons. + /// + internal static bool HasWildcardsSemicolonItemOrPropertyReferences(string filespec) + { + return + ( + (-1 != filespec.IndexOfAny(s_wildcardAndSemicolonCharacters)) || + filespec.Contains("$(") || + filespec.Contains("@(") + ); + } + + /// + /// Get the files and\or folders specified by the given path and pattern. + /// + /// Whether Files, Directories or both. + /// The path to search. + /// The pattern to search. + /// The directory for the project within which the call is made + /// If true the project directory should be stripped + /// + private static string[] GetAccessibleFileSystemEntries(FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) + { + string[] files = null; + switch (entityType) + { + case FileSystemEntity.Files: files = GetAccessibleFiles(path, pattern, projectDirectory, stripProjectDirectory); break; + case FileSystemEntity.Directories: files = GetAccessibleDirectories(path, pattern); break; + case FileSystemEntity.FilesAndDirectories: files = GetAccessibleFilesAndDirectories(path, pattern); break; + default: + ErrorUtilities.VerifyThrow(false, "Unexpected filesystem entity type."); + break; + } + + return files; + } + + /// + /// Returns an array of file system entries matching the specified search criteria. Inaccessible or non-existent file + /// system entries are skipped. + /// + /// + /// + /// Array of matching file system entries (can be empty). + private static string[] GetAccessibleFilesAndDirectories(string path, string pattern) + { + string[] entries = null; + + if (Directory.Exists(path)) + { + try + { + entries = Directory.GetFileSystemEntries(path, pattern); + } + // for OS security + catch (UnauthorizedAccessException) + { + // do nothing + } + // for code access security + catch (System.Security.SecurityException) + { + // do nothing + } + } + + if (entries == null) + { + entries = new string[0]; + } + + return entries; + } + + /// + /// Same as Directory.GetFiles(...) except that files that + /// aren't accessible are skipped instead of throwing an exception. + /// + /// Other exceptions are passed through. + /// + /// The path. + /// The pattern. + /// The project directory + /// + /// Files that can be accessed. + private static string[] GetAccessibleFiles + ( + string path, + string filespec, // can be null + string projectDirectory, + bool stripProjectDirectory + ) + { + try + { + // look in current directory if no path specified + string dir = ((path.Length == 0) ? ".\\" : path); + + // get all files in specified directory, unless a file-spec has been provided + string[] files = (filespec == null) + ? Directory.GetFiles(dir) + : Directory.GetFiles(dir, filespec); + + // If the Item is based on a relative path we need to strip + // the current directory from the front + if (stripProjectDirectory) + { + RemoveProjectDirectory(files, projectDirectory); + } + // Files in the current directory are coming back with a ".\" + // prepended to them. We need to remove this; it breaks the + // IDE, which expects just the filename if it is in the current + // directory. But only do this if the original path requested + // didn't itself contain a ".\". + else if (!path.StartsWith(".\\", StringComparison.Ordinal)) + { + RemoveInitialDotSlash(files); + } + + return files; + } + catch (System.Security.SecurityException) + { + // For code access security. + return new string[0]; + } + catch (System.UnauthorizedAccessException) + { + // For OS security. + return new string[0]; + } + } + + /// + /// Same as Directory.GetDirectories(...) except that files that + /// aren't accessible are skipped instead of throwing an exception. + /// + /// Other exceptions are passed through. + /// + /// The path. + /// Pattern to match + /// Accessible directories. + private static string[] GetAccessibleDirectories + ( + string path, + string pattern + ) + { + try + { + string[] directories = null; + + if (pattern == null) + { + directories = Directory.GetDirectories((path.Length == 0) ? ".\\" : path); + } + else + { + directories = Directory.GetDirectories((path.Length == 0) ? ".\\" : path, pattern); + } + + // Subdirectories in the current directory are coming back with a ".\" + // prepended to them. We need to remove this; it breaks the + // IDE, which expects just the filename if it is in the current + // directory. But only do this if the original path requested + // didn't itself contain a ".\". + if (!path.StartsWith(".\\", StringComparison.Ordinal)) + { + RemoveInitialDotSlash(directories); + } + + return directories; + } + catch (System.Security.SecurityException) + { + // For code access security. + return new string[0]; + } + catch (System.UnauthorizedAccessException) + { + // For OS security. + return new string[0]; + } + } + + /// + /// Given a path name, get its long version. + /// + /// The short path. + /// The long path. + internal static string GetLongPathName + ( + string path + ) + { + return GetLongPathName(path, s_defaultGetFileSystemEntries); + } + + /// + /// Given a path name, get its long version. + /// + /// The short path. + /// Delegate. + /// The long path. + internal static string GetLongPathName + ( + string path, + GetFileSystemEntries getFileSystemEntries + ) + { + if (path.IndexOf("~", StringComparison.Ordinal) == -1) + { + // A path with no '~' must not be a short name. + return path; + } + + ErrorUtilities.VerifyThrow(!HasWildcards(path), + "GetLongPathName does not handle wildcards and was passed '{0}'.", path); + + string[] parts = path.Split(directorySeparatorCharacters); + string pathRoot; + int startingElement = 0; + + bool isUnc = path.StartsWith(s_directorySeparator + s_directorySeparator, StringComparison.Ordinal); + if (isUnc) + { + pathRoot = s_directorySeparator + s_directorySeparator; + pathRoot += parts[2]; + pathRoot += s_directorySeparator; + pathRoot += parts[3]; + pathRoot += s_directorySeparator; + startingElement = 4; + } + else + { + // Is it relative? + if (path.Length > 2 && path[1] == ':') + { + // Not relative + pathRoot = parts[0] + s_directorySeparator; + startingElement = 1; + } + else + { + // Relative + pathRoot = String.Empty; + startingElement = 0; + } + } + + // Build up an array of parts. These elements may be "" if there are + // extra slashes. + string[] longParts = new string[parts.Length - startingElement]; + + string longPath = pathRoot; + for (int i = startingElement; i < parts.Length; ++i) + { + // If there is a zero-length part, then that means there was an extra slash. + if (parts[i].Length == 0) + { + longParts[i - startingElement] = String.Empty; + } + else + { + if (parts[i].IndexOf("~", StringComparison.Ordinal) == -1) + { + // If there's no ~, don't hit the disk. + longParts[i - startingElement] = parts[i]; + longPath = Path.Combine(longPath, parts[i]); + } + else + { + // getFileSystemEntries(...) returns an empty array if longPath doesn't exist. + string[] entries = getFileSystemEntries(FileSystemEntity.FilesAndDirectories, longPath, parts[i], null, false); + + if (0 == entries.Length) + { + // The next part doesn't exist. Therefore, no more of the path will exist. + // Just return the rest. + for (int j = i; j < parts.Length; ++j) + { + longParts[j - startingElement] = parts[j]; + } + break; + } + + // Since we know there are no wild cards, this should be length one. + ErrorUtilities.VerifyThrow(entries.Length == 1, + "Unexpected number of entries ({3}) found when enumerating '{0}' under '{1}'. Original path was '{2}'", + parts[i], longPath, path, entries.Length); + + // Entries[0] contains the full path. + longPath = entries[0]; + + // We just want the trailing node. + longParts[i - startingElement] = Path.GetFileName(longPath); + } + } + } + + return pathRoot + String.Join(s_directorySeparator, longParts); + } + + /// + /// Given a filespec, split it into left-most 'fixed' dir part, middle 'wildcard' dir part, and filename part. + /// The filename part may have wildcard characters in it. + /// + /// The filespec to be decomposed. + /// Receives the fixed directory part. + /// The wildcard directory part. + /// The filename part. + /// Delegate. + internal static void SplitFileSpec + ( + string filespec, + out string fixedDirectoryPart, + out string wildcardDirectoryPart, + out string filenamePart, + GetFileSystemEntries getFileSystemEntries + ) + { + PreprocessFileSpecForSplitting + ( + filespec, + out fixedDirectoryPart, + out wildcardDirectoryPart, + out filenamePart + ); + + /* + * Handle the special case in which filenamePart is '**'. + * In this case, filenamePart becomes '*.*' and the '**' is appended + * to the end of the wildcardDirectory part. + * This is so that later regular expression matching can accurately + * pull out the different parts (fixed, wildcard, filename) of given + * file specs. + */ + if (recursiveDirectoryMatch == filenamePart) + { + wildcardDirectoryPart += recursiveDirectoryMatch; + wildcardDirectoryPart += s_directorySeparator; + filenamePart = "*.*"; + } + + fixedDirectoryPart = FileMatcher.GetLongPathName(fixedDirectoryPart, getFileSystemEntries); + } + + /// + /// Do most of the grunt work of splitting the filespec into parts. + /// Does not handle post-processing common to the different matching + /// paths. + /// + /// The filespec to be decomposed. + /// Receives the fixed directory part. + /// The wildcard directory part. + /// The filename part. + private static void PreprocessFileSpecForSplitting + ( + string filespec, + out string fixedDirectoryPart, + out string wildcardDirectoryPart, + out string filenamePart + ) + { + int indexOfLastDirectorySeparator = filespec.LastIndexOfAny(directorySeparatorCharacters); + if (-1 == indexOfLastDirectorySeparator) + { + /* + * No dir separator found. This is either this form, + * + * Source.cs + * *.cs + * + * or this form, + * + * ** + */ + fixedDirectoryPart = String.Empty; + wildcardDirectoryPart = String.Empty; + filenamePart = filespec; + return; + } + + int indexOfFirstWildcard = filespec.IndexOfAny(s_wildcardCharacters); + if + ( + -1 == indexOfFirstWildcard + || indexOfFirstWildcard > indexOfLastDirectorySeparator + ) + { + /* + * There is at least one dir separator, but either there is no wild card or the + * wildcard is after the dir separator. + * + * The form is one of these: + * + * dir1\Source.cs + * dir1\*.cs + * + * Where the trailing spec is meant to be a filename. Or, + * + * dir1\** + * + * Where the trailing spec is meant to be any file recursively. + */ + + // We know the fixed director part now. + fixedDirectoryPart = filespec.Substring(0, indexOfLastDirectorySeparator + 1); + wildcardDirectoryPart = String.Empty; + filenamePart = filespec.Substring(indexOfLastDirectorySeparator + 1); + return; + } + + /* + * Find the separator right before the first wildcard. + */ + string filespecLeftOfWildcard = filespec.Substring(0, indexOfFirstWildcard); + int indexOfSeparatorBeforeWildCard = filespecLeftOfWildcard.LastIndexOfAny(directorySeparatorCharacters); + if (-1 == indexOfSeparatorBeforeWildCard) + { + /* + * There is no separator before the wildcard, so the form is like this: + * + * dir?\Source.cs + * + * or this, + * + * dir?\** + */ + fixedDirectoryPart = String.Empty; + wildcardDirectoryPart = filespec.Substring(0, indexOfLastDirectorySeparator + 1); + filenamePart = filespec.Substring(indexOfLastDirectorySeparator + 1); + return; + } + + /* + * There is at least one wildcard and one dir separator, split parts out. + */ + fixedDirectoryPart = filespec.Substring(0, indexOfSeparatorBeforeWildCard + 1); + wildcardDirectoryPart = filespec.Substring(indexOfSeparatorBeforeWildCard + 1, indexOfLastDirectorySeparator - indexOfSeparatorBeforeWildCard); + filenamePart = filespec.Substring(indexOfLastDirectorySeparator + 1); + } + + /// + /// Removes the leading ".\" from all of the paths in the array. + /// + /// Paths to remove .\ from. + private static void RemoveInitialDotSlash + ( + string[] paths + ) + { + for (int i = 0; i < paths.Length; i++) + { + if (paths[i].StartsWith(".\\", StringComparison.Ordinal)) + { + paths[i] = paths[i].Substring(2); + } + } + } + + + /// + /// Checks if the char is a DirectorySeparatorChar or a AltDirectorySeparatorChar + /// + /// + /// + internal static bool IsDirectorySeparator(char c) + { + return (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar); + } + /// + /// Removes the current directory converting the file back to relative path + /// + /// Paths to remove current directory from. + /// + internal static void RemoveProjectDirectory + ( + string[] paths, + string projectDirectory + ) + { + bool directoryLastCharIsSeparator = IsDirectorySeparator(projectDirectory[projectDirectory.Length - 1]); + for (int i = 0; i < paths.Length; i++) + { + if (paths[i].StartsWith(projectDirectory, StringComparison.Ordinal)) + { + // If the project directory did not end in a slash we need to check to see if the next char in the path is a slash + if (!directoryLastCharIsSeparator) + { + //If the next char after the project directory is not a slash, skip this path + if (paths[i].Length <= projectDirectory.Length || !IsDirectorySeparator(paths[i][projectDirectory.Length])) + { + continue; + } + paths[i] = paths[i].Substring(projectDirectory.Length + 1); + } + else + { + paths[i] = paths[i].Substring(projectDirectory.Length); + } + } + } + } + + /// + /// Get all files that match either the file-spec or the regular expression. + /// + /// List of files that gets populated. + /// The path to enumerate + /// The remaining, wildcard part of the directory. + /// The filespec. + /// + /// Wild-card matching. + /// If true, then recursion is required. + /// + /// + /// Delegate. + private static void GetFilesRecursive + ( + System.Collections.IList listOfFiles, + string baseDirectory, + string remainingWildcardDirectory, + string filespec, // can be null + int extensionLengthToEnforce, // only relevant when filespec is not null + Regex regexFileMatch, // can be null + bool needsRecursion, + string projectDirectory, + bool stripProjectDirectory, + GetFileSystemEntries getFileSystemEntries + ) + { + ErrorUtilities.VerifyThrow((filespec == null) || (regexFileMatch == null), + "File-spec overrides the regular expression -- pass null for file-spec if you want to use the regular expression."); + + ErrorUtilities.VerifyThrow((filespec != null) || (regexFileMatch != null), + "Need either a file-spec or a regular expression to match files."); + + ErrorUtilities.VerifyThrow(remainingWildcardDirectory != null, "Expected non-null remaning wildcard directory."); + + /* + * Get the matching files. + */ + bool considerFiles = false; + + // Only consider files if... + if (remainingWildcardDirectory.Length == 0) + { + // We've reached the end of the wildcard directory elements. + considerFiles = true; + } + else if (remainingWildcardDirectory.IndexOf(recursiveDirectoryMatch, StringComparison.Ordinal) == 0) + { + // or, we've reached a "**" so everything else is matched recursively. + considerFiles = true; + } + + if (considerFiles) + { + string[] files = getFileSystemEntries(FileSystemEntity.Files, baseDirectory, filespec, projectDirectory, stripProjectDirectory); + foreach (string file in files) + { + if ((filespec != null) || + // if no file-spec provided, match the file to the regular expression + // PERF NOTE: Regex.IsMatch() is an expensive operation, so we avoid it whenever possible + regexFileMatch.IsMatch(file)) + { + if ((filespec == null) || + // if we used a file-spec with a "loosely" defined extension + (extensionLengthToEnforce == 0) || + // discard all files that do not have extensions of the desired length + (Path.GetExtension(file).Length == extensionLengthToEnforce)) + { + listOfFiles.Add((object)file); + } + } + } + } + + /* + * Recurse into subdirectories. + */ + if (needsRecursion && remainingWildcardDirectory.Length > 0) + { + // Find the next directory piece. + string pattern = null; + + if (remainingWildcardDirectory != recursiveDirectoryMatch) + { + int indexOfNextSlash = remainingWildcardDirectory.IndexOfAny(directorySeparatorCharacters); + ErrorUtilities.VerifyThrow(indexOfNextSlash != -1, "Slash should be guaranteed."); + + // Peel off the leftmost directory piece. So for example, if remainingWildcardDirectory + // contains: + // + // ?emp\foo\**\bar + // + // then put '?emp' into pattern. Then put the remaining part, + // + // foo\**\bar + // + // back into remainingWildcardDirectory. + // This is a performance optimization. We don't want to enumerate everything if we + // don't have to. + pattern = remainingWildcardDirectory.Substring(0, indexOfNextSlash); + remainingWildcardDirectory = remainingWildcardDirectory.Substring(indexOfNextSlash + 1); + + // If pattern turned into **, then there's no choice but to enumerate everything. + if (pattern == recursiveDirectoryMatch) + { + pattern = null; + remainingWildcardDirectory = recursiveDirectoryMatch; + } + } + + // We never want to strip the project directory from the leaves, because the current + // process directory maybe different + string[] subdirs = getFileSystemEntries(FileSystemEntity.Directories, baseDirectory, pattern, null, false); + foreach (string subdir in subdirs) + { + GetFilesRecursive(listOfFiles, subdir, remainingWildcardDirectory, filespec, extensionLengthToEnforce, regexFileMatch, true, projectDirectory, stripProjectDirectory, getFileSystemEntries); + } + } + } + + /// + /// Given a file spec, create a regular expression that will match that + /// file spec. + /// + /// PERF WARNING: this method is called in performance-critical + /// scenarios, so keep it fast and cheap + /// + /// The fixed directory part. + /// The wildcard directory part. + /// The filename part. + /// Receives whether this pattern is legal or not. + /// The regular expression string. + private static string RegularExpressionFromFileSpec + ( + string fixedDirectoryPart, + string wildcardDirectoryPart, + string filenamePart, + out bool isLegalFileSpec + ) + { + isLegalFileSpec = true; + + /* + * The code below uses tags in the form <:tag:> to encode special information + * while building the regular expression. + * + * This format was chosen because it's not a legal form for filespecs. If the + * filespec comes in with either "<:" or ":>", return isLegalFileSpec=false to + * prevent intrusion into the special processing. + */ + if ((fixedDirectoryPart.IndexOf("<:", StringComparison.Ordinal) != -1) || + (fixedDirectoryPart.IndexOf(":>", StringComparison.Ordinal) != -1) || + (wildcardDirectoryPart.IndexOf("<:", StringComparison.Ordinal) != -1) || + (wildcardDirectoryPart.IndexOf(":>", StringComparison.Ordinal) != -1) || + (filenamePart.IndexOf("<:", StringComparison.Ordinal) != -1) || + (filenamePart.IndexOf(":>", StringComparison.Ordinal) != -1)) + { + isLegalFileSpec = false; + return String.Empty; + } + + /* + * Its not legal for there to be a ".." after a wildcard. + */ + if (wildcardDirectoryPart.Contains(dotdot)) + { + isLegalFileSpec = false; + return String.Empty; + } + + /* + * Trailing dots in file names have to be treated specially. + * We want: + * + * *. to match foo + * + * but 'foo' doesn't have a trailing '.' so we need to handle this while still being careful + * not to match 'foo.txt' + */ + if (filenamePart.EndsWith(".", StringComparison.Ordinal)) + { + filenamePart = filenamePart.Replace("*", "<:anythingbutdot:>"); + filenamePart = filenamePart.Replace("?", "<:anysinglecharacterbutdot:>"); + filenamePart = filenamePart.Substring(0, filenamePart.Length - 1); + } + + /* + * Now, build up the starting filespec but put tags in to identify where the fixedDirectory, + * wildcardDirectory and filenamePart are. Also tag the beginning of the line and the end of + * the line, so that we can identify patterns by whether they're on one end or the other. + */ + StringBuilder matchFileExpression = new StringBuilder(); + matchFileExpression.Append("<:bol:>"); + matchFileExpression.Append("<:fixeddir:>").Append(fixedDirectoryPart).Append("<:endfixeddir:>"); + matchFileExpression.Append("<:wildcarddir:>").Append(wildcardDirectoryPart).Append("<:endwildcarddir:>"); + matchFileExpression.Append("<:filename:>").Append(filenamePart).Append("<:endfilename:>"); + matchFileExpression.Append("<:eol:>"); + + /* + * Call out our special matching characters. + */ + matchFileExpression.Replace(s_directorySeparator, "<:dirseparator:>"); + matchFileExpression.Replace(s_altDirectorySeparator, "<:dirseparator:>"); + + /* + * Capture the leading \\ in UNC paths, so that the doubled slash isn't + * reduced in a later step. + */ + matchFileExpression.Replace("<:fixeddir:><:dirseparator:><:dirseparator:>", "<:fixeddir:><:uncslashslash:>"); + + /* + * Iteratively reduce four cases involving directory separators + * + * (1) <:dirseparator:>.<:dirseparator:> -> <:dirseparator:> + * This is an identity, so for example, these two are equivalent, + * + * dir1\.\dir2 == dir1\dir2 + * + * (2) <:dirseparator:><:dirseparator:> -> <:dirseparator:> + * Double directory separators are treated as a single directory separator, + * so, for example, this is an identity: + * + * f:\dir1\\dir2 == f:\dir1\dir2 + * + * The single exemption is for UNC path names, like this: + * + * \\server\share != \server\share + * + * This case is handled by the <:uncslashslash:> which was substituted in + * a prior step. + * + * (3) <:fixeddir:>.<:dirseparator:>.<:dirseparator:> -> <:fixeddir:>.<:dirseparator:> + * A ".\" at the beginning of a line is equivalent to nothing, so: + * + * .\.\dir1\file.txt == .\dir1\file.txt + * + * (4) <:dirseparator:>.<:eol:> -> <:eol:> + * A "\." at the end of a line is equivalent to nothing, so: + * + * dir1\dir2\. == dir1\dir2 * + */ + int sizeBefore; + do + { + sizeBefore = matchFileExpression.Length; + + // NOTE: all these replacements will necessarily reduce the expression length i.e. length will either reduce or + // stay the same through this loop + matchFileExpression.Replace("<:dirseparator:>.<:dirseparator:>", "<:dirseparator:>"); + matchFileExpression.Replace("<:dirseparator:><:dirseparator:>", "<:dirseparator:>"); + matchFileExpression.Replace("<:fixeddir:>.<:dirseparator:>.<:dirseparator:>", "<:fixeddir:>.<:dirseparator:>"); + matchFileExpression.Replace("<:dirseparator:>.<:endfilename:>", "<:endfilename:>"); + matchFileExpression.Replace("<:filename:>.<:endfilename:>", "<:filename:><:endfilename:>"); + + ErrorUtilities.VerifyThrow(matchFileExpression.Length <= sizeBefore, + "Expression reductions cannot increase the length of the expression."); + } while (matchFileExpression.Length < sizeBefore); + + /* + * Collapse **\** into **. + */ + do + { + sizeBefore = matchFileExpression.Length; + matchFileExpression.Replace(recursiveDirectoryMatch + "<:dirseparator:>" + recursiveDirectoryMatch, recursiveDirectoryMatch); + + ErrorUtilities.VerifyThrow(matchFileExpression.Length <= sizeBefore, + "Expression reductions cannot increase the length of the expression."); + } while (matchFileExpression.Length < sizeBefore); + + /* + * Call out legal recursion operators: + * + * fixed-directory + **\ + * \**\ + * **\** + * + */ + do + { + sizeBefore = matchFileExpression.Length; + matchFileExpression.Replace("<:dirseparator:>" + recursiveDirectoryMatch + "<:dirseparator:>", "<:middledirs:>"); + matchFileExpression.Replace("<:wildcarddir:>" + recursiveDirectoryMatch + "<:dirseparator:>", "<:wildcarddir:><:leftdirs:>"); + + ErrorUtilities.VerifyThrow(matchFileExpression.Length <= sizeBefore, + "Expression reductions cannot increase the length of the expression."); + } while (matchFileExpression.Length < sizeBefore); + + + /* + * By definition, "**" must appear alone between directory slashes. If there is any remaining "**" then this is not + * a valid filespec. + */ + // NOTE: this condition is evaluated left-to-right -- this is important because we want the length BEFORE stripping + // any "**"s remaining in the expression + if (matchFileExpression.Length > matchFileExpression.Replace(recursiveDirectoryMatch, null).Length) + { + isLegalFileSpec = false; + return String.Empty; + } + + /* + * Remaining call-outs not involving "**" + */ + matchFileExpression.Replace("*.*", "<:anynonseparator:>"); + matchFileExpression.Replace("*", "<:anynonseparator:>"); + matchFileExpression.Replace("?", "<:singlecharacter:>"); + + /* + * Escape all special characters defined for regular expresssions. + */ + matchFileExpression.Replace("\\", "\\\\"); // Must be first. + matchFileExpression.Replace("$", "\\$"); + matchFileExpression.Replace("(", "\\("); + matchFileExpression.Replace(")", "\\)"); + matchFileExpression.Replace("*", "\\*"); + matchFileExpression.Replace("+", "\\+"); + matchFileExpression.Replace(".", "\\."); + matchFileExpression.Replace("[", "\\["); + matchFileExpression.Replace("?", "\\?"); + matchFileExpression.Replace("^", "\\^"); + matchFileExpression.Replace("{", "\\{"); + matchFileExpression.Replace("|", "\\|"); + + /* + * Now, replace call-outs with their regex equivalents. + */ + matchFileExpression.Replace("<:middledirs:>", "((/)|(\\\\)|(/.*/)|(/.*\\\\)|(\\\\.*\\\\)|(\\\\.*/))"); + matchFileExpression.Replace("<:leftdirs:>", "((.*/)|(.*\\\\)|())"); + matchFileExpression.Replace("<:rightdirs:>", ".*"); + matchFileExpression.Replace("<:anything:>", ".*"); + matchFileExpression.Replace("<:anythingbutdot:>", "[^\\.]*"); + matchFileExpression.Replace("<:anysinglecharacterbutdot:>", "[^\\.]."); + matchFileExpression.Replace("<:anynonseparator:>", "[^/\\\\]*"); + matchFileExpression.Replace("<:singlecharacter:>", "."); + matchFileExpression.Replace("<:dirseparator:>", "[/\\\\]+"); + matchFileExpression.Replace("<:uncslashslash:>", @"\\\\"); + matchFileExpression.Replace("<:bol:>", "^"); + matchFileExpression.Replace("<:eol:>", "$"); + matchFileExpression.Replace("<:fixeddir:>", "(?"); + matchFileExpression.Replace("<:endfixeddir:>", ")"); + matchFileExpression.Replace("<:wildcarddir:>", "(?"); + matchFileExpression.Replace("<:endwildcarddir:>", ")"); + matchFileExpression.Replace("<:filename:>", "(?"); + matchFileExpression.Replace("<:endfilename:>", ")"); + + return matchFileExpression.ToString(); + } + + + + /// + /// Given a filespec, get the information needed for file matching. + /// + /// The filespec. + /// Receives the regular expression. + /// Receives the flag that is true if recursion is required. + /// Receives the flag that is true if the filespec is legal. + /// Delegate. + internal static void GetFileSpecInfo + ( + string filespec, + out Regex regexFileMatch, + out bool needsRecursion, + out bool isLegalFileSpec, + GetFileSystemEntries getFileSystemEntries + + ) + { + string fixedDirectoryPart; + string wildcardDirectoryPart; + string filenamePart; + string matchFileExpression; + + GetFileSpecInfo(filespec, + out fixedDirectoryPart, out wildcardDirectoryPart, out filenamePart, + out matchFileExpression, out needsRecursion, out isLegalFileSpec, + getFileSystemEntries); + + if (isLegalFileSpec) + { + regexFileMatch = new Regex(matchFileExpression, RegexOptions.IgnoreCase); + } + else + { + regexFileMatch = null; + } + } + + /// + /// Given a filespec, get the information needed for file matching. + /// + /// The filespec. + /// Receives the fixed directory part. + /// Receives the wildcard directory part. + /// Receives the filename part. + /// Receives the regular expression. + /// Receives the flag that is true if recursion is required. + /// Receives the flag that is true if the filespec is legal. + /// Delegate. + private static void GetFileSpecInfo + ( + string filespec, + out string fixedDirectoryPart, + out string wildcardDirectoryPart, + out string filenamePart, + out string matchFileExpression, + out bool needsRecursion, + out bool isLegalFileSpec, + GetFileSystemEntries getFileSystemEntries + ) + { + isLegalFileSpec = true; + needsRecursion = false; + fixedDirectoryPart = String.Empty; + wildcardDirectoryPart = String.Empty; + filenamePart = String.Empty; + matchFileExpression = null; + + // bail out if filespec contains illegal characters + if (-1 != filespec.IndexOfAny(s_invalidPathChars)) + { + isLegalFileSpec = false; + return; + } + + /* + * Check for patterns in the filespec that are explicitly illegal. + * + * Any path with "..." in it is illegal. + */ + if (-1 != filespec.IndexOf("...", StringComparison.Ordinal)) + { + isLegalFileSpec = false; + return; + } + + /* + * If there is a ':' anywhere but the second character, this is an illegal pattern. + * Catches this case among others, + * + * http://www.website.com + * + */ + int rightmostColon = filespec.LastIndexOf(":", StringComparison.Ordinal); + + if + ( + -1 != rightmostColon + && 1 != rightmostColon + ) + { + isLegalFileSpec = false; + return; + } + + /* + * Now break up the filespec into constituent parts--fixed, wildcard and filename. + */ + SplitFileSpec(filespec, out fixedDirectoryPart, out wildcardDirectoryPart, out filenamePart, getFileSystemEntries); + + /* + * Get a regular expression for matching files that will be found. + */ + matchFileExpression = RegularExpressionFromFileSpec(fixedDirectoryPart, wildcardDirectoryPart, filenamePart, out isLegalFileSpec); + + /* + * Was the filespec valid? If not, then just return now. + */ + if (!isLegalFileSpec) + { + return; + } + + /* + * Determine whether recursion will be required. + */ + needsRecursion = (wildcardDirectoryPart.Length != 0); + } + + /// + /// The results of a match between a filespec and a file name. + /// + internal sealed class Result + { + /// + /// Default constructor. + /// + internal Result() + { + // do nothing + } + + internal bool isLegalFileSpec; // initially false + internal bool isMatch; // initially false + internal bool isFileSpecRecursive; // initially false + internal string fixedDirectoryPart = String.Empty; + internal string wildcardDirectoryPart = String.Empty; + internal string filenamePart = String.Empty; + } + + /// + /// Given a pattern (filespec) and a candidate filename (fileToMatch) + /// return matching information. + /// + /// The filespec. + /// The candidate to match against. + /// The result class. + internal static Result FileMatch + ( + string filespec, + string fileToMatch + ) + { + Result matchResult = new Result(); + + fileToMatch = GetLongPathName(fileToMatch, s_defaultGetFileSystemEntries); + + Regex regexFileMatch; + GetFileSpecInfo + ( + filespec, + out regexFileMatch, + out matchResult.isFileSpecRecursive, + out matchResult.isLegalFileSpec, + s_defaultGetFileSystemEntries + ); + + if (matchResult.isLegalFileSpec) + { + Match match = regexFileMatch.Match(fileToMatch); + matchResult.isMatch = match.Success; + + if (matchResult.isMatch) + { + matchResult.fixedDirectoryPart = match.Groups["FIXEDDIR"].Value; + matchResult.wildcardDirectoryPart = match.Groups["WILDCARDDIR"].Value; + matchResult.filenamePart = match.Groups["FILENAME"].Value; + } + } + + return matchResult; + } + + /// + /// Given a filespec, find the files that match. + /// Will never throw IO exceptions: if there is no match, returns the input verbatim. + /// + /// The project directory. + /// Get files that match the given file spec. + /// The array of files. + internal static string[] GetFiles + ( + string projectDirectoryUnescaped, + string filespecUnescaped + ) + { + string[] files = GetFiles(projectDirectoryUnescaped, filespecUnescaped, s_defaultGetFileSystemEntries, s_defaultDirectoryExists); + return files; + } + + /// + /// Given a filespec, find the files that match. + /// Will never throw IO exceptions: if there is no match, returns the input verbatim. + /// + /// The project directory. + /// Get files that match the given file spec. + /// Get files that match the given file spec. + /// Determine whether a directory exists. + /// The array of files. + internal static string[] GetFiles + ( + string projectDirectoryUnescaped, + string filespecUnescaped, + GetFileSystemEntries getFileSystemEntries, + DirectoryExists directoryExists + ) + { + // For performance. Short-circuit iff there is no wildcard. + // Perf Note: Doing a [Last]IndexOfAny(...) is much faster than compiling a + // regular expression that does the same thing, regardless of whether + // filespec contains one of the characters. + // Choose LastIndexOfAny instead of IndexOfAny because it seems more likely + // that wildcards will tend to be towards the right side. + if (!HasWildcards(filespecUnescaped)) + { + return new string[] { filespecUnescaped }; + } + + // UNDONE (perf): Short circuit the complex processing when we only have a path and a wildcarded filename + + /* + * Even though we return a string[] we work internally with an IList. + * This is because it's cheaper to add items to an IList and this code + * might potentially do a lot of that. + */ + System.Collections.ArrayList arrayListOfFiles = new System.Collections.ArrayList(); + System.Collections.IList listOfFiles = (System.Collections.IList)arrayListOfFiles; + + /* + * Analyze the file spec and get the information we need to do the matching. + */ + string fixedDirectoryPart; + string wildcardDirectoryPart; + string filenamePart; + string matchFileExpression; + bool needsRecursion; + bool isLegalFileSpec; + GetFileSpecInfo + ( + filespecUnescaped, + out fixedDirectoryPart, + out wildcardDirectoryPart, + out filenamePart, + out matchFileExpression, + out needsRecursion, + out isLegalFileSpec, + getFileSystemEntries + ); + + /* + * If the filespec is invalid, then just return now. + */ + if (!isLegalFileSpec) + { + return new string[] { filespecUnescaped }; + } + + // The projectDirectory is not null only if we are running the evaluation from + // inside the engine (i.e. not from a task) + bool stripProjectDirectory = false; + if (projectDirectoryUnescaped != null) + { + if (fixedDirectoryPart != null) + { + string oldFixedDirectoryPart = fixedDirectoryPart; + try + { + fixedDirectoryPart = Path.Combine(projectDirectoryUnescaped, fixedDirectoryPart); + } + catch (ArgumentException) + { + return new string[0]; + } + + stripProjectDirectory = !String.Equals(fixedDirectoryPart, oldFixedDirectoryPart, StringComparison.OrdinalIgnoreCase); + } + else + { + fixedDirectoryPart = projectDirectoryUnescaped; + stripProjectDirectory = true; + } + } + + /* + * If the fixed directory part doesn't exist, then this means no files should be + * returned. + */ + if (fixedDirectoryPart.Length > 0 && !directoryExists(fixedDirectoryPart)) + { + return new string[0]; + } + + // determine if we need to use the regular expression to match the files + // PERF NOTE: Constructing a Regex object is expensive, so we avoid it whenever possible + bool matchWithRegex = + // if we have a directory specification that uses wildcards, and + (wildcardDirectoryPart.Length > 0) && + // the specification is not a simple "**" + (wildcardDirectoryPart != (recursiveDirectoryMatch + s_directorySeparator)); + // then we need to use the regular expression + + // if we're not using the regular expression, get the file pattern extension + string extensionPart = matchWithRegex + ? null + : Path.GetExtension(filenamePart); + + // check if the file pattern would cause Windows to match more loosely on the extension + // NOTE: Windows matches loosely in two cases (in the absence of the * wildcard in the extension): + // 1) if the extension ends with the ? wildcard, it matches files with shorter extensions also e.g. "file.tx?" would + // match both "file.txt" and "file.tx" + // 2) if the extension is three characters, and the filename contains the * wildcard, it matches files with longer + // extensions that start with the same three characters e.g. "*.htm" would match both "file.htm" and "file.html" + bool needToEnforceExtensionLength = + (extensionPart != null) && + (extensionPart.IndexOf('*') == -1) + && + (extensionPart.EndsWith("?", StringComparison.Ordinal) + || + ((extensionPart.Length == (3 + 1 /* +1 for the period */)) && + (filenamePart.IndexOf('*') != -1))); + + /* + * Now get the files that match, starting at the lowest fixed directory. + */ + try + { + GetFilesRecursive(listOfFiles, fixedDirectoryPart, wildcardDirectoryPart, + // if using the regular expression, ignore the file pattern + (matchWithRegex ? null : filenamePart), (needToEnforceExtensionLength ? extensionPart.Length : 0), + // if using the file pattern, ignore the regular expression + (matchWithRegex ? new Regex(matchFileExpression, RegexOptions.IgnoreCase) : null), + needsRecursion, projectDirectoryUnescaped, stripProjectDirectory, getFileSystemEntries); + } + catch (Exception ex) + { + if (!ExceptionHandling.IsIoRelatedException(ex)) + { + throw; + } + + // Assume it's not meant to be a path + return new[] { filespecUnescaped }; + } + + /* + * Build the return array. + */ + string[] files = (string[])arrayListOfFiles.ToArray(typeof(string)); + return files; + } + } +} \ No newline at end of file diff --git a/src/Shared/FileUtilities.cs b/src/Shared/FileUtilities.cs new file mode 100644 index 00000000000..1c6da527b4b --- /dev/null +++ b/src/Shared/FileUtilities.cs @@ -0,0 +1,934 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Security; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Text; +using System.Threading; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Build.Collections; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for file IO. + /// PERF\COVERAGE NOTE: Try to keep classes in 'shared' as granular as possible. All the methods in + /// each class get pulled into the resulting assembly. + /// + static internal partial class FileUtilities + { + // A list of possible test runners. If the program running has one of these substrings in the name, we assume + // this is a test harness. + private static readonly string[] s_testRunners = { + "NUNIT", "DEVENV", "MSTEST", "VSTEST", "TASKRUNNER", "VSTESTHOST", + "QTAGENT32", "CONCURRENT", "RESHARPER", "MDHOST", "TE.PROCESSHOST" + }; + + // This flag, when set, indicates that we are running tests. Initially assume it's true. It also implies that + // the currentExecutableOverride is set to a path (that is non-null). Assume this is not initialized when we + // have the impossible combination of runningTests = false and currentExecutableOverride = null. + private static bool s_runningTests = true; + + // This is the fake current executable we use in case we are running tests. + private static string s_currentExecutableOverride = null; + + // MaxPath accounts for the null-terminating character, for example, the maximum path on the D drive is "D:\<256 chars>\0". + // See: ndp\clr\src\BCL\System\IO\Path.cs + internal const int MaxPath = 260; + + /// + /// The directory where MSBuild stores cache information used during the build. + /// + internal static string cacheDirectory = null; + + /// + /// Check if we are running unit tests (under some kind of test runner). If so, set the flag and come up with a + /// (potentially) fake executable path. Generally, the path will be used to find the config file, but also to + /// start msbuild.exe for remote nodes. + /// + private static void GetTestExecutionInfo() + { + // Get the executable we are running + var program = Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0]); + + // Check if it matches the pattern + s_runningTests = program != null && + s_testRunners.Any(s => program.IndexOf(s, StringComparison.InvariantCultureIgnoreCase) == -1); + + // Does not look like it's a test, but check the process name + if (!s_runningTests) + { + program = Process.GetCurrentProcess().ProcessName; + s_runningTests = + s_testRunners.Any(s => program.IndexOf(s, StringComparison.InvariantCultureIgnoreCase) == -1); + } + + // Definitely not a test, leave + if (!s_runningTests) + { + s_currentExecutableOverride = null; + return; + } + + // We are running test harness. Pretend instead that we are running msbuild.exe. + // See if the path is provided. + s_currentExecutableOverride = Environment.GetEnvironmentVariable("MSBUILD_EXE_PATH"); + + if (s_currentExecutableOverride == null) + { + // Try to find msbuild.exe. Assume it's where the current assembly is + var dir = ExecutingAssemblyPath; + if (dir == null) + { + // Can't get the assembly path, use current directory + dir = Environment.CurrentDirectory; + } + else + { + // Get directory name from the assembly and make sure it does not end with a slash + var path = Path.GetDirectoryName(dir); + + // The result may be null if we were looking at a drive root. Strange, but keep it + // if it was the drive root + dir = (path ?? dir).TrimEnd(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + } + + // The executable is msbuild.exe. This should come up with a valid path to msbuild.exe, but + // no need to check it here. + s_currentExecutableOverride = Path.Combine(dir, "MSBuild.exe"); + } + } + + /// + /// FOR UNIT TESTS ONLY + /// Clear out the static variable used for the cache directory so that tests that + /// modify it can validate their modifications. + /// + internal static void ClearCacheDirectoryPath() + { + cacheDirectory = null; + } + + /// + /// Retrieves the MSBuild runtime cache directory + /// + internal static string GetCacheDirectory() + { + if (cacheDirectory == null) + { + cacheDirectory = Path.Combine(Path.GetTempPath(), String.Format(Thread.CurrentThread.CurrentUICulture, "MSBuild{0}", Process.GetCurrentProcess().Id)); + } + + return cacheDirectory; + } + + /// + /// Get the hex hash string for the string + /// + internal static string GetHexHash(string stringToHash) + { + return stringToHash.GetHashCode().ToString("X", CultureInfo.InvariantCulture); + } + + /// + /// Get the hash for the assemblyPaths + /// + internal static int GetPathsHash(IEnumerable assemblyPaths) + { + StringBuilder builder = new StringBuilder(); + + foreach (string path in assemblyPaths) + { + if (path != null) + { + string directoryPath = path.Trim(); + if (directoryPath.Length > 0) + { + DateTime lastModifiedTime; + if (NativeMethodsShared.GetLastWriteDirectoryUtcTime(directoryPath, out lastModifiedTime)) + { + builder.Append(lastModifiedTime.Ticks); + builder.Append('|'); + builder.Append(directoryPath.ToUpperInvariant()); + builder.Append('|'); + } + } + } + } + + return builder.ToString().GetHashCode(); + } + + /// + /// Clears the MSBuild runtime cache + /// + internal static void ClearCacheDirectory() + { + string cacheDirectory = GetCacheDirectory(); + + if (Directory.Exists(cacheDirectory)) + { + DeleteDirectoryNoThrow(cacheDirectory, true); + } + } + + /// + /// If the given path doesn't have a trailing slash then add one. + /// If the path is an empty string, does not modify it. + /// + /// The path to check. + /// A path with a slash. + internal static string EnsureTrailingSlash(string fileSpec) + { + if (fileSpec.Length > 0 && !EndsWithSlash(fileSpec)) + { + fileSpec += Path.DirectorySeparatorChar; + } + + return fileSpec; + } + + /// + /// Ensures the path does not have a leading slash. + /// + internal static string EnsureNoLeadingSlash(string path) + { + if (path.Length > 0 && IsSlash(path[0])) + { + path = path.Substring(1); + } + + return path; + } + + /// + /// Ensures the path does not have a trailing slash. + /// + internal static string EnsureNoTrailingSlash(string path) + { + if (EndsWithSlash(path)) + { + path = path.Substring(0, path.Length - 1); + } + + return path; + } + + /// + /// Indicates if the given file-spec ends with a slash. + /// + /// The file spec. + /// true, if file-spec has trailing slash + internal static bool EndsWithSlash(string fileSpec) + { + return (fileSpec.Length > 0) + ? IsSlash(fileSpec[fileSpec.Length - 1]) + : false; + } + + /// + /// Indicates if the given character is a slash. + /// + /// + /// true, if slash + internal static bool IsSlash(char c) + { + return ((c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar)); + } + + /// + /// Trims the string and removes any double quotes around it. + /// + internal static string TrimAndStripAnyQuotes(string path) + { + // Trim returns the same string if trimming isn't needed + path = path.Trim(); + path = path.Trim(new char[] { '"' }); + + return path; + } + + /// + /// Get the directory name of a rooted full path + /// + /// + /// + internal static String GetDirectoryNameOfFullPath(String fullPath) + { + if (fullPath != null) + { + int i = fullPath.Length; + while (i > 0 && fullPath[--i] != Path.DirectorySeparatorChar && fullPath[i] != Path.AltDirectorySeparatorChar) ; + return fullPath.Substring(0, i); + } + return null; + } + + /// + /// Compare an unsafe char buffer with a to see if their contents are identical. + /// + /// The beginning of the char buffer. + /// The length of the buffer. + /// The string. + /// True only if the contents of and the first characters in are identical. + private unsafe static bool AreStringsEqual(char* buffer, int len, string s) + { + if (len != s.Length) + { + return false; + } + + foreach (char ch in s) + { + if (ch != *buffer++) + { + return false; + } + } + + return true; + } + + /// + /// Gets the canonicalized full path of the provided path. + /// Path.GetFullPath is slow and creates strings in its work: this version doesn't. + /// Guidance for use: call this on all paths accepted through public entry + /// points that need normalization. After that point, only verify the path + /// is rooted, using ErrorUtilities.VerifyThrowPathRooted. + /// ASSUMES INPUT IS ALREADY UNESCAPED. + /// + internal unsafe static string NormalizePath(string path) + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + + int errorCode = 0; // 0 == success in Win32 + +#if _DEBUG + // Just to make sure and exercise the code that sets the correct buffer size + // we'll start out with it deliberately too small + int lenDir = 1; +#else + int lenDir = MaxPath; +#endif + + char* finalBuffer = stackalloc char[lenDir + 1]; // One extra for the null terminator + + int length = NativeMethodsShared.GetFullPathName(path, lenDir + 1, finalBuffer, IntPtr.Zero); + errorCode = Marshal.GetLastWin32Error(); + + // If the length returned from GetFullPathName is greater than the length of the buffer we've + // allocated, then reallocate the buffer with the correct size, and repeat the call + if (length > lenDir) + { + lenDir = length; + char* tempBuffer = stackalloc char[lenDir]; + finalBuffer = tempBuffer; + length = NativeMethodsShared.GetFullPathName(path, lenDir, finalBuffer, IntPtr.Zero); + errorCode = Marshal.GetLastWin32Error(); + // If we find that the length returned from GetFullPathName is longer than the buffer capacity, then + // something very strange is going on! + ErrorUtilities.VerifyThrow(length <= lenDir, "Final buffer capacity should be sufficient for full path name and null terminator."); + } + + if (length > 0) + { + // In order to prevent people from taking advantage of our ability to extend beyond MaxPath + // since it is unlikely that the CLR fix will be a complete removal of maxpath madness + // we reluctantly have to restrict things here. + if (length >= MaxPath) + { + throw new PathTooLongException(path); + } + + // Avoid creating new strings unnecessarily + string finalFullPath = AreStringsEqual(finalBuffer, length, path) ? path : new string(finalBuffer, startIndex: 0, length: length); + + // We really don't care about extensions here, but Path.HasExtension provides a great way to + // invoke the CLR's invalid path checks (these are independent of path length) + Path.HasExtension(finalFullPath); + + if (finalFullPath.StartsWith(@"\\", StringComparison.Ordinal)) + { + // If we detect we are a UNC path then we need to use the regular get full path in order to do the correct checks for UNC formatting + // and security checks for strings like \\?\GlobalRoot + int startIndex = 2; + while (startIndex < finalFullPath.Length) + { + if (finalFullPath[startIndex] == '\\') + { + startIndex++; + break; + } + else + { + startIndex++; + } + } + + /* + From Path.cs in the CLR + Throw an ArgumentException for paths like \\, \\server, \\server\ + This check can only be properly done after normalizing, so + \\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\ + (an internal kernel path) because it provides aliases for drives. + + throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC")); + + // Check for \\?\Globalroot, an internal mechanism to the kernel + // that provides aliases for drives and other undocumented stuff. + // The kernel team won't even describe the full set of what + // is available here - we don't want managed apps mucking + // with this for security reasons. + */ + if (startIndex == finalFullPath.Length || finalFullPath.IndexOf(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase) != -1) + { + finalFullPath = Path.GetFullPath(finalFullPath); + } + } + + return finalFullPath; + } + else + { + NativeMethodsShared.ThrowExceptionForErrorCode(errorCode); + return null; + } + } + + /// + /// Extracts the directory from the given file-spec. + /// + /// The filespec. + /// directory path + internal static string GetDirectory(string fileSpec) + { + string directory = Path.GetDirectoryName(fileSpec); + + // if file-spec is a root directory e.g. c:, c:\, \, \\server\share + // NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server + if (directory == null) + { + // just use the file-spec as-is + directory = fileSpec; + } + else if ((directory.Length > 0) && !EndsWithSlash(directory)) + { + // restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories) + directory += Path.DirectorySeparatorChar; + } + + return directory; + } + + /// + /// Determines whether the given assembly file name has one of the listed extensions. + /// + /// The name of the file + /// Array of extensions to consider. + /// + internal static bool HasExtension(string fileName, string[] allowedExtensions) + { + string fileExtension = Path.GetExtension(fileName); + foreach (string extension in allowedExtensions) + { + if (String.Compare(fileExtension, extension, true /* ignore case */, CultureInfo.CurrentCulture) == 0) + { + return true; + } + } + return false; + } + + // ISO 8601 Universal time with sortable format + internal const string FileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff"; + + /// + /// Cached path to the current exe + /// + private static string s_executablePath; + + /// + /// Get the currently executing assembly path + /// + internal static string ExecutingAssemblyPath + { + get + { + try + { + return Path.GetFullPath(new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath); + } + catch (InvalidOperationException e) + { + // Workaround for issue where people are getting relative uri crash here. + // Last resort. We may have a problem when the assembly is shadow-copied. + ExceptionHandling.DumpExceptionToFile(e); + return System.Reflection.Assembly.GetExecutingAssembly().Location; + } + } + } + + /// + /// Name of the current .exe without extension, such as "MSBuild" "Devenv" or "Blend". + /// This is much cheaper than calling Process.GetCurrentProcess().ProcessName. + /// + internal static string CurrentExecutableName + { + get + { + return Path.GetFileNameWithoutExtension(CurrentExecutablePath); + } + } + + /// + /// Full path to the current exe (for example, msbuild.exe) including the file name + /// + internal static string CurrentExecutablePath + { + get + { + if (s_executablePath == null) + { + s_executablePath = CurrentExecutableOverride; + } + + if (s_executablePath == null) + { + StringBuilder sb = new StringBuilder(NativeMethodsShared.MAX_PATH); + if (NativeMethodsShared.GetModuleFileName(NativeMethodsShared.NullHandleRef, sb, sb.Capacity) == 0) + { + throw new System.ComponentModel.Win32Exception(); + } + + s_executablePath = sb.ToString(); + } + + return s_executablePath; + } + } + + /// + /// Full path to the directory that the current exe (for example, msbuild.exe) is located in + /// + internal static string CurrentExecutableDirectory + { + get + { + return Path.GetDirectoryName(CurrentExecutablePath); + } + } + + /// + /// Full path to the current config file (for example, msbuild.exe.config) + /// + internal static string CurrentExecutableConfigurationFilePath + { + get + { + return String.Concat(CurrentExecutablePath, ".config"); + } + } + + /// + /// Determines the full path for the given file-spec. + /// ASSUMES INPUT IS STILL ESCAPED + /// + /// The file spec to get the full path of. + /// + /// full path + internal static string GetFullPath(string fileSpec, string currentDirectory) + { + // Sending data out of the engine into the filesystem, so time to unescape. + fileSpec = EscapingUtilities.UnescapeAll(fileSpec); + + // Data coming back from the filesystem into the engine, so time to escape it back. + string fullPath = EscapingUtilities.Escape(NormalizePath(Path.Combine(currentDirectory, fileSpec))); + + if (!EndsWithSlash(fullPath)) + { + Match drive = FileUtilitiesRegex.DrivePattern.Match(fileSpec); + Match UNCShare = FileUtilitiesRegex.UNCPattern.Match(fullPath); + + if ((drive.Success && (drive.Length == fileSpec.Length)) || + (UNCShare.Success && (UNCShare.Length == fullPath.Length))) + { + // append trailing slash if Path.GetFullPath failed to (this happens with drive-specs and UNC shares) + fullPath += Path.DirectorySeparatorChar; + } + } + + return fullPath; + } + + /// + /// A variation of Path.GetFullPath that will return the input value + /// instead of throwing any IO exception. + /// Useful to get a better path for an error message, without the risk of throwing + /// if the error message was itself caused by the path being invalid! + /// + internal static string GetFullPathNoThrow(string path) + { + try + { + path = NormalizePath(path); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + // Otherwise eat it. + } + + return path; + } + + /// + /// A variation on File.Delete that will throw ExceptionHandling.NotExpectedException exceptions + /// + internal static void DeleteNoThrow(string path) + { + try + { + File.Delete(path); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + // Otherwise eat it. + } + } + + /// + /// A variation on Directory.Delete that will throw ExceptionHandling.NotExpectedException exceptions + /// + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Int32.TryParse(System.String,System.Int32@)", Justification = "We expect the out value to be 0 if the parse fails and compensate accordingly")] + internal static void DeleteDirectoryNoThrow(string path, bool recursive) + { + int retryCount; + int retryTimeOut; + + // Try parse will set the out parameter to 0 if the string passed in is null, or is outside the range of an int. + if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETERETRYCOUNT"), out retryCount)) + { + retryCount = 0; + } + + if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETRETRYTIMEOUT"), out retryTimeOut)) + { + retryTimeOut = 0; + } + + retryCount = retryCount < 1 ? 2 : retryCount; + retryTimeOut = retryTimeOut < 1 ? 500 : retryTimeOut; + + for (int i = 0; i < retryCount; i++) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive); + break; + } + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + // Otherwise eat it. + } + + if (i + 1 < retryCount) // should not wait for the final iteration since we not gonna check anyway + { + Thread.Sleep(retryTimeOut); + } + } + } + + /// + /// A variation of Path.IsRooted that not throw any IO exception. + /// + internal static bool IsRootedNoThrow(string path) + { + bool result; + + try + { + result = Path.IsPathRooted(path); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + // Otherwise eat it. + result = false; + } + + return result; + } + + /// + /// Gets a file info object for the specified file path. If the file path + /// is invalid, or is a directory, or cannot be accessed, or does not exist, + /// it returns null rather than throwing or returning a FileInfo around a non-existent file. + /// This allows it to be called where File.Exists() (which never throws, and returns false + /// for directories) was called - but with the advantage that a FileInfo object is returned + /// that can be queried (e.g., for LastWriteTime) without hitting the disk again. + /// + /// + /// FileInfo around path if it is an existing /file/, else null + internal static FileInfo GetFileInfoNoThrow(string filePath) + { + filePath = AttemptToShortenPath(filePath); + + FileInfo fileInfo; + + try + { + fileInfo = new FileInfo(filePath); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + // Invalid or inaccessible path: treat as if nonexistent file, just as File.Exists does + return null; + } + + if (fileInfo.Exists) + { + // It's an existing file + return fileInfo; + } + else + { + // Nonexistent, or existing but a directory, just as File.Exists behaves + return null; + } + } + + /// + /// Returns if the directory exists + /// + /// Full path to the directory in the filesystem + /// + internal static bool DirectoryExistsNoThrow(string fullPath) + { + fullPath = AttemptToShortenPath(fullPath); + + NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA(); + bool success = false; + + success = NativeMethodsShared.GetFileAttributesEx(fullPath, 0, ref data); + if (success) + { + return ((data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) != 0); + } + + return false; + } + + + /// + /// Returns if the directory exists + /// + /// Full path to the file in the filesystem + /// + internal static bool FileExistsNoThrow(string fullPath) + { + fullPath = AttemptToShortenPath(fullPath); + + NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA(); + bool success = false; + + success = NativeMethodsShared.GetFileAttributesEx(fullPath, 0, ref data); + if (success) + { + return ((data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) == 0); + } + + return false; + } + + /// + /// If there is a directory or file at the specified path, returns true. + /// Otherwise, returns false. + /// Does not throw IO exceptions, to match Directory.Exists and File.Exists. + /// Unlike calling each of those in turn it only accesses the disk once, which is faster. + /// + internal static bool FileOrDirectoryExistsNoThrow(string fullPath) + { + fullPath = AttemptToShortenPath(fullPath); + + NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA(); + bool success = false; + + success = NativeMethodsShared.GetFileAttributesEx(fullPath, 0, ref data); + + return success; + } + + /// + /// This method returns true if the specified filename is a solution file (.sln), otherwise + /// it returns false. + /// + internal static bool IsSolutionFilename(string filename) + { + return (String.Equals(Path.GetExtension(filename), ".sln", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Returns true if the specified filename is a VC++ project file, otherwise returns false + /// + internal static bool IsVCProjFilename(string filename) + { + return (String.Equals(Path.GetExtension(filename), ".vcproj", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Returns true if the specified filename is a metaproject file (.metaproj), otherwise false. + /// + internal static bool IsMetaprojectFilename(string filename) + { + return (String.Equals(Path.GetExtension(filename), ".metaproj", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Given the absolute location of a file, and a disc location, returns relative file path to that disk location. + /// Throws UriFormatException. + /// + /// + /// The base path we want to relativize to. Must be absolute. + /// Should not include a filename as the last segment will be interpreted as a directory. + /// + /// + /// The path we need to make relative to basePath. The path can be either absolute path or a relative path in which case it is relative to the base path. + /// If the path cannot be made relative to the base path (for example, it is on another drive), it is returned verbatim. + /// If the basePath is an empty string, returns the path. + /// + /// relative path (can be the full path) + internal static string MakeRelative(string basePath, string path) + { + ErrorUtilities.VerifyThrowArgumentNull(basePath, "basePath"); + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + + if (basePath.Length == 0) + { + return path; + } + + Uri baseUri = new Uri(FileUtilities.EnsureTrailingSlash(basePath), UriKind.Absolute); // May throw UriFormatException + + Uri pathUri = CreateUriFromPath(path); + + if (!pathUri.IsAbsoluteUri) + { + // the path is already a relative url, we will just normalize it... + pathUri = new Uri(baseUri, pathUri); + } + + Uri relativeUri = baseUri.MakeRelativeUri(pathUri); + string relativePath = Uri.UnescapeDataString(relativeUri.IsAbsoluteUri ? relativeUri.LocalPath : relativeUri.ToString()); + + string result = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + return result; + } + + /// + /// Helper function to create an Uri object from path. + /// + /// path string + /// uri object + private static Uri CreateUriFromPath(string path) + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + + Uri pathUri = null; + + // Try absolute first, then fall back on relative, otherwise it + // makes some absolute UNC paths like (\\foo\bar) relative ... + if (!Uri.TryCreate(path, UriKind.Absolute, out pathUri)) + { + pathUri = new Uri(path, UriKind.Relative); + } + + return pathUri; + } + + /// + /// Normalizes the path if and only if it is longer than max path, + /// or would be if rooted by the current directory. + /// This may make it shorter by removing ".."'s. + /// + internal static string AttemptToShortenPath(string path) + { + // >= not > because MAX_PATH assumes a trailing null + if (path.Length >= NativeMethodsShared.MAX_PATH || + (!IsRootedNoThrow(path) && ((Environment.CurrentDirectory.Length + path.Length + 1 /* slash */) >= NativeMethodsShared.MAX_PATH))) + { + // Attempt to make it shorter -- perhaps there are some \..\ elements + path = GetFullPathNoThrow(path); + } + + return path; + } + + /// + /// Gets the flag that indicates if we are running in a test harness + /// + internal static bool RunningTests + { + get + { + // Check if initialized and do so if not yet + if (s_runningTests && s_currentExecutableOverride == null) + { + GetTestExecutionInfo(); + } + return s_runningTests; + } + } + + /// + /// Gets a supposed (computed) path for the msbuild.exe if running + /// in a test harness. Otherwise returns null. + /// + private static string CurrentExecutableOverride + { + get + { + // Check if initialized and do so if not yet + if (s_runningTests && s_currentExecutableOverride == null) + { + GetTestExecutionInfo(); + } + return s_currentExecutableOverride; + } + } + } +} diff --git a/src/Shared/FileUtilitiesRegex.cs b/src/Shared/FileUtilitiesRegex.cs new file mode 100644 index 00000000000..7b431d997e9 --- /dev/null +++ b/src/Shared/FileUtilitiesRegex.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Security; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for file IO. + /// Separate from FileUtilities because some assemblies may only need the patterns. + /// PERF\COVERAGE NOTE: Try to keep classes in 'shared' as granular as possible. All the methods in + /// each class get pulled into the resulting assembly. + /// + internal static class FileUtilitiesRegex + { + // regular expression used to match file-specs beginning with ":" + internal static readonly Regex DrivePattern = new Regex(@"^[A-Za-z]:"); + + // regular expression used to match UNC paths beginning with "\\\" + internal static readonly Regex UNCPattern = new Regex(String.Format(CultureInfo.InvariantCulture, + @"^[\{0}\{1}][\{0}\{1}][^\{0}\{1}]+[\{0}\{1}][^\{0}\{1}]+", Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + } +} diff --git a/src/Shared/FrameworkLocationHelper.cs b/src/Shared/FrameworkLocationHelper.cs new file mode 100644 index 00000000000..75b69eed372 --- /dev/null +++ b/src/Shared/FrameworkLocationHelper.cs @@ -0,0 +1,1580 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Configuration; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Text; +using Microsoft.Build.Evaluation; +using Microsoft.Win32; + +using PropertyElement = Microsoft.Build.Evaluation.ToolsetElement.PropertyElement; + +namespace Microsoft.Build.Shared +{ + /// + /// Used to specify the targeted bitness of the .NET Framework for some methods of FrameworkLocationHelper + /// + internal enum DotNetFrameworkArchitecture + { + /// + /// Indicates the .NET Framework that is currently being run under. + /// + Current = 0, + + /// + /// Indicates the 32-bit .NET Framework + /// + Bitness32 = 1, + + /// + /// Indicates the 64-bit .NET Framework + /// + Bitness64 = 2 + } + + /// + /// FrameworkLocationHelper provides utility methods for locating .NET Framework and .NET Framework SDK directories and files + /// + internal static class FrameworkLocationHelper + { + #region Constants + + internal const string dotNetFrameworkIdentifier = ".NETFramework"; + + // .net versions. + internal static readonly Version dotNetFrameworkVersion11 = new Version(1, 1); + internal static readonly Version dotNetFrameworkVersion20 = new Version(2, 0); + internal static readonly Version dotNetFrameworkVersion30 = new Version(3, 0); + internal static readonly Version dotNetFrameworkVersion35 = new Version(3, 5); + internal static readonly Version dotNetFrameworkVersion40 = new Version(4, 0); + internal static readonly Version dotNetFrameworkVersion45 = new Version(4, 5); + internal static readonly Version dotNetFrameworkVersion451 = new Version(4, 5, 1); + internal static readonly Version dotNetFrameworkVersion46 = new Version(4, 6); + + // visual studio versions. + internal static readonly Version visualStudioVersion100 = new Version(10, 0); + internal static readonly Version visualStudioVersion110 = new Version(11, 0); + internal static readonly Version visualStudioVersion120 = new Version(12, 0); + internal static readonly Version visualStudioVersion140 = new Version(14, 0); + + // keep this up-to-date; always point to the latest visual studio version. + internal static readonly Version visualStudioVersionLatest = visualStudioVersion140; + + private const string dotNetFrameworkRegistryPath = "SOFTWARE\\Microsoft\\.NETFramework"; + private const string dotNetFrameworkSetupRegistryPath = "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP"; + private const string dotNetFrameworkSetupRegistryInstalledName = "Install"; + + internal const string fullDotNetFrameworkRegistryKey = "HKEY_LOCAL_MACHINE\\" + dotNetFrameworkRegistryPath; + private const string dotNetFrameworkAssemblyFoldersRegistryPath = dotNetFrameworkRegistryPath + "\\AssemblyFolders"; + private const string referenceAssembliesRegistryValueName = "All Assemblies In"; + + internal const string dotNetFrameworkSdkInstallKeyValueV11 = "SDKInstallRootv1.1"; + internal const string dotNetFrameworkVersionFolderPrefixV11 = "v1.1"; // v1.1 is for Everett. + private const string dotNetFrameworkVersionV11 = "v1.1.4322"; // full Everett version to pass to NativeMethodsShared.GetRequestedRuntimeInfo(). + private const string dotNetFrameworkRegistryKeyV11 = dotNetFrameworkSetupRegistryPath + "\\" + dotNetFrameworkVersionV11; + + internal const string dotNetFrameworkSdkInstallKeyValueV20 = "SDKInstallRootv2.0"; + internal const string dotNetFrameworkVersionFolderPrefixV20 = "v2.0"; // v2.0 is for Whidbey. + private const string dotNetFrameworkVersionV20 = "v2.0.50727"; // full Whidbey version to pass to NativeMethodsShared.GetRequestedRuntimeInfo(). + private const string dotNetFrameworkRegistryKeyV20 = dotNetFrameworkSetupRegistryPath + "\\" + dotNetFrameworkVersionV20; + + internal const string dotNetFrameworkVersionFolderPrefixV30 = "v3.0"; // v3.0 is for WinFx. + private const string dotNetFrameworkVersionV30 = "v3.0"; // full WinFx version to pass to NativeMethodsShared.GetRequestedRuntimeInfo(). + private const string dotNetFrameworkAssemblyFoldersRegistryKeyV30 = dotNetFrameworkAssemblyFoldersRegistryPath + "\\" + dotNetFrameworkVersionFolderPrefixV30; + private const string dotNetFrameworkRegistryKeyV30 = dotNetFrameworkSetupRegistryPath + "\\" + dotNetFrameworkVersionFolderPrefixV30 + "\\Setup"; + + private const string fallbackDotNetFrameworkSdkRegistryInstallPath = "SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows"; + internal const string fallbackDotNetFrameworkSdkInstallKeyValue = "CurrentInstallFolder"; + + private const string dotNetFrameworkSdkRegistryPathForV35ToolsOnWinSDK70A = @"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v7.0A\WinSDK-NetFx35Tools-x86"; + private const string fullDotNetFrameworkSdkRegistryPathForV35ToolsOnWinSDK70A = "HKEY_LOCAL_MACHINE\\" + dotNetFrameworkSdkRegistryPathForV35ToolsOnWinSDK70A; + + private const string dotNetFrameworkSdkRegistryPathForV35ToolsOnManagedToolsSDK80A = @"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.0A\WinSDK-NetFx35Tools-x86"; + private const string fullDotNetFrameworkSdkRegistryPathForV35ToolsOnManagedToolsSDK80A = "HKEY_LOCAL_MACHINE\\" + dotNetFrameworkSdkRegistryPathForV35ToolsOnManagedToolsSDK80A; + + private const string dotNetFrameworkRegistryKeyV35 = dotNetFrameworkSetupRegistryPath + "\\" + dotNetFrameworkVersionFolderPrefixV35; + internal const string dotNetFrameworkVersionFolderPrefixV35 = "v3.5"; // v3.5 is for Orcas. + + internal const string fullDotNetFrameworkSdkRegistryKeyV35OnVS10 = fullDotNetFrameworkSdkRegistryPathForV35ToolsOnWinSDK70A; + internal const string fullDotNetFrameworkSdkRegistryKeyV35OnVS11 = fullDotNetFrameworkSdkRegistryPathForV35ToolsOnManagedToolsSDK80A; + + internal const string dotNetFrameworkVersionFolderPrefixV40 = "v4.0"; + + /// + /// Path to the ToolsVersion definitions in the registry + /// + private const string ToolsVersionsRegistryPath = @"SOFTWARE\Microsoft\MSBuild\ToolsVersions"; + + #endregion // Constants + + #region Delegates + + // This way, the methods that take these as parameters can also be overridden to do different things + // in unit tests. + private static readonly GetDirectories s_getDirectories = new GetDirectories(Directory.GetDirectories); + private static readonly DirectoryExists s_directoryExists = new DirectoryExists(Directory.Exists); + + #endregion // Delegates + + #region Static member variables + + /// + /// By default when a root path is not specified we would like to use the program files directory \ reference assemblies\framework as the root location + /// to generate the reference assembly paths from. + /// + internal static readonly string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + internal static readonly string programFiles32 = GenerateProgramFiles32(); + internal static readonly string programFiles64 = GenerateProgramFiles64(); + internal static readonly string programFilesReferenceAssemblyLocation = GenerateProgramFilesReferenceAssemblyRoot(); + + private static string s_fallbackDotNetFrameworkSdkInstallPath; + + private static string s_pathToV35ToolsInFallbackDotNetFrameworkSdk; + + private static string s_pathToV4ToolsInFallbackDotNetFrameworkSdk; + + /// + /// List the supported .net versions. + /// + private static readonly DotNetFrameworkSpec[] s_dotNetFrameworkSpecs = + { + // v1.1 + new DotNetFrameworkSpecLegacy( + dotNetFrameworkVersion11, + dotNetFrameworkRegistryKeyV11, + dotNetFrameworkSetupRegistryInstalledName, + dotNetFrameworkVersionFolderPrefixV11, + dotNetFrameworkSdkInstallKeyValueV11, + hasMSBuild: false), + + // v2.0 + new DotNetFrameworkSpecLegacy( + dotNetFrameworkVersion20, + dotNetFrameworkRegistryKeyV20, + dotNetFrameworkSetupRegistryInstalledName, + dotNetFrameworkVersionFolderPrefixV20, + dotNetFrameworkSdkInstallKeyValueV20, + hasMSBuild: true), + + // v3.0 + new DotNetFrameworkSpecV3( + dotNetFrameworkVersion30, + dotNetFrameworkRegistryKeyV30, + "InstallSuccess", + dotNetFrameworkVersionFolderPrefixV30, + null, + null, + hasMSBuild: false), + + // v3.5 + new DotNetFrameworkSpecV3( + dotNetFrameworkVersion35, + dotNetFrameworkRegistryKeyV35, + dotNetFrameworkSetupRegistryInstalledName, + dotNetFrameworkVersionFolderPrefixV35, + "WinSDK-NetFx35Tools-x86", + "InstallationFolder", + hasMSBuild: true), + + // v4.0 + CreateDotNetFrameworkSpecForV4(dotNetFrameworkVersion40, visualStudioVersion100), + + // v4.5 + CreateDotNetFrameworkSpecForV4(dotNetFrameworkVersion45, visualStudioVersion110), + + // v4.5.1 + CreateDotNetFrameworkSpecForV4(dotNetFrameworkVersion451, visualStudioVersion120), + + // v4.6 + CreateDotNetFrameworkSpecForV4(dotNetFrameworkVersion46, visualStudioVersion140), + }; + + /// + /// List the supported visual studio versions. + /// + /// + /// The items must be ordered by the version, because some methods depend on that fact to find the previous visual studio version. + /// + private static readonly VisualStudioSpec[] s_visualStudioSpecs = + { + // VS10 + new VisualStudioSpec(visualStudioVersion100, "Windows\\v7.0A", null, null, new [] + { + dotNetFrameworkVersion11, + dotNetFrameworkVersion20, + dotNetFrameworkVersion35, + dotNetFrameworkVersion40, + }), + + // VS11 + new VisualStudioSpec(visualStudioVersion110, "Windows\\v8.0A", "v8.0", "InstallationFolder", new [] + { + dotNetFrameworkVersion11, + dotNetFrameworkVersion20, + dotNetFrameworkVersion35, + dotNetFrameworkVersion40, + dotNetFrameworkVersion45, + }), + + // VS12 + new VisualStudioSpec(visualStudioVersion120, "Windows\\v8.1A", "v8.1", "InstallationFolder", new [] + { + dotNetFrameworkVersion11, + dotNetFrameworkVersion20, + dotNetFrameworkVersion35, + dotNetFrameworkVersion40, + dotNetFrameworkVersion45, + dotNetFrameworkVersion451, + }), + + // VS14 + new VisualStudioSpec(visualStudioVersion140, "NETFXSDK\\4.6", "v8.1", "InstallationFolder", new [] + { + dotNetFrameworkVersion11, + dotNetFrameworkVersion20, + dotNetFrameworkVersion35, + dotNetFrameworkVersion40, + dotNetFrameworkVersion45, + dotNetFrameworkVersion451, + dotNetFrameworkVersion46, + }), + }; + + /// + /// Define explicit fallback rules for the request to get path of .net framework sdk tools folder. + /// The default rule is fallback to previous VS. However, there are some special cases that need + /// explicit rules, i.e. v4.5.1 on VS12 fallbacks to v4.5 on VS12. + /// + /// + /// The rules are maintained in a 2-dimensions array. Each row defines a rule. The first column + /// defines the trigger condition. The second column defines the fallback .net and VS versions. + /// + private static readonly Tuple[,] s_explicitFallbackRulesForPathToDotNetFrameworkSdkTools = + { + // VS12 + { Tuple.Create(dotNetFrameworkVersion451, visualStudioVersion120), Tuple.Create(dotNetFrameworkVersion45, visualStudioVersion120) }, + + // VS14 + { Tuple.Create(dotNetFrameworkVersion451, visualStudioVersion140), Tuple.Create(dotNetFrameworkVersion45, visualStudioVersion140) }, + { Tuple.Create(dotNetFrameworkVersion46, visualStudioVersion140), Tuple.Create(dotNetFrameworkVersion451, visualStudioVersion140) }, + }; + + private static readonly IReadOnlyDictionary s_dotNetFrameworkSpecDict; + private static readonly IReadOnlyDictionary s_visualStudioSpecDict; + + #endregion // Static member variables + + static FrameworkLocationHelper() + { + s_dotNetFrameworkSpecDict = s_dotNetFrameworkSpecs.ToDictionary(spec => spec.Version); + s_visualStudioSpecDict = s_visualStudioSpecs.ToDictionary(spec => spec.Version); + } + + #region Static properties + + internal static string PathToDotNetFrameworkV11 + { + get + { + return GetPathToDotNetFrameworkV11(DotNetFrameworkArchitecture.Current); + } + } + + internal static string PathToDotNetFrameworkV20 + { + get + { + return GetPathToDotNetFrameworkV20(DotNetFrameworkArchitecture.Current); + } + } + + internal static string PathToDotNetFrameworkV30 + { + get + { + return GetPathToDotNetFrameworkV30(DotNetFrameworkArchitecture.Current); + } + } + + internal static string PathToDotNetFrameworkV35 + { + get + { + return GetPathToDotNetFrameworkV35(DotNetFrameworkArchitecture.Current); + } + } + + internal static string PathToDotNetFrameworkV40 + { + get + { + return GetPathToDotNetFrameworkV40(DotNetFrameworkArchitecture.Current); + } + } + + internal static string PathToDotNetFrameworkV45 + { + get + { + return GetPathToDotNetFrameworkV45(DotNetFrameworkArchitecture.Current); + } + } + + internal static string PathToDotNetFrameworkSdkV11 + { + get + { + return GetPathToDotNetFrameworkSdkTools(dotNetFrameworkVersion11, visualStudioVersionLatest); + } + } + + internal static string PathToDotNetFrameworkSdkV20 + { + get + { + return GetPathToDotNetFrameworkSdkTools(dotNetFrameworkVersion20, visualStudioVersionLatest); + } + } + + /// + /// Because there is no longer a strong 1:1 mapping between FX versions and SDK + /// versions, if we're unable to locate the desired SDK version, we will try to + /// use whichever SDK version is installed by looking at the key pointing to the + /// "latest" version. + /// + /// This isn't ideal, but it will allow our tasks to function on any of several + /// related SDKs even if they don't have exactly the same versions. + /// + /// NOTE: This returns the path to the root of the fallback SDK + /// + private static string FallbackDotNetFrameworkSdkInstallPath + { + get + { + if (s_fallbackDotNetFrameworkSdkInstallPath == null) + { + s_fallbackDotNetFrameworkSdkInstallPath = FindRegistryValueUnderKey(fallbackDotNetFrameworkSdkRegistryInstallPath, fallbackDotNetFrameworkSdkInstallKeyValue); + + if (Environment.Is64BitProcess && s_fallbackDotNetFrameworkSdkInstallPath == null) + { + // Since we're 64-bit, what we just checked was the 64-bit fallback key -- so now let's + // check the 32-bit one too, just in case. + s_fallbackDotNetFrameworkSdkInstallPath = FindRegistryValueUnderKey(fallbackDotNetFrameworkSdkRegistryInstallPath, fallbackDotNetFrameworkSdkInstallKeyValue, RegistryView.Registry32); + } + } + + return s_fallbackDotNetFrameworkSdkInstallPath; + } + } + + /// + /// Because there is no longer a strong 1:1 mapping between FX versions and SDK + /// versions, if we're unable to locate the desired SDK version, we will try to + /// use whichever SDK version is installed by looking at the key pointing to the + /// "latest" version. + /// + /// This isn't ideal, but it will allow our tasks to function on any of several + /// related SDKs even if they don't have exactly the same versions. + /// + /// NOTE: This explicitly returns the path to the 3.5 tools (bin) under the fallback + /// SDK, to match the data we're pulling from the registry now. + /// + private static string PathToV35ToolsInFallbackDotNetFrameworkSdk + { + get + { + if (s_pathToV35ToolsInFallbackDotNetFrameworkSdk == null) + { + if (FallbackDotNetFrameworkSdkInstallPath != null) + { + bool endsWithASlash = false; + + if (FallbackDotNetFrameworkSdkInstallPath.EndsWith("\\", StringComparison.Ordinal)) + { + endsWithASlash = true; + } + + s_pathToV35ToolsInFallbackDotNetFrameworkSdk = Path.Combine(FallbackDotNetFrameworkSdkInstallPath, "bin"); + + // Path.Combine leaves no trailing slash, so if we had one before, be sure to add it back in + if (endsWithASlash) + { + s_pathToV35ToolsInFallbackDotNetFrameworkSdk = s_pathToV35ToolsInFallbackDotNetFrameworkSdk + "\\"; + } + } + } + + return s_pathToV35ToolsInFallbackDotNetFrameworkSdk; + } + } + + /// + /// Because there is no longer a strong 1:1 mapping between FX versions and SDK + /// versions, if we're unable to locate the desired SDK version, we will try to + /// use whichever SDK version is installed by looking at the key pointing to the + /// "latest" version. + /// + /// This isn't ideal, but it will allow our tasks to function on any of several + /// related SDKs even if they don't have exactly the same versions. + /// + /// NOTE: This explicitly returns the path to the 4.X tools (bin\NetFX 4.0 Tools) + /// under the fallback SDK, to match the data we're pulling from the registry now. + /// + private static string PathToV4ToolsInFallbackDotNetFrameworkSdk + { + get + { + if (s_pathToV4ToolsInFallbackDotNetFrameworkSdk == null) + { + if (FallbackDotNetFrameworkSdkInstallPath != null) + { + bool endsWithASlash = false; + + if (FallbackDotNetFrameworkSdkInstallPath.EndsWith("\\", StringComparison.Ordinal)) + { + endsWithASlash = true; + } + + s_pathToV4ToolsInFallbackDotNetFrameworkSdk = Path.Combine(FallbackDotNetFrameworkSdkInstallPath, "bin", "NetFX 4.0 Tools"); + + // Path.Combine leaves no trailing slash, so if we had one before, be sure to add it back in + if (endsWithASlash) + { + s_pathToV4ToolsInFallbackDotNetFrameworkSdk = s_pathToV4ToolsInFallbackDotNetFrameworkSdk + "\\"; + } + } + } + + return s_pathToV4ToolsInFallbackDotNetFrameworkSdk; + } + } + + #endregion // Static properties + + #region Internal methods + + internal static string GetDotNetFrameworkSdkRootRegistryKey(Version dotNetFrameworkVersion, Version visualStudioVersion) + { + RedirectVersionsIfNecessary(ref dotNetFrameworkVersion, ref visualStudioVersion); + + var dotNetFrameworkSpec = GetDotNetFrameworkSpec(dotNetFrameworkVersion); + var visualStudioSpec = GetVisualStudioSpec(visualStudioVersion); + ErrorUtilities.VerifyThrowArgument(visualStudioSpec.SupportedDotNetFrameworkVersions.Contains(dotNetFrameworkVersion), "FrameworkLocationHelper.UnsupportedFrameworkVersion", dotNetFrameworkVersion); + return dotNetFrameworkSpec.GetDotNetFrameworkSdkRootRegistryKey(visualStudioSpec); + } + + internal static string GetDotNetFrameworkSdkInstallKeyValue(Version dotNetFrameworkVersion, Version visualStudioVersion) + { + RedirectVersionsIfNecessary(ref dotNetFrameworkVersion, ref visualStudioVersion); + + var dotNetFrameworkSpec = GetDotNetFrameworkSpec(dotNetFrameworkVersion); + var visualStudioSpec = GetVisualStudioSpec(visualStudioVersion); + ErrorUtilities.VerifyThrowArgument(visualStudioSpec.SupportedDotNetFrameworkVersions.Contains(dotNetFrameworkVersion), "FrameworkLocationHelper.UnsupportedFrameworkVersion", dotNetFrameworkVersion); + return dotNetFrameworkSpec.DotNetFrameworkSdkRegistryInstallationFolderName; + } + + internal static string GetDotNetFrameworkVersionFolderPrefix(Version dotNetFrameworkVersion) + { + return GetDotNetFrameworkSpec(dotNetFrameworkVersion).DotNetFrameworkFolderPrefix; + } + + internal static string GetPathToWindowsSdk(Version dotNetFrameworkVersion) + { + return GetDotNetFrameworkSpec(dotNetFrameworkVersion).GetPathToWindowsSdk(); + } + + internal static string GetPathToDotNetFrameworkReferenceAssemblies(Version dotNetFrameworkVersion) + { + return GetDotNetFrameworkSpec(dotNetFrameworkVersion).GetPathToDotNetFrameworkReferenceAssemblies(); + } + + internal static string GetPathToDotNetFrameworkSdkTools(Version dotNetFrameworkVersion, Version visualStudioVersion) + { + RedirectVersionsIfNecessary(ref dotNetFrameworkVersion, ref visualStudioVersion); + + var dotNetFrameworkSpec = GetDotNetFrameworkSpec(dotNetFrameworkVersion); + var visualStudioSpec = GetVisualStudioSpec(visualStudioVersion); + ErrorUtilities.VerifyThrowArgument(visualStudioSpec.SupportedDotNetFrameworkVersions.Contains(dotNetFrameworkVersion), "FrameworkLocationHelper.UnsupportedFrameworkVersion", dotNetFrameworkVersion); + return dotNetFrameworkSpec.GetPathToDotNetFrameworkSdkTools(visualStudioSpec); + } + + internal static string GetPathToDotNetFrameworkSdk(Version dotNetFrameworkVersion, Version visualStudioVersion) + { + RedirectVersionsIfNecessary(ref dotNetFrameworkVersion, ref visualStudioVersion); + + var dotNetFrameworkSpec = GetDotNetFrameworkSpec(dotNetFrameworkVersion); + var visualStudioSpec = GetVisualStudioSpec(visualStudioVersion); + ErrorUtilities.VerifyThrowArgument(visualStudioSpec.SupportedDotNetFrameworkVersions.Contains(dotNetFrameworkVersion), "FrameworkLocationHelper.UnsupportedFrameworkVersion", dotNetFrameworkVersion); + return dotNetFrameworkSpec.GetPathToDotNetFrameworkSdk(visualStudioSpec); + } + + internal static string GetPathToDotNetFrameworkV11(DotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFramework(dotNetFrameworkVersion11, architecture); + } + + internal static string GetPathToDotNetFrameworkV20(DotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFramework(dotNetFrameworkVersion20, architecture); + } + + internal static string GetPathToDotNetFrameworkV30(DotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFramework(dotNetFrameworkVersion30, architecture); + } + + internal static string GetPathToDotNetFrameworkV35(DotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFramework(dotNetFrameworkVersion35, architecture); + } + + internal static string GetPathToDotNetFrameworkV40(DotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFramework(dotNetFrameworkVersion40, architecture); + } + + internal static string GetPathToDotNetFrameworkV45(DotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFramework(dotNetFrameworkVersion45, architecture); + } + + internal static string GetPathToDotNetFramework(Version version) + { + return GetPathToDotNetFramework(version, DotNetFrameworkArchitecture.Current); + } + + internal static string GetPathToDotNetFramework(Version version, DotNetFrameworkArchitecture architecture) + { + return GetDotNetFrameworkSpec(version).GetPathToDotNetFramework(architecture); + } + + /// + /// Check the registry key and value to see if the .net Framework is installed on the machine. + /// + /// Registry path to look for the value + /// Key to retreive the value from + /// True if the registry key is 1 false if it is not there. This method also return true if the complus enviornment variables are set. + private static bool CheckForFrameworkInstallation(string registryEntryToCheckInstall, string registryValueToCheckInstall) + { + // Get the complus install root and version + string complusInstallRoot = Environment.GetEnvironmentVariable("COMPLUS_INSTALLROOT"); + string complusVersion = Environment.GetEnvironmentVariable("COMPLUS_VERSION"); + + // Complus is not set we need to make sure the framework we are targeting is installed. Check the registry key before trying to find the directory. + // If complus is set then we will return that directory as the framework directory, there is no need to check the registry value for the framework and it may not even be installed. + if (String.IsNullOrEmpty(complusInstallRoot) && String.IsNullOrEmpty(complusVersion)) + { + // If the registry entry is 1 then the framework is installed. Go ahead and find the directory. If it is not 1 then the framework is not installed, return null. + return String.Compare("1", FindRegistryValueUnderKey(registryEntryToCheckInstall, registryValueToCheckInstall), StringComparison.OrdinalIgnoreCase) == 0; + } + + return true; + } + + /// + /// Heuristic that first considers the current runtime path and then searches the base of that path for the given + /// frameworks version. + /// + /// The path to the runtime that is currently executing. + /// Should be something like 'v1.2' that indicates the runtime version we want. + /// Should be the full version number of the runtime version we want. + /// Delegate to method that can return filesystem entries. + /// Whether we should fall back to a search heuristic if other searches fail. + /// Will return 'null' if there is no target frameworks on this machine. + internal static string FindDotNetFrameworkPath + ( + string currentRuntimePath, + string prefix, + DirectoryExists directoryExists, + GetDirectories getDirectories, + DotNetFrameworkArchitecture architecture + ) + { + // If the COMPLUS variables are set, they override everything -- that's the directory we want. + string complusInstallRoot = Environment.GetEnvironmentVariable("COMPLUS_INSTALLROOT"); + string complusVersion = Environment.GetEnvironmentVariable("COMPLUS_VERSION"); + + if (!String.IsNullOrEmpty(complusInstallRoot) && !String.IsNullOrEmpty(complusVersion)) + { + return Path.Combine(complusInstallRoot, complusVersion); + } + + // If the current runtime starts with correct prefix, then this is the runtime we want to use. + // However, only if we're requesting current architecture -- otherwise, the base path may be different, so we'll need to look it up. + string leaf = Path.GetFileName(currentRuntimePath); + if (leaf.StartsWith(prefix, StringComparison.Ordinal) && architecture == DotNetFrameworkArchitecture.Current) + { + return currentRuntimePath; + } + + // We haven't managed to use exact methods to locate the FX, so + // search for the correct path with a heuristic. + string baseLocation = Path.GetDirectoryName(currentRuntimePath); + string searchPattern = prefix + "*"; + + int indexOfFramework64 = baseLocation.IndexOf("Framework64", StringComparison.OrdinalIgnoreCase); + + if (indexOfFramework64 != -1 && architecture == DotNetFrameworkArchitecture.Bitness32) + { + // need to get rid of just the 64, but want to look up 'Framework64' rather than '64' to avoid the case where + // the path is something like 'C:\MyPath\64\Framework64'. 9 = length of 'Framework', to make the index match + // the location of the '64'. + int indexOf64 = indexOfFramework64 + 9; + string tempLocation = baseLocation; + baseLocation = tempLocation.Substring(0, indexOf64) + tempLocation.Substring(indexOf64 + 2, tempLocation.Length - indexOf64 - 2); + } + else if (indexOfFramework64 == -1 && architecture == DotNetFrameworkArchitecture.Bitness64) + { + // need to add 64 -- since this is a heuristic, we assume that we just need to append. + baseLocation = baseLocation + "64"; + } + // we don't need to do anything if it's DotNetFrameworkArchitecture.Current. + + string[] directories; + + if (directoryExists(baseLocation)) + { + directories = getDirectories(baseLocation, searchPattern); + } + else + { + // If we can't even find the base path, might as well give up now. + return null; + } + + if (directories.Length == 0) + { + // Couldn't find the path, return a null. + return null; + } + + // We don't care which one we choose, but we want to be predictible. + // The intention here is to choose the alphabetical maximum. + string max = directories[0]; + + // the max.EndsWith condition: pre beta 2 versions of v3.5 have build number like v3.5.20111. + // This was removed in beta2 + // We should favor \v3.5 over \v3.5.xxxxx + // versions previous to 2.0 have .xxxx version numbers. 3.0 and 3.5 do not. + if (!max.EndsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + for (int i = 1; i < directories.Length; ++i) + { + if (directories[i].EndsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + max = directories[i]; + break; + } + else if (String.Compare(directories[i], max, StringComparison.OrdinalIgnoreCase) > 0) + { + max = directories[i]; + } + } + } + + return max; + } + + /// + /// Determine the 32 bit program files directory, this is used for finding where the reference assemblies live. + /// + internal static string GenerateProgramFiles32() + { + // On a 64 bit machine we always want to use the program files x86. If we are running as a 64 bit process then this variable will be set correctly + // If we are on a 32 bit machine or running as a 32 bit process then this variable will be null and the programFiles variable will be correct. + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + if (String.IsNullOrEmpty(programFilesX86)) + { + // 32 bit box + programFilesX86 = programFiles; + } + + return programFilesX86; + } + + /// + /// Determine the 64-bit program files directory, used as the basis for MSBuildExtensionsPath64. + /// Returns null if we're not on a 64-bit machine + /// + internal static string GenerateProgramFiles64() + { + string programFilesX64 = null; + if (string.Equals(programFiles, programFiles32)) + { + // either we're in a 32-bit window, or we're on a 32-bit machine. + // if we're on a 32-bit machine, ProgramW6432 won't exist + // if we're on a 64-bit machine, ProgramW6432 will point to the correct Program Files. + programFilesX64 = Environment.GetEnvironmentVariable("ProgramW6432"); + } + else + { + // 64-bit window on a 64-bit machine; %ProgramFiles% points to the 64-bit + // Program Files already. + programFilesX64 = programFiles; + } + + return programFilesX64; + } + + /// + /// Generate the path to the program files reference assembly location by taking in the program files special folder and then + /// using that path to generate the path to the reference assemblies location. + /// + internal static string GenerateProgramFilesReferenceAssemblyRoot() + { + string combinedPath = Path.Combine(programFiles32, "Reference Assemblies\\Microsoft\\Framework"); + return Path.GetFullPath(combinedPath); + } + + /// + /// Given a ToolsVersion, find the path to the build tools folder for that ToolsVersion. + /// + /// The ToolsVersion to look up + /// The path to the build tools folder for that ToolsVersion, if it exists, or + /// null otherwise + internal static string GeneratePathToBuildToolsForToolsVersion(string toolsVersion, DotNetFrameworkArchitecture architecture) + { + // Much like when reading toolsets, first check the .exe.config + string toolsPath = GetPathToBuildToolsFromConfig(toolsVersion); + + if (String.IsNullOrEmpty(toolsPath)) + { + // Or if it's not defined there, look it up in the registry + toolsPath = GetPathToBuildToolsFromRegistry(toolsVersion, architecture); + } + + return toolsPath; + } + + /// + /// Take the parts of the Target framework moniker and formulate the reference assembly path based on the the following pattern: + /// For a framework and version: + /// $(TargetFrameworkRootPath)\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion) + /// For a subtype: + /// $(TargetFrameworkRootPath)\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\SubType\$(TargetFrameworkSubType) + /// e.g.NET Framework v4.0 would locate its reference assemblies in: + /// \Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0 + /// e.g.Silverlight v2.0 would locate its reference assemblies in: + /// \Program Files\Reference Assemblies\Microsoft\Framework\Silverlight\v2.0 + /// e.g.NET Compact Framework v3.5, subtype PocketPC would locate its reference assemblies in: + /// \Program Files\Reference Assemblies\Microsoft\Framework\.NETCompactFramework\v3.5\SubType\PocketPC + /// + /// The path to the reference assembly location + internal static string GenerateReferenceAssemblyPath(string targetFrameworkRootPath, FrameworkName frameworkName) + { + ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkRootPath, "targetFrameworkRootPath"); + ErrorUtilities.VerifyThrowArgumentNull(frameworkName, "frameworkName"); + + try + { + string path = targetFrameworkRootPath; + path = Path.Combine(path, frameworkName.Identifier); + path = Path.Combine(path, "v" + frameworkName.Version.ToString()); + if (!String.IsNullOrEmpty(frameworkName.Profile)) + { + path = Path.Combine(path, "Profile"); + path = Path.Combine(path, frameworkName.Profile); + } + + path = Path.GetFullPath(path); + return path; + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + ErrorUtilities.ThrowInvalidOperation("FrameworkLocationHelper.CouldNotGenerateReferenceAssemblyDirectory", targetFrameworkRootPath, frameworkName.ToString(), e.Message); + // The compiler does not see the massage above an as exception; + return null; + } + } + + /// + /// Given a path, subtracts the requested number of directories and returns the result. + /// + /// + /// Internal only so that I can have the unit tests use it too, instead of duplicating the same code + /// + internal static string RemoveDirectories(string path, int numberOfLevelsToRemove) + { + ErrorUtilities.VerifyThrowArgumentOutOfRange(numberOfLevelsToRemove > 0, "what are you doing passing a negative number to this function??"); + + string fixedPath = null; + if (path != null) + { + bool endedWithASlash = false; + + // Record whether we had a slash or not so that we can tack it back on if necessary + if (path.EndsWith("\\", StringComparison.OrdinalIgnoreCase)) + { + endedWithASlash = true; + } + + DirectoryInfo fixedPathInfo = new DirectoryInfo(path); + for (int i = 0; i < numberOfLevelsToRemove; i++) + { + if (fixedPathInfo != null && fixedPathInfo.Parent != null) + { + fixedPathInfo = fixedPathInfo.Parent; + } + } + + if (fixedPathInfo != null) + { + fixedPath = fixedPathInfo.FullName; + } + + if (fixedPath != null && endedWithASlash) + { + fixedPath = fixedPath + "\\"; + } + } + + return fixedPath; + } + + /// + /// Look up the path to the build tools directory for the requested ToolsVersion in the .exe.config file of this executable + /// + private static string GetPathToBuildToolsFromConfig(string toolsVersion) + { + string toolPath = null; + + if (ToolsetConfigurationReaderHelpers.ConfigurationFileMayHaveToolsets()) + { + try + { + Configuration configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); + ToolsetConfigurationSection configurationSection = ToolsetConfigurationReaderHelpers.ReadToolsetConfigurationSection(configuration); + + if (configurationSection != null) + { + ToolsetElement toolset = configurationSection.Toolsets.GetElement(toolsVersion); + + if (toolset != null) + { + PropertyElement toolsPathFromConfiguration = toolset.PropertyElements.GetElement(MSBuildConstants.ToolsPath); + + if (toolsPathFromConfiguration != null) + { + toolPath = toolsPathFromConfiguration.Value; + + if (toolPath != null) + { + if (!FileUtilities.IsRootedNoThrow(toolPath)) + { + toolPath = FileUtilities.NormalizePath(Path.Combine(FileUtilities.CurrentExecutableDirectory, toolPath)); + } + + toolPath = FileUtilities.EnsureTrailingSlash(toolPath); + } + } + } + } + } + catch (ConfigurationException) + { + // may happen if the .exe.config contains bad data. Shouldn't ever happen in + // practice since we'll long since have loaded all toolsets in the toolset loading + // code and thrown errors to the user at that point if anything was invalid, but just + // in case, just eat the exception here, so that we can go on to look in the registry + // to see if there is any valid data there. + } + } + + return toolPath; + } + + /// + /// Look up the path to the build tools directory in the registry for the requested ToolsVersion and requested architecture + /// + private static string GetPathToBuildToolsFromRegistry(string toolsVersion, DotNetFrameworkArchitecture architecture) + { + string toolsVersionSpecificKey = ToolsVersionsRegistryPath + "\\" + toolsVersion; + + RegistryView view = RegistryView.Default; + + switch (architecture) + { + case DotNetFrameworkArchitecture.Bitness32: + view = RegistryView.Registry32; + break; + case DotNetFrameworkArchitecture.Bitness64: + view = RegistryView.Registry64; + break; + case DotNetFrameworkArchitecture.Current: + view = RegistryView.Default; + break; + } + + string toolsPath = FindRegistryValueUnderKey(toolsVersionSpecificKey, MSBuildConstants.ToolsPath, view); + return toolsPath; + } + + #endregion // Internal methods + + #region Private methods + + /// + /// Will return the path to the dot net framework reference assemblies if they exist under the program files\reference assembies\microsoft\framework directory + /// or null if the directory does not exist. + /// + private static string GenerateReferenceAssemblyDirectory(string versionPrefix) + { + string programFilesReferenceAssemblyDirectory = Path.Combine(programFilesReferenceAssemblyLocation, versionPrefix); + string referenceAssemblyDirectory = null; + + if (Directory.Exists(programFilesReferenceAssemblyDirectory)) + { + referenceAssemblyDirectory = programFilesReferenceAssemblyDirectory; + } + + return referenceAssemblyDirectory; + } + + /// + /// Look for the given registry value under the given key. + /// + private static string FindRegistryValueUnderKey + ( + string registryBaseKeyName, + string registryKeyName + ) + { + return FindRegistryValueUnderKey(registryBaseKeyName, registryKeyName, RegistryView.Default); + } + + /// + /// Look for the given registry value under the given key. + /// + private static string FindRegistryValueUnderKey + ( + string registryBaseKeyName, + string registryKeyName, + RegistryView registryView + ) + { + string keyValueAsString = String.Empty; + + using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, registryView)) + { + using (RegistryKey subKey = baseKey.OpenSubKey(registryBaseKeyName)) + { + if (null == subKey) + { + keyValueAsString = null; + } + else + { + object keyValue = subKey.GetValue(registryKeyName); + + if (null == keyValue) + { + keyValueAsString = null; + } + else + { + keyValueAsString = keyValue.ToString(); + } + } + } + } + + return keyValueAsString; + } + + private static VisualStudioSpec GetVisualStudioSpec(Version version) + { + ErrorUtilities.VerifyThrowArgument(s_visualStudioSpecDict.ContainsKey(version), "FrameworkLocationHelper.UnsupportedVisualStudioVersion", version); + return s_visualStudioSpecDict[version]; + } + + private static DotNetFrameworkSpec GetDotNetFrameworkSpec(Version version) + { + ErrorUtilities.VerifyThrowArgument(s_dotNetFrameworkSpecDict.ContainsKey(version), "FrameworkLocationHelper.UnsupportedFrameworkVersion", version); + return s_dotNetFrameworkSpecDict[version]; + } + + /// + /// Helper method to create an instance of for .net v4.x, + /// because most of attributes are the same for v4.x versions. + /// + /// .net framework version. + /// + private static DotNetFrameworkSpec CreateDotNetFrameworkSpecForV4(Version version, Version visualStudioVersion) + { + return new DotNetFrameworkSpec( + version, + dotNetFrameworkRegistryKey: dotNetFrameworkSetupRegistryPath + "\\v4\\Full", + dotNetFrameworkSetupRegistryInstalledName: "Install", + dotNetFrameworkVersionFolderPrefix: "v4.0", + dotNetFrameworkSdkRegistryToolsKey: "WinSDK-NetFx40Tools-x86", + dotNetFrameworkSdkRegistryInstallationFolderName: "InstallationFolder", + hasMSBuild: true, + visualStudioVersion: visualStudioVersion); + } + + private static void RedirectVersionsIfNecessary(ref Version dotNetFrameworkVersion, ref Version visualStudioVersion) + { + if (dotNetFrameworkVersion == dotNetFrameworkVersion45 && visualStudioVersion == visualStudioVersion100) + { + // There is no VS10 equivalent -- so just return the VS11 version + visualStudioVersion = visualStudioVersion110; + return; + } + + if (dotNetFrameworkVersion == dotNetFrameworkVersion35 && visualStudioVersion > visualStudioVersion110) + { + // Fall back to Dev11 location -- 3.5 tools MSI was reshipped unchanged, so there + // essentially are no 12-specific 3.5 tools. + visualStudioVersion = visualStudioVersion110; + return; + } + } + + #endregion + + private class VisualStudioSpec + { + /// + /// The version of this visual studio. + /// + private readonly Version _version; + + /// + /// The key in registry to indicate the corresponding .net framework in this visual studio. + /// i.e. 'v8.0A' for VS11. + /// + private readonly string _dotNetFrameworkSdkRegistryKey; + + /// + /// The key in registry to indicate the corresponding windows sdk, i.e. "v8.0" for VS11. + /// + private readonly string _windowsSdkRegistryKey; + + /// + /// The name in registry to indicate the sdk installation folder path, i.e. "InstallationFolder" for windows v8.0. + /// + private readonly string _windowsSdkRegistryInstallationFolderName; + + /// + /// The list of supported .net framework versions in this visual studio. + /// + private readonly Version[] _supportedDotNetFrameworkVersions; + + public VisualStudioSpec( + Version version, + string dotNetFrameworkSdkRegistryKey, + string windowsSdkRegistryKey, + string windowsSdkRegistryInstallationFolderName, + Version[] supportedDotNetFrameworkVersions) + { + _version = version; + _dotNetFrameworkSdkRegistryKey = dotNetFrameworkSdkRegistryKey; + _windowsSdkRegistryKey = windowsSdkRegistryKey; + _windowsSdkRegistryInstallationFolderName = windowsSdkRegistryInstallationFolderName; + _supportedDotNetFrameworkVersions = supportedDotNetFrameworkVersions; + } + + /// + /// The version of this visual studio. + /// + public Version Version + { + get { return _version; } + } + + /// + /// The key in registry to indicate the corresponding .net framework in this visual studio. + /// i.e. 'v8.0A' for VS11. + /// + public string DotNetFrameworkSdkRegistryKey + { + get { return _dotNetFrameworkSdkRegistryKey; } + } + + /// + /// The list of supported .net framework versions in this visual studio. + /// + public Version[] SupportedDotNetFrameworkVersions + { + get { return _supportedDotNetFrameworkVersions; } + } + + /// + /// The key in registry to indicate the corresponding windows sdk, i.e. "v8.0" for VS11. + /// + public string WindowsSdkRegistryKey + { + get { return _windowsSdkRegistryKey; } + } + + /// + /// The name in registry to indicate the sdk installation folder path, i.e. "InstallationFolder" for windows v8.0. + /// + public string WindowsSdkRegistryInstallationFolderName + { + get { return _windowsSdkRegistryInstallationFolderName; } + } + } + + private class DotNetFrameworkSpec + { + private const string HKLM = "HKEY_LOCAL_MACHINE"; + private const string MicrosoftSDKsRegistryKey = @"SOFTWARE\Microsoft\Microsoft SDKs"; + + /// + /// The version of this .net framework. + /// + protected readonly Version version; + + /// + /// The registry key of this .net framework, i.e. "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" for .net v4.5. + /// + protected readonly string dotNetFrameworkRegistryKey; + + /// + /// The name in registry to indicate that this .net framework is installed, i.e. "Install" for .net v4.5. + /// + protected readonly string dotNetFrameworkSetupRegistryInstalledName; + + /// + /// Folder prefix, i.e. v4.0 for .net v4.5. + /// + protected readonly string dotNetFrameworkFolderPrefix; + + /// + /// The key in registry to indicate the sdk tools folder, i.e. "WinSDK-NetFx40Tools-x86" for .net v4.5. + /// + protected readonly string dotNetFrameworkSdkRegistryToolsKey; + + /// + /// The name in registry to indicate the sdk installation folder path, i.e. "InstallationFolder" for .net v4.5. + /// + protected readonly string dotNetFrameworkSdkRegistryInstallationFolderName; + + /// + /// The version of visual studio that shipped with this .net framework. + /// + protected readonly Version visualStudioVersion; + + /// + /// Does this .net framework include MSBuild? + /// + protected readonly bool hasMSBuild; + + /// + /// Cached paths of .net framework on different architecture. + /// + protected readonly ConcurrentDictionary pathsToDotNetFramework; + + /// + /// Cached paths of .net framework sdk tools folder path on different visual studio version. + /// + protected readonly ConcurrentDictionary pathsToDotNetFrameworkSdkTools; + + /// + /// Cached path of the corresponding windows sdk. + /// + protected string pathToWindowsSdk; + + /// + /// Cached path of .net framework reference assemblies. + /// + protected string pathToDotNetFrameworkReferenceAssemblies; + + public DotNetFrameworkSpec( + Version version, + string dotNetFrameworkRegistryKey, + string dotNetFrameworkSetupRegistryInstalledName, + string dotNetFrameworkVersionFolderPrefix, + string dotNetFrameworkSdkRegistryToolsKey, + string dotNetFrameworkSdkRegistryInstallationFolderName, + bool hasMSBuild = true, + Version visualStudioVersion = null) + { + this.version = version; + this.visualStudioVersion = visualStudioVersion; + this.dotNetFrameworkRegistryKey = dotNetFrameworkRegistryKey; + this.dotNetFrameworkSetupRegistryInstalledName = dotNetFrameworkSetupRegistryInstalledName; + this.dotNetFrameworkFolderPrefix = dotNetFrameworkVersionFolderPrefix; + this.dotNetFrameworkSdkRegistryToolsKey = dotNetFrameworkSdkRegistryToolsKey; + this.dotNetFrameworkSdkRegistryInstallationFolderName = dotNetFrameworkSdkRegistryInstallationFolderName; + this.hasMSBuild = hasMSBuild; + this.pathsToDotNetFramework = new ConcurrentDictionary(); + this.pathsToDotNetFrameworkSdkTools = new ConcurrentDictionary(); + } + + /// + /// The version of this .net framework. + /// + public Version Version + { + get { return this.version; } + } + + /// + /// The name in registry to indicate the sdk installation folder path, i.e. "InstallationFolder" for .net v4.5. + /// + public string DotNetFrameworkSdkRegistryInstallationFolderName + { + get { return this.dotNetFrameworkSdkRegistryInstallationFolderName; } + } + + /// + /// Folder prefix, i.e. v4.0 for .net v4.5. + /// + public string DotNetFrameworkFolderPrefix + { + get { return this.dotNetFrameworkFolderPrefix; } + } + + private FrameworkName FrameworkName + { + get { return new FrameworkName(dotNetFrameworkIdentifier, this.version); } + } + + /// + /// Gets the full registry key of this .net framework Sdk for the given visual studio version. + /// i.e. "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.0A\WinSDK-NetFx40Tools-x86" for .net v4.5 on VS11. + /// + public virtual string GetDotNetFrameworkSdkRootRegistryKey(VisualStudioSpec visualStudioSpec) + { + return string.Join(@"\", HKLM, MicrosoftSDKsRegistryKey, visualStudioSpec.DotNetFrameworkSdkRegistryKey, this.dotNetFrameworkSdkRegistryToolsKey); + } + + /// + /// Gets the full path of .net framework for the given architecture. + /// + public virtual string GetPathToDotNetFramework(DotNetFrameworkArchitecture architecture) + { + string cachedPath; + if (this.pathsToDotNetFramework.TryGetValue(architecture, out cachedPath)) + { + return cachedPath; + } + + // Otherwise, check to see if we're even installed. If not, return null -- no point in setting the static + // variables to null when that's what they are already. + if (!CheckForFrameworkInstallation(this.dotNetFrameworkRegistryKey, this.dotNetFrameworkSetupRegistryInstalledName)) + { + return null; + } + + // We're installed and we haven't found this framework path yet -- so find it! + string generatedPathToDotNetFramework = + FindDotNetFrameworkPath( + Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName), + this.dotNetFrameworkFolderPrefix, + s_directoryExists, + s_getDirectories, + architecture + ); + + if (this.hasMSBuild && + generatedPathToDotNetFramework != null && + !File.Exists(Path.Combine(generatedPathToDotNetFramework, "msbuild.exe"))) // .net was improperly uninstalled: msbuild.exe isn't there + { + return null; + } + + if (!string.IsNullOrEmpty(generatedPathToDotNetFramework)) + { + pathsToDotNetFramework[architecture] = generatedPathToDotNetFramework; + } + + return generatedPathToDotNetFramework; + } + + /// + /// Gets the full path of .net framework sdk tools for the given visual studio version. + /// i.e. "C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\NETFX 4.0 Tools\" for .net v4.5 on VS11. + /// + public virtual string GetPathToDotNetFrameworkSdkTools(VisualStudioSpec visualStudioSpec) + { + string cachedPath; + if (this.pathsToDotNetFrameworkSdkTools.TryGetValue(visualStudioSpec.Version, out cachedPath)) + { + return cachedPath; + } + + string registryPath = string.Join(@"\", MicrosoftSDKsRegistryKey, visualStudioSpec.DotNetFrameworkSdkRegistryKey, this.dotNetFrameworkSdkRegistryToolsKey); + + // For the Dev10 SDK, we check the registry that corresponds to the current process' bitness, rather than + // always the 32-bit one the way we do for Dev11 and onward, since that's what we did in Dev10 as well. + // As of Dev11, the SDK reg keys are installed in the 32-bit registry. + RegistryView registryView = visualStudioSpec.Version == visualStudioVersion100 ? RegistryView.Default : RegistryView.Registry32; + + string generatedPathToDotNetFrameworkSdkTools = FindRegistryValueUnderKey( + registryPath, + this.dotNetFrameworkSdkRegistryInstallationFolderName, + registryView); + + if (string.IsNullOrEmpty(generatedPathToDotNetFrameworkSdkTools)) + { + // Fallback mechanisms. + + // Try to find explicit fallback rule. + // i.e. v4.5.1 on VS12 fallbacks to v4.5 on VS12. + bool foundExplicitRule = false; + for (int i = 0; i < s_explicitFallbackRulesForPathToDotNetFrameworkSdkTools.GetLength(0); ++i) + { + var trigger = s_explicitFallbackRulesForPathToDotNetFrameworkSdkTools[i, 0]; + if (trigger.Item1 == this.version && trigger.Item2 == visualStudioSpec.Version) + { + foundExplicitRule = true; + var fallback = s_explicitFallbackRulesForPathToDotNetFrameworkSdkTools[i, 1]; + generatedPathToDotNetFrameworkSdkTools = FallbackToPathToDotNetFrameworkSdkToolsInPreviousVersion(fallback.Item1, fallback.Item2); + break; + } + } + + // Otherwise, fallback to previous VS. + // i.e. fallback to v110 if the current visual studio version is v120. + if (!foundExplicitRule) + { + int index = Array.IndexOf(s_visualStudioSpecs, visualStudioSpec); + if (index > 0) + { + // The items in the array "visualStudioSpecs" must be ordered by version. That would allow us to fallback to the previous visual studio version easily. + VisualStudioSpec fallbackVisualStudioSpec = s_visualStudioSpecs[index - 1]; + generatedPathToDotNetFrameworkSdkTools = FallbackToPathToDotNetFrameworkSdkToolsInPreviousVersion(this.version, fallbackVisualStudioSpec.Version); + } + } + } + + if (string.IsNullOrEmpty(generatedPathToDotNetFrameworkSdkTools)) + { + // Fallback to "default" ultimately. + generatedPathToDotNetFrameworkSdkTools = FallbackToDefaultPathToDotNetFrameworkSdkTools(this.version); + } + + if (!string.IsNullOrEmpty(generatedPathToDotNetFrameworkSdkTools)) + { + this.pathsToDotNetFrameworkSdkTools[visualStudioSpec.Version] = generatedPathToDotNetFrameworkSdkTools; + } + + return generatedPathToDotNetFrameworkSdkTools; + } + + /// + /// Gets the full path of .net framework sdk. + /// i.e. "C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\" for .net v4.5 on VS11. + /// + public virtual string GetPathToDotNetFrameworkSdk(VisualStudioSpec visualStudioSpec) + { + string pathToBinRoot = this.GetPathToDotNetFrameworkSdkTools(visualStudioSpec); + pathToBinRoot = RemoveDirectories(pathToBinRoot, 2); + return pathToBinRoot; + } + + /// + /// Gets the full path of reference assemblies folder. + /// i.e. "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\" for .net v4.5. + /// + public virtual string GetPathToDotNetFrameworkReferenceAssemblies() + { + if (this.pathToDotNetFrameworkReferenceAssemblies == null) + { + // when a user requests the 40 reference assembly path we dont need to read the redist list because we will not be chaining so we may as well just + // generate the path and save us some time. + string referencePath = GenerateReferenceAssemblyPath(FrameworkLocationHelper.programFilesReferenceAssemblyLocation, this.FrameworkName); + if (Directory.Exists(referencePath)) + { + this.pathToDotNetFrameworkReferenceAssemblies = FileUtilities.EnsureTrailingSlash(referencePath); + } + } + + return this.pathToDotNetFrameworkReferenceAssemblies; + } + + /// + /// Gets the full path of the corresponding windows sdk shipped with this .net framework. + /// i.e. "C:\Program Files (x86)\Windows Kits\8.0\" for v8.0 (shipped with .net v4.5 and VS11). + /// + public virtual string GetPathToWindowsSdk() + { + if (this.pathToWindowsSdk == null) + { + ErrorUtilities.VerifyThrowArgument(this.visualStudioVersion != null, "FrameworkLocationHelper.UnsupportedFrameworkVersionForWindowsSdk", this.version); + + var visualStudioSpec = GetVisualStudioSpec(this.visualStudioVersion); + + if (string.IsNullOrEmpty(visualStudioSpec.WindowsSdkRegistryKey) || string.IsNullOrEmpty(visualStudioSpec.WindowsSdkRegistryInstallationFolderName)) + { + ErrorUtilities.ThrowArgument("FrameworkLocationHelper.UnsupportedFrameworkVersionForWindowsSdk", this.version); + } + + string registryPath = string.Join(@"\", MicrosoftSDKsRegistryKey, "Windows", visualStudioSpec.WindowsSdkRegistryKey); + + // As of Dev11, the SDK reg keys are installed in the 32-bit registry. + this.pathToWindowsSdk = FindRegistryValueUnderKey( + registryPath, + visualStudioSpec.WindowsSdkRegistryInstallationFolderName, + RegistryView.Registry32); + } + + return this.pathToWindowsSdk; + } + + protected static string FallbackToPathToDotNetFrameworkSdkToolsInPreviousVersion(Version dotNetFrameworkVersion, Version visualStudioVersion) + { + VisualStudioSpec visualStudioSpec; + DotNetFrameworkSpec dotNetFrameworkSpec; + if (s_visualStudioSpecDict.TryGetValue(visualStudioVersion, out visualStudioSpec) + && s_dotNetFrameworkSpecDict.TryGetValue(dotNetFrameworkVersion, out dotNetFrameworkSpec) + && visualStudioSpec.SupportedDotNetFrameworkVersions.Contains(dotNetFrameworkVersion)) + { + return dotNetFrameworkSpec.GetPathToDotNetFrameworkSdkTools(visualStudioSpec); + } + + return null; + } + + protected static string FallbackToDefaultPathToDotNetFrameworkSdkTools(Version dotNetFrameworkVersion) + { + if (dotNetFrameworkVersion.Major == 4) + { + return FrameworkLocationHelper.PathToV4ToolsInFallbackDotNetFrameworkSdk; + } + + if (dotNetFrameworkVersion == dotNetFrameworkVersion35) + { + return FrameworkLocationHelper.PathToV35ToolsInFallbackDotNetFrameworkSdk; + } + + return null; + } + } + + /// + /// Specialized implementation for legacy .net framework v1.1 and v2.0. + /// + private class DotNetFrameworkSpecLegacy : DotNetFrameworkSpec + { + private string _pathToDotNetFrameworkSdkTools; + + public DotNetFrameworkSpecLegacy( + Version version, + string dotNetFrameworkRegistryKey, + string dotNetFrameworkSetupRegistryInstalledName, + string dotNetFrameworkVersionFolderPrefix, + string dotNetFrameworkSdkRegistryInstallationFolderName, + bool hasMSBuild) + : base(version, + dotNetFrameworkRegistryKey, + dotNetFrameworkSetupRegistryInstalledName, + dotNetFrameworkVersionFolderPrefix, + null, + dotNetFrameworkSdkRegistryInstallationFolderName, + hasMSBuild) + { + } + + /// + /// Gets the full registry key of this .net framework Sdk for the given visual studio version. + /// i.e. "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework" for v1.1 and v2.0. + /// + public override string GetDotNetFrameworkSdkRootRegistryKey(VisualStudioSpec visualStudioSpec) + { + return FrameworkLocationHelper.fullDotNetFrameworkRegistryKey; + } + + /// + /// Gets the full path of .net framework sdk tools for the given visual studio version. + /// + public override string GetPathToDotNetFrameworkSdkTools(VisualStudioSpec visualStudioSpec) + { + if (_pathToDotNetFrameworkSdkTools == null) + { + _pathToDotNetFrameworkSdkTools = FindRegistryValueUnderKey( + dotNetFrameworkRegistryPath, + this.dotNetFrameworkSdkRegistryInstallationFolderName); + } + + return _pathToDotNetFrameworkSdkTools; + } + + /// + /// Gets the full path of .net framework sdk, which is the full path of .net framework sdk tools for v1.1 and v2.0. + /// + public override string GetPathToDotNetFrameworkSdk(VisualStudioSpec visualStudioSpec) + { + return this.GetPathToDotNetFrameworkSdkTools(visualStudioSpec); + } + + /// + /// Gets the full path of reference assemblies folder, which is the full path of .net framework for v1.1 and v2.0. + public override string GetPathToDotNetFrameworkReferenceAssemblies() + { + return this.GetPathToDotNetFramework(DotNetFrameworkArchitecture.Current); + } + } + + /// + /// Specialized implementation for legacy .net framework v3.0 and v3.5. + /// + private class DotNetFrameworkSpecV3 : DotNetFrameworkSpec + { + public DotNetFrameworkSpecV3( + Version version, + string dotNetFrameworkRegistryKey, + string dotNetFrameworkSetupRegistryInstalledName, + string dotNetFrameworkVersionFolderPrefix, + string dotNetFrameworkSdkRegistryToolsKey, + string dotNetFrameworkSdkRegistryInstallationFolderName, + bool hasMSBuild) + : base(version, + dotNetFrameworkRegistryKey, + dotNetFrameworkSetupRegistryInstalledName, + dotNetFrameworkVersionFolderPrefix, + dotNetFrameworkSdkRegistryToolsKey, + dotNetFrameworkSdkRegistryInstallationFolderName, + hasMSBuild) + { + } + + /// + /// Gets the full path of .net framework sdk. + /// i.e. "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\" for .net v3.5 on VS11. + /// + public override string GetPathToDotNetFrameworkSdk(VisualStudioSpec visualStudioSpec) + { + string pathToBinRoot = this.GetPathToDotNetFrameworkSdkTools(visualStudioSpec); + pathToBinRoot = RemoveDirectories(pathToBinRoot, 1); + return pathToBinRoot; + } + + /// + /// Gets the full path of reference assemblies folder. + /// i.e. "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\v3.5\" for v3.5. + /// + public override string GetPathToDotNetFrameworkReferenceAssemblies() + { + if (this.pathToDotNetFrameworkReferenceAssemblies == null) + { + this.pathToDotNetFrameworkReferenceAssemblies = FindRegistryValueUnderKey( + dotNetFrameworkAssemblyFoldersRegistryPath + "\\" + this.dotNetFrameworkFolderPrefix, + referenceAssembliesRegistryValueName); + + if (this.pathToDotNetFrameworkReferenceAssemblies == null) + { + this.pathToDotNetFrameworkReferenceAssemblies = GenerateReferenceAssemblyDirectory(this.dotNetFrameworkFolderPrefix); + } + } + + return this.pathToDotNetFrameworkReferenceAssemblies; + } + } + } +} diff --git a/src/Shared/FxCopExclusions/Microsoft.Build.Shared.Suppressions.cs b/src/Shared/FxCopExclusions/Microsoft.Build.Shared.Suppressions.cs new file mode 100644 index 00000000000..b9ec8edc123 --- /dev/null +++ b/src/Shared/FxCopExclusions/Microsoft.Build.Shared.Suppressions.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// FxCop Suppression file +// To Use: +// Add module level suppressions to this file to have them suppressed in the assembly +// + +using System.Diagnostics.CodeAnalysis; + +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#GetExplicitMSBuildArchitecture(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#GetExplicitMSBuildRuntime(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsBadlyCasedSpecialTaskAttribute(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsNonBatchingTargetAttribute(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsSpecialTaskAttribute(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsValidMSBuildArchitectureValue(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsValidMSBuildRuntimeValue(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#RuntimeValuesMatch(System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#TryMergeArchitectureValues(System.String,System.String,System.String&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#TryMergeRuntimeValues(System.String,System.String,System.String&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#ArchitectureValuesMatch(System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.TypeLoader.#ReflectionOnlyLoad(System.String,Microsoft.Build.Shared.AssemblyLoadInfo)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared+PROCESS_BASIC_INFORMATION.#get_Size()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#WaitForMultipleObjects(System.UInt32,System.IntPtr[],System.Boolean,System.UInt32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SetErrorMode_Win7AndNewer(System.Int32,System.Int32&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SetErrorMode_VistaAndOlder(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SetErrorMode(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SearchPath(System.String,System.String,System.String,System.Int32,System.Text.StringBuilder,System.Int32[])", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle,System.Byte[],System.UInt32,System.UInt32&,System.IntPtr)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#OpenProcess(Microsoft.Build.Shared.NativeMethodsShared+eDesiredAccess,System.Boolean,System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#NtQueryInformationProcess(Microsoft.Build.Shared.NativeMethodsShared+SafeProcessHandle,Microsoft.Build.Shared.NativeMethodsShared+PROCESSINFOCLASS,Microsoft.Build.Shared.NativeMethodsShared+PROCESS_BASIC_INFORMATION&,System.Int32,System.Int32&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle,System.TimeSpan)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle,System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#LoadLibrary(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#KillTree(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#HResultSucceeded(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#HResultFailed(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GlobalMemoryStatusEx(Microsoft.Build.Shared.NativeMethodsShared+MemoryStatus)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetSystemInfo(Microsoft.Build.Shared.NativeMethodsShared+SYSTEM_INFO&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetStdHandle(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetShortPathName(System.String,System.Text.StringBuilder,System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetShortFilePath(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetRequestedRuntimeInfo(System.String,System.String,System.String,System.UInt32,System.UInt32,System.Text.StringBuilder,System.Int32,System.UInt32&,System.Text.StringBuilder,System.Int32,System.UInt32&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetProcAddress(System.IntPtr,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetParentProcessId(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetOEMCP()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetNativeSystemInfo(Microsoft.Build.Shared.NativeMethodsShared+SYSTEM_INFO&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetModuleFileName(System.Runtime.InteropServices.HandleRef,System.Text.StringBuilder,System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetMemoryStatus()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLongPathName(System.String,System.Text.StringBuilder,System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#CreatePipe(Microsoft.Win32.SafeHandles.SafeFileHandle&,Microsoft.Win32.SafeHandles.SafeFileHandle&,Microsoft.Build.Shared.NativeMethodsShared+SecurityAttributes,System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#FindOnPath(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#FreeLibrary(System.IntPtr)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetChildProcessIds(System.Int32,System.DateTime)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetCurrentDirectory()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetCurrentDirectory(System.Int32,System.Text.StringBuilder)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetFileAttributesEx(System.String,System.Int32,Microsoft.Build.Shared.NativeMethodsShared+WIN32_FILE_ATTRIBUTE_DATA&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetFileType(System.IntPtr)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLastWriteDirectoryUtcTime(System.String,System.DateTime&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLastWriteFileUtcTime(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLongFilePath(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#IsMetaprojectFilename(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#IsSolutionFilename(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#IsVCProjFilename(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#MakeRelative(System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#TrimAndStripAnyQuotes(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities+ItemSpecModifiers.#GetItemSpecModifier(System.String,System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LoadedType.#.ctor(System.Type,Microsoft.Build.Shared.AssemblyLoadInfo)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LogMessagePacketBase.#.ctor(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LogMessagePacketBase.#get_EventType()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LogMessagePacketBase.#get_NodeBuildEvent()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#CoWaitForMultipleHandles(Microsoft.Build.Shared.NativeMethodsShared+COWAIT_FLAGS,System.Int32,System.Int32,System.IntPtr[],System.Int32&)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#EnsureNoLeadingSlash(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#EnsureNoTrailingSlash(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#EnsureTrailingSlash(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_ExecutingAssemblyPath()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#FileExistsNoThrow(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#FileOrDirectoryExistsNoThrow(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetCacheDirectory()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetDirectoryNameOfFullPath(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetHexHash(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetPathsHash(System.Collections.Generic.IEnumerable`1)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#HasExtension(System.String,System.String[])", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#set_Line(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#ClearCacheDirectory()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#CreateUriFromPath(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutableConfigurationFilePath()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutableDirectory()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutableName()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutablePath()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#DeleteDirectoryNoThrow(System.String,System.Boolean)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#DeleteNoThrow(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#DirectoryExistsNoThrow(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#GetXmlLineAndColumn(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#IsIoRelatedException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#IsXmlException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedFunctionException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedIoOrXmlException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedReflectionException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedRegistryException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedSerializationException(System.Exception)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#get_Column()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#set_Column(System.Int32)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#get_Line()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String,System.Object,System.Object,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgumentArraysSameLength(System.Array,System.Array,System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgumentLengthIfNotNull(System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgumentOutOfRange(System.Boolean,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInternalLength(System.String,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInternalLockHeld(System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInternalRooted(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInvalidOperation(System.Boolean,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInvalidOperation(System.Boolean,System.String,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInvalidOperation(System.Boolean,System.String,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.EscapingUtilities.#ContainsEscapedWildcards(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#ThrowIfTypeDoesNotImplementToString(System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrow(System.Boolean,System.String,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrow(System.Boolean,System.String,System.Object,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrow(System.Boolean,System.String,System.Object,System.Object,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String,System.Object,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String,System.Object,System.Object,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String,System.Object,System.Object,System.Object)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension,Microsoft.Build.Shared.PartialComparisonFlags)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension,Microsoft.Build.Shared.PartialComparisonFlags,System.Boolean)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension,System.Boolean)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_ProcessorArchitecture()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_RemappedFromEnumerator()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#ReplaceVersion(System.Version)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_UnnamedAssembly()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.CollectionHelpers.#RemoveNulls`1(System.Collections.Generic.List`1)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#DebugTraceMessage(System.String,System.String,System.Object[])", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#ThrowArgument(System.String,System.Object[])", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#Clone()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CloneImmutable()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CompareTo(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#EqualsIgnoreVersion(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#EscapeDisplayNameCharacters(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#GetAssemblyNameEx(System.String)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_HasProcessorArchitectureInFusionName()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_Immutable()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_IsSimpleName()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_IsUnnamedAssembly()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#MarkImmutable()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyLoadInfo.#FactoryForTranslation(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#.ctor(System.String,System.Boolean)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#AddRemappedAssemblyName(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_AssemblyName()", Justification="Source file used by several assemblies and this method is not called by all of them.")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutableName()", Justification="Source file used by several assemblies and this method is not called by all of them.")] + +#endif diff --git a/src/Shared/HybridDictionary.cs b/src/Shared/HybridDictionary.cs new file mode 100644 index 00000000000..352039bdc6c --- /dev/null +++ b/src/Shared/HybridDictionary.cs @@ -0,0 +1,953 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dictionary which changes its backing store to keep memory use low. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Runtime.Serialization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// HybridDictionary is a dictionary which is implemented to efficiently store both small and large numbers of items. When only a single item is stored, we use no + /// collections at all. When 1 < n <= MaxListSize is stored, we use a list. For any larger number of elements, we use a dictionary. + /// + /// The key type + /// The value type + [Serializable] + internal class HybridDictionary : IDictionary, IDictionary, ICollection where TValue : class + { + /// + /// The maximum number of entries we will store in a list before converting it to a dictionary. + /// + internal static readonly int MaxListSize = 15; + + /// + /// The dictionary, list, or pair used for a store + /// + private Object _store; + + /// + /// The comparer used to look up an item. + /// + private IEqualityComparer _comparer; + + /// + /// Static constructor + /// + static HybridDictionary() + { + int value; + if (Int32.TryParse(System.Environment.GetEnvironmentVariable("MSBuildHybridDictThreshold"), out value)) + { + MaxListSize = value; + } + } + + /// + /// Default constructor. + /// + public HybridDictionary() + : this(0) + { + } + + /// + /// Capacity constructor. + /// + /// The initial capacity of the collection. + public HybridDictionary(int capacity) + : this(capacity, EqualityComparer.Default) + { + } + + /// + /// Constructor. + /// + /// The comparer to use. + public HybridDictionary(IEqualityComparer comparer) + : this() + { + _comparer = comparer; + } + + /// + /// Constructor. + /// + /// The initial capacity. + /// The comparer to use. + public HybridDictionary(int capacity, IEqualityComparer comparer) + { + _comparer = comparer; + if (_comparer == null) + { + _comparer = EqualityComparer.Default; + } + + if (capacity > MaxListSize) + { + _store = new Dictionary(capacity, comparer); + } + else if (capacity > 1) + { + _store = new List>(capacity); + } + } + + /// + /// Serialization consturctor. + /// + public HybridDictionary(SerializationInfo info, StreamingContext context) + { + throw new NotImplementedException(); + } + + /// + /// Cloning constructor. + /// + public HybridDictionary(HybridDictionary other, IEqualityComparer comparer) + : this(other.Count, comparer) + { + foreach (KeyValuePair keyValue in other) + { + Add(keyValue.Key, keyValue.Value); + } + } + + /// + /// Gets the comparer used to compare keys. + /// + public IEqualityComparer Comparer + { + get { return _comparer; } + } + + /// + /// Returns the collection of keys in the dictionary. + /// + public ICollection Keys + { + get + { + if (_store == null) + { + return ReadOnlyEmptyCollection.Instance; + } + + if (_store is KeyValuePair) + { + return new TKey[] { ((KeyValuePair)_store).Key }; + } + + var list = _store as List>; + if (list != null) + { + TKey[] keys = new TKey[list.Count]; + for (int i = 0; i < list.Count; i++) + { + keys[i] = list[i].Key; + } + + return keys; + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + return dictionary.Keys; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } + + /// + /// Returns the collection of values in the dictionary. + /// + public ICollection Values + { + get + { + if (_store == null) + { + return ReadOnlyEmptyCollection.Instance; + } + + if (_store is KeyValuePair) // Can't use 'as' for structs + { + return new TValue[] { ((KeyValuePair)_store).Value }; + } + + var list = _store as List>; + if (list != null) + { + TValue[] values = new TValue[list.Count]; + for (int i = 0; i < list.Count; i++) + { + values[i] = list[i].Value; + } + + return values; + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + return dictionary.Values; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } + + /// + /// Gets the number of items in the dictionary. + /// + public int Count + { + get + { + if (_store == null) + { + return 0; + } + + if (_store is KeyValuePair) + { + return 1; + } + + return ((ICollection)_store).Count; + } + } + + /// + /// Returns true if this is a read-only collection. + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Returns true if this collection is synchronized. + /// + public bool IsSynchronized + { + get { return false; } + } + + /// + /// Gets the sync root for this collection. + /// + /// + /// NOTE: Returns "this", which is not normally recommended as a caller + /// could implement its own locking scheme on "this" and deadlock. However, a + /// sync object would be significant wasted space as there are a lot of these, + /// and the caller is not foolish. + /// + public object SyncRoot + { + get { return this; } + } + + /// + /// Returns true if the dictionary is a fixed size. + /// + public bool IsFixedSize + { + get { return false; } + } + + /// + /// Returns a collection of the keys in the dictionary. + /// + ICollection IDictionary.Keys + { + get { return (ICollection)((IDictionary)this).Keys; } + } + + /// + /// Returns a collection of the values in the dictionary. + /// + ICollection IDictionary.Values + { + get { return (ICollection)((IDictionary)this).Values; } + } + + /// + /// Item accessor. + /// + public TValue this[TKey key] + { + get + { + TValue value; + if (TryGetValue(key, out value)) + { + return value; + } + + throw new KeyNotFoundException("The specified key was not found in the collection."); + } + + set + { + if (_store == null) + { + _store = new KeyValuePair(key, value); + return; + } + + if (_store is KeyValuePair) + { + var single = ((KeyValuePair)_store); + if (_comparer.Equals(single.Key, key)) + { + _store = new KeyValuePair(key, value); + return; + } + + _store = new List> { { single }, { new KeyValuePair(key, value) } }; + return; + } + + var list = _store as List>; + if (list != null) + { + AddToOrUpdateList(list, key, value, throwIfPresent: false); + return; + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + dictionary[key] = value; + return; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + + /// + /// Item accessor. + /// + public object this[object key] + { + get { return (Object)this[key]; } + set { this[key] = value; } + } + + /// + /// Adds an item to the dictionary. + /// + public void Add(TKey key, TValue value) + { + ErrorUtilities.VerifyThrowArgumentNull(key, "key"); + + if (_store == null) + { + _store = new KeyValuePair(key, value); + return; + } + + if (_store is KeyValuePair) + { + var single = ((KeyValuePair)_store); + if (_comparer.Equals(single.Key, key)) + { + throw new ArgumentException("A value with the same key is already in the collection."); + } + + _store = new List> { { single }, { new KeyValuePair(key, value) } }; + return; + } + + var list = _store as List>; + if (list != null) + { + AddToOrUpdateList(list, key, value, throwIfPresent: true); + return; + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + dictionary.Add(key, value); + return; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Returns true if the specified key is contained within the dictionary. + /// + public bool ContainsKey(TKey key) + { + TValue discard; + return TryGetValue(key, out discard); + } + + /// + /// Removes a key from the dictionary. + /// + public bool Remove(TKey key) + { + ErrorUtilities.VerifyThrowArgumentNull(key, "key"); + + if (_store == null) + { + return false; + } + + if (_store is KeyValuePair) + { + if (_comparer.Equals(((KeyValuePair)_store).Key, key)) + { + _store = null; + return true; + } + + return false; + } + + var list = _store as List>; + if (list != null) + { + for (int i = 0; i < list.Count; i++) + { + if (_comparer.Equals(list[i].Key, key)) + { + list.RemoveAt(i); // POLICY: copy into new shorter list + return true; + } + } + + return false; + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + return dictionary.Remove(key); + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return false; + } + + /// + /// Returns true and the value for the specified key if it is present in the dictionary, false otherwise. + /// + public bool TryGetValue(TKey key, out TValue value) + { + value = null; + + if (_store == null) + { + return false; + } + + if (_store is KeyValuePair) + { + var single = ((KeyValuePair)_store); + if (_comparer.Equals(single.Key, key)) + { + value = single.Value; + return true; + } + else + { + return false; + } + } + + var list = _store as List>; + if (list != null) + { + foreach (var entry in list) + { + if (_comparer.Equals(entry.Key, key)) + { + value = entry.Value; + return true; + } + } + + return false; + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + return dictionary.TryGetValue(key, out value); + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return false; + } + + /// + /// Adds a key/value pair to the dictionary. + /// + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + /// Clears the dictionary. + /// + public void Clear() + { + _store = null; + } + + /// + /// Returns true of the dictionary contains the key/value pair. + /// + public bool Contains(KeyValuePair item) + { + TValue value; + return (TryGetValue(item.Key, out value) && (item.Value == value)); + } + + /// + /// Copies the contents of the dictionary to the specified array. + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + int i = arrayIndex; + foreach (var entry in this) + { + array[i] = new KeyValuePair(entry.Key, entry.Value); + } + } + + /// + /// Removed the specified key/value pair from the dictionary. + /// NOT IMPLEMENTED. + /// + public bool Remove(KeyValuePair item) + { + return (Contains(item) && Remove(item.Key)); + } + + /// + /// Gets an enumerator over the key/value pairs in the dictionary. + /// + public IEnumerator> GetEnumerator() + { + if (_store == null) + { + return ReadOnlyEmptyCollection>.Instance.GetEnumerator(); + } + + if (_store is KeyValuePair) + { + return new SingleEnumerator((KeyValuePair)_store); + } + + var list = _store as List>; + if (list != null) + { + return list.GetEnumerator(); + } + + var dictionary = _store as Dictionary; + if (dictionary != null) + { + return dictionary.GetEnumerator(); + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + /// + /// Gets an enumerator over the key/value pairs in the dictionary. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Copies the contents of the dictionary to the specified Array. + /// + public void CopyTo(Array array, int index) + { + int i = index; + foreach (var entry in this) + { + array.SetValue(new DictionaryEntry(entry.Key, entry.Value), i); + } + } + + /// + /// Adds the specified key/value pair to the dictionary. + /// + public void Add(object key, object value) + { + Add((TKey)key, (TValue)value); + } + + /// + /// Returns true if the dictionary contains the specified key. + /// + public bool Contains(object key) + { + return ContainsKey((TKey)key); + } + + /// + /// Returns an enumerator over the key/value pairs in the dictionary. + /// + IDictionaryEnumerator IDictionary.GetEnumerator() + { + if (_store == null) + { + return ((IDictionary)(ReadOnlyEmptyDictionary.Instance)).GetEnumerator(); + } + + if (_store is KeyValuePair) + { + return new SingleDictionaryEntryEnumerator(new DictionaryEntry(((KeyValuePair)_store).Key, ((KeyValuePair)_store).Value)); + } + + var list = _store as List>; + if (list != null) + { + return new ListDictionaryEntryEnumerator(list); + } + + var dictionary = _store as IDictionary; + if (dictionary != null) + { + return dictionary.GetEnumerator(); + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + /// + /// Removes the specified key from the dictionary. + /// + public void Remove(object key) + { + Remove((TKey)key); + } + + /// + /// Adds a value to the list, growing it to a dictionary if necessary + /// + private void AddToOrUpdateList(List> list, TKey key, TValue value, bool throwIfPresent) + { + if (list.Count < MaxListSize) // POLICY: Threshold balancing lookup time vs. space + { + for (int i = 0; i < list.Count; i++) + { + if (_comparer.Equals(list[i].Key, key)) + { + if (throwIfPresent) + { + throw new ArgumentException("A value with the same key is already in the collection."); + } + + list[i] = new KeyValuePair(key, value); + return; + } + } + + list.Add(new KeyValuePair(key, value)); + return; + } + else + { + var newDictionary = new Dictionary(list.Count + 1, _comparer); // POLICY: Don't aggressively encourage extra capacity + foreach (KeyValuePair entry in list) + { + newDictionary.Add(entry.Key, entry.Value); + } + + if (throwIfPresent) + { + newDictionary.Add(key, value); + } + else + { + newDictionary[key] = value; + } + + _store = newDictionary; + return; + } + } + + /// + /// An enumerator for when the dictionary has only a single entry in it. + /// + private struct SingleEnumerator : IEnumerator> + { + /// + /// The single value. + /// + private KeyValuePair _value; + + /// + /// Flag indicating when we are at the end of the enumeration. + /// + private bool _enumerationComplete; + + /// + /// Constructor. + /// + public SingleEnumerator(KeyValuePair value) + { + _value = value; + _enumerationComplete = false; + } + + /// + /// Gets the current value. + /// + public KeyValuePair Current + { + get + { + if (_enumerationComplete) + { + return _value; + } + + throw new InvalidOperationException("Past end of enumeration"); + } + } + + /// + /// Gets the current value. + /// + object IEnumerator.Current + { + get { return ((IEnumerator>)this).Current; } + } + + /// + /// Disposer. + /// + public void Dispose() + { + } + + /// + /// Moves to the next item. + /// + public bool MoveNext() + { + if (!_enumerationComplete) + { + _enumerationComplete = true; + return true; + } + + return false; + } + + /// + /// Resets the enumerator. + /// + public void Reset() + { + _enumerationComplete = false; + } + } + + /// + /// An enumerator for when the dictionary has only a single entry in it. + /// Cannot find a way to make the SingleEntryEnumerator serve both purposes, as foreach preferentially + /// casts to IEnumerable that returns the generic enumerator instead of an IDictionaryEnumerator. + /// + /// Don't want to use the List enumerator below as a throwaway one-entry list would need to be allocated. + /// + private struct SingleDictionaryEntryEnumerator : IDictionaryEnumerator + { + /// + /// The single value. + /// + private DictionaryEntry _value; + + /// + /// Flag indicating when we are at the end of the enumeration. + /// + private bool _enumerationComplete; + + /// + /// Constructor. + /// + public SingleDictionaryEntryEnumerator(DictionaryEntry value) + { + _value = value; + _enumerationComplete = false; + } + + /// + /// Key + /// + public object Key + { + get { return Entry.Key; } + } + + /// + /// Value + /// + public object Value + { + get { return Entry.Value; } + } + + /// + /// Current + /// + public object Current + { + get { return Entry; } + } + + /// + /// Gets the current value. + /// + public DictionaryEntry Entry + { + get + { + if (_enumerationComplete) + { + return _value; + } + + throw new InvalidOperationException("Past end of enumeration"); + } + } + + /// + /// Disposer. + /// + public void Dispose() + { + } + + /// + /// Moves to the next item. + /// + public bool MoveNext() + { + if (!_enumerationComplete) + { + _enumerationComplete = true; + return true; + } + + return false; + } + + /// + /// Resets the enumerator. + /// + public void Reset() + { + _enumerationComplete = false; + } + } + + /// + /// An enumerator for a list of KVP that implements IDictionaryEnumerator + /// + /// Key type + /// Value type + private struct ListDictionaryEntryEnumerator : IDictionaryEnumerator + { + /// + /// The value. + /// + private IEnumerator> _enumerator; + + /// + /// Constructor. + /// + public ListDictionaryEntryEnumerator(List> list) + { + _enumerator = list.GetEnumerator(); + } + + /// + /// Key + /// + public object Key + { + get { return _enumerator.Current.Key; } + } + + /// + /// Value + /// + public object Value + { + get { return _enumerator.Current.Value; } + } + + /// + /// Current + /// + public object Current + { + get { return Entry; } + } + + /// + /// Gets the current value. + /// + public DictionaryEntry Entry + { + get { return new DictionaryEntry(_enumerator.Current.Key, _enumerator.Current.Value); } + } + + /// + /// Disposer. + /// + public void Dispose() + { + } + + /// + /// Moves to the next item. + /// + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + /// + /// Resets the enumerator. + /// + public void Reset() + { + _enumerator.Reset(); + } + } + } +} \ No newline at end of file diff --git a/src/Shared/IElementLocation.cs b/src/Shared/IElementLocation.cs new file mode 100644 index 00000000000..365226df26f --- /dev/null +++ b/src/Shared/IElementLocation.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An internal interface used to represent element locations for run-time error reporting. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.Shared +{ + /// + /// Represents the location information for error reporting purposes. This is normally used to + /// associate a run-time error with the original XML. + /// This is not used for arbitrary errors from tasks, which store location in a BuildXXXXEventArgs. + /// All implementations should be IMMUTABLE. + /// This is not public because the current implementation only provides correct data for unedited projects. + /// DO NOT make it public without considering a solution to this problem. + /// + internal interface IElementLocation : INodePacketTranslatable + { + /// + /// The file from which this particular element originated. It may + /// differ from the ProjectFile if, for instance, it was part of + /// an import or originated in a targets file. + /// Should always have a value. + /// If not known, returns empty string. + /// + string File + { + get; + } + + /// + /// The line number where this element exists in its file. + /// The first line is numbered 1. + /// Zero indicates "unknown location". + /// + int Line + { + get; + } + + /// + /// The column number where this element exists in its file. + /// The first column is numbered 1. + /// Zero indicates "unknown location". + /// + int Column + { + get; + } + + /// + /// The location in a form suitable for replacement + /// into a message. + /// + string LocationString + { + get; + } + } +} diff --git a/src/Shared/IKeyed.cs b/src/Shared/IKeyed.cs new file mode 100644 index 00000000000..b459229d03f --- /dev/null +++ b/src/Shared/IKeyed.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface allowing items and metadata and properties to go into keyed collections +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Collections +{ + /// + /// Interface allowing items and metadata and properties to go into keyed collections + /// + /// + /// This can be internal as it is a constraint only on internal collections. + /// + internal interface IKeyed + { + /// + /// Returns some value useful for a key in a dictionary + /// + string Key + { + get; + } + } +} \ No newline at end of file diff --git a/src/Shared/INodeEndpoint.cs b/src/Shared/INodeEndpoint.cs new file mode 100644 index 00000000000..71682113b21 --- /dev/null +++ b/src/Shared/INodeEndpoint.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for node endpoints. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + #region Delegates + /// + /// Used to receive link status updates from an endpoint. + /// + /// The endpoint invoking the delegate. + /// The current status of the link. + internal delegate void LinkStatusChangedDelegate(INodeEndpoint endpoint, LinkStatus status); + + /// + /// Used to receive data from a node + /// + /// The endpoint invoking the delegate. + /// The packet received. + internal delegate void DataReceivedDelegate(INodeEndpoint endpoint, INodePacket packet); + #endregion + + #region Enums + /// + /// The connection status of a link between the NodeEndpoint on the host and the NodeEndpoint + /// on the peer. + /// + internal enum LinkStatus + { + /// + /// The connection has never been started. + /// + Inactive, + + /// + /// The connection is active, the most recent data has been successfully sent, and the + /// node is responding to pings. + /// + Active, + + /// + /// The connection has failed and been terminated. + /// + Failed, + + /// + /// The connection could not be made/timed out. + /// + ConnectionFailed, + } + + #endregion + + /// + /// This interface represents one end of a connection between the INodeProvider and a Node. + /// Implementations of this interface define the actual mechanism by which data is communicated. + /// + internal interface INodeEndpoint + { + #region Events + + /// + /// Raised when the status of the node's link has changed. + /// + event LinkStatusChangedDelegate OnLinkStatusChanged; + + #endregion + + #region Properties + + /// + /// The current link status for this endpoint. + /// + LinkStatus LinkStatus + { + get; + } + #endregion + + #region Methods + /// + /// Waits for the remote node to establish a connection. + /// + /// The factory used to deserialize packets. + /// Only one of Listen() or Connect() may be called on an endpoint. + void Listen(INodePacketFactory factory); + + /// + /// Instructs the node to connect to its peer endpoint. + /// + /// The factory used to deserialize packets. + void Connect(INodePacketFactory factory); + + /// + /// Instructs the node to disconnect from its peer endpoint. + /// + void Disconnect(); + + /// + /// Sends a data packet to the node. + /// + /// The packet to be sent. + void SendData(INodePacket packet); + #endregion + } +} diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs new file mode 100644 index 00000000000..013327b7659 --- /dev/null +++ b/src/Shared/INodePacket.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for node packets. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd +{ + #region Enums + /// + /// Enumeration of all of the packet types used for communication. + /// + internal enum NodePacketType : byte + { + /// + /// Notifies the Node to set a configuration for a particular build. This is sent before + /// any BuildRequests are made and will not be sent again for a particular build. This instructs + /// the node to prepare to receive build requests. + /// + /// Contains: + /// Build ID + /// Environment variables + /// Logging Services Configuration + /// Node ID + /// Default Global Properties + /// Toolset Definition Locations + /// Startup Directory + /// UI Culture Information + /// App Domain Configuration XML + /// + NodeConfiguration, + + /// + /// A BuildRequestConfiguration object. + /// When sent TO a node, this informs the node of a build configuration. + /// When sent FROM a node, this requests a BuildRequestConfigurationResponse to map the configuration to the + /// appropriate global configuration ID. + /// + /// Contents: + /// Configuration ID + /// Project Filename + /// Project Properties + /// Project Tools Version + /// + BuildRequestConfiguration, + + /// + /// A response to a request to map a build configuration + /// + /// Contents: + /// Node Configuration ID + /// Global Configuration ID + /// + BuildRequestConfigurationResponse, + + /// + /// Information about a project that has been loaded by a node. + /// + /// Contents: + /// Global Configuration ID + /// Initial Targets + /// Default Targets + /// + ProjectLoadInfo, + + /// + /// Packet used to inform the scheduler that a node's active build request is blocked. + /// + /// Contents: + /// Build Request ID + /// Active Targets + /// Blocked Target, if any + /// Child Requests, if any + /// + BuildRequestBlocker, + + /// + /// Packet used to unblocked a blocked request on a node. + /// + /// Contents: + /// Build Request ID + /// Build Results for child requests, if any. + /// + BuildRequestUnblocker, + + /// + /// A BuildRequest object + /// + /// Contents: + /// Build Request ID + /// Configuration ID + /// Project Instance ID + /// Targets + /// + BuildRequest, + + /// + /// A BuildResult object + /// + /// Contents: + /// Build ID + /// Project Instance ID + /// Targets + /// Outputs (per Target) + /// Results (per Target) + /// + BuildResult, + + /// + /// A logging message. + /// + /// Contents: + /// Build Event Type + /// Build Event Args + /// + LogMessage, + + /// + /// Informs the node that the build is complete. + /// + /// Contents: + /// Prepare For Reuse + /// + NodeBuildComplete, + + /// + /// Reported by the node (or node provider) when a node has terminated. This is the final packet that will be received + /// from a node. + /// + /// Contents: + /// Reason + /// + NodeShutdown, + + /// + /// Notifies the task host to set the task-specific configuration for a particular task execution. + /// This is sent in place of NodeConfiguration and gives the task host all the information it needs + /// to set itself up and execute the task that matches this particular configuration. + /// + /// Contains: + /// Node ID (of parent MSBuild node, to make the logging work out) + /// Startup directory + /// Environment variables + /// UI Culture information + /// App Domain Configuration XML + /// Task name + /// Task assembly location + /// Parameter names and values to set to the task prior to execution + /// + TaskHostConfiguration, + + /// + /// Informs the parent node that the task host has finished executing a + /// particular task. Does not need to contain identifying information + /// about the task, because the task host will only ever be connected to + /// one parent node at a a time, and will only ever be executing one task + /// for that node at any one time. + /// + /// Contents: + /// Task result (success / failure) + /// Resultant parameter values (for output gathering) + /// + TaskHostTaskComplete, + + /// + /// Message sent from the node to its paired task host when a task that + /// supports ICancellableTask is cancelled. + /// + /// Contents: + /// (nothing) + /// + TaskHostTaskCancelled + } + #endregion + + /// + /// This interface represents a packet which may be transmitted using an INodeEndpoint. + /// Implementations define the serialized form of the data. + /// + internal interface INodePacket : INodePacketTranslatable + { + #region Properties + /// + /// The type of the packet. Used to reconstitute the packet using the correct factory. + /// + NodePacketType Type + { + get; + } + #endregion + } +} diff --git a/src/Shared/INodePacketFactory.cs b/src/Shared/INodePacketFactory.cs new file mode 100644 index 00000000000..d33928943c0 --- /dev/null +++ b/src/Shared/INodePacketFactory.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the node packet factory. +//-----------------------------------------------------------------------using System; + +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A delegate representing factory methods used to re-create packets deserialized from a stream. + /// + /// The translator containing the packet data. + /// The packet reconstructed from the stream. + internal delegate INodePacket NodePacketFactoryMethod(INodePacketTranslator translator); + + /// + /// This interface represents an object which is used to reconstruct packet objects from + /// binary data. + /// + internal interface INodePacketFactory + { + #region Methods + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler); + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + void UnregisterPacketHandler(NodePacketType packetType); + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator); + + /// + /// Routes the specified packet + /// + /// The node from which the packet was received. + /// The packet to route. + void RoutePacket(int nodeId, INodePacket packet); + + #endregion + } +} diff --git a/src/Shared/INodePacketHandler.cs b/src/Shared/INodePacketHandler.cs new file mode 100644 index 00000000000..7da5d11a8c5 --- /dev/null +++ b/src/Shared/INodePacketHandler.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for node packet handlers. +//-----------------------------------------------------------------------using System; + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Objects which wish to receive packets from the NodePacketRouter must implement this interface. + /// + internal interface INodePacketHandler + { + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + void PacketReceived(int node, INodePacket packet); + } +} diff --git a/src/Shared/INodePacketTranslatable.cs b/src/Shared/INodePacketTranslatable.cs new file mode 100644 index 00000000000..66c7e179a81 --- /dev/null +++ b/src/Shared/INodePacketTranslatable.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for objects which can be serialized to packets for inter-node communication. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// An interface representing an object which may be serialized by the node packet serializer. + /// + internal interface INodePacketTranslatable + { + #region Methods + + /// + /// Reads or writes the packet to the serializer. + /// + void Translate(INodePacketTranslator translator); + + #endregion + } +} diff --git a/src/Shared/INodePacketTranslator.cs b/src/Shared/INodePacketTranslator.cs new file mode 100644 index 00000000000..93fb31a5ef6 --- /dev/null +++ b/src/Shared/INodePacketTranslator.cs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for objects which can Translate data for inter-node communication. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This delegate is used for objects which do not have public paramaterless constructors and must be constructed using + /// another method. When invoked, this delegate should return a new object which has been translated appropriately. + /// + /// The type to be translated. + internal delegate T NodePacketValueFactory(INodePacketTranslator translator); + + /// + /// This delegate is used to create arbitrary dictionary types for serialization. + /// + /// The type of dictionary to be created. + internal delegate T NodePacketDictionaryCreator(int capacity); + + /// + /// The serialization mode. + /// + internal enum TranslationDirection + { + /// + /// Indicates the serializer is operating in write mode. + /// + WriteToStream, + + /// + /// Indicates the serializer is operating in read mode. + /// + ReadFromStream + } + + /// + /// This interface represents an object which aids objects in serializing and + /// deserializing INodePackets. + /// + /// + /// The reason we bother with a custom serialization mechanism at all is two fold: + /// 1. The .Net serialization mechanism is inefficient, even if you implement ISerializable + /// with your own custom mechanism. This is because the serializer uses a bag called + /// SerializationInfo into which you are expected to drop all your data. This adds + /// an unnecessary level of indirection to the serialization routines and prevents direct, + /// efficient access to the byte-stream. + /// 2. You have to implement both a reader and writer part, which introduces the potential for + /// error should the classes be later modified. If the reader and writer methods are not + /// kept in perfect sync, serialization errors will occur. Our custom serializer eliminates + /// that by ensuring a single Translate method on a given object can handle both reads and + /// writes without referencing any field more than once. + /// + internal interface INodePacketTranslator + { + /// + /// Returns the current serialization mode. + /// + TranslationDirection Mode + { + get; + } + + /// + /// Returns the binary reader. + /// + /// + /// This should ONLY be used when absolutely necessary for translation. It is generally unnecessary for the + /// translating object to know the direction of translation. Use one of the Translate methods instead. + /// + BinaryReader Reader + { + get; + } + + /// + /// Returns the binary writer. + /// + /// + /// This should ONLY be used when absolutely necessary for translation. It is generally unnecessary for the + /// translating object to know the direction of translation. Use one of the Translate methods instead. + /// + BinaryWriter Writer + { + get; + } + + /// + /// Translates a boolean. + /// + /// The value to be translated. + void Translate(ref bool value); + + /// + /// Translates a byte. + /// + /// The value to be translated. + void Translate(ref byte value); + + /// + /// Translates a short. + /// + /// The value to be translated. + void Translate(ref short value); + + /// + /// Translates a unsigned short. + /// + /// The value to be translated. + void Translate(ref ushort value); + + /// + /// Translates an integer. + /// + /// The value to be translated. + void Translate(ref int value); + + /// + /// Translates a string. + /// + /// The value to be translated. + void Translate(ref string value); + + /// + /// Translates a string array. + /// + /// The array to be translated. + void Translate(ref string[] array); + + /// + /// Translates a list of strings + /// + /// The list to be translated. + void Translate(ref List list); + + /// + /// Translates a list of T where T implements INodePacketTranslateable + /// + /// The list to be translated. + /// factory to create type T + /// A TaskItemType + void Translate(ref List list, NodePacketValueFactory factory) where T : INodePacketTranslatable; + + /// + /// Translates a DateTime. + /// + /// The value to be translated. + void Translate(ref DateTime value); + + // MSBuildTaskHost is based on CLR 3.5, which does not have the 6-parameter constructor for BuildEventContext, + // which is what current implementations of this method use. However, it also does not ever need to translate + // BuildEventContexts, so it should be perfectly safe to compile this method out of that assembly. I am compiling + // the method out of the interface as well, instead of just making the method empty, so that if we ever do need + // to translate BuildEventContexts from the CLR 3.5 task host, it will become immediately obvious, rather than + // failing or misbehaving silently. +#if !CLR2COMPATIBILITY + + /// + /// Translates a BuildEventContext + /// + /// + /// This method exists only because there is no serialization method built into the BuildEventContext + /// class, and it lives in Framework and we don't want to add a public method to it. + /// + /// The context to be translated. + void Translate(ref BuildEventContext value); + +#endif + + /// + /// Translates an enumeration. + /// + /// The enumeration type. + /// The enumeration instance to be translated. + /// The enumeration value as an integer. + /// This is a bit ugly, but it doesn't seem like a nice method signature is possible because + /// you can't pass the enum type as a reference and constrain the generic parameter to Enum. Nor + /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. + /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This + /// works in all of our current cases, but certainly isn't perfectly generic. + void TranslateEnum(ref T value, int numericValue); + + /// + /// Translates a value using the .Net binary formatter. + /// + /// The reference type. + /// The value to be translated. + /// + /// The primary purpose of this method is to support serialization of Exceptions and + /// custom build logging events, since these do not support our custom serialization + /// methods. + /// + void TranslateDotNet(ref T value); + + /// + /// Translates an object implementing INodePacketTranslatable. + /// + /// The reference type. + /// The value to be translated. + void Translate(ref T value) + where T : INodePacketTranslatable, new(); + + /// + /// Translates an object implementing INodePacketTranslatable which does not expose a + /// public parameterless constructor. + /// + /// The reference type. + /// The value to be translated. + /// The factory method used to instantiate values of type T. + void Translate(ref T value, NodePacketValueFactory factory) + where T : INodePacketTranslatable; + + /// + /// Translates a culture + /// + /// The culture + void TranslateCulture(ref CultureInfo culture); + + /// + /// Translates a byte array + /// + /// The array to be translated. + void Translate(ref byte[] byteArray); + + /// + /// Translates an array of objects implementing INodePacketTranslatable. + /// + /// The reference type. + /// The array to be translated. + void TranslateArray(ref T[] array) + where T : INodePacketTranslatable, new(); + + /// + /// Translates an array of objects implementing INodePacketTranslatable requiring a factory to create. + /// + /// The reference type. + /// The array to be translated. + /// The factory method used to instantiate values of type T. + void TranslateArray(ref T[] array, NodePacketValueFactory factory) + where T : INodePacketTranslatable; + + /// + /// Translates a dictionary of { string, string }. + /// + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + void TranslateDictionary(ref Dictionary dictionary, IEqualityComparer comparer); + + /// + /// Translates a dictionary of { string, T }. + /// + /// The reference type for the values, which implements INodePacketTranslatable. + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + /// The factory used to instantiate values in the dictionary. + void TranslateDictionary(ref Dictionary dictionary, IEqualityComparer comparer, NodePacketValueFactory valueFactory) + where T : class, INodePacketTranslatable; + + /// + /// Translates a dictionary of { string, T } for dictionaries with public parameterless constructors. + /// + /// The reference type for the dictionary. + /// The reference type for values in the dictionary. + /// The dictionary to be translated. + /// The factory used to instantiate values in the dictionary. + void TranslateDictionary(ref D dictionary, NodePacketValueFactory valueFactory) + where D : IDictionary, new() + where T : class, INodePacketTranslatable; + + /// + /// Translates a dictionary of { string, T } for dictionaries with public parameterless constructors. + /// + /// The reference type for the dictionary. + /// The reference type for values in the dictionary. + /// The dictionary to be translated. + /// The factory used to instantiate values in the dictionary. + void TranslateDictionary(ref D dictionary, NodePacketValueFactory valueFactory, NodePacketDictionaryCreator dictionaryCreator) + where D : IDictionary + where T : class, INodePacketTranslatable; + + /// + /// Translates the boolean that says whether this value is null or not + /// + /// The object to test. + /// The type of object to test. + /// True if the object should be written, false otherwise. + bool TranslateNullable(T value); + } +} diff --git a/src/Shared/InprocTrackingNativeMethods.cs b/src/Shared/InprocTrackingNativeMethods.cs new file mode 100644 index 00000000000..a3daf078775 --- /dev/null +++ b/src/Shared/InprocTrackingNativeMethods.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interop methods for the FileTracker.dll interop APIs. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.IO; +using System.Linq; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Shared +{ + /// + /// Methods that are invoked on FileTracker.dll in order to handle inproc tracking + /// + /// + /// We want to P/Invoke to the FileTracker methods, but FileTracker.dll is not guaranteed to be on PATH (since it's + /// in the .NET Framework directory), and there is no DefaultDllImportSearchPath that explicitly points out at the + /// .NET Framework directory. Thus, we are sneaking around P/Invoke by manually acquiring the method pointers and + /// calling them ourselves. The vast majority of this code was lifted from ndp\fx\src\CLRCompression\ZLibNative.cs, + /// which does the same thing for that assembly. + /// + internal static class InprocTrackingNativeMethods + { + #region Delegates for the tracking functions + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int StartTrackingContextDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int StartTrackingContextWithRootDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName, [In, MarshalAs(UnmanagedType.LPWStr)] string rootMarker); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int EndTrackingContextDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int StopTrackingAndCleanupDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int SuspendTrackingDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int ResumeTrackingDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int WriteAllTLogsDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int WriteContextTLogsDelegate([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [SuppressUnmanagedCodeSecurity] + private delegate int SetThreadCountDelegate(int threadCount); + + #endregion // Delegates for the tracking functions + + #region Public API + + internal static void StartTrackingContext([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName) + { + int hresult = FileTrackerDllStub.startTrackingContextDelegate(intermediateDirectory, taskName); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void StartTrackingContextWithRoot([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string taskName, [In, MarshalAs(UnmanagedType.LPWStr)] string rootMarker) + { + int hresult = FileTrackerDllStub.startTrackingContextWithRootDelegate(intermediateDirectory, taskName, rootMarker); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void EndTrackingContext() + { + int hresult = FileTrackerDllStub.endTrackingContextDelegate(); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void StopTrackingAndCleanup() + { + int hresult = FileTrackerDllStub.stopTrackingAndCleanupDelegate(); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void SuspendTracking() + { + int hresult = FileTrackerDllStub.suspendTrackingDelegate(); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void ResumeTracking() + { + int hresult = FileTrackerDllStub.resumeTrackingDelegate(); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void WriteAllTLogs([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName) + { + int hresult = FileTrackerDllStub.writeAllTLogsDelegate(intermediateDirectory, tlogRootName); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void WriteContextTLogs([In, MarshalAs(UnmanagedType.LPWStr)] string intermediateDirectory, [In, MarshalAs(UnmanagedType.LPWStr)] string tlogRootName) + { + int hresult = FileTrackerDllStub.writeContextTLogsDelegate(intermediateDirectory, tlogRootName); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + internal static void SetThreadCount(int threadCount) + { + int hresult = FileTrackerDllStub.setThreadCountDelegate(threadCount); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + } + + #endregion // Public API + + private static class FileTrackerDllStub + { + private const string fileTrackerDllName = "FileTracker.dll"; + + // Handle for FileTracker.dll itself + [SecurityCritical] + private static SafeHandle s_fileTrackerDllHandle; + + #region Function pointers to native functions + + internal static StartTrackingContextDelegate startTrackingContextDelegate; + + internal static StartTrackingContextWithRootDelegate startTrackingContextWithRootDelegate; + + internal static EndTrackingContextDelegate endTrackingContextDelegate; + + internal static StopTrackingAndCleanupDelegate stopTrackingAndCleanupDelegate; + + internal static SuspendTrackingDelegate suspendTrackingDelegate; + + internal static ResumeTrackingDelegate resumeTrackingDelegate; + + internal static WriteAllTLogsDelegate writeAllTLogsDelegate; + + internal static WriteContextTLogsDelegate writeContextTLogsDelegate; + + internal static SetThreadCountDelegate setThreadCountDelegate; + + #endregion // Function pointers to native functions + + #region Declarations of Windows API needed to load the native library + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, BestFitMapping = false)] + [ResourceExposure(ResourceScope.Process)] + [SecurityCritical] + private static extern IntPtr GetProcAddress(SafeHandle moduleHandle, String procName); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [ResourceExposure(ResourceScope.Machine)] + [SecurityCritical] + private static extern SafeLibraryHandle LoadLibrary(String libPath); + + #endregion // Declarations of Windows API needed to load the native library + + #region Initialization code + + /// + /// Loads FileTracker.dll into a handle that we can use subsequently to grab the exported methods we're interested in. + /// + private static void LoadFileTrackerDll() + { + // Get the FileTracker in the framework directory that matches the currently running process + string frameworkDir = RuntimeEnvironment.GetRuntimeDirectory(); + string buildToolsPath = FrameworkLocationHelper.GeneratePathToBuildToolsForToolsVersion(MSBuildConstants.CurrentToolsVersion, DotNetFrameworkArchitecture.Current); + string fileTrackerPath = Path.Combine(buildToolsPath, fileTrackerDllName); + + if (String.IsNullOrEmpty(fileTrackerPath) || !File.Exists(fileTrackerPath)) + { + fileTrackerPath = Path.Combine(frameworkDir, fileTrackerDllName); + } + + if (!File.Exists(fileTrackerPath)) + { + throw new DllNotFoundException(fileTrackerDllName); + } + + SafeLibraryHandle handle = LoadLibrary(fileTrackerPath); + + if (handle.IsInvalid) + { + Int32 hresult = Marshal.GetHRForLastWin32Error(); + Marshal.ThrowExceptionForHR(hresult, new IntPtr(-1)); + + // If Marshal.ThrowExceptionForHR did not throw, we still need to make sure to throw: + throw new InvalidOperationException(); + } + + s_fileTrackerDllHandle = handle; + } + + /// + /// Generic code to grab the function pointer for a function exported by FileTracker.dll, given + /// that function's name, and transform that function pointer into a callable delegate. + /// + [SecurityCritical] + private static DT CreateDelegate
(String entryPointName) + { + IntPtr entryPoint = GetProcAddress(s_fileTrackerDllHandle, entryPointName); + + if (IntPtr.Zero == entryPoint) + throw new EntryPointNotFoundException(fileTrackerDllName + "!" + entryPointName); + + return (DT)(Object)Marshal.GetDelegateForFunctionPointer(entryPoint, typeof(DT)); + } + + /// + /// Actually generate all of the delegates that will be called by our public (or rather, internal) surface area methods. + /// + private static void InitDelegates() + { + ErrorUtilities.VerifyThrow(s_fileTrackerDllHandle != null, "fileTrackerDllHandle should not be null"); + ErrorUtilities.VerifyThrow(!s_fileTrackerDllHandle.IsInvalid, "Handle for FileTracker.dll should not be invalid"); + + startTrackingContextDelegate = CreateDelegate("StartTrackingContext"); + startTrackingContextWithRootDelegate = CreateDelegate("StartTrackingContextWithRoot"); + endTrackingContextDelegate = CreateDelegate("EndTrackingContext"); + stopTrackingAndCleanupDelegate = CreateDelegate("StopTrackingAndCleanup"); + suspendTrackingDelegate = CreateDelegate("SuspendTracking"); + resumeTrackingDelegate = CreateDelegate("ResumeTracking"); + writeAllTLogsDelegate = CreateDelegate("WriteAllTLogs"); + writeContextTLogsDelegate = CreateDelegate("WriteContextTLogs"); + setThreadCountDelegate = CreateDelegate("SetThreadCount"); + } + + /// + /// Static constructor -- generates the delegates for all of the export methods from + /// FileTracker.dll that we care about. + /// + [SecuritySafeCritical] + static FileTrackerDllStub() + { + LoadFileTrackerDll(); + InitDelegates(); + } + + #endregion // Initialization code + + // Specialized handle to make sure we free FileTracker.dll + [SecurityCritical] + private class SafeLibraryHandle : SafeHandle + { + internal SafeLibraryHandle() + : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid + { + [SecurityCritical] + get + { return IntPtr.Zero == handle; } + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + [SecurityCritical] + protected override bool ReleaseHandle() + { + // FileTracker expects to continue to exist even through ExitProcess -- if we forcibly unload it now, + // bad things can happen when the CLR attempts to call the (still detoured?) ExitProcess. + return true; + } + } // private class SafeLibraryHandle + } + } +} \ No newline at end of file diff --git a/src/Shared/InternalErrorException.cs b/src/Shared/InternalErrorException.cs new file mode 100644 index 00000000000..8be4ead72e0 --- /dev/null +++ b/src/Shared/InternalErrorException.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Exception to be thrown whenever an assumption we have made +// in the code turns out to be false. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Security.Permissions; // for SecurityPermissionAttribute +using System.Runtime.Serialization; + +namespace Microsoft.Build.Shared +{ + /// + /// This exception is to be thrown whenever an assumption we have made in the code turns out to be false. Thus, if this + /// exception ever gets thrown, it is because of a bug in our own code, not because of something the user or project author + /// did wrong. + /// + /// !~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~ + /// WARNING: When this file is shared into multiple assemblies each assembly will view this as a different type. + /// Don't throw this exception from one assembly and catch it in another. + /// !~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~ + /// + /// + [Serializable] + internal sealed class InternalErrorException : Exception + { + /// + /// Default constructor. + /// SHOULD ONLY BE CALLED BY DESERIALIZER. + /// SUPPLY A MESSAGE INSTEAD. + /// + internal InternalErrorException() : base() + { + // do nothing + } + + /// + /// Creates an instance of this exception using the given message. + /// + internal InternalErrorException + ( + String message + ) : + base("MSB0001: Internal MSBuild Error: " + message) + { + ConsiderDebuggerLaunch(message, null); + } + + /// + /// Creates an instance of this exception using the given message and inner exception. + /// Adds the inner exception's details to the exception message because most bug reporters don't bother + /// to provide the inner exception details which is typically what we care about. + /// + internal InternalErrorException + ( + String message, + Exception innerException + ) : + base("MSB0001: Internal MSBuild Error: " + message + (innerException == null ? String.Empty : ("\n=============\n" + innerException.ToString() + "\n\n")), innerException) + { + ConsiderDebuggerLaunch(message, innerException); + } + + #region Serialization (update when adding new class members) + + /// + /// Private constructor used for (de)serialization. The constructor is private as this class is sealed + /// If we ever add new members to this class, we'll need to update this. + /// + private InternalErrorException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + // Do nothing: no fields + } + + // Base implementation of GetObjectData() is sufficient; we have no fields + #endregion + + #region ConsiderDebuggerLaunch + /// + /// A fatal internal error due to a bug has occurred. Give the dev a chance to debug it, if possible. + /// + /// Will in all cases launch the debugger, if the environment variable "MSBUILDLAUNCHDEBUGGER" is set. + /// + /// In DEBUG build, will always launch the debugger, unless we are in razzle (_NTROOT is set) or in NUnit, + /// or MSBUILDDONOTLAUNCHDEBUGGER is set (that could be useful in suite runs). + /// We don't launch in retail or LKG so builds don't jam; they get a callstack, and continue or send a mail, etc. + /// We don't launch in NUnit as tests often intentionally cause InternalErrorExceptions. + /// + /// Because we only call this method from this class, just before throwing an InternalErrorException, there is + /// no danger that this suppression will cause a bug to only manifest itself outside NUnit + /// (which would be most unfortunate!). Do not make this non-private. + /// + /// Unfortunately NUnit can't handle unhandled exceptions like InternalErrorException on anything other than + /// the main test thread. However, there's still a callstack displayed before it quits. + /// + /// If it is going to launch the debugger, it first does a Debug.Fail to give information about what needs to + /// be debugged -- the exception hasn't been thrown yet. This automatically displays the current callstack. + /// + private static void ConsiderDebuggerLaunch(string message, Exception innerException) + { + string innerMessage = (innerException == null) ? String.Empty : innerException.ToString(); + + if (Environment.GetEnvironmentVariable("MSBUILDLAUNCHDEBUGGER") != null) + { + Debug.Fail(message, innerMessage); + Debugger.Launch(); + return; + } + +#if DEBUG + if (Environment.GetEnvironmentVariable("MSBUILDDONOTLAUNCHDEBUGGER") == null) + { + string processName = Process.GetCurrentProcess().ProcessName.ToUpperInvariant(); + + if (!FileUtilities.RunningTests) + { + if (Environment.GetEnvironmentVariable("_NTROOT") == null) + { + Debug.Fail(message, innerMessage); + Debugger.Launch(); + return; + } + } + } +#endif + } + #endregion + } +} diff --git a/src/Shared/InterningBinaryReader.cs b/src/Shared/InterningBinaryReader.cs new file mode 100644 index 00000000000..a5776c1b3fc --- /dev/null +++ b/src/Shared/InterningBinaryReader.cs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for node endpoints. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Diagnostics; +using System.Globalization; + +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; + +namespace Microsoft.Build +{ + /// + /// Replacement for BinaryReader which attempts to intern the strings read by ReadString. + /// + internal class InterningBinaryReader : BinaryReader + { + /// + /// The maximum size, in bytes, to read at once. + /// +#if _DEBUG + private const int MaxCharsBuffer = 10; +#else + private const int MaxCharsBuffer = 20000; +#endif + + /// + /// Shared buffer saves allocating these arrays many times. + /// + private Buffer _buffer; + + /// + /// The decoder used to translate from UTF8 (or whatever). + /// + private Decoder _decoder; + + /// + /// Comment about constructing. + /// + private InterningBinaryReader(Stream input, Buffer buffer) + : base(input, buffer.Encoding) + { + if (input == null) + { + throw new InvalidOperationException(); + } + + _buffer = buffer; + _decoder = buffer.Encoding.GetDecoder(); + } + + /// + /// Read a string while checking the string precursor for intern opportunities. + /// Taken from ndp\clr\src\bcl\system\io\binaryreader.cs-ReadString() + /// + override public String ReadString() + { + try + { + MemoryStream memoryStream = this.BaseStream as MemoryStream; + + int currPos = 0; + int n; + int stringLength; + int readLength; + int charsRead; + + // Length of the string in bytes, not chars + stringLength = Read7BitEncodedInt(); + if (stringLength < 0) + { + throw new IOException(); + } + + if (stringLength == 0) + { + return String.Empty; + } + + char[] charBuffer = _buffer.CharBuffer; + + StringBuilder sb = null; + do + { + readLength = ((stringLength - currPos) > MaxCharsBuffer) ? MaxCharsBuffer : (stringLength - currPos); + + byte[] rawBuffer; + int rawPosition; + + if (memoryStream != null) + { + // Optimization: we can avoid reading into a byte buffer + // and instead read directly from the memorystream's backing buffer + rawBuffer = memoryStream.GetBuffer(); + rawPosition = (int)memoryStream.Position; + int length = (int)memoryStream.Length; + n = (rawPosition + readLength) < length ? readLength : length - rawPosition; + + // Attempt to track down an intermittent failure -- n should not ever be negative, but + // we're occasionally seeing it when we do the decoder.GetChars below -- by providing + // a bit more information when we do hit the error, in the place where (by code inspection) + // the actual error seems most likely to be occurring. + if (n < 0) + { + ErrorUtilities.ThrowInternalError("From calculating based on the memorystream, about to read n = {0}. length = {1}, rawPosition = {2}, readLength = {3}, stringLength = {4}, currPos = {5}.", n, length, rawPosition, readLength, stringLength, currPos); + } + } + else + { + rawBuffer = _buffer.ByteBuffer; + rawPosition = 0; + n = BaseStream.Read(rawBuffer, 0, readLength); + + // See above explanation -- the OutOfRange exception may also be coming from our setting of n here ... + if (n < 0) + { + ErrorUtilities.ThrowInternalError("From getting the length out of BaseStream.Read directly, about to read n = {0}. readLength = {1}, stringLength = {2}, currPos = {3}", n, readLength, stringLength, currPos); + } + } + + if (n == 0) + { + throw new EndOfStreamException(); + } + + charsRead = _decoder.GetChars(rawBuffer, rawPosition, n, charBuffer, 0); + + if (memoryStream != null) + { + memoryStream.Seek(readLength, SeekOrigin.Current); + } + + if (currPos == 0 && n == stringLength) + { + return OpportunisticIntern.CharArrayToString(charBuffer, charsRead); + } + + if (sb == null) + { + sb = new StringBuilder(stringLength); // Actual string length in chars may be smaller. + } + + sb.Append(charBuffer, 0, charsRead); + currPos += n; + } + while (currPos < stringLength); + + return OpportunisticIntern.StringBuilderToString(sb); + } + catch (Exception e) + { + Debug.Assert(false, e.ToString()); + throw; + } + } + + /// + /// A shared buffer to avoid extra allocations in InterningBinaryReader. + /// + internal static SharedReadBuffer CreateSharedBuffer() + { + return new Buffer(); + } + + /// + /// Create a BinaryReader. It will either be an interning reader or standard binary reader + /// depending on whether the interning reader is possible given the buffer and stream. + /// + internal static BinaryReader Create(Stream stream, SharedReadBuffer sharedBuffer) + { + Buffer buffer = (Buffer)sharedBuffer; + + if (buffer == null) + { + buffer = new Buffer(); + } + + return new InterningBinaryReader(stream, buffer); + } + + /// + /// Holds thepreallocated buffer. + /// + private class Buffer : SharedReadBuffer + { + /// + /// Yes, we are constructing. + /// + internal Buffer() + { + this.Encoding = new UTF8Encoding(); + this.CharBuffer = new char[MaxCharsBuffer]; + this.ByteBuffer = new byte[Encoding.GetMaxByteCount(MaxCharsBuffer)]; + } + + /// + /// The char buffer. + /// + internal char[] CharBuffer + { + get; + private set; + } + + /// + /// The byte buffer. + /// + internal byte[] ByteBuffer + { + get; + private set; + } + + /// + /// The encoding. + /// + internal UTF8Encoding Encoding + { + get; + private set; + } + } + } + + /// + /// Opaque holder of shared buffer. + /// + abstract internal class SharedReadBuffer + { + } +} diff --git a/src/Shared/LanguageParser/CSharptokenCharReader.cs b/src/Shared/LanguageParser/CSharptokenCharReader.cs new file mode 100644 index 00000000000..3cadf619999 --- /dev/null +++ b/src/Shared/LanguageParser/CSharptokenCharReader.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: CSharpTokenCharReader + * + * Reads over the contents of a C# source file (in the form of a string). + * Provides utility functions for dealing with C#-specific tokens. + * + */ + sealed internal class CSharpTokenCharReader : TokenCharReader + { + /* + * Method: CSharpTokenCharReader + * + * Construct + */ + internal CSharpTokenCharReader(Stream binaryStream, bool forceANSI) + : base(binaryStream, forceANSI) + { + } + + /* + * Method: SinkLongIntegerSuffix + * + * Skip C# integer literal long suffix: L, U, l, u, ul, etc. + */ + internal bool SinkLongIntegerSuffix() + { + // Skip the long interger suffix if there is one. + if (CurrentCharacter == 'U' || CurrentCharacter == 'u') + { + Skip(); + if (CurrentCharacter == 'L' || CurrentCharacter == 'l') + { + Skip(); + } + } + else if (CurrentCharacter == 'L' || CurrentCharacter == 'l') + { + Skip(); + if (CurrentCharacter == 'U' || CurrentCharacter == 'u') + { + Skip(); + } + } + + return true; // An integer suffix can be zero characters, so there's always a match. + } + + /* + * Method: SinkOperatorOrPunctuator + * + * Determine whether this is a C# operator or punctuator + */ + internal bool SinkOperatorOrPunctuator() + { + const string operatorsAndPunctuators = "{}[]().,:;+-*/%&|^!~=<>?"; + if (operatorsAndPunctuators.IndexOf(CurrentCharacter) == -1) + { + return false; + } + Skip(); + return true; + } + + /* + * Method: SinkStringEscape + * + * Determine whether this is a valid escape character for strings? + */ + internal bool SinkStringEscape() + { + switch (CurrentCharacter) + { + case '\'': + case '\"': + case '0': + case 'a': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + case 'u': + case 'U': + case 'x': + case 'v': + case '\x005c' /* backslash */: + Skip(); + return true; + } + return false; + } + + /* + * Method: MatchRegularStringLiteral + * + * Determine whether this is a regular C# string literal character + */ + internal bool MatchRegularStringLiteral() + { + if (CurrentCharacter == '\"' || CurrentCharacter == '\\' || TokenChar.IsNewLine(CurrentCharacter)) + { + return false; + } + + return true; + } + + /* + * Method: SinkMultipleWhiteSpace + * + * Sink some C# whitespace + */ + internal bool SinkMultipleWhiteSpace() + { + int count = 0; + while (!EndOfLines && Char.IsWhiteSpace(CurrentCharacter)) + { + Skip(); + ++count; + } + + return (count > 0); + } + } +} + diff --git a/src/Shared/LanguageParser/CSharptokenEnumerator.cs b/src/Shared/LanguageParser/CSharptokenEnumerator.cs new file mode 100644 index 00000000000..eaedd296cf8 --- /dev/null +++ b/src/Shared/LanguageParser/CSharptokenEnumerator.cs @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Diagnostics; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: CSharpTokenEnumerator + * + * Given C# sources, enumerate over all tokens. + * + */ + sealed internal class CSharpTokenEnumerator : TokenEnumerator + { + // Reader over the sources. + private CSharpTokenCharReader _reader = null; + + /* + * Method: TokenEnumerator + * + * Construct + */ + internal CSharpTokenEnumerator(Stream binaryStream, bool forceANSI) + { + _reader = new CSharpTokenCharReader(binaryStream, forceANSI); + } + + /* + * Method: FindNextToken + * + * Find the next token. Return 'true' if one was found. False, otherwise. + */ + override internal bool FindNextToken() + { + int startPosition = _reader.Position; + + // Dealing with whitespace? + if (_reader.SinkMultipleWhiteSpace()) + { + current = new WhitespaceToken(); + return true; + } + // Check for one-line comment + else if (_reader.Sink("//")) + { + // Looks like a one-line comment. Follow it to the End-of-line + _reader.SinkToEndOfLine(); + + current = new CommentToken(); + return true; + } + // Check for multi-line comment + else if (_reader.Sink("/*")) + { + _reader.SinkUntil("*/"); + + // Was the ending */ found? + if (_reader.EndOfLines) + { + // No. There was a /* without a */. Return this a syntax error token. + current = new CSharpTokenizer.EndOfFileInsideCommentToken(); + return true; + } + + current = new CommentToken(); + return true; + } + // Handle chars + else if (_reader.Sink("\'")) + { + while (_reader.CurrentCharacter != '\'') + { + if (_reader.Sink("\\")) + { + /* reader.Skip the escape sequence. + This isn't exactly right. We should detect: + + simple-escape-sequence: one of + \' \" \\ \0 \a \b \f \n \r \t \v + + hexadecimal-escape-sequence: + \x hex-digit hex-digit[opt] hex-digit[opt] hex-digit[opt] + */ + } + + _reader.SinkCharacter(); + } + + if (_reader.SinkCharacter() != '\'') + { + Debug.Assert(false, "Code defect in tokenizer: Should have yielded a closing tick."); + } + current = new CSharpTokenizer.CharLiteralToken(); + return true; + } + // Check for verbatim string + else if (_reader.Sink("@\"")) + { + do + { + // Inside a verbatim string "" is treated as a special character + while (_reader.Sink("\"\"")) + { + } + } + while (!_reader.EndOfLines && _reader.SinkCharacter() != '\"'); + + // Can't end a file inside a string + if (_reader.EndOfLines) + { + current = new EndOfFileInsideStringToken(); + return true; + } + + // reader.Skip the ending quote. + current = new StringLiteralToken(); + current.InnerText = _reader.GetCurrentMatchedString(startPosition).Substring(1); + return true; + } + // Check for a quoted string. + else if (_reader.Sink("\"")) + { + while (_reader.CurrentCharacter == '\\' || _reader.MatchRegularStringLiteral()) + { + // See if we have an escape sequence. + if (_reader.SinkCharacter() == '\\') + { + // This is probably an escape character. + if (_reader.SinkStringEscape()) + { + // This isn't nearly right. We just do barely enough to make a string + // with an embedded escape sequence return _some_ string whose start and + // end match the real bounds of the string. + } + else + { + // This is a compiler error. + _reader.SinkCharacter(); + current = new CSharpTokenizer.UnrecognizedStringEscapeToken(); + return true; + } + } + } + + // Is it a newline? + if (TokenChar.IsNewLine(_reader.CurrentCharacter)) + { + current = new CSharpTokenizer.NewlineInsideStringToken(); + return true; + } + + // Create the token. + if (_reader.SinkCharacter() != '\"') + { + Debug.Assert(false, "Defect in tokenizer: Should have yielded a terminating quote."); + } + current = new StringLiteralToken(); + return true; + } + // Identifier or keyword? + else if + ( + // From 2.4.2 Identifiers: A '@' can be used to prefix an identifier so that a keword can be used as an identifier. + _reader.CurrentCharacter == '@' || + _reader.MatchNextIdentifierStart() + ) + { + if (_reader.CurrentCharacter == '@') + { + _reader.SinkCharacter(); + } + + // Now, the next character must be an identifier start. + if (!_reader.SinkIdentifierStart()) + { + current = new ExpectedIdentifierToken(); + return true; + } + + // Sink the rest of the identifier. + while (_reader.SinkIdentifierPart()) + { + } + string identifierOrKeyword = _reader.GetCurrentMatchedString(startPosition); + + switch (identifierOrKeyword) + { + default: + + if (Array.IndexOf(s_keywordList, identifierOrKeyword) >= 0) + { + current = new KeywordToken(); + return true; + } + + // If the identifier starts with '@' then we need to strip it off. + // The '@' is for escaping so that we can have an identifier called + // the same thing as a reserved keyword (i.e. class, if, foreach, etc) + string identifier = _reader.GetCurrentMatchedString(startPosition); + if (identifier.StartsWith("@", StringComparison.Ordinal)) + { + identifier = identifier.Substring(1); + } + + // Create the token. + current = new IdentifierToken(); + current.InnerText = identifier; + return true; + case "false": + case "true": + current = new BooleanLiteralToken(); + return true; + case "null": + current = new CSharpTokenizer.NullLiteralToken(); + return true; + } + } + // Open scope + else if (_reader.Sink("{")) + { + current = new CSharpTokenizer.OpenScopeToken(); + return true; + } + // Close scope + else if (_reader.Sink("}")) + { + current = new CSharpTokenizer.CloseScopeToken(); + return true; + } + // Hexidecimal integer literal + else if (_reader.SinkIgnoreCase("0x")) + { + // Sink the hex digits. + if (!_reader.SinkMultipleHexDigits()) + { + current = new ExpectedValidHexDigitToken(); + return true; + } + + // Skip the L, U, l, u, ul, etc. + _reader.SinkLongIntegerSuffix(); + + current = new HexIntegerLiteralToken(); + return true; + } + // Decimal integer literal + else if (_reader.SinkMultipleDecimalDigits()) + { + // reader.Skip the L, U, l, u, ul, etc. + _reader.SinkLongIntegerSuffix(); + + current = new DecimalIntegerLiteralToken(); + return true; + } + // Check for single-digit operators and punctuators + else if (_reader.SinkOperatorOrPunctuator()) + { + current = new OperatorOrPunctuatorToken(); + return true; + } + // Preprocessor line + else if (_reader.CurrentCharacter == '#') + { + if (_reader.Sink("#if")) + { + current = new OpenConditionalDirectiveToken(); + } + else if (_reader.Sink("#endif")) + { + current = new CloseConditionalDirectiveToken(); + } + else + { + current = new PreprocessorToken(); + } + + _reader.SinkToEndOfLine(); + + return true; + } + + // We didn't recognize the token, so this is a syntax error. + _reader.SinkCharacter(); + current = new UnrecognizedToken(); + return true; + } + + private static readonly string[] s_keywordList = + { "abstract", "as", "base", "bool", "break", + "byte", "case", "catch", "char", "checked", + "class", "const", "continue", "decimal", "default", + "delegate", "do", "double", "else", "enum", + "event", "explicit", "extern", "finally", "fixed", + "float", "for", "foreach", "goto", "if", + "implicit", "in", "int", "interface", "internal", + "is", "lock", "long", "namespace", "new", + "object", "operator", "out", "override", "params", + "private", "protected", "public", "readonly", + "ref", "return", "sbyte", "sealed", "short", + "sizeof", "stackalloc", "static", "string", + "struct", "switch", "this", "throw", "try", + "typeof", "uint", "ulong", "unchecked", "unsafe", + "ushort", "using", "virtual", "void", "volatile", + "while" }; + + + /* + * Method: Reader + * + * Return the token char reader. + */ + override internal TokenCharReader Reader + { + get + { + return _reader; + } + } + } +} diff --git a/src/Shared/LanguageParser/CSharptokenizer.cs b/src/Shared/LanguageParser/CSharptokenizer.cs new file mode 100644 index 00000000000..b784e842f12 --- /dev/null +++ b/src/Shared/LanguageParser/CSharptokenizer.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Diagnostics; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: CSharpTokenizer + * + * Given C# sources, return an enumerator that will provide tokens one at a time. + * + */ + sealed internal class CSharpTokenizer : IEnumerable + { + /* + These are the tokens that are specific to the C# tokenizer. + Tokens that should be shared with other tokenizers should go + into Token.cs. + */ + internal class CharLiteralToken : Token { } // i.e. A char value. + internal class NullLiteralToken : Token { } // i.e. A literal Null. + + internal class UnrecognizedStringEscapeToken : SyntaxErrorToken { } // An unrecognized string escape character was used. + internal class EndOfFileInsideStringToken : SyntaxErrorToken { } // The file ended inside a string. + internal class NewlineInsideStringToken : SyntaxErrorToken { } // The string has a newline embedded. + internal class EndOfFileInsideCommentToken : SyntaxErrorToken { } // The file ended inside a multi-line comment. + + internal class OpenScopeToken : OperatorOrPunctuatorToken { } // i.e. "{" + internal class CloseScopeToken : OperatorOrPunctuatorToken { } // i.e. "}" + + // The source lines + private Stream _binaryStream = null; + + // Whether to force ANSI or not. + private bool _forceANSI = false; + + /// + /// Construct. + /// + /// + /// + internal CSharpTokenizer(Stream binaryStream, bool forceANSI) + { + _binaryStream = binaryStream; + _forceANSI = forceANSI; + } + + /* + * Method: GetEnumerator + * + * Return a new token enumerator. + */ + public IEnumerator GetEnumerator() + { + return new CSharpTokenEnumerator(_binaryStream, _forceANSI); + } + } +} diff --git a/src/Shared/LanguageParser/StreamMappedString.cs b/src/Shared/LanguageParser/StreamMappedString.cs new file mode 100644 index 00000000000..18964d945d3 --- /dev/null +++ b/src/Shared/LanguageParser/StreamMappedString.cs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Diagnostics; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /// + /// A class with string-like semantics mapped over a Stream. + /// + sealed internal class StreamMappedString + { + /// + /// The raw binary stream that's being read. + /// + private Stream _binaryStream; + + /// + /// The reader on top of binaryStream. This is what interprets the encoding. + /// + private StreamReader _reader; + + /// + /// When false, try to guess the encoding of binaryStream. When true, force the + /// encoding to ANSI. + /// + private bool _forceANSI; + + /// + /// The page number that 'currentPage' is pointing to. + /// + private int _currentPageNumber = -1; + + /// + /// The final page number of the whole stream. + /// + private int _finalPageNumber = Int32.MaxValue; + + /// + /// The number of characters read into currentPage. + /// + private int _charactersRead = 0; + + /// + /// The page before currentPage. + /// + private char[] _priorPage = null; + + /// + /// The most recently read page. + /// + private char[] _currentPage = null; + + /// + /// Count of the total number of pages allocated. + /// + private int _pagesAllocated = 0; + + /// + /// Size of pages to use for reading from source file. + /// + private int _pageSize = 0; + + /// + /// Construct. + /// + /// + public StreamMappedString(Stream binaryStream, bool forceANSI) + : this(binaryStream, forceANSI, /* pageSize */ DefaultPageSize) + { + } + + /// + /// Construct. + /// + /// + internal StreamMappedString(Stream binaryStream, bool forceANSI, int pageSize) + { + _binaryStream = binaryStream; + _forceANSI = forceANSI; + _pageSize = pageSize; + RestartReader(); + } + + /// + /// Restart the stream reader at the beginning. + /// + /// + private void RestartReader() + { + _currentPageNumber = -1; + _charactersRead = 0; + _priorPage = null; + _currentPage = null; + + // Reset the stream if we're not at the beginning + if (_binaryStream.Position != 0) + { + _binaryStream.Seek(0, SeekOrigin.Begin); + } + + if (_forceANSI) + { + _reader = new StreamReader // HIGHCHAR: Falling back to ANSI for VB source files. + ( + _binaryStream, + Encoding.Default, // Default means ANSI. + false // If the reader had been able to guess the encoding it would have done so already. + ); + } + else + { + Encoding utf8Encoding = new UTF8Encoding(false, true /* throw on illegal bytes */); + + _reader = new StreamReader // HIGHCHAR: VB and C# source files are assumed to be UTF if there are no byte-order marks. + ( + _binaryStream, + utf8Encoding, + true // Ask the reader to try to guess the file's encoding. + ); + } + } + + /// + /// Get the total number of pages allocated. + /// + public int PagesAllocated + { + get { return _pagesAllocated; } + } + + /// + /// The pagesize in characters that will be used if not specified. + /// + public static int DefaultPageSize + { + get { return 256; } + } + + /// + /// Return a particular character within the file. + /// + /// + /// + public char GetAt(int offset) + { + // Find the page number that contains offset. + char[] page = GetPage(offset); + + // If null now, then the requested character is out of range. + if (page == null) + { + throw new ArgumentOutOfRangeException("offset"); + } + + // Get the relative offset within the buffer. + int relativeOffset = AbsoluteOffsetToPageOffset(offset); + + // Return the character. + return page[relativeOffset]; + } + + /// + /// Get the page that contains offset. Otherwise, null. + /// + /// + /// + private char[] GetPage(int offset) + { + int page = PageFromAbsoluteOffset(offset); + + // Is it earlier than the last page? + if (page < _currentPageNumber - 1) + { + // Restart the stream. Perf hit. + RestartReader(); + } + + // Read pages until the page is available. + while (page > _currentPageNumber) + { + int originalPageNumber = _currentPageNumber; + + if (!ReadNextPage()) + { + break; + } + + ErrorUtilities.VerifyThrow(originalPageNumber != _currentPageNumber, "Expected a new page."); + } + + // Is it the current page? + if (page == _currentPageNumber) + { + // If enough bytes were read to satisfy offset then return the buffer. + if (_charactersRead > AbsoluteOffsetToPageOffset(offset)) + { + return _currentPage; + } + + // Otherwise, null. + return null; + } + + // Is it the prior page now? + if (page == _currentPageNumber - 1) + { + return _priorPage; + } + + // Its out of range. + return null; + } + + /// + /// Read the next page. + /// + /// + private bool ReadNextPage() + { + // Is this already known to be the last page? + if (_currentPageNumber == _finalPageNumber) + { + // 0x0d should already be appended. + return false; + } + + // If so, read it in to the lastBuffer... + ReadBlockStripEOF(); + + // ...and then swap lastBuffer for buffer. + SwapPages(); + ++_currentPageNumber; + + // If the number of bytesRead is less than the number requested + // then this is the last page. + if (_charactersRead < _pageSize) + { + // Mark this as the last page. + _finalPageNumber = _currentPageNumber; + + // Add a 0xd if the last character is not a newline. + if (!IsZeroLengthStream() && !TokenChar.IsNewLine(LastCharacterInStream())) + { + AppendCharacterToStream('\xd'); + } + } + + return _charactersRead > 0; + } + + /// + /// Read characters from the file, and strip out any 1A characters. + /// + private void ReadBlockStripEOF() + { + if (_priorPage == null) + { + ++_pagesAllocated; + _priorPage = new char[_pageSize]; + } + + _charactersRead = _reader.ReadBlock(_priorPage, 0, _pageSize); + + for (int i = 0; i < _charactersRead; ++i) + { + if (_priorPage[i] == '\x1a') + { + Array.Copy(_priorPage, i + 1, _priorPage, i, _charactersRead - i - 1); + _charactersRead += _reader.ReadBlock(_priorPage, _charactersRead - 1, 1); + + --i; + --_charactersRead; + } + } + } + + /// + /// Add one character to the end of the stream. + /// + /// + private void AppendCharacterToStream(char c) + { + ErrorUtilities.VerifyThrow(_charactersRead != _pageSize, "Attempt to append to non-last page."); + + _currentPage[_charactersRead] = c; + ++_charactersRead; + } + + /// + /// Retrieve the last character in the stream. + /// + /// + private char LastCharacterInStream() + { + char c; + if (_charactersRead == 0) + { + ErrorUtilities.VerifyThrow(_priorPage != null, "There is no last character in the stream."); + c = _priorPage[_pageSize - 1]; + } + else + { + c = _currentPage[_charactersRead - 1]; + } + return c; + } + + /// + /// Swap the current page for the last page. + /// + private void SwapPages() + { + char[] swap = _currentPage; + _currentPage = _priorPage; + _priorPage = swap; + } + + /// + /// True if this stream is zero length. + /// + /// + private bool IsZeroLengthStream() + { + return _charactersRead == 0 && _currentPageNumber == 0; + } + + /// + /// COnvert from absolute offset to relative offset within a particular page. + /// + /// + /// + /// + private int AbsoluteOffsetToPageOffset(int offset) + { + return offset - (PageFromAbsoluteOffset(offset) * _pageSize); + } + + /// + /// Convert from offset to page number. + /// + /// + /// + private int PageFromAbsoluteOffset(int offset) + { + return offset / _pageSize; + } + + /// + /// Returns true of the given position is passed the end of the file. + /// + /// + /// + public bool IsPastEnd(int offset) + { + return GetPage(offset) == null; + } + + /// + /// Extract a substring. + /// + /// + /// + /// + public string Substring(int startPosition, int length) + { + StringBuilder result = new StringBuilder(length); + + int charactersExtracted = 0; + for (int i = 0; i < length; i += charactersExtracted) + { + char[] page = GetPage(startPosition + i); + + // If we weren't able to read enough characters then throw an exception. + if (page == null) + { + throw new ArgumentOutOfRangeException("length"); + } + + int relativeStartPosition = AbsoluteOffsetToPageOffset(startPosition + i); + int charactersOnPage = GetCharactersOnPage(startPosition + i); + + charactersExtracted = Math.Min(length - i, charactersOnPage - relativeStartPosition); + ErrorUtilities.VerifyThrow(charactersExtracted > 0, "Expected non-zero extraction count."); + + result.Append(page, relativeStartPosition, charactersExtracted); + } + return result.ToString(); + } + + /// + /// Returns the number of characters on the page given by offset. + /// + /// + /// + private int GetCharactersOnPage(int offset) + { + int page = PageFromAbsoluteOffset(offset); + ErrorUtilities.VerifyThrow(page >= _currentPageNumber - 1 && page <= _currentPageNumber, "Could not get character count for this page."); + + if (page == _currentPageNumber) + { + return _charactersRead; + } + + return _pageSize; + } + } +} diff --git a/src/Shared/LanguageParser/VisualBasictokenCharReader.cs b/src/Shared/LanguageParser/VisualBasictokenCharReader.cs new file mode 100644 index 00000000000..9faab70db0c --- /dev/null +++ b/src/Shared/LanguageParser/VisualBasictokenCharReader.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: VisualBasicTokenCharReader + * + * Reads over the contents of a vb source file (in the form of a string). + * Provides utility functions for dealing with VB-specific tokens. + * + */ + sealed internal class VisualBasicTokenCharReader : TokenCharReader + { + /* + * Method: VisualBasicTokenCharReader + * + * Construct + */ + internal VisualBasicTokenCharReader(Stream binaryStream, bool forceANSI) + : base(binaryStream, forceANSI) + { + } + + /* + * Method: SinkSeparatorCharacter + * + * Matches a vb separator character. + */ + internal bool SinkSeparatorCharacter() + { + if + ( + CurrentCharacter == '(' + || CurrentCharacter == ')' + || CurrentCharacter == '!' + || CurrentCharacter == '#' + || CurrentCharacter == ',' + || CurrentCharacter == '.' + || CurrentCharacter == ':' + || CurrentCharacter == '{' + || CurrentCharacter == '}' + ) + { + Skip(); + return true; + } + + return false; + } + + /* + * Method: SinkLineContinuationCharacter + * + * Matches a vb line continuation character. + */ + internal bool SinkLineContinuationCharacter() + { + if + ( + CurrentCharacter == '_' + ) + { + Skip(); + return true; + } + + return false; + } + + /* + * Method: SinkLineCommentStart + * + * Matches a vb start of comment indicator + */ + internal bool SinkLineCommentStart() + { + if (Sink("\'")) + { + return true; + } + else + { + int previousPosition = Position; + + if (SinkIgnoreCase("rem")) + { + if (SinkWhiteSpace()) + { + return true; + } + + // We've probably found an Identifier that starts with "rem", + // so return to the previous position. + Position = previousPosition; + } + } + return false; + } + + /* + * Method: SinkHexIntegerPrefix + * + * Matches a vb hex integer prefix + */ + internal bool SinkHexIntegerPrefix() + { + if (SinkIgnoreCase("&H")) + { + return true; + } + + return false; + } + + /* + * Method: SinkOctalIntegerPrefix + * + * Matches a vb octal integer prefix + */ + internal bool SinkOctalIntegerPrefix() + { + if (SinkIgnoreCase("&O")) + { + return true; + } + + return false; + } + + /* + * Method: SinkWhiteSpace + * + * Sink a single whitespace character. + * In vb, newlines are not considered whitespace. + */ + internal bool SinkWhiteSpace() + { + if (Char.IsWhiteSpace(CurrentCharacter) && !TokenChar.IsNewLine(CurrentCharacter)) + { + Skip(); + return true; + } + return false; + } + + /* + * Method: SinkIntegerSuffix + * + * Sink a vb integer suffix. + */ + internal bool SinkIntegerSuffix() + { + switch (CurrentCharacter) + { + case 'S': + case 's': + case 'I': + case 'i': + case 'L': + case 'l': + Skip(); + return true; + } + return true; // An integer suffix can be zero characters, so there's always a match. + } + + /* + * Method: SinkDecimalIntegerSuffix + * + * Sink a vb decimal integer suffix. + * Couldn't find this documented anywhere, but a decimal (as opposed to hex or octal) + * is also allowed a trailing '@', '!', '#' or '&' + */ + internal bool SinkDecimalIntegerSuffix() + { + switch (CurrentCharacter) + { + case 'S': + case 's': + case 'I': + case 'i': + case 'L': + case 'l': + case '@': + case '!': + case '#': + case '&': + case '%': + + Skip(); + return true; + } + return true; // An integer suffix can be zero characters, so there's always a match. + } + + + /* + * Method: SinkOctalDigits + * + * Sink multiple octal digits. + */ + internal bool SinkMultipleOctalDigits() + { + int count = 0; + while (TokenChar.IsOctalDigit(CurrentCharacter)) + { + ++count; + Skip(); + } + return count > 0; // Must match at least one + } + + /* + * Method: SinkOperator + * + * Determine whether this is a vb operator. + */ + internal bool SinkOperator() + { + const string operators = @"&|*+-/\^<=>"; + if (operators.IndexOf(CurrentCharacter) == -1) + { + return false; + } + Skip(); + return true; + } + + /* + * Method: SinkTypeCharacter + * + * Identifiers in vb can end with a special character to indicate type: + * IntegerTypeCharacter ::= % + * LongTypeCharacter ::= & + * DecimalTypeCharacter ::= @ + * SingleTypeCharacter ::= ! + * DoubleTypeCharacter ::= # + * StringTypeCharacter ::= $ + */ + internal bool SinkTypeCharacter() + { + const string types = @"%&@!#$"; + if (types.IndexOf(CurrentCharacter) == -1) + { + return false; + } + Skip(); + return true; + } + } +} + diff --git a/src/Shared/LanguageParser/VisualBasictokenEnumerator.cs b/src/Shared/LanguageParser/VisualBasictokenEnumerator.cs new file mode 100644 index 00000000000..486f1aedc26 --- /dev/null +++ b/src/Shared/LanguageParser/VisualBasictokenEnumerator.cs @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: VisualBasicTokenEnumerator + * + * Given vb sources, enumerate over all tokens. + * + */ + sealed internal class VisualBasicTokenEnumerator : TokenEnumerator + { + // Reader over the sources. + private VisualBasicTokenCharReader _reader = null; + + /* + * Method: TokenEnumerator + * + * Construct + */ + internal VisualBasicTokenEnumerator(Stream binaryStream, bool forceANSI) + { + _reader = new VisualBasicTokenCharReader(binaryStream, forceANSI); + } + + /* + * Method: FindNextToken + * + * Find the next token. Return 'true' if one was found. False, otherwise. + */ + override internal bool FindNextToken() + { + int startPosition = _reader.Position; + + // VB docs claim whitespace is Unicode category Zs. However, + // this category does not contain tabs. Assuming a less restrictive + // definition for whitespace... + if (_reader.SinkWhiteSpace()) + { + while (_reader.SinkWhiteSpace()) + { + } + + // Now, we need to check for the line continuation character. + if (_reader.SinkLineContinuationCharacter()) // Line continuation is '_' + { + // Save the current position because we may need to come back here. + int savePosition = _reader.Position - 1; + + // Skip all whitespace after the '_' + while (_reader.SinkWhiteSpace()) + { + } + + // Now, skip all the newlines. + // Need at least one newline for this to count as line continuation. + int count = 0; + while (_reader.SinkNewLine()) + { + ++count; + } + + if (count > 0) + { + current = new VisualBasicTokenizer.LineContinuationToken(); + return true; + } + + // Otherwise, fall back to plain old whitespace. + _reader.Position = savePosition; + } + + current = new WhitespaceToken(); + return true; + } + // Line terminators are separate from whitespace and are significant. + else if (_reader.SinkNewLine()) + { + // We want one token per line terminator. + current = new VisualBasicTokenizer.LineTerminatorToken(); + return true; + } + // Check for a comment--either those that start with ' or rem. + else if (_reader.SinkLineCommentStart()) + { + // Skip to the first EOL. + _reader.SinkToEndOfLine(); + + current = new CommentToken(); + return true; + } + // Identifier or keyword? + else if + ( + // VB allows escaping of identifiers by surrounding them with [] + // In other words, + // Date is a keyword but, + // [Date] is an identifier. + _reader.CurrentCharacter == '[' || + _reader.MatchNextIdentifierStart() + ) + { + bool escapedIdentifier = false; + if (_reader.CurrentCharacter == '[') + { + escapedIdentifier = true; + _reader.SinkCharacter(); + + // Now, the next character must be an identifier start. + if (!_reader.SinkIdentifierStart()) + { + current = new ExpectedIdentifierToken(); + return true; + } + } + + // Sink the rest of the identifier. + while (_reader.SinkIdentifierPart()) + { + } + + // If this was an escaped identifier the we need to get the terminating ']'. + if (escapedIdentifier) + { + if (!_reader.Sink("]")) + { + current = new ExpectedIdentifierToken(); + return true; + } + } + else + { + // Escaped identifiers are not allowed to have trailing type character. + _reader.SinkTypeCharacter(); // Type character is optional. + } + + // An identifier that is only a '_' is illegal because it is + // ambiguous with line continuation + string identifierOrKeyword = _reader.GetCurrentMatchedString(startPosition); + if (identifierOrKeyword == "_" || identifierOrKeyword == "[_]" || identifierOrKeyword == "[]") + { + current = new ExpectedIdentifierToken(); + return true; + } + + // Make an upper-case version in order to check whether this may be a keyword. + string upper = identifierOrKeyword.ToUpper(CultureInfo.InvariantCulture); + + switch (upper) + { + default: + + if (Array.IndexOf(s_keywordList, upper) >= 0) + { + current = new KeywordToken(); + return true; + } + + // Create the token. + current = new IdentifierToken(); + + // Trim off the [] if this is an escaped identifier. + if (escapedIdentifier) + { + current.InnerText = identifierOrKeyword.Substring(1, identifierOrKeyword.Length - 2); + } + return true; + case "FALSE": + case "TRUE": + current = new BooleanLiteralToken(); + return true; + } + } + // Is it a hex integer? + else if (_reader.SinkHexIntegerPrefix()) + { + if (!_reader.SinkMultipleHexDigits()) + { + current = new ExpectedValidHexDigitToken(); + return true; + } + + // Sink a suffix if there is one. + _reader.SinkIntegerSuffix(); + + current = new HexIntegerLiteralToken(); + return true; + } + // Is it an octal integer? + else if (_reader.SinkOctalIntegerPrefix()) + { + if (!_reader.SinkMultipleOctalDigits()) + { + current = new VisualBasicTokenizer.ExpectedValidOctalDigitToken(); + return true; + } + + // Sink a suffix if there is one. + _reader.SinkIntegerSuffix(); + + current = new VisualBasicTokenizer.OctalIntegerLiteralToken(); + return true; + } + // Is it a decimal integer? + else if (_reader.SinkMultipleDecimalDigits()) + { + // Sink a suffix if there is one. + _reader.SinkDecimalIntegerSuffix(); + + current = new DecimalIntegerLiteralToken(); + return true; + } + // Preprocessor line + else if (_reader.CurrentCharacter == '#') + { + if (_reader.SinkIgnoreCase("#if")) + { + current = new OpenConditionalDirectiveToken(); + } + else if (_reader.SinkIgnoreCase("#end if")) + { + current = new CloseConditionalDirectiveToken(); + } + else + { + current = new PreprocessorToken(); + } + + _reader.SinkToEndOfLine(); + + return true; + } + // Is it a separator? + else if (_reader.SinkSeparatorCharacter()) + { + current = new VisualBasicTokenizer.SeparatorToken(); + return true; + } + // Is it an operator? + else if (_reader.SinkOperator()) + { + current = new OperatorToken(); + return true; + } + // A string? + else if (_reader.Sink("\"")) + { + do + { + // Inside a verbatim string "" is treated as a special character + while (_reader.Sink("\"\"")) + { + } + } + while (!_reader.EndOfLines && _reader.SinkCharacter() != '\"'); + + // Can't end a file inside a string + if (_reader.EndOfLines) + { + current = new EndOfFileInsideStringToken(); + return true; + } + + current = new StringLiteralToken(); + return true; + } + + + // We didn't recognize the token, so this is a syntax error. + _reader.SinkCharacter(); + current = new UnrecognizedToken(); + return true; + } + + private static readonly string[] s_keywordList = + { "ADDHANDLER", "ADDRESSOF", "ANDALSO", "ALIAS", + "AND", "ANSI", "AS", "ASSEMBLY", + "AUTO", "BOOLEAN", "BYREF", "BYTE", + "BYVAL", "CALL", "CASE", "CATCH", + "CBOOL", "CBYTE", "CCHAR", "CDATE", + "CDEC", "CDBL", "CHAR", "CINT", + "CLASS", "CLNG", "COBJ", "CONST", "CONTINUE", "CSBYTE", + "CSHORT", "CSNG", "CSTR", "CTYPE", "CUINT", "CULNG", "CUSHORT", + "DATE", "DECIMAL", "DECLARE", "DEFAULT", + "DELEGATE", "DIM", "DIRECTCAST", "DO", + "DOUBLE", "EACH", "ELSE", "ELSEIF", + "END", "ENDIF", "ENUM", "ERASE", "ERROR", + "EVENT", "EXIT", "FALSE", "FINALLY", + "FOR", "FRIEND", "FUNCTION", "GET", + "GETTYPE", "GLOBAL", "GOSUB", "GOTO", "HANDLES", + "IF", "IMPLEMENTS", "IMPORTS", "IN", + "INHERITS", "INTEGER", "INTERFACE", "IS", "ISNOT", + "LET", "LIB", "LIKE", "LONG", + "LOOP", "ME", "MOD", "MODULE", + "MUSTINHERIT", "MUSTOVERRIDE", "MYBASE", "MYCLASS", + "NAMESPACE", "NARROWING", "NEW", "NEXT", "NOT", + "NOTHING", "NOTINHERITABLE", "NOTOVERRIDABLE", "OBJECT", + "OF", "ON", "OPERATOR", "OPTION", "OPTIONAL", "OR", + "ORELSE", "OVERLOADS", "OVERRIDABLE", "OVERRIDES", + "PARAMARRAY", "PARTIAL", "PRESERVE", "PRIVATE", "PROPERTY", + "PROTECTED", "PUBLIC", "RAISEEVENT", "READONLY", + "REDIM", "REM", "REMOVEHANDLER", "RESUME", + "RETURN", "SBYTE", "SELECT", "SET", "SHADOWS", + "SHARED", "SHORT", "SINGLE", "STATIC", + "STEP", "STOP", "STRING", "STRUCTURE", + "SUB", "SYNCLOCK", "THEN", "THROW", + "TO", "TRUE", "TRY", "TRYCAST", "TYPEOF", + "UNICODE", "UINTEGER", "ULONG", "UNTIL", "USHORT", "USING", "VARIANT", "WEND", "WHEN", + "WHILE", "WIDENING", "WITH", "WITHEVENTS", "WRITEONLY", + "XOR" }; + + + /* + * Method: Reader + * + * Return the token char reader. + */ + override internal TokenCharReader Reader + { + get + { + return _reader; + } + } + } +} diff --git a/src/Shared/LanguageParser/VisualBasictokenizer.cs b/src/Shared/LanguageParser/VisualBasictokenizer.cs new file mode 100644 index 00000000000..ef49547ea8d --- /dev/null +++ b/src/Shared/LanguageParser/VisualBasictokenizer.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: VisualBasicTokenizer + * + * Given vb sources, return an enumerator that will provide tokens one at a time. + * + */ + sealed internal class VisualBasicTokenizer : IEnumerable + { + /* + These are the tokens that are specific to the VB tokenizer. + Tokens that should be shared with other tokenizers should go + into Token.cs. + */ + internal class LineTerminatorToken : Token { } + internal class SeparatorToken : Token { } + + internal class LineContinuationToken : WhitespaceToken { } + + internal class OctalIntegerLiteralToken : IntegerLiteralToken { } + + internal class ExpectedValidOctalDigitToken : SyntaxErrorToken { } + + // The source lines + private Stream _binaryStream = null; + + // Whether or not to force ANSI reading. + private bool _forceANSI; + + /* + * Method: VisualBasicTokenizer + * + * Construct + */ + internal VisualBasicTokenizer(Stream binaryStream, bool forceANSI) + { + _binaryStream = binaryStream; + _forceANSI = forceANSI; + } + + /* + * Method: GetEnumerator + * + * Return a new token enumerator. + */ + public IEnumerator GetEnumerator() + { + return new VisualBasicTokenEnumerator(_binaryStream, _forceANSI); + } + } +} diff --git a/src/Shared/LanguageParser/token.cs b/src/Shared/LanguageParser/token.cs new file mode 100644 index 00000000000..5deedb0a4a6 --- /dev/null +++ b/src/Shared/LanguageParser/token.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: Token + * + * Base class for all token classes. + * + */ + internal abstract class Token + { + // The text from the originating source file that caused this token. + private string _innerText = null; + // The line number that the token fell on. + private int _line = 0; + + /* + * Method: InnerText + * + * Get or set the InnerText for this token + */ + internal string InnerText + { + get { return _innerText; } + set { _innerText = value; } + } + + /* + * Method: Line + * + * Get or set the Line for this token + */ + internal int Line + { + get + { + return _line; + } + set + { + _line = value; + } + } + + /* + * Method: EqualsIgnoreCase + * + * Return true if the given string equals the content of this token + */ + internal bool EqualsIgnoreCase(string compareTo) + { + return (String.Compare(_innerText, compareTo, StringComparison.OrdinalIgnoreCase) == 0); + } + } + + /* + Table of tokens shared by the parsers. + Tokens that are specific to a particular parser are nested within the given + parser class. + */ + internal class WhitespaceToken : Token { } + internal abstract class LiteralToken : Token { } + internal class BooleanLiteralToken : Token { } // i.e. true or false + internal abstract class IntegerLiteralToken : Token { } // i.e. a literal integer + internal class HexIntegerLiteralToken : IntegerLiteralToken { } // i.e. a hex literal integer + internal class DecimalIntegerLiteralToken : IntegerLiteralToken { } // i.e. a hex literal integer + internal class StringLiteralToken : Token { } // i.e. A string value. + internal abstract class SyntaxErrorToken : Token { } // A syntax error. + internal class ExpectedIdentifierToken : SyntaxErrorToken { } + internal class ExpectedValidHexDigitToken : SyntaxErrorToken { } // Got a non-hex digit when we expected to have one. + internal class EndOfFileInsideStringToken : SyntaxErrorToken { } // The file ended inside a string. + internal class UnrecognizedToken : SyntaxErrorToken { } // An unrecognized token was spotted. + internal class CommentToken : Token { } + internal class IdentifierToken : Token { } // An identifier + internal class KeywordToken : Token { } // An keyword + internal class PreprocessorToken : Token { } // #if, #region, etc. + internal class OpenConditionalDirectiveToken : PreprocessorToken { } + internal class CloseConditionalDirectiveToken : PreprocessorToken { } + internal class OperatorOrPunctuatorToken : Token { } // One of the predefined operators or punctuators + internal class OperatorToken : OperatorOrPunctuatorToken { } +} diff --git a/src/Shared/LanguageParser/tokenChar.cs b/src/Shared/LanguageParser/tokenChar.cs new file mode 100644 index 00000000000..c119d49c889 --- /dev/null +++ b/src/Shared/LanguageParser/tokenChar.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /// + /// Utility functions for classifying characters that might be found in a sources file. + /// + internal static class TokenChar + { + /// + /// Determine whether a given character is a newline character + /// + /// + /// + static internal bool IsNewLine(char c) + { + // From the C# spec and vb specs, newline characters are: + return c == 0x000d // Carriage return + || c == 0x000a // Linefeed + || c == 0x2028 // Line separator + || c == 0x2029 // Paragraph separator + ; + } + + /// + /// Determine whether a given character is a letter character + /// + /// + /// + static internal bool IsLetter(char c) + { + UnicodeCategory cat = Char.GetUnicodeCategory(c); + + // From 2.4.2 of the C# Language Specification + // letter-character: + if ( + cat == UnicodeCategory.UppercaseLetter + || cat == UnicodeCategory.LowercaseLetter + || cat == UnicodeCategory.TitlecaseLetter + || cat == UnicodeCategory.ModifierLetter + || cat == UnicodeCategory.OtherLetter + || cat == UnicodeCategory.LetterNumber + ) + { + return true; + } + return false; + } + + /// + /// Determine whether a given character is a decimal digit character + /// + /// + /// + static internal bool IsDecimalDigit(char c) + { + UnicodeCategory cat = Char.GetUnicodeCategory(c); + + // From 2.4.2 of the C# Language Specification + // decimal-digit-character: + if ( + cat == UnicodeCategory.DecimalDigitNumber + ) + { + return true; + } + return false; + } + + /// + /// Determine whether a given character is a connecting character + /// + /// + /// + static internal bool IsConnecting(char c) + { + UnicodeCategory cat = Char.GetUnicodeCategory(c); + + // From 2.4.2 of the C# Language Specification + // connecting-character: + if + ( + cat == UnicodeCategory.ConnectorPunctuation + ) + { + return true; + } + return false; + } + + /// + /// Determine whether a given character is a combining character + /// + /// + /// + static internal bool IsCombining(char c) + { + UnicodeCategory cat = Char.GetUnicodeCategory(c); + + // From 2.4.2 of the C# Language Specification + // combining-character: + if ( + cat == UnicodeCategory.NonSpacingMark // Mn + || cat == UnicodeCategory.SpacingCombiningMark // Mc + ) + { + return true; + } + return false; + } + + /// + /// Determine whether a given character is a C# formatting character + /// + /// + /// + static internal bool IsFormatting(char c) + { + UnicodeCategory cat = Char.GetUnicodeCategory(c); + + // From 2.4.2 of the C# Language Specification + // formatting-character: + if ( + cat == UnicodeCategory.Format // Cf + ) + { + return true; + } + return false; + } + + /// + /// Determine whether a given character is a hex digit character + /// + /// + /// + static internal bool IsHexDigit(char c) + { + // From 2.4.4.2 of the C# Language Specification + // hex-digit: + if + ( + (c >= '0' && c <= '9') + || (c >= 'A' && c <= 'F') + || (c >= 'a' && c <= 'f') + ) + { + return true; + } + return false; + } + + /// + /// Determine whether a given character is an octal digit character + /// + /// + /// + static internal bool IsOctalDigit(char c) + { + if + ( + (c >= '0' && c <= '7') + ) + { + return true; + } + return false; + } + } +} + diff --git a/src/Shared/LanguageParser/tokenCharReader.cs b/src/Shared/LanguageParser/tokenCharReader.cs new file mode 100644 index 00000000000..6e9e3d0f884 --- /dev/null +++ b/src/Shared/LanguageParser/tokenCharReader.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: TokenCharReader + * + * Reads over the contents of a source file (in the form of a string). + * Provides utility functions for skipping and checking the value of characters. + * + */ + internal class TokenCharReader + { + // The sources + private StreamMappedString _sources; + // Current character offset within sources. + private int _position; + // The current line. One-relative. + private int _currentLine; // One-relative + + /* + * Method: TokenCharReader + * + * Construct + */ + internal TokenCharReader(Stream binaryStream, bool forceANSI) + { + Reset(); + _sources = new StreamMappedString(binaryStream, forceANSI); + } + + /* + * Method: Reset + * + * Reset to the top of the sources. + */ + internal void Reset() + { + _position = 0; + _currentLine = 1; // One-relative + } + + /* + * Method: CurrentLine + * + * The current line number + */ + internal int CurrentLine + { + get { return _currentLine; } + } + + /* + * Method: Position + * + * The character offset within the sources. + */ + internal int Position + { + get { return _position; } + // Having a set operator makes this class not forward-only. + // If this becomes necessary later, then implement a push-pop + // scheme for saving current positions and get rid of this. + // This will force the caller to declare ahead of time whether + // they may want to return here. + set { _position = value; } + } + + /* + * Method: Skip + * + * Skip to the next character. + */ + protected void Skip() + { + if (TokenChar.IsNewLine(CurrentCharacter)) + { + ++_currentLine; + } + ++_position; + } + + /* + * Method: Skip (overload) + * + * Skip the next n characters. + */ + protected void Skip(int n) + { + for (int i = 0; i < n; ++i) + { + Skip(); + } + } + + /* + * Method: CurrentCharacter + * + * Get the current character. + */ + internal char CurrentCharacter + { + get { return _sources.GetAt(_position); } + } + + /* + * Method: EndOfLines + * + * Return true if we've reached the end of sources. + */ + internal bool EndOfLines + { + get { return _sources.IsPastEnd(_position); } + } + + + /* + * Method: GetCurrentMatchedString + * + * Get the string that starts with the given start position and ends with this.position. + */ + internal string GetCurrentMatchedString(int startPosition) + { + return _sources.Substring(startPosition, _position - startPosition); + } + + /* + * Method: Sink + * + * See if the next characters match the given string. If they do, + * sink this string. + */ + internal bool Sink(string match) + { + return Sink(match, false); + } + + /// + /// See if the next characters match the given string. If they do, sink this string. + /// + /// + /// + /// + private bool Sink(string match, bool ignoreCase) + { + // Is there enough left for this match? + if (_sources.IsPastEnd(_position + match.Length - 1)) + { + return false; + } + + + string compare = _sources.Substring(_position, match.Length); + + if + ( + String.Compare + ( + match, + compare, + (ignoreCase /* ignore case */) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal + ) == 0 + ) + { + Skip(match.Length); + return true; + } + + return false; + } + + /* + * Method: SinkCharacter + * + * Sink and return one character. + */ + internal char SinkCharacter() + { + char c = CurrentCharacter; + Skip(); + return c; + } + + /* + * Method: SinkIgnoreCase + * + * See if the next characters match the given string without case. + */ + internal bool SinkIgnoreCase(string match) + { + return Sink(match, true); + } + + /* + * Method: MatchNextIdentifierStart + * + * Determine whether a given character is a C# or VB identifier start character. + * Both languages agree on this format. + */ + internal bool MatchNextIdentifierStart() + { + // From 2.4.2 of the C# Language Specification + // identifier-start-letter-character: + if (CurrentCharacter == '_' || TokenChar.IsLetter(CurrentCharacter)) + { + return true; + } + return false; + } + + /* + * Method: SinkIdentifierStart + * + * Determine whether a given character is a C# or VB identifier start character. + * Both languages agree on this format. + */ + internal bool SinkIdentifierStart() + { + if (MatchNextIdentifierStart()) + { + Skip(); + return true; + } + return false; + } + + /* + * Method: SinkIdentifierPart + * + * Determine whether a given character is a C# or VB identifier part character + * Both languages agree on this format. + */ + internal bool SinkIdentifierPart() + { + // From 2.4.2 of the C# Language Specification + // identifier-part-letter-character: + if ( + TokenChar.IsLetter(CurrentCharacter) + || TokenChar.IsDecimalDigit(CurrentCharacter) + || TokenChar.IsConnecting(CurrentCharacter) + || TokenChar.IsCombining(CurrentCharacter) + || TokenChar.IsFormatting(CurrentCharacter) + ) + { + Skip(); + return true; + } + return false; + } + + /* + * Method: SinkNewLine + * + * Sink a newline. + */ + internal bool SinkNewLine() + { + if (EndOfLines) + { + return false; + } + + int originalPosition = _position; + + if (Sink("\xd\xa")) // This sequence is treated as a single new line. + { + ++_currentLine; + ErrorUtilities.VerifyThrow(originalPosition != _position, "Expected position to be incremented."); + return true; + } + + if (TokenChar.IsNewLine(CurrentCharacter)) + { + Skip(); + ErrorUtilities.VerifyThrow(originalPosition != _position, "Expected position to be incremented."); + return true; + } + + return false; + } + + /* + * Method: SinkToEndOfLine + * + * Sink from the current position to the first end-of-line. + */ + internal bool SinkToEndOfLine() + { + while (!TokenChar.IsNewLine(CurrentCharacter)) + { + Skip(); + } + return true; // Matching zero characters is ok. + } + + /* + * Method: SinkUntil + * + * Sink until the given string is found. Match including the given string. + */ + internal bool SinkUntil(string find) + { + bool found = false; + while (!EndOfLines && !found) + { + if (Sink(find)) + { + found = true; + } + else + { + Skip(); + } + } + + return found; // Return true if the matching string was found. + } + + /* + * Method: SinkMultipleHexDigits + * + * Sink multiple hex digits. + */ + internal bool SinkMultipleHexDigits() + { + int count = 0; + while (TokenChar.IsHexDigit(CurrentCharacter)) + { + ++count; + Skip(); + } + return count > 0; // Must match at least one + } + + /* + * Method: SinkMultipleDecimalDigits + * + * Sink multiple decimal digits. + */ + internal bool SinkMultipleDecimalDigits() + { + int count = 0; + while (TokenChar.IsDecimalDigit(CurrentCharacter)) + { + ++count; + Skip(); + } + return count > 0; // Must match at least one + } + } +} + diff --git a/src/Shared/LanguageParser/tokenEnumerator.cs b/src/Shared/LanguageParser/tokenEnumerator.cs new file mode 100644 index 00000000000..48b82b6be59 --- /dev/null +++ b/src/Shared/LanguageParser/tokenEnumerator.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; + +namespace Microsoft.Build.Shared.LanguageParser +{ + /* + * Class: TokenEnumerator + * + * Abstract base class for implementing IEnumerator over a TokenCharReader. + * Derived class is responsible for actual tokenization. + * + */ + internal abstract class TokenEnumerator : IEnumerator + { + // The current token that was found. + protected Token current = null; + + // Return the token char reader. + abstract internal TokenCharReader Reader { get; } + + // Implemented by derived class to find the next token. + abstract internal bool FindNextToken(); + + /* + * Method: MoveNext + * + * Declare the MoveNext method required by IEnumerator + */ + public bool MoveNext() + { + if (Reader.EndOfLines) + { + return false; + } + + int startLine = Reader.CurrentLine; + int startPosition = Reader.Position; + + bool found = FindNextToken(); + + // If a token was found, record the line number and text into + if (found && this.current != null) + { + this.current.Line = startLine; + + // Don't record if there is already something there. + // This is so that FindNextToken can set the value if it wants to. + if (this.current.InnerText == null) + { + this.current.InnerText = Reader.GetCurrentMatchedString(startPosition); + } + } + return found; + } + + /* + * Method: Reset + * + * Declare the Reset method required by IEnumerator + */ + public void Reset() + { + Reader.Reset(); + this.current = null; + } + + /* + * Method: Current + * + * Declare the Current property required by IEnumerator + */ + public object Current + { + get { return current; } + } + } +} diff --git a/src/Shared/LoadedType.cs b/src/Shared/LoadedType.cs new file mode 100644 index 00000000000..7c11ffc0738 --- /dev/null +++ b/src/Shared/LoadedType.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections.Generic; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Shared +{ + /// + /// This class packages information about a type loaded from an assembly: for example, + /// the GenerateResource task class type or the ConsoleLogger logger class type. + /// + internal sealed class LoadedType + { + #region Constructors + + /// + /// Creates an instance of this class for the given type. + /// + /// + /// + internal LoadedType(Type type, AssemblyLoadInfo assemblyLoadInfo) + : this(type, assemblyLoadInfo, null) + { + } + + /// + /// Creates an instance of this class for the given type. + /// + /// The Type to be loaded + /// Information used to load the assembly + /// The assembly which has been loaded, if any + internal LoadedType(Type type, AssemblyLoadInfo assemblyLoadInfo, Assembly loadedAssembly) + { + ErrorUtilities.VerifyThrow(type != null, "We must have the type."); + ErrorUtilities.VerifyThrow(assemblyLoadInfo != null, "We must have the assembly the type was loaded from."); + + _type = type; + _assembly = assemblyLoadInfo; + _loadedAssembly = loadedAssembly; + + CheckForHardcodedSTARequirement(); + HasLoadInSeparateAppDomainAttribute(); + HasSTAThreadAttribute(); + } + + + #endregion + + #region Methods + /// + /// Gets whether there's a LoadInSeparateAppDomain attribute on this type. + /// Caches the result - since it can't change during the build. + /// + /// + public bool HasLoadInSeparateAppDomainAttribute() + { + if (_hasLoadInSeparateAppDomainAttribute == null) + { + _hasLoadInSeparateAppDomainAttribute = this.Type.IsDefined(typeof(LoadInSeparateAppDomainAttribute), true /* inherited */); + } + + return (bool)_hasLoadInSeparateAppDomainAttribute; + } + + /// + /// Gets whether there's a STAThread attribute on the Execute method of this type. + /// Caches the result - since it can't change during the build. + /// + /// + public bool HasSTAThreadAttribute() + { + if (_hasSTAThreadAttribute == null) + { + _hasSTAThreadAttribute = this.Type.IsDefined(typeof(RunInSTAAttribute), true /* inherited */); + } + + return (bool)_hasSTAThreadAttribute; + } + + #endregion + + /// + /// Determines if the task has a hardcoded requirement for STA thread usage. + /// + private void CheckForHardcodedSTARequirement() + { + // Special hard-coded attributes for certain legacy tasks which need to run as STA because they were written before + // we changed to running all tasks in MTA. + if (String.Equals("Microsoft.Build.Tasks.Xaml.PartialClassGenerationTask", _type.FullName, StringComparison.OrdinalIgnoreCase)) + { + AssemblyName assemblyName = _type.Assembly.GetName(); + Version lastVersionToForce = new Version(3, 5); + if (assemblyName.Version.CompareTo(lastVersionToForce) > 0) + { + if (String.Equals(assemblyName.Name, "PresentationBuildTasks", StringComparison.OrdinalIgnoreCase)) + { + _hasSTAThreadAttribute = true; + } + } + } + } + + #region Properties + + /// + /// Gets the type that was loaded from an assembly. + /// + /// The loaded type. + internal Type Type + { + get + { + return _type; + } + } + + /// + /// If we loaded an assembly for this type. + /// We use this information to help created AppDomains to resolve types that it could not load successfuly + /// + internal Assembly LoadedAssembly + { + get + { + return _loadedAssembly; + } + } + + /// + /// Gets the assembly the type was loaded from. + /// + /// The assembly info for the loaded type. + internal AssemblyLoadInfo Assembly + { + get + { + return _assembly; + } + } + + #endregion + + // the type that was loaded + private Type _type; + // the assembly the type was loaded from + private AssemblyLoadInfo _assembly; + + // whether the loadinseparateappdomain attribute is applied to this type + private bool? _hasLoadInSeparateAppDomainAttribute; + + // whether the STAThread attribute is applied to this type + private bool? _hasSTAThreadAttribute; + + /// + /// Assembly, if any, that we loaded for this type. + /// We use this information to help created AppDomains to resolve types that it could not load successfuly + /// + private Assembly _loadedAssembly; + } +} diff --git a/src/Shared/LogMessagePacketBase.cs b/src/Shared/LogMessagePacketBase.cs new file mode 100644 index 00000000000..cec9d20c8f2 --- /dev/null +++ b/src/Shared/LogMessagePacketBase.cs @@ -0,0 +1,891 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Base class for the packets which are used for passing logged +// messages across nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; + +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using TaskEngineAssemblyResolver = Microsoft.Build.BackEnd.Logging.TaskEngineAssemblyResolver; + +namespace Microsoft.Build.Shared +{ + #region Enumerations + /// + /// An enumeration of all the types of BuildEventArgs that can be + /// packaged by this logMessagePacket + /// + internal enum LoggingEventType : int + { + /// + /// An invalid eventId, used during initialization of a LoggingEventType + /// + Invalid = -1, + + /// + /// Event is a CustomEventArgs + /// + CustomEvent = 0, + + /// + /// Event is a BuildErrorEventArgs + /// + BuildErrorEvent = 1, + + /// + /// Event is a BuildFinishedEventArgs + /// + BuildFinishedEvent = 2, + + /// + /// Event is a BuildMessageEventArgs + /// + BuildMessageEvent = 3, + + /// + /// Event is a BuildStartedEventArgs + /// + BuildStartedEvent = 4, + + /// + /// Event is a BuildWarningEventArgs + /// + BuildWarningEvent = 5, + + /// + /// Event is a ProjectFinishedEventArgs + /// + ProjectFinishedEvent = 6, + + /// + /// Event is a ProjectStartedEventArgs + /// + ProjectStartedEvent = 7, + + /// + /// Event is a TargetStartedEventArgs + /// + TargetStartedEvent = 8, + + /// + /// Event is a TargetFinishedEventArgs + /// + TargetFinishedEvent = 9, + + /// + /// Event is a TaskStartedEventArgs + /// + TaskStartedEvent = 10, + + /// + /// Event is a TaskFinishedEventArgs + /// + TaskFinishedEvent = 11, + + /// + /// Event is a TaskCommandLineEventArgs + /// + TaskCommandLineEvent = 12 + } + #endregion + + /// + /// A packet to encapsulate a BuildEventArg logging message. + /// Contents: + /// Build Event Type + /// Build Event Args + /// + internal abstract class LogMessagePacketBase : INodePacket + { + /// + /// The packet version, which is based on the CLR version. Cached because querying Environment.Version each time becomes an allocation bottleneck. + /// + private static readonly int s_defaultPacketVersion = (Environment.Version.Major * 10) + Environment.Version.Minor; + + /// + /// Dictionary of methods used to read BuildEventArgs. + /// + private static Dictionary s_readMethodCache = new Dictionary(); + + /// + /// Dictionary of methods used to write BuildEventArgs. + /// + private static Dictionary s_writeMethodCache = new Dictionary(); + + /// + /// Dictionary of assemblies we've added to the resolver. + /// + private static HashSet s_customEventsLoaded = new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// The resolver used to load custom event types. + /// + private static TaskEngineAssemblyResolver s_resolver; + + /// + /// The object used to synchronize access to shared data. + /// + private static object s_lockObject = new Object(); + + /// + /// Delegate for translating targetfinished events. + /// + private TargetFinishedTranslator _targetFinishedTranslator = null; + + #region Data + + /// + /// The event type of the buildEventArg based on the + /// LoggingEventType enumeration + /// + private LoggingEventType _eventType = LoggingEventType.Invalid; + + /// + /// The buildEventArg which is encapsulated by the packet + /// + private BuildEventArgs _buildEvent; + + /// + /// The sink id + /// + private int _sinkId; + + #endregion + + #region Constructors + + /// + /// Encapsulates the buildEventArg in this packet. + /// + internal LogMessagePacketBase(KeyValuePair? nodeBuildEvent, TargetFinishedTranslator targetFinishedTranslator) + { + ErrorUtilities.VerifyThrow(nodeBuildEvent != null, "nodeBuildEvent was null"); + _buildEvent = nodeBuildEvent.Value.Value; + _sinkId = nodeBuildEvent.Value.Key; + _eventType = GetLoggingEventId(_buildEvent); + _targetFinishedTranslator = targetFinishedTranslator; + } + + /// + /// Constructor for deserialization + /// + protected LogMessagePacketBase(INodePacketTranslator translator) + { + Translate(translator); + } + + #endregion + + #region Delegates + + /// + /// Delegate for translating TargetFinishedEventArgs + /// + internal delegate void TargetFinishedTranslator(INodePacketTranslator translator, TargetFinishedEventArgs finishedEvent); + + /// + /// Delegate representing a method on the BuildEventArgs classes used to write to a stream. + /// + private delegate void ArgsWriterDelegate(BinaryWriter writer); + + /// + /// Delegate representing a method on the BuildEventArgs classes used to read from a stream. + /// + private delegate void ArgsReaderDelegate(BinaryReader reader, int version); + + #endregion + + #region Properties + + /// + /// The nodePacket Type, in this case the packet is a Logging Message + /// + public NodePacketType Type + { + get { return NodePacketType.LogMessage; } + } + + /// + /// The buildEventArg wrapped by this packet + /// + internal KeyValuePair? NodeBuildEvent + { + get + { + return new KeyValuePair(_sinkId, _buildEvent); + } + } + + /// + /// The event type of the wrapped buildEventArg + /// based on the LoggingEventType enumeration + /// + internal LoggingEventType EventType + { + get + { + return _eventType; + } + } + #endregion + + #region INodePacket Methods + + /// + /// Reads/writes this packet + /// + public void Translate(INodePacketTranslator translator) + { + translator.TranslateEnum(ref _eventType, (int)_eventType); + translator.Translate(ref _sinkId); + if (translator.Mode == TranslationDirection.ReadFromStream) + { + ReadFromStream(translator); + } + else + { + WriteToStream(translator); + } + } + + #endregion + + /// + /// Writes the logging packet to the translator. + /// + internal void WriteToStream(INodePacketTranslator translator) + { + if (_eventType != LoggingEventType.CustomEvent) + { + MethodInfo methodInfo = null; + lock (s_writeMethodCache) + { + if (!s_writeMethodCache.TryGetValue(_eventType, out methodInfo)) + { + Type eventDerivedType = _buildEvent.GetType(); + methodInfo = eventDerivedType.GetMethod("WriteToStream", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod); + s_writeMethodCache.Add(_eventType, methodInfo); + } + } + + int packetVersion = s_defaultPacketVersion; + + // Make sure the other side knows what sort of serialization is coming + translator.Translate(ref packetVersion); + + bool eventCanSerializeItself = (methodInfo != null) ? true : false; + translator.Translate(ref eventCanSerializeItself); + + if (eventCanSerializeItself) + { + // 3.5 or later -- we have custom serialization methods, so let's use them. + ArgsWriterDelegate writerMethod = (ArgsWriterDelegate)CreateDelegateRobust(typeof(ArgsWriterDelegate), _buildEvent, methodInfo); + writerMethod(translator.Writer); + + if (_eventType == LoggingEventType.TargetFinishedEvent && _targetFinishedTranslator != null) + { + _targetFinishedTranslator(translator, (TargetFinishedEventArgs)_buildEvent); + } + } + else + { + WriteEventToStream(_buildEvent, _eventType, translator); + } + } + else + { + string assemblyLocation = _buildEvent.GetType().Assembly.Location; + translator.Translate(ref assemblyLocation); + translator.TranslateDotNet(ref _buildEvent); + } + } + + /// + /// Reads the logging packet from the translator. + /// + internal void ReadFromStream(INodePacketTranslator translator) + { + if (LoggingEventType.CustomEvent != _eventType) + { + _buildEvent = GetBuildEventArgFromId(); + + // The other side is telling us whether the event knows how to log itself, or whether we're going to have + // to do it manually + int packetVersion = s_defaultPacketVersion; + translator.Translate(ref packetVersion); + + bool eventCanSerializeItself = true; + translator.Translate(ref eventCanSerializeItself); + + if (eventCanSerializeItself) + { + MethodInfo methodInfo = null; + lock (s_readMethodCache) + { + if (!s_readMethodCache.TryGetValue(_eventType, out methodInfo)) + { + Type eventDerivedType = _buildEvent.GetType(); + methodInfo = eventDerivedType.GetMethod("CreateFromStream", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod); + s_readMethodCache.Add(_eventType, methodInfo); + } + } + + ArgsReaderDelegate readerMethod = (ArgsReaderDelegate)CreateDelegateRobust(typeof(ArgsReaderDelegate), _buildEvent, methodInfo); + + readerMethod(translator.Reader, packetVersion); + if (_eventType == LoggingEventType.TargetFinishedEvent && _targetFinishedTranslator != null) + { + _targetFinishedTranslator(translator, (TargetFinishedEventArgs)_buildEvent); + } + } + else + { + _buildEvent = ReadEventFromStream(_eventType, translator); + } + } + else + { + string fileLocation = null; + translator.Translate(ref fileLocation); + + bool resolveAssembly = false; + lock (s_lockObject) + { + if (!s_customEventsLoaded.Contains(fileLocation)) + { + resolveAssembly = true; + } + + // If we are to resolve the assembly add it to the list of assemblies resolved + if (resolveAssembly) + { + s_customEventsLoaded.Add(fileLocation); + } + } + + if (resolveAssembly) + { + s_resolver = new TaskEngineAssemblyResolver(); + s_resolver.InstallHandler(); + s_resolver.Initialize(fileLocation); + } + + try + { + translator.TranslateDotNet(ref _buildEvent); + } + finally + { + if (resolveAssembly) + { + s_resolver.RemoveHandler(); + s_resolver = null; + } + } + } + + _eventType = GetLoggingEventId(_buildEvent); + } + + #region Private Methods + + /// + /// Wrapper for Delegate.CreateDelegate with retries. + /// + /// + /// TODO: Investigate if it would be possible to use one of the overrides of CreateDelegate + /// that doesn't force the delegate to be closed over its first argument, so that we can + /// only create the delegate once per event type and cache it. + /// + private static Delegate CreateDelegateRobust(Type type, Object firstArgument, MethodInfo methodInfo) + { + Delegate delegateMethod = null; + + for (int i = 0; delegateMethod == null && i < 5; i++) + { + try + { + delegateMethod = Delegate.CreateDelegate(type, firstArgument, methodInfo); + } + catch (FileLoadException) + { + // Sometimes, in 64-bit processes, the fusion load of Microsoft.Build.Framework.dll + // spontaneously fails when trying to bind to the delegate. However, it seems to + // not repeat on additional tries -- so we'll try again a few times. However, if + // it keeps happening, it's probably a real problem, so we want to go ahead and + // throw to let the user know what's up. + if (i == 5) + { + throw; + } + } + } + + return delegateMethod; + } + + /// + /// Takes in a id (LoggingEventType as an int) and creates the correct specific logging class + /// + private BuildEventArgs GetBuildEventArgFromId() + { + switch (_eventType) + { + case LoggingEventType.BuildErrorEvent: + return new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, null, null, null); + case LoggingEventType.BuildFinishedEvent: + return new BuildFinishedEventArgs(null, null, false); + case LoggingEventType.BuildMessageEvent: + return new BuildMessageEventArgs(null, null, null, MessageImportance.Normal); + case LoggingEventType.BuildStartedEvent: + return new BuildStartedEventArgs(null, null); + case LoggingEventType.BuildWarningEvent: + return new BuildWarningEventArgs(null, null, null, -1, -1, -1, -1, null, null, null); + case LoggingEventType.ProjectFinishedEvent: + return new ProjectFinishedEventArgs(null, null, null, false); + case LoggingEventType.ProjectStartedEvent: + return new ProjectStartedEventArgs(null, null, null, null, null, null); + case LoggingEventType.TargetStartedEvent: + return new TargetStartedEventArgs(null, null, null, null, null); + case LoggingEventType.TargetFinishedEvent: + return new TargetFinishedEventArgs(null, null, null, null, null, false); + case LoggingEventType.TaskStartedEvent: + return new TaskStartedEventArgs(null, null, null, null, null); + case LoggingEventType.TaskFinishedEvent: + return new TaskFinishedEventArgs(null, null, null, null, null, false); + case LoggingEventType.TaskCommandLineEvent: + return new TaskCommandLineEventArgs(null, null, MessageImportance.Normal); + default: + ErrorUtilities.VerifyThrow(false, "Should not get to the default of GetBuildEventArgFromId ID: " + _eventType); + return null; + } + } + + /// + /// Based on the type of the BuildEventArg to be wrapped + /// generate an Id which identifies which concrete type the + /// BuildEventArg is. + /// + /// Argument to get the type Id for + /// An enumeration entry which represents the type + private LoggingEventType GetLoggingEventId(BuildEventArgs eventArg) + { + Type eventType = eventArg.GetType(); + if (eventType == typeof(BuildMessageEventArgs)) + { + return LoggingEventType.BuildMessageEvent; + } + else if (eventType == typeof(TaskCommandLineEventArgs)) + { + return LoggingEventType.TaskCommandLineEvent; + } + else if (eventType == typeof(ProjectFinishedEventArgs)) + { + return LoggingEventType.ProjectFinishedEvent; + } + else if (eventType == typeof(ProjectStartedEventArgs)) + { + return LoggingEventType.ProjectStartedEvent; + } + else if (eventType == typeof(TargetStartedEventArgs)) + { + return LoggingEventType.TargetStartedEvent; + } + else if (eventType == typeof(TargetFinishedEventArgs)) + { + return LoggingEventType.TargetFinishedEvent; + } + else if (eventType == typeof(TaskStartedEventArgs)) + { + return LoggingEventType.TaskStartedEvent; + } + else if (eventType == typeof(TaskFinishedEventArgs)) + { + return LoggingEventType.TaskFinishedEvent; + } + else if (eventType == typeof(BuildFinishedEventArgs)) + { + return LoggingEventType.BuildFinishedEvent; + } + else if (eventType == typeof(BuildStartedEventArgs)) + { + return LoggingEventType.BuildStartedEvent; + } + else if (eventType == typeof(BuildWarningEventArgs)) + { + return LoggingEventType.BuildWarningEvent; + } + else if (eventType == typeof(BuildErrorEventArgs)) + { + return LoggingEventType.BuildErrorEvent; + } + else + { + return LoggingEventType.CustomEvent; + } + } + + /// + /// Given a build event that is presumed to be 2.0 (due to its lack of a "WriteToStream" method) and its + /// LoggingEventType, serialize that event to the stream. + /// + private void WriteEventToStream(BuildEventArgs buildEvent, LoggingEventType eventType, INodePacketTranslator translator) + { + string message = buildEvent.Message; + string helpKeyword = buildEvent.HelpKeyword; + string senderName = buildEvent.SenderName; + + translator.Translate(ref message); + translator.Translate(ref helpKeyword); + translator.Translate(ref senderName); + + // It is essential that you translate in the same order during writing and reading + switch (eventType) + { + case LoggingEventType.BuildMessageEvent: + WriteBuildMessageEventToStream((BuildMessageEventArgs)buildEvent, translator); + break; + case LoggingEventType.TaskCommandLineEvent: + WriteTaskCommandLineEventToStream((TaskCommandLineEventArgs)buildEvent, translator); + break; + case LoggingEventType.BuildErrorEvent: + WriteBuildErrorEventToStream((BuildErrorEventArgs)buildEvent, translator); + break; + case LoggingEventType.BuildWarningEvent: + WriteBuildWarningEventToStream((BuildWarningEventArgs)buildEvent, translator); + break; + case LoggingEventType.ProjectStartedEvent: + WriteExternalProjectStartedEventToStream((ExternalProjectStartedEventArgs)buildEvent, translator); + break; + case LoggingEventType.ProjectFinishedEvent: + WriteExternalProjectFinishedEventToStream((ExternalProjectFinishedEventArgs)buildEvent, translator); + break; + default: + ErrorUtilities.ThrowInternalError("Not Supported LoggingEventType {0}", eventType.ToString()); + break; + } + } + + /// + /// Serialize ExternalProjectFinished Event Argument to the stream + /// + private void WriteExternalProjectFinishedEventToStream(ExternalProjectFinishedEventArgs externalProjectFinishedEventArgs, INodePacketTranslator translator) + { + string projectFile = externalProjectFinishedEventArgs.ProjectFile; + translator.Translate(ref projectFile); + + bool succeeded = externalProjectFinishedEventArgs.Succeeded; + translator.Translate(ref succeeded); + } + + /// + /// ExternalProjectStartedEvent + /// + private void WriteExternalProjectStartedEventToStream(ExternalProjectStartedEventArgs externalProjectStartedEventArgs, INodePacketTranslator translator) + { + string projectFile = externalProjectStartedEventArgs.ProjectFile; + translator.Translate(ref projectFile); + + string targetNames = externalProjectStartedEventArgs.TargetNames; + translator.Translate(ref targetNames); + } + + #region Writes to Stream + + /// + /// Write Build Warning Log message into the translator + /// + private void WriteBuildWarningEventToStream(BuildWarningEventArgs buildWarningEventArgs, INodePacketTranslator translator) + { + string code = buildWarningEventArgs.Code; + translator.Translate(ref code); + + int columnNumber = buildWarningEventArgs.ColumnNumber; + translator.Translate(ref columnNumber); + + int endColumnNumber = buildWarningEventArgs.EndColumnNumber; + translator.Translate(ref endColumnNumber); + + int endLineNumber = buildWarningEventArgs.EndLineNumber; + translator.Translate(ref endLineNumber); + + string file = buildWarningEventArgs.File; + translator.Translate(ref file); + + int lineNumber = buildWarningEventArgs.LineNumber; + translator.Translate(ref lineNumber); + + string subCategory = buildWarningEventArgs.Subcategory; + translator.Translate(ref subCategory); + } + + /// + /// Write a Build Error message into the translator + /// + private void WriteBuildErrorEventToStream(BuildErrorEventArgs buildErrorEventArgs, INodePacketTranslator translator) + { + string code = buildErrorEventArgs.Code; + translator.Translate(ref code); + + int columnNumber = buildErrorEventArgs.ColumnNumber; + translator.Translate(ref columnNumber); + + int endColumnNumber = buildErrorEventArgs.EndColumnNumber; + translator.Translate(ref endColumnNumber); + + int endLineNumber = buildErrorEventArgs.EndLineNumber; + translator.Translate(ref endLineNumber); + + string file = buildErrorEventArgs.File; + translator.Translate(ref file); + + int lineNumber = buildErrorEventArgs.LineNumber; + translator.Translate(ref lineNumber); + + string subCategory = buildErrorEventArgs.Subcategory; + translator.Translate(ref subCategory); + } + + /// + /// Write Task Command Line log message into the translator + /// + private void WriteTaskCommandLineEventToStream(TaskCommandLineEventArgs taskCommandLineEventArgs, INodePacketTranslator translator) + { + MessageImportance importance = taskCommandLineEventArgs.Importance; + translator.TranslateEnum(ref importance, (int)importance); + + string commandLine = taskCommandLineEventArgs.CommandLine; + translator.Translate(ref commandLine); + + string taskName = taskCommandLineEventArgs.TaskName; + translator.Translate(ref taskName); + } + + /// + /// Write a "standard" Message Log the translator + /// + private void WriteBuildMessageEventToStream(BuildMessageEventArgs buildMessageEventArgs, INodePacketTranslator translator) + { + MessageImportance importance = buildMessageEventArgs.Importance; + translator.TranslateEnum(ref importance, (int)importance); + } + + #endregion + + #region Reads from Stream + + /// + /// Given a build event that is presumed to be 2.0 (due to its lack of a "ReadFromStream" method) and its + /// LoggingEventType, read that event from the stream. + /// + private BuildEventArgs ReadEventFromStream(LoggingEventType eventType, INodePacketTranslator translator) + { + string message = null; + string helpKeyword = null; + string senderName = null; + + translator.Translate(ref message); + translator.Translate(ref helpKeyword); + translator.Translate(ref senderName); + + BuildEventArgs buildEvent = null; + switch (eventType) + { + case LoggingEventType.TaskCommandLineEvent: + buildEvent = ReadTaskCommandLineEventFromStream(translator, message, helpKeyword, senderName); + break; + case LoggingEventType.BuildErrorEvent: + buildEvent = ReadTaskBuildErrorEventFromStream(translator, message, helpKeyword, senderName); + break; + case LoggingEventType.ProjectStartedEvent: + buildEvent = ReadExternalProjectStartedEventFromStream(translator, message, helpKeyword, senderName); + break; + case LoggingEventType.ProjectFinishedEvent: + buildEvent = ReadExternalProjectFinishedEventFromStream(translator, message, helpKeyword, senderName); + break; + case LoggingEventType.BuildMessageEvent: + buildEvent = ReadBuildMessageEventFromStream(translator, message, helpKeyword, senderName); + break; + case LoggingEventType.BuildWarningEvent: + buildEvent = ReadBuildWarningEventFromStream(translator, message, helpKeyword, senderName); + break; + default: + ErrorUtilities.ThrowInternalError("Not Supported LoggingEventType {0}", eventType.ToString()); + break; + } + + return buildEvent; + } + + /// + /// Read and reconstruct a ProjectFinishedEventArgs from the stream + /// + private ExternalProjectFinishedEventArgs ReadExternalProjectFinishedEventFromStream(INodePacketTranslator translator, string message, string helpKeyword, string senderName) + { + string projectFile = null; + translator.Translate(ref projectFile); + + bool succeeded = true; + translator.Translate(ref succeeded); + + ExternalProjectFinishedEventArgs buildEvent = + new ExternalProjectFinishedEventArgs( + message, + helpKeyword, + senderName, + projectFile, + succeeded); + + return buildEvent; + } + + /// + /// Read and reconstruct a ProjectStartedEventArgs from the stream + /// + private ExternalProjectStartedEventArgs ReadExternalProjectStartedEventFromStream(INodePacketTranslator translator, string message, string helpKeyword, string senderName) + { + string projectFile = null; + translator.Translate(ref projectFile); + + string targetNames = null; + translator.Translate(ref targetNames); + + ExternalProjectStartedEventArgs buildEvent = + new ExternalProjectStartedEventArgs( + message, + helpKeyword, + senderName, + projectFile, + targetNames); + + return buildEvent; + } + + /// + /// Read and reconstruct a BuildWarningEventArgs from the stream + /// + private BuildWarningEventArgs ReadBuildWarningEventFromStream(INodePacketTranslator translator, string message, string helpKeyword, string senderName) + { + string code = null; + translator.Translate(ref code); + + int columnNumber = -1; + translator.Translate(ref columnNumber); + + int endColumnNumber = -1; + translator.Translate(ref endColumnNumber); + + int endLineNumber = -1; + translator.Translate(ref endLineNumber); + + string file = null; + translator.Translate(ref file); + + int lineNumber = -1; + translator.Translate(ref lineNumber); + + string subCategory = null; + translator.Translate(ref subCategory); + + BuildWarningEventArgs buildEvent = + new BuildWarningEventArgs( + subCategory, + code, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + senderName); + + return buildEvent; + } + + /// + /// Read and reconstruct a BuildErrorEventArgs from the stream + /// + private BuildErrorEventArgs ReadTaskBuildErrorEventFromStream(INodePacketTranslator translator, string message, string helpKeyword, string senderName) + { + string code = null; + translator.Translate(ref code); + + int columnNumber = -1; + translator.Translate(ref columnNumber); + + int endColumnNumber = -1; + translator.Translate(ref endColumnNumber); + + int endLineNumber = -1; + translator.Translate(ref endLineNumber); + + string file = null; + translator.Translate(ref file); + + int lineNumber = -1; + translator.Translate(ref lineNumber); + + string subCategory = null; + translator.Translate(ref subCategory); + + BuildErrorEventArgs buildEvent = + new BuildErrorEventArgs( + subCategory, + code, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + senderName); + + return buildEvent; + } + + /// + /// Read and reconstruct a TaskCommandLineEventArgs from the stream + /// + private TaskCommandLineEventArgs ReadTaskCommandLineEventFromStream(INodePacketTranslator translator, string message, string helpKeyword, string senderName) + { + MessageImportance importance = MessageImportance.Normal; + translator.TranslateEnum(ref importance, (int)importance); + + string commandLine = null; + translator.Translate(ref commandLine); + + string taskName = null; + translator.Translate(ref taskName); + + TaskCommandLineEventArgs buildEvent = new TaskCommandLineEventArgs(commandLine, taskName, importance); + return buildEvent; + } + + /// + /// Read and reconstruct a BuildMessageEventArgs from the stream + /// + private BuildMessageEventArgs ReadBuildMessageEventFromStream(INodePacketTranslator translator, string message, string helpKeyword, string senderName) + { + MessageImportance importance = MessageImportance.Normal; + + translator.TranslateEnum(ref importance, (int)importance); + + BuildMessageEventArgs buildEvent = new BuildMessageEventArgs(message, helpKeyword, senderName, importance); + return buildEvent; + } + + #endregion + + #endregion + } +} diff --git a/src/Shared/MSBuildNameIgnoreCaseComparer.cs b/src/Shared/MSBuildNameIgnoreCaseComparer.cs new file mode 100644 index 00000000000..d7ac19d60e6 --- /dev/null +++ b/src/Shared/MSBuildNameIgnoreCaseComparer.cs @@ -0,0 +1,455 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// A custom string comparer restricted to valid item/property names and with the +// ability to work on an indexed substring. +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// This is a custom string comparer that has three advantages over the regular + /// string comparer: + /// 1) It can generate hash codes and perform equivalence operations on parts of a string rather than a whole + /// 2) It uses "unsafe" pointers to maximize performance of those operations + /// 3) It takes advantage of limitations on MSBuild Property/Item names to cheaply do case insensitive comparison. + /// + [Serializable] + internal class MSBuildNameIgnoreCaseComparer : EqualityComparer, IEqualityComparer + { + /// + /// The default immutable comparer instance operating on the whole string that can be used instead of creating once each time + /// + private static MSBuildNameIgnoreCaseComparer s_immutableComparer = new MSBuildNameIgnoreCaseComparer(true /* immutable */); + + /// + /// The default mutable comparer instance that will ideally be shared by all users who need a mutable comparer. + /// + private static MSBuildNameIgnoreCaseComparer s_mutableComparer = new MSBuildNameIgnoreCaseComparer(false /* mutable */); + + /// + /// The processor architecture on which we are running, but default it will be x86 + /// + private static ushort s_runningProcessorArchitecture = NativeMethodsShared.PROCESSOR_ARCHITECTURE_INTEL; + + /// + /// Object used to lock the internal state s.t. we know that only one person is modifying + /// it at any one time. + /// This is necessary to prevent, e.g., someone from reading the comparer (through GetHashCode when setting + /// a property, for example) at the same time that someone else is writing to it. + /// + private Object _lockObject = new Object(); + + /// + /// String to be constrained. + /// If null, comparer is unconstrained. + /// If empty string, comparer is unconstrained and immutable. + /// + private string _constraintString; + + /// + /// Start of constraint + /// + private int _startIndex; + + /// + /// End of constraint + /// + private int _endIndex; + + /// + /// True if the comparer is immutable; false otherwise. + /// + private bool _immutable; + + /// + /// We need a static contructor to retrieve the running ProcessorArchitecture that way we can + /// Avoid using optimized code that will not run correctly on IA64 due to alignment issues + /// + static MSBuildNameIgnoreCaseComparer() + { + NativeMethodsShared.SYSTEM_INFO systemInfo = new NativeMethodsShared.SYSTEM_INFO(); + + NativeMethodsShared.GetSystemInfo(ref systemInfo); + + s_runningProcessorArchitecture = systemInfo.wProcessorArchitecture; + } + + /// + /// Constructor. If specified, comparer is immutable and operates on the whole string. + /// + private MSBuildNameIgnoreCaseComparer(bool immutable) + { + _immutable = immutable; + } + + /// + /// The default immutable comparer instance. + /// + internal static new MSBuildNameIgnoreCaseComparer Default + { + get { return s_immutableComparer; } + } + + /// + /// The default mutable comparer instance. + /// + internal static MSBuildNameIgnoreCaseComparer Mutable + { + get { return s_mutableComparer; } + } + + /// + /// Performs the "Equals" operation on two MSBuild property, item or metadata names + /// + public static bool Equals(string compareToString, string constrainedString, int start, int lengthToCompare) + { + if (Object.ReferenceEquals(compareToString, constrainedString)) + { + return true; + } + + if (compareToString == null || constrainedString == null) + { + return false; + } + + if (lengthToCompare != compareToString.Length) + { + return false; + } + +#if RETAIL + if ((runningProcessorArchitecture != NativeMethodsShared.PROCESSOR_ARCHITECTURE_IA64) && (runningProcessorArchitecture != NativeMethodsShared.PROCESSOR_ARCHITECTURE_ARM)) + { + // The use of unsafe here is quite a bit faster than the regular + // mechanism in the BCL. This is because we can make assumptions + // about the characters that are within the strings being compared + // i.e. they are valid MSBuild property, item and metadata names + unsafe + { + fixed (char* px = compareToString) + { + fixed (char* py = constrainedString) + { + for (int i = 0; i < compareToString.Length; i++) + { + int chx = (int)px[i]; + int chy = (int)py[i + start]; + chx = chx & 0x00DF; // Extract the uppercase character + chy = chy & 0x00DF; // Extract the uppercase character + + if (chx != chy) + { + return false; + } + } + } + } + } + } + else +#endif + { + return String.Compare(compareToString, 0, constrainedString, start, lengthToCompare, StringComparison.OrdinalIgnoreCase) == 0; + } +#if RETAIL + return true; +#endif + } + + /// + /// Given a set of constraints and a dictionary for which we are the comparer, return the value for the given key. + /// The key is also used as the string for the constraint. + /// + /// The value type of the dictionary being looked up + public T GetValueWithConstraints(IDictionary dictionary, string key, int startIndex, int endIndex) + where T : class + { + if (_immutable) + { + ErrorUtilities.ThrowInternalError("immutable"); + } + + ErrorUtilities.VerifyThrowInternalNull(dictionary, "dictionary"); + +#if DEBUG + // doing this rather than checking the strong type because otherwise, I would have to define T to be several other things + // (IKeyed, IValued, IImmutable, IEquatable), some of which are not compiled into Microsoft.Build.Utilities, which also + // uses the MSBuildNameIgnoreCaseComparer. + ErrorUtilities.VerifyThrow(dictionary.GetType().Name.Contains("PropertyDictionary"), "Needs to be PropertyDictionary or CopyOnWritePropertyDictionary"); +#endif + if (startIndex < 0) + { + ErrorUtilities.ThrowInternalError("Invalid start index '{0}' {1} {2}", key, startIndex, endIndex); + } + + if (key != null && (endIndex > key.Length || endIndex < startIndex)) + { + ErrorUtilities.ThrowInternalError("Invalid end index '{0}' {1} {2}", key, startIndex, endIndex); + } + + T returnValue; + lock (_lockObject) + { + _constraintString = key; + _startIndex = startIndex; + _endIndex = endIndex; + + try + { + returnValue = dictionary[key]; + } + finally + { + // Make sure we always reset the constraint + _constraintString = null; + _startIndex = 0; + _endIndex = 0; + } + } + + return returnValue; + } + + /// + /// Compare keyed operands + /// + public bool Equals(IKeyed x, IKeyed y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + + return Equals(x.Key, y.Key); + } + + /// + /// Performs the "Equals" operation on two MSBuild property, item or metadata names + /// + public override bool Equals(string x, string y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + + string compareToString; + string constrainedString; + int start; + int lengthToCompare; + + if (_immutable) + { + // by definition we don't have a constraint + if (Object.ReferenceEquals(x, y)) + { + return true; + } + + compareToString = x; + constrainedString = y; + start = 0; + lengthToCompare = y.Length; + } + else + { + lock (_lockObject) + { + if (_constraintString != null) + { + bool constraintInX = Object.ReferenceEquals(x, _constraintString); + bool constraintInY = Object.ReferenceEquals(y, _constraintString); + + if (!constraintInX && !constraintInY) + { + ErrorUtilities.ThrowInternalError("Expected to compare to constraint"); + } + + // Put constrained string in 'y', regular in 'x' + compareToString = constraintInX ? y : x; + constrainedString = constraintInY ? y : x; + + start = _startIndex; + lengthToCompare = _endIndex - _startIndex + 1; + } + else + { + if (Object.ReferenceEquals(x, y)) + { + return true; + } + + // Manually setup the "constraints" for the comparison + compareToString = x; + constrainedString = y; + start = 0; + lengthToCompare = y.Length; + } + } + } + + return Equals(compareToString, constrainedString, start, lengthToCompare); + } + + /// + /// Get case insensitive hashcode for key + /// + public int GetHashCode(IKeyed keyed) + { + if (keyed == null) + { + return 0; // per BCL convention + } + + return GetHashCode(keyed.Key); + } + + /// + /// Getting a case insensitive hash code for the msbuild property, item or metadata name + /// + public override int GetHashCode(string obj) + { + if (obj == null) + { + return 0; // per BCL convention + } + + int start = 0; + int length = obj.Length; + + if (!_immutable) + { + lock (_lockObject) + { + if (_constraintString != null && Object.ReferenceEquals(obj, _constraintString)) + { + start = _startIndex; + length = _endIndex - _startIndex + 1; + } + } + } +#if RETAIL + if ((runningProcessorArchitecture != NativeMethodsShared.PROCESSOR_ARCHITECTURE_IA64) && (runningProcessorArchitecture != NativeMethodsShared.PROCESSOR_ARCHITECTURE_ARM)) + { + unsafe + { + // This algorithm is based on the 32bit version from the CLR's string::GetHashCode + fixed (char* src = obj) + { + int hash1 = (5381 << 16) + 5381; + + int hash2 = hash1; + + char* src2 = src + start; + int* pint = (int*)src2; + + while (length > 0) + { + // We're only interested in uppercase ASCII characters + int val = pint[0] & 0x00DF00DF; + + // When we reach the end of the string, we need to + // stop short when gathering our data to compute the + // hash code - we are only interested in the data within + // the string, and not the null terminator etc. + if (length == 1) + { + val = val & 0xFFFF; + } + + hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ val; + if (length <= 2) + { + break; + } + + // Once again we're only interested in the uppercase ASCII characters + val = pint[1] & 0x00DF00DF; + if (length == 3) + { + val = val & 0xFFFF; + } + + hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ val; + pint += 2; + length -= 4; + } + + return hash1 + (hash2 * 1566083941); + } + } + } + else +#endif + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Substring(start, length)); + } + } + + /// + /// Set the constraints in the comparer explicitly -- should ONLY be used for unit tests + /// + internal void SetConstraintsForUnitTestingOnly(string constraintString, int startIndex, int endIndex) + { + if (_immutable) + { + ErrorUtilities.ThrowInternalError("immutable"); + } + + if (startIndex < 0) + { + ErrorUtilities.ThrowInternalError("Invalid start index '{0}' {1} {2}", constraintString, startIndex, endIndex); + } + + if (constraintString != null && (endIndex > constraintString.Length || endIndex < startIndex)) + { + ErrorUtilities.ThrowInternalError("Invalid end index '{0}' {1} {2}", constraintString, startIndex, endIndex); + } + + lock (_lockObject) + { + _constraintString = constraintString; + _startIndex = startIndex; + _endIndex = endIndex; + } + } + + /// + /// Companion to SetConstraintsForUnitTestingOnly -- makes the comparer unconstrained again. + /// + internal void RemoveConstraintsForUnitTestingOnly() + { + if (_immutable) + { + ErrorUtilities.ThrowInternalError("immutable"); + } + + lock (_lockObject) + { + _constraintString = null; + _startIndex = 0; + _endIndex = 0; + } + } + } +} diff --git a/src/Shared/MetadataConversionUtilities.cs b/src/Shared/MetadataConversionUtilities.cs new file mode 100644 index 00000000000..0f6ce114778 --- /dev/null +++ b/src/Shared/MetadataConversionUtilities.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains only static methods, which are useful throughout many + /// of the XMake classes and don't really belong in any specific class. + /// + internal static class MetadataConversionUtilities + { + /// + /// Convert a task item metadata to bool. Throw an exception if the string is badly formed and can't + /// be converted. + /// + /// If the metadata is not found, then set metadataFound to false and then return false. + /// + /// The item that contains the metadata. + /// The name of the metadata. + /// Receives true if the metadata was found, false otherwise. + /// The resulting boolean value. + internal static bool TryConvertItemMetadataToBool + ( + ITaskItem item, + string itemMetadataName, + out bool metadataFound + ) + { + string metadataValue = item.GetMetadata(itemMetadataName); + if (metadataValue == null || metadataValue.Length == 0) + { + metadataFound = false; + return false; + } + metadataFound = true; + + try + { + return Microsoft.Build.Shared.ConversionUtilities.ConvertStringToBool(metadataValue); + } + catch (System.ArgumentException e) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("General.InvalidAttributeMetadata", item.ItemSpec, itemMetadataName, metadataValue, "bool"), e); + } + } + + /// + /// Convert a task item metadata to bool. Throw an exception if the string is badly formed and can't + /// be converted. + /// + /// If the attibute is not found, then return false. + /// + /// The item that contains the metadata. + /// The name of the metadata. + /// The resulting boolean value. + internal static bool TryConvertItemMetadataToBool + ( + ITaskItem item, + string itemMetadataName + ) + { + bool metadataFound = false; + return TryConvertItemMetadataToBool(item, itemMetadataName, out metadataFound); + } + } +} diff --git a/src/Shared/Modifiers.cs b/src/Shared/Modifiers.cs new file mode 100644 index 00000000000..43f51106398 --- /dev/null +++ b/src/Shared/Modifiers.cs @@ -0,0 +1,616 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Security; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Text; +using System.Threading; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for file IO. + /// + /// + /// Partial class in order to reduce the amount of sharing into different assemblies + /// + static internal partial class FileUtilities + { + /// + /// Encapsulates the definitions of the item-spec modifiers a.k.a. reserved item metadata. + /// + static internal class ItemSpecModifiers + { +#if DEBUG + /// + /// Whether to dump when a modifier is in the "wrong" (slow) casing + /// + private static readonly bool s_traceModifierCasing = (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDTRACEMODIFIERCASING"))); +#endif + + // NOTE: If you add an item here that starts with a new letter, you need to update the case + // statements in IsItemSpecModifier and IsDerivableItemSpecModifier. + internal const string FullPath = "FullPath"; + internal const string RootDir = "RootDir"; + internal const string Filename = "Filename"; + internal const string Extension = "Extension"; + internal const string RelativeDir = "RelativeDir"; + internal const string Directory = "Directory"; + internal const string RecursiveDir = "RecursiveDir"; + internal const string Identity = "Identity"; + internal const string ModifiedTime = "ModifiedTime"; + internal const string CreatedTime = "CreatedTime"; + internal const string AccessedTime = "AccessedTime"; + internal const string DefiningProjectFullPath = "DefiningProjectFullPath"; + internal const string DefiningProjectDirectory = "DefiningProjectDirectory"; + internal const string DefiningProjectName = "DefiningProjectName"; + internal const string DefiningProjectExtension = "DefiningProjectExtension"; + + // These are all the well-known attributes. + internal static readonly string[] All = + { + FullPath, + RootDir, + Filename, + Extension, + RelativeDir, + Directory, + RecursiveDir, // <-- Not derivable. + Identity, + ModifiedTime, + CreatedTime, + AccessedTime, + DefiningProjectFullPath, + DefiningProjectDirectory, + DefiningProjectName, + DefiningProjectExtension + }; + + private static HashSet s_tableOfItemSpecModifiers = new HashSet(All, StringComparer.OrdinalIgnoreCase); + + /// + /// Indicates if the given name is reserved for an item-spec modifier. + /// + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Performance")] + internal static bool IsItemSpecModifier(string name) + { + if (name == null) + { + return false; + } + + + /* + * What follows requires some explanation. + * + * This function is called many times and slowness here will be amplified + * in critical performance scenarios. + * + * The following switch statement attempts to identify item spec modifiers that + * have the exact case that our constants in ItemSpecModifiers have. This is the + * 99% case. + * + * Further, the switch statement can identify certain cases in which there is + * definitely no chance that 'name' is an item spec modifier. For example, a + * 7 letter 'name' that doesn't start with 'r' or 'R' can't be RootDir and + * therefore is not an item spec modifier. + * + */ + switch (name.Length) + { + case 7: // RootDir + switch (name[0]) + { + default: + return false; + case 'R': // RootDir + if (name == FileUtilities.ItemSpecModifiers.RootDir) + { + return true; + } + break; + case 'r': + break; + } + break; + case 8: // FullPath, Filename, Identity + + switch (name[0]) + { + default: + return false; + case 'F': // Filename, FullPath + if (name == FileUtilities.ItemSpecModifiers.FullPath) + { + return true; + } + if (name == FileUtilities.ItemSpecModifiers.Filename) + { + return true; + } + break; + case 'f': + break; + case 'I': // Identity + if (name == FileUtilities.ItemSpecModifiers.Identity) + { + return true; + } + break; + case 'i': + break; + } + break; + case 9: // Extension, Directory + switch (name[0]) + { + default: + return false; + case 'D': // Directory + if (name == FileUtilities.ItemSpecModifiers.Directory) + { + return true; + } + break; + case 'd': + break; + case 'E': // Extension + if (name == FileUtilities.ItemSpecModifiers.Extension) + { + return true; + } + break; + case 'e': + break; + } + break; + case 11: // RelativeDir, CreatedTime + switch (name[0]) + { + default: + return false; + case 'C': // CreatedTime + if (name == FileUtilities.ItemSpecModifiers.CreatedTime) + { + return true; + } + break; + case 'c': + break; + case 'R': // RelativeDir + if (name == FileUtilities.ItemSpecModifiers.RelativeDir) + { + return true; + } + break; + case 'r': + break; + } + break; + case 12: // RecursiveDir, ModifiedTime, AccessedTime + + switch (name[0]) + { + default: + return false; + case 'A': // AccessedTime + if (name == FileUtilities.ItemSpecModifiers.AccessedTime) + { + return true; + } + break; + case 'a': + break; + case 'M': // ModifiedTime + if (name == FileUtilities.ItemSpecModifiers.ModifiedTime) + { + return true; + } + break; + case 'm': + break; + case 'R': // RecursiveDir + if (name == FileUtilities.ItemSpecModifiers.RecursiveDir) + { + return true; + } + break; + case 'r': + break; + } + break; + case 19: + case 23: + case 24: + return IsDefiningProjectModifier(name); + default: + // Not the right length for a match. + return false; + } + + // Could still be a case-insensitive match. + bool result = s_tableOfItemSpecModifiers.Contains(name); + +#if DEBUG + if (result && s_traceModifierCasing) + { + Console.WriteLine("'{0}' is a non-standard casing. Replace the use with the standard casing like 'RecursiveDir' or 'FullPath' for a small performance improvement.", name); + } +#endif + + return result; + } + + /// + /// Indicates if the given name is reserved for one of the specific subset of itemspec + /// modifiers to do with the defining project of the item. + /// + internal static bool IsDefiningProjectModifier(string name) + { + switch (name.Length) + { + case 19: // DefiningProjectName + if (name == FileUtilities.ItemSpecModifiers.DefiningProjectName) + { + return true; + } + break; + case 23: // DefiningProjectFullPath + if (name == FileUtilities.ItemSpecModifiers.DefiningProjectFullPath) + { + return true; + } + break; + case 24: // DefiningProjectDirectory, DefiningProjectExtension + + switch (name[15]) + { + default: + return false; + case 'D': // DefiningProjectDirectory + if (name == FileUtilities.ItemSpecModifiers.DefiningProjectDirectory) + { + return true; + } + break; + case 'd': + break; + case 'E': // DefiningProjectExtension + if (name == FileUtilities.ItemSpecModifiers.DefiningProjectExtension) + { + return true; + } + break; + case 'e': + break; + } + break; + default: + return false; + } + + // Could still be a case-insensitive match. + bool result = s_tableOfItemSpecModifiers.Contains(name); + +#if DEBUG + if (result && s_traceModifierCasing) + { + Console.WriteLine("'{0}' is a non-standard casing. Replace the use with the standard casing like 'RecursiveDir' or 'FullPath' for a small performance improvement.", name); + } +#endif + + return result; + } + + /// + /// Indicates if the given name is reserved for a derivable item-spec modifier. + /// Derivable means it can be computed given a file name. + /// + /// Name to check. + /// true, if name of a derivable modifier + internal static bool IsDerivableItemSpecModifier(string name) + { + bool isItemSpecModifier = IsItemSpecModifier(name); + + if (isItemSpecModifier) + { + if (name.Length == 12) + { + if (name[0] == 'R' || name[0] == 'r') + { + // The only 12 letter ItemSpecModifier that starts with 'R' is 'RecursiveDir' + return false; + } + } + } + + return isItemSpecModifier; + } + + /// + /// Performs path manipulations on the given item-spec as directed. + /// Does not cache the result. + /// + internal static string GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier) + { + string dummy = null; + return GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, modifier, ref dummy); + } + + /// + /// Performs path manipulations on the given item-spec as directed. + /// + /// Supported modifiers: + /// %(FullPath) = full path of item + /// %(RootDir) = root directory of item + /// %(Filename) = item filename without extension + /// %(Extension) = item filename extension + /// %(RelativeDir) = item directory as given in item-spec + /// %(Directory) = full path of item directory relative to root + /// %(RecursiveDir) = portion of item path that matched a recursive wildcard + /// %(Identity) = item-spec as given + /// %(ModifiedTime) = last write time of item + /// %(CreatedTime) = creation time of item + /// %(AccessedTime) = last access time of item + /// + /// NOTES: + /// 1) This method always returns an empty string for the %(RecursiveDir) modifier because it does not have enough + /// information to compute it -- only the BuildItem class can compute this modifier. + /// 2) All but the file time modifiers could be cached, but it's not worth the space. Only full path is cached, as the others are just string manipulations. + /// + /// + /// Methods of the Path class "normalize" slashes and periods. For example: + /// 1) successive slashes are combined into 1 slash + /// 2) trailing periods are discarded + /// 3) forward slashes are changed to back-slashes + /// + /// As a result, we cannot rely on any file-spec that has passed through a Path method to remain the same. We will + /// therefore not bother preserving slashes and periods when file-specs are transformed. + /// + /// Never returns null. + /// + /// The root directory for relative item-specs. When called on the Engine thread, this is the project directory. When called as part of building a task, it is null, indicating that the current directory should be used. + /// The item-spec to modify. + /// The path to the project that defined this item (may be null). + /// The modifier to apply to the item-spec. + /// Full path if any was previously computed, to cache. + /// The modified item-spec (can be empty string, but will never be null). + /// Thrown when the item-spec is not a path. + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Pre-existing")] + internal static string GetItemSpecModifier(string currentDirectory, string itemSpec, string definingProjectEscaped, string modifier, ref string fullPath) + { + ErrorUtilities.VerifyThrow(itemSpec != null, "Need item-spec to modify."); + ErrorUtilities.VerifyThrow(modifier != null, "Need modifier to apply to item-spec."); + + string modifiedItemSpec = null; + + try + { + if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.FullPath, StringComparison.OrdinalIgnoreCase) == 0) + { + if (fullPath != null) + { + return fullPath; + } + + if (currentDirectory == null) + { + currentDirectory = String.Empty; + } + + modifiedItemSpec = GetFullPath(itemSpec, currentDirectory); + fullPath = modifiedItemSpec; + + ThrowForUrl(modifiedItemSpec, itemSpec, currentDirectory); + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.RootDir, StringComparison.OrdinalIgnoreCase) == 0) + { + GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, ItemSpecModifiers.FullPath, ref fullPath); + + modifiedItemSpec = Path.GetPathRoot(fullPath); + + if (!EndsWithSlash(modifiedItemSpec)) + { + ErrorUtilities.VerifyThrow(FileUtilitiesRegex.UNCPattern.IsMatch(modifiedItemSpec), + "Only UNC shares should be missing trailing slashes."); + + // restore/append trailing slash if Path.GetPathRoot() has either removed it, or failed to add it + // (this happens with UNC shares) + modifiedItemSpec += Path.DirectorySeparatorChar; + } + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Filename, StringComparison.OrdinalIgnoreCase) == 0) + { + // if the item-spec is a root directory, it can have no filename + if (Path.GetDirectoryName(itemSpec) == null) + { + // NOTE: this is to prevent Path.GetFileNameWithoutExtension() from treating server and share elements + // in a UNC file-spec as filenames e.g. \\server, \\server\share + modifiedItemSpec = String.Empty; + } + else + { + modifiedItemSpec = Path.GetFileNameWithoutExtension(itemSpec); + } + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Extension, StringComparison.OrdinalIgnoreCase) == 0) + { + // if the item-spec is a root directory, it can have no extension + if (Path.GetDirectoryName(itemSpec) == null) + { + // NOTE: this is to prevent Path.GetExtension() from treating server and share elements in a UNC + // file-spec as filenames e.g. \\server.ext, \\server\share.ext + modifiedItemSpec = String.Empty; + } + else + { + modifiedItemSpec = Path.GetExtension(itemSpec); + } + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.RelativeDir, StringComparison.OrdinalIgnoreCase) == 0) + { + modifiedItemSpec = GetDirectory(itemSpec); + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Directory, StringComparison.OrdinalIgnoreCase) == 0) + { + GetItemSpecModifier(currentDirectory, itemSpec, definingProjectEscaped, ItemSpecModifiers.FullPath, ref fullPath); + + modifiedItemSpec = GetDirectory(fullPath); + Match root = FileUtilitiesRegex.DrivePattern.Match(modifiedItemSpec); + + if (!root.Success) + { + root = FileUtilitiesRegex.UNCPattern.Match(modifiedItemSpec); + } + + if (root.Success) + { + ErrorUtilities.VerifyThrow((modifiedItemSpec.Length > root.Length) && IsSlash(modifiedItemSpec[root.Length]), + "Root directory must have a trailing slash."); + + modifiedItemSpec = modifiedItemSpec.Substring(root.Length + 1); + } + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.RecursiveDir, StringComparison.OrdinalIgnoreCase) == 0) + { + // only the BuildItem class can compute this modifier -- so leave empty + modifiedItemSpec = String.Empty; + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.Identity, StringComparison.OrdinalIgnoreCase) == 0) + { + modifiedItemSpec = itemSpec; + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.ModifiedTime, StringComparison.OrdinalIgnoreCase) == 0) + { + // About to go out to the filesystem. This means data is leaving the engine, so need + // to unescape first. + string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); + + FileInfo info = FileUtilities.GetFileInfoNoThrow(unescapedItemSpec); + + if (info != null) + { + modifiedItemSpec = info.LastWriteTime.ToString(FileTimeFormat, null); + } + else + { + // File does not exist, or path is a directory + modifiedItemSpec = String.Empty; + } + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.CreatedTime, StringComparison.OrdinalIgnoreCase) == 0) + { + // About to go out to the filesystem. This means data is leaving the engine, so need + // to unescape first. + string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); + + if (File.Exists(unescapedItemSpec)) + { + modifiedItemSpec = File.GetCreationTime(unescapedItemSpec).ToString(FileTimeFormat, null); + } + else + { + // File does not exist, or path is a directory + modifiedItemSpec = String.Empty; + } + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.AccessedTime, StringComparison.OrdinalIgnoreCase) == 0) + { + // About to go out to the filesystem. This means data is leaving the engine, so need + // to unescape first. + string unescapedItemSpec = EscapingUtilities.UnescapeAll(itemSpec); + + if (File.Exists(unescapedItemSpec)) + { + modifiedItemSpec = File.GetLastAccessTime(unescapedItemSpec).ToString(FileTimeFormat, null); + } + else + { + // File does not exist, or path is a directory + modifiedItemSpec = String.Empty; + } + } + else if (IsDefiningProjectModifier(modifier)) + { + if (String.IsNullOrEmpty(definingProjectEscaped)) + { + // We have nothing to work with, but that's sometimes OK -- so just return String.Empty + modifiedItemSpec = String.Empty; + } + else + { + if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectDirectory, StringComparison.OrdinalIgnoreCase) == 0) + { + // ItemSpecModifiers.Directory does not contain the root directory + modifiedItemSpec = Path.Combine + ( + GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, ItemSpecModifiers.RootDir), + GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, ItemSpecModifiers.Directory) + ); + } + else + { + string additionalModifier = null; + + if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectFullPath, StringComparison.OrdinalIgnoreCase) == 0) + { + additionalModifier = ItemSpecModifiers.FullPath; + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectName, StringComparison.OrdinalIgnoreCase) == 0) + { + additionalModifier = ItemSpecModifiers.Filename; + } + else if (String.Compare(modifier, FileUtilities.ItemSpecModifiers.DefiningProjectExtension, StringComparison.OrdinalIgnoreCase) == 0) + { + additionalModifier = ItemSpecModifiers.Extension; + } + else + { + ErrorUtilities.ThrowInternalError("\"{0}\" is not a valid item-spec modifier.", modifier); + } + + modifiedItemSpec = GetItemSpecModifier(currentDirectory, definingProjectEscaped, null, additionalModifier); + } + } + } + else + { + ErrorUtilities.ThrowInternalError("\"{0}\" is not a valid item-spec modifier.", modifier); + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + ErrorUtilities.VerifyThrowInvalidOperation(false, "Shared.InvalidFilespecForTransform", modifier, itemSpec, e.Message); + } + + return modifiedItemSpec; + } + + /// + /// Temporary check for something like http://foo which will end up like c:\foo\bar\http://foo + /// We should either have no colon, or exactly one colon. + /// UNDONE: This is a minimal safe change for Dev10. The correct fix should be to make GetFullPath/NormalizePath throw for this. + /// + private static void ThrowForUrl(string fullPath, string itemSpec, string currentDirectory) + { + if (fullPath.IndexOf(':') != fullPath.LastIndexOf(':')) + { + // Cause a better error to appear + fullPath = Path.GetFullPath(Path.Combine(currentDirectory, itemSpec)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Shared/NGen.cs b/src/Shared/NGen.cs new file mode 100644 index 00000000000..2a82d685c13 --- /dev/null +++ b/src/Shared/NGen.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A hack to prevent certain cases of Jitting in our NGen'd assemblies. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Shared +{ + /// + /// To avoid CA908 warnings (types that in ngen images that will JIT) + /// wrap each problematic value type in the collection in + /// one of these objects. + /// + /// + /// This trick is based on advice from + /// http://sharepoint/sites/codeanalysis/Wiki%20Pages/Rule%20-%20Avoid%20Types%20That%20Require%20JIT%20Compilation%20In%20Precompiled%20Assemblies.aspx. + /// It works because although this is a value type, it is not defined in mscorlib. + /// + /// Wrapped type + internal struct NGen where T : struct + { + /// + /// Wrapped value + /// + private T _value; + + /// + /// Constructor + /// + public NGen(T value) + { + _value = value; + } + + /// + /// Exposes the value + /// + public static implicit operator T(NGen value) + { + return value._value; + } + + /// + /// Consumes the value + /// + public static implicit operator NGen(T value) + { + return new NGen(value); + } + } +} \ No newline at end of file diff --git a/src/Shared/NativeMethodsShared.cs b/src/Shared/NativeMethodsShared.cs new file mode 100644 index 00000000000..e5abf312a30 --- /dev/null +++ b/src/Shared/NativeMethodsShared.cs @@ -0,0 +1,930 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Build.Shared +{ + /// + /// Interop methods. + /// + internal static class NativeMethodsShared + { + #region Constants + + internal const uint ERROR_INSUFFICIENT_BUFFER = 0x8007007A; + internal const uint STARTUP_LOADER_SAFEMODE = 0x10; + internal const uint S_OK = 0x0; + internal const uint S_FALSE = 0x1; + internal const uint ERROR_ACCESS_DENIED = 0x5; + internal const uint ERROR_FILE_NOT_FOUND = 0x80070002; + internal const uint FUSION_E_PRIVATE_ASM_DISALLOWED = 0x80131044; // Tried to find unsigned assembly in GAC + internal const uint RUNTIME_INFO_DONT_SHOW_ERROR_DIALOG = 0x40; + internal const uint FILE_TYPE_CHAR = 0x0002; + internal const Int32 STD_OUTPUT_HANDLE = -11; + internal const uint RPC_S_CALLPENDING = 0x80010115; + internal const uint E_ABORT = (uint)0x80004004; + + internal const int FILE_ATTRIBUTE_READONLY = 0x00000001; + internal const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + internal const int FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400; + + private const string kernel32Dll = "kernel32.dll"; + private const string mscoreeDLL = "mscoree.dll"; + + internal static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); + + internal static IntPtr NullIntPtr = new IntPtr(0); + + // As defined in winnt.h: + internal const ushort PROCESSOR_ARCHITECTURE_INTEL = 0; + internal const ushort PROCESSOR_ARCHITECTURE_ARM = 5; + internal const ushort PROCESSOR_ARCHITECTURE_IA64 = 6; + internal const ushort PROCESSOR_ARCHITECTURE_AMD64 = 9; + + internal const uint INFINITE = 0xFFFFFFFF; + internal const uint WAIT_ABANDONED_0 = 0x00000080; + internal const uint WAIT_OBJECT_0 = 0x00000000; + internal const uint WAIT_TIMEOUT = 0x00000102; + + #endregion + + #region Enums + + private enum PROCESSINFOCLASS : int + { + ProcessBasicInformation = 0, + ProcessQuotaLimits, + ProcessIoCounters, + ProcessVmCounters, + ProcessTimes, + ProcessBasePriority, + ProcessRaisePriority, + ProcessDebugPort, + ProcessExceptionPort, + ProcessAccessToken, + ProcessLdtInformation, + ProcessLdtSize, + ProcessDefaultHardErrorMode, + ProcessIoPortHandlers, // Note: this is kernel mode only + ProcessPooledUsageAndLimits, + ProcessWorkingSetWatch, + ProcessUserModeIOPL, + ProcessEnableAlignmentFaultFixup, + ProcessPriorityClass, + ProcessWx86Information, + ProcessHandleCount, + ProcessAffinityMask, + ProcessPriorityBoost, + MaxProcessInfoClass + }; + + private enum eDesiredAccess : int + { + DELETE = 0x00010000, + READ_CONTROL = 0x00020000, + WRITE_DAC = 0x00040000, + WRITE_OWNER = 0x00080000, + SYNCHRONIZE = 0x00100000, + STANDARD_RIGHTS_ALL = 0x001F0000, + + PROCESS_TERMINATE = 0x0001, + PROCESS_CREATE_THREAD = 0x0002, + PROCESS_SET_SESSIONID = 0x0004, + PROCESS_VM_OPERATION = 0x0008, + PROCESS_VM_READ = 0x0010, + PROCESS_VM_WRITE = 0x0020, + PROCESS_DUP_HANDLE = 0x0040, + PROCESS_CREATE_PROCESS = 0x0080, + PROCESS_SET_QUOTA = 0x0100, + PROCESS_SET_INFORMATION = 0x0200, + PROCESS_QUERY_INFORMATION = 0x0400, + PROCESS_ALL_ACCESS = SYNCHRONIZE | 0xFFF + } + + /// + /// Flags for CoWaitForMultipleHandles + /// + [Flags] + public enum COWAIT_FLAGS : int + { + /// + /// Exit when a handle is signaled. + /// + COWAIT_NONE = 0, + + /// + /// Exit when all handles are signaled AND a message is received. + /// + COWAIT_WAITALL = 0x00000001, + + /// + /// Exit when an RPC call is serviced. + /// + COWAIT_ALERTABLE = 0x00000002 + } + + #endregion + + #region Structs + + /// + /// Structure that contain information about the system on which we are running + /// + [StructLayout(LayoutKind.Sequential)] + internal struct SYSTEM_INFO + { + // This is a union of a DWORD and a struct containing 2 WORDs. + internal ushort wProcessorArchitecture; + internal ushort wReserved; + + internal uint dwPageSize; + internal IntPtr lpMinimumApplicationAddress; + internal IntPtr lpMaximumApplicationAddress; + internal IntPtr dwActiveProcessorMask; + internal uint dwNumberOfProcessors; + internal uint dwProcessorType; + internal uint dwAllocationGranularity; + internal ushort wProcessorLevel; + internal ushort wProcessorRevision; + } + + + /// + /// Wrap the intptr returned by OpenProcess in a safe handle. + /// + internal class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvalid + { + // Create a SafeHandle, informing the base class + // that this SafeHandle instance "owns" the handle, + // and therefore SafeHandle should call + // our ReleaseHandle method when the SafeHandle + // is no longer in use + private SafeProcessHandle() : base(true) + { + } + + protected override bool ReleaseHandle() + { + return CloseHandle(handle); + } + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("KERNEL32.DLL")] + private static extern bool CloseHandle(IntPtr hObject); + } + + /// + /// Contains information about the current state of both physical and virtual memory, including extended memory + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class MemoryStatus + { + /// + /// Initializes a new instance of the class. + /// + public MemoryStatus() + { + _length = (uint)Marshal.SizeOf(typeof(NativeMethodsShared.MemoryStatus)); + } + + /// + /// Size of the structure, in bytes. You must set this member before calling GlobalMemoryStatusEx. + /// + private uint _length; + + /// + /// Number between 0 and 100 that specifies the approximate percentage of physical + /// memory that is in use (0 indicates no memory use and 100 indicates full memory use). + /// + public uint MemoryLoad; + + /// + /// Total size of physical memory, in bytes. + /// + public ulong TotalPhysical; + + /// + /// Size of physical memory available, in bytes. + /// + public ulong AvailablePhysical; + + /// + /// Size of the committed memory limit, in bytes. This is physical memory plus the + /// size of the page file, minus a small overhead. + /// + public ulong TotalPageFile; + + /// + /// Size of available memory to commit, in bytes. The limit is ullTotalPageFile. + /// + public ulong AvailablePageFile; + + /// + /// Total size of the user mode portion of the virtual address space of the calling process, in bytes. + /// + public ulong TotalVirtual; + + /// + /// Size of unreserved and uncommitted memory in the user mode portion of the virtual + /// address space of the calling process, in bytes. + /// + public ulong AvailableVirtual; + + /// + /// Size of unreserved and uncommitted memory in the extended portion of the virtual + /// address space of the calling process, in bytes. + /// + public ulong AvailableExtendedVirtual; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_BASIC_INFORMATION + { + public IntPtr ExitStatus; + public IntPtr PebBaseAddress; + public IntPtr AffinityMask; + public IntPtr BasePriority; + public IntPtr UniqueProcessId; + public IntPtr InheritedFromUniqueProcessId; + + public int Size + { + get { return (6 * IntPtr.Size); } + } + }; + + /// + /// Contains information about a file or directory; used by GetFileAttributesEx. + /// + [StructLayout(LayoutKind.Sequential)] + public struct WIN32_FILE_ATTRIBUTE_DATA + { + internal int fileAttributes; + internal uint ftCreationTimeLow; + internal uint ftCreationTimeHigh; + internal uint ftLastAccessTimeLow; + internal uint ftLastAccessTimeHigh; + internal uint ftLastWriteTimeLow; + internal uint ftLastWriteTimeHigh; + internal uint fileSizeHigh; + internal uint fileSizeLow; + } + + /// + /// Contains the security descriptor for an object and specifies whether + /// the handle retrieved by specifying this structure is inheritable. + /// + [StructLayout(LayoutKind.Sequential)] + internal class SecurityAttributes + { + public SecurityAttributes() + { + _nLength = (uint)Marshal.SizeOf(typeof(NativeMethodsShared.SecurityAttributes)); + } + + private uint _nLength; + + public IntPtr lpSecurityDescriptor; + + public bool bInheritHandle; + } + + #endregion + + #region Member data + + /// + /// Default buffer size to use when dealing with the Windows API. + /// + /// + /// This member is intentionally not a constant because we want to allow + /// unit tests to change it. + /// + internal static int MAX_PATH = 260; + + #endregion + + #region Set Error Mode (copied from BCL) + + private static readonly Version s_threadErrorModeMinOsVersion = new Version(6, 1, 0x1db0); + + internal static int SetErrorMode(int newMode) + { + if (Environment.OSVersion.Version >= s_threadErrorModeMinOsVersion) + { + int num; + SetErrorMode_Win7AndNewer(newMode, out num); + return num; + } + return SetErrorMode_VistaAndOlder(newMode); + } + + [DllImport("kernel32.dll", EntryPoint = "SetThreadErrorMode", SetLastError = true)] + private static extern bool SetErrorMode_Win7AndNewer(int newMode, out int oldMode); + + [DllImport("kernel32.dll", EntryPoint = "SetErrorMode", ExactSpelling = true)] + private static extern int SetErrorMode_VistaAndOlder(int newMode); + + #endregion + + #region Wrapper methods + + /// + /// Really truly non pumping wait. + /// Raw IntPtrs have to be used, because the marshaller does not support arrays of SafeHandle, only + /// single SafeHandles. + /// + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern Int32 WaitForMultipleObjects(uint handle, IntPtr[] handles, bool waitAll, uint milliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern void GetSystemInfo(ref SYSTEM_INFO lpSystemInfo); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo); + + /// + /// Get the last write time of the fullpath to a directory. If the pointed path is not a directory, or + /// if the directory does not exist, then false is returned and fileModifiedTimeUtc is set DateTime.MinValue. + /// + /// Full path to the file in the filesystem + /// The UTC last write time for the directory + internal static bool GetLastWriteDirectoryUtcTime(string fullPath, out DateTime fileModifiedTimeUtc) + { + // This code was copied from the reference mananger, if there is a bug fix in that code, see if the same fix should also be made + // there + + fileModifiedTimeUtc = DateTime.MinValue; + WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); + bool success = false; + + success = GetFileAttributesEx(fullPath, 0, ref data); + if (success) + { + if ((data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + long dt = ((long)(data.ftLastWriteTimeHigh) << 32) | ((long)data.ftLastWriteTimeLow); + fileModifiedTimeUtc = DateTime.FromFileTimeUtc(dt); + } + else + { + // Path does not point to a directory + success = false; + } + } + + return success; + } + + /// + /// Takes the path and returns the short path + /// + internal static string GetShortFilePath(string path) + { + if (path != null) + { + int length = GetShortPathName(path, null, 0); + int errorCode = Marshal.GetLastWin32Error(); + + if (length > 0) + { + System.Text.StringBuilder fullPathBuffer = new System.Text.StringBuilder(length); + length = GetShortPathName(path, fullPathBuffer, length); + errorCode = Marshal.GetLastWin32Error(); + + if (length > 0) + { + string fullPath = fullPathBuffer.ToString(); + path = fullPath; + } + } + + if (length == 0 && errorCode != 0) + { + ThrowExceptionForErrorCode(errorCode); + } + } + + return path; + } + + /// + /// Takes the path and returns a full path + /// + /// + /// + internal static string GetLongFilePath(string path) + { + if (path != null) + { + int length = GetLongPathName(path, null, 0); + int errorCode = Marshal.GetLastWin32Error(); + + if (length > 0) + { + System.Text.StringBuilder fullPathBuffer = new System.Text.StringBuilder(length); + length = GetLongPathName(path, fullPathBuffer, length); + errorCode = Marshal.GetLastWin32Error(); + + if (length > 0) + { + string fullPath = fullPathBuffer.ToString(); + path = fullPath; + } + } + + if (length == 0 && errorCode != 0) + { + ThrowExceptionForErrorCode(errorCode); + } + } + + return path; + } + + /// + /// Retrieves the current global memory status. + /// + internal static MemoryStatus GetMemoryStatus() + { + MemoryStatus status = new MemoryStatus(); + bool returnValue = NativeMethodsShared.GlobalMemoryStatusEx(status); + if (!returnValue) + { + return null; + } + + return status; + } + + /// + /// Get the last write time of the fullpath to the file. + /// If the file does not exist, then DateTime.MinValue is returned + /// + /// Full path to the file in the filesystem + /// + internal static DateTime GetLastWriteFileUtcTime(string fullPath) + { + DateTime fileModifiedTime = DateTime.MinValue; + WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); + bool success = false; + + success = NativeMethodsShared.GetFileAttributesEx(fullPath, 0, ref data); + if (success) + { + long dt = ((long)(data.ftLastWriteTimeHigh) << 32) | ((long)data.ftLastWriteTimeLow); + fileModifiedTime = DateTime.FromFileTimeUtc(dt); + } + + return fileModifiedTime; + } + + /// + /// Did the HRESULT succeed + /// + public static bool HResultSucceeded(int hr) + { + return (hr >= 0); + } + + /// + /// Did the HRESULT Fail + /// + public static bool HResultFailed(int hr) + { + return (hr < 0); + } + + /// + /// Given an error code, converts it to an HRESULT and throws the appropriate exception. + /// + /// + public static void ThrowExceptionForErrorCode(int errorCode) + { + // See ndp\clr\src\bcl\system\io\__error.cs for this code as it appears in the CLR. + + // Something really bad went wrong witht the call + // translate the error into an exception + + // Convert the errorcode into an HRESULT (See MakeHRFromErrorCode in Win32Native.cs in + // ndp\clr\src\bcl\microsoft\win32) + errorCode = unchecked(((int)0x80070000) | errorCode); + + // Throw an exception as best we can + Marshal.ThrowExceptionForHR(errorCode); + } + + /// + /// Looks for the given file in the system path i.e. all locations in + /// the %PATH% environment variable. + /// + /// + /// The location of the file, or null if file not found. + internal static string FindOnPath(string filename) + { + StringBuilder pathBuilder = new StringBuilder(MAX_PATH + 1); + string pathToFile = null; + + // we may need to make two attempts because there's a small chance + // the buffer may not be sized correctly the first time + for (int i = 0; i < 2; i++) + { + uint result = SearchPath + ( + null /* search the system path */, + filename /* look for this file */, + null /* don't add an extra extension to the filename when searching */, + pathBuilder.Capacity /* size of buffer */, + pathBuilder /* buffer to write path into */, + null /* don't want pointer to filename in the return path */ + ); + + // if the buffer is not big enough + if (result > pathBuilder.Capacity) + { + ErrorUtilities.VerifyThrow(i == 0, "We should not have to resize the buffer twice."); + + // resize the buffer and try again + pathBuilder.Capacity = (int)result; + } + else if (result > 0) + { + // file was found, so don't make another attempt + pathToFile = pathBuilder.ToString(); + break; + } + else + { + // file was not found, so quit + break; + } + } + + return pathToFile; + } + + /// + /// Kills the specified process by id and all of its children recursively. + /// + internal static void KillTree(int processIdToKill) + { + // Note that GetProcessById does *NOT* internally hold on to the process handle. + // Only when you create the process using the Process object + // does the Process object retain the original handle. + + Process thisProcess = null; + try + { + thisProcess = Process.GetProcessById(processIdToKill); + } + catch (ArgumentException) + { + // The process has already died for some reason. So shrug and assume that any child processes + // have all also either died or are in the process of doing so. + return; + } + + try + { + DateTime myStartTime = thisProcess.StartTime; + + // Grab the process handle. We want to keep this open for the duration of the function so that + // it cannot be reused while we are running. + SafeProcessHandle hProcess = OpenProcess(eDesiredAccess.PROCESS_QUERY_INFORMATION, false, processIdToKill); + if (hProcess.IsInvalid) + { + return; + } + + try + { + try + { + // Kill this process, so that no further children can be created. + thisProcess.Kill(); + } + catch (Win32Exception e) + { + // Access denied is potentially expected -- it happens when the process that + // we're attempting to kill is already dead. So just ignore in that case. + if (e.NativeErrorCode != ERROR_ACCESS_DENIED) + { + throw; + } + } + + // Now enumerate our children. Children of this process are any process which has this process id as its parent + // and which also started after this process did. + List> children = GetChildProcessIds(processIdToKill, myStartTime); + + try + { + foreach (KeyValuePair childProcessInfo in children) + { + KillTree(childProcessInfo.Key); + } + } + finally + { + foreach (KeyValuePair childProcessInfo in children) + { + childProcessInfo.Value.Dispose(); + } + } + } + finally + { + // Release the handle. After this point no more children of this process exist and this process has also exited. + hProcess.Dispose(); + } + } + finally + { + thisProcess.Dispose(); + } + } + + /// + /// Returns the parent process id for the specified process. + /// Returns zero if it cannot be gotten for some reason. + /// + internal static int GetParentProcessId(int processId) + { + int ParentID = 0; + SafeProcessHandle hProcess = OpenProcess(eDesiredAccess.PROCESS_QUERY_INFORMATION, false, processId); + + if (!hProcess.IsInvalid) + { + try + { + // UNDONE: NtQueryInformationProcess will fail if we are not elevated and other process is. Advice is to change to use ToolHelp32 API's + // For now just return zero and worst case we will not kill some children. + PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION(); + int pSize = 0; + + if (0 == NtQueryInformationProcess(hProcess, PROCESSINFOCLASS.ProcessBasicInformation, ref pbi, pbi.Size, ref pSize)) + { + ParentID = (int)pbi.InheritedFromUniqueProcessId; + } + } + finally + { + hProcess.Dispose(); + } + } + + return (ParentID); + } + + /// + /// Returns an array of all the immediate child processes by id. + /// NOTE: The IntPtr in the tuple is the handle of the child process. CloseHandle MUST be called on this. + /// + internal static List> GetChildProcessIds(int parentProcessId, DateTime parentStartTime) + { + List> myChildren = new List>(); + + foreach (Process possibleChildProcess in Process.GetProcesses()) + { + using (possibleChildProcess) + { + // Hold the child process handle open so that children cannot die and restart with a different parent after we've started looking at it. + // This way, any handle we pass back is guaranteed to be one of our actual children. + SafeProcessHandle childHandle = OpenProcess(eDesiredAccess.PROCESS_QUERY_INFORMATION, false, possibleChildProcess.Id); + if (childHandle.IsInvalid) + { + continue; + } + + bool keepHandle = false; + try + { + if (possibleChildProcess.StartTime > parentStartTime) + { + int childParentProcessId = GetParentProcessId(possibleChildProcess.Id); + if (childParentProcessId != 0) + { + if (parentProcessId == childParentProcessId) + { + // Add this one + myChildren.Add(new KeyValuePair(possibleChildProcess.Id, childHandle)); + keepHandle = true; + } + } + } + } + finally + { + if (!keepHandle) + { + childHandle.Dispose(); + } + } + } + } + + return myChildren; + } + + /// + /// Internal, optimized GetCurrentDirectory implementation that simply delegates to the native method + /// + /// + internal static string GetCurrentDirectory() + { + StringBuilder sb = new StringBuilder(MAX_PATH); + int pathLength = GetCurrentDirectory(MAX_PATH, sb); + + if (pathLength > 0) + { + return sb.ToString(); + } + else + { + return null; + } + } + + #endregion + + #region PInvoke + + /// + /// Gets the current OEM code page which is used by console apps + /// (as opposed to the Windows/ANSI code page used by the normal people) + /// Basically for each ANSI code page (set in Regional settings) there's a corresponding OEM code page + /// that needs to be used for instance when writing to batch files + /// + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport(kernel32Dll)] + internal static extern int GetOEMCP(); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport(kernel32Dll, SetLastError = true, CharSet = CharSet.Unicode)] + private static extern uint SearchPath + ( + string path, + string fileName, + string extension, + int numBufferChars, + [Out] StringBuilder buffer, + int[] filePart + ); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", PreserveSig = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FreeLibrary([In] IntPtr module); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", PreserveSig = true, BestFitMapping = false, ThrowOnUnmappableChar = true, CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern IntPtr GetProcAddress(IntPtr module, string procName); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true)] + internal static extern IntPtr LoadLibrary(string fileName); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport(mscoreeDLL, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern uint GetRequestedRuntimeInfo(String pExe, + String pwszVersion, + String pConfigurationFile, + uint startupFlags, + uint runtimeInfoFlags, + [Out] StringBuilder pDirectory, + int dwDirectory, + out uint dwDirectoryLength, + [Out] StringBuilder pVersion, + int cchBuffer, + out uint dwlength); + + /// + /// Gets the fully qualified filename of the currently executing .exe + /// + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport(kernel32Dll, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern int GetModuleFileName(HandleRef hModule, [Out] StringBuilder buffer, int length); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll")] + internal static extern IntPtr GetStdHandle(int nStdHandle); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll")] + internal static extern uint GetFileType(IntPtr hFile); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern int GetCurrentDirectory(int nBufferLength, [Out] StringBuilder lpBuffer); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetCurrentDirectory(string path); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static unsafe extern int GetFullPathName(string target, int bufferLength, char* buffer, IntPtr mustBeZero); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("KERNEL32.DLL")] + private static extern SafeProcessHandle OpenProcess(eDesiredAccess dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("NTDLL.DLL")] + private static extern int NtQueryInformationProcess(SafeProcessHandle hProcess, PROCESSINFOCLASS pic, ref PROCESS_BASIC_INFORMATION pbi, int cb, ref int pSize); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool GlobalMemoryStatusEx([In, Out] MemoryStatus lpBuffer); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)] + internal static extern int GetShortPathName(string path, [Out] System.Text.StringBuilder fullpath, [In] int length); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)] + internal static extern int GetLongPathName([In] string path, [Out] System.Text.StringBuilder fullpath, [In] int length); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SecurityAttributes lpPipeAttributes, int nSize); + + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool ReadFile(SafeFileHandle hFile, byte[] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped); + + /// + /// CoWaitForMultipleHandles allows us to wait in an STA apartment and still service RPC requests from other threads. + /// VS needs this in order to allow the in-proc compilers to properly initialize, since they will make calls from the + /// build thread which the main thread (blocked on BuildSubmission.Execute) must service. + /// + [SuppressMessage("Microsoft.Design", "CA1060:MovePInvokesToNativeMethodsClass", Justification = "Class name is NativeMethodsShared for increased clarity")] + [DllImport("ole32.dll")] + public static extern int CoWaitForMultipleHandles(COWAIT_FLAGS dwFlags, int dwTimeout, int cHandles, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] pHandles, out int pdwIndex); + + #endregion + + #region Extensions + + /// + /// Waits while pumping APC messages. This is important if the waiting thread is an STA thread which is potentially + /// servicing COM calls from other threads. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Runtime.InteropServices.SafeHandle.DangerousGetHandle", Scope = "member", Target = "Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle,System.Int32)", Justification = "This is necessary and it has been used for a long time. No need to change it now.")] + internal static bool MsgWaitOne(this WaitHandle handle) + { + return handle.MsgWaitOne(Timeout.Infinite); + } + + /// + /// Waits while pumping APC messages. This is important if the waiting thread is an STA thread which is potentially + /// servicing COM calls from other threads. + /// + internal static bool MsgWaitOne(this WaitHandle handle, TimeSpan timeout) + { + return MsgWaitOne(handle, (int)timeout.TotalMilliseconds); + } + + /// + /// Waits while pumping APC messages. This is important if the waiting thread is an STA thread which is potentially + /// servicing COM calls from other threads. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Runtime.InteropServices.SafeHandle.DangerousGetHandle", Justification = "Necessary to avoid pumping")] + internal static bool MsgWaitOne(this WaitHandle handle, int timeout) + { + // CoWaitForMultipleHandles allows us to wait in an STA apartment and still service RPC requests from other threads. + // VS needs this in order to allow the in-proc compilers to properly initialize, since they will make calls from the + // build thread which the main thread (blocked on BuildSubmission.Execute) must service. + int waitIndex; + int returnValue = CoWaitForMultipleHandles(COWAIT_FLAGS.COWAIT_NONE, timeout, 1, new IntPtr[] { handle.SafeWaitHandle.DangerousGetHandle() }, out waitIndex); + ErrorUtilities.VerifyThrow(returnValue == 0 || ((uint)returnValue == RPC_S_CALLPENDING && timeout != Timeout.Infinite), "Received {0} from CoWaitForMultipleHandles, but expected 0 (S_OK)", returnValue); + return returnValue == 0; + } + + #endregion + } +} diff --git a/src/Shared/NodeBuildComplete.cs b/src/Shared/NodeBuildComplete.cs new file mode 100644 index 00000000000..24843fee986 --- /dev/null +++ b/src/Shared/NodeBuildComplete.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A packet which instructs a node that the build is complete. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The NodeBuildComplete packet is used to indicate to a node that it should clean up its current build and + /// possibly prepare for node reuse. + /// + internal class NodeBuildComplete : INodePacket + { + /// + /// Flag indicating if the node should prepare for reuse after cleanup. + /// + private bool _prepareForReuse; + + /// + /// Constructor. + /// + public NodeBuildComplete(bool prepareForReuse) + { + _prepareForReuse = prepareForReuse; + } + + /// + /// Private constructor for translation + /// + private NodeBuildComplete() + { + } + + /// + /// Flag indicating if the node should prepare for reuse. + /// + public bool PrepareForReuse + { + [DebuggerStepThrough] + get + { return _prepareForReuse; } + } + + #region INodePacket Members + + /// + /// The packet type + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.NodeBuildComplete; } + } + + #endregion + + #region INodePacketTranslatable Members + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _prepareForReuse); + } + + /// + /// Factory for deserialization. + /// + internal static NodeBuildComplete FactoryForDeserialization(INodePacketTranslator translator) + { + NodeBuildComplete packet = new NodeBuildComplete(); + packet.Translate(translator); + return packet; + } + + #endregion + } +} diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs new file mode 100644 index 00000000000..ea546267911 --- /dev/null +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -0,0 +1,584 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Base class for the implementation of a node endpoint for out-of-proc nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using System.Security; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Security.Permissions; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This is an implementation of INodeEndpoint for the out-of-proc nodes. It acts only as a client. + /// + internal abstract class NodeEndpointOutOfProcBase : INodeEndpoint + { + #region Private Data + + /// + /// The amount of time to wait for the client to connect to the host. + /// + private const int ClientConnectTimeout = 60000; + + /// + /// The size of the buffers to use for named pipes + /// + private const int PipeBufferSize = 131072; + + /// + /// Flag indicating if we should debug communications or not. + /// + private bool _debugCommunications = false; + + /// + /// The current communication status of the node. + /// + private LinkStatus _status; + + /// + /// The pipe client used by the nodes. + /// + private NamedPipeServerStream _pipeServer; + + // The following private data fields are used only when the endpoint is in ASYNCHRONOUS mode. + + /// + /// Object used as a lock source for the async data + /// + private object _asyncDataMonitor; + + /// + /// Set when a packet is available in the packet queue + /// + private AutoResetEvent _packetAvailable; + + /// + /// Set when the asynchronous packet pump should terminate + /// + private AutoResetEvent _terminatePacketPump; + + /// + /// The thread which runs the asynchronous packet pump + /// + private Thread _packetPump; + + /// + /// The factory used to create and route packets. + /// + private INodePacketFactory _packetFactory; + + /// + /// The asynchronous packet queue. + /// + /// + /// Operations on this queue must be synchronized since it is accessible by multiple threads. + /// Use a lock on the packetQueue itself. + /// + private Queue _packetQueue; + + /// + /// Per-node shared read buffer. + /// + private SharedReadBuffer _sharedReadBuffer; + + #endregion + + #region INodeEndpoint Events + + /// + /// Raised when the link status has changed. + /// + public event LinkStatusChangedDelegate OnLinkStatusChanged; + + #endregion + + #region INodeEndpoint Properties + + /// + /// Returns the link status of this node. + /// + public LinkStatus LinkStatus + { + get { return _status; } + } + + #endregion + + #region Properties + + #endregion + + #region INodeEndpoint Methods + + /// + /// Causes this endpoint to wait for the remote endpoint to connect + /// + /// The factory used to create packets. + public void Listen(INodePacketFactory factory) + { + ErrorUtilities.VerifyThrow(_status == LinkStatus.Inactive, "Link not inactive. Status is {0}", _status); + ErrorUtilities.VerifyThrowArgumentNull(factory, "factory"); + _packetFactory = factory; + + InitializeAsyncPacketThread(); + } + + /// + /// Causes this node to connect to the matched endpoint. + /// + /// The factory used to create packets. + public void Connect(INodePacketFactory factory) + { + ErrorUtilities.ThrowInternalError("Connect() not valid on the out of proc endpoint."); + } + + /// + /// Shuts down the link + /// + public void Disconnect() + { + InternalDisconnect(); + } + + /// + /// Sends data to the peer endpoint. + /// + /// The packet to send. + public void SendData(INodePacket packet) + { + // PERF: Set up a priority system so logging packets are sent only when all other packet types have been sent. + if (_status == LinkStatus.Active) + { + EnqueuePacket(packet); + } + } + + #endregion + + #region Construction + + /// + /// Instantiates an endpoint to act as a client + /// + /// The name of the pipe to which we should connect. + internal void InternalConstruct(string pipeName) + { + ErrorUtilities.VerifyThrowArgumentLength(pipeName, "pipeName"); + + _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); + + _status = LinkStatus.Inactive; + _asyncDataMonitor = new object(); + _sharedReadBuffer = InterningBinaryReader.CreateSharedBuffer(); + + SecurityIdentifier identifier = WindowsIdentity.GetCurrent().Owner; + PipeSecurity security = new PipeSecurity(); + + // Restrict access to just this account. We set the owner specifically here, and on the + // pipe client side they will check the owner against this one - they must have identical + // SIDs or the client will reject this server. This is used to avoid attacks where a + // hacked server creates a less restricted pipe in an attempt to lure us into using it and + // then sending build requests to the real pipe client (which is the MSBuild Build Manager.) + PipeAccessRule rule = new PipeAccessRule(identifier, PipeAccessRights.ReadWrite, AccessControlType.Allow); + security.AddAccessRule(rule); + security.SetOwner(identifier); + + _pipeServer = new NamedPipeServerStream + ( + pipeName, + PipeDirection.InOut, + 1, // Only allow one connection at a time. + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | PipeOptions.WriteThrough, + PipeBufferSize, // Default input buffer + PipeBufferSize, // Default output buffer + security, + HandleInheritability.None + ); + } + + #endregion + + /// + /// Returns the host handshake for this node endpoint + /// + protected abstract long GetHostHandshake(); + + /// + /// Returns the client handshake for this node endpoint + /// + protected abstract long GetClientHandshake(); + + /// + /// Updates the current link status if it has changed and notifies any registered delegates. + /// + /// The status the node should now be in. + protected void ChangeLinkStatus(LinkStatus newStatus) + { + ErrorUtilities.VerifyThrow(_status != newStatus, "Attempting to change status to existing status {0}.", _status); + CommunicationsUtilities.Trace("Changing link status from {0} to {1}", _status.ToString(), newStatus.ToString()); + _status = newStatus; + RaiseLinkStatusChanged(_status); + } + + /// + /// Invokes the OnLinkStatusChanged event in a thread-safe manner. + /// + /// The new status of the endpoint link. + private void RaiseLinkStatusChanged(LinkStatus newStatus) + { + if (null != OnLinkStatusChanged) + { + LinkStatusChangedDelegate linkStatusDelegate = (LinkStatusChangedDelegate)OnLinkStatusChanged.Clone(); + linkStatusDelegate(this, newStatus); + } + } + + #region Private Methods + + /// + /// This does the actual work of changing the status and shutting down any threads we may have for + /// disconnection. + /// + private void InternalDisconnect() + { + ErrorUtilities.VerifyThrow(_packetPump.ManagedThreadId != Thread.CurrentThread.ManagedThreadId, "Can't join on the same thread."); + _terminatePacketPump.Set(); + _packetPump.Join(); + _terminatePacketPump.Close(); + _pipeServer.Close(); + _packetPump = null; + ChangeLinkStatus(LinkStatus.Inactive); + } + + #region Asynchronous Mode Methods + + /// + /// Adds a packet to the packet queue when asynchronous mode is enabled. + /// + /// The packet to be transmitted. + private void EnqueuePacket(INodePacket packet) + { + ErrorUtilities.VerifyThrowArgumentNull(packet, "packet"); + ErrorUtilities.VerifyThrow(null != _packetQueue, "packetQueue is null"); + ErrorUtilities.VerifyThrow(null != _packetAvailable, "packetAvailable is null"); + + lock (_packetQueue) + { + _packetQueue.Enqueue(packet); + _packetAvailable.Set(); + } + } + + /// + /// Initializes the packet pump thread and the supporting events as well as the packet queue. + /// + private void InitializeAsyncPacketThread() + { + lock (_asyncDataMonitor) + { + _packetPump = new Thread(PacketPumpProc); + _packetPump.IsBackground = true; + _packetPump.Name = "OutOfProc Endpoint Packet Pump"; + _packetAvailable = new AutoResetEvent(false); + _terminatePacketPump = new AutoResetEvent(false); + _packetQueue = new Queue(); + _packetPump.Start(); + } + } + + /// + /// This method handles the asynchronous message pump. It waits for messages to show up on the queue + /// and calls FireDataAvailable for each such packet. It will terminate when the terminate event is + /// set. + /// + private void PacketPumpProc() + { + NamedPipeServerStream localPipeServer = _pipeServer; + AutoResetEvent localPacketAvailable = _packetAvailable; + AutoResetEvent localTerminatePacketPump = _terminatePacketPump; + Queue localPacketQueue = _packetQueue; + + DateTime originalWaitStartTime = DateTime.UtcNow; + bool gotValidConnection = false; + while (!gotValidConnection) + { + DateTime restartWaitTime = DateTime.UtcNow; + + // We only wait to wait the difference between now and the last original start time, in case we have multiple hosts attempting + // to attach. This prevents each attempt from resetting the timer. + TimeSpan usedWaitTime = restartWaitTime - originalWaitStartTime; + int waitTimeRemaining = Math.Max(0, CommunicationsUtilities.NodeConnectionTimeout - (int)usedWaitTime.TotalMilliseconds); + + try + { + // Wait for a connection + IAsyncResult resultForConnection = localPipeServer.BeginWaitForConnection(null, null); + CommunicationsUtilities.Trace("Waiting for connection {0} ms...", waitTimeRemaining); + + bool connected = resultForConnection.AsyncWaitHandle.WaitOne(waitTimeRemaining, false); + if (!connected) + { + CommunicationsUtilities.Trace("Connection timed out waiting a host to contact us. Exiting comm thread."); + ChangeLinkStatus(LinkStatus.ConnectionFailed); + return; + } + + CommunicationsUtilities.Trace("Parent started connecting. Reading handshake from parent"); + localPipeServer.EndWaitForConnection(resultForConnection); + + // The handshake protocol is a simple long exchange. The host sends us a long, and we + // respond with another long. Once the handshake is complete, both sides can be assured the + // other is ready to accept data. + // To avoid mixing client and server builds, the long is the MSBuild binary timestamp. + + // Compatibility issue here. + // Previous builds of MSBuild 4.0 would exchange just a byte. + // Host would send either 0x5F or 0x60 depending on whether it was the toolset or not respectively. + // Client would return either 0xF5 or 0x06 respectively. + // Therefore an old host on a machine with new clients running will hang, + // sending a byte and waiting for a byte until it eventually times out; + // because the new client will want 7 more bytes before it returns anything. + // The other way around is not a problem, because the old client would immediately return the (wrong) + // byte on receiving the first byte of the long sent by the new host, and the new host would disconnect. + // To avoid the hang, special case here: + // Make sure our handshakes always start with 00. + // If we received ONLY one byte AND it's 0x5F or 0x60, return 0xFF (it doesn't matter what as long as + // it will cause the host to reject us; new hosts expect 00 and old hosts expect F5 or 06). + try + { + long handshake = localPipeServer.ReadLongForHandshake(/* reject these leads */ new byte[] { 0x5F, 0x60 }, 0xFF /* this will disconnect the host; it expects leading 00 or F5 or 06 */); + WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent(); + string remoteUserName = localPipeServer.GetImpersonationUserName(); + + if (handshake != GetHostHandshake()) + { + CommunicationsUtilities.Trace("Handshake failed. Received {0} from host not {1}. Probably the host is a different MSBuild build.", handshake, GetHostHandshake()); + localPipeServer.Disconnect(); + continue; + } + + // We will only talk to a host that was started by the same user as us. Even though the pipe access is set to only allow this user, we want to ensure they + // haven't attempted to change those permissions out from under us. This ensures that the only way they can truly gain access is to be impersonating the + // user we were started by. + WindowsIdentity clientIdentity = null; + localPipeServer.RunAsClient(delegate () { clientIdentity = WindowsIdentity.GetCurrent(true); }); + + if (clientIdentity == null || !String.Equals(clientIdentity.Name, currentIdentity.Name, StringComparison.OrdinalIgnoreCase)) + { + CommunicationsUtilities.Trace("Handshake failed. Host user is {0} but we were created by {1}.", (clientIdentity == null) ? "" : clientIdentity.Name, currentIdentity.Name); + localPipeServer.Disconnect(); + continue; + } + } + catch (IOException e) + { + // We will get here when: + // 1. The host (OOP main node) connects to us, it immediately checks for user priviledges + // and if they don't match it disconnects immediately leaving us still trying to read the blank handshake + // 2. The host is too old sending us bits we automatically reject in the handshake + CommunicationsUtilities.Trace("Client connection failed but we will wait for another connection. Exception: {0}", e.Message); + if (localPipeServer.IsConnected) + { + localPipeServer.Disconnect(); + } + + continue; + } + + gotValidConnection = true; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + CommunicationsUtilities.Trace("Client connection failed. Exiting comm thread. {0}", e); + if (localPipeServer.IsConnected) + { + localPipeServer.Disconnect(); + } + + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Failed); + return; + } + } + + CommunicationsUtilities.Trace("Writing handshake to parent"); + localPipeServer.WriteLongForHandshake(GetClientHandshake()); + ChangeLinkStatus(LinkStatus.Active); + + // Ordering of the wait handles is important. The first signalled wait handle in the array + // will be returned by WaitAny if multiple wait handles are signalled. We prefer to have the + // terminate event triggered so that we cannot get into a situation where packets are being + // spammed to the endpoint and it never gets an opportunity to shutdown. + CommunicationsUtilities.Trace("Entering read loop."); + byte[] headerByte = new byte[5]; + IAsyncResult result = localPipeServer.BeginRead(headerByte, 0, headerByte.Length, null, null); + + bool exitLoop = false; + do + { + // Ordering is important. We want packetAvailable to supercede terminate otherwise we will not properly wait for all + // packets to be sent by other threads which are shutting down, such as the logging thread. + WaitHandle[] handles = new WaitHandle[] { result.AsyncWaitHandle, localPacketAvailable, localTerminatePacketPump }; + + int waitId = WaitHandle.WaitAny(handles); + switch (waitId) + { + case 0: + { + int bytesRead = 0; + try + { + bytesRead = localPipeServer.EndRead(result); + } + catch (Exception e) + { + // Lost communications. Abort (but allow node reuse) + CommunicationsUtilities.Trace("Exception reading from server. {0}", e); + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Inactive); + exitLoop = true; + break; + } + + if (bytesRead != headerByte.Length) + { + // Incomplete read. Abort. + if (bytesRead == 0) + { + CommunicationsUtilities.Trace("Parent disconnected abruptly"); + } + else + { + CommunicationsUtilities.Trace("Incomplete header read from server. {0} of {1} bytes read", bytesRead, headerByte.Length); + } + + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + break; + } + + NodePacketType packetType = (NodePacketType)Enum.ToObject(typeof(NodePacketType), headerByte[0]); + int packetLength = BitConverter.ToInt32(headerByte, 1); + + try + { + _packetFactory.DeserializeAndRoutePacket(0, packetType, NodePacketTranslator.GetReadTranslator(localPipeServer, _sharedReadBuffer)); + } + catch (Exception e) + { + // Error while deserializing or handling packet. Abort. + CommunicationsUtilities.Trace("Exception while deserializing packet {0}: {1}", packetType, e); + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + break; + } + + result = localPipeServer.BeginRead(headerByte, 0, headerByte.Length, null, null); + } + + break; + + case 1: + case 2: + try + { + int packetCount = localPacketQueue.Count; + + // Write out all the queued packets. + while (packetCount > 0) + { + INodePacket packet; + lock (_packetQueue) + { + packet = localPacketQueue.Dequeue(); + } + + MemoryStream packetStream = new MemoryStream(); + INodePacketTranslator writeTranslator = NodePacketTranslator.GetWriteTranslator(packetStream); + + packetStream.WriteByte((byte)packet.Type); + + // Pad for packet length + packetStream.Write(BitConverter.GetBytes((int)0), 0, 4); + + // Reset the position in the write buffer. + packet.Translate(writeTranslator); + + // Now write in the actual packet length + packetStream.Position = 1; + packetStream.Write(BitConverter.GetBytes((int)packetStream.Length - 5), 0, 4); + + localPipeServer.Write(packetStream.GetBuffer(), 0, (int)packetStream.Length); + + packetCount--; + } + } + catch (Exception e) + { + // Error while deserializing or handling packet. Abort. + CommunicationsUtilities.Trace("Exception while serializing packets: {0}", e); + ExceptionHandling.DumpExceptionToFile(e); + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + break; + } + + if (waitId == 2) + { + CommunicationsUtilities.Trace("Disconnecting voluntarily"); + ChangeLinkStatus(LinkStatus.Failed); + exitLoop = true; + } + + break; + + default: + ErrorUtilities.ThrowInternalError("waitId {0} out of range.", waitId); + break; + } + } + while (!exitLoop); + + CommunicationsUtilities.Trace("Ending read loop"); + + try + { + if (localPipeServer.IsConnected) + { + localPipeServer.WaitForPipeDrain(); + localPipeServer.Disconnect(); + } + } + catch (Exception) + { + // We don't really care if Disconnect somehow fails, but it gives us a chance to do the right thing. + } + } + + #endregion + + #endregion + } +} diff --git a/src/Shared/NodeEngineShutdownReason.cs b/src/Shared/NodeEngineShutdownReason.cs new file mode 100644 index 00000000000..f98681e21a7 --- /dev/null +++ b/src/Shared/NodeEngineShutdownReason.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Enumeration of the reasons a node would shut down. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.Execution +{ + #region Enums + /// + /// Reasons for a node to shutdown. + /// + public enum NodeEngineShutdownReason + { + /// + /// The BuildManager sent a command instructing the node to terminate. + /// + BuildComplete, + + /// + /// The BuildManager sent a command instructing the node to terminate, but to restart for reuse. + /// + BuildCompleteReuse, + + /// + /// The communication link failed. + /// + ConnectionFailed, + + /// + /// The NodeEngine caught an exception which requires the Node to shut down. + /// + Error, + } + #endregion +} \ No newline at end of file diff --git a/src/Shared/NodePacketFactory.cs b/src/Shared/NodePacketFactory.cs new file mode 100644 index 00000000000..ce5475fa1a4 --- /dev/null +++ b/src/Shared/NodePacketFactory.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of INodePacketFactory. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Implementation of INodePacketFactory as a helper class for classes which expose this interface publicly. + /// + internal class NodePacketFactory : INodePacketFactory + { + /// + /// Mapping of packet types to factory information. + /// + private Dictionary _packetFactories; + + /// + /// Constructor + /// + public NodePacketFactory() + { + _packetFactories = new Dictionary(); + } + + #region INodePacketFactory Members + + /// + /// Registers a packet handler + /// + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _packetFactories[packetType] = new PacketFactoryRecord(handler, factory); + } + + /// + /// Unregisters a packet handler. + /// + public void UnregisterPacketHandler(NodePacketType packetType) + { + _packetFactories.Remove(packetType); + } + + /// + /// Creates and routes a packet with data from a binary stream. + /// + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + // PERF: Not using VerifyThrow to avoid boxing of packetType in the non-error case + if (!_packetFactories.ContainsKey(packetType)) + { + ErrorUtilities.ThrowInternalError("No packet handler for type {0}", packetType); + } + + PacketFactoryRecord record = _packetFactories[packetType]; + record.DeserializeAndRoutePacket(nodeId, translator); + } + + /// + /// Routes the specified packet. + /// + public void RoutePacket(int nodeId, INodePacket packet) + { + PacketFactoryRecord record = _packetFactories[packet.Type]; + record.RoutePacket(nodeId, packet); + } + + #endregion + + /// + /// A record for a packet factory + /// + private class PacketFactoryRecord + { + /// + /// The handler to invoke when the packet is deserialized. + /// + private INodePacketHandler _handler; + + /// + /// The method used to construct a packet from a translator stream. + /// + private NodePacketFactoryMethod _factoryMethod; + + /// + /// Constructor. + /// + public PacketFactoryRecord(INodePacketHandler handler, NodePacketFactoryMethod factoryMethod) + { + _handler = handler; + _factoryMethod = factoryMethod; + } + + /// + /// Creates a packet from a binary stream and sends it to the registered handler. + /// + public void DeserializeAndRoutePacket(int nodeId, INodePacketTranslator translator) + { + INodePacket packet = _factoryMethod(translator); + RoutePacket(nodeId, packet); + } + + /// + /// Routes the packet to the correct destination. + /// + public void RoutePacket(int nodeId, INodePacket packet) + { + _handler.PacketReceived(nodeId, packet); + } + } + } +} diff --git a/src/Shared/NodePacketTranslator.cs b/src/Shared/NodePacketTranslator.cs new file mode 100644 index 00000000000..8198454a8c9 --- /dev/null +++ b/src/Shared/NodePacketTranslator.cs @@ -0,0 +1,1024 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of INodePacketTranslator. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.IO; +using System.Threading; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Globalization; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class is responsible for serializing and deserializing simple types to and + /// from the byte streams used to communicate INodePacket-implementing classes. + /// Each class implements a Translate method on INodePacket which takes this class + /// as a parameter, and uses it to store and retrieve fields to the stream. + /// + static internal class NodePacketTranslator + { + /// + /// Returns a read-only serializer. + /// + /// The stream containing data to deserialize. + /// The serializer. + static internal INodePacketTranslator GetReadTranslator(Stream stream, SharedReadBuffer buffer) + { + return new NodePacketReadTranslator(stream, buffer); + } + + /// + /// Returns a write-only serializer. + /// + /// The stream containing data to serialize. + /// The serializer. + static internal INodePacketTranslator GetWriteTranslator(Stream stream) + { + return new NodePacketWriteTranslator(stream); + } + + /// + /// Implementation of INodePacketTranslator for reading from a stream. + /// + private class NodePacketReadTranslator : INodePacketTranslator + { + /// + /// The stream used as a source or destination for data. + /// + private Stream _packetStream; + + /// + /// The binary reader used in read mode. + /// + private BinaryReader _reader; + + /// + /// Constructs a serializer from the specified stream, operating in the designated mode. + /// + /// The stream serving as the source or destination of data. + public NodePacketReadTranslator(Stream packetStream, SharedReadBuffer buffer) + { + _packetStream = packetStream; + _reader = InterningBinaryReader.Create(packetStream, buffer); + } + + /// + /// Gets the reader, if any. + /// + public BinaryReader Reader + { + get { return _reader; } + } + + /// + /// Gets the writer, if any. + /// + public BinaryWriter Writer + { + get + { + ErrorUtilities.ThrowInternalError("Cannot get writer from reader."); + return null; + } + } + + /// + /// Returns the current serialization mode. + /// + public TranslationDirection Mode + { + [DebuggerStepThrough] + get + { return TranslationDirection.ReadFromStream; } + } + + /// + /// Translates a boolean. + /// + /// The value to be translated. + public void Translate(ref bool value) + { + value = _reader.ReadBoolean(); + } + + /// + /// Translates a byte. + /// + /// The value to be translated. + public void Translate(ref byte value) + { + value = _reader.ReadByte(); + } + + /// + /// Translates a short. + /// + /// The value to be translated. + public void Translate(ref short value) + { + value = _reader.ReadInt16(); + } + + /// + /// Translates an unsigned short. + /// + /// The value to be translated. + public void Translate(ref ushort value) + { + value = _reader.ReadUInt16(); + } + + /// + /// Translates an integer. + /// + /// The value to be translated. + public void Translate(ref int value) + { + value = _reader.ReadInt32(); + } + + /// + /// Translates a string. + /// + /// The value to be translated. + public void Translate(ref string value) + { + if (!TranslateNullable(value)) + { + return; + } + + value = _reader.ReadString(); + } + + /// + /// Translates a byte array + /// + /// The array to be translated + public void Translate(ref byte[] byteArray) + { + if (!TranslateNullable(byteArray)) + { + return; + } + + int count = _reader.ReadInt32(); + if (count > 0) + { + byteArray = _reader.ReadBytes(count); + } + else + { + byteArray = new byte[0]; + } + } + + /// + /// Translates a string array. + /// + /// The array to be translated. + public void Translate(ref string[] array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = _reader.ReadInt32(); + array = new string[count]; + + for (int i = 0; i < count; i++) + { + array[i] = _reader.ReadString(); + } + } + + /// + /// Translates a list of strings + /// + /// The list to be translated. + public void Translate(ref List list) + { + if (!TranslateNullable(list)) + { + return; + } + + int count = _reader.ReadInt32(); + list = new List(count); + + for (int i = 0; i < count; i++) + { + list.Add(_reader.ReadString()); + } + } + + /// + /// Translates a list of T where T implements INodePacketTranslateable + /// + /// The list to be translated. + /// Factory to deserialize T + /// TaskItem type + public void Translate(ref List list, NodePacketValueFactory factory) where T : INodePacketTranslatable + { + if (!TranslateNullable(list)) + { + return; + } + + int count = _reader.ReadInt32(); + list = new List(count); + + for (int i = 0; i < count; i++) + { + T value = default(T); + + if (!TranslateNullable(value)) + { + continue; + } + + value = factory(this); + list.Add(value); + } + } + + /// + /// Translates a DateTime. + /// + /// The value to be translated. + public void Translate(ref DateTime value) + { + DateTimeKind kind = DateTimeKind.Unspecified; + TranslateEnum(ref kind, 0); + value = new DateTime(_reader.ReadInt64(), kind); + } + + // MSBuildTaskHost is based on CLR 3.5, which does not have the 6-parameter constructor for BuildEventContext. + // However, it also does not ever need to translate BuildEventContexts, so it should be perfectly safe to + // compile this method out of that assembly. +#if !CLR2COMPATIBILITY + + /// + /// Translates a BuildEventContext + /// + /// + /// This method exists only because there is no serialization method built into the BuildEventContext + /// class, and it lives in Framework and we don't want to add a public method to it. + /// + /// The context to be translated. + public void Translate(ref BuildEventContext value) + { + value = new BuildEventContext + ( + _reader.ReadInt32(), + _reader.ReadInt32(), + _reader.ReadInt32(), + _reader.ReadInt32(), + _reader.ReadInt32(), + _reader.ReadInt32() + ); + } + +#endif + + /// + /// Translates a CultureInfo + /// + /// The CultureInfo to translate + public void TranslateCulture(ref CultureInfo value) + { + string cultureName = _reader.ReadString(); + value = new CultureInfo(cultureName); + } + + /// + /// Translates an enumeration. + /// + /// The enumeration type. + /// The enumeration instance to be translated. + /// The enumeration value as an integer. + /// This is a bit ugly, but it doesn't seem like a nice method signature is possible because + /// you can't pass the enum type as a reference and constrain the generic parameter to Enum. Nor + /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. + /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This + /// works in all of our current cases, but certainly isn't perfectly generic. + public void TranslateEnum(ref T value, int numericValue) + { + numericValue = _reader.ReadInt32(); + Type enumType = value.GetType(); + value = (T)Enum.ToObject(enumType, numericValue); + } + + /// + /// Translates a value using the .Net binary formatter. + /// + /// The reference type. + /// The value to be translated. + public void TranslateDotNet(ref T value) + { + if (!TranslateNullable(value)) + { + return; + } + + BinaryFormatter formatter = new BinaryFormatter(); + value = (T)formatter.Deserialize(_packetStream); + } + + /// + /// Translates an object implementing INodePacketTranslatable. + /// + /// The reference type. + /// The value to be translated. + public void Translate(ref T value) + where T : INodePacketTranslatable, new() + { + if (!TranslateNullable(value)) + { + return; + } + + value = new T(); + value.Translate(this); + } + + /// + /// Translates an object implementing INodePacketTranslatable which does not expose a + /// public parameterless constructor. + /// + /// The reference type. + /// The value to be translated. + /// The factory method used to instantiate values of type T. + public void Translate(ref T value, NodePacketValueFactory factory) + where T : INodePacketTranslatable + { + if (!TranslateNullable(value)) + { + return; + } + + value = factory(this); + } + + /// + /// Translates an array of objects implementing INodePacketTranslatable. + /// + /// The reference type. + /// The array to be translated. + public void TranslateArray(ref T[] array) + where T : INodePacketTranslatable, new() + { + if (!TranslateNullable(array)) + { + return; + } + + int count = _reader.ReadInt32(); + array = new T[count]; + + for (int i = 0; i < count; i++) + { + array[i] = new T(); + array[i].Translate(this); + } + } + + /// + /// Translates an array of objects implementing INodePacketTranslatable requiring a factory to create. + /// + /// The reference type. + /// The array to be translated. + /// The factory method used to instantiate values of type T. + public void TranslateArray(ref T[] array, NodePacketValueFactory factory) + where T : INodePacketTranslatable + { + if (!TranslateNullable(array)) + { + return; + } + + int count = _reader.ReadInt32(); + array = new T[count]; + + for (int i = 0; i < count; i++) + { + array[i] = factory(this); + } + } + + /// + /// Translates a dictionary of { string, string }. + /// + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + public void TranslateDictionary(ref Dictionary dictionary, IEqualityComparer comparer) + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = _reader.ReadInt32(); + dictionary = new Dictionary(count, comparer); + + for (int i = 0; i < count; i++) + { + string key = null; + Translate(ref key); + string value = null; + Translate(ref value); + dictionary[key] = value; + } + } + + /// + /// Translates a dictionary of { string, T }. + /// + /// The reference type for the values, which implements INodePacketTranslatable. + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + /// The factory used to instantiate values in the dictionary. + public void TranslateDictionary(ref Dictionary dictionary, IEqualityComparer comparer, NodePacketValueFactory valueFactory) + where T : class, INodePacketTranslatable + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = _reader.ReadInt32(); + dictionary = new Dictionary(count, comparer); + + for (int i = 0; i < count; i++) + { + string key = null; + Translate(ref key); + T value = null; + Translate(ref value, valueFactory); + dictionary[key] = value; + } + } + + /// + /// Translates a dictionary of { string, T } for dictionaries with public parameterless constructors. + /// + /// The reference type for the dictionary. + /// The reference type for values in the dictionary. + /// The dictionary to be translated. + /// The factory used to instantiate values in the dictionary. + public void TranslateDictionary(ref D dictionary, NodePacketValueFactory valueFactory) + where D : IDictionary, new() + where T : class, INodePacketTranslatable + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = _reader.ReadInt32(); + dictionary = new D(); + + for (int i = 0; i < count; i++) + { + string key = null; + Translate(ref key); + T value = null; + Translate(ref value, valueFactory); + dictionary[key] = value; + } + } + + /// + /// Translates a dictionary of { string, T } for dictionaries with public parameterless constructors. + /// + /// The reference type for the dictionary. + /// The reference type for values in the dictionary. + /// The dictionary to be translated. + /// The factory used to instantiate values in the dictionary. + /// The delegate used to instantiate the dictionary. + public void TranslateDictionary(ref D dictionary, NodePacketValueFactory valueFactory, NodePacketDictionaryCreator dictionaryCreator) + where D : IDictionary + where T : class, INodePacketTranslatable + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = _reader.ReadInt32(); + dictionary = dictionaryCreator(count); + + for (int i = 0; i < count; i++) + { + string key = null; + Translate(ref key); + T value = null; + Translate(ref value, valueFactory); + dictionary[key] = value; + } + } + + /// + /// Reads in the boolean which says if this object is null or not. + /// + /// The type of object to test. + /// True if the object should be read, false otherwise. + public bool TranslateNullable(T value) + { + bool haveRef = _reader.ReadBoolean(); + return haveRef; + } + } + + /// + /// Implementation of INodePacketTranslator for writing to a stream. + /// + private class NodePacketWriteTranslator : INodePacketTranslator + { + /// + /// The stream used as a source or destination for data. + /// + private Stream _packetStream; + + /// + /// The binary writer used in write mode. + /// + private BinaryWriter _writer; + + /// + /// Constructs a serializer from the specified stream, operating in the designated mode. + /// + /// The stream serving as the source or destination of data. + public NodePacketWriteTranslator(Stream packetStream) + { + _packetStream = packetStream; + _writer = new BinaryWriter(packetStream); + } + + /// + /// Gets the reader, if any. + /// + public BinaryReader Reader + { + get + { + ErrorUtilities.ThrowInternalError("Cannot get reader from writer."); + return null; + } + } + + /// + /// Gets the writer, if any. + /// + public BinaryWriter Writer + { + get { return _writer; } + } + + /// + /// Returns the current serialization mode. + /// + public TranslationDirection Mode + { + [DebuggerStepThrough] + get + { return TranslationDirection.WriteToStream; } + } + + /// + /// Translates a boolean. + /// + /// The value to be translated. + public void Translate(ref bool value) + { + _writer.Write(value); + } + + /// + /// Translates a byte. + /// + /// The value to be translated. + public void Translate(ref byte value) + { + _writer.Write(value); + } + + /// + /// Translates a short. + /// + /// The value to be translated. + public void Translate(ref short value) + { + _writer.Write(value); + } + + /// + /// Translates an unsigned short. + /// + /// The value to be translated. + public void Translate(ref ushort value) + { + _writer.Write(value); + } + + /// + /// Translates an integer. + /// + /// The value to be translated. + public void Translate(ref int value) + { + _writer.Write(value); + } + + /// + /// Translates a string. + /// + /// The value to be translated. + public void Translate(ref string value) + { + if (!TranslateNullable(value)) + { + return; + } + + _writer.Write(value); + } + + /// + /// Translates a string array. + /// + /// The array to be translated. + public void Translate(ref string[] array) + { + if (!TranslateNullable(array)) + { + return; + } + + int count = 0; + count = array.Length; + _writer.Write(count); + + for (int i = 0; i < count; i++) + { + _writer.Write(array[i]); + } + } + + /// + /// Translates a list of strings + /// + /// The list to be translated. + public void Translate(ref List list) + { + if (!TranslateNullable(list)) + { + return; + } + + int count = list.Count; + _writer.Write(count); + + for (int i = 0; i < count; i++) + { + _writer.Write(list[i]); + } + } + + /// + /// Translates a list of T where T implements INodePacketTranslateable + /// + /// The list to be translated. + /// factory to create type T + /// A TaskItemType + public void Translate(ref List list, NodePacketValueFactory factory) where T : INodePacketTranslatable + { + if (!TranslateNullable(list)) + { + return; + } + + int count = list.Count; + _writer.Write(count); + + for (int i = 0; i < count; i++) + { + T value = list[i]; + Translate(ref value, factory); + } + } + + /// + /// Translates a DateTime. + /// + /// The value to be translated. + public void Translate(ref DateTime value) + { + DateTimeKind kind = value.Kind; + TranslateEnum(ref kind, (int)kind); + _writer.Write(value.Ticks); + } + + // MSBuildTaskHost is based on CLR 3.5, which does not have the 6-parameter constructor for BuildEventContext. + // However, it also does not ever need to translate BuildEventContexts, so it should be perfectly safe to + // compile this method out of that assembly. +#if !CLR2COMPATIBILITY + + /// + /// Translates a BuildEventContext + /// + /// + /// This method exists only because there is no serialization method built into the BuildEventContext + /// class, and it lives in Framework and we don't want to add a public method to it. + /// + /// The context to be translated. + public void Translate(ref BuildEventContext value) + { + _writer.Write(value.SubmissionId); + _writer.Write(value.NodeId); + _writer.Write(value.ProjectInstanceId); + _writer.Write(value.ProjectContextId); + _writer.Write(value.TargetId); + _writer.Write(value.TaskId); + } + +#endif + + /// + /// Translates a CultureInfo + /// + /// The CultureInfo + public void TranslateCulture(ref CultureInfo value) + { + _writer.Write(value.Name); + } + + /// + /// Translates an enumeration. + /// + /// The enumeration type. + /// The enumeration instance to be translated. + /// The enumeration value as an integer. + /// This is a bit ugly, but it doesn't seem like a nice method signature is possible because + /// you can't pass the enum type as a reference and constrain the generic parameter to Enum. Nor + /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. + /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This + /// works in all of our current cases, but certainly isn't perfectly generic. + public void TranslateEnum(ref T value, int numericValue) + { + Type enumType = value.GetType(); + ErrorUtilities.VerifyThrow(enumType.IsEnum, "Must pass an enum type."); + + _writer.Write(numericValue); + } + + /// + /// Translates a value using the .Net binary formatter. + /// + /// The reference type. + /// The value to be translated. + public void TranslateDotNet(ref T value) + { + if (!TranslateNullable(value)) + { + return; + } + + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(_packetStream, value); + } + + /// + /// Translates an object implementing INodePacketTranslatable. + /// + /// The reference type. + /// The value to be translated. + public void Translate(ref T value) + where T : INodePacketTranslatable, new() + { + if (!TranslateNullable(value)) + { + return; + } + + value.Translate(this); + } + + /// + /// Translates an object implementing INodePacketTranslatable which does not expose a + /// public parameterless constructor. + /// + /// The reference type. + /// The value to be translated. + /// The factory method used to instantiate values of type T. + public void Translate(ref T value, NodePacketValueFactory factory) + where T : INodePacketTranslatable + { + if (!TranslateNullable(value)) + { + return; + } + + value.Translate(this); + } + + /// + /// Translates a byte array + /// + /// The byte array to be translated + public void Translate(ref byte[] byteArray) + { + if (!TranslateNullable(byteArray)) + { + return; + } + + int count = byteArray.Length; + _writer.Write(count); + if (count > 0) + { + _writer.Write(byteArray); + } + } + + /// + /// Translates an array of objects implementing INodePacketTranslatable. + /// + /// The reference type. + /// The array to be translated. + public void TranslateArray(ref T[] array) + where T : INodePacketTranslatable, new() + { + if (!TranslateNullable(array)) + { + return; + } + + int count = array.Length; + _writer.Write(count); + + for (int i = 0; i < count; i++) + { + array[i].Translate(this); + } + } + + /// + /// Translates an array of objects implementing INodePacketTranslatable requiring a factory to create. + /// + /// The reference type. + /// The array to be translated. + /// The factory method used to instantiate values of type T. + public void TranslateArray(ref T[] array, NodePacketValueFactory factory) + where T : INodePacketTranslatable + { + if (!TranslateNullable(array)) + { + return; + } + + int count = array.Length; + _writer.Write(count); + + for (int i = 0; i < count; i++) + { + array[i].Translate(this); + } + } + + /// + /// Translates a dictionary of { string, string }. + /// + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + public void TranslateDictionary(ref Dictionary dictionary, IEqualityComparer comparer) + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = dictionary.Count; + _writer.Write(count); + + foreach (KeyValuePair pair in dictionary) + { + string key = pair.Key; + Translate(ref key); + string value = pair.Value; + Translate(ref value); + } + } + + /// + /// Translates a dictionary of { string, T }. + /// + /// The reference type for the values, which implements INodePacketTranslatable. + /// The dictionary to be translated. + /// The comparer used to instantiate the dictionary. + /// The factory used to instantiate values in the dictionary. + public void TranslateDictionary(ref Dictionary dictionary, IEqualityComparer comparer, NodePacketValueFactory valueFactory) + where T : class, INodePacketTranslatable + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = dictionary.Count; + _writer.Write(count); + + foreach (KeyValuePair pair in dictionary) + { + string key = pair.Key; + Translate(ref key); + T value = pair.Value; + Translate(ref value, valueFactory); + } + } + + /// + /// Translates a dictionary of { string, T } for dictionaries with public parameterless constructors. + /// + /// The reference type for the dictionary. + /// The reference type for values in the dictionary. + /// The dictionary to be translated. + /// The factory used to instantiate values in the dictionary. + public void TranslateDictionary(ref D dictionary, NodePacketValueFactory valueFactory) + where D : IDictionary, new() + where T : class, INodePacketTranslatable + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = dictionary.Count; + _writer.Write(count); + + foreach (KeyValuePair pair in dictionary) + { + string key = pair.Key; + Translate(ref key); + T value = pair.Value; + Translate(ref value, valueFactory); + } + } + + /// + /// Translates a dictionary of { string, T } for dictionaries with public parameterless constructors. + /// + /// The reference type for the dictionary. + /// The reference type for values in the dictionary. + /// The dictionary to be translated. + /// The factory used to instantiate values in the dictionary. + /// The delegate used to instantiate the dictionary. + public void TranslateDictionary(ref D dictionary, NodePacketValueFactory valueFactory, NodePacketDictionaryCreator dictionaryCreator) + where D : IDictionary + where T : class, INodePacketTranslatable + { + if (!TranslateNullable(dictionary)) + { + return; + } + + int count = dictionary.Count; + _writer.Write(count); + + foreach (KeyValuePair pair in dictionary) + { + string key = pair.Key; + Translate(ref key); + T value = pair.Value; + Translate(ref value, valueFactory); + } + } + + /// + /// Writes out the boolean which says if this object is null or not. + /// + /// The object to test. + /// The type of object to test. + /// True if the object should be written, false otherwise. + public bool TranslateNullable(T value) + { + bool haveRef = (value != null); + _writer.Write(haveRef); + return haveRef; + } + } + } +} \ No newline at end of file diff --git a/src/Shared/NodeShutdown.cs b/src/Shared/NodeShutdown.cs new file mode 100644 index 00000000000..745b2502458 --- /dev/null +++ b/src/Shared/NodeShutdown.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Packet describing why a node shut down.. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Reasons why the node shut down. + /// + internal enum NodeShutdownReason + { + /// + /// The node shut down because it was requested to shut down. + /// + Requested, + + /// + /// The node shut down because of an error. + /// + Error, + + /// + /// The node shut down because the connection failed. + /// + ConnectionFailed, + } + + /// + /// Implementation of INodePacket for the packet informing the build manager than a node has shut down. + /// This is the last packet the BuildManager will receive from a Node, and as such can be used to trigger + /// any appropriate cleanup behavior. + /// + internal class NodeShutdown : INodePacket + { + /// + /// The reason the node shut down. + /// + private NodeShutdownReason _reason; + + /// + /// The exception - if any. + /// + private Exception _exception; + + /// + /// Constructor + /// + public NodeShutdown(NodeShutdownReason reason) + : this(reason, null) + { + } + + /// + /// Constructor + /// + public NodeShutdown(NodeShutdownReason reason, Exception e) + { + _reason = reason; + _exception = e; + } + + /// + /// Constructor for deserialization + /// + private NodeShutdown() + { + } + + #region INodePacket Members + + /// + /// Returns the packet type. + /// + public NodePacketType Type + { + get { return NodePacketType.NodeShutdown; } + } + + #endregion + + /// + /// The reason for shutting down. + /// + public NodeShutdownReason Reason + { + get { return _reason; } + } + + /// + /// The exception, if any. + /// + public Exception Exception + { + get { return _exception; } + } + + #region INodePacketTranslatable Members + + /// + /// Serializes or deserializes a packet. + /// + public void Translate(INodePacketTranslator translator) + { + translator.TranslateEnum(ref _reason, (int)_reason); + translator.TranslateDotNet(ref _exception); + } + + /// + /// Factory method for deserialization + /// + internal static NodeShutdown FactoryForDeserialization(INodePacketTranslator translator) + { + NodeShutdown shutdown = new NodeShutdown(); + shutdown.Translate(translator); + return shutdown; + } + + #endregion + } +} diff --git a/src/Shared/OpportunisticIntern.cs b/src/Shared/OpportunisticIntern.cs new file mode 100644 index 00000000000..4ec5cf496d7 --- /dev/null +++ b/src/Shared/OpportunisticIntern.cs @@ -0,0 +1,1162 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Selectively intern strings. +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build +{ + /// + /// This class is used to selectively intern strings. It should be used at the point of new string creation. + /// For example, + /// + /// string interned = OpportunisticIntern.Intern(String.Join(",",someStrings)); + /// + /// This class uses heuristics to decide whether it will be efficient to intern a string or not. There is no + /// guarantee that a string will intern. + /// + /// The thresholds and sizes were determined by experimentation to give the best number of bytes saved + /// at reasonable elapsed time cost. + /// + static internal class OpportunisticIntern + { + /// + /// The size of the small mru list. + /// + private static readonly int s_smallMruSize = AssignViaEnvironment("MSBUILDSMALLINTERNSIZE", 50); + + /// + /// The size of the large mru list. + /// + private static readonly int s_largeMruSize = AssignViaEnvironment("MSBUILDLARGEINTERNSIZE", 100); + + /// + /// The size of the huge mru list. + /// + private static readonly int s_hugeMruSize = AssignViaEnvironment("MSBUILDHUGEINTERNSIZE", 100); + + /// + /// The smallest size a string can be to be considered small. + /// + private static readonly int s_smallMruThreshhold = AssignViaEnvironment("MSBUILDSMALLINTERNTHRESHOLD", 50); + + /// + /// The smallest size a string can be to be considered large. + /// + private static readonly int s_largeMruThreshhold = AssignViaEnvironment("MSBUILDLARGEINTERNTHRESHOLD", 70); + + /// + /// The smallest size a string can be to be considered huge. + /// + private static readonly int s_hugeMruThreshhold = AssignViaEnvironment("MSBUILDHUGEINTERNTHRESHOLD", 200); + + /// + /// The smallest size a string can be to be ginormous. + /// 8K for large object heap. + /// + private static readonly int s_ginormousThreshhold = AssignViaEnvironment("MSBUILDGINORMOUSINTERNTHRESHOLD", 8000); + + /// + /// Manages the separate MRU lists. + /// + private static BucketedPrioritizedStringList s_si = new BucketedPrioritizedStringList(/*gatherStatistics*/ false, s_smallMruSize, s_largeMruSize, s_hugeMruSize, s_smallMruThreshhold, s_largeMruThreshhold, s_hugeMruThreshhold, s_ginormousThreshhold); + + #region Statistics + /// + /// What if Mru lists were infinitely long? + /// + private static BucketedPrioritizedStringList s_whatIfInfinite; + + /// + /// What if we doubled the size of the Mru lists? + /// + private static BucketedPrioritizedStringList s_whatIfDoubled; + + /// + /// What if we halved the size of the Mru lists? + /// + private static BucketedPrioritizedStringList s_whatIfHalved; + + /// + /// What if the size of Mru lists was zero? (We still intern tiny strings in this case) + /// + private static BucketedPrioritizedStringList s_whatIfZero; + #endregion + + #region IInternable + /// + /// Define the methods needed to intern something. + /// + internal interface IInternable + { + /// + /// The length of the target. + /// + int Length { get; } + + /// + /// Indexer into the target. Presumed to be fast. + /// + char this[int index] + { + get; + } + + /// + /// Convert target to string. Presumed to be slow (and will be called just once). + /// + string ExpensiveConvertToString(); + + /// + /// Compare target to string. Assumes lengths are equal. + /// + bool IsOrdinalEqualToStringOfSameLength(string other); + + /// + /// Reference compare target to string. If target is non-string this should return false. + /// + bool ReferenceEquals(string other); + } + #endregion + + /// + /// Assign an int from an environment variable. If its not present, use the default. + /// + static internal int AssignViaEnvironment(string env, int @default) + { + string threshhold = Environment.GetEnvironmentVariable(env); + if (!String.IsNullOrEmpty(threshhold)) + { + int result; + if (Int32.TryParse(threshhold, out result)) + { + return result; + } + } + + return @default; + } + + /// + /// Turn on statistics gathering. + /// + internal static void EnableStatisticsGathering() + { + // Statistics include several 'what if' scenarios such as doubling the size of the MRU lists. + s_si = new BucketedPrioritizedStringList(/*gatherStatistics*/ true, s_smallMruSize, s_largeMruSize, s_hugeMruSize, s_smallMruThreshhold, s_largeMruThreshhold, s_hugeMruThreshhold, s_ginormousThreshhold); + s_whatIfInfinite = new BucketedPrioritizedStringList(/*gatherStatistics*/ true, Int32.MaxValue, Int32.MaxValue, Int32.MaxValue, s_smallMruThreshhold, s_largeMruThreshhold, s_hugeMruThreshhold, s_ginormousThreshhold); + s_whatIfDoubled = new BucketedPrioritizedStringList(/*gatherStatistics*/ true, s_smallMruSize * 2, s_largeMruSize * 2, s_hugeMruSize * 2, s_smallMruThreshhold, s_largeMruThreshhold, s_hugeMruThreshhold, s_ginormousThreshhold); + s_whatIfHalved = new BucketedPrioritizedStringList(/*gatherStatistics*/ true, s_smallMruSize / 2, s_largeMruSize / 2, s_hugeMruSize / 2, s_smallMruThreshhold, s_largeMruThreshhold, s_hugeMruThreshhold, s_ginormousThreshhold); + s_whatIfZero = new BucketedPrioritizedStringList(/*gatherStatistics*/ true, 0, 0, 0, s_smallMruThreshhold, s_largeMruThreshhold, s_hugeMruThreshhold, s_ginormousThreshhold); + } + + /// + /// Intern the given internable. + /// + internal static string InternableToString(IInternable candidate) + { + if (s_whatIfInfinite != null) + { + s_whatIfInfinite.InterningToString(candidate); + s_whatIfDoubled.InterningToString(candidate); + s_whatIfHalved.InterningToString(candidate); + s_whatIfZero.InterningToString(candidate); + } + + string result = s_si.InterningToString(candidate); +#if _DEBUG + string expected = candidate.ExpensiveConvertToString(); + if (!String.Equals(result, expected)) + { + ErrorUtilities.ThrowInternalError("Interned string {0} should have been {1}", result, expected); + } +#endif + return result; + } + + /// + /// Potentially Intern the given string builder. + /// + internal static string StringBuilderToString(StringBuilder candidate) + { + return InternableToString(new StringBuilderInternTarget(candidate)); + } + + /// + /// Potentially Intern the given char array. + /// + internal static string CharArrayToString(char[] candidate, int count) + { + return InternableToString(new CharArrayInternTarget(candidate, count)); + } + + /// + /// Potentially Intern the given char array. + /// + internal static string CharArrayToString(char[] candidate, int startIndex, int count) + { + return InternableToString(new CharArrayInternTarget(candidate, startIndex, count)); + } + + /// + /// Potentially Intern the given string. + /// + /// The string to intern. + /// The interned string, or the same string if it could not be interned. + internal static string InternStringIfPossible(string candidate) + { + return InternableToString(new StringInternTarget(candidate)); + } + + /// + /// Report statistics about interning. Don't call unless GatherStatistics has been called beforehand. + /// + internal static void ReportStatistics() + { + s_si.ReportStatistics("Main"); + s_whatIfInfinite.ReportStatistics("if Infinite"); + s_whatIfDoubled.ReportStatistics("if Doubled"); + s_whatIfHalved.ReportStatistics("if Halved"); + s_whatIfZero.ReportStatistics("if Zero"); + Console.WriteLine(" * Even for MRU size of zero there will still be some intern hits because of the tiny "); + Console.WriteLine(" string matching (eg. 'true')"); + } + + #region IInternable Implementations + /// + /// A wrapper over StringBuilder. + /// + internal struct StringBuilderInternTarget : IInternable + { + /// + /// The held StringBuilder + /// + private StringBuilder _target; + + /// + /// Pointless comment about constructor. + /// + internal StringBuilderInternTarget(StringBuilder target) + { + _target = target; + } + + /// + /// The length of the target. + /// + public int Length + { + get + { + return _target.Length; + } + } + + /// + /// Indexer into the target. Presumed to be fast. + /// + public char this[int index] + { + get + { + return _target[index]; + } + } + + /// + /// Never reference equals to string. + /// + public bool ReferenceEquals(string other) + { + return false; + } + + /// + /// Convert target to string. Presumed to be slow (and will be called just once). + /// + public string ExpensiveConvertToString() + { + // PERF NOTE: This will be an allocation hot-spot because the StringBuilder is finally determined to + // not be internable. There is still only one conversion of StringBuilder into string it has just + // moved into this single spot. + return _target.ToString(); + } + + /// + /// Compare target to string. Assumes lengths are equal. + /// + public bool IsOrdinalEqualToStringOfSameLength(string other) + { +#if DEBUG + ErrorUtilities.VerifyThrow(other.Length == _target.Length, "should be same length"); +#endif + int length = _target.Length; + + // Backwards because the end of the string is (by observation of Australian Government build) more likely to be different earlier in the loop. + // For example, C:\project1, C:\project2 + for (int i = length - 1; i >= 0; --i) + { + if (_target[i] != other[i]) + { + return false; + } + } + + return true; + } + + /// + /// Don't use this function. Use ExpensiveConvertToString + /// + public override string ToString() + { + throw new InvalidOperationException(); + } + } + + /// + /// A wrapper over char[]. + /// + internal struct CharArrayInternTarget : IInternable + { + /// + /// Start index for the string + /// + private int _startIndex; + + /// + /// Number of characters. + /// + private int _count; + + /// + /// The held array + /// + private char[] _target; + + /// + /// Pointless comment about constructor. + /// + internal CharArrayInternTarget(char[] target, int count) + : this(target, 0, count) + { + } + + /// + /// Pointless comment about constructor. + /// + internal CharArrayInternTarget(char[] target, int startIndex, int count) + { +#if DEBUG + if (startIndex + count > target.Length) + { + ErrorUtilities.ThrowInternalError("wrong length"); + } +#endif + _target = target; + _startIndex = startIndex; + _count = count; + } + + /// + /// The length of the target. + /// + public int Length + { + get + { + return _count; + } + } + + /// + /// Indexer into the target. Presumed to be fast. + /// + public char this[int index] + { + get + { + if (index > _startIndex + _count - 1 || index < 0) + { + ErrorUtilities.ThrowInternalError("past end"); + } + + return _target[index + _startIndex]; + } + } + + /// + /// Convert target to string. Presumed to be slow (and will be called just once). + /// + public bool ReferenceEquals(string other) + { + return false; + } + + /// + /// Convert target to string. Presumed to be slow (and will be called just once). + /// + public string ExpensiveConvertToString() + { + // PERF NOTE: This will be an allocation hot-spot because the char[] is finally determined to + // not be internable. There is still only one conversion of char[] into string it has just + // moved into this single spot. + return new String(_target, _startIndex, _count); + } + + /// + /// Compare target to string. Assumes lengths are equal. + /// + public bool IsOrdinalEqualToStringOfSameLength(string other) + { +#if DEBUG + ErrorUtilities.VerifyThrow(other.Length == this.Length, "should be same length"); +#endif + // Backwards because the end of the string is (by observation of Australian Government build) more likely to be different earlier in the loop. + // For example, C:\project1, C:\project2 + for (int i = _count - 1; i >= 0; --i) + { + if (_target[i + _startIndex] != other[i]) + { + return false; + } + } + + return true; + } + + /// + /// Don't use this function. Use ExpensiveConvertToString + /// + public override string ToString() + { + throw new InvalidOperationException(); + } + } + + /// + /// Wrapper over a string. + /// + internal struct StringInternTarget : IInternable + { + /// + /// Stores the wrapped string. + /// + private string _target; + + /// + /// Constructor of the class + /// + /// The string to wrap + internal StringInternTarget(string target) + { + ErrorUtilities.VerifyThrowArgumentLength(target, "target"); + _target = target; + } + + /// + /// Gets the length of the target string. + /// + public int Length + { + get { return _target.Length; } + } + + /// + /// Gets the n character in the target string. + /// + /// Index of the character to gather. + /// The character in the position marked by index. + public char this[int index] + { + get { return _target[index]; } + } + + /// + /// Returs the target which is already a string. + /// + /// The target string. + public string ExpensiveConvertToString() + { + return _target; + } + + /// + /// Compare if the target string is equal to the given string. + /// + /// The string to compare with the target. + /// True if the strings are equal, false otherwise. + public bool IsOrdinalEqualToStringOfSameLength(string other) + { + return _target.Equals(other, StringComparison.Ordinal); + } + + /// + /// Verifies if the reference of the target string is the same of the given string. + /// + /// The string reference to compare to. + /// True if both references are equal, false otherwise. + public bool ReferenceEquals(string other) + { + return Object.ReferenceEquals(_target, other); + } + } + + #endregion + + /// + /// Manages a set of mru lists that hold strings in varying size ranges. + /// + private class BucketedPrioritizedStringList + { + /// + /// The small string Mru list. + /// + private PrioritizedStringList _smallMru; + + /// + /// The large string Mru list. + /// + private PrioritizedStringList _largeMru; + + /// + /// The huge string Mru list. + /// + private PrioritizedStringList _hugeMru; + + /// + /// Three most recently used strings over 8K. + /// + private LinkedList _ginormous = new LinkedList(); + + /// + /// The smallest size a string can be to be considered small. + /// + private int _smallMruThreshhold; + + /// + /// The smallest size a string can be to be considered large. + /// + private int _largeMruThreshhold; + + /// + /// The smallest size a string can be to be considered huge. + /// + private int _hugeMruThreshhold; + + /// + /// The smallest size a string can be to be ginormous. + /// + private int _ginormousThreshhold; + + #region Statistics + /// + /// Whether or not to gather statistics + /// + private bool _gatherStatistics = false; + + /// + /// Number of times interning worked. + /// + private int _internHits; + + /// + /// Number of times interning didn't work. + /// + private int _internMisses; + + /// + /// Number of times interning wasn't attempted. + /// + private int _internRejects; + + /// + /// Total number of strings eliminated by interning. + /// + private int _internEliminatedStrings; + + /// + /// Total number of chars eliminated across all strings. + /// + private int _internEliminatedChars; + + /// + /// Number of times the ginourmous string hit. + /// + private int _ginormousHits; + + /// + /// Number of times the ginourmous string missed. + /// + private int _ginormousMisses; + + /// + /// Chars interned for ginormous range. + /// + private int _ginormousCharsSaved; + + /// + /// Whether or not to track ginormous strings. + /// + private bool _dontTrack; + + /// + /// The time spent interning. + /// + private Stopwatch _stopwatch; + + /// + /// Strings which did not intern + /// + private Dictionary _missedStrings; + + /// + /// Strings which we didn't attempt to intern + /// + private Dictionary _rejectedStrings; + + /// + /// Number of ginormous strings to keep + /// By observation of Auto7, there are about three variations of the huge solution config blob + /// There aren't really any other strings of this size, but make it 10 to be sure. (There will barely be any misses) + /// + private int _ginormousSize = 10; + + #endregion + + /// + /// Construct. + /// + internal BucketedPrioritizedStringList(bool gatherStatistics, int smallMruSize, int largeMruSize, int hugeMruSize, int smallMruThreshhold, int largeMruThreshhold, int hugeMruThreshhold, int ginormousThreshhold) + { + if (smallMruSize == 0 && largeMruSize == 0 && hugeMruSize == 0) + { + _dontTrack = true; + } + + _smallMru = new PrioritizedStringList(smallMruSize); + _largeMru = new PrioritizedStringList(largeMruSize); + _hugeMru = new PrioritizedStringList(hugeMruSize); + _smallMruThreshhold = smallMruThreshhold; + _largeMruThreshhold = largeMruThreshhold; + _hugeMruThreshhold = hugeMruThreshhold; + _ginormousThreshhold = ginormousThreshhold; + + for (int i = 0; i < _ginormousSize; i++) + { + _ginormous.AddFirst(new WeakReference(String.Empty)); + } + + _gatherStatistics = gatherStatistics; + if (gatherStatistics) + { + _stopwatch = new Stopwatch(); + _missedStrings = new Dictionary(StringComparer.Ordinal); + _rejectedStrings = new Dictionary(StringComparer.Ordinal); + } + } + + /// + /// Intern the given internable. + /// + internal string InterningToString(IInternable candidate) + { + if (candidate.Length == 0) + { + // As in the case that a property or itemlist has evaluated to empty. + return String.Empty; + } + + if (_gatherStatistics) + { + return InternWithStatistics(candidate); + } + else + { + string result; + TryIntern(candidate, out result); + return result; + } + } + + /// + /// Report statistics to the console. + /// + internal void ReportStatistics(string heading) + { + string title = "Opportunistic Intern (" + heading + ")"; + Console.WriteLine("\n{0}{1}{0}", new String('=', 41 - (title.Length / 2)), title); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Intern Hits", _internHits, "hits"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Intern Misses", _internMisses, "misses"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Intern Rejects (as shorter than " + s_smallMruThreshhold + " bytes)", _internRejects, "rejects"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Strings*", _internEliminatedStrings, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Chars", _internEliminatedChars, "chars"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Estimated Eliminated Bytes", _internEliminatedChars * 2, "bytes"); + Console.WriteLine("Elimination assumes that strings provided were unique objects."); + Console.WriteLine("|---------------------------------------------------------------------------------|"); + KeyValuePair held = _smallMru.Statistics(); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Small Strings MRU Size", s_smallMruSize, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Small Strings (>=" + _smallMruThreshhold + " chars) Held", held.Key, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Small Estimated Bytes Held", held.Value * 2, "bytes"); + Console.WriteLine("|---------------------------------------------------------------------------------|"); + held = _largeMru.Statistics(); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Large Strings MRU Size", s_largeMruSize, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Large Strings (>=" + _largeMruThreshhold + " chars) Held", held.Key, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Large Estimated Bytes Held", held.Value * 2, "bytes"); + Console.WriteLine("|---------------------------------------------------------------------------------|"); + held = _hugeMru.Statistics(); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Huge Strings MRU Size", s_hugeMruSize, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Huge Strings (>=" + _hugeMruThreshhold + " chars) Held", held.Key, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Huge Estimated Bytes Held", held.Value * 2, "bytes"); + Console.WriteLine("|---------------------------------------------------------------------------------|"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous Strings MRU Size", _ginormousSize, "strings"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous (>=" + _ginormousThreshhold + " chars) Hits", _ginormousHits, "hits"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous Misses", _ginormousMisses, "misses"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous Chars Saved", _ginormousCharsSaved, "chars"); + Console.WriteLine("|---------------------------------------------------------------------------------|"); + + // There's no point in reporting the ginormous string because it will have evaporated by now. + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Time Spent Interning", _stopwatch.ElapsedMilliseconds, "ms"); + Console.WriteLine("{0}{0}", new String('=', 41)); + + var topMissingString = + _missedStrings + .OrderByDescending(kv => kv.Value * kv.Key.Length) + .Take(15) + .Where(kv => kv.Value > 1) + .Select(kv => String.Format(CultureInfo.InvariantCulture, "({1} instances x each {2} chars = {3}KB wasted)\n{0}", kv.Key, kv.Value, kv.Key.Length, (kv.Value - 1) * kv.Key.Length * 2 / 1024)); + + Console.WriteLine("##########Top Missed Strings: \n{0} ", String.Join("\n==============\n", topMissingString.ToArray())); + Console.WriteLine(); + + var topRejectedString = + _rejectedStrings + .OrderByDescending(kv => kv.Value * kv.Key.Length) + .Take(15) + .Where(kv => kv.Value > 1) + .Select(kv => String.Format(CultureInfo.InvariantCulture, "({1} instances x each {2} chars = {3}KB wasted)\n{0}", kv.Key, kv.Value, kv.Key.Length, (kv.Value - 1) * kv.Key.Length * 2 / 1024)); + + Console.WriteLine("##########Top Rejected Strings: \n{0} ", String.Join("\n==============\n", topRejectedString.ToArray())); + } + + /// + /// Try to intern the string. + /// Return true if an interned value could be returned. + /// Return false if it was added to the intern list, but wasn't there already. + /// Return null if it didn't meet the length criteria for any of the buckets. + /// + private bool? TryIntern(IInternable candidate, out string interned) + { + int length = candidate.Length; + + // First, try the hard coded intern strings. + // Each of the hard-coded small strings below showed up in a profile run with considerable duplication in memory. + if (!_dontTrack) + { + if (length == 2) + { + if (candidate[1] == '#') + { + if (candidate[0] == 'C') + { + interned = "C#"; + return true; + } + + if (candidate[0] == 'F') + { + interned = "F#"; + return true; + } + } + + if (candidate[0] == 'V' && candidate[1] == 'B') + { + interned = "VB"; + return true; + } + } + else if (length == 4) + { + if (candidate[0] == 'T') + { + if (candidate[1] == 'R' && candidate[2] == 'U' && candidate[3] == 'E') + { + interned = "TRUE"; + return true; + } + + if (candidate[1] == 'r' && candidate[2] == 'u' && candidate[3] == 'e') + { + interned = "True"; + return true; + } + } + + if (candidate[0] == 'C' && candidate[1] == 'o' && candidate[2] == 'p' && candidate[3] == 'y') + { + interned = "Copy"; + return true; + } + + if (candidate[0] == 't' && candidate[1] == 'r' && candidate[2] == 'u' && candidate[3] == 'e') + { + interned = "true"; + return true; + } + + if (candidate[0] == 'v' && candidate[1] == '4' && candidate[2] == '.' && candidate[3] == '0') + { + interned = "v4.0"; + return true; + } + } + else if (length == 5) + { + if (candidate[0] == 'F' && candidate[1] == 'A' && candidate[2] == 'L' && candidate[3] == 'S' && candidate[4] == 'E') + { + interned = "FALSE"; + return true; + } + + if (candidate[0] == 'f' && candidate[1] == 'a' && candidate[2] == 'l' && candidate[3] == 's' && candidate[4] == 'e') + { + interned = "false"; + return true; + } + + if (candidate[0] == 'D' && candidate[1] == 'e' && candidate[2] == 'b' && candidate[3] == 'u' && candidate[4] == 'g') + { + interned = "Debug"; + return true; + } + + if (candidate[0] == 'B' && candidate[1] == 'u' && candidate[2] == 'i' && candidate[3] == 'l' && candidate[4] == 'd') + { + interned = "Build"; + return true; + } + + if (candidate[0] == 'W' && candidate[1] == 'i' && candidate[2] == 'n' && candidate[3] == '3' && candidate[4] == '2') + { + interned = "Win32"; + return true; + } + } + else if (length == 6) + { + if (candidate[0] == '\'' && candidate[1] == '\'' && candidate[2] == '!' && candidate[3] == '=' && candidate[4] == '\'' && candidate[5] == '\'') + { + interned = "''!=''"; + return true; + } + + if (candidate[0] == 'A' && candidate[1] == 'n' && candidate[2] == 'y' && candidate[3] == 'C' && candidate[4] == 'P' && candidate[5] == 'U') + { + interned = "AnyCPU"; + return true; + } + } + else if (length == 7) + { + if (candidate[0] == 'L' && candidate[1] == 'i' && candidate[2] == 'b' && candidate[3] == 'r' && candidate[4] == 'a' && candidate[5] == 'r' && candidate[6] == 'y') + { + interned = "Library"; + return true; + } + + if (candidate[0] == 'M' && candidate[1] == 'S' && candidate[2] == 'B' && candidate[3] == 'u' && candidate[4] == 'i' && candidate[5] == 'l' && candidate[6] == 'd') + { + interned = "MSBuild"; + return true; + } + + if (candidate[0] == 'R' && candidate[1] == 'e' && candidate[2] == 'l' && candidate[3] == 'e' && candidate[4] == 'a' && candidate[5] == 's' && candidate[6] == 'e') + { + interned = "Release"; + return true; + } + } + else if (length == 24) + { + if (candidate[0] == 'R' && candidate[1] == 'e' && candidate[2] == 's' && candidate[3] == 'o' && candidate[4] == 'l' && candidate[5] == 'v' && candidate[6] == 'e') + { + if (candidate[7] == 'A' && candidate[8] == 's' && candidate[9] == 's' && candidate[10] == 'e' && candidate[11] == 'm' && candidate[12] == 'b' && candidate[13] == 'l' && candidate[14] == 'y') + { + if (candidate[15] == 'R' && candidate[16] == 'e' && candidate[17] == 'f' && candidate[18] == 'e' && candidate[19] == 'r' && candidate[20] == 'e' && candidate[21] == 'n' && candidate[22] == 'c' && candidate[23] == 'e') + { + interned = "ResolveAssemblyReference"; + return true; + } + } + } + } + else if (length > _ginormousThreshhold) + { + lock (_ginormous) + { + LinkedListNode current = _ginormous.First; + + while (current != null) + { + string last = current.Value.Target as string; + if (last != null && last.Length == candidate.Length && candidate.IsOrdinalEqualToStringOfSameLength(last)) + { + interned = last; + _ginormousHits++; + _ginormousCharsSaved += last.Length; + + _ginormous.Remove(current); + _ginormous.AddFirst(current); + + return true; + } + + current = current.Next; + } + + _ginormousMisses++; + interned = candidate.ExpensiveConvertToString(); + + var lastNode = _ginormous.Last; + _ginormous.RemoveLast(); + _ginormous.AddFirst(lastNode); + lastNode.Value.Target = interned; + + return false; + } + } + else if (length >= _hugeMruThreshhold) + { + lock (_hugeMru) + { + return _hugeMru.TryGet(candidate, out interned); + } + } + else if (length >= _largeMruThreshhold) + { + lock (_largeMru) + { + return _largeMru.TryGet(candidate, out interned); + } + } + else if (length >= _smallMruThreshhold) + { + lock (_smallMru) + { + return _smallMru.TryGet(candidate, out interned); + } + } + } + + interned = candidate.ExpensiveConvertToString(); + return null; + } + + /// + /// Version of Intern that gathers statistics + /// + private string InternWithStatistics(IInternable candidate) + { + string result; + _stopwatch.Start(); + bool? interned = TryIntern(candidate, out result); + _stopwatch.Stop(); + + if (interned.HasValue && !interned.Value) + { + // Could not intern. + _internMisses++; + + int priorCount = 0; + _missedStrings.TryGetValue(result, out priorCount); + _missedStrings[result] = priorCount + 1; + + return result; + } + else if (interned == null) + { + // Decided not to attempt interning + _internRejects++; + + int priorCount = 0; + _rejectedStrings.TryGetValue(result, out priorCount); + _rejectedStrings[result] = priorCount + 1; + + return result; + } + + _internHits++; + if (!candidate.ReferenceEquals(result)) + { + // Reference changed so 'candidate' is now released and should save memory. + _internEliminatedStrings++; + _internEliminatedChars += candidate.Length; + } + + return result; + } + + /// + /// A singly linked list of strings where the most recently accessed string is at the top. + /// Size expands up to a fixed number of strings. + /// + private class PrioritizedStringList + { + /// + /// Maximum size of the mru list. + /// + private readonly int _size; + + /// + /// Head of the mru list. + /// + private Node _mru; + + /// + /// Construct an Mru list with a fixed maximum size. + /// + internal PrioritizedStringList(int size) + { + _size = size; + } + + /// + /// Try to get one element from the list. Upon leaving the function 'candidate' will be at the head of the Mru list. + /// This function is not thread-safe. + /// + internal bool TryGet(IInternable candidate, out string interned) + { + if (_size == 0) + { + interned = candidate.ExpensiveConvertToString(); + return false; + } + + int length = candidate.Length; + Node secondPrior = null; + Node prior = null; + Node head = _mru; + bool found = false; + int itemCount = 0; + + while (head != null && !found) + { + if (head.Value.Length == length) + { + if (candidate.IsOrdinalEqualToStringOfSameLength(head.Value)) + { + found = true; + } + } + + if (!found) + { + secondPrior = prior; + prior = head; + head = head.Next; + } + + itemCount++; + } + + if (found) + { + // Move it to the top and return the interned version. + if (prior != null) + { + if (!candidate.ReferenceEquals(head.Value)) + { + // Wasn't at the top already, so move it there. + prior.Next = head.Next; + head.Next = _mru; + _mru = head; + interned = _mru.Value; + return true; + } + else + { + // But don't move it up if there is reference equality so that multiple calls to Intern don't redundantly emphasize a string. + interned = head.Value; + return true; + } + } + else + { + // Found the item in the top spot. No need to move anything. + interned = _mru.Value; + return true; + } + } + else + { + // Not found. Create a new entry and place it at the top. + Node old = _mru; + _mru = new Node(candidate.ExpensiveConvertToString()); + _mru.Next = old; + + // Cache miss. Use this opportunity to discard any element over the max size. + if (itemCount >= _size && secondPrior != null) + { + secondPrior.Next = null; + } + + interned = _mru.Value; + return false; + } + } + + /// + /// Returns the number of strings held and the total number of chars held. + /// + internal KeyValuePair Statistics() + { + Node head = _mru; + int chars = 0; + int strings = 0; + while (head != null) + { + chars += head.Value.Length; + strings++; + head = head.Next; + } + + return new KeyValuePair(strings, chars); + } + + /// + /// Singly linked list node. + /// + private class Node + { + /// + /// Construct a Node + /// + internal Node(string value) + { + Value = value; + } + + /// + /// The next node in the list. + /// + internal Node Next { get; set; } + + /// + /// The held string. + /// + internal string Value { get; private set; } + } + } + } + } +} diff --git a/src/Shared/OutOfProcTaskHostTaskResult.cs b/src/Shared/OutOfProcTaskHostTaskResult.cs new file mode 100644 index 00000000000..ced32befe84 --- /dev/null +++ b/src/Shared/OutOfProcTaskHostTaskResult.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The class representing a result from running a task in the +// out-of-proc task host. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.Shared +{ + /// + /// A result of executing a target or task. + /// + internal class OutOfProcTaskHostTaskResult + { + /// + /// Constructor + /// + internal OutOfProcTaskHostTaskResult(TaskCompleteType result) + : this(result, null /* no final parameters */, null /* no exception */, null /* no exception message */, null /* and no args to go with it */) + { + // do nothing else + } + + /// + /// Constructor + /// + internal OutOfProcTaskHostTaskResult(TaskCompleteType result, IDictionary finalParams) + : this(result, finalParams, null /* no exception */, null /* no exception message */, null /* and no args to go with it */) + { + // do nothing else + } + + /// + /// Constructor + /// + internal OutOfProcTaskHostTaskResult(TaskCompleteType result, Exception taskException) + : this(result, taskException, null /* no exception message */, null /* and no args to go with it */) + { + // do nothing else + } + + /// + /// Constructor + /// + internal OutOfProcTaskHostTaskResult(TaskCompleteType result, Exception taskException, string exceptionMessage, string[] exceptionMessageArgs) + : this(result, null /* no final parameters */, taskException, exceptionMessage, exceptionMessageArgs) + { + // do nothing else + } + + /// + /// Constructor + /// + internal OutOfProcTaskHostTaskResult(TaskCompleteType result, IDictionary finalParams, Exception taskException, string exceptionMessage, string[] exceptionMessageArgs) + { + // If we're returning a crashing result, we should always also be returning the exception that caused the crash, although + // we may not always be returning an accompanying message. + if (result == TaskCompleteType.CrashedDuringInitialization || + result == TaskCompleteType.CrashedDuringExecution || + result == TaskCompleteType.CrashedAfterExecution) + { + ErrorUtilities.VerifyThrowInternalNull(taskException, "taskException"); + } + + if (exceptionMessage != null) + { + ErrorUtilities.VerifyThrow + ( + result == TaskCompleteType.CrashedDuringInitialization || + result == TaskCompleteType.CrashedDuringExecution || + result == TaskCompleteType.CrashedAfterExecution, + "If we have an exception message, the result type should be 'crashed' of some variety." + ); + } + + if (exceptionMessageArgs != null && exceptionMessageArgs.Length > 0) + { + ErrorUtilities.VerifyThrow(exceptionMessage != null, "If we have message args, we need a message."); + } + + Result = result; + FinalParameterValues = finalParams; + TaskException = taskException; + ExceptionMessage = exceptionMessage; + ExceptionMessageArgs = exceptionMessageArgs; + } + + /// + /// The overall result of the task execution. + /// + public TaskCompleteType Result + { + get; + private set; + } + + /// + /// Dictionary of the final values of the task parameters + /// + public IDictionary FinalParameterValues + { + get; + private set; + } + + /// + /// The exception thrown by the task during initialization or execution, + /// if any. + /// + public Exception TaskException + { + get; + private set; + } + + /// + /// The name of the resource representing the message to be logged along with the + /// above exception. + /// + public string ExceptionMessage + { + get; + private set; + } + + /// + /// The arguments to be used when formatting ExceptionMessage + /// + public string[] ExceptionMessageArgs + { + get; + private set; + } + } +} diff --git a/src/Shared/Pair.cs b/src/Shared/Pair.cs new file mode 100644 index 00000000000..1e09e27ab3d --- /dev/null +++ b/src/Shared/Pair.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A hack to prevent certain cases of Jitting in our NGen'd assemblies. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Shared +{ + /// + /// This struct is functionally identical to KeyValuePair, but avoids + /// CA908 warnings (types that in ngen images that will JIT). + /// Instead of generic collections of KeyValuePair, use Pair. + /// + /// + /// This trick is based on advice from + /// http://sharepoint/sites/codeanalysis/Wiki%20Pages/Rule%20-%20Avoid%20Types%20That%20Require%20JIT%20Compilation%20In%20Precompiled%20Assemblies.aspx. + /// It works because although this is a value type, it is not defined in mscorlib. + /// + /// Key + /// Value + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Not possible as Equals cannot be implemented on the struct members")] + internal struct Pair + { + /// + /// Key + /// + private TKey _key; + + /// + /// Value + /// + private TValue _value; + + /// + /// Constructor + /// + public Pair(TKey key, TValue value) + { + _key = key; + _value = value; + } + + /// + /// Key + /// + internal TKey Key + { + get { return _key; } + } + + /// + /// Value + /// + internal TValue Value + { + get { return _value; } + } + } +} \ No newline at end of file diff --git a/src/Shared/ProjectErrorUtilities.cs b/src/Shared/ProjectErrorUtilities.cs new file mode 100644 index 00000000000..7dc18b4c380 --- /dev/null +++ b/src/Shared/ProjectErrorUtilities.cs @@ -0,0 +1,439 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Xml; + +/****************************************************************************** + * + * !! WARNING !! + * + * This class depends on the build engine assembly! Do not share this class + * into any assembly that is not supposed to take a dependency on the build + * engine assembly! + * + * + ******************************************************************************/ +using Microsoft.Build.Debugging; +using Microsoft.Build.Evaluation; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains methods that are useful for error checking and + /// validation of project files. + /// + /// + /// FUTURE: This class could except an optional inner exception to put in the + /// InvalidProjectFileException, which could make debugging a host easier in some circumstances. + /// + internal static class ProjectErrorUtilities + { + /// + /// This method is used to flag errors in the project file being processed. + /// Do NOT use this method in place of ErrorUtilities.VerifyThrow(), because + /// ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// The condition to check. + /// The invalid project node (can be null). + /// The resource string for the error message. + internal static void VerifyThrowInvalidProject + ( + bool condition, + IElementLocation elementLocation, + string resourceName + ) + { + VerifyThrowInvalidProject(condition, null, elementLocation, resourceName); + } + + /// + /// Overload for one string format argument. + /// + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + internal static void ThrowInvalidProject + ( + IElementLocation elementLocation, + string resourceName, + object arg0 + ) + { + VerifyThrowInvalidProject(false, null, elementLocation, resourceName, arg0); + } + + /// + /// Overload for one string format argument. + /// + /// The condition to check. + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + IElementLocation elementLocation, + string resourceName, + object arg0 + ) + { + VerifyThrowInvalidProject(condition, null, elementLocation, resourceName, arg0); + } + + /// + /// Overload for two string format arguments. + /// + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + internal static void ThrowInvalidProject + ( + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1 + ) + { + VerifyThrowInvalidProject(false, null, elementLocation, resourceName, arg0, arg1); + } + + /// + /// Overload for three string format arguments. + /// + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + /// + internal static void ThrowInvalidProject + ( + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1, + object arg2 + ) + { + VerifyThrowInvalidProject(false, null, elementLocation, resourceName, arg0, arg1, arg2); + } + + /// + /// Overload for four string format arguments. + /// + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + /// + /// + internal static void ThrowInvalidProject + ( + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1, + object arg2, + object arg3 + ) + { + VerifyThrowInvalidProject(false, null, elementLocation, resourceName, arg0, arg1, arg2, arg3); + } + + /// + /// Overload for if there are more than four string format arguments. + /// + /// The invalid project node (can be null). + /// The resource string for the error message. + internal static void ThrowInvalidProject + ( + IElementLocation elementLocation, + string resourceName, + params object[] args + ) + { + ThrowInvalidProject(null, elementLocation, resourceName, args); + } + + /// + /// Overload for two string format arguments. + /// + /// The condition to check. + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1 + ) + { + VerifyThrowInvalidProject(condition, null, elementLocation, resourceName, arg0, arg1); + } + + /// + /// Overload for three string format arguments. + /// + /// The condition to check. + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1, + object arg2 + ) + { + VerifyThrowInvalidProject(condition, null, elementLocation, resourceName, arg0, arg1, arg2); + } + + /// + /// Overload for four string format arguments. + /// + /// The condition to check. + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + /// + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1, + object arg2, + object arg3 + ) + { + VerifyThrowInvalidProject(condition, null, elementLocation, resourceName, arg0, arg1, arg2, arg3); + } + + /// + /// This method is used to flag errors in the project file being processed. + /// Do NOT use this method in place of ErrorUtilities.VerifyThrow(), because + /// ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// The condition to check. + /// The resource string for the + /// error sub-category (can be null). + /// The invalid project node (can be null). + /// The resource string for the error message. + internal static void VerifyThrowInvalidProject + ( + bool condition, + string errorSubCategoryResourceName, + IElementLocation elementLocation, + string resourceName + ) + { + if (!condition) + { + // PERF NOTE: explicitly passing null for the arguments array + // prevents memory allocation + ThrowInvalidProject(errorSubCategoryResourceName, elementLocation, resourceName, null); + } + } + + /// + /// Overload for one string format argument. + /// + /// The condition to check. + /// The resource string for the + /// error sub-category (can be null). + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + string errorSubCategoryResourceName, + IElementLocation elementLocation, + string resourceName, + object arg0 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidProject() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidProject(errorSubCategoryResourceName, elementLocation, resourceName, arg0); + } + } + + /// + /// Overload for two string format arguments. + /// + /// The condition to check. + /// The resource string for the + /// error sub-category (can be null). + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + string errorSubCategoryResourceName, + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidProject() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidProject(errorSubCategoryResourceName, elementLocation, resourceName, arg0, arg1); + } + } + + /// + /// Overload for three string format arguments. + /// + /// The condition to check. + /// The resource string for the + /// error sub-category (can be null). + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + string errorSubCategoryResourceName, + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1, + object arg2 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidProject() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidProject(errorSubCategoryResourceName, elementLocation, resourceName, arg0, arg1, arg2); + } + } + + /// + /// Overload for four string format arguments. + /// + /// The condition to check. + /// The resource string for the + /// error sub-category (can be null). + /// The invalid project node (can be null). + /// The resource string for the error message. + /// + /// + /// + /// + internal static void VerifyThrowInvalidProject + ( + bool condition, + string errorSubCategoryResourceName, + IElementLocation elementLocation, + string resourceName, + object arg0, + object arg1, + object arg2, + object arg3 + ) + { + // PERF NOTE: check the condition here instead of pushing it into + // the ThrowInvalidProject() method, because that method always + // allocates memory for its variable array of arguments + if (!condition) + { + ThrowInvalidProject(errorSubCategoryResourceName, elementLocation, resourceName, arg0, arg1, arg2, arg3); + } + } + + /// + /// Throws an InvalidProjectFileException using the given data. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments + /// is expensive, because memory is allocated for the array of arguments -- do + /// not call this method repeatedly in performance-critical scenarios + /// + /// + /// The resource string for the + /// error sub-category (can be null). + /// The invalid project node (can be null). + /// The resource string for the error message. + /// Extra arguments for formatting the error message. + private static void ThrowInvalidProject + ( + string errorSubCategoryResourceName, + IElementLocation elementLocation, + string resourceName, + params object[] args + ) + { + ErrorUtilities.VerifyThrowInternalNull(elementLocation, "elementLocation"); +#if DEBUG + if (errorSubCategoryResourceName != null) + { + ResourceUtilities.VerifyResourceStringExists(errorSubCategoryResourceName); + } + + ResourceUtilities.VerifyResourceStringExists(resourceName); +#endif + string errorSubCategory = null; + + if (errorSubCategoryResourceName != null) + { + errorSubCategory = AssemblyResources.GetString(errorSubCategoryResourceName); + } + + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, resourceName, args); + + Exception exceptionToThrow = new InvalidProjectFileException(elementLocation.File, elementLocation.Line, elementLocation.Column, 0 /* Unknown end line */, 0 /* Unknown end column */, message, errorSubCategory, errorCode, helpKeyword); + + if (!DebuggerManager.DebuggingEnabled) + { + throw exceptionToThrow; + } + + try + { + throw exceptionToThrow; + } + catch (InvalidProjectFileException ex) + { + // To help out the user debugging their project, break into the debugger here. + // That's because otherwise, since they're debugging our optimized code with JMC on, + // they may not be able to break on this exception at all themselves. + // Also, dump the exception information, as it's hard to see in optimized code. + // Note that we use Trace as Debug.WriteLine is not compiled in release builds, which is + // what we are in here. + Trace.WriteLine(ex.ToString()); + Debugger.Break(); + throw; + } + } + } +} diff --git a/src/Shared/ProjectFileErrorUtilities.cs b/src/Shared/ProjectFileErrorUtilities.cs new file mode 100644 index 00000000000..960be049f11 --- /dev/null +++ b/src/Shared/ProjectFileErrorUtilities.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains methods that are useful for error checking and validation of project files. + /// + static internal class ProjectFileErrorUtilities + { + /// + /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of + /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// The invalid project file. + /// The resource string for the error message. + /// Extra arguments for formatting the error message. + internal static void ThrowInvalidProjectFile + ( + BuildEventFileInfo projectFile, + string resourceName, + params object[] args + ) + { + VerifyThrowInvalidProjectFile(false, null, projectFile, resourceName, args); + } + + /// + /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of + /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// The invalid project file. + /// Any inner exception. May be null. + /// The resource string for the error message. + /// Extra arguments for formatting the error message. + internal static void ThrowInvalidProjectFile + ( + BuildEventFileInfo projectFile, + Exception innerException, + string resourceName, + params object[] args + ) + { + VerifyThrowInvalidProjectFile(false, null, projectFile, innerException, resourceName, args); + } + + /// + /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of + /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// The condition to check. + /// The invalid project file. + /// The resource string for the error message. + /// Extra arguments for formatting the error message. + internal static void VerifyThrowInvalidProjectFile + ( + bool condition, + BuildEventFileInfo projectFile, + string resourceName, + params object[] args + ) + { + VerifyThrowInvalidProjectFile(condition, null, projectFile, resourceName, args); + } + + /// + /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of + /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// The condition to check. + /// The resource string for the error sub-category (can be null). + /// The invalid project file. + /// The resource string for the error message. + /// Extra arguments for formatting the error message. + internal static void VerifyThrowInvalidProjectFile + ( + bool condition, + string errorSubCategoryResourceName, + BuildEventFileInfo projectFile, + string resourceName, + params object[] args + ) + { + VerifyThrowInvalidProjectFile(condition, errorSubCategoryResourceName, projectFile, null, resourceName, args); + } + + /// + /// This method is used to flag errors in the project file being processed. Do NOT use this method in place of + /// ErrorUtilities.VerifyThrow(), because ErrorUtilities.VerifyThrow() is used to flag internal/programming errors. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// The condition to check. + /// The resource string for the error sub-category (can be null). + /// The invalid project file. + /// The resource string for the error message. + /// Extra arguments for formatting the error message. + internal static void VerifyThrowInvalidProjectFile + ( + bool condition, + string errorSubCategoryResourceName, + BuildEventFileInfo projectFile, + Exception innerException, + string resourceName, + params object[] args + ) + { + ErrorUtilities.VerifyThrow(projectFile != null, "Must specify the invalid project file. If project file is not available, use VerifyThrowInvalidProject() and pass in the XML node instead."); + +#if DEBUG + if (errorSubCategoryResourceName != null) + { + ResourceUtilities.VerifyResourceStringExists(errorSubCategoryResourceName); + } + + ResourceUtilities.VerifyResourceStringExists(resourceName); +#endif + if (!condition) + { + string errorSubCategory = null; + + if (errorSubCategoryResourceName != null) + { + errorSubCategory = AssemblyResources.GetString(errorSubCategoryResourceName); + } + + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, resourceName, args); + + throw new InvalidProjectFileException(projectFile.File, projectFile.Line, projectFile.Column, projectFile.EndLine, projectFile.EndColumn, message, errorSubCategory, errorCode, helpKeyword, innerException); + } + } + } +} diff --git a/src/Shared/ProjectWriter.cs b/src/Shared/ProjectWriter.cs new file mode 100644 index 00000000000..381c5115670 --- /dev/null +++ b/src/Shared/ProjectWriter.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Shared +{ + /// + /// This class is used to save MSBuild project files. It contains special handling for MSBuild notations that are not saved + /// correctly by the XML DOM's default save mechanism. + /// + internal sealed class ProjectWriter : XmlTextWriter + { + #region Regular expressions for item vector transforms + + /************************************************************************************************************************** + * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ItemExpander class -- if the + * description of an item vector changes, the expressions must be updated in both places. + *************************************************************************************************************************/ + + // the portion of the expression that matches the item type or metadata name, eg: "foo123" + // Note that the pattern is more strict than the rules for valid XML element names. + internal const string itemTypeOrMetadataNameSpecification = @"[A-Za-z_][A-Za-z_0-9\-]*"; + + // the portion of an item transform that is the function that we wish to execute on the item + internal const string itemFunctionNameSpecification = @"[A-Za-z]*"; + + // description of an item vector transform, including the optional separator specification + private const string itemVectorTransformSpecification = + @"(?@\(\s*) + (?" + itemTypeOrMetadataNameSpecification + @") + (?(?\s*->\s*)(?'[^']*')) + (?\s*,\s*'[^']*')? + (?\s*\))"; + // ) + + // regular expression used to match item vector transforms + // internal for unit testing only + internal static readonly Regex itemVectorTransformPattern = new Regex(itemVectorTransformSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + + // description of an item vector transform, including the optional separator specification, but with no (named) capturing + // groups -- see the WriteString() method for details + private const string itemVectorTransformRawSpecification = + @"@\(\s* + (" + itemTypeOrMetadataNameSpecification + @") + (\s*->\s*'[^']*') + (\s*,\s*'[^']*')? + \s*\)"; + + // regular expression used to match item vector transforms, with no (named) capturing groups + // internal for unit testing only + internal static readonly Regex itemVectorTransformRawPattern = new Regex(itemVectorTransformRawSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + + /************************************************************************************************************************** + * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ItemExpander class. + *************************************************************************************************************************/ + + #endregion + + #region Constructors + + /// + /// Creates an instance of this class using the specified TextWriter. + /// + /// + internal ProjectWriter(TextWriter w) + : base(w) + { + _documentEncoding = w.Encoding; + } + + /// + /// Creates an instance of this class using the specified file. + /// + /// + /// If null, defaults to UTF-8 and omits encoding attribute from processing instruction. + internal ProjectWriter(string filename, Encoding encoding) + : base(filename, encoding) + { + _documentEncoding = encoding; + } + + #endregion + + #region Methods + /// + /// Initializes settings for the project to be saved. + /// + internal void Initialize(XmlDocument project) + { + XmlDeclaration declaration = project.FirstChild as XmlDeclaration; + + Initialize(project, declaration); + } + + /// + /// Initializes settings for the project to be saved. + /// + /// + /// If null, XML declaration is not written. + internal void Initialize(XmlDocument project, XmlDeclaration projectRootElementDeclaration) + { + // if the project's whitespace is not being preserved + if (!project.PreserveWhitespace) + { + // write out child elements in an indented fashion, instead of jamming all the XML into one line + base.Formatting = Formatting.Indented; + } + + // don't write an XML declaration unless the project already has one or has non-default encoding + _writeXmlDeclaration = + ((projectRootElementDeclaration != null) || + ((_documentEncoding != Encoding.UTF8) && (_documentEncoding != null))); + } + + /// + /// Writes item vector transforms embedded in the given string without escaping '->' into "-&gt;". + /// + /// + public override void WriteString(string text) + { + MatchCollection itemVectorTransforms = itemVectorTransformRawPattern.Matches(text); + + // if the string contains any item vector transforms + if (itemVectorTransforms.Count > 0) + { + // separate out the text that surrounds the transforms + // NOTE: use the Regex with no (named) capturing groups, otherwise Regex.Split() will split on them + string[] surroundingTextPieces = itemVectorTransformRawPattern.Split(text); + + ErrorUtilities.VerifyThrow(itemVectorTransforms.Count == (surroundingTextPieces.Length - 1), + "We must have two pieces of surrounding text for every item vector transform found."); + + // write each piece of text before a transform, followed by the transform + for (int i = 0; i < itemVectorTransforms.Count; i++) + { + // write the text before the transform + base.WriteString(surroundingTextPieces[i]); + + // break up the transform into its constituent pieces + Match itemVectorTransform = itemVectorTransformPattern.Match(itemVectorTransforms[i].Value); + + ErrorUtilities.VerifyThrow(itemVectorTransform.Success, + "Item vector transform must be matched by both the raw and decorated regular expressions."); + + // write each piece of the transform normally, except for the arrow -- write that without escaping + base.WriteString(itemVectorTransform.Groups["PREFIX"].Value); + base.WriteString(itemVectorTransform.Groups["TYPE"].Value); + base.WriteRaw(itemVectorTransform.Groups["ARROW"].Value); + base.WriteString(itemVectorTransform.Groups["TRANSFORM"].Value); + base.WriteString(itemVectorTransform.Groups["SEPARATOR_SPECIFICATION"].Value); + base.WriteString(itemVectorTransform.Groups["SUFFIX"].Value); + } + + // write the terminal piece of text after the last transform + base.WriteString(surroundingTextPieces[surroundingTextPieces.Length - 1]); + } + // if the string has no item vector transforms in it, write it out as usual + else + { + base.WriteString(text); + } + } + + #endregion + + // indicates whether an XML declaration e.g. will be written at the start of the project + private bool _writeXmlDeclaration; + + // encoding of the document, if specified when constructing + private Encoding _documentEncoding = null; + } +} diff --git a/src/Shared/PropertyParser.cs b/src/Shared/PropertyParser.cs new file mode 100644 index 00000000000..1fc03b156a3 --- /dev/null +++ b/src/Shared/PropertyParser.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +#if BUILD_ENGINE +namespace Microsoft.Build.BackEnd +#else +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +#endif +{ + static internal class PropertyParser + { + /// + /// Given a string of semi-colon delimited name=value pairs, this method parses it and creates + /// a hash table containing the property names as keys and the property values as values. + /// + /// + /// + /// + /// true on success, false on failure. + static internal bool GetTable(TaskLoggingHelper log, string parameterName, string[] propertyList, out Hashtable propertiesTable) + { + propertiesTable = null; + + if (propertyList != null) + { + propertiesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + + // Loop through the array. Each string in the array should be of the form: + // MyPropName=MyPropValue + foreach (string propertyNameValuePair in propertyList) + { + string propertyName = String.Empty; + string propertyValue = String.Empty; + + // Find the first '=' sign in the string. + int indexOfEqualsSign = propertyNameValuePair.IndexOf('='); + + // If we found one, then grab the stuff before it and put it into "propertyName", + // and grab the stuff after it and put it into "propertyValue". But trim the + // whitespace from beginning and end of both name and value. (When authoring a + // project/targets file, people like to use whitespace and newlines to pretty up + // the file format.) + if (indexOfEqualsSign != -1) + { + propertyName = propertyNameValuePair.Substring(0, indexOfEqualsSign).Trim(); + propertyValue = propertyNameValuePair.Substring(indexOfEqualsSign + 1).Trim(); + } + + // Make sure we have a property name and property value (though the value is allowed to be blank). + if (propertyName.Length == 0) + { + // No equals sign? No property name? That's no good to us. + if (log != null) + { + log.LogErrorWithCodeFromResources("General.InvalidPropertyError", parameterName, propertyNameValuePair); + } + + return false; + } + + // Bag the property and its value. Trim whitespace from beginning and end of + // both name and value. (When authoring a project/targets file, people like to + // use whitespace and newlines to pretty up the file format.) + propertiesTable[propertyName] = propertyValue; + } + } + + return true; + } + + /// + /// Given a string of semi-colon delimited name=value pairs, this method parses it and creates + /// a hash table containing the property names as keys and the property values as values. + /// This method escapes any special characters found in the property values, in case they + /// are going to be passed to a method (such as that expects the appropriate escaping to have happened + /// already. + /// + /// + /// + /// + /// true on success, false on failure. + static internal bool GetTableWithEscaping(TaskLoggingHelper log, string parameterName, string syntaxName, string[] propertyNameValueStrings, out Hashtable finalPropertiesTable) + { + finalPropertiesTable = null; + + if (propertyNameValueStrings != null) + { + finalPropertiesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + List finalPropertiesList = new List(); + + // Loop through the array. Each string in the array should be of the form: + // MyPropName=MyPropValue + foreach (string propertyNameValueString in propertyNameValueStrings) + { + // Find the first '=' sign in the string. + int indexOfEqualsSign = propertyNameValueString.IndexOf('='); + + if (indexOfEqualsSign != -1) + { + // If we found one, then grab the stuff before it and put it into "propertyName", + // and grab the stuff after it and put it into "propertyValue". But trim the + // whitespace from beginning and end of both name and value. (When authoring a + // project/targets file, people like to use whitespace and newlines to pretty up + // the file format.) + string propertyName = propertyNameValueString.Substring(0, indexOfEqualsSign).Trim(); + string propertyValue = EscapingUtilities.Escape(propertyNameValueString.Substring(indexOfEqualsSign + 1).Trim()); + + // Make sure we have a property name and property value (though the value is allowed to be blank). + if (propertyName.Length == 0) + { + // No property name? That's no good to us. + if (log != null) + { + log.LogErrorWithCodeFromResources("General.InvalidPropertyError", syntaxName, propertyNameValueString); + } + + return false; + } + + // Store the property in our list. + finalPropertiesList.Add(new PropertyNameValuePair(propertyName, propertyValue)); + } + else + { + // There's no '=' sign in the string. When this happens, we treat this string as basically + // an appendage on the value of the previous property. For example, if the project file contains + // + // + // 1234;5678;9999 + // + // + // + // + // + // , then this method (GetTableWithEscaping) will see this: + // + // propertyNameValueStrings[0] = "WarningsAsErrors=1234" + // propertyNameValueStrings[1] = "5678" + // propertyNameValueStrings[2] = "9999" + // + // And what we actually want to end up with in our final hashtable is this: + // + // NAME VALUE + // =================== ================================ + // WarningsAsErrors 1234;5678;9999 + // + if (finalPropertiesList.Count > 0) + { + // There was a property definition previous to this one. Append the current string + // to that previous value, using semicolon as a separator. + string propertyValue = EscapingUtilities.Escape(propertyNameValueString.Trim()); + finalPropertiesList[finalPropertiesList.Count - 1].Value.Append(';'); + finalPropertiesList[finalPropertiesList.Count - 1].Value.Append(propertyValue); + } + else + { + // No equals sign in the very first property? That's a problem. + if (log != null) + { + log.LogErrorWithCodeFromResources("General.InvalidPropertyError", syntaxName, propertyNameValueString); + } + + return false; + } + } + } + + // Convert the data in the List to a Hashtable, because that's what the MSBuild task eventually + // needs to pass onto the engine. + if (log != null) + { + log.LogMessageFromText(parameterName, MessageImportance.Low); + } + + foreach (PropertyNameValuePair propertyNameValuePair in finalPropertiesList) + { + string propertyValue = OpportunisticIntern.StringBuilderToString(propertyNameValuePair.Value); + finalPropertiesTable[propertyNameValuePair.Name] = propertyValue; + if (log != null) + { + log.LogMessageFromText(String.Format(CultureInfo.InvariantCulture, " {0}={1}", + propertyNameValuePair.Name, propertyValue), + MessageImportance.Low); + } + } + } + + return true; + } + + /// + /// A very simple class that holds two strings, a property name and property value. + /// + private class PropertyNameValuePair + { + private string _name; + private StringBuilder _value; + + /// + /// Property name + /// + internal string Name + { + get + { + return _name; + } + + set + { + _name = value; + } + } + + /// + /// Property value + /// + internal StringBuilder Value + { + get + { + return _value; + } + + set + { + _value = value; + } + } + + /// + /// Constructor + /// + /// + /// + internal PropertyNameValuePair(string propertyName, string propertyValue) + { + this.Name = propertyName; + + this.Value = new StringBuilder(); + this.Value.Append(propertyValue); + } + + /// + /// Default construction not allowed. + /// + private PropertyNameValuePair() + { + } + } + } +} diff --git a/src/Shared/QuotingUtilities.cs b/src/Shared/QuotingUtilities.cs new file mode 100644 index 00000000000..b2edbf4a3e4 --- /dev/null +++ b/src/Shared/QuotingUtilities.cs @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.Build.Shared +{ + /// + /// This class implements some static methods to assist with command-line parsing of + /// parameters that could be quoted, and thus could contain nested escaped quotes. + /// + internal static class QuotingUtilities + { + /* + * Quoting Rules: + * + * A string is considered quoted if it is enclosed in double-quotes. A double-quote can be escaped with a backslash, or it + * is automatically escaped if it is the last character in an explicitly terminated quoted string. A backslash itself can + * be escaped with another backslash IFF it precedes a double-quote, otherwise it is interpreted literally. + * + * e.g. + * abc"cde"xyz --> "cde" is quoted + * abc"xyz --> "xyz" is quoted (the terminal double-quote is assumed) + * abc"xyz" --> "xyz" is quoted (the terminal double-quote is explicit) + * + * abc\"cde"xyz --> "xyz" is quoted (the terminal double-quote is assumed) + * abc\\"cde"xyz --> "cde" is quoted + * abc\\\"cde"xyz --> "xyz" is quoted (the terminal double-quote is assumed) + * + * abc"""xyz --> """ is quoted + * abc""""xyz --> """ and "xyz" are quoted (the terminal double-quote is assumed) + * abc"""""xyz --> """ is quoted + * abc""""""xyz --> """ and """ are quoted + * abc"cde""xyz --> "cde"" is quoted + * abc"xyz"" --> "xyz"" is quoted (the terminal double-quote is explicit) + * + * abc""xyz --> nothing is quoted + * abc""cde""xyz --> nothing is quoted + */ + + // the null character is used to mark a string for splitting + private static readonly char[] s_splitMarker = { '\0' }; + + /// + /// Splits the given string on every instance of a separator character, as long as the separator is not quoted. Each split + /// piece is then unquoted if requested. + /// + /// + /// 1) Unless requested to keep empty splits, a block of contiguous (unquoted) separators is treated as one separator. + /// 2) If no separators are given, the string is split on whitespace. + /// + /// + /// + /// + /// + /// [out] a count of all pieces that were empty, and thus discarded, per remark (1) above + /// + /// ArrayList of all the pieces the string was split into. + internal static ArrayList SplitUnquoted + ( + string input, + int maxSplits, + bool keepEmptySplits, + bool unquote, + out int emptySplits, + params char[] separator + ) + { + ErrorUtilities.VerifyThrow(maxSplits >= 2, "There is no point calling this method for less than two splits."); + + string separators = new StringBuilder().Append(separator).ToString(); + + ErrorUtilities.VerifyThrow(separators.IndexOf('"') == -1, "The double-quote character is not supported as a separator."); + + StringBuilder splitString = new StringBuilder(); + splitString.EnsureCapacity(input.Length); + + bool isQuoted = false; + int precedingBackslashes = 0; + int splits = 1; + + for (int i = 0; (i < input.Length) && (splits < maxSplits); i++) + { + switch (input[i]) + { + case '\0': + // Pretend null characters just aren't there. Ignore them. + Debug.Assert(false, "Null character in parameter"); + break; + + case '\\': + splitString.Append('\\'); + precedingBackslashes++; + break; + + case '"': + splitString.Append('"'); + if ((precedingBackslashes % 2) == 0) + { + if (isQuoted && + (i < (input.Length - 1)) && + (input[i + 1] == '"')) + { + splitString.Append('"'); + i++; + } + isQuoted = !isQuoted; + } + precedingBackslashes = 0; + break; + + default: + if (!isQuoted && + (((separators.Length == 0) && char.IsWhiteSpace(input[i])) || + (separators.IndexOf(input[i]) != -1))) + { + splitString.Append('\0'); + if (++splits == maxSplits) + { + splitString.Append(input, i + 1, input.Length - (i + 1)); + } + } + else + { + splitString.Append(input[i]); + } + precedingBackslashes = 0; + break; + } + } + + ArrayList pieces = new ArrayList(); + emptySplits = 0; + + foreach (string splitPiece in splitString.ToString().Split(s_splitMarker, maxSplits)) + { + string piece = (unquote + ? Unquote(splitPiece) + : splitPiece); + + if ((piece.Length > 0) || keepEmptySplits) + { + pieces.Add(piece); + } + else + { + emptySplits++; + } + } + + return pieces; + } + + /// + /// Splits the given string on every instance of a separator character, as long as the separator is not quoted. + /// + /// + /// 1) A block of contiguous (unquoted) separators is considered as one separator. + /// 2) If no separators are given, the string is split on blocks of contiguous (unquoted) whitespace. + /// + /// + /// + /// ArrayList of all the pieces the string was split into. + internal static ArrayList SplitUnquoted(string input, params char[] separator) + { + int emptySplits; + return SplitUnquoted(input, int.MaxValue, false /* discard empty splits */, false /* don't unquote the split pieces */, out emptySplits, separator); + } + + /// + /// Removes unescaped (i.e. non-literal) double-quotes, and escaping backslashes, from the given string. + /// + /// + /// [out] the number of double-quotes removed from the string + /// The given string in unquoted form. + internal static string Unquote(string input, out int doubleQuotesRemoved) + { + StringBuilder unquotedString = new StringBuilder(); + unquotedString.EnsureCapacity(input.Length); + + bool isQuoted = false; + int precedingBackslashes = 0; + doubleQuotesRemoved = 0; + + for (int i = 0; i < input.Length; i++) + { + switch (input[i]) + { + case '\\': + precedingBackslashes++; + break; + + case '"': + unquotedString.Append('\\', (precedingBackslashes / 2)); + if ((precedingBackslashes % 2) == 0) + { + if (isQuoted && + (i < (input.Length - 1)) && + (input[i + 1] == '"')) + { + unquotedString.Append('"'); + i++; + } + isQuoted = !isQuoted; + doubleQuotesRemoved++; + } + else + { + unquotedString.Append('"'); + } + precedingBackslashes = 0; + break; + + default: + unquotedString.Append('\\', precedingBackslashes); + unquotedString.Append(input[i]); + precedingBackslashes = 0; + break; + } + } + + return unquotedString.Append('\\', precedingBackslashes).ToString(); + } + + /// + /// Removes unescaped (i.e. non-literal) double-quotes, and escaping backslashes, from the given string. + /// + /// + /// The given string in unquoted form. + internal static string Unquote(string input) + { + int doubleQuotesRemoved; + return Unquote(input, out doubleQuotesRemoved); + } + } +} diff --git a/src/Shared/README.txt b/src/Shared/README.txt new file mode 100644 index 00000000000..a1b378e5872 --- /dev/null +++ b/src/Shared/README.txt @@ -0,0 +1 @@ +Please read "Shared Code.doc" for guidelines on using shared code in MSBuild. diff --git a/src/Shared/ReadOnlyCollection.cs b/src/Shared/ReadOnlyCollection.cs new file mode 100644 index 00000000000..e18087a07e2 --- /dev/null +++ b/src/Shared/ReadOnlyCollection.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A read-only wrapper around an ICollection<K> +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// A read-only live wrapper over a collection. + /// It does not prevent modification of the values themselves. + /// + /// + /// There is a type with the same name in the BCL, but it is actually a ReadOnlyList and does not accept an ICollection>T<. + /// Thus this is an omission from the BCL. + /// + /// Type of element in the collection + internal class ReadOnlyCollection : ICollection, ICollection + { + /// + /// Backing live enumerable. + /// May be a collection. + /// + private IEnumerable _backing; + + /// + /// Construct a read only wrapper around the current contents + /// of the IEnumerable, or around the backing collection if the + /// IEnumerable is in fact a collection. + /// + internal ReadOnlyCollection(IEnumerable backing) + { + ErrorUtilities.VerifyThrow(backing != null, "Need backing collection"); + + _backing = backing; + } + + /// + /// Return the number of items in the backing collection + /// + public int Count + { + get + { + return BackingCollection.Count; + } + } + + /// + /// Returns true. + /// + public bool IsReadOnly + { + get { return true; } + } + + /// + /// Whether collection is synchronized + /// + bool ICollection.IsSynchronized + { + get { return false; } + } + + /// + /// Sync root + /// + object ICollection.SyncRoot + { + get { return this; } + } + + /// + /// Get a backing ICollection. + /// + private ICollection BackingCollection + { + get + { + ICollection backingCollection = _backing as ICollection; + if (backingCollection == null) + { + backingCollection = new List(_backing); + _backing = backingCollection; + } + + return backingCollection; + } + } + + /// + /// Prohibited on read only collection: throws + /// + public void Add(T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Prohibited on read only collection: throws + /// + public void Clear() + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Pass through for underlying collection + /// + public bool Contains(T item) + { + // UNDONE: IEnumerable.Contains() does the ICollection check, + // so we could just use IEnumerable.Contains() here. + if (!(_backing is ICollection)) + { + return _backing.Contains(item); + } + + return BackingCollection.Contains(item); + } + + /// + /// Pass through for underlying collection + /// + public void CopyTo(T[] array, int arrayIndex) + { + ErrorUtilities.VerifyThrowArgumentNull(array, "array"); + + ICollection backingCollection = _backing as ICollection; + if (backingCollection != null) + { + backingCollection.CopyTo(array, arrayIndex); + } + else + { + int i = arrayIndex; + foreach (T entry in _backing) + { + array[i] = entry; + i++; + } + } + } + + /// + /// Prohibited on read only collection: throws + /// + public bool Remove(T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + /// + /// Pass through for underlying collection + /// + /// + /// NOTE: This does NOT cause a copy into a List, since the + /// backing enumerable suffices. + /// + public IEnumerator GetEnumerator() + { + return _backing.GetEnumerator(); + } + + /// + /// Pass through for underlying collection + /// + /// + /// NOTE: This does NOT cause a copy into a List, since the + /// backing enumerable suffices. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_backing).GetEnumerator(); + } + + /// + /// ICollection version of CopyTo + /// + void ICollection.CopyTo(Array array, int index) + { + ErrorUtilities.VerifyThrowArgumentNull(array, "array"); + + int i = index; + foreach (T entry in _backing) + { + array.SetValue(entry, i); + i++; + } + } + } +} diff --git a/src/Shared/ReadOnlyEmptyCollection.cs b/src/Shared/ReadOnlyEmptyCollection.cs new file mode 100644 index 00000000000..1d4ec6120f2 --- /dev/null +++ b/src/Shared/ReadOnlyEmptyCollection.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A read-only wrapper around an empty ICollection<K> +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// A read-only wrapper over an empty collection. + /// + /// + /// Thus this is an omission from the BCL. + /// + /// Type of element in the collection + internal class ReadOnlyEmptyCollection : ICollection, ICollection + { + /// + /// Backing live collection + /// + private static ReadOnlyEmptyCollection s_instance; + + /// + /// Private default constructor as this is a singleton + /// + private ReadOnlyEmptyCollection() + { + } + + /// + /// Get the instance + /// + public static ReadOnlyEmptyCollection Instance + { + get + { + if (s_instance == null) + { + s_instance = new ReadOnlyEmptyCollection(); + } + + return s_instance; + } + } + + /// + /// Pass through for underlying collection + /// + public int Count + { + get { return 0; } + } + + /// + /// Returns true. + /// + public bool IsReadOnly + { + get { return true; } + } + + /// + /// Whether collection is synchronized + /// + bool ICollection.IsSynchronized + { + get { return false; } + } + + /// + /// Sync root + /// + object ICollection.SyncRoot + { + get { return this; } + } + + /// + /// Prohibited on read only collection: throws + /// + public void Add(T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Prohibited on read only collection: throws + /// + public void Clear() + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Pass through for underlying collection + /// + public bool Contains(T item) + { + return false; + } + + /// + /// Pass through for underlying collection + /// + public void CopyTo(T[] array, int arrayIndex) + { + } + + /// + /// Prohibited on read only collection: throws + /// + public bool Remove(T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + /// + /// Get an enumerator over an empty collection + /// + public IEnumerator GetEnumerator() + { + yield break; + } + + /// + /// Get an enumerator over an empty collection + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// ICollection version of CopyTo + /// + void ICollection.CopyTo(Array array, int index) + { + } + } +} diff --git a/src/Shared/ReadOnlyEmptyDictionary.cs b/src/Shared/ReadOnlyEmptyDictionary.cs new file mode 100644 index 00000000000..572f5d5e15f --- /dev/null +++ b/src/Shared/ReadOnlyEmptyDictionary.cs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Enumerable over a notional read-only empty dictionary +//----------------------------------------------------------------------- + +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// A special singleton enumerable that enumerates a read-only empty dictionary + /// + /// Key + /// Value + internal class ReadOnlyEmptyDictionary : IDictionary, IDictionary + { + /// + /// The single instance + /// + private static readonly Dictionary s_backing = new Dictionary(); + + /// + /// The single instance + /// + private static ReadOnlyEmptyDictionary s_instance; + + /// + /// Private default constructor as this is a singleton + /// + private ReadOnlyEmptyDictionary() + { + } + + /// + /// Get the instance + /// + public static ReadOnlyEmptyDictionary Instance + { + get + { + if (s_instance == null) + { + s_instance = new ReadOnlyEmptyDictionary(); + } + + return s_instance; + } + } + + /// + /// Empty returns zero + /// + public int Count + { + get { return 0; } + } + + /// + /// Returns true + /// + public bool IsReadOnly + { + get { return true; } + } + + /// + /// Gets empty collection + /// + public ICollection Keys + { + get { return ReadOnlyEmptyList.Instance; } + } + + /// + /// Gets empty collection + /// + public ICollection Values + { + get { return ReadOnlyEmptyList.Instance; } + } + + /// + /// Is it fixed size + /// + public bool IsFixedSize + { + get { return true; } + } + + /// + /// Not synchronized + /// + public bool IsSynchronized + { + get { return false; } + } + + /// + /// No sync root + /// + public object SyncRoot + { + get { return null; } + } + + /// + /// Keys + /// + ICollection IDictionary.Keys + { + get { return (ICollection)((IDictionary)this).Keys; } + } + + /// + /// Values + /// + ICollection IDictionary.Values + { + get { return (ICollection)((IDictionary)this).Values; } + } + + /// + /// Indexer + /// + public object this[object key] + { + get + { + return ((IDictionary)this)[(K)key]; + } + + set + { + ((IDictionary)this)[(K)key] = (V)value; + } + } + + /// + /// Get returns null as read-only + /// Set is prohibited and throws. + /// + public V this[K key] + { + get + { + // Trigger KeyNotFoundException + return (new Dictionary()[key]); + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + } + + /// + /// Pass through for underlying collection + /// + public void Add(K key, V value) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Empty returns false + /// + public bool ContainsKey(K key) + { + return false; + } + + /// + /// Prohibited on read only collection: throws + /// + public bool Remove(K key) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + /// + /// Empty returns false + /// + public bool TryGetValue(K key, out V value) + { + value = default(V); + return false; + } + + /// + /// Prohibited on read only collection: throws + /// + public void Add(KeyValuePair item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Prohibited on read only collection: throws + /// + public void Clear() + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Empty returns false + /// + public bool Contains(KeyValuePair item) + { + return false; + } + + /// + /// Empty does nothing + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + } + + /// + /// Prohibited on read only collection: throws + /// + public bool Remove(KeyValuePair item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + /// + /// Get empty enumerator + /// + public IEnumerator> GetEnumerator() + { + return ReadOnlyEmptyList>.Instance.GetEnumerator(); + } + + /// + /// Get empty enumerator + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Add + /// + public void Add(object key, object value) + { + ((IDictionary)this).Add((K)key, (V)value); + } + + /// + /// Contains + /// + public bool Contains(object key) + { + return ((IDictionary)this).ContainsKey((K)key); + } + + /// + /// Enumerator + /// + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return ((IDictionary)s_backing).GetEnumerator(); + } + + /// + /// Remove + /// + public void Remove(object key) + { + ((IDictionary)this).Remove((K)key); + } + + /// + /// CopyTo + /// + public void CopyTo(System.Array array, int index) + { + // Nothing to do + } + } +} \ No newline at end of file diff --git a/src/Shared/ReadOnlyEmptyList.cs b/src/Shared/ReadOnlyEmptyList.cs new file mode 100644 index 00000000000..75711c3801d --- /dev/null +++ b/src/Shared/ReadOnlyEmptyList.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A read-only empty list +//----------------------------------------------------------------------- + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// A special singleton read-only empty list + /// + /// Type of item + [DebuggerDisplay("Count = 0")] + internal class ReadOnlyEmptyList : IList, ICollection, ICollection + { + /// + /// The single instance + /// + private static ReadOnlyEmptyList s_instance; + + /// + /// Private default constructor as this is a singleton + /// + private ReadOnlyEmptyList() + { + } + + /// + /// Get the instance + /// + public static ReadOnlyEmptyList Instance + { + get + { + if (s_instance == null) + { + s_instance = new ReadOnlyEmptyList(); + } + + return s_instance; + } + } + + /// + /// There are no items in this list + /// + public int Count + { + get { return 0; } + } + + /// + /// Read-only list + /// + public bool IsReadOnly + { + get { return true; } + } + + /// + /// ICollection implementation + /// + int ICollection.Count + { + get { return 0; } + } + + /// + /// ICollection implementation + /// + bool ICollection.IsSynchronized + { + get { return false; } + } + + /// + /// ICollection implementation + /// + object ICollection.SyncRoot + { + get { return null; } + } + + /// + /// Items cannot be retrieved or added to a read-only list + /// + public T this[int index] + { + get + { + ErrorUtilities.ThrowArgumentOutOfRange("index"); + return default(T); + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + } + + /// + /// Get an enumerator over an empty collection + /// + public IEnumerator GetEnumerator() + { + yield break; + } + + /// + /// Get an enumerator over an empty collection + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Index of specified item + /// + public int IndexOf(T item) + { + return -1; + } + + /// + /// Items cannot be inserted into a read-only list + /// + public void Insert(int index, T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Items cannot be removed from a read-only list + /// + public void RemoveAt(int index) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Items cannot be added to a read-only list + /// + public void Add(T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Read-only list cannot be cleared + /// + public void Clear() + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// An empty list contains nothing + /// + public bool Contains(T item) + { + return false; + } + + /// + /// An empty list copies nothing + /// + public void CopyTo(T[] array, int arrayIndex) + { + } + + /// + /// Cannot remove items from a read-only list + /// + public bool Remove(T item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + /// + /// ICollection implementation + /// + void ICollection.CopyTo(System.Array array, int index) + { + // Do nothing + } + } +} \ No newline at end of file diff --git a/src/Shared/RegisteredTaskObjectCacheBase.cs b/src/Shared/RegisteredTaskObjectCacheBase.cs new file mode 100644 index 00000000000..31dc0e29251 --- /dev/null +++ b/src/Shared/RegisteredTaskObjectCacheBase.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implements a cache for registered task objects. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.Build.Framework; + +#if BUILD_ENGINE +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Components.Caching +#else +namespace Microsoft.Build.Shared +#endif +{ + /// + /// This is a cache of objects which are registered to be disposed of at a specified time. + /// + internal class RegisteredTaskObjectCacheBase + { + /// + /// The cache for AppDomain lifetime objects. + /// + private static Lazy> s_appDomainLifetimeObjects = new Lazy>(); + + /// + /// The cache for Build lifetime objects. + /// + private Lazy> _buildLifetimeObjects = new Lazy>(); + + /// + /// Static constructor which registers a callback to dispose of AppDomain-lifetime cache objects. + /// + static RegisteredTaskObjectCacheBase() + { + AppDomain.CurrentDomain.DomainUnload += new EventHandler((sender, args) => + { + DisposeObjects(s_appDomainLifetimeObjects); + }); + } + + #region IRegisteredTaskObjectCache + + /// + /// Disposes of all of the cached objects registered with the specified lifetime. + /// + public void DisposeCacheObjects(RegisteredTaskObjectLifetime lifetime) + { + var lazyCollection = GetLazyCollectionForLifetime(lifetime); + DisposeObjects(lazyCollection); + } + + /// + /// Registers a task object with the specified key and lifetime. + /// + public void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection) + { + ConcurrentDictionary dict = GetCollectionForLifetime(lifetime, dontCreate: false); + + if (dict != null) + { + dict.TryAdd(key, obj); + } + } + + /// + /// Gets a previously registered task object. + /// + public object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + ConcurrentDictionary dict = GetCollectionForLifetime(lifetime, dontCreate: true); + object obj = null; + if (dict != null) + { + dict.TryGetValue(key, out obj); + } + + return obj; + } + + /// + /// Unregisters a previously registered task object. + /// + public object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + ConcurrentDictionary dict = GetCollectionForLifetime(lifetime, dontCreate: true); + object obj = null; + if (dict != null) + { + dict.TryRemove(key, out obj); + } + + return obj; + } + + #endregion + + /// + /// Returns true if a collection is not yet created or if it has no content. + /// + protected bool IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime lifetime) + { + var collection = GetCollectionForLifetime(lifetime, dontCreate: true); + return (collection == null) || (collection.Count == 0); + } + + /// + /// Returns the collection associated with a particular lifetime. + /// + protected ConcurrentDictionary GetCollectionForLifetime(RegisteredTaskObjectLifetime lifetime, bool dontCreate) + { + Lazy> dict = GetLazyCollectionForLifetime(lifetime); + + // If we aren't supposed to create it, don't force the creation. + if (dontCreate && !dict.IsValueCreated) + { + return null; + } + + return dict.Value; + } + + /// + /// Gets the lazy cache for the specified lifetime. + /// + protected Lazy> GetLazyCollectionForLifetime(RegisteredTaskObjectLifetime lifetime) + { + Lazy> dict = null; + switch (lifetime) + { + case RegisteredTaskObjectLifetime.Build: + dict = _buildLifetimeObjects; + break; + + case RegisteredTaskObjectLifetime.AppDomain: + dict = RegisteredTaskObjectCacheBase.s_appDomainLifetimeObjects; + break; + } + + return dict; + } + + /// + /// Cleans up a cache collection. + /// + private static void DisposeObjects(Lazy> lifetimeObjects) + { + if (lifetimeObjects.IsValueCreated) + { + foreach (var obj in lifetimeObjects.Value.Values) + { + try + { + IDisposable disposable = obj as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + // Eat it. We don't have a way to log here because at a minimum the build has already completed. + } + } + + lifetimeObjects.Value.Clear(); + } + } + } +} diff --git a/src/Shared/RegistryDelegates.cs b/src/Shared/RegistryDelegates.cs new file mode 100644 index 00000000000..6580ab6cf1f --- /dev/null +++ b/src/Shared/RegistryDelegates.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Win32; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.Shared +{ /// + /// Given a registry hive and a request view open the base key for that registry location. + /// + internal delegate RegistryKey OpenBaseKey(RegistryHive hive, RegistryView view); + + /// + /// Simplified registry access delegate. Given a baseKey and a subKey, get all of the subkey + /// names. + /// + /// The base registry key. + /// The subkey + /// An enumeration of strings. + internal delegate IEnumerable GetRegistrySubKeyNames(RegistryKey baseKey, string subKey); + + /// + /// Simplified registry access delegate. Given a baseKey and subKey, get the default value + /// of the subKey. + /// + /// The base registry key. + /// The subkey + /// A string containing the default value. + internal delegate string GetRegistrySubKeyDefaultValue(RegistryKey baseKey, string subKey); +} \ No newline at end of file diff --git a/src/Shared/RegistryHelper.cs b/src/Shared/RegistryHelper.cs new file mode 100644 index 00000000000..c196a211e98 --- /dev/null +++ b/src/Shared/RegistryHelper.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.Win32; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections.Generic; + +namespace Microsoft.Build.Shared +{ + /// + /// Helper methods that simplify registry access. + /// + internal static class RegistryHelper + { + /// + /// Given a baseKey and a subKey, get all of the subkeys names. + /// + /// The base registry key. + /// The subkey + /// An enumeration of strings. + internal static IEnumerable GetSubKeyNames(RegistryKey baseKey, string subkey) + { + IEnumerable subKeys = null; + + using (RegistryKey subKey = baseKey.OpenSubKey(subkey)) + { + if (subKey != null) + { + subKeys = subKey.GetSubKeyNames(); + } + } + + return subKeys; + } + + /// + /// Given a baseKey and subKey, get the default value of the subKey. + /// + /// The base registry key. + /// The subkey + /// A string containing the default value. + internal static string GetDefaultValue(RegistryKey baseKey, string subkey) + { + string value = null; + + using (RegistryKey key = baseKey.OpenSubKey(subkey)) + { + if (key != null && key.ValueCount > 0) + { + value = (string)key.GetValue(""); + } + } + + return value; + } + + /// + /// Given a hive and a hive view open the base key + /// RegistryKey baseKey = RegistryKey.OpenBaseKey(hive, view); + /// + /// The base registry key. + /// The hive view + /// A registry Key for the given baseKey and view + internal static RegistryKey OpenBaseKey(RegistryHive hive, RegistryView view) + { + RegistryKey key = RegistryKey.OpenBaseKey(hive, view); + return key; + } + } +} diff --git a/src/Shared/ResourceUtilities.cs b/src/Shared/ResourceUtilities.cs new file mode 100644 index 00000000000..28bbc6da309 --- /dev/null +++ b/src/Shared/ResourceUtilities.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.RegularExpressions; +#if DEBUG && !BUILDING_DF_LKG +using Microsoft.Build.Framework; +#endif +using System.Reflection; +using System.Text; + +#if BUILDINGAPPXTASKS +namespace Microsoft.Build.AppxPackage.Shared +#else +namespace Microsoft.Build.Shared +#endif +{ + /// + /// This class contains utility methods for dealing with resources. + /// + internal static class ResourceUtilities + { + /// + /// Extracts the message code (if any) prefixed to the given string. + /// MSB\d\d\d\d):\s*(?.*)$" + /// Arbitrary codes match "^\s*(?[A-Za-z]+\d+):\s*(?.*)$" + /// ]]> + /// Thread safe. + /// + /// Whether to match only MSBuild error codes, or any error code. + /// The string to parse. + /// [out] The message code, or null if there was no code. + /// The string without its message code prefix, if any. + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Build.Shared.ResourceUtilities.#ExtractMessageCode(System.Boolean,System.String,System.String&)", Justification = "Unavoidable complexity")] + internal static string ExtractMessageCode(bool msbuildCodeOnly, string message, out string code) + { +#if !BUILDINGAPPXTASKS + ErrorUtilities.VerifyThrowInternalNull(message, "message"); +#endif + + code = null; + int i = 0; + + while (i < message.Length && Char.IsWhiteSpace(message[i])) + { + i++; + } + +#if !BUILDINGAPPXTASKS + if (msbuildCodeOnly) + { + if ( + message.Length < i + 8 || + message[i] != 'M' || + message[i + 1] != 'S' || + message[i + 2] != 'B' || + message[i + 3] < '0' || message[i + 3] > '9' || + message[i + 4] < '0' || message[i + 4] > '9' || + message[i + 5] < '0' || message[i + 5] > '9' || + message[i + 6] < '0' || message[i + 6] > '9' || + message[i + 7] != ':' + ) + { + return message; + } + + code = message.Substring(i, 7); + + i = i + 8; + } + else +#endif + { + int j = i; + for (; j < message.Length; j++) + { + char c = message[j]; + if (((c < 'a') || (c > 'z')) && ((c < 'A') || (c > 'Z'))) + { + break; + } + } + + if (j == i) + { + return message; // Should have been at least one letter + } + + int k = j; + + for (; k < message.Length; k++) + { + char c = message[k]; + if (c < '0' || c > '9') + { + break; + } + } + + if (k == j) + { + return message; // Should have been at least one digit + } + + if (k == message.Length || message[k] != ':') + { + return message; + } + + code = message.Substring(i, k - i); + + i = k + 1; + } + + while (i < message.Length && Char.IsWhiteSpace(message[i])) + { + i++; + } + + if (i < message.Length) + { + message = message.Substring(i, message.Length - i); + } + + return message; + } + + /// + /// Retrieves the MSBuild F1-help keyword for the given resource string. Help keywords are used to index help topics in + /// host IDEs. + /// + /// Resource string to get the MSBuild F1-keyword for. + /// The MSBuild F1-help keyword string. + private static string GetHelpKeyword(string resourceName) + { + return ("MSBuild." + resourceName); + } + +#if !BUILDINGAPPXTASKS + /// + /// Retrieves the contents of the named resource string. + /// + /// Resource string name. + /// Resource string contents. + internal static string GetResourceString(string resourceName) + { + string result = AssemblyResources.GetString(resourceName); + return result; + } + + /// + /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild + /// message code and help keyword associated with it, they too are returned. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// This method is thread-safe. + /// [out] The MSBuild message code, or null. + /// [out] The MSBuild F1-help keyword for the host IDE, or null. + /// Resource string to load. + /// Optional arguments for formatting the resource string. + /// The formatted resource string. + internal static string FormatResourceString(out string code, out string helpKeyword, string resourceName, params object[] args) + { + helpKeyword = GetHelpKeyword(resourceName); + + // NOTE: the AssemblyResources.GetString() method is thread-safe + return ExtractMessageCode(true /* msbuildCodeOnly */, FormatString(GetResourceString(resourceName), args), out code); + } + + /// + /// Looks up a string in the resources, and formats it with the arguments passed in. If the string resource has an MSBuild + /// message code and help keyword associated with it, they are discarded. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// This method is thread-safe. + /// Resource string to load. + /// Optional arguments for formatting the resource string. + /// The formatted resource string. + internal static string FormatResourceString(string resourceName, params object[] args) + { + string code; + string helpKeyword; + + return FormatResourceString(out code, out helpKeyword, resourceName, args); + } + + /// + /// Formats the given string using the variable arguments passed in. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for + /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios + /// + /// Thread safe. + /// + /// The string to format. + /// Optional arguments for formatting the given string. + /// The formatted string. + internal static string FormatString(string unformatted, params object[] args) + { + string formatted = unformatted; + + // NOTE: String.Format() does not allow a null arguments array + if ((args != null) && (args.Length > 0)) + { +#if DEBUG && !BUILDING_DF_LKG + +#if VALIDATERESOURCESTRINGS + // The code below reveals many places in our codebase where + // we're not using all of the data given to us to format + // strings -- but there are too many to presently fix. + // Rather than toss away the code, we should later build it + // and fix each offending resource (or the code processing + // the resource). + + // String.Format() will throw a FormatException if args does + // not have enough elements to match each format parameter. + // However, it provides no feedback in the case when args contains + // more elements than necessary to replace each format + // parameter. We'd like to know if we're providing too much + // data in cases like these, so we'll fail if this code runs. + + // We create an array with one fewer element + object[] trimmedArgs = new object[args.Length - 1]; + Array.Copy(args, 0, trimmedArgs, 0, args.Length - 1); + + bool caughtFormatException = false; + try + { + // This will throw if there aren't enough elements in trimmedArgs... + String.Format(CultureInfo.CurrentCulture, unformatted, trimmedArgs); + } + catch (FormatException) + { + caughtFormatException = true; + } + + // If we didn't catch an exception above, then some of the elements + // of args were unnecessary when formatting unformatted... + Debug.Assert + ( + caughtFormatException, + String.Format("The provided format string '{0}' had fewer format parameters than the number of format args, '{1}'.", unformatted, args.Length) + ); +#endif + // If you accidentally pass some random type in that can't be converted to a string, + // FormatResourceString calls ToString() which returns the full name of the type! + foreach (object param in args) + { + // Check it has a real implementation of ToString() + if (param != null) + { + if (String.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal)) + { + ErrorUtilities.ThrowInternalError("Invalid resource parameter type, was {0}", param.GetType().FullName); + } + } + } +#endif + // Format the string, using the variable arguments passed in. + // NOTE: all String methods are thread-safe + formatted = String.Format(CultureInfo.CurrentCulture, unformatted, args); + } + + return formatted; + } + + /// + /// Verifies that a particular resource string actually exists in the string table. This will only be called in debug + /// builds. It helps catch situations where a dev calls VerifyThrowXXX with a new resource string, but forgets to add the + /// resource string to the string table, or misspells it! + /// + /// This method is thread-safe. + /// Resource string to check. + internal static void VerifyResourceStringExists(string resourceName) + { +#if DEBUG + try + { + // Look up the resource string in the engine's string table. + // NOTE: the AssemblyResources.GetString() method is thread-safe + string unformattedMessage = AssemblyResources.GetString(resourceName); + + if (unformattedMessage == null) + { + ErrorUtilities.ThrowInternalError("The resource string \"" + resourceName + "\" was not found."); + } + } + catch (ArgumentException e) + { + Debug.Fail("The resource string \"" + resourceName + "\" was not found."); + ErrorUtilities.ThrowInternalError(e.Message); + } + catch (InvalidOperationException e) + { + Debug.Fail("The resource string \"" + resourceName + "\" was not found."); + ErrorUtilities.ThrowInternalError(e.Message); + } + catch (MissingManifestResourceException e) + { + Debug.Fail("The resource string \"" + resourceName + "\" was not found."); + ErrorUtilities.ThrowInternalError(e.Message); + } +#endif + } +#endif + } +} diff --git a/src/Shared/ReuseableStringBuilder.cs b/src/Shared/ReuseableStringBuilder.cs new file mode 100644 index 00000000000..405a2cae04f --- /dev/null +++ b/src/Shared/ReuseableStringBuilder.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A utility class that mediates access to a shared string builder. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Text; +using System.Threading; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Shared +{ + /// + /// A StringBuilder lookalike that reuses its internal storage. + /// + /// + /// You can add any properties or methods on the real StringBuilder that are needed. + /// + internal sealed class ReuseableStringBuilder : IDisposable, OpportunisticIntern.IInternable + { + /// + /// Captured string builder. + /// + private StringBuilder _borrowedBuilder; + + /// + /// Capacity to initialize the builder with. + /// + private int _capacity; + + /// + /// Create a new builder, under the covers wrapping a reused one. + /// + internal ReuseableStringBuilder(int capacity = 16) // StringBuilder default is 16 + { + _capacity = capacity; + + // lazy initialization of the builder + } + + /// + /// The length of the target. + /// + public int Length + { + get { return ((_borrowedBuilder == null) ? 0 : _borrowedBuilder.Length); } + } + + /// + /// Indexer into the target. Presumed to be fast. + /// + char OpportunisticIntern.IInternable.this[int index] + { + get + { + LazyPrepare(); // Must have one to call this + return _borrowedBuilder[index]; + } + } + + /// + /// Convert target to string. Presumed to be slow (and will be called just once). + /// + string OpportunisticIntern.IInternable.ExpensiveConvertToString() + { + { + return ((ReuseableStringBuilder)this).ToString(); + } + } + + /// + /// Compare target to string. + /// + bool OpportunisticIntern.IInternable.IsOrdinalEqualToStringOfSameLength(string other) + { +#if DEBUG + ErrorUtilities.VerifyThrow(other.Length == _borrowedBuilder.Length, "should be same length"); +#endif + // Backwards because the end of the string is (by observation of Australian Government build) more likely to be different earlier in the loop. + // For example, C:\project1, C:\project2 + for (int i = _borrowedBuilder.Length - 1; i >= 0; --i) + { + if (_borrowedBuilder[i] != other[i]) + { + return false; + } + } + + return true; + } + + /// + /// Never reference equals to string. + /// + bool OpportunisticIntern.IInternable.ReferenceEquals(string other) + { + return false; + } + + /// + /// Convert to a string. + /// + public override string ToString() + { + if (_borrowedBuilder == null) + { + return String.Empty; + } + + return _borrowedBuilder.ToString(); + } + + /// + /// Dispose, indicating you are done with this builder. + /// + void IDisposable.Dispose() + { + if (_borrowedBuilder != null) + { + ReuseableStringBuilderFactory.Release(_borrowedBuilder); + _borrowedBuilder = null; + _capacity = -1; + } + } + + /// + /// Append a character. + /// + internal ReuseableStringBuilder Append(char value) + { + LazyPrepare(); + _borrowedBuilder.Append(value); + return this; + } + + /// + /// Append a string. + /// + internal ReuseableStringBuilder Append(string value) + { + LazyPrepare(); + _borrowedBuilder.Append(value); + return this; + } + + /// + /// Append a substring. + /// + internal ReuseableStringBuilder Append(string value, int startIndex, int count) + { + LazyPrepare(); + _borrowedBuilder.Append(value, startIndex, count); + return this; + } + + /// + /// Remove a substring. + /// + internal ReuseableStringBuilder Remove(int startIndex, int length) + { + LazyPrepare(); + _borrowedBuilder.Remove(startIndex, length); + return this; + } + + /// + /// Grab a backing builder if necessary. + /// + private void LazyPrepare() + { + if (_borrowedBuilder == null) + { + ErrorUtilities.VerifyThrow(_capacity != -1, "Reusing after dispose"); + + _borrowedBuilder = ReuseableStringBuilderFactory.Get(_capacity); + } + } + + /// + /// A utility class that mediates access to a shared string builder. + /// + /// + /// If this shared builder is highly contended, this class could add + /// a second one and try both in turn. + /// + private static class ReuseableStringBuilderFactory + { + /// + /// Made up limit beyond which we won't share the builder + /// because we could otherwise hold a huge builder indefinitely. + /// This size seems reasonable for MSBuild uses (mostly expression expansion) + /// + private const int MaxBuilderSize = 1024; + + /// + /// The shared builder. + /// + private static StringBuilder s_sharedBuilder; + +#if DEBUG + /// + /// Flag to help expose bugs + /// + private static bool s_stress = String.Equals(Environment.GetEnvironmentVariable("MSBUILDRSBSTRESS"), "1", StringComparison.Ordinal); + + /// + /// Count of successful reuses + /// + private static int s_hits = 0; + + /// + /// Count of failed reuses - a new builder was created + /// + private static int s_misses = 0; + + /// + /// Count of times the builder capacity was raised to satisfy the caller's request + /// + private static int s_upsizes = 0; + + /// + /// Count of times the returned builder was discarded because it was too large + /// + private static int s_discards = 0; + + /// + /// Count of times the builder was returned. + /// + private static int s_accepts = 0; + + /// + /// Aggregate capacity saved (aggregate midpoints of requested and returned) + /// + private static int s_saved = 0; + + /// + /// Callstacks of those handed out and not returned yet + /// + private static ConcurrentDictionary s_handouts = new ConcurrentDictionary(); +#endif + /// + /// Obtains a string builder which may or may not already + /// have been used. + /// Never returns null. + /// + internal static StringBuilder Get(int capacity) + { +#if DEBUG + bool missed = false; +#endif + var returned = Interlocked.Exchange(ref s_sharedBuilder, null); + + if (returned == null) + { +#if DEBUG + missed = true; + Interlocked.Increment(ref s_misses); +#endif + // Currently loaned out so return a new one + returned = new StringBuilder(capacity); + } + else if (returned.Capacity < capacity) + { +#if DEBUG + Interlocked.Increment(ref s_upsizes); +#endif + // It's essential we guarantee the capacity because this + // may be used as a buffer to a PInvoke call. + returned.Capacity = capacity; + } + +#if DEBUG + Interlocked.Increment(ref s_hits); + + if (!missed) + { + Interlocked.Add(ref s_saved, (capacity + returned.Capacity) / 2); + } + + // handouts.TryAdd(returned, Environment.StackTrace); +#endif + return returned; + } + + /// + /// Returns the shared builder for the next caller to use. + /// ** CALLERS, DO NOT USE THE BUILDER AFTER RELEASING IT HERE! ** + /// + internal static void Release(StringBuilder returningBuilder) + { + // ErrorUtilities.VerifyThrow(handouts.TryRemove(returningBuilder, out dummy), "returned but not loaned"); + returningBuilder.Clear(); // This is free: it just sets length to zero internally + + // It's possible for someone to cause the builder to + // enlarge to such an extent that this static field + // would be a leak. To avoid that, only accept + // the builder if it's no more than a certain size. + // + // If some code has a bug and forgets to return their builder + // (or we refuse it here because it's too big) the next user will + // get given a new one, and then return it soon after. + // So the shared builder will be "replaced". + if (returningBuilder.Capacity < MaxBuilderSize) + { + Interlocked.Exchange(ref s_sharedBuilder, returningBuilder); +#if DEBUG + Interlocked.Increment(ref s_accepts); + } + else + { + Interlocked.Increment(ref s_discards); +#endif + } + } + +#if DEBUG + /// + /// Debugging dumping + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Handy helper method that can be used to annotate ReuseableStringBuilder when debugging it, but is not hooked up usually for the sake of perf.")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.String.Format(System.IFormatProvider,System.String,System.Object[])", Justification = "Handy string that can be used to annotate ReuseableStringBuilder when debugging it, but is not hooked up usually.")] + internal static void DumpUnreturned() + { + String.Format(CultureInfo.CurrentUICulture, "{0} Hits of which\n {1} Misses (was on loan)\n {2} Upsizes (needed bigger) \n\n{3} Returns=\n{4} Discards (returned too large)+\n {5} Accepts\n\n{6} estimated bytes saved", s_hits, s_misses, s_upsizes, s_discards + s_accepts, s_discards, s_accepts, s_saved); + + Console.WriteLine("Unreturned string builders were allocated here:"); + foreach (var entry in s_handouts.Values) + { + Console.WriteLine(entry + "\n"); + } + } +#endif + } + } +} \ No newline at end of file diff --git a/src/Shared/Shared Code.doc b/src/Shared/Shared Code.doc new file mode 100644 index 0000000000000000000000000000000000000000..3bb7c7745d233990c715c0375c64f26e2e1af219 GIT binary patch literal 37376 zcmeI537AyXnfFgsFEmR@v-1&`3qb-(T4)79T$)wffB+_H7)?=C-BnUu)l^l}Aju3S zhB1zDP@_?knP^65bj&1ba3(HEj2he!nM{!QjE*qhm{B5u7}PiqpZ)#*=iXagU0v0* zIEE)l-F|*`?%B_I&wI98=T>ijVaTpK?jN(;6kTVUD)WA4e^XoF9^ihSZ?89I0rzry zzq7Mb&U1m=%Jl~%fj95^iCO)*{?*3(Zs?EIh{n*eTr~WxV~jbUtw$7A~8r~Dsd-od^J_h&IL8Nx>|<}_-3HU2-uoQ(Mf=4j0C@e#zlUzR?a zD#rw*9w*{;M;r5b>^q1*LJ1UIzB~fMiab24%B9OU{cygibPF#(J>T@}?T3;5-?8i= z-blcmwXB@Kb=CizTEruIaGRn`hGclvoCp+3gf5e%jqigWe@?U z?7HTiW0pOsfze*R-aUnKUV^J1&-c%l8@-!?qx4UgA8x=gu6-;*-%5Pn?)$sGAO6!% z*H6ipkMj8rj=r3I)6d_x`=%eioau)vHhq2c&2sU5_r0TBJ@oD6%;J1}yDw+oUe5H> z^~Z-obNoP)A6E+9700*x?eE)*O<&G^H8r^?{h%3?*D={ES49FJTmsAD;Y=**%uPpQ zp=Ir@v1r^mFPw_yR)sU6&;^M|CY?<;=kQN8C!(=bE)hQ<~AIlx8X=PppSXaHgAJW3TmXoT3C5teu-~bH>eHlTJkEv?r3$ zM5<*`IyE<(Oomq`V`{%~&YDCz8O|lrsgmVyoYIxuh)}*MLbH=eC+n&~gk+r7_H52c zCf3B9M9RsX0BrJ5t;g|msa#By0Dl5TBFBvH3SDwlR*Yhsyo zPB@#5wXRIAa}qfxk##b$=2#|{iV$e!x&~)qx{ISTRgAq zxpi%LNAPJ)v}D4$m=l)vW^EWRmjofG5xHhmy9Or|n!hHNauUr>t|RTZNl@!(qS+QB z=NC^A<6%T1d1R@$quM*+RMbnVyNA{B$<}S|`AfSBo{2@$Evdv6ZaQvuNMdDIx|6X) z!tGh>lF0b^BOcrPaw>=lpD!W8pN$iO%eE&eZ>h$#B}e9Nj|sayCQXlNaF)jrH_e}J zPa<)um3G?NS0)pYE@2et2ASnmx?A(6VzDTta8ss6`BtS^ZkKXm4J{2$b2{TB(_v|^ z6U$_)8bxE1T{VryniDD7!HFcvlGaa3 zy=Hqll`pHW$zE0RnmgI)NaV-`4e+It&C!D*h%+3OgeYI@N)#}ShM-`MpX3GM6s6Ai zIp(YBm0KC9j(Is))n476&|r&vq9~hG+HY)|6+K{8D&3KFI(*e`4W|;#F}kClzN>Z< z!wst%!VO5WB|(*~<0KU^K_~NrX6a51&a#4zt-gW&rX079$Z8#-z5ZsE)i3T11<5ox z=TLXlzNb`Op;HZYO1CMZlf8~ileoS8leq*v#WptTgT9qbi2fX=^QIE5?XBJ@luoZ= zC|af7kjS1E3bkh?)n)6lxmat1-xk?f#qQ0?SVU?xD-_DNb(vu-v_9l;6}P`zwH{Tr zVci&|V&%2QFPg6=U5#=^C>84{R-=ibai^DV>odnqrhmIbWW(99-2An%NP7+;_>L1N zO`2s(^@Y$ELZL;Am(M@VwpPAvVr$#z&WW6KIheHxX6=8<5eKH&Jhag|tS>M4((9n3uxnR2F_m-?#c!+Bb9hK*=)xX1?%db4fG3lIP%6fnnKMXA+|EKC)$!oJl&=lK%G62$)7M0LzyXH0TI2Xb=PBz`?f$`t?MqG{2{I@ZFj(_ z%o9D=1bv$)>_qW_CN5V~x?11^PG8ccVrA=FQB_Jir7cf8*+eUISo1ooZqDNSKN>h^ zbaf;x8J5|sCMwS4&|GGc_Dt-YRCBsvZaZU5DtF;Io+As1ynq^CBJZuu1RDyeQLO8u@iG!-xq|t94e}t=BG8g7rZsR&S_>!w)O7o^8u@h{*sP0K-gDScYw9ZuU#hlP`gG*Ymen`)K|9W z>lkgTdVa3(@Xq^@|`AxiCvC&elU|G3* z@JCsd7>6SPzULcM$5U)L22O!#FdZ&{MX(r_z$!>WE2N+U*1|ft0d9n^!A)>8d>w9q zTj4f%5dInd1rGk|;O`|}g{C?MZE?PJHtlr*w8ItxXBkJ^RdD@OgXpS6!?vIOH~}AsO*E?P$T2tJ+aQ< zSL@!Hp(Ct|o8z>)E2r08X`B)9=dL)}li1vIhnvOcR+&%1r4Yt$h%IRzqwjf3wZGbJ zFPz$+Z#Usxcn_)u@V!5r4J*L#4M+fQ1Em1YD=|^jf4}`}DTB2%7*T>#jpI~@=(Dexg%!KHmfC((wLW`QtL4g2i z*pQ?A@A=;^ta0W3PeKC0Kp>#8_y@2Ro`-Gl-|#xT0ej$A@Fr+H_%-Z<0gMF$VGx91 zB#eU5Fcli%WH<$;!F14AG6QD9a<~va4WEG)h(iLt2zy`s`A>ho`KLes>E3(yzIyMg z*IWCByuI1li(1JYssEF+6MY}4-8rMhjZwoZI@+?sn)q<5`xYxiev*-O>#IJLq3uo*6A{%(bD!Q-GgeHZ)}?18tSo_T$! zH^-mm>1deg$rWZ@|OwBiI4Igc=s}bubzn7zYz!3Y-N^a5gM~rLYT5WbJS|oB^MP zi=hp2aPYvvHxIsk@b&-u`oF`@f7|)O&gXVMvGalFc0O?Db332A5fg{2HtgK6QwOL@ zUg$$Z^nR;VT|^`4Czv;Evs05qfq~(%odO(InUJ z!uQ}Q*af>G$Z@@Dm;%!w3s=LPum!flHh2Mk$mypa!3*#ooD_Q%GORPR@IQquP~P%( zVX%+T@_J!DI??GX;Kh6dZpvH!wQs@7C*Z}m>%guR-W$hVY!8|ZzUuaxAy;h= zUW)lS%$8`E&-S3}vpwkgSXH+x$F4gyzZtFm^kq=n51}1LK?c;;SHM@`4p5sv4{Gn1 z;6Gt69Dx6U;k5h5VIrIakAklc1JQ+gm<#7Y3f9B5@YnDP_&T$T`&v#e?tk;8XI^^f zrF(9^=cR{kxbB+k)?e3=?YQQK^s1Is5&M^pTozG(s2#e(_KP1JkGQbEH{xMy)(-qT zYg)nK{*?#;Ft59U^K|s~3H`oL|Iuebk97Mop;oMX)`ph9Vp-81Kf;=>ZO#dy!i|+S zRc-MNl;JqvKiJb>J5~JE2;^GU-z!yVe@dbFhe(Ao+oqwjfgi(D@G=~L>S4_7a5S6 z;8U;~G;Vwo{t0%%Oh&$gd*66%_iMX7DXWTCE*B2(8ajq2;d7N=>Hi3;8@`D;4X{Kfm2f()j$MC+B^b zb;!CuoB)&IBDfU30r$aH_%-Z<#*xh1&;hr?x8M%=Hhh)Snb*Oi@JBG@f9N@*ybQYe z{H(j)dN%f38qeLad}#87GULBhPu5$53EO@Aw?20K@4B-?+GzEyCb$jM#@~kf;Tcd{ zJGA8|U@EA+XTn0b46={|wg2tlxBcI7|1>-U+u&9BCAq5!Tr9Mx4yjfA+Y8>y1VNR{%(Nl^m)x)*W9u3uF_rKije*9|C1#@f=aaA> z29IW*hDK7>94fntncp9F8zv7howeSesGRA#Zq?;!1E!5;)g_dr?%3HcE#c+ zKeK1udk=3La_tw*KbrII87MtrG&4NMF_)R;CT${Sx|wEXno~@pnabaEeE#&ZKK01e z`Uqw{wGgt=*y{qfacd3{a34D|iXyUQ+Z#sjf$^M3pn^Ub>#G|8(X*FS! z#7U2pMw_|kjy9Jq3{;0PTNn1P&O#0@F@dH4VJ@*L*wYQBzgcKv#EzPTNtqVo@aRcD za1}7M;{(I)+;4{8dAezuV2MCvEo)T_Fu9vtJQKBi~Nd~s56%`>x`-s-%A=W!g?^{$cFVy2lov?GHYHCV!Z z23qWAn~CGL06C@b)wPtm{ zFTf8G+>hV|_z%uez6u!%orM=+C+q{At?Lgbz+|`xE`@KveXtdN4f~*xb3SK72iywZ zf;-^b@Kw(7TnCTBldvBKbB?DT8lef+!)?E_Qz z(6zxs_gs>-ePSeYgNN?9J}SS#y_Dtm?N_eu6RCeyFXb#3uQ!=}6lwOB-$&v1$54ll zLN1jw?T}GChV<)AE)`uWDYK7k+&!EztGD*6_*L5P_qAsDL(tuiVm#BkfqFko?+EEV z5uFv+d10Ly)VU$|_klUJgev09YTW&$T8{VGU%<7y8`97_20CY|vu29JveIBRSPjR6 zNYr&_32E2h7ho=eR#*!+!cCxKLhASb0?)(ium^qxdqMl1QP2pdz%Dx zq&5y@kVp0=bL?+a^T!^0jL%N9VZ#RVt~RNIO>qAWIMtiF{ls-6RZDzT`?WgcfzAGj z*1i9JB~W|?%>WyZ9gns?HvAmSp`LpHcYfd5EHeZ5AIt5JDOtUFPUSP5eAe*>DlKo$ zs^ZmA%pv;~L3f>VkWqkC7uQq1dkZI$kjgmGW+H~krIVE29 zW)gr2V)-nbhD9$qK3ou1N+#u!Qc0PlL{c6pjg&=7BIOvurfd{w<0f8$exx-FhQmql zDAXc0QRkr-CF{ZQsHHOvgyTGBVK%{RcmytB&%Fp9fCpgxn(^ zHtay>UW7M1_F#&Apv~MM)I?kYpM{)9JEph-w88tjhcEg+w8%)%miuPVmit-I4+b9# zOW-1S5FP@Uey`bXdThyT#y$o7SD{Sfu$Iu{KWt~NlCa zB#!wXWf;%B-W$=wIK6qOx2E(4l}?kUCrvdvb!y)vT+(FpzTxe)^UTpR78<>`q6cw0 z+N`%xbjDprnRV7xZ!+o(tR9}~JbmY=ZyLR&rT1m@o{QeU)VoSLv91%fdIwAIsOaq$ zy~C?FB0HO^HU#o@SU9PLpE7Xm6xh1U4ANWdrI~z*X0;|wT8!XS_d@$DF~t+p4({Zt ziu;!vN})A(>22CR1wVO$&3#~Pbx>E? z+-LLTG(k>DQaEdxjnl8He=t}RtS%d8E_t-uczW*Ymzp=VP>`v0SC2OPbp}||B5#RD zuBhF#e&Yh&A8Ur;wgta`V*m^AWLzp$RX-N9yQ^Gujjb{z7cH)D<+FDQ)b+c)X0zFB zuDo(653cK3MMsICwe%)5UVNB~X7sMQ#xD&~JJs1`lE~JYwJ?`%uOlLgH{m&0P zWB-eCxZlk%F%~C8fAfnz5Ur1+y&3vV)Hr-|kj9L8F$Uw?Zu?f$e*bZv?NKqlg;-_d z9Lm2(#(%O7?=#LZ0QIOp-P!E=^0(f4Z*jc--s|hkiJ$oXOH**W3J>k~hfBokKZD164nfqg+bNPvA>!$MPlE2~>{lk* zEUB86sYXxPHLl96TonmaBv6q+MFJHGR3uQ5Kt%!-2~;Fdkw8TP6$u=U1j_CIwXY8D zcz(w>8$MQlBR}MP;^gp**D?OEfXy?Pty8NepcGXOm!&~t;^LErn{ z4ScI*?t^;JGXw4E9|L`V{50qpz;;mDuS4Npzxy70ZaUMJ=6`uCwYrzAO1*0D zzZa``r<1ZwefNI2=}qM&eqC;Q$5~U)XbwL;NytyH-lkLcq0&3GAiW_rJ<0s=%QxMo zSNW(&pdx{a1S%4!NT4EtiUcYWs7Rn9fr$MNl{#oD6 zYpq&^=6myo8Qs-hO@D6@Z9EcUMB~KdkrwbeEup&U((S#zgBrMrWU*z-IutRJOY4YFXc0=_3IfnctarN82=C2eRyvG_f-bpH* zNmSdPgBCQ~GRY;!2|~SDUjCYR81g?^$!YN5s_9X4_heUJ%QJ)gHEyZyx@@9{_?RVs zFaLl&S=P$)%Eg?py!;R`dwLJ$>7ARi``I%@X_wWdoe#_PaPs#vlJ5Q~jB|xDs@!); zAVBNufBMO#c%QJr J%>Nw;{D0vdC-(pV literal 0 HcmV?d00001 diff --git a/src/Shared/StringBuilderCache.cs b/src/Shared/StringBuilderCache.cs new file mode 100644 index 00000000000..e4cab6545cc --- /dev/null +++ b/src/Shared/StringBuilderCache.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/*============================================================ +** +** +** Purpose: provide a cached reusable instance of StringBuilder +** per thread it's an optimisation that reduces the +** number of instances constructed and collected. +** +** Acquire - is used to get a string builder to use of a +** particular size. It can be called any number of +** times, if a StringBuilder is in the cache then +** it will be returned and the cache emptied. +** subsequent calls will return a new StringBuilder. +** +** A StringBuilder instance is cached in +** Thread Local Storage and so there is one per thread +** +** Release - Place the specified builder in the cache if it is +** not too big. +** The StringBuilder should not be used after it has +** been released. +** Unbalanced Releases are perfectly acceptable. It +** will merely cause the runtime to create a new +** StringBuilder next time Acquire is called. +** +** GetStringAndRelease +** - ToString() the StringBuilder, Release it to the +** cache and return the resulting string +** +===========================================================*/ + +using System; +using System.Text; + +namespace Microsoft.Build.Shared +{ + internal static class StringBuilderCache + { + // The value 360 was chosen in discussion with performance experts as a compromise between using + // as litle memory (per thread) as possible and still covering a large part of short-lived + // StringBuilder creations on the startup path of VS designers. + private const int MAX_BUILDER_SIZE = 360; + + [ThreadStatic] + private static StringBuilder t_cachedInstance; + + public static StringBuilder Acquire(int capacity = 16 /*StringBuilder.DefaultCapacity*/) + { + if (capacity <= MAX_BUILDER_SIZE) + { + StringBuilder sb = StringBuilderCache.t_cachedInstance; + if (sb != null) + { + // Avoid StringBuilder block fragmentation by getting a new StringBuilder + // when the requested size is larger than the current capacity + if (capacity <= sb.Capacity) + { + StringBuilderCache.t_cachedInstance = null; + sb.Length = 0; // Equivalent of sb.Clear() that works on .Net 3.5 + return sb; + } + } + } + return new StringBuilder(capacity); + } + + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= MAX_BUILDER_SIZE) + { + StringBuilderCache.t_cachedInstance = sb; + } + } + + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } + } +} diff --git a/src/Shared/Strings.shared.resx b/src/Shared/Strings.shared.resx new file mode 100644 index 00000000000..9a35ddb292e --- /dev/null +++ b/src/Shared/Strings.shared.resx @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + MSB4188: Build was canceled. + {StrBegin="MSB4188: "} Error when the build stops suddenly for some reason. For example, because a child node died. + + + MSB5022: The MSBuild task host does not support running tasks that perform IBuildEngine callbacks. If you wish to perform these operations, please run your task in the core MSBuild process instead. A task will automatically execute in the task host if the UsingTask has been attributed with a "Runtime" or "Architecture" value, or the task invocation has been attributed with an "MSBuildRuntime" or "MSBuildArchitecture" value, that does not match the current runtime or architecture of MSBuild. + {StrBegin="MSB5022: "} "Runtime", "Architecture", "MSBuildRuntime", and "MSBuildArchitecture" are all attributes in the project file, and thus should not be localized. + + + Build started. + + + MSB4008: A conflicting assembly for the task assembly "{0}" has been found at "{1}". + {StrBegin="MSB4008: "}UE: This message is shown when the type/class of a task cannot be resolved uniquely from a single assembly. + + + Event type "{0}" was expected to be serializable using the .NET serializer. The event was not serializable and has been ignored. + + + {0} ({1},{2}) + A file location to be embedded in a string. + + + Making the following modifications to the environment received from the parent node before applying it to the task host: + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + Setting '{0}' to '{1}' rather than the parent environment's value, '{2}'. + Only ever used when MSBuild is run under a "secret" environment variable switch, MSBuildTaskHostUpdateEnvironmentAndLog=1 + + + MSB4025: The project file could not be loaded. {0} + {StrBegin="MSB4025: "}UE: This message is shown when the project file given to the engine cannot be loaded because the filename/path is + invalid, or due to lack of permissions, or incorrect XML. The project filename is not part of the message because it is + provided separately to loggers. + LOCALIZATION: {0} is a localized message from the CLR/FX explaining why the project is invalid. + + + MSB4103: "{0}" is not a valid logger verbosity level. + {StrBegin="MSB4103: "} + + + MSBuild is expecting a valid "{0}" object. + + + MSB4132: The tools version "{0}" is unrecognized. Available tools versions are {1}. + {StrBegin="MSB4132: "}LOCALIZATION: {1} contains a comma separated list. + + + MSB5016: The name "{0}" contains an invalid character "{1}". + {StrBegin="MSB5016: "} + + + "{0}" is a reserved item metadata, and cannot be modified or deleted. + UE: Tasks and OM users are not allowed to remove or change the value of the built-in metadata on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + The string "{0}" cannot be converted to a boolean (true/false) value. + + + MSB5003: Failed to create a temporary file. Temporary files folder is full or its path is incorrect. {0} + {StrBegin="MSB5003: "} + + + MSB5018: Failed to delete the temporary file "{0}". {1} + {StrBegin="MSB5018: "} + + + The item metadata "%({0})" cannot be applied to the path "{1}". {2} + UE: This message is shown when the user tries to perform path manipulations using one of the built-in item metadata e.g. %(RootDir), on an item-spec that's not a valid path. LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4077: The "{0}" task has been marked with the attribute LoadInSeparateAppDomain, but does not derive from MarshalByRefObject. Check that the task derives from MarshalByRefObject or AppDomainIsolatedTask. + {StrBegin="MSB4077: "}LOCALIZATION: <LoadInSeparateAppDomain>, <MarshalByRefObject>, <AppDomainIsolatedTask> should not be localized. + + + .NET Framework version "{0}" is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion. + + + .NET Framework version "{0}" is not supported when explicitly targeting the Windows SDK, which is only supported on .NET 4.5 and later. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion that is Version45 or above. + + + Visual Studio version "{0}" is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.VisualStudioVersion. + + + When attempting to generate a reference assembly path from the path "{0}" and the framework moniker "{1}" there was an error. {2} + No Error code because this resource will be used in an exception. The error code is discarded if it is included + + + Could not find directory path: {0} + Directory must exist + + + You do not have access to: {0} + Directory must have access + + + Schema validation + + UE: this fragment is used to describe errors that are caused by schema validation. For example, if a normal error is + displayed like this: "MSBUILD : error MSB0000: This is an error.", then an error from schema validation would look like this: + "MSBUILD : Schema validation error MSB0000: This is an error." + LOCALIZATION: This fragment needs to be localized. + + + + MSB5002: The task executable has not completed within the specified limit of {0} milliseconds, terminating. + {StrBegin="MSB5002: "} + + + Parameter "{0}" cannot be null. + + + Parameter "{0}" cannot have zero length. + + + Parameters "{0}" and "{1}" must have the same number of elements. + + + The resource string "{0}" for the "{1}" task cannot be found. Confirm that the resource name "{0}" is correctly spelled, and the resource exists in the task's assembly. + + + The "{0}" task has not registered its resources. In order to use the "TaskLoggingHelper.FormatResourceString()" method this task needs to register its resources either during construction, or via the "TaskResources" property. + LOCALIZATION: "TaskLoggingHelper.FormatResourceString()" and "TaskResources" should not be localized. + + + MSB5004: The solution file has two projects named "{0}". + {StrBegin="MSB5004: "}UE: The solution filename is provided separately to loggers. + + + MSB5005: Error parsing project section for project "{0}". The project file name "{1}" contains invalid characters. + {StrBegin="MSB5005: "}UE: The solution filename is provided separately to loggers. + + + MSB5006: Error parsing project section for project "{0}". The project file name is empty. + {StrBegin="MSB5006: "}UE: The solution filename is provided separately to loggers. + + + MSB5007: Error parsing the project configuration section in solution file. The entry "{0}" is invalid. + {StrBegin="MSB5007: "}UE: The solution filename is provided separately to loggers. + + + MSB5008: Error parsing the solution configuration section in solution file. The entry "{0}" is invalid. + {StrBegin="MSB5008: "}UE: The solution filename is provided separately to loggers. + + + MSB5009: Error parsing the nested project section in solution file. + {StrBegin="MSB5009: "}UE: The solution filename is provided separately to loggers. + + + MSB5023: Error parsing the nested project section in solution file. A project with the GUID "{0}" is listed as being nested under project "{1}", but does not exist in the solution. + {StrBegin="MSB5023: "}UE: The solution filename is provided separately to loggers. + + + MSB5010: No file format header found. + {StrBegin="MSB5010: "}UE: The solution filename is provided separately to loggers. + + + MSB5011: Parent project GUID not found in "{0}" project dependency section. + {StrBegin="MSB5011: "}UE: The solution filename is provided separately to loggers. + + + MSB5012: Unexpected end-of-file reached inside "{0}" project section. + {StrBegin="MSB5012: "}UE: The solution filename is provided separately to loggers. + + + MSB5013: Error parsing a project section. + {StrBegin="MSB5013: "}UE: The solution filename is provided separately to loggers. + + + MSB5014: File format version is not recognized. MSBuild can only read solution files between versions {0}.0 and {1}.0, inclusive. + {StrBegin="MSB5014: "}UE: The solution filename is provided separately to loggers. + + + MSB5015: The properties could not be read from the WebsiteProperties section of the "{0}" project. + {StrBegin="MSB5015: "}UE: The solution filename is provided separately to loggers. + + + Unrecognized solution version "{0}", attempting to continue. + + + Solution file + UE: this fragment is used to describe errors found while parsing solution files. For example, if a normal error is + displayed like this: "MSBUILD : error MSB0000: This is an error.", then an error from solution parsing would look like this: + "MSBUILD : Solution file error MSB0000: This is an error." + LOCALIZATION: This fragment needs to be localized. + + + MSB5019: The project file is malformed: "{0}". {1} + {StrBegin="MSB5019: "} + + + MSB5020: Could not load the project file: "{0}". {1} + {StrBegin="MSB5020: "} + + + MSB5021: "{0}" and its child processes are being terminated in order to cancel the build. + {StrBegin="MSB5021: "} + + + + + + + + This collection is read-only. + + diff --git a/src/Shared/StrongNameHelpers.cs b/src/Shared/StrongNameHelpers.cs new file mode 100644 index 00000000000..b869e9785ff --- /dev/null +++ b/src/Shared/StrongNameHelpers.cs @@ -0,0 +1,719 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Security; +using System.Text; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Runtime.Hosting +{ + /// + /// The methods here are designed to aid in transition from the v2 StrongName APIs on mscoree.dll to the + /// v4 metahost APIs (which are in-proc SxS aware). + /// + internal static class StrongNameHelpers + { + [ThreadStatic] + private static int t_ts_LastStrongNameHR; + + [System.Security.SecurityCritical] + [ThreadStatic] + private static IClrStrongName s_StrongName; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + private static IClrStrongName StrongName + { + [System.Security.SecurityCritical] + get + { + if (s_StrongName == null) + { + s_StrongName = (IClrStrongName)RuntimeEnvironment.GetRuntimeInterfaceAsObject( + new Guid("B79B0ACD-F5CD-409b-B5A5-A16244610B92"), + new Guid("9FD93CCF-3280-4391-B3A9-96E1CDE77C8D")); + } + return s_StrongName; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + private static IClrStrongNameUsingIntPtr StrongNameUsingIntPtr + { + [System.Security.SecurityCritical] + get + { + return (IClrStrongNameUsingIntPtr)StrongName; + } + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static int StrongNameErrorInfo() + { + return t_ts_LastStrongNameHR; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "Microsoft.Runtime.Hosting.IClrStrongNameUsingIntPtr.StrongNameFreeBuffer(System.IntPtr)", Justification = "StrongNameFreeBuffer returns void but the new runtime wrappers return an HRESULT.")] + public static void StrongNameFreeBuffer(IntPtr pbMemory) + { + StrongNameUsingIntPtr.StrongNameFreeBuffer(pbMemory); + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameGetPublicKey(string pwzKeyContainer, IntPtr pbKeyBlob, int cbKeyBlob, out IntPtr ppbPublicKeyBlob, out int pcbPublicKeyBlob) + { + int hr = StrongNameUsingIntPtr.StrongNameGetPublicKey(pwzKeyContainer, pbKeyBlob, cbKeyBlob, out ppbPublicKeyBlob, out pcbPublicKeyBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + ppbPublicKeyBlob = IntPtr.Zero; + pcbPublicKeyBlob = 0; + return false; + } + return true; + } + + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameKeyDelete(string pwzKeyContainer) + { + int hr = StrongName.StrongNameKeyDelete(pwzKeyContainer); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameKeyGen(string pwzKeyContainer, int dwFlags, out IntPtr ppbKeyBlob, out int pcbKeyBlob) + { + int hr = StrongName.StrongNameKeyGen(pwzKeyContainer, dwFlags, out ppbKeyBlob, out pcbKeyBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + ppbKeyBlob = IntPtr.Zero; + pcbKeyBlob = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameKeyInstall(string pwzKeyContainer, IntPtr pbKeyBlob, int cbKeyBlob) + { + int hr = StrongNameUsingIntPtr.StrongNameKeyInstall(pwzKeyContainer, pbKeyBlob, cbKeyBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureGeneration(string pwzFilePath, string pwzKeyContainer, IntPtr pbKeyBlob, int cbKeyBlob) + { + IntPtr ppbSignatureBlob = IntPtr.Zero; + int cbSignatureBlob = 0; + return StrongNameSignatureGeneration(pwzFilePath, pwzKeyContainer, pbKeyBlob, cbKeyBlob, ref ppbSignatureBlob, out cbSignatureBlob); + } + + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureGeneration(string pwzFilePath, string pwzKeyContainer, IntPtr pbKeyBlob, int cbKeyBlob, ref IntPtr ppbSignatureBlob, out int pcbSignatureBlob) + { + int hr = StrongNameUsingIntPtr.StrongNameSignatureGeneration(pwzFilePath, pwzKeyContainer, pbKeyBlob, cbKeyBlob, ppbSignatureBlob, out pcbSignatureBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + pcbSignatureBlob = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureSize(IntPtr pbPublicKeyBlob, int cbPublicKeyBlob, out int pcbSize) + { + int hr = StrongNameUsingIntPtr.StrongNameSignatureSize(pbPublicKeyBlob, cbPublicKeyBlob, out pcbSize); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + pcbSize = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureVerification(string pwzFilePath, int dwInFlags, out int pdwOutFlags) + { + int hr = StrongName.StrongNameSignatureVerification(pwzFilePath, dwInFlags, out pdwOutFlags); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + pdwOutFlags = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureVerificationEx(string pwzFilePath, bool fForceVerification, out bool pfWasVerified) + { + int hr = StrongName.StrongNameSignatureVerificationEx(pwzFilePath, fForceVerification, out pfWasVerified); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + pfWasVerified = false; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameTokenFromPublicKey(IntPtr pbPublicKeyBlob, int cbPublicKeyBlob, out IntPtr ppbStrongNameToken, out int pcbStrongNameToken) + { + int hr = StrongNameUsingIntPtr.StrongNameTokenFromPublicKey(pbPublicKeyBlob, cbPublicKeyBlob, out ppbStrongNameToken, out pcbStrongNameToken); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + ppbStrongNameToken = IntPtr.Zero; + pcbStrongNameToken = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureSize(byte[] bPublicKeyBlob, int cbPublicKeyBlob, out int pcbSize) + { + int hr = StrongName.StrongNameSignatureSize(bPublicKeyBlob, cbPublicKeyBlob, out pcbSize); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + pcbSize = 0; + return false; + } + return true; + } + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameTokenFromPublicKey(byte[] bPublicKeyBlob, int cbPublicKeyBlob, out IntPtr ppbStrongNameToken, out int pcbStrongNameToken) + { + int hr = StrongName.StrongNameTokenFromPublicKey(bPublicKeyBlob, cbPublicKeyBlob, out ppbStrongNameToken, out pcbStrongNameToken); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + ppbStrongNameToken = IntPtr.Zero; + pcbStrongNameToken = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameGetPublicKey(string pwzKeyContainer, byte[] bKeyBlob, int cbKeyBlob, out IntPtr ppbPublicKeyBlob, out int pcbPublicKeyBlob) + { + int hr = StrongName.StrongNameGetPublicKey(pwzKeyContainer, bKeyBlob, cbKeyBlob, out ppbPublicKeyBlob, out pcbPublicKeyBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + ppbPublicKeyBlob = IntPtr.Zero; + pcbPublicKeyBlob = 0; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameKeyInstall(string pwzKeyContainer, byte[] bKeyBlob, int cbKeyBlob) + { + int hr = StrongName.StrongNameKeyInstall(pwzKeyContainer, bKeyBlob, cbKeyBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + return false; + } + return true; + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureGeneration(string pwzFilePath, string pwzKeyContainer, byte[] bKeyBlob, int cbKeyBlob) + { + IntPtr ppbSignatureBlob = IntPtr.Zero; + int cbSignatureBlob = 0; + return StrongNameSignatureGeneration(pwzFilePath, pwzKeyContainer, bKeyBlob, cbKeyBlob, ref ppbSignatureBlob, out cbSignatureBlob); + } + + [System.Security.SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This file is included in a lot of projects some of which only use a subset of the functions.")] + public static bool StrongNameSignatureGeneration(string pwzFilePath, string pwzKeyContainer, byte[] bKeyBlob, int cbKeyBlob, ref IntPtr ppbSignatureBlob, out int pcbSignatureBlob) + { + int hr = StrongName.StrongNameSignatureGeneration(pwzFilePath, pwzKeyContainer, bKeyBlob, cbKeyBlob, ppbSignatureBlob, out pcbSignatureBlob); + if (hr < 0) + { + t_ts_LastStrongNameHR = hr; + pcbSignatureBlob = 0; + return false; + } + return true; + } + } + + /// + /// This is a managed wrapper for the IClrStrongName interface defined in metahost.idl + /// This uses IntPtrs in some places where you'd normally expect a byte[] in order to + /// be compatible with callers who wrote their PInvoke signatures that way. + /// Ideally we'd probably just simplify all such callers to using byte[] and remove this + /// version of the interface. + /// + [System.Security.SecurityCritical] + [ComImport, ComConversionLoss, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("9FD93CCF-3280-4391-B3A9-96E1CDE77C8D")] + internal interface IClrStrongNameUsingIntPtr + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromAssemblyFile( + [In, MarshalAs(UnmanagedType.LPStr)] string pszFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromAssemblyFileW( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromBlob( + [In] IntPtr pbBlob, + [In, MarshalAs(UnmanagedType.U4)] int cchBlob, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromFile( + [In, MarshalAs(UnmanagedType.LPStr)] string pszFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromFileW( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromHandle( + [In] IntPtr hFile, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameCompareAssemblies( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzAssembly1, + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzAssembly2, + [MarshalAs(UnmanagedType.U4)] out int dwResult); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameFreeBuffer( + [In] IntPtr pbMemory); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameGetBlob( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] pbBlob, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int pcbBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameGetBlobFromImage( + [In] IntPtr pbBase, + [In, MarshalAs(UnmanagedType.U4)] int dwLength, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbBlob, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int pcbBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameGetPublicKey( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In] IntPtr pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob, + out IntPtr ppbPublicKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbPublicKeyBlob); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameHashSize( + [In, MarshalAs(UnmanagedType.U4)] int ulHashAlg, + [MarshalAs(UnmanagedType.U4)] out int cbSize); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyDelete( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyGen( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.U4)] int dwFlags, + out IntPtr ppbKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyGenEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.U4)] int dwFlags, + [In, MarshalAs(UnmanagedType.U4)] int dwKeySize, + out IntPtr ppbKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyInstall( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In] IntPtr pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureGeneration( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In] IntPtr pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob, + [In, Out] IntPtr ppbSignatureBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbSignatureBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureGenerationEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string wszFilePath, + [In, MarshalAs(UnmanagedType.LPWStr)] string wszKeyContainer, + [In] IntPtr pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob, + [In, Out] IntPtr ppbSignatureBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbSignatureBlob, + [In, MarshalAs(UnmanagedType.U4)] int dwFlags); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureSize( + [In] IntPtr pbPublicKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbPublicKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbSize); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureVerification( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, MarshalAs(UnmanagedType.U4)] int dwInFlags, + [MarshalAs(UnmanagedType.U4)] out int dwOutFlags); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureVerificationEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, MarshalAs(UnmanagedType.I1)] bool fForceVerification, + [MarshalAs(UnmanagedType.I1)] out bool fWasVerified); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureVerificationFromImage( + [In] IntPtr pbBase, + [In, MarshalAs(UnmanagedType.U4)] int dwLength, + [In, MarshalAs(UnmanagedType.U4)] int dwInFlags, + [MarshalAs(UnmanagedType.U4)] out int dwOutFlags); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameTokenFromAssembly( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + out IntPtr ppbStrongNameToken, + [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameTokenFromAssemblyEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + out IntPtr ppbStrongNameToken, + [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken, + out IntPtr ppbPublicKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbPublicKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameTokenFromPublicKey( + [In] IntPtr pbPublicKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbPublicKeyBlob, + out IntPtr ppbStrongNameToken, + [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken); + } + + /// + /// This is a managed wrapper for the IClrStrongName interface defined in metahost.idl + /// This is very similar to the standard RCWs provided in + /// ndp/fx/src/hosting/interop/microsoft/runtime/hosting/interop, but we don't want to + /// reference that assembly (part of the SDK only, not .NET redist). Also, our version + /// is designed specifically for easy migration from the old mscoree APIs, for example + /// all APIs return HResults rather than throw exceptions. + /// + [System.Security.SecurityCritical] + [ComImport, ComConversionLoss, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("9FD93CCF-3280-4391-B3A9-96E1CDE77C8D")] + internal interface IClrStrongName + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromAssemblyFile( + [In, MarshalAs(UnmanagedType.LPStr)] string pszFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromAssemblyFileW( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromBlob( + [In] IntPtr pbBlob, + [In, MarshalAs(UnmanagedType.U4)] int cchBlob, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromFile( + [In, MarshalAs(UnmanagedType.LPStr)] string pszFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromFileW( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int GetHashFromHandle( + [In] IntPtr hFile, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int piHashAlg, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbHash, + [In, MarshalAs(UnmanagedType.U4)] int cchHash, + [MarshalAs(UnmanagedType.U4)] out int pchHash); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameCompareAssemblies( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzAssembly1, + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzAssembly2, + [MarshalAs(UnmanagedType.U4)] out int dwResult); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameFreeBuffer( + [In] IntPtr pbMemory); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameGetBlob( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] pbBlob, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int pcbBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameGetBlobFromImage( + [In] IntPtr pbBase, + [In, MarshalAs(UnmanagedType.U4)] int dwLength, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbBlob, + [In, Out, MarshalAs(UnmanagedType.U4)] ref int pcbBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameGetPublicKey( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob, + out IntPtr ppbPublicKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbPublicKeyBlob); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameHashSize( + [In, MarshalAs(UnmanagedType.U4)] int ulHashAlg, + [MarshalAs(UnmanagedType.U4)] out int cbSize); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyDelete( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyGen( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.U4)] int dwFlags, + out IntPtr ppbKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyGenEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.U4)] int dwFlags, + [In, MarshalAs(UnmanagedType.U4)] int dwKeySize, + out IntPtr ppbKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameKeyInstall( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureGeneration( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzKeyContainer, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob, + [In, Out] IntPtr ppbSignatureBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbSignatureBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureGenerationEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string wszFilePath, + [In, MarshalAs(UnmanagedType.LPWStr)] string wszKeyContainer, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] byte[] pbKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbKeyBlob, + [In, Out] IntPtr ppbSignatureBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbSignatureBlob, + [In, MarshalAs(UnmanagedType.U4)] int dwFlags); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureSize( + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pbPublicKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbPublicKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbSize); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureVerification( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, MarshalAs(UnmanagedType.U4)] int dwInFlags, + [MarshalAs(UnmanagedType.U4)] out int dwOutFlags); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureVerificationEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + [In, MarshalAs(UnmanagedType.I1)] bool fForceVerification, + [MarshalAs(UnmanagedType.I1)] out bool fWasVerified); + + [return: MarshalAs(UnmanagedType.U4)] + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameSignatureVerificationFromImage( + [In] IntPtr pbBase, + [In, MarshalAs(UnmanagedType.U4)] int dwLength, + [In, MarshalAs(UnmanagedType.U4)] int dwInFlags, + [MarshalAs(UnmanagedType.U4)] out int dwOutFlags); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameTokenFromAssembly( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + out IntPtr ppbStrongNameToken, + [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameTokenFromAssemblyEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string pwzFilePath, + out IntPtr ppbStrongNameToken, + [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken, + out IntPtr ppbPublicKeyBlob, + [MarshalAs(UnmanagedType.U4)] out int pcbPublicKeyBlob); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig] + int StrongNameTokenFromPublicKey( + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pbPublicKeyBlob, + [In, MarshalAs(UnmanagedType.U4)] int cbPublicKeyBlob, + out IntPtr ppbStrongNameToken, + [MarshalAs(UnmanagedType.U4)] out int pcbStrongNameToken); + } +} diff --git a/src/Shared/TaskEngineAssemblyResolver.cs b/src/Shared/TaskEngineAssemblyResolver.cs new file mode 100644 index 00000000000..f0b8760a791 --- /dev/null +++ b/src/Shared/TaskEngineAssemblyResolver.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.Security.Permissions; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// This is a helper class to install an AssemblyResolver event handler in whatever AppDomain this class is created in. + /// + internal class TaskEngineAssemblyResolver : MarshalByRefObject + { + /// + /// This public default constructor is needed so that instances of this class can be created by NDP. + /// + internal TaskEngineAssemblyResolver() + { + // do nothing + } + + /// + /// Initializes the instance. + /// + /// + internal void Initialize(string taskAssemblyFileToResolve) + { + _taskAssemblyFile = taskAssemblyFileToResolve; + } + + /// + /// Installs an AssemblyResolve handler in the current AppDomain. This class can be created in any AppDomain, + /// so it's possible to create an AppDomain, create an instance of this class in it and use this method to install + /// an event handler in that AppDomain. Since the event handler instance is stored internally, this method + /// should only be called once before a corresponding call to RemoveHandler (not that it would make sense to do + /// anything else). + /// + internal void InstallHandler() + { + Debug.Assert(_eventHandler == null, "The TaskEngineAssemblyResolver.InstallHandler method should only be called once!"); + + _eventHandler = new ResolveEventHandler(ResolveAssembly); + + AppDomain.CurrentDomain.AssemblyResolve += _eventHandler; + } + + /// + /// Removes the event handler. + /// + internal void RemoveHandler() + { + if (_eventHandler != null) + { + AppDomain.CurrentDomain.AssemblyResolve -= _eventHandler; + _eventHandler = null; + } + else + { + Debug.Assert(false, "There is no handler to remove."); + } + } + + + /// + /// This is an assembly resolution handler necessary for fixing up types instantiated in different + /// AppDomains and loaded with a Assembly.LoadFrom equivalent call. See comments in TaskEngine.ExecuteTask + /// for more details. + /// + /// + /// + /// + internal Assembly ResolveAssembly(object sender, ResolveEventArgs args) + { + // Is this our task assembly? + if (_taskAssemblyFile != null) + { + if (File.Exists(_taskAssemblyFile)) + { + try + { + AssemblyNameExtension taskAssemblyName = new AssemblyNameExtension(AssemblyName.GetAssemblyName(_taskAssemblyFile)); + AssemblyNameExtension argAssemblyName = new AssemblyNameExtension(args.Name); + + if (taskAssemblyName.Equals(argAssemblyName)) + { +#if (!CLR2COMPATIBILITY) + return Assembly.UnsafeLoadFrom(_taskAssemblyFile); +#else + return Assembly.LoadFrom(taskAssemblyFile); +#endif + } + } + // any problems with the task assembly? return null. + catch (FileNotFoundException) + { + return null; + } + catch (BadImageFormatException) + { + return null; + } + } + } + + // otherwise, have a nice day. + return null; + } + + /// + /// Overridden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + [System.Security.SecurityCritical] + public override object InitializeLifetimeService() + { + // null means infinite lease time + return null; + } + + // path to the task assembly, but only if it's loaded using LoadFrom. If it's loaded with Load, this is null. + private string _taskAssemblyFile = null; + + // we have to store the event handler instance in case we have to remove it + private ResolveEventHandler _eventHandler = null; + } +} diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs new file mode 100644 index 00000000000..29c9704e8a3 --- /dev/null +++ b/src/Shared/TaskHostConfiguration.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A packet which contains information needed for the task host to +// configure itself for to execute a particular task. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Text; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// TaskHostConfiguration contains information needed for the task host to + /// configure itself for to execute a particular task. + /// + internal class TaskHostConfiguration : INodePacket + { + /// + /// The node id (of the parent node, to make the logging work out) + /// + private int _nodeId; + + /// + /// The startup directory + /// + private string _startupDirectory; + + /// + /// The process environment. + /// + private Dictionary _buildProcessEnvironment; + + /// + /// The culture + /// + private CultureInfo _culture = Thread.CurrentThread.CurrentCulture; + + /// + /// The UI culture. + /// + private CultureInfo _uiCulture = Thread.CurrentThread.CurrentUICulture; + + /// + /// The AppDomainSetup that we may want to use on AppDomainIsolated tasks. + /// + private AppDomainSetup _appDomainSetup; + + /// + /// Line number where the instance of this task is defined. + /// + private int _lineNumberOfTask; + + /// + /// Column number where the instance of this task is defined. + /// + private int _columnNumberOfTask; + + /// + /// Project file where the instance of this task is defined. + /// + private string _projectFileOfTask; + + /// + /// ContinueOnError flag for this particular task. + /// + private bool _continueOnError; + + /// + /// Name of the task to be executed on the task host. + /// + private string _taskName; + + /// + /// Location of the assembly containing the task to be executed. + /// + private string _taskLocation; + + /// + /// The set of parameters to apply to the task prior to execution. + /// + private Dictionary _taskParameters; + + /// + /// Constructor + /// + /// The ID of the node being configured. + /// The startup directory for the task being executed. + /// The set of environment variables to apply to the task execution process. + /// The culture of the thread that will execute the task. + /// The UI culture of the thread that will execute the task. + /// The AppDomainSetup that may be used to pass information to an AppDomainIsolated task. + /// The line number of the location from which this task was invoked. + /// The column number of the location from which this task was invoked. + /// The project file from which this task was invoked. + /// Flag to continue with the build after a the task failed + /// Name of the task. + /// Location of the assembly the task is to be loaded from. + /// Parameters to apply to the task. + public TaskHostConfiguration + ( + int nodeId, + string startupDirectory, + IDictionary buildProcessEnvironment, + CultureInfo culture, + CultureInfo uiCulture, + AppDomainSetup appDomainSetup, + int lineNumberOfTask, + int columnNumberOfTask, + string projectFileOfTask, + bool continueOnError, + string taskName, + string taskLocation, + IDictionary taskParameters + ) + { + ErrorUtilities.VerifyThrowInternalLength(taskName, "taskName"); + ErrorUtilities.VerifyThrowInternalLength(taskLocation, "taskLocation"); + + _nodeId = nodeId; + _startupDirectory = startupDirectory; + + if (buildProcessEnvironment != null) + { + _buildProcessEnvironment = buildProcessEnvironment as Dictionary; + + if (_buildProcessEnvironment == null) + { + _buildProcessEnvironment = new Dictionary(buildProcessEnvironment); + } + } + + _culture = culture; + _uiCulture = uiCulture; + _appDomainSetup = appDomainSetup; + _lineNumberOfTask = lineNumberOfTask; + _columnNumberOfTask = columnNumberOfTask; + _projectFileOfTask = projectFileOfTask; + _continueOnError = continueOnError; + _taskName = taskName; + _taskLocation = taskLocation; + + if (taskParameters != null) + { + _taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair parameter in taskParameters) + { + _taskParameters[parameter.Key] = new TaskParameter(parameter.Value); + } + } + } + + /// + /// Constructor for deserialization. + /// + private TaskHostConfiguration() + { + } + + /// + /// The node id + /// + public int NodeId + { + [DebuggerStepThrough] + get + { return _nodeId; } + } + + /// + /// The startup directory + /// + public string StartupDirectory + { + [DebuggerStepThrough] + get + { return _startupDirectory; } + } + + /// + /// The process environment. + /// + public Dictionary BuildProcessEnvironment + { + [DebuggerStepThrough] + get + { return _buildProcessEnvironment; } + } + + /// + /// The culture + /// + public CultureInfo Culture + { + [DebuggerStepThrough] + get + { return _culture; } + } + + /// + /// The UI culture. + /// + public CultureInfo UICulture + { + [DebuggerStepThrough] + get + { return _uiCulture; } + } + + /// + /// The AppDomain configuration bytes that we may want to use to initialize + /// AppDomainIsolated tasks. + /// + public AppDomainSetup AppDomainSetup + { + [DebuggerStepThrough] + get + { return _appDomainSetup; } + } + + /// + /// Line number where the instance of this task is defined. + /// + public int LineNumberOfTask + { + [DebuggerStepThrough] + get + { return _lineNumberOfTask; } + } + + /// + /// Column number where the instance of this task is defined. + /// + public int ColumnNumberOfTask + { + [DebuggerStepThrough] + get + { return _columnNumberOfTask; } + } + + /// + /// ContinueOnError flag for this particular task + /// + public bool ContinueOnError + { + [DebuggerStepThrough] + get + { return _continueOnError; } + } + + /// + /// Project file where the instance of this task is defined. + /// + public string ProjectFileOfTask + { + [DebuggerStepThrough] + get + { return _projectFileOfTask; } + } + + /// + /// Name of the task to execute. + /// + public string TaskName + { + [DebuggerStepThrough] + get + { return _taskName; } + } + + /// + /// Path to the assembly to load the task from. + /// + public string TaskLocation + { + [DebuggerStepThrough] + get + { return _taskLocation; } + } + + /// + /// Parameters to set on the instantiated task prior to execution. + /// + public Dictionary TaskParameters + { + [DebuggerStepThrough] + get + { return _taskParameters; } + } + + /// + /// The NodePacketType of this NodePacket + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.TaskHostConfiguration; } + } + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _nodeId); + translator.Translate(ref _startupDirectory); + translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); + translator.TranslateCulture(ref _culture); + translator.TranslateCulture(ref _uiCulture); + translator.TranslateDotNet(ref _appDomainSetup); + translator.Translate(ref _lineNumberOfTask); + translator.Translate(ref _columnNumberOfTask); + translator.Translate(ref _projectFileOfTask); + translator.Translate(ref _taskName); + translator.Translate(ref _taskLocation); + translator.TranslateDictionary(ref _taskParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization); + translator.Translate(ref _continueOnError); + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + TaskHostConfiguration configuration = new TaskHostConfiguration(); + configuration.Translate(translator); + return configuration; + } + } +} diff --git a/src/Shared/TaskHostTaskCancelled.cs b/src/Shared/TaskHostTaskCancelled.cs new file mode 100644 index 00000000000..80f8298156a --- /dev/null +++ b/src/Shared/TaskHostTaskCancelled.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A packet which informs the task host that the task it is +// currently executing has been cancelled. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// TaskHostTaskCancelled informs the task host that the task it is + /// currently executing has been cancelled. + /// + internal class TaskHostTaskCancelled : INodePacket + { + /// + /// Constructor + /// + public TaskHostTaskCancelled() + { + } + + /// + /// The type of this NodePacket + /// + public NodePacketType Type + { + get { return NodePacketType.TaskHostTaskCancelled; } + } + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(INodePacketTranslator translator) + { + // Do nothing -- this packet doesn't contain any parameters. + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + TaskHostTaskCancelled taskCancelled = new TaskHostTaskCancelled(); + taskCancelled.Translate(translator); + return taskCancelled; + } + } +} diff --git a/src/Shared/TaskHostTaskComplete.cs b/src/Shared/TaskHostTaskComplete.cs new file mode 100644 index 00000000000..523c2d7f703 --- /dev/null +++ b/src/Shared/TaskHostTaskComplete.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A packet which contains all the information the parent node +// needs from the task host on completion of task execution. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// How the task completed -- successful, failed, or crashed + /// + internal enum TaskCompleteType + { + /// + /// Task execution succeeded + /// + Success, + + /// + /// Task execution failed + /// + Failure, + + /// + /// Task crashed during initialization steps -- loading the task, + /// validating or setting the parameters, etc. + /// + CrashedDuringInitialization, + + /// + /// Task crashed while being executed + /// + CrashedDuringExecution, + + /// + /// Task crashed after being executed + /// -- Getting outputs, etc + /// + CrashedAfterExecution + } + + /// + /// TaskHostTaskComplete contains all the information the parent node + /// needs from the task host on completion of task execution. + /// + internal class TaskHostTaskComplete : INodePacket + { + /// + /// Result of the task's execution. + /// + private TaskCompleteType _taskResult; + + /// + /// If the task threw an exception during its initialization or execution, + /// save it here. + /// + private Exception _taskException; + + /// + /// If there's an additional message that should be attached to the error + /// logged beyond "task X failed unexpectedly", save it here. May be null. + /// + private string _taskExceptionMessage; + + /// + /// If the message saved in taskExceptionMessage requires arguments, save + /// them here. May be null. + /// + private string[] _taskExceptionMessageArgs; + + /// + /// The set of parameters / values from the task after it finishes execution. + /// + private Dictionary _taskOutputParameters = null; + + /// + /// The process environment at the end of task execution. + /// + private Dictionary _buildProcessEnvironment = null; + + /// + /// Constructor + /// + /// Result of the task's execution. + /// The build process environment as it was at the end of the task's execution. + public TaskHostTaskComplete(OutOfProcTaskHostTaskResult result, IDictionary buildProcessEnvironment) + { + ErrorUtilities.VerifyThrowInternalNull(result, "result"); + + _taskResult = result.Result; + _taskException = result.TaskException; + _taskExceptionMessage = result.ExceptionMessage; + _taskExceptionMessageArgs = result.ExceptionMessageArgs; + + if (result.FinalParameterValues != null) + { + _taskOutputParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair parameter in result.FinalParameterValues) + { + _taskOutputParameters[parameter.Key] = new TaskParameter(parameter.Value); + } + } + + if (buildProcessEnvironment != null) + { + _buildProcessEnvironment = buildProcessEnvironment as Dictionary; + + if (_buildProcessEnvironment == null) + { + _buildProcessEnvironment = new Dictionary(buildProcessEnvironment); + } + } + } + + /// + /// For deserialization. + /// + private TaskHostTaskComplete() + { + } + + /// + /// Result of the task's execution. + /// + public TaskCompleteType TaskResult + { + [DebuggerStepThrough] + get + { return _taskResult; } + } + + /// + /// If the task threw an exception during its initialization or execution, + /// save it here. + /// + public Exception TaskException + { + [DebuggerStepThrough] + get + { return _taskException; } + } + + /// + /// If there's an additional message that should be attached to the error + /// logged beyond "task X failed unexpectedly", put it here. May be null. + /// + public string TaskExceptionMessage + { + [DebuggerStepThrough] + get + { return _taskExceptionMessage; } + } + + /// + /// If there are arguments that need to be formatted into the message being + /// sent, set them here. May be null. + /// + public string[] TaskExceptionMessageArgs + { + [DebuggerStepThrough] + get + { return _taskExceptionMessageArgs; } + } + + /// + /// Task parameters and their values after the task has finished. + /// + public Dictionary TaskOutputParameters + { + [DebuggerStepThrough] + get + { + if (_taskOutputParameters == null) + { + _taskOutputParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + return _taskOutputParameters; + } + } + + /// + /// The process environment. + /// + public Dictionary BuildProcessEnvironment + { + [DebuggerStepThrough] + get + { return _buildProcessEnvironment; } + } + + /// + /// The type of this packet. + /// + public NodePacketType Type + { + get { return NodePacketType.TaskHostTaskComplete; } + } + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(INodePacketTranslator translator) + { + translator.TranslateEnum(ref _taskResult, (int)_taskResult); + translator.TranslateDotNet(ref _taskException); + translator.Translate(ref _taskExceptionMessage); + translator.Translate(ref _taskExceptionMessageArgs); + translator.TranslateDictionary(ref _taskOutputParameters, StringComparer.OrdinalIgnoreCase, TaskParameter.FactoryForDeserialization); + translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + TaskHostTaskComplete taskComplete = new TaskHostTaskComplete(); + taskComplete.Translate(translator); + return taskComplete; + } + } +} diff --git a/src/Shared/TaskLoader.cs b/src/Shared/TaskLoader.cs new file mode 100644 index 00000000000..e41d128f7f5 --- /dev/null +++ b/src/Shared/TaskLoader.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class that provides helper methods to abstract the loading of tasks. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using System.Reflection; + +namespace Microsoft.Build.Shared +{ + /// + /// Class for loading tasks + /// + internal static class TaskLoader + { + /// + /// For saving the assembly that was loaded by the TypeLoader + /// We only use this when the assembly failed to load properly into the appdomain + /// + private static LoadedType s_resolverLoadedType; + + /// + /// Delegate for logging task loading errors. + /// + internal delegate void LogError(string taskLocation, int taskLine, int taskColumn, string message, params object[] messageArgs); + + /// + /// Checks if the given type is a task factory. + /// + /// This method is used as a TypeFilter delegate. + /// true, if specified type is a task + internal static bool IsTaskClass(Type type, object unused) + { + return (type.IsClass && !type.IsAbstract && (type.GetInterface("Microsoft.Build.Framework.ITask") != null)); + } + + /// + /// Creates an ITask instance and returns it. + /// + internal static ITask CreateTask(LoadedType loadedType, string taskName, string taskLocation, int taskLine, int taskColumn, LogError logError, AppDomainSetup appDomainSetup, bool isOutOfProc, out AppDomain taskAppDomain) + { + bool separateAppDomain = loadedType.HasLoadInSeparateAppDomainAttribute(); + s_resolverLoadedType = null; + taskAppDomain = null; + ITask taskInstanceInOtherAppDomain = null; + + try + { + if (separateAppDomain) + { + if (!loadedType.Type.IsMarshalByRef) + { + logError + ( + taskLocation, + taskLine, + taskColumn, + "TaskNotMarshalByRef", + taskName + ); + + return null; + } + else + { + // Our task depend on this name to be precisely that, so if you change it make sure + // you also change the checks in the tasks run in separate AppDomains. Better yet, just don't change it. + + // Make sure we copy the appdomain configuration and send it to the appdomain we create so that if the creator of the current appdomain + // has done the binding redirection in code, that we will get those settings as well. + AppDomainSetup appDomainInfo = new AppDomainSetup(); + + // Get the current app domain setup settings + byte[] currentAppdomainBytes = appDomainSetup.GetConfigurationBytes(); + + // Apply the appdomain settings to the new appdomain before creating it + appDomainInfo.SetConfigurationBytes(currentAppdomainBytes); + + AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver; + s_resolverLoadedType = loadedType; + + taskAppDomain = AppDomain.CreateDomain(isOutOfProc ? "taskAppDomain (out-of-proc)" : "taskAppDomain (in-proc)", null, appDomainInfo); + + if (loadedType.LoadedAssembly != null) + { + taskAppDomain.Load(loadedType.LoadedAssembly.GetName()); + } + + // Hook up last minute dumping of any exceptions + taskAppDomain.UnhandledException += new UnhandledExceptionEventHandler(ExceptionHandling.UnhandledExceptionHandler); + } + } + else + { + // perf improvement for the same appdomain case - we already have the type object + // and don't want to go through reflection to recreate it from the name. + return (ITask)Activator.CreateInstance(loadedType.Type); + } + + if (loadedType.Assembly.AssemblyFile != null) + { + taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceFromAndUnwrap(loadedType.Assembly.AssemblyFile, loadedType.Type.FullName); + + // this will force evaluation of the task class type and try to load the task assembly + Type taskType = taskInstanceInOtherAppDomain.GetType(); + + // If the types don't match, we have a problem. It means that our AppDomain was able to load + // a task assembly using Load, and loaded a different one. I don't see any other choice than + // to fail here. + if (taskType != loadedType.Type) + { + logError + ( + taskLocation, + taskLine, + taskColumn, + "ConflictingTaskAssembly", + loadedType.Assembly.AssemblyFile, + loadedType.Type.Assembly.Location + ); + + taskInstanceInOtherAppDomain = null; + } + } + else + { + taskInstanceInOtherAppDomain = (ITask)taskAppDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName); + } + + return taskInstanceInOtherAppDomain; + } + finally + { + // Don't leave appdomains open + if (taskAppDomain != null && taskInstanceInOtherAppDomain == null) + { + AppDomain.Unload(taskAppDomain); + RemoveAssemblyResolver(); + } + } + } + + /// + /// This is a resolver to help created AppDomains when they are unable to load an assembly into their domain we will help + /// them succeed by providing the already loaded one in the currentdomain so that they can derive AssemblyName info from it + /// + internal static Assembly AssemblyResolver(object sender, ResolveEventArgs args) + { + if ((s_resolverLoadedType != null) && (s_resolverLoadedType.LoadedAssembly != null)) + { + // Match the name being requested by the resolver with the FullName of the assembly we have loaded + if (args.Name.Equals(s_resolverLoadedType.LoadedAssembly.FullName, StringComparison.Ordinal)) + { + return s_resolverLoadedType.LoadedAssembly; + } + } + + return null; + } + + /// + /// Check if we added a resolver and remove it + /// + internal static void RemoveAssemblyResolver() + { + if (s_resolverLoadedType != null) + { + AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolver; + s_resolverLoadedType = null; + } + } + } +} \ No newline at end of file diff --git a/src/Shared/TaskLoggingHelper.cs b/src/Shared/TaskLoggingHelper.cs new file mode 100644 index 00000000000..bb78646e085 --- /dev/null +++ b/src/Shared/TaskLoggingHelper.cs @@ -0,0 +1,1451 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Text; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Runtime.Remoting.Lifetime; +using System.Runtime.Remoting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +#if BUILD_ENGINE +namespace Microsoft.Build.BackEnd +#else +namespace Microsoft.Build.Utilities +#endif +{ + /// + /// Helper logging class - contains all the logging methods used by tasks. + /// A TaskLoggingHelper object is passed to every task by MSBuild. For tasks that derive + /// from the Task class, it is provided in the Log property. + /// This class is thread safe: tasks can log from any threads. + /// +#if BUILD_ENGINE + internal +#else + public +#endif + class TaskLoggingHelper : MarshalByRefObject + { + #region Constructors + + /// + /// public constructor + /// + /// task containing an instance of this class + public TaskLoggingHelper(ITask taskInstance) + { + ErrorUtilities.VerifyThrowArgumentNull(taskInstance, "taskInstance"); + _taskInstance = taskInstance; + _taskName = taskInstance.GetType().Name; + } + + /// + /// Public constructor which can be used by task factories to assist them in logging messages. + /// + /// task containing an instance of this class + public TaskLoggingHelper(IBuildEngine buildEngine, string taskName) + { + ErrorUtilities.VerifyThrowArgumentNull(buildEngine, "buildEngine"); + ErrorUtilities.VerifyThrowArgumentLength(taskName, "taskName"); + _taskName = taskName; + _buildEngine = buildEngine; + } + + #endregion + + #region Properties + + /// + /// A client sponsor is a class + /// which will respond to a lease renewal request and will + /// increase the lease time allowing the object to stay in memory + /// + private ClientSponsor _sponsor; + + // We have to pass an instance of ITask to BuildEngine, and since we call into the engine from this class we + // need to store the actual task instance. + private ITask _taskInstance; + + /// + /// Object to make this class thread-safe. + /// + private Object _locker = new Object(); + + /// + /// Gets the name of the parent task. + /// + /// Task name string. + protected string TaskName + { + get + { + return _taskName; + } + } + + // the name of the parent task + private string _taskName; + + /// + /// Gets the upper-case version of the parent task's name. + /// + /// Upper-case task name string. + private string TaskNameUpperCase + { + get + { + if (_taskNameUpperCase == null) + { + // NOTE: use the current thread culture, because this string will be displayed to the user + _taskNameUpperCase = TaskName.ToUpper(CultureInfo.CurrentCulture); + } + + return _taskNameUpperCase; + } + } + + // the upper-case version of the parent task's name (for logging purposes) + private string _taskNameUpperCase; + + /// + /// The build engine we are going to log against + /// + private IBuildEngine _buildEngine; + + /// + /// Shortcut property for getting our build engine - we retrieve it from the task instance + /// + protected IBuildEngine BuildEngine + { + get + { + // If the task instance does not equal null then use its build engine because + // the task instances build engine can be changed for example during tests. This changin of the engine on the same task object is not expected to happen + // during normal operation. + if (_taskInstance != null) + { + return _taskInstance.BuildEngine; + } + + return _buildEngine; + } + } + + /// + /// Used to load culture-specific resources. Derived classes should register their resources either during construction, or + /// via this property, if they have localized strings. + /// + public ResourceManager TaskResources + { + get + { + return _taskResources; + } + + set + { + _taskResources = value; + } + } + + // UI resources (including strings) used by the logging methods + private ResourceManager _taskResources; + + /// + /// Gets or sets the prefix used to compose help keywords from string resource names. + /// + /// The help keyword prefix string. + public string HelpKeywordPrefix + { + get + { + return _helpKeywordPrefix; + } + + set + { + _helpKeywordPrefix = value; + } + } + + // the prefix for composing help keywords + private string _helpKeywordPrefix; + + /// + /// Has the task logged any errors through this logging helper object? + /// + public bool HasLoggedErrors + { + get + { + return _hasLoggedErrors; + } + } + + // Has the task logged any errors through this logging helper object? + private bool _hasLoggedErrors = false; + + #endregion + + #region Utility methods + + /// + /// Extracts the message code (if any) prefixed to the given message string. Message code prefixes must match the + /// following .NET regular expression in order to be recognized: ^\s*[A-Za-z]+\d+:\s* + /// Thread safe. + /// + /// + /// If this method is given the string "MYTASK1001: This is an error message.", it will return "MYTASK1001" for the + /// message code, and "This is an error message." for the message. + /// + /// The message to parse. + /// The message with the code prefix removed (if any). + /// The message code extracted from the prefix, or null if there was no code. + /// Thrown when message is null. + public string ExtractMessageCode(string message, out string messageWithoutCodePrefix) + { + ErrorUtilities.VerifyThrowArgumentNull(message, "message"); + + string code; + messageWithoutCodePrefix = ResourceUtilities.ExtractMessageCode(false /* any code */, message, out code); + + return code; + } + + /// + /// Loads the specified resource string and optionally formats it using the given arguments. The current thread's culture + /// is used for formatting. + /// + /// Requires the owner task to have registered its resources either via the Task (or TaskMarshalByRef) base + /// class constructor, or the Task.TaskResources (or AppDomainIsolatedTask.TaskResources) property. + /// + /// Thread safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// The formatted string. + /// Thrown when resourceName is null. + /// Thrown when the string resource indicated by resourceName does not exist. + /// Thrown when the TaskResources property of the owner task is not set. + virtual public string FormatResourceString(string resourceName, params object[] args) + { + ErrorUtilities.VerifyThrowArgumentNull(resourceName, "resourceName"); + ErrorUtilities.VerifyThrowInvalidOperation(TaskResources != null, "Shared.TaskResourcesNotRegistered", TaskName); + + string resourceString = TaskResources.GetString(resourceName, CultureInfo.CurrentUICulture); + + ErrorUtilities.VerifyThrowArgument(resourceString != null, "Shared.TaskResourceNotFound", resourceName, TaskName); + + return FormatString(resourceString, args); + } + + /// + /// Formats the given string using the variable arguments passed in. The current thread's culture is used for formatting. + /// Thread safe. + /// + /// The string to format. + /// Arguments for formatting. + /// The formatted string. + /// Thrown when unformatted is null. + virtual public string FormatString(string unformatted, params object[] args) + { + ErrorUtilities.VerifyThrowArgumentNull(unformatted, "unformatted"); + + return ResourceUtilities.FormatString(unformatted, args); + } + + /// + /// Get the message from resource in task library. + /// Thread safe. + /// + /// The resource name. + /// The message from resource. + virtual public string GetResourceMessage(string resourceName) + { + string resourceString = FormatResourceString(resourceName, null); + return resourceString; + } + #endregion + + #region Message logging methods + + /// + /// Logs a message using the specified string. + /// Thread safe. + /// + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogMessage(string message, params object[] messageArgs) + { + // This API is poorly designed, because parameters misordered like LogMessage(message, MessageImportance.High) + // will use this overload, ignore the importance and accidentally format the string. + // Can't change it now as it's shipped, but this debug assert will help catch callers doing this. + // Debug only because it is in theory legitimate to pass importance as a string format parameter. + Debug.Assert(messageArgs == null || messageArgs.Length == 0 || messageArgs[0].GetType() != typeof(MessageImportance), "Did you call the wrong overload?"); + + LogMessage(MessageImportance.Normal, message, messageArgs); + } + + /// + /// Logs a message of the given importance using the specified string. + /// Thread safe. + /// + /// + /// Take care to order the parameters correctly or the other overload will be called inadvertently. + /// + /// The importance level of the message. + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogMessage(MessageImportance importance, string message, params object[] messageArgs) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ErrorUtilities.VerifyThrowArgumentNull(message, "message"); + + BuildMessageEventArgs e = new BuildMessageEventArgs + ( + message, // message + null, // help keyword + TaskName, // sender + importance, // importance + DateTime.UtcNow, // timestamp + messageArgs // message arguments + ); + + // If BuildEngine is null, task attempted to log before it was set on it, + // presumably in its constructor. This is not allowed, and all + // we can do is throw. + if (BuildEngine == null) + { + // Do not use Verify[...] as it would read e.Message ahead of time + ErrorUtilities.ThrowInvalidOperation("LoggingBeforeTaskInitialization", e.Message); + } + + BuildEngine.LogMessageEvent(e); +#if _DEBUG + // Assert that the message does not contain an error code. Only errors and warnings + // should have error codes. + string errorCode; + ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, message, out errorCode); + Debug.Assert(errorCode == null, errorCode, "This message contains an error code (" + errorCode + "), yet it was logged as a regular message: " + message); +#endif + } + + /// + /// Logs a message using the specified string and other message details. + /// Thread safe. + /// + /// Description of the warning type (can be null). + /// Message code (can be null) + /// The help keyword for the host IDE (can be null). + /// The path to the file causing the message (can be null). + /// The line in the file causing the message (set to zero if not available). + /// The column in the file causing the message (set to zero if not available). + /// The last line of a range of lines in the file causing the message (set to zero if not available). + /// The last column of a range of columns in the file causing the message (set to zero if not available). + /// Importance of the message. + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogMessage + ( + string subcategory, + string code, + string helpKeyword, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + MessageImportance importance, + string message, + params object[] messageArgs + ) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ErrorUtilities.VerifyThrowArgumentNull(message, "message"); + + // If BuildEngine is null, task attempted to log before it was set on it, + // presumably in its constructor. This is not allowed, and all + // we can do is throw. + ErrorUtilities.VerifyThrowInvalidOperation(BuildEngine != null, "LoggingBeforeTaskInitialization", message); + + // If the task has missed out all location information, add the location of the task invocation; + // that gives the user something. + bool fillInLocation = (String.IsNullOrEmpty(file) && (lineNumber == 0) && (columnNumber == 0)); + + BuildMessageEventArgs e = new BuildMessageEventArgs + ( + subcategory, + code, + fillInLocation ? BuildEngine.ProjectFileOfTaskNode : file, + fillInLocation ? BuildEngine.LineNumberOfTaskNode : lineNumber, + fillInLocation ? BuildEngine.ColumnNumberOfTaskNode : columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + TaskName, + importance, + DateTime.UtcNow, + messageArgs + ); + + BuildEngine.LogMessageEvent(e); + } + + /// + /// Logs a critical message using the specified string and other message details. + /// Thread safe. + /// + /// Description of the warning type (can be null). + /// Message code (can be null). + /// The help keyword for the host IDE (can be null). + /// The path to the file causing the message (can be null). + /// The line in the file causing the message (set to zero if not available). + /// The column in the file causing the message (set to zero if not available). + /// The last line of a range of lines in the file causing the message (set to zero if not available). + /// The last column of a range of columns in the file causing the message (set to zero if not available). + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogCriticalMessage + ( + string subcategory, + string code, + string helpKeyword, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + params object[] messageArgs + ) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ErrorUtilities.VerifyThrowArgumentNull(message, "message"); + + // If BuildEngine is null, task attempted to log before it was set on it, + // presumably in its constructor. This is not allowed, and all + // we can do is throw. + ErrorUtilities.VerifyThrowInvalidOperation(BuildEngine != null, "LoggingBeforeTaskInitialization", message); + + // If the task has missed out all location information, add the location of the task invocation; + // that gives the user something. + bool fillInLocation = (String.IsNullOrEmpty(file) && (lineNumber == 0) && (columnNumber == 0)); + + CriticalBuildMessageEventArgs e = new CriticalBuildMessageEventArgs + ( + subcategory, + code, + fillInLocation ? BuildEngine.ProjectFileOfTaskNode : file, + fillInLocation ? BuildEngine.LineNumberOfTaskNode : lineNumber, + fillInLocation ? BuildEngine.ColumnNumberOfTaskNode : columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + TaskName, + DateTime.UtcNow, + messageArgs + ); + + BuildEngine.LogMessageEvent(e); + } + + /// + /// Logs a message using the specified resource string. + /// Thread safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogMessageFromResources(string messageResourceName, params object[] messageArgs) + { + // No lock needed, as the logging methods are thread safe and the rest does not modify + // global state. + // + // This API is poorly designed, because parameters misordered like LogMessageFromResources(messageResourceName, MessageImportance.High) + // will use this overload, ignore the importance and accidentally format the string. + // Can't change it now as it's shipped, but this debug assert will help catch callers doing this. + // Debug only because it is in theory legitimate to pass importance as a string format parameter. + Debug.Assert(messageArgs == null || messageArgs.Length == 0 || messageArgs[0].GetType() != typeof(MessageImportance), "Did you call the wrong overload?"); + + LogMessageFromResources(MessageImportance.Normal, messageResourceName, messageArgs); + } + + /// + /// Logs a message of the given importance using the specified resource string. + /// Thread safe. + /// + /// + /// Take care to order the parameters correctly or the other overload will be called inadvertently. + /// + /// The importance level of the message. + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogMessageFromResources(MessageImportance importance, string messageResourceName, params object[] messageArgs) + { + // No lock needed, as the logging methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(messageResourceName, "messageResourceName"); + + LogMessage(importance, FormatResourceString(messageResourceName, messageArgs)); +#if _DEBUG + // Assert that the message does not contain an error code. Only errors and warnings + // should have error codes. + string errorCode; + ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, FormatResourceString(messageResourceName, messageArgs), out errorCode); + Debug.Assert(errorCode == null, errorCode, FormatResourceString(messageResourceName, messageArgs)); +#endif + } + + #endregion + + #region ExternalProjectStarted/Finished logging methods + + /// + /// Small helper for logging the custom ExternalProjectStarted build event + /// Thread safe. + /// + /// text message + /// help keyword + /// project name + /// targets we are going to build (empty indicates default targets) + public void LogExternalProjectStarted + ( + string message, + string helpKeyword, + string projectFile, + string targetNames + ) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ExternalProjectStartedEventArgs eps = new ExternalProjectStartedEventArgs(message, helpKeyword, TaskName, projectFile, targetNames); + BuildEngine.LogCustomEvent(eps); + } + + /// + /// Small helper for logging the custom ExternalProjectFinished build event. + /// Thread safe. + /// + /// text message + /// help keyword + /// project name + /// true indicates project built successfully + public void LogExternalProjectFinished + ( + string message, + string helpKeyword, + string projectFile, + bool succeeded + ) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ExternalProjectFinishedEventArgs epf = new ExternalProjectFinishedEventArgs(message, helpKeyword, TaskName, projectFile, succeeded); + BuildEngine.LogCustomEvent(epf); + } + + #endregion + + #region Command line logging methods + + /// + /// Logs the command line for a task's underlying tool/executable/shell command. + /// Thread safe. + /// + /// The command line string. + public void LogCommandLine(string commandLine) + { + LogCommandLine(MessageImportance.Low, commandLine); + } + + /// + /// Logs the command line for a task's underlying tool/executable/shell + /// command, using the given importance level. + /// Thread safe. + /// + /// The importance level of the command line. + /// The command line string. + public void LogCommandLine(MessageImportance importance, string commandLine) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ErrorUtilities.VerifyThrowArgumentNull(commandLine, "commandLine"); + + TaskCommandLineEventArgs e = new TaskCommandLineEventArgs(commandLine, TaskName, importance); + + // If BuildEngine is null, the task attempted to log before it was set on it, + // presumably in its constructor. This is not allowed, and all we can do is throw. + if (BuildEngine == null) + { + // Do not use Verify[...] as it would read e.Message ahead of time + ErrorUtilities.ThrowInvalidOperation("LoggingBeforeTaskInitialization", e.Message); + } + + + BuildEngine.LogMessageEvent(e); + } + + #endregion + + #region Error logging methods + + /// + /// Logs an error using the specified string. + /// Thread safe. + /// + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogError(string message, params object[] messageArgs) + { + LogError(null, null, null, null, 0, 0, 0, 0, message, messageArgs); + } + + /// + /// Logs an error using the specified string and other error details. + /// Thread safe. + /// + /// Description of the error type (can be null). + /// The error code (can be null). + /// The help keyword for the host IDE (can be null). + /// The path to the file containing the error (can be null). + /// The line in the file where the error occurs (set to zero if not available). + /// The column in the file where the error occurs (set to zero if not available). + /// The last line of a range of lines in the file where the error occurs (set to zero if not available). + /// The last column of a range of columns in the file where the error occurs (set to zero if not available). + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogError + ( + string subcategory, + string errorCode, + string helpKeyword, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + params object[] messageArgs + ) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ErrorUtilities.VerifyThrowArgumentNull(message, "message"); + + // If BuildEngine is null, task attempted to log before it was set on it, + // presumably in its constructor. This is not allowed, and all + // we can do is throw. + ErrorUtilities.VerifyThrowInvalidOperation(BuildEngine != null, "LoggingBeforeTaskInitialization", message); + +#if false + // All of our errors should have an error code, so the user has something + // to look up in the documentation. To help find errors without error codes, + // temporarily uncomment this line and run the unit tests. + //if (null == errorCode) File.AppendAllText("c:\\errorsWithoutCodes", message + "\n"); + // We don't have a Debug.Assert for this, because it would be triggered by and tags. +#endif + + // If the task has missed out all location information, add the location of the task invocation; + // that gives the user something. + bool fillInLocation = (String.IsNullOrEmpty(file) && (lineNumber == 0) && (columnNumber == 0)); + + BuildErrorEventArgs e = new BuildErrorEventArgs + ( + subcategory, + errorCode, + fillInLocation ? BuildEngine.ProjectFileOfTaskNode : file, + fillInLocation ? BuildEngine.LineNumberOfTaskNode : lineNumber, + fillInLocation ? BuildEngine.ColumnNumberOfTaskNode : columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + TaskName, + DateTime.UtcNow, + messageArgs + ); + BuildEngine.LogErrorEvent(e); + + _hasLoggedErrors = true; + } + + /// + /// Logs an error using the specified resource string. + /// Thread safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogErrorFromResources(string messageResourceName, params object[] messageArgs) + { + LogErrorFromResources(null, null, null, null, 0, 0, 0, 0, messageResourceName, messageArgs); + } + + /// + /// Logs an error using the specified resource string and other error details. + /// Thread safe. + /// + /// The name of the string resource that describes the error type (can be null). + /// The error code (can be null). + /// The help keyword for the host IDE (can be null). + /// The path to the file containing the error (can be null). + /// The line in the file where the error occurs (set to zero if not available). + /// The column in the file where the error occurs (set to zero if not available). + /// The last line of a range of lines in the file where the error occurs (set to zero if not available). + /// The last column of a range of columns in the file where the error occurs (set to zero if not available). + /// The name of the string resource containing the error message. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogErrorFromResources + ( + string subcategoryResourceName, + string errorCode, + string helpKeyword, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string messageResourceName, + params object[] messageArgs + ) + { + // No lock needed, as the logging methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(messageResourceName, "messageResourceName"); + + string subcategory = null; + + if (subcategoryResourceName != null) + { + subcategory = FormatResourceString(subcategoryResourceName); + } + +#if _DEBUG + // If the message does have a message code, LogErrorWithCodeFromResources + // should have been called instead, so that the errorCode field gets populated. + // Check this only in debug, to avoid the cost of attempting to extract a + // message code when there probably isn't one. + string messageCode; + string throwAwayMessageBody = ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, FormatResourceString(messageResourceName, messageArgs), out messageCode); + Debug.Assert(messageCode == null || messageCode.Length == 0, "Called LogErrorFromResources instead of LogErrorWithCodeFromResources, but message '" + throwAwayMessageBody + "' does have an error code '" + messageCode + "'"); +#endif + + LogError + ( + subcategory, + errorCode, + helpKeyword, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + FormatResourceString(messageResourceName, messageArgs) + ); + } + + /// + /// Logs an error using the specified resource string. + /// If the message has an error code prefixed to it, the code is extracted and logged with the message. If a help keyword + /// prefix has been provided, a help keyword for the host IDE is also logged with the message. The help keyword is + /// composed by appending the string resource name to the prefix. + /// + /// A task can provide a help keyword prefix either via the Task (or TaskMarshalByRef) base class constructor, or the + /// Task.HelpKeywordPrefix (or AppDomainIsolatedTask.HelpKeywordPrefix) property. + /// + /// Thread safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogErrorWithCodeFromResources(string messageResourceName, params object[] messageArgs) + { + LogErrorWithCodeFromResources(null, null, 0, 0, 0, 0, messageResourceName, messageArgs); + } + + /// + /// Logs an error using the specified resource string and other error details. + /// If the message has an error code prefixed, the code is extracted and logged with the message. If a + /// help keyword prefix has been provided, a help keyword for the host IDE is also logged with the message. The help + /// keyword is composed by appending the error message resource string name to the prefix. + /// + /// A task can provide a help keyword prefix either via the Task (or TaskMarshalByRef) base class constructor, or the + /// Task.HelpKeywordPrefix (or AppDomainIsolatedTask.HelpKeywordPrefix) property. + /// + /// Thread safe. + /// + /// The name of the string resource that describes the error type (can be null). + /// The path to the file containing the error (can be null). + /// The line in the file where the error occurs (set to zero if not available). + /// The column in the file where the error occurs (set to zero if not available). + /// The last line of a range of lines in the file where the error occurs (set to zero if not available). + /// The last column of a range of columns in the file where the error occurs (set to zero if not available). + /// The name of the string resource containing the error message. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogErrorWithCodeFromResources + ( + string subcategoryResourceName, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string messageResourceName, + params object[] messageArgs + ) + { + // No lock needed, as the logging methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(messageResourceName, "messageResourceName"); + + string subcategory = null; + + if (subcategoryResourceName != null) + { + subcategory = FormatResourceString(subcategoryResourceName); + } + + string errorCode; + string message = ResourceUtilities.ExtractMessageCode(false /* all codes */, FormatResourceString(messageResourceName, messageArgs), out errorCode); + + string helpKeyword = null; + + if (HelpKeywordPrefix != null) + { + helpKeyword = HelpKeywordPrefix + messageResourceName; + } + + LogError + ( + subcategory, + errorCode, + helpKeyword, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + message + ); + } + + /// + /// Logs an error using the message from the given exception context. + /// No callstack will be shown. + /// Thread safe. + /// + /// Exception to log. + /// Thrown when e is null. + public void LogErrorFromException(Exception exception) + { + LogErrorFromException(exception, false); + } + + /// + /// Logs an error using the message (and optionally the stack-trace) from the given exception context. + /// Thread safe. + /// + /// Exception to log. + /// If true, callstack will be appended to message. + /// Thrown when exception is null. + public void LogErrorFromException(Exception exception, bool showStackTrace) + { + LogErrorFromException(exception, showStackTrace, false, null); + } + + /// + /// Logs an error using the message, and optionally the stack-trace from the given exception, and + /// optionally inner exceptions too. + /// Thread safe. + /// + /// Exception to log. + /// If true, callstack will be appended to message. + /// Whether to log exception types and any inner exceptions. + /// File related to the exception, or null if the project file should be logged + /// Thrown when exception is null. + public void LogErrorFromException(Exception exception, bool showStackTrace, bool showDetail, string file) + { + // No lock needed, as the logging methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(exception, "exception"); + + string message = null; + + if (!showDetail && (Environment.GetEnvironmentVariable("MSBUILDDIAGNOSTICS") == null)) // This env var is also used in ToolTask + { + message = exception.Message; + + if (showStackTrace) + { + message += Environment.NewLine + exception.StackTrace; + } + } + else + { + // The more comprehensive output, showing exception types + // and inner exceptions + StringBuilder builder = new StringBuilder(200); + do + { + builder.Append(exception.GetType().Name); + builder.Append(": "); + builder.AppendLine(exception.Message); + if (showStackTrace) + { + builder.AppendLine(exception.StackTrace); + } + exception = exception.InnerException; + } while (exception != null); + + message = builder.ToString(); + } + + LogError(null, null, null, file, 0, 0, 0, 0, message); + } + + #endregion + + #region Warning logging methods + + /// + /// Logs a warning using the specified string. + /// Thread safe. + /// + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogWarning(string message, params object[] messageArgs) + { + LogWarning(null, null, null, null, 0, 0, 0, 0, message, messageArgs); + } + + /// + /// Logs a warning using the specified string and other warning details. + /// Thread safe. + /// + /// Description of the warning type (can be null). + /// The warning code (can be null). + /// The help keyword for the host IDE (can be null). + /// The path to the file causing the warning (can be null). + /// The line in the file causing the warning (set to zero if not available). + /// The column in the file causing the warning (set to zero if not available). + /// The last line of a range of lines in the file causing the warning (set to zero if not available). + /// The last column of a range of columns in the file causing the warning (set to zero if not available). + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + public void LogWarning + ( + string subcategory, + string warningCode, + string helpKeyword, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + params object[] messageArgs + ) + { + // No lock needed, as BuildEngine methods from v4.5 onwards are thread safe. + ErrorUtilities.VerifyThrowArgumentNull(message, "message"); + + // If BuildEngine is null, task attempted to log before it was set on it, + // presumably in its constructor. This is not allowed, and all + // we can do is throw. + ErrorUtilities.VerifyThrowInvalidOperation(BuildEngine != null, "LoggingBeforeTaskInitialization", message); + +#if false + // All of our warnings should have an error code, so the user has something + // to look up in the documentation. To help find warnings without error codes, + // temporarily uncomment this line and run the unit tests. + //if (null == warningCode) File.AppendAllText("c:\\warningsWithoutCodes", message + "\n"); + // We don't have a Debug.Assert for this, because it would be triggered by and tags. +#endif + + // If the task has missed out all location information, add the location of the task invocation; + // that gives the user something. + bool fillInLocation = (String.IsNullOrEmpty(file) && (lineNumber == 0) && (columnNumber == 0)); + + BuildWarningEventArgs e = new BuildWarningEventArgs + ( + subcategory, + warningCode, + fillInLocation ? BuildEngine.ProjectFileOfTaskNode : file, + fillInLocation ? BuildEngine.LineNumberOfTaskNode : lineNumber, + fillInLocation ? BuildEngine.ColumnNumberOfTaskNode : columnNumber, + endLineNumber, + endColumnNumber, + message, + helpKeyword, + TaskName, + DateTime.UtcNow, + messageArgs + ); + + BuildEngine.LogWarningEvent(e); + } + + /// + /// Logs a warning using the specified resource string. + /// Thread safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogWarningFromResources(string messageResourceName, params object[] messageArgs) + { + LogWarningFromResources(null, null, null, null, 0, 0, 0, 0, messageResourceName, messageArgs); + } + + /// + /// Logs a warning using the specified resource string and other warning details. + /// Thread safe. + /// + /// The name of the string resource that describes the warning type (can be null). + /// The warning code (can be null). + /// The help keyword for the host IDE (can be null). + /// The path to the file causing the warning (can be null). + /// The line in the file causing the warning (set to zero if not available). + /// The column in the file causing the warning (set to zero if not available). + /// The last line of a range of lines in the file causing the warning (set to zero if not available). + /// The last column of a range of columns in the file causing the warning (set to zero if not available). + /// The name of the string resource containing the warning message. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogWarningFromResources + ( + string subcategoryResourceName, + string warningCode, + string helpKeyword, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string messageResourceName, + params object[] messageArgs + ) + { + // No lock needed, as log methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(messageResourceName, "messageResourceName"); + + string subcategory = null; + + if (subcategoryResourceName != null) + { + subcategory = FormatResourceString(subcategoryResourceName); + } + +#if DEBUG + // If the message does have a message code, LogWarningWithCodeFromResources + // should have been called instead, so that the errorCode field gets populated. + // Check this only in debug, to avoid the cost of attempting to extract a + // message code when there probably isn't one. + string messageCode; + string throwAwayMessageBody = ResourceUtilities.ExtractMessageCode(true /* only msbuild codes */, FormatResourceString(messageResourceName, messageArgs), out messageCode); + Debug.Assert(messageCode == null || messageCode.Length == 0, "Called LogWarningFromResources instead of LogWarningWithCodeFromResources, but message '" + throwAwayMessageBody + "' does have an error code '" + messageCode + "'"); +#endif + + LogWarning + ( + subcategory, + warningCode, + helpKeyword, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + FormatResourceString(messageResourceName, messageArgs) + ); + } + + /// + /// Logs a warning using the specified resource string. + /// If the message has a warning code prefixed to it, the code is extracted and logged with the message. If a help keyword + /// prefix has been provided, a help keyword for the host IDE is also logged with the message. The help keyword is + /// composed by appending the string resource name to the prefix. + /// + /// A task can provide a help keyword prefix either via the Task (or TaskMarshalByRef) base class constructor, or the + /// Task.HelpKeywordPrefix (or AppDomainIsolatedTask.HelpKeywordPrefix) property. + /// + /// Thread safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogWarningWithCodeFromResources(string messageResourceName, params object[] messageArgs) + { + LogWarningWithCodeFromResources(null, null, 0, 0, 0, 0, messageResourceName, messageArgs); + } + + /// + /// Logs a warning using the specified resource string and other warning details. + /// If the message has a warning code, the code is extracted and logged with the message. + /// If a help keyword prefix has been provided, a help keyword for the host IDE is also logged with the message. The help + /// keyword is composed by appending the warning message resource string name to the prefix. + /// + /// A task can provide a help keyword prefix either via the Task (or TaskMarshalByRef) base class constructor, or the + /// Task.HelpKeywordPrefix (or AppDomainIsolatedTask.HelpKeywordPrefix) property. + /// + /// Thread safe. + /// + /// The name of the string resource that describes the warning type (can be null). + /// The path to the file causing the warning (can be null). + /// The line in the file causing the warning (set to zero if not available). + /// The column in the file causing the warning (set to zero if not available). + /// The last line of a range of lines in the file causing the warning (set to zero if not available). + /// The last column of a range of columns in the file causing the warning (set to zero if not available). + /// The name of the string resource containing the warning message. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + public void LogWarningWithCodeFromResources + ( + string subcategoryResourceName, + string file, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string messageResourceName, + params object[] messageArgs + ) + { + // No lock needed, as log methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(messageResourceName, "messageResourceName"); + + string subcategory = null; + + if (subcategoryResourceName != null) + { + subcategory = FormatResourceString(subcategoryResourceName); + } + + string warningCode; + string message = ResourceUtilities.ExtractMessageCode(false /* all codes */, FormatResourceString(messageResourceName, messageArgs), out warningCode); + + string helpKeyword = null; + + if (HelpKeywordPrefix != null) + { + helpKeyword = HelpKeywordPrefix + messageResourceName; + } + + LogWarning + ( + subcategory, + warningCode, + helpKeyword, + file, + lineNumber, + columnNumber, + endLineNumber, + endColumnNumber, + message + ); + } + + /// + /// Logs a warning using the message from the given exception context. + /// Thread safe. + /// + /// Exception to log. + /// Thrown when exception is null. + public void LogWarningFromException(Exception exception) + { + LogWarningFromException(exception, false); + } + + /// + /// Logs a warning using the message (and optionally the stack-trace) from the given exception context. + /// Thread safe. + /// + /// Exception to log. + /// If true, the exception callstack is appended to the message. + /// Thrown when exception is null. + public void LogWarningFromException(Exception exception, bool showStackTrace) + { + // No lock needed, as log methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(exception, "exception"); + + string message = exception.Message; + + if (showStackTrace) + { + message += Environment.NewLine + exception.StackTrace; + } + + LogWarning(message); + } + + #endregion + + #region Bulk logging methods + + /// + /// Logs errors/warnings/messages for each line of text in the given file. Errors/warnings are only logged for lines that + /// fit a particular (canonical) format -- the remaining lines are treated as messages. + /// Thread safe. + /// + /// The file to log from. + /// true, if any errors were logged + /// Thrown when filename is null. + public bool LogMessagesFromFile(string fileName) + { + return LogMessagesFromFile(fileName, MessageImportance.Low); + } + + /// + /// Logs errors/warnings/messages for each line of text in the given file. Errors/warnings are only logged for lines that + /// fit a particular (canonical) format -- the remaining lines are treated as messages. + /// Thread safe. + /// + /// The file to log from. + /// The importance level for messages that are neither errors nor warnings. + /// true, if any errors were logged + /// Thrown when filename is null. + public bool LogMessagesFromFile(string fileName, MessageImportance messageImportance) + { + // No lock needed, as log methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(fileName, "fileName"); + + bool errorsFound = false; + + // Command-line tools are generally going to emit their output using the current + // codepage, so that it displays correctly in the console window. + using (StreamReader fileStream = new StreamReader(fileName, System.Text.Encoding.Default)) // HIGHCHAR: Use ANSI for logging messages. + { + errorsFound = LogMessagesFromStream(fileStream, messageImportance); + } + + return errorsFound; + } + + /// + /// Logs errors/warnings/messages for each line of text in the given stream. Errors/warnings are only logged for lines + /// that fit a particular (canonical) format -- the remaining lines are treated as messages. + /// Thread safe. + /// + /// The stream to log from. + /// The importance level for messages that are neither errors nor warnings. + /// true, if any errors were logged + /// Thrown when stream is null. + public bool LogMessagesFromStream(TextReader stream, MessageImportance messageImportance) + { + // No lock needed, as log methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(stream, "stream"); + + bool errorsFound = false; + string lineOfText; + + // ReadLine() blocks until either A.) there is a complete line of text to be read from + // the stream, or B.) the stream is closed/done/finit/gone/byebye. + while ((lineOfText = stream.ReadLine()) != null) + { + errorsFound |= LogMessageFromText(lineOfText, messageImportance); + } + + return errorsFound; + } + + /// + /// Logs an error/warning/message from the given line of text. Errors/warnings are only logged for lines that fit a + /// particular (canonical) format -- all other lines are treated as messages. + /// Thread safe. + /// + /// The line of text to log from. + /// The importance level for messages that are neither errors nor warnings. + /// true, if an error was logged + /// Thrown when lineOfText is null. + public bool LogMessageFromText(string lineOfText, MessageImportance messageImportance) + { + // No lock needed, as log methods are thread safe and the rest does not modify + // global state. + ErrorUtilities.VerifyThrowArgumentNull(lineOfText, "lineOfText"); + + bool isError = false; + CanonicalError.Parts messageParts = CanonicalError.Parse(lineOfText); + + if (null == messageParts) + { + // Line was not recognized as a canonical error. Log it as a message. + LogMessage(messageImportance, lineOfText); + } + else + { + // The message was in Canonical format. + // Log it as a warning or error. + string origin = messageParts.origin; + + if ((origin == null) || (origin.Length == 0)) + { + // Use the task class name as the origin, if none specified. + origin = TaskNameUpperCase; + } + + switch (messageParts.category) + { + case CanonicalError.Parts.Category.Error: + { + LogError + ( + messageParts.subcategory, + messageParts.code, + null, + origin, + messageParts.line, + messageParts.column, + messageParts.endLine, + messageParts.endColumn, + messageParts.text + ); + + isError = true; + break; + } + + case CanonicalError.Parts.Category.Warning: + { + LogWarning + ( + messageParts.subcategory, + messageParts.code, + null, + origin, + messageParts.line, + messageParts.column, + messageParts.endLine, + messageParts.endColumn, + messageParts.text + ); + + break; + } + + default: + ErrorUtilities.VerifyThrow(false, "Impossible canonical part."); + break; + } + } + + return isError; + } + + #endregion + + #region AppDomain Code + + /// + /// InitializeLifetimeService is called when the remote object is activated. + /// This method will determine how long the lifetime for the object will be. + /// Thread safe. However, InitializeLifetimeService and MarkAsInactive should + /// only be called in that order, together or not at all, and no more than once. + /// + /// The lease object to control this object's lifetime. + public override object InitializeLifetimeService() + { + lock (_locker) + { + // Each MarshalByRef object has a reference to the service which + // controls how long the remote object will stay around + ILease lease = (ILease)base.InitializeLifetimeService(); + + // Set how long a lease should be initially. Once a lease expires + // the remote object will be disconnected and it will be marked as being availiable + // for garbage collection + int initialLeaseTime = 1; + + string initialLeaseTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDTASKLOGGINGHELPERINITIALLEASETIME"); + + if (!String.IsNullOrEmpty(initialLeaseTimeFromEnvironment)) + { + int leaseTimeFromEnvironment; + if (int.TryParse(initialLeaseTimeFromEnvironment, out leaseTimeFromEnvironment) && leaseTimeFromEnvironment > 0) + { + initialLeaseTime = leaseTimeFromEnvironment; + } + } + + lease.InitialLeaseTime = TimeSpan.FromMinutes(initialLeaseTime); + + // Make a new client sponsor. A client sponsor is a class + // which will respond to a lease renewal request and will + // increase the lease time allowing the object to stay in memory + _sponsor = new ClientSponsor(); + + // When a new lease is requested lets make it last 1 minutes longer. + int leaseExtensionTime = 1; + + string leaseExtensionTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDTASKLOGGINGHELPERLEASEEXTENSIONTIME"); + if (!String.IsNullOrEmpty(leaseExtensionTimeFromEnvironment)) + { + int leaseExtensionFromEnvironment; + if (int.TryParse(leaseExtensionTimeFromEnvironment, out leaseExtensionFromEnvironment) && leaseExtensionFromEnvironment > 0) + { + leaseExtensionTime = leaseExtensionFromEnvironment; + } + } + + _sponsor.RenewalTime = TimeSpan.FromMinutes(leaseExtensionTime); + + // Register the sponsor which will increase lease timeouts when the lease expires + lease.Register(_sponsor); + + return lease; + } + } + + /// + /// Notifies this object that its work is done. + /// Thread safe. However, InitializeLifetimeService and MarkAsInactive should + /// only be called in that order, together or not at all, and no more than once. + /// + /// + /// Indicates to the TaskLoggingHelper that it is no longer needed. + /// + public void MarkAsInactive() + { + lock (_locker) + { + // Clear out the sponsor (who is responsible for keeping the TaskLoggingHelper remoting lease alive until the task is done) + // this will be null if the engineproxy was never sent across an appdomain boundry. + if (_sponsor != null) + { + ILease lease = (ILease)RemotingServices.GetLifetimeService(this); + + if (lease != null) + { + lease.Unregister(_sponsor); + } + + _sponsor.Close(); + _sponsor = null; + } + } + } + + #endregion + } +} diff --git a/src/Shared/TaskLoggingHelperExtension.cs b/src/Shared/TaskLoggingHelperExtension.cs new file mode 100644 index 00000000000..b39a83e1c9f --- /dev/null +++ b/src/Shared/TaskLoggingHelperExtension.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Resources; + +/* Unmerged change from project 'Microsoft.Build.Tasks' +Before: +using System.Resources; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +//This is in the Tasks namespace because that's where it was before and it is public. +#if BUILD_ENGINE +namespace Microsoft.Build.BackEnd +#else +using Microsoft.Build.Utilities; +After: +using Microsoft.Build.Shared; + +//This is in the Tasks namespace because that's where it was before and it is public. +#if BUILD_ENGINE +namespace Microsoft.Build.BackEnd +#else +using Microsoft.Build.Utilities; +*/ + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +//This is in the Tasks namespace because that's where it was before and it is public. + +#if BUILD_ENGINE +namespace Microsoft.Build.BackEnd +#else +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +#endif +{ + /// + /// Helper logging class for tasks, used for dealing with two resource streams. + /// +#if WHIDBEY_VISIBILITY || BUILD_ENGINE + internal +#else + public +#endif + class TaskLoggingHelperExtension : TaskLoggingHelper + { + #region Constructors + + /// + /// public constructor + /// + public TaskLoggingHelperExtension(ITask taskInstance, ResourceManager primaryResources, ResourceManager sharedResources, string helpKeywordPrefix) : + base(taskInstance) + { + this.TaskResources = primaryResources; + this.TaskSharedResources = sharedResources; + this.HelpKeywordPrefix = helpKeywordPrefix; + } + + /// + /// private default constructor - we should be using the constructor accepting an ITask argument + /// to create instances of this class + /// + private TaskLoggingHelperExtension() : + base(null) + { + } + + #endregion + + #region Properties + + /// + /// Used to load culture-specific resources. Derived classes should register their resources either during construction, or + /// via this property, if they have localized strings. + /// + public ResourceManager TaskSharedResources + { + get + { + return _taskSharedResources; + } + + set + { + _taskSharedResources = value; + } + } + + // UI shared resources (including strings) used by the logging methods + private ResourceManager _taskSharedResources; + + #endregion + + #region Utility methods + + /// + /// Loads the specified resource string and optionally formats it using the given arguments. The current thread's culture + /// is used for formatting. + /// + /// + /// 1) This method requires the owner task to have registered its resources either via the Task (or TaskMarshalByRef) base + /// class constructor, or the "Task.TaskResources" (or "AppDomainIsolatedTask.TaskResources") property. + /// 2) This method is thread-safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// The formatted string. + /// Thrown when resourceName is null. + /// Thrown when the string resource indicated by resourceName does not exist. + /// Thrown when the TaskResources property of the owner task is not set. + override public string FormatResourceString(string resourceName, params object[] args) + { + ErrorUtilities.VerifyThrowArgumentNull(resourceName, "resourceName"); + ErrorUtilities.VerifyThrowInvalidOperation(TaskResources != null, "Shared.TaskResourcesNotRegistered", TaskName); + ErrorUtilities.VerifyThrowInvalidOperation(TaskSharedResources != null, "Shared.TaskResourcesNotRegistered", TaskName); + + // NOTE: the ResourceManager.GetString() method is thread-safe + string resourceString = TaskResources.GetString(resourceName, CultureInfo.CurrentUICulture); + + if (resourceString == null) + { + resourceString = TaskSharedResources.GetString(resourceName, CultureInfo.CurrentUICulture); + } + + ErrorUtilities.VerifyThrowArgument(resourceString != null, "Shared.TaskResourceNotFound", resourceName, TaskName); + + return FormatString(resourceString, args); + } + + #endregion + } +} diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs new file mode 100644 index 00000000000..99619353d4e --- /dev/null +++ b/src/Shared/TaskParameter.cs @@ -0,0 +1,752 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wrapper class to enable serialization of all allowed task parameter types. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security; +using System.Security.Permissions; + +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Type of parameter, used to figure out how to serialize it. + /// + internal enum TaskParameterType + { + /// + /// Parameter is null + /// + Null, + + /// + /// Parameter is a string + /// + String, + + /// + /// Parameter is an array of strings + /// + StringArray, + + /// + /// Parameter is a value type. Note: Must be serializable + /// + ValueType, + + /// + /// Parameter is an array of value types. Note: Must be serializable. + /// + ValueTypeArray, + + /// + /// Parameter is an ITaskItem + /// + ITaskItem, + + /// + /// Parameter is an array of ITaskItems + /// + ITaskItemArray, + + /// + /// An invalid parameter -- the value of this parameter contains the exception + /// that is thrown when trying to access it. + /// + Invalid + } + + /// + /// Wrapper for task parameters, to allow proper serialization even + /// in cases where the parameter is not .NET serializable. + /// + internal class TaskParameter : MarshalByRefObject, INodePacketTranslatable + { + /// + /// The TaskParameterType of the wrapped parameter + /// + private TaskParameterType _parameterType; + + /// + /// The actual task parameter that we're wrapping + /// + private object _wrappedParameter; + + /// + /// Create a new TaskParameter + /// + public TaskParameter(object wrappedParameter) + { + if (wrappedParameter == null) + { + _parameterType = TaskParameterType.Null; + _wrappedParameter = null; + return; + } + + Type wrappedParameterType = wrappedParameter.GetType(); + + if ((wrappedParameter as Exception) != null) + { + _parameterType = TaskParameterType.Invalid; + _wrappedParameter = wrappedParameter; + return; + } + + // It's not null or invalid, so it should be a valid parameter type. + ErrorUtilities.VerifyThrow + ( + TaskParameterTypeVerifier.IsValidInputParameter(wrappedParameterType) || TaskParameterTypeVerifier.IsValidOutputParameter(wrappedParameterType), + "How did we manage to get a task parameter that isn't a valid parameter type?" + ); + + if (wrappedParameterType.IsArray) + { + if (wrappedParameterType == typeof(string[])) + { + _parameterType = TaskParameterType.StringArray; + _wrappedParameter = wrappedParameter; + } + else if (typeof(ITaskItem[]).IsAssignableFrom(wrappedParameterType)) + { + _parameterType = TaskParameterType.ITaskItemArray; + ITaskItem[] inputAsITaskItemArray = (ITaskItem[])wrappedParameter; + ITaskItem[] taskItemArrayParameter = new ITaskItem[inputAsITaskItemArray.Length]; + + for (int i = 0; i < inputAsITaskItemArray.Length; i++) + { + if (inputAsITaskItemArray[i] != null) + { + taskItemArrayParameter[i] = CreateNewTaskItemFrom(inputAsITaskItemArray[i]); + } + } + + _wrappedParameter = taskItemArrayParameter; + } + else if (wrappedParameterType.GetElementType().IsValueType) + { + _parameterType = TaskParameterType.ValueTypeArray; + _wrappedParameter = wrappedParameter; + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + else + { + // scalar parameter + if (wrappedParameterType == typeof(string)) + { + _parameterType = TaskParameterType.String; + _wrappedParameter = wrappedParameter; + } + else if (typeof(ITaskItem).IsAssignableFrom(wrappedParameterType)) + { + _parameterType = TaskParameterType.ITaskItem; + _wrappedParameter = CreateNewTaskItemFrom((ITaskItem)wrappedParameter); + } + else if (wrappedParameterType.IsValueType) + { + _parameterType = TaskParameterType.ValueType; + _wrappedParameter = wrappedParameter; + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + } + + /// + /// Constructor for deserialization + /// + private TaskParameter() + { + } + + /// + /// The TaskParameterType of the wrapped parameter + /// + public TaskParameterType ParameterType + { + [DebuggerStepThrough] + get + { return _parameterType; } + } + + /// + /// The actual task parameter that we're wrapping + /// + public object WrappedParameter + { + [DebuggerStepThrough] + get + { return _wrappedParameter; } + } + + /// + /// TaskParameter's ToString should just pass through to whatever it's wrapping. + /// + public override string ToString() + { + return (WrappedParameter == null) ? String.Empty : WrappedParameter.ToString(); + } + + /// + /// Serialize / deserialize this item. + /// + public void Translate(INodePacketTranslator translator) + { + translator.TranslateEnum(ref _parameterType, (int)_parameterType); + + switch (_parameterType) + { + case TaskParameterType.Null: + _wrappedParameter = null; + break; + case TaskParameterType.String: + string stringParam = (string)_wrappedParameter; + translator.Translate(ref stringParam); + _wrappedParameter = stringParam; + break; + case TaskParameterType.StringArray: + string[] stringArrayParam = (string[])_wrappedParameter; + translator.Translate(ref stringArrayParam); + _wrappedParameter = stringArrayParam; + break; + case TaskParameterType.ValueType: + case TaskParameterType.ValueTypeArray: + translator.TranslateDotNet(ref _wrappedParameter); + break; + case TaskParameterType.ITaskItem: + TranslateITaskItem(translator); + break; + case TaskParameterType.ITaskItemArray: + TranslateITaskItemArray(translator); + break; + case TaskParameterType.Invalid: + Exception exceptionParam = (Exception)_wrappedParameter; + translator.TranslateDotNet(ref exceptionParam); + _wrappedParameter = exceptionParam; + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + } + + /// + /// Overridden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + [SecurityCritical] + public override object InitializeLifetimeService() + { + // null means infinite lease time + return null; + } + + /// + /// Factory for deserialization. + /// + internal static TaskParameter FactoryForDeserialization(INodePacketTranslator translator) + { + TaskParameter taskParameter = new TaskParameter(); + taskParameter.Translate(translator); + return taskParameter; + } + + /// + /// Creates a new ITaskItem with the contents of the old one. + /// + private ITaskItem CreateNewTaskItemFrom(ITaskItem copyFrom) + { + ITaskItem2 copyFromAsITaskItem2 = copyFrom as ITaskItem2; + + string escapedItemSpec = null; + string escapedDefiningProject = null; + Dictionary escapedMetadata = null; + + if (copyFromAsITaskItem2 != null) + { + escapedItemSpec = copyFromAsITaskItem2.EvaluatedIncludeEscaped; + escapedDefiningProject = copyFromAsITaskItem2.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + IDictionary nonGenericEscapedMetadata = copyFromAsITaskItem2.CloneCustomMetadataEscaped(); + + if (nonGenericEscapedMetadata is Dictionary) + { + escapedMetadata = (Dictionary)nonGenericEscapedMetadata; + } + else + { + escapedMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (object key in nonGenericEscapedMetadata.Keys) + { + escapedMetadata[(string)key] = (string)nonGenericEscapedMetadata[key] ?? String.Empty; + } + } + } + else + { + // If we don't have ITaskItem2 to fall back on, we have to make do with the fact that + // CloneCustomMetadata, GetMetadata, & ItemSpec returns unescaped values, and + // TaskParameterTaskItem's constructor expects escaped values, so escaping them all + // is the closest approximation to correct we can get. + escapedItemSpec = EscapingUtilities.Escape(copyFrom.ItemSpec); + + escapedDefiningProject = EscapingUtilities.EscapeWithCaching(copyFrom.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath)); + + IDictionary customMetadata = copyFrom.CloneCustomMetadata(); + escapedMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (customMetadata != null && customMetadata.Count > 0) + { + foreach (string key in customMetadata.Keys) + { + escapedMetadata.Add(key, EscapingUtilities.Escape((string)customMetadata[key] ?? String.Empty)); + } + } + } + + TaskParameterTaskItem taskItem = new TaskParameterTaskItem(escapedItemSpec, escapedDefiningProject, escapedMetadata); + return taskItem; + } + + /// + /// Serialize / deserialize this item. + /// + private void TranslateITaskItemArray(INodePacketTranslator translator) + { + if (!TranslateNullable(translator, _wrappedParameter)) + { + return; + } + + if (translator.Mode == TranslationDirection.WriteToStream) + { + ITaskItem[] wrappedItems = (ITaskItem[])_wrappedParameter; + + int length = wrappedItems.Length; + translator.Translate(ref length); + + foreach (ITaskItem wrappedItem in wrappedItems) + { + WriteITaskItem(translator, wrappedItem); + } + } + else + { + int length = 0; + translator.Translate(ref length); + ITaskItem[] wrappedItems = new ITaskItem[length]; + + for (int i = 0; i < length; i++) + { + ReadITaskItem(translator, ref wrappedItems[i]); + } + + _wrappedParameter = wrappedItems; + } + } + + /// + /// Serialize / deserialize this item. + /// + private void TranslateITaskItem(INodePacketTranslator translator) + { + if (translator.Mode == TranslationDirection.WriteToStream) + { + WriteITaskItem(translator, (ITaskItem)_wrappedParameter); + } + else // TranslationDirection.ReadFromStream + { + ITaskItem wrappedItem = null; + ReadITaskItem(translator, ref wrappedItem); + _wrappedParameter = wrappedItem; + } + } + + /// + /// Write the given ITaskItem, using the given write translator + /// + private void WriteITaskItem(INodePacketTranslator translator, ITaskItem wrappedItem) + { + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "Cannot call this method when reading!"); + + if (!TranslateNullable(translator, wrappedItem)) + { + return; + } + + string escapedItemSpec; + string escapedDefiningProject; + IDictionary wrappedMetadata; + bool wrappedMetadataIsEscaped; + + ITaskItem2 wrappedItemAsITaskItem2 = wrappedItem as ITaskItem2; + + if (wrappedItemAsITaskItem2 != null) + { + escapedItemSpec = wrappedItemAsITaskItem2.EvaluatedIncludeEscaped; + escapedDefiningProject = wrappedItemAsITaskItem2.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + wrappedMetadata = wrappedItemAsITaskItem2.CloneCustomMetadataEscaped(); + wrappedMetadataIsEscaped = true; + } + else + { + // We know that the ITaskItem constructor expects an escaped string, and that ITaskItem.ItemSpec + // is expected to be unescaped, so make sure we give the constructor what it wants. + escapedItemSpec = EscapingUtilities.Escape(wrappedItem.ItemSpec); + escapedDefiningProject = EscapingUtilities.EscapeWithCaching(wrappedItem.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath)); + wrappedMetadata = wrappedItem.CloneCustomMetadata(); + wrappedMetadataIsEscaped = false; + } + + Dictionary escapedGenericWrappedMetadata = wrappedMetadata as Dictionary; + + if (escapedGenericWrappedMetadata == null) + { + escapedGenericWrappedMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (object key in wrappedMetadata.Keys) + { + string value = (string)wrappedMetadata[key]; + + if (!wrappedMetadataIsEscaped) + { + value = (value == null) ? value : EscapingUtilities.Escape(value); + } + + escapedGenericWrappedMetadata.Add((string)key, value); + } + } + else if (!wrappedMetadataIsEscaped) + { + foreach (KeyValuePair entry in escapedGenericWrappedMetadata) + { + escapedGenericWrappedMetadata[entry.Key] = entry.Value == null ? entry.Value : EscapingUtilities.Escape(entry.Value); + } + } + + translator.Translate(ref escapedItemSpec); + translator.Translate(ref escapedDefiningProject); + translator.TranslateDictionary(ref escapedGenericWrappedMetadata, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Read an ITaskItem into the given parameter, using the given read translator + /// + private void ReadITaskItem(INodePacketTranslator translator, ref ITaskItem wrappedItem) + { + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.ReadFromStream, "Cannot call this method when writing!"); + + if (!TranslateNullable(translator, wrappedItem)) + { + return; + } + + string escapedItemSpec = null; + string escapedDefiningProject = null; + Dictionary escapedMetadata = null; + + translator.Translate(ref escapedItemSpec); + translator.Translate(ref escapedDefiningProject); + translator.TranslateDictionary(ref escapedMetadata, StringComparer.OrdinalIgnoreCase); + + wrappedItem = new TaskParameterTaskItem(escapedItemSpec, escapedDefiningProject, escapedMetadata); + } + + /// + /// Writes out the boolean which says if this object is null or not. + /// + /// The nullable type to translate. + private bool TranslateNullable(INodePacketTranslator translator, T value) + { + bool haveRef = false; + + if (translator.Mode == TranslationDirection.WriteToStream) + { + haveRef = (value != null); + translator.Translate(ref haveRef); + } + else // TranslationDirection.ReadFromStream + { + translator.Translate(ref haveRef); + } + + return haveRef; + } + + /// + /// Super simple ITaskItem derivative that we can use as a container for read items. + /// + private class TaskParameterTaskItem : MarshalByRefObject, ITaskItem, ITaskItem2 + { + /// + /// The item spec + /// + private string _escapedItemSpec = null; + + /// + /// The full path to the project that originally defined this item. + /// + private string _escapedDefiningProject = null; + + /// + /// The custom metadata + /// + private Dictionary _customEscapedMetadata = null; + + /// + /// Cache for fullpath metadata + /// + private string _fullPath; + + /// + /// Constructor for serialization + /// + public TaskParameterTaskItem(string escapedItemSpec, string escapedDefiningProject, Dictionary escapedMetadata) + { + ErrorUtilities.VerifyThrowInternalNull(escapedItemSpec, "escapedItemSpec"); + + _escapedItemSpec = escapedItemSpec; + _escapedDefiningProject = escapedDefiningProject; + _customEscapedMetadata = escapedMetadata; + } + + /// + /// Gets or sets the item "specification" e.g. for disk-based items this would be the file path. + /// + /// + /// This should be named "EvaluatedInclude" but that would be a breaking change to this interface. + /// + /// The item-spec string. + public string ItemSpec + { + get + { + return (_escapedItemSpec == null) ? String.Empty : EscapingUtilities.UnescapeAll(_escapedItemSpec); + } + + set + { + _escapedItemSpec = value; + } + } + + /// + /// Gets the names of all the metadata on the item. + /// Includes the built-in metadata like "FullPath". + /// + /// The list of metadata names. + public ICollection MetadataNames + { + get + { + List metadataNames = (_customEscapedMetadata == null) ? new List() : new List(_customEscapedMetadata.Keys); + metadataNames.AddRange(FileUtilities.ItemSpecModifiers.All); + + return metadataNames; + } + } + + /// + /// Gets the number of pieces of metadata on the item. Includes + /// both custom and built-in metadata. Used only for unit testing. + /// + /// Count of pieces of metadata. + public int MetadataCount + { + get + { + int count = (_customEscapedMetadata == null) ? 0 : _customEscapedMetadata.Count; + return (count + FileUtilities.ItemSpecModifiers.All.Length); + } + } + + /// + /// Returns the escaped version of this item's ItemSpec + /// + string ITaskItem2.EvaluatedIncludeEscaped + { + get + { + return _escapedItemSpec; + } + + set + { + _escapedItemSpec = value; + } + } + + /// + /// Allows the values of metadata on the item to be queried. + /// + /// The name of the metadata to retrieve. + /// The value of the specified metadata. + public string GetMetadata(string metadataName) + { + string metadataValue = (this as ITaskItem2).GetMetadataValueEscaped(metadataName); + return EscapingUtilities.UnescapeAll(metadataValue); + } + + /// + /// Allows a piece of custom metadata to be set on the item. + /// + /// The name of the metadata to set. + /// The metadata value. + public void SetMetadata(string metadataName, string metadataValue) + { + ErrorUtilities.VerifyThrowArgumentLength(metadataName, "metadataName"); + + // Non-derivable metadata can only be set at construction time. + // That's why this is IsItemSpecModifier and not IsDerivableItemSpecModifier. + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName), "Shared.CannotChangeItemSpecModifiers", metadataName); + + _customEscapedMetadata = _customEscapedMetadata ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + _customEscapedMetadata[metadataName] = metadataValue ?? String.Empty; + } + + /// + /// Allows the removal of custom metadata set on the item. + /// + /// The name of the metadata to remove. + public void RemoveMetadata(string metadataName) + { + ErrorUtilities.VerifyThrowArgumentNull(metadataName, "metadataName"); + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName), "Shared.CannotChangeItemSpecModifiers", metadataName); + + if (_customEscapedMetadata == null) + { + return; + } + + _customEscapedMetadata.Remove(metadataName); + } + + /// + /// Allows custom metadata on the item to be copied to another item. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should NOT copy over the item-spec + /// 2) if a particular piece of metadata already exists on the destination item, it should NOT be overwritten + /// 3) if there are pieces of metadata on the item that make no semantic sense on the destination item, they should NOT be copied + /// + /// The item to copy metadata to. + public void CopyMetadataTo(ITaskItem destinationItem) + { + ErrorUtilities.VerifyThrowArgumentNull(destinationItem, "destinationItem"); + + // also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata + // between items, and need to know the source item where the metadata came from + string originalItemSpec = destinationItem.GetMetadata("OriginalItemSpec"); + + if (_customEscapedMetadata != null) + { + foreach (KeyValuePair entry in _customEscapedMetadata) + { + string value = destinationItem.GetMetadata(entry.Key); + + if (String.IsNullOrEmpty(value)) + { + destinationItem.SetMetadata(entry.Key, entry.Value); + } + } + } + + if (String.IsNullOrEmpty(originalItemSpec)) + { + destinationItem.SetMetadata("OriginalItemSpec", EscapingUtilities.Escape(ItemSpec)); + } + } + + /// + /// Get the collection of custom metadata. This does not include built-in metadata. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should return a clone of the metadata + /// 2) writing to this dictionary should not be reflected in the underlying item. + /// + /// Dictionary of cloned metadata + public IDictionary CloneCustomMetadata() + { + IDictionary clonedMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (_customEscapedMetadata != null) + { + foreach (KeyValuePair metadatum in _customEscapedMetadata) + { + clonedMetadata.Add(metadatum.Key, EscapingUtilities.UnescapeAll(metadatum.Value)); + } + } + + return (IDictionary)clonedMetadata; + } + + /// + /// Overridden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + [SecurityCritical] + public override object InitializeLifetimeService() + { + // null means infinite lease time + return null; + } + + /// + /// Returns the escaped value of the requested metadata name. + /// + string ITaskItem2.GetMetadataValueEscaped(string metadataName) + { + ErrorUtilities.VerifyThrowArgumentNull(metadataName, "metadataName"); + + string metadataValue = null; + + if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName)) + { + // FileUtilities.GetItemSpecModifier is expecting escaped data, which we assume we already are. + // Passing in a null for currentDirectory indicates we are already in the correct current directory + metadataValue = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(null, _escapedItemSpec, _escapedDefiningProject, metadataName, ref _fullPath); + } + else if (_customEscapedMetadata != null) + { + _customEscapedMetadata.TryGetValue(metadataName, out metadataValue); + } + + return (metadataValue == null) ? String.Empty : metadataValue; + } + + /// + /// Sets the exact metadata value given to the metadata name requested. + /// + void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) + { + SetMetadata(metadataName, EscapingUtilities.Escape(metadataValue)); + } + + /// + /// Returns a dictionary containing all metadata and their escaped forms. + /// + IDictionary ITaskItem2.CloneCustomMetadataEscaped() + { + IDictionary clonedDictionary = new Dictionary(_customEscapedMetadata); + return clonedDictionary; + } + } + } +} \ No newline at end of file diff --git a/src/Shared/TaskParameterTypeVerifier.cs b/src/Shared/TaskParameterTypeVerifier.cs new file mode 100644 index 00000000000..2d64146d8ab --- /dev/null +++ b/src/Shared/TaskParameterTypeVerifier.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// TaskParameterTypeVerifier verifies the correct type for both input and output parameters. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Provide a class which can verify the correct type for both input and output parameters. + /// + internal static class TaskParameterTypeVerifier + { + /// + /// Is the parameter type a valid scalar input value + /// + internal static bool IsValidScalarInputParameter(Type parameterType) + { + bool result = (parameterType.IsValueType || parameterType == typeof(string) || parameterType == typeof(ITaskItem)); + return result; + } + + /// + /// Is the passed in parameterType a valid vector input parameter + /// + internal static bool IsValidVectorInputParameter(Type parameterType) + { + bool result = (parameterType.IsArray && parameterType.GetElementType().IsValueType) || + parameterType == typeof(string[]) || + parameterType == typeof(ITaskItem[]); + return result; + } + + /// + /// Is the passed in value type assignable to an ITask or Itask[] object + /// + internal static bool IsAssignableToITask(Type parameterType) + { + bool result = typeof(ITaskItem[]).IsAssignableFrom(parameterType) || /* ITaskItem array or derived type, or */ + typeof(ITaskItem).IsAssignableFrom(parameterType); /* ITaskItem or derived type */ + return result; + } + + /// + /// Is the passed parameter a valid value type output parameter + /// + internal static bool IsValueTypeOutputParameter(Type parameterType) + { + bool result = (parameterType.IsArray && parameterType.GetElementType().IsValueType) || /* array of value types, or */ + parameterType == typeof(string[]) || /* string array, or */ + parameterType.IsValueType || /* value type, or */ + parameterType == typeof(string); /* string */ + return result; + } + + /// + /// Is the parameter type a valid scalar or value type input parameter + /// + internal static bool IsValidInputParameter(Type parameterType) + { + return IsValidScalarInputParameter(parameterType) || IsValidVectorInputParameter(parameterType); + } + + /// + /// Is the parameter type a valid scalar or value type output parameter + /// + internal static bool IsValidOutputParameter(Type parameterType) + { + return IsValueTypeOutputParameter(parameterType) || IsAssignableToITask(parameterType); + } + } +} \ No newline at end of file diff --git a/src/Shared/TempFileUtilities.cs b/src/Shared/TempFileUtilities.cs new file mode 100644 index 00000000000..c1ae101d951 --- /dev/null +++ b/src/Shared/TempFileUtilities.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Methods to create temp files. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Security; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Text; +using System.Threading; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for file IO. + /// It is in a separate file so that it can be selectively included into an assembly. + /// + static internal partial class FileUtilities + { + /// + /// Generates a unique directory name in the temporary folder. + /// Caller must delete when finished. + /// + internal static string GetTemporaryDirectory() + { + string temporaryDirectory = Path.Combine(Path.GetTempPath(), "Temporary" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(temporaryDirectory); + + return temporaryDirectory; + } + + /// + /// Generates a unique temporary file name with a given extension in the temporary folder. + /// If no extension is provided, uses ".tmp". + /// File is guaranteed to be unique. + /// Caller must delete it when finished. + /// + internal static string GetTemporaryFile() + { + return GetTemporaryFile(".tmp"); + } + + /// + /// Generates a unique temporary file name with a given extension in the temporary folder. + /// File is guaranteed to be unique. + /// Extension may have an initial period. + /// Caller must delete it when finished. + /// May throw IOException. + /// + internal static string GetTemporaryFile(string extension) + { + return GetTemporaryFile(null, extension); + } + + /// + /// Creates a file with unique temporary file name with a given extension in the specified folder. + /// File is guaranteed to be unique. + /// Extension may have an initial period. + /// If folder is null, the temporary folder will be used. + /// Caller must delete it when finished. + /// May throw IOException. + /// + internal static string GetTemporaryFile(string directory, string extension) + { + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(directory, "directory"); + ErrorUtilities.VerifyThrowArgumentLength(extension, "extension"); + + if (extension[0] != '.') + { + extension = '.' + extension; + } + + string file = null; + + try + { + directory = directory ?? Path.GetTempPath(); + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + file = Path.Combine(directory, "tmp" + Guid.NewGuid().ToString("N") + extension); + + ErrorUtilities.VerifyThrow(!File.Exists(file), "Guid should be unique"); + + File.WriteAllText(file, String.Empty); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + throw new IOException(ResourceUtilities.FormatResourceString("Shared.FailedCreatingTempFile", ex.Message), ex); + } + + return file; + } + } +} diff --git a/src/Shared/ThreadPoolExtensions.cs b/src/Shared/ThreadPoolExtensions.cs new file mode 100644 index 00000000000..0a30fb4d300 --- /dev/null +++ b/src/Shared/ThreadPoolExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Make an extension to the threadpool class to allow the setting of culture on queued work item. +//----------------------------------------------------------------------- + +using System; +using System.Globalization; +using System.Threading; + +namespace Microsoft.Build.Shared +{ + /// + /// Class to wrap the saving and restoring of the culture of a threadpool thread + /// + internal static class ThreadPoolExtensions + { + /// + /// Queue a threadpool thread and set it to a certain culture. + /// + internal static bool QueueThreadPoolWorkItemWithCulture(WaitCallback callback, CultureInfo culture, CultureInfo uiCulture) + { + bool success = ThreadPool.QueueUserWorkItem( + delegate (Object state) + { + // Save the culture so at the end of the threadproc if something else reuses this thread then it will not have a culture which it was not expecting. + CultureInfo originalThreadCulture = Thread.CurrentThread.CurrentCulture; + CultureInfo originalThreadUICulture = Thread.CurrentThread.CurrentUICulture; + try + { + if (Thread.CurrentThread.CurrentCulture != culture) + { + Thread.CurrentThread.CurrentCulture = culture; + } + + if (Thread.CurrentThread.CurrentUICulture != uiCulture) + { + Thread.CurrentThread.CurrentUICulture = uiCulture; + } + + callback(state); + } + finally + { + // Set the culture back to the original one so that if something else reuses this thread then it will not have a culture which it was not expecting. + if (Thread.CurrentThread.CurrentCulture != originalThreadCulture) + { + Thread.CurrentThread.CurrentCulture = originalThreadCulture; + } + + if (Thread.CurrentThread.CurrentUICulture != originalThreadUICulture) + { + Thread.CurrentThread.CurrentUICulture = originalThreadUICulture; + } + } + }); + + return success; + } + } +} diff --git a/src/Shared/ThreadingUtilities.cs b/src/Shared/ThreadingUtilities.cs new file mode 100644 index 00000000000..c63f619bb8e --- /dev/null +++ b/src/Shared/ThreadingUtilities.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Utilities relating to threading. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Shared +{ + /// + /// Threading related utility methods. + /// + internal static class ThreadingUtilities + { + /// + /// Waits for a signal on a handle and guarantees no COM STA pumping. + /// + internal static bool WaitOneNoMessagePump(this WaitHandle handle) + { + int index = WaitAnyNoMessagePump(new WaitHandle[] { handle }, Timeout.Infinite); + + return (index != WaitHandle.WaitTimeout); + } + + /// + /// Waits for a signal on a handle and guarantees no COM STA pumping. + /// + internal static bool WaitOneNoMessagePump(this WaitHandle handle, TimeSpan timeout) + { + int index = WaitNoMessagePump(new WaitHandle[] { handle }, timeout); + + return (index != WaitHandle.WaitTimeout); + } + + /// + /// Waits for a signal on a handle and guarantees no COM STA pumping. + /// + internal static bool WaitOneNoMessagePump(this WaitHandle handle, int milliseconds) + { + int index = WaitNoMessagePump(new WaitHandle[] { handle }, new TimeSpan(0, 0, 0, 0, milliseconds)); + + return (index != WaitHandle.WaitTimeout); + } + + /// + /// Waits for a signal on a set of handles and guarantees no COM STA pumping. + /// Same semantics as WaitHandle.WaitAny: returns the index of the handle, or WaitHandle. \ No newline at end of file diff --git a/src/Shared/ToolsetElement.cs b/src/Shared/ToolsetElement.cs new file mode 100644 index 00000000000..3d7ccebcd52 --- /dev/null +++ b/src/Shared/ToolsetElement.cs @@ -0,0 +1,562 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Text; +using System.Globalization; +using System.Reflection; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Helper class for reading toolsets out of the configuration file. + /// + internal static class ToolsetConfigurationReaderHelpers + { + internal static ToolsetConfigurationSection ReadToolsetConfigurationSection(Configuration configuration) + { + ToolsetConfigurationSection configurationSection = null; + + // This will be null if the application config file does not have the following section + // definition for the msbuildToolsets section as the first child element. + // + //
+ // "; + // Note that the application config file may or may not contain an msbuildToolsets element. + // For example: + // If section definition is present and section is not present, this value is not null + // If section definition is not present and section is also not present, this value is null + // If the section definition is not present and section is present, then this value is null + if (null != configuration) + { + ConfigurationSection msbuildSection = configuration.GetSection("msbuildToolsets"); + configurationSection = msbuildSection as ToolsetConfigurationSection; + + if (configurationSection == null && msbuildSection != null) // we found msbuildToolsets but the wrong type of handler + { + if (String.IsNullOrEmpty(msbuildSection.SectionInformation.Type) || + msbuildSection.SectionInformation.Type.IndexOf("Microsoft.Build", StringComparison.OrdinalIgnoreCase) >= 0) + { + // Set the configuration type handler to the current ToolsetConfigurationSection type + msbuildSection.SectionInformation.Type = typeof(ToolsetConfigurationSection).AssemblyQualifiedName; + + try + { + // fabricate a temporary config file with the correct section handler type in it + string tempFileName = FileUtilities.GetTemporaryFile(); + + // Save the modified config + configuration.SaveAs(tempFileName + ".config"); + + // Open the configuration again, the new type for the section handler will do its stuff + // Note that the OpenExeConfiguraion call uses the config filename *without* the .config + // extension + configuration = ConfigurationManager.OpenExeConfiguration(tempFileName); + + // Get the toolset information from the section using our real handler + configurationSection = configuration.GetSection("msbuildToolsets") as ToolsetConfigurationSection; + + File.Delete(tempFileName + ".config"); + File.Delete(tempFileName); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + } + } + } + } + + return configurationSection; + } + + /// + /// Creating a ToolsetConfigurationReader, and also reading toolsets from the + /// configuration file, are a little expensive. To try to avoid this cost if it's + /// not necessary, we'll check if the file exists first. If it exists, we'll scan for + /// the string "toolsVersion" to see if it might actually have any tools versions + /// defined in it. + /// + /// True if there may be toolset definitions, otherwise false + internal static bool ConfigurationFileMayHaveToolsets() + { + bool result; + + try + { + var configFile = FileUtilities.CurrentExecutableConfigurationFilePath; + result = File.Exists(configFile) && File.ReadAllText(configFile).Contains("toolsVersion"); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + // Catching Exception, but rethrowing unless it's an IO related exception. + throw; + } + + // There was some problem reading the config file: let the configuration reader + // encounter it + result = true; + } + + return result; + } + } + + /// + /// Class representing the Toolset element + /// + /// + /// Internal for unit testing only + /// + internal sealed class ToolsetElement : ConfigurationElement + { + /// + /// ToolsVersion attribute of the element + /// + [ConfigurationProperty("toolsVersion", IsKey = true, IsRequired = true)] + public string toolsVersion + { + get + { + return (string)base["toolsVersion"]; + } + + set + { + base["toolsVersion"] = value; + } + } + + /// + /// Property element collection + /// + [ConfigurationProperty("", IsDefaultCollection = true)] + public PropertyElementCollection PropertyElements + { + get + { + return (PropertyElementCollection)base[""]; + } + } + + /// + /// Class representing collection of property elements + /// + internal sealed class PropertyElementCollection : ConfigurationElementCollection + { + #region Private Fields + + /// + /// We use this dictionary to track whether or not we've seen a given + /// property definition before, since the .NET configuration classes + /// won't perform this check without respect for case. + /// + private Dictionary _previouslySeenPropertyNames = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + + #endregion + + #region Properties + + /// + /// Collection type + /// This has to be public as cannot change access modifier when overriding + /// + public override ConfigurationElementCollectionType CollectionType + { + get + { + return ConfigurationElementCollectionType.BasicMap; + } + } + + /// + /// Throw exception if an element with a duplicate is added + /// + protected override bool ThrowOnDuplicate + { + get + { + return false; + } + } + + /// + /// name of the element + /// + protected override string ElementName + { + get + { + return "property"; + } + } + + #endregion + + #region Methods + + /// + /// Gets an element with the specified name + /// + /// name of the element + /// element + public PropertyElement GetElement(string name) + { + return (PropertyElement)this.BaseGet(name); + } + + /// + /// Gets an element at the specified position + /// + /// position + /// element + public PropertyElement GetElement(int index) + { + return (PropertyElement)this.BaseGet(index); + } + + /// + /// Creates a new element + /// + /// element + protected override ConfigurationElement CreateNewElement() + { + return new PropertyElement(); + } + + /// + /// overridden so we can track previously seen property names + /// + protected override void BaseAdd(int index, ConfigurationElement element) + { + UpdatePropertyNameMap(element); + + base.BaseAdd(index, element); + } + + /// + /// overridden so we can track previously seen property names + /// + protected override void BaseAdd(ConfigurationElement element) + { + UpdatePropertyNameMap(element); + + base.BaseAdd(element); + } + + /// + /// Gets the key for the element + /// + /// element + /// key + protected override object GetElementKey(ConfigurationElement element) + { + return ((PropertyElement)element).Name; + } + + /// + /// Stores the name of the tools version in a case-insensitive map + /// so we can detect if it is specified more than once but with + /// different case + /// + private void UpdatePropertyNameMap(ConfigurationElement element) + { + string propertyName = GetElementKey(element).ToString(); + + if (_previouslySeenPropertyNames.ContainsKey(propertyName)) + { + string message = ResourceUtilities.FormatResourceString("MultipleDefinitionsForSameProperty", propertyName); + + throw new ConfigurationErrorsException(message, element.ElementInformation.Source, element.ElementInformation.LineNumber); + } + + _previouslySeenPropertyNames.Add(propertyName, string.Empty); + } + + #endregion + } + + /// + /// This class represents property element + /// + internal sealed class PropertyElement : ConfigurationElement + { + /// + /// name attribute + /// + [ConfigurationProperty("name", IsKey = true, IsRequired = true)] + public string Name + { + get + { + return (string)base["name"]; + } + + set + { + base["name"] = value; + } + } + + /// + /// value attribute + /// + [ConfigurationProperty("value", IsRequired = true)] + public string Value + { + get + { + return (string)base["value"]; + } + + set + { + base["value"] = value; + } + } + } + } + + /// + /// Class representing the collection of toolset elements + /// + /// + /// Internal for unit testing only + /// + internal sealed class ToolsetElementCollection : ConfigurationElementCollection + { + /// + /// We use this dictionary to track whether or not we've seen a given + /// toolset definition before, since the .NET configuration classes + /// won't perform this check without respect for case. + /// + private Dictionary _previouslySeenToolsVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Type of the collection + /// This has to be public as cannot change access modifier when overriding + /// + public override ConfigurationElementCollectionType CollectionType + { + get + { + return ConfigurationElementCollectionType.BasicMap; + } + } + + /// + /// Throw exception if an element with a duplicate key is added to the collection + /// + protected override bool ThrowOnDuplicate + { + get + { + return false; + } + } + + /// + /// Name of the element + /// + protected override string ElementName + { + get + { + return "toolset"; + } + } + + /// + /// Gets an element with the specified name + /// + /// toolsVersion of the element + /// element + public ToolsetElement GetElement(string toolsVersion) + { + return (ToolsetElement)this.BaseGet(toolsVersion); + } + + /// + /// Gets an element based at the specified position + /// + /// position + /// element + public ToolsetElement GetElement(int index) + { + return (ToolsetElement)this.BaseGet(index); + } + + /// + /// Returns the key value for the given element + /// + /// element whose key is returned + /// key + protected override object GetElementKey(ConfigurationElement element) + { + return ((ToolsetElement)element).toolsVersion; + } + + /// + /// Creates a new element of the collection + /// + /// Created element + protected override ConfigurationElement CreateNewElement() + { + return new ToolsetElement(); + } + + /// + /// overridden so we can track previously seen tools versions + /// + protected override void BaseAdd(int index, ConfigurationElement element) + { + UpdateToolsVersionMap(element); + + base.BaseAdd(index, element); + } + + /// + /// overridden so we can track previously seen tools versions + /// + protected override void BaseAdd(ConfigurationElement element) + { + UpdateToolsVersionMap(element); + + base.BaseAdd(element); + } + + /// + /// Stores the name of the tools version in a case-insensitive map + /// so we can detect if it is specified more than once but with + /// different case + /// + private void UpdateToolsVersionMap(ConfigurationElement element) + { + string toolsVersion = GetElementKey(element).ToString(); + + if (_previouslySeenToolsVersions.ContainsKey(toolsVersion)) + { + string message = ResourceUtilities.FormatResourceString("MultipleDefinitionsForSameToolset", toolsVersion); + + throw new ConfigurationErrorsException(message, element.ElementInformation.Source, element.ElementInformation.LineNumber); + } + + _previouslySeenToolsVersions.Add(toolsVersion, string.Empty); + } + } + + /// + /// This class is used to programmatically read msbuildToolsets section + /// in from the configuration file. An example of application config file: + /// + /// <configuration> + /// <msbuildToolsets default="2.0"> + /// <toolset toolsVersion="2.0"> + /// <property name="MSBuildBinPath" value="D:\windows\Microsoft.NET\Framework\v2.0.x86ret\"/> + /// <property name="SomeOtherProperty" value="SomeOtherPropertyValue"/> + /// </toolset> + /// <toolset toolsVersion="3.5"> + /// <property name="MSBuildBinPath" value="D:\windows\Microsoft.NET\Framework\v3.5.x86ret\"/> + /// </toolset> + /// </msbuildToolsets> + /// </configuration> + /// + /// + /// + /// Internal for unit testing only + /// + internal sealed class ToolsetConfigurationSection : ConfigurationSection + { + /// + /// toolsVersion element collection + /// + [ConfigurationProperty("", IsDefaultCollection = true)] + public ToolsetElementCollection Toolsets + { + get + { + return (ToolsetElementCollection)base[""]; + } + } + + /// + /// default attribute on msbuildToolsets element, specifying the default ToolsVersion + /// + [ConfigurationProperty("default")] + public string Default + { + get + { + // The ConfigurationPropertyAttribute constructor accepts a named parameter "DefaultValue" + // that doesn't seem to work if null is the desired default value. So here we return null + // whenever the base class gives us an empty string. + // Note this means we can't distinguish between the attribute being present but containing + // an empty string for its value and the attribute not being present at all. + string defaultValue = (string)base["default"]; + return (String.IsNullOrEmpty(defaultValue) ? null : defaultValue); + } + + set + { + base["default"] = value; + } + } + + /// + /// MsBuildOverrideTasksPath attribute on msbuildToolsets element, specifying the path to find msbuildOverrideTasks files + /// + [ConfigurationProperty("msbuildOverrideTasksPath")] // This string is case sensitive, can't change it + public string MSBuildOverrideTasksPath + { + get + { + // The ConfigurationPropertyAttribute constructor accepts a named parameter "DefaultValue" + // that doesn't seem to work if null is the desired default value. So here we return null + // whenever the base class gives us an empty string. + // Note this means we can't distinguish between the attribute being present but containing + // an empty string for its value and the attribute not being present at all. + string defaultValue = (string)base["msbuildOverrideTasksPath"]; + return (String.IsNullOrEmpty(defaultValue) ? null : defaultValue); + } + + set + { + base["msbuildOverrideTasksPath"] = value; + } + } + + /// + /// DefaultOverrideToolsVersion attribute on msbuildToolsets element, specifying the toolsversion that should be used by + /// default to build projects with this version of MSBuild. + /// + [ConfigurationProperty("DefaultOverrideToolsVersion")] + public string DefaultOverrideToolsVersion + { + get + { + // The ConfigurationPropertyAttribute constructor accepts a named parameter "DefaultValue" + // that doesn't seem to work if null is the desired default value. So here we return null + // whenever the base class gives us an empty string. + // Note this means we can't distinguish between the attribute being present but containing + // an empty string for its value and the attribute not being present at all. + string defaultValue = (string)base["DefaultOverrideToolsVersion"]; + return (String.IsNullOrEmpty(defaultValue) ? null : defaultValue); + } + + set + { + base["DefaultOverrideToolsVersion"] = value; + } + } + } +} \ No newline at end of file diff --git a/src/Shared/Tracing.cs b/src/Shared/Tracing.cs new file mode 100644 index 00000000000..4d7484fef1e --- /dev/null +++ b/src/Shared/Tracing.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A debug only helper for tracing. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Microsoft.Build.Internal +{ + /// + /// A debug only helper class for tracing + /// + internal static class Tracing + { + // Disabling warning about unused fields -- this is effectively a + // debug-only class, so these fields cause a build break in RET +#pragma warning disable 649 + /// + /// A dictionary of named counters + /// + private static Dictionary s_counts; + + /// + /// Last time logging happened + /// + private static DateTime s_last = DateTime.MinValue; + + /// + /// How often to log + /// + private static TimeSpan s_interval; + + /// + /// A place callers can put something worth logging later + /// + private static string s_slot = String.Empty; + + /// + /// Short name of the current assembly - to distinguish statics when this type is shared into different assemblies + /// + private static string s_currentAssemblyName; +#pragma warning restore 649 + +#if DEBUG + /// + /// Setup + /// + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Clearly I can't inline this. Plus, it's debug only.")] + static Tracing() + { + s_counts = new Dictionary(); + + string val = Environment.GetEnvironmentVariable("MSBUILDTRACEINTERVAL"); + double seconds; + if (!String.IsNullOrEmpty(val) && System.Double.TryParse(val, out seconds)) + { + s_interval = TimeSpan.FromSeconds(seconds); + } + else + { + s_interval = TimeSpan.FromSeconds(1); + } + + s_currentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; + + // Trace.WriteLine(new string('/', 100)); + // Trace.WriteLine("interval: " + interval.Seconds); + } +#endif + + /// + /// Put something in the slot + /// + [Conditional("DEBUG")] + internal static void Slot(string tag, string value) + { + lock (s_counts) + { + s_slot = tag + ": " + value; + } + } + + /// + /// Put something in the slot + /// + /// The key type. + /// The value type. + [Conditional("DEBUG")] + internal static void Slot(string tag, KeyValuePair value) + { + Slot(tag, value.Key.ToString() + "=" + value.Key.ToString()); + } + + /// + /// Increment the named counter, and dump if it's time to do so + /// + [Conditional("DEBUG")] + internal static void Record(string counter) + { + lock (s_counts) + { + int existing; + s_counts.TryGetValue(counter, out existing); + int incremented = ++existing; + s_counts[counter] = incremented; + DateTime now = DateTime.Now; + + if (now > s_last + s_interval) + { + Trace.WriteLine("================================================"); + Trace.WriteLine(s_slot); + s_slot = String.Empty; + Dump(); + Trace.WriteLine(System.Environment.StackTrace); + s_last = now; + } + } + } + + /// + /// Log the provided items + /// + /// The item type. + [Conditional("DEBUG")] + internal static void List(IEnumerable items) + { + foreach (T item in items) + { + Trace.WriteLine(item.ToString()); + } + } + + /// + /// Dump all the named counters, if any + /// + [Conditional("DEBUG")] + [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Debug only")] + internal static void Dump() + { + if (s_counts.Count > 0) + { + Trace.WriteLine(s_currentAssemblyName); + foreach (KeyValuePair count in s_counts) + { + Trace.WriteLine("# " + count.Key + "=" + count.Value); + } + } + } + } +} diff --git a/src/Shared/TypeLoader.cs b/src/Shared/TypeLoader.cs new file mode 100644 index 00000000000..16ea874b64b --- /dev/null +++ b/src/Shared/TypeLoader.cs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Determines if a type is in a given assembly and loads that type. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; + +namespace Microsoft.Build.Shared +{ + /// + /// This class is used to load types from their assemblies. + /// + internal class TypeLoader + { + /// + /// Lock for initializing the dictionary + /// + private static readonly Object s_cacheOfLoadedTypesByFilterLock = new Object(); + + /// + /// Lock for initializing the dictionary + /// + private static readonly Object s_cacheOfReflectionOnlyLoadedTypesByFilterLock = new Object(); + + /// + /// Lock for initializing the dictionary + /// + private static readonly Object s_loadInfoToTypeLock = new Object(); + + /// + /// Lock for initializing the dictionary + /// + private static readonly Object s_reflectionOnlyloadInfoToTypeLock = new Object(); + + /// + /// Cache to keep track of the assemblyLoadInfos based on a given typeFilter. + /// + private static ConcurrentDictionary> s_cacheOfLoadedTypesByFilter = new ConcurrentDictionary>(); + + /// + /// Cache to keep track of the assemblyLoadInfos based on a given typeFilter for assemblies which are to be loaded for reflectionOnlyLoads. + /// + private static ConcurrentDictionary> s_cacheOfReflectionOnlyLoadedTypesByFilter = new ConcurrentDictionary>(); + + /// + /// Typefilter for this typeloader + /// + private TypeFilter _isDesiredType; + + /// + /// Constructor. + /// + internal TypeLoader(TypeFilter isDesiredType) + { + ErrorUtilities.VerifyThrow(isDesiredType != null, "need a type filter"); + + _isDesiredType = isDesiredType; + } + + /// + /// Given two type names, looks for a partial match between them. A partial match is considered valid only if it occurs on + /// the right side (tail end) of the name strings, and at the start of a class or namespace name. + /// + /// + /// 1) Matches are case-insensitive. + /// 2) .NET conventions regarding namespaces and nested classes are respected, including escaping of reserved characters. + /// + /// + /// "Csc" and "csc" ==> exact match + /// "Microsoft.Build.Tasks.Csc" and "Microsoft.Build.Tasks.Csc" ==> exact match + /// "Microsoft.Build.Tasks.Csc" and "Csc" ==> partial match + /// "Microsoft.Build.Tasks.Csc" and "Tasks.Csc" ==> partial match + /// "MyTasks.ATask+NestedTask" and "NestedTask" ==> partial match + /// "MyTasks.ATask\\+NestedTask" and "NestedTask" ==> partial match + /// "MyTasks.CscTask" and "Csc" ==> no match + /// "MyTasks.MyCsc" and "Csc" ==> no match + /// "MyTasks.ATask\.Csc" and "Csc" ==> no match + /// "MyTasks.ATask\\\.Csc" and "Csc" ==> no match + /// + /// true, if the type names match exactly or partially; false, if there is no match at all + internal static bool IsPartialTypeNameMatch(string typeName1, string typeName2) + { + bool isPartialMatch = false; + + // if the type names are the same length, a partial match is impossible + if (typeName1.Length != typeName2.Length) + { + string longerTypeName; + string shorterTypeName; + + // figure out which type name is longer + if (typeName1.Length > typeName2.Length) + { + longerTypeName = typeName1; + shorterTypeName = typeName2; + } + else + { + longerTypeName = typeName2; + shorterTypeName = typeName1; + } + + // if the shorter type name matches the end of the longer one + if (longerTypeName.EndsWith(shorterTypeName, StringComparison.OrdinalIgnoreCase)) + { + int matchIndex = longerTypeName.Length - shorterTypeName.Length; + + // if the matched sub-string looks like the start of a namespace or class name + if ((longerTypeName[matchIndex - 1] == '.') || (longerTypeName[matchIndex - 1] == '+')) + { + int precedingBackslashes = 0; + + // confirm there are zero, or an even number of \'s preceding it... + for (int i = matchIndex - 2; i >= 0; i--) + { + if (longerTypeName[i] == '\\') + { + precedingBackslashes++; + } + else + { + break; + } + } + + if ((precedingBackslashes % 2) == 0) + { + isPartialMatch = true; + } + } + } + } + else + { + isPartialMatch = (String.Compare(typeName1, typeName2, StringComparison.OrdinalIgnoreCase) == 0); + } + + return isPartialMatch; + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + internal LoadedType Load + ( + string typeName, + AssemblyLoadInfo assembly + ) + { + return GetLoadedType(s_cacheOfLoadedTypesByFilterLock, s_loadInfoToTypeLock, s_cacheOfLoadedTypesByFilter, typeName, assembly); + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + /// The loaded type, or null if the type was not found. + internal LoadedType ReflectionOnlyLoad + ( + string typeName, + AssemblyLoadInfo assembly + ) + { + return GetLoadedType(s_cacheOfReflectionOnlyLoadedTypesByFilterLock, s_reflectionOnlyloadInfoToTypeLock, s_cacheOfReflectionOnlyLoadedTypesByFilter, typeName, assembly); + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + private LoadedType GetLoadedType(object cacheLock, object loadInfoToTypeLock, ConcurrentDictionary> cache, string typeName, AssemblyLoadInfo assembly) + { + // A given typefilter have been used on a number of assemblies, Based on the typefilter we will get another dictionary which + // will map a specific AssemblyLoadInfo to a AssemblyInfoToLoadedTypes class which knows how to find a typeName in a given assembly. + ConcurrentDictionary loadInfoToType = null; + lock (cacheLock) + { + if (!cache.TryGetValue(_isDesiredType, out loadInfoToType)) + { + loadInfoToType = new ConcurrentDictionary(); + cache.TryAdd(_isDesiredType, loadInfoToType); + } + } + + // Get an object which is able to take a typename and determine if it is in the assembly pointed to by the AssemblyInfo. + AssemblyInfoToLoadedTypes typeNameToType = null; + lock (loadInfoToTypeLock) + { + if (!loadInfoToType.TryGetValue(assembly, out typeNameToType)) + { + typeNameToType = new AssemblyInfoToLoadedTypes(_isDesiredType, assembly); + loadInfoToType.TryAdd(assembly, typeNameToType); + } + } + + return typeNameToType.GetLoadedTypeByTypeName(typeName); + } + + /// + /// Given a type filter and an asssemblyInfo object keep track of what types in a given assembly which match the typefilter. + /// Also, use this information to determine if a given TypeName is in the assembly which is pointed to by the AssemblyLoadInfo object. + /// + /// This type represents a combination of a type filter and an assemblyInfo object. + /// + private class AssemblyInfoToLoadedTypes + { + /// + /// Lock to prevent two threads from using this object at the same time. + /// Since we fill up internal structures with what is in the assembly + /// + private readonly Object _lockObject = new Object(); + + /// + /// Type filter to pick the correct types out of an assembly + /// + private TypeFilter _isDesiredType; + + /// + /// Assembly load information so we can load an assembly + /// + private AssemblyLoadInfo _assemblyLoadInfo; + + /// + /// What is the type for the given type name, this may be null if the typeName does not map to a type. + /// + private Dictionary _typeNameToType; + + /// + /// List of public types in the assembly which match the typefilter and their corresponding types + /// + private Dictionary _publicTypeNameToType; + + /// + /// Have we scanned the public types for this assembly yet. + /// + private bool _haveScannedPublicTypes; + + /// + /// Assembly, if any, that we loaded for this type. + /// We use this information to set the LoadedType.LoadedAssembly so that this object can be used + /// to help created AppDomains to resolve those that it could not load successfuly + /// + private Assembly _loadedAssembly; + + /// + /// Given a type filter, and an assembly to load the type information from determine if a given type name is in the assembly or not. + /// + internal AssemblyInfoToLoadedTypes(TypeFilter typeFilter, AssemblyLoadInfo loadInfo) + { + ErrorUtilities.VerifyThrowArgumentNull(typeFilter, "typefilter"); + ErrorUtilities.VerifyThrowArgumentNull(loadInfo, "loadInfo"); + + _isDesiredType = typeFilter; + _assemblyLoadInfo = loadInfo; + _typeNameToType = new Dictionary(StringComparer.OrdinalIgnoreCase); + _publicTypeNameToType = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Determine if a given type name is in the assembly or not. Return null if the type is not in the assembly + /// + internal LoadedType GetLoadedTypeByTypeName(string typeName) + { + ErrorUtilities.VerifyThrowArgumentNull(typeName, "typeName"); + + // Only one thread should be doing operations on this instance of the object at a time. + lock (_lockObject) + { + Type type = null; + + // Maybe we've already cracked open this assembly before. Check to see if the typeName is in the list we dont look for partial matches here + // this is an optimization. + bool foundType = _typeNameToType.TryGetValue(typeName, out type); + if (!foundType) + { + // We could still not find the type, lets try and resolve it by doing a get type. + if ((_assemblyLoadInfo.AssemblyName != null) && (typeName.Length > 0)) + { + try + { + // try to load the type using its assembly qualified name + type = Type.GetType(typeName + "," + _assemblyLoadInfo.AssemblyName, false /* don't throw on error */, true /* case-insensitive */); + } + catch (ArgumentException) + { + // Type.GetType() will throw this exception if the type name is invalid -- but we have no idea if it's the + // type or the assembly name that's the problem -- so just ignore the exception, because we're going to + // check the existence/validity of the assembly and type respectively, below anyway + } + + // if we found the type, it means its assembly qualified name was also its fully qualified name + if (type != null) + { + // if it's not the right type, bail out -- there's no point searching further since we already matched on the + // fully qualified name + if (!_isDesiredType(type, null)) + { + _typeNameToType.Add(typeName, null); + return null; + } + else + { + _typeNameToType.Add(typeName, type); + } + } + } + + // We could not find the type based on the passed in type name, we now need to see if there is a type which + // will match based on partially matching the typename. To do this partial matching we need to get the public types in the assembly + if (type == null && !_haveScannedPublicTypes) + { + ScanAssemblyForPublicTypes(); + _haveScannedPublicTypes = true; + } + + // Could not find the type we need to look through the types in the assembly or in our cache. + if (type == null) + { + foreach (KeyValuePair desiredTypeInAssembly in _publicTypeNameToType) + { + // if type matches partially on its name + if (typeName.Length == 0 || TypeLoader.IsPartialTypeNameMatch(desiredTypeInAssembly.Key, typeName)) + { + type = desiredTypeInAssembly.Value; + _typeNameToType.Add(typeName, type); + break; + } + } + } + } + + if (type != null) + { + return new LoadedType(type, _assemblyLoadInfo, _loadedAssembly); + } + + return null; + } + } + + /// + /// Scan the assembly pointed to by the assemblyLoadInfo for public types. We will use these public types to do partial name matching on + /// to find tasks, loggers, and task factories. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Justification = "Necessary in this case.")] + private void ScanAssemblyForPublicTypes() + { + // we need to search the assembly for the type... + try + { + if (_assemblyLoadInfo.AssemblyName != null) + { + _loadedAssembly = Assembly.Load(_assemblyLoadInfo.AssemblyName); + } + else + { + _loadedAssembly = Assembly.UnsafeLoadFrom(_assemblyLoadInfo.AssemblyFile); + } + } + catch (ArgumentException e) + { + // Assembly.Load() and Assembly.LoadFrom() will throw an ArgumentException if the assembly name is invalid + // convert to a FileNotFoundException because it's more meaningful + // NOTE: don't use ErrorUtilities.VerifyThrowFileExists() here because that will hit the disk again + throw new FileNotFoundException(null, _assemblyLoadInfo.AssemblyLocation, e); + } + + // only look at public types + Type[] allPublicTypesInAssembly = _loadedAssembly.GetExportedTypes(); + foreach (Type publicType in allPublicTypesInAssembly) + { + if (_isDesiredType(publicType, null)) + { + _publicTypeNameToType.Add(publicType.FullName, publicType); + } + } + } + } + } +} diff --git a/src/Shared/UnitTests/App.config b/src/Shared/UnitTests/App.config new file mode 100644 index 00000000000..78932d640d9 --- /dev/null +++ b/src/Shared/UnitTests/App.config @@ -0,0 +1,51 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Shared/UnitTests/AssemblyNameEx_Tests.cs b/src/Shared/UnitTests/AssemblyNameEx_Tests.cs new file mode 100644 index 00000000000..aee1c5483b8 --- /dev/null +++ b/src/Shared/UnitTests/AssemblyNameEx_Tests.cs @@ -0,0 +1,679 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Globalization; +using System.Resources; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class AssemblyNameEx_Tests + { + /// + /// Delegate defines a function that produces an AssemblyNameExtension from a string. + /// + /// + /// + internal delegate AssemblyNameExtension ProduceAssemblyNameEx(string name); + + private static string[] s_assemblyStrings = + { + "System.Xml, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.XML, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, Version=2.0.0.0, Culture=neutral", + "System.XM, Version=2.0.0.0", + "System.XM, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, Culture=neutral", + "System.Xml", + "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Drawing" + }; + + private static string[] s_assembliesForPartialMatch = + { + "System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=Yes", + "System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=No", + "System.Xml, Culture=en, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=10.0.0.0, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=10.0.0.0, Culture=en" + }; + + /// + /// All the different ways the same assembly name can be represented. + /// + private static ProduceAssemblyNameEx[] s_producers = + { + new ProduceAssemblyNameEx(ProduceAsString), + new ProduceAssemblyNameEx(ProduceAsAssemblyName), + new ProduceAssemblyNameEx(ProduceAsBoth), + new ProduceAssemblyNameEx(ProduceAsLowerString), + new ProduceAssemblyNameEx(ProduceAsLowerAssemblyName), + new ProduceAssemblyNameEx(ProduceAsLowerBoth) + }; + + + + private static AssemblyNameExtension ProduceAsString(string name) + { + return new AssemblyNameExtension(name); + } + + private static AssemblyNameExtension ProduceAsLowerString(string name) + { + return new AssemblyNameExtension(name.ToLower()); + } + + private static AssemblyNameExtension ProduceAsAssemblyName(string name) + { + return new AssemblyNameExtension(new AssemblyName(name)); + } + + private static AssemblyNameExtension ProduceAsLowerAssemblyName(string name) + { + return new AssemblyNameExtension(new AssemblyName(name.ToLower())); + } + + private static AssemblyNameExtension ProduceAsBoth(string name) + { + AssemblyNameExtension result = new AssemblyNameExtension(new AssemblyName(name)); + + // Force the string version to be produced too. + string backToString = result.FullName; + + return result; + } + + private static AssemblyNameExtension ProduceAsLowerBoth(string name) + { + return ProduceAsBoth(name.ToLower()); + } + + /// + /// General base name comparison validator. + /// + [TestMethod] + public void CompareBaseName() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in s_assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in s_assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in s_producers) + { + foreach (ProduceAssemblyNameEx produce2 in s_producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + int result = a1.CompareBaseNameTo(a2); + int resultBaseline = String.Compare(baseName1.Name, baseName2.Name, StringComparison.OrdinalIgnoreCase); + if (resultBaseline != result) + { + Assert.AreEqual(resultBaseline, result); + } + } + } + } + } + } + + /// + /// General compareTo validator + /// + [TestMethod] + public void CompareTo() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in s_assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in s_assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in s_producers) + { + foreach (ProduceAssemblyNameEx produce2 in s_producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + int result = a1.CompareTo(a2); + + if (a1.Equals(a2)) + { + Assert.AreEqual(0, result); + } + + if (a1.CompareBaseNameTo(a2) != 0) + { + Assert.AreEqual(a1.CompareBaseNameTo(a2), result); + } + + if + ( + a1.CompareBaseNameTo(a2) == 0 // Only check version if basenames match + && a1.Version != a2.Version + ) + { + if (a1.Version == null) + { + // Expect -1 if a1.Version is null and the baseNames match + Assert.AreEqual(-1, result); + } + else + { + Assert.AreEqual(a1.Version.CompareTo(a2.Version), result); + } + } + + int resultBaseline = String.Compare(a1.FullName, a2.FullName, StringComparison.OrdinalIgnoreCase); + // Only check to see if the result and the resultBaseline match when the result baseline is 0 and the result is not 0. + if (resultBaseline != result && resultBaseline == 0) + { + Assert.AreEqual(resultBaseline, result); + } + } + } + } + } + } + + [TestMethod] + public void ExerciseMiscMethods() + { + AssemblyNameExtension a1 = s_producers[0](s_assemblyStrings[0]); + Assert.IsNotNull(a1.GetHashCode()); + + Version newVersion = new Version(1, 2); + a1.ReplaceVersion(newVersion); + Assert.IsTrue(a1.Version.Equals(newVersion)); + + Assert.IsNotNull(a1.ToString()); + } + + [TestMethod] + public void EscapeDisplayNameCharacters() + { + // /// Those characters are Equals(=), Comma(,), Quote("), Apostrophe('), Backslash(\). + string displayName = @"Hello,""Don't"" eat the \CAT"; + Assert.IsTrue(String.Compare(AssemblyNameExtension.EscapeDisplayNameCharacters(displayName), @"Hello\,\""Don\'t\"" eat the \\CAT", StringComparison.OrdinalIgnoreCase) == 0); + } + + + /// + /// General equals comparison validator. + /// + [TestMethod] + public void Equals() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in s_assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in s_assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in s_producers) + { + foreach (ProduceAssemblyNameEx produce2 in s_producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + // Baseline is a mismatch which is known to exercise + // the full code path. + AssemblyNameExtension a3 = ProduceAsAssemblyName(assemblyString1); + AssemblyNameExtension a4 = ProduceAsString(assemblyString2); + + bool result = a1.Equals(a2); + bool resultBaseline = a3.Equals(a4); + if (result != resultBaseline) + { + Assert.AreEqual(resultBaseline, result); + } + } + } + } + } + } + + + /// + /// General equals comparison validator when we are ignoring the version numbers in the name. + /// + [TestMethod] + public void EqualsIgnoreVersion() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in s_assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in s_assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in s_producers) + { + foreach (ProduceAssemblyNameEx produce2 in s_producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + // Baseline is a mismatch which is known to exercise + // the full code path. + AssemblyNameExtension a3 = ProduceAsAssemblyName(assemblyString1); + AssemblyNameExtension a4 = ProduceAsString(assemblyString2); + + bool result = a1.EqualsIgnoreVersion(a2); + bool resultBaseline = a3.EqualsIgnoreVersion(a4); + if (result != resultBaseline) + { + Assert.AreEqual(resultBaseline, result); + } + } + } + } + } + } + + /// + /// This repros a bug that was found while coding AssemblyNameExtension. + /// + [TestMethod] + public void CompareBaseNameRealCase1() + { + AssemblyNameExtension a1 = ProduceAsBoth("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension a2 = ProduceAsString("System.Drawing"); + + int result = a1.CompareBaseNameTo(a2); + + // Base names should be equal. + Assert.AreEqual(0, result); + } + + /// + /// Verify an exception is thrown when the simple name is not in the itemspec. + /// + /// + [TestMethod] + [ExpectedException(typeof(FileLoadException))] + public void CreateAssemblyNameExtensionWithNoSimpleName() + { + AssemblyNameExtension extension = new AssemblyNameExtension("Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a", true); + } + + /// + /// Verify an exception is thrown when the simple name is not in the itemspec. + /// + /// + [TestMethod] + [ExpectedException(typeof(FileLoadException))] + public void CreateAssemblyNameExtensionWithNoSimpleName2() + { + AssemblyNameExtension extension = new AssemblyNameExtension("Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension extension2 = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + extension2.PartialNameCompare(extension); + } + + /// + /// Create an assembly name extension providing the name, version, culture, and public key. Also test cases + /// where the public key is the only item specified + /// + [TestMethod] + public void CreateAssemblyNameWithNameAndVersionCulturePublicKey() + { + AssemblyNameExtension extension = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A, Version=2.0.0.0, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(Object.ReferenceEquals(extension.CultureInfo, null)); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Object.ReferenceEquals(extension.Version, null)); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Object.ReferenceEquals(extension.Version, null)); + Assert.IsTrue(Object.ReferenceEquals(extension.CultureInfo, null)); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Object.ReferenceEquals(extension.Version, null)); + Assert.IsTrue(Object.ReferenceEquals(extension.CultureInfo, null)); + } + + /// + /// Make sure processor architecture is seen when it is in the string. + /// + [TestMethod] + public void CreateAssemblyNameWithNameAndProcessorArchitecture() + { + AssemblyNameExtension extension = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, ProcessorArchitecture=MSIL"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + Assert.IsTrue(extension.FullName.Contains("MSIL")); + Assert.IsTrue(extension.HasProcessorArchitectureInFusionName); + + extension = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + Assert.IsFalse(extension.HasProcessorArchitectureInFusionName); + } + + + /// + /// Verify partial matching on the simple name works + /// + [TestMethod] + public void TestAssemblyPatialMatchSimpleName() + { + AssemblyNameExtension assemblyNameToMatch = new AssemblyNameExtension("System.Xml"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xmla"); + + foreach (string assembly in s_assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + Assert.IsTrue(assemblyNameToMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName)); + } + } + + /// + /// Verify partial matching on the simple name and version + /// + [TestMethod] + public void TestAssemblyPatialMatchSimpleNameVersion() + { + AssemblyNameExtension assemblyNameToMatchVersion = new AssemblyNameExtension("System.Xml, Version=10.0.0.0"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, Version=5.0.0.0"); + AssemblyNameExtension assemblyMatchNoVersion = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in s_assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + // If there is a version make sure the assembly name with the correct version matches + // Make sure the assembly with the wrong version does not match + if (assemblyToCompare.Version != null) + { + Assert.IsTrue(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + // Matches because version is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + } + else + { + // If there is no version make names with a version specified do not match + Assert.IsFalse(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + // Matches because version is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + } + } + } + + /// + /// Verify partial matching on the simple name and culture + /// + [TestMethod] + public void TestAssemblyPatialMatchSimpleNameCulture() + { + AssemblyNameExtension assemblyNameToMatchCulture = new AssemblyNameExtension("System.Xml, Culture=en"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, Culture=de-DE"); + AssemblyNameExtension assemblyMatchNoVersion = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in s_assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + // If there is a version make sure the assembly name with the correct culture matches + // Make sure the assembly with the wrong culture does not match + if (assemblyToCompare.CultureInfo != null) + { + Assert.IsTrue(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + // Matches because culture is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + } + else + { + // If there is no version make names with a culture specified do not match + Assert.IsFalse(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + // Matches because culture is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + } + } + } + + /// + /// Verify partial matching on the simple name and PublicKeyToken + /// + [TestMethod] + public void TestAssemblyPatialMatchSimpleNamePublicKeyToken() + { + AssemblyNameExtension assemblyNameToMatchPublicToken = new AssemblyNameExtension("System.Xml, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, PublicKeyToken=b03f5f7f11d50a3b"); + AssemblyNameExtension assemblyMatchNoVersion = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in s_assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + // If there is a version make sure the assembly name with the correct publicKeyToken matches + // Make sure the assembly with the wrong publicKeyToken does not match + if (assemblyToCompare.GetPublicKeyToken() != null) + { + Assert.IsTrue(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + // Matches because publicKeyToken is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + } + else + { + // If there is no version make names with a publicKeyToken specified do not match + Assert.IsFalse(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + // Matches because publicKeyToken is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + } + } + } + + /// + /// Verify partial matching on the simple name and retargetable + /// + [TestMethod] + public void TestAssemblyPartialMatchSimpleNameRetargetable() + { + AssemblyNameExtension assemblyNameToMatchRetargetable = new AssemblyNameExtension("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=Yes"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=No"); + AssemblyNameExtension assemblyMatchNoRetargetable = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in s_assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + if (assemblyToCompare.FullName.IndexOf("Retargetable=Yes", StringComparison.OrdinalIgnoreCase) >= 0) + { + Assert.IsTrue(assemblyNameToMatchRetargetable.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchRetargetable.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName, true)); + + Assert.IsTrue(assemblyToCompare.PartialNameCompare(assemblyNameToNotMatch)); + Assert.IsFalse(assemblyToCompare.PartialNameCompare(assemblyNameToNotMatch, PartialComparisonFlags.SimpleName, true)); + + Assert.IsFalse(assemblyToCompare.PartialNameCompare(assemblyMatchNoRetargetable)); + Assert.IsFalse(assemblyToCompare.PartialNameCompare(assemblyMatchNoRetargetable, PartialComparisonFlags.SimpleName, true)); + + Assert.IsTrue(assemblyMatchNoRetargetable.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyMatchNoRetargetable.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName, true)); + } + else + { + Assert.IsFalse(assemblyNameToMatchRetargetable.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName, true)); + + // Match because retargetable false is the same as no retargetable bit + bool match = assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare); + if (assemblyToCompare.FullName.IndexOf("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a", StringComparison.OrdinalIgnoreCase) >= 0) + { + Assert.IsTrue(match); + } + else + { + Assert.IsFalse(match); + } + Assert.IsTrue(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName, true)); + + Assert.IsTrue(assemblyMatchNoRetargetable.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoRetargetable.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName, true)); + } + } + } + + + /// + /// Make sure that our assemblyNameComparers correctly work. + /// + [TestMethod] + public void VerifyAssemblyNameComparers() + { + AssemblyNameExtension a = new AssemblyNameExtension("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=Yes"); + AssemblyNameExtension b = new AssemblyNameExtension("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=No"); + AssemblyNameExtension c = new AssemblyNameExtension("System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=Yes"); + + AssemblyNameExtension d = new AssemblyNameExtension("System.Xml, Version=9.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=No"); + AssemblyNameExtension e = new AssemblyNameExtension("System.Xml, Version=11.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, Retargetable=No"); + + Assert.IsTrue(AssemblyNameComparer.GenericComparer.Equals(a, b)); + Assert.IsFalse(AssemblyNameComparer.GenericComparer.Equals(a, d)); + + Assert.IsFalse(AssemblyNameComparer.GenericComparerConsiderRetargetable.Equals(a, b)); + Assert.IsTrue(AssemblyNameComparer.GenericComparerConsiderRetargetable.Equals(a, c)); + Assert.IsFalse(AssemblyNameComparer.GenericComparerConsiderRetargetable.Equals(a, d)); + + + Assert.IsTrue(AssemblyNameComparer.Comparer.Compare(a, b) == 0); + Assert.IsTrue(AssemblyNameComparer.Comparer.Compare(a, d) > 0); + Assert.IsTrue(AssemblyNameComparer.Comparer.Compare(a, e) < 0); + + Assert.IsTrue(AssemblyNameComparer.ComparerConsiderRetargetable.Compare(a, c) == 0); + Assert.IsTrue(AssemblyNameComparer.ComparerConsiderRetargetable.Compare(a, b) > 0); + Assert.IsTrue(AssemblyNameComparer.ComparerConsiderRetargetable.Compare(a, d) > 0); + Assert.IsTrue(AssemblyNameComparer.ComparerConsiderRetargetable.Compare(a, e) < 0); + } + + + /// + /// Make sure the reverse version comparer will compare the version in a way that would sort them in reverse order. + /// + [TestMethod] + public void VerifyReverseVersionComparer() + { + AssemblyNameExtension x = new AssemblyNameExtension("System, Version=2.0.0.0"); + AssemblyNameExtension y = new AssemblyNameExtension("System, Version=1.0.0.0"); + AssemblyNameExtension z = new AssemblyNameExtension("System, Version=2.0.0.0"); + AssemblyNameExtension a = new AssemblyNameExtension("Zar, Version=3.0.0.0"); + + AssemblyNameReverseVersionComparer reverseComparer = new AssemblyNameReverseVersionComparer(); + Assert.AreEqual(-1, reverseComparer.Compare(x, y)); + Assert.AreEqual(1, reverseComparer.Compare(y, x)); + Assert.AreEqual(0, reverseComparer.Compare(x, z)); + Assert.AreEqual(0, reverseComparer.Compare(null, null)); + Assert.AreEqual(-1, reverseComparer.Compare(x, null)); + Assert.AreEqual(1, reverseComparer.Compare(null, y)); + Assert.AreEqual(-1, reverseComparer.Compare(a, x)); + + List assemblies = new List(); + assemblies.Add(y); + assemblies.Add(x); + assemblies.Add(z); + + assemblies.Sort(AssemblyNameReverseVersionComparer.GenericComparer); + + Assert.IsTrue(assemblies[0].Equals(x)); + Assert.IsTrue(assemblies[1].Equals(z)); + Assert.IsTrue(assemblies[2].Equals(y)); + } + } +} + + + + diff --git a/src/Shared/UnitTests/BuildEventArgsExtension.cs b/src/Shared/UnitTests/BuildEventArgsExtension.cs new file mode 100644 index 00000000000..6010ea07a64 --- /dev/null +++ b/src/Shared/UnitTests/BuildEventArgsExtension.cs @@ -0,0 +1,449 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper extension to BuildEventArgs +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Collections; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using System.Collections.Generic; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests +{ + /// + /// A class containing an extension to BuildEventArgs + /// + internal static class BuildEventArgsExtension + { + /// + /// Extension method to help our tests without adding shipping code. + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// The 'this' object + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(this BuildEventArgs args, BuildEventArgs other) + { + if (Object.ReferenceEquals(args, other)) + { + return true; + } + + if (Object.ReferenceEquals(other, null) || Object.ReferenceEquals(args, null)) + { + return false; + } + + if (args.GetType() != other.GetType()) + { + return false; + } + + if (args.Timestamp.Ticks != other.Timestamp.Ticks) + { + return false; + } + + if (args.BuildEventContext != other.BuildEventContext) + { + return false; + } + + if (!String.Equals(args.HelpKeyword, other.HelpKeyword, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Just in case we're matching chk against ret or vice versa, make sure the message still registers as the same + string fixedArgsMessage = args.Message.Replace("\r\nThis is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER.", String.Empty); + string fixedOtherMessage = other.Message.Replace("\r\nThis is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER.", String.Empty); + + if (!String.Equals(fixedArgsMessage, fixedOtherMessage, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.SenderName, other.SenderName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (args.ThreadId != other.ThreadId) + { + return false; + } + + return true; + } + + /// + /// Extension method to help our tests without adding shipping code. + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// The 'this' object + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(this BuildFinishedEventArgs args, BuildFinishedEventArgs other) + { + if (args.Succeeded != other.Succeeded) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compares the value fields in the class to the passed in object and check to see if they are the same. + /// + /// Object to compare to this instance + /// True if the value fields are identical, false if otherwise + public static bool IsEquivalent(this BuildMessageEventArgs args, BuildMessageEventArgs other) + { + if (args.Importance != other.Importance) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(this BuildErrorEventArgs args, BuildErrorEventArgs other) + { + if (args.ColumnNumber != other.ColumnNumber) + { + return false; + } + + if (args.EndColumnNumber != other.EndColumnNumber) + { + return false; + } + + if (args.LineNumber != other.LineNumber) + { + return false; + } + + if (args.EndLineNumber != other.EndLineNumber) + { + return false; + } + + if (!String.Equals(args.File, other.File, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.Code, other.Code, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.Subcategory, other.Subcategory, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(this BuildWarningEventArgs args, BuildWarningEventArgs other) + { + if (args.ColumnNumber != other.ColumnNumber) + { + return false; + } + + if (args.EndColumnNumber != other.EndColumnNumber) + { + return false; + } + + if (args.LineNumber != other.LineNumber) + { + return false; + } + + if (args.EndLineNumber != other.EndLineNumber) + { + return false; + } + + if (!String.Equals(args.File, other.File, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.Code, other.Code, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.Subcategory, other.Subcategory, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(ProjectStartedEventArgs args, ProjectStartedEventArgs other) + { + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TargetNames, other.TargetNames, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(ExternalProjectStartedEventArgs args, ExternalProjectStartedEventArgs other) + { + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TargetNames, other.TargetNames, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(ProjectFinishedEventArgs args, ProjectFinishedEventArgs other) + { + if (args.Succeeded != other.Succeeded) + { + return false; + } + + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(ExternalProjectFinishedEventArgs args, ExternalProjectFinishedEventArgs other) + { + if (args.Succeeded != other.Succeeded) + { + return false; + } + + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(TargetStartedEventArgs args, TargetStartedEventArgs other) + { + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TargetFile, other.TargetFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TargetName, other.TargetName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.ParentTarget, other.ParentTarget, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(TargetFinishedEventArgs args, TargetFinishedEventArgs other) + { + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TargetFile, other.TargetFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TargetName, other.TargetName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + + if (!Object.ReferenceEquals(args.TargetOutputs, other.TargetOutputs)) + { + // See if one is null, if so they are not equal + if (args.TargetOutputs == null || other.TargetOutputs == null) + { + return false; + } + + List argItemIncludes = new List(); + foreach (TaskItem item in args.TargetOutputs) + { + argItemIncludes.Add(item.ToString()); + } + + List otherItemIncludes = new List(); + foreach (TaskItem item in other.TargetOutputs) + { + otherItemIncludes.Add(item.ToString()); + } + + argItemIncludes.Sort(); + otherItemIncludes.Sort(); + + if (argItemIncludes.Count != otherItemIncludes.Count) + { + return false; + } + + // Since the lists are sorted each include must match + for (int i = 0; i < argItemIncludes.Count; i++) + { + if (!argItemIncludes[i].Equals(otherItemIncludes[i], StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(TaskStartedEventArgs args, TaskStartedEventArgs other) + { + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TaskFile, other.TaskFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TaskName, other.TaskName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + + /// + /// Compare this build event context with another object to determine + /// equality. This means the values inside the object are identical. + /// + /// Object to compare to this object + /// True if the object values are identical, false if they are not identical + public static bool IsEquivalent(TaskFinishedEventArgs args, TaskFinishedEventArgs other) + { + if (!String.Equals(args.ProjectFile, other.ProjectFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TaskFile, other.TaskFile, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(args.TaskName, other.TaskName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (args.Succeeded != other.Succeeded) + { + return false; + } + + return ((BuildEventArgs)args).IsEquivalent(other); + } + } +} \ No newline at end of file diff --git a/src/Shared/UnitTests/CopyOnWriteDictionary_Tests.cs b/src/Shared/UnitTests/CopyOnWriteDictionary_Tests.cs new file mode 100644 index 00000000000..30facc6e6c7 --- /dev/null +++ b/src/Shared/UnitTests/CopyOnWriteDictionary_Tests.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the copy on write dictionary class +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for the CopyOnWriteDictionary + /// + [TestClass] + public class CopyOnWriteDictionary_Tests + { + /// + /// Find with the same key inserted using the indexer + /// + [TestMethod] + public void Indexer_ReferenceFound() + { + object k1 = new Object(); + object v1 = new Object(); + + var dictionary = new CopyOnWriteDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + object v2 = dictionary[k1]; + + Assert.AreEqual(true, Object.ReferenceEquals(v1, v2)); + Assert.AreEqual(true, dictionary.ContainsKey(k1)); + } + + /// + /// Find something not present with the indexer + /// + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void Indexer_NotFound() + { + var dictionary = new CopyOnWriteDictionary(); + object value = dictionary[new Object()]; + } + + /// + /// Find with the same key inserted using TryGetValue + /// + [TestMethod] + public void TryGetValue_ReferenceFound() + { + object k1 = new Object(); + object v1 = new Object(); + + var dictionary = new CopyOnWriteDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + object v2; + bool result = dictionary.TryGetValue(k1, out v2); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// Find something not present with TryGetValue + /// + [TestMethod] + public void TryGetValue_ReferenceNotFound() + { + var dictionary = new CopyOnWriteDictionary(); + + object v; + bool result = dictionary.TryGetValue(new Object(), out v); + + Assert.AreEqual(false, result); + Assert.AreEqual(null, v); + Assert.AreEqual(false, dictionary.ContainsKey(new Object())); + } + + /// + /// Find a key that wasn't inserted but is equal + /// + [TestMethod] + public void EqualityComparer() + { + string k1 = String.Concat("ke", "y"); + object v1 = new Object(); + + var dictionary = new CopyOnWriteDictionary(); + dictionary[k1] = v1; + + // Now look for a different but equatable key + // Don't create it with a literal or the compiler will intern it! + string k2 = String.Concat("k", "ey"); + + Assert.AreEqual(false, Object.ReferenceEquals(k1, k2)); + + object v2 = dictionary[k2]; + + Assert.AreEqual(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// Cloning sees the same values + /// + [TestMethod] + public void CloneVisibility() + { + var dictionary = new CopyOnWriteDictionary(); + dictionary["test"] = "1"; + Assert.AreEqual(dictionary["test"], "1"); + + var clone = dictionary.Clone(); + + Assert.AreEqual(clone["test"], "1"); + Assert.AreEqual(clone.Count, dictionary.Count); + } + + /// + /// Clone uses same comparer + /// + [TestMethod] + public void CloneComparer() + { + var dictionary = new CopyOnWriteDictionary(StringComparer.OrdinalIgnoreCase); + dictionary["test"] = "1"; + Assert.AreEqual(dictionary["test"], "1"); + + var clone = dictionary.Clone(); + + Assert.AreEqual(clone["TEST"], "1"); + } + + /// + /// Writes to original not visible to clone + /// + [TestMethod] + public void OriginalWritesNotVisibleToClones() + { + var dictionary = new CopyOnWriteDictionary(); + dictionary["test"] = "1"; + Assert.AreEqual(dictionary["test"], "1"); + + var clone = dictionary.Clone(); + var clone2 = dictionary.Clone(); + + Assert.IsTrue(dictionary.HasSameBacking(clone)); + Assert.IsTrue(dictionary.HasSameBacking(clone2)); + + dictionary["test"] = "2"; + + Assert.IsFalse(dictionary.HasSameBacking(clone)); + Assert.IsFalse(dictionary.HasSameBacking(clone2)); + Assert.IsTrue(clone.HasSameBacking(clone2)); + + Assert.AreEqual(clone["test"], "1"); + Assert.AreEqual(clone2["test"], "1"); + } + + /// + /// Writes to clone not visible to original + /// + [TestMethod] + public void CloneWritesNotVisibleToOriginal() + { + var dictionary = new CopyOnWriteDictionary(); + dictionary["test"] = "1"; + Assert.AreEqual(dictionary["test"], "1"); + + var clone = dictionary.Clone(); + var clone2 = dictionary.Clone(); + + Assert.IsTrue(dictionary.HasSameBacking(clone)); + Assert.IsTrue(dictionary.HasSameBacking(clone2)); + + clone["test"] = "2"; + Assert.IsFalse(dictionary.HasSameBacking(clone)); + Assert.IsFalse(clone2.HasSameBacking(clone)); + Assert.IsTrue(dictionary.HasSameBacking(clone2)); + + clone2["test"] = "3"; + Assert.IsFalse(dictionary.HasSameBacking(clone2)); + + Assert.AreEqual(dictionary["test"], "1"); + Assert.AreEqual(clone["test"], "2"); + } + + /// + /// Serialize basic case + /// + [TestMethod] + public void SerializeDeserialize() + { + CopyOnWriteDictionary dictionary = new CopyOnWriteDictionary(); + dictionary.Add(1, "1"); + + using (MemoryStream stream = new MemoryStream()) + { + BinaryFormatter formatter = new BinaryFormatter(); + + formatter.Serialize(stream, dictionary); + stream.Position = 0; + + var dictionary2 = (CopyOnWriteDictionary)formatter.Deserialize(stream); + + Assert.AreEqual(dictionary.Count, dictionary2.Count); + Assert.AreEqual(dictionary.Comparer, dictionary2.Comparer); + Assert.AreEqual("1", dictionary2[1]); + + dictionary2.Add(2, "2"); + } + } + + /// + /// Serialize custom comparer + /// + [TestMethod] + public void SerializeDeserialize2() + { + CopyOnWriteDictionary dictionary = new CopyOnWriteDictionary(MSBuildNameIgnoreCaseComparer.Default); + + using (MemoryStream stream = new MemoryStream()) + { + BinaryFormatter formatter = new BinaryFormatter(); + + formatter.Serialize(stream, dictionary); + stream.Position = 0; + + CopyOnWriteDictionary dictionary2 = (CopyOnWriteDictionary)formatter.Deserialize(stream); + + Assert.AreEqual(dictionary.Count, dictionary2.Count); + Assert.AreEqual(typeof(MSBuildNameIgnoreCaseComparer), dictionary2.Comparer.GetType()); + } + } + } +} diff --git a/src/Shared/UnitTests/ErrorUtilities_Tests.cs b/src/Shared/UnitTests/ErrorUtilities_Tests.cs new file mode 100644 index 00000000000..c35f6fbc076 --- /dev/null +++ b/src/Shared/UnitTests/ErrorUtilities_Tests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#region Using directives +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Shared; + + +#endregion +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ErrorUtilities_Tests + { + [TestMethod] + public void VerifyThrowFalse() + { + try + { + ErrorUtilities.VerifyThrow(false, "msbuild rules"); + } + catch (InternalErrorException e) + { + Assert.IsTrue(e.Message.Contains("msbuild rules"), "exception message"); + return; + } + + Assert.Fail("Should have thrown an exception"); + } + + [TestMethod] + public void VerifyThrowTrue() + { + // This shouldn't throw. + ErrorUtilities.VerifyThrow(true, "msbuild rules"); + } + + [TestMethod] + public void VerifyThrow0True() + { + // This shouldn't throw. + ErrorUtilities.VerifyThrow(true, "blah"); + } + + [TestMethod] + public void VerifyThrow1True() + { + // This shouldn't throw. + ErrorUtilities.VerifyThrow(true, "{0}", "a"); + } + + [TestMethod] + public void VerifyThrow2True() + { + // This shouldn't throw. + ErrorUtilities.VerifyThrow(true, "{0}{1}", "a", "b"); + } + + [TestMethod] + public void VerifyThrow3True() + { + // This shouldn't throw. + ErrorUtilities.VerifyThrow(true, "{0}{1}{2}", "a", "b", "c"); + } + + [TestMethod] + public void VerifyThrow4True() + { + // This shouldn't throw. + ErrorUtilities.VerifyThrow(true, "{0}{1}{2}{3}", "a", "b", "c", "d"); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void VerifyThrowArgumentArraysSameLength1() + { + ErrorUtilities.VerifyThrowArgumentArraysSameLength(null, new string[1], string.Empty, string.Empty); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void VerifyThrowArgumentArraysSameLength2() + { + ErrorUtilities.VerifyThrowArgumentArraysSameLength(new string[1], null, string.Empty, string.Empty); + } + + [ExpectedException(typeof(ArgumentException))] + [TestMethod] + public void VerifyThrowArgumentArraysSameLength3() + { + ErrorUtilities.VerifyThrowArgumentArraysSameLength(new string[1], new string[2], string.Empty, string.Empty); + } + + [TestMethod] + public void VerifyThrowArgumentArraysSameLength4() + { + ErrorUtilities.VerifyThrowArgumentArraysSameLength(new string[1], new string[1], string.Empty, string.Empty); + } + } +} diff --git a/src/Shared/UnitTests/EscapingUtilities_Tests.cs b/src/Shared/UnitTests/EscapingUtilities_Tests.cs new file mode 100644 index 00000000000..f382c2bde16 --- /dev/null +++ b/src/Shared/UnitTests/EscapingUtilities_Tests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#region Using directives +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Shared; + + +#endregion +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class EscapingUtilities_Tests + { + /// + /// + [TestMethod] + public void Unescape() + { + Assert.AreEqual("", EscapingUtilities.UnescapeAll("")); + Assert.AreEqual("foo", EscapingUtilities.UnescapeAll("foo")); + Assert.AreEqual("foo space", EscapingUtilities.UnescapeAll("foo%20space")); + Assert.AreEqual("foo2;", EscapingUtilities.UnescapeAll("foo2%3B")); + Assert.AreEqual(";foo3", EscapingUtilities.UnescapeAll("%3bfoo3")); + Assert.AreEqual(";", EscapingUtilities.UnescapeAll("%3b")); + Assert.AreEqual(";;;;;", EscapingUtilities.UnescapeAll("%3b%3B;%3b%3B")); + Assert.AreEqual("%3B", EscapingUtilities.UnescapeAll("%253B")); + Assert.AreEqual("===%ZZ %%%===", EscapingUtilities.UnescapeAll("===%ZZ%20%%%===")); + Assert.AreEqual("hello; escaping% how( are) you?", EscapingUtilities.UnescapeAll("hello%3B escaping%25 how%28 are%29 you%3f")); + + Assert.AreEqual("%*?*%*", EscapingUtilities.UnescapeAll("%25*?*%25*")); + Assert.AreEqual("%*?*%*", EscapingUtilities.UnescapeAll("%25%2a%3f%2a%25%2a")); + + Assert.AreEqual("*Star*craft or *War*cr@ft??", EscapingUtilities.UnescapeAll("%2aStar%2Acraft%20or %2aWar%2Acr%40ft%3f%3F")); + } + + /// + /// + [TestMethod] + public void Escape() + { + Assert.AreEqual("%2a", EscapingUtilities.Escape("*")); + Assert.AreEqual("%3f", EscapingUtilities.Escape("?")); + Assert.AreEqual("#%2a%3f%2a#%2a", EscapingUtilities.Escape("#*?*#*")); + Assert.AreEqual("%25%2a%3f%2a%25%2a", EscapingUtilities.Escape("%*?*%*")); + } + + /// + /// + [TestMethod] + public void UnescapeEscape() + { + string text; + + text = "*"; + Assert.AreEqual(text, EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(text))); + + text = "?"; + Assert.AreEqual(text, EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(text))); + + text = "#*?*#*"; + Assert.AreEqual(text, EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(text))); + } + + /// + /// + [TestMethod] + public void EscapeUnescape() + { + string text; + + text = "%2a"; + Assert.AreEqual(text, EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(text))); + + text = "%3f"; + Assert.AreEqual(text, EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(text))); + + text = "#%2a%3f%2a#%2a"; + Assert.AreEqual(text, EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(text))); + } + + [TestMethod] + public void ContainsEscapedWildcards() + { + Assert.IsFalse(EscapingUtilities.ContainsEscapedWildcards("NoStarOrQMark")); + Assert.IsFalse(EscapingUtilities.ContainsEscapedWildcards("%4")); + Assert.IsFalse(EscapingUtilities.ContainsEscapedWildcards("%3B")); + Assert.IsFalse(EscapingUtilities.ContainsEscapedWildcards("%2B")); + Assert.IsTrue(EscapingUtilities.ContainsEscapedWildcards("%2a")); + Assert.IsTrue(EscapingUtilities.ContainsEscapedWildcards("%2A")); + Assert.IsTrue(EscapingUtilities.ContainsEscapedWildcards("%3F")); + Assert.IsTrue(EscapingUtilities.ContainsEscapedWildcards("%3f")); + } + } +} diff --git a/src/Shared/UnitTests/FileMatcher_Tests.cs b/src/Shared/UnitTests/FileMatcher_Tests.cs new file mode 100644 index 00000000000..7cac54edcc0 --- /dev/null +++ b/src/Shared/UnitTests/FileMatcher_Tests.cs @@ -0,0 +1,1313 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class FileMatcherTest + { + /* + * Method: GetFileSystemEntries + * + * Simulate Directories.GetFileSystemEntries where file names are short. + * + */ + private static string[] GetFileSystemEntries(FileMatcher.FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) + { + if + ( + pattern == @"LONGDI~1" + && (@"D:\" == path || @"\\server\share\" == path || path.Length == 0) + ) + { + return new string[] { Path.Combine(path, "LongDirectoryName") }; + } + else if + ( + pattern == @"LONGSU~1" + && (@"D:\LongDirectoryName" == path || @"\\server\share\LongDirectoryName" == path || @"LongDirectoryName" == path) + ) + { + return new string[] { Path.Combine(path, "LongSubDirectory") }; + } + else if + ( + pattern == @"LONGFI~1.TXT" + && (@"D:\LongDirectoryName\LongSubDirectory" == path || @"\\server\share\LongDirectoryName\LongSubDirectory" == path || @"LongDirectoryName\LongSubDirectory" == path) + ) + { + return new string[] { Path.Combine(path, "LongFileName.txt") }; + } + else if + ( + pattern == @"pomegr~1" + && @"c:\apple\banana\tomato" == path + ) + { + return new string[] { Path.Combine(path, "pomegranate") }; + } + else if + ( + @"c:\apple\banana\tomato\pomegranate\orange" == path + ) + { + // No files exist here. This is an empty directory. + return new string[0]; + } + else + { + Console.WriteLine("GetFileSystemEntries('{0}', '{1}')", path, pattern); + Assert.Fail("Unexpected input into GetFileSystemEntries"); + } + return new string[] { "" }; + } + + /// + /// Simple test of the MatchDriver code. + /// + [TestMethod] + public void BasicMatchDriver() + { + MatchDriver + ( + @"Source\**", + new string[] // Files that exist and should match. + { + @"Source\Bart.txt", + @"Source\Sub\Homer.txt", + }, + new string[] // Files that exist and should not match. + { + @"Destination\Bart.txt", + @"Destination\Sub\Homer.txt", + }, + null + ); + } + + /// + /// This pattern should *not* recurse indefinitely since there is no '**' in the pattern: + /// + /// c:\?emp\foo + /// + /// + [TestMethod] + public void Regress162390() + { + MatchDriver + ( + @"c:\?emp\foo.txt", + new string[] { @"c:\temp\foo.txt" }, // Should match + new string[] { @"c:\timp\foo.txt" }, // Shouldn't match + new string[] // Should not even consider. + { + @"c:\temp\sub\foo.txt" + } + ); + } + + + /* + * Method: GetLongFileNameForShortLocalPath + * + * Convert a short local path to a long path. + * + */ + [TestMethod] + public void GetLongFileNameForShortLocalPath() + { + string longPath = FileMatcher.GetLongPathName + ( + @"D:\LONGDI~1\LONGSU~1\LONGFI~1.TXT", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(longPath, @"D:\LongDirectoryName\LongSubDirectory\LongFileName.txt"); + } + + /* + * Method: GetLongFileNameForLongLocalPath + * + * Convert a long local path to a long path (nop). + * + */ + [TestMethod] + public void GetLongFileNameForLongLocalPath() + { + string longPath = FileMatcher.GetLongPathName + ( + @"D:\LongDirectoryName\LongSubDirectory\LongFileName.txt", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(longPath, @"D:\LongDirectoryName\LongSubDirectory\LongFileName.txt"); + } + + /* + * Method: GetLongFileNameForShortUncPath + * + * Convert a short UNC path to a long path. + * + */ + [TestMethod] + public void GetLongFileNameForShortUncPath() + { + string longPath = FileMatcher.GetLongPathName + ( + @"\\server\share\LONGDI~1\LONGSU~1\LONGFI~1.TXT", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(longPath, @"\\server\share\LongDirectoryName\LongSubDirectory\LongFileName.txt"); + } + + /* + * Method: GetLongFileNameForLongUncPath + * + * Convert a long UNC path to a long path (nop) + * + */ + [TestMethod] + public void GetLongFileNameForLongUncPath() + { + string longPath = FileMatcher.GetLongPathName + ( + @"\\server\share\LongDirectoryName\LongSubDirectory\LongFileName.txt", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(longPath, @"\\server\share\LongDirectoryName\LongSubDirectory\LongFileName.txt"); + } + + /* + * Method: GetLongFileNameForRelativePath + * + * Convert a short relative path to a long path + * + */ + [TestMethod] + public void GetLongFileNameForRelativePath() + { + string longPath = FileMatcher.GetLongPathName + ( + @"LONGDI~1\LONGSU~1\LONGFI~1.TXT", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(longPath, @"LongDirectoryName\LongSubDirectory\LongFileName.txt"); + } + + /* + * Method: GetLongFileNameForRelativePathPreservesTrailingSlash + * + * Convert a short relative path with a trailing backslash to a long path + * + */ + [TestMethod] + public void GetLongFileNameForRelativePathPreservesTrailingSlash() + { + string longPath = FileMatcher.GetLongPathName + ( + @"LONGDI~1\LONGSU~1\", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(@"LongDirectoryName\LongSubDirectory\", longPath); + } + + /* + * Method: GetLongFileNameForRelativePathPreservesExtraSlashes + * + * Convert a short relative path with doubled embedded backslashes to a long path + * + */ + [TestMethod] + public void GetLongFileNameForRelativePathPreservesExtraSlashes() + { + string longPath = FileMatcher.GetLongPathName + ( + @"LONGDI~1\\LONGSU~1\\", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(@"LongDirectoryName\\LongSubDirectory\\", longPath); + } + + /* + * Method: GetLongFileNameForMixedLongAndShort + * + * Only part of the path might be short. + * + */ + [TestMethod] + public void GetLongFileNameForMixedLongAndShort() + { + string longPath = FileMatcher.GetLongPathName + ( + @"c:\apple\banana\tomato\pomegr~1\orange\", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(@"c:\apple\banana\tomato\pomegranate\orange\", longPath); + } + + /* + * Method: GetLongFileNameWherePartOfThePathDoesntExist + * + * Part of the path may not exist. In this case, we treat the non-existent parts + * as if they were already a long file name. + * + */ + [TestMethod] + public void GetLongFileNameWherePartOfThePathDoesntExist() + { + string longPath = FileMatcher.GetLongPathName + ( + @"c:\apple\banana\tomato\pomegr~1\orange\chocol~1\vanila~1", + new FileMatcher.GetFileSystemEntries(FileMatcherTest.GetFileSystemEntries) + ); + + Assert.AreEqual(@"c:\apple\banana\tomato\pomegranate\orange\chocol~1\vanila~1", longPath); + } + + [TestMethod] + public void BasicMatch() + { + ValidateFileMatch("file.txt", "File.txt", false); + ValidateNoFileMatch("file.txt", "File.bin", false); + } + + [TestMethod] + public void MatchSingleCharacter() + { + ValidateFileMatch("file.?xt", "File.txt", false); + ValidateNoFileMatch("file.?xt", "File.bin", false); + } + + [TestMethod] + public void MatchMultipleCharacters() + { + ValidateFileMatch("*.txt", "*.txt", false); + ValidateNoFileMatch("*.txt", "*.bin", false); + } + + [TestMethod] + public void SimpleRecursive() + { + ValidateFileMatch("**", ".\\File.txt", true); + } + + [TestMethod] + public void DotForCurrentDirectory() + { + ValidateFileMatch(".\\file.txt", ".\\File.txt", false); + ValidateNoFileMatch(".\\file.txt", ".\\File.bin", false); + } + + [TestMethod] + public void DotDotForParentDirectory() + { + ValidateFileMatch("..\\..\\*.*", "..\\..\\File.txt", false); + ValidateFileMatch("..\\..\\*.*", "..\\..\\File", false); + ValidateNoFileMatch("..\\..\\*.*", "..\\..\\dir1\\dir2\\File.txt", false); + ValidateNoFileMatch("..\\..\\*.*", "..\\..\\dir1\\dir2\\File", false); + } + + [TestMethod] + public void ReduceDoubleSlashesBaseline() + { + // Baseline + ValidateFileMatch("f:\\dir1\\dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("**\\*.cs", "dir1\\dir2\\file.cs", true); + ValidateFileMatch("**\\*.cs", "file.cs", true); + } + + + [TestMethod] + public void ReduceDoubleSlashes() + { + ValidateFileMatch("f:\\\\dir1\\dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\\\dir1\\\\\\dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\\\dir1\\\\\\dir2\\\\\\\\\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("..\\**/\\*.cs", "..\\dir1\\dir2\\file.cs", true); + ValidateFileMatch("..\\**/.\\*.cs", "..\\dir1\\dir2\\file.cs", true); + ValidateFileMatch("..\\**\\./.\\*.cs", "..\\dir1\\dir2\\file.cs", true); + } + + [TestMethod] + public void DoubleSlashesOnBothSidesOfComparison() + { + ValidateFileMatch("f:\\\\dir1\\dir2\\file.txt", "f:\\\\dir1\\dir2\\file.txt", false, false); + ValidateFileMatch("f:\\\\dir1\\\\\\dir2\\file.txt", "f:\\\\dir1\\\\\\dir2\\file.txt", false, false); + ValidateFileMatch("f:\\\\dir1\\\\\\dir2\\\\\\\\\\file.txt", "f:\\\\dir1\\\\\\dir2\\\\\\\\\\file.txt", false, false); + ValidateFileMatch("..\\**/\\*.cs", "..\\dir1\\dir2\\\\file.cs", true, false); + ValidateFileMatch("..\\**/.\\*.cs", "..\\dir1\\dir2//\\file.cs", true, false); + ValidateFileMatch("..\\**\\./.\\*.cs", "..\\dir1/\\/\\/dir2\\file.cs", true, false); + } + + [TestMethod] + public void DecomposeDotSlash() + { + ValidateFileMatch("f:\\.\\dir1\\dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\dir1\\.\\dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\dir1\\dir2\\.\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\.//dir1\\dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\dir1\\.//dir2\\file.txt", "f:\\dir1\\dir2\\file.txt", false); + ValidateFileMatch("f:\\dir1\\dir2\\.//file.txt", "f:\\dir1\\dir2\\file.txt", false); + + ValidateFileMatch(".\\dir1\\dir2\\file.txt", ".\\dir1\\dir2\\file.txt", false); + ValidateFileMatch(".\\.\\dir1\\dir2\\file.txt", ".\\dir1\\dir2\\file.txt", false); + ValidateFileMatch(".//dir1\\dir2\\file.txt", ".\\dir1\\dir2\\file.txt", false); + ValidateFileMatch(".//.//dir1\\dir2\\file.txt", ".\\dir1\\dir2\\file.txt", false); + } + + [TestMethod] + public void RecursiveDirRecursive() + { + // Check that a wildcardpath of **\x\**\ matches correctly since, \**\ is a + // separate code path. + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\x\file.txt", true); + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\y\x\file.txt", true); + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\x\y\file.txt", true); + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\y\x\y\file.txt", true); + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\x\x\file.txt", true); + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\x\x\file.txt", true); + ValidateFileMatch(@"c:\foo\**\x\**\*.*", @"c:\foo\x\x\x\file.txt", true); + } + + [TestMethod] + public void Regress155731() + { + ValidateFileMatch(@"a\b\**\**\**\**\**\e\*", @"a\b\c\d\e\f.txt", true); + ValidateFileMatch(@"a\b\**\e\*", @"a\b\c\d\e\f.txt", true); + ValidateFileMatch(@"a\b\**\**\e\*", @"a\b\c\d\e\f.txt", true); + ValidateFileMatch(@"a\b\**\**\**\e\*", @"a\b\c\d\e\f.txt", true); + ValidateFileMatch(@"a\b\**\**\**\**\e\*", @"a\b\c\d\e\f.txt", true); + } + + [TestMethod] + public void ParentWithoutSlash() + { + // However, we don't wtool this to match, + ValidateNoFileMatch(@"C:\foo\**", @"C:\foo", true); + // becase we don't know whether foo is a file or folder. + + // Same for UNC + ValidateNoFileMatch + ( + "\\\\server\\c$\\Documents and Settings\\User\\**", + "\\\\server\\c$\\Documents and Settings\\User", + true + ); + } + + [TestMethod] + public void Unc() + { + // Check UNC functionality + ValidateFileMatch + ( + "\\\\server\\c$\\**\\*.cs", + "\\\\server\\c$\\Documents and Settings\\User\\Source.cs", + true + ); + + ValidateNoFileMatch + ( + "\\\\server\\c$\\**\\*.cs", + "\\\\server\\c$\\Documents and Settings\\User\\Source.txt", + true + ); + ValidateFileMatch + ( + "\\\\**", + "\\\\server\\c$\\Documents and Settings\\User\\Source.cs", + true + ); + ValidateFileMatch + ( + "\\\\**\\*.*", + "\\\\server\\c$\\Documents and Settings\\User\\Source.cs", + true + ); + + + ValidateFileMatch + ( + "**", + "\\\\server\\c$\\Documents and Settings\\User\\Source.cs", + true + ); + } + + [TestMethod] + public void ExplicitToolCompatibility() + { + // Explicit ANT compatibility. These patterns taken from the ANT documentation. + ValidateFileMatch("**/SourceSafe/*", "./SourceSafe/Repository", true); + ValidateFileMatch("**\\SourceSafe/*", "./SourceSafe/Repository", true); + ValidateFileMatch("**/SourceSafe/*", ".\\SourceSafe\\Repository", true); + ValidateFileMatch("**/SourceSafe/*", "./org/IIS/SourceSafe/Entries", true); + ValidateFileMatch("**/SourceSafe/*", "./org/IIS/pluggin/tools/tool/SourceSafe/Entries", true); + ValidateNoFileMatch("**/SourceSafe/*", "./org/IIS/SourceSafe/foo/bar/Entries", true); + ValidateNoFileMatch("**/SourceSafe/*", "./SourceSafeRepository", true); + ValidateNoFileMatch("**/SourceSafe/*", "./aSourceSafe/Repository", true); + + ValidateFileMatch("org/IIS/pluggin/**", "org/IIS/pluggin/tools/tool/docs/index.html", true); + ValidateFileMatch("org/IIS/pluggin/**", "org/IIS/pluggin/test.xml", true); + ValidateFileMatch("org/IIS/pluggin/**", "org/IIS/pluggin\\test.xml", true); + ValidateNoFileMatch("org/IIS/pluggin/**", "org/IIS/abc.cs", true); + + ValidateFileMatch("org/IIS/**/SourceSafe/*", "org/IIS/SourceSafe/Entries", true); + ValidateFileMatch("org/IIS/**/SourceSafe/*", "org\\IIS/SourceSafe/Entries", true); + ValidateFileMatch("org/IIS/**/SourceSafe/*", "org/IIS\\SourceSafe/Entries", true); + ValidateFileMatch("org/IIS/**/SourceSafe/*", "org/IIS/pluggin/tools/tool/SourceSafe/Entries", true); + ValidateNoFileMatch("org/IIS/**/SourceSafe/*", "org/IIS/SourceSafe/foo/bar/Entries", true); + ValidateNoFileMatch("org/IIS/**/SourceSafe/*", "org/IISSourceSage/Entries", true); + } + + [TestMethod] + public void ExplicitToolIncompatibility() + { + // NOTE: Weirdly, ANT syntax is to match a file here. + // We don't because MSBuild philosophy is that a trailing slash indicates a directory + ValidateNoFileMatch("**/test/**", ".\\test", true); + + // NOTE: We deviate from ANT format here. ANT would append a ** to any path + // that ends with '/' or '\'. We think this is the wrong thing because 'folder\' + // is a valid folder name. + ValidateNoFileMatch("org/", "org/IISSourceSage/Entries", false); + ValidateNoFileMatch("org\\", "org/IISSourceSage/Entries", false); + } + + [TestMethod] + public void MultipleStarStar() + { + // Multiple-** matches + ValidateFileMatch("c:\\**\\user\\**\\*.*", "c:\\Documents and Settings\\user\\NTUSER.DAT", true); + ValidateNoFileMatch("c:\\**\\user1\\**\\*.*", "c:\\Documents and Settings\\user\\NTUSER.DAT", true); + ValidateFileMatch("c:\\**\\user\\**\\*.*", "c://Documents and Settings\\user\\NTUSER.DAT", true); + ValidateNoFileMatch("c:\\**\\user1\\**\\*.*", "c:\\Documents and Settings//user\\NTUSER.DAT", true); + } + + [TestMethod] + public void RegressItemRecursionWorksAsExpected() + { + // Regress bug#54411: Item recursion doesn't work as expected on "c:\foo\**" + ValidateFileMatch("c:\\foo\\**", "c:\\foo\\two\\subfile.txt", true); + } + + [TestMethod] + public void IllegalPaths() + { + // Certain patterns are illegal. + ValidateIllegal("**.cs"); + ValidateIllegal("***"); + ValidateIllegal("****"); + ValidateIllegal("*.cs**"); + ValidateIllegal("*.cs**"); + ValidateIllegal("...\\*.cs"); + ValidateIllegal("http://www.website.com"); + ValidateIllegal("<:tag:>"); + ValidateIllegal("<:\\**"); + } + + [TestMethod] + public void IllegalTooLongPath() + { + string longString = new string('X', 500) + "*"; // need a wildcard to do anything + string[] result = FileMatcher.GetFiles(@"c:\", longString); + + Assert.AreEqual(longString, result[0]); // Does not throw + + // Not checking that GetFileSpecInfo returns the illegal-path flag, + // not certain that won't break something; this fix is merely to avoid a crash. + } + + [TestMethod] + public void SplitFileSpec() + { + /************************************************************************************* + * Call ValidateSplitFileSpec with various supported combinations. + *************************************************************************************/ + ValidateSplitFileSpec("foo.cs", "", "", "foo.cs"); + ValidateSplitFileSpec("**\\foo.cs", "", "**\\", "foo.cs"); + ValidateSplitFileSpec("f:\\dir1\\**\\foo.cs", "f:\\dir1\\", "**\\", "foo.cs"); + ValidateSplitFileSpec("..\\**\\foo.cs", "..\\", "**\\", "foo.cs"); + ValidateSplitFileSpec("f:\\dir1\\foo.cs", "f:\\dir1\\", "", "foo.cs"); + ValidateSplitFileSpec("f:\\dir?\\foo.cs", "f:\\", "dir?\\", "foo.cs"); + ValidateSplitFileSpec("dir?\\foo.cs", "", "dir?\\", "foo.cs"); + ValidateSplitFileSpec(@"**\test\**", "", @"**\test\**\", "*.*"); + } + + [TestMethod] + public void Regress367780_CrashOnStarDotDot() + { + string workingPath = Path.Combine(Path.GetTempPath(), "Regress367780"); + string workingPathSubfolder = Path.Combine(workingPath, "SubDir"); + string offendingPattern = Path.Combine(workingPath, @"*\..\bar"); + string[] files = new string[0]; + + try + { + Directory.CreateDirectory(workingPath); + Directory.CreateDirectory(workingPathSubfolder); + + files = FileMatcher.GetFiles(workingPath, offendingPattern); + } + finally + { + Directory.Delete(workingPathSubfolder); + Directory.Delete(workingPath); + } + } + + [TestMethod] + public void Regress141071_StarStarSlashStarStarIsLiteral() + { + string workingPath = Path.Combine(Path.GetTempPath(), "Regress141071"); + string fileName = Path.Combine(workingPath, "MyFile.txt"); + string offendingPattern = Path.Combine(workingPath, @"**\**"); + + string[] files = new string[0]; + + try + { + Directory.CreateDirectory(workingPath); + File.WriteAllText(fileName, "Hello there."); + files = FileMatcher.GetFiles(workingPath, offendingPattern); + } + finally + { + File.Delete(fileName); + Directory.Delete(workingPath); + } + + string result = String.Join(", ", files); + Console.WriteLine(result); + Assert.IsFalse(result.Contains("**")); + Assert.IsTrue(result.Contains("MyFile.txt")); + } + + [TestMethod] + public void Regress14090_TrailingDotMatchesNoExtension() + { + string workingPath = Path.Combine(Path.GetTempPath(), "Regress141071"); + string workingPathSubdir = Path.Combine(workingPath, "subdir"); + string workingPathSubdirBing = Path.Combine(workingPathSubdir, "bing"); + + string offendingPattern = Path.Combine(workingPath, @"**\sub*\*."); + + string[] files = new string[0]; + + try + { + Directory.CreateDirectory(workingPath); + Directory.CreateDirectory(workingPathSubdir); + File.AppendAllText(workingPathSubdirBing, "y"); + files = FileMatcher.GetFiles(workingPath, offendingPattern); + } + finally + { + Directory.Delete(workingPath, true); + } + + string result = String.Join(", ", files); + Console.WriteLine(result); + Assert.AreEqual(1, files.Length); + } + + [TestMethod] + public void Regress14090_TrailingDotMatchesNoExtension_Part2() + { + ValidateFileMatch(@"c:\mydir\**\*.", @"c:\mydir\subdir\bing", true, /* simulate filesystem? */ false); + ValidateNoFileMatch(@"c:\mydir\**\*.", @"c:\mydir\subdir\bing.txt", true); + } + + [TestMethod] + public void RemoveProjectDirectory() + { + string[] strings = new string[1] { "c:\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "c:\\"); + Assert.AreEqual(strings[0], "1.file"); + + strings = new string[1] { "c:\\directory\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "c:\\"); + Assert.AreEqual(strings[0], "directory\\1.file"); + + strings = new string[1] { "c:\\directory\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "c:\\directory"); + Assert.AreEqual(strings[0], "1.file"); + + strings = new string[1] { "c:\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "c:\\directory"); + Assert.AreEqual(strings[0], "c:\\1.file"); + + strings = new string[1] { "c:\\directorymorechars\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "c:\\directory"); + Assert.AreEqual(strings[0], "c:\\directorymorechars\\1.file"); + + strings = new string[1] { "\\Machine\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "\\Machine"); + Assert.AreEqual(strings[0], "1.file"); + + strings = new string[1] { "\\Machine\\directory\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "\\Machine"); + Assert.AreEqual(strings[0], "directory\\1.file"); + + strings = new string[1] { "\\Machine\\directory\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "\\Machine\\directory"); + Assert.AreEqual(strings[0], "1.file"); + + strings = new string[1] { "\\Machine\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "\\Machine\\directory"); + Assert.AreEqual(strings[0], "\\Machine\\1.file"); + + strings = new string[1] { "\\Machine\\directorymorechars\\1.file" }; + FileMatcher.RemoveProjectDirectory(strings, "\\Machine\\directory"); + Assert.AreEqual(strings[0], "\\Machine\\directorymorechars\\1.file"); + } + + #region Support functions. + + /// + /// This support class simulates a file system. + /// It accepts multiple sets of files and keeps track of how many files were "hit" + /// In this case, "hit" means that the caller asked for that file directly. + /// + internal class MockFileSystem + { + /// + /// Array of files (set1) + /// + private string[] _fileSet1; + + /// + /// Array of files (set2) + /// + private string[] _fileSet2; + + /// + /// Array of files (set3) + /// + private string[] _fileSet3; + + /// + /// Number of times a file from set 1 was requested. + /// + private int _fileSet1Hits = 0; + + /// + /// Number of times a file from set 2 was requested. + /// + private int _fileSet2Hits = 0; + + /// + /// Number of times a file from set 3 was requested. + /// + private int _fileSet3Hits = 0; + + /// + /// Construct. + /// + /// First set of files. + /// Second set of files. + /// Third set of files. + internal MockFileSystem + ( + string[] fileSet1, + string[] fileSet2, + string[] fileSet3 + ) + { + _fileSet1 = fileSet1; + _fileSet2 = fileSet2; + _fileSet3 = fileSet3; + } + + /// + /// Number of times a file from set 1 was requested. + /// + internal int FileHits1 + { + get { return _fileSet1Hits; } + } + + /// + /// Number of times a file from set 2 was requested. + /// + internal int FileHits2 + { + get { return _fileSet2Hits; } + } + + /// + /// Number of times a file from set 3 was requested. + /// + internal int FileHits3 + { + get { return _fileSet3Hits; } + } + + + /// + /// Return files that match the given files. + /// + /// Candidate files. + /// The path to search within + /// The pattern to search for. + /// Hashtable receives the files. + /// + private int GetMatchingFiles(string[] candidates, string path, string pattern, Hashtable files) + { + int hits = 0; + if (candidates != null) + { + foreach (string candidate in candidates) + { + string normalizedCandidate = Normalize(candidate); + + // Get the candidate directory. + string candidateDirectoryName = ""; + if (normalizedCandidate.IndexOfAny(FileMatcher.directorySeparatorCharacters) != -1) + { + candidateDirectoryName = Path.GetDirectoryName(normalizedCandidate) + @"\"; + } + + // Does the candidate directory match the requested path? + if (String.Compare(path, candidateDirectoryName, StringComparison.OrdinalIgnoreCase) == 0) + { + // Match the basic *.* or null. These both match any file. + if + ( + pattern == null || + String.Compare(pattern, "*.*", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + ++hits; + files[normalizedCandidate] = String.Empty; + } + else if (pattern.Substring(0, 2) == "*.") // Match patterns like *.cs + { + string tail = pattern.Substring(1); + string candidateTail = candidate.Substring(candidate.Length - tail.Length); + if (String.Compare(tail, candidateTail, StringComparison.OrdinalIgnoreCase) == 0) + { + ++hits; + files[normalizedCandidate] = String.Empty; + } + } + else if (pattern.Substring(pattern.Length - 4, 2) == ".?") // Match patterns like foo.?xt + { + string leader = pattern.Substring(0, pattern.Length - 4); + string candidateLeader = candidate.Substring(candidate.Length - leader.Length - 4, leader.Length); + if (String.Compare(leader, candidateLeader, StringComparison.OrdinalIgnoreCase) == 0) + { + string tail = pattern.Substring(pattern.Length - 2); + string candidateTail = candidate.Substring(candidate.Length - 2); + if (String.Compare(tail, candidateTail, StringComparison.OrdinalIgnoreCase) == 0) + { + ++hits; + files[normalizedCandidate] = String.Empty; + } + } + } + else + { + Assert.Fail(String.Format("Unhandled case in GetMatchingFiles: {0}", pattern)); + } + } + } + } + + return hits; + } + + /// + /// Given a path and pattern, return all the simulated directories out of candidates. + /// + /// Candidate file to extract directories from. + /// The path to search. + /// The pattern to match. + /// Receives the directories. + private void GetMatchingDirectories(string[] candidates, string path, string pattern, Hashtable directories) + { + if (candidates != null) + { + foreach (string candidate in candidates) + { + string normalizedCandidate = Normalize(candidate); + + if (IsMatchingDirectory(path, normalizedCandidate)) + { + int nextSlash = normalizedCandidate.IndexOfAny(FileMatcher.directorySeparatorCharacters, path.Length + 1); + if (nextSlash != -1) + { + string match = normalizedCandidate.Substring(0, nextSlash + 1); + string baseMatch = Path.GetFileName(normalizedCandidate.Substring(0, nextSlash)); + if + ( + String.Compare(pattern, "*.*", StringComparison.OrdinalIgnoreCase) == 0 + || pattern == null + ) + { + directories[match] = String.Empty; + } + else if // Match patterns like ?emp + ( + pattern.Substring(0, 1) == "?" + && pattern.Length == baseMatch.Length + ) + { + string tail = pattern.Substring(1); + string baseMatchTail = baseMatch.Substring(1); + if (String.Compare(tail, baseMatchTail, StringComparison.OrdinalIgnoreCase) == 0) + { + directories[match] = String.Empty; + } + } + else + { + Assert.Fail(String.Format("Unhandled case in GetMatchingDirectories: {0}", pattern)); + } + } + } + } + } + } + + /// + /// Method that is delegable for use by FileMatcher. This method simulates a filesystem by returning + /// files and\or folders that match the requested path and pattern. + /// + /// Files, Directories or both + /// The path to search. + /// The pattern to search (may be null) + /// The matched files or folders. + internal string[] GetAccessibleFileSystemEntries(FileMatcher.FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) + { + string normalizedPath = Normalize(path); + + Hashtable files = new Hashtable(); + if (entityType == FileMatcher.FileSystemEntity.Files || entityType == FileMatcher.FileSystemEntity.FilesAndDirectories) + { + _fileSet1Hits += GetMatchingFiles(_fileSet1, normalizedPath, pattern, files); + _fileSet2Hits += GetMatchingFiles(_fileSet2, normalizedPath, pattern, files); + _fileSet3Hits += GetMatchingFiles(_fileSet3, normalizedPath, pattern, files); + } + + if (entityType == FileMatcher.FileSystemEntity.Directories || entityType == FileMatcher.FileSystemEntity.FilesAndDirectories) + { + GetMatchingDirectories(_fileSet1, normalizedPath, pattern, files); + GetMatchingDirectories(_fileSet2, normalizedPath, pattern, files); + GetMatchingDirectories(_fileSet3, normalizedPath, pattern, files); + } + ArrayList uniqueFiles = new ArrayList(); + uniqueFiles.AddRange(files.Keys); + + return (string[])uniqueFiles.ToArray(typeof(string)); + } + + /// + /// Given a path, fix it up so that it can be compared to another path. + /// + /// The path to fix up. + /// The normalized path. + internal static string Normalize(string path) + { + if (path.Length == 0) + { + return path; + } + + string normalized = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + // Replace leading UNC. + if (normalized.Substring(0, 2) == @"\\") + { + normalized = "<:UNC:>" + normalized.Substring(2); + } + + // Preserve parent-directory markers. + normalized = normalized.Replace(@"..\", "<:PARENT:>"); + + + // Just get rid of doubles enough to satisfy our test cases. + normalized = normalized.Replace(@"\\", @"\"); + normalized = normalized.Replace(@"\\", @"\"); + normalized = normalized.Replace(@"\\", @"\"); + + // Strip any .\ + normalized = normalized.Replace(@".\", ""); + + // Put back the preserved markers. + normalized = normalized.Replace("<:UNC:>", @"\\"); + normalized = normalized.Replace("<:PARENT:>", @"..\"); + + return normalized; + } + + /// + /// Determines whether candidate is in a subfolder of path. + /// + /// + /// + /// True if there is a match. + private bool IsMatchingDirectory(string path, string candidate) + { + string normalizedPath = Normalize(path); + string normalizedCandidate = Normalize(candidate); + + // Current directory always matches for non-rooted paths. + if (path.Length == 0 && !Path.IsPathRooted(candidate)) + { + return true; + } + + if (normalizedCandidate.Length > normalizedPath.Length) + { + if (String.Compare(normalizedPath, 0, normalizedCandidate, 0, normalizedPath.Length, StringComparison.OrdinalIgnoreCase) == 0) + { + if (FileUtilities.EndsWithSlash(normalizedPath)) + { + return true; + } + else if (FileUtilities.IsSlash(normalizedCandidate[normalizedPath.Length])) + { + return true; + } + } + } + + return false; + } + + + /// + /// Searches the candidates array for one that matches path + /// + /// + /// + /// The index of the first match or negative one. + private int IndexOfFirstMatchingDirectory(string path, string[] candidates) + { + if (candidates != null) + { + int i = 0; + foreach (string candidate in candidates) + { + if (IsMatchingDirectory(path, candidate)) + { + return i; + } + + ++i; + } + } + + return -1; + } + + /// + /// Delegable method that returns true if the given directory exists in this simulated filesystem + /// + /// The path to check. + /// True if the directory exists. + internal bool DirectoryExists(string path) + { + if (IndexOfFirstMatchingDirectory(path, _fileSet1) != -1) + { + return true; + } + + if (IndexOfFirstMatchingDirectory(path, _fileSet2) != -1) + { + return true; + } + + if (IndexOfFirstMatchingDirectory(path, _fileSet3) != -1) + { + return true; + } + + return false; + } + } + + /// + /// A general purpose method used to: + /// + /// (1) Simulate a file system. + /// (2) Check whether all matchingFiles where hit by the filespec pattern. + /// (3) Check whether all nonmatchingFiles were *not* hit by the filespec pattern. + /// (4) Check whether all untouchableFiles were not even requested (usually for perf reasons). + /// + /// These can be used in various combinations to test the filematcher framework. + /// + /// A FileMatcher filespec, possibly with wildcards. + /// Files that exist and should be matched. + /// Files that exists and should not be matched. + /// Files that exist but should not be requested. + private static void MatchDriver + ( + string filespec, + string[] matchingFiles, + string[] nonmatchingFiles, + string[] untouchableFiles + ) + { + MockFileSystem mockFileSystem = new MockFileSystem(matchingFiles, nonmatchingFiles, untouchableFiles); + string[] files = FileMatcher.GetFiles + ( + String.Empty, /* we don't need project directory as we use mock filesystem */ + filespec, + new FileMatcher.GetFileSystemEntries(mockFileSystem.GetAccessibleFileSystemEntries), + new DirectoryExists(mockFileSystem.DirectoryExists) + ); + + // Validate the matching files. + if (matchingFiles != null) + { + foreach (string matchingFile in matchingFiles) + { + int timesFound = 0; + foreach (string file in files) + { + string normalizedFile = MockFileSystem.Normalize(file); + string normalizedMatchingFile = MockFileSystem.Normalize(matchingFile); + if (String.Compare(normalizedFile, normalizedMatchingFile, StringComparison.OrdinalIgnoreCase) == 0) + { + ++timesFound; + } + } + Assert.IsTrue(timesFound == 1, String.Format("Expected to find matching file '{0}' exactly one times. Found it '{1}' times instead.", matchingFile, timesFound)); + } + } + + + // Validate the non-matching files + if (nonmatchingFiles != null) + { + foreach (string nonmatchingFile in nonmatchingFiles) + { + int timesFound = 0; + foreach (string file in files) + { + string normalizedFile = MockFileSystem.Normalize(file); + string normalizedNonmatchingFile = MockFileSystem.Normalize(nonmatchingFile); + if (String.Compare(normalizedFile, normalizedNonmatchingFile, StringComparison.OrdinalIgnoreCase) == 0) + { + ++timesFound; + } + } + Assert.IsTrue(timesFound == 0, String.Format("Expected not to match file '{0}' but did.", nonmatchingFile)); + } + } + + // Check untouchable files. + Assert.IsTrue(mockFileSystem.FileHits3 == 0, "At least one file that was marked untouchable was referenced."); + } + + + + /// + /// Simulate GetFileSystemEntries + /// + /// + /// + /// Array of matching file system entries (can be empty). + private static string[] GetFileSystemEntriesLoopBack(FileMatcher.FileSystemEntity entityType, string path, string pattern, string projectDirectory, bool stripProjectDirectory) + { + return new string[] { Path.Combine(path, pattern) }; + } + + /************************************************************************************* + * Validate that SplitFileSpec(...) is returning the expected constituent values. + *************************************************************************************/ + + private static void ValidateSplitFileSpec + ( + string filespec, + string expectedFixedDirectoryPart, + string expectedWildcardDirectoryPart, + string expectedFilenamePart + ) + { + string fixedDirectoryPart; + string wildcardDirectoryPart; + string filenamePart; + FileMatcher.SplitFileSpec + ( + filespec, + out fixedDirectoryPart, + out wildcardDirectoryPart, + out filenamePart, + new FileMatcher.GetFileSystemEntries(GetFileSystemEntriesLoopBack) + ); + + if + ( + expectedWildcardDirectoryPart != wildcardDirectoryPart + || expectedFixedDirectoryPart != fixedDirectoryPart + || expectedFilenamePart != filenamePart + ) + { + Console.WriteLine("Expect Fixed '{0}' got '{1}'", expectedFixedDirectoryPart, fixedDirectoryPart); + Console.WriteLine("Expect Wildcard '{0}' got '{1}'", expectedWildcardDirectoryPart, wildcardDirectoryPart); + Console.WriteLine("Expect Filename '{0}' got '{1}'", expectedFilenamePart, filenamePart); + Assert.Fail("FileMatcher Regression: Failure while validating SplitFileSpec."); + } + } + + /************************************************************************************* + * Given a pattern (filespec) and a candidate filename (fileToMatch). Verify that they + * do indeed match. + *************************************************************************************/ + private static void ValidateFileMatch + ( + string filespec, + string fileToMatch, + bool shouldBeRecursive + ) + { + ValidateFileMatch(filespec, fileToMatch, shouldBeRecursive, /* Simulate filesystem? */ true); + } + + /************************************************************************************* + * Given a pattern (filespec) and a candidate filename (fileToMatch). Verify that they + * do indeed match. + *************************************************************************************/ + private static void ValidateFileMatch + ( + string filespec, + string fileToMatch, + bool shouldBeRecursive, + bool fileSystemSimulation + ) + { + if (!IsFileMatchAssertIfIllegal(filespec, fileToMatch, shouldBeRecursive)) + { + Assert.Fail("FileMatcher Regression: Failure while validating that files match."); + } + + // Now, simulate a filesystem with only fileToMatch. Make sure the file exists that way. + if (fileSystemSimulation) + { + MatchDriver + ( + filespec, + new string[] { fileToMatch }, + null, + null + ); + } + } + + /************************************************************************************* + * Given a pattern (filespec) and a candidate filename (fileToMatch). Verify that they + * DON'T match. + *************************************************************************************/ + private static void ValidateNoFileMatch + ( + string filespec, + string fileToMatch, + bool shouldBeRecursive + ) + { + if (IsFileMatchAssertIfIllegal(filespec, fileToMatch, shouldBeRecursive)) + { + Assert.Fail("FileMatcher Regression: Failure while validating that files don't match."); + } + + // Now, simulate a filesystem with only fileToMatch. Make sure the file doesn't exist that way. + MatchDriver + ( + filespec, + null, + new string[] { fileToMatch }, + null + ); + } + + /************************************************************************************* + * Verify that the given filespec is illegal. + *************************************************************************************/ + private static void ValidateIllegal + ( + string filespec + ) + { + Regex regexFileMatch; + bool needsRecursion; + bool isLegalFileSpec; + FileMatcher.GetFileSpecInfo + ( + filespec, + out regexFileMatch, + out needsRecursion, + out isLegalFileSpec, + new FileMatcher.GetFileSystemEntries(GetFileSystemEntriesLoopBack) + ); + + if (isLegalFileSpec) + { + Assert.Fail("FileMatcher Regression: Expected an illegal filespec, but got a legal one."); + } + + // Now, FileMatcher is supposed to take any legal file name and just return it immediately. + // Let's see if it does. + MatchDriver + ( + filespec, // Not legal. + new string[] { filespec }, // Should match + null, + null + ); + } + /************************************************************************************* + * Given a pattern (filespec) and a candidate filename (fileToMatch) return true if + * FileMatcher would say that they match. + *************************************************************************************/ + private static bool IsFileMatchAssertIfIllegal + ( + string filespec, + string fileToMatch, + bool shouldBeRecursive + ) + { + FileMatcher.Result match = FileMatcher.FileMatch(filespec, fileToMatch); + + if (!match.isLegalFileSpec) + { + Console.WriteLine("Checking FileSpec: '{0}' against '{1}'", filespec, fileToMatch); + Assert.Fail("FileMatcher Regression: Invalid filespec."); + } + if (shouldBeRecursive != match.isFileSpecRecursive) + { + Console.WriteLine("Checking FileSpec: '{0}' against '{1}'", filespec, fileToMatch); + Assert.IsTrue(shouldBeRecursive, "FileMatcher Regression: Match was recursive when it shouldn't be."); + Assert.IsFalse(shouldBeRecursive, "FileMatcher Regression: Match was not recursive when it should have been."); + } + return match.isMatch; + } + + #endregion + } +} + + + + + diff --git a/src/Shared/UnitTests/FileUtilities_Tests.cs b/src/Shared/UnitTests/FileUtilities_Tests.cs new file mode 100644 index 00000000000..c641e120b8e --- /dev/null +++ b/src/Shared/UnitTests/FileUtilities_Tests.cs @@ -0,0 +1,732 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Text; + +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using System.IO; +using System.Collections.Generic; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class FileUtilities_Tests + { + /// + /// Exercises FileUtilities.ItemSpecModifiers.GetItemSpecModifier + /// + [TestMethod] + public void GetItemSpecModifier() + { + TestGetItemSpecModifier(Environment.CurrentDirectory); + TestGetItemSpecModifier(null); + } + + private static void TestGetItemSpecModifier(string currentDirectory) + { + string cache = null; + string modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, "foo", String.Empty, FileUtilities.ItemSpecModifiers.RecursiveDir, ref cache); + Assert.AreEqual(String.Empty, modifier); + + cache = null; + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, "foo", String.Empty, FileUtilities.ItemSpecModifiers.ModifiedTime, ref cache); + Assert.AreEqual(String.Empty, modifier); + + cache = null; + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"foo\goo", String.Empty, FileUtilities.ItemSpecModifiers.RelativeDir, ref cache); + Assert.AreEqual(@"foo\", modifier); + + // confirm we get the same thing back the second time + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"foo\goo", String.Empty, FileUtilities.ItemSpecModifiers.RelativeDir, ref cache); + Assert.AreEqual(@"foo\", modifier); + + cache = null; + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", String.Empty, FileUtilities.ItemSpecModifiers.FullPath, ref cache); + Assert.AreEqual(@"c:\foo.txt", modifier); + Assert.AreEqual(@"c:\foo.txt", cache); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", String.Empty, FileUtilities.ItemSpecModifiers.RootDir, ref cache); + Assert.AreEqual(@"c:\", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", String.Empty, FileUtilities.ItemSpecModifiers.Filename, ref cache); + Assert.AreEqual(@"foo", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", String.Empty, FileUtilities.ItemSpecModifiers.Extension, ref cache); + Assert.AreEqual(@".txt", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", String.Empty, FileUtilities.ItemSpecModifiers.Directory, ref cache); + Assert.AreEqual(String.Empty, modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", String.Empty, FileUtilities.ItemSpecModifiers.Identity, ref cache); + Assert.AreEqual(@"c:\foo.txt", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", @"c:\abc\goo.proj", FileUtilities.ItemSpecModifiers.DefiningProjectDirectory, ref cache); + Assert.AreEqual(@"c:\abc\", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", @"c:\abc\goo.proj", FileUtilities.ItemSpecModifiers.DefiningProjectExtension, ref cache); + Assert.AreEqual(@".proj", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", @"c:\abc\goo.proj", FileUtilities.ItemSpecModifiers.DefiningProjectFullPath, ref cache); + Assert.AreEqual(@"c:\abc\goo.proj", modifier); + + modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"c:\foo.txt", @"c:\abc\goo.proj", FileUtilities.ItemSpecModifiers.DefiningProjectName, ref cache); + Assert.AreEqual(@"goo", modifier); + } + + [TestMethod] + public void MakeRelativeTests() + { + Assert.AreEqual(@"foo.cpp", FileUtilities.MakeRelative(@"c:\abc\def", @"c:\abc\def\foo.cpp")); + Assert.AreEqual(@"def\foo.cpp", FileUtilities.MakeRelative(@"c:\abc\", @"c:\abc\def\foo.cpp")); + Assert.AreEqual(@"..\foo.cpp", FileUtilities.MakeRelative(@"c:\abc\def\xyz", @"c:\abc\def\foo.cpp")); + Assert.AreEqual(@"..\ttt\foo.cpp", FileUtilities.MakeRelative(@"c:\abc\def\xyz\", @"c:\abc\def\ttt\foo.cpp")); + Assert.AreEqual(@"e:\abc\def\foo.cpp", FileUtilities.MakeRelative(@"c:\abc\def", @"e:\abc\def\foo.cpp")); + Assert.AreEqual(@"foo.cpp", FileUtilities.MakeRelative(@"\\aaa\abc\def", @"\\aaa\abc\def\foo.cpp")); + Assert.AreEqual(@"foo.cpp", FileUtilities.MakeRelative(@"c:\abc\def", @"foo.cpp")); + Assert.AreEqual(@"foo.cpp", FileUtilities.MakeRelative(@"c:\abc\def", @"..\def\foo.cpp")); + Assert.AreEqual(@"\\host\path\file", FileUtilities.MakeRelative(@"c:\abc\def", @"\\host\path\file")); + Assert.AreEqual(@"\\host\d$\file", FileUtilities.MakeRelative(@"c:\abc\def", @"\\host\d$\file")); + } + + /// + /// Exercises FileUtilities.ItemSpecModifiers.GetItemSpecModifier on a bad path. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void GetItemSpecModifierOnBadPath() + { + TestGetItemSpecModifierOnBadPath(Environment.CurrentDirectory); + } + + /// + /// Exercises FileUtilities.ItemSpecModifiers.GetItemSpecModifier on a bad path. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void GetItemSpecModifierOnBadPath2() + { + TestGetItemSpecModifierOnBadPath(null); + } + + private static void TestGetItemSpecModifierOnBadPath(string currentDirectory) + { + try + { + string cache = null; + string modifier = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, @"http://www.microsoft.com", String.Empty, FileUtilities.ItemSpecModifiers.RootDir, ref cache); + } + catch (Exception e) + { + // so I can see the exception message in NUnit's "Standard Out" window + Console.WriteLine(e.Message); + throw; + } + } + + [TestMethod] + public void GetFileInfoNoThrowBasic() + { + string file = null; + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = FileUtilities.GetFileInfoNoThrow(file); + Assert.IsTrue(info.LastWriteTime == new FileInfo(file).LastWriteTime); + } + finally + { + if (file != null) File.Delete(file); + } + } + + [TestMethod] + public void GetFileInfoNoThrowNonexistent() + { + FileInfo info = FileUtilities.GetFileInfoNoThrow("this_file_is_nonexistent"); + Assert.IsTrue(info == null); + } + + /// + /// Exercises FileUtilities.EndsWithSlash + /// + [TestMethod] + public void EndsWithSlash() + { + Assert.IsTrue(FileUtilities.EndsWithSlash(@"C:\foo\")); + Assert.IsTrue(FileUtilities.EndsWithSlash(@"C:\")); + Assert.IsTrue(FileUtilities.EndsWithSlash(@"\")); + + Assert.IsTrue(FileUtilities.EndsWithSlash(@"http://www.microsoft.com/")); + Assert.IsTrue(FileUtilities.EndsWithSlash(@"//server/share/")); + Assert.IsTrue(FileUtilities.EndsWithSlash(@"/")); + + Assert.IsFalse(FileUtilities.EndsWithSlash(@"C:\foo")); + Assert.IsFalse(FileUtilities.EndsWithSlash(@"C:")); + Assert.IsFalse(FileUtilities.EndsWithSlash(@"foo")); + + // confirm that empty string doesn't barf + Assert.IsFalse(FileUtilities.EndsWithSlash(String.Empty)); + } + + /// + /// Exercises FileUtilities.GetDirectory + /// + [TestMethod] + public void GetDirectoryWithTrailingSlash() + { + Assert.AreEqual(@"c:\", FileUtilities.GetDirectory(@"c:\")); + Assert.AreEqual(@"c:\", FileUtilities.GetDirectory(@"c:\foo")); + Assert.AreEqual(@"c:", FileUtilities.GetDirectory(@"c:")); + Assert.AreEqual(@"\", FileUtilities.GetDirectory(@"\")); + Assert.AreEqual(@"\", FileUtilities.GetDirectory(@"\foo")); + Assert.AreEqual(@"..\", FileUtilities.GetDirectory(@"..\foo")); + Assert.AreEqual(@"\foo\", FileUtilities.GetDirectory(@"\foo\")); + Assert.AreEqual(@"\\server\share", FileUtilities.GetDirectory(@"\\server\share")); + Assert.AreEqual(@"\\server\share\", FileUtilities.GetDirectory(@"\\server\share\")); + Assert.AreEqual(@"\\server\share\", FileUtilities.GetDirectory(@"\\server\share\file")); + Assert.AreEqual(@"\\server\share\directory\", FileUtilities.GetDirectory(@"\\server\share\directory\")); + Assert.AreEqual(@"foo\", FileUtilities.GetDirectory(@"foo\bar")); + Assert.AreEqual(@"\foo\bar\", FileUtilities.GetDirectory(@"\foo\bar\")); + Assert.AreEqual(String.Empty, FileUtilities.GetDirectory("foo")); + } + + /// + /// Exercises FileUtilities.HasExtension + /// + [TestMethod] + public void HasExtension() + { + Assert.IsTrue(FileUtilities.HasExtension("foo.txt", new string[] { ".EXE", ".TXT" }), "test 1"); + Assert.IsFalse(FileUtilities.HasExtension("foo.txt", new string[] { ".EXE", ".DLL" }), "test 2"); + } + + /// + /// Exercises FileUtilities.EnsureTrailingSlash + /// + [TestMethod] + public void EnsureTrailingSlash() + { + // Doesn't have a trailing slash to start with. + Assert.AreEqual(@"foo\bar\", FileUtilities.EnsureTrailingSlash(@"foo\bar"), "test 1"); + Assert.AreEqual(@"foo/bar\", FileUtilities.EnsureTrailingSlash(@"foo/bar"), "test 2"); + + // Already has a trailing slash to start with. + Assert.AreEqual(@"foo/bar/", FileUtilities.EnsureTrailingSlash(@"foo/bar/"), "test 3"); + Assert.AreEqual(@"foo\bar\", FileUtilities.EnsureTrailingSlash(@"foo\bar\"), "test 4"); + Assert.AreEqual(@"foo/bar\", FileUtilities.EnsureTrailingSlash(@"foo/bar\"), "test 5"); + Assert.AreEqual(@"foo\bar/", FileUtilities.EnsureTrailingSlash(@"foo\bar/"), "test 5"); + } + + /// + /// Exercises FileUtilities.ItemSpecModifiers.IsItemSpecModifier + /// + [TestMethod] + public void IsItemSpecModifier() + { + // Positive matches using exact case. + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("FullPath"), "test 1"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("RootDir"), "test 2"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Filename"), "test 3"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Extension"), "test 4"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("RelativeDir"), "test 5"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Directory"), "test 6"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("RecursiveDir"), "test 7"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Identity"), "test 8"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("ModifiedTime"), "test 9"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("CreatedTime"), "test 10"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("AccessedTime"), "test 11"); + + // Positive matches using different case. + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("fullPath"), "test 21"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("rootDir"), "test 22"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("filename"), "test 23"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("extension"), "test 24"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("relativeDir"), "test 25"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("directory"), "test 26"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("recursiveDir"), "test 27"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("identity"), "test 28"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("modifiedTime"), "test 29"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("createdTime"), "test 30"); + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("accessedTime"), "test 31"); + + // Negative tests to get maximum code coverage inside the many many different branches + // of FileUtilities.ItemSpecModifiers.IsItemSpecModifier. + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("rootxxx"), "test 41"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Rootxxx"), "test 42"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("xxxxxxx"), "test 43"); + + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("filexxxx"), "test 44"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Filexxxx"), "test 45"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("idenxxxx"), "test 46"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Idenxxxx"), "test 47"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("xxxxxxxx"), "test 48"); + + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("extenxxxx"), "test 49"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Extenxxxx"), "test 50"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("direcxxxx"), "test 51"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Direcxxxx"), "test 52"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("xxxxxxxxx"), "test 53"); + + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("xxxxxxxxxx"), "test 54"); + + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("relativexxx"), "test 55"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Relativexxx"), "test 56"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("createdxxxx"), "test 57"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Createdxxxx"), "test 58"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("xxxxxxxxxxx"), "test 59"); + + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("recursivexxx"), "test 60"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Recursivexxx"), "test 61"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("accessedxxxx"), "test 62"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Accessedxxxx"), "test 63"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("modifiedxxxx"), "test 64"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("Modifiedxxxx"), "test 65"); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier("xxxxxxxxxxxx"), "test 66"); + + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsItemSpecModifier(null), "test 67"); + } + + [TestMethod] + public void CheckDerivableItemSpecModifiers() + { + Assert.IsTrue(FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier("Filename")); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier("RecursiveDir")); + Assert.IsFalse(FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier("recursivedir")); + } + + [TestMethod] + public void GetExecutablePath() + { + string path = Path.Combine(Environment.CurrentDirectory, "msbuild.exe").ToLowerInvariant(); + + string configPath = FileUtilities.CurrentExecutableConfigurationFilePath.ToLowerInvariant(); + string directoryName = FileUtilities.CurrentExecutableDirectory.ToLowerInvariant(); + string executablePath = FileUtilities.CurrentExecutablePath.ToLowerInvariant(); + + Assert.AreEqual(configPath, executablePath + ".config"); + Assert.AreEqual(path, executablePath); + Assert.AreEqual(directoryName, Path.GetDirectoryName(path)); + } + + [TestMethod] + public void NormalizePathThatFitsIntoMaxPath() + { + string currentDirectory = @"c:\aardvark\aardvark\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890"; + string filePath = @"..\..\..\..\..\..\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\a.cs"; + string fullPath = @"c:\aardvark\aardvark\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\a.cs"; + + Assert.AreEqual(fullPath, FileUtilities.NormalizePath(Path.Combine(currentDirectory, filePath))); + } + + [TestMethod] + [ExpectedException(typeof(PathTooLongException))] + public void NormalizePathThatDoesntFitIntoMaxPath() + { + string currentDirectory = @"c:\aardvark\aardvark\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890"; + string filePath = @"..\..\..\..\..\..\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\a.cs"; + + // This path ends up over 420 characters long + string fullPath = @"c:\aardvark\aardvark\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\a.cs"; + + Assert.AreEqual(fullPath, FileUtilities.NormalizePath(Path.Combine(currentDirectory, filePath))); + } + + [TestMethod] + public void GetItemSpecModifierRootDirThatFitsIntoMaxPath() + { + string currentDirectory = @"c:\aardvark\aardvark\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890"; + string fullPath = @"c:\aardvark\aardvark\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\1234567890\a.cs"; + string cache = fullPath; + + Assert.AreEqual(@"c:\", FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, fullPath, String.Empty, FileUtilities.ItemSpecModifiers.RootDir, ref cache)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NormalizePathNull() + { + Assert.AreEqual(null, FileUtilities.NormalizePath(null)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void NormalizePathEmpty() + { + Assert.AreEqual(null, FileUtilities.NormalizePath(String.Empty)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void NormalizePathBadUNC1() + { + Assert.AreEqual(null, FileUtilities.NormalizePath(@"\\")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void NormalizePathBadUNC2() + { + Assert.AreEqual(null, FileUtilities.NormalizePath(@"\\XXX\")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void NormalizePathBadUNC3() + { + Assert.AreEqual(@"\\localhost", FileUtilities.NormalizePath(@"\\localhost")); + } + + [TestMethod] + public void NormalizePathGoodUNC() + { + Assert.AreEqual(@"\\localhost\share", FileUtilities.NormalizePath(@"\\localhost\share")); + } + + [TestMethod] + public void NormalizePathTooLongWithDots() + { + string longPart = new string('x', 300); + Assert.AreEqual(@"c:\abc\def", FileUtilities.NormalizePath(@"c:\abc\" + longPart + @"\..\def")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void NormalizePathBadGlobalroot() + { + /* + From Path.cs + // Check for \\?\Globalroot, an internal mechanism to the kernel + // that provides aliases for drives and other undocumented stuff. + // The kernel team won't even describe the full set of what + // is available here - we don't want managed apps mucking + // with this for security reasons. + * */ + Assert.AreEqual(null, FileUtilities.NormalizePath(@"\\?\globalroot\XXX")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void NormalizePathInvalid() + { + string filePath = @"c:\aardvark\|||"; + Assert.AreEqual(null, FileUtilities.NormalizePath(filePath)); + } + + [TestMethod] + public void FileOrDirectoryExistsNoThrow() + { + Assert.AreEqual(false, FileUtilities.FileOrDirectoryExistsNoThrow("||")); + Assert.AreEqual(false, FileUtilities.FileOrDirectoryExistsNoThrow("c:\\doesnot_exist")); + Assert.AreEqual(true, FileUtilities.FileOrDirectoryExistsNoThrow("c:\\")); + Assert.AreEqual(true, FileUtilities.FileOrDirectoryExistsNoThrow(Path.GetTempPath())); + + string path = null; + + try + { + path = FileUtilities.GetTemporaryFile(); + Assert.AreEqual(true, FileUtilities.FileOrDirectoryExistsNoThrow(path)); + } + finally + { + File.Delete(path); + } + } + + [TestMethod] + public void FileOrDirectoryExistsNoThrowTooLongWithDots() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3)).Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = Environment.SystemDirectory + @"\" + longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3); + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + Assert.AreEqual(true, FileUtilities.FileOrDirectoryExistsNoThrow(inputPath)); + Assert.AreEqual(false, FileUtilities.FileOrDirectoryExistsNoThrow(inputPath.Replace('\\', 'X'))); + } + + [TestMethod] + public void FileOrDirectoryExistsNoThrowTooLongWithDotsRelative() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3)).Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3); + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + + string currentDirectory = Environment.CurrentDirectory; + + try + { + currentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = Environment.SystemDirectory; + + Assert.AreEqual(true, FileUtilities.FileOrDirectoryExistsNoThrow(inputPath)); + Assert.AreEqual(false, FileUtilities.FileOrDirectoryExistsNoThrow(inputPath.Replace('\\', 'X'))); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + + [TestMethod] + public void DirectoryExistsNoThrowTooLongWithDots() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3)).Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = Environment.SystemDirectory + @"\" + longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3); + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + Assert.AreEqual(true, FileUtilities.DirectoryExistsNoThrow(inputPath)); + } + + [TestMethod] + public void DirectoryExistsNoThrowTooLongWithDotsRelative() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3)).Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3); + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + + string currentDirectory = Environment.CurrentDirectory; + + try + { + currentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = Environment.SystemDirectory; + + Assert.AreEqual(true, FileUtilities.DirectoryExistsNoThrow(inputPath)); + Assert.AreEqual(false, FileUtilities.DirectoryExistsNoThrow(inputPath.Replace('\\', 'X'))); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + + [TestMethod] + public void FileExistsNoThrowTooLongWithDots() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe").Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = Environment.SystemDirectory + @"\" + longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe"; + Console.WriteLine(inputPath.Length); + Console.WriteLine(inputPath); + + // "c:\windows\system32\\..\..\windows\system32" exists + Assert.AreEqual(true, FileUtilities.FileExistsNoThrow(inputPath)); + } + + [TestMethod] + public void FileExistsNoThrowTooLongWithDotsRelative() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe").Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe"; + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + + string currentDirectory = Environment.CurrentDirectory; + + try + { + currentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = Environment.SystemDirectory; + + Assert.AreEqual(true, FileUtilities.FileExistsNoThrow(inputPath)); + Assert.AreEqual(false, FileUtilities.FileExistsNoThrow(inputPath.Replace('\\', 'X'))); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + + [TestMethod] + public void GetFileInfoNoThrowTooLongWithDots() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe").Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = Environment.SystemDirectory + @"\" + longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe"; + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + Assert.AreEqual(true, FileUtilities.GetFileInfoNoThrow(inputPath) != null); + Assert.AreEqual(false, FileUtilities.GetFileInfoNoThrow(inputPath.Replace('\\', 'X')) != null); + } + + [TestMethod] + public void GetFileInfoNoThrowTooLongWithDotsRelative() + { + int length = (Environment.SystemDirectory + @"\" + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe").Length; + + string longPart = new string('x', 260 - length); // We want the shortest that is > max path. + + string inputPath = longPart + @"\..\..\..\" + Environment.SystemDirectory.Substring(3) + @"\..\explorer.exe"; + Console.WriteLine(inputPath.Length); + + // "c:\windows\system32\\..\..\windows\system32" exists + + string currentDirectory = Environment.CurrentDirectory; + + try + { + currentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = Environment.SystemDirectory; + + Assert.AreEqual(true, FileUtilities.GetFileInfoNoThrow(inputPath) != null); + Assert.AreEqual(false, FileUtilities.GetFileInfoNoThrow(inputPath.Replace('\\', 'X')) != null); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + + /// + /// Simple test, neither the base file nor retry files exist + /// + [TestMethod] + public void GenerateTempFileNameSimple() + { + string path = null; + + try + { + path = FileUtilities.GetTemporaryFile(); + + Assert.AreEqual(true, path.EndsWith(".tmp")); + Assert.AreEqual(true, File.Exists(path)); + Assert.AreEqual(true, path.StartsWith(Path.GetTempPath())); + } + finally + { + File.Delete(path); + } + } + + /// + /// Choose an extension + /// + [TestMethod] + public void GenerateTempFileNameWithExtension() + { + string path = null; + + try + { + path = Shared.FileUtilities.GetTemporaryFile(".bat"); + + Assert.AreEqual(true, path.EndsWith(".bat")); + Assert.AreEqual(true, File.Exists(path)); + Assert.AreEqual(true, path.StartsWith(Path.GetTempPath())); + } + finally + { + File.Delete(path); + } + } + + /// + /// Choose a (missing) directory and extension + /// + [TestMethod] + public void GenerateTempFileNameWithDirectoryAndExtension() + { + string path = null; + string directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "subfolder"); + + try + { + path = Shared.FileUtilities.GetTemporaryFile(directory, ".bat"); + + Assert.AreEqual(true, path.EndsWith(".bat")); + Assert.AreEqual(true, File.Exists(path)); + Assert.AreEqual(true, path.StartsWith(directory)); + } + finally + { + File.Delete(path); + Directory.Delete(directory); + } + } + + /// + /// Extension without a period + /// + [TestMethod] + public void GenerateTempFileNameWithExtensionNoPeriod() + { + string path = null; + + try + { + path = Shared.FileUtilities.GetTemporaryFile("bat"); + + Assert.AreEqual(true, path.EndsWith(".bat")); + Assert.AreEqual(true, File.Exists(path)); + Assert.AreEqual(true, path.StartsWith(Path.GetTempPath())); + } + finally + { + File.Delete(path); + } + } + + /// + /// Extension is invalid + /// + [TestMethod] + [ExpectedException(typeof(IOException))] + public void GenerateTempBatchFileWithBadExtension() + { + Shared.FileUtilities.GetTemporaryFile("|"); + } + + /// + /// No extension is given + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void GenerateTempBatchFileWithEmptyExtension() + { + Shared.FileUtilities.GetTemporaryFile(String.Empty); + } + + /// + /// Directory is invalid + /// + [TestMethod] + [ExpectedException(typeof(IOException))] + public void GenerateTempBatchFileWithBadDirectory() + { + Shared.FileUtilities.GetTemporaryFile("|", ".tmp"); + } + } +} diff --git a/src/Shared/UnitTests/HybridDictionary_Tests.cs b/src/Shared/UnitTests/HybridDictionary_Tests.cs new file mode 100644 index 00000000000..f9e515888ad --- /dev/null +++ b/src/Shared/UnitTests/HybridDictionary_Tests.cs @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests of a dictionary which changes its backing store to keep memory use low. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; +using System.Collections; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for the HybridDictionary. Most of the more interesting tests are handled by the CopyOnWriteDictionary tests + /// which use this as a backing store. + /// + [TestClass] + public class HybridDictionary_Tests + { + /// + /// Tests usage of the major functions. + /// + [TestMethod] + public void TestUsage() + { + var dict = new HybridDictionary(); + for (int i = 1; i < HybridDictionary.MaxListSize + 2; i++) + { + dict[i] = (i * 2).ToString(); + + // Verify the entry exists. + Assert.IsTrue(dict.ContainsKey(i)); + + // Verify the count has increased. + Assert.AreEqual(dict.Count, i); + + // Verify the correct item was added + Assert.AreEqual(dict[i], (i * 2).ToString()); + + // Verify we don't incorrectly find non-existent items + Assert.IsFalse(dict.ContainsKey(i + 10000)); + try + { + string x = dict[i + 10000]; + } + catch (Exception e) + { + if (e.GetType() != typeof(KeyNotFoundException)) + { + throw; + } + } + + // Verify we can change the entry + dict[i] = (i * 3).ToString(); + Assert.AreEqual(dict[i], (i * 3).ToString()); + } + } + + /// + /// Tests usage by random activities, comparing matching regular dictionary + /// + [TestMethod] + public void Medley() + { + string keys = "AAAAAAABCDEFGabcdefg"; + + Random rand = new Random(); + + for (int capacity = 0; capacity < HybridDictionary.MaxListSize + 2; capacity++) + { + var dict = new HybridDictionary(capacity, StringComparer.OrdinalIgnoreCase); + var shadow = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < 2000; i++) + { + switch (rand.Next(10 + 1)) + { + case 0: + // Set something + if (shadow.Count < HybridDictionary.MaxListSize + 2) // Don't bother exploring above here + { + string key = new String(keys[rand.Next(keys.Length)], 1); + string value = rand.Next(10).ToString(); + dict[key] = value; + shadow[key] = value.ToString(); + AssertDictionariesIdentical(dict, shadow); + } + + break; + + case 1: + // Remove something existing + if (shadow.Count > 0) + { + var entry = shadow.ElementAt(rand.Next(shadow.Count - 1)).Key; + Assert.AreEqual(dict.Remove(entry), shadow.Remove(entry)); + AssertDictionariesIdentical(dict, shadow); + } + + break; + + case 2: + // Remove something nonexisting + AssertBothOrNeitherThrew(delegate () { return dict.Remove("ZZ"); }, delegate () { return shadow.Remove("ZZ"); }); + AssertDictionariesIdentical(dict, shadow); + break; + + case 3: + // Look up something existing + if (shadow.Count > 0) + { + var entry2 = shadow.ElementAt(rand.Next(shadow.Count - 1)).Key; + Assert.AreEqual(dict[entry2], shadow[entry2]); + AssertDictionariesIdentical(dict, shadow); + } + + break; + + case 4: + // Look up something non existing + AssertBothOrNeitherThrew(delegate () { return dict["ZZ"]; }, delegate () { return shadow["ZZ"]; }); + AssertDictionariesIdentical(dict, shadow); + break; + + case 5: + // Try look up something existing + if (shadow.Count > 0) + { + var entry2 = shadow.ElementAt(rand.Next(shadow.Count - 1)).Key; + string value1; + string value2; + Assert.AreEqual(dict.TryGetValue(entry2, out value1), shadow.TryGetValue(entry2, out value2)); + Assert.AreEqual(value1, value2); + AssertDictionariesIdentical(dict, shadow); + } + + break; + + case 6: + // Try look up something non existing + string value3; + string value4; + Assert.AreEqual(dict.TryGetValue("ZZ", out value3), shadow.TryGetValue("ZZ", out value4)); + AssertDictionariesIdentical(dict, shadow); + break; + + case 7: + dict.Clear(); + shadow.Clear(); + break; + + case 8: + // Add something existing key same value + if (shadow.Count > 0) + { + var entry = shadow.ElementAt(rand.Next(shadow.Count - 1)); + AssertBothOrNeitherThrew(delegate () { dict.Add(entry.Key, entry.Value); }, delegate () { shadow.Add(entry.Key, entry.Value); }); + AssertDictionariesIdentical(dict, shadow); + } + + break; + + case 9: + // Add something existing key different value + if (shadow.Count > 0) + { + var key = shadow.ElementAt(rand.Next(shadow.Count - 1)).Key; + string value = rand.Next(10).ToString(); + AssertBothOrNeitherThrew(delegate () { dict.Add(key, value); }, delegate () { shadow.Add(key, value); }); + AssertDictionariesIdentical(dict, shadow); + } + + break; + + case 10: + // Add something nonexisting + string key2 = new String(keys[rand.Next(keys.Length)], 1); + string value5 = rand.Next(10).ToString(); + AssertBothOrNeitherThrew(delegate () { dict.Add(key2, value5); }, delegate () { shadow.Add(key2, value5); }); + AssertDictionariesIdentical(dict, shadow); + break; + } + } + + dict.Clear(); + Assert.IsTrue(dict.Count == 0); + } + } + + /// + /// Performs both actions supplied and asserts either both or neither threw + /// + private void AssertBothOrNeitherThrew(Action one, Action two) + { + AssertBothOrNeitherThrew( + delegate () + { + one(); + return null; + }, + delegate () + { + two(); + return null; + } + + ); + } + + /// + /// Performs both functions supplied and verifies either both or neither threw + /// + /// Type + private void AssertBothOrNeitherThrew(Func one, Func two) + { + Exception caughtOne = null; + Exception caughtTwo = null; + R result1 = default(R); + R result2 = default(R); + + try + { + result1 = one(); + } + catch (Exception ex) + { + caughtOne = ex; + } + + try + { + result2 = two(); + } + catch (Exception ex) + { + caughtTwo = ex; + } + + if (caughtOne != null ^ caughtTwo != null) + { + Assert.Fail("One threw KNF exception, the other didn't"); + } + + Assert.AreEqual(result1, result2, "One returned {0} the other {1}", result1, result2); + } + + /// + /// Verify the two dictionaries have identical content + /// + /// key + /// value + private void AssertDictionariesIdentical(IDictionary one, IDictionary two) + { + Assert.AreEqual(one.Count, two.Count, "Counts are unequal, {0} and {1}", one.Count, two.Count); + + foreach (KeyValuePair entry in one) + { + V value; + if (!two.TryGetValue(entry.Key, out value)) + { + Assert.Fail("one had key {0} the other didn't", entry.Key); + } + + if (!value.Equals(entry.Value)) + { + Assert.Fail("one had {0}={1} the other {2}={3}", entry.Key, entry.Value, entry.Key, value); + } + } + + foreach (var key in one.Keys) + { + Assert.AreEqual(true, two.ContainsKey(key)); + } + + var oneValues = new List(one.Values); + var twoValues = new List(two.Values); + + oneValues.Sort(); + twoValues.Sort(); + + Assert.AreEqual(oneValues.Count, twoValues.Count, "Value counts are unequal, {0} and {1}", oneValues.Count, twoValues.Count); + + for (int i = 0; i < oneValues.Count; i++) + { + Assert.AreEqual(oneValues[i], twoValues[i]); + } + + // Now repeat, using IDictionary interface + IDictionary oneId = (IDictionary)one; + IDictionary twoId = (IDictionary)two; + + Assert.AreEqual(oneId.Count, twoId.Count, "Counts are unequal, {0} and {1}", oneId.Count, twoId.Count); + + foreach (DictionaryEntry entry in oneId) + { + if (!twoId.Contains(entry.Key)) + { + Assert.Fail("oneId had key {0} the other didn't", entry.Key); + } + + if (!entry.Value.Equals(twoId[entry.Key])) + { + Assert.Fail("oneId had {0}={1} the other {2}={3}", entry.Key, entry.Value, entry.Key, twoId[entry.Key]); + } + } + + foreach (var key in oneId.Keys) + { + Assert.AreEqual(true, twoId.Contains(key)); + } + + var oneIdValues = new ArrayList(oneId.Values); + var twoIdValues = new ArrayList(twoId.Values); + + oneIdValues.Sort(); + twoIdValues.Sort(); + + Assert.AreEqual(oneIdValues.Count, twoIdValues.Count, "Value counts are unequal, {0} and {1}", oneIdValues.Count, twoIdValues.Count); + + for (int i = 0; i < oneIdValues.Count; i++) + { + Assert.AreEqual(oneIdValues[i], twoIdValues[i]); + } + } + } +} diff --git a/src/Shared/UnitTests/MockEngine.cs b/src/Shared/UnitTests/MockEngine.cs new file mode 100644 index 00000000000..4c2eb295715 --- /dev/null +++ b/src/Shared/UnitTests/MockEngine.cs @@ -0,0 +1,457 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /*************************************************************************** + * + * Class: MockEngine + * + * In order to execute tasks, we have to pass in an Engine object, so the + * task can log events. It doesn't have to be the real Engine object, just + * something that implements the IBuildEngine4 interface. So, we mock up + * a fake engine object here, so we're able to execute tasks from the unit tests. + * + * The unit tests could have instantiated the real Engine object, but then + * we would have had to take a reference onto the Microsoft.Build.Engine assembly, which + * is somewhat of a no-no for task assemblies. + * + **************************************************************************/ + sealed internal class MockEngine : IBuildEngine4 + { + private bool _isRunningMultipleNodes; + private int _messages = 0; + private int _warnings = 0; + private int _errors = 0; + private string _log = ""; + private string _upperLog = null; + private ProjectCollection _projectCollection = new ProjectCollection(); + private bool _logToConsole = false; + private MockLogger _mockLogger = null; + private Dictionary _objectCashe = new Dictionary(); + + internal MockEngine() : this(false) + { + } + + internal int Messages + { + set { _messages = value; } + get { return _messages; } + } + + internal int Warnings + { + set { _warnings = value; } + get { return _warnings; } + } + + internal int Errors + { + set { _errors = value; } + get { return _errors; } + } + + internal MockLogger MockLogger + { + get { return _mockLogger; } + } + + public MockEngine(bool logToConsole) + { + _mockLogger = new MockLogger(); + _logToConsole = logToConsole; + } + + + public void LogErrorEvent(BuildErrorEventArgs eventArgs) + { + if (eventArgs.File != null && eventArgs.File.Length > 0) + { + if (_logToConsole) + Console.Write("{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber); + _log += String.Format("{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber); + } + + if (_logToConsole) + Console.Write("ERROR " + eventArgs.Code + ": "); + _log += "ERROR " + eventArgs.Code + ": "; + ++_errors; + + if (_logToConsole) + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + } + + public void LogWarningEvent(BuildWarningEventArgs eventArgs) + { + if (eventArgs.File != null && eventArgs.File.Length > 0) + { + if (_logToConsole) + Console.Write("{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber); + _log += String.Format("{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber); + } + + if (_logToConsole) + Console.Write("WARNING " + eventArgs.Code + ": "); + _log += "WARNING " + eventArgs.Code + ": "; + ++_warnings; + + if (_logToConsole) + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + } + + public void LogCustomEvent(CustomBuildEventArgs eventArgs) + { + if (_logToConsole) + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + } + + public void LogMessageEvent(BuildMessageEventArgs eventArgs) + { + if (_logToConsole) + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + ++_messages; + } + + public bool ContinueOnError + { + get + { + return false; + } + } + + public string ProjectFileOfTaskNode + { + get + { + return String.Empty; + } + } + + public int LineNumberOfTaskNode + { + get + { + return 0; + } + } + + public int ColumnNumberOfTaskNode + { + get + { + return 0; + } + } + + internal string Log + { + set { _log = value; } + get { return _log; } + } + + public bool IsRunningMultipleNodes + { + get { return _isRunningMultipleNodes; } + set { _isRunningMultipleNodes = value; } + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalPropertiesPassedIntoTask, + IDictionary targetOutputs + ) + { + ILogger[] loggers = new ILogger[2] { _mockLogger, new ConsoleLogger() }; + + return this.BuildProjectFile(projectFileName, targetNames, globalPropertiesPassedIntoTask, targetOutputs, null); + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalPropertiesPassedIntoTask, + IDictionary targetOutputs, + string toolsVersion + ) + { + Dictionary finalGlobalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Finally, whatever global properties were passed into the task ... those are the final winners. + if (globalPropertiesPassedIntoTask != null) + { + foreach (DictionaryEntry newGlobalProperty in globalPropertiesPassedIntoTask) + { + finalGlobalProperties[(string)newGlobalProperty.Key] = (string)newGlobalProperty.Value; + } + } + + Project project = _projectCollection.LoadProject(projectFileName, finalGlobalProperties, toolsVersion); + + ILogger[] loggers = new ILogger[2] { _mockLogger, new ConsoleLogger() }; + + return project.Build(targetNames, loggers); + } + + public bool BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IDictionary[] targetOutputsPerProject, + string[] toolsVersion, + bool useResultsCache, + bool unloadProjectsOnCompletion + ) + { + bool includeTargetOutputs = targetOutputsPerProject != null; + + BuildEngineResult result = BuildProjectFilesInParallel(projectFileNames, targetNames, globalProperties, new List[projectFileNames.Length], toolsVersion, includeTargetOutputs); + + if (includeTargetOutputs) + { + for (int i = 0; i < targetOutputsPerProject.Length; i++) + { + if (targetOutputsPerProject[i] != null) + { + foreach (KeyValuePair output in result.TargetOutputsPerProject[i]) + { + targetOutputsPerProject[i].Add(output.Key, output.Value); + } + } + } + } + + return result.Result; + } + + public BuildEngineResult BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IList[] undefineProperties, + string[] toolsVersion, + bool returnTargetOutputs + ) + { + List> targetOutputsPerProject = null; + + ILogger[] loggers = new ILogger[2] { _mockLogger, new ConsoleLogger() }; + + bool allSucceeded = true; + + if (returnTargetOutputs) + { + targetOutputsPerProject = new List>(); + } + + for (int i = 0; i < projectFileNames.Length; i++) + { + Dictionary finalGlobalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (globalProperties[i] != null) + { + foreach (DictionaryEntry newGlobalProperty in globalProperties[i]) + { + finalGlobalProperties[(string)newGlobalProperty.Key] = (string)newGlobalProperty.Value; + } + } + + ProjectInstance instance = _projectCollection.LoadProject((string)projectFileNames[i], finalGlobalProperties, null).CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = instance.Build(targetNames, loggers, out targetOutputs); + + if (targetOutputsPerProject != null) + { + targetOutputsPerProject.Add(new Dictionary(StringComparer.OrdinalIgnoreCase)); + + foreach (KeyValuePair resultEntry in targetOutputs) + { + targetOutputsPerProject[i][resultEntry.Key] = resultEntry.Value.Items; + } + } + + allSucceeded = allSucceeded && success; + } + + return new BuildEngineResult(allSucceeded, targetOutputsPerProject); + } + + public void Yield() + { + } + + public void Reacquire() + { + } + + public bool BuildProjectFile + ( + string projectFileName + ) + { + return (_projectCollection.LoadProject(projectFileName)).Build(); + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames + ) + { + return (_projectCollection.LoadProject(projectFileName)).Build(targetNames); + } + + public bool BuildProjectFile + ( + string projectFileName, + string targetName + ) + { + return (_projectCollection.LoadProject(projectFileName)).Build(targetName); + } + + public void UnregisterAllLoggers + ( + ) + { + _projectCollection.UnregisterAllLoggers(); + } + + public void UnloadAllProjects + ( + ) + { + _projectCollection.UnloadAllProjects(); + } + + + /// + /// Assert that the mock log in the engine doesn't contain a certain message based on a resource string and some parameters + /// + internal void AssertLogDoesntContainMessageFromResource(GetStringDelegate getString, string resourceName, params string[] parameters) + { + string resource = getString(resourceName); + string stringToSearchFor = String.Format(resource, parameters); + AssertLogDoesntContain(stringToSearchFor); + } + + /// + /// Assert that the mock log in the engine contains a certain message based on a resource string and some parameters + /// + internal void AssertLogContainsMessageFromResource(GetStringDelegate getString, string resourceName, params string[] parameters) + { + string resource = getString(resourceName); + string stringToSearchFor = String.Format(resource, parameters); + AssertLogContains(stringToSearchFor); + } + + /// + /// Assert that the log file contains the given string. + /// Case insensitive. + /// First check if the string is in the log string. If not + /// than make sure it is also check the MockLogger + /// + /// + internal void AssertLogContains(string contains) + { + if (_upperLog == null) + { + _upperLog = _log; + _upperLog = _upperLog.ToUpperInvariant(); + } + + // If we do not contain this string than pass it to + // MockLogger. Since MockLogger is also registered as + // a logger it may have this string. + if (!_upperLog.Contains + ( + contains.ToUpperInvariant() + ) + ) + { + Console.WriteLine(_log); + _mockLogger.AssertLogContains(contains); + } + } + + /// + /// Assert that the log doesnt contain the given string. + /// First check if the string is in the log string. If not + /// than make sure it is also not in the MockLogger + /// + /// + internal void AssertLogDoesntContain(string contains) + { + Console.WriteLine(_log); + + if (_upperLog == null) + { + _upperLog = _log; + _upperLog = _upperLog.ToUpperInvariant(); + } + + Assert.IsTrue + ( + !_upperLog.Contains + ( + contains.ToUpperInvariant() + ) + ); + + // If we do not contain this string than pass it to + // MockLogger. Since MockLogger is also registered as + // a logger it may have this string. + _mockLogger.AssertLogDoesntContain + ( + contains + ); + } + + /// + /// Delegate which will get the resource from the correct resoruce manager + /// + public delegate string GetStringDelegate(string resourceName); + + public object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + object obj = null; + _objectCashe.TryGetValue(key, out obj); + return obj; + } + + public void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection) + { + _objectCashe[key] = obj; + } + + public object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + var obj = _objectCashe[key]; + _objectCashe.Remove(key); + return obj; + } + } +} diff --git a/src/Shared/UnitTests/MockLogger.cs b/src/Shared/UnitTests/MockLogger.cs new file mode 100644 index 00000000000..8bcfe4b9bf8 --- /dev/null +++ b/src/Shared/UnitTests/MockLogger.cs @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Text; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using ProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; + + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: MockLogger + * + * Mock logger class. Keeps track of errors and warnings and also builds + * up a raw string (fullLog) that contains all messages, warnings, errors. + * + */ + internal sealed class MockLogger : ILogger + { + #region Properties + private int _errorCount = 0; + private int _warningCount = 0; + private StringBuilder _fullLog = new StringBuilder(); + private List _errors = new List(); + private List _warnings = new List(); + private List _externalProjectStartedEvents = new List(); + private List _externalProjectFinishedEvents = new List(); + private bool _logBuildFinishedEvent = true; + private List _projectStartedEvents = new List(); + private List _projectFinishedEvents = new List(); + private List _targetStartedEvents = new List(); + private List _targetFinishedEvents = new List(); + private List _taskStartedEvents = new List(); + private List _taskFinishedEvents = new List(); + private List _buildMessageEvents = new List(); + private List _buildStartedEvents = new List(); + private List _buildFinishedEvents = new List(); + + /// + /// Should the build finished event be logged in the log file. This is to work around the fact we have different + /// localized strings between env and xmake for the build finished event. + /// + internal bool LogBuildFinished + { + get + { + return _logBuildFinishedEvent; + } + set + { + _logBuildFinishedEvent = value; + } + } + + /* + * Method: ErrorCount + * + * The count of all errors seen so far. + * + */ + internal int ErrorCount + { + get { return _errorCount; } + } + + /* + * Method: WarningCount + * + * The count of all warnings seen so far. + * + */ + internal int WarningCount + { + get { return _warningCount; } + } + + /// + /// Return the list of logged errors + /// + internal List Errors + { + get + { + return _errors; + } + } + + /// + /// Returns the list of logged warnings + /// + internal List Warnings + { + get + { + return _warnings; + } + } + + /// + /// When set to true, allows task crashes to be logged without causing an assert. + /// + internal bool AllowTaskCrashes + { + get; + set; + } + + /// + /// List of ExternalProjectStarted events + /// + internal List ExternalProjectStartedEvents + { + get { return _externalProjectStartedEvents; } + } + + /// + /// List of ExternalProjectFinished events + /// + internal List ExternalProjectFinishedEvents + { + get { return _externalProjectFinishedEvents; } + } + + /// + /// List of ProjectStarted events + /// + internal List ProjectStartedEvents + { + get { return _projectStartedEvents; } + } + + /// + /// List of ProjectFinished events + /// + internal List ProjectFinishedEvents + { + get { return _projectFinishedEvents; } + } + + /// + /// List of TargetStarted events + /// + internal List TargetStartedEvents + { + get { return _targetStartedEvents; } + } + + /// + /// List of TargetFinished events + /// + internal List TargetFinishedEvents + { + get { return _targetFinishedEvents; } + } + + /// + /// List of TaskStarted events + /// + internal List TaskStartedEvents + { + get { return _taskStartedEvents; } + } + + /// + /// List of TaskFinished events + /// + internal List TaskFinishedEvents + { + get { return _taskFinishedEvents; } + } + + /// + /// List of BuildMessage events + /// + internal List BuildMessageEvents + { + get { return _buildMessageEvents; } + } + + /// + /// List of BuildStarted events, thought we expect there to only be one, a valid check is to make sure this list is length 1 + /// + internal List BuildStartedEvents + { + get { return _buildStartedEvents; } + } + + /// + /// List of BuildFinished events, thought we expect there to only be one, a valid check is to make sure this list is length 1 + /// + internal List BuildFinishedEvents + { + get { return _buildFinishedEvents; } + } + + /* + * Method: FullLog + * + * The raw concatenation of all messages, errors and warnings seen so far. + * + */ + internal string FullLog + { + get { return _fullLog.ToString(); } + } + #endregion + + #region Minimal ILogger implementation + + /* + * Property: Verbosity + * + * The level of detail to show in the event log. + * + */ + public LoggerVerbosity Verbosity + { + get { return LoggerVerbosity.Normal; } + set {/* do nothing */} + } + + /* + * Property: Parameters + * + * The mock logger does not take parameters. + * + */ + public string Parameters + { + get + { + return null; + } + + set + { + // do nothing + } + } + + /* + * Method: Initialize + * + * Add a new build event. + * + */ + public void Initialize(IEventSource eventSource) + { + eventSource.AnyEventRaised += + new AnyEventHandler(LoggerEventHandler); + } + + /// + /// Clears the content of the log "file" + /// + public void ClearLog() + { + _fullLog = new StringBuilder(); + } + + /* + * Method: Shutdown + * + * The mock logger does not need to release any resources. + * + */ + public void Shutdown() + { + // do nothing + } + #endregion + + /* + * Method: LoggerEventHandler + * + * Recieves build events and logs them the way we like. + * + */ + internal void LoggerEventHandler(object sender, BuildEventArgs eventArgs) + { + if (eventArgs is BuildWarningEventArgs) + { + BuildWarningEventArgs w = (BuildWarningEventArgs)eventArgs; + + // hack: disregard the MTA warning. + // need the second condition to pass on ploc builds + if (w.Code != "MSB4056" && !w.Message.Contains("MSB4056")) + { + _fullLog.AppendFormat("{0}({1},{2}): {3} warning {4}: {5}\r\n", + w.File, + w.LineNumber, + w.ColumnNumber, + w.Subcategory, + w.Code, + w.Message); + + ++_warningCount; + _warnings.Add(w); + } + } + else if (eventArgs is BuildErrorEventArgs) + { + BuildErrorEventArgs e = (BuildErrorEventArgs)eventArgs; + + _fullLog.AppendFormat("{0}({1},{2}): {3} error {4}: {5}\r\n", + e.File, + e.LineNumber, + e.ColumnNumber, + e.Subcategory, + e.Code, + e.Message); + + ++_errorCount; + _errors.Add(e); + } + else + { + // Log the message unless we are a build finished event and logBuildFinished is set to false. + bool logMessage = !(eventArgs is BuildFinishedEventArgs) || (eventArgs is BuildFinishedEventArgs && _logBuildFinishedEvent); + if (logMessage) + { + _fullLog.Append(eventArgs.Message); + _fullLog.Append("\r\n"); + } + } + + if (eventArgs is ExternalProjectStartedEventArgs) + { + this.ExternalProjectStartedEvents.Add((ExternalProjectStartedEventArgs)eventArgs); + } + else if (eventArgs is ExternalProjectFinishedEventArgs) + { + this.ExternalProjectFinishedEvents.Add((ExternalProjectFinishedEventArgs)eventArgs); + } + + if (eventArgs is ProjectStartedEventArgs) + { + this.ProjectStartedEvents.Add((ProjectStartedEventArgs)eventArgs); + } + else if (eventArgs is ProjectFinishedEventArgs) + { + this.ProjectFinishedEvents.Add((ProjectFinishedEventArgs)eventArgs); + } + else if (eventArgs is TargetStartedEventArgs) + { + this.TargetStartedEvents.Add((TargetStartedEventArgs)eventArgs); + } + else if (eventArgs is TargetFinishedEventArgs) + { + this.TargetFinishedEvents.Add((TargetFinishedEventArgs)eventArgs); + } + else if (eventArgs is TaskStartedEventArgs) + { + this.TaskStartedEvents.Add((TaskStartedEventArgs)eventArgs); + } + else if (eventArgs is TaskFinishedEventArgs) + { + this.TaskFinishedEvents.Add((TaskFinishedEventArgs)eventArgs); + } + else if (eventArgs is BuildMessageEventArgs) + { + this.BuildMessageEvents.Add((BuildMessageEventArgs)eventArgs); + } + else if (eventArgs is BuildStartedEventArgs) + { + this.BuildStartedEvents.Add((BuildStartedEventArgs)eventArgs); + } + else if (eventArgs is BuildFinishedEventArgs) + { + this.BuildFinishedEvents.Add((BuildFinishedEventArgs)eventArgs); + + if (!AllowTaskCrashes) + { + // We should not have any task crashes. Sometimes a test will validate that their expected error + // code appeared, but not realize it then crashed. + AssertLogDoesntContain("MSB4018"); + } + + // We should not have any Engine crashes. + AssertLogDoesntContain("MSB0001"); + + // Console.Write in the context of a unit test is very expensive. A hundred + // calls to Console.Write can easily take two seconds on a fast machine. Therefore, only + // do the Console.Write once at the end of the build. + Console.Write(FullLog); + } + } + + // Lazy-init property returning the MSBuild engine resource manager + static private ResourceManager EngineResourceManager + { + get + { + if (s_engineResourceManager == null) + { + s_engineResourceManager = new ResourceManager("Microsoft.Build.Resources.Strings", typeof(ProjectCollection).Assembly); + } + + return s_engineResourceManager; + } + } + + static private ResourceManager s_engineResourceManager = null; + + // Gets the resource string given the resource ID + static public string GetString(string stringId) + { + return EngineResourceManager.GetString(stringId, CultureInfo.CurrentUICulture); + } + + /// + /// Assert that the log file contains the given strings, in order. + /// + /// + internal void AssertLogContains(params string[] contains) + { + AssertLogContains(true, contains); + } + + /// + /// Assert that the log file contains the given string, in order. Includes the option of case invariance + /// + /// False if we do not care about case sensitivity + /// + internal void AssertLogContains(bool isCaseSensitive, params string[] contains) + { + StringReader reader = new StringReader(FullLog); + int index = 0; + + string currentLine = reader.ReadLine(); + if (!isCaseSensitive) + { + currentLine = currentLine.ToUpper(); + } + + while (currentLine != null) + { + string comparer = contains[index]; + if (!isCaseSensitive) + { + comparer = comparer.ToUpper(); + } + + if (currentLine.Contains(comparer)) + { + index++; + if (index == contains.Length) break; + } + + currentLine = reader.ReadLine(); + if (!isCaseSensitive) + { + currentLine = currentLine.ToUpper(); + } + } + if (index != contains.Length) + { + Console.WriteLine(FullLog); + Assert.Fail(String.Format(CultureInfo.CurrentCulture, "Log was expected to contain '{0}', but did not.\n=======\n{1}\n=======", contains[index], FullLog)); + } + } + + /// + /// Assert that the log file contains the given string. + /// + /// + internal void AssertLogDoesntContain(string contains) + { + if (FullLog.Contains(contains)) + { + Console.WriteLine(FullLog); + Assert.Fail(String.Format("Log was not expected to contain '{0}', but did.", contains)); + } + } + + /// + /// Assert that no errors were logged + /// + internal void AssertNoErrors() + { + Assert.AreEqual(0, _errorCount); + } + + /// + /// Assert that no warnings were logged + /// + internal void AssertNoWarnings() + { + Assert.AreEqual(0, _warningCount); + } + } +} diff --git a/src/Shared/UnitTests/NativeMethodsShared_Tests.cs b/src/Shared/UnitTests/NativeMethodsShared_Tests.cs new file mode 100644 index 00000000000..70e480765f9 --- /dev/null +++ b/src/Shared/UnitTests/NativeMethodsShared_Tests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public sealed class NativeMethodsShared_Tests + { + #region Data + + // Create a delegate to test the GetPRocessId method when using GetProcAddress + private delegate uint GetProcessIdDelegate(); + + #endregion + + #region Tests + + /// + /// Confirms we can find a file on the system path. + /// + [TestMethod] + public void FindFileOnPath() + { + string expectedCmdPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); + string cmdPath = NativeMethodsShared.FindOnPath("cmd.exe"); + + Assert.IsNotNull(cmdPath); + + // for the NUnit "Standard Out" tab + Console.WriteLine("Expected location of \"cmd.exe\": " + expectedCmdPath); + Console.WriteLine("Found \"cmd.exe\" here: " + cmdPath); + + Assert.AreEqual(0, String.Compare(cmdPath, expectedCmdPath, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Confirms we can find a file on the system path even if the path + /// to the file is very long. + /// + [TestMethod] + public void FindFileOnPathAfterResizingBuffer() + { + int savedMaxPath = NativeMethodsShared.MAX_PATH; + + try + { + // make the default buffer size very small -- intentionally don't use + // zero, otherwise StringBuilder will use some default larger capacity + NativeMethodsShared.MAX_PATH = 1; + + FindFileOnPath(); + } + finally + { + NativeMethodsShared.MAX_PATH = savedMaxPath; + } + } + /// + /// Confirms we cannot find a bogus file on the system path. + /// + [TestMethod] + public void DoNotFindFileOnPath() + { + string bogusFile = Path.ChangeExtension(Guid.NewGuid().ToString(), ".txt"); + // for the NUnit "Standard Out" tab + Console.WriteLine("The bogus file name is: " + bogusFile); + + string bogusFilePath = NativeMethodsShared.FindOnPath(bogusFile); + + Assert.IsNull(bogusFilePath); + } + + /// + /// Verify that getProcAddress works, bug previously was due to a bug in the attributes used to pinvoke the method + /// when that bug was in play this test would fail. + /// + [TestMethod] + public void TestGetProcAddress() + { + IntPtr kernel32Dll = NativeMethodsShared.LoadLibrary("kernel32.dll"); + try + { + IntPtr processHandle = NativeMethodsShared.NullIntPtr; + if (kernel32Dll != NativeMethodsShared.NullIntPtr) + { + processHandle = NativeMethodsShared.GetProcAddress(kernel32Dll, "GetCurrentProcessId"); + } + else + { + Assert.Fail(); + } + + // Make sure the pointer passed back for the method is not null + Assert.IsTrue(processHandle != NativeMethodsShared.NullIntPtr); + + //Actually call the method + GetProcessIdDelegate processIdDelegate = (GetProcessIdDelegate)Marshal.GetDelegateForFunctionPointer(processHandle, typeof(GetProcessIdDelegate)); + uint processId = processIdDelegate(); + + //Make sure the return value is the same as retreived from the .net methods to make sure everything works + Assert.AreEqual((uint)Process.GetCurrentProcess().Id, processId, "Expected the .net processId to match the one from GetCurrentProcessId"); + } + finally + { + if (kernel32Dll != NativeMethodsShared.NullIntPtr) + { + NativeMethodsShared.FreeLibrary(kernel32Dll); + } + } + } + + /// + /// Verifies that when NativeMethodsShared.GetLastWriteFileUtcTime() is called on a + /// missing time, DateTime.MinValue is returned. + /// + [TestMethod] + public void GetLastWriteFileUtcTimeReturnsMinValueForMissingFile() + { + string nonexistentFile = FileUtilities.GetTemporaryFile(); + // Make sure that the file does not, in fact, exist. + File.Delete(nonexistentFile); + + DateTime nonexistentFileTime = NativeMethodsShared.GetLastWriteFileUtcTime(nonexistentFile); + Assert.AreEqual(nonexistentFileTime, DateTime.MinValue); + } + + /// + /// Verifies that NativeMethodsShared.SetCurrentDirectory(), when called on a nonexistent + /// directory, will not set the current directory to that location. + /// + [TestMethod] + public void SetCurrentDirectoryDoesNotSetNonexistentFolder() + { + string currentDirectory = Environment.CurrentDirectory; + string nonexistentDirectory = currentDirectory + @"foo\bar\baz"; + + // Make really sure the nonexistent directory doesn't actually exist + if (Directory.Exists(nonexistentDirectory)) + { + for (int i = 0; i < 10; i++) + { + nonexistentDirectory = currentDirectory + @"foo\bar\baz" + Guid.NewGuid(); + + if (!Directory.Exists(nonexistentDirectory)) + { + break; + } + } + } + + if (Directory.Exists(nonexistentDirectory)) + { + Assert.Fail("Directory.Exists(nonexistentDirectory)", "Tried 10 times to get a nonexistent directory name and failed -- please try again"); + } + else + { + bool exceptionCaught = false; + try + { + NativeMethodsShared.SetCurrentDirectory(nonexistentDirectory); + } + catch (Exception e) + { + exceptionCaught = true; + Console.WriteLine(e.Message); + } + finally + { + // verify that the current directory did not change + Assert.IsFalse(exceptionCaught, "SetCurrentDirectory should not throw!"); + Assert.AreEqual(currentDirectory, Environment.CurrentDirectory); + } + } + } + + #endregion + } +} diff --git a/src/Shared/UnitTests/ObjectModelHelpers.cs b/src/Shared/UnitTests/ObjectModelHelpers.cs new file mode 100644 index 00000000000..a4493c12d75 --- /dev/null +++ b/src/Shared/UnitTests/ObjectModelHelpers.cs @@ -0,0 +1,1339 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: ObjectModelHelpers + * + * Utility methods for unit tests that work through the object model. + * + */ + public static class ObjectModelHelpers + { + private const string msbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; + private const string msbuildDefaultToolsVersion = MSBuildConstants.CurrentToolsVersion; + private const string msbuildAssemblyVersion = MSBuildConstants.CurrentAssemblyVersion; + private const string currentVisualStudioVersion = MSBuildConstants.CurrentVisualStudioVersion; + + /// + /// Return the the current Visual Studio version + /// + internal static string CurrentVisualStudioVersion + { + get + { + return currentVisualStudioVersion; + } + } + + /// + /// Return the default tools version + /// + internal static string MSBuildDefaultToolsVersion + { + get + { + return msbuildDefaultToolsVersion; + } + } + + /// + /// Return the current assembly version + /// + internal static string MSBuildAssemblyVersion + { + get + { + return msbuildAssemblyVersion; + } + } + + + /// + /// Helper method to tell us whether a particular metadata name is an MSBuild well-known metadata + /// (e.g., "RelativeDir", "FullPath", etc.) + /// + private static Hashtable s_builtInMetadataNames = null; + static private bool IsBuiltInItemMetadataName(string metadataName) + { + if (s_builtInMetadataNames == null) + { + s_builtInMetadataNames = new Hashtable(); + + Microsoft.Build.Utilities.TaskItem dummyTaskItem = new Microsoft.Build.Utilities.TaskItem(); + foreach (string builtInMetadataName in dummyTaskItem.MetadataNames) + { + s_builtInMetadataNames[builtInMetadataName] = String.Empty; + } + } + + return s_builtInMetadataNames.Contains(metadataName); + } + + internal delegate void MethodUnderTest(); + + /// + /// Gets an item list from the project and assert that it contains + /// exactly one item with the supplied name. + /// + static internal ProjectItem AssertSingleItem(Project p, string type, string itemInclude) + { + ProjectItem[] items = p.GetItems(type).ToArray(); + int count = 0; + foreach (ProjectItem item in items) + { + Assert.AreEqual(itemInclude.ToUpperInvariant(), item.EvaluatedInclude.ToUpperInvariant()); + ++count; + } + + Assert.AreEqual(1, count); + + return items[0]; + } + + /// + /// Helper that asserts if an exception of type specified is + /// not thrown when calling specified method + /// + /// + /// AssertThrows(typeof(InvalidOperationException), delegate { object o = ((IEnumerator)enumerator).Current; }); + /// + internal static void AssertThrows(Type exception, MethodUnderTest method) + { + try + { + method(); + } + catch (Exception ex) + { + if (ex.GetType() == exception) + { + return; + } + } + Assert.Fail("Didn't throw " + exception.ToString()); + } + + /// + /// Amazingly sophisticated :) helper function to determine if the set of ITaskItems returned from + /// a task match the expected set of ITaskItems. It can also check that the ITaskItems have the expected + /// metadata, and that the ITaskItems are returned in the correct order. + /// + /// The "expectedItemsString" is a formatted way of easily specifying which items you expect to see. + /// The format is: + /// + /// itemspec1 : metadataname1=metadatavalue1 ; metadataname2=metadatavalue2 ; ... + /// itemspec2 : metadataname3=metadatavalue3 ; metadataname4=metadatavalue4 ; ... + /// itemspec3 : metadataname5=metadatavalue5 ; metadataname6=metadatavalue6 ; ... + /// + /// (Each item needs to be on its own line.) + /// + /// + /// + /// + static internal void AssertItemsMatch(string expectedItemsString, ITaskItem[] actualItems) + { + AssertItemsMatch(expectedItemsString, actualItems, true); + } + + /// + /// Amazingly sophisticated :) helper function to determine if the set of ITaskItems returned from + /// a task match the expected set of ITaskItems. It can also check that the ITaskItems have the expected + /// metadata, and that the ITaskItems are returned in the correct order. + /// + /// The "expectedItemsString" is a formatted way of easily specifying which items you expect to see. + /// The format is: + /// + /// itemspec1 : metadataname1=metadatavalue1 ; metadataname2=metadatavalue2 ; ... + /// itemspec2 : metadataname3=metadatavalue3 ; metadataname4=metadatavalue4 ; ... + /// itemspec3 : metadataname5=metadatavalue5 ; metadataname6=metadatavalue6 ; ... + /// + /// (Each item needs to be on its own line.) + /// + /// + /// + /// + /// + static internal void AssertItemsMatch(string expectedItemsString, ITaskItem[] actualItems, bool orderOfItemsShouldMatch) + { + List expectedItems = ParseExpectedItemsString(expectedItemsString); + + // Form a string of expected item specs. For logging purposes only. + StringBuilder expectedItemSpecs = new StringBuilder(); + foreach (ITaskItem expectedItem in expectedItems) + { + if (expectedItemSpecs.Length > 0) + { + expectedItemSpecs.Append("; "); + } + + expectedItemSpecs.Append(expectedItem.ItemSpec); + } + + // Form a string of expected item specs. For logging purposes only. + StringBuilder actualItemSpecs = new StringBuilder(); + foreach (ITaskItem actualItem in actualItems) + { + if (actualItemSpecs.Length > 0) + { + actualItemSpecs.Append("; "); + } + + actualItemSpecs.Append(actualItem.ItemSpec); + } + + bool outOfOrder = false; + + // Loop through all the actual items. + for (int actualItemIndex = 0; actualItemIndex < actualItems.Length; actualItemIndex++) + { + ITaskItem actualItem = actualItems[actualItemIndex]; + + // Loop through all the expected items to find one with the same item spec. + ITaskItem expectedItem = null; + int expectedItemIndex; + for (expectedItemIndex = 0; expectedItemIndex < expectedItems.Count; expectedItemIndex++) + { + if (expectedItems[expectedItemIndex].ItemSpec == actualItem.ItemSpec) + { + expectedItem = expectedItems[expectedItemIndex]; + + // If the items are expected to be in the same order, then the expected item + // should always be found at index zero, because we remove items from the expected + // list as we find them. + if ((expectedItemIndex != 0) && (orderOfItemsShouldMatch)) + { + outOfOrder = true; + } + + break; + } + } + + Assert.IsNotNull(expectedItem, String.Format("Item '{0}' was returned but not expected.", actualItem.ItemSpec)); + + // Make sure all the metadata on the expected item matches the metadata on the actual item. + // Don't check built-in metadata ... only check custom metadata. + foreach (string metadataName in expectedItem.MetadataNames) + { + // This check filters out any built-in item metadata, like "RelativeDir", etc. + if (!IsBuiltInItemMetadataName(metadataName)) + { + string expectedMetadataValue = expectedItem.GetMetadata(metadataName); + string actualMetadataValue = actualItem.GetMetadata(metadataName); + + Assert.IsTrue + ( + actualMetadataValue.Length > 0 || expectedMetadataValue.Length == 0, + string.Format("Item '{0}' does not have expected metadata '{1}'.", actualItem.ItemSpec, metadataName) + ); + + Assert.IsTrue + ( + actualMetadataValue.Length == 0 || expectedMetadataValue.Length > 0, + string.Format("Item '{0}' has unexpected metadata {1}={2}.", actualItem.ItemSpec, metadataName, actualMetadataValue) + ); + + Assert.AreEqual + ( + expectedMetadataValue, + actualMetadataValue, + string.Format + ( + "Item '{0}' has metadata {1}={2} instead of expected {1}={3}.", + actualItem.ItemSpec, + metadataName, + actualMetadataValue, + expectedMetadataValue + ) + ); + } + } + expectedItems.RemoveAt(expectedItemIndex); + } + + // Log an error for any leftover items in the expectedItems collection. + foreach (ITaskItem expectedItem in expectedItems) + { + Assert.Fail(String.Format("Item '{0}' was expected but not returned.", expectedItem.ItemSpec)); + } + + if (outOfOrder) + { + Console.WriteLine("ERROR: Items were returned in the incorrect order..."); + Console.WriteLine("Expected: " + expectedItemSpecs); + Console.WriteLine("Actual: " + actualItemSpecs); + Assert.Fail("Items were returned in the incorrect order. See 'Standard Out' tab for more details."); + } + } + + /// + /// Used to compare the contents of two arrays. + /// + internal static void AssertArrayContentsMatch(object[] expected, object[] actual) + { + if (expected == null) + { + Assert.IsNull(actual, "Expected a null array"); + } + else + { + Assert.IsNotNull(actual, "Result should be non-null."); + } + + Assert.AreEqual(expected.Length, actual.Length, "Expected array length of <" + expected.Length + "> but was <" + actual.Length + ">."); + + // Now that we've verified they're both non-null and of the same length, compare each item in the array. + for (int i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], actual[i], "At index " + i + " expected " + expected[i].ToString() + " but was " + actual.ToString()); + } + } + + /// + /// Parses the crazy string passed into AssertItemsMatch and returns a list of ITaskItems. + /// + /// + /// + static private List ParseExpectedItemsString(string expectedItemsString) + { + List expectedItems = new List(); + + // First, parse this massive string that we've been given, and create an ITaskItem[] out of it, + // so we can more easily compare it against the actual items. + string[] expectedItemsStringSplit = expectedItemsString.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string singleExpectedItemString in expectedItemsStringSplit) + { + string singleExpectedItemStringTrimmed = singleExpectedItemString.Trim(); + if (singleExpectedItemStringTrimmed.Length > 0) + { + int indexOfColon = singleExpectedItemStringTrimmed.IndexOf(": "); + if (indexOfColon == -1) + { + expectedItems.Add(new Microsoft.Build.Utilities.TaskItem(singleExpectedItemStringTrimmed)); + } + else + { + // We found a colon, which means there's metadata in there. + + // The item spec is the part before the colon. + string itemSpec = singleExpectedItemStringTrimmed.Substring(0, indexOfColon).Trim(); + + // The metadata is the part after the colon. + string itemMetadataString = singleExpectedItemStringTrimmed.Substring(indexOfColon + 1); + + ITaskItem expectedItem = new Microsoft.Build.Utilities.TaskItem(itemSpec); + + string[] itemMetadataPieces = itemMetadataString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string itemMetadataPiece in itemMetadataPieces) + { + string itemMetadataPieceTrimmed = itemMetadataPiece.Trim(); + if (itemMetadataPieceTrimmed.Length > 0) + { + int indexOfEquals = itemMetadataPieceTrimmed.IndexOf('='); + Assert.IsTrue(indexOfEquals != -1, String.Format("Could not find in item metadata definition '{0}'", itemMetadataPieceTrimmed)); + + string itemMetadataName = itemMetadataPieceTrimmed.Substring(0, indexOfEquals).Trim(); + string itemMetadataValue = itemMetadataPieceTrimmed.Substring(indexOfEquals + 1).Trim(); + + expectedItem.SetMetadata(itemMetadataName, itemMetadataValue); + } + } + + expectedItems.Add(expectedItem); + } + } + } + + return expectedItems; + } + + /// + /// Assert that a given file exists within the temp project directory. + /// + /// + internal static void AssertFileExistsInTempProjectDirectory(string fileRelativePath) + { + AssertFileExistsInTempProjectDirectory(fileRelativePath, null); + } + + /// + /// Assert that a given file exists within the temp project directory. + /// + /// + /// Can be null. + internal static void AssertFileExistsInTempProjectDirectory(string fileRelativePath, string message) + { + if (message == null) + { + message = fileRelativePath + " doesn't exist, but it should."; + } + + AssertFileExistenceInTempProjectDirectory(fileRelativePath, message, true); + } + + /// + /// Assert that a given file exists (or not) within the temp project directory. + /// + /// + /// Can be null. + private static void AssertFileExistenceInTempProjectDirectory(string fileRelativePath, string message, bool exists) + { + Assert.AreEqual(exists, File.Exists(Path.Combine(TempProjectDir, fileRelativePath)), message); + } + + /// + /// Does certain replacements in a string representing the project file contents. + /// This makes it easier to write unit tests because the author doesn't have + /// to worry about escaping double-quotes, etc. + /// + /// + /// + static internal string CleanupFileContents(string projectFileContents) + { + // Replace reverse-single-quotes with double-quotes. + projectFileContents = projectFileContents.Replace("`", "\""); + + // Place the correct MSBuild namespace into the tag. + projectFileContents = projectFileContents.Replace("msbuildnamespace", msbuildNamespace); + projectFileContents = projectFileContents.Replace("msbuilddefaulttoolsversion", msbuildDefaultToolsVersion); + projectFileContents = projectFileContents.Replace("msbuildassemblyversion", msbuildAssemblyVersion); + + return projectFileContents; + } + + /// + /// Normalizes all the whitespace in an Xml document so that two documents that + /// differ only in whitespace can be easily compared to each other for sameness. + /// + /// + /// + static internal string NormalizeXmlWhitespace(XmlDocument xmldoc) + { + // Normalize all the whitespace by writing the Xml document out to a + // string, with PreserveWhitespace=false. + xmldoc.PreserveWhitespace = false; + StringWriter stringWriter = new StringWriter(); + xmldoc.Save(stringWriter); + return stringWriter.ToString(); + } + + /// + /// Create an MSBuild project file on disk and return the full path to it. + /// + /// + /// + static internal string CreateTempFileOnDisk(string fileContents, params object[] args) + { + return CreateTempFileOnDiskNoFormat(String.Format(fileContents, args)); + } + + /// + /// Create an MSBuild project file on disk and return the full path to it. + /// + /// + /// + static internal string CreateTempFileOnDiskNoFormat(string fileContents) + { + string projectFilePath = FileUtilities.GetTemporaryFile(); + + File.WriteAllText(projectFilePath, CleanupFileContents(fileContents)); + + return projectFilePath; + } + + /// + /// Create a project in memory. Load up the given XML. + /// + /// + /// + static internal Project CreateInMemoryProject(string xml) + { + return CreateInMemoryProject(xml, new ConsoleLogger()); + } + + /// + /// Create a project in memory. Load up the given XML. + /// + /// + /// + /// + static internal Project CreateInMemoryProject(string xml, ILogger logger /* May be null */) + { + return CreateInMemoryProject(new ProjectCollection(), xml, logger); + } + + /// + /// Create an in-memory project and attach it to the passed-in engine. + /// + /// + /// + /// May be null + /// + static internal Project CreateInMemoryProject(ProjectCollection e, string xml, ILogger logger /* May be null */) + { + return CreateInMemoryProject(e, xml, logger, null); + } + + /// + /// Create an in-memory project and attach it to the passed-in engine. + /// + /// May be null + /// May be null + static internal Project CreateInMemoryProject + ( + ProjectCollection projectCollection, + string xml, + ILogger logger /* May be null */, + string toolsVersion /* may be null */ + ) + { + // Anonymous in-memory projects use the current directory for $(MSBuildProjectDirectory). + // We need to set the directory to something reasonable. + string originalDir = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); + + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.DtdProcessing = DtdProcessing.Ignore; + + Project project = new Project + ( + XmlReader.Create(new StringReader(CleanupFileContents(xml)), readerSettings), + null, + toolsVersion, + projectCollection + ); + + Guid guid = Guid.NewGuid(); + project.FullPath = Path.Combine(Path.GetTempPath(), "Temporary" + guid.ToString("N") + ".csproj"); + project.ReevaluateIfNecessary(); + + if (logger != null) + { + project.ProjectCollection.RegisterLogger(logger); + } + + // Return to the original directory. + Directory.SetCurrentDirectory(originalDir); + + return project; + } + /// + /// Creates a project in memory and builds the default targets. The build is + /// expected to succeed. + /// + /// + /// + internal static MockLogger BuildProjectExpectSuccess + ( + string projectContents + ) + { + MockLogger logger = new MockLogger(); + BuildProjectExpectSuccess(projectContents, logger); + return logger; + } + + internal static void BuildProjectExpectSuccess + ( + string projectContents, + ILogger logger + ) + { + Project project = ObjectModelHelpers.CreateInMemoryProject(projectContents, logger); + + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + } + + /// + /// Creates a project in memory and builds the default targets. The build is + /// expected to fail. + /// + /// + /// + internal static MockLogger BuildProjectExpectFailure + ( + string projectContents + ) + { + MockLogger logger = new MockLogger(); + BuildProjectExpectFailure(projectContents, logger); + + return logger; + } + + internal static void BuildProjectExpectFailure + ( + string projectContents, + ILogger logger + ) + { + Project project = ObjectModelHelpers.CreateInMemoryProject(projectContents, logger); + + bool success = project.Build(logger); + Assert.IsFalse(success, "Build succeeded, but shouldn't have. See Standard Out tab for details"); + } + + /// + /// This helper method compares the final project contents with the expected + /// value. + /// + /// + /// + internal static void CompareProjectContents + ( + Project project, + string newExpectedProjectContents + ) + { + // Get the new XML for the project, normalizing the whitespace. + string newActualProjectContents = project.Xml.RawXml; + + // Replace single-quotes with double-quotes, and normalize whitespace. + XmlDocument xmldoc = new XmlDocument(); + xmldoc.LoadXml(ObjectModelHelpers.CleanupFileContents(newExpectedProjectContents)); + newExpectedProjectContents = ObjectModelHelpers.NormalizeXmlWhitespace(xmldoc); + + // Compare the actual XML with the expected XML. + Console.WriteLine("================================= EXPECTED ==========================================="); + Console.WriteLine(newExpectedProjectContents); + Console.WriteLine(); + Console.WriteLine("================================== ACTUAL ============================================"); + Console.WriteLine(newActualProjectContents); + Console.WriteLine(); + Assert.AreEqual(newExpectedProjectContents, newActualProjectContents, "Project XML does not match expected XML. See 'Standard Out' tab for details."); + } + + + private static string s_tempProjectDir = null; + + /// + /// Returns the path %TEMP%\TempDirForMSBuildUnitTests + /// + internal static string TempProjectDir + { + get + { + if (s_tempProjectDir == null) + { + s_tempProjectDir = Path.Combine(Path.GetTempPath(), "TempDirForMSBuildUnitTests"); + } + + return s_tempProjectDir; + } + } + + /// + /// Deletes the directory %TEMP%\TempDirForMSBuildUnitTests, and all its contents. + /// + internal static void DeleteTempProjectDirectory() + { + DeleteDirectory(TempProjectDir); + } + + /// + /// Deletes the directory and all its contents. + /// + internal static void DeleteDirectory(string dir) + { + // Manually deleting all children, but intentionally leaving the + // Temp project directory behind due to locking issues which were causing + // failures in main on Amd64-WOW runs. + + // retries to deal with occasional locking issues where the file / directory can't be deleted to initially + for (int retries = 0; retries < 5; retries++) + { + try + { + if (Directory.Exists(dir)) + { + foreach (string directory in Directory.GetDirectories(dir)) + { + Directory.Delete(directory, true); + } + + foreach (string file in Directory.GetFiles(dir)) + { + File.Delete(file); + } + } + + break; + } + catch (Exception ex) + { + if (retries < 4) + { + Console.WriteLine(ex.ToString()); + } + else + { + // All the retries have failed. We will now fail with the + // actual problem now instead of with some more difficult-to-understand + // issue later. + throw ex; + } + } + } + } + + /// + /// Creates a file in the %TEMP%\TempDirForMSBuildUnitTests directory, after cleaning + /// up the file contents (replacing single-back-quote with double-quote, etc.). + /// Silently OVERWRITES existing file. + /// + /// + /// + internal static string CreateFileInTempProjectDirectory(string fileRelativePath, string fileContents) + { + Assert.IsFalse(String.IsNullOrEmpty(fileRelativePath)); + string fullFilePath = Path.Combine(TempProjectDir, fileRelativePath); + Directory.CreateDirectory(Path.GetDirectoryName(fullFilePath)); + + // retries to deal with occasional locking issues where the file can't be written to initially + for (int retries = 0; retries < 5; retries++) + { + try + { + File.WriteAllText(fullFilePath, CleanupFileContents(fileContents)); + break; + } + catch (Exception ex) + { + if (retries < 4) + { + Console.WriteLine(ex.ToString()); + } + else + { + // All the retries have failed. We will now fail with the + // actual problem now instead of with some more difficult-to-understand + // issue later. + throw ex; + } + } + } + + return fullFilePath; + } + + /// + /// Builds a project file from disk, and asserts if the build does not succeed. + /// + /// + /// + internal static MockLogger BuildTempProjectFileExpectSuccess(string projectFileRelativePath) + { + return BuildTempProjectFileWithTargetsExpectSuccess(projectFileRelativePath, null, null); + } + + /// + /// Builds a project file from disk, and asserts if the build does not succeed. + /// + internal static MockLogger BuildTempProjectFileWithTargetsExpectSuccess(string projectFileRelativePath, string[] targets, IDictionary additionalProperties) + { + MockLogger logger = new MockLogger(); + bool success = BuildTempProjectFileWithTargets(projectFileRelativePath, targets, additionalProperties, logger); + + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + + return logger; + } + + /// + /// Builds a project file from disk, and asserts if the build succeeds. + /// + internal static MockLogger BuildTempProjectFileExpectFailure(string projectFileRelativePath) + { + MockLogger logger = new MockLogger(); + bool success = BuildTempProjectFileWithTargets(projectFileRelativePath, null, null, logger); + + Assert.IsFalse(success, "Build unexpectedly succeeded. See Standard Out tab for details"); + + return logger; + } + + /// + /// Builds a project file from disk, and asserts if the build succeeds. + /// + internal static MockLogger BuildTempProjectFileWithTargetsExpectFailure(string projectFileRelativePath, string[] targets, IDictionary additionalProperties) + { + MockLogger logger = new MockLogger(); + bool success = BuildTempProjectFileWithTargets(projectFileRelativePath, targets, additionalProperties, logger); + + Assert.IsFalse(success, "Build unexpectedly succeeded. See Standard Out tab for details"); + + return logger; + } + + /// + /// Loads a project file from disk + /// + /// + /// + internal static Project LoadProjectFileInTempProjectDirectory(string projectFileRelativePath) + { + return LoadProjectFileInTempProjectDirectory(projectFileRelativePath, false /* don't touch project*/); + } + + /// + /// Loads a project file from disk + /// + /// + /// + internal static Project LoadProjectFileInTempProjectDirectory(string projectFileRelativePath, bool touchProject) + { + string projectFileFullPath = Path.Combine(ObjectModelHelpers.TempProjectDir, projectFileRelativePath); + + ProjectCollection projectCollection = new ProjectCollection(); + + Project project = new Project(projectFileFullPath, null, null, projectCollection); + + if (touchProject) + { + File.SetLastWriteTime(projectFileFullPath, DateTime.Now); + } + + return project; + } + + /// + /// Builds a project file from disk, and asserts if the build does not succeed. + /// + /// + /// + /// Can be null. + /// + /// + internal static bool BuildTempProjectFileWithTargets + ( + string projectFileRelativePath, + string[] targets, + IDictionary globalProperties, + ILogger logger + ) + { + // Build the default targets. + List loggers = new List(1); + loggers.Add(logger); + + if (String.Equals(Path.GetExtension(projectFileRelativePath), ".sln")) + { + string projectFileFullPath = Path.Combine(ObjectModelHelpers.TempProjectDir, projectFileRelativePath); + BuildRequestData data = new BuildRequestData(projectFileFullPath, globalProperties ?? new Dictionary(), null, targets, null); + BuildParameters parameters = new BuildParameters(); + parameters.Loggers = loggers; + BuildResult result = BuildManager.DefaultBuildManager.Build(parameters, data); + return result.OverallResult == BuildResultCode.Success; + } + else + { + Project project = LoadProjectFileInTempProjectDirectory(projectFileRelativePath); + + if (globalProperties != null) + { + // add extra properties + foreach (KeyValuePair globalProperty in globalProperties) + { + project.SetGlobalProperty(globalProperty.Key, globalProperty.Value); + } + } + + return project.Build(targets, loggers); + } + } + + /// + /// Delete any files in the list that currently exist. + /// + /// + internal static void DeleteTempFiles(string[] files) + { + for (int i = 0; i < files.Length; i++) + { + if (File.Exists(files[i])) File.Delete(files[i]); + } + } + + /// + /// Returns the requested number of temporary files. + /// + internal static string[] GetTempFiles(int number) + { + return GetTempFiles(number, DateTime.Now); + } + + /// + /// Returns the requested number of temporary files, with the requested write time. + /// + internal static string[] GetTempFiles(int number, DateTime lastWriteTime) + { + string[] files = new string[number]; + + for (int i = 0; i < number; i++) + { + files[i] = FileUtilities.GetTemporaryFile(); + File.SetLastWriteTime(files[i], lastWriteTime); + } + return files; + } + } + + /// + /// Various generic unit test helper methods + /// + internal static partial class Helpers + { + /// + /// Returns the count of objects returned by an enumerator + /// + internal static int Count(IEnumerable enumerable) + { + int i = 0; + foreach (object o in enumerable) + { + i++; + } + + return i; + } + + /// + /// Makes a temporary list out of an enumerable + /// + internal static List MakeList(IEnumerable enumerable) + { + List list = new List(); + foreach (T item in enumerable) + { + list.Add(item); + } + return list; + } + + /// + /// Gets the first element in the enumeration, or null if there are none + /// + internal static T GetFirst(IEnumerable enumerable) + where T : class + { + T first = null; + + foreach (T element in enumerable) + { + first = element; + break; + } + + return first; + } + + /// + /// Gets the last element in the enumeration, or null if there are none + /// + internal static T GetLast(IEnumerable enumerable) + where T : class + { + T last = null; + + foreach (T item in enumerable) + { + last = item; + } + + return last; + } + + /// + /// Makes a temporary dictionary out of an enumerable of keyvaluepairs. + /// Throws + /// + internal static Dictionary MakeDictionary(IEnumerable> enumerable) + { + Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair item in enumerable) + { + dictionary.Add(item.Key, item.Value); + } + return dictionary; + } + + /// + /// Verify that the two lists are value identical + /// + internal static void AssertListsValueEqual(IList one, IList two) + { + Assert.AreEqual(one.Count, two.Count); + + for (int i = 0; i < one.Count; i++) + { + Assert.AreEqual(one[i], two[i]); + } + } + + /// + /// Verify that the two collections are value identical + /// + internal static void AssertCollectionsValueEqual(ICollection one, ICollection two) + { + Assert.AreEqual(one.Count, two.Count); + + foreach (T item in one) + { + Assert.IsTrue(two.Contains(item)); + } + + foreach (T item in two) + { + Assert.IsTrue(one.Contains(item)); + } + } + + /// + /// Verify that the two enumerables are value identical + /// + internal static void AssertEnumerationsValueEqual(IEnumerable one, IEnumerable two) + { + List listOne = new List(); + List listTwo = new List(); + + foreach (T item in one) + { + listOne.Add(item); + } + + foreach (T item in two) + { + listTwo.Add(item); + } + + AssertCollectionsValueEqual(listOne, listTwo); + } + + /// + /// Build a project with the provided content in memory. + /// Assert that it succeeded, and return the mock logger with the output. + /// + internal static MockLogger BuildProjectWithNewOMExpectSuccess(string content) + { + MockLogger logger; + bool result; + BuildProjectWithNewOM(content, out logger, out result, false); + Assert.IsTrue(result); + + return logger; + } + + /// + /// Build a project in memory using the new OM + /// + private static void BuildProjectWithNewOM(string content, out MockLogger logger, out bool result, bool allowTaskCrash) + { + // Replace the crazy quotes with real ones + content = ObjectModelHelpers.CleanupFileContents(content); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + logger = new MockLogger(); + logger.AllowTaskCrashes = allowTaskCrash; + List loggers = new List(); + loggers.Add(logger); + result = project.Build(loggers); + } + + /// + /// Build a project with the provided content in memory. + /// Assert that it fails, and return the mock logger with the output. + /// + internal static MockLogger BuildProjectWithNewOMExpectFailure(string content, bool allowTaskCrash) + { + MockLogger logger; + bool result; + BuildProjectWithNewOM(content, out logger, out result, allowTaskCrash); + Assert.IsFalse(result); + return logger; + } + + /// + /// Compare the expected project XML to actual project XML, after doing a little normalization + /// of + /// + /// + /// + internal static void CompareProjectXml(string newExpectedProjectContents, string newActualProjectContents) + { + // Replace single-quotes with double-quotes, and normalize whitespace. + XmlDocument xmldoc = new XmlDocument(); + xmldoc.LoadXml(ObjectModelHelpers.CleanupFileContents(newExpectedProjectContents)); + newExpectedProjectContents = ObjectModelHelpers.NormalizeXmlWhitespace(xmldoc); + + // Compare the actual XML with the expected XML. + if (newExpectedProjectContents != newActualProjectContents) + { + Console.WriteLine("================================= EXPECTED ==========================================="); + Console.WriteLine(newExpectedProjectContents); + Console.WriteLine(); + Console.WriteLine("================================== ACTUAL ============================================"); + Console.WriteLine(newActualProjectContents); + Console.WriteLine(); + Assert.AreEqual(newExpectedProjectContents, newActualProjectContents, "Project XML does not match expected XML. See 'Standard Out' tab for details."); + } + } + + /// + /// Verify that the saved project content matches the provided content + /// + internal static void VerifyAssertProjectContent(string expected, Project project) + { + VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Verify that the saved project content matches the provided content + /// + internal static void VerifyAssertProjectContent(string expected, ProjectRootElement project) + { + VerifyAssertLineByLine(expected, project.RawXml, true /* ignoreFirstLineOfActual */); + } + + /// + /// Verify that the expected content matches the actual content + /// + internal static void VerifyAssertLineByLine(string expected, string actual) + { + VerifyAssertLineByLine(expected, actual, false /* do not ignore first line */); + } + + /// + /// Creates a bunch of temporary files with the specified names and returns + /// their full paths (so they can ultimately be cleaned up) + /// + internal static string[] CreateFiles(params string[] files) + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(directory); + + string[] result = new string[files.Length]; + + for (int i = 0; i < files.Length; i++) + { + string fullPath = Path.Combine(directory, files[i]); + File.WriteAllText(fullPath, String.Empty); + result[i] = fullPath; + } + + return result; + } + + /// + /// Deletes a bunch of files, including their containing directories + /// if they become empty + /// + internal static void DeleteFiles(params string[] paths) + { + foreach (string path in paths) + { + if (File.Exists(path)) + { + File.Delete(path); + } + + string directory = Path.GetDirectoryName(path); + if (Directory.Exists(directory) && (Directory.GetFileSystemEntries(directory).Length == 0)) + { + Directory.Delete(directory); + } + } + } + + /// + /// Given two methods accepting no parameters and returning none, verifies they + /// both throw, and throw the same exception type. + /// + internal static void VerifyAssertThrowsSameWay(Action method1, Action method2) + { + Exception ex1 = null; + Exception ex2 = null; + + try + { + method1(); + } + catch (Exception ex) + { + ex1 = ex; + } + + try + { + method2(); + } + catch (Exception ex) + { + ex2 = ex; + } + + if (ex1 == null && ex2 == null) + { + Assert.Fail("Neither threw"); + } + + Assert.AreNotEqual(null, ex1, "First method did not throw, second: {0}", ex2 == null ? "" : ex2.GetType() + ex2.Message); + Assert.AreNotEqual(null, ex2, "Second method did not throw, first: {0}", ex1 == null ? "" : ex1.GetType() + ex1.Message); + Assert.AreEqual(ex1.GetType(), ex2.GetType(), "Both methods threw but the first threw {0} '{1}' and the second threw {2} '{3}'", ex1.GetType(), ex1.Message, ex2.GetType(), ex2.Message); + + Console.WriteLine("COMPARE EXCEPTIONS:\n\n#1: {0}\n\n#2: {1}", ex1.Message, ex2.Message); + } + + /// + /// Given a method accepting no parameters and returning none, verifies it + /// throws an exception of the specified type. + /// + internal static void VerifyAssertThrows(Action method, Type expectedExceptionType) + { + try + { + method(); + } + catch (Exception ex) + { + Assert.IsInstanceOfType(ex, expectedExceptionType); + Console.WriteLine("Caught '{0}'", ex.Message); + return; + } + + Debugger.Break(); + Assert.Fail("Did not throw but expected {0} exception", expectedExceptionType.ToString()); + } + + /// + /// Verify method throws invalid operation exception. + /// + internal static void VerifyAssertThrowsInvalidOperation(Action method) + { + VerifyAssertThrows(method, typeof(InvalidOperationException)); + } + + /// + /// Verify that the expected content matches the actual content + /// + private static void VerifyAssertLineByLine(string expected, string actual, bool ignoreFirstLineOfActual) + { + string[] actualLines = SplitIntoLines(actual); + + if (ignoreFirstLineOfActual) + { + // Remove the first line of the actual content we got back, + // since it's just the xml declaration, which we don't care about + string[] temporary = new string[actualLines.Length - 1]; + + for (int i = 0; i < temporary.Length; i++) + { + temporary[i] = actualLines[i + 1]; + } + + actualLines = temporary; + } + + string[] expectedLines = SplitIntoLines(expected); + + bool expectedAndActualDontMatch = false; + for (int i = 0; i < Math.Min(actualLines.Length, expectedLines.Length); i++) + { + if (expectedLines[i] != actualLines[i]) + { + expectedAndActualDontMatch = true; + Console.WriteLine("< " + expectedLines[i] + "\n> " + actualLines[i] + "\n"); + } + } + + if (actualLines.Length == expectedLines.Length && expectedAndActualDontMatch) + { + string output = "\r\n#################################Expected#################################\n" + String.Join("\r\n", expectedLines); + output += "\r\n#################################Actual#################################\n" + String.Join("\r\n", actualLines); + + Assert.Fail(output); + } + + if (actualLines.Length > expectedLines.Length) + { + Console.WriteLine("\n#################################Expected#################################\n" + String.Join("\n", expectedLines)); + Console.WriteLine("#################################Actual#################################\n" + String.Join("\n", actualLines)); + + Assert.Fail("Expected content was shorter, actual had this extra line: '" + actualLines[expectedLines.Length] + "'"); + } + else if (actualLines.Length < expectedLines.Length) + { + Console.WriteLine("\n#################################Expected#################################\n" + String.Join("\n", expectedLines)); + Console.WriteLine("#################################Actual#################################\n" + String.Join("\n", actualLines)); + + Assert.Fail("Actual content was shorter, expected had this extra line: '" + expectedLines[actualLines.Length] + "'"); + } + } + + /// + /// Clear the dirty flag of a ProjectRootElement by saving to a dummy writer. + /// + internal static void ClearDirtyFlag(ProjectRootElement project) + { + project.Save(new StringWriter()); + Assert.IsFalse(project.HasUnsavedChanges); + } + + /// + /// Command you can pass to Exec to sleep for rough number of milliseconds. + /// @for /l %i in (1,1,X) do "@dir %windir% > nul" + /// sleeps for X/100 seconds, roughly + /// This works around not having sleep.exe on the path. + /// + internal static string SleepCommandInMilliseconds(int milliseconds) + { + return String.Format(@"@for /l %25%25i in (1,1,{0}) do @dir %25windir%25 > nul", milliseconds / 10); + } + + /// + /// Command you can pass to Exec to sleep for rough number of seconds. + /// @for /l %i in (1,1,X) do "@dir %windir% > nul" + /// sleeps for X/100 seconds, roughly + /// This works around not having sleep.exe on the path. + /// + /// + /// + internal static string SleepCommand(int seconds) + { + return SleepCommandInMilliseconds(seconds * 1000); + } + + /// + /// Break the provided string into an array, on newlines + /// + private static string[] SplitIntoLines(string content) + { + string[] result = content.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + + return result; + } + } +} diff --git a/src/Shared/UnitTests/ResourceUtilities_Tests.cs b/src/Shared/UnitTests/ResourceUtilities_Tests.cs new file mode 100644 index 00000000000..174a4b18669 --- /dev/null +++ b/src/Shared/UnitTests/ResourceUtilities_Tests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ResourceUtilitiesTests + { + [TestMethod] + public void ExtractMSBuildCode() + { + // most common message pattern + string code; + string messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB7007: This is a message.", out code); + Assert.AreEqual("MSB7007", code); + Assert.AreEqual("This is a message.", messageOnly); + + // no whitespace between colon and message is ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB7007:This is a message.", out code); + Assert.AreEqual("MSB7007", code); + Assert.AreEqual("This is a message.", messageOnly); + + // whitespace before code and after colon is ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, " MSB7007: This is a message.", out code); + Assert.AreEqual("MSB7007", code); + Assert.AreEqual("This is a message.", messageOnly); + + // whitespace between code and colon is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB7007 : This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("MSB7007 : This is a message.", messageOnly); + + // whitespace in code is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB 7007: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("MSB 7007: This is a message.", messageOnly); + + // code with less than 4 digits is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB007: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("MSB007: This is a message.", messageOnly); + + // code without digits is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("MSB: This is a message.", messageOnly); + + // code without MSB prefix is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "1001: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("1001: This is a message.", messageOnly); + + // digits before MSB prefix is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "7001MSB: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("7001MSB: This is a message.", messageOnly); + + // mixing letters and digits is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "MSB564B: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("MSB564B: This is a message.", messageOnly); + + // lowercase code is not ok + code = null; + messageOnly = ResourceUtilities.ExtractMessageCode(true /* msbuild code only */, "msb1001: This is a message.", out code); + Assert.IsNull(code); + Assert.AreEqual("msb1001: This is a message.", messageOnly); + } + } +} diff --git a/src/Shared/UnitTests/StreamHelpers.cs b/src/Shared/UnitTests/StreamHelpers.cs new file mode 100644 index 00000000000..c1ab572ec8f --- /dev/null +++ b/src/Shared/UnitTests/StreamHelpers.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + sealed internal class StreamHelpers + { + /// + /// Take a string and convert it to a StreamReader. + /// + /// + /// + static internal StreamReader StringToStreamReader(string value) + { + MemoryStream m = new MemoryStream(); + TextWriter w = new StreamWriter(m, System.Text.Encoding.Default); + + w.Write(value); + w.Flush(); + m.Seek(0, SeekOrigin.Begin); + + return new StreamReader(m); + } + } +} diff --git a/src/Shared/UnitTests/TaskParameter_Tests.cs b/src/Shared/UnitTests/TaskParameter_Tests.cs new file mode 100644 index 00000000000..f3d94418f4b --- /dev/null +++ b/src/Shared/UnitTests/TaskParameter_Tests.cs @@ -0,0 +1,428 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit Tests for TaskParameter class, specifically focusing on +// testing its serialization. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.UnitTests.BackEnd; +using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Class to specifically test the TaskParameter class, particularly its serialization + /// of various types of parameters. + /// + [TestClass] + public class TaskParameter_Tests + { + /// + /// Verifies that construction and serialization with a null parameter is OK. + /// + [TestMethod] + public void NullParameter() + { + TaskParameter t = new TaskParameter(null); + + Assert.IsNull(t.WrappedParameter); + Assert.AreEqual(TaskParameterType.Null, t.ParameterType); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.IsNull(t2.WrappedParameter); + Assert.AreEqual(TaskParameterType.Null, t2.ParameterType); + } + + /// + /// Verifies that construction and serialization with a string parameter is OK. + /// + [TestMethod] + public void StringParameter() + { + TaskParameter t = new TaskParameter("foo"); + + Assert.AreEqual("foo", t.WrappedParameter); + Assert.AreEqual(TaskParameterType.String, t.ParameterType); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual("foo", t2.WrappedParameter); + Assert.AreEqual(TaskParameterType.String, t2.ParameterType); + } + + /// + /// Verifies that construction and serialization with a string array parameter is OK. + /// + [TestMethod] + public void StringArrayParameter() + { + TaskParameter t = new TaskParameter(new string[] { "foo", "bar" }); + + Assert.AreEqual(TaskParameterType.StringArray, t.ParameterType); + + string[] wrappedParameter = t.WrappedParameter as string[]; + Assert.IsNotNull(wrappedParameter); + Assert.AreEqual(2, wrappedParameter.Length); + Assert.AreEqual("foo", wrappedParameter[0]); + Assert.AreEqual("bar", wrappedParameter[1]); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.StringArray, t2.ParameterType); + + string[] wrappedParameter2 = t2.WrappedParameter as string[]; + Assert.IsNotNull(wrappedParameter2); + Assert.AreEqual(2, wrappedParameter2.Length); + Assert.AreEqual("foo", wrappedParameter2[0]); + Assert.AreEqual("bar", wrappedParameter2[1]); + } + + /// + /// Verifies that construction and serialization with a value type (integer) parameter is OK. + /// + [TestMethod] + public void ValueTypeParameter() + { + TaskParameter t = new TaskParameter(1); + + Assert.AreEqual(1, t.WrappedParameter); + Assert.AreEqual(TaskParameterType.ValueType, t.ParameterType); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(1, t2.WrappedParameter); + Assert.AreEqual(TaskParameterType.ValueType, t2.ParameterType); + } + + /// + /// Verifies that construction and serialization with a parameter that is an array of value types (ints) is OK. + /// + [TestMethod] + public void ValueTypeArrayParameter() + { + TaskParameter t = new TaskParameter(new int[] { 2, 15 }); + + Assert.AreEqual(TaskParameterType.ValueTypeArray, t.ParameterType); + + int[] wrappedParameter = t.WrappedParameter as int[]; + Assert.IsNotNull(wrappedParameter); + Assert.AreEqual(2, wrappedParameter.Length); + Assert.AreEqual(2, wrappedParameter[0]); + Assert.AreEqual(15, wrappedParameter[1]); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ValueTypeArray, t2.ParameterType); + + int[] wrappedParameter2 = t2.WrappedParameter as int[]; + Assert.IsNotNull(wrappedParameter2); + Assert.AreEqual(2, wrappedParameter2.Length); + Assert.AreEqual(2, wrappedParameter2[0]); + Assert.AreEqual(15, wrappedParameter2[1]); + } + + /// + /// Verifies that construction and serialization with an ITaskItem parameter is OK. + /// + [TestMethod] + public void ITaskItemParameter() + { + TaskParameter t = new TaskParameter(new TaskItem("foo")); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem foo = t.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo); + Assert.AreEqual("foo", foo.ItemSpec); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem foo2 = t2.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo", foo2.ItemSpec); + } + + /// + /// Verifies that construction and serialization with an ITaskItem parameter that has custom metadata is OK. + /// + [TestMethod] + public void ITaskItemParameterWithMetadata() + { + TaskItem baseItem = new TaskItem("foo"); + baseItem.SetMetadata("a", "a1"); + baseItem.SetMetadata("b", "b1"); + + TaskParameter t = new TaskParameter(baseItem); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem foo = t.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo); + Assert.AreEqual("foo", foo.ItemSpec); + Assert.AreEqual("a1", foo.GetMetadata("a")); + Assert.AreEqual("b1", foo.GetMetadata("b")); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem foo2 = t2.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo", foo2.ItemSpec); + Assert.AreEqual("a1", foo2.GetMetadata("a")); + Assert.AreEqual("b1", foo2.GetMetadata("b")); + } + + /// + /// Verifies that construction and serialization with a parameter that is an array of ITaskItems is OK. + /// + [TestMethod] + public void ITaskItemArrayParameter() + { + TaskParameter t = new TaskParameter(new ITaskItem[] { new TaskItem("foo"), new TaskItem("bar") }); + + Assert.AreEqual(TaskParameterType.ITaskItemArray, t.ParameterType); + + ITaskItem[] wrappedParameter = t.WrappedParameter as ITaskItem[]; + Assert.IsNotNull(wrappedParameter); + Assert.AreEqual(2, wrappedParameter.Length); + Assert.AreEqual("foo", wrappedParameter[0].ItemSpec); + Assert.AreEqual("bar", wrappedParameter[1].ItemSpec); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItemArray, t.ParameterType); + + ITaskItem[] wrappedParameter2 = t.WrappedParameter as ITaskItem[]; + Assert.IsNotNull(wrappedParameter2); + Assert.AreEqual(2, wrappedParameter2.Length); + Assert.AreEqual("foo", wrappedParameter2[0].ItemSpec); + Assert.AreEqual("bar", wrappedParameter2[1].ItemSpec); + } + + /// + /// Verifies that construction and serialization with a parameter that is an ITaskItem with an + /// itemspec containing escapable characters translates the escaping correctly. + /// + [TestMethod] + public void ITaskItemParameter_EscapedItemSpec() + { + TaskParameter t = new TaskParameter(new TaskItem("foo%3bbar")); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem foo = t.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo); + Assert.AreEqual("foo;bar", foo.ItemSpec); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem foo2 = t2.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo;bar", foo2.ItemSpec); + } + + /// + /// Verifies that construction and serialization with a parameter that is an ITaskItem with an + /// itemspec containing doubly-escaped characters translates the escaping correctly. + /// + [TestMethod] + public void ITaskItemParameter_DoubleEscapedItemSpec() + { + TaskParameter t = new TaskParameter(new TaskItem("foo%253bbar")); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem foo = t.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo); + Assert.AreEqual("foo%3bbar", foo.ItemSpec); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem foo2 = t2.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo%3bbar", foo2.ItemSpec); + + TaskParameter t3 = new TaskParameter(t2.WrappedParameter); + + ((INodePacketTranslatable)t3).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t4 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t4.ParameterType); + + ITaskItem foo4 = t4.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo4); + Assert.AreEqual("foo%3bbar", foo4.ItemSpec); + } + + /// + /// Verifies that construction and serialization with a parameter that is an ITaskItem with an + /// itemspec containing the non-escaped forms of escapable characters translates the escaping correctly. + /// + [TestMethod] + public void ITaskItemParameter_EscapableNotEscapedItemSpec() + { + TaskParameter t = new TaskParameter(new TaskItem("foo;bar")); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem2 foo = t.WrappedParameter as ITaskItem2; + Assert.IsNotNull(foo); + Assert.AreEqual("foo;bar", foo.ItemSpec); + Assert.AreEqual("foo;bar", foo.EvaluatedIncludeEscaped); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem2 foo2 = t2.WrappedParameter as ITaskItem2; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo;bar", foo2.ItemSpec); + Assert.AreEqual("foo;bar", foo2.EvaluatedIncludeEscaped); + } + + /// + /// Verifies that construction and serialization with a parameter that is an ITaskItem with + /// metadata containing escapable characters translates the escaping correctly. + /// + [TestMethod] + public void ITaskItemParameter_EscapedMetadata() + { + IDictionary metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + metadata.Add("a", "a1%25b1"); + metadata.Add("b", "c1%28d1"); + + TaskParameter t = new TaskParameter(new TaskItem("foo", metadata)); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem foo = t.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo); + Assert.AreEqual("foo", foo.ItemSpec); + Assert.AreEqual("a1%b1", foo.GetMetadata("a")); + Assert.AreEqual("c1(d1", foo.GetMetadata("b")); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem foo2 = t2.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo", foo2.ItemSpec); + Assert.AreEqual("a1%b1", foo2.GetMetadata("a")); + Assert.AreEqual("c1(d1", foo2.GetMetadata("b")); + } + + /// + /// Verifies that construction and serialization with a parameter that is an ITaskItem with + /// metadata containing doubly-escapabed characters translates the escaping correctly. + /// + [TestMethod] + public void ITaskItemParameter_DoubleEscapedMetadata() + { + IDictionary metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + metadata.Add("a", "a1%2525b1"); + metadata.Add("b", "c1%2528d1"); + + TaskParameter t = new TaskParameter(new TaskItem("foo", metadata)); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem foo = t.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo); + Assert.AreEqual("foo", foo.ItemSpec); + Assert.AreEqual("a1%25b1", foo.GetMetadata("a")); + Assert.AreEqual("c1%28d1", foo.GetMetadata("b")); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem foo2 = t2.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo", foo2.ItemSpec); + Assert.AreEqual("a1%25b1", foo2.GetMetadata("a")); + Assert.AreEqual("c1%28d1", foo2.GetMetadata("b")); + + TaskParameter t3 = new TaskParameter(t2.WrappedParameter); + + ((INodePacketTranslatable)t3).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t4 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t4.ParameterType); + + ITaskItem foo4 = t4.WrappedParameter as ITaskItem; + Assert.IsNotNull(foo4); + Assert.AreEqual("foo", foo4.ItemSpec); + Assert.AreEqual("a1%25b1", foo4.GetMetadata("a")); + Assert.AreEqual("c1%28d1", foo4.GetMetadata("b")); + } + + /// + /// Verifies that construction and serialization with a parameter that is an ITaskItem with + /// metadata containing the non-escaped versions of escapable characters translates the + /// escaping correctly. + /// + [TestMethod] + public void ITaskItemParameter_EscapableNotEscapedMetadata() + { + IDictionary metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + metadata.Add("a", "a1(b1"); + metadata.Add("b", "c1)d1"); + + TaskParameter t = new TaskParameter(new TaskItem("foo", metadata)); + + Assert.AreEqual(TaskParameterType.ITaskItem, t.ParameterType); + + ITaskItem2 foo = t.WrappedParameter as ITaskItem2; + Assert.IsNotNull(foo); + Assert.AreEqual("foo", foo.ItemSpec); + Assert.AreEqual("a1(b1", foo.GetMetadata("a")); + Assert.AreEqual("c1)d1", foo.GetMetadata("b")); + Assert.AreEqual("a1(b1", foo.GetMetadataValueEscaped("a")); + Assert.AreEqual("c1)d1", foo.GetMetadataValueEscaped("b")); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + TaskParameter t2 = TaskParameter.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(TaskParameterType.ITaskItem, t2.ParameterType); + + ITaskItem2 foo2 = t2.WrappedParameter as ITaskItem2; + Assert.IsNotNull(foo2); + Assert.AreEqual("foo", foo2.ItemSpec); + Assert.AreEqual("a1(b1", foo2.GetMetadata("a")); + Assert.AreEqual("c1)d1", foo2.GetMetadata("b")); + Assert.AreEqual("a1(b1", foo2.GetMetadataValueEscaped("a")); + Assert.AreEqual("c1)d1", foo2.GetMetadataValueEscaped("b")); + } + } +} \ No newline at end of file diff --git a/src/Shared/UnitTests/TypeLoader_Tests.cs b/src/Shared/UnitTests/TypeLoader_Tests.cs new file mode 100644 index 00000000000..9a8eaa66e14 --- /dev/null +++ b/src/Shared/UnitTests/TypeLoader_Tests.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Shared; +using System.Reflection; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class TypeLoader_Tests + { + [TestMethod] + public void Basic() + { + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("Csc", "csc")); // ==> exact match + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Microsoft.Build.Tasks.Csc")); // ==> exact match + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Csc")); // ==> partial match + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Tasks.Csc")); // ==> partial match + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask+NestedTask", "NestedTask")); // ==> partial match + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask\\\\+NestedTask", "NestedTask")); // ==> partial match + Assert.IsFalse(TypeLoader.IsPartialTypeNameMatch("MyTasks.CscTask", "Csc")); // ==> no match + Assert.IsFalse(TypeLoader.IsPartialTypeNameMatch("MyTasks.MyCsc", "Csc")); // ==> no match + Assert.IsFalse(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask\\.Csc", "Csc")); // ==> no match + Assert.IsFalse(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask\\\\\\.Csc", "Csc")); // ==> no match + } + + [TestMethod] + public void Regress_Mutation_TrailingPartMustMatch() + { + Assert.IsFalse(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Vbc")); + } + + [TestMethod] + public void Regress_Mutation_ParameterOrderDoesntMatter() + { + Assert.IsTrue(TypeLoader.IsPartialTypeNameMatch("Csc", "Microsoft.Build.Tasks.Csc")); + } + + + /// + /// Make sure that when we load multiple types out of the same assembly with different typefilters that both the fullyqualified name matching and the + /// partial name matching still work. + /// + [TestMethod] + public void Regress640476PartialName() + { + string forwardingLoggerLocation = typeof(Microsoft.Build.Logging.ConfigurableForwardingLogger).Assembly.Location; + TypeLoader loader = new TypeLoader(new TypeFilter(IsForwardingLoggerClass)); + LoadedType loadedType = loader.Load("ConfigurableForwardingLogger", AssemblyLoadInfo.Create(null, forwardingLoggerLocation)); + Assert.IsNotNull(loadedType); + Assert.IsTrue(loadedType.Assembly.AssemblyLocation.Equals(forwardingLoggerLocation, StringComparison.OrdinalIgnoreCase)); + + string fileLoggerLocation = typeof(Microsoft.Build.Logging.FileLogger).Assembly.Location; + loader = new TypeLoader(new TypeFilter(IsLoggerClass)); + loadedType = loader.Load("FileLogger", AssemblyLoadInfo.Create(null, fileLoggerLocation)); + Assert.IsNotNull(loadedType); + Assert.IsTrue(loadedType.Assembly.AssemblyLocation.Equals(fileLoggerLocation, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Make sure that when we load multiple types out of the same assembly with different typefilters that both the fullyqualified name matching and the + /// partial name matching still work. + /// + [TestMethod] + public void Regress640476FullyQualifiedName() + { + Type forwardingLoggerType = typeof(Microsoft.Build.Logging.ConfigurableForwardingLogger); + string forwardingLoggerLocation = forwardingLoggerType.Assembly.Location; + TypeLoader loader = new TypeLoader(new TypeFilter(IsForwardingLoggerClass)); + LoadedType loadedType = loader.Load(forwardingLoggerType.FullName, AssemblyLoadInfo.Create(null, forwardingLoggerLocation)); + Assert.IsNotNull(loadedType); + Assert.IsTrue(loadedType.Assembly.AssemblyLocation.Equals(forwardingLoggerLocation, StringComparison.OrdinalIgnoreCase)); + + Type fileLoggerType = typeof(Microsoft.Build.Logging.FileLogger); + string fileLoggerLocation = fileLoggerType.Assembly.Location; + loader = new TypeLoader(new TypeFilter(IsLoggerClass)); + loadedType = loader.Load(fileLoggerType.FullName, AssemblyLoadInfo.Create(null, fileLoggerLocation)); + Assert.IsNotNull(loadedType); + Assert.IsTrue(loadedType.Assembly.AssemblyLocation.Equals(fileLoggerLocation, StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// Make sure if no typeName is passed in then pick the first type which matches the desired typefilter. + /// This has been in since whidby but there has been no test for it and it was broken in the last refactoring of TypeLoader. + /// This test is to prevent that from happening again. + /// + [TestMethod] + public void NoTypeNamePicksFirstType() + { + Type forwardingLoggerType = typeof(Microsoft.Build.Logging.ConfigurableForwardingLogger); + string forwardingLoggerAssemblyLocation = forwardingLoggerType.Assembly.Location; + TypeFilter forwardingLoggerfilter = new TypeFilter(IsForwardingLoggerClass); + Type firstPublicType = FirstPublicDesiredType(forwardingLoggerfilter, forwardingLoggerAssemblyLocation); + + TypeLoader loader = new TypeLoader(forwardingLoggerfilter); + LoadedType loadedType = loader.Load(String.Empty, AssemblyLoadInfo.Create(null, forwardingLoggerAssemblyLocation)); + Assert.IsNotNull(loadedType); + Assert.IsTrue(loadedType.Assembly.AssemblyLocation.Equals(forwardingLoggerAssemblyLocation, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(loadedType.Type.Equals(firstPublicType)); + + + Type fileLoggerType = typeof(Microsoft.Build.Logging.FileLogger); + string fileLoggerAssemblyLocation = forwardingLoggerType.Assembly.Location; + TypeFilter fileLoggerfilter = new TypeFilter(IsLoggerClass); + firstPublicType = FirstPublicDesiredType(fileLoggerfilter, fileLoggerAssemblyLocation); + + loader = new TypeLoader(fileLoggerfilter); + loadedType = loader.Load(String.Empty, AssemblyLoadInfo.Create(null, fileLoggerAssemblyLocation)); + Assert.IsNotNull(loadedType); + Assert.IsTrue(loadedType.Assembly.AssemblyLocation.Equals(fileLoggerAssemblyLocation, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(loadedType.Type.Equals(firstPublicType)); + } + + + private static Type FirstPublicDesiredType(TypeFilter filter, string assemblyLocation) + { + Assembly loadedAssembly = Assembly.UnsafeLoadFrom(assemblyLocation); + + // only look at public types + Type[] allPublicTypesInAssembly = loadedAssembly.GetExportedTypes(); + foreach (Type publicType in allPublicTypesInAssembly) + { + if (filter(publicType, null)) + { + return publicType; + } + } + + return null; + } + + + private static bool IsLoggerClass(Type type, object unused) + { + return (type.IsClass && + !type.IsAbstract && + (type.GetInterface("ILogger") != null)); + } + + private static bool IsForwardingLoggerClass(Type type, object unused) + { + return (type.IsClass && + !type.IsAbstract && + (type.GetInterface("IForwardingLogger") != null)); + } + } +} diff --git a/src/Shared/UnitTests/XmakeAttributes_Tests.cs b/src/Shared/UnitTests/XmakeAttributes_Tests.cs new file mode 100644 index 00000000000..12100e04c84 --- /dev/null +++ b/src/Shared/UnitTests/XmakeAttributes_Tests.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class XmakeAttributesTest + { + [TestMethod] + public void TestIsSpecialTaskAttribute() + { + Assert.IsFalse(XMakeAttributes.IsSpecialTaskAttribute("NotAnAttribute")); + Assert.IsTrue(XMakeAttributes.IsSpecialTaskAttribute(XMakeAttributes.xmlns)); + Assert.IsTrue(XMakeAttributes.IsSpecialTaskAttribute(XMakeAttributes.continueOnError)); + Assert.IsTrue(XMakeAttributes.IsSpecialTaskAttribute(XMakeAttributes.condition)); + Assert.IsTrue(XMakeAttributes.IsSpecialTaskAttribute(XMakeAttributes.msbuildArchitecture)); + Assert.IsTrue(XMakeAttributes.IsSpecialTaskAttribute(XMakeAttributes.msbuildRuntime)); + } + + [TestMethod] + public void TestIsBadlyCasedSpecialTaskAttribute() + { + Assert.IsFalse(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute("NotAnAttribute")); + Assert.IsFalse(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute(XMakeAttributes.include)); + Assert.IsFalse(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute(XMakeAttributes.continueOnError)); + Assert.IsFalse(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute(XMakeAttributes.condition)); + Assert.IsFalse(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute(XMakeAttributes.msbuildArchitecture)); + Assert.IsFalse(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute(XMakeAttributes.msbuildRuntime)); + Assert.IsTrue(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute("continueOnError")); + Assert.IsTrue(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute("condition")); + Assert.IsTrue(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute("MsbuildRuntime")); + Assert.IsTrue(XMakeAttributes.IsBadlyCasedSpecialTaskAttribute("msbuildarchitecture")); + } + + [TestMethod] + public void TestIsNonBatchingTargetAttribute() + { + Assert.IsFalse(XMakeAttributes.IsNonBatchingTargetAttribute("NotAnAttribute")); + Assert.IsTrue(XMakeAttributes.IsNonBatchingTargetAttribute(XMakeAttributes.dependsOnTargets)); + Assert.IsTrue(XMakeAttributes.IsNonBatchingTargetAttribute(XMakeAttributes.name)); + Assert.IsTrue(XMakeAttributes.IsNonBatchingTargetAttribute(XMakeAttributes.condition)); + } + + [TestMethod] + public void TestRuntimeValuesMatch() + { + Assert.IsTrue(XMakeAttributes.RuntimeValuesMatch(XMakeAttributes.MSBuildRuntimeValues.any, XMakeAttributes.MSBuildRuntimeValues.currentRuntime)); + Assert.IsTrue(XMakeAttributes.RuntimeValuesMatch(XMakeAttributes.MSBuildRuntimeValues.any, XMakeAttributes.MSBuildRuntimeValues.clr4)); + Assert.IsTrue(XMakeAttributes.RuntimeValuesMatch(XMakeAttributes.MSBuildRuntimeValues.clr2, XMakeAttributes.MSBuildRuntimeValues.any)); + Assert.IsTrue(XMakeAttributes.RuntimeValuesMatch(XMakeAttributes.MSBuildRuntimeValues.currentRuntime, XMakeAttributes.MSBuildRuntimeValues.clr4)); + + Assert.IsFalse(XMakeAttributes.RuntimeValuesMatch(XMakeAttributes.MSBuildRuntimeValues.currentRuntime, XMakeAttributes.MSBuildRuntimeValues.clr2)); + Assert.IsFalse(XMakeAttributes.RuntimeValuesMatch(XMakeAttributes.MSBuildRuntimeValues.clr4, XMakeAttributes.MSBuildRuntimeValues.clr2)); + } + + [TestMethod] + public void TestMergeRuntimeValues() + { + string mergedRuntime = null; + + Assert.IsTrue(XMakeAttributes.TryMergeRuntimeValues(XMakeAttributes.MSBuildRuntimeValues.any, XMakeAttributes.MSBuildRuntimeValues.currentRuntime, out mergedRuntime)); + Assert.AreEqual(XMakeAttributes.MSBuildRuntimeValues.clr4, mergedRuntime); + + Assert.IsTrue(XMakeAttributes.TryMergeRuntimeValues(XMakeAttributes.MSBuildRuntimeValues.any, XMakeAttributes.MSBuildRuntimeValues.clr4, out mergedRuntime)); + Assert.AreEqual(XMakeAttributes.MSBuildRuntimeValues.clr4, mergedRuntime); + + Assert.IsTrue(XMakeAttributes.TryMergeRuntimeValues(XMakeAttributes.MSBuildRuntimeValues.clr2, XMakeAttributes.MSBuildRuntimeValues.any, out mergedRuntime)); + Assert.AreEqual(XMakeAttributes.MSBuildRuntimeValues.clr2, mergedRuntime); + + Assert.IsTrue(XMakeAttributes.TryMergeRuntimeValues(XMakeAttributes.MSBuildRuntimeValues.currentRuntime, XMakeAttributes.MSBuildRuntimeValues.clr4, out mergedRuntime)); + Assert.AreEqual(XMakeAttributes.MSBuildRuntimeValues.clr4, mergedRuntime); + + Assert.IsFalse(XMakeAttributes.TryMergeRuntimeValues(XMakeAttributes.MSBuildRuntimeValues.currentRuntime, XMakeAttributes.MSBuildRuntimeValues.clr2, out mergedRuntime)); + Assert.IsNull(mergedRuntime); + + Assert.IsFalse(XMakeAttributes.TryMergeRuntimeValues(XMakeAttributes.MSBuildRuntimeValues.clr4, XMakeAttributes.MSBuildRuntimeValues.clr2, out mergedRuntime)); + Assert.IsNull(mergedRuntime); + } + + [TestMethod] + public void TestArchitectureValuesMatch() + { + string currentArchitecture = Environment.Is64BitProcess ? XMakeAttributes.MSBuildArchitectureValues.x64 : XMakeAttributes.MSBuildArchitectureValues.x86; + string notCurrentArchitecture = Environment.Is64BitProcess ? XMakeAttributes.MSBuildArchitectureValues.x86 : XMakeAttributes.MSBuildArchitectureValues.x64; + + Assert.IsTrue(XMakeAttributes.ArchitectureValuesMatch(XMakeAttributes.MSBuildArchitectureValues.any, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture)); + Assert.IsTrue(XMakeAttributes.ArchitectureValuesMatch(XMakeAttributes.MSBuildArchitectureValues.any, XMakeAttributes.MSBuildArchitectureValues.x64)); + Assert.IsTrue(XMakeAttributes.ArchitectureValuesMatch(XMakeAttributes.MSBuildArchitectureValues.x86, XMakeAttributes.MSBuildArchitectureValues.any)); + Assert.IsTrue(XMakeAttributes.ArchitectureValuesMatch(XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, currentArchitecture)); + + Assert.IsFalse(XMakeAttributes.ArchitectureValuesMatch(XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, notCurrentArchitecture)); + Assert.IsFalse(XMakeAttributes.ArchitectureValuesMatch(XMakeAttributes.MSBuildArchitectureValues.x64, XMakeAttributes.MSBuildArchitectureValues.x86)); + } + + [TestMethod] + public void TestMergeArchitectureValues() + { + string mergedArchitecture = null; + + string currentArchitecture = Environment.Is64BitProcess ? XMakeAttributes.MSBuildArchitectureValues.x64 : XMakeAttributes.MSBuildArchitectureValues.x86; + string notCurrentArchitecture = Environment.Is64BitProcess ? XMakeAttributes.MSBuildArchitectureValues.x86 : XMakeAttributes.MSBuildArchitectureValues.x64; + + Assert.IsTrue(XMakeAttributes.TryMergeArchitectureValues(XMakeAttributes.MSBuildArchitectureValues.any, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, out mergedArchitecture)); + Assert.AreEqual(currentArchitecture, mergedArchitecture); + + Assert.IsTrue(XMakeAttributes.TryMergeArchitectureValues(XMakeAttributes.MSBuildArchitectureValues.any, XMakeAttributes.MSBuildArchitectureValues.x64, out mergedArchitecture)); + Assert.AreEqual(XMakeAttributes.MSBuildArchitectureValues.x64, mergedArchitecture); + + Assert.IsTrue(XMakeAttributes.TryMergeArchitectureValues(XMakeAttributes.MSBuildArchitectureValues.x86, XMakeAttributes.MSBuildArchitectureValues.any, out mergedArchitecture)); + Assert.AreEqual(XMakeAttributes.MSBuildArchitectureValues.x86, mergedArchitecture); + + Assert.IsTrue(XMakeAttributes.TryMergeArchitectureValues(XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, currentArchitecture, out mergedArchitecture)); + Assert.AreEqual(currentArchitecture, mergedArchitecture); + + Assert.IsFalse(XMakeAttributes.TryMergeArchitectureValues(XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, notCurrentArchitecture, out mergedArchitecture)); + Assert.IsFalse(XMakeAttributes.TryMergeArchitectureValues(XMakeAttributes.MSBuildArchitectureValues.x64, XMakeAttributes.MSBuildArchitectureValues.x86, out mergedArchitecture)); + } + } +} \ No newline at end of file diff --git a/src/Shared/UnitTests/XmlUtilities_Tests.cs b/src/Shared/UnitTests/XmlUtilities_Tests.cs new file mode 100644 index 00000000000..f2f5cd641d9 --- /dev/null +++ b/src/Shared/UnitTests/XmlUtilities_Tests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class XmlUtilities_Tests + { + // Various invalid names, with the first invalid character listed for each + private string[,] _invalidNames = new string[,] { + {"foo.bar", "."}, + {"1baz", "1"}, + {"+", "+"}, + {"a+", "+"}, + {"_-.", "."}, + {"AZaz09%", "%"}, + {" x", " "}, + {"A Zaz09%", " "}, + {"foo.", "."}, + {"\u03A3", "\u03A3"}, // sigma + {"a1\u03A3", "\u03A3"} + }; + + /// + /// Verify we get the right invalid char listed in the error string + /// + [TestMethod] + public void InvalidNameErrorLocation() + { + for (int i = 0; i <= _invalidNames.GetUpperBound(0); i++) + { + InvalidNameErrorLocationHelper(_invalidNames[i, 0], _invalidNames[i, 1]); + } + } + + /// + /// Helper for invalid name error location test + /// + /// + /// + private void InvalidNameErrorLocationHelper(string name, string badChar) + { + string expected = ResourceUtilities.FormatResourceString("OM_NameInvalid", name, badChar); + string actual = String.Empty; + + try + { + XmlUtilities.VerifyThrowArgumentValidElementName(name); + } + catch (ArgumentException ex) + { + actual = ex.Message; + } + + Assert.AreEqual(expected, actual); + } + } +} diff --git a/src/Shared/VersionUtilities.cs b/src/Shared/VersionUtilities.cs new file mode 100644 index 00000000000..d2922842a57 --- /dev/null +++ b/src/Shared/VersionUtilities.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Deal with converting strings to .net versions +//----------------------------------------------------------------------- + +using System; +using Microsoft.Win32; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; + +namespace Microsoft.Build.Shared +{ + /// + /// Set of methods to deal with versions in the tasks + /// + internal static class VersionUtilities + { + /// + /// Convert a version number like 0.0.0.0 to a Version instance. + /// The method will return null if the string is not a valid value + /// + /// Version string to convert to a version object + internal static Version ConvertToVersion(string version) + { + return ConvertToVersion(version, false); + } + + /// + /// Go though an enumeration and create a sorted list of strings which can be parsed as versions. Keep around the original + /// string because it may contain a v and this would be required to create the correct path on disk if the string was part of a path. + /// + internal static SortedDictionary> GatherVersionStrings(Version targetPlatformVersion, IEnumerable versions) + { + SortedDictionary> versionValues = new SortedDictionary>(ReverseVersionGenericComparer.Comparer); + + // Loop over versions from registry. + foreach (string version in versions) + { + if (version.Length > 0) + { + Version candidateVersion = VersionUtilities.ConvertToVersion(version); + + if (candidateVersion != null && (targetPlatformVersion == null || (candidateVersion <= targetPlatformVersion))) + { + if (versionValues.ContainsKey(candidateVersion)) + { + List versionList = versionValues[candidateVersion]; + if (!versionList.Contains(version)) + { + versionList.Add(version); + } + } + else + { + versionValues.Add(candidateVersion, new List() { version }); + } + } + } + } + + return versionValues; + } + + /// + /// Convert a version number like 0.0.0.0 to a Version instance. + /// + /// Should we use Parse to TryParse (parse means we throw an exception, tryparse means we will not). + internal static Version ConvertToVersion(string version, bool throwException) + { + Version result = null; + + if (version.Length > 0 && (version[0] == 'v' || version[0] == 'V')) + { + version = version.Substring(1); + } + + if (throwException) + { + result = Version.Parse(version); + } + else + { + if (!Version.TryParse(version, out result)) + { + return null; + } + } + + return result; + } + } + + sealed internal class ReverseStringGenericComparer : IComparer + { + /// + /// Static accessor for a ReverseVersionGenericComparer + /// + internal static readonly ReverseStringGenericComparer Comparer = new ReverseStringGenericComparer(); + + /// + /// The Compare implements a reverse comparison + /// + int IComparer.Compare(string x, string y) + { + // Reverse the sign of the return value. + return StringComparer.OrdinalIgnoreCase.Compare(y, x); + } + } + + sealed internal class ReverseVersionGenericComparer : IComparer + { + /// + /// Static accessor for a ReverseVersionGenericComparer + /// + internal static readonly ReverseVersionGenericComparer Comparer = new ReverseVersionGenericComparer(); + + /// + /// The Compare implements a reverse comparison + /// + int IComparer.Compare(Version x, Version y) + { + // Reverse the sign of the return value. + return y.CompareTo(x); + } + } +} \ No newline at end of file diff --git a/src/Shared/VisualStudioConstants.cs b/src/Shared/VisualStudioConstants.cs new file mode 100644 index 00000000000..9e32700f16e --- /dev/null +++ b/src/Shared/VisualStudioConstants.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Shared Visual Studio related constants. +//----------------------------------------------------------------------- + +using System; + +namespace Microsoft.Build.Shared +{ + /// + /// Shared Visual Studio related constants + /// + internal static class VisualStudioConstants + { + /// + /// This is the version number of the most recent solution file format + /// we will read. It will be the version number used in solution files + /// by the latest version of Visual Studio. + /// + internal const int CurrentVisualStudioSolutionFileVersion = 12; // VS11 + + /// + /// This is the version number of the latest version of Visual Studio. + /// + /// + /// We use it for the version of the VC PIA we try to load and to find + /// Visual Studio registry hive that we use to find where vcbuild.exe might be. + /// + internal const string CurrentVisualStudioVersion = "10.0"; + } +} diff --git a/src/Shared/XMakeAttributes.cs b/src/Shared/XMakeAttributes.cs new file mode 100644 index 00000000000..efb5e68048f --- /dev/null +++ b/src/Shared/XMakeAttributes.cs @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// Contains the names of the known attributes in the XML project file. + /// + internal static class XMakeAttributes + { + internal const string condition = "Condition"; + internal const string executeTargets = "ExecuteTargets"; + internal const string name = "Name"; + internal const string msbuildVersion = "MSBuildVersion"; + internal const string xmlns = "xmlns"; + internal const string defaultTargets = "DefaultTargets"; + internal const string initialTargets = "InitialTargets"; + internal const string treatAsLocalProperty = "TreatAsLocalProperty"; + internal const string dependsOnTargets = "DependsOnTargets"; + internal const string beforeTargets = "BeforeTargets"; + internal const string afterTargets = "AfterTargets"; + internal const string include = "Include"; + internal const string exclude = "Exclude"; + internal const string remove = "Remove"; + internal const string keepMetadata = "KeepMetadata"; + internal const string removeMetadata = "RemoveMetadata"; + internal const string keepDuplicates = "KeepDuplicates"; + internal const string inputs = "Inputs"; + internal const string outputs = "Outputs"; + internal const string keepDuplicateOutputs = "KeepDuplicateOutputs"; + internal const string assemblyName = "AssemblyName"; + internal const string assemblyFile = "AssemblyFile"; + internal const string taskName = "TaskName"; + internal const string continueOnError = "ContinueOnError"; + internal const string project = "Project"; + internal const string taskParameter = "TaskParameter"; + internal const string itemName = "ItemName"; + internal const string propertyName = "PropertyName"; + internal const string toolsVersion = "ToolsVersion"; + internal const string runtime = "Runtime"; + internal const string msbuildRuntime = "MSBuildRuntime"; + internal const string architecture = "Architecture"; + internal const string msbuildArchitecture = "MSBuildArchitecture"; + internal const string taskFactory = "TaskFactory"; + internal const string parameterType = "ParameterType"; + internal const string required = "Required"; + internal const string output = "Output"; + internal const string defaultValue = "DefaultValue"; + internal const string evaluate = "Evaluate"; + internal const string label = "Label"; + internal const string returns = "Returns"; + + // Obsolete + internal const string requiredRuntime = "RequiredRuntime"; + internal const string requiredPlatform = "RequiredPlatform"; + + internal struct ContinueOnErrorValues + { + internal const string errorAndContinue = "ErrorAndContinue"; + internal const string errorAndStop = "ErrorAndStop"; + internal const string warnAndContinue = "WarnAndContinue"; + } + + internal struct MSBuildRuntimeValues + { + internal const string clr2 = "CLR2"; + internal const string clr4 = "CLR4"; + internal const string currentRuntime = "CurrentRuntime"; + internal const string any = "*"; + } + + internal struct MSBuildArchitectureValues + { + internal const string x86 = "x86"; + internal const string x64 = "x64"; + internal const string currentArchitecture = "CurrentArchitecture"; + internal const string any = "*"; + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // If we ever add a new MSBuild namespace (or change this one) we must update the registry key + // we set during install to disable the XSL debugger from working on MSBuild format files. + ///////////////////////////////////////////////////////////////////////////////////////////// + internal const string defaultXmlNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; + + /// + /// Returns true if and only if the specified attribute is one of the attributes that the engine specifically recognizes + /// on a task and treats in a special way. + /// + /// + /// true, if given attribute is a reserved task attribute + internal static bool IsSpecialTaskAttribute + ( + string attribute + ) + { + // Currently the known "special" attributes for a task are: + // Condition, ContinueOnError + // + // We want to match case-sensitively on all of them + return ((attribute == condition) || + (attribute == continueOnError) || + (attribute == msbuildRuntime) || + (attribute == msbuildArchitecture) || + (attribute == xmlns)); + } + + /// + /// Checks if the specified attribute is a reserved task attribute with incorrect casing. + /// + /// + /// true, if the given attribute is reserved and badly cased + internal static bool IsBadlyCasedSpecialTaskAttribute(string attribute) + { + return (!IsSpecialTaskAttribute(attribute) && + ((String.Compare(attribute, condition, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(attribute, continueOnError, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(attribute, msbuildRuntime, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(attribute, msbuildArchitecture, StringComparison.OrdinalIgnoreCase) == 0))); + } + + /// + /// Indicates if the specified attribute cannot be used for batching targets. + /// + /// + /// true, if a target cannot batch on the given attribute + internal static bool IsNonBatchingTargetAttribute(string attribute) + { + return ((attribute == name) || + (attribute == condition) || + (attribute == dependsOnTargets) || + (attribute == beforeTargets) || + (attribute == afterTargets)); + } + + /// + /// Returns true if the given string is a valid member of the MSBuildRuntimeValues set + /// + internal static bool IsValidMSBuildRuntimeValue(string runtime) + { + return (runtime == null || + XMakeAttributes.MSBuildRuntimeValues.clr2.Equals(runtime, StringComparison.OrdinalIgnoreCase) || + XMakeAttributes.MSBuildRuntimeValues.clr4.Equals(runtime, StringComparison.OrdinalIgnoreCase) || + XMakeAttributes.MSBuildRuntimeValues.currentRuntime.Equals(runtime, StringComparison.OrdinalIgnoreCase) || + XMakeAttributes.MSBuildRuntimeValues.any.Equals(runtime, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Returns true if the given string is a valid member of the MSBuildArchitectureValues set + /// + internal static bool IsValidMSBuildArchitectureValue(string architecture) + { + return (architecture == null || + XMakeAttributes.MSBuildArchitectureValues.x86.Equals(architecture, StringComparison.OrdinalIgnoreCase) || + XMakeAttributes.MSBuildArchitectureValues.x64.Equals(architecture, StringComparison.OrdinalIgnoreCase) || + XMakeAttributes.MSBuildArchitectureValues.currentArchitecture.Equals(architecture, StringComparison.OrdinalIgnoreCase) || + XMakeAttributes.MSBuildArchitectureValues.any.Equals(architecture, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Compares two members of MSBuildRuntimeValues, returning true if they count as a match, and false otherwise. + /// + internal static bool RuntimeValuesMatch(string runtimeA, string runtimeB) + { + ErrorUtilities.VerifyThrow(runtimeA != String.Empty && runtimeB != String.Empty, "We should never get an empty string passed to this method"); + + if (runtimeA == null || runtimeB == null) + { + // neither one cares, or only one cares, so they match by default. + return true; + } + + if (runtimeA.Equals(runtimeB, StringComparison.OrdinalIgnoreCase)) + { + // if they are equal, of course they match + return true; + } + + if (runtimeA.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase) || runtimeB.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase)) + { + // one or both explicitly don't care -- still a match. + return true; + } + + if ((runtimeA.Equals(MSBuildRuntimeValues.currentRuntime, StringComparison.OrdinalIgnoreCase) && runtimeB.Equals(MSBuildRuntimeValues.clr4, StringComparison.OrdinalIgnoreCase)) || + (runtimeA.Equals(MSBuildRuntimeValues.clr4, StringComparison.OrdinalIgnoreCase) && runtimeB.Equals(MSBuildRuntimeValues.currentRuntime, StringComparison.OrdinalIgnoreCase))) + { + // CLR4 is the current runtime, so this is also a match. + return true; + } + + // if none of the above is true, then it doesn't match ... + return false; + } + + /// + /// Given two MSBuildRuntime values, returns the concrete result of merging the two. If the merge fails, the merged runtime + /// string is returned null, and the return value of the method is false. Otherwise, if the merge succeeds, the method returns + /// true with the merged runtime value. E.g.: + /// "CLR4" + "CLR2" = null (false) + /// "CLR2" + "don't care" = "CLR2" (true) + /// "current runtime" + "CLR4" = "CLR4" (true) + /// "current runtime" + "don't care" = "CLR4" (true) + /// If both specify "don't care", then defaults to the current runtime -- CLR4. + /// A null or empty string is interpreted as "don't care". + /// + internal static bool TryMergeRuntimeValues(string runtimeA, string runtimeB, out string mergedRuntime) + { + ErrorUtilities.VerifyThrow(runtimeA != String.Empty && runtimeB != String.Empty, "We should never get an empty string passed to this method"); + + // set up the defaults + if (runtimeA == null) + { + runtimeA = MSBuildRuntimeValues.any; + } + + if (runtimeB == null) + { + runtimeB = MSBuildRuntimeValues.any; + } + + // if they're equal, then there's no problem -- just return the equivalent runtime. + if (runtimeA.Equals(runtimeB, StringComparison.OrdinalIgnoreCase)) + { + if (runtimeA.Equals(MSBuildRuntimeValues.currentRuntime, StringComparison.OrdinalIgnoreCase) || + runtimeA.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase)) + { + mergedRuntime = MSBuildRuntimeValues.clr4; + } + else + { + mergedRuntime = runtimeA; + } + + return true; + } + + // if both A and B are one of CLR4, don't care, or current, then the end result will be CLR4 no matter what. + if ( + ( + runtimeA.Equals(MSBuildRuntimeValues.clr4, StringComparison.OrdinalIgnoreCase) || + runtimeA.Equals(MSBuildRuntimeValues.currentRuntime, StringComparison.OrdinalIgnoreCase) || + runtimeA.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase) + ) && + ( + runtimeB.Equals(MSBuildRuntimeValues.clr4, StringComparison.OrdinalIgnoreCase) || + runtimeB.Equals(MSBuildRuntimeValues.currentRuntime, StringComparison.OrdinalIgnoreCase) || + runtimeB.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase) + ) + ) + { + mergedRuntime = MSBuildRuntimeValues.clr4; + return true; + } + + // If A doesn't care, then it's B -- and we can say B straight out, because if B were one of the + // special cases (current runtime or don't care) then it would already have been caught in the + // previous clause. + if (runtimeA.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase)) + { + mergedRuntime = runtimeB; + return true; + } + + // And vice versa + if (runtimeB.Equals(MSBuildRuntimeValues.any, StringComparison.OrdinalIgnoreCase)) + { + mergedRuntime = runtimeA; + return true; + } + + // and now we've run out of things that it could be -- all the remaining options are non-matches. + mergedRuntime = null; + return false; + } + + /// + /// Compares two members of MSBuildArchitectureValues, returning true if they count as a match, and false otherwise. + /// + internal static bool ArchitectureValuesMatch(string architectureA, string architectureB) + { + ErrorUtilities.VerifyThrow(architectureA != String.Empty && architectureB != String.Empty, "We should never get an empty string passed to this method"); + + if (architectureA == null || architectureB == null) + { + // neither one cares, or only one cares, so they match by default. + return true; + } + + if (architectureA.Equals(architectureB, StringComparison.OrdinalIgnoreCase)) + { + // if they are equal, of course they match + return true; + } + + if (architectureA.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase) || architectureB.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase)) + { + // one or both explicitly don't care -- still a match. + return true; + } + + string currentArchitecture = GetCurrentMSBuildArchitecture(); + + if ((architectureA.Equals(MSBuildArchitectureValues.currentArchitecture, StringComparison.OrdinalIgnoreCase) && architectureB.Equals(currentArchitecture, StringComparison.OrdinalIgnoreCase)) || + (architectureA.Equals(currentArchitecture, StringComparison.OrdinalIgnoreCase) && architectureB.Equals(MSBuildArchitectureValues.currentArchitecture, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + // if none of the above is true, then it doesn't match ... + return false; + } + + /// + /// Given an MSBuildRuntime value that may be non-explicit -- e.g. "CurrentRuntime" or "Any" -- + /// return the specific MSBuildRuntime value that it would map to in this case. If it does not map + /// to any known runtime, just return it as is -- maybe someone else knows what to do with it; if + /// not, they'll certainly have more context on logging or throwing the error. + /// + internal static string GetExplicitMSBuildRuntime(string runtime) + { + if (runtime == null || + MSBuildRuntimeValues.any.Equals(runtime, StringComparison.OrdinalIgnoreCase) || + MSBuildRuntimeValues.currentRuntime.Equals(runtime, StringComparison.OrdinalIgnoreCase)) + { + // Default to CLR4. + return MSBuildRuntimeValues.clr4; + } + else + { + // either it's already a valid, specific runtime, or we don't know what to do with it. Either way, return. + return runtime; + } + } + + /// + /// Given two MSBuildArchitecture values, returns the concrete result of merging the two. If the merge fails, the merged architecture + /// string is returned null, and the return value of the method is false. Otherwise, if the merge succeeds, the method returns + /// true with the merged architecture value. E.g.: + /// "x86" + "x64" = null (false) + /// "x86" + "don't care" = "x86" (true) + /// "current architecture" + "x86" = "x86" (true) on a 32-bit process, and null (false) on a 64-bit process + /// "current architecture" + "don't care" = "x86" (true) on a 32-bit process, and "x64" (true) on a 64-bit process + /// A null or empty string is interpreted as "don't care". + /// If both specify "don't care", then defaults to whatever the current process architecture is. + /// + internal static bool TryMergeArchitectureValues(string architectureA, string architectureB, out string mergedArchitecture) + { + ErrorUtilities.VerifyThrow(architectureA != String.Empty && architectureB != String.Empty, "We should never get an empty string passed to this method"); + + // set up the defaults + if (architectureA == null) + { + architectureA = MSBuildArchitectureValues.any; + } + + if (architectureB == null) + { + architectureB = MSBuildArchitectureValues.any; + } + + string currentArchitecture = GetCurrentMSBuildArchitecture(); + + // if they're equal, then there's no problem -- just return the equivalent runtime. + if (architectureA.Equals(architectureB, StringComparison.OrdinalIgnoreCase)) + { + if (architectureA.Equals(MSBuildArchitectureValues.currentArchitecture, StringComparison.OrdinalIgnoreCase) || + architectureA.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase)) + { + mergedArchitecture = currentArchitecture; + } + else + { + mergedArchitecture = architectureA; + } + + return true; + } + + // if both A and B are one of CLR4, don't care, or current, then the end result will be CLR4 no matter what. + if ( + ( + architectureA.Equals(currentArchitecture, StringComparison.OrdinalIgnoreCase) || + architectureA.Equals(MSBuildArchitectureValues.currentArchitecture, StringComparison.OrdinalIgnoreCase) || + architectureA.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase) + ) && + ( + architectureB.Equals(currentArchitecture, StringComparison.OrdinalIgnoreCase) || + architectureB.Equals(MSBuildArchitectureValues.currentArchitecture, StringComparison.OrdinalIgnoreCase) || + architectureB.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase) + ) + ) + { + mergedArchitecture = currentArchitecture; + return true; + } + + // If A doesn't care, then it's B -- and we can say B straight out, because if B were one of the + // special cases (current runtime or don't care) then it would already have been caught in the + // previous clause. + if (architectureA.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase)) + { + mergedArchitecture = architectureB; + return true; + } + + // And vice versa + if (architectureB.Equals(MSBuildArchitectureValues.any, StringComparison.OrdinalIgnoreCase)) + { + mergedArchitecture = architectureA; + return true; + } + + // and now we've run out of things that it could be -- all the remaining options are non-matches. + mergedArchitecture = null; + return false; + } + + /// + /// Returns the MSBuildArchitecture value corresponding to the current process' architecture. + /// + /// + /// Revisit if we ever run on something other than Intel. + /// + internal static string GetCurrentMSBuildArchitecture() + { + string currentArchitecture = (IntPtr.Size == sizeof(Int64)) ? MSBuildArchitectureValues.x64 : MSBuildArchitectureValues.x86; + return currentArchitecture; + } + + /// + /// Given an MSBuildArchitecture value that may be non-explicit -- e.g. "CurrentArchitecture" or "Any" -- + /// return the specific MSBuildArchitecture value that it would map to in this case. If it does not map + /// to any known architecture, just return it as is -- maybe someone else knows what to do with it; if + /// not, they'll certainly have more context on logging or throwing the error. + /// + internal static string GetExplicitMSBuildArchitecture(string architecture) + { + if (architecture == null || + MSBuildArchitectureValues.any.Equals(architecture, StringComparison.OrdinalIgnoreCase) || + MSBuildArchitectureValues.currentArchitecture.Equals(architecture, StringComparison.OrdinalIgnoreCase)) + { + string currentArchitecture = GetCurrentMSBuildArchitecture(); + return currentArchitecture; + } + else + { + // either it's already a valid, specific architecture, or we don't know what to do with it. Either way, return. + return architecture; + } + } + } +} diff --git a/src/Shared/XMakeElements.cs b/src/Shared/XMakeElements.cs new file mode 100644 index 00000000000..d059744d7a7 --- /dev/null +++ b/src/Shared/XMakeElements.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Xml; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// Contains the names of the known elements in the XML project file. + /// + internal static class XMakeElements + { + internal const string project = "Project"; + internal const string visualStudioProject = "VisualStudioProject"; + internal const string target = "Target"; + internal const string propertyGroup = "PropertyGroup"; + internal const string output = "Output"; + internal const string itemGroup = "ItemGroup"; + internal const string itemDefinitionGroup = "ItemDefinitionGroup"; + internal const string usingTask = "UsingTask"; + internal const string projectExtensions = "ProjectExtensions"; + internal const string onError = "OnError"; + internal const string error = "Error"; + internal const string warning = "Warning"; + internal const string message = "Message"; + internal const string import = "Import"; + internal const string importGroup = "ImportGroup"; + internal const string choose = "Choose"; + internal const string when = "When"; + internal const string otherwise = "Otherwise"; + internal const string usingTaskParameterGroup = "ParameterGroup"; + internal const string usingTaskParameter = "Parameter"; + internal const string usingTaskBody = "Task"; + + internal static readonly char[] illegalTargetNameCharacters = new char[] { '$', '@', '(', ')', '%', '*', '?', '.' }; + + // Names that cannot be used as property or item names because they are reserved + internal static readonly string[] illegalPropertyOrItemNames = new string[] { + // XMakeElements.project, // "Project" is not reserved, because unfortunately ProjectReference items + // already use it as metadata name. + XMakeElements.visualStudioProject, + XMakeElements.target, + XMakeElements.propertyGroup, + XMakeElements.output, + XMakeElements.itemGroup, + XMakeElements.usingTask, + XMakeElements.projectExtensions, + XMakeElements.onError, + // XMakeElements.import, // "Import" items are used by Visual Basic projects + XMakeElements.importGroup, + XMakeElements.choose, + XMakeElements.when, + XMakeElements.otherwise + }; + + // The set of XMake reserved item/property names (e.g. Choose, Message etc.) + private static Lazy s_illegalItemOrPropertyNamesHashtable = new Lazy( + () => + { + var table = new Hashtable(illegalPropertyOrItemNames.Length); + foreach (string reservedName in illegalPropertyOrItemNames) + { + table.Add(reservedName, string.Empty); + } + + return table; + }, + true /* only one thread can initialize */); + + /// + /// Read-only internal accessor for the hashtable containing + /// MSBuild reserved item/property names (like "Choose", for example). + /// + internal static Hashtable IllegalItemPropertyNames + { + get + { + return s_illegalItemOrPropertyNamesHashtable.Value; + } + } + } +} diff --git a/src/Shared/XamlUtilities.cs b/src/Shared/XamlUtilities.cs new file mode 100644 index 00000000000..6e7a8206b6e --- /dev/null +++ b/src/Shared/XamlUtilities.cs @@ -0,0 +1,435 @@ +using System; +using System.Xml; +using System.Text.RegularExpressions; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework.XamlTypes; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for Xaml types. + /// + static internal class XamlUtilities + { + /// + /// Gets an identifier for a property based on its name, its containing object's name and/or type. This is intended to + /// help the user zero in on the offending line/element in a xaml file, when we don't have access to a parser to + /// report line numbers. + /// + /// The name of the property. + /// The name of the containing object. + /// The object which contains this property. + /// Returns "(containingObject's type name)containingObjectName.PropertyName". + internal static string GetPropertyId(string propertyName, string containingObjectName, object containingObject) + { + ErrorUtilities.VerifyThrowArgumentLength(propertyName, "propertyName"); + ErrorUtilities.VerifyThrowArgumentLength(containingObjectName, "containingObjectName"); + ErrorUtilities.VerifyThrowArgumentNull(containingObject, "containingObject"); + + StringBuilder propertyId = new StringBuilder(); + + propertyId.Append("("); + propertyId.Append(containingObject.GetType().Name); + propertyId.Append(")"); + + if (!string.IsNullOrEmpty(containingObjectName)) + { + propertyId.Append(containingObjectName); + } + + propertyId.Append("."); + + propertyId.Append(propertyName); + + return propertyId.ToString(); + } + + /// + /// Returns an identifier for a property based on its name and its containing object's type. Use this + /// overload when the containin object's name is not known (which can be the case when the property + /// being tested is the Name property itself). + /// + /// The name of the property. + /// The object which contains this property. + /// Returns "(containingObject's type name)unknown.PropertyName". + internal static string GetPropertyId(string propertyName, object containingObject) + { + string propertyId = GetPropertyId(propertyName, "unknown", containingObject); + return propertyId; + } + + /// + /// Throws an if the given property is null. + /// + /// The property to test. + /// An identifier of the property to check. + internal static void VerifyThrowPropertyNotSet(object property, string propertyId) + { + ErrorUtilities.VerifyThrowArgumentLength(propertyId, "propertyId"); + + if (property == null) + { + ErrorUtilities.VerifyThrowArgumentNull(null, propertyId, Strings.PropertyValueMustBeSet); + } + } + + /// + /// Throws an if the given property is null. + /// + /// The property to test. + /// An identifier of the property to check. + /// The text message to display. + internal static void VerifyThrowPropertyNotSet(object property, string propertyId, string unformattedMessage) + { + ErrorUtilities.VerifyThrowArgumentLength(propertyId, "propertyId"); + + if (property == null) + { + ErrorUtilities.VerifyThrowArgumentNull(null, propertyId, unformattedMessage); + } + } + + /// + /// Throws an if the given property's value is the empty string. + /// + /// The parameter to test. + /// An identifier of the property to check. + internal static void VerifyThrowPropertyEmptyString(string property, string propertyId) + { + ErrorUtilities.VerifyThrowArgumentNull(property, "property"); + ErrorUtilities.VerifyThrowArgumentLength(propertyId, "propertyId"); + + if (property.Length == 0) + { + ErrorUtilities.ThrowArgument(Strings.PropertyCannotBeSetToTheEmptyString, propertyId); + } + } + + /// + /// Throws an if the given property is null and + /// if the given property's value is the empty string. + /// + /// The parameter to test. + /// An identifier of the property to check. + internal static void VerifyThrowPropertyNotSetOrEmptyString(string property, string propertyId) + { + VerifyThrowPropertyNotSet(property, propertyId); + VerifyThrowPropertyEmptyString(property, propertyId); + } + + /// + /// Throws an if the given list property has zero elements. + /// + /// The list parameter to test. + /// An identifier of the property to check. + internal static void VerifyThrowListPropertyEmpty(IList listProperty, string propertyId) + { + ErrorUtilities.VerifyThrowArgumentNull(listProperty, "listProperty"); + ErrorUtilities.VerifyThrowArgumentLength(propertyId, "propertyId"); + + if (listProperty.Count == 0) + { + ErrorUtilities.ThrowArgument(Strings.ListPropertyShouldHaveAtLeastOneElement, propertyId); + } + } + + #region Extension Methods + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this Argument type) + { + string propertyId = GetPropertyId("Property", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Property, propertyId); + } + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this BaseProperty type) + { + string namePropertyId = GetPropertyId("Name", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Name, namePropertyId); + + string categoryPropertyId = GetPropertyId("Category", type.Name, type); + VerifyThrowPropertyEmptyString(typeCategory, categoryPropertyId); + + // Validate children. + if (null != type.DataSource) + { + type.DataSource.Validate(); + } + + foreach (Argument argument in type.Arguments) + { + argument.Validate(); + } + + foreach (ValueEditor editor in type.ValueEditors) + { + editor.Validate(); + } + + // Validate any known derivations. + BoolProperty boolProp = type as BoolProperty; + if (null != boolProp) + { + return; + } + + DynamicEnumProperty dynamicEnumProp = type as DynamicEnumProperty; + if (dynamicEnumProp != null) + { + dynamicEnumProp.Validate(); + return; + } + + EnumProperty enumProp = type as EnumProperty; + if (enumProp != null) + { + enumProp.Validate(); + return; + } + + IntProperty intProp = type as IntProperty; + if (intProp != null) + { + intProp.Validate(); + return; + } + + StringListProperty stringListProp = type as StringListProperty; + if (stringListProp != null) + { + return; + } + + StringProperty stringProp = type as StringProperty; + if (stringProp != null) + { + return; + } + + // Unknown derivation, but that's ok. + } + + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this Category type) + { + string namePropertyId = GetPropertyId("Name", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Name, namePropertyId); + } + + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this DataSource type) + { + string persistencePropertyId = GetPropertyId("Persistence", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Persistence, persistencePropertyId); + } + + /// + /// Validate the content type data integrity afte deserializing from XAML file + /// + internal void Validate(this ContentType type) + { + // content type must at least declare name, and msbuild ItemType to be workable at minimum level + string namePropertyId = GetPropertyId("Name", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Name, namePropertyId); + + string itemTypePropertyId = GetPropertyId("ItemType", type); + VerifyThrowPropertyNotSetOrEmptyString(type.ItemType, itemTypePropertyId); + } + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this DynamicEnumProperty type) + { + (type as BaseProperty).Validate(); + ErrorUtilities.VerifyThrowArgumentLength(type.EnumProvider, "EnumProvider"); + } + + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + private void Validate(this EnumProperty type) + { + (type as BaseProperty).Validate(); + + // Validate that the "Default" field is not set on this property. + string defaultPropertyId = GetPropertyId("Default", type.Name, type); + if (null != Default) + { + ErrorUtilities.ThrowArgument(Strings.CannotSetDefaultPropertyOnEnumProperty, typeof(EnumProperty).Name, typeof(EnumValue).Name); + } + + // Make sure that at least one value was defined in AdmissibleValues. + string admissibleValuesId = GetPropertyId("AdmissibleValues", type.Name, type); + VerifyThrowListPropertyEmpty(type.AdmissibleValues, admissibleValuesId); + + // Validate that only one of the EnumValues under AdmissibleValues is marked IsDefault. + string admissibleValuesPropertyId = GetPropertyId("AdmissibleValues", type.Name, type); + + bool seen = false; + foreach (EnumValue enumValue in type.AdmissibleValues) + { + if (enumValue.IsDefault) + { + if (!seen) + { + seen = true; + } + else + { + ErrorUtilities.ThrowArgument(Strings.OnlyOneEnumValueCanBeSetAsDefault, typeof(EnumValue).Name, admissibleValuesPropertyId); + } + } + } + } + + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this IntProperty type) + { + (type as BaseProperty).Validate(); + + if (null != type.MaxValue && null != type.MinValue) + { + if (type.MinValue > type.MaxValue) + { + string minValuePropertyId = GetPropertyId("MinValue", type.Name, type); + ErrorUtilities.ThrowArgument(Strings.MinValueShouldNotBeGreaterThanMaxValue, minValuePropertyId); + } + } + } + + /// + /// Validate the content type data integrity afte deserializing from XAML file + /// + internal void Validate(this ItemType type) + { + // content type must at least declare name, and msbuild ItemType to be workable at minimum level + string namePropertyId = GetPropertyId("Name", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Name, namePropertyId); + + if (type.DisplayName == null) + { + type.DisplayName = type.Name; + } + } + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this Rule type) + { + // Validate "Name" property. + string namePropertyId = GetPropertyId("Name", type); + VerifyThrowPropertyNotSetOrEmptyString(type.Name, namePropertyId); + + // Make sure that at least one Property was defined in this Rule. + string propertiesId = XamlErrorUtilities.GetPropertyId("Properties", Name, this); + VerifyThrowListPropertyEmpty(type.Properties, propertiesId); + + // Validate the child objects + foreach (BaseProperty property in type.Properties) + { + property.Validate(); + } + + foreach (Category category in type.Categories) + { + category.Validate(); + } + + // If the DataSource property is not defined on this Rule, check that a DataSource is + // specified locally on every property. + if (null == type.DataSource) + { + foreach (BaseProperty property in type.Properties) + { + string dataSourcePropertyId = GetPropertyId("DataSource", property.Name, property); + VerifyThrowPropertyNotSet(property.DataSource, dataSourcePropertyId, Strings.DataSourceMustBeDefinedOnPropertyOrOnRule); + } + } + else + { + type.DataSource.Validate(); + } + + // Create a HashSet for O(1) lookup. + HashSet propertyNames = new HashSet(); + foreach (BaseProperty property in type.Properties) + { + if (!propertyNames.Contains(property.Name)) + { + propertyNames.Add(property.Name); + } + } + + // Validate that every argument refers to a valid property. + foreach (BaseProperty property in type.Properties) + { + if (property.Arguments == null || property.Arguments.Count == 0) + { + continue; + } + + foreach (Argument argument in property.Arguments) + { + if (!propertyNames.Contains(argument.Property)) + { + ErrorUtilities.ThrowArgument(Strings.PropertyReferredToByArgumentDoesNotExist, argument.Property); + } + } + } + } + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this RuleBag type) + { + // Make sure that at least one Rule was defined in this RuleBag. + string rulesId = GetPropertyId("Rules", type); + VerifyThrowListPropertyEmpty(type.Rules, rulesId); + + foreach (Rule rule in Rules) + { + rule.Validate(); + } + } + + /// + /// Validates the properties of this object. This method should be called + /// after initialization is complete. + /// + internal void Validate(this ValueEditor type) + { + string propertyId = GetPropertyId("EditorType", type); + VerifyThrowPropertyNotSetOrEmptyString(type.EditorType, propertyId); + } + + #endregion + + } +} diff --git a/src/Shared/XmlUtilities.cs b/src/Shared/XmlUtilities.cs new file mode 100644 index 00000000000..106e99c1c06 --- /dev/null +++ b/src/Shared/XmlUtilities.cs @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text.RegularExpressions; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Shared +{ + /// + /// This class contains utility methods for XML manipulation. + /// + static internal class XmlUtilities + { + /// + /// This method renames an XML element. Well, actually you can't directly + /// rename an XML element using the DOM, so what you have to do is create + /// a brand new XML element with the new name, and copy over all the attributes + /// and children. This method returns the new XML element object. + /// If the name is the same, does nothing and returns the element passed in. + /// + /// + /// + /// Can be null if global namespace. + /// new/renamed element + internal static XmlElementWithLocation RenameXmlElement(XmlElementWithLocation oldElement, string newElementName, string xmlNamespace) + { + if (String.Equals(oldElement.Name, newElementName, StringComparison.Ordinal) && String.Equals(oldElement.NamespaceURI, xmlNamespace, StringComparison.Ordinal)) + { + return oldElement; + } + + XmlElementWithLocation newElement = (xmlNamespace == null) + ? (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName) + : (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName, xmlNamespace); + + // Copy over all the attributes. + foreach (XmlAttribute oldAttribute in oldElement.Attributes) + { + XmlAttribute newAttribute = (XmlAttribute)oldAttribute.CloneNode(true); + newElement.SetAttributeNode(newAttribute); + } + + // Move over all the child nodes - no need to change their identity + while (oldElement.HasChildNodes) + { + // This conveniently updates FirstChild and HasChildNodes on oldElement. + newElement.AppendChild(oldElement.FirstChild); + } + + if (oldElement.ParentNode != null) + { + // Add the new element in the same place the old element was. + oldElement.ParentNode.ReplaceChild(newElement, oldElement); + } + + return newElement; + } + + + /// + /// Verifies that a name is valid for the name of an item, property, or piece of metadata. + /// If it isn't, throws an ArgumentException indicating the invalid character. + /// + /// + /// Note that our restrictions are more stringent than the XML Standard's restrictions. + /// + /// ArgumentException + /// name to validate + internal static void VerifyThrowArgumentValidElementName(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + int firstInvalidCharLocation = LocateFirstInvalidElementNameCharacter(name); + + if (-1 != firstInvalidCharLocation) + { + ErrorUtilities.ThrowArgument("OM_NameInvalid", name, name[firstInvalidCharLocation]); + } + } + + /// + /// Verifies that a name is valid for the name of an item, property, or piece of metadata. + /// If it isn't, throws an InvalidProjectException indicating the invalid character. + /// + /// + /// Note that our restrictions are more stringent than the XML Standard's restrictions. + /// + internal static void VerifyThrowProjectValidElementName(string name, IElementLocation location) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + int firstInvalidCharLocation = LocateFirstInvalidElementNameCharacter(name); + + if (-1 != firstInvalidCharLocation) + { + ProjectErrorUtilities.ThrowInvalidProject(location, "NameInvalid", name, name[firstInvalidCharLocation]); + } + } + + /// + /// Verifies that a name is valid for the name of an item, property, or piece of metadata. + /// If it isn't, throws an InvalidProjectException indicating the invalid character. + /// + /// + /// Note that our restrictions are more stringent than the XML Standard's restrictions. + /// + internal static void VerifyThrowProjectValidElementName(XmlElementWithLocation element) + { + string name = element.Name; + int firstInvalidCharLocation = LocateFirstInvalidElementNameCharacter(name); + + if (-1 != firstInvalidCharLocation) + { + ProjectErrorUtilities.ThrowInvalidProject(element.Location, "NameInvalid", name, name[firstInvalidCharLocation]); + } + } + + /// + /// Indicates if the given name is valid as the name of an item, property or metadatum. + /// + /// + /// Note that our restrictions are more stringent than those of the XML Standard. + /// + /// + /// true, if name is valid + internal static bool IsValidElementName(string name) + { + return (LocateFirstInvalidElementNameCharacter(name) == -1); + } + + /// + /// Finds the location of the first invalid character, if any, in the name of an + /// item, property, or piece of metadata. Returns the location of the first invalid character, or -1 if there are none. + /// Valid names must match this pattern: [A-Za-z_][A-Za-z_0-9\-.]* + /// Note, this is a subset of all possible valid XmlElement names: we use a subset because we also + /// have to match this same set in our regular expressions, and allowing all valid XmlElement name + /// characters in a regular expression would be impractical. + /// + /// + /// Note that our restrictions are more stringent than the XML Standard's restrictions. + /// PERF: This method has to be as fast as possible, as it's called when any item, property, or piece + /// of metadata is constructed. + /// + internal static int LocateFirstInvalidElementNameCharacter(string name) + { + // Check the first character. + // Try capital letters first. + // Optimize slightly for success. + if (!IsValidInitialElementNameCharacter(name[0])) + { + return 0; + } + + // Check subsequent characters. + // Try lower case letters first. + // Optimize slightly for success. + for (int i = 1; i < name.Length; i++) + { + if (!IsValidSubsequentElementNameCharacter(name[i])) + { + return i; + } + } + + // If we got here, the name was valid. + return -1; + } + + /// + /// Load the xml file using XMLTextReader and locate the element and attribute specified and then + /// return the value. This is a quick way to peek at the xml file whithout having the go through + /// the XMLDocument (MSDN article (Chapter 9 - Improving XML Performance)). + /// Does not throw for IO or XML issues. + /// Returns null if the attribute is not present. + /// + internal static string SniffAttributeValueFromXmlFile + ( + string projectFileName, + string elementName, + string attributeName + ) + { + string attributeValue = null; + + try + { + using (XmlTextReader xmlReader = new XmlTextReader(projectFileName)) + { + xmlReader.DtdProcessing = DtdProcessing.Ignore; + while (xmlReader.Read()) + { + if (xmlReader.NodeType == XmlNodeType.Element) + { + if (String.Compare(xmlReader.Name, elementName, StringComparison.OrdinalIgnoreCase) == 0) + { + if (xmlReader.HasAttributes) + { + for (int i = 0; i < xmlReader.AttributeCount; i++) + { + xmlReader.MoveToAttribute(i); + if (String.Compare(xmlReader.Name, attributeName, StringComparison.OrdinalIgnoreCase) == 0) + { + attributeValue = xmlReader.Value; + break; + } + } + } + // if we have already located the element then we are done + break; + } + } + } + } + } + catch (Exception ex) + { + // Ignore any IO or XML exceptions as it will be caught later on + if (ExceptionHandling.NotExpectedIoOrXmlException(ex)) + { + throw; + } + } + + return attributeValue; + } + + internal static bool IsValidInitialElementNameCharacter(char c) + { + return (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == '_'); + } + + internal static bool IsValidSubsequentElementNameCharacter(char c) + { + return (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_') || + (c == '-'); + } + } +} diff --git a/src/Utilities/ApiContract.cs b/src/Utilities/ApiContract.cs new file mode 100644 index 00000000000..dd5d54d4f76 --- /dev/null +++ b/src/Utilities/ApiContract.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Loads and represents API contract definitions +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Collections.Generic; + +namespace Microsoft.Build.Utilities +{ + /// + /// Represents an API contract definition + /// + internal struct ApiContract + { + /// + /// Name of the contract + /// + internal string Name; + + /// + /// Version of the contract + /// + internal string Version; + + /// + /// Constructor. + /// + internal ApiContract(string name, string version) + { + Name = name; + Version = version; + } + + /// + /// Returns true if this element is a "ContainedApiContracts" element. + /// + internal static bool IsContainedApiContractsElement(string elementName) + { + return String.Equals(elementName, Elements.ContainedApiContracts, StringComparison.Ordinal); + } + + /// + /// Given an XML element containing API contracts, read out all contracts within that element. + /// + internal static void ReadContractsElement(XmlElement element, ICollection apiContracts) + { + if (element != null && IsContainedApiContractsElement(element.Name)) + { + // + // + // + foreach (XmlNode contractNode in element.ChildNodes) + { + XmlElement contractElement = contractNode as XmlElement; + + if (contractElement == null || !String.Equals(contractNode.Name, Elements.ApiContract, StringComparison.Ordinal)) + { + continue; + } + + apiContracts.Add(new ApiContract + ( + contractElement.GetAttribute(Attributes.Name), + contractElement.GetAttribute(Attributes.Version) + )); + } + } + } + + /// + /// Helper class with ApiContract element names + /// + internal static class Elements + { + /// + /// Element containing a bucket of contracts + /// + public const string ContainedApiContracts = "ContainedApiContracts"; + + /// + /// Element representing an individual API contract + /// + public const string ApiContract = "ApiContract"; + } + + /// + /// Helper class with attribute names + /// + private static class Attributes + { + /// + /// Name associated with this element + /// + public const string Name = "name"; + + /// + /// Version associated with this element + /// + public const string Version = "version"; + } + } +} \ No newline at end of file diff --git a/src/Utilities/AppDomainIsolatedTask.cs b/src/Utilities/AppDomainIsolatedTask.cs new file mode 100644 index 00000000000..4caf49da856 --- /dev/null +++ b/src/Utilities/AppDomainIsolatedTask.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Security; +using System.Security.Permissions; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Utilities +{ + /// + /// This class provides the same functionality as the Task class, but derives from MarshalByRefObject so that it can be + /// instantiated in its own app domain. + /// + [LoadInSeparateAppDomain] + public abstract class AppDomainIsolatedTask : MarshalByRefObject, ITask + { + #region Constructors + + /// + /// Default (family) constructor. + /// + protected AppDomainIsolatedTask() + { + _log = new TaskLoggingHelper(this); + } + + /// + /// This (family) constructor allows derived task classes to register their resources. + /// + /// The task resources. + protected AppDomainIsolatedTask(ResourceManager taskResources) + : this() + { + _log.TaskResources = taskResources; + } + + /// + /// This (family) constructor allows derived task classes to register their resources, as well as provide a prefix for + /// composing help keywords from string resource names. If the prefix is an empty string, then string resource names will + /// be used verbatim as help keywords. For an example of how the prefix is used, see the + /// method. + /// + /// The task resources. + /// The help keyword prefix. + protected AppDomainIsolatedTask(ResourceManager taskResources, string helpKeywordPrefix) + : this(taskResources) + { + _log.HelpKeywordPrefix = helpKeywordPrefix; + } + + #endregion + + #region Properties + + /// + /// The build engine automatically sets this property to allow tasks to call back into it. + /// + /// The build engine interface available to tasks. + public IBuildEngine BuildEngine + { + get + { + return _buildEngine; + } + + set + { + _buildEngine = value; + } + } + + // callback interface on the build engine + private IBuildEngine _buildEngine; + + /// + /// The build engine sets this property if the host IDE has associated a host object with this particular task. + /// + /// The host object instance (can be null). + public ITaskHost HostObject + { + get + { + return _hostObject; + } + + set + { + _hostObject = value; + } + } + + // Optional host object that might be used by certain IDE-aware tasks. + private ITaskHost _hostObject; + + /// + /// Gets an instance of a TaskLoggingHelper class containing task logging methods. + /// + /// The logging helper object. + public TaskLoggingHelper Log + { + get + { + return _log; + } + } + + // the logging helper + private TaskLoggingHelper _log; + + /// + /// Gets or sets the task's culture-specific resources. Derived classes should register their resources either during + /// construction, or via this property, if they have localized strings. + /// + /// The task's resources (can be null). + protected ResourceManager TaskResources + { + get + { + return Log.TaskResources; + } + + set + { + Log.TaskResources = value; + } + } + + /// + /// Gets or sets the prefix used to compose help keywords from string resource names. If a task does not have help + /// keywords associated with its messages, it can ignore this property or set it to null. If the prefix is set to an empty + /// string, then string resource names will be used verbatim as help keywords. For an example of how this prefix is used, + /// see the method. + /// + /// The help keyword prefix string (can be null). + protected string HelpKeywordPrefix + { + get + { + return Log.HelpKeywordPrefix; + } + + set + { + Log.HelpKeywordPrefix = value; + } + } + + #endregion + + #region Methods + + /// + /// Must be implemented by derived class. + /// + /// true, if successful + public abstract bool Execute(); + + /// + /// Overriden to give tasks deriving from this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and task instances can expire if they take long time processing. + /// + [SecurityCritical] + public override object InitializeLifetimeService() + { + // null means infinite lease time + return null; + } + + #endregion + } +} diff --git a/src/Utilities/AssemblyFoldersExInfo.cs b/src/Utilities/AssemblyFoldersExInfo.cs new file mode 100644 index 00000000000..866893b03f6 --- /dev/null +++ b/src/Utilities/AssemblyFoldersExInfo.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Contains information about entries in the AssemblyFoldersEx registry keys. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Shared; +using Microsoft.Win32; + +namespace Microsoft.Build.Utilities +{ + /// + /// Contains information about entries in the AssemblyFoldersEx registry keys. + /// + public class AssemblyFoldersExInfo + { + /// + /// Constructor + /// + public AssemblyFoldersExInfo(RegistryHive hive, RegistryView view, string registryKey, string directoryPath, Version targetFrameworkVersion) + { + ErrorUtilities.VerifyThrowArgumentNull(registryKey, "registryKey"); + ErrorUtilities.VerifyThrowArgumentNull(directoryPath, "directoryPath"); + ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkVersion, "targetFrameworkVersion"); + + Hive = hive; + View = view; + Key = registryKey; + DirectoryPath = directoryPath; + TargetFrameworkVersion = targetFrameworkVersion; + } + + /// + /// Registry hive used + /// + public RegistryHive Hive + { + get; + private set; + } + + /// + /// Registry view used + /// + public RegistryView View + { + get; + private set; + } + + /// + /// The registry key to the component + /// + public string Key + { + get; + private set; + } + + /// + /// Folder found at the registry keys default value + /// + public string DirectoryPath + { + get; + private set; + } + + /// + /// Target framework version for the registry key + /// + public Version TargetFrameworkVersion + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/src/Utilities/AssemblyInfo.cs b/src/Utilities/AssemblyInfo.cs new file mode 100644 index 00000000000..7da239d54c4 --- /dev/null +++ b/src/Utilities/AssemblyInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Security.Permissions; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable 618 +[assembly: SecurityPermission(SecurityAction.RequestMinimum, Flags = SecurityPermissionFlag.Execution)] +#pragma warning restore 618 +#if (STANDALONEBUILD) +[assembly: AssemblyVersion(Microsoft.Build.Shared.MSBuildConstants.CurrentAssemblyVersion)] +[assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests")] +#else +[assembly: InternalsVisibleTo("Microsoft.Build.Utilities.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Utilities.Whidbey.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +#endif +// This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, +// so that we don't run into known security issues with loading libraries from unsafe locations +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/Utilities/AssemblyResources.cs b/src/Utilities/AssemblyResources.cs new file mode 100644 index 00000000000..f1005550327 --- /dev/null +++ b/src/Utilities/AssemblyResources.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Resources; +using System.Reflection; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// This class provides access to the assembly's resources. + /// + internal static class AssemblyResources + { + /// + /// Loads the specified resource string, either from the assembly's primary resources, or its shared resources. + /// + /// This method is thread-safe. + /// + /// The resource string, or null if not found. + internal static string GetString(string name) + { + // NOTE: the ResourceManager.GetString() method is thread-safe + string resource = s_resources.GetString(name, CultureInfo.CurrentUICulture); + + if (resource == null) + { + resource = s_sharedResources.GetString(name, CultureInfo.CurrentUICulture); + } + + ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); + + return resource; + } + + /// + /// Gets the assembly's primary resources i.e. the resources exclusively owned by this assembly. + /// + /// This property is thread-safe. + /// ResourceManager for primary resources. + internal static ResourceManager PrimaryResources + { + get + { + return s_resources; + } + } + + /// + /// Gets the assembly's shared resources i.e. the resources this assembly shares with other assemblies. + /// + /// This property is thread-safe. + /// ResourceManager for shared resources. + internal static ResourceManager SharedResources + { + get + { + return s_sharedResources; + } + } + + /// + /// Formats the given string using the variable arguments passed in. The current thread's culture is used for formatting. + /// + /// This method is thread-safe. + /// The string to format. + /// Arguments for formatting. + /// The formatted string. + internal static string FormatString(string unformatted, params object[] args) + { + ErrorUtilities.VerifyThrowArgumentNull(unformatted, "unformatted"); + + return ResourceUtilities.FormatString(unformatted, args); + } + + /// + /// Loads the specified resource string and optionally formats it using the given arguments. The current thread's culture + /// is used for formatting. + /// + /// + /// 1) This method requires the owner task to have registered its resources either via the Task (or TaskMarshalByRef) base + /// class constructor, or the Task.TaskResources (or AppDomainIsolatedTask.TaskResources) property. + /// 2) This method is thread-safe. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// The formatted string. + internal static string FormatResourceString(string resourceName, params object[] args) + { + ErrorUtilities.VerifyThrowArgumentNull(resourceName, "resourceName"); + + // NOTE: the ResourceManager.GetString() method is thread-safe + string resourceString = GetString(resourceName); + + return FormatString(resourceString, args); + } + + // assembly resources + private static readonly ResourceManager s_resources = new ResourceManager("Microsoft.Build.Utilities.Strings", Assembly.GetExecutingAssembly()); + // shared resources + private static readonly ResourceManager s_sharedResources = new ResourceManager("Microsoft.Build.Utilities.Strings.shared", Assembly.GetExecutingAssembly()); + } +} diff --git a/src/Utilities/CommandLineBuilder.cs b/src/Utilities/CommandLineBuilder.cs new file mode 100644 index 00000000000..d494756495b --- /dev/null +++ b/src/Utilities/CommandLineBuilder.cs @@ -0,0 +1,774 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Resources; +using System.Globalization; +using Microsoft.Build.Framework; +using System.Text.RegularExpressions; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// (1) Make sure values containing hyphens are quoted (RC at least requires this) + /// (2) Escape any embedded quotes. + /// -- Literal double quotes should be written in the form \" not "" + /// -- Backslashes falling just before doublequotes must be doubled. + /// -- Literal double quotes can only occur in pairs (you cannot pass a single literal double quote) + /// -- Functional double quotes (for example to handle spaces) are best put around both name and value + /// in switches like /Dname=value. + /// + /// + /// + /// Below are some quoting experiments, using the /D switch with the CL and RC preprocessor. + /// The /D switch is a little more tricky than most switches, because it has a name=value pair. + /// The table below contains what the preprocessor actually embeds when passed the switch in the + /// first column: + /// + /// CL via cmd line CL via response file RC + /// /DFOO="A" A A + /// /D"FOO="A"" A A A + /// /DFOO=A A A + /// /D"FOO=A" A A + /// /DFOO=""A"" A A A + /// + /// /DFOO=\"A\" "A" "A" + /// /DFOO="""A""" "A" broken "A" + /// /D"FOO=\"A\"" "A" "A" + /// /D"FOO=""A""" "A" "A" + /// + /// /DFOO="A B" A B A B + /// /D"FOO=A B" A B A B + /// + /// /D"FOO="A B"" broken + /// /DFOO=\"A B\" broken + /// /D"FOO=\"A B\"" "A B" "A B" "A B" + /// /D"FOO=""A B""" "A B" broken broken + /// + /// From my experiments (with CL and RC only) it seems that + /// -- Literal double quotes are most reliably written in the form \" not "" + /// -- Backslashes falling just before doublequotes must be doubled. + /// -- Values containing literal double quotes must be quoted. + /// -- Literal double quotes can only occur in pairs (you cannot pass a single literal double quote) + /// -- For /Dname=value style switches, functional double quotes (for example to handle spaces) are best put around both + /// name and value (in other words, these kinds of switches don't need special treatment for their '=' signs). + /// -- Values containing hyphens should be quoted; RC requires this, and CL does not mind. + /// + public class CommandLineBuilder + { + #region Constructors + + /// + /// Default constructor + /// + public CommandLineBuilder() + { + } + + /// + /// Default constructor + /// + public CommandLineBuilder(bool quoteHyphensOnCommandLine) + { + _quoteHyphens = quoteHyphensOnCommandLine; + } + + #endregion + + #region Properties + + /// + /// Returns the length of the current command + /// + public int Length + { + get + { + return CommandLine.Length; + } + } + + /// + /// Retrieves the private StringBuilder instance for inheriting classes + /// + protected StringBuilder CommandLine + { + get + { + return _commandLine; + } + } + + private StringBuilder _commandLine = new StringBuilder(); + + #endregion + + #region Basic methods + + /// + /// Return the command-line as a string. + /// + /// + override public string ToString() + { + return CommandLine.ToString(); + } + + + // Use if escaping of hyphens is supposed to take place + static private string s_allowedUnquotedRegexNoHyphen = + "^" // Beginning of line + + @"[a-z\\/:0-9\._+=]*" + + "$"; + + static private string s_definitelyNeedQuotesRegexWithHyphen = @"[|><\s,;\-""]+"; + + // Use if escaping of hyphens is not to take place + static private string s_allowedUnquotedRegexWithHyphen = + "^" // Beginning of line + + @"[a-z\\/:0-9\._\-+=]*" // Allow hyphen to be unquoted + + "$"; + static private string s_definitelyNeedQuotesRegexNoHyphen = @"[|><\s,;""]+"; + + /// + /// Should hyphens be quoted or not + /// + private bool _quoteHyphens = false; + + /// + /// Instead of defining which characters must be quoted, define + /// which characters we know its safe to not quote. This way leads + /// to more false-positives (which still work, but don't look as + /// nice coming out of the logger), but is less likely to leave a + /// security hole. + /// + private Regex _allowedUnquoted; + + /// + /// Also, define the characters that we know for certain need quotes. + /// This is partly to document which characters we know can cause trouble + /// and partly as a sanity check against a bug creeping in. + /// + private Regex _definitelyNeedQuotes; + + /// + /// Use a private property so that we can lazy initialize the regex + /// + private Regex DefinitelyNeedQuotes + { + get + { + if (null == _definitelyNeedQuotes) + { + if (_quoteHyphens) + { + _definitelyNeedQuotes = new Regex(s_definitelyNeedQuotesRegexWithHyphen, RegexOptions.None); + } + else + { + // We do not want to quote hyphen + _definitelyNeedQuotes = new Regex(s_definitelyNeedQuotesRegexNoHyphen, RegexOptions.None); + } + } + return _definitelyNeedQuotes; + } + } + + /// + /// Use a private getter property to we can lazy initialize the regex + /// + private Regex AllowedUnquoted + { + get + { + if (null == _allowedUnquoted) + { + if (_quoteHyphens) + { + _allowedUnquoted = new Regex(s_allowedUnquotedRegexNoHyphen, RegexOptions.IgnoreCase); + } + else + { + _allowedUnquoted = new Regex(s_allowedUnquotedRegexWithHyphen, RegexOptions.IgnoreCase); + } + } + return _allowedUnquoted; + } + } + + /// + /// Checks the given switch parameter to see if it must/can be quoted. + /// + /// the string to examine for characters that require quoting + /// true, if parameter should be quoted + virtual protected bool IsQuotingRequired(string parameter) + { + bool isQuotingRequired = false; + + if (parameter != null) + { + #region Security Note: About cross-parameter injection + /* + If string parameters have whitespace in them, then a possible attack would + be like the following: + + MyFile.ico /out:c:\windows\system32\notepad.exe + + + + Since we just build up a command-line to pass into CSC.EXE, without quoting, + the project might overwrite notepad.exe. + + If there are spaces in the parameter, then we must quote that parameter. + */ + #endregion + bool hasAllUnquotedCharacters = AllowedUnquoted.IsMatch(parameter); + bool hasSomeQuotedCharacters = DefinitelyNeedQuotes.IsMatch(parameter); + + isQuotingRequired = !hasAllUnquotedCharacters; + isQuotingRequired = isQuotingRequired || hasSomeQuotedCharacters; + + Debug.Assert(!hasAllUnquotedCharacters || !hasSomeQuotedCharacters, + "At least one of allowedUnquoted or definitelyNeedQuotes is wrong."); + } + + return isQuotingRequired; + } + + /// + /// Add a space to the specified string if and only if it's not empty. + /// + /// + /// This is a pretty obscure method and so it's only available to inherited classes. + /// + protected void AppendSpaceIfNotEmpty() + { + if (CommandLine.Length != 0 && CommandLine[CommandLine.Length - 1] != ' ') + { + CommandLine.Append(" "); + } + } + + #endregion + + #region Methods for use in inherited classes, do not prepend a space before doing their thing + + /// + /// Appends a string. Quotes are added if they are needed. + /// This method does not append a space to the command line before executing. + /// + /// + /// Escapes any double quotes in the string. + /// + /// The string to append + protected void AppendTextWithQuoting(string textToAppend) + { + AppendQuotedTextToBuffer(CommandLine, textToAppend); + } + + /// + /// Appends given text to the buffer after first quoting the text if necessary. + /// + /// + /// + protected void AppendQuotedTextToBuffer(StringBuilder buffer, string unquotedTextToAppend) + { + ErrorUtilities.VerifyThrowArgumentNull(buffer, "buffer"); + + if (unquotedTextToAppend != null) + { + bool addQuotes = IsQuotingRequired(unquotedTextToAppend); + + if (addQuotes) + { + buffer.Append('"'); + } + + // Count the number of quotes + int literalQuotes = 0; + for (int i = 0; i < unquotedTextToAppend.Length; i++) + { + if ('"' == unquotedTextToAppend[i]) + { + literalQuotes++; + } + } + if (literalQuotes > 0) + { + // Command line parsers typically break if you attempt to pass in an odd number of + // escaped double quotes. We can only error if there isn't an even number. + ErrorUtilities.VerifyThrowArgument(((literalQuotes % 2) == 0), "General.StringsCannotContainOddNumberOfDoubleQuotes", unquotedTextToAppend); + // Replace any \" sequences with \\" + unquotedTextToAppend = unquotedTextToAppend.Replace("\\\"", "\\\\\""); + // Now replace any " with \" + unquotedTextToAppend = unquotedTextToAppend.Replace("\"", "\\\""); + } + + buffer.Append(unquotedTextToAppend); + + // Be careful any trailing slash doesn't escape the quote we're about to add + if (addQuotes && unquotedTextToAppend.EndsWith("\\", StringComparison.Ordinal)) + { + buffer.Append('\\'); + } + + if (addQuotes) + { + buffer.Append('"'); + } + } + } + + /// + /// Appends a string. No quotes are added. + /// This method does not append a space to the command line before executing. + /// + /// + /// AppendTextUnquoted(@"Folder name\filename.cs") => "Folder name\\filename.cs" + /// + /// + /// In the future, this function may fixup 'textToAppend' to handle + /// literal embedded quotes. + /// + /// The string to append + public void AppendTextUnquoted(string textToAppend) + { + if (textToAppend != null) + { + CommandLine.Append(textToAppend); + } + } + + /// + /// Appends a file name. Quotes are added if they are needed. + /// If the first character of the file name is a dash, ".\" is prepended to avoid confusing the file name with a switch + /// This method does not append a space to the command line before executing. + /// + /// + /// AppendFileNameWithQuoting("-StrangeFileName.cs") => ".\-StrangeFileName.cs" + /// + /// + /// In the future, this function may fixup 'text' to handle + /// literal embedded quotes. + /// + /// The file name to append + protected void AppendFileNameWithQuoting(string fileName) + { + if (fileName != null) + { + // Don't let injection attackers escape from our quotes by sticking in + // their own quotes. Quotes are illegal. + VerifyThrowNoEmbeddedDoubleQuotes(string.Empty, fileName); + + if ((fileName.Length != 0) && (fileName[0] == '-')) + { + AppendTextWithQuoting(".\\" + fileName); + } + else + { + AppendTextWithQuoting(fileName); + } + } + } + + #endregion + + #region Appending file names + + /// + /// Appends a file name quoting it if necessary. + /// This method appends a space to the command line (if it's not currently empty) before the file name. + /// + /// + /// AppendFileNameIfNotNull("-StrangeFileName.cs") => ".\-StrangeFileName.cs" + /// + /// File name to append, if it's null this method has no effect + public void AppendFileNameIfNotNull(string fileName) + { + if (fileName != null) + { + // Don't let injection attackers escape from our quotes by sticking in + // their own quotes. Quotes are illegal. + VerifyThrowNoEmbeddedDoubleQuotes(string.Empty, fileName); + + AppendSpaceIfNotEmpty(); + AppendFileNameWithQuoting(fileName); + } + } + + /// + /// Appends a file name quoting it if necessary. + /// This method appends a space to the command line (if it's not currently empty) before the file name. + /// + /// + /// See the string overload version + /// + /// File name to append, if it's null this method has no effect + public void AppendFileNameIfNotNull(ITaskItem fileItem) + { + if (fileItem != null) + { + // Don't let injection attackers escape from our quotes by sticking in + // their own quotes. Quotes are illegal. + VerifyThrowNoEmbeddedDoubleQuotes(string.Empty, fileItem.ItemSpec); + + AppendFileNameIfNotNull(fileItem.ItemSpec); + } + } + + /// + /// Appends array of file name strings, quoting them if necessary, delimited by a delimiter. + /// This method appends a space to the command line (if it's not currently empty) before the file names. + /// + /// + /// AppendFileNamesIfNotNull(new string[] {"Alpha.cs", "Beta.cs"}, ",") => "Alpha.cs,Beta.cs" + /// + /// File names to append, if it's null this method has no effect + /// The delimiter between file names + public void AppendFileNamesIfNotNull(string[] fileNames, string delimiter) + { + ErrorUtilities.VerifyThrowArgumentNull(delimiter, "delimiter"); + + if ((fileNames != null) && (fileNames.Length > 0)) + { + // Don't let injection attackers escape from our quotes by sticking in + // their own quotes. Quotes are illegal. + for (int i = 0; i < fileNames.Length; ++i) + { + VerifyThrowNoEmbeddedDoubleQuotes(string.Empty, fileNames[i]); + } + + AppendSpaceIfNotEmpty(); + for (int i = 0; i < fileNames.Length; ++i) + { + if (i != 0) + { + AppendTextUnquoted(delimiter); + } + + AppendFileNameWithQuoting(fileNames[i]); + } + } + } + + /// + /// Appends array of ITaskItem specs as file names, quoting them if necessary, delimited by a delimiter. + /// This method appends a space to the command line (if it's not currently empty) before the file names. + /// + /// + /// See the string[] overload version + /// + /// Task items to append, if null this method has no effect + /// Delimiter to put between items in the command line + public void AppendFileNamesIfNotNull(ITaskItem[] fileItems, string delimiter) + { + ErrorUtilities.VerifyThrowArgumentNull(delimiter, "delimiter"); + + if ((fileItems != null) && (fileItems.Length > 0)) + { + // Don't let injection attackers escape from our quotes by sticking in + // their own quotes. Quotes are illegal. + for (int i = 0; i < fileItems.Length; ++i) + { + if (fileItems[i] != null) + { + VerifyThrowNoEmbeddedDoubleQuotes(string.Empty, fileItems[i].ItemSpec); + } + } + + AppendSpaceIfNotEmpty(); + for (int i = 0; i < fileItems.Length; ++i) + { + if (i != 0) + { + AppendTextUnquoted(delimiter); + } + + if (fileItems[i] != null) + { + AppendFileNameWithQuoting(fileItems[i].ItemSpec); + } + } + } + } + + #endregion + + #region Appending switches with quoted parameters + + /// + /// Appends a command-line switch that has no separate value, without any quoting. + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// AppendSwitch("/utf8output") => "/utf8output" + /// + /// The switch to append to the command line, may not be null + public void AppendSwitch(string switchName) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + + AppendSpaceIfNotEmpty(); + AppendTextUnquoted(switchName); + } + + /// + /// Appends a command-line switch that takes a single string parameter, quoting the parameter if necessary. + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// AppendSwitchIfNotNull("/source:", "File Name.cs") => "/source:\"File Name.cs\"" + /// + /// The switch to append to the command line, may not be null + /// Switch parameter to append, quoted if necessary. If null, this method has no effect. + public void AppendSwitchIfNotNull(string switchName, string parameter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + + if (parameter != null) + { + // Now, stick the parameter in. + AppendSwitch(switchName); + AppendTextWithQuoting(parameter); + } + } + + /// + /// Throws if the parameter has a double-quote in it. This is used to prevent parameter + /// injection. It's virtual so that tools can override this method if they want to have quotes escaped in filenames + /// + /// Switch name for error message + /// Switch parameter to scan + protected virtual void VerifyThrowNoEmbeddedDoubleQuotes(string switchName, string parameter) + { + if (parameter != null) + { + if (string.IsNullOrEmpty(switchName)) + { + ErrorUtilities.VerifyThrowArgument + ( + -1 == parameter.IndexOf('"'), + "General.QuotesNotAllowedInThisKindOfTaskParameterNoSwitchName", + parameter + ); + } + else + { + ErrorUtilities.VerifyThrowArgument + ( + -1 == parameter.IndexOf('"'), + "General.QuotesNotAllowedInThisKindOfTaskParameter", + switchName, + parameter + ); + } + } + } + + /// + /// Append a switch [overload] + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// See the string overload version + /// + /// The switch to append to the command line, may not be null + /// Switch parameter to append, quoted if necessary. If null, this method has no effect. + public void AppendSwitchIfNotNull(string switchName, ITaskItem parameter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + + if (parameter != null) + { + AppendSwitchIfNotNull(switchName, parameter.ItemSpec); + } + } + + /// + /// Appends a command-line switch that takes a string[] parameter, + /// and add double-quotes around the individual filenames if necessary. + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// AppendSwitchIfNotNull("/sources:", new string[] {"Alpha.cs", "Be ta.cs"}, ";") => "/sources:Alpha.cs;\"Be ta.cs\"" + /// + /// The switch to append to the command line, may not be null + /// Switch parameters to append, quoted if necessary. If null, this method has no effect. + /// Delimiter to put between individual parameters, may not be null (may be empty) + public void AppendSwitchIfNotNull(string switchName, string[] parameters, string delimiter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + ErrorUtilities.VerifyThrowArgumentNull(delimiter, "delimiter"); + + if ((parameters != null) && (parameters.Length > 0)) + { + AppendSwitch(switchName); + bool first = true; + foreach (string parameter in parameters) + { + if (!first) + { + AppendTextUnquoted(delimiter); + } + first = false; + AppendTextWithQuoting(parameter); + } + } + } + + /// + /// Appends a command-line switch that takes a ITaskItem[] parameter, + /// and add double-quotes around the individual filenames if necessary. + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// See the string[] overload version + /// + /// The switch to append to the command line, may not be null + /// Switch parameters to append, quoted if necessary. If null, this method has no effect. + /// Delimiter to put between individual parameters, may not be null (may be empty) + public void AppendSwitchIfNotNull(string switchName, ITaskItem[] parameters, string delimiter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + ErrorUtilities.VerifyThrowArgumentNull(delimiter, "delimiter"); + + if ((parameters != null) && (parameters.Length > 0)) + { + AppendSwitch(switchName); + bool first = true; + foreach (ITaskItem parameter in parameters) + { + if (!first) + { + AppendTextUnquoted(delimiter); + } + first = false; + + if (parameter != null) + { + AppendTextWithQuoting(parameter.ItemSpec); + } + } + } + } + + #endregion + + #region Append switches with unquoted parameters + + /// + /// Appends the literal parameter without trying to quote. + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// AppendSwitchUnquotedIfNotNull("/source:", "File Name.cs") => "/source:File Name.cs" + /// + /// The switch to append to the command line, may not be null + /// Switch parameter to append, not quoted. If null, this method has no effect. + public void AppendSwitchUnquotedIfNotNull(string switchName, string parameter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + + if (parameter != null) + { + // Now, stick the parameter in. + AppendSwitch(switchName); + AppendTextUnquoted(parameter); + } + } + + /// + /// Appends the literal parameter without trying to quote. + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// See the string overload version + /// + /// The switch to append to the command line, may not be null + /// Switch parameter to append, not quoted. If null, this method has no effect. + public void AppendSwitchUnquotedIfNotNull(string switchName, ITaskItem parameter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + + if (parameter != null) + { + AppendSwitchUnquotedIfNotNull(switchName, parameter.ItemSpec); + } + } + + /// + /// Appends a command-line switch that takes a string[] parameter, not quoting the individual parameters + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// AppendSwitchUnquotedIfNotNull("/sources:", new string[] {"Alpha.cs", "Be ta.cs"}, ";") => "/sources:Alpha.cs;Be ta.cs" + /// + /// The switch to append to the command line, may not be null + /// Switch parameters to append, not quoted. If null, this method has no effect. + /// Delimiter to put between individual parameters, may not be null (may be empty) + public void AppendSwitchUnquotedIfNotNull(string switchName, string[] parameters, string delimiter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + ErrorUtilities.VerifyThrowArgumentNull(delimiter, "delimiter"); + + if ((parameters != null) && (parameters.Length > 0)) + { + AppendSwitch(switchName); + bool first = true; + foreach (string parameter in parameters) + { + if (!first) + { + AppendTextUnquoted(delimiter); + } + first = false; + AppendTextUnquoted(parameter); + } + } + } + + /// + /// Appends a command-line switch that takes a ITaskItem[] parameter, not quoting the individual parameters + /// This method appends a space to the command line (if it's not currently empty) before the switch. + /// + /// + /// See the string[] overload version + /// + /// The switch to append to the command line, may not be null + /// Switch parameters to append, not quoted. If null, this method has no effect. + /// Delimiter to put between individual parameters, may not be null (may be empty) + public void AppendSwitchUnquotedIfNotNull(string switchName, ITaskItem[] parameters, string delimiter) + { + ErrorUtilities.VerifyThrowArgumentNull(switchName, "switchName"); + ErrorUtilities.VerifyThrowArgumentNull(delimiter, "delimiter"); + + if ((parameters != null) && (parameters.Length > 0)) + { + AppendSwitch(switchName); + bool first = true; + foreach (ITaskItem parameter in parameters) + { + if (!first) + { + AppendTextUnquoted(delimiter); + } + first = false; + + if (parameter != null) + { + AppendTextUnquoted(parameter.ItemSpec); + } + } + } + } + + #endregion + } +} diff --git a/src/Utilities/ExtensionSDK.cs b/src/Utilities/ExtensionSDK.cs new file mode 100644 index 00000000000..f8d0c929395 --- /dev/null +++ b/src/Utilities/ExtensionSDK.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Internal representation of the extension SDK +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Utilities +{ + /// + /// Structure to represent an extension sdk + /// + internal class ExtensionSDK + { + /// + /// Path to the platform sdk may be null if not a platform sdk. + /// + private string _path; + + /// + /// Extension SDK moniker + /// + private string _sdkMoniker; + + /// + /// SDK version + /// + private Version _sdkVersion; + + /// + /// SDK identifier + /// + private string _sdkIdentifier; + + /// + /// Object containing the properties in the SDK manifest + /// + private SDKManifest _manifest = null; + + /// + /// Caches minimum Visual Studio version from the manifest + /// + private Version _minVSVersion = null; + + /// + /// Caches max platform version from the manifest + /// + private Version _maxPlatformVersion = null; + + /// + /// Constructor + /// + public ExtensionSDK(string extensionSdkMoniker, string extensionSdkPath) + { + _sdkMoniker = extensionSdkMoniker; + _path = extensionSdkPath; + } + + /// + /// SDK version from the moniker + /// + public Version Version + { + get + { + if (_sdkVersion == null) + { + ParseMoniker(_sdkMoniker); + } + + return _sdkVersion; + } + } + + /// + /// SDK identifier from the moniker + /// + public string Identifier + { + get + { + if (_sdkIdentifier == null) + { + ParseMoniker(_sdkMoniker); + } + + return _sdkIdentifier; + } + } + + /// + /// The type of the SDK. + /// + public SDKType SDKType + { + get + { + return Manifest.SDKType; + } + } + + /// + /// Minimum Visual Studio version from SDKManifest.xml + /// + public Version MinVSVersion + { + get + { + if (_minVSVersion == null && Manifest.MinVSVersion != null) + { + if (!Version.TryParse(Manifest.MinVSVersion, out _minVSVersion)) + { + _minVSVersion = null; + } + } + + return _minVSVersion; + } + } + + /// + /// Maximum platform version from SDKManifest.xml + /// + public Version MaxPlatformVersion + { + get + { + if (_maxPlatformVersion == null && Manifest.MaxPlatformVersion != null) + { + if (!Version.TryParse(Manifest.MaxPlatformVersion, out _maxPlatformVersion)) + { + _maxPlatformVersion = null; + } + } + + return _maxPlatformVersion; + } + } + + /// + /// Api contracts from the SDKManifest, if any + /// + public ICollection ApiContracts + { + get + { + return Manifest.ApiContracts; + } + } + + /// + /// Reference to the manifest object + /// Maks sure manifest is instantiated only once + /// + private SDKManifest Manifest + { + get + { + if (_manifest == null) + { + // Load manifest from disk the first time it is needed + _manifest = new SDKManifest(_path); + } + + return _manifest; + } + } + + /// + /// Parse SDK moniker + /// + internal void ParseMoniker(string moniker) + { + string[] properties = moniker.Split(','); + + foreach (string property in properties) + { + string[] words = property.Split('='); + + if (words[0].Trim().StartsWith("Version", StringComparison.OrdinalIgnoreCase)) + { + Version ver = null; + if (words.Length > 1 && System.Version.TryParse(words[1], out ver)) + { + _sdkVersion = ver; + } + } + else + { + _sdkIdentifier = words[0]; + } + } + } + } +} \ No newline at end of file diff --git a/src/Utilities/FxCopExclusions/Microsoft.Build.Utilities.Suppressions.cs b/src/Utilities/FxCopExclusions/Microsoft.Build.Utilities.Suppressions.cs new file mode 100644 index 00000000000..0a30b020544 --- /dev/null +++ b/src/Utilities/FxCopExclusions/Microsoft.Build.Utilities.Suppressions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// FxCop Suppression file +// To Use: +// Add module level suppressions to this file to have them suppressed in the assembly +// + +using System.Diagnostics.CodeAnalysis; + +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="0", Scope="module", Target="microsoft.build.utilities.core.dll", Justification="It's been named this way for several versions now.")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="v", Justification="v in v3.5 is correctly cased")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="5", Justification="5 in v3.5 is correctly spelled")] +[module: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification="We delay sign our assemblies.")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetInstalledSDKLocations()", Justification="SDK is the proper casing")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetInstalledSDKLocations(System.String,System.String,System.String,System.String)", Justification="SDK is the proper casing")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetInstalledSDKLocation(System.String)", Justification="SDK casing is correct")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetInstalledSDKLocation(System.String,System.String,System.String)", Justification="SDK casing is correct")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetInstalledSDKLocation(System.String,System.String,System.String,System.String,System.String)", Justification="SDK casing is correct")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetInstalledSDKLocations(System.String,System.String)", Justification="SDK casing is correct")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Utilities.ToolLocationHelper.#GetWindowsSDKMetadataFolderLocations()", Justification="SDK casing is correct")] + +#endif + diff --git a/src/Utilities/Logger.cs b/src/Utilities/Logger.cs new file mode 100644 index 00000000000..b9b603ff145 --- /dev/null +++ b/src/Utilities/Logger.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// This helper base class provides default functionality for loggers that handle events raised by the build engine. This + /// class can only be instantiated in a derived form. + /// + public abstract class Logger : ILogger + { + /// + /// Default (family) constructor. + /// + protected Logger() + { + // do nothing + } + + /// + /// Gets or sets the level of detail to show in the event log. + /// + /// The verbosity level. + public virtual LoggerVerbosity Verbosity + { + get + { + return _verbosity; + } + + set + { + _verbosity = value; + } + } + + // level of detail for logging + private LoggerVerbosity _verbosity; + + /// + /// Gets or sets the user-specified parameters to the logger. Derived class can ignore if it doesn't take parameters. + /// + /// The parameter string (can be null). + public virtual string Parameters + { + get + { + return _parameters; + } + + set + { + _parameters = value; + } + } + + // logger parameters (can be null) + private string _parameters; + + /// + /// Must be implemented by derived class to subscribe to build events. + /// + /// The available events that a logger can subscribe to. + public abstract void Initialize(IEventSource eventSource); + + /// + /// Derived class should override if it needs to release any resources. + /// + public virtual void Shutdown() + { + // do nothing + } + + /// + /// Generates a message in the default format from a BuildErrorEventArgs object. + /// + /// Error event arguments + /// Error message in canonical format + public virtual string FormatErrorEvent(BuildErrorEventArgs args) + { + return EventArgsFormatting.FormatEventMessage(args); + } + + /// + /// Generates a message in the default format from a BuildWarningEventArgs object. + /// + /// Warning event arguments + /// Warning message in canonical format + public virtual string FormatWarningEvent(BuildWarningEventArgs args) + { + return EventArgsFormatting.FormatEventMessage(args); + } + + /// + /// Determines whether the current verbosity setting is at least the value + /// passed in. + /// + /// + /// + public bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity) + { + return (_verbosity >= checkVerbosity); + } + } +} diff --git a/src/Utilities/Microsoft.Build.Utilities.csproj b/src/Utilities/Microsoft.Build.Utilities.csproj new file mode 100644 index 00000000000..54b69ecd172 --- /dev/null +++ b/src/Utilities/Microsoft.Build.Utilities.csproj @@ -0,0 +1,233 @@ + + + + + Debug + AnyCPU + {828566EE-6F6A-4EF4-98B0-513F7DF9C628} + Library + Properties + Microsoft.Build.Utilities + Microsoft.Build.Utilities.Core + true + + + + + + + + + + true + + + AssemblyFoldersEx.cs + true + + + CanonicalError.cs + true + + + Constants.cs + true + + + ExtensionFoldersRegistryKey.cs + + + FileDelegates.cs + true + + + Collections\CopyOnWriteDictionary.cs + + + EncodingUtilities.cs + true + + + ErrorUtilities.cs + true + + + EscapingUtilities.cs + true + + + EventArgsFormatting.cs + true + + + ExceptionHandling.cs + + + FileUtilities.cs + true + + + FileMatcher.cs + true + + + FileUtilitiesRegex.cs + true + + + FrameworkLocationHelper.cs + true + + + Collections\HybridDictionary.cs + + + InternalErrorException.cs + + + + + true + + + NativeMethodsShared.cs + true + + + InprocTrackingNativeMethods.cs + true + + + OpportunisticIntern.cs + + + Collections\ReadOnlyEmptyCollection.cs + + + Collections\ReadOnlyEmptyDictionary.cs + + + Collections\ReadOnlyEmptyList.cs + + + RegistryDelegates.cs + true + + + RegistryHelper.cs + true + + + ResourceUtilities.cs + true + + + StringBuilderCache.cs + true + + + TaskLoggingHelper.cs + True + + + + + VersionUtilities.cs + true + + + ToolsetElement.cs + true + + + + true + + + + true + + + true + + + true + + + true + + + true + + + + true + + + + + + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + Designer + + + Strings.shared.resx + Designer + Microsoft.Build.Utilities.Strings.shared.resources + + + + + + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + \ No newline at end of file diff --git a/src/Utilities/MuxLogger.cs b/src/Utilities/MuxLogger.cs new file mode 100644 index 00000000000..f42a1f810c0 --- /dev/null +++ b/src/Utilities/MuxLogger.cs @@ -0,0 +1,1315 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of the Multiplexing logger. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// This is a multiplexing logger. The purpose of this logger is to allow the registration and deregistration of + /// multiple loggers during the build. This is to support the VS IDE scenario where loggers are regisered and unregistered + /// for each project system's build request. This means one physical build may have multiple logical builds + /// each with their own set of loggers. + /// + /// The Mux logger will register itself with the build manager as a regular central /l style logger. + /// It will be responsible for recieving messages from the build manager and route them to the correct + /// logger based on the logical build the message came from. + /// + /// Requirements: + /// 1) Multiplexing logger will be registered at the beginning of the build manager's Begin build + /// Any loggers registered before the build manager actually started building will get the build started event at the same time as the MUX logger + /// Any loggers registered after the build manager starts the build will get a synthesised build started event. The event cannot be cached because the + /// timestamp of the build started event is determined when the event is created, caching the event would give incorrect build times in the loggers registered to the MUX. + /// + /// 2) The MUX logger will be initialized by the build manager. + /// The mux will listen to all events on the event source from the build manager and will route events correctly to the registered loggers. + /// + /// 3) The MUX logger will be shutdown when the build is finished in end build . At this time it will un-register any loggers attached to it. + /// + /// 4) The MUX logger will log the build finsished event when the project finished event for the first project started event is seen for each logger. + /// + /// Registering Loggers: + /// + /// The multiplexing logger will function in the following way: + /// A logger will be passed to the MUX Register logger method with a submission ID which will be used to route a the message to the correct logger. + /// A new event source will be created so that the logger passed in can be registered to that event source + /// If the build started event has already been logged the MUX logger will create a new BuildStartedEvent and send that to the event source. + /// + /// UnRegisterLogger: + /// When a build submission is completed the UnregisterLogger method will be called with the submission ID. + /// At this point we will look up the success state of the project finished event for the submission ID and log a build finished event to the logger. + /// The event source will be cleaned up. This may be interesting because the unregister will come from a thread other than what is doign the logging. + /// This may create a Synchronization issue, if unregister is called while events are being logged. + /// + /// UNDONE: If we can use ErrorUtilities, replace all InvalidOperation and Argument exceptions with the appropriate calls. + /// + /// + public class MuxLogger : INodeLogger + { + /// + /// The mapping of submission IDs to the submission record. + /// + private Dictionary _submissionRecords = new Dictionary(); + + /// + /// Keep the build started event if it has been seen, we need the message off it. + /// + private BuildStartedEventArgs _buildStartedEvent = null; + + /// + /// Event source which events from the build manager will be raised on. + /// + private IEventSource _eventSourceForBuild = null; + + /// + /// The handler for the build started event + /// + private BuildStartedEventHandler _buildStartedEventHandler = null; + + /// + /// The handler for the build finished event. + /// + private BuildFinishedEventHandler _buildFinishedEventHandler = null; + + /// + /// The handler for the project started event. + /// + private ProjectStartedEventHandler _projectStartedEventHandler = null; + + /// + /// The handler for the project finished event. + /// + private ProjectFinishedEventHandler _projectFinishedEventHandler = null; + + /// + /// Dictionary mapping submission id to projects in progress. + /// + private Dictionary _submissionProjectsInProgress = new Dictionary(); + + /// + /// The maximum node count as specified in the call to Initialze() + /// + private int _maxNodeCount = 1; + + /// + /// Constructor. + /// + public MuxLogger() + { + _buildStartedEventHandler = new BuildStartedEventHandler(BuildStarted); + _buildFinishedEventHandler = new BuildFinishedEventHandler(BuildFinished); + _projectStartedEventHandler = new ProjectStartedEventHandler(ProjectStarted); + _projectFinishedEventHandler = new ProjectFinishedEventHandler(ProjectFinished); + } + + /// + /// Required for ILogger interface + /// + public LoggerVerbosity Verbosity + { + get; + set; + } + + /// + /// Required for the ILoggerInterface + /// + public string Parameters + { + get; + set; + } + + /// + /// Initialize the logger. + /// + public void Initialize(IEventSource eventSource) + { + Initialize(eventSource, 1); + } + + /// + /// Initialize the logger. + /// + public void Initialize(IEventSource eventSource, int maxNodeCount) + { + if (_eventSourceForBuild != null) + { + throw new InvalidOperationException("MuxLogger already initialized."); + } + + _eventSourceForBuild = eventSource; + _maxNodeCount = maxNodeCount; + + _eventSourceForBuild.BuildStarted += _buildStartedEventHandler; + _eventSourceForBuild.BuildFinished += _buildFinishedEventHandler; + _eventSourceForBuild.ProjectStarted += _projectStartedEventHandler; + _eventSourceForBuild.ProjectFinished += _projectFinishedEventHandler; + } + + /// + /// Shutdown the mux logger and clear out any state + /// + public void Shutdown() + { + if (_eventSourceForBuild == null) + { + throw new InvalidOperationException("MuxLogger not initialized."); + } + + // Go through ALL loggers and shutdown any which remain. + List recordsToShutdown; + lock (_submissionRecords) + { + recordsToShutdown = new List(_submissionRecords.Values); + _submissionRecords.Clear(); + } + + foreach (SubmissionRecord record in recordsToShutdown) + { + record.Shutdown(); + } + + _eventSourceForBuild.ProjectStarted -= _projectStartedEventHandler; + _eventSourceForBuild.ProjectFinished -= _projectFinishedEventHandler; + _eventSourceForBuild.BuildStarted -= _buildStartedEventHandler; + _eventSourceForBuild.BuildFinished -= _buildFinishedEventHandler; + + _submissionProjectsInProgress.Clear(); + _buildStartedEvent = null; + _eventSourceForBuild = null; + } + + /// + /// This method will register a logger on the MUX logger and then raise a build started event if the build started event has already been logged + /// + public void RegisterLogger(int submissionId, ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + if (_eventSourceForBuild == null) + { + throw new InvalidOperationException("Cannot register a logger before the MuxLogger has been initialized."); + } + + if (_submissionProjectsInProgress.ContainsKey(submissionId)) + { + throw new InvalidOperationException("Cannot register a logger for a submission once it has started."); + } + + // See if another logger has been registered already with the same submission ID + SubmissionRecord record = null; + lock (_submissionRecords) + { + if (!_submissionRecords.TryGetValue(submissionId, out record)) + { + record = new SubmissionRecord(submissionId, _eventSourceForBuild, _buildStartedEvent, _maxNodeCount); + _submissionRecords.Add(submissionId, record); + } + } + + record.AddLogger(logger); + } + + /// + /// Unregisters all the loggers for a given submission id. + /// + public bool UnregisterLoggers(int submissionId) + { + SubmissionRecord record; + lock (_submissionRecords) + { + if (!_submissionRecords.TryGetValue(submissionId, out record)) + { + return false; + } + + _submissionRecords.Remove(submissionId); + } + + record.Shutdown(); + return true; + } + + /// + /// Initialize the logger + /// + internal void InitializeLogger(List loggerList, ILogger logger, IEventSource sourceForLogger) + { + // Node loggers are central /l loggers which can understand how many CPU's the build is running with, they are only different in that + // they can take a number of CPU + INodeLogger nodeLogger = logger as INodeLogger; + if (null != nodeLogger) + { + nodeLogger.Initialize(sourceForLogger, 1); + } + else + { + logger.Initialize(sourceForLogger); + } + } + + /// + /// Receives the build started event for the whole build. + /// + private void BuildStarted(object sender, BuildStartedEventArgs e) + { + _buildStartedEvent = e; + lock (_submissionRecords) + { + foreach (SubmissionRecord record in _submissionRecords.Values) + { + record.SetGlobalBuildStartedEvent(e); + } + } + } + + /// + /// Receives the build finished event. + /// + private void BuildFinished(object sender, BuildFinishedEventArgs e) + { + _buildStartedEvent = null; + } + + /// + /// Receives the project started event and records the submission as being in-progress. + /// + private void ProjectStarted(object sender, ProjectStartedEventArgs e) + { + int value = 0; + _submissionProjectsInProgress.TryGetValue(e.BuildEventContext.SubmissionId, out value); + _submissionProjectsInProgress[e.BuildEventContext.SubmissionId] = value + 1; + } + + /// + /// Receives the project finished event. + /// + private void ProjectFinished(object sender, ProjectFinishedEventArgs e) + { + int value = _submissionProjectsInProgress[e.BuildEventContext.SubmissionId]; + + if (value == 1) + { + _submissionProjectsInProgress.Remove(e.BuildEventContext.SubmissionId); + SubmissionRecord record = null; + lock (_submissionRecords) + { + if (_submissionRecords.TryGetValue(e.BuildEventContext.SubmissionId, out record)) + { + _submissionRecords.Remove(e.BuildEventContext.SubmissionId); + } + } + } + else + { + _submissionProjectsInProgress[e.BuildEventContext.SubmissionId] = value - 1; + } + } + + /// + /// This class holds everything the logger needs to know about a particular submission, including the event source. + /// + private class SubmissionRecord : MarshalByRefObject, IEventSource + { + #region Fields + /// + /// Object used to synchronize access to internals. + /// + private Object _syncLock = new Object(); + + /// + /// List of loggers + /// + private List _loggers = null; + + /// + /// The maximum node count + /// + private int _maxNodeCount; + + /// + /// The event source which will have events raised from the buld manager. + /// + private IEventSource _eventSourceForBuild = null; + + /// + /// The buildStartedEvent to use when synthesizing the build started event. + /// + private BuildStartedEventArgs _buildStartedEvent = null; + + /// + /// The project build event coontext for the first project started event seen, this is the root of the submission. + /// + private BuildEventContext _firstProjectStartedEventContext = null; + + /// + /// SubmissionId for this submission record + /// + private int _submissionId = 0; + + /// + /// Has the record been shutdown yet. + /// + private bool _shutdown = false; + #endregion + + // Keep instance of event handlers so they can be unregistered at the end of the submissionID. + // If we wait for the entire build to finish we will leak the handlers until we unregister ALL of the handlers from the + // event source on the build manager. + #region RegisteredHandlers + /// + /// Even hander for "anyEvent" this is a handler which will be called from each of the other event handlers + /// + private AnyEventHandler _anyEventHandler = null; + + /// + /// Handle the Build Finished event + /// + private BuildFinishedEventHandler _buildFinishedEventHandler = null; + + /// + /// Handle the Build started event + /// + private BuildStartedEventHandler _buildStartedEventHandler = null; + + /// + /// Handle custom build events + /// + private CustomBuildEventHandler _customBuildEventHandler = null; + + /// + /// Handle error events + /// + private BuildErrorEventHandler _buildErrorEventHandler = null; + + /// + /// Handle message events + /// + private BuildMessageEventHandler _buildMessageEventHandler = null; + + /// + /// Handle project finished events + /// + private ProjectFinishedEventHandler _projectFinishedEventHandler = null; + + /// + /// Handle project started events + /// + private ProjectStartedEventHandler _projectStartedEventHandler = null; + + /// + /// Handle build sttus events + /// + private BuildStatusEventHandler _buildStatusEventHandler = null; + + /// + /// Handle target finished events + /// + private TargetFinishedEventHandler _targetFinishedEventHandler = null; + + /// + /// Handle target started events + /// + private TargetStartedEventHandler _targetStartedEventHandler = null; + + /// + /// Handle task finished + /// + private TaskFinishedEventHandler _taskFinishedEventHandler = null; + + /// + /// Handle task started + /// + private TaskStartedEventHandler _taskStartedEventHandler = null; + + /// + /// Handle warning events + /// + private BuildWarningEventHandler _buildWarningEventHandler = null; + #endregion + + /// + /// Constructor. + /// + internal SubmissionRecord(int submissionId, IEventSource buildEventSource, BuildStartedEventArgs buildStartedEvent, int maxNodeCount) + { + if (buildEventSource == null) + { + throw new InvalidOperationException("eventSourceForBuild cannot be null"); + } + + _maxNodeCount = maxNodeCount; + _submissionId = submissionId; + _buildStartedEvent = buildStartedEvent; + _eventSourceForBuild = buildEventSource; + _loggers = new List(); + InitializeInternalEventSource(); + } + + #region Events + /// + /// This event is raised to log a message. + /// + public event BuildMessageEventHandler MessageRaised; + + /// + /// This event is raised to log an error. + /// + public event BuildErrorEventHandler ErrorRaised; + + /// + /// This event is raised to log a warning. + /// + public event BuildWarningEventHandler WarningRaised; + + /// + /// this event is raised to log the start of a build + /// + public event BuildStartedEventHandler BuildStarted; + + /// + /// this event is raised to log the end of a build + /// + public event BuildFinishedEventHandler BuildFinished; + + /// + /// this event is raised to log the start of a project build + /// + public event ProjectStartedEventHandler ProjectStarted; + + /// + /// this event is raised to log the end of a project build + /// + public event ProjectFinishedEventHandler ProjectFinished; + + /// + /// this event is raised to log the start of a target build + /// + public event TargetStartedEventHandler TargetStarted; + + /// + /// this event is raised to log the end of a target build + /// + public event TargetFinishedEventHandler TargetFinished; + + /// + /// this event is raised to log the start of task execution + /// + public event TaskStartedEventHandler TaskStarted; + + /// + /// this event is raised to log the end of task execution + /// + public event TaskFinishedEventHandler TaskFinished; + + /// + /// this event is raised to log a custom event + /// + public event CustomBuildEventHandler CustomEventRaised; + + /// + /// this event is raised to log build status events, such as + /// build/project/target/task started/stopped + /// + public event BuildStatusEventHandler StatusEventRaised; + + /// + /// This event is raised to log that some event has + /// occurred. It is raised on every event. + /// + public event AnyEventHandler AnyEventRaised; + + #endregion + + #region Internal Methods + /// + /// Adds the specified logger to the set of loggers for this submission. + /// + internal void AddLogger(ILogger logger) + { + lock (_syncLock) + { + if (_loggers.Contains(logger)) + { + throw new InvalidOperationException("Cannot register the same logger twice."); + } + + // Node loggers are central /l loggers which can understand how many CPU's the build is running with, they are only different in that + // they can take a number of CPU + INodeLogger nodeLogger = logger as INodeLogger; + if (null != nodeLogger) + { + nodeLogger.Initialize(this, _maxNodeCount); + } + else + { + logger.Initialize(this); + } + + _loggers.Add(logger); + } + } + + /// + /// Shuts down the loggers and removes them + /// + internal void Shutdown() + { + lock (_syncLock) + { + if (!_shutdown) + { + _shutdown = true; + _firstProjectStartedEventContext = null; + + UnregisterAllEventHandlers(); + + foreach (ILogger logger in _loggers) + { + logger.Shutdown(); + } + + _loggers.Clear(); + } + } + } + + /// + /// Sets the build started event for this event source if it hasn't already been set. + /// + internal void SetGlobalBuildStartedEvent(BuildStartedEventArgs buildStartedEvent) + { + lock (_syncLock) + { + if (_buildStartedEvent == null) + { + _buildStartedEvent = buildStartedEvent; + } + } + } + + /// + /// Raises a message event to all registered loggers. + /// + /// sender of the event + /// BuildMessageEventArgs + internal void RaiseMessageEvent(object sender, BuildMessageEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (MessageRaised != null) + { + try + { + MessageRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises an error event to all registered loggers. + /// + /// sender of the event + /// BuildErrorEventArgs + internal void RaiseErrorEvent(object sender, BuildErrorEventArgs buildEvent) + { + lock (_syncLock) + { + if ( + buildEvent.BuildEventContext != null && + ( + buildEvent.BuildEventContext.SubmissionId != _submissionId && /* The build submission does not match the submissionId for this logger */ + buildEvent.BuildEventContext.SubmissionId != BuildEventContext.InvalidSubmissionId /*We do not have a build submissionid this can happen if the error comes from the nodeloggingcontext*/ + ) + ) + { + return; + } + + if (ErrorRaised != null) + { + try + { + ErrorRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + } + } + } + } + + /// + /// Raises a warning event to all registered loggers. + /// + /// sender of the event + /// BuildWarningEventArgs + internal void RaiseWarningEvent(object sender, BuildWarningEventArgs buildEvent) + { + lock (_syncLock) + { + if ( + buildEvent.BuildEventContext != null && + ( + buildEvent.BuildEventContext.SubmissionId != _submissionId && /* The build submission does not match the submissionId for this logger */ + buildEvent.BuildEventContext.SubmissionId != BuildEventContext.InvalidSubmissionId /*We do not have a build submissionid this can happen if the error comes from the nodeloggingcontext*/ + ) + ) + { + return; + } + + if (WarningRaised != null) + { + try + { + WarningRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + } + } + } + } + + /// + /// Raises a "build started" event to all registered loggers. + /// + /// sender of the event + /// BuildStartedEventArgs + internal void RaiseBuildStartedEvent(object sender, BuildStartedEventArgs buildEvent) + { + lock (_syncLock) + { + // If we receive a REAL build started event, ignore it. We only want the one we get as a result of the project started event + if (_firstProjectStartedEventContext == null) + { + return; + } + + if (BuildStarted != null) + { + try + { + BuildStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + + RaiseStatusEvent(sender, buildEvent, true /* cascade to AnyEvent */); + } + } + + /// + /// Raises a "build finished" event to all registered loggers. + /// + /// sender of the event + /// BuildFinishedEventArgs + internal void RaiseBuildFinishedEvent(object sender, BuildFinishedEventArgs buildEvent) + { + lock (_syncLock) + { + // If we already did the build finished event (synthesized from project finished), we don't want to do it again if we happen + // to still be registered when the REAL build finished event comes through. + if (_firstProjectStartedEventContext == null) + { + return; + } + + if (BuildFinished != null) + { + try + { + BuildFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + + RaiseStatusEvent(sender, buildEvent, true /* cascade to AnyEvent */); + } + } + + /// + /// Raises a "project build started" event to all registered loggers. + /// + /// sender of the event + /// ProjectStartedEventArgs + internal void RaiseProjectStartedEvent(object sender, ProjectStartedEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (_firstProjectStartedEventContext == null) + { + // Capture the build event context for the first project started event so we can make sure we know when to fire the + // build finished event (in the case of loggers on the mux logger this is on the last project finished event for the submission + _firstProjectStartedEventContext = buildEvent.BuildEventContext; + + // We've never seen a project started event, so raise the build started event and save this project started event. + BuildStartedEventArgs startedEvent = new BuildStartedEventArgs(_buildStartedEvent.Message, _buildStartedEvent.HelpKeyword, _buildStartedEvent.BuildEnvironment); + RaiseBuildStartedEvent(sender, startedEvent); + } + + if (ProjectStarted != null) + { + try + { + ProjectStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a "project build finished" event to all registered loggers. + /// + /// sender of the event + /// ProjectFinishedEventArgs + internal void RaiseProjectFinishedEvent(object sender, ProjectFinishedEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (ProjectFinished != null) + { + try + { + ProjectFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a "target build started" event to all registered loggers. + /// + /// sender of the event + /// TargetStartedEventArgs + internal void RaiseTargetStartedEvent(object sender, TargetStartedEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (TargetStarted != null) + { + try + { + TargetStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a "target build finished" event to all registered loggers. + /// + /// sender of the event + /// TargetFinishedEventArgs + internal void RaiseTargetFinishedEvent(object sender, TargetFinishedEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (TargetFinished != null) + { + try + { + TargetFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a "task execution started" event to all registered loggers. + /// + /// sender of the event + /// TaskStartedEventArgs + internal void RaiseTaskStartedEvent(object sender, TaskStartedEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (TaskStarted != null) + { + try + { + TaskStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a "task finished executing" event to all registered loggers. + /// + /// sender of the event + /// TaskFinishedEventArgs + internal void RaiseTaskFinishedEvent(object sender, TaskFinishedEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (TaskFinished != null) + { + try + { + TaskFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a custom event to all registered loggers. + /// + /// sender of the event + /// CustomBuildEventArgs + internal void RaiseCustomEvent(object sender, CustomBuildEventArgs buildEvent) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (CustomEventRaised != null) + { + try + { + CustomEventRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + } + } + + /// + /// Raises a catch-all build status event to all registered loggers. + /// + /// sender of the event + /// BuildStatusEventArgs + internal void RaiseStatusEvent(object sender, BuildStatusEventArgs buildEvent) + { + RaiseStatusEvent(sender, buildEvent, false); + } + + /// + /// Raises a status event, optionally cascading to an any event. + /// + internal void RaiseStatusEvent(object sender, BuildStatusEventArgs buildEvent, bool cascade) + { + lock (_syncLock) + { + // If the event does not have the submissionId for our loggers then drop it. + if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId) + { + return; + } + + if (StatusEventRaised != null) + { + try + { + StatusEventRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + + if (cascade) + { + RaiseAnyEvent(sender, buildEvent); + } + } + } + + /// + /// Raises a catch-all build event to all registered loggers. + /// + /// sender of the event + /// Build EventArgs + internal void RaiseAnyEvent(object sender, BuildEventArgs buildEvent) + { + lock (_syncLock) + { + bool eventIsErrorOrWarning = (buildEvent is BuildWarningEventArgs) || (buildEvent is BuildErrorEventArgs); + + if ( + buildEvent.BuildEventContext != null && + ( + buildEvent.BuildEventContext.SubmissionId != _submissionId && /* The build submission does not match the submissionId for this logger */ + !( /* We do not have a build submissionid this can happen if the event comes from the nodeloggingcontext -- but we only want to raise it if it was an error or warning */ + buildEvent.BuildEventContext.SubmissionId == BuildEventContext.InvalidSubmissionId && eventIsErrorOrWarning + ) + ) + ) + { + return; + } + + // If we receive a REAL build started or finished event, ignore it. We only want the one we get as a result of the project started and finished events + if (_firstProjectStartedEventContext == null && (buildEvent is BuildStartedEventArgs || buildEvent is BuildFinishedEventArgs)) + { + return; + } + + if (AnyEventRaised != null) + { + try + { + AnyEventRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + throw; + } + } + + // If this project finished event matches our first project started event, then send build finished. + // Because of the way the event source works, we actually have to process this here rather than in project finished because if the + // logger is registered without a ProjectFinished handler, but does have an Any handler (as the mock logger does) then we would end up + // sending the BuildFinished event before the ProjectFinished event got processed in the Any handler. + ProjectFinishedEventArgs projectFinishedEvent = buildEvent as ProjectFinishedEventArgs; + if (projectFinishedEvent != null && buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.Equals(_firstProjectStartedEventContext)) + { + string message = projectFinishedEvent.Succeeded ? ResourceUtilities.GetResourceString("MuxLogger_BuildFinishedSuccess") : ResourceUtilities.GetResourceString("MuxLogger_BuildFinishedFailure"); + RaiseBuildFinishedEvent(sender, new BuildFinishedEventArgs(message, null, projectFinishedEvent.Succeeded)); + Shutdown(); + } + } + } + #endregion + + #region private methods + /// + /// Initialize the internal event source which is used to raise events on loggers registered to this submission + /// + private void InitializeInternalEventSource() + { + _anyEventHandler = new AnyEventHandler(RaiseAnyEvent); + _buildFinishedEventHandler = new BuildFinishedEventHandler(RaiseBuildFinishedEvent); + _buildStartedEventHandler = new BuildStartedEventHandler(RaiseBuildStartedEvent); + _customBuildEventHandler = new CustomBuildEventHandler(RaiseCustomEvent); + _buildErrorEventHandler = new BuildErrorEventHandler(RaiseErrorEvent); + _buildMessageEventHandler = new BuildMessageEventHandler(RaiseMessageEvent); + _projectFinishedEventHandler = new ProjectFinishedEventHandler(RaiseProjectFinishedEvent); + _projectStartedEventHandler = new ProjectStartedEventHandler(RaiseProjectStartedEvent); + _buildStatusEventHandler = new BuildStatusEventHandler(RaiseStatusEvent); + _targetFinishedEventHandler = new TargetFinishedEventHandler(RaiseTargetFinishedEvent); + _targetStartedEventHandler = new TargetStartedEventHandler(RaiseTargetStartedEvent); + _taskFinishedEventHandler = new TaskFinishedEventHandler(RaiseTaskFinishedEvent); + _taskStartedEventHandler = new TaskStartedEventHandler(RaiseTaskStartedEvent); + _buildWarningEventHandler = new BuildWarningEventHandler(RaiseWarningEvent); + + _eventSourceForBuild.AnyEventRaised += _anyEventHandler; + _eventSourceForBuild.BuildFinished += _buildFinishedEventHandler; + _eventSourceForBuild.BuildStarted += _buildStartedEventHandler; + _eventSourceForBuild.CustomEventRaised += _customBuildEventHandler; + _eventSourceForBuild.ErrorRaised += _buildErrorEventHandler; + _eventSourceForBuild.MessageRaised += _buildMessageEventHandler; + _eventSourceForBuild.ProjectFinished += _projectFinishedEventHandler; + _eventSourceForBuild.ProjectStarted += _projectStartedEventHandler; + _eventSourceForBuild.StatusEventRaised += _buildStatusEventHandler; + _eventSourceForBuild.TargetFinished += _targetFinishedEventHandler; + _eventSourceForBuild.TargetStarted += _targetStartedEventHandler; + _eventSourceForBuild.TaskFinished += _taskFinishedEventHandler; + _eventSourceForBuild.TaskStarted += _taskStartedEventHandler; + _eventSourceForBuild.WarningRaised += _buildWarningEventHandler; + } + + /// + /// Clears out all events. + /// + private void UnregisterAllEventHandlers() + { + _eventSourceForBuild.AnyEventRaised -= _anyEventHandler; + _eventSourceForBuild.BuildFinished -= _buildFinishedEventHandler; + _eventSourceForBuild.BuildStarted -= _buildStartedEventHandler; + _eventSourceForBuild.CustomEventRaised -= _customBuildEventHandler; + _eventSourceForBuild.ErrorRaised -= _buildErrorEventHandler; + _eventSourceForBuild.MessageRaised -= _buildMessageEventHandler; + _eventSourceForBuild.ProjectFinished -= _projectFinishedEventHandler; + _eventSourceForBuild.ProjectStarted -= _projectStartedEventHandler; + _eventSourceForBuild.StatusEventRaised -= _buildStatusEventHandler; + _eventSourceForBuild.TargetFinished -= _targetFinishedEventHandler; + _eventSourceForBuild.TargetStarted -= _targetStartedEventHandler; + _eventSourceForBuild.TaskFinished -= _taskFinishedEventHandler; + _eventSourceForBuild.TaskStarted -= _taskStartedEventHandler; + _eventSourceForBuild.WarningRaised -= _buildWarningEventHandler; + + MessageRaised = null; + ErrorRaised = null; + WarningRaised = null; + BuildStarted = null; + BuildFinished = null; + ProjectStarted = null; + ProjectFinished = null; + TargetStarted = null; + TargetFinished = null; + TaskStarted = null; + TaskFinished = null; + CustomEventRaised = null; + StatusEventRaised = null; + AnyEventRaised = null; + } + #endregion + } + } +} diff --git a/src/Utilities/PlatformManifest.cs b/src/Utilities/PlatformManifest.cs new file mode 100644 index 00000000000..c867a6880a7 --- /dev/null +++ b/src/Utilities/PlatformManifest.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Loads and stores the contents of Platform.xml +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using System.Collections.Generic; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// Structure to represent the information contained in Platform.xml + /// + internal class PlatformManifest + { + /// + /// Location of Platform.xml + /// + private string _pathToManifest; + + /// + /// Constructor + /// Takes the location of Platform.xml and populates the structure with manifest data + /// + public PlatformManifest(string pathToManifest) + { + ErrorUtilities.VerifyThrowArgumentLength(pathToManifest, "pathToManifest"); + _pathToManifest = pathToManifest; + LoadManifestFile(); + } + + /// + /// Platform name + /// + public string Name + { + get; + private set; + } + + /// + /// Platform friendly name + /// + public string FriendlyName + { + get; + private set; + } + + /// + /// Platform version + /// + public string PlatformVersion + { + get; + private set; + } + + /// + /// The platforms that this platform depends on. + /// Item1: Platform name + /// Item2: Platform version + /// + public ICollection DependentPlatforms + { + get; + private set; + } + + /// + /// The contracts contained by this platform + /// Item1: Contract name + /// Item2: Contract version + /// + public ICollection ApiContracts + { + get; + private set; + } + + /// + /// Flag set to true if an exception occurred while reading the manifest + /// + public bool ReadError + { + get + { + return !String.IsNullOrEmpty(ReadErrorMessage); + } + } + + /// + /// Message from exception thrown while reading manifest + /// + public string ReadErrorMessage + { + get; + private set; + } + + /// + /// Load content of Platform.xml + /// + private void LoadManifestFile() + { + /* + Platform.xml format: + + + + + + + + */ + try + { + string platformManifestPath = Path.Combine(_pathToManifest, "Platform.xml"); + + if (File.Exists(platformManifestPath)) + { + XmlDocument doc = new XmlDocument(); + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader xmlReader = XmlReader.Create(platformManifestPath, readerSettings)) + { + doc.Load(xmlReader); + } + + XmlElement rootElement = null; + + foreach (XmlNode childNode in doc.ChildNodes) + { + if (childNode.NodeType == XmlNodeType.Element && + String.Equals(childNode.Name, Elements.ApplicationPlatform, StringComparison.Ordinal)) + { + rootElement = (XmlElement)childNode; + break; + } + } + + if (rootElement != null) + { + Name = rootElement.GetAttribute(Attributes.Name); + FriendlyName = rootElement.GetAttribute(Attributes.FriendlyName); + PlatformVersion = rootElement.GetAttribute(Attributes.Version); + } + + DependentPlatforms = new List(); + ApiContracts = new List(); + + foreach (XmlNode childNode in rootElement.ChildNodes) + { + XmlElement childElement = childNode as XmlElement; + if (childElement == null) + { + continue; + } + + if (ApiContract.IsContainedApiContractsElement(childElement.Name)) + { + ApiContract.ReadContractsElement(childElement, ApiContracts); + } + else if (String.Equals(childElement.Name, Elements.DependentPlatform, StringComparison.Ordinal)) + { + DependentPlatforms.Add(new DependentPlatform(childElement.GetAttribute(Attributes.Name), childElement.GetAttribute(Attributes.Version))); + } + } + } + else + { + this.ReadErrorMessage = ResourceUtilities.FormatResourceString("PlatformManifest.MissingPlatformXml", platformManifestPath); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + this.ReadErrorMessage = e.Message; + } + } + + /// + /// Represents a dependency on another platform + /// + internal struct DependentPlatform + { + /// + /// Name of the platform on which this platform depends + /// + internal string Name; + + /// + /// Version of the platform on which this platform depends + /// + internal string Version; + + /// + /// Constructor + /// + internal DependentPlatform(string name, string version) + { + Name = name; + Version = version; + } + } + + /// + /// Helper class with element names in Platform.xml + /// + private static class Elements + { + /// + /// Root element + /// + public const string ApplicationPlatform = "ApplicationPlatform"; + + /// + /// Element describing a platform this platform is dependent on + /// + public const string DependentPlatform = "DependentPlatform"; + } + + /// + /// Helper class with attribute names in Platform.xml + /// + private static class Attributes + { + /// + /// Name associated with this element + /// + public const string Name = "name"; + + /// + /// Friendly name associated with this element + /// + public const string FriendlyName = "friendlyName"; + + /// + /// Version associated with this element + /// + public const string Version = "version"; + + /// + /// Architecture associated with this element + /// + public const string Architecture = "architecture"; + + /// + /// Config associated with this element + /// + public const string Configuration = "configuration"; + } + } +} \ No newline at end of file diff --git a/src/Utilities/ProcessorArchitecture.cs b/src/Utilities/ProcessorArchitecture.cs new file mode 100644 index 00000000000..a1c580c2176 --- /dev/null +++ b/src/Utilities/ProcessorArchitecture.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// Processor architecture utilities + /// + static public class ProcessorArchitecture + { + // Known processor architectures + public const string X86 = "x86"; + public const string IA64 = "IA64"; + + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "AMD", Justification = "This is the correct casing for ProcessorArchitecture")] + public const string AMD64 = "AMD64"; + + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "MSIL", Justification = "This is the correct casing for ProcessorArchitecture")] + public const string MSIL = "MSIL"; + + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ARM", Justification = "This is the correct casing for ProcessorArchitecture")] + public const string ARM = "ARM"; + + static private string s_currentProcessArchitecture = null; + static private bool s_currentProcessArchitectureInitialized = false; + + /// + /// Lazy-initted property for getting the architecture of the currently running process + /// + static public string CurrentProcessArchitecture + { + get + { + if (s_currentProcessArchitectureInitialized) + { + return s_currentProcessArchitecture; + } + + s_currentProcessArchitectureInitialized = true; + s_currentProcessArchitecture = ProcessorArchitecture.GetCurrentProcessArchitecture(); + + return s_currentProcessArchitecture; + } + } + + // PInvoke delegate for IsWow64Process + private delegate bool IsWow64ProcessDelegate([In] IntPtr hProcess, [Out] out bool Wow64Process); + + /// + /// Gets the processor architecture of the currently running process + /// + /// null if unknown architecture or error, one of the known architectures otherwise + static private string GetCurrentProcessArchitecture() + { + string architecture = null; + + IntPtr kernel32Dll = NativeMethodsShared.LoadLibrary("kernel32.dll"); + try + { + if (kernel32Dll != NativeMethodsShared.NullIntPtr) + { + // This method gets the current architecture from the currently running msbuild. + // If the entry point is missing, we're running on Kernel older than WinXP + // http://msdn.microsoft.com/en-us/library/ms684139.aspx + IntPtr isWow64ProcessHandle = NativeMethodsShared.GetProcAddress(kernel32Dll, "IsWow64Process"); + + if (isWow64ProcessHandle == NativeMethodsShared.NullIntPtr) + { + architecture = ProcessorArchitecture.X86; + } + else + { + // entry point present, check if running in WOW + IsWow64ProcessDelegate isWow64Process = (IsWow64ProcessDelegate)Marshal.GetDelegateForFunctionPointer(isWow64ProcessHandle, typeof(IsWow64ProcessDelegate)); + bool isWow64 = false; + bool success = isWow64Process(Process.GetCurrentProcess().Handle, out isWow64); + + if (success) + { + // if it's running on WOW, must be an x86 process + if (isWow64) + { + architecture = ProcessorArchitecture.X86; + } + else + { + // it's a native process. Check the system architecture to determine the process architecture. + NativeMethodsShared.SYSTEM_INFO systemInfo = new NativeMethodsShared.SYSTEM_INFO(); + + NativeMethodsShared.GetSystemInfo(ref systemInfo); + + switch (systemInfo.wProcessorArchitecture) + { + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_INTEL: + architecture = ProcessorArchitecture.X86; + break; + + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_AMD64: + architecture = ProcessorArchitecture.AMD64; + break; + + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_IA64: + architecture = ProcessorArchitecture.IA64; + break; + + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_ARM: + architecture = ProcessorArchitecture.ARM; + break; + + // unknown architecture? return null + default: + architecture = null; + break; + } + } + } + } + } + } + finally + { + if (kernel32Dll != NativeMethodsShared.NullIntPtr) + { + NativeMethodsShared.FreeLibrary(kernel32Dll); + } + } + + return architecture; + } + } +} diff --git a/src/Utilities/SDKManifest.cs b/src/Utilities/SDKManifest.cs new file mode 100644 index 00000000000..2d7708139d2 --- /dev/null +++ b/src/Utilities/SDKManifest.cs @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Loads and stores contents SDKManifest.xml - The sdkmanifest parser has been factored out from ResolveSDKReference.cs +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Win32; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Build.Utilities +{ + /// + /// What should happen if multiple versions of a given productfamily or sdk name are found + /// + public enum MultipleVersionSupport + { + /// + /// No action should be taken if multiple versions are detected + /// + Allow = 0, + + /// + /// Log warning + /// + Warning = 1, + + /// + /// Log an error + /// + Error = 2 + } + + /// + /// Structure to represent the information contained in SDKManifest.xml + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Not worth breaking partners")] + public class SDKManifest + { + /// + /// Pattern in path to extension SDK used to help determine if manifest is from a framework SDK + /// + static private string s_extensionSDKPathPattern = @"\MICROSOFT SDKS\WINDOWS\V8.0\EXTENSIONSDKS"; + + /// + /// Default version of MaxPlatformVersion in framework extension SDKs with manifest not containing such a property + /// + static private string s_defaultMaxPlatformVersion = "8.0"; + + /// + /// Default version of MinOSVersion in framework extension SDKs with manifest not containing such a property + /// + static private string s_defaultMinOSVersion = "6.2.1"; + + /// + /// Default version of MaxOSVersionTested in framework extension SDKs with manifest not containing such a property + /// + static private string s_defaultMaxOSVersionTested = "6.2.1"; + + /// + /// What should happen if this sdk is resolved with other sdks of the same productfamily or same sdk name. + /// + private MultipleVersionSupport _supportsMultipleVersions; + + /// + /// Path to where the file SDKManifest.xml is stored + /// + private string _pathToSdk; + + /// + /// Whatever appx locations we found in the manifest + /// + private IDictionary _appxLocations; + + /// + /// Whatever framework identities we found in the manifest. + /// + private IDictionary _frameworkIdentities; + + /// + /// Whatever MaxOSVersionTested we found in the manifest. + /// + private string _maxOSVersionTested; + + /// + /// Whatever MinOSVersion we found in the manifest + /// + private string _minOSVersion; + + /// + /// Whatever MaxPlatformVersion we found in the manifest + /// + private string _maxPlatformVersion; + + /// + /// The SDKType, default of unspecified + /// + private SDKType _sdkType = SDKType.Unspecified; + + /// + /// Constructor + /// Takes the path to SDKManifest.xml and populates the structure with manifest data + /// + public SDKManifest(string pathToSdk) + { + ErrorUtilities.VerifyThrowArgumentLength(pathToSdk, "pathToSdk"); + _pathToSdk = pathToSdk; + LoadManifestFile(); + } + + /// + /// Whatever information regarding support for multiple versions is found in the manifest + /// + public MultipleVersionSupport SupportsMultipleVersions + { + get + { + return _supportsMultipleVersions; + } + } + + /// + /// Whatever framework identities we found in the manifest. + /// + public IDictionary FrameworkIdentities + { + get + { + if (_frameworkIdentities != null) + { + return new ReadOnlyDictionary(_frameworkIdentities); + } + + return null; + } + } + + /// + /// Whatever appx locations we found in the manifest + /// + public IDictionary AppxLocations + { + get + { + if (_appxLocations != null) + { + return new ReadOnlyDictionary(_appxLocations); + } + + return null; + } + } + + /// + /// PlatformIdentity if it exists in the appx manifest for this sdk. + /// + public string PlatformIdentity + { + get; + private set; + } + + /// + /// The FrameworkIdentity for the sdk, this may be a single name or a | delimited name + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Necessary for compatibility with specs of SDKManifest.xml")] + public string FrameworkIdentity + { + get; + private set; + } + + /// + /// Support Prefer32bit found in the sdk manifest + /// + public string SupportPrefer32Bit + { + get; + private set; + } + + /// + /// SDKType found in the sdk manifest + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Want to keep same case as the attribute in sdkmanifest.xml")] + public SDKType SDKType + { + get + { + return _sdkType; + } + } + + /// + /// CopyRedistToSubDirectory specifies where the redist files should be copied to relative to the root of the package. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubDirectory", Justification = "Want to keep case compliant with the attributes in the sdkmanifest.xml")] + public string CopyRedistToSubDirectory + { + get; + private set; + } + + /// + /// Supported Architectures is a semicolon delimited list of architectures that the SDK supports. + /// + public string SupportedArchitectures + { + get; + private set; + } + + /// + /// DependsOnSDK is a semicolon delimited list of SDK identities that the SDK requires be resolved in order to function. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Not worth breaking consumers")] + public string DependsOnSDK + { + get; + private set; + } + + /// + /// ProductFamilyName specifies the product family for the SDK. This is offered up as metadata on the resolved sdkreference and is used to detect sdk conflicts. + /// + public string ProductFamilyName + { + get; + private set; + } + + /// + /// The platform the SDK targets. + /// + public string TargetPlatform + { + get; + private set; + } + + /// + /// Minimum version of the platform the SDK supports. + /// + public string TargetPlatformMinVersion + { + get; + private set; + } + + /// + /// Maximum version of the platform that the SDK supports. + /// + public string TargetPlatformVersion + { + get; + private set; + } + + /// + /// DisplayName found in the sdk manifest + /// + public string DisplayName + { + get; + private set; + } + + /// + /// MinVSVersion found in the sdk manifest + /// + public string MinVSVersion + { + get; + private set; + } + + /// + /// MinOSVersion found in the sdk manifest, defaults to 6.2.1 for framework extension SDKs when manifest does not have this property set + /// + public string MinOSVersion + { + get + { + if (_minOSVersion == null && IsFrameworkExtensionSdkManifest) + { + return s_defaultMinOSVersion; + } + else + { + return _minOSVersion; + } + } + } + + /// + /// MaxPlatformVersion found in the sdk manifest, defaults to 8.0 for framework extension SDKs when manifest does not have this property set + /// + public string MaxPlatformVersion + { + get + { + if (_maxPlatformVersion == null && IsFrameworkExtensionSdkManifest) + { + return s_defaultMaxPlatformVersion; + } + else + { + return _maxPlatformVersion; + } + } + } + + /// + /// MaxOSVersionTested found in the sdk manifest, defaults to 6.2.1 for framework extension SDKs when manifest does not have this property set + /// + public string MaxOSVersionTested + { + get + { + if (_maxOSVersionTested == null && IsFrameworkExtensionSdkManifest) + { + return s_defaultMaxOSVersionTested; + } + else + { + return _maxOSVersionTested; + } + } + } + + /// + /// MoreInfo as found in the sdk manifest + /// + public string MoreInfo + { + get; + private set; + } + + /// + /// Flag set to true if an exception occurred while reading the manifest + /// + public bool ReadError + { + get; + private set; + } + + /// + /// Message from exception thrown while reading manifest + /// + public string ReadErrorMessage + { + get; + private set; + } + + /// + /// The contracts contained by this manifest, if any + /// Item1: Contract name + /// Item2: Contract version + /// + internal ICollection ApiContracts + { + get; + private set; + } + + /// + /// Decide on whether it is a framework extension sdk based on manifest's FrameworkIdentify and path + /// + private bool IsFrameworkExtensionSdkManifest + { + get + { + if (_frameworkIdentities != null && _frameworkIdentities.Count > 0 && _pathToSdk != null && _pathToSdk.ToUpperInvariant().Contains(s_extensionSDKPathPattern)) + { + return true; + } + else + { + return false; + } + } + } + + /// + /// Load content of SDKManifest.xml + /// + private void LoadManifestFile() + { + /* + Extension SDK Manifest: + + + + + + + + + + + + + + + Platform SDK Manifest: + + + + + + + */ + string sdkManifestPath = Path.Combine(_pathToSdk, "SDKManifest.xml"); + + try + { + if (File.Exists(sdkManifestPath)) + { + XmlDocument doc = new XmlDocument(); + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader xmlReader = XmlReader.Create(sdkManifestPath, readerSettings)) + { + doc.Load(xmlReader); + } + + XmlElement rootElement = null; + + foreach (XmlNode childNode in doc.ChildNodes) + { + if (childNode.NodeType == XmlNodeType.Element && + String.Equals(childNode.Name, Elements.FileList, StringComparison.Ordinal)) + { + rootElement = (XmlElement)childNode; + break; + } + } + + if (rootElement != null) + { + ReadFileListAttributes(rootElement.Attributes); + foreach (XmlNode childNode in rootElement.ChildNodes) + { + XmlElement childElement = childNode as XmlElement; + if (childElement == null) + { + continue; + } + + if (ApiContract.IsContainedApiContractsElement(childElement.Name)) + { + ApiContracts = new List(); + ApiContract.ReadContractsElement(childElement, ApiContracts); + } + } + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + this.ReadError = true; + this.ReadErrorMessage = e.Message; + } + } + + /// + /// Reads the attributes from the "FileList" element of the SDK manifest. + /// + private void ReadFileListAttributes(XmlAttributeCollection attributes) + { + foreach (XmlAttribute attribute in attributes.OfType()) + { + string value = attribute.Value.Trim(); + if (value.Length > 0) + { + if (attribute.Name.StartsWith(SDKManifest.Attributes.FrameworkIdentity, StringComparison.OrdinalIgnoreCase)) + { + if (_frameworkIdentities == null) + { + _frameworkIdentities = new Dictionary(); + } + + _frameworkIdentities.Add(attribute.Name, value); + continue; + } + + if (attribute.Name.StartsWith(SDKManifest.Attributes.APPX, StringComparison.OrdinalIgnoreCase)) + { + if (_appxLocations == null) + { + _appxLocations = new Dictionary(); + } + + _appxLocations.Add(attribute.Name, value); + continue; + } + + switch (attribute.Name) + { + case SDKManifest.Attributes.TargetPlatform: + TargetPlatform = value; + break; + case SDKManifest.Attributes.TargetPlatformMinVersion: + TargetPlatformMinVersion = value; + break; + case SDKManifest.Attributes.TargetPlatformVersion: + TargetPlatformVersion = value; + break; + case SDKManifest.Attributes.MinVSVersion: + MinVSVersion = value; + break; + case SDKManifest.Attributes.MinOSVersion: + _minOSVersion = value; + break; + case SDKManifest.Attributes.MaxOSVersionTested: + _maxOSVersionTested = value; + break; + case SDKManifest.Attributes.MaxPlatformVersion: + _maxPlatformVersion = value; + break; + case SDKManifest.Attributes.PlatformIdentity: + PlatformIdentity = value; + break; + case SDKManifest.Attributes.SupportPrefer32Bit: + SupportPrefer32Bit = value; + break; + case SDKManifest.Attributes.SupportsMultipleVersions: + if (!ParseSupportMultipleVersions(value)) + { + _supportsMultipleVersions = MultipleVersionSupport.Allow; + } + + break; + case SDKManifest.Attributes.SDKType: + Enum.TryParse(value, out _sdkType); + break; + case SDKManifest.Attributes.DisplayName: + DisplayName = value; + break; + case SDKManifest.Attributes.MoreInfo: + MoreInfo = value; + break; + case SDKManifest.Attributes.CopyRedistToSubDirectory: + CopyRedistToSubDirectory = value; + break; + case SDKManifest.Attributes.SupportedArchitectures: + SupportedArchitectures = value; + break; + case SDKManifest.Attributes.DependsOnSDK: + DependsOnSDK = value; + break; + case SDKManifest.Attributes.ProductFamilyName: + ProductFamilyName = value; + break; + } + } + } + } + + /// + /// Parse the multipleversions string and set supportsMultipleVersions if it can be parsed correctly. + /// + private bool ParseSupportMultipleVersions(string multipleVersionsValue) + { + return !String.IsNullOrEmpty(multipleVersionsValue) && Enum.TryParse(multipleVersionsValue, /*ignoreCase*/true, out _supportsMultipleVersions); + } + + /// + /// Helper class with attributes of SDKManifest.xml + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.Build.Utilities.SDKManifest+Attributes", Justification = "Not worth breaking customers / spending resources to fix.")] + public static class Attributes + { + /// + /// Platform that the SDK targets + /// + public const string TargetPlatform = "TargetPlatform"; + + /// + /// The minimum version of the platform that the SDK targets + /// + public const string TargetPlatformMinVersion = "TargetPlatformMinVersion"; + + /// + /// The max version of the platform that the SDK targets + /// + public const string TargetPlatformVersion = "TargetPlatformVersion"; + + /// + /// Framework Identity metadata name and manifest attribute + /// + public const string FrameworkIdentity = "FrameworkIdentity"; + + /// + /// Supported Architectures metadata name and manifest attribute + /// + public const string SupportedArchitectures = "SupportedArchitectures"; + + /// + /// Prefer32BitSupport metadata name and manifest attribute + /// + public const string SupportPrefer32Bit = "SupportPrefer32Bit"; + + /// + /// AppxLocation metadata + /// + public const string AppxLocation = "AppxLocation"; + + /// + /// APPX manifest attribute + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "APPX", Justification = "Want to keep same case as the attribute in sdkmanifest.xml")] + public const string APPX = "APPX"; + + /// + /// PlatformIdentity metadata name and manifest attribute + /// + public const string PlatformIdentity = "PlatformIdentity"; + + /// + /// SDKType metadata name and manifest attribute + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Want to keep same case as the attribute in sdkmanifest.xml")] + public const string SDKType = "SDKType"; + + /// + /// DisplayName metadata name and manifest attribute + /// + public const string DisplayName = "DisplayName"; + + /// + /// CopyRedistToSubDirectory metadata name and manifest attribute + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubDirectory", Justification = "Want to keep same case as in sdkmanifest.sdk")] + public const string CopyRedistToSubDirectory = "CopyRedistToSubDirectory"; + + /// + /// ProductFamilyName metadata name and manifest attribute + /// + public const string ProductFamilyName = "ProductFamilyName"; + + /// + /// SupportsMultipleVersions metadata name and manifest attribute + /// + public const string SupportsMultipleVersions = "SupportsMultipleVersions"; + + /// + /// TargetedSDKArchitecture metadata name + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Not worth breaking customers")] + public const string TargetedSDK = "TargetedSDKArchitecture"; + + /// + /// TargetedSDKConfiguration metadata name + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Not worth breaking customers")] + public const string TargetedSDKConfiguration = "TargetedSDKConfiguration"; + + /// + /// ExpandReferenceAssemblies metadata name + /// + public const string ExpandReferenceAssemblies = "ExpandReferenceAssemblies"; + + /// + /// DependsOn metadata name + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Not worth breaking customers")] + public const string DependsOnSDK = "DependsOn"; + + /// + /// CopyRedist metadata name + /// + public const string CopyRedist = "CopyRedist"; + + /// + /// CopyLocalExpandedReferenceAssemblies metadata name + /// + public const string CopyLocalExpandedReferenceAssemblies = "CopyLocalExpandedReferenceAssemblies"; + + // Dev 12 new attributes + + /// + /// MinOSVersion metadata name + /// + public const string MinOSVersion = "MinOSVersion"; + + /// + /// MinVSVersion metadata name + /// + public const string MinVSVersion = "MinVSVersion"; + + /// + /// MaxPlatformVersionAttribute metadata name + /// + public const string MaxPlatformVersion = "MaxPlatformVersion"; + + /// + /// MoreInfoAttribute metadata name + /// + public const string MoreInfo = "MoreInfo"; + + /// + /// MaxOSVersionTestedAttribute metadata name + /// + public const string MaxOSVersionTested = "MaxOSVersionTested"; + } + + /// + /// Helper class with elements of SDKManifest.xml + /// + internal static class Elements + { + /// + /// Root element + /// + public const string FileList = "FileList"; + } + } +} \ No newline at end of file diff --git a/src/Utilities/SDKType.cs b/src/Utilities/SDKType.cs new file mode 100644 index 00000000000..b9e776d2f23 --- /dev/null +++ b/src/Utilities/SDKType.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Internal representation of the extension SDK's type +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Utilities +{ + /// + /// Type of SDK + /// + public enum SDKType + { + /// + /// Not specified + /// + Unspecified, + + /// + /// Traditional 3rd party SDK + /// + External, + + /// + /// Platform extension SDK + /// + Platform, + + /// + /// Framework extension SDK + /// + Framework + } +} diff --git a/src/Utilities/Strings.resx b/src/Utilities/Strings.resx new file mode 100644 index 00000000000..cf479a291af --- /dev/null +++ b/src/Utilities/Strings.resx @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + MSB6001: Invalid command line switch for "{0}". {1} + {StrBegin="MSB6001: "}UE: This message is shown when a tool-based task (i.e. the task is a wrapper for an .exe) is given a parameter value that converts into an invalid command line switch for the tool. "{0}" is the name of the tool e.g. "csc.exe", and "{1}" is a message explaining the problem. LOCALIZATION: "{1}" is a localized message. + + + Illegal quote passed to the command line switch named "{0}". The value was [{1}]. + + + Illegal quote in the command line value [{0}]. + + + The value [{0}] contains an odd number of double-quote characters. Only even numbers of literal double-quote characters are acceptable to command line tools. + + + The command exited with code {0}. + + + MSB6005: Task attempted to log before it was initialized. Message was: {0} + {StrBegin="MSB6005: "}UE: This occurs if the task attempts to log something in its own constructor. + + + MSB6010: Could not find platform manifest file at "{0}". + {StrBegin="MSB6010: "} + + + .NET Framework version "{0}" is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion. + + + .NET Framework version "{0}" is not supported when explicitly targeting the Windows SDK, which is only supported on .NET 4.5 and later. Please specify a value from the enumeration Microsoft.Build.Utilities.TargetDotNetFrameworkVersion that is Version45 or above. + + + Visual Studio version "{0}" is not supported. Please specify a value from the enumeration Microsoft.Build.Utilities.VisualStudioVersion. + + + MSB6002: The command-line for the "{0}" task is too long. Command-lines longer than 32000 characters are likely to fail. Try reducing the length of the command-line by breaking down the call to "{0}" into multiple calls with fewer parameters per call. + {StrBegin="MSB6002: "} + + + MSB6003: The specified task executable "{0}" could not be run. {1} + {StrBegin="MSB6003: "} + + + MSB6006: "{0}" exited with code {1}. + {StrBegin="MSB6006: "} + + + MSB6004: The specified task executable location "{0}" is invalid. + {StrBegin="MSB6004: "} + + + There was an error reading the redist list file "{0}". {1} + No Error code because this resource will be used in an exception. The error code is discarded if it is included + + + The Framework at path "{0}" tried to include the framework at path "{1}" as part of its reference assembly paths but there was an error. {2} + No Error code because this resource will be used in an exception. The error code is discarded if it is included + + + When attempting to generate a reference assembly path from the path "{0}" and the framework moniker "{1}" there was an error. {2} + No Error code because this resource will be used in an exception. The error code is discarded if it is included + + + MSB6007: The "{0}" value passed to the Environment property is not in the format "name=value", where the value part may be empty. + {StrBegin="MSB6007: "} + + + Environment Variables passed to tool: + + + + Tracking logs are not available, minimal rebuild will be disabled. + + + Missing input files detected, minimal rebuild will be disabled. + + + Missing output files detected, minimal rebuild will be disabled. + + + Skipping task because it is up-to-date. + + + Write Tracking log not available, minimal rebuild will be disabled. + + + Write Tracking Logs: + + + Using cached output dependency table built from: + + + Tracking Logs: + + + Using cached dependency table built from: + + + Outputs for {0}: + + + Inputs for {0}: + + + Output details ({0} of them) were not logged for performance reasons. + + + Input details ({0} of them) were not logged for performance reasons. + + + Tracking log {0} is not available. + + + {0} will be compiled as the tracking log is not available. + + + {0} will be compiled as it was not found in the tracking log. + + + {0} will be compiled as not all outputs are available. + + + {0} will be compiled as dependency {1} was missing. + + + {0} will be compiled as {1} was modified at {2}. + + + {0} will be compiled. + + + All outputs are up-to-date. + + + File {0} was modified at {1} which is newer than {2} modified at {3}. + + + {0} does not exist; source compilation required. + + + {0} will be compiled as output {1} does not exist. + + + Read Tracking Logs: + + + Using cached input dependency table built from: + + + No output for {0} was found in the tracking log; source compilation required. + + + No dependencies for output {0} were found in the tracking log; source compilation required. + + + Could not find {0} in the write tracking log. + + + Could not find {0} in the read tracking log. + + + The number of source files and corresponding outputs must match. + + + Source compilation required: input {0} is newer than output {1}. + + + MSB6008: Forcing a rebuild of all sources due to an error with the tracking logs. {0} + {StrBegin="MSB6008: "} + + + MSB6009: Forcing a rebuild of all source files due to the contents of "{0}" being invalid. + {StrBegin="MSB6009: "} + + + Build FAILED. + + + Build succeeded. + + + + \ No newline at end of file diff --git a/src/Utilities/TargetPlatformSDK.cs b/src/Utilities/TargetPlatformSDK.cs new file mode 100644 index 00000000000..90012124036 --- /dev/null +++ b/src/Utilities/TargetPlatformSDK.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Internal representation of a target platform +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// Structure to represent a target platform sdk + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Dev11 Beta (go-live) is shipping this way")] + public class TargetPlatformSDK : IEquatable + { + /// + /// Path to the platform sdk may be null if not a platform sdk. + /// + private string _path; + + /// + /// Object containing the properties in the SDK manifest + /// + private SDKManifest _manifest; + + /// + /// Cache for min Visual Studio version from manifest + /// + private Version _minVSVersion; + + /// + /// Cache for min OS version from manifest + /// + private Version _minOSVersion; + + /// + /// Constructor + /// + public TargetPlatformSDK(string targetPlatformIdentifier, Version targetPlatformVersion, string path) + { + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + TargetPlatformIdentifier = targetPlatformIdentifier; + TargetPlatformVersion = targetPlatformVersion; + Path = path; + ExtensionSDKs = new Dictionary(StringComparer.OrdinalIgnoreCase); + Platforms = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Min Visual Studio version from manifest + /// + public Version MinVSVersion + { + get + { + if (_minVSVersion == null && Manifest != null && Manifest.MinVSVersion != null) + { + if (!Version.TryParse(Manifest.MinVSVersion, out _minVSVersion)) + { + _minVSVersion = null; + } + } + + return _minVSVersion; + } + } + + /// + /// Min OS version from manifest + /// + public Version MinOSVersion + { + get + { + if (_minOSVersion == null && Manifest != null && Manifest.MinOSVersion != null) + { + if (!Version.TryParse(Manifest.MinOSVersion, out _minOSVersion)) + { + _minOSVersion = null; + } + } + + return _minOSVersion; + } + } + + /// + /// Target platform identifier + /// + public string TargetPlatformIdentifier + { + get; + private set; + } + + /// + /// Target platform version + /// + public Version TargetPlatformVersion + { + get; + private set; + } + + /// + /// Path to target platform sdk if it exists, it may not if there is no target platform is installed + /// + public string Path + { + get + { + return _path; + } + + set + { + if (value != null) + { + _path = FileUtilities.EnsureTrailingSlash(value); + } + else + { + _path = null; + } + } + } + + /// + /// The SDK's display name, or null if one is not defined. + /// + public string DisplayName + { + get + { + return Manifest != null ? Manifest.DisplayName : null; + } + } + + /// + /// Extension sdks within this platform, + /// + internal Dictionary ExtensionSDKs + { + get; + private set; + } + + /// + /// Set of platforms supported by this SDK. + /// + internal Dictionary Platforms + { + get; + private set; + } + + /// + /// Reference to manifest object + /// Makes it is instantiated only once + /// + private SDKManifest Manifest + { + get + { + // Load manifest from disk the first time it is needed + if (_manifest == null && _path != null) + { + _manifest = new SDKManifest(_path); + } + + return _manifest; + } + } + + /// + /// Override GetHashCode + /// + public override int GetHashCode() + { + return TargetPlatformIdentifier.ToLower(CultureInfo.InvariantCulture).GetHashCode() ^ TargetPlatformVersion.GetHashCode(); + } + + /// + /// Override equals + /// + public override bool Equals(object obj) + { + TargetPlatformSDK moniker = obj as TargetPlatformSDK; + if (moniker == null) + { + return false; + } + + if (Object.ReferenceEquals(this, moniker)) + { + return true; + } + + return Equals(moniker); + } + + /// + /// Implement IEquatable + /// + public bool Equals(TargetPlatformSDK other) + { + if (other == null) + { + return false; + } + + return TargetPlatformIdentifier.Equals(other.TargetPlatformIdentifier, StringComparison.OrdinalIgnoreCase) && TargetPlatformVersion.Equals(other.TargetPlatformVersion); + } + + /// + /// Returns true if this SDK supports the given platform, or false otherwise. + /// + public bool ContainsPlatform(string targetPlatformIdentifier, string targetPlatformVersion) + { + string sdkKey = GetSdkKey(targetPlatformIdentifier, targetPlatformVersion); + return Platforms.ContainsKey(sdkKey); + } + + /// + /// Given an identifier and version, construct a string to use as a key for that combination. + /// + internal static string GetSdkKey(string sdkIdentifier, string sdkVersion) + { + return String.Format(CultureInfo.InvariantCulture, "{0}, Version={1}", sdkIdentifier, sdkVersion); + } + } +} \ No newline at end of file diff --git a/src/Utilities/Task.cs b/src/Utilities/Task.cs new file mode 100644 index 00000000000..14063979825 --- /dev/null +++ b/src/Utilities/Task.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Runtime.InteropServices; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Utilities +{ + /// + /// This helper base class provides default functionality for tasks. This class can only be instantiated in a derived form. + /// + public abstract class Task : ITask + { + #region Constructors + + /// + /// Default (family) constructor. + /// + protected Task() + { + _log = new TaskLoggingHelper(this); + } + + /// + /// This (family) constructor allows derived task classes to register their resources. + /// + /// The task resources. + protected Task(ResourceManager taskResources) + : this() + { + _log.TaskResources = taskResources; + } + + /// + /// This (family) constructor allows derived task classes to register their resources, as well as provide a prefix for + /// composing help keywords from string resource names. If the prefix is an empty string, then string resource names will + /// be used verbatim as help keywords. For an example of how the prefix is used, see the + /// TaskLoggingHelper.LogErrorWithCodeFromResources(string, object[]) method. + /// + /// The task resources. + /// The help keyword prefix. + protected Task(ResourceManager taskResources, string helpKeywordPrefix) + : this(taskResources) + { + _log.HelpKeywordPrefix = helpKeywordPrefix; + } + + #endregion + + #region Properties + + /// + /// The build engine automatically sets this property to allow tasks to call back into it. + /// + /// The build engine interface available to tasks. + public IBuildEngine BuildEngine + { + get + { + return _buildEngine; + } + + set + { + _buildEngine = value; + } + } + + // callback interface on the build engine + private IBuildEngine _buildEngine; + + /// + /// The build engine automatically sets this property to allow tasks to call back into it. + /// This is a convenience property so that task authors inheriting from this class do not + /// have to cast the value from IBuildEngine to IBuildEngine2. + /// + /// The build engine interface available to tasks. + public IBuildEngine2 BuildEngine2 + { + get + { + // This cast is always possible because this class is built against the + // Orcas Framework assembly, or later, so the version of MSBuild that does not + // know about IBuildEngine2 will never load it. + return (IBuildEngine2)_buildEngine; + } + // No setter needed: the Engine always sets through the BuildEngine setter + } + + /// + /// Retrieves the IBuildEngine3 version of the build engine interface provided by the host. + /// + public IBuildEngine3 BuildEngine3 + { + get + { + return (IBuildEngine3)_buildEngine; + } + } + + /// + /// Retrieves the IBuildEngine4 version of the build engine interface provided by the host. + /// + public IBuildEngine4 BuildEngine4 + { + get + { + return (IBuildEngine4)_buildEngine; + } + } + + /// + /// The build engine sets this property if the host IDE has associated a host object with this particular task. + /// + /// The host object instance (can be null). + public ITaskHost HostObject + { + get + { + return _hostObject; + } + + set + { + _hostObject = value; + } + } + + // Optional host object that might be used by certain IDE-aware tasks. + private ITaskHost _hostObject; + + /// + /// Gets an instance of a TaskLoggingHelper class containing task logging methods. + /// The taskLoggingHelper is a MarshallByRef object which needs to have MarkAsInactive called + /// if the parent task is making the appdomain and marshaling this object into it. If the appdomain is not unloaded at the end of + /// the task execution and the MarkAsInactive method is not called this will result in a leak of the task instances in the appdomain the task was created within. + /// + /// The logging helper object. + public TaskLoggingHelper Log + { + get + { + return _log; + } + } + + // the logging helper + private TaskLoggingHelper _log; + + /// + /// Gets or sets the task's culture-specific resources. Derived classes should register their resources either during + /// construction, or via this property, if they have localized strings. + /// + /// The task's resources (can be null). + protected ResourceManager TaskResources + { + get + { + return Log.TaskResources; + } + + set + { + Log.TaskResources = value; + } + } + + /// + /// Gets or sets the prefix used to compose help keywords from string resource names. If a task does not have help + /// keywords associated with its messages, it can ignore this property or set it to null. If the prefix is set to an empty + /// string, then string resource names will be used verbatim as help keywords. For an example of how this prefix is used, + /// see the TaskLoggingHelper.LogErrorWithCodeFromResources(string, object[]) method. + /// + /// The help keyword prefix string (can be null). + protected string HelpKeywordPrefix + { + get + { + return Log.HelpKeywordPrefix; + } + + set + { + Log.HelpKeywordPrefix = value; + } + } + + #endregion + + #region Methods + + /// + /// Must be implemented by derived class. + /// + /// true, if successful + public abstract bool Execute(); + + #endregion + } +} diff --git a/src/Utilities/TaskItem.cs b/src/Utilities/TaskItem.cs new file mode 100644 index 00000000000..a168d8e59c7 --- /dev/null +++ b/src/Utilities/TaskItem.cs @@ -0,0 +1,495 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Utilities +{ + /// + /// This class represents a single item of the project, as it is passed into a task. TaskItems do not exactly correspond to + /// item elements in project files, because then tasks would have access to data that wasn't explicitly passed into the task + /// via the project file. It's not a security issue, but more just an issue with project file clarity and transparency. + /// + /// Note: This class has to be sealed. It has to be sealed because the engine instantiates it's own copy of this type and + /// thus if someone were to extend it, they would not get the desired behavior from the engine. + /// + /// + /// Surprisingly few of these Utilities TaskItems are created: typically several orders of magnitude fewer than the number of engine TaskItems. + /// + public sealed class TaskItem : MarshalByRefObject, ITaskItem, ITaskItem2 + { + #region Member Data + + // This is the final evaluated item specification. Stored in escaped form. + private string _itemSpec; + + // These are the user-defined metadata on the item, specified in the + // project file via XML child elements of the item element. These have + // no meaning to MSBuild, but tasks may use them. + // Values are stored in escaped form. + private CopyOnWriteDictionary _metadata; + + // cache of the fullpath value + private string _fullPath; + + /// + /// May be defined if we're copying this item from a pre-existing one. Otherwise, + /// we simply don't know enough to set it properly, so it will stay null. + /// + private string _definingProject; + + #endregion + + #region Constructors + + /// + /// Default constructor -- we need it so this type is COM-createable. + /// + public TaskItem() + { + _itemSpec = String.Empty; + } + + /// + /// This constructor creates a new task item, given the item spec. + /// + /// Assumes the itemspec passed in is escaped. + /// The item-spec string. + public TaskItem + ( + string itemSpec + ) + { + ErrorUtilities.VerifyThrowArgumentNull(itemSpec, "itemSpec"); + + _itemSpec = itemSpec; + } + + /// + /// This constructor creates a new TaskItem, using the given item spec and metadata. + /// + /// + /// Assumes the itemspec passed in is escaped, and also that any escapable metadata values + /// are passed in escaped form. + /// + /// The item-spec string. + /// Custom metadata on the item. + public TaskItem + ( + string itemSpec, + IDictionary itemMetadata + ) : + this(itemSpec) + { + ErrorUtilities.VerifyThrowArgumentNull(itemMetadata, "itemMetadata"); + + if (itemMetadata.Count > 0) + { + _metadata = new CopyOnWriteDictionary(MSBuildNameIgnoreCaseComparer.Default); + + foreach (DictionaryEntry singleMetadata in itemMetadata) + { + // don't import metadata whose names clash with the names of reserved metadata + string key = (string)singleMetadata.Key; + if (!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(key)) + { + _metadata[key] = (string)singleMetadata.Value ?? String.Empty; + } + } + } + } + + /// + /// This constructor creates a new TaskItem, using the given ITaskItem. + /// + /// The item to copy. + public TaskItem + ( + ITaskItem sourceItem + ) + { + ErrorUtilities.VerifyThrowArgumentNull(sourceItem, "sourceItem"); + + ITaskItem2 sourceItemAsITaskItem2 = sourceItem as ITaskItem2; + + // Attempt to preserve escaped state + if (sourceItemAsITaskItem2 == null) + { + _itemSpec = EscapingUtilities.Escape(sourceItem.ItemSpec); + _definingProject = EscapingUtilities.EscapeWithCaching(sourceItem.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath)); + } + else + { + _itemSpec = sourceItemAsITaskItem2.EvaluatedIncludeEscaped; + _definingProject = sourceItemAsITaskItem2.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + } + + sourceItem.CopyMetadataTo(this); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the item-spec. + /// + /// + /// This one is a bit tricky. Orcas assumed that the value being set was escaped, but + /// that the value being returned was unescaped. Maintain that behaviour here. To get + /// the escaped value, use ITaskItem2.EvaluatedIncludeEscaped. + /// + /// The item-spec string. + public string ItemSpec + { + get + { + return (_itemSpec == null) ? String.Empty : EscapingUtilities.UnescapeAll(_itemSpec); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "ItemSpec"); + + _itemSpec = value; + _fullPath = null; + } + } + + /// + /// Gets or sets the escaped include, or "name", for the item. + /// + /// + /// Taking the opportunity to fix the property name, although this doesn't + /// make it obvious it's an improvement on ItemSpec. + /// + string ITaskItem2.EvaluatedIncludeEscaped + { + get + { + // It's already escaped + return _itemSpec; + } + + set + { + _itemSpec = value; + _fullPath = null; + } + } + + /// + /// Gets the names of all the item's metadata. + /// + /// List of metadata names. + public ICollection MetadataNames + { + get + { + List metadataNames = new List((_metadata == null) ? ReadOnlyEmptyList.Instance : _metadata.Keys); + metadataNames.AddRange(FileUtilities.ItemSpecModifiers.All); + + return metadataNames; + } + } + + /// + /// Gets the number of metadata set on the item. + /// + /// Count of metadata. + public int MetadataCount + { + get + { + int count = (_metadata == null) ? 0 : _metadata.Count; + return (count + FileUtilities.ItemSpecModifiers.All.Length); + } + } + + /// + /// Gets the metadata dictionary + /// Property is required so that we can access the metadata dictionary in an item from + /// another appdomain, as the CLR has implemented remoting policies that disallow accessing + /// private fields in remoted items. + /// + private CopyOnWriteDictionary Metadata + { + get + { + return _metadata; + } + set + { + _metadata = value; + } + } + + #endregion + + #region Methods + + /// + /// Removes one of the arbitrary metadata on the item. + /// + /// Name of metadata to remove. + public void RemoveMetadata + ( + string metadataName + ) + { + ErrorUtilities.VerifyThrowArgumentNull(metadataName, "metadataName"); + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName), + "Shared.CannotChangeItemSpecModifiers", metadataName); + + if (_metadata == null) + { + return; + } + + _metadata.Remove(metadataName); + } + + /// + /// Sets one of the arbitrary metadata on the item. + /// + /// + /// Assumes that the value being passed in is in its escaped form. + /// + /// Name of metadata to set or change. + /// Value of metadata. + public void SetMetadata + ( + string metadataName, + string metadataValue + ) + { + ErrorUtilities.VerifyThrowArgumentLength(metadataName, "metadataName"); + + // Non-derivable metadata can only be set at construction time. + // That's why this is IsItemSpecModifier and not IsDerivableItemSpecModifier. + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName), + "Shared.CannotChangeItemSpecModifiers", metadataName); + + _metadata = _metadata ?? new CopyOnWriteDictionary(MSBuildNameIgnoreCaseComparer.Default); + + _metadata[metadataName] = metadataValue ?? String.Empty; + } + + /// + /// Retrieves one of the arbitrary metadata on the item. + /// If not found, returns empty string. + /// + /// + /// Returns the unescaped value of the metadata requested. + /// + /// The name of the metadata to retrieve. + /// The metadata value. + public string GetMetadata + ( + string metadataName + ) + { + string metadataValue = (this as ITaskItem2).GetMetadataValueEscaped(metadataName); + return EscapingUtilities.UnescapeAll(metadataValue); + } + + /// + /// Copy the metadata (but not the ItemSpec) to destinationItem. If a particular metadata already exists on the + /// destination item, then it is not overwritten -- the original value wins. + /// + /// The item to copy metadata to. + public void CopyMetadataTo + ( + ITaskItem destinationItem + ) + { + ErrorUtilities.VerifyThrowArgumentNull(destinationItem, "destinationItem"); + + // also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata + // between items, and need to know the source item where the metadata came from + string originalItemSpec = destinationItem.GetMetadata("OriginalItemSpec"); + ITaskItem2 destinationAsITaskItem2 = destinationItem as ITaskItem2; + + if (_metadata != null) + { + TaskItem destinationAsTaskItem = destinationItem as TaskItem; + + // Avoid a copy if we can + if (destinationAsTaskItem != null && destinationAsTaskItem.Metadata == null) + { + destinationAsTaskItem.Metadata = _metadata.Clone(); // Copy on write! + } + else + { + foreach (KeyValuePair entry in _metadata) + { + string value; + + if (destinationAsITaskItem2 != null) + { + value = destinationAsITaskItem2.GetMetadataValueEscaped(entry.Key); + + if (String.IsNullOrEmpty(value)) + { + destinationAsITaskItem2.SetMetadata(entry.Key, entry.Value); + } + } + else + { + value = destinationItem.GetMetadata(entry.Key); + + if (String.IsNullOrEmpty(value)) + { + destinationItem.SetMetadata(entry.Key, EscapingUtilities.Escape(entry.Value)); + } + } + } + } + } + + if (String.IsNullOrEmpty(originalItemSpec)) + { + if (destinationAsITaskItem2 != null) + { + destinationAsITaskItem2.SetMetadata("OriginalItemSpec", ((ITaskItem2)this).EvaluatedIncludeEscaped); + } + else + { + destinationItem.SetMetadata("OriginalItemSpec", EscapingUtilities.Escape(ItemSpec)); + } + } + } + + /// + /// Get the collection of custom metadata. This does not include built-in metadata. + /// + /// + /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS: + /// 1) this method should return a clone of the metadata + /// 2) writing to this dictionary should not be reflected in the underlying item. + /// + /// + /// Returns an UNESCAPED version of the custom metadata. For the escaped version (which + /// is how it is stored internally), call ITaskItem2.CloneCustomMetadataEscaped. + /// + public IDictionary CloneCustomMetadata() + { + CopyOnWriteDictionary dictionary = new CopyOnWriteDictionary(MSBuildNameIgnoreCaseComparer.Default); + + if (_metadata != null) + { + foreach (KeyValuePair entry in _metadata) + { + dictionary.Add(entry.Key, EscapingUtilities.UnescapeAll(entry.Value)); + } + } + + return dictionary; + } + + /// + /// Gets the item-spec. + /// + /// The item-spec string. + public override string ToString() + { + return _itemSpec; + } + + /// + /// Overriden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + [SecurityCritical] + public override object InitializeLifetimeService() + { + // null means infinite lease time + return null; + } + + #endregion + + #region Operators + + /// + /// This allows an explicit typecast from a "TaskItem" to a "string", returning the escaped ItemSpec for this item. + /// + /// The item to operate on. + /// The item-spec of the item. + public static explicit operator string + ( + TaskItem taskItemToCast + ) + { + ErrorUtilities.VerifyThrowArgumentNull(taskItemToCast, "taskItemToCast"); + + return taskItemToCast.ItemSpec; + } + + #endregion + + #region ITaskItem2 implementation + + /// + /// Returns the escaped value of the metadata with the specified key. + /// + string ITaskItem2.GetMetadataValueEscaped(string metadataName) + { + ErrorUtilities.VerifyThrowArgumentNull(metadataName, "metadataName"); + + string metadataValue = null; + + if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName)) + { + // FileUtilities.GetItemSpecModifier is expecting escaped data, which we assume we already are. + // Passing in a null for currentDirectory indicates we are already in the correct current directory + metadataValue = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(null, _itemSpec, _definingProject, metadataName, ref _fullPath); + } + else if (_metadata != null) + { + _metadata.TryGetValue(metadataName, out metadataValue); + } + + return (metadataValue == null) ? String.Empty : metadataValue; + } + + /// + /// Sets the escaped value of the metadata with the specified name. + /// + /// + /// Assumes the value is passed in unescaped. + /// + void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) + { + SetMetadata(metadataName, EscapingUtilities.Escape(metadataValue)); + } + + /// + /// ITaskItem2 implementation which returns a clone of the metadata on this object. + /// Values returned are in their original escaped form. + /// + /// The cloned metadata. + IDictionary ITaskItem2.CloneCustomMetadataEscaped() + { + if (_metadata == null) + { + return new CopyOnWriteDictionary(MSBuildNameIgnoreCaseComparer.Default); + } + + return (IDictionary)_metadata.Clone(); + } + + #endregion + } +} diff --git a/src/Utilities/ToolLocationHelper.cs b/src/Utilities/ToolLocationHelper.cs new file mode 100644 index 00000000000..3d9aedd3431 --- /dev/null +++ b/src/Utilities/ToolLocationHelper.cs @@ -0,0 +1,3614 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Build.Shared; +using System.Xml; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using UtilitiesDotNetFrameworkArchitecture = Microsoft.Build.Utilities.DotNetFrameworkArchitecture; +using SharedDotNetFrameworkArchitecture = Microsoft.Build.Shared.DotNetFrameworkArchitecture; +using Microsoft.Win32; +using System.Collections.ObjectModel; + +namespace Microsoft.Build.Utilities +{ + /// + /// Used to specify the targeted version of the .NET Framework for some methods of ToolLocationHelper. + /// + public enum TargetDotNetFrameworkVersion + { + /// + /// version 1.1 + /// + Version11 = 0, + + /// + /// version 2.0 + /// + Version20 = 1, + + /// + /// version 3.0 + /// + Version30 = 2, + + /// + /// version 3.5 + /// + Version35 = 3, + + /// + /// version 4.0 + /// + Version40 = 4, + + /// + /// version 4.5 + /// + Version45 = 5, + + /// + /// version 4.5.1 + /// + Version451 = 6, + + /// + /// version 4.6 + /// + Version46 = 7, + + // keep this up to date, this should always point to the last entry + /// + /// the latest version available at the time of release + /// + VersionLatest = Version46 + } + + /// + /// Used to specify the version of Visual Studio from which to select associated + /// tools for some methods of ToolLocationHelper + /// + public enum VisualStudioVersion + { + /// + /// Visual Studio 2010 and SP1 + /// + Version100, + + /// + /// Visual Studio Dev11 + /// + Version110, + + /// + /// Visual Studio Dev12 + /// + Version120, + + /// + /// Visual Studio Dev14 + /// + Version140, + + // keep this up-to-date; always point to the last entry. + /// + /// The latest version available at the time of release + /// + VersionLatest = Version140 + } + + /// + /// Used to specify the targeted bitness of the .NET Framework for some methods of ToolLocationHelper + /// + public enum DotNetFrameworkArchitecture + { + /// + /// Indicates the .NET Framework that is currently being run under. + /// + Current = 0, + + /// + /// Indicates the 32-bit .NET Framework + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Bitness", Justification = "Bitness is a reasonable term")] + Bitness32 = 1, + + /// + /// Indicates the 64-bit .NET Framework + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Bitness", Justification = "Bitness is a reasonable term")] + Bitness64 = 2 + } + + /// + /// ToolLocationHelper provides utility methods for locating .NET Framework and .NET Framework SDK directories and files. + /// NOTE: All public methods of this class are available to MSBuild projects for use in functions - they must be safe for + /// use during project evaluation. + /// + public static class ToolLocationHelper + { + /// + /// Cache the results of reading the redist list so that we do not have to read the redist list over and over again to get the chains. + /// + private static Dictionary s_chainedReferenceAssemblyPath; + + /// + /// Lock object to synchronize chainedReferenceAssemblyPath dictionary + /// + private static object s_locker = new Object(); + + /// + /// Cache the results of calling the GetPathToRefernceAssemblies so that we do not recaculate it everytime we call the method + /// + private static Dictionary> s_cachedReferenceAssemblyPaths; + + /// + /// Cache the frameworkName of the highest version of a framework given its root path and identifier. + /// This is to optimize calls to GetHighestVersionOfTargetFramework + /// + private static Dictionary s_cachedHighestFrameworkNameForTargetFrameworkIdentifier; + + /// + /// Cache the sdk structure as found by enumerating the disk and registry. + /// + private static Dictionary> s_cachedTargetPlatforms; + + /// + /// Cache new style extension sdks that we've enumerated + /// + private static Dictionary s_cachedExtensionSdks; + + /// + /// Cache the display name for the TFM/FrameworkName, keyed by the target framework directory. + /// This is generated by the "Name" attribute on the root tag of the primary matching redist list. + /// Value is never an empty string or null: a name will be synthesized if necessary. + /// + private static Dictionary s_cachedTargetFrameworkDisplayNames; + + /// + /// Cache the set of target platform references for a particular combination of inputs. For legacy + /// target platforms, this is just grabbing all winmds from the References\CommonConfiguration\Neutral + /// folder; for OneCore-based platforms, this involves reading the list from Platform.xml and synthesizing + /// the locations. + /// + private static Dictionary s_cachedTargetPlatformReferences; + + /// + /// Cache the set of extension Sdk references for a particular combination of inputs. + /// + private static Dictionary s_cachedExtensionSdkReferences; + + /// + /// Cache the list of supported frameworks + /// + private static List s_targetFrameworkMonikers = null; + + private const string retailConfigurationName = "Retail"; + private const string neutralArchitectureName = "Neutral"; + private const string commonConfigurationFolderName = "CommonConfiguration"; + private const string redistFolderName = "Redist"; + private const string referencesFolderName = "References"; + private const string designTimeFolderName = "DesignTime"; + private const string platformsFolderName = "Platforms"; + private static readonly char[] s_diskRootSplitChars = new char[] { ';' }; + + /// + /// Delegate to a method which takes a version enumeration and return a string path + /// + internal delegate string VersionToPath(TargetDotNetFrameworkVersion version); + + #region Public methods + + /// + /// The current ToolsVersion. + /// + public static string CurrentToolsVersion + { + get + { + return MSBuildConstants.CurrentToolsVersion; + } + } + + /// + /// Get a sorted list of AssemblyFoldersExInfo which contain information about what directories the 3rd party assemblies are registered under for use during build and design time. + /// + /// This method will enumerate the AssemblyFoldersEx regisry location and return a list of AssemblyFoldersExInfo in the same order in which + /// they will be searched during both design and build time for reference assemblies. + /// + /// The root registry location for the targeted framework. For .NET this is SOFTWARE\MICROSOFT\.NETFramework + /// The targeted framework version (2.0, 3.0, 3.5, 4.0, ect) + /// The name of the folder (AssemblyFoldersEx) could also be PocketPC\AssemblyFoldersEx, or others + /// Components may declare Min and Max OSVersions in the registry this value can be used filter directories returned based on whether or not the osversion is bounded by the Min and Max versions declared by the component. If this value is blank or null no filtering is done + /// Components may declare platform guids in the registry this can be used to return only directories which have a certain platform guid. If this value is blank or null no filtering is done + /// What processor architecture is being targetd. This determines which registry hives are searched in what order. + /// On a 64 bit operating system we do the following + /// If you are targeting 64 bit (target x64 or ia64) + /// Add in the 64 bit hive first + /// Add in the 32 bit hive second + /// If you are not targeting a 64 bit + /// Add in the 32 bit hive first + /// Add in the 64 bit hive second + /// On a 32 bit machine we only add in the 32 bit hive. + /// + /// List of AssemblyFoldersExInfo + public static IList GetAssemblyFoldersExInfo(string registryRoot, string targetFrameworkVersion, string registryKeySuffix, string osVersion, string platform, System.Reflection.ProcessorArchitecture targetProcessorArchitecture) + { + ErrorUtilities.VerifyThrowArgumentLength(registryRoot, "RegistryRoot"); + ErrorUtilities.VerifyThrowArgumentLength(registryKeySuffix, "RegistryKeySuffix"); + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion, "targetFrameworkVersion"); + + AssemblyFoldersEx assemblyFoldersEx = new AssemblyFoldersEx(registryRoot, targetFrameworkVersion, registryKeySuffix, osVersion, platform, new GetRegistrySubKeyNames(RegistryHelper.GetSubKeyNames), new GetRegistrySubKeyDefaultValue(RegistryHelper.GetDefaultValue), targetProcessorArchitecture, new OpenBaseKey(RegistryHelper.OpenBaseKey)); + + List assemblyFolders = new List(); + assemblyFolders.AddRange(assemblyFoldersEx); + return assemblyFolders; + } + + /// + /// Get a list of SDK's installed on the machine for a given target platform + /// + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// IDictionary of installed SDKS and their location. K:SDKName V:SDK installation location + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IDictionary GetPlatformExtensionSDKLocations(string targetPlatformIdentifier, Version targetPlatformVersion) + { + return GetPlatformExtensionSDKLocations(null, null, targetPlatformIdentifier, targetPlatformVersion); + } + + /// + /// Get a list of SDK's installed on the machine for a given target platform + /// + /// Array of disk locations to search for sdks + /// Root registry location to look for sdks + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// IDictionary of installed SDKS and their location. K:SDKName V:SDK installation location + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IDictionary GetPlatformExtensionSDKLocations(string[] diskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) + { + return ToolLocationHelper.GetPlatformExtensionSDKLocations(diskRoots, null, registryRoot, targetPlatformIdentifier, targetPlatformVersion); + } + + /// + /// Get a list of SDK's installed on the machine for a given target platform + /// + /// Array of disk locations to search for sdks + /// New style extension SDK roots + /// Root registry location to look for sdks + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// IDictionary of installed SDKS and their location. K:SDKName V:SDK installation location + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IDictionary GetPlatformExtensionSDKLocations(string[] diskRoots, string[] extensionDiskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) + { + Dictionary extensionSDKs = new Dictionary(StringComparer.OrdinalIgnoreCase); + var targetPlatformMonikers = GetTargetPlatformMonikers(diskRoots, extensionDiskRoots, registryRoot, targetPlatformIdentifier, targetPlatformVersion); + foreach (TargetPlatformSDK moniker in targetPlatformMonikers) + { + foreach (KeyValuePair extension in moniker.ExtensionSDKs) + { + extensionSDKs[extension.Key] = extension.Value; + } + } + return extensionSDKs; + } + + /// + /// Get a list of SDK's installed on the machine for a given target platform + /// + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// IDictionary of installed SDKS and their tuples containing (location, platform version). + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Casing kept to maintain consistency with existing APIs")] + public static IDictionary> GetPlatformExtensionSDKLocationsAndVersions(string targetPlatformIdentifier, Version targetPlatformVersion) + { + return GetPlatformExtensionSDKLocationsAndVersions(null, null, targetPlatformIdentifier, targetPlatformVersion); + } + + /// + /// Set of installed SDKs and their location and platform versions + /// + /// Array of disk locations to search for sdks + /// Root registry location to look for sdks + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// IDictionary of installed SDKS and their tuples containing (location, platform version). + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Casing kept to maintain consistency with existing APIs")] + public static IDictionary> GetPlatformExtensionSDKLocationsAndVersions(string[] diskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) + { + return GetPlatformExtensionSDKLocationsAndVersions(diskRoots, null, registryRoot, targetPlatformIdentifier, targetPlatformVersion); + } + + /// + /// Set of installed SDKs and their location and platform versions + /// + /// Array of disk locations to search for sdks + /// Array of disk locations to search for SDKs that target multiple versions + /// Root registry location to look for sdks + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// IDictionary of installed SDKS and their tuples containing (location, platform version). Version may be null if the SDK targets multiple versions. + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Casing kept to maintain consistency with existing APIs")] + public static IDictionary> GetPlatformExtensionSDKLocationsAndVersions(string[] diskRoots, string[] multiPlatformDiskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) + { + Dictionary> extensionSDKsAndVersions = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var targetPlatformMonikers = GetTargetPlatformMonikers(diskRoots, multiPlatformDiskRoots, registryRoot, targetPlatformIdentifier, targetPlatformVersion); + + foreach (TargetPlatformSDK moniker in targetPlatformMonikers) + { + foreach (KeyValuePair extension in moniker.ExtensionSDKs) + { + extensionSDKsAndVersions[extension.Key] = Tuple.Create(extension.Value, moniker.TargetPlatformVersion.ToString()); + } + } + return extensionSDKsAndVersions; + } + + /// + /// Get target platform monikers used to extract ESDK information in the methods GetPlatformExtensionSDKLocationsAndVersions and GetPlatformExtensionSDKLocations + /// + private static IEnumerable GetTargetPlatformMonikers(string[] diskRoots, string[] extensionDiskRoots, string registryRoot, string targetPlatformIdentifier, Version targetPlatformVersion) + { + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + + ErrorUtilities.DebugTraceMessage("GetPlatformExtensionSDKLocations", "Calling with TargetPlatformIdentifier:'{0}' and TargetPlatformVersion: '{1}'", targetPlatformIdentifier, targetPlatformVersion.ToString()); + IEnumerable targetPlatformSDKs = RetrieveTargetPlatformList(diskRoots, extensionDiskRoots, registryRoot); + + return targetPlatformSDKs + .Where(platformSDK => + String.IsNullOrEmpty(platformSDK.TargetPlatformIdentifier) + || + ( + platformSDK.TargetPlatformIdentifier.Equals(targetPlatformIdentifier, StringComparison.OrdinalIgnoreCase) + && platformSDK.TargetPlatformVersion <= targetPlatformVersion + )) + .OrderBy(platform => platform.TargetPlatformVersion); + } + + /// + /// Given an SDKName, targetPlatformIdentifier and TargetPlatformVersion search the default sdk locations for the passed in sdk name. + /// The format of the sdk moniker is SDKName, Version=X.X + /// + /// Name of the SDK to determine the installation location for. + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// Location of the SDK if it is found, empty string if it could not be found + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, Version targetPlatformVersion) + { + return GetPlatformExtensionSDKLocation(sdkMoniker, targetPlatformIdentifier, targetPlatformVersion, null, null); + } + + /// + /// Given an SDKName, targetPlatformIdentifier and TargetPlatformVersion search the default sdk locations for the passed in sdk name. + /// The format of the sdk moniker is SDKName, Version=X.X + /// + /// Name of the SDK to determine the installation location for. + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// List of disk roots to search for sdks within + /// Registry root to look for sdks within + /// Location of the SDK if it is found, empty string if it could not be found + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string registryRoot) + { + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + ErrorUtilities.VerifyThrowArgumentLength(sdkMoniker, "sdkMoniker"); + + IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, null, registryRoot); + var targetPlatformMoniker = targetPlatforms.Where(platform => platform.TargetPlatformIdentifier.Equals(targetPlatformIdentifier, StringComparison.OrdinalIgnoreCase) + && platform.TargetPlatformVersion <= targetPlatformVersion + && platform.ExtensionSDKs.ContainsKey(sdkMoniker) + ).OrderByDescending(platform => platform.TargetPlatformVersion) + .DefaultIfEmpty(null).FirstOrDefault(); + if (targetPlatformMoniker != null) + { + return targetPlatformMoniker.ExtensionSDKs[sdkMoniker]; + } + else + { + return String.Empty; + } + } + + /// + /// Given an SDK moniker and the targeted platform get the path to the SDK root if it exists. + /// + /// Moniker for the sdk + /// Identifier for the platform + /// Version of the platform + /// A full path to the sdk root if the sdk exists in the targeted platform or an empty string if it does not exist. + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, string targetPlatformVersion) + { + return GetPlatformExtensionSDKLocation(sdkMoniker, targetPlatformIdentifier, targetPlatformVersion, null, null); + } + + /// + /// Given an SDKName, targetPlatformIdentifier and TargetPlatformVersion search the default sdk locations for the passed in sdk name. + /// The format of the sdk moniker is SDKName, Version=X.X + /// + /// Name of the SDK to determine the installation location for. + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// List of disk roots to search for sdks within + /// Registry root to look for sdks within + /// Location of the SDK if it is found, empty string if it could not be found + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformExtensionSDKLocation(string sdkMoniker, string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string registryRoot) + { + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + + string[] sdkDiskRoots = null; + if (!String.IsNullOrEmpty(diskRoots)) + { + sdkDiskRoots = diskRoots.Split(s_diskRootSplitChars, StringSplitOptions.RemoveEmptyEntries); + } + + Version platformVersion = null; + string sdkLocation = String.Empty; + + if (Version.TryParse(targetPlatformVersion, out platformVersion)) + { + sdkLocation = GetPlatformExtensionSDKLocation(sdkMoniker, targetPlatformIdentifier, platformVersion, sdkDiskRoots, registryRoot); + } + + return sdkLocation; + } + + /// + /// Gets a dictionary containing a collection of extension SDKs and filter it based on the target platform version + /// if max platform version isn't set in the extension sdk manifest, add the extension sdk to the filtered list + /// + /// + /// + /// A IDictionary collection of filtered extension SDKs + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Not worth breaking customers")] + public static IDictionary FilterPlatformExtensionSDKs(Version targetPlatformVersion, IDictionary extensionSdks) + { + Dictionary filteredExtensionSdks = new Dictionary(); + foreach (KeyValuePair sdk in extensionSdks) + { + ExtensionSDK extensionSDK = new ExtensionSDK(sdk.Key, sdk.Value); + + // filter based on platform version - let pass if not in manifest or parameter + if (extensionSDK.MaxPlatformVersion == null || targetPlatformVersion == null || extensionSDK.MaxPlatformVersion >= targetPlatformVersion) + { + filteredExtensionSdks.Add(sdk.Key, sdk.Value); + } + } + return filteredExtensionSdks; + } + + /// + /// Get the list of SDK folders which contains the references for the sdk at the sdkRoot provided + /// in the order in which they should be searched for references. + /// + /// Root folder for the SDK + /// A list of folders in the order which they should be used when looking for references in the SDK + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IList GetSDKReferenceFolders(string sdkRoot) + { + return GetSDKReferenceFolders(sdkRoot, retailConfigurationName, neutralArchitectureName); + } + + /// + /// Get the list of SDK folders which contains the references for the sdk at the sdkRoot provided + /// in the order in which they should be searched for references. + /// + /// Root folder for the SDK + /// The configuration the SDK is targeting. This should be Debug or Retail + /// The architecture the SDK is targeting + /// A list of folders in the order which they should be used when looking for references in the SDK + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IList GetSDKReferenceFolders(string sdkRoot, string targetConfiguration, string targetArchitecture) + { + ErrorUtilities.VerifyThrowArgumentLength(sdkRoot, "sdkRoot"); + ErrorUtilities.VerifyThrowArgumentLength(targetConfiguration, "targetConfiguration"); + ErrorUtilities.VerifyThrowArgumentLength(targetArchitecture, "targetArchitecture"); + + List referenceDirectories = new List(4); + + string legacyWindowsMetadataLocation = Path.Combine(sdkRoot, "Windows Metadata"); + if (FileUtilities.DirectoryExistsNoThrow(legacyWindowsMetadataLocation)) + { + legacyWindowsMetadataLocation = FileUtilities.EnsureTrailingSlash(legacyWindowsMetadataLocation); + referenceDirectories.Add(legacyWindowsMetadataLocation); + } + + AddSDKPaths(sdkRoot, referencesFolderName, targetConfiguration, targetArchitecture, referenceDirectories); + + return referenceDirectories; + } + + /// + /// Add the set of paths for where sdk files should be found. Where is redist, references, designtime + /// + private static void AddSDKPaths(string sdkRoot, string folderName, string targetConfiguration, string targetArchitecture, List directories) + { + targetArchitecture = RemapSdkArchitecture(targetArchitecture); + + // \\Debug\X86 + AddSDKPath(sdkRoot, folderName, targetConfiguration, targetArchitecture, directories); + + if (!neutralArchitectureName.Equals(targetArchitecture, StringComparison.OrdinalIgnoreCase)) + { + // \\Debug\Neutral + AddSDKPath(sdkRoot, folderName, targetConfiguration, neutralArchitectureName, directories); + } + + // \\CommonConfiguration\x86 + AddSDKPath(sdkRoot, folderName, commonConfigurationFolderName, targetArchitecture, directories); + + if (!neutralArchitectureName.Equals(targetArchitecture, StringComparison.OrdinalIgnoreCase)) + { + // \\CommonConfiguration\Neutral + AddSDKPath(sdkRoot, folderName, commonConfigurationFolderName, neutralArchitectureName, directories); + } + } + + /// + /// Get the list of SDK folders which contains the redist files for the sdk at the sdkRoot provided + /// in the order in which they should be searched for references. + /// + /// Root folder for the SDK must contain a redist folder + /// A list of folders in the order which they should be used when looking for redist files in the SDK + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IList GetSDKRedistFolders(string sdkRoot) + { + return GetSDKRedistFolders(sdkRoot, retailConfigurationName, neutralArchitectureName); + } + + /// + /// Get the list of SDK folders which contains the redist files for the sdk at the sdkRoot provided + /// in the order in which they should be searched for references. + /// + /// Root folder for the SDK must contain a redist folder + /// The configuration the SDK is targeting. This should be Debug or Retail + /// The architecture the SDK is targeting + /// A list of folders in the order which they should be used when looking for redist files in the SDK + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IList GetSDKRedistFolders(string sdkRoot, string targetConfiguration, string targetArchitecture) + { + ErrorUtilities.VerifyThrowArgumentLength(sdkRoot, "sdkRoot"); + ErrorUtilities.VerifyThrowArgumentLength(targetConfiguration, "targetConfiguration"); + ErrorUtilities.VerifyThrowArgumentLength(targetArchitecture, "targetArchitecture"); + + List redistDirectories = new List(4); + + AddSDKPaths(sdkRoot, redistFolderName, targetConfiguration, targetArchitecture, redistDirectories); + return redistDirectories; + } + + /// + /// Get the list of SDK folders which contains the designtime files for the sdk at the sdkRoot provided + /// in the order in which they should be searched for references. + /// + /// Root folder for the SDK must contain a Designtime folder + /// A list of folders in the order which they should be used when looking for DesignTime files in the SDK + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IList GetSDKDesignTimeFolders(string sdkRoot) + { + return GetSDKDesignTimeFolders(sdkRoot, retailConfigurationName, neutralArchitectureName); + } + + /// + /// Get the list of SDK folders which contains the DesignTime files for the sdk at the sdkRoot provided + /// in the order in which they should be searched for references. + /// + /// Root folder for the SDK must contain a DesignTime folder + /// The configuration the SDK is targeting. This should be Debug or Retail + /// The architecture the SDK is targeting + /// A list of folders in the order which they should be used when looking for DesignTime files in the SDK + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static IList GetSDKDesignTimeFolders(string sdkRoot, string targetConfiguration, string targetArchitecture) + { + ErrorUtilities.VerifyThrowArgumentLength(sdkRoot, "sdkRoot"); + ErrorUtilities.VerifyThrowArgumentLength(targetConfiguration, "targetConfiguration"); + ErrorUtilities.VerifyThrowArgumentLength(targetArchitecture, "targetArchitecture"); + + List designTimeDirectories = new List(4); + + AddSDKPaths(sdkRoot, designTimeFolderName, targetConfiguration, targetArchitecture, designTimeDirectories); + return designTimeDirectories; + } + + /// + /// Get a list target platform sdks on the machine. + /// + /// List of Target Platform SDKs, Item1: TargetPlatformName Item2: Version of SDK Item3: Path to sdk root + public static IList GetTargetPlatformSdks() + { + return GetTargetPlatformSdks(null, null); + } + + /// + /// Get a list target platform sdks on the machine. + /// + /// List of disk locations to search for platform sdks + /// Registry root location to look for platform sdks + /// List of Target Platform SDKs + public static IList GetTargetPlatformSdks(string[] diskRoots, string registryRoot) + { + IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, null, registryRoot); + return targetPlatforms.Where(platform => platform.Path != null).ToList(); + } + + /// + /// Filter list of platform sdks based on minimum OS and VS versions + /// + /// List of platform sdks + /// Operating System version. Pass null to not filter based on this parameter + /// Visual Studio version. Pass null not to filter based on this parameter + /// List of Target Platform SDKs + public static IList FilterTargetPlatformSdks(IList targetPlatformSdkList, Version osVersion, Version vsVersion) + { + List filteredTargetPlatformSdkList = new List(); + + foreach (TargetPlatformSDK targetPlatformSdk in targetPlatformSdkList) + { + if ( + (targetPlatformSdk.MinOSVersion == null || osVersion == null || targetPlatformSdk.MinOSVersion <= osVersion) && // filter based on OS version - let pass if not in manifest or parameter + (targetPlatformSdk.MinVSVersion == null || vsVersion == null || targetPlatformSdk.MinVSVersion <= vsVersion) // filter based on VS version - let pass if not in manifest or parameter + ) + { + filteredTargetPlatformSdkList.Add(targetPlatformSdk); + } + } + + return filteredTargetPlatformSdkList; + } + + /// + /// Get the location of the target platform SDK props file for a given {SDKI, SDKV, TPI, TPMinV, TPV} combination. + /// + /// The OneCore SDK identifier that defines OnceCore SDK root + /// The verision of the OneCore SDK + /// Identifier for the targeted platform + /// The min version of the targeted platform + /// The version of the targeted platform + /// Location of the target platform SDK props file without .props filename + public static string GetPlatformSDKPropsFileLocation + ( + string sdkIdentifier, + string sdkVersion, + string targetPlatformIdentifier, + string targetPlatformMinVersion, + string targetPlatformVersion + ) + { + return GetPlatformSDKPropsFileLocation(sdkIdentifier, sdkVersion, targetPlatformIdentifier, targetPlatformMinVersion, targetPlatformVersion, null, null); + } + + /// + /// Get the location of the target platform SDK props file for a given {SDKI, SDKV, TPI, TPMinV, TPV} combination. + /// + /// The OneCore SDK identifier that defines OnceCore SDK root + /// The verision of the OneCore SDK + /// Identifier for the targeted platform + /// The min version of the targeted platform + /// The version of the targeted platform + /// List of disk roots to search for sdks within + /// Registry root to look for sdks within + /// Location of the target platform SDK props file without .props filename + public static string GetPlatformSDKPropsFileLocation + ( + string sdkIdentifier, + string sdkVersion, + string targetPlatformIdentifier, + string targetPlatformMinVersion, + string targetPlatformVersion, + string diskRoots, + string registryRoot + ) + { + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion, "targetPlatformVersion"); + + string propsFileLocation = null; + + try + { + string sdkRoot = null; + + if (String.IsNullOrEmpty(sdkIdentifier)) + { + // Falls back to the old SDK world, e.g. C:\Program Files (x86)\Windows Kits\8.2 + sdkRoot = ToolLocationHelper.GetPlatformSDKLocation(targetPlatformIdentifier, targetPlatformVersion, diskRoots, registryRoot); + } + else + { + // Get the root to the new Once Core SDK. For now it's like: C:\Program Files (x86)\Windows SDKs\1.0 + sdkRoot = ToolLocationHelper.GetPlatformSDKLocation(sdkIdentifier, sdkVersion, diskRoots, registryRoot); + } + + if (!String.IsNullOrEmpty(sdkRoot)) + { + // In the old SDK world, it is e.g. C:\Program Files (x86)\Windows Kits\8.2\DesignTime\CommonConfiguration\Neutral + // In OneCore SDK world, it is e.g. C:\Program Files (x86)\Windows SDKs\1.0\DesignTime\CommonConfiguration\Neutral\UAP\0.8.0.0 + + if (String.IsNullOrEmpty(sdkIdentifier)) + { + propsFileLocation = Path.Combine(sdkRoot, designTimeFolderName, commonConfigurationFolderName, neutralArchitectureName); + } + else + { + propsFileLocation = Path.Combine(sdkRoot, designTimeFolderName, commonConfigurationFolderName, neutralArchitectureName, targetPlatformIdentifier, targetPlatformVersion); + } + + if (Directory.Exists(propsFileLocation)) + { + return propsFileLocation; + } + else + { + ErrorUtilities.DebugTraceMessage("GetPlatformSDKPropsFileLocation", "Target platform props file location '{0}' did not exist.", propsFileLocation); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GetPlatformSDKPropsFileLocation", "Could not find root SDK location for SDKI = '{0}', SDKV = '{1}'", sdkIdentifier, sdkVersion); + } + } + catch (Exception e) + { + if (!ExceptionHandling.IsIoRelatedException(e)) + { + throw; + } + + ErrorUtilities.DebugTraceMessage("GetPlatformSDKPropsFileLocation", "Encountered exception trying to get the SDK props file Location : {0}", e.Message); + } + + return null; + } + + /// + /// Gathers the set of platform winmds for a particular {SDKI, SDKV, TPI, TPMinV, TPV} combination + /// + public static string[] GetTargetPlatformReferences + ( + string sdkIdentifier, + string sdkVersion, + string targetPlatformIdentifier, + string targetPlatformMinVersion, + string targetPlatformVersion + ) + { + return GetTargetPlatformReferences(sdkIdentifier, sdkVersion, targetPlatformIdentifier, targetPlatformMinVersion, targetPlatformVersion, null, null); + } + + /// + /// Gathers the set of platform winmds for a particular {SDKI, SDKV, TPI, TPMinV, TPV} combination + /// + public static string[] GetTargetPlatformReferences + ( + string sdkIdentifier, + string sdkVersion, + string targetPlatformIdentifier, + string targetPlatformMinVersion, + string targetPlatformVersion, + string diskRoots, + string registryRoot + ) + { + if (s_cachedTargetPlatformReferences == null) + { + s_cachedTargetPlatformReferences = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + string cacheKey = String.Join("|", sdkIdentifier, sdkVersion, targetPlatformIdentifier, targetPlatformMinVersion, targetPlatformVersion, diskRoots, registryRoot); + + string[] targetPlatformReferences = null; + if (s_cachedTargetPlatformReferences.TryGetValue(cacheKey, out targetPlatformReferences)) + { + return targetPlatformReferences; + } + + if (String.IsNullOrEmpty(sdkIdentifier) && String.IsNullOrEmpty(sdkVersion)) + { + targetPlatformReferences = GetLegacyTargetPlatformReferences(targetPlatformIdentifier, targetPlatformVersion, diskRoots, registryRoot); + } + else + { + targetPlatformReferences = GetTargetPlatformReferencesFromManifest(sdkIdentifier, sdkVersion, targetPlatformIdentifier, targetPlatformMinVersion, targetPlatformVersion, diskRoots, registryRoot); + } + + s_cachedTargetPlatformReferences.Add(cacheKey, targetPlatformReferences); + return targetPlatformReferences; + } + + /// + /// Gathers the specified extension SDK references for the given target SDK + /// + /// The moniker is the Name/Version string. Example: "Windows Desktop, Version=10.0.0.1" + /// The target SDK name. + /// The target SDK version. + /// The disk roots used to gather installed SDKs. + /// The disk roots used to gather installed extension SDKs. + /// The registry root used to gather installed extension SDKs. + public static string[] GetPlatformOrFrameworkExtensionSdkReferences + ( + string extensionSdkMoniker, + string targetSdkIdentifier, + string targetSdkVersion, + string diskRoots, + string extensionDiskRoots, + string registryRoot + ) + { + if (s_cachedExtensionSdkReferences == null) + { + s_cachedExtensionSdkReferences = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + string cacheKey = String.Join("|", extensionSdkMoniker, targetSdkIdentifier, targetSdkVersion); + + string[] extensionSdkReferences = null; + if (s_cachedExtensionSdkReferences.TryGetValue(cacheKey, out extensionSdkReferences)) + { + return extensionSdkReferences; + } + + TargetPlatformSDK matchingSdk = GetMatchingPlatformSDK(targetSdkIdentifier, targetSdkVersion, diskRoots, extensionDiskRoots, registryRoot); + + if (matchingSdk == null) + { + ErrorUtilities.DebugTraceMessage("GetExtensionSdkReferences", "Could not find root SDK for SDKI = '{0}', SDKV = '{1}'", targetSdkIdentifier, targetSdkVersion); + } + else + { + string targetSdkPath = matchingSdk.Path; + string extensionSdkPath = null; + + if (matchingSdk.ExtensionSDKs.TryGetValue(extensionSdkMoniker, out extensionSdkPath) + || + ( + // It is possible the SDK may be of the newer style (targets multiple). We need to hit the untargeted SDK cache to look for a hit. + s_cachedExtensionSdks.TryGetValue(extensionDiskRoots, out matchingSdk) + && matchingSdk.ExtensionSDKs.TryGetValue(extensionSdkMoniker, out extensionSdkPath) + )) + { + ExtensionSDK extensionSdk = new ExtensionSDK(extensionSdkMoniker, extensionSdkPath); + if (extensionSdk.SDKType == SDKType.Framework || extensionSdk.SDKType == SDKType.Platform) + { + // We don't want to attempt to gather ApiContract references if the framework isn't explicitly marked as Framework/Platform + extensionSdkReferences = GetApiContractReferences(extensionSdk.ApiContracts, targetSdkPath); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GetExtensionSdkReferences", "Could not find matching extension SDK = '{0}'", extensionSdkMoniker); + } + } + + s_cachedExtensionSdkReferences.Add(cacheKey, extensionSdkReferences); + return extensionSdkReferences; + } + + /// + /// Gathers the set of platform winmds based on the assumption that they come from + /// an SDK that is specified solely by TPI / TPV. + /// + private static string[] GetLegacyTargetPlatformReferences + ( + string targetPlatformIdentifier, + string targetPlatformVersion, + string diskRoots, + string registryRoot + ) + { + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion, "targetPlatformVersion"); + + try + { + // TODO: Add caching so that we only have to read all this stuff in once. + string sdkRoot = ToolLocationHelper.GetPlatformSDKLocation(targetPlatformIdentifier, targetPlatformVersion, diskRoots, registryRoot); + string winmdLocation = null; + + if (!String.IsNullOrEmpty(sdkRoot)) + { + winmdLocation = Path.Combine(sdkRoot, referencesFolderName, commonConfigurationFolderName, neutralArchitectureName); + + if (!Directory.Exists(winmdLocation)) + { + ErrorUtilities.DebugTraceMessage("GetLegacyTargetPlatformReferences", "Target platform location '{0}' did not exist", winmdLocation); + winmdLocation = null; + } + } + else + { + ErrorUtilities.DebugTraceMessage("GetLegacyTargetPlatformReferences", "Could not find root SDK location for TPI = '{0}', TPV = '{1}'", targetPlatformIdentifier, targetPlatformVersion); + } + + if (!String.IsNullOrEmpty(winmdLocation)) + { + string[] winmdPaths = Directory.GetFiles(winmdLocation, "*.winmd"); + + if (winmdPaths.Length > 0) + { + ErrorUtilities.DebugTraceMessage("GetLegacyTargetPlatformReferences", "Found {0} contract winmds in '{1}'", winmdPaths.Length, winmdLocation); + return winmdPaths; + } + } + } + catch (Exception e) + { + if (!ExceptionHandling.IsIoRelatedException(e)) + { + throw; + } + + ErrorUtilities.DebugTraceMessage("GetLegacyTargetPlatformReferences", "Encountered exception trying to gather the platform references: {0}", e.Message); + } + + return new string[0]; + } + + /// + /// Gathers the set of platform winmds for a particular {SDKI, SDKV, TPI, TPMinV, TPV} combination, + /// based on the assumption that it is an SDK that has both {SDKI, SDKV} and TP* specifiers. + /// + private static string[] GetTargetPlatformReferencesFromManifest + ( + string sdkIdentifier, + string sdkVersion, + string targetPlatformIdentifier, + string targetPlatformMinVersion, + string targetPlatformVersion, + string diskRoots, + string registryRoot + ) + { + ErrorUtilities.VerifyThrowArgumentLength(sdkIdentifier, "sdkIdentifier"); + ErrorUtilities.VerifyThrowArgumentLength(sdkVersion, "sdkVersion"); + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformVersion, "targetPlatformVersion"); + + string[] contractWinMDs = new string[0]; + + try + { + // TODO: Add caching so that we only have to read all this stuff in once. + TargetPlatformSDK matchingSdk = GetMatchingPlatformSDK(sdkIdentifier, sdkVersion, diskRoots, null, registryRoot); + string platformManifestLocation = null; + + if (matchingSdk != null) + { + string platformKey = TargetPlatformSDK.GetSdkKey(targetPlatformIdentifier, targetPlatformVersion); + + if (!matchingSdk.Platforms.TryGetValue(platformKey, out platformManifestLocation)) + { + ErrorUtilities.DebugTraceMessage("GetTargetPlatformReferencesFromManifest", "Target platform location '{0}' did not exist or did not contain Platform.xml", platformManifestLocation); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GetTargetPlatformReferencesFromManifest", "Could not find root SDK for SDKI = '{0}', SDKV = '{1}'", sdkIdentifier, sdkVersion); + } + + if (!String.IsNullOrEmpty(platformManifestLocation)) + { + PlatformManifest manifest = new PlatformManifest(platformManifestLocation); + + if (!manifest.ReadError) + { + contractWinMDs = GetApiContractReferences(manifest.ApiContracts, matchingSdk.Path); + } + } + } + catch (Exception e) + { + if (!ExceptionHandling.IsIoRelatedException(e)) + { + throw; + } + + ErrorUtilities.DebugTraceMessage("GetTargetPlatformReferences", "Encountered exception trying to gather the platform references: {0}", e.Message); + } + + return contractWinMDs; + } + + /// + /// Return the WinMD paths referenced by the given api contracts and target sdk root + /// + /// The API contract definitions + /// The root of the target platform SDK + /// List of matching WinMDs + internal static string[] GetApiContractReferences(IEnumerable apiContracts, string targetPlatformSdkRoot) + { + List contractWinMDs = new List(); + + string referencesRoot = Path.Combine(targetPlatformSdkRoot, referencesFolderName); + + foreach (ApiContract contract in apiContracts) + { + ErrorUtilities.DebugTraceMessage("GetApiContractReferences", "Gathering contract references for contract with name '{0}' and version '{1}", contract.Name, contract.Version); + string contractPath = Path.Combine(referencesRoot, contract.Name, contract.Version); + + if (Directory.Exists(contractPath)) + { + string[] winmdPaths = Directory.GetFiles(contractPath, "*.winmd"); + + if (winmdPaths.Length > 0) + { + ErrorUtilities.DebugTraceMessage("GetApiContractReferences", "Found {0} contract winmds in '{1}'", winmdPaths.Length, contractPath); + contractWinMDs.AddRange(winmdPaths); + } + } + } + + return contractWinMDs.ToArray(); + } + + /// + /// Given a target platform identifier and a target platform version search the default sdk locations for the platform sdk for the target platform. + /// + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// Location of the SDK if it is found, empty string if it could not be found + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformSDKLocation(string targetPlatformIdentifier, Version targetPlatformVersion) + { + return GetPlatformSDKLocation(targetPlatformIdentifier, targetPlatformVersion, null, null); + } + + /// + /// Given a target platform identifier and a target platform version search the default sdk locations for the platform sdk for the target platform. + /// + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// List of disk roots to search for sdks within + /// Registry root to look for sdks within + /// Location of the SDK if it is found, empty string if it could not be found + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformSDKLocation(string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string registryRoot) + { + var targetPlatform = GetMatchingPlatformSDK(targetPlatformIdentifier, targetPlatformVersion, diskRoots, registryRoot); + + if (targetPlatform != null && targetPlatform.Path != null) + { + return targetPlatform.Path; + } + else + { + return String.Empty; + } + } + + /// + /// Given a target platform identifier and a target platform version search the default sdk locations for the platform sdk for the target platform. + /// + /// Identifier for the platform + /// Version of the platform + /// A full path to the sdk root if the sdk exists in the targeted platform or an empty string if it does not exist. + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformSDKLocation(string targetPlatformIdentifier, string targetPlatformVersion) + { + return GetPlatformSDKLocation(targetPlatformIdentifier, targetPlatformVersion, null, null); + } + + /// + /// Given a target platform identifier and a target platform version search the default sdk locations for the platform sdk for the target platform. + /// + /// Targeted platform to find SDKs for + /// Targeted platform version to find SDKs for + /// List of disk roots to search for sdks within + /// Registry root to look for sdks within + /// Location of the platform SDK if it is found, empty string if it could not be found + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static string GetPlatformSDKLocation(string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string registryRoot) + { + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + + string[] sdkDiskRoots = null; + if (!String.IsNullOrEmpty(diskRoots)) + { + sdkDiskRoots = diskRoots.Split(s_diskRootSplitChars, StringSplitOptions.RemoveEmptyEntries); + } + + Version platformVersion = null; + string sdkLocation = String.Empty; + + if (Version.TryParse(targetPlatformVersion, out platformVersion)) + { + sdkLocation = GetPlatformSDKLocation(targetPlatformIdentifier, platformVersion, sdkDiskRoots, registryRoot); + } + + return sdkLocation; + } + + /// + /// Given a target platform identifier and version, get the display name for that platform SDK. + /// + public static string GetPlatformSDKDisplayName(string targetPlatformIdentifier, string targetPlatformVersion) + { + return GetPlatformSDKDisplayName(targetPlatformIdentifier, targetPlatformVersion, null, null); + } + + /// + /// Given a target platform identifier and version, get the display name for that platform SDK. + /// + public static string GetPlatformSDKDisplayName(string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string registryRoot) + { + TargetPlatformSDK targetPlatform = GetMatchingPlatformSDK(targetPlatformIdentifier, targetPlatformVersion, diskRoots, null, registryRoot); + + if (targetPlatform != null && targetPlatform.DisplayName != null) + { + return targetPlatform.DisplayName; + } + else + { + return GenerateDefaultSDKDisplayName(targetPlatformIdentifier, targetPlatformVersion); + } + } + + /// + /// Given an SDK identifier and an SDK version, return a list of installed platforms. + /// + /// SDK for which to find the installed platforms + /// SDK version for which to find the installed platforms + /// A list of keys for the installed platforms for the given SDK + public static IEnumerable GetPlatformsForSDK(string sdkIdentifier, Version sdkVersion) + { + return GetPlatformsForSDK(sdkIdentifier, sdkVersion, null, null); + } + + /// + /// Given an SDK identifier and an SDK version, return a list of installed platforms. + /// + /// SDK for which to find the installed platforms + /// SDK version for which to find the installed platforms + /// List of disk roots to search for sdks within + /// Registry root to look for sdks within + /// A list of keys for the installed platforms for the given SDK + public static IEnumerable GetPlatformsForSDK(string sdkIdentifier, Version sdkVersion, string[] diskRoots, string registryRoot) + { + ErrorUtilities.VerifyThrowArgumentNull(sdkIdentifier, "sdkIdentifier"); + ErrorUtilities.VerifyThrowArgumentNull(sdkVersion, "sdkVersion"); + + IEnumerable targetPlatformSDKs = RetrieveTargetPlatformList(diskRoots, null, registryRoot); + + List platforms = new List(); + foreach (TargetPlatformSDK sdk in targetPlatformSDKs) + { + bool isSDKMatch = string.Equals(sdk.TargetPlatformIdentifier, sdkIdentifier, StringComparison.OrdinalIgnoreCase) && Version.Equals(sdk.TargetPlatformVersion, sdkVersion); + if (!isSDKMatch || sdk.Platforms == null) + { + continue; + } + + foreach (string platform in sdk.Platforms.Keys) + { + platforms.Add(platform); + } + } + + return platforms; + } + + /// + /// Given a target platform identifier and version and locations in which to search, find the TargetPlatformSDK + /// object that matches. + /// + private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIdentifier, string targetPlatformVersion, string diskRoots, string multiPlatformDiskRoots, string registryRoot) + { + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + + string[] sdkDiskRoots = null; + if (!String.IsNullOrEmpty(diskRoots)) + { + sdkDiskRoots = diskRoots.Split(s_diskRootSplitChars, StringSplitOptions.RemoveEmptyEntries); + } + + Version platformVersion; + if (Version.TryParse(targetPlatformVersion, out platformVersion)) + { + return GetMatchingPlatformSDK(targetPlatformIdentifier, platformVersion, sdkDiskRoots, registryRoot); + } + + return null; + } + + /// + /// Given a target platform identifier and version and locations in which to search, find the TargetPlatformSDK + /// object that matches. + /// + private static TargetPlatformSDK GetMatchingPlatformSDK(string targetPlatformIdentifier, Version targetPlatformVersion, string[] diskRoots, string registryRoot) + { + ErrorUtilities.VerifyThrowArgumentLength(targetPlatformIdentifier, "targetPlatformIdentifier"); + ErrorUtilities.VerifyThrowArgumentNull(targetPlatformVersion, "targetPlatformVersion"); + + IEnumerable targetPlatforms = RetrieveTargetPlatformList(diskRoots, null, registryRoot); + + TargetPlatformSDK matchingSdk = targetPlatforms + .Where( + platform => + ( + String.Equals(platform.TargetPlatformIdentifier, targetPlatformIdentifier, StringComparison.OrdinalIgnoreCase) + && Version.Equals(platform.TargetPlatformVersion, targetPlatformVersion)) + ).FirstOrDefault(); + + // For UAP platforms match against registered platforms... + // Logic is same as used for managed UAP projects + // vsproject\flavors\ProjectFlavoring\Microsoft.VisualStudio.ProjectFlavoring\Microsoft\VisualStudio\ProjectFlavoring\Retargeting\Management\VsMultiTargetingPlatformProvider.cs:FindPlatformSdk + if (matchingSdk == null && + string.Equals(targetPlatformIdentifier, "UAP", StringComparison.OrdinalIgnoreCase)) + { + string versionString = targetPlatformVersion.ToString(); + matchingSdk = targetPlatforms.FirstOrDefault(platform => platform.ContainsPlatform(targetPlatformIdentifier, versionString)); + } + return matchingSdk; + } + + /// + /// Given a target platform identifier and version, generate a reasonable default display name. + /// + /// + /// + private static string GenerateDefaultSDKDisplayName(string targetPlatformIdentifier, string targetPlatformVersion) + { + return targetPlatformIdentifier + " " + targetPlatformVersion; + } + + /// + /// Gets the fully qualified path to the system directory i.e. %SystemRoot%\System32 + /// + /// The system path. + public static string PathToSystem + { + get + { + return Environment.GetFolderPath(Environment.SpecialFolder.System); + } + } + + /// + /// Returns the prefix of the .NET Framework version folder (e.g. "v2.0") + /// + /// Version of the targeted .NET Framework + /// + public static string GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion version) + { + return FrameworkLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersionToSystemVersion(version)); + } + + /// + /// Returns the full name of the .NET Framework root registry key + /// + /// Version of the targeted .NET Framework + /// + public static string GetDotNetFrameworkRootRegistryKey(TargetDotNetFrameworkVersion version) + { + return FrameworkLocationHelper.fullDotNetFrameworkRegistryKey; + } + + /// + /// Returns the full name of the .NET Framework SDK root registry key. When targeting .NET 3.5 or + /// above, looks in the locations associated with Visual Studio 2010. If you wish to target the + /// .NET Framework SDK that ships with Visual Studio Dev11 or later, please use the override that + /// specifies a VisualStudioVersion. + /// + /// Version of the targeted .NET Framework + public static string GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion version) + { + return GetDotNetFrameworkSdkRootRegistryKey(version, VisualStudioVersion.VersionLatest); + } + + /// + /// Returns the full name of the .NET Framework SDK root registry key + /// + /// Version of the targeted .NET Framework + /// Version of Visual Studio the requested SDK is associated with + public static string GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + var dotNetFrameworkVersion = TargetDotNetFrameworkVersionToSystemVersion(version); + var vsVersion = VisualStudioVersionToSystemVersion(visualStudioVersion); + return FrameworkLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(dotNetFrameworkVersion, vsVersion); + } + + /// + /// Name of the value of GetDotNetFrameworkRootRegistryKey that contains the SDK install root path. When + /// targeting .NET 3.5 or above, looks in the locations associated with Visual Studio 2010. If you wish + /// to target the .NET Framework SDK that ships with Visual Studio Dev11 or later, please use the override + /// that specifies a VisualStudioVersion. + /// + /// Version of the targeted .NET Framework + public static string GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion version) + { + return GetDotNetFrameworkSdkInstallKeyValue(version, VisualStudioVersion.VersionLatest); + } + + /// + /// Name of the value of GetDotNetFrameworkRootRegistryKey that contains the SDK install root path + /// + /// Version of the targeted .NET Framework + /// Version of Visual Studio the requested SDK is associated with + public static string GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + var dotNetFrameworkVersion = TargetDotNetFrameworkVersionToSystemVersion(version); + var vsVersion = VisualStudioVersionToSystemVersion(visualStudioVersion); + return FrameworkLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(dotNetFrameworkVersion, vsVersion); + } + + /// + /// Get a fully qualified path to the frameworks root directory. + /// + /// Version of the targeted .NET Framework + /// Will return 'null' if there is no target frameworks on this machine. + public static string GetPathToDotNetFramework(TargetDotNetFrameworkVersion version) + { + return GetPathToDotNetFramework(version, UtilitiesDotNetFrameworkArchitecture.Current); + } + + /// + /// Get a fully qualified path to the framework's root directory. + /// + /// Version of the targeted .NET Framework + /// Desired architecture, or DotNetFrameworkArchitecture.Current for the architecture this process is currently running under. + /// + public static string GetPathToDotNetFramework(TargetDotNetFrameworkVersion version, UtilitiesDotNetFrameworkArchitecture architecture) + { + Version frameworkVersion = TargetDotNetFrameworkVersionToSystemVersion(version); + SharedDotNetFrameworkArchitecture sharedArchitecture = ConvertToSharedDotNetFrameworkArchitecture(architecture); + return FrameworkLocationHelper.GetPathToDotNetFramework(frameworkVersion, sharedArchitecture); + } + + /// + /// Returns the path to the "bin" directory of the .NET Framework SDK. When targeting .NET 3.5 + /// or above, looks in the locations associated with Visual Studio 2010. If you wish to target + /// the .NET Framework SDK that ships with Visual Studio Dev11 or later, please use the override + /// that specifies a VisualStudioVersion. + /// + /// Version of the targeted .NET Framework + /// Path string. + public static string GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion version) + { + return GetPathToDotNetFrameworkSdk(version, VisualStudioVersion.VersionLatest); + } + + public static string GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + var dotNetFrameworkVersion = TargetDotNetFrameworkVersionToSystemVersion(version); + var vsVersion = VisualStudioVersionToSystemVersion(visualStudioVersion); + return FrameworkLocationHelper.GetPathToDotNetFrameworkSdk(dotNetFrameworkVersion, vsVersion); + } + + /// + /// Returns the path to the reference assemblies location for the given framework version. + /// + /// Version of the targeted .NET Framework + /// Path string. + public static string GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion version) + { + return FrameworkLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersionToSystemVersion(version)); + } + + /// + /// Returns the path to the reference assemblies location for the given target framework's standard libraries (i.e. mscorlib). + /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles + /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths + /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths. + /// + /// Identifier being targeted + /// Version being targeted + /// Profile being targeted + /// When the frameworkName is null + /// Collection of reference assembly locations. + public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile) + { + IList referenceAssemblyDirectories = GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile); + + // Check each returned reference assembly directory for one containing mscorlib.dll + // When we find it (most of the time it will be the first in the set) we'll + // return that directory. + foreach (string referenceAssemblyDirectory in referenceAssemblyDirectories) + { + if (File.Exists(Path.Combine(referenceAssemblyDirectory, "mscorlib.dll"))) + { + // We found the framework reference assembly directory with mscorlib in it + // that's our standard lib path, so return it, with no trailing slash. + return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory); + } + } + + // We didn't find a standard library path in our set, return empty. + return String.Empty; + } + + /// + /// Returns the path to mscorlib and system.dll + /// + /// Identifier being targeted + /// Version being targeted + /// Profile being targeted + /// What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0 + /// When the frameworkName is null + /// Collection of reference assembly locations. + public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget) + { + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkIdentifier, "targetFrameworkIdentifier"); + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion, "targetFrameworkVersion"); + + Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion); + // For .net framework less than 4 the mscorlib should be found in the .net 2.0 directory + if (targetFrameworkIdentifier.Equals(FrameworkLocationHelper.dotNetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase) && frameworkVersion.Major < 4 && String.IsNullOrEmpty(targetFrameworkProfile)) + { + // The default + SharedDotNetFrameworkArchitecture targetedArchitecture = SharedDotNetFrameworkArchitecture.Current; + + if (platformTarget != null) + { + // If we are a 32 bit operating system the we should always return the 32 bit directory, or we are targeting x86, arm is also 32 bit + if (!Environment.Is64BitOperatingSystem || platformTarget.Equals("x86", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("arm", StringComparison.OrdinalIgnoreCase)) + { + targetedArchitecture = SharedDotNetFrameworkArchitecture.Bitness32; + } + else if (platformTarget.Equals("x64", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("Itanium", StringComparison.OrdinalIgnoreCase)) + { + targetedArchitecture = SharedDotNetFrameworkArchitecture.Bitness64; + } + } + + string legacyMsCorlib20Path = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(targetedArchitecture); + if (legacyMsCorlib20Path != null && File.Exists(Path.Combine(legacyMsCorlib20Path, "mscorlib.dll"))) + { + // We found the framework reference assembly directory with mscorlib in it + // that's our standard lib path, so return it, with no trailing slash. + return FileUtilities.EnsureNoTrailingSlash(legacyMsCorlib20Path); + } + + // If for some reason the 2.0 framework is not installed in its default location then maybe someone is using the ".net 4.0" reference assembly + // location, if so then we can just use what ever version they passed in becaues it should be MSIL now and not bit specific. + } + + IList referenceAssemblyDirectories = GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile); + // Check each returned reference assembly directory for one containing mscorlib.dll + // When we find it (most of the time it will be the first in the set) we'll + // return that directory. + foreach (string referenceAssemblyDirectory in referenceAssemblyDirectories) + { + if (File.Exists(Path.Combine(referenceAssemblyDirectory, "mscorlib.dll"))) + { + // We found the framework reference assembly directory with mscorlib in it + // that's our standard lib path, so return it, with no trailing slash. + return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory); + } + } + + // We didn't find a standard library path in our set, return empty. + return String.Empty; + } + + /// + /// Returns the paths to the reference assemblies location for the given target framework. + /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles + /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths + /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths. + /// + /// Identifier being targeted + /// Version being targeted + /// Profile being targeted + /// When the frameworkName is null + /// Collection of reference assembly locations. + public static IList GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile) + { + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkVersion, "targetFrameworkVersion"); + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkIdentifier, "targetFrameworkIdentifier"); + ErrorUtilities.VerifyThrowArgumentNull(targetFrameworkProfile, "targetFrameworkProfile"); + + Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion); + FrameworkNameVersioning targetFrameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, frameworkVersion, targetFrameworkProfile); + return GetPathToReferenceAssemblies(targetFrameworkName); + } + + + /// + /// Returns the paths to the reference assemblies location for the given target framework. + /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles + /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths + /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths. + /// + /// Moniker being targeted + /// When the frameworkName is null + /// Collection of reference assembly locations. + public static IList GetPathToReferenceAssemblies(FrameworkNameVersioning frameworkName) + { + // Verify the framework class passed in is not null. Other than being null the class will ensure the framework moniker is correct + ErrorUtilities.VerifyThrowArgumentNull(frameworkName, "frameworkName"); + IList paths = GetPathToReferenceAssemblies(FrameworkLocationHelper.programFilesReferenceAssemblyLocation, frameworkName); + return paths; + } + + /// + /// Call either the static method or the delegate. This is done purely for performance as the delegate is only required for ease of unit testing and since + /// the methods being called are static this will be a non 0 cost to use delegates vs the static methods directly. + /// + internal static string VersionToDotNetFrameworkPath(VersionToPath PathToDotNetFramework, TargetDotNetFrameworkVersion version) + { + if (PathToDotNetFramework == null) + { + return ToolLocationHelper.GetPathToDotNetFramework(version); + } + else + { + return PathToDotNetFramework(version); + } + } + + /// + /// Call either the static method or the delegate. This is done purely for performance as the delegate is only required for ease of unit testing and since + /// the methods being called are static this will be a non 0 cost to use delegates vs the static methods directly. + /// + internal static string VersionToDotNetReferenceAssemblies(VersionToPath PathToDotReferenceAssemblies, TargetDotNetFrameworkVersion version) + { + if (PathToDotReferenceAssemblies == null) + { + return ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(version); + } + else + { + return PathToDotReferenceAssemblies(version); + } + } + + /// + /// Generate the list of reference assembly paths for well known .net framework versions + /// + /// Target framework moniker class wich contains the targetframeworkVersion + /// A collection of strings which list the chained reference assembly paths with the highest version being first + internal static IList HandleLegacyDotNetFrameworkReferenceAssemblyPaths(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies, FrameworkNameVersioning frameworkName) + { + if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion20) + { + return HandleLegacy20(PathToDotNetFramework); + } + else if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion30) + { + return HandleLegacy30(PathToDotNetFramework, PathToReferenceAssemblies); + } + else if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion35) + { + return HandleLegacy35(PathToDotNetFramework, PathToReferenceAssemblies); + } + + // Dont know the framework send back an empty list because it does not exist + return new List(); + } + + /// + /// Returns the path to the "bin" directory of the .NET Framework SDK. + /// + /// Version of the targeted .NET Framework + /// Version of Visual Studio the requested SDK is associated with + /// Path string. + internal static string GetPathToDotNetFrameworkSdkToolsFolderRoot(TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + var dotNetFrameworkVersion = TargetDotNetFrameworkVersionToSystemVersion(version); + var vsVersion = VisualStudioVersionToSystemVersion(visualStudioVersion); + return FrameworkLocationHelper.GetPathToDotNetFrameworkSdkTools(dotNetFrameworkVersion, vsVersion); + } + + private static Version TargetDotNetFrameworkVersionToSystemVersion(TargetDotNetFrameworkVersion version) + { + switch (version) + { + case TargetDotNetFrameworkVersion.Version11: + return FrameworkLocationHelper.dotNetFrameworkVersion11; + + case TargetDotNetFrameworkVersion.Version20: + return FrameworkLocationHelper.dotNetFrameworkVersion20; + + case TargetDotNetFrameworkVersion.Version30: + return FrameworkLocationHelper.dotNetFrameworkVersion30; + + case TargetDotNetFrameworkVersion.Version35: + return FrameworkLocationHelper.dotNetFrameworkVersion35; + + case TargetDotNetFrameworkVersion.Version40: + return FrameworkLocationHelper.dotNetFrameworkVersion40; + + case TargetDotNetFrameworkVersion.Version45: + return FrameworkLocationHelper.dotNetFrameworkVersion45; + + case TargetDotNetFrameworkVersion.Version451: + return FrameworkLocationHelper.dotNetFrameworkVersion451; + + case TargetDotNetFrameworkVersion.Version46: + return FrameworkLocationHelper.dotNetFrameworkVersion46; + + default: + ErrorUtilities.ThrowArgument("ToolLocationHelper.UnsupportedFrameworkVersion", version); + return null; + } + } + + private static Version VisualStudioVersionToSystemVersion(VisualStudioVersion version) + { + switch (version) + { + case VisualStudioVersion.Version100: + return FrameworkLocationHelper.visualStudioVersion100; + + case VisualStudioVersion.Version110: + return FrameworkLocationHelper.visualStudioVersion110; + + case VisualStudioVersion.Version120: + return FrameworkLocationHelper.visualStudioVersion120; + + case VisualStudioVersion.Version140: + return FrameworkLocationHelper.visualStudioVersion140; + + default: + ErrorUtilities.ThrowArgument("ToolLocationHelper.UnsupportedVisualStudioVersion", version); + return null; + } + } + + /// + /// Generate the key which will be used for the reference assembly cache so that multiple static methods will generate it in the same way. + /// + private static string GenerateReferenceAssemblyCacheKey(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName) + { + return targetFrameworkRootPath + "|" + frameworkName.FullName; + } + + /// + /// Create the shared cache if it is not null + /// + private static void CreateReferenceAssemblyPathsCache() + { + lock (s_locker) + { + if (s_cachedReferenceAssemblyPaths == null) + { + s_cachedReferenceAssemblyPaths = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + } + } + + /// + /// Do the correct chaining of .net 3.5, 3.0 and 2.0. Throw an exception if any of the chain is missing + /// + private static IList HandleLegacy35(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies) + { + List referencePaths = new List(); + string referenceAssemblyPath = VersionToDotNetReferenceAssemblies(PathToReferenceAssemblies, TargetDotNetFrameworkVersion.Version35); + string dotNetFrameworkPath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version35); + + if (referenceAssemblyPath != null && dotNetFrameworkPath != null) + { + referencePaths.Add(referenceAssemblyPath); + referencePaths.Add(dotNetFrameworkPath); + } + else + { + return referencePaths; + } + + // This method will return either an empty list or a list with elements in the order 3.0 2.0 + IList referenceAssembly30Paths = HandleLegacy30(PathToDotNetFramework, PathToReferenceAssemblies); + + referencePaths.AddRange(referenceAssembly30Paths); + return referencePaths; + } + + /// + /// Do the correct chaining of .net 3.5, 3.0 and 2.0. Throw an exception if any of the chain is missing + /// + private static IList HandleLegacy30(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies) + { + List referencePaths = new List(); + string referenceAssemblyPath = VersionToDotNetReferenceAssemblies(PathToReferenceAssemblies, TargetDotNetFrameworkVersion.Version30); + string dotNetFrameworkPath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version30); + + if (referenceAssemblyPath != null && dotNetFrameworkPath != null) + { + referencePaths.Add(referenceAssemblyPath); + referencePaths.Add(dotNetFrameworkPath); + } + else + { + return referencePaths; + } + + IList referenceAssembly20Paths = HandleLegacy20(PathToDotNetFramework); + referencePaths.AddRange(referenceAssembly20Paths); + return referencePaths; + } + + /// + /// Check to see if .net 2.0 is installed + /// + private static IList HandleLegacy20(VersionToPath PathToDotNetFramework) + { + List referencePaths = new List(); + string referencePath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version20); + + if (referencePath != null) + { + referencePaths.Add(referencePath); + } + + return referencePaths; + } + + + /// + /// Returns the paths to the reference assemblies location for the given framework version relative to a given targetFrameworkRoot. + /// The method will not check to see if the path exists or not. + /// + /// Root directory which will be used to calculate the reference assembly path. The references assembies will be + /// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType. + /// + /// A frameworkName class which represents a TargetFrameworkMoniker. This cannot be null. + /// Collection of reference assembly locations. + public static IList GetPathToReferenceAssemblies(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName) + { + // Verify the root path is not null throw an ArgumentNullException if the given string parameter is null and ArgumentException if it has zero length. + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkRootPath, "targetFrameworkRootPath"); + //Verify the framework class passed in is not null. Other than being null the class will ensure it is consistent and the internal state is correct + ErrorUtilities.VerifyThrowArgumentNull(frameworkName, "frameworkName"); + + string referenceAssemblyCacheKey = GenerateReferenceAssemblyCacheKey(targetFrameworkRootPath, frameworkName); + CreateReferenceAssemblyPathsCache(); + + lock (s_locker) + { + IList referenceAssemblies; + if (s_cachedReferenceAssemblyPaths.TryGetValue(referenceAssemblyCacheKey, out referenceAssemblies)) + { + return referenceAssemblies; + } + } + + // Try and find the reference assemblies using the reference assembly path generation algorithm + IList dotNetFrameworkReferenceAssemblies = GetPathAndChainReferenceAssemblyLocations(targetFrameworkRootPath, frameworkName, true); + + // We have not found any reference assembly locations, if we are the .net framework we can try and fallback to the old legacy tool location helper methods + if (String.Equals(frameworkName.Identifier, ".NETFramework", StringComparison.OrdinalIgnoreCase) && dotNetFrameworkReferenceAssemblies.Count == 0) + { + if (String.IsNullOrEmpty(frameworkName.Profile)) // profiles are always in new locations + { + // If the identifier is ".NET Framework" and the version is a well know legacy version. Manually generate the list of reference assembly paths + // based on the known chaining order. Pass null in for the two delegates so we call the static methods rather than require the creation and calling + // of two delegates + dotNetFrameworkReferenceAssemblies = HandleLegacyDotNetFrameworkReferenceAssemblyPaths(null, null, frameworkName); + } + } + + lock (s_locker) + { + s_cachedReferenceAssemblyPaths[referenceAssemblyCacheKey] = dotNetFrameworkReferenceAssemblies; + } + + for (int i = 0; i < dotNetFrameworkReferenceAssemblies.Count; i++) + { + if (!dotNetFrameworkReferenceAssemblies[i].EndsWith("\\", StringComparison.Ordinal)) + { + dotNetFrameworkReferenceAssemblies[i] = String.Concat(dotNetFrameworkReferenceAssemblies[i], "\\"); + } + } + + return dotNetFrameworkReferenceAssemblies; + } + + /// + /// Figures out a display name given the target framework details. + /// This is the equivalent of the target framework moniker, but for display. + /// If one cannot be found from the redist list file, a synthesized one is returned, so there is always a display name. + /// + public static string GetDisplayNameForTargetFrameworkDirectory(string targetFrameworkDirectory, FrameworkNameVersioning frameworkName) + { + string displayName; + lock (s_locker) + { + if (s_cachedTargetFrameworkDisplayNames != null && s_cachedTargetFrameworkDisplayNames.TryGetValue(targetFrameworkDirectory, out displayName)) + { + return displayName; + } + } + + // Not in the cache, try to find it and if so cache it + ChainReferenceAssemblyPath(targetFrameworkDirectory); + + lock (s_locker) + { + if (s_cachedTargetFrameworkDisplayNames.TryGetValue(targetFrameworkDirectory, out displayName)) + { + return displayName; + } + } + + // Still don't have one. + // Probably it's 3.5 or earlier: make something reasonable. + // VS uses the same algorithm to find something to display + StringBuilder displayNameBuilder = new StringBuilder(); + + displayNameBuilder.Append(frameworkName.Identifier); + displayNameBuilder.Append(" "); + displayNameBuilder.Append("v" + frameworkName.Version.ToString()); + + if (!String.IsNullOrEmpty(frameworkName.Profile)) + { + displayNameBuilder.Append(" "); + displayNameBuilder.Append(frameworkName.Profile); + } + + displayName = displayNameBuilder.ToString(); + + // Cache it + lock (s_locker) + { + s_cachedTargetFrameworkDisplayNames[targetFrameworkDirectory] = displayName; + } + + return displayName; + } + + /// + /// Returns the paths to the reference assemblies location for the given framework version and properly chains the reference assemblies if required. + /// + /// Moniker being targeted + /// Collection of reference assembly locations. + internal static IList GetPathAndChainReferenceAssemblyLocations(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName, bool chain) + { + List referencePaths = new List(); + + string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName); + if (Directory.Exists(path)) + { + referencePaths.Add(path); + + if (chain) + { + while (!String.IsNullOrEmpty(path)) + { + // Will return String.Empty when there are no longer any paths to chain to + // We will return null if the chain is invalid and we need to return an empty chain. + path = ChainReferenceAssemblyPath(path); + + if (!String.IsNullOrEmpty(path)) + { + if (referencePaths.Contains(path)) + { + break; + } + referencePaths.Add(path); + } + else if (path == null) + { + // We have an invalid chain, we need to clear out any reference paths we have already added. + referencePaths.Clear(); + break; + } + } + } + } + + return referencePaths; + } + + /// + /// Clear out the appdomain wide cache of Platform and Extension SDKs. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public static void ClearSDKStaticCache() + { + lock (s_locker) + { + if (s_cachedTargetPlatforms != null) + { + s_cachedTargetPlatforms.Clear(); + } + + if (s_cachedTargetPlatformReferences != null) + { + s_cachedTargetPlatformReferences.Clear(); + } + + if (s_cachedExtensionSdks != null) + { + s_cachedExtensionSdks.Clear(); + } + + if (s_cachedExtensionSdkReferences != null) + { + s_cachedExtensionSdkReferences.Clear(); + } + } + } + + /// + /// Clear our the appdomain wide caches + /// + internal static void ClearStaticCaches() + { + lock (s_locker) + { + if (s_chainedReferenceAssemblyPath != null) + { + s_chainedReferenceAssemblyPath.Clear(); + } + + if (s_cachedHighestFrameworkNameForTargetFrameworkIdentifier != null) + { + s_cachedHighestFrameworkNameForTargetFrameworkIdentifier.Clear(); + } + + if (s_targetFrameworkMonikers != null) + { + s_targetFrameworkMonikers.Clear(); + } + + if (s_cachedTargetFrameworkDisplayNames != null) + { + s_cachedTargetFrameworkDisplayNames.Clear(); + } + + if (s_cachedReferenceAssemblyPaths != null) + { + s_cachedReferenceAssemblyPaths.Clear(); + } + + if (s_cachedTargetPlatforms != null) + { + s_cachedTargetPlatforms.Clear(); + } + + if (s_cachedTargetPlatformReferences != null) + { + s_cachedTargetPlatformReferences.Clear(); + } + + if (s_cachedExtensionSdks != null) + { + s_cachedExtensionSdks.Clear(); + } + + if (s_cachedExtensionSdkReferences != null) + { + s_cachedExtensionSdkReferences.Clear(); + } + } + } + + /// + /// Remap some common architectures to a single one that will be in the SDK. + /// + private static string RemapSdkArchitecture(string targetArchitecture) + { + if (targetArchitecture.Equals("msil", StringComparison.OrdinalIgnoreCase) || + targetArchitecture.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) || + targetArchitecture.Equals("Any CPU", StringComparison.OrdinalIgnoreCase)) + { + targetArchitecture = "Neutral"; + } + else if (targetArchitecture.Equals("Amd64", StringComparison.OrdinalIgnoreCase)) + { + targetArchitecture = "x64"; + } + return targetArchitecture; + } + + /// + /// Add the reference folder to the list of reference directories if it exists. + /// + private static void AddSDKPath(string sdkRoot, string contentFolderName, string targetConfiguration, string targetArchitecture, List contentDirectories) + { + string referenceAssemblyPath = Path.Combine(sdkRoot, contentFolderName, targetConfiguration, targetArchitecture); + + if (FileUtilities.DirectoryExistsNoThrow(referenceAssemblyPath)) + { + referenceAssemblyPath = FileUtilities.EnsureTrailingSlash(referenceAssemblyPath); + contentDirectories.Add(referenceAssemblyPath); + } + } + + /// + /// Get the list of extension sdks for a given platform and version + /// + internal static IEnumerable RetrieveTargetPlatformList(string[] diskRoots, string[] extensionDiskRoots, string registrySearchLocation) + { + // Get the disk and registry roots to search for sdks under + List sdkDiskRoots = GetTargetPlatformMonikerDiskRoots(diskRoots); + List extensionSdkDiskRoots = GetExtensionSdkDiskRoots(extensionDiskRoots); + + string registryRoot = GetTargetPlatformMonikerRegistryRoots(registrySearchLocation); + + string cachedTargetPlatformsKey = String.Join("|", + String.Join(";", sdkDiskRoots.ToArray()), + registryRoot); + + string cachedExtensionSdksKey = extensionDiskRoots == null ? String.Empty : String.Join(";", extensionDiskRoots.ToArray()); + + lock (s_locker) + { + if (s_cachedTargetPlatforms == null) + { + s_cachedTargetPlatforms = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + if (s_cachedExtensionSdks == null) + { + s_cachedExtensionSdks = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + IEnumerable collection = null; + if (!s_cachedTargetPlatforms.TryGetValue(cachedTargetPlatformsKey, out collection)) + { + Dictionary monikers = new Dictionary(); + GatherSDKListFromDirectory(sdkDiskRoots, monikers); + GatherSDKListFromRegistry(registryRoot, monikers); + + collection = monikers.Keys.ToList(); + s_cachedTargetPlatforms.Add(cachedTargetPlatformsKey, collection); + } + + TargetPlatformSDK extensionSdk = null; + if (!String.IsNullOrEmpty(cachedExtensionSdksKey)) + { + if (!s_cachedExtensionSdks.TryGetValue(cachedExtensionSdksKey, out extensionSdk)) + { + // These extension SDKs can target multiple platforms under the same Target SDK, stash in a null platform key for later filtering + extensionSdk = new TargetPlatformSDK(String.Empty, new Version(), null); + + GatherExtensionSDKListFromDirectory(extensionSdkDiskRoots, extensionSdk); + s_cachedExtensionSdks.Add(cachedExtensionSdksKey, extensionSdk); + } + collection = collection.Concat(new TargetPlatformSDK[] { extensionSdk }); + } + + return collection; + } + } + + /// + /// Gets new style extension SDKs (those that are under the target SDK name and version and are driven by manifest, not directory structure). + /// + internal static void GatherExtensionSDKListFromDirectory(IEnumerable diskRoots, TargetPlatformSDK extensionSdk) + { + // In this case we're passing in roots with the SDK and Version, such as C:\Program Files (x86)\Windows SDKs\1.0 + foreach (string diskRoot in diskRoots) + { + DirectoryInfo rootInfo = new DirectoryInfo(diskRoot); + if (!rootInfo.Exists) + { + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKListFromDirectory", "DiskRoot '{0}'does not exist, skipping it", diskRoot); + continue; + } + + DirectoryInfo extensionSdksDirectory = rootInfo.GetDirectories("Extension SDKs", SearchOption.TopDirectoryOnly).FirstOrDefault(); + if (extensionSdksDirectory != null) + { + GatherExtensionSDKs(extensionSdksDirectory, extensionSdk); + } + } + } + + internal static void GatherExtensionSDKs(DirectoryInfo extensionSdksDirectory, TargetPlatformSDK targetPlatformSDK) + { + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "Found ExtensionsSDK folder '{0}'. ", extensionSdksDirectory.FullName); + + DirectoryInfo[] sdkNameDirectories = extensionSdksDirectory.GetDirectories(); + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "Found '{0}' sdkName directories under '{1}'", sdkNameDirectories.Length, extensionSdksDirectory.FullName); + + // For each SDKName under the ExtensionSDKs directory + foreach (DirectoryInfo sdkNameFolders in sdkNameDirectories) + { + DirectoryInfo[] sdkVersionDirectories = sdkNameFolders.GetDirectories(); + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "Found '{0}' sdkVersion directories under '{1}'", sdkVersionDirectories.Length, sdkNameFolders.FullName); + + // For each Version directory under the SDK Name + foreach (DirectoryInfo sdkVersionDirectory in sdkVersionDirectories) + { + // Make sure the version folder parses to a version, anything that cannot parse directly to a version is to be ignored. + Version tempVersion; + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "Parsed sdk version folder '{0}' under '{1}'", sdkVersionDirectory.Name, sdkVersionDirectory.FullName); + if (Version.TryParse(sdkVersionDirectory.Name, out tempVersion)) + { + // Create SDK name based on the folder structure. We could open the manifest here and read the display name, but that would + // add complexity and since things are supposed to be in a certain structure I don't think that is needed at this point. + string SDKKey = TargetPlatformSDK.GetSdkKey(sdkNameFolders.Name, sdkVersionDirectory.Name); + + // Make sure we have not added the SDK to the list of found SDKs before. + if (!targetPlatformSDK.ExtensionSDKs.ContainsKey(SDKKey)) + { + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "SDKKey '{0}' was not already found.", SDKKey); + string pathToSDKManifest = Path.Combine(sdkVersionDirectory.FullName, "sdkManifest.xml"); + if (FileUtilities.FileExistsNoThrow(pathToSDKManifest)) + { + targetPlatformSDK.ExtensionSDKs.Add(SDKKey, FileUtilities.EnsureTrailingSlash(sdkVersionDirectory.FullName)); + } + else + { + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "No sdkManifest.xml files could be found at '{0}'. Not adding sdk", pathToSDKManifest); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "SDKKey '{0}' was already found, not adding sdk under '{1}'", SDKKey, sdkVersionDirectory.FullName); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherExtensionSDKs", "Failed to parse sdk version folder '{0}' under '{1}'", sdkVersionDirectory.Name, sdkVersionDirectory.FullName); + } + } + } + } + + /// + /// Given a root disk location and the target platform properties find all of the SDKs installed in that location. + /// + internal static void GatherSDKListFromDirectory(List diskroots, Dictionary platformSDKs) + { + foreach (string diskRoot in diskroots) + { + DirectoryInfo rootInfo = new DirectoryInfo(diskRoot); + if (!rootInfo.Exists) + { + ErrorUtilities.DebugTraceMessage("GatherSDKListFromDirectory", "DiskRoot '{0}'does not exist, skipping it", diskRoot); + continue; + } + + foreach (DirectoryInfo rootPathWithIdentifier in rootInfo.GetDirectories()) + { + // This makes a list of directories under the target framework identifier. + // This should make something like c:\Program files\Microsoft SDKs\Windows + + if (!rootPathWithIdentifier.Exists) + { + ErrorUtilities.DebugTraceMessage("GatherSDKListFromDirectory", "Disk root with Identifier: '{0}' does not exist. ", rootPathWithIdentifier); + continue; + } + + ErrorUtilities.DebugTraceMessage("GatherSDKListFromDirectory", "Disk root with Identifier: '{0}' does exist. Enumerating version folders under it. ", rootPathWithIdentifier); + + // Get a list of subdirectories under the root path and identifier, Ie. c:\Program files\Microsoft SDKs\Windows we should see things like, V8.0, 8.0, 9.0 ect. + // Only grab the folders that have a version number (they can start with a v or not). + + SortedDictionary> versionsInRoot = VersionUtilities.GatherVersionStrings(null, rootPathWithIdentifier.GetDirectories().Select(directory => directory.Name)); + + ErrorUtilities.DebugTraceMessage("GatherSDKListFromDirectory", "Found '{0}' version folders under the identifier path '{1}'. ", versionsInRoot.Count, rootPathWithIdentifier); + + // Go through each of the targetplatform versions under the targetplatform identifier. + foreach (KeyValuePair> directoryUnderRoot in versionsInRoot) + { + TargetPlatformSDK platformSDKKey = new TargetPlatformSDK(rootPathWithIdentifier.Name, directoryUnderRoot.Key, null); + TargetPlatformSDK targetPlatformSDK = null; + + // DirectoryUnderRoot.Value will be a list of the raw directory strings under the targetplatform identifier directory that map to the versions specified in directoryUnderRoot.Key. + foreach (string version in directoryUnderRoot.Value) + { + // This should make something like c:\Program files\Microsoft SDKs\Windows\v8.0\ + string platformSDKDirectory = Path.Combine(rootPathWithIdentifier.FullName, version); + string platformSDKManifest = Path.Combine(platformSDKDirectory, "sdkmanifest.xml"); + + // If we are gathering the sdk platform manifests then check to see if there is a sdk manifest in the directory if not then skip over it as a platform sdk + bool platformSDKManifestExists = File.Exists(platformSDKManifest); + if (targetPlatformSDK == null && !platformSDKs.TryGetValue(platformSDKKey, out targetPlatformSDK)) + { + targetPlatformSDK = new TargetPlatformSDK(platformSDKKey.TargetPlatformIdentifier, platformSDKKey.TargetPlatformVersion, platformSDKManifestExists ? platformSDKDirectory : null); + platformSDKs.Add(targetPlatformSDK, targetPlatformSDK); + } + + if (targetPlatformSDK.Path == null && platformSDKManifestExists) + { + targetPlatformSDK.Path = platformSDKDirectory; + } + + // Gather the set of platforms supported by this SDK if it's a valid one. + if (!String.IsNullOrEmpty(targetPlatformSDK.Path)) + { + GatherPlatformsForSdk(targetPlatformSDK); + } + + // If we are passed an extension sdk dictionary we will continue to look through the extension sdk directories and try and fill it up. + // This should make something like c:\Program files\Microsoft SDKs\Windows\v8.0\ExtensionSDKs + string sdkFolderPath = Path.Combine(platformSDKDirectory, "ExtensionSDKs"); + DirectoryInfo extensionSdksDirectory = new DirectoryInfo(sdkFolderPath); + + if (extensionSdksDirectory.Exists) + { + GatherExtensionSDKs(extensionSdksDirectory, targetPlatformSDK); + } + else + { + ErrorUtilities.DebugTraceMessage("GatherSDKListFromDirectory", "Could not find ExtensionsSDK folder '{0}'. ", sdkFolderPath); + } + } + } + } + } + } + + /// + /// Given a registy location enumerate the registry and find the installed SDKs. + /// + internal static void GatherSDKsFromRegistryImpl(Dictionary platformMonikers, string registryKeyRoot, RegistryView registryView, RegistryHive registryHive, GetRegistrySubKeyNames getRegistrySubKeyNames, GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, OpenBaseKey openBaseKey, FileExists fileExists) + { + ErrorUtilities.VerifyThrowArgumentNull(platformMonikers, "PlatformMonikers"); + if (String.IsNullOrEmpty(registryKeyRoot)) + { + return; + } + + // Open the hive for a given view + using (RegistryKey baseKey = openBaseKey(registryHive, registryView)) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Gathering SDKS from registryRoot '{0}', Hive '{1}', View '{2}'", registryKeyRoot, registryHive, registryView); + + // Attach the target platform to the registry root. This should give us something like + // SOFTWARE\MICROSOFT\Microsoft SDKs\Windows + + // Get all of the platform identifiers + IEnumerable platformIdentifiers = getRegistrySubKeyNames(baseKey, registryKeyRoot); + + // No identifiers found. + if (platformIdentifiers == null) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "No sub keys found under registryKeyRoot {0}", registryKeyRoot); + return; + } + + foreach (string platformIdentifier in platformIdentifiers) + { + string platformIdentifierKey = registryKeyRoot + @"\" + platformIdentifier; + + // Get all of the version folders under the targetplatform identifier key + IEnumerable versions = getRegistrySubKeyNames(baseKey, platformIdentifierKey); + + // No versions found. + if (versions == null) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "No sub keys found under platformIdentifierKey {0}", platformIdentifierKey); + return; + } + + // Returns a a sorted set of versions and their associated registry strings. The reason we need the original strings is that + // they may contain a v where as a version does not support a v. + SortedDictionary> sortedVersions = VersionUtilities.GatherVersionStrings(null, versions); + + foreach (KeyValuePair> registryVersions in sortedVersions) + { + TargetPlatformSDK platformSDKKey = new TargetPlatformSDK(platformIdentifier, registryVersions.Key, null); + TargetPlatformSDK targetPlatformSDK = null; + + // Go through each of the raw version strings which were found in the registry + foreach (string version in registryVersions.Value) + { + // Attach the version and extensionSDKs strings to the platformIdentifier key we built up above. + // Make something like SOFTWARE\MICROSOFT\Microsoft SDKs\Windows\8.0\ + string platformSDKsRegistryKey = platformIdentifierKey + @"\" + version; + + string platformSDKDirectory = getRegistrySubKeyDefaultValue(baseKey, platformSDKsRegistryKey); + + // May be null because some use installationfolder instead + if (platformSDKDirectory == null) + { + using (RegistryKey versionKey = baseKey.OpenSubKey(platformSDKsRegistryKey)) + { + if (versionKey != null) + { + platformSDKDirectory = versionKey.GetValue("InstallationFolder") as string; + } + } + } + + bool platformSDKmanifestExists = false; + + if (platformSDKDirectory != null) + { + string platformSDKManifest = Path.Combine(platformSDKDirectory, "sdkmanifest.xml"); + // Windows kits is special because they do not have an sdk manifest yet, this is for the windows sdk. We will accept them as they are. For others + // we will require that an sdkmanifest exists. + platformSDKmanifestExists = fileExists(platformSDKManifest) || platformSDKDirectory.IndexOf("Windows Kits", StringComparison.OrdinalIgnoreCase) >= 0; + } + + if (targetPlatformSDK == null && !platformMonikers.TryGetValue(platformSDKKey, out targetPlatformSDK)) + { + targetPlatformSDK = new TargetPlatformSDK(platformSDKKey.TargetPlatformIdentifier, platformSDKKey.TargetPlatformVersion, platformSDKmanifestExists ? platformSDKDirectory : null); + platformMonikers.Add(targetPlatformSDK, targetPlatformSDK); + } + + if (targetPlatformSDK.Path == null && platformSDKmanifestExists) + { + targetPlatformSDK.Path = platformSDKDirectory; + } + + // Gather the set of platforms supported by this SDK if it's a valid one. + if (!String.IsNullOrEmpty(targetPlatformSDK.Path)) + { + GatherPlatformsForSdk(targetPlatformSDK); + } + + // Make something like SOFTWARE\MICROSOFT\Microsoft SDKs\Windows\8.0\ExtensionSdks + string extensionSDKsKey = platformSDKsRegistryKey + @"\ExtensionSDKs"; + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Getting subkeys of '{0}'", extensionSDKsKey); + + // Get all of the SDK name folders under the ExtensionSDKs registry key + IEnumerable sdkNames = getRegistrySubKeyNames(baseKey, extensionSDKsKey); + if (sdkNames == null) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Could not find subkeys of '{0}'", extensionSDKsKey); + continue; + } + + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Found subkeys of '{0}'", extensionSDKsKey); + + // For each SDK folder under ExtensionSDKs + foreach (string sdkName in sdkNames) + { + // Combine the SDK Name with the ExtensionSDKs key we have built up above. + // Make something like SOFTWARE\MICROSOFT\Windows SDKs\Windows\8.0\ExtensionSDKs\XNA + string sdkNameKey = extensionSDKsKey + @"\" + sdkName; + + //Get all of the version registry keys under the SDK Name Key. + IEnumerable sdkVersions = getRegistrySubKeyNames(baseKey, sdkNameKey); + + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Getting subkeys of '{0}'", sdkNameKey); + if (sdkVersions == null) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Could not find subkeys of '{0}'", sdkNameKey); + continue; + } + + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Found subkeys of '{0}'", sdkNameKey); + + // For each version registry entry under the SDK Name registry key + foreach (string sdkVersion in sdkVersions) + { + // We only want registry keys which parse directly to versions + Version tempVersion; + if (Version.TryParse(sdkVersion, out tempVersion)) + { + string sdkDirectoryKey = sdkNameKey + @"\" + sdkVersion; + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Getting default key for '{0}'", sdkDirectoryKey); + + // Now that we found the registry key we need to get its default value which points to the directory this SDK is in. + string directoryName = getRegistrySubKeyDefaultValue(baseKey, sdkDirectoryKey); + string sdkKey = TargetPlatformSDK.GetSdkKey(sdkName, sdkVersion); + if (directoryName != null) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "SDK installation location = '{0}'", directoryName); + + // Make sure the directory exists and that it has not been added before. + if (!targetPlatformSDK.ExtensionSDKs.ContainsKey(sdkKey)) + { + if (FileUtilities.DirectoryExistsNoThrow(directoryName)) + { + string sdkManifestFileLocation = Path.Combine(directoryName, "sdkManifest.xml"); + if (fileExists(sdkManifestFileLocation)) + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Adding SDK '{0}' at '{1}' to the list of found sdks.", sdkKey, directoryName); + targetPlatformSDK.ExtensionSDKs.Add(sdkKey, FileUtilities.EnsureTrailingSlash(directoryName)); + } + else + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "No sdkManifest.xml file found at '{0}'.", sdkManifestFileLocation); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "SDK directory '{0}' does not exist", directoryName); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "SDK key was previously added. '{0}'", sdkKey); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherSDKsFromRegistryImpl", "Default key is null for '{0}'", sdkDirectoryKey); + } + } + } + } + } + } + } + } + } + + /// + /// Gather the list of SDKs installed on the machine from the registry. + /// Do not parallelize the getting of these entries, order is important, we want the first ones in to win. + /// + private static void GatherSDKListFromRegistry(string registryRoot, Dictionary platformMonikers) + { + // Setup some delegates because the methods we call use them during unit testing. + GetRegistrySubKeyNames getSubkeyNames = new GetRegistrySubKeyNames(RegistryHelper.GetSubKeyNames); + GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue = new GetRegistrySubKeyDefaultValue(RegistryHelper.GetDefaultValue); + OpenBaseKey openBaseKey = new OpenBaseKey(RegistryHelper.OpenBaseKey); + FileExists fileExists = new FileExists(File.Exists); + + bool is64bitOS = Environment.Is64BitOperatingSystem; + + // Under WOW64 the HKEY_CURRENT_USER\SOFTWARE key is shared. This means the values are the same in the 64 bit and 32 bit views. This means we only need to get one view of this key. + GatherSDKsFromRegistryImpl(platformMonikers, registryRoot, RegistryView.Default, RegistryHive.CurrentUser, getSubkeyNames, getRegistrySubKeyDefaultValue, openBaseKey, fileExists); + + // Since SDKS can contain multiple architecture it makes sense to register both 32 bit and 64 bit in one location, but if for some reason that + // is not possible then we need to look at both hives. Choosing the 32 bit one first because is where we expect to find them usually. + if (is64bitOS) + { + GatherSDKsFromRegistryImpl(platformMonikers, registryRoot, RegistryView.Registry32, RegistryHive.LocalMachine, getSubkeyNames, getRegistrySubKeyDefaultValue, openBaseKey, fileExists); + GatherSDKsFromRegistryImpl(platformMonikers, registryRoot, RegistryView.Registry64, RegistryHive.LocalMachine, getSubkeyNames, getRegistrySubKeyDefaultValue, openBaseKey, fileExists); + } + else + { + GatherSDKsFromRegistryImpl(platformMonikers, registryRoot, RegistryView.Default, RegistryHive.LocalMachine, getSubkeyNames, getRegistrySubKeyDefaultValue, openBaseKey, fileExists); + } + } + + /// + /// Get the disk locations to search for sdks under. This can be overidden by an environment variable + /// + private static void GetDefaultSDKDiskRoots(List diskRoots) + { + // The order is important here becuase we want to look in the users location first before the non privileged location. + + // We need this so that a user can also have an sdk installed in a non privileged location + string userLocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (userLocalAppData.Length > 0) + { + string localAppdataFolder = Path.Combine(userLocalAppData, "Microsoft SDKs"); + if (Directory.Exists(localAppdataFolder)) + { + diskRoots.Add(localAppdataFolder); + } + } + + string defaultProgramFilesLocation = Path.Combine(FrameworkLocationHelper.programFiles32, "Microsoft SDKs"); + diskRoots.Add(defaultProgramFilesLocation); + } + + /// + /// Extract the disk roots from the environment + /// + private static void ExtractSdkDiskRootsFromEnvironment(List diskRoots, string directoryRoots) + { + if (!String.IsNullOrEmpty(directoryRoots)) + { + string[] splitRoots = directoryRoots.Split(s_diskRootSplitChars, StringSplitOptions.RemoveEmptyEntries); + ErrorUtilities.DebugTraceMessage("ExtractSdkDiskRootsFromEnvironment", "DiskRoots from Registry '{0}'", String.Join(";", splitRoots)); + diskRoots.AddRange(splitRoots); + } + + if (diskRoots != null) + { + diskRoots.ForEach(x => x = x.Trim()); + diskRoots.RemoveAll(x => !FileUtilities.DirectoryExistsNoThrow(x)); + } + } + + /// + /// Get the disk roots to search for both platform and extension sdks in. The environment variable can + /// override the defaults. + /// + /// + private static List GetTargetPlatformMonikerDiskRoots(string[] diskRoots) + { + List sdkDiskRoots = new List(); + string sdkDirectoryRootsFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY"); + ExtractSdkDiskRootsFromEnvironment(sdkDiskRoots, sdkDirectoryRootsFromEnvironment); + if (sdkDiskRoots.Count == 0) + { + if (diskRoots != null && diskRoots.Length > 0) + { + ErrorUtilities.DebugTraceMessage("GetTargetPlatformMonikerDiskRoots", "Passed in DiskRoots '{0}'", String.Join(";", diskRoots)); + sdkDiskRoots.AddRange(diskRoots); + } + else + { + ErrorUtilities.DebugTraceMessage("GetTargetPlatformMonikerDiskRoots", "Getting default disk roots"); + GetDefaultSDKDiskRoots(sdkDiskRoots); + } + } + + ErrorUtilities.DebugTraceMessage("GetTargetPlatformMonikerDiskRoots", "Diskroots being used '{0}'", String.Join(";", sdkDiskRoots.ToArray())); + return sdkDiskRoots; + } + + /// + /// Get the disk roots to search for multi platform extension sdks in. The environment variable can + /// override the defaults. + /// + private static List GetExtensionSdkDiskRoots(string[] diskRoots) + { + List sdkDiskRoots = new List(); + string sdkDirectoryRootsFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDMULTIPLATFORMSDKREFERENCEDIRECTORY"); + ExtractSdkDiskRootsFromEnvironment(sdkDiskRoots, sdkDirectoryRootsFromEnvironment); + if (sdkDiskRoots.Count == 0 && diskRoots != null && diskRoots.Length > 0) + { + ErrorUtilities.DebugTraceMessage("GetMultiPlatformSdkDiskRoots", "Passed in DiskRoots '{0}'", String.Join(";", diskRoots)); + sdkDiskRoots.AddRange(diskRoots); + } + + ErrorUtilities.DebugTraceMessage("GetMultiPlatformSdkDiskRoots", "Diskroots being used '{0}'", String.Join(";", sdkDiskRoots.ToArray())); + return sdkDiskRoots; + } + + /// + /// Get the registry root to find sdks under. The registry can be disabled if we are in a checked in sceario + /// + /// + private static string GetTargetPlatformMonikerRegistryRoots(string registryRootLocation) + { + ErrorUtilities.DebugTraceMessage("GetTargetPlatformMonikerRegistryRoots", "RegistryRoot passed in '{0}'", registryRootLocation != null ? registryRootLocation : String.Empty); + + string disableRegistryForSDKLookup = Environment.GetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP"); + // If we are not disabling the registry for platform sdk lookups then lets look in the default location. + string registryRoot = String.Empty; + if (disableRegistryForSDKLookup == null) + { + if (!String.IsNullOrEmpty(registryRootLocation)) + { + registryRoot = registryRootLocation; + } + else + { + registryRoot = @"SOFTWARE\MICROSOFT\Microsoft SDKs\"; + } + + ErrorUtilities.DebugTraceMessage("GetTargetPlatformMonikerRegistryRoots", "RegistryRoot to be looked under '{0}'", registryRoot); + } + else + { + ErrorUtilities.DebugTraceMessage("GetTargetPlatformMonikerRegistryRoots", "MSBUILDDISABLEREGISTRYFORSDKLOOKUP is set registry sdk lookup is disabled"); + } + + + return registryRoot; + } + + /// + /// Given a platform SDK object, populate its supported platforms. + /// + private static void GatherPlatformsForSdk(TargetPlatformSDK sdk) + { + ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(sdk.Path), "SDK path must be set"); + + try + { + string platformsRoot = Path.Combine(sdk.Path, platformsFolderName); + DirectoryInfo platformsRootInfo = new DirectoryInfo(platformsRoot); + + if (platformsRootInfo.Exists) + { + DirectoryInfo[] platformIdentifiers = platformsRootInfo.GetDirectories(); + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "Found '{0}' platform identifier directories under '{1}'", platformIdentifiers.Length, platformsRoot); + + // Iterate through all identifiers + foreach (DirectoryInfo platformIdentifier in platformIdentifiers) + { + DirectoryInfo[] platformVersions = platformIdentifier.GetDirectories(); + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "Found '{0}' platform version directories under '{1}'", platformVersions.Length, platformIdentifier.FullName); + + // and all versions under each of those identifiers + foreach (DirectoryInfo platformVersion in platformVersions) + { + // If this version directory is not actually a proper version format, ignore it. + Version tempVersion; + if (Version.TryParse(platformVersion.Name, out tempVersion)) + { + string sdkKey = TargetPlatformSDK.GetSdkKey(platformIdentifier.Name, platformVersion.Name); + + // make sure we haven't already seen this one somehow + if (!sdk.Platforms.ContainsKey(sdkKey)) + { + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "SDKKey '{0}' was not already found.", sdkKey); + + string pathToPlatformManifest = Path.Combine(platformVersion.FullName, "Platform.xml"); + if (FileUtilities.FileExistsNoThrow(pathToPlatformManifest)) + { + sdk.Platforms.Add(sdkKey, FileUtilities.EnsureTrailingSlash(platformVersion.FullName)); + } + else + { + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "No Platform.xml could be found at '{0}'. Not adding this platform", pathToPlatformManifest); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "SDKKey '{0}' was already found, not adding platform under '{1}'", sdkKey, platformVersion.FullName); + } + } + else + { + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "Failed to parse platform version folder '{0}' under '{1}'", platformVersion.Name, platformVersion.FullName); + } + } + } + } + } + catch (Exception e) + { + if (!ExceptionHandling.IsIoRelatedException(e)) + { + throw; + } + + ErrorUtilities.DebugTraceMessage("GatherPlatformsForSdk", "Encountered exception trying to gather platform-specific data: {0}", e.Message); + } + } + + /// + /// Take the path to a reference assembly directory which contains a RedistList folder which then contains a FrameworkList.xml file. + /// We will read in the xml file and determine if it has an IncludedFramework element in the redist list. If it does it will calculate + /// the path where the next link in the chain should be and return that path. + /// Also, when reading the redist list, if any display name is found it will be cached, keyed off the path passed in. + /// + /// Return null if we could not chain due to an error or the path not being found. return String.Empty if there is no next element in the chain + internal static string ChainReferenceAssemblyPath(string targetFrameworkDirectory) + { + string path = Path.GetFullPath(targetFrameworkDirectory); + + lock (s_locker) + { + // Cache the results of the chain search so that we do not have to do an expensive read more than once per process per redist list. + s_chainedReferenceAssemblyPath = s_chainedReferenceAssemblyPath ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + s_cachedTargetFrameworkDisplayNames = s_cachedTargetFrameworkDisplayNames ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + string cachedPath = null; + if (s_chainedReferenceAssemblyPath.TryGetValue(path, out cachedPath)) + { + return cachedPath; + } + } + + // Read in the redist list at the specified path, and return + // the display name and the "include framework" value for chaining. + // If display name is not available, returns empty string. + // If include framework is not available, returns null. + // Caches the display name keyed by the path. + + // Make sure we have a directory with a redist list folder and a FrameworkList.xml file in there as this is what we will use for chaining. + string redistListFolder = Path.Combine(path, "RedistList"); + string redistFile = Path.Combine(redistListFolder, "FrameworkList.xml"); + + // If the redist list does not exist then the entire chain is incorrect. + if (!File.Exists(redistFile)) + { + lock (s_locker) + { + s_chainedReferenceAssemblyPath[path] = null; + s_cachedTargetFrameworkDisplayNames[path] = null; + } + + return null; + } + + string includeFramework = null; + string displayName = null; + + try + { + // Read in the xml file looking for the includeFramework inorder to chain. + using (XmlTextReader reader = new XmlTextReader(redistFile)) + { + reader.DtdProcessing = DtdProcessing.Ignore; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + if (string.Equals(reader.Name, "FileList", StringComparison.OrdinalIgnoreCase)) + { + reader.MoveToFirstAttribute(); + do + { + if (String.Equals(reader.Name, "IncludeFramework", StringComparison.OrdinalIgnoreCase)) + { + includeFramework = reader.Value; + continue; + } + + if (String.Equals(reader.Name, "Name", StringComparison.OrdinalIgnoreCase)) + { + displayName = reader.Value; + continue; + } + } + while (reader.MoveToNextAttribute()); + reader.MoveToElement(); + break; + } + } + } + } + } + catch (XmlException ex) + { + ErrorUtilities.ThrowInvalidOperation("ToolsLocationHelper.InvalidRedistFile", redistFile, ex.Message); + } + catch (Exception ex) + { + // If there was a problem writing the file (like it's read-only or locked on disk, for + // example), then eat the exception and log a warning. Otherwise, rethrow. + if (ExceptionHandling.NotExpectedException(ex)) + throw; + + ErrorUtilities.ThrowInvalidOperation("ToolsLocationHelper.InvalidRedistFile", redistFile, ex.Message); + } + + // Cache the display name if we have one + if (displayName != null) + { + lock (s_locker) + { + s_cachedTargetFrameworkDisplayNames[path] = displayName; + } + } + + string pathToReturn = String.Empty; + + try + { + // The IncludeFramework element could not be found so our chain is done. + if (!String.IsNullOrEmpty(includeFramework)) + { + // Take the path which should point to something like c:\ProgramFiles\ReferenceAssemblies\Framework\.NETFramework\v4.1 + // We will take the path, to "up" a directory then append the name found in the redist. For example if the redist list had v4.0 + // the path which would be expected would be c:\ProgramFiles\ReferenceAssemblies\Framework\.NETFramework\v4.0 + pathToReturn = path; + pathToReturn = Directory.GetParent(pathToReturn).FullName; + pathToReturn = Path.Combine(pathToReturn, includeFramework); + pathToReturn = Path.GetFullPath(pathToReturn); + + // The directory which we are chaining to does not exist, return null indicating the chain is incorrect. + if (!Directory.Exists(pathToReturn)) + { + pathToReturn = null; + } + } + + lock (s_locker) + { + s_chainedReferenceAssemblyPath[path] = pathToReturn; + } + + return pathToReturn; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + throw; + + ErrorUtilities.ThrowInvalidOperation("ToolsLocationHelper.CouldNotCreateChain", path, pathToReturn, e.Message); + } + + return null; + } + + /// + /// Get a fully qualified path to a file in the .NET Framework SDK. Error if the .NET Framework SDK can't be found. + /// When targeting .NET 3.5 or above, looks in the locations associated with Visual Studio 2010. If you wish to + /// target the .NET Framework SDK that ships with Visual Studio Dev11 or later, please use the override that + /// specifies a VisualStudioVersion. + /// + /// File name to locate in the .NET Framework SDK directory + /// Version of the targeted .NET Framework + /// Path string. + public static string GetPathToDotNetFrameworkSdkFile(string fileName, TargetDotNetFrameworkVersion version) + { + return GetPathToDotNetFrameworkSdkFile(fileName, version, VisualStudioVersion.VersionLatest); + } + + /// + /// Get a fully qualified path to a file in the .NET Framework SDK. Error if the .NET Framework SDK can't be found. + /// + /// File name to locate in the .NET Framework SDK directory + /// Version of the targeted .NET Framework + /// Version of Visual Studio the requested SDK is associated with + /// Path string. + public static string GetPathToDotNetFrameworkSdkFile(string fileName, TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + return GetPathToDotNetFrameworkSdkFile + ( + fileName, + version, + visualStudioVersion, + UtilitiesDotNetFrameworkArchitecture.Current, + true /* If the file is not found for the current architecture, it's OK to follow fallback mechanisms. */ + ); + } + + /// + /// Get a fully qualified path to a file in the .NET Framework SDK. Error if the .NET Framework SDK can't be found. + /// + /// File name to locate in the .NET Framework SDK directory + /// Version of the targeted .NET Framework + /// The required architecture of the requested file. + /// Path string. + public static string GetPathToDotNetFrameworkSdkFile(string fileName, TargetDotNetFrameworkVersion version, UtilitiesDotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFrameworkSdkFile(fileName, version, VisualStudioVersion.VersionLatest, architecture); + } + + /// + /// Get a fully qualified path to a file in the .NET Framework SDK. Error if the .NET Framework SDK can't be found. + /// + /// File name to locate in the .NET Framework SDK directory + /// Version of the targeted .NET Framework + /// The required architecture of the requested file. + /// Path string. + public static string GetPathToDotNetFrameworkSdkFile(string fileName, TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion, UtilitiesDotNetFrameworkArchitecture architecture) + { + return GetPathToDotNetFrameworkSdkFile + ( + fileName, + version, + visualStudioVersion, + architecture, + false /* Do _not_ fall back -- if the user is specifically requesting a particular architecture, they want that architecture. */ + ); + } + + /// + /// Get a fully qualified path to a file in the .NET Framework SDK. Error if the .NET Framework SDK can't be found. + /// + /// File name to locate in the .NET Framework SDK directory + /// Version of the targeted .NET Framework + /// The required architecture of the requested file. + /// If true, will follow the fallback pattern -- from requested architecture, to + /// current architecture, to x86. Otherwise, if the requested architecture path doesn't exist, that's it -- no path + /// will be returned. + /// + internal static string GetPathToDotNetFrameworkSdkFile(string fileName, TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion, UtilitiesDotNetFrameworkArchitecture architecture, bool canFallBackIfNecessary) + { + string pathToSdk = ToolLocationHelper.GetPathToDotNetFrameworkSdkToolsFolderRoot(version, visualStudioVersion); + string filePath = null; + + if (pathToSdk != null) + { + string convertedArchitecture = ConvertDotNetFrameworkArchitectureToProcessorArchitecture(architecture); + + // first take a look at the requested architecture + filePath = GetPathToDotNetFrameworkSdkFile(fileName, pathToSdk, convertedArchitecture); + + if (filePath == null && canFallBackIfNecessary) + { + // Now look for a version of the tool which matches the bitness of this process if we haven't already + if (!String.Equals(ProcessorArchitecture.CurrentProcessArchitecture, convertedArchitecture, StringComparison.OrdinalIgnoreCase)) + { + filePath = GetPathToDotNetFrameworkSdkFile(fileName, pathToSdk, ProcessorArchitecture.CurrentProcessArchitecture); + } + + // If we couldn't find that and we're in a non-x86 process, then fall back to the x86 version + if (filePath == null && !String.Equals(ProcessorArchitecture.X86, ProcessorArchitecture.CurrentProcessArchitecture, StringComparison.OrdinalIgnoreCase)) + { + filePath = GetPathToDotNetFrameworkSdkFile(fileName, pathToSdk, ProcessorArchitecture.X86); + } + } + } + + return filePath; + } + + /// + /// Gets the path to a sdk exe based on the processor architecture and the provided bin directory path. + /// If the fileName cannot be found in the pathToSDK after the processor architecture has been taken into account a null is returned. + /// + internal static string GetPathToDotNetFrameworkSdkFile(string fileName, string pathToSdk, string processorArchitecture) + { + if (pathToSdk == null || fileName == null || processorArchitecture == null) + { + return null; + } + + switch (processorArchitecture) + { + case ProcessorArchitecture.AMD64: + pathToSdk = Path.Combine(pathToSdk, "x64"); + break; + case ProcessorArchitecture.IA64: + pathToSdk = Path.Combine(pathToSdk, "ia64"); + break; + case ProcessorArchitecture.X86: + case ProcessorArchitecture.ARM: + default: + break; + } + + string filePath = Path.Combine(pathToSdk, fileName); + + // Use FileInfo instead of File.Exists(...) because the latter fails silently (by design) if CAS + // doesn't grant access. We want the security exception if there is going to be one. + bool exists = new FileInfo(filePath).Exists; + if (!exists) + { + return null; + } + + return filePath; + } + + /// + /// Given a member of the DotNetFrameworkArchitecture enumeration, returns the equivalent ProcessorArchitecture string. + /// Internal for Testing Purposes Only + /// + /// + /// + internal static string ConvertDotNetFrameworkArchitectureToProcessorArchitecture(DotNetFrameworkArchitecture architecture) + { + switch (architecture) + { + case DotNetFrameworkArchitecture.Bitness32: + if (ProcessorArchitecture.CurrentProcessArchitecture == ProcessorArchitecture.ARM) + { + return ProcessorArchitecture.ARM; + } + return ProcessorArchitecture.X86; + case DotNetFrameworkArchitecture.Bitness64: + // We need to know which 64-bit architecture we're on. + NativeMethodsShared.SYSTEM_INFO systemInfo = new NativeMethodsShared.SYSTEM_INFO(); + NativeMethodsShared.GetNativeSystemInfo(ref systemInfo); + + switch (systemInfo.wProcessorArchitecture) + { + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_AMD64: + return ProcessorArchitecture.AMD64; + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_IA64: + return ProcessorArchitecture.IA64; + // Errr, OK, we're trying to get the 64-bit path on a 32-bit machine. + // That ... doesn't make sense. + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_INTEL: + return null; + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_ARM: + return null; + // unknown architecture? return null + default: + return null; + } + case DotNetFrameworkArchitecture.Current: + return ProcessorArchitecture.CurrentProcessArchitecture; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + /// + /// Returns the path to the Windows SDK for the desired .NET Framework and Visual Studio version. Note that + /// this is only supported for a targeted .NET Framework version of 4.5 and above. + /// + /// Target .NET Framework version + /// Version of Visual Studio associated with the SDK. + /// Path to the appropriate Windows SDK location + [Obsolete("Consider using GetPlatformSDKLocation instead")] + public static string GetPathToWindowsSdk(TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + return FrameworkLocationHelper.GetPathToWindowsSdk(TargetDotNetFrameworkVersionToSystemVersion(version)); + } + + /// + /// Returns the path to a file in the Windows SDK for the desired .NET Framework and Visual Studio version. Note that + /// this is only supported for a targeted .NET Framework version of 4.5 and above. + /// + /// The name of the file being requested. + /// Target .NET Framework version. + /// Version of Visual Studio associated with the SDK. + /// Path to the appropriate Windows SDK file + [Obsolete("Consider using GetPlatformSDKLocationFile instead")] + public static string GetPathToWindowsSdkFile(string fileName, TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion) + { + return GetPathToWindowsSdkFile + ( + fileName, + version, + visualStudioVersion, + UtilitiesDotNetFrameworkArchitecture.Current, + true /* If the file is not found for the current architecture, it's OK to follow fallback mechanisms. */ + ); + } + + /// + /// Returns the path to a file in the Windows SDK for the desired .NET Framework and Visual Studio version and the desired + /// architecture. Note that this is only supported for a targeted .NET Framework version of 4.5 and above. + /// + /// The name of the file being requested. + /// Target .NET Framework version. + /// Version of Visual Studio associated with the SDK. + /// Desired architecture of the resultant file. + /// Path to the appropriate Windows SDK file + [Obsolete("Consider using GetPlatformSDKLocationFile instead")] + public static string GetPathToWindowsSdkFile(string fileName, TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion, DotNetFrameworkArchitecture architecture) + { + return GetPathToWindowsSdkFile + ( + fileName, + version, + visualStudioVersion, + architecture, + false /* Do _not_ fall back -- if the user is specifically requesting a particular architecture, they want that architecture. */ + ); + } + + /// + /// Returns the path to a file in the Windows SDK for the desired .NET Framework and Visual Studio version and the desired + /// architecture. Note that this is only supported for a targeted .NET Framework version of 4.5 and above. + /// + /// The name of the file being requested. + /// Target .NET Framework version. + /// Version of Visual Studio associated with the SDK. + /// Desired architecture of the resultant file. + /// Path to the appropriate Windows SDK file + [Obsolete("Consider using GetPlatformSDKLocationFile instead")] + private static string GetPathToWindowsSdkFile(string fileName, TargetDotNetFrameworkVersion version, VisualStudioVersion visualStudioVersion, DotNetFrameworkArchitecture architecture, bool canFallBackIfNecessary) + { + string pathToSdk = ToolLocationHelper.GetPathToWindowsSdk(version, visualStudioVersion); + string filePath = null; + + if (pathToSdk != null) + { + pathToSdk = Path.Combine(pathToSdk, "bin"); + + string convertedArchitecture = ConvertDotNetFrameworkArchitectureToProcessorArchitecture(architecture); + + // first take a look at the requested architecture + filePath = GetPathToWindowsSdkFile(fileName, pathToSdk, convertedArchitecture); + + if (filePath == null && canFallBackIfNecessary) + { + // Now look for a version of the tool which matches the bitness of this process if we haven't already + if (!String.Equals(ProcessorArchitecture.CurrentProcessArchitecture, convertedArchitecture, StringComparison.OrdinalIgnoreCase)) + { + filePath = GetPathToWindowsSdkFile(fileName, pathToSdk, ProcessorArchitecture.CurrentProcessArchitecture); + } + + // If we couldn't find that and we're in a non-x86 process, then fall back to the x86 version + if (filePath == null && !String.Equals(ProcessorArchitecture.X86, ProcessorArchitecture.CurrentProcessArchitecture, StringComparison.OrdinalIgnoreCase)) + { + filePath = GetPathToWindowsSdkFile(fileName, pathToSdk, ProcessorArchitecture.X86); + } + } + } + + return filePath; + } + + /// + /// Gets the path to a sdk exe based on the processor architecture and the provided bin directory path. + /// If the fileName cannot be found in the pathToSDK after the processor architecture has been taken into account a null is returned. + /// + [Obsolete("Consider using GetPlatformSDKLocationFile instead")] + internal static string GetPathToWindowsSdkFile(string fileName, string pathToSdk, string processorArchitecture) + { + if (pathToSdk == null || fileName == null || processorArchitecture == null) + { + return null; + } + + switch (processorArchitecture) + { + case ProcessorArchitecture.X86: + pathToSdk = Path.Combine(pathToSdk, "x86"); + break; + case ProcessorArchitecture.AMD64: + pathToSdk = Path.Combine(pathToSdk, "x64"); + break; + case ProcessorArchitecture.IA64: + case ProcessorArchitecture.ARM: + default: + break; + } + + string filePath = Path.Combine(pathToSdk, fileName); + + // Use FileInfo instead of File.Exists(...) because the latter fails silently (by design) if CAS + // doesn't grant access. We want the security exception if there is going to be one. + bool exists = new FileInfo(filePath).Exists; + if (!exists) + { + return null; + } + + return filePath; + } + + /// + /// Given a ToolsVersion, return the path to the MSBuild tools for that ToolsVersion + /// + /// The ToolsVersion for which to get the tools path + /// The tools path folder of the appropriate ToolsVersion if it exists, otherwise null. + public static string GetPathToBuildTools(string toolsVersion) + { + string pathToFile = GetPathToBuildTools(toolsVersion, UtilitiesDotNetFrameworkArchitecture.Current); + return pathToFile; + } + + /// + /// Given a ToolsVersion, return the path to the MSBuild tools for that ToolsVersion + /// + /// The ToolsVersion for which to get the tools path + /// The architecture of the build tools location to get + /// The tools path folder of the appropriate ToolsVersion if it exists, otherwise null. + public static string GetPathToBuildTools(string toolsVersion, UtilitiesDotNetFrameworkArchitecture architecture) + { + switch (toolsVersion) + { + case "2.0": + return GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20, architecture); + case "3.5": + return GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35, architecture); + case "4.0": + return GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version40, architecture); + } + + // Doesn't map to an existing .NET Framework, so let's grab it out of the toolset. + string toolPath = FrameworkLocationHelper.GeneratePathToBuildToolsForToolsVersion(toolsVersion, ConvertToSharedDotNetFrameworkArchitecture(architecture)); + return toolPath; + } + + /// + /// Given the name of a file and a ToolsVersion, return the path to that file in the MSBuild + /// tools path for that ToolsVersion + /// + /// The file to find the path to + /// The ToolsVersion in which to find the file + /// The path to the file in the tools path folder of the appropriate ToolsVersion if it + /// exists, otherwise null. + public static string GetPathToBuildToolsFile(string fileName, string toolsVersion) + { + string pathToFile = GetPathToBuildToolsFile(fileName, toolsVersion, UtilitiesDotNetFrameworkArchitecture.Current); + return pathToFile; + } + + /// + /// Given the name of a file and a ToolsVersion, return the path to that file in the MSBuild + /// tools path for that ToolsVersion + /// + /// The file to find the path to + /// The ToolsVersion in which to find the file + /// The architecture of the build tools file to get + /// The path to the file in the tools path folder of the appropriate ToolsVersion if it + /// exists, otherwise null. + public static string GetPathToBuildToolsFile(string fileName, string toolsVersion, UtilitiesDotNetFrameworkArchitecture architecture) + { + string toolPath = GetPathToBuildTools(toolsVersion, architecture); + + if (toolPath != null) + { + toolPath = Path.Combine(toolPath, fileName); + + if (!File.Exists(toolPath)) + { + toolPath = null; + } + } + + return toolPath; + } + + /// + /// Get a fully qualified path to a file in the frameworks root directory. + /// + /// File name to locate in the .NET Framework directory + /// Version of the targeted .NET Framework + /// Will return 'null' if there is no target frameworks on this machine. + public static string GetPathToDotNetFrameworkFile(string fileName, TargetDotNetFrameworkVersion version) + { + return GetPathToDotNetFrameworkFile(fileName, version, UtilitiesDotNetFrameworkArchitecture.Current); + } + + /// + /// Get a fully qualified path to a file in the frameworks root directory for the specified architecture. + /// + /// File name to locate in the .NET Framework directory + /// Version of the targeted .NET Framework + /// Desired architecture, or DotNetFrameworkArchitecture.Current for the architecture this process is currently running under. + /// Will return 'null' if there is no target frameworks on this machine. + public static string GetPathToDotNetFrameworkFile(string fileName, TargetDotNetFrameworkVersion version, UtilitiesDotNetFrameworkArchitecture architecture) + { + string pathToFx = ToolLocationHelper.GetPathToDotNetFramework(version, architecture); + + if (pathToFx == null) + { + return null; + } + + string filePath = Path.Combine(pathToFx, fileName); + return filePath; + } + + /// + /// Get a fully qualified path to a file in the system directory (i.e. %SystemRoot%\System32) + /// + /// File name to locate in the system directory + /// Path string. + public static string GetPathToSystemFile(string fileName) + { + string basePath = ToolLocationHelper.PathToSystem; + string filePath = Path.Combine(basePath, fileName); + return filePath; + } + + /// + /// Gets a IList of supported target framework monikers. + /// + /// the base path for the framework reference assemblies + /// list of supported target framework monikers + public static IList GetSupportedTargetFrameworks() + { + lock (s_locker) + { + if (s_targetFrameworkMonikers == null) + { + s_targetFrameworkMonikers = new List(); + IList frameworkIdentifiers = GetFrameworkIdentifiers(FrameworkLocationHelper.programFilesReferenceAssemblyLocation); + foreach (string frameworkIdentifier in frameworkIdentifiers) + { + IList frameworkVersions = GetFrameworkVersions(FrameworkLocationHelper.programFilesReferenceAssemblyLocation, frameworkIdentifier); + foreach (string frameworkVersion in frameworkVersions) + { + Version version = VersionUtilities.ConvertToVersion(frameworkVersion); + s_targetFrameworkMonikers.Add((new FrameworkNameVersioning(frameworkIdentifier, version, null)).FullName); + + IList frameworkProfile = GetFrameworkProfiles(FrameworkLocationHelper.programFilesReferenceAssemblyLocation, frameworkIdentifier, frameworkVersion); + foreach (string profile in frameworkProfile) + { + s_targetFrameworkMonikers.Add((new FrameworkNameVersioning(frameworkIdentifier, version, profile)).FullName); + } + } + } + } + } + + return s_targetFrameworkMonikers; + } + + + /// + /// This method will return the highest version of a target framework moniker based on the identifier. This method will only + /// find full frameworks, this means no profiles will be returned. + /// + public static FrameworkNameVersioning HighestVersionOfTargetFrameworkIdentifier(string targetFrameworkRootDirectory, string frameworkIdentifier) + { + ErrorUtilities.VerifyThrowArgumentLength(targetFrameworkRootDirectory, "targetFrameworkRootDirectory"); + ErrorUtilities.VerifyThrowArgumentLength(frameworkIdentifier, "frameworkIdentifier"); + + string key = targetFrameworkRootDirectory + ";" + frameworkIdentifier; + FrameworkNameVersioning highestFrameworkName = null; + bool foundInCache = false; + + lock (s_locker) + { + if (s_cachedHighestFrameworkNameForTargetFrameworkIdentifier == null) + { + s_cachedHighestFrameworkNameForTargetFrameworkIdentifier = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + else + { + foundInCache = s_cachedHighestFrameworkNameForTargetFrameworkIdentifier.TryGetValue(key, out highestFrameworkName); + } + + if (!foundInCache) + { + IList frameworkVersions = GetFrameworkVersions(targetFrameworkRootDirectory, frameworkIdentifier); + if (frameworkVersions.Count > 0) + { + Version targetFrameworkVersion = ConvertTargetFrameworkVersionToVersion(frameworkVersions[frameworkVersions.Count - 1]); + highestFrameworkName = new FrameworkNameVersioning(frameworkIdentifier, targetFrameworkVersion); + } + + s_cachedHighestFrameworkNameForTargetFrameworkIdentifier.Add(key, highestFrameworkName); + } + } + + return highestFrameworkName; + } + + /// + /// Will return the root location for the reference assembly directory under the program files directory. + /// + /// + public static string GetProgramFilesReferenceAssemblyRoot() + { + return FrameworkLocationHelper.programFilesReferenceAssemblyLocation; + } + #endregion + + #region private methods + + /// + /// Converts a member of the Microsoft.Build.Utilities.DotNetFrameworkArchitecture enum to the equivalent member of the + /// Microsoft.Build.Shared.DotNetFrameworkArchitecture enum. + /// + private static SharedDotNetFrameworkArchitecture ConvertToSharedDotNetFrameworkArchitecture(UtilitiesDotNetFrameworkArchitecture architecture) + { + SharedDotNetFrameworkArchitecture sharedArchitecture = SharedDotNetFrameworkArchitecture.Current; + switch (architecture) + { + case UtilitiesDotNetFrameworkArchitecture.Current: + sharedArchitecture = SharedDotNetFrameworkArchitecture.Current; + break; + case UtilitiesDotNetFrameworkArchitecture.Bitness32: + sharedArchitecture = SharedDotNetFrameworkArchitecture.Bitness32; + break; + case UtilitiesDotNetFrameworkArchitecture.Bitness64: + sharedArchitecture = SharedDotNetFrameworkArchitecture.Bitness64; + break; + default: + // Should never reach here -- If any new values are added to the DotNetFrameworkArchitecture enum, they should be added here as well. + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + + return sharedArchitecture; + } + + /// + /// Given a string which may start with a "v" convert the string to a version object. + /// + private static Version ConvertTargetFrameworkVersionToVersion(string targetFrameworkVersion) + { + // Trim off the v if is is there. + if (!String.IsNullOrEmpty(targetFrameworkVersion) && targetFrameworkVersion.Substring(0, 1).Equals("v", StringComparison.OrdinalIgnoreCase)) + { + targetFrameworkVersion = targetFrameworkVersion.Substring(1); + } + + return new Version(targetFrameworkVersion); + } + + /// + /// Gets the installed framework identifiers + /// + /// + /// + internal static IList GetFrameworkIdentifiers(string frameworkReferenceRoot) + { + if (String.IsNullOrEmpty(frameworkReferenceRoot)) + { + throw new ArgumentException("Invalid frameworkReferenceRoot", "frameworkReferenceRoot"); + } + + List frameworkIdentifiers = new List(); + + bool bAddDotNetFrameworkIdentifier = false; + bool bFoundDotNetFrameworkIdentifier = false; + bool programFilesReferenceAssemblyLocationFound = false; + + DirectoryInfo di = new DirectoryInfo(frameworkReferenceRoot); + if (di.Exists) + { + if (frameworkReferenceRoot.Equals(FrameworkLocationHelper.programFilesReferenceAssemblyLocation, StringComparison.OrdinalIgnoreCase)) + { + programFilesReferenceAssemblyLocationFound = true; + } + + foreach (DirectoryInfo folder in di.GetDirectories()) + { + if (programFilesReferenceAssemblyLocationFound && + ( + String.Compare(folder.Name, FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV30, StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(folder.Name, FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV35, StringComparison.OrdinalIgnoreCase) == 0 + ) + ) + { + bAddDotNetFrameworkIdentifier = true; + continue; + } + + if (String.Compare(folder.Name, FrameworkLocationHelper.dotNetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase) == 0) + { + bFoundDotNetFrameworkIdentifier = true; + } + + frameworkIdentifiers.Add(folder.Name); + } + } + + + if (programFilesReferenceAssemblyLocationFound && bFoundDotNetFrameworkIdentifier == false) + { + if (bAddDotNetFrameworkIdentifier == false) + { + // special case for .NETFramework v2.0 - check also in the framework path because v20 does not have reference + // assembly folders + + string dotNetFx20Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20); + if (dotNetFx20Path != null) + { + if (Directory.Exists(dotNetFx20Path)) + { + frameworkIdentifiers.Add(FrameworkLocationHelper.dotNetFrameworkIdentifier); + } + } + } + else + { + frameworkIdentifiers.Add(FrameworkLocationHelper.dotNetFrameworkIdentifier); + } + } + + + return frameworkIdentifiers; + } + + + /// + /// Gets the installed versions for a given framework + /// + private static IList GetFrameworkVersions(string frameworkReferenceRoot, string frameworkIdentifier) + { + if (String.IsNullOrEmpty(frameworkReferenceRoot)) + { + throw new ArgumentException("Invalid frameworkReferenceRoot", "frameworkReferenceRoot"); + } + + if (String.IsNullOrEmpty(frameworkIdentifier)) + { + throw new ArgumentException("Invalid frameworkIdentifier", "frameworkIdentifier"); + } + + List frameworkVersions = new List(); + + //backward compatibility with orcas + //In case of orcas .NETFramework v3.0, v3.5 - the version folders are directly under the frameworkReferenceRoot + //first check here + if (String.Compare(frameworkIdentifier, FrameworkLocationHelper.dotNetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase) == 0) + { + IList versions = GetFx35AndEarlierVersions(frameworkReferenceRoot); + if (versions.Count > 0) + { + frameworkVersions.AddRange(versions); + } + } + + //then look under the extensible multi-targeting layout - even for .NETFramework because future .NETFramework + //versions would be at the right place + string frameworkIdentifierPath = Path.Combine(frameworkReferenceRoot, frameworkIdentifier); + + DirectoryInfo dirInfoFxIdentifierPath = new DirectoryInfo(frameworkIdentifierPath); + if (dirInfoFxIdentifierPath.Exists) + { + foreach (DirectoryInfo folder in dirInfoFxIdentifierPath.GetDirectories()) + { + //the expected version folder name is of the format v. e.g. v3.5 + //only add if the version folder name is of the right format + if (folder.Name.Length >= 4 && folder.Name.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + Version ver = null; + if (Version.TryParse(folder.Name.Substring(1), out ver)) + { + frameworkVersions.Add(folder.Name); + } + } + } + } + + //sort in ascending order of the version numbers, this is important as later when we search for assemblies in other methods + //we should be looking in ascending order of the framework version folders on disk + frameworkVersions.Sort(new VersionComparer()); + + return frameworkVersions; + } + + /// + /// Get installed framework profiles + /// + /// + /// + /// + /// + private static IList GetFrameworkProfiles(string frameworkReferenceRoot, string frameworkIdentifier, string frameworkVersion) + { + if (String.IsNullOrEmpty(frameworkReferenceRoot)) + { + throw new ArgumentException("Invalid frameworkReferenceRoot", "frameworkReferenceRoot"); + } + + if (String.IsNullOrEmpty(frameworkIdentifier)) + { + throw new ArgumentException("Invalid frameworkIdentifier", "frameworkIdentifier"); + } + + if (String.IsNullOrEmpty(frameworkVersion)) + { + throw new ArgumentException("Invalid frameworkVersion", "frameworkVersion"); + } + + List frameworkProfiles = new List(); + + string frameworkProfilePath = null; + frameworkProfilePath = Path.Combine(frameworkReferenceRoot, frameworkIdentifier); + frameworkProfilePath = Path.Combine(frameworkProfilePath, frameworkVersion); + frameworkProfilePath = Path.Combine(frameworkProfilePath, "Profiles"); + + DirectoryInfo dirInfoFxProfilePath = new DirectoryInfo(frameworkProfilePath); + if (dirInfoFxProfilePath.Exists) + { + foreach (DirectoryInfo subType in dirInfoFxProfilePath.GetDirectories()) + { + Version ver = VersionUtilities.ConvertToVersion(frameworkVersion); + // check if profile is installed correctly + IList refAssemblyPaths = GetPathToReferenceAssemblies(new FrameworkNameVersioning(frameworkIdentifier, ver, subType.Name)); + if (refAssemblyPaths != null && refAssemblyPaths.Count > 0) + { + frameworkProfiles.Add(subType.Name); + } + } + } + + return frameworkProfiles; + } + + + /// + /// returns the .NETFramework versions lessthanOrEqualTo 3.5 installed in the machine + /// Only returns Fx versions lessthanOrEqualTo 3.5 if DNFx3.5 is installed + /// + /// + /// + private static IList GetFx35AndEarlierVersions(string frameworkReferenceRoot) + { + IList versions = new List(); + + // only return v35 and earlier versions if .NetFx35 is installed + string dotNetFx35Path = null; + dotNetFx35Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35); + + if (dotNetFx35Path != null) + { + // .NetFx35 is installed + + // check v20 + string dotNetFx20Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20); + if (dotNetFx20Path != null) + { + versions.Add("v2.0"); + } + + // check v30 + string dotNextFx30RefPath = Path.Combine(frameworkReferenceRoot, FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV30); + if (Directory.Exists(dotNextFx30RefPath)) + { + versions.Add(FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV30); + } + + // check v35 + string dotNextFx35RefPath = Path.Combine(frameworkReferenceRoot, FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV35); + if (Directory.Exists(dotNextFx35RefPath)) + { + versions.Add(FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV35); + } + } + + return versions; + } + + #endregion + + #region private class + /// + /// Compares framework version strings of the format v4.1.2.3 + /// major version and minor version are mandatory others are optional + /// + private class VersionComparer : IComparer + { + public int Compare(string versionX, string versionY) + { + return (new Version(versionX.Substring(1))).CompareTo(new Version(versionY.Substring(1))); + } + } + + #endregion + } +} diff --git a/src/Utilities/ToolTask.cs b/src/Utilities/ToolTask.cs new file mode 100644 index 00000000000..dc16313ddd5 --- /dev/null +++ b/src/Utilities/ToolTask.cs @@ -0,0 +1,1806 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Collections.Specialized; +using System.Collections.Generic; +using System.Text; +using System.Resources; +using System.Threading; +using System.Reflection; +using System.Diagnostics; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Utilities +{ + /// + /// The return value from InitializeHostObject. This enumeration defines what action the ToolTask + /// should take next, after we've tried to initialize the host object. + /// + public enum HostObjectInitializationStatus + { + /// + /// This means that there exists an appropriate host object for this task, it can support + /// all of the parameters passed in, and it should be invoked to do the real work of the task. + /// + UseHostObjectToExecute, + + /// + /// This means that either there is no host object available, or that the host object is + /// not capable of supporting all of the features required for this build. Therefore, + /// ToolTask should fallback to an alternate means of doing the build, such as invoking + /// the command-line tool. + /// + UseAlternateToolToExecute, + + /// + /// This means that the host object is already up-to-date, and no further action is necessary. + /// + NoActionReturnSuccess, + + /// + /// This means that some of the parameters being passed into the task are invalid, and the + /// task should fail immediately. + /// + NoActionReturnFailure + } + + /// + /// Base class used for tasks that spawn an executable. This class implements the ToolPath property which can be used to + /// override the default path. + /// + /// + /// INTERNAL WARNING: DO NOT USE the Log property in this class! Log points to resources in the task assembly itself, and + /// we want to use resources from Utilities. Use LogPrivate (for private Utilities resources) and LogShared (for shared MSBuild resources) + /// + public abstract class ToolTask : Task, ICancelableTask + { + #region Constructors + + /// + /// Protected constructor + /// + protected ToolTask() + { + _logPrivate = new TaskLoggingHelper(this); + _logPrivate.TaskResources = AssemblyResources.PrimaryResources; + _logPrivate.HelpKeywordPrefix = "MSBuild."; + + _logShared = new TaskLoggingHelper(this); + _logShared.TaskResources = AssemblyResources.SharedResources; + _logShared.HelpKeywordPrefix = "MSBuild."; + + // 5 second is the default termination timeout. + TaskProcessTerminationTimeout = 5000; + ToolCanceled = new ManualResetEvent(false); + } + + /// + /// Protected constructor + /// + /// The resource manager for task resources + protected ToolTask(ResourceManager taskResources) + : this() + { + this.TaskResources = taskResources; + } + + /// + /// Protected constructor + /// + /// The resource manager for task resources + /// The help keyword prefix for task's messages + protected ToolTask(ResourceManager taskResources, string helpKeywordPrefix) + : this(taskResources) + { + this.HelpKeywordPrefix = helpKeywordPrefix; + } + + #endregion + + #region Properties + + /// + /// The return code of the spawned process. If the task logged any errors, but the process + /// had an exit code of 0 (success), this will be set to -1. + /// + [Output] + public int ExitCode + { + get + { + return _exitCode; + } + } + + /// + /// When set to true, this task will yield the node when its task is executing. + /// + public bool YieldDuringToolExecution + { + get; + set; + } + + /// + /// When set to true, the tool task will create a batch file for the command-line and execute that using the command-processor, + /// rather than executing the command directly. + /// + public bool UseCommandProcessor + { + get; + set; + } + + /// + /// When set to true, it passes /Q to the cmd.exe command line such that the command line does not get echo-ed on stdout + /// + public bool EchoOff + { + get; + set; + } + + /// + /// A timeout to wait for a task to terminate before killing it. In milliseconds. + /// + protected int TaskProcessTerminationTimeout + { + get; + set; + } + + /// + /// Used to signal when a tool has been cancelled. + /// + protected ManualResetEvent ToolCanceled + { + get; + private set; + } + + private int _exitCode; + + /// + /// This is the batch file created when UseCommandProcessor is set to true. + /// + private string _temporaryBatchFile; + + /// + /// Implemented by the derived class. Returns a string which is the name of the underlying .EXE to run e.g. "resgen.exe" + /// Only used by the ToolExe getter. + /// + /// Name of tool. + abstract protected string ToolName { get; } + + /// + /// Projects may set this to override a task's ToolName. + /// Tasks may override this to prevent that. + /// + public virtual string ToolExe + { + get + { + if (!String.IsNullOrEmpty(_toolExe)) + { + // If the ToolExe has been overridden then return the value + return _toolExe; + } + else + { + // We have no override, so simply delegate to ToolName + return this.ToolName; + } + } + set + { + _toolExe = value; + } + } + + /// + /// Project-visible property allows the user to override the path to the executable. + /// + /// Path to tool. + public string ToolPath + { + set { _toolPath = value; } + get { return _toolPath; } + } + + /// + /// Array of equals-separated pairs of environment + /// variables that should be passed to the spawned executable, + /// in addition to (or selectively overriding) the regular environment block. + /// + /// + /// Using this instead of EnvironmentOverride as that takes a StringDictionary, + /// which cannot be set from an MSBuild project. + /// + public string[] EnvironmentVariables + { + get; + set; + } + + private string _toolPath; + + /// + /// Project visible property that allows the user to specify an amount of time after which the task executable + /// is terminated. + /// + /// Time-out in milliseconds. Default is (no time-out). + virtual public int Timeout + { + set { _timeout = value; } + get { return _timeout; } + } + + private int _timeout = System.Threading.Timeout.Infinite; + + /// + /// Overridable property specifying the encoding of the response file, UTF8 by default + /// + virtual protected Encoding ResponseFileEncoding + { + get { return Encoding.UTF8; } + } + + /// + /// Overridable method to escape content of the response file + /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Shipped this way in Dev11 Beta (go-live)")] + virtual protected string ResponseFileEscape(string responseString) + { + return responseString; + } + + /// + /// Overridable property specifying the encoding of the captured task standard output stream + /// + /// + /// Console-based output uses the current system OEM code page by default. Note that we should not use Console.OutputEncoding + /// here since processes we run don't really have much to do with our console window (and also Console.OutputEncoding + /// doesn't return the OEM code page if the running application that hosts MSBuild is not a console application). + /// + virtual protected Encoding StandardOutputEncoding + { + get { return EncodingUtilities.CurrentSystemOemEncoding; } + } + + /// + /// Overridable property specifying the encoding of the captured task standard error stream + /// + /// + /// Console-based output uses the current system OEM code page by default. Note that we should not use Console.OutputEncoding + /// here since processes we run don't really have much to do with our console window (and also Console.OutputEncoding + /// doesn't return the OEM code page if the running application that hosts MSBuild is not a console application). + /// + virtual protected Encoding StandardErrorEncoding + { + get { return EncodingUtilities.CurrentSystemOemEncoding; } + } + + /// + /// Gets the Path override value. + /// + /// The new value for the Environment for the task. + [Obsolete("Use EnvironmentVariables property")] + virtual protected StringDictionary EnvironmentOverride + { + get { return null; } + } + + /// + /// Importance with which to log text from the + /// standard error stream. + /// + virtual protected MessageImportance StandardErrorLoggingImportance + { + get { return MessageImportance.Normal; } + } + + /// + /// Whether this ToolTask has logged any errors + /// + protected virtual bool HasLoggedErrors + { + get + { + return (Log.HasLoggedErrors || LogPrivate.HasLoggedErrors || LogShared.HasLoggedErrors); + } + } + + /// + /// Task Parameter: Importance with which to log text from the + /// standard out stream. + /// + public string StandardOutputImportance + { + get + { + return _standardOutputImportance; + } + set + { + _standardOutputImportance = value; + } + } + + /// + /// Task Parameter: Importance with which to log text from the + /// standard error stream. + /// + public string StandardErrorImportance + { + get + { + return _standardErrorImportance; + } + set + { + _standardErrorImportance = value; + } + } + + /// + /// Should ALL messages received on the standard error stream be logged as errors. + /// + public bool LogStandardErrorAsError + { + get + { + return _logStandardErrorAsError; + } + + set + { + _logStandardErrorAsError = value; + } + } + + /// + /// Importance with which to log text from in the + /// standard out stream. + /// + virtual protected MessageImportance StandardOutputLoggingImportance + { + get { return MessageImportance.Low; } + } + + /// + /// The actual importance at which standard out messages will be logged. + /// + protected MessageImportance StandardOutputImportanceToUse + { + get { return _standardOutputImportanceToUse; } + } + + /// + /// The actual importance at which standard error messages will be logged. + /// + protected MessageImportance StandardErrorImportanceToUse + { + get { return _standardErrorImportanceToUse; } + } + + #endregion + + #region Private properties + + /// + /// Gets an instance of a private TaskLoggingHelper class containing task logging methods. + /// This is necessary because ToolTask lives in a different assembly than the task inheriting from it + /// and needs its own separate resources. + /// + /// The logging helper object. + private TaskLoggingHelper LogPrivate + { + get + { + return _logPrivate; + } + } + + // the private logging helper + private TaskLoggingHelper _logPrivate; + + /// + /// Gets an instance of a shared resources TaskLoggingHelper class containing task logging methods. + /// This is necessary because ToolTask lives in a different assembly than the task inheriting from it + /// and needs its own separate resources. + /// + /// The logging helper object. + private TaskLoggingHelper LogShared + { + get + { + return _logShared; + } + } + + // the shared resources logging helper + private TaskLoggingHelper _logShared; + + #endregion + + #region Overridable methods + + /// + /// Gets the fully qualified tool name. Should return ToolExe if ToolTask should search for the tool + /// in the system path. If ToolPath is set, this is ignored. + /// + /// Path string. + abstract protected string GenerateFullPathToTool(); + + /// + /// Gets the working directory to use for the process. Should return null if ToolTask should use the + /// current directory. + /// + /// This is a method rather than a property so that derived classes (like Exec) can choose to + /// expose a public WorkingDirectory property, and it would be confusing to have two properties. + /// + virtual protected string GetWorkingDirectory() + { + return null; + } + + /// + /// Implemented in the derived class + /// + /// true, if successful + protected internal virtual bool ValidateParameters() + { + // Default is no validation. This is useful for tools that don't need validation. + return true; + } + + /// + /// Returns true if task execution is not necessary. Executed after ValidateParameters + /// + /// + virtual protected bool SkipTaskExecution() + { + return false; + } + + /// + /// Returns a string with those switches and other information that can go into a response file. + /// Called after ValidateParameters and SkipTaskExecution + /// + /// + virtual protected string GenerateResponseFileCommands() + { + // Default is nothing. This is useful for tools that don't need or support response files. + return string.Empty; + } + + /// + /// Returns a string with those switches and other information that can't go into a response file and + /// must go directly onto the command line. + /// Called after ValidateParameters and SkipTaskExecution + /// + /// + virtual protected string GenerateCommandLineCommands() + { + // Default is nothing. This is useful for tools where all the parameters can go into a response file. + return string.Empty; + } + + /// + /// Returns the command line switch used by the tool executable to specify the response file. + /// Will only be called if the task returned a non empty string from GetResponseFileCommands + /// Called after ValidateParameters, SkipTaskExecution and GetResponseFileCommands + /// + /// full path to the temporarily created response file + /// + virtual protected string GetResponseFileSwitch(string responseFilePath) + { + // by default, return @"" + return "@\"" + responseFilePath + "\""; + } + + /// + /// Allows tool to handle the return code. + /// This method will only be called with non-zero exitCode. + /// + /// The return value of this method will be used as the task return value + virtual protected bool HandleTaskExecutionErrors() + { + Debug.Assert(_exitCode != 0, "HandleTaskExecutionErrors should only be called if there were problems executing the task"); + + if (HasLoggedErrors) + { + // Emit a message. + LogPrivate.LogMessageFromResources(MessageImportance.Low, "General.ToolCommandFailedNoErrorCode", _exitCode); + } + else + { + // If the tool itself did not log any errors on its own, then we log one now simply saying + // that the tool exited with a non-zero exit code. This way, the customer nevers sees + // "Build failed" without at least one error being logged. + LogPrivate.LogErrorWithCodeFromResources("ToolTask.ToolCommandFailed", ToolExe, _exitCode); + } + + // by default, always fail the task + return false; + } + + /// + /// We expect the tasks to override this method, if they support host objects. The implementation should call into the + /// host object to perform the real work of the task. For example, for compiler tasks like Csc and Vbc, this method would + /// call Compile() on the host object. + /// + /// The return value indicates success (true) or failure (false) if the host object was actually called to do the work. + virtual protected bool CallHostObjectToExecute() + { + return false; + } + + /// + /// We expect tasks to override this method if they support host objects. The implementation should + /// make sure that the host object is ready to perform the real work of the task. + /// + /// The return value indicates what steps to take next. The default is to assume that there + /// is no host object provided, and therefore we should fallback to calling the command-line tool. + virtual protected HostObjectInitializationStatus InitializeHostObject() + { + return HostObjectInitializationStatus.UseAlternateToolToExecute; + } + + /// + /// Logs the actual command line about to be executed (or what the task wants the log to show) + /// + /// + /// Descriptive message about what is happening - usually the command line to be executed. + /// + virtual protected void LogToolCommand + ( + string message + ) + { + // Log a descriptive message about what's happening. + LogPrivate.LogCommandLine(MessageImportance.High, message); + } + + /// + /// Logs the tool name and the path from where it is being run. + /// + /// + /// The tool to Log. This is the actual tool being used, ie. if ToolExe has been specified it will be used, otherwise it will be ToolName + /// + /// + /// The path from where the tool is being run. + /// + virtual protected void LogPathToTool + ( + string toolName, + string pathToTool + ) + { + // We don't do anything here any more, as it was just duplicative and noise. + // The method only remains for backwards compatibility - to avoid breaking tasks that override it + } + + #endregion + + #region Methods + + /// + /// Figures out the path to the tool (including the .exe), either by using the ToolPath + /// parameter, or by asking the derived class to tell us where it should be located. + /// + /// path to the tool, or null + private string ComputePathToTool() + { + string pathToTool; + + if (UseCommandProcessor) + { + return ToolExe; + } + + if (ToolPath != null && ToolPath.Length > 0) + { + // If the project author passed in a ToolPath, always use that. + pathToTool = Path.Combine(ToolPath, ToolExe); + } + else + { + // Otherwise, try to find the tool ourselves. + pathToTool = GenerateFullPathToTool(); + + // We have no toolpath, but we have been given an override + // for the tool exe, fix up the path, assuming that the tool is in the same location + if (pathToTool != null && !String.IsNullOrEmpty(_toolExe)) + { + string directory = Path.GetDirectoryName(pathToTool); + pathToTool = Path.Combine(directory, ToolExe); + } + } + + // only look for the file if we have a path to it. If we have just the file name, we'll + // look for it in the path + if (pathToTool != null) + { + bool isOnlyFileName = (Path.GetFileName(pathToTool).Length == pathToTool.Length); + if (!isOnlyFileName) + { + bool isExistingFile = File.Exists(pathToTool); + if (!isExistingFile) + { + LogPrivate.LogErrorWithCodeFromResources("ToolTask.ToolExecutableNotFound", pathToTool); + return null; + } + } + else + { + // if we just have the file name, search for the file on the system path + string actualPathToTool = NativeMethodsShared.FindOnPath(pathToTool); + + // if we find the file + if (actualPathToTool != null) + { + // point to it + pathToTool = actualPathToTool; + } + else + { + // if we cannot find the file, we'll probably error out later on when + // we try to launch the tool; so do nothing for now + } + } + } + + return pathToTool; + } + + /// + /// Creates a temporary response file for the given command line arguments. + /// We put as many command line arguments as we can into a response file to + /// prevent the command line from getting too long. An overly long command + /// line can cause the process creation to fail. + /// + /// + /// Command line arguments that cannot be put into response files, and which + /// must appear on the command line, should not be passed to this method. + /// + /// The command line arguments that need + /// to go into the temporary response file. + /// [out] The command line switch for using + /// the temporary response file, or null if the response file is not needed. + /// + /// The path to the temporary response file, or null if the response + /// file is not needed. + private string GetTemporaryResponseFile(string responseFileCommands, out string responseFileSwitch) + { + string responseFile = null; + responseFileSwitch = null; + + // if this tool supports response files + if (!String.IsNullOrEmpty(responseFileCommands)) + { + // put all the parameters into a temporary response file so we don't + // have to worry about how long the command-line is going to be + + // May throw IO-related exceptions + responseFile = FileUtilities.GetTemporaryFile(".rsp"); + + // Use the encoding specified by the overridable ResponseFileEncoding property + using (StreamWriter responseFileStream = new StreamWriter(responseFile, false, this.ResponseFileEncoding)) + { + responseFileStream.Write(ResponseFileEscape(responseFileCommands)); + } + + responseFileSwitch = GetResponseFileSwitch(responseFile); + } + + return responseFile; + } + + /// + /// Initializes the information required to spawn the process executing the tool. + /// + /// + /// + /// + /// The information required to start the process. + protected ProcessStartInfo GetProcessStartInfo + ( + string pathToTool, + string commandLineCommands, + string responseFileSwitch + ) + { + // Build up the command line that will be spawned. + string commandLine = commandLineCommands; + + if (!UseCommandProcessor) + { + if (!String.IsNullOrEmpty(responseFileSwitch)) + { + commandLine += " " + responseFileSwitch; + } + } + + // If the command is too long, it will most likely fail. The command line + // arguments passed into any process cannot exceed 32768 characters, but + // depending on the structure of the command (e.g. if it contains embedded + // environment variables that will be expanded), longer commands might work, + // or shorter commands might fail -- to play it safe, we warn at 32000. + // NOTE: cmd.exe has a buffer limit of 8K, but we're not using cmd.exe here, + // so we can go past 8K easily. + if (commandLine.Length > 32000) + { + LogPrivate.LogWarningWithCodeFromResources("ToolTask.CommandTooLong", this.GetType().Name); + } + + ProcessStartInfo startInfo = new ProcessStartInfo(pathToTool, commandLine); + startInfo.CreateNoWindow = true; + startInfo.UseShellExecute = false; + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + // ensure the redirected streams have the encoding we want + startInfo.StandardErrorEncoding = StandardErrorEncoding; + startInfo.StandardOutputEncoding = StandardOutputEncoding; + + // Some applications such as xcopy.exe fail without error if there's no stdin stream. + startInfo.RedirectStandardInput = true; + + // Generally we won't set a working directory, and it will use the current directory + string workingDirectory = GetWorkingDirectory(); + if (null != workingDirectory) + { + startInfo.WorkingDirectory = workingDirectory; + } + + // Old style environment overrides +#pragma warning disable 0618 // obsolete + StringDictionary envOverrides = EnvironmentOverride; + if (null != envOverrides) + { + foreach (DictionaryEntry entry in envOverrides) + { + startInfo.EnvironmentVariables[(string)entry.Key] = (string)entry.Value; + } +#pragma warning restore 0618 + } + + // New style environment overrides + if (_environmentVariablePairs != null) + { + foreach (KeyValuePair variable in _environmentVariablePairs) + { + startInfo.EnvironmentVariables[(string)variable.Key] = (string)variable.Value; + } + } + + return startInfo; + } + + /// + /// Writes out a temporary response file and shell-executes the tool requested. Enables concurrent + /// logging of the output of the tool. + /// + /// The computed path to tool executable on disk + /// Command line arguments that should go into a temporary response file + /// Command line arguments that should be passed to the tool executable directly + /// exit code from the tool - if errors were logged and the tool has an exit code of zero, then we sit it to -1 + virtual protected int ExecuteTool + ( + string pathToTool, + string responseFileCommands, + string commandLineCommands + ) + { + if (!UseCommandProcessor) + { + LogPathToTool(ToolExe, pathToTool); + } + + string responseFile = null; + Process proc = null; + + _standardErrorData = new Queue(); + _standardOutputData = new Queue(); + + _standardErrorDataAvailable = new ManualResetEvent(false); + _standardOutputDataAvailable = new ManualResetEvent(false); + + _toolExited = new ManualResetEvent(false); + _toolTimeoutExpired = new ManualResetEvent(false); + + _eventsDisposed = false; + + try + { + string responseFileSwitch; + responseFile = GetTemporaryResponseFile(responseFileCommands, out responseFileSwitch); + + // create/initialize the process to run the tool + proc = new Process(); + proc.StartInfo = GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch); + + // turn on the Process.Exited event + proc.EnableRaisingEvents = true; + // sign up for the exit notifcation + proc.Exited += new EventHandler(ReceiveExitNotification); + + // turn on async stderr notifications + proc.ErrorDataReceived += new DataReceivedEventHandler(ReceiveStandardErrorData); + // turn on async stdout notifications + proc.OutputDataReceived += new DataReceivedEventHandler(ReceiveStandardOutputData); + + // if we've got this far, we expect to get an exit code from the process. If we don't + // get one from the process, we want to use an exit code value of -1. + _exitCode = -1; + + // Start the process + proc.Start(); + + // Close the input stream. This is done to prevent commands from + // blocking the build waiting for input from the user. + proc.StandardInput.Close(); + + // sign up for stderr callbacks + proc.BeginErrorReadLine(); + // sign up for stdout callbacks + proc.BeginOutputReadLine(); + + // start the time-out timer + _toolTimer = new Timer(new TimerCallback(ReceiveTimeoutNotification)); + _toolTimer.Change(Timeout, System.Threading.Timeout.Infinite /* no periodic timeouts */); + + // deal with the various notifications + HandleToolNotifications(proc); + } + finally + { + // Delete the temp file used for the response file. + if (responseFile != null) + { + DeleteTempFile(responseFile); + } + + // get the exit code and release the process handle + if (proc != null) + { + try + { + _exitCode = proc.ExitCode; + } + catch (InvalidOperationException) + { + // The process was never launched successfully. + // Leave the exit code at -1. + } + + proc.Close(); + proc = null; + } + + // If the tool exited cleanly, but logged errors then assign a failing exit code (-1) + if ((_exitCode == 0) && HasLoggedErrors) + { + _exitCode = -1; + } + + // release all the OS resources + // setting a bool to make sure tardy notification threads + // don't try to set the event after this point + lock (_eventCloseLock) + { + _eventsDisposed = true; + _standardErrorDataAvailable.Close(); + _standardOutputDataAvailable.Close(); + + _toolExited.Close(); + _toolTimeoutExpired.Close(); + + if (_toolTimer != null) + { + _toolTimer.Dispose(); + } + } + } + + return _exitCode; + } + + /// + /// Cancels the process executing the task by asking it to close nicely, then after a short period, forcing termination. + /// + public virtual void Cancel() + { + ToolCanceled.Set(); + } + + /// + /// Delete temporary file. If the delete fails for some reason (e.g. file locked by anti-virus) then + /// the call will not throw an exception. Instead a warning will be logged, but the build will not fail. + /// + /// File to delete + protected void DeleteTempFile(string fileName) + { + try + { + File.Delete(fileName); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + // Warn only -- occasionally temp files fail to delete because of virus checkers; we + // don't want the build to fail in such cases + LogShared.LogWarningWithCodeFromResources("Shared.FailedDeletingTempFile", fileName, e.Message); + } + } + + /// + /// Handles all the notifications sent while the tool is executing. The + /// notifications can be for tool output, tool time-out, or tool completion. + /// + /// + /// The slightly convoluted use of the async stderr/stdout streams of the + /// Process class is necessary because we want to log all our messages from + /// the main thread, instead of from a worker or callback thread. + /// + /// + private void HandleToolNotifications(Process proc) + { + // NOTE: the ordering of this array is deliberate -- if multiple + // notifications are sent simultaneously, we want to handle them + // in the order specified by the array, so that we can observe the + // following rules: + // 1) if a tool times-out we want to abort it immediately regardless + // of whether its stderr/stdout queues are empty + // 2) if a tool exits, we first want to flush its stderr/stdout queues + // 3) if a tool exits and times-out at the same time, we want to let + // it exit gracefully + WaitHandle[] notifications = new WaitHandle[] + { + _toolTimeoutExpired, + ToolCanceled, + _standardErrorDataAvailable, + _standardOutputDataAvailable, + _toolExited + }; + + bool isToolRunning = true; + + if (YieldDuringToolExecution) + { + BuildEngine3.Yield(); + } + + try + { + while (isToolRunning) + { + // wait for something to happen -- we block the main thread here + // because we don't want to uselessly consume CPU cycles; in theory + // we could poll the stdout and stderr queues, but polling is not + // good for performance, and so we use ManualResetEvents to wake up + // the main thread only when necessary + // NOTE: the return value from WaitAny() is the array index of the + // notification that was sent; if multiple notifications are sent + // simultaneously, the return value is the index of the notification + // with the smallest index value of all the sent notifications + int notificationIndex = WaitHandle.WaitAny(notifications); + + switch (notificationIndex) + { + // tool timed-out + case 0: + // tool was canceled + case 1: + TerminateToolProcess(proc, notificationIndex == 1); + _terminatedTool = true; + isToolRunning = false; + break; + // tool wrote to stderr (and maybe stdout also) + case 2: + LogMessagesFromStandardError(); + // if stderr and stdout notifications were sent simultaneously, we + // must alternate between the queues, and not starve the stdout queue + LogMessagesFromStandardOutput(); + break; + + // tool wrote to stdout + case 3: + LogMessagesFromStandardOutput(); + break; + + // tool exited + case 4: + // We need to do this to guarantee the stderr/stdout streams + // are empty -- there seems to be no other way of telling when the + // process is done sending its async stderr/stdout notifications; why + // is the Process class sending the exit notification prematurely? + WaitForProcessExit(proc); + + // flush the stderr and stdout queues to clear out the data placed + // in them while we were waiting for the process to exit + LogMessagesFromStandardError(); + LogMessagesFromStandardOutput(); + isToolRunning = false; + break; + + default: + ErrorUtilities.VerifyThrow(false, "Unknown tool notification."); + break; + } + } + } + finally + { + if (YieldDuringToolExecution) + { + BuildEngine3.Reacquire(); + } + } + } + + /// + /// Kills the given process that is executing the tool, because the tool's + /// time-out period expired. + /// + /// + private void KillToolProcessOnTimeout(Process proc, bool isBeingCancelled) + { + // kill the process if it's not finished yet + if (!proc.HasExited) + { + if (!isBeingCancelled) + { + ErrorUtilities.VerifyThrow(Timeout != System.Threading.Timeout.Infinite, + "A time-out value must have been specified or the task must be cancelled."); + + LogShared.LogWarningWithCodeFromResources("Shared.KillingProcess", this.Timeout); + } + else + { + LogShared.LogWarningWithCodeFromResources("Shared.KillingProcessByCancellation", proc.ProcessName); + } + + try + { + // issue the kill command + NativeMethodsShared.KillTree(proc.Id); + } + catch (InvalidOperationException) + { + // The process already exited, which is fine, + // just continue. + } + + // wait until the process finishes exiting/getting killed. + // We don't want to wait forever here because the task is already supposed to be dieing, we just want to give it long enought + // to try and flush what it can and stop. If it cannot do that in a reasonable time frame then we will just ignore it. + int timeout = 5000; + string timeoutFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDTOOLTASKCANCELPROCESSWAITTIMEOUT"); + if (timeoutFromEnvironment != null) + { + int result = 0; + if (int.TryParse(timeoutFromEnvironment, out result) && result >= 0) + { + timeout = result; + } + } + + proc.WaitForExit(timeout); + } + } + + /// + /// Kills the specified process + /// + private void TerminateToolProcess(Process proc, bool isBeingCancelled) + { + if (proc != null) + { + if (proc.HasExited) + { + return; + } + + if (isBeingCancelled) + { + try + { + proc.CancelOutputRead(); + proc.CancelErrorRead(); + } + catch (InvalidOperationException) + { + // The task possibly never started. + } + } + + KillToolProcessOnTimeout(proc, isBeingCancelled); + } + } + + /// + /// Confirms that the given process has really and truly exited. If the + /// process is still finishing up, this method waits until it is done. + /// + /// + /// This method is a hack, but it needs to be called after both + /// Process.WaitForExit() and Process.Kill(). + /// + /// + private void WaitForProcessExit(Process proc) + { + proc.WaitForExit(); + + // Process.WaitForExit() may return prematurely. We need to check to be sure. + while (!proc.HasExited) + { + System.Threading.Thread.Sleep(50); + } + } + + /// + /// Logs all the messages that the tool wrote to stderr. The messages + /// are read out of the stderr data queue. + /// + private void LogMessagesFromStandardError() + { + LogMessagesFromStandardErrorOrOutput(_standardErrorData, _standardErrorDataAvailable, _standardErrorImportanceToUse, StandardOutputOrErrorQueueType.StandardError); + } + + /// + /// Logs all the messages that the tool wrote to stdout. The messages + /// are read out of the stdout data queue. + /// + private void LogMessagesFromStandardOutput() + { + LogMessagesFromStandardErrorOrOutput(_standardOutputData, _standardOutputDataAvailable, _standardOutputImportanceToUse, StandardOutputOrErrorQueueType.StandardOutput); + } + + /// + /// Logs all the messages that the tool wrote to either stderr or stdout. + /// The messages are read out of the given data queue. This method is a + /// helper for the () and () methods. + /// + /// + /// + /// + private void LogMessagesFromStandardErrorOrOutput + ( + Queue dataQueue, + ManualResetEvent dataAvailableSignal, + MessageImportance messageImportance, + StandardOutputOrErrorQueueType queueType + ) + { + ErrorUtilities.VerifyThrow(dataQueue != null, + "The data queue must be available."); + + // synchronize access to the queue -- this is a producer-consumer problem + // NOTE: the synchronization problem here is actually not about the queue + // at all -- if we only cared about reading from and writing to the queue, + // we could use a synchronized wrapper around the queue, and things would + // work perfectly; the synchronization problem here is actually around the + // ManualResetEvent -- while a ManualResetEvent itself is a thread-safe + // type, the information we infer from the state of a ManualResetEvent is + // not thread-safe; because a ManualResetEvent does not have a ref count, + // we cannot safely set (or reset) it outside of a synchronization block; + // therefore instead of using synchronized queue wrappers, we just lock the + // entire queue, empty it, and reset the ManualResetEvent before releasing + // the lock; this also allows proper alternation between the stderr and + // stdout queues -- otherwise we would continuously read from one queue and + // starve the other; locking out the producer allows the consumer to + // alternate between the queues + lock (dataQueue.SyncRoot) + { + while (dataQueue.Count > 0) + { + string errorOrOutMessage = dataQueue.Dequeue() as String; + if (!LogStandardErrorAsError || queueType == StandardOutputOrErrorQueueType.StandardOutput) + { + this.LogEventsFromTextOutput(errorOrOutMessage, messageImportance); + } + else if (LogStandardErrorAsError && queueType == StandardOutputOrErrorQueueType.StandardError) + { + Log.LogError(errorOrOutMessage); + } + } + + ErrorUtilities.VerifyThrow(dataAvailableSignal != null, + "The signalling event must be available."); + + // the queue is empty, so reset the notification + // NOTE: intentionally, do the reset inside the lock, because + // ManualResetEvents don't have ref counts, and we want to make + // sure we don't reset the notification just after the producer + // signals it + dataAvailableSignal.Reset(); + } + } + + /// + /// Calls a method on the TaskLoggingHelper to parse a single line of text to + /// see if there are any errors or warnings in canonical format. This can + /// be overridden by the derived class if necessary. + /// + /// + /// + virtual protected void LogEventsFromTextOutput + ( + string singleLine, + MessageImportance messageImportance + ) + { + Log.LogMessageFromText(singleLine, messageImportance); + } + + /// + /// Signals when the tool times-out. The tool timer calls this method + /// when the time-out period on the tool expires. + /// + /// This method is used as a System.Threading.TimerCallback delegate. + /// + private void ReceiveTimeoutNotification(object unused) + { + ErrorUtilities.VerifyThrow(_toolTimeoutExpired != null, + "The signalling event for tool time-out must be available."); + lock (_eventCloseLock) + { + if (!_eventsDisposed) + { + _toolTimeoutExpired.Set(); + } + } + } + + /// + /// Signals when the tool exits. The Process object executing the tool + /// calls this method when the tool exits. + /// + /// This method is used as a System.EventHandler delegate. + /// + /// + private void ReceiveExitNotification(object sender, EventArgs e) + { + ErrorUtilities.VerifyThrow(_toolExited != null, + "The signalling event for tool exit must be available."); + + lock (_eventCloseLock) + { + if (!_eventsDisposed) + { + _toolExited.Set(); + } + } + } + + /// + /// Queues up the output from the stderr stream of the process executing + /// the tool, and signals the availability of the data. The Process object + /// executing the tool calls this method for every line of text that the + /// tool writes to stderr. + /// + /// This method is used as a System.Diagnostics.DataReceivedEventHandler delegate. + /// + /// + private void ReceiveStandardErrorData(object sender, DataReceivedEventArgs e) + { + ReceiveStandardErrorOrOutputData(e, _standardErrorData, _standardErrorDataAvailable); + } + + /// + /// Queues up the output from the stdout stream of the process executing + /// the tool, and signals the availability of the data. The Process object + /// executing the tool calls this method for every line of text that the + /// tool writes to stdout. + /// + /// This method is used as a System.Diagnostics.DataReceivedEventHandler delegate. + /// + /// + private void ReceiveStandardOutputData(object sender, DataReceivedEventArgs e) + { + ReceiveStandardErrorOrOutputData(e, _standardOutputData, _standardOutputDataAvailable); + } + + /// + /// Queues up the output from either the stderr or stdout stream of the + /// process executing the tool, and signals the availability of the data. + /// This method is a helper for the () + /// and () methods. + /// + /// + /// + /// + private void ReceiveStandardErrorOrOutputData + ( + DataReceivedEventArgs e, + Queue dataQueue, + ManualResetEvent dataAvailableSignal + ) + { + // NOTE: don't ignore empty string, because we need to log that + if (e.Data != null) + { + ErrorUtilities.VerifyThrow(dataQueue != null, + "The data queue must be available."); + + // synchronize access to the queue -- this is a producer-consumer problem + // NOTE: we lock the entire queue instead of using synchronized queue + // wrappers, because ManualResetEvents don't have ref counts, and it's + // difficult to discretely signal the availability of each instance of + // data in the queue -- so instead we let the consumer lock and empty + // the queue and reset the ManualResetEvent, before we add more data + // into the queue, and signal the ManualResetEvent again + lock (dataQueue.SyncRoot) + { + dataQueue.Enqueue(e.Data); + + ErrorUtilities.VerifyThrow(dataAvailableSignal != null, + "The signalling event must be available."); + + // signal the availability of data + // NOTE: intentionally, do the signalling inside the lock, because + // ManualResetEvents don't have ref counts, and we want to make sure + // we don't signal the notification just before the consumer resets it + lock (_eventCloseLock) + { + if (!_eventsDisposed) + { + dataAvailableSignal.Set(); + } + } + } + } + } + + /// + /// Assign the importances that will be used for stdout/stderr logging of messages from this tool task. + /// This takes into account (1 is highest precedence): + /// 1. the override value supplied as a task parameter. + /// 2. those overridden by any derived class and + /// 3. the defaults given by tooltask + /// + private bool AssignStandardStreamLoggingImportance() + { + // Gather the importance for the Standard Error stream: + if ((_standardErrorImportance == null) || (_standardErrorImportance.Length == 0)) + { + // If we have no task parameter override then ask the task for its default + _standardErrorImportanceToUse = StandardErrorLoggingImportance; + } + else + { + try + { + // Parse the raw importance string into a strongly typed enumeration. + _standardErrorImportanceToUse = (MessageImportance)Enum.Parse(typeof(MessageImportance), _standardErrorImportance, true /* case-insensitive */); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("Message.InvalidImportance", _standardErrorImportance); + return false; + } + } + + // Gather the importance for the Standard Output stream: + if ((_standardOutputImportance == null) || (_standardOutputImportance.Length == 0)) + { + // If we have no task parameter override then ask the task for its default + _standardOutputImportanceToUse = StandardOutputLoggingImportance; + } + else + { + try + { + // Parse the raw importance string into a strongly typed enumeration. + _standardOutputImportanceToUse = (MessageImportance)Enum.Parse(typeof(MessageImportance), _standardOutputImportance, true /* case-insensitive */); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("Message.InvalidImportance", _standardOutputImportance); + return false; + } + } + + return true; + } + + #endregion + + #region ITask Members + + /// + /// This method invokes the tool with the given parameters. + /// + /// true, if task executes successfully + public override bool Execute() + { + // Let the tool validate its parameters. ToolTask is responsible for logging + // useful information about what was wrong with the parameters. + if (!ValidateParameters()) + { + return false; + } + + if (EnvironmentVariables != null) + { + _environmentVariablePairs = new List>(EnvironmentVariables.Length); + + foreach (string entry in EnvironmentVariables) + { + string[] nameValuePair = entry.Split(s_equalsSplitter, 2); + + if (nameValuePair.Length == 1 || (nameValuePair.Length == 2 && nameValuePair[0].Length == 0)) + { + LogPrivate.LogErrorWithCodeFromResources("ToolTask.InvalidEnvironmentParameter", nameValuePair[0]); + return false; + } + + _environmentVariablePairs.Add(new KeyValuePair((object)nameValuePair[0], (object)nameValuePair[1])); + } + } + + // Assign standard stream logging importances + if (!AssignStandardStreamLoggingImportance()) + { + return false; + } + + try + { + if (SkipTaskExecution()) + { + // the task has said there's no command-line that we need to run, so + // return true to indicate this task completed successfully (without + // doing any actual work). + return true; + } + + string commandLineCommands = GenerateCommandLineCommands(); + // If there are response file commands, then we need a response file later. + string batchFileContents = commandLineCommands; + string responseFileCommands = GenerateResponseFileCommands(); + + if (UseCommandProcessor) + { + ToolExe = "cmd.exe"; + + // Generate the temporary batch file + // May throw IO-related exceptions + _temporaryBatchFile = FileUtilities.GetTemporaryFile(".cmd"); + + File.AppendAllText(_temporaryBatchFile, commandLineCommands, EncodingUtilities.CurrentSystemOemEncoding); + + string batchFileForCommandLine = _temporaryBatchFile; + + // If for some crazy reason the path has a & character and a space in it + // then get the short path of the temp path, which should not have spaces in it + // and then escape the & + if (batchFileForCommandLine.Contains("&") && !batchFileForCommandLine.Contains("^&")) + { + batchFileForCommandLine = NativeMethodsShared.GetShortFilePath(batchFileForCommandLine); + batchFileForCommandLine = batchFileForCommandLine.Replace("&", "^&"); + } + + commandLineCommands = "/C \"" + batchFileForCommandLine + "\""; + if (EchoOff) + { + commandLineCommands = "/Q " + commandLineCommands; + } + } + + // ensure the command line arguments string is not null + if ((commandLineCommands == null) || (commandLineCommands.Length == 0)) + { + commandLineCommands = String.Empty; + } + // add a leading space to the command line arguments (if any) to + // separate them from the tool path + else + { + commandLineCommands = " " + commandLineCommands; + } + + // Initialize the host object. At this point, the task may elect + // to not proceed. Compiler tasks do this for purposes of up-to-date + // checking in the IDE. + HostObjectInitializationStatus nextAction = InitializeHostObject(); + if (nextAction == HostObjectInitializationStatus.NoActionReturnSuccess) + { + return true; + } + else if (nextAction == HostObjectInitializationStatus.NoActionReturnFailure) + { + _exitCode = 1; + return HandleTaskExecutionErrors(); + } + + string pathToTool = ComputePathToTool(); + if (pathToTool == null) + { + // An appropriate error should have been logged already. + return false; + } + + // Log the environment. We do this up here, + // rather than later where the enviroment is set, + // so that it appears before the command line is logged. + bool alreadyLoggedEnvironmentHeader = false; + + // Old style environment overrides +#pragma warning disable 0618 // obsolete + StringDictionary envOverrides = EnvironmentOverride; + if (null != envOverrides) + { + foreach (DictionaryEntry entry in envOverrides) + { + alreadyLoggedEnvironmentHeader = LogEnvironmentVariable(alreadyLoggedEnvironmentHeader, (string)entry.Key, (string)entry.Value); + } +#pragma warning restore 0618 + } + + // New style environment overrides + if (_environmentVariablePairs != null) + { + foreach (KeyValuePair variable in _environmentVariablePairs) + { + alreadyLoggedEnvironmentHeader = LogEnvironmentVariable(alreadyLoggedEnvironmentHeader, (string)variable.Key, (string)variable.Value); + } + } + + if (UseCommandProcessor) + { + // Log that we are about to invoke the specified command. + LogToolCommand(pathToTool + commandLineCommands); + LogToolCommand(batchFileContents); + } + else + { + // Log that we are about to invoke the specified command. + LogToolCommand(pathToTool + commandLineCommands + " " + responseFileCommands); + } + _exitCode = 0; + + if (nextAction == HostObjectInitializationStatus.UseHostObjectToExecute) + { + // The hosting IDE passed in a host object to this task. Give the task + // a chance to call this host object to do the actual work. + try + { + if (!CallHostObjectToExecute()) + { + _exitCode = 1; + } + } + catch (Exception e) + { + LogPrivate.LogErrorFromException(e); + return false; + } + } + else + { + ErrorUtilities.VerifyThrow(nextAction == HostObjectInitializationStatus.UseAlternateToolToExecute, + "Invalid return status"); + + // No host object was provided, or at least not one that supports all of the + // switches/parameters we need. So shell out to the command-line tool. + _exitCode = ExecuteTool(pathToTool, responseFileCommands, commandLineCommands); + } + + // Raise a comment event to notify that the process completed + if (_terminatedTool) + { + return false; + } + else if (_exitCode != 0) + { + return HandleTaskExecutionErrors(); + } + else + { + return true; + } + } + catch (ArgumentException e) + { + if (!_terminatedTool) + { + LogPrivate.LogErrorWithCodeFromResources("General.InvalidToolSwitch", ToolExe, GetErrorMessageWithDiagnosticsCheck(e)); + } + return false; + } + catch (Win32Exception e) + { + if (!_terminatedTool) + { + LogPrivate.LogErrorWithCodeFromResources("ToolTask.CouldNotStartToolExecutable", ToolExe, GetErrorMessageWithDiagnosticsCheck(e)); + } + return false; + } + catch (IOException e) + { + if (!_terminatedTool) + { + LogPrivate.LogErrorWithCodeFromResources("ToolTask.CouldNotStartToolExecutable", ToolExe, GetErrorMessageWithDiagnosticsCheck(e)); + } + return false; + } + catch (UnauthorizedAccessException e) + { + if (!_terminatedTool) + { + LogPrivate.LogErrorWithCodeFromResources("ToolTask.CouldNotStartToolExecutable", ToolExe, GetErrorMessageWithDiagnosticsCheck(e)); + } + return false; + } + finally + { + // Clean up after ourselves. + if (_temporaryBatchFile != null && File.Exists(_temporaryBatchFile)) + { + File.Delete(_temporaryBatchFile); + } + } + } // Execute() + + /// + /// This method takes in an exception and if MSBuildDiagnostics is set then it will display the stack trace + /// if it is not set only the message will be displayed, this is to fix the problem where the user was getting + /// stack trace when a shorter message was better + /// + /// exception message + private string GetErrorMessageWithDiagnosticsCheck(Exception e) + { + // If MSBuildDiagnostics is set show stack trace information + if (Environment.GetEnvironmentVariable("MSBuildDiagnostics") != null) + { + // Includes stack trace + return e.ToString(); + } + else + { + // does not include stack trace + return e.Message; + } + } + + /// + /// Log a single environment variable that's about to be applied to the tool + /// + private bool LogEnvironmentVariable(bool alreadyLoggedEnvironmentHeader, string key, string value) + { + if (!alreadyLoggedEnvironmentHeader) + { + LogPrivate.LogMessageFromResources(MessageImportance.Low, "ToolTask.EnvironmentVariableHeader"); + alreadyLoggedEnvironmentHeader = true; + } + + Log.LogMessage(MessageImportance.Low, " {0}={1}", key, value); + + return alreadyLoggedEnvironmentHeader; + } + + #endregion + + #region Member data + + /// + /// An object to hold the event shutdown lock + /// + private object _eventCloseLock = new object(); + + /// + /// Splitter for environment variables + /// + private static char[] s_equalsSplitter = new char[] { '=' }; + + /// + /// Task Parameter: Override the importance at which standard out messages will be logged + /// + private string _standardOutputImportance = null; + + /// + /// Task Parameter: Override the importance at which standard error messages will be logged + /// + private string _standardErrorImportance = null; + + /// + /// Task Parameter: Should messages received on the standard error stream be logged as errros + /// + private bool _logStandardErrorAsError = false; + + /// + /// The actual importance at which standard out messages will be logged + /// + private MessageImportance _standardOutputImportanceToUse = MessageImportance.Low; + + /// + /// The actual importance at which standard error messages will be logged + /// + private MessageImportance _standardErrorImportanceToUse = MessageImportance.Normal; + + /// + /// Holds the stderr output from the tool. + /// + /// This collection is NOT thread-safe. + private Queue _standardErrorData; + + /// + /// Holds the stdout output from the tool. + /// + /// This collection is NOT thread-safe. + private Queue _standardOutputData; + + /// + /// Used for signalling when the tool writes to stderr. + /// + private ManualResetEvent _standardErrorDataAvailable; + + /// + /// Used for signalling when the tool writes to stdout. + /// + private ManualResetEvent _standardOutputDataAvailable; + + /// + /// Used for signalling when the tool exits. + /// + private ManualResetEvent _toolExited; + + /// + /// Set to true if the tool process was terminated, + /// either because the timeout was reached or it was canceled. + /// + private bool _terminatedTool; + + /// + /// Used for signalling when the tool times-out. + /// + private ManualResetEvent _toolTimeoutExpired; + + /// + /// Used for timing-out the tool. + /// + private Timer _toolTimer; + + /// + /// Used to support overriding the toolExe name. + /// + private string _toolExe = null; + + /// + /// Set when the events are about to be disposed, so that tardy + /// calls on the event handlers don't try to reset a disposed event + /// + private bool _eventsDisposed; + + /// + /// List of name, value pairs to be passed to the spawned tool's environment. + /// May be null. + /// Object is used instead of string to avoid NGen/JIT FXcop flagging. + /// + private List> _environmentVariablePairs; + + /// + /// Enumeration which indicates what kind of queue is being passed + /// + private enum StandardOutputOrErrorQueueType + { + StandardError = 0, + StandardOutput = 1 + } + + #endregion + } +} diff --git a/src/Utilities/TrackedDependencies/CanonicalTrackedFilesHelper.cs b/src/Utilities/TrackedDependencies/CanonicalTrackedFilesHelper.cs new file mode 100644 index 00000000000..0da5009ec87 --- /dev/null +++ b/src/Utilities/TrackedDependencies/CanonicalTrackedFilesHelper.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + internal static class CanonicalTrackedFilesHelper + { + internal const int MaxLogCount = 100; + + /// + /// Check that the given composite root contains all entries in the composite sub root + /// + /// The root to look for all sub roots in + /// The root that is comprised of subroots to look for + /// + internal static bool RootContainsAllSubRootComponents(string compositeRoot, string compositeSubRoot) + { + bool containsAllSubRoots = true; + + // If the two are identical, then clearly all keys are present + if (string.Compare(compositeRoot, compositeSubRoot, StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else + { + // look for each sub key in the main composite key + string[] rootComponents = compositeSubRoot.Split('|'); + foreach (string subRoot in rootComponents) + { + containsAllSubRoots &= compositeRoot.Contains(subRoot); + // we didn't find this subkey, so bail out + if (!containsAllSubRoots) + { + break; + } + } + } + return containsAllSubRoots; + } + + /// + /// This method checks that the specified files exist. During the scan the + /// most recent file write time of all the outputs is remembered. It will be + /// the basis for up to date comparisons. + /// + /// The files being checked for existence. + /// The TaskLoggingHelper used to log the nonexistent files. + /// Name of the most recently modified file. + /// Timestamp of the most recently modified file. + /// True if all members of 'files' exist, false otherwise + internal static bool FilesExistAndRecordNewestWriteTime(ITaskItem[] files, TaskLoggingHelper log, out DateTime outputNewestTime, out string outputNewestFilename) + { + return FilesExistAndRecordRequestedWriteTime(files, log, true /* return information about the newest file */, out outputNewestTime, out outputNewestFilename); + } + + /// + /// This method checks that the specified files exist. During the scan the + /// least recent file write time of all the outputs is remembered. It will be + /// the basis for up to date comparisons. + /// + /// The files being checked for existence. + /// The TaskLoggingHelper used to log the nonexistent files. + /// Name of the least recently modified file. + /// Timestamp of the least recently modified file. + /// True if all members of 'files' exist, false otherwise + internal static bool FilesExistAndRecordOldestWriteTime(ITaskItem[] files, TaskLoggingHelper log, out DateTime outputOldestTime, out string outputOldestFilename) + { + return FilesExistAndRecordRequestedWriteTime(files, log, false /* return information about the oldest file */, out outputOldestTime, out outputOldestFilename); + } + + private static bool FilesExistAndRecordRequestedWriteTime(ITaskItem[] files, TaskLoggingHelper log, bool getNewest, out DateTime requestedTime, out string requestedFilename) + { + bool allExist = true; + requestedTime = getNewest ? DateTime.MinValue : DateTime.MaxValue; + requestedFilename = String.Empty; + + // No output files for the source were tracked + // safely assume that this is becuase we didn't track them because they were'nt compiled + if ((files == null) || (files.Length == 0)) + { + allExist = false; + } + else + { + foreach (ITaskItem item in files) + { + DateTime lastWriteTime = NativeMethods.GetLastWriteTimeUtc(item.ItemSpec); + // If the file does not exist + if (lastWriteTime == DateTime.MinValue) + { + FileTracker.LogMessageFromResources(log, MessageImportance.Low, "Tracking_OutputDoesNotExist", item.ItemSpec); + allExist = false; + break; + } + else + { + if ( + (getNewest && lastWriteTime > requestedTime) || + (!getNewest && lastWriteTime < requestedTime) + ) + { + requestedTime = lastWriteTime; + requestedFilename = item.ItemSpec; + } + } + } + } + return allExist; + } + } +} diff --git a/src/Utilities/TrackedDependencies/CanonicalTrackedInputFiles.cs b/src/Utilities/TrackedDependencies/CanonicalTrackedInputFiles.cs new file mode 100644 index 00000000000..9baf642177f --- /dev/null +++ b/src/Utilities/TrackedDependencies/CanonicalTrackedInputFiles.cs @@ -0,0 +1,1168 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using System.IO; +using Microsoft.Build.Shared; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Utilities +{ + /// + /// This class is the filetracking log interpreter for .read. tracking logs in canonical form + /// or those that have been rooted (^) to make them canonical + /// + public class CanonicalTrackedInputFiles + { + #region Member Data + // The most recently modified output time + private DateTime _outputNewestTime = DateTime.MinValue; + // The most recently modified output + private string _outputNewest = ""; + // The table of dependencies + private Dictionary> _dependencyTable; + // The .read. tracking log files + private ITaskItem[] _tlogFiles; + // Primary source files + private ITaskItem[] _sourceFiles; + // The TaskLoggingHelper that we log progress to + private TaskLoggingHelper _log; + // Sources needing compilation + private ITaskItem[] _sourcesNeedingCompilation = null; + // The output graph + private CanonicalTrackedOutputFiles _outputs; + // Output files for all sources in the current set as a group + private ITaskItem[] _outputFileGroup; + // Output files that are manually specified + private ITaskItem[] _outputFiles; + // Use minimal rebuild optimization (WARNING: this may cause underbuild) + private bool _useMinimalRebuildOptimization = false; + // Are the tracking logs that we were constructed with actually available + private bool _tlogAvailable; + // Do we want to keep composite rooting markers around (many-to-one case) or + // shred them (one-to-one or one-to-many case) + private bool _maintainCompositeRootingMarkers = false; + // The set of paths that contain files that are to be ignored during up to date check + private HashSet _excludedInputPaths = new HashSet(StringComparer.Ordinal); + // Cache of last write times + private ConcurrentDictionary _lastWriteTimeCache = new ConcurrentDictionary(StringComparer.Ordinal); + #endregion + + #region Properties + + // This is provided to faciltate unit testing + internal ITaskItem[] SourcesNeedingCompilation + { + get { return _sourcesNeedingCompilation; } + set { _sourcesNeedingCompilation = value; } + } + + // Provide external access to the dependencyTable + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] + public Dictionary> DependencyTable + { + get { return _dependencyTable; } + } + #endregion + + #region Constructors + /// + /// Constructor for multiple input source files + /// + /// The .read. tlog files to interpret + /// The primary source files to interpret dependencies for + /// The output files produced by compiling this set of sources + /// WARNING: Minimal rebuild optimization requires 100% accurate computed outputs to be specified! + /// True to keep composite rooting markers around (many-to-one case) or false to shred them (one-to-one or one-to-many case) + public CanonicalTrackedInputFiles(ITaskItem[] tlogFiles, ITaskItem[] sourceFiles, CanonicalTrackedOutputFiles outputs, bool useMinimalRebuildOptimization, bool maintainCompositeRootingMarkers) + { + InternalConstruct(null, tlogFiles, sourceFiles, null, null, outputs, useMinimalRebuildOptimization, maintainCompositeRootingMarkers); + } + + /// + /// Constructor for multiple input source files + /// + /// The .read. tlog files to interpret + /// The primary source files to interpret dependencies for + /// The set of paths that contain files that are to be ignored during up to date check + /// The output files produced by compiling this set of sources + /// WARNING: Minimal rebuild optimization requires 100% accurate computed outputs to be specified! + /// True to keep composite rooting markers around (many-to-one case) or false to shred them (one-to-one or one-to-many case) + public CanonicalTrackedInputFiles(ITaskItem[] tlogFiles, ITaskItem[] sourceFiles, ITaskItem[] excludedInputPaths, CanonicalTrackedOutputFiles outputs, bool useMinimalRebuildOptimization, bool maintainCompositeRootingMarkers) + { + InternalConstruct(null, tlogFiles, sourceFiles, null, excludedInputPaths, outputs, useMinimalRebuildOptimization, maintainCompositeRootingMarkers); + } + + /// + /// Constructor for multiple input source files + /// + /// The task that is using file tracker + /// The .read. tlog files to interpret + /// The primary source files to interpret dependencies for + /// The set of paths that contain files that are to be ignored during up to date check + /// The output files produced by compiling this set of sources + /// WARNING: Minimal rebuild optimization requires 100% accurate computed outputs to be specified! + /// True to keep composite rooting markers around (many-to-one case) or false to shred them (one-to-one or one-to-many case) + public CanonicalTrackedInputFiles(ITask ownerTask, ITaskItem[] tlogFiles, ITaskItem[] sourceFiles, ITaskItem[] excludedInputPaths, CanonicalTrackedOutputFiles outputs, bool useMinimalRebuildOptimization, bool maintainCompositeRootingMarkers) + { + InternalConstruct(ownerTask, tlogFiles, sourceFiles, null, excludedInputPaths, outputs, useMinimalRebuildOptimization, maintainCompositeRootingMarkers); + } + + /// + /// Constructor for multiple input source files + /// + /// The task that is using file tracker + /// The .read. tlog files to interpret + /// The primary source files to interpret dependencies for + /// The set of paths that contain files that are to be ignored during up to date check + /// The output files produced by compiling this set of sources + /// WARNING: Minimal rebuild optimization requires 100% accurate computed outputs to be specified! + /// True to keep composite rooting markers around (many-to-one case) or false to shred them (one-to-one or one-to-many case) + public CanonicalTrackedInputFiles(ITask ownerTask, ITaskItem[] tlogFiles, ITaskItem[] sourceFiles, ITaskItem[] excludedInputPaths, ITaskItem[] outputs, bool useMinimalRebuildOptimization, bool maintainCompositeRootingMarkers) + { + InternalConstruct(ownerTask, tlogFiles, sourceFiles, outputs, excludedInputPaths, null, useMinimalRebuildOptimization, maintainCompositeRootingMarkers); + } + + /// + /// Constructor for a single input source file + /// + /// The task that is using file tracker + /// The .read. tlog files to interpret + /// The primary source file to interpret dependencies for + /// The set of paths that contain files that are to be ignored during up to date check + /// The output files produced by compiling this source + /// WARNING: Minimal rebuild optimization requires 100% accurate computed outputs to be specified! + /// True to keep composite rooting markers around (many-to-one case) or false to shred them (one-to-one or one-to-many case) + public CanonicalTrackedInputFiles(ITask ownerTask, ITaskItem[] tlogFiles, ITaskItem sourceFile, ITaskItem[] excludedInputPaths, CanonicalTrackedOutputFiles outputs, bool useMinimalRebuildOptimization, bool maintainCompositeRootingMarkers) + { + ITaskItem[] sourceFiles = new ITaskItem[] { sourceFile }; + InternalConstruct(ownerTask, tlogFiles, sourceFiles, null, excludedInputPaths, outputs, useMinimalRebuildOptimization, maintainCompositeRootingMarkers); + } + + /// + /// Common internal constructor + /// + /// The task that is using file tracker + /// The .read. tlog files to interpret + /// The primary source files to interpret dependencies for + /// The output files produced by compiling this set of sources + /// The set of paths that contain files that are to be ignored during up to date check + /// WARNING: Minimal rebuild optimization requires 100% accurate computed outputs to be specified! + /// True to keep composite rooting markers around (many-to-one case) or false to shred them (one-to-one or one-to-many case) + private void InternalConstruct(ITask ownerTask, ITaskItem[] tlogFiles, ITaskItem[] sourceFiles, ITaskItem[] outputFiles, ITaskItem[] excludedInputPaths, CanonicalTrackedOutputFiles outputs, bool useMinimalRebuildOptimization, bool maintainCompositeRootingMarkers) + { + if (ownerTask != null) + { + _log = new TaskLoggingHelper(ownerTask); + _log.TaskResources = AssemblyResources.PrimaryResources; + _log.HelpKeywordPrefix = "MSBuild."; + } + + _tlogFiles = TrackedDependencies.ExpandWildcards(tlogFiles); + _tlogAvailable = TrackedDependencies.ItemsExist(_tlogFiles); + _sourceFiles = sourceFiles; + _outputs = outputs; + _outputFiles = outputFiles; + _useMinimalRebuildOptimization = useMinimalRebuildOptimization; + _maintainCompositeRootingMarkers = maintainCompositeRootingMarkers; + + if (excludedInputPaths != null) + { + // Assign our exclude paths to our lookup + foreach (ITaskItem excludePath in excludedInputPaths) + { + string fullexcludePath = FileUtilities.EnsureNoTrailingSlash(FileUtilities.NormalizePath(excludePath.ItemSpec)).ToUpperInvariant(); + _excludedInputPaths.Add(fullexcludePath); + } + } + + _dependencyTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (_tlogFiles != null) + { + ConstructDependencyTable(); + } + } + #endregion + + #region Methods + /// + /// This method computes the sources that need to be compiled based on the output files and the + /// full dependency graph of inputs + /// + /// Array of files that need to be compiled + public ITaskItem[] ComputeSourcesNeedingCompilation() + { + return ComputeSourcesNeedingCompilation(true); + } + + /// + /// This method computes the sources that need to be compiled based on the output files and the + /// full dependency graph of inputs, optionally searching composite rooting markers + /// for subroots that may contain input files + /// + /// Array of files that need to be compiled + public ITaskItem[] ComputeSourcesNeedingCompilation(bool searchForSubRootsInCompositeRootingMarkers) + { + if (_outputFiles != null) + { + _outputFileGroup = _outputFiles; + } + else if (_sourceFiles != null && _outputs != null && _maintainCompositeRootingMarkers) + { + _outputFileGroup = _outputs.OutputsForSource(_sourceFiles, searchForSubRootsInCompositeRootingMarkers); + } + else if (_sourceFiles != null && _outputs != null) + { + _outputFileGroup = _outputs.OutputsForNonCompositeSource(_sourceFiles); + } + + if (_maintainCompositeRootingMarkers) + { + return ComputeSourcesNeedingCompilationFromCompositeRootingMarker(searchForSubRootsInCompositeRootingMarkers); + } + else + { + return ComputeSourcesNeedingCompilationFromPrimaryFiles(); + } + } + + /// + /// This method computes the sources that need to be compiled based on the output files and the + /// full dependency graph of inputs, making the assumption that the source files are all primary + /// files -- ie. there is either a one-to-one or a one-to-many correspondence between inputs + /// and outputs + /// + /// Array of files that need to be compiled + internal ITaskItem[] ComputeSourcesNeedingCompilationFromPrimaryFiles() + { + if (_sourcesNeedingCompilation == null) + { + ConcurrentQueue sourcesNeedingCompilationList = new ConcurrentQueue(); + bool allOutputFilesExist = false; + + if (_tlogAvailable) + { + if (!_useMinimalRebuildOptimization) + { + allOutputFilesExist = FilesExistAndRecordNewestWriteTime(_outputFileGroup); + } + } + + // If the TLOG file is not available, or not up to date then add source to sourcesNeedingCompilationList + Parallel.For(0, _sourceFiles.Length, (index) => + { + CheckIfSourceNeedsCompilation(sourcesNeedingCompilationList, allOutputFilesExist, _sourceFiles[index]); + }); + _sourcesNeedingCompilation = sourcesNeedingCompilationList.ToArray(); + } + + if (_sourcesNeedingCompilation.Length == 0) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Normal, "Tracking_AllOutputsAreUpToDate"); + _sourcesNeedingCompilation = new ITaskItem[0]; + } + else + { + Array.Sort(_sourcesNeedingCompilation, new Comparison(CompareTaskItems)); + foreach (ITaskItem compileSource in _sourcesNeedingCompilation) + { + string modifiedPath = compileSource.GetMetadata("_trackerModifiedPath"); + string modifiedTime = compileSource.GetMetadata("_trackerModifiedTime"); + string outputFilePath = compileSource.GetMetadata("_trackerOutputFile"); + string trackerCompileReason = compileSource.GetMetadata("_trackerCompileReason"); + + if (String.Equals(trackerCompileReason, "Tracking_SourceWillBeCompiledDependencyWasModifiedAt", StringComparison.Ordinal)) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, trackerCompileReason, compileSource.ItemSpec, modifiedPath, modifiedTime); + } + else if (String.Equals(trackerCompileReason, "Tracking_SourceWillBeCompiledMissingDependency", StringComparison.Ordinal)) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, trackerCompileReason, compileSource.ItemSpec, modifiedPath); + } + else if (String.Equals(trackerCompileReason, "Tracking_SourceWillBeCompiledOutputDoesNotExist", StringComparison.Ordinal)) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, trackerCompileReason, compileSource.ItemSpec, outputFilePath); + } + else + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, trackerCompileReason, compileSource.ItemSpec); + } + + // Now zero out the metadata that was set, so that it doesn't show up if these items + // flow through the task + compileSource.RemoveMetadata("_trackerModifiedPath"); + compileSource.RemoveMetadata("_trackerModifiedTime"); + compileSource.RemoveMetadata("_trackerOutputFile"); + compileSource.RemoveMetadata("_trackerCompileReason"); + } + } + + return _sourcesNeedingCompilation; + } + + /// + /// Check to see if the source specified needs compilation relative to its outputs + /// + private void CheckIfSourceNeedsCompilation(ConcurrentQueue sourcesNeedingCompilationList, bool allOutputFilesExist, ITaskItem source) + { + if ((!_tlogAvailable) || (_outputFileGroup == null)) + { + source.SetMetadata("_trackerCompileReason", "Tracking_SourceWillBeCompiledAsNoTrackingLog"); + sourcesNeedingCompilationList.Enqueue(source); + } + else if (!_useMinimalRebuildOptimization && !allOutputFilesExist) + { + source.SetMetadata("_trackerCompileReason", "Tracking_SourceOutputsNotAvailable"); + sourcesNeedingCompilationList.Enqueue(source); + } + else if (!IsUpToDate(source)) + { + if (String.IsNullOrEmpty(source.GetMetadata("_trackerCompileReason"))) + { + source.SetMetadata("_trackerCompileReason", "Tracking_SourceWillBeCompiled"); + } + + sourcesNeedingCompilationList.Enqueue(source); + } + else if (!_useMinimalRebuildOptimization && _outputNewestTime == DateTime.MinValue) + { + source.SetMetadata("_trackerCompileReason", "Tracking_SourceNotInTrackingLog"); + sourcesNeedingCompilationList.Enqueue(source); + } + } + + /// + /// A very simple comparer for TaskItems so that up to date check results can be sorted. + /// + private int CompareTaskItems(ITaskItem left, ITaskItem right) + { + return String.Compare(left.ItemSpec, right.ItemSpec, StringComparison.Ordinal); + } + + /// + /// This method computes the sources that need to be compiled based on the output files and the + /// full dependency graph of inputs, making the assumption that the source files are the components + /// of a composite rooting marker, as in the case where there is a many-to-one correspondence + /// between inputs and outputs. + /// + /// Array of files that need to be compiled + internal ITaskItem[] ComputeSourcesNeedingCompilationFromCompositeRootingMarker(bool searchForSubRootsInCompositeRootingMarkers) + { + // We need to find all the source dependencies for the outputs + // Because we are assuming that this is a many-to-one situation, we need to + // build a composite rooting marker from the source files and then look through + // the dependency table to discover the dependencies of those sources. + // + // If any of the dependencies are newer than the outputs, then a rebuild is required. + + Dictionary sourcesNeedingCompilation = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // There were no tlogs available, that means we need to build + if (!_tlogAvailable) + { + return _sourceFiles; + } + + // Construct a rooting marker from the set of sources + string upperSourcesRoot = FileTracker.FormatRootingMarker(_sourceFiles); + List sourcesNeedingCompilationList = new List(); + + // Check each root in the table to see if it matches. + foreach (string tableEntryRoot in _dependencyTable.Keys) + { + string upperTableEntryRoot = tableEntryRoot.ToUpperInvariant(); + + if (searchForSubRootsInCompositeRootingMarkers) + { + if (upperTableEntryRoot.Contains(upperSourcesRoot) || + CanonicalTrackedFilesHelper.RootContainsAllSubRootComponents(upperSourcesRoot, upperTableEntryRoot)) + { + // Gather the unique outputs for this root + SourceDependenciesForOutputRoot(sourcesNeedingCompilation, upperTableEntryRoot, _outputFileGroup); + } + } + else + { + if (upperTableEntryRoot.Equals(upperSourcesRoot, StringComparison.Ordinal)) + { + // Gather the unique outputs for this root + SourceDependenciesForOutputRoot(sourcesNeedingCompilation, upperTableEntryRoot, _outputFileGroup); + } + } + } + + // There were no outputs for the requested root + if (sourcesNeedingCompilation.Count == 0) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_DependenciesForRootNotFound", upperSourcesRoot); + return _sourceFiles; + } + + // We have our set of outputs, construct our array + sourcesNeedingCompilationList.AddRange(sourcesNeedingCompilation.Values); + ITaskItem[] sourcesNeedingCompilationArray = sourcesNeedingCompilationList.ToArray(); + + // now that we have our dependencies, we need to check if any of them are newer than the outputs. + DateTime newestSourceDependencyTime; + DateTime oldestOutputTime; + string newestSourceDependencyFile = String.Empty; + string oldestOutputFile = String.Empty; + + if ( + CanonicalTrackedFilesHelper.FilesExistAndRecordNewestWriteTime(sourcesNeedingCompilationArray, _log, out newestSourceDependencyTime, out newestSourceDependencyFile) && + CanonicalTrackedFilesHelper.FilesExistAndRecordOldestWriteTime(_outputFileGroup, _log, out oldestOutputTime, out oldestOutputFile) + ) + { + if (newestSourceDependencyTime <= oldestOutputTime) + { + // All sources and outputs exist, and the oldest output is newer than the newest input -- we're up to date! + FileTracker.LogMessageFromResources(_log, MessageImportance.Normal, "Tracking_AllOutputsAreUpToDate"); + return new ITaskItem[0]; + } + } + + // Too much logging leads to poor performance + if (sourcesNeedingCompilation.Count > CanonicalTrackedFilesHelper.MaxLogCount) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_InputsNotShown", sourcesNeedingCompilation.Count); + } + else + { + // We have our set of outputs, log the details + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_InputsFor", upperSourcesRoot); + + foreach (ITaskItem inputItem in sourcesNeedingCompilationArray) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t" + inputItem); + } + } + + // Log the reasons that we're not up to date + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_InputNewerThanOutput", newestSourceDependencyFile, oldestOutputFile); + + return _sourceFiles; + } + + /// + /// Given a composite output rooting marker, gathers up all the sources it depends on. + /// + /// List of outputs to populate + /// The source to gather outputs for + private void SourceDependenciesForOutputRoot(Dictionary sourceDependencies, string sourceKey, ITaskItem[] filesToIgnore) + { + Dictionary dependencies; + bool ignoreDependentFile; + bool thereAreFilesToIgnore = filesToIgnore != null && filesToIgnore.Length > 0; + + if (_dependencyTable.TryGetValue(sourceKey, out dependencies)) + { + foreach (string dependee in dependencies.Keys) + { + ignoreDependentFile = false; + + if (thereAreFilesToIgnore) + { + // This is probably OK, because "filesToIgnore" is expected to be small + foreach (ITaskItem fileToIgnore in filesToIgnore) + { + if (String.Equals(dependee, fileToIgnore.ItemSpec, StringComparison.OrdinalIgnoreCase)) + { + // don't add this file to the dependency list + ignoreDependentFile = true; + break; + } + } + } + + // add the dependency if it is not already in the dictionary and if + // it's not in the group of files we're explicitly ignoring + ITaskItem existingTaskItem; + if (!ignoreDependentFile && !sourceDependencies.TryGetValue(dependee, out existingTaskItem)) + { + sourceDependencies.Add(dependee, new TaskItem(dependee)); + } + } + } + } + + /// + /// Check if the source file needs to be compiled + /// + /// The primary dependency + /// bool + private bool IsUpToDate(ITaskItem sourceFile) + { + string sourceFullPath = FileUtilities.NormalizePath(sourceFile.ItemSpec); + Dictionary dependencies; + bool dependenciesAvailable = _dependencyTable.TryGetValue(sourceFullPath, out dependencies); + DateTime thisSourceOutputNewestTime = _outputNewestTime; + + if ((_useMinimalRebuildOptimization) && (_outputs != null) && dependenciesAvailable) + { + Dictionary outputFiles; + + thisSourceOutputNewestTime = DateTime.MinValue; + // Missing outputs from the graph means that the source is out of date + if (_outputs.DependencyTable.TryGetValue(sourceFullPath, out outputFiles)) + { + DateTime sourceTime = NativeMethods.GetLastWriteTimeUtc(sourceFullPath); + + foreach (string outputFile in outputFiles.Keys) + { + DateTime outputFileTime = NativeMethods.GetLastWriteTimeUtc(outputFile); + // If the file exists + if (outputFileTime > DateTime.MinValue) + { + if (outputFileTime < sourceTime) + { + sourceFile.SetMetadata("_trackerCompileReason", "Tracking_SourceWillBeCompiledDependencyWasModifiedAt"); + sourceFile.SetMetadata("_trackerModifiedPath", sourceFullPath); + sourceFile.SetMetadata("_trackerModifiedTime", sourceTime.ToLocalTime().ToString()); + return false; + } + else if (outputFileTime > thisSourceOutputNewestTime) + { + thisSourceOutputNewestTime = outputFileTime; + } + } + else + { + sourceFile.SetMetadata("_trackerCompileReason", "Tracking_SourceWillBeCompiledOutputDoesNotExist"); + sourceFile.SetMetadata("_trackerOutputFile", outputFile); + return false; + } + } + } + else + { + sourceFile.SetMetadata("_trackerCompileReason", "Tracking_SourceOutputsNotAvailable"); + return false; + } + } + + if (dependenciesAvailable) + { + foreach (string file in dependencies.Keys) + { + // The file that we are encountering in the dependencies may be excluded from + // the dependency check, if so we don't want to go checking it + if (!FileIsExcludedFromDependencyCheck(file)) + { + // If the file tracked during the build exists, then do a time-stamp check on it + // to determine up-to-dateness + DateTime dependeeTime = DateTime.MinValue; + if (!_lastWriteTimeCache.TryGetValue(file, out dependeeTime)) + { + dependeeTime = NativeMethods.GetLastWriteTimeUtc(file); + _lastWriteTimeCache[file] = dependeeTime; + } + + // If the file exists + if (dependeeTime > DateTime.MinValue) + { + if (dependeeTime > thisSourceOutputNewestTime) + { + sourceFile.SetMetadata("_trackerCompileReason", "Tracking_SourceWillBeCompiledDependencyWasModifiedAt"); + sourceFile.SetMetadata("_trackerModifiedPath", file); + sourceFile.SetMetadata("_trackerModifiedTime", dependeeTime.ToLocalTime().ToString()); + return false; + } + } + else // if the file no longer exists, then assume we are out of date and cause a compile + { + sourceFile.SetMetadata("_trackerCompileReason", "Tracking_SourceWillBeCompiledMissingDependency"); + sourceFile.SetMetadata("_trackerModifiedPath", file); + return false; + } + } + } + } + else + { + sourceFile.SetMetadata("_trackerCompileReason", "Tracking_SourceNotInTrackingLog"); + return false; + } + // It appears that all our dependencies are earlier than the outputs + // So no need to compile + return true; + } + + /// + /// Test to see if the specified file is excluded from tracked dependency checking + /// + /// + /// Full path of the file to test + /// + public bool FileIsExcludedFromDependencyCheck(string fileName) + { + string fileDirectoryName = FileUtilities.GetDirectoryNameOfFullPath(fileName); + + if (_excludedInputPaths.Contains(fileDirectoryName)) + { + return true; + } + else + { + return false; + } + } + + private bool FilesExistAndRecordNewestWriteTime(ITaskItem[] files) + { + return CanonicalTrackedFilesHelper.FilesExistAndRecordNewestWriteTime(files, _log, out _outputNewestTime, out _outputNewest); + } + + /// + /// Construct our dependency table for our source files. + /// + private void ConstructDependencyTable() + { + string tLogRootingMarker = null; + try + { + // construct a rooting marker from the tlog files + tLogRootingMarker = DependencyTableCache.FormatNormalizedTlogRootingMarker(_tlogFiles); + } + catch (ArgumentException e) + { + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLog", e.Message); + return; + } + + // Record the current directory (which under normal circumstances will be the project directory) + // so that we can compare tracked paths against it for inclusion in the dependency graph + string currentProjectDirectory = FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()); + + if (!_tlogAvailable) + { + foreach (ITaskItem tlogFileName in _tlogFiles) + { + if (!FileUtilities.FileExistsNoThrow(tlogFileName.ItemSpec)) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_SingleLogFileNotAvailable", tlogFileName.ItemSpec); + } + } + + lock (DependencyTableCache.DependencyTable) + { + // The tracking logs are not available, they may have been deleted at some point. + // Be safe and remove any references from the cache. + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + } + return; + } + + DependencyTableCacheEntry cachedEntry = null; + lock (DependencyTableCache.DependencyTable) + { + // Look in the dependency table cache to see if its available and up to date + cachedEntry = DependencyTableCache.GetCachedEntry(tLogRootingMarker); + } + + // We have an up to date cached entry + if (cachedEntry != null) + { + _dependencyTable = (Dictionary>)cachedEntry.DependencyTable; + // Log information about what we're using + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_ReadTrackingCached"); + foreach (ITaskItem tlogItem in cachedEntry.TlogFiles) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t{0}", tlogItem.ItemSpec); + } + return; + } + + // Now we need to construct a dependency table for the primary sources from the TLOG files + // If there are any errors in the tlogs, we want to warn, stop parsing tlogs, and empty + // out the dependency table, essentially forcing a rebuild. + bool encounteredInvalidTLogContents = false; + bool exceptionCaught = false; + string invalidTLogName = null; + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_ReadTrackingLogs"); + foreach (ITaskItem tlogFileName in _tlogFiles) + { + try + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t{0}", tlogFileName.ItemSpec); + + using (StreamReader tlog = File.OpenText(tlogFileName.ItemSpec)) + { + string tlogEntry = tlog.ReadLine(); + + while (tlogEntry != null) + { + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + + if (tlogEntry[0] != '#') // command marker + { + bool rootingRecord = false; + // If this is a rooting record, remove the rooting marker + if (tlogEntry[0] == '^') + { + tlogEntry = tlogEntry.Substring(1); + + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + + rootingRecord = true; + } + + // found one of our primary sources + if (rootingRecord) + { + // dependency table for the source file + Dictionary dependencies; + Dictionary primaryFiles; + + if (!_maintainCompositeRootingMarkers) + { + primaryFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (tlogEntry.Contains("|")) + { + foreach (ITaskItem file in _sourceFiles) + { + if (!primaryFiles.ContainsKey(FileUtilities.NormalizePath(file.ItemSpec))) + { + primaryFiles.Add(FileUtilities.NormalizePath(file.ItemSpec), null); + } + } + } + else + { + primaryFiles.Add(tlogEntry, null); + } + } + else + { + primaryFiles = null; + } + + // We haven't seen this source before in the tracking log + // so create a new dependency table and add the source file(s) + if (!_dependencyTable.TryGetValue(tlogEntry, out dependencies)) + { + dependencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (!_maintainCompositeRootingMarkers) + { + dependencies.Add(tlogEntry, null); + } + + _dependencyTable.Add(tlogEntry, dependencies); + } + + tlogEntry = tlog.ReadLine(); + + if (_maintainCompositeRootingMarkers) + { + // Process each file encountered until we reach: + // the end of the or, + // A command marker or, + // we hit a rooting marker + while (tlogEntry != null) + { + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + else if ((tlogEntry[0] != '#') && (tlogEntry[0] != '^')) + { + if (!dependencies.ContainsKey(tlogEntry)) + { + if (FileTracker.FileIsUnderPath(tlogEntry, currentProjectDirectory) || !FileTracker.FileIsExcludedFromDependencies(tlogEntry)) + { + dependencies.Add(tlogEntry, null); + } + } + } + else + { + break; + } + + tlogEntry = tlog.ReadLine(); + } + } + else + { + while (tlogEntry != null) + { + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + else if (tlogEntry[0] != '#' && tlogEntry[0] != '^') + { + if (primaryFiles.ContainsKey(tlogEntry)) + { + // if this is a primary file, we need to add it to the dependency table, and we need + // to reset "dependencies" so that the following dependencies get written into this + // primary file's table instead of the previous one. + if (!_dependencyTable.TryGetValue(tlogEntry, out dependencies)) + { + dependencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + dependencies.Add(tlogEntry, null); + + _dependencyTable.Add(tlogEntry, dependencies); + } + } + else if (!dependencies.ContainsKey(tlogEntry)) + { + // however, if it's not a primary file, just add it to the current dependency table + if (FileTracker.FileIsUnderPath(tlogEntry, currentProjectDirectory) || !FileTracker.FileIsExcludedFromDependencies(tlogEntry)) + { + dependencies.Add(tlogEntry, null); + } + } + } + else + { + break; + } + + + tlogEntry = tlog.ReadLine(); + } + } + } + else // don't know what this entry is, so skip it + { + tlogEntry = tlog.ReadLine(); + } + } + else // skip over the initial '#' line + { + tlogEntry = tlog.ReadLine(); + } + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLog", e.Message); + break; + } + + if (encounteredInvalidTLogContents) + { + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLogContents", invalidTLogName); + break; + } + } + + lock (DependencyTableCache.DependencyTable) + { + // There were problems with the tracking logs -- we've already warned or errored; now we want to make + // sure that we essentially force a rebuild of this particular root. + if (encounteredInvalidTLogContents || exceptionCaught) + { + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + + _dependencyTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + else + { + // Record the newly built dependency table in the cache + DependencyTableCache.DependencyTable[tLogRootingMarker] = new DependencyTableCacheEntry(_tlogFiles, _dependencyTable); + } + } + } + + /// + /// This method will re-write the tlogs from the current output table new entries will + /// be tracked. + /// + public void SaveTlog() + { + SaveTlog(null); + } + + /// + /// This method will re-write the tlogs from the current dependency. As the sources are compiled, + /// new entries willbe tracked. + /// + /// + /// Delegate used to determine whether a particular file should + /// be included in the compacted tlog. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public void SaveTlog(DependencyFilter includeInTLog) + { + // If there are no tlog files, then this will be a clean build + // so there is no need to write a new tlog + if (_tlogFiles != null && (_tlogFiles.Length > 0)) + { + string tLogRootingMarker = DependencyTableCache.FormatNormalizedTlogRootingMarker(_tlogFiles); + + lock (DependencyTableCache.DependencyTable) + { + // The tracking logs in the cache will be invalidated by this compaction + // remove the cached entries + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + } + + string firstTlog = _tlogFiles[0].ItemSpec; + + // empty all tlogs + foreach (ITaskItem tlogFile in _tlogFiles) + { + File.WriteAllText(tlogFile.ItemSpec, "", System.Text.Encoding.Unicode); + } + + // Write out the remaining dependency information as a new tlog + using (StreamWriter inputs = new StreamWriter(firstTlog, false, System.Text.Encoding.Unicode)) + { + if (!_maintainCompositeRootingMarkers) + { + foreach (string primaryFile in _dependencyTable.Keys) + { + if (!primaryFile.Contains("|")) // composite roots are not needed + { + Dictionary dependencies = _dependencyTable[primaryFile]; + inputs.WriteLine("^" + primaryFile); + foreach (string file in dependencies.Keys) + { + // We only want to write the tlog entry if it isn't the primary file + // and we aren't being asked to filter it out + if (file != primaryFile && (includeInTLog == null || includeInTLog(file))) + { + inputs.WriteLine(file); + } + } + } + } + } + else + { + // Just output the rooting markers and their dependencies -- we don't want to + // compact out the composite ones. + foreach (string rootingMarker in _dependencyTable.Keys) + { + Dictionary dependencies = _dependencyTable[rootingMarker]; + inputs.WriteLine("^" + rootingMarker); + foreach (string file in dependencies.Keys) + { + // Give the task a chance to filter dependencies out of the written TLog + if (includeInTLog == null || includeInTLog(file)) + { + // Write out the entry + inputs.WriteLine(file); + } + } + } + } + } + } + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Source that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveEntriesForSource(ITaskItem source) + { + RemoveEntriesForSource(new ITaskItem[] { source }); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveEntriesForSource(ITaskItem[] source) + { + // construct a root marker for the sources and outputs to remove from the graph + string rootMarkerToRemove = FileTracker.FormatRootingMarker(source); + + // remove the entry from the graph for the combined root + _dependencyTable.Remove(rootMarkerToRemove); + + // remove the entry for each source item + foreach (ITaskItem sourceItem in source) + { + _dependencyTable.Remove(FileUtilities.NormalizePath(sourceItem.ItemSpec)); + } + } + + /// + /// Remove the entry in the input dependency graph corresponding to the rooting marker + /// passed in. + /// + /// The root to remove + public void RemoveEntryForSourceRoot(string rootingMarker) + { + _dependencyTable.Remove(rootingMarker); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + public void RemoveDependencyFromEntry(ITaskItem[] sources, ITaskItem dependencyToRemove) + { + string rootingMarker = FileTracker.FormatRootingMarker(sources); + RemoveDependencyFromEntry(rootingMarker, dependencyToRemove); + } + + /// + /// Remove the output graph entries for the given source and corresponding outputs + /// + /// Source that should be removed from the graph + public void RemoveDependencyFromEntry(ITaskItem source, ITaskItem dependencyToRemove) + { + string rootingMarker = FileTracker.FormatRootingMarker(source); + RemoveDependencyFromEntry(rootingMarker, dependencyToRemove); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + private void RemoveDependencyFromEntry(string rootingMarker, ITaskItem dependencyToRemove) + { + // construct a root marker for the source that will remove the dependency from + Dictionary dependencies; + + if (_dependencyTable.TryGetValue(rootingMarker, out dependencies)) + { + dependencies.Remove(FileUtilities.NormalizePath(dependencyToRemove.ItemSpec)); + } + else + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Normal, "Tracking_ReadLogEntryNotFound", rootingMarker); + } + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Source that should be removed from the graph + public void RemoveDependenciesFromEntryIfMissing(ITaskItem source) + { + RemoveDependenciesFromEntryIfMissing(new ITaskItem[] { source }, null); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Source that should be removed from the graph + /// Output that correspond ot the sources (used for same file processing) + public void RemoveDependenciesFromEntryIfMissing(ITaskItem source, ITaskItem correspondingOutput) + { + RemoveDependenciesFromEntryIfMissing(new ITaskItem[] { source }, new ITaskItem[] { correspondingOutput }); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + public void RemoveDependenciesFromEntryIfMissing(ITaskItem[] source) + { + RemoveDependenciesFromEntryIfMissing(source, null); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveDependenciesFromEntryIfMissing(ITaskItem[] source, ITaskItem[] correspondingOutputs) + { + if (correspondingOutputs != null) + { + ErrorUtilities.VerifyThrowArgument(source.Length == correspondingOutputs.Length, "Tracking_SourcesAndCorrespondingOutputMismatch"); + } + + // construct a combined root marker for the sources and outputs to remove from the graph + string rootingMarker = FileTracker.FormatRootingMarker(source, correspondingOutputs); + + RemoveDependenciesFromEntryIfMissing(rootingMarker); + + // Remove entries for each individual source + for (int sourceIndex = 0; sourceIndex < source.Length; sourceIndex++) + { + if (correspondingOutputs != null) + { + rootingMarker = FileTracker.FormatRootingMarker(source[sourceIndex], correspondingOutputs[sourceIndex]); + } + else + { + rootingMarker = FileTracker.FormatRootingMarker(source[sourceIndex]); + } + + RemoveDependenciesFromEntryIfMissing(rootingMarker); + } + } + + /// + /// Remove the output graph entries for the given rooting marker + /// + /// + private void RemoveDependenciesFromEntryIfMissing(string rootingMarker) + { + Dictionary dependencies; + + // In the event of incomplete tracking information (i.e. this root was not present), just continue quietly + // as the user could have killed the tool being tracked, or another error occured during its execution. + if (_dependencyTable.TryGetValue(rootingMarker, out dependencies)) + { + Dictionary dependenciesWithoutMissingFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + int keyIndex = 0; + + foreach (string file in dependencies.Keys) + { + if (keyIndex++ > 0) + { + // If we are ignoring missing files, then only record those that exist + if (FileUtilities.FileExistsNoThrow(file)) + { + dependenciesWithoutMissingFiles.Add(file, dependencies[file]); + } + } + else + { + dependenciesWithoutMissingFiles.Add(file, file); + } + } + + _dependencyTable[rootingMarker] = dependenciesWithoutMissingFiles; + } + } + #endregion + } +} diff --git a/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs b/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs new file mode 100644 index 00000000000..77db40d02d6 --- /dev/null +++ b/src/Utilities/TrackedDependencies/CanonicalTrackedOutputFiles.cs @@ -0,0 +1,889 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.IO; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// This class is the filetracking log interpreter for .write. tracking logs in canonical form + /// Canoncial .write. logs need to be rooted, since the outputs need to be associated with an input. + /// + public class CanonicalTrackedOutputFiles + { + #region Member Data + // The output dependency table + private Dictionary> _dependencyTable; + // The .write. trackg log files + private ITaskItem[] _tlogFiles; + // The TaskLoggingHelper that we log progress to + private TaskLoggingHelper _log; + // Are the tracking logs that we were constructed with actually available + private bool _tlogAvailable; + #endregion + + #region Properties + + // Provide external access to the dependencyTable +#if WHIDBEY_VISIBILITY + internal +#else + public +#endif + Dictionary> DependencyTable + { + get { return _dependencyTable; } + } + #endregion + + #region Constructors + /// + /// Constructor + /// + /// The task that is using file tracker + /// The .write. tlog files to interpret + public CanonicalTrackedOutputFiles(ITaskItem[] tlogFiles) + { + InternalConstruct(null, tlogFiles, true); + } + + /// + /// Constructor + /// + /// The task that is using file tracker + /// The .write. tlog files to interpret + public CanonicalTrackedOutputFiles(ITask ownerTask, ITaskItem[] tlogFiles) + { + InternalConstruct(ownerTask, tlogFiles, true); + } + + /// + /// Constructor + /// + /// The task that is using file tracker + /// The .write. tlog files to interpret + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLogs", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public CanonicalTrackedOutputFiles(ITask ownerTask, ITaskItem[] tlogFiles, bool constructOutputsFromTLogs) + { + InternalConstruct(ownerTask, tlogFiles, constructOutputsFromTLogs); + } + + /// + /// Internal constructor + /// + /// The task that is using file tracker + /// The .write. tlog files to interpret + /// The output graph is built from the .write. tlogs + private void InternalConstruct(ITask ownerTask, ITaskItem[] tlogFiles, bool constructOutputsFromTLogs) + { + if (ownerTask != null) + { + _log = new TaskLoggingHelper(ownerTask); + _log.TaskResources = AssemblyResources.PrimaryResources; + _log.HelpKeywordPrefix = "MSBuild."; + } + + _tlogFiles = TrackedDependencies.ExpandWildcards(tlogFiles); + _tlogAvailable = TrackedDependencies.ItemsExist(_tlogFiles); + _dependencyTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (_tlogFiles != null && constructOutputsFromTLogs) + { + ConstructOutputTable(); + } + } + #endregion + + #region Methods + /// + /// Construct our dependency table for our source files + /// + private void ConstructOutputTable() + { + string tLogRootingMarker = null; + try + { + // construct a rooting marker from the tlog files + tLogRootingMarker = DependencyTableCache.FormatNormalizedTlogRootingMarker(_tlogFiles); + } + catch (ArgumentException e) + { + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLog", e.Message); + return; + } + + // Record the current directory (which under normal circumstances will be the project directory) + // so that we can compare tracked paths against it for inclusion in the dependency graph + string currentProjectDirectory = FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()); + + if (!_tlogAvailable) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_TrackingLogNotAvailable"); + lock (DependencyTableCache.DependencyTable) + { + // The tracking logs are not available, they may have been deleted at some point. + // Be safe and remove any references from the cache. + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + } + return; + } + + DependencyTableCacheEntry cachedEntry = null; + + lock (DependencyTableCache.DependencyTable) + { + // Look in the dependency table cache to see if its available and up to date + cachedEntry = DependencyTableCache.GetCachedEntry(tLogRootingMarker); + } + + // We have an up to date cached entry + if (cachedEntry != null) + { + _dependencyTable = (Dictionary>)cachedEntry.DependencyTable; + // Log information about what we're using + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_WriteTrackingCached"); + foreach (ITaskItem tlogItem in cachedEntry.TlogFiles) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t{0}", tlogItem.ItemSpec); + } + return; + } + + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_WriteTrackingLogs"); + + // Now we need to construct the rest of the table from the TLOG files + // If there are any errors in the tlogs, we want to warn, stop parsing tlogs, and empty + // out the dependency table, essentially forcing a rebuild. + bool encounteredInvalidTLogContents = false; + bool exceptionCaught = false; + string invalidTLogName = null; + foreach (ITaskItem tlogFileName in _tlogFiles) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t{0}", tlogFileName.ItemSpec); + + try + { + using (StreamReader tlog = File.OpenText(tlogFileName.ItemSpec)) + { + string tlogEntry = tlog.ReadLine(); + + while (tlogEntry != null) + { + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + + if (tlogEntry[0] == '^') // This is a rooting record, follow the outputs for it + { + Dictionary dependencies; + + tlogEntry = tlogEntry.Substring(1); + + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + + if (!_dependencyTable.TryGetValue(tlogEntry, out dependencies)) + { + dependencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + _dependencyTable.Add(tlogEntry, dependencies); + } + + // Process each file encountered until we hit a rooting marker + do + { + tlogEntry = tlog.ReadLine(); + + if (tlogEntry != null) + { + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + else if ((tlogEntry[0] != '^') && (tlogEntry[0] != '#') && (!dependencies.ContainsKey(tlogEntry))) + { + // Allows incremental build of projects existing under temp, only for those reads / writes that + // either are not under temp, or are recursively beneath the current project directory. + if (FileTracker.FileIsUnderPath(tlogEntry, currentProjectDirectory) || !FileTracker.FileIsExcludedFromDependencies(tlogEntry)) + { + DateTime fileModifiedTime = NativeMethods.GetLastWriteTimeUtc(tlogEntry); + + dependencies.Add(tlogEntry, fileModifiedTime); + } + } + } + } while ((tlogEntry != null) && (tlogEntry[0] != '^')); + + if (encounteredInvalidTLogContents) + { + break; + } + } + else // don't know what this entry is, so skip it + { + tlogEntry = tlog.ReadLine(); + } + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLog", e.Message); + break; + } + + if (encounteredInvalidTLogContents) + { + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLogContents", invalidTLogName); + break; + } + } + + lock (DependencyTableCache.DependencyTable) + { + // There were problems with the tracking logs -- we've already warned or errored; now we want to make + // sure that we essentially force a rebuild of this particular root. + if (encounteredInvalidTLogContents || exceptionCaught) + { + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + + _dependencyTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + else + { + // Record the newly built valid dependency table in the cache + DependencyTableCache.DependencyTable[tLogRootingMarker] = new DependencyTableCacheEntry(_tlogFiles, _dependencyTable); + } + } + } + + /// + /// Given a set of sources, removes from the dependency graph any roots that share + /// the same outputs as the rooting marker constructed from the given set of sources. + /// + /// + /// Used when there's a possibility that more than one set of inputs may produce the + /// same output -- this is a way to invalidate any other roots that produce that same + /// outputs, so that the next time the task is run with that other set of inputs, it + /// won't incorrectly believe that it is up-to-date. + /// + /// The set of sources that form the rooting marker whose outputs + /// should not be shared by any other rooting marker. + /// An array of the rooting markers that were removed. + public string[] RemoveRootsWithSharedOutputs(ITaskItem[] sources) + { + ErrorUtilities.VerifyThrowArgumentNull(sources, "sources"); + + List removedMarkers = new List(); + string currentRoot = FileTracker.FormatRootingMarker(sources); + Dictionary currentOutputs; + + if (DependencyTable.TryGetValue(currentRoot, out currentOutputs)) + { + // This is O(n*m), but in most cases, both n (the number of roots in the file) and m (the number + // of outputs per root) should be fairly small. + // UNDONE: Can we make this faster? + foreach (KeyValuePair> root in DependencyTable) + { + if (!currentRoot.Equals(root.Key, StringComparison.Ordinal)) + { + // If the current entry contains any of the outputs of the rooting marker we have sources for, + // then we want to remove it from the dependency table. + foreach (string output in currentOutputs.Keys) + { + if (root.Value.ContainsKey(output)) + { + removedMarkers.Add(root.Key); + break; + } + } + } + } + + // Now actually remove the markers that we intend to remove. + foreach (string removedMarker in removedMarkers) + { + DependencyTable.Remove(removedMarker); + } + } + + return removedMarkers.ToArray(); + } + + /// + /// Remove the specified ouput from the dependency graph for the given source file + /// + /// The source file who's output is to be discarded + /// The output path to be removed + public bool RemoveOutputForSourceRoot(string sourceRoot, string outputPathToRemove) + { + if (DependencyTable.ContainsKey(sourceRoot)) + { + bool removed = DependencyTable[sourceRoot].Remove(outputPathToRemove); + // If we just removed the last entry for this root, remove the root. + if (DependencyTable[sourceRoot].Count == 0) + { + DependencyTable.Remove(sourceRoot); + } + // If the output didn't exist then return false + return removed; + } + else + { + // If we don't have it, then that's as good as success + return true; + } + } + + /// + /// This method determines the outputs for a source root (as in the contents of a rooting marker) + /// + /// The sources to find outputs for + /// Array of outputs for the source + public ITaskItem[] OutputsForNonCompositeSource(params ITaskItem[] sources) + { + Dictionary outputs = new Dictionary(StringComparer.OrdinalIgnoreCase); + List outputsArray = new List(); + string upperSourcesRoot = FileTracker.FormatRootingMarker(sources); + + // Check each root in the output table to see if meets case 1 or two described above + foreach (ITaskItem source in sources) + { + string upperSourceRoot = FileUtilities.NormalizePath(source.ItemSpec); + OutputsForSourceRoot(outputs, upperSourceRoot); + } + + // There were no outputs for the requested root + if (outputs.Count == 0) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_OutputForRootNotFound", upperSourcesRoot); + } + else + { + // We have our set of outputs, construct our array to return + outputsArray.AddRange(outputs.Values); + + // Too much output logging leads to poor performance + if (outputs.Count > CanonicalTrackedFilesHelper.MaxLogCount) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_OutputsNotShown", outputs.Count); + } + else + { + // We have our set of outputs, log the details + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_OutputsFor", upperSourcesRoot); + + foreach (ITaskItem outputItem in outputsArray) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t" + outputItem); + } + } + } + + return outputsArray.ToArray(); + } + + /// + /// This method determines the outputs for a source root (as in the contents of a rooting marker) + /// + /// The sources to find outputs for + /// Array of outputs for the source + public ITaskItem[] OutputsForSource(params ITaskItem[] sources) + { + return OutputsForSource(sources, true); + } + + /// + /// This method determines the outputs for a source root (as in the contents of a rooting marker) + /// + /// The sources to find outputs for + /// When set true, this will consider using outputs found in rooting markers that are composed of the sub-root. + /// Array of outputs for the source + public ITaskItem[] OutputsForSource(ITaskItem[] sources, bool searchForSubRootsInCompositeRootingMarkers) + { + // We need to find all the outputs for the sources + // This happens in two ways; Look at all the roots in the output table and.. + // 1. If the root in the table is comprised entirely from sources in the set + // being requested then the outputs for that root should be included + // *This is the mechanism used by CL, MIDL, RC + // + // 2. If the root for the set of sources being requested fully contains (or equals) + // the root in the table then the outputs should be included + // *This is currently only in use by unit tests (it is a valid scenario) + + Dictionary outputs = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // There were no tlogs available, that means we need to build + if (!_tlogAvailable) + { + return null; + } + + // Construct a rooting marker from the set of sources + string upperSourcesRoot = FileTracker.FormatRootingMarker(sources); + List outputsArray = new List(); + + // Check each root in the output table to see if meets case 1 or two described above + foreach (string tableEntryRoot in _dependencyTable.Keys) + { + string upperTableEntryRoot = tableEntryRoot.ToUpperInvariant(); + if (searchForSubRootsInCompositeRootingMarkers && + (upperSourcesRoot.Contains(upperTableEntryRoot) || + upperTableEntryRoot.Contains(upperSourcesRoot) || + RootContainsAllSubRootComponents(upperSourcesRoot, upperTableEntryRoot))) + { + // Gather the unique outputs for this root + OutputsForSourceRoot(outputs, upperTableEntryRoot); + } + else if (!searchForSubRootsInCompositeRootingMarkers && + upperTableEntryRoot.Equals(upperSourcesRoot, StringComparison.Ordinal)) + { + OutputsForSourceRoot(outputs, upperTableEntryRoot); + } + } + + // There were no outputs for the requested root + if (outputs.Count == 0) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_OutputForRootNotFound", upperSourcesRoot); + } + else + { + // We have our set of outputs, construct our array to return + outputsArray.AddRange(outputs.Values); + + // Too much output logging leads to poor performance + if (outputs.Count > CanonicalTrackedFilesHelper.MaxLogCount) + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_OutputsNotShown", outputs.Count); + } + else + { + // We have our set of outputs, log the details + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_OutputsFor", upperSourcesRoot); + + foreach (ITaskItem outputItem in outputsArray) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t" + outputItem); + } + } + } + + return outputsArray.ToArray(); + } + + /// + /// Check that the given composite root contains all entries in the composite sub root + /// + /// The root to look for all sub roots in + /// The root that is comprised of subroots to look for + /// + internal bool RootContainsAllSubRootComponents(string compositeRoot, string compositeSubRoot) + { + bool containsAllSubRoots = true; + + // If the two are identical, then clearly all keys are present + if (string.Compare(compositeRoot, compositeSubRoot, StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else + { + // look for each sub key in the main composite key + string[] rootComponents = compositeSubRoot.Split('|'); + foreach (string subRoot in rootComponents) + { + containsAllSubRoots &= compositeRoot.Contains(subRoot); + // we didn't find this subkey, so bail out + if (!containsAllSubRoots) + { + break; + } + } + } + return containsAllSubRoots; + } + + /// + /// This method determines the outputs for a source root (as in the contents of a rooting marker) + /// + /// List of outputs to populate + /// The source to gather outputs for + private void OutputsForSourceRoot(Dictionary outputs, string sourceKey) + { + Dictionary dependencies; + + if (_dependencyTable.TryGetValue(sourceKey, out dependencies)) + { + foreach (string dependee in dependencies.Keys) + { + // only if we don't have the output already should we + // add it again + ITaskItem existingTaskItem; + if (!outputs.TryGetValue(dependee, out existingTaskItem)) + { + outputs.Add(dependee, new TaskItem(dependee)); + } + } + } + } + + /// + /// This method adds computed outputs for the given source key to the output graph + /// + /// The source to add outputs for + /// The computed outputs for this source key + public void AddComputedOutputForSourceRoot(string sourceKey, string computedOutput) + { + Dictionary dependencies; + + dependencies = GetSourceKeyOutputs(sourceKey); + + AddOutput(dependencies, computedOutput); + } + + /// + /// This method adds computed outputs for the given source key to the output graph + /// + /// The source to add outputs for + /// The computed outputs for this source key + public void AddComputedOutputsForSourceRoot(string sourceKey, string[] computedOutputs) + { + Dictionary dependencies; + + dependencies = GetSourceKeyOutputs(sourceKey); + + foreach (string computedOutput in computedOutputs) + { + AddOutput(dependencies, computedOutput); + } + } + + /// + /// This method adds computed outputs for the given source key to the output graph + /// + /// The source to add outputs for + /// The computed outputs for this source key + public void AddComputedOutputsForSourceRoot(string sourceKey, ITaskItem[] computedOutputs) + { + Dictionary dependencies; + + dependencies = GetSourceKeyOutputs(sourceKey); + + foreach (ITaskItem computedOutput in computedOutputs) + { + AddOutput(dependencies, FileUtilities.NormalizePath(computedOutput.ItemSpec)); + } + } + + /// + /// This method returns the output dictionary for the given source key + /// if non exists, one is created + /// + /// The source to retrieve outputs for + private Dictionary GetSourceKeyOutputs(string sourceKey) + { + Dictionary dependencies; + if (!_dependencyTable.TryGetValue(sourceKey, out dependencies)) + { + dependencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + _dependencyTable.Add(sourceKey, dependencies); + } + return dependencies; + } + + /// + /// This method adds a computed output for the given source key to the dictionary specified + /// + /// The dictionary to add outputs to + /// The computed outputs for this source key + private void AddOutput(Dictionary dependencies, string computedOutput) + { + string fullComputedOutput = FileUtilities.NormalizePath(computedOutput).ToUpperInvariant(); + if (!dependencies.ContainsKey(fullComputedOutput)) + { + DateTime fileModifiedTime; + if (FileUtilities.FileExistsNoThrow(fullComputedOutput)) + { + fileModifiedTime = NativeMethods.GetLastWriteTimeUtc(fullComputedOutput); + } + else + { + fileModifiedTime = DateTime.MinValue; + } + dependencies.Add(fullComputedOutput, fileModifiedTime); + } + } + + /// + /// This method will re-write the tlogs from the current output table new entries will + /// be tracked. + /// + public void SaveTlog() + { + SaveTlog(null); + } + + /// + /// This method will re-write the tlogs from the current output table new entries will + /// be tracked. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public void SaveTlog(DependencyFilter includeInTLog) + { + if (_tlogFiles != null && (_tlogFiles.Length > 0)) + { + string tLogRootingMarker = DependencyTableCache.FormatNormalizedTlogRootingMarker(_tlogFiles); + + lock (DependencyTableCache.DependencyTable) + { + // The tracking logs in the cache will be invalidated by this compaction + // remove the cached entries to be sure + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + } + + string firstTlog = _tlogFiles[0].ItemSpec; + + // empty all tlogs + foreach (ITaskItem tlogFile in _tlogFiles) + { + File.WriteAllText(tlogFile.ItemSpec, "", System.Text.Encoding.Unicode); + } + + // Write out the dependency information as a new tlog + using (StreamWriter outputs = new StreamWriter(firstTlog, false, System.Text.Encoding.Unicode)) + { + foreach (string rootingMarker in _dependencyTable.Keys) + { + Dictionary dependencies = _dependencyTable[rootingMarker]; + outputs.WriteLine("^" + rootingMarker); + foreach (string file in dependencies.Keys) + { + // Give the task a chance to filter dependencies out of the written TLog + if (includeInTLog == null || includeInTLog(file)) + { + // Write out the entry + outputs.WriteLine(file); + } + } + } + } + } + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveEntriesForSource(ITaskItem source) + { + RemoveEntriesForSource(new ITaskItem[] { source }, null); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveEntriesForSource(ITaskItem source, ITaskItem correspondingOutput) + { + RemoveEntriesForSource(new ITaskItem[] { source }, new ITaskItem[] { correspondingOutput }); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveEntriesForSource(ITaskItem[] source) + { + RemoveEntriesForSource(source, null); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveEntriesForSource(ITaskItem[] source, ITaskItem[] correspondingOutputs) + { + // construct a root marker for the sources and outputs to remove from the graph + string rootMarkerToRemove = FileTracker.FormatRootingMarker(source, correspondingOutputs); + + // remove the entry from the graph for the combined root + _dependencyTable.Remove(rootMarkerToRemove); + + // remove the entry for each source item + foreach (ITaskItem sourceItem in source) + { + _dependencyTable.Remove(FileUtilities.NormalizePath(sourceItem.ItemSpec)); + } + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + public void RemoveDependencyFromEntry(ITaskItem[] sources, ITaskItem dependencyToRemove) + { + string rootingMarker = FileTracker.FormatRootingMarker(sources); + RemoveDependencyFromEntry(rootingMarker, dependencyToRemove); + } + + /// + /// Remove the output graph entries for the given source and corresponding outputs + /// + /// Source that should be removed from the graph + public void RemoveDependencyFromEntry(ITaskItem source, ITaskItem dependencyToRemove) + { + string rootingMarker = FileTracker.FormatRootingMarker(source); + RemoveDependencyFromEntry(rootingMarker, dependencyToRemove); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + private void RemoveDependencyFromEntry(string rootingMarker, ITaskItem dependencyToRemove) + { + // construct a root marker for the source that will remove the dependency from + Dictionary dependencies; + + if (_dependencyTable.TryGetValue(rootingMarker, out dependencies)) + { + dependencies.Remove(FileUtilities.NormalizePath(dependencyToRemove.ItemSpec)); + } + else + { + FileTracker.LogMessageFromResources(_log, MessageImportance.Normal, "Tracking_WriteLogEntryNotFound", rootingMarker); + } + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Source that should be removed from the graph + /// Output that correspond ot the sources (used for same file processing) + public void RemoveDependenciesFromEntryIfMissing(ITaskItem source) + { + RemoveDependenciesFromEntryIfMissing(new ITaskItem[] { source }, null); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Source that should be removed from the graph + /// Output that correspond ot the sources (used for same file processing) + public void RemoveDependenciesFromEntryIfMissing(ITaskItem source, ITaskItem correspondingOutput) + { + RemoveDependenciesFromEntryIfMissing(new ITaskItem[] { source }, new ITaskItem[] { correspondingOutput }); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + public void RemoveDependenciesFromEntryIfMissing(ITaskItem[] source) + { + RemoveDependenciesFromEntryIfMissing(source, null); + } + + /// + /// Remove the output graph entries for the given sources and corresponding outputs + /// + /// Sources that should be removed from the graph + /// Outputs that correspond ot the sources (used for same file processing) + public void RemoveDependenciesFromEntryIfMissing(ITaskItem[] source, ITaskItem[] correspondingOutputs) + { + if (correspondingOutputs != null) + { + ErrorUtilities.VerifyThrowArgument(source.Length == correspondingOutputs.Length, "Tracking_SourcesAndCorrespondingOutputMismatch"); + } + + // construct a combined root marker for the sources and outputs to remove from the graph + string rootingMarker = FileTracker.FormatRootingMarker(source, correspondingOutputs); + + RemoveDependenciesFromEntryIfMissing(rootingMarker); + + // Remove entries for each individual source + for (int sourceIndex = 0; sourceIndex < source.Length; sourceIndex++) + { + if (correspondingOutputs != null) + { + rootingMarker = FileTracker.FormatRootingMarker(source[sourceIndex], correspondingOutputs[sourceIndex]); + } + else + { + rootingMarker = FileTracker.FormatRootingMarker(source[sourceIndex]); + } + + RemoveDependenciesFromEntryIfMissing(rootingMarker); + } + } + + /// + /// Remove the output graph entries for the given rooting marker + /// + /// + private void RemoveDependenciesFromEntryIfMissing(string rootingMarker) + { + Dictionary dependencies; + + // In the event of incomplete tracking information (i.e. this root was not present), just continue quietly + // as the user could have killed the tool being tracked, or another error occured during its execution. + if (_dependencyTable.TryGetValue(rootingMarker, out dependencies)) + { + Dictionary dependenciesWithoutMissingFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + int keyIndex = 0; + + foreach (string file in dependencies.Keys) + { + if (keyIndex++ > 0) + { + // If we are ignoring missing files, then only record those that exist + if (FileUtilities.FileExistsNoThrow(file)) + { + dependenciesWithoutMissingFiles.Add(file, dependencies[file]); + } + } + else + { + dependenciesWithoutMissingFiles.Add(file, DateTime.Now); + } + } + + _dependencyTable[rootingMarker] = dependenciesWithoutMissingFiles; + } + } + #endregion + } +} diff --git a/src/Utilities/TrackedDependencies/DependencyTableCache.cs b/src/Utilities/TrackedDependencies/DependencyTableCache.cs new file mode 100644 index 00000000000..129b27cfbec --- /dev/null +++ b/src/Utilities/TrackedDependencies/DependencyTableCache.cs @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System.Text; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// A static cache that will hold the dependency graph as built from tlog files. + /// The cache is keyed on the root marker created from the full paths of the tlog files concerned. + /// As an entry is added to the cache so is the datetime it was added. + /// + internal static class DependencyTableCache + { + #region Member Data + /// + /// The dictionary that maps the root of the tlog filenames to the dependencytable built from their content + /// + private static Dictionary s_dependencyTableCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly char[] s_numerals = new char[10] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + private static TaskItemItemSpecIgnoreCaseComparer s_taskItemComparer = new TaskItemItemSpecIgnoreCaseComparer(); + + #endregion + + #region Properties + /// + /// Access to the table + /// + internal static Dictionary DependencyTable + { + get { return DependencyTableCache.s_dependencyTableCache; } + } + #endregion + + #region Methods + /// + /// Determine if a cache entry is up to date + /// + /// The cache entry to check + /// true if up to date + private static bool DependencyTableIsUpToDate(DependencyTableCacheEntry dependencyTable) + { + DateTime tableTime = dependencyTable.TableTime; + + foreach (ITaskItem tlogFile in dependencyTable.TlogFiles) + { + string tlogFilename = FileUtilities.NormalizePath(tlogFile.ItemSpec); + + DateTime lastWriteTime = NativeMethods.GetLastWriteTimeUtc(tlogFilename); + if (lastWriteTime > tableTime) + { + // one of the tlog files is newer than the table, so return false + return false; + } + } + + return true; + } + + /// + /// Get the cached entry for the given tlog set, if the table is out of date it is removed from the cache + /// + /// The rooting marker for the set of tlogs + /// The cached table entry + internal static DependencyTableCacheEntry GetCachedEntry(string tLogRootingMarker) + { + if (DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCacheEntry cacheEntry = DependencyTable[tLogRootingMarker]; + if (DependencyTableIsUpToDate(cacheEntry)) + { + return cacheEntry; + } + else + { + // Remove the cached entry from memory + DependencyTable.Remove(tLogRootingMarker); + } + } + // Either there was no cache entry, or it was out of date and was removed + return null; + } + + /// + /// Given a set of TLog names, formats a rooting marker from them, that additionally replaces + /// all PIDs and TIDs with "[ID]" so the cache doesn't get overloaded with entries + /// that should be basically the same but have different PIDs or TIDs in the name. + /// + /// The set of tlogs to format + /// The normalized rooting marker based on that set of tlogs + internal static string FormatNormalizedTlogRootingMarker(ITaskItem[] tlogFiles) + { + HashSet normalizedFiles = new HashSet(s_taskItemComparer); + + for (int i = 0; i < tlogFiles.Length; i++) + { + ITaskItem normalizedFile = new TaskItem(tlogFiles[i]); + normalizedFile.ItemSpec = NormalizeTlogPath(tlogFiles[i].ItemSpec); + normalizedFiles.Add(normalizedFile); + } + + string normalizedRootingMarker = FileTracker.FormatRootingMarker(normalizedFiles.ToArray()); + return normalizedRootingMarker; + } + + /// + /// Given a TLog path, replace all PIDs and TIDs with "[ID]" in the filename, where + /// the typical format of a filename is "tool[.PID][-tool].read/write/command/delete.TID.tlog" + /// + /// + /// The algorithm used finds all instances of .\d+. and .\d+- in the filename and translates them + /// to .[ID]. and .[ID]- respectively, where "filename" is defined as the part of the path following + /// the final '\' in the path. + /// + /// In the VS 2010 C++ project system, there are artificially constructed tlogs that instead follow the + /// pattern "ProjectName.read/write.1.tlog", which means that one result of this change is that such + /// tlogs, should the project name also contain this pattern (e.g. ClassLibrary.1.csproj), will also end up + /// with [ID] being substituted for digits in the project name itself -- so the tlog name would end up being + /// ClassLibrary.[ID].read.[ID].tlog, rather than ClassLibrary.1.read.[ID].tlog. This could potentially + /// cause issues if there are multiple projects differentiated only by the digits in their names; however + /// we believe this is not an interesting scenario to watch for and support, given that the resultant rooting + /// marker is constructed from full paths, so either: + /// - The project directories are also different, and are never substituted, leading to different full paths (e.g. + /// C:\ClassLibrary.1\Debug\ClassLibrary.[ID].read.[ID].tlog and C:\ClassLibrary.2\Debug\ClassLibrary.[ID].read.[ID].tlog) + /// - The project directories are the same, in which case there are two projects that share the same intermediate + /// directory, which has a host of other problems and is explicitly NOT a supported scenario. + /// + /// The tlog path to normalize + /// The normalized path + internal static string NormalizeTlogPath(string tlogPath) + { + if (tlogPath.IndexOfAny(s_numerals) == -1) + { + // no reason to make modifications if there aren't any numerical IDs in the + // log filename to begin with. + return tlogPath; + } + else + { + int i = 0; + StringBuilder normalizedTlogFilename = new StringBuilder(); + + // We're walking the filename backwards since once we hit the final '\', we know we can stop parsing. + // So as to avoid allocating more memory and/or forcing StringBuilder to do more character copies + // than necessary, we append the reversed filename character by character to its own StringBuilder, + // and then reverse it again when constructing the final normalized path. + for (i = tlogPath.Length - 1; i >= 0 && tlogPath[i] != '\\'; i--) + { + // final character in the pattern can be either '.' or '-' + if (tlogPath[i] == '.' || tlogPath[i] == '-') + { + normalizedTlogFilename.Append(tlogPath[i]); + + int j = i - 1; + // to match the pattern, all preceding characters must be numeric + while (j >= 0 && tlogPath[j] != '\\' && tlogPath[j] >= '0' && tlogPath[j] <= '9') + { + j--; + } + + // and the pattern must begin with '.' + if (j >= 0 && tlogPath[j] == '.') + { + // [ID] backwards. :) + normalizedTlogFilename.Append("]DI["); + normalizedTlogFilename.Append(tlogPath[j]); + i = j; + } + } + else + { + // append this character -- it's not interesting. + normalizedTlogFilename.Append(tlogPath[i]); + } + } + + StringBuilder normalizedTlogPath = new StringBuilder(i + normalizedTlogFilename.Length); + + if (i >= 0) + { + // If we bailed out early, add everything else before reversing the filename itself + normalizedTlogPath.Append(tlogPath.Substring(0, i + 1)); + } + + // now add the reversed filename + for (int k = normalizedTlogFilename.Length - 1; k >= 0; k--) + { + normalizedTlogPath.Append(normalizedTlogFilename[k]); + } + + return normalizedTlogPath.ToString(); + } + } + + #endregion + + #region TaskItemItemSpecIgnoreCaseComparer + + /// + /// EqualityComparer for ITaskItems that only looks at the itemspec + /// + private class TaskItemItemSpecIgnoreCaseComparer : IEqualityComparer + { + /// + /// Returns whether the two ITaskItems are equal, where they are judged to be + /// equal as long as the itemspecs, compared case-insensitively, are equal. + /// + public bool Equals(ITaskItem x, ITaskItem y) + { + if (Object.ReferenceEquals(x, y)) + { + return true; + } + + if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) + { + return false; + } + + return String.Equals(x.ItemSpec, y.ItemSpec, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Returns the hashcode of this ITaskItem. Given that equality is judged solely based + /// on the itemspec, the hash code for this particular comparer also only uses the + /// itemspec to make its determination. + /// + public int GetHashCode(ITaskItem obj) + { + if (obj == null) + { + return 0; + } + + return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.ItemSpec); + } + } + + #endregion + } + + /// + /// A cache entry + /// + internal class DependencyTableCacheEntry + { + #region Member Data + // the set of tlog files used to build this cache entry + private ITaskItem[] _tlogFiles; + private DateTime _tableTime; + private IDictionary _dependencyTable; + #endregion + + #region Properties + public ITaskItem[] TlogFiles + { + get { return _tlogFiles; } + } + + public DateTime TableTime + { + get { return _tableTime; } + } + + public IDictionary DependencyTable + { + get { return _dependencyTable; } + } + #endregion + + #region Constructor + /// + /// Construct a new entry + /// + /// The tlog files used to build this dependency table + /// The dependency table to be cached + internal DependencyTableCacheEntry(ITaskItem[] tlogFiles, IDictionary dependencyTable) + { + _tlogFiles = new ITaskItem[tlogFiles.Length]; + _tableTime = DateTime.MinValue; + + // Our cache's knowledge of the tlog items needs their full path + for (int tlogItemCount = 0; tlogItemCount < tlogFiles.Length; tlogItemCount++) + { + string tlogFilename = FileUtilities.NormalizePath(tlogFiles[tlogItemCount].ItemSpec); + _tlogFiles[tlogItemCount] = new TaskItem(tlogFilename); + // Our cache entry needs to use the last modified time of the latest tlog + // involved so that our cache can be invalidated if any tlog is updated + DateTime modifiedTime = NativeMethods.GetLastWriteTimeUtc(tlogFilename); + if (modifiedTime > _tableTime) + { + _tableTime = modifiedTime; + } + } + + _dependencyTable = dependencyTable; + } + #endregion + } +} diff --git a/src/Utilities/TrackedDependencies/FileTracker.cs b/src/Utilities/TrackedDependencies/FileTracker.cs new file mode 100644 index 00000000000..935edac8cb5 --- /dev/null +++ b/src/Utilities/TrackedDependencies/FileTracker.cs @@ -0,0 +1,932 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections; +using System.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Globalization; +using System.Linq; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Utilities +{ + /// + /// Enumeration to express the type of executable being wrapped by Tracker.exe + /// + public enum ExecutableType + { + /// + /// 32-bit native executable + /// + Native32Bit = 0, + + /// + /// 64-bit native executable + /// + Native64Bit = 1, + + /// + /// A managed executable without a specified bitness + /// + ManagedIL = 2, + + /// + /// A managed executable specifically marked as 32-bit + /// + Managed32Bit = 3, + + /// + /// A managed executable specifically marked as 64-bit + /// + Managed64Bit = 4, + + /// + /// Use the same bitness as the currently running executable. + /// + SameAsCurrentProcess = 5 + } + + /// + /// This class contains utility functions to encapsulate launching and logging for the Tracker + /// + public static class FileTracker + { + #region Static Member Data + // The default path to temp, used to create explicitly short and long paths + private static string s_tempPath = Path.GetDirectoryName(Path.GetTempPath()); + + // The short path to temp + private static string s_tempShortPath = FileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetShortFilePath(s_tempPath).ToUpperInvariant()); + + // The long path to temp + private static string s_tempLongPath = FileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetLongFilePath(s_tempPath).ToUpperInvariant()); + + // The path to ApplicationData (is equal to %USERPROFILE%\Application Data folder in Windows XP and %USERPROFILE%\AppData\Roaming in Vista and later) + private static string s_applicationDataPath = FileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).ToUpperInvariant()); + + // The path to LocalApplicationData (is equal to %USERPROFILE%\Local Settings\Application Data folder in Windows XP and %USERPROFILE%\AppData\Local in Vista and later). + private static string s_localApplicationDataPath = FileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).ToUpperInvariant()); + + // The path to the LocalLow folder. In Vista and later, user application data is organized across %USERPROFILE%\AppData\LocalLow, %USERPROFILE%\AppData\Local (%LOCALAPPDATA%) + // and %USERPROFILE%\AppData\Roaming (%APPDATA%). The LocalLow folder is not present in XP. + private static string s_localLowApplicationDataPath = FileUtilities.EnsureTrailingSlash(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData\\LocalLow").ToUpperInvariant()); + + // The path to the common Application Data, which is also used by some programs (e.g. antivirus) that we wish to ignore. + // Is equal to C:\Documents and Settings\All Users\Application Data on XP, and C:\ProgramData on Vista+. + // But for backward compatibility, the paths "C:\Documents and Settings\All Users\Application Data" and "C:\Users\All Users\Application Data" are still accessible via Junction point on Vista+. + // Thus this list is created to store all possible common application data paths to cover more cases as possible. + private static List s_commonApplicationDataPaths; + + // The name of the standalone tracker tool. + private static string s_TrackerFilename = "Tracker.exe"; + + // The name of the assembly that is injected into the executing process + private static string s_FileTrackerFilename = "FileTracker.dll"; + #endregion + + #region Static constructor + + static FileTracker() + { + s_commonApplicationDataPaths = new List(); + + string defaultCommonApplicationDataPath = FileUtilities.EnsureTrailingSlash(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData).ToUpperInvariant()); + s_commonApplicationDataPaths.Add(defaultCommonApplicationDataPath); + + string defaultRootDirectory = Path.GetPathRoot(defaultCommonApplicationDataPath); + string alternativeCommonApplicationDataPath1 = FileUtilities.EnsureTrailingSlash(Path.Combine(defaultRootDirectory, @"Documents and Settings\All Users\Application Data").ToUpperInvariant()); + + if (!alternativeCommonApplicationDataPath1.Equals(defaultCommonApplicationDataPath, StringComparison.Ordinal)) + { + s_commonApplicationDataPaths.Add(alternativeCommonApplicationDataPath1); + } + + string alternativeCommonApplicationDataPath2 = FileUtilities.EnsureTrailingSlash(Path.Combine(defaultRootDirectory, @"Users\All Users\Application Data").ToUpperInvariant()); + + if (!alternativeCommonApplicationDataPath2.Equals(defaultCommonApplicationDataPath, StringComparison.Ordinal)) + { + s_commonApplicationDataPaths.Add(alternativeCommonApplicationDataPath2); + } + } + + #endregion + + #region Native method wrappers + + /// + /// Stops tracking file accesses. + /// + public static void EndTrackingContext() + { + InprocTrackingNativeMethods.EndTrackingContext(); + } + + /// + /// Resume tracking file accesses in the current tracking context. + /// + public static void ResumeTracking() + { + InprocTrackingNativeMethods.ResumeTracking(); + } + + /// + /// Set the global thread count, and assign that count to the current thread. + /// + public static void SetThreadCount(int threadCount) + { + InprocTrackingNativeMethods.SetThreadCount(threadCount); + } + + /// + /// Starts tracking file accesses. + /// + /// The directory into which to write the tracking log files + /// The name of the task calling this function, used to determine the + /// names of the tracking log files + public static void StartTrackingContext(string intermediateDirectory, string taskName) + { + InprocTrackingNativeMethods.StartTrackingContext(intermediateDirectory, taskName); + } + + /// + /// Starts tracking file accesses, using the rooting marker in the response file provided. To + /// automatically generate a response file given a rooting marker, call + /// FileTracker.CreateRootingMarkerResponseFile. + /// + /// The directory into which to write the tracking log files + /// The name of the task calling this function, used to determine the + /// names of the tracking log files + public static void StartTrackingContextWithRoot(string intermediateDirectory, string taskName, string rootMarkerResponseFile) + { + InprocTrackingNativeMethods.StartTrackingContextWithRoot(intermediateDirectory, taskName, rootMarkerResponseFile); + } + + /// + /// Stop tracking file accesses and get rid of the current tracking contexts. + /// + public static void StopTrackingAndCleanup() + { + InprocTrackingNativeMethods.StopTrackingAndCleanup(); + } + + /// + /// Temporarily suspend tracking of file accesses in the current tracking context. + /// + public static void SuspendTracking() + { + InprocTrackingNativeMethods.SuspendTracking(); + } + + /// + /// Write tracking logs for all contexts and threads. + /// + /// The directory into which to write the tracking log files + /// The name of the task calling this function, used to determine the + /// names of the tracking log files + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLogs", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public static void WriteAllTLogs(string intermediateDirectory, string taskName) + { + InprocTrackingNativeMethods.WriteAllTLogs(intermediateDirectory, taskName); + } + + /// + /// Write tracking logs corresponding to the current tracking context. + /// + /// The directory into which to write the tracking log files + /// The name of the task calling this function, used to determine the + /// names of the tracking log files + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLogs", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public static void WriteContextTLogs(string intermediateDirectory, string taskName) + { + InprocTrackingNativeMethods.WriteContextTLogs(intermediateDirectory, taskName); + } + + #endregion // Native method wrappers + + #region Methods + + /// + /// Test to see if the specified file is excluded from tracked dependencies + /// + /// + /// Full path of the file to test + /// + public static bool FileIsExcludedFromDependencies(string fileName) + { + bool exclude = true; + + // UNDONE: This check means that we cannot incremental build projects + // that exist under the following directories on XP: + // %USERPROFILE%\Application Data + // %USERPROFILE%\Local Settings\Application Data + // + // and the following directories on Vista: + // %USERPROFILE%\AppData\Local + // %USERPROFILE%\AppData\LocalLow + // %USERPROFILE%\AppData\Roaming + + // We don't want to be including these as dependencies or outputs: + // 1. Files under %USERPROFILE%\Application Data in XP and %USERPROFILE%\AppData\Roaming in Vista and later. + // 2. Files under %USERPROFILE%\Local Settings\Application Data in XP and %USERPROFILE%\AppData\Local in Vista and later. + // 3. Files under %USERPROFILE%\AppData\LocalLow in Vista and later. + // 4. Files that are in the TEMP directory (Since on XP, temp files are not + // located under AppData, they would not be compacted out correctly otherwise). + // 5. Files under the common ("All Users") Application Data location -- C:\Documents and Settings\All Users\Application Data + // on XP and either C:\Users\All Users\Application Data or C:\ProgramData on Vista+ + + exclude = FileTracker.FileIsUnderPath(fileName, s_applicationDataPath) || + FileTracker.FileIsUnderPath(fileName, s_localApplicationDataPath) || + FileTracker.FileIsUnderPath(fileName, s_localLowApplicationDataPath) || + FileTracker.FileIsUnderPath(fileName, s_tempShortPath) || + FileTracker.FileIsUnderPath(fileName, s_tempLongPath) || + s_commonApplicationDataPaths.Any(p => FileTracker.FileIsUnderPath(fileName, p)); + + return exclude; + } + + /// + /// Test to see if the specified file is under the specified path + /// + /// + /// Full path of the file to test + /// + /// + /// Is the file under this full path? + /// + public static bool FileIsUnderPath(string fileName, string path) + { + // UNDONE: Get the long file path for the entry + // This is an incredibly expensive operation. The tracking log + // as written by CL etc. does not contain short paths + // fileDirectory = NativeMethods.GetFullLongFilePath(fileDirectory); + + // Ensure that the path has a trailing slash that we are checking under + // By default the paths that we check for most often will have, so this will + // return fast and not allocate memory in the process + path = FileUtilities.EnsureTrailingSlash(path); + + // Is the fileName under the filePath? + return String.Compare(fileName, 0, path, 0, path.Length, StringComparison.OrdinalIgnoreCase) == 0; + } + + /// + /// Construct a rooting marker string from the ITaskItem array of primary sources. + /// + /// + /// ITaskItem array of primary sources. + /// + public static string FormatRootingMarker(ITaskItem source) + { + return FormatRootingMarker(new ITaskItem[] { source }, null); + } + + /// + /// Construct a rooting marker string from the ITaskItem array of primary sources. + /// + /// + /// ITaskItem array of primary sources. + /// + public static string FormatRootingMarker(ITaskItem source, ITaskItem output) + { + return FormatRootingMarker(new ITaskItem[] { source }, new ITaskItem[] { output }); + } + + /// + /// Construct a rooting marker string from the ITaskItem array of primary sources. + /// + /// + /// ITaskItem array of primary sources. + /// + public static string FormatRootingMarker(ITaskItem[] sources) + { + return FormatRootingMarker(sources, null); + } + + /// + /// Construct a rooting marker string from the ITaskItem array of primary sources. + /// + /// + /// ITaskItem array of primary sources. + /// + public static string FormatRootingMarker(ITaskItem[] sources, ITaskItem[] outputs) + { + ErrorUtilities.VerifyThrowArgumentNull(sources, "sources"); + + ArrayList rootSources = new ArrayList(); + StringBuilder rootSourcesList = new StringBuilder(); + int builderLength = 0; + + foreach (ITaskItem source in sources) + { + rootSources.Add(FileUtilities.NormalizePath(source.ItemSpec).ToUpperInvariant()); + } + + if (outputs != null) + { + foreach (ITaskItem output in outputs) + { + rootSources.Add(FileUtilities.NormalizePath(output.ItemSpec).ToUpperInvariant()); + } + } + + rootSources.Sort(StringComparer.OrdinalIgnoreCase); + + foreach (string source in rootSources) + { + rootSourcesList.Append(source); + rootSourcesList.Append('|'); + } + + builderLength = rootSourcesList.Length - 1; + if (builderLength < 0) + { + builderLength = 0; + } + + return rootSourcesList.ToString(0, builderLength); + } + + /// + /// Given a set of source files in the form of ITaskItem, creates a temporary response + /// file containing the rooting marker that corresponds to those sources. + /// + /// The rooting marker to put in the response file. + /// The response file path. + public static string CreateRootingMarkerResponseFile(ITaskItem[] sources) + { + return CreateRootingMarkerResponseFile(FormatRootingMarker(sources)); + } + + /// + /// Given a rooting marker, creates a temporary response file with that rooting marker + /// in it. + /// + /// The rooting marker to put in the response file. + /// The response file path. + public static string CreateRootingMarkerResponseFile(string rootMarker) + { + string trackerResponseFile = FileUtilities.GetTemporaryFile(".rsp"); + File.WriteAllText(trackerResponseFile, "/r \"" + rootMarker + "\"", Encoding.Unicode); + + return trackerResponseFile; + } + + /// + /// Prepends the path to the appropriate FileTracker assembly to the PATH + /// environment variable. Used for inproc tracking, or when the .NET Framework may + /// not be on the PATH. + /// + /// The old value of PATH + public static string EnsureFileTrackerOnPath() + { + return EnsureFileTrackerOnPath(null); + } + + /// + /// Prepends the path to the appropriate FileTracker assembly to the PATH + /// environment variable. Used for inproc tracking, or when the .NET Framework may + /// not be on the PATH. + /// + /// The root path for FileTracker.dll. Overrides the toolType if specified. + /// The old value of PATH + public static string EnsureFileTrackerOnPath(string rootPath) + { + string oldPath = Environment.GetEnvironmentVariable("PATH"); + string fileTrackerPath = GetFileTrackerPath(ExecutableType.SameAsCurrentProcess, rootPath); + + if (!String.IsNullOrEmpty(fileTrackerPath)) + { + Environment.SetEnvironmentVariable + ( + "Path", + Path.GetDirectoryName(fileTrackerPath) + ";" + oldPath + ); + } + + return oldPath; + } + + /// + /// Searches %PATH% for the location of Tracker.exe, and returns the first + /// path that matches. + /// Matching full path to Tracker.exe or null if a matching path is not found. + /// + public static string FindTrackerOnPath() + { + string[] paths = Environment.GetEnvironmentVariable("PATH").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string path in paths) + { + string trackerPath; + + try + { + if (!Path.IsPathRooted(path)) + { + trackerPath = Path.GetFullPath(path); + } + else + { + trackerPath = path; + } + + trackerPath = Path.Combine(trackerPath, s_TrackerFilename); + + if (File.Exists(trackerPath)) + { + return trackerPath; + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + // Otherwise, just ignore this path and move on -- it's just bad for some reason. + } + } + + // Still haven't found it. + return null; + } + + /// + /// Determines whether we must track out-of-proc, or whether inproc tracking will work. + /// + /// The executable type for the tool being tracked + /// True if we need to track out-of-proc, false if inproc tracking is OK + public static bool ForceOutOfProcTracking(ExecutableType toolType) + { + return ForceOutOfProcTracking(toolType, null, null); + } + + /// + /// Determines whether we must track out-of-proc, or whether inproc tracking will work. + /// + /// The executable type for the tool being tracked + /// The name of the cancel event tracker should listen for, or null if there isn't one + /// True if we need to track out-of-proc, false if inproc tracking is OK + public static bool ForceOutOfProcTracking(ExecutableType toolType, string dllName, string cancelEventName) + { + bool trackOutOfProc = false; + string trackerPath = null; + + if (cancelEventName != null) + { + // If we have a cancel event, we must track out-of-proc. + trackOutOfProc = true; + } + else if (dllName != null) + { + // If we have a DLL name, we need to track out of proc -- inproc tracking just uses + // the default FileTracker.dll from the path. + trackOutOfProc = true; + } + else if (IntPtr.Size == sizeof(Int32)) + { + // Current process is 32-bit. So we need to spawn Tracker.exe if our tool is + // explicitly marked as 64-bit OR is MSIL and we're installed on a 64-bit OS. + if (toolType == ExecutableType.Managed64Bit || toolType == ExecutableType.Native64Bit) + { + trackOutOfProc = true; + } + else if (toolType == ExecutableType.ManagedIL) + { + trackerPath = ToolLocationHelper.GetPathToDotNetFrameworkFile(s_TrackerFilename, TargetDotNetFrameworkVersion.VersionLatest, DotNetFrameworkArchitecture.Bitness64); + + if (trackerPath != null) + { + // If we found a 64-bit path, we're on a 64-bit OS and need to use it. Otherwise, we're fine. + trackOutOfProc = true; + } + } + } + else if (IntPtr.Size == sizeof(Int64)) + { + // Current process is 64-bit. We need to spawn Tracker.exe if our tool is + // explicitly marked as 32-bit. + if (toolType == ExecutableType.Managed32Bit || toolType == ExecutableType.Native32Bit) + { + trackOutOfProc = true; + } + } + + return trackOutOfProc; + } + + /// + /// Given the ExecutableType of the tool being wrapped and information that we + /// know about our current bitness, figures out and returns the path to the correct + /// Tracker.exe. + /// + /// The executable type of the tool being wrapped + public static string GetTrackerPath(ExecutableType toolType) + { + return GetTrackerPath(toolType, null); + } + + /// + /// Given the ExecutableType of the tool being wrapped and information that we + /// know about our current bitness, figures out and returns the path to the correct + /// Tracker.exe. + /// + /// The executable type of the tool being wrapped + /// The root path for Tracker.exe. Overrides the toolType if specified. + public static string GetTrackerPath(ExecutableType toolType, string rootPath) + { + return GetPath(s_TrackerFilename, toolType, rootPath); + } + + /// + /// Given the ExecutableType of the tool being wrapped and information that we + /// know about our current bitness, figures out and returns the path to the correct + /// FileTracker.dll. + /// + /// The executable type of the tool being wrapped + public static string GetFileTrackerPath(ExecutableType toolType) + { + return GetFileTrackerPath(toolType, null); + } + + /// + /// Given the ExecutableType of the tool being wrapped and information that we + /// know about our current bitness, figures out and returns the path to the correct + /// FileTracker.dll. + /// + /// The executable type of the tool being wrapped + /// The root path for FileTracker.dll. Overrides the toolType if specified. + public static string GetFileTrackerPath(ExecutableType toolType, string rootPath) + { + return GetPath(s_FileTrackerFilename, toolType, rootPath); + } + + /// + /// Given a filename (only really meant to support either Tracker.exe or FileTracker.dll), returns + /// the appropriate path for the appropriate file type. + /// + /// + /// + /// The root path for the file. Overrides the toolType if specified. + private static string GetPath(string filename, ExecutableType toolType, string rootPath) + { + string trackerPath = null; + + if (!String.IsNullOrEmpty(rootPath)) + { + trackerPath = Path.Combine(rootPath, filename); + + if (!File.Exists(trackerPath)) + { + // if an override path was specified, that's it -- we don't want to fall back if the file + // is not found there. + trackerPath = null; + } + } + else + { + switch (toolType) + { + case ExecutableType.Native32Bit: + // A native executable that's 32-bit. Just return the 32-bit path + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness32); + break; + case ExecutableType.Native64Bit: + // A native executable that's 64-bit. Just return the 64-bit path + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness64); + break; + case ExecutableType.ManagedIL: + // Next most likely -- the tool is a managed executable that has not been explicitly marked + // either 32 or 64 bit. + // This case is slightly tricky -- we have to return the path to the 64-bit tracker if we're running + // on a 64-bit machine, and the 32-bit tracker if we're running on a 32-bit machine. + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness64); + + // If the path is null, that means there is no 64-bit framework -- that's fine, return the 32-bit path instead. + if (trackerPath == null) + { + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness32); + } + break; + case ExecutableType.Managed32Bit: + // A managed executable that has been explicitly marked 32-bit. Just return the 32-bit path. + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness32); + break; + case ExecutableType.Managed64Bit: + // A managed executable that has been explicitly marked 64-bit. Just return the 64-bit path. If this + // is a 32-bit machine, then we will return null, and the error will be caught and handled appropriately + // elsewhere. + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness64); + break; + case ExecutableType.SameAsCurrentProcess: + // Figure out what bitness the current process is and return that bitness of Tracker.exe. + if (IntPtr.Size == sizeof(Int32)) + { + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness32); + } + else + { + trackerPath = GetPath(filename, DotNetFrameworkArchitecture.Bitness64); + } + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } + + return trackerPath; + } + + /// + /// Given a filename (currently only Tracker.exe and FileTracker.dll are supported), return + /// the path to that file. + /// + /// + /// + /// + private static string GetPath(string filename, DotNetFrameworkArchitecture bitness) + { + string trackerPath; + + // Make sure that if someone starts passing the wrong thing to this method we don't silently + // eat it and do something possibly unexpected. + ErrorUtilities.VerifyThrow( + s_TrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase) || + s_FileTrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase), + "This method should only be passed s_TrackerFilename or s_FileTrackerFilename, but was passed {0} instead!", + filename + ); + + // Look for FileTracker.dll/FileTracker.exe first in the MSBuild tools directory, then fall back to .NET framework directory + // and finally to .NET SDK directories. + trackerPath = ToolLocationHelper.GetPathToBuildToolsFile(filename, ToolLocationHelper.CurrentToolsVersion, bitness); + + if (String.IsNullOrEmpty(trackerPath)) + { + trackerPath = ToolLocationHelper.GetPathToDotNetFrameworkFile(filename, TargetDotNetFrameworkVersion.VersionLatest, bitness); + + if ((String.IsNullOrEmpty(trackerPath) || !File.Exists(trackerPath)) && s_TrackerFilename.Equals(filename, StringComparison.OrdinalIgnoreCase)) + { + // fall back to looking in the SDK directory -- this is where Tracker.exe will be in the typical VS case. First check + // in the SDK for the latest target framework and latest Visual Studio version, since we want to make sure that we get + // the most up-to-date version of FileTracker. + trackerPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(filename, TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.VersionLatest, bitness); + + // If that didn't work, we may be in a scenario where, e.g., we have VS 10 on Windows 8, which comes with .NET 4.5 + // pre-installed. In which case, the Dev11 (or other "latest" SDK) may not exist, but the Dev10 SDK might. Check + // for that here. + if (trackerPath == null) + { + trackerPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(filename, TargetDotNetFrameworkVersion.Version40, bitness); + } + } + } + return trackerPath; + } + + /// + /// This method constructs the correct Tracker.exe response file arguments from its parameters + /// + /// The name of the dll that will do the tracking + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// The arguments as a string + public static string TrackerResponseFileArguments(string dllName, string intermediateDirectory, string rootFiles) + { + return TrackerResponseFileArguments(dllName, intermediateDirectory, rootFiles, null); + } + + /// + /// This method constructs the correct Tracker.exe response file arguments from its parameters + /// + /// The name of the dll that will do the tracking + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// If a cancel event has been created that Tracker should be listening for, its name is passed here + /// The arguments as a string + public static string TrackerResponseFileArguments(string dllName, string intermediateDirectory, string rootFiles, string cancelEventName) + { + CommandLineBuilder builder = new CommandLineBuilder(); + + builder.AppendSwitchIfNotNull("/d ", dllName); + + if (!String.IsNullOrEmpty(intermediateDirectory)) + { + intermediateDirectory = FileUtilities.NormalizePath(intermediateDirectory); + // If the intermediate directory ends up with a trailing slash, then be rid of it! + if (FileUtilities.EndsWithSlash(intermediateDirectory)) + { + intermediateDirectory = Path.GetDirectoryName(intermediateDirectory); + } + builder.AppendSwitchIfNotNull("/i ", intermediateDirectory); + } + + builder.AppendSwitchIfNotNull("/r ", rootFiles); + + builder.AppendSwitchIfNotNull("/b ", cancelEventName); // b for break + + return builder.ToString() + " "; + } + + /// + /// This method constructs the correct Tracker.exe command arguments from its parameters + /// + /// The command to track + /// The command to track's arguments + /// The arguments as a string + public static string TrackerCommandArguments(string command, string arguments) + { + CommandLineBuilder builder = new CommandLineBuilder(); + + builder.AppendSwitch(" /c"); + builder.AppendFileNameIfNotNull(command); + + string fullArguments = builder.ToString(); + + fullArguments += " " + arguments; + + return fullArguments; + } + + /// + /// This method constructs the correct Tracker.exe arguments from its parameters + /// + /// The command to track + /// The command to track's arguments + /// The name of the dll that will do the tracking + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// The arguments as a string + public static string TrackerArguments(string command, string arguments, string dllName, string intermediateDirectory, string rootFiles) + { + return TrackerArguments(command, arguments, dllName, intermediateDirectory, rootFiles, null); + } + + /// + /// This method constructs the correct Tracker.exe arguments from its parameters + /// + /// The command to track + /// The command to track's arguments + /// The name of the dll that will do the tracking + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// If a cancel event has been created that Tracker should be listening for, its name is passed here + /// The arguments as a string + public static string TrackerArguments(string command, string arguments, string dllName, string intermediateDirectory, string rootFiles, string cancelEventName) + { + string fullArguments = TrackerResponseFileArguments(dllName, intermediateDirectory, rootFiles, cancelEventName); + + fullArguments += TrackerCommandArguments(command, arguments); + + return fullArguments; + } + + #region StartProcess methods + + /// + /// Start the process; tracking the command. + /// + /// The command to track + /// The command to track's arguments + /// The type of executable the wrapped tool is + /// The name of the dll that will do the tracking + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// If Tracker should be listening on a particular event for cancellation, pass its name here + /// Process instance + public static Process StartProcess(string command, string arguments, ExecutableType toolType, string dllName, string intermediateDirectory, string rootFiles, string cancelEventName) + { + dllName = dllName ?? GetFileTrackerPath(toolType); + + string fullArguments = TrackerArguments(command, arguments, dllName, intermediateDirectory, rootFiles, cancelEventName); + return Process.Start(GetTrackerPath(toolType), fullArguments); + } + + /// + /// Start the process; tracking the command. + /// + /// The command to track + /// The command to track's arguments + /// The type of executable the wrapped tool is + /// The name of the dll that will do the tracking + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// Process instance + public static Process StartProcess(string command, string arguments, ExecutableType toolType, string dllName, string intermediateDirectory, string rootFiles) + { + return StartProcess(command, arguments, toolType, dllName, intermediateDirectory, rootFiles, null); + } + + /// + /// Start the process; tracking the command. + /// + /// The command to track + /// The command to track's arguments + /// The type of executable the wrapped tool is + /// Intermediate directory where tracking logs will be written + /// Rooting marker + /// Process instance + public static Process StartProcess(string command, string arguments, ExecutableType toolType, string intermediateDirectory, string rootFiles) + { + return StartProcess(command, arguments, toolType, null, intermediateDirectory, rootFiles, null); + } + + /// + /// Start the process; tracking the command. + /// + /// The command to track + /// The command to track's arguments + /// The type of executable the wrapped tool is + /// Rooting marker + /// Process instance + public static Process StartProcess(string command, string arguments, ExecutableType toolType, string rootFiles) + { + return StartProcess(command, arguments, toolType, null, null, rootFiles, null); + } + + /// + /// Start the process; tracking the command. + /// + /// The command to track + /// The command to track's arguments + /// The type of executable the wrapped tool is + /// Process instance + public static Process StartProcess(string command, string arguments, ExecutableType toolType) + { + return StartProcess(command, arguments, toolType, null, null, null, null); + } + + #endregion // StartProcess methods + + /// + /// Logs a message of the given importance using the specified resource string. To the specified Log. + /// + /// This method is not thread-safe. + /// The Log to log to. + /// The importance level of the message. + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + internal static void LogMessageFromResources(TaskLoggingHelper Log, MessageImportance importance, string messageResourceName, params object[] messageArgs) + { + // Only log when we have been passed a TaskLoggingHelper + if (Log != null) + { + ErrorUtilities.VerifyThrowArgumentNull(messageResourceName, "messageResourceName"); + + Log.LogMessage(importance, AssemblyResources.FormatResourceString(messageResourceName, messageArgs)); + } + } + + /// + /// Logs a message of the given importance using the specified string. + /// + /// This method is not thread-safe. + /// The importance level of the message. + /// The message string. + /// Optional arguments for formatting the message string. + /// Thrown when message is null. + internal static void LogMessage(TaskLoggingHelper Log, MessageImportance importance, string message, params object[] messageArgs) + { + // Only log when we have been passed a TaskLoggingHelper + if (Log != null) + { + Log.LogMessage(importance, message, messageArgs); + } + } + + /// + /// Logs a warning using the specified resource string. + /// + /// The name of the string resource to load. + /// Optional arguments for formatting the loaded string. + /// Thrown when messageResourceName is null. + internal static void LogWarningWithCodeFromResources(TaskLoggingHelper Log, string messageResourceName, params object[] messageArgs) + { + // Only log when we have been passed a TaskLoggingHelper + if (Log != null) + { + Log.LogWarningWithCodeFromResources(messageResourceName, messageArgs); + } + } + + #endregion + } + + /// + /// Dependency filter delegate. Used during TLog saves in order for tasks to selectively remove dependencies from the written + /// graph. + /// + /// The full path to the dependency file about to be written to the compacted TLog + /// If the file should actually be written to the TLog (true) or not (false) + public delegate bool DependencyFilter(string fullPath); +} diff --git a/src/Utilities/TrackedDependencies/FlatTrackingData.cs b/src/Utilities/TrackedDependencies/FlatTrackingData.cs new file mode 100644 index 00000000000..b6e7d1ec6e3 --- /dev/null +++ b/src/Utilities/TrackedDependencies/FlatTrackingData.cs @@ -0,0 +1,973 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Resources; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + public class FlatTrackingData + { + #region Constants + // The maximum number of outputs that should be logged, if more than this, then no outputs are logged + private const int MaxLogCount = 100; + #endregion + + #region Member Data + // The output dependency table + private IDictionary _dependencyTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + // The .write. trackg log files + private ITaskItem[] _tlogFiles; + + // The tlog marker is used if the tracking data is empty + // even if the tracked execution was successful + private string _tlogMarker = String.Empty; + + // The TaskLoggingHelper that we log progress to + private TaskLoggingHelper _log; + // Are the tracking logs that we were constructed with actually available + private bool _tlogsAvailable; + + // The oldest file that we have seen + private string _oldestFileName = String.Empty; + private DateTime _oldestFileTimeUtc = DateTime.MaxValue; + + // The newest file what we have seen + private string _newestFileName = String.Empty; + private DateTime _newestFileTimeUtc = DateTime.MinValue; + + // Should rooting markers be treated as tracking entries + private bool _treatRootMarkersAsEntries = false; + + // If files are missing when reading the Tlog, skip them + private bool _skipMissingFiles = false; + // If we are not skipping missing files, what DateTime should they be given? + private DateTime _missingFileTimeUtc = DateTime.MinValue; + // Missing files that have been detected in the TLog + private List _missingFiles = new List(); + + // The newest Tlog that we have seen + private DateTime _newestTLogTimeUtc = DateTime.MinValue; + private string _newestTLogFileName = String.Empty; + + // Cache of last write times + private IDictionary _lastWriteTimeUtcCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // The set of paths that contain files that are to be ignored during up to date check - these directories or their subdirectories + private List _excludedInputPaths = new List(); + #endregion + + #region Properties + + // Provide external access to the dependencyTable +#if WHIDBEY_VISIBILITY + internal +#else + public +#endif + IDictionary DependencyTable + { + get { return _dependencyTable; } + } + + /// + /// Missing files have been detected in the TLog + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Has shipped as public API, so we can't easily change it now. ")] + public List MissingFiles + { + get { return _missingFiles; } + set { _missingFiles = value; } + } + + /// + /// The path for the oldest file we have seen + /// + public string OldestFileName + { + get { return _oldestFileName; } + set { _oldestFileName = value; } + } + + /// + /// The time for the oldest file we have seen + /// + public DateTime OldestFileTime + { + get { return _oldestFileTimeUtc.ToLocalTime(); } + set { _oldestFileTimeUtc = value.ToUniversalTime(); } + } + + /// + /// The time for the oldest file we have seen + /// + public DateTime OldestFileTimeUtc + { + get { return _oldestFileTimeUtc; } + set { _oldestFileTimeUtc = value.ToUniversalTime(); } + } + + /// + /// The path for the newest file we have seen + /// + public string NewestFileName + { + get { return _newestFileName; } + set { _newestFileName = value; } + } + + /// + /// The time for the newest file we have seen + /// + public DateTime NewestFileTime + { + get { return _newestFileTimeUtc.ToLocalTime(); } + set { _newestFileTimeUtc = value.ToUniversalTime(); } + } + + /// + /// The time for the newest file we have seen + /// + public DateTime NewestFileTimeUtc + { + get { return _newestFileTimeUtc; } + set { _newestFileTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Should root markers in the TLog be treated as file accesses, or only as markers? + /// + public bool TreatRootMarkersAsEntries + { + get { return _treatRootMarkersAsEntries; } + set { _treatRootMarkersAsEntries = value; } + } + + /// + /// Should files in the TLog but no longer exist be skipped or recorded? + /// + public bool SkipMissingFiles + { + get { return _skipMissingFiles; } + set { _skipMissingFiles = value; } + } + + /// + /// The TLog files that back this structure + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Has shipped as public API, so we can't easily change it now. ")] + public ITaskItem[] TlogFiles + { + get { return _tlogFiles; } + set { _tlogFiles = value; } + } + + /// + /// The time of the newest Tlog + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public DateTime NewestTLogTime + { + get { return _newestTLogTimeUtc.ToLocalTime(); } + set { _newestTLogTimeUtc = value.ToUniversalTime(); } + } + + /// + /// The time of the newest Tlog + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public DateTime NewestTLogTimeUtc + { + get { return _newestTLogTimeUtc; } + set { _newestTLogTimeUtc = value.ToUniversalTime(); } + } + + /// + /// The path of the newest TLog file + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public string NewestTLogFileName + { + get { return _newestTLogFileName; } + set { _newestTLogFileName = value; } + } + + /// + /// Are all the TLogs that were passed to us actually available on disk? + /// + public bool TlogsAvailable + { + get { return _tlogsAvailable; } + set { _tlogsAvailable = value; } + } + + #endregion + + #region Constructors + /// + /// Constructor + /// + /// The .write. tlog files to interpret + /// Ignore files that do not exist on disk + /// The DateTime that should be recorded for missing file. + public FlatTrackingData(ITaskItem[] tlogFiles, DateTime missingFileTimeUtc) + { + InternalConstruct(null, tlogFiles, null, false, missingFileTimeUtc, null); + } + + /// + /// Constructor + /// + /// The .write. tlog files to interpret + /// Ignore files that do not exist on disk + /// The DateTime that should be recorded for missing file. + public FlatTrackingData(ITaskItem[] tlogFiles, ITaskItem[] tlogFilesToIgnore, DateTime missingFileTimeUtc) + { + InternalConstruct(null, tlogFiles, tlogFilesToIgnore, false, missingFileTimeUtc, null); + } + + /// + /// Constructor + /// + /// The .tlog files to interpret + /// The .tlog files to ignore + /// The DateTime that should be recorded for missing file. + /// The set of paths that contain files that are to be ignored during up to date check, including any subdirectories. + /// Cache to be used for all timestamp/exists comparisons, which can be shared between multiple FlatTrackingData instances. + public FlatTrackingData(ITaskItem[] tlogFiles, ITaskItem[] tlogFilesToIgnore, DateTime missingFileTimeUtc, string[] excludedInputPaths, IDictionary sharedLastWriteTimeUtcCache) + { + if (sharedLastWriteTimeUtcCache != null) + { + _lastWriteTimeUtcCache = sharedLastWriteTimeUtcCache; + } + + InternalConstruct(null, tlogFiles, tlogFilesToIgnore, false, missingFileTimeUtc, excludedInputPaths); + } + + + /// + /// Constructor + /// + /// The task that is using file tracker + /// The tlog files to interpret + /// Ignore files that do not exist on disk + /// The DateTime that should be recorded for missing file. + public FlatTrackingData(ITask ownerTask, ITaskItem[] tlogFiles, DateTime missingFileTimeUtc) + { + InternalConstruct(ownerTask, tlogFiles, null, false, missingFileTimeUtc, null); + } + + /// + /// Constructor + /// + /// The .write. tlog files to interpret + /// Ignore files that do not exist on disk + public FlatTrackingData(ITaskItem[] tlogFiles, bool skipMissingFiles) + { + InternalConstruct(null, tlogFiles, null, skipMissingFiles, DateTime.MinValue, null); + } + + /// + /// Constructor + /// + /// The task that is using file tracker + /// The tlog files to interpret + /// Ignore files that do not exist on disk + public FlatTrackingData(ITask ownerTask, ITaskItem[] tlogFiles, bool skipMissingFiles) + { + InternalConstruct(ownerTask, tlogFiles, null, skipMissingFiles, DateTime.MinValue, null); + } + + /// + /// Internal constructor + /// + /// The task that is using file tracker + /// The .write. tlog files to interpret + /// Ignore files that do not exist on disk + /// The set of paths that contain files that are to be ignored during up to date check + private void InternalConstruct(ITask ownerTask, ITaskItem[] tlogFilesLocal, ITaskItem[] tlogFilesToIgnore, bool skipMissingFiles, DateTime missingFileTimeUtc, string[] excludedInputPaths) + { + if (ownerTask != null) + { + _log = new TaskLoggingHelper(ownerTask); + _log.TaskResources = AssemblyResources.PrimaryResources; + _log.HelpKeywordPrefix = "MSBuild."; + } + + ITaskItem[] expandedTlogFiles = TrackedDependencies.ExpandWildcards(tlogFilesLocal); + + if (tlogFilesToIgnore != null) + { + ITaskItem[] expandedTlogFilesToIgnore = TrackedDependencies.ExpandWildcards(tlogFilesToIgnore); + + if (expandedTlogFilesToIgnore.Length > 0) + { + HashSet ignore = new HashSet(); + List remainingTlogFiles = new List(); + + foreach (ITaskItem tlogFileToIgnore in expandedTlogFilesToIgnore) + { + ignore.Add(tlogFileToIgnore.ItemSpec); + } + + foreach (ITaskItem tlogFile in expandedTlogFiles) + { + if (!ignore.Contains(tlogFile.ItemSpec)) + { + remainingTlogFiles.Add(tlogFile); + } + } + + _tlogFiles = remainingTlogFiles.ToArray(); + } + else + { + _tlogFiles = expandedTlogFiles; + } + } + else + { + _tlogFiles = expandedTlogFiles; + } + + // We have no TLog files on disk, create a TLog marker from the + // TLogFiles ItemSpec so we can fabricate one if we need to + // This becomes our "first" tlog, since on the very first run, no tlogs + // will exist, and if a compaction has been run (as part of the initial up-to-date check) then this + // marker tlog will be created as empty. + if (_tlogFiles == null || _tlogFiles.Length == 0) + { + _tlogMarker = tlogFilesLocal[0].ItemSpec.Replace("*", "1"); + _tlogMarker = _tlogMarker.Replace("?", "2"); + } + + if (excludedInputPaths != null) + { + // Assign our exclude paths to our lookup - and make sure that all recorded paths end in a slash so that + // our "starts with" comparison doesn't pick up incomplete matches, such as C:\Foo matching C:\FooFile.txt + foreach (string excludePath in excludedInputPaths) + { + string fullexcludePath = FileUtilities.EnsureTrailingSlash(FileUtilities.NormalizePath(excludePath)).ToUpperInvariant(); + _excludedInputPaths.Add(fullexcludePath); + } + } + + _tlogsAvailable = TrackedDependencies.ItemsExist(_tlogFiles); + _skipMissingFiles = skipMissingFiles; + _missingFileTimeUtc = missingFileTimeUtc.ToUniversalTime(); + if (_tlogFiles != null) + { + // Read the TLogs into our internal structures + ConstructFileTable(); + } + } + #endregion + + #region Methods + /// + /// Construct our dependency table for our source files + /// + private void ConstructFileTable() + { + string tLogRootingMarker = null; + try + { + // construct a rooting marker from the tlog files + tLogRootingMarker = DependencyTableCache.FormatNormalizedTlogRootingMarker(_tlogFiles); + } + catch (ArgumentException e) + { + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLog", e.Message); + return; + } + if (!_tlogsAvailable) + { + lock (DependencyTableCache.DependencyTable) + { + // The tracking logs are not available, they may have been deleted at some point. + // Be safe and remove any references from the cache. + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + } + return; + } + + DependencyTableCacheEntry cachedEntry = null; + + lock (DependencyTableCache.DependencyTable) + { + // Look in the dependency table cache to see if its available and up to date + cachedEntry = DependencyTableCache.GetCachedEntry(tLogRootingMarker); + } + + // We have an up to date cached entry + if (cachedEntry != null) + { + _dependencyTable = (IDictionary)cachedEntry.DependencyTable; + + // We may have stored the dependency table in the cache, but all the other information + // (newest file time, number of missing files, etc.) has been reset to default. Refresh + // the data. + this.UpdateFileEntryDetails(); + + // Log information about what we're using + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_TrackingCached"); + foreach (ITaskItem tlogItem in cachedEntry.TlogFiles) + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t{0}", tlogItem.ItemSpec); + } + return; + } + + FileTracker.LogMessageFromResources(_log, MessageImportance.Low, "Tracking_TrackingLogs"); + // Now we need to construct the rest of the table from the TLOG files + // If there are any errors in the tlogs, we want to warn, stop parsing tlogs, and empty + // out the dependency table, essentially forcing a rebuild. + bool encounteredInvalidTLogContents = false; + bool exceptionCaught = false; + string invalidTLogName = null; + foreach (ITaskItem tlogFileName in _tlogFiles) + { + try + { + FileTracker.LogMessage(_log, MessageImportance.Low, "\t{0}", tlogFileName.ItemSpec); + + DateTime tlogLastWriteTimeUtc = NativeMethods.GetLastWriteTimeUtc(tlogFileName.ItemSpec); + if (tlogLastWriteTimeUtc > _newestTLogTimeUtc) + { + _newestTLogTimeUtc = tlogLastWriteTimeUtc; + _newestTLogFileName = tlogFileName.ItemSpec; + } + + using (StreamReader tlog = File.OpenText(tlogFileName.ItemSpec)) + { + string tlogEntry = tlog.ReadLine(); + + while (tlogEntry != null) + { + if (tlogEntry.Length == 0) // empty lines are a sign that something has gone wrong + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + // Preprocessing for the line entry + else if (tlogEntry[0] == '#') // a comment marker should be skipped + { + tlogEntry = tlog.ReadLine(); + continue; + } + else if (tlogEntry[0] == '^' && TreatRootMarkersAsEntries) // This is a rooting record, and we should keep it + { + tlogEntry = tlogEntry.Substring(1); + + if (tlogEntry.Length == 0) + { + encounteredInvalidTLogContents = true; + invalidTLogName = tlogFileName.ItemSpec; + break; + } + } + else if (tlogEntry[0] == '^') // root marker is not being treated as an entry, skip it + { + tlogEntry = tlog.ReadLine(); + continue; + } + + // If we haven't seen this file before, then record it + if (!_dependencyTable.ContainsKey(tlogEntry)) + { + // It may be that this is one of the locations that we should ignore + if (!FileTracker.FileIsExcludedFromDependencies(tlogEntry)) + { + RecordEntryDetails(tlogEntry, true); + } + } + tlogEntry = tlog.ReadLine(); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLog", e.Message); + break; + } + + if (encounteredInvalidTLogContents) + { + FileTracker.LogWarningWithCodeFromResources(_log, "Tracking_RebuildingDueToInvalidTLogContents", invalidTLogName); + break; + } + } + + lock (DependencyTableCache.DependencyTable) + { + // There were problems with the tracking logs -- we've already warned or errored; now we want to make + // sure that we essentially force a rebuild of this particular root. + if (encounteredInvalidTLogContents || exceptionCaught) + { + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + + _dependencyTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + else + { + // Record the newly built dependency table in the cache + DependencyTableCache.DependencyTable[tLogRootingMarker] = new DependencyTableCacheEntry(_tlogFiles, (IDictionary)_dependencyTable); + } + } + } + + /// + /// Update the current state of entry details for the dependency table + /// + public void UpdateFileEntryDetails() + { + _oldestFileName = String.Empty; + _oldestFileTimeUtc = DateTime.MaxValue; + + _newestFileName = String.Empty; + _newestFileTimeUtc = DateTime.MinValue; + + _newestTLogFileName = String.Empty; + _newestTLogTimeUtc = DateTime.MinValue; + + this.MissingFiles.Clear(); + + // First update the details of our Tlogs + foreach (ITaskItem tlogFileName in _tlogFiles) + { + DateTime tlogLastWriteTimeUtc = NativeMethods.GetLastWriteTimeUtc(tlogFileName.ItemSpec); + if (tlogLastWriteTimeUtc > _newestTLogTimeUtc) + { + _newestTLogTimeUtc = tlogLastWriteTimeUtc; + _newestTLogFileName = tlogFileName.ItemSpec; + } + } + + // Now for each entry in the table + foreach (string entry in this.DependencyTable.Keys) + { + RecordEntryDetails(entry, false); + } + } + + /// + /// Test to see if the specified file is excluded from tracked dependency checking + /// + /// + /// Full path of the file to test + /// + /// + /// The file is excluded if it is within any of the specified excluded input paths or any subdirectory of the paths. + /// It also assumes the file name is already converted to Uppercase Invariant. + /// + public bool FileIsExcludedFromDependencyCheck(string fileName) + { + foreach (string path in _excludedInputPaths) + { + if (fileName.StartsWith(path, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Record the time and missing state of the entry in the tlog + /// + /// + private void RecordEntryDetails(string tlogEntry, bool populateTable) + { + if (FileIsExcludedFromDependencyCheck(tlogEntry)) + { + return; + } + + DateTime fileModifiedTimeUtc = GetLastWriteTimeUtc(tlogEntry); + if (_skipMissingFiles && fileModifiedTimeUtc == DateTime.MinValue) // the file is missing + { + return; + } + else if (fileModifiedTimeUtc == DateTime.MinValue) + { + // Record the file in our table even though it was missing + // use the missingFileTimeUtc as indicated. + if (populateTable) + { + _dependencyTable[tlogEntry] = _missingFileTimeUtc.ToUniversalTime(); + } + _missingFiles.Add(tlogEntry); + } + else + { + if (populateTable) + { + _dependencyTable[tlogEntry] = fileModifiedTimeUtc; + } + } + + // Record this file if it is newer than our current newest + if (fileModifiedTimeUtc > _newestFileTimeUtc) + { + _newestFileTimeUtc = fileModifiedTimeUtc; + _newestFileName = tlogEntry; + } + + // Record this file if it is older than our current oldest + if (fileModifiedTimeUtc < _oldestFileTimeUtc) + { + _oldestFileTimeUtc = fileModifiedTimeUtc; + _oldestFileName = tlogEntry; + } + } + + /// + /// This method will re-write the tlogs from the output table + /// + public void SaveTlog() + { + SaveTlog(null); + } + + /// + /// This method will re-write the tlogs from the current table + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public void SaveTlog(DependencyFilter includeInTLog) + { + if (_tlogFiles != null && (_tlogFiles.Length > 0)) + { + string tLogRootingMarker = DependencyTableCache.FormatNormalizedTlogRootingMarker(_tlogFiles); + + lock (DependencyTableCache.DependencyTable) + { + // The tracking logs in the cache will be invalidated by this write + // remove the cached entries to be sure + if (DependencyTableCache.DependencyTable.ContainsKey(tLogRootingMarker)) + { + DependencyTableCache.DependencyTable.Remove(tLogRootingMarker); + } + } + + string firstTlog = _tlogFiles[0].ItemSpec; + + // empty all tlogs + foreach (ITaskItem tlogFile in _tlogFiles) + { + File.WriteAllText(tlogFile.ItemSpec, "", System.Text.Encoding.Unicode); + } + + // Write out the dependency information as a new tlog + using (StreamWriter newTlog = new StreamWriter(firstTlog, false, System.Text.Encoding.Unicode)) + { + foreach (string fileEntry in _dependencyTable.Keys) + { + // Give the task a chance to filter dependencies out of the written TLog + if (includeInTLog == null || includeInTLog(fileEntry)) + { + // Write out the entry + newTlog.WriteLine(fileEntry); + } + } + } + } + else if (_tlogMarker != String.Empty) + { + string markerDirectory = Path.GetDirectoryName(_tlogMarker); + if (!Directory.Exists(markerDirectory)) + { + Directory.CreateDirectory(markerDirectory); + } + + // There were no TLogs to save, so use the TLog marker + // to create a marker file that can be used for up-to-date check. + File.WriteAllText(_tlogMarker, ""); + } + } + + /// + /// Returns cached value for last write time of file. Update the cache if it is the first + /// time someone asking for that file + /// + public DateTime GetLastWriteTimeUtc(string file) + { + DateTime fileModifiedTimeUtc = DateTime.MinValue; + if (!_lastWriteTimeUtcCache.TryGetValue(file, out fileModifiedTimeUtc)) + { + fileModifiedTimeUtc = NativeMethods.GetLastWriteTimeUtc(file); + _lastWriteTimeUtcCache[file] = fileModifiedTimeUtc; + } + + return fileModifiedTimeUtc; + } + + + #endregion + + #region Static Methods + + /// + /// Checks to see if the tracking data indicates that everything is up to date according to UpToDateCheckType. + /// Note: If things are not up to date, then the TLogs are compacted to remove all entries in preparation to + /// re-track execution of work. + /// + /// TaskLoggingHelper from the host task + /// UpToDateCheckType + /// The array of read tlogs + /// The array of write tlogs + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public static bool IsUpToDate(Task hostTask, UpToDateCheckType upToDateCheckType, ITaskItem[] readTLogNames, ITaskItem[] writeTLogNames) + { + bool isUpToDate; + // Read the input graph (missing inputs are infinitely new - i.e. outputs are out of date) + FlatTrackingData inputs = new FlatTrackingData(hostTask, readTLogNames, DateTime.MaxValue); + + // Read the output graph (missing outputs are infinitely old - i.e. outputs are out of date) + FlatTrackingData outputs = new FlatTrackingData(hostTask, writeTLogNames, DateTime.MinValue); + + // Find out if we are up to date + isUpToDate = FlatTrackingData.IsUpToDate(hostTask.Log, upToDateCheckType, inputs, outputs); + + // We're going to execute, so clear out the tlogs so + // the new execution will correctly populate the tlogs a-new + if (!isUpToDate) + { + // Remove all from inputs tlog + inputs.DependencyTable.Clear(); + inputs.SaveTlog(); + + // Remove all from outputs tlog + outputs.DependencyTable.Clear(); + outputs.SaveTlog(); + } + return isUpToDate; + } + + /// + /// Simple check of up to date state according to the tracking data and the UpToDateCheckType. + /// Note: No tracking log compaction will take place when using this overload + /// + /// TaskLoggingHelper from the host task + /// UpToDateCheckType to use + /// FlatTrackingData structure containing the inputs + /// FlatTrackingData structure containing the outputs + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Log", Justification = "Has shipped as public API; plus it is a closer match to other locations in our codebase where 'Log' is a property and cased properly")] + public static bool IsUpToDate(TaskLoggingHelper Log, UpToDateCheckType upToDateCheckType, FlatTrackingData inputs, FlatTrackingData outputs) + { + bool isUpToDate = false; + // Keep a record of the task resources that was in use before + ResourceManager taskResources = Log.TaskResources; + + Log.TaskResources = AssemblyResources.PrimaryResources; + + inputs.UpdateFileEntryDetails(); + outputs.UpdateFileEntryDetails(); + + if (!inputs.TlogsAvailable || !outputs.TlogsAvailable || inputs.DependencyTable.Count == 0) + { + // 1) The TLogs are somehow missing, which means we need to build + // 2) Because we are flat tracking, there are no roots which means that all the input file information + // comes from the input Tlogs, if they are empty then we must build. + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_LogFilesNotAvailable"); + } + else if (inputs.MissingFiles.Count > 0 || outputs.MissingFiles.Count > 0) + { + // Files are missing from either inputs or outputs, that means we need to build + + // Files are missing from inputs, that means we need to build + if (inputs.MissingFiles.Count > 0) + { + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_MissingInputs"); + } + // Too much logging leads to poor performance + if (inputs.MissingFiles.Count > MaxLogCount) + { + FileTracker.LogMessageFromResources(Log, MessageImportance.Low, "Tracking_InputsNotShown", inputs.MissingFiles.Count); + } + else + { + // We have our set of inputs, log the details + foreach (string input in inputs.MissingFiles) + { + FileTracker.LogMessage(Log, MessageImportance.Low, "\t" + input); + } + } + + // Files are missing from outputs, that means we need to build + if (outputs.MissingFiles.Count > 0) + { + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_MissingOutputs"); + } + // Too much logging leads to poor performance + if (outputs.MissingFiles.Count > MaxLogCount) + { + FileTracker.LogMessageFromResources(Log, MessageImportance.Low, "Tracking_OutputsNotShown", outputs.MissingFiles.Count); + } + else + { + // We have our set of inputs, log the details + foreach (string output in outputs.MissingFiles) + { + FileTracker.LogMessage(Log, MessageImportance.Low, "\t" + output); + } + } + } + else if (upToDateCheckType == UpToDateCheckType.InputOrOutputNewerThanTracking && + (inputs.NewestFileTimeUtc > inputs.NewestTLogTimeUtc)) + { + // One of the inputs is newer than the input tlog + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_DependencyWasModifiedAt", inputs.NewestFileName, inputs.NewestFileTimeUtc, inputs.NewestTLogFileName, inputs.NewestTLogTimeUtc); + } + else if (upToDateCheckType == UpToDateCheckType.InputOrOutputNewerThanTracking && + (outputs.NewestFileTimeUtc > outputs.NewestTLogTimeUtc)) + { + // one of the outputs is newer than the output tlog + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_DependencyWasModifiedAt", outputs.NewestFileName, outputs.NewestFileTimeUtc, outputs.NewestTLogFileName, outputs.NewestTLogTimeUtc); + } + else if (upToDateCheckType == UpToDateCheckType.InputNewerThanOutput && + (inputs.NewestFileTimeUtc > outputs.NewestFileTimeUtc)) + { + // One of the inputs is newer than the outputs + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_DependencyWasModifiedAt", inputs.NewestFileName, inputs.NewestFileTimeUtc, outputs.NewestFileName, outputs.NewestFileTimeUtc); + } + else if (upToDateCheckType == UpToDateCheckType.InputNewerThanTracking && + (inputs.NewestFileTimeUtc > inputs.NewestTLogTimeUtc)) + { + // One of the inputs is newer than the one of the TLogs + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_DependencyWasModifiedAt", inputs.NewestFileName, inputs.NewestFileTimeUtc, inputs.NewestTLogFileName, inputs.NewestTLogTimeUtc); + } + else if (upToDateCheckType == UpToDateCheckType.InputNewerThanTracking && + (inputs.NewestFileTimeUtc > outputs.NewestTLogTimeUtc)) + { + // One of the inputs is newer than the one of the TLogs + Log.LogMessageFromResources(MessageImportance.Low, "Tracking_DependencyWasModifiedAt", inputs.NewestFileName, inputs.NewestFileTimeUtc, outputs.NewestTLogFileName, outputs.NewestTLogTimeUtc); + } + else + { + // Nothing appears to have changed.. + isUpToDate = true; + Log.LogMessageFromResources(MessageImportance.Normal, "Tracking_UpToDate"); + } + + // Set the task resources back now that we're done with it + Log.TaskResources = taskResources; + + return isUpToDate; + } + + /// + /// Once tracked operations have been completed then we need to compact / finalize the Tlogs based + /// on the success of the tracked execution. If it fails, then we clean out the TLogs. If it succeeds + /// then we clean temporary files from the TLogs and re-write them. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLogs", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public static void FinalizeTLogs(bool trackedOperationsSucceeded, ITaskItem[] readTLogNames, ITaskItem[] writeTLogNames, ITaskItem[] trackedFilesToRemoveFromTLogs) + { + // Read the input table, skipping missing files + FlatTrackingData inputs = new FlatTrackingData(readTLogNames, true); + + // Read the output table, skipping missing files + FlatTrackingData outputs = new FlatTrackingData(writeTLogNames, true); + + + // If we failed we need to clean the Tlogs + if (!trackedOperationsSucceeded) + { + // If the tool errors in some way, we assume that any and all inputs and outputs it wrote during + // execution are wrong. So we compact the read and write tlogs to remove the entries for the + // set of sources being compiled - the next incremental build will find no entries + // and correctly cause the sources to be compiled + // Remove all from inputs tlog + inputs.DependencyTable.Clear(); + inputs.SaveTlog(); + + // Remove all from outputs tlog + outputs.DependencyTable.Clear(); + outputs.SaveTlog(); + } + else + { + // If all went well with the tool execution, then compact the tlogs + // to remove any files that are no longer on disk. + // This removes any temporary files from the dependency graph + + // In addition to temporary file removal, an optional set of files to remove may be been supplied + + if (trackedFilesToRemoveFromTLogs != null && trackedFilesToRemoveFromTLogs.Length > 0) + { + IDictionary trackedFilesToRemove = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (ITaskItem removeFile in trackedFilesToRemoveFromTLogs) + { + trackedFilesToRemove.Add(FileUtilities.NormalizePath(removeFile.ItemSpec), removeFile); + } + + // UNDONE: If necessary we could have two independent sets of "ignore" files, one for inputs and one for outputs + // Use an anonymous method to encapsulate the contains check for the output tlogs + outputs.SaveTlog(delegate (string fullTrackedPath) + { + // We need to answer the question "should fullTrackedPath be included in the TLog?" + return (!trackedFilesToRemove.ContainsKey(fullTrackedPath)); + } + ); + + // Use an anonymous method to encapsulate the contains check for the input tlogs + inputs.SaveTlog(delegate (string fullTrackedPath) + { + // We need to answer the question "should fullTrackedPath be included in the TLog?" + return (!trackedFilesToRemove.ContainsKey(fullTrackedPath)); + } + ); + } + else + { + // Compact the write tlog + outputs.SaveTlog(); + + // Compact the read tlog + inputs.SaveTlog(); + } + } + } + #endregion + } + + /// + /// The possible types of up to date check that we can support + /// +#if WHIDBEY_VISIBILITY + internal +#else + public +#endif + enum UpToDateCheckType + { + InputNewerThanOutput, + InputOrOutputNewerThanTracking, + InputNewerThanTracking + } +} diff --git a/src/Utilities/TrackedDependencies/NativeMethods.cs b/src/Utilities/TrackedDependencies/NativeMethods.cs new file mode 100644 index 00000000000..39b6c7acc10 --- /dev/null +++ b/src/Utilities/TrackedDependencies/NativeMethods.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Runtime.InteropServices; +using System.Globalization; + +namespace Microsoft.Build.Utilities +{ + internal static partial class NativeMethods + { + private const int MAX_PATH = 260; + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] + internal static extern bool GetFileAttributesEx(String name, int fileInfoLevel, ref WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); + + /// + /// Get the last write time in utc of the fullpath to the file. + /// If the file does not exist, then DateTime.MinValue is returned + /// + /// Full path to the file in the filesystem + /// + internal static DateTime GetLastWriteTimeUtc(string fullPath) + { + DateTime fileModifiedTime = DateTime.MinValue; + WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA(); + bool success = false; + + success = NativeMethods.GetFileAttributesEx(fullPath, 0, ref data); + if (success) + { + long dt = ((long)(data.ftLastWriteTimeHigh) << 32) | ((long)data.ftLastWriteTimeLow); + fileModifiedTime = DateTime.FromFileTimeUtc(dt); + } + + return fileModifiedTime; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WIN32_FILE_ATTRIBUTE_DATA + { + internal int fileAttributes; + internal uint ftCreationTimeLow; + internal uint ftCreationTimeHigh; + internal uint ftLastAccessTimeLow; + internal uint ftLastAccessTimeHigh; + internal uint ftLastWriteTimeLow; + internal uint ftLastWriteTimeHigh; + internal uint fileSizeHigh; + internal uint fileSizeLow; + } +} diff --git a/src/Utilities/TrackedDependencies/TrackedDependencies.cs b/src/Utilities/TrackedDependencies/TrackedDependencies.cs new file mode 100644 index 00000000000..66f501c8a56 --- /dev/null +++ b/src/Utilities/TrackedDependencies/TrackedDependencies.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections; +using System.Text; +using System.IO; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// This class contains utility functions to assist with tracking dependencies + /// +#if WHIDBEY_VISIBILITY + internal +#else + public +#endif + static class TrackedDependencies + { + #region Methods + /// + /// Expand wildcards in the item list. + /// + /// + /// Array of items expanded + public static ITaskItem[] ExpandWildcards(ITaskItem[] expand) + { + if (expand == null) + { + return null; + } + else + { + List expanded = new List(expand.Length); + foreach (ITaskItem i in expand) + { + if (FileMatcher.HasWildcards(i.ItemSpec)) + { + string[] files; + string directoryName = Path.GetDirectoryName(i.ItemSpec); + string searchPattern = Path.GetFileName(i.ItemSpec); + + // Very often with TLog files we're talking about + // a directory and a simply wildcarded filename + // Optimize for that case here. + if (!FileMatcher.HasWildcards(directoryName) && Directory.Exists(directoryName)) + { + files = Directory.GetFiles(directoryName, searchPattern); + } + else + { + files = FileMatcher.GetFiles(null, i.ItemSpec); + } + + foreach (string file in files) + { + TaskItem newItem = new TaskItem((ITaskItem)i); + newItem.ItemSpec = file; + expanded.Add(newItem); + } + } + else + { + expanded.Add(i); + } + } + return expanded.ToArray(); + } + } + + /// + /// This method checks that all the files exist + /// + /// + /// bool + internal static bool ItemsExist(ITaskItem[] files) + { + bool allExist = true; + + if (files != null && files.Length > 0) + { + foreach (ITaskItem item in files) + { + if (!FileUtilities.FileExistsNoThrow(item.ItemSpec)) + { + allExist = false; + break; + } + } + } + else + { + allExist = false; + } + return allExist; + } + #endregion + } +} diff --git a/src/Utilities/UnitTests/ApiContract_Tests.cs b/src/Utilities/UnitTests/ApiContract_Tests.cs new file mode 100644 index 00000000000..e530f10f081 --- /dev/null +++ b/src/Utilities/UnitTests/ApiContract_Tests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.Utilities.Unittest +{ + [TestClass] + public class ApiContract_Tests + { + [TestMethod] + public void ContainedElementHandlesNull() + { + bool value = ApiContract.IsContainedApiContractsElement(null); + Assert.IsFalse(value, "should not be valid and not throw on null"); + } + + [TestMethod] + public void ReadContractsElementHandlesNullElement() + { + ApiContract.ReadContractsElement(null, new List()); + } + + [TestMethod] + public void ReadContractsElementBasicRead() + { + XmlDocument document = new XmlDocument(); + document.LoadXml(@""); + List contracts = new List(); + + ApiContract.ReadContractsElement(document.FirstChild as XmlElement, contracts); + Assert.AreEqual(1, contracts.Count); + Assert.AreEqual("UAP", contracts[0].Name); + Assert.AreEqual("1.0.0.0", contracts[0].Version); + } + + [TestMethod] + public void ReadContractsElementNoAttributes() + { + XmlDocument document = new XmlDocument(); + document.LoadXml(@""); + List contracts = new List(); + + ApiContract.ReadContractsElement(document.FirstChild as XmlElement, contracts); + Assert.AreEqual(1, contracts.Count); + } + + [TestMethod] + public void ReadContractsElementNoContent() + { + XmlDocument document = new XmlDocument(); + document.LoadXml(@""); + List contracts = new List(); + + ApiContract.ReadContractsElement(document.FirstChild as XmlElement, contracts); + Assert.AreEqual(0, contracts.Count); + } + } +} diff --git a/src/Utilities/UnitTests/CanonicalError_Tests.cs b/src/Utilities/UnitTests/CanonicalError_Tests.cs new file mode 100644 index 00000000000..b5304075de6 --- /dev/null +++ b/src/Utilities/UnitTests/CanonicalError_Tests.cs @@ -0,0 +1,398 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Utilities; +using CanonicalError = Microsoft.Build.Shared.CanonicalError; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class CanonicalErrorTest + { + [TestMethod] + public void EmptyOrigin() + { + ValidateToolError(@"error CS0006: Metadata file 'C:\WINDOWS\Microsoft.NET\Framework\v1.2.21213\System.dll' could not be found", "", CanonicalError.Parts.Category.Error, "CS0006", @"Metadata file 'C:\WINDOWS\Microsoft.NET\Framework\v1.2.21213\System.dll' could not be found"); + } + + [TestMethod] + public void Alink() + { + // From AL.EXE + ValidateToolError(@"ALINK: error AL1017: No target filename was specified", "ALINK", CanonicalError.Parts.Category.Error, "AL1017", @"No target filename was specified"); + } + + [TestMethod] + public void CscWithFilename() + { + // From CSC.EXE + ValidateFileNameLineColumnError(@"foo.resx(2,1): error CS0116: A namespace does not directly contain members such as fields or methods", @"foo.resx", 2, 1, CanonicalError.Parts.Category.Error, "CS0116", "A namespace does not directly contain members such as fields or methods"); + ValidateFileNameLineColumnError(@"Main.cs(17,20): warning CS0168: The variable 'foo' is declared but never used", @"Main.cs", 17, 20, CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + } + + [TestMethod] + public void VbcWithFilename() + { + // From VBC.EXE + ValidateFileNameLineError(@"C:\WINDOWS\Microsoft.NET\Framework\v1.2.x86fre\foo.resx(2) : error BC30188: Declaration expected.", @"C:\WINDOWS\Microsoft.NET\Framework\v1.2.x86fre\foo.resx", 2, CanonicalError.Parts.Category.Error, "BC30188", "Declaration expected."); + } + + [TestMethod] + public void ClWithFilename() + { + // From CL.EXE + ValidateFileNameLineError(@"foo.cpp(1) : error C2143: syntax error : missing ';' before '++'", @"foo.cpp", 1, CanonicalError.Parts.Category.Error, "C2143", "syntax error : missing ';' before '++'"); + } + + [TestMethod] + public void JscWithFilename() + { + // From JSC.EXE + ValidateFileNameLineColumnError(@"foo.resx(2,1) : error JS1135: Variable 'blech' has not been declared", @"foo.resx", 2, 1, CanonicalError.Parts.Category.Error, "JS1135", "Variable 'blech' has not been declared"); + } + + [TestMethod] + public void LinkWithFilename() + { + // From Link.exe + // Note that this is impossible to distinguish from a tool error without + // actually looking at the disk to see if the given file is there. + ValidateFileNameError(@"foo.cpp : fatal error LNK1106: invalid file or disk full: cannot seek to 0x5361", @"foo.cpp", CanonicalError.Parts.Category.Error, "LNK1106", "invalid file or disk full: cannot seek to 0x5361"); + } + + [TestMethod] + public void BscMake() + { + // From BSCMAKE.EXE + ValidateToolError(@"BSCMAKE: error BK1510 : corrupt .SBR file 'foo.cpp'", "BSCMAKE", CanonicalError.Parts.Category.Error, "BK1510", @"corrupt .SBR file 'foo.cpp'"); + } + + [TestMethod] + public void CvtRes() + { + // From CVTRES.EXE + ValidateToolError(@"CVTRES : warning CVT4001: machine type not specified; assumed X86", "CVTRES", CanonicalError.Parts.Category.Warning, "CVT4001", @"machine type not specified; assumed X86"); + ValidateToolError(@"CVTRES : fatal error CVT1103: cannot read file", "CVTRES", CanonicalError.Parts.Category.Error, "CVT1103", @"cannot read file"); + } + + [TestMethod] + public void DumpBinWithFilename() + { + // From DUMPBIN.EXE (notice that an 'LNK' error is returned). + ValidateFileNameError(@"foo.cpp : warning LNK4048: Invalid format file; ignored", @"foo.cpp", CanonicalError.Parts.Category.Warning, "LNK4048", "Invalid format file; ignored"); + } + + + [TestMethod] + public void LibWithFilename() + { + // From LIB.EXE + ValidateFileNameError(@"foo.cpp : fatal error LNK1106: invalid file or disk full: cannot seek to 0x5361", @"foo.cpp", CanonicalError.Parts.Category.Error, "LNK1106", "invalid file or disk full: cannot seek to 0x5361"); + } + + [TestMethod] + public void MlWithFilename() + { + // From ML.EXE + ValidateFileNameLineError(@"bar.h(2) : error A2008: syntax error : lksdflksj", @"bar.h", 2, CanonicalError.Parts.Category.Error, "A2008", "syntax error : lksdflksj"); + ValidateFileNameLineError(@"bar.h(2) : error A2088: END directive required at end of file", @"bar.h", 2, CanonicalError.Parts.Category.Error, "A2088", "END directive required at end of file"); + } + + [TestMethod] + public void VcDeployWithFilename() + { + // From VCDEPLOY.EXE + ValidateToolError(@"vcdeploy : error VCD0041: IIS must be installed on this machine in order for this program to function correctly.", "vcdeploy", CanonicalError.Parts.Category.Error, "VCD0041", @"IIS must be installed on this machine in order for this program to function correctly."); + } + + [TestMethod] + public void VCBuildError() + { + // From VCBUILD.EXE + ValidateFileNameLineError(@"1>c:\temp\testprefast\testprefast\testprefast.cpp(12) : error C4996: 'sprintf' was declared deprecated", @"c:\temp\testprefast\testprefast\testprefast.cpp", 12, CanonicalError.Parts.Category.Error, "C4996", "'sprintf' was declared deprecated"); + ValidateFileNameLineError(@"1234>c:\temp\testprefast\testprefast\testprefast.cpp(12) : error C4996: 'sprintf' was declared deprecated", @"c:\temp\testprefast\testprefast\testprefast.cpp", 12, CanonicalError.Parts.Category.Error, "C4996", "'sprintf' was declared deprecated"); + } + + [TestMethod] + public void FileNameLine() + { + ValidateFileNameMultiLineColumnError("foo.cpp(1):error TST0000:Text", "foo.cpp", 1, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.Category.Error, "TST0000", "Text"); + } + + [TestMethod] + public void FileNameLineLine() + { + ValidateFileNameMultiLineColumnError("foo.cpp(1-5):error TST0000:Text", "foo.cpp", 1, CanonicalError.Parts.numberNotSpecified, 5, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.Category.Error, "TST0000", "Text"); + } + + [TestMethod] + public void FileNameLineCol() + { + ValidateFileNameMultiLineColumnError("foo.cpp(1,15):error TST0000:Text", "foo.cpp", 1, 15, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.Category.Error, "TST0000", "Text"); + } + + [TestMethod] + public void FileNameLineColCol() + { + ValidateFileNameMultiLineColumnError("foo.cpp(1,15-25):error TST0000:Text", "foo.cpp", 1, 15, CanonicalError.Parts.numberNotSpecified, 25, CanonicalError.Parts.Category.Error, "TST0000", "Text"); + } + + [TestMethod] + public void FileNameLineColLineCol() + { + ValidateFileNameMultiLineColumnError("foo.cpp(1,15,2,25):error TST0000:Text", "foo.cpp", 1, 15, 2, 25, CanonicalError.Parts.Category.Error, "TST0000", "Text"); + } + + [TestMethod] + public void PathologicalFileNameWithParens() + { + // Pathological case, there is actually a file with () at the end (Doesn't work, treats the (1) as a line number anyway). + ValidateFileNameMultiLineColumnError("PathologicalFile.txt(1):error TST0000:Text", "PathologicalFile.txt", 1, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.Category.Error, "TST0000", "Text"); + } + + [TestMethod] + public void OverflowTrimmingShouldNotDropChar() + { + // A devdiv build produced a huge message like this! + string message = @"The name 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' does not exist in the current context"; + string error = @"test.cs(1,32): error CS0103: " + message; + + CanonicalError.Parts parts = CanonicalError.Parse(error); + + Helpers.VerifyAssertLineByLine(message, parts.text); + } + + [TestMethod] + public void ValidateErrorMessageWithFileName() + { + ValidateFileNameError("error CS2011: Error opening response file 'e:\foo\test.rsp' -- 'The device is not ready. '", + "", CanonicalError.Parts.Category.Error, "CS2011", "Error opening response file 'e:\foo\test.rsp' -- 'The device is not ready. '"); + } + + [TestMethod] + public void ValidateErrorMessageWithFileName2() + { + ValidateFileNameError(@"BUILDMSG: error: Path 'c:\binaries.x86chk\bin\i386\System.AddIn.Contract.dll' is not under client's root 'c:\vstamq'.", + "BUILDMSG", CanonicalError.Parts.Category.Error, "", @"Path 'c:\binaries.x86chk\bin\i386\System.AddIn.Contract.dll' is not under client's root 'c:\vstamq'."); + + ValidateFileNameError(@"BUILDMSG: error : Path 'c:\binaries.x86chk\bin\i386\System.AddIn.Contract.dll' is not under client's root 'c:\vstamq'.", + "BUILDMSG", CanonicalError.Parts.Category.Error, "", @"Path 'c:\binaries.x86chk\bin\i386\System.AddIn.Contract.dll' is not under client's root 'c:\vstamq'."); + } + + [TestMethod] + public void ValidateErrorMessageWithFileName3() + { + ValidateNormalMessage(@"BUILDMSG: errorgarbage: Path 'c:\binaries.x86chk\bin\i386\System.AddIn.Contract.dll' is not under client's root 'c:\vstamq'."); + + ValidateNormalMessage(@"BUILDMSG: errorgarbage : Path 'c:\binaries.x86chk\bin\i386\System.AddIn.Contract.dll' is not under client's root 'c:\vstamq'."); + } + + [TestMethod] + public void ValidateErrorMessageVariableNotUsed() + { + // (line) + ValidateFileNameMultiLineColumnError("Main.cs():Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // This one actually falls under the (line-line) category. I'm not going to tweak the regex for this incorrect input just so we can + // pretend -3 == 0, and just leaving it here for completeness + ValidateFileNameMultiLineColumnError("Main.cs(-3):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, 3, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // (line-line) + ValidateFileNameMultiLineColumnError("Main.cs(-):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(-2):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, 2, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(1-):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", 1, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // (line,col) + ValidateFileNameMultiLineColumnError("Main.cs(,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,2):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, 2, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(1,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", 1, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // Similarly to the previous odd case, this really falls under (line,col-col). Included for completeness, even if results are + // not intuitive + ValidateFileNameMultiLineColumnError("Main.cs(,-2):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, 2, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(-1,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // (line,col-col) + ValidateFileNameMultiLineColumnError("Main.cs(,-):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(2,-):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", 2, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,4-):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, 4, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,-6):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, 6, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(-1,-):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // (line,col,line,col) + ValidateFileNameMultiLineColumnError("Main.cs(,,,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(2,,,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", 2, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,3,,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, 3, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,,4,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, 4, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,,,5):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, 5, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + // negative numbers are not matched at all for this format and I don't think we should tweak regexes to accept invalid input + // in that form + ValidateFileNameMultiLineColumnError("Main.cs(-2,,1,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,-3,,2):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(3,,-4,):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + + ValidateFileNameMultiLineColumnError("Main.cs(,4,,-5):Command line warning CS0168: The variable 'foo' is declared but never used", + "Main.cs", CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, + CanonicalError.Parts.Category.Warning, "CS0168", "The variable 'foo' is declared but never used"); + } + + #region Support functions. + private static void AssertEqual(string str1, string str2) + { + if (str1 != str2) + { + str1 = null == str1 ? "{null}" : str1; + str2 = null == str2 ? "{null}" : str2; + + string message = "Regression: string compare '" + str1 + "'!='" + str2 + "'"; + + Assert.IsTrue(str1 == str2, message); + } + } + + private static void AssertEqual(int int1, int int2) + { + if (int1 != int2) + { + string message = "Regression: int compare '" + int1.ToString() + "'!='" + int2.ToString() + "'"; + + Assert.IsTrue(int1 == int2, message); + } + } + + private static void AssertEqual(CanonicalError.Parts.Category cat1, CanonicalError.Parts.Category cat2) + { + if (cat1 != cat2) + { + string message = "Regression: category compare '" + cat1.ToString() + "'!='" + cat2.ToString() + "'"; + + Assert.IsTrue(cat1 == cat2, message); + } + } + + private static void ValidateToolError(string message, string tool, CanonicalError.Parts.Category severity, string code, string text) + { + CanonicalError.Parts errorParts = CanonicalError.Parse(message); + + Assert.IsNotNull(errorParts, "The message '" + message + "' could not be interpretted."); + AssertEqual(errorParts.origin, tool); + AssertEqual(errorParts.category, severity); + AssertEqual(errorParts.code, code); + AssertEqual(errorParts.text, text); + AssertEqual(errorParts.line, CanonicalError.Parts.numberNotSpecified); + AssertEqual(errorParts.column, CanonicalError.Parts.numberNotSpecified); + AssertEqual(errorParts.endLine, CanonicalError.Parts.numberNotSpecified); + AssertEqual(errorParts.endColumn, CanonicalError.Parts.numberNotSpecified); + } + + private static void ValidateFileNameMultiLineColumnError(string message, string filename, int line, int column, int endLine, int endColumn, CanonicalError.Parts.Category severity, string code, string text) + { + CanonicalError.Parts errorParts = CanonicalError.Parse(message); + + Assert.IsNotNull(errorParts, "The message '" + message + "' could not be interpretted."); + AssertEqual(errorParts.origin, filename); + AssertEqual(errorParts.category, severity); + AssertEqual(errorParts.code, code); + AssertEqual(errorParts.text, text); + AssertEqual(errorParts.line, line); + AssertEqual(errorParts.column, column); + AssertEqual(errorParts.endLine, endLine); + AssertEqual(errorParts.endColumn, endColumn); + } + + private static void ValidateFileNameLineColumnError(string message, string filename, int line, int column, CanonicalError.Parts.Category severity, string code, string text) + { + ValidateFileNameMultiLineColumnError(message, filename, line, column, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, severity, code, text); + } + + private static void ValidateFileNameLineError(string message, string filename, int line, CanonicalError.Parts.Category severity, string code, string text) + { + ValidateFileNameMultiLineColumnError(message, filename, line, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, severity, code, text); + } + + private static void ValidateFileNameError(string message, string filename, CanonicalError.Parts.Category severity, string code, string text) + { + ValidateFileNameMultiLineColumnError(message, filename, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, CanonicalError.Parts.numberNotSpecified, severity, code, text); + } + + private static void ValidateNormalMessage(string message) + { + CanonicalError.Parts errorParts = CanonicalError.Parse(message); + + Assert.IsNull(errorParts, "The message '" + message + "' is an error/warning message"); + } + } + #endregion +} + + + + + diff --git a/src/Utilities/UnitTests/CommandLineBuilder_Tests.cs b/src/Utilities/UnitTests/CommandLineBuilder_Tests.cs new file mode 100644 index 00000000000..bd4e958d1f6 --- /dev/null +++ b/src/Utilities/UnitTests/CommandLineBuilder_Tests.cs @@ -0,0 +1,443 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CommandLineBuilderTest + { + /* + * Method: AppendSwitchSimple + * + * Just append a simple switch. + */ + [TestMethod] + public void AppendSwitchSimple() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/a"); + c.AppendSwitch("-b"); + Assert.AreEqual("/a -b", c.ToString()); + } + + /* + * Method: AppendSwitchWithStringParameter + * + * Append a switch that has a string parameter. + */ + [TestMethod] + public void AppendSwitchWithStringParameter() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/animal:", "dog"); + Assert.AreEqual("/animal:dog", c.ToString()); + } + + /* + * Method: AppendSwitchWithSpacesInParameter + * + * This should trigger implicit quoting. + */ + [TestMethod] + public void AppendSwitchWithSpacesInParameter() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/animal:", "dog and pony"); + Assert.AreEqual("/animal:\"dog and pony\"", c.ToString()); + } + + /// + /// Test for AppendSwitchIfNotNull for the ITaskItem version + /// + [TestMethod] + public void AppendSwitchWithSpacesInParameterTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/animal:", (ITaskItem)new TaskItem("dog and pony")); + Assert.AreEqual("/animal:\"dog and pony\"", c.ToString()); + } + + /* + * Method: AppendLiteralSwitchWithSpacesInParameter + * + * Implicit quoting should not happen. + */ + [TestMethod] + public void AppendLiteralSwitchWithSpacesInParameter() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchUnquotedIfNotNull("/animal:", "dog and pony"); + Assert.AreEqual("/animal:dog and pony", c.ToString()); + } + + /* + * Method: AppendTwoStringsEnsureNoSpace + * + * When appending two comma-delimted strings, there should be no space before the comma. + */ + [TestMethod] + public void AppendTwoStringsEnsureNoSpace() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNamesIfNotNull(new string[] { "Form1.resx", @"built\Form1.resources" }, ","); + + // There shouldn't be a space before or after the comma + // Tools like resgen require comma-delimited lists to be bumped up next to each other. + Assert.AreEqual(@"Form1.resx,built\Form1.resources", c.ToString()); + } + + /* + * Method: AppendSourcesArray + * + * Append several sources files using JoinAppend + */ + [TestMethod] + public void AppendSourcesArray() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNamesIfNotNull(new string[] { "Mercury.cs", "Venus.cs", "Earth.cs" }, " "); + + // Managed compilers use this function to append sources files. + Assert.AreEqual(@"Mercury.cs Venus.cs Earth.cs", c.ToString()); + } + + /* + * Method: AppendSourcesArrayWithDashes + * + * Append several sources files starting with dashes using JoinAppend + */ + [TestMethod] + public void AppendSourcesArrayWithDashes() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNamesIfNotNull(new string[] { "-Mercury.cs", "-Venus.cs", "-Earth.cs" }, " "); + + // Managed compilers use this function to append sources files. + Assert.AreEqual(@".\-Mercury.cs .\-Venus.cs .\-Earth.cs", c.ToString()); + } + + /// + /// Test AppendFileNamesIfNotNull, the ITaskItem version + /// + [TestMethod] + public void AppendSourcesArrayWithDashesTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNamesIfNotNull(new TaskItem[] { new TaskItem("-Mercury.cs"), null, new TaskItem("Venus.cs"), new TaskItem("-Earth.cs") }, " "); + + // Managed compilers use this function to append sources files. + Assert.AreEqual(@".\-Mercury.cs Venus.cs .\-Earth.cs", c.ToString()); + } + + /* + * Method: JoinAppendEmpty + * + * Append append and empty array. Result should be NOP. + */ + [TestMethod] + public void JoinAppendEmpty() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNamesIfNotNull(new string[] { "" }, " "); + + // Managed compilers use this function to append sources files. + Assert.AreEqual(@"", c.ToString()); + } + + /* + * Method: JoinAppendNull + * + * Append append and empty array. Result should be NOP. + */ + [TestMethod] + public void JoinAppendNull() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNamesIfNotNull((string[])null, " "); + + // Managed compilers use this function to append sources files. + Assert.AreEqual(@"", c.ToString()); + } + + /// + /// Append a switch with parameter array, quoting + /// + [TestMethod] + public void AppendSwitchWithParameterArrayQuoting() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/something"); + c.AppendSwitchIfNotNull("/switch:", new string[] { "Mer cury.cs", "Ve nus.cs", "Ear th.cs" }, ","); + + // Managed compilers use this function to append sources files. + Assert.AreEqual("/something /switch:\"Mer cury.cs\",\"Ve nus.cs\",\"Ear th.cs\"", c.ToString()); + } + + /// + /// Append a switch with parameter array, quoting, ITaskItem version + /// + [TestMethod] + public void AppendSwitchWithParameterArrayQuotingTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/something"); + c.AppendSwitchIfNotNull("/switch:", new TaskItem[] { new TaskItem("Mer cury.cs"), null, new TaskItem("Ve nus.cs"), new TaskItem("Ear th.cs") }, ","); + + // Managed compilers use this function to append sources files. + Assert.AreEqual("/something /switch:\"Mer cury.cs\",,\"Ve nus.cs\",\"Ear th.cs\"", c.ToString()); + } + + /// + /// Append a switch with parameter array, no quoting + /// + [TestMethod] + public void AppendSwitchWithParameterArrayNoQuoting() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/something"); + c.AppendSwitchUnquotedIfNotNull("/switch:", new string[] { "Mer cury.cs", "Ve nus.cs", "Ear th.cs" }, ","); + + // Managed compilers use this function to append sources files. + Assert.AreEqual("/something /switch:Mer cury.cs,Ve nus.cs,Ear th.cs", c.ToString()); + } + + /// + /// Append a switch with parameter array, no quoting, ITaskItem version + /// + [TestMethod] + public void AppendSwitchWithParameterArrayNoQuotingTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/something"); + c.AppendSwitchUnquotedIfNotNull("/switch:", new TaskItem[] { new TaskItem("Mer cury.cs"), null, new TaskItem("Ve nus.cs"), new TaskItem("Ear th.cs") }, ","); + + // Managed compilers use this function to append sources files. + Assert.AreEqual("/something /switch:Mer cury.cs,,Ve nus.cs,Ear th.cs", c.ToString()); + } + + /// + /// Appends a single file name + /// + [TestMethod] + public void AppendSingleFileName() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/something"); + c.AppendFileNameIfNotNull("-Mercury.cs"); + c.AppendFileNameIfNotNull("Mercury.cs"); + c.AppendFileNameIfNotNull("Mer cury.cs"); + + // Managed compilers use this function to append sources files. + Assert.AreEqual("/something .\\-Mercury.cs Mercury.cs \"Mer cury.cs\"", c.ToString()); + } + + /// + /// Appends a single file name, ITaskItem version + /// + [TestMethod] + public void AppendSingleFileNameTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitch("/something"); + c.AppendFileNameIfNotNull((ITaskItem)new TaskItem("-Mercury.cs")); + c.AppendFileNameIfNotNull((ITaskItem)new TaskItem("Mercury.cs")); + c.AppendFileNameIfNotNull((ITaskItem)new TaskItem("Mer cury.cs")); + + // Managed compilers use this function to append sources files. + Assert.AreEqual("/something .\\-Mercury.cs Mercury.cs \"Mer cury.cs\"", c.ToString()); + } + + /// + /// Verify that we throw an exception correctly for the case where we don't have a switch name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AppendSingleFileNameWithQuotes() + { + // Cannot have escaped quotes in a file name + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendFileNameIfNotNull("string with \"quotes\""); + + Assert.AreEqual("\"string with \\\"quotes\\\"\"", c.ToString()); + } + + /// + /// Trigger escaping of literal quotes. + /// + [TestMethod] + public void AppendSwitchWithLiteralQuotesInParameter() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", "LSYSTEM_COMPATIBLE_ASSEMBLY_NAME=L\"Microsoft.Windows.SystemCompatible\""); + Assert.AreEqual("/D\"LSYSTEM_COMPATIBLE_ASSEMBLY_NAME=L\\\"Microsoft.Windows.SystemCompatible\\\"\"", c.ToString()); + } + + /// + /// Trigger escaping of literal quotes. + /// + [TestMethod] + public void AppendSwitchWithLiteralQuotesInParameter2() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"ASSEMBLY_KEY_FILE=""c:\\foo\\FinalKeyFile.snk"""); + Assert.AreEqual(@"/D""ASSEMBLY_KEY_FILE=\""c:\\foo\\FinalKeyFile.snk\""""", c.ToString()); + } + + /// + /// Trigger escaping of literal quotes. This time, a double set of literal quotes. + /// + [TestMethod] + public void AppendSwitchWithLiteralQuotesInParameter3() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"""A B"" and ""C"""); + Assert.AreEqual(@"/D""\""A B\"" and \""C\""""", c.ToString()); + } + + /// + /// When a value contains a backslash, it doesn't normally need escaping. + /// + [TestMethod] + public void AppendQuotableSwitchContainingBackslash() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"A \B"); + Assert.AreEqual(@"/D""A \B""", c.ToString()); + } + + /// + /// Backslashes before quotes need escaping themselves. + /// + [TestMethod] + public void AppendQuotableSwitchContainingBackslashBeforeLiteralQuote() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"A"" \""B"); + Assert.AreEqual(@"/D""A\"" \\\""B""", c.ToString()); + } + + /// + /// Don't quote if not asked to + /// + [TestMethod] + public void AppendSwitchUnquotedIfNotNull() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchUnquotedIfNotNull("/D", @"A"" \""B"); + Assert.AreEqual(@"/DA"" \""B", c.ToString()); + } + + /// + /// When a value ends with a backslash, that certainly should be escaped if it's + /// going to be quoted. + /// + [TestMethod] + public void AppendQuotableSwitchEndingInBackslash() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"A B\"); + Assert.AreEqual(@"/D""A B\\""", c.ToString()); + } + + /// + /// Backslashes don't need to be escaped if the string isn't going to get quoted. + /// + [TestMethod] + public void AppendNonQuotableSwitchEndingInBackslash() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"AB\"); + Assert.AreEqual(@"/DAB\", c.ToString()); + } + + /// + /// Quoting of hyphens + /// + [TestMethod] + public void AppendQuotableSwitchWithHyphen() + { + CommandLineBuilder c = new CommandLineBuilder(/* do not quote hyphens*/); + c.AppendSwitchIfNotNull("/D", @"foo-bar"); + Assert.AreEqual(@"/Dfoo-bar", c.ToString()); + } + + /// + /// Quoting of hyphens 2 + /// + [TestMethod] + public void AppendQuotableSwitchWithHyphenQuoting() + { + CommandLineBuilder c = new CommandLineBuilder(true /* quote hyphens*/); + c.AppendSwitchIfNotNull("/D", @"foo-bar"); + Assert.AreEqual(@"/D""foo-bar""", c.ToString()); + } + + /// + /// Appends an ITaskItem item spec as a parameter + /// + [TestMethod] + public void AppendSwitchTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(true); + c.AppendSwitchIfNotNull("/D", new TaskItem(@"foo-bar")); + Assert.AreEqual(@"/D""foo-bar""", c.ToString()); + } + + /// + /// Appends an ITaskItem item spec as a parameter + /// + [TestMethod] + public void AppendSwitchUnQuotedTaskItem() + { + CommandLineBuilder c = new CommandLineBuilder(true); + c.AppendSwitchUnquotedIfNotNull("/D", new TaskItem(@"foo-bar")); + Assert.AreEqual(@"/Dfoo-bar", c.ToString()); + } + + /// + /// Odd number of literal quotes. This should trigger an exception, because command line parsers + /// generally can't handle this case. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AppendSwitchWithOddNumberOfLiteralQuotesInParameter() + { + CommandLineBuilder c = new CommandLineBuilder(); + c.AppendSwitchIfNotNull("/D", @"ASSEMBLY_KEY_FILE=""c:\\foo\\FinalKeyFile.snk"); + } + + internal class TestCommandLineBuilder : CommandLineBuilder + { + internal void TestVerifyThrow(string switchName, string parameter) + { + VerifyThrowNoEmbeddedDoubleQuotes(switchName, parameter); + } + + protected override void VerifyThrowNoEmbeddedDoubleQuotes(string switchName, string parameter) + { + base.VerifyThrowNoEmbeddedDoubleQuotes(switchName, parameter); + } + } + + /// + /// Test the else of VerifyThrowNOEmbeddedDouble quotes where the switch name is not empty or null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void TestVerifyThrowElse() + { + TestCommandLineBuilder c = new TestCommandLineBuilder(); + c.TestVerifyThrow("SuperSwitch", @"Parameter"); + c.TestVerifyThrow("SuperSwitch", @"Para""meter"); + } + } +} diff --git a/src/Utilities/UnitTests/EventArgsFormatting_Tests.cs b/src/Utilities/UnitTests/EventArgsFormatting_Tests.cs new file mode 100644 index 00000000000..e1495b8f0c2 --- /dev/null +++ b/src/Utilities/UnitTests/EventArgsFormatting_Tests.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class EventArgsFormattingTests + { + [TestMethod] + public void NoLineInfoFormatEventMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 0, 0, 0, 0, 0); + Assert.AreEqual( + "source.cs : CS error 312: Missing ;", s); + } + + // Valid forms for line/col number patterns: + // (line) or (line-line) or (line,col) or (line,col-col) or (line,col,line,col) + [TestMethod] + public void LineNumberRange() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 0, 0, 0); + Assert.AreEqual( + "source.cs(1-2): CS error 312: Missing ;", s); + } + + [TestMethod] + public void ColumnNumberRange() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 0, 0, 1, 2, 0); + Assert.AreEqual( + "source.cs : CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 3, 4, 0); + Assert.AreEqual( + "source.cs(1,3,2,4): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange2() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 0, 3, 4, 0); + Assert.AreEqual( + "source.cs(1,3-4): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange3() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 3, 0, 0); + Assert.AreEqual( + "source.cs(1-2,3): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange4() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 0, 3, 0); + Assert.AreEqual( + "source.cs(1-2): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange5() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 0, 2, 0, 0); + Assert.AreEqual( + "source.cs(1,2): CS error 312: Missing ;", s); + } + + [TestMethod] + public void BasicFormatEventMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 42, 0, 0, 0, 0); + Assert.AreEqual( + "source.cs(42): CS error 312: Missing ;", s); + } + + [TestMethod] + public void EscapeCarriageReturnMessages() + { + BuildErrorEventArgs error = new BuildErrorEventArgs("CS", "312", "source.cs", 42, 0, 0, 0, "message\r Hello", "help", "sender"); + BuildWarningEventArgs warning = new BuildWarningEventArgs("CS", "312", "source.cs", 42, 0, 0, 0, "message\r Hello", "help", "sender"); + + // Testing the method in Shared.EventArgsFormatting directly + string errorString = EventArgsFormatting.FormatEventMessage(error, true); + string warningString = EventArgsFormatting.FormatEventMessage(warning, true); + string errorString2 = EventArgsFormatting.FormatEventMessage(error, false); + string warningString2 = EventArgsFormatting.FormatEventMessage(warning, false); + + Assert.AreEqual("source.cs(42): CS error 312: message\\r Hello", errorString); + Assert.AreEqual("source.cs(42): CS warning 312: message\\r Hello", warningString); + + Assert.AreEqual("source.cs(42): CS error 312: message\r Hello", errorString2); + Assert.AreEqual("source.cs(42): CS warning 312: message\r Hello", warningString2); + } + + [TestMethod] + public void ExactLocationFormatEventMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 233, 236, 4, 8, 0); + Assert.AreEqual( + "source.cs(233,4,236,8): CS error 312: Missing ;", s); + } + + [TestMethod] + public void NullMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + null, "312", "source.cs", 233, 236, 4, 8, 0); + // No exception was thrown + + } + + /// + /// Mainline test FormatEventMessage(BuildErrorEvent) 's common case + /// + [TestMethod] + public void FormatEventMessageOnBEEA() + { + MyLogger l = new MyLogger(); + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + string s = l.FormatErrorEvent(beea); + Assert.AreEqual( + "file.vb(42): VBC error 31415: Some long message", s); + } + + /// + /// Mainline test FormatEventMessage(BuildWarningEvent) 's common case + /// + [TestMethod] + public void FormatEventMessageOnBWEA() + { + MyLogger l = new MyLogger(); + BuildWarningEventArgs bwea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + string s = l.FormatWarningEvent(bwea); + Assert.AreEqual( + "file.vb(42): VBC warning 31415: Some long message", s); + } + + /// + /// Check null handling + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void FormatEventMessageOnNullBEEA() + { + MyLogger l = new MyLogger(); + BuildErrorEventArgs beea = null; + string s = l.FormatErrorEvent(beea); + } + + /// + /// Check null handling + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void FormatEventMessageOnNullBWEA() + { + MyLogger l = new MyLogger(); + BuildWarningEventArgs bwea = null; + string s = l.FormatWarningEvent(bwea); + } + } + + /// + /// Minimal logger implementation + /// + internal class MyLogger : Logger + { + public override void Initialize(IEventSource eventSource) + { + // do nothing + } + } +} + diff --git a/src/Utilities/UnitTests/GetTargetPlatformReferences_Tests.cs b/src/Utilities/UnitTests/GetTargetPlatformReferences_Tests.cs new file mode 100644 index 00000000000..77d9e58adab --- /dev/null +++ b/src/Utilities/UnitTests/GetTargetPlatformReferences_Tests.cs @@ -0,0 +1,288 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class GetTargetPlatformReferences_Tests + { + /// + /// Location of the fake SDK structure + /// + static string fakeStructureRoot = null; + + /// + /// Setup the fake SDK structure used by these tests + /// + [ClassInitialize] + public static void ClassInit(TestContext context) + { + fakeStructureRoot = MakeFakeSDKStructure(); + } + + /// + /// Clean up the fake SDK structure used by these tests + /// + [ClassCleanup] + public static void ClassCleanup() + { + if (fakeStructureRoot != null) + { + if (FileUtilities.DirectoryExistsNoThrow(fakeStructureRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(fakeStructureRoot, true); + } + } + } + + /// + /// Verify GetTargetPlatformReferences returns nothing when there's no matching root SDK + /// + [TestMethod] + public void NoRootSdk() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("MissingSDK", "1.1", "MissingPlatform", "0.0.0.0", "0.0.0.0", fakeStructureRoot, null); + Assert.AreEqual(0, winmds.Length); + } + + /// + /// Verify GetTargetPlatformReferences still returns valid output when passed a legacy-style SDK + /// + [TestMethod] + public void LegacySdk() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences(null, null, "OldSdk", null, "5.0", fakeStructureRoot, null); + + Assert.AreEqual(2, winmds.Length); + Assert.AreEqual(Path.Combine(fakeStructureRoot, "OldSdk\\5.0\\References\\CommonConfiguration\\Neutral\\Another.winmd"), winmds[0]); + Assert.AreEqual(Path.Combine(fakeStructureRoot, "OldSdk\\5.0\\References\\CommonConfiguration\\Neutral\\Windows.winmd"), winmds[1]); + } + + /// + /// Verify GetTargetPlatformReferences returns nothing when there's no matching platforms directory + /// + [TestMethod] + public void MissingPlatformDirectory() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "MissingPlatform", "0.0.0.0", "0.0.0.0", fakeStructureRoot, null); + Assert.AreEqual(0, winmds.Length); + } + + /// + /// Verify GetTargetPlatformReferences returns nothing when there's no Platform.xml in the platforms directory + /// + [TestMethod] + public void MissingPlatformXml() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "NoXml", "0.0.0.0", "1.0.1.0", fakeStructureRoot, null); + Assert.AreEqual(0, winmds.Length); + } + + /// + /// Verify GetTargetPlatformReferences returns nothing when there's an invalid Platform.xml in the platforms directory + /// + [TestMethod] + public void BadPlatformXml() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "BadXml", "0.0.0.0", "1.0.2.2", fakeStructureRoot, null); + Assert.AreEqual(0, winmds.Length); + } + + /// + /// Verify GetTargetPlatformReferences returns nothing when the platform.xml is valid but there are no contracts defined + /// + [TestMethod] + public void NoContracts() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "NoContracts", "0.0.0.0", "3.0.5.4", fakeStructureRoot, null); + Assert.AreEqual(0, winmds.Length); + } + + /// + /// Verify GetTargetPlatformReferences returns all winmds defined in the contracts + /// + [TestMethod] + public void ValidContracts() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "SomeContracts", "0.0.0.0", "1.0.9.9", fakeStructureRoot, null); + string referencesRoot = Path.Combine(fakeStructureRoot, "RootSdk\\1.1\\References"); + + Assert.AreEqual(4, winmds.Length); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Core\\0.7.0.0\\Windows.Core.winmd"), winmds[0]); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Foundation\\1.0.0.0\\OtherNamesAreStillFine.winmd"), winmds[1]); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Foundation.OtherStuff\\1.5.0.0\\MultipleWinmdsAreFine.winmd"), winmds[2]); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Foundation.OtherStuff\\1.5.0.0\\Windows.Foundation.OtherStuff.winmd"), winmds[3]); + } + + /// + /// Verify GetTargetPlatformReferences returns all winmds defined in the valid contracts, but ignores missing + /// contract directories + /// + [TestMethod] + public void MissingContract() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "SomeContracts", "0.0.0.0", "2.3.4.5", fakeStructureRoot, null); + string referencesRoot = Path.Combine(fakeStructureRoot, "RootSdk\\1.1\\References"); + + Assert.AreEqual(1, winmds.Length); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Core\\0.7.0.0\\Windows.Core.winmd"), winmds[0]); + } + + /// + /// Verify GetTargetPlatformReferences returns all winmds defined in the valid contracts, but ignores empty + /// contract directories + /// + [TestMethod] + public void EmptyContract() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "SomeContracts", "0.0.0.0", "3.9.9.4", fakeStructureRoot, null); + string referencesRoot = Path.Combine(fakeStructureRoot, "RootSdk\\1.1\\References"); + + Assert.AreEqual(1, winmds.Length); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Core\\0.7.0.0\\Windows.Core.winmd"), winmds[0]); + } + + /// + /// Verify GetTargetPlatformReferences returns all winmds defined in the valid contracts, but ignores + /// contract directories that don't contain winmds + /// + [TestMethod] + public void NonEmptyContractFolderButNoWinMDs() + { + string[] winmds = ToolLocationHelper.GetTargetPlatformReferences("RootSdk", "1.1", "SomeContracts", "0.0.0.0", "4.3.2.8", fakeStructureRoot, null); + string referencesRoot = Path.Combine(fakeStructureRoot, "RootSdk\\1.1\\References"); + + Assert.AreEqual(1, winmds.Length); + Assert.AreEqual(Path.Combine(referencesRoot, "Windows.Core\\0.7.0.0\\Windows.Core.winmd"), winmds[0]); + } + + /// + /// Generate a fake SDK structure + /// + /// + private static string MakeFakeSDKStructure() + { + // no contracts + string platformManifest1 = @" + + "; + + // some contracts + string platformManifest2 = @" + + + + + + "; + + // one missing contract, other good + string platformManifest3 = @" + + + + + "; + + // one empty contracts directory, other good + string platformManifest4 = @" + + + + + "; + + // one contracts directory without winmds in it, other good + string platformManifest5 = @" + + + + + "; + + string fakeSdkRoot = FileUtilities.GetTemporaryDirectory(); + + // Legacy-style SDK + string oldSdkRoot = Path.Combine(fakeSdkRoot, "OldSdk\\5.0"); + Directory.CreateDirectory(oldSdkRoot); + File.WriteAllText(Path.Combine(oldSdkRoot, "SDKManifest.xml"), "Hello"); + Directory.CreateDirectory(Path.Combine(oldSdkRoot, "References\\CommonConfiguration\\Neutral")); + File.WriteAllText(Path.Combine(oldSdkRoot, "References\\CommonConfiguration\\Neutral\\Windows.winmd"), "Hello"); + File.WriteAllText(Path.Combine(oldSdkRoot, "References\\CommonConfiguration\\Neutral\\Another.winmd"), "Hello"); + + // OneCore-style SDK + string rootSdkRoot = Path.Combine(fakeSdkRoot, "RootSdk\\1.1"); + Directory.CreateDirectory(rootSdkRoot); + File.WriteAllText(Path.Combine(rootSdkRoot, "SDKManifest.xml"), "Hello"); + + // -- References -- + string referencesRoot = Path.Combine(rootSdkRoot, "References"); + + Directory.CreateDirectory(Path.Combine(referencesRoot, "Windows.Core\\0.7.0.0")); + File.WriteAllText(Path.Combine(referencesRoot, "Windows.Core\\0.7.0.0\\Windows.Core.winmd"), "Hello"); + + Directory.CreateDirectory(Path.Combine(referencesRoot, "Windows.Foundation\\1.0.0.0")); + File.WriteAllText(Path.Combine(referencesRoot, "Windows.Foundation\\1.0.0.0\\OtherNamesAreStillFine.winmd"), "Hello"); + + Directory.CreateDirectory(Path.Combine(referencesRoot, "Windows.Foundation.OtherStuff\\1.5.0.0")); + File.WriteAllText(Path.Combine(referencesRoot, "Windows.Foundation.OtherStuff\\1.5.0.0\\Windows.Foundation.OtherStuff.winmd"), "Hello"); + File.WriteAllText(Path.Combine(referencesRoot, "Windows.Foundation.OtherStuff\\1.5.0.0\\MultipleWinmdsAreFine.winmd"), "Hello"); + + Directory.CreateDirectory(Path.Combine(referencesRoot, "Empty\\0.8.0.0")); + + Directory.CreateDirectory(Path.Combine(referencesRoot, "NonEmptyButNoWinmds\\0.8.0.0")); + File.WriteAllText(Path.Combine(referencesRoot, "NonEmptyButNoWinmds\\0.8.0.0\\NonEmptyButNoWinmds.xml"), "Hello"); + + // -- Platforms -- + string platformsRoot = Path.Combine(rootSdkRoot, "Platforms"); + + // bad: missing platform.xml + Directory.CreateDirectory(Path.Combine(platformsRoot, "NoXml\\1.0.1.0")); + + // bad: invalid platform.xml + Directory.CreateDirectory(Path.Combine(platformsRoot, "BadXml\\1.0.2.2")); + File.WriteAllText(Path.Combine(platformsRoot, "BadXml\\1.0.2.2\\Platform.xml"), "||||"); + + // good: no contracts + Directory.CreateDirectory(Path.Combine(platformsRoot, "NoContracts\\3.0.5.4")); + CleanupAndWrite(Path.Combine(platformsRoot, "NoContracts\\3.0.5.4\\Platform.xml"), platformManifest1); + + // good: all contracts exist + Directory.CreateDirectory(Path.Combine(platformsRoot, "SomeContracts\\1.0.9.9")); + CleanupAndWrite(Path.Combine(platformsRoot, "SomeContracts\\1.0.9.9\\Platform.xml"), platformManifest2); + + // partially good: one missing contract + Directory.CreateDirectory(Path.Combine(platformsRoot, "SomeContracts\\2.3.4.5")); + CleanupAndWrite(Path.Combine(platformsRoot, "SomeContracts\\2.3.4.5\\Platform.xml"), platformManifest3); + + // partially good: one empty contract directory + Directory.CreateDirectory(Path.Combine(platformsRoot, "SomeContracts\\3.9.9.4")); + CleanupAndWrite(Path.Combine(platformsRoot, "SomeContracts\\3.9.9.4\\Platform.xml"), platformManifest4); + + // partially good: one contract directory without winmds in it + Directory.CreateDirectory(Path.Combine(platformsRoot, "SomeContracts\\4.3.2.8")); + CleanupAndWrite(Path.Combine(platformsRoot, "SomeContracts\\4.3.2.8\\Platform.xml"), platformManifest5); + + return fakeSdkRoot; + } + + /// + /// Fixes up the contents passed to it and writes the updated string to disk + /// + private static void CleanupAndWrite(string path, string content) + { + File.WriteAllText(path, ObjectModelHelpers.CleanupFileContents(content)); + } + } +} diff --git a/src/Utilities/UnitTests/Logger_Tests.cs b/src/Utilities/UnitTests/Logger_Tests.cs new file mode 100644 index 00000000000..8c1f662687e --- /dev/null +++ b/src/Utilities/UnitTests/Logger_Tests.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Reflection; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + internal class EmptyLogger : Logger + { + /// + /// Create a logger instance with a specific verbosity. + /// + /// Verbosity level. + public EmptyLogger(LoggerVerbosity verbosity) + { + this.Verbosity = verbosity; + } + + /// + /// Subscribe to events. + /// + /// + public override void Initialize(IEventSource eventSource) + { + } + } + + [TestClass] + public class Logger_Tests + { + [TestMethod] + public void ExerciseMiscProperties() + { + EmptyLogger logger = new EmptyLogger(LoggerVerbosity.Diagnostic); + logger.Parameters = "Parameters"; + Assert.IsTrue(string.Compare(logger.Parameters, "Parameters", StringComparison.OrdinalIgnoreCase) == 0); + Assert.AreEqual(LoggerVerbosity.Diagnostic, logger.Verbosity); + logger.Shutdown(); + } + /// + /// Exercises every combination of the Logger.IsVerbosityAtLeast method. + /// + [TestMethod] + public void IsVerbosityAtLeast() + { + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new EmptyLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(true, + (new EmptyLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + } + } +} diff --git a/src/Utilities/UnitTests/Microsoft.Build.Utilities.UnitTests.csproj b/src/Utilities/UnitTests/Microsoft.Build.Utilities.UnitTests.csproj new file mode 100644 index 00000000000..3b36f644677 --- /dev/null +++ b/src/Utilities/UnitTests/Microsoft.Build.Utilities.UnitTests.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {31768BCA-AF65-4D9C-B3AC-B6DA3338B1BE} + Library + Microsoft.Build.Utilities.UnitTests + Microsoft.Build.Utilities.UnitTests + + + + + + + + + true + + + + + + + + NativeMethods.cs + + + + + + + + + + + + + + + + + + Designer + + + + + + App.config + Designer + + + + + + + + {828566EE-6F6A-4EF4-98B0-513F7DF9C628} + Microsoft.Build.Utilities + + + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58} + Microsoft.Build + + + {59A73FE0-D3B7-4299-9063-3A587D429AF4} + Microsoft.Build.Tasks + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + + + \ No newline at end of file diff --git a/src/Utilities/UnitTests/MockEngine.cs b/src/Utilities/UnitTests/MockEngine.cs new file mode 100644 index 00000000000..2c5c54bd396 --- /dev/null +++ b/src/Utilities/UnitTests/MockEngine.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + /*************************************************************************** + * + * Class: MockEngine + * + * In order to execute tasks, we have to pass in an Engine object, so the + * task can log events. It doesn't have to be the real Engine object, just + * something that implements the IBuildEngine2 interface. So, we mock up + * a fake engine object here, so we're able to execute tasks from the unit tests. + * + * The unit tests could have instantiated the real Engine object, but then + * we would have had to take a reference onto the Microsoft.Build.Engine assembly, which + * is somewhat of a no-no for task assemblies. + * + **************************************************************************/ + sealed internal class MockEngine : IBuildEngine3 + { + private bool _isRunningMultipleNodes; + private int _messages = 0; + private int _warnings = 0; + private int _errors = 0; + private int _commandLine = 0; + private string _log = ""; + private string _upperLog = null; + private MessageImportance _minimumMessageImportance = MessageImportance.Low; + + public MessageImportance MinimumMessageImportance + { + get { return _minimumMessageImportance; } + set { _minimumMessageImportance = value; } + } + + internal int Messages + { + set { _messages = value; } + get { return _messages; } + } + + internal int Warnings + { + set { _warnings = value; } + get { return _warnings; } + } + + internal int Errors + { + set { _errors = value; } + get { return _errors; } + } + + internal int CommandLine + { + set { _commandLine = value; } + get { return _commandLine; } + } + + public bool IsRunningMultipleNodes + { + get { return _isRunningMultipleNodes; } + set { _isRunningMultipleNodes = value; } + } + + public void LogErrorEvent(BuildErrorEventArgs eventArgs) + { + Console.WriteLine(EventArgsFormatting.FormatEventMessage(eventArgs)); + _log += EventArgsFormatting.FormatEventMessage(eventArgs); + ++_errors; + + _log += "\n"; + _upperLog = null; + } + + public void LogWarningEvent(BuildWarningEventArgs eventArgs) + { + Console.WriteLine(EventArgsFormatting.FormatEventMessage(eventArgs)); + _log += EventArgsFormatting.FormatEventMessage(eventArgs); + ++_warnings; + + _log += "\n"; + _upperLog = null; + } + + public void LogCustomEvent(CustomBuildEventArgs eventArgs) + { + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + _upperLog = null; + } + + public void LogMessageEvent(BuildMessageEventArgs eventArgs) + { + // Only if the message is above the minimum importance should we record the log message + if (eventArgs.Importance <= _minimumMessageImportance) + { + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + _upperLog = null; + ++_messages; + } + } + + public void LogCommandLine(TaskCommandLineEventArgs eventArgs) + { + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + _upperLog = null; + ++_commandLine; + } + + public bool ContinueOnError + { + get + { + return false; + } + } + + public string ProjectFileOfTaskNode + { + get + { + return String.Empty; + } + } + + public int LineNumberOfTaskNode + { + get + { + return 0; + } + } + + public int ColumnNumberOfTaskNode + { + get + { + return 0; + } + } + + internal string Log + { + set { _log = value; _upperLog = null; } + get { return _log; } + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs + ) + { + return false; + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs, + string toolsVersion + ) + { + return false; + } + + /// + /// Assert that the log file contains the given string. + /// Case insensitive. + /// + /// + internal void AssertLogContains(string contains) + { + if (_upperLog == null) + { + _upperLog = _log; + _upperLog = _upperLog.ToUpperInvariant(); + } + + Assert.IsTrue + ( + _upperLog.Contains + ( + contains.ToUpperInvariant() + ) + ); + } + + public bool BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IDictionary[] targetOutputsPerProject, + string[] toolsVersion, + bool useResultsCache, + bool unloadProjectsOnCompletion + ) + { + return false; + } + + + public BuildEngineResult BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IList[] undefineProperties, + string[] toolsVersion, + bool includeTargetOutputs + ) + { + return new BuildEngineResult(false, null); + } + + public void Yield() + { + } + + public void Reacquire() + { + } + + /// + /// Assert that the log doesnt contain the given string. + /// + /// + internal void AssertLogDoesntContain(string contains) + { + if (_upperLog == null) + { + _upperLog = _log; + _upperLog = _upperLog.ToUpperInvariant(); + } + + Assert.IsTrue + ( + !_upperLog.Contains + ( + contains.ToUpperInvariant() + ) + ); + } + } +} diff --git a/src/Utilities/UnitTests/MockLogger.cs b/src/Utilities/UnitTests/MockLogger.cs new file mode 100644 index 00000000000..3c5c8ffb6f3 --- /dev/null +++ b/src/Utilities/UnitTests/MockLogger.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Text; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using ProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; + +namespace Microsoft.VisualStudio.Build.UnitTest +{ + /* + * Class: MockLogger + * + * Mock logger class. Keeps track of errors and warnings and also builds + * up a raw string (fullLog) that contains all messages, warnings, errors. + * + */ + internal sealed class MockLogger : ILogger + { +#region Properties + private int errorCount = 0; + private int warningCount = 0; + private StringBuilder fullLog = new StringBuilder(); + private List errors = new List(); + private List buildFinishedEvents = new List(); + private List warnings = new List(); + private List externalProjectStartedEvents = new List(); + private List externalProjectFinishedEvents = new List(); + private bool logBuildFinishedEvent = true; + + /// + /// Should the build finished event be logged in the log file. This is to work around the fact we have different + /// localized strings between env and xmake for the build finished event. + /// + internal bool LogBuildFinished + { + get + { + return logBuildFinishedEvent; + } + set + { + logBuildFinishedEvent = value; + } + } + + /* + * Method: ErrorCount + * + * The count of all errors seen so far. + * + */ + internal int ErrorCount + { + get { return this.errorCount; } + } + + /* + * Method: WarningCount + * + * The count of all warnings seen so far. + * + */ + internal int WarningCount + { + get { return this.warningCount; } + } + + /// + /// Build finished events + /// + internal List BuildFinishedEvents + { + get + { + return buildFinishedEvents; + } + } + + /// + /// Return the list of logged errors + /// + internal List Errors + { + get + { + return this.errors; + } + } + + /// + /// Returns the list of logged warnings + /// + internal List Warnings + { + get + { + return this.warnings; + } + } + + /// + /// List of ExternalProjectStarted events + /// + internal List ExternalProjectStartedEvents + { + get { return this.externalProjectStartedEvents; } + } + + /// + /// List of ExternalProjectFinished events + /// + internal List ExternalProjectFinishedEvents + { + get { return this.externalProjectFinishedEvents; } + } + + /* + * Method: FullLog + * + * The raw concatenation of all messages, errors and warnings seen so far. + * + */ + internal string FullLog + { + get { return this.fullLog.ToString(); } + } +#endregion + +#region Minimal ILogger implementation + + /* + * Property: Verbosity + * + * The level of detail to show in the event log. + * + */ + public LoggerVerbosity Verbosity + { + get {return LoggerVerbosity.Normal;} + set {/* do nothing */} + } + + /* + * Property: Parameters + * + * The mock logger does not take parameters. + * + */ + public string Parameters + { + get + { + return null; + } + + set + { + // do nothing + } + } + + /* + * Method: Initialize + * + * Add a new build event. + * + */ + public void Initialize(IEventSource eventSource) + { + eventSource.AnyEventRaised += + new AnyEventHandler(LoggerEventHandler); + } + + /// + /// Clears the content of the log "file" + /// + public void ClearLog() + { + this.fullLog = new StringBuilder(); + } + + /* + * Method: Shutdown + * + * The mock logger does not need to release any resources. + * + */ + public void Shutdown() + { + // do nothing + } +#endregion + + /* + * Method: LoggerEventHandler + * + * Recieves build events and logs them the way we like. + * + */ + internal void LoggerEventHandler(object sender, BuildEventArgs eventArgs) + { + if (eventArgs is BuildWarningEventArgs) + { + BuildWarningEventArgs w = (BuildWarningEventArgs) eventArgs; + + // hack: disregard the MTA warning. + // need the second condition to pass on ploc builds + if (w.Code != "MSB4056" && !w.Message.Contains("MSB4056")) + { + fullLog.AppendFormat("{0}({1},{2}): {3} warning {4}: {5}\r\n", + w.File, + w.LineNumber, + w.ColumnNumber, + w.Subcategory, + w.Code, + w.Message); + + ++warningCount; + this.warnings.Add(w); + } + } + else if (eventArgs is BuildErrorEventArgs) + { + BuildErrorEventArgs e = (BuildErrorEventArgs) eventArgs; + + fullLog.AppendFormat("{0}({1},{2}): {3} error {4}: {5}\r\n", + e.File, + e.LineNumber, + e.ColumnNumber, + e.Subcategory, + e.Code, + e.Message); + + ++errorCount; + this.errors.Add(e); + } + else + { + // Log the message unless we are a build finished event and logBuildFinished is set to false. + bool logMessage = !(eventArgs is BuildFinishedEventArgs) || (eventArgs is BuildFinishedEventArgs && logBuildFinishedEvent); + if (logMessage) + { + fullLog.Append(eventArgs.Message); + fullLog.Append("\r\n"); + } + } + + if (eventArgs is ExternalProjectStartedEventArgs) + { + this.ExternalProjectStartedEvents.Add((ExternalProjectStartedEventArgs)eventArgs); + } + else if (eventArgs is ExternalProjectFinishedEventArgs) + { + this.ExternalProjectFinishedEvents.Add((ExternalProjectFinishedEventArgs)eventArgs); + } + + if (eventArgs is BuildFinishedEventArgs) + { + buildFinishedEvents.Add((BuildFinishedEventArgs)eventArgs); + // We should not have any task crashes. Sometimes a test will validate that their expected error + // code appeared, but not realize it then crashed. + AssertLogDoesntContain("MSB4018"); + + // We should not have any Engine crashes. + AssertLogDoesntContain("MSB0001"); + + // Console.Write in the context of a unit test is very expensive. A hundred + // calls to Console.Write can easily take two seconds on a fast machine. Therefore, only + // do the Console.Write once at the end of the build. + Console.Write(FullLog); + } + } + + // Lazy-init property returning the MSBuild engine resource manager + static private ResourceManager EngineResourceManager + { + get + { + if (engineResourceManager == null) + { + engineResourceManager = new ResourceManager("Microsoft.Build.Resources.Strings", typeof(ProjectCollection).Assembly); + } + + return engineResourceManager; + } + } + + static private ResourceManager engineResourceManager = null; + + // Gets the resource string given the resource ID + static public string GetString(string stringId) + { + return EngineResourceManager.GetString(stringId, CultureInfo.CurrentUICulture); + } + + /// + /// Assert that the log file contains the given strings, in order. + /// + /// + internal void AssertLogContains(params string[] contains) + { + StringReader reader = new StringReader(FullLog); + int index = 0; + string currentLine = reader.ReadLine(); + while(currentLine != null) + { + if (currentLine.Contains(contains[index])) + { + index++; + if (index == contains.Length) break; + } + currentLine = reader.ReadLine(); + } + if (index != contains.Length) + { + Assert.Fail(String.Format(CultureInfo.CurrentCulture, "Log was expected to contain '{0}', but did not.\n=======\n" + FullLog + "\n=======", contains[index])); + } + } + + /// + /// Assert that the log file contains the given string. + /// + /// + internal void AssertLogDoesntContain(string contains) + { + Assert.IsFalse(FullLog.Contains(contains), String.Format("Log was not expected to contain '{0}', but did.", contains)); + } + + /// + /// Assert that no errors were logged + /// + internal void AssertNoErrors() + { + Assert.AreEqual(0, errorCount); + } + + /// + /// Assert that no warnings were logged + /// + internal void AssertNoWarnings() + { + Assert.AreEqual(0, warningCount); + } + + } +} diff --git a/src/Utilities/UnitTests/MockTask.cs b/src/Utilities/UnitTests/MockTask.cs new file mode 100644 index 00000000000..16d7c3b099a --- /dev/null +++ b/src/Utilities/UnitTests/MockTask.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + internal sealed class MockTask : Task + { + internal MockTask() : base() + { + RegisterResources(); + } + + internal MockTask(bool registerResources) + { + if (registerResources) + { + RegisterResources(); + } + } + + private void RegisterResources() + { + ResourceManager rm = new ResourceManager("Microsoft.Build.Utilities.UnitTests.strings", + typeof(MockTask).Assembly); + this.Log.TaskResources = rm; + } + + public override bool Execute() + { + return true; + } + } +} diff --git a/src/Utilities/UnitTests/MuxLogger_Tests.cs b/src/Utilities/UnitTests/MuxLogger_Tests.cs new file mode 100644 index 00000000000..014ccbc8167 --- /dev/null +++ b/src/Utilities/UnitTests/MuxLogger_Tests.cs @@ -0,0 +1,428 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the MuxLogger. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.UnitTests; + +using MuxLogger = Microsoft.Build.Utilities.MuxLogger; + +namespace Microsoft.VisualStudio.Build.UnitTest +{ + /// + /// Tests for the MuxLogger. + /// + [TestClass] + public class MuxLogger_Tests + { + /// + /// Verifies that an empty build with no loggers causes no exceptions. + /// + [TestMethod] + public void EmptyBuildWithNoLoggers() + { + BuildManager buildManager = BuildManager.DefaultBuildManager; + MuxLogger muxLogger = new MuxLogger(); + BuildParameters parameters = new BuildParameters(); + parameters.Loggers = new ILogger[] { muxLogger }; + buildManager.BeginBuild(parameters); + buildManager.EndBuild(); + } + + /// + /// Verifies that a simple build with no loggers causes no exceptions. + /// + [TestMethod] + public void SimpleBuildWithNoLoggers() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + ProjectInstance project = (new Project(XmlReader.Create(new StringReader(projectBody)))).CreateProjectInstance(); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + MuxLogger muxLogger = new MuxLogger(); + BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + parameters.Loggers = new ILogger[] { muxLogger }; + buildManager.Build(parameters, new BuildRequestData(project, new string[0], null)); + } + + /// + /// Verifies that attempting to register a logger before a build has started is invalid. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void RegisteringLoggerBeforeBuildStartedThrows() + { + MuxLogger muxLogger = new MuxLogger(); + muxLogger.RegisterLogger(1, new MockLogger()); + } + + /// + /// Verifies that building with a logger attached to the mux logger is equivalent to building with the logger directly. + /// + [TestMethod] + public void BuildWithMuxLoggerEquivalentToNormalLogger() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + + // Build with a 'normal' logger + MockLogger mockLogger2 = new MockLogger(); + mockLogger2.LogBuildFinished = false; + ProjectCollection projectCollection = new ProjectCollection(); + ProjectInstance project = (new Project(XmlReader.Create(new StringReader(projectBody)), null, "4.0", projectCollection)).CreateProjectInstance(); + BuildParameters parameters = new BuildParameters(projectCollection); + parameters.Loggers = new ILogger[] { mockLogger2 }; + buildManager.Build(parameters, new BuildRequestData(project, new string[0], null)); + + // Build with the mux logger + MuxLogger muxLogger = new MuxLogger(); + muxLogger.Verbosity = LoggerVerbosity.Normal; + projectCollection = new ProjectCollection(); + project = (new Project(XmlReader.Create(new StringReader(projectBody)), null, "4.0", projectCollection)).CreateProjectInstance(); + parameters = new BuildParameters(projectCollection); + parameters.Loggers = new ILogger[] { muxLogger }; + buildManager.BeginBuild(parameters); + MockLogger mockLogger = new MockLogger(); + mockLogger.LogBuildFinished = false; + + try + { + BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, new string[0], null)); + muxLogger.RegisterLogger(submission.SubmissionId, mockLogger); + submission.Execute(); + } + finally + { + buildManager.EndBuild(); + } + + Assert.IsTrue(mockLogger2.BuildFinishedEvents.Count > 0); + Assert.AreEqual(mockLogger2.BuildFinishedEvents.Count, mockLogger.BuildFinishedEvents.Count); + Assert.AreEqual(mockLogger2.BuildFinishedEvents[0].Succeeded, mockLogger.BuildFinishedEvents[0].Succeeded); + Assert.AreEqual(mockLogger2.FullLog, mockLogger.FullLog); + } + + /// + /// Verifies correctness of a simple build with one logger. + /// + [TestMethod] + public void OneSubmissionOneLogger() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + ProjectInstance project = (new Project(XmlReader.Create(new StringReader(projectBody)))).CreateProjectInstance(); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + MuxLogger muxLogger = new MuxLogger(); + BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + parameters.Loggers = new ILogger[] { muxLogger }; + buildManager.BeginBuild(parameters); + MockLogger mockLogger = new MockLogger(); + + try + { + BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, new string[0], null)); + + muxLogger.RegisterLogger(submission.SubmissionId, mockLogger); + submission.Execute(); + } + finally + { + buildManager.EndBuild(); + } + + mockLogger.AssertLogContains("Foo"); + mockLogger.AssertLogContains("Error"); + Assert.AreEqual(1, mockLogger.ErrorCount); + mockLogger.AssertNoWarnings(); + } + + /// + /// Verifies correctness of a two submissions in a single build using separate loggers. + /// + [TestMethod] + public void TwoSubmissionsWithSeparateLoggers() + { + string projectBody1 = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + string projectBody2 = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + ProjectInstance project1 = (new Project(XmlReader.Create(new StringReader(projectBody1)))).CreateProjectInstance(); + ProjectInstance project2 = (new Project(XmlReader.Create(new StringReader(projectBody2)))).CreateProjectInstance(); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + MuxLogger muxLogger = new MuxLogger(); + BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + parameters.Loggers = new ILogger[] { muxLogger }; + MockLogger mockLogger1 = new MockLogger(); + MockLogger mockLogger2 = new MockLogger(); + buildManager.BeginBuild(parameters); + + try + { + BuildSubmission submission1 = buildManager.PendBuildRequest(new BuildRequestData(project1, new string[0], null)); + muxLogger.RegisterLogger(submission1.SubmissionId, mockLogger1); + submission1.Execute(); + + BuildSubmission submission2 = buildManager.PendBuildRequest(new BuildRequestData(project2, new string[0], null)); + muxLogger.RegisterLogger(submission2.SubmissionId, mockLogger2); + submission2.Execute(); + } + finally + { + buildManager.EndBuild(); + } + + mockLogger1.AssertLogContains("Foo"); + mockLogger1.AssertLogContains("Error"); + mockLogger1.AssertLogDoesntContain("Bar"); + mockLogger1.AssertLogDoesntContain("Warning"); + Assert.AreEqual(1, mockLogger1.ErrorCount); + Assert.AreEqual(0, mockLogger1.WarningCount); + + mockLogger2.AssertLogDoesntContain("Foo"); + mockLogger2.AssertLogDoesntContain("Error"); + mockLogger2.AssertLogContains("Bar"); + mockLogger2.AssertLogContains("Warning"); + Assert.AreEqual(0, mockLogger2.ErrorCount); + Assert.AreEqual(1, mockLogger2.WarningCount); + } + + /// + /// Verifies correctness of a simple build with one logger. + /// + [TestMethod] + public void OneSubmissionTwoLoggers() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + ProjectInstance project = (new Project(XmlReader.Create(new StringReader(projectBody)))).CreateProjectInstance(); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + MuxLogger muxLogger = new MuxLogger(); + BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + parameters.Loggers = new ILogger[] { muxLogger }; + MockLogger mockLogger1 = new MockLogger(); + MockLogger mockLogger2 = new MockLogger(); + buildManager.BeginBuild(parameters); + try + { + BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, new string[0], null)); + + muxLogger.RegisterLogger(submission.SubmissionId, mockLogger1); + muxLogger.RegisterLogger(submission.SubmissionId, mockLogger2); + submission.Execute(); + } + finally + { + buildManager.EndBuild(); + } + + mockLogger1.AssertLogContains("Foo"); + mockLogger1.AssertLogContains("Error"); + Assert.AreEqual(1, mockLogger1.ErrorCount); + mockLogger1.AssertNoWarnings(); + + mockLogger2.AssertLogContains("Foo"); + mockLogger2.AssertLogContains("Error"); + Assert.AreEqual(1, mockLogger2.ErrorCount); + mockLogger2.AssertNoWarnings(); + + Assert.AreEqual(mockLogger1.FullLog, mockLogger2.FullLog); + } + + /// + /// Verifies correctness of a simple build with one logger. + /// + [TestMethod] + public void RegisteringLoggerDuringBuildThrowsException() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + ProjectInstance project = (new Project(XmlReader.Create(new StringReader(projectBody)))).CreateProjectInstance(); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + MuxLogger muxLogger = new MuxLogger(); + BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + AutoResetEvent projectStartedEvent = new AutoResetEvent(false); + parameters.Loggers = new ILogger[] { muxLogger, new EventingLogger(projectStartedEvent) }; + MockLogger mockLogger = new MockLogger(); + bool gotException = false; + buildManager.BeginBuild(parameters); + + try + { + BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, new string[0], null)); + + submission.ExecuteAsync(null, null); + projectStartedEvent.WaitOne(); + + try + { + muxLogger.RegisterLogger(submission.SubmissionId, mockLogger); + } + catch (InvalidOperationException) + { + gotException = true; + } + catch + { + } + } + finally + { + buildManager.EndBuild(); + } + + Assert.IsTrue(gotException, "Failed to get exception registering logger during build."); + } + + /// + /// A logger which signals an event when it gets a project started message. + /// + private class EventingLogger : ILogger + { + /// + /// The event source + /// + private IEventSource _eventSource; + + /// + /// The event handler + /// + private ProjectStartedEventHandler _eventHandler; + + /// + /// The event to signal. + /// + private AutoResetEvent _projectStartedEvent; + + /// + /// Constructor. + /// + public EventingLogger(AutoResetEvent projectStartedEvent) + { + _projectStartedEvent = projectStartedEvent; + } + + #region ILogger Members + + /// + /// Verbosity accessor. + /// + public LoggerVerbosity Verbosity + { + get + { + return LoggerVerbosity.Normal; + } + + set + { + } + } + + /// + /// Parameters accessor. + /// + public string Parameters + { + get + { + return null; + } + + set + { + } + } + + /// + /// Initialize the logger. + /// + public void Initialize(IEventSource eventSource) + { + _eventSource = eventSource; + _eventHandler = new ProjectStartedEventHandler(ProjectStarted); + _eventSource.ProjectStarted += _eventHandler; + } + + /// + /// Shut down the logger. + /// + public void Shutdown() + { + _eventSource.ProjectStarted -= _eventHandler; + } + + /// + /// Event handler which signals the event. + /// + private void ProjectStarted(object sender, ProjectStartedEventArgs e) + { + _projectStartedEvent.Set(); + } + + #endregion + } + } +} diff --git a/src/Utilities/UnitTests/PlatformManifest_Tests.cs b/src/Utilities/UnitTests/PlatformManifest_Tests.cs new file mode 100644 index 00000000000..87dc79bd717 --- /dev/null +++ b/src/Utilities/UnitTests/PlatformManifest_Tests.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Basic tests of Platform.xml parsing + /// + [TestClass] + sealed public class PlatformManifest_Tests + { + /// + /// Should get a read error when the manifest location is invalid + /// + [TestMethod] + public void InvalidManifestLocation() + { + PlatformManifest manifest = new PlatformManifest("|||||||"); + + Assert.IsTrue(manifest.ReadError); + } + + /// + /// Should get a read error when the manifest location is valid but empty + /// + [TestMethod] + public void EmptyManifestLocation() + { + string manifestDirectory = null; + + try + { + manifestDirectory = FileUtilities.GetTemporaryDirectory(); + PlatformManifest manifest = new PlatformManifest(manifestDirectory); + + Assert.IsTrue(manifest.ReadError); + } + finally + { + if (manifestDirectory != null) + { + FileUtilities.DeleteDirectoryNoThrow(manifestDirectory, recursive: true); + } + } + } + + /// + /// Should get a read error when the manifest location is valid but doesn't have a + /// file named Platform.xml + /// + [TestMethod] + public void ManifestLocationHasNoPlatformXml() + { + string manifestDirectory = null; + + try + { + manifestDirectory = FileUtilities.GetTemporaryDirectory(); + File.WriteAllText(Path.Combine(manifestDirectory, "SomeOtherFile.xml"), "hello"); + PlatformManifest manifest = new PlatformManifest(manifestDirectory); + + Assert.IsTrue(manifest.ReadError); + } + finally + { + if (manifestDirectory != null) + { + FileUtilities.DeleteDirectoryNoThrow(manifestDirectory, recursive: true); + } + } + } + + /// + /// Should get a read error when trying to read an invalid manifest file. + /// + [TestMethod] + public void InvalidManifest() + { + string contents = @"|||||"; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsTrue(manifest.Manifest.ReadError); + } + } + + /// + /// Verify that a simple PlatformManifest can be successfully constructed. + /// + [TestMethod] + public void SimpleValidManifest() + { + string contents = @""; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsFalse(manifest.Manifest.ReadError); + Assert.AreEqual("UAP", manifest.Manifest.Name); + Assert.AreEqual("Universal Application Platform", manifest.Manifest.FriendlyName); + Assert.AreEqual("1.0.0.0", manifest.Manifest.PlatformVersion); + + Assert.AreEqual(0, manifest.Manifest.DependentPlatforms.Count); + Assert.AreEqual(0, manifest.Manifest.ApiContracts.Count); + } + } + + /// + /// Verify that a simple PlatformManifest can be successfully constructed, even if it's missing + /// some fields. + /// + [TestMethod] + public void SimpleValidManifestWithMissingFriendlyName() + { + string contents = @""; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsFalse(manifest.Manifest.ReadError); + Assert.AreEqual("UAP", manifest.Manifest.Name); + Assert.AreEqual(String.Empty, manifest.Manifest.FriendlyName); + Assert.AreEqual("1.0.0.0", manifest.Manifest.PlatformVersion); + + Assert.AreEqual(0, manifest.Manifest.DependentPlatforms.Count); + Assert.AreEqual(0, manifest.Manifest.ApiContracts.Count); + } + } + + /// + /// Platform manifest with a dependent platform missing some information. + /// NOTE: probably ought to be an error. + /// + [TestMethod] + public void DependentPlatformMissingName() + { + string contents = @" + + "; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsFalse(manifest.Manifest.ReadError); + + Assert.AreEqual(0, manifest.Manifest.ApiContracts.Count); + Assert.AreEqual(1, manifest.Manifest.DependentPlatforms.Count); + + List platforms = new List(manifest.Manifest.DependentPlatforms); + Assert.AreEqual(String.Empty, platforms[0].Name); + Assert.AreEqual("1.0.0.0", platforms[0].Version); + } + } + + /// + /// Verify a PlatformManifest with multiple dependent platforms. + /// + [TestMethod] + public void MultipleDependentPlatforms() + { + string contents = @" + + + + "; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsFalse(manifest.Manifest.ReadError); + + Assert.AreEqual(0, manifest.Manifest.ApiContracts.Count); + Assert.AreEqual(3, manifest.Manifest.DependentPlatforms.Count); + + List platforms = new List(manifest.Manifest.DependentPlatforms); + Assert.AreEqual("UAP", platforms[0].Name); + Assert.AreEqual("1.0.0.0", platforms[0].Version); + Assert.AreEqual("UAP", platforms[1].Name); + Assert.AreEqual("1.0.2.3", platforms[1].Version); + Assert.AreEqual("MyPlatform", platforms[2].Name); + Assert.AreEqual("8.8.8.8", platforms[2].Version); + } + } + + /// + /// Platform manifest with a contract missing some information. + /// NOTE: technically probably ought to be an error. + /// + [TestMethod] + public void ContractMissingVersion() + { + string contents = @" + + + + + "; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsFalse(manifest.Manifest.ReadError); + + Assert.AreEqual(1, manifest.Manifest.DependentPlatforms.Count); + PlatformManifest.DependentPlatform platform = manifest.Manifest.DependentPlatforms.First(); + Assert.AreEqual("UAP", platform.Name); + Assert.AreEqual("1.0.2.3", platform.Version); + + Assert.AreEqual(1, manifest.Manifest.ApiContracts.Count); + ApiContract contract = manifest.Manifest.ApiContracts.First(); + Assert.AreEqual("System", contract.Name); + Assert.AreEqual(String.Empty, contract.Version); + } + } + + /// + /// Verify a platform manifest with API contracts. + /// + [TestMethod] + public void MultipleContracts() + { + string contents = @" + + + + + + "; + + using (TemporaryPlatformManifest manifest = new TemporaryPlatformManifest(contents)) + { + Assert.IsFalse(manifest.Manifest.ReadError); + + Assert.AreEqual(0, manifest.Manifest.DependentPlatforms.Count); + Assert.AreEqual(3, manifest.Manifest.ApiContracts.Count); + + List contracts = new List(manifest.Manifest.ApiContracts); + + Assert.AreEqual("System", contracts[0].Name); + Assert.AreEqual("1.2.0.4", contracts[0].Version); + Assert.AreEqual("Windows.Foundation", contracts[1].Name); + Assert.AreEqual("1.0.0.0", contracts[1].Version); + Assert.AreEqual("Windows.Foundation.OtherStuff", contracts[2].Name); + Assert.AreEqual("1.5.0.0", contracts[2].Version); + } + } + + /// + /// Wrapper around PlatformManifest that creates one with the specified content in + /// the temporary directory and deletes it on disposal. + /// + private class TemporaryPlatformManifest : IDisposable + { + /// + /// Directory in which the PlatformManifest wrapped by this class lives + /// + private string manifestDirectory = null; + + /// + /// Accessor for the PlatformManifest wrapped by this class + /// + public PlatformManifest Manifest + { + get; + private set; + } + + /// + /// Constructor + /// + public TemporaryPlatformManifest(string contents) + { + manifestDirectory = FileUtilities.GetTemporaryDirectory(); + File.WriteAllText(Path.Combine(manifestDirectory, "Platform.xml"), ObjectModelHelpers.CleanupFileContents(contents)); + + Manifest = new PlatformManifest(manifestDirectory); + } + + #region IDisposable Support + + /// + /// Dispose this object + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (FileUtilities.DirectoryExistsNoThrow(manifestDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(manifestDirectory, recursive: true); + } + } + } + + /// + /// Dispose this object + /// + public void Dispose() + { + Dispose(true); + } + #endregion + + } + } +} diff --git a/src/Utilities/UnitTests/ProcessorArchitecture_Tests.cs b/src/Utilities/UnitTests/ProcessorArchitecture_Tests.cs new file mode 100644 index 00000000000..878eb26a3fa --- /dev/null +++ b/src/Utilities/UnitTests/ProcessorArchitecture_Tests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using BuildUtilities = Microsoft.Build.Utilities; +using System.Runtime.InteropServices; +using System.Reflection; +using System.Diagnostics; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ProcessorArchitectureTests + { + internal static string ProcessorArchitectureIntToString(NativeMethodsShared.SYSTEM_INFO systemInfo) + { + switch (systemInfo.wProcessorArchitecture) + { + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_INTEL: + return BuildUtilities.ProcessorArchitecture.X86; + + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_AMD64: + return BuildUtilities.ProcessorArchitecture.AMD64; + + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_IA64: + return BuildUtilities.ProcessorArchitecture.IA64; + + case NativeMethodsShared.PROCESSOR_ARCHITECTURE_ARM: + return BuildUtilities.ProcessorArchitecture.ARM; + + // unknown architecture? return null + default: + return null; + } + } + + [TestMethod] + public void ValidateProcessorArchitectureStrings() + { + // Make sure changes to BuildUtilities.ProcessorArchitecture.cs source don't accidentally get mangle ProcessorArchitecture + Assert.AreEqual("x86", BuildUtilities.ProcessorArchitecture.X86, "x86 ProcessorArchitecture isn't correct"); + Assert.AreEqual("IA64", BuildUtilities.ProcessorArchitecture.IA64, "IA64 ProcessorArchitecture isn't correct"); + Assert.AreEqual("AMD64", BuildUtilities.ProcessorArchitecture.AMD64, "AMD64 ProcessorArchitecture isn't correct"); + Assert.AreEqual("MSIL", BuildUtilities.ProcessorArchitecture.MSIL, "MSIL ProcessorArchitecture isn't correct"); + Assert.AreEqual("ARM", BuildUtilities.ProcessorArchitecture.ARM, "ARM ProcessorArchitecture isn't correct"); + } + + [TestMethod] + public void ValidateCurrentProcessorArchitectureCall() + { + NativeMethodsShared.SYSTEM_INFO systemInfo = new NativeMethodsShared.SYSTEM_INFO(); + NativeMethodsShared.GetSystemInfo(ref systemInfo); + Assert.AreEqual(ProcessorArchitectureIntToString(systemInfo), BuildUtilities.ProcessorArchitecture.CurrentProcessArchitecture, "BuildUtilities.ProcessorArchitecture.CurrentProcessArchitecture returned an invalid match"); + } + + [TestMethod] + public void ValidateConvertDotNetFrameworkArchitectureToProcessorArchitecture() + { + Console.WriteLine("BuildUtilities.ProcessorArchitecture.CurrentProcessArchitecture is: {0}", BuildUtilities.ProcessorArchitecture.CurrentProcessArchitecture); + string procArchitecture; + switch (BuildUtilities.ProcessorArchitecture.CurrentProcessArchitecture) + { + case BuildUtilities.ProcessorArchitecture.ARM: + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness32); + Assert.AreEqual(BuildUtilities.ProcessorArchitecture.ARM, procArchitecture); + + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness64); + Assert.IsNull(procArchitecture, "We should not have any Bitness64 Processor architecture returned in arm"); + break; + + case BuildUtilities.ProcessorArchitecture.X86: + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness32); + Assert.AreEqual(BuildUtilities.ProcessorArchitecture.X86, procArchitecture); + + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness64); + + //We should also allow NULL if the machine is true x86 only. + bool isValidResult = procArchitecture == null ? true : procArchitecture.Equals(BuildUtilities.ProcessorArchitecture.AMD64) || procArchitecture.Equals(BuildUtilities.ProcessorArchitecture.IA64); + + Assert.IsTrue(isValidResult); + break; + + case BuildUtilities.ProcessorArchitecture.AMD64: + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness64); + Assert.AreEqual(BuildUtilities.ProcessorArchitecture.AMD64, procArchitecture); + + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness32); + Assert.AreEqual(BuildUtilities.ProcessorArchitecture.X86, procArchitecture); + break; + + case BuildUtilities.ProcessorArchitecture.IA64: + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness64); + Assert.AreEqual(BuildUtilities.ProcessorArchitecture.IA64, procArchitecture); + + procArchitecture = ToolLocationHelper.ConvertDotNetFrameworkArchitectureToProcessorArchitecture(Utilities.DotNetFrameworkArchitecture.Bitness32); + Assert.AreEqual(BuildUtilities.ProcessorArchitecture.X86, procArchitecture); + break; + + case BuildUtilities.ProcessorArchitecture.MSIL: + Assert.Fail("We should never hit ProcessorArchitecture.MSIL"); + break; + + default: + Assert.Fail("Untested or new ProcessorArchitecture type"); + break; + } + } + } +} diff --git a/src/Utilities/UnitTests/TaskItem_Tests.cs b/src/Utilities/UnitTests/TaskItem_Tests.cs new file mode 100644 index 00000000000..4a5e456646f --- /dev/null +++ b/src/Utilities/UnitTests/TaskItem_Tests.cs @@ -0,0 +1,462 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using System.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class TaskItemTests + { + // Make sure a TaskItem can be constructed using an ITaskItem + [TestMethod] + public void ConstructWithITaskItem() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + from.SetMetadata("Dog", "Bingo"); + from.SetMetadata("Cat", "Morris"); + + TaskItem to = new TaskItem((ITaskItem)from); + Assert.AreEqual("Monkey.txt", to.ItemSpec); + Assert.AreEqual("Monkey.txt", (string)to); + Assert.AreEqual("Bingo", to.GetMetadata("Dog")); + Assert.AreEqual("Morris", to.GetMetadata("Cat")); + + // Test that item metadata are case-insensitive. + to.SetMetadata("CaT", ""); + Assert.AreEqual("", to.GetMetadata("Cat")); + + // manipulate the item-spec a bit + Assert.AreEqual("Monkey", to.GetMetadata(FileUtilities.ItemSpecModifiers.Filename)); + Assert.AreEqual(".txt", to.GetMetadata(FileUtilities.ItemSpecModifiers.Extension)); + Assert.AreEqual(String.Empty, to.GetMetadata(FileUtilities.ItemSpecModifiers.RelativeDir)); + } + + // Make sure metadata can be cloned from an existing ITaskItem + [TestMethod] + public void CopyMetadataFromITaskItem() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + from.SetMetadata("Dog", "Bingo"); + from.SetMetadata("Cat", "Morris"); + from.SetMetadata("Bird", "Big"); + + TaskItem to = new TaskItem(); + to.ItemSpec = "Bonobo.txt"; + to.SetMetadata("Sponge", "Bob"); + to.SetMetadata("Dog", "Harriet"); + to.SetMetadata("Cat", "Mike"); + from.CopyMetadataTo(to); + + Assert.AreEqual("Bonobo.txt", to.ItemSpec); // ItemSpec is never overwritten + Assert.AreEqual("Bob", to.GetMetadata("Sponge")); // Metadata not in source are preserverd. + Assert.AreEqual("Harriet", to.GetMetadata("Dog")); // Metadata present on destination are not overwritten. + Assert.AreEqual("Mike", to.GetMetadata("Cat")); + Assert.AreEqual("Big", to.GetMetadata("Bird")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullITaskItem() + { + ITaskItem item = null; + TaskItem taskItem = new TaskItem(item); + + // no NullReferenceException + } + + /// + /// Even without any custom metadata metadatanames should + /// return the built in metadata + /// + [TestMethod] + public void MetadataNamesNoCustomMetadata() + { + TaskItem taskItem = new TaskItem("x"); + + Assert.AreEqual(FileUtilities.ItemSpecModifiers.All.Length, taskItem.MetadataNames.Count); + Assert.AreEqual(FileUtilities.ItemSpecModifiers.All.Length, taskItem.MetadataCount); + + // Now add one + taskItem.SetMetadata("m", "m1"); + + Assert.AreEqual(FileUtilities.ItemSpecModifiers.All.Length + 1, taskItem.MetadataNames.Count); + Assert.AreEqual(FileUtilities.ItemSpecModifiers.All.Length + 1, taskItem.MetadataCount); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullITaskItemCast() + { + TaskItem item = null; + string result = (string)item; + + // no NullReferenceException + } + + [TestMethod] + public void ConstructFromDictionary() + { + Hashtable h = new Hashtable(); + h[FileUtilities.ItemSpecModifiers.Filename] = "foo"; + h[FileUtilities.ItemSpecModifiers.Extension] = "bar"; + h["custom"] = "hello"; + + TaskItem t = new TaskItem("bamboo.baz", h); + + // item-spec modifiers were not overridden by dictionary passed to constructor + Assert.AreEqual("bamboo", t.GetMetadata(FileUtilities.ItemSpecModifiers.Filename)); + Assert.AreEqual(".baz", t.GetMetadata(FileUtilities.ItemSpecModifiers.Extension)); + Assert.AreEqual("hello", t.GetMetadata("CUSTOM")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void CannotChangeModifiers() + { + TaskItem t = new TaskItem("foo"); + + try + { + t.SetMetadata(FileUtilities.ItemSpecModifiers.FullPath, "bazbaz"); + } + catch (Exception e) + { + // so I can see the exception message in NUnit's "Standard Out" window + Console.WriteLine(e.Message); + throw; + } + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void CannotRemoveModifiers() + { + TaskItem t = new TaskItem("foor"); + + try + { + t.RemoveMetadata(FileUtilities.ItemSpecModifiers.RootDir); + } + catch (Exception e) + { + // so I can see the exception message in NUnit's "Standard Out" window + Console.WriteLine(e.Message); + throw; + } + } + + [TestMethod] + public void CheckMetadataCount() + { + TaskItem t = new TaskItem("foo"); + + Assert.AreEqual(FileUtilities.ItemSpecModifiers.All.Length, t.MetadataCount); + + t.SetMetadata("grog", "RUM"); + + Assert.AreEqual(FileUtilities.ItemSpecModifiers.All.Length + 1, t.MetadataCount); + } + + + [TestMethod] + public void NonexistentRequestFullPath() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + Assert.AreEqual + ( + Path.Combine + ( + Directory.GetCurrentDirectory(), + "Monkey.txt" + ), + from.GetMetadata(FileUtilities.ItemSpecModifiers.FullPath) + ); + } + + [TestMethod] + public void NonexistentRequestRootDir() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + Assert.AreEqual + ( + Path.GetPathRoot + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.FullPath) + ), + from.GetMetadata(FileUtilities.ItemSpecModifiers.RootDir) + ); + } + + [TestMethod] + public void NonexistentRequestFilename() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + Assert.AreEqual + ( + "Monkey", + from.GetMetadata(FileUtilities.ItemSpecModifiers.Filename) + ); + } + + [TestMethod] + public void NonexistentRequestExtension() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + Assert.AreEqual + ( + ".txt", + from.GetMetadata(FileUtilities.ItemSpecModifiers.Extension) + ); + } + + [TestMethod] + public void NonexistentRequestRelativeDir() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.RelativeDir).Length == 0 + ); + } + + [TestMethod] + public void NonexistentRequestDirectory() + { + TaskItem from = new TaskItem(); + from.ItemSpec = @"c:\subdir\Monkey.txt"; + Assert.AreEqual + ( + @"subdir\", + from.GetMetadata(FileUtilities.ItemSpecModifiers.Directory) + ); + } + + [TestMethod] + public void NonexistentRequestDirectoryUNC() + { + TaskItem from = new TaskItem(); + from.ItemSpec = @"\\local\share\subdir\Monkey.txt"; + Assert.AreEqual + ( + @"subdir\", + from.GetMetadata(FileUtilities.ItemSpecModifiers.Directory) + ); + } + + [TestMethod] + public void NonexistentRequestRecursiveDir() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.RecursiveDir).Length == 0 + ); + } + + [TestMethod] + public void NonexistentRequestIdentity() + { + TaskItem from = new TaskItem(); + from.ItemSpec = "Monkey.txt"; + Assert.AreEqual + ( + "Monkey.txt", + from.GetMetadata(FileUtilities.ItemSpecModifiers.Identity) + ); + } + + [TestMethod] + public void RequestTimeStamps() + { + TaskItem from = new TaskItem(); + from.ItemSpec = FileUtilities.GetTemporaryFile(); + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.ModifiedTime).Length > 0 + ); + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.CreatedTime).Length > 0 + ); + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.AccessedTime).Length > 0 + ); + + File.Delete(from.ItemSpec); + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.ModifiedTime).Length == 0 + ); + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.CreatedTime).Length == 0 + ); + + Assert.IsTrue + ( + from.GetMetadata(FileUtilities.ItemSpecModifiers.AccessedTime).Length == 0 + ); + } + + /// + /// Verify metadata cannot be created with null name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void CreateNullNamedMetadata() + { + TaskItem item = new TaskItem("foo"); + item.SetMetadata(null, "x"); + } + + /// + /// Verify metadata cannot be created with empty name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void CreateEmptyNamedMetadata() + { + TaskItem item = new TaskItem("foo"); + item.SetMetadata("", "x"); + } + + /// + /// Create a TaskItem with a null metadata value -- this is allowed, but + /// internally converted to the empty string. + /// + [TestMethod] + public void CreateTaskItemWithNullMetadata() + { + IDictionary metadata = new Dictionary(); + metadata.Add("m", null); + + TaskItem item = new TaskItem("bar", (IDictionary)metadata); + Assert.AreEqual(String.Empty, item.GetMetadata("m")); + } + + /// + /// Set metadata value to null value -- this is allowed, but + /// internally converted to the empty string. + /// + [TestMethod] + public void SetNullMetadataValue() + { + TaskItem item = new TaskItem("bar"); + item.SetMetadata("m", null); + Assert.AreEqual(String.Empty, item.GetMetadata("m")); + } + + /// + /// Test that task items can be successfully constructed based on a task item from another appdomain. + /// + [TestMethod] + public void RemoteTaskItem() + { + AppDomain appDomain = null; + try + { + appDomain = AppDomain.CreateDomain + ( + "generateResourceAppDomain", + null, + AppDomain.CurrentDomain.SetupInformation + ); + + object obj = appDomain.CreateInstanceFromAndUnwrap + ( + typeof(TaskItemCreator).Module.FullyQualifiedName, + typeof(TaskItemCreator).FullName + ); + + TaskItemCreator creator = (TaskItemCreator)obj; + + IDictionary metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + metadata.Add("c", "C"); + metadata.Add("d", "D"); + + creator.Run(new string[] { "a", "b" }, metadata); + + ITaskItem[] itemsInThisAppDomain = new ITaskItem[creator.CreatedTaskItems.Length]; + + for (int i = 0; i < creator.CreatedTaskItems.Length; i++) + { + itemsInThisAppDomain[i] = new TaskItem(creator.CreatedTaskItems[i]); + + Assert.AreEqual(creator.CreatedTaskItems[i].ItemSpec, itemsInThisAppDomain[i].ItemSpec); + Assert.AreEqual(creator.CreatedTaskItems[i].MetadataCount + 1, itemsInThisAppDomain[i].MetadataCount); + + foreach (string metadatum in creator.CreatedTaskItems[i].MetadataNames) + { + if (!String.Equals("OriginalItemSpec", metadatum)) + { + Assert.AreEqual(creator.CreatedTaskItems[i].GetMetadata(metadatum), itemsInThisAppDomain[i].GetMetadata(metadatum)); + } + } + } + } + finally + { + if (appDomain != null) + { + AppDomain.Unload(appDomain); + } + } + } + + /// + /// Miniature class to be remoted to another appdomain that just creates some TaskItems and makes them available for returning. + /// + private sealed class TaskItemCreator : MarshalByRefObject + { + /// + /// Task items that will be consumed by the other appdomain + /// + public ITaskItem[] CreatedTaskItems + { + get; + private set; + } + + /// + /// Creates task items + /// + public void Run(string[] includes, IDictionary metadataToAdd) + { + ErrorUtilities.VerifyThrowArgumentNull(includes, "includes"); + + CreatedTaskItems = new TaskItem[includes.Length]; + + for (int i = 0; i < includes.Length; i++) + { + CreatedTaskItems[i] = new TaskItem(includes[i], (IDictionary)metadataToAdd); + } + } + } + } +} diff --git a/src/Utilities/UnitTests/TaskLoggingHelper_Tests.cs b/src/Utilities/UnitTests/TaskLoggingHelper_Tests.cs new file mode 100644 index 00000000000..bc78fd5eac4 --- /dev/null +++ b/src/Utilities/UnitTests/TaskLoggingHelper_Tests.cs @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Reflection; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class TaskLoggingHelperTests + { + [TestMethod] + public void CheckMessageCode() + { + Task t = new MockTask(); + + // normal + string messageOnly; + string code = t.Log.ExtractMessageCode("AL001: This is a message.", out messageOnly); + Assert.AreEqual("AL001", code); + Assert.AreEqual("This is a message.", messageOnly); + + // whitespace before code and after colon is ok + messageOnly = null; + code = t.Log.ExtractMessageCode(" AL001: This is a message.", out messageOnly); + Assert.AreEqual("AL001", code); + Assert.AreEqual("This is a message.", messageOnly); + + // whitespace after colon is not ok + messageOnly = null; + code = t.Log.ExtractMessageCode("AL001 : This is a message.", out messageOnly); + Assert.IsNull(code); + Assert.AreEqual("AL001 : This is a message.", messageOnly); + + // big code is ok + messageOnly = null; + code = t.Log.ExtractMessageCode(" RESGEN7905001: This is a message.", out messageOnly); + Assert.AreEqual("RESGEN7905001", code); + Assert.AreEqual("This is a message.", messageOnly); + + // small code is ok + messageOnly = null; + code = t.Log.ExtractMessageCode("R7: This is a message.", out messageOnly); + Assert.AreEqual("R7", code); + Assert.AreEqual("This is a message.", messageOnly); + + // lowercase code is ok + messageOnly = null; + code = t.Log.ExtractMessageCode("alink3456: This is a message.", out messageOnly); + Assert.AreEqual("alink3456", code); + Assert.AreEqual("This is a message.", messageOnly); + + // whitespace in code is not ok + messageOnly = null; + code = t.Log.ExtractMessageCode(" RES 7905: This is a message.", out messageOnly); + Assert.IsNull(code); + Assert.AreEqual(" RES 7905: This is a message.", messageOnly); + + // only digits in code is not ok + messageOnly = null; + code = t.Log.ExtractMessageCode("7905: This is a message.", out messageOnly); + Assert.IsNull(code); + Assert.AreEqual("7905: This is a message.", messageOnly); + + // only letters in code is not ok + messageOnly = null; + code = t.Log.ExtractMessageCode("ALINK: This is a message.", out messageOnly); + Assert.IsNull(code); + Assert.AreEqual("ALINK: This is a message.", messageOnly); + + // digits before letters in code is not ok + messageOnly = null; + code = t.Log.ExtractMessageCode("6780ALINK: This is a message.", out messageOnly); + Assert.IsNull(code); + Assert.AreEqual("6780ALINK: This is a message.", messageOnly); + + // mixing digits and letters in code is not ok + messageOnly = null; + code = t.Log.ExtractMessageCode("LNK658A: This is a message.", out messageOnly); + Assert.IsNull(code); + Assert.AreEqual("LNK658A: This is a message.", messageOnly); + } + + /// + /// LogMessageFromStream parses the stream and decides if it is an error/warning/message. + /// The way it figures out if a message is an error or warning is by parsing it against + /// the canonical error/warning format. If it happens to be an error this method returns + /// true ... isError. This unit test ensures that passing a cannonical error format results + /// in this method returning true and passing a non canonical message results in it returning + /// false + /// + [TestMethod] + public void CheckMessageFromStreamParsesErrorsAndMessagesCorrectly() + { + IBuildEngine2 mockEngine = new MockEngine(); + Task t = new MockTask(); + t.BuildEngine = mockEngine; + + // This should return true since I am passing a canonical error as the stream + StringReader sr = new StringReader("error MSB4040: There is no target in the project."); + Assert.IsTrue(t.Log.LogMessagesFromStream(sr, MessageImportance.High)); + + // This should return false since I am passing a canonical warning as the stream + sr = new StringReader("warning ABCD123MyCode: Felix is a cat."); + Assert.IsFalse(t.Log.LogMessagesFromStream(sr, MessageImportance.Low)); + + // This should return false since I am passing a non canonical message in the stream + sr = new StringReader("Hello World"); + Assert.IsFalse(t.Log.LogMessagesFromStream(sr, MessageImportance.High)); + } + + [TestMethod] + public void LogCommandLine() + { + MockEngine mockEngine = new MockEngine(); + Task t = new MockTask(); + t.BuildEngine = mockEngine; + + t.Log.LogCommandLine("MySuperCommand"); + Assert.IsTrue(mockEngine.Log.Contains("MySuperCommand")); + } + + /// + /// This verifies that we don't try to run FormatString on a string + /// that isn't a resource (if we did, the unmatched curly would give an exception) + /// + [TestMethod] + public void LogMessageWithUnmatchedCurly() + { + MockEngine mockEngine = new MockEngine(); + Task t = new MockTask(); + t.BuildEngine = mockEngine; + + t.Log.LogMessage("echo {"); + t.Log.LogMessageFromText("{1", MessageImportance.High); + t.Log.LogCommandLine("{2"); + t.Log.LogWarning("{3"); + t.Log.LogError("{4"); + + mockEngine.AssertLogContains("echo {"); + mockEngine.AssertLogContains("{1"); + mockEngine.AssertLogContains("{2"); + mockEngine.AssertLogContains("{3"); + mockEngine.AssertLogContains("{4"); + } + + [TestMethod] + public void LogFromResources() + { + MockEngine mockEngine = new MockEngine(); + Task t = new MockTask(); + t.BuildEngine = mockEngine; + + t.Log.LogErrorFromResources("MySubcategoryResource", null, + "helpkeyword", "filename", 1, 2, 3, 4, "MyErrorResource", "foo"); + + t.Log.LogErrorFromResources("MyErrorResource", "foo"); + + t.Log.LogWarningFromResources("MySubcategoryResource", null, + "helpkeyword", "filename", 1, 2, 3, 4, "MyWarningResource", "foo"); + + t.Log.LogWarningFromResources("MyWarningResource", "foo"); + + Assert.IsTrue(mockEngine.Log.Contains("filename(1,2,3,4): Romulan error : Oops I wiped your harddrive foo")); + Assert.IsTrue(mockEngine.Log.Contains("filename(1,2,3,4): Romulan warning : Be nice or I wipe your harddrive foo")); + Assert.IsTrue(mockEngine.Log.Contains("Oops I wiped your harddrive foo")); + Assert.IsTrue(mockEngine.Log.Contains("Be nice or I wipe your harddrive foo")); + } + + [TestMethod] + public void CheckLogMessageFromFile() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + + string contents = @"a message here + error abcd12345: hey jude. + warning xy11: I wanna hold your hand. + this is not an error or warning + nor is this + error def222: norwegian wood"; + + // This closes the reader + File.WriteAllText(file, contents); + + MockEngine mockEngine = new MockEngine(); + Task t = new MockTask(); + t.BuildEngine = mockEngine; + t.Log.LogMessagesFromFile(file, MessageImportance.High); + + Assert.AreEqual(2, mockEngine.Errors); + Assert.AreEqual(1, mockEngine.Warnings); + Assert.AreEqual(3, mockEngine.Messages); + + mockEngine = new MockEngine(); + t = new MockTask(); + t.BuildEngine = mockEngine; + t.Log.LogMessagesFromFile(file); + + Assert.AreEqual(2, mockEngine.Errors); + Assert.AreEqual(1, mockEngine.Warnings); + Assert.AreEqual(3, mockEngine.Messages); + } + finally + { + if (null != file) File.Delete(file); + } + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void CheckResourcesRegistered() + { + Task t = new MockTask(false /*don't register resources*/); + + try + { + t.Log.FormatResourceString("bogus"); + } + catch (Exception e) + { + // so I can see the exception message in NUnit's "Standard Out" window + Console.WriteLine(e.Message); + throw; + } + } + + /// + /// Verify the LogErrorFromException & LogWarningFromException methods + /// + [TestMethod] + public void TestLogFromException() + { + string message = "exception message"; + string stackTrace = "TaskLoggingHelperTests.TestLogFromException"; + + MockEngine engine = new MockEngine(); + MockTask task = new MockTask(); + task.BuildEngine = engine; + + // need to throw and catch an exception so that its stack trace is initialized to something + try + { + Exception inner = new InvalidOperationException(); + throw new Exception(message, inner); + } + catch (Exception e) + { + // log error without stack trace + task.Log.LogErrorFromException(e); + engine.AssertLogContains(message); + engine.AssertLogDoesntContain(stackTrace); + engine.AssertLogDoesntContain("InvalidOperationException"); + + engine.Log = string.Empty; + + // log warning with stack trace + task.Log.LogWarningFromException(e); + engine.AssertLogContains(message); + engine.AssertLogDoesntContain(stackTrace); + + engine.Log = string.Empty; + + // log error with stack trace + task.Log.LogErrorFromException(e, true); + engine.AssertLogContains(message); + engine.AssertLogContains(stackTrace); + engine.AssertLogDoesntContain("InvalidOperationException"); + + engine.Log = string.Empty; + + // log warning with stack trace + task.Log.LogWarningFromException(e, true); + engine.AssertLogContains(message); + engine.AssertLogContains(stackTrace); + engine.Log = string.Empty; + + // log error with stack trace and inner exceptions + task.Log.LogErrorFromException(e, true, true, "foo.cs"); + engine.AssertLogContains(message); + engine.AssertLogContains(stackTrace); + engine.AssertLogContains("InvalidOperationException"); + } + } + } +} diff --git a/src/Utilities/UnitTests/ToolLocationHelper_Tests.cs b/src/Utilities/UnitTests/ToolLocationHelper_Tests.cs new file mode 100644 index 00000000000..c89fc5b8d55 --- /dev/null +++ b/src/Utilities/UnitTests/ToolLocationHelper_Tests.cs @@ -0,0 +1,4363 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Diagnostics; + +using UtilitiesDotNetFrameworkArchitecture = Microsoft.Build.Utilities.DotNetFrameworkArchitecture; +using SharedDotNetFrameworkArchitecture = Microsoft.Build.Shared.DotNetFrameworkArchitecture; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using Microsoft.Win32; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ToolLocationHelper_Tests + { + [TestInitialize] + public void SetUpTest() + { + ToolLocationHelper.ClearStaticCaches(); + } + + [TestMethod] + public void GetApiContractReferencesHandlesEmptyContracts() + { + string[] returnValue = ToolLocationHelper.GetApiContractReferences(Enumerable.Empty(), String.Empty); + Assert.AreEqual(0, returnValue.Length); + } + + [TestMethod] + public void GetApiContractReferencesHandlesNonExistingLocation() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string[] returnValue = ToolLocationHelper.GetApiContractReferences(new ApiContract[] { new ApiContract { Name = "Foo", Version = "Bar" } }, tempDirectory); + Assert.AreEqual(0, returnValue.Length); + } + + [TestMethod] + public void GetApiContractReferencesFindsWinMDs() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string referenceDirectory = Path.Combine(tempDirectory, @"References\Foo\Bar"); + + try + { + Directory.CreateDirectory(referenceDirectory); + File.WriteAllText(Path.Combine(referenceDirectory, "One.winmd"), "First"); + File.WriteAllText(Path.Combine(referenceDirectory, "Two.winmd"), "Second"); + File.WriteAllText(Path.Combine(referenceDirectory, "Three.winmd"), "Third"); + string[] returnValue = ToolLocationHelper.GetApiContractReferences(new ApiContract[] { new ApiContract { Name = "Foo", Version = "Bar" } }, tempDirectory); + Assert.AreEqual(3, returnValue.Length); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + [TestMethod] + public void GatherExtensionSDKsInvalidVersionDirectory() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string sdkDirectory = Path.Combine(tempDirectory, @"Foo\Bar"); + + try + { + Directory.CreateDirectory(sdkDirectory); + DirectoryInfo info = new DirectoryInfo(tempDirectory); + TargetPlatformSDK sdk = new TargetPlatformSDK("Foo", new Version(), String.Empty); + ToolLocationHelper.GatherExtensionSDKs(info, sdk); + Assert.AreEqual(0, sdk.ExtensionSDKs.Count); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + [TestMethod] + public void GatherExtensionSDKsNoManifest() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string sdkDirectory = Path.Combine(tempDirectory, @"Foo\1.0"); + + try + { + Directory.CreateDirectory(sdkDirectory); + DirectoryInfo info = new DirectoryInfo(tempDirectory); + TargetPlatformSDK sdk = new TargetPlatformSDK("Foo", new Version(), String.Empty); + ToolLocationHelper.GatherExtensionSDKs(info, sdk); + Assert.AreEqual(0, sdk.ExtensionSDKs.Count); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + [TestMethod] + public void GatherExtensionSDKsEmptyManifest() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string sdkDirectory = Path.Combine(tempDirectory, @"Foo\1.0"); + + try + { + Directory.CreateDirectory(sdkDirectory); + File.WriteAllText(Path.Combine(sdkDirectory, "sdkManifest.xml"), ""); + DirectoryInfo info = new DirectoryInfo(tempDirectory); + TargetPlatformSDK sdk = new TargetPlatformSDK("Foo", new Version(), String.Empty); + ToolLocationHelper.GatherExtensionSDKs(info, sdk); + Assert.AreEqual(1, sdk.ExtensionSDKs.Count); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + [TestMethod] + public void GatherExtensionSDKsGarbageManifest() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string sdkDirectory = Path.Combine(tempDirectory, @"Foo\1.0"); + + try + { + Directory.CreateDirectory(sdkDirectory); + File.WriteAllText(Path.Combine(sdkDirectory, "sdkManifest.xml"), "Garbaggggge"); + DirectoryInfo info = new DirectoryInfo(tempDirectory); + TargetPlatformSDK sdk = new TargetPlatformSDK("Foo", new Version(), String.Empty); + ToolLocationHelper.GatherExtensionSDKs(info, sdk); + Assert.AreEqual(1, sdk.ExtensionSDKs.Count); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + /// + /// Verify the case where we ask for a tool using a target framework version of 3.5 + /// We make sure in the fake sdk path we also create a 4.0 folder in order to make sure we do not return that when we only want the bin directory. + /// + [TestMethod] + public void VerifyinternalGetPathToDotNetFrameworkSdkFileNot40() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "VGPTDNFSFN40"); + string temp35Directory = Path.Combine(tempDirectory, "bin"); + string temp40Directory = Path.Combine(temp35Directory, "NETFX 4.0 Tools"); + string toolPath = Path.Combine(temp35Directory, "MyTool.exe"); + string toolPath40 = Path.Combine(temp40Directory, "MyTool.exe"); + + try + { + if (!Directory.Exists(temp35Directory)) + { + Directory.CreateDirectory(temp35Directory); + } + + // Make a .NET 4.0 Tools so that we can make sure that we do not return it if we are not targeting 4.0 + if (!Directory.Exists(temp40Directory)) + { + Directory.CreateDirectory(temp40Directory); + } + + // Write a tool to disk to the existence check works + File.WriteAllText(toolPath, "Contents"); + File.WriteAllText(toolPath40, "Contents"); + + string foundToolPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("MyTool.exe", temp35Directory, "x86"); + Assert.IsNotNull(foundToolPath); + Assert.IsTrue(toolPath.Equals(foundToolPath, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + /// + /// Make sure that if a unknown framework identifier with a root directory which does not exist in it is passed in then we get an empty list back out. + /// + [TestMethod] + public void GetFrameworkIdentifiersNoReferenceAssemblies() + { + IList installedIdentifiers = ToolLocationHelper.GetFrameworkIdentifiers("f:\\IDontExistAtAll"); + Assert.IsTrue(installedIdentifiers.Count == 0); + } + + /// + /// When the root does not exist make sure nothing is returned + /// + [TestMethod] + public void HighestVersionOfTargetFrameworkIdentifierRootDoesNotExist() + { + FrameworkNameVersioning highestMoniker = ToolLocationHelper.HighestVersionOfTargetFrameworkIdentifier("f:\\IDontExistAtAll", ".UnKNownFramework"); + Assert.IsNull(highestMoniker); + } + + /// + /// When the root contains no folders with versions on them make sure nothing is returned + /// + [TestMethod] + public void HighestVersionOfTargetFrameworkIdentifierRootNoVersions() + { + string tempPath = Path.GetTempPath(); + string testPath = Path.Combine(tempPath, "HighestVersionOfTargetFrameworkIdentifierRootNoVersions"); + string nonVersionFolder = Path.Combine(testPath, ".UnknownFramework\\NotAVersion"); + + if (!Directory.Exists(nonVersionFolder)) + { + Directory.CreateDirectory(nonVersionFolder); + } + + FrameworkNameVersioning highestMoniker = ToolLocationHelper.HighestVersionOfTargetFrameworkIdentifier(testPath, ".UnKNownFramework"); + Assert.IsNull(highestMoniker); + } + + + /// + /// If a directory contains multiple versions make sure we pick the highest one. + /// + [TestMethod] + public void HighestVersionOfTargetFrameworkIdentifierRootMultipleVersions() + { + string tempPath = Path.GetTempPath(); + string testPath = Path.Combine(tempPath, "HighestVersionOfTargetFrameworkIdentifierRootMultipleVersions"); + string folder10 = Path.Combine(testPath, ".UnknownFramework\\v1.0"); + string folder20 = Path.Combine(testPath, ".UnknownFramework\\v2.0"); + string folder40 = Path.Combine(testPath, ".UnknownFramework\\v4.0"); + + + if (!Directory.Exists(folder10)) + { + Directory.CreateDirectory(folder10); + } + + if (!Directory.Exists(folder20)) + { + Directory.CreateDirectory(folder20); + } + + if (!Directory.Exists(folder40)) + { + Directory.CreateDirectory(folder40); + } + + FrameworkNameVersioning highestMoniker = ToolLocationHelper.HighestVersionOfTargetFrameworkIdentifier(testPath, ".UnKNownFramework"); + Assert.IsNotNull(highestMoniker); + Assert.IsTrue(highestMoniker.Version.Major == 4); + } + + /// + /// Verify the case where we ask for a tool using a target framework version of 4.0 + /// + [TestMethod] + public void VerifyinternalGetPathToDotNetFrameworkSdkFile40() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "VGPTDNFSFN40"); + string temp35Directory = Path.Combine(tempDirectory, "bin"); + string temp40Directory = Path.Combine(temp35Directory, "NETFX 4.0 Tools"); + string toolPath = Path.Combine(temp35Directory, "MyTool.exe"); + string toolPath40 = Path.Combine(temp40Directory, "MyTool.exe"); + + try + { + if (!Directory.Exists(temp35Directory)) + { + Directory.CreateDirectory(temp35Directory); + } + + // Make a .NET 4.0 Tools so that we can make sure that we do not return it if we are not targeting 4.0 + if (!Directory.Exists(temp40Directory)) + { + Directory.CreateDirectory(temp40Directory); + } + + // Write a tool to disk to the existence check works + File.WriteAllText(toolPath, "Contents"); + File.WriteAllText(toolPath40, "Contents"); + + string foundToolPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("MyTool.exe", temp40Directory, "x86"); + Assert.IsNotNull(foundToolPath); + Assert.IsTrue(toolPath40.Equals(foundToolPath, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, true); + } + } + } + + /// + /// Make sure if null is passed in for any of the arguments that the method returns null and does not crash. + /// + [TestMethod] + public void VerifyinternalGetPathToDotNetFrameworkSdkFileNullPassedIn() + { + string foundToolPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("MyTool.exe", "C:\\Path", null); + Assert.IsNull(foundToolPath); + + foundToolPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("MyTool.exe", null, "x86"); + Assert.IsNull(foundToolPath); + + foundToolPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(null, "c:\\path", "x86"); + Assert.IsNull(foundToolPath); + } + + /* + * Method: FindFrameworksPathRunningThisTest + * + * Our FX path should be resolved as the one we're running on by default + */ + [TestMethod] + public void FindFrameworksPathRunningThisTest() + { + string path = FrameworkLocationHelper.FindDotNetFrameworkPath( + Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName), + ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version40), + new DirectoryExists(ToolLocationHelper_Tests.DirectoryExists), + new GetDirectories(ToolLocationHelper_Tests.GetDirectories), + SharedDotNetFrameworkArchitecture.Current + ); + + Assert.AreEqual(Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName), path); + } + + /* + * Method: FindFrameworksPathRunningUnderWhidbey + * + * Search for a whidbey when whidbey is the current version. + */ + [TestMethod] + public void FindFrameworksPathRunningUnderWhidbey() + { + string path = FrameworkLocationHelper.FindDotNetFrameworkPath + ( + @"{runtime-base}\v1.2.x86dbg", // Simulate "Whidbey" as the current runtime. + "v1.2", + new DirectoryExists(ToolLocationHelper_Tests.DirectoryExists), + new GetDirectories(ToolLocationHelper_Tests.GetDirectories), + SharedDotNetFrameworkArchitecture.Current + ); + Assert.AreEqual(@"{runtime-base}\v1.2.x86dbg", path); + } + + /* + * Method: FindFrameworksPathRunningUnderOrcas + * + * Search for a whidbey when orcas is the current version. + */ + [TestMethod] + public void FindFrameworksPathRunningUnderOrcas() + { + string path = FrameworkLocationHelper.FindDotNetFrameworkPath + ( + @"{runtime-base}\v1.3.x86dbg", // Simulate "Orcas" as the current runtime. + "v1.2", // But we're looking for "Whidbey" + new DirectoryExists(ToolLocationHelper_Tests.DirectoryExists), + new GetDirectories(ToolLocationHelper_Tests.GetDirectories), + SharedDotNetFrameworkArchitecture.Current + ); + Assert.AreEqual(@"{runtime-base}\v1.2.x86fre", path); + } + + /* + * Method: FindFrameworksPathRunningUnderEverett + * + * Search for a whidbey when orcas is the current version. + */ + [TestMethod] + public void FindFrameworksPathRunningUnderEverett() + { + string path = FrameworkLocationHelper.FindDotNetFrameworkPath + ( + @"{runtime-base}\v1.1.x86dbg", // Simulate "Everett" as the current runtime. + "v1.2", // But we're looking for "Whidbey" + new DirectoryExists(ToolLocationHelper_Tests.DirectoryExists), + new GetDirectories(ToolLocationHelper_Tests.GetDirectories), + SharedDotNetFrameworkArchitecture.Current + ); + + Assert.AreEqual(@"{runtime-base}\v1.2.x86fre", path); + } + + /* + * Method: FindPathForNonexistentFrameworks + * + * Trying to find a non-existent path should return null. + */ + [TestMethod] + public void FindPathForNonexistentFrameworks() + { + string path = FrameworkLocationHelper.FindDotNetFrameworkPath + ( + @"{runtime-base}\v1.1", // Simulate "everett" as the current runtime + "v1.3", // And we're trying to find "orchas" runtime which isn't installed. + new DirectoryExists(ToolLocationHelper_Tests.DirectoryExists), + new GetDirectories(ToolLocationHelper_Tests.GetDirectories), + SharedDotNetFrameworkArchitecture.Current + ); + + Assert.AreEqual(null, path); + } + + /* + * Method: FindPathForEverettThatIsntProperlyInstalled + * + * Trying to find a path if GetRequestedRuntimeInfo fails and useHeuristic=false should return null. + */ + [TestMethod] + public void FindPathForEverettThatIsntProperlyInstalled() + { + string tempPath = Path.GetTempPath(); + string fakeWhidbeyPath = Path.Combine(tempPath, "v2.0.50224"); + string fakeEverettPath = Path.Combine(tempPath, "v1.1.43225"); + Directory.CreateDirectory(fakeEverettPath); + + string path = FrameworkLocationHelper.FindDotNetFrameworkPath + ( + fakeWhidbeyPath, // Simulate "whidbey" as the current runtime + "v1.1", // We're looking for "everett" + new DirectoryExists(ToolLocationHelper_Tests.DirectoryExists), + new GetDirectories(ToolLocationHelper_Tests.GetDirectories), + SharedDotNetFrameworkArchitecture.Current + ); + + Directory.Delete(fakeEverettPath); + Assert.AreEqual(null, path); + } + + [TestMethod] + public void ExerciseMiscToolLocationHelperMethods() + { + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version11), FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV11); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version20), FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV20); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version30), FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV30); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version35), FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV35); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version40), FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV40); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.VersionLatest), FrameworkLocationHelper.dotNetFrameworkVersionFolderPrefixV40); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkRootRegistryKey(TargetDotNetFrameworkVersion.VersionLatest), FrameworkLocationHelper.fullDotNetFrameworkRegistryKey); + + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version11), FrameworkLocationHelper.PathToDotNetFrameworkV11); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20), FrameworkLocationHelper.PathToDotNetFrameworkV20); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version30), FrameworkLocationHelper.PathToDotNetFrameworkV30); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35), FrameworkLocationHelper.PathToDotNetFrameworkV35); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version40), FrameworkLocationHelper.PathToDotNetFrameworkV40); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.VersionLatest), FrameworkLocationHelper.PathToDotNetFrameworkV40); + + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version11, UtilitiesDotNetFrameworkArchitecture.Bitness32), + FrameworkLocationHelper.GetPathToDotNetFrameworkV11(SharedDotNetFrameworkArchitecture.Bitness32) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20, UtilitiesDotNetFrameworkArchitecture.Bitness32), + FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Bitness32) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version30, UtilitiesDotNetFrameworkArchitecture.Bitness32), + FrameworkLocationHelper.GetPathToDotNetFrameworkV30(SharedDotNetFrameworkArchitecture.Bitness32) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35, UtilitiesDotNetFrameworkArchitecture.Bitness32), + FrameworkLocationHelper.GetPathToDotNetFrameworkV35(SharedDotNetFrameworkArchitecture.Bitness32) + ); + + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version40, UtilitiesDotNetFrameworkArchitecture.Bitness32), + FrameworkLocationHelper.GetPathToDotNetFrameworkV40(SharedDotNetFrameworkArchitecture.Bitness32) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.VersionLatest, UtilitiesDotNetFrameworkArchitecture.Bitness32), + FrameworkLocationHelper.GetPathToDotNetFrameworkV40(SharedDotNetFrameworkArchitecture.Bitness32) + ); + + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("ProgramFiles(x86)"))) + { + // 64-bit machine, so we should test the 64-bit overloads as well + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version11, UtilitiesDotNetFrameworkArchitecture.Bitness64), + FrameworkLocationHelper.GetPathToDotNetFrameworkV11(SharedDotNetFrameworkArchitecture.Bitness64) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20, UtilitiesDotNetFrameworkArchitecture.Bitness64), + FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Bitness64) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version30, UtilitiesDotNetFrameworkArchitecture.Bitness64), + FrameworkLocationHelper.GetPathToDotNetFrameworkV30(SharedDotNetFrameworkArchitecture.Bitness64) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35, UtilitiesDotNetFrameworkArchitecture.Bitness64), + FrameworkLocationHelper.GetPathToDotNetFrameworkV35(SharedDotNetFrameworkArchitecture.Bitness64) + ); + + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version40, UtilitiesDotNetFrameworkArchitecture.Bitness64), + FrameworkLocationHelper.GetPathToDotNetFrameworkV40(SharedDotNetFrameworkArchitecture.Bitness64) + ); + Assert.AreEqual( + ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.VersionLatest, UtilitiesDotNetFrameworkArchitecture.Bitness64), + FrameworkLocationHelper.GetPathToDotNetFrameworkV40(SharedDotNetFrameworkArchitecture.Bitness64) + ); + } + } + + [TestMethod] + public void TestGetPathToBuildToolsFile() + { + string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version20); + + if (net20Path != null) + { + Assert.AreEqual(net20Path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "2.0")); + } + + string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version35); + + if (net35Path != null) + { + Assert.AreEqual(net35Path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "3.5")); + } + + Assert.AreEqual + ( + ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version40), + ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "4.0") + ); + + string tv12path = Path.Combine(ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion).ToolsPath, "msbuild.exe"); + + Assert.AreEqual(tv12path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ObjectModelHelpers.MSBuildDefaultToolsVersion)); + Assert.AreEqual(tv12path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ToolLocationHelper.CurrentToolsVersion)); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void TestGetPathToBuildToolsFile_32Bit() + { + string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version20, UtilitiesDotNetFrameworkArchitecture.Bitness32); + + if (net20Path != null) + { + Assert.AreEqual(net20Path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "2.0", UtilitiesDotNetFrameworkArchitecture.Bitness32)); + } + + string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version35, UtilitiesDotNetFrameworkArchitecture.Bitness32); + + if (net35Path != null) + { + Assert.AreEqual(net35Path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "3.5", UtilitiesDotNetFrameworkArchitecture.Bitness32)); + } + + Assert.AreEqual + ( + ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version40, UtilitiesDotNetFrameworkArchitecture.Bitness32), + ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "4.0", UtilitiesDotNetFrameworkArchitecture.Bitness32) + ); + + string tv12path = Path.Combine(ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion).Properties["MSBuildToolsPath32"].EvaluatedValue, "msbuild.exe"); + + Assert.AreEqual(tv12path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ObjectModelHelpers.MSBuildDefaultToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness32)); + Assert.AreEqual(tv12path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ToolLocationHelper.CurrentToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness32)); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void TestGetPathToBuildToolsFile_64Bit() + { + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("ProgramFiles(x86)"))) + { + // 32-bit machine, so just ignore + return; + } + + string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version20, UtilitiesDotNetFrameworkArchitecture.Bitness64); + + if (net20Path != null) + { + Assert.AreEqual(net20Path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "2.0", UtilitiesDotNetFrameworkArchitecture.Bitness64)); + } + + string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version35, UtilitiesDotNetFrameworkArchitecture.Bitness64); + + if (net35Path != null) + { + Assert.AreEqual(net35Path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "3.5", UtilitiesDotNetFrameworkArchitecture.Bitness64)); + } + + Assert.AreEqual + ( + ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version40, UtilitiesDotNetFrameworkArchitecture.Bitness64), + ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "4.0", UtilitiesDotNetFrameworkArchitecture.Bitness64) + ); + + string tv12path = Path.Combine(ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion).Properties["MSBuildToolsPath32"].EvaluatedValue, "amd64", "msbuild.exe"); + + Assert.AreEqual(tv12path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ObjectModelHelpers.MSBuildDefaultToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness64)); + Assert.AreEqual(tv12path, ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ToolLocationHelper.CurrentToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness64)); + } + + [TestMethod] + public void TestGetDotNetFrameworkSdkRootRegistryKey() + { + // Test out of range .net version. + foreach (var vsVersion in EnumVisualStudioVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey((TargetDotNetFrameworkVersion)99, vsVersion); }); + } + + // Test out of range visual studio version. + foreach (var dotNetVersion in EnumDotNetFrameworkVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(dotNetVersion, (VisualStudioVersion)99); }); + } + + foreach (var vsVersion in EnumVisualStudioVersions()) + { + // v1.1 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version11, vsVersion), FrameworkLocationHelper.fullDotNetFrameworkRegistryKey); + + // v2.0 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version20, vsVersion), FrameworkLocationHelper.fullDotNetFrameworkRegistryKey); + + // v3.0 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version30, vsVersion); }); + + // v3.5 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version35, vsVersion), + vsVersion == VisualStudioVersion.Version100 ? FrameworkLocationHelper.fullDotNetFrameworkSdkRegistryKeyV35OnVS10 : FrameworkLocationHelper.fullDotNetFrameworkSdkRegistryKeyV35OnVS11); + } + + string fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK70A = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v7.0A\WinSDK-NetFx40Tools-x86"; + string fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK80A = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.0A\WinSDK-NetFx40Tools-x86"; + string fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK81A = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.1A\WinSDK-NetFx40Tools-x86"; + string fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK46 = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\NETFXSDK\4.6\WinSDK-NetFx40Tools-x86"; + + // v4.0 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version100), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK70A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version110), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK80A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version120), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK81A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version140), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK46); + + // v4.5 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version100), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK80A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version110), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK80A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version120), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK81A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version140), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK46); + + // v4.5.1 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version100); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version110); }); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version120), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK81A); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version140), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK46); + + // v4.6 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version100); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version110); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version120); }); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version140), fullDotNetFrameworkSdkRegistryPathForV4ToolsOnManagedToolsSDK46); + } + + [TestMethod] + public void TestGetDotNetFrameworkSdkInstallKeyValue() + { + // Test out of range .net version. + foreach (var vsVersion in EnumVisualStudioVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue((TargetDotNetFrameworkVersion)99, vsVersion); }); + } + + // Test out of range visual studio version. + foreach (var dotNetVersion in EnumDotNetFrameworkVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(dotNetVersion, (VisualStudioVersion)99); }); + } + + string InstallationFolder = "InstallationFolder"; + + foreach (var vsVersion in EnumVisualStudioVersions()) + { + // v1.1 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version11, vsVersion), FrameworkLocationHelper.dotNetFrameworkSdkInstallKeyValueV11); + + // v2.0 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version20, vsVersion), FrameworkLocationHelper.dotNetFrameworkSdkInstallKeyValueV20); + + // v3.0 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version30, vsVersion); }); + + // v3.5 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version35, vsVersion), InstallationFolder); + + // v4.0 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version40, vsVersion), InstallationFolder); + + // v4.5 + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version45, vsVersion), InstallationFolder); + } + + // v4.5.1 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version100); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version110); }); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version120), InstallationFolder); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version140), InstallationFolder); + + // v4.6 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version100); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version110); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version120); }); + Assert.AreEqual(ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version140), InstallationFolder); + } + + [TestMethod] + public void GetPathToDotNetFrameworkSdk() + { + // Test out of range .net version. + foreach (var vsVersion in EnumVisualStudioVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk((TargetDotNetFrameworkVersion)99, vsVersion); }); + } + + // Test out of range visual studio version. + foreach (var dotNetVersion in EnumDotNetFrameworkVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(dotNetVersion, (VisualStudioVersion)99); }); + } + + string pathToSdk35InstallRoot = Path.Combine(FrameworkLocationHelper.programFiles32, @"Microsoft SDKs\Windows\v7.0A\"); + string pathToSdkV4InstallRootOnVS10 = Path.Combine(FrameworkLocationHelper.programFiles32, @"Microsoft SDKs\Windows\v7.0A\"); + string pathToSdkV4InstallRootOnVS11 = Path.Combine(FrameworkLocationHelper.programFiles32, @"Microsoft SDKs\Windows\v8.0A\"); + + // After uninstalling the 4.5 (Dev11) SDK, the Bootstrapper folder is left behind, so we can't + // just check for the root folder. + if (!Directory.Exists(Path.Combine(pathToSdkV4InstallRootOnVS11, "bin"))) + { + // falls back to the Dev10 location (7.0A) + pathToSdkV4InstallRootOnVS11 = pathToSdkV4InstallRootOnVS10; + } + + string pathToSdkV4InstallRootOnVS12 = Path.Combine(FrameworkLocationHelper.programFiles32, @"Microsoft SDKs\Windows\v8.1A\"); + + if (!Directory.Exists(pathToSdkV4InstallRootOnVS12)) + { + // falls back to the Dev11 location (8.0A) + pathToSdkV4InstallRootOnVS12 = pathToSdkV4InstallRootOnVS11; + } + + string pathToSdkV4InstallRootOnVS14 = Path.Combine(FrameworkLocationHelper.programFiles32, @"Microsoft SDKs\Windows\v10.0A\"); + + foreach (var vsVersion in EnumVisualStudioVersions()) + { + // v1.1 + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version11, vsVersion), FrameworkLocationHelper.PathToDotNetFrameworkSdkV11); + + // v2.0 + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version20, vsVersion), FrameworkLocationHelper.PathToDotNetFrameworkSdkV20); + + // v3.0 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version30, vsVersion); }); + + // v3.5 + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version35, vsVersion), pathToSdk35InstallRoot); + } + + // v4.0 + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version100), pathToSdkV4InstallRootOnVS10); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version110), pathToSdkV4InstallRootOnVS11); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version120), pathToSdkV4InstallRootOnVS12); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version140), pathToSdkV4InstallRootOnVS14); + + // v4.5 + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version100), pathToSdkV4InstallRootOnVS11); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version110), pathToSdkV4InstallRootOnVS11); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version120), pathToSdkV4InstallRootOnVS12); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version140), pathToSdkV4InstallRootOnVS14); + + // v4.5.1 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version100); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version110); }); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version120), pathToSdkV4InstallRootOnVS12); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.Version140), pathToSdkV4InstallRootOnVS14); + + // v4.6 + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version100); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version110); }); + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version120); }); + Assert.AreEqual(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.Version140), pathToSdkV4InstallRootOnVS14); + } + +#pragma warning disable 618 //The test below tests a deprecated API. We disable the warning for obsolete methods for this paricular test + + [TestMethod] + public void GetPathToWindowsSdk() + { + // Test out of range .net version. + foreach (var vsVersion in EnumVisualStudioVersions()) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToWindowsSdk((TargetDotNetFrameworkVersion)99, vsVersion); }); + } + + string pathToWindowsSdkV80 = GetRegistryValueHelper(RegistryHive.LocalMachine, RegistryView.Registry32, @"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.0", "InstallationFolder"); + string pathToWindowsSdkV81 = GetRegistryValueHelper(RegistryHive.LocalMachine, RegistryView.Registry32, @"SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.1", "InstallationFolder"); + + foreach (var vsVersion in EnumVisualStudioVersions().Concat(new[] { (VisualStudioVersion)99 })) + { + // v1.1, v2.0, v3.0, v3.5, v4.0 + foreach (var dotNetVersion in EnumDotNetFrameworkVersions().Where(v => v <= TargetDotNetFrameworkVersion.Version40)) + { + ObjectModelHelpers.AssertThrows(typeof(ArgumentException), delegate { ToolLocationHelper.GetPathToWindowsSdk(dotNetVersion, vsVersion); }); + } + + // v4.5 + Assert.AreEqual(ToolLocationHelper.GetPathToWindowsSdk(TargetDotNetFrameworkVersion.Version45, vsVersion), pathToWindowsSdkV80); + + // v4.5.1 + Assert.AreEqual(ToolLocationHelper.GetPathToWindowsSdk(TargetDotNetFrameworkVersion.Version451, vsVersion), pathToWindowsSdkV81); + + // v4.6 + Assert.AreEqual(ToolLocationHelper.GetPathToWindowsSdk(TargetDotNetFrameworkVersion.Version46, vsVersion), pathToWindowsSdkV81); + } + } + +#pragma warning restore 618 + + private static string s_verifyToolsetAndToolLocationHelperProjectCommonContent = @" + string currentInstallFolderLocation = null; + + using (RegistryKey baseKey = Registry.LocalMachine.OpenSubKey(""SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows"")) + { + if (baseKey != null) + { + object keyValue = baseKey.GetValue(""CurrentInstallFolder""); + + if (keyValue != null) + { + currentInstallFolderLocation = keyValue.ToString(); + } + } + } + + string sdk35ToolsPath = Sdk35ToolsPath == null ? Sdk35ToolsPath : Path.GetFullPath(Sdk35ToolsPath); + string sdk40ToolsPath = Sdk40ToolsPath == null ? Sdk40ToolsPath : Path.GetFullPath(Sdk40ToolsPath); + pathTo35Sdk = pathTo35Sdk == null ? pathTo35Sdk : Path.GetFullPath(pathTo35Sdk); + pathTo40Sdk = pathTo40Sdk == null ? pathTo40Sdk : Path.GetFullPath(pathTo40Sdk); + string currentInstall35Location = null; + string currentInstall40Location = null; + + if (currentInstallFolderLocation != null) + { + currentInstall35Location = Path.GetFullPath(Path.Combine(currentInstallFolderLocation, ""bin\\"")); + currentInstall40Location = Path.GetFullPath(Path.Combine(currentInstallFolderLocation, ""bin\\NetFX 4.0 Tools\\"")); + } + + Log.LogMessage(MessageImportance.High, ""SDK35ToolsPath = {0}"", Sdk35ToolsPath); + Log.LogMessage(MessageImportance.High, ""SDK40ToolsPath = {0}"", Sdk40ToolsPath); + Log.LogMessage(MessageImportance.High, ""pathTo35Sdk = {0}"", pathTo35Sdk); + Log.LogMessage(MessageImportance.High, ""pathTo40Sdk = {0}"", pathTo40Sdk); + Log.LogMessage(MessageImportance.High, ""currentInstall35Location = {0}"", currentInstall35Location); + Log.LogMessage(MessageImportance.High, ""currentInstall40Location = {0}"", currentInstall40Location); + + if (!String.Equals(sdk35ToolsPath, pathTo35Sdk, StringComparison.OrdinalIgnoreCase) && + (currentInstall35Location != null && /* this will be null on win8 express since 35 tools and this registry key will not be written, for vsultimate it is written*/ + !String.Equals(currentInstall35Location, pathTo35Sdk, StringComparison.OrdinalIgnoreCase)) + ) + { + Log.LogError(""Sdk35ToolsPath is incorrect! Registry: {0} ToolLocationHelper: {1} CurrentInstallFolder: {2}"", sdk35ToolsPath, pathTo35Sdk, currentInstall35Location); + } + + if (!String.Equals(sdk40ToolsPath, pathTo40Sdk, StringComparison.OrdinalIgnoreCase) && + (currentInstall40Location != null && /* this will be null on win8 express since 35 tools and this registry key will not be written, for vsultimate it is written*/ + !String.Equals(currentInstall40Location, pathTo40Sdk, StringComparison.OrdinalIgnoreCase)) + ) + { + Log.LogError(""Sdk40ToolsPath is incorrect! Registry: {0} ToolLocationHelper: {1} CurrentInstallFolder: {2}"", sdk40ToolsPath, pathTo40Sdk, currentInstall40Location); + } + "; + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyToolsetAndToolLocationHelperAgree() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + + + "); + + ILogger logger = new MockLogger(); + ProjectCollection collection = new ProjectCollection(); + Project p = ObjectModelHelpers.CreateInMemoryProject(collection, projectContents, logger); + + bool success = p.Build(logger); + + Assert.IsTrue(success, "Build Failed. See Std Out for details."); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyToolsetAndToolLocationHelperAgreeWhenVisualStudioVersionIsEmpty() + { + string projectContents = @" + + + + + + + + + + + + + + + + + + "; + + ILogger logger = new MockLogger(); + + ProjectCollection collection = new ProjectCollection(); + Project p = ObjectModelHelpers.CreateInMemoryProject(collection, projectContents, logger, "4.0"); + + bool success = p.Build(logger); + + Assert.IsTrue(success, "Build Failed. See Std Out for details."); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyToolsetAndToolLocationHelperAgreeWhenVisualStudioVersionIs10() + { + string projectContents = @" + + + + + + + + + + + + + + + + + + "; + + ILogger logger = new MockLogger(); + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "10.0"); + + ProjectCollection collection = new ProjectCollection(globalProperties); + Project p = ObjectModelHelpers.CreateInMemoryProject(collection, projectContents, logger, "4.0"); + + bool success = p.Build(logger); + + Assert.IsTrue(success, "Build Failed. See Std Out for details."); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyToolsetAndToolLocationHelperAgreeWhenVisualStudioVersionIs11() + { + string projectContents = @" + + + + + + + + + + + + + + + + + + "; + + ILogger logger = new MockLogger(); + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "11.0"); + + ProjectCollection collection = new ProjectCollection(globalProperties); + Project p = ObjectModelHelpers.CreateInMemoryProject(collection, projectContents, logger, "4.0"); + + bool success = p.Build(logger); + + Assert.IsTrue(success, "Build Failed. See Std Out for details."); + } + + #region GenerateReferenceAssemblyPath + [TestMethod] + public void GenerateReferencAssemblyPathAllElements() + { + string targetFrameworkRootPath = "c:\\Program Files\\Reference Assemblies\\Microsoft\\Framework"; + string targetFrameworkIdentifier = "Compact Framework"; + Version targetFrameworkVersion = new Version("1.0"); + string targetFrameworkProfile = "PocketPC"; + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile); + + string expectedPath = Path.Combine(targetFrameworkRootPath, targetFrameworkIdentifier); + expectedPath = Path.Combine(expectedPath, "v" + targetFrameworkVersion.ToString()); + expectedPath = Path.Combine(expectedPath, "Profile"); + expectedPath = Path.Combine(expectedPath, targetFrameworkProfile); + + string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName); + Assert.IsTrue(String.Equals(expectedPath, path, StringComparison.InvariantCultureIgnoreCase)); + } + + [TestMethod] + public void GenerateReferencAssemblyPathNoProfile() + { + string targetFrameworkRootPath = "c:\\Program Files\\Reference Assemblies\\Microsoft\\Framework"; + string targetFrameworkIdentifier = "Compact Framework"; + Version targetFrameworkVersion = new Version("1.0"); + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, targetFrameworkVersion, String.Empty); + string expectedPath = Path.Combine(targetFrameworkRootPath, targetFrameworkIdentifier); + expectedPath = Path.Combine(expectedPath, "v" + targetFrameworkVersion.ToString()); + + string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName); + Assert.IsTrue(String.Equals(expectedPath, path, StringComparison.InvariantCultureIgnoreCase)); + } + + /// + /// Make sure if the profile has invalid chars which would be used as part of path generation that we get an InvalidOperationException + /// which indicates there was a problem generating the reference assembly path. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void GenerateReferencAssemblyInvalidProfile() + { + string targetFrameworkRootPath = "c:\\Program Files\\Reference Assemblies\\Microsoft\\Framework"; + string targetFrameworkIdentifier = "Compact Framework"; + Version targetFrameworkVersion = new Version("1.0"); + string targetFrameworkProfile = "PocketPC" + new String(Path.GetInvalidFileNameChars()); + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile); + + string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName); + } + + /// + /// Make sure if the identifier has invalid chars which would be used as part of path generation that we get an InvalidOperationException + /// which indicates there was a problem generating the reference assembly path. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void GenerateReferencAssemblyInvalidIdentifier() + { + string targetFrameworkRootPath = "c:\\Program Files\\Reference Assemblies\\Microsoft\\Framework"; + string targetFrameworkIdentifier = "Compact Framework" + new String(Path.GetInvalidFileNameChars()); + Version targetFrameworkVersion = new Version("1.0"); + string targetFrameworkProfile = "PocketPC"; + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile); + + string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName); + } + + /// + /// Make sure if the moniker and the root make a too long path that an InvalidOperationException is raised + /// which indicates there was a problem generating the reference assembly path. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void GenerateReferencAssemblyPathTooLong() + { + string pathTooLong = new String('a', 500); + + string targetFrameworkRootPath = "c:\\Program Files\\Reference Assemblies\\Microsoft\\Framework"; + string targetFrameworkIdentifier = "Compact Framework" + pathTooLong; + Version targetFrameworkVersion = new Version("1.0"); + string targetFrameworkProfile = "PocketPC"; + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile); + + string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName); + } + + #endregion + + #region ChainReferenceAssemblyPath + + /// + /// Verify the chaining method returns a null if there is no redist list file for the framework we are trying to chaing with. This is ok because the lack of a redist list file means we + /// do not have anything to chain with. + /// + [TestMethod] + public void ChainReferenceAssembliesRedistExistsNoRedistList() + { + string path = ToolLocationHelper.ChainReferenceAssemblyPath(@"PathDoesNotExistSoICannotChain"); + Assert.IsNull(path, " Expected the path to be null when the path to the FrameworkList.xml does not exist"); + } + + /// + /// Verify we do not hang, crash, go on forever if there is a circular reference with the include frameworks. What should happen is + /// we should notice that we have already chained to a given framework and not try and chain with it again. + /// + [TestMethod] + public void ChainReferenceAssembliesRedistExistsCircularRefernce() + { + string redistString41 = "" + + "" + + ""; + + string redistString40 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistExistsChain"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + string redist40Directory = Path.Combine(tempDirectory, "v4.0\\RedistList\\"); + string redist40 = Path.Combine(redist40Directory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(redist41Directory); + Directory.CreateDirectory(redist40Directory); + File.WriteAllText(redist40, redistString40); + File.WriteAllText(redist41, redistString41); + + string path = ToolLocationHelper.ChainReferenceAssemblyPath(Path.Combine(tempDirectory, "v4.1")); + + string expectedChainedPath = Path.Combine(tempDirectory, "v4.0"); + Assert.IsTrue(String.Equals(path, expectedChainedPath, StringComparison.InvariantCultureIgnoreCase)); + } + finally + { + if (Directory.Exists(redist40Directory)) + { + Directory.Delete(redist40Directory, true); + } + + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + + /// + /// Verify the case where there is no Inclded framework attribute, there should be no errors and we should continue on as if there were no further framework chained with the current one + /// + [TestMethod] + public void ChainReferenceAssembliesRedistExistsNoInclude() + { + string redistString41 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistExistsNoInclude"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(redist41Directory); + File.WriteAllText(redist41, redistString41); + string path = ToolLocationHelper.ChainReferenceAssemblyPath(Path.Combine(tempDirectory, "v4.1")); + Assert.IsTrue(path == String.Empty, "Expected the path to be empty"); + } + finally + { + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + + /// + /// Verify the case where the include framework is empty, this is ok, we should error but should just continue on as if there was no chaining of the redist list file. + /// + [TestMethod] + public void ChainReferenceAssembliesRedistExistsEmptyInclude() + { + string redistString41 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistExistsNoInclude"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(redist41Directory); + File.WriteAllText(redist41, redistString41); + string path = ToolLocationHelper.ChainReferenceAssemblyPath(Path.Combine(tempDirectory, "v4.1")); + Assert.IsTrue(path == String.Empty, "Expected the path to be empty"); + } + finally + { + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + + /// + /// Verify the case where the redist is a valid xml file but does not have the FileListElement, this is to make sure we do not crash or get an exception if the FileList element cannot be found + /// + [TestMethod] + public void ChainReferenceAssembliesRedistExistsNoFileList() + { + string redistString41 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistExistsNoFileList"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(redist41Directory); + File.WriteAllText(redist41, redistString41); + string path = ToolLocationHelper.ChainReferenceAssemblyPath(Path.Combine(tempDirectory, "v4.1")); + Assert.IsTrue(path == String.Empty, "Expected the path to be empty"); + } + finally + { + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + + /// + /// Make sure we get the correct exception when there is no xml in the redist list file + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ChainReferenceAssembliesRedistExistsBadFile() + { + string redistString40 = "GARBAGE"; + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistExistsBadFile"); + + string redist40Directory = Path.Combine(tempDirectory, "v4.0\\RedistList\\"); + string redist40 = Path.Combine(redist40Directory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(redist40Directory); + File.WriteAllText(redist40, redistString40); + + string path = ToolLocationHelper.ChainReferenceAssemblyPath(Path.Combine(tempDirectory, "v4.0")); + Assert.IsNull(path, "Expected the path to be null"); + } + finally + { + if (Directory.Exists(redist40Directory)) + { + Directory.Delete(redist40Directory, true); + } + } + } + + /// + /// Make sure we get the correct exception when the xml file points to an included framwork which does not exist. + /// + [TestMethod] + public void ChainReferenceAssembliesRedistPointsToInvalidInclude() + { + string redistString41 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistPointsToInvalidInclude"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + string tempDirectoryPath = Path.Combine(tempDirectory, "v4.1"); + try + { + Directory.CreateDirectory(redist41Directory); + File.WriteAllText(redist41, redistString41); + + string path = ToolLocationHelper.ChainReferenceAssemblyPath(tempDirectoryPath); + Assert.IsNull(path); + } + finally + { + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + + /// + /// Make sure we get the correct exception when the xml file points to an included framwork which has invalid path chars. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ChainReferenceAssembliesRedistInvalidPathChars() + { + char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); + + string redistString41 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistInvalidPathChars"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + string tempDirectoryPath = Path.Combine(tempDirectory, "v4.1"); + try + { + Directory.CreateDirectory(redist41Directory); + File.WriteAllText(redist41, redistString41); + + string path = ToolLocationHelper.ChainReferenceAssemblyPath(tempDirectoryPath); + } + finally + { + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + + /// + /// Make sure we get the correct exception when the xml file points to an included framwork which has invalid path chars. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ChainReferenceAssembliesRedistPathTooLong() + { + string tooLong = new String('a', 500); + string redistString41 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "ChainReferenceAssembliesRedistPathTooLong"); + + string redist41Directory = Path.Combine(tempDirectory, "v4.1\\RedistList\\"); + string redist41 = Path.Combine(redist41Directory, "FrameworkList.xml"); + string tempDirectoryPath = Path.Combine(tempDirectory, "v4.1"); + try + { + Directory.CreateDirectory(redist41Directory); + File.WriteAllText(redist41, redistString41); + + string path = ToolLocationHelper.ChainReferenceAssemblyPath(tempDirectoryPath); + } + finally + { + if (Directory.Exists(redist41Directory)) + { + Directory.Delete(redist41Directory, true); + } + } + } + #endregion + + #region GetReferenceAssemblyPathWithRootPath + + /// + /// Verify the case where we are chaining redist lists and they are properly formatted + /// + [TestMethod] + public void GetPathToReferenceAssembliesWithRootGoodWithChain() + { + string redistString41 = "" + + "" + + ""; + + string redistString40 = "" + + "" + + ""; + + string redistString39 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "GetPathToReferenceAssembliesWithRootGoodWithChain"); + + string framework41Directory = Path.Combine(tempDirectory, "MyFramework\\v4.1\\"); + string framework41redistDirectory = Path.Combine(framework41Directory, "RedistList"); + string framework41RedistList = Path.Combine(framework41redistDirectory, "FrameworkList.xml"); + + string framework40Directory = Path.Combine(tempDirectory, "MyFramework\\v4.0\\"); + string framework40redistDirectory = Path.Combine(framework40Directory, "RedistList"); + string framework40RedistList = Path.Combine(framework40redistDirectory, "FrameworkList.xml"); + + string framework39Directory = Path.Combine(tempDirectory, "MyFramework\\v3.9\\"); + string framework39redistDirectory = Path.Combine(framework39Directory, "RedistList"); + string framework39RedistList = Path.Combine(framework39redistDirectory, "FrameworkList.xml"); + + + try + { + Directory.CreateDirectory(framework41redistDirectory); + Directory.CreateDirectory(framework40redistDirectory); + Directory.CreateDirectory(framework39redistDirectory); + + File.WriteAllText(framework39RedistList, redistString39); + File.WriteAllText(framework40RedistList, redistString40); + File.WriteAllText(framework41RedistList, redistString41); + + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("MyFramework", new Version("4.1")); + IList directories = ToolLocationHelper.GetPathToReferenceAssemblies(tempDirectory, frameworkName); + + Assert.IsTrue(directories.Count == 3, "Expected the method to return three paths."); + Assert.IsTrue(String.Equals(directories[0], framework41Directory, StringComparison.OrdinalIgnoreCase), "Expected first entry to be first in chain but it was" + directories[0]); + Assert.IsTrue(String.Equals(directories[1], framework40Directory, StringComparison.OrdinalIgnoreCase), "Expected first entry to be second in chain but it was" + directories[1]); + Assert.IsTrue(String.Equals(directories[2], framework39Directory, StringComparison.OrdinalIgnoreCase), "Expected first entry to be third in chain but it was" + directories[2]); + } + finally + { + if (Directory.Exists(framework41Directory)) + { + Directory.Delete(framework41Directory, true); + } + + if (Directory.Exists(framework40Directory)) + { + Directory.Delete(framework40Directory, true); + } + + if (Directory.Exists(framework39Directory)) + { + Directory.Delete(framework39Directory, true); + } + } + } + + /// + /// Verify the correct display name returned + /// + [TestMethod] + public void DisplayNameGeneration() + { + string redistString40 = "" + + "" + + ""; + + string redistString39 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "DisplayNameGeneration"); + + string framework40Directory = Path.Combine(tempDirectory, "MyFramework\\v4.0\\"); + string framework40redistDirectory = Path.Combine(framework40Directory, "RedistList"); + string framework40RedistList = Path.Combine(framework40redistDirectory, "FrameworkList.xml"); + + string framework39Directory = Path.Combine(tempDirectory, "MyFramework\\v3.9\\Profile\\Client"); + string framework39redistDirectory = Path.Combine(framework39Directory, "RedistList"); + string framework39RedistList = Path.Combine(framework39redistDirectory, "FrameworkList.xml"); + + try + { + Directory.CreateDirectory(framework40redistDirectory); + Directory.CreateDirectory(framework39redistDirectory); + + File.WriteAllText(framework39RedistList, redistString39); + File.WriteAllText(framework40RedistList, redistString40); + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("MyFramework", new Version("4.0")); + string displayName40 = ToolLocationHelper.GetDisplayNameForTargetFrameworkDirectory(framework40Directory, frameworkName); + + frameworkName = new FrameworkNameVersioning("MyFramework", new Version("3.9"), "Client"); + string displayName39 = ToolLocationHelper.GetDisplayNameForTargetFrameworkDirectory(framework39Directory, frameworkName); + Assert.IsTrue(displayName40.Equals("MyFramework 4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(displayName39.Equals("MyFramework v3.9 Client", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(framework40Directory)) + { + Directory.Delete(framework40Directory, true); + } + + if (Directory.Exists(framework39Directory)) + { + Directory.Delete(framework39Directory, true); + } + } + } + + + /// + /// Make sure we do not crach if there is a circular reference in the redist lists, we should only have a path in our reference assembly list once. + /// + /// + [TestMethod] + public void GetPathToReferenceAssembliesWithRootCircularReference() + { + string redistString41 = "" + + "" + + ""; + + string redistString40 = "" + + "" + + ""; + + string tempDirectory = Path.Combine(Path.GetTempPath(), "GetPathToReferenceAssembliesWithRootGoodWithChain"); + + string framework41Directory = Path.Combine(tempDirectory, "MyFramework\\v4.1\\"); + string framework41redistDirectory = Path.Combine(framework41Directory, "RedistList"); + string framework41RedistList = Path.Combine(framework41redistDirectory, "FrameworkList.xml"); + + string framework40Directory = Path.Combine(tempDirectory, "MyFramework\\v4.0\\"); + string framework40redistDirectory = Path.Combine(framework40Directory, "RedistList"); + string framework40RedistList = Path.Combine(framework40redistDirectory, "FrameworkList.xml"); + + try + { + Directory.CreateDirectory(framework41redistDirectory); + Directory.CreateDirectory(framework40redistDirectory); + + File.WriteAllText(framework40RedistList, redistString40); + File.WriteAllText(framework41RedistList, redistString41); + + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("MyFramework", new Version("4.1")); + IList directories = ToolLocationHelper.GetPathToReferenceAssemblies(tempDirectory, frameworkName); + + Assert.IsTrue(directories.Count == 2, "Expected the method to return two paths."); + Assert.IsTrue(String.Equals(directories[0], framework41Directory, StringComparison.OrdinalIgnoreCase), "Expected first entry to be first in chain but it was" + directories[0]); + Assert.IsTrue(String.Equals(directories[1], framework40Directory, StringComparison.OrdinalIgnoreCase), "Expected first entry to be second in chain but it was" + directories[1]); + } + finally + { + if (Directory.Exists(framework41Directory)) + { + Directory.Delete(framework41Directory, true); + } + + if (Directory.Exists(framework40Directory)) + { + Directory.Delete(framework40Directory, true); + } + } + } + + + /// + /// Test the case where the root path is a string but the framework name is null. + /// We should expect the correct argument null exception + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void GetPathToReferenceAssembliesNullFrameworkName() + { + ToolLocationHelper.GetPathToReferenceAssemblies("Not Null String", (FrameworkNameVersioning)null); + } + + /// + /// Make sure we get the correct exception when both parameters are null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void GetPathToReferenceAssembliesNullArgumentNameandFrameworkName() + { + ToolLocationHelper.GetPathToReferenceAssemblies(null, (FrameworkNameVersioning)null); + } + + /// + /// Make sure we get the correct exception when the root is null but the frameworkname is not null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void GetPathToReferenceAssembliesNullArgumentGoodFrameworkNameNullRoot() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Ident", new Version("2.0")); + ToolLocationHelper.GetPathToReferenceAssemblies(null, frameworkName); + } + + /// + /// Make sure we get the correct exception when the root is null but the frameworkname is not null + /// With no framework name we cannot generate the path + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void GetPathToReferenceAssembliesNullArgumentGoodFrameworkNameEmptyRoot() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Ident", new Version("2.0")); + ToolLocationHelper.GetPathToReferenceAssemblies(String.Empty, frameworkName); + } + + /// + /// Make sure we get the correct exception when the root is null but the frameworkname is not empty to make sure we cover the different input cases + /// With no root we cannot properly generate the path. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void GetPathToReferenceAssembliesNullArgumentGoodFrameworkNameEmptyRoot2() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Ident", new Version("2.0")); + ToolLocationHelper.GetPathToReferenceAssemblies(String.Empty, frameworkName); + } + #endregion + + #region GetReferenceAssemblyPathWithDefaultRoot + + /// + /// Test the case where the method which only takes in a FrameworkName will throw an exception when + /// the input is null since a null framework name is not useful + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void GetPathToReferenceAssembliesDefaultLocationNullFrameworkName() + { + ToolLocationHelper.GetPathToReferenceAssemblies((FrameworkNameVersioning)null); + } + + /// + /// Verify the method correctly returns the 4.5 reference assembly location information if .net 4.5 and + /// its corresponding reference assemblies are installed. + /// If they are not installed, the test should be ignored. + /// + [TestMethod] + public void GetPathToReferenceAssembliesDefaultLocation45() + { + FrameworkNameVersioning frameworkName = null; + IList directories = null; + if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45) != null) + { + frameworkName = new FrameworkNameVersioning(".NETFramework", new Version("4.5")); + directories = ToolLocationHelper.GetPathToReferenceAssemblies(frameworkName); + Assert.IsTrue(directories.Count == 1, "Expected the method to return one path."); + + string referenceAssemblyPath = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45); + Assert.IsTrue(String.Equals(directories[0], referenceAssemblyPath, StringComparison.OrdinalIgnoreCase), "Expected referenceassembly directory to be " + referenceAssemblyPath + " but it was " + directories[0]); + } + // else + // "Ignored because v4.5 did not seem to be installed" + } + + /// + /// Test the case where the framework requested does not exist. Since we do an existence check before returning the path this non existent path should return an empty list + /// + [TestMethod] + public void GetPathToReferenceAssembliesDefaultLocation99() + { + string targetFrameworkRootPath = Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Reference Assemblies\\Microsoft\\Framework"); + string targetFrameworkIdentifier = ".Net Framework"; + Version targetFrameworkVersion = new Version("99.99"); + + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, targetFrameworkVersion, String.Empty); + + IList directories = ToolLocationHelper.GetPathToReferenceAssemblies(frameworkName); + Assert.IsTrue(directories.Count == 0, "Expected the method to return no paths."); + } + + /// + /// Make sure we choose the correct path for program files based on the environment variables + /// + [TestMethod] + public void TestGenerateProgramFiles32() + { + string programFilesX86Original = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); + string programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + try + { + Environment.SetEnvironmentVariable("ProgramFiles(x86)", null); + string result = FrameworkLocationHelper.GenerateProgramFiles32(); + Assert.IsTrue(programFiles.Equals(result, StringComparison.OrdinalIgnoreCase), "Expected to use program files but used program files x86"); + + Environment.SetEnvironmentVariable("ProgramFiles(x86)", String.Empty); + + result = FrameworkLocationHelper.GenerateProgramFiles32(); + Assert.IsTrue(programFiles.Equals(result, StringComparison.OrdinalIgnoreCase), "Expected to use program files but used program files x86"); + } + finally + { + Environment.SetEnvironmentVariable("ProgramFiles(x86)", programFilesX86Original); + } + } + + /// + /// Verify we get the correct reference assembly path out of the framework location helper + /// + [TestMethod] + public void TestGeneratedReferenceAssemblyPath() + { + string programFiles32 = FrameworkLocationHelper.GenerateProgramFiles32(); + string referenceAssemblyRoot = FrameworkLocationHelper.GenerateProgramFilesReferenceAssemblyRoot(); + string pathToCombineWith = "Reference Assemblies\\Microsoft\\Framework"; + string combinedPath = Path.Combine(programFiles32, pathToCombineWith); + string fullPath = Path.GetFullPath(combinedPath); + + Assert.IsTrue(referenceAssemblyRoot.Equals(fullPath, StringComparison.OrdinalIgnoreCase), String.Format("Expected the path to be '{0}' but it was '{1}'", fullPath, referenceAssemblyRoot)); + } + + + #endregion + + #region HandleLegacyFrameworks + + /// + /// Verify when 20 is simulated to be installed that the method returns the 2.0 directory + /// + [TestMethod] + public void LegacyFramework20Good() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("2.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNet20Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 1); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet20FrameworkPath, list[0]); + } + + /// + /// Verify when 20 is simulated to not be installed that the method returns an empty list + /// + [TestMethod] + public void LegacyFramework20NotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("2.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + /// + /// Verify when 30 is simulated to be installed that the method returns the 3.0 directory + /// + [TestMethod] + public void LegacyFramework30Good() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies30Installed = true; + legacyHelper.DotNet30Installed = true; + legacyHelper.DotNet20Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 3); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet30ReferenceAssemblyPath, list[0]); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet30FrameworkPath, list[1]); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet20FrameworkPath, list[2]); + } + + /// + /// Verify when 30 is simulated to not be installed that the method returns an empty list + /// + [TestMethod] + public void LegacyFramework30NotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies30Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + /// + /// Verify when the 30 reference assemblies are simulated to not be installed that the method returns an empty list + /// + [TestMethod] + public void LegacyFramework30ReferenceAssembliesNotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNet30Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + /// + /// Verify when 30 is installed but 2.0 is not installed that we only get one of the paths back. + /// + [TestMethod] + public void LegacyFramework30WithNo20Installed() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNet30Installed = true; + legacyHelper.DotNetReferenceAssemblies30Installed = true; + // Note no 2.0 installed + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 2); + Assert.IsTrue(list[0].Equals(LegacyFrameworkTestHelper.DotNet30ReferenceAssemblyPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(list[1].Equals(LegacyFrameworkTestHelper.DotNet30FrameworkPath, StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// Verify when 35 is simulated to be installed that the method returns the 3.5 directory + /// + [TestMethod] + public void LegacyFramework35Good() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.5")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies35Installed = true; + legacyHelper.DotNetReferenceAssemblies30Installed = true; + legacyHelper.DotNet30Installed = true; + legacyHelper.DotNet35Installed = true; + legacyHelper.DotNet20Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 5); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet35ReferenceAssemblyPath, list[0]); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet35FrameworkPath, list[1]); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet30ReferenceAssemblyPath, list[2]); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet30FrameworkPath, list[3]); + Assert.AreEqual(LegacyFrameworkTestHelper.DotNet20FrameworkPath, list[4]); + } + + /// + /// Verify when 35 is simulated to not be installed that the method returns an empty list + /// + [TestMethod] + public void LegacyFramework35NotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.5")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies35Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + + /// + /// Verify when 35 reference asssemblie are simulated to not be installed that the method returns an empty list + /// + [TestMethod] + public void LegacyFramework35ReferenceAssembliesNotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.5")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNet35Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + /// + /// Make sure when we are targeting .net framework 3.5 and are on a 64 bit machine we get the correct framework path. + /// + /// We are on a 64 bit machine + /// Targeting .net framework 3.5 + /// + /// 1) Target platform is x86. We expect to get the 32 bit framework directory + /// 2) Target platform is x64, we expect to get the 64 bit framework directory + /// 3) Target platform is Itanium, we expect to get the 64 bit framework directory + /// 3) Target platform is some other value (AnyCpu, or anything else) expect the framework directory for the "current" bitness of the process we are running under. + /// + /// + [TestMethod] + public void GetPathToStandardLibraries64Bit35() + { + string frameworkDirectory2032bit = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Bitness32); + string frameworkDirectory2064bit = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Bitness64); + string frameworkDirectory20Current = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Current); + + if (!Environment.Is64BitOperatingSystem) + { + // "Not 64 bit OS " + return; + } + + if (String.IsNullOrEmpty(frameworkDirectory2032bit) || String.IsNullOrEmpty(frameworkDirectory2064bit) || String.IsNullOrEmpty(frameworkDirectory20Current)) + { + // ".Net 2.0 not installed: checked current {0} :: 64 bit :: {1} :: 32 bit {2}", frameworkDirectory20Current, frameworkDirectory2064bit, frameworkDirectory2032bit + return; + } + + string pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "x86"); + Assert.IsTrue(frameworkDirectory2032bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2032bit, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "x64"); + Assert.IsTrue(frameworkDirectory2064bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2064bit, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "itanium"); + Assert.IsTrue(frameworkDirectory2064bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2064bit, pathToFramework)); + + if (!Environment.Is64BitProcess) + { + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "RandomPlatform"); + Assert.IsTrue(frameworkDirectory2032bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2032bit, pathToFramework)); + } + else + { + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "RandomPlatform"); + Assert.IsTrue(frameworkDirectory2064bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2064bit, pathToFramework)); + } + } + + /// + /// Make sure when we are targeting .net framework 3.5 and are on a 64 bit machine we get the correct framework path. + /// + /// We are on a 64 bit machine + /// Targeting .net framework 4.0 + /// + /// We expect to always get the same path which is returned by GetPathToReferenceAssemblies. + /// + [TestMethod] + public void GetPathToStandardLibraries64Bit40() + { + IList referencePaths = ToolLocationHelper.GetPathToReferenceAssemblies(new FrameworkNameVersioning(".NETFramework", new Version("4.0"))); + + if (!Environment.Is64BitOperatingSystem) + { + // "Not 64 bit OS " + return; + } + + if (referencePaths.Count == 0) + { + // ".Net 4.0 not installed" + return; + } + + string pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "x86"); + string dotNet40Path = FileUtilities.EnsureNoTrailingSlash(referencePaths[0]); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "x64"); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "itanium"); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "RandomPlatform"); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + } + + /// + /// Make sure when we are targeting .net framework 3.5 and are on a 32 bit machine we get the correct framework path. + /// + /// We are on a 32 bit machine + /// Targeting .net framework 3.5 + /// + /// 1) Target platform is x86. We expect to get the 32 bit framework directory + /// 2) Target platform is x64, we expect to get the 32 bit framework directory + /// 3) Target platform is Itanium, we expect to get the 32 bit framework directory + /// 3) Target platform is some other value (AnyCpu, or anything else) expect the framework directory for the "current" bitness of the process we are running under. In the + /// case of the unit test this should be the 32 bit framework directory. + /// + /// + [TestMethod] + public void GetPathToStandardLibraries32Bit35() + { + string frameworkDirectory2032bit = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Bitness32); + string frameworkDirectory20Current = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(SharedDotNetFrameworkArchitecture.Current); + + if (Environment.Is64BitOperatingSystem) + { + // "Is a 64 bit OS " + return; + } + + if (String.IsNullOrEmpty(frameworkDirectory2032bit) || String.IsNullOrEmpty(frameworkDirectory20Current)) + { + // ".Net 2.0 not installed: checked current {0} :: 32 bit {2}", frameworkDirectory20Current, frameworkDirectory2032bit + return; + } + + string pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "x86"); + Assert.IsTrue(frameworkDirectory2032bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2032bit, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "x64"); + Assert.IsTrue(frameworkDirectory2032bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2032bit, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "itanium"); + Assert.IsTrue(frameworkDirectory2032bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2032bit, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v3.5", String.Empty, "RandomPlatform"); + Assert.IsTrue(frameworkDirectory2032bit.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", frameworkDirectory2032bit, pathToFramework)); + } + + /// + /// Make sure when we are targeting .net framework 4.0 and are on a 32 bit machine we get the correct framework path. + /// + /// We are on a 32 bit machine + /// Targeting .net framework 4.0 + /// + /// We expect to always get the same path which is returned by GetPathToReferenceAssemblies. + /// + [TestMethod] + public void GetPathToStandardLibraries32Bit40() + { + IList referencePaths = ToolLocationHelper.GetPathToReferenceAssemblies(new FrameworkNameVersioning(".NETFramework", new Version("4.0"))); + + if (Environment.Is64BitOperatingSystem) + { + // "Is 64 bit OS " + return; + } + + if (referencePaths.Count == 0) + { + // ".Net 4.0 not installed" + return; + } + + string pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "x86"); + string dotNet40Path = FileUtilities.EnsureNoTrailingSlash(referencePaths[0]); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "x64"); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "itanium"); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + + pathToFramework = ToolLocationHelper.GetPathToStandardLibraries(".NetFramework", "v4.0", String.Empty, "RandomPlatform"); + Assert.IsTrue(dotNet40Path.Equals(pathToFramework, StringComparison.OrdinalIgnoreCase), String.Format("Expected {0} but got {1}", dotNet40Path, pathToFramework)); + } + + /// + /// Verify when 35 is installed but 2.0 is not installed we to find 3.5 and 3.0 but no 2.0 because it does not exist. + /// + [TestMethod] + public void LegacyFramework35WithNo20Installed() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.5")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies35Installed = true; + legacyHelper.DotNetReferenceAssemblies30Installed = true; + legacyHelper.DotNet35Installed = true; + legacyHelper.DotNet30Installed = true; + // Note no 2.0 installed + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 4); + Assert.IsTrue(list[0].Equals(LegacyFrameworkTestHelper.DotNet35ReferenceAssemblyPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(list[1].Equals(LegacyFrameworkTestHelper.DotNet35FrameworkPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(list[2].Equals(LegacyFrameworkTestHelper.DotNet30ReferenceAssemblyPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(list[3].Equals(LegacyFrameworkTestHelper.DotNet30FrameworkPath, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when 35 is installed but 3.0 is not installed we expect not to find 3.0 or 2.0. + /// + [TestMethod] + public void LegacyFramework35WithNo30Installed() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("3.5")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies35Installed = true; + legacyHelper.DotNet35Installed = true; + legacyHelper.DotNet20Installed = true; + // Note no 3.0 + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 2); + Assert.IsTrue(list[0].Equals(LegacyFrameworkTestHelper.DotNet35ReferenceAssemblyPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(list[1].Equals(LegacyFrameworkTestHelper.DotNet35FrameworkPath, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when 40 is simulated to not be installed that the method returns an empty list + /// + [TestMethod] + public void LegacyFramework40NotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("4.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + /// + /// Verify when 40 reference assemblies are installed but the dot net framework is not, in this case we return empty indicating .net 4.0 is not properly installed + /// + [TestMethod] + public void LegacyFramework40DotNetFrameworkDirectoryNotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("4.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNetReferenceAssemblies40Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + + /// + /// Verify when 40 reference assemblies are installed but the dot net framework is not we only get one of the paths back, this is because right now the assemblies are not in the right location + /// + [TestMethod] + public void LegacyFramework40DotNetReferenceAssemblyDirectoryNotInstalled() + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning("Anything", new Version("4.0")); + LegacyFrameworkTestHelper legacyHelper = new LegacyFrameworkTestHelper(); + legacyHelper.DotNet40Installed = true; + + IList list = ToolLocationHelper.HandleLegacyDotNetFrameworkReferenceAssemblyPaths(legacyHelper.GetDotNetVersionToPathDelegate, legacyHelper.GetDotNetReferenceAssemblyDelegate, frameworkName); + Assert.IsTrue(list.Count == 0); + } + #endregion + + /// + /// Verify we can an argument exception if we try and pass a empty registry root + /// + [ExpectedException(typeof(ArgumentException))] + [TestMethod] + public void GetAssemblyFoldersExInfoTestEmptyRegistryRoot() + { + ToolLocationHelper.GetAssemblyFoldersExInfo("", "v3.0", "AssemblyFoldersEx", null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + + /// + /// Verify we can an argumentNull exception if we try and pass a null registry root + /// + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void GetAssemblyFoldersExInfoListTestNullRegistryRoot() + { + ToolLocationHelper.GetAssemblyFoldersExInfo(null, "v3.0", "AssemblyFoldersEx", null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + + /// + /// Verify we can an argument exception if we try and pass a empty registry suffix + /// + [ExpectedException(typeof(ArgumentException))] + [TestMethod] + public void GetAssemblyFoldersExInfoTestEmptyRegistrySuffix() + { + ToolLocationHelper.GetAssemblyFoldersExInfo(@"SOFTWARE\Microsoft\.UnitTest", "v3.0", "", null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + + /// + /// Verify we can an argumentNull exception if we try and pass a null registry suffix + /// + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void GetAssemblyFoldersExInfoTestNullRegistrySuffix() + { + ToolLocationHelper.GetAssemblyFoldersExInfo(@"SOFTWARE\Microsoft\.UnitTest", "v3.0", null, null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + + /// + /// Verify we can an argument exception if we try and pass a empty registry suffix + /// + [ExpectedException(typeof(ArgumentException))] + [TestMethod] + public void GetAssemblyFoldersExInfoTestEmptyTargetRuntime() + { + ToolLocationHelper.GetAssemblyFoldersExInfo(@"SOFTWARE\Microsoft\.UnitTest", "", "AssemblyFoldersEx", null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + + /// + /// Verify we can an argumentNull exception if we try and pass a null target runtime version + /// + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void GetAssemblyFoldersExInfoTestNullTargetRuntimeVersion() + { + ToolLocationHelper.GetAssemblyFoldersExInfo(@"SOFTWARE\Microsoft\.UnitTest", null, "AssemblyFoldersEx", null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + + /// + /// Verify we can get a list of directories out of the public API. + /// + [TestMethod] + public void GetAssemblyFoldersExInfoTest() + { + SetupAssemblyFoldersExTestConditionRegistryKey(); + IList directories = null; + try + { + directories = ToolLocationHelper.GetAssemblyFoldersExInfo(@"SOFTWARE\Microsoft\.UnitTest", "v3.0", "AssemblyFoldersEx", null, null, System.Reflection.ProcessorArchitecture.MSIL); + } + finally + { + RemoveAssemblyFoldersExTestConditionRegistryKey(); + } + Assert.IsNotNull(directories); + Assert.IsTrue(directories.Count == 2); + Assert.IsTrue(@"C:\V1Control2".Equals(directories[0].DirectoryPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(@"C:\V1Control".Equals(directories[1].DirectoryPath, StringComparison.OrdinalIgnoreCase)); + } + + private void SetupAssemblyFoldersExTestConditionRegistryKey() + { + RegistryKey baseKey = Registry.CurrentUser; + baseKey.DeleteSubKeyTree(@"SOFTWARE\Microsoft\.UnitTest", false); + RegistryKey folderKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\.UnitTest\v2.0.3600\AssemblyFoldersEx\Component1"); + folderKey.SetValue("", @"C:\V1Control"); + + RegistryKey servicePackKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\.UnitTest\v2.0.3600\AssemblyFoldersEx\Component2"); + servicePackKey.SetValue("", @"C:\V1Control2"); + } + + private void RemoveAssemblyFoldersExTestConditionRegistryKey() + { + RegistryKey baseKey = Registry.CurrentUser; + try + { + baseKey.DeleteSubKeyTree(@"SOFTWARE\Microsoft\.UnitTest\v2.0.3600\AssemblyFoldersEx\Component1"); + baseKey.DeleteSubKeyTree(@"SOFTWARE\Microsoft\.UnitTest\v2.0.3600\AssemblyFoldersEx\Component2"); + } + catch (Exception) + { + } + } + + /* + * Method: GetDirectories + * + * Delegate method simulates a file system for testing location methods. + */ + private static string[] GetDirectories(string path, string pattern) + { + if (path == "{runtime-base}" && pattern == "v1.2*") + { + return new string[] { @"{runtime-base}\v1.2.30617", @"{runtime-base}\v1.2.x86dbg", @"{runtime-base}\v1.2.x86fre" }; + } + return new string[0]; + } + + /* + * Method: GetDirectories35 + * + * Delegate method simulates a file system for testing location methods. + */ + private static string[] GetDirectories35(string path, string pattern) + { + return new string[] { @"{runtime-base}\v3.5.12333", @"{runtime-base}\v3.5", @"{runtime-base}\v3.5.23455" }; + } + + /// + /// Delegate method simulates a file system for testing location methods. + /// + /// + /// + private static bool DirectoryExists(string path) + { + return path.Contains("{runtime-base}") || Directory.Exists(path); + } + + private static string GetRegistryValueHelper(RegistryHive hive, RegistryView view, string subKeyPath, string name) + { + using (var key = RegistryHelper.OpenBaseKey(hive, view)) + using (var subKey = key.OpenSubKey(subKeyPath)) + { + if (subKey != null) + { + return (string)subKey.GetValue(name); + } + } + + return null; + } + + private static IEnumerable EnumVisualStudioVersions() + { + for (VisualStudioVersion vsVersion = VisualStudioVersion.Version100; vsVersion <= VisualStudioVersion.VersionLatest; ++vsVersion) + { + yield return vsVersion; + } + } + + private static IEnumerable EnumDotNetFrameworkVersions() + { + for (TargetDotNetFrameworkVersion dotNetVersion = TargetDotNetFrameworkVersion.Version11; dotNetVersion <= TargetDotNetFrameworkVersion.VersionLatest; ++dotNetVersion) + { + yield return dotNetVersion; + } + } + + /// + /// This class will provide delegates and properties to allow differen combinations of ToolLocationHelper GetDotNetFrameworkPaths and GetReferenceAssemblyPaths to be simulated. + /// + internal class LegacyFrameworkTestHelper + { + /// + /// Paths which simulate the fact that the frameworks are installed including their reference assemblies + /// + internal const string DotNet40ReferenceAssemblyPath = "C:\\Program Files\\Reference Assemblies\\Framework\\V4.0"; + internal const string DotNet35ReferenceAssemblyPath = "C:\\Program Files\\Reference Assemblies\\Framework\\V3.5"; + internal const string DotNet30ReferenceAssemblyPath = "C:\\Program Files\\Reference Assemblies\\Framework\\V3.0"; + internal const string DotNet20FrameworkPath = "C:\\Microsoft\\.Net Framework\\V2.0.57027"; + internal const string DotNet30FrameworkPath = "C:\\Microsoft\\.Net Framework\\V3.0"; + internal const string DotNet35FrameworkPath = "C:\\Microsoft\\.Net Framework\\V3.5"; + internal const string DotNet40FrameworkPath = "C:\\Microsoft\\.Net Framework\\V4.0"; + + /// + /// Should the delegate respond with a path or null when asked for Version20 on the delegate which gets the DotNetFrameworkPath + /// + internal bool DotNet20Installed + { + get; + set; + } + + /// + /// Should the delegate respond with a path or null when asked for Version30 on the delegate which gets the DotNetFrameworkPath + /// + internal bool DotNet30Installed + { + get; + set; + } + + /// + /// Should the delegate respond with a path or null when asked for Version35 on the delegate which gets the DotNetFrameworkPath + /// + internal bool DotNet35Installed + { + get; + set; + } + + /// + /// Should the delegate respond with a path or null when asked for Version40 on the delegate which gets the DotNetFrameworkPath + /// + internal bool DotNet40Installed + { + get; + set; + } + + /// + /// Should the delegate respond with a path or null when asked for Version40 on the delegate which gets the DotNetReferenceAssembliesPath is called + /// + internal bool DotNetReferenceAssemblies40Installed + { + get; + set; + } + + /// + /// Should the delegate respond with a path or null when asked for Version35 on the delegate which gets the DotNetReferenceAssembliesPath is called + /// + internal bool DotNetReferenceAssemblies35Installed + { + get; + set; + } + + /// + /// Should the delegate respond with a path or null when asked for Version30 on the delegate which gets the DotNetReferenceAssembliesPath is called + /// + internal bool DotNetReferenceAssemblies30Installed + { + get; + set; + } + + /// + /// Return a delegate which will return a path or null depending on whether or not frameworks and their reference assembly paths are being simulated as being installed + /// + internal ToolLocationHelper.VersionToPath GetDotNetVersionToPathDelegate + { + get + { + return new ToolLocationHelper.VersionToPath(GetDotNetFramework); + } + } + + /// + /// Return a delegate which will return a path or null depending on whether or not frameworks and their reference assembly paths are being simulated as being installed + /// + internal ToolLocationHelper.VersionToPath GetDotNetReferenceAssemblyDelegate + { + get + { + return new ToolLocationHelper.VersionToPath(GetDotNetFrameworkReferenceAssemblies); + } + } + + /// + /// Return a path to the .net framework reference assemblies if the boolean property said we should return one. + /// Return null if we should not fake the fact that the framework reference assemblies are installed + /// + internal string GetDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion version) + { + if (version == TargetDotNetFrameworkVersion.Version40) + { + if (DotNetReferenceAssemblies40Installed) + { + return DotNet40ReferenceAssemblyPath; + } + else + { + return null; + } + } + + if (version == TargetDotNetFrameworkVersion.Version35) + { + if (DotNetReferenceAssemblies35Installed) + { + return DotNet35ReferenceAssemblyPath; + } + else + { + return null; + } + } + + if (version == TargetDotNetFrameworkVersion.Version30) + { + if (DotNetReferenceAssemblies30Installed) + { + return DotNet30ReferenceAssemblyPath; + } + else + { + return null; + } + } + + return null; + } + + /// + /// Return a path to the .net framework if the boolean property said we should return one. + /// Return null if we should not fake the fact that the framework is installed + /// + internal string GetDotNetFramework(TargetDotNetFrameworkVersion version) + { + if (version == TargetDotNetFrameworkVersion.Version20) + { + if (DotNet20Installed) + { + return DotNet20FrameworkPath; + } + else + { + return null; + } + } + + if (version == TargetDotNetFrameworkVersion.Version30) + { + if (DotNet30Installed) + { + return DotNet30FrameworkPath; + } + else + { + return null; + } + } + + if (version == TargetDotNetFrameworkVersion.Version35) + { + if (DotNet35Installed) + { + return DotNet35FrameworkPath; + } + else + { + return null; + } + } + + if (version == TargetDotNetFrameworkVersion.Version40) + { + if (DotNet40Installed) + { + return DotNet40FrameworkPath; + } + else + { + return null; + } + } + + return null; + } + } + } + + /// + /// Verify the toolLocation helper method that enumerates the disk and registry to get the list of installed SDKs. + /// + [TestClass] + public class GetPlatformExtensionSDKLocationsTestFixture + { + // Create delegates to mock the registry for the registry portion of the test. + private static Microsoft.Build.Shared.OpenBaseKey s_openBaseKey = new Microsoft.Build.Shared.OpenBaseKey(GetBaseKey); + internal static Microsoft.Build.Shared.GetRegistrySubKeyNames getRegistrySubKeyNames = new Microsoft.Build.Shared.GetRegistrySubKeyNames(GetRegistrySubKeyNames); + internal Microsoft.Build.Shared.GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue; + + // Path to the fake SDk directory structure created under the temp directory. + private static string s_fakeStructureRoot = null; + private static string s_fakeStructureRoot2 = null; + + public GetPlatformExtensionSDKLocationsTestFixture() + { + getRegistrySubKeyDefaultValue = new Microsoft.Build.Shared.GetRegistrySubKeyDefaultValue(GetRegistrySubKeyDefaultValue); + } + + #region TestMethods + + [ClassInitialize] + public static void ClassInit(TestContext context) + { + s_fakeStructureRoot = MakeFakeSDKStructure(); + s_fakeStructureRoot2 = MakeFakeSDKStructure2(); + } + + [ClassCleanup] + public static void ClassCleanup() + { + if (s_fakeStructureRoot != null) + { + if (FileUtilities.DirectoryExistsNoThrow(s_fakeStructureRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(s_fakeStructureRoot, true); + } + } + + if (s_fakeStructureRoot2 != null) + { + if (FileUtilities.DirectoryExistsNoThrow(s_fakeStructureRoot2)) + { + FileUtilities.DeleteDirectoryNoThrow(s_fakeStructureRoot2, true); + } + } + } + + /// + /// Pass empty and null target platform identifier and target platform version string to make sure we get the correct exceptions out. + /// + [TestMethod] + public void PassEmptyAndNullTPM() + { + VerifyExceptionOnEmptyOrNullPlatformAttributes(String.Empty, new Version("1.0")); + VerifyExceptionOnEmptyOrNullPlatformAttributes(null, new Version("1.0")); + VerifyExceptionOnEmptyOrNullPlatformAttributes(null, null); + VerifyExceptionOnEmptyOrNullPlatformAttributes("Windows", null); + } + + /// + /// Verify that we get argument exceptions where different combinations of identifier and version are passed in. + /// + private static void VerifyExceptionOnEmptyOrNullPlatformAttributes(string identifier, Version version) + { + bool caughtCorrectException = false; + try + { + ToolLocationHelper.GetPlatformExtensionSDKLocations(identifier, version); + } + catch (ArgumentException) + { + caughtCorrectException = true; + } + + Assert.IsTrue(caughtCorrectException); + + caughtCorrectException = false; + try + { + ToolLocationHelper.GetPlatformSDKLocation(identifier, version); + } + catch (ArgumentException) + { + caughtCorrectException = true; + } + + Assert.IsTrue(caughtCorrectException); + } + + /// + /// Verify we can get a list of extension sdks out of the API + /// + [TestMethod] + public void TestGetExtensionSDKLocations() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + // Identifier does not exist + IDictionary sdks = ToolLocationHelper.GetPlatformExtensionSDKLocations(new string[] { s_fakeStructureRoot }, null, "FOO", new Version(1, 0)); + Assert.IsTrue(sdks.Count == 0); + + // Identifier exists + sdks = ToolLocationHelper.GetPlatformExtensionSDKLocations(new string[] { s_fakeStructureRoot }, null, "MyPlatform", new Version(3, 0)); + Assert.IsTrue(sdks.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(sdks.Count == 1); + + // Targeting version higher than exists, however since we are using a russian doll model for extension sdks we will return ones in lower versions of the targeted platform. + sdks = ToolLocationHelper.GetPlatformExtensionSDKLocations(new string[] { s_fakeStructureRoot }, null, "MyPlatform", new Version(4, 0)); + Assert.IsTrue(sdks.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(sdks["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(sdks.ContainsKey("AnotherAssembly, Version=1.0")); + Assert.IsTrue(sdks["AnotherAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\4.0\\ExtensionSDKs\\AnotherAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(sdks.Count == 2); + + // Identifier exists but no extensions are in sdks this version or lower + sdks = ToolLocationHelper.GetPlatformExtensionSDKLocations(new string[] { s_fakeStructureRoot }, null, "MyPlatform", new Version(1, 0)); + Assert.IsTrue(sdks.Count == 0); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Verify we can get a single extension sdk location out of the API + /// + [TestMethod] + public void TestGetExtensionSDKLocation() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + // Identifier does not exist + IDictionary sdks = ToolLocationHelper.GetPlatformExtensionSDKLocations(new string[] { s_fakeStructureRoot }, null, "FOO", new Version(1, 0)); + Assert.IsTrue(sdks.Count == 0); + + // Identifier exists + string path = ToolLocationHelper.GetPlatformExtensionSDKLocation("MyAssembly, Version=1.0", "MyPlatform", new Version(3, 0), new string[] { s_fakeStructureRoot }, null); + Assert.IsTrue(path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + // Identifier exists in lower version + path = ToolLocationHelper.GetPlatformExtensionSDKLocation("MyAssembly, Version=1.0", "MyPlatform", new Version(4, 0), new string[] { s_fakeStructureRoot }, null); + Assert.IsTrue(path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + // Identifier does not exist + path = ToolLocationHelper.GetPlatformExtensionSDKLocation("Something, Version=1.0", "MyPlatform", new Version(4, 0), new string[] { s_fakeStructureRoot }, null); + Assert.IsTrue(path.Length == 0); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Verify we do not get any resolved paths when we pass in a root which is too long + /// + /// + [TestMethod] + [ExpectedException(typeof(PathTooLongException))] + public void ResolveFromDirectoryPathTooLong() + { + // Try a path too long, which does not exist + string tooLongPath = "C:\\" + new String('g', 1800); + List paths = new List() { tooLongPath }; + Dictionary targetPlatform = new Dictionary(); + + ToolLocationHelper.GatherSDKListFromDirectory(paths, targetPlatform); + } + + /// + /// Verify we get no resolved paths when we pass in a root with invalid chars + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ResolveFromDirectoryInvalidChar() + { + Dictionary targetPlatform = new Dictionary(); + + // Try a path with invalid chars which does not exist + string directoryWithInvalidChars = "c:\\<>?"; + List paths = new List() { directoryWithInvalidChars }; + ToolLocationHelper.GatherSDKListFromDirectory(paths, targetPlatform); + Assert.IsTrue(targetPlatform.Count == 0); + } + + /// + /// Verify we get no resolved paths when we pass in a path which does not exist. + /// + /// + [TestMethod] + public void ResolveFromDirectoryNotExist() + { + Dictionary targetPlatform = new Dictionary(); + + // Try a regular path which does not exist. + string normalDirectory = "c:\\SDKPath"; + List paths = new List() { normalDirectory }; + ToolLocationHelper.GatherSDKListFromDirectory(paths, targetPlatform); + Assert.IsTrue(targetPlatform.Count == 0); + } + + [TestMethod] + public void VerifySDKManifestWithNullOrEmptyParameter() + { + bool exceptionCaught = false; + try + { + SDKManifest manifestObject = new SDKManifest(null); + Assert.Fail(); + } + catch (ArgumentNullException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught); + + exceptionCaught = false; + try + { + SDKManifest manifestObject = new SDKManifest(""); + Assert.Fail(); + } + catch (ArgumentException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught); + } + + /// + /// Verify SDKManifest defaults values for MaxPlatformVersion, MinOSVersion, MaxOSVersion when these are not + /// present in the manifest and the SDK is a framework extension SDK + /// + [TestMethod] + public void VerifyFrameworkSdkWithOldManifest() + { + string tmpRootDirectory = Path.GetTempPath(); + string frameworkPathPattern = @"Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MyFramework"; + string frameworkPathPattern2 = @"ExtensionSDKs\MyFramework"; + + string frameworkPath = Path.Combine(tmpRootDirectory, frameworkPathPattern); + string manifestFile = Path.Combine(frameworkPath, "sdkManifest.xml"); + + string frameworkPath2 = Path.Combine(tmpRootDirectory, frameworkPathPattern2); + string manifestFile2 = Path.Combine(frameworkPath, "sdkManifest.xml"); + + try + { + Directory.CreateDirectory(frameworkPath); + Directory.CreateDirectory(frameworkPath2); + + // This is a framework SDK with specified values, no default ones are used + string manifestExtensionSDK = @" + + + + + + + + "; + + File.WriteAllText(manifestFile, manifestExtensionSDK); + SDKManifest sdkManifest = new SDKManifest(frameworkPath); + + Assert.IsTrue(sdkManifest.FrameworkIdentities != null && sdkManifest.FrameworkIdentities.Count > 0); + Assert.AreEqual(sdkManifest.MaxPlatformVersion, "9.0"); + Assert.AreEqual(sdkManifest.MinOSVersion, "6.2.3"); + Assert.AreEqual(sdkManifest.MaxOSVersionTested, "6.2.2"); + + // This is a framework SDK and the values default b/c they are not in the manifest + string manifestExtensionSDK2 = @" + + + + + + + + + "; + + File.WriteAllText(manifestFile, manifestExtensionSDK2); + SDKManifest sdkManifest2 = new SDKManifest(frameworkPath); + + Assert.IsTrue(sdkManifest.FrameworkIdentities != null && sdkManifest.FrameworkIdentities.Count > 0); + Assert.AreEqual(sdkManifest2.MaxPlatformVersion, "8.0"); + Assert.AreEqual(sdkManifest2.MinOSVersion, "6.2.1"); + Assert.AreEqual(sdkManifest2.MaxOSVersionTested, "6.2.1"); + + // This is not a framework SDK because it does not have FrameworkIdentity set + string manifestExtensionSDK3 = @" + + + + + + + + + "; + + File.WriteAllText(manifestFile, manifestExtensionSDK3); + SDKManifest sdkManifest3 = new SDKManifest(frameworkPath); + + Assert.AreEqual(sdkManifest3.FrameworkIdentity, null); + Assert.AreEqual(sdkManifest3.MaxPlatformVersion, null); + Assert.AreEqual(sdkManifest3.MinOSVersion, null); + Assert.AreEqual(sdkManifest3.MaxOSVersionTested, null); + + // This is not a framework SDK because of its location + string manifestExtensionSDK4 = @" + + + + + + "; + + File.WriteAllText(manifestFile2, manifestExtensionSDK4); + SDKManifest sdkManifest4 = new SDKManifest(frameworkPath2); + + Assert.AreEqual(sdkManifest4.FrameworkIdentity, null); + Assert.AreEqual(sdkManifest4.MaxPlatformVersion, null); + Assert.AreEqual(sdkManifest4.MinOSVersion, null); + Assert.AreEqual(sdkManifest4.MaxOSVersionTested, null); + + // This is a framework SDK with partially specified values, some default values are used + string manifestExtensionSDK5 = @" + + + + + + + + "; + + File.WriteAllText(manifestFile, manifestExtensionSDK5); + SDKManifest sdkManifest5 = new SDKManifest(frameworkPath); + + Assert.IsTrue(sdkManifest5.FrameworkIdentities != null && sdkManifest5.FrameworkIdentities.Count > 0); + Assert.AreEqual(sdkManifest5.MaxPlatformVersion, "8.0"); + Assert.AreEqual(sdkManifest5.MinOSVersion, "6.2.1"); + Assert.AreEqual(sdkManifest5.MaxOSVersionTested, "6.2.2"); + } + finally + { + Directory.Delete(frameworkPath, true /* for recursive deletion */); + Directory.Delete(frameworkPath2, true /* for recursive deletion */); + } + } + /// + /// Verify that SDKManifest properties map correctly to properties in SDKManifest.xml. + /// + [TestMethod] + public void VerifySDKManifest() + { + string manifestPath = Path.Combine(Path.GetTempPath(), "ManifestTmp"); + + try + { + Directory.CreateDirectory(manifestPath); + + string manifestFile = Path.Combine(manifestPath, "sdkManifest.xml"); + + + string manifestPlatformSDK = @" + + + + + + "; + + File.WriteAllText(manifestFile, manifestPlatformSDK); + SDKManifest sdkManifest = new SDKManifest(manifestPath); + + Assert.AreEqual(sdkManifest.AppxLocations, null); + Assert.AreEqual(sdkManifest.CopyRedistToSubDirectory, null); + Assert.AreEqual(sdkManifest.DependsOnSDK, null); + Assert.AreEqual(sdkManifest.DisplayName, "Windows"); + Assert.AreEqual(sdkManifest.FrameworkIdentities, null); + Assert.AreEqual(sdkManifest.FrameworkIdentity, null); + Assert.AreEqual(sdkManifest.MaxPlatformVersion, null); + Assert.AreEqual(sdkManifest.MinVSVersion, "11.0"); + Assert.AreEqual(sdkManifest.MinOSVersion, "6.2.1"); + Assert.AreEqual(sdkManifest.PlatformIdentity, "Windows, version=8.0"); + Assert.AreEqual(sdkManifest.ProductFamilyName, null); + Assert.AreEqual(sdkManifest.SDKType, SDKType.Unspecified); + Assert.AreEqual(sdkManifest.SupportedArchitectures, null); + Assert.AreEqual(sdkManifest.SupportPrefer32Bit, null); + Assert.AreEqual(sdkManifest.SupportsMultipleVersions, MultipleVersionSupport.Allow); + Assert.AreEqual(sdkManifest.ReadError, false); + + string manifestExtensionSDK = @" + + + + + + + + "; + + + File.WriteAllText(manifestFile, manifestExtensionSDK); + sdkManifest = new SDKManifest(manifestPath); + + Assert.IsTrue(sdkManifest.AppxLocations.ContainsKey("AppX-Debug-x86")); + Assert.IsTrue(sdkManifest.AppxLocations.ContainsKey("AppX-Debug-x64")); + Assert.IsTrue(sdkManifest.AppxLocations.ContainsKey("AppX-Debug-ARM")); + + Assert.IsTrue(sdkManifest.AppxLocations.ContainsKey("AppX-Retail-x86")); + Assert.IsTrue(sdkManifest.AppxLocations.ContainsKey("AppX-Retail-x64")); + Assert.IsTrue(sdkManifest.AppxLocations.ContainsKey("AppX-Retail-ARM")); + + Assert.AreEqual(sdkManifest.AppxLocations["AppX-Debug-x86"], ".\\AppX\\Debug\\x86\\Microsoft.MySDK.x86.Debug.1.0.appx"); + Assert.AreEqual(sdkManifest.AppxLocations["AppX-Debug-x64"], ".\\AppX\\Debug\\x64\\Microsoft.MySDK.x64.Debug.1.0.appx"); + Assert.AreEqual(sdkManifest.AppxLocations["AppX-Debug-ARM"], ".\\AppX\\Debug\\ARM\\Microsoft.MySDK.ARM.Debug.1.0.appx"); + + Assert.AreEqual(sdkManifest.AppxLocations["AppX-Retail-x86"], ".\\AppX\\Retail\\x86\\Microsoft.MySDK.x86.1.0.appx"); + Assert.AreEqual(sdkManifest.AppxLocations["AppX-Retail-x64"], ".\\AppX\\Retail\\x64\\Microsoft.MySDK.x64.1.0.appx"); + Assert.AreEqual(sdkManifest.AppxLocations["AppX-Retail-ARM"], ".\\AppX\\Retail\\ARM\\Microsoft.MySDK.ARM.1.0.appx"); + + Assert.AreEqual(sdkManifest.CopyRedistToSubDirectory, "."); + Assert.AreEqual(sdkManifest.DependsOnSDK, "SDKB, version=2.0"); + Assert.AreEqual(sdkManifest.DisplayName, "My SDK"); + + Assert.IsTrue(sdkManifest.FrameworkIdentities.ContainsKey("FrameworkIdentity-Debug")); + Assert.IsTrue(sdkManifest.FrameworkIdentities.ContainsKey("FrameworkIdentity-Retail")); + + Assert.AreEqual(sdkManifest.FrameworkIdentities["FrameworkIdentity-Debug"], "Name=MySDK.10.Debug, MinVersion=1.0.0.0"); + Assert.AreEqual(sdkManifest.FrameworkIdentities["FrameworkIdentity-Retail"], "Name=MySDK.10, MinVersion=1.0.0.0"); + + Assert.AreEqual(sdkManifest.FrameworkIdentity, null); + Assert.AreEqual(sdkManifest.MaxPlatformVersion, "8.0"); + Assert.AreEqual(sdkManifest.MinVSVersion, "11.0"); + Assert.AreEqual(sdkManifest.MinOSVersion, "6.2.1"); + Assert.AreEqual(sdkManifest.MaxOSVersionTested, "6.2.3"); + Assert.AreEqual(sdkManifest.PlatformIdentity, null); + Assert.AreEqual(sdkManifest.ProductFamilyName, "UnitTest SDKs"); + Assert.AreEqual(sdkManifest.SDKType, SDKType.Unspecified); + Assert.AreEqual(sdkManifest.SupportedArchitectures, "x86;x64;ARM"); + Assert.AreEqual(sdkManifest.SupportPrefer32Bit, "True"); + Assert.AreEqual(sdkManifest.SupportsMultipleVersions, MultipleVersionSupport.Error); + Assert.AreEqual(sdkManifest.MoreInfo, "http://msdn.microsoft.com/MySDK"); + Assert.AreEqual(sdkManifest.ReadError, false); + + File.WriteAllText(manifestFile, "Hello"); + sdkManifest = new SDKManifest(manifestPath); + + Assert.AreEqual(sdkManifest.ReadError, true); + } + finally + { + Directory.Delete(manifestPath, true /* for recursive deletion */); + } + } + + /// + /// Verify ExtensionSDK + /// + [TestMethod] + public void VerifyExtensionSDK() + { + string manifestPath = Path.Combine(Path.GetTempPath(), "ManifestTmp"); + + try + { + Directory.CreateDirectory(manifestPath); + + string manifestFile = Path.Combine(manifestPath, "sdkManifest.xml"); + + string manifestExtensionSDK = @" + + + + + + + + "; + + File.WriteAllText(manifestFile, manifestExtensionSDK); + ExtensionSDK extensionSDK = new ExtensionSDK(String.Format("CppUnitTestFramework, Version={0}", ObjectModelHelpers.MSBuildDefaultToolsVersion), manifestPath); + + Assert.AreEqual(extensionSDK.Identifier, "CppUnitTestFramework"); + Assert.AreEqual(extensionSDK.MaxPlatformVersion, new Version("8.0")); + Assert.AreEqual(extensionSDK.MinVSVersion, new Version("11.0")); + Assert.AreEqual(extensionSDK.Version, new Version(ObjectModelHelpers.MSBuildDefaultToolsVersion)); + } + finally + { + Directory.Delete(manifestPath, true /* for recursive deletion */); + } + } + + /// + /// Verify Platform SDKs are filtered correctly + /// + [TestMethod] + public void VerifyFilterPlatformSdks() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "True"); + + IList sdkList = ToolLocationHelper.GetTargetPlatformSdks(new string[] { s_fakeStructureRoot }, null); + IList filteredSdkList = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, new Version(6, 2, 5), new Version(12, 0)); + IList filteredSdkList1 = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, new Version(6, 2, 1), new Version(10, 0)); + IList filteredSdkList2 = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, new Version(6, 2, 3), new Version(10, 0)); + IList filteredSdkList3 = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, new Version(6, 2, 3), new Version(11, 0)); + + // Filter based only on OS version + IList filteredSdkList4 = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, new Version(6, 2, 3), null); + + // Filter based only on VS version + IList filteredSdkList5 = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, null, new Version(10, 0)); + + // Pass both versions as null. Don't filter anything + IList filteredSdkList6 = ToolLocationHelper.FilterTargetPlatformSdks(sdkList, null, null); + + Assert.AreEqual(sdkList.Count, 7); + Assert.AreEqual(filteredSdkList.Count, 7); + Assert.AreEqual(filteredSdkList1.Count, 2); + Assert.AreEqual(filteredSdkList2.Count, 3); + Assert.AreEqual(filteredSdkList3.Count, 4); + Assert.AreEqual(filteredSdkList4.Count, 5); + Assert.AreEqual(filteredSdkList5.Count, 5); + Assert.AreEqual(filteredSdkList6.Count, 7); + + Assert.AreEqual(filteredSdkList2[0].TargetPlatformIdentifier, "MyPlatform"); + Assert.AreEqual(filteredSdkList2[2].TargetPlatformVersion, new Version(3, 0)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Verify Extension SDKs are filtered correctly + /// + [TestMethod] + public void VerifyFilterPlatformExtensionSdks() + { + // Create fake directory tree + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "True"); + + IDictionary extensionSDKs = ToolLocationHelper.GetPlatformExtensionSDKLocations(new string[] { s_fakeStructureRoot }, null, "MyPlatform", new Version(4, 0)); + IDictionary filteredExtensionSDKs1 = ToolLocationHelper.FilterPlatformExtensionSDKs(new Version(8, 0), extensionSDKs); + IDictionary filteredExtensionSDKs2 = ToolLocationHelper.FilterPlatformExtensionSDKs(new Version(9, 0), extensionSDKs); + IDictionary filteredExtensionSDKs3 = ToolLocationHelper.FilterPlatformExtensionSDKs(new Version(10, 0), extensionSDKs); + + Assert.AreEqual(filteredExtensionSDKs1.Count, 2); + Assert.AreEqual(filteredExtensionSDKs2.Count, 1); + Assert.AreEqual(filteredExtensionSDKs3.Count, 0); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Verify that the GetPlatformExtensionSDKLocation method can be correctly called during evaluation time as a msbuild function. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyGetInstalledSDKLocations() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "VerifyGetInstalledSDKLocations"); + string platformDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\"); + string sdkDirectory = Path.Combine(platformDirectory, "ExtensionSDKs\\SDkWithManifest\\2.0\\"); + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents(@" + + + MyPlatform + 8.0 + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformExtensionSDKLocation('SDkWithManifest, Version=2.0','MyPlatform','8.0')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformExtensionSDKLocation('SDkWithManifest, Version=V2.0','MyPlatform','8.0')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation('MyPlatform','8.0')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKDisplayName('MyPlatform','8.0')) + + + + "); + + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", testDirectoryRoot); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + File.WriteAllText(Path.Combine(platformDirectory, "sdkManifest.xml"), "HI"); + File.WriteAllText(Path.Combine(sdkDirectory, "sdkManifest.xml"), "HI"); + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(testProjectFile, tempProjectContents); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + Project project = pc.LoadProject(testProjectFile); + string propertyValue1 = project.GetPropertyValue("SDKLocation1"); + string propertyValue2 = project.GetPropertyValue("SDKLocation2"); + string propertyValue3 = project.GetPropertyValue("SDKLocation3"); + string sdkName = project.GetPropertyValue("SDKName"); + + Assert.IsTrue(propertyValue1.Equals(sdkDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propertyValue2.Length == 0); + Assert.IsTrue(propertyValue3.Equals(platformDirectory, StringComparison.OrdinalIgnoreCase)); + + // No displayname set in the SDK manifest, so it mocks one up + Assert.AreEqual("MyPlatform 8.0", sdkName); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Verify that the GetPlatformExtensionSDKLocation method can be correctly called during evaluation time as a msbuild function. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyGetInstalledSDKLocations2() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "VerifyGetInstalledSDKLocations2"); + string platformDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\"); + string sdkDirectory = Path.Combine(platformDirectory, "ExtensionSDKs\\SDkWithManifest\\2.0\\"); + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents(@" + + + MyPlatform + 8.0" + + @"" + testDirectoryRoot + "" + + @"$([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformExtensionSDKLocation('SDkWithManifest, Version=2.0','MyPlatform','8.0', '$(SDKDirectoryRoot)','')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformExtensionSDKLocation('SDkWithManifest, Version=V2.0','MyPlatform','8.0', '$(SDKDirectoryRoot)','')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation('MyPlatform','8.0', '$(SDKDirectoryRoot)','')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKDisplayName('MyPlatform','8.0', '$(SDKDirectoryRoot)', '')) + + + + "); + + string platformSDKManifestContents = @" + "; + + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + File.WriteAllText(Path.Combine(platformDirectory, "sdkManifest.xml"), platformSDKManifestContents); + File.WriteAllText(Path.Combine(sdkDirectory, "sdkManifest.xml"), "HI"); + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(testProjectFile, tempProjectContents); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + Project project = pc.LoadProject(testProjectFile); + string propertyValue1 = project.GetPropertyValue("SDKLocation1"); + string propertyValue2 = project.GetPropertyValue("SDKLocation2"); + string propertyValue3 = project.GetPropertyValue("SDKLocation3"); + string sdkName = project.GetPropertyValue("SDKName"); + + Assert.IsTrue(propertyValue1.Equals(sdkDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propertyValue3.Equals(platformDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propertyValue2.Length == 0); + Assert.AreEqual("My cool platform SDK!", sdkName); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + + /// + /// Setup some fake entries in the registry and verify we get the correct sdk from there. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyGetInstalledSDKLocations3() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "VerifyGetInstalledSDKLocations3"); + string platformDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\"); + string sdkDirectory = Path.Combine(platformDirectory, "ExtensionSDKs\\SDkWithManifest\\2.0\\"); + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents(@" + + + MyPlatform + 8.0 + SOFTWARE\Microsoft\VerifyGetInstalledSDKLocations3 + Somewhere + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformExtensionSDKLocation('SDkWithManifest, Version=2.0','MyPlatform','8.0', '$(SDKDirectoryRoot)','$(SDKRegistryRoot)')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformExtensionSDKLocation('SDkWithManifest, Version=V2.0','MyPlatform','8.0', '$(SDKDirectoryRoot)','$(SDKRegistryRoot)')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation('MyPlatform','8.0', '$(SDKDirectoryRoot)','$(SDKRegistryRoot)')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKDisplayName('MyPlatform','8.0', '$(SDKDirectoryRoot)', '$(SDKRegistryRoot)')) + + + "); + + string platformSDKManifestContents = @" + + + + + "; + + string registryKey = @"SOFTWARE\Microsoft\VerifyGetInstalledSDKLocations3\"; + RegistryKey baseKey = Registry.CurrentUser; + + try + { + RegistryKey folderKey = baseKey.CreateSubKey(registryKey + @"\MyPlatform\v8.0\ExtensionSDKS\SDKWithManifest\2.0"); + folderKey.SetValue("", Path.Combine(testDirectoryRoot, sdkDirectory)); + + folderKey = baseKey.CreateSubKey(registryKey + @"\MyPlatform\v8.0"); + folderKey.SetValue("", Path.Combine(testDirectoryRoot, platformDirectory)); + + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + File.WriteAllText(Path.Combine(sdkDirectory, "sdkManifest.xml"), "HI"); + File.WriteAllText(Path.Combine(platformDirectory, "sdkManifest.xml"), platformSDKManifestContents); + + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(testProjectFile, tempProjectContents); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + Project project = pc.LoadProject(testProjectFile); + string propertyValue1 = project.GetPropertyValue("SDKLocation1"); + string propertyValue2 = project.GetPropertyValue("SDKLocation2"); + string propertyValue3 = project.GetPropertyValue("SDKLocation3"); + string sdkName = project.GetPropertyValue("SDKName"); + + Assert.IsTrue(propertyValue1.Equals(sdkDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propertyValue3.Equals(platformDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propertyValue2.Length == 0); + Assert.AreEqual("MyPlatform from the registry", sdkName); + } + finally + { + try + { + baseKey.DeleteSubKeyTree(registryKey); + } + catch (Exception) + { + } + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Verify based on a fake directory structure with some good directories and some invalid ones at each level that we + /// get the expected set out. + /// + [TestMethod] + public void ResolveSDKFromDirectory() + { + Dictionary extensionSDKs = new Dictionary(StringComparer.OrdinalIgnoreCase); + List paths = new List { s_fakeStructureRoot, s_fakeStructureRoot2 }; + Dictionary targetPlatforms = new Dictionary(); + + ToolLocationHelper.GatherSDKListFromDirectory(paths, targetPlatforms); + + TargetPlatformSDK key = new TargetPlatformSDK("Windows", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 2); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=2.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=2.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("Windows", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 2); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=3.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=3.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=4.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=4.0"].Equals(Path.Combine(s_fakeStructureRoot2, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\4.0\\"), StringComparison.OrdinalIgnoreCase)); + + // Windows kits special case is only in registry + key = new TargetPlatformSDK("MyPlatform", new Version("6.0"), null); + Assert.IsFalse(targetPlatforms.ContainsKey(key)); + + key = new TargetPlatformSDK("MyPlatform", new Version("4.0"), null); + Assert.IsTrue(targetPlatforms[key].Path == null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("AnotherAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["AnotherAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\4.0\\ExtensionSDKs\\AnotherAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("3.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\2.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 0); + + key = new TargetPlatformSDK("MyPlatform", new Version("8.0"), null); + Assert.AreEqual(Path.Combine(s_fakeStructureRoot, "MyPlatform\\8.0\\"), targetPlatforms[key].Path); + Assert.AreEqual(0, targetPlatforms[key].ExtensionSDKs.Count); + Assert.AreEqual(3, targetPlatforms[key].Platforms.Count); + Assert.IsTrue(targetPlatforms[key].ContainsPlatform("PlatformAssembly", "0.1.2.3")); + Assert.AreEqual(Path.Combine(s_fakeStructureRoot, "MyPlatform\\8.0\\Platforms\\PlatformAssembly\\0.1.2.3\\"), targetPlatforms[key].Platforms["PlatformAssembly, Version=0.1.2.3"]); + Assert.IsTrue(targetPlatforms[key].ContainsPlatform("PlatformAssembly", "1.2.3.0")); + Assert.IsTrue(targetPlatforms[key].ContainsPlatform("Sparkle", "3.3.3.3")); + + key = new TargetPlatformSDK("MyPlatform", new Version("9.0"), null); + Assert.AreEqual(Path.Combine(s_fakeStructureRoot, "MyPlatform\\9.0\\"), targetPlatforms[key].Path); + Assert.AreEqual(0, targetPlatforms[key].ExtensionSDKs.Count); + Assert.AreEqual(1, targetPlatforms[key].Platforms.Count); + Assert.IsTrue(targetPlatforms[key].ContainsPlatform("PlatformAssembly", "0.1.2.3")); + Assert.AreEqual(Path.Combine(s_fakeStructureRoot, "MyPlatform\\9.0\\Platforms\\PlatformAssembly\\0.1.2.3\\"), targetPlatforms[key].Platforms["PlatformAssembly, Version=0.1.2.3"]); + } + + /// + /// Verify based on a fake directory structure with some good directories and some invalid ones at each level that we + /// get the expected set out. + /// + [TestMethod] + public void ResolveSDKFromRegistry() + { + Dictionary targetPlatforms = new Dictionary(); + ToolLocationHelper.GatherSDKsFromRegistryImpl(targetPlatforms, "Software\\Microsoft\\MicrosoftSDks", RegistryView.Registry32, RegistryHive.CurrentUser, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, s_openBaseKey, new FileExists(File.Exists)); + ToolLocationHelper.GatherSDKsFromRegistryImpl(targetPlatforms, "Software\\Microsoft\\MicrosoftSDks", RegistryView.Registry32, RegistryHive.LocalMachine, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, s_openBaseKey, new FileExists(File.Exists)); + + TargetPlatformSDK key = new TargetPlatformSDK("Windows", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 2); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=2.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=2.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("Windows", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=3.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=3.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("5.0"), null); + Assert.IsTrue(targetPlatforms.ContainsKey(key)); + Assert.IsTrue(targetPlatforms[key].Path == null); + + key = new TargetPlatformSDK("MyPlatform", new Version("6.0"), null); + Assert.IsTrue(targetPlatforms.ContainsKey(key)); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows Kits\\6.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("4.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("9.0"), null); + Assert.AreEqual(Path.Combine(s_fakeStructureRoot, "MyPlatform\\9.0\\"), targetPlatforms[key].Path); + Assert.AreEqual(0, targetPlatforms[key].ExtensionSDKs.Count); + Assert.AreEqual(1, targetPlatforms[key].Platforms.Count); + Assert.IsTrue(targetPlatforms[key].ContainsPlatform("PlatformAssembly", "0.1.2.3")); + Assert.AreEqual(Path.Combine(s_fakeStructureRoot, "MyPlatform\\9.0\\Platforms\\PlatformAssembly\\0.1.2.3\\"), targetPlatforms[key].Platforms["PlatformAssembly, Version=0.1.2.3"]); + } + + /// + /// Verify based on a fake directory structure with some good directories and some invalid ones at each level that we + /// get the expected set out. Make sure that when we resolve from both the disk and registry that there are no duplicates + /// and make sure we get the expected results. + /// + [TestMethod] + public void ResolveSDKFromRegistryAndDisk() + { + Dictionary targetPlatforms = new Dictionary(); + + List paths = new List() { s_fakeStructureRoot }; + + ToolLocationHelper.GatherSDKListFromDirectory(paths, targetPlatforms); + ToolLocationHelper.GatherSDKsFromRegistryImpl(targetPlatforms, "Software\\Microsoft\\MicrosoftSDks", RegistryView.Registry32, RegistryHive.CurrentUser, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, s_openBaseKey, new FileExists(File.Exists)); + ToolLocationHelper.GatherSDKsFromRegistryImpl(targetPlatforms, "Software\\Microsoft\\MicrosoftSDks", RegistryView.Registry32, RegistryHive.LocalMachine, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, s_openBaseKey, new FileExists(File.Exists)); + + TargetPlatformSDK key = new TargetPlatformSDK("Windows", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 2); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=2.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=2.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("Windows", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=3.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=3.0"].Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("6.0"), null); + Assert.IsTrue(targetPlatforms.ContainsKey(key)); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows Kits\\6.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("5.0"), null); + Assert.IsTrue(targetPlatforms.ContainsKey(key)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 0); + Assert.IsTrue(targetPlatforms[key].Path == null); + + key = new TargetPlatformSDK("MyPlatform", new Version("4.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 2); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("AnotherAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["AnotherAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\4.0\\ExtensionSDKs\\AnotherAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("3.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 1); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\2.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetPlatforms[key].ExtensionSDKs.Count == 0); + } + + /// + /// Make sure if the sdk identifier is null we get an ArgumentNullException because without specifying the + /// sdk identifier we can't get any platforms back. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void GetPlatformsForSDKNullSDKIdentifier() + { + ToolLocationHelper.GetPlatformsForSDK(null, new Version("1.0")); + } + + /// + /// Make sure if the sdk version is null we get an ArgumentNullException because without specifying the + /// sdk version we can't get any platforms back. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void GetPlatformsForSDKNullSDKVersion() + { + ToolLocationHelper.GetPlatformsForSDK("AnySDK", null); + } + + /// + /// Verify that when there are no sdks with target platforms installed, our list of platforms is empty + /// to make sure we are not getting platforms from somewhere else. + /// + [TestMethod] + public void GetPlatformsForSDKWithNoInstalledTargetPlatforms() + { + IEnumerable platforms = ToolLocationHelper.GetPlatformsForSDK("AnySDK", new Version("1.0"), new string[0], ""); + Assert.AreEqual(false, platforms.Any()); + } + + /// + /// Verify that the list of platforms returned is exactly as we expect when we have platforms + /// installed and we pass in a matching sdk identifier and version number for one of the + /// installed platforms. + /// + [TestMethod] + public void GetPlatformsForSDKWithMatchingInstalledTargetPlatforms() + { + IEnumerable myPlatforms = ToolLocationHelper.GetPlatformsForSDK("MyPlatform", new Version("8.0"), new string[] { s_fakeStructureRoot }, null); + Assert.IsTrue(myPlatforms.Contains("Sparkle, Version=3.3.3.3")); + Assert.IsTrue(myPlatforms.Contains("PlatformAssembly, Version=0.1.2.3")); + Assert.IsTrue(myPlatforms.Contains("PlatformAssembly, Version=1.2.3.0")); + Assert.AreEqual(3, myPlatforms.Count()); + } + + /// + /// Verify that the list of platforms is empty if we ask for an sdk that is not installed. + /// + [TestMethod] + public void GetPlatformsForSDKWithInstalledTargetPlatformsNoMatch() + { + IEnumerable platforms = ToolLocationHelper.GetPlatformsForSDK("DoesNotExistPlatform", new Version("0.0.0.0"), new string[] { s_fakeStructureRoot }, null); + Assert.AreEqual(false, platforms.Any()); + } + + /// + /// Verify that the list of platforms is empty if we ask for a valid sdk identifier but + /// a version number that isn't installed. + /// + [TestMethod] + public void GetPlatformsForSDKWithMatchingPlatformNotMatchingVersion() + { + IEnumerable platforms = ToolLocationHelper.GetPlatformsForSDK("MyPlatform", new Version("0.0.0.0"), new string[] { s_fakeStructureRoot }, null); + Assert.AreEqual(false, platforms.Any()); + } + + /// + /// Verify that if we pass in an sdk identifier and version for an installed legacy platform sdk + /// that the list of platforms is empty because it has no platforms. + /// + [TestMethod] + public void GetPlatformsForSDKForLegacyPlatformSDK() + { + IEnumerable platforms = ToolLocationHelper.GetPlatformsForSDK("Windows", new Version("8.0"), new string[] { s_fakeStructureRoot }, null); + Assert.AreEqual(false, platforms.Any()); + } + + /// + /// Verify based on a fake directory structure with some good directories and some invalid ones at each level that we + /// get the expected set out. Make sure that when we resolve from both the disk and registry that there are no duplicates + /// and make sure we get the expected results. + /// + [TestMethod] + public void GetALLTargetPlatformSDKs() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + var sdks = ToolLocationHelper.GetTargetPlatformSdks(new string[] { s_fakeStructureRoot }, null); + + Dictionary targetPlatforms = new Dictionary(); + foreach (TargetPlatformSDK sdk in sdks) + { + targetPlatforms.Add(sdk, sdk); + } + + TargetPlatformSDK key = new TargetPlatformSDK("Windows", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("Windows", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("3.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("2.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("1.0"), null); + Assert.IsTrue(targetPlatforms[key].Path.Equals(Path.Combine(s_fakeStructureRoot, "MyPlatform\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + + key = new TargetPlatformSDK("MyPlatform", new Version("5.0"), null); + Assert.IsTrue(!targetPlatforms.ContainsKey(key)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Verify that the GetPlatformSDKPropsFileLocation method can be correctly called for pre-OneCore SDKs during evaluation time as a msbuild function. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyGetPreOneCoreSDKPropsLocation() + { + // This is the mockup layout for SDKs before One Core SDK. + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "VerifyGetPreOneCoreSDKPropsLocation"); + string platformDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\"); + string propsDirectory = Path.Combine(platformDirectory, "DesignTime\\CommonConfiguration\\Neutral"); + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents(@" + + + MyPlatform + 8.0 + SOFTWARE\Microsoft\VerifyGetPlatformSDKPropsLocation + Somewhere + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation('MyPlatform', '8.0', '$(SDKDirectoryRoot)', '$(SDKRegistryRoot)')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKPropsFileLocation('',' ','MyPlatform',' ','8.0', '$(SDKDirectoryRoot)', '$(SDKRegistryRoot)')) + + + "); + + string registryKey = @"SOFTWARE\Microsoft\VerifyGetPlatformSDKPropsLocation\"; + RegistryKey baseKey = Registry.CurrentUser; + + try + { + using (RegistryKey platformKey = baseKey.CreateSubKey(registryKey + @"\MyPlatform\v8.0")) + { + platformKey.SetValue("InstallationFolder", platformDirectory); + } + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(propsDirectory); + + File.WriteAllText(Path.Combine(platformDirectory, "sdkManifest.xml"), "Test"); + + Project project = ObjectModelHelpers.CreateInMemoryProject(new ProjectCollection(), tempProjectContents, null); + + string propertyValue = project.GetPropertyValue("PlatformSDKLocation"); + string propsLocation = project.GetPropertyValue("PropsLocation"); + + Assert.IsTrue(propertyValue.Equals(platformDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propsLocation.Equals(propsDirectory, StringComparison.OrdinalIgnoreCase)); + } + finally + { + try + { + baseKey.DeleteSubKeyTree(registryKey); + } + + catch (Exception) + { + } + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Verify that the GetPlatformSDKPropsFileLocation method can be correctly called for OneCore SDK during evaluation time as a msbuild function. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyGetOneCoreSDKPropsLocation() + { + // This is the mockup layout for One Core SDK. + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "VerifyGetOneCoreSDKPropsLocation"); + string platformDirectory = Path.Combine(testDirectoryRoot, "OneCoreSDK\\1.0\\"); + string propsDirectory = Path.Combine(platformDirectory, "DesignTime\\CommonConfiguration\\Neutral\\MyPlatform\\0.8.0.0"); + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents(@" + + + MyPlatform + 8.0 + SOFTWARE\Microsoft\VerifyGetOneCoreSDKPropsLocation + Somewhere + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation('OneCoreSDK', '1.0', '', '$(SDKRegistryRoot)')) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKPropsFileLocation('OneCoreSDK','1.0','MyPlatform',' ','0.8.0.0', '', '$(SDKRegistryRoot)')) + + + "); + + string registryKey = @"SOFTWARE\Microsoft\VerifyGetOneCoreSDKPropsLocation\"; + RegistryKey baseKey = Registry.CurrentUser; + + try + { + using (RegistryKey platformKey = baseKey.CreateSubKey(registryKey + @"\OneCoreSDK\1.0")) + { + platformKey.SetValue("InstallationFolder", platformDirectory); + } + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(propsDirectory); + + File.WriteAllText(Path.Combine(platformDirectory, "sdkManifest.xml"), "Test"); + + Project project = ObjectModelHelpers.CreateInMemoryProject(new ProjectCollection(), tempProjectContents, null); + + string propertyValue = project.GetPropertyValue("PlatformSDKLocation"); + string propsLocation = project.GetPropertyValue("PropsLocation"); + + Assert.IsTrue(propertyValue.Equals(platformDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(propsLocation.Equals(propsDirectory, StringComparison.OrdinalIgnoreCase)); + } + finally + { + try + { + baseKey.DeleteSubKeyTree(registryKey); + } + + catch (Exception) + { + } + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Make a fake SDK structure on disk for testing. + /// + private static string MakeFakeSDKStructure() + { + string manifestPlatformSDK1 = @" + + + + + + "; + + string manifestPlatformSDK2 = @" + + + + + + "; + + string manifestPlatformSDK3 = @" + + + + + + "; + + string manifestPlatformSDK4 = @" + + + + + + "; + + string manifestPlatformSDK5 = @" + + + + + + "; + + string manifestPlatformSDK6 = @" + + + + + + "; + + string manifestPlatformSDK7 = @" + + + + + + "; + + string manifestExtensionSDK1 = @" + + + + + "; + + string manifestExtensionSDK2 = @" + + + + + "; + + string tempPath = Path.Combine(Path.GetTempPath(), "FakeSDKDirectory"); + try + { + // Good + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\ExtensionSDKs\\MyAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "WindowsKits\\6.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\5.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\4.0\\ExtensionSDKs\\AnotherAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\2.0\\ExtensionSDKs\\MyAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\8.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\8.0\\Platforms\\PlatformAssembly\\0.1.2.3")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\8.0\\Platforms\\PlatformAssembly\\1.2.3.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\8.0\\Platforms\\Sparkle\\3.3.3.3")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\9.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\9.0\\Platforms\\PlatformAssembly\\0.1.2.3")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\9.0\\PlatformAssembly\\Sparkle")); + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\9.0\\Platforms\\PlatformAssembly\\Sparkle")); + + File.WriteAllText(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0", "sdkmanifest.xml"), "Hello"); + + File.WriteAllText(Path.Combine(tempPath, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\ExtensionSDKs\\MyAssembly\\1.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\1.0", "sdkmanifest.xml"), manifestPlatformSDK1); + File.WriteAllText(Path.Combine(tempPath, "Windows\\2.0", "sdkmanifest.xml"), manifestPlatformSDK2); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\4.0\\ExtensionSDKs\\AnotherAssembly\\1.0", "sdkmanifest.xml"), manifestExtensionSDK2); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\3.0", "sdkmanifest.xml"), manifestPlatformSDK3); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\2.0", "sdkmanifest.xml"), manifestPlatformSDK4); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\3.0\\ExtensionSDKs\\MyAssembly\\1.0", "sdkmanifest.xml"), manifestExtensionSDK1); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\2.0\\ExtensionSDKs\\MyAssembly\\1.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\1.0", "sdkmanifest.xml"), manifestPlatformSDK5); + + // Contains a couple of sub-platforms + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\8.0", "sdkmanifest.xml"), manifestPlatformSDK6); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\8.0\\Platforms\\PlatformAssembly\\0.1.2.3", "Platform.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\8.0\\Platforms\\PlatformAssembly\\1.2.3.0", "Platform.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\8.0\\Platforms\\Sparkle\\3.3.3.3", "Platform.xml"), "Hello"); + + // Contains invalid sub-platforms as well as valid ones + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\9.0", "sdkmanifest.xml"), manifestPlatformSDK7); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\9.0\\Platforms\\PlatformAssembly\\0.1.2.3", "Platform.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\9.0\\PlatformAssembly\\Sparkle", "Platform.xml"), "Hello"); // not under the Platforms directory + File.WriteAllText(Path.Combine(tempPath, "MyPlatform\\9.0\\Platforms\\PlatformAssembly\\Sparkle", "Platform.xml"), "Hello"); // bad version + Directory.CreateDirectory(Path.Combine(tempPath, "MyPlatform\\9.0\\Platforms\\Sparkle\\3.3.3.3")); // no platform.xml + + //Bad because of v in the sdk version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\AnotherAssembly\\v1.1")); + + //Bad because no extensionsdks directory under the platform version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v3.0\\")); + + // Bad because the directory under the identifier is not a version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\NotAVersion\\")); + + // Bad because the directory under the identifier is not a version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\NotAVersion\\ExtensionSDKs\\Assembly\\1.0")); + } + catch (Exception) + { + FileUtilities.DeleteDirectoryNoThrow(tempPath, true); + return null; + } + + return tempPath; + } + + /// + /// Make a fake SDK structure on disk for testing. + /// + private static string MakeFakeSDKStructure2() + { + string tempPath = Path.Combine(Path.GetTempPath(), "FakeSDKDirectory2"); + try + { + // Good + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\4.0")); + + File.WriteAllText(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\4.0", "sdkmanifest.xml"), "Hello"); + } + catch (Exception) + { + FileUtilities.DeleteDirectoryNoThrow(tempPath, true); + return null; + } + + return tempPath; + } + #endregion + + #region HelperMethods + + /// + /// Simplified registry access delegate. Given a baseKey and a subKey, get all of the subkey + /// names. + /// + /// The base registry key. + /// The subkey + /// An enumeration of strings. + private static IEnumerable GetRegistrySubKeyNames(RegistryKey baseKey, string subKey) + { + if (baseKey == Registry.CurrentUser) + { + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Windows", "MyPlatform" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "v1.0", "1.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v1.0\ExtensionSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "MyAssembly" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\1.0\ExtensionSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "MyAssembly" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v1.0\ExtensionSDKs\MyAssembly", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "v1.1", "1.0", "2.0", "3.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\1.0\ExtensionSDKs\MyAssembly", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "2.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "4.0", "5.0", "6.0", "9.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\4.0\ExtensionSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "MyAssembly" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\5.0\ExtensionSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { String.Empty }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\4.0\ExtensionSDKs\MyAssembly", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "1.0" }; + } + } + + if (baseKey == Registry.LocalMachine) + { + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Windows" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "v2.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v2.0\ExtensionSDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "MyAssembly" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v2.0\ExtensionSDKs\MyAssembly", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "3.0" }; + } + } + + return new string[] { }; + } + + /// + /// Simplified registry access delegate. Given a baseKey and subKey, get the default value + /// of the subKey. + /// + /// The base registry key. + /// The subkey + /// A string containing the default value. + private string GetRegistrySubKeyDefaultValue(RegistryKey baseKey, string subKey) + { + if (baseKey == Registry.CurrentUser) + { + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v1.0\ExtensionSDKs\MyAssembly\1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0"); + } + // This has a v in the sdk version and should not be found but we need a real path incase it is so it will show up in the returned list and fail the test. + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v1.0\ExtensionSDKs\MyAssembly\v1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\1.0\ExtensionSDKs\MyAssembly\2.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0"); + } + + // This has a set of bad char in the returned directory so it should not be allowed. + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v1.0\ExtensionSDKs\MyAssembly\3.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return s_fakeStructureRoot + @"\Windows\1.0\ExtensionSDKs\MyAssembly\<>?/"; + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\5.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "MyPlatform\\5.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\4.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\6.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows Kits\\6.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\9.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "MyPlatform\\9.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\MyPlatform\4.0\ExtensionSDKs\MyAssembly\1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "SomeOtherPlace\\MyPlatformOtherLocation\\4.0\\ExtensionSDKs\\MyAssembly\\1.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows\\1.0"); + } + } + + if (baseKey == Registry.LocalMachine) + { + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v2.0\ExtensionSDKs\MyAssembly\3.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0"); + } + + if (String.Compare(subKey, @"Software\Microsoft\MicrosoftSDKs\Windows\v2.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return Path.Combine(s_fakeStructureRoot, "Windows\\2.0"); + } + } + + return null; + } + + /// + /// Registry access delegate. Given a hive and a view, return the registry base key. + /// + private static RegistryKey GetBaseKey(RegistryHive hive, RegistryView view) + { + if (hive == RegistryHive.CurrentUser) + { + return Registry.CurrentUser; + } + else if (hive == RegistryHive.LocalMachine) + { + return Registry.LocalMachine; + } + + return null; + } + + #endregion + } +} diff --git a/src/Utilities/UnitTests/ToolTask_Tests.cs b/src/Utilities/UnitTests/ToolTask_Tests.cs new file mode 100644 index 00000000000..60c4c94c3c6 --- /dev/null +++ b/src/Utilities/UnitTests/ToolTask_Tests.cs @@ -0,0 +1,592 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Diagnostics; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ToolTask_Tests + { + internal class MyTool : ToolTask, IDisposable + { + private string _fullToolName; + private string _responseFileCommands = String.Empty; + private string _commandLineCommands = String.Empty; + private string _pathToToolUsed; + + public MyTool() + : base() + { + _fullToolName = + Path.Combine + ( + Environment.GetFolderPath(Environment.SpecialFolder.System), + "cmd.exe" + ); + } + + public void Dispose() + { + } + + public string PathToToolUsed + { + get { return _pathToToolUsed; } + } + + public string MockResponseFileCommands + { + set { _responseFileCommands = value; } + } + + public string MockCommandLineCommands + { + set { _commandLineCommands = value; } + } + + public string FullToolName + { + set { _fullToolName = value; } + } + + /// + /// Intercepted start info + /// + internal ProcessStartInfo StartInfo + { + get; + private set; + } + + /// + /// Whether execute was called + /// + internal bool ExecuteCalled + { + get; + private set; + } + + protected override string ToolName + { + get { return Path.GetFileName(_fullToolName); } + } + + protected override string GenerateFullPathToTool() + { + return _fullToolName; + } + + override protected string GenerateResponseFileCommands() + { + return _responseFileCommands; + } + + override protected string GenerateCommandLineCommands() + { + // Default is nothing. This is useful for tools where all the parameters can go into a response file. + return _commandLineCommands; + } + + override protected void LogEventsFromTextOutput + ( + string singleLine, + MessageImportance messageImportance + ) + { + if (singleLine.Contains("BADTHINGHAPPENED")) + { + // This is where a customer's tool task implementation could do its own + // parsing of the errors in the stdout/stderr output of the tool being wrapped. + Log.LogError(singleLine); + } + else + { + base.LogEventsFromTextOutput(singleLine, messageImportance); + } + } + + override protected int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands) + { + Console.WriteLine("executetool"); + _pathToToolUsed = pathToTool; + ExecuteCalled = true; + int result = base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands); + StartInfo = base.GetProcessStartInfo(GenerateFullPathToTool(), "/x", null); + return result; + } + }; + + [TestMethod] + public void Regress_Mutation_UserSuppliedToolPathIsLogged() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.ToolPath = @"C:\MyAlternatePath"; + + t.Execute(); + + // The alternate path should be mentioned in the log. + engine.AssertLogContains("MyAlternatePath"); + } + } + + [TestMethod] + public void Regress_Mutation_MissingExecutableIsLogged() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.ToolPath = @"C:\MyAlternatePath"; + + Assert.IsFalse(t.Execute()); + + // There should be an error about invalid task location. + engine.AssertLogContains("MSB6004"); + } + } + + [TestMethod] + public void Regress_Mutation_WarnIfCommandLineTooLong() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + // "cmd.exe" croaks big-time when given a very long command-line. It pops up a message box on + // Windows XP. We can't have that! So use "attrib.exe" for this exercise instead. + t.FullToolName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "attrib.exe"); + + t.MockCommandLineCommands = new String('x', 32001); + + // It's only a warning, we still succeed + Assert.IsTrue(t.Execute()); + Assert.AreEqual(0, t.ExitCode); + // There should be a warning about the command-line being too long. + engine.AssertLogContains("MSB6002"); + } + } + + /// + /// Exercise the code in ToolTask's default implementation of HandleExecutionErrors. + /// + [TestMethod] + public void HandleExecutionErrorsWhenToolDoesntLogError() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.MockCommandLineCommands = "/C garbagegarbagegarbagegarbage.exe"; + + Assert.IsFalse(t.Execute()); + Assert.AreEqual(1, t.ExitCode); // cmd.exe error code is 1 + + // We just tried to run "cmd.exe /C garbagegarbagegarbagegarbage.exe". This should fail, + // but since "cmd.exe" doesn't log its errors in canonical format, no errors got + // logged by the tool itself. Therefore, ToolTask's default implementation of + // HandleTaskExecutionErrors should have logged error MSB6006. + engine.AssertLogContains("MSB6006"); + } + } + + /// + /// Exercise the code in ToolTask's default implementation of HandleExecutionErrors. + /// + [TestMethod] + public void HandleExecutionErrorsWhenToolLogsError() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.MockCommandLineCommands = "/C echo Main.cs(17,20): error CS0168: The variable 'foo' is declared but never used"; + + Assert.IsFalse(t.Execute()); + + // The above command logged a canonical error message. Therefore ToolTask should + // not log its own error beyond that. + engine.AssertLogDoesntContain("MSB6006"); + engine.AssertLogContains("CS0168"); + engine.AssertLogContains("The variable 'foo' is declared but never used"); + Assert.AreEqual(-1, t.ExitCode); + Assert.AreEqual(1, engine.Errors); + } + } + + /// + /// ToolTask should never run String.Format on strings that are + /// not meant to be formatted. + /// + [TestMethod] + public void DoNotFormatTaskCommandOrMessage() + { + MyTool t = new MyTool(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.MockCommandLineCommands = "/C echo hello world {"; // Unmatched curly would crash if they did + t.Execute(); + engine.AssertLogContains("echo hello world {"); + Assert.AreEqual(0, engine.Errors); + } + + /// + /// When a message is logged to the standard error stream do not error is LogStandardErrorAsError is not true or set. + /// + [TestMethod] + public void DoNotErrorWhenTextSentToStandardError() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.MockCommandLineCommands = "/C Echo 'Who made you king anyways' 1>&2"; + + Assert.IsTrue(t.Execute()); + + engine.AssertLogDoesntContain("MSB"); + engine.AssertLogContains("Who made you king anyways"); + Assert.AreEqual(0, t.ExitCode); + Assert.AreEqual(0, engine.Errors); + } + } + + /// + /// When a message is logged to the standard output stream do not error is LogStandardErrorAsError is true + /// + [TestMethod] + public void DoNotErrorWhenTextSentToStandardOutput() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.LogStandardErrorAsError = true; + t.MockCommandLineCommands = "/C Echo 'Who made you king anyways'"; + + Assert.IsTrue(t.Execute()); + + engine.AssertLogDoesntContain("MSB"); + engine.AssertLogContains("Who made you king anyways"); + Assert.AreEqual(0, t.ExitCode); + Assert.AreEqual(0, engine.Errors); + } + } + + /// + /// When a message is logged to the standard error stream error if LogStandardErrorAsError is true + /// + [TestMethod] + public void ErrorWhenTextSentToStandardError() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.LogStandardErrorAsError = true; + t.MockCommandLineCommands = "/C Echo 'Who made you king anyways' 1>&2"; + + Assert.IsFalse(t.Execute()); + + engine.AssertLogDoesntContain("MSB3073"); + engine.AssertLogContains("Who made you king anyways"); + Assert.AreEqual(-1, t.ExitCode); + Assert.AreEqual(1, engine.Errors); + } + } + + + /// + /// When ToolExe is set, it is used instead of ToolName + /// + [TestMethod] + public void ToolExeWinsOverToolName() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FullToolName = "c:\\baz\\foo.exe"; + + Assert.AreEqual("foo.exe", t.ToolExe); + t.ToolExe = "bar.exe"; + Assert.AreEqual("bar.exe", t.ToolExe); + } + } + + /// + /// When ToolExe is set, it is appended to ToolPath instead + /// of the regular tool name + /// + [TestMethod] + public void ToolExeIsFoundOnToolPath() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FullToolName = "cmd.exe"; + string systemPath = Environment.GetFolderPath(Environment.SpecialFolder.System); + t.ToolPath = systemPath; + + t.Execute(); + Assert.AreEqual(Path.Combine(systemPath, "cmd.exe"), t.PathToToolUsed); + engine.AssertLogContains("cmd.exe"); + engine.Log = String.Empty; + + t.ToolExe = "xcopy.exe"; + t.Execute(); + Assert.AreEqual(Path.Combine(systemPath, "xcopy.exe"), t.PathToToolUsed); + engine.AssertLogContains("xcopy.exe"); + engine.AssertLogDoesntContain("cmd.exe"); + } + } + + /// + /// Task is not found on path - regress #499196 + /// + [TestMethod] + public void TaskNotFoundOnPath() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FullToolName = "doesnotexist.exe"; + + Assert.IsFalse(t.Execute()); + Assert.AreEqual(-1, t.ExitCode); + Assert.AreEqual(1, engine.Errors); + + // Does not throw an exception + } + } + + /// + /// Task is found on path. + /// + [TestMethod] + public void TaskFoundOnPath() + { + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.FullToolName = "cmd.exe"; + + Assert.IsTrue(t.Execute()); + Assert.AreEqual(0, t.ExitCode); + Assert.AreEqual(0, engine.Errors); + + engine.AssertLogContains(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe")); + } + } + + /// + /// StandardOutputImportance set to Low should now show up in our log + /// + [TestMethod] + public void OverrideStdOutImportanceToLow() + { + string tempFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(tempFile, @"hello world"); + + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + engine.MinimumMessageImportance = MessageImportance.High; + + t.BuildEngine = engine; + t.FullToolName = "find.exe"; + t.MockCommandLineCommands = "\"hello\" \"" + tempFile + "\""; + t.StandardOutputImportance = "Low"; + + Assert.IsTrue(t.Execute()); + Assert.AreEqual(0, t.ExitCode); + Assert.AreEqual(0, engine.Errors); + + engine.AssertLogDoesntContain("hello world"); + } + File.Delete(tempFile); + } + + /// + /// StandardOutputImportance set to Low should now show up in our log + /// + [TestMethod] + public void OverrideStdOutImportanceToHigh() + { + string tempFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(tempFile, @"hello world"); + + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + engine.MinimumMessageImportance = MessageImportance.High; + + t.BuildEngine = engine; + t.FullToolName = "find.exe"; + t.MockCommandLineCommands = "\"hello\" \"" + tempFile + "\""; + t.StandardOutputImportance = "High"; + + Assert.IsTrue(t.Execute()); + Assert.AreEqual(0, t.ExitCode); + Assert.AreEqual(0, engine.Errors); + + engine.AssertLogContains("hello world"); + } + File.Delete(tempFile); + } + + /// + /// This is to ensure that somebody could write a task that implements ToolTask, + /// wraps some .EXE tool, and still have the ability to parse the stdout/stderr + /// himself. This is so that in case the tool doesn't log its errors in canonical + /// format, the task can still opt to do something reasonable with it. + /// + [TestMethod] + public void ToolTaskCanChangeCanonicalErrorFormat() + { + string tempFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(tempFile, @" + Main.cs(17,20): warning CS0168: The variable 'foo' is declared but never used. + BADTHINGHAPPENED: This is my custom error format that's not in canonical error format. + "); + + using (MyTool t = new MyTool()) + { + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + // The command we're giving is the command to spew the contents of the temp + // file we created above. + t.MockCommandLineCommands = "/C type \"" + tempFile + "\""; + + t.Execute(); + + // The above command logged a canonical warning, as well as a custom error. + engine.AssertLogContains("CS0168"); + engine.AssertLogContains("The variable 'foo' is declared but never used"); + engine.AssertLogContains("BADTHINGHAPPENED"); + engine.AssertLogContains("This is my custom error format"); + + Assert.AreEqual(1, engine.Warnings, "Expected one warning in log."); + Assert.AreEqual(1, engine.Errors, "Expected one error in log."); + } + + File.Delete(tempFile); + } + + /// + /// Passing env vars through the tooltask public property + /// + [TestMethod] + public void EnvironmentVariablesToToolTask() + { + MyTool task = new MyTool(); + task.BuildEngine = new MockEngine(); + task.EnvironmentVariables = new string[] { "a=b", "c=d", "username=x" /* built-in */, "path=" /* blank value */}; + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, task.ExecuteCalled); + + ProcessStartInfo startInfo = task.StartInfo; + + Assert.AreEqual("b", startInfo.EnvironmentVariables["a"]); + Assert.AreEqual("d", startInfo.EnvironmentVariables["c"]); + Assert.AreEqual("x", startInfo.EnvironmentVariables["username"]); + Assert.AreEqual(String.Empty, startInfo.EnvironmentVariables["path"]); + Assert.IsTrue(String.Equals(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), startInfo.EnvironmentVariables["programfiles"], StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Equals sign in value + /// + [TestMethod] + public void EnvironmentVariablesToToolTaskEqualsSign() + { + MyTool task = new MyTool(); + task.BuildEngine = new MockEngine(); + task.EnvironmentVariables = new string[] { "a=b=c" }; + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual("b=c", task.StartInfo.EnvironmentVariables["a"]); + } + + /// + /// No value provided + /// + [TestMethod] + public void EnvironmentVariablesToToolTaskInvalid1() + { + MyTool task = new MyTool(); + task.BuildEngine = new MockEngine(); + task.EnvironmentVariables = new string[] { "x" }; + bool result = task.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(false, task.ExecuteCalled); + } + + /// + /// Empty string provided + /// + [TestMethod] + public void EnvironmentVariablesToToolTaskInvalid2() + { + MyTool task = new MyTool(); + task.BuildEngine = new MockEngine(); + task.EnvironmentVariables = new string[] { "" }; + bool result = task.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(false, task.ExecuteCalled); + } + + /// + /// Empty name part provided + /// + [TestMethod] + public void EnvironmentVariablesToToolTaskInvalid3() + { + MyTool task = new MyTool(); + task.BuildEngine = new MockEngine(); + task.EnvironmentVariables = new string[] { "=a;b=c" }; + bool result = task.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(false, task.ExecuteCalled); + } + + /// + /// Not set should not wipe out other env vars + /// + [TestMethod] + public void EnvironmentVariablesToToolTaskNotSet() + { + MyTool task = new MyTool(); + task.BuildEngine = new MockEngine(); + task.EnvironmentVariables = null; + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, task.ExecuteCalled); + Assert.AreEqual(true, task.StartInfo.EnvironmentVariables["username"].Length > 0); + } + } +} diff --git a/src/Utilities/UnitTests/TrackedDependencies/FileTrackerTests.cs b/src/Utilities/UnitTests/TrackedDependencies/FileTrackerTests.cs new file mode 100644 index 00000000000..e90dfa64596 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/FileTrackerTests.cs @@ -0,0 +1,2626 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; +using System.Collections; +using System.Diagnostics; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; +using System.Linq; + +using BackEndNativeMethods = Microsoft.Build.BackEnd.NativeMethods; +using ObjectModelHelpers = Microsoft.Build.UnitTests.ObjectModelHelpers; + +// PLEASE NOTE: This is a UNICODE file as it contains UNICODE characters! + +namespace Microsoft.Build.UnitTests.FileTracking +{ + [TestClass] + sealed public class FileTrackerTests + { + private static string s_defaultFileTrackerPathUnquoted; + private static string s_defaultFileTrackerPath; + private static string s_defaultTrackerPath; + + private static string s_oldPath = null; + + [ClassInitialize] + public static void ClassSetup(TestContext testContext) + { + s_defaultFileTrackerPathUnquoted = null;//FileTracker.GetFileTrackerPath(ExecutableType.SameAsCurrentProcess); + s_defaultFileTrackerPath = null; //"\"" + defaultFileTrackerPathUnquoted + "\""; + s_defaultTrackerPath = null;//FileTracker.GetTrackerPath(ExecutableType.SameAsCurrentProcess); + } + + [TestInitialize] + public void Setup() + { + // blank out the path so that we know we're not inadvertently depending on it. + s_oldPath = Environment.GetEnvironmentVariable("PATH"); + Environment.SetEnvironmentVariable("PATH", Environment.ExpandEnvironmentVariables("%windir%\\system32;%windir%")); + + // Call StopTrackingAndCleanup here, just in case one of the unit tests failed before it called it + // In real code StopTrackingAndCleanup(); would always be in a finally {} block. + FileTracker.StopTrackingAndCleanup(); + FileTrackerTestHelper.CleanTlogs(); + FileTracker.SetThreadCount(1); + } + + [TestCleanup] + public void CleanUp() + { + // Reset PATH to its original value. + if (s_oldPath != null) + { + Environment.SetEnvironmentVariable("PATH", s_oldPath); + s_oldPath = null; + } + + FileTrackerTestHelper.CleanTlogs(); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerHelp() + { + Console.WriteLine("Test: FileTracker"); + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, ""); + + Assert.AreEqual(1, exit); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerBadArg() + { + Console.WriteLine("Test: FileTrackerBadArg"); + + string log; + int exit = FileTrackerTestHelper.RunCommandWithLog(s_defaultTrackerPath, "/q", out log); + + Assert.AreEqual(1, exit); + Assert.IsTrue(log.Contains("TRK0000")); // bad arg + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerNoUIDll() + { + Console.WriteLine("Test: FileTrackerNoUIDll"); + string testDirectory = Path.Combine(Environment.CurrentDirectory, "FileTrackerNoUIDll"); + string testTrackerPath = Path.Combine(testDirectory, Path.GetFileName(s_defaultTrackerPath)); + + try + { + if (Directory.Exists(testDirectory)) + { + ObjectModelHelpers.DeleteDirectory(testDirectory); + Directory.Delete(testDirectory, true); + } + + // create an empty directory and copy Tracker.exe -- BUT NOT TrackerUI.dll -- to + // that directory. + Directory.CreateDirectory(testDirectory); + File.Copy(s_defaultTrackerPath, testTrackerPath); + + string log; + int exit = FileTrackerTestHelper.RunCommandWithLog(testTrackerPath, "/?", out log); + + Assert.AreEqual(9, exit); + // It's OK to look for the English message since that's all we're capable of printing when we can't find + // our resource dll. + Assert.IsTrue(log.Contains("FileTracker : ERROR : Could not load UI satellite dll 'TrackerUI.dll'")); + } + finally + { + // Doesn't delete the directory itself, but deletes its contents. If you try to delete the directory, + // even after calling this method, it sometimes throws IO exceptions due to not recognizing that the + // contents have been deleted yet. + ObjectModelHelpers.DeleteDirectory(testDirectory); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerNonexistentRspFile() + { + Console.WriteLine("Test: FileTrackerNonexistentRspFile"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + string log; + int exit = FileTrackerTestHelper.RunCommandWithLog(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " @abc.rsp /c findstr /ip foo test.in", out log); + Console.WriteLine(""); + + // missing rsp file is a non-fatal error + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + + // but it should still be reported + Assert.IsTrue(log.Contains("Tracker.exe:")); + Assert.IsTrue(log.Contains("abc.rsp")); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerWithDll() + { + Console.WriteLine("Test: FileTrackerWithDll"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath); + + Assert.AreEqual(1, exit); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerReadOnlyTlog() + { + Console.WriteLine("Test: FileTrackerTlogWriteFailure"); + string tlog = "findstr.read.1.tlog"; + string trackerCommand = "/d " + s_defaultFileTrackerPath + " /c findstr /ip foo test.in"; + + File.Delete(tlog); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + string log = null; + try + { + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, trackerCommand); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), tlog); + + File.SetAttributes(tlog, FileAttributes.ReadOnly); + + exit = FileTrackerTestHelper.RunCommandWithLog(s_defaultTrackerPath, trackerCommand, out log); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.IsTrue(log.Contains("FTK1011")); // could not create new log: the file exists. + } + finally + { + File.SetAttributes(tlog, FileAttributes.Normal); + File.Delete(tlog); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrIn() + { + Console.WriteLine("Test: FileTrackerFindStrIn"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /c findstr /ip foo test.in"); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInOperations() + { + Console.WriteLine("Test: FileTrackerFindStrInOperations"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /o /c findstr /ip foo test.in"); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // On some OS's it calls CreateFileA as well, on Windows7 it doesn't, but it calls CreateFileW on defaultsort.nls.. + bool foundW = FileTrackerTestHelper.FindStringInTlog("CreateFileW, Desired Access=0x80000000, Creation Disposition=0x3:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + bool foundA = FileTrackerTestHelper.FindStringInTlog("CreateFileA, Desired Access=0x80000000, Creation Disposition=0x3:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + Assert.IsTrue(foundW || foundA); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInOperationsExtended() + { + Console.WriteLine("Test: FileTrackerFindStrInOperationsExtended"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /o /e /c findstr /ip foo test.in"); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // On some OS's it calls GetFileAttributesW as well, on Windows 2k8 R2 it doesn't + bool foundGetFileAttributesW = FileTrackerTestHelper.FindStringInTlog("GetFileAttributesW:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + bool foundGetFileAttributesA = FileTrackerTestHelper.FindStringInTlog("GetFileAttributesA:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + Assert.IsTrue(foundGetFileAttributesW || foundGetFileAttributesA); + + // On some OS's it calls CreateFileA as well, on Windows7 it doesn't, but it calls CreateFileW on defaultsort.nls.. + bool foundCreateFileW = FileTrackerTestHelper.FindStringInTlog("CreateFileW, Desired Access=0x80000000, Creation Disposition=0x3:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + bool foundCreateFileA = FileTrackerTestHelper.FindStringInTlog("CreateFileA, Desired Access=0x80000000, Creation Disposition=0x3:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + Assert.IsTrue(foundCreateFileW || foundCreateFileA); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInOperationsExtended_AttributesOnly() + { + Console.WriteLine("Test: FileTrackerFindStrInOperationsExtended_AttributesOnly"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /o /a /c findstr /ip foo test.in"); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + // On some OS's it calls GetFileAttributesW as well, on Windows 2k8 R2 it doesn't + bool foundGetFileAttributesW = FileTrackerTestHelper.FindStringInTlog("GetFileAttributesW:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + bool foundGetFileAttributesA = FileTrackerTestHelper.FindStringInTlog("GetFileAttributesA:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + Assert.IsTrue(foundGetFileAttributesW || foundGetFileAttributesA); + + // On some OS's it calls CreateFileA as well, on Windows7 it doesn't, but it calls CreateFileW on defaultsort.nls.. + bool foundCreateFileW = FileTrackerTestHelper.FindStringInTlog("CreateFileW, Desired Access=0x80000000, Creation Disposition=0x3:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + bool foundCreateFileA = FileTrackerTestHelper.FindStringInTlog("CreateFileA, Desired Access=0x80000000, Creation Disposition=0x3:" + Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + Assert.IsTrue(foundCreateFileW || foundCreateFileA); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerExtendedDirectoryTracking() + { + Console.WriteLine("Test: FileTrackerExtendedDirectoryTracking"); + + File.Delete("directoryattributes.read.1.tlog"); + File.Delete("directoryattributes.write.1.tlog"); + + string codeFile = null; + string outputFile = Path.Combine(Path.GetTempPath(), "directoryattributes.exe"); + string codeContent = @" +using System.IO; +using System.Runtime.InteropServices; + +namespace ConsoleApplication4 +{ + class Program + { + static void Main(string[] args) + { + File.GetAttributes(Directory.GetCurrentDirectory()); + GetFileAttributes(Directory.GetCurrentDirectory()); + } + + [DllImport(""Kernel32.dll"", SetLastError = true, CharSet = CharSet.Unicode)] + private extern static uint GetFileAttributes(string FileName); + } +}"; + + File.Delete(outputFile); + + try + { + codeFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(codeFile, codeContent); + Csc csc = new Csc(); + csc.BuildEngine = new MockEngine(); + csc.Sources = new ITaskItem[] { new TaskItem(codeFile) }; + csc.OutputAssembly = new TaskItem(outputFile); + csc.Execute(); + + string trackerPath = FileTracker.GetTrackerPath(ExecutableType.ManagedIL); + string fileTrackerPath = FileTracker.GetFileTrackerPath(ExecutableType.ManagedIL); + string commandArgs = "/d \"" + fileTrackerPath + "\" /o /u /e /c \"" + outputFile + "\""; + + int exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // Should track directories when '/e' is passed + FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + + File.Delete("directoryattributes.read.1.tlog"); + File.Delete("directoryattributes.write.1.tlog"); + + commandArgs = "/d \"" + fileTrackerPath + "\" /o /u /a /c \"" + outputFile + "\""; + + exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // With '/a', should *not* track GetFileAttributes on directories, even though we do so on files. + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + + File.Delete("directoryattributes.read.1.tlog"); + File.Delete("directoryattributes.write.1.tlog"); + + commandArgs = "/d \"" + fileTrackerPath + "\" /o /u /c \"" + outputFile + "\""; + + exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // With neither '/a' nor '/e', should not do any directory tracking whatsoever + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesExW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + FileTrackerTestHelper.AssertDidntFindStringInTLog("GetFileAttributesW:" + FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + + File.Delete("directoryattributes.read.1.tlog"); + File.Delete("directoryattributes.write.1.tlog"); + + commandArgs = "/d \"" + fileTrackerPath + "\" /u /e /c \"" + outputFile + "\""; + + exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // Should track directories when '/e' is passed + FileTrackerTestHelper.AssertFoundStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + + File.Delete("directoryattributes.read.1.tlog"); + File.Delete("directoryattributes.write.1.tlog"); + + commandArgs = "/d \"" + fileTrackerPath + "\" /u /a /c \"" + outputFile + "\""; + + exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // With '/a', should *not* track GetFileAttributes on directories, even though we do so on files. + FileTrackerTestHelper.AssertDidntFindStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + + File.Delete("directoryattributes.read.1.tlog"); + File.Delete("directoryattributes.write.1.tlog"); + + commandArgs = "/d \"" + fileTrackerPath + "\" /u /c \"" + outputFile + "\""; + + exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + + // With neither '/a' nor '/e', should not do any directory tracking whatsoever + FileTrackerTestHelper.AssertDidntFindStringInTLog(FileUtilities.EnsureTrailingSlash(Directory.GetCurrentDirectory()).ToUpperInvariant(), "directoryattributes.read.1.tlog"); + } + finally + { + File.Delete(codeFile); + File.Delete(outputFile); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInIncludeDuplicates() + { + Console.WriteLine("Test: FileTrackerFindStrInIncludeDuplicates"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + string codeFile = null; + string outputFile = Path.Combine(Path.GetTempPath(), "readtwice.exe"); + File.Delete(outputFile); + + try + { + string inputPath = Path.GetFullPath("test.in"); + codeFile = FileUtilities.GetTemporaryFile(); + string codeContent = @"using System.IO; class X { static void Main() { File.ReadAllText(@""" + inputPath + @"""); File.ReadAllText(@""" + inputPath + @"""); }}"; + File.WriteAllText(codeFile, codeContent); + Csc csc = new Csc(); + csc.BuildEngine = new MockEngine(); + csc.Sources = new TaskItem[] { new TaskItem(codeFile) }; + csc.OutputAssembly = new TaskItem(outputFile); + csc.Execute(); + + string trackerPath = FileTracker.GetTrackerPath(ExecutableType.ManagedIL); + string fileTrackerPath = FileTracker.GetFileTrackerPath(ExecutableType.ManagedIL); + string commandArgs = "/d \"" + fileTrackerPath + "\" /u /c \"" + outputFile + "\""; + + int exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + } + finally + { + File.Delete(codeFile); + File.Delete(outputFile); + } + + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "readtwice.read.1.tlog", 2); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerDoNotRecordWriteAsRead() + { + Console.WriteLine("Test: FileTrackerDoNotRecordWriteAsRead"); + + File.Delete("writenoread.read.1.tlog"); + File.Delete("writenoread.write.1.tlog"); + + string testDirectory = Path.Combine(Environment.CurrentDirectory, "FileTrackerDoNotRecordWriteAsRead"); + + if (Directory.Exists(testDirectory)) + { + ObjectModelHelpers.DeleteDirectory(testDirectory); + Directory.Delete(testDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(testDirectory); + string codeFile = null; + string writeFile = null; + string outputFile = Path.Combine(testDirectory, "writenoread.exe"); + + try + { + writeFile = Path.Combine(testDirectory, "test.out"); + codeFile = Path.Combine(testDirectory, "code.cs"); + string codeContent = @" +using System.IO; +using System.Runtime.InteropServices; +class X +{ + static void Main() + { + FileStream f = File.Open(@""" + writeFile + @""", FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite); + f.WriteByte(8); + f.Close(); + } +}"; + + File.WriteAllText(codeFile, codeContent); + Csc csc = new Csc(); + csc.BuildEngine = new MockEngine(); + csc.Sources = new TaskItem[] { new TaskItem(codeFile) }; + csc.OutputAssembly = new TaskItem(outputFile); + bool success = csc.Execute(); + + Assert.IsTrue(success); + + string trackerPath = FileTracker.GetTrackerPath(ExecutableType.ManagedIL); + string fileTrackerPath = FileTracker.GetFileTrackerPath(ExecutableType.ManagedIL); + string commandArgs = "/d \"" + fileTrackerPath + "\" /o /c \"" + outputFile + "\""; + + int exit = FileTrackerTestHelper.RunCommand(trackerPath, commandArgs); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + } + finally + { + // Doesn't delete the directory itself, but deletes its contents. If you try to delete the directory, + // even after calling this method, it sometimes throws IO exceptions due to not recognizing that the + // contents have been deleted yet. + ObjectModelHelpers.DeleteDirectory(testDirectory); + } + + if (writeFile != null) + { + FileTrackerTestHelper.AssertDidntFindStringInTLog("CreateFileW, Desired Access=0xc0000000, Creation Disposition=0x1:" + writeFile.ToUpperInvariant(), "writenoread.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog("CreateFileW, Desired Access=0xc0000000, Creation Disposition=0x1:" + writeFile.ToUpperInvariant(), "writenoread.write.1.tlog"); + } + else + { + Assert.Fail("The output file was never assigned or written to"); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInCommandLine() + { + Console.WriteLine("Test: FileTrackerFindStrInCommandLine"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /t /c findstr /ip foo test.in"); + string line = FileTrackerTestHelper.ReadLineFromFile("findstr.command.1.tlog", 1); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.AreEqual("findstr /ip foo test.in", line); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInArgumentSpaces() + { + Console.WriteLine("Test: FileTrackerFindStrIn"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test file.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /c findstr /ip foo \"test file.in\""); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test file.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindUnicode() + { + Console.WriteLine("Test: FileTrackerFindUnicode"); + + File.Delete("find.read.1.tlog"); + FileTrackerTestHelper.WriteAll("t\u1EBCst.in", "foo"); + + // FINDSTR.EXE doesn't support unicode, so we'll use FIND.EXE which does + int exit = FileTrackerTestHelper.RunCommandNoStdOut(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /i . /c find /I \"\\\"foo\"\\\" t\u1EBCst.in"); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("t\u1EBCst.in").ToUpperInvariant(), "find.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerStartProcessFindStrIn() + { + Console.WriteLine("Test: FileTrackerStartProcessFindStrIn"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + Process p = FileTracker.StartProcess("findstr", "/ip foo test.in", ExecutableType.Native32Bit); + p.WaitForExit(); + int exit = p.ExitCode; + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerResponseFile() + { + Console.WriteLine("Test: FileTrackerResponseFile"); + + File.Delete("tracker.rsp"); + FileTrackerTestHelper.WriteAll("tracker.rsp", "/d " + s_defaultFileTrackerPath + " /r jibbit"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "@tracker.rsp /c findstr /ip foo test.in"); + + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.AreEqual("^JIBBIT", + FileTrackerTestHelper.ReadLineFromFile("findstr.read.1.tlog", 1).ToUpperInvariant()); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInRootFiles() + { + Console.WriteLine("Test: FileTrackerFindStrInRootFiles"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /r jibbit /c findstr /ip foo test.in"); + + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.AreEqual("^JIBBIT", + FileTrackerTestHelper.ReadLineFromFile("findstr.read.1.tlog", 1).ToUpperInvariant()); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInRootFilesCommand() + { + Console.WriteLine("Test: FileTrackerFindStrInRootFilesCommand"); + + File.Delete("findstr.read.1.tlog"); + File.Delete("findstr.command.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/t /d " + s_defaultFileTrackerPath + " /r jibbit /c findstr /ip foo test.in"); + + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.AreEqual("^JIBBIT", + FileTrackerTestHelper.ReadLineFromFile("findstr.read.1.tlog", 1).ToUpperInvariant()); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + Assert.AreEqual("findstr /ip foo test.in", + FileTrackerTestHelper.ReadLineFromFile("findstr.command.1.tlog", 2)); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInRootFilesSpaces() + { + Console.WriteLine("Test: FileTrackerFindStrInRootFilesSpaces"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /r \"jibbit goo\" /c findstr /ip foo test.in"); + + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.AreEqual("^JIBBIT GOO", + FileTrackerTestHelper.ReadLineFromFile("findstr.read.1.tlog", 1).ToUpperInvariant()); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerHelperCommandLine() + { + Console.WriteLine("Test: FileTrackerHelperCommandLine"); + + File.Delete("findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, FileTracker.TrackerArguments("findstr", "/ip foo test.in", "" + s_defaultFileTrackerPathUnquoted, ".", "jibbit goo")); + + Console.WriteLine(""); + Assert.AreEqual(0, exit); + Assert.AreEqual("^JIBBIT GOO", + FileTrackerTestHelper.ReadLineFromFile("findstr.read.1.tlog", 1).ToUpperInvariant()); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerSortOut() + { + Console.WriteLine("Test: FileTrackerSortOut"); + + File.Delete("sort.read.1.tlog"); + File.Delete("sort.write.1.tlog"); + File.WriteAllLines("test.in", new string[] { + "bfoo", + "afoo" + }); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /c sort test.in /O test.out"); + + Assert.AreEqual(0, exit); + + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "sort.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.out").ToUpperInvariant(), "sort.write.1.tlog"); + + Assert.AreEqual("AFOO", + FileTrackerTestHelper.ReadLineFromFile("test.out", 0).ToUpperInvariant()); + + Assert.AreEqual("BFOO", + FileTrackerTestHelper.ReadLineFromFile("test.out", 1).ToUpperInvariant()); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerSortOutIntermediate() + { + Console.WriteLine("Test: FileTrackerSortOutIntermediate"); + + Directory.CreateDirectory("outdir"); + File.Delete("outdir\\sort.read.1.tlog"); + File.Delete("outdir\\sort.write.1.tlog"); + File.WriteAllLines("test.in", new string[] { + "bfoo", + "afoo" + }); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /i outdir /c sort test.in /O test.out"); + + Assert.AreEqual(0, exit); + + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "outdir\\sort.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.out").ToUpperInvariant(), "outdir\\sort.write.1.tlog"); + + Assert.AreEqual("AFOO", + FileTrackerTestHelper.ReadLineFromFile("test.out", 0).ToUpperInvariant()); + + Assert.AreEqual("BFOO", + FileTrackerTestHelper.ReadLineFromFile("test.out", 1).ToUpperInvariant()); + } + + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerIntermediateDirMissing() + { + Console.WriteLine("Test: FileTrackerIntermediateDirMissing"); + + // Make sure it really is missing + if (Directory.Exists("outdir")) + { + Directory.Delete("outdir", true); + } + + File.WriteAllLines("test.in", new string[] { + "bfoo", + "afoo" + }); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /i outdir /c sort test.in /O test.out"); + + Assert.AreEqual(0, exit); + + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "outdir\\sort.read.1.tlog"); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.out").ToUpperInvariant(), "outdir\\sort.write.1.tlog"); + + Assert.AreEqual("AFOO", + FileTrackerTestHelper.ReadLineFromFile("test.out", 0).ToUpperInvariant()); + + Assert.AreEqual("BFOO", + FileTrackerTestHelper.ReadLineFromFile("test.out", 1).ToUpperInvariant()); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInChain() + { + Console.WriteLine("Test: FileTrackerFindStrInChain"); + + File.Delete("cmd-findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /c cmd /c findstr /ip foo test.in"); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "cmd-findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFindStrInChainRepeatCommand() + { + Console.WriteLine("Test: FileTrackerFindStrInChainRepeatCommand"); + + string[] tlogFiles = Directory.GetFiles(Environment.CurrentDirectory, "cmd*-findstr.*.1.tlog", SearchOption.TopDirectoryOnly); + foreach (string tlogFile in tlogFiles) + { + File.Delete(tlogFile); + } + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + int exit = FileTrackerTestHelper.RunCommand(s_defaultTrackerPath, "/d " + s_defaultFileTrackerPath + " /c cmd /c cmd /c findstr /ip foo test.in"); + tlogFiles = Directory.GetFiles(Environment.CurrentDirectory, "cmd*-findstr.read.1.tlog", SearchOption.TopDirectoryOnly); + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), tlogFiles[0]); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFileIsUnderPath() + { + Console.WriteLine("Test: FileTrackerFileIsUnderPath"); + + // YES: Both refer to something under baz, so yes this is on the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\", @"c:\foo\bar\baz\")); + + // NO: Not under the path, since this *is* the path + Assert.AreEqual(false, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz", @"c:\foo\bar\baz\")); + + // NO: Not under the path, since the path is below + Assert.AreEqual(false, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz", @"c:\foo\bar\baz\")); + + // YES: Since the first parameter is a filename the extra '\' indicates we are referring to something + // other than the actual directory - so this would be under the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\", @"c:\foo\bar\baz")); + + // YES: this is under the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\hobbits.tmp", @"c:\foo\bar\baz\")); + + // YES: this is under the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\hobbits.tmp", @"c:\foo\bar\baz")); + + // YES: this is under the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\hobbits", @"c:\foo\bar\baz\")); + + // YES: this is under the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\hobbits", @"c:\foo\bar\baz")); + + // YES: this is under the path + Assert.AreEqual(true, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\bootle\hobbits.tmp", @"c:\foo\bar\baz\")); + + // NO: this is not under the path + Assert.AreEqual(false, FileTracker.FileIsUnderPath(@"c:\foo\bar\baz\hobbits.tmp", @"c:\boo1\far\chaz\")); + + // NO: this is not under the path + Assert.AreEqual(false, FileTracker.FileIsUnderPath(@"c:\foo1.cpp", @"c:\averyveryverylongtemp\path\this\is")); + + // NO: this is not under the path + Assert.AreEqual(false, FileTracker.FileIsUnderPath(@"c:\foo\rumble.cpp", @"c:\foo\rumble")); + + // NO: this is not under the path + Assert.AreEqual(false, FileTracker.FileIsUnderPath(@"c:\foo\rumble.cpp", @"c:\foo\rumble\")); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void FileTrackerFileIsExcludedFromDependencies() + { + Console.WriteLine("Test: FileTrackerFileIsExcludedFromDependencies"); + + string applicationDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string localApplicationDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string localLowApplicationDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData\\LocalLow"); + // The default path to temp, used to create explicitly short and long paths + string tempPath = Path.GetDirectoryName(Path.GetTempPath()); + // The short path to temp + string tempShortPath = FileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetShortFilePath(tempPath).ToUpperInvariant()); + // The long path to temp + string tempLongPath = FileUtilities.EnsureTrailingSlash(NativeMethodsShared.GetLongFilePath(tempPath).ToUpperInvariant()); + string testFile; + + // We don't want to be including these as dependencies or outputs: + // 1. Files under %USERPROFILE%\Application Data in XP and %USERPROFILE%\AppData\Roaming in Vista and later. + // 2. Files under %USERPROFILE%\Local Settings\Application Data in XP and %USERPROFILE%\AppData\Local in Vista and later. + // 3. Files under %USERPROFILE%\AppData\LocalLow in Vista and later. + // 4. Files that are in the TEMP directory (Since on XP, temp files are not + // located under AppData, they would not be compacted out correctly otherwise). + + // This file's NOT excluded from dependencies + testFile = @"c:\foo\bar\baz"; + Assert.AreEqual(false, FileTracker.FileIsExcludedFromDependencies(testFile)); + + // This file IS excluded from dependencies + testFile = Path.Combine(applicationDataPath, "blah.log"); + Assert.AreEqual(true, FileTracker.FileIsExcludedFromDependencies(testFile)); + + // This file IS excluded from dependencies + testFile = Path.Combine(localApplicationDataPath, "blah.log"); + Assert.AreEqual(true, FileTracker.FileIsExcludedFromDependencies(testFile)); + + // This file IS excluded from dependencies + testFile = Path.Combine(localLowApplicationDataPath, "blah.log"); + Assert.AreEqual(true, FileTracker.FileIsExcludedFromDependencies(testFile)); + + // This file IS excluded from dependencies + testFile = Path.Combine(tempShortPath, "blah.log"); + Assert.AreEqual(true, FileTracker.FileIsExcludedFromDependencies(testFile)); + + // This file IS excluded from dependencies + testFile = Path.Combine(tempLongPath, "blah.log"); + Assert.AreEqual(true, FileTracker.FileIsExcludedFromDependencies(testFile)); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTest1() + { + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + + File.Delete(tlogWriteFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingTest1"); + + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + + FileTracker.StopTrackingAndCleanup(); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + Assert.AreEqual(2, lines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + + File.Delete(tlogWriteFile); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTest2() + { + // Do test 1 twice in a row to make sure there is no leakage + InProcTrackingTest1(); + InProcTrackingTest1(); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTestSuspendResume() + { + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + + File.Delete(tlogWriteFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingTestSuspendResume"); + + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + // Nothing should be tracked following this call + FileTracker.SuspendTracking(); + + File.WriteAllText(sourceFile + "_s", "this is a inline tracking test"); + + // And tracking should resume + FileTracker.ResumeTracking(); + + File.WriteAllText(sourceFile + "_r", "this is a inline tracking test"); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + + FileTracker.StopTrackingAndCleanup(); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + Assert.AreEqual(3, lines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile + "_r").ToUpperInvariant(), lines[2]); + + File.Delete(tlogWriteFile); + File.Delete(sourceFile); + File.Delete(sourceFile + "_s"); + File.Delete(sourceFile + "_r"); + } + + [TestMethod] + [ExpectedException(typeof(COMException))] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTestStopBeforeWrite() + { + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + + File.Delete(tlogWriteFile); + File.Delete(sourceFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingTestStopBeforeWrite"); + + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + FileTracker.StopTrackingAndCleanup(); + + // This should throw a COMException, since we have cleaned up + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + + public void InProcTrackingTestNotStop() + { + InProcTrackingTesterNoStop(1); + // Since we didn't stop in the test, we should stop now + // to ensure we don't leak into the other tests + FileTracker.StopTrackingAndCleanup(); + } + + public void InProcTrackingTesterNoStop(int iteration) + { + string sourceFile = String.Format("inlinetrackingtest{0}.txt", iteration); + string tlogRootName = String.Format("foo_nonstopinline{0}", iteration); + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogReadFile = String.Format("{0}.read.1.tlog", tlogRootName); + + File.Delete(tlogWriteFile); + File.Delete(tlogReadFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingTesterNoStop"); + + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + + File.WriteAllText(sourceFile + "_s", "this is a inline tracking test - again"); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + Assert.AreEqual(4, lines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile + "_s").ToUpperInvariant(), lines[3]); + + File.Delete(tlogWriteFile); + // Since we are non-stop during iteration we actually get read tlogs + // Because of the "ReadLinesFromFile" above. However it will be empty + // Since by default the tracker does not write entries for files that + // do not exist - and we did delete the file being tracked on the previous + // iteration! + File.Delete(tlogReadFile); + File.Delete(sourceFile); + File.Delete(sourceFile + "_s"); + } + + private static void InProcTrackingTester(int iteration) + { + string sourceFile = String.Format("inlinetrackingtest{0}.txt", iteration); + string tlogRootName = String.Format("foo_inline{0}", iteration); + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + + File.Delete(tlogWriteFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingTester"); + + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + + FileTracker.StopTrackingAndCleanup(); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + Assert.AreEqual(2, lines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + + File.Delete(tlogWriteFile); + File.Delete(sourceFile); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTestIteration() + { + for (int iter = 0; iter < 50; iter++) + { + InProcTrackingTester(iter); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingNonStopTestIteration() + { + for (int iter = 0; iter < 50; iter++) + { + InProcTrackingTesterNoStop(iter); + } + FileTracker.StopTrackingAndCleanup(); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTwoContexts() + { + string sourceFile = "inlinetrackingtest.txt"; + string sourceFile2 = "inlinetrackingtest2.txt"; + string sourceFile3 = "inlinetrackingtest3.txt"; + string tlogRootName = "foo_inline"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogWriteFile2 = String.Format("{0}2.write.1.tlog", tlogRootName); + + File.Delete(tlogWriteFile); + File.Delete(tlogWriteFile2); + + // Context 1 + FileTracker.StartTrackingContext(Path.GetFullPath("."), "Context1"); + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + // Context 2 + FileTracker.StartTrackingContext(Path.GetFullPath("."), "Context2"); + File.WriteAllText(sourceFile2, "this is a inline tracking test - in a second context"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName + "2"); + FileTracker.EndTrackingContext(); + + // Back to context 1 + File.WriteAllText(sourceFile3, "this is a second inline tracking test in the first context"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + FileTracker.EndTrackingContext(); + + FileTracker.StopTrackingAndCleanup(); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + string[] lines2 = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile2); + Assert.AreEqual(3, lines.Length); + Assert.AreEqual(2, lines2.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile3).ToUpperInvariant(), lines[2]); + Assert.AreEqual(Path.GetFullPath(sourceFile2).ToUpperInvariant(), lines2[1]); + + File.Delete(tlogWriteFile); + File.Delete(tlogWriteFile2); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingTwoContextsWithRoot() + { + string sourceFile = "inlinetrackingtest.txt"; + string sourceFile2 = "vi\u00FCes\u00E4tato633833475975527668.txt"; + string sourceFile3 = "inlinetrackingtest3.txt"; + string tlogRootName = "foo_inline"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogWriteFile2 = String.Format("{0}2.write.1.tlog", tlogRootName); + + string rootMarker = FileTracker.FormatRootingMarker(new TaskItem(sourceFile2)); + string responseFile = FileTracker.CreateRootingMarkerResponseFile(rootMarker); + + File.Delete(tlogWriteFile); + File.Delete(tlogWriteFile2); + + try + { + // Context 1 + FileTracker.StartTrackingContext(Path.GetFullPath("."), "Context1"); + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + // Context 2 + FileTracker.StartTrackingContextWithRoot(Path.GetFullPath("."), "Context2", responseFile); + File.WriteAllText(sourceFile2, "this is a inline tracking test - in a second context"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName + "2"); + FileTracker.EndTrackingContext(); + + // Back to context 1 + File.WriteAllText(sourceFile3, "this is a second inline tracking test in the first context"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + FileTracker.EndTrackingContext(); + + FileTracker.StopTrackingAndCleanup(); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + string[] lines2 = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile2); + Assert.AreEqual(3, lines.Length); + Assert.AreEqual(3, lines2.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile3).ToUpperInvariant(), lines[2]); + Assert.AreEqual("^" + rootMarker, lines2[1]); + Assert.IsTrue(String.Equals(rootMarker, lines2[2], StringComparison.OrdinalIgnoreCase)); + } + finally + { + File.Delete(tlogWriteFile); + File.Delete(tlogWriteFile2); + File.Delete(responseFile); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingSpawnsOutOfProcTool() + { + string intermediateDir = Path.GetTempPath() + @"InProcTrackingSpawnsOutOfProcTool\"; + + string sourceFile = intermediateDir + @"inlinetracking1.txt"; + string commandFile = intermediateDir + @"command.bat"; + string tlogRootName = "inproc_spawn"; + string tlogWriteFile = intermediateDir + String.Format("{0}-cmd.write.1.tlog", tlogRootName); + string rootMarker = @"\\THIS\IS\MY\ROOT|\\IT\IS\COMPOUND\TOO"; + string rootMarkerRsp = intermediateDir + @"rootmarker.rsp"; + + if (Directory.Exists(intermediateDir)) + { + Directory.Delete(intermediateDir, true); + } + + try + { + Directory.CreateDirectory(intermediateDir); + + File.WriteAllText(commandFile, "echo this is out of proc tracking writing stuff > \"" + sourceFile + "\""); + File.WriteAllText(rootMarkerRsp, "/r " + rootMarker); + + FileTracker.StartTrackingContextWithRoot(intermediateDir, tlogRootName, rootMarkerRsp); + + ProcessStartInfo ps = new ProcessStartInfo("cmd.exe", "/C \"" + commandFile + "\""); + // Clear out all environment variables + Process cmd = Process.Start(ps); + cmd.WaitForExit(); + + FileTracker.StopTrackingAndCleanup(); + + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + + Assert.AreEqual(3, lines.Length); + Assert.AreEqual("^" + rootMarker, lines[1]); + Assert.AreEqual(sourceFile.ToUpperInvariant(), lines[2]); + } + finally + { + if (Directory.Exists(intermediateDir)) + { + Directory.Delete(intermediateDir, true); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingSpawnsOutOfProcTool_OverrideEnvironment() + { + string intermediateDir = Path.GetTempPath() + @"InProcTrackingSpawnsOutOfProcTool_OverrideEnvironment\"; + + string sourceFile = intermediateDir + @"inlinetracking1.txt"; + string commandFile = intermediateDir + @"command.bat"; + string tlogRootName = "inproc_spawn_env"; + string tlogWriteFile = intermediateDir + String.Format("{0}-cmd.write.1.tlog", tlogRootName); + string rootMarker = @"\\THIS\IS\MY\ROOT|\\IT\IS\COMPOUND\TOO"; + string rootMarkerRsp = intermediateDir + @"rootmarker.rsp"; + + if (Directory.Exists(intermediateDir)) + { + Directory.Delete(intermediateDir, true); + } + + try + { + Directory.CreateDirectory(intermediateDir); + + File.WriteAllText(commandFile, "echo this is out of proc tracking writing stuff > \"" + sourceFile + "\""); + File.WriteAllText(rootMarkerRsp, "/r " + rootMarker); + + FileTracker.StartTrackingContextWithRoot(intermediateDir, tlogRootName, rootMarkerRsp); + + ProcessStartInfo ps = new ProcessStartInfo("cmd.exe", "/C \"" + commandFile + "\""); + ps.EnvironmentVariables["TRACKER_TOOLCHAIN"] = "MSBuild"; + ps.UseShellExecute = false; + + Process cmd = Process.Start(ps); + cmd.WaitForExit(); + + FileTracker.StopTrackingAndCleanup(); + + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + + Assert.AreEqual(3, lines.Length); + Assert.AreEqual("^" + rootMarker, lines[1]); + Assert.AreEqual(sourceFile.ToUpperInvariant(), lines[2]); + } + finally + { + if (Directory.Exists(intermediateDir)) + { + Directory.Delete(intermediateDir, true); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingSpawnsToolWithTrackerResponseFile() + { + Console.WriteLine("Test: InProcTrackingSpawnsToolWithTrackerResponseFile"); + + InProcTrackingSpawnsToolWithTracker(true); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingSpawnsToolWithTrackerNoResponseFile() + { + Console.WriteLine("Test: InProcTrackingSpawnsToolWithTrackerNoResponseFile"); + + InProcTrackingSpawnsToolWithTracker(false); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + [ExpectedException(typeof(COMException))] + public void InProcTrackingTwoContextsTwoEnds() + { + string sourceFile = "inlinetrackingtest.txt"; + string sourceFile2 = "inlinetrackingtest2.txt"; + string tlogRootName = "foo_inline"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogWriteFile2 = String.Format("{0}2.write.1.tlog", tlogRootName); + + try + { + File.Delete(tlogWriteFile); + File.Delete(tlogWriteFile2); + + // Context 1 + FileTracker.StartTrackingContext(Path.GetFullPath("."), "Context1"); + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + // Context 2 + FileTracker.StartTrackingContext(Path.GetFullPath("."), "Context2"); + File.WriteAllText(sourceFile2, "this is a inline tracking test - in a second context"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName + "2"); + FileTracker.EndTrackingContext(); + // This will cause the outer context to end which will mean there is nothing in the context for the write + FileTracker.EndTrackingContext(); + + // There is nothing in the context to write from, we should get an exception here: + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + FileTracker.EndTrackingContext(); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + + File.Delete(tlogWriteFile); + File.Delete(tlogWriteFile2); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingStartProcessFindStrIn() + { + Console.WriteLine("Test: InProcTrackingStartProcessFindStrIn"); + int exit = 0; + + try + { + File.Delete("findstr.read.1.tlog"); + File.Delete("InProcTrackingStartProcessFindStrIn-findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingStartProcessFindStrIn"); + exit = FileTrackerTestHelper.RunCommand("findstr", "/ip foo test.in"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), "inlinefind"); + FileTracker.EndTrackingContext(); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + } + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "InProcTrackingStartProcessFindStrIn-findstr.read.1.tlog"); + File.Delete("findstr.read.1.tlog"); + File.Delete("InProcTrackingStartProcessFindStrIn-findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingStartProcessFindStrNullCommandLine() + { + Console.WriteLine("Test: InProcTrackingStartProcessFindStrNullCommandLine"); + + try + { + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingStartProcessFindStrIn"); + BackEndNativeMethods.STARTUP_INFO startInfo = new BackEndNativeMethods.STARTUP_INFO(); + startInfo.cb = Marshal.SizeOf(startInfo); + uint dwCreationFlags = BackEndNativeMethods.NORMALPRIORITYCLASS; + + startInfo.hStdError = BackEndNativeMethods.InvalidHandle; + startInfo.hStdInput = BackEndNativeMethods.InvalidHandle; + startInfo.hStdOutput = BackEndNativeMethods.InvalidHandle; + startInfo.dwFlags = BackEndNativeMethods.STARTFUSESTDHANDLES; + dwCreationFlags = dwCreationFlags | BackEndNativeMethods.CREATENOWINDOW; + + BackEndNativeMethods.SECURITY_ATTRIBUTES pSec = new BackEndNativeMethods.SECURITY_ATTRIBUTES(); + BackEndNativeMethods.SECURITY_ATTRIBUTES tSec = new BackEndNativeMethods.SECURITY_ATTRIBUTES(); + pSec.nLength = Marshal.SizeOf(pSec); + tSec.nLength = Marshal.SizeOf(tSec); + + BackEndNativeMethods.PROCESS_INFORMATION pInfo = new BackEndNativeMethods.PROCESS_INFORMATION(); + + string appName = NativeMethodsShared.FindOnPath("findstr.exe"); + + string cmdLine = null; + bool created = BackEndNativeMethods.CreateProcess(appName, cmdLine, + ref pSec, ref tSec, + false, dwCreationFlags, + BackEndNativeMethods.NullPtr, null, ref startInfo, out pInfo); + + // We should have correctly started the process even though the command-line was null + Assert.IsTrue(created); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), "inlinefind"); + FileTracker.EndTrackingContext(); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + } + File.Delete("inlinefind.read.1.tlog"); + } + + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingStartProcessFindStrInDefaultTaskName() + { + Console.WriteLine("Test: InProcTrackingStartProcessFindStrInDefaultTaskName"); + int exit = 0; + + try + { + File.Delete("findstr.read.1.tlog"); + File.Delete("InProcTrackingStartProcessFindStrIn-findstr.read.1.tlog"); + FileTrackerTestHelper.WriteAll("test.in", "foo"); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), ""); + exit = FileTrackerTestHelper.RunCommand("findstr", "/ip foo test.in"); + FileTracker.EndTrackingContext(); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + } + + Console.WriteLine(""); + Assert.AreEqual(0, exit); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath("test.in").ToUpperInvariant(), "findstr.read.1.tlog"); + + File.Delete("findstr.read.1.tlog"); + File.Delete("InProcTrackingStartProcessFindStrIn-findstr.read.1.tlog"); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingChildThreadTrackedAuto() + { + FileTracker.SetThreadCount(1); + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline_parent"; + string tlogChildRootName = "InProcTrackingChildThreadTrackedAuto"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogChildWriteFile = String.Format("{0}.write.2.tlog", tlogChildRootName); + + File.Delete(tlogWriteFile); + File.Delete(tlogChildWriteFile); + File.Delete(sourceFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingChildThreadTrackedAuto"); + + File.WriteAllText(sourceFile, "parent thread\r\n"); + + System.Threading.Thread t = new Thread(new ThreadStart(ThreadProcAutoTLog)); + t.Start(); + t.Join(); // wait for our child to complete + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); // parent will write an explicit tlog + + FileTracker.StopTrackingAndCleanup(); + string[] writtenlines = FileTrackerTestHelper.ReadLinesFromFile(sourceFile); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + string[] childLines = FileTrackerTestHelper.ReadLinesFromFile(tlogChildWriteFile); + Assert.AreEqual(2, lines.Length); + Assert.AreEqual(2, childLines.Length); + Assert.AreEqual(2, writtenlines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), childLines[1]); + Assert.AreEqual("parent thread", writtenlines[0]); + Assert.AreEqual("child thread", writtenlines[1]); + + File.Delete(tlogWriteFile); + File.Delete(tlogChildWriteFile); + File.Delete(sourceFile); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingChildThreadTrackedManual() + { + FileTracker.SetThreadCount(1); + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline_parent"; + string tlogChildRootName = "foo_inline_child"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogChildWriteFile = String.Format("{0}.write.2.tlog", tlogChildRootName); + + File.Delete(tlogWriteFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingChildThreadTrackedAuto"); + + File.WriteAllText(sourceFile, "parent thread\r\n"); + + System.Threading.Thread t = new Thread(new ThreadStart(ThreadProcManualTLog)); + t.Start(); + t.Join(); // wait for our child to complete + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); // parent will write an explicit tlog + + FileTracker.StopTrackingAndCleanup(); + string[] writtenlines = FileTrackerTestHelper.ReadLinesFromFile(sourceFile); + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + string[] childLines = FileTrackerTestHelper.ReadLinesFromFile(tlogChildWriteFile); + Assert.AreEqual(2, lines.Length); + Assert.AreEqual(2, childLines.Length); + Assert.AreEqual(2, writtenlines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), lines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), childLines[1]); + Assert.AreEqual("parent thread", writtenlines[0]); + Assert.AreEqual("child thread", writtenlines[1]); + + File.Delete(tlogWriteFile); + File.Delete(tlogChildWriteFile); + File.Delete(sourceFile); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingChildThreadNotTracked() + { + FileTracker.SetThreadCount(1); + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline_parent"; + string tlogChildRootName = "ThreadProcTrackedAutoTLog"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogChildWriteFile = String.Format("{0}.write.2.tlog", tlogChildRootName); + + File.Delete(tlogWriteFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingChildThreadTrackedAuto"); + FileTracker.SuspendTracking(); + + File.WriteAllText(sourceFile, "parent thread\r\n"); + + System.Threading.Thread t = new Thread(new ThreadStart(ThreadProcAutoTLog)); + t.Start(); + t.Join(); // wait for our child to complete + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); // parent will write an explicit tlog + + FileTracker.StopTrackingAndCleanup(); + Assert.AreEqual(false, File.Exists(tlogWriteFile)); + Assert.AreEqual(false, File.Exists(tlogChildRootName)); + string[] writtenlines = FileTrackerTestHelper.ReadLinesFromFile(sourceFile); + Assert.AreEqual(2, writtenlines.Length); + Assert.AreEqual("parent thread", writtenlines[0]); + Assert.AreEqual("child thread", writtenlines[1]); + + File.Delete(tlogWriteFile); + File.Delete(tlogChildWriteFile); + File.Delete(sourceFile); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingChildThreadNotTrackedLocallyTracked() + { + FileTracker.SetThreadCount(1); + string sourceFile = "inlinetrackingtest.txt"; + string tlogRootName = "foo_inline_parent"; + string tlogChildRootName = "ThreadProcLocallyTracked"; + string tlogWriteFile = String.Format("{0}.write.1.tlog", tlogRootName); + string tlogChildWriteFile = String.Format("{0}.write.2.tlog", tlogChildRootName); + + File.Delete(tlogWriteFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), "InProcTrackingChildThreadNotTrackedLocallyTracked"); + FileTracker.SuspendTracking(); + + File.WriteAllText(sourceFile, "parent thread\r\n"); + + System.Threading.Thread t = new Thread(new ThreadStart(ThreadProcLocallyTracked)); + t.Start(); + t.Join(); // wait for our child to complete + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); // parent will write an explicit tlog + + FileTracker.StopTrackingAndCleanup(); + Assert.AreEqual(false, File.Exists(tlogWriteFile)); + string[] writtenlines = FileTrackerTestHelper.ReadLinesFromFile(sourceFile); + string[] childLines = FileTrackerTestHelper.ReadLinesFromFile(tlogChildWriteFile); + Assert.AreEqual(2, childLines.Length); + Assert.AreEqual(2, writtenlines.Length); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), childLines[1]); + Assert.AreEqual("parent thread", writtenlines[0]); + Assert.AreEqual("child thread", writtenlines[1]); + + File.Delete(tlogWriteFile); + File.Delete(tlogChildWriteFile); + File.Delete(sourceFile); + } + + public void ThreadProcLocallyTracked() + { + FileTracker.StartTrackingContext(Path.GetFullPath("."), "ThreadProcLocallyTracked"); + string sourceFile = "inlinetrackingtest.txt"; + File.AppendAllText(sourceFile, "child thread\r\n"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), "ThreadProcLocallyTracked"); // will write an explicit tlog + FileTracker.EndTrackingContext(); + } + + public void ThreadProcAutoTLog() + { + string sourceFile = "inlinetrackingtest.txt"; + File.AppendAllText(sourceFile, "child thread\r\n"); + } + + public void ThreadProcManualTLog() + { + string tlogRootName = "foo_inline_child"; + string sourceFile = "inlinetrackingtest.txt"; + File.AppendAllText(sourceFile, "child thread\r\n"); + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + } + + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void InProcTrackingChildCustomEnvironment() + { + string sourceFile = "allenvironment.txt"; + string commandFile = "inlinetrackingtest.cmd"; + string tlogRootName = "CustomEnvironment"; + string tlogReadFile = String.Format("{0}-cmd.read.1.tlog", tlogRootName); + string tlogWriteFile = String.Format("{0}-cmd.write.1.tlog", tlogRootName); + File.Delete(tlogWriteFile); + + File.WriteAllText(commandFile, "SET > " + sourceFile); + + FileTracker.StartTrackingContext(Path.GetFullPath("."), tlogRootName); + + ProcessStartInfo ps = new ProcessStartInfo("cmd.exe", "/C " + commandFile); + + int envVarCount = ps.EnvironmentVariables.Count; + ps.EnvironmentVariables.Add("TESTVAR", "THE_RIGHT_VALUE"); + ps.UseShellExecute = false; + + Process cmd = Process.Start(ps); + cmd.WaitForExit(); + + FileTracker.StopTrackingAndCleanup(); + + // Read in the environment file and check that the variable that we set is there + string[] envLines = File.ReadAllLines(sourceFile); + int trackerEnvValueCount = 0; + + string varValue = null; + string toolChainValue = null; + foreach (string envLine in envLines) + { + if (envLine.StartsWith("TRACKER_", StringComparison.OrdinalIgnoreCase)) + { + trackerEnvValueCount++; + } + + if (envLine.StartsWith("TESTVAR=", StringComparison.OrdinalIgnoreCase) && varValue == null) + { + string[] varVal = envLine.Split('='); + varValue = varVal[1]; + } + else if (envLine.StartsWith("TRACKER_TOOLCHAIN=", StringComparison.OrdinalIgnoreCase) && toolChainValue == null) + { + string[] varVal = envLine.Split('='); + toolChainValue = varVal[1]; + } + } + + Assert.IsTrue(trackerEnvValueCount >= 7, "Not enough tracking environment set"); + Assert.AreEqual("THE_RIGHT_VALUE", varValue); + Assert.AreEqual(tlogRootName + "-cmd", toolChainValue); + string[] writeLines = FileTrackerTestHelper.ReadLinesFromFile(tlogWriteFile); + string[] readLines = FileTrackerTestHelper.ReadLinesFromFile(tlogReadFile); + Assert.AreEqual(2, writeLines.Length); + Assert.AreEqual(2, readLines.Length); + Assert.AreEqual(Path.GetFullPath(commandFile).ToUpperInvariant(), readLines[1]); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), writeLines[1]); + + File.Delete(tlogReadFile); + File.Delete(tlogWriteFile); + File.Delete(sourceFile); + File.Delete(commandFile); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void CreateFileDoesntRecordWriteIfNotWrittenTo() + { + string testDir = Path.Combine(Path.GetTempPath(), "CreateFileDoesntRecordWriteIfNotWrittenTo"); + string readFile = Path.Combine(testDir, "readfile.txt"); + string tlogRootName = "CreateFileRead"; + + if (Directory.Exists(testDir)) + { + Directory.Delete(testDir, true /* recursive */); + } + + try + { + Directory.CreateDirectory(testDir); + File.WriteAllText(readFile, "this is some sample text that doesn't really matter"); + + // wait a bit to give the timestamps time to settle + Thread.Sleep(100); + + FileTracker.StartTrackingContext(testDir, tlogRootName); + + byte[] buffer = new byte[10]; + using (FileStream fs = File.Open(readFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + fs.Read(buffer, 0, 10); + } + + FileTracker.WriteContextTLogs(testDir, tlogRootName); + + FileTrackerTestHelper.AssertFoundStringInTLog(readFile.ToUpperInvariant(), Path.Combine(testDir, tlogRootName + ".read.1.tlog")); + FileTrackerTestHelper.AssertDidntFindStringInTLog(readFile.ToUpperInvariant(), Path.Combine(testDir, tlogRootName + ".write.1.tlog")); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + if (Directory.Exists(testDir)) + { + Directory.Delete(testDir, true /* recursive */); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void CopyAlwaysRecordsWrites() + { + string testDir = Path.Combine(Path.GetTempPath(), "CopyAlwaysRecordsWrites"); + string tlogRootName = "CopyFileTest"; + string copyFromFile = Path.Combine(testDir, "copyFrom.txt"); + string copyToFile = Path.Combine(testDir, "copyTo.txt"); + string tlogReadFile = Path.Combine(testDir, tlogRootName + ".read.1.tlog"); + string tlogWriteFile = Path.Combine(testDir, tlogRootName + ".write.1.tlog"); + + if (Directory.Exists(testDir)) + { + Directory.Delete(testDir, true /* recursive */); + } + + try + { + Directory.CreateDirectory(testDir); + + try + { + File.WriteAllText(copyFromFile, "text in the file!"); + + FileTracker.StartTrackingContext(testDir, tlogRootName); + + File.Copy(copyFromFile, copyToFile); + + FileTracker.WriteContextTLogs(testDir, tlogRootName); + + FileTrackerTestHelper.AssertFoundStringInTLog(copyFromFile.ToUpperInvariant(), tlogReadFile); + FileTrackerTestHelper.AssertFoundStringInTLog(copyToFile.ToUpperInvariant(), tlogWriteFile); + } + finally + { + File.Delete(tlogReadFile); + File.Delete(tlogWriteFile); + FileTracker.StopTrackingAndCleanup(); + } + + // wait a bit to give the timestamps time to settle + Thread.Sleep(100); + + try + { + File.Delete(copyToFile); + + FileTracker.StartTrackingContext(testDir, tlogRootName); + + File.Copy(copyFromFile, copyToFile); + + FileTracker.WriteContextTLogs(testDir, tlogRootName); + + FileTrackerTestHelper.AssertFoundStringInTLog(copyFromFile.ToUpperInvariant(), tlogReadFile); + FileTrackerTestHelper.AssertFoundStringInTLog(copyToFile.ToUpperInvariant(), tlogWriteFile); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + } + } + finally + { + if (Directory.Exists(testDir)) + { + Directory.Delete(testDir, true /* recursive */); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void MoveAlwaysRecordsWrites() + { + string testDir = Path.Combine(Path.GetTempPath(), "MoveAlwaysRecordsWrites"); + string tlogRootName = "MoveFileTest"; + string moveFromFile = Path.Combine(testDir, "MoveFrom.txt"); + string moveToFile = Path.Combine(testDir, "MoveTo.txt"); + string moveToFile2 = Path.Combine(testDir, "MoveTo2.txt"); + string tlogDeleteFile = Path.Combine(testDir, tlogRootName + ".delete.1.tlog"); + string tlogWriteFile = Path.Combine(testDir, tlogRootName + ".write.1.tlog"); + + if (Directory.Exists(testDir)) + { + Directory.Delete(testDir, true /* recursive */); + } + + try + { + Directory.CreateDirectory(testDir); + + try + { + File.WriteAllText(moveFromFile, "text in the file!"); + + FileTracker.StartTrackingContext(testDir, tlogRootName); + + File.Move(moveFromFile, moveToFile); + + FileTracker.WriteContextTLogs(testDir, tlogRootName); + + FileTrackerTestHelper.AssertFoundStringInTLog(moveFromFile.ToUpperInvariant(), tlogDeleteFile); + FileTrackerTestHelper.AssertFoundStringInTLog(moveToFile.ToUpperInvariant(), tlogWriteFile); + } + finally + { + File.Delete(tlogDeleteFile); + File.Delete(tlogWriteFile); + FileTracker.StopTrackingAndCleanup(); + } + + // wait a bit to give the timestamps time to settle + Thread.Sleep(100); + + try + { + File.WriteAllText(moveFromFile, "text in the file!"); + File.Delete(moveToFile); + + FileTracker.StartTrackingContext(testDir, tlogRootName); + + File.Move(moveFromFile, moveToFile); + File.Move(moveToFile, moveToFile2); + + FileTracker.WriteContextTLogs(testDir, tlogRootName); + + FileTrackerTestHelper.AssertFoundStringInTLog(moveFromFile.ToUpperInvariant(), tlogDeleteFile); + FileTrackerTestHelper.AssertFoundStringInTLog(moveToFile.ToUpperInvariant(), tlogDeleteFile); + FileTrackerTestHelper.AssertFoundStringInTLog(moveToFile2.ToUpperInvariant(), tlogWriteFile); + } + finally + { + FileTracker.StopTrackingAndCleanup(); + } + } + finally + { + if (Directory.Exists(testDir)) + { + Directory.Delete(testDir, true /* recursive */); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_SameCommand() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_SameCommand"); + Directory.CreateDirectory(testDir); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "abc.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, "foo baz"); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/i baz " + tempFilePath, 3)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-abc*tlog", 3)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_DifferentCommands1() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_DifferentCommands1"); + Directory.CreateDirectory(testDir); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "abc.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, "foo baz"); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/i foo " + tempFilePath, 3)); + toolsToLaunch.Add(new Tuple(null, "\"" + destinationFindstrPath + "\" /i baz " + tempFilePath, 3)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-abc*tlog", 6)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_DifferentCommands2() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_DifferentCommands2"); + + try + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + + Directory.CreateDirectory(testDir); + + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, ""); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "abc.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/i baz " + tempFilePath, 3)); + toolsToLaunch.Add(new Tuple(null, "\"" + destinationFindstrPath + "\" /i foo " + tempFilePath, 2)); + toolsToLaunch.Add(new Tuple(null, "\"" + destinationFindstrPath + "\" /i ba " + tempFilePath, 2)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-abc*tlog", 7)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + finally + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_DifferentCommands3() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_DifferentCommands3"); + string oldCurrentDirectory = Environment.CurrentDirectory; + try + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + + Directory.CreateDirectory(testDir); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "findstr.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, "foo baz"); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(originalFindstrPath, "/i foo " + tempFilePath, 3)); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/i baz " + tempFilePath, 3)); + toolsToLaunch.Add(new Tuple(null, "FIndsTr /i ba " + tempFilePath, 2)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-findstr*tlog", 8)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + finally + { + if (oldCurrentDirectory != null) + { + Environment.CurrentDirectory = oldCurrentDirectory; + } + + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_DifferentCommands4() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_DifferentCommands4"); + string oldPath = Environment.GetEnvironmentVariable("PATH"); + + try + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + + Directory.CreateDirectory(testDir); + + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, ""); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "abc.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + Environment.SetEnvironmentVariable("PATH", Path.GetDirectoryName(destinationFindstrPath) + ";" + oldPath); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/ip oo " + tempFilePath, 3)); + toolsToLaunch.Add(new Tuple(null, "abc.exe /i foo " + tempFilePath, 3)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-abc*tlog", 6)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + finally + { + Environment.SetEnvironmentVariable("PATH", oldPath); + + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleDifferentTools() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleDifferentTools"); + + try + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + + Directory.CreateDirectory(testDir); + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, ""); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "abc.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/i foo " + tempFilePath, 3)); + toolsToLaunch.Add(new Tuple(null, "findstr /i foo " + tempFilePath, 3)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-abc*tlog", 3)); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-findstr*tlog", 3)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + finally + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_DifferentContexts() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_DifferentContexts"); + Directory.CreateDirectory(testDir); + + string originalFindstrPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "findstr.exe"); + string destinationFindstrPath = Path.Combine(testDir, "abc.exe"); + File.Copy(originalFindstrPath, destinationFindstrPath); + + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, "foo baz"); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(destinationFindstrPath, "/i baz " + tempFilePath, 3)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest2", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-abc*tlog", 3)); + tlogPatterns.Add(new Tuple("ProcessLaunchTest2-abc*tlog", 3)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void LaunchMultipleOfSameTool_ToolLaunchesOthers() + { + string testDir = Path.Combine(Path.GetTempPath(), "LaunchMultipleOfSameTool_ToolLaunchesOthers"); + + try + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + + Directory.CreateDirectory(testDir); + + // File to run findstr against. + string tempFilePath = Path.Combine(testDir, "bar.txt"); + File.WriteAllText(tempFilePath, ""); + + // Sample app that runs findstr. + string codeFile = null; + string outputFile = Path.Combine(testDir, "FindstrLauncher.exe"); + string codeContent = @" +using System; +using System.Diagnostics; + +namespace ConsoleApplication4 +{ + class Program + { + static void Main(string[] args) + { + if (args.Length > 1) + { + for (int i = 0; i < Int32.Parse(args[0]); i++) + { + Process p = Process.Start(""findstr"", ""/i foo "" + args[1]); + p.WaitForExit(); + } + } + } + } +}"; + + File.Delete(outputFile); + + codeFile = Path.Combine(testDir, "Program.cs"); + File.WriteAllText(codeFile, codeContent); + Csc csc = new Csc(); + csc.BuildEngine = new MockEngine(); + csc.Sources = new ITaskItem[] { new TaskItem(codeFile) }; + csc.OutputAssembly = new TaskItem(outputFile); + csc.Platform = "x86"; + bool compileSucceeded = csc.Execute(); + + Assert.IsTrue(compileSucceeded); + + // Item1: appname + // Item2: command line + // Item3: number of times to launch + IList> toolsToLaunch = new List>(); + toolsToLaunch.Add(new Tuple(outputFile, outputFile + " 3 " + tempFilePath, 3)); + + // Item1: FileTracker context name + // Item2: Tuple as described above + IList>>> contextSpecifications = new List>>>(); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest", toolsToLaunch)); + contextSpecifications.Add(new Tuple>>("ProcessLaunchTest2", toolsToLaunch)); + + // Item1: tlog pattern + // Item2: # times it's expected to appear + IList> tlogPatterns = new List>(); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-FindstrLauncher-findstr*tlog", 3)); + tlogPatterns.Add(new Tuple("ProcessLaunchTest-FindstrLauncher.*-findstr*tlog", 6)); + tlogPatterns.Add(new Tuple("ProcessLaunchTest2-FindstrLauncher-findstr*tlog", 3)); + tlogPatterns.Add(new Tuple("ProcessLaunchTest2-FindstrLauncher.*-findstr*tlog", 6)); + + LaunchDuplicateToolsAndVerifyTlogExistsForEach(testDir, contextSpecifications, tlogPatterns, createTestDirectory: false); + } + finally + { + if (FileUtilities.DirectoryExistsNoThrow(testDir)) + { + FileUtilities.DeleteDirectoryNoThrow(testDir, true); + } + } + } + + private void InProcTrackingSpawnsToolWithTracker(bool useTrackerResponseFile) + { + const string testInFile = "test.in"; + const string testInFileContent = "foo"; + const string tool = "findstr"; + const string toolReadTlog = tool + ".read.1.tlog"; + const string rootingMarker = "jibbit goo"; + const string inprocTrackingContext = "Context1"; + const string tlogRootName = "foo_inline"; + const string sourceFile = "inlinetrackingtest.txt"; + const string trackerResponseFile = "test-tracker.rsp"; + const string fileTrackerParameters = "/d FileTracker.dll /r \"" + rootingMarker + "\""; + + File.Delete(toolReadTlog); + File.Delete(sourceFile); + FileTrackerTestHelper.WriteAll(testInFile, testInFileContent); + FileTrackerTestHelper.WriteAll(trackerResponseFile, fileTrackerParameters); + + try + { + FileTracker.StartTrackingContext(Path.GetFullPath("."), inprocTrackingContext); + File.WriteAllText(sourceFile, "this is a inline tracking test"); + + string firstParameters = useTrackerResponseFile ? "@\"" + trackerResponseFile + "\"" : fileTrackerParameters; + int exit = FileTrackerTestHelper.RunCommand( + s_defaultTrackerPath, + string.Format( + CultureInfo.CurrentCulture, + "{0} /c {1} /ip {2} {3}", + firstParameters, + tool, + testInFileContent, + testInFile)); + + Assert.AreEqual(0, exit); + Assert.AreEqual("^" + rootingMarker.ToUpperInvariant(), + FileTrackerTestHelper.ReadLineFromFile(toolReadTlog, 1).ToUpperInvariant()); + FileTrackerTestHelper.AssertFoundStringInTLog(Path.GetFullPath(testInFile).ToUpperInvariant(), toolReadTlog); + + FileTracker.WriteContextTLogs(Path.GetFullPath("."), tlogRootName); + Assert.AreEqual(Path.GetFullPath(sourceFile).ToUpperInvariant(), + FileTrackerTestHelper.ReadLineFromFile(tlogRootName + ".write.1.tlog", 1).ToUpperInvariant()); + } + finally + { + File.Delete(trackerResponseFile); + File.Delete(testInFile); + File.Delete(sourceFile); + File.Delete(tlogRootName + ".write.1.tlog"); + File.Delete(tlogRootName + ".read.1.tlog"); + File.Delete(tool + ".read.1.tlog"); + File.Delete(inprocTrackingContext + "-Tracker.read.1.tlog"); + FileTracker.StopTrackingAndCleanup(); + } + } + + private void LaunchDuplicateToolsAndVerifyTlogExistsForEach(string tlogPath, IList>>> contextSpecifications, IList> tlogPatterns) + { + LaunchDuplicateToolsAndVerifyTlogExistsForEach(tlogPath, contextSpecifications, tlogPatterns, createTestDirectory: true); + } + + private void LaunchDuplicateToolsAndVerifyTlogExistsForEach(string tlogPath, IList>>> contextSpecifications, IList> tlogPatterns, bool createTestDirectory) + { + try + { + if (createTestDirectory) + { + if (FileUtilities.DirectoryExistsNoThrow(tlogPath)) + { + FileUtilities.DeleteDirectoryNoThrow(tlogPath, true); + } + + Directory.CreateDirectory(tlogPath); + } + + BackEndNativeMethods.STARTUP_INFO startInfo = new BackEndNativeMethods.STARTUP_INFO(); + startInfo.cb = Marshal.SizeOf(startInfo); + uint dwCreationFlags = BackEndNativeMethods.NORMALPRIORITYCLASS; + + startInfo.hStdError = BackEndNativeMethods.InvalidHandle; + startInfo.hStdInput = BackEndNativeMethods.InvalidHandle; + startInfo.hStdOutput = BackEndNativeMethods.InvalidHandle; + startInfo.dwFlags = BackEndNativeMethods.STARTFUSESTDHANDLES; + dwCreationFlags = dwCreationFlags | BackEndNativeMethods.CREATENOWINDOW; + + BackEndNativeMethods.SECURITY_ATTRIBUTES pSec = new BackEndNativeMethods.SECURITY_ATTRIBUTES(); + BackEndNativeMethods.SECURITY_ATTRIBUTES tSec = new BackEndNativeMethods.SECURITY_ATTRIBUTES(); + pSec.nLength = Marshal.SizeOf(pSec); + tSec.nLength = Marshal.SizeOf(tSec); + + BackEndNativeMethods.PROCESS_INFORMATION pInfo = new BackEndNativeMethods.PROCESS_INFORMATION(); + + foreach (var specification in contextSpecifications) + { + // Item1: FileTracker context name + // Item2: Tuple as described below + FileTracker.StartTrackingContext(tlogPath, specification.Item1); + + foreach (var processSpecification in specification.Item2) + { + // Item1: appname + // Item2: command line + // Item3: number of times to launch + for (int i = 0; i < processSpecification.Item3; i++) + { + BackEndNativeMethods.CreateProcess(processSpecification.Item1, processSpecification.Item2, + ref pSec, ref tSec, + false, dwCreationFlags, + BackEndNativeMethods.NullPtr, null, ref startInfo, out pInfo); + } + } + + FileTracker.WriteContextTLogs(tlogPath, specification.Item1); + FileTracker.StopTrackingAndCleanup(); + } + + int tlogCount = 0; + foreach (Tuple pattern in tlogPatterns) + { + tlogCount += pattern.Item2; + } + + // make sure the disk write gets time for NTFS to recognize its existence. Estimate time needed to sleep based + // roughly on the number of tlogs that we're looking for (presumably roughly proportional to the number of tlogs + // being written. + Thread.Sleep(Math.Max(200, 250 * tlogCount)); + + // Item1: The pattern the tlog name should follow + // Item2: The number of tlogs following that pattern that should exist in the output directory + foreach (Tuple pattern in tlogPatterns) + { + string[] tlogNames = Directory.GetFiles(tlogPath, pattern.Item1, SearchOption.TopDirectoryOnly); + + Assert.AreEqual(pattern.Item2, tlogNames.Length); + } + } + finally + { + if (FileUtilities.DirectoryExistsNoThrow(tlogPath)) + { + FileUtilities.DeleteDirectoryNoThrow(tlogPath, true); + } + } + } + } + + internal class FileTrackerTestHelper + { + public static int RunCommand(string command, string arguments) + { + int exitCode = 0; + string throwawayLog = null; + + exitCode = RunCommandWithOptions(command, arguments, true /* print stdout & stderr */, out throwawayLog); + return exitCode; + } + + public static int RunCommandNoStdOut(string command, string arguments) + { + int exitCode = 0; + string throwawayLog = null; + + exitCode = RunCommandWithOptions(command, arguments, false /* don't print stdout & stderr */, out throwawayLog); + return exitCode; + } + + public static int RunCommandWithLog(string command, string arguments, out string outputAsLog) + { + int exitCode = 0; + + exitCode = RunCommandWithOptions(command, arguments, true /* print stdout & stderr */, out outputAsLog); + return exitCode; + } + + private static int RunCommandWithOptions(string command, string arguments, bool printOutput, out string outputAsLog) + { + outputAsLog = null; + ProcessStartInfo si = new ProcessStartInfo(command, arguments); + if (printOutput) + { + si.RedirectStandardOutput = true; + si.RedirectStandardError = true; + } + + si.UseShellExecute = false; + si.CreateNoWindow = true; + Process p = Process.Start(si); + p.WaitForExit(); + + if (printOutput) + { + outputAsLog = "StdOut: \n" + p.StandardOutput.ReadToEnd() + "\nStdErr: \n" + p.StandardError.ReadToEnd(); + Console.Write(outputAsLog); + } + + return p.ExitCode; + } + + public static string ReadLineFromFile(string filename, int linenumber) + { + string[] lines = File.ReadAllLines(filename); + return lines[linenumber]; + } + + public static string[] ReadLinesFromFile(string filename) + { + string[] lines = File.ReadAllLines(filename); + return lines; + } + + public static void CleanTlogs() + { + string[] tlogFiles = Directory.GetFiles(".", "*.tlog", SearchOption.AllDirectories); + + foreach (string file in tlogFiles) + { + File.Delete(file); + } + + File.Delete("test.in"); + File.Delete("t\u1EBCst.in"); + File.Delete("test.out"); + + if (Directory.Exists("outdir")) + { + Directory.Delete("outdir", true); + } + } + + public static void WriteAll(string filename, string content) + { + File.WriteAllText(filename, content); + } + + + public static bool FindStringInTlog(string file, string tlog) + { + return FileTrackerTestHelper.ReadLinesFromFile(tlog).ToList().Contains(file, StringComparer.OrdinalIgnoreCase); + } + + public static void AssertDidntFindStringInTLog(string file, string tlog) + { + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlog); + + for (int i = 0; i < lines.Length; i++) + { + if (file.Equals(lines[i], StringComparison.OrdinalIgnoreCase)) + { + Assert.Fail("Found string '" + file + "' in '" + tlog + "' at line " + i + ", when it shouldn't have been in the log at all."); + } + } + } + + public static void AssertFoundStringInTLog(string file, string tlog, int timesFound) + { + int timesFoundSoFar = 0; + string[] lines = FileTrackerTestHelper.ReadLinesFromFile(tlog); + + foreach (string line in lines) + { + if (file.Equals(line, StringComparison.OrdinalIgnoreCase)) + { + timesFoundSoFar++; + + if (timesFoundSoFar == timesFound) + { + break; + } + } + } + + if (timesFound != timesFoundSoFar) + { + Assert.Fail("Searched " + tlog + " but didn't find " + timesFound + " instances of " + file); + } + } + + public static void AssertFoundStringInTLog(string file, string tlog) + { + AssertFoundStringInTLog(file, tlog, 1); + } + } +} diff --git a/src/Utilities/UnitTests/TrackedDependencies/MockEngine.cs b/src/Utilities/UnitTests/TrackedDependencies/MockEngine.cs new file mode 100644 index 00000000000..6aef7a74686 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/MockEngine.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests.TrackedDependencies +{ + /*************************************************************************** + * + * Class: MockEngine + * + * In order to execute tasks, we have to pass in an Engine object, so the + * task can log events. It doesn't have to be the real Engine object, just + * something that implements the IBuildEngine2 interface. So, we mock up + * a fake engine object here, so we're able to execute tasks from the unit tests. + * + * The unit tests could have instantiated the real Engine object, but then + * we would have had to take a reference onto the Microsoft.Build.Engine assembly, which + * is somewhat of a no-no for task assemblies. + * + **************************************************************************/ +#if WHIDBEY_BUILD + sealed internal class MockEngine : IBuildEngine +#else + sealed internal class MockEngine : IBuildEngine2 +#endif + { + private bool _isRunningMultipleNodes; + private int _messages = 0; + private int _warnings = 0; + private int _errors = 0; + private string _log = ""; + private string _upperLog = null; + + internal int Messages + { + set { _messages = value; } + get { return _messages; } + } + + internal int Warnings + { + set { _warnings = value; } + get { return _warnings; } + } + + internal int Errors + { + set { _errors = value; } + get { return _errors; } + } + + + public void LogErrorEvent(BuildErrorEventArgs eventArgs) + { + if (eventArgs.File != null && eventArgs.File.Length > 0) + { + Console.Write("{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber); + } + + Console.Write("ERROR: "); + _log += "ERROR: "; + Console.Write("ERROR " + eventArgs.Code + ": "); + _log += "ERROR " + eventArgs.Code + ": "; + ++_errors; + + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + } + + public void LogWarningEvent(BuildWarningEventArgs eventArgs) + { + if (eventArgs.File != null && eventArgs.File.Length > 0) + { + Console.Write("{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber); + } + + Console.Write("WARNING " + eventArgs.Code + ": "); + _log += "WARNING " + eventArgs.Code + ": "; + ++_warnings; + + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + } + + public void LogCustomEvent(CustomBuildEventArgs eventArgs) + { + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + } + + public void LogMessageEvent(BuildMessageEventArgs eventArgs) + { + Console.WriteLine(eventArgs.Message); + _log += eventArgs.Message; + _log += "\n"; + ++_messages; + } + + internal string Log + { + set { _log = value; } + get { return _log; } + } + + public bool ContinueOnError + { + get + { + return false; + } + } + + public string ProjectFileOfTaskNode + { + get + { + return String.Empty; + } + } + + public int LineNumberOfTaskNode + { + get + { + return 0; + } + } + + public int ColumnNumberOfTaskNode + { + get + { + return 0; + } + } + + public bool IsRunningMultipleNodes + { + get { return _isRunningMultipleNodes; } + set { _isRunningMultipleNodes = value; } + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs, + string toolsVersion + ) + { + return false; + } + + public bool BuildProjectFile + ( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs + ) + { + return false; + } + + public bool BuildProjectFilesInParallel + ( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IDictionary[] targetOutputsPerProject, + string[] toolsVersion, + bool useResultsCache, + bool unloadProjectsOnCompletion + ) + { + return false; + } + + /// + /// Assert that the log file contains the given string. + /// Case insensitive. + /// + /// + internal void AssertLogContains(string contains) + { + if (_upperLog == null) + { + _upperLog = _log; + _upperLog = _upperLog.ToUpperInvariant(); + } + + Assert.IsTrue + ( + _upperLog.Contains + ( + contains.ToUpperInvariant() + ) + ); + } + + /// + /// Assert that the log doesnt contain the given string. + /// + /// + internal void AssertLogDoesntContain(string contains) + { + if (_upperLog == null) + { + _upperLog = _log; + _upperLog = _upperLog.ToUpperInvariant(); + } + + Assert.IsTrue + ( + !_upperLog.Contains + ( + contains.ToUpperInvariant() + ) + ); + } + } +} diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.cpp b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.cpp new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.cpp @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.tst b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.tst new file mode 100644 index 00000000000..eeace3b4cf9 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one.tst @@ -0,0 +1 @@ +Test diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one1.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one1.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one1.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one2.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one2.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one2.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one3.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one3.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/one3.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two.cpp b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two.cpp new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two.cpp @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two1.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two1.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two1.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two2.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two2.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two2.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two3.h b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two3.h new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TestFiles/two3.h @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Utilities/UnitTests/TrackedDependencies/TrackedDependenciesTests.cs b/src/Utilities/UnitTests/TrackedDependencies/TrackedDependenciesTests.cs new file mode 100644 index 00000000000..78350c89bb9 --- /dev/null +++ b/src/Utilities/UnitTests/TrackedDependencies/TrackedDependenciesTests.cs @@ -0,0 +1,3729 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; +using System.Collections; +using System.Resources; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests.TrackedDependencies +{ + [TestClass] + sealed public class TrackedDependenciesTests + { + private const int sleepTimeMilliseconds = 100; + + [TestInitialize] + public void SetupTestEnvironment() + { + string tempPath = Path.GetTempPath(); + string tempTestFilesPath = Path.Combine(tempPath, "TestFiles"); + + if (Directory.Exists("TestFiles")) + { + for (int i = 0; i < 5; i++) + { + try + { + Directory.Delete("TestFiles", true /* recursive */); + break; + } + catch (Exception) + { + Thread.Sleep(1000); + // Eat exceptions from the delete + } + } + } + + if (Directory.Exists(tempTestFilesPath)) + { + for (int i = 0; i < 5; i++) + { + try + { + Directory.Delete(tempTestFilesPath, true /* recursive */); + break; + } + catch (Exception) + { + Thread.Sleep(1000); + // Eat exceptions from the delete + } + } + } + + Directory.CreateDirectory(tempTestFilesPath); + Directory.CreateDirectory("TestFiles"); + + // Sleep for a period before each test is run so that + // there is enough time for files to have distinct + // last modified times - this ensures that the tracking + // dependency caching of tracking logs (which is based on + // last write time) can be relied upon + Thread.Sleep(sleepTimeMilliseconds); + } + + /// + /// Tests DependencyTableCache.FormatNormalizedTlogRootingMarker, which should do effectively the same + /// thing as FileTracker.FormatRootingMarker, except with some extra initial normalization to get rid of + /// pesky PIDs and TIDs in the tlog names. + /// + [TestMethod] + public void FormatNormalizedRootingMarkerTests() + { + Dictionary tests = new Dictionary(); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.9999-cvtres.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID]-cvtres.write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.0000-cvtres.read.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID]-cvtres.read.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.4567-cvtres.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID]-cvtres.write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.9999.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID].write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.0000.read.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID].read.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.4567.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID].write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link2345.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link2345.write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("link.4567.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "link.[ID].write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\a.1234.b\\link.4567.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\a.1234.b\\link.[ID].write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("link.write.tlog") }, + Path.Combine(Environment.CurrentDirectory, "link.write.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("link%20with%20spaces.write.3.tlog") }, + Path.Combine(Environment.CurrentDirectory, "link with spaces.write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[2] { new TaskItem("link.write.tlog"), new TaskItem("Debug\\link2345.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link2345.write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "link.write.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("link.write.tlog1234") }, + Path.Combine(Environment.CurrentDirectory, "link.write.tlog1234").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("1234link.write.tlog") }, + Path.Combine(Environment.CurrentDirectory, "1234link.write.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("link-1234.write.tlog") }, + Path.Combine(Environment.CurrentDirectory, "link-1234.write.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("C:\\Debug\\a.1234.b\\link.4567.write.1.tlog") }, + "C:\\DEBUG\\A.1234.B\\LINK.[ID].WRITE.[ID].TLOG" + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("a\\") }, + Path.Combine(Environment.CurrentDirectory, "a\\").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.45\\67.write.1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.45\\67.write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("Debug\\link.4567.write.1.tlog\\") }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.4567.write.1.tlog\\").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[0] { }, + "" + ); + tests.Add + ( + new ITaskItem[3] + { + new TaskItem("Debug\\link.write.1.tlog"), + new TaskItem("Debug\\link.2345.write.1.tlog"), + new TaskItem("Debug\\link.2345-cvtres.6789-mspdbsrv.1111.write.4.tlog") + }, + Path.Combine(Environment.CurrentDirectory, "Debug\\link.write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID]-cvtres.[ID]-mspdbsrv.[ID].write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "Debug\\link.[ID].write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[3] { new TaskItem("link.1234-write.1.tlog"), new TaskItem("link.1234-write.3.tlog"), new TaskItem("cl.write.2.tlog") }, + Path.Combine(Environment.CurrentDirectory, "cl.write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "link.[ID]-write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[3] { new TaskItem("lINk.1234-write.1.tlog"), new TaskItem("link.1234-WRitE.3.tlog"), new TaskItem("cl.write.2.tlog") }, + Path.Combine(Environment.CurrentDirectory, "cl.write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "link.[ID]-write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[3] { new TaskItem("a\\link.1234-write.1.tlog"), new TaskItem("b\\link.1234-write.3.tlog"), new TaskItem("cl.write.2.tlog") }, + Path.Combine(Environment.CurrentDirectory, "a\\link.[ID]-write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "b\\link.[ID]-write.[ID].tlog").ToUpperInvariant() + "|" + + Path.Combine(Environment.CurrentDirectory, "cl.write.[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("foo\\.tlog") }, + Path.Combine(Environment.CurrentDirectory, "foo\\.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("foo\\1.tlog") }, + Path.Combine(Environment.CurrentDirectory, "foo\\1.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("\\1.tlog") }, + Path.Combine(Path.GetPathRoot(Environment.CurrentDirectory), "1.tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem(".1.tlog") }, + Path.Combine(Environment.CurrentDirectory, ".[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("-2") }, + Path.Combine(Environment.CurrentDirectory, "-2").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem(".2") }, + Path.Combine(Environment.CurrentDirectory, ".2").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("2-") }, + Path.Combine(Environment.CurrentDirectory, "2-").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("2.") }, + Path.Combine(Environment.CurrentDirectory, "2").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("\\.1.tlog") }, + Path.Combine(Path.GetPathRoot(Environment.CurrentDirectory), ".[ID].tlog").ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("\\") }, + Path.GetPathRoot(Environment.CurrentDirectory).ToUpperInvariant() + ); + tests.Add + ( + new ITaskItem[1] { new TaskItem("\\\\share\\foo.read.8.tlog") }, + "\\\\share\\foo.read.[ID].tlog".ToUpperInvariant() + ); + foreach (KeyValuePair test in tests) + { + Assert.AreEqual(test.Value, DependencyTableCache.FormatNormalizedTlogRootingMarker(test.Key), "Incorrectly formatted rooting marker"); + } + + bool exceptionCaught = false; + try + { + DependencyTableCache.FormatNormalizedTlogRootingMarker(new ITaskItem[1] { new TaskItem("\\\\") }); + } + catch (ArgumentException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught, "Should have failed to format a rooting marker from a malformed UNC path"); + } + + [TestMethod] + public void CreateTrackedDependencies() + { + Console.WriteLine("Test: CreateTrackedDependencies"); + ITaskItem[] sources = null; + ITaskItem[] outputs = null; + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + null, + sources, + null, + outputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + Assert.IsNotNull(d); + } + + [TestMethod] + public void SingleCanonicalCL() + { + Console.WriteLine("Test: SingleCanonicalCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\oNe.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void NonExistentTlog() + { + Console.WriteLine("Test: NonExistentTlog"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + // Just to be sure, delete the test tlog. + File.Delete("TestFiles\\one.tlog"); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void EmptyTLog() + { + Console.WriteLine("Test: EmptyTLog"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlog", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void InvalidReadTLogName() + { + Console.WriteLine("Test: InvalidReadTLogName"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlog", ""); + + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\|one|.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have an error."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void ReadTLogWithInitialEmptyLine() + { + Console.WriteLine("Test: ReadTLogWithInitialEmptyLine"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "", "^FOO" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void ReadTLogWithEmptyLineImmediatelyAfterRoot() + { + Console.WriteLine("Test: ReadTLogWithEmptyLineImmediatelyAfterRoot"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^FOO", "", "FOO" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void ReadTLogWithEmptyLineBetweenRoots() + { + Console.WriteLine("Test: ReadTLogWithEmptyLineImmediatelyAfterRoot"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^FOO", "FOO", "", "^BAR", "BAR" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void ReadTLogWithEmptyRoot() + { + Console.WriteLine("Test: ReadTLogWithEmptyRoot"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^", "FOO" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void ReadTLogWithDuplicateInRoot() + { + Console.WriteLine("Test: ReadTLogWithDuplicateInRoot"); + + //Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\foo.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + ITaskItem[] sources = new ITaskItem[] { new TaskItem("TestFiles\\foo.cpp"), new TaskItem("TestFiles\\foo.cpp") }; + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^TestFiles\\foo.cpp|TestFiles\\foo.cpp", "TestFiles\\bar.cpp", "TestFiles\\foo.cpp" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + sources, + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.AreNotEqual(0, d.DependencyTable.Count, "Dependency Table should not be empty."); + } + + [TestMethod] + public void InvalidWriteTLogName() + { + Console.WriteLine("Test: InvalidWriteTLogName"); + + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\|one|.write.tlog")) + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have an error."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void WriteTLogWithInitialEmptyLine() + { + Console.WriteLine("Test: WriteTLogWithInitialEmptyLine"); + + // Prepare files + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { "", "^FOO" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")) + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void WriteTLogWithEmptyLineImmediatelyAfterRoot() + { + Console.WriteLine("Test: ReadTLogWithEmptyLineImmediatelyAfterRoot"); + + // Prepare files + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { "^FOO", "", "FOO" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")) + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void WriteTLogWithEmptyLineBetweenRoots() + { + Console.WriteLine("Test: WriteTLogWithEmptyLineImmediatelyAfterRoot"); + + // Prepare files + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { "^FOO", "FOO", "", "^BAR", "BAR" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")) + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void WriteTLogWithEmptyRoot() + { + Console.WriteLine("Test: WriteTLogWithEmptyRoot"); + + // Prepare files + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { "^", "FOO" }); + MockTask task = DependencyTestHelper.MockTask; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")) + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, d.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void PrimarySourceNotInTlog() + { + Console.WriteLine("Test: PrimarySourceNotInTlog"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + // Primary Source; not appearing in this Tlog.. + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\foo.cpp"), + Path.GetFullPath("TestFiles\\foo.h"), + }); + + // Touch the obj - normally this would mean uptodate, but since there + // is no tlog entry for the primary source, we want a rebuild of it. + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleCanonicalCL() + { + Console.WriteLine("Test: MultipleCanonicalCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleCanonicalCLCompactMissingOnSuccess() + { + Console.WriteLine("Test: MultipleCanonicalCLCompactMissingOnSuccess"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + Path.GetFullPath("TestFiles\\sometempfile.obj") + }); + + CanonicalTrackedOutputFiles compactOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + compactOutputs.RemoveDependenciesFromEntryIfMissing(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + compactOutputs.SaveTlog(); + + // Compact the read tlog + CanonicalTrackedInputFiles compactInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + compactOutputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + compactInputs.RemoveDependenciesFromEntryIfMissing(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + compactInputs.SaveTlog(); + + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + outputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 0); + } + + [TestMethod] + public void MultipleCanonicalCLCompactMissingOnSuccessMultiEntry() + { + Console.WriteLine("Test: MultipleCanonicalCLCompactMissingOnSuccessMultiEntry"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\two2.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + Path.GetFullPath("TestFiles\\sometempfile.obj"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\sometempfile2.obj") + }); + + CanonicalTrackedOutputFiles compactOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + compactOutputs.RemoveDependenciesFromEntryIfMissing(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + compactOutputs.SaveTlog(); + // Compact the read tlog + CanonicalTrackedInputFiles compactInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + compactOutputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + compactInputs.RemoveDependenciesFromEntryIfMissing(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + compactInputs.SaveTlog(); + + CanonicalTrackedOutputFiles writtenOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + CanonicalTrackedInputFiles writtenInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + writtenOutputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.AreEqual(1, writtenOutputs.DependencyTable[Path.GetFullPath("TestFiles\\one.cpp")].Count); + Assert.AreEqual(4, writtenInputs.DependencyTable[Path.GetFullPath("TestFiles\\one.cpp")].Count); + // Everything to do with two.cpp should be left intact + Assert.AreEqual(2, writtenOutputs.DependencyTable[Path.GetFullPath("TestFiles\\two.cpp")].Count); + Assert.AreEqual(3, writtenInputs.DependencyTable[Path.GetFullPath("TestFiles\\two.cpp")].Count); + } + + [TestMethod] + public void RemoveDependencyFromEntry() + { + Console.WriteLine("Test: RemoveDependencyFromEntry"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlh", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tli", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + Path.GetFullPath("TestFiles\\one3.obj"), + Path.GetFullPath("TestFiles\\one3.tlh"), + Path.GetFullPath("TestFiles\\one3.tli"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + Path.GetFullPath("TestFiles\\one3.obj"), + Path.GetFullPath("TestFiles\\one3.tlh"), + Path.GetFullPath("TestFiles\\one3.tli"), + }); + + CanonicalTrackedOutputFiles compactOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + compactOutputs.RemoveDependencyFromEntry(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\one3.obj"))); + compactOutputs.SaveTlog(); + + CanonicalTrackedOutputFiles writtenOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + Assert.IsFalse(writtenOutputs.DependencyTable[Path.GetFullPath("TestFiles\\one.cpp")].ContainsKey(Path.GetFullPath("TestFiles\\one3.obj"))); + + CanonicalTrackedInputFiles compactInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + compactOutputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + compactInputs.RemoveDependencyFromEntry(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\one3.obj"))); + compactInputs.SaveTlog(); + + CanonicalTrackedInputFiles writtenInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + writtenOutputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + Assert.IsFalse(writtenInputs.DependencyTable[Path.GetFullPath("TestFiles\\one.cpp")].ContainsKey(Path.GetFullPath("TestFiles\\one3.obj"))); + } + + [TestMethod] + public void RemoveDependencyFromEntries() + { + Console.WriteLine("Test: RemoveDependencyFromEntry"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlh", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tli", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + + string rootingMarker = Path.GetFullPath("TestFiles\\one.cpp") + "|" + Path.GetFullPath("TestFiles\\three.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp"); + + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + rootingMarker, + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + Path.GetFullPath("TestFiles\\one3.obj"), + Path.GetFullPath("TestFiles\\one3.tlh"), + Path.GetFullPath("TestFiles\\one3.tli"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + rootingMarker, + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + Path.GetFullPath("TestFiles\\one3.obj"), + Path.GetFullPath("TestFiles\\one3.tlh"), + Path.GetFullPath("TestFiles\\one3.tli"), + }); + + CanonicalTrackedOutputFiles compactOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + compactOutputs.RemoveDependencyFromEntry(new TaskItem[] { new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\three.cpp")) }, new TaskItem(Path.GetFullPath("TestFiles\\one3.obj"))); + compactOutputs.SaveTlog(); + + CanonicalTrackedOutputFiles writtenOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + Assert.IsFalse(writtenOutputs.DependencyTable[rootingMarker].ContainsKey(Path.GetFullPath("TestFiles\\one3.obj"))); + + CanonicalTrackedInputFiles compactInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + new TaskItem[] { new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\three.cpp")) }, + null, + compactOutputs, + false, /* no minimal rebuild optimization */ + true /* shred composite rooting markers */ + ); + + compactInputs.RemoveDependencyFromEntry(new TaskItem[] { new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\three.cpp")) }, new TaskItem(Path.GetFullPath("TestFiles\\one3.obj"))); + compactInputs.SaveTlog(); + + CanonicalTrackedInputFiles writtenInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + new TaskItem[] { new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), new TaskItem(Path.GetFullPath("TestFiles\\three.cpp")) }, + null, + writtenOutputs, + false, /* no minimal rebuild optimization */ + true /* shred composite rooting markers */ + ); + + Assert.IsFalse(writtenInputs.DependencyTable[rootingMarker].ContainsKey(Path.GetFullPath("TestFiles\\one3.obj"))); + } + + [TestMethod] + public void RemoveRootsWithSharedOutputs() + { + Console.WriteLine("Test: RemoveRootsWithSharedOutputs"); + + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlh", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tli", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + + string rootingMarker1 = Path.GetFullPath("TestFiles\\one.cpp") + "|" + Path.GetFullPath("TestFiles\\three.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp"); + string rootingMarker2 = Path.GetFullPath("TestFiles\\one.cpp") + "|" + Path.GetFullPath("TestFiles\\three.cpp"); + string rootingMarker3 = Path.GetFullPath("TestFiles\\one.cpp"); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + rootingMarker1.ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + rootingMarker2.ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one3.obj"), + Path.GetFullPath("TestFiles\\one3.tlh"), + Path.GetFullPath("TestFiles\\one3.tli"), + "^" + rootingMarker3.ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.obj"), + }); + + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker1)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker2)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker3)); + + outputs.RemoveRootsWithSharedOutputs(new ITaskItem[] { new TaskItem("TestFiles\\one.cpp"), new TaskItem("TestFiles\\three.cpp"), new TaskItem("TestFiles\\two.cpp") }); + + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker1)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker2)); + Assert.IsFalse(outputs.DependencyTable.ContainsKey(rootingMarker3)); + } + + [TestMethod] + public void RemoveRootsWithSharedOutputs_CurrentRootNotInTable() + { + Console.WriteLine("Test: RemoveRootsWithSharedOutputs"); + + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlh", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tli", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + + string rootingMarker1 = Path.GetFullPath("TestFiles\\one.cpp") + "|" + Path.GetFullPath("TestFiles\\three.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp"); + string rootingMarker2 = Path.GetFullPath("TestFiles\\one.cpp") + "|" + Path.GetFullPath("TestFiles\\three.cpp"); + string rootingMarker3 = Path.GetFullPath("TestFiles\\one.cpp"); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + rootingMarker1.ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + rootingMarker2.ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one3.obj"), + Path.GetFullPath("TestFiles\\one3.tlh"), + Path.GetFullPath("TestFiles\\one3.tli"), + "^" + rootingMarker3.ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.obj"), + }); + + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker1)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker2)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker3)); + + outputs.RemoveRootsWithSharedOutputs(new ITaskItem[] { new TaskItem("TestFiles\\four.cpp"), new TaskItem("TestFiles\\one.cpp"), new TaskItem("TestFiles\\three.cpp"), new TaskItem("TestFiles\\two.cpp") }); + + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker1)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker2)); + Assert.IsTrue(outputs.DependencyTable.ContainsKey(rootingMarker3)); + } + + [TestMethod] + public void MultipleCanonicalCLMissingDependency() + { + Console.WriteLine("Test: MultipleCanonicalCLMissingDependency"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Delete one of our dependencies + string missing = Path.GetFullPath("TestFiles\\one2.h"); + File.Delete(missing); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // We're out of date, since a missing dependency indicates out-of-dateness + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 1); + + // The dependency has been recorded and retrieved correctly + Assert.IsTrue(d.DependencyTable[Path.GetFullPath("TestFiles\\one.cpp")].ContainsKey(missing)); + + // Save out the compacted read log - our missing dependency will be compacted away + // The tlog will have to entries compacted, since we're not up to date + d.RemoveEntriesForSource(d.SourcesNeedingCompilation); + d.SaveTlog(); + + // read the tlog back in again + d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // We're out of date, since a missing dependency indicates out-of-dateness + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 1); + + // We have a source outstanding for recompilation, it will not appear in + // the tracking information as it will be written again + Assert.IsFalse(d.DependencyTable.ContainsKey(Path.GetFullPath("TestFiles\\one.cpp"))); + } + + [TestMethod] + public void MultipleCanonicalCLMissingOutputDependencyRemoved() + { + Console.WriteLine("Test: MultipleCanonicalCLMissingOutputDependencyRemoved"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\sometempfile2.obj") + }); + + string missing = Path.GetFullPath("TestFiles\\sometempfile2.obj"); + + CanonicalTrackedOutputFiles compactOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + // Save out the compacted read log - our missing dependency will be compacted away + // Use an anonymous method to encapsulate the contains check for the tlogs + compactOutputs.SaveTlog(delegate (string fullTrackedPath) + { + // We need to answer the question "should fullTrackedPath be included in the TLog?" + return (String.Compare(fullTrackedPath, missing, StringComparison.OrdinalIgnoreCase) != 0); + }); + + // Read the Tlogs back in.. + compactOutputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + // Compact the read tlog + CanonicalTrackedInputFiles compactInputs = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + new TaskItem[] { new TaskItem("TestFiles\\one.cpp"), new TaskItem("TestFiles\\two.cpp") }, + null, + compactOutputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + compactInputs.SaveTlog(); + + ITaskItem[] outofDate = compactInputs.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofDate.Length == 0); + } + + + [TestMethod] + public void MultipleCanonicalCLMissingInputDependencyRemoved() + { + Console.WriteLine("Test: MultipleCanonicalCLMissingInputDependencyRemoved"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Delete one of our dependencies + string missing = Path.GetFullPath("TestFiles\\one2.h"); + File.Delete(missing); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // We're out of date, since a missing dependency indicates out-of-dateness + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 1); + + // The dependency has been recorded and retrieved correctly + Assert.IsTrue(d.DependencyTable[Path.GetFullPath("TestFiles\\one.cpp")].ContainsKey(missing)); + + // Save out the compacted read log - our missing dependency will be compacted away + // Use an anonymous method to encapsulate the contains check for the tlogs + d.SaveTlog(delegate (string fullTrackedPath) + { + // We need to answer the question "should fullTrackedPath be included in the TLog?" + return (String.Compare(fullTrackedPath, missing, StringComparison.OrdinalIgnoreCase) != 0); + }); + + // read the tlog back in again + d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // We're not out of date, since the missing dependency has been removed + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 0); + } + + + [TestMethod] + public void MultiplePrimaryCanonicalCL() + { + Console.WriteLine("Test: MultiplePrimaryCanonicalCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + new ITaskItem[] { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + }, + null, + new ITaskItem[] { + new TaskItem("TestFiles\\one.obj"), + new TaskItem("TestFiles\\two.obj"), + }, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 2); + Assert.IsTrue((outofdate[0].ItemSpec == "TestFiles\\one.cpp" && outofdate[1].ItemSpec == "TestFiles\\two.cpp") || + (outofdate[1].ItemSpec == "TestFiles\\one.cpp" && outofdate[0].ItemSpec == "TestFiles\\two.cpp")); + } + + [TestMethod] + public void MultiplePrimaryCanonicalCLUnderTemp() + { + string currentDirectory = Directory.GetCurrentDirectory(); + string tempPath = Path.GetTempPath(); + + try + { + Directory.SetCurrentDirectory(tempPath); + + Console.WriteLine("Test: MultiplePrimaryCanonicalCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + new ITaskItem[] { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + }, + null, + new ITaskItem[] { + new TaskItem("TestFiles\\one.obj"), + new TaskItem("TestFiles\\two.obj"), + }, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 2); + Assert.IsTrue((outofdate[0].ItemSpec == "TestFiles\\one.cpp" && outofdate[1].ItemSpec == "TestFiles\\two.cpp") || + (outofdate[1].ItemSpec == "TestFiles\\one.cpp" && outofdate[0].ItemSpec == "TestFiles\\two.cpp")); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + } + + [TestMethod] + public void MultiplePrimaryCanonicalCLSharedDependency() + { + Console.WriteLine("Test: MultiplePrimaryCanonicalCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), // the shared dependency + Path.GetFullPath("TestFiles\\one3.h"), + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\one2.h"), // the shared dependency + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + new ITaskItem[] { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + }, + null, + new ITaskItem[] { + new TaskItem("TestFiles\\one.obj"), + new TaskItem("TestFiles\\two.obj"), + }, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 2); + Assert.IsTrue((outofdate[0].ItemSpec == "TestFiles\\one.cpp" && outofdate[1].ItemSpec == "TestFiles\\two.cpp") || + (outofdate[1].ItemSpec == "TestFiles\\one.cpp" && outofdate[0].ItemSpec == "TestFiles\\two.cpp")); + } + + [TestMethod] + public void MultipleCanonicalCLAcrossCommand1() + { + Console.WriteLine("Test: MultipleCanonicalCLAcrossCommand1"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + "#Command some-command1", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleCanonicalCLAcrossCommand2() + { + Console.WriteLine("Test: MultipleCanonicalCLAcrossCommand2"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + "#Command some-command1", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleCanonicalCLAcrossCommandNonDependency() + { + Console.WriteLine("Test: MultipleCanonicalCLAcrossCommandNonDependency"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\two.cpp"), // this root marker represents the end of the dependencies for one.cpp + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 0); + } + + [TestMethod] + public void MultipleCanonicalCLAcrossTlogs1() + { + Console.WriteLine("Test: MultipleCanonicalCLAcrossTlogs1"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + }); + + File.WriteAllLines("TestFiles\\one2.tlog", new string[] { + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog"), + new TaskItem("TestFiles\\one2.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleCanonicalCLAcrossTlogs2() + { + Console.WriteLine("Test: MultipleCanonicalCLAcrossTlogs2"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + }); + + File.WriteAllLines("TestFiles\\one2.tlog", new string[] { + "#Command some-command1", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog"), + new TaskItem("TestFiles\\one2.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void SingleRootedCL() + { + Console.WriteLine("Test: SingleRootedCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleRootedCLAcrossTlogs1() + { + Console.WriteLine("Test: MultipleRootedCLAcrossTlogs1"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + }); + + File.WriteAllLines("TestFiles\\one2.tlog", new string[] { + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog"), + new TaskItem("TestFiles\\one2.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + [TestMethod] + public void MultipleRootedCL() + { + Console.WriteLine("Test: MultipleRootedCL"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog"), + new TaskItem("TestFiles\\one2.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\two.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\two.cpp"); + } + + [TestMethod] + public void MultipleRootedCLNonDependency() + { + Console.WriteLine("Test: MultipleRootedCLNonDependency"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\two.cpp"), // this root marker represents the end of the dependencies for one.cpp + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 0); + } + + [TestMethod] + public void MultipleRootedCLAcrossTlogs2() + { + Console.WriteLine("Test: MultipleRootedCLAcrossTlogs2"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + }); + + File.WriteAllLines("TestFiles\\one2.tlog", new string[] { + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog"), + new TaskItem("TestFiles\\one2.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + } + + + [TestMethod] + public void OutputSingleCanonicalCL() + { + Console.WriteLine("Test: OutputSingleCanonicalCL"); + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\oNe.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + + Assert.IsTrue(outputs.Length == 1); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + } + + [TestMethod] + public void OutputSingleCanonicalCLAcrossTlogs() + { + Console.WriteLine("Test: OutputSingleCanonicalCLAcrossTlogs"); + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\oNe.obj"), + }); + + File.WriteAllLines("TestFiles\\two.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.pch"), + }); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one.tlog"), + new TaskItem("TestFiles\\two.tlog") + }; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + tlogs); + + ITaskItem[] outputs = d.OutputsForSource(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + + Assert.IsTrue(outputs.Length == 2); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\one.pch")); + } + + [TestMethod] + public void OutputNonExistentTlog() + { + Console.WriteLine("Test: NonExistentTlog"); + + // Just to be sure, delete the test tlog. + File.Delete("TestFiles\\one.tlog"); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(new TaskItem(Path.GetFullPath("TestFiles\\one.cpp"))); + + Assert.IsTrue(outputs == null); + } + + [TestMethod] + public void OutputMultipleCanonicalCL() + { + Console.WriteLine("Test: OutputMultipleCanonicalCL"); + + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\oNe.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(sources); + + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + } + + [TestMethod] + public void OutputMultipleCanonicalCLSubrootMatch() + { + Console.WriteLine("Test: OutputMultipleCanonicalCLSubrootMatch"); + + // sources is a subset of source2 + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + ITaskItem[] sources2 = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\four.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\five.cpp"))}; + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\oNe.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + "^" + FileTracker.FormatRootingMarker(sources2), + Path.GetFullPath("TestFiles\\fOUr.obj"), + Path.GetFullPath("TestFiles\\fIve.obj"), + Path.GetFullPath("TestFiles\\sIx.obj"), + Path.GetFullPath("TestFiles\\sEvEn.obj"), + Path.GetFullPath("TestFiles\\EIght.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(sources2, /*searchForSubRootsInCompositeRootingMarkers*/ false); + + Assert.IsTrue(outputs.Length == 5); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\fOUr.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\fIve.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\sIx.obj")); + Assert.IsTrue(outputs[3].ItemSpec == Path.GetFullPath("TestFiles\\sEvEn.obj")); + Assert.IsTrue(outputs[4].ItemSpec == Path.GetFullPath("TestFiles\\EIght.obj")); + + ITaskItem[] outputs2 = d.OutputsForSource(sources2, /*searchForSubRootsInCompositeRootingMarkers*/ true); + + Assert.IsTrue(outputs2.Length == 8); + Assert.IsTrue(outputs2[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs2[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs2[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + Assert.IsTrue(outputs2[3].ItemSpec == Path.GetFullPath("TestFiles\\fOUr.obj")); + Assert.IsTrue(outputs2[4].ItemSpec == Path.GetFullPath("TestFiles\\fIve.obj")); + Assert.IsTrue(outputs2[5].ItemSpec == Path.GetFullPath("TestFiles\\sIx.obj")); + Assert.IsTrue(outputs2[6].ItemSpec == Path.GetFullPath("TestFiles\\sEvEn.obj")); + Assert.IsTrue(outputs2[7].ItemSpec == Path.GetFullPath("TestFiles\\EIght.obj")); + + // Test if sources can find the superset. + ITaskItem[] outputs3 = d.OutputsForSource(sources, /*searchForSubRootsInCompositeRootingMarkers*/ true); + + Assert.IsTrue(outputs3.Length == 8); + Assert.IsTrue(outputs3[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs3[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs3[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + Assert.IsTrue(outputs3[3].ItemSpec == Path.GetFullPath("TestFiles\\fOUr.obj")); + Assert.IsTrue(outputs3[4].ItemSpec == Path.GetFullPath("TestFiles\\fIve.obj")); + Assert.IsTrue(outputs3[5].ItemSpec == Path.GetFullPath("TestFiles\\sIx.obj")); + Assert.IsTrue(outputs3[6].ItemSpec == Path.GetFullPath("TestFiles\\sEvEn.obj")); + Assert.IsTrue(outputs3[7].ItemSpec == Path.GetFullPath("TestFiles\\EIght.obj")); + + ITaskItem[] outputs4 = d.OutputsForSource(sources, /*searchForSubRootsInCompositeRootingMarkers*/ false); + + Assert.IsTrue(outputs4.Length == 3); + Assert.IsTrue(outputs4[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs4[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs4[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + } + + [TestMethod] + public void OutputMultipleCanonicalCLSubrootMisMatch() + { + Console.WriteLine("Test: OutputMultipleCanonicalCLSubrootMisMatch"); + + // sources is NOT a subset of source + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + ITaskItem[] sources2 = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\four.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\five.cpp"))}; + ITaskItem[] sources2Match = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\four.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\five.cpp"))}; + ITaskItem[] sourcesPlusOne = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\eight.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + + // Do note sources2Match and source2 is missing three.cpp. It is to test if the RootContainsAllSubRootComponents can handle the case. + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\oNe.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + "^" + FileTracker.FormatRootingMarker(sources2), + Path.GetFullPath("TestFiles\\fOUr.obj"), + Path.GetFullPath("TestFiles\\fIve.obj"), + Path.GetFullPath("TestFiles\\sIx.obj"), + Path.GetFullPath("TestFiles\\sEvEn.obj"), + Path.GetFullPath("TestFiles\\EIght.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(sources2Match, /*searchForSubRootsInCompositeRootingMarkers*/ false); + + Assert.IsTrue(outputs.Length == 5); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\fOUr.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\fIve.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\sIx.obj")); + Assert.IsTrue(outputs[3].ItemSpec == Path.GetFullPath("TestFiles\\sEvEn.obj")); + Assert.IsTrue(outputs[4].ItemSpec == Path.GetFullPath("TestFiles\\EIght.obj")); + + ITaskItem[] outputs2 = d.OutputsForSource(sources2Match, /*searchForSubRootsInCompositeRootingMarkers*/ true); + + Assert.IsTrue(outputs2.Length == 5); + Assert.IsTrue(outputs2[0].ItemSpec == Path.GetFullPath("TestFiles\\fOUr.obj")); + Assert.IsTrue(outputs2[1].ItemSpec == Path.GetFullPath("TestFiles\\fIve.obj")); + Assert.IsTrue(outputs2[2].ItemSpec == Path.GetFullPath("TestFiles\\sIx.obj")); + Assert.IsTrue(outputs2[3].ItemSpec == Path.GetFullPath("TestFiles\\sEvEn.obj")); + Assert.IsTrue(outputs2[4].ItemSpec == Path.GetFullPath("TestFiles\\EIght.obj")); + + ITaskItem[] outputs3 = d.OutputsForSource(sourcesPlusOne, /*searchForSubRootsInCompositeRootingMarkers*/ true); + + Assert.IsTrue(outputs3.Length == 3); + Assert.IsTrue(outputs3[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs3[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs3[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + + ITaskItem[] outputs4 = d.OutputsForSource(sourcesPlusOne, /*searchForSubRootsInCompositeRootingMarkers*/ false); + + Assert.IsTrue(outputs4.Length == 0); + } + + [TestMethod] + public void OutputMultipleCanonicalCLLongTempPath() + { + Console.WriteLine("Test: OutputMultipleCanonicalCLLongTempPath"); + + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + + string oldTempPath = Environment.GetEnvironmentVariable("TEMP"); + string oldTmpPath = Environment.GetEnvironmentVariable("TMP"); + string newTempPath = Path.GetFullPath("TestFiles\\ThisIsAReallyVeryLongTemporaryPlace\\ThatIsLongerThanTheSourcePaths"); + + Directory.CreateDirectory(newTempPath); + Environment.SetEnvironmentVariable("TEMP", newTempPath); + Environment.SetEnvironmentVariable("TMP", newTempPath); + + Console.WriteLine("Test: OutputMultipleCanonicalCL"); + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\oNe.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(sources); + + Environment.SetEnvironmentVariable("TEMP", oldTempPath); + Environment.SetEnvironmentVariable("TMP", oldTmpPath); + + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + } + + [TestMethod] + public void OutputMultipleCanonicalCLAcrossTLogs() + { + Console.WriteLine("Test: OutputMultipleCanonicalCLAcrossTLogs"); + + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\oNe.obj"), + }); + + File.WriteAllLines("TestFiles\\two.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one.tlog"), + new TaskItem("TestFiles\\two.tlog") + }; + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + tlogs); + + ITaskItem[] outputs = d.OutputsForSource(sources); + + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + } + + [TestMethod] + public void OutputMultipleSingleSubRootCanonicalCL() + { + Console.WriteLine("Test: OutputMultipleSingleSubRootCanonicalCL"); + + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\oNe.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(new TaskItem(Path.GetFullPath("TestFiles\\two.cpp"))); + + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + } + + [TestMethod] + public void OutputMultipleUnrecognisedRootCanonicalCL() + { + Console.WriteLine("Test: OutputMultipleUnrecognisedRootCanonicalCL"); + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp") + "|" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\oNe.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog"))); + + ITaskItem[] outputs = d.OutputsForSource(new TaskItem(Path.GetFullPath("TestFiles\\four.cpp"))); + + Assert.IsTrue(outputs.Length == 0); + } + + [TestMethod] + public void OutputCLMinimalRebuildOptimization() + { + Console.WriteLine("Test: OutputCLMinimalRebuildOptimization"); + + // Prepare read tlog + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Our source files + ITaskItem[] sources = { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + new TaskItem("TestFiles\\three.cpp"), + }; + + // Prepare write tlog + // This includes individual output information for each root + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two.obj"), + "^" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\three.obj"), + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\one.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + + // Represent our tracked and computed outputs + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + // Represent our tracked and provided inputs + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + sources, + null, + outputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // First of all, all things should be up to date + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 0); + + // Delete one of the outputs in the group + File.Delete(Path.GetFullPath("TestFiles\\two.obj")); + + // With optimization off, all sources in the group will need compilation + d.SourcesNeedingCompilation = null; + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 3); + + // With optimization on, only the source that matches the output will need compilation + d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + sources, + null, + outputs, + true, /* enable minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 1); + // And the source is.. two.cpp! + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\two.cpp"); + } + + [TestMethod] + public void OutputCLMinimalRebuildOptimizationComputed() + { + Console.WriteLine("Test: OutputCLMinimalRebuildOptimizationComputed"); + + // Prepare read tlog + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Our source files + ITaskItem[] sources = { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + new TaskItem("TestFiles\\three.cpp"), + }; + + // Prepare write tlog + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + FileTracker.FormatRootingMarker(sources), + Path.GetFullPath("TestFiles\\one.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + + // Represent our tracked and computed outputs + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + // "Compute" the additional output information for this compilation, rather than them being tracked + outputs.AddComputedOutputForSourceRoot(Path.GetFullPath("TestFiles\\one.cpp"), Path.GetFullPath("TestFiles\\one.obj")); + outputs.AddComputedOutputForSourceRoot(Path.GetFullPath("TestFiles\\two.cpp"), Path.GetFullPath("TestFiles\\two.obj")); + outputs.AddComputedOutputForSourceRoot(Path.GetFullPath("TestFiles\\three.cpp"), Path.GetFullPath("TestFiles\\three.obj")); + + // Represent our tracked and provided inputs + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + sources, + null, + outputs, + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // First of all, all things should be up to date + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 0); + + // Delete one of the outputs in the group + File.Delete(Path.GetFullPath("TestFiles\\two.obj")); + + // With optimization off, all sources in the group will need compilation + d.SourcesNeedingCompilation = null; + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 3); + + // With optimization on, only the source that matches the output will need compilation + d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + sources, + null, + outputs, + true, /* enable minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 1); + // And the source is.. two.cpp! + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\two.cpp"); + } + + [TestMethod] + public void ReplaceOutputForSource() + { + Console.WriteLine("Test: ReplaceOutputForSource"); + + if (File.Exists(Path.GetFullPath("TestFiles\\three.i"))) + { + File.Delete(Path.GetFullPath("TestFiles\\three.i")); + } + + // Prepare read tlog + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + // Our source files + ITaskItem[] sources = { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + new TaskItem("TestFiles\\three.cpp"), + }; + + // Prepare write tlog + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two.obj"), + "^" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\three.obj"), + }); + + // Represent our tracked and computed outputs + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + // Change the output (note that this doesn't affect the timestamp) + File.Move(Path.GetFullPath("TestFiles\\three.obj"), Path.GetFullPath("TestFiles\\three.i")); + + string threeRootingMarker = FileTracker.FormatRootingMarker(new TaskItem("TestFiles\\three.cpp")); + // Remove the fact that three.obj was the tracked output + bool removed = outputs.RemoveOutputForSourceRoot(threeRootingMarker, Path.GetFullPath("TestFiles\\three.obj")); + Assert.IsTrue(removed); + // "Compute" the replacement output information for this compilation, rather than the one originally tracked + outputs.AddComputedOutputForSourceRoot(threeRootingMarker, Path.GetFullPath("TestFiles\\three.i")); + + // Represent our tracked and provided inputs + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + sources, + null, + outputs, + true, /* minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // We should have one output for three.cpp + Assert.AreEqual(1, outputs.DependencyTable[threeRootingMarker].Count); + Assert.AreEqual(false, outputs.DependencyTable[threeRootingMarker].ContainsKey(Path.GetFullPath("TestFiles\\three.obj"))); + + // All things should be up to date + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.AreEqual(0, outofdate.Length); + + // Delete the new output + File.Delete(Path.GetFullPath("TestFiles\\three.i")); + + // This means a recompile would be required for the roots + d.SourcesNeedingCompilation = null; + outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.IsTrue(outofdate.Length == 1); + } + + [TestMethod] + public void ExcludeSpecificDirectory() + { + Console.WriteLine("Test: ExcludeSpecificDirectory"); + + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.cpp", ""); + + Thread.Sleep(sleepTimeMilliseconds); + + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); + + Directory.CreateDirectory("TestFiles\\Foo"); + DependencyTestHelper.WriteAll("TestFiles\\Foo\\one2.h", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\Foo\\one2.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one3.h").ToUpperInvariant(), + "^" + Path.GetFullPath("TestFiles\\two.cpp").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\Foo\\one2.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one3.h").ToUpperInvariant(), + "^" + Path.GetFullPath("TestFiles\\three.cpp").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one1.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\Foo\\one2.h").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one3.h").ToUpperInvariant(), + }); + + // Our source files + ITaskItem[] sources = { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + new TaskItem("TestFiles\\three.cpp"), + }; + + // Prepare write tlog + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\one.obj").ToUpperInvariant(), + "^" + Path.GetFullPath("TestFiles\\two.cpp").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\two.obj").ToUpperInvariant(), + "^" + Path.GetFullPath("TestFiles\\three.cpp").ToUpperInvariant(), + Path.GetFullPath("TestFiles\\three.obj").ToUpperInvariant(), + }); + + // Represent our tracked and computed outputs + CanonicalTrackedOutputFiles outputs = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog"))); + + // Represent our tracked and provided inputs + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), + sources, + new TaskItem[] { new TaskItem(Path.GetFullPath("TeSTfiles\\Foo")) }, + outputs, + true, /* minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + // All things should be up to date + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + Assert.AreEqual(0, outofdate.Length); + } + + [TestMethod] + public void SaveCompactedReadTlog() + { + Console.WriteLine("Test: SaveCompactedReadTlog"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + }); + + File.WriteAllLines("TestFiles\\one2.tlog", new string[] { + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + File.WriteAllLines("TestFiles\\two1.tlog", new string[] { + "#Command some-command2", + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + // Touch one + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.tlog"), + new TaskItem("TestFiles\\one2.tlog"), + new TaskItem("TestFiles\\two1.tlog") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + + d.RemoveEntriesForSource(d.SourcesNeedingCompilation); + d.SaveTlog(); + + // All the tlogs need to still be there even after compaction + // It's OK for them to be empty, but their absence might mean a partial clean + // A missing tlog would mean a clean build + Assert.IsTrue(Microsoft.Build.Utilities.TrackedDependencies.ItemsExist(tlogs)); + + // There should be no difference in the out of date files after compaction + CanonicalTrackedInputFiles d1 = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + outofdate = d1.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 1); + Assert.IsTrue(outofdate[0].ItemSpec == "TestFiles\\one.cpp"); + + ITaskItem[] tlogs2 = { + tlogs[0] + }; + + // All log information should now be in the tlog[0] + CanonicalTrackedInputFiles d2 = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs2, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\two.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\two.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + outofdate = d2.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 0); + Assert.IsTrue(d2.DependencyTable.Count == 1); + Assert.IsFalse(d2.DependencyTable.ContainsKey(Path.GetFullPath("TestFiles\\one.cpp"))); + + // There should be no difference even if we send in all the original tlogs + CanonicalTrackedInputFiles d3 = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\two.cpp")), + null, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\two.obj")), + false, /* no minimal rebuild optimization */ + false /* shred composite rooting markers */ + ); + + outofdate = d3.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 0); + Assert.IsTrue(d3.DependencyTable.Count == 1); + Assert.IsFalse(d3.DependencyTable.ContainsKey(Path.GetFullPath("TestFiles\\one.cpp"))); + } + + [TestMethod] + public void SaveCompactedWriteTlog() + { + Console.WriteLine("Test: SaveCompactedWriteTlog"); + TaskItem fooItem = new TaskItem("foo"); + + ITaskItem[] sources = new TaskItem[] { + new TaskItem(Path.GetFullPath("TestFiles\\one.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\two.cpp")), + new TaskItem(Path.GetFullPath("TestFiles\\three.cpp"))}; + + string rootMarker = FileTracker.FormatRootingMarker(sources); + + // Prepare files + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + "^" + rootMarker, + Path.GetFullPath("TestFiles\\oNe.obj"), + "^" + fooItem.GetMetadata("Fullpath"), + Path.GetFullPath("TestFiles\\foo1.bar"), + Path.GetFullPath("TestFiles\\bar1.baz"), + }); + + File.WriteAllLines("TestFiles\\two.tlog", new string[] { + "#Command some-command", + "^" + rootMarker, + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\three.obj"), + "^" + fooItem.GetMetadata("Fullpath"), + Path.GetFullPath("TestFiles\\foo2.bar"), + Path.GetFullPath("TestFiles\\bar2.baz"), + }); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one.tlog"), + new TaskItem("TestFiles\\two.tlog") + }; + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + CanonicalTrackedOutputFiles d = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + tlogs); + + ITaskItem[] outputs = d.OutputsForSource(sources); + + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + + outputs = d.OutputsForSource(fooItem); + Assert.IsTrue(outputs.Length == 4); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\foo1.bar")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\bar1.baz")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\foo2.bar")); + Assert.IsTrue(outputs[3].ItemSpec == Path.GetFullPath("TestFiles\\bar2.baz")); + + // Compact the tlog removing all entries for "foo" leaving the other entries intact + d.RemoveEntriesForSource(fooItem); + d.SaveTlog(); + + // All the tlogs need to still be there even after compaction + // It's OK for them to be empty, but their absence might mean a partial clean + // A missing tlog would mean a clean build + Assert.IsTrue(Microsoft.Build.Utilities.TrackedDependencies.ItemsExist(tlogs)); + + // All log information should now be in the tlog[0] + ITaskItem[] tlogs2 = { + tlogs[0] + }; + + CanonicalTrackedOutputFiles d2 = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + tlogs2); + + outputs = d2.OutputsForSource(fooItem); + Assert.IsTrue(outputs.Length == 0); + + outputs = d2.OutputsForSource(sources); + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + + // There should be no difference even if we send in all the original tlogs + CanonicalTrackedOutputFiles d3 = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + tlogs); + + outputs = d3.OutputsForSource(fooItem); + Assert.IsTrue(outputs.Length == 0); + + outputs = d3.OutputsForSource(sources); + Assert.IsTrue(outputs.Length == 3); + Assert.IsTrue(outputs[0].ItemSpec == Path.GetFullPath("TestFiles\\oNe.obj")); + Assert.IsTrue(outputs[1].ItemSpec == Path.GetFullPath("TestFiles\\two.obj")); + Assert.IsTrue(outputs[2].ItemSpec == Path.GetFullPath("TestFiles\\three.obj")); + } + + /// + /// Make sure that the compacted read tlog contains the correct information when the composite rooting + /// markers are kept, as in the case where there is a many-to-one relationship between inputs and + /// outputs (ie. Lib, Link) + /// + [TestMethod] + public void SaveCompactedReadTlog_MaintainCompositeRootingMarkers() + { + Console.WriteLine("Test: SaveCompactedReadTlog_MaintainCompositeRootingMarkers"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\two3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\three1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\three2.h", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\three.cpp", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\twothree.obj", ""); + + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one1.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + }); + + File.WriteAllLines("TestFiles\\one2.read.tlog", new string[] { + "#Command some-command1", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + File.WriteAllLines("TestFiles\\two1.read.tlog", new string[] { + "#Command some-command2", + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + }); + + File.WriteAllLines("TestFiles\\three1.read.tlog", new string[] { + "#Command some-command2", + "^" + Path.GetFullPath("TestFiles\\three.cpp"), + Path.GetFullPath("TestFiles\\three1.h") + }); + + File.WriteAllLines("TestFiles\\twothree.read.tlog", new string[] { + "#Command some-command2", + "^" + Path.GetFullPath("TestFiles\\three.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two2.h"), + Path.GetFullPath("TestFiles\\two3.h"), + Path.GetFullPath("TestFiles\\three1.h"), + Path.GetFullPath("TestFiles\\three2.h") + }); + + ITaskItem[] tlogs = { + new TaskItem("TestFiles\\one1.read.tlog"), + new TaskItem("TestFiles\\one2.read.tlog"), + new TaskItem("TestFiles\\two1.read.tlog"), + new TaskItem("TestFiles\\three1.read.tlog"), + new TaskItem("TestFiles\\twothree.read.tlog") + }; + + ITaskItem[] inputs = { + new TaskItem("TestFiles\\one.cpp"), + new TaskItem("TestFiles\\two.cpp"), + new TaskItem("TestFiles\\three.cpp") + }; + + ITaskItem[] outputs = { + new TaskItem("TestFiles\\one.obj"), + new TaskItem("TestFiles\\twothree.obj") + }; + + CanonicalTrackedInputFiles d = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + inputs, + null, + outputs, + false, /* no minimal rebuild optimization */ + true /* keep composite rooting markers */ + ); + + ITaskItem[] outofdate = d.ComputeSourcesNeedingCompilation(); + + // nothing should be out of date + Assert.IsTrue(outofdate.Length == 0); + Assert.IsTrue(d.DependencyTable.Count == 4); + + // dependencies should include the three .h files written into the .tlogs + the rooting marker + Assert.IsTrue(d.DependencyTable[Path.GetFullPath("TestFiles\\three.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp")].Values.Count == 4); + + d.SaveTlog(); + + CanonicalTrackedInputFiles d2 = new CanonicalTrackedInputFiles + ( + DependencyTestHelper.MockTask, + tlogs, + inputs, + null, + outputs, + false, /* no minimal rebuild optimization */ + true /* keep composite rooting markers */ + ); + + ITaskItem[] outofdate2 = d2.ComputeSourcesNeedingCompilation(); + + Assert.IsTrue(outofdate.Length == 0); + Assert.IsTrue(d2.DependencyTable.Count == 4); + + // dependencies should include the three .h files written into the .tlogs + the two rooting marker files + Assert.IsTrue(d2.DependencyTable[Path.GetFullPath("TestFiles\\three.cpp") + "|" + Path.GetFullPath("TestFiles\\two.cpp")].Values.Count == 4); + } + + [TestMethod] + public void InvalidFlatTrackingTLogName() + { + Console.WriteLine("Test: InvalidFlatTrackingTLogName"); + + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.tlog", ""); + + MockTask task = DependencyTestHelper.MockTask; + FlatTrackingData data = new FlatTrackingData + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\|one|.write.tlog")), + false /* don't skip missing files */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, data.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void FlatTrackingTLogWithInitialEmptyLine() + { + Console.WriteLine("Test: FlatTrackingTLogWithInitialEmptyLine"); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "", "^FOO" }); + + MockTask task = DependencyTestHelper.MockTask; + FlatTrackingData data = new FlatTrackingData + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + false /* don't skip missing files */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, data.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void FlatTrackingTLogWithEmptyLineImmediatelyAfterRoot() + { + Console.WriteLine("Test: FlatTrackingTLogWithEmptyLineImmediatelyAfterRoot"); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^FOO", "", "FOO" }); + + MockTask task = DependencyTestHelper.MockTask; + FlatTrackingData data = new FlatTrackingData + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + false /* don't skip missing files */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, data.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void FlatTrackingTLogWithEmptyLineBetweenRoots() + { + Console.WriteLine("Test: FlatTrackingTLogWithEmptyLineBetweenRoots"); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^FOO", "FOO", "", "^BAR", "BAR" }); + + MockTask task = DependencyTestHelper.MockTask; + FlatTrackingData data = new FlatTrackingData + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + false /* don't skip missing files */ + ); + + Assert.AreEqual(1, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should have a warning."); + Assert.AreEqual(0, data.DependencyTable.Count, "DependencyTable should be empty."); + } + + [TestMethod] + public void FlatTrackingTLogWithEmptyRoot() + { + Console.WriteLine("Test: FlatTrackingTLogWithEmptyRoot"); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { "^", "FOO" }); + + MockTask task = DependencyTestHelper.MockTask; + FlatTrackingData data = new FlatTrackingData + ( + task, + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + false /* don't skip missing files */ + ); + + Assert.AreEqual(0, ((task as ITask).BuildEngine as MockEngine).Warnings, "Should not warn -- root markers are ignored by default"); + Assert.AreEqual(1, data.DependencyTable.Count, "DependencyTable should only contain one entry."); + Assert.IsNotNull(data.DependencyTable["FOO"], "FOO should be the only entry."); + } + + [TestMethod] + public void FlatTrackingDataMissingInputsAndOutputs() + { + Console.WriteLine("Test: FlatTrackingDataMissingInputsAndOutputs"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\two2.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + Path.GetFullPath("TestFiles\\sometempfile.obj"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\sometempfile2.obj") + }); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + Assert.AreEqual(2, inputs.MissingFiles.Count); + Assert.AreEqual(3, outputs.MissingFiles.Count); + } + + [TestMethod] + public void FlatTrackingDataMissingInputs() + { + Console.WriteLine("Test: FlatTrackingDataMissingInputs"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two1.h"), + Path.GetFullPath("TestFiles\\two2.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + }); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // No matter which way you look at it, if we're missing inputs, we're out of date + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + Assert.AreEqual(2, inputs.MissingFiles.Count); + Assert.AreEqual(0, outputs.MissingFiles.Count); + } + + [TestMethod] + public void FlatTrackingDataMissingOutputs() + { + Console.WriteLine("Test: FlatTrackingDataMissingOutputs"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + Thread.Sleep(sleepTimeMilliseconds); // need to wait since the timestamp check needs some time to register + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + Path.GetFullPath("TestFiles\\two.obj"), + Path.GetFullPath("TestFiles\\sometempfile2.obj") + }); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // No matter which way you look at it, if we're missing outputs, we're out of date + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + Assert.AreEqual(0, inputs.MissingFiles.Count); + Assert.AreEqual(2, outputs.MissingFiles.Count); + } + + [TestMethod] + public void FlatTrackingDataEmptyInputTLogs() + { + Console.WriteLine("Test: FlatTrackingDataEmptyInputTLogs"); + // Prepare files + File.WriteAllText("TestFiles\\one.read.tlog", String.Empty); + File.WriteAllText("TestFiles\\one.write.tlog", String.Empty); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // No matter which way you look at it, if we're missing inputs, we're out of date + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + } + + [TestMethod] + public void FlatTrackingDataEmptyOutputTLogs() + { + Console.WriteLine("Test: FlatTrackingDataEmptyOutputTLogs"); + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + // Prepare files + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + File.WriteAllText("TestFiles\\one.write.tlog", String.Empty); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // Inputs newer than outputs - if there are no outputs, then we're out of date + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + // Inputs newer than tracking - if there are no outputs, then we don't care + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + // Inputs or Outputs newer than tracking - if there is an output tlog, even if there's no text written to it, we're not out of date + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + } + + [TestMethod] + public void FlatTrackingDataInputNewerThanTracking() + { + Console.WriteLine("Test: FlatTrackingDataInputNewerThanTracking"); + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + }); + + Thread.Sleep(sleepTimeMilliseconds); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + // Compact the read tlog + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + + // Touch the tracking logs so that are more recent that any of the inputs + Thread.Sleep(sleepTimeMilliseconds); + File.SetLastWriteTime("TestFiles\\one.read.tlog", DateTime.Now); + File.SetLastWriteTime("TestFiles\\one.write.tlog", DateTime.Now); + Thread.Sleep(sleepTimeMilliseconds); + // Touch the output so that we would be out of date with respect to the inputs, but up to date with respect to the tracking logs + File.SetLastWriteTime(Path.GetFullPath("TestFiles\\one.obj"), DateTime.Now - TimeSpan.FromHours(1)); + + outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // We should be out of date with respect to the outputs + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + // We should be up to date with respect to the tracking data + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + } + + [TestMethod] + public void FlatTrackingDataInputNewerThanTrackingNoOutput() + { + Console.WriteLine("Test: FlatTrackingDataInputNewerThanTrackingNoOutput"); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + + Thread.Sleep(sleepTimeMilliseconds); + + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\*-one.write.?.tlog")), false); + // Compact the read tlog + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + inputs.SaveTlog(); + outputs.SaveTlog(); + + outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\*-one.write.?.tlog")), false); + // Compact the read tlog + inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + } + + [TestMethod] + public void FlatTrackingDataInputNewerThanOutput() + { + Console.WriteLine("Test: FlatTrackingDataInputOrOutputNewerThanTracking"); + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + }); + // Wait so that our tlogs are old + Thread.Sleep(sleepTimeMilliseconds); + + // Prepare the source files (later than tracking logs) + // Therefore newer + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + + // Prepate the output files (later than tracking logs and source files + // Therefore newer + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + // Compact the read tlog + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // We should be up to date inputs vs outputs + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + + // We should be out of date inputs & outputs vs tracking (since we wrote the files after the tracking logs) + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + + // Touch the input so that we would be out of date with respect to the outputs, and out of date with respect to the tracking logs + Thread.Sleep(sleepTimeMilliseconds); + File.SetLastWriteTime(Path.GetFullPath("TestFiles\\one.cpp"), DateTime.Now); + + outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // We should be out of date with respect to the tracking logs + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanTracking, inputs, outputs)); + + // We should be out of date with respect to the outputs + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + } + + [TestMethod] + public void FlatTrackingDataInputOrOutputNewerThanTracking() + { + Console.WriteLine("Test: FlatTrackingDataInputOrOutputNewerThanTracking"); + File.WriteAllLines("TestFiles\\one.read.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\one2.h"), + Path.GetFullPath("TestFiles\\one3.h"), + }); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + }); + + Thread.Sleep(sleepTimeMilliseconds); + // Prepare files + DependencyTestHelper.WriteAll("TestFiles\\one1.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one2.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one3.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + Thread.Sleep(sleepTimeMilliseconds); + DependencyTestHelper.WriteAll("TestFiles\\one.obj", ""); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + // Compact the read tlog + FlatTrackingData inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + // We should be up to date inputs vs outputs + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + // We should be out of date inputs & outputs vs tracking (since we wrote the files after the tracking logs) + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + + + // Touch the tracking logs so that are more recent that any of the inputs + Thread.Sleep(sleepTimeMilliseconds); + File.SetLastWriteTime("TestFiles\\one.read.tlog", DateTime.Now); + File.SetLastWriteTime("TestFiles\\one.write.tlog", DateTime.Now); + Thread.Sleep(sleepTimeMilliseconds); + + outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // We should be up to date with respect to the tracking data + Assert.AreEqual(true, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputOrOutputNewerThanTracking, inputs, outputs)); + + // Touch the input so that we would be out of date with respect to the outputs, but up to date with respect to the tracking logs + File.SetLastWriteTime(Path.GetFullPath("TestFiles\\one.cpp"), DateTime.Now); + + outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + inputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.read.tlog")), false); + + // We should be out of date with respect to the outputs + Assert.AreEqual(false, FlatTrackingData.IsUpToDate(DependencyTestHelper.MockTask.Log, UpToDateCheckType.InputNewerThanOutput, inputs, outputs)); + } + + [TestMethod] + public void FlatTrackingExcludeDirectories() + { + Console.WriteLine("Test: FlatTrackingExcludeDirectories"); + + // Prepare files + if (!Directory.Exists("TestFiles\\ToBeExcluded")) + { + Directory.CreateDirectory("TestFiles\\ToBeExcluded"); + } + + DependencyTestHelper.WriteAll("TestFiles\\ToBeExcluded\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\ToBeExcluded\\two.h", ""); + + DependencyTestHelper.WriteAll("TestFiles\\one.h", ""); + DependencyTestHelper.WriteAll("TestFiles\\one.cpp", ""); + + File.WriteAllLines("TestFiles\\one.tlog", new string[] { + "#Command some-command", + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one1.h"), + Path.GetFullPath("TestFiles\\ToBeExcluded\\two.cpp"), + Path.GetFullPath("TestFiles\\ToBeExcluded\\two.h"), + Path.GetFullPath("TestFiles\\SubdirectoryExcluded\\three.cpp"), + Path.GetFullPath("TestFiles\\SubdirectoryExcluded\\three.h"), + }); + + // Get the newest time w/o any exclude paths + Dictionary sharedLastWriteTimeUtcCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + FlatTrackingData data = new FlatTrackingData + ( + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + null, + DateTime.MinValue, + null, + sharedLastWriteTimeUtcCache + ); + + DateTime originalNewest = data.NewestFileTimeUtc; + + // Force an update to the files we don't care about + DependencyTestHelper.WriteAll("TestFiles\\ToBeExcluded\\two.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\ToBeExcluded\\two.h", ""); + if (!Directory.Exists("TestFiles\\ToBeExcluded\\SubdirectoryExcluded")) + { + Directory.CreateDirectory("TestFiles\\ToBeExcluded\\SubdirectoryExcluded"); + } + DependencyTestHelper.WriteAll("TestFiles\\ToBeExcluded\\SubdirectoryExcluded\\three.cpp", ""); + DependencyTestHelper.WriteAll("TestFiles\\ToBeExcluded\\SubdirectoryExcluded\\three.h", ""); + + // Now do a flat tracker ignoring the exclude directories and make sure the time didn't change + data = new FlatTrackingData + ( + DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.tlog")), + null, + DateTime.MinValue, + new string[] { Path.GetFullPath("TestFiles\\ToBeExcluded") }, + sharedLastWriteTimeUtcCache + ); + + Assert.AreEqual(originalNewest, data.NewestFileTimeUtc, "Timestamp changed when no tracked files changed."); + } + + [TestMethod] + public void TrackingDataCacheResetOnTlogChange() + { + Console.WriteLine("Test: FlatTrackingDataCacheResetOnTlogChange"); + + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\one.cpp"), + Path.GetFullPath("TestFiles\\one.obj"), + }); + + FlatTrackingData outputs = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + // Sleep once, so that NTFS has enough time to register a file modified time change + Thread.Sleep(sleepTimeMilliseconds); + File.WriteAllLines("TestFiles\\one.write.tlog", new string[] { + "#Command some-command", + "^" + Path.GetFullPath("TestFiles\\two.cpp"), + Path.GetFullPath("TestFiles\\two.obj"), + }); + + FlatTrackingData outputs2 = new FlatTrackingData(DependencyTestHelper.MockTask, DependencyTestHelper.ItemArray(new TaskItem("TestFiles\\one.write.tlog")), false); + + // We should not use the cached dependency table, since it has been updated since it was last read from disk + Assert.IsTrue(outputs.DependencyTable != outputs2.DependencyTable); + } + + [TestMethod] + public void RootContainsSubRoots() + { + Console.WriteLine("Test: RootContainsSubRoots"); + CanonicalTrackedOutputFiles output = new CanonicalTrackedOutputFiles(DependencyTestHelper.MockTask, + null); + + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "a|b|C|d|e|F|g")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "a")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "g")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "d")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "a|b")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "f|g")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "b|a")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "g|f")); + Assert.IsTrue(output.RootContainsAllSubRootComponents("a|b|c|d|e|f|g", "b|e")); + } + } + + internal class MockTask : Task + { + public MockTask(ResourceManager resourceManager) + : base(resourceManager) + { + } + + public TaskLoggingHelper LogHelper + { + get { return Log; } + } + + public override bool Execute() + { + return true; + } + } + + internal class DependencyTestHelper + { + public static ITaskItem[] ItemArray(ITaskItem item) + { + List itemList = new List(); + itemList.Add(item); + return itemList.ToArray(); + } + + public static TaskLoggingHelper MockTaskLoggingHelper + { + get + { + MockTask t = new MockTask(Microsoft.Build.Shared.AssemblyResources.PrimaryResources); + t.BuildEngine = new MockEngine(); + return t.LogHelper; + } + } + + public static MockTask MockTask + { + get + { + MockTask t = new MockTask(Microsoft.Build.Shared.AssemblyResources.PrimaryResources); + t.BuildEngine = new MockEngine(); + return t; + } + } + + public static void WriteAll(string filename, string content) + { + File.WriteAllText(filename, content); + } + } +} + diff --git a/src/Utilities/UnitTests/strings.resx b/src/Utilities/UnitTests/strings.resx new file mode 100644 index 00000000000..0a575d62f28 --- /dev/null +++ b/src/Utilities/UnitTests/strings.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oops I wiped your harddrive {0} + + + Be nice or I wipe your harddrive {0} + + + Romulan + + \ No newline at end of file diff --git a/src/Utilities/native.rc b/src/Utilities/native.rc new file mode 100644 index 00000000000..402d9dacd4f --- /dev/null +++ b/src/Utilities/native.rc @@ -0,0 +1,5 @@ +// From Dev12 on we want the versioning strings to be the VS ones instead of .NET Framework ones. +#include +#include + +#include diff --git a/src/XMakeBuildEngine/AllEngine/allengine.proj b/src/XMakeBuildEngine/AllEngine/allengine.proj new file mode 100644 index 00000000000..be36d2bfd1e --- /dev/null +++ b/src/XMakeBuildEngine/AllEngine/allengine.proj @@ -0,0 +1,19 @@ + + + + + true + + + + + + + + + + + + + + diff --git a/src/XMakeBuildEngine/AllEngineAndConversion/allengineandconversion.proj b/src/XMakeBuildEngine/AllEngineAndConversion/allengineandconversion.proj new file mode 100644 index 00000000000..49223bb8de1 --- /dev/null +++ b/src/XMakeBuildEngine/AllEngineAndConversion/allengineandconversion.proj @@ -0,0 +1,22 @@ + + + + + true + + + + + + + + + + + + + + + + + diff --git a/src/XMakeBuildEngine/ApexTests/ApexTests.csproj b/src/XMakeBuildEngine/ApexTests/ApexTests.csproj new file mode 100644 index 00000000000..37fc9860942 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/ApexTests.csproj @@ -0,0 +1,79 @@ + + + + + $(SuiteBinPath) + library + Microsoft.Build.ApexTests + Microsoft.Build.ApexTests + true + 10.0.10621 + 2.0 + {73D3ECA8-BBCC-4D19-8417-AEE7414059E8} + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {784BF121-CE8F-4314-AA55-E86AB61670FE} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Framework %28components\xmake\Framework\Framework%29 + Needs 'microsoft.build.framework.dll' + true + + + {653E79B4-118B-4E0A-9E34-8EB30E5E0881} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Utilities %28components\xmake\Framework\Utilities%29 + Needs 'microsoft.build.utilities.v12.0.dll' + + + {3F1F7307-EE89-4110-B636-BDE1E45358C4} + XMakeBuildEngine + Needs 'microsoft.build.dll' + true + + + + true + + + + true + + + + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/AssemblyInfo.cs b/src/XMakeBuildEngine/ApexTests/AssemblyInfo.cs new file mode 100644 index 00000000000..5b507673354 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/AssemblyInfo.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// AssemblyInfo for Apex test extensions. +//----------------------------------------------------------------------- +using System.ComponentModel.Composition; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Security.Permissions; +using Microsoft.Test.Apex; + +// Apex specific Assembly attributes. +[assembly: AssemblyApexTestExtensionLibrary()] + +// Friend the MSBuild assembly so that the BackEnd callback works. +[assembly: InternalsVisibleTo("Microsoft.Build.Engine, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerConfiguration.cs b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerConfiguration.cs new file mode 100644 index 00000000000..f258e23542c --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerConfiguration.cs @@ -0,0 +1,203 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Configuration for generating Test Extension Container. +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Threading; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Framework; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// An enumeration of all component types recognized by the system + /// + public enum ComponentType + { + /// + /// Request Manager component type. + /// + RequestManager, + + /// + /// Scheduler component type. + /// + Scheduler, + + /// + /// Results Cache component type. + /// + ResultsCache, + + /// + /// Property Cache component type. + /// + PropertyCache, + + /// + /// The Build Request Configuration Cache component type. + /// + ConfigCache, + + /// + /// Node Manager component type. + /// + NodeManager, + + /// + /// InProcNodeProvider component type. + /// + InProcNodeProvider, + + /// + /// OutOfProcNodeProvider component type. + /// + OutOfProcNodeProvider, + + /// + /// RemoteNodeProvider component type. + /// + RemoteNodeProvider, + + /// + /// Node packet factory component type. + /// + NodePacketFactory, + + /// + /// Request engine component type. + /// + RequestEngine, + + /// + /// File monitor component type. + /// + FileMonitor, + + /// + /// The endpoint on a Node component type. + /// + NodeEndpoint, + + /// + /// The logging service component type. + /// + LoggingService, + + /// + /// The component responsible for building requests. + /// + RequestBuilder, + + /// + /// The component responsible for building targets. + /// + TargetBuilder, + + /// + /// The component responsible for building tasks. + /// + TaskBuilder, + + /// + /// The component which holds all of the building projects. + /// + ProjectCollection, + + /// + /// The component which is responsible for providing test data to the variour components. + /// + TestDataProvider + } + + /// + /// Provides configuration information on how the Test Extension Container should be generated. + /// + public class BuildManagerContainerConfiguration : ContainerGeneratorConfiguration, IDisposable + { + /// + /// Initializes a new instance of the BuildManagerContainerConfiguration class. + /// + public BuildManagerContainerConfiguration() + { + this.ComponentsToMock = new Dictionary(); + this.TestExtensionForComponents = new Dictionary(); + } + + /// + /// Gets Default configuration just registers the BuildManagerTestExtenison. + /// + public static BuildManagerContainerConfiguration Default + { + get + { + BuildManagerContainerConfiguration configuration = new BuildManagerContainerConfiguration(); + + // Attach a test extension for a default component. + configuration.TestExtensionForComponents.Add(ComponentType.ConfigCache, "Microsoft.Build.ApexTests.Library.ConfigurationCacheTestExtension"); + configuration.TestExtensionForComponents.Add(ComponentType.ResultsCache, "Microsoft.Build.ApexTests.Library.ResultsCacheTestExtension"); + return configuration; + } + } + + /// + /// Gets Build components which should be replaced by the mock version of the components. The mock class name should be specified. + /// + public Dictionary ComponentsToMock + { + get; + private set; + } + + /// + /// Gets Build components which should be aggregated by test extensions. The test extension class name should be specified. + /// + public Dictionary TestExtensionForComponents + { + get; + private set; + } + + /// + /// Gets Generator type. + /// + public override Type GeneratorType + { + get + { + return typeof(BuildManagerContainerGenerator); + } + } + + /// + /// Cleanup any resources created by this object. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Cleanup any resources created by this object. + /// + /// If we are in the process of disposing. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.TestExtensionForComponents.Clear(); + this.ComponentsToMock.Clear(); + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerGenerator.cs b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerGenerator.cs new file mode 100644 index 00000000000..193bf974b1a --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerContainerGenerator.cs @@ -0,0 +1,145 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Container containing the Test Extensions. +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Reflection; + using System.ComponentModel.Composition; + using System.Diagnostics.CodeAnalysis; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + using Microsoft.Test.Apex.Services; + + /// + /// Responsible for creating a Test Extension Container which hosts the Test Extensions required + /// for testing. + /// + public class BuildManagerContainerGenerator : ContainerGenerator + { + /// + /// Gets or sets the LifetimeService for factoried extensions to be added to. + /// + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "The setter is invoked by binding of a component domain.")] + [Import] + public IFactoryProductActivatorService LifetimeService + { + get; + set; + } + + /// + /// This method is passed as a delegate to the BuildManager - so when the BuildManager needs to create an instance of this component this method is invoked. + /// + /// Mock object to instantiate. + /// IBuildComponent of the Mock. + internal IBuildComponent CreateMockComponent(BuildComponentType buildComponentType) + { + string typeToMock = buildComponentType.ToString(); + if (String.IsNullOrEmpty(typeToMock)) + { + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "No mocks are available for {0} build component type.", buildComponentType.ToString())); + } + + ComponentType componentType = StringToEnum(typeToMock); + string typeToMockName = this.Configuration.ComponentsToMock[componentType]; + Type mockType = Assembly.GetAssembly(typeof(BuildManagerContainerGenerator)).GetType(typeToMockName, true, true); + return (IBuildComponent)mockType.InvokeMember(null, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.CreateInstance, null, null, new object[] { }, CultureInfo.InvariantCulture); + } + + /// + /// Responsible for creating the build manager. + /// For MSBuild backend testing BuildManager is the Entry point for all tests. That is - the environment + /// for testing starts with the BuildManager. Configuration can specify which components should be mocked + /// and which test extension should be attached to which component. + /// + /// TextExtensions created. + protected override TestExtensionContainer Generate() + { + List testExtensions = new List(); + + // To workaround the problem where extensions derived out from Testextension does not have a LifeTimeManagmentService. + LifeTimeManagmentServiceTestExtension lifetimeServiceExtension = new LifeTimeManagmentServiceTestExtension(LifetimeService); + LifetimeService.Compose(lifetimeServiceExtension); + testExtensions.Add(lifetimeServiceExtension); + + // Create the build manager and the associated test extension first. + BuildManagerTestExtension buildManagerTestExtension = new BuildManagerTestExtension(BuildManager.DefaultBuildManager); + LifetimeService.Compose(buildManagerTestExtension); + testExtensions.Add(buildManagerTestExtension); + + // When the BuildManager is created it registers a default set of components. + // Loop through each of the components that we want to mock and then replace the component in the BuildManager. + foreach (KeyValuePair componentTypePair in this.Configuration.ComponentsToMock) + { + buildManagerTestExtension.ReplaceRegisterdFactory(GetBuildComponentTypeFromComponentType(componentTypePair.Key.ToString()), this.CreateMockComponent); + } + + // Loop through each of the components that we want to wrap with a test extension - create the test extension and aggregate the internal component. + // This component could be a mock that we create above or the real implementation. + foreach (KeyValuePair componentTypePair in this.Configuration.TestExtensionForComponents) + { + TestExtension extension = CreateTestExtensionForComponent(componentTypePair.Key.ToString(), componentTypePair.Value, buildManagerTestExtension); + LifetimeService.Compose(extension); + testExtensions.Add(extension); + } + + TestExtensionContainer testContainer = new TestExtensionContainer(testExtensions); + return testContainer; + } + + /// + /// Create a TestExtension for a given component type. + /// + /// Name of the component for which the TestExtension is to be created. + /// Fully qualified TestExtension name. + /// BuildManager Test extension entry. + /// New TestExtension. + private static TestExtension CreateTestExtensionForComponent(string componentName, string testExtensionName, BuildManagerTestExtension buildManagerTestExtension) + { + BuildComponentType type = StringToEnum(componentName); + IBuildComponent component = buildManagerTestExtension.GetComponent(type); + Type testExtensionType = Assembly.GetAssembly(typeof(BuildManagerContainerGenerator)).GetType(testExtensionName, true, true); + object[] parameters = { component }; + return (TestExtension)testExtensionType.InvokeMember(null, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.CreateInstance, null, null, parameters, CultureInfo.InvariantCulture); + } + + /// + /// Converts the ComponentType to a BuildComponentType. + /// + /// ComponentType string. + /// BuildComponentType for the requested string. + private static BuildComponentType GetBuildComponentTypeFromComponentType(string componentTypeString) + { + try + { + BuildComponentType component = StringToEnum(componentTypeString); + return component; + } + catch (ArgumentException) + { + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Component type: {0} is not a valid MSBuild build component type.", componentTypeString)); + } + } + + /// + /// Helper method to convert string to enum. + /// + /// Enum to get. + /// String value of enum. + /// Enum type of requested string. + private static T StringToEnum(string name) + { + return (T)Enum.Parse(typeof(T), name); + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerTestExtension.cs b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerTestExtension.cs new file mode 100644 index 00000000000..4e0f19c9c14 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerTestExtension.cs @@ -0,0 +1,440 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the BuildManager implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Threading; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Collections; + using Microsoft.Build.Evaluation; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension for BuildManager implementation. BuildManager is the main entry point to the MSBuild Backend Engine. The test extension below allows us to create an instance of the actual + /// implementation of the BuildManager and invoke methods provided by the build manager. The test extension also allows a mechanism to overwrite any internal components that the BuildManager + /// may have created so that we can mock internal components. This allows us to acheive isolating testing of components. + /// + public class BuildManagerTestExtension : TestExtension, IDisposable + { + /// + /// Path to the temporary folder. + /// + private string tempPath = null; + + /// + /// Path to the loaded assembly. + /// + private string assemblyPath = null; + + /// + /// List of all the temp folder that we created that needs to be cleaned up + /// + private List tempFoldersCreated = null; + + /// + /// Initializes a new instance of the BuildManagerTestExtension class. + /// + /// Instance of the actual BuildManager. + internal BuildManagerTestExtension(BuildManager buildManager) + : base() + { + this.BuildManager = buildManager; + this.tempPath = Path.GetTempPath(); + this.tempFoldersCreated = new List(); + this.tempPath = Path.GetTempPath(); + this.assemblyPath = Assembly.GetAssembly(typeof(BuildManagerTestExtension)).Location; + } + + /// + /// Gets the default build parameters to use. + /// + public static BuildParameters DefaultBuildParameters + { + get + { + return new BuildParameters(); + } + } + + /// + /// Gets or sets the instance of the actual MSBuild BuildManager + /// + internal BuildManager BuildManager + { + get; + private set; + } + + /// + /// Creates a new BuildParameters object with the specified parameters. + /// + /// Remove node should be re-used between different builds. + /// Max number of nodes to use in a build process. + /// Max memory in KB to use for the build process. + /// Default tools version to use if one is not specified. + /// New instance of the BuildParameters object created using the above parameters. + public static BuildParameters CreateBuildParameters(bool nodeReuse, int maxNodeCount, int maxMemoryLimit, string defaultToolsVersion) + { + BuildParameters parameter = new BuildParameters(); + parameter.EnableNodeReuse = nodeReuse; + parameter.DefaultToolsVersion = defaultToolsVersion; + parameter.MaxNodeCount = maxNodeCount; + parameter.MemoryUseLimit = maxMemoryLimit; + return parameter; + } + + /// + /// Method to mimic WaitAll in STA. WaitAll for multiple handles on an STA thread is not supported. + /// + /// Handles to wait for. + /// Number of miliseconds to wait for before timing out. + /// True if the wait succeedes. + public static bool WaitAll(WaitHandle[] waitHandles, int timeout) + { + bool waitStatus = true; + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + foreach (WaitHandle myWaitHandle in waitHandles) + { + if (!BuildManagerTestExtension.Wait(myWaitHandle, timeout)) + { + return false; + } + } + } + else + { + waitStatus = WaitHandle.WaitAll(waitHandles); + } + + return waitStatus; + } + + /// + /// Wait for a single handle. + /// + /// Handle to wait for + /// Number of miliseconds to wait for before timing out. + /// True if the wait succeedes. + public static bool Wait(WaitHandle myWaitHandle, int timeout) + { + if (WaitHandle.WaitAny(new WaitHandle[] { myWaitHandle }, timeout) == WaitHandle.WaitTimeout) + { + return false; + } + + return true; + } + + /// + /// Prepares the BuildManager to receive build requests. + /// + /// The build parameters. May be null. + /// Thrown if a build is already in progress. + public void BeginBuild(BuildParameters parameters) + { + this.BuildManager.BeginBuild(parameters); + } + + /// + /// Cancels all outstanding submissions. + /// + public void CancelAllSubmissions() + { + this.BuildManager.CancelAllSubmissions(); + } + + /// + /// Creates a new BuildRequestData object with the specified parameters. This will also create a project file with the specified file contents. + /// + /// Name of the project file to create. + /// Global properties to pass to the project when building it. If this parameter is null then an empty dictonary will be used. + /// Tools version to use when building the project file. If this parameter is null or empty then 2.0 will be used. + /// Targets to build. If this parameter is null then a empty string array will be used. + /// Hostservices for tasks being executed when building the project. + /// Contents of the project file. If the content is empty then a default content is used. + /// Number of miliseconds the project should try to run. + /// New instance of the BuildRequestData object created using the above parameters. + public BuildRequestData CreateBuildRequestData(string projectFileName, Dictionary globalProperties, string toolsVersion, string[] targetsToBuild, HostServices hostServices, string fileContents, int projectExecutionTime) + { + string projectFilePath = Path.Combine(this.tempPath, String.Format(CultureInfo.InvariantCulture, "MSBuildBackEndApexTests{0}", DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture))); + + if (!Directory.Exists(projectFilePath)) + { + Directory.CreateDirectory(projectFilePath); + } + else + { + for (int i = 0; i < 100; i++) + { + projectFilePath = projectFilePath + "_" + i; + + if (!Directory.Exists(projectFilePath)) + { + Directory.CreateDirectory(projectFilePath); + break; + } + } + } + + this.tempFoldersCreated.Add(projectFilePath); + projectFilePath = Path.Combine(projectFilePath, (String.IsNullOrEmpty(projectFileName) ? "testproject.proj" : projectFileName)); + + string defaultFileContent = + @" + + + + + + + "; + string fileContent = String.Format(CultureInfo.InvariantCulture, (String.IsNullOrEmpty(fileContents) ? defaultFileContent : fileContents), this.assemblyPath, projectExecutionTime.ToString(CultureInfo.InvariantCulture)); + using (TextWriter ts = new StreamWriter(projectFilePath, false, Encoding.UTF8)) + { + ts.WriteLine(fileContent); + } + + globalProperties = (globalProperties == null ? new Dictionary() : globalProperties); + targetsToBuild = (targetsToBuild == null ? new string[] { } : targetsToBuild); + return new BuildRequestData(projectFilePath, globalProperties, toolsVersion, targetsToBuild, hostServices); + } + + /// + /// Creates a new BuildRequestData object with the specified parameters. This will also create a project file with the specified file contents. + /// + /// Number of miliseconds the project should try to run. + /// New instance of the BuildRequestData object created using the above parameters. + public BuildRequestData CreateBuildRequestData(int projectExecutionTime) + { + return this.CreateBuildRequestData(null, null, null, null, null, null, projectExecutionTime); + } + + /// + /// Submits a build request to the current build but does not start it immediately. Allows the user to + /// perform asynchronous execution or access the submission ID prior to executing the request. + /// + /// Data containing the request to build. + /// BuildSubmissionTestExtension which contains the BuildSubmission instance returned to the internal BuildManager. + public BuildSubmissionTestExtension PendBuildRequest(BuildRequestData requestData) + { + BuildSubmission submission = this.BuildManager.PendBuildRequest(requestData); + return TestExtensionHelper.Create(submission, this); + } + + /// + /// Convenience method. Submits a build request and blocks until the results are available. + /// + /// Data containing the request to build. + /// BuildResultTestExtension which contains the BuildResult instance returned to the internal BuildManager. + public BuildResultTestExtension BuildRequest(BuildRequestData requestData) + { + BuildResult result = this.BuildManager.BuildRequest(requestData); + return TestExtensionHelper.Create(result, this); + } + + /// + /// Signals that no more build requests are expected (or allowed) and the BuildManager may clean up. + /// + public void EndBuild() + { + this.BuildManager.EndBuild(); + } + + /// + /// Convenience method. Submits a lone build request and blocks until results are available. + /// + /// Build settings to use. + /// Data containing the request to build. + /// BuildResultTestExtension which contains the BuildResult instance returned to the internal BuildManager. + public BuildResultTestExtension Build(BuildParameters parameters, BuildRequestData requestData) + { + BuildResult result = this.BuildManager.Build(parameters, requestData); + return TestExtensionHelper.Create(result, this); + } + + /// + /// Cleanup any resources created by this object. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Executes each of the requests and waits for all of them to be completed. + /// + /// Array of BuildRequestData to be built. + /// Number of mili seconds to wait for the pending build requests. + /// Should wait for the builds to complete. + /// Array of AsyncBuildRequestStatus which contain information about each request executed asynchronously. + /// True if the builds completed successfully. + public bool ExecuteAsyncBuildRequests(BuildRequestData[] buildRequests, int timeout, bool waitForCompletion, out AsyncBuildRequestStatus[] asyncBuildRequestsStatus) + { + BuildSubmissionTestExtension[] submissionTestExtensions = new BuildSubmissionTestExtension[buildRequests.Length]; + AutoResetEvent[] buildCompletedEvents = new AutoResetEvent[buildRequests.Length]; + asyncBuildRequestsStatus = new AsyncBuildRequestStatus[buildRequests.Length]; + try + { + for (int i = 0; i < buildRequests.Length; i++) + { + buildCompletedEvents[i] = new AutoResetEvent(false); + submissionTestExtensions[i] = this.PendBuildRequest(buildRequests[i]); + asyncBuildRequestsStatus[i] = new AsyncBuildRequestStatus(buildCompletedEvents[i], submissionTestExtensions[i]); + submissionTestExtensions[i].ExecuteAsync(asyncBuildRequestsStatus[i].SubmissionCompletedCallback, null); + } + + if (waitForCompletion) + { + if (!BuildManagerTestExtension.WaitAll(buildCompletedEvents, timeout)) + { + return false; + } + + return true; + } + else + { + return true; + } + } + finally + { + if (waitForCompletion) + { + for (int i = 0; i < buildRequests.Length; i++) + { + buildCompletedEvents[i].Close(); + buildCompletedEvents[i] = null; + } + } + } + } + + /// + /// Returns an instance of the requested component type. + /// + /// Instance of the type of component. + /// IBuildComponent instance returned by the internal BuildManager. + internal IBuildComponent GetComponent(BuildComponentType componentType) + { + return ((IBuildComponentHost)this.BuildManager).GetComponent(componentType); + } + + /// + /// Registers a factory which will be used to create the necessary components of the build + /// system. + /// + /// The type which is created by this factory. + /// The factory to be registered. + internal void ReplaceRegisterdFactory(BuildComponentType componentType, BuildComponentFactoryDelegate componentProvider) + { + ((IBuildComponentHost)this.BuildManager).RegisterFactory(componentType, componentProvider); + } + + /// + /// Cleanup all the temp folders created. Also cleanup the buildmanager. + /// + /// If we are in the process of disposing. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (string path in this.tempFoldersCreated) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + + this.BuildManager = null; + } + } + + /// + /// Class to wrap the completed callback for the build submission so that each submission can have it own instance of the callback and event handlers. + /// This way we can have multiple tests which can be doing async calls and failure in one does not affect the other tests. + /// + public class BuildSubmissionCompleteCallBack + { + /// + /// Initializes a new instance of the BuildSubmissionCompleteCallBack class. + /// + /// Event handler which is to be set when the callback is called. + public BuildSubmissionCompleteCallBack(AutoResetEvent submissionCompletedEvent) + { + this.SubmissionCompletedEvent = submissionCompletedEvent; + } + + /// + /// Initializes a new instance of the BuildSubmissionCompleteCallBack class. + /// + /// Event handler which is to be set when the callback is called. + /// This submission was cancelled after execute. + public BuildSubmissionCompleteCallBack(AutoResetEvent submissionCompletedEvent, bool cancelledAfterExecute) + : this(submissionCompletedEvent) + { + this.CancelledAfterExecute = cancelledAfterExecute; + } + + /// + /// Gets the event to signal when the submission callback has been called. + /// + public AutoResetEvent SubmissionCompletedEvent + { + get; + private set; + } + + /// + /// Gets a value indicating whether the submission could be cancelled after the build was executed. + /// + public bool CancelledAfterExecute + { + get; + private set; + } + + /// + /// Callback method when the asynchronous BuildSubmission is completed. The verification done on completed submission is + /// that the build completed and succeeded. This is the default behavior. If the verification is to be different then SubmissionCompletedVerificationType + /// has to be used. + /// + /// Contains the BuildSubmission for which the request was completed. + public void SubmissionCompletedCallback(BuildSubmissionTestExtension submissionTestExtension) + { + try + { + submissionTestExtension.Verify.BuildIsCompleted(); + if (this.CancelledAfterExecute) + { + submissionTestExtension.Verify.BuildCompletedButFailed(); + } + else + { + submissionTestExtension.Verify.BuildCompletedSuccessfully(); + } + } + finally + { + this.SubmissionCompletedEvent.Set(); + } + } + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerVerifier.cs b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerVerifier.cs new file mode 100644 index 00000000000..dfc872d90bd --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildManager/BuildManagerVerifier.cs @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension verifier for the BuildManager implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension verifier for the BasicScheduler implementation. + /// + public class BuildManagerVerifier : TestExtensionVerifier + { + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationTestExtension.cs b/src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationTestExtension.cs new file mode 100644 index 00000000000..82b2d995f22 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationTestExtension.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the BuildRequestConfiguration implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension for BuildRequestConfigurationTestExtension implementation. + /// + public class BuildRequestConfigurationTestExtension : TestExtension + { + /// + /// Initializes a new instance of the BuildRequestConfigurationTestExtension class. + /// + /// Configuration entry from the cache. + internal BuildRequestConfigurationTestExtension(BuildRequestConfiguration configuration) + : base() + { + this.Configuration = configuration; + } + + /// + /// Gets a value indicating the name of the cache file for this configuration. + /// + public string CacheFileName + { + get + { + return this.Configuration.GetCacheFile(); + } + } + + /// + /// Gets the configuration id. + /// + public int ConfigurationId + { + get + { + return this.Configuration.ConfigurationId; + } + } + + /// + /// Gets BuildRequestConfiguration object from the cache. + /// + internal BuildRequestConfiguration Configuration + { + get; + private set; + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationVerifier.cs b/src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationVerifier.cs new file mode 100644 index 00000000000..8e2eceaab2d --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildRequestConfiguration/BuildRequestConfigurationVerifier.cs @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension verifier for the BuildRequestConfiguration implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension verifier for the BuildRequestConfigurationVerifier implementation. + /// + public class BuildRequestConfigurationVerifier : TestExtensionVerifier + { + /// + /// Gets Test extension associated with this verifier. + /// + internal new BuildRequestConfigurationTestExtension TestExtension + { + get + { + return base.TestExtension as BuildRequestConfigurationTestExtension; + } + } + + /// + /// Verifies if the configuration is set to cacheable. + /// + public void ConfigurationIsCacheable() + { + this.Verifier.IsTrue(this.TestExtension.Configuration.IsCacheable, "Configuration should be cacheable."); + } + + /// + /// Verifies if the configuration is loaded. + /// + public void ConfigurationIsLoaded() + { + this.Verifier.IsTrue(this.TestExtension.Configuration.IsLoaded, "Configuration should be loaded."); + } + + /// + /// Verifies if the configuration is not loaded. + /// + public void ConfigurationIsUnloaded() + { + this.Verifier.IsFalse(this.TestExtension.Configuration.IsLoaded, "Configuration should not be loaded."); + } + + /// + /// Verifies if the configuration is cached. + /// + public void ConfigurationIsCached() + { + this.Verifier.IsTrue(this.TestExtension.Configuration.IsCached, "Configuration should be cached."); + } + + /// + /// Verifies if the configuration is not cached. + /// + public void ConfigurationIsNotCached() + { + this.Verifier.IsFalse(this.TestExtension.Configuration.IsCached, "Configuration should not be cached."); + } + + /// + /// Verifies if the configuration cache file has been created in disk. + /// + public void ConfigurationCacheFileExists() + { + this.Verifier.IsTrue(File.Exists(this.TestExtension.CacheFileName), "Cache filename {0} does not exist.", this.TestExtension.CacheFileName); + } + + /// + /// Verifies if the configuration cache file has not been created in disk. + /// + public void ConfigurationCacheFileDoesNotExists() + { + this.Verifier.IsFalse(File.Exists(this.TestExtension.CacheFileName), "Cache filename {0} does not exist.", this.TestExtension.CacheFileName); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultTestExtension.cs b/src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultTestExtension.cs new file mode 100644 index 00000000000..deaf4d1d9ce --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultTestExtension.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the BuildResult implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Text; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension for BuildResult implementation. + /// + public class BuildResultTestExtension : TestExtension + { + /// + /// Initializes a new instance of the BuildResultTestExtension class. + /// + /// Instance of the result returned by the internal BuildManager. + internal BuildResultTestExtension(BuildResult buildResult) + : base() + { + this.BuildResult = buildResult; + } + + /// + /// Gets the BuildResult object returned by the BuildManager. + /// + internal BuildResult BuildResult + { + get; + private set; + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultVerifier.cs b/src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultVerifier.cs new file mode 100644 index 00000000000..5ba412ac28a --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildResult/BuildResultVerifier.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the BuildResult implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Exceptions; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test verifier for BuildResult implementation. + /// + public class BuildResultVerifier : TestExtensionVerifier + { + /// + /// Gets Test extension associated with this verifier. + /// + internal new BuildResultTestExtension TestExtension + { + get + { + return base.TestExtension as BuildResultTestExtension; + } + } + + /// + /// Verify if the build succeeded. + /// + public void BuildSucceeded() + { + this.Verifier.IsTrue((this.TestExtension.BuildResult.OverallResult == BuildResultCode.Success), "OverallResult of the build was expected to be successful."); + } + + /// + /// Verify if the build failed. + /// + public void BuildFailed() + { + this.Verifier.IsTrue((this.TestExtension.BuildResult.OverallResult == BuildResultCode.Failure), "OverallResult of the build was expected to be Failure."); + } + + /// + /// Verify if the result contains BuildAborted exception due to a cancel. + /// + public void BuildWasAborted() + { + this.Verifier.IsTrue(this.TestExtension.BuildResult.Exception.GetType() == typeof(BuildAbortedException), "Result should contain BuildAborted exception."); + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTestExtension.cs b/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTestExtension.cs new file mode 100644 index 00000000000..daa0cbf18a2 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTestExtension.cs @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the BuildSubmission implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Delegate which is called when the submission is completed. + /// + /// BuildSubmissionTestExtension returned from the internal callback. + public delegate void SubmissionCompletedCallback(BuildSubmissionTestExtension submissionTestExtension); + + /// + /// Test extension for BuildSubmission implementation. + /// + public class BuildSubmissionTestExtension : TestExtension + { + /// + /// Initializes a new instance of the BuildSubmissionTestExtension class. + /// + /// BuildSubmission returned to the internal BuildManager. + internal BuildSubmissionTestExtension(BuildSubmission buildSubmission) + : base() + { + this.BuildSubmission = buildSubmission; + } + + /// + /// Gets the results test extension of the build. This is valid only after the build is completed. + /// + public BuildResultTestExtension BuildResultTestExtension + { + get + { + BuildResult result = this.BuildSubmission.BuildResult; + return TestExtensionHelper.Create(result, this); + } + } + + /// + /// Gets the configuration id of the build request encapsulated by this submission. + /// + public int ConfigurationIdForSubmission + { + get + { + return this.BuildSubmission.BuildRequest.ConfigurationId; + } + } + + /// + /// Gets BuildSubmission type which is returned by the BuildManager. + /// + internal BuildSubmission BuildSubmission + { + get; + private set; + } + + /// + /// Gets or sets a callback method which is to be called when the submission completes. + /// + internal SubmissionCompletedCallback SubmissionCompletedCallback + { + get; + set; + } + + /// + /// Starts the request and blocks until results are available. + /// + /// BuildResultTestExtension of the submitted build request. Returns only after the build is completed. + public BuildResultTestExtension Execute() + { + BuildResult result = this.BuildSubmission.Execute(); + return TestExtensionHelper.Create(result, this); + } + + /// + /// Starts the request asynchronously and immediately returns control to the caller. + /// + /// Method to call back when the submission is completed. + /// The context of the submission. + public void ExecuteAsync(SubmissionCompletedCallback callback, object context) + { + this.SubmissionCompletedCallback = callback; + this.BuildSubmission.ExecuteAsync(this.BuildSubmissionCompleteCallback, context); + } + + /// + /// Callback method when the internal asynchronous BuildSubmission is completed. This basically + /// creates a new BuildSubmissionTestExtension and calls the clients callback. + /// + /// BuildSubmission record for which the build was completed. + internal void BuildSubmissionCompleteCallback(BuildSubmission buildSubmission) + { + BuildSubmissionTestExtension submissionTestExtension = new BuildSubmissionTestExtension(buildSubmission); + this.SubmissionCompletedCallback(submissionTestExtension); + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTests.cs b/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTests.cs new file mode 100644 index 00000000000..e047f038599 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionTests.cs @@ -0,0 +1,425 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests to cover Build submission feature in MSBuild. +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Tests +{ + using System; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Threading; + using Microsoft.Build.ApexTests.Library; + using Microsoft.Build.Execution; + using Microsoft.Test.Apex; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// BuildSubmission tests. + /// + [TestClass] + public class BuildSubmissionTests : ApexTest + { + /// + /// Simulated time in mili-seconds a normal project should execute for. + /// + private const int NormalProjectExecutionTime = 1 * 1000; + + /// + /// Simulated time in mili-seconds a long project should execute for. + /// + private const int LongProjectExecutionTime = 3 * 1000; + + /// + /// Total time in mili-seconds the entire build should complete in. + /// + private const int DefaultBuildCompletionTimeout = 60 * 1000; + + /// + /// Default node count for build is 1. + /// + private const int DefaultNodeCount = 1; + + /// + /// When multiple nodes are specified the value to use will be 3. + /// + private const int MultipleNodeCount = 3; + + /// + /// Unlimited memory usage when building projects. + /// + private const int DefaultMemoryLimit = 0; + + /// + /// No node re-use by default; + /// + private const bool DefaultNodeReUseAction = false; + + /// + /// Async action should wait for completion before returning control back to the test. + /// + private const bool WaitForCompletion = true; + + /// + /// Minimim number of BuildRequests to create. + /// + private const int MinimimBuildRequestCount = 3; + + /// + /// Maximum number of BuildRequests to create. + /// + private const int MaximumBuildRequestCount = 5; + + /// + /// Used to generate random numbers. + /// + private Random randomNumbers; + + /// + /// TestConfiguration parameters to use when creating the container. + /// + private BuildManagerContainerConfiguration testConfiguration = null; + + /// + /// Test container which holds the default test extensions. + /// + private TestExtensionContainer testExtensionContainer = null; + + /// + /// Build Manager to use for the tests. + /// + private BuildManagerTestExtension buildManagerTestExtension = null; + + /// + /// Constructor. + /// + public BuildSubmissionTests() + { + this.randomNumbers = new Random((int)DateTime.Now.Ticks); + } + + /// + /// Method called before each test executes. + /// + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + this.testConfiguration = BuildManagerContainerConfiguration.Default; + this.testExtensionContainer = this.Operations.GenerateContainer(this.testConfiguration); + this.buildManagerTestExtension = this.testExtensionContainer.GetFirstTestExtension(); + } + + /// + /// Method called after each test completes. + /// + [TestCleanup] + public override void TestCleanup() + { + base.TestCleanup(); + Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + this.buildManagerTestExtension.Dispose(); + this.buildManagerTestExtension = null; + this.testConfiguration.Dispose(); + this.testConfiguration = null; + this.testExtensionContainer = null; + } + + /// + /// A simple successful single build of 1 project using Build. + /// + [TestMethod] + [Description("A simple successful single build of 1 project using Build.")] + public void SimpleSuccessfulBuild() + { + BuildRequestData data = this.buildManagerTestExtension.CreateBuildRequestData(BuildSubmissionTests.NormalProjectExecutionTime); + BuildResultTestExtension resultTestExtension = this.buildManagerTestExtension.Build(BuildManagerTestExtension.DefaultBuildParameters, data); + resultTestExtension.Verify.BuildSucceeded(); + } + + /// + /// Multiple simple successful build of 1 project using Build with different parameters. + /// + [TestMethod] + [Description("Multiple simple successful build of 1 project using Build with different parameters.")] + public void MultipleSuccessfulBuildsWithDifferentParameters() + { + BuildRequestData requestData1 = this.buildManagerTestExtension.CreateBuildRequestData(BuildSubmissionTests.NormalProjectExecutionTime); + BuildRequestData requestData2 = this.buildManagerTestExtension.CreateBuildRequestData(BuildSubmissionTests.NormalProjectExecutionTime); + AsyncBuildRequestStatus[] requestData1Status = null; + AsyncBuildRequestStatus[] requestData2Status = null; + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "2.0")); + bool success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(new BuildRequestData[] { requestData1 }, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestData1Status); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + requestData1Status[0].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + configurationCachetestExtension.Verify.CacheContainsConfigurationForBuildRequest(requestData1, "2.0"); + this.buildManagerTestExtension.EndBuild(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(new BuildRequestData[] { requestData2 }, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestData2Status); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + requestData2Status[0].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + configurationCachetestExtension.Verify.CacheContainsConfigurationForBuildRequest(requestData2, "4.0"); + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Multiple simple successful build of projects using Build with same parameters. + /// + [TestMethod] + [Description("Multiple simple successful build of projects using Build with same parameters.")] + public void MultipleSuccessfulBuildsWithSameParameters() + { + BuildRequestData requestData1 = this.buildManagerTestExtension.CreateBuildRequestData(BuildSubmissionTests.NormalProjectExecutionTime); + BuildRequestData requestData2 = this.buildManagerTestExtension.CreateBuildRequestData(BuildSubmissionTests.NormalProjectExecutionTime); + AsyncBuildRequestStatus[] requestData1Status = null; + AsyncBuildRequestStatus[] requestData2Status = null; + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + bool success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(new BuildRequestData[] { requestData1 }, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestData1Status); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + requestData1Status[0].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + configurationCachetestExtension.Verify.CacheContainsConfigurationForBuildRequest(requestData1, "4.0"); + this.buildManagerTestExtension.EndBuild(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(new BuildRequestData[] { requestData2 }, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestData2Status); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + requestData2Status[0].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + configurationCachetestExtension.Verify.CacheContainsConfigurationForBuildRequest(requestData2, "4.0"); + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Asynchronous build with multiple submissions on multiple nodes. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions on multiple nodes.")] + public void AsyncBuildWithMultipleSubmissionsOnMultipleNodes() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.NormalProjectExecutionTime); + + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.MultipleNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + bool success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestsStatus); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + Verify.IsTrue(requestsStatus.Length == buildRequests.Length, "Number of submission should match the number of requests."); + for (int j = 0; j < buildRequests.Length; j++) + { + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + configurationCachetestExtension.Verify.CacheContainsConfigurationForBuildRequest(buildRequests[j], "4.0"); + } + + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Asynchronous build with multiple submissions on single nodes. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions on single nodes.")] + public void AsyncBuildWithMultipleSubmissionsOnSingleNodes() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.NormalProjectExecutionTime); + + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + bool success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestsStatus); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + Verify.IsTrue(requestsStatus.Length == buildRequests.Length, "Number of submission should match the number of requests."); + for (int j = 0; j < buildRequests.Length; j++) + { + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + configurationCachetestExtension.Verify.CacheContainsConfigurationForBuildRequest(buildRequests[j], "4.0"); + } + + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Asynchronous build with multiple submissions and then cancellation on single node. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions and then cancellations on a single node.")] + public void AsyncBuildWithMultipleSubmissionsAndThenCancelOnSingleNode() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.LongProjectExecutionTime); + + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + ResultsCacheTestExtension resultsCacheTestExtension = this.testExtensionContainer.GetFirstTestExtension(); + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, !BuildSubmissionTests.WaitForCompletion, out requestsStatus); + this.buildManagerTestExtension.CancelAllSubmissions(); + + for (int j = 0; j < buildRequests.Length; j++) + { + Verify.IsTrue(BuildManagerTestExtension.Wait(requestsStatus[j].SubmissionCompletedEvent, BuildSubmissionTests.DefaultBuildCompletionTimeout), "Submissions should have completed in the specified 60 seconds timeout."); + requestsStatus[j].SubmissionCompletedEvent.Close(); + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildWasAborted(); + BuildResultTestExtension resultFromCacheTestExtension = resultsCacheTestExtension.GetResultFromCache(requestsStatus[j].SubmissionTestExtension.ConfigurationIdForSubmission); + resultFromCacheTestExtension.Verify.BuildWasAborted(); + } + + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Asynchronous build with multiple submissions and then cancellation on multiple nodes. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions and then cancellations on a multiple node.")] + public void AsyncBuildWithMultipleSubmissionsAndThenCancelOnMultipleNodes() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.LongProjectExecutionTime); + + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + ResultsCacheTestExtension resultsCacheTestExtension = this.testExtensionContainer.GetFirstTestExtension(); + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.MultipleNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, !BuildSubmissionTests.WaitForCompletion, out requestsStatus); + this.buildManagerTestExtension.CancelAllSubmissions(); + + for (int j = 0; j < buildRequests.Length; j++) + { + Verify.IsTrue(BuildManagerTestExtension.Wait(requestsStatus[j].SubmissionCompletedEvent, BuildSubmissionTests.DefaultBuildCompletionTimeout), "Submissions should have completed in the specified 60 seconds timeout."); + requestsStatus[j].SubmissionCompletedEvent.Close(); + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildWasAborted(); + BuildResultTestExtension resultFromCacheTestExtension = resultsCacheTestExtension.GetResultFromCache(requestsStatus[j].SubmissionTestExtension.ConfigurationIdForSubmission); + resultFromCacheTestExtension.Verify.BuildWasAborted(); + } + + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Asynchronous build with multiple submissions and then EndBuild without completion. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions and then EndBuild without completion.")] + public void AsyncBuildWithMultipleSubmissionsAndThenEndWithoutCompletion() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.NormalProjectExecutionTime); + + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + ResultsCacheTestExtension resultsCacheTestExtension = this.testExtensionContainer.GetFirstTestExtension(); + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.DefaultNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, !BuildSubmissionTests.WaitForCompletion, out requestsStatus); + this.buildManagerTestExtension.EndBuild(); + + for (int j = 0; j < buildRequests.Length; j++) + { + Verify.IsTrue(BuildManagerTestExtension.Wait(requestsStatus[j].SubmissionCompletedEvent, BuildSubmissionTests.DefaultBuildCompletionTimeout), "Submissions should have completed in the specified 60 seconds timeout."); + requestsStatus[j].SubmissionCompletedEvent.Close(); + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + } + } + + /// + /// Asynchronous build with multiple submissions Cancel and then new submissions. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions, Cancel and then new submissions.")] + public void AsyncBuildWithMultipleSubmissionsCancelAndNewSubmission() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.NormalProjectExecutionTime); + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + ResultsCacheTestExtension resultsCacheTestExtension = this.testExtensionContainer.GetFirstTestExtension(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.MultipleNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, !BuildSubmissionTests.WaitForCompletion, out requestsStatus); + this.buildManagerTestExtension.CancelAllSubmissions(); + + for (int j = 0; j < buildRequests.Length; j++) + { + Verify.IsTrue(BuildManagerTestExtension.Wait(requestsStatus[j].SubmissionCompletedEvent, BuildSubmissionTests.DefaultBuildCompletionTimeout), "Submissions should have completed in the specified 60 seconds timeout."); + requestsStatus[j].SubmissionCompletedEvent.Close(); + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildWasAborted(); + BuildResultTestExtension resultFromCacheTestExtension = resultsCacheTestExtension.GetResultFromCache(requestsStatus[j].SubmissionTestExtension.ConfigurationIdForSubmission); + resultFromCacheTestExtension.Verify.BuildWasAborted(); + } + + this.buildManagerTestExtension.EndBuild(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.MultipleNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + BuildRequestData[] buildRequestsAfterCancel = GenerateBuildRequests(BuildSubmissionTests.NormalProjectExecutionTime); + + AsyncBuildRequestStatus[] requestsStatusAfterCancel = null; + bool success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequestsAfterCancel, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestsStatusAfterCancel); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + + for (int k = 0; k < buildRequestsAfterCancel.Length; k++) + { + requestsStatusAfterCancel[k].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + } + + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Asynchronous build with multiple submissions Cancel and then re-submit the same requests. + /// + [TestMethod] + [Description("Asynchronous build with multiple submissions, Cancel and then re-submit.")] + public void AsyncBuildWithMultipleSubmissionsCancelAndReSubmit() + { + BuildRequestData[] buildRequests = GenerateBuildRequests(BuildSubmissionTests.NormalProjectExecutionTime); + ConfigurationCacheTestExtension configurationCachetestExtension = this.testExtensionContainer.GetFirstTestExtension(); + ResultsCacheTestExtension resultsCacheTestExtension = this.testExtensionContainer.GetFirstTestExtension(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.MultipleNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatus = null; + this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, !BuildSubmissionTests.WaitForCompletion, out requestsStatus); + this.buildManagerTestExtension.CancelAllSubmissions(); + + for (int j = 0; j < buildRequests.Length; j++) + { + Verify.IsTrue(BuildManagerTestExtension.Wait(requestsStatus[j].SubmissionCompletedEvent, BuildSubmissionTests.DefaultBuildCompletionTimeout), "Submissions should have completed in the specified 60 seconds timeout."); + requestsStatus[j].SubmissionCompletedEvent.Close(); + requestsStatus[j].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildWasAborted(); + BuildResultTestExtension resultFromCacheTestExtension = resultsCacheTestExtension.GetResultFromCache(requestsStatus[j].SubmissionTestExtension.ConfigurationIdForSubmission); + resultFromCacheTestExtension.Verify.BuildWasAborted(); + } + + this.buildManagerTestExtension.EndBuild(); + + this.buildManagerTestExtension.BeginBuild(BuildManagerTestExtension.CreateBuildParameters(BuildSubmissionTests.DefaultNodeReUseAction, BuildSubmissionTests.MultipleNodeCount, BuildSubmissionTests.DefaultMemoryLimit, "4.0")); + AsyncBuildRequestStatus[] requestsStatusAfterCancel = null; + bool success = this.buildManagerTestExtension.ExecuteAsyncBuildRequests(buildRequests, BuildSubmissionTests.DefaultBuildCompletionTimeout, BuildSubmissionTests.WaitForCompletion, out requestsStatusAfterCancel); + Verify.IsTrue(success, "Submissions should have completed in the specified 60 seconds timeout."); + + for (int k = 0; k < buildRequests.Length; k++) + { + requestsStatusAfterCancel[k].SubmissionTestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + } + + this.buildManagerTestExtension.EndBuild(); + } + + /// + /// Generates a random amout of BuildRequestData between 5 and 10. + /// + /// Time in mili-seconds the project default target will sleep for before completion. + /// Generated BuildRequestData. + private BuildRequestData[] GenerateBuildRequests(int simulatedProjectBuildTime) + { + int requestCount = this.randomNumbers.Next(BuildSubmissionTests.MinimimBuildRequestCount, BuildSubmissionTests.MaximumBuildRequestCount); + BuildRequestData[] buildRequests = new BuildRequestData[requestCount]; + for (int i = 0; i < requestCount; i++) + { + buildRequests[i] = buildManagerTestExtension.CreateBuildRequestData(simulatedProjectBuildTime); + } + + return buildRequests; + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionVerifier.cs b/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionVerifier.cs new file mode 100644 index 00000000000..431b5ea2639 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/BuildSubmission/BuildSubmissionVerifier.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension verifier for the BuildSubmission implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension verifier for the BuildSubmission implementation. + /// + public class BuildSubmissionVerifier : TestExtensionVerifier + { + /// + /// Gets Test extension associated with this verifier. + /// + internal new BuildSubmissionTestExtension TestExtension + { + get + { + return base.TestExtension as BuildSubmissionTestExtension; + } + } + + /// + /// Verifies if the BuildSubmission was completed. + /// + public void BuildIsCompleted() + { + this.Verifier.IsTrue(this.TestExtension.BuildSubmission.IsCompleted, "BuildSubmission should have been completed."); + } + + /// + /// Verifies if the BuildSubmission is pending. + /// + public void BuildIsRunning() + { + this.Verifier.IsTrue(!this.TestExtension.BuildSubmission.IsCompleted, "BuildSubmission should have been not completed or executing."); + } + + /// + /// Verify that build completed and the result is a success. + /// + public void BuildCompletedSuccessfully() + { + this.BuildIsCompleted(); + this.TestExtension.BuildResultTestExtension.Verify.BuildSucceeded(); + } + + /// + /// Verify that build completed and the result is a Failure. + /// + public void BuildCompletedButFailed() + { + this.BuildIsCompleted(); + this.TestExtension.BuildResultTestExtension.Verify.BuildFailed(); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheTestExtension.cs b/src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheTestExtension.cs new file mode 100644 index 00000000000..dc4650f6bd1 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheTestExtension.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the ConfigCache implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension for ConfigCache implementation. + /// + public class ConfigurationCacheTestExtension : TestExtension + { + /// + /// Initializes a new instance of the ConfigurationCacheTestExtension class. + /// + /// ConfigurationCache component instance. + internal ConfigurationCacheTestExtension(ConfigCache configCache) + : base() + { + this.ConfigCache = configCache; + } + + /// + /// Gets BuildManager component for MSBuild. BuildManager should not be exposed publicly. + /// + internal ConfigCache ConfigCache + { + get; + private set; + } + + /// + /// Writes project configurations to disk. + /// + public void WriteConfigurationsToDisk() + { + this.ConfigCache.WriteConfigurationsToDisk(); + } + + /// + /// Retreives the configuration cache from the data provided. + /// + /// Data containing the build request entry. + /// Tools version used to build the request. + /// BuildRequestConfigurationTestExtensuin which contains the configuration retreived from the cache. Can return NULL if the entry for the data is not in the cache. + public BuildRequestConfigurationTestExtension GetConfigurationFromCache(BuildRequestData requestData, string toolsVersion) + { + BuildRequestConfiguration unresolvedConfiguration = new BuildRequestConfiguration(requestData, toolsVersion); + BuildRequestConfiguration resolvedConfiguration = this.ConfigCache.GetMatchingConfiguration(unresolvedConfiguration); + if (resolvedConfiguration == null) + { + return null; + } + + return TestExtensionHelper.Create(resolvedConfiguration, this); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheVerifier.cs b/src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheVerifier.cs new file mode 100644 index 00000000000..bc35ebdca55 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/ConfigurationCache/ConfigurationCacheVerifier.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension verifier for the ConfigCache implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension verifier for the ConfigCache implementation. + /// + public class ConfigurationCacheVerifier : TestExtensionVerifier + { + /// + /// Gets Test extension associated with this verifier. + /// + internal new ConfigurationCacheTestExtension TestExtension + { + get + { + return base.TestExtension as ConfigurationCacheTestExtension; + } + } + + /// + /// Verifies if a configuration for the build request data exists in the cache. + /// + /// BuildRequestData used to create the original configuration. + /// Tools version of the configuration. + public void CacheContainsConfigurationForBuildRequest(BuildRequestData requestData, string toolsVersion) + { + BuildRequestConfigurationTestExtension configurationTestExtension = this.TestExtension.GetConfigurationFromCache(requestData, toolsVersion); + this.Verifier.IsNotNull(configurationTestExtension, "Configuration should exist in the cache."); + this.Verifier.IsTrue(String.Compare(configurationTestExtension.Configuration.ToolsVersion, toolsVersion, StringComparison.OrdinalIgnoreCase) == 0, "Configuration tools version should match the passed in tools version."); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/Helpers/AsyncBuildRequestStatus.cs b/src/XMakeBuildEngine/ApexTests/Helpers/AsyncBuildRequestStatus.cs new file mode 100644 index 00000000000..b6ed3bf9577 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/Helpers/AsyncBuildRequestStatus.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Class which has information about an execution of a build request. +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Threading; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Collections; + using Microsoft.Build.Evaluation; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + + /// + /// Class containing information about a BuildRequest which was executed asynchronously. + /// + public class AsyncBuildRequestStatus + { + /// + /// Initializes a new instance of the AsyncBuildRequestStatus class. + /// + /// Event handler which is to be set when the callback is called. + /// Test extension for the submission. + public AsyncBuildRequestStatus(AutoResetEvent submissionCompletedEvent, BuildSubmissionTestExtension submissionTestExtension) + { + this.SubmissionCompletedEvent = submissionCompletedEvent; + this.SubmissionTestExtension = submissionTestExtension; + this.SubmissionsAreSame = false; + } + + /// + /// Gets the event to signal when the submission callback has been called. + /// + public AutoResetEvent SubmissionCompletedEvent + { + get; + private set; + } + + /// + /// Gets the submission testextension passed when creating this class. + /// + public BuildSubmissionTestExtension SubmissionTestExtension + { + get; + private set; + } + + /// + /// Gets the submission testextension passed by MSBuild during the callback. + /// + public BuildSubmissionTestExtension SubmissionTestExtensionFromClassBack + { + get; + private set; + } + + /// + /// Gets the value indicating whether the submission which was used to create this callback is the same as the one received in the callback. + /// + public bool SubmissionsAreSame + { + get; + private set; + } + + /// + /// Callback method when the asynchronous BuildSubmission is completed. The verification done on completed submission is + /// that the build completed and succeeded. This is the default behavior. If the verification is to be different then SubmissionCompletedVerificationType + /// has to be used. + /// + /// Contains the BuildSubmission for which the request was completed. + public void SubmissionCompletedCallback(BuildSubmissionTestExtension submissionTestExtension) + { + if (this.SubmissionTestExtension.BuildSubmission.BuildRequest.ConfigurationId == submissionTestExtension.BuildSubmission.BuildRequest.ConfigurationId) + { + this.SubmissionsAreSame = true; + } + + this.SubmissionTestExtensionFromClassBack = submissionTestExtension; + this.SubmissionCompletedEvent.Set(); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceTestExtension.cs b/src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceTestExtension.cs new file mode 100644 index 00000000000..cee90934733 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceTestExtension.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension Apex LifeTimeManagmentService implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Threading; + using Microsoft.Test.Apex; + using Microsoft.Test.Apex.Services; + + /// + /// Temporary workaround to pass the Apex LifeTimeManagmentService so that extensions derived from TestExtension can be registered with the domain. + /// + public class LifeTimeManagmentServiceTestExtension : TestExtension + { + /// + /// Initializes a new instance of the LifeTimeManagmentServiceTestExtension class. + /// + /// Instance Apex life time managment service. + internal LifeTimeManagmentServiceTestExtension(IFactoryProductActivatorService lifetimeManagementService) + : base() + { + this.LifetimeManagementService = lifetimeManagementService; + } + + /// + /// Gets or sets the instance Apex life time managment service. + /// + internal IFactoryProductActivatorService LifetimeManagementService + { + get; + private set; + } + + /// + /// Add a component to the lifetime management domain. + /// + /// + /// The object instance to be placed in application domain and bound. + /// + public void AddToCompositionContainer(object instance) + { + this.LifetimeManagementService.Compose(instance); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceVerifier.cs b/src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceVerifier.cs new file mode 100644 index 00000000000..cd7a7428f45 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/Helpers/LifeTimeManagmentServiceVerifier.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension verifier for the Apex LifeTimeManagmentService implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.Test.Apex; + + /// + /// Test extension verifier for the Apex LifeTimeManagmentService implementation. + /// + public class LifeTimeManagmentServiceVerifier : TestExtensionVerifier + { + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/Helpers/SimpleTaskHelper.cs b/src/XMakeBuildEngine/ApexTests/Helpers/SimpleTaskHelper.cs new file mode 100644 index 00000000000..e7207337a84 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/Helpers/SimpleTaskHelper.cs @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Simple Task implementation which will be used by the tests. +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.Build.Framework; + + /// + /// Simple Task implementation which will be used by the tests. + /// + public class SimpleTaskHelper : ITask + { + /// + /// Initializes a new instance of the SimpleTaskHelper class. + /// + public SimpleTaskHelper() + { + this.TaskShouldError = false; + this.TaskShouldThrowException = false; + this.TaskOutput = null; + } + + /// + /// Gets or sets the BuildEngine callback. + /// + public IBuildEngine BuildEngine + { + get; + set; + } + + /// + /// Gets or sets the TaskHost callback. + /// + public ITaskHost HostObject + { + get; + set; + } + + /// + /// Gets or sets the expected output to populate. + /// + public string ExpectedOutput + { + get; + set; + } + + /// + /// Gets or sets a value indicating whether task should fail. + /// + public bool TaskShouldError + { + get; + set; + } + + /// + /// Gets or sets a value indicating whether task should throw an exception. + /// + public bool TaskShouldThrowException + { + get; + set; + } + + /// + /// Gets or sets Gets or sets a value indicating whether task should sleep before exiting. + /// + public int SleepTime + { + get; + set; + } + + /// + /// Gets or sets the output from the task which is set to the expected output. + /// + [Output] + public string TaskOutput + { + get; + set; + } + + /// + /// Execution of the task. + /// + /// True if the task succeeded else false. + public bool Execute() + { + if (this.TaskShouldThrowException) + { + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Test exception from SimpleTaskHelper.")); + } + + if (this.TaskShouldError) + { + BuildErrorEventArgs eventArgs = new BuildErrorEventArgs("Subcategory", "666", "foo.cs", 1, 1, 1, 1, String.Format(CultureInfo.InvariantCulture, "Test error from SimpleTaskHelper."), "Helpme", "SimpleTaskHelper"); + this.BuildEngine.LogErrorEvent(eventArgs); + return false; + } + + if (this.SleepTime > 0) + { + System.Threading.Thread.Sleep(this.SleepTime); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/Helpers/TestExtensionHelper.cs b/src/XMakeBuildEngine/ApexTests/Helpers/TestExtensionHelper.cs new file mode 100644 index 00000000000..22dd2c67c2f --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/Helpers/TestExtensionHelper.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension verifier for the BuildManager implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Globalization; + using System.Reflection; + using Microsoft.Test.Apex; + + /// + /// Helper class to create new Test Extensions for extensions which derive of TestExtension + /// + public static class TestExtensionHelper + { + /// + /// Creates a new test extension. + /// + /// Type of test extension. + /// Type the test extension is for. + /// Object which is passed to the test extension constructor. + /// TestExtension from where this is called. + /// Test extension created. + public static TTestExtension Create(TType type, TestExtension currentTestExtension) + where TType : class + where TTestExtension : TestExtension + { + Type testExtensionType = typeof(TTestExtension); + object[] parameters = { type }; + TTestExtension extension = (TTestExtension)testExtensionType.InvokeMember(null, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.CreateInstance, null, null, parameters, CultureInfo.InvariantCulture); + LifeTimeManagmentServiceTestExtension lifeTimeManagmentService = currentTestExtension.Container.GetFirstTestExtension(); + + // This is a temporary workaround to get any test extension to be able to get the lifetime managment service till we implement our own test extension factory. + if (extension.Container == null) + { + extension.Container = currentTestExtension.Container; + } + + lifeTimeManagmentService.AddToCompositionContainer(extension); + + return extension; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheTestExtension.cs b/src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheTestExtension.cs new file mode 100644 index 00000000000..b09194a9462 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheTestExtension.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the ResultsCache implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test extension for ResultsCache implementation. + /// + public class ResultsCacheTestExtension : TestExtension + { + /// + /// Initializes a new instance of the ResultsCacheTestExtension class. + /// + /// Instance of the result cache created by the build manager. + internal ResultsCacheTestExtension(ResultsCache resultsCache) + : base() + { + this.ResultsCache = resultsCache; + } + + /// + /// Gets the BuildResult object returned by the BuildManager. + /// + internal ResultsCache ResultsCache + { + get; + private set; + } + + /// + /// BuildResult for a specified configuration id. + /// + /// Configuration id. + /// BuildResultTestExtension for the configuration id. + public BuildResultTestExtension GetResultFromCache(int configurationId) + { + BuildResult result = this.ResultsCache.GetResultsForConfiguration(configurationId); + return TestExtensionHelper.Create(result, this); + } + } +} diff --git a/src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheVerifier.cs b/src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheVerifier.cs new file mode 100644 index 00000000000..a4121ace4f7 --- /dev/null +++ b/src/XMakeBuildEngine/ApexTests/ResultsCache/ResultsCacheVerifier.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test extension for the BuildResult implementation +//----------------------------------------------------------------------- +namespace Microsoft.Build.ApexTests.Library +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.Build.BackEnd; + using Microsoft.Build.Shared; + using Microsoft.Test.Apex; + + /// + /// Test verifier for BuildResult implementation. + /// + public class ResultsCacheVerifier : TestExtensionVerifier + { + /// + /// Gets Test extension associated with this verifier. + /// + internal new ResultsCacheTestExtension TestExtension + { + get + { + return base.TestExtension as ResultsCacheTestExtension; + } + } + } +} diff --git a/src/XMakeBuildEngine/AssemblyInfo.cs b/src/XMakeBuildEngine/AssemblyInfo.cs new file mode 100644 index 00000000000..19a8a3569af --- /dev/null +++ b/src/XMakeBuildEngine/AssemblyInfo.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// AssemblyInfo for Engine. +//----------------------------------------------------------------------- + +using System.Reflection; +using System.Security.Permissions; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable 618 +[assembly: SecurityPermission(SecurityAction.RequestMinimum, Flags = SecurityPermissionFlag.Execution)] +#pragma warning restore 618 + +#if (STANDALONEBUILD) +[assembly: AssemblyVersion(Microsoft.Build.Shared.MSBuildConstants.CurrentAssemblyVersion)] +[assembly: InternalsVisibleTo("Microsoft.Build.Engine.UnitTests")] +#else +[assembly: InternalsVisibleTo("Microsoft.Build.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Framework.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Engine.Whidbey.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.UnitTesting.Targets, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Engine.BackEndUnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.ApexTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +#endif +// DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface + +// This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, +// so that we don't run into known security issues with loading libraries from unsafe locations +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/XMakeBuildEngine/BackEnd/BuildManager/BuildManager.cs b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildManager.cs new file mode 100644 index 00000000000..224dd90473e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildManager.cs @@ -0,0 +1,1911 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of the Build Manager. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks.Dataflow; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; + +namespace Microsoft.Build.Execution +{ + /// + /// This class is the public entry point for executing builds. + /// + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Refactoring at the end of Beta1 is not appropriate.")] + public class BuildManager : INodePacketHandler, IBuildComponentHost, IDisposable + { + /// + /// The object used for thread-safe synchronization of static members. + /// + private static readonly Object s_staticSyncLock = new Object(); + + /// + /// The object used for thread-safe synchronization of BuildManager shared data and the Scheduler. + /// + private readonly Object _syncLock = new Object(); + + /// + /// The singleton instance for the BuildManager. + /// + static private BuildManager s_singletonInstance; + + /// + /// The next build id; + /// + static private int s_nextBuildId; + + /// + /// The next build request configuration ID to use. + /// These must be unique across build managers, as they + /// are used as part of cache file names, for example. + /// + private static int s_nextBuildRequestConfigurationId; + + /// + /// The cache for build request configurations. + /// + private IConfigCache _configCache; + + /// + /// The cache for build results. + /// + private IResultsCache _resultsCache; + + /// + /// The object responsible for creating and managing nodes. + /// + private INodeManager _nodeManager; + + /// + /// The object responsible for creating and managing task host nodes. + /// + private INodeManager _taskHostNodeManager; + + /// + /// The object which determines which projects to build, and where. + /// + private IScheduler _scheduler; + + /// + /// The node configuration to use for spawning new nodes. + /// + private NodeConfiguration _nodeConfiguration; + + /// + /// Any exception which occurs on a logging thread will go here. + /// + private Exception _threadException; + + /// + /// Set of active nodes in the system. + /// + private HashSet> _activeNodes; + + /// + /// Event signalled when all nodes have shutdown. + /// + private AutoResetEvent _noNodesActiveEvent; + + /// + /// Mapping of nodes to the configurations they know about. + /// + private Dictionary, HashSet>> _nodeIdToKnownConfigurations; + + /// + /// Flag indicating if we are currently shutting down. When set, we stop processing packets other than NodeShutdown. + /// + private bool _shuttingDown = false; + + /// + /// The current state of the BuildManager. + /// + private BuildManagerState _buildManagerState; + + /// + /// The name given to this BuildManager as the component host. + /// + private string _hostName; + + /// + /// The parameters with which the build was started. + /// + private BuildParameters _buildParameters; + + /// + /// The current pending and active submissions. + /// + /// + /// { submissionId, BuildSubmission } + /// + private Dictionary _buildSubmissions; + + /// + /// Event signalled when all build submissions are complete. + /// + private AutoResetEvent _noActiveSubmissionsEvent; + + /// + /// The overall success of the build. + /// + private bool _overallBuildSuccess; + + /// + /// The next build submission id. + /// + private int _nextBuildSubmissionId; + + /// + /// Mapping of unnamed project instances to the file names assigned to them. + /// + private Dictionary _unnamedProjectInstanceToNames; + + /// + /// The next ID to assign to a project which has no name. + /// + private int _nextUnnamedProjectId; + + /// + /// The build component factories. + /// + private BuildComponentFactoryCollection _componentFactories; + + /// + /// Mapping of submission IDs to their first project started events. + /// + private Dictionary _projectStartedEvents; + + /// + /// Whether a cache has been provided by a project instance, meaning + /// we've acquired at least one build submission that included a project instance. + /// Once that has happened, we use the provided one, rather than our default. + /// + private bool _acquiredProjectRootElementCacheFromProjectInstance; + + /// + /// The project started event handler + /// + private ProjectStartedEventHandler _projectStartedEventHandler; + + /// + /// The project finished event handler + /// + private ProjectFinishedEventHandler _projectFinishedEventHandler; + + /// + /// The logging exception event handler + /// + private LoggingExceptionDelegate _loggingThreadExceptionEventHandler; + + /// + /// Legacy threading semantic data associated with this build manager. + /// + private LegacyThreadingData _legacyThreadingData; + + /// + /// The worker queue. + /// + private ActionBlock _workQueue; + + /// + /// Flag indicating we have disposed. + /// + private bool _disposed = false; + + /// + /// Creates a new unnamed build manager. + /// Normally there is only one build manager in a process, and it is the default build manager. + /// Access it with + /// + public BuildManager() + : this("Unnamed") + { + } + + /// + /// Creates a new build manager with an arbitrary distinct name. + /// Normally there is only one build manager in a process, and it is the default build manager. + /// Access it with + /// + public BuildManager(string hostName) + { + ErrorUtilities.VerifyThrowArgumentNull(hostName, "hostName"); + _hostName = hostName; + _buildManagerState = BuildManagerState.Idle; + _buildSubmissions = new Dictionary(); + _noActiveSubmissionsEvent = new AutoResetEvent(true); + _activeNodes = new HashSet>(); + _noNodesActiveEvent = new AutoResetEvent(true); + _nodeIdToKnownConfigurations = new Dictionary, HashSet>>(); + _unnamedProjectInstanceToNames = new Dictionary(); + _nextUnnamedProjectId = 1; + _componentFactories = new BuildComponentFactoryCollection(this); + _componentFactories.RegisterDefaultFactories(); + _projectStartedEvents = new Dictionary(); + + _projectStartedEventHandler = new ProjectStartedEventHandler(OnProjectStarted); + _projectFinishedEventHandler = new ProjectFinishedEventHandler(OnProjectFinished); + _loggingThreadExceptionEventHandler = new LoggingExceptionDelegate(OnThreadException); + _legacyThreadingData = new LegacyThreadingData(); + } + + /// + /// Finalizer + /// + ~BuildManager() + { + Dispose(false /* disposing */); + } + + /// + /// Enumeration describing the current state of the build manager. + /// + private enum BuildManagerState + { + /// + /// This is the default state. may be called in this state. All other methods raise InvalidOperationException + /// + Idle, + + /// + /// This is the state the BuildManager is in after has been called but before has been called. + /// , and may be called in this state. + /// + Building, + + /// + /// This is the state the BuildManager is in after has been called but before all existing submissions have completed. + /// + WaitingForBuildToComplete + } + + /// + /// Gets the singleton instance of the Build Manager. + /// + public static BuildManager DefaultBuildManager + { + get + { + if (s_singletonInstance == null) + { + lock (s_staticSyncLock) + { + if (s_singletonInstance == null) + { + s_singletonInstance = new BuildManager("Default"); + } + } + } + + return s_singletonInstance; + } + } + + /// + /// Retrieves the logging service associated with a particular build + /// + /// The logging service. + ILoggingService IBuildComponentHost.LoggingService + { + get + { + return _componentFactories.GetComponent(BuildComponentType.LoggingService) as ILoggingService; + } + } + + /// + /// Retrieves the name of the component host. + /// + string IBuildComponentHost.Name + { + get + { + return _hostName; + } + } + + /// + /// Retrieves the build parameters associated with this build. + /// + /// The build parameters. + BuildParameters IBuildComponentHost.BuildParameters + { + get + { + return _buildParameters; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular build manager + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + /// + /// Prepares the BuildManager to receive build requests. + /// + /// The build parameters. May be null. + /// Thrown if a build is already in progress. + public void BeginBuild(BuildParameters parameters) + { + lock (_syncLock) + { + // Check for build in progress. + RequireState(BuildManagerState.Idle, "BuildInProgress"); + + if (BuildParameters.DumpOpportunisticInternStats) + { + OpportunisticIntern.EnableStatisticsGathering(); + } + + _overallBuildSuccess = true; + + // Clone off the build parameters. + if (parameters != null) + { + _buildParameters = parameters.Clone(); + } + else + { + _buildParameters = new BuildParameters(); + } + + // Initialize additional build parameters. + _buildParameters.BuildId = GetNextBuildId(); + + // Initialize components. + _nodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager) as INodeManager; + _taskHostNodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.TaskHostNodeManager) as INodeManager; + _scheduler = ((IBuildComponentHost)this).GetComponent(BuildComponentType.Scheduler) as IScheduler; + _configCache = ((IBuildComponentHost)this).GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + _resultsCache = ((IBuildComponentHost)this).GetComponent(BuildComponentType.ResultsCache) as IResultsCache; + + _nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestBlocker, BuildRequestBlocker.FactoryForDeserialization, this); + _nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestConfiguration, BuildRequestConfiguration.FactoryForDeserialization, this); + _nodeManager.RegisterPacketHandler(NodePacketType.BuildRequestConfigurationResponse, BuildRequestConfigurationResponse.FactoryForDeserialization, this); + _nodeManager.RegisterPacketHandler(NodePacketType.BuildResult, BuildResult.FactoryForDeserialization, this); + _nodeManager.RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this); + + if (_buildParameters.ResetCaches || _configCache.IsConfigCacheSizeLargerThanThreshold()) + { + ResetCaches(); + } + else + { + List configurationsCleared = _configCache.ClearNonExplicitlyLoadedConfigurations(); + + if (configurationsCleared != null) + { + foreach (int configurationId in configurationsCleared) + { + _resultsCache.ClearResultsForConfiguration(configurationId); + } + } + + foreach (var config in _configCache) + { + config.ResultsNodeId = Scheduler.InvalidNodeId; + } + + _buildParameters.ProjectRootElementCache.DiscardImplicitReferences(); + } + + // Set up the logging service. + ILoggingService loggingService = CreateLoggingService(_buildParameters.Loggers, _buildParameters.ForwardingLoggers); + + _nodeManager.RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, loggingService as INodePacketHandler); + try + { + loggingService.LogBuildStarted(); + } + catch (Exception) + { + ShutdownLoggingService(loggingService); + throw; + } + + if (_threadException != null) + { + ShutdownLoggingService(loggingService); + + // Unfortunately this will reset the callstack + throw _threadException; + } + + if (_workQueue == null) + { + _workQueue = new ActionBlock(action => ProcessWorkQueue(action)); + } + + _buildManagerState = BuildManagerState.Building; + + _noActiveSubmissionsEvent.Set(); + _noNodesActiveEvent.Set(); + } + } + + /// + /// Cancels all outstanding submissions asynchronously. + /// + public void CancelAllSubmissions() + { + CultureInfo parentThreadCulture = _buildParameters != null ? _buildParameters.Culture : Thread.CurrentThread.CurrentCulture; + CultureInfo parentThreadUICulture = _buildParameters != null ? _buildParameters.UICulture : Thread.CurrentThread.CurrentUICulture; + + WaitCallback callback = new WaitCallback( + delegate (object state) + { + lock (_syncLock) + { + if (_shuttingDown) + { + return; + } + + // If we are Idle, obviously there is nothing to cancel. If we are waiting for the build to end, then presumably all requests have already completed + // and there is nothing left to cancel. Putting this here eliminates the possibility of us racing with EndBuild to access the nodeManager before + // EndBuild sets it to null. + if (_buildManagerState != BuildManagerState.Building) + { + return; + } + + _overallBuildSuccess = false; + + foreach (BuildSubmission submission in _buildSubmissions.Values) + { + if (submission.BuildRequest != null) + { + BuildResult result = new BuildResult(submission.BuildRequest, new BuildAbortedException()); + _resultsCache.AddResult(result); + submission.CompleteResults(result); + } + } + + ShutdownConnectedNodesAsync(true /* abort */); + CheckForActiveNodesAndCleanUpSubmissions(); + } + }); + + ThreadPoolExtensions.QueueThreadPoolWorkItemWithCulture(callback, parentThreadCulture, parentThreadUICulture); + } + + /// + /// Clears out all of the cached information. + /// + public void ResetCaches() + { + lock (_syncLock) + { + ErrorIfState(BuildManagerState.WaitingForBuildToComplete, "WaitingForEndOfBuild"); + ErrorIfState(BuildManagerState.Building, "BuildInProgress"); + + _configCache = ((IBuildComponentHost)this).GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + _resultsCache = ((IBuildComponentHost)this).GetComponent(BuildComponentType.ResultsCache) as IResultsCache; + _resultsCache.ClearResults(); + + // This call clears out the directory. + _configCache.ClearConfigurations(); + + if (_buildParameters != null) + { + _buildParameters.ProjectRootElementCache.DiscardImplicitReferences(); + } + } + } + + /// + /// This methods requests the BuildManager to find a matching ProjectInstance in its cache of previously-built projects. + /// If none exist, a new instance will be created from the specified project. + /// + /// The Project for which an instance should be retrieved. + /// The instance. + public ProjectInstance GetProjectInstanceForBuild(Project project) + { + lock (_syncLock) + { + _configCache = ((IBuildComponentHost)this).GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + BuildRequestConfiguration configuration = _configCache.GetMatchingConfiguration( + new ConfigurationMetadata(project), + (config, loadProject) => CreateConfiguration(project, config), + loadProject: true); + ErrorUtilities.VerifyThrow(configuration.Project != null, "Configuration should have been loaded."); + return configuration.Project; + } + } + + /// + /// Submits a build request to the current build but does not start it immediately. Allows the user to + /// perform asynchronous execution or access the submission ID prior to executing the request. + /// + /// Thrown if StartBuild has not been called or if EndBuild has been called. + public BuildSubmission PendBuildRequest(BuildRequestData requestData) + { + lock (_syncLock) + { + ErrorUtilities.VerifyThrowArgumentNull(requestData, "requestData"); + ErrorIfState(BuildManagerState.WaitingForBuildToComplete, "WaitingForEndOfBuild"); + ErrorIfState(BuildManagerState.Idle, "NoBuildInProgress"); + VerifyStateInternal(BuildManagerState.Building); + + BuildSubmission newSubmission = new BuildSubmission(this, GetNextSubmissionId(), requestData, _buildParameters.LegacyThreadingSemantics); + _buildSubmissions.Add(newSubmission.SubmissionId, newSubmission); + _noActiveSubmissionsEvent.Reset(); + return newSubmission; + } + } + + /// + /// Convenience method. Submits a build request and blocks until the results are available. + /// + /// Thrown if StartBuild has not been called or if EndBuild has been called. + public BuildResult BuildRequest(BuildRequestData requestData) + { + BuildSubmission submission = PendBuildRequest(requestData); + return submission.Execute(); + } + + /// + /// Signals that no more build requests are expected (or allowed) and the BuildManager may clean up. + /// + /// + /// This call blocks until all currently pending requests are complete. + /// + /// Thrown if there is no build in progress. + public void EndBuild() + { + lock (_syncLock) + { + ErrorIfState(BuildManagerState.WaitingForBuildToComplete, "WaitingForEndOfBuild"); + ErrorIfState(BuildManagerState.Idle, "NoBuildInProgress"); + VerifyStateInternal(BuildManagerState.Building); + + // If there are any submissions which never started, remove them now. + List submissionsToCheck = new List(_buildSubmissions.Values); + foreach (BuildSubmission submission in submissionsToCheck) + { + CheckSubmissionCompletenessAndRemove(submission); + } + + _buildManagerState = BuildManagerState.WaitingForBuildToComplete; + } + + ILoggingService loggingService = ((IBuildComponentHost)this).LoggingService; + + try + { + _noActiveSubmissionsEvent.WaitOne(); + ShutdownConnectedNodesAsync(false /* normal termination */); + _noNodesActiveEvent.WaitOne(); + + // Wait for all of the actions in the work queue to drain. Wait() could throw here if there was an unhandled exception + // in the work queue, but the top level exception handler there should catch everything and have forwarded it to the + // OnThreadException method in this class already. + _workQueue.Complete(); + + ErrorUtilities.VerifyThrow(_buildSubmissions.Count == 0, "All submissions not yet complete."); + ErrorUtilities.VerifyThrow(_activeNodes.Count == 0, "All nodes not yet shut down."); + + if (loggingService != null) + { + loggingService.LogBuildFinished(_overallBuildSuccess); + } + +#if DEBUG + if (_projectStartedEvents.Count != 0) + { + bool allMismatchedProjectStartedEventsDueToLoggerErrors = true; + + foreach (var projectStartedEvent in _projectStartedEvents) + { + BuildResult result = _resultsCache.GetResultsForConfiguration(projectStartedEvent.Value.BuildEventContext.ProjectInstanceId); + + // It's valid to have a mismatched project started event IFF that particular + // project had some sort of unhandled exception. If there is no result, we + // can't tell for sure one way or the other, so err on the side of throwing + // the assert, but if there is a result, make sure that it actually has an + // exception attached. + if (result == null || result.Exception == null) + { + allMismatchedProjectStartedEventsDueToLoggerErrors = false; + break; + } + } + + Debug.Assert(allMismatchedProjectStartedEventsDueToLoggerErrors, "There was a mismatched project started event not caused by an exception result"); + } +#endif + } + finally + { + try + { + ShutdownLoggingService(loggingService); + } + finally + { + if (_buildParameters.LegacyThreadingSemantics == true) + { + _legacyThreadingData.MainThreadSubmissionId = -1; + } + + Reset(); + _buildManagerState = BuildManagerState.Idle; + + if (_threadException != null) + { + // Unfortunately this will reset the callstack + throw _threadException; + } + + if (BuildParameters.DumpOpportunisticInternStats) + { + OpportunisticIntern.ReportStatistics(); + } + } + } + } + + /// + /// Convenience method. Submits a lone build request and blocks until results are available. + /// + /// Thrown if a build is already in progress. + public BuildResult Build(BuildParameters parameters, BuildRequestData requestData) + { + BuildResult result; + BeginBuild(parameters); + try + { + result = BuildRequest(requestData); + if (result.Exception == null && _threadException != null) + { + result.Exception = _threadException; + _threadException = null; + } + } + finally + { + EndBuild(); + } + + return result; + } + + /// + /// Shuts down all idle MSBuild nodes on the machine + /// + public void ShutdownAllNodes() + { + if (null == _nodeManager) + { + _nodeManager = ((IBuildComponentHost)this).GetComponent(BuildComponentType.NodeManager) as INodeManager; + } + + _nodeManager.ShutdownAllNodes(); + } + + /// + /// Dispose of the build manager. + /// + public void Dispose() + { + Dispose(true /* disposing */); + GC.SuppressFinalize(this); + } + + #region INodePacketHandler Members + + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + void INodePacketHandler.PacketReceived(int node, INodePacket packet) + { + _workQueue.Post(() => this.ProcessPacket(node, packet)); + } + + #endregion + + #region IBuildComponentHost Members + + /// + /// Registers a factory which will be used to create the necessary components of the build + /// system. + /// + /// The type which is created by this factory. + /// The factory to be registered. + /// + /// It is not necessary to register any factories. If no factory is registered for a specific kind + /// of object, the system will use the default factory. + /// + void IBuildComponentHost.RegisterFactory(BuildComponentType componentType, BuildComponentFactoryDelegate factory) + { + _componentFactories.ReplaceFactory(componentType, factory); + } + + /// + /// Gets an instance of the specified component type from the host. + /// + /// The component type to be retrieved + /// The component + IBuildComponent IBuildComponentHost.GetComponent(BuildComponentType type) + { + return _componentFactories.GetComponent(type); + } + + #endregion + + /// + /// This method adds the request in the specified submission to the set of requests being handled by the scheduler. + /// + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Standard ExpectedException pattern used")] + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Complex class might need refactoring to separate scheduling elements from submission elements.")] + internal void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadBuild) + { + ErrorUtilities.VerifyThrowArgumentNull(submission, "submission"); + ErrorUtilities.VerifyThrow(!submission.IsCompleted, "Submission already complete."); + + ProjectInstance projectInstance = submission.BuildRequestData.ProjectInstance; + if (projectInstance != null) + { + if (_acquiredProjectRootElementCacheFromProjectInstance) + { + ErrorUtilities.VerifyThrowArgument(_buildParameters.ProjectRootElementCache == projectInstance.ProjectRootElementCache, "OM_BuildSubmissionsMultipleProjectCollections"); + } + else + { + _buildParameters.ProjectRootElementCache = projectInstance.ProjectRootElementCache; + _acquiredProjectRootElementCacheFromProjectInstance = true; + } + } + else if (_buildParameters.ProjectRootElementCache == null) + { + // Create our own cache; if we subsequently get a build submission with a project instance attached, + // we'll dump our cache and use that one. + _buildParameters.ProjectRootElementCache = new ProjectRootElementCache(false /* do not automatically reload from disk */); + } + + VerifyStateInternal(BuildManagerState.Building); + + try + { + // If we have an unnamed project, assign it a temporary name. + if (String.IsNullOrEmpty(submission.BuildRequestData.ProjectFullPath)) + { + ErrorUtilities.VerifyThrow(submission.BuildRequestData.ProjectInstance != null, "Unexpected null path for a submission with no ProjectInstance."); + + string tempName; + + // If we have already named this instance when it was submitted previously during this build, use the same + // name so that we get the same configuration (and thus don't cause it to rebuild.) + if (!_unnamedProjectInstanceToNames.TryGetValue(submission.BuildRequestData.ProjectInstance, out tempName)) + { + tempName = "Unnamed_" + _nextUnnamedProjectId++; + _unnamedProjectInstanceToNames[submission.BuildRequestData.ProjectInstance] = tempName; + } + + submission.BuildRequestData.ProjectFullPath = Path.Combine(submission.BuildRequestData.ProjectInstance.GetProperty(ReservedPropertyNames.projectDirectory).EvaluatedValue, tempName); + } + + // Create/Retrieve a configuration for each request + BuildRequestConfiguration buildRequestConfiguration = new BuildRequestConfiguration(submission.BuildRequestData, _buildParameters.DefaultToolsVersion, _buildParameters.GetToolset); + BuildRequestConfiguration matchingConfiguration = _configCache.GetMatchingConfiguration(buildRequestConfiguration); + BuildRequestConfiguration newConfiguration = ResolveConfiguration(buildRequestConfiguration, matchingConfiguration, (submission.BuildRequestData.Flags & BuildRequestDataFlags.ReplaceExistingProjectInstance) == BuildRequestDataFlags.ReplaceExistingProjectInstance); + + newConfiguration.ExplicitlyLoaded = true; + + // Now create the build request + submission.BuildRequest = new BuildRequest( + submission.SubmissionId, + Microsoft.Build.BackEnd.BuildRequest.InvalidNodeRequestId, + newConfiguration.ConfigurationId, + submission.BuildRequestData.TargetNames, + submission.BuildRequestData.HostServices, + BuildEventContext.Invalid, + null, + submission.BuildRequestData.Flags); + + if (_shuttingDown) + { + // We were already canceled! + BuildResult result = new BuildResult(submission.BuildRequest, new BuildAbortedException()); + submission.CompleteResults(result); + submission.CompleteLogging(true); + CheckSubmissionCompletenessAndRemove(submission); + return; + } + + // Submit the build request. + BuildRequestBlocker blocker = new BuildRequestBlocker(-1, new string[0], new BuildRequest[] { submission.BuildRequest }); + _workQueue.Post(() => + { + try + { + IssueRequestToScheduler(submission, allowMainThreadBuild, blocker); + } + catch (BuildAbortedException bae) + { + // We were canceled before we got issued by the work queue. + BuildResult result = new BuildResult(submission.BuildRequest, bae); + submission.CompleteResults(result); + submission.CompleteLogging(true); + CheckSubmissionCompletenessAndRemove(submission); + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + HandleExecuteSubmissionException(submission, ex); + } + }); + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + HandleExecuteSubmissionException(submission, ex); + throw; + } + } + + /// + /// Creates the traversal and metaproject instances necessary to represent the solution and populates new configurations with them. + /// + internal void LoadSolutionIntoConfiguration(BuildRequestConfiguration config, BuildEventContext buildEventContext) + { + if (config.IsLoaded) + { + // We've already processed it, nothing to do. + return; + } + + ErrorUtilities.VerifyThrow(FileUtilities.IsSolutionFilename(config.ProjectFullPath), "{0} is not a solution", config.ProjectFullPath); + ProjectInstance[] instances = ProjectInstance.LoadSolutionForBuild(config.ProjectFullPath, config.Properties, config.ExplicitToolsVersionSpecified ? config.ToolsVersion : null, _buildParameters, ((IBuildComponentHost)this).LoggingService, buildEventContext, false /* loaded by solution parser*/); + + // The first instance is the traversal project, which goes into this configuration + config.Project = instances[0]; + + // The remaining instances are the metaprojects which describe the dependencies for each project as well as how to invoke the project itself. + for (int i = 1; i < instances.Length; i++) + { + // Create new configurations for each of these if they don't already exist. That could happen if there are multiple + // solutions in this build which refer to the same project, in which case we want them to refer to the same + // metaproject as well. + BuildRequestConfiguration newConfig = new BuildRequestConfiguration(GetNewConfigurationId(), instances[i]); + newConfig.ExplicitlyLoaded = config.ExplicitlyLoaded; + if (_configCache.GetMatchingConfiguration(newConfig) == null) + { + _configCache.AddConfiguration(newConfig); + } + } + } + + /// + /// Gets the next build id. + /// + private static int GetNextBuildId() + { + return Interlocked.Increment(ref s_nextBuildId); + } + + /// + /// Creates and optionally populates a new configuration. + /// + private BuildRequestConfiguration CreateConfiguration(Project project, BuildRequestConfiguration existingConfiguration) + { + ProjectInstance newInstance = project.CreateProjectInstance(); + + if (existingConfiguration == null) + { + existingConfiguration = new BuildRequestConfiguration(GetNewConfigurationId(), new BuildRequestData(newInstance, new string[] { }), null /* use the instance's tools version */, null /* shouldn't need to get toolsets because ProjectInstance's ToolsVersion overrides */); + } + else + { + existingConfiguration.Project = newInstance; + } + + return existingConfiguration; + } + + /// + /// Processes the next action in the work queue. + /// + /// The action to be processed. + private void ProcessWorkQueue(Action action) + { + try + { + var oldCulture = Thread.CurrentThread.CurrentCulture; + var oldUICulture = Thread.CurrentThread.CurrentUICulture; + + try + { + if (Thread.CurrentThread.CurrentCulture != _buildParameters.Culture) + { + Thread.CurrentThread.CurrentCulture = _buildParameters.Culture; + } + + if (Thread.CurrentThread.CurrentUICulture != _buildParameters.UICulture) + { + Thread.CurrentThread.CurrentUICulture = _buildParameters.UICulture; + } + + action(); + } + catch (Exception ex) + { + // These need to go to the main thread exception handler. We can't rethrow here because that will just silently stop the + // action block. Instead, send them over to the main handler for the BuildManager. + this.OnThreadException(ex); + } + finally + { + // Set the culture back to the original one so that if something else reuses this thread then it will not have a culture which it was not expecting. + if (Thread.CurrentThread.CurrentCulture != oldCulture) + { + Thread.CurrentThread.CurrentCulture = oldCulture; + } + + if (Thread.CurrentThread.CurrentUICulture != oldUICulture) + { + Thread.CurrentThread.CurrentUICulture = oldUICulture; + } + } + } + catch (Exception e) + { + // On the off chance we get an exception from our exception handler (oh, the irony!), we want to know about it (and still not kill this block + // which could lead to a somewhat mysterious hang.) + ExceptionHandling.DumpExceptionToFile(e); + } + } + + /// + /// Processes a packet + /// + private void ProcessPacket(int node, INodePacket packet) + { + lock (_syncLock) + { + if (_shuttingDown && packet.Type != NodePacketType.NodeShutdown) + { + // Console.WriteLine("Discarding packet {0} from node {1} because we are shutting down.", packet.Type, node); + return; + } + + switch (packet.Type) + { + case NodePacketType.BuildRequestBlocker: + BuildRequestBlocker blocker = ExpectPacketType(packet, NodePacketType.BuildRequestBlocker); + HandleNewRequest(node, blocker); + break; + + case NodePacketType.BuildRequestConfiguration: + BuildRequestConfiguration requestConfiguration = ExpectPacketType(packet, NodePacketType.BuildRequestConfiguration); + HandleConfigurationRequest(node, requestConfiguration); + break; + + case NodePacketType.BuildResult: + BuildResult result = ExpectPacketType(packet, NodePacketType.BuildResult); + HandleResult(node, result); + break; + + case NodePacketType.NodeShutdown: + // Remove the node from the list of active nodes. When they are all done, we have shut down fully + NodeShutdown shutdownPacket = ExpectPacketType(packet, NodePacketType.NodeShutdown); + HandleNodeShutdown(node, shutdownPacket); + break; + + default: + ErrorUtilities.ThrowInternalError("Unexpected packet received by BuildManager: {0}", packet.Type); + break; + } + } + } + + /// + /// Deals with exceptions that may be thrown as a result of ExecuteSubmission. + /// + private void HandleExecuteSubmissionException(BuildSubmission submission, Exception ex) + { + InvalidProjectFileException projectException = ex as InvalidProjectFileException; + + if (projectException != null) + { + if (projectException.HasBeenLogged != true) + { + BuildEventContext buildEventContext = new BuildEventContext(submission.SubmissionId, 1, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); + ((IBuildComponentHost)this).LoggingService.LogInvalidProjectFileError(buildEventContext, projectException); + projectException.HasBeenLogged = true; + } + } + + // BuildRequest may be null if the submission fails early on. + if (submission.BuildRequest != null) + { + BuildResult result = new BuildResult(submission.BuildRequest, ex); + submission.CompleteResults(result); + submission.CompleteLogging(true); + } + + _overallBuildSuccess = false; + CheckSubmissionCompletenessAndRemove(submission); + } + + /// + /// Sends the request to the scheduler with optional legacy threading semantics behavior. + /// + private void IssueRequestToScheduler(BuildSubmission submission, bool allowMainThreadBuild, BuildRequestBlocker blocker) + { + bool resetMainThreadOnFailure = false; + try + { + lock (_syncLock) + { + if (_shuttingDown) + { + throw new BuildAbortedException(); + } + + if (allowMainThreadBuild && _buildParameters.LegacyThreadingSemantics) + { + if (_legacyThreadingData.MainThreadSubmissionId == -1) + { + resetMainThreadOnFailure = true; + _legacyThreadingData.MainThreadSubmissionId = submission.SubmissionId; + } + } + + HandleNewRequest(Scheduler.VirtualNode, blocker); + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + InvalidProjectFileException projectException = ex as InvalidProjectFileException; + if (projectException != null) + { + if (projectException.HasBeenLogged != true) + { + BuildEventContext projectBuildEventContext = new BuildEventContext(submission.SubmissionId, 1, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); + ((IBuildComponentHost)this).LoggingService.LogInvalidProjectFileError(projectBuildEventContext, projectException); + projectException.HasBeenLogged = true; + } + } + else if ((ex is BuildAbortedException) || ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + if (resetMainThreadOnFailure) + { + _legacyThreadingData.MainThreadSubmissionId = -1; + } + + if (projectException == null) + { + BuildEventContext buildEventContext = new BuildEventContext(submission.SubmissionId, 1, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); + ((IBuildComponentHost)this).LoggingService.LogFatalBuildError(buildEventContext, ex, new BuildEventFileInfo(submission.BuildRequestData.ProjectFullPath)); + } + + submission.CompleteLogging(true); + ReportResultsToSubmission(new BuildResult(submission.BuildRequest, ex)); + _overallBuildSuccess = false; + } + } + + /// + /// Asks the nodeManager to tell the currently connected nodes to shut down and sets a flag preventing all non-shutdown-related packets from + /// being processed. + /// + private void ShutdownConnectedNodesAsync(bool abort) + { + _shuttingDown = true; + + // If we are aborting, we will NOT reuse the nodes because their state may be compromised by attempts to shut down while the build is in-progress. + _nodeManager.ShutdownConnectedNodes(abort ? false : _buildParameters.EnableNodeReuse); + + // if we are aborting, the task host will hear about it in time through the task building infrastructure; + // so only shut down the task host nodes if we're shutting down tidily (in which case, it is assumed that all + // tasks are finished building and thus that there's no risk of a race between the two shutdown pathways). + if (!abort) + { + _taskHostNodeManager.ShutdownConnectedNodes(_buildParameters.EnableNodeReuse); + } + } + + /// + /// Retrieves the next build submission id. + /// + private int GetNextSubmissionId() + { + return _nextBuildSubmissionId++; + } + + /// + /// Errors if the BuildManager is in the specified state. + /// + private void ErrorIfState(BuildManagerState disallowedState, string exceptionResouorce) + { + if (_buildManagerState == disallowedState) + { + ErrorUtilities.ThrowInvalidOperation(exceptionResouorce); + } + } + + /// + /// Verifies the BuildManager is in the required state, and throws a if it is not. + /// + private void RequireState(BuildManagerState requiredState, string exceptionResouorce) + { + ErrorUtilities.VerifyThrowInvalidOperation(_buildManagerState == requiredState, exceptionResouorce); + } + + /// + /// Verifies the BuildManager is in the required state, and throws a if it is not. + /// + private void VerifyStateInternal(BuildManagerState requiredState) + { + if (_buildManagerState != requiredState) + { + ErrorUtilities.ThrowInternalError("Expected state {0}, actual state {1}", requiredState, _buildManagerState); + } + } + + /// + /// Method called to reset the state of the system after a build. + /// + private void Reset() + { + _nodeManager.UnregisterPacketHandler(NodePacketType.BuildRequestBlocker); + _nodeManager.UnregisterPacketHandler(NodePacketType.BuildRequestConfiguration); + _nodeManager.UnregisterPacketHandler(NodePacketType.BuildRequestConfigurationResponse); + _nodeManager.UnregisterPacketHandler(NodePacketType.BuildResult); + _nodeManager.UnregisterPacketHandler(NodePacketType.NodeShutdown); + _nodeManager.ClearPerBuildState(); + _nodeManager = null; + + _shuttingDown = false; + _nodeConfiguration = null; + _buildSubmissions.Clear(); + _scheduler.Reset(); + _scheduler = null; + _workQueue = null; + _acquiredProjectRootElementCacheFromProjectInstance = false; + + _unnamedProjectInstanceToNames.Clear(); + _projectStartedEvents.Clear(); + _nodeIdToKnownConfigurations.Clear(); + _nextUnnamedProjectId = 1; + + if (_configCache != null) + { + foreach (BuildRequestConfiguration config in _configCache) + { + config.ActivelyBuildingTargets.Clear(); + } + } + + if (Environment.GetEnvironmentVariable("MSBUILDCLEARXMLCACHEONBUILDMANAGER") == "1") + { + // Optionally clear out the cache. This has the advantage of releasing memory, + // but the disadvantage of causing the next build to repeat the load and parse. + // We'll experiment here and ship with the best default. + _buildParameters.ProjectRootElementCache.Clear(); + } + } + + /// + /// Returns a new, valid configuration id. + /// + private int GetNewConfigurationId() + { + int newId = Interlocked.Increment(ref s_nextBuildRequestConfigurationId); + + if (_scheduler != null) + { + // Minimum configuration id is always the lowest valid configuration id available, so increment after returning. + while (newId <= _scheduler.MinimumAssignableConfigurationId) // Currently this minimum is one + { + newId = Interlocked.Increment(ref s_nextBuildRequestConfigurationId); + } + } + + return newId; + } + + /// + /// Finds a matching configuration in the cache and returns it, or stores the configuration passed in. + /// + private BuildRequestConfiguration ResolveConfiguration(BuildRequestConfiguration unresolvedConfiguration, BuildRequestConfiguration matchingConfigurationFromCache, bool replaceProjectInstance) + { + BuildRequestConfiguration resolvedConfiguration = matchingConfigurationFromCache ?? _configCache.GetMatchingConfiguration(unresolvedConfiguration); + if (resolvedConfiguration == null) + { + int newConfigurationId = _scheduler.GetConfigurationIdFromPlan(unresolvedConfiguration.ProjectFullPath); + if (_configCache.HasConfiguration(newConfigurationId) || (newConfigurationId == BuildRequestConfiguration.InvalidConfigurationId)) + { + // There is already a configuration like this one or one didn't exist in a plan, so generate a new ID. + newConfigurationId = GetNewConfigurationId(); + } + + resolvedConfiguration = unresolvedConfiguration.ShallowCloneWithNewId(newConfigurationId); + _configCache.AddConfiguration(resolvedConfiguration); + } + else if (replaceProjectInstance && unresolvedConfiguration.Project != null) + { + resolvedConfiguration.Project = unresolvedConfiguration.Project; + _resultsCache.ClearResultsForConfiguration(resolvedConfiguration.ConfigurationId); + } + else if (unresolvedConfiguration.Project != null && resolvedConfiguration.Project != null && !Object.ReferenceEquals(unresolvedConfiguration.Project, resolvedConfiguration.Project)) + { + // The user passed in a different instance than the one we already had. Throw away any corresponding results. + _resultsCache.ClearResultsForConfiguration(resolvedConfiguration.ConfigurationId); + resolvedConfiguration.Project = unresolvedConfiguration.Project; + } + + return resolvedConfiguration; + } + + /// + /// Handles a new request coming from a node. + /// + private void HandleNewRequest(int node, BuildRequestBlocker blocker) + { + // If we received any solution files, populate their configurations now. + if (blocker.BuildRequests != null) + { + foreach (BuildRequest request in blocker.BuildRequests) + { + BuildRequestConfiguration config = _configCache[request.ConfigurationId]; + if (FileUtilities.IsSolutionFilename(config.ProjectFullPath)) + { + try + { + LoadSolutionIntoConfiguration(config, request.BuildEventContext); + } + catch (InvalidProjectFileException e) + { + // Throw the error in the cache. The Scheduler will pick it up and return the results correctly. + _resultsCache.AddResult(new BuildResult(request, e)); + if (node == Scheduler.VirtualNode) + { + throw; + } + } + } + } + } + + IEnumerable response = _scheduler.ReportRequestBlocked(node, blocker); + PerformSchedulingActions(response); + } + + /// + /// Handles a configuration request coming from a node. + /// + private void HandleConfigurationRequest(int node, BuildRequestConfiguration unresolvedConfiguration) + { + BuildRequestConfiguration resolvedConfiguration = ResolveConfiguration(unresolvedConfiguration, null, false); + + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(unresolvedConfiguration.ConfigurationId, resolvedConfiguration.ConfigurationId, resolvedConfiguration.ResultsNodeId); + + HashSet> configurationsOnNode = null; + if (!_nodeIdToKnownConfigurations.TryGetValue(node, out configurationsOnNode)) + { + configurationsOnNode = new HashSet>(); + _nodeIdToKnownConfigurations[node] = configurationsOnNode; + } + + configurationsOnNode.Add(resolvedConfiguration.ConfigurationId); + + _nodeManager.SendData(node, response); + } + + /// + /// Handles a build result coming from a node. + /// + private void HandleResult(int node, BuildResult result) + { + // Update cache with the default and initial targets, as needed. + BuildRequestConfiguration configuration = _configCache[result.ConfigurationId]; + if (result.DefaultTargets != null) + { + // If the result has Default and Initial targets, we populate the configuration cache with them if it + // doesn't already have entries. This can happen if we created a configuration based on a request from + // an external node, but hadn't yet received a result since we may not have loaded the Project locally + // and thus wouldn't know what the default and initial targets were. + if (configuration.ProjectDefaultTargets == null) + { + configuration.ProjectDefaultTargets = result.DefaultTargets; + } + + if (configuration.ProjectInitialTargets == null) + { + configuration.ProjectInitialTargets = result.InitialTargets; + } + } + + IEnumerable response = _scheduler.ReportResult(node, result); + PerformSchedulingActions(response); + } + + /// + /// Handles the NodeShutdown packet + /// + private void HandleNodeShutdown(int node, NodeShutdown shutdownPacket) + { + _shuttingDown = true; + ErrorUtilities.VerifyThrow(_activeNodes.Contains(node), "Unexpected shutdown from node {0} which shouldn't exist.", node); + _activeNodes.Remove(node); + + if (shutdownPacket.Reason != NodeShutdownReason.Requested) + { + if (shutdownPacket.Reason == NodeShutdownReason.ConnectionFailed) + { + ILoggingService loggingService = ((IBuildComponentHost)this).GetComponent(BuildComponentType.LoggingService) as ILoggingService; + foreach (BuildSubmission submission in _buildSubmissions.Values) + { + BuildEventContext buildEventContext = new BuildEventContext(submission.SubmissionId, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); + loggingService.LogError(buildEventContext, new BuildEventFileInfo(String.Empty) /* no project file */, "ChildExitedPrematurely", node); + } + } + else if (shutdownPacket.Reason == NodeShutdownReason.Error && _buildSubmissions.Values.Count == 0) + { + // We have no submissions to attach any exceptions to, lets just log it here. + if (shutdownPacket.Exception != null) + { + ILoggingService loggingService = ((IBuildComponentHost)this).GetComponent(BuildComponentType.LoggingService) as ILoggingService; + loggingService.LogError(BuildEventContext.Invalid, new BuildEventFileInfo(String.Empty) /* no project file */, "ChildExitedPrematurely", shutdownPacket.Exception.ToString()); + OnThreadException(shutdownPacket.Exception); + } + } + + _nodeManager.ShutdownConnectedNodes(_buildParameters.EnableNodeReuse); + _taskHostNodeManager.ShutdownConnectedNodes(_buildParameters.EnableNodeReuse); + + foreach (BuildSubmission submission in _buildSubmissions.Values) + { + // The submission has not started + if (submission.BuildRequest == null) + { + continue; + } + + _resultsCache.AddResult(new BuildResult(submission.BuildRequest, shutdownPacket.Exception ?? new BuildAbortedException())); + } + + _scheduler.ReportBuildAborted(node); + } + + CheckForActiveNodesAndCleanUpSubmissions(); + } + + /// + /// If there are no more active nodes, cleans up any remaining submissions. + /// + /// + /// Must only be called from within the sync lock. + /// + private void CheckForActiveNodesAndCleanUpSubmissions() + { + if (_activeNodes.Count == 0) + { + List submissions = new List(_buildSubmissions.Values); + foreach (BuildSubmission submission in submissions) + { + // The submission has not started do not add it to the results cache + if (submission.BuildRequest == null) + { + continue; + } + + // UNDONE: (stability) It might be best to trigger the logging service to shut down here, + // since the full build is complete. This would allow us to ensure all logging messages have been + // drained and all submissions can complete their logging requirements. + BuildResult result = _resultsCache.GetResultsForConfiguration(submission.BuildRequest.ConfigurationId); + if (result == null) + { + // If we had no results, the build aborted before we had a chance to generate any. + result = new BuildResult(submission.BuildRequest, new BuildAbortedException()); + } + + submission.CompleteResults(result); + + // If we never received a project started event, consider logging complete anyhow, since the nodes have + // shut down. + submission.CompleteLogging(waitForLoggingThread: false); + + _overallBuildSuccess = _overallBuildSuccess && (submission.BuildResult.OverallResult == BuildResultCode.Success); + CheckSubmissionCompletenessAndRemove(submission); + } + + _noNodesActiveEvent.Set(); + } + } + + /// + /// Carries out the actions specified by the scheduler. + /// + private void PerformSchedulingActions(IEnumerable responses) + { + foreach (ScheduleResponse response in responses) + { + switch (response.Action) + { + case ScheduleActionType.NoAction: + break; + + case ScheduleActionType.SubmissionComplete: + if (_buildParameters.DetailedSummary) + { + _scheduler.WriteDetailedSummary(response.BuildResult.SubmissionId); + } + + ReportResultsToSubmission(response.BuildResult); + break; + + case ScheduleActionType.CircularDependency: + case ScheduleActionType.ResumeExecution: + case ScheduleActionType.ReportResults: + _nodeManager.SendData(response.NodeId, response.Unblocker); + break; + + case ScheduleActionType.CreateNode: + List newNodes = new List(); + + for (int i = 0; i < response.NumberOfNodesToCreate; i++) + { + NodeInfo createdNode = _nodeManager.CreateNode(GetNodeConfiguration(), response.RequiredNodeType); + + if (null != createdNode) + { + _noNodesActiveEvent.Reset(); + _activeNodes.Add(createdNode.NodeId); + newNodes.Add(createdNode); + ErrorUtilities.VerifyThrow(_activeNodes.Count != 0, "Still 0 nodes after asking for a new node. Build cannot proceed."); + } + else + { + BuildEventContext buildEventContext = new BuildEventContext(0, Scheduler.VirtualNode, BuildEventContext.InvalidProjectInstanceId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); + ((IBuildComponentHost)this).LoggingService.LogError(buildEventContext, new BuildEventFileInfo(String.Empty), "UnableToCreateNode", response.RequiredNodeType.ToString("G")); + + throw new BuildAbortedException(ResourceUtilities.FormatResourceString("UnableToCreateNode", response.RequiredNodeType.ToString("G"))); + } + } + + IEnumerable newResponses = _scheduler.ReportNodesCreated(newNodes); + PerformSchedulingActions(newResponses); + + break; + + case ScheduleActionType.Schedule: + case ScheduleActionType.ScheduleWithConfiguration: + if (response.Action == ScheduleActionType.ScheduleWithConfiguration) + { + // Only actually send the configuration if the node doesn't know about it. The scheduler only keeps track + // of which nodes have had configurations specifically assigned to them for building. However, a node may + // have created a configuration based on a build request it needs to wait on. In this + // case we need not send the configuration since it will already have been mapped earlier. + HashSet> configurationsOnNode = null; + if (!_nodeIdToKnownConfigurations.TryGetValue(response.NodeId, out configurationsOnNode) || + !configurationsOnNode.Contains(response.BuildRequest.ConfigurationId)) + { + IConfigCache configCache = _componentFactories.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + _nodeManager.SendData(response.NodeId, configCache[response.BuildRequest.ConfigurationId]); + } + } + + _nodeManager.SendData(response.NodeId, response.BuildRequest); + break; + + default: + ErrorUtilities.ThrowInternalError("Scheduling action {0} not handled.", response.Action); + break; + } + } + } + + /// + /// Completes a submission using the specified overall results. + /// + private void ReportResultsToSubmission(BuildResult result) + { + lock (_syncLock) + { + // The build submission has not already been completed. + if (_buildSubmissions.ContainsKey(result.SubmissionId)) + { + BuildSubmission submission = _buildSubmissions[result.SubmissionId]; + submission.CompleteResults(result); + + // If the request failed because we caught an exception from the loggers, we can assume we will receive no more logging messages for + // this submission, therefore set the logging as complete. IntrnalLoggerExceptions are unhandled exceptions from the logger. If the logger author does + // not handle an exception the eventsource wraps all exceptions (except a logging exception) into an internal logging exception. + // These exceptions will have their stack logged on the commandline as an unexpected failure. If a logger author wants the logger + // to fail gracefully then can catch an exception and log a LoggerException. This has the same effect of stopping the build but it logs only + // the exception error message rather than the whole stack trace. + if (result.Exception is InternalLoggerException || result.Exception is LoggerException || result.Exception is InvalidOperationException) + { + submission.CompleteLogging(false /* waitForLoggingThread */); + } + + _overallBuildSuccess = _overallBuildSuccess && (_buildSubmissions[result.SubmissionId].BuildResult.OverallResult == BuildResultCode.Success); + + CheckSubmissionCompletenessAndRemove(submission); + } + } + } + + /// + /// Determines if the submission is fully completed. + /// + private void CheckSubmissionCompletenessAndRemove(BuildSubmission submission) + { + lock (_syncLock) + { + // If the submission has completed or never started, remove it. + if (submission.IsCompleted || submission.BuildRequest == null) + { + _buildSubmissions.Remove(submission.SubmissionId); + } + + if (_buildSubmissions.Count == 0) + { + _noActiveSubmissionsEvent.Set(); + } + } + } + + /// + /// Retrieves the configuration structure for a node. + /// + private NodeConfiguration GetNodeConfiguration() + { + if (null == _nodeConfiguration) + { + // Get the remote loggers + ILoggingService loggingService = ((IBuildComponentHost)this).GetComponent(BuildComponentType.LoggingService) as ILoggingService; + List remoteLoggers = new List(loggingService.LoggerDescriptions); + + _nodeConfiguration = new NodeConfiguration + ( + -1, /* must be assigned by the NodeManager */ + _buildParameters, + remoteLoggers.ToArray(), + AppDomain.CurrentDomain.SetupInformation + ); + } + + return _nodeConfiguration; + } + + /// + /// Handler for thread exceptions (logging thread, communications thread). This handler will only get called if the exception did not previously + /// get handled by a node exception handlers (for instance because the build is complete for the node.) In this case we + /// get the exception and will put it into the OverallBuildResult so that the host can see what happened. + /// + private void OnThreadException(Exception e) + { + lock (_syncLock) + { + if (_threadException == null) + { + _threadException = e; + List submissions = new List(_buildSubmissions.Values); + foreach (BuildSubmission submission in submissions) + { + // Submission has not started + if (submission.BuildRequest == null) + { + continue; + } + + submission.CompleteLogging(false); + + // Attach the exception to this submission if it does not already have an exception associated with it + if (submission.BuildResult == null || (submission.BuildResult.Exception == null)) + { + if (submission.BuildResult == null) + { + submission.BuildResult = new BuildResult(submission.BuildRequest, e); + } + else + { + submission.BuildResult.Exception = _threadException; + } + } + + CheckSubmissionCompletenessAndRemove(submission); + } + } + } + } + + /// + /// Raised when a project finished logging message has been processed. + /// + private void OnProjectFinished(object sender, ProjectFinishedEventArgs e) + { + lock (_syncLock) + { + BuildEventArgs originalArgs; + if (_projectStartedEvents.TryGetValue(e.BuildEventContext.SubmissionId, out originalArgs)) + { + if (originalArgs.BuildEventContext.Equals(e.BuildEventContext)) + { + BuildSubmission submission; + _projectStartedEvents.Remove(e.BuildEventContext.SubmissionId); + if (_buildSubmissions.TryGetValue(e.BuildEventContext.SubmissionId, out submission)) + { + submission.CompleteLogging(false); + CheckSubmissionCompletenessAndRemove(submission); + } + } + } + } + } + + /// + /// Raised when a project started logging message is about to be processed. + /// + private void OnProjectStarted(object sender, ProjectStartedEventArgs e) + { + if (!_projectStartedEvents.ContainsKey(e.BuildEventContext.SubmissionId)) + { + _projectStartedEvents[e.BuildEventContext.SubmissionId] = e; + } + } + + /// + /// Creates a logging service around the specified set of loggers. + /// + private ILoggingService CreateLoggingService(IEnumerable loggers, IEnumerable forwardingLoggers) + { + int cpuCount = _buildParameters.MaxNodeCount; + + LoggerMode loggerMode = (cpuCount == 1 && _buildParameters.UseSynchronousLogging) ? LoggerMode.Synchronous : LoggerMode.Asynchronous; + + ILoggingService loggingService = (ILoggingService)Microsoft.Build.BackEnd.Logging.LoggingService.CreateLoggingService(loggerMode, 1 /*This logging service is used for the build manager and the inproc node, therefore it should have the first nodeId*/); + + ((IBuildComponent)loggingService).InitializeComponent(this); + _componentFactories.ReplaceFactory(BuildComponentType.LoggingService, loggingService as IBuildComponent); + + _threadException = null; + loggingService.OnLoggingThreadException += _loggingThreadExceptionEventHandler; + loggingService.OnProjectStarted += _projectStartedEventHandler; + loggingService.OnProjectFinished += _projectFinishedEventHandler; + + try + { + if (loggers != null) + { + foreach (ILogger logger in loggers) + { + loggingService.RegisterLogger(logger); + } + } + + if (loggingService.Loggers.Count == 0) + { + // We need to register SOME logger if we don't have any. This ensures the out of proc nodes will still send us message, + // ensuring we receive project started and finished events. + Assembly engineAssembly = Assembly.GetAssembly(typeof(ProjectCollection)); + LoggerDescription forwardingLoggerDescription = new LoggerDescription( + loggerClassName: typeof(ConfigurableForwardingLogger).FullName, + loggerAssemblyName: typeof(ConfigurableForwardingLogger).Assembly.GetName().FullName, + loggerAssemblyFile: null, + loggerSwitchParameters: "PROJECTSTARTEDEVENT;PROJECTFINISHEDEVENT", + verbosity: LoggerVerbosity.Quiet); + + ForwardingLoggerRecord[] forwardingLogger = { new ForwardingLoggerRecord(new NullLogger(), forwardingLoggerDescription) }; + forwardingLoggers = forwardingLoggers == null ? forwardingLogger : forwardingLoggers.Concat(forwardingLogger); + } + + if (forwardingLoggers != null) + { + foreach (ForwardingLoggerRecord forwardingLoggerRecord in forwardingLoggers) + { + loggingService.RegisterDistributedLogger(forwardingLoggerRecord.CentralLogger, forwardingLoggerRecord.ForwardingLoggerDescription); + } + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + if (loggingService != null) + { + ShutdownLoggingService(loggingService); + } + + throw; + } + + return loggingService; + } + + /// + /// Ensures that the packet type matches the expected type + /// + /// The instance-type of packet being expected + private I ExpectPacketType(INodePacket packet, NodePacketType expectedType) where I : class, INodePacket + { + I castPacket = packet as I; + + // PERF: Not using VerifyThrow here to avoid boxing of expectedType. + if (castPacket == null) + { + ErrorUtilities.ThrowInternalError("Incorrect packet type: {0} should have been {1}", packet.Type, expectedType); + } + + return castPacket; + } + + /// + /// Shutdown the logging service + /// + private void ShutdownLoggingService(ILoggingService loggingService) + { + try + { + if (loggingService != null) + { + loggingService.OnLoggingThreadException -= _loggingThreadExceptionEventHandler; + loggingService.OnProjectFinished -= _projectFinishedEventHandler; + loggingService.OnProjectStarted -= _projectStartedEventHandler; + _componentFactories.ShutdownComponent(BuildComponentType.LoggingService); + } + } + finally + { + // Even if an exception is thrown, we want to make sure we null out the logging service so that + // we don't try to shut it down again in some other cleanup code. + _componentFactories.ReplaceFactory(BuildComponentType.LoggingService, (IBuildComponent)null); + } + } + + /// + /// Dispose implementation + /// + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + lock (_syncLock) + { + // We should always have finished cleaning up before calling Dispose. + RequireState(BuildManagerState.Idle, "ShouldNotDisposeWhenBuildManagerActive"); + + if (_componentFactories != null) + { + _componentFactories.ShutdownComponents(); + } + + if (_workQueue != null) + { + _workQueue.Complete(); + _workQueue = null; + } + + if (_noActiveSubmissionsEvent != null) + { + _noActiveSubmissionsEvent.Dispose(); + _noActiveSubmissionsEvent = null; + } + + if (_noNodesActiveEvent != null) + { + _noNodesActiveEvent.Dispose(); + _noNodesActiveEvent = null; + } + + if (Object.ReferenceEquals(this, BuildManager.s_singletonInstance)) + { + BuildManager.s_singletonInstance = null; + } + + _disposed = true; + } + } + } + } + + /// + /// The logger registered to the logging service when no other one is. + /// + private class NullLogger : ILogger + { + #region ILogger Members + + /// + /// The logger verbosity. + /// + public LoggerVerbosity Verbosity + { + get + { + return LoggerVerbosity.Normal; + } + + set + { + } + } + + /// + /// The logger parameters. + /// + public string Parameters + { + get + { + return String.Empty; + } + + set + { + } + } + + /// + /// Initialize. + /// + public void Initialize(IEventSource eventSource) + { + } + + /// + /// Shutdown. + /// + public void Shutdown() + { + } + + #endregion + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/BuildManager/BuildParameters.cs b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildParameters.cs new file mode 100644 index 00000000000..383f01c8650 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildParameters.cs @@ -0,0 +1,1086 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class holding the parameters and settings which are global to the build. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; + +namespace Microsoft.Build.Execution +{ + /// + /// This class represents all of the settings which must be specified to start a build. + /// + public class BuildParameters : INodePacketTranslatable + { + /// + /// The default thread stack size for threads owned by MSBuild. + /// + private const int DefaultThreadStackSize = 262144; // 256k + + /// + /// The timeout for endpoints to shut down. + /// + private const int DefaultEndpointShutdownTimeout = 30 * 1000; // 30 seconds + + /// + /// The timeout for the engine to shutdown. + /// + private const int DefaultEngineShutdownTimeout = Timeout.Infinite; + + /// + /// The shutdown timeout for the logging thread. + /// + private const int DefaultLoggingThreadShutdownTimeout = 30 * 1000; // 30 seconds + + /// + /// The shutdown timeout for the request builder. + /// + private const int DefaultRequestBuilderShutdownTimeout = Timeout.Infinite; + + /// + /// The maximum number of idle request builders to retain before we start discarding them. + /// + private const int DefaultIdleRequestBuilderLimit = 2; + + /// + /// The startup directory. + /// + private static string s_startupDirectory = NativeMethodsShared.GetCurrentDirectory(); + + /// + /// Indicates whether we should warn when a property is uninitialized when it is used. + /// + private static bool? s_warnOnUninitializedProperty = null; + + /// + /// Indicates if we should dump string interning stats. + /// + private static bool? s_dumpOpportunisticInternStats = null; + + /// + /// Indicates if we should debug the expander. + /// + private static bool? s_debugExpansion = null; + + /// + /// Indicates if we should keep duplicate target outputs. + /// + private static bool? s_keepDuplicateOutputs = null; + + /// + /// Indicates if we should enable the build plan + /// + private static bool? s_enableBuildPlan = null; + + /// + /// The maximum number of idle request builders we will retain. + /// + private static int? s_idleRequestBuilderLimit = null; + + /// + /// Location that msbuild.exe was last successfully found at. + /// + private static string s_msbuildExeKnownToExistAt; + + /// + /// The build id + /// + private int _buildId = 0; + + /// + /// The thread priority with which to run the in-proc node. + /// + private ThreadPriority _buildThreadPriority = ThreadPriority.Normal; + + /// + /// The culture + /// + private CultureInfo _culture = Thread.CurrentThread.CurrentCulture; + + /// + /// The default tools version. + /// + private string _defaultToolsVersion = "2.0"; + + /// + /// When true, causes the build to emit a summary of project build information + /// + private bool _detailedSummary = false; + + /// + /// Flag indicating whether node reuse should be enabled. + /// By default, it is enabled. + /// + private bool _enableNodeReuse = true; + + /// + /// The original process environment. + /// + private Dictionary _buildProcessEnvironment; + + /// + /// The environment properties for the build. + /// + private PropertyDictionary _environmentProperties = new PropertyDictionary(); + + /// + /// The forwarding logger records. + /// + private IEnumerable _forwardingLoggers = null; + + /// + /// The build-global properties. + /// + private PropertyDictionary _globalProperties = new PropertyDictionary(); + + /// + /// The host services object. + /// + private HostServices _hostServices = null; + + /// + /// The loggers. + /// + private IEnumerable _loggers = null; + + /// + /// The maximum number of nodes to use. + /// + private int _maxNodeCount = 1; + + /// + /// The maximum amount of memory to use. + /// + private int _memoryUseLimit = 0; // Unlimited + + /// + /// The location of the node exe. This is the full path including the exe file itself. + /// + private string _nodeExeLocation = null; + + /// + /// The node id. + /// + private int _nodeId = 0; + + /// + /// Flag indicating if we should only log critical events. + /// + private bool _onlyLogCriticalEvents = false; + + /// + /// The location of the toolset definitions. + /// + private ToolsetDefinitionLocations _toolsetDefinitionLocations = ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry; + + /// + /// The UI culture. + /// + private CultureInfo _uiCulture = Thread.CurrentThread.CurrentUICulture; + + /// + /// The toolset provider + /// + private ToolsetProvider _toolsetProvider; + + /// + /// Should the operating environment such as the current directory and environment be saved and restored between project builds and task invocations + /// This should be defaulted to true as we should normally do this. This should be set to false for GlobalDTAR which could run at the same time in a different build manager. + /// + private bool _saveOperatingEnvironment = true; + + /// + /// Should the logging service be done Synchronously when the number of cps's is 1 + /// + private bool _useSynchronousLogging = false; + + /// + /// Should the inprocess node be shutdown when the build finishes. By default this is false + /// since visual studio needs to keep the inprocess node around after the build has finished. + /// + private bool _shutdownInProcNodeOnBuildFinish = false; + + /// + /// When true, the in-proc node will not be available. + /// + private bool _disableInProcNode = false; + + /// + /// When true, the build should log task inputs to the loggers. + /// + private bool _logTaskInputs = false; + + /// + /// When true, the build should log the input parameters. Note - logging these is very expensive! + /// + private bool _logInitialPropertiesAndItems = false; + + /// + /// Constructor for those who intend to set all properties themselves. + /// + public BuildParameters() + { + Initialize(Utilities.GetEnvironmentProperties(), new ProjectRootElementCache(false), null); + } + + /// + /// Creates BuildParameters from a ProjectCollection. + /// + /// The ProjectCollection from which the BuildParameters should populate itself. + public BuildParameters(ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + Initialize(new PropertyDictionary(projectCollection.EnvironmentProperties), projectCollection.ProjectRootElementCache, new ToolsetProvider(projectCollection.Toolsets)); + + _maxNodeCount = projectCollection.MaxNodeCount; + _onlyLogCriticalEvents = projectCollection.OnlyLogCriticalEvents; + _toolsetDefinitionLocations = projectCollection.ToolsetLocations; + _defaultToolsVersion = projectCollection.DefaultToolsVersion; + + _globalProperties = new PropertyDictionary(projectCollection.GlobalPropertiesCollection); + } + + /// + /// Private constructor for translation + /// + private BuildParameters(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Copy constructor + /// + private BuildParameters(BuildParameters other) + { + ErrorUtilities.VerifyThrowInternalNull(other, "other"); + + _buildId = other._buildId; + _culture = other._culture; + _defaultToolsVersion = other._defaultToolsVersion; + _enableNodeReuse = other._enableNodeReuse; + _buildProcessEnvironment = other._buildProcessEnvironment != null ? new Dictionary(other._buildProcessEnvironment) : null; + _environmentProperties = other._environmentProperties != null ? new PropertyDictionary(other._environmentProperties) : null; + _forwardingLoggers = other._forwardingLoggers != null ? new List(other._forwardingLoggers) : null; + _globalProperties = other._globalProperties != null ? new PropertyDictionary(other._globalProperties) : null; + _hostServices = other._hostServices; + _loggers = other._loggers != null ? new List(other._loggers) : null; + _maxNodeCount = other._maxNodeCount; + _memoryUseLimit = other._memoryUseLimit; + _nodeExeLocation = other._nodeExeLocation; + _nodeId = other._nodeId; + _onlyLogCriticalEvents = other._onlyLogCriticalEvents; + _buildThreadPriority = other._buildThreadPriority; + _toolsetProvider = other._toolsetProvider; + _toolsetDefinitionLocations = other._toolsetDefinitionLocations; + _toolsetProvider = other._toolsetProvider; + _uiCulture = other._uiCulture; + _detailedSummary = other._detailedSummary; + _shutdownInProcNodeOnBuildFinish = other._shutdownInProcNodeOnBuildFinish; + this.ProjectRootElementCache = other.ProjectRootElementCache; + this.ResetCaches = other.ResetCaches; + this.LegacyThreadingSemantics = other.LegacyThreadingSemantics; + _saveOperatingEnvironment = other._saveOperatingEnvironment; + _useSynchronousLogging = other._useSynchronousLogging; + _disableInProcNode = other._disableInProcNode; + _logTaskInputs = other._logTaskInputs; + _logInitialPropertiesAndItems = other._logInitialPropertiesAndItems; + } + + /// + /// Sets the desired thread priority for building. + /// + public ThreadPriority BuildThreadPriority + { + get { return _buildThreadPriority; } + set { _buildThreadPriority = value; } + } + + /// + /// By default if the number of processes is set to 1 we will use Asynchronous logging. However if we want to use synchronous logging when the number of cpu's is set to 1 + /// this property needs to be set to true. + /// + public bool UseSynchronousLogging + { + get { return _useSynchronousLogging; } + set { _useSynchronousLogging = value; } + } + + /// + /// Gets the environment variables which were set when this build was created. + /// + public IDictionary BuildProcessEnvironment + { + get + { + if (_buildProcessEnvironment == null) + { + return new ReadOnlyDictionary(new Dictionary(0)); + } + + return new ReadOnlyDictionary(_buildProcessEnvironment); + } + } + + /// + /// The name of the culture to use during the build. + /// + public CultureInfo Culture + { + get { return _culture; } + set { _culture = value; } + } + + /// + /// The default tools version for the build. + /// + public string DefaultToolsVersion + { + get { return _defaultToolsVersion; } + set { _defaultToolsVersion = value; } + } + + /// + /// When true, indicates that the build should emit a detailed summary at the end of the log. + /// + public bool DetailedSummary + { + get { return _detailedSummary; } + set { _detailedSummary = value; } + } + + /// + /// When true, indicates the in-proc node should not be used. + /// + public bool DisableInProcNode + { + get { return _disableInProcNode; } + set { _disableInProcNode = value; } + } + + /// + /// When true, indicates that the task parameters should be logged. + /// + public bool LogTaskInputs + { + get { return _logTaskInputs; } + set { _logTaskInputs = value; } + } + + /// + /// When true, indicates that the initial properties and items should be logged. + /// + public bool LogInitialPropertiesAndItems + { + get { return _logInitialPropertiesAndItems; } + set { _logInitialPropertiesAndItems = value; } + } + + /// + /// Indicates that the build should reset the configuration and results caches. + /// + public bool ResetCaches + { + get; + set; + } + + /// + /// Flag indicating whether out-of-proc nodes should remain after the build and wait for further builds. + /// + public bool EnableNodeReuse + { + get { return _enableNodeReuse; } + set { _enableNodeReuse = value; } + } + + /// + /// Gets an immutable collection of environment properties. + /// + /// + /// This differs from the BuildProcessEnvironment in that there are certain MSBuild-specific properties which are added, and those environment variables which + /// would not be valid as MSBuild properties are removed. + /// + public IDictionary EnvironmentProperties + { + get + { + return new ReadOnlyConvertingDictionary(_environmentProperties, delegate (ProjectPropertyInstance instance) { return ((IProperty)instance).EvaluatedValueEscaped; }); + } + } + + /// + /// The collection of forwarding logger descriptions. + /// + public IEnumerable ForwardingLoggers + { + get + { + return _forwardingLoggers; + } + + set + { + if (value != null) + { + foreach (ForwardingLoggerRecord logger in value) + { + ErrorUtilities.VerifyThrowArgumentNull(logger, "ForwardingLoggers", "NullLoggerNotAllowed"); + } + } + + _forwardingLoggers = value; + } + } + + /// + /// Sets or retrieves an immutable collection of global properties. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Accessor returns a readonly collection, and the BuildParameters class is immutable.")] + public IDictionary GlobalProperties + { + get + { + return new ReadOnlyConvertingDictionary(_globalProperties, delegate (ProjectPropertyInstance instance) { return ((IProperty)instance).EvaluatedValueEscaped; }); + } + + set + { + _globalProperties = new PropertyDictionary(value.Count); + foreach (KeyValuePair property in value) + { + _globalProperties[property.Key] = ProjectPropertyInstance.Create(property.Key, property.Value); + } + } + } + + /// + /// Interface allowing the host to provide additional control over the build process. + /// + public HostServices HostServices + { + get { return _hostServices; } + set { _hostServices = value; } + } + + /// + /// Enables or disables legacy threading semantics + /// + /// + /// Legacy threading semantics indicate that if a submission is to be built + /// only on the in-proc node and the submission is executed synchronously, then all of its + /// requests will be built on the thread which invoked the build rather than a + /// thread owned by the BuildManager. + /// + public bool LegacyThreadingSemantics + { + get; + set; + } + + /// + /// The collection of loggers to use during the build. + /// + public IEnumerable Loggers + { + get + { + return _loggers; + } + + set + { + if (value != null) + { + foreach (ILogger logger in value) + { + ErrorUtilities.VerifyThrowArgumentNull(logger, "Loggers", "NullLoggerNotAllowed"); + } + } + + _loggers = value; + } + } + + /// + /// The maximum number of nodes this build may use. + /// + public int MaxNodeCount + { + get + { + return _maxNodeCount; + } + + set + { + ErrorUtilities.VerifyThrowArgument(value > 0, "InvalidMaxNodeCount"); + _maxNodeCount = value; + } + } + + /// + /// The amount of memory the build should limit itself to using, in megabytes. + /// + public int MemoryUseLimit + { + get { return _memoryUseLimit; } + set { _memoryUseLimit = value; } + } + + /// + /// The location of the build node executable. + /// + public string NodeExeLocation + { + get { return _nodeExeLocation; } + set { _nodeExeLocation = value; } + } + + /// + /// Flag indicating if non-critical logging events should be discarded. + /// + public bool OnlyLogCriticalEvents + { + get { return _onlyLogCriticalEvents; } + set { _onlyLogCriticalEvents = value; } + } + + /// + /// Locations to search for toolsets. + /// + public ToolsetDefinitionLocations ToolsetDefinitionLocations + { + get { return _toolsetDefinitionLocations; } + set { _toolsetDefinitionLocations = value; } + } + + /// + /// Returns all of the toolsets. + /// + /// + /// toolsetProvider.Toolsets is already a readonly collection. + /// + public ICollection Toolsets + { + get { return _toolsetProvider.Toolsets; } + } + + /// + /// The name of the UI culture to use during the build. + /// + public CultureInfo UICulture + { + get { return _uiCulture; } + set { _uiCulture = value; } + } + + /// + /// Flag indicating if the operating environment such as the current directory and environment be saved and restored between project builds and task invocations. + /// This should be set to false for any other build managers running in the system so that we do not have two build managers trampling on each others environment. + /// + public bool SaveOperatingEnvironment + { + get { return _saveOperatingEnvironment; } + set { _saveOperatingEnvironment = value; } + } + + /// + /// Shutdown the inprocess node when the build finishes. By default this is false + /// since visual studio needs to keep the inprocess node around after the build finishes. + /// + public bool ShutdownInProcNodeOnBuildFinish + { + get { return _shutdownInProcNodeOnBuildFinish; } + set { _shutdownInProcNodeOnBuildFinish = value; } + } + + /// + /// Gets the internal msbuild thread stack size. + /// + static internal int ThreadStackSize + { + get { return CommunicationsUtilities.GetIntegerVariableOrDefault("MSBUILDTHREADSTACKSIZE", DefaultThreadStackSize); } + } + + /// + /// Gets or sets the endpoint shutdown timeout. + /// + static internal int EndpointShutdownTimeout + { + get { return CommunicationsUtilities.GetIntegerVariableOrDefault("MSBUILDENDPOINTSHUTDOWNTIMEOUT", DefaultEndpointShutdownTimeout); } + } + + /// + /// Gets or sets the engine shutdown timeout. + /// + static internal int EngineShutdownTimeout + { + get { return CommunicationsUtilities.GetIntegerVariableOrDefault("MSBUILDENGINESHUTDOWNTIMEOUT", DefaultEngineShutdownTimeout); } + } + + /// + /// Gets the maximum number of idle request builders to retain. + /// + static internal int IdleRequestBuilderLimit + { + get { return GetStaticIntVariableOrDefault("MSBUILDIDLEREQUESTBUILDERLIMIT", ref s_idleRequestBuilderLimit, DefaultIdleRequestBuilderLimit); } + } + + /// + /// Gets the logging thread shutdown timeout. + /// + static internal int LoggingThreadShutdownTimeout + { + get { return CommunicationsUtilities.GetIntegerVariableOrDefault("MSBUILDLOGGINGTHREADSHUTDOWNTIMEOUT", DefaultLoggingThreadShutdownTimeout); } + } + + /// + /// Gets or sets the request builder shutdown timeout. + /// + static internal int RequestBuilderShutdownTimeout + { + get { return CommunicationsUtilities.GetIntegerVariableOrDefault("MSBUILDREQUESTBUILDERSHUTDOWNTIMEOUT", DefaultRequestBuilderShutdownTimeout); } + } + + /// + /// Gets or sets the startup directory. + /// + internal static string StartupDirectory + { + get { return BuildParameters.s_startupDirectory; } + } + + /// + /// Indicates whether the build plan is enabled or not. + /// + internal static bool EnableBuildPlan + { + get + { + return GetStaticBoolVariableOrDefault("MSBUILDENABLEBUILDPLAN", ref s_enableBuildPlan, false); + } + } + + /// + /// Indicates whether we should warn when a property is uninitialized when it is used. + /// + internal static bool WarnOnUninitializedProperty + { + get + { + return GetStaticBoolVariableOrDefault("MSBUILDWARNONUNINITIALIZEDPROPERTY", ref s_warnOnUninitializedProperty, false); + } + + set + { + s_warnOnUninitializedProperty = value; + } + } + + /// + /// Indicates whether we should dump string interning stats + /// + internal static bool DumpOpportunisticInternStats + { + get + { + return GetStaticBoolVariableOrDefault("MSBUILDDUMPOPPORTUNISTICINTERNSTATS", ref s_dumpOpportunisticInternStats, false); + } + } + + /// + /// Indicates whether we should dump debugging information about the expander + /// + internal static bool DebugExpansion + { + get + { + return GetStaticBoolVariableOrDefault("MSBUILDDEBUGEXPANSION", ref s_debugExpansion, false); + } + } + + /// + /// Indicates whether we should keep duplicate target outputs + /// + internal static bool KeepDuplicateOutputs + { + get + { + return GetStaticBoolVariableOrDefault("MSBUILDKEEPDUPLICATEOUTPUTS", ref s_keepDuplicateOutputs, false); + } + } + + /// + /// Gets or sets the build id. + /// + internal int BuildId + { + get { return _buildId; } + set { _buildId = value; } + } + + /// + /// Gets or sets the environment properties. + /// + /// + /// This is not the same as BuildProcessEnvironment. See EnvironmentProperties. These properties are those which + /// are used during evaluation of a project, and exclude those properties which would not be valid MSBuild properties + /// because they contain invalid characters (such as 'Program Files (x86)'). + /// + internal PropertyDictionary EnvironmentPropertiesInternal + { + get + { + return _environmentProperties; + } + + set + { + ErrorUtilities.VerifyThrowInternalNull(value, "EnvironmentPropertiesInternal"); + _environmentProperties = value; + } + } + + /// + /// Gets the global properties. + /// + internal PropertyDictionary GlobalPropertiesInternal + { + get { return _globalProperties; } + } + + /// + /// Gets or sets the node id. + /// + internal int NodeId + { + get { return _nodeId; } + set { _nodeId = value; } + } + + /// + /// Gets the toolset provider. + /// + internal IToolsetProvider ToolsetProvider + { + get + { + EnsureToolsets(); + return _toolsetProvider; + } + } + + /// + /// The one and only project root element cache to be used for the build. + /// + internal ProjectRootElementCache ProjectRootElementCache + { + get; + set; + } + + /// + /// Information for configuring child AppDomains. + /// + internal AppDomainSetup AppDomainSetup + { + get; + set; + } + + /// + /// (for diagnostic use) Whether or not this is out of proc + /// + internal bool IsOutOfProc + { + get; + set; + } + + /// + /// Retrieves a toolset. + /// + public Toolset GetToolset(string toolsVersion) + { + EnsureToolsets(); + return _toolsetProvider.GetToolset(toolsVersion); + } + + /// + /// Creates a clone of this BuildParameters object. This creates a clone of the logger collections, but does not deep clone + /// the loggers within. + /// + public BuildParameters Clone() + { + return new BuildParameters(this); + } + + /// + /// Implementation of the serialization mechanism. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _buildId); + /* No build thread priority during translation. We specifically use the default (which is ThreadPriority.Normal) */ + translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); + translator.TranslateCulture(ref _culture); + translator.Translate(ref _defaultToolsVersion); + translator.Translate(ref _disableInProcNode); + translator.Translate(ref _enableNodeReuse); + translator.TranslateProjectPropertyInstanceDictionary(ref _environmentProperties); + /* No forwarding logger information sent here - that goes with the node configuration */ + translator.TranslateProjectPropertyInstanceDictionary(ref _globalProperties); + /* No host services during translation */ + /* No loggers during translation */ + translator.Translate(ref _maxNodeCount); + translator.Translate(ref _memoryUseLimit); + translator.Translate(ref _nodeExeLocation); + /* No node id during translation */ + translator.Translate(ref _onlyLogCriticalEvents); + translator.Translate(ref s_startupDirectory); + translator.TranslateCulture(ref _uiCulture); + translator.Translate(ref _toolsetProvider, Microsoft.Build.Evaluation.ToolsetProvider.FactoryForDeserialization); + translator.Translate(ref _useSynchronousLogging); + translator.Translate(ref _shutdownInProcNodeOnBuildFinish); + translator.Translate(ref _logTaskInputs); + translator.Translate(ref _logInitialPropertiesAndItems); + + // ProjectRootElementCache is not transmitted. + // ResetCaches is not transmitted. + // LegacyThreadingSemantics is not transmitted. + } + + #region INodePacketTranslatable Members + + /// + /// The class factory for deserialization. + /// + internal static BuildParameters FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildParameters(translator); + } + + #endregion + + /// + /// Gets the value of a boolean environment setting which is not expected to change. + /// + private static bool GetStaticBoolVariableOrDefault(string environmentVariable, ref bool? backing, bool @default) + { + if (!backing.HasValue) + { + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable(environmentVariable))) + { + backing = @default; + } + else + { + backing = true; + } + } + + return backing.Value; + } + + /// + /// Gets the value of an integer environment variable, or returns the default if none is set or it cannot be converted. + /// + private static int GetStaticIntVariableOrDefault(string environmentVariable, ref int? backingValue, int defaultValue) + { + if (!backingValue.HasValue) + { + string environmentValue = Environment.GetEnvironmentVariable(environmentVariable); + if (String.IsNullOrEmpty(environmentValue)) + { + backingValue = defaultValue; + } + else + { + int parsedValue = defaultValue; + if (!Int32.TryParse(environmentValue, out parsedValue)) + { + backingValue = defaultValue; + } + else + { + backingValue = parsedValue; + } + } + } + + return backingValue.Value; + } + + /// + /// Centralization of the common parts of construction. + /// + private void Initialize(PropertyDictionary environmentProperties, ProjectRootElementCache projectRootElementCache, ToolsetProvider toolsetProvider) + { + _buildProcessEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + _environmentProperties = environmentProperties; + this.ProjectRootElementCache = projectRootElementCache; + this.ResetCaches = true; + _toolsetProvider = toolsetProvider; + + if (Environment.GetEnvironmentVariable("MSBUILDDISABLENODEREUSE") == "1") // For example to disable node reuse within Visual Studio + { + _enableNodeReuse = false; + } + + if (Environment.GetEnvironmentVariable("MSBUILDDETAILEDSUMMARY") == "1") // For example to get detailed summary within Visual Studio + { + _detailedSummary = true; + } + + FindMSBuildExe(); + } + + /// + /// Loads the toolsets if we don't have them already. + /// + private void EnsureToolsets() + { + if (_toolsetProvider != null) + { + return; + } + + _toolsetProvider = new ToolsetProvider(DefaultToolsVersion, _environmentProperties, _globalProperties, ToolsetDefinitionLocations); + } + + /// + /// This method determines where MSBuild.Exe is and sets the NodeExePath to that by default. + /// + private void FindMSBuildExe() + { + // Use the location specified by the user in code. + if (_nodeExeLocation != null && CheckMSBuildExeExistsAt(_nodeExeLocation)) + { + return; + } + + // Use the location specified in the environment. + // MSBUILD_EXE_PATH optionally contains the full path to msbuild.exe, and if present + // overrides the rest + string path = Environment.GetEnvironmentVariable("MSBUILD_EXE_PATH"); + if (path != null && CheckMSBuildExeExistsAt(path)) + { + _nodeExeLocation = path; + return; + } + + // Use the default location of the directory from which the engine was loaded. + path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MSBuild.exe"); + if (path != null && CheckMSBuildExeExistsAt(path)) + { + _nodeExeLocation = path; + return; + } + + // Get the location pointed to by the MSBuildToolsPath in the "current" ToolsVersion + // for this version of MSBuild. In certain strange circumstances (e.g. checked-in redist + // the current toolset might not be available. In which case, shrug and move on.) + EnsureToolsets(); + Toolset currentToolset = _toolsetProvider.GetToolset(MSBuildConstants.CurrentToolsVersion); + + if (currentToolset != null) + { + path = Path.Combine(currentToolset.ToolsPath, "MSBuild.exe"); + if (path != null && CheckMSBuildExeExistsAt(path)) + { + _nodeExeLocation = path; + return; + } + } + + // Search in the location of any assemblies we have loaded. + foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!assembly.IsDynamic) + { + try + { + path = Path.GetDirectoryName(assembly.Location); + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + // In some circumstances, assembly.Location throws NotSupportedException (ex. when + // it's an anonymous dynamic assembly -- which we're already protecting against, but + // there could be other examples we don't know of). If there's an error here, we really don't + // care -- it's just one fewer place we can look for the path to MSBuild.exe. So + // just continue. + continue; + } + + if (CheckMSBuildExeExistsAt(Path.Combine(path, "MSBuild.exe"))) + { + _nodeExeLocation = Path.Combine(path, "MSBuild.exe"); + return; + } + } + } + + // Search in the framework directory. Checks the COMPLUS_INSTALL_ROOT among other things. + path = FrameworkLocationHelper.PathToDotNetFrameworkV40; + if (path != null && CheckMSBuildExeExistsAt(Path.Combine(path, "MSBuild.exe"))) + { + _nodeExeLocation = Path.Combine(path, "MSBuild.exe"); + } + + // Well, we just can't find it. Maybe they will only build in-proc and won't need it... + return; + } + + /// + /// Helper to avoid doing an expensive disk check for MSBuild.exe when + /// we already checked in a previous build. + /// This File.Exists otherwise can show up in profiles when there's a lot of + /// design time builds going on. + /// + private bool CheckMSBuildExeExistsAt(string path) + { + if (s_msbuildExeKnownToExistAt != null && String.Equals(path, s_msbuildExeKnownToExistAt, StringComparison.OrdinalIgnoreCase)) + { + // We found it there last time: it must exist there. + return true; + } + + if (File.Exists(path)) + { + s_msbuildExeKnownToExistAt = path; + return true; + } + + return false; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/BuildManager/BuildRequestData.cs b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildRequestData.cs new file mode 100644 index 00000000000..b768e8d7438 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildRequestData.cs @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The public class representing the data for a build request. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Execution +{ + /// + /// Flags providing additional control over the build request + /// + [Flags] + public enum BuildRequestDataFlags + { + /// + /// No flags. + /// + None = 0, + + /// + /// When this flag is present, the existing ProjectInstance in the build will be replaced by this one. + /// + ReplaceExistingProjectInstance = 0x1, + + /// + /// When this flag is present, issued in response to this request will + /// include . + /// + ProvideProjectStateAfterBuild = 0x2, + + /// + /// When this flag is present and the project has previously been built on a node whose affinity is + /// incompatible with the affinity this request requires, we will ignore the project state (but not + /// target results) that were previously generated. + /// + /// + /// This usually is not desired behavior. It is only provided for those cases where the client + /// knows that the new build request does not depend on project state generated by a previous request. Setting + /// this flag can provide a performance boost in the case of incompatible node affinities, as MSBuild would + /// otherwise have to serialize the project state from one node to another, which may be + /// expensive depending on how much data the project previously generated. + /// + /// This flag has no effect on target results, so if a previous request already built a target, the new + /// request will not re-build that target (nor will any of the project state mutations which previously + /// occurred as a consequence of building that target be re-applied.) + /// + IgnoreExistingProjectState = 0x4, + } + + /// + /// BuildRequestData encapsulates all of the data needed to submit a build request. + /// + public class BuildRequestData + { + /// + /// Constructs a BuildRequestData for build requests based on project instances. + /// + /// The instance to build. + /// The targets to build. + public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild) + : this(projectInstance, targetsToBuild, null, BuildRequestDataFlags.None) + { + } + + /// + /// Constructs a BuildRequestData for build requests based on project instances. + /// + /// The instance to build. + /// The targets to build. + /// The host services to use, if any. May be null. + public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild, HostServices hostServices) + : this(projectInstance, targetsToBuild, hostServices, BuildRequestDataFlags.None) + { + } + + /// + /// Constructs a BuildRequestData for build requests based on project instances. + /// + /// The instance to build. + /// The targets to build. + /// The host services to use, if any. May be null. + /// Flags controlling this build request. + public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild, HostServices hostServices, BuildRequestDataFlags flags) + : this(projectInstance, targetsToBuild, hostServices, flags, null) + { + } + + /// + /// Constructs a BuildRequestData for build requests based on project instances. + /// + /// The instance to build. + /// The targets to build. + /// The host services to use, if any. May be null. + /// Flags controlling this build request. + /// The list of properties whose values should be transferred from the project to any out-of-proc node. + public BuildRequestData(ProjectInstance projectInstance, string[] targetsToBuild, HostServices hostServices, BuildRequestDataFlags flags, IEnumerable propertiesToTransfer) + : this(targetsToBuild, hostServices, flags) + { + ErrorUtilities.VerifyThrowArgumentNull(projectInstance, "projectInstance"); + + foreach (string targetName in targetsToBuild) + { + ErrorUtilities.VerifyThrowArgumentNull(targetName, "target"); + } + + ProjectInstance = projectInstance; + + ProjectFullPath = projectInstance.FullPath; + GlobalPropertiesDictionary = projectInstance.GlobalPropertiesDictionary; + ExplicitlySpecifiedToolsVersion = projectInstance.ExplicitToolsVersion; + if (propertiesToTransfer != null) + { + this.PropertiesToTransfer = new List(propertiesToTransfer); + } + } + + /// + /// Constructs a BuildRequestData for build requests based on project files. + /// + /// The full path to the project file. + /// The global properties which should be used during evaluation of the project. Cannot be null. + /// The tools version to use for the build. May be null. + /// The targets to build. + /// The host services to use. May be null. + public BuildRequestData(string projectFullPath, IDictionary globalProperties, string toolsVersion, string[] targetsToBuild, HostServices hostServices) + : this(projectFullPath, globalProperties, toolsVersion, targetsToBuild, hostServices, BuildRequestDataFlags.None) + { + } + + /// + /// Constructs a BuildRequestData for build requests based on project files. + /// + /// The full path to the project file. + /// The global properties which should be used during evaluation of the project. Cannot be null. + /// The tools version to use for the build. May be null. + /// The targets to build. + /// The host services to use. May be null. + public BuildRequestData(string projectFullPath, IDictionary globalProperties, string toolsVersion, string[] targetsToBuild, HostServices hostServices, BuildRequestDataFlags flags) + : this(targetsToBuild, hostServices, flags) + { + ErrorUtilities.VerifyThrowArgumentLength(projectFullPath, "projectFullPath"); + ErrorUtilities.VerifyThrowArgumentNull(globalProperties, "globalProperties"); + + this.ProjectFullPath = FileUtilities.NormalizePath(projectFullPath); + TargetNames = (ICollection)targetsToBuild.Clone(); + GlobalPropertiesDictionary = new PropertyDictionary(globalProperties.Count); + foreach (KeyValuePair propertyPair in globalProperties) + { + GlobalPropertiesDictionary.Set(ProjectPropertyInstance.Create(propertyPair.Key, propertyPair.Value)); + } + + ExplicitlySpecifiedToolsVersion = toolsVersion; + } + + /// + /// Common constructor. + /// + private BuildRequestData(string[] targetsToBuild, HostServices hostServices, BuildRequestDataFlags flags) + { + ErrorUtilities.VerifyThrowArgumentNull(targetsToBuild, "targetsToBuild"); + + HostServices = hostServices; + TargetNames = new List(targetsToBuild); + Flags = flags; + } + + /// + /// The actual project, in the case where the project doesn't come from disk. + /// May be null. + /// + /// The project instance. + public ProjectInstance ProjectInstance + { + get; + private set; + } + + /// The project file. + /// The project file to be built. + public string ProjectFullPath + { + get; + internal set; + } + + /// + /// The name of the targets to build. + /// + /// An array of targets in the project to be built. + public ICollection TargetNames + { + get; + private set; + } + + /// + /// Extra flags for this BuildRequest. + /// + public BuildRequestDataFlags Flags + { + get; + private set; + } + + /// + /// The global properties to use. + /// + /// The set of global properties to be used to build this request. + public ICollection GlobalProperties + { + get + { + return (GlobalPropertiesDictionary == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new ReadOnlyCollection(GlobalPropertiesDictionary); + } + } + + /// + /// The explicitly requested tools version to use. + /// + public string ExplicitlySpecifiedToolsVersion + { + get; + private set; + } + + /// + /// Gets the HostServices object for this request. + /// + public HostServices HostServices + { + get; + private set; + } + + /// + /// Returns a list of properties to transfer out of proc for the build. + /// + public IEnumerable PropertiesToTransfer + { + get; + private set; + } + + /// + /// Whether the tools version used originated from an explicit specification, + /// for example from an MSBuild task or /tv switch. + /// + internal bool ExplicitToolsVersionSpecified + { + get { return (ExplicitlySpecifiedToolsVersion != null); } + } + + /// + /// Returns the global properties as a dictionary. + /// + internal PropertyDictionary GlobalPropertiesDictionary + { + get; + private set; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/BuildManager/BuildSubmission.cs b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildSubmission.cs new file mode 100644 index 00000000000..2b996920bb9 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/BuildManager/BuildSubmission.cs @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of the Build Submission. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Runtime.InteropServices; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using System.Globalization; + +namespace Microsoft.Build.Execution +{ + /// + /// A callback used to receive notification that a build has completed. + /// + /// + /// When this delegate is invoked, the WaitHandle on the BuildSubmission will have been be signalled and the OverallBuildResult will be valid. + /// + public delegate void BuildSubmissionCompleteCallback(BuildSubmission submission); + + /// + /// A BuildSubmission represents an build request which has been submitted to the BuildManager for processing. It may be used to + /// execute synchronous or asynchronous build requests and provides access to the results upon completion. + /// + /// + /// This class is thread-safe. + /// + public class BuildSubmission + { + /// + /// The callback to invoke when the submission is complete. + /// + private BuildSubmissionCompleteCallback _completionCallback; + + /// + /// The completion event. + /// + private ManualResetEvent _completionEvent; + + /// + /// Flag indicating if logging is done. + /// + private bool _loggingCompleted; + + /// + /// True if it has been invoked + /// + private int _completionInvoked; + + /// + /// The results of the build. + /// + private BuildResult _buildResult; + + /// + /// Flag indicating whether synchronous wait should support legacy threading semantics. + /// + private bool _legacyThreadingSemantics; + + /// + /// Constructor + /// + internal BuildSubmission(BuildManager buildManager, int submissionId, BuildRequestData requestData, bool legacyThreadingSemantics) + { + ErrorUtilities.VerifyThrowArgumentNull(buildManager, "buildManager"); + ErrorUtilities.VerifyThrowArgumentNull(requestData, "requestData"); + + BuildManager = buildManager; + SubmissionId = submissionId; + BuildRequestData = requestData; + _completionEvent = new ManualResetEvent(false); + _loggingCompleted = false; + _completionInvoked = 0; + _legacyThreadingSemantics = legacyThreadingSemantics; + } + + /// + /// The BuildManager with which this submission is associated. + /// + public BuildManager BuildManager + { + get; + private set; + } + + /// + /// An ID uniquely identifying this request from among other submissions within the same build. + /// + public int SubmissionId + { + get; + private set; + } + + /// + /// The asynchronous context provided to , if any. + /// + public Object AsyncContext + { + get; + private set; + } + + /// + /// A which will be signalled when the build is complete. Valid after or returns, otherwise null. + /// + public WaitHandle WaitHandle + { + get + { + return _completionEvent; + } + } + + /// + /// Returns true if this submission is complete. + /// + public bool IsCompleted + { + get + { + return WaitHandle.WaitOne(new TimeSpan(0)); + } + } + + /// + /// The result of the build. Valid only after WaitHandle has become signalled. + /// + public BuildResult BuildResult + { + get + { + return _buildResult; + } + + set + { + _buildResult = value; + } + } + + /// + /// The BuildRequestData being used for this submission. + /// + internal BuildRequestData BuildRequestData + { + get; + private set; + } + + /// + /// The build request for execution. + /// + internal BuildRequest BuildRequest + { + get; + set; + } + + /// + /// Starts the request and blocks until results are available. + /// + /// The request has already been started or is already complete. + public BuildResult Execute() + { + LegacyThreadingData legacyThreadingData = ((IBuildComponentHost)BuildManager).LegacyThreadingData; + legacyThreadingData.RegisterSubmissionForLegacyThread(this.SubmissionId); + + ExecuteAsync(null, null, _legacyThreadingSemantics); + if (_legacyThreadingSemantics) + { + RequestBuilder.WaitWithBuilderThreadStart(new WaitHandle[] { WaitHandle }, false, legacyThreadingData, this.SubmissionId); + } + else + { + WaitHandle.WaitOne(); + } + + legacyThreadingData.UnregisterSubmissionForLegacyThread(this.SubmissionId); + + return BuildResult; + } + + /// + /// Starts the request asynchronously and immediately returns control to the caller. + /// + /// The request has already been started or is already complete. + public void ExecuteAsync(BuildSubmissionCompleteCallback callback, object context) + { + ExecuteAsync(callback, context, false); + } + + /// + /// Sets the event signaling that the build is complete. + /// + internal void CompleteResults(BuildResult result) + { + ErrorUtilities.VerifyThrowArgumentNull(result, "result"); + + // We verify that we got results from the same configuration, but not necessarily the same request, because we are + // rather flexible in how users are allowed to submit multiple requests for the same configuration. In this case, the + // request id of the result will match the first request, even though it will contain results for all requests (including + // this one.) + ErrorUtilities.VerifyThrow(result.ConfigurationId == BuildRequest.ConfigurationId, "BuildResult doesn't match BuildRequest configuration"); + + if (BuildResult == null) + { + BuildResult = result; + } + + CheckForCompletion(); + } + + /// + /// Indicates that all logging events for this submission are complete. + /// + internal void CompleteLogging(bool waitForLoggingThread) + { + if (waitForLoggingThread) + { + ((Microsoft.Build.BackEnd.Logging.LoggingService)((IBuildComponentHost)BuildManager).LoggingService).WaitForThreadToProcessEvents(); + } + + _loggingCompleted = true; + CheckForCompletion(); + } + + /// + /// Starts the request asynchronously and immediately returns control to the caller. + /// + /// The request has already been started or is already complete. + private void ExecuteAsync(BuildSubmissionCompleteCallback callback, object context, bool allowMainThreadBuild) + { + ErrorUtilities.VerifyThrowInvalidOperation(!IsCompleted, "SubmissionAlreadyComplete"); + _completionCallback = callback; + AsyncContext = context; + BuildManager.ExecuteSubmission(this, allowMainThreadBuild); + } + + /// + /// Determines if we are completely done with this submission and can complete it so the user may access results. + /// + private void CheckForCompletion() + { + if (BuildResult != null && _loggingCompleted) + { + bool hasCompleted = (Interlocked.Exchange(ref _completionInvoked, 1) == 1); + if (!hasCompleted) + { + _completionEvent.Set(); + + if (null != _completionCallback) + { + WaitCallback callback = new WaitCallback + ( + delegate (object state) + { + _completionCallback(this); + }); + + ThreadPoolExtensions.QueueThreadPoolWorkItemWithCulture(callback, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture); + } + } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/BuildManager/LegacyThreadingData.cs b/src/XMakeBuildEngine/BackEnd/BuildManager/LegacyThreadingData.cs new file mode 100644 index 00000000000..d27739d584a --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/BuildManager/LegacyThreadingData.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Contains a set of data used for legacy threading semantics +//----------------------------------------------------------------------- + +using System; +using System.Threading; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Build.Execution +{ + /// + /// This class represents the data which is used for legacy threading semantics for the build + /// + internal class LegacyThreadingData + { + #region Fields + /// + /// Store the pair of start/end events used by a particular submission to track their ownership + /// of the legacy thread. + /// Item1: Start event, tracks when the submission has permission to start building. + /// Item2: End event, signalled when that submission is no longer using the legacy thread. + /// + private IDictionary> _legacyThreadingEventsById = new Dictionary>(); + + /// + /// The current submission id building on the main thread, if any. + /// + private int _mainThreadSubmissionId = -1; + + /// + /// The instance to be used when the new request builder is started on the main thread. + /// + private RequestBuilder _instanceForMainThread; + + /// + /// Lock object for startNewRequestBuilderMainThreadEventsById, since it's possible for multiple submissions to be + /// submitted at the same time. + /// + private Object _legacyThreadingEventsLock = new Object(); + #endregion + + #region Properties + + /// + /// The instance to be used when the new request builder is started on the main thread. + /// + internal RequestBuilder InstanceForMainThread + { + get + { + return _instanceForMainThread; + } + + set + { + ErrorUtilities.VerifyThrow(_instanceForMainThread == null || (_instanceForMainThread != null && value == null) || (_instanceForMainThread == value), "Should not assign to instanceForMainThread twice without cleaning it"); + _instanceForMainThread = value; + } + } + + /// + /// The current submission id building on the main thread, if any. + /// + internal int MainThreadSubmissionId + { + get + { + return _mainThreadSubmissionId; + } + + set + { + if (value == -1) + { + _instanceForMainThread = null; + } + + _mainThreadSubmissionId = value; + } + } + #endregion + + /// + /// Given a submission ID, assign it "start" and "finish" events to track its use of + /// the legacy thread. + /// + internal void RegisterSubmissionForLegacyThread(int submissionId) + { + lock (_legacyThreadingEventsLock) + { + ErrorUtilities.VerifyThrow(!_legacyThreadingEventsById.ContainsKey(submissionId), "Submission {0} should not already be registered with LegacyThreadingData", submissionId); + + _legacyThreadingEventsById[submissionId] = new Tuple + ( + new AutoResetEvent(false), + new ManualResetEvent(false) + ); + } + } + + /// + /// This submission is completely done with the legacy thread, so unregister it + /// from the dictionary so that we don't leave random events lying around. + /// + internal void UnregisterSubmissionForLegacyThread(int submissionId) + { + lock (_legacyThreadingEventsLock) + { + ErrorUtilities.VerifyThrow(_legacyThreadingEventsById.ContainsKey(submissionId), "Submission {0} should have been previously registered with LegacyThreadingData", submissionId); + + if (_legacyThreadingEventsById.ContainsKey(submissionId)) + { + _legacyThreadingEventsById.Remove(submissionId); + } + } + } + + /// + /// Given a submission ID, return the event being used to track when that submission is ready + /// to be executed on the legacy thread. + /// + internal WaitHandle GetStartRequestBuilderMainThreadEventForSubmission(int submissionId) + { + Tuple legacyThreadingEvents = null; + + lock (_legacyThreadingEventsLock) + { + _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); + } + + ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, "We're trying to wait on the legacy thread for submission {0}, but that submission has not been registered.", submissionId); + + return legacyThreadingEvents.Item1; + } + + /// + /// Given a submission ID, return the event being used to track when that submission is ready + /// to be executed on the legacy thread. + /// + internal Task GetLegacyThreadInactiveTask(int submissionId) + { + Tuple legacyThreadingEvents = null; + + lock (_legacyThreadingEventsLock) + { + _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); + } + + ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, "We're trying to track when the legacy thread for submission {0} goes inactive, but that submission has not been registered.", submissionId); + + return legacyThreadingEvents.Item2.ToTask(); + } + + /// + /// Signal that the legacy thread is starting work. + /// + internal void SignalLegacyThreadStart(RequestBuilder instance) + { + ErrorUtilities.VerifyThrow + ( + instance != null && + instance.RequestEntry != null && + instance.RequestEntry.Request != null, + "Cannot signal legacy thread start for a RequestBuilder without a request" + ); + + int submissionId = instance.RequestEntry.Request.SubmissionId; + this.InstanceForMainThread = instance; + + Tuple legacyThreadingEvents = null; + lock (_legacyThreadingEventsLock) + { + _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); + } + + ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, "We're trying to signal that the legacy thread is ready for submission {0} to execute, but that submission has not been registered", submissionId); + + // signal that this submission is currently controlling the legacy thread + legacyThreadingEvents.Item1.Set(); + + // signal that the legacy thread is not currently idle + legacyThreadingEvents.Item2.Reset(); + } + + /// + /// Signal that the legacy thread has finished its work. + /// + internal void SignalLegacyThreadEnd(int submissionId) + { + this.MainThreadSubmissionId = -1; + + Tuple legacyThreadingEvents = null; + lock (_legacyThreadingEventsLock) + { + _legacyThreadingEventsById.TryGetValue(submissionId, out legacyThreadingEvents); + } + + ErrorUtilities.VerifyThrow(legacyThreadingEvents != null, "We're trying to signal that submission {0} is done with the legacy thread, but that submission has not been registered", submissionId); + + // The legacy thread is now idle + legacyThreadingEvents.Item2.Set(); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/BuildComponentFactoryCollection.cs b/src/XMakeBuildEngine/BackEnd/Components/BuildComponentFactoryCollection.cs new file mode 100644 index 00000000000..632714e8451 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/BuildComponentFactoryCollection.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A collection of component factories. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.BackEnd.Components.Caching; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Helper class for maintaining the component collection + /// + internal class BuildComponentFactoryCollection + { + /// + /// The build component factories. + /// + private Dictionary _componentEntriesByType; + + /// + /// The host used to initialize components. + /// + private IBuildComponentHost _host; + + /// + /// Constructor. + /// + public BuildComponentFactoryCollection(IBuildComponentHost host) + { + _host = host; + _componentEntriesByType = new Dictionary(); + } + + /// + /// The creation pattern to use for this component. + /// + public enum CreationPattern + { + /// + /// The component should be created as a singleton. + /// + Singleton, + + /// + /// A new instance of the component should be created with every request. + /// + CreateAlways + } + + /// + /// Registers the default factories. + /// + public void RegisterDefaultFactories() + { + _componentEntriesByType[BuildComponentType.Scheduler] = new BuildComponentEntry(BuildComponentType.Scheduler, Scheduler.CreateComponent, CreationPattern.Singleton); + _componentEntriesByType[BuildComponentType.ConfigCache] = new BuildComponentEntry(BuildComponentType.ConfigCache, ConfigCache.CreateComponent, CreationPattern.Singleton); + _componentEntriesByType[BuildComponentType.ResultsCache] = new BuildComponentEntry(BuildComponentType.ResultsCache, ResultsCache.CreateComponent, CreationPattern.Singleton); + _componentEntriesByType[BuildComponentType.NodeManager] = new BuildComponentEntry(BuildComponentType.NodeManager, NodeManager.CreateComponent, CreationPattern.Singleton); + _componentEntriesByType[BuildComponentType.TaskHostNodeManager] = new BuildComponentEntry(BuildComponentType.TaskHostNodeManager, TaskHostNodeManager.CreateComponent, CreationPattern.Singleton); + + _componentEntriesByType[BuildComponentType.InProcNodeProvider] = new BuildComponentEntry(BuildComponentType.InProcNodeProvider, NodeProviderInProc.CreateComponent, CreationPattern.Singleton); + _componentEntriesByType[BuildComponentType.OutOfProcNodeProvider] = new BuildComponentEntry(BuildComponentType.OutOfProcNodeProvider, NodeProviderOutOfProc.CreateComponent, CreationPattern.Singleton); + _componentEntriesByType[BuildComponentType.OutOfProcTaskHostNodeProvider] = new BuildComponentEntry(BuildComponentType.OutOfProcTaskHostNodeProvider, NodeProviderOutOfProcTaskHost.CreateComponent, CreationPattern.Singleton); + + // PropertyCache, + // RemoteNodeProvider, + // NodePacketFactory, + _componentEntriesByType[BuildComponentType.RequestEngine] = new BuildComponentEntry(BuildComponentType.RequestEngine, BuildRequestEngine.CreateComponent, CreationPattern.Singleton); + + // FileMonitor, + // NodeEndpoint, + _componentEntriesByType[BuildComponentType.LoggingService] = new BuildComponentEntry(BuildComponentType.LoggingService, null); + _componentEntriesByType[BuildComponentType.RequestBuilder] = new BuildComponentEntry(BuildComponentType.RequestBuilder, RequestBuilder.CreateComponent, CreationPattern.CreateAlways); + _componentEntriesByType[BuildComponentType.TargetBuilder] = new BuildComponentEntry(BuildComponentType.TargetBuilder, TargetBuilder.CreateComponent, CreationPattern.CreateAlways); + _componentEntriesByType[BuildComponentType.TaskBuilder] = new BuildComponentEntry(BuildComponentType.TaskBuilder, TaskBuilder.CreateComponent, CreationPattern.CreateAlways); + _componentEntriesByType[BuildComponentType.RegisteredTaskObjectCache] = new BuildComponentEntry(BuildComponentType.RegisteredTaskObjectCache, RegisteredTaskObjectCache.CreateComponent, CreationPattern.Singleton); + } + + /// + /// Shuts down all factories registered to this component factory collection. + /// + public void ShutdownComponents() + { + foreach (KeyValuePair componentEntry in _componentEntriesByType) + { + if (componentEntry.Value.Pattern == CreationPattern.Singleton) + { + componentEntry.Value.ShutdownSingletonInstance(); + } + } + } + + /// + /// Shuts down a specific singleton component. + /// + public void ShutdownComponent(BuildComponentType componentType) + { + BuildComponentEntry existingEntry = _componentEntriesByType[componentType]; + existingEntry.ShutdownSingletonInstance(); + } + + /// + /// Registers a factory to replace one of the defaults. Creation pattern is inherited from the original. + /// + /// The type which is created by this factory. + /// The factory to be registered. + public void ReplaceFactory(BuildComponentType componentType, BuildComponentFactoryDelegate factory) + { + BuildComponentEntry existingEntry = _componentEntriesByType[componentType]; + _componentEntriesByType[componentType] = new BuildComponentEntry(componentType, factory, existingEntry.Pattern); + } + + /// + /// Registers a factory to replace one of the defaults. Creation pattern is inherited from the original. + /// + /// The type which is created by this factory. + /// The instance to be registered. + public void ReplaceFactory(BuildComponentType componentType, IBuildComponent instance) + { + ErrorUtilities.VerifyThrow(_componentEntriesByType[componentType].Pattern == CreationPattern.Singleton, "Previously existing factory for type {0} was not a singleton factory.", componentType); + _componentEntriesByType[componentType] = new BuildComponentEntry(componentType, instance); + } + + /// + /// Adds a factory. + /// + /// The type which is created by this factory. + /// Delegate which is responsible for creating the Component. + /// Creation pattern. + public void AddFactory(BuildComponentType componentType, BuildComponentFactoryDelegate factory, CreationPattern creationPattern) + { + _componentEntriesByType[componentType] = new BuildComponentEntry(componentType, factory, creationPattern); + } + + /// + /// Gets an instance of the specified component type from the host. + /// + /// The component type to be retrieved + /// The component + public IBuildComponent GetComponent(BuildComponentType type) + { + BuildComponentEntry componentEntry = null; + + if (!_componentEntriesByType.TryGetValue(type, out componentEntry)) + { + ErrorUtilities.ThrowInternalError("No factory registered for component type {0}", type); + } + + return componentEntry.GetInstance(_host); + } + + /// + /// A helper class wrapping build components. + /// + private class BuildComponentEntry + { + /// + /// The factory used to construct instances of the component. + /// + private readonly BuildComponentFactoryDelegate _factory; + + /// + /// The singleton instance for components which adhere to the singleton pattern. + /// + private IBuildComponent _singleton; + + /// + /// Constructor. + /// + public BuildComponentEntry(BuildComponentType type, BuildComponentFactoryDelegate factory, CreationPattern pattern) + { + this.ComponentType = type; + _factory = factory; + this.Pattern = pattern; + } + + /// + /// Constructor for existing singleton. + /// + public BuildComponentEntry(BuildComponentType type, IBuildComponent singleton) + { + _singleton = singleton; + this.Pattern = CreationPattern.Singleton; + } + + /// + /// Retrieves the component type. + /// + public BuildComponentType ComponentType + { + get; + private set; + } + + /// + /// Retrieves the creation pattern. + /// + public CreationPattern Pattern + { + get; + private set; + } + + /// + /// Gets an instance of the component. + /// + public IBuildComponent GetInstance(IBuildComponentHost host) + { + if (Pattern == CreationPattern.Singleton) + { + if (_singleton == null) + { + _singleton = _factory(ComponentType); + _singleton.InitializeComponent(host); + } + + return _singleton; + } + else + { + IBuildComponent component = _factory(ComponentType); + component.InitializeComponent(host); + return component; + } + } + + /// + /// Shuts down the single instance for this component type. + /// + public void ShutdownSingletonInstance() + { + ErrorUtilities.VerifyThrow(Pattern == CreationPattern.Singleton, "Cannot shutdown non-singleton."); + if (_singleton != null) + { + _singleton.ShutdownComponent(); + _singleton = null; + } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestConfigurationResponse.cs b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestConfigurationResponse.cs new file mode 100644 index 00000000000..cd54b5a8065 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestConfigurationResponse.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Packet used to communicate configuration id back to the node +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This is the packet which is sent in response to a build configuration ID request. When the node generates a new configuration which it has + /// never seen before, it gives that configuration a temporary, "unresolved" configuration id. The node then asks the Build Request Manager + /// for the "resolved" configuration id, which is global to all nodes in the system. This packet maps the unresolved to the resolved + /// configuration id. Once this packet is received, the node engine can then continue processing requests associated with the configuration. + /// + internal class BuildRequestConfigurationResponse : INodePacket + { + /// + /// The configuration ID assigned by the node + /// + private int _nodeConfigId; + + /// + /// The configuration ID assigned by the build manager. + /// + private int _globalConfigId; + + /// + /// The results node assigned to this configuration + /// + private int _resultsNodeId; + + /// + /// Constructor for non-deserialization initialization. + /// + /// The node-assigned configuration id + /// The build manager-assigned configuration id + public BuildRequestConfigurationResponse(int nodeConfigId, int globalConfigId, int resultsNodeId) + { + _nodeConfigId = nodeConfigId; + _globalConfigId = globalConfigId; + _resultsNodeId = resultsNodeId; + } + + /// + /// Constructor for deserialization + /// + private BuildRequestConfigurationResponse(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Returns the node-assigned configuration id + /// + public int NodeConfigurationId + { + get { return _nodeConfigId; } + } + + /// + /// Returns the build manager assigned configuration id + /// + public int GlobalConfigurationId + { + get { return _globalConfigId; } + } + + /// + /// Returns the results node for the global configuration. + /// + public int ResultsNodeId + { + get { return _resultsNodeId; } + } + + #region INodePacket Members + + /// + /// INodePacket property. Returns the packet type. + /// + public NodePacketType Type + { + get + { + return NodePacketType.BuildRequestConfigurationResponse; + } + } + + /// + /// Reads/writes this packet + /// + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _nodeConfigId); + translator.Translate(ref _globalConfigId); + translator.Translate(ref _resultsNodeId); + } + + /// + /// Factory for serialization. + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildRequestConfigurationResponse(translator); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs new file mode 100644 index 00000000000..6b47c562b94 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -0,0 +1,1413 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The build request engine. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The BuildRequestEngine is responsible for managing the building of projects on a given node. It + /// receives build requests, reports results and deals with BuildRequestConfiguration transactions. + /// As it runs on its own thread, all BuildRequestEngine operations are asynchronous. + /// + /// + /// Internally, the BuildRequestEngine manages the requests in the form of BuildRequestEntry objects. + /// Each of these maintains the complete state of a build request, accumulating results until completion. + /// The EngineLoop method is the separate thread proc which handles state changes for BuildRequestEntries + /// and shutting down. However, each RequestBuilder can call back into the BuildRequestEngine (via events) + /// to cause new requests to be submitted. See . + /// + internal class BuildRequestEngine : IBuildRequestEngine, IBuildComponent + { + /// + /// The starting unresolved configuration id assigned by the engine. + /// + private const int StartingUnresolvedConfigId = -1; + + /// + /// The starting build request id + /// + private const int StartingBuildRequestId = 1; + + /// + /// The current engine status + /// + private BuildRequestEngineStatus _status; + + /// + /// Ths component host + /// + private IBuildComponentHost _componentHost; + + /// + /// The work queue. + /// + private ActionBlock _workQueue; + + /// + /// The list of current requests the engine is working on. + /// + private List _requests; + + /// + /// Mapping of global request ids to the request entries. + /// + private Dictionary _requestsByGlobalRequestId; + + /// + /// The list of requests currently waiting to be submitted from RequestBuilders. + /// + private Queue _unsubmittedRequests; + + /// + /// The next available local unresolved configuration Id + /// + private int _nextUnresolvedConfigurationId; + + /// + /// The next available build request Id + /// + private int _nextBuildRequestId; + + /// + /// The global configuration cache + /// + private IConfigCache _configCache; + + /// + /// The list of unresolved configurations + /// + private IConfigCache _unresolvedConfigurations; + + /// + /// The logging context for the node + /// + private NodeLoggingContext _nodeLoggingContext; + + /// + /// Flag indicating if we should trace. + /// + private bool _debugDumpState; + + /// + /// The path where we will store debug files + /// + private string _debugDumpPath; + + /// + /// Forces caching of all configurations and results. + /// + private bool _debugForceCaching; + + /// + /// Constructor + /// + internal BuildRequestEngine() + { + _debugDumpState = Environment.GetEnvironmentVariable("MSBUILDDEBUGSCHEDULER") == "1"; + _debugDumpPath = Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH"); + _debugForceCaching = Environment.GetEnvironmentVariable("MSBUILDDEBUGFORCECACHING") == "1"; + + if (String.IsNullOrEmpty(_debugDumpPath)) + { + _debugDumpPath = Path.GetTempPath(); + } + + _status = BuildRequestEngineStatus.Uninitialized; + _nextUnresolvedConfigurationId = -1; + _nextBuildRequestId = 0; + _requests = new List(); + _unsubmittedRequests = new Queue(); + _requestsByGlobalRequestId = new Dictionary(); + } + + #region IBuildRequestEngine Members + + /// + /// Raised when a request has completed. + /// + public event RequestCompleteDelegate OnRequestComplete; + + /// + /// Raised when a request is resumed by the engine itself. + /// + public event RequestResumedDelegate OnRequestResumed; + + /// + /// Raised when a new request is generated. + /// + public event RequestBlockedDelegate OnRequestBlocked; + + /// + /// Raised when the engine's status has changed. + /// + public event EngineStatusChangedDelegate OnStatusChanged; + + /// + /// Raised when a configuration needs its ID resolved. + /// + public event NewConfigurationRequestDelegate OnNewConfigurationRequest; + + /// + /// Raised when an unexpected exception occurs. + /// + public event EngineExceptionDelegate OnEngineException; + + /// + /// Returns the current engine status. + /// + public BuildRequestEngineStatus Status + { + get + { + return _status; + } + } + + /// + /// Prepares the build request engine to run a build. + /// + /// The logging context to use. + /// + /// Called by the Node. Non-overlapping with other calls from the Node. + public void InitializeForBuild(NodeLoggingContext loggingContext) + { + ErrorUtilities.VerifyThrow(_componentHost != null, "BuildRequestEngine not initialized by component host."); + ErrorUtilities.VerifyThrow(_status == BuildRequestEngineStatus.Uninitialized, "Engine must be in the Uninitiailzed state, but is {0}", _status); + + _nodeLoggingContext = loggingContext; + + // Create a work queue that will take an action and invoke it. The generic parameter is the type which ActionBlock.Post() will + // take (an Action in this case) and the parameter to this constructor is a function which takes that parameter of type Action + // (which we have named action) and does something with it (in this case calls invoke on it.) + _workQueue = new ActionBlock(action => action.Invoke()); + ChangeStatus(BuildRequestEngineStatus.Idle); + } + + /// + /// Cleans up after a build but leaves the engine thread running. Aborts + /// any outstanding requests. Blocks until the engine has cleaned up + /// everything. After this method is called, InitializeForBuild may be + /// called to start a new build, or the component may be shut down. + /// + /// + /// Called by the Node. Non-overlapping with other calls from the Node. + /// + public void CleanupForBuild() + { + QueueAction( + () => + { + ErrorUtilities.VerifyThrow(_status == BuildRequestEngineStatus.Active || _status == BuildRequestEngineStatus.Idle || _status == BuildRequestEngineStatus.Waiting, "Engine must be Active, Idle or Waiting to clean up, but is {0}.", _status); + TraceEngine("CFB: Cleaning up build. Requests Count {0} Status {1}", _requests.Count, _status); + ErrorUtilities.VerifyThrow(_nodeLoggingContext != null, "Node logging context not set."); + + // Determine how many requests there are to shut down, then terminate all of their builders. + // We will capture the exceptions which happen here (but continue shutting down gracefully.) + List requestsToShutdown = new List(_requests); + List deactivateExceptions = new List(_requests.Count); + + // VC observed their tasks (e.g. "CL") received the "cancel" event in several seconds after CTRL+C was captured; + // and the reason was we signaled the "cancel" event to the build request and wait for its completion one by one. + // So we made this minor optimization to signal the "cancel" events to all the build requests and then wait for all of them to be completed. + // From the experiments on a big VC solution, this optimization showed slightly better result consistently. + // For the extremely bad case, say, it takes 10 seconds to "cancel" the build; this optimization could save 2 to 4 seconds. + var requestsToWait = new List(_requests.Count); + foreach (BuildRequestEntry entry in requestsToShutdown) + { + try + { + BeginDeactivateBuildRequest(entry); + requestsToWait.Add(entry); + } + catch (Exception e) + { + TraceEngine("CFB: Shutting down request {0}({1}) (nr {2}) failed due to exception: {3}", entry.Request.GlobalRequestId, entry.Request.ConfigurationId, entry.Request.NodeRequestId, e.ToString()); + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + TraceEngine("CFB: Aggregating last shutdown exception"); + deactivateExceptions.Add(e); + } + } + + foreach (BuildRequestEntry entry in requestsToWait) + { + try + { + WaitForDeactivateCompletion(entry); + } + catch (Exception e) + { + TraceEngine("CFB: Shutting down request {0}({1}) (nr {2}) failed due to exception: {3}", entry.Request.GlobalRequestId, entry.Request.ConfigurationId, entry.Request.NodeRequestId, e.ToString()); + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + TraceEngine("CFB: Aggregating last shutdown exception"); + deactivateExceptions.Add(e); + } + } + + // Report our results. + foreach (BuildRequestEntry entry in requestsToShutdown) + { + BuildResult result = entry.Result ?? new BuildResult(entry.Request, new BuildAbortedException()); + TraceEngine("CFB: Request is now {0}({1}) (nr {2}) has been deactivated.", entry.Request.GlobalRequestId, entry.Request.ConfigurationId, entry.Request.NodeRequestId); + RaiseRequestComplete(entry.Request, result); + } + + // Any exceptions which occurred while we are shutting down request builders should be thrown now so the outer handler + // can report them. + if (deactivateExceptions.Count > 0) + { + TraceEngine("CFB: Rethrowing shutdown exceptions"); + throw new AggregateException(deactivateExceptions); + } + }, + isLastTask: true); + + // Wait for the task to finish + try + { + _workQueue.Completion.Wait(); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // If we caught an exception during cleanup, we need to log that + ErrorUtilities.ThrowInternalError("Failure during engine shutdown. Exception: {0}", e.ToString()); + } + finally + { + // Now all requests have been deactivated. Any requests which got placed in the queue while we were waiting + // for builders to shut down will be discarded, so when we return from this function there will be no work + // to do. + _workQueue = null; + _requests.Clear(); + _requestsByGlobalRequestId.Clear(); + _unsubmittedRequests.Clear(); + _unresolvedConfigurations.ClearConfigurations(); + ChangeStatus(BuildRequestEngineStatus.Uninitialized); + } + } + + /// + /// Adds a new build request to the request queue. + /// + /// The request to be added. + /// + /// Called by the Node. Non-overlapping with other calls from the Node. + /// + public void SubmitBuildRequest(BuildRequest request) + { + QueueAction( + () => + { + ErrorUtilities.VerifyThrow(_status != BuildRequestEngineStatus.Shutdown && _status != BuildRequestEngineStatus.Uninitialized, "Engine loop not yet started, status is {0}.", _status); + TraceEngine("Request {0}({1}) (nr {2}) received and activated.", request.GlobalRequestId, request.ConfigurationId, request.NodeRequestId); + + ErrorUtilities.VerifyThrow(!_requestsByGlobalRequestId.ContainsKey(request.GlobalRequestId), "Request {0} is already known to the engine.", request.GlobalRequestId); + ErrorUtilities.VerifyThrow(_configCache.HasConfiguration(request.ConfigurationId), "Request {0} refers to configuration {1} which is not known to the engine.", request.GlobalRequestId, request.ConfigurationId); + + if (request.NodeRequestId == BuildRequest.ResultsTransferNodeRequestId) + { + // Grab the results from the requested configuration + IResultsCache cache = (IResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache); + BuildResult result = cache.GetResultsForConfiguration(request.ConfigurationId); + BuildResult resultToReport = new BuildResult(request, result, null); + BuildRequestConfiguration config = ((IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache))[request.ConfigurationId]; + + // Retrieve the config if it has been cached, since this would contain our instance data. It is safe to do this outside of a lock + // since only one thread can run in the BuildRequestEngine at a time, and it is EvaluateRequestStates which would cause us to + // cache configurations if we are running out of memory. + config.RetrieveFromCache(); + ((IBuildResults)resultToReport).SavedCurrentDirectory = config.SavedCurrentDirectory; + ((IBuildResults)resultToReport).SavedEnvironmentVariables = config.SavedEnvironmentVariables; + if (!request.BuildRequestDataFlags.HasFlag(BuildRequestDataFlags.IgnoreExistingProjectState)) + { + resultToReport.ProjectStateAfterBuild = config.Project; + } + + TraceEngine("Request {0}({1}) (nr {2}) retrieved results for configuration {3} from node {4} for transfer.", request.GlobalRequestId, request.ConfigurationId, request.NodeRequestId, request.ConfigurationId, _componentHost.BuildParameters.NodeId); + + // If this is the inproc node, we've already set the configuration's ResultsNodeId to the correct value in + // HandleRequestBlockedOnResultsTransfer, and don't want to set it again, because we actually have less + // information available to us now. + // + // On the other hand, if this is not the inproc node, we want to make sure that our copy of this configuration + // knows that its results are no longer on this node. Since we don't know enough here to know where the + // results are going, we satisfy ourselves with marking that they are simply "not here". + if (_componentHost.BuildParameters.NodeId != Scheduler.InProcNodeId) + { + config.ResultsNodeId = Scheduler.ResultsTransferredId; + } + + RaiseRequestComplete(request, resultToReport); + } + else + { + BuildRequestEntry entry = new BuildRequestEntry(request, _configCache[request.ConfigurationId]); + + entry.OnStateChanged += BuildRequestEntry_StateChanged; + + _requests.Add(entry); + _requestsByGlobalRequestId[request.GlobalRequestId] = entry; + ActivateBuildRequest(entry); + EvaluateRequestStates(); + } + }, + isLastTask: false); + } + + /// + /// Reports a build result to the engine, allowing it to satisfy outstanding requests. This result + /// is reported to each entry, allowing it the opportunity to determine for itself if the + /// result applies. + /// + /// Information needed to unblock the engine. + /// + /// Called by the Node. Non-overlapping with other calls from the Node. + /// + public void UnblockBuildRequest(BuildRequestUnblocker unblocker) + { + QueueAction( + () => + { + ErrorUtilities.VerifyThrow(_status != BuildRequestEngineStatus.Shutdown && _status != BuildRequestEngineStatus.Uninitialized, "Engine loop not yet started, status is {0}.", _status); + ErrorUtilities.VerifyThrow(_requestsByGlobalRequestId.ContainsKey(unblocker.BlockedRequestId), "Request {0} is not known to the engine.", unblocker.BlockedRequestId); + BuildRequestEntry entry = _requestsByGlobalRequestId[unblocker.BlockedRequestId]; + + // Are we resuming execution or reporting results? + if (unblocker.Result == null) + { + // We are resuming execution. + TraceEngine("Request {0}({1}) (nr {2}) is now proceeding from current state {3}.", entry.Request.GlobalRequestId, entry.Request.ConfigurationId, entry.Request.NodeRequestId, entry.State); + + // UNDONE: (Refactor) This is a bit icky because we still have the concept of blocking on an in-progress request + // versus blocking on requests waiting for results. They come to the same thing, and its been rationalized correctly in + // the scheduler, but we should remove the dichotomy in the BuildRequestEntry so that that entry directly tracks in the same + // way as the SchedulableRequest does. Alternately, it could just not track at all, and assume that when the scheduler tells it + // to resume it is able to do so (it has no other way of knowing anyhow.) + if (entry.State == BuildRequestEntryState.Waiting) + { + entry.Unblock(); + } + + ActivateBuildRequest(entry); + } + else + { + // We must be reporting results. + BuildResult result = unblocker.Result; + + if (result.NodeRequestId == BuildRequest.ResultsTransferNodeRequestId) + { + TraceEngine("Request {0}({1}) (nr {2}) has retrieved the results for configuration {3} and cached them on node {4} (UBR).", entry.Request.GlobalRequestId, entry.Request.ConfigurationId, entry.Request.NodeRequestId, entry.Request.ConfigurationId, _componentHost.BuildParameters.NodeId); + + IResultsCache resultsCache = (IResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache); + IConfigCache configCache = (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache); + BuildRequestConfiguration config = configCache[result.ConfigurationId]; + + config.RetrieveFromCache(); + config.SavedEnvironmentVariables = ((IBuildResults)result).SavedEnvironmentVariables; + config.SavedCurrentDirectory = ((IBuildResults)result).SavedCurrentDirectory; + config.ApplyTransferredState(result.ProjectStateAfterBuild); + + // Don't need them anymore on the result since they were just piggybacking to get accross the wire. + ((IBuildResults)result).SavedEnvironmentVariables = null; + ((IBuildResults)result).SavedCurrentDirectory = null; + + // Our results node is now this node, since we've just cached those results + resultsCache.AddResult(result); + config.ResultsNodeId = _componentHost.BuildParameters.NodeId; + + entry.Unblock(); + ActivateBuildRequest(entry); + } + else + { + TraceEngine("Request {0}({1}) (nr {2}) is no longer waiting on nr {3} (UBR). Results are {4}.", entry.Request.GlobalRequestId, entry.Request.ConfigurationId, entry.Request.NodeRequestId, result.NodeRequestId, result.OverallResult); + + // Update the configuration with targets information, if we received any and didn't already have it. + if (result.DefaultTargets != null) + { + BuildRequestConfiguration configuration = _configCache[result.ConfigurationId]; + if (configuration.ProjectDefaultTargets == null) + { + configuration.ProjectDefaultTargets = result.DefaultTargets; + configuration.ProjectInitialTargets = result.InitialTargets; + } + } + + entry.ReportResult(result); + } + } + }, + isLastTask: false); + } + + /// + /// Reports a configuration response to the request, allowing it to satisfy outstanding requests. + /// + /// + /// The configuration response. + /// + /// Called by the Node. Non-overlapping with other calls from the Node. + /// + public void ReportConfigurationResponse(BuildRequestConfigurationResponse response) + { + QueueAction( + () => + { + ErrorUtilities.VerifyThrow(_status != BuildRequestEngineStatus.Shutdown && _status != BuildRequestEngineStatus.Uninitialized, "Engine loop not yet started, status is {0}.", _status); + + TraceEngine("Received configuration response for node config {0}, now global config {1}.", response.NodeConfigurationId, response.GlobalConfigurationId); + ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + + // Remove the unresolved configuration entry from the unresolved cache. + BuildRequestConfiguration config = _unresolvedConfigurations[response.NodeConfigurationId]; + _unresolvedConfigurations.RemoveConfiguration(response.NodeConfigurationId); + + // Add the configuration to the resolved cache unless it already exists there. This will be + // the case in single-proc mode as we share the global cache with the Build Manager. + IConfigCache globalConfigurations = (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache); + if (!globalConfigurations.HasConfiguration(response.GlobalConfigurationId)) + { + config.ConfigurationId = response.GlobalConfigurationId; + config.ResultsNodeId = response.ResultsNodeId; + globalConfigurations.AddConfiguration(config); + } + + // Evaluate the current list of requests and tell any waiting requests about our new configuration update. + // If any requests can now issue build requests, do so. + IResultsCache resultsCache = (IResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache); + + List blockersToIssue = new List(); + foreach (BuildRequestEntry currentEntry in _requests) + { + List requestsToIssue = new List(); + if (currentEntry.State == BuildRequestEntryState.Waiting) + { + // Resolve the configuration id and get the list of requests to be issued, if any. + bool issueRequests = currentEntry.ResolveConfigurationRequest(response.NodeConfigurationId, response.GlobalConfigurationId); + + // If we had any requests which are now ready to be issued, do so. + if (issueRequests) + { + IEnumerable resolvedRequests = currentEntry.GetRequestsToIssueIfReady(); + foreach (BuildRequest request in resolvedRequests) + { + // If we have results already in the cache for this request, give them to the + // entry now. + ResultsCacheResponse cacheResponse = resultsCache.SatisfyRequest(request, config.ProjectInitialTargets, config.ProjectDefaultTargets, config.GetAfterTargetsForDefaultTargets(request), skippedResultsAreOK: false); + if (cacheResponse.Type == ResultsCacheResponseType.Satisfied) + { + // We have a result, give it back to this request. + currentEntry.ReportResult(cacheResponse.Results); + } + else + { + requestsToIssue.Add(request); + } + } + } + } + + if (requestsToIssue.Count != 0) + { + BuildRequestBlocker blocker = new BuildRequestBlocker(currentEntry.Request.GlobalRequestId, currentEntry.GetActiveTargets(), requestsToIssue.ToArray()); + blockersToIssue.Add(blocker); + } + } + + // Issue all of the outstanding build requests. + foreach (BuildRequestBlocker blocker in blockersToIssue) + { + // Issue the build request + IssueBuildRequest(blocker); + } + }, + isLastTask: false); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the build component host for this object. + /// + /// The host. + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + ErrorUtilities.VerifyThrow(_componentHost == null, "BuildRequestEngine already initialized!"); + _componentHost = host; + _configCache = (IConfigCache)host.GetComponent(BuildComponentType.ConfigCache); + + // Create a local configuration cache which is used to temporarily hold configurations which don't have + // proper IDs yet. We don't get this from the global config cache because that singleton shouldn't be polluted + // with our temporaries. + // NOTE: Because we don't get this from the component host, we cannot override it. + ConfigCache unresolvedConfigCache = new ConfigCache(); + unresolvedConfigCache.InitializeComponent(host); + _unresolvedConfigurations = unresolvedConfigCache; + } + + /// + /// Called to terminate the functions of this component + /// + public void ShutdownComponent() + { + ErrorUtilities.VerifyThrow(_status == BuildRequestEngineStatus.Uninitialized, "Cleanup wasn't called, status is {0}", _status); + _componentHost = null; + + ChangeStatus(BuildRequestEngineStatus.Shutdown); + } + + #endregion + + /// + /// Class factory for component creation. + /// + internal static IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.RequestEngine, "Cannot create component of type {0}", type); + return new BuildRequestEngine(); + } + + /// + /// Called when a build request entry has a state change. We should re-evaluate our requests when this happens. + /// + /// The entry raising the event. + /// The event's new state. + private void BuildRequestEntry_StateChanged(BuildRequestEntry entry, BuildRequestEntryState newState) + { + QueueAction(() => { EvaluateRequestStates(); }, isLastTask: false); + } + + #region RaiseEvents + + /// + /// Raises the OnRequestComplete event. + /// + /// The request which completed. + /// The result for the request + private void RaiseRequestComplete(BuildRequest request, BuildResult result) + { + RequestCompleteDelegate requestComplete = OnRequestComplete; + if (null != requestComplete) + { + TraceEngine("RRC: Reporting result for request {0}({1}) (nr {2}).", request.GlobalRequestId, request.ConfigurationId, request.NodeRequestId); + requestComplete(request, result); + } + } + + /// + /// Raises the OnRequestResumed event. + /// + /// The request being resumed. + private void RaiseRequestResumed(BuildRequest request) + { + RequestResumedDelegate requestResumed = OnRequestResumed; + if (null != requestResumed) + { + requestResumed(request); + } + } + + /// + /// Raises the OnEngineException event. + /// + /// The exception being thrown. + private void RaiseEngineException(Exception e) + { + EngineExceptionDelegate engineException = OnEngineException; + if (null != engineException) + { + engineException(e); + } + } + + /// + /// Raises the OnNewRequest event. + /// + /// Information about what is blocking the current request. + private void RaiseRequestBlocked(BuildRequestBlocker blocker) + { + RequestBlockedDelegate requestBlocked = OnRequestBlocked; + if (null != requestBlocked) + { + requestBlocked(blocker); + } + } + + /// + /// Raises the OnStatusChanged event. + /// + /// The new engine status. + private void RaiseEngineStatusChanged(BuildRequestEngineStatus newStatus) + { + EngineStatusChangedDelegate statusChanged = OnStatusChanged; + if (null != statusChanged) + { + statusChanged(newStatus); + } + } + + /// + /// Raises the OnNewConfigurationRequest event. + /// + /// The configuration which needs resolving. + private void RaiseNewConfigurationRequest(BuildRequestConfiguration config) + { + NewConfigurationRequestDelegate configRequest = OnNewConfigurationRequest; + if (null != configRequest) + { + configRequest(config); + } + } + + #endregion + + /// + /// Changes the engine's status and raises the OnStatsChanged event. + /// + /// The new engine status. + private void ChangeStatus(BuildRequestEngineStatus newStatus) + { + if (_status != newStatus) + { + _status = newStatus; + RaiseEngineStatusChanged(newStatus); + } + } + + /// + /// This method examines the current list of requests to determine if any requests should change + /// state, possibly reactivating a previously inactive request or removing a now-completed + /// request from the list. + /// + private void EvaluateRequestStates() + { + BuildRequestEntry activeEntry = null; + BuildRequestEntry firstReadyEntry = null; + int waitingRequests = 0; + List completedEntries = new List(); + + foreach (BuildRequestEntry currentEntry in _requests) + { + switch (currentEntry.State) + { + // This request is currently being built + case BuildRequestEntryState.Active: + ErrorUtilities.VerifyThrow(activeEntry == null, "Multiple active requests"); + activeEntry = currentEntry; + TraceEngine("ERS: Active request is now {0}({1}) (nr {2}).", currentEntry.Request.GlobalRequestId, currentEntry.Request.ConfigurationId, currentEntry.Request.NodeRequestId); + break; + + // This request is now complete. + case BuildRequestEntryState.Complete: + completedEntries.Add(currentEntry); + TraceEngine("ERS: Request {0}({1}) (nr {2}) is marked as complete.", currentEntry.Request.GlobalRequestId, currentEntry.Request.ConfigurationId, currentEntry.Request.NodeRequestId); + break; + + // This request is waiting for configurations or results + case BuildRequestEntryState.Waiting: + waitingRequests++; + break; + + // This request is ready to be built + case BuildRequestEntryState.Ready: + if (null == firstReadyEntry) + { + firstReadyEntry = currentEntry; + } + + break; + + default: + ErrorUtilities.ThrowInternalError("Unexpected BuildRequestEntry state " + currentEntry.State); + break; + } + } + + // Remove completed requests + foreach (BuildRequestEntry completedEntry in completedEntries) + { + TraceEngine("ERS: Request {0}({1}) (nr {2}) is being removed from the requests list.", completedEntry.Request.GlobalRequestId, completedEntry.Request.ConfigurationId, completedEntry.Request.NodeRequestId); + _requests.Remove(completedEntry); + _requestsByGlobalRequestId.Remove(completedEntry.Request.GlobalRequestId); + } + + // If we completed a request, that means we may be able to unload the configuration if there is memory pressure. Further we + // will also cache any result items we can find since they are rarely used. + if (completedEntries.Count > 0) + { + CheckMemoryUsage(); + } + + // Update current engine status and start the next request, if applicable. + if (null == activeEntry) + { + if (null != firstReadyEntry) + { + // We are now active because we have an entry which is building. + ChangeStatus(BuildRequestEngineStatus.Active); + } + else + { + if (waitingRequests == 0) + { + ChangeStatus(BuildRequestEngineStatus.Idle); + } + else + { + ChangeStatus(BuildRequestEngineStatus.Waiting); + } + } + } + else + { + ChangeStatus(BuildRequestEngineStatus.Active); + } + + // Finally, raise the completed events so they occur AFTER the state of the engine has changed, + // otherwise the client might observe the engine as being active after having received + // completed notifications for all requests, which would be odd. + foreach (BuildRequestEntry completedEntry in completedEntries) + { + // Shut it down because we already have enough in reserve. + completedEntry.Builder.OnNewBuildRequests -= new NewBuildRequestsDelegate(Builder_OnNewBuildRequests); + completedEntry.Builder.OnBuildRequestBlocked -= new BuildRequestBlockedDelegate(Builder_OnBlockedRequest); + ((IBuildComponent)completedEntry.Builder).ShutdownComponent(); + + BuildRequestConfiguration configuration = _configCache[completedEntry.Request.ConfigurationId]; + + // Update the default targets. Note that if the project failed to load, Project will be null so we can't do this. + if (configuration.IsLoaded) + { + // Now update this result so that the Build Manager can correctly match results from its + // own cache. + completedEntry.Result.DefaultTargets = configuration.ProjectDefaultTargets; + completedEntry.Result.InitialTargets = configuration.ProjectInitialTargets; + } + + TraceEngine("ERS: Request is now {0}({1}) (nr {2}) has had its builder cleaned up.", completedEntry.Request.GlobalRequestId, completedEntry.Request.ConfigurationId, completedEntry.Request.NodeRequestId); + RaiseRequestComplete(completedEntry.Request, completedEntry.Result); + } + } + + /// + /// Check the amount of memory we are using and, if we exceed the threshold, unload cacheable items. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect", Justification = "We're trying to get rid of memory because we're running low, so we need to collect NOW in order to free it up ASAP")] + private void CheckMemoryUsage() + { + // Jeffrey Richter suggests that when the memory load in the system exceeds 80% it is a good + // idea to start finding ways to unload unnecessary data to prevent memory starvation. We use this metric in + // our calculations below. + NativeMethodsShared.MemoryStatus memoryStatus = NativeMethodsShared.GetMemoryStatus(); + if (memoryStatus != null) + { + try + { + // The minimum limit must be no more than 80% of the virtual memory limit to reduce the chances of a single unfortunately + // large project resulting in allocations which exceed available VM space between calls to this function. This situation + // is more likely on 32-bit machines where VM space is only 2 gigs. + ulong memoryUseLimit = Convert.ToUInt64(memoryStatus.TotalVirtual * 0.8); + + // See how much memory we are using and compart that to our limit. + ulong memoryInUse = memoryStatus.TotalVirtual - memoryStatus.AvailableVirtual; + while ((memoryInUse > memoryUseLimit) || _debugForceCaching) + { + TraceEngine("Memory usage at {0}, limit is {1}. Caching configurations and results cache and collecting.", memoryInUse, memoryUseLimit); + IResultsCache resultsCache = _componentHost.GetComponent(BuildComponentType.ResultsCache) as IResultsCache; + + resultsCache.WriteResultsToDisk(); + if (_configCache.WriteConfigurationsToDisk()) + { + // We have to collect here because WriteConfigurationsToDisk only collects 10% of the configurations. It is entirely possible + // that those 10% don't constitute enough collected memory to reduce our usage below the threshold. The only way to know is to + // force the collection then re-test the memory usage. We repeat until we have reduced our use below the threshold or + // we failed to write any more configurations to disk. + GC.Collect(); + } + else + { + break; + } + + memoryStatus = NativeMethodsShared.GetMemoryStatus(); + memoryInUse = memoryStatus.TotalVirtual - memoryStatus.AvailableVirtual; + TraceEngine("Memory usage now at {0}", memoryInUse); + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + else + { + _nodeLoggingContext.LogFatalBuildError(new BuildEventFileInfo(Microsoft.Build.Construction.ElementLocation.EmptyLocation), e); + throw new BuildAbortedException(e.Message, e); + } + } + } + } + + /// + /// Makes the specified build request entry the active one, loading the project if necessary. + /// + /// The entry to activate. + private void ActivateBuildRequest(BuildRequestEntry entry) + { + ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + + entry.RequestConfiguration.RetrieveFromCache(); + + // First, determine if we have a loaded project for this entry + if (entry.Builder == null) + { + // This is the first time this project has been activated. + // Set the request builder. + entry.Builder = GetRequestBuilder(); + + // Now call into the request builder to do the building + entry.Builder.BuildRequest(_nodeLoggingContext, entry); + } + else + { + // We are resuming the build request + entry.Builder.ContinueRequest(); + } + + RaiseRequestResumed(entry.Request); + } + + /// + /// Returns an unused request builder if there are any, or creates a new one. + /// + /// An IRequestBuilder to use. + private IRequestBuilder GetRequestBuilder() + { + IRequestBuilder builder = (IRequestBuilder)_componentHost.GetComponent(BuildComponentType.RequestBuilder); + + // NOTE: We do NOT need to register for the OnBuildRequestCompleted because we already watch the BuildRequestEntry + // state changes. + builder.OnNewBuildRequests += new NewBuildRequestsDelegate(Builder_OnNewBuildRequests); + builder.OnBuildRequestBlocked += new BuildRequestBlockedDelegate(Builder_OnBlockedRequest); + + return builder; + } + + /// + /// Starts to terminate any builder associated with the entry and clean it up in preparation for removal. + /// + /// The entry to be deactivated + private void BeginDeactivateBuildRequest(BuildRequestEntry entry) + { + if (entry.Builder != null) + { + entry.BeginCancel(); + } + } + + /// + /// Waits for the builders until they are terminated. + /// + /// The entry to be deactivated + private void WaitForDeactivateCompletion(BuildRequestEntry entry) + { + if (entry.Builder != null) + { + entry.WaitForCancelCompletion(); + } + } + + #region RequestBuilder Event Handlers + + /// + /// Raised when the active request needs to build new requests. + /// + /// The request issuing the requests. + /// The requests being issued. + /// Called by the RequestBuilder (implicitly through an event). Non-overlapping with other RequestBuilders. + private void Builder_OnNewBuildRequests(BuildRequestEntry sourceEntry, FullyQualifiedBuildRequest[] newRequests) + { + QueueAction( + () => + { + _unsubmittedRequests.Enqueue(new PendingUnsubmittedBuildRequests(sourceEntry, newRequests)); + IssueUnsubmittedRequests(); + EvaluateRequestStates(); + }, + isLastTask: false); + } + + /// + /// Called when the request builder needs to block on another request. + /// + /// + /// Called by the RequestBuilder (implicitly through an event). Non-overlapping with other RequestBuilders. + private void Builder_OnBlockedRequest(BuildRequestEntry sourceEntry, int blockingGlobalRequestId, string blockingTarget) + { + QueueAction( + () => + { + _unsubmittedRequests.Enqueue(new PendingUnsubmittedBuildRequests(sourceEntry, blockingGlobalRequestId, blockingTarget)); + IssueUnsubmittedRequests(); + EvaluateRequestStates(); + }, + isLastTask: false); + } + + #endregion + + /// + /// Dequeue some requests from the unsubmitted request queue and submit them. + /// + private void IssueUnsubmittedRequests() + { + // We will only submit as many items as were in the queue at the time this method was called. + // This prevents us from a) having to lock the queue for the whole loop or b) getting into + // an endless loop where another thread pushes requests into the queue as fast as we can + // discharge them. + int countToSubmit = _unsubmittedRequests.Count; + while (countToSubmit != 0) + { + PendingUnsubmittedBuildRequests unsubmittedRequest; + unsubmittedRequest = _unsubmittedRequests.Dequeue(); + + BuildRequestEntry sourceEntry = unsubmittedRequest.SourceEntry; + + if (unsubmittedRequest.BlockingGlobalRequestId == sourceEntry.Request.GlobalRequestId) + { + if (unsubmittedRequest.BlockingTarget == null) + { + // We are yielding + IssueBuildRequest(new BuildRequestBlocker(sourceEntry.Request.GlobalRequestId, sourceEntry.GetActiveTargets(), YieldAction.Yield)); + lock (sourceEntry.GlobalLock) + { + sourceEntry.WaitForBlockingRequest(sourceEntry.Request.GlobalRequestId); + } + } + else + { + // We are ready to continue + IssueBuildRequest(new BuildRequestBlocker(sourceEntry.Request.GlobalRequestId, sourceEntry.GetActiveTargets(), YieldAction.Reacquire)); + } + } + else if (unsubmittedRequest.BlockingGlobalRequestId == BuildRequest.InvalidGlobalRequestId) + { + if (unsubmittedRequest.NewRequests != null) + { + // We aren't blocked on another request, we are blocked on new requests + IssueBuildRequests(sourceEntry, unsubmittedRequest.NewRequests); + } + else + { + // We are blocked waiting for our results to transfer + lock (sourceEntry.GlobalLock) + { + sourceEntry.WaitForBlockingRequest(sourceEntry.Request.GlobalRequestId); + } + + IssueBuildRequest(new BuildRequestBlocker(sourceEntry.Request.GlobalRequestId)); + } + } + else + { + // We are blocked on an existing build request. + lock (sourceEntry.GlobalLock) + { + sourceEntry.WaitForBlockingRequest(unsubmittedRequest.BlockingGlobalRequestId); + } + + IssueBuildRequest(new BuildRequestBlocker(sourceEntry.Request.GlobalRequestId, sourceEntry.GetActiveTargets(), unsubmittedRequest.BlockingGlobalRequestId, unsubmittedRequest.BlockingTarget)); + } + + countToSubmit--; + } + } + + /// + /// This method is responsible for evaluating whether we have enough information to make the request of the Build Manager, + /// or if we need to obtain additional configuration information. It then issues either configuration + /// requests or build requests, or both as needed. + /// + /// The BuildRequestEntry which is making the request + /// The array of "child" build requests to be issued. + /// + /// When we receive a build request, we first have to determine if we already have a configuration which matches the + /// one used by the request. We do this because everywhere we deal with requests and results beyond this function, we + /// use configuration ids, which are assigned once by the Build Manager and are global to the system. If we do + /// not have a global configuration id, we can't check to see if we already have build results for the request, so we + /// cannot send the request out. Thus, first we determine the configuration id. + /// + /// Assuming we don't have the global configuration id locally, we will send the configuration to the Build Manager. + /// It will look up or assign the global configuration id and send it back to us. + /// + /// Once we have the global configuration id, we can then look up results locally. If we have enough results to fulfill + /// the request, we give them back to the request, otherwise we have to forward the request to the Build Mangager + /// for scheduling. + /// + private void IssueBuildRequests(BuildRequestEntry issuingEntry, FullyQualifiedBuildRequest[] newRequests) + { + ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + + // For each request, we need to determine if we have a local configuration in the + // configuration cache. If we do, we can issue the build request immediately. + // Otherwise, we need to ask the Build Manager for configuration IDs and issue those requests + // later. + IConfigCache globalConfigCache = (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache); + + // We are going to potentially issue several requests. We don't want the state of the request being modified by + // other threads while this occurs, so we lock the request for the duration. This lock is the same lock + // used by the BuildRequestEntry itself to lock each of its data-modifying methods, effectively preventing + // any other thread from modifying the BuildRequestEntry while we hold it. This mechanism also means that it + // is not necessary for other threads to take the global lock explicitly if they are just doing single operations + // to the entry rather than a series of them. + lock (issuingEntry.GlobalLock) + { + List existingResultsToReport = new List(); + HashSet> unresolvedConfigurationsAdded = new HashSet>(); + + foreach (FullyQualifiedBuildRequest request in newRequests) + { + // Do we have a matching configuration? + BuildRequestConfiguration matchingConfig = globalConfigCache.GetMatchingConfiguration(request.Config); + BuildRequest newRequest = null; + if (matchingConfig == null) + { + // No configuration locally, are we already waiting for it? + matchingConfig = _unresolvedConfigurations.GetMatchingConfiguration(request.Config); + if (matchingConfig == null) + { + // Not waiting for it + request.Config.ConfigurationId = GetNextUnresolvedConfigurationId(); + _unresolvedConfigurations.AddConfiguration(request.Config); + unresolvedConfigurationsAdded.Add(request.Config.ConfigurationId); + } + else + { + request.Config.ConfigurationId = matchingConfig.ConfigurationId; + } + + // Whether we are already waiting for a configuration or we need to wait for another one + // we will add this request as waiting for a configuration. As new configuration resolutions + // come in, we will check our requests which are waiting for configurations move them to + // waiting for results. It is important that we tell the issuing request to wait for a result + // prior to issuing any necessary configuration request so that we don't get into a state where + // we receive the configuration response before we enter the wait state. + newRequest = new BuildRequest(issuingEntry.Request.SubmissionId, GetNextBuildRequestId(), request.Config.ConfigurationId, request.Targets, issuingEntry.Request.HostServices, issuingEntry.Request.BuildEventContext, issuingEntry.Request); + + issuingEntry.WaitForResult(newRequest); + + if (matchingConfig == null) + { + // Issue the config resolution request + TraceEngine("Request {0}({1}) (nr {2}) is waiting on configuration {3} (IBR)", issuingEntry.Request.GlobalRequestId, issuingEntry.Request.ConfigurationId, issuingEntry.Request.NodeRequestId, request.Config.ConfigurationId); + issuingEntry.WaitForConfiguration(request.Config); + } + } + else + { + // We have a configuration, see if we already have results locally. + newRequest = new BuildRequest(issuingEntry.Request.SubmissionId, GetNextBuildRequestId(), matchingConfig.ConfigurationId, request.Targets, issuingEntry.Request.HostServices, issuingEntry.Request.BuildEventContext, issuingEntry.Request); + + IResultsCache resultsCache = (IResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache); + ResultsCacheResponse response = resultsCache.SatisfyRequest(newRequest, matchingConfig.ProjectInitialTargets, matchingConfig.ProjectDefaultTargets, matchingConfig.GetAfterTargetsForDefaultTargets(newRequest), skippedResultsAreOK: false); + if (response.Type == ResultsCacheResponseType.Satisfied) + { + // We have a result, give it back to this request. + issuingEntry.WaitForResult(newRequest); + + // Log the fact that we handled this from the cache. + _nodeLoggingContext.LogRequestHandledFromCache(newRequest, _configCache[newRequest.ConfigurationId], response.Results); + + // Can't report the result directly here, because that could cause the request to go from + // Waiting to Ready. + existingResultsToReport.Add(response.Results); + } + else + { + // No result, to wait for it. + issuingEntry.WaitForResult(newRequest); + } + } + } + + // If we have any results we had to report, do so now. + foreach (BuildResult existingResult in existingResultsToReport) + { + issuingEntry.ReportResult(existingResult); + } + + // Issue any configuration requests we may still need. + List unresolvedConfigurationsToIssue = issuingEntry.GetUnresolvedConfigurationsToIssue(); + if (unresolvedConfigurationsToIssue != null) + { + foreach (BuildRequestConfiguration unresolvedConfigurationToIssue in unresolvedConfigurationsToIssue) + { + unresolvedConfigurationsAdded.Remove(unresolvedConfigurationToIssue.ConfigurationId); + IssueConfigurationRequest(unresolvedConfigurationToIssue); + } + } + + // Remove any configurations we ended up not waiting for, otherwise future requests will think we are still waiting for them + // and will never get submitted. + foreach (int unresolvedConfigurationId in unresolvedConfigurationsAdded) + { + _unresolvedConfigurations.RemoveConfiguration(unresolvedConfigurationId); + } + + // Finally, if we can issue build requests, do so. + List requestsToIssue = issuingEntry.GetRequestsToIssueIfReady(); + if (requestsToIssue != null) + { + BuildRequestBlocker blocker = new BuildRequestBlocker(issuingEntry.Request.GlobalRequestId, issuingEntry.GetActiveTargets(), requestsToIssue.ToArray()); + IssueBuildRequest(blocker); + } + + if (issuingEntry.State == BuildRequestEntryState.Ready) + { + ErrorUtilities.VerifyThrow((requestsToIssue == null) || (requestsToIssue.Count == 0), "Entry shouldn't be ready if we also issued requests."); + ActivateBuildRequest(issuingEntry); + } + } + } + + /// + /// Retrieves a new configuration ID + /// + /// The next unused local configuration ID. + private int GetNextUnresolvedConfigurationId() + { + unchecked + { + do + { + _nextUnresolvedConfigurationId--; + if (_nextUnresolvedConfigurationId >= 0) + { + _nextUnresolvedConfigurationId = StartingUnresolvedConfigId; + } + } + while (_unresolvedConfigurations.HasConfiguration(_nextUnresolvedConfigurationId)); + } + + return _nextUnresolvedConfigurationId; + } + + /// + /// Retrieves a new build request ID + /// + /// The next build request ID. + private int GetNextBuildRequestId() + { + unchecked + { + _nextBuildRequestId++; + if (_nextBuildRequestId < 0) + { + _nextBuildRequestId = StartingBuildRequestId; + } + } + + return _nextBuildRequestId; + } + + /// + /// This method forms a configuration request from an unresolved configuration and posts it to the + /// Build Manager. + /// + /// The configuration to be mapped. + private void IssueConfigurationRequest(BuildRequestConfiguration config) + { + ErrorUtilities.VerifyThrowArgument(config.WasGeneratedByNode, "InvalidConfigurationId"); + ErrorUtilities.VerifyThrowArgumentNull(config, "config"); + ErrorUtilities.VerifyThrowInvalidOperation(_unresolvedConfigurations.HasConfiguration(config.ConfigurationId), "NoUnresolvedConfiguration"); + TraceEngine("Issuing configuration request for node config {0}", config.ConfigurationId); + RaiseNewConfigurationRequest(config); + } + + /// + /// Sends a build request to the Build Manager for scheduling + /// + /// The information about why the request is blocked. + private void IssueBuildRequest(BuildRequestBlocker blocker) + { + ErrorUtilities.VerifyThrowArgumentNull(blocker, "blocker"); + + if (blocker.BuildRequests == null) + { + // This is the case when we aren't blocking on new requests, but rather an in-progress request which is executing a target for which we need results. + TraceEngine("Blocking global request {0} on global request {1} because it is already executing target {2}", blocker.BlockedRequestId, blocker.BlockingRequestId, blocker.BlockingTarget); + } + else + { + foreach (BuildRequest blockingRequest in blocker.BuildRequests) + { + TraceEngine("Sending node request {0} (configuration {1}) with parent {2} to Build Manager", blockingRequest.NodeRequestId, blockingRequest.ConfigurationId, blocker.BlockedRequestId); + } + } + + RaiseRequestBlocked(blocker); + } + + /// + /// Queue an action to be run in the engine. + /// + /// The action to execute. + /// True if the task was scheduled, false otherwise. + /// This method will return false if an attempt is made to schedule an action after the queue has been shut down. + private bool QueueAction(Action action, bool isLastTask) + { + bool actionQueued = false; + var queue = _workQueue; + if (queue != null) + { + lock (queue) + { + actionQueued = queue.Post( + () => + { + try + { + action.Invoke(); + } + catch (Exception e) + { + TraceEngine("EL: EXCEPTION caught in engine: {0} - {1}", e.GetType(), e.Message); + + // Dump all engine exceptions to a temp file + // so that we have something to go on in the + // event of a failure + ExceptionHandling.DumpExceptionToFile(e); + + // Raise the exception to the host, so that it can signal termination of the build. + RaiseEngineException(e); + + TraceEngine("EL: Deactivating requests due to exception."); + + // Let the critical ones melt down the system. + if (ExceptionHandling.IsCriticalException(e)) + { + ErrorUtilities.ThrowInternalError(e.Message, e); + } + + // This is fatal to the execution of the ActionBlock. No more messages will be processed, and the + // build will be terminated. + throw; + } + }); + + if (isLastTask) + { + // No more tasks will be allowed to post to this queue. + queue.Complete(); + } + } + } + + return actionQueued; + } + + /// + /// Method used for debugging purposes. + /// + private void TraceEngine(string format, params object[] stuff) + { + if (_debugDumpState) + { + lock (this) + { + StreamWriter file = new StreamWriter(String.Format(CultureInfo.CurrentCulture, Path.Combine(_debugDumpPath, @"EngineTrace_{0}.txt"), Process.GetCurrentProcess().Id), true); + string message = String.Format(CultureInfo.CurrentCulture, format, stuff); + file.WriteLine("{0}({1})-{2}: {3}", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId, DateTime.UtcNow.Ticks, message); + file.Flush(); + file.Close(); + } + } + } + + /// + /// Struct used to contain information about requests submitted by the RequestBuilder. + /// + private struct PendingUnsubmittedBuildRequests + { + /// + /// The global request id on which we are blocking + /// + public int BlockingGlobalRequestId; + + /// + /// The target on which we are blocking + /// + public string BlockingTarget; + + /// + /// The issuing request + /// + public BuildRequestEntry SourceEntry; + + /// + /// The new requests to issue + /// + public FullyQualifiedBuildRequest[] NewRequests; + + /// + /// Create a new unsubmitted request entry + /// + /// The build request originating these requests. + /// The new requests to be issued. + public PendingUnsubmittedBuildRequests(BuildRequestEntry sourceEntry, FullyQualifiedBuildRequest[] newRequests) + { + this.SourceEntry = sourceEntry; + this.NewRequests = newRequests; + this.BlockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; + this.BlockingTarget = null; + } + + /// + /// Create a new unsubmitted request entry + /// + /// The build request originating these requests. + /// The request on which we are blocked. + public PendingUnsubmittedBuildRequests(BuildRequestEntry sourceEntry, int blockingGlobalRequestId, string blockingTarget) + { + this.SourceEntry = sourceEntry; + this.NewRequests = null; + this.BlockingGlobalRequestId = blockingGlobalRequestId; + this.BlockingTarget = blockingTarget; + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs new file mode 100644 index 00000000000..8a4e421ece9 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/BuildRequestEntry.cs @@ -0,0 +1,633 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class representing a build request entry in the request engine's queue. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using System.Diagnostics; + +using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Delegate is called when the state for a build request entry has changed. + /// + /// The entry whose state has changed. + /// The new state value. + internal delegate void BuildRequestEntryStateChangedDelegate(BuildRequestEntry entry, BuildRequestEntryState newState); + + /// + /// The set of states in which a build request entry can be. + /// + internal enum BuildRequestEntryState + { + /// + /// There should only ever be one entry in the Active state. This is the request which is + /// being actively built by the engine - i.e. it has a running task thread. All other requests + /// must be in one of the other states. When in this state, the outstandingRequest and + /// receivedResult members must be null. + /// + /// Transitions: + /// Waiting: When an msbuild callback is made the active build request needs to wait + /// for the results in order to continue to process. + /// Complete: The build request has generated all of the required results. + /// + Active, + + /// + /// This state means the node has received all of the results needed to continue processing this + /// request. When this state is set, the receivedResult member of this entry must be non-null. + /// The request engine can continue it at some later point when it is no longer busy. + /// Any number of entries may be in this state. + /// + /// Transitions: + /// Active: The build request engine picks this ready request to process. + /// + Ready, + + /// + /// This state means the node is waiting for results from outstanding build requests. When this + /// state is set, the outstandingRequest or outstandingConfiguration members of the entry + /// must be non-null. + /// + /// Transitions: + /// Ready: All of the results which caused the build request to wait have been recieved + /// + Waiting, + + /// + /// This state means the request has completed and results are available. The engine will remove + /// the request from the list and the results will be returned to the node for processing. + /// + /// Transitions: None, this is the final state of the build request + /// + Complete + } + + /// + /// BuildRequestEntry holds a build request and associated state data. + /// + internal class BuildRequestEntry + { + /// + /// A class used for thread synchronization + /// + private Object _dataMonitor; + + /// + /// The request for this entry + /// + private BuildRequest _request; + + /// + /// The request configuration. + /// + private BuildRequestConfiguration _requestConfiguration; + + /// + /// The result of building this entry's request + /// + private BuildResult _completedResult; + + /// + /// Mapping of Build Request Configurations to Build Requests waiting for configuration resolution. + /// + private Dictionary> _unresolvedConfigurations; + + /// + /// The set of requests to issue. This holds all of the requests as we prepare them. Once their configurations + /// have all been resolved, we will issue them to the Scheduler in the order received. + /// + private List _requestsToIssue; + + /// + /// The list of unresolved configurations we need to issue. + /// + private List _unresolvedConfigurationsToIssue; + + /// + /// Mapping of nodeRequestIDs to Build Requests waiting for results. + /// + private Dictionary _outstandingRequests; + + /// + /// Mapping of nodeRequestIDs to Build Results. + /// + private Dictionary _outstandingResults; + + /// + /// The ID of the request we are blocked waiting for. + /// + private int _blockingGlobalRequestId; + + /// + /// The current state of this entry. + /// + private BuildRequestEntryState _state; + + /// + /// The object used to build this request. + /// + private IRequestBuilder _requestBuilder; + + /// + /// The project's root directory. + /// + private string _projectRootDirectory; + + /// + /// Creates a build request entry from a build request. + /// + /// The originating build request. + /// The build request configuration. + internal BuildRequestEntry(BuildRequest request, BuildRequestConfiguration requestConfiguration) + { + ErrorUtilities.VerifyThrowArgumentNull(request, "request"); + ErrorUtilities.VerifyThrowArgumentNull(requestConfiguration, "requestConfiguration"); + ErrorUtilities.VerifyThrow(requestConfiguration.ConfigurationId == request.ConfigurationId, "Configuration id mismatch"); + + _dataMonitor = new Object(); + _request = request; + _requestConfiguration = requestConfiguration; + _blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; + _completedResult = null; + ChangeState(BuildRequestEntryState.Ready); + } + + /// + /// Raised when the state changes. + /// + public event BuildRequestEntryStateChangedDelegate OnStateChanged; + + /// + /// Returns the object used to lock for synchronization of long-running operations. + /// + public Object GlobalLock + { + [DebuggerStepThrough] + get + { return _dataMonitor; } + } + + /// + /// Returns the root directory for the project being built by this request. + /// + public string ProjectRootDirectory + { + get + { + if (_projectRootDirectory == null) + { + _projectRootDirectory = Path.GetDirectoryName(RequestConfiguration.ProjectFullPath); + } + + return _projectRootDirectory; + } + } + + /// + /// Returns the current state of the build request. + /// + public BuildRequestEntryState State + { + [DebuggerStepThrough] + get + { return _state; } + } + + /// + /// Returns the request which originated this entry. + /// + public BuildRequest Request + { + [DebuggerStepThrough] + get + { return _request; } + } + + /// + /// Returns the build request configuration + /// + public BuildRequestConfiguration RequestConfiguration + { + [DebuggerStepThrough] + get + { return _requestConfiguration; } + } + + /// + /// Returns the overall result for this request. + /// + public BuildResult Result + { + [DebuggerStepThrough] + get + { return _completedResult; } + } + + /// + /// Returns the request builder. + /// + public IRequestBuilder Builder + { + [DebuggerStepThrough] + get + { + return _requestBuilder; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow(value == null || _requestBuilder == null, "Request Builder already set."); + _requestBuilder = value; + } + } + + /// + /// Informs the entry that it has configurations which need to be resolved. + /// + /// The configuration to be resolved. + public void WaitForConfiguration(BuildRequestConfiguration configuration) + { + ErrorUtilities.VerifyThrow(configuration.WasGeneratedByNode, "Configuration has already been resolved."); + + _unresolvedConfigurationsToIssue = _unresolvedConfigurationsToIssue ?? new List(); + _unresolvedConfigurationsToIssue.Add(configuration); + } + + /// + /// Waits for a result from a request. + /// + /// The build request + public void WaitForResult(BuildRequest newRequest) + { + WaitForResult(newRequest, true); + } + + /// + /// Signals that we are waiting for a specific blocking request to finish. + /// + public void WaitForBlockingRequest(int blockingGlobalRequestId) + { + lock (_dataMonitor) + { + ErrorUtilities.VerifyThrow(_state == BuildRequestEntryState.Active, "Must be in Active state to wait for blocking request. Config: {0} State: {1}", _requestConfiguration.ConfigurationId, _state); + + _blockingGlobalRequestId = blockingGlobalRequestId; + + ChangeState(BuildRequestEntryState.Waiting); + } + } + + /// + /// Waits for a result from a request which previously had an unresolved configuration. + /// + /// The id of the unresolved configuration. + /// The id of the resolved configuration. + /// True if all unresolved configurations have been resolved, false otherwise. + public bool ResolveConfigurationRequest(int unresolvedConfigId, int configId) + { + lock (_dataMonitor) + { + if (_unresolvedConfigurations == null || !_unresolvedConfigurations.ContainsKey(unresolvedConfigId)) + { + return false; + } + + List requests = _unresolvedConfigurations[unresolvedConfigId]; + _unresolvedConfigurations.Remove(unresolvedConfigId); + + if (_unresolvedConfigurations.Count == 0) + { + _unresolvedConfigurations = null; + } + + foreach (BuildRequest request in requests) + { + request.ResolveConfiguration(configId); + WaitForResult(request, false); + } + + return (_unresolvedConfigurations == null); + } + } + + /// + /// Returns the set of build requests which should be issued to the scheduler. + /// + public List GetRequestsToIssueIfReady() + { + if (_unresolvedConfigurations == null && _requestsToIssue != null) + { + List requests = _requestsToIssue; + _requestsToIssue = null; + + return requests; + } + + return null; + } + + /// + /// Returns the list of unresolved configurations to issue. + /// + public List GetUnresolvedConfigurationsToIssue() + { + if (_unresolvedConfigurationsToIssue != null) + { + List configurationsToIssue = _unresolvedConfigurationsToIssue; + _unresolvedConfigurationsToIssue = null; + return configurationsToIssue; + } + + return null; + } + + /// + /// Returns the list of currently active targets. + /// + public string[] GetActiveTargets() + { + string[] activeTargets = new string[_requestConfiguration.ActivelyBuildingTargets.Count]; + + int index = 0; + foreach (string target in _requestConfiguration.ActivelyBuildingTargets.Keys) + { + activeTargets[index++] = target; + } + + return activeTargets; + } + + /// + /// This reports a result for a request on which this entry was waiting. + /// PERF: Once we have fixed up all the result reporting, we can probably + /// optimize this. See the comment in BuildRequestEngine.ReportBuildResult. + /// + /// The result for the request. + public void ReportResult(BuildResult result) + { + lock (_dataMonitor) + { + ErrorUtilities.VerifyThrowArgumentNull(result, "result"); + ErrorUtilities.VerifyThrow(_state == BuildRequestEntryState.Waiting || _outstandingRequests == null, "Entry must be in the Waiting state to report results, or we must have flushed our requests due to an error. Config: {0} State: {1} Requests: {2}", _requestConfiguration.ConfigurationId, _state, _outstandingRequests != null); + + // If the matching request is in the issue list, remove it so we don't try to ask for it to be built. + if (_requestsToIssue != null) + { + for (int i = 0; i < _requestsToIssue.Count; i++) + { + if (_requestsToIssue[i].NodeRequestId == result.NodeRequestId) + { + _requestsToIssue.RemoveAt(i); + if (_requestsToIssue.Count == 0) + { + _requestsToIssue = null; + } + + break; + } + } + } + + // If this result is for the request we were blocked on locally (target re-entrancy) clear out our blockage. + bool addResults = false; + if (_blockingGlobalRequestId == result.GlobalRequestId) + { + _blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; + if (_outstandingRequests == null) + { + ErrorUtilities.VerifyThrow(result.CircularDependency, "Received result for target in progress and it wasn't a circular dependency error."); + addResults = true; + } + } + + // We could be in the waiting state but waiting on configurations instead of results, or we received a circular dependency + // result, which blows away everything else we were waiting on. + if (_outstandingRequests != null) + { + _outstandingRequests.Remove(result.NodeRequestId); + + // If we wish to implement behavior where we stop building after the first failing request, then check for + // overall results being failure rather than just circular dependency. Sync with BasicScheduler.ReportResult and + // BasicScheduler.ReportRequestBlocked. + if (result.CircularDependency || (_outstandingRequests.Count == 0 && (_unresolvedConfigurations == null || _unresolvedConfigurations.Count == 0))) + { + _outstandingRequests = null; + _unresolvedConfigurations = null; + + // If we are in the middle of IssueBuildRequests and collecting requests (and cached results), one of those results + // was a failure. As a result, this entry will fail, and submitting further requests from it would be pointless. + _requestsToIssue = null; + _unresolvedConfigurationsToIssue = null; + } + + addResults = true; + } + + if (addResults) + { + // Update the local results record + _outstandingResults = _outstandingResults ?? new Dictionary(); + ErrorUtilities.VerifyThrow(!_outstandingResults.ContainsKey(result.NodeRequestId), "Request already contains results."); + _outstandingResults.Add(result.NodeRequestId, result); + } + + // If we are out of outstanding requests, we are ready to continue. + if (_outstandingRequests == null && _unresolvedConfigurations == null && _blockingGlobalRequestId == BuildRequest.InvalidGlobalRequestId) + { + ChangeState(BuildRequestEntryState.Ready); + } + } + } + + /// + /// Unblocks an entry which was waiting for a specific global request id. + /// + public void Unblock() + { + lock (_dataMonitor) + { + ErrorUtilities.VerifyThrow(_state == BuildRequestEntryState.Waiting, "Entry must be in the waiting state to be unblocked. Config: {0} State: {1} Request: {2}", _requestConfiguration.ConfigurationId, _state, this.Request.GlobalRequestId); + ErrorUtilities.VerifyThrow(_blockingGlobalRequestId != BuildRequest.InvalidGlobalRequestId, "Entry must be waiting on another request to be unblocked. Config: {0} Request: {1}", _requestConfiguration.ConfigurationId, this.Request.GlobalRequestId); + + _blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; + + ChangeState(BuildRequestEntryState.Ready); + } + } + + /// + /// Marks the entry as active and returns all of the results needed to continue. + /// Results are returned as { nodeRequestId -> BuildResult } + /// + /// The results for all previously pending requests, or null if there were none. + public IDictionary Continue() + { + lock (_dataMonitor) + { + ErrorUtilities.VerifyThrow(_unresolvedConfigurations == null, "All configurations must be resolved before Continue may be called."); + ErrorUtilities.VerifyThrow(_outstandingRequests == null, "All outstanding requests must have been satisfied."); + ErrorUtilities.VerifyThrow(_state == BuildRequestEntryState.Ready, "Entry must be in the Ready state. Config: {0} State: {1}", _requestConfiguration.ConfigurationId, _state); + + IDictionary ret = _outstandingResults; + _outstandingResults = null; + + ChangeState(BuildRequestEntryState.Active); + + return ret; + } + } + + /// + /// Starts to cancel the current request. + /// + public void BeginCancel() + { + lock (_dataMonitor) + { + if (_state == BuildRequestEntryState.Waiting) + { + if (_outstandingResults == null && _outstandingRequests != null) + { + _outstandingResults = new Dictionary(_outstandingRequests.Count); + } + + if (_outstandingRequests != null) + { + foreach (KeyValuePair requestEntry in _outstandingRequests) + { + _outstandingResults[requestEntry.Key] = new BuildResult(requestEntry.Value, new BuildAbortedException()); + } + } + + if (_unresolvedConfigurations != null && _outstandingResults != null) + { + foreach (List requests in _unresolvedConfigurations.Values) + { + foreach (BuildRequest request in requests) + { + _outstandingResults[request.NodeRequestId] = new BuildResult(request, new BuildAbortedException()); + } + } + } + + _unresolvedConfigurations = null; + _outstandingRequests = null; + ChangeState(BuildRequestEntryState.Ready); + } + } + + if (null != _requestBuilder) + { + _requestBuilder.BeginCancel(); + } + } + + /// + /// Waits for the current request until it's canceled. + /// + public void WaitForCancelCompletion() + { + if (null != _requestBuilder) + { + _requestBuilder.WaitForCancelCompletion(); + } + } + + /// + /// Marks this entry as complete and sets the final results. + /// + /// The result of the build. + public void Complete(BuildResult result) + { + lock (_dataMonitor) + { + ErrorUtilities.VerifyThrowArgumentNull(result, "result"); + ErrorUtilities.VerifyThrow(_completedResult == null, "Entry already Completed."); + + // If this request is determined to be a success, then all outstanding items must have been taken care of + // and it must be in the correct state. It can complete unsuccessfully for a variety of reasons in a variety + // of states. + if (result.OverallResult == BuildResultCode.Success) + { + ErrorUtilities.VerifyThrow(_state == BuildRequestEntryState.Active, "Entry must be active before it can be Completed successfully. Config: {0} State: {1}", _requestConfiguration.ConfigurationId, _state); + ErrorUtilities.VerifyThrow(_unresolvedConfigurations == null, "Entry must not have any unresolved configurations."); + ErrorUtilities.VerifyThrow(_outstandingRequests == null, "Entry must have no outstanding requests."); + ErrorUtilities.VerifyThrow(_outstandingResults == null, "Results must be consumed before request may be completed."); + } + + _completedResult = result; + ChangeState(BuildRequestEntryState.Complete); + } + } + + /// + /// Adds a request to the set of waiting requests. + /// + private void WaitForResult(BuildRequest newRequest, bool addToIssueList) + { + lock (_dataMonitor) + { + ErrorUtilities.VerifyThrow(_state == BuildRequestEntryState.Active || _state == BuildRequestEntryState.Waiting, "Must be in Active or Waiting state to wait for results. Config: {0} State: {1}", _requestConfiguration.ConfigurationId, _state); + + if (newRequest.IsConfigurationResolved) + { + _outstandingRequests = _outstandingRequests ?? new Dictionary(); + + ErrorUtilities.VerifyThrow(!_outstandingRequests.ContainsKey(newRequest.NodeRequestId), "Already waiting for local request {0}", newRequest.NodeRequestId); + _outstandingRequests.Add(newRequest.NodeRequestId, newRequest); + } + else + { + ErrorUtilities.VerifyThrow(addToIssueList == true, "Requests with unresolved configurations should always be added to the issue list."); + _unresolvedConfigurations = _unresolvedConfigurations ?? new Dictionary>(); + + if (!_unresolvedConfigurations.ContainsKey(newRequest.ConfigurationId)) + { + _unresolvedConfigurations.Add(newRequest.ConfigurationId, new List()); + } + + _unresolvedConfigurations[newRequest.ConfigurationId].Add(newRequest); + } + + if (addToIssueList) + { + _requestsToIssue = _requestsToIssue ?? new List(); + _requestsToIssue.Add(newRequest); + } + + ChangeState(BuildRequestEntryState.Waiting); + } + } + + /// + /// Updates the state of this entry. + /// + /// The new state for this entry. + private void ChangeState(BuildRequestEntryState newState) + { + if (_state != newState) + { + _state = newState; + + BuildRequestEntryStateChangedDelegate stateEvent = OnStateChanged; + + if (stateEvent != null) + { + stateEvent(this, newState); + } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs new file mode 100644 index 00000000000..ab56f2f66be --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/FullyQualifiedBuildRequest.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Build request which has not had its configuration resolved yet. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class represents a build request as it would be received from an MSBuild callback. Such requests have + /// configurations which have not yet been assigned a global configuration ID, and therefore must be resolved + /// with the build manager before a formal request can be sent. + /// + /// + /// This class is called "Fully Qualified" because it completely and directly specifies all of the configuration information. + /// A standard Build Request only specifies the configuration id, so to get the configuration requires an additional lookup + /// in a configuration cache. + /// + internal class FullyQualifiedBuildRequest + { + /// + /// The request's configuration. + /// + private BuildRequestConfiguration _requestConfiguration; + + /// + /// The set of targets to build. + /// + private string[] _targets; + + /// + /// Whether or not we need to wait for results before completing this request. + /// + private bool _resultsNeeded; + + /// + /// Initializes a build request. + /// + /// The configuration to use for the request. + /// The set of targets to build. + /// Whether or not to wait for the results of this request. + public FullyQualifiedBuildRequest(BuildRequestConfiguration config, string[] targets, bool resultsNeeded) + { + ErrorUtilities.VerifyThrowArgumentNull(config, "config"); + ErrorUtilities.VerifyThrowArgumentNull(targets, "targets"); + + _requestConfiguration = config; + _targets = targets; + _resultsNeeded = resultsNeeded; + } + + /// + /// Returns the configuration for this request. + /// + public BuildRequestConfiguration Config + { + get + { + return _requestConfiguration; + } + } + + /// + /// Returns the set of targets to be satisfied for this request. + /// + public string[] Targets + { + get + { + return _targets; + } + } + + /// + /// Returns true if this request must wait for its results in order to complete. + /// + public bool ResultsNeeded + { + get + { + return _resultsNeeded; + } + } + + /// + /// Implementation of the equality operator. + /// + /// The left hand argument + /// The right hand argument + /// True if the objects are equivalent, false otherwise. + public static bool operator ==(FullyQualifiedBuildRequest left, FullyQualifiedBuildRequest right) + { + if (Object.ReferenceEquals(left, null)) + { + if (Object.ReferenceEquals(right, null)) + { + return true; + } + else + { + return false; + } + } + else + { + if (Object.ReferenceEquals(right, null)) + { + return false; + } + else + { + return left.InternalEquals(right); + } + } + } + + /// + /// Implementation of the inequality operator. + /// + /// The left-hand argument + /// The right-hand argument + /// True if the objects are not equivalent, false otherwise. + public static bool operator !=(FullyQualifiedBuildRequest left, FullyQualifiedBuildRequest right) + { + return !(left == right); + } + + /// + /// Returns the hashcode for this object. + /// + /// The hashcode + public override int GetHashCode() + { + return _requestConfiguration.GetHashCode(); + } + + /// + /// Determines equivalence between this object and another. + /// + /// The object to which this one should be compared. + /// True if the objects are equivalent, false otherwise. + public override bool Equals(object obj) + { + if (null == obj) + { + return false; + } + + if (this.GetType() != obj.GetType()) + { + return false; + } + + return InternalEquals((FullyQualifiedBuildRequest)obj); + } + + /// + /// Determines equivalence with another object of the same type. + /// + /// The other object with which to compare this one. + /// True if the objects are equivalent, false otherwise. + private bool InternalEquals(FullyQualifiedBuildRequest other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (_requestConfiguration != other._requestConfiguration) + { + return false; + } + + if (_resultsNeeded != other._resultsNeeded) + { + return false; + } + + if (_targets.Length != other._targets.Length) + { + return false; + } + + for (int i = 0; i < _targets.Length; ++i) + { + if (_targets[i] != other._targets[i]) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/IBuildRequestEngine.cs b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/IBuildRequestEngine.cs new file mode 100644 index 00000000000..9a78d2d86cf --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/BuildRequestEngine/IBuildRequestEngine.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the build request engine. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; +using BuildResult = Microsoft.Build.Execution.BuildResult; + +namespace Microsoft.Build.BackEnd +{ + #region Delegates + /// + /// Callback for event raised when a build request is completed + /// + /// The request which completed + /// The result for the request + internal delegate void RequestCompleteDelegate(BuildRequest request, BuildResult result); + + /// + /// Callback for event raised when a request is resumed + /// + /// The request being resumed + internal delegate void RequestResumedDelegate(BuildRequest request); + + /// + /// Callback for event raised when a new build request is generated by an MSBuild callback + /// + /// Information about what is blocking the engine. + internal delegate void RequestBlockedDelegate(BuildRequestBlocker blocker); + + /// + /// Callback for event raised when the build request engine's status changes. + /// + /// The new status for the engine + internal delegate void EngineStatusChangedDelegate(BuildRequestEngineStatus newStatus); + + /// + /// Callback for event raised when a new configuration needs an ID resolved. + /// + /// The configuration needing an ID + internal delegate void NewConfigurationRequestDelegate(BuildRequestConfiguration config); + + /// + /// Callback for event raised when there is an unhandled exception in the engine. + /// + /// The exception. + internal delegate void EngineExceptionDelegate(Exception e); + + #endregion + + /// + /// Status types for the build request engine + /// + internal enum BuildRequestEngineStatus + { + /// + /// The engine has not yet been initialized, and cannot accept requests. + /// + Uninitialized, + + /// + /// The engine has no active or waiting build requests. + /// + Idle, + + /// + /// The engine is presently working on a build request. + /// + Active, + + /// + /// The engine has only build requests which are waiting for build results to continue. + /// + Waiting, + + /// + /// The engine has shut down. + /// + Shutdown + } + + /// + /// Objects implementing this interface may be used by a Node to process build requests + /// and generate build results. + /// + internal interface IBuildRequestEngine + { + #region Events + /// + /// Raised when a build request is completed and results are available. + /// + event RequestCompleteDelegate OnRequestComplete; + + /// + /// Raised when a build request is resumed from a previously waiting state. + /// + event RequestResumedDelegate OnRequestResumed; + + /// + /// Raised when a new build request is generated by an MSBuild callback. + /// + event RequestBlockedDelegate OnRequestBlocked; + + /// + /// Raised when the engine status changes. + /// + event EngineStatusChangedDelegate OnStatusChanged; + + /// + /// Raised when a configuration needs an id. + /// + event NewConfigurationRequestDelegate OnNewConfigurationRequest; + + /// + /// Raised when an unhandled exception occurs in the engine. + /// + event EngineExceptionDelegate OnEngineException; + + #endregion + + #region Properties + /// + /// Gets the current engine status. + /// + BuildRequestEngineStatus Status + { + get; + } + #endregion + + #region Methods + /// + /// Prepares the engine for a new build and spins up the engine thread. + /// The engine must be in the Idle state, and not already be initialized. + /// + /// The logging context for the node. + void InitializeForBuild(NodeLoggingContext loggingContext); + + /// + /// Cleans up after a build but leaves the engine thread running. Aborts + /// any outstanding requests. Blocks until the engine has cleaned up + /// everything. After this method is called, InitializeForBuild may be + /// called to start a new build, or the component may be shut down. + /// + void CleanupForBuild(); + + /// + /// Submits the specified request to the build queue. + /// + /// The request to build. + /// It is only valid to call this method when the engine is in the Idle or + /// Waiting state because the engine can only service one active request at a time. + void SubmitBuildRequest(BuildRequest request); + + /// + /// Notifies the engine of a build result for a waiting build request. + /// + /// The unblocking information + void UnblockBuildRequest(BuildRequestUnblocker unblocker); + + /// + /// Notifies the engine of a configuration response packet, typically generated by the Build Request Manager. This packet is used to set + /// the global configuration ID for a specific configuration. + /// + /// The build configuration response. + void ReportConfigurationResponse(BuildRequestConfigurationResponse response); + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/ConfigCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/ConfigCache.cs new file mode 100644 index 00000000000..fb861ae0b96 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/ConfigCache.cs @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of IConfigCache. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Implements a build request configuration cache. + /// + internal class ConfigCache : IConfigCache + { + /// + /// The configurations + /// + private Dictionary _configurations; + + /// + /// Lookup which can be used to find a configuration with the specified metadata. + /// + private Dictionary _configurationIdsByMetadata; + + /// + /// The maximum cache entries allowed before a sweep can occur. + /// + private int _sweepThreshhold; + + /// + /// Creates a new build configuration cache. + /// + public ConfigCache() + { + _configurations = new Dictionary(); + _configurationIdsByMetadata = new Dictionary(); + if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDCONFIGCACHESWEEPTHRESHHOLD"), out _sweepThreshhold)) + { + _sweepThreshhold = 500; + } + } + + /// + /// Returns the configuration cached under the specified configuration id. + /// + /// The id of the configuration to return. + /// The cached configuration. + /// Returned if a configuration with the specified id is not in the cache. + public BuildRequestConfiguration this[int configId] + { + get + { + lock (_configurations) + { + return _configurations[configId]; + } + } + } + + #region IConfigCache Members + + /// + /// Adds the specified configuration to the cache. + /// + /// The configuration to add. + public void AddConfiguration(BuildRequestConfiguration config) + { + ErrorUtilities.VerifyThrowArgumentNull(config, "config"); + ErrorUtilities.VerifyThrow(config.ConfigurationId != 0, "Invalid configuration ID"); + + lock (_configurations) + { + int configId = GetKeyForConfiguration(config); + ErrorUtilities.VerifyThrow(!_configurations.ContainsKey(configId), "Configuration {0} already cached", config.ConfigurationId); + _configurations.Add(configId, config); + _configurationIdsByMetadata.Add(new ConfigurationMetadata(config), configId); + } + } + + /// + /// Removes the specified configuration from the cache. + /// + /// The id of the configuration to remove. + public void RemoveConfiguration(int configId) + { + lock (_configurations) + { + BuildRequestConfiguration config = _configurations[configId]; + _configurations.Remove(configId); + _configurationIdsByMetadata.Remove(new ConfigurationMetadata(config)); + config.ClearCacheFile(); + } + } + + /// + /// Returns the entry in the cache which matches the specified config. + /// + /// The configuration to match + /// A matching configuration if one exists, null otherwise. + public BuildRequestConfiguration GetMatchingConfiguration(BuildRequestConfiguration config) + { + ErrorUtilities.VerifyThrowArgumentNull(config, "config"); + return GetMatchingConfiguration(new ConfigurationMetadata(config)); + } + + /// + /// Returns the entry in the cache which matches the specified config. + /// + /// The configuration metadata to match + /// A matching configuration if one exists, null otherwise. + public BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata configMetadata) + { + ErrorUtilities.VerifyThrowArgumentNull(configMetadata, "configMetadata"); + lock (_configurations) + { + int configId; + if (!_configurationIdsByMetadata.TryGetValue(configMetadata, out configId)) + { + return null; + } + + return _configurations[configId]; + } + } + + /// + /// Gets a matching configuration. If no such configuration exists, one is created and optionally loaded. + /// + public BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata configMetadata, ConfigCreateCallback callback, bool loadProject) + { + lock (_configurations) + { + BuildRequestConfiguration configuration = GetMatchingConfiguration(configMetadata); + + // If there is no matching configuration, let the caller create one. + if (configuration == null) + { + configuration = callback(null, loadProject); + AddConfiguration(configuration); + } + else if (loadProject) + { + // We already had a configuration, load the project + // If it exists but it cached, retrieve it + if (configuration.IsCached) + { + configuration.RetrieveFromCache(); + } + + // If it is still not loaded (because no instance was ever created here), let the caller populate the instance. + if (!configuration.IsLoaded) + { + callback(configuration, loadProject: true); + } + } + + // In either case, make sure the project is loaded if it was requested. + if (loadProject) + { + ErrorUtilities.VerifyThrow(configuration.IsLoaded, "Request to create configuration did not honor request to also load project."); + } + + return configuration; + } + } + + /// + /// Returns true if the cache contains a configuration with the specified id, false otherwise. + /// + /// The configuration id to check. + /// True if the cache contains a configuration with this id, false otherwise. + public bool HasConfiguration(int configId) + { + lock (_configurations) + { + return _configurations.ContainsKey(configId); + } + } + + /// + /// Clear all configurations + /// + public void ClearConfigurations() + { + if (_configurations != null) + { + foreach (var config in _configurations.Values) + { + config.ClearCacheFile(); + } + } + + _configurations = new Dictionary(); + _configurationIdsByMetadata = new Dictionary(); + } + + /// + /// Clears configurations from the configuration cache which have not been explicitly loaded. + /// + /// Set if configurations which have been cleared. + public List ClearNonExplicitlyLoadedConfigurations() + { + List configurationIdsCleared = null; + configurationIdsCleared = new List(); + + Dictionary configurationsToKeep = new Dictionary(); + Dictionary configurationIdsByMetadataToKeep = new Dictionary(); + + foreach (KeyValuePair metadata in _configurationIdsByMetadata) + { + BuildRequestConfiguration configuration; + int configId = metadata.Value; + + if (_configurations.TryGetValue(configId, out configuration)) + { + // We do not want to retain this configuration + if (!configuration.ExplicitlyLoaded) + { + configurationIdsCleared.Add(configId); + configuration.ClearCacheFile(); + continue; + } + + configurationsToKeep.Add(configId, configuration); + configurationIdsByMetadataToKeep.Add(metadata.Key, metadata.Value); + } + } + + _configurations = configurationsToKeep; + _configurationIdsByMetadata = configurationIdsByMetadataToKeep; + + return configurationIdsCleared; + } + + /// + /// Check whether the config cache has more items that the predefined threshold + /// + public bool IsConfigCacheSizeLargerThanThreshold() + { + return _configurations.Count > _sweepThreshhold; + } + + /// + /// Writes out as many configurations to disk as we can, under the assumption that inactive configurations + /// probably aren't going to be accessed again (the exception is re-entrant builds) and we want to make as much + /// space as possible now for future projects to load. + /// + /// True if any configurations were cached, false otherwise. + public bool WriteConfigurationsToDisk() + { + lock (_configurations) + { + bool cachedAtLeastOneProject = false; + + // Cache 10% of configurations to release some memory + int remainingToRelease = _configurations.Count; + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDENABLEAGGRESSIVECACHING"))) + { + // Cache only 10% of configurations to release some memory + remainingToRelease = Convert.ToInt32(Math.Max(1, Math.Floor(_configurations.Count * 0.1))); + } + + foreach (BuildRequestConfiguration configuration in _configurations.Values) + { + if (!configuration.IsCached) + { + configuration.CacheIfPossible(); + + if (configuration.IsCached) + { + cachedAtLeastOneProject = true; + + remainingToRelease--; + + if (remainingToRelease == 0) + { + break; + } + } + } + } + + return cachedAtLeastOneProject; + } + } + + #endregion + + #region IEnumerable Members + + /// + /// Gets the enumerator over the configurations in the cache. + /// + public IEnumerator GetEnumerator() + { + return _configurations.Values.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + /// + /// Gets the enumerator over the configurations in the cache. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _configurations.Values.GetEnumerator(); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host. + /// + /// The build component host. + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + } + + /// + /// Shuts down this component + /// + public void ShutdownComponent() + { + lock (_configurations) + { + _configurations.Clear(); + } + } + + #endregion + + /// + /// Factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType componentType) + { + ErrorUtilities.VerifyThrow(componentType == BuildComponentType.ConfigCache, "Cannot create components of type {0}", componentType); + return new ConfigCache(); + } + + /// + /// Override which determines the key for entry into the collection from the specified build request configuration. + /// + /// The build request configuration. + /// The configuration id. + protected int GetKeyForConfiguration(BuildRequestConfiguration config) + { + return config.ConfigurationId; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/IConfigCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/IConfigCache.cs new file mode 100644 index 00000000000..391eea2e78e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/IConfigCache.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface representing a configuration cache. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Delegate invoked to create a configuration + /// + internal delegate BuildRequestConfiguration ConfigCreateCallback(BuildRequestConfiguration existingConfiguration, bool loadProject); + + /// + /// An interfacing representing a build request configuration cache. + /// + internal interface IConfigCache : IBuildComponent, IEnumerable + { + /// + /// Returns the configuration with the specified id. + /// + /// The configuration id. + /// The configuration with the specified id. + BuildRequestConfiguration this[int configId] + { + get; + } + + /// + /// Adds the configuration to the cache. + /// + /// The configuration to add. + void AddConfiguration(BuildRequestConfiguration config); + + /// + /// Removes the specified configuration from the cache. + /// + /// The id of the configuration to remove. + void RemoveConfiguration(int configId); + + /// + /// Gets the cached configuration which matches the specified configuration + /// + /// The configuration to match. + /// The matching configuration if any, null otherwise. + BuildRequestConfiguration GetMatchingConfiguration(BuildRequestConfiguration config); + + /// + /// Gets the cached configuration which matches the specified configuration + /// + /// The configuration metadata to match. + /// The matching configuration if any, null otherwise. + BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata configMetadata); + + /// + /// Gets a matching configuration. If no such configration exists, one is created and optionally loaded. + /// + /// The configuration metadata to match. + /// Callback to be invoked if the configuration does not exist. + /// True if the configuration should also be loaded. + /// The matching configuration if any, null otherwise. + BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata configMetadata, ConfigCreateCallback callback, bool loadProject); + + /// + /// Returns true if a configuration with the specified id exists in the cache. + /// + /// The configuration id to check. + /// + /// True if there is a configuration with the specified id, false otherwise. + /// + bool HasConfiguration(int configId); + + /// + /// Clears out the configurations + /// + void ClearConfigurations(); + + /// + /// Clear non explicltly loaded configurations. + /// + /// The configuration ids which have been cleared. + List ClearNonExplicitlyLoadedConfigurations(); + + /// + /// Check whether the config cache has more items that the predefined threshold + /// + bool IsConfigCacheSizeLargerThanThreshold(); + + /// + /// Unloads any configurations not in use. + /// + /// True if any configurations were cached, false otherwise. + bool WriteConfigurationsToDisk(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/IPropertyCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/IPropertyCache.cs new file mode 100644 index 00000000000..2faabcbd2a7 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/IPropertyCache.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for a property cache. +//-----------------------------------------------------------------------using System; + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Represents a cache for build properties + /// + internal interface IPropertyCache + { + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/IRegisteredTaskObjectCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/IRegisteredTaskObjectCache.cs new file mode 100644 index 00000000000..08844ebcc13 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/IRegisteredTaskObjectCache.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Defines a cache for registered task objects. +//----------------------------------------------------------------------- + +using Microsoft.Build.Framework; + +#if BUILD_ENGINE +namespace Microsoft.Build.BackEnd.Components.Caching +#else +namespace Microsoft.Build.Shared +#endif +{ + /// + /// Defines a cache for registered task objects. + /// + internal interface IRegisteredTaskObjectCache + { + /// + /// Disposes of all of the objects with the specified lifetime. + /// + void DisposeCacheObjects(RegisteredTaskObjectLifetime lifetime); + + /// + /// Registers a task object with the specified key and lifetime. + /// + void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection); + + /// + /// Gets a previously registered task object. + /// + object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime); + + /// + /// Unregisters a task object. + /// + object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/IResultsCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/IResultsCache.cs new file mode 100644 index 00000000000..4ef89426764 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/IResultsCache.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for a results cache. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using BuildResult = Microsoft.Build.Execution.BuildResult; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This interface represents an object which holds build results. + /// + internal interface IResultsCache : IBuildComponent + { + /// + /// Adds a result to the cache + /// + /// The result to add. + void AddResult(BuildResult result); + + /// + /// Deletes all results from the cache for the specified build. + /// + void ClearResults(); + + /// + /// Retrieves a BuildResult for the specified matching BuildRequest. + /// + /// The request for which the result should be returned. + /// A BuildResult if there is a matching one in the cache, otherwise null. + BuildResult GetResultForRequest(BuildRequest request); + + /// + /// Retrieves a BuildResult for the specified configuration. + /// + /// The configuration for which results should be returned. + /// A BuildResult if there is a matching one in the cache, otherwise null. + BuildResult GetResultsForConfiguration(int configurationId); + + /// + /// Attempts to satisfy the request from the cache. The request can be satisfied only if: + /// 1. All specified targets in the request have non-skipped results in the cache. + /// 2. All initial targets in the configuration for the request have non-skipped results in the cache. + /// 3. If there are no specified targets, then all default targets in the request must have non-skipped results + /// in the cache. + /// + /// The request whose results we should return + /// The initial targets for the request's configuration. + /// The default targets for the request's configuration. + /// Any additional targets that need to be checked to determine overall + /// pass or failure, but that are not included as actual results. (E.g. AfterTargets of an entrypoint target) + /// If false, a cached skipped target will cause this method to return "NotSatisfied". + /// If true, then as long as there is a result in the cache (regardless of whether it was skipped or not), this method + /// will return "Satisfied". In most cases this should be false, but it may be set to true in a situation where there is no + /// chance of re-execution (which is the usual response to missing / skipped targets), and the caller just needs the data. + /// A response indicating the results, if any, and the targets needing to be built, if any. + ResultsCacheResponse SatisfyRequest(BuildRequest request, List configInitialTargets, List configDefaultTargets, List additionalTargetsToCheckForOverallResult, bool skippedResultsAreOK); + + /// + /// Clears the results for a specific configuration. + /// + /// The configuration id. + void ClearResultsForConfiguration(int configurationId); + + /// + /// Caches results to disk if possible. + /// + void WriteResultsToDisk(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs new file mode 100644 index 00000000000..4fefae06cf2 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/RegisteredTaskObjectCache.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implements a cache for registered task objects. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Components.Caching +{ + /// + /// This is a cache of objects which are registered to be disposed of at a specified time. + /// + internal class RegisteredTaskObjectCache : RegisteredTaskObjectCacheBase, IBuildComponent, IRegisteredTaskObjectCache, IDisposable + { + /// + /// Finalizer + /// + ~RegisteredTaskObjectCache() + { + Dispose(disposing: false); + } + + #region IBuildComponent + + /// + /// Initialize the build component. + /// + public void InitializeComponent(IBuildComponentHost host) + { + } + + /// + /// Shuts down the build component. + /// + public void ShutdownComponent() + { + ErrorUtilities.VerifyThrow(IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime.Build), "Build lifetime objects were not disposed at the end of the build"); + } + + #endregion + + #region IDisposable + + /// + /// Implementation of Dispose pattern. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Component factory. + /// + internal static IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.RegisteredTaskObjectCache, "Cannot create components of type {0}", type); + return new RegisteredTaskObjectCache(); + } + + /// + /// Implementation of Dispose pattern. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + ErrorUtilities.VerifyThrow(IsCollectionEmptyOrUncreated(RegisteredTaskObjectLifetime.Build), "Build lifetime objects were not disposed at the end of the build"); + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCache.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCache.cs new file mode 100644 index 00000000000..b8869d75b83 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCache.cs @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of the results cache. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Implementation of the results cache. + /// + internal class ResultsCache : IResultsCache + { + #region Private Data + /// + /// The table of all build results. This table is indexed by configuration id and + /// contains BuildResult objects which have all of the target information. + /// + private ConcurrentDictionary _resultsByConfiguration; + + /// + /// The component host. + /// + private IBuildComponentHost _componentHost; + + #endregion + + /// + /// Creates an empty results cache. + /// + public ResultsCache() + { + _resultsByConfiguration = new ConcurrentDictionary(); + } + + /// + /// Enum for CheckResults helper function. + /// + private enum TargetClass + { + /// + /// Targets explicitly specified in the build request. + /// + Explicit, + + /// + /// Targets which are declared as initial targets. + /// + Initial, + + /// + /// Targets which are the default when no explicit targets are specified. + /// + Default, + } + + /// + /// Returns the internal cache for testing purposes. + /// + internal IDictionary ResultsDictionary + { + get + { + return _resultsByConfiguration; + } + } + + #region IResultsCache Members + + /// + /// Adds the specified build result to the cache + /// + /// The result to add. + public void AddResult(BuildResult result) + { + lock (_resultsByConfiguration) + { + if (_resultsByConfiguration.ContainsKey(result.ConfigurationId)) + { + if (Object.ReferenceEquals(_resultsByConfiguration[result.ConfigurationId], result)) + { + // Merging results would be meaningless as we would be merging the object with itself. + return; + } + + _resultsByConfiguration[result.ConfigurationId].MergeResults(result); + } + else + { + // Note that we are not making a copy here. This is by-design. The TargetBuilder uses this behavior + // to ensure that re-entering a project will be able to see all previously built targets and avoid + // building them again. + if (!_resultsByConfiguration.TryAdd(result.ConfigurationId, result)) + { + ErrorUtilities.ThrowInternalError("Failed to add result for configuration {0}", result.ConfigurationId); + } + } + } + } + + /// + /// Clears the results for the specified build. + /// + public void ClearResults() + { + lock (_resultsByConfiguration) + { + foreach (KeyValuePair result in _resultsByConfiguration) + { + result.Value.ClearCachedFiles(); + } + + _resultsByConfiguration.Clear(); + } + } + + /// + /// Retrieves the results for the specified build request. + /// + /// The request for which results should be retrieved. + /// The build results for the specified request. + public BuildResult GetResultForRequest(BuildRequest request) + { + ErrorUtilities.VerifyThrowArgument(request.IsConfigurationResolved, "UnresolvedConfigurationInRequest"); + + lock (_resultsByConfiguration) + { + if (_resultsByConfiguration.ContainsKey(request.ConfigurationId)) + { + BuildResult result = _resultsByConfiguration[request.ConfigurationId]; + foreach (string target in request.Targets) + { + ErrorUtilities.VerifyThrow(result.HasResultsForTarget(target), "No results in cache for target " + target); + } + + return result; + } + } + + return null; + } + + /// + /// Retrieves the results for the specified configuration + /// + /// The configuration for which results should be returned. + /// The results, if any + public BuildResult GetResultsForConfiguration(int configurationId) + { + BuildResult results; + lock (_resultsByConfiguration) + { + _resultsByConfiguration.TryGetValue(configurationId, out results); + } + + return results; + } + + /// + /// Attempts to satisfy the request from the cache. The request can be satisfied only if: + /// 1. All specified targets in the request have successful results in the cache or if the sequence of target results + /// includes 0 or more successful targets followed by at least one failed target. + /// 2. All initial targets in the configuration for the request have non-skipped results in the cache. + /// 3. If there are no specified targets, then all default targets in the request must have non-skipped results + /// in the cache. + /// + /// The request whose results we should return + /// The initial targets for the request's configuration. + /// The default targets for the request's configuration. + /// Any additional targets that need to be checked to determine overall + /// pass or failure, but that are not included as actual results. (E.g. AfterTargets of an entrypoint target) + /// If false, a cached skipped target will cause this method to return "NotSatisfied". + /// If true, then as long as there is a result in the cache (regardless of whether it was skipped or not), this method + /// will return "Satisfied". In most cases this should be false, but it may be set to true in a situation where there is no + /// chance of re-execution (which is the usual response to missing / skipped targets), and the caller just needs the data. + /// A response indicating the results, if any, and the targets needing to be built, if any. + public ResultsCacheResponse SatisfyRequest(BuildRequest request, List configInitialTargets, List configDefaultTargets, List additionalTargetsToCheckForOverallResult, bool skippedResultsAreOK) + { + ErrorUtilities.VerifyThrowArgument(request.IsConfigurationResolved, "UnresolvedConfigurationInRequest"); + ResultsCacheResponse response = new ResultsCacheResponse(ResultsCacheResponseType.NotSatisfied); + + lock (_resultsByConfiguration) + { + if (_resultsByConfiguration.ContainsKey(request.ConfigurationId)) + { + BuildResult allResults = _resultsByConfiguration[request.ConfigurationId]; + + // Check for targets explicitly specified. + bool explicitTargetsSatisfied = CheckResults(allResults, request.Targets, TargetClass.Explicit, response.ExplicitTargetsToBuild, skippedResultsAreOK); + + if (explicitTargetsSatisfied) + { + // All of the explicit targets, if any, have been satisfied + response.Type = ResultsCacheResponseType.Satisfied; + + // Check for the initial targets. If we don't know what the initial targets are, we assume they are not satisfied. + if (configInitialTargets == null || !CheckResults(allResults, configInitialTargets, TargetClass.Initial, null, skippedResultsAreOK)) + { + response.Type = ResultsCacheResponseType.NotSatisfied; + } + + // We could still be missing implicit targets, so check those... + if (request.Targets.Count == 0) + { + // Check for the default target, if necessary. If we don't know what the default targets are, we + // assume they are not satisfied. + if (configDefaultTargets == null || !CheckResults(allResults, configDefaultTargets, TargetClass.Default, null, skippedResultsAreOK)) + { + response.Type = ResultsCacheResponseType.NotSatisfied; + } + } + + // Now report those results requested, if they are satisfied. + if (response.Type == ResultsCacheResponseType.Satisfied) + { + List targetsToAddResultsFor = new List(configInitialTargets); + + // Now report either the explicit targets or the default targets + if (request.Targets.Count > 0) + { + targetsToAddResultsFor.AddRange(request.Targets); + } + else + { + targetsToAddResultsFor.AddRange(configDefaultTargets); + } + + response.Results = new BuildResult(request, allResults, targetsToAddResultsFor.ToArray(), additionalTargetsToCheckForOverallResult, null); + } + } + else + { + // Some targets were not satisfied. + response.Type = ResultsCacheResponseType.NotSatisfied; + } + } + } + + return response; + } + + /// + /// Removes the results for a particular configuration. + /// + /// The configuration + public void ClearResultsForConfiguration(int configurationId) + { + lock (_resultsByConfiguration) + { + BuildResult removedResult; + _resultsByConfiguration.TryRemove(configurationId, out removedResult); + + if (removedResult != null) + { + removedResult.ClearCachedFiles(); + } + } + } + + /// + /// Cache as many results as we can. + /// + public void WriteResultsToDisk() + { + lock (_resultsByConfiguration) + { + foreach (BuildResult resultToCache in _resultsByConfiguration.Values) + { + resultToCache.CacheIfPossible(); + } + } + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the build component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + _componentHost = host; + } + + /// + /// Shuts down this component + /// + public void ShutdownComponent() + { + _resultsByConfiguration.Clear(); + } + + #endregion + + /// + /// Factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType componentType) + { + ErrorUtilities.VerifyThrow(componentType == BuildComponentType.ResultsCache, "Cannot create components of type {0}", componentType); + return new ResultsCache(); + } + + /// + /// Looks for results for the specified targets. + /// + /// The result to examine + /// The targets to search for + /// The class of targets + /// An optional list to be populated with missing targets + /// If true, a status of "skipped" counts as having valid results + /// for that target. Otherwise, a skipped target is treated as equivalent to a missing target. + /// False if there were missing results, true otherwise. + private bool CheckResults(BuildResult result, List targets, TargetClass targetClass, HashSet targetsMissingResults, bool skippedResultsAreOK) + { + bool returnValue = true; + foreach (string target in targets) + { + if (!result.HasResultsForTarget(target) || (result[target].ResultCode == TargetResultCode.Skipped && !skippedResultsAreOK)) + { + if (null != targetsMissingResults) + { + targetsMissingResults.Add(target); + returnValue = false; + } + else + { + return false; + } + } + else + { + // If the result was a failure and we have not seen any skipped targets up to this point, then we conclude we do + // have results for this request, and they indicate failure. + if (result[target].ResultCode == TargetResultCode.Failure && (targetsMissingResults == null || targetsMissingResults.Count == 0)) + { + return true; + } + } + } + + return returnValue; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCacheResponse.cs b/src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCacheResponse.cs new file mode 100644 index 00000000000..d6d0284408e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Caching/ResultsCacheResponse.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Struct which contains the response from a cache satisfaction request. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using BuildResult = Microsoft.Build.Execution.BuildResult; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The type of response. + /// + internal enum ResultsCacheResponseType + { + /// + /// There were no matching results, or some implicit targets need to be built. + /// + NotSatisfied, + + /// + /// All explicit and implicit targets have results. + /// + Satisfied + } + + /// + /// Container for results of IResultsCache.SatisfyRequest + /// + internal struct ResultsCacheResponse + { + /// + /// The results type. + /// + public ResultsCacheResponseType Type; + + /// + /// The actual results, if the request was satisfied. + /// + public BuildResult Results; + + /// + /// The subset of explicit targets which must be built because there are no results for them in the cache. + /// + public HashSet ExplicitTargetsToBuild; + + /// + /// Constructor. + /// + /// The response type. + public ResultsCacheResponse(ResultsCacheResponseType type) + { + Type = type; + Results = null; + ExplicitTargetsToBuild = new HashSet(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/INodeManager.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/INodeManager.cs new file mode 100644 index 00000000000..0a242e414c2 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/INodeManager.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the node manager. +//-----------------------------------------------------------------------using System; + +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Represents a collection of all node providers in the system. Reports events concerning + /// the topology of the system and provides a means to send and receive data to nodes. + /// + internal interface INodeManager : IBuildComponent, + INodePacketFactory + { + #region Methods + + /// + /// Requests that a new node be created. + /// + /// The configuration to use to create the node. + /// Information about the node created + /// + /// Throws an exception if the node could not be created. + /// + NodeInfo CreateNode(NodeConfiguration configuration, NodeAffinity affinity); + + /// + /// Sends a data packet to a specific node + /// + /// The node to which the data packet should be sent. + /// The packet to send. + void SendData(int node, INodePacket packet); + + /// + /// Shuts down all of the managed nodes. This is an asynchronous method - the nodes are + /// not considered shut down until a NodeShutdown packet has been received. + /// + /// Flag indicating if nodes should prepare for reuse. + void ShutdownConnectedNodes(bool enableReuse); + + /// + /// Shuts down all of the managed nodes permanently. This is an asynchronous method - the nodes are + /// not considered shut down until a NodeShutdown packet has been received. + /// + void ShutdownAllNodes(); + + /// + /// The node manager contains state which is not supposed to persist between builds, make sure this is cleared. + /// + void ClearPerBuildState(); + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/INodeProvider.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/INodeProvider.cs new file mode 100644 index 00000000000..f5b2f7e6b1a --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/INodeProvider.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for node providers. +//-----------------------------------------------------------------------using System; + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The type of nodes provided by the node provider. + /// + internal enum NodeProviderType + { + /// + /// The provider provides the in-proc node. + /// + InProc, + + /// + /// The provider provides out-of-proc nodes. + /// + OutOfProc, + + /// + /// The provider provides remote nodes. + /// + Remote + } + + /// + /// This interface represents a collection of nodes in the system. It provides methods to + /// enumerate active nodes as well as send data and receive events from those nodes. + /// + internal interface INodeProvider : IBuildComponent + { + #region Properties + + /// + /// The type of nodes provided by this node provider. + /// + NodeProviderType ProviderType + { + get; + } + + /// + /// The number of nodes this provider can create. + /// + int AvailableNodes + { + get; + } + + #endregion + + #region Methods + + /// + /// Requests that a new node be created on the specified machine. + /// + /// The id to assign to the node. + /// + /// The packet factory used to create packets when data is + /// received on this node. + /// + /// The configuration to use to create the remote node. + /// True if the node was created, false otherwise. + bool CreateNode(int nodeId, INodePacketFactory packetFactory, NodeConfiguration configuration); + + /// + /// Sends data to a specific node. + /// + /// The node to which data should be sent. + /// The packet to be sent. + void SendData(int node, INodePacket packet); + + /// + /// Shuts down all of the connected, managed nodes. This call will not return until all nodes are shut down. + /// + /// Flag indicating if nodes should prepare for reuse. + void ShutdownConnectedNodes(bool enableReuse); + + /// + /// Shuts down all of the managed nodes. This call will not return until all nodes are shut down. + /// + void ShutdownAllNodes(); + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/LogMessagePacket.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/LogMessagePacket.cs new file mode 100644 index 00000000000..d87faf474a0 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/LogMessagePacket.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// NodePackets which are used for node communication +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using TaskEngineAssemblyResolver = Microsoft.Build.BackEnd.Logging.TaskEngineAssemblyResolver; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A packet to encapsulate a BuildEventArg logging message. + /// Contents: + /// Build Event Type + /// Build Event Args + /// + internal class LogMessagePacket : LogMessagePacketBase + { + /// + /// Encapsulates the buildEventArg in this packet. + /// + internal LogMessagePacket(KeyValuePair? nodeBuildEvent) + : base(nodeBuildEvent, new TargetFinishedTranslator(TranslateTargetFinishedEvent)) + { + } + + /// + /// Constructor for deserialization + /// + private LogMessagePacket(INodePacketTranslator translator) + : base(translator) + { + } + + /// + /// Factory for serialization + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new LogMessagePacket(translator); + } + + /// + /// Translate the TargetOutputs for the target finished event. + /// + private static void TranslateTargetFinishedEvent(INodePacketTranslator translator, TargetFinishedEventArgs finishedEvent) + { + List targetOutputs = null; + if (translator.Mode == TranslationDirection.WriteToStream) + { + if (finishedEvent.TargetOutputs != null) + { + targetOutputs = new List(); + foreach (TaskItem item in finishedEvent.TargetOutputs) + { + targetOutputs.Add(item); + } + } + } + + translator.Translate(ref targetOutputs, TaskItem.FactoryForDeserialization); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + finishedEvent.TargetOutputs = targetOutputs; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointInProc.cs new file mode 100644 index 00000000000..ed483862274 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of a node endpoint for in-proc nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Shared; + +using BuildParameters = Microsoft.Build.Execution.BuildParameters; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This is an implementation of INodeEndpoint for in-proc nodes. This endpoint can use either + /// synchronous or asynchronous packet processing methods. When synchronous processing methods are + /// used, the SendData method will cause the OnDataReceived event on the receiving endpoint to be called + /// on the same thread, blocking until the handler returns. The asynchronous method more closely emulates + /// the way other kinds of endpoints work, as the recipient processes the packet on a different thread + /// than that from which the packet originated, but with the cost of the extra thread. + /// + internal class NodeEndpointInProc : INodeEndpoint + { + #region Private Data + /// + /// An object for the two inproc endpoints to synchronize on. + /// + private static Object s_locker = new Object(); + + /// + /// The current communication status of the node. + /// + private LinkStatus _status; + + /// + /// The communications mode + /// + private EndpointMode _mode; + + /// + /// The peer endpoint + /// + private NodeEndpointInProc _peerEndpoint; + + /// + /// The build component host + /// + private IBuildComponentHost _componentHost; + + /// + /// The packet factory used to route packets. + /// + private INodePacketFactory _packetFactory; + + // The following private data fields are used only when the endpoint is in ASYNCHRONOUS mode. + + /// + /// Object used as a lock source for the async data + /// + private object _asyncDataMonitor; + + /// + /// Set when a packet is available in the packet queue + /// + private AutoResetEvent _packetAvailable; + + /// + /// Set when the asynchronous packet pump should terminate + /// + private AutoResetEvent _terminatePacketPump; + + /// + /// The thread which runs the asynchronous packet pump + /// + private Thread _packetPump; + + /// + /// Set to true if our peer is connected to us. + /// + private bool _peerConnected; + + /// + /// The asynchronous packet queue. + /// + /// + /// Operations on this queue must be synchronized since it is accessible by multiple threads. + /// Use a lock on the packetQueue itself. + /// + private Queue _packetQueue; + #endregion + + #region Constructors and Factories + /// + /// Instantiates a Node and initializes it to unconnected. + /// + /// The communications mode for this endpoint. + /// The component host. + private NodeEndpointInProc(EndpointMode commMode, IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + + _status = LinkStatus.Inactive; + _mode = commMode; + _componentHost = host; + + if (commMode == EndpointMode.Asynchronous) + { + _asyncDataMonitor = new object(); + } + } + + #endregion + + #region INodeEndpoint Events + + /// + /// Raised when the link status has changed. + /// + public event LinkStatusChangedDelegate OnLinkStatusChanged; + + #endregion + + #region Public Types and Enums + /// + /// Represents the style of communications used by the in-proc endpoint. + /// + internal enum EndpointMode + { + /// + /// The DataReceived event is raised on the same thread as that which called SendData. + /// + Synchronous, + + /// + /// The DataReceived event is raised on a separate thread from that which called SendData + /// + Asynchronous + } + + #endregion + + #region INodeEndpoint Properties + + /// + /// Returns the link status of this node. + /// + public LinkStatus LinkStatus + { + get { return _status; } + } + + #endregion + + #region INodeEndpoint Methods + /// + /// Causes this endpoint to wait for the remote endpoint to connect + /// + /// Unused + public void Listen(INodePacketFactory factory) + { + ErrorUtilities.VerifyThrowInternalNull(factory, "factory"); + _packetFactory = factory; + + // Initialize our thread in async mode so we are ready when the Node-side endpoint "connects". + if (_mode == EndpointMode.Asynchronous) + { + InitializeAsyncPacketThread(); + } + + _peerEndpoint.SetPeerNodeConnected(); + } + + /// + /// Causes this node to connect to the matched endpoint. + /// + /// Unused + public void Connect(INodePacketFactory factory) + { + ErrorUtilities.VerifyThrowInternalNull(factory, "factory"); + _packetFactory = factory; + + // Set up asynchronous packet pump, if necessary. + if (_mode == EndpointMode.Asynchronous) + { + InitializeAsyncPacketThread(); + } + + // Notify the Build Manager-side endpoint that the connection is now active. + _peerEndpoint.SetPeerNodeConnected(); + } + + /// + /// Shuts down the link + /// + public void Disconnect() + { + InternalDisconnect(); + + // Notify the remote endpoint that the link is dead + _peerEndpoint.SetPeerNodeDisconnected(); + } + + /// + /// Sends data to the peer endpoint. + /// + /// The packet to send. + public void SendData(INodePacket packet) + { + ErrorUtilities.VerifyThrow(_status == LinkStatus.Active, "Cannot send when link status is not active. Current status {0}", _status); + + if (_mode == EndpointMode.Synchronous) + { + _peerEndpoint._packetFactory.RoutePacket(0, packet); + } + else + { + EnqueuePacket(packet); + } + } + #endregion + + #region Internal Methods + /// + /// This method is used to create a matched pair of endpoints used by the Node Provider and + /// the Node. The inputs and outputs for each node are automatically configured. + /// + /// The communications mode for the endpoints. + /// The component host. + /// A matched pair of endpoints. + internal static EndpointPair CreateInProcEndpoints(EndpointMode mode, IBuildComponentHost host) + { + NodeEndpointInProc node = new NodeEndpointInProc(mode, host); + NodeEndpointInProc manager = new NodeEndpointInProc(mode, host); + + // NOTE: This creates a circular reference which must be explicitly broken before these + // objects can be reclaimed by the garbage collector. + node._peerEndpoint = manager; + manager._peerEndpoint = node; + + return new EndpointPair(node, manager); + } + #endregion + + #region Private Event Methods + + /// + /// Invokes the OnLinkStatusChanged event in a thread-safe manner. + /// + /// The new status of the endpoint link. + private void RaiseLinkStatusChanged(LinkStatus newStatus) + { + if (null != OnLinkStatusChanged) + { + LinkStatusChangedDelegate linkStatusDelegate = (LinkStatusChangedDelegate)OnLinkStatusChanged.Clone(); + linkStatusDelegate(this, newStatus); + } + } + + #endregion + + #region Private Methods + + /// + /// This method is called by the other endpoint when it is ready to establish the connection. + /// + private void SetPeerNodeConnected() + { + lock (s_locker) + { + _peerConnected = true; + if (_peerEndpoint._peerConnected) + { + ChangeLinkStatus(LinkStatus.Active); + _peerEndpoint.ChangeLinkStatus(LinkStatus.Active); + } + } + } + + /// + /// This method is called by either side to notify this endpoint that the link is inactive. + /// + private void SetPeerNodeDisconnected() + { + _peerConnected = false; + InternalDisconnect(); + } + + /// + /// This does the actual work of changing the status and shutting down any threads we may have for + /// disconnection. + /// + private void InternalDisconnect() + { + ErrorUtilities.VerifyThrow(_status == LinkStatus.Active, "Endpoint is not connected. Current status {0}", _status); + + ChangeLinkStatus(LinkStatus.Inactive); + + // Terminate our thread if we were in async mode + if (_mode == EndpointMode.Asynchronous) + { + TerminateAsyncPacketThread(); + } + } + + /// + /// Updates the current link status if it has changed and notifies any registered delegates. + /// + /// The status the node should now be in. + private void ChangeLinkStatus(LinkStatus newStatus) + { + ErrorUtilities.VerifyThrow(_status != newStatus, "Attempting to change status to existing status {0}.", _status); + _status = newStatus; + RaiseLinkStatusChanged(_status); + } + + #region Asynchronous Mode Methods + + /// + /// Adds a packet to the packet queue when asynchronous mode is enabled. + /// + /// The packet to be transmitted. + private void EnqueuePacket(INodePacket packet) + { + ErrorUtilities.VerifyThrowArgumentNull(packet, "packet"); + ErrorUtilities.VerifyThrow(_mode == EndpointMode.Asynchronous, "EndPoint mode is synchronous, should be asynchronous"); + ErrorUtilities.VerifyThrow(null != _packetQueue, "packetQueue is null"); + ErrorUtilities.VerifyThrow(null != _packetAvailable, "packetAvailable is null"); + + lock (_packetQueue) + { + _packetQueue.Enqueue(packet); + _packetAvailable.Set(); + } + } + + /// + /// Initializes the packet pump thread and the supporting events as well as the packet queue. + /// + private void InitializeAsyncPacketThread() + { + lock (_asyncDataMonitor) + { + ErrorUtilities.VerifyThrow(null == _packetPump, "packetPump != null"); + ErrorUtilities.VerifyThrow(null == _packetAvailable, "packetAvailable != null"); + ErrorUtilities.VerifyThrow(null == _terminatePacketPump, "terminatePacketPump != null"); + ErrorUtilities.VerifyThrow(null == _packetQueue, "packetQueue != null"); + + _packetPump = new Thread(PacketPumpProc); + _packetPump.Name = "InProc Endpoint Packet Pump"; + _packetAvailable = new AutoResetEvent(false); + _terminatePacketPump = new AutoResetEvent(false); + _packetQueue = new Queue(); + _packetPump.CurrentCulture = _componentHost.BuildParameters.Culture; + _packetPump.CurrentUICulture = _componentHost.BuildParameters.UICulture; + _packetPump.Start(); + } + } + + /// + /// Shuts down the packet pump thread and cleans up associated data. + /// + private void TerminateAsyncPacketThread() + { + lock (_asyncDataMonitor) + { + ErrorUtilities.VerifyThrow(null != _packetPump, "packetPump == null"); + ErrorUtilities.VerifyThrow(null != _packetAvailable, "packetAvailable == null"); + ErrorUtilities.VerifyThrow(null != _terminatePacketPump, "terminatePacketPump == null"); + ErrorUtilities.VerifyThrow(null != _packetQueue, "packetQueue == null"); + + _terminatePacketPump.Set(); + if (!_packetPump.Join(new TimeSpan(0, 0, BuildParameters.EndpointShutdownTimeout))) + { + // We timed out. Kill it. + _packetPump.Abort(); + } + + _packetPump = null; + _packetAvailable.Close(); + _packetAvailable = null; + _terminatePacketPump.Close(); + _terminatePacketPump = null; + _packetQueue = null; + } + } + + /// + /// This method handles the asynchronous message pump. It waits for messages to show up on the queue + /// and calls FireDataAvailable for each such packet. It will terminate when the terminate event is + /// set. + /// + private void PacketPumpProc() + { + try + { + // Ordering of the wait handles is important. The first signalled wait handle in the array + // will be returned by WaitAny if multiple wait handles are signalled. We prefer to have the + // terminate event triggered so that we cannot get into a situation where packets are being + // spammed to the endpoint and it never gets an opportunity to shutdown. + WaitHandle[] handles = new WaitHandle[] { _terminatePacketPump, _packetAvailable }; + + bool exitLoop = false; + do + { + int waitId = WaitHandle.WaitAny(handles); + switch (waitId) + { + case 0: + exitLoop = true; + break; + case 1: + { + // Figure out how many packets are currently in the queue to process. We + // will only process the number in the queue at this moment so that we don't + // get into a condition where the sending thread continues to pump packets in + // as fast as we can send them, potentially starving us from detecting a + // terminate event. + int packets = 0; + lock (_packetQueue) + { + packets = _packetQueue.Count; + } + + while (packets > 0) + { + // Grab the first packet in the queue. + INodePacket packet = null; + + lock (_packetQueue) + { + ErrorUtilities.VerifyThrow(_packetQueue.Count > 0, "Packet Queue count is zero."); + packet = _packetQueue.Dequeue(); + } + + _peerEndpoint._packetFactory.RoutePacket(0, packet); + --packets; + } + } + + break; + + default: + ErrorUtilities.ThrowInternalError("waitId {0} out of range.", waitId); + break; + } + } + while (!exitLoop); + } + catch (Exception e) + { + // Dump all engine exceptions to a temp file + // so that we have something to go on in the + // event of a failure + ExceptionHandling.DumpExceptionToFile(e); + throw; + } + } + + #endregion + + #endregion + + #region Structs + /// + /// Used to return a matched pair of endpoints for in-proc nodes to use with the Build Manager. + /// + internal struct EndpointPair + { + /// + /// The endpoint destined for use by a node. + /// + internal readonly NodeEndpointInProc NodeEndpoint; + + /// + /// The endpoint destined for use by the Build Manager + /// + internal readonly NodeEndpointInProc ManagerEndpoint; + + /// + /// Creates an endpoint pair + /// + /// The node-side endpoint. + /// The manager-side endpoint. + internal EndpointPair(NodeEndpointInProc node, NodeEndpointInProc manager) + { + NodeEndpoint = node; + ManagerEndpoint = manager; + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs new file mode 100644 index 00000000000..4b0e620e6b5 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of a node endpoint for out-of-proc nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using System.Security; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Security.Permissions; + +using BuildParameters = Microsoft.Build.Execution.BuildParameters; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This is an implementation of INodeEndpoint for the out-of-proc nodes. It acts only as a client. + /// + internal class NodeEndpointOutOfProc : NodeEndpointOutOfProcBase + { + #region Private Data + + /// + /// The build component host + /// + private IBuildComponentHost _componentHost; + + #endregion + + #region Constructors and Factories + + /// + /// Instantiates an endpoint to act as a client + /// + /// The name of the pipe to which we should connect. + /// The component host. + internal NodeEndpointOutOfProc(string pipeName, IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + _componentHost = host; + + InternalConstruct(pipeName); + } + + #endregion + + /// + /// Returns the host handshake for this node endpoint + /// + protected override long GetHostHandshake() + { + return NodeProviderOutOfProc.HostHandshake; + } + + /// + /// Returns the client handshake for this node endpoint + /// + protected override long GetClientHandshake() + { + return NodeProviderOutOfProc.ClientHandshake; + } + + #region Structs + /// + /// Used to return a matched pair of endpoints for in-proc nodes to use with the Build Manager. + /// + internal struct EndpointPair + { + /// + /// The endpoint destined for use by a node. + /// + internal readonly NodeEndpointInProc NodeEndpoint; + + /// + /// The endpoint destined for use by the Build Manager + /// + internal readonly NodeEndpointInProc ManagerEndpoint; + + /// + /// Creates an endpoint pair + /// + /// The node-side endpoint. + /// The manager-side endpoint. + internal EndpointPair(NodeEndpointInProc node, NodeEndpointInProc manager) + { + NodeEndpoint = node; + ManagerEndpoint = manager; + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeFailedToLaunchException.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeFailedToLaunchException.cs new file mode 100644 index 00000000000..fa0950f92ad --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeFailedToLaunchException.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The exception which gets thrown if the an out of proc task host failed to launch. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.Serialization; +using System.Security.Permissions; +using Microsoft.Build.Shared; +using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; + +namespace Microsoft.Build.BackEnd +{ + /// + /// An exception representing the case where a TaskHost node failed to launch. + /// This may happen for example when the TaskHost binary is corrupted. + /// + /// + /// If you add fields to this class, add a custom serialization constructor and override GetObjectData(). + /// + [Serializable] + internal class NodeFailedToLaunchException : Exception + { + /// + /// Constructs a standard NodeFailedToLaunchException. + /// + internal NodeFailedToLaunchException() + : base() + { + } + + /// + /// Constructs a standard NodeFailedToLaunchException. + /// + internal NodeFailedToLaunchException(string errorCode, string message) + : base(message) + { + ErrorCode = errorCode; + ErrorDescription = message; + } + + /// + /// Constructor for deserialization. + /// + protected NodeFailedToLaunchException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Gets the error code (if any) associated with the exception message. + /// + /// Error code string, or null. + public string ErrorCode + { + get; + private set; + } + + /// + /// Gets the error code (if any) associated with the exception message. + /// + /// Error code string, or null. + public string ErrorDescription + { + get; + private set; + } + + /// + /// ISerializable method which we must override since Exception implements this interface + /// If we ever add new members to this class, we'll need to update this. + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + override public void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("ErrorCode", ErrorCode); + info.AddValue("ErrorDescription", ErrorDescription); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeInfo.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeInfo.cs new file mode 100644 index 00000000000..6d0392a7e32 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeInfo.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Class containing information about a node. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Contains information about the state of a node. + /// + internal class NodeInfo + { + /// + /// The node ID + /// + private int _nodeId; + + /// + /// The provider type + /// + private NodeProviderType _providerType; + + /// + /// The configuration IDs the node knows about. These are not necessarily the ones + /// currently assigned to the node, as that can change. + /// + private HashSet _configurationIDs; + + /// + /// Constructor. + /// + public NodeInfo(int nodeId, NodeProviderType providerType) + { + _nodeId = nodeId; + _providerType = providerType; + _configurationIDs = new HashSet(); + } + + /// + /// The ID of the node. + /// + public int NodeId + { + get { return _nodeId; } + } + + /// + /// The type of provider which manages this node. + /// + public NodeProviderType ProviderType + { + get { return _providerType; } + } + + /// + /// Assigns the specific configuration ID to the node. + /// + /// + /// True if the configuration is not already known to the node and must be sent to it, false otherwise. + /// + public bool AssignConfiguration(int configId) + { + if (!HasConfiguration(configId)) + { + _configurationIDs.Add(configId); + return true; + } + + return false; + } + + /// + /// Determines whether the specified configuration if is known to the node. + /// + public bool HasConfiguration(int configId) + { + return _configurationIDs.Contains(configId); + } + + /// + /// Returns true if this node can service requests with the specified affinity. + /// + internal bool CanServiceRequestWithAffinity(NodeAffinity nodeAffinity) + { + switch (nodeAffinity) + { + case NodeAffinity.Any: + return true; + + case NodeAffinity.InProc: + return _providerType == NodeProviderType.InProc; + + case NodeAffinity.OutOfProc: + return _providerType != NodeProviderType.InProc; + } + + return true; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeManager.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeManager.cs new file mode 100644 index 00000000000..ebdce676f3b --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeManager.cs @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The class which manages build nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The NodeManager class is responsible for marshalling data to/from the NodeProviders and organizing the + /// creation of new nodes on request. + /// + internal class NodeManager : INodeManager + { + /// + /// The invalid node id + /// + private const int InvalidNodeId = 0; + + /// + /// The node provider for the in-proc node. + /// + private INodeProvider _inProcNodeProvider; + + /// + /// The node provider for out-of-proc nodes. + /// + private INodeProvider _outOfProcNodeProvider; + + /// + /// The build component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// Mapping of manager-produced node IDs to the provider hosting the node. + /// + private Dictionary _nodeIdToProvider; + + /// + /// The packet factory used to translate and route packets + /// + private NodePacketFactory _packetFactory; + + /// + /// The next node id to assign to a node. + /// + private int _nextNodeId; + + /// + /// The nodeID for the inproc node. + /// + private int _inprocNodeId = 1; + + /// + /// Flag indicating when the nodes have been shut down. + /// BUGBUG: This is a fix which corrects an RI blocking BVT failure. The real fix must be determined before RTM. + /// This must be investigated and resolved before RTM. The apparent issue is that a design-time build has already called EndBuild + /// through the BuildManagerAccessor, and the nodes are shut down. Shortly thereafter, the solution build manager comes through and calls EndBuild, which throws + /// another Shutdown packet in the queue, and causes the following build to stop prematurely. This is all timing related - not every sequence of builds seems to + /// cause the problem, probably due to the order in which the packet queue gets serviced relative to other threads. + /// + /// It appears that the problem is that the BuildRequestEngine is being invoked in a way that causes a shutdown packet to appear to overlap with a build request packet. + /// Interactions between the in-proc node communication thread and the shutdown mechanism must be investigated to determine how BuildManager.EndBuild is allowing itself + /// to return before the node has indicated it is actually finished. + /// + private bool _nodesShutdown = true; + + /// + /// Tracks whether ShutdownComponent has been called. + /// + private bool _componentShutdown; + + /// + /// Constructor. + /// + private NodeManager() + { + _nodeIdToProvider = new Dictionary(); + _packetFactory = new NodePacketFactory(); + _nextNodeId = _inprocNodeId + 1; + } + + #region INodeManager Members + + /// + /// Creates a node on an available NodeProvider, if any.. + /// + /// The configuration to use for the remote node. + /// A NodeInfo describing the node created, or null if none could be created. + public NodeInfo CreateNode(NodeConfiguration configuration, NodeAffinity nodeAffinity) + { + // We will prefer to make nodes on the "closest" providers first; in-proc, then + // out-of-proc, then remote. + // When we support distributed build, we will also consider the remote provider. + int nodeId = InvalidNodeId; + if ((nodeAffinity == NodeAffinity.Any || nodeAffinity == NodeAffinity.InProc) && !_componentHost.BuildParameters.DisableInProcNode) + { + nodeId = AttemptCreateNode(_inProcNodeProvider, configuration); + } + + if (nodeId == InvalidNodeId && (nodeAffinity == NodeAffinity.Any || nodeAffinity == NodeAffinity.OutOfProc)) + { + nodeId = AttemptCreateNode(_outOfProcNodeProvider, configuration); + } + + if (nodeId == InvalidNodeId) + { + return null; + } + + // If we created a node, they should no longer be considered shut down. + _nodesShutdown = false; + + return new NodeInfo(nodeId, _nodeIdToProvider[nodeId].ProviderType); + } + + /// + /// Sends data to the specified node. + /// + /// The node. + /// The packet to send. + public void SendData(int node, INodePacket packet) + { + // Look up the node provider for this node in the mapping. + INodeProvider provider = null; + if (!_nodeIdToProvider.TryGetValue(node, out provider)) + { + ErrorUtilities.ThrowInternalError("Node {0} does not have a provider.", node); + } + + // Send the data. + provider.SendData(node, packet); + } + + /// + /// Shuts down all of the connected managed nodes. + /// + /// Flag indicating if nodes should prepare for reuse. + public void ShutdownConnectedNodes(bool enableReuse) + { + ErrorUtilities.VerifyThrow(!_componentShutdown, "We should never be calling ShutdownNodes after ShutdownComponent has been called"); + + if (_nodesShutdown) + { + return; + } + + _nodesShutdown = true; + + if (null != _inProcNodeProvider) + { + _inProcNodeProvider.ShutdownConnectedNodes(enableReuse); + } + + if (null != _outOfProcNodeProvider) + { + _outOfProcNodeProvider.ShutdownConnectedNodes(enableReuse); + } + } + + /// + /// Shuts down all of managed nodes permanently. + /// + public void ShutdownAllNodes() + { + // don't worry about inProc + if (null != _outOfProcNodeProvider) + { + _outOfProcNodeProvider.ShutdownAllNodes(); + } + } + + #endregion + + #region IBuildComponent Members + + /// + /// Initializes the component + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrow(_componentHost == null, "NodeManager already initialized."); + ErrorUtilities.VerifyThrow(host != null, "We can't create a NodeManager with a null componentHost"); + _componentHost = host; + + _inProcNodeProvider = _componentHost.GetComponent(BuildComponentType.InProcNodeProvider) as INodeProvider; + _outOfProcNodeProvider = _componentHost.GetComponent(BuildComponentType.OutOfProcNodeProvider) as INodeProvider; + + _componentShutdown = false; + + // DISTRIBUTED: Get the remote node provider. + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + if (_inProcNodeProvider != null && _inProcNodeProvider is IDisposable) + { + ((IDisposable)_inProcNodeProvider).Dispose(); + } + + if (_outOfProcNodeProvider != null && _outOfProcNodeProvider is IDisposable) + { + ((IDisposable)_outOfProcNodeProvider).Dispose(); + } + + _inProcNodeProvider = null; + _outOfProcNodeProvider = null; + _componentHost = null; + _componentShutdown = true; + + ClearPerBuildState(); + } + + /// + /// Reset the state of objects in the node manager which need to be reset between builds. + /// + public void ClearPerBuildState() + { + _packetFactory = new NodePacketFactory(); + _nodeIdToProvider.Clear(); + + // because the inproc node is always 1 therefore when new nodes are requested we need to start at 2 + _nextNodeId = _inprocNodeId + 1; + } + + #endregion + + #region INodePacketFactory Members + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _packetFactory.RegisterPacketHandler(packetType, factory, handler); + } + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + public void UnregisterPacketHandler(NodePacketType packetType) + { + _packetFactory.UnregisterPacketHandler(packetType); + } + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + if (packetType == NodePacketType.NodeShutdown) + { + RemoveNodeFromMapping(nodeId); + } + + _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + } + + /// + /// Routes the specified packet. This is called by the Inproc node directly since it does not have to do any deserialization + /// + /// The node from which the packet was received. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + { + if (packet.Type == NodePacketType.NodeShutdown) + { + RemoveNodeFromMapping(nodeId); + } + + _packetFactory.RoutePacket(nodeId, packet); + } + + #endregion + + /// + /// Factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.NodeManager, "Cannot create component of type {0}", type); + return new NodeManager(); + } + + /// + /// We have recieved the node shutdown packet for this node, we should remove it from our list of providers. + /// + private void RemoveNodeFromMapping(int nodeId) + { + _nodeIdToProvider.Remove(nodeId); + if (_nodeIdToProvider.Count == 0) + { + // The inproc node is always 1 therefore when new nodes are requested we need to start at 2 + _nextNodeId = _inprocNodeId + 1; + } + } + + /// + /// Attempts to create a node on the specified machine using the specified provider. + /// + /// The provider used to create the node. + /// The id of the node created. + private int AttemptCreateNode(INodeProvider nodeProvider, NodeConfiguration nodeConfiguration) + { + // If no provider was passed in, we obviously can't create a node. + if (null == nodeProvider) + { + ErrorUtilities.ThrowInternalError("No node provider provided."); + return InvalidNodeId; + } + + // Are there any free slots on this provider? + if (nodeProvider.AvailableNodes == 0) + { + return InvalidNodeId; + } + + // Assign a global ID to the node we are about to create. + int nodeId = InvalidNodeId; + + if (nodeProvider is NodeProviderInProc) + { + nodeId = _inprocNodeId; + } + else + { + nodeId = _nextNodeId; + _nextNodeId++; + } + + NodeConfiguration configToSend = nodeConfiguration.Clone(); + configToSend.NodeId = nodeId; + + // Create the node and add it to our mapping. + bool createdNode = nodeProvider.CreateNode(nodeId, this, configToSend); + + if (!createdNode) + { + return InvalidNodeId; + } + + _nodeIdToProvider.Add(nodeId, nodeProvider); + return nodeId; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodePacketTranslatorExtensions.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodePacketTranslatorExtensions.cs new file mode 100644 index 00000000000..a3f20362f7f --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodePacketTranslatorExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of Some special translation methods that we +// can't put in INodePacketTranslator. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.IO; +using System.Threading; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Globalization; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class is responsible for serializing and deserializing anything that is not + /// officially supported by INodePacketTranslator, but that we still want to do + /// custom translation of. + /// + static internal class NodePacketTranslatorExtensions + { + /// + /// Translates a PropertyDictionary of ProjectPropertyInstances. + /// + /// The tranlator doing the translating + /// The dictionary to translate. + public static void TranslateProjectPropertyInstanceDictionary(this INodePacketTranslator translator, ref PropertyDictionary value) + { + if (!translator.TranslateNullable(value)) + { + return; + } + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + int count = 0; + translator.Translate(ref count); + + value = new PropertyDictionary(count); + for (int i = 0; i < count; i++) + { + ProjectPropertyInstance instance = null; + translator.Translate(ref instance, ProjectPropertyInstance.FactoryForDeserialization); + value[instance.Name] = instance; + } + } + else // TranslationDirection.WriteToStream + { + int count = value.Count; + translator.Translate(ref count); + + foreach (ProjectPropertyInstance instance in value) + { + ProjectPropertyInstance instanceForSerialization = instance; + translator.Translate(ref instanceForSerialization, ProjectPropertyInstance.FactoryForDeserialization); + } + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderInProc.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderInProc.cs new file mode 100644 index 00000000000..28ff82ad940 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderInProc.cs @@ -0,0 +1,455 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Implementation of an in-proc node provider. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Diagnostics; + +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +using BuildParameters = Microsoft.Build.Execution.BuildParameters; +using NodeEngineShutdownReason = Microsoft.Build.Execution.NodeEngineShutdownReason; + +namespace Microsoft.Build.BackEnd +{ + /// + /// An implementation of a node provider for in-proc nodes. + /// + internal class NodeProviderInProc : INodeProvider, INodePacketFactory, IDisposable + { + #region Private Data + + /// + /// The invalid in-proc node id + /// + private const int InvalidInProcNodeId = 0; + + /// + /// Flag indicating we have disposed. + /// + private bool _disposed = false; + + /// + /// Value used to ensure multiple in-proc nodes which save the operating environment are not created. + /// + private Semaphore _inProcNodeOwningOperatingEnvironment; + + /// + /// The component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// The in-proc node. + /// + private INode _inProcNode; + + /// + /// The in-proc node endpoint. + /// + private INodeEndpoint _inProcNodeEndpoint; + + /// + /// The packet factory used to route packets from the node. + /// + private INodePacketFactory _packetFactory; + + /// + /// The in-proc node thread. + /// + private Thread _inProcNodeThread; + + /// + /// Event which is raised when the in-proc endpoint is connected. + /// + private AutoResetEvent _endpointConnectedEvent; + + /// + /// The ID of the in-proc node. + /// + private int _inProcNodeId = InvalidInProcNodeId; + + /// + /// Check to allow the inproc node to have exclusive ownership of the operating environment + /// + private bool _exclusiveOperatingEnvironment = false; + + #endregion + + #region Constructor + /// + /// Initializes the node provider. + /// + public NodeProviderInProc() + { + _endpointConnectedEvent = new AutoResetEvent(false); + } + + #endregion + + /// + /// Finalizer + /// + ~NodeProviderInProc() + { + Dispose(false /* disposing */); + } + + /// + /// Returns the type of nodes managed by this provider. + /// + public NodeProviderType ProviderType + { + get { return NodeProviderType.InProc; } + } + + /// + /// Returns the number of nodes available to create on this provider. + /// + public int AvailableNodes + { + get + { + if (_inProcNodeId != InvalidInProcNodeId) + { + return 0; + } + + return 1; + } + } + + #region IBuildComponent Members + + /// + /// Sets the build component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + _componentHost = host; + } + + /// + /// Shuts down this component. + /// + public void ShutdownComponent() + { + _componentHost = null; + _inProcNode = null; + } + + #endregion + + #region INodeProvider Members + + /// + /// Sends data to the specified node. + /// + /// The node to which data should be sent. + /// The data to send. + public void SendData(int nodeId, INodePacket packet) + { + ErrorUtilities.VerifyThrowArgumentOutOfRange(nodeId == _inProcNodeId, "node"); + ErrorUtilities.VerifyThrowArgumentNull(packet, "packet"); + + if (null == _inProcNode) + { + return; + } + + _inProcNodeEndpoint.SendData(packet); + } + + /// + /// Causes all connected nodes to be shut down. + /// + /// Flag indicating if the nodes should prepare for reuse. + public void ShutdownConnectedNodes(bool enableReuse) + { + if (null != _inProcNode) + { + _inProcNodeEndpoint.SendData(new NodeBuildComplete(enableReuse)); + } + } + + /// + /// Causes all nodes to be shut down permanently - for InProc nodes it is the same as ShutdownConnectedNodes + /// with enableReuse = false + /// + public void ShutdownAllNodes() + { + ShutdownConnectedNodes(false /* no node reuse */); + } + + /// + /// Requests that a node be created on the specified machine. + /// + /// The id of the node to create. + /// The factory to use to create packets from this node. + /// The configuration for the node. + public bool CreateNode(int nodeId, INodePacketFactory factory, NodeConfiguration configuration) + { + ErrorUtilities.VerifyThrow(nodeId != InvalidInProcNodeId, "Cannot create in-proc node."); + + // Attempt to get the operating environment semaphore if requested. + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + // We can only create additional in-proc nodes if we have decided not to save the operating environment. This is the global + // DTAR case in Visual Studio, but other clients might enable this as well under certain special circumstances. + ErrorUtilities.VerifyThrow(_inProcNodeOwningOperatingEnvironment == null, "Unexpected non-null in-proc node semaphore."); + + // Blend.exe v4.x or earlier launches two nodes that co-own the same operating environment + // and they will not patch this + var p = Process.GetCurrentProcess(); + { + // This should be reasonably sufficient to assume MS Expression Blend 4 or earlier + if ((FileUtilities.CurrentExecutableName.Equals("Blend", StringComparison.OrdinalIgnoreCase)) && + (p.MainModule.FileVersionInfo.OriginalFilename.Equals("Blend.exe", StringComparison.OrdinalIgnoreCase)) && + (p.MainModule.FileVersionInfo.ProductMajorPart < 5)) + { + _exclusiveOperatingEnvironment = false; + } + } + + if (Environment.GetEnvironmentVariable("MSBUILDINPROCENVCHECK") == "1") + { + _exclusiveOperatingEnvironment = true; + } + + if (_exclusiveOperatingEnvironment) + { + _inProcNodeOwningOperatingEnvironment = new Semaphore(1, 1, "MSBuildIPN_" + Process.GetCurrentProcess().Id); + if (!_inProcNodeOwningOperatingEnvironment.WaitOne(0)) + { + // Can't take the operating environment. + return false; + } + } + } + + // If it doesn't already exist, create it. + if (_inProcNode == null) + { + if (!InstantiateNode(factory)) + { + return false; + } + } + + _inProcNodeEndpoint.SendData(configuration); + _inProcNodeId = nodeId; + + return true; + } + + #endregion + + #region INodePacketFactory Members + + /// + /// Registers a packet handler. Not used in the in-proc node. + /// + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + // Not used + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Unregisters a packet handler. Not used in the in-proc node. + /// + public void UnregisterPacketHandler(NodePacketType packetType) + { + // Not used + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Deserializes and routes a packet. Not used in the in-proc node. + /// + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + // Not used + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Routes a packet. + /// + /// The id of the node from which the packet is being routed. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + { + INodePacketFactory factory = _packetFactory; + + if (_inProcNodeId != InvalidInProcNodeId) + { + // If this was a shutdown packet, we are done with the node. Release all context associated with it. Do this here, rather + // than after we route the packet, because otherwise callbacks to the NodeManager to determine if we have available nodes + // will report that the in-proc node is still in use when it has actually shut down. + int savedInProcNodeId = _inProcNodeId; + if (packet.Type == NodePacketType.NodeShutdown) + { + _inProcNodeId = InvalidInProcNodeId; + + // Release the operating environment semaphore if we were holding it. + if ((_componentHost.BuildParameters.SaveOperatingEnvironment) && + (_inProcNodeOwningOperatingEnvironment != null)) + { + _inProcNodeOwningOperatingEnvironment.Release(); + _inProcNodeOwningOperatingEnvironment.Close(); + _inProcNodeOwningOperatingEnvironment = null; + } + + if (!_componentHost.BuildParameters.EnableNodeReuse) + { + _inProcNode = null; + _inProcNodeEndpoint = null; + _inProcNodeThread = null; + _packetFactory = null; + } + } + + // Route the packet back to the NodeManager. + factory.RoutePacket(savedInProcNodeId, packet); + } + } + + #endregion + + /// + /// IDisposable implementation + /// + public void Dispose() + { + Dispose(true /* disposing */); + GC.SuppressFinalize(this); + } + + /// + /// Factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.InProcNodeProvider, "Cannot create component of type {0}", type); + return new NodeProviderInProc(); + } + + #region Private Methods + + /// + /// Creates a new in-proc node. + /// + private bool InstantiateNode(INodePacketFactory factory) + { + ErrorUtilities.VerifyThrow(null == _inProcNode, "In Proc node already instantiated."); + ErrorUtilities.VerifyThrow(null == _inProcNodeEndpoint, "In Proc node endpoint already instantiated."); + + NodeEndpointInProc.EndpointPair endpoints = NodeEndpointInProc.CreateInProcEndpoints(NodeEndpointInProc.EndpointMode.Synchronous, _componentHost); + + _inProcNodeEndpoint = endpoints.ManagerEndpoint; + _inProcNodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(InProcNodeEndpoint_OnLinkStatusChanged); + + _packetFactory = factory; + _inProcNode = new InProcNode(_componentHost, endpoints.NodeEndpoint); + + _inProcNodeThread = new Thread(InProcNodeThreadProc, BuildParameters.ThreadStackSize); + _inProcNodeThread.Name = String.Format(CultureInfo.CurrentCulture, "In-proc Node ({0})", _componentHost.Name); + _inProcNodeThread.IsBackground = true; + _inProcNodeThread.CurrentCulture = _componentHost.BuildParameters.Culture; + _inProcNodeThread.CurrentUICulture = _componentHost.BuildParameters.UICulture; + _inProcNodeThread.Start(); + + _inProcNodeEndpoint.Connect(this); + + int connectionTimeout = CommunicationsUtilities.NodeConnectionTimeout; + bool connected = _endpointConnectedEvent.WaitOne(connectionTimeout, false); + ErrorUtilities.VerifyThrow(connected, "In-proc node failed to start up within {0}ms", connectionTimeout); + return true; + } + + /// + /// Thread proc which runs the in-proc node. + /// + private void InProcNodeThreadProc() + { + Exception e; + NodeEngineShutdownReason reason = _inProcNode.Run(out e); + InProcNodeShutdown(reason, e); + } + + /// + /// Callback invoked when the link status of the endpoint has changed. + /// + /// The endpoint whose status has changed. + /// The new link status. + private void InProcNodeEndpoint_OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + if (status == LinkStatus.Active) + { + // We don't verify this outside of the 'if' because we don't care about the link going down, which will occur + // after we have cleared the inProcNodeEndpoint due to shutting down the node. + ErrorUtilities.VerifyThrow(endpoint == _inProcNodeEndpoint, "Received link status event for a node other than our peer."); + _endpointConnectedEvent.Set(); + } + } + + /// + /// Callback invoked when the endpoint shuts down. + /// + /// The reason the endpoint is shutting down. + /// Any exception which was raised that caused the endpoint to shut down. + private void InProcNodeShutdown(NodeEngineShutdownReason reason, Exception e) + { + switch (reason) + { + case NodeEngineShutdownReason.BuildComplete: + case NodeEngineShutdownReason.BuildCompleteReuse: + case NodeEngineShutdownReason.Error: + break; + + case NodeEngineShutdownReason.ConnectionFailed: + ErrorUtilities.ThrowInternalError("Unexpected shutdown code {0} received.", reason); + break; + } + } + + /// + /// Dispose implementation. + /// + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_inProcNodeOwningOperatingEnvironment != null) + { + _inProcNodeOwningOperatingEnvironment.Release(); + _inProcNodeOwningOperatingEnvironment.Dispose(); + _inProcNodeOwningOperatingEnvironment = null; + } + + if (_endpointConnectedEvent != null) + { + _endpointConnectedEvent.Dispose(); + _endpointConnectedEvent = null; + } + } + + _disposed = true; + } + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProc.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProc.cs new file mode 100644 index 00000000000..af18d1a7e76 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProc.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing INodeProvider for out-of-proc nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using System.IO.Pipes; +using System.Diagnostics; +using System.Threading; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Security.Permissions; + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The provider for out-of-proc nodes. This manages the lifetime of external MSBuild.exe processes + /// which act as child nodes for the build system. + /// + internal class NodeProviderOutOfProc : NodeProviderOutOfProcBase, INodeProvider + { + /// + /// A mapping of all the nodes managed by this provider. + /// + private Dictionary _nodeContexts; + + /// + /// Constructor. + /// + private NodeProviderOutOfProc() + { + } + + #region INodeProvider Members + + /// + /// Returns the node provider type. + /// + public NodeProviderType ProviderType + { + [DebuggerStepThrough] + get + { return NodeProviderType.OutOfProc; } + } + + /// + /// Returns the number of available nodes. + /// + public int AvailableNodes + { + get + { + return ComponentHost.BuildParameters.MaxNodeCount - _nodeContexts.Count; + } + } + + /// + /// Magic number sent by the host to the client during the handshake. + /// Derived from the binary timestamp to avoid mixing binary versions. + /// + internal static long HostHandshake + { + get + { + long baseHandshake = Constants.AssemblyTimestamp; + if (Environment.Is64BitProcess) + { + unchecked + { + baseHandshake = baseHandshake ^ (long)0x8b8b8b8b8b8b8b8b; + } + } + + baseHandshake = CommunicationsUtilities.GenerateHostHandshakeFromBase(baseHandshake, ClientHandshake); + return baseHandshake; + } + } + + /// + /// Magic number sent by the client to the host during the handshake. + /// Munged version of the host handshake. + /// + internal static long ClientHandshake + { + get + { + // Mask out the first byte. That's because old + // builds used a single, non zero initial byte, + // and we don't want to risk communicating with them + return (Constants.AssemblyTimestamp ^ Int64.MaxValue) & 0x00FFFFFFFFFFFFFF; + } + } + + /// + /// Instantiates a new MSBuild process acting as a child node. + /// + public bool CreateNode(int nodeId, INodePacketFactory factory, NodeConfiguration configuration) + { + ErrorUtilities.VerifyThrowArgumentNull(factory, "factory"); + + if (_nodeContexts.Count == ComponentHost.BuildParameters.MaxNodeCount) + { + ErrorUtilities.ThrowInternalError("All allowable nodes already created ({0}).", _nodeContexts.Count); + return false; + } + + // Start the new process. We pass in a node mode with a node number of 1, to indicate that we + // want to start up just a standard MSBuild out-of-proc node. + string commandLineArgs = " /nologo /nodemode:1 "; + + // Enable node re-use if it is set. + if (ComponentHost.BuildParameters.EnableNodeReuse) + { + commandLineArgs += "/nr"; + } + + // Make it here. + CommunicationsUtilities.Trace("Starting to acquire a new or existing node to establish node ID {0}...", nodeId); + NodeContext context = GetNode(null, commandLineArgs, nodeId, factory, NodeProviderOutOfProc.HostHandshake, NodeProviderOutOfProc.ClientHandshake, NodeContextTerminated); + + if (null != context) + { + _nodeContexts[nodeId] = context; + + // Start the asynchronous read. + context.BeginAsyncPacketRead(); + + // Configure the node. + context.SendData(configuration); + + return true; + } + + throw new BuildAbortedException(ResourceUtilities.FormatResourceString("CouldNotConnectToMSBuildExe", ComponentHost.BuildParameters.NodeExeLocation)); + } + + /// + /// Sends data to the specified node. + /// + /// The node to which data shall be sent. + /// The packet to send. + public void SendData(int nodeId, INodePacket packet) + { + ErrorUtilities.VerifyThrow(_nodeContexts.ContainsKey(nodeId), "Invalid node id specified: {0}.", nodeId); + + SendData(_nodeContexts[nodeId], packet); + } + + /// + /// Shuts down all of the connected managed nodes. + /// + /// Flag indicating if nodes should prepare for reuse. + public void ShutdownConnectedNodes(bool enableReuse) + { + // Send the build completion message to the nodes, causing them to shutdown or reset. + List contextsToShutDown; + + lock (_nodeContexts) + { + contextsToShutDown = new List(_nodeContexts.Values); + } + + ShutdownConnectedNodes(contextsToShutDown, enableReuse); + } + + /// + /// Shuts down all of the managed nodes permanently. + /// + public void ShutdownAllNodes() + { + ShutdownAllNodes(NodeProviderOutOfProc.HostHandshake, NodeProviderOutOfProc.ClientHandshake, NodeContextTerminated); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Initializes the component. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + this.ComponentHost = host; + _nodeContexts = new Dictionary(); + } + + /// + /// Shuts down the component + /// + public void ShutdownComponent() + { + } + + #endregion + + /// + /// Static factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType componentType) + { + ErrorUtilities.VerifyThrow(componentType == BuildComponentType.OutOfProcNodeProvider, "Factory cannot create components of type {0}", componentType); + return new NodeProviderOutOfProc(); + } + + /// + /// Method called when a context terminates. + /// + private void NodeContextTerminated(int nodeId) + { + lock (_nodeContexts) + { + _nodeContexts.Remove(nodeId); + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs new file mode 100644 index 00000000000..ecffbf608ac --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -0,0 +1,752 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class containing the shared pieces of code from NodeProviderOutOfProc +// and NodeProviderOutOfProcTaskHost. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using System.IO.Pipes; +using System.Diagnostics; +using System.Threading; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Security.Permissions; + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Internal; + +using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Contains the shared pieces of code from NodeProviderOutOfProc + /// and NodeProviderOutOfProcTaskHost. + /// + internal abstract class NodeProviderOutOfProcBase + { + /// + /// The maximum number of bytes to write + /// + private const int MaxPacketWriteSize = 1048576; + + /// + /// The number of times to retry creating an out-of-proc node. + /// + private const int NodeCreationRetries = 10; + + /// + /// The amount of time to wait for an out-of-proc node to spool up before we give up. + /// + private const int TimeoutForNewNodeCreation = 30000; + + /// + /// The build component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// Keeps track of the processes we've already checked for nodes so we don't check them again. + /// + private HashSet _processesToIgnore = new HashSet(); + + /// + /// Delegate used to tell the node provider that a context has terminated. + /// + /// The id of the node which terminated. + internal delegate void NodeContextTerminateDelegate(int nodeId); + + /// + /// The build component host. + /// + protected IBuildComponentHost ComponentHost + { + get { return _componentHost; } + set { _componentHost = value; } + } + + /// + /// Sends data to the specified node. + /// + /// The node to which data shall be sent. + /// The packet to send. + protected void SendData(NodeContext context, INodePacket packet) + { + ErrorUtilities.VerifyThrowArgumentNull(packet, "packet"); + context.SendData(packet); + } + + /// + /// Shuts down all of the connected managed nodes. + /// + /// List of the contexts to be shut down + /// Flag indicating if nodes should prepare for reuse. + protected void ShutdownConnectedNodes(List contextsToShutDown, bool enableReuse) + { + // Send the build completion message to the nodes, causing them to shutdown or reset. + _processesToIgnore.Clear(); + + foreach (NodeContext nodeContext in contextsToShutDown) + { + if (null != nodeContext) + { + nodeContext.SendData(new NodeBuildComplete(enableReuse)); + } + } + } + + /// + /// Shuts down all of the managed nodes permanently. + /// + /// host handshake key + /// client handshake key + /// Delegate used to tell the node provider that a context has terminated + protected void ShutdownAllNodes(long hostHandshake, long clientHandshake, NodeContextTerminateDelegate terminateNode) + { + // INodePacketFactory + INodePacketFactory factory = new NodePacketFactory(); + + // Find proper msbuild executable name + string msbuildExeName = Environment.GetEnvironmentVariable("MSBUILD_EXE_NAME"); + + if (String.IsNullOrEmpty(msbuildExeName)) + { + msbuildExeName = "MSBuild.exe"; + } + + // Search for all instances of the msbuild process and create a list of them + List nodeProcesses = new List(Process.GetProcessesByName(Path.GetFileNameWithoutExtension(msbuildExeName))); + + // Find proper MSBuildTaskHost executable name + string msbuildtaskhostExeName = NodeProviderOutOfProcTaskHost.TaskHostNameForClr2TaskHost; + + // Search for all instances of msbuildtaskhost process and add them to the process list + nodeProcesses.AddRange(new List(Process.GetProcessesByName(Path.GetFileNameWithoutExtension(msbuildtaskhostExeName)))); + + // For all processes in the list, send signal to terminate if able to connect + foreach (Process nodeProcess in nodeProcesses) + { + NamedPipeClientStream nodeStream = TryConnectToProcess(nodeProcess.Id, 30/*verified to miss nodes if smaller*/, hostHandshake, clientHandshake); + if (null != nodeStream) + { + // If we're able to connect to such a process, send a packet requesting its termination + CommunicationsUtilities.Trace("Shutting down node with pid = {0}", nodeProcess.Id); + NodeContext nodeContext = new NodeContext(0, nodeProcess.Id, nodeStream, factory, terminateNode); + nodeContext.SendData(new NodeBuildComplete(false /* no node reuse */)); + nodeStream.Close(); + } + } + } + + /// + /// Finds or creates a child process which can act as a node. + /// + /// The pipe stream representing the node. + protected NodeContext GetNode(string msbuildLocation, string commandLineArgs, int nodeId, INodePacketFactory factory, long hostHandshake, long clientHandshake, NodeContextTerminateDelegate terminateNode) + { + if (String.IsNullOrEmpty(msbuildLocation)) + { + msbuildLocation = _componentHost.BuildParameters.NodeExeLocation; + } + + if (String.IsNullOrEmpty(msbuildLocation)) + { + string msbuildExeName = Environment.GetEnvironmentVariable("MSBUILD_EXE_NAME"); + + if (!String.IsNullOrEmpty(msbuildExeName)) + { + // we assume that MSBUILD_EXE_NAME is, in fact, just the name. + msbuildLocation = Path.Combine(msbuildExeName, ".exe"); + } + } + + if (String.IsNullOrEmpty(msbuildLocation)) + { + msbuildLocation = "MSBuild.exe"; + } + + string msbuildName = Path.GetFileNameWithoutExtension(msbuildLocation); + + List nodeProcesses = new List(Process.GetProcessesByName(msbuildName)); + + // Trivial sort to try to prefer most recently used nodes + nodeProcesses.Sort + ( + delegate (Process left, Process right) + { + return left.Id - right.Id; + } + + ); + + CommunicationsUtilities.Trace("Attempting to connect to each existing msbuild.exe process in turn to establish node {0}...", nodeId); + foreach (Process nodeProcess in nodeProcesses) + { + if (nodeProcess.Id == Process.GetCurrentProcess().Id) + { + continue; + } + + // Get the full context of this inspection so that we can always skip this process when we have the same taskhost context + string nodeLookupKey = GetProcessesToIgnoreKey(hostHandshake, clientHandshake, nodeProcess.Id); + if (_processesToIgnore.Contains(nodeLookupKey)) + { + continue; + } + + // We don't need to check this again + _processesToIgnore.Add(nodeLookupKey); + + // Attempt to connect to each process in turn. + NamedPipeClientStream nodeStream = TryConnectToProcess(nodeProcess.Id, 0 /* poll, don't wait for connections */, hostHandshake, clientHandshake); + if (nodeStream != null) + { + // Connection successful, use this node. + CommunicationsUtilities.Trace("Successfully connected to existed node {0} which is PID {1}", nodeId, nodeProcess.Id); + return new NodeContext(nodeId, nodeProcess.Id, nodeStream, factory, terminateNode); + } + } + + // None of the processes we tried to connect to allowed a connection, so create a new one. + // We try this in a loop because it is possible that there is another MSBuild multiproc + // host process running somewhere which is also trying to create nodes right now. It might + // find our newly created node and connect to it before we get a chance. + CommunicationsUtilities.Trace("Could not connect to existing process, now creating a process..."); + int retries = NodeCreationRetries; + while (retries-- > 0) + { + // We will also check to see if .NET 3.5 is installed in the case where we need to launch a CLR2 OOP TaskHost. + // Failure to detect this has been known to stall builds when Windows pops up a related dialog. + // It's also a waste of time when we attempt several times to launch multiple MSBuildTaskHost.exe (CLR2 TaskHost) + // nodes because we should never be able to connect in this case. + string taskHostNameForClr2TaskHost = Path.GetFileNameWithoutExtension(NodeProviderOutOfProcTaskHost.TaskHostNameForClr2TaskHost); + if (msbuildName.Equals(taskHostNameForClr2TaskHost, StringComparison.OrdinalIgnoreCase)) + { + if (FrameworkLocationHelper.GetPathToDotNetFrameworkV35(DotNetFrameworkArchitecture.Current) == null) + { + CommunicationsUtilities.Trace + ( + "Failed to launch node from {0}. The required .NET Framework v3.5 is not installed or enabled. CommandLine: {1}", + msbuildLocation, + commandLineArgs + ); + + string nodeFailedToLaunchError = ResourceUtilities.FormatResourceString("TaskHostNodeFailedToLaunchErrorCodeNet35NotInstalled"); + throw new NodeFailedToLaunchException(null, nodeFailedToLaunchError); + } + } + + // Create the node process + int msbuildProcessId = LaunchNode(msbuildLocation, commandLineArgs); + _processesToIgnore.Add(GetProcessesToIgnoreKey(hostHandshake, clientHandshake, msbuildProcessId)); + + // Note, when running under IMAGEFILEEXECUTIONOPTIONS registry key to debug, the process ID + // gotten back from CreateProcess is that of the debugger, which causes this to try to connect + // to the debugger process. Instead, use MSBUILDDEBUGONSTART=1 + + // Now try to connect to it. + NamedPipeClientStream nodeStream = TryConnectToProcess(msbuildProcessId, TimeoutForNewNodeCreation, hostHandshake, clientHandshake); + if (nodeStream != null) + { + // Connection successful, use this node. + CommunicationsUtilities.Trace("Successfully connected to created node {0} which is PID {1}", nodeId, msbuildProcessId); + return new NodeContext(nodeId, msbuildProcessId, nodeStream, factory, terminateNode); + } + } + + // We were unable to launch a node. + CommunicationsUtilities.Trace("FAILED TO CONNECT TO A CHILD NODE"); + return null; + } + + /// + /// Generate a string from task host context and the remote process to be used as key to lookup processes we have already + /// attempted to connect to or are already connected to + /// + private string GetProcessesToIgnoreKey(long hostHandshake, long clientHandshake, int nodeProcessId) + { + return hostHandshake.ToString(CultureInfo.InvariantCulture) + "|" + clientHandshake.ToString(CultureInfo.InvariantCulture) + "|" + nodeProcessId.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Attempts to connect to the specified process. + /// + private NamedPipeClientStream TryConnectToProcess(int nodeProcessId, int timeout, long hostHandshake, long clientHandshake) + { + // Try and connect to the process. + string pipeName = "MSBuild" + nodeProcessId; + + NamedPipeClientStream nodeStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + CommunicationsUtilities.Trace("Attempting connect to PID {0} with pipe {1} with timeout {2} ms", nodeProcessId, pipeName, timeout); + + try + { + nodeStream.Connect(timeout); + + // Verify that the owner of the pipe is us. This prevents a security hole where a remote node has + // been faked up with ACLs that would let us attach to it. It could then issue fake build requests back to + // us, potentially causing us to execute builds that do harmful or unexpected things. The pipe owner can + // only be set to the user's own SID by a normal, unprivileged process. The conditions where a faked up + // remote node could set the owner to something else would also let it change owners on other objects, so + // this would be a security flaw upstream of us. + SecurityIdentifier identifier = WindowsIdentity.GetCurrent().Owner; + PipeSecurity remoteSecurity = nodeStream.GetAccessControl(); + IdentityReference remoteOwner = remoteSecurity.GetOwner(typeof(SecurityIdentifier)); + if (remoteOwner != identifier) + { + CommunicationsUtilities.Trace("The remote pipe owner {0} does not match {1}", remoteOwner.Value, identifier.Value); + throw new UnauthorizedAccessException(); + } + + CommunicationsUtilities.Trace("Writing handshake to pipe {0}", pipeName); +#if true + nodeStream.WriteLongForHandshake(hostHandshake); +#else + // When the 4th and subsequent node start up, we see this taking a long period of time (0.5s or greater.) This is strictly for debugging purposes. + DateTime writeStart = DateTime.UtcNow; + nodeStream.WriteLong(HostHandshake); + DateTime writeEnd = DateTime.UtcNow; + Console.WriteLine("Node ProcessId {0} WriteLong {1}", nodeProcessId, (writeEnd - writeStart).TotalSeconds); +#endif + CommunicationsUtilities.Trace("Reading handshake from pipe {0}", pipeName); + long handshake = nodeStream.ReadLongForHandshake(); + + if (handshake != clientHandshake) + { + CommunicationsUtilities.Trace("Handshake failed. Received {0} from client not {1}. Probably the client is a different MSBuild build.", handshake, clientHandshake); + throw new InvalidOperationException(); + } + + // We got a connection. + CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", pipeName); + return nodeStream; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Can be: + // UnauthorizedAccessException -- Couldn't connect, might not be a node. + // IOException -- Couldn't connect, already in use. + // TimeoutException -- Couldn't connect, might not be a node. + // InvalidOperationException – Couldn’t connect, probably a different build + CommunicationsUtilities.Trace("Failed to connect to pipe {0}. {1}", pipeName, e.Message.TrimEnd()); + + // If we don't close any stream, we might hang up the child + if (nodeStream != null) + { + nodeStream.Close(); + } + } + + return null; + } + + /// + /// Creates a new MSBuild process + /// + private int LaunchNode(string msbuildLocation, string commandLineArgs) + { + // Should always have been set already. + ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, "msbuildLocation"); + + if (!File.Exists(msbuildLocation)) + { + throw new BuildAbortedException(ResourceUtilities.FormatResourceString("CouldNotFindMSBuildExe", msbuildLocation)); + } + + // Repeat the executable name as the first token of the command line because the command line + // parser logic expects it and will otherwise skip the first argument + commandLineArgs = msbuildLocation + " " + commandLineArgs; + + BackendNativeMethods.STARTUP_INFO startInfo = new BackendNativeMethods.STARTUP_INFO(); + startInfo.cb = Marshal.SizeOf(startInfo); + + // Null out the process handles so that the parent process does not wait for the child process + // to exit before it can exit. + uint creationFlags = BackendNativeMethods.NORMALPRIORITYCLASS; + startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES; + + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW"))) + { + startInfo.hStdError = BackendNativeMethods.InvalidHandle; + startInfo.hStdInput = BackendNativeMethods.InvalidHandle; + startInfo.hStdOutput = BackendNativeMethods.InvalidHandle; + creationFlags = creationFlags | BackendNativeMethods.CREATENOWINDOW; + } + else + { + creationFlags = creationFlags | BackendNativeMethods.CREATE_NEW_CONSOLE; + } + + BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new BackendNativeMethods.SECURITY_ATTRIBUTES(); + BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new BackendNativeMethods.SECURITY_ATTRIBUTES(); + processSecurityAttributes.nLength = Marshal.SizeOf(processSecurityAttributes); + threadSecurityAttributes.nLength = Marshal.SizeOf(threadSecurityAttributes); + + BackendNativeMethods.PROCESS_INFORMATION processInfo = new BackendNativeMethods.PROCESS_INFORMATION(); + + string appName = msbuildLocation; + + CommunicationsUtilities.Trace("Launching node from {0}", msbuildLocation); + bool result = BackendNativeMethods.CreateProcess + ( + msbuildLocation, + commandLineArgs, + ref processSecurityAttributes, + ref threadSecurityAttributes, + false, + creationFlags, + BackendNativeMethods.NullPtr, + null, + ref startInfo, + out processInfo + ); + + if (!result) + { + // Creating an instance of this exception calls GetLastWin32Error and also converts it to a user-friendly string. + System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(); + + CommunicationsUtilities.Trace + ( + "Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {2}", + msbuildLocation, + e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), + e.Message, + commandLineArgs + ); + + throw new NodeFailedToLaunchException(e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), e.Message); + } + + CommunicationsUtilities.Trace("Successfully launched msbuild.exe node with PID {0}", processInfo.dwProcessId); + return processInfo.dwProcessId; + } + + /// + /// Class which wraps up the communications infrastructure for a given node. + /// + internal class NodeContext + { + /// + /// Whether to trace communications. + /// Stored here as a field to avoid a function call when writing packets + /// + private static bool s_trace = String.Equals(Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM"), "1", StringComparison.Ordinal); + + /// + /// The pipe used to communicate with the node. + /// + private NamedPipeClientStream _nodePipe; + + /// + /// The factory used to create packets from data read off the pipe. + /// + private INodePacketFactory _packetFactory; + + /// + /// The node id assigned by the node provider. + /// + private int _nodeId; + + /// + /// The process id + /// + private int _processId; + + /// + /// An array used to store the header byte for each packet when read. + /// + private byte[] _headerByte; + + /// + /// A buffer typically big enough to handle a packet body. + /// + private byte[] _smallReadBuffer; + + /// + /// Event indicating the node has terminated. + /// + private ManualResetEvent _nodeTerminated; + + /// + /// Delegate called when the context terminates. + /// + private NodeContextTerminateDelegate _terminateDelegate; + + /// + /// Per node read buffers + /// + private SharedReadBuffer _sharedReadBuffer; + + /// + /// Constructor. + /// + public NodeContext(int nodeId, int processId, NamedPipeClientStream nodePipe, INodePacketFactory factory, NodeContextTerminateDelegate terminateDelegate) + { + _nodeId = nodeId; + _processId = processId; + _nodePipe = nodePipe; + _packetFactory = factory; + _headerByte = new byte[5]; // 1 for the packet type, 4 for the body length + _smallReadBuffer = new byte[1000]; // 1000 was just an average seen on one profile run. + _nodeTerminated = new ManualResetEvent(false); + _terminateDelegate = terminateDelegate; + _sharedReadBuffer = InterningBinaryReader.CreateSharedBuffer(); + } + + /// + /// Starts a new asynchronous read operation for this node. + /// + public void BeginAsyncPacketRead() + { + _nodePipe.BeginRead(_headerByte, 0, _headerByte.Length, HeaderReadComplete, this); + } + + /// + /// Sends the specified packet to this node. + /// + /// The packet to send. + public void SendData(INodePacket packet) + { + MemoryStream writeStream = new MemoryStream(); + INodePacketTranslator writeTranslator = NodePacketTranslator.GetWriteTranslator(writeStream); + try + { + writeStream.WriteByte((byte)packet.Type); + + // Pad for the packet length + writeStream.Write(BitConverter.GetBytes((int)0), 0, 4); + packet.Translate(writeTranslator); + + // Now plug in the real packet length + writeStream.Position = 1; + writeStream.Write(BitConverter.GetBytes((int)writeStream.Length - 5), 0, 4); + +#if FALSE + if (trace) // Avoid method call + { + CommunicationsUtilities.Trace(nodeId, "Sending Packet of type {0} with length {1}", packet.Type.ToString(), writeStream.Length - 5); + } +#endif + + for (int i = 0; i < writeStream.Length; i += MaxPacketWriteSize) + { + int lengthToWrite = Math.Min((int)writeStream.Length - i, MaxPacketWriteSize); + if ((int)writeStream.Length - i <= MaxPacketWriteSize) + { + // We are done, write the last bit asynchronously. This is actually the general case for + // most packets in the build, and the asynchronous behavior here is desirable. + _nodePipe.BeginWrite(writeStream.GetBuffer(), i, lengthToWrite, PacketWriteComplete, null); + return; + } + else + { + // If this packet is longer that we can write in one go, then we need to break it up. We can't + // return out of this function and let the rest of the system continue because another operation + // might want to send data immediately afterward, and that could result in overlapping writes + // to the pipe on different threads. + IAsyncResult result = _nodePipe.BeginWrite(writeStream.GetBuffer(), i, lengthToWrite, null, null); + _nodePipe.EndWrite(result); + } + } + } + catch (IOException e) + { + // Do nothing here because any exception will be caught by the async read handler + CommunicationsUtilities.Trace(_nodeId, "EXCEPTION in SendData: {0}", e); + } + catch (ObjectDisposedException) // This happens if a child dies unexpectedly + { + // Do nothing here because any exception will be caught by the async read handler + } + } + + /// + /// Closes the node's context, disconnecting it from the node. + /// + public void Close() + { + _nodePipe.Close(); + _terminateDelegate(_nodeId); + } + + /// + /// Completes the asynchronous packet write to the node. + /// + private void PacketWriteComplete(IAsyncResult result) + { + try + { + _nodePipe.EndWrite(result); + } + catch (IOException) + { + // Do nothing here because any exception will be caught by the async read handler + } + } + + /// + /// Callback invoked by the completion of a read of a header byte on one of the named pipes. + /// + private void HeaderReadComplete(IAsyncResult result) + { + int bytesRead; + try + { + try + { + bytesRead = _nodePipe.EndRead(result); + } + + // Workaround for CLR stress bug; it sporadically calls us twice on the same async + // result, and EndRead will throw on the second one. Pretend the second one never happened. + catch (ArgumentException) + { + CommunicationsUtilities.Trace(_nodeId, "Hit CLR bug #825607: called back twice on same async result; ignoring"); + return; + } + + if (bytesRead != _headerByte.Length) + { + CommunicationsUtilities.Trace(_nodeId, "COMMUNICATIONS ERROR (HRC) Node: {0} Process: {1} Bytes Read: {2} Expected: {3}", _nodeId, _processId, bytesRead, _headerByte.Length); + try + { + Process childProcess = Process.GetProcessById(_processId); + if (childProcess == null || childProcess.HasExited) + { + CommunicationsUtilities.Trace(_nodeId, " Child Process {0} has exited.", _processId); + } + else + { + CommunicationsUtilities.Trace(_nodeId, " Child Process {0} is still running.", _processId); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + CommunicationsUtilities.Trace(_nodeId, "Unable to retrieve remote process information. {0}", e); + } + + _packetFactory.RoutePacket(_nodeId, new NodeShutdown(NodeShutdownReason.ConnectionFailed)); + Close(); + return; + } + } + catch (IOException e) + { + CommunicationsUtilities.Trace(_nodeId, "EXCEPTION in HeaderReadComplete: {0}", e); + _packetFactory.RoutePacket(_nodeId, new NodeShutdown(NodeShutdownReason.ConnectionFailed)); + Close(); + return; + } + + NodePacketType packetType = (NodePacketType)_headerByte[0]; + int packetLength = BitConverter.ToInt32(_headerByte, 1); + + byte[] packetData; + if (packetLength < _smallReadBuffer.Length) + { + packetData = _smallReadBuffer; + } + else + { + // Preallocated buffer is not large enough to hold the body. Allocate now, but don't hold it forever. + packetData = new byte[packetLength]; + } + + _nodePipe.BeginRead(packetData, 0, packetLength, BodyReadComplete, new Tuple(packetData, packetLength)); + } + + /// + /// Method called when the body of a packet has been read. + /// + private void BodyReadComplete(IAsyncResult result) + { + NodePacketType packetType = (NodePacketType)_headerByte[0]; + var state = (Tuple)result.AsyncState; + byte[] packetData = state.Item1; + int packetLength = state.Item2; + int bytesRead; + + try + { + try + { + bytesRead = _nodePipe.EndRead(result); + } + + // Workaround for CLR stress bug; it sporadically calls us twice on the same async + // result, and EndRead will throw on the second one. Pretend the second one never happened. + catch (ArgumentException) + { + CommunicationsUtilities.Trace(_nodeId, "Hit CLR bug #825607: called back twice on same async result; ignoring"); + return; + } + + if (bytesRead != packetLength) + { + CommunicationsUtilities.Trace(_nodeId, "Bad packet read for packet {0} - Expected {1} bytes, got {2}", packetType, packetLength, bytesRead); + _packetFactory.RoutePacket(_nodeId, new NodeShutdown(NodeShutdownReason.ConnectionFailed)); + Close(); + return; + } + } + catch (IOException e) + { + CommunicationsUtilities.Trace(_nodeId, "EXCEPTION in BodyReadComplete (Reading): {0}", e); + _packetFactory.RoutePacket(_nodeId, new NodeShutdown(NodeShutdownReason.ConnectionFailed)); + Close(); + return; + } + + // Read and route the packet. + try + { + // The buffer is publicly visible so that InterningBinaryReader doesn't have to copy to an intermediate buffer. + // Since the buffer is publicly visible dispose right away to discourage outsiders from holding a reference to it. + using (var packetStream = new MemoryStream(packetData, 0, packetLength, /*writeable*/ false, /*bufferIsPubliclyVisible*/ true)) + { + INodePacketTranslator readTranslator = NodePacketTranslator.GetReadTranslator(packetStream, _sharedReadBuffer); + _packetFactory.DeserializeAndRoutePacket(_nodeId, packetType, readTranslator); + } + } + catch (IOException e) + { + CommunicationsUtilities.Trace(_nodeId, "EXCEPTION in BodyReadComplete (Routing): {0}", e); + _packetFactory.RoutePacket(_nodeId, new NodeShutdown(NodeShutdownReason.ConnectionFailed)); + Close(); + return; + } + + if (packetType != NodePacketType.NodeShutdown) + { + // Read the next packet. + BeginAsyncPacketRead(); + } + else + { + Close(); + } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs new file mode 100644 index 00000000000..0000db0cfc7 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -0,0 +1,628 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing INodeProvider for out-of-proc nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.IO; +using System.IO.Pipes; +using System.Diagnostics; +using System.Threading; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Security.Permissions; + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Internal; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The provider for out-of-proc nodes. This manages the lifetime of external MSBuild.exe processes + /// which act as child nodes for the build system. + /// + internal class NodeProviderOutOfProcTaskHost : NodeProviderOutOfProcBase, INodeProvider, INodePacketFactory, INodePacketHandler + { + /// + /// The maximum number of nodes that this provider supports. Should + /// always be equivalent to the number of different TaskHostContexts + /// that exist. + /// + private const int MaxNodeCount = 4; + + /// + /// Store the path for MSBuild / MSBuildTaskHost so that we don't have to keep recalculating it. + /// + private static string s_baseTaskHostPath; + + /// + /// Store the 64-bit path for MSBuild / MSBuildTaskHost so that we don't have to keep recalculating it. + /// + private static string s_baseTaskHostPath64; + + /// + /// Store the path for the 32-bit MSBuildTaskHost so that we don't have to keep re-calculating it. + /// + private static string s_pathToX32Clr2; + + /// + /// Store the path for the 64-bit MSBuildTaskHost so that we don't have to keep re-calculating it. + /// + private static string s_pathToX64Clr2; + + /// + /// Store the path for the 32-bit MSBuild so that we don't have to keep re-calculating it. + /// + private static string s_pathToX32Clr4; + + /// + /// Store the path for the 64-bit MSBuild so that we don't have to keep re-calculating it. + /// + private static string s_pathToX64Clr4; + + /// + /// Name for MSBuild.exe + /// + private static string s_msbuildName; + + /// + /// Name for MSBuildTaskHost.exe + /// + private static string s_msbuildTaskHostName; + + /// + /// Are there any active nodes? + /// + private ManualResetEvent _noNodesActiveEvent; + + /// + /// A mapping of all the nodes managed by this provider. + /// + private Dictionary _nodeContexts; + + /// + /// A mapping of all of the INodePacketFactories wrapped by this provider. + /// + private IDictionary _nodeIdToPacketFactory; + + /// + /// A mapping of all of the INodePacketHandlers wrapped by this provider. + /// + private IDictionary _nodeIdToPacketHandler; + + /// + /// Keeps track of the set of nodes for which we have not yet received shutdown notification. + /// + private HashSet _activeNodes; + + /// + /// Packet factory we use if there's not already one associated with a particular context. + /// + private NodePacketFactory _localPacketFactory; + + /// + /// Constructor. + /// + private NodeProviderOutOfProcTaskHost() + { + } + + #region INodeProvider Members + + /// + /// Returns the node provider type. + /// + public NodeProviderType ProviderType + { + [DebuggerStepThrough] + get + { return NodeProviderType.OutOfProc; } + } + + /// + /// Returns the number of available nodes. + /// + public int AvailableNodes + { + get + { + return MaxNodeCount - _nodeContexts.Count; + } + } + + /// + /// Returns the name of the CLR2 Task Host executable + /// + internal static string TaskHostNameForClr2TaskHost + { + get + { + if (s_msbuildTaskHostName == null) + { + s_msbuildTaskHostName = Environment.GetEnvironmentVariable("MSBUILDTASKHOST_EXE_NAME"); + + if (s_msbuildTaskHostName == null) + { + s_msbuildTaskHostName = "MSBuildTaskHost.exe"; + } + } + + return s_msbuildTaskHostName; + } + } + + /// + /// Instantiates a new MSBuild process acting as a child node. + /// + public bool CreateNode(int nodeId, INodePacketFactory factory, NodeConfiguration configuration) + { + throw new NotImplementedException("Use the other overload of CreateNode instead"); + } + + /// + /// Sends data to the specified node. + /// + /// The node to which data shall be sent. + /// The packet to send. + public void SendData(int nodeId, INodePacket packet) + { + throw new NotImplementedException("Use the other overload of SendData instead"); + } + + /// + /// Sends data to the specified node. + /// + /// The node to which data shall be sent. + /// The packet to send. + public void SendData(TaskHostContext hostContext, INodePacket packet) + { + ErrorUtilities.VerifyThrow(_nodeContexts.ContainsKey(hostContext), "Invalid host context specified: {0}.", hostContext.ToString()); + + SendData(_nodeContexts[hostContext], packet); + } + + /// + /// Shuts down all of the connected managed nodes. + /// + /// Flag indicating if nodes should prepare for reuse. + public void ShutdownConnectedNodes(bool enableReuse) + { + // Send the build completion message to the nodes, causing them to shutdown or reset. + List contextsToShutDown; + + lock (_nodeContexts) + { + contextsToShutDown = new List(_nodeContexts.Values); + } + + ShutdownConnectedNodes(contextsToShutDown, enableReuse); + + _noNodesActiveEvent.WaitOne(); + } + + /// + /// Shuts down all of the managed nodes permanently. + /// + public void ShutdownAllNodes() + { + ShutdownAllNodes(NodeProviderOutOfProc.HostHandshake, NodeProviderOutOfProc.ClientHandshake, NodeContextTerminated); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Initializes the component. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + this.ComponentHost = host; + _nodeContexts = new Dictionary(); + _nodeIdToPacketFactory = new Dictionary(); + _nodeIdToPacketHandler = new Dictionary(); + _activeNodes = new HashSet(); + + _noNodesActiveEvent = new ManualResetEvent(true); + _localPacketFactory = new NodePacketFactory(); + + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostTaskComplete, TaskHostTaskComplete.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this); + } + + /// + /// Shuts down the component + /// + public void ShutdownComponent() + { + } + + #endregion + + #region INodePacketFactory Members + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _localPacketFactory.RegisterPacketHandler(packetType, factory, handler); + } + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + public void UnregisterPacketHandler(NodePacketType packetType) + { + _localPacketFactory.UnregisterPacketHandler(packetType); + } + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + if (_nodeIdToPacketFactory.ContainsKey(nodeId)) + { + _nodeIdToPacketFactory[nodeId].DeserializeAndRoutePacket(nodeId, packetType, translator); + } + else + { + _localPacketFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + } + } + + /// + /// Routes the specified packet + /// + /// The node from which the packet was received. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + { + if (_nodeIdToPacketFactory.ContainsKey(nodeId)) + { + _nodeIdToPacketFactory[nodeId].RoutePacket(nodeId, packet); + } + else + { + _localPacketFactory.RoutePacket(nodeId, packet); + } + } + + #endregion + + #region INodePacketHandler Members + + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + public void PacketReceived(int node, INodePacket packet) + { + if (_nodeIdToPacketHandler.ContainsKey(node)) + { + _nodeIdToPacketHandler[node].PacketReceived(node, packet); + } + else + { + ErrorUtilities.VerifyThrow(packet.Type == NodePacketType.NodeShutdown, "We should only ever handle packets of type NodeShutdown -- everything else should only come in when there's an active task"); + + // May also be removed by unnatural termination, so don't assume it's there + lock (_activeNodes) + { + if (_activeNodes.Contains(node)) + { + _activeNodes.Remove(node); + } + + if (_activeNodes.Count == 0) + { + _noNodesActiveEvent.Set(); + } + } + } + } + + #endregion + + /// + /// Static factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType componentType) + { + ErrorUtilities.VerifyThrow(componentType == BuildComponentType.OutOfProcTaskHostNodeProvider, "Factory cannot create components of type {0}", componentType); + return new NodeProviderOutOfProcTaskHost(); + } + + /// + /// Clears out our cached values for the various task host names and paths. + /// FOR UNIT TESTING ONLY + /// + internal static void ClearCachedTaskHostPaths() + { + s_msbuildName = null; + s_msbuildTaskHostName = null; + s_pathToX32Clr2 = null; + s_pathToX32Clr4 = null; + s_pathToX64Clr2 = null; + s_pathToX64Clr4 = null; + s_baseTaskHostPath = null; + s_baseTaskHostPath64 = null; + } + + /// + /// Given a TaskHostContext, returns the name of the executable we should be searching for. + /// + internal static string GetTaskHostNameFromHostContext(TaskHostContext hostContext) + { + if (hostContext == TaskHostContext.X64CLR4 || hostContext == TaskHostContext.X32CLR4) + { + if (s_msbuildName == null) + { + s_msbuildName = Environment.GetEnvironmentVariable("MSBUILD_EXE_NAME"); + + if (s_msbuildName == null) + { + s_msbuildName = "MSBuild.exe"; + } + } + + return s_msbuildName; + } + else if (hostContext == TaskHostContext.X32CLR2 || hostContext == TaskHostContext.X64CLR2) + { + return TaskHostNameForClr2TaskHost; + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } + + /// + /// Given a TaskHostContext, return the appropriate location of the + /// executable (MSBuild or MSBuildTaskHost) that we wish to use, or null + /// if that location cannot be resolved. + /// + internal static string GetMSBuildLocationFromHostContext(TaskHostContext hostContext) + { + string toolName = GetTaskHostNameFromHostContext(hostContext); + string toolPath = null; + + if (s_baseTaskHostPath == null) + { + Toolset currentToolset = ProjectCollection.GlobalProjectCollection.GetToolset(MSBuildConstants.CurrentToolsVersion); + + if (currentToolset != null) + { + ProjectPropertyInstance toolsPath32 = null; + + if (!currentToolset.Properties.TryGetValue("MSBuildToolsPath32", out toolsPath32)) + { + // ... This is a very weird toolset. But try falling back to the base ToolsPath since we + // know for a fact that that will always exist. + s_baseTaskHostPath = currentToolset.ToolsPath; + } + else + { + s_baseTaskHostPath = toolsPath32.EvaluatedValue; + } + } + } + + if (s_baseTaskHostPath64 == null && s_baseTaskHostPath != null) + { + s_baseTaskHostPath64 = Path.Combine(s_baseTaskHostPath, "amd64"); + if (!Directory.Exists(s_baseTaskHostPath64)) + { + // time to give up + s_baseTaskHostPath64 = null; + } + } + + switch (hostContext) + { + case TaskHostContext.X32CLR2: + if (s_pathToX32Clr2 == null) + { + s_pathToX32Clr2 = Environment.GetEnvironmentVariable("MSBUILDTASKHOSTLOCATION"); + if (s_pathToX32Clr2 == null || !FileUtilities.FileExistsNoThrow(Path.Combine(s_pathToX32Clr2, toolName))) + { + s_pathToX32Clr2 = s_baseTaskHostPath; + } + } + + toolPath = s_pathToX32Clr2; + break; + case TaskHostContext.X64CLR2: + if (s_pathToX64Clr2 == null) + { + s_pathToX64Clr2 = Environment.GetEnvironmentVariable("MSBUILDTASKHOSTLOCATION64"); + + if (s_pathToX64Clr2 == null || !FileUtilities.FileExistsNoThrow(Path.Combine(s_pathToX64Clr2, toolName))) + { + s_pathToX64Clr2 = s_baseTaskHostPath64; + } + } + + toolPath = s_pathToX64Clr2; + break; + case TaskHostContext.X32CLR4: + if (s_pathToX32Clr4 == null) + { + s_pathToX32Clr4 = s_baseTaskHostPath; + } + + toolPath = s_pathToX32Clr4; + break; + case TaskHostContext.X64CLR4: + if (s_pathToX64Clr4 == null) + { + s_pathToX64Clr4 = s_baseTaskHostPath64; + } + + toolPath = s_pathToX64Clr4; + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + + if (toolName != null && toolPath != null) + { + return Path.Combine(toolPath, toolName); + } + else + { + return null; + } + } + + /// + /// Make sure a node in the requested context exists. + /// + internal bool AcquireAndSetUpHost(TaskHostContext hostContext, INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration) + { + NodeContext context = null; + bool nodeCreationSucceeded = false; + + if (!(_nodeContexts.TryGetValue(hostContext, out context))) + { + nodeCreationSucceeded = CreateNode(hostContext, factory, handler, configuration); + } + else + { + // node already exists, so "creation" automatically succeeded + nodeCreationSucceeded = true; + } + + if (nodeCreationSucceeded) + { + context = _nodeContexts[hostContext]; + _nodeIdToPacketFactory[(int)hostContext] = factory; + _nodeIdToPacketHandler[(int)hostContext] = handler; + + // Configure the node. + context.SendData(configuration); + return true; + } + + return false; + } + + /// + /// Expected to be called when TaskHostTask is done with host of the given context. + /// + internal void DisconnectFromHost(TaskHostContext hostContext) + { + ErrorUtilities.VerifyThrow(_nodeIdToPacketFactory.ContainsKey((int)hostContext) && _nodeIdToPacketHandler.ContainsKey((int)hostContext), "Why are we trying to disconnect from a context that we already disconnected from? Did we call DisconnectFromHost twice?"); + + _nodeIdToPacketFactory.Remove((int)hostContext); + _nodeIdToPacketHandler.Remove((int)hostContext); + } + + /// + /// Instantiates a new MSBuild or MSBuildTaskHost process acting as a child node. + /// + internal bool CreateNode(TaskHostContext hostContext, INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration) + { + ErrorUtilities.VerifyThrowArgumentNull(factory, "factory"); + ErrorUtilities.VerifyThrow(!_nodeIdToPacketFactory.ContainsKey((int)hostContext), "We should not already have a factory for this context! Did we forget to call DisconnectFromHost somewhere?"); + + if (AvailableNodes == 0) + { + ErrorUtilities.ThrowInternalError("All allowable nodes already created ({0}).", _nodeContexts.Count); + return false; + } + + // Start the new process. We pass in a node mode with a node number of 2, to indicate that we + // want to start up an MSBuild task host node. + string commandLineArgs = " /nologo /nodemode:2 "; + string msbuildLocation = GetMSBuildLocationFromHostContext(hostContext); + + // we couldn't even figure out the location we're trying to launch ... just go ahead and fail. + if (msbuildLocation == null) + { + return false; + } + + CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext.ToString(), msbuildLocation ?? "MSBuild.exe"); + + // Make it here. + NodeContext context = GetNode + ( + msbuildLocation, + commandLineArgs, + (int)hostContext, + this, + CommunicationsUtilities.GetTaskHostHostHandshake(hostContext), + CommunicationsUtilities.GetTaskHostClientHandshake(hostContext), + NodeContextTerminated + ); + + if (null != context) + { + _nodeContexts[hostContext] = context; + + // Start the asynchronous read. + context.BeginAsyncPacketRead(); + + _activeNodes.Add((int)hostContext); + _noNodesActiveEvent.Reset(); + + return true; + } + + return false; + } + + /// + /// Method called when a context terminates. + /// + private void NodeContextTerminated(int nodeId) + { + lock (_nodeContexts) + { + _nodeContexts.Remove((TaskHostContext)nodeId); + } + + // May also be removed by unnatural termination, so don't assume it's there + lock (_activeNodes) + { + if (_activeNodes.Contains(nodeId)) + { + _activeNodes.Remove(nodeId); + } + + if (_activeNodes.Count == 0) + { + _noNodesActiveEvent.Set(); + } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Communications/TaskHostNodeManager.cs b/src/XMakeBuildEngine/BackEnd/Components/Communications/TaskHostNodeManager.cs new file mode 100644 index 00000000000..51524e1359b --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Communications/TaskHostNodeManager.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The class which manages task host nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The NodeManager class is responsible for marshalling data to/from the NodeProviders and organizing the + /// creation of new nodes on request. + /// + internal class TaskHostNodeManager : INodeManager + { + /// + /// The node provider for task hosts. + /// + private INodeProvider _outOfProcTaskHostNodeProvider; + + /// + /// The build component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// Tracks whether ShutdownComponent has been called. + /// + private bool _componentShutdown; + + /// + /// Constructor. + /// + private TaskHostNodeManager() + { + // do nothing + } + + #region INodeManager Members + + /// + /// Creates a node on an available NodeProvider, if any.. + /// + /// The configuration to use for the remote node. + /// A NodeInfo describing the node created, or null if none could be created. + public NodeInfo CreateNode(NodeConfiguration configuration, NodeAffinity nodeAffinity) + { + throw new NotSupportedException("not used"); + } + + /// + /// Sends data to the specified node. + /// + /// The node. + /// The packet to send. + public void SendData(int node, INodePacket packet) + { + throw new NotSupportedException("not used"); + } + + /// + /// Shuts down all of the connected managed nodes. + /// + /// Flag indicating if nodes should prepare for reuse. + public void ShutdownConnectedNodes(bool enableReuse) + { + ErrorUtilities.VerifyThrow(!_componentShutdown, "We should never be calling ShutdownNodes after ShutdownComponent has been called"); + + if (null != _outOfProcTaskHostNodeProvider) + { + _outOfProcTaskHostNodeProvider.ShutdownConnectedNodes(enableReuse); + } + } + + /// + /// Shuts down all of the managed nodes permanently. + /// + public void ShutdownAllNodes() + { + if (null != _outOfProcTaskHostNodeProvider) + { + _outOfProcTaskHostNodeProvider.ShutdownAllNodes(); + } + } + #endregion + + #region IBuildComponent Members + + /// + /// Initializes the component + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrow(_componentHost == null, "TaskHostNodeManager already initialized."); + ErrorUtilities.VerifyThrow(host != null, "We can't create a TaskHostNodeManager with a null componentHost"); + + _componentHost = host; + _outOfProcTaskHostNodeProvider = _componentHost.GetComponent(BuildComponentType.OutOfProcTaskHostNodeProvider) as INodeProvider; + _componentShutdown = false; + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + _outOfProcTaskHostNodeProvider = null; + _componentHost = null; + _componentShutdown = true; + + ClearPerBuildState(); + } + + /// + /// Reset the state of objects in the node manager which need to be reset between builds. + /// + public void ClearPerBuildState() + { + // do nothing + } + + #endregion + + #region INodePacketFactory Members + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + throw new NotSupportedException("not used"); + } + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + public void UnregisterPacketHandler(NodePacketType packetType) + { + throw new NotSupportedException("not used"); + } + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + throw new NotSupportedException("not used"); + } + + /// + /// Routes the specified packet. This is called by the Inproc node directly since it does not have to do any deserialization + /// + /// The node from which the packet was received. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + { + throw new NotSupportedException("not used"); + } + + #endregion + + /// + /// Factory for component creation. + /// + static internal IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.TaskHostNodeManager, "Cannot create component of type {0}", type); + return new TaskHostNodeManager(); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/IBuildComponent.cs b/src/XMakeBuildEngine/BackEnd/Components/IBuildComponent.cs new file mode 100644 index 00000000000..452f5d19769 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/IBuildComponent.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Interface for build components. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Each component in the build system which is registered with the Build Manager or a Node must + /// implement this interface. + /// + /// REFACTOR: Maybe these could all implement IDisposable. + /// + internal interface IBuildComponent + { + /// + /// Called by the build component host when a component is first initialized + /// + /// The host for the component. + void InitializeComponent(IBuildComponentHost host); + + /// + /// Called by the build component host when the component host is about to shutdown + /// + void ShutdownComponent(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/IBuildComponentHost.cs b/src/XMakeBuildEngine/BackEnd/Components/IBuildComponentHost.cs new file mode 100644 index 00000000000..1f2c36cd8a4 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/IBuildComponentHost.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Interface for the build component host. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using BuildParameters = Microsoft.Build.Execution.BuildParameters; +using LegacyThreadingData = Microsoft.Build.Execution.LegacyThreadingData; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Factory delegate which instantiates a component of the type specified. + /// + /// The type of component to be instantiated. + /// An instance of the component. + internal delegate IBuildComponent BuildComponentFactoryDelegate(BuildComponentType type); + + /// + /// An enumeration of all component types recognized by the system + /// + internal enum BuildComponentType + { + /// + /// Request Manager + /// + RequestManager, + + /// + /// Scheduler + /// + Scheduler, + + /// + /// Results Cache + /// + ResultsCache, + + /// + /// Property Cache + /// + PropertyCache, + + /// + /// The Build Request Configuration Cache + /// + ConfigCache, + + /// + /// Node Manager + /// + NodeManager, + + /// + /// InProcNodeProvider + /// + InProcNodeProvider, + + /// + /// OutOfProcNodeProvider + /// + OutOfProcNodeProvider, + + /// + /// RemoteNodeProvider + /// + RemoteNodeProvider, + + /// + /// Node packet factory + /// + NodePacketFactory, + + /// + /// Request engine + /// + RequestEngine, + + /// + /// File monitor + /// + FileMonitor, + + /// + /// The endpoint on a Node + /// + NodeEndpoint, + + /// + /// The logging service + /// + LoggingService, + + /// + /// The component responsible for building requests. + /// + RequestBuilder, + + /// + /// The component responsible for building targets. + /// + TargetBuilder, + + /// + /// The component responsible for building tasks. + /// + TaskBuilder, + + /// + /// The component which is responsible for providing test data to the variour components + /// + TestDataProvider, + + /// + /// OutOfProcTaskHostNodeProvider + /// + OutOfProcTaskHostNodeProvider, + + /// + /// Node manager for task host nodes + /// + TaskHostNodeManager, + + /// + /// The cache of registered disposable objects. + /// + RegisteredTaskObjectCache, + } + + /// + /// This interface is implemented by objects which host build components. + /// + internal interface IBuildComponentHost + { + #region Methods + + /// + /// Retrieves the name of the host. + /// + string Name + { + get; + } + + /// + /// Retrieves the BuildParameters used during the build. + /// + BuildParameters BuildParameters + { + get; + } + + /// + /// The data structure which holds the data for the use of legacy threading semantics + /// + LegacyThreadingData LegacyThreadingData + { + get; + } + + /// + /// Retrieves the logging service associated with a particular build + /// + ILoggingService LoggingService + { + get; + } + + /// + /// Registers a factory which will be used to create the necessary components of the build + /// system. + /// + /// The type which is created by this factory. + /// The factory to be registered. + /// + /// It is not necessary to register any factories. If no factory is registered for a specific kind + /// of object, the system will use the default factory. + /// + void RegisterFactory(BuildComponentType factoryType, BuildComponentFactoryDelegate factory); + + /// + /// Gets an instance of the specified component type from the host. + /// + /// The component type to be retrieved + /// The component + IBuildComponent GetComponent(BuildComponentType type); + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/BaseLoggingContext.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/BaseLoggingContext.cs new file mode 100644 index 00000000000..c9fe4a6ba7e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/BaseLoggingContext.cs @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A base class providing type-safe methods of logging in various contexts. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// This object encapsulates the logging service plus the current BuildEventContext and + /// hides the requirement to pass BuildEventContexts to the logging service or query the + /// host for the logging service all of the time. + /// + /// Its intended use is in the nodes, where a base LoggingContext is created when the node + /// initializes for a build (this is the public constructor.) When a new project, target batch + /// or task batch is started, the appropriate method on the current LoggingContext is invoked + /// and a new LoggingContext is returned. This new LoggingContext should be used for all + /// subsequent logging within the subcontext. + /// + internal class BaseLoggingContext + { + /// + /// The logging service to which this context is attached + /// + private ILoggingService _loggingService; + + /// + /// The build event context understood by the logging service. + /// + private BuildEventContext _eventContext; + + /// + /// True if this context is still valid (i.e. hasn't been "finished") + /// + private bool _isValid; + + /// + /// True if this context comes from the in-proc node. + /// + private bool _isInProcNode; + + /// + /// Constructs the logging context from a logging service and an event context. + /// + /// The logging service to use + /// The event context + /// Flag indicating if this context belongs to an in-proc node. + protected BaseLoggingContext(ILoggingService loggingService, BuildEventContext eventContext, bool inProc) + { + ErrorUtilities.VerifyThrowArgumentNull(loggingService, "loggingService"); + ErrorUtilities.VerifyThrowArgumentNull(eventContext, "eventContext"); + + _loggingService = loggingService; + _eventContext = eventContext; + _isValid = false; + _isInProcNode = inProc; + } + + /// + /// Constructs a logging context from another logging context. This is used primarily in + /// the constructors for other logging contexts to populate the logging service parameter, + /// while the event context will come from a call into the logging service itself. + /// + /// The context from which this context is being created. + protected BaseLoggingContext(BaseLoggingContext baseContext) + { + _loggingService = baseContext._loggingService; + _eventContext = null; + _isValid = baseContext._isValid; + _isInProcNode = baseContext._isInProcNode; + } + + /// + /// Retrieves the logging service + /// + public ILoggingService LoggingService + { + [DebuggerStepThrough] + get + { return _loggingService; } + } + + /// + /// Retrieves the build event context + /// UNDONE: (Refactor) We eventually want to remove this because all logging should go + /// through a context object. This exists only so we can make certain + /// logging calls in code which has not yet been fully refactored. + /// + public BuildEventContext BuildEventContext + { + [DebuggerStepThrough] + get + { + return _eventContext; + } + + protected set + { + ErrorUtilities.VerifyThrow(_eventContext == null, "eventContext should be null"); + _eventContext = value; + } + } + + /// + /// Returns true if the context is still valid, false if the + /// appropriate 'Finished' call has been invoked. + /// + public bool IsValid + { + [DebuggerStepThrough] + get + { + return _isValid; + } + + [DebuggerStepThrough] + protected set + { + _isValid = value; + } + } + + /// + /// Flag indicating if the context is being used for the in-proc node. + /// + public bool IsInProcNode + { + [DebuggerStepThrough] + get + { return _isInProcNode; } + } + #region Log comments + /// + /// Helper method to create a message build event from a string resource and some parameters + /// + /// Importance level of the message + /// string within the resource which indicates the format string to use + /// string resource arguments + internal void LogComment(MessageImportance importance, string messageResourceName, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogComment(_eventContext, importance, messageResourceName, messageArgs); + } + + /// + /// Helper method to create a message build event from a string + /// + /// Importance level of the message + /// message to log + internal void LogCommentFromText(MessageImportance importance, string message) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogCommentFromText(_eventContext, importance, message); + } + #endregion + + #region Log events + /// + /// Will Log a build Event. Will also take into account OnlyLogCriticalEvents when determining if to drop the event or to log it. + /// + /// The event to log + internal void LogBuildEvent(BuildEventArgs buildEvent) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogBuildEvent(buildEvent); + } + + #endregion + + #region Log errors + /// + /// Log an error + /// + /// The file in which the error occurred + /// The resource name for the error + /// Parameters for the resource string + internal void LogError(BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogError(_eventContext, file, messageResourceName, messageArgs); + } + + /// + /// Log an error + /// + /// The file in which the error occurred + /// The resource name which indicates the subCategory + /// The resource name for the error + /// Parameters for the resource string + internal void LogErrorWithSubcategory(BuildEventFileInfo file, string subcategoryResourceName, string messageResourceName, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogError(_eventContext, subcategoryResourceName, file, messageResourceName, messageArgs); + } + + /// + /// Log an error + /// + /// The file in which the error occurred + /// The resource name which indicates the subCategory + /// Error code + /// Help keyword + /// Error message + internal void LogErrorFromText(BuildEventFileInfo file, string subcategoryResourceName, string errorCode, string helpKeyword, string message) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogErrorFromText(_eventContext, subcategoryResourceName, errorCode, helpKeyword, file, message); + } + + /// + /// Log an invalid project file exception + /// + /// The invalid Project File Exception which is to be logged + internal void LogInvalidProjectFileError(InvalidProjectFileException invalidProjectFileException) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogInvalidProjectFileError(_eventContext, invalidProjectFileException); + } + + /// + /// Log an error based on an exception + /// + /// The file in which the error occurred + /// The exception wich is to be logged + internal void LogFatalBuildError(BuildEventFileInfo file, Exception exception) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogFatalBuildError(_eventContext, exception, file); + } + + /// + /// Log an error based on an exception during the execution of a task + /// + /// The file in which the error occurred + /// The exception wich is to be logged + /// The task in which the error occurred + internal void LogFatalTaskError(BuildEventFileInfo file, Exception exception, string taskName) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogFatalTaskError(_eventContext, exception, file, taskName); + } + + /// + /// Log an error based on an exception + /// + /// The file in which the error occurred + /// The exception wich is to be logged + /// The string resource which has the formatting string for the error + /// The arguments for the error message + internal void LogFatalError(BuildEventFileInfo file, Exception exception, string messageResourceName, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogFatalError(_eventContext, exception, file, messageResourceName, messageArgs); + } + + #endregion + + #region Log warnings + /// + /// Log a warning based on an exception + /// + /// The file in which the warning occurred + /// The exception to be logged as a warning + /// The task in which the warning occurred + internal void LogTaskWarningFromException(BuildEventFileInfo file, Exception exception, string taskName) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogTaskWarningFromException(_eventContext, exception, file, taskName); + } + + /// + /// Log a warning + /// + /// The file in which the warning occurred + /// The subcategory resource name + /// The string resource which contains the formatted warning string + /// parameters for the string resource + internal void LogWarning(BuildEventFileInfo file, string subcategoryResourceName, string messageResourceName, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogWarning(_eventContext, subcategoryResourceName, file, messageResourceName, messageArgs); + } + + /// + /// Log a warning based on a text message + /// + /// The file in which the warning occurred + /// The subcategory resource name + /// Warning code + /// Help keyword + /// The message to be logged as a warning + internal void LogWarningFromText(BuildEventFileInfo file, string subcategoryResourceName, string warningCode, string helpKeyword, string message) + { + ErrorUtilities.VerifyThrow(_isValid, "must be valid"); + _loggingService.LogWarningFromText(_eventContext, subcategoryResourceName, warningCode, helpKeyword, file, message); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/BuildEventArgTransportSink.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/BuildEventArgTransportSink.cs new file mode 100644 index 00000000000..a82766f51f2 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/BuildEventArgTransportSink.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// BuildEventArg sink which will consume a buildEventArg, wrap it in a INodePacket, and +// send it to the transport component to be sent accross the wire +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Delegate to a method which will transport the packet accross the wire. + /// + /// A node packet to send accross the wire + internal delegate void SendDataDelegate(INodePacket packetToSend); + + /// + /// This class will consume the BuildEventArgs forwarded by the EventRedirectorToSink class. + /// The sink will then create a packet and then pass this along to the transport layer to be + /// sent back to the build manager. + /// + internal class BuildEventArgTransportSink : IBuildEventSink + { + #region Data + /// + /// Delegate to a method which accepts a INodePacket and send the packet to where it needs to go + /// + private SendDataDelegate _sendDataDelegate; + #endregion + + #region Constructor + + /// + /// Create the sink which will consume a buildEventArg + /// Create a INodePacket and send it to the transport component + /// + /// A delegate which takes an INodePacket and sends the packet to where it needs to go + /// Send data delegate is null + internal BuildEventArgTransportSink(SendDataDelegate sendData) + { + ErrorUtilities.VerifyThrow(sendData != null, "sendData delegate is null"); + _sendDataDelegate = sendData; + } + + #endregion + + #region Properties + /// + /// Provide a friendly name for the sink to make it easier to differentiate during + /// debugging and display + /// + public string Name + { + get; + set; + } + + /// + /// Has the sink logged the BuildStartedEvent. This is important to know because we only want to log the build started event once + /// + public bool HaveLoggedBuildStartedEvent + { + get; + set; + } + + /// + /// Has the sink logged the BuildFinishedEvent. This is important to know because we only want to log the build finished event once + /// + public bool HaveLoggedBuildFinishedEvent + { + get; + set; + } + + #endregion + #region IBuildEventSink Methods + + /// + /// This method should not be used since we need the sinkID + /// + public void Consume(BuildEventArgs buildEvent) + { + ErrorUtilities.VerifyThrow(false, "Do not use this method for the transport sink"); + } + + /// + /// Consumes the buildEventArg and creates a logMessagePacket + /// + /// Build event to package into a INodePacket + /// buildEvent is null + public void Consume(BuildEventArgs buildEvent, int sinkId) + { + ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + if (buildEvent is BuildStartedEventArgs) + { + HaveLoggedBuildStartedEvent = true; + return; + } + else if (buildEvent is BuildFinishedEventArgs) + { + HaveLoggedBuildFinishedEvent = true; + return; + } + + LogMessagePacket logPacket = new LogMessagePacket(new KeyValuePair(sinkId, buildEvent)); + _sendDataDelegate(logPacket); + } + + /// + /// Dispose of any resources the sink is holding onto. + /// + public void ShutDown() + { + _sendDataDelegate = null; + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/CentralForwardingLogger.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/CentralForwardingLogger.cs new file mode 100644 index 00000000000..64f05fc4bd2 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/CentralForwardingLogger.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Forwarding logger which forwards all events +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Text; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Logger that forwards events to loggers registered with the LoggingServices + /// + internal class CentralForwardingLogger : IForwardingLogger + { + #region Properties + + /// + /// An IEventRedirector which will redirect any events forwarded from the logger. The eventRedirector determines where the events will + /// be redirected. + /// + public IEventRedirector BuildEventRedirector + { + get; + set; + } + + /// + /// The nodeId of the node on which the logger is currently running. + /// + public int NodeId + { + get; + set; + } + + /// + /// Verbosity of the logger, in the central forwarding logger this is currently ignored + /// + public LoggerVerbosity Verbosity + { + get; + set; + } + + /// + /// Logging Parameters + /// + public string Parameters + { + get; + set; + } + + #endregion + + #region Members + + #region Public + + /// + /// Initialize the logger with an eventSource and a node count. + /// The logger will register and listen to anyEvents on the eventSource. + /// The node count is for informational purposes. The logger may want to take different + /// actions depending on how many nodes there are in the system. + /// + /// Event source which the logger will register with to receive events + /// Number of nodes the system was started with + public void Initialize(IEventSource eventSource, int nodeCount) + { + Initialize(eventSource); + } + + /// + /// Initialize the logger. The logger will register with AnyEventRaised on the eventSource + /// + /// eventSource which the logger will register on to listen for events + /// EventSource is null + public void Initialize(IEventSource eventSource) + { + ErrorUtilities.VerifyThrow(eventSource != null, "eventSource is null"); + eventSource.AnyEventRaised += new AnyEventHandler(EventSource_AnyEventRaised); + } + + /// + /// Shuts down the logger. This will null out the buildEventRedirector + /// + public void Shutdown() + { + BuildEventRedirector = null; + } + + #endregion + + #region Private + + /// + /// Forwards any event raised to the BuildEventRedirector, this redirector will send the event on a path which will + /// take it to a logger. + /// + /// Who sent the message, this is not used + /// BuildEvent to forward + private void EventSource_AnyEventRaised(object sender, BuildEventArgs buildEvent) + { + // If no central logger was registered with the system + // there will not be a build event redirector as there is + // nowhere to forward the events to. + if (BuildEventRedirector != null) + { + BuildEventRedirector.ForwardEvent(buildEvent); + } + } + + #endregion + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/EventRedirectorToSink.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/EventRedirectorToSink.cs new file mode 100644 index 00000000000..7e884d1c5ab --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/EventRedirectorToSink.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// NodePackets which are used for node communication +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Will redirect events from forwarding loggers to the a IBuildEventSink, many redirectors may redirect to one sink. + /// + internal class EventRedirectorToSink : IEventRedirector + { + #region Data + /// + /// The Id of the central logger to which this event should be forwarded + /// + private int _centralLoggerId; + + /// + /// The sink which will consume the messages + /// + private IBuildEventSink _sink; + #endregion + + #region Constructors + /// + /// Initalize this class with a central logger id identifying the central logger to which + /// these events should consumed by. The redirector will send the messages to the registered sink to + /// be consumed + /// + /// Id which will be attached to the build event arguments to indicate which logger the events came from + /// sink which will initially consume the events + /// Eventsink is null + /// LoggerId is less than 0 + internal EventRedirectorToSink(int loggerId, IBuildEventSink eventSink) + { + ErrorUtilities.VerifyThrow(eventSink != null, "eventSink is null"); + ErrorUtilities.VerifyThrow(loggerId >= 0, "loggerId should be greater or equal to 0"); + _centralLoggerId = loggerId; + _sink = eventSink; + } + #endregion + + #region IEventRedirector Methods + + /// + /// This method is called by the node loggers to forward the events to cenral logger + /// + /// Build event to forward + /// BuildEvent is null + void IEventRedirector.ForwardEvent(BuildEventArgs buildEvent) + { + ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + _sink.Consume(buildEvent, _centralLoggerId); + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/EventSourceSink.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/EventSourceSink.cs new file mode 100644 index 00000000000..60401f8d498 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/EventSourceSink.cs @@ -0,0 +1,846 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Sink which will take in a build event and raiise it on its internal event source +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// This class raises events on behalf of the build engine to all registered loggers. + /// + internal sealed class EventSourceSink : MarshalByRefObject, IEventSource, IBuildEventSink + { + #region Events + + /// + /// This event is raised to log a message. + /// + public event BuildMessageEventHandler MessageRaised; + + /// + /// This event is raised to log an error. + /// + public event BuildErrorEventHandler ErrorRaised; + + /// + /// This event is raised to log a warning. + /// + public event BuildWarningEventHandler WarningRaised; + + /// + /// this event is raised to log the start of a build + /// + public event BuildStartedEventHandler BuildStarted; + + /// + /// this event is raised to log the end of a build + /// + public event BuildFinishedEventHandler BuildFinished; + + /// + /// this event is raised to log the start of a project build + /// + public event ProjectStartedEventHandler ProjectStarted; + + /// + /// this event is raised to log the end of a project build + /// + public event ProjectFinishedEventHandler ProjectFinished; + + /// + /// this event is raised to log the start of a target build + /// + public event TargetStartedEventHandler TargetStarted; + + /// + /// this event is raised to log the end of a target build + /// + public event TargetFinishedEventHandler TargetFinished; + + /// + /// this event is raised to log the start of task execution + /// + public event TaskStartedEventHandler TaskStarted; + + /// + /// this event is raised to log the end of task execution + /// + public event TaskFinishedEventHandler TaskFinished; + + /// + /// this event is raised to log a custom event + /// + public event CustomBuildEventHandler CustomEventRaised; + + /// + /// this event is raised to log build status events, such as + /// build/project/target/task started/stopped + /// + public event BuildStatusEventHandler StatusEventRaised; + + /// + /// This event is raised to log that some event has + /// occurred. It is raised on every event. + /// + public event AnyEventHandler AnyEventRaised; + + #endregion + + #region Properties + /// + /// Provide a friendly name for the sink to make it easier to differentiate during + /// debugging and display + /// + public string Name + { + get; + set; + } + + /// + /// Has the sink logged the BuildStartedEvent. This is important to know because we only want to log the build started event once + /// + public bool HaveLoggedBuildStartedEvent + { + get; + set; + } + + /// + /// Has the sink logged the BuildFinishedEvent. This is important to know because we only want to log the build finished event once + /// + public bool HaveLoggedBuildFinishedEvent + { + get; + set; + } + #endregion + + #region Methods + + #region IEventSink Methods + + /// + /// Raises the given event to all registered loggers. This method up-cast the events + /// extracted from the queue. + /// + /// BuildEventArgs + /// Note this is not used in the eventsource sink + public void Consume(BuildEventArgs buildEvent, int sinkId) + { + Consume(buildEvent); + } + + /// + /// Raises the given event to all registered loggers. This method up-cast the events + /// extracted from the queue. + /// + public void Consume(BuildEventArgs buildEvent) + { + // FXCop may complain that there are unecessary casts here, and there are, but + // using "as" and allocating another variable for each event is extremely costly + // and is much slower then this approach even with the additional casts + if (buildEvent is BuildMessageEventArgs) + { + this.RaiseMessageEvent(null, (BuildMessageEventArgs)buildEvent); + } + else if (buildEvent is TaskStartedEventArgs) + { + this.RaiseTaskStartedEvent(null, (TaskStartedEventArgs)buildEvent); + } + else if (buildEvent is TaskFinishedEventArgs) + { + this.RaiseTaskFinishedEvent(null, (TaskFinishedEventArgs)buildEvent); + } + else if (buildEvent is TargetStartedEventArgs) + { + this.RaiseTargetStartedEvent(null, (TargetStartedEventArgs)buildEvent); + } + else if (buildEvent is TargetFinishedEventArgs) + { + this.RaiseTargetFinishedEvent(null, (TargetFinishedEventArgs)buildEvent); + } + else if (buildEvent is ProjectStartedEventArgs) + { + this.RaiseProjectStartedEvent(null, (ProjectStartedEventArgs)buildEvent); + } + else if (buildEvent is ProjectFinishedEventArgs) + { + this.RaiseProjectFinishedEvent(null, (ProjectFinishedEventArgs)buildEvent); + } + else if (buildEvent is BuildStartedEventArgs) + { + HaveLoggedBuildStartedEvent = true; + this.RaiseBuildStartedEvent(null, (BuildStartedEventArgs)buildEvent); + } + else if (buildEvent is BuildFinishedEventArgs) + { + HaveLoggedBuildFinishedEvent = true; + this.RaiseBuildFinishedEvent(null, (BuildFinishedEventArgs)buildEvent); + } + else if (buildEvent is CustomBuildEventArgs) + { + this.RaiseCustomEvent(null, (CustomBuildEventArgs)buildEvent); + } + else if (buildEvent is BuildStatusEventArgs) + { + this.RaiseStatusEvent(null, (BuildStatusEventArgs)buildEvent); + } + else if (buildEvent is BuildWarningEventArgs) + { + this.RaiseWarningEvent(null, (BuildWarningEventArgs)buildEvent); + } + else if (buildEvent is BuildErrorEventArgs) + { + this.RaiseErrorEvent(null, (BuildErrorEventArgs)buildEvent); + } + else + { + ErrorUtilities.VerifyThrow(false, "Unknown event args type."); + } + } + + /// + /// Shutdown and displose of any resource this object is holding onto. + /// + public void ShutDown() + { + this.UnregisterAllEventHandlers(); + } + #endregion + + #region Internal Methods + + /// + /// Clears out all events. + /// + internal void UnregisterAllEventHandlers() + { + MessageRaised = null; + ErrorRaised = null; + WarningRaised = null; + BuildStarted = null; + BuildFinished = null; + ProjectStarted = null; + ProjectFinished = null; + TargetStarted = null; + TargetFinished = null; + TaskStarted = null; + TaskFinished = null; + CustomEventRaised = null; + StatusEventRaised = null; + AnyEventRaised = null; + } + + #endregion + + #region Private Methods + + /// + /// Raises a message event to all registered loggers. + /// + /// sender of the event + /// BuildMessageEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseMessageEvent(object sender, BuildMessageEventArgs buildEvent) + { + if (MessageRaised != null) + { + try + { + MessageRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseAnyEvent(sender, buildEvent); + } + + /// + /// Raises an error event to all registered loggers. + /// + /// sender of the event + /// BuildErrorEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseErrorEvent(object sender, BuildErrorEventArgs buildEvent) + { + if (ErrorRaised != null) + { + try + { + ErrorRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseAnyEvent(sender, buildEvent); + } + + /// + /// Raises a warning event to all registered loggers. + /// + /// sender of the event + /// BuildWarningEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseWarningEvent(object sender, BuildWarningEventArgs buildEvent) + { + if (WarningRaised != null) + { + try + { + WarningRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseAnyEvent(sender, buildEvent); + } + + /// + /// Raises a "build started" event to all registered loggers. + /// + /// sender of the event + /// BuildStartedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseBuildStartedEvent(object sender, BuildStartedEventArgs buildEvent) + { + if (BuildStarted != null) + { + try + { + BuildStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "build finished" event to all registered loggers. + /// + /// sender of the event + /// BuildFinishedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseBuildFinishedEvent(object sender, BuildFinishedEventArgs buildEvent) + { + if (BuildFinished != null) + { + try + { + BuildFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "project build started" event to all registered loggers. + /// + /// sender of the event + /// ProjectStartedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseProjectStartedEvent(object sender, ProjectStartedEventArgs buildEvent) + { + if (ProjectStarted != null) + { + try + { + ProjectStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "project build finished" event to all registered loggers. + /// + /// sender of the event + /// ProjectFinishedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseProjectFinishedEvent(object sender, ProjectFinishedEventArgs buildEvent) + { + if (ProjectFinished != null) + { + try + { + ProjectFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "target build started" event to all registered loggers. + /// + /// sender of the event + /// TargetStartedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseTargetStartedEvent(object sender, TargetStartedEventArgs buildEvent) + { + if (TargetStarted != null) + { + try + { + TargetStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "target build finished" event to all registered loggers. + /// + /// sender of the event + /// TargetFinishedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseTargetFinishedEvent(object sender, TargetFinishedEventArgs buildEvent) + { + if (TargetFinished != null) + { + try + { + TargetFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "task execution started" event to all registered loggers. + /// + /// sender of the event + /// TaskStartedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseTaskStartedEvent(object sender, TaskStartedEventArgs buildEvent) + { + if (TaskStarted != null) + { + try + { + TaskStarted(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a "task finished executing" event to all registered loggers. + /// + /// sender of the event + /// TaskFinishedEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseTaskFinishedEvent(object sender, TaskFinishedEventArgs buildEvent) + { + if (TaskFinished != null) + { + try + { + TaskFinished(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseStatusEvent(sender, buildEvent); + } + + /// + /// Raises a custom event to all registered loggers. + /// + /// sender of the event + /// CustomBuildEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseCustomEvent(object sender, CustomBuildEventArgs buildEvent) + { + if (CustomEventRaised != null) + { + try + { + CustomEventRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseAnyEvent(sender, buildEvent); + } + + /// + /// Raises a catch-all build status event to all registered loggers. + /// + /// sender of the event + /// BuildStatusEventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseStatusEvent(object sender, BuildStatusEventArgs buildEvent) + { + if (StatusEventRaised != null) + { + try + { + StatusEventRaised(sender, buildEvent); + } + catch (LoggerException) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + + RaiseAnyEvent(sender, buildEvent); + } + + /// + /// Raises a catch-all build event to all registered loggers. + /// + /// sender of the event + /// Build EventArgs + /// When EventHandler raises an logger exception the LoggerException is rethrown + /// Any exceptions which are not LoggerExceptions are wrapped in an InternalLoggerException + /// ExceptionHandling.IsCriticalException exceptions will not be wrapped + private void RaiseAnyEvent(object sender, BuildEventArgs buildEvent) + { + if (AnyEventRaised != null) + { + try + { + AnyEventRaised(sender, buildEvent); + } + catch (LoggerException exception) + { + // if a logger has failed politely, abort immediately + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + // We ought to dump this farther up the stack, but if for example a task is logging an event within a + // catch(Exception) block and not rethrowing it, there's the possibility that this exception could + // just get silently eaten. So better to have duplicates than to not log the problem at all. :) + ExceptionHandling.DumpExceptionToFile(exception); + + throw; + } + catch (Exception exception) + { + // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings + // if a fellow logger is throwing in an event handler. + this.UnregisterAllEventHandlers(); + + // We ought to dump this farther up the stack, but if for example a task is logging an event within a + // catch(Exception) block and not rethrowing it, there's the possibility that this exception could + // just get silently eaten. So better to have duplicates than to not log the problem at all. :) + ExceptionHandling.DumpExceptionToFile(exception); + + if (ExceptionHandling.IsCriticalException(exception)) + { + throw; + } + + InternalLoggerException.Throw(exception, buildEvent, "FatalErrorWhileLogging", false); + } + } + } + + #endregion + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/ForwardingLoggerRecord.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/ForwardingLoggerRecord.cs new file mode 100644 index 00000000000..7aac66d17d4 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/ForwardingLoggerRecord.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class describing a central/forwarding logger pair. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Logging +{ + /// + /// This class descibes a central/forwarding logger pair used in multiproc logging. + /// + public class ForwardingLoggerRecord + { + /// + /// Constructor. + /// + /// The central logger + /// The description for the forwarding logger. + public ForwardingLoggerRecord(ILogger centralLogger, LoggerDescription forwardingLoggerDescription) + { + // The logging service allows a null central logger, so we don't check for it here. + ErrorUtilities.VerifyThrowArgumentNull(forwardingLoggerDescription, "forwardingLoggerDescription"); + + this.CentralLogger = centralLogger; + this.ForwardingLoggerDescription = forwardingLoggerDescription; + } + + /// + /// Retrieves the central logger. + /// + public ILogger CentralLogger + { + get; + private set; + } + + /// + /// Retrieves the forwarding logger description. + /// + public LoggerDescription ForwardingLoggerDescription + { + get; + private set; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/ILoggingService.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/ILoggingService.cs new file mode 100644 index 00000000000..3f70f430e2d --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/ILoggingService.cs @@ -0,0 +1,457 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Interface for the logging services. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.BackEnd.Logging +{ + #region Delegates + /// + /// Delegate for an event which will take an exception and raise it on the registered event handlers. + /// + /// Exception to be raised with registered event handlers + internal delegate void LoggingExceptionDelegate(Exception e); + #endregion + + /// + /// Interface representing logging services in the build system. + /// Implementations should be thread-safe. + /// + internal interface ILoggingService + { + #region Events + /// + /// When there is an exception on the logging thread, we do not want to throw the exception from there + /// instead we would like the exception to be thrown on the engine thread as this is where hosts expect + /// to see the exception. This event will transport the exception from the loggingService to the engine + /// which will register on this event. + /// + event LoggingExceptionDelegate OnLoggingThreadException; + + /// + /// Raised when a ProjectStarted event is about to be sent to the loggers. + /// + event ProjectStartedEventHandler OnProjectStarted; + + /// + /// Raised when a ProjectFinished event has just been sent to the loggers. + /// + event ProjectFinishedEventHandler OnProjectFinished; + + #endregion + + #region Properties + + /// + /// Provide the current state of the loggingService. + /// Is it Inistantiated + /// Has it been Initialized + /// Is it starting to shutdown + /// Has it shutdown + /// + LoggingServiceState ServiceState + { + get; + } + + /// + /// Returns the synchronous/asynchronous mode for the logging service. + /// + LoggerMode LoggingMode + { + get; + } + + /// + /// When true, only log critical events such as warnings and errors. Has to be in here for API compat + /// + bool OnlyLogCriticalEvents + { + get; + set; + } + + /// + /// Number of nodes in the system when it was initially started + /// + int MaxCPUCount + { + get; + set; + } + + /// + /// Enumerator over all registered loggers. + /// + ICollection Loggers + { + get; + } + + /// + /// The list of descriptions which describe how to create forwarding loggers on a node. + /// This is used by the node provider to get a list of registered descriptions so that + /// they can be transmitted to child nodes. + /// + ICollection LoggerDescriptions + { + get; + } + + /// + /// Return an array which contains the logger type names + /// this can be used to display which loggers are registered on the node + /// + ICollection RegisteredLoggerTypeNames + { + get; + } + + /// + /// Return an array which contains the sink names + /// this can be used to display which sinks are on the node + /// + ICollection RegisteredSinkNames + { + get; + } + + /// + /// List of properties to serialize from the child node + /// + string[] PropertiesToSerialize + { + get; + set; + } + + /// + /// Should all properties be serialized from the child to the parent process + /// + bool SerializeAllProperties + { + get; + set; + } + + /// + /// Is the logging running on a remote node + /// + bool RunningOnRemoteNode + { + get; + set; + } + #endregion + + #region Register + + /// + /// Allows the registering of an ICentralLogger and a forwarding logger pair + /// + /// Central logger which is to recieve the events created by the forwarding logger + /// A description of the forwarding logger + /// True if the central and forwarding loggers were registered. False if the central logger or the forwarding logger were already registered + bool RegisterDistributedLogger(ILogger centralLogger, LoggerDescription forwardingLogger); + + /// + /// Register an logger which expects all logging events from the system + /// + /// The logger to register. + ///True if the central was registered. False if the central logger was already registered + bool RegisterLogger(ILogger logger); + + /// + /// Clear out all registered loggers so that none are registered. + /// + void UnregisterAllLoggers(); + + /// + /// In order to setup the forwarding loggers on a node, we need to take in the logger descriptions and initialize them. + /// The method will create a forwarding logger, an eventRedirector which will redirect all forwarded messages to the forwardingLoggerSink. + /// All forwarding loggers will use the same forwardingLoggerSink. + /// + /// Collection of logger descriptions which we would like to use to create a set of forwarding loggers on a node + /// The buildEventSink which the fowarding loggers will forward their events to + /// The id of the node the logging services is on + /// When forwardingLoggerSink is null + /// When loggerDescriptions is null + void InitializeNodeLoggers(ICollection loggerDescriptions, IBuildEventSink forwardingLoggerSink, int nodeId); + + #endregion + + #region Log comments + /// + /// Helper method to create a message build event from a string resource and some parameters + /// + /// Event context which describes where in the build the message came from + /// Importance level of the message + /// string within the resource which indicates the format string to use + /// string resource arguments + void LogComment(BuildEventContext buildEventContext, MessageImportance importance, string messageResourceName, params object[] messageArgs); + + /// + /// Helper method to create a message build event from a string + /// + /// Event context which describes where in the build the message came from + /// Importance level of the message + /// message to log + void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message); + #endregion + + #region Log events + /// + /// Will Log a build Event. Will also take into account OnlyLogCriticalEvents when determining if to drop the event or to log it. + /// + void LogBuildEvent(BuildEventArgs buildEvent); + + #endregion + + #region Log errors + /// + /// Log an error + /// + /// The event context information as to where the error occurred + /// The file in which the error occurred + /// The resource name for the error + /// Parameters for the resource string + void LogError(BuildEventContext buildEventContext, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs); + + /// + /// Log an error + /// + /// The event context for where the error occurred + /// The resource name which indicates the subCategory + /// The file in which the error occurred + /// The resource name for the error + /// Parameters for the resource string + void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs); + + /// + /// Log an error + /// + /// The event context for where the error occurred + /// The resource name which indicates the subCategory + /// Error code + /// Help keyword + /// The file in which the error occurred + /// Error message + void LogErrorFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string errorCode, string helpKeyword, BuildEventFileInfo file, string message); + + /// + /// Log an invalid project file exception + /// + /// The event context for where the error occurred + /// The invalid Project File Exception which is to be logged + void LogInvalidProjectFileError(BuildEventContext buildEventContext, InvalidProjectFileException invalidProjectFileException); + + /// + /// Log an error based on an exception + /// + /// The event context for where the error occurred + /// The exception wich is to be logged + /// The file in which the error occurred + void LogFatalBuildError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file); + + /// + /// Log an error based on an exception during the execution of a task + /// + /// The event context for where the error occurred + /// The exception wich is to be logged + /// The file in which the error occurred + /// The task in which the error occurred + void LogFatalTaskError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName); + + /// + /// Log an error based on an exception + /// + /// The event context for where the error occurred + /// The exception wich is to be logged + /// The file in which the error occurred + /// The string resource which has the formatting string for the error + /// The arguments for the error message + void LogFatalError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs); + #endregion + + #region Log warnings + /// + /// Log a warning based on an exception + /// + /// The event context for where the warning occurred + /// The exception to be logged as a warning + /// The file in which the warning occurred + /// The task in which the warning occurred + void LogTaskWarningFromException(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName); + + /// + /// Log a warning + /// + /// The event context for where the warning occurred + /// The subcategory resource name + /// The file in which the warning occurred + /// The string resource which contains the formatted warning string + /// parameters for the string resource + void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs); + + /// + /// Log a warning based on a text message + /// + /// The event context for where the warning occurred + /// The subcategory resource name + /// Warning code + /// Help keyword + /// The file in which the warning occurred + /// The message to be logged as a warning + void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message); + #endregion + + #region Log status + /// + /// Log the start of the build + /// + void LogBuildStarted(); + + /// + /// Log the completion of a build + /// + /// Did the build succeed or not + void LogBuildFinished(bool success); + + /// + /// Log that a project has started + /// + /// The logging context of the node which is building this project. + /// The id of the build submission. + /// The id of the project instance which is about to start + /// The build context of the parent project which asked this project to build + /// The project file path of the project about to be built + /// The entrypoint target names for this project + /// The initial properties of the project + /// The initial items of the project + /// The BuildEventContext to use for this project. + BuildEventContext LogProjectStarted(BuildEventContext nodeBuildEventContext, int submissionId, int projectId, BuildEventContext parentBuildEventContext, string projectFile, string targetNames, IEnumerable properties, IEnumerable items); + + /// + /// Log that the project has finished + /// + /// The build context of the project which has just finished + /// The path to the projec file which was just built + /// Did the build succeede or not + void LogProjectFinished(BuildEventContext projectBuildEventContext, string projectFile, bool success); + + /// + /// Log that a target has started + /// + /// The build event context of the project spawning this target. + /// The name of the target which is about to start + /// The project file which is being built + /// The file in which the target is defined - typically a .targets file + /// The target build event context + BuildEventContext LogTargetStarted(BuildEventContext projectBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, string parentTargetName); + + /// + /// Log that a target has finished + /// + /// The event context of the target which has just completed + /// The name of the target which has just completed + /// The project file which the target was being run in + /// The file in which the target is defined - typically a .targets file + /// Did the target finish successfully or not + /// List of target outputs for the target, right now this is for all batches and only is on the last target finished event + void LogTargetFinished(BuildEventContext targetBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, bool success, IEnumerable targetOutputs); + + /// + /// Log that a task is about to start + /// + /// The event context of the task. + /// The name of the task + /// The project file which is being built + /// The file in which the task is defined - typically a .targets file + void LogTaskStarted(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode); + + /// + /// Log that a task is about to start + /// + /// The event context of the target which is spawning this task. + /// The name of the task + /// The project file which is being built + /// The file in which the task is defined - typically a .targets file + /// The task build event context + BuildEventContext LogTaskStarted2(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode); + + /// + /// Log that a task has just completed + /// + /// The event context of the task which has just finished + /// The name of the task + /// The project file which is being built + /// The file in which the task is defined - typically a .targets file + /// True of the task finished successfully, false otherwise. + void LogTaskFinished(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, bool success); + #endregion + } + + /// + /// Acts as an endpoint for a buildEventArg. The objects which implement this interface are intended to consume the BuildEventArg. + /// + internal interface IBuildEventSink + { + #region Properties + /// + /// Provide a the sink a friendly name which can be used to distinguish sinks in memory + /// and for display + /// + string Name + { + get; + set; + } + + /// + /// Has the sink logged the BuildStartedEvent. This is important to know because we only want to log the build started event once + /// + bool HaveLoggedBuildStartedEvent + { + get; + set; + } + + /// + /// Has the sink logged the BuildFinishedEvent. This is important to know because we only want to log the build finished event once + /// + bool HaveLoggedBuildFinishedEvent + { + get; + set; + } + + #endregion + /// + /// Entry point for a sink to consume an event. + /// + /// The event to be consumed by the sink. + /// Sink where the message should go to, this is really only used for the transport sink + void Consume(BuildEventArgs buildEvent, int sinkId); + + /// + /// Entry point for a sink to consume an event. + /// + void Consume(BuildEventArgs buildEvent); + + /// + /// Shutsdown the sink and any resources it may be holding + /// + void ShutDown(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingService.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingService.cs new file mode 100644 index 00000000000..29a61384f5f --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingService.cs @@ -0,0 +1,1363 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Logging service which assists in getting build events to the correct loggers +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks.Dataflow; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using BuildParameters = Microsoft.Build.Execution.BuildParameters; +using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException; +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// What is the mode of the logger, should there be a thread + /// processing the buildEvents and raising them on the filters and sinks + /// or should they be done synchronously + /// + internal enum LoggerMode + { + /// + /// Events are processed synchronously + /// + Synchronous, + + /// + /// A thread is started which will process build events by raising them on a filter event source + /// or on the correct sink. + /// + Asynchronous + } + + /// + /// What is the current state of the logging service + /// + internal enum LoggingServiceState + { + /// + /// When the logging service has been instantiated but not yet initialized through a call + /// to initializecomponent + /// + Instantiated, + + /// + /// The logging service has been initialized through a call to initialize component + /// + Initialized, + + /// + /// The logging service is in the process of starting to shutdown. + /// + ShuttingDown, + + /// + /// The logging service completly shutdown + /// + Shutdown + } + + /// + /// Logging services is used as a helper class to assist logging messages in getting to the correct loggers. + /// + internal partial class LoggingService : ILoggingService, INodePacketHandler, IBuildComponent + { + /// + /// The default maximum size for the logging event queue. + /// + private const uint DefaultQueueCapacity = 200000; + + /// + /// Lock for the nextProjectId + /// + private readonly object _lockObject = new Object(); + + /// + /// A cached reflection accessor for an internal member. + /// + /// + /// We use a BindingFlags.Public flag here because the getter is public, so although the setter is internal, + /// it is only discoverable with Reflection using the Public flag (go figure!) + /// + private static Lazy s_projectStartedEventArgsGlobalProperties = new Lazy(() => typeof(ProjectStartedEventArgs).GetProperty("GlobalProperties", BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty), LazyThreadSafetyMode.PublicationOnly); + + /// + /// A cached reflection accessor for an internal member. + /// + /// + /// We use a BindingFlags.Public flag here because the getter is public, so although the setter is internal, + /// it is only discoverable with Reflection using the Public flag (go figure!) + /// + private static Lazy s_projectStartedEventArgsToolsVersion = new Lazy(() => typeof(ProjectStartedEventArgs).GetProperty("ToolsVersion", BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty), LazyThreadSafetyMode.PublicationOnly); + + #region Data + + /// + /// The mapping of build request configuration ids to project file names. + /// + private Dictionary _projectFileMap; + + /// + /// The current state of the logging service + /// + private LoggingServiceState _serviceState; + + /// + /// Use to optimize away status messages. When this is set to true, only "critical" + /// events like errors are logged. Default is false + /// + private bool _onlyLogCriticalEvents; + + /// + /// Contains a dictionary of loggerId's and the sink which the logger (of the given Id) is expecting to consume its messages + /// + private Dictionary _eventSinkDictionary; + + /// + /// A list of ILoggers registered with the LoggingService + /// + private List _iloggerList; + + /// + /// A list of LoggerDescriptions which describe how to create a forwarding logger on a node. These are + /// passed to each node as they are created so that the forwarding loggers can be registered on them. + /// + private List _loggerDescriptions; + + /// + /// The event source to which filters will listen to to get the build events which are logged to the logging service through the + /// logging helper methods. Ie LogMessage and LogMessageEvent + /// + private EventSourceSink _filterEventSource; + + /// + /// Index into the eventSinkDictionary which indicates which sink is the sink for any logger registered through RegisterLogger + /// + private int _centralLoggerSinkId = -1; + + /// + /// What is the Id for the next logger registered with the logging service. + /// This Id is unique for this instance of the loggingService. + /// + private int _nextSinkId = 0; + + /// + /// The number of nodes in the system. Loggers may take different action depending on how many nodes are in the system. + /// + private int _maxCPUCount = 1; + + /// + /// Component host for this component which is used to get system parameters and other initialization information. + /// + private IBuildComponentHost _componentHost; + + /// + /// The IConfigCache instance obtained from componentHost (stored here to avoid repeated dictionary lookups). + /// + private Lazy _configCache; + + /// + /// The next project ID to assign when a project started event is received. + /// + private int _nextProjectId = 1; + + /// + /// The next target ID to assign when a target started event is received. + /// + private int _nextTargetId = 1; + + /// + /// The next task ID to assign when a task started event is received. + /// + private int _nextTaskId = 1; + + /// + /// What node is this logging service running on + /// + private int _nodeId = 0; + + #region LoggingThread Data + + /// + /// The data flow buffer for logging events. + /// + private BufferBlock _loggingQueue; + + /// + /// The data flow processor for logging events. + /// + private ActionBlock _loggingQueueProcessor; + + /// + /// The queue size above which the queue will close to messages from remote nodes. + /// This value should be selected such that during normal builds it is never reached. + /// It should also be low enough that we do not accumulate enough messages to cause + /// virtual memory exhaustion in extremely large builds. + /// + private uint _queueCapacity = DefaultQueueCapacity; + + /// + /// By default our logMode is Asynchronous. We do this + /// because we are hoping it will make the system + /// more responsive when there are a large number of logging messages + /// + private LoggerMode _logMode = LoggerMode.Asynchronous; + + #endregion + + #endregion + + #region Constructors + + /// + /// Initialize an instance of a loggingService. + /// + /// Should the events be processed synchronously or asynchronously + protected LoggingService(LoggerMode loggerMode, int nodeId) + { + _projectFileMap = new Dictionary(); + _logMode = loggerMode; + _iloggerList = new List(); + _loggerDescriptions = new List(); + _eventSinkDictionary = new Dictionary(); + _nodeId = nodeId; + _configCache = new Lazy(() => (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache), LazyThreadSafetyMode.PublicationOnly); + + // Start the project context id count at the nodeId + _nextProjectId = nodeId; + + string queueCapacityEnvironment = Environment.GetEnvironmentVariable("MSBUILDLOGGINGQUEUECAPACITY"); + if (!String.IsNullOrEmpty(queueCapacityEnvironment)) + { + uint localQueueCapacity; + if (UInt32.TryParse(queueCapacityEnvironment, out localQueueCapacity)) + { + _queueCapacity = localQueueCapacity; + } + + _queueCapacity = Math.Max(0, _queueCapacity); + } + + if (_logMode == LoggerMode.Asynchronous) + { + CreateLoggingEventQueue(); + } + + _serviceState = LoggingServiceState.Instantiated; + } + + #endregion + + #region Events + + /// + /// When there is an exception on the logging thread, we do not want to throw the exception from there + /// instead we would like the exception to be thrown on the engine thread as this is where hosts expect + /// to see the exception. This event will transport the exception from the loggingService to the engine + /// which will register on this event. + /// + public event LoggingExceptionDelegate OnLoggingThreadException; + + /// + /// Raised when a ProjectStarted event is about to be sent to the loggers. + /// + public event ProjectStartedEventHandler OnProjectStarted; + + /// + /// Raised when a ProjectFinished event has just been sent to the loggers. + /// + public event ProjectFinishedEventHandler OnProjectFinished; + + #endregion + + #region Properties + + /// + /// Properties we need to serialize from the child node + /// + public string[] PropertiesToSerialize + { + get; + set; + } + + /// + /// Should all properties be serialized from the child to the parent node + /// + public bool SerializeAllProperties + { + get; + set; + } + + /// + /// Is the logging running on a remote node + /// + public bool RunningOnRemoteNode + { + get; + set; + } + + /// + /// Gets the next project id. + /// + /// This property is thread-safe + public int NextProjectId + { + get + { + lock (_lockObject) + { + _nextProjectId = _nextProjectId += MaxCPUCount + 2 /* We can create one node more than the maxCPU count (this can happen if either the inproc or out of proc node has not been created yet and the project collection needs to be counted also)*/; + return _nextProjectId; + } + } + } + + /// + /// Gets the next target id. + /// + /// This property is thread-safe + public int NextTargetId + { + get + { + lock (_lockObject) + { + _nextTargetId++; + return _nextTargetId; + } + } + } + + /// + /// Gets the next task id. + /// + /// This property is thread-safe + public int NextTaskId + { + get + { + lock (_lockObject) + { + _nextTaskId++; + return _nextTaskId; + } + } + } + + /// + /// Provide the current state of the loggingService. + /// Is it Inistantiated + /// Has it been Initialized + /// Is it starting to shutdown + /// Has it shutdown + /// + public LoggingServiceState ServiceState + { + get + { + return _serviceState; + } + } + + /// + /// Use to optimize away status messages. When this is set to true, only "critical" + /// events like errors are logged. + /// + public bool OnlyLogCriticalEvents + { + get + { + return _onlyLogCriticalEvents; + } + + set + { + _onlyLogCriticalEvents = value; + } + } + + /// + /// Number of nodes in the system when the system is initially started + /// + public int MaxCPUCount + { + get + { + return _maxCPUCount; + } + + set + { + _maxCPUCount = value; + } + } + + /// + /// The list of descriptions which describe how to create forwarding loggers on a node. + /// This is used by the node provider to get a list of registered descriptions so that + /// they can be transmitted to child nodes. + /// + public ICollection LoggerDescriptions + { + get + { + return _loggerDescriptions; + } + } + + /// + /// Enumerator over all registered loggers. + /// + public ICollection Loggers + { + get { return _iloggerList; } + } + + /// + /// What type of logging mode is the logger running under. + /// Is it Synchronous or Asynchronous + /// + public LoggerMode LoggingMode + { + get + { + return _logMode; + } + } + + /// + /// Return whether or not the LoggingQueue has any events left in it + /// + public bool LoggingQueueHasEvents + { + get + { + lock (_lockObject) + { + if (_loggingQueue != null) + { + return _loggingQueue.Count > 0; + } + else + { + ErrorUtilities.ThrowInternalError("loggingQueue is null"); + return false; + } + } + } + } + + /// + /// Return an array which contains the logger type names + /// this can be used to display which loggers are registered on the node + /// + public ICollection RegisteredLoggerTypeNames + { + get + { + lock (_lockObject) + { + if (_iloggerList == null) + { + return null; + } + + List loggerTypes = new List(); + foreach (ILogger logger in _iloggerList) + { + loggerTypes.Add(logger.GetType().FullName); + } + + return loggerTypes; + } + } + } + + /// + /// Return an array which contains the sink names + /// this can be used to display which sinks are on the node + /// + public ICollection RegisteredSinkNames + { + get + { + lock (_lockObject) + { + if (_eventSinkDictionary == null) + { + return null; + } + + List eventSinkNames = new List(); + foreach (KeyValuePair kvp in _eventSinkDictionary) + { + eventSinkNames.Add(kvp.Value.Name); + } + + return eventSinkNames; + } + } + } + + #endregion + + #region Members + + #region Public methods + + /// + /// Create an instance of a LoggingService using the specified mode. + /// This method is used by the object factories to create instances of components. + /// + /// Should the logger component created be synchronous or asynchronous + /// An instantiated LoggingService as a IBuildComponent + public static ILoggingService CreateLoggingService(LoggerMode mode, int node) + { + return new LoggingService(mode, node); + } + + /// + /// NotThreadSafe, this method should only be called from the component host thread + /// Called by the build component host when a component is first initialized. + /// + /// The component host for this object + /// When buildComponentHost is null + /// Service has already shutdown + public void InitializeComponent(IBuildComponentHost buildComponentHost) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + ErrorUtilities.VerifyThrow(buildComponentHost != null, "BuildComponentHost was null"); + + _componentHost = buildComponentHost; + + // Get the number of initial nodes the host is running with, if the component host does not have + // this information default to 1 + _maxCPUCount = buildComponentHost.BuildParameters.MaxNodeCount; + + // Ask the component host if onlyLogCriticalEvents is true or false. If the host does + // not have this information default to false. + _onlyLogCriticalEvents = buildComponentHost.BuildParameters.OnlyLogCriticalEvents; + + _serviceState = LoggingServiceState.Initialized; + } + } + + /// + /// NotThreadSafe, this method should only be called from the component host thread + /// Called by the build component host when the component host is about to shutdown. + /// 1. Shutdown forwarding loggers so that any events they have left to forward can get into the queue + /// 2. Terminate the logging thread + /// 3. Null out sinks and the filter event source so that no more events can get to the central loggers + /// 4. Shutdown the central loggers + /// + /// Service has already shutdown + /// A logger may throw a logger exception when shutting down + /// A logger will wrap other exceptions (except ExceptionHandling.IsCriticalException exceptions) in a InternalLoggerException if it crashes during shutdown + public void ShutdownComponent() + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + + // Set the state to indicate we are starting the shutdown process. + _serviceState = LoggingServiceState.ShuttingDown; + + try + { + try + { + // 1. Shutdown forwarding loggers so that any events they have left to forward can get into the queue + foreach (ILogger logger in _iloggerList) + { + if (logger is IForwardingLogger) + { + ShutdownLogger(logger); + } + } + } + finally + { + // 2. Terminate the logging event queue + if (_logMode == LoggerMode.Asynchronous) + { + TerminateLoggingEventQueue(); + } + } + + // 3. Null out sinks and the filter event source so that no more events can get to the central loggers + if (_filterEventSource != null) + { + _filterEventSource.ShutDown(); + } + + foreach (IBuildEventSink sink in _eventSinkDictionary.Values) + { + sink.ShutDown(); + } + + // 4. Shutdown the central loggers + foreach (ILogger logger in _iloggerList) + { + ShutdownLogger(logger); + } + } + finally + { + // Revert the centralLogger sinId back to -1 so that when another central logger is registered, it will generate a new + // sink for the central loggers. + _centralLoggerSinkId = -1; + + // Clean up anything related to the asynchronous logging + if (_logMode == LoggerMode.Asynchronous) + { + _loggingQueue = null; + _loggingQueueProcessor = null; + } + + _iloggerList = new List(); + _loggerDescriptions = null; + _eventSinkDictionary = null; + _filterEventSource = null; + _serviceState = LoggingServiceState.Shutdown; + } + } + } + + /// + /// Will recieve a logging packet and send it to the correct + /// sink which is registered to the LoggingServices. + /// PacketReceived should be called from a single thread. + /// + /// The node from which the packet was received. + /// A LogMessagePacket + /// Packet is null + /// Packet is not a NodePacketType.LogMessage + public void PacketReceived(int node, INodePacket packet) + { + // The packet cannot be null + ErrorUtilities.VerifyThrow(packet != null, "packet was null"); + + // Expected the packet type to be a logging message packet + // PERF: Not using VerifyThrow to avoid allocations for enum.ToString (boxing of NodePacketType) in the non-error case. + if (packet.Type != NodePacketType.LogMessage) + { + ErrorUtilities.ThrowInternalError("Expected packet type \"{0}\" but instead got packet type \"{1}\".", NodePacketType.LogMessage.ToString(), packet.Type.ToString()); + } + + LogMessagePacket loggingPacket = (LogMessagePacket)packet; + InjectNonSerializedData(loggingPacket); + ProcessLoggingEvent(loggingPacket.NodeBuildEvent, allowThrottling: true); + } + + /// + /// Register an instantiated logger which implements the ILogger interface. This logger will be registered to a specific event + /// source (the central logger event source) which will recieve all logging messages for a given build. + /// This should not be used on a node, Loggers are not to be registered on a child node. + /// + /// ILogger + /// True if the logger has been registered successfully. False if the logger was not registered due to it already being registered before + /// If logger is null + public bool RegisterLogger(ILogger logger) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + ErrorUtilities.VerifyThrow(logger != null, "logger was null"); + + // If the logger is already in the list it should not be registered again. + if (_iloggerList.Contains(logger)) + { + return false; + } + + // If we have not created a distributed logger to forward all events to the central loggers and + // a sink which will consume the events and send them to each of the central loggers, we + // should do that now + if (_centralLoggerSinkId == -1) + { + // Create a forwarding logger which forwards all events to an eventSourceSink + Assembly engineAssembly = Assembly.GetAssembly(typeof(LoggingService)); + string loggerClassName = "Microsoft.Build.BackEnd.Logging.CentralForwardingLogger"; + string loggerAssemblyName = engineAssembly.GetName().FullName; + LoggerDescription centralLoggerDescrption = new LoggerDescription + ( + loggerClassName, + loggerAssemblyName, + null /*Not needed as we are loading from current assembly*/, + string.Empty /*No parameters needed as we are forwarding all events*/, + LoggerVerbosity.Diagnostic /*Not used, but the spirit of the logger is to forward everything so this is the most appropriate verbosity */ + ); + + // Registering a distributed logger will initialize the logger, and create and initialize the forwarding logger. + // In addition it will register the logging description so that it can be instantiated on a node. + RegisterDistributedLogger(logger, centralLoggerDescrption); + + // Get the Id of the eventSourceSink which was created for the first logger. + // We keep a reference to this Id so that all other central loggers registered on this logging service (from registerLogger) + // will be attached to that eventSource sink so that they get all of the events forwarded by + // forwarded by the CentralForwardingLogger + _centralLoggerSinkId = centralLoggerDescrption.LoggerId; + } + else + { + // We have already create a forwarding logger and have a single eventSink which + // a logger can listen to inorder to get all events in the system + EventSourceSink eventSource = (EventSourceSink)_eventSinkDictionary[_centralLoggerSinkId]; + + // Initialize and register the logger + InitializeLogger(logger, eventSource); + } + + // Logger has been registered successfully + return true; + } + } + + /// + /// Clear out all registered loggers so that none are registered. + /// If no loggers are registered, does nothing. + /// + /// + /// UNDONE: (Logging) I don't like the semantics of this. Why should unregistering imply shutting down? VS actually calls it before registering any loggers. + /// Also, why not just have ShutdownComponent? Or call this Shutdown or Dispose? + /// + public void UnregisterAllLoggers() + { + lock (_lockObject) + { + if (_iloggerList.Count > 0) + { + ShutdownComponent(); + } + } + + // UNDONE: (Logging) This should re-initialize this logging service. + } + + /// + /// Register a distributed logger. This involves creating a new eventsource sink + /// and associating this with the central logger. In addition the sinkId needs + /// to be put in the loggerDescription so that nodes know what they need to + /// tag onto the event so that the message goes to the correct logger. + /// + /// The central logger is initialized before the distributed logger + /// + /// Central logger to recieve messages from the forwarding logger, This logger cannot have been registered before + /// Logger description which describes how to create the forwarding logger, the logger description canot have been used before + /// True if the distributed and central logger were registered, false if they either were already registered + /// If forwardingLogger is null + /// If a logger exception is thown while creating or initializing the distributed or central logger + /// If any exception (other than a loggerException)is thown while creating or initializing the distributed or central logger, we will wrap these exceptions in an InternalLoggerException + public bool RegisterDistributedLogger(ILogger centralLogger, LoggerDescription forwardingLogger) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + ErrorUtilities.VerifyThrow(forwardingLogger != null, "forwardingLogger was null"); + if (centralLogger == null) + { + centralLogger = new NullCentralLogger(); + } + + IForwardingLogger localForwardingLogger = null; + + // create an eventSourceSink which the central logger will register with to receive the events from the forwarding logger + EventSourceSink eventSourceSink = new EventSourceSink(); + + // If the logger is already in the list it should not be registered again. + if (_iloggerList.Contains(centralLogger)) + { + return false; + } + + // Assign a unique logger Id to this distributed logger + int sinkId = _nextSinkId++; + forwardingLogger.LoggerId = sinkId; + eventSourceSink.Name = "Sink for forwarding logger \"" + sinkId + "\"."; + + // Initialize and register the central logger + InitializeLogger(centralLogger, eventSourceSink); + + localForwardingLogger = forwardingLogger.CreateForwardingLogger(); + EventRedirectorToSink newRedirector = new EventRedirectorToSink(sinkId, eventSourceSink); + localForwardingLogger.BuildEventRedirector = newRedirector; + localForwardingLogger.Parameters = forwardingLogger.LoggerSwitchParameters; + localForwardingLogger.Verbosity = forwardingLogger.Verbosity; + + // Give the forwarding logger registered on the inproc node the correct ID. + localForwardingLogger.NodeId = 1; + + // Convert the path to the logger DLL to full path before passing it to the node provider + forwardingLogger.ConvertPathsToFullPaths(); + + CreateFilterEventSource(); + + // Initialize and register the forwarding logger + InitializeLogger(localForwardingLogger, _filterEventSource); + + _loggerDescriptions.Add(forwardingLogger); + + _eventSinkDictionary.Add(sinkId, eventSourceSink); + + return true; + } + } + + /// + /// In order to setup the forwarding loggers on a node, we need to take in the logger descriptions and initialize them. + /// The method will create a forwarding logger, an eventRedirector which will redirect all forwarded messages to the forwardingLoggerSink. + /// All forwarding loggers will use the same forwardingLoggerSink. + /// + /// Collection of logger descriptions which we would like to use to create a set of forwarding loggers on a node + /// The buildEventSink which the fowarding loggers will forward their events to + /// The id of the node the logging services is on + /// When forwardingLoggerSink is null + /// When loggerDescriptions is null + public void InitializeNodeLoggers(ICollection descriptions, IBuildEventSink forwardingLoggerSink, int nodeId) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(_serviceState != LoggingServiceState.Shutdown, " The object is shutdown, should not do any operations on a shutdown component"); + ErrorUtilities.VerifyThrow(forwardingLoggerSink != null, "forwardingLoggerSink was null"); + ErrorUtilities.VerifyThrow(descriptions != null, "loggerDescriptions was null"); + ErrorUtilities.VerifyThrow(descriptions.Count > 0, "loggerDescriptions was null"); + + bool sinkAlreadyRegistered = false; + int sinkId = -1; + + // Check to see if the forwardingLoggerSink has been registered before + foreach (KeyValuePair sinkPair in _eventSinkDictionary) + { + if (sinkPair.Value == forwardingLoggerSink) + { + sinkId = sinkPair.Key; + sinkAlreadyRegistered = true; + } + } + + if (!sinkAlreadyRegistered) + { + sinkId = _nextSinkId++; + _eventSinkDictionary.Add(sinkId, forwardingLoggerSink); + } + + CreateFilterEventSource(); + + foreach (LoggerDescription description in descriptions) + { + IForwardingLogger forwardingLogger = description.CreateForwardingLogger(); + forwardingLogger.Verbosity = description.Verbosity; + forwardingLogger.Parameters = description.LoggerSwitchParameters; + forwardingLogger.NodeId = nodeId; + forwardingLogger.BuildEventRedirector = new EventRedirectorToSink(description.LoggerId, forwardingLoggerSink); + + // Initialize and register the forwarding logger + InitializeLogger(forwardingLogger, _filterEventSource); + } + } + } + + #region Event based logging methods + + /// + /// Will Log a build Event. Will also take into account OnlyLogCriticalEvents when determining + /// if to drop the event or to log it. + /// + /// Only the following events will be logged if OnlyLogCriticalEvents is true: + /// CustomEventArgs + /// BuildErrorEventArgs + /// BuildWarningEventArgs + /// + /// BuildEvent to log + /// buildEvent is null + public void LogBuildEvent(BuildEventArgs buildEvent) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + + BuildWarningEventArgs warningEvent = null; + BuildErrorEventArgs errorEvent = null; + BuildMessageEventArgs messageEvent = null; + + if ((warningEvent = buildEvent as BuildWarningEventArgs) != null && warningEvent.BuildEventContext != null && warningEvent.BuildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) + { + warningEvent.ProjectFile = GetAndVerifyProjectFileFromContext(warningEvent.BuildEventContext); + } + else if ((errorEvent = buildEvent as BuildErrorEventArgs) != null && errorEvent.BuildEventContext != null && errorEvent.BuildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) + { + errorEvent.ProjectFile = GetAndVerifyProjectFileFromContext(errorEvent.BuildEventContext); + } + else if ((messageEvent = buildEvent as BuildMessageEventArgs) != null && messageEvent.BuildEventContext != null && messageEvent.BuildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) + { + messageEvent.ProjectFile = GetAndVerifyProjectFileFromContext(messageEvent.BuildEventContext); + } + + if (OnlyLogCriticalEvents) + { + // Only log certain events if OnlyLogCriticalEvents is true + if ( + (warningEvent != null) + || (errorEvent != null) + || (buildEvent is CustomBuildEventArgs) + || (buildEvent is CriticalBuildMessageEventArgs) + ) + { + ProcessLoggingEvent(buildEvent); + } + } + else + { + // Log all events if OnlyLogCriticalEvents is false + ProcessLoggingEvent(buildEvent); + } + } + } + + #endregion + + /// + /// This method will becalled from multiple threads in asynchronous mode. + /// + /// Determine where to send the buildevent either to the filters or to a specific sink. + /// When in Asynchronous mode the event should to into the logging queue (as long as we are initialized). + /// In Synchronous mode the event should be routed to the correct sink or logger right away + /// + /// BuildEventArgs to process + /// buildEvent is null + internal virtual void ProcessLoggingEvent(object buildEvent, bool allowThrottling = false) + { + ErrorUtilities.VerifyThrow(buildEvent != null, "buildEvent is null"); + if (_logMode == LoggerMode.Asynchronous) + { + // If the queue is at capacity, this call will block - the task returned by SendAsync only completes + // when the message is actually consumed or rejected (permanently) by the buffer. + var task = _loggingQueue.SendAsync(buildEvent); + if (allowThrottling) + { + task.Wait(); + } + } + else + { + lock (_lockObject) + { + RouteBuildEvent(buildEvent); + } + } + } + + /// + /// Wait for the logging messages in the logging queue to be completly processed. + /// This is required because for Logging build finished or when the component is to shutdown + /// we need to make sure we process all of the events before the build finished event is raised + /// and we need to make sure we process all of the logging events before we shutdown the component. + /// + internal void WaitForThreadToProcessEvents() + { + // This method may be called in the shutdown submission callback, this callback may be called after the logging service has + // shutdown and nulled out the events we were going to wait on. + if (_logMode == LoggerMode.Asynchronous && _loggingQueue != null) + { + TerminateLoggingEventQueue(); + CreateLoggingEventQueue(); + } + } + + /// + /// Adds data to the EventArgs of the log packet that the main node is aware of, but doesn't + /// get serialized for perf reasons. + /// + internal void InjectNonSerializedData(LogMessagePacket loggingPacket) + { + if (loggingPacket != null && loggingPacket.NodeBuildEvent != null && _componentHost != null) + { + var projectStartedEventArgs = loggingPacket.NodeBuildEvent.Value.Value as ProjectStartedEventArgs; + if (projectStartedEventArgs != null && _configCache.Value != null) + { + ErrorUtilities.VerifyThrow(_configCache.Value.HasConfiguration(projectStartedEventArgs.ProjectId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node."); + BuildRequestConfiguration buildRequestConfiguration = _configCache.Value[projectStartedEventArgs.ProjectId]; + s_projectStartedEventArgsGlobalProperties.Value.SetValue(projectStartedEventArgs, buildRequestConfiguration.Properties.ToDictionary(), null); + s_projectStartedEventArgsToolsVersion.Value.SetValue(projectStartedEventArgs, buildRequestConfiguration.ToolsVersion, null); + } + } + } + + #endregion + + #region Private Methods + /// + /// Create a logging thread to process the logging queue + /// + private void CreateLoggingEventQueue() + { + // We are creating a two-node dataflow graph here. The first node is a buffer, which will hold up to the number of + // logging events we have specified as the queueCapacity. The second node is the processor which will actually process each message. + // When the capacity of the buffer is reached, further attempts to send messages to it will block. + // The reason we can't just set the BoundedCapacity on the processing block is that ActionBlock has some weird behavior + // when the queue capacity is reached. Specifically, it will block new messages from being processed until it has + // entirely drained its input queue, as opposed to letting new ones in as old ones are processed. This is logged as + // a perf bug (305575) against Dataflow. If they choose to fix it, we can eliminate the buffer node from the graph. + var dataBlockOptions = new DataflowBlockOptions + { + BoundedCapacity = Convert.ToInt32(_queueCapacity) + }; + + _loggingQueue = new BufferBlock(dataBlockOptions); + + var executionDataBlockOptions = new ExecutionDataflowBlockOptions + { + BoundedCapacity = 1 + }; + + _loggingQueueProcessor = new ActionBlock(loggingEvent => LoggingEventProcessor(loggingEvent), executionDataBlockOptions); + + var dataLinkOptions = new DataflowLinkOptions + { + PropagateCompletion = true + }; + + _loggingQueue.LinkTo(_loggingQueueProcessor, dataLinkOptions); + } + + /// + /// Wait for the logginQueue to empty and then terminate the logging thread + /// + private void TerminateLoggingEventQueue() + { + // Dont accept any more items from other threads. + _loggingQueue.Complete(); + + // Wait for completion + _loggingQueueProcessor.Completion.Wait(); + } + + /// + /// Shutdown an ILogger + /// Rethrow LoggerExceptions + /// Wrap all other exceptions in an InternalLoggerException + /// + /// Logger to shutdown + /// Any exception comming from a logger during shutdown that is not a LoggerException is wrapped in an InternalLoggerException and thrown + /// Errors during logger shutdown may throw a LoggerException, in this case the exception is re-thrown + private void ShutdownLogger(ILogger logger) + { + try + { + if (logger != null) + { + logger.Shutdown(); + } + } + catch (LoggerException) + { + throw; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + InternalLoggerException.Throw(e, null, "FatalErrorDuringLoggerShutdown", false, logger.GetType().Name); + } + } + + /// + /// Create an event source to which the distributed (filter loggers) can attach to and listen + /// for logging events. This event source will consume events which are logged against + /// the logging service and raise them on itself. + /// + private void CreateFilterEventSource() + { + if (_filterEventSource == null) + { + _filterEventSource = new EventSourceSink(); + _filterEventSource.Name = "Sink for Distributed/Filter loggers"; + } + } + + /// + /// The logging services thread loop. This loop will wait until the logging queue has build events. + /// When this happens the thread will start to process the queue items by raising the build event + /// on either a filter event source or a sink depending on where the message is supposed to go. + /// + /// WaitHandle returns something other than 0 or 1 + private void LoggingEventProcessor(object loggingEvent) + { + // Save the culture so at the end of the threadproc if something else reuses this thread then it will not have a culture which it was not expecting. + CultureInfo originalCultureInfo = null; + CultureInfo originalUICultureInfo = null; + + bool cultureSet = false; + try + { + // If we have a componenthost then set the culture on the first message we recieve + if (_componentHost != null) + { + originalCultureInfo = Thread.CurrentThread.CurrentCulture; + originalUICultureInfo = Thread.CurrentThread.CurrentUICulture; + + Thread.CurrentThread.CurrentCulture = _componentHost.BuildParameters.Culture; + Thread.CurrentThread.CurrentUICulture = _componentHost.BuildParameters.UICulture; + cultureSet = true; + } + + RouteBuildEvent(loggingEvent); + } + catch (Exception e) + { + // Dump all engine exceptions to a temp file + // so that we have something to go on in the + // event of a failure + ExceptionHandling.DumpExceptionToFile(e); + + // Catch all exceptions in order to pass them over to the engine thread. Due to + // hosts expecting to get logger exceptions on the same thread the engine was called from. + if (OnLoggingThreadException == null) + { + throw; + } + + RaiseLoggingExceptionEvent(e); + } + finally + { + if (cultureSet) + { + // Set the culture back to the original one so that if something else reuses this thread then it will not have a culture which it was not expecting. + Thread.CurrentThread.CurrentCulture = originalCultureInfo; + Thread.CurrentThread.CurrentUICulture = originalUICultureInfo; + } + } + } + + /// + /// Route the event to the correct location, this is mostly used by the logging thread since it may have a buildevent or a tuple. + /// + private void RouteBuildEvent(object loggingEvent) + { + if (loggingEvent is BuildEventArgs) + { + RouteBuildEvent((BuildEventArgs)loggingEvent); + } + else if (loggingEvent is KeyValuePair) + { + RouteBuildEvent((KeyValuePair)loggingEvent); + } + else + { + ErrorUtilities.VerifyThrow(false, "Unknown logging item in queue:" + loggingEvent.GetType().FullName); + } + } + + /// + /// Route the build event to the correct filter or sink depending on what the sinId is in the build event. + /// + private void RouteBuildEvent(KeyValuePair nodeEvent) + { + TryRaiseProjectStartedEvent(nodeEvent.Value); + + // Get the sink which will handle the build event, then send the event to that sink + IBuildEventSink sink; + bool gotSink = _eventSinkDictionary.TryGetValue(nodeEvent.Key, out sink); + if (gotSink && sink != null) + { + // Sinks in the eventSinkDictionary are expected to not be null. + sink.Consume(nodeEvent.Value, nodeEvent.Key); + } + + TryRaiseProjectFinishedEvent(nodeEvent.Value); + } + + /// + /// Route the build event to the filter + /// + /// Build event that needs to be routed to the correct filter or sink. + private void RouteBuildEvent(BuildEventArgs eventArg) + { + TryRaiseProjectStartedEvent(eventArg); + + // The event has not been through a filter yet. All events must go through a filter before they make it to a logger + if (_filterEventSource != null) // Loggers may not be registered + { + // Send the event to the filter, the Consume will not return until all of the loggers which have registered to the event have process + // them. + _filterEventSource.Consume(eventArg); + + // Now that the forwarding loggers have been given the chance to log the build started and finished events we need to check the + // central logger sinks to see if they have recieved the events or not. If the sink has not recieved the event we need to send it to the + // logger for backwards compatibility with orcas. + // In addition we need to make sure we manually forward the events because in orcas the forwarding loggers were not allowed to + // forward build started or build finished events. In the new OM we allow the loggers to forward the events. However since orcas did not forward them + // we need to support loggers which cannot forward the events. + if (eventArg is BuildStartedEventArgs) + { + foreach (KeyValuePair pair in _eventSinkDictionary) + { + IBuildEventSink sink = pair.Value; + if (sink != null) + { + if (!sink.HaveLoggedBuildStartedEvent) + { + sink.Consume(eventArg, (int)pair.Key); + } + + // Reset the HaveLoggedBuildStarted event because no one else will be sending a build started event to any loggers at this time. + sink.HaveLoggedBuildStartedEvent = false; + } + } + } + else if (eventArg is BuildFinishedEventArgs) + { + foreach (KeyValuePair pair in _eventSinkDictionary) + { + IBuildEventSink sink = pair.Value; + + if (sink != null) + { + if (!sink.HaveLoggedBuildFinishedEvent) + { + sink.Consume(eventArg, (int)pair.Key); + } + + // Reset the HaveLoggedBuildFinished event because no one else will be sending a build finished event to any loggers at this time. + sink.HaveLoggedBuildFinishedEvent = false; + } + } + } + } + + TryRaiseProjectFinishedEvent(eventArg); + } + + /// + /// Initializes the logger and adds it to the list of loggers maintained by the engine. + /// This method is not expected to be called from multiple threads + /// + /// A logger exception thrown by a logger when its initialize call is made + /// Any exceptions from initializing the logger which are not loggerExceptions are caught and wrapped in a InternalLoggerException + /// Any exception which is a ExceptionHandling.IsCriticalException will not be wrapped + private void InitializeLogger(ILogger logger, IEventSource sourceForLogger) + { + try + { + INodeLogger nodeLogger = logger as INodeLogger; + if (null != nodeLogger) + { + nodeLogger.Initialize(sourceForLogger, _maxCPUCount); + } + else + { + logger.Initialize(sourceForLogger); + } + } + catch (LoggerException) + { + throw; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + InternalLoggerException.Throw(e, null, "FatalErrorWhileInitializingLogger", true, logger.GetType().Name); + } + + // Keep track of the loggers so they can be unregistered later on + _iloggerList.Add(logger); + } + + /// + /// When an exception is raised in the logging thread, we do not want the application to terminate right away. + /// Whidby and orcas msbuild have the logger exceptions occuring on the engine thread so that the host can + /// catch and deal with these exceptions as they may occure somewhat frequently due to user generated loggers. + /// This method will raise the exception on a delegate to which the engine is registered to. This delegate will + /// send the exception to the engine so that it can be raised on the engine thread. + /// + /// Exception to raise to event handlers + private void RaiseLoggingExceptionEvent(Exception ex) + { + LoggingExceptionDelegate loggingException = OnLoggingThreadException; + + if (loggingException != null) + { + loggingException(ex); + } + } + + /// + /// Raise the project started event, if necessary. + /// + private void TryRaiseProjectStartedEvent(BuildEventArgs args) + { + ProjectStartedEventHandler eventHandler = OnProjectStarted; + + if (eventHandler != null) + { + ProjectStartedEventArgs startedEventArgs = args as ProjectStartedEventArgs; + if (startedEventArgs != null) + { + eventHandler(this, startedEventArgs); + } + } + } + + /// + /// Raise the project finished event, if necessary. + /// + private void TryRaiseProjectFinishedEvent(BuildEventArgs args) + { + ProjectFinishedEventHandler eventHandler = OnProjectFinished; + + if (eventHandler != null) + { + ProjectFinishedEventArgs finishedEventArgs = args as ProjectFinishedEventArgs; + if (finishedEventArgs != null) + { + eventHandler(this, finishedEventArgs); + } + } + } + + /// + /// Get the project name from a context ID. Throw an exception if it's not found. + /// + private string GetAndVerifyProjectFileFromContext(BuildEventContext context) + { + string projectFile; + _projectFileMap.TryGetValue(context.ProjectContextId, out projectFile); + + // PERF: Not using VerifyThrow to avoid boxing an int in the non-error case. + if (projectFile == null) + { + ErrorUtilities.ThrowInternalError("ContextID {0} should have been in the ID-to-project file mapping but wasn't!", context.ProjectContextId); + } + + return projectFile; + } + #endregion + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceFactory.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceFactory.cs new file mode 100644 index 00000000000..54ce7f75f2b --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceFactory.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Factory to create components of the type LoggingService +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Factory to create components of the type LoggingService + /// + internal class LoggingServiceFactory + { + #region Data + + /// + /// What kind of LoggerMode are the logging services when created. + /// They could be Synchronous or Asynchronous + /// + private LoggerMode _logMode = LoggerMode.Synchronous; + + /// + /// What node is this logging service being created on. + /// + private int _nodeId = 0; + #endregion + + #region Constructor + + /// + /// Tell the factory what kind of logging services is should create + /// + /// Synchronous or Asynchronous + internal LoggingServiceFactory(LoggerMode mode, int nodeId) + { + _logMode = mode; + _nodeId = nodeId; + } + + #endregion + #region Members + + /// + /// Create an instance of a LoggingService and returns is as an IBuildComponent + /// + /// An instance of a LoggingService as a IBuildComponent + public IBuildComponent CreateInstance(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.LoggingService, "Cannot create components of type {0}", type); + IBuildComponent loggingService = (IBuildComponent)LoggingService.CreateLoggingService(_logMode, _nodeId); + return loggingService; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceLogMethods.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceLogMethods.cs new file mode 100644 index 00000000000..4472a0783e1 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/LoggingServiceLogMethods.cs @@ -0,0 +1,812 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Logging service which assists in getting build events to the correct loggers +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Partial class half of LoggingService which contains the Logging methods. + /// + internal partial class LoggingService : ILoggingService, INodePacketHandler, IBuildComponent + { + #region Log comments + + /// + /// Logs a comment (BuildMessageEventArgs) with a certain MessageImportance level + /// + /// Event context information which describes who is logging the event + /// How important is the message, this will determine which verbosities the message will show up on. + /// The higher the importance the lower the verbosity needs to be for the message to be seen + /// String which identifies the message in the string resx + /// Arguments for the format string indexed by messageResourceName + /// MessageResourceName is null + public void LogComment(BuildEventContext buildEventContext, MessageImportance importance, string messageResourceName, params object[] messageArgs) + { + lock (_lockObject) + { + if (!OnlyLogCriticalEvents) + { + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for comment message."); + + LogCommentFromText(buildEventContext, importance, ResourceUtilities.GetResourceString(messageResourceName), messageArgs); + } + } + } + + /// + /// Log a comment + /// + /// Event context information which describes who is logging the event + /// How important is the message, this will determine which verbosities the message will show up on. + /// The higher the importance the lower the verbosity needs to be for the message to be seen + /// Message to log + /// BuildEventContext is null + /// Message is null + public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message) + { + lock (_lockObject) + { + this.LogCommentFromText(buildEventContext, importance, message, null); + } + } + + /// + /// Log a comment + /// + /// Event context information which describes who is logging the event + /// How important is the message, this will determine which verbosities the message will show up on. + /// The higher the importance the lower the verbosity needs to be for the message to be seen + /// Message to log + /// Message formatting arguments + /// BuildEventContext is null + /// Message is null + public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message, params object[] messageArgs) + { + lock (_lockObject) + { + if (!OnlyLogCriticalEvents) + { + ErrorUtilities.VerifyThrow(buildEventContext != null, "buildEventContext was null"); + ErrorUtilities.VerifyThrow(message != null, "message was null"); + + BuildMessageEventArgs buildEvent = new BuildMessageEventArgs + ( + message, + null, + "MSBuild", + importance, + DateTime.UtcNow, + messageArgs + ); + buildEvent.BuildEventContext = buildEventContext; + ProcessLoggingEvent(buildEvent); + } + } + } + #endregion + + #region Log errors + /************************************************************************************************************************** + * WARNING: Do not add overloads that allow raising events without specifying a file. In general ALL events should have a + * file associated with them. We've received a LOT of feedback from dogfooders about the lack of information in our + * events. If an event TRULY does not have an associated file, then String.Empty can be passed in for the file. However, + * that burden should lie on the caller -- these wrapper methods should NOT make it easy to skip the filename. + *************************************************************************************************************************/ + + /// + /// Logs an error with all registered loggers using the specified resource string. + /// + /// Event context information which describes who is logging the event + /// File information where the error happened + /// String key to find the correct string resource + /// Arguments for the string resource + public void LogError(BuildEventContext location, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + lock (_lockObject) + { + LogError(location, null, file, messageResourceName, messageArgs); + } + } + + /// + /// Logs an error + /// + /// Event context information which describes who is logging the event + /// Can be null. + /// File information about where the error happened + /// String index into the string.resx file + /// Arguments for the format string in the resource file + /// MessageResouceName is null + public void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for error message."); + + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, messageResourceName, messageArgs); + + LogErrorFromText(buildEventContext, subcategoryResourceName, errorCode, helpKeyword, file, message); + } + } + + /// + /// Logs an error with a given message + /// + /// Event context information which describes who is logging the event + /// Can be null. + /// Can be null. + /// Can be null. + /// File information about where the error happened + /// Error message which will be displayed + /// File is null + /// Message is null + public void LogErrorFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string errorCode, string helpKeyword, BuildEventFileInfo file, string message) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(buildEventContext != null, "Must specify the buildEventContext"); + ErrorUtilities.VerifyThrow(file != null, "Must specify the associated file."); + ErrorUtilities.VerifyThrow(message != null, "Need error message."); + + string subcategory = null; + + if (subcategoryResourceName != null) + { + subcategory = AssemblyResources.GetString(subcategoryResourceName); + } + + BuildErrorEventArgs buildEvent = + new BuildErrorEventArgs + ( + subcategory, + errorCode, + file.File, + file.Line, + file.Column, + file.EndLine, + file.EndColumn, + message, + helpKeyword, + "MSBuild" + ); + + buildEvent.BuildEventContext = buildEventContext; + if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) + { + string projectFile; + _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out projectFile); + ErrorUtilities.VerifyThrow(projectFile != null, "ContextID {0} should have been in the ID-to-project file mapping but wasn't!", buildEventContext.ProjectContextId); + buildEvent.ProjectFile = projectFile; + } + + ProcessLoggingEvent(buildEvent); + } + } + + /// + /// Logs an error regarding an invalid project file . Since this method may be multiple times for the same InvalidProjectException + /// we do not want to log the error multiple times. Once the exception has been logged we set a flag on the exception to note that + /// it has already been logged. + /// + /// Event context information which describes who is logging the event + /// Exception which is causing the error + /// InvalidProjectFileException is null + /// BuildEventContext is null + public void LogInvalidProjectFileError(BuildEventContext buildEventContext, InvalidProjectFileException invalidProjectFileException) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(invalidProjectFileException != null, "Need exception context."); + ErrorUtilities.VerifyThrow(buildEventContext != null, "buildEventContext is null"); + + // Don't log the exception more than once. + if (!invalidProjectFileException.HasBeenLogged) + { + BuildErrorEventArgs buildEvent = + new BuildErrorEventArgs + ( + invalidProjectFileException.ErrorSubcategory, + invalidProjectFileException.ErrorCode, + invalidProjectFileException.ProjectFile, + invalidProjectFileException.LineNumber, + invalidProjectFileException.ColumnNumber, + invalidProjectFileException.EndLineNumber, + invalidProjectFileException.EndColumnNumber, + invalidProjectFileException.BaseMessage, + invalidProjectFileException.HelpKeyword, + "MSBuild" + ); + buildEvent.BuildEventContext = buildEventContext; + if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) + { + string projectFile; + _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out projectFile); + ErrorUtilities.VerifyThrow(projectFile != null, "ContextID {0} should have been in the ID-to-project file mapping but wasn't!", buildEventContext.ProjectContextId); + buildEvent.ProjectFile = projectFile; + } + + ProcessLoggingEvent(buildEvent); + invalidProjectFileException.HasBeenLogged = true; + } + } + } + + /// + /// Logs an error regarding an unexpected build failure + /// This will include a stack dump. + /// + /// BuildEventContext of the error + /// Exception wihch caused the build error + /// Provides file information about where the build error happened + public void LogFatalBuildError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file) + { + lock (_lockObject) + { + LogFatalError(buildEventContext, exception, file, "FatalBuildError"); + } + } + + /// + /// Logs an error regarding an unexpected task failure. + /// This will include a stack dump. + /// + /// BuildEventContext of the error + /// Exceptionm which caused the error + /// File information which indicates which file the error is happening in + /// Task which the error is happening in + /// TaskName is null + public void LogFatalTaskError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(taskName != null, "Must specify the name of the task that failed."); + + LogFatalError(buildEventContext, exception, file, "FatalTaskError", taskName); + } + } + + /// + /// Logs an error regarding an unexpected failure using the specified resource string. + /// This will include a stack dump. + /// + /// BuildEventContext of the error + /// Exception which will be used to generate the error message + /// File information which describes where the error happened + /// String name for the resource string to be used + /// Arguments for messageResourceName + /// MessageResourceName is null + public void LogFatalError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for error message."); + + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, messageResourceName, messageArgs); +#if DEBUG + message += Environment.NewLine + "This is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER."; +#endif + if (exception != null) + { + message += Environment.NewLine + exception.ToString(); + } + + LogErrorFromText(buildEventContext, null, errorCode, helpKeyword, file, message); + } + } + + #endregion + + #region Log warnings + /************************************************************************************************************************** + * WARNING: Do not add overloads that allow raising events without specifying a file. In general ALL events should have a + * file associated with them. We've received a LOT of feedback from dogfooders about the lack of information in our + * events. If an event TRULY does not have an associated file, then String.Empty can be passed in for the file. However, + * that burden should lie on the caller -- these wrapper methods should NOT make it easy to skip the filename. + *************************************************************************************************************************/ + + /// + /// Logs an warning regarding an unexpected task failure + /// This will include a stack dump. + /// + /// Event context information which describes who is logging the event + /// The exception to be used to create the warning text + /// The file information which indicates where the warning happened + /// Name of the task which the warning is being raised from + public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(taskName), "Must specify the name of the task that failed."); + + string warningCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out warningCode, out helpKeyword, "FatalTaskError", taskName); +#if DEBUG + message += Environment.NewLine + "This is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER."; +#endif + + if (exception != null) + { + message += Environment.NewLine + exception.ToString(); + } + + LogWarningFromText(buildEventContext, null, warningCode, helpKeyword, file, message); + } + } + + /// + /// Logs a warning using the specified resource string. + /// + /// Event context information which describes who is logging the event + /// Can be null. + /// File information which describes where the warning happened + /// String name for the resource string to be used + /// Arguments for messageResourceName + public void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(messageResourceName), "Need resource string for warning message."); + + string warningCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out warningCode, out helpKeyword, messageResourceName, messageArgs); + LogWarningFromText(buildEventContext, subcategoryResourceName, warningCode, helpKeyword, file, message); + } + } + + /// + /// Logs a warning + /// + /// Event context information which describes who is logging the event + /// Subcategory resource Name. Can be null. + /// The warning code of the message. Can be null. + /// Help keyword for the message. Can be null. + /// The file information which will describe where the warning happened + /// Warning message to log + public void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(file != null, "Must specify the associated file."); + ErrorUtilities.VerifyThrow(message != null, "Need warning message."); + ErrorUtilities.VerifyThrow(buildEventContext != null, "Need a BuildEventContext"); + + string subcategory = null; + + if (subcategoryResourceName != null) + { + subcategory = AssemblyResources.GetString(subcategoryResourceName); + } + + BuildWarningEventArgs buildEvent = new BuildWarningEventArgs + ( + subcategory, + warningCode, + file.File, + file.Line, + file.Column, + file.EndLine, + file.EndColumn, + message, + helpKeyword, + "MSBuild" + ); + + buildEvent.BuildEventContext = buildEventContext; + if (buildEvent.ProjectFile == null && buildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId) + { + string projectFile; + _projectFileMap.TryGetValue(buildEventContext.ProjectContextId, out projectFile); + ErrorUtilities.VerifyThrow(projectFile != null, "ContextID {0} should have been in the ID-to-project file mapping but wasn't!", buildEventContext.ProjectContextId); + buildEvent.ProjectFile = projectFile; + } + + ProcessLoggingEvent(buildEvent); + } + } + + #endregion + + #region Log status + + /// + /// Logs that the build has started + /// + public void LogBuildStarted() + { + lock (_lockObject) + { + // If we're only logging critical events, don't risk causing all the resources to load by formatting + // a string that won't get emitted anyway. + string message = String.Empty; + if (!OnlyLogCriticalEvents) + { + message = ResourceUtilities.FormatResourceString("BuildStarted"); + } + + IDictionary environmentProperties = null; + + if (_componentHost != null && _componentHost.BuildParameters != null) + { + environmentProperties = _componentHost.BuildParameters.BuildProcessEnvironment; + } + + BuildStartedEventArgs buildEvent = new BuildStartedEventArgs(message, null /* no help keyword */, environmentProperties); + + // Raise the event with the filters + ProcessLoggingEvent(buildEvent); + + // Make sure we process this event before going any further + if (_logMode == LoggerMode.Asynchronous) + { + WaitForThreadToProcessEvents(); + } + } + } + + /// + /// Logs that the build has finished + /// + /// Did the build pass or fail + public void LogBuildFinished(bool success) + { + lock (_lockObject) + { + // If we're only logging critical events, don't risk causing all the resources to load by formatting + // a string that won't get emitted anyway. + string message = String.Empty; + if (!OnlyLogCriticalEvents) + { + message = ResourceUtilities.FormatResourceString(success ? "BuildFinishedSuccess" : "BuildFinishedFailure"); + } + + BuildFinishedEventArgs buildEvent = new BuildFinishedEventArgs(message, null /* no help keyword */, success); + + ProcessLoggingEvent(buildEvent); + + if (_logMode == LoggerMode.Asynchronous) + { + WaitForThreadToProcessEvents(); + } + } + } + + /// + /// Logs that a project build has started + /// + /// The event context of the node which is spawning this project. + /// The id of the submission. + /// Id of the project instance which is being started + /// BuildEventContext of the project who is requesting "projectFile" to build + /// Project file to build + /// Target names to build + /// Initial property list + /// Initial items list + /// The build event context for the project. + /// parentBuildEventContext is null + /// projectBuildEventContext is null + public BuildEventContext LogProjectStarted(BuildEventContext nodeBuildEventContext, int submissionId, int projectInstanceId, BuildEventContext parentBuildEventContext, string projectFile, string targetNames, IEnumerable properties, IEnumerable items) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(nodeBuildEventContext != null, "Need a nodeBuildEventContext"); + BuildEventContext projectBuildEventContext = new BuildEventContext(submissionId, nodeBuildEventContext.NodeId, projectInstanceId, NextProjectId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidTaskId); + + // PERF: Not using VerifyThrow to avoid boxing of projectBuildEventContext.ProjectContextId in the non-error case. + if (_projectFileMap.ContainsKey(projectBuildEventContext.ProjectContextId)) + { + ErrorUtilities.ThrowInternalError("ContextID {0} for project {1} should not already be in the ID-to-file mapping!", projectBuildEventContext.ProjectContextId, projectFile); + } + + _projectFileMap[projectBuildEventContext.ProjectContextId] = projectFile; + + ErrorUtilities.VerifyThrow(parentBuildEventContext != null, "Need a parentBuildEventContext"); + + string message = string.Empty; + string projectFilePath = Path.GetFileName(projectFile); + + // Check to see if the there are any specific target names to be built. + // If targetNames is null or empty then we will be building with the + // default targets. + if (!String.IsNullOrEmpty(targetNames)) + { + message = ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithTargetNames", projectFilePath, targetNames); + } + else + { + message = ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", projectFilePath); + } + + ErrorUtilities.VerifyThrow(_configCache.Value.HasConfiguration(projectInstanceId), "Cannot find the project configuration while injecting non-serialized data from out-of-proc node."); + var buildRequestConfiguration = _configCache.Value[projectInstanceId]; + ProjectStartedEventArgs buildEvent = new ProjectStartedEventArgs + ( + projectInstanceId, + message, + null, // no help keyword + projectFile, + targetNames, + properties, + items, + parentBuildEventContext, + buildRequestConfiguration.Properties.ToDictionary(), + buildRequestConfiguration.ToolsVersion + ); + buildEvent.BuildEventContext = projectBuildEventContext; + + ProcessLoggingEvent(buildEvent); + + return projectBuildEventContext; + } + } + + /// + /// Logs that a project has finished + /// + /// Event context for the project. + /// Project file being built + /// Did the project pass or fail + /// BuildEventContext is null + public void LogProjectFinished(BuildEventContext projectBuildEventContext, string projectFile, bool success) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(projectBuildEventContext != null, "projectBuildEventContext"); + + string message = ResourceUtilities.FormatResourceString((success ? "ProjectFinishedSuccess" : "ProjectFinishedFailure"), Path.GetFileName(projectFile)); + + ProjectFinishedEventArgs buildEvent = new ProjectFinishedEventArgs + ( + message, + null, // no help keyword + projectFile, + success + ); + buildEvent.BuildEventContext = projectBuildEventContext; + ProcessLoggingEvent(buildEvent); + + // PERF: Not using VerifyThrow to avoid boxing of projectBuildEventContext.ProjectContextId in the non-error case. + if (!_projectFileMap.ContainsKey(projectBuildEventContext.ProjectContextId)) + { + ErrorUtilities.ThrowInternalError("ContextID {0} for project {1} should be in the ID-to-file mapping!", projectBuildEventContext.ProjectContextId, projectFile); + } + + _projectFileMap.Remove(projectBuildEventContext.ProjectContextId); + } + } + + /// + /// Logs that a target started + /// + /// Event context for the project spawning this target + /// Name of target + /// Project file being built + /// Project file which contains the target + /// The build event context for the target. + /// BuildEventContext is null + public BuildEventContext LogTargetStarted(BuildEventContext projectBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, string parentTargetName) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(projectBuildEventContext != null, "projectBuildEventContext is null"); + BuildEventContext targetBuildEventContext = new BuildEventContext + ( + projectBuildEventContext.SubmissionId, + projectBuildEventContext.NodeId, + projectBuildEventContext.ProjectInstanceId, + projectBuildEventContext.ProjectContextId, + NextTargetId, + BuildEventContext.InvalidTaskId + ); + + string message = String.Empty; + if (!OnlyLogCriticalEvents) + { + if (String.Equals(projectFile, projectFileOfTargetElement, StringComparison.OrdinalIgnoreCase)) + { + if (!String.IsNullOrEmpty(parentTargetName)) + { + message = ResourceUtilities.FormatResourceString("TargetStartedProjectDepends", targetName, projectFile, parentTargetName); + } + else + { + message = ResourceUtilities.FormatResourceString("TargetStartedProjectEntry", targetName, projectFile); + } + } + else + { + if (!String.IsNullOrEmpty(parentTargetName)) + { + message = ResourceUtilities.FormatResourceString("TargetStartedFileProjectDepends", targetName, projectFileOfTargetElement, projectFile, parentTargetName); + } + else + { + message = ResourceUtilities.FormatResourceString("TargetStartedFileProjectEntry", targetName, projectFileOfTargetElement, projectFile); + } + } + + TargetStartedEventArgs buildEvent = new TargetStartedEventArgs + ( + message, + null, // no help keyword + targetName, + projectFile, + projectFileOfTargetElement, + parentTargetName, + DateTime.UtcNow + ); + buildEvent.BuildEventContext = targetBuildEventContext; + ProcessLoggingEvent(buildEvent); + } + + return targetBuildEventContext; + } + } + + /// + /// Logs that a target has finished. + /// + /// Event context for the target + /// Target which has just finished + /// Project file being built + /// Project file which contains the target + /// Did the target pass or fail + /// Target outputs for the target. + /// BuildEventContext is null + public void LogTargetFinished(BuildEventContext targetBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, bool success, IEnumerable targetOutputs) + { + lock (_lockObject) + { + if (!OnlyLogCriticalEvents) + { + ErrorUtilities.VerifyThrow(targetBuildEventContext != null, "targetBuildEventContext is null"); + + string message = ResourceUtilities.FormatResourceString((success ? "TargetFinishedSuccess" : "TargetFinishedFailure"), targetName, Path.GetFileName(projectFile)); + + TargetFinishedEventArgs buildEvent = new TargetFinishedEventArgs + ( + message, + null, // no help keyword + targetName, + projectFile, + projectFileOfTargetElement, + success, + targetOutputs + ); + + buildEvent.BuildEventContext = targetBuildEventContext; + ProcessLoggingEvent(buildEvent); + } + } + } + + /// + /// Logs that task execution has started. + /// + /// Event context for the task + /// Task Name + /// Project file being built + /// Project file which contains the task + /// BuildEventContext is null + public void LogTaskStarted(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(taskBuildEventContext != null, "targetBuildEventContext is null"); + if (!OnlyLogCriticalEvents) + { + TaskStartedEventArgs buildEvent = new TaskStartedEventArgs + ( + ResourceUtilities.FormatResourceString("TaskStarted", taskName), + null, // no help keyword + projectFile, + projectFileOfTaskNode, + taskName + ); + buildEvent.BuildEventContext = taskBuildEventContext; + ProcessLoggingEvent(buildEvent); + } + } + } + + /// + /// Logs that task execution has started. + /// + /// Event context for the target spawning this task. + /// Task Name + /// Project file being built + /// Project file which contains the task + /// The build event context for the task. + /// BuildEventContext is null + public BuildEventContext LogTaskStarted2(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode) + { + lock (_lockObject) + { + ErrorUtilities.VerifyThrow(targetBuildEventContext != null, "targetBuildEventContext is null"); + BuildEventContext taskBuildEventContext = new BuildEventContext + ( + targetBuildEventContext.SubmissionId, + targetBuildEventContext.NodeId, + targetBuildEventContext.ProjectInstanceId, + targetBuildEventContext.ProjectContextId, + targetBuildEventContext.TargetId, + NextTaskId + ); + + if (!OnlyLogCriticalEvents) + { + TaskStartedEventArgs buildEvent = new TaskStartedEventArgs + ( + ResourceUtilities.FormatResourceString("TaskStarted", taskName), + null, // no help keyword + projectFile, + projectFileOfTaskNode, + taskName + ); + buildEvent.BuildEventContext = taskBuildEventContext; + ProcessLoggingEvent(buildEvent); + } + + return taskBuildEventContext; + } + } + + /// + /// Logs that a task has finished executing. + /// + /// Event context for the task + /// Name of the task + /// Project which is being processed + /// Project file which contains the task + /// Did the task pass or fail + /// BuildEventContext is null + public void LogTaskFinished(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, bool success) + { + lock (_lockObject) + { + if (!OnlyLogCriticalEvents) + { + ErrorUtilities.VerifyThrow(taskBuildEventContext != null, "taskBuildEventContext is null"); + string message = ResourceUtilities.FormatResourceString((success ? "TaskFinishedSuccess" : "TaskFinishedFailure"), taskName); + + TaskFinishedEventArgs buildEvent = new TaskFinishedEventArgs + ( + message, + null, // no help keyword + projectFile, + projectFileOfTaskNode, + taskName, + success + ); + buildEvent.BuildEventContext = taskBuildEventContext; + ProcessLoggingEvent(buildEvent); + } + } + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/NodeLoggingContext.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/NodeLoggingContext.cs new file mode 100644 index 00000000000..c8b11fab307 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/NodeLoggingContext.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A logging context for nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// The logging context for an entire node. + /// + internal class NodeLoggingContext : BaseLoggingContext + { + /// + /// Used to create the initial, base logging context for the node. + /// + /// The logging service to use. + /// The + internal NodeLoggingContext(ILoggingService loggingService, int nodeId, bool inProcNode) + : base(loggingService, new BuildEventContext(nodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId), inProcNode) + { + ErrorUtilities.VerifyThrow(nodeId != BuildEventContext.InvalidNodeId, "Should not ever be given an invalid NodeId"); + + // The in-proc node will have its BuildStarted, BuildFinished events sent by the BuildManager itself. + if (!IsInProcNode) + { + LoggingService.LogBuildStarted(); + } + + this.IsValid = true; + } + + /// + /// Log the completion of a build + /// + /// Did the build succeed or not + internal void LogBuildFinished(bool success) + { + ErrorUtilities.VerifyThrow(this.IsValid, "Build not started."); + + // The in-proc node will have its BuildStarted, BuildFinished events sent by the BuildManager itself. + if (!IsInProcNode) + { + LoggingService.LogBuildFinished(success); + } + + this.IsValid = false; + } + + /// + /// Log that a project has started if it has no parent (the first project) + /// + /// The build request entry for this project. + /// The BuildEventContext to use for this project. + internal ProjectLoggingContext LogProjectStarted(BuildRequestEntry requestEntry) + { + ErrorUtilities.VerifyThrow(this.IsValid, "Build not started."); + return new ProjectLoggingContext(this, requestEntry, requestEntry.Request.ParentBuildEventContext); + } + + /// + /// Log that a project has started if it is serviced from the cache + /// + /// The build request. + /// The configuration used to build the request. + /// The BuildEventContext to use for this project. + internal ProjectLoggingContext LogProjectStarted(BuildRequest request, BuildRequestConfiguration configuration) + { + ErrorUtilities.VerifyThrow(this.IsValid, "Build not started."); + return new ProjectLoggingContext(this, request, configuration.ProjectFullPath, configuration.ToolsVersion, request.ParentBuildEventContext); + } + + /// + /// Logs the project started/finished pair for projects which are skipped entirely because all + /// of their results are available in the cache. + /// + internal void LogRequestHandledFromCache(BuildRequest request, BuildRequestConfiguration configuration, BuildResult result) + { + ProjectLoggingContext projectLoggingContext = LogProjectStarted(request, configuration); + + // When pulling a request from the cache, we want to make sure we log a task skipped message for any targets which + // were used to build the request including default and inital targets. + foreach (string target in configuration.GetTargetsUsedToBuildRequest(request)) + { + projectLoggingContext.LogComment + ( + MessageImportance.Low, + result[target].ResultCode == TargetResultCode.Failure ? "TargetAlreadyCompleteFailure" : "TargetAlreadyCompleteSuccess", + target + ); + + if (result[target].ResultCode == TargetResultCode.Failure) + { + break; + } + } + + projectLoggingContext.LogProjectFinished(result.OverallResult == BuildResultCode.Success); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/ProjectLoggingContext.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/ProjectLoggingContext.cs new file mode 100644 index 00000000000..5a0e4c1f978 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/ProjectLoggingContext.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A logging context for projects. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using System.Collections; +using Microsoft.Build.Shared; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// A logging context for a project. + /// + internal class ProjectLoggingContext : BaseLoggingContext + { + /// + /// The project's full path + /// + private string _projectFullPath; + + /// + /// The parent node logging context this context was derived from. + /// + private NodeLoggingContext _nodeLoggingContext; + + /// + /// Constructs a project logging context. + /// + internal ProjectLoggingContext(NodeLoggingContext nodeLoggingContext, BuildRequestEntry requestEntry, BuildEventContext parentBuildEventContext) + : this + ( + nodeLoggingContext, + requestEntry.Request.SubmissionId, + requestEntry.Request.ConfigurationId, + requestEntry.RequestConfiguration.ProjectFullPath, + requestEntry.Request.Targets, + requestEntry.RequestConfiguration.ToolsVersion, + requestEntry.RequestConfiguration.Project.PropertiesToBuildWith, + requestEntry.RequestConfiguration.Project.ItemsToBuildWith, + parentBuildEventContext + ) + { + } + + /// + /// Constructs a project logging context. + /// + internal ProjectLoggingContext(NodeLoggingContext nodeLoggingContext, BuildRequest request, string projectFullPath, string toolsVersion, BuildEventContext parentBuildEventContext) + : this + ( + nodeLoggingContext, + request.SubmissionId, + request.ConfigurationId, + projectFullPath, + request.Targets, + toolsVersion, + null, + null, + parentBuildEventContext + ) + { + } + + /// + /// Constructs a project logging contexts. + /// + private ProjectLoggingContext(NodeLoggingContext nodeLoggingContext, int submissionId, int configurationId, string projectFullPath, List targets, string toolsVersion, PropertyDictionary projectProperties, ItemDictionary projectItems, BuildEventContext parentBuildEventContext) + : base(nodeLoggingContext) + { + _nodeLoggingContext = nodeLoggingContext; + _projectFullPath = projectFullPath; + + ProjectPropertyInstanceEnumeratorProxy properties = null; + ProjectItemInstanceEnumeratorProxy items = null; + + IEnumerable projectPropertiesEnumerator = projectProperties == null ? Collections.ReadOnlyEmptyList.Instance : null; + IEnumerable projectItemsEnumerator = projectItems == null ? Collections.ReadOnlyEmptyList.Instance : null; + + string[] propertiesToSerialize = LoggingService.PropertiesToSerialize; + + // If we are only logging critical events lets not pass back the items or properties + if (!LoggingService.OnlyLogCriticalEvents && (!LoggingService.RunningOnRemoteNode || LoggingService.SerializeAllProperties)) + { + if (projectProperties != null) + { + projectPropertiesEnumerator = projectProperties.GetCopyOnReadEnumerable(); + } + + if (projectItems != null) + { + projectItemsEnumerator = projectItems.GetCopyOnReadEnumerable(); + } + + properties = new ProjectPropertyInstanceEnumeratorProxy(projectPropertiesEnumerator); + items = new ProjectItemInstanceEnumeratorProxy(projectItemsEnumerator); + } + + if (projectProperties != null && propertiesToSerialize != null && propertiesToSerialize.Length > 0 && !LoggingService.SerializeAllProperties) + { + PropertyDictionary projectPropertiesToSerialize = new PropertyDictionary(); + foreach (string propertyToGet in propertiesToSerialize) + { + ProjectPropertyInstance instance = projectProperties[propertyToGet]; + { + if (instance != null) + { + projectPropertiesToSerialize.Set(instance); + } + } + } + + properties = new ProjectPropertyInstanceEnumeratorProxy(projectPropertiesToSerialize); + } + + this.BuildEventContext = LoggingService.LogProjectStarted + ( + nodeLoggingContext.BuildEventContext, + submissionId, + configurationId, + parentBuildEventContext, + projectFullPath, + String.Join(";", targets.ToArray()), + properties, + items + ); + LoggingService.LogComment(this.BuildEventContext, MessageImportance.Low, "ToolsVersionInEffectForBuild", toolsVersion); + + this.IsValid = true; + } + + /// + /// Retrieves the node logging context. + /// + internal NodeLoggingContext NodeLoggingContext + { + get + { + return _nodeLoggingContext; + } + } + + /// + /// Log that the project has finished + /// + /// Did the build succeede or not + internal void LogProjectFinished(bool success) + { + ErrorUtilities.VerifyThrow(this.IsValid, "invalid"); + LoggingService.LogProjectFinished(BuildEventContext, _projectFullPath, success); + this.IsValid = false; + } + + /// + /// Log that a target has started + /// + internal TargetLoggingContext LogTargetBatchStarted(string projectFullPath, ProjectTargetInstance target, string parentTargetName) + { + ErrorUtilities.VerifyThrow(this.IsValid, "invalid"); + return new TargetLoggingContext(this, projectFullPath, target, parentTargetName); + } + + /// + /// An enumerable wrapper for items that clones items as they are requested, + /// so that writes have no effect on the items. + /// + /// + /// This class is designed to be passed to loggers. + /// The expense of copying items is only incurred if and when + /// a logger chooses to enumerate over it. + /// The type of the items enumerated over is imposed by backwards compatibility for ProjectStartedEvent. + /// + private class ProjectItemInstanceEnumeratorProxy : IEnumerable + { + /// + /// Enumerable that this proxies + /// + private IEnumerable _backingItems; + + /// + /// Constructor + /// + /// Enumerator this class should proxy + internal ProjectItemInstanceEnumeratorProxy(IEnumerable backingItems) + { + _backingItems = backingItems; + } + + /// + /// Returns an enumerator that provides copies of the items + /// in the backing store. + /// Each dictionary entry has key of the item type and value of an ITaskItem. + /// Type of the enumerator is imposed by backwards compatibility for ProjectStartedEvent. + /// + public IEnumerator GetEnumerator() + { + foreach (ProjectItemInstance item in _backingItems) + { + yield return new DictionaryEntry(item.ItemType, new TaskItem(item)); + } + } + + /// + /// Returns an enumerator that provides copies of the items + /// in the backing store. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + } + + /// + /// An enumerable wrapper for properties that clones properties as they are requested, + /// so that writes have no effect on the properties. + /// + /// + /// This class is designed to be passed to loggers. + /// The expense of copying items is only incurred if and when + /// a logger chooses to enumerate over it. + /// The type of the items enumerated over is imposed by backwards compatibility for ProjectStartedEvent. + /// + private class ProjectPropertyInstanceEnumeratorProxy : IEnumerable + { + /// + /// Enumerable that this proxies + /// + private IEnumerable _backingProperties; + + /// + /// Constructor + /// + /// Enumerator this class should proxy + internal ProjectPropertyInstanceEnumeratorProxy(IEnumerable backingProperties) + { + _backingProperties = backingProperties; + } + + /// + /// Returns an enumerator that provides copies of the properties + /// in the backing store. + /// Each DictionaryEntry has key of the property name and value of the property value. + /// Type of the enumerator is imposed by backwards compatibility for ProjectStartedEvent. + /// + public IEnumerator GetEnumerator() + { + foreach (ProjectPropertyInstance property in _backingProperties) + { + yield return new DictionaryEntry(property.Name, property.EvaluatedValue); + } + } + + /// + /// Returns an enumerator that provides copies of the properties + /// in the backing store. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/TargetLoggingContext.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/TargetLoggingContext.cs new file mode 100644 index 00000000000..e8f77250cb7 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/TargetLoggingContext.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A logging context for targets. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using System.Collections; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// A logging context for building a specific target within a project. + /// + internal class TargetLoggingContext : BaseLoggingContext + { + /// + /// Should target outputs be logged also. + /// + private static bool s_enableTargetOutputLogging = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING")); + + /// + /// The project to which this target is attached. + /// + private ProjectLoggingContext _projectLoggingContext; + + /// + /// The target being built. + /// + private ProjectTargetInstance _target; + + /// + /// Creates a new target logging context from an existing project context and target. + /// + internal TargetLoggingContext(ProjectLoggingContext projectLoggingContext, string projectFullPath, ProjectTargetInstance target, string parentTargetName) + : base(projectLoggingContext) + { + _projectLoggingContext = projectLoggingContext; + _target = target; + + this.BuildEventContext = LoggingService.LogTargetStarted(projectLoggingContext.BuildEventContext, target.Name, projectFullPath, target.Location.File, parentTargetName); + this.IsValid = true; + } + + /// + /// Constructor used to support out-of-proc task host (proxy for in-proc logging service.) + /// + internal TargetLoggingContext(ILoggingService loggingService, BuildEventContext outOfProcContext) + : base(loggingService, outOfProcContext, true) + { + this.IsValid = true; + } + + /// + /// Should target outputs be logged also. + /// + internal static bool EnableTargetOutputLogging + { + get { return s_enableTargetOutputLogging; } + set { s_enableTargetOutputLogging = value; } + } + + /// + /// Retrieves the project logging context. + /// + internal ProjectLoggingContext ProjectLoggingContext + { + get + { + return _projectLoggingContext; + } + } + + /// + /// Retrieves the target. + /// + internal ProjectTargetInstance Target + { + get + { + return _target; + } + } + + /// + /// Log that a target has finished + /// + internal void LogTargetBatchFinished(string projectFullPath, bool success, IEnumerable targetOutputs) + { + ErrorUtilities.VerifyThrow(IsValid, "Should be valid"); + + TargetOutputItemsInstanceEnumeratorProxy targetOutputWrapper = null; + + // Only log target outputs if we are going to log a target finished event and the environment variable is set and the target outputs are not null + if (!LoggingService.OnlyLogCriticalEvents && s_enableTargetOutputLogging && targetOutputs != null) + { + targetOutputWrapper = new TargetOutputItemsInstanceEnumeratorProxy(targetOutputs); + } + + LoggingService.LogTargetFinished(BuildEventContext, _target.Name, projectFullPath, _target.Location.File, success, targetOutputWrapper); + this.IsValid = false; + } + + /// + /// Log that a task is about to start + /// + internal TaskLoggingContext LogTaskBatchStarted(string projectFullPath, ProjectTargetInstanceChild task) + { + ErrorUtilities.VerifyThrow(IsValid, "Should be valid"); + + return new TaskLoggingContext(this, projectFullPath, task); + } + + /// + /// An enumerable wrapper for items that clones items as they are requested, + /// so that writes have no effect on the items. + /// + /// + /// This class is designed to be passed to loggers. + /// The expense of copying items is only incurred if and when + /// a logger chooses to enumerate over it. + /// + internal class TargetOutputItemsInstanceEnumeratorProxy : IEnumerable + { + /// + /// Enumerable that this proxies + /// + private IEnumerable _backingItems; + + /// + /// Constructor + /// + /// Enumerator this class should proxy + internal TargetOutputItemsInstanceEnumeratorProxy(IEnumerable backingItems) + { + _backingItems = backingItems; + } + + /// + /// Returns an enumerator that provides copies of the items + /// in the backing store. + /// Each dictionary entry has key of the item type and value of an ITaskItem. + /// Type of the enumerator is imposed by backwards compatibility for ProjectStartedEvent. + /// + public IEnumerator GetEnumerator() + { + foreach (TaskItem item in _backingItems) + { + yield return item.DeepClone(); + } + } + + /// + /// Returns an enumerator that provides copies of the items + /// in the backing store. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Logging/TaskLoggingContext.cs b/src/XMakeBuildEngine/BackEnd/Components/Logging/TaskLoggingContext.cs new file mode 100644 index 00000000000..70b13836705 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Logging/TaskLoggingContext.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A logging context for tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// A logging context representing a task being built. + /// + internal class TaskLoggingContext : BaseLoggingContext + { + /// + /// The target context in which this task is being built. + /// + private TargetLoggingContext _targetLoggingContext; + + /// + /// The task instance + /// + private ProjectTargetInstanceChild _task; + + /// + /// The name of the task + /// + private string _taskName; + + /// + /// Constructs a task logging context from a parent target context and a task node. + /// + internal TaskLoggingContext(TargetLoggingContext targetLoggingContext, string projectFullPath, ProjectTargetInstanceChild task) + : base(targetLoggingContext) + { + _targetLoggingContext = targetLoggingContext; + _task = task; + + ProjectTaskInstance taskInstance = task as ProjectTaskInstance; + if (taskInstance != null) + { + _taskName = taskInstance.Name; + } + else + { + ProjectPropertyGroupTaskInstance propertyGroupInstance = task as ProjectPropertyGroupTaskInstance; + if (propertyGroupInstance != null) + { + _taskName = "PropertyGroup"; + } + else + { + ProjectItemGroupTaskInstance itemGroupInstance = task as ProjectItemGroupTaskInstance; + if (itemGroupInstance != null) + { + _taskName = "ItemGroup"; + } + else + { + _taskName = "Unknown"; + } + } + } + + this.BuildEventContext = LoggingService.LogTaskStarted2 + ( + targetLoggingContext.BuildEventContext, + _taskName, + projectFullPath, + task.Location.File + ); + this.IsValid = true; + } + + /// + /// Constructor used to support out-of-proc task host (proxy for in-proc logging service.) + /// + internal TaskLoggingContext(ILoggingService loggingService, BuildEventContext outOfProcContext) + : base(loggingService, outOfProcContext, true) + { + this.IsValid = true; + } + + /// + /// Retrieves the target logging context. + /// + internal TargetLoggingContext TargetLoggingContext + { + get + { + return _targetLoggingContext; + } + } + + /// + /// Retrieves the task node. + /// + internal ProjectTargetInstanceChild Task + { + get + { + return _task; + } + } + + /// + /// Retrieves the task node. + /// + internal string TaskName + { + get + { + return _taskName; + } + } + + /// + /// Log that a task has just completed + /// + internal void LogTaskBatchFinished(string projectFullPath, bool success) + { + ErrorUtilities.VerifyThrow(this.IsValid, "invalid"); + + LoggingService.LogTaskFinished + ( + BuildEventContext, + _taskName, + projectFullPath, + _task.Location.File, + success + ); + this.IsValid = false; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/BatchingEngine.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/BatchingEngine.cs new file mode 100644 index 00000000000..8b257d4ed72 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/BatchingEngine.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +using Microsoft.Build.Collections; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class is used by objects in the build engine that have the ability to execute themselves in batches, to partition the + /// items they consume into "buckets", based on the values of select item metadata. + /// + /// + /// What batching does + /// + /// Batching partitions the items consumed by the batchable object into buckets, where each bucket + /// contains a set of items that have the same value set on all item metadata consumed by the object. + /// Metadata consumed may be unqualified, for example %(m), or qualified by the item list to which it + /// refers, for example %(a.m). + /// + /// If metadata is qualified, for example %(a.m), then this is considered distinct to metadata with the + /// same name on a different item type. For example, %(a.m) is distinct to %(b.m), and items of type ‘b’ + /// are considered to always have a blank value for %(a.m). This means items of type ‘b’ will only be + /// placed in buckets where %(a.m) is blank. However %(a.m) is equivalent to %(m) on items of type ‘a’. + /// + /// There is an extra ambiguity rule: every items consumed by the object must have an explicit value for + /// every piece of unqualified metadata. For example, if @(a), %(m), and %(a.n) are consumed, every item + /// of type ‘a’ must have a value for the metadata ‘m’ but need not all necessarily have a value for the + /// metadata ‘n’. This rule eliminates ambiguity about whether items that do not define values for an + /// unqualified metadata should go in all buckets, or just into buckets with a blank value for + /// that metadata. + /// + /// For example + /// + /// + /// + /// m0 + /// + /// + /// m1 + /// + /// + /// n0 + /// + /// + /// n1 + /// + /// + /// + /// + /// + /// + /// + /// + /// Will produce 5 buckets: + /// + /// a={a1;a2.m0} b={.} + /// a={a3.m1} b={.} + /// a={.} b={b1.n0} + /// a={.} b={b2;b3.n1} + /// a={.} b={b4.} + /// + /// + internal static class BatchingEngine + { + #region Methods + + /// + /// Determines how many times the batchable object needs to be executed (each execution is termed a "batch"), and prepares + /// buckets of items to pass to the object in each batch. + /// + /// ArrayList containing ItemBucket objects, each one representing an execution batch. + internal static List PrepareBatchingBuckets + ( + List batchableObjectParameters, + Lookup lookup, + ElementLocation elementLocation + ) + { + return PrepareBatchingBuckets(batchableObjectParameters, lookup, null, elementLocation); + } + + /// + /// Determines how many times the batchable object needs to be executed (each execution is termed a "batch"), and prepares + /// buckets of items to pass to the object in each batch. + /// + /// + /// + /// + /// Any item type that can be considered an implicit input to this batchable object. + /// This is useful for items inside targets, where the item name is plainly an item type that's an "input" to the object. + /// ArrayList containing ItemBucket objects, each one representing an execution batch. + internal static List PrepareBatchingBuckets + ( + List batchableObjectParameters, + Lookup lookup, + string implicitBatchableItemType, + ElementLocation elementLocation + ) + { + if (batchableObjectParameters == null) + { + ErrorUtilities.ThrowInternalError("Need the parameters of the batchable object to determine if it can be batched."); + } + + if (lookup == null) + { + ErrorUtilities.ThrowInternalError("Need to specify the lookup."); + } + + ItemsAndMetadataPair pair = ExpressionShredder.GetReferencedItemNamesAndMetadata(batchableObjectParameters); + + // All the @(itemname) item list references in the tag, including transforms, etc. + HashSet consumedItemReferences = pair.Items; + + // All the %(itemname.metadataname) references in the tag (not counting those embedded + // inside item transforms), and note that the itemname portion is optional. + // The keys in the returned hash table are the qualified metadata names (e.g. "EmbeddedResource.Culture" + // or just "Culture"). The values are MetadataReference structs, which simply split out the item + // name (possibly null) and the actual metadata name. + Dictionary consumedMetadataReferences = pair.Metadata; + + List buckets = null; + if (consumedMetadataReferences != null && consumedMetadataReferences.Count > 0) + { + // Add any item types that we were explicitly told to assume. + if (implicitBatchableItemType != null) + { + consumedItemReferences = consumedItemReferences ?? new HashSet(MSBuildNameIgnoreCaseComparer.Default); + consumedItemReferences.Add(implicitBatchableItemType); + } + + // This method goes through all the item list references and figures out which ones + // will be participating in batching, and which ones won't. We get back a hashtable + // where the key is the item name that will be participating in batching. The values + // are all String.Empty (not used). This method may return additional item names + // that weren't represented in "consumedItemReferences"... this would happen if there + // were qualified metadata references in the consumedMetadataReferences table, such as + // %(EmbeddedResource.Culture). + Dictionary> itemListsToBeBatched = GetItemListsToBeBatched(consumedMetadataReferences, consumedItemReferences, lookup, elementLocation); + + // At this point, if there were any metadata references in the tag, but no item + // references to batch on, we've got a problem because we can't figure out which + // item lists the user wants us to batch. + if (itemListsToBeBatched.Count == 0) + { + foreach (string unqualifiedMetadataName in consumedMetadataReferences.Keys) + { + // Of course, since this throws an exception, there's no way we're ever going + // to really loop here... it's just that the foreach is the only way I can + // figure out how to get data out of the hashtable without knowing any of the + // keys! + ProjectErrorUtilities.VerifyThrowInvalidProject(false, + elementLocation, "CannotReferenceItemMetadataWithoutItemName", unqualifiedMetadataName); + } + } + else + { + // If the batchable object consumes item metadata as well as items to be batched, + // we need to partition the items consumed by the object. + buckets = BucketConsumedItems(lookup, itemListsToBeBatched, consumedMetadataReferences, elementLocation); + } + } + + // if the batchable object does not consume any item metadata or items, or if the item lists it consumes are all + // empty, then the object does not need to be batched + if ((buckets == null) || (buckets.Count == 0)) + { + // create a default bucket that references the project items and properties -- this way we always have a bucket + buckets = new List(1); + buckets.Add(new ItemBucket(null, null, lookup, buckets.Count)); + } + + return buckets; + } + + /// + /// Of all the item lists that are referenced in this batchable object, which ones should we + /// batch on, and which ones should we just pass in wholesale to every invocation of the + /// target/task? + /// + /// Rule #1. If the user has referenced any *qualified* item metadata such as %(EmbeddedResource.Culture), + /// then that item list "EmbeddedResource" will definitely get batched. + /// + /// Rule #2. For all the unqualified item metadata such as %(Culture), we make sure that + /// every single item in every single item list being passed into the task contains a value + /// for that metadata. If not, it's an error. If so, we batch all of those item lists. + /// + /// All other item lists will not be batched, and instead will be passed in wholesale to all buckets. + /// + /// Dictionary containing the item names that should be batched. If the items match unqualified metadata, + /// the entire list of items will be returned in the Value. Otherwise, the Value will be empty, indicating only the + /// qualified item set (in the Key) should be batched. + /// + private static Dictionary> GetItemListsToBeBatched + ( + Dictionary consumedMetadataReferences, // Key is [string] potentially qualified metadata name + // Value is [struct MetadataReference] + HashSet consumedItemReferenceNames, + Lookup lookup, + ElementLocation elementLocation + ) + { + // The keys in this hashtable are the names of the items that we will batch on. + // The values are always String.Empty (not used). + Dictionary> itemListsToBeBatched = new Dictionary>(MSBuildNameIgnoreCaseComparer.Default); + + // Loop through all the metadata references and find the ones that are qualified + // with an item name. + foreach (MetadataReference consumedMetadataReference in consumedMetadataReferences.Values) + { + if (consumedMetadataReference.ItemName != null) + { + // Rule #1. Qualified metadata reference. + // For metadata references that are qualified with an item name + // (e.g., %(EmbeddedResource.Culture) ), we add that item name to the list of + // consumed item names, even if the item name wasn't otherwise referenced via + // @(...) syntax, and even if every item in the list doesn't necessary contain + // a value for this metadata. This is the special power that you get by qualifying + // the metadata reference with an item name. + itemListsToBeBatched[consumedMetadataReference.ItemName] = null; + + // Also add this qualified item to the consumed item references list, because + // %(EmbeddedResource.Culture) effectively means that @(EmbeddedResource) is + // being consumed, even though we may not see literally "@(EmbeddedResource)" + // in the tag anywhere. Adding it to this list allows us (down below in this + // method) to check that every item in this list has a value for each + // unqualified metadata reference. + consumedItemReferenceNames = consumedItemReferenceNames ?? new HashSet(MSBuildNameIgnoreCaseComparer.Default); + consumedItemReferenceNames.Add(consumedMetadataReference.ItemName); + } + } + + // Loop through all the metadata references and find the ones that are unqualified. + foreach (MetadataReference consumedMetadataReference in consumedMetadataReferences.Values) + { + if (consumedMetadataReference.ItemName == null) + { + // Rule #2. Unqualified metadata reference. + // For metadata references that are unqualified, every single consumed item + // must contain a value for that metadata. If any item doesn't, it's an error + // to use unqualified metadata. + if (consumedItemReferenceNames != null) + { + foreach (string consumedItemName in consumedItemReferenceNames) + { + // Loop through all the items in the item list. + ICollection items = lookup.GetItems(consumedItemName); + + if (items != null) + { + // Loop through all the items in the BuildItemGroup. + foreach (ProjectItemInstance item in items) + { + ProjectErrorUtilities.VerifyThrowInvalidProject( + item.HasMetadata(consumedMetadataReference.MetadataName), + elementLocation, "ItemDoesNotContainValueForUnqualifiedMetadata", + item.EvaluatedInclude, consumedItemName, consumedMetadataReference.MetadataName); + } + } + + // This item list passes the test of having every single item containing + // a value for this metadata. Therefore, add this item list to the batching list. + // Also, to save doing lookup.GetItems again, put the items in the table as the value. + itemListsToBeBatched[consumedItemName] = items; + } + } + } + } + + return itemListsToBeBatched; + } + + /// + /// Partitions the items consumed by the batchable object into buckets, where each bucket contains a set of items that + /// have the same value set on all item metadata consumed by the object. + /// + /// + /// PERF NOTE: Given n items and m batching metadata that produce l buckets, it is usually the case that n > l > m, + /// because a batchable object typically uses one or two item metadata to control batching, and only has a handful of + /// buckets. The number of buckets is typically only large if a batchable object is using single-item batching + /// (where l == n). Any algorithm devised for bucketing therefore, should try to minimize n and l in its complexity + /// equation. The algorithm below has a complexity of O(n*lg(l)*m/2) in its comparisons, and is effectively O(n) when + /// l is small, and O(n*lg(n)) in the worst case as l -> n. However, note that the comparison complexity is not the + /// same as the operational complexity for this algorithm. The operational complexity of this algorithm is actually + /// O(n*m + n*lg(l)*m/2 + n*l/2 + n + l), which is effectively O(n^2) in the worst case. The additional complexity comes + /// from the array and metadata operations that are performed. However, those operations are extremely cheap compared + /// to the comparison operations, which dominate the time spent in this method. + /// + /// ArrayList containing ItemBucket objects (can be empty), each one representing an execution batch. + private static List BucketConsumedItems + ( + Lookup lookup, + Dictionary> itemListsToBeBatched, + Dictionary consumedMetadataReferences, + ElementLocation elementLocation + ) + { + ErrorUtilities.VerifyThrow(itemListsToBeBatched.Count > 0, "Need item types consumed by the batchable object."); + ErrorUtilities.VerifyThrow(consumedMetadataReferences.Count > 0, "Need item metadata consumed by the batchable object."); + + List buckets = new List(); + + // Get and iterate through the list of item names that we're supposed to batch on. + foreach (KeyValuePair> entry in itemListsToBeBatched) + { + string itemName = (string)entry.Key; + + // Use the previously-fetched items, if possible + ICollection items = entry.Value; + if (items == null) + { + items = lookup.GetItems(itemName); + } + + if (items != null) + { + foreach (ProjectItemInstance item in items) + { + // Get this item's values for all the metadata consumed by the batchable object. + Dictionary itemMetadataValues = GetItemMetadataValues(item, consumedMetadataReferences, elementLocation); + + // put the metadata into a dummy bucket we can use for searching + ItemBucket dummyBucket = ItemBucket.GetDummyBucketForComparisons(itemMetadataValues); + + // look through all previously created buckets to find a bucket whose items have the same values as + // this item for all metadata consumed by the batchable object + int matchingBucketIndex = buckets.BinarySearch(dummyBucket); + + ItemBucket matchingBucket = (matchingBucketIndex >= 0) + ? (ItemBucket)buckets[matchingBucketIndex] + : null; + + // If we didn't find a bucket that matches this item, create a new one, adding + // this item to the bucket. + if (null == matchingBucket) + { + matchingBucket = new ItemBucket(itemListsToBeBatched.Keys, itemMetadataValues, lookup, buckets.Count); + + // make sure to put the new bucket into the appropriate location + // in the sorted list as indicated by the binary search + // NOTE: observe the ~ operator (bitwise complement) in front of + // the index -- see MSDN for more information on the return value + // from the ArrayList.BinarySearch() method + buckets.Insert(~matchingBucketIndex, matchingBucket); + } + + // We already have a bucket for this type of item, so add this item to + // the bucket. + matchingBucket.AddItem(item); + } + } + } + + // Put the buckets back in the order in which they were discovered, so that the first + // item declared in the project file ends up in the first batch passed into the target/task. + List orderedBuckets = new List(buckets.Count); + for (int i = 0; i < buckets.Count; ++i) + { + orderedBuckets.Add(null); + } + + foreach (ItemBucket bucket in buckets) + { + orderedBuckets[bucket.BucketSequenceNumber] = bucket; + } + return orderedBuckets; + } + + /// + /// Gets the values of the specified metadata for the given item. + /// The keys in the dictionary returned may be qualified and/or unqualified, exactly + /// as they are found in the metadata reference. + /// For example if %(x) is found, the key is "x", if %(z.x) is found, the key is "z.x". + /// This dictionary in each bucket is used by Expander to expand exactly the same metadata references, so + /// %(x) is expanded using the key "x", and %(z.x) is expanded using the key "z.x". + /// + /// the metadata values + private static Dictionary GetItemMetadataValues + ( + ProjectItemInstance item, + Dictionary consumedMetadataReferences, + ElementLocation elementLocation + ) + { + Dictionary itemMetadataValues = new Dictionary(consumedMetadataReferences.Count, MSBuildNameIgnoreCaseComparer.Default); + + foreach (KeyValuePair consumedMetadataReference in consumedMetadataReferences) + { + string metadataQualifiedName = consumedMetadataReference.Key; + string metadataItemName = consumedMetadataReference.Value.ItemName; + string metadataName = consumedMetadataReference.Value.MetadataName; + + if ( + (metadataItemName != null) && + (0 != String.Compare(item.ItemType, metadataItemName, StringComparison.OrdinalIgnoreCase)) + ) + { + itemMetadataValues[metadataQualifiedName] = String.Empty; + } + else + { + try + { + // This returns String.Empty for both metadata that is undefined and metadata that has + // an empty value; they are treated the same. + itemMetadataValues[metadataQualifiedName] = ((IItem)item).GetMetadataValueEscaped(metadataName); + } + catch (InvalidOperationException e) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, + "CannotEvaluateItemMetadata", metadataName, e.Message); + } + } + } + + return itemMetadataValues; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/FullTracking.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/FullTracking.cs new file mode 100644 index 00000000000..214fcde1bb6 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/FullTracking.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Manages full tracking activation and suspension. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Manages full tracking activation and suspension. + /// + internal class FullTracking : IDisposable + { + /// + /// The default name of the MSBuild property to read for the relative path to the full tracking .tlog files. + /// If this property isn't set in the project, full tracking is turned off. + /// + private const string FullTrackingDirectoryPropertyName = "MSBuildFullTrackingPath"; + + /// + /// The full path to where full tracking .tlog files should be written. + /// + private string _tlogDirectory; + + /// + /// A value indicating whether this instance is tracking a full tracking suspension + /// (as opposed to activation). + /// + private TrackingMode _trackingMode; + + /// + /// The name of the task as given to FileTracker.dll. + /// + private string _taskName; + + /// + /// Initializes a new instance of the class. + /// + private FullTracking() + { + _trackingMode = TrackingMode.None; + } + + /// + /// The state of the object regarding whether it is actively tracking or suspending tracking. + /// + private enum TrackingMode + { + /// + /// No tracking or suspension operation is in effect as a result of this instance. + /// + None, + + /// + /// This instance has invoked full tracking. + /// + Active, + + /// + /// This instance has suspended full tracking. + /// + Suspended, + } + + /// + /// Disposes the FullTracking object, causing full tracking to end, or resume, + /// depending on how this object was created. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Starts full tracking. + /// + /// taskLoggingContext.TargetLoggingContext.Target.Name + /// taskNode.Name + /// buildRequestEntry.ProjectRootDirectory + /// buildRequestEntry.RequestConfiguration.Project.PropertiesToBuildWith + /// + /// An object that will stop full tracking when disposed. + /// + internal static IDisposable Track(string targetName, string taskName, string projectRootDirectory, PropertyDictionary projectProperties) + { + FullTracking tracking = new FullTracking(); + + ProjectPropertyInstance tlogRelativeDirectoryProperty = projectProperties[FullTrackingDirectoryPropertyName]; + string tlogRelativeDirectoryValue = null; + if (tlogRelativeDirectoryProperty != null) + { + tlogRelativeDirectoryValue = tlogRelativeDirectoryProperty.EvaluatedValue; + } + + if (!String.IsNullOrEmpty(tlogRelativeDirectoryValue)) + { + tracking._taskName = GenerateUniqueTaskName(targetName, taskName); + tracking._tlogDirectory = Path.Combine(projectRootDirectory, tlogRelativeDirectoryValue); + InprocTrackingNativeMethods.StartTrackingContext(tracking._tlogDirectory, tracking._taskName); + tracking._trackingMode = TrackingMode.Active; + } + + return tracking; + } + + /// + /// Suspends full tracking. + /// + /// An object that will resume full tracking when disposed. + internal static IDisposable Suspend() + { + FullTracking tracking = new FullTracking(); + + if (tracking._trackingMode == TrackingMode.Active) + { + InprocTrackingNativeMethods.SuspendTracking(); + tracking._trackingMode = TrackingMode.Suspended; + } + + return tracking; + } + + /// + /// Disposes the FullTracking object, causing full tracking to end, or resume, + /// depending on how this object was created. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + switch (_trackingMode) + { + case TrackingMode.Active: + // Stop tracking + InprocTrackingNativeMethods.WriteContextTLogs(_tlogDirectory, _taskName); + InprocTrackingNativeMethods.EndTrackingContext(); + break; + case TrackingMode.Suspended: + // Stop suspending tracking + InprocTrackingNativeMethods.ResumeTracking(); + break; + default: + // nothing to do here if we weren't actively tracking or suspended. + break; + } + } + } + + /// + /// Gets the task name to pass to Tracker. + /// + private static string GenerateUniqueTaskName(string targetName, string taskName) + { + return "__FT__" + targetName + "-" + taskName; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilder.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilder.cs new file mode 100644 index 00000000000..c9e876e870e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilder.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the build request builder component. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Delegate for event raised when a new build request needs to be issued. + /// + /// The entry issuing the request. + /// The request to be issued. + internal delegate void NewBuildRequestsDelegate(BuildRequestEntry sourceEntry, FullyQualifiedBuildRequest[] requests); + + /// + /// Delegate for event raised when a build request has completed. + /// + /// The entry which completed. + internal delegate void BuildRequestCompletedDelegate(BuildRequestEntry completedEntry); + + /// + /// Delegate for event raised when a build request is blocked on another request which is in progress. + /// + /// The build request entry which is being blocked. + /// The request on which we are blocked. + /// The target on which we are blocked. + internal delegate void BuildRequestBlockedDelegate(BuildRequestEntry sourceEntry, int blockingGlobalRequestId, string blockingTarget); + + /// + /// Represents a class which is capable of building BuildRequestEntries. + /// + internal interface IRequestBuilder + { + /// + /// Raised when a new build request is to be issued. + /// + event NewBuildRequestsDelegate OnNewBuildRequests; + + /// + /// Raised when the build request is complete. + /// + event BuildRequestCompletedDelegate OnBuildRequestCompleted; + + /// + /// Raised when a build request is blocked on another one in progress. + /// + event BuildRequestBlockedDelegate OnBuildRequestBlocked; + + /// + /// Builds the request contained in the specified entry. + /// + /// The logging context for the node. + /// The entry to be built. + void BuildRequest(NodeLoggingContext nodeLoggingContext, BuildRequestEntry entry); + + /// + /// Continues building a request which was previously waiting for results. + /// + void ContinueRequest(); + + /// + /// Cancels an existing request. + /// + void CancelRequest(); + + /// + /// Starts to cancel an existing request. + /// + /// + /// This method should return immediately after signal the cancel event. + /// "CancelRequest()" is equal to call "BeginCancel()" and "WaitForCancelCompletion()". + /// We break "CancelRequest()" to 2 phases, so that we could signal cancel event + /// to a bunch of requests without waiting, in order to optimize the "cancel build" scenario. + /// + void BeginCancel(); + + /// + /// Waits for the cancellation until it's completed, and cleans up the internal states. + /// + void WaitForCancelCompletion(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilderCallback.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilderCallback.cs new file mode 100644 index 00000000000..2145b800740 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IRequestBuilderCallback.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface used by the TargetBuilder to communicate with the RequestBuilder. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using System.Threading.Tasks; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This interface is passed to objects which might need to call back into the request builder, such as the Target and Task builders + /// + internal interface IRequestBuilderCallback + { + /// + /// This method instructs the request builder to build the specified projects using the specified parameters. This is + /// what is ultimately used by something like an MSBuild task which needs to invoke a project-to-project reference. IBuildEngine + /// and IBuildEngine2 have BuildProjectFile methods which boil down to an invocation of this method as well. + /// + /// An array of projects to be built. + /// The property groups to use for each project. Must be the same number as there are project files. + /// The tools version to use for each project. Must be the same number as there are project files. + /// The targets to be built. Each project will be built with the same targets. + /// True to wait for the results + /// An Task representing the work which will be done. + Task BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults); + + /// + /// This method instructs the request builder that the target builder is blocked on a target which is already in progress on the + /// configuration due to another request. + /// + /// The request on which we are blocked. + Task BlockOnTargetInProgress(int blockingRequestId, string blockingTarget); + + /// + /// Instructs the RequestBuilder that it may yield its control of the node. + /// + void Yield(); + + /// + /// Instructs the RequestBuilder to suspend until the node is reacquired. + /// + void Reacquire(); + + /// + /// Instructs the RequestBuilder that next Build request from a task should post its request + /// and immeditely return so that the thread may be freed up. May not be nested. + /// + void EnterMSBuildCallbackState(); + + /// + /// Exits the previous MSBuild callback state. + /// + void ExitMSBuildCallbackState(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilder.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilder.cs new file mode 100644 index 00000000000..ebecdb4358d --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilder.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the target builder. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd.Logging; +using BuildResult = Microsoft.Build.Execution.BuildResult; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Represents an object which can build targets for a project. + /// + internal interface ITargetBuilder + { + /// + /// Builds the specified targets. + /// + /// The logging context for the project. + /// The BuildRequestEntry for which we are building targets. + /// The callback to be used to handle new project build requests. + /// The targets to build. + /// The Lookup containing all current items and properties for this target. + /// The cancellation token used to cancel processing of targets. + /// A Task representing the work to be done. + Task BuildTargets(ProjectLoggingContext projectLoggingContext, BuildRequestEntry entry, IRequestBuilderCallback callback, string[] targets, Lookup baseLookup, CancellationToken cancellationToken); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilderCallback.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilderCallback.cs new file mode 100644 index 00000000000..e355806e485 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITargetBuilderCallback.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the target builder callback. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using System.Threading.Tasks; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Interface implemted by the Target Builder which allows tasks to invoke targets and build projects. + /// + internal interface ITargetBuilderCallback : IRequestBuilderCallback + { + /// + /// Invokes the specified targets using Dev9 behavior. + /// + /// The targets to build. + /// True to continue building the remaining targets if one fails. + /// The results for each target. + /// + /// The target is run using the data context of the Project, rather than the data context + /// of the current target. This has the following effects: + /// 1. Data visible to the CALLING target at the time it was first invoked is the only + /// data which the CALLED target can see. No changes made between the time the CALLING + /// target starts and the CALLED target starts are visible to the CALLED target. + /// 2. Items and Properties modified by the CALLED target are not visible to the CALLING + /// target, even after the CALLED target returns. However, any changes made to + /// items and properties by the CALLING target will override any changes made by the + /// CALLED target. + /// + Task LegacyCallTarget(string[] targets, bool continueOnError, ElementLocation referenceLocation); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITaskBuilder.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITaskBuilder.cs new file mode 100644 index 00000000000..1d12874712a --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ITaskBuilder.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface implemented by objects which can build tasks. +//----------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Execution; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Flags indicating the mode in which the task builder should operate. + /// + [Flags] + internal enum TaskExecutionMode + { + /// + /// This entry is necessary to use the enum with binary math. It is never used outside + /// intermediate calculations. + /// + Invalid = 0, + + /// + /// In this mode, the task engine actually runs the task and retrieves its outputs. + /// + ExecuteTaskAndGatherOutputs = 1, + + /// + /// In this mode, the task engine only infers the task's outputs from its <Output> tags. + /// + InferOutputsOnly = 2 + } + + /// + /// Interface representing an object which can build tasks. + /// + internal interface ITaskBuilder + { + /// + /// Executes the specified task, batching it is necessary. + /// + /// The logging context for the target + /// The build request entry + /// The callback to use for handling new build requests. + /// The node for the task + /// The mode to use when executing the task. + /// The lookup used when we are inferring outputs from inputs. + /// The lookup used when executing the task to get its outputs. + /// The cancellation token used to cancel processing of the task. + /// A Task representing the work to be done. + Task ExecuteTask(TargetLoggingContext targetLoggingContext, BuildRequestEntry requestEntry, ITargetBuilderCallback targetBuilderCallback, ProjectTargetInstanceChild task, TaskExecutionMode mode, Lookup lookupForInference, Lookup lookupForExecution, CancellationToken cancellationToken); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTask.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTask.cs new file mode 100644 index 00000000000..97c271f8e52 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTask.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Base class for intrinsic tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Xml; +using System.Reflection; +using System.Globalization; +using System.Collections; +using System.Diagnostics; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.BackEnd.Logging; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A class that evaluates an ItemGroup or PropertyGroup that is within a target. + /// + internal abstract class IntrinsicTask + { + /// + /// Initializes this base class. + /// + /// The logging context + /// The project instance + /// Flag to determine whether or not to log task inputs. + protected IntrinsicTask(TargetLoggingContext loggingContext, ProjectInstance projectInstance, bool logTaskInputs) + { + this.LoggingContext = loggingContext; + this.Project = projectInstance; + this.LogTaskInputs = logTaskInputs; + } + + /// + /// Gets or sets the logging context. + /// + internal TargetLoggingContext LoggingContext + { + get; + private set; + } + + /// + /// Gets or sets the project instance. + /// + internal ProjectInstance Project + { + get; + private set; + } + + /// + /// Flag to determine whether or not to log task inputs. + /// + protected bool LogTaskInputs + { + get; + private set; + } + + /// + /// Factory for intrinsic tasks. + /// + /// The task instance object. + /// The logging context. + /// The project instance. + /// An instantiated intrinsic task. + internal static IntrinsicTask InstantiateTask(ProjectTargetInstanceChild taskInstance, TargetLoggingContext loggingContext, ProjectInstance projectInstance, bool logTaskInputs) + { + if (taskInstance is ProjectPropertyGroupTaskInstance) + { + return new PropertyGroupIntrinsicTask(taskInstance as ProjectPropertyGroupTaskInstance, loggingContext, projectInstance, logTaskInputs); + } + else if (taskInstance is ProjectItemGroupTaskInstance) + { + return new ItemGroupIntrinsicTask(taskInstance as ProjectItemGroupTaskInstance, loggingContext, projectInstance, logTaskInputs); + } + else + { + ErrorUtilities.ThrowInternalError("Unhandled intrinsic task type {0}", taskInstance.GetType().BaseType); + return null; + } + } + + /// + /// Called to execute a task within a target. This method instantiates the task, sets its parameters, and executes it. + /// + /// The lookup used for expansion and to receive created items and properties. + internal abstract void ExecuteTask(Lookup lookup); + + /// + /// If value is not an empty string, adds it to list. + /// + /// The list of strings to which this should be added, if it is not empty. + /// The string to add. + protected static void AddIfNotEmptyString(List list, string value) + { + if (!String.IsNullOrEmpty(value)) + { + list.Add(value); + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs new file mode 100644 index 00000000000..2c5f76524bc --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +// This CallTarget intrinsic task replaces the one on Microsoft.Build.Tasks, which is now deprecated. + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class implements the "CallTarget" task, which invokes other targets within the same + /// project file. Marked RunInMTA because we do not want this task to ever be invoked explicitly + /// on the STA if the RequestBuilder is running on another thread, as this will cause thread + /// id validation checks to fail. + /// + [RunInMTA] + internal class CallTarget : ITask + { + /// + /// The task logging helper + /// + private TaskLoggingHelper _logHelper = null; + + /// + /// Default constructor. + /// + public CallTarget() + { + } + + #region Properties + + // A list of targets to build. This is a required parameter. If you want to build the + // default targets, use the task and pass in Projects=$(MSBuildProjectFile). + private string[] _targets = null; + + // outputs of all built targets + private ArrayList _targetOutputs = new ArrayList(); + + // When this is true, instead of calling the engine once to build all the targets, + // we would call the engine once per target. The benefit of this is that + // if one target fails, you can still continue with the remaining targets. + private bool _runEachTargetSeparately = false; + + // If true the cache will be checked for the result and the result will be stored if the operation + // is run + private bool _useResultsCache = false; + + /// + /// The targets to build. + /// + /// Array of target names. + public string[] Targets + { + get + { + return _targets; + } + + set + { + _targets = value; + } + } + + /// + /// Outputs of the targets built in each project. + /// + /// Array of output items. + [Output] + public ITaskItem[] TargetOutputs + { + get + { + return (ITaskItem[])_targetOutputs.ToArray(typeof(ITaskItem)); + } + } + + /// + /// When this is true, instead of calling the engine once to build all the targets (for each project), + /// we would call the engine once per target (for each project). The benefit of this is that + /// if one target fails, you can still continue with the remaining targets. + /// + public bool RunEachTargetSeparately + { + get + { + return _runEachTargetSeparately; + } + + set + { + _runEachTargetSeparately = value; + } + } + + /// + /// If true the cached result will be returned if present and a if MSBuild + /// task is run its result will be cached in a scope (ProjectFileName, GlobalProperties)[TargetNames] + /// as a list of build items + /// + public bool UseResultsCache + { + get + { + return _useResultsCache; + } + set + { + _useResultsCache = value; + } + } + + #endregion + + #region ITask Members + + public IBuildEngine BuildEngine + { + get; + set; + } + + public IBuildEngine2 BuildEngine2 + { + get { return (IBuildEngine2)BuildEngine; } + } + + public IBuildEngine3 BuildEngine3 + { + get { return (IBuildEngine3)BuildEngine; } + } + + /// + /// The host object, from ITask + /// + public ITaskHost HostObject + { + get; + set; + } + + public TaskLoggingHelper Log + { + get + { + if (_logHelper == null) + { + _logHelper = new TaskLoggingHelper(this); + } + + return _logHelper; + } + } + + public bool Execute() + { + throw new NotImplementedException(); + } + + /// + /// Instructs the MSBuild engine to build one or more targets in the current project. + /// + /// true if all targets built successfully; false if any target fails + public Task ExecuteInternal() + { + // Make sure the list of targets was passed in. + if ((Targets == null) || (Targets.Length == 0)) + { + return Task.FromResult(true); + } + + // This is a list of string[]. That is, each element in the list is a string[]. Each + // string[] represents a set of target names to build. Depending on the value + // of the RunEachTargetSeparately parameter, we each just call the engine to run all + // the targets together, or we call the engine separately for each target. + ArrayList targetLists = MSBuild.CreateTargetLists(this.Targets, this.RunEachTargetSeparately); + + ITaskItem[] singleProject = new ITaskItem[1]; + singleProject[0] = null; + // 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 + this.BuildEngine3, + this.Log, + _targetOutputs, + this.UseResultsCache, + false, + null // toolsVersion = null + ); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs new file mode 100644 index 00000000000..4cb2b29328b --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/IntrinsicTaskFactory.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The task factory for intrinsic tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The factory + /// + internal class IntrinsicTaskFactory : ITaskFactory + { + /// + /// Constructor + /// + public IntrinsicTaskFactory(Type intrinsicType) + { + this.TaskType = intrinsicType; + } + + /// + /// Returns the factory name + /// + public string FactoryName + { + get { return "Intrinsic Task Factory"; } + } + + /// + /// Returns the task type. + /// + public Type TaskType + { + get; + private set; + } + + /// + /// Initialize the factory. + /// + public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) + { + if (!String.Equals(taskName, TaskType.Name, StringComparison.OrdinalIgnoreCase)) + { + ErrorUtilities.ThrowInternalError("Unexpected task name {0}. Expected {1}", taskName, TaskType.Name); + } + + return true; + } + + /// + /// Gets all of the parameters on the task. + /// + public TaskPropertyInfo[] GetTaskParameters() + { + PropertyInfo[] infos = TaskType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var propertyInfos = new TaskPropertyInfo[infos.Length]; + for (int i = 0; i < infos.Length; i++) + { + propertyInfos[i] = new ReflectableTaskPropertyInfo(infos[i]); + } + + return propertyInfos; + } + + /// + /// Creates an instance of the task. + /// + public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) + { + if (TaskType == typeof(MSBuild)) + { + return new MSBuild(); + } + else if (TaskType == typeof(CallTarget)) + { + return new CallTarget(); + } + + ErrorUtilities.ThrowInternalError("Unexpected intrinsic task type {0}", TaskType); + return null; + } + + /// + /// Cleanup for the task. + /// + public void CleanupTask(ITask task) + { + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs new file mode 100644 index 00000000000..34658f04ba4 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs @@ -0,0 +1,631 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of the ItemGroup intrinsic task. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Microsoft.Build.Collections; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory; +using EngineFileUtilities = Microsoft.Build.Internal.EngineFileUtilities; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Implementation of the ItemGroup intrinsic task + /// + internal class ItemGroupIntrinsicTask : IntrinsicTask + { + /// + /// The task instance data + /// + private ProjectItemGroupTaskInstance _taskInstance; + + /// + /// Instantiates an ItemGroup task + /// + /// The original task instance data + /// The logging context + /// The project instance + /// Flag to determine whether or not to log task inputs. + public ItemGroupIntrinsicTask(ProjectItemGroupTaskInstance taskInstance, TargetLoggingContext loggingContext, ProjectInstance projectInstance, bool logTaskInputs) + : base(loggingContext, projectInstance, logTaskInputs) + { + _taskInstance = taskInstance; + } + + /// + /// Execute an ItemGroup element, including each child item expression + /// + /// The lookup used for evaluation and as a destination for these items. + internal override void ExecuteTask(Lookup lookup) + { + foreach (ProjectItemGroupTaskItemInstance child in _taskInstance.Items) + { + List buckets = null; + + try + { + List parameterValues = new List(); + GetBatchableValuesFromBuildItemGroupChild(parameterValues, child); + buckets = BatchingEngine.PrepareBatchingBuckets(parameterValues, lookup, child.ItemType, _taskInstance.Location); + + // "Execute" each bucket + foreach (ItemBucket bucket in buckets) + { + bool condition = ConditionEvaluator.EvaluateCondition + ( + child.Condition, + ParserOptions.AllowAll, + bucket.Expander, + ExpanderOptions.ExpandAll, + Project.Directory, + child.ConditionLocation, + LoggingContext.LoggingService, + LoggingContext.BuildEventContext + ); + + if (condition) + { + HashSet keepMetadata = null; + HashSet removeMetadata = null; + if (!String.IsNullOrEmpty(child.KeepMetadata)) + { + var keepMetadataEvaluated = bucket.Expander.ExpandIntoStringListLeaveEscaped(child.KeepMetadata, ExpanderOptions.ExpandAll, child.KeepMetadataLocation); + if (keepMetadataEvaluated.Count > 0) + { + keepMetadata = new HashSet(keepMetadataEvaluated); + } + } + + if (!String.IsNullOrEmpty(child.RemoveMetadata)) + { + var removeMetadataEvaluated = bucket.Expander.ExpandIntoStringListLeaveEscaped(child.RemoveMetadata, ExpanderOptions.ExpandAll, child.RemoveMetadataLocation); + if (removeMetadataEvaluated.Count > 0) + { + removeMetadata = new HashSet(removeMetadataEvaluated); + } + } + + if ((child.Include.Length != 0) || + (child.Exclude.Length != 0)) + { + // It's an item -- we're "adding" items to the world + ExecuteAdd(child, bucket, keepMetadata, removeMetadata); + } + else if (child.Remove.Length != 0) + { + // It's a remove -- we're "removing" items from the world + ExecuteRemove(child, bucket); + } + else + { + // It's a modify -- changing existing items + ExecuteModify(child, bucket, keepMetadata, removeMetadata); + } + } + } + } + finally + { + if (buckets != null) + { + // Propagate the item changes to the bucket above + foreach (ItemBucket bucket in buckets) + { + bucket.LeaveScope(); + } + } + } + } + } + + /// + /// Add items to the world. This is the in-target equivalent of an item include expression outside of a target. + /// + /// The item specification to evaluate and add. + /// The batching bucket. + private void ExecuteAdd(ProjectItemGroupTaskItemInstance child, ItemBucket bucket, ISet keepMetadata, ISet removeMetadata) + { + // First, collect up the appropriate metadata collections. We need the one from the item definition, if any, and + // the one we are using for this batching bucket. + ProjectItemDefinitionInstance itemDefinition = null; + Project.ItemDefinitions.TryGetValue(child.ItemType, out itemDefinition); + + // The NestedMetadataTable will handle the aggregation of the different metadata collections + NestedMetadataTable metadataTable = new NestedMetadataTable(child.ItemType, bucket.Expander.Metadata, itemDefinition); + IMetadataTable originalMetadataTable = bucket.Expander.Metadata; + + bucket.Expander.Metadata = metadataTable; + + // Second, expand the item include and exclude, and filter existing metadata as appropriate. + IList itemsToAdd = ExpandItemIntoItems(child, bucket.Expander, keepMetadata, removeMetadata); + + // Third, expand the metadata. + foreach (ProjectItemGroupTaskMetadataInstance metadataInstance in child.Metadata) + { + bool condition = ConditionEvaluator.EvaluateCondition + ( + metadataInstance.Condition, + ParserOptions.AllowAll, + bucket.Expander, + ExpanderOptions.ExpandAll, + Project.Directory, + metadataInstance.Location, + LoggingContext.LoggingService, + LoggingContext.BuildEventContext + ); + + if (condition) + { + string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, ExpanderOptions.ExpandAll, metadataInstance.Location); + + // This both stores the metadata so we can add it to all the items we just created later, and + // exposes this metadata to further metadata evaluations in subsequent loop iterations. + metadataTable.SetValue(metadataInstance.Name, evaluatedValue); + } + } + + // Finally, copy the added metadata onto the new items. The set call is additive. + ProjectItemInstance.SetMetadata(metadataTable.AddedMetadata, itemsToAdd); // Add in one operation for potential copy-on-write + + // Restore the original metadata table. + bucket.Expander.Metadata = originalMetadataTable; + + // Determine if we should NOT add duplicate entries + bool keepDuplicates = ConditionEvaluator.EvaluateCondition + ( + child.KeepDuplicates, + ParserOptions.AllowAll, + bucket.Expander, + ExpanderOptions.ExpandAll, + Project.Directory, + child.KeepDuplicatesLocation, + LoggingContext.LoggingService, + LoggingContext.BuildEventContext + ); + + if (LogTaskInputs && !LoggingContext.LoggingService.OnlyLogCriticalEvents && itemsToAdd != null && itemsToAdd.Count > 0) + { + var itemGroupText = ItemGroupLoggingHelper.GetParameterText(ResourceUtilities.GetResourceString("ItemGroupIncludeLogMessagePrefix"), child.ItemType, itemsToAdd.ToArray()); + LoggingContext.LogCommentFromText(MessageImportance.Low, itemGroupText); + } + + // Now add the items we created to the lookup. + bucket.Lookup.AddNewItemsOfItemType(child.ItemType, itemsToAdd, !keepDuplicates); // Add in one operation for potential copy-on-write + } + + /// + /// Remove items from the world. Removes to items that are part of the project manifest are backed up, so + /// they can be reverted when the project is reset after the end of the build. + /// + /// The item specification to evaluate and remove. + /// The batching bucket. + private void ExecuteRemove(ProjectItemGroupTaskItemInstance child, ItemBucket bucket) + { + ICollection group = bucket.Lookup.GetItems(child.ItemType); + if (group == null) + { + // No items of this type to remove + return; + } + + List itemsToRemove = FindItemsMatchingSpecification(group, child.Remove, child.RemoveLocation, bucket.Expander); + + if (itemsToRemove != null) + { + if (LogTaskInputs && !LoggingContext.LoggingService.OnlyLogCriticalEvents && itemsToRemove.Count > 0) + { + var itemGroupText = ItemGroupLoggingHelper.GetParameterText(ResourceUtilities.GetResourceString("ItemGroupRemoveLogMessage"), child.ItemType, itemsToRemove.ToArray()); + LoggingContext.LogCommentFromText(MessageImportance.Low, itemGroupText); + } + + bucket.Lookup.RemoveItems(itemsToRemove); + } + } + + /// + /// Modifies items in the world - specifically, changes their metadata. Changes to items that are part of the project manifest are backed up, so + /// they can be reverted when the project is reset after the end of the build. + /// + /// The item specification to evaluate and modify. + /// The batching bucket. + private void ExecuteModify(ProjectItemGroupTaskItemInstance child, ItemBucket bucket, ISet keepMetadata, ISet removeMetadata) + { + ICollection group = bucket.Lookup.GetItems(child.ItemType); + if (group == null || group.Count == 0) + { + // No items of this type to modify + return; + } + + // Figure out what metadata names and values we need to set + var metadataToSet = new Lookup.MetadataModifications(keepMetadata != null); + + // Filter the metadata as appropriate + if (keepMetadata != null) + { + foreach (var metadataName in keepMetadata) + { + metadataToSet[metadataName] = Lookup.MetadataModification.CreateFromNoChange(); + } + } + else if (removeMetadata != null) + { + foreach (var metadataName in removeMetadata) + { + metadataToSet[metadataName] = Lookup.MetadataModification.CreateFromRemove(); + } + } + + foreach (ProjectItemGroupTaskMetadataInstance metadataInstance in child.Metadata) + { + bool condition = ConditionEvaluator.EvaluateCondition + ( + metadataInstance.Condition, + ParserOptions.AllowAll, + bucket.Expander, + ExpanderOptions.ExpandAll, + Project.Directory, + metadataInstance.ConditionLocation, + LoggingContext.LoggingService, + LoggingContext.BuildEventContext + ); + + if (condition) + { + string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, ExpanderOptions.ExpandAll, metadataInstance.Location); + metadataToSet[metadataInstance.Name] = Lookup.MetadataModification.CreateFromNewValue(evaluatedValue); + } + } + + // Now apply the changes. This must be done after filtering, since explicitly set metadata overrides filters. + bucket.Lookup.ModifyItems(child.ItemType, group, metadataToSet); + } + + /// + /// Adds batchable parameters from an item element into the list. If the item element was a task, these + /// would be its raw parameter values. + /// + /// The list of batchable values + /// The item from which to find batchable values + private void GetBatchableValuesFromBuildItemGroupChild(List parameterValues, ProjectItemGroupTaskItemInstance child) + { + AddIfNotEmptyString(parameterValues, child.Include); + AddIfNotEmptyString(parameterValues, child.Exclude); + AddIfNotEmptyString(parameterValues, child.Remove); + AddIfNotEmptyString(parameterValues, child.Condition); + + foreach (ProjectItemGroupTaskMetadataInstance metadataElement in child.Metadata) + { + AddIfNotEmptyString(parameterValues, metadataElement.Value); + AddIfNotEmptyString(parameterValues, metadataElement.Condition); + } + } + + /// + /// Takes an item specification, evaluates it and expands it into a list of items + /// + /// The original item data + /// The expander to use. + /// + /// This code is very close to that which exists in the Evaluator.EvaluateItemXml method. However, because + /// it invokes type constructors, and those constructors take arguments of fundamentally different types, it has not + /// been refactored. + /// + /// A list of items. + private IList ExpandItemIntoItems + ( + ProjectItemGroupTaskItemInstance originalItem, + Expander expander, + ISet keepMetadata, + ISet removeMetadata + ) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(!(keepMetadata != null && removeMetadata != null), originalItem.KeepMetadataLocation, "KeepAndRemoveMetadataMutuallyExclusive"); + IList items = new List(); + + // UNDONE: (Refactor) This code also exists in largely the same form in Evaluator.CreateItemsFromInclude. + // STEP 1: Expand properties and metadata in Include + string evaluatedInclude = expander.ExpandIntoStringLeaveEscaped(originalItem.Include, ExpanderOptions.ExpandPropertiesAndMetadata, originalItem.IncludeLocation); + + // STEP 2: Split Include on any semicolons, and take each split in turn + if (evaluatedInclude.Length > 0) + { + IList includeSplits = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedInclude); + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(this.Project, originalItem.ItemType); + + foreach (string includeSplit in includeSplits) + { + // STEP 3: If expression is "@(x)" copy specified list with its metadata, otherwise just treat as string + bool throwaway; + + IList itemsFromSplit = expander.ExpandSingleItemVectorExpressionIntoItems(includeSplit, itemFactory, ExpanderOptions.ExpandItems, false /* do not include null expansion results */, out throwaway, originalItem.IncludeLocation); + + if (itemsFromSplit != null) + { + // Expression is in form "@(X)", so add these items directly. + foreach (ProjectItemInstance item in itemsFromSplit) + { + items.Add(item); + } + } + else + { + // The expression is not of the form "@(X)". Treat as string + string[] includeSplitFiles = EngineFileUtilities.GetFileListEscaped(Project.Directory, includeSplit); + + foreach (string includeSplitFile in includeSplitFiles) + { + items.Add(new ProjectItemInstance(Project, originalItem.ItemType, includeSplitFile, includeSplit /* before wildcard expansion */, null, null, originalItem.Location.File)); + } + } + } + + // STEP 4: Evaluate, split, expand and subtract any Exclude + if (originalItem.Exclude.Length > 0) + { + string evaluatedExclude = expander.ExpandIntoStringLeaveEscaped(originalItem.Exclude, ExpanderOptions.ExpandAll, originalItem.ExcludeLocation); + + if (evaluatedExclude.Length > 0) + { + IList excludeSplits = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedExclude); + HashSet excludesUnescapedForComparison = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string excludeSplit in excludeSplits) + { + string[] excludeSplitFiles = EngineFileUtilities.GetFileListUnescaped(Project.Directory, excludeSplit); + + foreach (string excludeSplitFile in excludeSplitFiles) + { + excludesUnescapedForComparison.Add(excludeSplitFile); + } + } + + List remainingItems = new List(); + + for (int i = 0; i < items.Count; i++) + { + if (!excludesUnescapedForComparison.Contains(((IItem)items[i]).EvaluatedInclude)) + { + remainingItems.Add(items[i]); + } + } + + items = remainingItems; + } + } + } + + // Filter the metadata as appropriate + if (keepMetadata != null) + { + foreach (var item in items) + { + var metadataToRemove = item.MetadataNames.Where(name => !keepMetadata.Contains(name)); + foreach (var metadataName in metadataToRemove) + { + item.RemoveMetadata(metadataName); + } + } + } + else if (removeMetadata != null) + { + foreach (var item in items) + { + var metadataToRemove = item.MetadataNames.Where(name => removeMetadata.Contains(name)); + foreach (var metadataName in metadataToRemove) + { + item.RemoveMetadata(metadataName); + } + } + } + + return items; + } + + /// + /// Returns a list of all items in the provided item group whose itemspecs match the specification, after it is split and any wildcards are expanded. + /// If no items match, returns null. + /// + /// The items to match + /// The specification to match against the items. + /// The specification to match against the provided items + /// The expander to use + /// A list of matching items + private List FindItemsMatchingSpecification + ( + ICollection items, + string specification, + ElementLocation specificationLocation, + Expander expander + ) + { + if (items.Count == 0 || specification.Length == 0) + { + return null; + } + + // This is a hashtable whose key is the filename for the individual items + // in the Exclude list, after wildcard expansion. + HashSet specificationsToFind = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Split by semicolons + IList specificationPieces = expander.ExpandIntoStringListLeaveEscaped(specification, ExpanderOptions.ExpandAll, specificationLocation); + + foreach (string piece in specificationPieces) + { + // Take each individual path or file expression, and expand any + // wildcards. Then loop through each file returned, and add it + // to our hashtable. + + // Don't unescape wildcards just yet - if there were any escaped, the caller wants to treat them + // as literals. Everything else is safe to unescape at this point, since we're only matching + // against the file system. + string[] fileList = EngineFileUtilities.GetFileListEscaped(Project.Directory, piece); + + foreach (string file in fileList) + { + // Now unescape everything, because this is the end of the road for this filename. + // We're just going to compare it to the unescaped include path to filter out the + // file excludes. + specificationsToFind.Add(EscapingUtilities.UnescapeAll(file)); + } + } + + if (specificationsToFind.Count == 0) + { + return null; + } + + // Now loop through our list and filter out any that match a + // filename in the remove list. + List itemsRemoved = new List(); + + foreach (ProjectItemInstance item in items) + { + // Even if the case for the excluded files is different, they + // will still get excluded, as expected. However, if the excluded path + // references the same file in a different way, such as by relative + // path instead of absolute path, we will not realize that they refer + // to the same file, and thus we will not exclude it. + if (specificationsToFind.Contains(item.EvaluatedInclude)) + { + itemsRemoved.Add(item); + } + } + + return itemsRemoved; + } + + /// + /// This class is used during ItemGroup intrinsic tasks to resolve metadata references. It consists of three tables: + /// 1. The metadata added during evaluation. + /// 1. The metadata table created for the bucket, may be null. + /// 2. The metadata table derived from the item definition group, may be null. + /// + private class NestedMetadataTable : IMetadataTable + { + /// + /// The table for all metadata added during expansion + /// + private Dictionary _addTable; + + /// + /// The table for metadata which was generated for this batch bucket. + /// May be null. + /// + private IMetadataTable _bucketTable; + + /// + /// The table for metadata from the item definition + /// May be null. + /// + private IMetadataTable _itemDefinitionTable; + + /// + /// The item type to which this metadata applies. + /// + private string _itemType; + + /// + /// Creates a new metadata table aggregating the bucket and item definition tables. + /// + /// The type of item for which we are doing evaluation. + /// The metadata table created for this batch bucket. May be null. + /// The metadata table for the item definition representing this item. May be null. + internal NestedMetadataTable(string itemType, IMetadataTable bucketTable, IMetadataTable itemDefinitionTable) + { + _itemType = itemType; + _addTable = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + _bucketTable = bucketTable; + _itemDefinitionTable = itemDefinitionTable; + } + + /// + /// Retrieves the metadata table used to collect additions. + /// + internal Dictionary AddedMetadata + { + get { return _addTable; } + } + + #region IMetadataTable Members + // NOTE: Leaving these methods public so as to avoid having to explicitly define them + // through the IMetadataTable interface and then cast everywhere they're used. This class + // is private, so it ultimately doesn't matter. + + /// + /// Gets the specified metadata value. Returns an empty string if none is set. + /// + public string GetEscapedValue(string name) + { + return GetEscapedValue(null, name); + } + + /// + /// Gets the specified metadata value for the qualified item type. Returns an empty string if none is set. + /// + public string GetEscapedValue(string specifiedItemType, string name) + { + return GetEscapedValueIfPresent(specifiedItemType, name) ?? String.Empty; + } + + /// + /// Gets the specified metadata value for the qualified item type. Returns null if none is set. + /// + public string GetEscapedValueIfPresent(string specifiedItemType, string name) + { + string value = null; + if (null == specifiedItemType || specifiedItemType == _itemType) + { + // Look in the addTable + if (_addTable.TryGetValue(name, out value)) + { + return value; + } + } + + // Look in the bucket table + if (null != _bucketTable) + { + value = _bucketTable.GetEscapedValueIfPresent(specifiedItemType, name); + if (null != value) + { + return value; + } + } + + // Look in the item definition table + if (null != _itemDefinitionTable) + { + value = _itemDefinitionTable.GetEscapedValueIfPresent(specifiedItemType, name); + } + + return value; + } + + #endregion + + /// + /// Sets the metadata value. + /// + internal void SetValue(string name, string value) + { + _addTable[name] = value; + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs new file mode 100644 index 00000000000..981109969fe --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper class to convert ItemGroup parameters to a string value for logging. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Helper class to convert ItemGroup parameters to a string value for logging. + /// + internal static class ItemGroupLoggingHelper + { + /// + /// Gets a text serialized value of a parameter for logging. + /// + internal static string GetParameterText(string prefix, string parameterName, params object[] parameterValues) + { + return GetParameterText(prefix, parameterName, (IList)parameterValues); + } + + /// + /// Gets a text serialized value of a parameter for logging. + /// + internal static string GetParameterText(string prefix, string parameterName, IList parameterValue) + { + if (parameterValue == null || parameterValue.Count == 0) + { + return parameterName; + } + + using (var sb = new ReuseableStringBuilder()) + { + sb.Append(prefix); + + bool firstEntryIsTaskItemWithSomeCustomMetadata = false; + var firstItem = parameterValue[0] as ITaskItem; + if (firstItem != null) + { + if (firstItem.CloneCustomMetadata().Count > 0) + { + firstEntryIsTaskItemWithSomeCustomMetadata = true; + } + } + + // If it's just one entry in the list, and it's not a task item with metadata, keep it on one line like a scalar + bool specialTreatmentForSingle = (parameterValue.Count == 1 && !firstEntryIsTaskItemWithSomeCustomMetadata); + + if (!specialTreatmentForSingle) + { + sb.Append("\n "); + } + + sb.Append(parameterName + "="); + + if (!specialTreatmentForSingle) + { + sb.Append("\n"); + } + + for (int i = 0; i < parameterValue.Count; i++) + { + if (parameterValue[i] == null) + { + continue; + } + + if (!specialTreatmentForSingle) + { + sb.Append(" "); + } + + sb.Append(GetStringFromParameterValue(parameterValue[i])); + + if (!specialTreatmentForSingle && i < parameterValue.Count - 1) + { + sb.Append("\n"); + } + } + + return sb.ToString(); + } + } + + /// + /// Given an object wrapping a scalar value that will be set on a task, + /// returns a suitable string to log its value, with a trailing newline. + /// First line is already indented. + /// Indent of any subsequent line should be 12 spaces. + /// + internal static string GetStringFromParameterValue(object parameterValue) + { + var type = parameterValue.GetType(); + + ErrorUtilities.VerifyThrow(!type.IsArray, "scalars only"); + + if (type == typeof(string)) + { + return (string)parameterValue; + } + else if (type.IsValueType) + { + return (string)Convert.ChangeType(parameterValue, typeof(string), CultureInfo.CurrentCulture); + } + else if (typeof(ITaskItem).IsAssignableFrom(type)) + { + var item = ((ITaskItem)parameterValue); + string result = item.ItemSpec; + + var customMetadata = item.CloneCustomMetadata(); + + if (customMetadata.Count > 0) + { + result += "\n"; + var names = new List(); + + foreach (string name in customMetadata.Keys) + { + names.Add(name); + } + + names.Sort(); + + for (int i = 0; i < names.Count; i++) + { + result += " " + names[i] + "=" + customMetadata[names[i]]; + + if (i < names.Count - 1) + { + result += "\n"; + } + } + } + + return result; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs new file mode 100644 index 00000000000..faad7091da6 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs @@ -0,0 +1,929 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +// NOTE: This is nearly identical to the MSBuild task in Microsoft.Build.Tasks. We are deprecating that task, +// so this is the governing implementation. + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class implements the "MSBuild" task, which hands off child project files to the MSBuild engine to be built. + /// + internal class MSBuild : ITask + { + /// + /// Enum describing the behavior when a project doesn't exist on disk. + /// + private enum SkipNonexistentProjectsBehavior + { + /// + /// Skip the project if there is no file on disk. + /// + Skip, + + /// + /// Error if the project does not exist on disk. + /// + Error, + + /// + /// Build even if the project does not exist on disk. + /// + Build + } + + /// + /// Default constructor. + /// + public MSBuild() + { + } + + #region Properties + + // projects to build + private ITaskItem[] _projects = null; + // A list of targets to build. This is an optional parameter. If it's omitted, + // the default targets are built. + private string[] _targets = null; + // A list of property name/value pairs to apply as global properties to the child project. + // Each string in this array should be of the form: "propname=propvalue" + private string[] _properties = null; + + /// + /// A semicolon-delimited list of global properties to undefine + /// + private string _undefineProperties = null; + + // outputs of all built targets + private ArrayList _targetOutputs = new ArrayList(); + // indicates if the paths of target output items should be rebased relative to the calling project + private bool _rebaseOutputs = false; + // Indicates that we should stop building remaining projects as soon as one fails to build. + // The default is that we chug ahead despite failures. + private bool _stopOnFirstFailure = false; + // When this is true, instead of calling the engine once to build all the targets (for each project), + // we would call the engine once per target (for each project). The benefit of this is that + // if one target fails, you can still continue with the remaining targets. + private bool _runEachTargetSeparately = false; + // When this is true we call the engine with all the projects at once instead of + // calling the engine once per project + private bool _buildInParallel = false; + // If true the project will be unloaded once the operation is completed + private bool _unloadProjectsOnCompletion = false; + // If true the cache will be checked for the result and the result will be stored if the operation + // is run + private bool _useResultsCache = true; + // Whether to skip project files that don't exist on disk. By default we error for such projects. + private SkipNonexistentProjectsBehavior _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Error; + // Value of ToolsVersion to use when building projects passed to this task. + private string _toolsVersion = null; + // Should Targets, Properties (+ properties as project metadata) be un-escaped before processing + private string[] _targetAndPropertyListSeparators = null; + + /// + /// The task logging helper + /// + private TaskLoggingHelper _logHelper = null; + + /// + /// The build engine, from ITask + /// + public IBuildEngine BuildEngine + { + get; + set; + } + + public IBuildEngine2 BuildEngine2 + { + get { return (IBuildEngine2)BuildEngine; } + } + + public IBuildEngine3 BuildEngine3 + { + get { return (IBuildEngine3)BuildEngine; } + } + + public TaskLoggingHelper Log + { + get + { + if (_logHelper == null) + { + _logHelper = new TaskLoggingHelperExtension(this, AssemblyResources.PrimaryResources, AssemblyResources.SharedResources, "MSBuild."); + } + + return _logHelper; + } + } + + /// + /// The host object, from ITask + /// + public ITaskHost HostObject + { + get; + set; + } + + /// + /// 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". + /// + /// + /// 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 + { + return _properties; + } + + set + { + _properties = value; + } + } + + /// + /// Gets or sets the set of global properties to remove. + /// + public string RemoveProperties + { + get + { + return _undefineProperties; + } + + set + { + _undefineProperties = value; + } + } + + /// + /// The targets to build in each project specified by the property. + /// + /// Array of target names. + public string[] Targets + { + get + { + return _targets; + } + + set + { + _targets = value; + } + } + + /// + /// The projects to build. + /// + /// Array of project items. + [Required] + public ITaskItem[] Projects + { + get + { + return _projects; + } + + set + { + _projects = value; + } + } + + /// + /// Outputs of the targets built in each project. + /// + /// Array of output items. + [Output] + public ITaskItem[] TargetOutputs + { + get + { + return (ITaskItem[])_targetOutputs.ToArray(typeof(ITaskItem)); + } + } + + /// + /// Indicates if the paths of target output items should be rebased relative to the calling project. + /// + /// true, if target output item paths should be rebased + public bool RebaseOutputs + { + get + { + return _rebaseOutputs; + } + + set + { + _rebaseOutputs = value; + } + } + + /// + /// Forces the task to stop building the remaining projects as soon as any of + /// them fail. + /// + public bool StopOnFirstFailure + { + get + { + return _stopOnFirstFailure; + } + + set + { + _stopOnFirstFailure = value; + } + } + + /// + /// When this is true, instead of calling the engine once to build all the targets (for each project), + /// we would call the engine once per target (for each project). The benefit of this is that + /// if one target fails, you can still continue with the remaining targets. + /// + public bool RunEachTargetSeparately + { + get + { + return _runEachTargetSeparately; + } + + set + { + _runEachTargetSeparately = value; + } + } + + /// + /// Value of ToolsVersion to use when building projects passed to this task. + /// + public string ToolsVersion + { + get + { + return _toolsVersion; + } + + set + { + _toolsVersion = value; + } + } + + /// + /// When this is true we call the engine with all the projects at once instead of + /// calling the engine once per project + /// + public bool BuildInParallel + { + get + { + return _buildInParallel; + } + set + { + _buildInParallel = value; + } + } + + /// + /// If true the project will be unloaded once the operation is completed + /// + public bool UnloadProjectsOnCompletion + { + get + { + return _unloadProjectsOnCompletion; + } + set + { + _unloadProjectsOnCompletion = value; + } + } + + /// + /// If true the cached result will be returned if present and a if MSBuild + /// task is run its result will be cached in a scope (ProjectFileName, GlobalProperties)[TargetNames] + /// as a list of build items + /// + public bool UseResultsCache + { + get + { + return _useResultsCache; + } + set + { + _useResultsCache = value; + } + } + + /// + /// When this is true, project files that do not exist on the disk will be skipped. By default, + /// such projects will cause an error. + /// + public string SkipNonexistentProjects + { + get + { + switch (_skipNonexistentProjects) + { + case SkipNonexistentProjectsBehavior.Build: + return "Build"; + + case SkipNonexistentProjectsBehavior.Error: + return "False"; + + case SkipNonexistentProjectsBehavior.Skip: + return "True"; + + default: + ErrorUtilities.ThrowInternalError("Unexpected case {0}", _skipNonexistentProjects); + break; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + set + { + if (String.Equals("Build", value, StringComparison.OrdinalIgnoreCase)) + { + _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Build; + } + else + { + ErrorUtilities.VerifyThrowArgument(ConversionUtilities.CanConvertStringToBool(value), "MSBuild.InvalidSkipNonexistentProjectValue"); + bool originalSkipValue = ConversionUtilities.ConvertStringToBool(value); + if (originalSkipValue) + { + _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Skip; + } + else + { + _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Error; + } + } + } + } + + /// + /// Unescape Targets, Properties (including Properties and AdditionalProperties as Project item metadata) + /// will be un-escaped before processing. e.g. %3B (an escaped ';') in the string for any of them will + /// be treated as if it were an un-escaped ';' + /// + public string[] TargetAndPropertyListSeparators + { + get + { + return _targetAndPropertyListSeparators; + } + set + { + _targetAndPropertyListSeparators = value; + } + } + + #endregion + + #region ITask Members + + public bool Execute() + { + throw new NotImplementedException(); + } + + /// + /// Instructs the MSBuild engine to build one or more project files whose locations are specified by the + /// property. + /// + /// true if all projects build successfully; false if any project fails + public async Task ExecuteInternal() + { + // If no projects were passed in, just return success. + if ((Projects == null) || (Projects.Length == 0)) + { + return true; + } + + // We have been asked to unescape all escaped characters before processing + if (this.TargetAndPropertyListSeparators != null && this.TargetAndPropertyListSeparators.Length > 0) + { + ExpandAllTargetsAndProperties(); + } + + // Parse the global properties into a hashtable. + Hashtable propertiesTable; + if (!PropertyParser.GetTableWithEscaping(Log, ResourceUtilities.FormatResourceString("General.GlobalProperties"), "Properties", this.Properties, out propertiesTable)) + { + return false; + } + + // Parse out the properties to undefine, if any. + string[] undefinePropertiesArray = null; + if (!String.IsNullOrEmpty(_undefineProperties)) + { + Log.LogMessageFromResources(MessageImportance.Low, "General.UndefineProperties"); + undefinePropertiesArray = _undefineProperties.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string property in undefinePropertiesArray) + { + Log.LogMessageFromText(String.Format(CultureInfo.InvariantCulture, " {0}", property), MessageImportance.Low); + } + } + + bool isRunningMultipleNodes = BuildEngine2.IsRunningMultipleNodes; + // If we are in single proc mode and stopOnFirstFailure is true, we cannot build in parallel because + // building in parallel sends all of the projects to the engine at once preventing us from not sending + // any more projects after the first failure. Therefore, to preserve compatibility with whidby if we are in this situation disable buildInParallel. + if (!isRunningMultipleNodes && _stopOnFirstFailure && _buildInParallel) + { + _buildInParallel = false; + Log.LogMessageFromResources(MessageImportance.Low, "MSBuild.NotBuildingInParallel"); + } + + // When the condition below is met, provide an information message indicating stopOnFirstFailure + // will have no effect. The reason there will be no effect is, when buildInParallel is true + // All project files will be submitted to the engine all at once, this mean there is no stopping for failures between projects. + // When RunEachTargetSpearately is false, all targets will be submitted to the engine at once, this means there is no way to stop between target failures. + // therefore the first failure seen will be the only failure seen. + if (isRunningMultipleNodes && _buildInParallel && _stopOnFirstFailure && !_runEachTargetSeparately) + { + Log.LogMessageFromResources(MessageImportance.Low, "MSBuild.NoStopOnFirstFailure"); + } + + // This is a list of string[]. That is, each element in the list is a string[]. Each + // string[] represents a set of target names to build. Depending on the value + // of the RunEachTargetSeparately parameter, we each just call the engine to run all + // the targets together, or we call the engine separately for each target. + ArrayList targetLists = CreateTargetLists(this.Targets, this.RunEachTargetSeparately); + + + bool success = true; + ITaskItem[] singleProject = null; + bool[] skipProjects = null; + + + if (_buildInParallel) + { + skipProjects = new bool[Projects.Length]; + for (int i = 0; i < skipProjects.Length; i++) + { + skipProjects[i] = true; + } + } + else + { + singleProject = new ITaskItem[1]; + } + + // Read in each project file. If there are any errors opening the file or parsing the XML, + // raise an event and return False. If any one of the projects fails to build, return False, + // otherwise return True. If parallel build is requested we first check for file existence so + // that we don't pass a non-existent file to IBuildEngine causing an exception + for (int i = 0; i < Projects.Length; i++) + { + ITaskItem project = Projects[i]; + + string projectPath = FileUtilities.AttemptToShortenPath(project.ItemSpec); + + if (_stopOnFirstFailure && !success) + { + // Inform the user that we skipped the remaining projects because StopOnFirstFailure=true. + Log.LogMessageFromResources(MessageImportance.Low, "MSBuild.SkippingRemainingProjects"); + + // We have encountered a failure. Caller has requested that we not + // continue with remaining projects. + break; + } + + if (File.Exists(projectPath) || (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Build)) + { + if (FileUtilities.IsVCProjFilename(projectPath)) + { + Log.LogErrorWithCodeFromResources("MSBuild.ProjectUpgradeNeededToVcxProj", project.ItemSpec); + success = false; + continue; + } + + // If we are building in parallel we want to only make one call to + // ExecuteTargets once we verified that all projects exist + if (!_buildInParallel) + { + singleProject[0] = project; + bool executeResult = await ExecuteTargets( + singleProject, + propertiesTable, + undefinePropertiesArray, + targetLists, + StopOnFirstFailure, + RebaseOutputs, + BuildEngine3, + Log, + _targetOutputs, + _useResultsCache, + _unloadProjectsOnCompletion, + ToolsVersion + ); + + if (!executeResult) + { + success = false; + } + } + else + { + skipProjects[i] = false; + } + } + else + { + if (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Skip) + { + Log.LogMessageFromResources(MessageImportance.High, "MSBuild.ProjectFileNotFoundMessage", project.ItemSpec); + } + else + { + ErrorUtilities.VerifyThrow(_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Error, "skipNonexistentProjects has unexpected value {0}", _skipNonexistentProjects); + Log.LogErrorWithCodeFromResources("MSBuild.ProjectFileNotFound", project.ItemSpec); + success = false; + } + } + } + + // We need to build all the projects that were not skipped + if (_buildInParallel) + { + success = await BuildProjectsInParallel(propertiesTable, undefinePropertiesArray, targetLists, success, skipProjects); + } + + return success; + } + + /// + /// Build projects which have not been skipped. This will be done in parallel + /// + private async Task BuildProjectsInParallel(Hashtable propertiesTable, string[] undefinePropertiesArray, ArrayList targetLists, bool success, bool[] skipProjects) + { + ITaskItem[] projectToBuildInParallel = Projects; + // There were some projects that were skipped so we need to recreate the + // project array with those projects removed + List projectsToBuildArrayList = new List(); + for (int i = 0; i < Projects.Length; i++) + { + if (!skipProjects[i]) + { + projectsToBuildArrayList.Add(Projects[i]); + } + } + projectToBuildInParallel = projectsToBuildArrayList.ToArray(); + + // Make the call to build the projects + if (projectToBuildInParallel.Length > 0) + { + bool executeResult = await ExecuteTargets + ( + projectToBuildInParallel, + propertiesTable, + undefinePropertiesArray, + targetLists, + StopOnFirstFailure, + RebaseOutputs, + BuildEngine3, + Log, + _targetOutputs, + _useResultsCache, + _unloadProjectsOnCompletion, + ToolsVersion + ); + + if (!executeResult) + { + success = false; + } + } + + return success; + } + + /// + /// Expand and re-construct arrays of all targets and properties + /// + private void ExpandAllTargetsAndProperties() + { + List expandedProperties = new List(); + List expandedTargets = new List(); + + if (this.Properties != null) + { + // Expand all properties + for (int n = 0; n < this.Properties.Length; n++) + { + // Split each property according to the separators + string[] expandedPropertyValues = this.Properties[n].Split(this.TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + // Add the resultant properties to the final list + foreach (string property in expandedPropertyValues) + { + expandedProperties.Add(property); + } + } + this.Properties = expandedProperties.ToArray(); + } + + if (this.Targets != null) + { + // Expand all targets + for (int n = 0; n < this.Targets.Length; n++) + { + // Split each target according to the separators + string[] expandedTargetValues = this.Targets[n].Split(this.TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + // Add the resultant targets to the final list + foreach (string target in expandedTargetValues) + { + expandedTargets.Add(target); + } + } + this.Targets = expandedTargets.ToArray(); + } + } + + /// + /// + /// + /// + /// + /// + internal static ArrayList CreateTargetLists + ( + string[] targets, + bool runEachTargetSeparately + ) + { + // This is a list of string[]. That is, each element in the list is a string[]. Each + // string[] represents a set of target names to build. Depending on the value + // of the RunEachTargetSeparately parameter, we each just call the engine to run all + // the targets together, or we call the engine separately for each target. + ArrayList targetLists = new ArrayList(); + if ((runEachTargetSeparately) && (targets != null) && (targets.Length > 0)) + { + // Separate target invocations for each individual target. + foreach (string targetName in targets) + { + targetLists.Add(new string[1] { targetName }); + } + } + else + { + // Just one target list, and that's whatever was passed in. We will call the engine + // once (per project) with the entire target list. + targetLists.Add(targets); + } + + return targetLists; + } + + /// + /// + /// + /// True if the operation was successful + internal static async Task ExecuteTargets + ( + ITaskItem[] projects, + Hashtable propertiesTable, + string[] undefineProperties, + ArrayList targetLists, + bool stopOnFirstFailure, + bool rebaseOutputs, + IBuildEngine3 buildEngine, + TaskLoggingHelper log, + ArrayList targetOutputs, + bool useResultsCache, + bool unloadProjectsOnCompletion, + string toolsVersion + ) + { + bool success = true; + + // We don't log a message about the project and targets we're going to + // build, because it'll all be in the immediately subsequent ProjectStarted event. + + string[] projectDirectory = new string[projects.Length]; + string[] projectNames = new string[projects.Length]; + string[] toolsVersions = new string[projects.Length]; + IList> targetOutputsPerProject = null; + IDictionary[] projectProperties = new IDictionary[projects.Length]; + List[] undefinePropertiesPerProject = new List[projects.Length]; + + for (int i = 0; i < projectNames.Length; i++) + { + projectNames[i] = null; + projectProperties[i] = propertiesTable; + + if (projects[i] != null) + { + // Retrieve projectDirectory only the first time. It never changes anyway. + string projectPath = FileUtilities.AttemptToShortenPath(projects[i].ItemSpec); + projectDirectory[i] = Path.GetDirectoryName(projectPath); + projectNames[i] = projects[i].ItemSpec; + toolsVersions[i] = toolsVersion; + + + // 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"))) + { + Hashtable preProjectPropertiesTable; + if (!PropertyParser.GetTableWithEscaping + (log, ResourceUtilities.FormatResourceString("General.OverridingProperties", projectNames[i]), "Properties", projects[i].GetMetadata("Properties").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries), + out preProjectPropertiesTable) + ) + { + return false; + } + + projectProperties[i] = preProjectPropertiesTable; + } + + if (undefineProperties != null) + { + undefinePropertiesPerProject[i] = new List(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[] propertiesToUndefine = projectUndefineProperties.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + if (undefinePropertiesPerProject[i] == null) + { + undefinePropertiesPerProject[i] = new List(propertiesToUndefine.Length); + } + + if (log != null && propertiesToUndefine.Length > 0) + { + log.LogMessageFromResources(MessageImportance.Low, "General.ProjectUndefineProperties", projectNames[i]); + foreach (string property in propertiesToUndefine) + { + undefinePropertiesPerProject[i].Add(property); + log.LogMessageFromText(String.Format(CultureInfo.InvariantCulture, " {0}", property), MessageImportance.Low); + } + } + } + + // 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"))) + { + Hashtable additionalProjectPropertiesTable; + if (!PropertyParser.GetTableWithEscaping + (log, ResourceUtilities.FormatResourceString("General.AdditionalProperties", projectNames[i]), "AdditionalProperties", projects[i].GetMetadata("AdditionalProperties").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries), + out additionalProjectPropertiesTable) + ) + { + return false; + } + + Hashtable combinedTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + // First copy in the properties from the global table that not in the additional properties table + if (projectProperties[i] != null) + { + foreach (DictionaryEntry entry in projectProperties[i]) + { + if (!additionalProjectPropertiesTable.Contains(entry.Key)) + { + combinedTable.Add(entry.Key, entry.Value); + } + } + } + // Add all the additional properties + foreach (DictionaryEntry entry in additionalProjectPropertiesTable) + { + combinedTable.Add(entry.Key, entry.Value); + } + projectProperties[i] = combinedTable; + } + + // If the user specified a different toolsVersion for this project - then override the setting + if (!String.IsNullOrEmpty(projects[i].GetMetadata("ToolsVersion"))) + { + toolsVersions[i] = projects[i].GetMetadata("ToolsVersion"); + } + } + } + + foreach (string[] targetList in targetLists) + { + if (stopOnFirstFailure && !success) + { + // Inform the user that we skipped the remaining targets StopOnFirstFailure=true. + log.LogMessageFromResources(MessageImportance.Low, "MSBuild.SkippingRemainingTargets"); + + // We have encountered a failure. Caller has requested that we not + // continue with remaining targets. + break; + } + + // Send the project off to the build engine. By passing in null to the + // first param, we are indicating that the project to build is the same + // as the *calling* project file. + bool currentTargetResult = true; + + TaskHost taskHost = (TaskHost)buildEngine; + BuildEngineResult result = await taskHost.InternalBuildProjects(projectNames, targetList, projectProperties, undefinePropertiesPerProject, toolsVersions, true /* ask that target outputs are returned in the buildengineresult */); + + currentTargetResult = result.Result; + targetOutputsPerProject = result.TargetOutputsPerProject; + success = success && currentTargetResult; + + // If the engine was able to satisfy the build request + if (currentTargetResult) + { + for (int i = 0; i < projects.Length; i++) + { + IEnumerable nonNullTargetList = (targetList != null) ? targetList : targetOutputsPerProject[i].Keys; + + foreach (string targetName in nonNullTargetList) + { + if (targetOutputsPerProject[i].ContainsKey(targetName)) + { + ITaskItem[] outputItemsFromTarget = (ITaskItem[])targetOutputsPerProject[i][targetName]; + + foreach (ITaskItem outputItemFromTarget in outputItemsFromTarget) + { + // No need to rebase if the calling project is the same as the callee project + // (project == null). Also no point in trying to copy item metadata either, + // because no items were passed into the Projects parameter! + if (projects[i] != null) + { + // Rebase the output item paths if necessary. No need to rebase if the calling + // project is the same as the callee project (project == null). + if (rebaseOutputs) + { + try + { + outputItemFromTarget.ItemSpec = Path.Combine(projectDirectory[i], outputItemFromTarget.ItemSpec); + } + catch (ArgumentException e) + { + log.LogWarningWithCodeFromResources(null, projects[i].ItemSpec, 0, 0, 0, 0, "MSBuild.CannotRebaseOutputItemPath", outputItemFromTarget.ItemSpec, e.Message); + } + } + + // Copy the custom item metadata from the "Projects" items to these + // output items. + projects[i].CopyMetadataTo(outputItemFromTarget); + + // Set a metadata on the output items called "MSBuildProjectFile" which tells you which project file produced this item. + if (String.IsNullOrEmpty(outputItemFromTarget.GetMetadata(ItemMetadataNames.msbuildSourceProjectFile))) + { + outputItemFromTarget.SetMetadata(ItemMetadataNames.msbuildSourceProjectFile, projects[i].GetMetadata(FileUtilities.ItemSpecModifiers.FullPath)); + } + } + + // Set a metadata on the output items called "MSBuildTargetName" which tells you which target produced this item. + if (String.IsNullOrEmpty(outputItemFromTarget.GetMetadata(ItemMetadataNames.msbuildSourceTargetName))) + { + outputItemFromTarget.SetMetadata(ItemMetadataNames.msbuildSourceTargetName, targetName); + } + } + + targetOutputs.AddRange(outputItemsFromTarget); + } + } + } + } + } + + return success; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs new file mode 100644 index 00000000000..9ead6ab0d81 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/IntrinsicTasks/PropertyGroupIntrinsicTask.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The PropertyGroup intrinsic task. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class represents a PropertyGroup intrinsic task. + /// + internal class PropertyGroupIntrinsicTask : IntrinsicTask + { + /// + /// The original task instance data. + /// + private ProjectPropertyGroupTaskInstance _taskInstance; + + /// + /// Create a new PropertyGroup task. + /// + /// The task instance data + /// The logging context + /// The project instance + /// Flag to determine whether or not to log task inputs. + public PropertyGroupIntrinsicTask(ProjectPropertyGroupTaskInstance taskInstance, TargetLoggingContext loggingContext, ProjectInstance projectInstance, bool logTaskInputs) + : base(loggingContext, projectInstance, logTaskInputs) + { + _taskInstance = taskInstance; + } + + /// + /// Execute a PropertyGroup element, including each child property + /// + /// The lookup use for evaluation and as a destination for these properties. + internal override void ExecuteTask(Lookup lookup) + { + foreach (ProjectPropertyGroupTaskPropertyInstance property in _taskInstance.Properties) + { + List buckets = null; + + try + { + // Find all the metadata references in order to create buckets + List parameterValues = new List(); + GetBatchableValuesFromProperty(parameterValues, property); + buckets = BatchingEngine.PrepareBatchingBuckets(parameterValues, lookup, property.Location); + + // "Execute" each bucket + foreach (ItemBucket bucket in buckets) + { + bool condition = ConditionEvaluator.EvaluateCondition + ( + property.Condition, + ParserOptions.AllowAll, + bucket.Expander, + ExpanderOptions.ExpandAll, + Project.Directory, + property.ConditionLocation, + LoggingContext.LoggingService, + LoggingContext.BuildEventContext + ); + + if (condition) + { + // Check for a reserved name now, so it fails right here instead of later when the property eventually reaches + // the outer scope. + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + !ReservedPropertyNames.IsReservedProperty(property.Name), + property.Location, + "CannotModifyReservedProperty", + property.Name + ); + + string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(property.Value, ExpanderOptions.ExpandAll, property.Location); + + if (LogTaskInputs && !LoggingContext.LoggingService.OnlyLogCriticalEvents) + { + LoggingContext.LogComment(MessageImportance.Low, "PropertyGroupLogMessage", property.Name, evaluatedValue); + } + + bucket.Lookup.SetProperty(ProjectPropertyInstance.Create(property.Name, evaluatedValue, property.Location, Project.IsImmutable)); + } + } + } + finally + { + if (buckets != null) + { + // Propagate the property changes to the bucket above + foreach (ItemBucket bucket in buckets) + { + bucket.LeaveScope(); + } + } + } + } + } + + /// + /// Adds batchable parameters from a property element into the list. If the property element was + /// a task, these would be its raw parameter values. + /// + /// The list which will contain the batchable values. + /// The property from which to take the values. + private void GetBatchableValuesFromProperty(List parameterValues, ProjectPropertyGroupTaskPropertyInstance property) + { + AddIfNotEmptyString(parameterValues, property.Value); + AddIfNotEmptyString(parameterValues, property.Condition); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ItemBucket.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ItemBucket.cs new file mode 100644 index 00000000000..5a0bb568d03 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/ItemBucket.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Globalization; + +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class represents a collection of items that are homogeneous w.r.t. + /// a certain set of metadata. + /// + internal sealed class ItemBucket : IComparable + { + #region Member data + + /// + /// This single object contains all of the data necessary to perform expansion of metadata, properties, + /// and items. + /// + private Expander _expander; + + /// + /// Metadata in this bucket + /// + private Dictionary _metadata; + + /// + /// The items for this bucket. + /// + private Lookup _lookup; + + /// + /// When buckets are being created for batching purposes, this indicates which order the + /// buckets were created in, so that the target/task being batched gets called with the items + /// in the same order as they were declared in the project file. For example, the first + /// bucket created gets bucketSequenceNumber=0, the second bucket created gets + /// bucketSequenceNumber=1, etc. + /// + private int _bucketSequenceNumber; + + /// + /// The entry we enter when we create the bucket. + /// + private Lookup.Scope _lookupEntry; + + #endregion + + #region Constructors + + /// + /// Private default constructor disallows parameterless instantiation. + /// + private ItemBucket() + { + // do nothing + } + + /// + /// Creates an instance of this class using the given bucket data. + /// + /// Item types being batched on: null indicates no batching is occurring + /// Hashtable of item metadata values: null indicates no batching is occurring + internal ItemBucket + ( + ICollection itemNames, + Dictionary metadata, + Lookup lookup, + int bucketSequenceNumber + ) + { + ErrorUtilities.VerifyThrow(lookup != null, "Need lookup."); + + // Create our own lookup just for this bucket + _lookup = lookup.Clone(); + + // Push down the items, so that item changes in this batch are not visible to parallel batches + _lookupEntry = _lookup.EnterScope("ItemBucket()"); + + // Add empty item groups for each of the item names, so that (unless items are added to this bucket) there are + // no item types visible in this bucket among the item types being batched on + if (itemNames != null) + { + foreach (string name in itemNames) + { + _lookup.PopulateWithItems(name, new List()); + } + } + + _metadata = metadata; + _expander = new Expander(_lookup.ReadOnlyLookup, _lookup.ReadOnlyLookup, new StringMetadataTable(metadata)); + + _bucketSequenceNumber = bucketSequenceNumber; + } + + #endregion + + #region Comparison methods + + /// + /// Compares this item bucket against the given one. The comparison is + /// solely based on the values of the item metadata in the buckets. + /// + /// + /// + /// -1, if this bucket is "less than" the second one + /// 0, if this bucket is equivalent to the second one + /// +1, if this bucket is "greater than" the second one + /// + public int CompareTo(object obj) + { + return HashTableUtility.Compare(_metadata, ((ItemBucket)obj)._metadata); + } + + /// + /// Constructs a token bucket object that can be compared against other + /// buckets. This dummy bucket is a patently invalid bucket, and cannot + /// be used for any other operations besides comparison. + /// + /// + /// PERF NOTE: A dummy bucket is intentionally very light-weight, and it + /// allocates a minimum of memory compared to a real bucket. + /// + /// + /// An item bucket that is invalid for everything except comparisons. + internal static ItemBucket GetDummyBucketForComparisons(Dictionary metadata) + { + ItemBucket bucket = new ItemBucket(); + bucket._metadata = metadata; + + return bucket; + } + + #endregion + + #region Properties + /// + /// Returns the object that knows how to handle all kinds of expansion for this bucket. + /// + internal Expander Expander + { + get + { + return _expander; + } + } + + + /// + /// When buckets are being created for batching purposes, this indicates which order the + /// buckets were created in, so that the target/task being batched gets called with the items + /// in the same order as they were declared in the project file. For example, the first + /// bucket created gets bucketSequenceNumber=0, the second bucket created gets + /// bucketSequenceNumber=1, etc. + /// + internal int BucketSequenceNumber + { + get + { + return _bucketSequenceNumber; + } + } + + /// + /// The items for this bucket. + /// + internal Lookup Lookup + { + get + { + return _lookup; + } + } + + #endregion + + #region Methods + + /// + /// Adds a new item to this bucket. + /// + internal void AddItem(ProjectItemInstance item) + { + _lookup.PopulateWithItem(item); + } + + /// + /// Leaves the lookup scope created for this bucket. + /// + internal void LeaveScope() + { + _lookupEntry.LeaveScope(); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/Lookup.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/Lookup.cs new file mode 100644 index 00000000000..f8ce22652c2 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/Lookup.cs @@ -0,0 +1,1513 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Microsoft.Build.Shared; +using System.Diagnostics; +using System.Threading; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; + +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace Microsoft.Build.BackEnd +{ + using ItemsMetadataUpdateDictionary = Dictionary; + using ItemTypeToItemsMetadataUpdateDictionary = Dictionary>; + + /// + /// Contains a list of item and property collections, optimized to allow + /// - very fast "cloning" + /// - quick lookups + /// - scoping down of item subsets in nested scopes (useful for batches) + /// - isolation of adds, removes, modifies, and property sets inside nested scopes + /// + /// When retrieving the item group for an item type, each table is consulted in turn, + /// starting with the primary table (the "top" or "innermost" table), until a table is found that has an entry for that type. + /// When an entry is found, it is returned without looking deeper. + /// This makes it possible to let callers see only a subset of items without affecting or cloning the original item groups, + /// by populating a scope with item groups that are empty or contain subsets of items in lower scopes. + /// + /// Instances of this class can be cloned with Clone() to share between batches. + /// + /// When EnterScope() is called, a fresh primary table is inserted, and all adds and removes will be invisible to + /// any clones made before the scope was entered and anyone who has access to item groups in lower tables. + /// + /// When LeaveScope() is called, the primary tables are merged into the secondary tables, and the primary tables are discarded. + /// This makes the adds and removes in the primary tables visible to clones made during the previous scope. + /// + /// Scopes can be populated (before Adds, Removes, and Lookups) using PopulateWithItem(). This reduces the set of items of a particular + /// type that are visible in a scope, because lookups of items of this type will stop at this level and see the subset, rather than the + /// larger set in a scope below. + /// + /// Items can be added or removed by calling AddNewItem() and RemoveItem(). Only the primary level is modified. + /// When items are added or removed they enter into a primary table exclusively for adds or removes, instead of the main primary table. + /// This allows the adds and removes to be applied to the scope below on LeaveScope(). Even when LeaveScope() is called, the adds and removes + /// stay in their separate add and remove tables: if they were applied to a main table, they could truncate the downward traversal performed by lookups + /// and hide items in a lower main table. Only on the final call of LeaveScope() can all adds and removes be applied to the outermost table, i.e., the project. + /// + /// Much the same applies to properties. + /// + /// For sensible semantics, only the current primary scope can be modified at any point. + /// + internal class Lookup + { + #region Fields + + /// + /// Ordered list of scope used for lookup. + /// Each scope contains multiple tables: + /// - the main item table (populated with subsets of lists, in order to create batches) + /// - the add table (items that have been added during execution) + /// - the remove table (items that have been removed during execution) + /// - the modify table (item metadata modifications) + /// - the main property table (populated with properties that are visible in this scope) + /// - the property set table (changes made to properties) + /// All have to be consulted to find the items and properties available in the current scope. + /// We have to keep them separate, because the adds and removes etc need to be applied to the table + /// below when we leave a scope. + /// + private LinkedList _lookupScopes = new LinkedList(); + + /// + /// When we are asked for all the items of a certain type using the GetItems() method, we may have to handle items + /// that have been modified earlier with ModifyItems(). These pending modifications can't be applied immediately to + /// the item because that would affect other batches. Instead we clone the item, apply the modification, and hand that over. + /// The problem is that later we might get asked to remove or modify that item. We want to make sure that we record that as + /// a remove or modify of the real item, not the clone we handed over. So we keep a lookup of (clone, original) to consult. + /// + private Dictionary _cloneTable; + + /// + /// Read-only wrapper around this lookup. + /// + private ReadOnlyLookup _readOnlyLookup; + + /// + /// A dictionary of named values for debugger display only. If + /// not debugging, this should be null. + /// + private IDictionary _globalsForDebugging; + + #endregion + + #region Constructors + + /// + /// Construct a lookup over specified items and properties. + /// Accept a dictionary of named values for debugger display only. If + /// not debugging, this should be null. + /// + internal Lookup(ItemDictionary projectItems, PropertyDictionary properties, IDictionary globalsForDebugging) + { + ErrorUtilities.VerifyThrowInternalNull(projectItems, "projectItems"); + ErrorUtilities.VerifyThrowInternalNull(properties, "properties"); + + Lookup.Scope scope = new Lookup.Scope(this, "Lookup()", projectItems, properties); + _lookupScopes.AddFirst(scope); + + _globalsForDebugging = globalsForDebugging; + } + + /// + /// Copy constructor (called via Clone() - clearer semantics) + /// + private Lookup(Lookup that) + { + // Add the same tables from the original + foreach (Lookup.Scope scope in that._lookupScopes) + { + _lookupScopes.AddLast(scope); + } + + // Clones need to share an (item)clone table; the batching engine asks for items from the lookup, + // then populates buckets with them, which have clone lookups. + _cloneTable = that._cloneTable; + + _globalsForDebugging = that._globalsForDebugging; + } + + #endregion + + #region Properties + + /// + /// Returns a read-only wrapper around this lookup + /// + internal ReadOnlyLookup ReadOnlyLookup + { + get + { + if (_readOnlyLookup == null) + { + _readOnlyLookup = new ReadOnlyLookup(this); + } + return _readOnlyLookup; + } + } + + // Convenience private properties + // "Primary" is the "top" or "innermost" scope + // "Secondary" is the next from the top. + private ItemDictionary PrimaryTable + { + get { return _lookupScopes.First.Value.Items; } + set { _lookupScopes.First.Value.Items = value; } + } + + private ItemDictionary PrimaryAddTable + { + get { return _lookupScopes.First.Value.Adds; } + set { _lookupScopes.First.Value.Adds = value; } + } + + private ItemDictionary PrimaryRemoveTable + { + get { return _lookupScopes.First.Value.Removes; } + set { _lookupScopes.First.Value.Removes = value; } + } + + private ItemTypeToItemsMetadataUpdateDictionary PrimaryModifyTable + { + get { return _lookupScopes.First.Value.Modifies; } + set { _lookupScopes.First.Value.Modifies = value; } + } + + private PropertyDictionary PrimaryPropertySets + { + get { return _lookupScopes.First.Value.PropertySets; } + set { _lookupScopes.First.Value.PropertySets = value; } + } + + private ItemDictionary SecondaryTable + { + get { return _lookupScopes.First.Next.Value.Items; } + set { _lookupScopes.First.Next.Value.Items = value; } + } + + private ItemDictionary SecondaryAddTable + { + get { return _lookupScopes.First.Next.Value.Adds; } + set { _lookupScopes.First.Next.Value.Adds = value; } + } + + private ItemDictionary SecondaryRemoveTable + { + get { return _lookupScopes.First.Next.Value.Removes; } + set { _lookupScopes.First.Next.Value.Removes = value; } + } + + private ItemTypeToItemsMetadataUpdateDictionary SecondaryModifyTable + { + get { return _lookupScopes.First.Next.Value.Modifies; } + set { _lookupScopes.First.Next.Value.Modifies = value; } + } + + private PropertyDictionary SecondaryProperties + { + get { return _lookupScopes.First.Next.Value.Properties; } + set { _lookupScopes.First.Next.Value.Properties = value; } + } + + private PropertyDictionary SecondaryPropertySets + { + get { return _lookupScopes.First.Next.Value.PropertySets; } + set { _lookupScopes.First.Next.Value.PropertySets = value; } + } + + /// + /// A dictionary of named values for debugger display only. If + /// not debugging, this should be null. + /// + internal IDictionary GlobalsForDebugging + { + get { return _globalsForDebugging; } + } + + #endregion + + #region Internal Methods + + /// + /// Compares the primary property sets of the passed in lookups to determine if there are properties which are shared between + /// the lookups. We find these shared property names because this indicates that the current Lookup is overriding the property value of another Lookup + /// When an override is detected a messages is generated to inform the users that the property is being changed between batches + /// + /// array or error messages to log + internal List GetPropertyOverrideMessages(Dictionary lookupHash) + { + List errorMessages = null; + // For each batch lookup list we need to compare the property items to see if they have already been set + if (PrimaryPropertySets != null) + { + foreach (ProjectPropertyInstance property in PrimaryPropertySets) + { + if (String.Equals(property.Name, ReservedPropertyNames.lastTaskResult, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string propertyName = property.Name; + // If the hash contains the property name, output a messages that displays the previous property value and the new property value + if (lookupHash.ContainsKey(propertyName)) + { + if (errorMessages == null) + { + errorMessages = new List(); + } + errorMessages.Add(ResourceUtilities.FormatResourceString("PropertyOutputOverridden", propertyName, EscapingUtilities.UnescapeAll(lookupHash[propertyName]), property.EvaluatedValue)); + } + + // Set the value of the hash to the new property value + // PERF: we store the EvaluatedValueEscaped here to avoid unnecessary unescaping (the value is stored + // escaped in the property) + lookupHash[propertyName] = ((IProperty)property).EvaluatedValueEscaped; + } + } + + return errorMessages; + } + + /// + /// Clones this object, to create another one with its own list, but the same contents. + /// Then the clone can enter scope and have its own fresh primary list without affecting the other object. + /// + internal Lookup Clone() + { + return new Lookup(this); + } + + /// + /// Enters the scope using the specified description. + /// Callers keep the scope in order to pass it to LeaveScope. + /// + internal Lookup.Scope EnterScope(string description) + { + // We don't create the tables unless we need them + Scope scope = new Scope(this, description, null, null); + _lookupScopes.AddFirst(scope); + return scope; + } + + /// + /// Leaves the specified scope, which must be the active one. + /// Moves all tables up one: the tertiary table becomes the secondary table, and so on. The primary + /// and secondary table are merged. This has the effect of "applying" the adds applied to the primary + /// table into the secondary table. + /// + private void LeaveScope(Lookup.Scope scopeToLeave) + { + ErrorUtilities.VerifyThrow(_lookupScopes.Count >= 2, "Too many calls to Leave()."); + ErrorUtilities.VerifyThrow(Object.ReferenceEquals(scopeToLeave, _lookupScopes.First.Value), "Attempting to leave with scope '{0}' but scope '{1}' is on top of the stack.", scopeToLeave.Description, _lookupScopes.First.Value.Description); + + // Our lookup works by stopping the first time it finds an item group of the appropriate type. + // So we can't apply an add directly into the table below because that could create a new group + // of that type, which would cause the next lookup to stop there and miss any existing items in a table below. + // Instead we keep adds stored separately until we're leaving the very last scope. Until then + // we only move adds down into the next add table below, and when we lookup we consider both tables. + // Same applies to removes. + if (_lookupScopes.Count == 2) + { + MergeScopeIntoLastScope(); + } + else + { + MergeScopeIntoNotLastScope(); + } + + // Let go of our pointer into the clone table; we assume we won't need it after leaving scope and want to save memory. + // This is an assumption on IntrinsicTask, that it won't ask to remove or modify a clone in a higher scope than it was handed out in. + // We mustn't call cloneTable.Clear() because other clones of this lookup may still be using it. When the last lookup clone leaves scope, + // the table will be collected. + _cloneTable = null; + + // Move all tables up one, discarding the primary tables + _lookupScopes.RemoveFirst(); + } + + /// + /// Leaving an arbitrary scope, just merging all the adds, removes, modifies, and sets into the scope below. + /// + private void MergeScopeIntoNotLastScope() + { + // Move all the adds down + if (PrimaryAddTable != null) + { + if (SecondaryAddTable == null) + { + SecondaryAddTable = PrimaryAddTable; + } + else + { + SecondaryAddTable.ImportItems(PrimaryAddTable); + } + } + + // Move all the removes down + if (PrimaryRemoveTable != null) + { + if (SecondaryRemoveTable == null) + { + SecondaryRemoveTable = PrimaryRemoveTable; + } + else + { + // When merging remove lists from two or more batches both tables (primary and secondary) may contain identical items. The reason is when removing the items we get the original items rather than a clone + // so the same item may have already been added to the secondary table. If we then try and add the same item from the primary table we will get a duplicate key exception from the + // dictionary. Therefore we must not merge in an item if it already is in the secondary remove table. + foreach (ProjectItemInstance item in PrimaryRemoveTable) + { + if (!SecondaryRemoveTable.Contains(item)) + { + SecondaryRemoveTable.Add(item); + } + } + } + } + + // Move all the modifies down + if (PrimaryModifyTable != null) + { + if (SecondaryModifyTable == null) + { + SecondaryModifyTable = PrimaryModifyTable; + } + else + { + foreach (KeyValuePair> entry in PrimaryModifyTable) + { + Dictionary modifiesOfType; + if (SecondaryModifyTable.TryGetValue(entry.Key, out modifiesOfType)) + { + // There are already modifies of this type: add to the existing table + foreach (KeyValuePair modify in entry.Value) + { + MergeModificationsIntoModificationTable(modifiesOfType, modify, ModifyMergeType.SecondWins); + } + } + else + { + SecondaryModifyTable.Add(entry.Key, entry.Value); + } + } + } + } + + // Move all the sets down + if (PrimaryPropertySets != null) + { + if (SecondaryPropertySets == null) + { + SecondaryPropertySets = PrimaryPropertySets; + } + else + { + SecondaryPropertySets.ImportProperties(PrimaryPropertySets); + } + } + } + + /// + /// Merge the current scope down into the base scope. This means applying the adds, removes, modifies, and sets + /// directly into the item and property tables in this scope. + /// + private void MergeScopeIntoLastScope() + { + // End of the line for this object: we are done with add tables, and we want to expose our + // adds to the world + if (PrimaryAddTable != null) + { + SecondaryTable = SecondaryTable ?? new ItemDictionary(); + SecondaryTable.ImportItems(PrimaryAddTable); + } + + if (PrimaryRemoveTable != null) + { + SecondaryTable = SecondaryTable ?? new ItemDictionary(); + SecondaryTable.RemoveItems(PrimaryRemoveTable); + } + + if (PrimaryModifyTable != null) + { + foreach (KeyValuePair> entry in PrimaryModifyTable) + { + SecondaryTable = SecondaryTable ?? new ItemDictionary(); + ApplyModificationsToTable(SecondaryTable, entry.Key, entry.Value); + } + } + + if (PrimaryPropertySets != null) + { + SecondaryProperties = SecondaryProperties ?? new PropertyDictionary(PrimaryPropertySets.Count); + SecondaryProperties.ImportProperties(PrimaryPropertySets); + } + } + + /// + /// Gets the effective property for the current scope. + /// taking the name from the provided string within the specified start and end indexes. + /// If no match is found, returns null. + /// Caller must not modify the property returned. + /// + internal ProjectPropertyInstance GetProperty(string name, int startIndex, int endIndex) + { + // Walk down the tables and stop when the first + // property with this name is found + foreach (Scope scope in _lookupScopes) + { + if (scope.PropertySets != null) + { + ProjectPropertyInstance property = scope.PropertySets.GetProperty(name, startIndex, endIndex); + if (property != null) + { + return property; + } + } + + if (scope.Properties != null) + { + ProjectPropertyInstance property = scope.Properties.GetProperty(name, startIndex, endIndex); + if (property != null) + { + return property; + } + } + + if (scope.TruncateLookupsAtThisScope) + { + break; + } + } + + return null; + } + + /// + /// Gets the effective property for the current scope. + /// If no match is found, returns null. + /// Caller must not modify the property returned. + /// + internal ProjectPropertyInstance GetProperty(string name) + { + ErrorUtilities.VerifyThrowInternalLength(name, "name"); + + return GetProperty(name, 0, name.Length - 1); + } + + /// + /// Gets the items of the specified type that are visible in the current scope. + /// If no match is found, returns an empty list. + /// Caller must not modify the group returned. + /// + internal ICollection GetItems(string itemType) + { + // The visible items consist of the adds (accumulated as we go down) + // plus the first set of regular items we encounter + // minus any removes + ItemDictionary allAdds = null; + ItemDictionary allRemoves = null; + Dictionary allModifies = null; + ICollection groupFound = null; + + foreach (Scope scope in _lookupScopes) + { + // Accumulate adds while we look downwards + if (scope.Adds != null) + { + ICollection adds = scope.Adds[itemType]; + if (adds.Count != 0) + { + allAdds = allAdds ?? new ItemDictionary(); + allAdds.ImportItemsOfType(itemType, adds); + } + } + + // Accumulate removes while we look downwards + if (scope.Removes != null) + { + ICollection removes = scope.Removes[itemType]; + if (removes.Count != 0) + { + allRemoves = allRemoves ?? new ItemDictionary(); + allRemoves.ImportItems(removes); + } + } + + // Accumulate modifications as we look downwards + if (scope.Modifies != null) + { + Dictionary modifies; + if (scope.Modifies.TryGetValue(itemType, out modifies)) + { + if (modifies.Count != 0) + { + allModifies = allModifies ?? new Dictionary(); + + // We already have some modifies for this type + foreach (KeyValuePair modify in modifies) + { + // If earlier scopes modify the same metadata on the same item, + // they have priority + MergeModificationsIntoModificationTable(allModifies, modify, ModifyMergeType.FirstWins); + } + } + } + } + + if (scope.Items != null) + { + groupFound = scope.Items[itemType]; + if (groupFound.Count != 0 || scope.Items.HasEmptyMarker(itemType)) + { + // Found a group: we go no further + break; + } + } + + if (scope.TruncateLookupsAtThisScope) + { + break; + } + } + + if ((allAdds == null) && + (allRemoves == null) && + (allModifies == null)) + { + // We can just hand out this group verbatim - + // that avoids any importing + groupFound = groupFound ?? ReadOnlyEmptyList.Instance; + + return groupFound; + } + + // We have adds and/or removes and/or modifies to incorporate. + // We can't modify the group, because that might + // be visible to other batches; we have to create + // a new one. + ItemDictionary result = new ItemDictionary(); + + if (groupFound != null) + { + result.ImportItemsOfType(itemType, groupFound); + } + // Removes are processed after adds; this means when we remove there's no need to concern ourselves + // with the case where the item being removed is in an add table somewhere. The converse case is not possible + // using a project file: a project file cannot create an item that was already removed, it can only create + // a unique new item. + if (allAdds != null) + { + result.ImportItemsOfType(itemType, allAdds); + } + + if (allRemoves != null) + { + result.RemoveItems(allRemoves); + } + + // Modifies can be processed last; if a modified item was removed, the modify will be ignored + if (allModifies != null) + { + ApplyModifies(result, allModifies); + } + + return result[itemType]; + } + + /// + /// Populates with an item group. This is done before the item lookup is used in this scope. + /// Assumes all the items in the group have the same, provided, type. + /// Assumes there is no item group of this type in the primary table already. + /// Should be used only by batching buckets, and if no items are passed, + /// explicitly stores a marker for this item type indicating this. + /// + internal void PopulateWithItems(string itemType, ICollection group) + { + PrimaryTable = PrimaryTable ?? new ItemDictionary(); + ICollection existing = PrimaryTable[itemType]; + ErrorUtilities.VerifyThrow(existing.Count == 0, "Cannot add an itemgroup of this type."); + + if (group.Count > 0) + { + PrimaryTable.ImportItemsOfType(itemType, group); + } + else + { + PrimaryTable.AddEmptyMarker(itemType); + } + } + + /// + /// Populates with an item. This is done before the item lookup is used in this scope. + /// There may or may not already be a group for it. + /// + internal void PopulateWithItem(ProjectItemInstance item) + { + PrimaryTable = PrimaryTable ?? new ItemDictionary(); + PrimaryTable.Add(item); + } + + /// + /// Apply a property to this scope. + /// + internal void SetProperty(ProjectPropertyInstance property) + { + // Setting in outer scope could be easily implemented, but our code does not do it at present + MustNotBeOuterScope(); + + // Put in the set table + PrimaryPropertySets = PrimaryPropertySets ?? new PropertyDictionary(); + PrimaryPropertySets.Set(property); + } + + /// + /// Implements a true add, an item that has been created in a batch. + /// + internal void AddNewItemsOfItemType(string itemType, ICollection group, bool doNotAddDuplicates = false) + { + // Adding to outer scope could be easily implemented, but our code does not do it at present + MustNotBeOuterScope(); + +#if DEBUG + foreach (ProjectItemInstance item in group) + { + MustNotBeInAnyTables(item); + } +#endif + + if (group.Count == 0) + { + return; + } + + // Put them in the add table + PrimaryAddTable = PrimaryAddTable ?? new ItemDictionary(); + IEnumerable itemsToAdd = group; + if (doNotAddDuplicates) + { + // Remove duplicates from the inputs. + itemsToAdd = itemsToAdd.Distinct(ProjectItemInstance.EqualityComparer); + + // Ensure we don't also add any that already exist. + var existingItems = GetItems(itemType); + if (existingItems.Count > 0) + { + itemsToAdd = itemsToAdd.Where(item => !(existingItems.Contains(item, ProjectItemInstance.EqualityComparer))); + } + } + + PrimaryAddTable.ImportItemsOfType(itemType, itemsToAdd); + } + + /// + /// Implements a true add, an item that has been created in a batch. + /// + internal void AddNewItem(ProjectItemInstance item) + { + // Adding to outer scope could be easily implemented, but our code does not do it at present + MustNotBeOuterScope(); + +#if DEBUG + // This item must not be in any table already; a project cannot create an item + // that already exists + MustNotBeInAnyTables(item); +#endif + + // Put in the add table + PrimaryAddTable = PrimaryAddTable ?? new ItemDictionary(); + PrimaryAddTable.Add(item); + } + + /// + /// Remove a bunch of items from this scope + /// + internal void RemoveItems(IEnumerable items) + { + foreach (ProjectItemInstance item in items) + { + RemoveItem(item); + } + } + + /// + /// Remove an item from this scope + /// + internal void RemoveItem(ProjectItemInstance item) + { + // Removing from outer scope could be easily implemented, but our code does not do it at present + MustNotBeOuterScope(); + + item = RetrieveOriginalFromCloneTable(item); + + // Put in the remove table + PrimaryRemoveTable = PrimaryRemoveTable ?? new ItemDictionary(); + PrimaryRemoveTable.Add(item); + + // No need to remove this item from the primary add table if it's + // already there -- we always apply removes after adds, so that add + // will be reversed anyway. + } + + /// + /// Modifies items in this scope with the same set of metadata modifications. + /// Assumes all the items in the group have the same, provided, type. + /// + internal void ModifyItems(string itemType, ICollection group, MetadataModifications metadataChanges) + { + // Modifying in outer scope could be easily implemented, but our code does not do it at present + MustNotBeOuterScope(); + +#if DEBUG + // This item should not already be in any remove table; there is no way a project can + // modify items that were already removed + // Obviously, do this only in debug, as it's a slow check for bugs. + LinkedListNode node = _lookupScopes.First; + while (node != null) + { + Scope scope = node.Value; + foreach (ProjectItemInstance item in group) + { + ProjectItemInstance actualItem = RetrieveOriginalFromCloneTable(item); + MustNotBeInTable(scope.Removes, actualItem); + } + node = node.Next; + } +#endif + + if (!metadataChanges.HasChanges) + { + return; + } + + // Put in the modify table + + // We don't need to check whether the item is in the add table vs. the main table; either + // way the modification will be applied. + PrimaryModifyTable = PrimaryModifyTable ?? new ItemTypeToItemsMetadataUpdateDictionary(MSBuildNameIgnoreCaseComparer.Default); + Dictionary modifiesOfType; + if (!PrimaryModifyTable.TryGetValue(itemType, out modifiesOfType)) + { + modifiesOfType = new Dictionary(); + PrimaryModifyTable[itemType] = modifiesOfType; + } + + foreach (ProjectItemInstance item in group) + { + // Each item needs its own collection for metadata changes, even if this particular change is the same + // for more than one item, subsequent changes might not be. + var metadataChangeCopy = metadataChanges.Clone(); + + // If we're asked to modify a clone we handed out, record it as a modify of the original + // instead + ProjectItemInstance actualItem = RetrieveOriginalFromCloneTable(item); + var modify = new KeyValuePair(actualItem, metadataChangeCopy); + MergeModificationsIntoModificationTable(modifiesOfType, modify, ModifyMergeType.SecondWins); + } + } + + #endregion + + #region Private Methods + + /// + /// Apply modifies to a temporary result group. + /// Items to be modified are virtual-cloned so the original isn't changed. + /// + private void ApplyModifies(ItemDictionary result, Dictionary allModifies) + { + // Clone, because we're modifying actual items, and this would otherwise be visible to other batches, + // and would be "published" even if a target fails. + // FUTURE - don't need to clone here for non intrinsic tasks, but at present, they don't do modifies + + // Store the clone, in case we're asked to modify or remove it later (we will record it against the real item) + _cloneTable = _cloneTable ?? new Dictionary(); + + foreach (var modify in allModifies) + { + ProjectItemInstance originalItem = modify.Key; + + if (result.Contains(originalItem)) + { + var modificationsToApply = modify.Value; + + // Modify the cloned item and replace the original with it. + ProjectItemInstance cloneItem = modify.Key.DeepClone(); + + ApplyMetadataModificationsToItem(modificationsToApply, cloneItem); + + result.Replace(originalItem, cloneItem); + + // This will be null if the item wasn't in the result group, ie, it had been removed after being modified + ErrorUtilities.VerifyThrow(!_cloneTable.ContainsKey(cloneItem), "Should be new, not already in table!"); + _cloneTable[cloneItem] = originalItem; + } + } + } + + /// + /// Applies the specified modifications to the supplied item. + /// + private static void ApplyMetadataModificationsToItem(MetadataModifications modificationsToApply, ProjectItemInstance itemToModify) + { + // Remove any metadata from the item which is slated for removal. The indexer in the modifications table will + // return a modification with Remove == true either if there is an explicit entry for that name in the modifications + // or if keepOnlySpecified == true and there is no entry for that name. + if (modificationsToApply.KeepOnlySpecified) + { + List metadataToRemove = new List(itemToModify.Metadata.Where(m => modificationsToApply[m.Name].Remove).Select(m => m.Name)); + foreach (var metadataName in metadataToRemove) + { + itemToModify.RemoveMetadata(metadataName); + } + } + + // Now make any additions or modifications + foreach (var modificationPair in modificationsToApply.ExplicitModifications) + { + if (modificationPair.Value.Remove) + { + itemToModify.RemoveMetadata(modificationPair.Key); + } + else if (modificationPair.Value.NewValue != null) + { + itemToModify.SetMetadata(modificationPair.Key, modificationPair.Value.NewValue); + } + } + } + + /// + /// Look up the "real" item by using its clone, and return the real item. + /// See for explanation of the clone table. + /// + private ProjectItemInstance RetrieveOriginalFromCloneTable(ProjectItemInstance item) + { + ProjectItemInstance original; + if (_cloneTable != null) + { + if (_cloneTable.TryGetValue(item, out original)) + { + item = original; + } + } + + return item; + } + + /// + /// Applies a list of modifications to the appropriate ItemDictionary in a main table. + /// If any modifications conflict, these modifications win. + /// + private void ApplyModificationsToTable(ItemDictionary table, string itemType, ItemsMetadataUpdateDictionary modify) + { + ICollection existing = table[itemType]; + if (existing != null) + { + foreach (var kvPair in modify) + { + if (table.Contains(kvPair.Key)) + { + var itemToModify = kvPair.Key; + var modificationsToApply = kvPair.Value; + + ApplyMetadataModificationsToItem(modificationsToApply, itemToModify); + } + } + } + } + + /// + /// Applies a modification to an item in a table of modifications. + /// If the item already exists in the table, merges in the modifications; if there is a conflict + /// the mergeType indicates which should win. + /// + private void MergeModificationsIntoModificationTable(Dictionary modifiesOfType, + KeyValuePair modify, + ModifyMergeType mergeType) + { + MetadataModifications existingMetadataChanges; + if (modifiesOfType.TryGetValue(modify.Key, out existingMetadataChanges)) + { + // There's already modifications for this item; merge with those + if (mergeType == ModifyMergeType.SecondWins) + { + // Merge the new modifications on top of the existing modifications. + existingMetadataChanges.ApplyModifications(modify.Value); + } + else + { + // Only apply explicit modifications. + foreach (var metadataChange in modify.Value.ExplicitModifications) + { + // If the existing metadata change list has an entry for this metadata, ignore this change. + // We continue to allow changes made when KeepOnlySpecified is set because it is assumed that explicit metadata changes + // always trump implicit ones. + if (!existingMetadataChanges.ContainsExplicitModification(metadataChange.Key)) + { + existingMetadataChanges[metadataChange.Key] = metadataChange.Value; + } + } + } + } + else + { + modifiesOfType.Add(modify.Key, modify.Value); + } + } + +#if DEBUG + /// + /// Verify item is not in the table + /// + private void MustNotBeInTable(ItemDictionary table, ProjectItemInstance item) + { + if (table != null && table.ItemTypes.Contains(item.ItemType)) + { + ICollection tableOfItemsOfSameType = table[item.ItemType]; + if (tableOfItemsOfSameType != null) + { + ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.Contains(item), "Item should not be in table"); + } + } + } + + /// + /// Verify item is not in the modify table + /// + private void MustNotBeInTable(ItemTypeToItemsMetadataUpdateDictionary table, ProjectItemInstance item) + { + if (table != null && table.ContainsKey(item.ItemType)) + { + ItemsMetadataUpdateDictionary tableOfItemsOfSameType = table[item.ItemType]; + if (tableOfItemsOfSameType != null) + { + ErrorUtilities.VerifyThrow(!tableOfItemsOfSameType.ContainsKey(item), "Item should not be in table"); + } + } + } + + /// + /// Verify item is not in any table in any scope + /// + private void MustNotBeInAnyTables(ProjectItemInstance item) + { + // This item should not already be in any table; there is no way a project can + // create items that already existed + // Obviously, do this only in debug, as it's a slow check for bugs. + LinkedListNode node = _lookupScopes.First; + while (node != null) + { + Scope scope = node.Value; + MustNotBeInTable(scope.Adds, item); + MustNotBeInTable(scope.Removes, item); + MustNotBeInTable(scope.Modifies, item); + node = node.Next; + } + } + +#endif + + /// + /// Add/remove/modify/set directly on an outer scope would need to be handled separately - it would apply + /// directly to the main tables. Our code isn't expected to do this. + /// + private void MustNotBeOuterScope() + { + ErrorUtilities.VerifyThrow(_lookupScopes.Count > 1, "Operation in outer scope not supported"); + } + + #endregion + + /// + /// When merging metadata, we can deal with a conflict two different ways: + /// FirstWins = any previous metadata with the name takes precedence + /// SecondWins = the new metadata with the name takes precedence + /// + private enum ModifyMergeType + { + FirstWins = 1, + SecondWins = 2 + } + + /// + /// A class representing a set of additions, modifications or removal of metadata from items. + /// + internal class MetadataModifications + { + /// + /// Flag indicating if the modifications should be interpreted such that the lack of an explicit entry for a metadata name + /// means that that metadata should be removed. + /// + private bool _keepOnlySpecified; + + /// + /// A set of explicitly-specified modifications. + /// + private HybridDictionary _modifications; + + /// + /// Constructor. + /// + /// When true, metadata which is not explicitly-specified here but which is present on the target + /// item should be removed. When false, only explicitly-specified modifications apply. + public MetadataModifications(bool keepOnlySpecified) + { + _keepOnlySpecified = keepOnlySpecified; + _modifications = new HybridDictionary(MSBuildNameIgnoreCaseComparer.Default); + } + + /// + /// Cloning constructor. + /// + /// The metadata modifications to clone. + private MetadataModifications(MetadataModifications other) + { + _keepOnlySpecified = other._keepOnlySpecified; + _modifications = new HybridDictionary(other._modifications, MSBuildNameIgnoreCaseComparer.Default); + } + + /// + /// Clones this modification set. + /// + /// A copy of the modifications. + public MetadataModifications Clone() + { + return new MetadataModifications(this); + } + + /// + /// A flag indicating whether or not there are any changes which might apply. + /// + public bool HasChanges + { + get { return _modifications.Count > 0 || _keepOnlySpecified == true; } + } + + /// + /// A flag indicating whether only those metadata explicitly-specified should be retained on a target item. + /// + public bool KeepOnlySpecified + { + get { return _keepOnlySpecified; } + } + + /// + /// Applies the modifications from the specified modifications to this one, performing a proper merge. + /// + /// The set of metadata modifications to merge into this one. + public void ApplyModifications(MetadataModifications other) + { + // Apply implicit modifications + if (other._keepOnlySpecified) + { + // Any metadata not specified in other must be removed from this one. + var metadataToRemove = new List(_modifications.Keys.Where(m => other[m].Remove)); + foreach (var metadata in metadataToRemove) + { + _modifications.Remove(metadata); + } + } + + _keepOnlySpecified |= other._keepOnlySpecified; + + // Now apply the explicit modifications from the other table + foreach (var modificationPair in other.ExplicitModifications) + { + MetadataModification existingModification; + if (modificationPair.Value.KeepValue && _modifications.TryGetValue(modificationPair.Key, out existingModification)) + { + // The incoming modification requests we maintain the "current value" of the metadata. If we have + // an existing change, maintain that as-is. Otherwise, fall through and apply our change directly. + if (existingModification.Remove || existingModification.NewValue != null) + { + continue; + } + } + + // Just copy over the changes from the other table to this one. + _modifications[modificationPair.Key] = modificationPair.Value; + } + } + + /// + /// Returns true if this block contains an explicitly-specified modification for the provided metadata name. + /// + /// The name of the metadata. + /// True if there is an explicit modification for this metadata, false otherwise. + /// The return value of this method is unaffected by the property. + public bool ContainsExplicitModification(string metadataName) + { + return _modifications.ContainsKey(metadataName); + } + + /// + /// Adds metadata to the modification table. + /// + /// The name of the metadata to add (or change) in the target item. + /// The metadata value. + public void Add(string metadataName, string metadataValue) + { + _modifications.Add(metadataName, MetadataModification.CreateFromNewValue(metadataValue)); + } + + /// + /// Provides an enumeration of the explicit metadata modifications. + /// + public IEnumerable> ExplicitModifications + { + get { return _modifications; } + } + + /// + /// Sets or retrieves a modification from the modifications table. + /// + /// The metadata name. + /// If is true, this will return a modification with + /// set to true if the metadata has no other explicitly-specified modification. Otherwise it will return only the explicitly-specified + /// modification if one exists. + /// When if false, this is thrown if the metadata + /// specified does not exist when attempting to retrieve a metadata modification. + public MetadataModification this[string metadataName] + { + get + { + MetadataModification modification = null; + if (!_modifications.TryGetValue(metadataName, out modification)) + { + if (_keepOnlySpecified) + { + // This metadata was not specified and we are only keeping specified metadata, so remove it. + return MetadataModification.CreateFromRemove(); + } + + return MetadataModification.CreateFromNoChange(); + } + + return modification; + } + + set + { + ErrorUtilities.VerifyThrowInternalNull(value, "value"); + _modifications[metadataName] = value; + } + } + } + + /// + /// A type of metadata modification. + /// + internal enum ModificationType + { + /// + /// Indicates the metadata value should be kept unchanged. + /// + Keep, + + /// + /// Indicates the metadata value should be changed. + /// + Update, + + /// + /// Indicates the metadata value should be removed. + /// + Remove + } + + /// + /// Represents a modification for a single metadata. + /// + internal class MetadataModification + { + /// + /// When true, indicates the metadata should be removed from the target item. + /// + private readonly bool _remove; + + /// + /// The value to which the metadata should be set. If null, the metadata value should be retained unmodified. + /// + private readonly string _newValue; + + /// + /// A modification which indicates the metadata value should be retained without modification. + /// + private static readonly MetadataModification s_keepModification = new MetadataModification(ModificationType.Keep); + + /// + /// A modification which indicates the metadata should be removed. + /// + private static readonly MetadataModification s_removeModification = new MetadataModification(ModificationType.Remove); + + /// + /// Constructor for metadata modifications of type Keep or Remove. + /// + /// The type of modification to make. + private MetadataModification(ModificationType modificationType) + { + ErrorUtilities.VerifyThrow(modificationType != ModificationType.Update, "Modification type may only be update when a value is specified."); + _remove = modificationType == ModificationType.Remove; + _newValue = null; + } + + /// + /// Constructor for metadata modifications of type Update. + /// + /// The new value for the metadata. + private MetadataModification(string value) + { + _remove = false; + _newValue = value; + } + + /// + /// Creates a metadata modification of type Keep. + /// + /// The metadata modification. + public static MetadataModification CreateFromNoChange() + { + return s_keepModification; + } + + /// + /// Creates a metadata modification of type Update with the specified metadata value. + /// + /// The new metadata value. + /// The metadata modification. + public static MetadataModification CreateFromNewValue(string newValue) + { + return new MetadataModification(newValue); + } + + /// + /// Creates a metadata modification of type Remove. + /// + /// The metadata modification. + public static MetadataModification CreateFromRemove() + { + return s_removeModification; + } + + /// + /// When true, this modification indicates the associated metadata should be removed. + /// + public bool Remove + { + get { return _remove; } + } + + /// + /// When true, this modification indicates the associated metadata should retain its existing value. + /// + public bool KeepValue + { + get { return (_remove == false && _newValue == null); } + } + + /// + /// The new value of the metadata. Only valid when is false. + /// + public string NewValue + { + get { return _newValue; } + } + } + + /// + /// Represents an entry in the lookup list. + /// Class rather than a struct so that it can be modified in the list. + /// + internal class Scope + { + /// + /// Contains all of the original items at this level in the Lookup + /// + private ItemDictionary _items; + + /// + /// Contains all of the items which have been added at this level in the Lookup + /// + private ItemDictionary _adds; + + /// + /// Contails all of the items which have been removed at this level in the Lookup + /// + private ItemDictionary _removes; + + /// + /// Contains all of the metadata which has been changed for items at this level in the Lookup. + /// Schema: { K=type, V= { K=item, V=table of { K=metadata name, V=metadata value }}} + /// + private ItemTypeToItemsMetadataUpdateDictionary _modifies; + + /// + /// Contains all of the original properties at this level in the Lookup + /// + private PropertyDictionary _properties; + + /// + /// Contains all of the properties which have been set at this level or above in the Lookup + /// + private PropertyDictionary _propertySets; + + /// + /// The managed thread id which entered this scope. + /// + private int _threadIdThatEnteredScope; + + /// + /// A description of this scope, for error checking + /// + private string _description; + + /// + /// The lookup which owns this scope, for error checking. + /// + private Lookup _owningLookup; + + /// + /// Indicates whether or not further levels in the Lookup should be consulted beyond this one + /// to find the actual value for the desired item or property. + /// + private bool _truncateLookupsAtThisScope; + + internal Scope(Lookup lookup, string description, ItemDictionary items, PropertyDictionary properties) + { + _owningLookup = lookup; + _description = description; + _items = items; + _adds = null; + _removes = null; + _modifies = null; + _properties = properties; + _propertySets = null; + _threadIdThatEnteredScope = Thread.CurrentThread.ManagedThreadId; + _truncateLookupsAtThisScope = false; + } + + /// + /// The main table, populated with items that + /// are initially visible in this scope. Does not + /// include adds or removes unless it's the table in + /// the outermost scope. + /// + internal ItemDictionary Items + { + get { return _items; } + set { _items = value; } + } + /// + /// Adds made in this scope or above. + /// + internal ItemDictionary Adds + { + get { return _adds; } + set { _adds = value; } + } + /// + /// Removes made in this scope or above. + /// + internal ItemDictionary Removes + { + get { return _removes; } + set { _removes = value; } + } + /// + /// Modifications made in this scope or above. + /// + internal ItemTypeToItemsMetadataUpdateDictionary Modifies + { + get { return _modifies; } + set { _modifies = value; } + } + /// + /// The main property table, populated with properties + /// that are initially visible in this scope. Does not + /// include sets unless it's the table in the outermost scope. + /// + internal PropertyDictionary Properties + { + get { return _properties; } + set { _properties = value; } + } + /// + /// Properties set in this scope or above. + /// + internal PropertyDictionary PropertySets + { + get { return _propertySets; } + set { _propertySets = value; } + } + /// + /// ID of thread owning this scope + /// + internal int ThreadIdThatEnteredScope + { + get { return _threadIdThatEnteredScope; } + } + /// + /// Whether to stop lookups going beyond this scope downwards + /// + internal bool TruncateLookupsAtThisScope + { + get { return _truncateLookupsAtThisScope; } + } + + /// + /// The description assigned to this scope. + /// + internal string Description + { + get { return _description; } + } + + /// + /// Leaves the current lookup scope. + /// + internal void LeaveScope() + { + _owningLookup.LeaveScope(this); + } + } + } + + #region Related Types + + /// + /// Read-only wrapper around a lookup. + /// Passed to Expander and ItemExpander, which only need to + /// use a lookup in a read-only fashion, thus increasing + /// encapsulation of the data in the Lookup. + /// + internal class ReadOnlyLookup : IPropertyProvider, IItemProvider + { + private Lookup _lookup; + + internal ReadOnlyLookup(Lookup lookup) + { + _lookup = lookup; + } + + public ICollection GetItems(string itemType) + { + return _lookup.GetItems(itemType); + } + + public ProjectPropertyInstance GetProperty(string name) + { + return _lookup.GetProperty(name); + } + + public ProjectPropertyInstance GetProperty(string name, int startIndex, int endIndex) + { + return _lookup.GetProperty(name, startIndex, endIndex); + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/RequestBuilder.cs new file mode 100644 index 00000000000..05075b77b33 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -0,0 +1,1231 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The build request builder component. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Globalization; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#if MSBUILDENABLEVSPROFILING +using Microsoft.VisualStudio.Profiler; +#endif +#endif +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; +using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Implementation of IRequestBuilder + /// + internal class RequestBuilder : IRequestBuilder, IRequestBuilderCallback, IBuildComponent + { + /// + /// The event used to signal that this request should immediately terminate. + /// + private ManualResetEvent _terminateEvent; + + /// + /// The event used to signal that this request should wake up from its wait state. + /// + private AutoResetEvent _continueEvent; + + /// + /// The results used when a build request entry continues. + /// + private IDictionary _continueResults; + + /// + /// The task representing the currently-executing build request. + /// + private Task _requestTask; + + /// + /// The cancellation token source for the currently-executing build request. + /// + private CancellationTokenSource _cancellationTokenSource; + + /// + /// The build request entry being built. + /// + private BuildRequestEntry _requestEntry; + + /// + /// The component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// The node logging context + /// + private NodeLoggingContext _nodeLoggingContext; + + /// + /// The project logging context + /// + private ProjectLoggingContext _projectLoggingContext; + + /// + /// The target builder. + /// + private ITargetBuilder _targetBuilder; + + /// + /// Block type + /// + private BlockType _blockType = BlockType.Unblocked; + + /// + /// Flag indicating we are in an MSBuild callback + /// + private bool _inMSBuildCallback = false; + + /// + /// Flag indicating whether this request builder has been zombied by a cancellation request. + /// + private bool _isZombie = false; + + /// + /// Creates a new request builder. + /// + internal RequestBuilder() + { + _terminateEvent = new ManualResetEvent(false); + _continueEvent = new AutoResetEvent(false); + } + + /// + /// The event raised when a new build request should be issued. + /// + public event NewBuildRequestsDelegate OnNewBuildRequests; + + /// + /// The event raised when the build request has completed. + /// + public event BuildRequestCompletedDelegate OnBuildRequestCompleted; + + /// + /// The event raised when the build request has completed. + /// + public event BuildRequestBlockedDelegate OnBuildRequestBlocked; + + /// + /// The current block type + /// + private enum BlockType + { + /// + /// We are blocked waiting on a target in progress. + /// + BlockedOnTargetInProgress, + + /// + /// We are blocked waiting for results from child requests. + /// + BlockedOnChildRequests, + + /// + /// We are blocked because we have yielded control + /// + Yielded, + + /// + /// We are not blocked at all. + /// + Unblocked + } + + /// + /// Retrieves the request entry associated with this RequestBuilder. + /// + internal BuildRequestEntry RequestEntry + { + get + { + VerifyIsNotZombie(); + return _requestEntry; + } + } + + /// + /// Returns true if this RequestBuilder has an active build request + /// + internal bool HasActiveBuildRequest + { + get + { + VerifyIsNotZombie(); + + return (_requestTask != null && !_requestTask.IsCompleted) || (_componentHost.LegacyThreadingData.MainThreadSubmissionId != -1); + } + } + + /// + /// Starts a build request + /// + /// The logging context for the node. + /// The entry to build. + public void BuildRequest(NodeLoggingContext loggingContext, BuildRequestEntry entry) + { + ErrorUtilities.VerifyThrowArgumentNull(loggingContext, "loggingContext"); + ErrorUtilities.VerifyThrowArgumentNull(entry, "entry"); + ErrorUtilities.VerifyThrow(null != _componentHost, "Host not set."); + ErrorUtilities.VerifyThrow(_targetBuilder == null, "targetBuilder not null"); + ErrorUtilities.VerifyThrow(_nodeLoggingContext == null, "nodeLoggingContext not null"); + ErrorUtilities.VerifyThrow(_requestEntry == null, "requestEntry not null"); + ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Cancel already called"); + + _nodeLoggingContext = loggingContext; + _blockType = BlockType.Unblocked; + _requestEntry = entry; + _requestEntry.Continue(); + _continueResults = null; + + _targetBuilder = (ITargetBuilder)_componentHost.GetComponent(BuildComponentType.TargetBuilder); + + VerifyEntryInActiveState(); + InitializeOperatingEnvironment(); + StartBuilderThread(); + } + + /// + /// Continues a build request + /// + public void ContinueRequest() + { + ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building"); + ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0, false), "Request already terminated"); + ErrorUtilities.VerifyThrow(!_continueEvent.WaitOne(0, false), "Request already continued"); + VerifyEntryInReadyState(); + + _continueResults = _requestEntry.Continue(); + ErrorUtilities.VerifyThrow((_blockType == BlockType.BlockedOnTargetInProgress || _blockType == BlockType.Yielded) || (_continueResults != null), "Unexpected null results for request {0} (nr {1})", _requestEntry.Request.GlobalRequestId, _requestEntry.Request.NodeRequestId); + + // Setting the continue event will wake up the build thread, which is suspended in StartNewBuildRequests. + _continueEvent.Set(); + } + + /// + /// Terminates the build request + /// + /// + /// Once we have entered this method, no more methods will be invoked on this class (save ShutdownComponent) + /// as we should no longer be receiving any messages from the BuildManager. + /// + public void CancelRequest() + { + this.BeginCancel(); + this.WaitForCancelCompletion(); + } + + /// + /// Starts to cancel an existing request. + /// + public void BeginCancel() + { + _terminateEvent.Set(); + + // Cancel the current build. + if (_cancellationTokenSource != null) + { + if (_cancellationTokenSource.IsCancellationRequested) + { + return; + } + + _cancellationTokenSource.Cancel(); + } + } + + /// + /// Waits for the cancellation until it's completed, and cleans up the internal states. + /// + public void WaitForCancelCompletion() + { + // Wait for the request thread to terminate. + if (_requestTask != null) + { + bool taskCleanedUp = false; + + try + { + taskCleanedUp = _requestTask.Wait(BuildParameters.RequestBuilderShutdownTimeout); + } + catch (AggregateException e) + { + AggregateException flattenedException = e.Flatten(); + + if (flattenedException.InnerExceptions.All(ex => (ex is TaskCanceledException || ex is OperationCanceledException))) + { + // ignore -- just indicates that the task finished cancelling before we got a chance to wait on it. + taskCleanedUp = true; + } + else + { + throw; + } + } + + if (!taskCleanedUp) + { + // This can happen when a task has locked us up. + _projectLoggingContext.LogError(new BuildEventFileInfo(String.Empty), "FailedToReceiveTaskThreadStatus", BuildParameters.RequestBuilderShutdownTimeout); + ErrorUtilities.ThrowInvalidOperation("UnableToCancel"); + } + } + + _isZombie = true; + } + + #region IRequestBuilderCallback Members + + /// + /// This method instructs the request builder to build the specified projects using the specified parameters. This is + /// what is ultimately used by something like an MSBuild task which needs to invoke a project-to-project reference. IBuildEngine + /// and IBuildEngine2 have BuildProjectFile methods which boil down to an invocation of this method as well. + /// + /// An array of projects to be built. + /// The property groups to use for each project. Must be the same number as there are project files. + /// The tools version to use for each project. Must be the same number as there are project files. + /// The targets to be built. Each project will be built with the same targets. + /// True to wait for the results + /// True if the requests were satisfied, false if they were aborted. + public async Task BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults) + { + VerifyIsNotZombie(); + ErrorUtilities.VerifyThrowArgumentNull(projectFiles, "projectFiles"); + ErrorUtilities.VerifyThrowArgumentNull(properties, "properties"); + ErrorUtilities.VerifyThrowArgumentNull(targets, "targets"); + ErrorUtilities.VerifyThrowArgumentNull(toolsVersions, "toolsVersions"); + ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set"); + ErrorUtilities.VerifyThrow(projectFiles.Length == properties.Length, "Properties and project counts not the same"); + ErrorUtilities.VerifyThrow(projectFiles.Length == toolsVersions.Length, "Tools versions and project counts not the same"); + + FullyQualifiedBuildRequest[] requests = new FullyQualifiedBuildRequest[projectFiles.Length]; + + for (int i = 0; i < projectFiles.Length; ++i) + { + if (!Path.IsPathRooted(projectFiles[i])) + { + projectFiles[i] = Path.Combine(_requestEntry.ProjectRootDirectory, projectFiles[i]); + } + + // Canonicalize + projectFiles[i] = FileUtilities.NormalizePath(projectFiles[i]); + + // A tools version specified by an MSBuild task or similar has priority + string explicitToolsVersion = toolsVersions[i]; + + // Otherwise go to any explicit tools version on the project who made this callback + if (explicitToolsVersion == null && _requestEntry.RequestConfiguration.ExplicitToolsVersionSpecified) + { + explicitToolsVersion = _requestEntry.RequestConfiguration.ToolsVersion; + } + + // Otherwise let the BuildRequestConfiguration figure out what tools version will be used + BuildRequestData data = new BuildRequestData(projectFiles[i], properties[i].ToDictionary(), explicitToolsVersion, targets, null); + + BuildRequestConfiguration config = new BuildRequestConfiguration(data, _componentHost.BuildParameters.DefaultToolsVersion, _componentHost.BuildParameters.GetToolset); + + requests[i] = new FullyQualifiedBuildRequest(config, targets, waitForResults); + } + + // Send the requests off + BuildResult[] results = await StartNewBuildRequests(requests); + + ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests"); + + return results; + } + + /// + /// This method is called when the current request needs to build a target which is already in progress on this configuration, but which + /// is being built by another request. + /// + /// The id of the request on which we are blocked. + public async Task BlockOnTargetInProgress(int blockingGlobalRequestId, string blockingTarget) + { + VerifyIsNotZombie(); + SaveOperatingEnvironment(); + + _blockType = BlockType.BlockedOnTargetInProgress; + + RaiseOnBlockedRequest(blockingGlobalRequestId, blockingTarget); + + WaitHandle[] handles = new WaitHandle[] { _terminateEvent, _continueEvent }; + + int handle; + if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry)) + { + handle = WaitHandle.WaitAny(handles); + } + else + { + handle = await handles.ToTask(); + } + + RestoreOperatingEnvironment(); + + if (handle == 0) + { + // We've been aborted + throw new BuildAbortedException(); + } + + _blockType = BlockType.Unblocked; + + VerifyEntryInActiveState(); + } + + /// + /// Yields the node. + /// + public void Yield() + { + VerifyIsNotZombie(); + SaveOperatingEnvironment(); + + _blockType = BlockType.Yielded; + + RaiseOnBlockedRequest(_requestEntry.Request.GlobalRequestId, null); + } + + /// + /// Waits for the node to be reacquired. + /// + public void Reacquire() + { + VerifyIsNotZombie(); + RaiseOnBlockedRequest(_requestEntry.Request.GlobalRequestId, String.Empty); + + WaitHandle[] handles = new WaitHandle[] { _terminateEvent, _continueEvent }; + + int handle = WaitHandle.WaitAny(handles); + + RestoreOperatingEnvironment(); + + if (handle == 0) + { + // We've been aborted + throw new BuildAbortedException(); + } + + _blockType = BlockType.Unblocked; + + VerifyEntryInActiveState(); + } + + /// + /// Enters the state where we are going to perform a build request callback. + /// + public void EnterMSBuildCallbackState() + { + VerifyIsNotZombie(); + ErrorUtilities.VerifyThrow(!_inMSBuildCallback, "Already in an MSBuild callback!"); + _inMSBuildCallback = true; + } + + /// + /// Exits the build request callback state. + /// + public void ExitMSBuildCallbackState() + { + VerifyIsNotZombie(); + ErrorUtilities.VerifyThrow(_inMSBuildCallback, "Not in an MSBuild callback!"); + _inMSBuildCallback = false; + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + ErrorUtilities.VerifyThrow(_componentHost == null, "RequestBuilder already initialized."); + _componentHost = host; + } + + /// + /// Shuts down this component + /// + public void ShutdownComponent() + { + _componentHost = null; + } + + #endregion + + /// + /// Returns true if this builder is using legacy threading semantics. + /// + internal static bool IsBuilderUsingLegacyThreadingSemantics(IBuildComponentHost host, BuildRequestEntry entry) + { + return host.BuildParameters.LegacyThreadingSemantics && (host.LegacyThreadingData.MainThreadSubmissionId == entry.Request.SubmissionId); + } + + /// + /// This method waits for the specified handles, but will also spawn a request builder "thread" if that event is set. + /// This mechanism is used to implement running RequestBuilder threads on the main UI thread in VS. + /// + /// The index of the handle which was signaled. + internal static int WaitWithBuilderThreadStart(WaitHandle[] handles, bool recursive, LegacyThreadingData threadingData, int submissionId) + { + WaitHandle[] allHandles = new WaitHandle[handles.Length + 1]; + allHandles[0] = threadingData.GetStartRequestBuilderMainThreadEventForSubmission(submissionId); + Array.Copy(handles, 0, allHandles, 1, handles.Length); + + while (true) + { + try + { + int signaledIndex = WaitHandle.WaitAny(allHandles, Timeout.Infinite); + + if (signaledIndex == 0) + { + // Grab the request builder reserved for running on this thread. + RequestBuilder builder = threadingData.InstanceForMainThread; + + // This clears out the value so we can re-enter with legacy-threading semantics on another request builder + // which must use this same thread. It is safe to perform this operation because request activations cannot + // happen in parallel on the same thread, so there is no race. + threadingData.InstanceForMainThread = null; + + // Now wait for the request to build. + builder.RequestThreadProc(setThreadParameters: false).Wait(); + } + else + { + // We were signalled on one of the other handles. Return control to the caller. + return signaledIndex - 1; + } + } + finally + { + // If this was the top level submission doing the waiting, we are done with this submission and it's + // main thread building context + if (!recursive) + { + // Set the event indicating the legacy thread is no longer being used, so it is safe to clean up. + threadingData.SignalLegacyThreadEnd(submissionId); + } + } + } + } + + /// + /// Class factory for component creation. + /// + internal static IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.RequestBuilder, "Cannot create components of type {0}", type); + return new RequestBuilder(); + } + + /// + /// Starts the thread used to build + /// + private void StartBuilderThread() + { + ErrorUtilities.VerifyThrow(_requestTask == null, "Already have a task."); + + _cancellationTokenSource = new CancellationTokenSource(); + + // IMPLEMENTATION NOTE: It may look strange that we are creating new tasks here which immediately turn around and create + // more tasks that look async. The reason for this is that while these methods are technically async, they really only + // unwind at very specific times according to the needs of MSBuild, in particular when we are waiting for results from + // another project or when we are Yielding the Build Engine while running certain tasks. Essentially, the Request Builder + // and related components form a giant state machine and the tasks are used to implement one very deep co-routine. + if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry)) + { + // Create a task which completes when the legacy threading task thread is finished. + _componentHost.LegacyThreadingData.SignalLegacyThreadStart(this); + + _requestTask = Task.Factory.StartNew( + () => + { + // If this is a very quick-running request, it is possible that the request will have built and completed in + // the time between when StartBuilderThread is called, and when the threadpool gets around to actually servicing + // this request. If that's the case, it's also possible that ShutdownComponent() could have already been called, + // in which case the componentHost will be null. + + // In that circumstance, by definition we don't have anyone who will want to wait on the LegacyThreadInactiveEvent + // task, so we can safely just return. Take a snapshot so that we don't fall victim to componentHost being set + // to null between the null check and asking the LegacyThreadingData for the Task. + IBuildComponentHost componentHostSnapshot = _componentHost; + + if (componentHostSnapshot != null && componentHostSnapshot.LegacyThreadingData != null) + { + return componentHostSnapshot.LegacyThreadingData.GetLegacyThreadInactiveTask(_requestEntry.Request.SubmissionId); + } + else + { + return Task.FromResult(null); + } + }, + _cancellationTokenSource.Token, + TaskCreationOptions.None, + TaskScheduler.Default).Unwrap(); + } + else + { + ErrorUtilities.VerifyThrow(_componentHost.LegacyThreadingData.MainThreadSubmissionId != _requestEntry.Request.SubmissionId, "Can't start builder thread when we are using legacy threading semantics for this request."); + + // We do not run in STA by default. Most code does not + // require the STA apartment and the .Net default is to + // create threads with MTA semantics. We provide this + // switch so that those few tasks which may require it + // can be made to work. + if (Environment.GetEnvironmentVariable("MSBUILDFORCESTA") == "1") + { + _requestTask = Task.Factory.StartNew( + () => + { + return this.RequestThreadProc(setThreadParameters: true); + }, + _cancellationTokenSource.Token, + TaskCreationOptions.None, + AwaitExtensions.OneSTAThreadPerTaskSchedulerInstance).Unwrap(); + } + else + { + // Start up the request thread. When it starts it will begin building our current entry. + _requestTask = Task.Factory.StartNew( + () => + { + return this.RequestThreadProc(setThreadParameters: true); + }, + _cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default).Unwrap(); + } + } + } + + /// + /// Set some parameters common to all worker threads we use + /// + private void SetCommonWorkerThreadParameters() + { + Thread.CurrentThread.CurrentCulture = _componentHost.BuildParameters.Culture; + Thread.CurrentThread.CurrentUICulture = _componentHost.BuildParameters.UICulture; + Thread.CurrentThread.Priority = _componentHost.BuildParameters.BuildThreadPriority; + Thread.CurrentThread.IsBackground = true; + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + // NOTE: This is safe to do because the STA scheduler always gives us our own new thread. + Thread.CurrentThread.Name = "RequestBuilder STA thread"; + } + else + { + // NOTE: This is safe to do because we have specified long-running so we get our own new thread. + Thread.CurrentThread.Name = "RequestBuilder thread"; + } + } + + /// + /// Asserts that the entry is in the ready state. + /// + private void VerifyEntryInReadyState() + { + ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Ready, "Entry is not in the Ready state, it is in the {0} state.", _requestEntry.State); + } + + /// + /// Asserts that the entry is in the active state. + /// + private void VerifyEntryInActiveState() + { + ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active, "Entry is not in the Active state, it is in the {0} state.", _requestEntry.State); + } + + /// + /// The entry point for the request builder thread. + /// + private async Task RequestThreadProc(bool setThreadParameters) + { + try + { + if (setThreadParameters) + { + SetCommonWorkerThreadParameters(); + } +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildEngineBuildProjectBegin, CodeMarkerEvent.perfMSBuildEngineBuildProjectEnd)) + { +#if MSBUILDENABLEVSPROFILING + try + { + string beginProjectBuild = String.Format(CultureInfo.CurrentCulture, "Build Project {0} - Start", requestEntry.RequestConfiguration.ProjectFullPath); + DataCollection.CommentMarkProfile(8802, beginProjectBuild); +#endif +#endif + await BuildAndReport(); +#if (!STANDALONEBUILD) +#if MSBUILDENABLEVSPROFILING + } + finally + { + DataCollection.CommentMarkProfile(8803, "Build Project - End"); + } +#endif + } +#endif + } + catch (ThreadAbortException) + { + // Do nothing. This will happen when the thread is forcibly terminated because we are shutting down, for example + // when the unit test framework terminates. + throw; + } + catch (Exception e) + { + // Dump all engine exceptions to a temp file + // so that we have something to go on in the + // event of a failure + ExceptionHandling.DumpExceptionToFile(e); + + // This is fatal: process will terminate: make sure the + // debugger launches + ErrorUtilities.ThrowInternalError(e.Message, e); + throw; + } + } + + /// + /// Launch the project and gather the results, reporting them back to the BuildRequestEngine. + /// + private async Task BuildAndReport() + { + Exception thrownException = null; + BuildResult result = null; + VerifyEntryInActiveState(); + + // Start the build request + try + { + result = await BuildProject(); + } + catch (InvalidProjectFileException ex) + { + if (null != _projectLoggingContext) + { + _projectLoggingContext.LogInvalidProjectFileError(ex); + } + else + { + _nodeLoggingContext.LogInvalidProjectFileError(ex); + } + + thrownException = ex; + } + catch (LoggerException ex) + { + // Polite logger failure + thrownException = ex; + } + catch (InternalLoggerException ex) + { + // Logger threw arbitrary exception + thrownException = ex; + } + catch (Exception ex) + { + thrownException = ex; + + if (ExceptionHandling.IsCriticalException(ex)) + { + // This includes InternalErrorException, which we definitely want a callstack for. + // Fortunately the default console UnhandledExceptionHandler will log the callstack even + // for unhandled exceptions thrown from threads other than the main thread, like here. + // Less fortunately NUnit doesn't. + throw; + } + } + finally + { + _blockType = BlockType.Unblocked; + + if (null != thrownException) + { + ErrorUtilities.VerifyThrow(result == null, "Result already set when exception was thrown."); + result = new BuildResult(_requestEntry.Request, thrownException); + } + + ReportResultAndCleanUp(result); + } + + return; + } + + /// + /// Reports this result to the engine and cleans up. + /// + private void ReportResultAndCleanUp(BuildResult result) + { + if (null != _projectLoggingContext) + { + try + { + _projectLoggingContext.LogProjectFinished(result.OverallResult == BuildResultCode.Success); + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + if (result.Exception == null) + { + result.Exception = ex; + } + } + } + + // Clear out our state now in case any of these callbacks cause the engine to try and immediately + // reuse this builder. + BuildRequestEntry entryToComplete = _requestEntry; + _nodeLoggingContext = null; + _requestEntry = null; + if (_targetBuilder != null) + { + ((IBuildComponent)_targetBuilder).ShutdownComponent(); + } + + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + entryToComplete.RequestConfiguration.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + entryToComplete.RequestConfiguration.SavedEnvironmentVariables = CommunicationsUtilities.GetEnvironmentVariables(); + } + + entryToComplete.Complete(result); + RaiseBuildRequestCompleted(entryToComplete); + } + + /// + /// This is called back when this request needs to issue new requests and possible wait on them. This method will + /// block the builder's thread if any of the requests require us to wait for their results. + /// + /// The list of build requests to be built. + /// The results, or null if the build should terminate. + private async Task StartNewBuildRequests(FullyQualifiedBuildRequest[] requests) + { + // Determine if we need to wait for results from any of these requests. + // UNDONE: Currently we never set ResultsNeeded to anything but true. The purpose of this flag would be + // to issue another top-level build request which no other request depends on, but which must finish in order for + // the build to be considered complete. This would be brand new semantics. + bool waitForResults = false; + foreach (FullyQualifiedBuildRequest request in requests) + { + if (request.ResultsNeeded) + { + waitForResults = true; + break; + } + } + + _blockType = BlockType.BlockedOnChildRequests; + + // Save the current operating environment, if necessary + if (waitForResults) + { + SaveOperatingEnvironment(); + } + + // Issue the requests to the engine + RaiseOnNewBuildRequests(requests); + + // TODO: OPTIMIZATION: By returning null here, we commit to having to unwind the stack all the + // way back to RequestThreadProc and then shutting down the thread before we can receive the + // results and continue with them. It is not always the case that this will be desirable, however, + // particularly if the results we need are immediately available. In those cases, it would be + // useful to wait here for a short period in case those results become available - one second + // might be enough. This means we may occasionally get more than one builder thread lying around + // waiting for something to happen, but that would be short lived. At the same time it would + // allow these already-available results to be utilized immediately without the unwind + // semantics. + + // Now wait for results if we are supposed to. + BuildResult[] results; + if (waitForResults) + { + WaitHandle[] handles = new WaitHandle[] { _terminateEvent, _continueEvent }; + + int handle; + if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry)) + { + handle = RequestBuilder.WaitWithBuilderThreadStart(handles, true, _componentHost.LegacyThreadingData, _requestEntry.Request.SubmissionId); + } + else if (_inMSBuildCallback) + { + CultureInfo savedCulture = CultureInfo.CurrentCulture; + CultureInfo savedUICulture = CultureInfo.CurrentUICulture; + + handle = await handles.ToTask(); + + Thread.CurrentThread.CurrentCulture = savedCulture; + Thread.CurrentThread.CurrentUICulture = savedUICulture; + } + else + { + handle = WaitHandle.WaitAny(handles); + } + + // If this is not a shutdown case, then the entry should be in the active state. + if (handle == 1) + { + // Restore the operating environment. + RestoreOperatingEnvironment(); + VerifyEntryInActiveState(); + } + + results = GetResultsForContinuation(requests, handle == 1); + } + else + { + results = new BuildResult[] { }; + } + + ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests"); + + _blockType = BlockType.Unblocked; + return results; + } + + /// + /// Gets the results uses to continue the current build request. + /// + private BuildResult[] GetResultsForContinuation(FullyQualifiedBuildRequest[] requests, bool isContinue) + { + IDictionary results; + results = _continueResults; + _continueResults = null; + if (results == null) + { + // This catches the case where an MSBuild call is making a series of non-parallel build requests after Cancel has been + // invoked. In this case, the wait above will immediately fall through and there will be no results. We just need to be + // sure that we return a complete set of results which indicate we are aborting. + ErrorUtilities.VerifyThrow(!isContinue, "Unexpected null results during continue"); + results = new Dictionary(); + for (int i = 0; i < requests.Length; i++) + { + results[i] = new BuildResult(new BuildRequest(), new BuildAbortedException()); + } + } + + // See if we got any exceptions we should throw. + foreach (BuildResult result in results.Values) + { + if (result.CircularDependency) + { + throw new CircularDependencyException(); + } + } + + // The build results will have node request IDs in the same order as the requests were issued, + // which is in the array order above. + List resultsList = new List(results.Values); + resultsList.Sort(delegate (BuildResult left, BuildResult right) + { + if (left.NodeRequestId < right.NodeRequestId) + { + return -1; + } + else if (left.NodeRequestId == right.NodeRequestId) + { + return 0; + } + + return 1; + }); + + return resultsList.ToArray(); + } + + /// + /// Invokes the OnNewBuildRequests event + /// + /// The requests to be fulfilled. + private void RaiseOnNewBuildRequests(FullyQualifiedBuildRequest[] requests) + { + NewBuildRequestsDelegate newRequestDelegate = OnNewBuildRequests; + + if (null != newRequestDelegate) + { + newRequestDelegate(_requestEntry, requests); + } + } + + /// + /// Invokes the OnBuildRequestCompleted event + /// + private void RaiseBuildRequestCompleted(BuildRequestEntry entryToComplete) + { + BuildRequestCompletedDelegate completeRequestDelegate = OnBuildRequestCompleted; + + if (null != completeRequestDelegate) + { + completeRequestDelegate(entryToComplete); + } + } + + /// + /// Invokes the OnBlockedRequest event + /// + private void RaiseOnBlockedRequest(int blockingGlobalRequestId, string blockingTarget) + { + BuildRequestBlockedDelegate blockedRequestDelegate = OnBuildRequestBlocked; + + if (null != blockedRequestDelegate) + { + blockedRequestDelegate(_requestEntry, blockingGlobalRequestId, blockingTarget); + } + } + + /// + /// This method is called to reset the current directory to the one appropriate for this project. It should be called any time + /// the project is resumed. + /// If the directory does not exist, does nothing. + /// This is because if the project has not been saved, this directory may not exist, yet it is often useful to still be able to build the project. + /// No errors are masked by doing this: errors loading the project from disk are reported at load time, if necessary. + /// + private void SetProjectCurrentDirectory() + { + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + NativeMethodsShared.SetCurrentDirectory(_requestEntry.ProjectRootDirectory); + } + } + + /// + /// Kicks off the build of the project file. Doesn't return until the build is complete (or aborted.) + /// + private async Task BuildProject() + { + ErrorUtilities.VerifyThrow(_targetBuilder != null, "Target builder is null"); + + // Make sure it is null before loading the configuration into the request, because if there is a problem + // we do not wand to have an invalid projectLoggingContext floating around. Also if this is null the error will be + // logged with the node logging context + _projectLoggingContext = null; + + try + { + // Load the project + if (!_requestEntry.RequestConfiguration.IsLoaded) + { + LoadProjectIntoConfiguration(); + } + } + catch + { + // make sure that any errors thrown by a child project are logged in the context of their parent project: create a temporary projectLoggingContext + _projectLoggingContext = new ProjectLoggingContext + ( + _nodeLoggingContext, + _requestEntry.Request, + _requestEntry.RequestConfiguration.ProjectFullPath, + _requestEntry.RequestConfiguration.ToolsVersion, + _requestEntry.Request.ParentBuildEventContext + ); + + throw; + } + + _projectLoggingContext = _nodeLoggingContext.LogProjectStarted(_requestEntry); + + // See comment on ProjectItemInstance.Initialize for full details + // We have been asked to build with a tools verison that we don't know about + // so we'll report that we're building as if the project had been marked with a known toolsversion instead + _requestEntry.RequestConfiguration.RetrieveFromCache(); + if (_requestEntry.RequestConfiguration.Project.UsingDifferentToolsVersionFromProjectFile) + { + _projectLoggingContext.LogComment(MessageImportance.Low, "UsingDifferentToolsVersionFromProjectFile", _requestEntry.RequestConfiguration.Project.OriginalProjectToolsVersion, _requestEntry.RequestConfiguration.Project.ToolsVersion); + } + + _requestEntry.Request.BuildEventContext = _projectLoggingContext.BuildEventContext; + + // Determine the set of targets we need to build + string[] allTargets = _requestEntry.RequestConfiguration.GetTargetsUsedToBuildRequest(_requestEntry.Request).ToArray(); + + ProjectErrorUtilities.VerifyThrowInvalidProject(allTargets.Length > 0, _requestEntry.RequestConfiguration.Project.ProjectFileLocation, "NoTargetSpecified"); + + // Set the current directory to that required by the project. + SetProjectCurrentDirectory(); + + // Transfer results and state from the previous node, if necessary. + // In order for the check for target completeness for this project to be valid, all of the target results from the project must be present + // in the results cache. It is possible that this project has been moved from its original node and when it was its results did not come + // with it. This would be signified by the ResultsNode value in the configuration pointing to a different node than the current one. In that + // case we will need to request those results be moved from their original node to this one. + if ((_requestEntry.RequestConfiguration.ResultsNodeId != Scheduler.InvalidNodeId) && + (_requestEntry.RequestConfiguration.ResultsNodeId != _componentHost.BuildParameters.NodeId)) + { + // This indicates to the system that we will block waiting for a results transfer. We will block here until those results become available. + await BlockOnTargetInProgress(Microsoft.Build.BackEnd.BuildRequest.InvalidGlobalRequestId, null); + + // All of the results should now be on this node. + ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.ResultsNodeId == _componentHost.BuildParameters.NodeId, "Results for configuration {0} were not retrieved from node {1}", _requestEntry.RequestConfiguration.ConfigurationId, _requestEntry.RequestConfiguration.ResultsNodeId); + } + + // Build the targets + BuildResult result = await _targetBuilder.BuildTargets(_projectLoggingContext, _requestEntry, this, allTargets, _requestEntry.RequestConfiguration.BaseLookup, _cancellationTokenSource.Token); + return result; + } + + /// + /// Loads the project specified by the configuration's parameters into the configuration block. + /// + private void LoadProjectIntoConfiguration() + { + ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.Project == null, "We've already loaded the project for this configuration id {0}.", _requestEntry.RequestConfiguration.ConfigurationId); + + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + try + { + NativeMethodsShared.SetCurrentDirectory(BuildParameters.StartupDirectory); + } + catch (DirectoryNotFoundException) + { + // Somehow the startup directory vanished. This can happen if build was started from a USB Key and it was removed. + NativeMethodsShared.SetCurrentDirectory(Environment.SystemDirectory); + } + } + + Dictionary globalProperties = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectPropertyInstance property in _requestEntry.RequestConfiguration.Properties) + { + globalProperties.Add(property.Name, ((IProperty)property).EvaluatedValueEscaped); + } + + string toolsVersionOverride = _requestEntry.RequestConfiguration.ExplicitToolsVersionSpecified ? _requestEntry.RequestConfiguration.ToolsVersion : null; + + _requestEntry.RequestConfiguration.Project = new ProjectInstance(_requestEntry.RequestConfiguration.ProjectFullPath, globalProperties, toolsVersionOverride, _componentHost.BuildParameters, _nodeLoggingContext.LoggingService, _requestEntry.Request.BuildEventContext); + } + + /// + /// Saves the current operating environment. + /// + private void SaveOperatingEnvironment() + { + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + _requestEntry.RequestConfiguration.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + _requestEntry.RequestConfiguration.SavedEnvironmentVariables = CommunicationsUtilities.GetEnvironmentVariables(); + } + } + + /// + /// Sets the operationg environment to the initial build environment. + /// + private void InitializeOperatingEnvironment() + { + if (_requestEntry.RequestConfiguration.SavedEnvironmentVariables != null && _componentHost.BuildParameters.SaveOperatingEnvironment) + { + // Restore the saved environment variables. + SetEnvironmentVariableBlock(_requestEntry.RequestConfiguration.SavedEnvironmentVariables); + } + else + { + // Restore the original build environment variables. + SetEnvironmentVariableBlock(_componentHost.BuildParameters.BuildProcessEnvironment); + } + } + + /// + /// Restores a previously saved operating environment. + /// + private void RestoreOperatingEnvironment() + { + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.SavedCurrentDirectory != null, "Current directory not previously saved."); + ErrorUtilities.VerifyThrow(_requestEntry.RequestConfiguration.SavedEnvironmentVariables != null, "Current environment not previously saved."); + + // Restore the saved environment variables. + SetEnvironmentVariableBlock(_requestEntry.RequestConfiguration.SavedEnvironmentVariables); + NativeMethodsShared.SetCurrentDirectory(_requestEntry.RequestConfiguration.SavedCurrentDirectory); + } + } + + /// + /// Sets the environment block to the set of saved variables. + /// + private void SetEnvironmentVariableBlock(IDictionary savedEnvironment) + { + IDictionary currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + ClearVariablesNotInEnvironment(savedEnvironment, currentEnvironment); + UpdateEnvironmentVariables(savedEnvironment, currentEnvironment); + } + + /// + /// Clears from the current environment any variables which do not exist in the saved environment + /// + private void ClearVariablesNotInEnvironment(IDictionary savedEnvironment, IDictionary currentEnvironment) + { + foreach (KeyValuePair entry in currentEnvironment) + { + if (!savedEnvironment.ContainsKey(entry.Key)) + { + Environment.SetEnvironmentVariable(entry.Key, null); + } + } + } + + /// + /// Updates the current environment with values in the saved environment which differ or are not yet set. + /// + private void UpdateEnvironmentVariables(IDictionary savedEnvironment, IDictionary currentEnvironment) + { + foreach (KeyValuePair entry in savedEnvironment) + { + // If the environment doesn't have the variable set, or if its value differs from what we have saved, set it + // to the saved value. Doing the comparison before setting is faster than unconditionally setting it using + // the API. + string value; + if (!currentEnvironment.TryGetValue(entry.Key, out value) || !String.Equals(entry.Value, value, StringComparison.Ordinal)) + { + Environment.SetEnvironmentVariable(entry.Key, entry.Value); + } + } + } + + /// + /// Throws if the RequestBuilder has been zombied. + /// + private void VerifyIsNotZombie() + { + ErrorUtilities.VerifyThrow(!_isZombie, "RequestBuilder has been zombied."); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetBuilder.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetBuilder.cs new file mode 100644 index 00000000000..b1514a05244 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetBuilder.cs @@ -0,0 +1,732 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The standard implementation of ITargetBuilder. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Text; +using System.Threading; +using System.Xml; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext; +using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; +using System.Threading.Tasks; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The Target Builder is responsible for building a single target within a given project. + /// + /// + /// The Target Builder is a stack machine which builds project targets. Each time a target needs to be built, it is + /// pushed onto the stack. The main loop for the Target Builder simply evaluates the top item on the stack to determine + /// which action to take. These actions comprise the target state machine, as represented by the states of the + /// TargetEntry object. + /// + /// When a target completes, all of its outputs are available in the Lookup contained in the TargetEntry. In fact, everything that it changed + /// in the global state is available by virtue of its Lookup being merged with the current Target's lookup. + /// + /// For CallTarget tasks, this behavior is not the same. Rather the Lookup from a CallTarget call does not get merged until the calling + /// Target has completed. This is considered erroneous behavior and 'normal' version of CallTarget will be implemented which does not exhibit + /// this. + /// + internal class TargetBuilder : ITargetBuilder, ITargetBuilderCallback, IBuildComponent + { + /// + /// The cancellation token. + /// + private CancellationToken _cancellationToken; + + /// + /// The current stack of targets and dependents. The top-most entry on the stack is the target + /// currently being built. + /// + private ConcurrentStack _targetsToBuild; + + /// + /// The component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// The BuildRequestEntry for which we are building targets. + /// + private BuildRequestEntry _requestEntry; + + /// + /// The lookup representing the project's state. + /// + private Lookup _baseLookup; + + /// + /// The callback interface used to invoke new project builds. + /// + private IRequestBuilderCallback _requestBuilderCallback; + + /// + /// The project logging context + /// + private ProjectLoggingContext _projectLoggingContext; + + /// + /// The aggregate build result from running the targets + /// + private BuildResult _buildResult; + + /// + /// The project instance we are building + /// + private ProjectInstance _projectInstance; + + /// + /// Flag indicating whether we are under the influence of the legacy CallTarget's ContinueOnError behavior. + /// + private bool _legacyCallTargetContinueOnError; + + /// + /// Enum describing the type of targets we are pushing on the stack. + /// + private enum TargetPushType + { + /// + /// We are pushing BeforeTargets. When pushed, if these are already executing, we ignore them. + /// + BeforeTargets, + + /// + /// We are pushing AfterTargets. When pushed, if they have already executed, we ignore them. + /// + AfterTargets, + + /// + /// We are pushing normal targets. We never ignore them. + /// + Normal + } + + /// + /// Builds the specified targets. + /// + /// The logging context for the project. + /// The BuildRequestEntry for which we are building targets. + /// The callback to be used to handle new project build requests. + /// The names of the targets to build. + /// The Lookup containing all current items and properties for this target. + /// The target's outputs and result codes + public async Task BuildTargets(ProjectLoggingContext loggingContext, BuildRequestEntry entry, IRequestBuilderCallback callback, string[] targetNames, Lookup baseLookup, CancellationToken cancellationToken) + { + ErrorUtilities.VerifyThrowArgumentNull(loggingContext, "projectLoggingContext"); + ErrorUtilities.VerifyThrowArgumentNull(entry, "entry"); + ErrorUtilities.VerifyThrowArgumentNull(callback, "requestBuilderCallback"); + ErrorUtilities.VerifyThrowArgumentNull(targetNames, "targetNames"); + ErrorUtilities.VerifyThrowArgumentNull(baseLookup, "baseLookup"); + ErrorUtilities.VerifyThrow(targetNames.Length > 0, "List of targets must be non-empty"); + ErrorUtilities.VerifyThrow(_componentHost != null, "InitializeComponent must be called before building targets."); + + _requestEntry = entry; + _requestBuilderCallback = callback; + _projectLoggingContext = loggingContext; + _cancellationToken = cancellationToken; + + // Clone the base lookup so that if we are re-entered by another request while this one in blocked, we don't have visibility to + // their state, and they have no visibility into ours. + _baseLookup = baseLookup.Clone(); + + _targetsToBuild = new ConcurrentStack(); + + // Get the actual target objects from the names + BuildRequestConfiguration configuration = _requestEntry.RequestConfiguration; + + bool previousCacheableStatus = configuration.IsCacheable; + configuration.IsCacheable = false; + configuration.RetrieveFromCache(); + _projectInstance = configuration.Project; + + // Now get the current results cache entry. + ResultsCache resultsCache = (ResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache); + BuildResult existingBuildResult = null; + resultsCache.ResultsDictionary.TryGetValue(_requestEntry.Request.ConfigurationId, out existingBuildResult); + + _buildResult = new BuildResult(entry.Request, existingBuildResult, null); + + if (existingBuildResult == null) + { + // Add this result so that if our project gets re-entered we won't rebuild any targets we have already built. + resultsCache.AddResult(_buildResult); + } + + List targets = new List(targetNames.Length); + + foreach (string targetName in targetNames) + { + targets.Add(new TargetSpecification(targetName, _projectInstance.Targets.ContainsKey(targetName) ? _projectInstance.Targets[targetName].Location : _projectInstance.ProjectFileLocation)); + } + + // Push targets onto the stack. This method will reverse their push order so that they + // get built in the same order specified in the array. + await PushTargets(targets, null, baseLookup, false, false, TargetPushType.Normal); + + // Now process the targets + ITaskBuilder taskBuilder = _componentHost.GetComponent(BuildComponentType.TaskBuilder) as ITaskBuilder; + try + { + await ProcessTargetStack(taskBuilder); + } + finally + { + // If there are still targets left on the stack, they need to be removed from the 'active targets' list + foreach (TargetEntry target in _targetsToBuild) + { + configuration.ActivelyBuildingTargets.Remove(target.Name); + } + + ((IBuildComponent)taskBuilder).ShutdownComponent(); + } + + if (_cancellationToken.IsCancellationRequested) + { + throw new BuildAbortedException(); + } + + // Gather up outputs for the requested targets and return those. All of our information should be in the base lookup now. + BuildResult resultsToReport = new BuildResult(_buildResult, targetNames); + + // Return after-build project state if requested. + if (_requestEntry.Request.BuildRequestDataFlags.HasFlag(BuildRequestDataFlags.ProvideProjectStateAfterBuild)) + { + resultsToReport.ProjectStateAfterBuild = _projectInstance; + } + + configuration.IsCacheable = previousCacheableStatus; + + return resultsToReport; + } + + #region IBuildComponent Members + + /// + /// Sets the component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + _componentHost = host; + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + _componentHost = null; + } + + #endregion + + #region ITargetBuilderCallback Members + + /// + /// Invokes the specified targets using Dev9 behavior. + /// + /// The targets to build. + /// True to continue building the remaining targets if one fails. + /// The results for each target. + /// + /// Dev9 behavior refers to the following: + /// 1. The changes made during the calling target up to this point are NOT visible to this target. + /// 2. The changes made by this target are NOT visible to the calling target. + /// 3. Changes made by the calling target OVERRIDE changes made by this target. + /// + async Task ITargetBuilderCallback.LegacyCallTarget(string[] targets, bool continueOnError, ElementLocation taskLocation) + { + List targetToPush = new List(); + ITargetResult[] results = new TargetResult[targets.Length]; + bool originalLegacyCallTargetContinueOnError = _legacyCallTargetContinueOnError; + + _legacyCallTargetContinueOnError = _legacyCallTargetContinueOnError || continueOnError; + + // Our lookup is the one used at the beginning of the calling target. + Lookup callTargetLookup = _baseLookup; + + // We now record this lookup in the calling target's entry so that it may + // leave the scope just before it commits its own changes to the base lookup. + TargetEntry currentTargetEntry = _targetsToBuild.Peek(); + currentTargetEntry.EnterLegacyCallTargetScope(callTargetLookup); + + ITaskBuilder taskBuilder = _componentHost.GetComponent(BuildComponentType.TaskBuilder) as ITaskBuilder; + try + { + // Flag set to true if one of the targets we call fails. + bool errorResult = false; + + // Now walk through the list of targets, invoking each one. + for (int i = 0; i < targets.Length; i++) + { + if (_cancellationToken.IsCancellationRequested || errorResult) + { + results[i] = new TargetResult(new TaskItem[] { }, new WorkUnitResult(WorkUnitResultCode.Skipped, WorkUnitActionCode.Continue, null)); + } + else + { + targetToPush.Clear(); + targetToPush.Add(new TargetSpecification(targets[i], taskLocation)); + + // We push the targets one at a time to emulate the original CallTarget behavior. + bool pushed = await PushTargets(targetToPush, currentTargetEntry, callTargetLookup, false, true, TargetPushType.Normal); + ErrorUtilities.VerifyThrow(pushed, "Failed to push any targets onto the stack. Target: {0} Current Target: {1}", targets[i], currentTargetEntry.Target.Name); + await ProcessTargetStack(taskBuilder); + + if (!_cancellationToken.IsCancellationRequested) + { + results[i] = _buildResult[targets[i]]; + if (results[i].ResultCode == TargetResultCode.Failure) + { + errorResult = true; + } + } + else + { + results[i] = new TargetResult(new TaskItem[] { }, new WorkUnitResult(WorkUnitResultCode.Skipped, WorkUnitActionCode.Continue, null)); + } + } + } + } + finally + { + // Restore the state of the TargetBuilder to that it was prior to the CallTarget call. + // Any targets we have pushed on at this point we need to get rid of since we aren't going to process them. + // If there were normal task errors, standard error handling semantics would have taken care of them. + // If there was an exception, such as a circular dependency error, items may still be on the stack so we must clear them. + while (!Object.ReferenceEquals(_targetsToBuild.Peek(), currentTargetEntry)) + { + _targetsToBuild.Pop(); + } + + _legacyCallTargetContinueOnError = originalLegacyCallTargetContinueOnError; + ((IBuildComponent)taskBuilder).ShutdownComponent(); + } + + return results; + } + + #endregion + + #region IRequestBuilderCallback Members + + /// + /// Forwarding implementation of BuildProjects + /// + async Task IRequestBuilderCallback.BuildProjects(string[] projectFiles, Microsoft.Build.Collections.PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults) + { + return await _requestBuilderCallback.BuildProjects(projectFiles, properties, toolsVersions, targets, waitForResults); + } + + /// + /// Required for interface - this should never be called. + /// + Task IRequestBuilderCallback.BlockOnTargetInProgress(int blockingGlobalBuildRequestId, string blockingTarget) + { + ErrorUtilities.ThrowInternalError("This method should never be called by anyone except the TargetBuilder."); + return Task.FromResult(false); + } + + /// + /// Yields the node. + /// + void IRequestBuilderCallback.Yield() + { + _requestBuilderCallback.Yield(); + } + + /// + /// Reacquires the node. + /// + void IRequestBuilderCallback.Reacquire() + { + _requestBuilderCallback.Reacquire(); + } + + /// + /// Enters the MSBuild callback state for asynchronous processing of referenced projects. + /// + void IRequestBuilderCallback.EnterMSBuildCallbackState() + { + _requestBuilderCallback.EnterMSBuildCallbackState(); + } + + /// + /// Exits the MSBuild callback state. + /// + void IRequestBuilderCallback.ExitMSBuildCallbackState() + { + _requestBuilderCallback.ExitMSBuildCallbackState(); + } + + #endregion + + /// + /// Class factory for component creation. + /// + internal static IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.TargetBuilder, "Cannot create components of type {0}", type); + return new TargetBuilder(); + } + + /// + /// Processes the target stack until its empty or we hit a recursive break (due to CallTarget etc.) + /// + private async Task ProcessTargetStack(ITaskBuilder taskBuilder) + { + // Keep building while we have targets to build and haven't been cancelled. + bool stopProcessingStack = false; + while + ( + !_cancellationToken.IsCancellationRequested && + !stopProcessingStack && + !_targetsToBuild.IsEmpty + ) + { + TargetEntry currentTargetEntry = _targetsToBuild.Peek(); + switch (currentTargetEntry.State) + { + case TargetEntryState.Dependencies: + // Ensure we are dealing with a target which actually exists. + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + _requestEntry.RequestConfiguration.Project.Targets.ContainsKey(currentTargetEntry.Name), + currentTargetEntry.ReferenceLocation, + "TargetDoesNotExist", + currentTargetEntry.Name + ); + + // If we already have results for this target which were not skipped, we can ignore it. In + // addition, we can also ignore its before and after targets -- if this target has already run, + // then so have they. + if (!CheckSkipTarget(ref stopProcessingStack, currentTargetEntry)) + { + // Temporarily remove this entry so we can push our after targets + _targetsToBuild.Pop(); + + // Push our after targets, if any. Our parent is the parent of the target after which we are running. + IList afterTargets = _requestEntry.RequestConfiguration.Project.GetTargetsWhichRunAfter(currentTargetEntry.Name); + bool didPushTargets = await PushTargets(afterTargets, currentTargetEntry.ParentEntry, currentTargetEntry.Lookup, currentTargetEntry.ErrorTarget, currentTargetEntry.StopProcessingOnCompletion, TargetPushType.AfterTargets); + + // If we have after targets, the last one to run will inherit the stopProcessing flag and we will reset ours. If we didn't push any targets, then we shouldn't clear the + // flag because it means we are still on the bottom of this CallTarget stack. + if ((afterTargets.Count != 0) && didPushTargets) + { + currentTargetEntry.StopProcessingOnCompletion = false; + } + + // Put us back on the stack + _targetsToBuild.Push(currentTargetEntry); + + // Determine which targets are dependencies. This will also test to see if the target should be skipped due to condition. + // If it is determined the target should skip, the dependency list returned will be empty. + IList dependencies = currentTargetEntry.GetDependencies(_projectLoggingContext); + + // Push our before targets now, unconditionally. If we have marked that we should stop processing the stack here, which can only + // happen if our current target was supposed to stop processing AND we had no after targets, then our last before target should + // inherit the stop processing flag and we will reset it. + // Our parent is the target before which we run, just like a depends-on target. + IList beforeTargets = _requestEntry.RequestConfiguration.Project.GetTargetsWhichRunBefore(currentTargetEntry.Name); + bool pushedTargets = await PushTargets(beforeTargets, currentTargetEntry, currentTargetEntry.Lookup, currentTargetEntry.ErrorTarget, stopProcessingStack, TargetPushType.BeforeTargets); + if (beforeTargets.Count != 0 && pushedTargets) + { + stopProcessingStack = false; + } + + // And if we have dependencies to run, push them now. + if (null != dependencies) + { + await PushTargets(dependencies, currentTargetEntry, currentTargetEntry.Lookup, false, false, TargetPushType.Normal); + } + } + + break; + + case TargetEntryState.Execution: + + // It's possible that our target got pushed onto the stack for one build and had its dependencies process, then a re-entrant build came in and + // actually built this target while we were waiting, so that by the time we get here, it's already been finished. In this case, just blow it away. + if (!CheckSkipTarget(ref stopProcessingStack, currentTargetEntry)) + { + // This target is now actively building. + _requestEntry.RequestConfiguration.ActivelyBuildingTargets.Add(currentTargetEntry.Name, _requestEntry.Request.GlobalRequestId); + + // Execute all of the tasks on this target. + await currentTargetEntry.ExecuteTarget(taskBuilder, _requestEntry, _projectLoggingContext, _cancellationToken); + } + + break; + + case TargetEntryState.ErrorExecution: + // Push the error targets onto the stack. This target will now be marked as completed. + // When that state is processed, it will mark its parent for error execution + List errorTargets = currentTargetEntry.GetErrorTargets(_projectLoggingContext); + try + { + await PushTargets(errorTargets, currentTargetEntry, currentTargetEntry.Lookup, true, false, TargetPushType.Normal); + } + catch + { + if (_requestEntry.RequestConfiguration.ActivelyBuildingTargets.ContainsKey(currentTargetEntry.Name)) + { + _requestEntry.RequestConfiguration.ActivelyBuildingTargets.Remove(currentTargetEntry.Name); + } + + throw; + } + + break; + + case TargetEntryState.Completed: + // The target is complete, we can gather up the results and remove this target + // from the stack. + TargetResult targetResult = currentTargetEntry.GatherResults(); + + // If this result failed but we are under the influence of the legacy ContinueOnError behavior for a + // CallTarget, make sure we don't contribute this failure to the overall success of the build. + targetResult.TargetFailureDoesntCauseBuildFailure = _legacyCallTargetContinueOnError; + + // This target is no longer actively building. + _requestEntry.RequestConfiguration.ActivelyBuildingTargets.Remove(currentTargetEntry.Name); + + _buildResult.AddResultsForTarget(currentTargetEntry.Name, targetResult); + + TargetEntry topEntry = _targetsToBuild.Pop(); + if (topEntry.StopProcessingOnCompletion) + { + stopProcessingStack = true; + } + + PopDependencyTargetsOnTargetFailure(topEntry, targetResult, ref stopProcessingStack); + + break; + + default: + ErrorUtilities.ThrowInternalError("Unexpected target state {0}", currentTargetEntry.State); + break; + } + } + + return; + } + + /// + /// Determines if the current target should be skipped, and logs the appropriate message. + /// + /// True to skip the target, false otherwise. + private bool CheckSkipTarget(ref bool stopProcessingStack, TargetEntry currentTargetEntry) + { + if (_buildResult.HasResultsForTarget(currentTargetEntry.Name)) + { + TargetResult targetResult = _buildResult[currentTargetEntry.Name] as TargetResult; + ErrorUtilities.VerifyThrowInternalNull(targetResult, "targetResult"); + + if (targetResult.ResultCode != TargetResultCode.Skipped) + { + // If we've already dealt with this target and it didn't skip, let's log appropriately + // Otherwise we don't want anything more to do with it. + _projectLoggingContext.LogComment + ( + Microsoft.Build.Framework.MessageImportance.Low, + targetResult.ResultCode == TargetResultCode.Success ? "TargetAlreadyCompleteSuccess" : "TargetAlreadyCompleteFailure", + currentTargetEntry.Name + ); + + if (currentTargetEntry.StopProcessingOnCompletion) + { + stopProcessingStack = true; + } + + if (targetResult.ResultCode == TargetResultCode.Success) + { + _targetsToBuild.Peek().LeaveLegacyCallTargetScopes(); + _targetsToBuild.Pop(); + } + else + { + TargetEntry topEntry = _targetsToBuild.Pop(); + + // If this is a skip because of target failure, we should behave in the same way as we + // would if this target actually failed -- remove all its dependencies from the stack as + // well. Otherwise, we could encounter a situation where a failure target happens in the + // middle of execution once, then exits, then a request comes through to build the same + // targets, reaches that target, skips-already-failed, and then continues building. + PopDependencyTargetsOnTargetFailure(topEntry, targetResult, ref stopProcessingStack); + } + + return true; + } + } + + return false; + } + + /// + /// When a target build fails, we don't just stop building that target; we also pop all of the other dependency targets of its + /// parent target off the stack. Extract that logic into a standalone method so that it can be used when dealing with targets that + /// are skipped-unsuccessful as well as first-time failures. + /// + private void PopDependencyTargetsOnTargetFailure(TargetEntry topEntry, TargetResult targetResult, ref bool stopProcessingStack) + { + if (targetResult.WorkUnitResult.ActionCode == WorkUnitActionCode.Stop) + { + // Pop down to our parent, since any other dependencies our parent had should no longer + // execute. If we encounter an error target on the way down, also stop since the failure + // of one error target in a set declared in OnError should not cause the others to stop running. + while ((!_targetsToBuild.IsEmpty) && (_targetsToBuild.Peek() != topEntry.ParentEntry) && !_targetsToBuild.Peek().ErrorTarget) + { + TargetEntry entry = _targetsToBuild.Pop(); + entry.LeaveLegacyCallTargetScopes(); + + // This target is no longer actively building (if it was). + _requestEntry.RequestConfiguration.ActivelyBuildingTargets.Remove(topEntry.Name); + + // If we come across an entry which requires us to stop processing (for instance, an aftertarget of the original + // CallTarget target) then we need to use that flag, not the one from the top entry. + if (entry.StopProcessingOnCompletion) + { + stopProcessingStack = true; + } + } + + // Mark our parent for error execution + if (topEntry.ParentEntry != null && topEntry.ParentEntry.State != TargetEntryState.Completed) + { + topEntry.ParentEntry.MarkForError(); + } + } + } + + /// + /// Pushes the list of targets specified onto the target stack in reverse order specified, so that + /// they will be built in the order specified. + /// + /// List of targets to build. + /// The target which should be considered the parent of these targets. + /// The lookup to be used to build these targets. + /// True if this should be considered an error target. + /// True if target stack processing should terminate when the last target in the list is processed. + /// True if we actually pushed any targets, false otherwise. + private async Task PushTargets(IList targets, TargetEntry parentTargetEntry, Lookup baseLookup, bool addAsErrorTarget, bool stopProcessingOnCompletion, TargetPushType pushType) + { + List targetsToPush = new List(targets.Count); + + // Iterate the list in reverse order so that the first target in the list is the last pushed, and thus the first to be executed. + for (int i = targets.Count - 1; i >= 0; i--) + { + TargetSpecification targetSpecification = targets[i]; + + if (pushType != TargetPushType.Normal) + { + // Don't build any Before or After targets for which we already have results. Unlike other targets, + // we don't explicitly log a skipped-with-results message because it is not interesting. + if (_buildResult.HasResultsForTarget(targetSpecification.TargetName)) + { + if (_buildResult[targetSpecification.TargetName].ResultCode != TargetResultCode.Skipped) + { + continue; + } + } + } + + ElementLocation targetLocation = targetSpecification.ReferenceLocation; + + // See if this target is already building under a different build request. If so, we need to wait. + int idOfAlreadyBuildingRequest = BuildRequest.InvalidGlobalRequestId; + if (_requestEntry.RequestConfiguration.ActivelyBuildingTargets.TryGetValue(targetSpecification.TargetName, out idOfAlreadyBuildingRequest)) + { + if (idOfAlreadyBuildingRequest != _requestEntry.Request.GlobalRequestId) + { + // Another request elsewhere is building it. We need to wait. + await _requestBuilderCallback.BlockOnTargetInProgress(idOfAlreadyBuildingRequest, targetSpecification.TargetName); + + // If we come out of here and the target is *still* active, it means the scheduler detected a circular dependency and told us to + // continue so we could throw the exception. + if (_requestEntry.RequestConfiguration.ActivelyBuildingTargets.ContainsKey(targetSpecification.TargetName)) + { + ProjectErrorUtilities.ThrowInvalidProject(targetLocation, "CircularDependency", targetSpecification.TargetName); + } + } + else + { + if (pushType == TargetPushType.AfterTargets) + { + // If the target we are pushing is supposed to run after the current target and it is already set to run after us then skip adding it now. + continue; + } + + // We are already building this target on this request. That's a circular dependency. + ProjectErrorUtilities.ThrowInvalidProject(targetLocation, "CircularDependency", targetSpecification.TargetName); + } + } + else + { + // Does this target exist in our direct parent chain, if it is a before target (since these can cause circular dependency issues) + if (pushType == TargetPushType.BeforeTargets || pushType == TargetPushType.Normal) + { + TargetEntry currentParent = parentTargetEntry; + while (currentParent != null) + { + if (String.Equals(currentParent.Name, targetSpecification.TargetName, StringComparison.OrdinalIgnoreCase)) + { + // We are already building this target on this request. That's a circular dependency. + ProjectErrorUtilities.ThrowInvalidProject(targetLocation, "CircularDependency", targetSpecification.TargetName); + } + + currentParent = currentParent.ParentEntry; + } + } + else + { + // For an after target, if it is already ANYWHERE on the stack, we don't need to push it because it is already going to run + // after whatever target is causing it to be pushed now. + bool alreadyPushed = false; + foreach (TargetEntry entry in _targetsToBuild) + { + if (String.Equals(entry.Name, targetSpecification.TargetName, StringComparison.OrdinalIgnoreCase)) + { + alreadyPushed = true; + break; + } + } + + if (alreadyPushed) + { + continue; + } + } + } + + // Add to the list of targets to push. We don't actually put it on the stack here because we could run into a circular dependency + // during this loop, in which case the target stack would be out of whack. + TargetEntry newEntry = new TargetEntry(_requestEntry, this as ITargetBuilderCallback, targetSpecification, baseLookup, parentTargetEntry, _componentHost, stopProcessingOnCompletion); + newEntry.ErrorTarget = addAsErrorTarget; + targetsToPush.Add(newEntry); + stopProcessingOnCompletion = false; // The first target on the stack (the last one to be run) always inherits the stopProcessing flag. + } + + // Now push the targets since this operation cannot fail. + foreach (TargetEntry targetToPush in targetsToPush) + { + _targetsToBuild.Push(targetToPush); + } + + bool pushedTargets = (targetsToPush.Count > 0); + return pushedTargets; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetEntry.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetEntry.cs new file mode 100644 index 00000000000..8654369b42e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetEntry.cs @@ -0,0 +1,921 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An entry on the TargetBuilder's target stack. This class +// does most of the work of building an individual target. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Collections; +using Microsoft.Build.Debugging; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using MessageImportance = Microsoft.Build.Framework.MessageImportance; +using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +#if MSBUILDENABLEVSPROFILING +using Microsoft.VisualStudio.Profiler; +#endif +namespace Microsoft.Build.BackEnd +{ + /// + /// Represents which state the target entry is currently in. + /// + internal enum TargetEntryState + { + /// + /// The target's dependencies need to be evaluated and pushed onto the target stack. + /// + /// Transitions: + /// Execution, ErrorExecution + /// + Dependencies, + + /// + /// The target is ready to execute its tasks, batched as needed. + /// + /// Transitions: + /// ErrorExecution, Completed + /// + Execution, + + /// + /// The target is ready to provide error tasks. + /// + /// Transitions: + /// None + /// + ErrorExecution, + + /// + /// The target has finished building. All of the results are in the Lookup. + /// + /// Transitions: + /// None + /// + Completed + } + + /// + /// This class represents a single target in the TargetBuilder. It maintains the state machine for a particular target as well as + /// relevant information on outputs generated while a target is running. + /// + [DebuggerDisplay("Name={targetSpecification.TargetName} State={state} Result={targetResult.ResultCode}")] + internal class TargetEntry : IEquatable + { + /// + /// The BuildRequestEntry to which this target invocation belongs + /// + private BuildRequestEntry _requestEntry; + + /// + /// The specification of the target being built. + /// + private TargetSpecification _targetSpecification; + + /// + /// The Target being built. This will be null until the GetTargetInstance() is called, which + /// will cause us to attempt to resolve the actual project instance. At that point + /// if the target doesn't exist, we will throw an InvalidProjectFileException. We do this lazy + /// evaluation because the 'target doesn't exist' message is not supposed to be emitted until + /// the target is actually needed, as opposed to when it is specified, such as in an OnError + /// clause, DependsOnTargets or on the command-line. + /// + private ProjectTargetInstance _target; + + /// + /// The current state of this entry + /// + private TargetEntryState _state; + + /// + /// The completion state of the target. + /// + private TargetResult _targetResult; + + /// + /// The parent entry, which is waiting for us to complete before proceeding. + /// + private TargetEntry _parentTarget; + + /// + /// The expander used to expand item and property markup to evaluated values. + /// + private Expander _expander; + + /// + /// The lookup containing our environment. + /// + private Lookup _baseLookup; + + /// + /// The build component host. + /// + private IBuildComponentHost _host; + + /// + /// The target builder callback + /// + private ITargetBuilderCallback _targetBuilderCallback; + + /// + /// A queue of legacy CallTarget lookup scopes to leave when this target is finished. + /// + private Stack _legacyCallTargetScopes; + + /// + /// The lock taken when dealing with cancel-synchronized objects + /// + private Object _cancelLock = new Object(); + + /// + /// The cancellation token. + /// + private CancellationToken _cancellationToken; + + /// + /// Flag indicating whether we are currently executing this target. Used for assertions. + /// + private bool _isExecuting; + + /// + /// The current task builder. + /// + private ITaskBuilder _currentTaskBuilder; + + /// + /// The constructor. + /// + /// The build request entry for the target. + /// The target builder callback. + /// The specification for the target to build. + /// The lookup to use. + /// The parent of this entry, if any. + /// The Build Component Host to use. + /// True if the target builder should stop processing the current target stack when this target is complete. + internal TargetEntry(BuildRequestEntry requestEntry, ITargetBuilderCallback targetBuilderCallback, TargetSpecification targetSpecification, Lookup baseLookup, TargetEntry parentTarget, IBuildComponentHost host, bool stopProcessingOnCompletion) + { + ErrorUtilities.VerifyThrowArgumentNull(requestEntry, "requestEntry"); + ErrorUtilities.VerifyThrowArgumentNull(targetBuilderCallback, "targetBuilderCallback"); + ErrorUtilities.VerifyThrowArgumentNull(targetSpecification, "targetName"); + ErrorUtilities.VerifyThrowArgumentNull(baseLookup, "lookup"); + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + + _requestEntry = requestEntry; + _targetBuilderCallback = targetBuilderCallback; + _targetSpecification = targetSpecification; + _parentTarget = parentTarget; + _expander = new Expander(baseLookup.ReadOnlyLookup, baseLookup.ReadOnlyLookup); + _state = TargetEntryState.Dependencies; + _baseLookup = baseLookup; + _host = host; + this.StopProcessingOnCompletion = stopProcessingOnCompletion; + } + + /// + /// Gets or sets a flag indicating if this entry is the result of being listed as an error target in + /// an OnError clause. + /// + internal bool ErrorTarget + { + get; + set; + } + + /// + /// Sets or sets the location from which this target was referred. + /// + internal ElementLocation ReferenceLocation + { + get { return _targetSpecification.ReferenceLocation; } + } + + /// + /// Gets or sets a flag indicating that the target builder should stop processing the target + /// stack when this target completes. + /// + internal bool StopProcessingOnCompletion + { + get; + set; + } + + /// + /// Retrieves the name of the target. + /// + internal string Name + { + get + { + return _targetSpecification.TargetName; + } + } + + /// + /// Gets the current state of the target + /// + internal TargetEntryState State + { + get + { + return _state; + } + } + + /// + /// The result of this target. + /// + internal TargetResult Result + { + get + { + return _targetResult; + } + } + + /// + /// Retrieves the Lookup this target was initialized with, including any modifications which have + /// been made to it while running. + /// + internal Lookup Lookup + { + get + { + return _baseLookup; + } + } + + /// + /// The target contained by the entry. + /// + internal ProjectTargetInstance Target + { + get + { + if (_target == null) + { + GetTargetInstance(); + } + + return _target; + } + } + + /// + /// The build request entry to which this target belongs. + /// + internal BuildRequestEntry RequestEntry + { + get + { + return _requestEntry; + } + } + + /// + /// The target entry for which we are a dependency. + /// + internal TargetEntry ParentEntry + { + get + { + return _parentTarget; + } + } + + #region IEquatable Members + + /// + /// Determines equivalence of two target entries. They are considered the same + /// if their names are the same. + /// + /// The entry to which we compare this one. + /// True if they are equivalent, false otherwise. + public bool Equals(TargetEntry other) + { + return String.Equals(this.Name, other.Name, StringComparison.OrdinalIgnoreCase); + } + + #endregion + + /// + /// Retrieves the list of dependencies this target needs to have built and moves the target to the next state. + /// Never returns null. + /// + /// A collection of targets on which this target depends. + internal List GetDependencies(ProjectLoggingContext projectLoggingContext) + { + VerifyState(_state, TargetEntryState.Dependencies); + + // Resolve the target now, since from this point on we are going to be doing work with the actual instance. + GetTargetInstance(); + + // We first make sure no batching was attempted with the target's condition. + // UNDONE: (Improvement) We want to allow this actually. In order to do this we need to determine what the + // batching buckets are, and if there are any which aren't empty, return our list of dependencies. + // Only in the case where all bucket conditions fail do we want to skip the target entirely (and + // this skip building the dependencies.) + if (ExpressionShredder.ContainsMetadataExpressionOutsideTransform(_target.Condition)) + { + ProjectErrorUtilities.ThrowInvalidProject(_target.ConditionLocation, "TargetConditionHasInvalidMetadataReference", _target.Name, _target.Condition); + } + + // If condition is false (based on propertyBag), set this target's state to + // "Skipped" since we won't actually build it. + bool condition = ConditionEvaluator.EvaluateCondition + ( + _target.Condition, + ParserOptions.AllowPropertiesAndItemLists, + _expander, + ExpanderOptions.ExpandPropertiesAndItems, + _requestEntry.ProjectRootDirectory, + _target.ConditionLocation, + projectLoggingContext.LoggingService, + projectLoggingContext.BuildEventContext + ); + + if (!condition) + { + _targetResult = new TargetResult(new TaskItem[0] { }, new WorkUnitResult(WorkUnitResultCode.Skipped, WorkUnitActionCode.Continue, null)); + _state = TargetEntryState.Completed; + + if (!projectLoggingContext.LoggingService.OnlyLogCriticalEvents) + { + // Expand the expression for the Log. + string expanded = _expander.ExpandIntoStringAndUnescape(_target.Condition, ExpanderOptions.ExpandPropertiesAndItems, _target.ConditionLocation); + + // By design: Not building dependencies. This is what NAnt does too. + // NOTE: In the original code, this was logged from the target logging context. However, the target + // hadn't been "started" by then, so you'd get a target message outside the context of a started + // target. In the Task builder (and original Task Engine), a Task Skipped message would be logged in + // the context of the target, not the task. This should be the same, especially given that we + // wish to allow batching on the condition of a target. + projectLoggingContext.LogComment(MessageImportance.Low, "TargetSkippedFalseCondition", _target.Name, _target.Condition, expanded); + } + + return new List(); + } + + IList dependencies = _expander.ExpandIntoStringListLeaveEscaped(_target.DependsOnTargets, ExpanderOptions.ExpandPropertiesAndItems, _target.DependsOnTargetsLocation); + List dependencyTargets = new List(dependencies.Count); + foreach (string escapedDependency in dependencies) + { + string dependencyTargetName = EscapingUtilities.UnescapeAll(escapedDependency); + dependencyTargets.Add(new TargetSpecification(dependencyTargetName, _target.DependsOnTargetsLocation)); + } + + _state = TargetEntryState.Execution; + + return dependencyTargets; + } + + /// + /// Runs all of the tasks for this target, batched as necessary. + /// + internal async Task ExecuteTarget(ITaskBuilder taskBuilder, BuildRequestEntry requestEntry, ProjectLoggingContext projectLoggingContext, CancellationToken cancellationToken) + { +#if MSBUILDENABLEVSPROFILING + try + { + string beginTargetBuild = String.Format(CultureInfo.CurrentCulture, "Build Target {0} in Project {1} - Start", this.Name, projectFullPath); + DataCollection.CommentMarkProfile(8800, beginTargetBuild); +#endif + + try + { + VerifyState(_state, TargetEntryState.Execution); + ErrorUtilities.VerifyThrow(!_isExecuting, "Target {0} is already executing", _target.Name); + _cancellationToken = cancellationToken; + _isExecuting = true; + + // Generate the batching buckets. Note that each bucket will get a lookup based on the baseLookup. This lookup will be in its + // own scope, which we will collapse back down into the baseLookup at the bottom of the function. + List buckets = BatchingEngine.PrepareBatchingBuckets(GetBatchableParametersForTarget(), _baseLookup, _target.Location); + + WorkUnitResult aggregateResult = new WorkUnitResult(); + TargetLoggingContext targetLoggingContext = null; + bool targetSuccess = false; + int numberOfBuckets = buckets.Count; + string projectFullPath = requestEntry.RequestConfiguration.ProjectFullPath; + + string parentTargetName = null; + if (ParentEntry != null && ParentEntry.Target != null) + { + parentTargetName = ParentEntry.Target.Name; + } + + for (int i = 0; i < numberOfBuckets; i++) + { + ItemBucket bucket = buckets[i]; + + // If one of the buckets failed, stop building. + if (aggregateResult.ActionCode == WorkUnitActionCode.Stop) + { + break; + } + + targetLoggingContext = projectLoggingContext.LogTargetBatchStarted(projectFullPath, _target, parentTargetName); + WorkUnitResult bucketResult = null; + targetSuccess = false; + + Lookup.Scope entryForInference = null; + Lookup.Scope entryForExecution = null; + + try + { + // This isn't really dependency analysis. This is up-to-date checking. Based on this we will be able to determine if we should + // run tasks in inference or execution mode (or both) or just skip them altogether. + ItemDictionary changedTargetInputs; + ItemDictionary upToDateTargetInputs; + Lookup lookupForInference; + Lookup lookupForExecution; + + // UNDONE: (Refactor) Refactor TargetUpToDateChecker to take a logging context, not a logging service. + TargetUpToDateChecker dependencyAnalyzer = new TargetUpToDateChecker(requestEntry.RequestConfiguration.Project, _target, targetLoggingContext.LoggingService, targetLoggingContext.BuildEventContext); + DependencyAnalysisResult dependencyResult = dependencyAnalyzer.PerformDependencyAnalysis(bucket, out changedTargetInputs, out upToDateTargetInputs); + + switch (dependencyResult) + { + // UNDONE: Need to enter/leave debugger scope properly for the element. + case DependencyAnalysisResult.FullBuild: + case DependencyAnalysisResult.IncrementalBuild: + case DependencyAnalysisResult.SkipUpToDate: + // Create the lookups used to hold the current set of properties and items + lookupForInference = bucket.Lookup; + lookupForExecution = bucket.Lookup.Clone(); + + // Push the lookup stack up one so that we are only modifying items and properties in that scope. + entryForInference = lookupForInference.EnterScope("ExecuteTarget() Inference"); + entryForExecution = lookupForExecution.EnterScope("ExecuteTarget() Execution"); + + // if we're doing an incremental build, we need to effectively run the task twice -- once + // to infer the outputs for up-to-date input items, and once to actually execute the task; + // as a result we need separate sets of item and property collections to track changes + if (dependencyResult == DependencyAnalysisResult.IncrementalBuild) + { + // subset the relevant items to those that are up-to-date + foreach (string itemType in upToDateTargetInputs.ItemTypes) + { + lookupForInference.PopulateWithItems(itemType, upToDateTargetInputs[itemType]); + } + + // subset the relevant items to those that have changed + foreach (string itemType in changedTargetInputs.ItemTypes) + { + lookupForExecution.PopulateWithItems(itemType, changedTargetInputs[itemType]); + } + } + + // We either have some work to do or at least we need to infer outputs from inputs. + bucketResult = await ProcessBucket(taskBuilder, targetLoggingContext, GetTaskExecutionMode(dependencyResult), lookupForInference, lookupForExecution); + + // Now aggregate the result with the existing known results. There are four rules, assuming the target was not + // skipped due to being up-to-date: + // 1. If this bucket failed or was cancelled, the aggregate result is failure. + // 2. If this bucket succeded and we have not previously failed, the aggregate result is a success. + // 3. Otherwise, the bucket was skipped, which has no effect on the aggregate result. + // 4. If the bucket's action code says to stop, then we stop, regardless of the success or failure state. + if (dependencyResult != DependencyAnalysisResult.SkipUpToDate) + { + aggregateResult = aggregateResult.AggregateResult(bucketResult); + } + else + { + if (aggregateResult.ResultCode == WorkUnitResultCode.Skipped) + { + aggregateResult = aggregateResult.AggregateResult(new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null)); + } + } + + // Pop the lookup scopes, causing them to collapse their values back down into the + // bucket's lookup. + // NOTE: this order is important because when we infer outputs, we are trying + // to produce the same results as would be produced from a full build; as such + // if we're doing both the infer and execute steps, we want the outputs from + // the execute step to override the outputs of the infer step -- this models + // the full build scenario more correctly than if the steps were reversed + entryForInference.LeaveScope(); + entryForInference = null; + entryForExecution.LeaveScope(); + entryForExecution = null; + targetSuccess = (bucketResult != null) && (bucketResult.ResultCode == WorkUnitResultCode.Success); + break; + + case DependencyAnalysisResult.SkipNoInputs: + case DependencyAnalysisResult.SkipNoOutputs: + // We have either no inputs or no outputs, so there is nothing to do. + targetSuccess = true; + break; + } + } + catch (InvalidProjectFileException e) + { + // Make sure the Invalid Project error gets logged *before* TargetFinished. Otherwise, + // the log is confusing. + targetLoggingContext.LogInvalidProjectFileError(e); + + if (null != entryForInference) + { + entryForInference.LeaveScope(); + } + + if (null != entryForExecution) + { + entryForExecution.LeaveScope(); + } + + aggregateResult = aggregateResult.AggregateResult(new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null)); + } + finally + { + // Don't log the last target finished event until we can process the target outputs as we want to attach them to the + // last target batch. + if (targetLoggingContext != null && i < numberOfBuckets - 1) + { + targetLoggingContext.LogTargetBatchFinished(projectFullPath, targetSuccess, null); + targetLoggingContext = null; + } + } + } + + // Produce the final results. + List targetOutputItems = new List(); + + try + { + // If any legacy CallTarget operations took place, integrate them back in to the main lookup now. + LeaveLegacyCallTargetScopes(); + + // Publish the items for each bucket back into the baseLookup. Note that EnterScope() was actually called on each + // bucket inside of the ItemBucket constructor, which is why you don't see it anywhere around here. + foreach (ItemBucket bucket in buckets) + { + bucket.LeaveScope(); + } + + string targetReturns = _target.Returns; + ElementLocation targetReturnsLocation = _target.ReturnsLocation; + + // If there are no targets in the project file that use the "Returns" attribute, that means that we + // revert to the legacy behavior in the case where Returns is not specified (null, rather + // than the empty string, which indicates no returns). Legacy behavior is for all + // of the target's Outputs to be returned. + // On the other hand, if there is at least one target in the file that uses the Returns attribute, + // then all targets in the file are run according to the new behaviour (return nothing unless otherwise + // specified by the Returns attribute). + if (targetReturns == null) + { + if (!_target.ParentProjectSupportsReturnsAttribute) + { + targetReturns = _target.Outputs; + targetReturnsLocation = _target.OutputsLocation; + } + } + + if (!String.IsNullOrEmpty(targetReturns)) + { + // Determine if we should keep duplicates. + bool keepDupes = ConditionEvaluator.EvaluateCondition + ( + _target.KeepDuplicateOutputs, + ParserOptions.AllowPropertiesAndItemLists, + _expander, + ExpanderOptions.ExpandPropertiesAndItems, + requestEntry.ProjectRootDirectory, + _target.KeepDuplicateOutputsLocation, + projectLoggingContext.LoggingService, + projectLoggingContext.BuildEventContext + ); + + // NOTE: we need to gather the outputs in batches, because the output specification may reference item metadata + // Also, we are using the baseLookup, which has possibly had changes made to it since the project started. Because of this, the + // set of outputs calculated here may differ from those which would have been calculated at the beginning of the target. It is + // assumed the user intended this. + List batchingBuckets = BatchingEngine.PrepareBatchingBuckets(GetBatchableParametersForTarget(), _baseLookup, _target.Location); + + if (keepDupes) + { + foreach (ItemBucket bucket in batchingBuckets) + { + targetOutputItems.AddRange(bucket.Expander.ExpandIntoTaskItemsLeaveEscaped(targetReturns, ExpanderOptions.ExpandAll, targetReturnsLocation)); + } + } + else + { + HashSet addedItems = new HashSet(); + foreach (ItemBucket bucket in batchingBuckets) + { + IList itemsToAdd = bucket.Expander.ExpandIntoTaskItemsLeaveEscaped(targetReturns, ExpanderOptions.ExpandAll, targetReturnsLocation); + + foreach (TaskItem item in itemsToAdd) + { + if (!addedItems.Contains(item)) + { + targetOutputItems.Add(item); + addedItems.Add(item); + } + } + } + } + } + } + finally + { + if (targetLoggingContext != null) + { + // log the last target finished since we now have the target outputs. + targetLoggingContext.LogTargetBatchFinished(projectFullPath, targetSuccess, targetOutputItems != null && targetOutputItems.Count > 0 ? targetOutputItems : null); + } + } + + _targetResult = new TargetResult(targetOutputItems.ToArray(), aggregateResult); + + if (aggregateResult.ResultCode == WorkUnitResultCode.Failed && aggregateResult.ActionCode == WorkUnitActionCode.Stop) + { + _state = TargetEntryState.ErrorExecution; + } + else + { + _state = TargetEntryState.Completed; + } + } + finally + { + _isExecuting = false; + } +#if MSBUILDENABLEVSPROFILING + } + finally + { + string endTargetBuild = String.Format(CultureInfo.CurrentCulture, "Build Target {0} in Project {1} - End", this.Name, projectFullPath); + DataCollection.CommentMarkProfile(8801, endTargetBuild); + } +#endif + return; + } + + /// + /// Retrieves the error targets for this target. + /// + /// The project logging context. + /// A list of error targets. + internal List GetErrorTargets(ProjectLoggingContext projectLoggingContext) + { + VerifyState(_state, TargetEntryState.ErrorExecution); + ErrorUtilities.VerifyThrow(_legacyCallTargetScopes == null, "We should have already left any legacy call target scopes."); + + List allErrorTargets = new List(_target.OnErrorChildren.Count); + + foreach (ProjectOnErrorInstance errorTargetInstance in _target.OnErrorChildren) + { + bool condition = ConditionEvaluator.EvaluateCondition + ( + errorTargetInstance.Condition, + ParserOptions.AllowPropertiesAndItemLists, + _expander, + ExpanderOptions.ExpandPropertiesAndItems, + _requestEntry.ProjectRootDirectory, + errorTargetInstance.ConditionLocation, + projectLoggingContext.LoggingService, + projectLoggingContext.BuildEventContext + ); + + if (condition) + { + IList errorTargets = _expander.ExpandIntoStringListLeaveEscaped(errorTargetInstance.ExecuteTargets, ExpanderOptions.ExpandPropertiesAndItems, errorTargetInstance.ExecuteTargetsLocation); + + foreach (string escapedErrorTarget in errorTargets) + { + string errorTargetName = EscapingUtilities.UnescapeAll(escapedErrorTarget); + allErrorTargets.Add(new TargetSpecification(errorTargetName, errorTargetInstance.ExecuteTargetsLocation)); + } + } + } + + // If this target never executed (for instance, because one of its dependencies errored) then we need to + // create a result for this target to report when it gets to the Completed state. + if (null == _targetResult) + { + _targetResult = new TargetResult(new TaskItem[] { }, new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null)); + } + + _state = TargetEntryState.Completed; + + return allErrorTargets; + } + + /// + /// Gathers the results from the target into the base lookup of the target. + /// + /// The base lookup for this target. + internal TargetResult GatherResults() + { + VerifyState(_state, TargetEntryState.Completed); + ErrorUtilities.VerifyThrow(_legacyCallTargetScopes == null, "We should have already left any legacy call target scopes."); + + // By now all of the bucket lookups have been collapsed into this lookup, which we can return. + return _targetResult; + } + + /// + /// Enters a legacy calltarget scope. + /// + /// The lookup to enter with. + internal void EnterLegacyCallTargetScope(Lookup lookup) + { + if (null == _legacyCallTargetScopes) + { + _legacyCallTargetScopes = new Stack(); + } + + _legacyCallTargetScopes.Push(lookup.EnterScope("EnterLegacyCallTargetScope()")); + } + + /// + /// This method is used by the Target Builder to indicate that the target should run in error mode rather than normal mode. + /// + internal void MarkForError() + { + ErrorUtilities.VerifyThrow(_state != TargetEntryState.Completed, "State must not be Completed. State is {0}.", _state); + _state = TargetEntryState.ErrorExecution; + } + + /// + /// Leaves all the call target scopes in the order they were entered. + /// + internal void LeaveLegacyCallTargetScopes() + { + if (null != _legacyCallTargetScopes) + { + while (_legacyCallTargetScopes.Count != 0) + { + Lookup.Scope entry = _legacyCallTargetScopes.Pop(); + entry.LeaveScope(); + } + + _legacyCallTargetScopes = null; + } + } + + /// + /// Walks through the set of tasks for this target and processes them by handing them off to the TaskBuilder. + /// + /// + /// The result of the tasks, based on the last task which ran. + /// + private async Task ProcessBucket(ITaskBuilder taskBuilder, TargetLoggingContext targetLoggingContext, TaskExecutionMode mode, Lookup lookupForInference, Lookup lookupForExecution) + { + WorkUnitResultCode aggregatedTaskResult = WorkUnitResultCode.Success; + WorkUnitActionCode finalActionCode = WorkUnitActionCode.Continue; + WorkUnitResult lastResult = new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null); + + try + { + // Grab the task builder so if cancel is called it will have something to operate on. + _currentTaskBuilder = taskBuilder; + + int currentTask = 0; + + // Walk through all of the tasks and execute them in order. + for (; (currentTask < _target.Children.Count) && !_cancellationToken.IsCancellationRequested; ++currentTask) + { + ProjectTargetInstanceChild targetChildInstance = _target.Children[currentTask]; + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.EnterState(targetChildInstance.Location, lookupForExecution.GlobalsForDebugging /* does not matter which lookup we get this from */); + } + + // Execute the task. + lastResult = await taskBuilder.ExecuteTask(targetLoggingContext, _requestEntry, _targetBuilderCallback, targetChildInstance, mode, lookupForInference, lookupForExecution, _cancellationToken); + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(targetChildInstance.Location); + } + + if (lastResult.ResultCode == WorkUnitResultCode.Failed) + { + aggregatedTaskResult = WorkUnitResultCode.Failed; + } + else if (lastResult.ResultCode == WorkUnitResultCode.Success && aggregatedTaskResult != WorkUnitResultCode.Failed) + { + aggregatedTaskResult = WorkUnitResultCode.Success; + } + + if (lastResult.ActionCode == WorkUnitActionCode.Stop) + { + finalActionCode = WorkUnitActionCode.Stop; + break; + } + } + + if (_cancellationToken.IsCancellationRequested) + { + aggregatedTaskResult = WorkUnitResultCode.Canceled; + finalActionCode = WorkUnitActionCode.Stop; + } + } + finally + { + _currentTaskBuilder = null; + } + + return new WorkUnitResult(aggregatedTaskResult, finalActionCode, lastResult.Exception); + } + + /// + /// Gets the task execution mode based + /// + /// The result of the up-to-date check. + /// The mode to be used to execute tasks. + private TaskExecutionMode GetTaskExecutionMode(DependencyAnalysisResult analysis) + { + TaskExecutionMode executionMode; + if ((analysis == DependencyAnalysisResult.SkipUpToDate) || + (analysis == DependencyAnalysisResult.IncrementalBuild)) + { + executionMode = TaskExecutionMode.InferOutputsOnly; + } + else + { + executionMode = TaskExecutionMode.ExecuteTaskAndGatherOutputs; + } + + // Execute the task using the items that need to be (re)built + if ((analysis == DependencyAnalysisResult.FullBuild) || + (analysis == DependencyAnalysisResult.IncrementalBuild)) + { + executionMode = executionMode | TaskExecutionMode.ExecuteTaskAndGatherOutputs; + } + + return executionMode; + } + + /// + /// Verifies that the target's state is as expected. + /// + /// The actual value + /// The expected value + private void VerifyState(TargetEntryState actual, TargetEntryState expected) + { + ErrorUtilities.VerifyThrow(actual == expected, "Expected state {1}. Got {0}", actual, expected); + } + + /// + /// Gets the list of parameters which are batchable for a target + /// PERF: (Refactor) This used to be a method on the target, and it would + /// cache its values so this would only be computed once for each + /// target. We should consider doing something similar for perf reasons. + /// + /// A list of batchable parameters + private List GetBatchableParametersForTarget() + { + List batchableTargetParameters = new List(); + + if (_target.Inputs.Length > 0) + { + batchableTargetParameters.Add(_target.Inputs); + } + + if (_target.Outputs.Length > 0) + { + batchableTargetParameters.Add(_target.Outputs); + } + + if (_target.Returns != null && _target.Returns.Length > 0) + { + batchableTargetParameters.Add(_target.Returns); + } + + return batchableTargetParameters; + } + + /// + /// Resolves the target. If it doesn't exist in the project, throws an InvalidProjectFileException. + /// + private void GetTargetInstance() + { + _requestEntry.RequestConfiguration.Project.Targets.TryGetValue(_targetSpecification.TargetName, out _target); + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + _target != null, + _targetSpecification.ReferenceLocation ?? _requestEntry.RequestConfiguration.Project.ProjectFileLocation, + "TargetDoesNotExist", + _targetSpecification.TargetName + ); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetSpecification.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetSpecification.cs new file mode 100644 index 00000000000..712db0bbc72 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetSpecification.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implements the TargetSpecification class +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using Microsoft.Build.Shared; +using System.Diagnostics; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Contains information about a target name and reference location. + /// + [DebuggerDisplay("Name={TargetName}")] + internal class TargetSpecification + { + /// + /// Construct a target specification. + /// + /// The name of the target + /// The location from which it was referred. + internal TargetSpecification(string targetName, ElementLocation referenceLocation) + { + ErrorUtilities.VerifyThrowArgumentLength(targetName, "targetName"); + ErrorUtilities.VerifyThrowArgumentNull(referenceLocation, "referenceLocation"); + + this.TargetName = targetName; + this.ReferenceLocation = referenceLocation; + } + + /// + /// Gets or sets the target name + /// + public string TargetName + { + get; + private set; + } + + /// + /// Gets or sets the reference location + /// + public ElementLocation ReferenceLocation + { + get; + private set; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs new file mode 100644 index 00000000000..8d16e7fcae6 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TargetUpToDateChecker.cs @@ -0,0 +1,1413 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Text; +using System.Threading; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory; + +namespace Microsoft.Build.BackEnd +{ + using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; + + // ItemVectorPartitionCollection is designed to contains a set of project items which have possibly undergone transforms. + // The outer dictionary it usually keyed by item type, so if items originally came from + // an expression like @(Foo), the outer dictionary would have a key of "Foo" in it. + // Under that is a dictionary of expressions to items resulting from the expression. + // For instance, if items were generated from an expression @(Foo->'%(Filename).obj'), then + // the inner dictionary would have a key of "@(Foo->'%(Filename).obj')", in which would be + // contained a list of the items which were created/transformed using that pattern. + using ItemVectorPartitionCollection = Dictionary>>; + using ItemVectorPartition = Dictionary>; + + /// + /// Enumeration of the results of target dependency analysis. + /// + internal enum DependencyAnalysisResult + { + SkipUpToDate, + SkipNoInputs, + SkipNoOutputs, + IncrementalBuild, + FullBuild + } + + /// + /// This class is used for performing dependency analysis on targets to determine if they should be built/rebuilt/skipped. + /// + internal sealed class TargetUpToDateChecker + { + #region Constructors + + /// + /// Creates an instance of this class for the given target. + /// + internal TargetUpToDateChecker(ProjectInstance project, ProjectTargetInstance targetToAnalyze, ILoggingService loggingServices, BuildEventContext buildEventContext) + { + ErrorUtilities.VerifyThrow(project != null, "Need a project."); + ErrorUtilities.VerifyThrow(targetToAnalyze != null, "Need a target to analyze."); + + _project = project; + _targetToAnalyze = targetToAnalyze; + _targetInputSpecification = targetToAnalyze.Inputs; + _targetOutputSpecification = targetToAnalyze.Outputs; + _loggingService = loggingServices; + _buildEventContext = buildEventContext; + } + + #endregion + + #region Properties + + /// + /// Gets the target to perform dependency analysis on. + /// + /// Target object. + internal ProjectTargetInstance TargetToAnalyze + { + get + { + return _targetToAnalyze; + } + } + + /// + /// Gets the value of the target's "Inputs" attribute. + /// + /// Input specification string (can be empty). + private string TargetInputSpecification + { + get + { + ErrorUtilities.VerifyThrow(_targetInputSpecification != null, "targetInputSpecification is null"); + return _targetInputSpecification; + } + } + + /// + /// Gets the value of the target's "Outputs" attribute. + /// + /// Output specification string (can be empty). + private string TargetOutputSpecification + { + get + { + ErrorUtilities.VerifyThrow(_targetOutputSpecification != null, "targetOutputSpecification is null"); + return _targetOutputSpecification; + } + } + #endregion + + #region Methods + + /// + /// Compares the target's inputs against its outputs to determine if the target needs to be built/rebuilt/skipped. + /// + /// + /// The collections of changed and up-to-date inputs returned from this method are valid IFF this method decides an + /// incremental build is needed. + /// + /// + /// + /// + /// + /// DependencyAnalysisResult.SkipUpToDate, if target is up-to-date; + /// DependencyAnalysisResult.SkipNoInputs, if target has no inputs; + /// DependencyAnalysisResult.SkipNoOutputs, if target has no outputs; + /// DependencyAnalysisResult.IncrementalBuild, if only some target outputs are out-of-date; + /// DependencyAnalysisResult.FullBuild, if target is out-of-date + /// + internal DependencyAnalysisResult PerformDependencyAnalysis + ( + ItemBucket bucket, + out ItemDictionary changedTargetInputs, + out ItemDictionary upToDateTargetInputs + ) + { + // Clear any old dependency analysis logging details + _dependencyAnalysisDetail.Clear(); + _uniqueTargetInputs.Clear(); + _uniqueTargetOutputs.Clear(); + + ProjectErrorUtilities.VerifyThrowInvalidProject((TargetOutputSpecification.Length > 0) || (TargetInputSpecification.Length == 0), + _targetToAnalyze.InputsLocation, "TargetInputsSpecifiedWithoutOutputs", TargetToAnalyze.Name); + + DependencyAnalysisResult result = DependencyAnalysisResult.SkipUpToDate; + + changedTargetInputs = null; + upToDateTargetInputs = null; + + if (TargetOutputSpecification.Length == 0) + { + // if the target has no output specification, we always build it + result = DependencyAnalysisResult.FullBuild; + } + else + { + ItemVectorPartitionCollection itemVectorsInTargetInputs; + ItemVectorPartitionCollection itemVectorTransformsInTargetInputs; + Dictionary discreteItemsInTargetInputs; // UNDONE: (Refactor) Change to HashSet + + ItemVectorPartitionCollection itemVectorsInTargetOutputs; + Dictionary discreteItemsInTargetOutputs; // UNDONE: (Refactor) Change to HashSet + List targetOutputItemSpecs; + + ParseTargetInputOutputSpecifications(bucket, + out itemVectorsInTargetInputs, + out itemVectorTransformsInTargetInputs, + out discreteItemsInTargetInputs, + out itemVectorsInTargetOutputs, + out discreteItemsInTargetOutputs, + out targetOutputItemSpecs); + + List itemVectorsReferencedInBothTargetInputsAndOutputs = null; + List itemVectorsReferencedOnlyInTargetInputs = null; + List itemVectorsReferencedOnlyInTargetOutputs = null; + + // if the target has no outputs because the output specification evaluated to empty + if (targetOutputItemSpecs.Count == 0) + { + result = PerformDependencyAnalysisIfNoOutputs(); + } + // if there are no discrete output items... + else if (discreteItemsInTargetOutputs.Count == 0) + { + // try to correlate inputs and outputs by checking: + // 1) which item vectors are referenced by both input and output items + // 2) which item vectors are referenced only by input items + // 3) which item vectors are referenced only by output items + // NOTE: two item vector transforms cannot be correlated, even if they reference the same item vector, because + // depending on the transform expression, there might be no relation between the results of the transforms; as + // a result, input items that are item vector transforms are treated as discrete items + DiffHashtables(itemVectorsInTargetInputs, itemVectorsInTargetOutputs, + out itemVectorsReferencedInBothTargetInputsAndOutputs, + out itemVectorsReferencedOnlyInTargetInputs, + out itemVectorsReferencedOnlyInTargetOutputs); + + // if there are no item vectors only referenced by output items... + // NOTE: we consider output items that reference item vectors not referenced by any input item, as discrete + // items, since we cannot correlate them to any input items + if (itemVectorsReferencedOnlyInTargetOutputs.Count == 0) + { + /* + * At this point, we know the following: + * 1) the target has outputs + * 2) the target has NO discrete outputs + * + * This implies: + * 1) the target only references vectors (incl. transforms) in its outputs + * 2) all vectors referenced in the outputs are also referenced in the inputs + * 3) the referenced vectors are not empty + * + * We can thus conclude: the target MUST have (non-discrete) inputs + * + */ + ErrorUtilities.VerifyThrow(itemVectorsReferencedInBothTargetInputsAndOutputs.Count > 0, "The target must have inputs."); + ErrorUtilities.VerifyThrow(GetItemSpecsFromItemVectors(itemVectorsInTargetInputs).Count > 0, "The target must have inputs."); + + result = PerformDependencyAnalysisIfDiscreteInputs(itemVectorsInTargetInputs, + itemVectorTransformsInTargetInputs, discreteItemsInTargetInputs, itemVectorsReferencedOnlyInTargetInputs, + targetOutputItemSpecs); + + if (result != DependencyAnalysisResult.FullBuild) + { + // once the inputs and outputs have been correlated, we can do a 1-to-1 comparison between each input + // and its corresponding output, to discover which inputs have changed, and which are up-to-date... + result = PerformDependencyAnalysisIfCorrelatedInputsOutputs(itemVectorsInTargetInputs, itemVectorsInTargetOutputs, + itemVectorsReferencedInBothTargetInputsAndOutputs, + out changedTargetInputs, out upToDateTargetInputs); + } + } + } + + // if there are any discrete items in the target outputs, then we have no obvious correlation to the inputs they + // depend on, since any input can contribute to a discrete output, so we compare all inputs against all outputs + // NOTE: output items are considered discrete, if + // 1) they do not reference any item vector + // 2) they reference item vectors that are not referenced by any input item + if ((discreteItemsInTargetOutputs.Count > 0) || + ((itemVectorsReferencedOnlyInTargetOutputs != null) && (itemVectorsReferencedOnlyInTargetOutputs.Count > 0))) + { + result = PerformDependencyAnalysisIfDiscreteOutputs( + itemVectorsInTargetInputs, itemVectorTransformsInTargetInputs, discreteItemsInTargetInputs, + targetOutputItemSpecs); + } + + if (result == DependencyAnalysisResult.SkipUpToDate) + { + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, + "SkipTargetBecauseOutputsUpToDate", + TargetToAnalyze.Name); + + // Log the target inputs & outputs + if (!_loggingService.OnlyLogCriticalEvents) + { + LogUniqueInputsAndOutputs(); + } + } + } + + LogReasonForBuildingTarget(result); + + return result; + } + + /// + /// Does appropriate logging to indicate why this target is being built fully or partially. + /// + /// + private void LogReasonForBuildingTarget(DependencyAnalysisResult result) + { + // Only if we are not logging just critical events should we be logging the details + if (!_loggingService.OnlyLogCriticalEvents) + { + if (result == DependencyAnalysisResult.FullBuild && _dependencyAnalysisDetail.Count > 0) + { + // For the full build decision the are three possible outcomes + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "BuildTargetCompletely", _targetToAnalyze.Name); + + foreach (DependencyAnalysisLogDetail logDetail in _dependencyAnalysisDetail) + { + string reason = GetFullBuildReason(logDetail); + _loggingService.LogCommentFromText(_buildEventContext, MessageImportance.Low, reason); + } + } + else if (result == DependencyAnalysisResult.IncrementalBuild) + { + // For the partial build decision the are three possible outcomes + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, "BuildTargetPartially", _targetToAnalyze.Name); + foreach (DependencyAnalysisLogDetail logDetail in _dependencyAnalysisDetail) + { + string reason = GetIncrementalBuildReason(logDetail); + _loggingService.LogCommentFromText(_buildEventContext, MessageImportance.Low, reason); + } + } + } + } + + /// + /// Returns a string indicating why a full build is occurring. + /// + internal static string GetFullBuildReason(DependencyAnalysisLogDetail logDetail) + { + string reason = null; + + if (logDetail.Reason == OutofdateReason.NewerInput) + { + // One of the inputs was newer than all of the outputs + reason = ResourceUtilities.FormatResourceString("BuildTargetCompletelyInputNewer", logDetail.Input, logDetail.Output); + } + else if (logDetail.Reason == OutofdateReason.MissingOutput) + { + // One of the outputs was missing + reason = ResourceUtilities.FormatResourceString("BuildTargetCompletelyOutputDoesntExist", logDetail.Output); + } + else if (logDetail.Reason == OutofdateReason.MissingInput) + { + // One of the inputs was missing + reason = ResourceUtilities.FormatResourceString("BuildTargetCompletelyInputDoesntExist", logDetail.Input); + } + + return reason; + } + + /// + /// Returns a string indicating why an incremental build is occurring. + /// + private static string GetIncrementalBuildReason(DependencyAnalysisLogDetail logDetail) + { + string reason = null; + + if (logDetail.Reason == OutofdateReason.NewerInput) + { + // One of the inputs was newer than its corresponding output + reason = ResourceUtilities.FormatResourceString("BuildTargetPartiallyInputNewer", logDetail.InputItemName, logDetail.Input, logDetail.Output); + } + else if (logDetail.Reason == OutofdateReason.MissingOutput) + { + // One of the outputs was missing + reason = ResourceUtilities.FormatResourceString("BuildTargetPartiallyOutputDoesntExist", logDetail.OutputItemName, logDetail.Input, logDetail.Output); + } + else if (logDetail.Reason == OutofdateReason.MissingInput) + { + // One of the inputs was missing + reason = ResourceUtilities.FormatResourceString("BuildTargetPartiallyInputDoesntExist", logDetail.InputItemName, logDetail.Input, logDetail.Output); + } + + return reason; + } + + /// + /// Extract only the unique inputs and outputs from all the inputs and outputs gathered + /// during depedency analysis + /// + /// [out] the unique inputs + /// [out] the unique outputs + private void LogUniqueInputsAndOutputs() + { + string inputs = null; + string outputs = null; + + if (_uniqueTargetInputs.Count > 0) + { + StringBuilder inputsBuilder = new StringBuilder(); + foreach (string input in _uniqueTargetInputs.Keys) + { + inputsBuilder.Append(input); + inputsBuilder.Append(";"); + } + + // We don't want the trailing ; so remove it + inputs = inputsBuilder.ToString(0, inputsBuilder.Length - 1); + } + else + { + inputs = String.Empty; + } + + if (_uniqueTargetOutputs.Count > 0) + { + StringBuilder outputsBuilder = new StringBuilder(); + foreach (string input in _uniqueTargetOutputs.Keys) + { + outputsBuilder.Append(input); + outputsBuilder.Append(";"); + } + + // We don't want the trailing ; so remove it + outputs = outputsBuilder.ToString(0, outputsBuilder.Length - 1); + } + else + { + outputs = String.Empty; + } + + if (inputs != null) + { + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "SkipTargetUpToDateInputs", inputs); + } + + if (outputs != null) + { + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "SkipTargetUpToDateOutputs", outputs); + } + } + + /// + /// Parses the target's "Inputs" and "Outputs" attributes and gathers up referenced items. + /// + /// + /// + /// + /// + /// + /// + /// + private void ParseTargetInputOutputSpecifications + ( + ItemBucket bucket, + out ItemVectorPartitionCollection itemVectorsInTargetInputs, + out ItemVectorPartitionCollection itemVectorTransformsInTargetInputs, + out Dictionary discreteItemsInTargetInputs, + out ItemVectorPartitionCollection itemVectorsInTargetOutputs, + out Dictionary discreteItemsInTargetOutputs, + out List targetOutputItemSpecs + ) + { + // break down the input/output specifications along the standard separator, after expanding all embedded properties + // and item metadata + IList targetInputs = bucket.Expander.ExpandIntoStringListLeaveEscaped(TargetInputSpecification, ExpanderOptions.ExpandPropertiesAndMetadata, _targetToAnalyze.InputsLocation); + IList targetOutputs = bucket.Expander.ExpandIntoStringListLeaveEscaped(TargetOutputSpecification, ExpanderOptions.ExpandPropertiesAndMetadata, _targetToAnalyze.OutputsLocation); + + itemVectorTransformsInTargetInputs = new ItemVectorPartitionCollection(MSBuildNameIgnoreCaseComparer.Default); + + // figure out which of the inputs are: + // 1) item vectors + // 2) item vectors with transforms + // 3) "discrete" items i.e. items that do not reference item vectors + SeparateItemVectorsFromDiscreteItems( + targetInputs, + bucket, + out itemVectorsInTargetInputs, + itemVectorTransformsInTargetInputs, + out discreteItemsInTargetInputs, + _targetToAnalyze.InputsLocation); + + // figure out which of the outputs are: + // 1) item vectors (with or without transforms) + // 2) "discrete" items i.e. items that do not reference item vectors + SeparateItemVectorsFromDiscreteItems( + targetOutputs, + bucket, + out itemVectorsInTargetOutputs, + null /* don't want transforms separated */, + out discreteItemsInTargetOutputs, + _targetToAnalyze.OutputsLocation); + + // list out all the output item-specs + targetOutputItemSpecs = GetItemSpecsFromItemVectors(itemVectorsInTargetOutputs); + targetOutputItemSpecs.AddRange(discreteItemsInTargetOutputs.Values); + } + + /// + /// Determines if the target needs to be built/rebuilt/skipped if it has no inputs (because they evaluated to empty). + /// + private DependencyAnalysisResult PerformDependencyAnalysisIfNoInputs() + { + DependencyAnalysisResult result; + + // if the target did declare inputs, but the specification evaluated to nothing + if (TargetInputSpecification.Length > 0) + { + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, + "SkipTargetBecauseNoInputs", TargetToAnalyze.Name); + // detailed reason is low importance to keep log clean + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, + "SkipTargetBecauseNoInputsDetail"); + + // don't build the target + result = DependencyAnalysisResult.SkipNoInputs; + } + else + { + // There were no inputs specified, so build completely + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "BuildTargetCompletely", _targetToAnalyze.Name); + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "BuildTargetCompletelyNoInputsSpecified"); + + + // otherwise, do a full build + result = DependencyAnalysisResult.FullBuild; + } + + return result; + } + + /// + /// Determines if the target needs to be built/rebuilt/skipped if it has no outputs (because they evaluated to empty). + /// + /// Indication of how to build the target. + private DependencyAnalysisResult PerformDependencyAnalysisIfNoOutputs() + { + DependencyAnalysisResult result = DependencyAnalysisResult.SkipNoOutputs; + + // If the target has no inputs declared and the outputs evaluated to empty, do a full build. Remember that somebody + // may specify Outputs="@(blah)", where the item list "blah" is actually produced by some task within this target. So + // at the beginning, when we're trying to do to the dependency analysis, there's nothing in the "blah" list, but after + // the target executes, there will be. + if (TargetInputSpecification.Length == 0) + { + result = DependencyAnalysisResult.FullBuild; + } + // otherwise, don't build the target + else + { + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, + "SkipTargetBecauseNoOutputs", TargetToAnalyze.Name); + // detailed reason is low importance to keep log clean + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, + "SkipTargetBecauseNoOutputsDetail"); + } + + return result; + } + + /// + /// Determines if the target needs to be built/rebuilt/skipped if it has discrete inputs. + /// + /// + /// + /// + /// + /// + /// Indication of how to build the target. + private DependencyAnalysisResult PerformDependencyAnalysisIfDiscreteInputs + ( + ItemVectorPartitionCollection itemVectorsInTargetInputs, + ItemVectorPartitionCollection itemVectorTransformsInTargetInputs, + Dictionary discreteItemsInTargetInputs, + List itemVectorsReferencedOnlyInTargetInputs, + List targetOutputItemSpecs + ) + { + DependencyAnalysisResult result = DependencyAnalysisResult.SkipUpToDate; + + // list out all the discrete input item-specs... + // NOTE: we treat input items that are item vector transforms, as discrete items, since we cannot correlate them to + // any output item + List discreteTargetInputItemSpecs = GetItemSpecsFromItemVectors(itemVectorTransformsInTargetInputs); + discreteTargetInputItemSpecs.AddRange(discreteItemsInTargetInputs.Values); + + // we treat input items that reference item vectors not referenced by any output item, as discrete items, since we + // cannot correlate them to any output item + foreach (string itemVectorType in itemVectorsReferencedOnlyInTargetInputs) + { + discreteTargetInputItemSpecs.AddRange(GetItemSpecsFromItemVectors(itemVectorsInTargetInputs, itemVectorType)); + } + + // if there are any discrete input items, we can treat them as "meta" inputs, because: + // 1) we have already confirmed there are no discrete output items + // 2) apart from the discrete input items, we can correlate all input items to all output items, since we know they + // both reference the same item vectors + // NOTES: + // 1) a typical example of a "meta" input is when the project file itself is listed as an input -- this forces + // rebuilds when the project file changes, even if no actual inputs have changed + // 2) discrete input items and discrete output items are not treated symmetrically, because it is more likely that + // an uncorrelated input is a "meta" input, than an uncorrelated output is a "meta" output, since outputs can + // typically be built out of more than one set of inputs + if (discreteTargetInputItemSpecs.Count > 0) + { + List inputs = CollectionHelpers.RemoveNulls(discreteTargetInputItemSpecs); + List outputs = CollectionHelpers.RemoveNulls(targetOutputItemSpecs); + + if (inputs.Count == 0) + { + return PerformDependencyAnalysisIfNoInputs(); + } + + if (outputs.Count == 0) + { + return PerformDependencyAnalysisIfNoOutputs(); + } + + // if any output item is out-of-date w.r.t. any discrete input item, do a full build + DependencyAnalysisLogDetail dependencyAnalysisDetailEntry; + bool someOutOfDate = IsAnyOutOfDate(out dependencyAnalysisDetailEntry, _project.Directory, inputs, outputs); + + if (someOutOfDate) + { + _dependencyAnalysisDetail.Add(dependencyAnalysisDetailEntry); + result = DependencyAnalysisResult.FullBuild; + } + else + { + RecordUniqueInputsAndOutputs(inputs, outputs); + } + } + + return result; + } + + /// + /// Determines if the target needs to be built/rebuilt/skipped if its inputs and outputs can be correlated. + /// + /// The set of items which are in the inputs + /// The set of items which are in the outputs. + /// A list of item types referenced in both the inputs and the outputs + /// The inputs which are "changed" and require a build + /// The inpurt which are "up to date" and do not require a build + /// Indication of how to build the target. + private DependencyAnalysisResult PerformDependencyAnalysisIfCorrelatedInputsOutputs + ( + ItemVectorPartitionCollection itemVectorsInTargetInputs, + ItemVectorPartitionCollection itemVectorsInTargetOutputs, + List itemVectorsReferencedInBothTargetInputsAndOutputs, + out ItemDictionary changedTargetInputs, + out ItemDictionary upToDateTargetInputs + ) + { + DependencyAnalysisResult result = DependencyAnalysisResult.SkipUpToDate; + + changedTargetInputs = new ItemDictionary(); + upToDateTargetInputs = new ItemDictionary(); + + // indicates if an incremental build is really just a full build, because all input items have changed + int numberOfInputItemVectorsWithAllChangedItems = 0; + + foreach (string itemVectorType in itemVectorsReferencedInBothTargetInputsAndOutputs) + { + ItemVectorPartition inputItemVectors = itemVectorsInTargetInputs[itemVectorType]; + ItemVectorPartition outputItemVectors = itemVectorsInTargetOutputs[itemVectorType]; + + // NOTE: recall that transforms have been separated out already + ErrorUtilities.VerifyThrow(inputItemVectors.Count == 1, + "There should only be one item vector of a particular type in the target inputs that can be filtered."); + + // NOTE: Because the input items which were transformed have already been pulled out, this loop + // will only execute a single time. + foreach (IList inputItems in inputItemVectors.Values) + { + if (inputItems.Count > 0) + { + // By default, we assume that all of the input items are up to date. As we go through + // our checks below, we will remove some of these and place them in the changed items dictionary + // which gets returned to the caller. + List upToDateInputItems = new List(inputItems); + int itemsChanged = 0; + + // Iterate over each of the correlated lists of output items. The keys to the outputItemVectors ItemDictionary + // are the transform expressions, not the item type from which the items were originally derived. + foreach (KeyValuePair> outputEntry in outputItemVectors) + { + string outputItemExpression = outputEntry.Key; + IList outputItems = outputEntry.Value; + + // We count backwards so that as we remove items, we are removing them from the end, thereby + // not invalidating our iteration. + + if (upToDateInputItems.Count == outputItems.Count) + { + for (int i = 0; i < upToDateInputItems.Count; i++) + { + // If we have already determined this item is out of date, don't check again. + if (upToDateInputItems[i] != null) + { + // Perform the out-of-date check only if we have an output-specification. + if (outputItems[i] != null) + { + // check if it has changed + bool outOfDate = IsOutOfDate(((IItem)upToDateInputItems[i]).EvaluatedIncludeEscaped, ((IItem)outputItems[i]).EvaluatedIncludeEscaped, upToDateInputItems[i].ItemType, outputItems[i].ItemType); + if (outOfDate) + { + changedTargetInputs.Add(upToDateInputItems[i]); + itemsChanged++; + upToDateInputItems[i] = null; + + result = DependencyAnalysisResult.IncrementalBuild; + } + } + } + } + } + else + { + // if any input is newer than any output, do a full build + DependencyAnalysisLogDetail dependencyAnalysisDetailEntry; + bool someOutOfDate = IsAnyOutOfDate(out dependencyAnalysisDetailEntry, _project.Directory, upToDateInputItems, outputItems); + + if (someOutOfDate) + { + _dependencyAnalysisDetail.Add(dependencyAnalysisDetailEntry); + itemsChanged = inputItems.Count; + result = DependencyAnalysisResult.IncrementalBuild; + } + else + { + RecordUniqueInputsAndOutputs(upToDateInputItems, outputItems); + result = DependencyAnalysisResult.SkipUpToDate; + } + } + + // If we have exhausted all of the input items of this type, move on to the next. + if (itemsChanged == inputItems.Count) + { + numberOfInputItemVectorsWithAllChangedItems++; + break; + } + } + + // Add all of the items which remain up-to-date to the up-to-date target inputs dictionary. + if (itemsChanged < inputItems.Count) + { + foreach (ProjectItemInstance item in upToDateInputItems) + { + if (item != null) + { + upToDateTargetInputs.Add(item); + } + } + } + + // If we end up with no items of a particular type in the changed set, + // then add an empty marker so that lookups will correctly *not* find + // them. + if (!changedTargetInputs.ItemTypes.Contains(inputItems[0].ItemType)) + { + changedTargetInputs.AddEmptyMarker(inputItems[0].ItemType); + } + + // We need to perform the same operation on the up-to-date side + // too. + if (!upToDateTargetInputs.ItemTypes.Contains(inputItems[0].ItemType)) + { + upToDateTargetInputs.AddEmptyMarker(inputItems[0].ItemType); + } + } + } + } + + ErrorUtilities.VerifyThrow(numberOfInputItemVectorsWithAllChangedItems <= itemVectorsReferencedInBothTargetInputsAndOutputs.Count, + "The number of vectors containing all changed items cannot exceed the number of correlated vectors."); + + // if all correlated input items have changed + if (numberOfInputItemVectorsWithAllChangedItems == itemVectorsReferencedInBothTargetInputsAndOutputs.Count) + { + ErrorUtilities.VerifyThrow(result == DependencyAnalysisResult.IncrementalBuild, + "If inputs have changed, this must be an incremental build."); + + // then the incremental build is really a full build + result = DependencyAnalysisResult.FullBuild; + } + + return result; + } + + /// + /// Determines if the target needs to be built/rebuilt/skipped if it has discrete outputs. + /// + /// + /// + /// + /// + /// Indication of how to build the target. + private DependencyAnalysisResult PerformDependencyAnalysisIfDiscreteOutputs + ( + ItemVectorPartitionCollection itemVectorsInTargetInputs, + ItemVectorPartitionCollection itemVectorTransformsInTargetInputs, + Dictionary discreteItemsInTargetInputs, + List targetOutputItemSpecs + ) + { + DependencyAnalysisResult result = DependencyAnalysisResult.SkipUpToDate; + + List targetInputItemSpecs = GetItemSpecsFromItemVectors(itemVectorsInTargetInputs); + targetInputItemSpecs.AddRange(GetItemSpecsFromItemVectors(itemVectorTransformsInTargetInputs)); + targetInputItemSpecs.AddRange(discreteItemsInTargetInputs.Values); + + List inputs = CollectionHelpers.RemoveNulls(targetInputItemSpecs); + List outputs = CollectionHelpers.RemoveNulls(targetOutputItemSpecs); + + if (inputs.Count == 0) + { + return PerformDependencyAnalysisIfNoInputs(); + } + + if (outputs.Count == 0) + { + return PerformDependencyAnalysisIfNoOutputs(); + } + + // if any input is newer than any output, do a full build + DependencyAnalysisLogDetail dependencyAnalysisDetailEntry; + bool someOutOfDate = IsAnyOutOfDate(out dependencyAnalysisDetailEntry, _project.Directory, inputs, outputs); + + if (someOutOfDate) + { + _dependencyAnalysisDetail.Add(dependencyAnalysisDetailEntry); + result = DependencyAnalysisResult.FullBuild; + } + else + { + RecordUniqueInputsAndOutputs(inputs, outputs); + result = DependencyAnalysisResult.SkipUpToDate; + } + + return result; + } + + /// + /// Separates item vectors from discrete items, and discards duplicates. If requested, item vector transforms are also + /// separated out. The item vectors (and the transforms) are partitioned by type, since there can be more than one item + /// vector of the same type. + /// + /// + /// The item vector collection is a table of tables, where the top-level table is indexed by item type, and + /// each "partition" table is indexed by the item vector itself. + /// + /// + /// + /// Collection for item vectors + /// Collection for transforms if they should be collected separately, else null + /// + private void SeparateItemVectorsFromDiscreteItems + ( + IList items, + ItemBucket bucket, + out ItemVectorPartitionCollection itemVectors, + ItemVectorPartitionCollection itemVectorTransforms, + out Dictionary discreteItems, + ElementLocation elementLocation + ) + { + itemVectors = new ItemVectorPartitionCollection(MSBuildNameIgnoreCaseComparer.Default); + discreteItems = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + + // Iterate over all of the items specified. Each of these may be in one of the following forms: + // 1. A discrete item. e.g. foo.cs + // 2. An item vector. e.g. @(Foo) + // 3. An item vector transform. e.g. @(Foo->'%(Filename).obj') + foreach (string item in items) + { + // Expand the items in the item expression. Note that the items returned will have the same type as the original expression + // specified. For example, both @(Foo) and @(Foo->'%(Filename).obj) will return items of type 'Foo'. If the item in question + // is discrete, itemVectorContents will be null. + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(_project /* no item type specified; use item type of vector itself */ ); + + bool isTransformExpression; + IList itemVectorContents = bucket.Expander.ExpandSingleItemVectorExpressionIntoItems(item, itemFactory, ExpanderOptions.ExpandItems, true /* include null entries from transforms */, out isTransformExpression, elementLocation); + + if (itemVectorContents != null) + { + // There were item expressions + + if (itemVectorContents.Count > 0) + { + ItemVectorPartitionCollection itemVectorCollection = null; + + // Expander set the item type it found + string itemVectorType = itemFactory.ItemType; + + if (itemVectorTransforms == null || !isTransformExpression) + { + // We either don't want transforms separated out, or this was not a transform. + itemVectorCollection = itemVectors; + } + else + { + itemVectorCollection = itemVectorTransforms; + } + + // Do we already have a partition for this? + if (!itemVectorCollection.ContainsKey(itemVectorType)) + { + // Nope, create one. + itemVectorCollection[itemVectorType] = new ItemVectorPartition(MSBuildNameIgnoreCaseComparer.Default); + } + + ItemVectorPartition itemVectorPartition = itemVectorCollection[itemVectorType]; + + ErrorUtilities.VerifyThrow(!itemVectorCollection[itemVectorType].ContainsKey(item), "ItemVectorPartition already contains a vector for items with the expression '{0}'", item); + itemVectorPartition[item] = itemVectorContents; + + ErrorUtilities.VerifyThrow((itemVectorTransforms == null) || (itemVectorCollection.Equals(itemVectorTransforms)) || (itemVectorPartition.Count == 1), + "If transforms have been separated out, there should only be one item vector per partition."); + } + } + else + { + // There was no item expression + discreteItems[item] = item; + } + } + } + + /// + /// Retrieves the item-specs of all items in the given item vector collection. + /// + /// + /// list of item-specs + private static List GetItemSpecsFromItemVectors(ItemVectorPartitionCollection itemVectors) + { + List itemSpecs = new List(); + + foreach (string itemType in itemVectors.Keys) + { + itemSpecs.AddRange(GetItemSpecsFromItemVectors(itemVectors, itemType)); + } + + return itemSpecs; + } + + /// + /// Retrieves the item-specs of all items of the specified type in the given item vector collection. + /// + /// + /// + /// list of item-specs + private static List GetItemSpecsFromItemVectors(ItemVectorPartitionCollection itemVectors, string itemType) + { + List itemSpecs = new List(); + + ItemVectorPartition itemVectorPartition = itemVectors[itemType]; + + if (itemVectorPartition != null) + { + foreach (IList items in itemVectorPartition.Values) + { + foreach (ProjectItemInstance item in items) + { + // The item can be null in the case of an item transform. + // eg., @(Compile->'%(NonExistentMetadata)') + // Nevertheless, include these, so that correlation can still occur. + itemSpecs.Add((item == null) ? null : ((IItem)item).EvaluatedIncludeEscaped); + } + } + } + + return itemSpecs; + } + + /// + /// Finds the differences in the keys between the two given hashtables. + /// + /// + /// + /// + /// + /// + private static void DiffHashtables(IDictionary h1, IDictionary h2, out List commonKeys, out List uniqueKeysInH1, out List uniqueKeysInH2) where K : class, IEquatable where V : class + { + commonKeys = new List(); + uniqueKeysInH1 = new List(); + uniqueKeysInH2 = new List(); + + foreach (K h1Key in h1.Keys) + { + if (h2.ContainsKey(h1Key)) + { + commonKeys.Add(h1Key); + } + else + { + uniqueKeysInH1.Add(h1Key); + } + } + + foreach (K h2Key in h2.Keys) + { + if (!h1.ContainsKey(h2Key)) + { + uniqueKeysInH2.Add(h2Key); + } + } + } + + /// + /// Compares the set of files/directories designated as "inputs" against the set of files/directories designated as + /// "outputs", and indicates if any "output" file/directory is out-of-date w.r.t. any "input" file/directory. + /// + /// + /// NOTE: Internal for unit test purposes only. + /// + /// + /// + /// true, if any "input" is newer than any "output", or if any input or output does not exist. + internal static bool IsAnyOutOfDate(out DependencyAnalysisLogDetail dependencyAnalysisDetailEntry, string projectDirectory, IList inputs, IList outputs) + { + ErrorUtilities.VerifyThrow((inputs.Count > 0) && (outputs.Count > 0), "Need to specify inputs and outputs."); + if (inputs.Count > 0) + { + ErrorUtilities.VerifyThrow(inputs[0] is string || inputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + } + + if (outputs.Count > 0) + { + ErrorUtilities.VerifyThrow(outputs[0] is string || outputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + } + + // Algorithm: walk through all the outputs to find the oldest output + // walk through the inputs as far as we need to until we find one that's newer (if any) + + // PERF -- we could change this to ensure that we walk the shortest list first (because we walk that one entirely): + // possibly the outputs list isn't actually the shortest list. However it always is the shortest + // in the cases I've seen, and adding this optimization would make the code hard to read. + + string oldestOutput = EscapingUtilities.UnescapeAll(outputs[0].ToString()); + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(outputs[0]); + + DateTime oldestOutputFileTime = DateTime.MinValue; + try + { + string oldestOutputFullPath = Path.Combine(projectDirectory, oldestOutput); + oldestOutputFileTime = NativeMethodsShared.GetLastWriteFileUtcTime(oldestOutputFullPath); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + // Output does not exist + oldestOutputFileTime = DateTime.MinValue; + } + + if (oldestOutputFileTime == DateTime.MinValue) + { + // First output is missing: we must build the target + string arbitraryInput = EscapingUtilities.UnescapeAll(inputs[0].ToString()); + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(inputs[0]); + dependencyAnalysisDetailEntry = new DependencyAnalysisLogDetail(arbitraryInput, oldestOutput, null, null, OutofdateReason.MissingOutput); + return true; + } + + for (int i = 1; i < outputs.Count; i++) + { + string candidateOutput = EscapingUtilities.UnescapeAll(outputs[i].ToString()); + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(outputs[i]); + DateTime candidateOutputFileTime = DateTime.MinValue; + try + { + string candidateOutputFullPath = Path.Combine(projectDirectory, candidateOutput); + candidateOutputFileTime = NativeMethodsShared.GetLastWriteFileUtcTime(candidateOutputFullPath); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + // Output does not exist + candidateOutputFileTime = DateTime.MinValue; + } + + if (candidateOutputFileTime == DateTime.MinValue) + { + // An output is missing: we must build the target + string arbitraryInput = EscapingUtilities.UnescapeAll(inputs[0].ToString()); + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(inputs[0]); + dependencyAnalysisDetailEntry = new DependencyAnalysisLogDetail(arbitraryInput, candidateOutput, null, null, OutofdateReason.MissingOutput); + return true; + } + + if (oldestOutputFileTime > candidateOutputFileTime) + { + // This output is older than the previous record holder + oldestOutputFileTime = candidateOutputFileTime; + oldestOutput = candidateOutput; + } + } + + // Now compare the oldest output with each input and break out if we find one newer. + foreach (T input in inputs) + { + string unescapedInput = EscapingUtilities.UnescapeAll(input.ToString()); + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(input); + DateTime inputFileTime = DateTime.MaxValue; + try + { + string unescapedInputFullPath = Path.Combine(projectDirectory, unescapedInput); + inputFileTime = NativeMethodsShared.GetLastWriteFileUtcTime(unescapedInputFullPath); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + // Output does not exist + inputFileTime = DateTime.MinValue; + } + + if (inputFileTime == DateTime.MinValue) + { + // An input is missing: we must build the target + dependencyAnalysisDetailEntry = new DependencyAnalysisLogDetail(unescapedInput, oldestOutput, null, null, OutofdateReason.MissingInput); + return true; + } + else + { + if (inputFileTime > oldestOutputFileTime) + { + // This input is newer than the oldest output: we must build the target + dependencyAnalysisDetailEntry = new DependencyAnalysisLogDetail(unescapedInput, oldestOutput, null, null, OutofdateReason.NewerInput); + return true; + } + } + } + + // All exist and no inputs are newer than any outputs; up to date + dependencyAnalysisDetailEntry = null; + return false; + } + + + /// + /// Record the unique input and output files so that the "up to date" message + /// can list them in the log later. + /// + private void RecordUniqueInputsAndOutputs(IList inputs, IList outputs) + { + if (inputs.Count > 0) + { + ErrorUtilities.VerifyThrow(inputs[0] is string || inputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + } + + if (outputs.Count > 0) + { + ErrorUtilities.VerifyThrow(outputs[0] is string || outputs[0] is ProjectItemInstance, "Must be either string or ProjectItemInstance"); + } + + // Only if we are not logging just critical events should we be gathering full details + if (!_loggingService.OnlyLogCriticalEvents) + { + foreach (T input in inputs) + { + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(input); + if (!_uniqueTargetInputs.ContainsKey(input.ToString())) + { + _uniqueTargetInputs.Add(input.ToString(), null); + } + } + foreach (T output in outputs) + { + ErrorUtilities.ThrowIfTypeDoesNotImplementToString(output); + if (!_uniqueTargetOutputs.ContainsKey(output.ToString())) + { + _uniqueTargetOutputs.Add(output.ToString(), null); + } + } + } + } + /// + /// Compares the file/directory designated as "input" against the file/directory designated as "output", and indicates if + /// the "output" file/directory is out-of-date w.r.t. the "input" file/directory. + /// + /// + /// If the "input" does not exist on disk, we treat its disappearance as a change, and consider the "input" to be newer + /// than the "output", regardless of whether the "output" itself exists. + /// + /// + /// + /// + /// + /// true, if "input" is newer than "output" + private bool IsOutOfDate(string input, string output, string inputItemName, string outputItemName) + { + bool inputDoesNotExist; + bool outputDoesNotExist; + input = EscapingUtilities.UnescapeAll(input); + output = EscapingUtilities.UnescapeAll(output); + bool outOfDate = (CompareLastWriteTimes(input, output, out inputDoesNotExist, out outputDoesNotExist) == 1) || inputDoesNotExist; + + // Only if we are not logging just critical events should we be gathering full details + if (!_loggingService.OnlyLogCriticalEvents) + { + // Make a note of unique inputs + if (!_uniqueTargetInputs.ContainsKey(input)) + { + _uniqueTargetInputs.Add(input, null); + } + + // Make a note of unique outputs + if (!_uniqueTargetOutputs.ContainsKey(output)) + { + _uniqueTargetOutputs.Add(output, null); + } + } + + RecordComparisonResults(input, output, inputItemName, outputItemName, inputDoesNotExist, outputDoesNotExist, outOfDate); + + return outOfDate; + } + + /// + /// Add timestamp comparison results to a list, to log them together later. + /// + private void RecordComparisonResults(string input, string output, string inputItemName, string outputItemName, bool inputDoesNotExist, bool outputDoesNotExist, bool outOfDate) + { + // Only if we are not logging just critical events should we be gathering full details + if (!_loggingService.OnlyLogCriticalEvents) + { + // Record the details of the out-of-date decision + if (inputDoesNotExist) + { + _dependencyAnalysisDetail.Add(new DependencyAnalysisLogDetail(input, output, inputItemName, outputItemName, OutofdateReason.MissingInput)); + } + else if (outputDoesNotExist) + { + _dependencyAnalysisDetail.Add(new DependencyAnalysisLogDetail(input, output, inputItemName, outputItemName, OutofdateReason.MissingOutput)); + } + else if (outOfDate) + { + _dependencyAnalysisDetail.Add(new DependencyAnalysisLogDetail(input, output, inputItemName, outputItemName, OutofdateReason.NewerInput)); + } + } + } + + /// + /// Compares the last-write times of the given files/directories. + /// + /// + /// Existing files/directories are always considered newer than non-existent ones, and two non-existent files/directories + /// are considered to have the same last-write time. + /// + /// + /// + /// [out] indicates if the first file/directory does not exist on disk + /// [out] indicates if the second file/directory does not exist on disk + /// + /// -1 if the first file/directory is older than the second; + /// 0 if the files/directories were both last written to at the same time; + /// +1 if the first file/directory is newer than the second + /// + private int CompareLastWriteTimes(string path1, string path2, out bool path1DoesNotExist, out bool path2DoesNotExist) + { + ErrorUtilities.VerifyThrow((path1 != null) && (path1.Length > 0) && (path2 != null) && (path2.Length > 0), + "Need to specify paths to compare."); + + FileInfo path1Info = null; + try + { + path1 = Path.Combine(_project.Directory, path1); + path1Info = FileUtilities.GetFileInfoNoThrow(path1); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + path1Info = null; + } + + FileInfo path2Info = null; + try + { + path2 = Path.Combine(_project.Directory, path2); + path2Info = FileUtilities.GetFileInfoNoThrow(path2); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + path2Info = null; + } + + path1DoesNotExist = (path1Info == null); + path2DoesNotExist = (path2Info == null); + + if (path1DoesNotExist) + { + if (path2DoesNotExist) + { + // Neither exist + return 0; + } + else + { + // Only path 2 exists + return -1; + } + } + else if (path2DoesNotExist) + { + // Only path 1 exists + return +1; + } + + // Both exist + return DateTime.Compare(path1Info.LastWriteTime, path2Info.LastWriteTime); + } + + #endregion + + // the project whose target we are analyzing. + private ProjectInstance _project; + // the target to analyze + private ProjectTargetInstance _targetToAnalyze; + + // the value of the target's "Inputs" attribute + private string _targetInputSpecification; + // the value of the target's "Outputs" attribute + private string _targetOutputSpecification; + // Details of the dependency analysis for logging + private ArrayList _dependencyAnalysisDetail = new ArrayList(); + + // Engine logging service which to log message to + private ILoggingService _loggingService; + // Event context information where event is raised from + private BuildEventContext _buildEventContext; + + /// + /// By default we do not sort target inputs and outputs as it has significant perf impact. + /// But allow suites to enable this so they get consistent results. + /// + private static readonly bool s_sortInputsOutputs = (Environment.GetEnvironmentVariable("MSBUILDSORTINPUTSOUTPUTS") == "1"); + + /// + /// The unique target inputs. + /// + private IDictionary _uniqueTargetInputs = + (s_sortInputsOutputs ? (IDictionary)new SortedDictionary(StringComparer.OrdinalIgnoreCase) : (IDictionary)new Dictionary(StringComparer.OrdinalIgnoreCase)); + + /// + /// The unique target outputs. + /// + private IDictionary _uniqueTargetOutputs = + (s_sortInputsOutputs ? (IDictionary)new SortedDictionary(StringComparer.OrdinalIgnoreCase) : (IDictionary)new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + /// + /// Why TLDA decided this entry was out of date + /// + internal enum OutofdateReason + { + MissingInput, // The input file was missing + MissingOutput, // The output file was missing + NewerInput // The input file was newer + } + + /// + /// A logging detail entry. Describes what TLDA decided about inputs / outputs + /// + internal class DependencyAnalysisLogDetail + { + private OutofdateReason _reason; + private string _inputItemName; + private string _outputItemName; + private string _input; + private string _output; + + /// + /// The reason that we are logging this entry + /// + internal OutofdateReason Reason + { + get { return _reason; } + } + + /// + /// The input item name (can be null) + /// + public string InputItemName + { + get { return _inputItemName; } + } + + /// + /// The output item name (can be null) + /// + public string OutputItemName + { + get { return _outputItemName; } + } + + /// + /// The input file + /// + public string Input + { + get { return _input; } + } + + /// + /// The output file + /// + public string Output + { + get { return _output; } + } + + /// + /// Construct a log detail element + /// + /// Input file + /// Output file + /// Input item name (can be null) + /// Output item name (can be null) + /// The reason we are logging + public DependencyAnalysisLogDetail(string input, string output, string inputItemName, string outputItemName, OutofdateReason reason) + { + _reason = reason; + _inputItemName = inputItemName; + _outputItemName = outputItemName; + _input = input; + _output = output; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskBuilder.cs new file mode 100644 index 00000000000..389565e5d0e --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -0,0 +1,1119 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The object which executes tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; +using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The possible values for a task's ContinueOnError attribute. + /// + internal enum ContinueOnError + { + /// + /// If the task fails, error and stop. + /// + ErrorAndStop, + + /// + /// If the task fails, error and continue. + /// + ErrorAndContinue, + + /// + /// If the task fails, warn and continue. + /// + WarnAndContinue + } + + /// + /// The TaskBuilder is one of two components related to building tasks, the other being the TaskExecutionHost. The TaskBuilder is + /// responsible for all parts dealing with the XML/task declaration. It determines if the task is intrinsic or extrinsic, + /// looks up the task in the task registry, determines the task parameters and requests them to be set, and requests outputs + /// when task execution has been completed. It is not responsible for reflection over the task instance or anything which + /// requires dealing with the task instance directly - those actions are handled by the TaskExecutionHost. + /// + internal class TaskBuilder : ITaskBuilder, IBuildComponent + { + /// + /// The Build Request Entry for which this task is executing. + /// + private BuildRequestEntry _buildRequestEntry; + + /// + /// The cancellation token + /// + private CancellationToken _cancellationToken; + + /// + /// The build component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// The original target child instance + /// + private ProjectTargetInstanceChild _targetChildInstance; + + /// + /// The task instance for extrinsic tasks + /// + private ProjectTaskInstance _taskNode; + + /// + /// Host callback for host-aware tasks. + /// + private ITaskHost _taskHostObject; + + /// + /// indicates whether to ignore task execution failures + /// + private ContinueOnError _continueOnError; + + /// + /// The logging context for the target in which we are executing. + /// + private TargetLoggingContext _targetLoggingContext; + + /// + /// Full path to the project, for errors + /// + private string _projectFullPath; + + /// + /// The target builder callback. + /// + private ITargetBuilderCallback _targetBuilderCallback; + + /// + /// The task execution host for in-proc tasks. + /// + private ITaskExecutionHost _taskExecutionHost; + + /// + /// The object used to synchronize access to the task execution host. + /// + private Object _taskExecutionHostSync = new Object(); + + /// + /// Constructor + /// + internal TaskBuilder() + { + } + + /// + /// Builds the task specified by the XML. + /// + /// The logging context of the target + /// The build request entry being built + /// The target builder callback. + /// The task instance. + /// The mode in which to execute tasks. + /// The lookup to be used for inference. + /// The lookup to be used during execution. + /// The result of running the task batch. + /// + /// The ExecuteTask method takes a task as specified by XML and executes it. This procedure is comprised + /// of the following steps: + /// 1. Loading the Task from its containing assembly by looking it up in the task registry + /// 2. Determining if the task is batched. If it is, create the batches and execute each as if it were a non-batched task + /// 3. If the task is not batched, execute it. + /// 4. If the task was batched, hold on to its Lookup until all of the natches are done, then merge them. + /// + public async Task ExecuteTask(TargetLoggingContext loggingContext, BuildRequestEntry requestEntry, ITargetBuilderCallback targetBuilderCallback, ProjectTargetInstanceChild taskInstance, TaskExecutionMode mode, Lookup inferLookup, Lookup executeLookup, CancellationToken cancellationToken) + { + ErrorUtilities.VerifyThrow(taskInstance != null, "Need to specify the task instance."); + + _buildRequestEntry = requestEntry; + _targetBuilderCallback = targetBuilderCallback; + _cancellationToken = cancellationToken; + _targetChildInstance = taskInstance; + + // In the case of Intrinsic tasks, taskNode will end up null. Currently this is how we distinguish + // intrinsic from extrinsic tasks. + _taskNode = taskInstance as ProjectTaskInstance; + + if (_taskNode != null && requestEntry.Request.HostServices != null) + { + _taskHostObject = requestEntry.Request.HostServices.GetHostObject(requestEntry.RequestConfiguration.Project.FullPath, loggingContext.Target.Name, _taskNode.Name); + } + + _projectFullPath = requestEntry.RequestConfiguration.Project.FullPath; + + // this.handleId = handleId; No handles + // this.parentModule = parentModule; No task execution module + _continueOnError = ContinueOnError.ErrorAndStop; + + _targetLoggingContext = loggingContext; + + WorkUnitResult taskResult = new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null); + if ((mode & TaskExecutionMode.InferOutputsOnly) == TaskExecutionMode.InferOutputsOnly) + { + taskResult = await ExecuteTask(TaskExecutionMode.InferOutputsOnly, inferLookup); + } + + if ((mode & TaskExecutionMode.ExecuteTaskAndGatherOutputs) == TaskExecutionMode.ExecuteTaskAndGatherOutputs) + { + taskResult = await ExecuteTask(TaskExecutionMode.ExecuteTaskAndGatherOutputs, executeLookup); + } + + return taskResult; + } + + #region IBuildComponent Members + + /// + /// Sets the build component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + _componentHost = host; + _taskExecutionHost = new TaskExecutionHost(host); + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + lock (_taskExecutionHostSync) + { + ErrorUtilities.VerifyThrow(_taskExecutionHost != null, "taskExecutionHost not initialized."); + _componentHost = null; + + IDisposable disposable = _taskExecutionHost as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + } + + _taskExecutionHost = null; + } + } + + #endregion + + /// + /// Class factory for component creation. + /// + internal static IBuildComponent CreateComponent(BuildComponentType type) + { + ErrorUtilities.VerifyThrow(type == BuildComponentType.TaskBuilder, "Cannot create components of type {0}", type); + return new TaskBuilder(); + } + + #region Methods + + /// + /// Build up a list of all parameters on the task, including those in any Output tags, + /// in order to find batchable metadata references + /// + /// The list of parameter values + private List CreateListOfParameterValues() + { + if (_taskNode == null) + { + // This is an intrinsic task. Batching is not handled here. + return new List(); + } + + List taskParameters = new List(_taskNode.ParametersForBuild.Count + _taskNode.Outputs.Count); + + foreach (KeyValuePair> taskParameter in _taskNode.ParametersForBuild) + { + taskParameters.Add(taskParameter.Value.Item1); + } + + // Add parameters on any output tags + foreach (ProjectTaskInstanceChild taskOutputSpecification in _taskNode.Outputs) + { + ProjectTaskOutputItemInstance outputItemInstance = taskOutputSpecification as ProjectTaskOutputItemInstance; + if (outputItemInstance != null) + { + taskParameters.Add(outputItemInstance.TaskParameter); + taskParameters.Add(outputItemInstance.ItemType); + } + + ProjectTaskOutputPropertyInstance outputPropertyInstance = taskOutputSpecification as ProjectTaskOutputPropertyInstance; + if (outputPropertyInstance != null) + { + taskParameters.Add(outputPropertyInstance.TaskParameter); + taskParameters.Add(outputPropertyInstance.PropertyName); + } + + if (!String.IsNullOrEmpty(taskOutputSpecification.Condition)) + { + taskParameters.Add(taskOutputSpecification.Condition); + } + } + + if (!String.IsNullOrEmpty(_taskNode.Condition)) + { + taskParameters.Add(_taskNode.Condition); + } + + if (!String.IsNullOrEmpty(_taskNode.ContinueOnError)) + { + taskParameters.Add(_taskNode.ContinueOnError); + } + + return taskParameters; + } + + /// + /// Called to execute a task within a target. This method instantiates the task, sets its parameters, and executes it. + /// + /// true, if successful + private async Task ExecuteTask(TaskExecutionMode mode, Lookup lookup) + { + ErrorUtilities.VerifyThrowArgumentNull(lookup, "lookup"); + + WorkUnitResult taskResult = new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null); + TaskHost taskHost = null; + + List buckets = null; + + try + { + if (_taskNode != null) + { + taskHost = new TaskHost(_componentHost, _buildRequestEntry, _targetChildInstance.Location, _targetBuilderCallback); + _taskExecutionHost.InitializeForTask(taskHost, _targetLoggingContext, _buildRequestEntry.RequestConfiguration.Project, _taskNode.Name, _taskNode.Location, _taskHostObject, _continueOnError != ContinueOnError.ErrorAndStop, taskHost.AppDomainSetup, taskHost.IsOutOfProc, _cancellationToken); + } + + List taskParameterValues = CreateListOfParameterValues(); + buckets = BatchingEngine.PrepareBatchingBuckets(taskParameterValues, lookup, _targetChildInstance.Location); + + Dictionary lookupHash = null; + + // Only create a hash table if there are more than one bucket as this is the only time a property can be overridden + if (buckets.Count > 1) + { + lookupHash = lookupHash ?? new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + } + + WorkUnitResult aggregateResult = new WorkUnitResult(); + + // Loop through each of the batch buckets and execute them one at a time + for (int i = 0; i < buckets.Count; i++) + { + // Execute the batch bucket, pass in which bucket we are executing so that we know when to get a new taskId for the bucket. + taskResult = await ExecuteBucket(taskHost, (ItemBucket)buckets[i], mode, lookupHash); + + aggregateResult = aggregateResult.AggregateResult(taskResult); + + if (aggregateResult.ActionCode == WorkUnitActionCode.Stop) + { + break; + } + } + + taskResult = aggregateResult; + } + finally + { + _taskExecutionHost.CleanupForTask(); + + if (taskHost != null) + { + taskHost.MarkAsInactive(); + } + + // Now all task batches are done, apply all item adds to the outer + // target batch; we do this even if the task wasn't found (in that case, + // no items or properties will have been added to the scope) + if (buckets != null) + { + foreach (ItemBucket bucket in buckets) + { + bucket.LeaveScope(); + } + } + } + + return taskResult; + } + + /// + /// Execute a single bucket + /// + /// true if execution succeeded + private async Task ExecuteBucket(TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask, Dictionary lookupHash) + { + // On Intrinsic tasks, we do not allow batchable params, therefore metadata is excluded. + ParserOptions parserOptions = (_taskNode == null) ? ParserOptions.AllowPropertiesAndItemLists : ParserOptions.AllowAll; + WorkUnitResult taskResult = new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null); + + bool condition = ConditionEvaluator.EvaluateCondition + ( + _targetChildInstance.Condition, + parserOptions, + bucket.Expander, + ExpanderOptions.ExpandAll, + _buildRequestEntry.ProjectRootDirectory, + _targetChildInstance.ConditionLocation, + _targetLoggingContext.LoggingService, + _targetLoggingContext.BuildEventContext + ); + + if (!condition) + { + LogSkippedTask(bucket, howToExecuteTask); + taskResult = new WorkUnitResult(WorkUnitResultCode.Skipped, WorkUnitActionCode.Continue, null); + + return taskResult; + } + + // If this is an Intrinsic task, it gets handled in a special fashion. + if (_taskNode == null) + { + ExecuteIntrinsicTask(bucket); + taskResult = new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null); + } + else + { + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + // Change to the project root directory. + // If that directory does not exist, do nothing. (Do not check first as it is almost always there and it is slow) + // This is because if the project has not been saved, this directory may not exist, yet it is often useful to still be able to build the project. + // No errors are masked by doing this: errors loading the project from disk are reported at load time, if necessary. + NativeMethodsShared.SetCurrentDirectory(_buildRequestEntry.ProjectRootDirectory); + } + + if (howToExecuteTask == TaskExecutionMode.ExecuteTaskAndGatherOutputs) + { + // We need to find the task before logging the task started event so that the using task statement comes before the task started event + IDictionary taskIdentityParameters = GatherTaskIdentityParameters(bucket.Expander); + TaskRequirements? requirements = _taskExecutionHost.FindTask(taskIdentityParameters); + if (requirements != null) + { + TaskLoggingContext taskLoggingContext = _targetLoggingContext.LogTaskBatchStarted(_projectFullPath, _targetChildInstance); + try + { + if ( + ((requirements.Value & TaskRequirements.RequireSTAThread) == TaskRequirements.RequireSTAThread) && + (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + ) + { + taskResult = ExecuteTaskInSTAThread(bucket, taskLoggingContext, taskIdentityParameters, taskHost, howToExecuteTask); + } + else + { + taskResult = await InitializeAndExecuteTask(taskLoggingContext, bucket, taskIdentityParameters, taskHost, howToExecuteTask); + } + + if (lookupHash != null) + { + List overrideMessages = bucket.Lookup.GetPropertyOverrideMessages(lookupHash); + if (overrideMessages != null) + { + foreach (string s in overrideMessages) + { + taskLoggingContext.LogCommentFromText(MessageImportance.Low, s); + } + } + } + } + catch (InvalidProjectFileException e) + { + // Make sure the Invalid Project error gets logged *before* TaskFinished. Otherwise, + // the log is confusing. + taskLoggingContext.LogInvalidProjectFileError(e); + _continueOnError = ContinueOnError.ErrorAndStop; + } + finally + { + // Flag the completion of the task. + taskLoggingContext.LogTaskBatchFinished(_projectFullPath, taskResult.ResultCode == WorkUnitResultCode.Success || taskResult.ResultCode == WorkUnitResultCode.Skipped); + + if (taskResult.ResultCode == WorkUnitResultCode.Failed && _continueOnError == ContinueOnError.WarnAndContinue) + { + // We coerce the failing result to a successful result. + taskResult = new WorkUnitResult(WorkUnitResultCode.Success, taskResult.ActionCode, taskResult.Exception); + } + } + } + } + else + { + ErrorUtilities.VerifyThrow(howToExecuteTask == TaskExecutionMode.InferOutputsOnly, "should be inferring"); + + ErrorUtilities.VerifyThrow + ( + GatherTaskOutputs(null, howToExecuteTask, bucket), + "The method GatherTaskOutputs() should never fail when inferring task outputs." + ); + + if (lookupHash != null) + { + List overrideMessages = bucket.Lookup.GetPropertyOverrideMessages(lookupHash); + if (overrideMessages != null) + { + foreach (string s in overrideMessages) + { + _targetLoggingContext.LogCommentFromText(MessageImportance.Low, s); + } + } + } + + taskResult = new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null); + } + } + + return taskResult; + } + + /// + /// Returns the set of parameters that can contribute to a task's identity, and their values for this particular task. + /// + private IDictionary GatherTaskIdentityParameters(Expander expander) + { + ErrorUtilities.VerifyThrowInternalNull(_taskNode, "taskNode"); // taskNode should never be null when we're calling this method. + + string msbuildArchitecture = expander.ExpandIntoStringAndUnescape(_taskNode.MSBuildArchitecture ?? String.Empty, ExpanderOptions.ExpandAll, _taskNode.MSBuildArchitectureLocation ?? ElementLocation.EmptyLocation); + string msbuildRuntime = expander.ExpandIntoStringAndUnescape(_taskNode.MSBuildRuntime ?? String.Empty, ExpanderOptions.ExpandAll, _taskNode.MSBuildRuntimeLocation ?? ElementLocation.EmptyLocation); + + IDictionary taskIdentityParameters = null; + + // only bother to create a task identity parameter set if we're putting anything in there -- otherwise, + // a null set will be treated as equivalent to all parameters being "don't care". + if (msbuildRuntime != String.Empty || msbuildArchitecture != String.Empty) + { + taskIdentityParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + msbuildArchitecture = msbuildArchitecture == String.Empty ? XMakeAttributes.MSBuildArchitectureValues.any : msbuildArchitecture.Trim(); + msbuildRuntime = msbuildRuntime == String.Empty ? XMakeAttributes.MSBuildRuntimeValues.any : msbuildRuntime.Trim(); + + taskIdentityParameters.Add(XMakeAttributes.runtime, msbuildRuntime); + taskIdentityParameters.Add(XMakeAttributes.architecture, msbuildArchitecture); + } + + return taskIdentityParameters; + } + + /// + /// Executes the task using an STA thread. + /// + /// + /// STA thread launching also being used in XMakeCommandLine\OutOfProcTaskAppDomainWrapperBase.cs, InstantiateAndExecuteTaskInSTAThread method. + /// Any bug fixes made to this code, please ensure that you also fix that code. + /// + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is caught and rethrown in the correct thread.")] + private WorkUnitResult ExecuteTaskInSTAThread(ItemBucket bucket, TaskLoggingContext taskLoggingContext, IDictionary taskIdentityParameters, TaskHost taskHost, TaskExecutionMode howToExecuteTask) + { + WorkUnitResult taskResult = new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null); + Thread staThread = null; + Exception exceptionFromExecution = null; + ManualResetEvent taskRunnerFinished = new ManualResetEvent(false); + try + { + ThreadStart taskRunnerDelegate = delegate () + { + Lookup.Scope scope = bucket.Lookup.EnterScope("STA Thread for Task"); + try + { + taskResult = InitializeAndExecuteTask(taskLoggingContext, bucket, taskIdentityParameters, taskHost, howToExecuteTask).Result; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + exceptionFromExecution = e; + } + finally + { + scope.LeaveScope(); + taskRunnerFinished.Set(); + } + }; + + staThread = new Thread(taskRunnerDelegate); + staThread.SetApartmentState(ApartmentState.STA); + staThread.Name = "MSBuild STA task runner thread"; + staThread.CurrentCulture = _componentHost.BuildParameters.Culture; + staThread.CurrentUICulture = _componentHost.BuildParameters.UICulture; + staThread.Start(); + + // TODO: Why not just Join on the thread??? + taskRunnerFinished.WaitOne(); + } + finally + { + taskRunnerFinished.Close(); + taskRunnerFinished = null; + } + + if (exceptionFromExecution != null) + { + // Unfortunately this will reset the callstack + throw exceptionFromExecution; + } + + return taskResult; + } + + /// + /// Logs a task skipped message if necessary. + /// + private void LogSkippedTask(ItemBucket bucket, TaskExecutionMode howToExecuteTask) + { + // If this is an Intrinsic task, it does not log skips. + if (_taskNode != null) + { + if (howToExecuteTask == TaskExecutionMode.ExecuteTaskAndGatherOutputs) + { + if (!_targetLoggingContext.LoggingService.OnlyLogCriticalEvents) + { + // Expand the expression for the Log. + string expanded = bucket.Expander.ExpandIntoStringAndUnescape(_targetChildInstance.Condition, ExpanderOptions.ExpandAll, _targetChildInstance.ConditionLocation); + + // Whilst we are within the processing of the task, we haven't actually started executing it, so + // our skip task message needs to be in the context of the target. However any errors should be reported + // at the point where the task appears in the project. + _targetLoggingContext.LogComment + ( + MessageImportance.Low, + "TaskSkippedFalseCondition", + _taskNode.Name, + _targetChildInstance.Condition, + expanded + ); + } + } + } + } + + /// + /// Runs an intrinsic task. + /// + private void ExecuteIntrinsicTask(ItemBucket bucket) + { + IntrinsicTask task = IntrinsicTask.InstantiateTask + ( + _targetChildInstance, + _targetLoggingContext, + _buildRequestEntry.RequestConfiguration.Project, + _taskExecutionHost.LogTaskInputs); + + task.ExecuteTask(bucket.Lookup); + } + + /// + /// Initializes and executes the task. + /// + private async Task InitializeAndExecuteTask(TaskLoggingContext taskLoggingContext, ItemBucket bucket, IDictionary taskIdentityParameters, TaskHost taskHost, TaskExecutionMode howToExecuteTask) + { + if (!_taskExecutionHost.InitializeForBatch(taskLoggingContext, bucket, taskIdentityParameters)) + { + ProjectErrorUtilities.ThrowInvalidProject(_targetChildInstance.Location, "TaskDeclarationOrUsageError", _taskNode.Name); + } + + try + { + // UNDONE: Move this and the task host. + taskHost.LoggingContext = taskLoggingContext; + WorkUnitResult executionResult = await ExecuteInstantiatedTask(_taskExecutionHost, taskLoggingContext, taskHost, bucket, howToExecuteTask); + + ErrorUtilities.VerifyThrow(executionResult != null, "Unexpected null execution result"); + + return executionResult; + } + finally + { + _taskExecutionHost.CleanupForBatch(); + } + } + + /// + /// Recomputes the task's "ContinueOnError" setting. + /// + /// The bucket being executed. + /// The task host to use. + /// + /// There are four possible values: + /// false - Error and stop if the task fails. + /// true - Warn and continue if the task fails. + /// ErrorAndContinue - Error and continue if the task fails. + /// WarnAndContinue - Same as true. + /// + private void UpdateContinueOnError(ItemBucket bucket, TaskHost taskHost) + { + string continueOnErrorAttribute = _taskNode.ContinueOnError; + _continueOnError = ContinueOnError.ErrorAndStop; + + if (_taskNode.ContinueOnErrorLocation != null) + { + string expandedValue = bucket.Expander.ExpandIntoStringAndUnescape(continueOnErrorAttribute, ExpanderOptions.ExpandAll, _taskNode.ContinueOnErrorLocation); // expand embedded item vectors after expanding properties and item metadata + try + { + if (String.Equals(XMakeAttributes.ContinueOnErrorValues.errorAndContinue, expandedValue, StringComparison.OrdinalIgnoreCase)) + { + _continueOnError = ContinueOnError.ErrorAndContinue; + } + else if (String.Equals(XMakeAttributes.ContinueOnErrorValues.warnAndContinue, expandedValue, StringComparison.OrdinalIgnoreCase)) + { + _continueOnError = ContinueOnError.WarnAndContinue; + } + else if (String.Equals(XMakeAttributes.ContinueOnErrorValues.errorAndStop, expandedValue, StringComparison.OrdinalIgnoreCase)) + { + _continueOnError = ContinueOnError.ErrorAndStop; + } + else + { + // if attribute doesn't exist, default to "false" + // otherwise, convert its value to a boolean + bool value = ConversionUtilities.ConvertStringToBool(expandedValue); + _continueOnError = value ? ContinueOnError.WarnAndContinue : ContinueOnError.ErrorAndStop; + } + } + catch (ArgumentException e) + { + // handle errors in string-->bool conversion + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _taskNode.ContinueOnErrorLocation, "InvalidContinueOnErrorAttribute", _taskNode.Name, e.Message); + } + } + + // We need to access an internal method of the EngineProxy in order to update the value + // of continueOnError that will be returned to the task when the task queries IBuildEngine for it + taskHost.ContinueOnError = (_continueOnError != ContinueOnError.ErrorAndStop); + taskHost.ConvertErrorsToWarnings = (_continueOnError == ContinueOnError.WarnAndContinue); + } + + /// + /// Execute a task object for a given bucket. + /// + /// The host used to execute the task. + /// The logging context. + /// The task host for the task. + /// The batching bucket + /// The task execution mode + /// The result of running the task. + private async Task ExecuteInstantiatedTask(ITaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask) + { + UpdateContinueOnError(bucket, taskHost); + + bool taskResult = false; + + WorkUnitResultCode resultCode = WorkUnitResultCode.Success; + WorkUnitActionCode actionCode = WorkUnitActionCode.Continue; + + if (!taskExecutionHost.SetTaskParameters(_taskNode.ParametersForBuild)) + { + // The task cannot be initialized. + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _targetChildInstance.Location, "TaskParametersError", _taskNode.Name, String.Empty); + } + else + { + bool taskReturned = false; + Exception taskException = null; + + // If this is the MSBuild task, we need to execute it's special internal method. + TaskExecutionHost host = taskExecutionHost as TaskExecutionHost; + Type taskType = host.TaskInstance.GetType(); + + try + { + if (taskType == typeof(MSBuild)) + { + MSBuild msbuildTask = host.TaskInstance as MSBuild; + ErrorUtilities.VerifyThrow(msbuildTask != null, "Unexpected MSBuild internal task."); + _targetBuilderCallback.EnterMSBuildCallbackState(); + + try + { + taskResult = await msbuildTask.ExecuteInternal(); + } + finally + { + _targetBuilderCallback.ExitMSBuildCallbackState(); + } + } + else if (taskType == typeof(CallTarget)) + { + CallTarget callTargetTask = host.TaskInstance as CallTarget; + taskResult = await callTargetTask.ExecuteInternal(); + } + else + { + using (FullTracking.Track(taskLoggingContext.TargetLoggingContext.Target.Name, _taskNode.Name, _buildRequestEntry.ProjectRootDirectory, _buildRequestEntry.RequestConfiguration.Project.PropertiesToBuildWith)) + { + taskResult = taskExecutionHost.Execute(); + } + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex) || (Environment.GetEnvironmentVariable("MSBUILDDONOTCATCHTASKEXCEPTIONS") == "1")) + { + throw; + } + + taskException = ex; + } + + if (taskException == null) + { + taskReturned = true; + + // Set the property "MSBuildLastTaskResult" to reflect whether the task succeeded or not. + // The main use of this is if ContinueOnError is true -- so that the next task can consult the result. + // So we want it to be "false" even if ContinueOnError is true. + // The constants "true" and "false" should NOT be localized. They become property values. + bucket.Lookup.SetProperty(ProjectPropertyInstance.Create(ReservedPropertyNames.lastTaskResult, taskResult ? "true" : "false", true/* may be reserved */, _buildRequestEntry.RequestConfiguration.Project.IsImmutable)); + } + else + { + Type type = taskException.GetType(); + + if (type == typeof(LoggerException)) + { + // if a logger has failed, abort immediately + // Polite logger failure + _continueOnError = ContinueOnError.ErrorAndStop; + + // Rethrow wrapped in order to avoid losing the callstack + throw new LoggerException(taskException.Message, taskException); + } + else if (type == typeof(InternalLoggerException)) + { + // Logger threw arbitrary exception + _continueOnError = ContinueOnError.ErrorAndStop; + InternalLoggerException ex = taskException as InternalLoggerException; + + // Rethrow wrapped in order to avoid losing the callstack + throw new InternalLoggerException(taskException.Message, taskException, ex.BuildEventArgs, ex.ErrorCode, ex.HelpKeyword, ex.InitializationException); + } + else if (type == typeof(ThreadAbortException)) + { + Thread.ResetAbort(); + _continueOnError = ContinueOnError.ErrorAndStop; + + // Cannot rethrow wrapped as ThreadAbortException is sealed and has no appropriate constructor + // Stack will be lost + throw taskException; + } + else if (type == typeof(BuildAbortedException)) + { + _continueOnError = ContinueOnError.ErrorAndStop; + + // Rethrow wrapped in order to avoid losing the callstack + throw new BuildAbortedException(taskException.Message, ((BuildAbortedException)taskException)); + } + else if (type == typeof(CircularDependencyException)) + { + _continueOnError = ContinueOnError.ErrorAndStop; + ProjectErrorUtilities.ThrowInvalidProject(taskLoggingContext.Task.Location, "CircularDependency", taskLoggingContext.TargetLoggingContext.Target.Name); + } + else if (type == typeof(InvalidProjectFileException)) + { + // Just in case this came out of a task, make sure it's not + // marked as having been logged. + InvalidProjectFileException ipex = (InvalidProjectFileException)taskException; + ipex.HasBeenLogged = false; + + if (_continueOnError != ContinueOnError.ErrorAndStop) + { + taskLoggingContext.LogInvalidProjectFileError(ipex); + taskLoggingContext.LogComment(MessageImportance.Normal, "ErrorConvertedIntoWarning"); + } + else + { + // Rethrow wrapped in order to avoid losing the callstack + throw new InvalidProjectFileException(ipex.Message, ipex); + } + } + else if (type == typeof(Exception) || type.IsSubclassOf(typeof(Exception))) + { + // Occasionally, when debugging a very uncommon task exception, it is useful to loop the build with + // a debugger attached to break on 2nd chance exceptions. + // That requires that there needs to be a way to not catch here, by setting an environment variable. + if (ExceptionHandling.IsCriticalException(taskException) || (Environment.GetEnvironmentVariable("MSBUILDDONOTCATCHTASKEXCEPTIONS") == "1")) + { + // Wrapping in an Exception will unfortunately mean that this exception would fly through any IsCriticalException above. + // However, we should not have any, also we should not have stashed such an exception anyway. + throw new Exception(taskException.Message, taskException); + } + + Exception exceptionToLog = taskException; + + if (exceptionToLog is TargetInvocationException) + { + exceptionToLog = exceptionToLog.InnerException; + } + + // handle any exception thrown by the task during execution + // NOTE: We catch ALL exceptions here, to attempt to completely isolate the Engine + // from failures in the task. + if (_continueOnError == ContinueOnError.WarnAndContinue) + { + taskLoggingContext.LogTaskWarningFromException + ( + new BuildEventFileInfo(_targetChildInstance.Location), + exceptionToLog, + _taskNode.Name + ); + + // Log a message explaining why we converted the previous error into a warning. + taskLoggingContext.LogComment(MessageImportance.Normal, "ErrorConvertedIntoWarning"); + } + else + { + taskLoggingContext.LogFatalTaskError + ( + new BuildEventFileInfo(_targetChildInstance.Location), + exceptionToLog, + _taskNode.Name + ); + } + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + + // If the task returned attempt to gather its outputs. If gathering outputs fails set the taskResults + // to false + if (taskReturned) + { + taskResult = GatherTaskOutputs(taskExecutionHost, howToExecuteTask, bucket) && taskResult; + } + + // If the taskResults are false look at ContinueOnError. If ContinueOnError=false (default) + // mark the taskExecutedSuccessfully=false. Otherwise let the task succeed but log a normal + // pri message that says this task is continuing because ContinueOnError=true + resultCode = taskResult ? WorkUnitResultCode.Success : WorkUnitResultCode.Failed; + actionCode = WorkUnitActionCode.Continue; + if (resultCode == WorkUnitResultCode.Failed) + { + if (_continueOnError == ContinueOnError.ErrorAndStop) + { + actionCode = WorkUnitActionCode.Stop; + } + else + { + // This is the ErrorAndContinue or WarnAndContinue case... + string settingString = "true"; + if (_taskNode.ContinueOnErrorLocation != null) + { + settingString = bucket.Expander.ExpandIntoStringAndUnescape(_taskNode.ContinueOnError, ExpanderOptions.ExpandAll, _taskNode.ContinueOnErrorLocation); // expand embedded item vectors after expanding properties and item metadata + } + + taskLoggingContext.LogComment + ( + MessageImportance.Normal, + "TaskContinuedDueToContinueOnError", + "ContinueOnError", + _taskNode.Name, + settingString + ); + + actionCode = WorkUnitActionCode.Continue; + } + } + } + + WorkUnitResult result = new WorkUnitResult(resultCode, actionCode, null); + + return result; + } + + /// + /// Gathers task outputs in two ways: + /// 1) Given an instantiated task that has finished executing, it extracts the outputs using .NET reflection. + /// 2) Otherwise, it parses the task's output specifications and (statically) infers the outputs. + /// + /// The task execution host. + /// The task execution mode + /// The bucket to which the task execution belongs. + /// true, if successful + private bool GatherTaskOutputs(ITaskExecutionHost taskExecutionHost, TaskExecutionMode howToExecuteTask, ItemBucket bucket) + { + bool gatheredTaskOutputsSuccessfully = true; + + foreach (ProjectTaskInstanceChild taskOutputSpecification in _taskNode.Outputs) + { + // if the task's outputs are supposed to be gathered + bool condition = ConditionEvaluator.EvaluateCondition + ( + taskOutputSpecification.Condition, + ParserOptions.AllowAll, + bucket.Expander, + ExpanderOptions.ExpandAll, + _buildRequestEntry.ProjectRootDirectory, + taskOutputSpecification.ConditionLocation, + _targetLoggingContext.LoggingService, + _targetLoggingContext.BuildEventContext + ); + + if (condition) + { + string taskParameterName = null; + bool outputTargetIsItem = false; + string outputTargetName = null; + + // check where the outputs are going -- into a vector, or a property? + ProjectTaskOutputItemInstance taskOutputItemInstance = taskOutputSpecification as ProjectTaskOutputItemInstance; + + if (taskOutputItemInstance != null) + { + // expand all embedded properties, item metadata and item vectors in the item type name + outputTargetIsItem = true; + outputTargetName = bucket.Expander.ExpandIntoStringAndUnescape(taskOutputItemInstance.ItemType, ExpanderOptions.ExpandAll, taskOutputItemInstance.ItemTypeLocation); + taskParameterName = taskOutputItemInstance.TaskParameter; + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + outputTargetName.Length > 0, + taskOutputItemInstance.ItemTypeLocation, + "InvalidEvaluatedAttributeValue", + outputTargetName, + taskOutputItemInstance.ItemType, + XMakeAttributes.itemName, + XMakeElements.output + ); + } + else + { + ProjectTaskOutputPropertyInstance taskOutputPropertyInstance = taskOutputSpecification as ProjectTaskOutputPropertyInstance; + outputTargetIsItem = false; + + // expand all embedded properties, item metadata and item vectors in the property name + outputTargetName = bucket.Expander.ExpandIntoStringAndUnescape(taskOutputPropertyInstance.PropertyName, ExpanderOptions.ExpandAll, taskOutputPropertyInstance.PropertyNameLocation); + taskParameterName = taskOutputPropertyInstance.TaskParameter; + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + outputTargetName.Length > 0, + taskOutputPropertyInstance.PropertyNameLocation, + "InvalidEvaluatedAttributeValue", + outputTargetName, + taskOutputPropertyInstance.PropertyName, + XMakeAttributes.propertyName, + XMakeElements.output + ); + } + + string unexpandedTaskParameterName = taskParameterName; + taskParameterName = bucket.Expander.ExpandIntoStringAndUnescape(taskParameterName, ExpanderOptions.ExpandAll, taskOutputSpecification.TaskParameterLocation); + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + taskParameterName.Length > 0, + taskOutputSpecification.TaskParameterLocation, + "InvalidEvaluatedAttributeValue", + taskParameterName, + unexpandedTaskParameterName, + XMakeAttributes.taskParameter, + XMakeElements.output + ); + + // if we're gathering outputs by .NET reflection + if (howToExecuteTask == TaskExecutionMode.ExecuteTaskAndGatherOutputs) + { + gatheredTaskOutputsSuccessfully = taskExecutionHost.GatherTaskOutputs(taskParameterName, taskOutputSpecification.Location, outputTargetIsItem, outputTargetName); + } + else + { + // If we're inferring outputs based on information in the task and tags + ErrorUtilities.VerifyThrow(howToExecuteTask == TaskExecutionMode.InferOutputsOnly, "should be inferring"); + + // UNDONE: Refactor this method to use the same flag/string paradigm we use above, rather than two strings and the task output spec. + InferTaskOutputs(bucket.Lookup, taskOutputSpecification, taskParameterName, outputTargetName, outputTargetName, bucket); + } + } + + if (!gatheredTaskOutputsSuccessfully) + { + break; + } + } + + return gatheredTaskOutputsSuccessfully; + } + + /// + /// Uses the given task output specification to (statically) infer the task's outputs. + /// + /// The lookup + /// The task output specification + /// The task parameter name + /// can be null + /// can be null + /// The bucket for the batch. + private void InferTaskOutputs + ( + Lookup lookup, + ProjectTaskInstanceChild taskOutputSpecification, + string taskParameterName, + string itemName, + string propertyName, + ItemBucket bucket + ) + { + string taskParameterAttribute = _taskNode.GetParameter(taskParameterName); + + if (null != taskParameterAttribute) + { + ProjectTaskOutputItemInstance taskItemInstance = taskOutputSpecification as ProjectTaskOutputItemInstance; + if (taskItemInstance != null) + { + // This is an output item. + // Expand only with properties first, so that expressions like Include="@(foo)" will transfer the metadata of the "foo" items as well, not just their item specs. + IList outputItemSpecs = bucket.Expander.ExpandIntoStringListLeaveEscaped(taskParameterAttribute, ExpanderOptions.ExpandPropertiesAndMetadata, taskItemInstance.TaskParameterLocation); + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(_buildRequestEntry.RequestConfiguration.Project, itemName); + + foreach (string outputItemSpec in outputItemSpecs) + { + ICollection items = bucket.Expander.ExpandIntoItemsLeaveEscaped(outputItemSpec, itemFactory, ExpanderOptions.ExpandItems, taskItemInstance.TaskParameterLocation); + + lookup.AddNewItemsOfItemType(itemName, items); + } + } + else + { + // This is an output property. + ProjectTaskOutputPropertyInstance taskPropertyInstance = (ProjectTaskOutputPropertyInstance)taskOutputSpecification; + + string taskParameterValue = bucket.Expander.ExpandIntoStringAndUnescape(taskParameterAttribute, ExpanderOptions.ExpandAll, taskPropertyInstance.TaskParameterLocation); + + if (!String.IsNullOrEmpty(taskParameterValue)) + { + lookup.SetProperty(ProjectPropertyInstance.Create(propertyName, taskParameterValue, taskPropertyInstance.TaskParameterLocation, _buildRequestEntry.RequestConfiguration.Project.IsImmutable)); + } + } + } + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskHost.cs new file mode 100644 index 00000000000..16168b7fcc3 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -0,0 +1,898 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The object tasks use to interact with the rest of the build system. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.Remoting.Lifetime; +using System.Runtime.Remoting; +using System.Threading; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using System.Diagnostics; +using Microsoft.Build.Collections; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd.Components.Caching; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The task host object which allows tasks to interface with the rest of the build system. + /// Implementation of IBuildEngineX is thread-safe, so, for example, tasks can log concurrently on multiple threads. + /// + internal class TaskHost : MarshalByRefObject, IBuildEngine4 + { + /// + /// True if the "secret" environment variable MSBUILDNOINPROCNODE is set. + /// + private static bool s_onlyUseOutOfProcNodes = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; + + /// + /// Help diagnose tasks that log after they return. + /// + private static bool s_breakOnLogAfterTaskReturns = Environment.GetEnvironmentVariable("MSBUILDBREAKONLOGAFTERTASKRETURNS") == "1"; + + /// + /// The build component host + /// + private IBuildComponentHost _host; + + /// + /// The build request entry + /// + private BuildRequestEntry _requestEntry; + + /// + /// Location of the task node in the original file + /// + private ElementLocation _taskLocation; + + /// + /// The task logging context + /// + private TaskLoggingContext _taskLoggingContext; + + /// + /// True if the task connected to this proxy is alive + /// + private bool _activeProxy; + + /// + /// The callback used to invoke the target builder. + /// + private ITargetBuilderCallback _targetBuilderCallback; + + /// + /// This reference type is used to block access to a single entry methods of the interface + /// + private object _callbackMonitor; + + /// + /// A client sponsor is a class + /// which will respond to a lease renewal request and will + /// increase the lease time allowing the object to stay in memory + /// + private ClientSponsor _sponsor; + + /// + /// Legacy continue on error value per batch exposed via IBuildEngine + /// + private bool _continueOnError; + + /// + /// Flag indicating if errors should be converted to warnings. + /// + private bool _convertErrorsToWarnings; + + /// + /// The thread on which we yielded. + /// + private int _yieldThreadId = -1; + + /// + /// Constructor + /// + /// The component host + /// The build request entry + public TaskHost(IBuildComponentHost host, BuildRequestEntry requestEntry, ElementLocation taskLocation, ITargetBuilderCallback targetBuilderCallback) + { + ErrorUtilities.VerifyThrowArgumentNull(host, "host"); + ErrorUtilities.VerifyThrowArgumentNull(requestEntry, "requestEntry"); + ErrorUtilities.VerifyThrowInternalNull(taskLocation, "taskLocation"); + + _host = host; + _requestEntry = requestEntry; + _taskLocation = taskLocation; + _targetBuilderCallback = targetBuilderCallback; + _continueOnError = false; + _activeProxy = true; + _callbackMonitor = new Object(); + } + + /// + /// Returns true in the multiproc case + /// + /// + /// If MSBUILDNOINPROCNODE is set, then even if there's only one node in the buildparameters, it will be an out-of-proc node. + /// + public bool IsRunningMultipleNodes + { + get + { + VerifyActiveProxy(); + return _host.BuildParameters.MaxNodeCount > 1 || s_onlyUseOutOfProcNodes; + } + } + + /// + /// Reflects the value of the ContinueOnError attribute. + /// + public bool ContinueOnError + { + get + { + VerifyActiveProxy(); + return _continueOnError; + } + + internal set + { + _continueOnError = value; + } + } + + /// + /// The line number this task is on + /// + public int LineNumberOfTaskNode + { + get + { + return _taskLocation.Line; + } + } + + /// + /// The column number this task is on + /// + public int ColumnNumberOfTaskNode + { + get + { + return _taskLocation.Column; + } + } + + /// + /// The project file this task is in. + /// Typically this is an imported .targets file. + /// Unfortunately the interface has shipped with a poor name, so we cannot change it. + /// + public string ProjectFileOfTaskNode + { + get + { + return _taskLocation.File; + } + } + + /// + /// Indicates whether or not errors should be converted to warnings. + /// + internal bool ConvertErrorsToWarnings + { + get { return _convertErrorsToWarnings; } + set { _convertErrorsToWarnings = value; } + } + + /// + /// Sets or retrieves the logging context + /// + internal TaskLoggingContext LoggingContext + { + [DebuggerStepThrough] + get + { return _taskLoggingContext; } + + [DebuggerStepThrough] + set + { _taskLoggingContext = value; } + } + + /// + /// For configuring child AppDomains. + /// + internal AppDomainSetup AppDomainSetup + { + get + { + return _host.BuildParameters.AppDomainSetup; + } + } + + /// + /// Whether or not this is out of proc. + /// + internal bool IsOutOfProc + { + get + { + return _host.BuildParameters.IsOutOfProc; + } + } + + #region IBuildEngine2 Members + + /// + /// Builds a single project file + /// Thread safe. + /// + /// The project file + /// The list of targets to build + /// The global properties to use + /// The outputs from the targets + /// The tools version to use + /// True on success, false otherwise. + public bool BuildProjectFile(string projectFileName, string[] targetNames, System.Collections.IDictionary globalProperties, System.Collections.IDictionary targetOutputs, string toolsVersion) + { + VerifyActiveProxy(); + return BuildProjectFilesInParallel + ( + new string[] { projectFileName }, + targetNames, + new IDictionary[] { globalProperties }, + new IDictionary[] { targetOutputs }, + new string[] { toolsVersion }, + true, + false + ); + } + + /// + /// Builds multiple project files in parallel. This is the method the old MSBuild task invokes. + /// Thread safe. + /// + /// The list of projects to build + /// The set of targets to build + /// The global properties to use for each project + /// The outputs for each target on each project + /// The tools versions to use + /// Whether to use the results cache + /// Whether to unload projects when we are done. + /// True on success, false otherwise. + public bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, System.Collections.IDictionary[] globalProperties, System.Collections.IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) + { + bool includeTargetOutputs = (targetOutputsPerProject != null); + + // If the caller supplies an array to put the target outputs in, it must have the same length as the array of project file names they provided, too. + // "MSB3094: "{2}" refers to {0} item(s), and "{3}" refers to {1} item(s). They must have the same number of items." + ErrorUtilities.VerifyThrowArgument((targetOutputsPerProject == null) || (projectFileNames.Length == targetOutputsPerProject.Length), "General.TwoVectorsMustHaveSameLength", projectFileNames.Length, targetOutputsPerProject.Length, "projectFileNames", "targetOutputsPerProject"); + + BuildEngineResult result = BuildProjectFilesInParallel(projectFileNames, targetNames, globalProperties, new List[projectFileNames.Length], toolsVersion, includeTargetOutputs); + + if (includeTargetOutputs) + { + // Copy results from result.TargetOutputsPerProject to targetOutputsPerProject + // We should always have the same number of entries - although an entry might be empty if a project failed. + ErrorUtilities.VerifyThrow(targetOutputsPerProject.Length == result.TargetOutputsPerProject.Count, "{0} != {1}", targetOutputsPerProject.Length, result.TargetOutputsPerProject.Count); + + for (int i = 0; i < targetOutputsPerProject.Length; i++) + { + if (targetOutputsPerProject[i] != null) + { + foreach (KeyValuePair output in result.TargetOutputsPerProject[i]) + { + targetOutputsPerProject[i].Add(output.Key, output.Value); + } + } + } + } + + return result.Result; + } + + #endregion + + #region IBuildEngine3 Members + + /// + /// Builds multiple project files in parallel. + /// Thread safe. + /// + /// The list of projects to build + /// The set of targets to build + /// The global properties to use for each project + /// The list of global properties to undefine + /// The tools versions to use + /// Should the target outputs be returned in teh BuildEngineResult + /// A structure containing the result of the build, success or failure and the list of target outputs per project + public BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, System.Collections.IDictionary[] globalProperties, IList[] undefineProperties, string[] toolsVersion, bool returnTargetOutputs) + { + lock (_callbackMonitor) + { + return BuildProjectFilesInParallelAsync(projectFileNames, targetNames, globalProperties, undefineProperties, toolsVersion, returnTargetOutputs).Result; + } + } + + /// + /// Requests to yield the node. + /// Thread safe, however Yield cannot be called unless the + /// last call to Yield or Reaquire was Reaquire. + /// + public void Yield() + { + lock (_callbackMonitor) + { + IRequestBuilderCallback builderCallback = _requestEntry.Builder as IRequestBuilderCallback; + ErrorUtilities.VerifyThrow(_yieldThreadId == -1, "Cannot call Yield() while yielding."); + _yieldThreadId = Thread.CurrentThread.ManagedThreadId; + builderCallback.Yield(); + } + } + + /// + /// Requests to reacquire the node. + /// Thread safe, however Reaquire cannot be called unless the + /// last call to Yield or Reaquire was Yield. + /// + public void Reacquire() + { + lock (_callbackMonitor) + { + IRequestBuilderCallback builderCallback = _requestEntry.Builder as IRequestBuilderCallback; + ErrorUtilities.VerifyThrow(_yieldThreadId != -1, "Cannot call Reacquire() before Yield()."); + ErrorUtilities.VerifyThrow(_yieldThreadId == Thread.CurrentThread.ManagedThreadId, "Cannot call Reacquire() on thread {0} when Yield() was called on thread {1}", Thread.CurrentThread.ManagedThreadId, _yieldThreadId); + builderCallback.Reacquire(); + _yieldThreadId = -1; + } + } + + #endregion + + #region IBuildEngine Members + + /// + /// Logs an error event for the current task + /// Thread safe. + /// + /// The event args + public void LogErrorEvent(Microsoft.Build.Framework.BuildErrorEventArgs e) + { + lock (_callbackMonitor) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + if (!_activeProxy) + { + // The task has been logging on another thread, typically + // because of logging a spawned process's output, and has + // not terminated this logging before it returned. This is common + // enough that we don't want to crash and break the entire build. But + // we don't have any good way to log it any more, as not only has this task + // finished, the whole build might have finished! The task author will + // just have to figure out that their task has a bug by themselves. + if (s_breakOnLogAfterTaskReturns) + { + Trace.Fail(String.Format(CultureInfo.CurrentUICulture, "Task at {0}, after already returning, attempted to log '{1}'", _taskLocation.ToString(), e.Message)); + } + + return; + } + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + if (_convertErrorsToWarnings) + { + // Convert the error into a warning. We do this because the whole point of + // ContinueOnError is that a project author expects that the task might fail, + // but wants to ignore the failures. This implies that we shouldn't be logging + // errors either, because you should never have a successful build with errors. + BuildWarningEventArgs warningEvent = new BuildWarningEventArgs + ( + e.Subcategory, + e.Code, + e.File, + e.LineNumber, + e.ColumnNumber, + e.EndLineNumber, + e.EndColumnNumber, + e.Message, + e.HelpKeyword, + e.SenderName + ); + + warningEvent.BuildEventContext = _taskLoggingContext.BuildEventContext; + _taskLoggingContext.LoggingService.LogBuildEvent(warningEvent); + + // Log a message explaining why we converted the previous error into a warning. + _taskLoggingContext.LoggingService.LogComment(_taskLoggingContext.BuildEventContext, MessageImportance.Normal, "ErrorConvertedIntoWarning"); + } + else + { + e.BuildEventContext = _taskLoggingContext.BuildEventContext; + _taskLoggingContext.LoggingService.LogBuildEvent(e); + } + } + } + + /// + /// Logs a warning event for the current task + /// Thread safe. + /// + /// The event args + public void LogWarningEvent(Microsoft.Build.Framework.BuildWarningEventArgs e) + { + lock (_callbackMonitor) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + if (!_activeProxy) + { + // The task has been logging on another thread, typically + // because of logging a spawned process's output, and has + // not terminated this logging before it returned. This is common + // enough that we don't want to crash and break the entire build. But + // we don't have any good way to log it any more, as not only has this task + // finished, the whole build might have finished! The task author will + // just have to figure out that their task has a bug by themselves. + if (s_breakOnLogAfterTaskReturns) + { + Trace.Fail(String.Format(CultureInfo.CurrentUICulture, "Task at {0}, after already returning, attempted to log '{1}'", _taskLocation.ToString(), e.Message)); + } + + return; + } + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _taskLoggingContext.BuildEventContext; + _taskLoggingContext.LoggingService.LogBuildEvent(e); + } + } + + /// + /// Logs a message event for the current task + /// Thread safe. + /// + /// The event args + public void LogMessageEvent(Microsoft.Build.Framework.BuildMessageEventArgs e) + { + lock (_callbackMonitor) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + if (!_activeProxy) + { + // The task has been logging on another thread, typically + // because of logging a spawned process's output, and has + // not terminated this logging before it returned. This is common + // enough that we don't want to crash and break the entire build. But + // we don't have any good way to log it any more, as not only has this task + // finished, the whole build might have finished! The task author will + // just have to figure out that their task has a bug by themselves. + if (s_breakOnLogAfterTaskReturns) + { + Trace.Fail(String.Format(CultureInfo.CurrentUICulture, "Task at {0}, after already returning, attempted to log '{1}'", _taskLocation.ToString(), e.Message)); + } + + return; + } + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _taskLoggingContext.BuildEventContext; + _taskLoggingContext.LoggingService.LogBuildEvent(e); + } + } + + /// + /// Logs a custom event for the current task + /// Thread safe. + /// + /// The event args + public void LogCustomEvent(Microsoft.Build.Framework.CustomBuildEventArgs e) + { + lock (_callbackMonitor) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + + if (!_activeProxy) + { + // The task has been logging on another thread, typically + // because of logging a spawned process's output, and has + // not terminated this logging before it returned. This is common + // enough that we don't want to crash and break the entire build. But + // we don't have any good way to log it any more, as not only has this task + // finished, the whole build might have finished! The task author will + // just have to figure out that their task has a bug by themselves. + if (s_breakOnLogAfterTaskReturns) + { + Trace.Fail(String.Format(CultureInfo.CurrentUICulture, "Task at {0}, after already returning, attempted to log '{1}'", _taskLocation.ToString(), e.Message)); + } + + return; + } + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _taskLoggingContext.BuildEventContext; + _taskLoggingContext.LoggingService.LogBuildEvent(e); + } + } + + /// + /// Builds a single project file + /// Thread safe. + /// + /// The project file name + /// The set of targets to build. + /// The global properties to use + /// The outputs from the targets + /// True on success, false otherwise. + public bool BuildProjectFile(string projectFileName, string[] targetNames, System.Collections.IDictionary globalProperties, System.Collections.IDictionary targetOutputs) + { + VerifyActiveProxy(); + return BuildProjectFile(projectFileName, targetNames, globalProperties, targetOutputs, null); + } + + #endregion + + #region IBuildEngine4 Members + + /// + /// Disposes of all of the objects with the specified lifetime. + /// + public void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection) + { + var objectCache = (IRegisteredTaskObjectCache)_host.GetComponent(BuildComponentType.RegisteredTaskObjectCache); + objectCache.RegisterTaskObject(key, obj, lifetime, allowEarlyCollection); + } + + /// + /// Gets a previously registered task object. + /// + public object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + var objectCache = (IRegisteredTaskObjectCache)_host.GetComponent(BuildComponentType.RegisteredTaskObjectCache); + return objectCache.GetRegisteredTaskObject(key, lifetime); + } + + /// + /// Unregisters a task object. + /// + public object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + var objectCache = (IRegisteredTaskObjectCache)_host.GetComponent(BuildComponentType.RegisteredTaskObjectCache); + return objectCache.UnregisterTaskObject(key, lifetime); + } + + #endregion + + /// + /// Called by the internal MSBuild task. + /// Does not take the lock because it is called by another request builder thread. + /// + public async Task InternalBuildProjects(string[] projectFileNames, string[] targetNames, System.Collections.IDictionary[] globalProperties, IList[] undefineProperties, string[] toolsVersion, bool returnTargetOutputs) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFileNames, "projectFileNames"); + ErrorUtilities.VerifyThrowArgumentNull(globalProperties, "globalProperties"); + VerifyActiveProxy(); + + BuildEngineResult result; + if (projectFileNames.Length == 1 && projectFileNames[0] == null && globalProperties[0] == null && (undefineProperties == null || undefineProperties[0] == null) && toolsVersion[0] == null) + { + bool overallSuccess = true; + List> targetOutputsPerProject = null; + + // This is really a legacy CallTarget invocation + ITargetResult[] results = await _targetBuilderCallback.LegacyCallTarget(targetNames, ContinueOnError, _taskLocation); + + if (returnTargetOutputs) + { + targetOutputsPerProject = new List>(1); + targetOutputsPerProject.Add(new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + for (int i = 0; i < targetNames.Length; i++) + { + targetOutputsPerProject[0][targetNames[i]] = results[i].Items; + if (results[i].ResultCode == TargetResultCode.Failure) + { + overallSuccess = false; + } + } + + result = new BuildEngineResult(overallSuccess, targetOutputsPerProject); + } + else + { + // Post the request, then yield up the thread. + result = await BuildProjectFilesInParallelAsync(projectFileNames, targetNames, globalProperties, undefineProperties, toolsVersion, true /* ask that target outputs are returned in the buildengineresult */); + } + + return result; + } + + /// + /// InitializeLifetimeService is called when the remote object is activated. + /// This method will determine how long the lifetime for the object will be. + /// + /// The lease object to control this object's lifetime. + public override object InitializeLifetimeService() + { + lock (_callbackMonitor) + { + VerifyActiveProxy(); + + // Each MarshalByRef object has a reference to the service which + // controls how long the remote object will stay around + ILease lease = (ILease)base.InitializeLifetimeService(); + + // Set how long a lease should be initially. Once a lease expires + // the remote object will be disconnected and it will be marked as being availiable + // for garbage collection + int initialLeaseTime = 1; + + string initialLeaseTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDENGINEPROXYINITIALLEASETIME"); + + if (!String.IsNullOrEmpty(initialLeaseTimeFromEnvironment)) + { + int leaseTimeFromEnvironment; + if (int.TryParse(initialLeaseTimeFromEnvironment, out leaseTimeFromEnvironment) && leaseTimeFromEnvironment > 0) + { + initialLeaseTime = leaseTimeFromEnvironment; + } + } + + lease.InitialLeaseTime = TimeSpan.FromMinutes(initialLeaseTime); + + // Make a new client sponsor. A client sponsor is a class + // which will respond to a lease renewal request and will + // increase the lease time allowing the object to stay in memory + _sponsor = new ClientSponsor(); + + // When a new lease is requested lets make it last 1 minutes longer. + int leaseExtensionTime = 1; + + string leaseExtensionTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDENGINEPROXYLEASEEXTENSIONTIME"); + if (!String.IsNullOrEmpty(leaseExtensionTimeFromEnvironment)) + { + int leaseExtensionFromEnvironment; + if (int.TryParse(leaseExtensionTimeFromEnvironment, out leaseExtensionFromEnvironment) && leaseExtensionFromEnvironment > 0) + { + leaseExtensionTime = leaseExtensionFromEnvironment; + } + } + + _sponsor.RenewalTime = TimeSpan.FromMinutes(leaseExtensionTime); + + // Register the sponsor which will increase lease timeouts when the lease expires + lease.Register(_sponsor); + + return lease; + } + } + + /// + /// Indicates to the TaskHost that it is no longer needed. + /// Called by TaskBuilder when the task using the EngineProxy is done. + /// + internal void MarkAsInactive() + { + lock (_callbackMonitor) + { + VerifyActiveProxy(); + _activeProxy = false; + + // Since the task has a pointer to this class it may store it in a static field. Null out + // internal data so the leak of this object doesn't lead to a major memory leak. + _host = null; + _requestEntry = null; + + // Don't bother clearing the tiny task location + _taskLoggingContext = null; + _targetBuilderCallback = null; + + // Clear out the sponsor (who is responsible for keeping the EngineProxy remoting lease alive until the task is done) + // this will be null if the engineproxy was never sent across an appdomain boundry. + if (_sponsor != null) + { + ILease lease = (ILease)RemotingServices.GetLifetimeService(this); + + if (lease != null) + { + lease.Unregister(_sponsor); + } + + _sponsor.Close(); + _sponsor = null; + } + } + } + + /// + /// Determine if the event is serializable. If we are running with multiple nodes we need to make sure the logging events are serializable. If not + /// we need to log a warning. + /// + internal bool IsEventSerializable(BuildEventArgs e) + { + if (!e.GetType().IsSerializable) + { + _taskLoggingContext.LogWarning(new BuildEventFileInfo(string.Empty), null, "ExpectedEventToBeSerializable", e.GetType().Name); + return false; + } + + return true; + } + + /// + /// Async version of BuildProjectFilesInParallel. + /// + /// The list of projects to build + /// The set of targets to build + /// The global properties to use for each project + /// The list of global properties to undefine + /// The tools versions to use + /// Should the target outputs be returned in teh BuildEngineResult + /// A Task returning a structure containing the result of the build, success or failure and the list of target outputs per project + private async Task BuildProjectFilesInParallelAsync(string[] projectFileNames, string[] targetNames, System.Collections.IDictionary[] globalProperties, IList[] undefineProperties, string[] toolsVersion, bool returnTargetOutputs) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFileNames, "projectFileNames"); + ErrorUtilities.VerifyThrowArgumentNull(globalProperties, "globalProperties"); + VerifyActiveProxy(); + + List> targetOutputsPerProject = null; + + using (FullTracking.Suspend()) + { + bool overallSuccess = true; + + if (projectFileNames.Length == 1 && projectFileNames[0] == null && globalProperties[0] == null && (undefineProperties == null || undefineProperties[0] == null) && toolsVersion[0] == null) + { + // This is really a legacy CallTarget invocation + ITargetResult[] results = await _targetBuilderCallback.LegacyCallTarget(targetNames, ContinueOnError, _taskLocation); + + if (returnTargetOutputs) + { + targetOutputsPerProject = new List>(1); + targetOutputsPerProject.Add(new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + for (int i = 0; i < targetNames.Length; i++) + { + targetOutputsPerProject[0][targetNames[i]] = results[i].Items; + if (results[i].ResultCode == TargetResultCode.Failure) + { + overallSuccess = false; + } + } + } + else + { + // UNDONE: (Refactor) Investigate making this a ReadOnly collection of some sort. + PropertyDictionary[] propertyDictionaries = new PropertyDictionary[projectFileNames.Length]; + + for (int i = 0; i < projectFileNames.Length; i++) + { + // Copy in the original project's global properties + propertyDictionaries[i] = new PropertyDictionary(_requestEntry.RequestConfiguration.Project.GlobalPropertiesDictionary); + + // Now add/replace any which may have been specified. + if (globalProperties[i] != null) + { + foreach (DictionaryEntry entry in globalProperties[i]) + { + propertyDictionaries[i].Set(ProjectPropertyInstance.Create(entry.Key as string, entry.Value as string, _taskLocation)); + } + } + + // Finally, remove any which were requested to be removed. + if (undefineProperties != null && undefineProperties[i] != null) + { + foreach (string property in undefineProperties[i]) + { + propertyDictionaries[i].Remove(property); + } + } + } + + IRequestBuilderCallback builderCallback = _requestEntry.Builder as IRequestBuilderCallback; + BuildResult[] results = await builderCallback.BuildProjects + ( + projectFileNames, + propertyDictionaries, + toolsVersion ?? new string[] { }, + targetNames ?? new string[] { }, + true + ); + + // Even if one of the projects fails to build and therefore has no outputs, it should still have an entry in the results array (albeit with an empty list in it) + ErrorUtilities.VerifyThrow(results.Length == projectFileNames.Length, "{0}!={1}.", results.Length, projectFileNames.Length); + + if (returnTargetOutputs) + { + targetOutputsPerProject = new List>(results.Length); + } + + // Now walk through the results, and report that subset which was asked for. + for (int i = 0; i < results.Length; i++) + { + if (targetOutputsPerProject != null) + { + targetOutputsPerProject.Add(new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + foreach (KeyValuePair resultEntry in results[i].ResultsByTarget) + { + if (targetOutputsPerProject != null) + { + // We need to clone the task items because if we did not then we would be passing live references + // out to the caller of the msbuild callback and any modifications they make to those items + // would appear in the results cache. + ITaskItem[] clonedTaskItem = new ITaskItem[resultEntry.Value.Items.Length]; + for (int j = 0; j < resultEntry.Value.Items.Length; j++) + { + clonedTaskItem[j] = ((TaskItem)resultEntry.Value.Items[j]).DeepClone(); + } + + targetOutputsPerProject[i][resultEntry.Key] = clonedTaskItem; + } + } + + if (results[i].OverallResult == BuildResultCode.Failure) + { + overallSuccess = false; + } + } + + ErrorUtilities.VerifyThrow(results.Length == projectFileNames.Length || overallSuccess == false, "The number of results returned {0} cannot be less than the number of project files {1} unless one of the results indicated failure.", results.Length, projectFileNames.Length); + } + + return new BuildEngineResult(overallSuccess, targetOutputsPerProject); + } + } + + /// + /// Verify the task host is active or not + /// Thread safe. + /// + private void VerifyActiveProxy() + { + ErrorUtilities.VerifyThrow(_activeProxy == true, "Attempted to use an inactive task host."); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/IScheduler.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/IScheduler.cs new file mode 100644 index 00000000000..d36a4b632aa --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/IScheduler.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Interface for the scheduler. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Represents an object which provides scheduling services for BuildRequests over Nodes. + /// + internal interface IScheduler : IBuildComponent + { + /// + /// Retrieves the minimum assignable configuration id + /// + int MinimumAssignableConfigurationId { get; } + + /// + /// Determines if the specified configuration is currently being built + /// + /// The configuration to query for + /// True if the configuration is being built somewhere, false otherwise. + bool IsCurrentlyBuildingConfiguration(int configurationId); + + /// + /// Retrieves a configuration id for a configuration which has a matching path + /// + /// The path for the configuration + /// A positive configuration id if one exists in the plan, 0 otherwise. + int GetConfigurationIdFromPlan(string configurationPath); + + /// + /// Reports to the scheduler that a request is blocked. + /// + /// The node making the report. + /// The thing blocking the active request on the node. + /// Action to be taken. + IEnumerable ReportRequestBlocked(int nodeId, BuildRequestBlocker blocker); + + /// + /// Reports to the scheduler that a new result has been generated for a build request. + /// + /// The node reporting the request. + /// The result. + /// Action to be taken. + IEnumerable ReportResult(int nodeId, BuildResult result); + + /// + /// Reports to the scheduler that a node has been created. + /// + /// Info about the created nodes. + /// Action to be taken. + IEnumerable ReportNodesCreated(IEnumerable nodeInfo); + + /// + /// Reports to the scheduler than a node aborted the build. + /// + /// The node which aborted. + void ReportBuildAborted(int nodeId); + + /// + /// Resets the scheduler. + /// + void Reset(); + + /// + /// Writes a detailed summary of the build state which includes informaiton about the scheduling plan. + /// + void WriteDetailedSummary(int submissionId); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulableRequest.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulableRequest.cs new file mode 100644 index 00000000000..66ac131a20f --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulableRequest.cs @@ -0,0 +1,693 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class holding data about requests used by the Scheduler. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The state enumeration for SchedulableRequests. + /// + internal enum SchedulableRequestState + { + /// + /// This request has been submitted but has never been scheduled so it has executed no tasks and does not currently have an + /// entry residing on any node. There may be multiple requests with the same global request id in this state. + /// + Unscheduled, + + /// + /// This request may continue executing. It already has an entry on a node. There may only ever be one request with a given + /// global request id in this state. + /// + Ready, + + /// + /// This request is currently executing tasks on its node. In this case it will be the only task executing on the node - + /// all other tasks are either Ready or Blocked. There may only ever be one request with a given global request id in this state. + /// + Executing, + + /// + /// This request is currently blocked on one or more requests which must complete before it may continue. There may only ever be one + /// request with a given global request id in this state. + /// + Blocked, + + /// + /// This request has yielded control of the node while it is running a long-running out-of-process program. Any number of tasks on a + /// node may be in the yielding state. + /// + Yielding, + + /// + /// This request has completed and removed from the system. + /// + Completed + } + + /// + /// A representation of a BuildRequest and associated data used by the Scheduler to track work being done by the build system. + /// SchedulableRequests implicitly form a directed acyclic graph showing the blocking/blocked relationship between the requests + /// known to the system at any given time. These associations are updated by the BlockByRequest, UnblockWithResult and ResumeExecution + /// methods. These methods, along with Complete, cause state changes which the SchedulingData object will record. That data can be + /// queried to determine the state of any request or node in the system. + /// + internal class SchedulableRequest + { + /// + /// The request collection to which this belongs. + /// + private SchedulingData _schedulingData; + + /// + /// The current state. + /// + private SchedulableRequestState _state; + + /// + /// The node to which this request is assigned. + /// + private int _assignedNodeId; + + /// + /// The BuildRequest this class represents. + /// + private BuildRequest _request; + + /// + /// The schedulable request which issued this request. + /// + private SchedulableRequest _parent; + + /// + /// The list of targets which were actively building at the time we were blocked. + /// + private string[] _activeTargetsWhenBlocked; + + /// + /// The requests which must complete before we can continue executing. Indexed by global request id and node request id. + /// Each global request id may have multiple requests which map to it, but they will have separate node request ids. + /// + private Dictionary _requestsWeAreBlockedBy; + + /// + /// The requests which cannot continue until we have finished executing. + /// + private HashSet _requestsWeAreBlocking; + + /// + /// The time this request was created. + /// + private DateTime _creationTime; + + /// + /// The time this request started building. + /// + private DateTime _startTime; + + /// + /// The time this request was completed. + /// + private DateTime _endTime; + + /// + /// Records of the amount of time spent in each of the states. + /// + private Dictionary _timeRecords; + + /// + /// Constructor. + /// + public SchedulableRequest(SchedulingData collection, BuildRequest request, SchedulableRequest parent) + { + ErrorUtilities.VerifyThrowArgumentNull(collection, "collection"); + ErrorUtilities.VerifyThrowArgumentNull(request, "request"); + ErrorUtilities.VerifyThrow((parent == null) || (parent._schedulingData == collection), "Parent request does not belong to the same collection."); + + _schedulingData = collection; + _request = request; + _parent = parent; + _assignedNodeId = -1; + _requestsWeAreBlockedBy = new Dictionary(); + _requestsWeAreBlocking = new HashSet(); + + _timeRecords = new Dictionary(5); + _timeRecords[SchedulableRequestState.Unscheduled] = new ScheduleTimeRecord(); + _timeRecords[SchedulableRequestState.Blocked] = new ScheduleTimeRecord(); + _timeRecords[SchedulableRequestState.Yielding] = new ScheduleTimeRecord(); + _timeRecords[SchedulableRequestState.Executing] = new ScheduleTimeRecord(); + _timeRecords[SchedulableRequestState.Ready] = new ScheduleTimeRecord(); + _timeRecords[SchedulableRequestState.Completed] = new ScheduleTimeRecord(); + + ChangeToState(SchedulableRequestState.Unscheduled); + } + + /// + /// The current state of the request. + /// + public SchedulableRequestState State + { + get { return _state; } + } + + /// + /// The underlying BuildRequest. + /// + public BuildRequest BuildRequest + { + get { return _request; } + } + + /// + /// The request which issued this request. + /// + public SchedulableRequest Parent + { + get { return _parent; } + } + + /// + /// Returns the node to which this request is assigned. + /// + public int AssignedNode + { + get { return _assignedNodeId; } + } + + /// + /// The set of active targets. + /// + public IEnumerable ActiveTargets + { + get + { + VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Yielding, SchedulableRequestState.Blocked, SchedulableRequestState.Executing }); + return _activeTargetsWhenBlocked; + } + } + + /// + /// Gets a count of the requests we are blocked by. + /// + public int RequestsWeAreBlockedByCount + { + get + { + return _requestsWeAreBlockedBy.Count; + } + } + + /// + /// Gets the set of requests for which we require results before we may proceed. + /// + public IEnumerable RequestsWeAreBlockedBy + { + get + { + return _requestsWeAreBlockedBy.Values; + } + } + + /// + /// Gets a count of the requests we are blocking. + /// + public int RequestsWeAreBlockingCount + { + get + { + return _requestsWeAreBlocking.Count; + } + } + + /// + /// Gets the set of requests which cannot proceed because they are waiting for results from us. + /// + public IEnumerable RequestsWeAreBlocking + { + get + { + return _requestsWeAreBlocking; + } + } + + /// + /// The time this request was created. + /// + public DateTime CreationTime + { + get + { + return _creationTime; + } + + set + { + ErrorUtilities.VerifyThrow(_creationTime == DateTime.MinValue, "Cannot set CreationTime twice."); + _creationTime = value; + } + } + + /// + /// The time this request started building. + /// + public DateTime StartTime + { + get + { + return _startTime; + } + + set + { + ErrorUtilities.VerifyThrow(_startTime == DateTime.MinValue, "Cannot set StartTime twice."); + _startTime = value; + } + } + + /// + /// The time this request was completed. + /// + public DateTime EndTime + { + get + { + return _endTime; + } + + set + { + ErrorUtilities.VerifyThrow(_endTime == DateTime.MinValue, "Cannot set EndTime twice."); + _endTime = value; + } + } + + /// + /// Gets the amount of time we spent in the specified state. + /// + public TimeSpan GetTimeSpentInState(SchedulableRequestState desiredState) + { + return _timeRecords[desiredState].AccumulatedTime; + } + + /// + /// Inticates the request is yielding the node. + /// + public void Yield(string[] activeTargets) + { + VerifyState(SchedulableRequestState.Executing); + ErrorUtilities.VerifyThrowArgumentNull(activeTargets, "activeTargets"); + _activeTargetsWhenBlocked = activeTargets; + ChangeToState(SchedulableRequestState.Yielding); + } + + /// + /// Indicates the request is ready to reacquire the node. + /// + public void Reacquire() + { + VerifyState(SchedulableRequestState.Yielding); + _activeTargetsWhenBlocked = null; + ChangeToState(SchedulableRequestState.Ready); + } + + /// + /// Marks this request as being blocked by the specified request. Establishes the correct relationships between the requests. + /// + /// The request which is blocking this one. + /// The list of targets this request was currently building at the time it became blocked. + public void BlockByRequest(SchedulableRequest blockingRequest, string[] activeTargets) + { + VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Blocked, SchedulableRequestState.Executing }); + ErrorUtilities.VerifyThrowArgumentNull(blockingRequest, "blockingRequest"); + ErrorUtilities.VerifyThrowArgumentNull(activeTargets, "activeTargets"); + + // Note that the blocking request will typically be our parent UNLESS it is a request we blocked on because it was executing a target we wanted to execute. + // Thus, we do not assert the parent-child relationship here. + BlockingRequestKey key = new BlockingRequestKey(blockingRequest.BuildRequest); + ErrorUtilities.VerifyThrow(!_requestsWeAreBlockedBy.ContainsKey(key), "We are already blocked by this request."); + ErrorUtilities.VerifyThrow(!blockingRequest._requestsWeAreBlocking.Contains(this), "The blocking request thinks it is already blocking us."); + + // This method is only called when a request reports that it is blocked on other requests. If the request is being blocked by a brand new + // request, that request will be unscheduled. If this request is blocked by an in-progress request which was executing a target it needed + // to also execute, then that request is not unscheduled (because it was running on the node) and it is not executing (because this condition + // can only occur against requests which are executing on the same node and since the request which called this method is the one currently + // executing on that node, that means the request it is blocked by must either be itself blocked or ready.) + blockingRequest.VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Yielding, SchedulableRequestState.Blocked, SchedulableRequestState.Ready, SchedulableRequestState.Unscheduled }); + + // Update our list of active targets. This has to be done before we detect circular dependencies because we use this information to detect + // re-entrancy circular dependencies. + _activeTargetsWhenBlocked = activeTargets; + DetectCircularDependency(blockingRequest); + + _requestsWeAreBlockedBy[key] = blockingRequest; + blockingRequest._requestsWeAreBlocking.Add(this); + + ChangeToState(SchedulableRequestState.Blocked); + } + + /// + /// Indicates that there are results which can be used to unblock this request. Updates the relationships between requests. + /// + public void UnblockWithResult(BuildResult result) + { + VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Blocked, SchedulableRequestState.Unscheduled }); + ErrorUtilities.VerifyThrowArgumentNull(result, "result"); + + BlockingRequestKey key = new BlockingRequestKey(result); + DisconnectRequestWeAreBlockedBy(key); + _activeTargetsWhenBlocked = null; + } + + /// + /// Resumes execution of the request on the specified node. + /// + public void ResumeExecution(int nodeId) + { + ErrorUtilities.VerifyThrow(_assignedNodeId == Scheduler.InvalidNodeId || _assignedNodeId == nodeId, "Request must always resume on the same node on which it was started."); + + VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Ready, SchedulableRequestState.Unscheduled }); + ErrorUtilities.VerifyThrow((_state == SchedulableRequestState.Ready) || !_schedulingData.IsRequestScheduled(this), "Another instance of request {0} is already scheduled.", _request.GlobalRequestId); + ErrorUtilities.VerifyThrow(!_schedulingData.IsNodeWorking(nodeId), "Cannot resume execution of request {0} because node {1} is already working.", _request.GlobalRequestId, nodeId); + + int requiredNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(_request.ConfigurationId); + ErrorUtilities.VerifyThrow(requiredNodeId == Scheduler.InvalidNodeId || requiredNodeId == nodeId, "Request {0} cannot be assigned to node {1} because its configuration is already assigned to node {2}", _request.GlobalRequestId, nodeId, requiredNodeId); + + _assignedNodeId = nodeId; + ChangeToState(SchedulableRequestState.Executing); + } + + /// + /// Completes this request. + /// + public void Complete(BuildResult result) + { + VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Ready, SchedulableRequestState.Executing, SchedulableRequestState.Unscheduled }); + ErrorUtilities.VerifyThrow(_state != SchedulableRequestState.Ready || result.CircularDependency, "Request can only be Completed from the Ready state if the result indicates a circular dependency occurred."); + ErrorUtilities.VerifyThrow(_requestsWeAreBlockedBy.Count == 0, "We can't be complete if we are still blocked on requests."); + + // Any requests we were blocking we will no longer be blocking. + List requestsToUnblock = new List(_requestsWeAreBlocking); + foreach (SchedulableRequest requestWeAreBlocking in requestsToUnblock) + { + requestWeAreBlocking.UnblockWithResult(result); + } + + ChangeToState(SchedulableRequestState.Completed); + } + + /// + /// Removes an unscheduled request. + /// + public void Delete() + { + VerifyState(SchedulableRequestState.Unscheduled); + ErrorUtilities.VerifyThrow(_requestsWeAreBlockedBy.Count == 0, "We are blocked by requests."); + ErrorUtilities.VerifyThrow(_requestsWeAreBlocking.Count == 0, "We are blocking by requests."); + ChangeToState(SchedulableRequestState.Completed); + } + + /// + /// Verifies that the current state is as expected. + /// + public void VerifyState(SchedulableRequestState requiredState) + { + ErrorUtilities.VerifyThrow(_state == requiredState, "Request {0} expected to be in state {1} but state is actually {2}", _request.GlobalRequestId, requiredState, _state); + } + + /// + /// Verifies that the current state is as expected. + /// + public void VerifyOneOfStates(SchedulableRequestState[] requiredStates) + { + foreach (SchedulableRequestState requiredState in requiredStates) + { + if (_state == requiredState) + { + return; + } + } + + ErrorUtilities.ThrowInternalError("State {0} is not one of the expected states.", _state); + } + + /// + /// Change to the specified state. Update internal counters. + /// + private void ChangeToState(SchedulableRequestState newState) + { + DateTime currentTime = DateTime.UtcNow; + _timeRecords[_state].EndState(currentTime); + _timeRecords[newState].StartState(currentTime); + if (_state != newState) + { + SchedulableRequestState previousState = _state; + _state = newState; + _schedulingData.UpdateFromState(this, previousState); + } + } + + /// + /// Detects a circular dependency. Throws a CircularDependencyException if one exists. Circular dependencies can occur + /// under the following conditions: + /// 1. If the blocking request's global request ID appears in the ancestor chain (Direct). + /// 2. If a request appears in the ancestor chain and has a different global request ID but has an active target that + /// matches one of the targets specified in the blocking request (Direct). + /// 3. If the blocking request exists elsewhere as a blocked request with the same global request ID, and one of its children + /// (recursively) matches this request's global request ID (Indirect). + /// 4. If the blocking request's configuration is part of another request elsewhere which is also blocked, and that request + /// is building targets this blocking request is building, and one of that blocked request's children (recursively) + /// matches this request's global request ID (Indirect). + /// + private void DetectCircularDependency(SchedulableRequest blockingRequest) + { + DetectDirectCircularDependency(blockingRequest); + DetectIndirectCircularDependency(blockingRequest); + } + + /// + /// Detects a circular dependency where the request which is about to block us is already blocked by us, usually as a result + /// of it having been previously scheduled in a multiproc scenario, but before this request was able to execute. + /// + /// + /// Let A be 'this' project and B be 'blockingRequest' (the request which is going to block A.) + /// An indirect circular dependency exists if there is a dependency path from B to A. If there is no + /// existing blocked request B' with the same global request id as B, then there can be no path from B to A because B is a brand new + /// request with no other dependencies. If there is an existing blocked request B' with the same global request ID as B, then we + /// walk the set of dependencies recursively searching for A. If A is found, we have a circular dependency. + /// + private void DetectIndirectCircularDependency(SchedulableRequest blockingRequest) + { + // If there is already a blocked request which has the same configuration id as the blocking request and that blocked request is (recursively) + // waiting on this request, then that is an indirect circular dependency. + SchedulableRequest alternateRequest = _schedulingData.GetBlockedRequestIfAny(blockingRequest.BuildRequest.GlobalRequestId); + if (alternateRequest == null) + { + return; + } + + Stack requestsToEvaluate = new Stack(16); + HashSet evaluatedRequests = new HashSet(); + requestsToEvaluate.Push(alternateRequest); + + while (requestsToEvaluate.Count > 0) + { + SchedulableRequest requestToEvaluate = requestsToEvaluate.Pop(); + + // If we make it to a child which is us, then it's a circular dependency. + if (requestToEvaluate.BuildRequest.GlobalRequestId == this.BuildRequest.GlobalRequestId) + { + ThrowIndirectCircularDependency(blockingRequest, requestToEvaluate); + } + + evaluatedRequests.Add(requestToEvaluate); + + // If the request is not scheduled, it's possible that is because it's been scheduled elsewhere and is blocked. + // Follow that path if it exists. + if (requestToEvaluate.State == SchedulableRequestState.Unscheduled) + { + requestToEvaluate = _schedulingData.GetBlockedRequestIfAny(requestToEvaluate.BuildRequest.GlobalRequestId); + + // If there was no scheduled request to evaluate, move on. + if (requestToEvaluate == null || evaluatedRequests.Contains(requestToEvaluate)) + { + continue; + } + } + + // This request didn't cause a circular dependency, check its children. + foreach (SchedulableRequest childRequest in requestToEvaluate.RequestsWeAreBlockedBy) + { + if (!evaluatedRequests.Contains(childRequest)) + { + requestsToEvaluate.Push(childRequest); + } + } + } + } + + /// + /// Build our ancestor list then throw the circular dependency error. + /// + private void ThrowIndirectCircularDependency(SchedulableRequest blockingRequest, SchedulableRequest requestToEvaluate) + { + // We found a request which has the same global request ID as us in a chain which leads from the (already blocked) request + // which is trying to block us. Calculate its list of ancestors by walking up the parent list. + List ancestors = new List(16); + while (requestToEvaluate.Parent != null) + { + ancestors.Add(requestToEvaluate.Parent); + requestToEvaluate = requestToEvaluate.Parent; + } + + ancestors.Reverse(); // Because the list should be in the order from root to child. + CleanupForCircularDependencyAndThrow(blockingRequest, ancestors); + } + + /// + /// Detects a circular dependency where the blocking request is in our direct ancestor chain. + /// + private void DetectDirectCircularDependency(SchedulableRequest blockingRequest) + { + // A circular dependency occurs when this project (or any of its ancestors) has the same global request id as the + // blocking request. + List ancestors = new List(16); + SchedulableRequest currentRequest = this; + do + { + ancestors.Add(currentRequest); + if (currentRequest.BuildRequest.GlobalRequestId == blockingRequest.BuildRequest.GlobalRequestId) + { + // We are directly conflicting with an instance of ourselves. + CleanupForCircularDependencyAndThrow(blockingRequest, ancestors); + } + + currentRequest = currentRequest.Parent; + } + while (currentRequest != null); + } + + /// + /// Removes associations with all blocking requests and throws an exception. + /// + private void CleanupForCircularDependencyAndThrow(SchedulableRequest requestCausingFailure, List ancestors) + { + if (_requestsWeAreBlockedBy.Count != 0) + { + List tempRequests = new List(_requestsWeAreBlockedBy.Values); + foreach (SchedulableRequest requestWeAreBlockedBy in tempRequests) + { + BlockingRequestKey key = new BlockingRequestKey(requestWeAreBlockedBy.BuildRequest); + DisconnectRequestWeAreBlockedBy(key); + } + } + else + { + ChangeToState(SchedulableRequestState.Ready); + } + + _activeTargetsWhenBlocked = null; + + // The blocking request itself is no longer valid if it was unscheduled. + if (requestCausingFailure.State == SchedulableRequestState.Unscheduled) + { + requestCausingFailure.Delete(); + } + + throw new SchedulerCircularDependencyException(requestCausingFailure.BuildRequest, ancestors); + } + + /// + /// Removes the association between this request and the one we are blocked by. + /// + private void DisconnectRequestWeAreBlockedBy(BlockingRequestKey blockingRequestKey) + { + ErrorUtilities.VerifyThrow(_requestsWeAreBlockedBy.ContainsKey(blockingRequestKey), "We are not blocked by the specified request."); + + SchedulableRequest unblockingRequest = _requestsWeAreBlockedBy[blockingRequestKey]; + ErrorUtilities.VerifyThrow(unblockingRequest._requestsWeAreBlocking.Contains(this), "The request unblocking us doesn't think it is blocking us."); + + _requestsWeAreBlockedBy.Remove(blockingRequestKey); + unblockingRequest._requestsWeAreBlocking.Remove(this); + + // If the request we are blocked by also happens to be unscheduled, remove it as well so we don't try to run it later. This is + // because circular dependency errors cause us to fail all outstanding requests on the current request. See BuildRequsetEntry.ReportResult. + if (unblockingRequest.State == SchedulableRequestState.Unscheduled) + { + unblockingRequest.Delete(); + } + + if (_requestsWeAreBlockedBy.Count == 0) + { + ChangeToState(SchedulableRequestState.Ready); + } + } + + /// + /// A key for blocking requests combining the global request and node request ids. + /// + private class BlockingRequestKey + { + /// + /// The global request id. + /// + private int _globalRequestId; + + /// + /// The request id known to the node. + /// + private int _nodeRequestId; + + /// + /// Constructor over a request. + /// + public BlockingRequestKey(BuildRequest request) + { + _globalRequestId = request.GlobalRequestId; + _nodeRequestId = request.NodeRequestId; + } + + /// + /// Constructor over a result. + /// + public BlockingRequestKey(BuildResult result) + { + _globalRequestId = result.GlobalRequestId; + _nodeRequestId = result.NodeRequestId; + } + + /// + /// Equals override. + /// + public override bool Equals(object obj) + { + if (obj != null) + { + BlockingRequestKey other = obj as BlockingRequestKey; + if (other != null) + { + return (other._globalRequestId == _globalRequestId) && (other._nodeRequestId == _nodeRequestId); + } + } + + return base.Equals(obj); + } + + /// + /// GetHashCode override. + /// + public override int GetHashCode() + { + return _globalRequestId ^ _nodeRequestId; + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleResponse.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleResponse.cs new file mode 100644 index 00000000000..74c8c3bdf51 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleResponse.cs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Struct containing a response from a scheduler. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Globalization; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The type of action to take in response to a scheduling request. + /// + internal enum ScheduleActionType + { + /// + /// The response indicates that no action should be taken. + /// + NoAction, + + /// + /// The response indicates that the request should be sent to the specified node. + /// + Schedule, + + /// + /// The response indicates that the request should be send to the specified node, + /// along with the configuration for the request. + /// + ScheduleWithConfiguration, + + /// + /// The response has results for a particular blocked request + /// + ReportResults, + + /// + /// The specified request id should now resume execution + /// + ResumeExecution, + + /// + /// The response indicates that a new node should be created rather than scheduling this request. + /// The request may be scheduled at a later time. + /// + CreateNode, + + /// + /// The response indicates that the submission is complete. + /// + SubmissionComplete, + + /// + /// The last action caused a circular dependency which cannot be resolved. + /// + CircularDependency + } + + /// + /// A response from the scheduler indicating where a build request should be handled. + /// + internal class ScheduleResponse + { + /// + /// The type of action to take on this response. + /// + internal readonly ScheduleActionType Action; + + /// + /// The node ID to which the request should be sent. + /// + internal readonly int NodeId; + + /// + /// The results for a completed submission. + /// + internal readonly BuildResult BuildResult; + + /// + /// The build request to send. + /// + internal readonly BuildRequest BuildRequest; + + /// + /// The unblocking information. + /// + internal readonly BuildRequestUnblocker Unblocker; + + /// + /// The type of node we must create. + /// + internal readonly NodeAffinity RequiredNodeType; + + /// + /// The number of nodes of the requested affinity to create. + /// + internal readonly int NumberOfNodesToCreate; + + /// + /// Constructs a response where no action should be taken. + /// + internal ScheduleResponse(ScheduleActionType type) + { + Action = type; + } + + /// + /// Constructs a response indicating what type of node we need to create. + /// + private ScheduleResponse(NodeAffinity affinity, int count) + { + Action = ScheduleActionType.CreateNode; + RequiredNodeType = affinity; + NumberOfNodesToCreate = count; + } + + /// + /// Constructs a response indicating that a specific submission has completed. + /// + private ScheduleResponse(BuildResult result) + { + Action = ScheduleActionType.SubmissionComplete; + BuildResult = result; + } + + /// + /// Constructs a response indicating there is a circular dependency caused by the specified request. + /// + private ScheduleResponse(int nodeId, BuildRequest parentRequest, BuildRequest requestCausingCircularDependency) + { + Action = ScheduleActionType.CircularDependency; + BuildRequest = requestCausingCircularDependency; + NodeId = nodeId; + Unblocker = new BuildRequestUnblocker(parentRequest, new BuildResult(requestCausingCircularDependency, true /* circularDependency */)); + } + + /// + /// Constructs a response where a request should be scheduled. + /// + /// The node ID to which the request should be sent. + /// The request to send. + private ScheduleResponse(int node, BuildRequest request, bool sendConfiguration) + { + Action = sendConfiguration ? ScheduleActionType.ScheduleWithConfiguration : ScheduleActionType.Schedule; + NodeId = node; + BuildRequest = request; + } + + /// + /// Constructs a response where a result should be sent or execution should be resumed. + /// + /// The node ID to which the result should be sent. + /// The result to send. + private ScheduleResponse(int node, BuildRequestUnblocker unblocker) + { + Action = (unblocker.Result == null) ? ScheduleActionType.ResumeExecution : ScheduleActionType.ReportResults; + NodeId = node; + Unblocker = unblocker; + BuildResult = unblocker.Result; + } + + /// + /// Creates a Schedule or ScheduleWithConfiguration response + /// + /// The node to which the response should be sent. + /// The request to be scheduled. + /// Flag indicating whether or not the configuration for the request must be sent to the node as well. + /// The ScheduleResponse. + public static ScheduleResponse CreateScheduleResponse(int node, BuildRequest requestToSchedule, bool sendConfiguration) + { + return new ScheduleResponse(node, requestToSchedule, sendConfiguration); + } + + /// + /// Creates a ReportResult response. + /// + /// The node to which the response should be sent. + /// The result to be reported. + /// The ScheduleResponse. + public static ScheduleResponse CreateReportResultResponse(int node, BuildResult resultToReport) + { + return new ScheduleResponse(node, new BuildRequestUnblocker(resultToReport)); + } + + /// + /// Creates a ResumeExecution response. + /// + /// The node to which the response should be sent. + /// The request which should resume executing. + /// The ScheduleResponse. + public static ScheduleResponse CreateResumeExecutionResponse(int node, int globalRequestIdToResume) + { + return new ScheduleResponse(node, new BuildRequestUnblocker(globalRequestIdToResume)); + } + + /// + /// Creates a CircularDependency response. + /// + /// The node to which the response should be sent. + /// The request which attempted to invoke the request causing the circular dependency. + /// The request which caused the circular dependency. + /// The ScheduleResponse. + public static ScheduleResponse CreateCircularDependencyResponse(int node, BuildRequest parentRequest, BuildRequest requestCausingCircularDependency) + { + return new ScheduleResponse(node, parentRequest, requestCausingCircularDependency); + } + + /// + /// Creates a SubmissionComplete response. + /// + /// The result for the submission's root request. + /// The ScheduleResponse. + public static ScheduleResponse CreateSubmissionCompleteResponse(BuildResult rootRequestResult) + { + return new ScheduleResponse(rootRequestResult); + } + + /// + /// Create a CreateNode response + /// + /// The type of node to create. + /// The number of new nodes of that particular affinity to create. + /// The ScheduleResponse. + public static ScheduleResponse CreateNewNodeResponse(NodeAffinity typeOfNodeToCreate, int count) + { + return new ScheduleResponse(typeOfNodeToCreate, count); + } + + /// + /// Returns the schedule response as a descriptive string. + /// + public override string ToString() + { + switch (Action) + { + case ScheduleActionType.ReportResults: + case ScheduleActionType.ResumeExecution: + return String.Format(CultureInfo.CurrentCulture, "Act: {0} Node: {1} Request: {2}", Action, NodeId, Unblocker.BlockedRequestId); + + case ScheduleActionType.Schedule: + return String.Format(CultureInfo.CurrentCulture, "Act: {0} Node: {1} Request: {2} Parent {3}", Action, NodeId, BuildRequest.GlobalRequestId, BuildRequest.ParentGlobalRequestId); + + case ScheduleActionType.ScheduleWithConfiguration: + return String.Format(CultureInfo.CurrentCulture, "Act: {0} Node: {1} Request: {2} Parent {3} Configuration: {4}", Action, NodeId, BuildRequest.GlobalRequestId, BuildRequest.ParentGlobalRequestId, BuildRequest.ConfigurationId); + + case ScheduleActionType.CircularDependency: + return String.Format(CultureInfo.CurrentCulture, "Act: {0} Node: {1} Request: {2} Parent {3} Configuration: {4}", Action, NodeId, BuildRequest.GlobalRequestId, BuildRequest.ParentGlobalRequestId, BuildRequest.ConfigurationId); + + case ScheduleActionType.SubmissionComplete: + return String.Format(CultureInfo.CurrentCulture, "Act: {0} Submission: {1}", Action, BuildResult.SubmissionId); + + case ScheduleActionType.CreateNode: + return String.Format(CultureInfo.CurrentCulture, "Act: {0} Count: {1}", Action, NumberOfNodesToCreate); + + case ScheduleActionType.NoAction: + default: + return String.Format(CultureInfo.CurrentCulture, "Act: {0}", Action); + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs new file mode 100644 index 00000000000..fe2bed3847c --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/ScheduleTimeRecord.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper class used to track the time spent by requests in the Scheduler. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Class used to track time accumulated during scheduling. + /// + internal class ScheduleTimeRecord + { + /// + /// The time the current counter started. + /// + private DateTime _startTimeForCurrentState; + + /// + /// The accumulated time for this counter. + /// + private TimeSpan _accumulatedTime; + + /// + /// Constructor. + /// + public ScheduleTimeRecord() + { + _startTimeForCurrentState = DateTime.MinValue; + _accumulatedTime = TimeSpan.Zero; + } + + /// + /// Retrieve the accumulated time. + /// + public TimeSpan AccumulatedTime + { + get + { + ErrorUtilities.VerifyThrow(_startTimeForCurrentState == DateTime.MinValue, "Can't get the accumulated time while the timer is still running."); + return _accumulatedTime; + } + } + + /// + /// Start the timer. + /// + public void StartState(DateTime currentTime) + { + ErrorUtilities.VerifyThrow(_startTimeForCurrentState == DateTime.MinValue, "Cannot start the counter when it is already running."); + _startTimeForCurrentState = currentTime; + } + + /// + /// End the timer and update the accumulated time. + /// + public void EndState(DateTime currentTime) + { + if (_startTimeForCurrentState != DateTime.MinValue) + { + _accumulatedTime += currentTime - _startTimeForCurrentState; + _startTimeForCurrentState = DateTime.MinValue; + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/Scheduler.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/Scheduler.cs new file mode 100644 index 00000000000..caaba2a8024 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/Scheduler.cs @@ -0,0 +1,2419 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class responsible for distributing work to nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; +using CommunicationsUtilities = Microsoft.Build.Internal.CommunicationsUtilities; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The MSBuild Scheduler + /// + internal class Scheduler : IScheduler + { + /// + /// The invalid node id + /// + internal const int InvalidNodeId = -1; + + /// + /// ID used to indicate that the results for a particular configuration may at one point + /// have resided on this node, but currently do not and will need to be transferred back + /// in order to be used. + /// + internal const int ResultsTransferredId = -2; + + /// + /// The in-proc node id + /// + internal const int InProcNodeId = 1; + + /// + /// The virtual node, used when a request is initially given to the scheduler. + /// + internal const int VirtualNode = 0; + + /// + /// If MSBUILDCUSTOMSCHEDULER = CustomSchedulerForSQL, the default multiplier for the amount by which + /// the count of configurations on any one node can exceed the average configuration count is 1.1 -- + /// + 10%. + /// + private const double DefaultCustomSchedulerForSQLConfigurationLimitMultiplier = 1.1; + + #region Scheduler Data + + /// + /// Content of the environment variable MSBUILDSCHEDULINGUNLIMITED + /// + private string _schedulingUnlimitedVariable; + + /// + /// If MSBUILDSCHEDULINGUNLIMITED is set, this flag will make AtSchedulingLimit() always return false + /// + private bool _schedulingUnlimited; + + /// + /// If MSBUILDNODELIMITOFFSET is set, this will add an offset to the limit used in AtSchedulingLimit() + /// + private int _nodeLimitOffset; + + /// + /// { nodeId -> NodeInfo } + /// A list of nodes we know about. For the non-distributed case, there will be no more nodes than the + /// maximum specified on the command-line. + /// + private Dictionary _availableNodes; + + /// + /// The number of inproc nodes that can be created without hitting the + /// node limit. + /// + private int _currentInProcNodeCount = 0; + + /// + /// The number of out-of-proc nodes that can be created without hitting the + /// node limit. + /// + private int _currentOutOfProcNodeCount = 0; + + /// + /// The collection of all requests currently known to the system. + /// + private SchedulingData _schedulingData; + + #endregion + + /// + /// The component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// The configuration cache. + /// + private IConfigCache _configCache; + + /// + /// The results cache. + /// + private IResultsCache _resultsCache; + + /// + /// The next ID to assign for a global request id. + /// + private int _nextGlobalRequestId; + + /// + /// Flag indicating that we are supposed to dump the scheduler state to the disk periodically. + /// + private bool _debugDumpState; + + /// + /// Flag used for debugging by forcing all scheduling to go out-of-proc. + /// + private bool _forceAffinityOutOfProc; + + /// + /// The path into which debug files will be written. + /// + private string _debugDumpPath; + + /// + /// If MSBUILDCUSTOMSCHEDULER = CustomSchedulerForSQL, the user may also choose to set + /// MSBUILDCUSTOMSCHEDULERFORSQLCONFIGURATIONLIMITMULTIPLIER to the value by which they want + /// the max configuration count for any one node to exceed the average configuration count. + /// If that env var is not set, or is set to an invalid value (negative, less than 1, non-numeric) + /// then we use the default value instead. + /// + private double _customSchedulerForSQLConfigurationLimitMultiplier; + + /// + /// The plan. + /// + private SchedulingPlan _schedulingPlan; + + /// + /// If MSBUILDCUSTOMSCHEDULER is set, contains the requested scheduling algorithm + /// + private AssignUnscheduledRequestsDelegate _customRequestSchedulingAlgorithm; + + /// + /// Constructor. + /// + public Scheduler() + { + _debugDumpState = Environment.GetEnvironmentVariable("MSBUILDDEBUGSCHEDULER") == "1"; + _forceAffinityOutOfProc = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; + _debugDumpPath = Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH"); + _schedulingUnlimitedVariable = Environment.GetEnvironmentVariable("MSBUILDSCHEDULINGUNLIMITED"); + string strNodeLimitOffset = null; + + _nodeLimitOffset = 0; + + if (!String.IsNullOrEmpty(_schedulingUnlimitedVariable)) + { + _schedulingUnlimited = true; + } + else + { + _schedulingUnlimited = false; + + strNodeLimitOffset = Environment.GetEnvironmentVariable("MSBUILDNODELIMITOFFSET"); + + if (!String.IsNullOrEmpty(strNodeLimitOffset)) + { + _nodeLimitOffset = Int16.Parse(strNodeLimitOffset, CultureInfo.InvariantCulture); + + if (_nodeLimitOffset < 0) + { + _nodeLimitOffset = 0; + } + } + } + + if (String.IsNullOrEmpty(_debugDumpPath)) + { + _debugDumpPath = Path.GetTempPath(); + } + + Reset(); + } + + #region Delegates + + /// + /// In the circumstance where we want to specify the scheduling algorithm via the secret environment variable + /// MSBUILDCUSTOMSCHEDULING, the scheduling algorithm used will be assigned to a delegate of this type. + /// + internal delegate void AssignUnscheduledRequestsDelegate(List responses, HashSet idleNodes); + + #endregion + + #region IScheduler Members + + /// + /// Retrieves the minimum configuration id which can be assigned that won't conflict with those in the scheduling plan. + /// + public int MinimumAssignableConfigurationId + { + get + { + if (_schedulingPlan == null) + { + return 1; + } + + return _schedulingPlan.MaximumConfigurationId + 1; + } + } + + /// + /// Returns true if the specified configuration is currently in the scheduler. + /// + /// The configuration id + /// True if the specified configuration is already building. + public bool IsCurrentlyBuildingConfiguration(int configurationId) + { + return _schedulingData.GetRequestsAssignedToConfigurationCount(configurationId) > 0; + } + + /// + /// Gets a configuration id from the plan which matches the specified path. + /// + /// The path. + /// The configuration id which has been assigned to this path. + public int GetConfigurationIdFromPlan(string configPath) + { + if (_schedulingPlan == null) + { + return BuildRequestConfiguration.InvalidConfigurationId; + } + + return _schedulingPlan.GetConfigIdForPath(configPath); + } + + /// + /// Reports that the specified request has become blocked and cannot proceed. + /// + public IEnumerable ReportRequestBlocked(int nodeId, BuildRequestBlocker blocker) + { + _schedulingData.EventTime = DateTime.UtcNow; + List responses = new List(); + + // Get the parent, if any + SchedulableRequest parentRequest = null; + if (blocker.BlockedRequestId != BuildRequest.InvalidGlobalRequestId) + { + if (blocker.YieldAction == YieldAction.Reacquire) + { + parentRequest = _schedulingData.GetYieldingRequest(blocker.BlockedRequestId); + } + else + { + parentRequest = _schedulingData.GetExecutingRequest(blocker.BlockedRequestId); + } + } + + try + { + // We are blocked either on new requests (top-level or MSBuild task) or on an in-progress request that is + // building a target we want to build. + if (blocker.YieldAction != YieldAction.None) + { + TraceScheduler("Request {0} on node {1} is performing yield action {2}.", blocker.BlockedRequestId, nodeId, blocker.YieldAction); + HandleYieldAction(parentRequest, blocker); + } + else if ((blocker.BlockingRequestId == blocker.BlockedRequestId) && blocker.BlockingRequestId != BuildRequest.InvalidGlobalRequestId) + { + // We are blocked waiting for a transfer of results. + HandleRequestBlockedOnResultsTransfer(parentRequest, responses); + } + else if (blocker.BlockingRequestId != BuildRequest.InvalidGlobalRequestId) + { + // We are blocked by a request executing a target for which we need results. + try + { + HandleRequestBlockedOnInProgressTarget(parentRequest, blocker); + } + catch (SchedulerCircularDependencyException ex) + { + TraceScheduler("Circular dependency caused by request {0}({1}) (nr {2}), parent {3}({4}) (nr {5})", ex.Request.GlobalRequestId, ex.Request.ConfigurationId, ex.Request.NodeRequestId, parentRequest.BuildRequest.GlobalRequestId, parentRequest.BuildRequest.ConfigurationId, parentRequest.BuildRequest.NodeRequestId); + responses.Add(ScheduleResponse.CreateCircularDependencyResponse(nodeId, parentRequest.BuildRequest, ex.Request)); + } + } + else + { + // We are blocked by new requests, either top-level or MSBuild task. + HandleRequestBlockedByNewRequests(parentRequest, blocker, responses); + } + } + catch (SchedulerCircularDependencyException ex) + { + TraceScheduler("Circular dependency caused by request {0}({1}) (nr {2}), parent {3}({4}) (nr {5})", ex.Request.GlobalRequestId, ex.Request.ConfigurationId, ex.Request.NodeRequestId, parentRequest.BuildRequest.GlobalRequestId, parentRequest.BuildRequest.ConfigurationId, parentRequest.BuildRequest.NodeRequestId); + responses.Add(ScheduleResponse.CreateCircularDependencyResponse(nodeId, parentRequest.BuildRequest, ex.Request)); + } + + // Now see if we can schedule requests somewhere since we + // a) have a new request; and + // b) have a node which is now waiting and not doing anything. + ScheduleUnassignedRequests(responses); + return responses; + } + + /// + /// Informs the scheduler of a specific result. + /// + public IEnumerable ReportResult(int nodeId, BuildResult result) + { + _schedulingData.EventTime = DateTime.UtcNow; + List responses = new List(); + TraceScheduler("Reporting result from node {0} for request {1}, parent {2}.", nodeId, result.GlobalRequestId, result.ParentGlobalRequestId); + + // Record these results to the cache. + _resultsCache.AddResult(result); + + if (result.NodeRequestId == BuildRequest.ResultsTransferNodeRequestId) + { + // We are transferring results. The node to which they should be sent has already been recorded by the + // HandleRequestBlockedOnResultsTransfer method in the configuration. + BuildRequestConfiguration config = _configCache[result.ConfigurationId]; + ScheduleResponse response = ScheduleResponse.CreateReportResultResponse(config.ResultsNodeId, result); + responses.Add(response); + } + else + { + // Tell the request to which this result belongs than it is done. + SchedulableRequest request = _schedulingData.GetExecutingRequest(result.GlobalRequestId); + request.Complete(result); + + // Report results to our parent, or report submission complete as necessary. + if (request.Parent != null) + { + // responses.Add(new ScheduleResponse(request.Parent.AssignedNode, new BuildRequestUnblocker(request.Parent.BuildRequest.GlobalRequestId, result))); + ErrorUtilities.VerifyThrow(result.ParentGlobalRequestId == request.Parent.BuildRequest.GlobalRequestId, "Result's parent doesn't match request's parent."); + + // When adding the result to the cache we merge the result with what ever is already in the cache this may cause + // the result to have more target outputs in it than was was requested. To fix this we can ask the cache itself for the result we just added. + // When results are returned from the cache we filter them based on the targets we requested. This causes our result to only + // include the targets we requested rather than the merged result. + + // Note: In this case we do not need to log that we got the results from the cache because we are only using the cache + // for filtering the targets for the result instead rather than using the cache as the location where this result came from. + ScheduleResponse response = TrySatisfyRequestFromCache(request.Parent.AssignedNode, request.BuildRequest, skippedResultsAreOK: false); + + // response may be null if the result was never added to the cache. This can happen if the result has an exception in it + // or the results could not be satisfied becasue the initial or default targets have been skipped. If that is the case + // we need to report the result directly since it contains an exception + if (response == null) + { + response = ScheduleResponse.CreateReportResultResponse(request.Parent.AssignedNode, result.Clone()); + } + + responses.Add(response); + } + else + { + // This was root request, we can report submission complete. + // responses.Add(new ScheduleResponse(result)); + responses.Add(ScheduleResponse.CreateSubmissionCompleteResponse(result)); + if (result.OverallResult != BuildResultCode.Failure) + { + WriteSchedulingPlan(result.SubmissionId); + } + } + + // This result may apply to a number of other unscheduled requests which are blocking active requests. Report to them as well. + List unscheduledRequests = new List(_schedulingData.UnscheduledRequests); + foreach (SchedulableRequest unscheduledRequest in unscheduledRequests) + { + if (unscheduledRequest.BuildRequest.GlobalRequestId == result.GlobalRequestId) + { + TraceScheduler("Request {0} (node request {1}) also satisfied by result.", unscheduledRequest.BuildRequest.GlobalRequestId, unscheduledRequest.BuildRequest.NodeRequestId); + BuildResult newResult = new BuildResult(unscheduledRequest.BuildRequest, result, null); + + // Report results to the parent. + int parentNode = (unscheduledRequest.Parent == null) ? InvalidNodeId : unscheduledRequest.Parent.AssignedNode; + + // There are other requests which we can satisfy based on this result, lets pull the result out of the cache + // and satisfy those requests. Normally a skipped result would lead to the cache refusing to satisfy the + // request, because the correct response in that case would be to attempt to rebuild the target in case there + // are state changes that would cause it to now excute. At this point, however, we already know that the parent + // request has completed, and we already know that this request has the same global request ID, which means that + // its configuration and set of targets are identical -- from MSBuild's perspective, it's the same. So since + // we're not going to attempt to re-execute it, if there are skipped targets in the result, that's fine. We just + // need to know what the target results are so that we can log them. + ScheduleResponse response = TrySatisfyRequestFromCache(parentNode, unscheduledRequest.BuildRequest, skippedResultsAreOK: true); + + // If we have a response we need to tell the loggers that we satisified that request from the cache. + if (response != null) + { + LogRequestHandledFromCache(unscheduledRequest.BuildRequest, response.BuildResult); + } + else + { + // Response may be null if the result was never added to the cache. This can happen if the result has + // an exception in it. If that is the case, we should report the result directly so that the + // build manager knows that it needs to shut down logging manually. + response = GetResponseForResult(parentNode, unscheduledRequest.BuildRequest, newResult.Clone()); + } + + responses.Add(response); + + // Mark the request as complete (and the parent is no longer blocked by this request.) + unscheduledRequest.Complete(newResult); + } + } + } + + // This node may now be free, so run the scheduler. + ScheduleUnassignedRequests(responses); + return responses; + } + + /// + /// Signals that a node has been created. + /// + /// Information about the nodes which were created. + /// A new set of scheduling actions to take. + public IEnumerable ReportNodesCreated(IEnumerable nodeInfos) + { + _schedulingData.EventTime = DateTime.UtcNow; + + foreach (NodeInfo nodeInfo in nodeInfos) + { + _availableNodes[nodeInfo.NodeId] = nodeInfo; + TraceScheduler("Node {0} created", nodeInfo.NodeId); + + switch (nodeInfo.ProviderType) + { + case NodeProviderType.InProc: + _currentInProcNodeCount++; + break; + case NodeProviderType.OutOfProc: + _currentOutOfProcNodeCount++; + break; + case NodeProviderType.Remote: + default: + // this should never happen in the current MSBuild. + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + } + + List responses = new List(); + ScheduleUnassignedRequests(responses); + return responses; + } + + /// + /// Signals that the build has been aborted by the specified node. + /// + /// The node which reported the failure. + public void ReportBuildAborted(int nodeId) + { + _schedulingData.EventTime = DateTime.UtcNow; + + // Get the list of build requests currently assigned to the node and report aborted results for them. + TraceScheduler("Build aborted by node {0}", nodeId); + + foreach (SchedulableRequest request in _schedulingData.GetScheduledRequestsByNode(nodeId)) + { + MarkRequestAborted(request); + } + } + + /// + /// Resets the scheduler. + /// + public void Reset() + { + DumpConfigurations(); + DumpRequests(); + _schedulingPlan = null; + _schedulingData = new SchedulingData(); + _availableNodes = new Dictionary(8); + _currentInProcNodeCount = 0; + _currentOutOfProcNodeCount = 0; + + _nextGlobalRequestId = 0; + _customRequestSchedulingAlgorithm = null; + } + + /// + /// Writes out the detailed summary of the build. + /// + /// The id of the submission which is at the root of the build. + public void WriteDetailedSummary(int submissionId) + { + ILoggingService loggingService = _componentHost.LoggingService; + BuildEventContext context = new BuildEventContext(submissionId, 0, 0, 0, 0, 0); + loggingService.LogComment(context, MessageImportance.Normal, "DetailedSummaryHeader"); + + foreach (SchedulableRequest request in _schedulingData.GetRequestsByHierarchy(null)) + { + if (request.BuildRequest.SubmissionId == submissionId) + { + loggingService.LogComment(context, MessageImportance.Normal, "BuildHierarchyHeader"); + WriteRecursiveSummary(loggingService, context, submissionId, request, 0, false /* useConfigurations */, true /* isLastChild */); + } + } + + WriteNodeUtilizationGraph(loggingService, context, false /* useConfigurations */); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Initializes the component with the specified component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + _componentHost = host; + _resultsCache = (IResultsCache)_componentHost.GetComponent(BuildComponentType.ResultsCache); + _configCache = (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache); + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + Reset(); + } + + #endregion + + /// + /// Factory for component construction. + /// + static internal IBuildComponent CreateComponent(BuildComponentType componentType) + { + ErrorUtilities.VerifyThrow(componentType == BuildComponentType.Scheduler, "Cannot create components of type {0}", componentType); + return new Scheduler(); + } + + /// + /// Updates the state of a request based on its desire to yield or reacquire control of its node. + /// + private void HandleYieldAction(SchedulableRequest parentRequest, BuildRequestBlocker blocker) + { + if (blocker.YieldAction == YieldAction.Yield) + { + // Mark the request blocked. + parentRequest.Yield(blocker.TargetsInProgress); + } + else + { + // Mark the request ready. + parentRequest.Reacquire(); + } + } + + /// + /// Attempts to schedule unassigned requests to free nodes. + /// + /// The list which should be populated with responses from the scheduling. + private void ScheduleUnassignedRequests(List responses) + { + DateTime schedulingTime = DateTime.UtcNow; + + // See if we are done. We are done if there are no unassigned requests and no requests assigned to nodes. + if (_schedulingData.UnscheduledRequestsCount == 0 && + _schedulingData.ReadyRequestsCount == 0 && + _schedulingData.BlockedRequestsCount == 0 + ) + { + if (_schedulingData.ExecutingRequestsCount == 0 && _schedulingData.YieldingRequestsCount == 0) + { + // We are done. + TraceScheduler("Build complete"); + } + else + { + // Nodes still have work, but we have no requests. Let them proceed. + TraceScheduler("{0}: Waiting for existing work to proceed.", schedulingTime); + } + + return; + } + + // Resume any work available which has already been assigned to specific nodes. + ResumeRequiredWork(responses); + + int nodesFreeToDoWorkPriorToScheduling = 0; + HashSet idleNodes = new HashSet(); + foreach (int availableNodeId in _availableNodes.Keys) + { + if (!_schedulingData.IsNodeWorking(availableNodeId)) + { + idleNodes.Add(availableNodeId); + } + } + + nodesFreeToDoWorkPriorToScheduling = idleNodes.Count; + + // Assign requests to any nodes which are currently idle. + if (idleNodes.Count > 0 && _schedulingData.UnscheduledRequestsCount > 0) + { + AssignUnscheduledRequestsToNodes(responses, idleNodes); + } + + // If we have no nodes free to do work, we might need more nodes. This will occur if: + // 1) We still have unscheduled requests, because an additional node might allow us to execute those in parallel, or + // 2) We didn't schedule anything because there were no nodes to schedule to + bool createNodePending = false; + if (_schedulingData.UnscheduledRequestsCount > 0 || responses.Count == 0) + { + createNodePending = CreateNewNodeIfPossible(responses, _schedulingData.UnscheduledRequests); + } + + if (_availableNodes.Count > 0) + { + // If we failed to schedule any requests, report any results or create any nodes, we might be done. + if (_schedulingData.ExecutingRequestsCount > 0 || _schedulingData.YieldingRequestsCount > 0) + { + // We are still doing work. + } + else if (_schedulingData.UnscheduledRequestsCount == 0 && + _schedulingData.ReadyRequestsCount == 0 && + _schedulingData.BlockedRequestsCount == 0) + { + // We've exhausted our supply of work. + TraceScheduler("Build complete"); + } + else if (_schedulingData.BlockedRequestsCount != 0) + { + // It is legitimate to have blocked requests with none executing if none of the requests can + // be serviced by any currently existing node, or if they are blocked by requests, none of + // which can be serviced by any currently existing node. However, in that case, we had better + // be requesting the creation of a node that can service them. + // + // Note: This is O(# nodes * closure of requests blocking current set of blocked requests), + // but all three numbers should usually be fairly small and, more importantly, this situation + // should occur at most once per build, since it requires a situation where all blocked requests + // are blocked on the creation of a node that can service them. + foreach (SchedulableRequest request in _schedulingData.BlockedRequests) + { + if (RequestOrAnyItIsBlockedByCanBeServiced(request)) + { + DumpSchedulerState(); + ErrorUtilities.ThrowInternalError("Somehow no requests are currently executing, and at least one of the {0} requests blocked by in-progress requests is servicable by a currently existing node, but no circular dependency was detected ...", _schedulingData.BlockedRequestsCount); + } + } + + if (!createNodePending) + { + DumpSchedulerState(); + ErrorUtilities.ThrowInternalError("None of the {0} blocked requests can be serviced by currently existing nodes, but we aren't requesting a new one.", _schedulingData.BlockedRequestsCount); + } + } + else if (_schedulingData.ReadyRequestsCount != 0) + { + DumpSchedulerState(); + ErrorUtilities.ThrowInternalError("Somehow we have {0} requests which are ready to go but we didn't tell the nodes to continue.", _schedulingData.ReadyRequestsCount); + } + else if (_schedulingData.UnscheduledRequestsCount != 0 && !createNodePending) + { + DumpSchedulerState(); + ErrorUtilities.ThrowInternalError("Somehow we have {0} unassigned build requests but {1} of our nodes are free and we aren't requesting a new one...", _schedulingData.UnscheduledRequestsCount, idleNodes.Count); + } + } + else + { + ErrorUtilities.VerifyThrow(responses.Count > 0, "We failed to request a node to be created."); + } + + TraceScheduler("Requests scheduled: {0} Unassigned Requests: {1} Blocked Requests: {2} Unblockable Requests: {3} Free Nodes: {4}/{5} Responses: {6}", nodesFreeToDoWorkPriorToScheduling - idleNodes.Count, _schedulingData.UnscheduledRequestsCount, _schedulingData.BlockedRequestsCount, _schedulingData.ReadyRequestsCount, idleNodes.Count, _availableNodes.Count, responses.Count); + DumpSchedulerState(); + } + + /// + /// Determines which requests to assign to available nodes. + /// + /// + /// This is where all the real scheduling decisions take place. It should not be necessary to edit functions outside of this + /// to alter how scheduling occurs. + /// + private void AssignUnscheduledRequestsToNodes(List responses, HashSet idleNodes) + { + if (_componentHost.BuildParameters.MaxNodeCount == 1) + { + // In the single-proc case, there are no decisions to be made. First-come, first-serve. + AssignUnscheduledRequestsFIFO(responses, idleNodes); + } + else + { + bool haveValidPlan = GetSchedulingPlanAndAlgorithm(); + + if (_customRequestSchedulingAlgorithm != null) + { + _customRequestSchedulingAlgorithm(responses, idleNodes); + } + else + { + // We want to find more work first, and we assign traversals to the in-proc node first, if possible. + AssignUnscheduledRequestsByTraversalsFirst(responses, idleNodes); + if (idleNodes.Count == 0) + { + return; + } + + if (haveValidPlan) + { + if (_componentHost.BuildParameters.MaxNodeCount == 2) + { + AssignUnscheduledRequestsWithPlanByMostImmediateReferences(responses, idleNodes); + } + else + { + AssignUnscheduledRequestsWithPlanByGreatestPlanTime(responses, idleNodes); + } + } + else + { + AssignUnscheduledRequestsWithConfigurationCountLevelling(responses, idleNodes); + } + } + } + } + + /// + /// Reads in the scheduling plan if one exists and has not previously been read; returns true if the scheduling plan + /// both exists and is valid, or false otherwise. + /// + private bool GetSchedulingPlanAndAlgorithm() + { + // Read the plan, if any. + if (_schedulingPlan == null) + { + _schedulingPlan = new SchedulingPlan(_configCache, _schedulingData); + ReadSchedulingPlan(_schedulingData.GetRequestsByHierarchy(null).First().BuildRequest.SubmissionId); + } + + if (_customRequestSchedulingAlgorithm == null) + { + string customScheduler = Environment.GetEnvironmentVariable("MSBUILDCUSTOMSCHEDULER"); + + if (!String.IsNullOrEmpty(customScheduler)) + { + // Assign to the delegate + if (customScheduler.Equals("WithPlanByMostImmediateReferences", StringComparison.OrdinalIgnoreCase) && _schedulingPlan.IsPlanValid) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithPlanByMostImmediateReferences; + } + else if (customScheduler.Equals("WithPlanByGreatestPlanTime", StringComparison.OrdinalIgnoreCase) && _schedulingPlan.IsPlanValid) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithPlanByGreatestPlanTime; + } + else if (customScheduler.Equals("ByTraversalsFirst", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsByTraversalsFirst; + } + else if (customScheduler.Equals("WithConfigurationCountLevelling", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithConfigurationCountLevelling; + } + else if (customScheduler.Equals("WithSmallestFileSize", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithSmallestFileSize; + } + else if (customScheduler.Equals("WithLargestFileSize", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithLargestFileSize; + } + else if (customScheduler.Equals("WithMaxWaitingRequests", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithMaxWaitingRequests; + } + else if (customScheduler.Equals("WithMaxWaitingRequests2", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsWithMaxWaitingRequests2; + } + else if (customScheduler.Equals("FIFO", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsFIFO; + } + else if (customScheduler.Equals("CustomSchedulerForSQL", StringComparison.OrdinalIgnoreCase)) + { + _customRequestSchedulingAlgorithm = AssignUnscheduledRequestsUsingCustomSchedulerForSQL; + + string multiplier = Environment.GetEnvironmentVariable("MSBUILDCUSTOMSCHEDULERFORSQLCONFIGURATIONLIMITMULTIPLIER"); + double convertedMultiplier = 0; + if (!Double.TryParse(multiplier, out convertedMultiplier) || convertedMultiplier < 1) + { + _customSchedulerForSQLConfigurationLimitMultiplier = DefaultCustomSchedulerForSQLConfigurationLimitMultiplier; + } + else + { + _customSchedulerForSQLConfigurationLimitMultiplier = convertedMultiplier; + } + } + } + } + + return _schedulingPlan.IsPlanValid; + } + + /// + /// Assigns requests to nodes based on those which refer to the most other projects. + /// + private void AssignUnscheduledRequestsWithPlanByMostImmediateReferences(List responses, HashSet idleNodes) + { + foreach (int idleNodeId in idleNodes) + { + Dictionary configsWhichCanBeScheduledToThisNode = new Dictionary(); + + // Find the most expensive request in the plan to schedule from among the ones available. + foreach (SchedulableRequest request in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + if (CanScheduleRequestToNode(request, idleNodeId)) + { + configsWhichCanBeScheduledToThisNode[request.BuildRequest.ConfigurationId] = request; + } + } + + if (configsWhichCanBeScheduledToThisNode.Count > 0) + { + int configToSchedule = _schedulingPlan.GetConfigWithGreatestNumberOfReferences(configsWhichCanBeScheduledToThisNode.Keys); + + ErrorUtilities.VerifyThrow(configToSchedule != BuildRequestConfiguration.InvalidConfigurationId, "No configuration returned even though there are some available."); + AssignUnscheduledRequestToNode(configsWhichCanBeScheduledToThisNode[configToSchedule], idleNodeId, responses); + } + } + } + + /// + /// Assigns requests to nodes based on those which have the most plan time. + /// + private void AssignUnscheduledRequestsWithPlanByGreatestPlanTime(List responses, HashSet idleNodes) + { + foreach (int idleNodeId in idleNodes) + { + Dictionary configsWhichCanBeScheduledToThisNode = new Dictionary(); + + // Find the most expensive request in the plan to schedule from among the ones available. + foreach (SchedulableRequest request in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + if (CanScheduleRequestToNode(request, idleNodeId)) + { + configsWhichCanBeScheduledToThisNode[request.BuildRequest.ConfigurationId] = request; + } + } + + if (configsWhichCanBeScheduledToThisNode.Count > 0) + { + int configToSchedule = _schedulingPlan.GetConfigWithGreatestPlanTime(configsWhichCanBeScheduledToThisNode.Keys); + + ErrorUtilities.VerifyThrow(configToSchedule != BuildRequestConfiguration.InvalidConfigurationId, "No configuration returned even though there are some available."); + AssignUnscheduledRequestToNode(configsWhichCanBeScheduledToThisNode[configToSchedule], idleNodeId, responses); + } + } + } + + /// + /// Assigns requests preferring those which are traversal projects as determined by filename. + /// + private void AssignUnscheduledRequestsByTraversalsFirst(List responses, HashSet idleNodes) + { + if (idleNodes.Contains(InProcNodeId)) + { + // Assign traversal projects first (to find more work.) + List unscheduledRequests = new List(_schedulingData.UnscheduledRequestsWhichCanBeScheduled); + foreach (SchedulableRequest request in unscheduledRequests) + { + if (CanScheduleRequestToNode(request, InProcNodeId)) + { + if (IsTraversalRequest(request.BuildRequest)) + { + AssignUnscheduledRequestToNode(request, InProcNodeId, responses); + idleNodes.Remove(InProcNodeId); + break; + } + } + } + } + } + + /// + /// Returns true if the request is for a traversal project. Traversals are used to find more work. + /// + private bool IsTraversalRequest(BuildRequest request) + { + return _configCache[request.ConfigurationId].IsTraversal; + } + + /// + /// Assigns requests to nodes attempting to ensure each node has the same number of configurations assigned to it. + /// + private void AssignUnscheduledRequestsWithConfigurationCountLevelling(List responses, HashSet idleNodes) + { + // Assign requests but try to keep the same number of configurations on each node + List nodesByConfigurationCountAscending = new List(_availableNodes.Keys); + nodesByConfigurationCountAscending.Sort(delegate (int left, int right) + { + return Comparer.Default.Compare(_schedulingData.GetConfigurationsCountByNode(left, true /* excludeTraversals */, _configCache), _schedulingData.GetConfigurationsCountByNode(right, true /* excludeTraversals */, _configCache)); + }); + + // Assign projects to nodes, preferring to assign work to nodes with the fewest configurations first. + foreach (int nodeId in nodesByConfigurationCountAscending) + { + if (!idleNodes.Contains(nodeId)) + { + continue; + } + + if (AtSchedulingLimit()) + { + TraceScheduler("System load limit reached, cannot schedule new work. Executing: {0} Yielding: {1} Max Count: {2}", _schedulingData.ExecutingRequestsCount, _schedulingData.YieldingRequestsCount, _componentHost.BuildParameters.MaxNodeCount); + break; + } + + foreach (SchedulableRequest request in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + if (CanScheduleRequestToNode(request, nodeId)) + { + AssignUnscheduledRequestToNode(request, nodeId, responses); + idleNodes.Remove(nodeId); + break; + } + } + } + + // Now they either all have the same number of configurations of we can no longer assign work. Let the default scheduling algorithm + // determine if any more work can be assigned. + AssignUnscheduledRequestsFIFO(responses, idleNodes); + } + + /// + /// Assigns requests with the smallest file sizes first. + /// + private void AssignUnscheduledRequestsWithSmallestFileSize(List responses, HashSet idleNodes) + { + // Assign requests with the largest file sizes. + while (idleNodes.Count > 0 && _schedulingData.UnscheduledRequestsCount > 0) + { + SchedulableRequest requestWithSmallestSourceFile = null; + int requestRequiredNodeId = InvalidNodeId; + long sizeOfSmallestSourceFile = long.MaxValue; + + foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + int requiredNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(unscheduledRequest.BuildRequest.ConfigurationId); + if (requiredNodeId == InvalidNodeId || idleNodes.Contains(requiredNodeId)) + { + // Look for a request with the smallest source file + System.IO.FileInfo f = new FileInfo(_configCache[unscheduledRequest.BuildRequest.ConfigurationId].ProjectFullPath); + if (f.Length < sizeOfSmallestSourceFile) + { + sizeOfSmallestSourceFile = f.Length; + requestWithSmallestSourceFile = unscheduledRequest; + requestRequiredNodeId = requiredNodeId; + } + } + } + + if (requestWithSmallestSourceFile != null) + { + int nodeIdToAssign = requestRequiredNodeId == InvalidNodeId ? idleNodes.First() : requestRequiredNodeId; + AssignUnscheduledRequestToNode(requestWithSmallestSourceFile, nodeIdToAssign, responses); + idleNodes.Remove(nodeIdToAssign); + } + else + { + // No more requests we can schedule. + break; + } + } + } + + /// + /// Assigns requests with the largest file sizes first. + /// + private void AssignUnscheduledRequestsWithLargestFileSize(List responses, HashSet idleNodes) + { + // Assign requests with the largest file sizes. + while (idleNodes.Count > 0 && _schedulingData.UnscheduledRequestsCount > 0) + { + SchedulableRequest requestWithLargestSourceFile = null; + int requestRequiredNodeId = InvalidNodeId; + long sizeOfLargestSourceFile = 0; + + foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + int requiredNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(unscheduledRequest.BuildRequest.ConfigurationId); + if (requiredNodeId == InvalidNodeId || idleNodes.Contains(requiredNodeId)) + { + // Look for a request with the largest source file + System.IO.FileInfo f = new FileInfo(_configCache[unscheduledRequest.BuildRequest.ConfigurationId].ProjectFullPath); + if (f.Length > sizeOfLargestSourceFile) + { + sizeOfLargestSourceFile = f.Length; + requestWithLargestSourceFile = unscheduledRequest; + requestRequiredNodeId = requiredNodeId; + } + } + } + + if (requestWithLargestSourceFile != null) + { + int nodeIdToAssign = requestRequiredNodeId == InvalidNodeId ? idleNodes.First() : requestRequiredNodeId; + AssignUnscheduledRequestToNode(requestWithLargestSourceFile, nodeIdToAssign, responses); + idleNodes.Remove(nodeIdToAssign); + } + else + { + // No more requests we can schedule. + break; + } + } + } + + /// + /// Assigns requests preferring the ones which have the most other requests waiting on them using the transitive closure. + /// + private void AssignUnscheduledRequestsWithMaxWaitingRequests(List responses, HashSet idleNodes) + { + // Assign requests based on how many other requests depend on them + foreach (int nodeId in idleNodes) + { + int maxWaitingRequests = 0; + SchedulableRequest requestToSchedule = null; + SchedulableRequest requestToScheduleNoAffinity = null; + SchedulableRequest requestToScheduleWithAffinity = null; + foreach (SchedulableRequest currentSchedulableRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + BuildRequest currentRequest = currentSchedulableRequest.BuildRequest; + int requiredNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(currentRequest.ConfigurationId); + + // This performs the depth-first traversal, assuming that the unassigned build requests has been populated such that the + // top-most requests are the ones most recently issued. We schedule the first request which can be scheduled to this node. + if (requiredNodeId == InvalidNodeId || requiredNodeId == nodeId) + { + // Get the affinity from the request first. + NodeAffinity nodeAffinity = GetNodeAffinityForRequest(currentRequest); + + if (_availableNodes[nodeId].CanServiceRequestWithAffinity(nodeAffinity)) + { + // Get the 'most depended upon' request and schedule that. + int requestsWaiting = ComputeClosureOfWaitingRequests(currentSchedulableRequest); + bool selectedRequest = false; + if (requestsWaiting > maxWaitingRequests) + { + requestToSchedule = currentSchedulableRequest; + maxWaitingRequests = requestsWaiting; + selectedRequest = true; + } + else if (maxWaitingRequests == 0 && requestToSchedule == null) + { + requestToSchedule = currentSchedulableRequest; + selectedRequest = true; + } + + // If we decided this request is a candidate, update the affinity-specific reference + // for later. + if (selectedRequest) + { + if (requiredNodeId == InvalidNodeId) + { + requestToScheduleNoAffinity = requestToSchedule; + } + else + { + requestToScheduleWithAffinity = requestToSchedule; + } + } + } + } + } + + // Prefer to schedule requests which MUST go on this node instead of those which could go on any node. + // This helps to prevent us from accumulating tons of request affinities toward a single node. + if (requestToScheduleWithAffinity != null) + { + requestToSchedule = requestToScheduleWithAffinity; + } + else + { + requestToSchedule = requestToScheduleNoAffinity; + } + + if (requestToSchedule != null) + { + AssignUnscheduledRequestToNode(requestToSchedule, nodeId, responses); + } + } + } + + /// + /// Assigns requests preferring those with the most requests waiting on them, but only counting those requests which are + /// directly waiting, as opposed to the transitive closure. + /// + private void AssignUnscheduledRequestsWithMaxWaitingRequests2(List responses, HashSet idleNodes) + { + // Assign requests based on how many other requests depend on them + foreach (int nodeId in idleNodes) + { + // Find the request with the most waiting requests + SchedulableRequest mostWaitingRequests = null; + foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + if (CanScheduleRequestToNode(unscheduledRequest, nodeId)) + { + if (mostWaitingRequests == null || unscheduledRequest.RequestsWeAreBlockingCount > mostWaitingRequests.RequestsWeAreBlockingCount) + { + mostWaitingRequests = unscheduledRequest; + } + } + } + + if (mostWaitingRequests != null) + { + AssignUnscheduledRequestToNode(mostWaitingRequests, nodeId, responses); + } + } + } + + /// + /// Assigns requests on a first-come, first-serve basis. + /// + private void AssignUnscheduledRequestsFIFO(List responses, HashSet idleNodes) + { + // Assign requests on a first-come/first-serve basis + foreach (int nodeId in idleNodes) + { + // Don't overload the system. + if (AtSchedulingLimit()) + { + TraceScheduler("System load limit reached, cannot schedule new work. Executing: {0} Yielding: {1} Max Count: {2}", _schedulingData.ExecutingRequestsCount, _schedulingData.YieldingRequestsCount, _componentHost.BuildParameters.MaxNodeCount); + return; + } + + foreach (SchedulableRequest unscheduledRequest in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + if (CanScheduleRequestToNode(unscheduledRequest, nodeId)) + { + AssignUnscheduledRequestToNode(unscheduledRequest, nodeId, responses); + break; + } + } + } + } + + /// + /// Custom scheduler for the SQL folks to solve a performance problem with their builds where they end up with a few long-running + /// requests on all but one node, and then a very large number of short-running requests on that one node -- which is by design for + /// our current scheduler, but makes it so that later in the build, when these configurations are re-entered with new requests, the + /// build becomes essentially serial because so many of the configurations are tied to that one node. + /// + /// Fixes that problem by intentionally choosing to refrain from assigning new configurations to idle nodes if those idle nodes already + /// have more than their fair share of the existing configurations assigned to them. + /// + private void AssignUnscheduledRequestsUsingCustomSchedulerForSQL(List responses, HashSet idleNodes) + { + // We want to find more work first, and we assign traversals to the in-proc node first, if possible. + AssignUnscheduledRequestsByTraversalsFirst(responses, idleNodes); + if (idleNodes.Count == 0) + { + return; + } + + Dictionary configurationCountsByNode = new Dictionary(_availableNodes.Count); + + // The configuration count limit will be the average configuration count * X (to allow for some wiggle room) where + // the default value of X is 1.1 (+ 10%) + int configurationCountLimit = 0; + + foreach (int availableNodeId in _availableNodes.Keys) + { + configurationCountsByNode[availableNodeId] = _schedulingData.GetConfigurationsCountByNode(availableNodeId, true /* excludeTraversals */, _configCache); + configurationCountLimit += configurationCountsByNode[availableNodeId]; + } + + configurationCountLimit = Math.Max(1, (int)Math.Ceiling(configurationCountLimit * _customSchedulerForSQLConfigurationLimitMultiplier / _availableNodes.Count)); + + // Assign requests but try to keep the same number of configurations on each node + List nodesByConfigurationCountAscending = new List(_availableNodes.Keys); + nodesByConfigurationCountAscending.Sort(delegate (int left, int right) + { + return Comparer.Default.Compare(configurationCountsByNode[left], configurationCountsByNode[right]); + }); + + // Assign projects to nodes, preferring to assign work to nodes with the fewest configurations first. + foreach (int nodeId in nodesByConfigurationCountAscending) + { + if (!idleNodes.Contains(nodeId)) + { + continue; + } + + if (AtSchedulingLimit()) + { + TraceScheduler("System load limit reached, cannot schedule new work. Executing: {0} Yielding: {1} Max Count: {2}", _schedulingData.ExecutingRequestsCount, _schedulingData.YieldingRequestsCount, _componentHost.BuildParameters.MaxNodeCount); + break; + } + + foreach (SchedulableRequest request in _schedulingData.UnscheduledRequestsWhichCanBeScheduled) + { + if (CanScheduleRequestToNode(request, nodeId)) + { + int requiredNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(request.BuildRequest.ConfigurationId); + + // Only schedule an entirely new configuration (one not already tied to this node) to this node if we're + // not already over the limit needed to keep a reasonable balance. + if (request.AssignedNode == nodeId || requiredNodeId == nodeId || configurationCountsByNode[nodeId] <= configurationCountLimit) + { + AssignUnscheduledRequestToNode(request, nodeId, responses); + idleNodes.Remove(nodeId); + break; + } + else if (configurationCountsByNode[nodeId] > configurationCountLimit) + { + TraceScheduler("Chose not to assign request {0} to node {2} because its count of configurations ({3}) exceeds the current limit ({4}).", request.BuildRequest.GlobalRequestId, request.BuildRequest.ConfigurationId, nodeId, configurationCountsByNode[nodeId], configurationCountLimit); + } + } + } + } + + // at this point, we may still have work left unassigned, but that's OK -- we're deliberately choosing to delay assigning all available + // requests in order to avoid overloading certain nodes with excess numbers of requests. + } + + /// + /// Assigns the specified request to the specified node. + /// + private void AssignUnscheduledRequestToNode(SchedulableRequest request, int nodeId, List responses) + { + ErrorUtilities.VerifyThrowArgumentNull(request, "request"); + ErrorUtilities.VerifyThrowArgumentNull(responses, "responses"); + ErrorUtilities.VerifyThrow(nodeId != InvalidNodeId, "Invalid node id specified."); + + // Currently we cannot move certain kinds of traversals (notably solution metaprojects) to other nodes because + // they only have a ProjectInstance representation, and besides these kinds of projects build very quickly + // and produce more references (more work to do.) This just verifies we do not attempt to send a traversal to + // an out-of-proc node because doing so is inefficient and presently will cause the engine to fail on the remote + // node because these projects cannot be found. + ErrorUtilities.VerifyThrow(nodeId == InProcNodeId || _forceAffinityOutOfProc || !IsTraversalRequest(request.BuildRequest), "Can't assign traversal request to out-of-proc node!"); + request.VerifyState(SchedulableRequestState.Unscheduled); + + // Determine if this node has seen our configuration before. If not, we must send it along with this request. + bool mustSendConfigurationToNode = _availableNodes[nodeId].AssignConfiguration(request.BuildRequest.ConfigurationId); + + // If this is the first time this configuration has been assigned to a node, we will mark the configuration with the assigned node + // indicating that the master set of results is located there. Should we ever need to move the results, we will know where to find them. + BuildRequestConfiguration config = _configCache[request.BuildRequest.ConfigurationId]; + if (config.ResultsNodeId == InvalidNodeId) + { + config.ResultsNodeId = nodeId; + } + + ErrorUtilities.VerifyThrow(config.ResultsNodeId != InvalidNodeId, "Configuration's results node is not set."); + + responses.Add(ScheduleResponse.CreateScheduleResponse(nodeId, request.BuildRequest, mustSendConfigurationToNode)); + TraceScheduler("Executing request {0} on node {1} with parent {2}", request.BuildRequest.GlobalRequestId, nodeId, (request.Parent == null) ? -1 : request.Parent.BuildRequest.GlobalRequestId); + request.ResumeExecution(nodeId); + } + + /// + /// Returns true if we are at the limit of work we can schedule. + /// + private bool AtSchedulingLimit() + { + if (_schedulingUnlimited) + { + return false; + } + + int limit = 0; + switch (_componentHost.BuildParameters.MaxNodeCount) + { + case 1: + limit = 1; + break; + + case 2: + limit = _componentHost.BuildParameters.MaxNodeCount + 1 + _nodeLimitOffset; + break; + + default: + limit = _componentHost.BuildParameters.MaxNodeCount + 2 + _nodeLimitOffset; + break; + } + + // We're at our limit of schedulable requests if: + // (1) MaxNodeCount requests are currently executing + // (2) Fewer than MaxNodeCount requests are currently executing but the sum of executing + // and yielding requests exceeds the limit set out above. + return _schedulingData.ExecutingRequestsCount + _schedulingData.YieldingRequestsCount >= limit || + _schedulingData.ExecutingRequestsCount >= _componentHost.BuildParameters.MaxNodeCount; + } + + /// + /// Returns true if a request can be scheduled to a node, false otherwise. + /// + private bool CanScheduleRequestToNode(SchedulableRequest request, int nodeId) + { + if (_schedulingData.CanScheduleRequestToNode(request, nodeId)) + { + NodeAffinity affinity = GetNodeAffinityForRequest(request.BuildRequest); + bool result = _availableNodes[nodeId].CanServiceRequestWithAffinity(affinity); + return result; + } + + return false; + } + + /// + /// Adds CreateNode responses to satisfy all the affinities in the list of requests, with the following constraints: + /// + /// a) Issue no more than one response to create an inproc node, and aggressively issues as many requests for an out-of-proc node + /// as there are requests to assign to them. + /// + /// b) Don't exceed the max node count, *unless* there isn't even one node of the necessary affinity yet. (That means that even if there's a max + /// node count of e.g., 3, and we have already created 3 out of proc nodes, we will still create an inproc node if affinity requires it; if + /// we didn't, the build would jam.) + /// + /// Returns true if there is a pending response to create a new node. + /// + private bool CreateNewNodeIfPossible(List responses, IEnumerable requests) + { + int availableNodesWithInProcAffinity = 1 - _currentInProcNodeCount; + int availableNodesWithOutOfProcAffinity = _componentHost.BuildParameters.MaxNodeCount - _currentOutOfProcNodeCount; + int requestsWithOutOfProcAffinity = 0; + int requestsWithAnyAffinityOnInProcNodes = 0; + + int inProcNodesToCreate = 0; + int outOfProcNodesToCreate = 0; + + foreach (SchedulableRequest request in requests) + { + int assignedNodeForConfiguration = _schedulingData.GetAssignedNodeForRequestConfiguration(request.BuildRequest.ConfigurationId); + + // Although this request has not been scheduled, this configuration may previously have been + // scheduled to an existing node. If so, we shouldn't count it in our checks for new node + // creation, because it'll only eventually get assigned to its existing node anyway. + if (assignedNodeForConfiguration != Scheduler.InvalidNodeId) + { + continue; + } + + NodeAffinity affinityRequired = GetNodeAffinityForRequest(request.BuildRequest); + + switch (affinityRequired) + { + case NodeAffinity.InProc: + inProcNodesToCreate++; + + // If we've previously seen "Any"-affinitized requests, now that there are some + // genuine inproc requests, they get to play with the inproc node first, so + // push the "Any" requests to the out-of-proc nodes. + if (requestsWithAnyAffinityOnInProcNodes > 0) + { + requestsWithAnyAffinityOnInProcNodes--; + outOfProcNodesToCreate++; + } + + break; + case NodeAffinity.OutOfProc: + outOfProcNodesToCreate++; + requestsWithOutOfProcAffinity++; + break; + case NodeAffinity.Any: + // Prefer inproc node if there's space, but otherwise apportion to out-of-proc. + if (inProcNodesToCreate < availableNodesWithInProcAffinity && !_componentHost.BuildParameters.DisableInProcNode) + { + inProcNodesToCreate++; + requestsWithAnyAffinityOnInProcNodes++; + } + else + { + outOfProcNodesToCreate++; + + // If we are *required* to create an OOP node because the IP node is disabled, then treat this as if + // the request had an OOP affinity. + if (_componentHost.BuildParameters.DisableInProcNode) + { + requestsWithOutOfProcAffinity++; + } + } + + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + + // We've already hit the limit of the number of nodes we'll be allowed to create, so just quit counting now. + if (inProcNodesToCreate >= availableNodesWithInProcAffinity && outOfProcNodesToCreate >= availableNodesWithOutOfProcAffinity) + { + break; + } + } + + // If we think we want to create inproc nodes + if (inProcNodesToCreate > 0) + { + // In-proc node determination is simple: we want as many as are available. + inProcNodesToCreate = Math.Min(availableNodesWithInProcAffinity, inProcNodesToCreate); + + // If we still want to create one, go ahead + if (inProcNodesToCreate > 0) + { + ErrorUtilities.VerifyThrow(inProcNodesToCreate == 1, "We should never be trying to create more than one inproc node"); + TraceScheduler("Requesting creation of new node satisfying affinity {0}", NodeAffinity.InProc); + responses.Add(ScheduleResponse.CreateNewNodeResponse(NodeAffinity.InProc, 1)); + + // We only want to submit one node creation request at a time -- as part of node creation we recursively re-request the scheduler + // to do more scheduling, so the other request will be dealt with soon enough. + return true; + } + } + + // If we think we want to create out-of-proc nodes + if (outOfProcNodesToCreate > 0) + { + // Out-of-proc node determination is a bit more complicated. If we have N out-of-proc requests, we want to + // fill up to N out-of-proc nodes. However, if we have N "any" requests, we must assume that at least some of them + // will be fulfilled by the inproc node, in which case we only want to launch up to N-1 out-of-proc nodes, for a + // total of N nodes overall -- the scheduler will only schedule to N nodes at a time, so launching any more than that + // is ultimately pointless. + int maxCreatableOutOfProcNodes = availableNodesWithOutOfProcAffinity; + + if (requestsWithOutOfProcAffinity < availableNodesWithOutOfProcAffinity) + { + // We don't have enough explicitly out-of-proc requests to justify creating every technically allowed + // out-of-proc node, so our max is actually one less than the absolute max for the reasons explained above. + maxCreatableOutOfProcNodes--; + } + + outOfProcNodesToCreate = Math.Min(maxCreatableOutOfProcNodes, outOfProcNodesToCreate); + + // If we still want to create them, go ahead + if (outOfProcNodesToCreate > 0) + { + TraceScheduler("Requesting creation of {0} new node(s) satisfying affinity {1}", outOfProcNodesToCreate, NodeAffinity.OutOfProc); + responses.Add(ScheduleResponse.CreateNewNodeResponse(NodeAffinity.OutOfProc, outOfProcNodesToCreate)); + } + + // We only want to submit one node creation request at a time -- as part of node creation we recursively re-request the scheduler + // to do more scheduling, so the other request will be dealt with soon enough. + return true; + } + + // If we haven't returned before now, we haven't asked that any new nodes be created. + return false; + } + + /// + /// Marks the specified request and all of its ancestors as having aborted. + /// + private void MarkRequestAborted(SchedulableRequest request) + { + _resultsCache.AddResult(new BuildResult(request.BuildRequest, new BuildAbortedException())); + + // Recursively abort all of the requests we are blocking. + foreach (SchedulableRequest blockedRequest in request.RequestsWeAreBlocking) + { + MarkRequestAborted(blockedRequest); + } + } + + /// + /// Marks the request as being blocked by another request which is currently building a target whose results we need to proceed. + /// + private void HandleRequestBlockedOnInProgressTarget(SchedulableRequest blockedRequest, BuildRequestBlocker blocker) + { + ErrorUtilities.VerifyThrowArgumentNull(blockedRequest, "blockedRequest"); + ErrorUtilities.VerifyThrowArgumentNull(blocker, "blocker"); + + // We are blocked on an in-progress request building a target whose results we need. + SchedulableRequest blockingRequest = _schedulingData.GetScheduledRequest(blocker.BlockingRequestId); + + // The request we blocked on couldn't have been executing (because we are) so it must either be yielding (which is ok because + // it isn't modifying its own state, just running a background process), ready, or still blocked. + blockingRequest.VerifyOneOfStates(new SchedulableRequestState[] { SchedulableRequestState.Yielding, SchedulableRequestState.Ready, SchedulableRequestState.Blocked }); + + blockedRequest.BlockByRequest(blockingRequest, blocker.TargetsInProgress); + } + + /// + /// Marks the parent as blocked waiting for results from a results transfer. + /// + private void HandleRequestBlockedOnResultsTransfer(SchedulableRequest parentRequest, List responses) + { + // Create the new request which will go to the configuration's results node. + BuildRequest newRequest = new BuildRequest(parentRequest.BuildRequest.SubmissionId, BuildRequest.ResultsTransferNodeRequestId, parentRequest.BuildRequest.ConfigurationId, new string[0], null, parentRequest.BuildRequest.BuildEventContext, parentRequest.BuildRequest, parentRequest.BuildRequest.BuildRequestDataFlags); + + // Assign a new global request id - always different from any other. + newRequest.GlobalRequestId = _nextGlobalRequestId; + _nextGlobalRequestId++; + + // Now add the response. Send it to the node where the configuration's results are stored. When those results come back + // we will update the storage location in the configuration. This is doing a bit of a run around the scheduler - we don't + // create a new formal request, so we treat the blocked request as if it is still executing - this prevents any other requests + // from getting onto that node and also means we don't have to do additional work to get the scheduler to understand the bizarre + // case of sending a request for results from a project's own configuration (which it believes reside on the very node which + // is actually requesting the results in the first place.) + BuildRequestConfiguration configuration = _configCache[parentRequest.BuildRequest.ConfigurationId]; + responses.Add(ScheduleResponse.CreateScheduleResponse(configuration.ResultsNodeId, newRequest, false)); + + TraceScheduler("Created request {0} (node request {1}) for transfer of configuration {2}'s results from node {3} to node {4}", newRequest.GlobalRequestId, newRequest.NodeRequestId, configuration.ConfigurationId, configuration.ResultsNodeId, parentRequest.AssignedNode); + + // The configuration's results will now be homed at the new location (once they have come back from the + // original node.) + configuration.ResultsNodeId = parentRequest.AssignedNode; + } + + /// + /// Marks the request as being blocked by new requests whose results we must get before we can proceed. + /// + private void HandleRequestBlockedByNewRequests(SchedulableRequest parentRequest, BuildRequestBlocker blocker, List responses) + { + ErrorUtilities.VerifyThrowArgumentNull(blocker, "blocker"); + ErrorUtilities.VerifyThrowArgumentNull(responses, "responses"); + + // The request is waiting on new requests. + bool abortRequestBatch = false; + Stack requestsToAdd = new Stack(blocker.BuildRequests.Length); + foreach (BuildRequest request in blocker.BuildRequests) + { + // Assign a global request id to this request. + if (request.GlobalRequestId == BuildRequest.InvalidGlobalRequestId) + { + AssignGlobalRequestId(request); + } + + int nodeForResults = (parentRequest == null) ? InvalidNodeId : parentRequest.AssignedNode; + TraceScheduler("Received request {0} (node request {1}) with parent {2} from node {3}", request.GlobalRequestId, request.NodeRequestId, request.ParentGlobalRequestId, nodeForResults); + + // First, determine if we have already built this request and have results for it. If we do, we prepare the responses for it + // directly here. We COULD simply report these as blocking the parent request and let the scheduler pick them up later when the parent + // comes back up as schedulable, but we prefer to send the results back immediately so this request can (potentially) continue uninterrupted. + ScheduleResponse response = TrySatisfyRequestFromCache(nodeForResults, request, skippedResultsAreOK: false); + if (null != response) + { + TraceScheduler("Request {0} (node request {1}) satisfied from the cache.", request.GlobalRequestId, request.NodeRequestId); + + // BuildResult result = (response.Action == ScheduleActionType.Unblock) ? response.Unblocker.Results[0] : response.BuildResult; + LogRequestHandledFromCache(request, response.BuildResult); + responses.Add(response); + + // If we wish to implement an algorithm where the first failing request aborts the remaining request, check for + // overall result being failure rather than just circular dependency. Sync with BasicScheduler.ReportResult and + // BuildRequestEntry.ReportResult. + if (response.BuildResult.CircularDependency) + { + abortRequestBatch = true; + } + } + else + { + // Ensure there is no affinity mismatch between this request and a previous request of the same configuration. + NodeAffinity requestAffinity = GetNodeAffinityForRequest(request); + NodeAffinity existingRequestAffinity = NodeAffinity.Any; + if (requestAffinity != NodeAffinity.Any) + { + bool affinityMismatch = false; + int assignedNodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(request.ConfigurationId); + if (assignedNodeId != Scheduler.InvalidNodeId) + { + if (!_availableNodes[assignedNodeId].CanServiceRequestWithAffinity(GetNodeAffinityForRequest(request))) + { + // This request's configuration has already been assigned to a node which cannot service this affinity. + if (_schedulingData.GetRequestsAssignedToConfigurationCount(request.ConfigurationId) == 0) + { + // If there are no other requests already scheduled for that configuration, we can safely reassign. + _schedulingData.UnassignNodeForRequestConfiguration(request.ConfigurationId); + } + else + { + existingRequestAffinity = (_availableNodes[assignedNodeId].ProviderType == NodeProviderType.InProc) ? NodeAffinity.InProc : NodeAffinity.OutOfProc; + affinityMismatch = true; + } + } + } + else if (_schedulingData.GetRequestsAssignedToConfigurationCount(request.ConfigurationId) > 0) + { + // Would any other existing requests for this configuration mismatch? + foreach (SchedulableRequest existingRequest in _schedulingData.GetRequestsAssignedToConfiguration(request.ConfigurationId)) + { + existingRequestAffinity = GetNodeAffinityForRequest(existingRequest.BuildRequest); + if (existingRequestAffinity != NodeAffinity.Any && existingRequestAffinity != requestAffinity) + { + // The existing request has an affinity which doesn't match this one, so this one could never be scheduled. + affinityMismatch = true; + break; + } + } + } + + if (affinityMismatch) + { + BuildResult result = new BuildResult(request, new InvalidOperationException(ResourceUtilities.FormatResourceString("AffinityConflict", requestAffinity, existingRequestAffinity))); + response = GetResponseForResult(nodeForResults, request, result); + responses.Add(response); + continue; + } + } + + // Now add the requests so they would naturally be picked up by the scheduler in the order they were issued, + // but before any other requests in the list. This makes us prefer a depth-first traversal. + requestsToAdd.Push(request); + } + } + + // Now add any unassigned build requests. + if (!abortRequestBatch) + { + if (requestsToAdd.Count == 0) + { + // All of the results are being reported directly from the cache (from above), so this request can continue on its merry way. + if (parentRequest != null) + { + // responses.Add(new ScheduleResponse(parentRequest.AssignedNode, new BuildRequestUnblocker(parentRequest.BuildRequest.GlobalRequestId))); + responses.Add(ScheduleResponse.CreateResumeExecutionResponse(parentRequest.AssignedNode, parentRequest.BuildRequest.GlobalRequestId)); + } + } + else + { + while (requestsToAdd.Count > 0) + { + BuildRequest requestToAdd = requestsToAdd.Pop(); + SchedulableRequest blockingRequest = _schedulingData.CreateRequest(requestToAdd, parentRequest); + + if (parentRequest != null) + { + parentRequest.BlockByRequest(blockingRequest, blocker.TargetsInProgress); + } + } + } + } + } + + /// + /// Resumes executing a request which was in the Ready state for the specified node, if any. + /// + private void ResumeReadyRequestIfAny(int nodeId, List responses) + { + // Look for ready requests. We prefer to let these continue first rather than finding new work. + // We only actually look at the first one, since that is all we need. + foreach (SchedulableRequest request in _schedulingData.GetReadyRequestsByNode(nodeId)) + { + TraceScheduler("Unblocking request {0} on node {1}", request.BuildRequest.GlobalRequestId, nodeId); + + // ScheduleResponse response = new ScheduleResponse(nodeId, new BuildRequestUnblocker(request.BuildRequest.GlobalRequestId)); + ScheduleResponse response = ScheduleResponse.CreateResumeExecutionResponse(nodeId, request.BuildRequest.GlobalRequestId); + request.ResumeExecution(nodeId); + responses.Add(response); + return; + } + } + + /// + /// Attempts to get results from the cache for this request. If results are available, reports them to the + /// correct node. If that action causes the parent to become ready and its node is idle, the parent is + /// resumed. + /// + private void ResolveRequestFromCacheAndResumeIfPossible(SchedulableRequest request, List responses) + { + int nodeForResults = (request.Parent != null) ? request.Parent.AssignedNode : InvalidNodeId; + + // Do we already have results? If so, just return them. + ScheduleResponse response = TrySatisfyRequestFromCache(nodeForResults, request.BuildRequest, skippedResultsAreOK: false); + if (response != null) + { + if (response.Action == ScheduleActionType.SubmissionComplete) + { + ErrorUtilities.VerifyThrow(request.Parent == null, "Unexpectedly generated a SubmissionComplete response for a request which is not top-level."); + LogRequestHandledFromCache(request.BuildRequest, response.BuildResult); + + // This was root request, we can report submission complete. + responses.Add(ScheduleResponse.CreateSubmissionCompleteResponse(response.BuildResult)); + if (response.BuildResult.OverallResult != BuildResultCode.Failure) + { + WriteSchedulingPlan(response.BuildResult.SubmissionId); + } + } + else + { + LogRequestHandledFromCache(request.BuildRequest, response.Unblocker.Result); + request.Complete(response.Unblocker.Result); + + TraceScheduler("Reporting results for request {0} with parent {1} to node {2} from cache.", request.BuildRequest.GlobalRequestId, request.BuildRequest.ParentGlobalRequestId, response.NodeId); + if (response.NodeId != InvalidNodeId) + { + responses.Add(response); + } + + // Is the node we are reporting to idle? If so, does reporting this result allow it to proceed with work? + if (!_schedulingData.IsNodeWorking(response.NodeId)) + { + ResumeReadyRequestIfAny(response.NodeId, responses); + } + } + } + } + + /// + /// Determines which work is available which must be assigned to the nodes. This includes: + /// 1. Ready requests - those requests which can immediately resume executing. + /// 2. Requests which can continue because results are now available but we haven't distributed them. + /// + private void ResumeRequiredWork(List responses) + { + // Resume any ready requests on the existing nodes. + foreach (int nodeId in _availableNodes.Keys) + { + // Don't overload the system. + if (AtSchedulingLimit()) + { + TraceScheduler("System load limit reached, cannot resume any more work. Executing: {0} Yielding: {1} Max Count: {2}", _schedulingData.ExecutingRequestsCount, _schedulingData.YieldingRequestsCount, _componentHost.BuildParameters.MaxNodeCount); + return; + } + + // Determine if this node is actually free to do work. + if (_schedulingData.IsNodeWorking(nodeId)) + { + continue; // Check the next node to see if it is free. + } + + // Resume a ready request, if any. We prefer to let existing requests complete before finding new work. + ResumeReadyRequestIfAny(nodeId, responses); + } + + // Now determine which unscheduled requests have results. Reporting these may cause an blocked request to become ready + // and potentially allow us to continue it. + List unscheduledRequests = new List(_schedulingData.UnscheduledRequests); + foreach (SchedulableRequest request in unscheduledRequests) + { + ResolveRequestFromCacheAndResumeIfPossible(request, responses); + } + } + + /// + /// Attempts to get a result from the cache to satisfy the request, and returns the appropriate response if possible. + /// + private ScheduleResponse TrySatisfyRequestFromCache(int nodeForResults, BuildRequest request, bool skippedResultsAreOK) + { + BuildRequestConfiguration config = _configCache[request.ConfigurationId]; + ResultsCacheResponse resultsResponse = _resultsCache.SatisfyRequest(request, config.ProjectInitialTargets, config.ProjectDefaultTargets, config.GetAfterTargetsForDefaultTargets(request), skippedResultsAreOK); + if (resultsResponse.Type == ResultsCacheResponseType.Satisfied) + { + return GetResponseForResult(nodeForResults, request, resultsResponse.Results); + } + + return null; + } + + /// + /// Gets the appropriate ScheduleResponse for a result, either to complete a submission or to report to a node. + /// + private ScheduleResponse GetResponseForResult(int parentRequestNode, BuildRequest requestWhichGeneratedResult, BuildResult result) + { + // We have results, return them to the originating node, or if it is a root request, mark the submission complete. + if (requestWhichGeneratedResult.IsRootRequest) + { + // return new ScheduleResponse(result); + return ScheduleResponse.CreateSubmissionCompleteResponse(result); + } + else + { + ErrorUtilities.VerifyThrow(parentRequestNode != InvalidNodeId, "Invalid parent node provided."); + + // return new ScheduleResponse(parentRequestNode, new BuildRequestUnblocker(requestWhichGeneratedResult.ParentGlobalRequestId, result)); + ErrorUtilities.VerifyThrow(result.ParentGlobalRequestId == requestWhichGeneratedResult.ParentGlobalRequestId, "Result's parent doesn't match request's parent."); + return ScheduleResponse.CreateReportResultResponse(parentRequestNode, result); + } + } + + /// + /// Logs the project started/finished pair for projects which are skipped entirely because all + /// of their results are available in the cache. + /// + private void LogRequestHandledFromCache(BuildRequest request, BuildResult result) + { + BuildRequestConfiguration configuration = _configCache[request.ConfigurationId]; + int nodeId = _schedulingData.GetAssignedNodeForRequestConfiguration(request.ConfigurationId); + NodeLoggingContext nodeContext = new NodeLoggingContext(_componentHost.LoggingService, nodeId, true); + nodeContext.LogRequestHandledFromCache(request, configuration, result); + } + + /// + /// This method determines how many requests are waiting for this request, taking into account the full tree of all requests + /// in all dependency chains which are waiting. + /// + private int ComputeClosureOfWaitingRequests(SchedulableRequest request) + { + int waitingRequests = 0; + + // In single-proc, this doesn't matter since scheduling is always 100% efficient. + if (_componentHost.BuildParameters.MaxNodeCount > 1) + { + foreach (SchedulableRequest waitingRequest in request.RequestsWeAreBlocking) + { + waitingRequests++; + waitingRequests += ComputeClosureOfWaitingRequests(waitingRequest); + } + } + + return waitingRequests; + } + + /// + /// Gets the node affinity for the specified request. + /// + private NodeAffinity GetNodeAffinityForRequest(BuildRequest request) + { + if (_forceAffinityOutOfProc) + { + return NodeAffinity.OutOfProc; + } + + if (IsTraversalRequest(request)) + { + return NodeAffinity.InProc; + } + + BuildRequestConfiguration configuration = _configCache[request.ConfigurationId]; + + // The affinity may have been specified by the host services. + NodeAffinity affinity = NodeAffinity.Any; + string pathOfProject = configuration.ProjectFullPath; + if (request.HostServices != null) + { + affinity = request.HostServices.GetNodeAffinity(pathOfProject); + } + + // If the request itself had no specific node affinity, it may be that the overall build still has + // a requirement, so check that. + if (affinity == NodeAffinity.Any) + { + if (_componentHost.BuildParameters.HostServices != null) + { + affinity = _componentHost.BuildParameters.HostServices.GetNodeAffinity(pathOfProject); + } + } + + return affinity; + } + + /// + /// Iterates through the set of available nodes and checks whether any of them is + /// capable of servicing this request or any of the requests that it is blocked + /// by (regardless of whether they are currently available to do so). + /// + private bool RequestOrAnyItIsBlockedByCanBeServiced(SchedulableRequest request) + { + if (request.RequestsWeAreBlockedByCount > 0) + { + foreach (SchedulableRequest requestWeAreBlockedBy in request.RequestsWeAreBlockedBy) + { + if (RequestOrAnyItIsBlockedByCanBeServiced(requestWeAreBlockedBy)) + { + return true; + } + } + + // if none of the requests we are blocked by can be serviced, it doesn't matter + // whether we can be serviced or not -- the reason we're blocked is because none + // of the requests we are blocked by can be serviced. + return false; + } + else + { + foreach (NodeInfo node in _availableNodes.Values) + { + if (CanScheduleRequestToNode(request, node.NodeId)) + { + return true; + } + } + + return false; + } + } + + /// + /// Determines if we have a matching request somewhere, and if so, assigns the same request ID. Otherwise + /// assigns a new request id. + /// + /// + /// UNDONE: (Performance) This algorithm should be modified so we don't have to iterate over all of the + /// requests to find a matching one. A HashSet with proper equality semantics and a good hash code for the BuildRequest + /// would speed this considerably, especially for large numbers of projects in a build. + /// + /// The request whose ID should be assigned + private void AssignGlobalRequestId(BuildRequest request) + { + bool assignNewId = false; + if (request.GlobalRequestId == BuildRequest.InvalidGlobalRequestId && _schedulingData.GetRequestsAssignedToConfigurationCount(request.ConfigurationId) > 0) + { + foreach (SchedulableRequest existingRequest in _schedulingData.GetRequestsAssignedToConfiguration(request.ConfigurationId)) + { + if (existingRequest.BuildRequest.Targets.Count == request.Targets.Count) + { + List leftTargets = new List(existingRequest.BuildRequest.Targets); + List rightTargets = new List(request.Targets); + + leftTargets.Sort(StringComparer.OrdinalIgnoreCase); + rightTargets.Sort(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < leftTargets.Count; i++) + { + if (!leftTargets[i].Equals(rightTargets[i], StringComparison.OrdinalIgnoreCase)) + { + assignNewId = true; + break; + } + } + + if (!assignNewId) + { + request.GlobalRequestId = existingRequest.BuildRequest.GlobalRequestId; + return; + } + } + } + } + + request.GlobalRequestId = _nextGlobalRequestId; + _nextGlobalRequestId++; + } + + /// + /// Writes the graph representation of how the nodes were utilized. + /// + private void WriteNodeUtilizationGraph(ILoggingService loggingService, BuildEventContext context, bool useConfigurations) + { + int[] currentWork = new int[_availableNodes.Count]; + int[] previousWork = new int[currentWork.Length]; + HashSet[] runningRequests = new HashSet[currentWork.Length]; + DateTime currentEventTime = DateTime.MinValue; + DateTime previousEventTime = DateTime.MinValue; + double accumulatedDuration = 0; + + TimeSpan[] nodeActiveTimes = new TimeSpan[_availableNodes.Count]; + DateTime[] nodeStartTimes = new DateTime[_availableNodes.Count]; + int eventIndex = 0; + + Dictionary availableNodeIdsToIndex = new Dictionary(_availableNodes.Count); + int[] indexToAvailableNodeId = new int[_availableNodes.Count]; + int indexIntoArrays = 0; + + foreach (int availableNodeId in _availableNodes.Keys) + { + availableNodeIdsToIndex[availableNodeId] = indexIntoArrays; + indexToAvailableNodeId[indexIntoArrays] = availableNodeId; + indexIntoArrays++; + } + + // Prepare the arrays and headers. + StringBuilder nodeIndices = new StringBuilder(); + int invalidWorkId = useConfigurations ? BuildRequestConfiguration.InvalidConfigurationId : BuildRequest.InvalidGlobalRequestId; + for (int i = 0; i < currentWork.Length; i++) + { + currentWork[i] = invalidWorkId; + previousWork[i] = invalidWorkId; + runningRequests[i] = new HashSet(); + nodeIndices.Append(String.Format(CultureInfo.InvariantCulture, "{0,-5} ", indexToAvailableNodeId[i])); + } + + loggingService.LogComment(context, MessageImportance.Normal, "NodeUtilizationHeader", nodeIndices.ToString()); + + // Walk through each of the events and grab all of the events which have the same timestamp to determine what occurred. + foreach (SchedulingData.SchedulingEvent buildEvent in _schedulingData.BuildEvents) + { + int workId = useConfigurations ? buildEvent.Request.BuildRequest.ConfigurationId : buildEvent.Request.BuildRequest.GlobalRequestId; + + if (buildEvent.EventTime > currentEventTime) + { + WriteNodeUtilizationGraphLine(loggingService, context, currentWork, previousWork, buildEvent.EventTime, currentEventTime, invalidWorkId, ref accumulatedDuration); + + if (currentEventTime != DateTime.MinValue) + { + // Accumulate time for nodes which were not idle. + for (int i = 0; i < currentWork.Length; i++) + { + if (currentWork[i] != invalidWorkId) + { + for (int x = 0; x < runningRequests[i].Count; x++) + { + nodeActiveTimes[i] += buildEvent.EventTime - currentEventTime; + } + } + } + } + + currentWork.CopyTo(previousWork, 0); + previousEventTime = currentEventTime; + currentEventTime = buildEvent.EventTime; + eventIndex++; + } + + // The assigned node may be invalid if the request was completed from the cache. + // In that case, just skip assessing it -- it did effectively no work. + if (buildEvent.Request.AssignedNode != InvalidNodeId) + { + int nodeForEvent = availableNodeIdsToIndex[buildEvent.Request.AssignedNode]; + + switch (buildEvent.NewState) + { + case SchedulableRequestState.Executing: + case SchedulableRequestState.Yielding: + currentWork[nodeForEvent] = workId; + if (!runningRequests[nodeForEvent].Contains(workId)) + { + runningRequests[nodeForEvent].Add(workId); + } + + if (nodeStartTimes[nodeForEvent] == DateTime.MinValue) + { + nodeStartTimes[nodeForEvent] = buildEvent.EventTime; + } + + break; + + default: + if (runningRequests[nodeForEvent].Contains(workId)) + { + runningRequests[nodeForEvent].Remove(workId); + } + + if (previousWork[nodeForEvent] == workId) + { + // The previously executing request is no longer executing here. + if (runningRequests[nodeForEvent].Count == 0) + { + currentWork[nodeForEvent] = invalidWorkId; // Idle + } + else + { + currentWork[nodeForEvent] = runningRequests[nodeForEvent].First(); + } + } + + break; + } + } + } + + WriteNodeUtilizationGraphLine(loggingService, context, currentWork, previousWork, currentEventTime, previousEventTime, invalidWorkId, ref accumulatedDuration); + + // Write out the node utilization percentage. + double utilizationAverage = 0; + StringBuilder utilitzationPercentages = new StringBuilder(); + for (int i = 0; i < nodeActiveTimes.Length; i++) + { + TimeSpan totalDuration = currentEventTime - nodeStartTimes[i]; + double utilizationPercent = (double)nodeActiveTimes[i].TotalMilliseconds / (double)totalDuration.TotalMilliseconds; + + utilitzationPercentages.AppendFormat("{0,-5:###.0} ", utilizationPercent * 100); + utilizationAverage += utilizationPercent; + } + + loggingService.LogComment(context, MessageImportance.Normal, "NodeUtilizationSummary", utilitzationPercentages.ToString(), (utilizationAverage / (double)_availableNodes.Count) * 100); + } + + /// + /// Writes a single line of node utilization information. + /// + private void WriteNodeUtilizationGraphLine(ILoggingService loggingService, BuildEventContext context, int[] currentWork, int[] previousWork, DateTime currentEventTime, DateTime previousEventTime, int invalidWorkId, ref double accumulatedDuration) + { + if (currentEventTime == DateTime.MinValue) + { + return; + } + + bool haveNonIdleNode = false; + StringBuilder stringBuilder = new StringBuilder(64); + stringBuilder.AppendFormat("{0}: ", previousEventTime.Ticks); + for (int i = 0; i < currentWork.Length; i++) + { + if (currentWork[i] == invalidWorkId) + { + stringBuilder.Append("x "); // Idle + } + else if (currentWork[i] == previousWork[i]) + { + stringBuilder.Append("| "); // Continuing the work from the previous time. + haveNonIdleNode = true; + } + else + { + stringBuilder.Append(String.Format(CultureInfo.InvariantCulture, "{0,-5} ", currentWork[i])); + haveNonIdleNode = true; + } + } + + double duration = 0; + if (previousEventTime != DateTime.MinValue) + { + duration = (currentEventTime - previousEventTime).TotalSeconds; + accumulatedDuration += duration; + } + + string durationBar = new String('#', (int)(duration / 0.05)); + if (haveNonIdleNode) + { + loggingService.LogComment(context, MessageImportance.Normal, "NodeUtilizationEntry", stringBuilder, duration, accumulatedDuration, durationBar); + } + } + + /// + /// Recursively dumps the build information for the specified hierarchy + /// + private void WriteRecursiveSummary(ILoggingService loggingService, BuildEventContext context, int submissionId, SchedulableRequest request, int level, bool useConfigurations, bool isLastChild) + { + int postPad = Math.Max(20 /* field width */ - (2 * level) /* spacing for hierarchy lines */ - 3 /* length allocated for config/request id */, 0); + + StringBuilder prePadString = new StringBuilder(2 * level); + if (level != 0) + { + int levelsToPad = level; + if (isLastChild) + { + levelsToPad--; + } + + while (levelsToPad > 0) + { + prePadString.Append("| "); + levelsToPad--; + } + + if (isLastChild) + { + prePadString.Append(@". "); + } + } + + loggingService.LogComment + ( + context, + MessageImportance.Normal, + "BuildHierarchyEntry", + prePadString.ToString(), + useConfigurations ? request.BuildRequest.ConfigurationId : request.BuildRequest.GlobalRequestId, + new String(' ', postPad), + String.Format(CultureInfo.InvariantCulture, "{0:0.000}", request.GetTimeSpentInState(SchedulableRequestState.Executing).TotalSeconds), + String.Format(CultureInfo.InvariantCulture, "{0:0.000}", request.GetTimeSpentInState(SchedulableRequestState.Executing).TotalSeconds + request.GetTimeSpentInState(SchedulableRequestState.Blocked).TotalSeconds + request.GetTimeSpentInState(SchedulableRequestState.Ready).TotalSeconds), + _configCache[request.BuildRequest.ConfigurationId].ProjectFullPath, + String.Join(", ", request.BuildRequest.Targets.ToArray()) + ); + + List childRequests = new List(_schedulingData.GetRequestsByHierarchy(request)); + childRequests.Sort(delegate (SchedulableRequest left, SchedulableRequest right) + { + if (left.StartTime < right.StartTime) + { + return -1; + } + else if (left.StartTime > right.StartTime) + { + return 1; + } + + return 0; + }); + + for (int i = 0; i < childRequests.Count; i++) + { + SchedulableRequest childRequest = childRequests[i]; + WriteRecursiveSummary(loggingService, context, submissionId, childRequest, level + 1, useConfigurations, i == childRequests.Count - 1); + } + } + + #region Debug Information + + /// + /// Method used for debugging purposes. + /// + private void TraceScheduler(string format, params object[] stuff) + { + if (_debugDumpState) + { + StreamWriter file = new StreamWriter(String.Format(CultureInfo.CurrentCulture, Path.Combine(_debugDumpPath, "SchedulerTrace_{0}.txt"), Process.GetCurrentProcess().Id), true); + file.Write("{0}({1})-{2}: ", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId, _schedulingData.EventTime.Ticks); + file.WriteLine(format, stuff); + file.Flush(); + file.Close(); + } + } + + /// + /// Dumps the current state of the scheduler. + /// + private void DumpSchedulerState() + { + if (_debugDumpState) + { + if (_schedulingData != null) + { + using (StreamWriter file = new StreamWriter(String.Format(CultureInfo.CurrentCulture, Path.Combine(_debugDumpPath, "SchedulerState_{0}.txt"), Process.GetCurrentProcess().Id), true)) + { + file.WriteLine("Scheduler state at timestamp {0}:", _schedulingData.EventTime.Ticks); + file.WriteLine("------------------------------------------------", _schedulingData.EventTime.Ticks); + + foreach (int nodeId in _availableNodes.Keys) + { + file.WriteLine("Node {0} {1} ({2} assigned requests, {3} configurations)", nodeId, _schedulingData.IsNodeWorking(nodeId) ? String.Format(CultureInfo.InvariantCulture, "Active ({0} executing)", _schedulingData.GetExecutingRequestByNode(nodeId).BuildRequest.GlobalRequestId) : "Idle", _schedulingData.GetScheduledRequestsCountByNode(nodeId), _schedulingData.GetConfigurationsCountByNode(nodeId, false, null)); + + List scheduledRequestsByNode = new List(_schedulingData.GetScheduledRequestsByNode(nodeId)); + + foreach (SchedulableRequest request in scheduledRequestsByNode) + { + DumpRequestState(file, request, 1); + file.WriteLine(); + } + + // If the node is idle, we want to know why. + if (!_schedulingData.IsNodeWorking(nodeId)) + { + file.WriteLine("Top-level requests causing this node to be idle:"); + + if (scheduledRequestsByNode.Count == 0) + { + file.WriteLine(" Node is idle because there is no work available for this node to do."); + file.WriteLine(); + } + else + { + Queue blockingRequests = new Queue(); + HashSet topLevelBlockingRequests = new HashSet(); + foreach (SchedulableRequest request in scheduledRequestsByNode) + { + if (request.RequestsWeAreBlockedByCount > 0) + { + foreach (SchedulableRequest blockingRequest in request.RequestsWeAreBlockedBy) + { + blockingRequests.Enqueue(blockingRequest); + } + } + } + + while (blockingRequests.Count > 0) + { + SchedulableRequest request = blockingRequests.Dequeue(); + + if (request.RequestsWeAreBlockedByCount > 0) + { + foreach (SchedulableRequest blockingRequest in request.RequestsWeAreBlockedBy) + { + blockingRequests.Enqueue(blockingRequest); + } + } + else + { + topLevelBlockingRequests.Add(request); + } + } + + foreach (SchedulableRequest request in topLevelBlockingRequests) + { + DumpRequestState(file, request, 1); + file.WriteLine(); + } + } + } + } + + if (_schedulingData.UnscheduledRequestsCount == 0) + { + file.WriteLine("No unscheduled requests."); + } + else + { + file.WriteLine("Unscheduled requests:"); + + foreach (SchedulableRequest request in _schedulingData.UnscheduledRequests) + { + DumpRequestState(file, request, 1); + } + } + + file.WriteLine(); + } + } + } + } + + /// + /// Dumps all of the configurations. + /// + private void DumpConfigurations() + { + if (_debugDumpState) + { + if (_schedulingData != null) + { + using (StreamWriter file = new StreamWriter(String.Format(CultureInfo.CurrentCulture, Path.Combine(_debugDumpPath, "SchedulerState_{0}.txt"), Process.GetCurrentProcess().Id), true)) + { + file.WriteLine("Configurations used during this build"); + file.WriteLine("-------------------------------------"); + + List configurations = new List(_schedulingData.Configurations); + configurations.Sort(); + + foreach (int config in configurations) + { + file.WriteLine("Config {0} Node {1} TV: {2} File {3}", config, _schedulingData.GetAssignedNodeForRequestConfiguration(config), _configCache[config].ToolsVersion, _configCache[config].ProjectFullPath); + foreach (ProjectPropertyInstance property in _configCache[config].Properties) + { + file.WriteLine("{0} = \"{1}\"", property.Name, property.EvaluatedValue); + } + + file.WriteLine(); + } + + file.Flush(); + } + } + } + } + + /// + /// Dumps all of the requests. + /// + private void DumpRequests() + { + if (_debugDumpState) + { + if (_schedulingData != null) + { + using (StreamWriter file = new StreamWriter(String.Format(CultureInfo.CurrentCulture, Path.Combine(_debugDumpPath, "SchedulerState_{0}.txt"), Process.GetCurrentProcess().Id), true)) + { + file.WriteLine("Requests used during the build:"); + file.WriteLine("-------------------------------"); + file.WriteLine("Format: GlobalRequestId: [NodeId] FinalState (ConfigId) Path (Targets)"); + DumpRequestHierarchy(file, null, 0); + file.Flush(); + } + } + } + } + + /// + /// Dumps the hierarchy of requests. + /// + private void DumpRequestHierarchy(StreamWriter file, SchedulableRequest root, int indent) + { + foreach (SchedulableRequest child in _schedulingData.GetRequestsByHierarchy(root)) + { + DumpRequestSpec(file, child, indent, null); + DumpRequestHierarchy(file, child, indent + 1); + } + } + + /// + /// Dumps the state of a request. + /// + private void DumpRequestState(StreamWriter file, SchedulableRequest request, int indent) + { + DumpRequestSpec(file, request, indent, null); + if (request.RequestsWeAreBlockedByCount > 0) + { + foreach (SchedulableRequest blockingRequest in request.RequestsWeAreBlockedBy) + { + DumpRequestSpec(file, blockingRequest, indent + 1, "!"); + } + } + + if (request.RequestsWeAreBlockingCount > 0) + { + foreach (SchedulableRequest blockedRequest in request.RequestsWeAreBlocking) + { + DumpRequestSpec(file, blockedRequest, indent + 1, ">"); + } + } + } + + /// + /// Dumps detailed information about a request. + /// + private void DumpRequestSpec(StreamWriter file, SchedulableRequest request, int indent, string prefix) + { + file.WriteLine("{0}{1}{2}: [{3}] {4}{5} ({6}){7} ({8})", new String(' ', indent * 2), (prefix == null) ? "" : prefix, request.BuildRequest.GlobalRequestId, _schedulingData.GetAssignedNodeForRequestConfiguration(request.BuildRequest.ConfigurationId), _schedulingData.IsRequestScheduled(request) ? "RUNNING " : "", request.State, request.BuildRequest.ConfigurationId, _configCache[request.BuildRequest.ConfigurationId].ProjectFullPath, String.Join(", ", request.BuildRequest.Targets.ToArray())); + } + + /// + /// Write out the scheduling information so the next time we can read the plan back in and use it. + /// + private void WriteSchedulingPlan(int submissionId) + { + SchedulingPlan plan = new SchedulingPlan(_configCache, _schedulingData); + plan.WritePlan(submissionId, _componentHost.LoggingService, new BuildEventContext(submissionId, 0, 0, 0, 0, 0)); + } + + /// + /// Retrieves the scheduling plan from the previous run. + /// + private void ReadSchedulingPlan(int submissionId) + { + _schedulingPlan = new SchedulingPlan(_configCache, _schedulingData); + _schedulingPlan.ReadPlan(submissionId, _componentHost.LoggingService, new BuildEventContext(submissionId, 0, 0, 0, 0, 0)); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulerCircularDependencyException.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulerCircularDependencyException.cs new file mode 100644 index 00000000000..c8f24719c27 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulerCircularDependencyException.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Exception class thrown when the Scheduler detects a circular dependency. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Exception thrown when a circular dependency is detected in the Scheduler. + /// + [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "No point in adding the serialization constructors since BuildRequest is not serializable")] + [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "No point in marking as ISerializable since BuildRequest is not. ")] + internal class SchedulerCircularDependencyException : Exception + { + /// + /// The ancestors which led to this circular dependency. + /// + private IList _ancestors; + + /// + /// The request which caused the circular dependency. + /// + private BuildRequest _request; + + /// + /// Constructor. + /// + public SchedulerCircularDependencyException(BuildRequest request, IList ancestors) + { + _request = request; + _ancestors = ancestors; + } + + /// + /// Gets an enumeration of the ancestors which led to this circular dependency. + /// + public IEnumerable Ancestors + { + get { return _ancestors; } + } + + /// + /// Gets the request which caused the circular dependency. + /// + public BuildRequest Request + { + get { return _request; } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingData.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingData.cs new file mode 100644 index 00000000000..fee8201b12a --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingData.cs @@ -0,0 +1,778 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class which maintains the relationships between requests for the Scheduler. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class manages the set of schedulable requests. In concert with SchedulableRequest, it tracks all relationships + /// between requests in the system, verifies state change validity and provides efficient methods for querying request relationships. + /// + internal class SchedulingData + { + #region Requests By State + + /// + /// Maps global request Id to an executing request. + /// + private readonly Dictionary _executingRequests = new Dictionary(32); + + /// + /// Maps global request Id to a blocked request. + /// + private readonly Dictionary _blockedRequests = new Dictionary(32); + + /// + /// Maps global request Id to a blocked request. + /// + private readonly Dictionary _yieldingRequests = new Dictionary(32); + + /// + /// Maps global request Id to a ready request. + /// + private readonly Dictionary _readyRequests = new Dictionary(32); + + /// + /// Holds all of the unscheduled requests. + /// + private readonly LinkedList _unscheduledRequests = new LinkedList(); + + /// + /// Maps a schedulable request directly to the node holding it in the linked list. This allows us to perform an O(1) operation to + /// remove the node from the linked list without exposing the list directly. + /// + private readonly Dictionary> _unscheduledRequestNodesByRequest = new Dictionary>(32); + + #endregion + + #region Requests By Node + + /// + /// Maps node id to the requests scheduled on it. + /// + private readonly Dictionary> _scheduledRequestsByNode = new Dictionary>(32); + + /// + /// Maps a node id to the currently executing request, if any. + /// + private readonly Dictionary _executingRequestByNode = new Dictionary(32); + + /// + /// Maps a node id to those requests which are ready to execute, if any. + /// + private readonly Dictionary> _readyRequestsByNode = new Dictionary>(32); + + /// + /// Maps a node id to the set of configurations assigned to it. + /// + private readonly Dictionary> _configurationsByNode = new Dictionary>(32); + + #endregion + + #region Configuration-related Information + /// + /// Maps a configuration id to the number of requests in the system which are assigned to that configuration. + /// + private readonly Dictionary _configurationCounts = new Dictionary(32); + + /// + /// Maps a configuration id to the node to which it is assigned. + /// + private readonly Dictionary _configurationToNode = new Dictionary(32); + + /// + /// Maps a configuration id to the requests which apply to it. + /// + private readonly Dictionary> _configurationToRequests = new Dictionary>(32); + + #endregion + + #region Diagnostic Information + + /// + /// This is the hierarchy of build requests as they were created. + /// + private readonly Dictionary> _buildHierarchy = new Dictionary>(32); + + /// + /// The sequence of events which have taken place during this build. + /// + private readonly List _buildEvents = new List(64); + + /// + /// The current time for events. This is set by the scheduler when it does a scheduling cycle in response to an event. + /// + private DateTime _currentEventTime; + + #endregion + + /// + /// Constructor. + /// + public SchedulingData() + { + } + + /// + /// Retrieves all of the build events. + /// + public IEnumerable BuildEvents + { + get { return _buildEvents; } + } + + /// + /// Retrieves all of the executing requests. + /// + public IEnumerable ExecutingRequests + { + get { return _executingRequests.Values; } + } + + /// + /// Gets a count of all executing requests. + /// + public int ExecutingRequestsCount + { + get { return _executingRequests.Count; } + } + + /// + /// Retrieves all of the ready requests. + /// + public IEnumerable ReadyRequests + { + get { return _readyRequests.Values; } + } + + /// + /// Gets a count of all the ready requests. + /// + public int ReadyRequestsCount + { + get { return _readyRequests.Count; } + } + + /// + /// Retrieves all of the blocked requests. + /// + public IEnumerable BlockedRequests + { + get { return _blockedRequests.Values; } + } + + /// + /// Gets a count of all of the blocked requests. + /// + public int BlockedRequestsCount + { + get { return _blockedRequests.Count; } + } + + /// + /// Retrieves all of the yielded requests. + /// + public IEnumerable YieldingRequests + { + get { return _yieldingRequests.Values; } + } + + /// + /// Gets a count of all of the yielded requests. + /// + public int YieldingRequestsCount + { + get { return _yieldingRequests.Count; } + } + + /// + /// Retrieves all of the unscheduled requests. + /// + public IEnumerable UnscheduledRequests + { + get { return _unscheduledRequests; } + } + + /// + /// Gets a count of all the unscheduled requests. + /// + public int UnscheduledRequestsCount + { + get { return _unscheduledRequests.Count; } + } + + /// + /// Enumerates the unscheduled requests which don't have other instances scheduled already. + /// + public IEnumerable UnscheduledRequestsWhichCanBeScheduled + { + get + { + foreach (SchedulableRequest request in _unscheduledRequests) + { + if (!IsRequestScheduled(request)) + { + yield return request; + } + } + } + } + + /// + /// Gets all of the configurations for this build. + /// + /// + public IEnumerable Configurations + { + get + { + return _configurationToNode.Keys; + } + } + + /// + /// Gets or sets the current event time. + /// + public DateTime EventTime + { + get { return _currentEventTime; } + set { _currentEventTime = value; } + } + + /// + /// Creates a new request and adds it to the system + /// + /// + /// New requests always go on the front of the queue, because we prefer to build the projects we just received first (depth first, absent + /// any particular scheduling algorithm such as in the single-proc case.) + /// + public SchedulableRequest CreateRequest(BuildRequest buildRequest, SchedulableRequest parent) + { + SchedulableRequest request = new SchedulableRequest(this, buildRequest, parent); + request.CreationTime = EventTime; + + LinkedListNode requestNode = _unscheduledRequests.AddFirst(request); + _unscheduledRequestNodesByRequest[request] = requestNode; + + // Update the configuration information. + HashSet requests; + if (!_configurationToRequests.TryGetValue(request.BuildRequest.ConfigurationId, out requests)) + { + requests = new HashSet(); + _configurationToRequests[request.BuildRequest.ConfigurationId] = requests; + } + + requests.Add(request); + + // Update the build hierarchy. + if (!_buildHierarchy.ContainsKey(request)) + { + _buildHierarchy[request] = new List(8); + } + + if (parent != null) + { + ErrorUtilities.VerifyThrow(_buildHierarchy.ContainsKey(parent), "Parent doesn't exist in build hierarchy for request {0}", request.BuildRequest.GlobalRequestId); + _buildHierarchy[parent].Add(request); + } + + return request; + } + + /// + /// Updates the state of the specified request. + /// + public void UpdateFromState(SchedulableRequest request, SchedulableRequestState previousState) + { + // Remove from its old collection + switch (previousState) + { + case SchedulableRequestState.Blocked: + _blockedRequests.Remove(request.BuildRequest.GlobalRequestId); + break; + + case SchedulableRequestState.Yielding: + _yieldingRequests.Remove(request.BuildRequest.GlobalRequestId); + break; + + case SchedulableRequestState.Completed: + ErrorUtilities.ThrowInternalError("Should not be updating a request after it has reached the Completed state."); + break; + + case SchedulableRequestState.Executing: + _executingRequests.Remove(request.BuildRequest.GlobalRequestId); + _executingRequestByNode[request.AssignedNode] = null; + break; + + case SchedulableRequestState.Ready: + _readyRequests.Remove(request.BuildRequest.GlobalRequestId); + _readyRequestsByNode[request.AssignedNode].Remove(request); + break; + + case SchedulableRequestState.Unscheduled: + LinkedListNode requestNode = _unscheduledRequestNodesByRequest[request]; + _unscheduledRequestNodesByRequest.Remove(request); + _unscheduledRequests.Remove(requestNode); + + if (request.State != SchedulableRequestState.Completed) + { + // Map the request to the node. + HashSet requestsAssignedToNode; + if (!_scheduledRequestsByNode.TryGetValue(request.AssignedNode, out requestsAssignedToNode)) + { + requestsAssignedToNode = new HashSet(); + _scheduledRequestsByNode[request.AssignedNode] = requestsAssignedToNode; + } + + ErrorUtilities.VerifyThrow(!requestsAssignedToNode.Contains(request), "Request {0} is already scheduled to node {1}", request.BuildRequest.GlobalRequestId, request.AssignedNode); + requestsAssignedToNode.Add(request); + + // Map the configuration to the node. + HashSet configurationsAssignedToNode; + if (!_configurationsByNode.TryGetValue(request.AssignedNode, out configurationsAssignedToNode)) + { + configurationsAssignedToNode = new HashSet(); + _configurationsByNode[request.AssignedNode] = configurationsAssignedToNode; + } + + if (!configurationsAssignedToNode.Contains(request.BuildRequest.ConfigurationId)) + { + configurationsAssignedToNode.Add(request.BuildRequest.ConfigurationId); + } + } + + break; + } + + // Add it to its new location + switch (request.State) + { + case SchedulableRequestState.Blocked: + ErrorUtilities.VerifyThrow(!_blockedRequests.ContainsKey(request.BuildRequest.GlobalRequestId), "Request with global id {0} is already blocked!"); + _blockedRequests[request.BuildRequest.GlobalRequestId] = request; + break; + + case SchedulableRequestState.Yielding: + ErrorUtilities.VerifyThrow(!_yieldingRequests.ContainsKey(request.BuildRequest.GlobalRequestId), "Request with global id {0} is already yielded!"); + _yieldingRequests[request.BuildRequest.GlobalRequestId] = request; + break; + + case SchedulableRequestState.Completed: + ErrorUtilities.VerifyThrow(_configurationToRequests.ContainsKey(request.BuildRequest.ConfigurationId), "Configuration {0} never had requests assigned to it.", request.BuildRequest.ConfigurationId); + ErrorUtilities.VerifyThrow(_configurationToRequests[request.BuildRequest.ConfigurationId].Count > 0, "Configuration {0} has no requests assigned to it.", request.BuildRequest.ConfigurationId); + _configurationToRequests[request.BuildRequest.ConfigurationId].Remove(request); + if (_scheduledRequestsByNode.ContainsKey(request.AssignedNode)) + { + _scheduledRequestsByNode[request.AssignedNode].Remove(request); + } + + request.EndTime = EventTime; + break; + + case SchedulableRequestState.Executing: + ErrorUtilities.VerifyThrow(!_executingRequests.ContainsKey(request.BuildRequest.GlobalRequestId), "Request with global id {0} is already executing!"); + ErrorUtilities.VerifyThrow(!_executingRequestByNode.ContainsKey(request.AssignedNode) || _executingRequestByNode[request.AssignedNode] == null, "Node {0} is currently executing a request.", request.AssignedNode); + + _executingRequests[request.BuildRequest.GlobalRequestId] = request; + _executingRequestByNode[request.AssignedNode] = request; + _configurationToNode[request.BuildRequest.ConfigurationId] = request.AssignedNode; + if (previousState == SchedulableRequestState.Unscheduled) + { + request.StartTime = EventTime; + } + + break; + + case SchedulableRequestState.Ready: + ErrorUtilities.VerifyThrow(!_readyRequests.ContainsKey(request.BuildRequest.GlobalRequestId), "Request with global id {0} is already ready!"); + _readyRequests[request.BuildRequest.GlobalRequestId] = request; + HashSet readyRequestsOnNode; + if (!_readyRequestsByNode.TryGetValue(request.AssignedNode, out readyRequestsOnNode)) + { + readyRequestsOnNode = new HashSet(); + _readyRequestsByNode[request.AssignedNode] = readyRequestsOnNode; + } + + ErrorUtilities.VerifyThrow(!readyRequestsOnNode.Contains(request), "Request with global id {0} is already marked as ready on node {1}", request.BuildRequest.GlobalRequestId, request.AssignedNode); + readyRequestsOnNode.Add(request); + break; + + case SchedulableRequestState.Unscheduled: + ErrorUtilities.ThrowInternalError("Request with global id {0} cannot transition to the Unscheduled state", request.BuildRequest.GlobalRequestId); + break; + } + + _buildEvents.Add(new SchedulingEvent(EventTime, request, previousState, request.State)); + } + + /// + /// Gets the requests assigned to a particular configuration. + /// + public IEnumerable GetRequestsAssignedToConfiguration(int configurationId) + { + return _configurationToRequests[configurationId]; + } + + /// + /// Retrieves the number of requests which exist in the system that are attributed to the specified configuration. + /// + public int GetRequestsAssignedToConfigurationCount(int configurationId) + { + HashSet requests; + if (!_configurationToRequests.TryGetValue(configurationId, out requests)) + { + return 0; + } + + return requests.Count; + } + + /// + /// Retrieves a request which is currently executing. + /// + public SchedulableRequest GetExecutingRequest(int globalRequestId) + { + ExpectScheduledRequestState(globalRequestId, SchedulableRequestState.Executing); + return _executingRequests[globalRequestId]; + } + + /// + /// Retrieves a request which is currently blocked. + /// + public SchedulableRequest GetBlockedRequest(int globalRequestId) + { + ExpectScheduledRequestState(globalRequestId, SchedulableRequestState.Blocked); + return _blockedRequests[globalRequestId]; + } + + /// + /// Retrieves a request which is currently blocked, or null if there is none. + /// + public SchedulableRequest GetBlockedRequestIfAny(int globalRequestId) + { + SchedulableRequest request; + if (_blockedRequests.TryGetValue(globalRequestId, out request)) + { + return request; + } + + return null; + } + + /// + /// Retrieves a request which is currently yielding. + /// + public SchedulableRequest GetYieldingRequest(int globalRequestId) + { + ExpectScheduledRequestState(globalRequestId, SchedulableRequestState.Yielding); + return _yieldingRequests[globalRequestId]; + } + + /// + /// Retrieves a request which is ready to continue executing. + /// + public SchedulableRequest GetReadyRequest(int globalRequestId) + { + ExpectScheduledRequestState(globalRequestId, SchedulableRequestState.Ready); + return _readyRequests[globalRequestId]; + } + + /// + /// Retrieves a request which has been assigned to a node and is in the executing, blocked or ready states. + /// + public SchedulableRequest GetScheduledRequest(int globalRequestId) + { + SchedulableRequest returnValue = InternalGetScheduledRequestByGlobalRequestId(globalRequestId); + ErrorUtilities.VerifyThrow(returnValue != null, "Global Request Id {0} has not been assigned and cannot be retrieved.", globalRequestId); + return returnValue; + } + + /// + /// Returns true if the specified node has an executing request, false otherwise. + /// + public bool IsNodeWorking(int nodeId) + { + SchedulableRequest request; + if (!_executingRequestByNode.TryGetValue(nodeId, out request)) + { + return false; + } + + return (request != null); + } + + /// + /// Returns the number of configurations assigned to the specified node. + /// + public int GetConfigurationsCountByNode(int nodeId, bool excludeTraversals, IConfigCache configCache) + { + HashSet configurationsAssignedToNode; + + if (!_configurationsByNode.TryGetValue(nodeId, out configurationsAssignedToNode)) + { + return 0; + } + + int excludeCount = 0; + if (excludeTraversals && (configCache != null)) + { + foreach (int config in configurationsAssignedToNode) + { + if (configCache[config].IsTraversal) + { + excludeCount++; + } + } + } + + return configurationsAssignedToNode.Count - excludeCount; + } + + /// + /// Gets the request currently executing on the node. + /// + public SchedulableRequest GetExecutingRequestByNode(int nodeId) + { + return _executingRequestByNode[nodeId]; + } + + /// + /// Determines if the specified request is currently scheduled. + /// + public bool IsRequestScheduled(SchedulableRequest request) + { + return InternalGetScheduledRequestByGlobalRequestId(request.BuildRequest.GlobalRequestId) != null; + } + + /// + /// Retrieves the count all of the requests scheduled to the specified node. + /// + public int GetScheduledRequestsCountByNode(int nodeId) + { + HashSet requests; + if (!_scheduledRequestsByNode.TryGetValue(nodeId, out requests)) + { + return 0; + } + + return requests.Count; + } + + /// + /// Retrieves all of the requests scheduled to the specified node. + /// + public IEnumerable GetScheduledRequestsByNode(int nodeId) + { + HashSet requests; + if (!_scheduledRequestsByNode.TryGetValue(nodeId, out requests)) + { + return ReadOnlyEmptyCollection.Instance; + } + + return requests; + } + + /// + /// Retrieves all of the ready requests on the specified node. + /// + public IEnumerable GetReadyRequestsByNode(int nodeId) + { + HashSet requests; + if (!_readyRequestsByNode.TryGetValue(nodeId, out requests)) + { + return ReadOnlyEmptyCollection.Instance; + } + + return requests; + } + + /// + /// Retrieves a set of build requests which have the specified parent. If root is null, this will retrieve all of the + /// top-level requests. + /// + public IEnumerable GetRequestsByHierarchy(SchedulableRequest root) + { + if (root == null) + { + // Retrieve all requests which are roots of the tree. + List roots = new List(); + foreach (SchedulableRequest key in _buildHierarchy.Keys) + { + if (key.Parent == null) + { + roots.Add(key); + } + } + + return roots; + } + + return _buildHierarchy[root]; + } + + /// + /// Returns the node id to which this request should be assigned based on its configuration. + /// + /// The node if one has been assigned for this configuration, otherwise -1. + public int GetAssignedNodeForRequestConfiguration(int configurationId) + { + int assignedNode; + if (!_configurationToNode.TryGetValue(configurationId, out assignedNode)) + { + return Scheduler.InvalidNodeId; + } + + return assignedNode; + } + + /// + /// Returns true if the request can be scheduled to the specified node. + /// + public bool CanScheduleRequestToNode(SchedulableRequest request, int nodeId) + { + int requiredNodeId = GetAssignedNodeForRequestConfiguration(request.BuildRequest.ConfigurationId); + return requiredNodeId == Scheduler.InvalidNodeId || requiredNodeId == nodeId; + } + + /// + /// Unassigns the node associated with a particular configuration. + /// + /// + /// The operation is only valid when there are no scheduled requests for this configuration. + /// + internal void UnassignNodeForRequestConfiguration(int configurationId) + { + ErrorUtilities.VerifyThrow( + GetRequestsAssignedToConfigurationCount(configurationId) == 0, + "Configuration with ID {0} cannot be unassigned from a node, because there are requests scheduled with that configuration.", + configurationId); + + _configurationToNode.Remove(configurationId); + } + + /// + /// Gets a schedulable request with the specified global request id if it is currently scheduled. + /// + private SchedulableRequest InternalGetScheduledRequestByGlobalRequestId(int globalRequestId) + { + SchedulableRequest returnValue; + if (_executingRequests.TryGetValue(globalRequestId, out returnValue)) + { + return returnValue; + } + + if (_blockedRequests.TryGetValue(globalRequestId, out returnValue)) + { + return returnValue; + } + + if (_yieldingRequests.TryGetValue(globalRequestId, out returnValue)) + { + return returnValue; + } + + if (_readyRequests.TryGetValue(globalRequestId, out returnValue)) + { + return returnValue; + } + + return null; + } + + /// + /// Verifies that the request is scheduled and in the expected state. + /// + private void ExpectScheduledRequestState(int globalRequestId, SchedulableRequestState state) + { + SchedulableRequest request = InternalGetScheduledRequestByGlobalRequestId(globalRequestId); + if (request == null) + { + ErrorUtilities.ThrowInternalError("Request {0} was expected to be in state {1} but is not scheduled at all (it may be unscheduled or may be unknown to the system.)", globalRequestId, state); + } + else + { + request.VerifyState(state); + } + } + + /// + /// A scheduling event. + /// + internal class SchedulingEvent + { + /// + /// The time the event took place. + /// + private DateTime _eventTime; + + /// + /// The request involved in the event. + /// + private SchedulableRequest _request; + + /// + /// The state of the request before the event. + /// + private SchedulableRequestState _oldState; + + /// + /// The state of the request as a result of the event. + /// + private SchedulableRequestState _newState; + + /// + /// Constructor. + /// + public SchedulingEvent(DateTime eventTime, SchedulableRequest request, SchedulableRequestState oldState, SchedulableRequestState newState) + { + _eventTime = eventTime; + _request = request; + _oldState = oldState; + _newState = newState; + } + + /// + /// The time the event took place. + /// + public DateTime EventTime + { + get { return _eventTime; } + } + + /// + /// The request involved in the event. + /// + public SchedulableRequest Request + { + get { return _request; } + } + + /// + /// The state of the request before the event. + /// + public SchedulableRequestState OldState + { + get { return _oldState; } + } + + /// + /// The state of the request as a result of the event. + /// + public SchedulableRequestState NewState + { + get { return _newState; } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingPlan.cs b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingPlan.cs new file mode 100644 index 00000000000..e8a05e04953 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Components/Scheduler/SchedulingPlan.cs @@ -0,0 +1,733 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class which maintains the plan used by the Scheduler to efficiently distribute work to multiple nodes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd.Logging; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A SchedulingPlan contains timing and relationship information for a build which has already occurred. This data can then be + /// used by subsequent builds to determine how best to distribute work among several nodes. + /// + internal class SchedulingPlan + { + /// + /// The configuration cache. + /// + private IConfigCache _configCache; + + /// + /// The active scheduling data. + /// + private SchedulingData _schedulingData; + + /// + /// Mapping of project full paths to plan configuration data. + /// + private Dictionary _configPathToData = new Dictionary(); + + /// + /// Mapping of configuration ids to plan configuration data. + /// + private Dictionary _configIdToData = new Dictionary(); + + /// + /// Mapping of configuration ids to the set of configurations which were traversed to get to this configuration. + /// + private Dictionary>> _configIdToPaths = new Dictionary>>(); + + /// + /// Constructor. + /// + public SchedulingPlan(IConfigCache configCache, SchedulingData schedulingData) + { + _configCache = configCache; + _schedulingData = schedulingData; + this.MaximumConfigurationId = BuildRequestConfiguration.InvalidConfigurationId; + } + + /// + /// Returns true if a valid plan was read, false otherwise. + /// + public bool IsPlanValid + { + get; + private set; + } + + /// + /// Returns the largest configuration id known to the plan. + /// + public int MaximumConfigurationId + { + get; + private set; + } + + /// + /// Writes a plan for the specified submission id. + /// + public void WritePlan(int submissionId, ILoggingService loggingService, BuildEventContext buildEventContext) + { + if (!BuildParameters.EnableBuildPlan) + { + return; + } + + SchedulableRequest rootRequest = GetRootRequest(submissionId); + if (rootRequest == null) + { + return; + } + + string planName = GetPlanName(rootRequest); + if (String.IsNullOrEmpty(planName)) + { + return; + } + + try + { + using (StreamWriter file = new StreamWriter(File.Open(planName, FileMode.Create))) + { + // Write the accumulated configuration times. + Dictionary accumulatedTimeByConfiguration = new Dictionary(); + RecursiveAccumulateConfigurationTimes(rootRequest, accumulatedTimeByConfiguration); + + List configurationsInOrder = new List(accumulatedTimeByConfiguration.Keys); + configurationsInOrder.Sort(); + foreach (int configId in configurationsInOrder) + { + file.WriteLine(String.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", configId, accumulatedTimeByConfiguration[configId], _configCache[configId].ProjectFullPath)); + } + + file.WriteLine(); + + // Write out the dependency information. + RecursiveWriteDependencies(file, rootRequest); + } + } + catch (IOException) + { + loggingService.LogCommentFromText(buildEventContext, MessageImportance.Low, ResourceUtilities.FormatResourceString("CantWriteBuildPlan", planName)); + } + } + + /// + /// Reads a plan for the specified submission Id. + /// + public void ReadPlan(int submissionId, ILoggingService loggingService, BuildEventContext buildEventContext) + { + if (!BuildParameters.EnableBuildPlan) + { + return; + } + + SchedulableRequest rootRequest = GetRootRequest(submissionId); + if (rootRequest == null) + { + return; + } + + string planName = GetPlanName(rootRequest); + if (String.IsNullOrEmpty(planName)) + { + return; + } + + if (!File.Exists(planName)) + { + return; + } + + try + { + using (StreamReader file = new StreamReader(File.Open(planName, FileMode.Open))) + { + ReadTimes(file); + ReadHierarchy(file); + } + + if (_configIdToData.Count > 0) + { + AnalyzeData(); + } + } + catch (IOException) + { + loggingService.LogCommentFromText(buildEventContext, MessageImportance.Low, ResourceUtilities.FormatResourceString("CantReadBuildPlan", planName)); + } + catch (InvalidDataException) + { + loggingService.LogCommentFromText(buildEventContext, MessageImportance.Low, ResourceUtilities.FormatResourceString("BuildPlanCorrupt", planName)); + } + catch (FormatException) + { + loggingService.LogCommentFromText(buildEventContext, MessageImportance.Low, ResourceUtilities.FormatResourceString("BuildPlanCorrupt", planName)); + } + } + + /// + /// Returns the config id for the config specified by the path, if any. + /// + /// The config id if one exists, otherwise BuildRequestConfiguration.InvalidConfigurationId + public int GetConfigIdForPath(string configPath) + { + PlanConfigData config; + if (!_configPathToData.TryGetValue(configPath, out config)) + { + return BuildRequestConfiguration.InvalidConfigurationId; + } + + return config.ConfigId; + } + + /// + /// Given a list of configuration IDs, returns the id of the config with the greatest number of immediate references. + /// + /// The set of configurations to consider. + /// The id of the configuration with the most immediate references. + public int GetConfigWithGreatestNumberOfReferences(IEnumerable configsToSchedule) + { + return GetConfigWithComparison(configsToSchedule, delegate (PlanConfigData left, PlanConfigData right) { return Comparer.Default.Compare(left.ReferencesCount, right.ReferencesCount); }); + } + + /// + /// Given a list of real configuration IDs, returns the id of the config with the largest plan time. + /// + public int GetConfigWithGreatestPlanTime(IEnumerable realConfigsToSchedule) + { + return GetConfigWithComparison(realConfigsToSchedule, delegate (PlanConfigData left, PlanConfigData right) { return Comparer.Default.Compare(left.TotalPlanTime, right.TotalPlanTime); }); + } + + /// + /// Determines how many references a config with a particular path has. + /// + public int GetReferencesCountForConfigByPath(string configFullPath) + { + PlanConfigData data; + if (!_configPathToData.TryGetValue(configFullPath, out data)) + { + return 0; + } + + return data.ReferencesCount; + } + + /// + /// Advances the state of the plan by removing the specified config from all paths + /// + public void VisitConfig(string configName) + { + PlanConfigData data; + if (!_configPathToData.TryGetValue(configName, out data)) + { + return; + } + + // UNDONE: Parallelize + foreach (List> paths in _configIdToPaths.Values) + { + foreach (Stack path in paths) + { + if (path.Count > 0 && path.Peek() == data) + { + path.Pop(); + } + } + } + } + + /// + /// Advances the state of the plan by zeroing out the time spend on the config. + /// + public void CompleteConfig(string configName) + { + PlanConfigData data; + if (!_configPathToData.TryGetValue(configName, out data)) + { + return; + } + + ErrorUtilities.VerifyThrow(data.AccumulatedTimeOfReferences < 0.00001, "Unexpected config completed before references were completed."); + + // Recursively subtract the amount of time from this config's referrers. + data.RecursivelyApplyReferenceTimeToReferrers(-data.AccumulatedTime); + data.AccumulatedTime = 0; + } + + /// + /// Gets the name of the plan file for a specified submission. + /// + private string GetPlanName(SchedulableRequest rootRequest) + { + if (rootRequest == null) + { + return null; + } + + return _configCache[rootRequest.BuildRequest.ConfigurationId].ProjectFullPath + ".buildplan"; + } + + /// + /// Returns the config id with the greatest value according to the comparer. + /// + private int GetConfigWithComparison(IEnumerable realConfigsToSchedule, Comparison comparer) + { + PlanConfigData bestConfig = null; + int bestRealConfigId = BuildRequestConfiguration.InvalidConfigurationId; + + foreach (int realConfigId in realConfigsToSchedule) + { + PlanConfigData configToConsider; + if (!_configPathToData.TryGetValue(_configCache[realConfigId].ProjectFullPath, out configToConsider)) + { + // By default we assume configs we don't know about aren't as important, and will only schedule them + // if nothing else is suitable + if (bestRealConfigId == BuildRequestConfiguration.InvalidConfigurationId) + { + bestRealConfigId = realConfigId; + } + + continue; + } + + if (bestConfig == null || (comparer(bestConfig, configToConsider) < 0)) + { + bestConfig = configToConsider; + bestRealConfigId = realConfigId; + } + } + + return bestRealConfigId; + } + + /// + /// Analyzes the plan data which has been read. + /// + private void AnalyzeData() + { + DoRecursiveAnalysis(); + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDDEBUGSCHEDULER"))) + { + DetermineExpensiveConfigs(); + DetermineConfigsByNumberOfOccurrences(); + DetermineConfigsWithTheMostImmediateReferences(); + DetermineConfigsWithGreatestPlanTime(); + } + + IsPlanValid = true; + } + + /// + /// Writes out configuration in order of the greatest total plan time. + /// + private void DetermineConfigsWithGreatestPlanTime() + { + List projectsInOrderOfTotalPlanTime = new List(_configIdToData.Keys); + projectsInOrderOfTotalPlanTime.Sort(delegate (int left, int right) { return -Comparer.Default.Compare(_configIdToData[left].TotalPlanTime, _configIdToData[right].TotalPlanTime); }); + foreach (int configId in projectsInOrderOfTotalPlanTime) + { + PlanConfigData config = _configIdToData[configId]; + Console.WriteLine("{0}: {1} ({2} referrers) {3}", configId, config.TotalPlanTime, config.ReferrerCount, config.ConfigFullPath); + foreach (PlanConfigData referrer in config.Referrers) + { + Console.WriteLine(" {0} {1}", referrer.ConfigId, referrer.ConfigFullPath); + } + + Console.WriteLine(); + } + + Console.WriteLine(); + } + + /// + /// Writes out configs in order of most immediate references. + /// + private void DetermineConfigsWithTheMostImmediateReferences() + { + Console.WriteLine("Projects with the most immediate children:"); + List projectsInOrderOfImmediateChildCount = new List(_configIdToData.Keys); + projectsInOrderOfImmediateChildCount.Sort(delegate (int left, int right) { return -Comparer.Default.Compare(_configIdToData[left].ReferencesCount, _configIdToData[right].ReferencesCount); }); + foreach (int configId in projectsInOrderOfImmediateChildCount) + { + Console.WriteLine("{0}: {1} {2}", configId, _configIdToData[configId].ReferencesCount, _configIdToData[configId].ConfigFullPath); + } + + Console.WriteLine(); + } + + /// + /// Writes out configs in order of how often they are seen in the hierarchy. + /// + private void DetermineConfigsByNumberOfOccurrences() + { + Console.WriteLine("Configs in hierarchy by number of occurrences:"); + List projectsInOrderOfReference = new List(_configIdToData.Keys); + projectsInOrderOfReference.Sort(delegate (int left, int right) { return -Comparer.Default.Compare(_configIdToPaths[left].Count, _configIdToPaths[right].Count); }); + foreach (int configId in projectsInOrderOfReference) + { + Console.WriteLine("{0}: {1} {2}", configId, _configIdToPaths[configId].Count, _configIdToData[configId].ConfigFullPath); + } + + Console.WriteLine(); + } + + /// + /// This method finds all of the paths which lead to any given project + /// + private void DoRecursiveAnalysis() + { + Stack currentPath = new Stack(); + PlanConfigData root = _configIdToData[1]; + RecursiveVisitNodes(root, currentPath); + } + + /// + /// Recursively visits all nodes in the hierarchy. + /// + private void RecursiveVisitNodes(PlanConfigData root, Stack currentPath) + { + // Store the current path as a new path for this config + List> pathsForConfig; + if (!_configIdToPaths.TryGetValue(root.ConfigId, out pathsForConfig)) + { + pathsForConfig = new List>(); + _configIdToPaths[root.ConfigId] = pathsForConfig; + } + + // Reverse the stack so we get a path from the root to this node. + Stack pathToAdd = new Stack(currentPath); + + // And add it to the list of paths. + pathsForConfig.Add(pathToAdd); + + // Now add ourselves to the current path + currentPath.Push(root); + + // Visit our children + foreach (PlanConfigData child in root.References) + { + RecursiveVisitNodes(child, currentPath); + } + + // Remove ourselves from the current path + currentPath.Pop(); + } + + /// + /// Finds projects in order of expense and displays the paths leading to them. + /// + private void DetermineExpensiveConfigs() + { + Console.WriteLine("Projects by expense:"); + + List projectsByExpense = new List(_configIdToData.Values); + + // Sort most expensive to least expensive. + projectsByExpense.Sort(delegate (PlanConfigData left, PlanConfigData right) { return -Comparer.Default.Compare(left.AccumulatedTime, right.AccumulatedTime); }); + + foreach (PlanConfigData config in projectsByExpense) + { + Console.WriteLine("{0}: {1} {2}", config.ConfigId, config.AccumulatedTime, config.ConfigFullPath); + List> pathsByLength = _configIdToPaths[config.ConfigId]; + + // Sort the paths from shortest to longest. + pathsByLength.Sort(delegate (Stack left, Stack right) { return Comparer.Default.Compare(left.Count, right.Count); }); + foreach (Stack path in pathsByLength) + { + Console.Write(" "); + foreach (PlanConfigData pathEntry in path) + { + Console.Write(" {0}", pathEntry.ConfigId); + } + + Console.WriteLine(); + } + + Console.WriteLine(); + } + } + + /// + /// Reads the hierarchy from a plan file. + /// + private void ReadHierarchy(StreamReader file) + { + while (!file.EndOfStream) + { + string line = file.ReadLine(); + if (line.Length == 0) + { + return; + } + + string[] values = line.Split(new char[] { ' ' }); + if (values.Length < 1) + { + throw new InvalidDataException("Too few values in hierarchy"); + } + + int configId = Convert.ToInt32(values[0], CultureInfo.InvariantCulture); + PlanConfigData parent = _configIdToData[configId]; + + for (int i = 1; i < values.Length; i++) + { + int childId = Convert.ToInt32(values[i], CultureInfo.InvariantCulture); + PlanConfigData child = _configIdToData[childId]; + parent.AddReference(child); + } + } + } + + /// + /// Reads the accumulated time and path information for each configuration from the plan file. + /// + private void ReadTimes(StreamReader file) + { + while (!file.EndOfStream) + { + string line = file.ReadLine(); + if (line.Length == 0) + { + return; + } + + string[] values = line.Split(new char[] { ' ' }); + if (values.Length < 3) + { + throw new InvalidDataException("Too few values in build plan."); + } + + int configId = Convert.ToInt32(values[0], CultureInfo.InvariantCulture); + double accumulatedTime = Convert.ToDouble(values[1], CultureInfo.InvariantCulture); + string configFullPath = values[2]; + + PlanConfigData data = new PlanConfigData(configId, configFullPath, accumulatedTime); + _configIdToData[configId] = data; + _configPathToData[configFullPath] = data; + MaximumConfigurationId = Math.Max(MaximumConfigurationId, configId); + } + } + + /// + /// Retrieves the root request for the specified submission id. + /// + /// The request if one exists, otherwise null. + private SchedulableRequest GetRootRequest(int submissionId) + { + foreach (SchedulableRequest request in _schedulingData.GetRequestsByHierarchy(null)) + { + if (request.BuildRequest.SubmissionId == submissionId) + { + return request; + } + } + + return null; + } + + /// + /// Writes out all of the dependencies for a specified request, recursively. + /// + private void RecursiveWriteDependencies(StreamWriter file, SchedulableRequest request) + { + file.Write(request.BuildRequest.ConfigurationId); + foreach (SchedulableRequest child in _schedulingData.GetRequestsByHierarchy(request)) + { + file.Write(" {0}", child.BuildRequest.ConfigurationId); + } + + file.WriteLine(); + + foreach (SchedulableRequest child in _schedulingData.GetRequestsByHierarchy(request)) + { + RecursiveWriteDependencies(file, child); + } + } + + /// + /// Recursively accumulates the amount of time spent in each configuration. + /// + private void RecursiveAccumulateConfigurationTimes(SchedulableRequest request, Dictionary accumulatedTimeByConfiguration) + { + double accumulatedTime; + + // NOTE: Do we want to count it each time the config appears in the hierarchy? This will inflate the + // cost of frequently referenced configurations. + accumulatedTimeByConfiguration.TryGetValue(request.BuildRequest.ConfigurationId, out accumulatedTime); + accumulatedTimeByConfiguration[request.BuildRequest.ConfigurationId] = accumulatedTime + request.GetTimeSpentInState(SchedulableRequestState.Executing).TotalMilliseconds; + + foreach (SchedulableRequest childRequest in _schedulingData.GetRequestsByHierarchy(request)) + { + RecursiveAccumulateConfigurationTimes(childRequest, accumulatedTimeByConfiguration); + } + } + + /// + /// The data associated with a config as read from a build plan. + /// + private class PlanConfigData + { + /// + /// The configuration id. + /// + private int _configId; + + /// + /// The full path to the project. + /// + private string _configFullPath; + + /// + /// The amount of time spent in the configuration. + /// + private double _accumulatedTime; + + /// + /// The total time of all of the references. + /// + private double _accumulatedTimeOfReferences; + + /// + /// The set of references. + /// + private HashSet _references = new HashSet(); + + /// + /// The set of referrers. + /// + private HashSet _referrers = new HashSet(); + + /// + /// Constructor. + /// + public PlanConfigData(int configId, string configFullPath, double accumulatedTime) + { + _configId = configId; + _configFullPath = configFullPath; + _accumulatedTime = accumulatedTime; + } + + /// + /// Gets the configuration id. + /// + public int ConfigId + { + get { return _configId; } + } + + /// + /// Gets the configuration's full path. + /// + public string ConfigFullPath + { + get { return _configFullPath; } + } + + /// + /// Gets the configuration's accumulated time. + /// + public double AccumulatedTime + { + get { return _accumulatedTime; } + set { _accumulatedTime = value; } + } + + /// + /// Gets the configuration's accumulated time for all of its references. + /// + public double AccumulatedTimeOfReferences + { + get { return _accumulatedTimeOfReferences; } + } + + /// + /// Retrieves the total time for this configuration, which includes the time spent on its references. + /// + public double TotalPlanTime + { + // Count our time, plus the amount of time all of our children take. Multiply this by the total number + // of referrers to weight us higher the more configurations depend on us. + get { return (AccumulatedTime + AccumulatedTimeOfReferences); } + } + + /// + /// Retrieves the number of references this configuration has. + /// + public int ReferencesCount + { + get { return _references.Count; } + } + + /// + /// Retrieves the references from this configuration. + /// + public IEnumerable References + { + get { return _references; } + } + + /// + /// Retrieves the number of configurations which refer to this one. + /// + public int ReferrerCount + { + get { return _referrers.Count; } + } + + /// + /// Retrieves the configurations which refer to this one. + /// + public IEnumerable Referrers + { + get { return _referrers; } + } + + /// + /// Adds the specified configuration as a reference. + /// + public void AddReference(PlanConfigData reference) + { + if (!_references.Contains(reference)) + { + _references.Add(reference); + + // My own accumulated reference time and that of all of my referrers increases as well + if (!reference._referrers.Contains(this)) + { + reference._referrers.Add(this); + } + + _accumulatedTimeOfReferences += reference.AccumulatedTime; + RecursivelyApplyReferenceTimeToReferrers(reference.AccumulatedTime); + } + } + + /// + /// Applies the specified duration offset to the configurations which refer to this one. + /// + public void RecursivelyApplyReferenceTimeToReferrers(double duration) + { + foreach (PlanConfigData referrer in _referrers) + { + referrer._accumulatedTimeOfReferences += duration; + referrer.RecursivelyApplyReferenceTimeToReferrers(duration); + } + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Node/INode.cs b/src/XMakeBuildEngine/BackEnd/Node/INode.cs new file mode 100644 index 00000000000..e77d2702a05 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Node/INode.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Interface for a node. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + using NodeEngineShutdownReason = Microsoft.Build.Execution.NodeEngineShutdownReason; + + #region Delegates + /// + /// Delegate is called when a node shuts down. + /// + /// The reason for the shutdown + /// The exception which caused an unexpected shutdown, if any. + internal delegate void NodeShutdownDelegate(NodeEngineShutdownReason reason, Exception e); + #endregion + + /// + /// This interface is implemented by a build node, and allows the host process to control its execution. + /// + internal interface INode + { + #region Methods + + /// + /// Runs the Node. Returns the reason the node shut down. + /// + NodeEngineShutdownReason Run(out Exception shutdownException); + + #endregion + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Node/InProcNode.cs b/src/XMakeBuildEngine/BackEnd/Node/InProcNode.cs new file mode 100644 index 00000000000..b2927add66c --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Node/InProcNode.cs @@ -0,0 +1,549 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing an in-proc node. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Threading; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +using BuildEventArgTransportSink = Microsoft.Build.BackEnd.Logging.BuildEventArgTransportSink; +using LoggingService = Microsoft.Build.BackEnd.Logging.LoggingService; +using LoggingServiceFactory = Microsoft.Build.BackEnd.Logging.LoggingServiceFactory; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; +using LoggingExceptionDelegate = Microsoft.Build.BackEnd.Logging.LoggingExceptionDelegate; +using Microsoft.Build.BackEnd.Components.Caching; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class represents an implementation of INode for out-of-proc nodes. + /// + internal class InProcNode : INode, INodePacketFactory + { + /// + /// The build component host. + /// + private IBuildComponentHost _componentHost; + + /// + /// The environment at the time the build is started. + /// + private IDictionary _savedEnvironment; + + /// + /// The current directory at the time the build is started. + /// + private string _savedCurrentDirectory; + + /// + /// The node logging context. + /// + private NodeLoggingContext _loggingContext; + + /// + /// The build request engine. + /// + private IBuildRequestEngine _buildRequestEngine; + + /// + /// The current node configuration + /// + private NodeConfiguration _currentConfiguration; + + /// + /// The queue of packets we have received but which have not yet been processed. + /// + private Queue _receivedPackets; + + /// + /// The event which is set when we receive packets. + /// + private AutoResetEvent _packetReceivedEvent; + + /// + /// The event which is set when we should shut down. + /// + private AutoResetEvent _shutdownEvent; + + /// + /// The reason we are shutting down. + /// + private NodeEngineShutdownReason _shutdownReason; + + /// + /// The exception, if any, which caused shutdown. + /// + private Exception _shutdownException; + + /// + /// The set of configurations which have had projects loaded. + /// + private HashSet> _configurationProjectsLoaded; + + /// + /// The node endpoint + /// + private INodeEndpoint _nodeEndpoint; + + /// + /// Handler for engine exceptions. + /// + private EngineExceptionDelegate _engineExceptionEventHandler; + + /// + /// Handler for new configuration requests. + /// + private NewConfigurationRequestDelegate _newConfigurationRequestEventHandler; + + /// + /// Handler for blocked request events. + /// + private RequestBlockedDelegate _requestBlockedEventHandler; + + /// + /// Handler for request completed events. + /// + private RequestCompleteDelegate _requestCompleteEventHandler; + + /// + /// Constructor. + /// + public InProcNode(IBuildComponentHost componentHost, INodeEndpoint inProcNodeEndpoint) + { + _componentHost = componentHost; + _nodeEndpoint = inProcNodeEndpoint; + _receivedPackets = new Queue(); + _packetReceivedEvent = new AutoResetEvent(false); + _shutdownEvent = new AutoResetEvent(false); + + _configurationProjectsLoaded = new HashSet>(); + + _buildRequestEngine = componentHost.GetComponent(BuildComponentType.RequestEngine) as IBuildRequestEngine; + + _engineExceptionEventHandler = new EngineExceptionDelegate(OnEngineException); + _newConfigurationRequestEventHandler = new NewConfigurationRequestDelegate(OnNewConfigurationRequest); + _requestBlockedEventHandler = new RequestBlockedDelegate(OnNewRequest); + _requestCompleteEventHandler = new RequestCompleteDelegate(OnRequestComplete); + } + + #region INode Members + + /// + /// Starts up the node and processes messages until the node is requested to shut down. + /// + /// The exception which caused shutdown, if any. + /// The reason for shutting down. + public NodeEngineShutdownReason Run(out Exception shutdownException) + { + try + { + _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged); + _nodeEndpoint.Listen(this); + + WaitHandle[] waitHandles = new WaitHandle[] { _shutdownEvent, _packetReceivedEvent }; + + // Get the current directory before doing work. We need this so we can restore the directory when the node shuts down. + _savedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + while (true) + { + int index = WaitHandle.WaitAny(waitHandles); + switch (index) + { + case 0: + { + NodeEngineShutdownReason shutdownReason = HandleShutdown(out shutdownException); + if (_componentHost.BuildParameters.ShutdownInProcNodeOnBuildFinish) + { + return shutdownReason; + } + + break; + } + + case 1: + INodePacket packet = null; + + int packetCount = _receivedPackets.Count; + + while (packetCount > 0) + { + lock (_receivedPackets) + { + if (_receivedPackets.Count > 0) + { + packet = _receivedPackets.Dequeue(); + } + else + { + break; + } + } + + if (packet != null) + { + HandlePacket(packet); + } + } + + break; + } + } + } + catch (ThreadAbortException) + { + // Do nothing. This will happen when the thread is forcibly terminated because we are shutting down, for example + // when the unit test framework terminates. + throw; + } + catch (Exception e) + { + // Dump all engine exceptions to a temp file + // so that we have something to go on in the + // event of a failure + ExceptionHandling.DumpExceptionToFile(e); + + // This is fatal: process will terminate: make sure the + // debugger launches + ErrorUtilities.ThrowInternalError(e.Message, e); + throw; + } + + // UNREACHABLE + } + + #endregion + + #region INodePacketFactory Members + + /// + /// Not necessary for in-proc node - we don't serialize. + /// + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + // The in-proc node doesn't need to do this. + } + + /// + /// Not necessary for in-proc node - we don't serialize. + /// + public void UnregisterPacketHandler(NodePacketType packetType) + { + // The in-proc node doesn't need to do this. + } + + /// + /// Not necessary for in-proc node - we don't serialize. + /// + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + // The in-proc endpoint shouldn't be serializing, just routing. + ErrorUtilities.ThrowInternalError("Unexpected call to DeserializeAndRoutePacket on the in-proc node."); + } + + /// + /// Routes the packet to the appropriate handler. + /// + /// The node id. + /// The packet. + public void RoutePacket(int nodeId, INodePacket packet) + { + lock (_receivedPackets) + { + _receivedPackets.Enqueue(packet); + _packetReceivedEvent.Set(); + } + } + + #endregion + + /// + /// Event handler for the BuildEngine's OnRequestComplete event. + /// + private void OnRequestComplete(BuildRequest request, BuildResult result) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(result); + } + } + + /// + /// Event handler for the BuildEngine's OnNewRequest event. + /// + private void OnNewRequest(BuildRequestBlocker blocker) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(blocker); + } + } + + /// + /// Event handler for the BuildEngine's OnNewConfigurationRequest event. + /// + private void OnNewConfigurationRequest(BuildRequestConfiguration config) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(config); + } + } + + /// + /// Event handler for the LoggingService's OnLoggingThreadException event. + /// + private void OnLoggingThreadException(Exception e) + { + OnEngineException(e); + } + + /// + /// Event handler for the BuildEngine's OnEngineException event. + /// + private void OnEngineException(Exception e) + { + _shutdownException = e; + _shutdownReason = NodeEngineShutdownReason.Error; + _shutdownEvent.Set(); + } + + /// + /// Perform necessary actions to shut down the node. + /// + private NodeEngineShutdownReason HandleShutdown(out Exception exception) + { + // Console.WriteLine("Node shutting down with reason {0} and exception: {1}", shutdownReason, shutdownException); + try + { + // Clean up the engine + if (null != _buildRequestEngine && _buildRequestEngine.Status != BuildRequestEngineStatus.Uninitialized) + { + _buildRequestEngine.CleanupForBuild(); + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + // If we had some issue shutting down, don't reuse the node because we may be in some weird state. + if (_shutdownReason == NodeEngineShutdownReason.BuildCompleteReuse) + { + _shutdownReason = NodeEngineShutdownReason.BuildComplete; + } + } + + // Dispose of any build registered objects + IRegisteredTaskObjectCache objectCache = (IRegisteredTaskObjectCache)(_componentHost.GetComponent(BuildComponentType.RegisteredTaskObjectCache)); + objectCache.DisposeCacheObjects(RegisteredTaskObjectLifetime.Build); + + if (_shutdownReason != NodeEngineShutdownReason.BuildCompleteReuse) + { + // Dispose of any node registered objects. + ((IBuildComponent)objectCache).ShutdownComponent(); + } + + if (_componentHost.BuildParameters.SaveOperatingEnvironment) + { + // Restore the original current directory. + NativeMethodsShared.SetCurrentDirectory(_savedCurrentDirectory); + + // Restore the original environment. + foreach (KeyValuePair entry in CommunicationsUtilities.GetEnvironmentVariables()) + { + if (!_savedEnvironment.ContainsKey(entry.Key)) + { + Environment.SetEnvironmentVariable(entry.Key, null); + } + } + + foreach (KeyValuePair entry in _savedEnvironment) + { + Environment.SetEnvironmentVariable(entry.Key, entry.Value); + } + } + + exception = _shutdownException; + + if (null != _loggingContext) + { + _loggingContext.LoggingService.OnLoggingThreadException -= new LoggingExceptionDelegate(OnLoggingThreadException); + _loggingContext = null; + } + + // Notify the BuildManager that we are done. + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(new NodeShutdown(_shutdownReason == NodeEngineShutdownReason.Error ? NodeShutdownReason.Error : NodeShutdownReason.Requested, exception)); + } + + _buildRequestEngine.OnEngineException -= _engineExceptionEventHandler; + _buildRequestEngine.OnNewConfigurationRequest -= _newConfigurationRequestEventHandler; + _buildRequestEngine.OnRequestBlocked -= _requestBlockedEventHandler; + _buildRequestEngine.OnRequestComplete -= _requestCompleteEventHandler; + + return _shutdownReason; + } + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + switch (packet.Type) + { + case NodePacketType.BuildRequest: + HandleBuildRequest(packet as BuildRequest); + break; + + case NodePacketType.BuildRequestConfiguration: + HandleBuildRequestConfiguration(packet as BuildRequestConfiguration); + break; + + case NodePacketType.BuildRequestConfigurationResponse: + HandleBuildRequestConfigurationResponse(packet as BuildRequestConfigurationResponse); + break; + + case NodePacketType.BuildRequestUnblocker: + HandleBuildResult(packet as BuildRequestUnblocker); + break; + + case NodePacketType.NodeConfiguration: + HandleNodeConfiguration(packet as NodeConfiguration); + break; + + case NodePacketType.NodeBuildComplete: + HandleNodeBuildComplete(packet as NodeBuildComplete); + break; + } + } + + /// + /// Event handler for the node endpoint's LinkStatusChanged event. + /// + private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + switch (status) + { + case LinkStatus.ConnectionFailed: + case LinkStatus.Failed: + _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; + _shutdownEvent.Set(); + break; + + case LinkStatus.Inactive: + break; + + default: + break; + } + } + + /// + /// Handles the BuildRequest packet. + /// + private void HandleBuildRequest(BuildRequest request) + { + _buildRequestEngine.SubmitBuildRequest(request); + } + + /// + /// Handles the BuildRequestConfiguration packet. + /// + private void HandleBuildRequestConfiguration(BuildRequestConfiguration configuration) + { + // Configurations are already in the cache, which we share with the BuildManager. + } + + /// + /// Handles the BuildRequestConfigurationResponse packet. + /// + private void HandleBuildRequestConfigurationResponse(BuildRequestConfigurationResponse response) + { + _buildRequestEngine.ReportConfigurationResponse(response); + } + + /// + /// Handles the BuildResult packet. + /// + private void HandleBuildResult(BuildRequestUnblocker unblocker) + { + _buildRequestEngine.UnblockBuildRequest(unblocker); + } + + /// + /// Handles the NodeConfiguration packet. + /// + private void HandleNodeConfiguration(NodeConfiguration configuration) + { + // Set the culture. + Thread.CurrentThread.CurrentCulture = configuration.BuildParameters.Culture; + Thread.CurrentThread.CurrentUICulture = configuration.BuildParameters.UICulture; + + // Snapshot the initial environment. + _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + + // Save the current directory. + _savedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); + + // Set the node id. + _componentHost.BuildParameters.NodeId = configuration.NodeId; + _shutdownException = null; + + // And the AppDomainSetup + _componentHost.BuildParameters.AppDomainSetup = configuration.AppDomainSetup; + + // Declare in-proc + _componentHost.BuildParameters.IsOutOfProc = false; + + // Set the logging exception handler + ILoggingService loggingService = _componentHost.LoggingService; + loggingService.OnLoggingThreadException += new LoggingExceptionDelegate(OnLoggingThreadException); + + // Now prep the buildRequestEngine for the build. + _loggingContext = new NodeLoggingContext(loggingService, configuration.NodeId, true /* inProcNode */); + + _buildRequestEngine.OnEngineException += _engineExceptionEventHandler; + _buildRequestEngine.OnNewConfigurationRequest += _newConfigurationRequestEventHandler; + _buildRequestEngine.OnRequestBlocked += _requestBlockedEventHandler; + _buildRequestEngine.OnRequestComplete += _requestCompleteEventHandler; + + if (_shutdownException != null) + { + Exception exception; + HandleShutdown(out exception); + throw exception; + } + + _buildRequestEngine.InitializeForBuild(_loggingContext); + + // Finally store off this configuration packet. + _currentConfiguration = configuration; + } + + /// + /// Handles the NodeBuildComplete packet. + /// + private void HandleNodeBuildComplete(NodeBuildComplete buildComplete) + { + _shutdownReason = buildComplete.PrepareForReuse ? NodeEngineShutdownReason.BuildCompleteReuse : NodeEngineShutdownReason.BuildComplete; + _shutdownEvent.Set(); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Node/NativeMethods.cs b/src/XMakeBuildEngine/BackEnd/Node/NativeMethods.cs new file mode 100644 index 00000000000..2b8110f36d6 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Node/NativeMethods.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class which contains the native methods used by the BackEnd +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Native methods used by the backend. This was copied from the oldOM so we can make it stylecop compliant and allow + /// easier deletion of the native code in the old OM + /// + internal static class NativeMethods + { + /// + /// Null Pointer + /// + internal static readonly IntPtr NullPtr = IntPtr.Zero; + + /// + /// Invalid Handle + /// + internal static readonly IntPtr InvalidHandle = new IntPtr(-1); + + /// + /// Start the process with a normal priority class + /// + internal const uint NORMALPRIORITYCLASS = 0x0020; + + /// + /// Do not create a window + /// + internal const uint CREATENOWINDOW = 0x08000000; + + /// + /// Use the standard handles + /// + internal const Int32 STARTFUSESTDHANDLES = 0x00000100; + + /// + /// Create a new console. + /// + internal const Int32 CREATE_NEW_CONSOLE = 0x00000010; + + /// + /// Create a new process + /// + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CreateProcess + ( + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + [In, MarshalAs(UnmanagedType.Bool)] + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + [In] ref STARTUP_INFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation + ); + + /// + /// Structure that contains the startupinfo + /// Represents STARTUP_INFO in win32 + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct STARTUP_INFO + { + /// + /// The size of the structure, in bytes. + /// + internal Int32 cb; + + /// + /// Reserved; must be NULL + /// + internal string lpReserved; + + /// + /// The name of the desktop, or the name of both the desktop and window station for this process. + /// A backslash in the string indicates that the string includes both the desktop and window station names + /// + internal string lpDesktop; + + /// + /// For console processes, this is the title displayed in the title bar if a new console window is created. + /// If NULL, the name of the executable file is used as the window title instead. + /// This parameter must be NULL for GUI or console processes that do not create a new console window + /// + internal string lpTitle; + + /// + /// If dwFlags specifies STARTF_USEPOSITION, this member is the x offset of the upper left corner of a window if a new window is created, in pixels. Otherwise, this member is ignored + /// + internal Int32 dwX; + + /// + /// If dwFlags specifies STARTF_USEPOSITION, this member is the y offset of the upper left corner of a window if a new window is created, in pixels. Otherwise, this member is ignored. + /// + internal Int32 dwY; + + /// + /// If dwFlags specifies STARTF_USESIZE, this member is the width of the window if a new window is created, in pixels. Otherwise, this member is ignored. + /// + internal Int32 dwXSize; + + /// + /// If dwFlags specifies STARTF_USESIZE, this member is the height of the window if a new window is created, in pixels. Otherwise, this member is ignored. + /// + internal Int32 dwYSize; + + /// + /// If dwFlags specifies STARTF_USECOUNTCHARS, if a new console window is created in a console process, this member specifies the screen buffer width, in character columns. Otherwise, this member is ignored. + /// + internal Int32 dwXCountChars; + + /// + /// If dwFlags specifies STARTF_USECOUNTCHARS, if a new console window is created in a console process, this member specifies the screen buffer height, in character rows. Otherwise, this member is ignored.dwFillAttribute + /// + internal Int32 dwYCountChars; + + /// + /// If dwFlags specifies STARTF_USEFILLATTRIBUTE, this member is the initial text and background colors if a new console window is created in a console application. Otherwise, this member is ignored. + /// + internal Int32 dwFillAttribute; + + /// + /// A bit field that determines whether certain STARTUPINFO members are used when the process creates a window + /// + internal Int32 dwFlags; + + /// + /// If dwFlags specifies STARTF_USESHOWWINDOW, this member can be any of the SW_ constants defined in Winuser.h. Otherwise, this member is ignored. + /// + internal Int16 wShowWindow; + + /// + /// Reserved for use by the C Run-time; must be zero. + /// + internal Int16 cbReserved2; + + /// + /// Reserved for use by the C Run-time; must be NULL. + /// + internal IntPtr lpReserved2; + + /// + /// If dwFlags specifies STARTF_USESTDHANDLES, this member is the standard input handle for the process. Otherwise, this member is ignored and the default for standard input is the keyboard buffer. + /// + internal IntPtr hStdInput; + + /// + /// If dwFlags specifies STARTF_USESTDHANDLES, this member is the standard output handle for the process. Otherwise, this member is ignored and the default for standard output is the console window's buffer. + /// + internal IntPtr hStdOutput; + + /// + /// If dwFlags specifies STARTF_USESTDHANDLES, this member is the standard error handle for the process. Otherwise, this member is ignored and the default for standard error is the console window's buffer. + /// + internal IntPtr hStdError; + } + + /// + /// Structure to contain security attributes from the create process call represents + /// SECURITY_ATTRIBUTE in win32 + /// + [StructLayout(LayoutKind.Sequential)] + internal struct SECURITY_ATTRIBUTES + { + /// + /// The size, in bytes, of this structure. Set this value to the size of the SECURITY_ATTRIBUTES structure + /// + public int nLength; + + /// + /// A pointer to a security descriptor for the object that controls the sharing of it. + /// If NULL is specified for this member, the object is assigned the default security descriptor of the calling process. + /// This is not the same as granting access to everyone by assigning a NULL discretionary access control list (DACL). + /// The default security descriptor is based on the default DACL of the access token belonging to the calling process. + /// By default, the default DACL in the access token of a process allows access only to the user represented by the access token. + /// If other users must access the object, you can either create a security descriptor with the appropriate access, + /// or add ACEs to the DACL that grants access to a group of users. + /// + public IntPtr lpSecurityDescriptor; + + /// + /// A Boolean value that specifies whether the returned handle is inherited when a new process is created. + /// If this member is TRUE, the new process inherits the handle. + /// + public int bInheritHandle; + } + + /// + /// Process information from the create process call + /// Represents PROCESS_INFORMATION in win32 + /// + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_INFORMATION + { + /// + /// A handle to the newly created process. The handle is used to specify the process in all functions that perform operations on the process object. + /// + public IntPtr hProcess; + + /// + /// A handle to the primary thread of the newly created process. The handle is used to specify the thread in all functions that perform operations on the thread object + /// + public IntPtr hThread; + + /// + /// A value that can be used to identify a process. + /// The value is valid from the time the process is created until all handles to the process are closed and + /// the process object is freed; at this point, the identifier may be reused. + /// + public int dwProcessId; + + /// + /// A value that can be used to identify a thread. The value is valid from the time the thread is created until all handles to the thread are closed and the thread object is freed; at this point, the identifier may be reused. + /// + public int dwThreadId; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Node/NodeConfiguration.cs b/src/XMakeBuildEngine/BackEnd/Node/NodeConfiguration.cs new file mode 100644 index 00000000000..b9e4a2cbf1b --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Node/NodeConfiguration.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A packet which contains information needed for a node to configure itself for build. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; + +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; +using BuildParameters = Microsoft.Build.Execution.BuildParameters; + +namespace Microsoft.Build.BackEnd +{ + /// + /// NodeConfiguration contains all of the information necessary for a node to configure itself for building. + /// + internal class NodeConfiguration : INodePacket + { + /// + /// The node id + /// + private int _nodeId; + + /// + /// The system parameters which were defined on the host. + /// + private BuildParameters _buildParameters; + + /// + /// The app domain information needed for setting up AppDomain-isolated tasks. + /// + private AppDomainSetup _appDomainSetup; + + /// + /// The forwarding loggers to use. + /// + private LoggerDescription[] _forwardingLoggers; + + /// + /// Constructor + /// + /// The node id. + /// The build parameters + /// The forwarding loggers. + /// The AppDomain setup information. + public NodeConfiguration + ( + int nodeId, + BuildParameters buildParameters, + LoggerDescription[] forwardingLoggers, + AppDomainSetup appDomainSetup + ) + { + _nodeId = nodeId; + _buildParameters = buildParameters; + _forwardingLoggers = forwardingLoggers; + _appDomainSetup = appDomainSetup; + } + + /// + /// Private constructor for deserialization + /// + private NodeConfiguration() + { + } + + /// + /// Gets or sets the node id + /// + public int NodeId + { + [DebuggerStepThrough] + get + { return _nodeId; } + + [DebuggerStepThrough] + set + { _nodeId = value; } + } + + /// + /// Retrieves the system parameters. + /// + public BuildParameters BuildParameters + { + [DebuggerStepThrough] + get + { return _buildParameters; } + } + + /// + /// Retrieves the logger descriptions. + /// + public LoggerDescription[] LoggerDescriptions + { + [DebuggerStepThrough] + get + { return _forwardingLoggers; } + } + + /// + /// Retrieves the app domain setup information. + /// + public AppDomainSetup AppDomainSetup + { + [DebuggerStepThrough] + get + { return _appDomainSetup; } + } + + #region INodePacket Members + + /// + /// Retrieves the packet type. + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.NodeConfiguration; } + } + + #endregion + + #region INodePacketTranslatable Members + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _nodeId); + translator.Translate(ref _buildParameters, BuildParameters.FactoryForDeserialization); + translator.TranslateArray(ref _forwardingLoggers, LoggerDescription.FactoryForTranslation); + translator.TranslateDotNet(ref _appDomainSetup); + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + NodeConfiguration configuration = new NodeConfiguration(); + configuration.Translate(translator); + return configuration; + } + #endregion + + /// + /// We need to clone this object since it gets modified for each node which is launched. + /// + internal NodeConfiguration Clone() + { + return new NodeConfiguration(_nodeId, _buildParameters, _forwardingLoggers, _appDomainSetup); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Node/OutOfProcNode.cs b/src/XMakeBuildEngine/BackEnd/Node/OutOfProcNode.cs new file mode 100644 index 00000000000..397ede13ae4 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Node/OutOfProcNode.cs @@ -0,0 +1,796 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing an out-of-proc node. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Threading; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; +using Microsoft.Build.BackEnd.Components.Caching; + +namespace Microsoft.Build.Execution +{ + /// + /// This class represents an implementation of INode for out-of-proc nodes. + /// + public class OutOfProcNode : INode, IBuildComponentHost, INodePacketFactory, INodePacketHandler + { + /// + /// Whether the current appdomain has an out of proc node. + /// For diagnostics. + /// + private static bool s_isOutOfProcNode; + + /// + /// The one and only project root element cache to be used for the build + /// on this out of proc node. + /// + private static ProjectRootElementCache s_projectRootElementCache; + + /// + /// The endpoint used to talk to the host. + /// + private NodeEndpointOutOfProc _nodeEndpoint; + + /// + /// The saved environment for the process. + /// + private IDictionary _savedEnvironment; + + /// + /// The component factories. + /// + private BuildComponentFactoryCollection _componentFactories; + + /// + /// The build system parameters. + /// + private BuildParameters _buildParameters; + + /// + /// The logging service. + /// + private ILoggingService _loggingService; + + /// + /// The node logging context. + /// + private NodeLoggingContext _loggingContext; + + /// + /// The global config cache. + /// + private IConfigCache _globalConfigCache; + + /// + /// The global node manager + /// + private INodeManager _taskHostNodeManager; + + /// + /// The build request engine. + /// + private IBuildRequestEngine _buildRequestEngine; + + /// + /// The packet factory. + /// + private NodePacketFactory _packetFactory; + + /// + /// The current node configuration + /// + private NodeConfiguration _currentConfiguration; + + /// + /// The queue of packets we have received but which have not yet been processed. + /// + private Queue _receivedPackets; + + /// + /// The event which is set when we receive packets. + /// + private AutoResetEvent _packetReceivedEvent; + + /// + /// The event which is set when we should shut down. + /// + private ManualResetEvent _shutdownEvent; + + /// + /// The reason we are shutting down. + /// + private NodeEngineShutdownReason _shutdownReason; + + /// + /// The exception, if any, which caused shutdown. + /// + private Exception _shutdownException; + + /// + /// Flag indicating if we should debug communications or not. + /// + private bool _debugCommunications = false; + + /// + /// Data for the use of LegacyThreading semantics. + /// + private LegacyThreadingData _legacyThreadingData; + + /// + /// Constructor. + /// + public OutOfProcNode() + { + s_isOutOfProcNode = true; + + AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(ExceptionHandling.UnhandledExceptionHandler); + + _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); + + _receivedPackets = new Queue(); + _packetReceivedEvent = new AutoResetEvent(false); + _shutdownEvent = new ManualResetEvent(false); + _legacyThreadingData = new LegacyThreadingData(); + + _componentFactories = new BuildComponentFactoryCollection(this); + _componentFactories.RegisterDefaultFactories(); + _packetFactory = new NodePacketFactory(); + + _buildRequestEngine = (this as IBuildComponentHost).GetComponent(BuildComponentType.RequestEngine) as IBuildRequestEngine; + _globalConfigCache = (this as IBuildComponentHost).GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + _taskHostNodeManager = (this as IBuildComponentHost).GetComponent(BuildComponentType.TaskHostNodeManager) as INodeManager; + + if (s_projectRootElementCache == null) + { + s_projectRootElementCache = new ProjectRootElementCache(true /* automatically reload any changes from disk */); + } + + _buildRequestEngine.OnEngineException += new EngineExceptionDelegate(OnEngineException); + _buildRequestEngine.OnNewConfigurationRequest += new NewConfigurationRequestDelegate(OnNewConfigurationRequest); + _buildRequestEngine.OnRequestBlocked += new RequestBlockedDelegate(OnNewRequest); + _buildRequestEngine.OnRequestComplete += new RequestCompleteDelegate(OnRequestComplete); + + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.BuildRequest, BuildRequest.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.BuildRequestConfiguration, BuildRequestConfiguration.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.BuildRequestConfigurationResponse, BuildRequestConfigurationResponse.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.BuildRequestUnblocker, BuildRequestUnblocker.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeConfiguration, NodeConfiguration.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); + } + + /// + /// Get the logging service for a build. + /// + /// The logging service. + ILoggingService IBuildComponentHost.LoggingService + { + get + { + return _loggingService; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular build manager + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + /// + /// Retrieves the name of this component host. + /// + string IBuildComponentHost.Name + { + get + { + return "OutOfProc"; + } + } + + /// + /// Retrieves the build parameters for the current build. + /// + /// The build parameters. + BuildParameters IBuildComponentHost.BuildParameters + { + get + { + return _buildParameters; + } + } + + /// + /// Whether the current appdomain has an out of proc node. + /// + internal static bool IsOutOfProcNode + { + get { return s_isOutOfProcNode; } + } + + #region INode Members + + /// + /// Starts up the node and processes messages until the node is requested to shut down. + /// + /// The exception which caused shutdown, if any. + /// The reason for shutting down. + public NodeEngineShutdownReason Run(out Exception shutdownException) + { + // Console.WriteLine("Run called at {0}", DateTime.Now); + string pipeName = "MSBuild" + Process.GetCurrentProcess().Id; + + _nodeEndpoint = new NodeEndpointOutOfProc(pipeName, this); + _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged); + _nodeEndpoint.Listen(this); + + WaitHandle[] waitHandles = new WaitHandle[] { _shutdownEvent, _packetReceivedEvent }; + + // Get the current directory before doing any work. We need this so we can restore the directory when the node shutsdown. + while (true) + { + int index = WaitHandle.WaitAny(waitHandles); + switch (index) + { + case 0: + NodeEngineShutdownReason shutdownReason = HandleShutdown(out shutdownException); + return shutdownReason; + + case 1: + INodePacket packet = null; + + int packetCount = _receivedPackets.Count; + + while (packetCount > 0) + { + lock (_receivedPackets) + { + if (_receivedPackets.Count > 0) + { + packet = _receivedPackets.Dequeue(); + } + else + { + break; + } + } + + if (packet != null) + { + HandlePacket(packet); + } + } + + break; + } + } + + // UNREACHABLE + } + + #endregion + + #region IBuildComponentHost Members + + /// + /// Registers a factory with the component host. + /// + /// The factory type to register. + /// The factory method. + void IBuildComponentHost.RegisterFactory(BuildComponentType factoryType, BuildComponentFactoryDelegate factory) + { + _componentFactories.ReplaceFactory(factoryType, factory); + } + + /// + /// Get a component from the host. + /// + /// The component type to get. + /// The component. + IBuildComponent IBuildComponentHost.GetComponent(BuildComponentType type) + { + return _componentFactories.GetComponent(type); + } + + #endregion + + #region INodePacketFactory Members + + /// + /// Registers a packet handler. + /// + /// The packet type for which the handler should be registered. + /// The factory used to create packets. + /// The handler for the packets. + void INodePacketFactory.RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _packetFactory.RegisterPacketHandler(packetType, factory, handler); + } + + /// + /// Unregisters a packet handler. + /// + /// The type of packet for which the handler should be unregistered. + void INodePacketFactory.UnregisterPacketHandler(NodePacketType packetType) + { + _packetFactory.UnregisterPacketHandler(packetType); + } + + /// + /// Deserializes and routes a packer to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator to use as a source for packet data. + void INodePacketFactory.DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + } + + /// + /// Routes a packet to the appropriate handler. + /// + /// The node id from which the packet was received. + /// The packet to route. + void INodePacketFactory.RoutePacket(int nodeId, INodePacket packet) + { + _packetFactory.RoutePacket(nodeId, packet); + } + + #endregion + + #region INodePacketHandler Members + + /// + /// Called when a packet has been received. + /// + /// The node from which the packet was received. + /// The packet. + void INodePacketHandler.PacketReceived(int node, INodePacket packet) + { + lock (_receivedPackets) + { + _receivedPackets.Enqueue(packet); + _packetReceivedEvent.Set(); + } + } + + #endregion + + /// + /// Event handler for the BuildEngine's OnRequestComplete event. + /// + private void OnRequestComplete(BuildRequest request, BuildResult result) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(result); + } + } + + /// + /// Event handler for the BuildEngine's OnNewRequest event. + /// + private void OnNewRequest(BuildRequestBlocker blocker) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(blocker); + } + } + + /// + /// Event handler for the BuildEngine's OnNewConfigurationRequest event. + /// + private void OnNewConfigurationRequest(BuildRequestConfiguration config) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(config); + } + } + + /// + /// Event handler for the LoggingService's OnLoggingThreadException event. + /// + private void OnLoggingThreadException(Exception e) + { + OnEngineException(e); + } + + /// + /// Event handler for the BuildEngine's OnEngineException event. + /// + private void OnEngineException(Exception e) + { + _shutdownException = e; + _shutdownReason = NodeEngineShutdownReason.Error; + _shutdownEvent.Set(); + } + + /// + /// Perform necessary actions to shut down the node. + /// + private NodeEngineShutdownReason HandleShutdown(out Exception exception) + { + if (_debugCommunications) + { + using (StreamWriter writer = File.CreateText(String.Format(CultureInfo.CurrentCulture, Path.Combine(Path.GetTempPath(), @"MSBuild_NodeShutdown_{0}.txt"), Process.GetCurrentProcess().Id))) + { + writer.WriteLine("Node shutting down with reason {0} and exception: {1}", _shutdownReason, _shutdownException); + } + } + + // Clean up the engine + if (null != _buildRequestEngine && _buildRequestEngine.Status != BuildRequestEngineStatus.Uninitialized) + { + _buildRequestEngine.CleanupForBuild(); + + if (_shutdownReason == NodeEngineShutdownReason.BuildCompleteReuse) + { + ((IBuildComponent)_buildRequestEngine).ShutdownComponent(); + } + } + + // Dispose of any build registered objects + IRegisteredTaskObjectCache objectCache = (IRegisteredTaskObjectCache)(_componentFactories.GetComponent(BuildComponentType.RegisteredTaskObjectCache)); + objectCache.DisposeCacheObjects(RegisteredTaskObjectLifetime.Build); + + if (_shutdownReason != NodeEngineShutdownReason.BuildCompleteReuse) + { + // Dispose of any node registered objects. + ((IBuildComponent)objectCache).ShutdownComponent(); + } + + // Shutdown any Out Of Proc Nodes Created + _taskHostNodeManager.ShutdownConnectedNodes(_shutdownReason == NodeEngineShutdownReason.BuildCompleteReuse); + + // Restore the original current directory. + NativeMethodsShared.SetCurrentDirectory(Environment.SystemDirectory); + + // Restore the original environment. + // If the node was never configured, this will be null. + if (_savedEnvironment != null) + { + foreach (KeyValuePair entry in CommunicationsUtilities.GetEnvironmentVariables()) + { + if (!_savedEnvironment.ContainsKey(entry.Key)) + { + Environment.SetEnvironmentVariable(entry.Key, null); + } + } + + foreach (KeyValuePair entry in _savedEnvironment) + { + Environment.SetEnvironmentVariable(entry.Key, entry.Value); + } + } + + try + { + // Shut down logging, which will cause all queued logging messages to be sent. + if (null != _loggingContext && null != _loggingService) + { + _loggingContext.LogBuildFinished(true); + ((IBuildComponent)_loggingService).ShutdownComponent(); + } + } + finally + { + // Shut down logging, which will cause all queued logging messages to be sent. + if (null != _loggingContext && null != _loggingService) + { + _loggingContext.LoggingService.OnLoggingThreadException -= new LoggingExceptionDelegate(OnLoggingThreadException); + _loggingContext = null; + } + + exception = _shutdownException; + + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + // Notify the BuildManager that we are done. + _nodeEndpoint.SendData(new NodeShutdown(_shutdownReason == NodeEngineShutdownReason.Error ? NodeShutdownReason.Error : NodeShutdownReason.Requested, exception)); + + // Flush all packets to the pipe and close it down. This blocks until the shutdown is complete. + _nodeEndpoint.OnLinkStatusChanged -= new LinkStatusChangedDelegate(OnLinkStatusChanged); + } + + _nodeEndpoint.Disconnect(); + CleanupCaches(); + } + + return _shutdownReason; + } + + /// + /// Clears all the caches used during the build. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect", Justification = "Required because when calling this method, we want the memory back NOW.")] + private void CleanupCaches() + { + IConfigCache configCache = _componentFactories.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + if (null != configCache) + { + configCache.ClearConfigurations(); + } + + IResultsCache resultsCache = _componentFactories.GetComponent(BuildComponentType.ResultsCache) as IResultsCache; + if (null != resultsCache) + { + resultsCache.ClearResults(); + } + + if (Environment.GetEnvironmentVariable("MSBUILDCLEARXMLCACHEONCHILDNODES") == "1") + { + // Optionally clear out the cache. This has the advantage of releasing memory, + // but the disadvantage of causing the next build to repeat the load and parse. + // We'll experiment here and ship with the best default. + s_projectRootElementCache = null; + } + + // Since we aren't going to be doing any more work, lets clean up all our memory usage. + GC.Collect(); + } + + /// + /// Event handler for the node endpoint's LinkStatusChanged event. + /// + private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + switch (status) + { + case LinkStatus.ConnectionFailed: + case LinkStatus.Failed: + _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; + _shutdownEvent.Set(); + break; + + case LinkStatus.Inactive: + break; + + default: + break; + } + } + + /// + /// Callback for logging packets to be sent. + /// + private void SendLoggingPacket(INodePacket packet) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(packet); + } + } + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + // Console.WriteLine("Handling packet {0} at {1}", packet.Type, DateTime.Now); + switch (packet.Type) + { + case NodePacketType.BuildRequest: + HandleBuildRequest(packet as BuildRequest); + break; + + case NodePacketType.BuildRequestConfiguration: + HandleBuildRequestConfiguration(packet as BuildRequestConfiguration); + break; + + case NodePacketType.BuildRequestConfigurationResponse: + HandleBuildRequestConfigurationResponse(packet as BuildRequestConfigurationResponse); + break; + + case NodePacketType.BuildRequestUnblocker: + HandleBuildRequestUnblocker(packet as BuildRequestUnblocker); + break; + + case NodePacketType.NodeConfiguration: + HandleNodeConfiguration(packet as NodeConfiguration); + break; + + case NodePacketType.NodeBuildComplete: + HandleNodeBuildComplete(packet as NodeBuildComplete); + break; + } + } + + /// + /// Handles the BuildRequest packet. + /// + private void HandleBuildRequest(BuildRequest request) + { + _buildRequestEngine.SubmitBuildRequest(request); + } + + /// + /// Handles the BuildRequestConfiguration packet. + /// + private void HandleBuildRequestConfiguration(BuildRequestConfiguration configuration) + { + _globalConfigCache.AddConfiguration(configuration); + } + + /// + /// Handles the BuildRequestConfigurationResponse packet. + /// + private void HandleBuildRequestConfigurationResponse(BuildRequestConfigurationResponse response) + { + _buildRequestEngine.ReportConfigurationResponse(response); + } + + /// + /// Handles the BuildResult packet. + /// + private void HandleBuildRequestUnblocker(BuildRequestUnblocker unblocker) + { + _buildRequestEngine.UnblockBuildRequest(unblocker); + } + + /// + /// Handles the NodeConfiguration packet. + /// + private void HandleNodeConfiguration(NodeConfiguration configuration) + { + // Grab the system parameters. + _buildParameters = configuration.BuildParameters; + + _buildParameters.ProjectRootElementCache = s_projectRootElementCache; + + // Snapshot the current environment + _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + + // Change to the startup directory + try + { + NativeMethodsShared.SetCurrentDirectory(BuildParameters.StartupDirectory); + } + catch (DirectoryNotFoundException) + { + // Somehow the startup directory vanished. This can happen if build was started from a USB Key and it was removed. + NativeMethodsShared.SetCurrentDirectory(Environment.SystemDirectory); + } + + // Replicate the environment. First, unset any environment variables set by the previous configuration. + if (_currentConfiguration != null) + { + foreach (string key in _currentConfiguration.BuildParameters.BuildProcessEnvironment.Keys) + { + Environment.SetEnvironmentVariable(key, null); + } + } + + // Now set the new environment + foreach (KeyValuePair environmentPair in _buildParameters.BuildProcessEnvironment) + { + Environment.SetEnvironmentVariable(environmentPair.Key, environmentPair.Value); + } + + // We want to make sure the global project collection has the toolsets which were defined on the parent + // so that any custom toolsets defined can be picked up by tasks who may use the global project collection but are + // executed on the child node. + ICollection parentToolSets = _buildParameters.ToolsetProvider.Toolsets; + if (parentToolSets != null) + { + ProjectCollection.GlobalProjectCollection.RemoveAllToolsets(); + + foreach (Toolset toolSet in parentToolSets) + { + ProjectCollection.GlobalProjectCollection.AddToolset(toolSet); + } + } + + // Set the culture. + Thread.CurrentThread.CurrentCulture = _buildParameters.Culture; + Thread.CurrentThread.CurrentUICulture = _buildParameters.UICulture; + + // Get the node ID. + _buildParameters.NodeId = configuration.NodeId; + _buildParameters.IsOutOfProc = true; + + // And the AppDomainSetup + _buildParameters.AppDomainSetup = configuration.AppDomainSetup; + + // Set up the logging service. + LoggingServiceFactory loggingServiceFactory = new LoggingServiceFactory(LoggerMode.Asynchronous, configuration.NodeId); + _componentFactories.ReplaceFactory(BuildComponentType.LoggingService, loggingServiceFactory.CreateInstance); + + _loggingService = _componentFactories.GetComponent(BuildComponentType.LoggingService) as ILoggingService; + + BuildEventArgTransportSink sink = new BuildEventArgTransportSink(SendLoggingPacket); + + _shutdownException = null; + + try + { + // If there are no node loggers to initialize dont do anything + if (configuration.LoggerDescriptions != null && configuration.LoggerDescriptions.Length > 0) + { + _loggingService.InitializeNodeLoggers(configuration.LoggerDescriptions, sink, configuration.NodeId); + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + OnEngineException(ex); + } + + _loggingService.OnLoggingThreadException += new LoggingExceptionDelegate(OnLoggingThreadException); + + string forwardPropertiesFromChild = Environment.GetEnvironmentVariable("MSBUILDFORWARDPROPERTIESFROMCHILD"); + string[] propertyListToSerialize = null; + + // Get a list of properties which should be serialized + if (!String.IsNullOrEmpty(forwardPropertiesFromChild)) + { + propertyListToSerialize = forwardPropertiesFromChild.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + } + + _loggingService.PropertiesToSerialize = propertyListToSerialize; + _loggingService.RunningOnRemoteNode = true; + + string forwardAllProperties = Environment.GetEnvironmentVariable("MSBUILDFORWARDALLPROPERTIESFROMCHILD"); + if (String.Equals(forwardAllProperties, "1", StringComparison.OrdinalIgnoreCase) || _buildParameters.LogInitialPropertiesAndItems) + { + _loggingService.SerializeAllProperties = true; + } + else + { + _loggingService.SerializeAllProperties = false; + } + + // Now prep the buildRequestEngine for the build. + _loggingContext = new NodeLoggingContext(_loggingService, configuration.NodeId, false /* inProcNode */); + + if (_shutdownException != null) + { + Exception exception; + HandleShutdown(out exception); + throw exception; + } + + _buildRequestEngine.InitializeForBuild(_loggingContext); + + // Finally store off this configuration packet. + _currentConfiguration = configuration; + } + + /// + /// Handles the NodeBuildComplete packet. + /// + private void HandleNodeBuildComplete(NodeBuildComplete buildComplete) + { + _shutdownReason = buildComplete.PrepareForReuse ? NodeEngineShutdownReason.BuildCompleteReuse : NodeEngineShutdownReason.BuildComplete; + _shutdownEvent.Set(); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/BuildAbortedException.cs b/src/XMakeBuildEngine/BackEnd/Shared/BuildAbortedException.cs new file mode 100644 index 00000000000..d140825860c --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/BuildAbortedException.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The exception which gets thrown if the build is aborted gracefully. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.Serialization; +using System.Security.Permissions; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Exceptions +{ + /// + /// An exception representing the case where the build was aborted by request, as opposed to being + /// unceremoniously shut down due to another kind of error exception. + /// + /// + /// This is public because it may be returned in the Exceptions collection of a BuildResult. + /// If you add fields to this class, add a custom serialization constructor and override GetObjectData(). + /// + [Serializable] + public class BuildAbortedException : Exception + { + /// + /// Constructs a standard BuildAbortedException. + /// + public BuildAbortedException() + : base(ResourceUtilities.FormatResourceString("BuildAborted")) + { + string errorCode; + string helpKeyword; + + ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "BuildAborted"); + + this.ErrorCode = errorCode; + } + + /// + /// Constructs a BuildAbortedException with an additional message attached. + /// + public BuildAbortedException(string message) + : base(ResourceUtilities.FormatResourceString("BuildAbortedWithMessage", message)) + { + string errorCode; + string helpKeyword; + + ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "BuildAbortedWithMessage", message); + + this.ErrorCode = errorCode; + } + + /// + /// Constructs a BuildAbortedException with an additional message attached and an inner exception. + /// + public BuildAbortedException(string message, Exception innerException) + : base(ResourceUtilities.FormatResourceString("BuildAbortedWithMessage", message), innerException) + { + string errorCode; + string helpKeyword; + + ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "BuildAbortedWithMessage", message); + + this.ErrorCode = errorCode; + } + + /// + /// Protected constructor used for (de)serialization. + /// If we ever add new members to this class, we'll need to update this. + /// + protected BuildAbortedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + this.ErrorCode = info.GetString("ErrorCode"); + } + + /// + /// Gets the error code (if any) associated with the exception message. + /// + /// Error code string, or null. + public string ErrorCode + { + get; + private set; + } + + /// + /// ISerializable method which we must override since Exception implements this interface + /// If we ever add new members to this class, we'll need to update this. + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + override public void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("ErrorCode", ErrorCode); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/BuildRequest.cs b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequest.cs new file mode 100644 index 00000000000..aa864ff8f3c --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequest.cs @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class containing data for a build request. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; + +using Constants = Microsoft.Build.Internal.Constants; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A build request contains information about the configuration used to build as well + /// as which targets need to be built. + /// + internal class BuildRequest : INodePacket + { + /// + /// The invalid global request id + /// + public const int InvalidGlobalRequestId = -1; + + /// + /// The invalid node request id + /// + public const int InvalidNodeRequestId = 0; + + /// + /// The results transfer request id + /// + public const int ResultsTransferNodeRequestId = -1; + + /// + /// The submission with which this request is associated. + /// + private int _submissionId; + + /// + /// The configuration id. + /// + private int _configurationId; + + /// + /// The global build request id, assigned by the Build Manager + /// + private int _globalRequestId; + + /// + /// The global request id of the request which spawned this one. + /// + private int _parentGlobalRequestId; + + /// + /// The build request id assigned by the node originating this request. + /// + private int _nodeRequestId; + + /// + /// The targets specified when the request was made. Doesn't include default or initial targets. + /// + private List _targets; + + /// + /// The route for host-aware tasks back to the host + /// + private HostServices _hostServices; + + /// + /// The build event context of the parent + /// + private BuildEventContext _parentBuildEventContext; + + /// + /// The build event context of this request + /// + private BuildEventContext _buildEventContext; + + /// + /// Whether or not the issued in response to this request should include . + /// + private BuildRequestDataFlags _buildRequestDataFlags; + + /// + /// Constructor for serialization. + /// + public BuildRequest() + { + } + + /// + /// Initializes a build request with a parent context. + /// + /// The id of the build submission. + /// The id of the node issuing the request + /// The configuration id to use. + /// The targets to be built + /// Host services if any. May be null. + /// The build event context of the parent project. + /// The parent build request, if any. + /// Additional flags for the request. + public BuildRequest( + int submissionId, + int nodeRequestId, + int configurationId, + ICollection escapedTargets, + HostServices hostServices, + BuildEventContext parentBuildEventContext, + BuildRequest parentRequest, + BuildRequestDataFlags buildRequestDataFlags = BuildRequestDataFlags.None) + { + ErrorUtilities.VerifyThrowArgumentNull(escapedTargets, "targets"); + ErrorUtilities.VerifyThrowArgumentNull(parentBuildEventContext, "parentBuildEventContext"); + + _submissionId = submissionId; + _configurationId = configurationId; + + // When targets come into a build request, we unescape them. + _targets = new List(escapedTargets.Count); + foreach (string target in escapedTargets) + { + _targets.Add(EscapingUtilities.UnescapeAll(target)); + } + + _hostServices = hostServices; + _buildEventContext = BuildEventContext.Invalid; + _parentBuildEventContext = parentBuildEventContext; + _globalRequestId = InvalidGlobalRequestId; + if (null != parentRequest) + { + _parentGlobalRequestId = parentRequest.GlobalRequestId; + } + else + { + _parentGlobalRequestId = InvalidGlobalRequestId; + } + + _nodeRequestId = nodeRequestId; + _buildRequestDataFlags = buildRequestDataFlags; + } + + /// + /// Private constructor for deserialization + /// + private BuildRequest(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Returns true if the configuration has been resolved, false otherwise. + /// + public bool IsConfigurationResolved + { + [DebuggerStepThrough] + get + { return _configurationId > 0; } + } + + /// + /// Returns the submission id + /// + public int SubmissionId + { + [DebuggerStepThrough] + get + { return _submissionId; } + } + + /// + /// Returns the configuration id + /// + public int ConfigurationId + { + [DebuggerStepThrough] + get + { return _configurationId; } + } + + /// + /// Gets the global request id + /// + public int GlobalRequestId + { + [DebuggerStepThrough] + get + { + return _globalRequestId; + } + + set + { + ErrorUtilities.VerifyThrow(_globalRequestId == InvalidGlobalRequestId, "Global Request ID cannot be set twice."); + _globalRequestId = value; + } + } + + /// + /// Gets the global request id of the parent request. + /// + public int ParentGlobalRequestId + { + [DebuggerStepThrough] + get + { return _parentGlobalRequestId; } + } + + /// + /// Gets the node request id + /// + public int NodeRequestId + { + [DebuggerStepThrough] + get + { return _nodeRequestId; } + + [DebuggerStepThrough] + set + { _nodeRequestId = value; } + } + + /// + /// Returns the set of unescaped targets to be built + /// + public List Targets + { + [DebuggerStepThrough] + get + { return _targets; } + } + + /// + /// Returns the type of packet. + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.BuildRequest; } + } + + /// + /// Returns the build event context of the parent, if any. + /// + public BuildEventContext ParentBuildEventContext + { + [DebuggerStepThrough] + get + { return _parentBuildEventContext; } + } + + /// + /// Returns the build event context for this request, if any. + /// + public BuildEventContext BuildEventContext + { + [DebuggerStepThrough] + get + { + return _buildEventContext; + } + + set + { + ErrorUtilities.VerifyThrow(_buildEventContext == BuildEventContext.Invalid, "The build event context is already set."); + _buildEventContext = value; + } + } + + /// + /// The set of flags specified in the BuildRequestData for this request. + /// + public BuildRequestDataFlags BuildRequestDataFlags + { + get { return _buildRequestDataFlags; } + set { _buildRequestDataFlags = value; } + } + + /// + /// The route for host-aware tasks back to the host + /// + internal HostServices HostServices + { + [DebuggerStepThrough] + get + { return _hostServices; } + } + + /// + /// Returns true if this is a root request (one which has no parent.) + /// + internal bool IsRootRequest + { + [DebuggerStepThrough] + get + { return _parentGlobalRequestId == InvalidGlobalRequestId; } + } + + /// + /// Sets the configuration id to a resolved id. + /// + /// The new configuration id for this request. + public void ResolveConfiguration(int newConfigId) + { + ErrorUtilities.VerifyThrow(!IsConfigurationResolved, "Configuration already resolved"); + _configurationId = newConfigId; + ErrorUtilities.VerifyThrow(IsConfigurationResolved, "Configuration not resolved"); + } + + #region INodePacket Members + + /// + /// Reads/writes this packet + /// + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _submissionId); + translator.Translate(ref _configurationId); + translator.Translate(ref _globalRequestId); + translator.Translate(ref _parentGlobalRequestId); + translator.Translate(ref _nodeRequestId); + translator.Translate(ref _targets); + translator.Translate(ref _parentBuildEventContext); + translator.Translate(ref _buildEventContext); + translator.TranslateEnum(ref _buildRequestDataFlags, (int)_buildRequestDataFlags); + + // UNDONE: (Compat) Serialize the host object. + } + + /// + /// Factory for serialization. + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildRequest(translator); + } + + #endregion + /// + /// Returns true if the result applies to this request. + /// + internal bool DoesResultApplyToRequest(BuildResult result) + { + return _globalRequestId == result.GlobalRequestId && _nodeRequestId == result.NodeRequestId; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestBlocker.cs b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestBlocker.cs new file mode 100644 index 00000000000..0f3c057722a --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestBlocker.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class describing what is blocking a build request from continuing. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Indicates what the action is for requests which are yielding. + /// + internal enum YieldAction : byte + { + /// + /// The request is yielding its control of the node. + /// + Yield, + + /// + /// The request is ready to reacquire control of the node. + /// + Reacquire, + + /// + /// There is no yield action + /// + None, + } + + /// + /// This class is used to inform the Scheduler that a request on a node is being blocked from further progress. There + /// are two cases for this: + /// 1) The request may be blocked waiting for a target to complete in the same project but which is assigned to + /// another request. + /// 2) The request may be blocked because it has child requests which need to be satisfied to proceed. + /// + internal class BuildRequestBlocker : INodePacketTranslatable, INodePacket + { + /// + /// The yield action, if any. + /// + private YieldAction _yieldAction; + + /// + /// The global request id of the request which is being blocked from continuing. + /// + private int _blockedGlobalRequestId; + + /// + /// The set of targets which are currently in progress for the blocked global request ID. + /// + private string[] _targetsInProgress; + + /// + /// The request on which we are blocked, if any. + /// + private int _blockingGlobalRequestId; + + /// + /// The name of the blocking target, if any. + /// + private string _blockingTarget; + + /// + /// The requests which need to be built to unblock the request, if any. + /// + private BuildRequest[] _buildRequests; + + /// + /// Constructor for deserialization. + /// + internal BuildRequestBlocker(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Constructor for the blocker where we are blocked waiting for a target. + /// + internal BuildRequestBlocker(int blockedGlobalRequestId, string[] targetsInProgress, int blockingGlobalRequestId, string blockingTarget) + : this(blockedGlobalRequestId, targetsInProgress) + { + _blockingGlobalRequestId = blockingGlobalRequestId; + _blockingTarget = blockingTarget; + } + + /// + /// Constructor for the blocker where we are blocked waiting for requests to be satisfied. + /// + internal BuildRequestBlocker(int blockedGlobalRequestId, string[] targetsInProgress, BuildRequest[] buildRequests) + : this(blockedGlobalRequestId, targetsInProgress) + { + _buildRequests = buildRequests; + } + + /// + /// Constructor for a blocker used by yielding requests. + /// + internal BuildRequestBlocker(int blockedGlobalRequestId, string[] targetsInProgress, YieldAction action) + : this(blockedGlobalRequestId, targetsInProgress) + { + _yieldAction = action; + _blockingGlobalRequestId = blockedGlobalRequestId; + } + + /// + /// Constructor for a blocker used by results-transfer requests + /// + /// The request needing results transferred + internal BuildRequestBlocker(int blockedGlobalRequestId) + { + _blockedGlobalRequestId = blockedGlobalRequestId; + _blockingGlobalRequestId = blockedGlobalRequestId; + _targetsInProgress = null; + _yieldAction = YieldAction.None; + } + + /// + /// Constructor for common values. + /// + private BuildRequestBlocker(int blockedGlobalRequestId, string[] targetsInProgress) + { + _blockedGlobalRequestId = blockedGlobalRequestId; + _blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId; + _targetsInProgress = targetsInProgress; + _yieldAction = YieldAction.None; + } + + /// + /// Returns the type of packet. + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.BuildRequestBlocker; } + } + + /// + /// Accessor for the blocked request id. + /// + public int BlockedRequestId + { + [DebuggerStepThrough] + get + { + return _blockedGlobalRequestId; + } + } + + /// + /// Accessor for the set of targets currently in progress. + /// + public string[] TargetsInProgress + { + [DebuggerStepThrough] + get + { + return _targetsInProgress; + } + } + + /// + /// Accessor for the blocking request id, if any. + /// + public int BlockingRequestId + { + [DebuggerStepThrough] + get + { + return _blockingGlobalRequestId; + } + } + + /// + /// Accessor for the blocking request id, if any. + /// + public string BlockingTarget + { + [DebuggerStepThrough] + get + { + return _blockingTarget; + } + } + + /// + /// Accessor for the blocking build requests, if any. + /// + public BuildRequest[] BuildRequests + { + [DebuggerStepThrough] + get + { + return _buildRequests; + } + } + + /// + /// Accessor for the yield action. + /// + public YieldAction YieldAction + { + [DebuggerStepThrough] + get + { + return _yieldAction; + } + } + + #region INodePacketTranslatable Members + + /// + /// Serialization method. + /// + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _blockedGlobalRequestId); + translator.Translate(ref _targetsInProgress); + translator.Translate(ref _blockingGlobalRequestId); + translator.Translate(ref _blockingTarget); + translator.TranslateEnum(ref _yieldAction, (int)_yieldAction); + translator.TranslateArray(ref _buildRequests); + } + + #endregion + + /// + /// Factory for serialization. + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildRequestBlocker(translator); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestConfiguration.cs b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestConfiguration.cs new file mode 100644 index 00000000000..37f30c1dcbb --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestConfiguration.cs @@ -0,0 +1,1012 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A configuration for a build request. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Microsoft.Build.Construction; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using System.Diagnostics; +using System.IO; +using System.Xml; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A build request configuration represents all of the data necessary to know which project to build + /// and the environment in which it should be built. + /// + internal class BuildRequestConfiguration : IEquatable, + INodePacket + { + /// + /// The invalid configuration id + /// + public const int InvalidConfigurationId = 0; + + #region Static State + + /// + /// This is the ID of the configuration as set by the generator of the configuration. When + /// a node generates a configuration, this is set to a negative number. The Build Manager will + /// generate positive IDs + /// + private int _configId; + + /// + /// The full path to the project to build. + /// + private string _projectFullPath; + + /// + /// The tools version specified for the configuration. + /// Always specified. + /// May have originated from a /tv switch, or an MSBuild task, + /// or a Project tag, or the default. + /// + private string _toolsVersion; + + /// + /// Whether the tools version was set by the /tv switch or passed in through an msbuild callback + /// directly or indirectly. + /// + private bool _explicitToolsVersionSpecified; + + /// + /// The set of global properties which should be used when building this project. + /// + private PropertyDictionary _globalProperties; + + /// + /// Flag indicating if the project in this configuration is a traversal + /// + private bool? _isTraversalProject; + + /// + /// Synchronization object. Currently this just prevents us from caching and uncaching at the + /// same time, causing a race condition. This class is not made 100% threadsafe by the presence + /// and current usage of this lock. + /// + private Object _syncLock = new Object(); + + #endregion + + #region Build State + + /// + /// The project object, representing the project to be built. + /// + private ProjectInstance _project; + + /// + /// The state of a project instance which has been transferred from one node to another. + /// + private ProjectInstance _transferredState; + + /// + /// The project instance properties we should transfer. + /// + private List _transferredProperties; + + /// + /// The initial targets for the project + /// + private List _projectInitialTargets; + + /// + /// The default targets for the project + /// + private List _projectDefaultTargets; + + /// + /// This is the lookup representing the current project items and properties 'state'. + /// + private Lookup _baseLookup; + + /// + /// This is the set of targets which are currently building but which have not yet completed. + /// { targetName -> globalRequestId } + /// + private Dictionary _activelyBuildingTargets; + + /// + /// The node where this configuration's master results are stored. + /// + private int _resultsNodeId = Scheduler.InvalidNodeId; + + /// + /// Holds a snapshot of the environment at the time we blocked. + /// + private Dictionary _savedEnvironmentVariables; + + /// + /// Holds a snapshot of the current working directory at the time we blocked. + /// + private string _savedCurrentDirectory; + + #endregion + + /// + /// Initializes a configuration from a BuildRequestData structure. Used by the BuildManager. + /// Figures out the correct tools version to use, falling back to the provided default if necessary. + /// May throw InvalidProjectFileException. + /// + /// The data containing the configuration information. + /// The default ToolsVersion to use as a fallback + /// Callback used to get a Toolset based on a ToolsVersion + internal BuildRequestConfiguration(BuildRequestData data, string defaultToolsVersion, Utilities.GetToolset getToolset = null) + : this(0, data, defaultToolsVersion, getToolset) + { + } + + /// + /// Initializes a configuration from a BuildRequestData structure. Used by the BuildManager. + /// Figures out the correct tools version to use, falling back to the provided default if necessary. + /// May throw InvalidProjectFileException. + /// + /// The configuration ID to assign to this new configuration. + /// The data containing the configuration information. + /// The default ToolsVersion to use as a fallback + /// Callback used to get a Toolset based on a ToolsVersion + internal BuildRequestConfiguration(int configId, BuildRequestData data, string defaultToolsVersion, Utilities.GetToolset getToolset = null) + { + ErrorUtilities.VerifyThrowArgumentNull(data, "data"); + ErrorUtilities.VerifyThrowInternalLength(data.ProjectFullPath, "data.ProjectFullPath"); + + _configId = configId; + _projectFullPath = data.ProjectFullPath; + _explicitToolsVersionSpecified = data.ExplicitToolsVersionSpecified; + _toolsVersion = ResolveToolsVersion(data, defaultToolsVersion, getToolset); + _globalProperties = data.GlobalPropertiesDictionary; + + // The following information only exists when the request is populated with an existing project. + if (data.ProjectInstance != null) + { + _project = data.ProjectInstance; + _projectInitialTargets = data.ProjectInstance.InitialTargets; + _projectDefaultTargets = data.ProjectInstance.DefaultTargets; + if (data.PropertiesToTransfer != null) + { + _transferredProperties = new List(); + foreach (var name in data.PropertiesToTransfer) + { + _transferredProperties.Add(data.ProjectInstance.GetProperty(name)); + } + } + + this.IsCacheable = false; + } + else + { + this.IsCacheable = true; + } + } + + /// + /// Creates a new BuildRequestConfiguration based on an existing project instance. + /// Used by the BuildManager to populate configurations from a solution. + /// + /// The configuration id + /// The project instance. + internal BuildRequestConfiguration(int configId, ProjectInstance instance) + { + ErrorUtilities.VerifyThrowArgumentNull(instance, "instance"); + + _configId = configId; + _projectFullPath = instance.FullPath; + _explicitToolsVersionSpecified = instance.ExplicitToolsVersionSpecified; + _toolsVersion = instance.ToolsVersion; + _globalProperties = instance.GlobalPropertiesDictionary; + + _project = instance; + _projectInitialTargets = instance.InitialTargets; + _projectDefaultTargets = instance.DefaultTargets; + this.IsCacheable = false; + } + + /// + /// Creates a new configuration which is a clone of the old one but with a new id. + /// + private BuildRequestConfiguration(int configId, BuildRequestConfiguration other) + { + ErrorUtilities.VerifyThrow(configId != 0, "Configuration ID must not be zero when using this constructor."); + ErrorUtilities.VerifyThrowArgumentNull(other, "other"); + ErrorUtilities.VerifyThrow(other._transferredState == null, "Unexpected transferred state still set on other configuration."); + + _project = other._project; + _transferredProperties = other._transferredProperties; + _projectDefaultTargets = other._projectDefaultTargets; + _projectInitialTargets = other._projectInitialTargets; + _projectFullPath = other._projectFullPath; + _toolsVersion = other._toolsVersion; + _explicitToolsVersionSpecified = other._explicitToolsVersionSpecified; + _globalProperties = other._globalProperties; + this.IsCacheable = other.IsCacheable; + _configId = configId; + } + + /// + /// Private constructor for deserialization + /// + private BuildRequestConfiguration(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Flag indicating whether the configuration is allowed to cache. This does not mean that the configuration will + /// actually cache - there are several criteria which must for that. + /// + public bool IsCacheable + { + get; + set; + } + + /// + /// When reset caches is false we need to only keep around the configurations which are being asked for during the design time build. + /// Other configurations need to be cleared. If this configuration is marked as ExplicitlyLoadedConfiguration then it should not be cleared when + /// Reset Caches is false. + /// + public bool ExplicitlyLoaded + { + get; + set; + } + + /// + /// Flag indicating whether or not the configuration is actually building. + /// + public bool IsActivelyBuilding + { + get + { + return (_activelyBuildingTargets != null) && (_activelyBuildingTargets.Count > 0); + } + } + + /// + /// Flag indicating whether or not the configuration has been loaded before. + /// + public bool IsLoaded + { + get { return _project != null; } + } + + /// + /// Flag indicating if the configuration is cached or not. + /// + public bool IsCached + { + get; + private set; + } + + /// + /// Flag indicating if this configuration represents a traversal project. Traversal projects + /// are projects which typically do little or no work themselves, but have references to other + /// projects (and thus are used to find more work.) The scheduler can treat these differently + /// in order to fill its work queue with other options for scheduling. + /// + public bool IsTraversal + { + get + { + if (!_isTraversalProject.HasValue) + { + if (String.Equals(Path.GetFileName(ProjectFullPath), "dirs.proj", StringComparison.OrdinalIgnoreCase)) + { + // dirs.proj are assumed to be traversals + _isTraversalProject = true; + } + else if (FileUtilities.IsMetaprojectFilename(ProjectFullPath)) + { + // Metaprojects generated by the SolutionProjectGenerator are traversals. They have no + // on-disk representation - they are ProjectInstances which exist only in memory. + _isTraversalProject = true; + } + else if (FileUtilities.IsSolutionFilename(ProjectFullPath)) + { + // Solution files are considered to be traversals. + _isTraversalProject = true; + } + else + { + _isTraversalProject = false; + } + } + + return _isTraversalProject.Value; + } + } + + /// + /// Returns true if this configuration was generated on a node and has not yet been resolved. + /// + public bool WasGeneratedByNode + { + [DebuggerStepThrough] + get + { return _configId < 0; } + } + + /// + /// Sets or returns the configuration id + /// + public int ConfigurationId + { + [DebuggerStepThrough] + get + { + return _configId; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow((_configId == 0) || (WasGeneratedByNode && (value > 0)), "Configuration ID must be zero, or it must be less than zero and the new config must be greater than zero. It was {0}, the new value was {1}.", _configId, value); + _configId = value; + } + } + + /// + /// Returns the filename of the project to build. + /// + public string ProjectFullPath + { + [DebuggerStepThrough] + get + { return _projectFullPath; } + } + + /// + /// The tools version specified for the configuration. + /// Always specified. + /// May have originated from a /tv switch, or an MSBuild task, + /// or a Project tag, or the default. + /// + public string ToolsVersion + { + [DebuggerStepThrough] + get + { return _toolsVersion; } + } + + /// + /// Returns the global properties to use to build this project. + /// + public PropertyDictionary Properties + { + [DebuggerStepThrough] + get + { return _globalProperties; } + } + + /// + /// Sets or returns the project to build. + /// + public ProjectInstance Project + { + [DebuggerStepThrough] + get + { + ErrorUtilities.VerifyThrow(!IsCached, "We shouldn't be accessing the ProjectInstance when the configuration is cached."); + return _project; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow(value != null, "Cannot set null project."); + _project = value; + _baseLookup = null; + + // Clear these out so the other accessors don't complain. We don't want to generally enable resetting these fields. + _projectDefaultTargets = null; + _projectInitialTargets = null; + ProjectDefaultTargets = _project.DefaultTargets; + ProjectInitialTargets = _project.InitialTargets; + + if (IsCached) + { + ClearCacheFile(); + IsCached = false; + } + + // If we have transferred the state of a project previously, then we need to assume its items and properties. + if (_transferredState != null) + { + ErrorUtilities.VerifyThrow(_transferredProperties == null, "Shouldn't be transferring entire state of ProjectInstance when transferredProperties is not null."); + _project.UpdateStateFrom(_transferredState); + _transferredState = null; + } + + // If we have just requested a limited transfer of properties, do that. + if (_transferredProperties != null) + { + foreach (var property in _transferredProperties) + { + _project.SetProperty(property.Name, ((IProperty)property).EvaluatedValueEscaped); + } + + _transferredProperties = null; + } + } + } + + /// + /// Returns true if the default and initial targets have been resolved. + /// + public bool HasTargetsResolved + { + get { return ProjectInitialTargets != null && ProjectDefaultTargets != null; } + } + + /// + /// Gets the initial targets for the project + /// + public List ProjectInitialTargets + { + [DebuggerStepThrough] + get + { + return _projectInitialTargets; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow(_projectInitialTargets == null, "Initial targets cannot be reset once they have been set."); + _projectInitialTargets = value; + } + } + + /// + /// Gets the default targets for the project + /// + public List ProjectDefaultTargets + { + [DebuggerStepThrough] + get + { + return _projectDefaultTargets; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow(_projectDefaultTargets == null, "Default targets cannot be reset once they have been set."); + _projectDefaultTargets = value; + } + } + + /// + /// Returns the node packet type + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.BuildRequestConfiguration; } + } + + /// + /// Returns the lookup which collects all items and properties during the run of this project. + /// + public Lookup BaseLookup + { + get + { + ErrorUtilities.VerifyThrow(!IsCached, "Configuration is cached, we shouldn't be accessing the lookup."); + + if (null == _baseLookup) + { + _baseLookup = new Lookup(Project.ItemsToBuildWith, Project.PropertiesToBuildWith, Project.InitialGlobalsForDebugging); + } + + return _baseLookup; + } + } + + /// + /// Retrieves the set of targets currently building, mapped to the request id building them. + /// + public Dictionary ActivelyBuildingTargets + { + get + { + if (null == _activelyBuildingTargets) + { + _activelyBuildingTargets = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + return _activelyBuildingTargets; + } + } + + /// + /// Holds a snapshot of the environment at the time we blocked. + /// + public Dictionary SavedEnvironmentVariables + { + get + { + return _savedEnvironmentVariables; + } + + set + { + _savedEnvironmentVariables = value; + } + } + + /// + /// Holds a snapshot of the current working directory at the time we blocked. + /// + public string SavedCurrentDirectory + { + get + { + return _savedCurrentDirectory; + } + + set + { + _savedCurrentDirectory = value; + } + } + + /// + /// Whether the tools version was set by the /tv switch or passed in through an msbuild callback + /// directly or indirectly. + /// + public bool ExplicitToolsVersionSpecified + { + [DebuggerStepThrough] + get + { return _explicitToolsVersionSpecified; } + } + + /// + /// Gets or sets the node on which this configuration's results are stored. + /// + internal int ResultsNodeId + { + get + { + return _resultsNodeId; + } + + set + { + _resultsNodeId = value; + } + } + + /// + /// Implementation of the equality operator. + /// + /// The left hand argument + /// The right hand argument + /// True if the objects are equivalent, false otherwise. + public static bool operator ==(BuildRequestConfiguration left, BuildRequestConfiguration right) + { + if (Object.ReferenceEquals(left, null)) + { + if (Object.ReferenceEquals(right, null)) + { + return true; + } + else + { + return false; + } + } + else + { + if (Object.ReferenceEquals(right, null)) + { + return false; + } + else + { + return left.InternalEquals(right); + } + } + } + + /// + /// Implementation of the inequality operator. + /// + /// The left-hand argument + /// The right-hand argument + /// True if the objects are not equivalent, false otherwise. + public static bool operator !=(BuildRequestConfiguration left, BuildRequestConfiguration right) + { + return !(left == right); + } + + /// + /// Requests that the configuration be cached to disk. + /// + public void CacheIfPossible() + { + lock (_syncLock) + { + if (IsActivelyBuilding || IsCached || !IsLoaded || !IsCacheable) + { + return; + } + + lock (_project) + { + if (IsCacheable) + { + INodePacketTranslator translator = GetConfigurationTranslator(TranslationDirection.WriteToStream); + + try + { + _project.Cache(translator); + _baseLookup = null; + + IsCached = true; + } + finally + { + translator.Writer.BaseStream.Close(); + } + } + } + } + } + + /// + /// Retrieves the configuration data from the cache. + /// + public void RetrieveFromCache() + { + lock (_syncLock) + { + if (!IsLoaded) + { + return; + } + + if (!IsCached) + { + return; + } + + INodePacketTranslator translator = GetConfigurationTranslator(TranslationDirection.ReadFromStream); + try + { + _project.RetrieveFromCache(translator); + + IsCached = false; + } + finally + { + translator.Reader.BaseStream.Close(); + } + } + } + + /// + /// Gets the list of targets which are used to build the specified request, including all initial and applicable default targets + /// + /// The request + /// An array of t + public List GetTargetsUsedToBuildRequest(BuildRequest request) + { + ErrorUtilities.VerifyThrow(request.ConfigurationId == ConfigurationId, "Request does not match configuration."); + ErrorUtilities.VerifyThrow(_projectInitialTargets != null, "Initial targets have not been set."); + ErrorUtilities.VerifyThrow(_projectDefaultTargets != null, "Default targets have not been set."); + + List initialTargets = _projectInitialTargets; + List nonInitialTargets = (request.Targets.Count == 0) ? _projectDefaultTargets : request.Targets; + + List allTargets = new List(initialTargets.Count + nonInitialTargets.Count); + + allTargets.AddRange(initialTargets); + allTargets.AddRange(nonInitialTargets); + + return allTargets; + } + + /// + /// Returns the list of targets that are AfterTargets (or AfterTargets of the AfterTargets) + /// of the entrypoint targets. + /// + public List GetAfterTargetsForDefaultTargets(BuildRequest request) + { + // We may not have a project available. In which case, return nothing -- we simply don't have + // enough information to figure out what the correct answer is. + if (this.Project != null) + { + HashSet afterTargetsFound = new HashSet(); + + Queue targetsToCheckForAfterTargets = new Queue((request.Targets.Count == 0) ? this.ProjectDefaultTargets : request.Targets); + + while (targetsToCheckForAfterTargets.Count > 0) + { + string targetToCheck = targetsToCheckForAfterTargets.Dequeue(); + + IList targetsWhichRunAfter = this.Project.GetTargetsWhichRunAfter(targetToCheck); + + foreach (TargetSpecification targetWhichRunsAfter in targetsWhichRunAfter) + { + if (afterTargetsFound.Add(targetWhichRunsAfter.TargetName)) + { + // If it's already in there, we've already looked into it so no need to do so again. Otherwise, add it + // to the list to check. + targetsToCheckForAfterTargets.Enqueue(targetWhichRunsAfter.TargetName); + } + } + } + + return new List(afterTargetsFound); + } + + return null; + } + + /// + /// This override is used to provide a hash code for storage in dictionaries and the like. + /// + /// + /// If two objects are Equal, they must have the same hash code, for dictionaries to work correctly. + /// Two configurations are Equal if their global properties are equivalent, not necessary reference equals. + /// So only include filename and tools version in the hashcode. + /// + /// A hash code + public override int GetHashCode() + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(_projectFullPath) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_toolsVersion); + } + + /// + /// Returns a string representation of the object + /// + /// String representation of the object + public override string ToString() + { + return String.Format(CultureInfo.CurrentCulture, "{0} {1} {2} {3}", _configId, _projectFullPath, _toolsVersion, _globalProperties); + } + + /// + /// Determines object equality + /// + /// The object to compare with + /// True if they contain the same data, false otherwise + public override bool Equals(object obj) + { + if (Object.ReferenceEquals(obj, null)) + { + return false; + } + + if (this.GetType() != obj.GetType()) + { + return false; + } + + return InternalEquals((BuildRequestConfiguration)obj); + } + + #region IEquatable Members + + /// + /// Equality of the configuration is the product of the equality of its members. + /// + /// The other configuration to which we will compare ourselves. + /// True if equal, false otherwise. + public bool Equals(BuildRequestConfiguration other) + { + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + return InternalEquals(other); + } + + #endregion + + #region INodePacket Members + + /// + /// Reads or writes the packet to the serializer. + /// + public void Translate(INodePacketTranslator translator) + { + if (translator.Mode == TranslationDirection.WriteToStream && _transferredProperties == null) + { + // When writing, we will transfer the state of any loaded project instance if we aren't transferring a limited subset. + _transferredState = _project; + } + + translator.Translate(ref _configId); + translator.Translate(ref _projectFullPath); + translator.Translate(ref _transferredState, ProjectInstance.FactoryForDeserialization); + translator.Translate(ref _transferredProperties, ProjectPropertyInstance.FactoryForDeserialization); + translator.Translate(ref _resultsNodeId); + translator.Translate(ref _toolsVersion); + translator.Translate(ref _explicitToolsVersionSpecified); + translator.TranslateDictionary, ProjectPropertyInstance>(ref _globalProperties, ProjectPropertyInstance.FactoryForDeserialization); + translator.Translate(ref _savedCurrentDirectory); + translator.TranslateDictionary(ref _savedEnvironmentVariables, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Factory for serialization. + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildRequestConfiguration(translator); + } + + #endregion + + /// + /// Applies the state from the specified instance to the loaded instance. This overwrites the items and properties. + /// + /// + /// Used when we transfer results and state from a previous node to the current one. + /// + internal void ApplyTransferredState(ProjectInstance instance) + { + if (instance != null) + { + _project.UpdateStateFrom(instance); + } + } + + /// + /// Gets the name of the cache file for this configuration. + /// + internal string GetCacheFile() + { + string filename = Path.Combine(FileUtilities.GetCacheDirectory(), String.Format(CultureInfo.InvariantCulture, "Configuration{0}.cache", _configId)); + return filename; + } + + /// + /// Deletes the cache file + /// + internal void ClearCacheFile() + { + string cacheFile = GetCacheFile(); + if (File.Exists(cacheFile)) + { + FileUtilities.DeleteNoThrow(cacheFile); + } + } + + /// + /// Clones this BuildRequestConfiguration but sets a new configuration id. + /// + internal BuildRequestConfiguration ShallowCloneWithNewId(int newId) + { + return new BuildRequestConfiguration(newId, this); + } + + /// + /// Compares this object with another for equality + /// + /// The object with which to compare this one. + /// True if the objects contain the same data, false otherwise. + private bool InternalEquals(BuildRequestConfiguration other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if ((other.WasGeneratedByNode == WasGeneratedByNode) && + (other._configId != 0) && + (_configId != 0)) + { + return _configId == other._configId; + } + else + { + return _projectFullPath.Equals(other._projectFullPath, StringComparison.OrdinalIgnoreCase) && + _toolsVersion.Equals(other._toolsVersion, StringComparison.OrdinalIgnoreCase) && + _globalProperties.Equals(other._globalProperties); + } + } + + /// + /// Determines what the real tools version is. + /// + private string ResolveToolsVersion(BuildRequestData data, string defaultToolsVersion, Utilities.GetToolset getToolset) + { + if (data.ExplicitToolsVersionSpecified) + { + return data.ExplicitlySpecifiedToolsVersion; + } + + // None was specified by the call, fall back to the project's ToolsVersion attribute + if (data.ProjectInstance != null) + { + return data.ProjectInstance.Toolset.ToolsVersion; + } + else if (FileUtilities.IsVCProjFilename(data.ProjectFullPath)) + { + ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(data.ProjectFullPath), "ProjectUpgradeNeededToVcxProj", data.ProjectFullPath); + } + else if (!FileUtilities.IsSolutionFilename(data.ProjectFullPath)) + { + // If the file does not exist, it's a failure of the host, possibly msbuild.exe; so it's an ArgumentException here + ErrorUtilities.VerifyThrowArgument(File.Exists(data.ProjectFullPath), "ProjectFileNotFound", data.ProjectFullPath); + + string toolsVersionFromFile = null; + + // We use an XmlTextReader to sniff, rather than simply loading a ProjectRootElement into the cache, because + // quite likely this won't be the node on which the request will be built, so we'd be loading the ProjectRootElement + // on this node unnecessarily. + toolsVersionFromFile = XmlUtilities.SniffAttributeValueFromXmlFile(ProjectFullPath, XMakeAttributes.project, XMakeAttributes.toolsVersion); + + // Instead of just using the ToolsVersion from the file, though, ask our "source of truth" what the ToolsVersion + // we should use is. This takes into account the various environment variables that can affect ToolsVersion, etc., + // to make it more likely that the ToolsVersion we come up with is going to be the one actually being used by the + // project at build time. + string toolsVersionToUse = Utilities.GenerateToolsVersionToUse + ( + data.ExplicitlySpecifiedToolsVersion, + toolsVersionFromFile, + getToolset, + defaultToolsVersion + ); + + return toolsVersionToUse; + } + + // Couldn't find out the right ToolsVersion any other way, so just return the default. + return defaultToolsVersion; + } + + /// + /// Gets the translator for this configuration. + /// + private INodePacketTranslator GetConfigurationTranslator(TranslationDirection direction) + { + string cacheFile = GetCacheFile(); + try + { + if (direction == TranslationDirection.WriteToStream) + { + Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + return NodePacketTranslator.GetWriteTranslator(File.Create(cacheFile)); + } + else + { + // Not using sharedReadBuffer because this is not a memory stream and so the buffer won't be used anyway. + return NodePacketTranslator.GetReadTranslator(File.OpenRead(cacheFile), null); + } + } + catch (Exception e) + { + if (e is DirectoryNotFoundException || e is UnauthorizedAccessException) + { + ErrorUtilities.ThrowInvalidOperation("CacheFileInaccessible", cacheFile, e); + } + + // UNREACHABLE + throw; + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestUnblocker.cs b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestUnblocker.cs new file mode 100644 index 00000000000..70c744bfbea --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/BuildRequestUnblocker.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class used to unblock a blocked build request. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Shared; + +using BuildResult = Microsoft.Build.Execution.BuildResult; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This class is used by the Scheduler to unblock a blocked build request on the BuildRequestEngine. + /// There are two cases: + /// 1. The request was blocked waiting on a target in the same project. In this case this class will contain + /// no information other than the request id. + /// 2. The request was blocked on some set of build requests. This class will then contain the build results + /// needed to satisfy those requests. + /// + internal class BuildRequestUnblocker : INodePacketTranslatable, INodePacket + { + /// + /// The node request id of the request which is blocked and now will either result or have results reported. + /// + private int _blockedGlobalRequestId = BuildRequest.InvalidGlobalRequestId; + + /// + /// The build result which we wish to report. + /// + private BuildResult _buildResult; + + /// + /// Constructor for deserialization. + /// + internal BuildRequestUnblocker(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Constructor for the unblocker where we are blocked waiting for a target. + /// + internal BuildRequestUnblocker(int globalRequestIdToResume) + { + ErrorUtilities.VerifyThrowArgumentOutOfRange(globalRequestIdToResume != BuildRequest.InvalidGlobalRequestId, "globalRequestIdToResume"); + _blockedGlobalRequestId = globalRequestIdToResume; + } + + /// + /// Constructor for the unblocker where we are blocked waiting for results. + /// + internal BuildRequestUnblocker(BuildResult buildResult) + { + ErrorUtilities.VerifyThrowArgumentNull(buildResult, "buildResult"); + _buildResult = buildResult; + _blockedGlobalRequestId = buildResult.ParentGlobalRequestId; + } + + /// + /// Constructor for the unblocker for circular dependencies + /// + internal BuildRequestUnblocker(BuildRequest parentRequest, BuildResult buildResult) + : this(buildResult) + { + ErrorUtilities.VerifyThrowArgumentNull(parentRequest, "parentRequest"); + _blockedGlobalRequestId = parentRequest.GlobalRequestId; + } + + /// + /// Returns the type of packet. + /// + public NodePacketType Type + { + [DebuggerStepThrough] + get + { return NodePacketType.BuildRequestUnblocker; } + } + + /// + /// Accessor for the blocked node request id. + /// + public int BlockedRequestId + { + [DebuggerStepThrough] + get + { + return _blockedGlobalRequestId; + } + } + + /// + /// Accessor for the build results, if any. + /// + public BuildResult Result + { + [DebuggerStepThrough] + get + { + return _buildResult; + } + } + + #region INodePacketTranslatable Members + + /// + /// Serialization method. + /// + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _blockedGlobalRequestId); + translator.Translate(ref _buildResult); + } + + #endregion + + /// + /// Factory for serialization. + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildRequestUnblocker(translator); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/BuildResult.cs b/src/XMakeBuildEngine/BackEnd/Shared/BuildResult.cs new file mode 100644 index 00000000000..22004eca01f --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/BuildResult.cs @@ -0,0 +1,670 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A build result. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Execution +{ + /// + /// Overall results for targets and requests + /// + public enum BuildResultCode + { + /// + /// The target or request was a complete success. + /// + Success, + + /// + /// The target or request failed in some way. + /// + Failure + } + + /// + /// Contains the current results for all of the targets which have produced results for a particular configuration. + /// + public class BuildResult : INodePacket, IBuildResults + { + /// + /// The submission with which this result is associated. + /// + private int _submissionId; + + /// + /// The configuration ID with which this result is associated. + /// + private int _configurationId; + + /// + /// The global build request ID for which these results are intended. + /// + private int _globalRequestId; + + /// + /// The global build request ID which issued the request leading to this result. + /// + private int _parentGlobalRequestId; + + /// + /// The build request ID on the originating node. + /// + private int _nodeRequestId; + + /// + /// The first build request to generate results for a configuration will set this so that future + /// requests may be properly satisfied from the cache. + /// + private List _initialTargets; + + /// + /// The first build request to generate results for a configuration will set this so that future + /// requests may be properly satisfied from the cache. + /// + private List _defaultTargets; + + /// + /// The set of results for each target. + /// + private ConcurrentDictionary _resultsByTarget; + + /// + /// The request caused a circular dependency in scheduling. + /// + private bool _circularDependency; + + /// + /// The exception generated while this request was running, if any. + /// Note that this can be set if the request itself fails, or if it receives + /// an exception from a target or task. + /// + private Exception _requestException; + + /// + /// The overall result calculated in the constructor. + /// + private bool _baseOverallResult = true; + + /// + /// Snapshot of the environment from the configuration this results comes from. + /// This should only be populated when the configuration for this result is moved between nodes. + /// + private Dictionary _savedEnvironmentVariables; + + /// + /// Snapshot of the current directory from the configuration this result comes from. + /// This should only be populated when the configuration for this result is moved between nodes. + /// + private string _savedCurrentDirectory; + + /// + /// state after the build. This is only provided if + /// includes for the build request which this object is a result of, + /// and will be null otherwise. In general, where available, it may be a non buildable-dummy object, and should only + /// be used to retrieve , and + /// from it. Any other operation is not guaranteed to be supported. + /// + private ProjectInstance _projectStateAfterBuild; + + /// + /// Constructor for serialization. + /// + public BuildResult() + { + } + + /// + /// Constructor creates an empty build result + /// + /// The build request to which these results should be associated. + internal BuildResult(BuildRequest request) + : this(request, null) + { + } + + /// + /// Constructs a build result with an exception + /// + /// The build request to which these results should be associated. + /// The exception, if any. + internal BuildResult(BuildRequest request, Exception exception) + : this(request, null, exception) + { + } + + /// + /// Constructor creates a build result indicating a circular dependency was created. + /// + /// The build request to which these results should be associated. + /// Set to true if a circular dependency was detected. + internal BuildResult(BuildRequest request, bool circularDependency) + : this(request, null) + { + _circularDependency = circularDependency; + } + + /// + /// Constructs a new build result based on existing results, but filtered by a specified set of target names + /// + /// The existing results. + /// The target names whose results we will take from the existing results, if they exist. + internal BuildResult(BuildResult existingResults, string[] targetNames) + { + _submissionId = existingResults._submissionId; + _configurationId = existingResults._configurationId; + _globalRequestId = existingResults._globalRequestId; + _parentGlobalRequestId = existingResults._parentGlobalRequestId; + _nodeRequestId = existingResults._nodeRequestId; + _requestException = existingResults._requestException; + _resultsByTarget = CreateTargetResultDictionaryWithContents(existingResults, targetNames); + _baseOverallResult = existingResults.OverallResult == BuildResultCode.Success; + + _circularDependency = existingResults._circularDependency; + } + + /// + /// Constructs a new build result with existing results, but associated with the specified request. + /// + /// The build request with which these results should be associated. + /// The existing results, if any. + /// The exception, if any + internal BuildResult(BuildRequest request, BuildResult existingResults, Exception exception) + : this(request, existingResults, null, null, exception) + { + } + + /// + /// Constructs a new build result with existing results, but associated with the specified request. + /// + /// The build request with which these results should be associated. + /// The existing results, if any. + /// The list of target names that are the subset of results that should be returned. + /// The additional targets that need to be taken into account when computing the overall result, if any. + /// The exception, if any + internal BuildResult(BuildRequest request, BuildResult existingResults, string[] targetNames, List additionalTargetsToCheck, Exception exception) + { + ErrorUtilities.VerifyThrow(request != null, "Must specify a request."); + _submissionId = request.SubmissionId; + _configurationId = request.ConfigurationId; + _globalRequestId = request.GlobalRequestId; + _parentGlobalRequestId = request.ParentGlobalRequestId; + _nodeRequestId = request.NodeRequestId; + _circularDependency = false; + + if (existingResults == null) + { + _requestException = exception; + _resultsByTarget = CreateTargetResultDictionary(0); + _baseOverallResult = true; + } + else + { + _requestException = exception ?? existingResults._requestException; + + if (targetNames == null) + { + _resultsByTarget = existingResults._resultsByTarget; + } + else + { + _resultsByTarget = CreateTargetResultDictionaryWithContents(existingResults, targetNames); + } + + if (existingResults.OverallResult == BuildResultCode.Success || (additionalTargetsToCheck == null || additionalTargetsToCheck.Count == 0)) + { + // If we know for a fact that all of the existing results succeeded, then by definition we'll have + // succeeded too. Alternately, if we don't have any additional targets to check, then we want the + // overall result to reflect only the targets included in this result, which the OverallResult + // property already does -- so just default to true in that case as well. + _baseOverallResult = true; + } + else + { + // If the existing result is a failure, then we need to determine whether the targets we are + // specifically interested in contributed to that failure or not. If they did not, then this + // result should be sucessful even though the result it is based on failed. + // + // For the most part, this is taken care of for us because any dependent targets that fail also + // mark their parent targets (up to and including the entrypoint target) as failed. However, + // there is one case in which this is not true: if the entrypoint target has AfterTargets that + // fail, then as far as the entrypoint target knows when it is executing, it has succeeded. The + // failure doesn't come until after. On the other hand, we don't want to actually include the + // AfterTarget results in the build result itself if the user hasn't asked for them. + // + // So in the case where there are AfterTargets, we will check them for failure so that we can + // make sure the overall success/failure result is correct, but not actually add their contents + // to the new result. + _baseOverallResult = true; + + foreach (string additionalTarget in additionalTargetsToCheck) + { + TargetResult targetResult; + if (existingResults.ResultsByTarget.TryGetValue(additionalTarget, out targetResult)) + { + if (targetResult.ResultCode == TargetResultCode.Failure && !targetResult.TargetFailureDoesntCauseBuildFailure) + { + _baseOverallResult = false; + break; + } + } + } + } + } + } + + /// + /// Constructor which allows reporting results for a different nodeRequestId + /// + internal BuildResult(BuildResult result, int nodeRequestId) + { + _configurationId = result._configurationId; + _globalRequestId = result._globalRequestId; + _parentGlobalRequestId = result._parentGlobalRequestId; + _nodeRequestId = nodeRequestId; + _requestException = result._requestException; + _resultsByTarget = result._resultsByTarget; + _circularDependency = result._circularDependency; + _initialTargets = result._initialTargets; + _defaultTargets = result._defaultTargets; + _baseOverallResult = result.OverallResult == BuildResultCode.Success; + } + + /// + /// Constructor for deserialization + /// + private BuildResult(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Returns the submission id. + /// + public int SubmissionId + { + [DebuggerStepThrough] + get + { return _submissionId; } + } + + /// + /// Returns the configuration ID for this result. + /// + public int ConfigurationId + { + [DebuggerStepThrough] + get + { return _configurationId; } + } + + /// + /// Returns the build request id for which this result was generated + /// + public int GlobalRequestId + { + [DebuggerStepThrough] + get + { return _globalRequestId; } + } + + /// + /// Returns the build request id for the parent of the request for which this result was generated + /// + public int ParentGlobalRequestId + { + [DebuggerStepThrough] + get + { return _parentGlobalRequestId; } + } + + /// + /// Returns the node build request id for which this result was generated + /// + public int NodeRequestId + { + [DebuggerStepThrough] + get + { return _nodeRequestId; } + } + + /// + /// Returns the exception generated while this result was run, if any. + /// + public Exception Exception + { + [DebuggerStepThrough] + get + { return _requestException; } + + [DebuggerStepThrough] + internal set + { _requestException = value; } + } + + /// + /// Returns a flag indicating if a circular dependency was detected. + /// + public bool CircularDependency + { + [DebuggerStepThrough] + get + { return _circularDependency; } + } + + /// + /// Returns the overall result for this result set. + /// + public BuildResultCode OverallResult + { + get + { + if (null != _requestException || _circularDependency || !_baseOverallResult) + { + return BuildResultCode.Failure; + } + + foreach (KeyValuePair result in _resultsByTarget) + { + if (result.Value.ResultCode == TargetResultCode.Failure && !result.Value.TargetFailureDoesntCauseBuildFailure) + { + return BuildResultCode.Failure; + } + } + + return BuildResultCode.Success; + } + } + + /// + /// Returns an enumerator for all target results in this build result + /// + public IDictionary ResultsByTarget + { + [DebuggerStepThrough] + get + { return _resultsByTarget; } + } + + /// + /// state after the build. In general, it may be a non buildable-dummy object, and should only + /// be used to retrieve , and + /// from it. Any other operation is not guaranteed to be supported. + /// + public ProjectInstance ProjectStateAfterBuild + { + get { return _projectStateAfterBuild; } + set { _projectStateAfterBuild = value; } + } + + /// + /// Returns the node packet type. + /// + NodePacketType INodePacket.Type + { + [DebuggerStepThrough] + get + { return NodePacketType.BuildResult; } + } + + /// + /// Holds a snapshot of the environment at the time we blocked. + /// + Dictionary IBuildResults.SavedEnvironmentVariables + { + get + { + return _savedEnvironmentVariables; + } + + set + { + _savedEnvironmentVariables = value; + } + } + + /// + /// Holds a snapshot of the current working directory at the time we blocked. + /// + string IBuildResults.SavedCurrentDirectory + { + get + { + return _savedCurrentDirectory; + } + + set + { + _savedCurrentDirectory = value; + } + } + + /// + /// Returns the initial targets for the configuration which requested these results. + /// + internal List InitialTargets + { + [DebuggerStepThrough] + get + { return _initialTargets; } + + [DebuggerStepThrough] + set + { _initialTargets = value; } + } + + /// + /// Returns the default targets for the configuration which requested these results. + /// + internal List DefaultTargets + { + [DebuggerStepThrough] + get + { return _defaultTargets; } + + [DebuggerStepThrough] + set + { _defaultTargets = value; } + } + + /// + /// Returns true if this result belongs to a root request (that is, no node is waiting for + /// these results. + /// + internal bool ResultBelongsToRootRequest + { + [DebuggerStepThrough] + get + { return _parentGlobalRequestId == BuildRequest.InvalidGlobalRequestId; } + } + + /// + /// Indexer which sets or returns results for the specified target + /// + /// The target + /// The results for the specified target + /// KeyNotFoundException is returned if the specified target doesn't exist when reading this property. + /// ArgumentException is returned if the specified target already has results. + public ITargetResult this[string target] + { + [DebuggerStepThrough] + get + { return _resultsByTarget[target]; } + } + + /// + /// Adds the results for the specified target to this result collection. + /// + /// The target to which these results apply. + /// The results for the target. + public void AddResultsForTarget(string target, TargetResult result) + { + ErrorUtilities.VerifyThrowArgumentNull(target, "target"); + ErrorUtilities.VerifyThrowArgumentNull(result, "result"); + if (_resultsByTarget.ContainsKey(target)) + { + ErrorUtilities.VerifyThrow(_resultsByTarget[target].ResultCode == TargetResultCode.Skipped, "Items already exist for target {0}.", target); + } + + _resultsByTarget[target] = result; + } + + /// + /// Merges the specified results with the results contained herein. + /// + /// The results to merge in. + public void MergeResults(BuildResult results) + { + ErrorUtilities.VerifyThrowArgumentNull(results, "results"); + ErrorUtilities.VerifyThrow(results.ConfigurationId == ConfigurationId, "Result configurations don't match"); + + // If we are merging with ourself or with a shallow clone, do nothing. + if (Object.ReferenceEquals(this, results) || Object.ReferenceEquals(_resultsByTarget, results._resultsByTarget)) + { + return; + } + + // Merge in the results + foreach (KeyValuePair targetResult in results._resultsByTarget) + { + // NOTE: I believe that because we only allow results for a given target to be produced and cached once for a given configuration, + // we can never receive conflicting results for that target, since the cache and build request manager would always return the + // cached results after the first time the target is built. As such, we can allow "duplicates" to be merged in because there is + // no change. If, however, this turns out not to be the case, we need to re-evaluate this merging and possibly re-enable the + // assertion below. +#if false + // Allow no duplicates. + ErrorUtilities.VerifyThrow(!HasResultsForTarget(targetResult.Key), "Results already exist"); +#endif + // Copy the new results in. + _resultsByTarget[targetResult.Key] = targetResult.Value; + } + + // If there is an exception and we did not previously have one, add it in. + _requestException = _requestException ?? results.Exception; + } + + /// + /// Determines if there are any results for the specified target. + /// + /// The target for which results are desired. + /// True if results exist, false otherwise. + public bool HasResultsForTarget(string target) + { + return _resultsByTarget.ContainsKey(target); + } + + #region INodePacket Members + + /// + /// Reads or writes the packet to the serializer. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _submissionId); + translator.Translate(ref _configurationId); + translator.Translate(ref _globalRequestId); + translator.Translate(ref _parentGlobalRequestId); + translator.Translate(ref _nodeRequestId); + translator.Translate(ref _initialTargets); + translator.Translate(ref _defaultTargets); + translator.Translate(ref _circularDependency); + translator.TranslateDotNet(ref _requestException); + translator.TranslateDictionary, TargetResult>(ref _resultsByTarget, TargetResult.FactoryForDeserialization, CreateTargetResultDictionary); + translator.Translate(ref _baseOverallResult); + translator.Translate(ref _projectStateAfterBuild, ProjectInstance.FactoryForDeserialization); + translator.Translate(ref _savedCurrentDirectory); + translator.TranslateDictionary(ref _savedEnvironmentVariables, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Factory for serialization + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new BuildResult(translator); + } + + #endregion + + /// + /// Caches all of the targets results we can. + /// + internal void CacheIfPossible() + { + foreach (string target in _resultsByTarget.Keys) + { + _resultsByTarget[target].CacheItems(ConfigurationId, target); + } + } + + /// + /// Clear cached files from disk. + /// + internal void ClearCachedFiles() + { + string resultsDirectory = TargetResult.GetCacheDirectory(_configurationId, "None" /*Does not matter because we just need the directory name not the file*/); + if (Directory.Exists(resultsDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(resultsDirectory, true /*recursive*/); + } + } + + /// + /// Clones the build result (the resultsByTarget field is only a shallow copy). + /// + internal BuildResult Clone() + { + BuildResult result = new BuildResult(); + result._submissionId = _submissionId; + result._configurationId = _configurationId; + result._globalRequestId = _globalRequestId; + result._parentGlobalRequestId = _parentGlobalRequestId; + result._nodeRequestId = _nodeRequestId; + result._requestException = _requestException; + result._resultsByTarget = new ConcurrentDictionary(_resultsByTarget, StringComparer.OrdinalIgnoreCase); + result._baseOverallResult = this.OverallResult == BuildResultCode.Success; + result._circularDependency = _circularDependency; + + return result; + } + + /// + /// Creates the target result dictionary. + /// + private ConcurrentDictionary CreateTargetResultDictionary(int capacity) + { + return new ConcurrentDictionary(1, capacity, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates the target result dictionary and populates it with however many target results are + /// available given the list of targets passed. + /// + private ConcurrentDictionary CreateTargetResultDictionaryWithContents(BuildResult existingResults, string[] targetNames) + { + var resultsByTarget = CreateTargetResultDictionary(targetNames.Length); + + foreach (string target in targetNames) + { + TargetResult targetResult; + if (existingResults.ResultsByTarget.TryGetValue(target, out targetResult)) + { + resultsByTarget[target] = targetResult; + } + } + + return resultsByTarget; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/CircularDependencyException.cs b/src/XMakeBuildEngine/BackEnd/Shared/CircularDependencyException.cs new file mode 100644 index 00000000000..1f287a32eb7 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/CircularDependencyException.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The exception which gets thrown if a circular dependency is indicated by the BuildManager. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.Serialization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// An exception representing the case where a BuildRequest has caused a circular project dependency. This is used to + /// terminate the request builder which initiated the failure path. + /// + /// + /// If you add fields to this class, add a custom serialization constructor and override GetObjectData(). + /// + [Serializable] + internal class CircularDependencyException : Exception + { + /// + /// Constructs a standard BuildAbortedException. + /// + internal CircularDependencyException() + : base() + { + } + + /// + /// Constructor for deserialization. + /// + protected CircularDependencyException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/ConfigurationMetadata.cs b/src/XMakeBuildEngine/BackEnd/Shared/ConfigurationMetadata.cs new file mode 100644 index 00000000000..c78ee1668c5 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/ConfigurationMetadata.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class used for efficiently indexing BuildRequestConfigurations in the cache. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// A struct representing the uniquely-identifying portion of a BuildRequestConfiguration. Used for lookups. + /// + internal class ConfigurationMetadata : IEquatable + { + /// + /// Constructor over a BuildRequestConfiguration. + /// + public ConfigurationMetadata(BuildRequestConfiguration configuration) + { + ErrorUtilities.VerifyThrowArgumentNull(configuration, "configuration"); + GlobalProperties = new PropertyDictionary(configuration.Properties); + ProjectFullPath = FileUtilities.NormalizePath(configuration.ProjectFullPath); + ToolsVersion = configuration.ToolsVersion; + } + + /// + /// Constructor over a Project. + /// + public ConfigurationMetadata(Project project) + { + ErrorUtilities.VerifyThrowArgumentNull(project, "project"); + GlobalProperties = new PropertyDictionary(project.GlobalProperties.Count); + foreach (KeyValuePair entry in project.GlobalProperties) + { + this.GlobalProperties[entry.Key] = ProjectPropertyInstance.Create(entry.Key, entry.Value); + } + + ToolsVersion = project.ToolsVersion; + ProjectFullPath = FileUtilities.NormalizePath(project.FullPath); + } + + /// + /// The full path to the project to build. + /// + public string ProjectFullPath + { + get; + private set; + } + + /// + /// The tools version specified for the configuration. + /// Always specified. + /// May have originated from a /tv switch, or an MSBuild task, + /// or a Project tag, or the default. + /// + public string ToolsVersion + { + get; + private set; + } + + /// + /// The set of global properties which should be used when building this project. + /// + public PropertyDictionary GlobalProperties + { + get; + private set; + } + + /// + /// This override is used to provide a hash code for storage in dictionaries and the like. + /// + /// + /// If two objects are Equal, they must have the same hash code, for dictionaries to work correctly. + /// Two configurations are Equal if their global properties are equivalent, not necessary reference equals. + /// So only include filename and tools version in the hashcode. + /// + /// A hash code + public override int GetHashCode() + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(ProjectFullPath) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(ToolsVersion); + } + + /// + /// Determines object equality + /// + /// The object to compare with + /// True if they contain the same data, false otherwise + public override bool Equals(object obj) + { + if (Object.ReferenceEquals(obj, null)) + { + return false; + } + + if (this.GetType() != obj.GetType()) + { + return false; + } + + return InternalEquals((ConfigurationMetadata)obj); + } + + #region IEquatable Members + + /// + /// Equality of the configuration is the product of the equality of its members. + /// + /// The other configuration to which we will compare ourselves. + /// True if equal, false otherwise. + public bool Equals(ConfigurationMetadata other) + { + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + return InternalEquals(other); + } + + #endregion + + /// + /// Compares this object with another for equality + /// + /// The object with which to compare this one. + /// True if the objects contain the same data, false otherwise. + private bool InternalEquals(ConfigurationMetadata other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + return ProjectFullPath.Equals(other.ProjectFullPath, StringComparison.OrdinalIgnoreCase) && + ToolsVersion.Equals(other.ToolsVersion, StringComparison.OrdinalIgnoreCase) && + GlobalProperties.Equals(other.GlobalProperties); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/IBuildResults.cs b/src/XMakeBuildEngine/BackEnd/Shared/IBuildResults.cs new file mode 100644 index 00000000000..12b77833edb --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/IBuildResults.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The interface for build results. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.BackEnd +{ + /// + /// An interface representing results for a build request + /// + internal interface IBuildResults + { + /// + /// The exception, if any, generated while the build ran. + /// + Exception Exception + { + get; + } + + /// + /// The overall build result code. + /// + BuildResultCode OverallResult + { + get; + } + + /// + /// Returns an enumerator for all target results in this build result + /// + IDictionary ResultsByTarget + { + get; + } + + /// + /// Set of environment variables for the configuration this result came from + /// + Dictionary SavedEnvironmentVariables + { + get; + set; + } + + /// + /// The current directory for the configuration this result came from + /// + string SavedCurrentDirectory + { + get; + set; + } + + /// + /// Gets the results for a target in the build request + /// + /// The target name + /// The target results + ITargetResult this[string target] + { + get; + } + + /// + /// Returns true if there are results for the specified target + /// + /// The target name + /// True if results exist, false otherwise. + bool HasResultsForTarget(string target); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/ITargetResult.cs b/src/XMakeBuildEngine/BackEnd/Shared/ITargetResult.cs new file mode 100644 index 00000000000..400b3e128cd --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/ITargetResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An interface for target results. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Execution +{ + /// + /// The result code for a given target. + /// + [SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32", Justification = "TargetResultCode is serialized - additional bytes waste bandwidth")] + public enum TargetResultCode : byte + { + /// + /// The target was skipped because its condition was not met. + /// + Skipped, + + /// + /// The target successfully built. + /// + Success, + + /// + /// The target failed to build. + /// + Failure, + } + + /// + /// An interface representing results for a specific target + /// + public interface ITargetResult + { + /// + /// The exception generated when the target ran, if any. + /// + Exception Exception + { + get; + } + + /// + /// The set of build items output by the target. + /// These are ITaskItem's, so they have no item-type. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This isn't worth fixing. The current code depends too on the fact that TaskItem[] can implicitly cast to ITaskItem[] but the same is not true for List and List. Also a public interface (IBuildEngine) would have to be changed, or the items copied into an array")] + ITaskItem[] Items + { + get; + } + + /// + /// The result code for the target run. + /// + TargetResultCode ResultCode + { + get; + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/TargetResult.cs b/src/XMakeBuildEngine/BackEnd/Shared/TargetResult.cs new file mode 100644 index 00000000000..6a901ba9470 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/TargetResult.cs @@ -0,0 +1,577 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents results for a specific target. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Globalization; +using System.IO.Compression; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Execution +{ + /// + /// Contains the result items for a single target as well as the overall result code. + /// + public class TargetResult : ITargetResult, INodePacketTranslatable + { + /// + /// The result for this target. + /// + private WorkUnitResult _result; + + /// + /// Flag indicating whether to consider this target failure as having caused a build failure. + /// + private bool _targetFailureDoesntCauseBuildFailure; + + /// + /// The store of items in this result. + /// + private ItemsStore _itemsStore; + + /// + /// The context under which these results have been cached. + /// + private CacheInfo _cacheInfo; + + /// + /// Initializes the results with specified items and result. + /// + /// The items produced by the target. + /// The overall result for the target. + internal TargetResult(TaskItem[] items, WorkUnitResult result) + { + ErrorUtilities.VerifyThrowArgumentNull(items, "items"); + ErrorUtilities.VerifyThrowArgumentNull(result, "result"); + _itemsStore = new ItemsStore(items); + _result = result; + } + + /// + /// Private constructor for deserialization + /// + private TargetResult(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Returns the exception which aborted this target, if any. + /// + /// The exception which aborted this target, if any. + public Exception Exception + { + [DebuggerStepThrough] + get + { return _result.Exception; } + } + + /// + /// Returns the items produced by the target. + /// These are ITaskItem's, so they have no item-type. + /// + /// The items produced by the target. + public ITaskItem[] Items + { + [DebuggerStepThrough] + get + { + lock (_result) + { + RetrieveItemsFromCache(); + + // NOTE: If the items in the ItemsStore were compressed, this will decompress them. If the only purpose to + // getting these items is to check the length, then that is inefficient and we should come up with a better + // way of getting the count (such as another interface method which delegates to the ItemsStore itself. + return _itemsStore.Items; + } + } + } + + /// + /// Returns the result code for the target. + /// + /// The result code for the target. + public TargetResultCode ResultCode + { + [DebuggerStepThrough] + get + { + switch (_result.ResultCode) + { + case WorkUnitResultCode.Canceled: + case WorkUnitResultCode.Failed: + return TargetResultCode.Failure; + + case WorkUnitResultCode.Skipped: + return TargetResultCode.Skipped; + + case WorkUnitResultCode.Success: + return TargetResultCode.Success; + + default: + return TargetResultCode.Skipped; + } + } + } + + /// + /// Returns the internal result for the target. + /// + internal WorkUnitResult WorkUnitResult + { + [DebuggerStepThrough] + get + { return _result; } + } + + /// + /// Sets or gets a flag indicating whether or not a failure results should cause the build to fail. + /// + internal bool TargetFailureDoesntCauseBuildFailure + { + [DebuggerStepThrough] + get + { return _targetFailureDoesntCauseBuildFailure; } + + [DebuggerStepThrough] + set + { _targetFailureDoesntCauseBuildFailure = value; } + } + + #region INodePacketTranslatable Members + + /// + /// Reads or writes the packet to the serializer. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + if (translator.Mode == TranslationDirection.WriteToStream) + { + lock (_result) + { + // Should we have cached these items but now want to send them to another node, we need to + // ensure they are loaded before doing so. + RetrieveItemsFromCache(); + InternalTranslate(translator); + } + } + else + { + InternalTranslate(translator); + } + } + + /// + /// Factory for serialization. + /// + static internal TargetResult FactoryForDeserialization(INodePacketTranslator translator) + { + return new TargetResult(translator); + } + + #endregion + + /// + /// Gets the name of the cache file for this configuration. + /// + internal static string GetCacheFile(int configId, string targetToCache) + { + string filename = Path.Combine(FileUtilities.GetCacheDirectory(), String.Format(CultureInfo.InvariantCulture, Path.Combine("Results{0}", "{1}.cache"), configId, targetToCache)); + return filename; + } + + /// + /// Gets the name of the cache file for this configuration. + /// + internal static string GetCacheDirectory(int configId, string targetToCache) + { + string filename = GetCacheFile(configId, targetToCache); + string directoryName = Path.GetDirectoryName(filename); + return directoryName; + } + + /// + /// Cache the items. + /// + internal void CacheItems(int configId, string targetName) + { + lock (_result) + { + if (_itemsStore == null) + { + // Already cached. + return; + } + + if (_itemsStore.ItemsCount == 0) + { + // Nothing to cache. + return; + } + + INodePacketTranslator translator = GetResultsCacheTranslator(configId, targetName, TranslationDirection.WriteToStream); + + // If the translator is null, it means these results were cached once before. Since target results are immutable once they + // have been created, there is no point in writing them again. + if (translator != null) + { + try + { + translator.Translate(ref _itemsStore, ItemsStore.FactoryForDeserialization); + _itemsStore = null; + _cacheInfo = new CacheInfo(configId, targetName); + } + finally + { + translator.Writer.BaseStream.Close(); + } + } + } + } + + /// + /// Performs the actual translation + /// + private void InternalTranslate(INodePacketTranslator translator) + { + translator.Translate(ref _result, WorkUnitResult.FactoryForDeserialization); + translator.Translate(ref _targetFailureDoesntCauseBuildFailure); + translator.Translate(ref _itemsStore, ItemsStore.FactoryForDeserialization); + } + + /// + /// Retrieve the items from the cache. + /// + private void RetrieveItemsFromCache() + { + lock (_result) + { + if (_itemsStore == null) + { + INodePacketTranslator translator = GetResultsCacheTranslator(_cacheInfo.ConfigId, _cacheInfo.TargetName, TranslationDirection.ReadFromStream); + + try + { + translator.Translate(ref _itemsStore, ItemsStore.FactoryForDeserialization); + _cacheInfo = new CacheInfo(); + } + finally + { + translator.Reader.BaseStream.Close(); + } + } + } + } + + /// + /// Gets the translator for this configuration. + /// + private INodePacketTranslator GetResultsCacheTranslator(int configId, string targetToCache, TranslationDirection direction) + { + string cacheFile = GetCacheFile(configId, targetToCache); + if (direction == TranslationDirection.WriteToStream) + { + Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + if (File.Exists(cacheFile)) + { + // If the file already exists, then we have cached this once before. No need to cache it again since it cannot have changed. + return null; + } + + return NodePacketTranslator.GetWriteTranslator(File.Create(cacheFile)); + } + else + { + return NodePacketTranslator.GetReadTranslator(File.OpenRead(cacheFile), null); + } + } + + /// + /// Information about where the cache for the items in this result are stored. + /// + private struct CacheInfo + { + /// + /// The configuration ID for these results. + /// + private int _configId; + + /// + /// The target with which these results are associated. + /// + private string _targetName; + + /// + /// Constructor. + /// + public CacheInfo(int configId, string targetName) + { + _configId = configId; + _targetName = targetName; + } + + /// + /// Retrieves the configuration id. + /// + public int ConfigId + { + get { return _configId; } + } + + /// + /// Retrieves the target name. + /// + public string TargetName + { + get { return _targetName; } + } + } + + /// + /// The store of items for the target result. This class is responsible for the serialization of the items collection, which is + /// useful to keep separate as it is where we spend most of our time serializing for large projects, and these are the bits + /// we throw out of memory when the cache gets collected. + /// + private class ItemsStore : INodePacketTranslatable + { + /// + /// The default compression threshold. + /// + private const int DefaultCompressionThreshold = 32; + + /// + /// The count of items we will store before we start using compression. + /// + /// + /// This value was determined empirically by looking at how many items tend to be transmitted for "normal" projects versus the ones + /// which benefit from this technique. + /// + private static readonly int s_compressionThreshold; + + /// + /// The compressed set of items, if any. + /// + private byte[] _compressedItems = null; + + /// + /// The count of items, stored here so that we don't have to decompress the items if we are + /// only looking at the count. + /// + private int _itemsCount = 0; + + /// + /// The items produced by this target. + /// + private TaskItem[] _uncompressedItems; + + /// + /// Static constructor. + /// + static ItemsStore() + { + if (Int32.TryParse(Environment.GetEnvironmentVariable("MSBUILDTARGETRESULTCOMPRESSIONTHRESHOLD"), out ItemsStore.s_compressionThreshold)) + { + if (ItemsStore.s_compressionThreshold < 0) + { + ItemsStore.s_compressionThreshold = 0; + } + } + else + { + ItemsStore.s_compressionThreshold = ItemsStore.DefaultCompressionThreshold; + } + } + + /// + /// Constructor + /// + public ItemsStore(TaskItem[] items) + { + ErrorUtilities.VerifyThrowArgumentNull(items, "items"); + _uncompressedItems = items; + _itemsCount = items.Length; + } + + /// + /// Constructor for serialization. + /// + private ItemsStore(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Gets the count of items. + /// + public int ItemsCount + { + get { return _itemsCount; } + } + + /// + /// Retrieves the items. + /// + /// + /// It's important not to call this method merely to get a count of the items held in the collection. + /// Instead use ItemsCount (above) for that. + /// + public TaskItem[] Items + { + get + { + if (_uncompressedItems == null) + { + DecompressItems(); + } + + return _uncompressedItems; + } + } + + /// + /// Throws out the deserialized items. + /// + /// + /// Not presently used, but could be used for a multi-stage caching mechanism which first throws out decompressed items, + /// then if more space is needed, starts throwing out the compressed ones. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Keeping around so that we can potentially expand on our current caching mechanism later")] + public void ReleaseItems() + { + if (_compressedItems == null) + { + CompressItems(); + } + } + + /// + /// Translates an items store. + /// + public void Translate(INodePacketTranslator translator) + { + if (_compressedItems == null && translator.Mode == TranslationDirection.WriteToStream) + { + CompressItemsIfNecessary(); + } + + // Note we only translate the serialized buffer (which contains the compressed and interned + // representation of the items.) If the actual items are needed (for instance on child nodes) + // then the Items accessor will reconstitute them at the point they are needed. + ErrorUtilities.VerifyThrow((translator.Mode == TranslationDirection.ReadFromStream) || ((_compressedItems == null) ^ (_uncompressedItems == null)), "One of the compressed or uncompressed items arrays should be null."); + translator.Translate(ref _itemsCount); + translator.Translate(ref _compressedItems); + translator.TranslateArray(ref _uncompressedItems, TaskItem.FactoryForDeserialization); + } + + /// + /// Factory for the serializer. + /// + static internal ItemsStore FactoryForDeserialization(INodePacketTranslator translator) + { + return new ItemsStore(translator); + } + + /// + /// Compresses the items, but only if we have reached the threshold where it makes sense to do so. + /// + private void CompressItemsIfNecessary() + { + if (_itemsCount > s_compressionThreshold) + { + CompressItems(); + } + } + + /// + /// Decompresses the items. + /// + private void DecompressItems() + { + ErrorUtilities.VerifyThrow(_uncompressedItems == null, "Items already decompressed."); + using (MemoryStream serializedStream = new MemoryStream(_compressedItems, 0, _compressedItems.Length, writable: false, publiclyVisible: true)) + { + using (DeflateStream inflateStream = new DeflateStream(serializedStream, CompressionMode.Decompress)) + { + INodePacketTranslator serializedBufferTranslator = NodePacketTranslator.GetReadTranslator(inflateStream, null); + LookasideStringInterner interner = new LookasideStringInterner(serializedBufferTranslator); + + byte[] buffer = null; + serializedBufferTranslator.Translate(ref buffer); + ErrorUtilities.VerifyThrow(buffer != null, "Unexpected null items buffer during decompression."); + + using (MemoryStream itemsStream = new MemoryStream(buffer, 0, buffer.Length, writable: false, publiclyVisible: true)) + { + INodePacketTranslator itemTranslator = NodePacketTranslator.GetReadTranslator(itemsStream, null); + _uncompressedItems = new TaskItem[_itemsCount]; + for (int i = 0; i < _uncompressedItems.Length; i++) + { + _uncompressedItems[i] = TaskItem.FactoryForDeserialization(itemTranslator, interner); + } + } + } + } + + _compressedItems = null; + } + + /// + /// Compresses the items. + /// + private void CompressItems() + { + ErrorUtilities.VerifyThrow(_compressedItems == null, "Items already compressed."); + + // We will just calculate a very rough starting buffer size for the memory stream based on the number of items and a + // rough guess for an average number of bytes needed to store them compressed. This doesn't have to be accurate, just + // big enough to avoid unnecessary buffer reallocations in most cases. + int defaultCompressedBufferCapacity = _uncompressedItems.Length * 64; + using (MemoryStream serializedStream = new MemoryStream(defaultCompressedBufferCapacity)) + { + using (DeflateStream deflateStream = new DeflateStream(serializedStream, CompressionMode.Compress)) + { + INodePacketTranslator serializedBufferTranslator = NodePacketTranslator.GetWriteTranslator(deflateStream); + + // Again, a rough calculation of buffer size, this time for an uncompressed buffer. We assume compression + // will give us 2:1, as it's all text. + int defaultUncompressedBufferCapacity = defaultCompressedBufferCapacity * 2; + using (MemoryStream itemsStream = new MemoryStream(defaultUncompressedBufferCapacity)) + { + INodePacketTranslator itemTranslator = NodePacketTranslator.GetWriteTranslator(itemsStream); + + // When creating the interner, we use the number of items as the initial size of the collections since the + // number of strings will be of the order of the number of items in the collection. This assumes basically + // one unique string per item (frequently a path related to the item) with most of the rest of the metadata + // being the same (and thus interning.) This is a hueristic meant to get us in the ballpark to avoid + // too many reallocations when growing the collections. + LookasideStringInterner interner = new LookasideStringInterner(StringComparer.Ordinal, _uncompressedItems.Length); + for (int i = 0; i < _uncompressedItems.Length; i++) + { + _uncompressedItems[i].TranslateWithInterning(itemTranslator, interner); + } + + interner.Translate(serializedBufferTranslator); + byte[] buffer = itemsStream.ToArray(); + serializedBufferTranslator.Translate(ref buffer); + } + } + + _compressedItems = serializedStream.ToArray(); + } + + _uncompressedItems = null; + } + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/Shared/WorkUnitResult.cs b/src/XMakeBuildEngine/BackEnd/Shared/WorkUnitResult.cs new file mode 100644 index 00000000000..04c2b2ce78d --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/Shared/WorkUnitResult.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The class representing a result from running a task. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The result of executing the task or target. + /// + internal enum WorkUnitResultCode + { + /// + /// The work unit was skipped. + /// + Skipped, + + /// + /// The work unit succeeded. + /// + Success, + + /// + /// The work unit failed. + /// + Failed, + + /// + /// The work unit was cancelled. + /// + Canceled, + } + + /// + /// Indicates whether further work should be done. + /// + internal enum WorkUnitActionCode + { + /// + /// Work should proceed with the next work unit. + /// + Continue, + + /// + /// No further work units should be executed. + /// + Stop, + } + + /// + /// A result of executing a target or task. + /// + internal class WorkUnitResult : INodePacketTranslatable + { + /// + /// The result. + /// + private WorkUnitResultCode _resultCode; + + /// + /// The next action to take. + /// + private WorkUnitActionCode _actionCode; + + /// + /// The exception from the failure, if any. + /// + private Exception _exception; + + /// + /// Creates a new work result ready for aggregation during batches. + /// + internal WorkUnitResult() + { + _resultCode = WorkUnitResultCode.Skipped; + _actionCode = WorkUnitActionCode.Continue; + _exception = null; + } + + /// + /// Creates a work result with the specified result codes. + /// + internal WorkUnitResult(WorkUnitResultCode resultCode, WorkUnitActionCode actionCode, Exception e) + { + _resultCode = resultCode; + _actionCode = actionCode; + _exception = e; + } + + /// + /// Translator constructor + /// + private WorkUnitResult(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Get the result code. + /// + internal WorkUnitResultCode ResultCode + { + get { return _resultCode; } + } + + /// + /// Get the action code. + /// + internal WorkUnitActionCode ActionCode + { + get { return _actionCode; } + } + + /// + /// Get the exception + /// + internal Exception Exception + { + get { return _exception; } + } + + #region INodePacketTranslatable Members + + /// + /// Translator. + /// + public void Translate(INodePacketTranslator translator) + { + translator.TranslateEnum(ref _resultCode, (int)_resultCode); + translator.TranslateEnum(ref _actionCode, (int)_actionCode); + translator.TranslateDotNet(ref _exception); + } + + #endregion + + /// + /// Factory for serialization. + /// + static internal WorkUnitResult FactoryForDeserialization(INodePacketTranslator translator) + { + return new WorkUnitResult(translator); + } + + /// + /// Aggregates the specified result with this result and returns the aggregation. + /// + /// + /// The rules are: + /// 1. Errors take precedence over success. + /// 2. Success takes precedence over skipped. + /// 3. Stop takes precedence over continue. + /// 4. The first exception in the result wins. + /// + internal WorkUnitResult AggregateResult(WorkUnitResult result) + { + WorkUnitResultCode aggregateResult = _resultCode; + WorkUnitActionCode aggregateAction = _actionCode; + Exception aggregateException = _exception; + + if (result._resultCode == WorkUnitResultCode.Canceled || result.ResultCode == WorkUnitResultCode.Failed) + { + // Failed and canceled take priority + aggregateResult = result._resultCode; + } + else if (result._resultCode == WorkUnitResultCode.Success && aggregateResult == WorkUnitResultCode.Skipped) + { + // Success only counts if we were previously in the skipped category. + aggregateResult = result._resultCode; + } + + if (result._actionCode == WorkUnitActionCode.Stop) + { + aggregateAction = result.ActionCode; + } + + if (aggregateException == null) + { + aggregateException = result._exception; + } + + return new WorkUnitResult(aggregateResult, aggregateAction, aggregateException); + } + } +} diff --git a/src/XMakeBuildEngine/BackEnd/TaskExecutionHost/AddInParts/ITaskExecutionHost.cs b/src/XMakeBuildEngine/BackEnd/TaskExecutionHost/AddInParts/ITaskExecutionHost.cs new file mode 100644 index 00000000000..1193be9d244 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/TaskExecutionHost/AddInParts/ITaskExecutionHost.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface representing an object which can execute tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; +using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Flags requrned by ITaskExecutionHost.FindTask(). + /// + [Flags] + internal enum TaskRequirements + { + /// + /// The task was not found. + /// + None = 0, + + /// + /// The task must be executed on an STA thread. + /// + RequireSTAThread = 0x01, + + /// + /// The task must be executed in a separate AppDomain. + /// + RequireSeparateAppDomain = 0x02 + } + + /// + /// This interface represents the host for task execution. When used in the in-proc scenario, these method calls essentially + /// are pass-throughs to just set some member variables and call methods directly on the task and associated objects. + /// In the out-of-proc/AppDomain-isolated case, the object implementing these methods may break apart the information + /// in the parameters to be consumed by the IContract representing the remote object through MAF. + /// + /// REFACTOR - Eliminate this interface. + /// + internal interface ITaskExecutionHost + { + /// + /// The associated project. + /// + ProjectInstance ProjectInstance + { + get; + } + + /// + /// Flag to determine whether or not to log task inputs. + /// + bool LogTaskInputs + { + get; + } + + /// + /// Initialize the host with the objects required to communicate with the host process. + /// + void InitializeForTask(IBuildEngine2 buildEngine, TargetLoggingContext loggingContext, ProjectInstance projectInstance, string taskName, ElementLocation taskLocation, ITaskHost taskHost, bool continueOnError, AppDomainSetup appDomainSetup, bool isOutOfProc, CancellationToken cancellationToken); + + /// + /// Ask the task host to find its task in the registry and get it ready for initializing the batch + /// + /// The task requirements if the task is found, null otherwise. + TaskRequirements? FindTask(IDictionary taskIdentityParameters); + + /// + /// Initializes for running a particular batch + /// + /// True if the task is instantiated, false otherwise. + bool InitializeForBatch(TaskLoggingContext loggingContext, ItemBucket batchBucket, IDictionary taskIdentityParameters); + + /// + /// Sets a task parameter using an unevaluated value, which will be expanded by the batch bucket. + /// + bool SetTaskParameters(IDictionary> parameters); + + /// + /// Gets all of the outputs and stores them in the batch bucket. + /// + bool GatherTaskOutputs(string parameterName, ElementLocation parameterLocation, bool outputTargetIsItem, string outputTargetName); + + /// + /// Signal that we are done with this bucket. + /// + void CleanupForBatch(); + + /// + /// Signal that we are done with this task. + /// + void CleanupForTask(); + + /// + /// Executes the task. + /// + /// + /// True if execution succeeded, false otherwise. + /// + bool Execute(); + } +} diff --git a/src/XMakeBuildEngine/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/XMakeBuildEngine/BackEnd/TaskExecutionHost/TaskExecutionHost.cs new file mode 100644 index 00000000000..080949fbe26 --- /dev/null +++ b/src/XMakeBuildEngine/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -0,0 +1,1649 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of ITaskExecutionHost for executing tasks in-proc. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using Microsoft.Build.Execution; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.BackEnd.Logging; +using System.Globalization; +using System.Reflection; +using System.Runtime.Remoting; + +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The TaskExecutionHost is responsible for instantiating tasks, setting their parameters and gathering outputs using + /// reflection, and executing the task in the appropriate context.The TaskExecutionHost does not deal with any part of the task declaration or + /// XML. + /// + internal class TaskExecutionHost : ITaskExecutionHost, IDisposable + { + /// + /// Time interval in miliseconds to wait between receiving a cancelation signal and emitting the first warning that a non-cancelable task has not finished + /// + private const int CancelFirstWarningWaitInterval = 5000; + + /// + /// Time interval in miliseconds between subsequent warnings that a non-cancelable task has not finished + /// + private const int CancelWarningWaitInterval = 15000; + + /// + /// Whether to log task input parameters. Can either be set through an environment variable + /// or by the BuildParameters. + /// + private bool _logTaskInputs; + + /// + /// Resolver to assist in resolving types when a new appdomain is created + /// + private TaskEngineAssemblyResolver _resolver; + + /// + /// The interface used to call back into the build engine. + /// + private IBuildEngine2 _buildEngine; + + /// + /// The project instance in whose context we are executing + /// + private ProjectInstance _projectInstance; + + // Items required for all batches of a task + + /// + /// The logging context for the target. + /// + private TargetLoggingContext _targetLoggingContext; + + /// + /// The logging context for the task. + /// + private TaskLoggingContext _taskLoggingContext; + + /// + /// The registration which handles the callback when task cancellation is invoked. + /// + private CancellationTokenRegistration _cancellationTokenRegistration; + + /// + /// The name of the task to execute. + /// + private string _taskName; + + /// + /// The XML location of the task element. + /// + private ElementLocation _taskLocation; + + /// + /// The arbitrary task host object. + /// + private ITaskHost _taskHost; + + // Items required for a particular batch of a task + + /// + /// The bucket used to evaluate items and properties. + /// + private ItemBucket _batchBucket; + + // Items used to execute the task + + /// + /// The task type retrieved from the assembly. + /// + private TaskFactoryWrapper _taskFactoryWrapper; + + /// + /// The instantiated task class. + /// + private ITask _taskInstance; + + /// + /// The continueOnError flag + /// + private bool _continueOnError; + + /// + /// Set to true if the execution has been cancelled. + /// + private bool _cancelled; + + /// + /// Event which is signalled when a task is not executing. Used for cancellation. + /// + private ManualResetEvent _taskExecutionIdle = new ManualResetEvent(true); + + /// + /// The task items that we remoted across the appdomain boundary + /// we use this list to disconnect the task items once we're done. + /// + private List _remotedTaskItems; + + /// + /// We need access to the build component host so that we can get at the + /// task host node provider when running a task wrapped by TaskHostTask + /// + private IBuildComponentHost _buildComponentHost; + + /// + /// The set of intrinsic tasks mapped for this process. + /// + private Dictionary _intrinsicTasks = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Constructor + /// + internal TaskExecutionHost(IBuildComponentHost host) + { + _buildComponentHost = host; + if (host != null && host.BuildParameters != null) + { + _logTaskInputs = host.BuildParameters.LogTaskInputs; + } + + // If this is false, check the environment variable to see if it's there: + if (!_logTaskInputs) + { + _logTaskInputs = (Environment.GetEnvironmentVariable("MSBUILDLOGTASKINPUTS") == "1"); + } + } + + /// + /// Constructor, for unit testing only. + /// + internal TaskExecutionHost() + { + // do nothing + } + + /// + /// Finalizer + /// + ~TaskExecutionHost() + { + Debug.Fail("Unexpected finalization. Dispose should already have been called."); + Dispose(false); + } + + /// + /// Flag to determine whether or not to log task inputs. + /// + public bool LogTaskInputs + { + get + { + return _logTaskInputs; + } + } + + /// + /// The associated project. + /// + ProjectInstance ITaskExecutionHost.ProjectInstance + { + get + { + return _projectInstance; + } + } + + /// + /// Gets the task instance + /// + internal ITask TaskInstance + { + get + { + return _taskInstance; + } + } + + /// + /// FOR UNIT TESTING ONLY + /// + internal TaskFactoryWrapper _UNITTESTONLY_TaskFactoryWrapper + { + get + { + return _taskFactoryWrapper; + } + + set + { + _taskFactoryWrapper = value; + } + } + + /// + /// App domain configuration. + /// + internal AppDomainSetup AppDomainSetup + { + get; + set; + } + + /// + /// Whether or not this is out-of-proc. + /// + internal bool IsOutOfProc + { + get; + set; + } + + /// + /// Implementation of IDisposable + /// + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region ITaskExecutionHost Members + + /// + /// Initialize to run a specific task. + /// + void ITaskExecutionHost.InitializeForTask(IBuildEngine2 buildEngine, TargetLoggingContext loggingContext, ProjectInstance projectInstance, string taskName, ElementLocation taskLocation, ITaskHost taskHost, bool continueOnError, AppDomainSetup appDomainSetup, bool isOutOfProc, CancellationToken cancellationToken) + { + _buildEngine = buildEngine; + _projectInstance = projectInstance; + _targetLoggingContext = loggingContext; + _taskName = taskName; + _taskLocation = taskLocation; + _cancellationTokenRegistration = cancellationToken.Register(this.Cancel); + _taskHost = taskHost; + _continueOnError = continueOnError; + _taskExecutionIdle.Set(); + this.AppDomainSetup = appDomainSetup; + this.IsOutOfProc = isOutOfProc; + } + + /// + /// Ask the task host to find its task in the registry and get it ready for initializing the batch + /// + /// True if the task is found in the task registry false if otherwise. + TaskRequirements? ITaskExecutionHost.FindTask(IDictionary taskIdentityParameters) + { + if (_taskFactoryWrapper == null) + { + _taskFactoryWrapper = FindTaskInRegistry(taskIdentityParameters); + } + + if (_taskFactoryWrapper == null) + { + return null; + } + + TaskRequirements requirements = TaskRequirements.None; + + if (_taskFactoryWrapper.TaskFactoryLoadedType.HasSTAThreadAttribute()) + { + requirements = requirements | TaskRequirements.RequireSTAThread; + } + + if (_taskFactoryWrapper.TaskFactoryLoadedType.HasLoadInSeparateAppDomainAttribute()) + { + requirements = requirements | TaskRequirements.RequireSeparateAppDomain; + + // we're going to be remoting across the appdomain boundary, so + // create the list that we'll use to disconnect the taskitems once we're done + _remotedTaskItems = new List(); + } + + return requirements; + } + + /// + /// Initialize to run a specific batch of the current task. + /// + bool ITaskExecutionHost.InitializeForBatch(TaskLoggingContext loggingContext, ItemBucket batchBucket, IDictionary taskIdentityParameters) + { + ErrorUtilities.VerifyThrowArgumentNull(loggingContext, "loggingContext"); + ErrorUtilities.VerifyThrowArgumentNull(batchBucket, "batchBucket"); + + _taskLoggingContext = loggingContext; + _batchBucket = batchBucket; + + if (_taskFactoryWrapper == null) + { + return false; + } + + // If the task assembly is loaded into a separate AppDomain using LoadFrom, then we have a problem + // to solve - when the task class Type is marshalled back into our AppDomain, it's not just transferred + // here. Instead, NDP will try to Load (not LoadFrom!) the task assembly into our AppDomain, and since + // we originally used LoadFrom, it will fail miserably not knowing where to find it. + // We need to temporarily subscribe to the AppDomain.AssemblyResolve event to fix it. + if (null == _resolver) + { + _resolver = new TaskEngineAssemblyResolver(); + _resolver.Initialize(_taskFactoryWrapper.TaskFactoryLoadedType.Assembly.AssemblyFile); + _resolver.InstallHandler(); + } + + // We instantiate a new task object for each batch + _taskInstance = InstantiateTask(taskIdentityParameters); + + if (_taskInstance == null) + { + return false; + } + + _taskInstance.BuildEngine = _buildEngine; + _taskInstance.HostObject = _taskHost; + + return true; + } + + /// + /// Sets all of the specified parameters on the task. + /// + /// The name/value pairs for the parameters. + /// True if the parameters were set correctly, false otherwise. + bool ITaskExecutionHost.SetTaskParameters(IDictionary> parameters) + { + ErrorUtilities.VerifyThrowArgumentNull(parameters, "parameters"); + + bool taskInitialized = true; + + // Get the properties that exist on this task. We need to gather all of the ones that are marked + // "required" so that we can keep track of whether or not they all get set. + Dictionary setParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + IDictionary requiredParameters = GetNamesOfPropertiesWithRequiredAttribute(); + + // look through all the attributes of the task element + foreach (KeyValuePair> parameter in parameters) + { + bool taskParameterSet = false; // Did we actually call the setter on this task parameter? + bool success = false; + + try + { + success = SetTaskParameter(parameter.Key, parameter.Value.Item1, parameter.Value.Item2, requiredParameters.ContainsKey(parameter.Key), out taskParameterSet); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + // Reflection related exception + _taskLoggingContext.LogError(new BuildEventFileInfo(_taskLocation), "TaskParametersError", _taskName, e.Message); + + success = false; + } + + if (!success) + { + // stop processing any more attributes + taskInitialized = false; + break; + } + else if (taskParameterSet) + { + // Keep track that we've set a value for this property. Note that this will + // keep track of non-required properties as well, but that's okay. We just + // to check at the end that there are no values in the requiredParameters + // table that aren't also in the setParameters table. + setParameters[parameter.Key] = String.Empty; + } + } + + if (taskInitialized) + { + // See if any required properties were not set + foreach (KeyValuePair requiredParameter in requiredParameters) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + setParameters.ContainsKey(requiredParameter.Key), + _taskLocation, + "RequiredPropertyNotSetError", + _taskName, + requiredParameter.Key + ); + } + } + + return taskInitialized; + } + + /// + /// Retrieve the outputs from the task. + /// + /// True of the outputs were gathered successfully, false otherwise. + bool ITaskExecutionHost.GatherTaskOutputs(string parameterName, ElementLocation parameterLocation, bool outputTargetIsItem, string outputTargetName) + { + ErrorUtilities.VerifyThrow(_taskFactoryWrapper != null, "Need a taskFactoryWrapper to retrieve outputs from."); + + bool gatheredGeneratedOutputsSuccessfully = true; + + try + { + TaskPropertyInfo parameter = _taskFactoryWrapper.GetProperty(parameterName); + + // flag an error if we find a parameter that has no .NET property equivalent + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + parameter != null, + parameterLocation, + "UnexpectedTaskOutputAttribute", + parameterName, + _taskName + ); + + // output parameters must have their corresponding .NET properties marked with the Output attribute + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + _taskFactoryWrapper.GetNamesOfPropertiesWithOutputAttribute.ContainsKey(parameterName), + parameterLocation, + "UnmarkedOutputTaskParameter", + parameter.Name, + _taskName + ); + + // grab the outputs from the task's designated output parameter (which is a .NET property) + Type type = parameter.PropertyType; + + if (TaskParameterTypeVerifier.IsAssignableToITask(type)) + { + ITaskItem[] outputs = GetItemOutputs(parameter, parameterLocation); + GatherTaskItemOutputs(outputTargetIsItem, outputTargetName, outputs, parameterLocation); + } + else if (TaskParameterTypeVerifier.IsValueTypeOutputParameter(type)) + { + string[] outputs = GetValueOutputs(parameter, parameterLocation); + GatherArrayStringAndValueOutputs(outputTargetIsItem, outputTargetName, parameter, outputs, parameterLocation); + } + else + { + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + false, + parameterLocation, + "UnsupportedTaskParameterTypeError", + parameter.PropertyType.FullName, + parameter.Name, + _taskName + ); + } + } + catch (InvalidOperationException e) + { + // handle invalid TaskItems in task outputs + _targetLoggingContext.LogError + ( + new BuildEventFileInfo(parameterLocation), + "InvalidTaskItemsInTaskOutputs", + _taskName, + parameterName, + e.Message + ); + + gatheredGeneratedOutputsSuccessfully = false; + } + catch (TargetInvocationException e) + { + // handle any exception thrown by the task's getter + // Exception thrown by the called code itself + // Log the stack, so the task vendor can fix their code + // Log the task line number, whatever the value of ContinueOnError; + // because this will be a hard error anyway. + _targetLoggingContext.LogFatalTaskError + ( + new BuildEventFileInfo(parameterLocation), + e.InnerException, + _taskName + ); + + // We do not recover from a task exception while getting outputs, + // so do not merely set gatheredGeneratedOutputsSuccessfully = false; here + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + false, + parameterLocation, + "FailedToRetrieveTaskOutputs", + _taskName, + parameterName, + e.InnerException.Message + ); + } + catch (Exception e) + { + // Catching Exception, but rethrowing unless it's a well-known exception. + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + false, + parameterLocation, + "FailedToRetrieveTaskOutputs", + _taskName, + parameterName, + e.Message + ); + } + + return gatheredGeneratedOutputsSuccessfully; + } + + /// + /// Cleans up after running a batch. + /// + void ITaskExecutionHost.CleanupForBatch() + { + try + { + if (_taskFactoryWrapper != null && _taskInstance != null) + { + _taskFactoryWrapper.TaskFactory.CleanupTask(_taskInstance); + } + } + finally + { + _taskInstance = null; + } + } + + /// + /// Cleans up after running the task. + /// + void ITaskExecutionHost.CleanupForTask() + { + if (_resolver != null) + { + _resolver.RemoveHandler(); + _resolver = null; + } + + _taskFactoryWrapper = null; + + // We must null this out because it could be a COM object (or any other ref-counted object) which needs to + // be released. + _taskHost = null; + CleanupCancellationToken(); + + ErrorUtilities.VerifyThrow(_taskInstance == null, "Task Instance should be null"); + } + + /// + /// Executes the task. + /// + bool ITaskExecutionHost.Execute() + { + // If cancel is called before we get here, we simply don't execute and return failure. If cancel is called after this check + // the task needs to be able to handle the possibility that Cancel has been called before the task has done anything meaningful, + // and Execute may not even have been called yet. + _taskExecutionIdle.Reset(); + + if (_cancelled) + { + _taskExecutionIdle.Set(); + return false; + } + + bool taskReturnValue; + + try + { + taskReturnValue = _taskInstance.Execute(); + } + finally + { + _taskExecutionIdle.Set(); + } + + return taskReturnValue; + } + + #endregion + + /// + /// Implementation of IDisposable + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _taskExecutionIdle.Close(); + CleanupCancellationToken(); + } + + // if we've been asked to remote these items then + // we need to disconnect them from .NET Remoting now we're all done with them + if (_remotedTaskItems != null) + { + foreach (TaskItem item in _remotedTaskItems) + { + // Tell remoting to forget connections to the taskitem + RemotingServices.Disconnect(item); + } + } + + _remotedTaskItems = null; + } + + /// + /// Disposes of the cancellation token registration. + /// + private void CleanupCancellationToken() + { + _cancellationTokenRegistration.Dispose(); + } + + /// + /// Cancels the currently-running task. + /// Kick off a task to wait for the currently-running task and log the wait message. + /// + private void Cancel() + { + // This will prevent the current and any future tasks from running on this TaskExecutionHost, because we don't reset the cancelled flag. + _cancelled = true; + + ITask currentInstance = _taskInstance; + ICancelableTask cancellableTask = null; + if (currentInstance != null) + { + cancellableTask = currentInstance as ICancelableTask; + } + + if (cancellableTask != null) + { + try + { + cancellableTask.Cancel(); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + try + { + _taskLoggingContext.LogFatalTaskError(new BuildEventFileInfo(_taskLocation), e, ((ProjectTaskInstance)_taskLoggingContext.Task).Name); + } + catch (InternalErrorException) + { + // If this fails it could be due to the task logging context no longer being valid due to a race condition where the task completes while we + // are in this method. In that case we simply ignore the exception and carry on since we can't log anything anyhow. + if (_taskLoggingContext.IsValid) + { + throw; + } + } + } + } + + // Let the task finish now. If cancellation worked, hopefully it finishes sooner than it would have otherwise. + // If the task builder crashed, this could have already been disposed + if (!_taskExecutionIdle.SafeWaitHandle.IsClosed) + { + // Kick off a task to log the message so that we don't block the calling thread. + Task.Run(async delegate + { + await _taskExecutionIdle.ToTask(CancelFirstWarningWaitInterval); + if (!_taskExecutionIdle.WaitOne(0)) + { + DisplayCancelWaitMessage(); + await _taskExecutionIdle.ToTask(CancelWarningWaitInterval); + while (!_taskExecutionIdle.WaitOne(0)) + { + DisplayCancelWaitMessage(); + await _taskExecutionIdle.ToTask(CancelWarningWaitInterval); + } + } + }); + } + } + + #region Local Methods + /// + /// Called on the local side. + /// + private bool SetTaskItemParameter(TaskPropertyInfo parameter, ITaskItem item, ElementLocation parameterLocation) + { + return InternalSetTaskParameter(parameter, item); + } + + /// + /// Called on the local side. + /// + private bool SetValueParameter(TaskPropertyInfo parameter, Type parameterType, string expandedParameterValue, ElementLocation parameterLocation) + { + if (parameterType == typeof(bool)) + { + // Convert the string to the appropriate datatype, and set the task's parameter. + return InternalSetTaskParameter(parameter, ConversionUtilities.ConvertStringToBool(expandedParameterValue)); + } + else if (parameterType == typeof(string)) + { + return InternalSetTaskParameter(parameter, expandedParameterValue); + } + else + { + return InternalSetTaskParameter(parameter, Convert.ChangeType(expandedParameterValue, parameterType, CultureInfo.InvariantCulture)); + } + } + + /// + /// Called on the local side. + /// + private bool SetParameterArray(TaskPropertyInfo parameter, Type parameterType, IList taskItems, ElementLocation parameterLocation) + { + TaskItem currentItem = null; + + try + { + // Loop through all the TaskItems in our arraylist, and convert them. + ArrayList finalTaskInputs = new ArrayList(); + + if (parameterType != typeof(ITaskItem[])) + { + foreach (TaskItem item in taskItems) + { + currentItem = item; + if (parameterType == typeof(string[])) + { + finalTaskInputs.Add(item.ItemSpec); + } + else if (parameterType == typeof(bool[])) + { + finalTaskInputs.Add(ConversionUtilities.ConvertStringToBool(item.ItemSpec)); + } + else + { + finalTaskInputs.Add(Convert.ChangeType(item.ItemSpec, parameterType.GetElementType(), CultureInfo.InvariantCulture)); + } + } + } + else + { + foreach (TaskItem item in taskItems) + { + // if we've been asked to remote these items then + // remember them so we can disconnect them from remoting later + RecordItemForDisconnectIfNecessary(item); + + finalTaskInputs.Add(item); + } + } + + return InternalSetTaskParameter(parameter, finalTaskInputs.ToArray(parameterType.GetElementType())); + } + catch (Exception ex) + { + if (ex is InvalidCastException || // invalid type + ex is ArgumentException || // can't convert to bool + ex is FormatException || // bad string representation of a type + ex is OverflowException) // overflow when converting string representation of a numerical type + { + ProjectErrorUtilities.ThrowInvalidProject + ( + parameterLocation, + "InvalidTaskParameterValueError", + currentItem.ItemSpec, + parameter.Name, + parameterType.FullName, + _taskName + ); + } + + throw; + } + } + + /// + /// Remember this TaskItem so that we can disconnect it when this Task has finished executing + /// Only if we're passing TaskItems to another AppDomain is this necessary. This call + /// Will make that determination for you. + /// + private void RecordItemForDisconnectIfNecessary(TaskItem item) + { + if (_remotedTaskItems != null) + { + // remember that we need to disconnect this item + _remotedTaskItems.Add(item); + } + } + + /// + /// Gets the outputs (as an array of ITaskItem) from the specified output parameter. + /// + private ITaskItem[] GetItemOutputs(TaskPropertyInfo parameter, ElementLocation parameterLocation) + { + object outputs = _taskFactoryWrapper.GetPropertyValue(_taskInstance, parameter); + + ITaskItem[] taskItemOutputs = outputs as ITaskItem[]; + if (null == taskItemOutputs) + { + taskItemOutputs = new ITaskItem[] { (ITaskItem)outputs }; + } + + return taskItemOutputs; + } + + /// + /// Gets the outputs (as an array of string values) from the specified output parameter. + /// + private string[] GetValueOutputs(TaskPropertyInfo parameter, ElementLocation parameterLocation) + { + object outputs = _taskFactoryWrapper.GetPropertyValue(_taskInstance, parameter); + + Array convertibleOutputs = parameter.PropertyType.IsArray ? (Array)outputs : new object[] { outputs }; + + if (convertibleOutputs == null) + { + return null; + } + + string[] stringOutputs = new string[convertibleOutputs.Length]; + for (int i = 0; i < convertibleOutputs.Length; i++) + { + object output = convertibleOutputs.GetValue(i); + if (output != null) + { + stringOutputs[i] = (string)Convert.ChangeType(output, typeof(string), CultureInfo.InvariantCulture); + } + } + + return stringOutputs; + } + + #endregion + + /// + /// Given the task name, this method tries to find the task. It uses the following search order: + /// 1) checks the tasks declared by the project, searching by exact name and task identity parameters + /// 2) checks the global task declarations (in *.TASKS in MSbuild bin dir), searching by exact name and task identity parameters + /// 3) checks the tasks declared by the project, searching by fuzzy match (missing namespace, etc.) and task identity parameters + /// 4) checks the global task declarations (in *.TASKS in MSbuild bin dir), searching by fuzzy match (missing namespace, etc.) and task identity parameters + /// 5) 1-4 again in order without the task identity parameters, to gather additional information for the user (if the task identity + /// parameters don't match, it is an error, but at least we can return them a more useful error in this case than just "could not + /// find task") + /// + /// The search ordering is meant to reduce the number of assemblies we scan, because loading assemblies can be expensive. + /// The tasks and assemblies declared by the project are scanned first, on the assumption that if the project declared + /// them, they are likely used. + /// + /// If the set of task identity parameters are defined, only tasks that match that identity are chosen. + /// + /// The Type of the task, or null if it was not found. + private TaskFactoryWrapper FindTaskInRegistry(IDictionary taskIdentityParameters) + { + TaskFactoryWrapper returnClass; + if (!_intrinsicTasks.TryGetValue(_taskName, out returnClass)) + { + returnClass = _projectInstance.TaskRegistry.GetRegisteredTask(_taskName, null, taskIdentityParameters, true /* exact match */, _targetLoggingContext, _taskLocation); + if (null == returnClass) + { + returnClass = _projectInstance.TaskRegistry.GetRegisteredTask(_taskName, null, taskIdentityParameters, false /* fuzzy match */, _targetLoggingContext, _taskLocation); + + if (null == returnClass) + { + returnClass = _projectInstance.TaskRegistry.GetRegisteredTask(_taskName, null, null, true /* exact match */, _targetLoggingContext, _taskLocation); + + if (null == returnClass) + { + returnClass = _projectInstance.TaskRegistry.GetRegisteredTask(_taskName, null, null, false /* fuzzy match */, _targetLoggingContext, _taskLocation); + + if (null == returnClass) + { + _targetLoggingContext.LogError + ( + new BuildEventFileInfo(_taskLocation), + "MissingTaskError", + _taskName, + _projectInstance.TaskRegistry.Toolset.ToolsPath + ); + + return returnClass; + } + } + + string usingTaskRuntime = null; + string usingTaskArchitecture = null; + string taskRuntime = null; + string taskArchitecture = null; + + if (returnClass.FactoryIdentityParameters != null) + { + returnClass.FactoryIdentityParameters.TryGetValue(XMakeAttributes.runtime, out usingTaskRuntime); + returnClass.FactoryIdentityParameters.TryGetValue(XMakeAttributes.architecture, out usingTaskArchitecture); + } + + taskIdentityParameters.TryGetValue(XMakeAttributes.runtime, out taskRuntime); + taskIdentityParameters.TryGetValue(XMakeAttributes.architecture, out taskArchitecture); + + _targetLoggingContext.LogError + ( + new BuildEventFileInfo(_taskLocation), + "TaskExistsButHasMismatchedIdentityError", + _taskName, + usingTaskRuntime ?? XMakeAttributes.MSBuildRuntimeValues.any, + usingTaskArchitecture ?? XMakeAttributes.MSBuildArchitectureValues.any, + taskRuntime ?? XMakeAttributes.MSBuildRuntimeValues.any, + taskArchitecture ?? XMakeAttributes.MSBuildArchitectureValues.any + ); + + // if we've logged this error, even though we've found something, we want to act like we didn't. + return null; + } + } + + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDDISABLEINTRINSICMSBUILDTASK"))) + { + // Map to an intrinsic task, if necessary. + if (String.Equals(returnClass.TaskFactory.TaskType.FullName, "Microsoft.Build.Tasks.MSBuild", StringComparison.OrdinalIgnoreCase)) + { + returnClass = new TaskFactoryWrapper(new IntrinsicTaskFactory(typeof(MSBuild)), new LoadedType(typeof(MSBuild), AssemblyLoadInfo.Create(Assembly.GetExecutingAssembly().FullName, null)), _taskName, null); + _intrinsicTasks[_taskName] = returnClass; + } + else if (String.Equals(returnClass.TaskFactory.TaskType.FullName, "Microsoft.Build.Tasks.CallTarget", StringComparison.OrdinalIgnoreCase)) + { + returnClass = new TaskFactoryWrapper(new IntrinsicTaskFactory(typeof(CallTarget)), new LoadedType(typeof(CallTarget), AssemblyLoadInfo.Create(Assembly.GetExecutingAssembly().FullName, null)), _taskName, null); + _intrinsicTasks[_taskName] = returnClass; + } + } + } + + return returnClass; + } + + /// + /// Instantiates the task. + /// + private ITask InstantiateTask(IDictionary taskIdentityParameters) + { + ITask task = null; + + try + { + AssemblyTaskFactory assemblyTaskFactory = _taskFactoryWrapper.TaskFactory as AssemblyTaskFactory; + if (assemblyTaskFactory != null) + { + task = assemblyTaskFactory.CreateTaskInstance(_taskLocation, _taskLoggingContext, _buildComponentHost, taskIdentityParameters, this.AppDomainSetup, this.IsOutOfProc); + } + else + { + TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); + try + { + ITaskFactory2 taskFactory2 = _taskFactoryWrapper.TaskFactory as ITaskFactory2; + + if (taskFactory2 == null) + { + task = _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + } + else + { + task = taskFactory2.CreateTask(loggingHost, taskIdentityParameters); + } + } + finally + { + loggingHost.MarkAsInactive(); + } + } + } + catch (InvalidCastException e) + { + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(_taskLocation), + "TaskInstantiationFailureErrorInvalidCast", + _taskName, + _taskFactoryWrapper.TaskFactory.FactoryName, + e.Message + ); + } + catch (TargetInvocationException e) + { + // Exception thrown by the called code itself + // Log the stack, so the task vendor can fix their code + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(_taskLocation), + "TaskInstantiationFailureError", + _taskName, + _taskFactoryWrapper.TaskFactory.FactoryName, + Environment.NewLine + e.InnerException.ToString() + ); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Reflection related exception + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(_taskLocation), + "TaskInstantiationFailureError", + _taskName, + _taskFactoryWrapper.TaskFactory.FactoryName, + e.Message + ); + } + + return task; + } + + /// + /// Set the specified parameter based on its type. + /// + private bool SetTaskParameter + ( + string parameterName, + string parameterValue, + ElementLocation parameterLocation, + bool isRequired, + out bool parameterSet + ) + { + bool success = false; + parameterSet = false; + + try + { + // check if the task has a .NET property corresponding to the parameter + TaskPropertyInfo parameter = _taskFactoryWrapper.GetProperty(parameterName); + + if (parameter != null) + { + Type parameterType = parameter.PropertyType; + + // try to set the parameter + if (TaskParameterTypeVerifier.IsValidScalarInputParameter(parameterType)) + { + success = InitializeTaskScalarParameter + ( + parameter, + parameterType, + parameterValue, + parameterLocation, + out parameterSet + ); + } + else if (TaskParameterTypeVerifier.IsValidVectorInputParameter(parameterType)) + { + success = InitializeTaskVectorParameter + ( + parameter, + parameterType, + parameterValue, + parameterLocation, + isRequired, + out parameterSet + ); + } + else + { + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(parameterLocation), + "UnsupportedTaskParameterTypeError", + parameterType.FullName, + parameter.Name, + _taskName + ); + } + + if (!success) + { + // flag an error if the parameter could not be set + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(parameterLocation), + "InvalidTaskAttributeError", + parameterName, + parameterValue, + _taskName + ); + } + } + else + { + // flag an error if we find a parameter that has no .NET property equivalent + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(parameterLocation), + "UnexpectedTaskAttribute", + parameterName, + _taskName + ); + } + } + catch (AmbiguousMatchException) + { + _taskLoggingContext.LogError + ( + new BuildEventFileInfo(parameterLocation), + "AmbiguousTaskParameterError", + _taskName, + parameterName + ); + } + catch (ArgumentException) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + parameterLocation, + "SetAccessorNotAvailableOnTaskParameter", + parameterName, + _taskName + ); + } + + return success; + } + + /// + /// Given an instantiated task, this helper method sets the specified scalar parameter based on its type. + /// + private bool InitializeTaskScalarParameter + ( + TaskPropertyInfo parameter, + Type parameterType, + string parameterValue, + ElementLocation parameterLocation, + out bool taskParameterSet + ) + { + taskParameterSet = false; + + bool success = false; + + try + { + if (parameterType == typeof(ITaskItem)) + { + // We don't know how many items we're going to end up with, but we'll + // keep adding them to this arraylist as we find them. + IList finalTaskItems = _batchBucket.Expander.ExpandIntoTaskItemsLeaveEscaped(parameterValue, ExpanderOptions.ExpandAll, parameterLocation); + + if (finalTaskItems.Count == 0) + { + success = true; + } + else + { + if (finalTaskItems.Count != 1) + { + // We only allow a single item to be passed into a parameter of ITaskItem. + + // Some of the computation (expansion) here is expensive, so don't make the above + // "if" statement directly part of the first param to VerifyThrowInvalidProject. + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + false, + parameterLocation, + "CannotPassMultipleItemsIntoScalarParameter", + _batchBucket.Expander.ExpandIntoStringAndUnescape(parameterValue, ExpanderOptions.ExpandAll, parameterLocation), + parameter.Name, + parameterType.FullName, + _taskName + ); + } + + RecordItemForDisconnectIfNecessary(finalTaskItems[0]); + + success = SetTaskItemParameter(parameter, finalTaskItems[0], parameterLocation); + + taskParameterSet = true; + } + } + else + { + // Expand out all the metadata, properties, and item vectors in the string. + string expandedParameterValue = _batchBucket.Expander.ExpandIntoStringAndUnescape(parameterValue, ExpanderOptions.ExpandAll, parameterLocation); + + if (expandedParameterValue.Length == 0) + { + success = true; + } + else + { + success = SetValueParameter(parameter, parameterType, expandedParameterValue, parameterLocation); + taskParameterSet = true; + } + } + } + catch (Exception ex) + { + if (ex is InvalidCastException || // invalid type + ex is ArgumentException || // can't convert to bool + ex is FormatException || // bad string representation of a type + ex is OverflowException) // overflow when converting string representation of a numerical type + { + ProjectErrorUtilities.ThrowInvalidProject + ( + parameterLocation, + "InvalidTaskParameterValueError", + _batchBucket.Expander.ExpandIntoStringAndUnescape(parameterValue, ExpanderOptions.ExpandAll, parameterLocation), + parameter.Name, + parameterType.FullName, + _taskName + ); + } + + throw; + } + + return success; + } + + /// + /// Given an instantiated task, this helper method sets the specified vector parameter. Vector parameters can be composed + /// of multiple item vectors. The semicolon is the only separator allowed, and white space around the semicolon is + /// ignored. Any item separator strings are not allowed, and embedded item vectors are not allowed. + /// + /// This method is marked "internal" for unit-testing purposes only -- it should be "private" ideally. + /// + /// If @(CPPFiles) is a vector for the files a.cpp and b.cpp, and @(IDLFiles) is a vector for the files a.idl and b.idl: + /// + /// "@(CPPFiles)" converts to { a.cpp, b.cpp } + /// + /// "@(CPPFiles); c.cpp; @(IDLFiles); c.idl" converts to { a.cpp, b.cpp, c.cpp, a.idl, b.idl, c.idl } + /// + /// "@(CPPFiles,';')" converts to <error> + /// + /// "xxx@(CPPFiles)xxx" converts to <error> + /// + private bool InitializeTaskVectorParameter + ( + TaskPropertyInfo parameter, + Type parameterType, + string parameterValue, + ElementLocation parameterLocation, + bool isRequired, + out bool taskParameterSet + ) + { + ErrorUtilities.VerifyThrow(parameterValue != null, "Didn't expect null parameterValue in InitializeTaskVectorParameter"); + + taskParameterSet = false; + bool success = false; + IList finalTaskItems = _batchBucket.Expander.ExpandIntoTaskItemsLeaveEscaped(parameterValue, ExpanderOptions.ExpandAll, parameterLocation); + + // If there were no items, don't change the parameter's value. EXCEPT if it's marked as a required + // parameter, in which case we made an explicit decision to pass in an empty array. This is + // to avoid project authors having to add Conditions on all their tasks to avoid calling them + // when a particular item list is empty. This way, we just call the task with an empty list, + // the task will loop over an empty list, and return quickly. + if ((finalTaskItems.Count > 0) || isRequired) + { + // If the task parameter is not a ITaskItem[], then we need to convert + // all the TaskItem's in our arraylist to the appropriate datatype. + success = SetParameterArray(parameter, parameterType, finalTaskItems, parameterLocation); + taskParameterSet = true; + } + else + { + success = true; + } + + return success; + } + + /// + /// Variation to handle arrays, to help with logging the parameters. + /// + /// + /// Logging currently enabled only by an env var. + /// + private bool InternalSetTaskParameter(TaskPropertyInfo parameter, IList parameterValue) + { + if (_logTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents && parameterValue.Count > 0) + { + string parameterText = ResourceUtilities.FormatResourceString("TaskParameterPrefix"); + parameterText = ItemGroupLoggingHelper.GetParameterText(parameterText, parameter.Name, parameterValue); + _taskLoggingContext.LogCommentFromText(MessageImportance.Low, parameterText); + } + + return InternalSetTaskParameter(parameter, (object)parameterValue); + } + + /// + /// Given an instantiated task, this helper method sets the specified parameter + /// + private bool InternalSetTaskParameter + ( + TaskPropertyInfo parameter, + object parameterValue + ) + { + bool success = false; + + // Logging currently enabled only by an env var. + if (_logTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents) + { + // If the type is a list, we already logged the parameters + if (!(parameterValue is IList)) + { + _taskLoggingContext.LogCommentFromText( + MessageImportance.Low, + ResourceUtilities.FormatResourceString("TaskParameterPrefix") + parameter.Name + "=" + ItemGroupLoggingHelper.GetStringFromParameterValue(parameterValue)); + } + } + + try + { + _taskFactoryWrapper.SetPropertyValue(_taskInstance, parameter, parameterValue); + success = true; + } + catch (LoggerException) + { + // if a logger has failed, abort immediately + // Polite logger failure + throw; + } + catch (InternalLoggerException) + { + // Logger threw arbitrary exception + throw; + } + catch (TargetInvocationException e) + { + // handle any exception thrown by the task's setter itself + // At this point, the interesting stack is the internal exception. + // Log the task line number, whatever the value of ContinueOnError; + // because this will be a hard error anyway. + + // Exception thrown by the called code itself + // Log the stack, so the task vendor can fix their code + _taskLoggingContext.LogFatalTaskError + ( + new BuildEventFileInfo(_taskLocation), + e.InnerException, + _taskName + ); + } + catch (Exception e) + { + // Catching Exception, but rethrowing unless it's a well-known exception. + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + _taskLoggingContext.LogFatalTaskError + ( + new BuildEventFileInfo(_taskLocation), + e, + _taskName + ); + } + + return success; + } + + /// + /// Gets task item outputs + /// + private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetName, ITaskItem[] outputs, ElementLocation parameterLocation) + { + // if the task has generated outputs (if it didn't, don't do anything) + if (outputs != null) + { + if (outputTargetIsItem) + { + foreach (ITaskItem output in outputs) + { + // if individual items in the array are null, ignore them + if (output != null) + { + ProjectItemInstance newItem; + + ProjectItemInstance.TaskItem outputAsProjectItem = output as ProjectItemInstance.TaskItem; + string parameterLocationEscaped = EscapingUtilities.EscapeWithCaching(parameterLocation.File); + + if (outputAsProjectItem != null) + { + // The common case -- all items involved are Microsoft.Build.Execution.ProjectItemInstance.TaskItems. + // Furthermore, because that is true, we know by definition that they also implement ITaskItem2. + newItem = new ProjectItemInstance(_projectInstance, outputTargetName, outputAsProjectItem.IncludeEscaped, parameterLocationEscaped); + + newItem.SetMetadata(outputAsProjectItem.MetadataCollection); // copy-on-write! + } + else + { + ITaskItem2 outputAsITaskItem2 = output as ITaskItem2; + + if (outputAsITaskItem2 != null) + { + // Probably a Microsoft.Build.Utilities.TaskItem. Not quite as good, but we can still preserve escaping. + newItem = new ProjectItemInstance(_projectInstance, outputTargetName, outputAsITaskItem2.EvaluatedIncludeEscaped, parameterLocationEscaped); + + // It would be nice to be copy-on-write here, but Utilities.TaskItem doesn't know about CopyOnWritePropertyDictionary. + foreach (DictionaryEntry entry in outputAsITaskItem2.CloneCustomMetadataEscaped()) + { + newItem.SetMetadataOnTaskOutput((string)entry.Key, (string)entry.Value); + } + } + else + { + // Not a ProjectItemInstance.TaskItem or even a ITaskItem2, so we have to fake it. + // Setting an item spec expects the escaped value, as does setting metadata. + newItem = new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output.ItemSpec), parameterLocationEscaped); + + foreach (DictionaryEntry entry in output.CloneCustomMetadata()) + { + newItem.SetMetadataOnTaskOutput((string)entry.Key, EscapingUtilities.Escape((string)entry.Value)); + } + } + } + + _batchBucket.Lookup.AddNewItem(newItem); + } + } + + if (_logTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents && outputs.Length > 0) + { + string parameterText = ItemGroupLoggingHelper.GetParameterText( + ResourceUtilities.FormatResourceString("OutputItemParameterMessagePrefix"), + outputTargetName, + outputs); + + _taskLoggingContext.LogCommentFromText(MessageImportance.Low, parameterText); + } + } + else + { + // to store an ITaskItem array in a property, join all the item-specs with semi-colons to make the + // property value, and ignore/discard the attributes on the ITaskItems. + // + // An empty ITaskItem[] should create a blank value property, for compatibility. + StringBuilder joinedOutputs = (outputs.Length == 0) ? new StringBuilder() : null; + + foreach (ITaskItem output in outputs) + { + // if individual items in the array are null, ignore them + if (output != null) + { + joinedOutputs = joinedOutputs ?? new StringBuilder(); + + if (joinedOutputs.Length > 0) + { + joinedOutputs.Append(';'); + } + + ITaskItem2 outputAsITaskItem2 = output as ITaskItem2; + + if (outputAsITaskItem2 != null) + { + joinedOutputs.Append(outputAsITaskItem2.EvaluatedIncludeEscaped); + } + else + { + joinedOutputs.Append(EscapingUtilities.Escape(output.ItemSpec)); + } + } + } + + if (joinedOutputs != null) + { + var outputString = joinedOutputs.ToString(); + if (_logTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents) + { + _taskLoggingContext.LogComment(MessageImportance.Low, "OutputPropertyLogMessage", outputTargetName, outputString); + } + + _batchBucket.Lookup.SetProperty(ProjectPropertyInstance.Create(outputTargetName, outputString, parameterLocation, _projectInstance.IsImmutable)); + } + } + } + } + + /// + /// Gather task outputs in array form + /// + private void GatherArrayStringAndValueOutputs(bool outputTargetIsItem, string outputTargetName, TaskPropertyInfo parameter, string[] outputs, ElementLocation parameterLocation) + { + // if the task has generated outputs (if it didn't, don't do anything) + if (outputs != null) + { + if (outputTargetIsItem) + { + // to store the outputs as items, use the string representations of the outputs as item-specs + foreach (string output in outputs) + { + // if individual outputs in the array are null, ignore them + if (output != null) + { + // attempting to put an empty string into an item is a no-op. + if (output.Length > 0) + { + _batchBucket.Lookup.AddNewItem(new ProjectItemInstance(_projectInstance, outputTargetName, EscapingUtilities.Escape(output), EscapingUtilities.Escape(parameterLocation.File))); + } + } + } + + if (_logTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents && outputs.Length > 0) + { + string parameterText = ItemGroupLoggingHelper.GetParameterText(ResourceUtilities.FormatResourceString("OutputItemParameterMessagePrefix"), outputTargetName, outputs); + _taskLoggingContext.LogCommentFromText(MessageImportance.Low, parameterText); + } + } + else + { + // to store an object array in a property, join all the string representations of the objects with + // semi-colons to make the property value + // + // An empty ITaskItem[] should create a blank value property, for compatibility. + StringBuilder joinedOutputs = (outputs.Length == 0) ? new StringBuilder() : null; + + foreach (string output in outputs) + { + // if individual outputs in the array are null, ignore them + if (output != null) + { + joinedOutputs = joinedOutputs ?? new StringBuilder(); + + if (joinedOutputs.Length > 0) + { + joinedOutputs.Append(';'); + } + + joinedOutputs.Append(EscapingUtilities.Escape(output)); + } + } + + if (joinedOutputs != null) + { + var outputString = joinedOutputs.ToString(); + if (_logTaskInputs && !_taskLoggingContext.LoggingService.OnlyLogCriticalEvents) + { + _taskLoggingContext.LogComment(MessageImportance.Low, "OutputPropertyLogMessage", outputTargetName, outputString); + } + + _batchBucket.Lookup.SetProperty(ProjectPropertyInstance.Create(outputTargetName, outputString, parameterLocation, _projectInstance.IsImmutable)); + } + } + } + } + + /// + /// Finds all the task properties that are required. + /// Returns them as keys in a dictionary. + /// + /// Gets a list of properties which are required. + private IDictionary GetNamesOfPropertiesWithRequiredAttribute() + { + ErrorUtilities.VerifyThrow(_taskFactoryWrapper != null, "Expected taskFactoryWrapper to not be null"); + IDictionary requiredParameters = null; + + try + { + requiredParameters = _taskFactoryWrapper.GetNamesOfPropertiesWithRequiredAttribute; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + // Reflection related exception + _targetLoggingContext.LogError(new BuildEventFileInfo(_taskLocation), "AttributeTypeLoadError", _taskName, e.Message); + + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _taskLocation, "TaskDeclarationOrUsageError", _taskName); + } + + return requiredParameters; + } + + /// + /// Show a message that cancel has not yet finished. + /// + private void DisplayCancelWaitMessage() + { + string warningCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out warningCode, out helpKeyword, "UnableToCancelTask", _taskName); + try + { + _taskLoggingContext.LogWarningFromText(new BuildEventFileInfo(_taskLocation), null, warningCode, helpKeyword, message); + } + catch (InternalErrorException) // BUGBUG, should never catch this + { + // We can get an exception from this when we encounter a race between a task finishing and a cancel occurring. In this situation + // if the task logging context is no longer valid, we choose to eat the exception because we can't log the message anyway. + if (_taskLoggingContext.IsValid) + { + throw; + } + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/ConcurrentQueueExtensions.cs b/src/XMakeBuildEngine/Collections/ConcurrentQueueExtensions.cs new file mode 100644 index 00000000000..3738c064367 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/ConcurrentQueueExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Extension methods for the ConcurrentQueue. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// The extensions class for ConcurrentQueue<T> + /// + internal static class ConcurrentQueueExtensions + { + /// + /// The dequeue method. + /// + /// The type contained within the queue + static public T Dequeue(this ConcurrentQueue stack) where T : class + { + T result = null; + ErrorUtilities.VerifyThrow(stack.TryDequeue(out result), "Unable to dequeue from queue"); + return result; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Collections/ConcurrentStackExtensions.cs b/src/XMakeBuildEngine/Collections/ConcurrentStackExtensions.cs new file mode 100644 index 00000000000..de5842219f8 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/ConcurrentStackExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Extension methods for the ConcurrentStack. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// The extensions class for ConcurrentStack<T> + /// + internal static class ConcurrentStackExtensions + { + /// + /// The peek method. + /// + /// The type contained within the stack. + static public T Peek(this ConcurrentStack stack) where T : class + { + T result = null; + ErrorUtilities.VerifyThrow(stack.TryPeek(out result), "Unable to peek from stack"); + return result; + } + + /// + /// The pop method. + /// + /// The type contained within the stack. + static public T Pop(this ConcurrentStack stack) where T : class + { + T result = null; + ErrorUtilities.VerifyThrow(stack.TryPop(out result), "Unable to pop from stack"); + return result; + } + } +} diff --git a/src/XMakeBuildEngine/Collections/ConvertingEnumerable.cs b/src/XMakeBuildEngine/Collections/ConvertingEnumerable.cs new file mode 100644 index 00000000000..b8d26b0dde1 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/ConvertingEnumerable.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Enumerable that converts each entry as it is retrieved. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.Collections +{ + /// + /// Enumerable that uses a provided delegate to + /// convert each item from a backing enumerator as it is returned. + /// + /// Type of underlying enumerator + /// Type returned + internal class ConvertingEnumerable : IEnumerable + { + /// + /// Enumerable behind this one + /// + private IEnumerable _backingEnumerable; + + /// + /// Converter delegate used on each item in the backing enumerable as it is returned + /// + private Converter _converter; + + /// + /// Constructor + /// + internal ConvertingEnumerable(IEnumerable backingEnumerable, Converter converter) + { + _backingEnumerable = backingEnumerable; + _converter = converter; + } + + /// + /// Gets the converting enumerator + /// + public IEnumerator GetEnumerator() + { + return new ConvertingEnumerator(_backingEnumerable.GetEnumerator(), _converter); + } + + /// + /// IEnumerable version of GetEnumerator + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Enumerable that uses a provided delegate to + /// convert each item from a backing enumerator as it is returned. + /// + /// Type of underlying enumerator + /// Type returned + private struct ConvertingEnumerator : IEnumerator + { + /// + /// Enumerator behind this one + /// + private IEnumerator _backingEnumerator; + + /// + /// Converter delegate used on each item in the backing enumerator as it is returned + /// + private Converter _converter; + + /// + /// Constructor + /// + internal ConvertingEnumerator(IEnumerator backingEnumerator, Converter converter) + { + _backingEnumerator = backingEnumerator; + _converter = converter; + } + + /// + /// Get the current element, converted + /// + public TTo2 Current + { + get + { + TFrom2 current = _backingEnumerator.Current; + + return _converter(current); + } + } + + /// + /// Get the current element, converted + /// + Object IEnumerator.Current + { + get + { + return Current; + } + } + + /// + /// Move to the next element + /// + public bool MoveNext() + { + return _backingEnumerator.MoveNext(); + } + + /// + /// Reset the enumerator + /// + public void Reset() + { + _backingEnumerator.Reset(); + } + + /// + /// Dispose of the enumerator + /// + public void Dispose() + { + _backingEnumerator.Dispose(); + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/CopyOnReadEnumerable.cs b/src/XMakeBuildEngine/Collections/CopyOnReadEnumerable.cs new file mode 100644 index 00000000000..498d0828305 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/CopyOnReadEnumerable.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A class which implements IEnumerable by creating an optionally-deep copy of the backing collection. +//----------------------------------------------------------------------- + +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// A class which implements IEnumerable by creating an optionally-deep copy of the backing collection. + /// + /// + /// If the type contained in the collection implements IDeepCloneable then the copies will be deep clones instead + /// of mere reference copies. + /// is thread safe for concurrent access. + /// + /// The type contained in the backing collection. + internal class CopyOnReadEnumerable : IEnumerable + { + /// + /// The backing collection. + /// + private IEnumerable _backingEnumerable; + + /// + /// The object used to synchronize access for copying. + /// + private object _syncRoot; + + /// + /// Constructor. + /// + /// The collection which serves as a source for enumeration. + /// The object used to synchronize access for copying. + public CopyOnReadEnumerable(IEnumerable backingEnumerable, object syncRoot) + { + ErrorUtilities.VerifyThrowArgumentNull(backingEnumerable, "backingCollection"); + ErrorUtilities.VerifyThrowArgumentNull(syncRoot, "syncRoot"); + + _backingEnumerable = backingEnumerable; + _syncRoot = syncRoot; + } + + #region IEnumerable Members + + /// + /// Returns an enumerator over the collection. + /// + /// The enumerator. + public IEnumerator GetEnumerator() + { + List list; + ICollection backingCollection = _backingEnumerable as ICollection; + if (backingCollection != null) + { + list = new List(backingCollection.Count); + } + else + { + list = new List(); + } + + bool isCloneable = false; + bool checkForCloneable = true; + lock (_syncRoot) + { + foreach (T item in _backingEnumerable) + { + if (checkForCloneable) + { + if (item is IDeepCloneable) + { + isCloneable = true; + } + + checkForCloneable = false; + } + + T copiedItem = isCloneable ? (item as IDeepCloneable).DeepClone() : item; + list.Add(copiedItem); + } + } + + return list.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an numerator over the collection. + /// + /// The enumerator. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Collections/CopyOnWritePropertyDictionary.cs b/src/XMakeBuildEngine/Collections/CopyOnWritePropertyDictionary.cs new file mode 100644 index 00000000000..80208cc3a35 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/CopyOnWritePropertyDictionary.cs @@ -0,0 +1,537 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dictionary over properties or metadata with copy-on-write semantics. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Collections +{ + /// + /// A dictionary of unordered property or metadata name/value pairs, with copy-on-write semantics. + /// + /// The copy-on-write semantics are only possible if the contained type is immutable, which currently + /// means it can only be used for ProjectMetadataInstance's. + /// USE THIS DICTIONARY ONLY FOR IMMUTABLE TYPES. OTHERWISE USE PROPERTYDICTIONARY. + /// + /// + /// + /// The value that this adds over IDictionary<string, T> is: + /// - enforces that key = T.Name + /// - default enumerator is over values + /// - (marginal) enforces the correct key comparer + /// - potentially makes copy on write possible + /// + /// Really a Dictionary<string, T> where the key (the name) is obtained from IKeyed.Key. + /// Is not observable, so if clients wish to observe modifications they must mediate them themselves and + /// either not expose this collection or expose it through a readonly wrapper. + /// + /// At various places in this class locks are taken on the backing collection. The reason for this is to allow + /// this class to be asynchronously enumerated. This is accomplished by the CopyOnReadEnumerable which will + /// lock the backing collection when it does its deep cloning. This prevents asynchronous access from corrupting + /// the state of the enumeration until the collection has been fully copied. + /// + /// The use of a CopyOnWriteDictionary does not reduce the concurrency of this collection, because CopyOnWriteDictionary + /// offers the same concurrency guarantees (concurrent readers OR single writer) for EACH user of the dictionary. + /// + /// Since we use the mutable ignore case comparer we need to make sure that we lock our self before we call the comparer since the comparer can call back + /// into this dictionary which could cause a deadlock if another thread is also accessing another method in the dictionary. + /// + /// Property or Metadata class type to store + [DebuggerDisplay("#Entries={Count}")] + internal sealed class CopyOnWritePropertyDictionary : IEnumerable, IEquatable>, IPropertyProvider, IDictionary + where T : class, IKeyed, IValued, IEquatable, IImmutable + { + /// + /// Backing dictionary + /// + private readonly CopyOnWriteDictionary _properties; + + /// + /// Comparer whose start and end indexes we can manipulate as necessary. + /// + private readonly MSBuildNameIgnoreCaseComparer _comparer = MSBuildNameIgnoreCaseComparer.Mutable; + + /// + /// Creates empty dictionary + /// + public CopyOnWritePropertyDictionary() + { + // Tracing.Record("New COWD1"); + _properties = new CopyOnWriteDictionary(_comparer); + } + + /// + /// Creates empty dictionary with specified initial capacity + /// + public CopyOnWritePropertyDictionary(int capacity) + { + // Tracing.Record("New COWD2"); + _properties = new CopyOnWriteDictionary(capacity, _comparer); + } + + /// + /// Cloning constructor, with deferred cloning semantics + /// + private CopyOnWritePropertyDictionary(CopyOnWritePropertyDictionary that) + { + _properties = that._properties.Clone(); // copy on write! + } + + /// + /// Accessor for the list of property names + /// + ICollection IDictionary.Keys + { + get + { + return PropertyNames; + } + } + + /// + /// Accessor for the list of properties + /// + ICollection IDictionary.Values + { + get + { + lock (_properties) + { + return _properties.Values; + } + } + } + + /// + /// Returns the number of properties in the collection + /// + int ICollection>.Count + { + get + { + lock (_properties) + { + return _properties.Count; + } + } + } + + /// + /// Whether the collection is read-only. + /// + bool ICollection>.IsReadOnly + { + get + { + return false; + } + } + + /// + /// Returns the number of property in the collection. + /// + internal int Count + { + get + { + lock (_properties) + { + return _properties.Count; + } + } + } + + /// + /// Retrieves a collection containing the names of all the properties present in the dictionary. + /// + internal ICollection PropertyNames + { + get + { + lock (_properties) + { + return _properties.Keys; + } + } + } + + /// + /// Get the property with the specified name, or null if none exists. + /// Sets the property with the specified name, overwriting it if already exists. + /// + /// + /// Unlike Dictionary<K,V>[K], the getter returns null instead of throwing if the key does not exist. + /// This better matches the semantics of property, which are considered to have a blank value if they + /// are not defined. + /// + T IDictionary.this[string name] + { + // The backing properties dictionary is locked in the indexor + get + { + return this[name]; + } + + set + { + this[name] = value; + } + } + + /// + /// Get the property with the specified name, or null if none exists. + /// Sets the property with the specified name, overwriting it if already exists. + /// + /// + /// Unlike Dictionary<K,V>[K], the getter returns null instead of throwing if the key does not exist. + /// This better matches the semantics of property, which are considered to have a blank value if they + /// are not defined. + /// + internal T this[string name] + { + get + { + // We don't want to check for a zero length name here, since that is a valid name + // and should return a null instance which will be interpreted as blank + T projectProperty; + lock (_properties) + { + _properties.TryGetValue(name, out projectProperty); + } + + return projectProperty; + } + + set + { + ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value"); + ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key"); + Set(value); + } + } + + /// + /// Returns an enumerable which clones the properties + /// + /// Returns a cloning enumerable. + public IEnumerable GetCopyOnReadEnumerable() + { + return new CopyOnReadEnumerable(this, _properties); + } + + /// + /// Returns true if a property with the specified name is present in the collection, + /// otherwise false. + /// + public bool Contains(string name) + { + return ((IDictionary)this).ContainsKey(name); + } + + /// + /// Empties the collection + /// + public void Clear() + { + lock (_properties) + { + _properties.Clear(); + } + } + + /// + /// Gets an enumerator over all the properties in the collection + /// Enumeration is in undefined order + /// + public IEnumerator GetEnumerator() + { + lock (_properties) + { + return _properties.Values.GetEnumerator(); + } + } + + /// + /// Get an enumerator over entries + /// + IEnumerator IEnumerable.GetEnumerator() + { + lock (_properties) + { + return ((IEnumerable)_properties.Values).GetEnumerator(); + } + } + + #region IEquatable> Members + + /// + /// Compares two property dictionaries for equivalence. They are equal if each contains the same properties with the + /// same values as the other, unequal otherwise. + /// + /// The dictionary to which this should be compared + /// True if they are equivalent, false otherwise. + public bool Equals(CopyOnWritePropertyDictionary other) + { + if (null == other) + { + return false; + } + + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (Count != other.Count) + { + return false; + } + + lock (_properties) + { + foreach (T leftProp in this) + { + T rightProp = other[leftProp.Key]; + if (rightProp == null || !EqualityComparer.Default.Equals(leftProp, rightProp)) + { + return false; + } + } + } + + return true; + } + + #endregion + + /// + /// Get the property with the specified name or null if it is not present + /// + T IPropertyProvider.GetProperty(string name) + { + return this[name]; + } + + /// + /// Get the property with the specified name or null if it is not present. + /// Name is the segment of the provided string with the provided start and end indexes. + /// + T IPropertyProvider.GetProperty(string name, int startIndex, int endIndex) + { + lock (_properties) + { + if (startIndex == 0 && endIndex == name.Length - 1) + { + return this[name]; + } + + T returnValue = _comparer.GetValueWithConstraints(this, name, startIndex, endIndex); + return returnValue; + } + } + + #region IDictionary Members + + /// + /// Adds a property + /// + void IDictionary.Add(string key, T value) + { + ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key"); + Set(value); + } + + /// + /// Returns true if the dictionary contains the key + /// + bool IDictionary.ContainsKey(string key) + { + return PropertyNames.Contains(key); + } + + /// + /// Removes a property + /// + bool IDictionary.Remove(string key) + { + // Backing properties are locked in the remove method + return Remove(key); + } + + /// + /// Attempts to retrieve the a property. + /// + bool IDictionary.TryGetValue(string key, out T value) + { + value = this[key]; + + return (value != null); + } + + #endregion + + #region ICollection> Members + + /// + /// Adds a property + /// + void ICollection>.Add(KeyValuePair item) + { + ((IDictionary)this).Add(item.Key, item.Value); + } + + /// + /// Clears the property collection + /// + void ICollection>.Clear() + { + Clear(); + } + + /// + /// Checks for a property in the collection + /// + bool ICollection>.Contains(KeyValuePair item) + { + T value; + lock (_properties) + { + if (_properties.TryGetValue(item.Key, out value)) + { + return EqualityComparer.Default.Equals(value, item.Value); + } + } + + return false; + } + + /// + /// Not implemented + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ErrorUtilities.ThrowInternalError("CopyTo is not supported on PropertyDictionary."); + } + + /// + /// Removes a property from the collection + /// + bool ICollection>.Remove(KeyValuePair item) + { + ErrorUtilities.VerifyThrow(item.Key == item.Value.Key, "Key must match value's key"); + return ((IDictionary)this).Remove(item.Key); + } + + #endregion + + #region IEnumerable> Members + + /// + /// Get an enumerator over the entries. + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + lock (_properties) + { + return _properties.GetEnumerator(); + } + } + + #endregion + + /// + /// Removes any property with the specified name. + /// Returns true if the property was in the collection, otherwise false. + /// + internal bool Remove(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + lock (_properties) + { + bool result = _properties.Remove(name); + return result; + } + } + + /// + /// Add the specified property to the collection. + /// Overwrites any property with the same name already in the collection. + /// To remove a property, use Remove(...) instead. + /// + internal void Set(T projectProperty) + { + ErrorUtilities.VerifyThrowArgumentNull(projectProperty, "projectProperty"); + + lock (_properties) + { + _properties[projectProperty.Key] = projectProperty; + } + } + + /// + /// Adds the specified properties to this dictionary. + /// + /// An enumerator over the properties to add. + internal void ImportProperties(IEnumerable other) + { + // Properties are locked in the set method + foreach (T property in other) + { + Set(property); + } + } + + /// + /// Removes the specified properties from this dictionary + /// + /// An enumerator over the properties to remove. + internal void RemoveProperties(IEnumerable other) + { + // Properties are locked in the remove method + foreach (T property in other) + { + Remove(property.Key); + } + } + + /// + /// Helper to convert into a read-only dictionary of string, string. + /// + internal IDictionary ToDictionary() + { + Dictionary dictionary = null; + + lock (_properties) + { + dictionary = new Dictionary(_properties.Count, StringComparer.OrdinalIgnoreCase); + foreach (T property in this) + { + dictionary[property.Key] = property.EscapedValue; + } + } + + return dictionary; + } + + /// + /// Clone. As we're copy on write, this + /// should be cheap. + /// + internal CopyOnWritePropertyDictionary DeepClone() + { + return new CopyOnWritePropertyDictionary(this); + } + } +} diff --git a/src/XMakeBuildEngine/Collections/FilteringEnumerable.cs b/src/XMakeBuildEngine/Collections/FilteringEnumerable.cs new file mode 100644 index 00000000000..407d339a867 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/FilteringEnumerable.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Enumerable providing a filtered view of a collection +//----------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Build.Collections +{ + /// + /// An enumerable over the provided collection that only exposes the members of the + /// collection that have the type for which it is specialized for + /// + /// + /// If the 'is' checks are expensive, a field containing a type enumeration could be used instead. + /// + /// Type of element in the underlying collection + /// Type to filter for + internal struct FilteringEnumerable : IEnumerable + where Filter : class, Base + { + /// + /// Backing collection + /// + private readonly IEnumerable _enumerable; + + /// + /// Constructor accepting the backing collection + /// Backing collection may be null, indicating an empty collection + /// + internal FilteringEnumerable(IEnumerable enumerable) + { + _enumerable = enumerable; + } + + /// + /// Gets an enumerator over all the elements in the backing collection that meet + /// the filter criteria + /// + public IEnumerator GetEnumerator() + { + if (_enumerable == null) + { + return ReadOnlyEmptyList.Instance.GetEnumerator(); + } + + return new FilteringEnumerator(_enumerable.GetEnumerator()); + } + + /// + /// Gets an enumerator over all the elements in the backing collection that meet + /// the filter criteria + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Custom enumerator that allows enumeration over only the items in + /// the collection that are of the type it is specialized for. + /// + /// Type of element in the underlying collection + /// Type to filter for + private struct FilteringEnumerator : IEnumerator + where Filter2 : class, Base2 + { + /// + /// The real enumerator + /// + private IEnumerator _enumerator; + + /// + /// Constructor initializing with the real enumerator + /// + internal FilteringEnumerator(IEnumerator enumerator) + { + _enumerator = enumerator; + } + + /// + /// Get the current, if any, otherwise null + /// + /// + /// Current is undefined if enumerator is before the start of the collection + /// or if MoveNext() returned false + /// + public Filter2 Current + { + get { return _enumerator.Current as Filter2; } + } + + /// + /// Get the current, if any + /// + /// + /// Current is undefined if enumerator is before the start of the collection + /// or if MoveNext() returned false + /// + object System.Collections.IEnumerator.Current + { + get { return _enumerator.Current; } + } + + /// + /// Dispose + /// + public void Dispose() + { + _enumerator.Dispose(); + } + + /// + /// Move to the next object of the specialized type, if any, and return true; + /// otherwise return false + /// + public bool MoveNext() + { + bool result; + do + { + result = _enumerator.MoveNext(); + } + while (result && !(_enumerator.Current is Filter2)); + + return result; + } + + /// + /// Reset + /// + public void Reset() + { + _enumerator.Reset(); + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/HashTableUtility.cs b/src/XMakeBuildEngine/Collections/HashTableUtility.cs new file mode 100644 index 00000000000..32ff8d0279e --- /dev/null +++ b/src/XMakeBuildEngine/Collections/HashTableUtility.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.Build.Collections +{ + /// + /// Tools for working with Hashtables. + /// + internal static class HashTableUtility + { + /// + /// Compares the given hashtables. + /// + /// May be null + /// May be null + /// + /// -1, if first hashtable is "less than" the second one + /// 0, if hashtables have identical keys and equivalent (case-insensitive) values + /// +1, if first hashtable is "greater than" the second one + /// + internal static int Compare(IDictionary h1, IDictionary h2) + { + if (h1 == h2) // eg null + { + return 0; + } + else if (h1 == null) + { + return -1; + } + else if (h2 == null) + { + return +1; + } + + int comparison = Math.Sign(h1.Count - h2.Count); + + if (comparison == 0) + { + foreach (KeyValuePair h1Entry in h1) + { + // NOTE: String.Compare() allows null values -- any string, + // including the empty string (""), compares greater than a + // null reference, and two null references compare equal + comparison = String.Compare(h1Entry.Value, h2[h1Entry.Key], + StringComparison.OrdinalIgnoreCase); + + if (comparison != 0) + { + break; + } + } + } + + return comparison; + } + } +} diff --git a/src/XMakeBuildEngine/Collections/IDeepCloneable.cs b/src/XMakeBuildEngine/Collections/IDeepCloneable.cs new file mode 100644 index 00000000000..8522b1f3a91 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/IDeepCloneable.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An interface for objects supporting deep clone semantics. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Collections +{ + /// + /// An interface representing an item which can clone itself. + /// + /// The type returned by the clone operation. + internal interface IDeepCloneable + { + /// + /// Creates a clone of the item where no data references are shared. Changes made to the clone + /// do not affect the original item. + /// + /// The cloned item. + T DeepClone(); + } +} diff --git a/src/XMakeBuildEngine/Collections/IImmutable.cs b/src/XMakeBuildEngine/Collections/IImmutable.cs new file mode 100644 index 00000000000..d4a81d4b35f --- /dev/null +++ b/src/XMakeBuildEngine/Collections/IImmutable.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface indicating a type is immutable, to constrain generic types. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Collections +{ + /// + /// Interface indicating a type is immutable, to constrain generic types. + /// + /// + /// This can be internal as it is a constraint only on internal collections. + /// + internal interface IImmutable + { + } +} diff --git a/src/XMakeBuildEngine/Collections/IValued.cs b/src/XMakeBuildEngine/Collections/IValued.cs new file mode 100644 index 00000000000..ca1b8385710 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/IValued.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface allowing values of things to be gotten. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Collections +{ + /// + /// Interface allowing values of things to be gotten. + /// + /// + /// This can be internal as it is a constraint only on internal collections. + /// + internal interface IValued + { + /// + /// Returns some value of a thing + /// + string EscapedValue + { + get; + } + } +} diff --git a/src/XMakeBuildEngine/Collections/ItemDictionary.cs b/src/XMakeBuildEngine/Collections/ItemDictionary.cs new file mode 100644 index 00000000000..45e7518d3ee --- /dev/null +++ b/src/XMakeBuildEngine/Collections/ItemDictionary.cs @@ -0,0 +1,522 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Collection of instance or definition items +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using ObjectModel = System.Collections.ObjectModel; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Collections +{ + /// + /// Collection of items that allows a list of all items of a specified type to be + /// retrieved in O(1), and specific items to be added, removed, or checked for in O(1). + /// All items of a particular type can also be removed in O(1). + /// Items are ordered with respect to all other items of their type. + /// + /// + /// Really a Dictionary<string, ICollection<T>> where the key (the item type) is obtained from IKeyed.Key + /// Is not observable, so if clients wish to observe modifications they must mediate them themselves and + /// either not expose this collection or expose it through a readonly wrapper. + /// At various places in this class locks are taken on the backing collection. The reason for this is to allow + /// this class to be asynchronously enumerated. This is accomplished by the CopyOnReadEnumerable which will + /// lock the backing collection when it does its deep cloning. This prevents asynchronous access from corrupting + /// the state of the enumeration until the collection has been fully copied. + /// + /// Item class type to store + [DebuggerDisplay("#Item types={ItemTypes.Count} #Items={Count}")] + internal sealed class ItemDictionary : IEnumerable, IItemProvider + where T : class, IKeyed, IItem + { + /// + /// Dictionary of item lists used as a backing store. + /// An empty list should never be stored in here unless it is an empty marker. + /// See AddEmptyMarker. + /// This collection provides quick access to the ordered set of items of a particular type. + /// + private readonly Dictionary> _itemLists; + + /// + /// Dictionary of items in the collection, to speed up Contains, + /// Remove, and Replace. For those operations, we look up here, + /// then modify the other dictionary to match. + /// + private readonly Dictionary> _nodes; + + /// + /// Constructor for an empty collection. + /// + internal ItemDictionary() + { + // Tracing.Record("new item dictionary"); + _itemLists = new Dictionary>(MSBuildNameIgnoreCaseComparer.Default); + _nodes = new Dictionary>(); + } + + /// + /// Constructor for an empty collection taking an initial capacity + /// for the number of distinct item types + /// + internal ItemDictionary(int initialItemTypesCapacity) + { + // Tracing.Record("new item dictionary"); + _itemLists = new Dictionary>(initialItemTypesCapacity, MSBuildNameIgnoreCaseComparer.Default); + _nodes = new Dictionary>(); + } + + /// + /// Constructor for an collection holding items from a specified enumerable. + /// + internal ItemDictionary(IEnumerable items) + { + // Tracing.Record("new item dictionary"); + _itemLists = new Dictionary>(MSBuildNameIgnoreCaseComparer.Default); + _nodes = new Dictionary>(); + ImportItems(items); + } + + /// + /// Number of items in total, for debugging purposes. + /// + internal int Count + { + get { return _nodes.Count; } + } + + /// + /// Get the item types that have at least one item in this collection + /// + /// + /// KeyCollection<K> is already a read only collection, so no protection + /// is necessary. + /// + internal ICollection ItemTypes + { + get + { + lock (_itemLists) + { + return _itemLists.Keys; + } + } + } + + /// + /// Returns the item list for a particular item type, + /// creating and adding a new item list if necessary. + /// Does not throw if there are no items of this type. + /// This is a read-only list. + /// If the result is not empty it is a live list. + /// Use AddItem or RemoveItem to modify items in this project. + /// Using the return value from this in a multithreaded situation is unsafe. + /// + internal ICollection this[string itemtype] + { + get + { + LinkedList list; + lock (_itemLists) + { + if (!_itemLists.TryGetValue(itemtype, out list)) + { + return ReadOnlyEmptyList.Instance; + } + } + + return new ReadOnlyCollection(list); + } + } + + /// + /// Empty the collection + /// + public void Clear() + { + lock (_itemLists) + { + foreach (ICollection list in _itemLists.Values) + { + list.Clear(); + } + + _itemLists.Clear(); + _nodes.Clear(); + } + } + + /// + /// Returns an enumerable which copies the underlying data on read. + /// + public IEnumerable GetCopyOnReadEnumerable() + { + return new CopyOnReadEnumerable(this, _itemLists); + } + + /// + /// Gets an enumerator over the items in the collection + /// + public IEnumerator GetEnumerator() + { + return new ItemDictionary.Enumerator(_itemLists.Values); + } + + /// + /// Get an enumerator over entries + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _itemLists.GetEnumerator(); + } + + #region ItemDictionary Members + + /// + /// Returns all of the items for the specified type. + /// If there are no items of this type, returns an empty list. + /// Using the return from this method in a multithreaded scenario is unsafe. + /// + /// The item type to return + /// The list of matching items. + public ICollection GetItems(string itemType) + { + ICollection result = this[itemType]; + + return (result == null) ? ReadOnlyEmptyList.Instance : result; + } + + #endregion + + /// + /// Whether the provided item is in this table or not. + /// + internal bool Contains(T projectItem) + { + lock (_itemLists) + { + return _nodes.ContainsKey(projectItem); + } + } + + /// + /// Add a new item to the collection, at the + /// end of the list of other items with its key. + /// + internal void Add(T projectItem) + { + lock (_itemLists) + { + LinkedList list; + if (!_itemLists.TryGetValue(projectItem.Key, out list)) + { + list = new LinkedList(); + _itemLists[projectItem.Key] = list; + } + + LinkedListNode node = list.AddLast(projectItem); + _nodes.Add(projectItem, node); + } + } + + /// + /// Removes an item, if it is in the collection. + /// Returns true if it was found, otherwise false. + /// + /// + /// If a list is emptied, removes the list from the enclosing collection + /// so it can be garbage collected. + /// + internal bool Remove(T projectItem) + { + lock (_itemLists) + { + LinkedListNode node; + if (!_nodes.TryGetValue(projectItem, out node)) + { + return false; + } + + LinkedList list = node.List; + list.Remove(node); + _nodes.Remove(projectItem); + + // Save memory + if (list.Count == 0) + { + _itemLists.Remove(projectItem.Key); + } + + return true; + } + } + + /// + /// Replaces an exsting item with a new item. This is necessary to preserve the original ordering semantics of Lookup.GetItems + /// when items with metadata modifications are being returned. See Dev10 bug 480737. + /// If the item is not found, does nothing. + /// + /// The item to be replaced. + /// The replacement item. + internal void Replace(T existingItem, T newItem) + { + ErrorUtilities.VerifyThrow(existingItem.Key == newItem.Key, "Cannot replace an item {0} with an item {1} with a different name.", existingItem.Key, newItem.Key); + lock (_itemLists) + { + LinkedListNode node; + if (_nodes.TryGetValue(existingItem, out node)) + { + node.Value = newItem; + _nodes.Remove(existingItem); + _nodes.Add(newItem, node); + } + } + } + + /// + /// Add the set of items specified to this dictionary + /// + /// An enumerator over the items to remove. + internal void ImportItems(IEnumerable other) + { + foreach (T item in other) + { + Add(item); + } + } + + /// + /// Add the set of items specified, all sharing an item type, to this dictionary. + /// + /// + /// This is a little faster than ImportItems where all the items have the same item type. + /// + internal void ImportItemsOfType(string itemType, IEnumerable items) + { + lock (_itemLists) + { + LinkedList list; + if (!_itemLists.TryGetValue(itemType, out list)) + { + list = new LinkedList(); + _itemLists[itemType] = list; + } + + foreach (T item in items) + { +#if DEBUG + // Debug only: hot code path + ErrorUtilities.VerifyThrow(String.Equals(itemType, item.Key, StringComparison.OrdinalIgnoreCase), "Item type mismatch"); +#endif + LinkedListNode node = list.AddLast(item); + _nodes.Add(item, node); + } + } + } + + /// + /// Remove the set of items specified from this dictionary + /// + /// An enumerator over the items to remove. + internal void RemoveItems(IEnumerable other) + { + foreach (T item in other) + { + Remove(item); + } + } + + /// + /// Special method used for batching buckets. + /// Adds an explicit marker indicating there are no items for the specified item type. + /// In the general case, this is redundant, but batching buckets use this to indicate that they are + /// batching over the item type, but their bucket does not contain items of that type. + /// See HasEmptyMarker. + /// + internal void AddEmptyMarker(string itemType) + { + lock (_itemLists) + { + ErrorUtilities.VerifyThrow(!_itemLists.ContainsKey(itemType), "Should be none"); + _itemLists.Add(itemType, new LinkedList()); + } + } + + /// + /// Special method used for batching buckets. + /// Lookup can call this to see whether there was an explicit marker placed indicating that + /// there are no items of this type. See comment on AddEmptyMarker. + /// + internal bool HasEmptyMarker(string itemType) + { + lock (_itemLists) + { + LinkedList list; + + if (_itemLists.TryGetValue(itemType, out list) && list.Count == 0) + { + return true; + } + + return false; + } + } + + /// + /// Custom enumerator that allows enumeration over all the items in the collection + /// as though they were in a single list. + /// All items of a type are returned consecutively in their correct order. + /// However the order in which item types are returned is not defined. + /// + private class Enumerator : IEnumerator, IDisposable + { + /// + /// Enumerator over lists + /// + private IEnumerator> _listEnumerator; + + /// + /// Enumerator over items in the current list + /// + private IEnumerator _itemEnumerator; + + /// + /// Constructs an item enumerator over a list enumerator + /// + internal Enumerator(IEnumerable> listEnumerable) + { + _listEnumerator = listEnumerable.GetEnumerator(); // Now get the enumerator, since we now have the lock. + _itemEnumerator = null; // Must assign all struct fields first + _itemEnumerator = GetNextItemEnumerator(); + } + + /// + /// Finalizer + /// + ~Enumerator() + { + Dispose(false); + } + + /// + /// Get the current item + /// + public T Current + { + get + { + // Undefined if enumerator is before or after collection: we return null + return _itemEnumerator != null ? _itemEnumerator.Current : null; + } + } + + /// + /// Implementation of IEnumerator.Current, which unlike IEnumerator>T<.Current throws + /// if there is no current object + /// + object IEnumerator.Current + { + get + { + if (_itemEnumerator != null) + { + return _itemEnumerator.Current; + } + + // will throw InvalidOperationException, per IEnumerator contract + return ((IEnumerator)_listEnumerator).Current; + } + } + + /// + /// Move to the next object if any, + /// otherwise returns false + /// + public bool MoveNext() + { + if (_itemEnumerator == null) + { + return false; + } + + while (!_itemEnumerator.MoveNext()) + { + _itemEnumerator = GetNextItemEnumerator(); + + if (_itemEnumerator == null) + { + return false; + } + } + + return true; + } + + /// + /// Reset the enumerator + /// + public void Reset() + { + if (_itemEnumerator != null) + { + _itemEnumerator.Reset(); + } + + _listEnumerator.Reset(); + } + + /// + /// IDisposable implementation. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// The real disposer. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_listEnumerator != null) + { + if (_itemEnumerator != null) + { + _itemEnumerator.Dispose(); + _itemEnumerator = null; + } + + _listEnumerator.Dispose(); + _listEnumerator = null; + } + } + } + + /// + /// Get an item enumerator over the next list with items in it + /// + private IEnumerator GetNextItemEnumerator() + { + do + { + if (!_listEnumerator.MoveNext()) + { + return null; + } + } + while (_listEnumerator.Current == null || _listEnumerator.Current.Count == 0); + + return _listEnumerator.Current.GetEnumerator(); + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/LookasideStringInterner.cs b/src/XMakeBuildEngine/Collections/LookasideStringInterner.cs new file mode 100644 index 00000000000..d78858f32ce --- /dev/null +++ b/src/XMakeBuildEngine/Collections/LookasideStringInterner.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An interner used for serialization. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// A simple string interner designed for IPC. + /// + /// + /// This interner works by providing a way to convert strings to integer IDs. When used as a form of compression, + /// clients will intern their strings and record the set of IDs returned, then transmit those IDs instead of the + /// original strings. The interner itself is also transmitted ahead of time, with the IDs, allowing + /// reconstruction of the original strings. This ensures each string is transmitted exactly once. + /// + internal class LookasideStringInterner : INodePacketTranslatable + { + /// + /// Index used for null strings. + /// + private const int NullStringIndex = -1; + + /// + /// Index used for empty strings. + /// + private const int EmptyStringIndex = -2; + + /// + /// The map used to intern strings for serialization. This map doesn't exist when the strings + /// are deserialized (it is not needed.) + /// + private Dictionary _stringToIdsMap; + + /// + /// The list of strings by ID. + /// + private List _strings; + + /// + /// Constructor to be used during serialization. + /// + public LookasideStringInterner(StringComparer comparer, int defaultCollectionSize) + { + _stringToIdsMap = new Dictionary(defaultCollectionSize, comparer); + _strings = new List(defaultCollectionSize); + } + + /// + /// Constructor to be used during deserialization. + /// + /// + /// Intern cannot be used on this interner if it came from serialization, since we do + /// not reconstruct the interning dictionary. + /// + public LookasideStringInterner(INodePacketTranslator translator) + { + this.Translate(translator); + } + + /// + /// Interns the specified string. + /// + /// The string to intern. + /// The index representing the string. + public int Intern(string str) + { + if (str == null) + { + return NullStringIndex; + } + else if (str.Length == 0) + { + return EmptyStringIndex; + } + else + { + int index = -1; + + // If stringToIdsMap is null here, it means we probably tried to intern a string to an interner which came from + // deserialization (and thus doesn't support further interning for efficiency reasons.) No VerifyThrow here + // because this function is called a lot. + if (!_stringToIdsMap.TryGetValue(str, out index)) + { + index = _strings.Count; // This will be the index of the string we are about to add. + _stringToIdsMap.Add(str, index); + _strings.Add(str); + } + + return index; + } + } + + /// + /// Retrieves a string corresponding to the provided index. + /// + /// The index. + /// The corresponding string. + public string GetString(int index) + { + switch (index) + { + case NullStringIndex: + return null; + + case EmptyStringIndex: + return String.Empty; + + default: + return _strings[index]; + } + } + + /// + /// The translator, for serialization. + /// + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _strings); + } + } +} diff --git a/src/XMakeBuildEngine/Collections/MultiDictionary.cs b/src/XMakeBuildEngine/Collections/MultiDictionary.cs new file mode 100644 index 00000000000..4d4d6eeec25 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/MultiDictionary.cs @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dictionary that can hold more than one distinct value with the same key. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections.ObjectModel; +using System.Collections; +using Microsoft.Build.Shared; +using System.Diagnostics; + +namespace Microsoft.Build.Collections +{ + /// + /// A dictionary that can hold more than one distinct value with the same key. + /// All keys must have at least one value: null values are currently rejected. + /// + /// + /// Order of values for a key is not defined but is currently the order of add. + /// A variation could store the values in a HashSet, for different tradeoffs. + /// + /// Type of key + /// Type of value + [DebuggerDisplay("#Keys={KeyCount} #Values={ValueCount}")] + internal class MultiDictionary + where K : class + where V : class + { + // The simplest implementation of MultiDictionary would use a Dictionary>. + // However, a List with one element is 44 bytes (empty, 24 bytes) + // even though a single Object takes up only 12 bytes. + // If most values are only one element, we can save space by storing Object + // and using its implicit type field to discriminate. + // + // Experiments, using a large number of keys: + // + // Dictionary>, each key with one item, 127 bytes/key + // Dictionary>, each key with 1.01 items, 127 bytes/key + // Dictionary>, each key with 1.1 items, 128 bytes/key + // Dictionary>, each key with 1.5 items, 133 bytes/key + // Dictionary>, each key with 2 items, 139 bytes/key + // + // MultiDictionary, each key with one item, 83 bytes/key + // MultiDictionary, each key with 1.01 items, 84 bytes/key + // MultiDictionary, each key with 1.1 item, 88 bytes/key + // MultiDictionary, each key with 1.5 items, 111 bytes/key + // MultiDictionary, each key with 2 items, 139 bytes/key + // + // Savings for 10,000 objects with 1.01 per entry is 420Kb out of 1.2Mb + // If keys and values are already allocated (e.g., strings in use elsewhere) then this is + // the complete cost of the collection. + + /// + /// Backing dictionary + /// + private Dictionary> _backing; + + /// + /// Number of values over all keys + /// + private int _valueCount; + + /// + /// Constructor taking a specified comparer for the keys + /// + internal MultiDictionary(IEqualityComparer keyComparer) + { + _backing = new Dictionary.SmallList>(keyComparer); + } + + /// + /// Number of keys + /// + internal int KeyCount + { + get { return _backing.Count; } + } + + /// + /// Number of values over all keys + /// + internal int ValueCount + { + get { return _valueCount; } + } + + /// + /// return keys in the dictionary + /// + internal IEnumerable Keys + { + get { return _backing.Keys; } + } + + /// + /// Enumerator over values that have the specified key. + /// + internal IEnumerable this[K key] + { + get + { + SmallList entry; + if (!_backing.TryGetValue(key, out entry)) + { + yield break; + } + + foreach (V value in entry) + { + yield return value; + } + } + } + + /// + /// Add a single value under the specified key. + /// Value may not be null. + /// + internal void Add(K key, V value) + { + ErrorUtilities.VerifyThrow(value != null, "Null value not allowed"); + + SmallList entry; + if (!_backing.TryGetValue(key, out entry)) + { + _backing.Add(key, new MultiDictionary.SmallList(value)); + } + else + { + entry.Add(value); + } + + _valueCount++; + } + + /// + /// Removes an entry with the specified key and value. + /// Returns true if found, false otherwise. + /// + internal bool Remove(K key, V value) + { + ErrorUtilities.VerifyThrow(value != null, "Null value not allowed"); + + SmallList entry; + if (!_backing.TryGetValue(key, out entry)) + { + return false; + } + + bool result = entry.Remove(value); + + if (result) + { + if (entry.Count == 0) + { + _backing.Remove(key); + } + + _valueCount--; + } + + return result; + } + + /// + /// Empty the collection + /// + internal void Clear() + { + _backing = new Dictionary.SmallList>(); + _valueCount = 0; + } + + /// + /// List capable of holding 0-n items. + /// Uses less memory than List for less than 2 items. + /// + /// Type of the value + private class SmallList : IEnumerable + where TT : class + { + /// + /// Entry - either a TT or a list of TT. + /// + private Object _entry; + + /// + /// Constructor taking the initial object + /// + internal SmallList(TT first) + { + _entry = first; + } + + /// + /// Number of entries in this multivalue. + /// + internal int Count + { + get + { + if (_entry == null) + { + return 0; + } + + List list = _entry as List; + + if (list == null) + { + return 1; + } + + return list.Count; + } + } + + /// + /// Enumerable over the values. + /// + public IEnumerator GetEnumerator() + { + if (_entry == null) + { + yield break; + } + else if (_entry is TT) + { + yield return (TT)_entry; + } + else + { + List list = _entry as List; + + foreach (TT item in list) + { + yield return item; + } + } + } + + /// + /// Enumerable over the values. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Add a value. + /// Does not verify the value is not already present. + /// + public void Add(TT value) + { + if (_entry == null) + { + _entry = value; + } + else if (_entry is TT) + { + List list = new List(); + list.Add((TT)_entry); + list.Add(value); + _entry = list; + } + else + { + List list = _entry as List; + list.Add(value); + } + } + + /// + /// Remove a value. + /// Returns true if the value existed, otherwise false. + /// + public bool Remove(TT value) + { + if (_entry == null) + { + return false; + } + else if (_entry is TT) + { + if (Object.ReferenceEquals((TT)_entry, value)) + { + _entry = null; + return true; + } + + return false; + } + + List list = _entry as List; + + for (int i = 0; i < list.Count; i++) + { + if (Object.ReferenceEquals(value, list[i])) + { + if (list.Count == 2) + { + if (i == 0) + { + _entry = list[1]; + } + else + { + _entry = list[0]; + } + } + else + { + list.RemoveAt(i); + } + + return true; + } + } + + return false; + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/OrdinalIgnoreCaseKeyedComparer.cs b/src/XMakeBuildEngine/Collections/OrdinalIgnoreCaseKeyedComparer.cs new file mode 100644 index 00000000000..4e10a5edef9 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/OrdinalIgnoreCaseKeyedComparer.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// Equality comparer for IKeyed objects that uses Ordinal comparison. +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// Equality comparer for IKeyed objects that uses Ordinal comparison. + /// + [Serializable] + internal class OrdinalIgnoreCaseKeyedComparer : IEqualityComparer + { + /// + /// The one instance + /// + private static OrdinalIgnoreCaseKeyedComparer s_instance = new OrdinalIgnoreCaseKeyedComparer(); + + /// + /// Only create myself + /// + private OrdinalIgnoreCaseKeyedComparer() + { + } + + /// + /// The one instance + /// + internal static OrdinalIgnoreCaseKeyedComparer Instance + { + get { return s_instance; } + } + + /// + /// Performs the "Equals" operation Ordinally + /// + public bool Equals(IKeyed one, IKeyed two) + { + return String.Equals(one.Key, two.Key, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Get hash + /// + public int GetHashCode(IKeyed item) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(item.Key); + } + } +} diff --git a/src/XMakeBuildEngine/Collections/OrdinalKeyedComparer.cs b/src/XMakeBuildEngine/Collections/OrdinalKeyedComparer.cs new file mode 100644 index 00000000000..ff2788e7c17 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/OrdinalKeyedComparer.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// Equality comparer for IKeyed objects that uses Ordinal comparison. +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// Equality comparer for IKeyed objects that uses Ordinal comparison. + /// + [Serializable] + internal class OrdinalKeyedComparer : IEqualityComparer + { + /// + /// The one instance + /// + private static OrdinalKeyedComparer s_instance = new OrdinalKeyedComparer(); + + /// + /// Only create myself + /// + private OrdinalKeyedComparer() + { + } + + /// + /// The one instance + /// + internal static OrdinalKeyedComparer Instance + { + get { return s_instance; } + } + + /// + /// Performs the "Equals" operation Ordinally + /// + public bool Equals(IKeyed one, IKeyed two) + { + return String.Equals(one.Key, two.Key, StringComparison.Ordinal); + } + + /// + /// Get hash + /// + public int GetHashCode(IKeyed item) + { + return StringComparer.Ordinal.GetHashCode(item.Key); + } + } +} diff --git a/src/XMakeBuildEngine/Collections/PropertyDictionary.cs b/src/XMakeBuildEngine/Collections/PropertyDictionary.cs new file mode 100644 index 00000000000..6b66560d16d --- /dev/null +++ b/src/XMakeBuildEngine/Collections/PropertyDictionary.cs @@ -0,0 +1,546 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dictionary over properties or metadata. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Collections +{ + /// + /// A dictionary of unordered property or metadata name/value pairs. + /// + /// + /// The value that this adds over IDictionary<string, T> is: + /// - enforces that key = T.Name + /// - default enumerator is over values + /// - (marginal) enforces the correct key comparer + /// - potentially makes copy on write possible + /// + /// Really a Dictionary<string, T> where the key (the name) is obtained from IKeyed.Key. + /// Is not observable, so if clients wish to observe modifications they must mediate them themselves and + /// either not expose this collection or expose it through a readonly wrapper. + /// At various places in this class locks are taken on the backing collection. The reason for this is to allow + /// this class to be asynchronously enumerated. This is accomplished by the CopyOnReadEnumerable which will + /// lock the backing collection when it does its deep cloning. This prevents asynchronous access from corrupting + /// the state of the enumeration until the collection has been fully copied. + /// + /// Since we use the mutable ignore case comparer we need to make sure that we lock our self before we call the comparer since the comparer can call back + /// into this dictionary which could cause a deadlock if another thread is also accessing another method in the dictionary. + /// + /// Property or Metadata class type to store + [DebuggerDisplay("#Entries={Count}")] + internal sealed class PropertyDictionary : IEnumerable, IEquatable>, IPropertyProvider, IDictionary + where T : class, IKeyed, IValued, IEquatable + { + /// + /// Backing dictionary + /// + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + private RetrievableEntryHashSet _properties; + + /// + /// Comparer whose start and end indexes we can manipulate as necessary. + /// + private MSBuildNameIgnoreCaseComparer _comparer = MSBuildNameIgnoreCaseComparer.Mutable; + + /// + /// Creates empty dictionary + /// + public PropertyDictionary() + { + _properties = new RetrievableEntryHashSet(_comparer); + } + + /// + /// Creates empty dictionary, optionally specifying initial capacity + /// + internal PropertyDictionary(int capacity) + { + _properties = new RetrievableEntryHashSet(capacity, _comparer); + } + + /// + /// Create a new dictionary from an enumerator + /// + internal PropertyDictionary(IEnumerable elements) + : this() + { + foreach (T element in elements) + { + Set(element); + } + } + + /// + /// Create a new dictionary from an enumerator + /// + internal PropertyDictionary(int capacity, IEnumerable elements) + : this(capacity) + { + foreach (T element in elements) + { + Set(element); + } + } + + /// + /// Accessor for the list of property names + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + ICollection IDictionary.Keys + { + get + { + return PropertyNames; + } + } + + /// + /// Accessor for the list of properties + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + ICollection IDictionary.Values + { + get + { + lock (_properties) + { + return _properties.Values; + } + } + } + + /// + /// Returns the number of properties in the collection + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + int ICollection>.Count + { + get + { + lock (_properties) + { + return _properties.Count; + } + } + } + + /// + /// Whether the collection is read-only. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + bool ICollection>.IsReadOnly + { + get + { + return false; + } + } + + /// + /// Returns the number of property in the collection. + /// + internal int Count + { + get + { + lock (_properties) + { + return _properties.Count; + } + } + } + + /// + /// Retrieves a collection containing the names of all the properties present in the dictionary. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal ICollection PropertyNames + { + get + { + lock (_properties) + { + return _properties.Keys; + } + } + } + + /// + /// Get the property with the specified name, or null if none exists. + /// Sets the property with the specified name, overwriting it if already exists. + /// + /// + /// Unlike Dictionary<K,V>[K], the getter returns null instead of throwing if the key does not exist. + /// This better matches the semantics of property, which are considered to have a blank value if they + /// are not defined. + /// + T IDictionary.this[string name] + { + get + { + return this[name]; + } + + set + { + this[name] = value; + } + } + + /// + /// Get the property with the specified name, or null if none exists. + /// Sets the property with the specified name, overwriting it if already exists. + /// + /// + /// Unlike Dictionary<K,V>[K], the getter returns null instead of throwing if the key does not exist. + /// This better matches the semantics of property, which are considered to have a blank value if they + /// are not defined. + /// + internal T this[string name] + { + get + { + // We don't want to check for a zero length name here, since that is a valid name + // and should return a null instance which will be interpreted as blank + T projectProperty; + lock (_properties) + { + _properties.TryGetValue(name, out projectProperty); + } + + return projectProperty; + } + + set + { + ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value"); + ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key"); + Set(value); + } + } + + /// + /// Returns an enumerable which clones the properties + /// + /// Returns a cloning enumerable. + public IEnumerable GetCopyOnReadEnumerable() + { + return new CopyOnReadEnumerable(this, _properties); + } + + /// + /// Returns true if a property with the specified name is present in the collection, + /// otherwise false. + /// + public bool Contains(string name) + { + return ((IDictionary)this).ContainsKey(name); + } + + /// + /// Empties the collection + /// + public void Clear() + { + lock (_properties) + { + _properties.Clear(); + } + } + + /// + /// Gets an enumerator over all the properties in the collection + /// Enumeration is in undefined order + /// + public IEnumerator GetEnumerator() + { + lock (_properties) + { + return _properties.Values.GetEnumerator(); + } + } + + /// + /// Get an enumerator over entries + /// + IEnumerator IEnumerable.GetEnumerator() + { + lock (_properties) + { + return ((IEnumerable)_properties.Values).GetEnumerator(); + } + } + + #region IEquatable> Members + + /// + /// Compares two property dictionaries for equivalence. They are equal if each contains the same properties with the + /// same values as the other, unequal otherwise. + /// + /// The dictionary to which this should be compared + /// True if they are equivalent, false otherwise. + public bool Equals(PropertyDictionary other) + { + if (null == other) + { + return false; + } + + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (Count != other.Count) + { + return false; + } + + lock (_properties) + { + foreach (T leftProp in this) + { + T rightProp = other[leftProp.Key]; + if (rightProp == null || !rightProp.Equals(leftProp)) + { + return false; + } + } + } + + return true; + } + + #endregion + + /// + /// Get the property with the specified name or null if it is not present + /// + public T GetProperty(string name) + { + // The properties lock is locked in indexor + return this[name]; + } + + /// + /// Get the property with the specified name or null if it is not present. + /// Name is the segment of the provided string with the provided start and end indexes. + /// + public T GetProperty(string name, int startIndex, int endIndex) + { + lock (_properties) + { + if (startIndex == 0 && endIndex == name.Length - 1) + { + return this[name]; + } + + T returnValue = _comparer.GetValueWithConstraints(this, name, startIndex, endIndex); + + return returnValue; + } + } + + #region IDictionary Members + + /// + /// Adds a property + /// + void IDictionary.Add(string key, T value) + { + ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key"); + + // The properties lock is locked in the set method + Set(value); + } + + /// + /// Returns true if the dictionary contains the key + /// + bool IDictionary.ContainsKey(string key) + { + return PropertyNames.Contains(key); + } + + /// + /// Removes a property + /// + bool IDictionary.Remove(string key) + { + return Remove(key); + } + + /// + /// Attempts to retrieve the a property. + /// + bool IDictionary.TryGetValue(string key, out T value) + { + value = this[key]; + + return (value != null); + } + + #endregion + + #region ICollection> Members + + /// + /// Adds a property + /// + void ICollection>.Add(KeyValuePair item) + { + ((IDictionary)this).Add(item.Key, item.Value); + } + + /// + /// Clears the property collection + /// + void ICollection>.Clear() + { + Clear(); + } + + /// + /// Checks for a property in the collection + /// + bool ICollection>.Contains(KeyValuePair item) + { + lock (_properties) + { + T value; + if (_properties.TryGetValue(item.Key, out value)) + { + return Object.ReferenceEquals(value, item.Value); + } + } + + return false; + } + + /// + /// Not implemented + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ErrorUtilities.ThrowInternalError("CopyTo is not supported on PropertyDictionary."); + } + + /// + /// Removes a property from the collection + /// + bool ICollection>.Remove(KeyValuePair item) + { + ErrorUtilities.VerifyThrow(item.Key == item.Value.Key, "Key must match value's key"); + + // The properties lock is locked in the remove method + return ((IDictionary)this).Remove(item.Key); + } + + #endregion + + #region IEnumerable> Members + + /// + /// Get an enumerator over the entries. + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + lock (_properties) + { + foreach (var entry in _properties) + { + yield return new KeyValuePair(entry.Key, entry); + } + } + } + + #endregion + + /// + /// Removes any property with the specified name. + /// Returns true if the property was in the collection, otherwise false. + /// + internal bool Remove(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + lock (_properties) + { + bool result = _properties.Remove(name); + return result; + } + } + + /// + /// Add the specified property to the collection. + /// Overwrites any property with the same name already in the collection. + /// To remove a property, use Remove(...) instead. + /// + internal void Set(T projectProperty) + { + ErrorUtilities.VerifyThrowArgumentNull(projectProperty, "projectProperty"); + + lock (_properties) + { + _properties[projectProperty.Key] = projectProperty; + } + } + + /// + /// Adds the specified properties to this dictionary. + /// + /// An enumerator over the properties to add. + internal void ImportProperties(IEnumerable other) + { + // The properties lock is locked in the set method + foreach (T property in other) + { + Set(property); + } + } + + /// + /// Removes the specified properties from this dictionary + /// + /// An enumerator over the properties to remove. + internal void RemoveProperties(IEnumerable other) + { + // The properties lock is locked in the set method + foreach (T property in other) + { + Remove(property.Key); + } + } + + /// + /// Helper to convert into a read-only dictionary of string, string. + /// + internal IDictionary ToDictionary() + { + Dictionary dictionary = null; + + lock (_properties) + { + dictionary = new Dictionary(_properties.Count, MSBuildNameIgnoreCaseComparer.Default); + + foreach (T property in this) + { + dictionary[property.Key] = property.EscapedValue; + } + } + + return dictionary; + } + } +} diff --git a/src/XMakeBuildEngine/Collections/ReadOnlyConvertingCollection.cs b/src/XMakeBuildEngine/Collections/ReadOnlyConvertingCollection.cs new file mode 100644 index 00000000000..f68a461ce45 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/ReadOnlyConvertingCollection.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A read-only collection wrapper which converts values as they are accessed. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Collections +{ + /// + /// A function that can be passed in so that the converting + /// collection can do a "contains" operation on the backing + /// collection, using an object of the "converted to" type. + /// + /// Type converted to + /// Whether the item is present + internal delegate bool Contains(N item); + + /// + /// Implementation of ICollection which converts values to the specified type when they are accessed. + /// + /// The backing collection's value type. + /// The desired value type. + internal class ReadOnlyConvertingCollection : ICollection + { + /// + /// The backing collection. + /// + private readonly ICollection _backing; + + /// + /// The delegate used to convert values. + /// + private readonly Converter _converter; + + /// + /// The delegate used to satisfy contains operations, optionally + /// + private readonly Contains _contains; + + /// + /// Constructor. + /// + internal ReadOnlyConvertingCollection(ICollection backing, Converter converter) + : this(backing, converter, null) + { + } + + /// + /// Constructor, optionally taking a delegate to do a "backwards" contains operation. + /// + internal ReadOnlyConvertingCollection(ICollection backing, Converter converter, Contains contains) + { + ErrorUtilities.VerifyThrowArgumentNull(backing, "backing"); + ErrorUtilities.VerifyThrowArgumentNull(converter, "converter"); + + _backing = backing; + _converter = converter; + _contains = contains; + } + + #region ICollection Members + + /// + /// Return the number of items in the collection. + /// + public int Count + { + get { return _backing.Count; } + } + + /// + /// Returns true if the collection is readonly. + /// + public bool IsReadOnly + { + get { return true; } + } + + /// + /// Adds the specified item to the collection. + /// + public void Add(N item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Clears all items from the collection. + /// + public void Clear() + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Returns true if the collection contains the speciied item. + /// + public bool Contains(N item) + { + if (_contains == null) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedConvertingCollectionValueToBacking"); + return false; + } + + return _contains(item); + } + + /// + /// Copy the elements of the collection to the specified array. + /// + public void CopyTo(N[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + /// + /// Remove the specified item from the collection. + /// + public bool Remove(N item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + #endregion + + #region IEnumerable Members + + /// + /// Implementation of generic IEnumerable.GetEnumerator() + /// + public IEnumerator GetEnumerator() + { + return new ConvertingEnumerable(_backing, _converter).GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + /// + /// Implementation of IEnumerable.GetEnumerator() + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Collections/ReadOnlyConvertingDictionary.cs b/src/XMakeBuildEngine/Collections/ReadOnlyConvertingDictionary.cs new file mode 100644 index 00000000000..4ce4e5775b8 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/ReadOnlyConvertingDictionary.cs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A read-only dictionary wrapper which converts values as they are accessed. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// Implementation of a dictionary which acts as a read-only wrapper on another dictionary, but + /// converts values as they are accessed to another type. + /// + /// The backing dictionary's key type. + /// The backing dictionary's value type. + /// The desired value type. + internal class ReadOnlyConvertingDictionary : IDictionary + { + /// + /// The backing dictionary. + /// + private readonly IDictionary _backing; + + /// + /// The delegate used to convert values. + /// + private readonly Converter _converter; + + /// + /// Constructor. + /// + internal ReadOnlyConvertingDictionary(IDictionary backing, Converter converter) + { + ErrorUtilities.VerifyThrowArgumentNull(backing, "backing"); + ErrorUtilities.VerifyThrowArgumentNull(converter, "converter"); + + _backing = backing; + _converter = converter; + } + + #region IDictionary Members + + /// + /// Returns the collection of keys in the dictionary. + /// + public ICollection Keys + { + get { return _backing.Keys; } + } + + /// + /// Returns the collection of values in the dictionary. + /// + public ICollection Values + { + get { return new ReadOnlyConvertingCollection(_backing.Values, _converter); } + } + + /// + /// Returns the number of items in the collection. + /// + public int Count + { + get { return _backing.Count; } + } + + /// + /// Returns true if the collection is read-only. + /// + public bool IsReadOnly + { + get { return true; } + } + + /// + /// Accesses the value for the specified key. + /// + public N this[K key] + { + get + { + return _converter(_backing[key]); + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + } + + /// + /// Adds a value to the dictionary. + /// + public void Add(K key, N value) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Returns true if the dictionary contains the specified key. + /// + public bool ContainsKey(K key) + { + return _backing.ContainsKey(key); + } + + /// + /// Removes the entry for the specified key from the dictionary. + /// + public bool Remove(K key) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + /// + /// Attempts to find the value for the specified key in the dictionary. + /// + public bool TryGetValue(K key, out N value) + { + V originalValue; + if (_backing.TryGetValue(key, out originalValue)) + { + value = _converter(originalValue); + return true; + } + + value = default(N); + return false; + } + + #endregion + + #region ICollection> Members + + /// + /// Adds an item to the collection. + /// + public void Add(KeyValuePair item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Clears the collection. + /// + public void Clear() + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + /// + /// Returns true ff the collection contains the specified item. + /// + public bool Contains(KeyValuePair item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedConvertingCollectionValueToBacking"); + return false; + } + + /// + /// Copies all of the elements of the collection to the specified array. + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ErrorUtilities.VerifyThrow(array.Length - arrayIndex >= _backing.Count, "Specified array size insufficient to hold the contents of the collection."); + + foreach (KeyValuePair pair in _backing) + { + array[arrayIndex++] = KeyValueConverter(pair); + } + } + + /// + /// Remove an item from the dictionary. + /// + public bool Remove(KeyValuePair item) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + return false; + } + + #endregion + + #region IEnumerable> Members + + /// + /// Implementation of generic IEnumerable.GetEnumerator() + /// + public IEnumerator> GetEnumerator() + { + return new ConvertingEnumerable, KeyValuePair>(_backing, KeyValueConverter).GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + /// + /// Implementation of IEnumerable.GetEnumerator() + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + + #endregion + + /// + /// Delegate used by ConvertingEnumerable + /// + private KeyValuePair KeyValueConverter(KeyValuePair original) + { + return new KeyValuePair(original.Key, _converter(original.Value)); + } + } +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/BitHelper.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/BitHelper.cs new file mode 100644 index 00000000000..48e34eecb2d --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/BitHelper.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Security; +using System.Text; + +namespace Microsoft.Build.Collections +{ + /// + /// ABOUT: + /// Helps with operations that rely on bit marking to indicate whether an item in the + /// collection should be added, removed, visited already, etc. + /// + /// BitHelper doesn't allocate the array; you must pass in an array or ints allocated on the + /// stack or heap. ToIntArrayLength() tells you the int array size you must allocate. + /// + /// USAGE: + /// Suppose you need to represent a bit array of length (i.e. logical bit array length) + /// BIT_ARRAY_LENGTH. Then this is the suggested way to instantiate BitHelper: + /// *************************************************************************** + /// int intArrayLength = BitHelper.ToIntArrayLength(BIT_ARRAY_LENGTH); + /// BitHelper bitHelper; + /// if (intArrayLength less than stack alloc threshold) + /// int* m_arrayPtr = stackalloc int[intArrayLength]; + /// bitHelper = new BitHelper(m_arrayPtr, intArrayLength); + /// else + /// int[] m_arrayPtr = new int[intArrayLength]; + /// bitHelper = new BitHelper(m_arrayPtr, intArrayLength); + /// *************************************************************************** + /// + /// IMPORTANT: + /// The second ctor args, length, should be specified as the length of the int array, not + /// the logical bit array. Because length is used for bounds checking into the int array, + /// it's especially important to get this correct for the stackalloc version. See the code + /// samples above; this is the value gotten from ToIntArrayLength(). + /// + /// The length ctor argument is the only exception; for other methods -- MarkBit and + /// IsMarked -- pass in values as indices into the logical bit array, and it will be mapped + /// to the position within the array of ints. + /// + /// FUTURE OPTIMIZATIONS: + /// A method such as FindFirstMarked/Unmarked Bit would be useful for callers that operate + /// on a bit array and then need to loop over it. In particular, if it avoided visiting + /// every bit, it would allow good perf improvements when the bit array is sparse. + /// + unsafe internal class BitHelper + { // should not be serialized + private const byte MarkedBitFlag = 1; + private const byte IntSize = 32; + + // m_length of underlying int array (not logical bit array) + private int _length; + + // ptr to stack alloc'd array of ints + [SecurityCritical] + private int* _arrayPtr; + + // array of ints + private int[] _array; + + // whether to operate on stack alloc'd or heap alloc'd array + private bool _useStackAlloc; + + /// + /// Instantiates a BitHelper with a heap alloc'd array of ints + /// + /// int array to hold bits + /// length of int array + [SecurityCritical] + internal BitHelper(int* bitArrayPtr, int length) + { + _arrayPtr = bitArrayPtr; + _length = length; + _useStackAlloc = true; + } + + /// + /// Instantiates a BitHelper with a heap alloc'd array of ints + /// + /// int array to hold bits + /// length of int array + [SecurityCritical] + internal BitHelper(int[] bitArray, int length) + { + _array = bitArray; + _length = length; + } + + /// + /// Mark bit at specified position + /// + /// + internal unsafe void MarkBit(int bitPosition) + { + if (_useStackAlloc) + { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < _length && bitArrayIndex >= 0) + { + _arrayPtr[bitArrayIndex] |= (MarkedBitFlag << (bitPosition % IntSize)); + } + } + else + { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < _length && bitArrayIndex >= 0) + { + _array[bitArrayIndex] |= (MarkedBitFlag << (bitPosition % IntSize)); + } + } + } + + /// + /// Is bit at specified position marked? + /// + /// + /// + internal unsafe bool IsMarked(int bitPosition) + { + if (_useStackAlloc) + { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < _length && bitArrayIndex >= 0) + { + return ((_arrayPtr[bitArrayIndex] & (MarkedBitFlag << (bitPosition % IntSize))) != 0); + } + return false; + } + else + { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < _length && bitArrayIndex >= 0) + { + return ((_array[bitArrayIndex] & (MarkedBitFlag << (bitPosition % IntSize))) != 0); + } + return false; + } + } + + /// + /// How many ints must be allocated to represent n bits. Returns (n+31)/32, but + /// avoids overflow + /// + /// + /// + internal static int ToIntArrayLength(int n) + { + return n > 0 ? ((n - 1) / IntSize + 1) : 0; + } + } +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashHelpers.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashHelpers.cs new file mode 100644 index 00000000000..93992408b7c --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashHelpers.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +#if !SILVERLIGHT +using System.Runtime.ConstrainedExecution; +#endif +using System.Text; + +namespace Microsoft.Build.Collections +{ + /// + /// Duplicated because internal to mscorlib + /// + internal static class HashHelpers + { + // Table of prime numbers to use as hash table sizes. + // The entry used for capacity is the smallest prime number in this array + // that is larger than twice the previous capacity. + + internal static readonly int[] primes = { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369}; + +#if !SILVERLIGHT + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + internal static bool IsPrime(int candidate) + { + if ((candidate & 1) != 0) + { + int limit = (int)Math.Sqrt(candidate); + for (int divisor = 3; divisor <= limit; divisor += 2) + { + if ((candidate % divisor) == 0) + { + return false; + } + } + return true; + } + return (candidate == 2); + } + +#if !SILVERLIGHT + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + internal static int GetPrime(int min) + { + Debug.Assert(min >= 0, "min less than zero; handle overflow checking before calling HashHelpers"); + + for (int i = 0; i < primes.Length; i++) + { + int prime = primes[i]; + if (prime >= min) + { + return prime; + } + } + + // Outside of our predefined table. Compute the hard way. + for (int i = (min | 1); i < Int32.MaxValue; i += 2) + { + if (IsPrime(i)) + { + return i; + } + } + return min; + } + + internal static int GetMinPrime() + { + return primes[0]; + } + + // Returns size of hashtable to grow to. + internal static int ExpandPrime(int oldSize) + { + int newSize = 2 * oldSize; + + // Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newSize > MaxPrimeArrayLength) + return MaxPrimeArrayLength; + + return GetPrime(newSize); + } + + // This is the maximum prime smaller than Array.MaxArrayLength + internal const int MaxPrimeArrayLength = 0x7FEFFFFD; + } +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSet.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSet.cs new file mode 100644 index 00000000000..f38237d42bd --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSet.cs @@ -0,0 +1,1822 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +#if !SILVERLIGHT +using System.Runtime.Serialization; +#endif +using System.Security.Permissions; +using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Security; +#if SILVERLIGHT +using System.Core; // for System.Core.SR +#endif +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; + +/* + ================================================================================================================== + MSBUILD COMMENT: + + Ripped off from Hashset.cs with the following changes: + + * class renamed + * unnecessary methods and attributes if-deffed out (code retained to help windiff, but indented) + * require T implements IKeyed, and accept IKeyed directly where necessary + * all constructors require a comparer -- an IEqualityComparer -- to avoid mistakes + * change Contains to give you back the found entry, rather than a boolean + * change Add so that it always adds, even if there's an entry already present with the same name. + We want "replacement" semantics, like a dictionary keyed on name. + * constructor that allows the collection to be read-only + * implement IDictionary + * some convenience methods taking 'string' as overloads of methods taking IKeyed + + Other than this it is modified absolutely minimally to make it easy to diff with the originals (in the Originals folder) + to verify that no errors were introduced, and make it easier to possibly pick up any future bug fixes to the original. + The care taken to minimally modify this means that it is not necessary to carefully code review this complex class, + nor unit test it directly. + ================================================================================================================== +*/ + +namespace Microsoft.Build.Collections +{ + /// + /// Implementation notes: + /// This uses an array-based implementation similar to Dictionary, using a buckets array + /// to map hash values to the Slots array. Items in the Slots array that hash to the same value + /// are chained together through the "next" indices. + /// + /// The capacity is always prime; so during resizing, the capacity is chosen as the next prime + /// greater than double the last capacity. + /// + /// The underlying data structures are lazily initialized. Because of the observation that, + /// in practice, hashtables tend to contain only a few elements, the initial capacity is + /// set very small (3 elements) unless the ctor with a collection is used. + /// + /// The +/- 1 modifications in methods that add, check for containment, etc allow us to + /// distinguish a hash code of 0 from an uninitialized bucket. This saves us from having to + /// reset each bucket to -1 when resizing. See Contains, for example. + /// + /// Set methods such as UnionWith, IntersectWith, ExceptWith, and SymmetricExceptWith modify + /// this set. + /// + /// Some operations can perform faster if we can assume "other" contains unique elements + /// according to this equality comparer. The only times this is efficient to check is if + /// other is a hashset. Note that checking that it's a hashset alone doesn't suffice; we + /// also have to check that the hashset is using the same equality comparer. If other + /// has a different equality comparer, it will have unique elements according to its own + /// equality comparer, but not necessarily according to ours. Therefore, to go these + /// optimized routes we check that other is a hashset using the same equality comparer. + /// + /// A HashSet with no elements has the properties of the empty set. (See IsSubset, etc. for + /// special empty set checks.) + /// + /// A couple of methods have a special case if other is this (e.g. SymmetricExceptWith). + /// If we didn't have these checks, we could be iterating over the set and modifying at + /// the same time. + /// + /// + [DebuggerTypeProxy(typeof(Microsoft.Build.Collections.HashSetDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "By design")] +#if SILVERLIGHT + public class HashSet : ICollection, ISet +#else + [Serializable()] + [System.Security.Permissions.HostProtection(MayLeakOnAbort = true)] + internal class RetrievableEntryHashSet : ICollection, ISerializable, IDeserializationCallback, IDictionary + where T : class, IKeyed +#endif + { + // store lower 31 bits of hash code + private const int Lower31BitMask = 0x7FFFFFFF; + // cutoff point, above which we won't do stackallocs. This corresponds to 100 integers. + private const int StackAllocThreshold = 100; + // when constructing a hashset from an existing collection, it may contain duplicates, + // so this is used as the max acceptable excess ratio of capacity to count. Note that + // this is only used on the ctor and not to automatically shrink if the hashset has, e.g, + // a lot of adds followed by removes. Users must explicitly shrink by calling TrimExcess. + // This is set to 3 because capacity is acceptable as 2x rounded up to nearest prime. + private const int ShrinkThreshold = 3; + +#if !SILVERLIGHT + // constants for serialization + private const String CapacityName = "Capacity"; + private const String ElementsName = "Elements"; + private const String ComparerName = "Comparer"; + private const String VersionName = "Version"; +#endif + + private int[] _buckets; + private Slot[] _slots; + private int _count; + private int _lastIndex; + private int _freeList; + private IEqualityComparer _comparer; + private int _version; + private bool _readOnly; + +#if !SILVERLIGHT + // temporary variable needed during deserialization + private SerializationInfo _siInfo; +#endif + + #region Constructors + + public RetrievableEntryHashSet(IEqualityComparer comparer) + { + if (comparer == null) + { + ErrorUtilities.ThrowInternalError("use explicit comparer"); + } + + _comparer = comparer; + _lastIndex = 0; + _count = 0; + _freeList = -1; + _version = 0; + } + + public RetrievableEntryHashSet(IEnumerable collection, IEqualityComparer comparer, bool readOnly = false) + : this(collection, comparer) + { + _readOnly = true; // Set after possible initialization from another collection + } + + public RetrievableEntryHashSet(IEnumerable> collection, IEqualityComparer comparer, bool readOnly = false) + : this(collection.Values(), comparer, readOnly) + { + _readOnly = true; // Set after possible initialization from another collection + } + + /// + /// Implementation Notes: + /// Since resizes are relatively expensive (require rehashing), this attempts to minimize + /// the need to resize by setting the initial capacity based on size of collection. + /// + /// + /// + public RetrievableEntryHashSet(int suggestedCapacity, IEqualityComparer comparer) + : this(comparer) + { + Initialize(suggestedCapacity); + } + + /// + /// Implementation Notes: + /// Since resizes are relatively expensive (require rehashing), this attempts to minimize + /// the need to resize by setting the initial capacity based on size of collection. + /// + /// + /// + public RetrievableEntryHashSet(IEnumerable collection, IEqualityComparer comparer) + : this(comparer) + { + if (collection == null) + { + throw new ArgumentNullException("collection"); + } + + Contract.EndContractBlock(); + + // to avoid excess resizes, first set size based on collection's count. Collection + // may contain duplicates, so call TrimExcess if resulting hashset is larger than + // threshold + int suggestedCapacity = 0; + ICollection coll = collection as ICollection; + if (coll != null) + { + suggestedCapacity = coll.Count; + } + Initialize(suggestedCapacity); + + this.UnionWith(collection); + if ((_count == 0 && _slots.Length > HashHelpers.GetMinPrime()) || + (_count > 0 && _slots.Length / _count > ShrinkThreshold)) + { + TrimExcess(); + } + } + +#if !SILVERLIGHT + protected RetrievableEntryHashSet(SerializationInfo info, StreamingContext context) + { + // We can't do anything with the keys and values until the entire graph has been + // deserialized and we have a reasonable estimate that GetHashCode is not going to + // fail. For the time being, we'll just cache this. The graph is not valid until + // OnDeserialization has been called. + _siInfo = info; + } +#endif + + #endregion + + // Convenience to minimise change to callers used to dictionaries + public ICollection Keys + { + get + { + return new ReadOnlyConvertingCollection(this, delegate (T input) { return input.Key; }, delegate (string key) { return Contains(key); }); + } + } + + // Convenience to minimise change to callers used to dictionaries + public ICollection Values + { + get { return this; } + } + + #region ICollection methods + + // Convenience to minimise change to callers used to dictionaries + internal T this[string name] + { + get + { + return Get(name); + } + + set + { + Debug.Assert(String.Equals(name, value.Key, StringComparison.Ordinal)); + Add(value); + } + } + + /// + /// Add item to this hashset. This is the explicit implementation of the ICollection + /// interface. The other Add method returns bool indicating whether item was added. + /// + /// item to add + void ICollection.Add(T item) + { + AddEvenIfPresent(item); + } + + /// + /// Remove all items from this set. This clears the elements but not the underlying + /// buckets and slots array. Follow this call by TrimExcess to release these. + /// + public void Clear() + { + if (_readOnly) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + if (_lastIndex > 0) + { + Debug.Assert(_buckets != null, "m_buckets was null but m_lastIndex > 0"); + + // clear the elements so that the gc can reclaim the references. + // clear only up to m_lastIndex for m_slots + Array.Clear(_slots, 0, _lastIndex); + Array.Clear(_buckets, 0, _buckets.Length); + _lastIndex = 0; + _count = 0; + _freeList = -1; + } + _version++; + } + + // Convenience + internal bool Contains(string key) + { + return (Get(key) != null); + } + + bool ICollection>.Contains(KeyValuePair entry) + { + Debug.Assert(String.Equals(entry.Key, entry.Value.Key, StringComparison.Ordinal)); + return (Get(entry.Value) != null); + } + + public bool ContainsKey(string key) + { + return (Get(key) != null); + } + + T IDictionary.this[string name] + { + get { return Get(name); } + set { Add(value); } + } + + /// + /// Checks if this hashset contains the item + /// + /// item to check for containment + /// true if item contained; false if not + public bool Contains(T item) + { + return (Get(item.Key) != null); + } + + // Convenience to minimise change to callers used to dictionaries + public bool TryGetValue(string key, out T item) + { + item = Get(key); + return (item != null); + } + + /// + /// Gets the item if any with the given name + /// + /// item to check for containment + /// true if item contained; false if not + public T Get(string key) + { + return Get(new KeyedObject(key)); + } + + /// + /// Gets the item if any with the given name + /// + /// item to check for containment + /// true if item contained; false if not + public T Get(IKeyed item) + { + if (_buckets != null) + { + int hashCode = InternalGetHashCode(item); + // see note at "HashSet" level describing why "- 1" appears in for loop + for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].next) + { + if (_slots[i].hashCode == hashCode && _comparer.Equals(_slots[i].value, item)) + { + return _slots[i].value; + } + } + } + // either m_buckets is null or wasn't found + return default(T); + } + + /// + /// Copy items in this hashset to array, starting at arrayIndex + /// + /// array to add items to + /// index to start at + public void CopyTo(T[] array, int arrayIndex) + { + CopyTo(array, arrayIndex, _count); + } + + /// + /// Remove by key + /// + public bool Remove(string item) + { + return Remove(new KeyedObject(item)); + } + + /// + /// Remove entry that compares equal to T + /// + public bool Remove(T item) + { + return Remove((IKeyed)item); + } + + bool ICollection>.Remove(KeyValuePair entry) + { + Debug.Assert(String.Equals(entry.Key, entry.Value.Key, StringComparison.Ordinal)); + return Remove(entry.Value); + } + + /// + /// Remove item from this hashset + /// + /// item to remove + /// true if removed; false if not (i.e. if the item wasn't in the HashSet) + private bool Remove(IKeyed item) + { + if (_readOnly) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + if (_buckets != null) + { + int hashCode = InternalGetHashCode(item); + int bucket = hashCode % _buckets.Length; + int last = -1; + for (int i = _buckets[bucket] - 1; i >= 0; last = i, i = _slots[i].next) + { + if (_slots[i].hashCode == hashCode && _comparer.Equals(_slots[i].value, item)) + { + if (last < 0) + { + // first iteration; update buckets + _buckets[bucket] = _slots[i].next + 1; + } + else + { + // subsequent iterations; update 'next' pointers + _slots[last].next = _slots[i].next; + } + _slots[i].hashCode = -1; + _slots[i].value = default(T); + _slots[i].next = _freeList; + + _count--; + _version++; + if (_count == 0) + { + _lastIndex = 0; + _freeList = -1; + } + else + { + _freeList = i; + } + return true; + } + } + } + // either m_buckets is null or wasn't found + return false; + } + + /// + /// Number of elements in this hashset + /// + public int Count + { + get { return _count; } + } + + /// + /// Whether this is readonly + /// + public bool IsReadOnly + { + get { return _readOnly; } + } + + /// + /// Permanently prevent changes to the set. + /// + internal void MakeReadOnly() + { + _readOnly = true; + } + + #endregion + + #region IEnumerable methods + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (var entry in this) + { + yield return new KeyValuePair(entry.Key, entry); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this); + } + + #endregion + + #region ISerializable methods + +#if !SILVERLIGHT + // [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] + [SecurityCritical] + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + // need to serialize version to avoid problems with serializing while enumerating + info.AddValue(VersionName, _version); + info.AddValue(ComparerName, _comparer, typeof(IEqualityComparer)); + info.AddValue(CapacityName, _buckets == null ? 0 : _buckets.Length); + if (_buckets != null) + { + T[] array = new T[_count]; + CopyTo(array); + info.AddValue(ElementsName, array, typeof(T[])); + } + } +#endif + #endregion + + #region IDeserializationCallback methods + +#if !SILVERLIGHT + public virtual void OnDeserialization(Object sender) + { + if (_siInfo == null) + { + // It might be necessary to call OnDeserialization from a container if the + // container object also implements OnDeserialization. However, remoting will + // call OnDeserialization again. We can return immediately if this function is + // called twice. Note we set m_siInfo to null at the end of this method. + return; + } + + int capacity = _siInfo.GetInt32(CapacityName); + _comparer = (IEqualityComparer)_siInfo.GetValue(ComparerName, typeof(IEqualityComparer)); + _freeList = -1; + + if (capacity != 0) + { + _buckets = new int[capacity]; + _slots = new Slot[capacity]; + + T[] array = (T[])_siInfo.GetValue(ElementsName, typeof(T[])); + + if (array == null) + { + throw new SerializationException(); + } + + // there are no resizes here because we already set capacity above + for (int i = 0; i < array.Length; i++) + { + AddEvenIfPresent(array[i]); + } + } + else + { + _buckets = null; + } + + _version = _siInfo.GetInt32(VersionName); + _siInfo = null; + } +#endif + + #endregion + + #region HashSet methods + + /// + /// Add item to this HashSet. + /// *** MSBUILD NOTE: Always added - overwrite semantics + /// + public void Add(T item) + { + AddEvenIfPresent(item); + } + + void IDictionary.Add(string key, T item) + { + if (key != item.Key) + throw new InvalidOperationException(); + + AddEvenIfPresent(item); + } + + void ICollection>.Add(KeyValuePair entry) + { + Debug.Assert(String.Equals(entry.Key, entry.Value.Key, StringComparison.Ordinal)); + + AddEvenIfPresent(entry.Value); + } + + /// + /// Take the union of this HashSet with other. Modifies this set. + /// + /// Implementation note: GetSuggestedCapacity (to increase capacity in advance avoiding + /// multiple resizes ended up not being useful in practice; quickly gets to the + /// point where it's a wasteful check. + /// + /// enumerable with items to add + public void UnionWith(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + foreach (T item in other) + { + AddEvenIfPresent(item); + } + } + +#if NEVER + /// + /// Takes the intersection of this set with other. Modifies this set. + /// + /// Implementation Notes: + /// We get better perf if other is a hashset using same equality comparer, because we + /// get constant contains check in other. Resulting cost is O(n1) to iterate over this. + /// + /// If we can't go above route, iterate over the other and mark intersection by checking + /// contains in this. Then loop over and delete any unmarked elements. Total cost is n2+n1. + /// + /// Attempts to return early based on counts alone, using the property that the + /// intersection of anything with the empty set is the empty set. + /// + /// enumerable with items to add + public void IntersectWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // intersection of anything with empty set is empty set, so return if count is 0 + if (m_count == 0) { + return; + } + + // if other is empty, intersection is empty set; remove all elements and we're done + // can only figure this out if implements ICollection. (IEnumerable has no count) + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + if (otherAsCollection.Count == 0) { + Clear(); + return; + } + + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // faster if other is a hashset using same equality comparer; so check + // that other is a hashset using the same equality comparer. + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + IntersectWithHashSetWithSameEC(otherAsSet); + return; + } + } + + IntersectWithEnumerable(other); + } + + /// + /// Remove items in other from this set. Modifies this set. + /// + /// enumerable with items to remove + public void ExceptWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // this is already the enpty set; return + if (m_count == 0) { + return; + } + + // special case if other is this; a set minus itself is the empty set + if (other == this) { + Clear(); + return; + } + + // remove every element in other from this + foreach (T element in other) { + Remove(element); + } + } + + + /// + /// Takes symmetric difference (XOR) with other and this set. Modifies this set. + /// + /// enumerable with items to XOR + public void SymmetricExceptWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // if set is empty, then symmetric difference is other + if (m_count == 0) { + UnionWith(other); + return; + } + + // special case this; the symmetric difference of a set with itself is the empty set + if (other == this) { + Clear(); + return; + } + + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // If other is a HashSet, it has unique elements according to its equality comparer, + // but if they're using different equality comparers, then assumption of uniqueness + // will fail. So first check if other is a hashset using the same equality comparer; + // symmetric except is a lot faster and avoids bit array allocations if we can assume + // uniqueness + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + SymmetricExceptWithUniqueHashSet(otherAsSet); + } + else { + SymmetricExceptWithEnumerable(other); + } + } + + /// + /// Checks if this is a subset of other. + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a subset of anything, including the empty set + /// 2. If other has unique elements according to this equality comparer, and this has more + /// elements than other, then it can't be a subset. + /// + /// Furthermore, if other is a hashset using the same equality comparer, we can use a + /// faster element-wise check. + /// + /// + /// true if this is a subset of other; false if not + public bool IsSubsetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // The empty set is a subset of any set + if (m_count == 0) { + return true; + } + + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // faster if other has unique elements according to this equality comparer; so check + // that other is a hashset using the same equality comparer. + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + // if this has more elements then it can't be a subset + if (m_count > otherAsSet.Count) { + return false; + } + + // already checked that we're using same equality comparer. simply check that + // each element in this is contained in other. + return IsSubsetOfHashSetWithSameEC(otherAsSet); + } + else { + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == m_count && result.unfoundCount >= 0); + } + } + + /// + /// Checks if this is a proper subset of other (i.e. strictly contained in) + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a proper subset of a set that contains at least + /// one element, but it's not a proper subset of the empty set. + /// 2. If other has unique elements according to this equality comparer, and this has >= + /// the number of elements in other, then this can't be a proper subset. + /// + /// Furthermore, if other is a hashset using the same equality comparer, we can use a + /// faster element-wise check. + /// + /// + /// true if this is a proper subset of other; false if not + public bool IsProperSubsetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // the empty set is a proper subset of anything but the empty set + if (m_count == 0) { + return otherAsCollection.Count > 0; + } + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // faster if other is a hashset (and we're using same equality comparer) + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + if (m_count >= otherAsSet.Count) { + return false; + } + // this has strictly less than number of items in other, so the following + // check suffices for proper subset. + return IsSubsetOfHashSetWithSameEC(otherAsSet); + } + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == m_count && result.unfoundCount > 0); + + } + + /// + /// Checks if this is a superset of other + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If other has no elements (it's the empty set), then this is a superset, even if this + /// is also the empty set. + /// 2. If other has unique elements according to this equality comparer, and this has less + /// than the number of elements in other, then this can't be a superset + /// + /// + /// + /// true if this is a superset of other; false if not + public bool IsSupersetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // try to fall out early based on counts + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // if other is the empty set then this is a superset + if (otherAsCollection.Count == 0) { + return true; + } + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // try to compare based on counts alone if other is a hashset with + // same equality comparer + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + if (otherAsSet.Count > m_count) { + return false; + } + } + } + + return ContainsAllElements(other); + } + + /// + /// Checks if this is a proper superset of other (i.e. other strictly contained in this) + /// + /// Implementation Notes: + /// This is slightly more complicated than above because we have to keep track if there + /// was at least one element not contained in other. + /// + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it can't be a proper superset of any set, even if + /// other is the empty set. + /// 2. If other is an empty set and this contains at least 1 element, then this is a proper + /// superset. + /// 3. If other has unique elements according to this equality comparer, and other's count + /// is greater than or equal to this count, then this can't be a proper superset + /// + /// Furthermore, if other has unique elements according to this equality comparer, we can + /// use a faster element-wise check. + /// + /// + /// true if this is a proper superset of other; false if not + public bool IsProperSupersetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // the empty set isn't a proper superset of any set. + if (m_count == 0) { + return false; + } + + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // if other is the empty set then this is a superset + if (otherAsCollection.Count == 0) { + // note that this has at least one element, based on above check + return true; + } + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // faster if other is a hashset with the same equality comparer + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + if (otherAsSet.Count >= m_count) { + return false; + } + // now perform element check + return ContainsAllElements(otherAsSet); + } + } + // couldn't fall out in the above cases; do it the long way + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount < m_count && result.unfoundCount == 0); + + } + + /// + /// Checks if this set overlaps other (i.e. they share at least one item) + /// + /// + /// true if these have at least one common element; false if disjoint + public bool Overlaps(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + if (m_count == 0) { + return false; + } + + foreach (T element in other) { + if (Contains(element)) { + return true; + } + } + return false; + } + + /// + /// Checks if this and other contain the same elements. This is set equality: + /// duplicates and order are ignored + /// + /// + /// + public bool SetEquals(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + RetrievableEntryHashSet otherAsSet = other as RetrievableEntryHashSet; + // faster if other is a hashset and we're using same equality comparer + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + // attempt to return early: since both contain unique elements, if they have + // different counts, then they can't be equal + if (m_count != otherAsSet.Count) { + return false; + } + + // already confirmed that the sets have the same number of distinct elements, so if + // one is a superset of the other then they must be equal + return ContainsAllElements(otherAsSet); + } + else { + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // if this count is 0 but other contains at least one element, they can't be equal + if (m_count == 0 && otherAsCollection.Count > 0) { + return false; + } + } + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount == m_count && result.unfoundCount == 0); + } + } +#endif + + // Copy all elements into array starting at zero based index specified + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Decently informative for an exception that will probably never actually see the light of day")] + void ICollection>.CopyTo(KeyValuePair[] array, int index) + { + if (index < 0 || Count > array.Length - index) + throw new ArgumentException("index"); + + int i = index; + foreach (var entry in this) + { + array[i] = new KeyValuePair(entry.Key, entry); + i++; + } + } + + public void CopyTo(T[] array) { CopyTo(array, 0, _count); } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Decently informative for an exception that will probably never actually see the light of day")] + public void CopyTo(T[] array, int arrayIndex, int count) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + Contract.EndContractBlock(); + + // check array index valid index into array + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException("arrayIndex"); + } + + // also throw if count less than 0 + if (count < 0) + { + throw new ArgumentOutOfRangeException("count"); + } + + // will array, starting at arrayIndex, be able to hold elements? Note: not + // checking arrayIndex >= array.Length (consistency with list of allowing + // count of 0; subsequent check takes care of the rest) + if (arrayIndex > array.Length || count > array.Length - arrayIndex) + { + throw new ArgumentException("arrayIndex"); + } + + int numCopied = 0; + for (int i = 0; i < _lastIndex && numCopied < count; i++) + { + if (_slots[i].hashCode >= 0) + { + array[arrayIndex + numCopied] = _slots[i].value; + numCopied++; + } + } + } + +#if NEVER + /// + /// Remove elements that match specified predicate. Returns the number of elements removed + /// + /// + /// + public int RemoveWhere(Predicate match) { + if (match == null) { + throw new ArgumentNullException("match"); + } + Contract.EndContractBlock(); + + int numRemoved = 0; + for (int i = 0; i < m_lastIndex; i++) { + if (m_slots[i].hashCode >= 0) { + // cache value in case delegate removes it + T value = m_slots[i].value; + if (match(value)) { + // check again that remove actually removed it + if (Remove(value)) { + numRemoved++; + } + } + } + } + return numRemoved; + } +#endif + /// + /// Gets the IEqualityComparer that is used to determine equality of keys for + /// the HashSet. + /// + public IEqualityComparer Comparer + { + get + { + return _comparer; + } + } + + /// + /// Sets the capacity of this list to the size of the list (rounded up to nearest prime), + /// unless count is 0, in which case we release references. + /// + /// This method can be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and release all + /// memory referenced by the list, execute the following statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + public void TrimExcess() + { + Debug.Assert(_count >= 0, "m_count is negative"); + + if (_count == 0) + { + // if count is zero, clear references + _buckets = null; + _slots = null; + _version++; + } + else + { + Debug.Assert(_buckets != null, "m_buckets was null but m_count > 0"); + + // similar to IncreaseCapacity but moves down elements in case add/remove/etc + // caused fragmentation + int newSize = HashHelpers.GetPrime(_count); + Slot[] newSlots = new Slot[newSize]; + int[] newBuckets = new int[newSize]; + + // move down slots and rehash at the same time. newIndex keeps track of current + // position in newSlots array + int newIndex = 0; + for (int i = 0; i < _lastIndex; i++) + { + if (_slots[i].hashCode >= 0) + { + newSlots[newIndex] = _slots[i]; + + // rehash + int bucket = newSlots[newIndex].hashCode % newSize; + newSlots[newIndex].next = newBuckets[bucket] - 1; + newBuckets[bucket] = newIndex + 1; + + newIndex++; + } + } + + Debug.Assert(newSlots.Length <= _slots.Length, "capacity increased after TrimExcess"); + + _lastIndex = newIndex; + _slots = newSlots; + _buckets = newBuckets; + _freeList = -1; + } + } + +#if NEVER +#if !SILVERLIGHT || FEATURE_NETCORE + /// + /// Used for deep equality of HashSet testing + /// + /// + public static IEqualityComparer> CreateSetComparer() { + return new HashSetEqualityComparer(); + } +#endif +#endif + + #endregion + + #region Helper methods + + /// + /// Initializes buckets and slots arrays. Uses suggested capacity by finding next prime + /// greater than or equal to capacity. + /// + /// + private void Initialize(int capacity) + { + Debug.Assert(_buckets == null, "Initialize was called but m_buckets was non-null"); + + int size = HashHelpers.GetPrime(capacity); + + _buckets = new int[size]; + _slots = new Slot[size]; + } + + /// + /// Expand to new capacity. New capacity is next prime greater than or equal to suggested + /// size. This is called when the underlying array is filled. This performs no + /// defragmentation, allowing faster execution; note that this is reasonable since + /// AddEvenIfPresent attempts to insert new elements in re-opened spots. + /// + /// + private void IncreaseCapacity() + { + Debug.Assert(_buckets != null, "IncreaseCapacity called on a set with no elements"); + + int newSize = HashHelpers.ExpandPrime(_count); + if (newSize <= _count) + { + throw new ArgumentException("newSize"); + } + + // Able to increase capacity; copy elements to larger array and rehash + Slot[] newSlots = new Slot[newSize]; + if (_slots != null) + { + Array.Copy(_slots, 0, newSlots, 0, _lastIndex); + } + + int[] newBuckets = new int[newSize]; + for (int i = 0; i < _lastIndex; i++) + { + int bucket = newSlots[i].hashCode % newSize; + newSlots[i].next = newBuckets[bucket] - 1; + newBuckets[bucket] = i + 1; + } + _slots = newSlots; + _buckets = newBuckets; + } + + /// + /// Adds value to HashSet if not contained already + /// Returns true if added and false if already present + /// ** MSBUILD: Modified so that it DOES add even if present. It will return false in that case, though.** + /// + /// value to find + /// + private bool AddEvenIfPresent(T value) + { + if (_readOnly) + { + ErrorUtilities.ThrowInvalidOperation("OM_NotSupportedReadOnlyCollection"); + } + + if (_buckets == null) + { + Initialize(0); + } + + int hashCode = InternalGetHashCode(value); + int bucket = hashCode % _buckets.Length; + for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].next) + { + if (_slots[i].hashCode == hashCode && _comparer.Equals(_slots[i].value, value)) + { + // NOTE: this must add EVEN IF it is already present, + // as it may be a different object with the same name, + // and we want "last wins" semantics + _slots[i].hashCode = hashCode; + _slots[i].value = value; + return false; + } + } + int index; + if (_freeList >= 0) + { + index = _freeList; + _freeList = _slots[index].next; + } + else + { + if (_lastIndex == _slots.Length) + { + IncreaseCapacity(); + // this will change during resize + bucket = hashCode % _buckets.Length; + } + index = _lastIndex; + _lastIndex++; + } + _slots[index].hashCode = hashCode; + _slots[index].value = value; + _slots[index].next = _buckets[bucket] - 1; + _buckets[bucket] = index + 1; + _count++; + _version++; + return true; + } + + /// + /// Equality comparer against another of this type. + /// Compares entries by reference - not merely by using the comparer on the key + /// + internal bool EntriesAreReferenceEquals(RetrievableEntryHashSet other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (this.Count != other.Count) + { + return false; + } + + T ours; + foreach (T element in other) + { + if (!TryGetValue(element.Key, out ours) || !Object.ReferenceEquals(element, ours)) + { + return false; + } + } + + return true; + } + +#if NEVER + /// + /// Checks if this contains of other's elements. Iterates over other's elements and + /// returns false as soon as it finds an element in other that's not in this. + /// Used by SupersetOf, ProperSupersetOf, and SetEquals. + /// + /// + /// + private bool ContainsAllElements(IEnumerable other) { + foreach (T element in other) { + if (!Contains(element)) { + return false; + } + } + return true; + } + + /// + /// Implementation Notes: + /// If other is a hashset and is using same equality comparer, then checking subset is + /// faster. Simply check that each element in this is in other. + /// + /// Note: if other doesn't use same equality comparer, then Contains check is invalid, + /// which is why callers must take are of this. + /// + /// If callers are concerned about whether this is a proper subset, they take care of that. + /// + /// + /// + /// + private bool IsSubsetOfHashSetWithSameEC(RetrievableEntryHashSet other) { + + foreach (T item in this) { + if (!other.Contains(item)) { + return false; + } + } + return true; + } + + /// + /// If other is a hashset that uses same equality comparer, intersect is much faster + /// because we can use other's Contains + /// + /// + private void IntersectWithHashSetWithSameEC(RetrievableEntryHashSet other) { + for (int i = 0; i < m_lastIndex; i++) { + if (m_slots[i].hashCode >= 0) { + T item = m_slots[i].value; + if (!other.Contains(item)) { + Remove(item); + } + } + } + } + + /// + /// Iterate over other. If contained in this, mark an element in bit array corresponding to + /// its position in m_slots. If anything is unmarked (in bit array), remove it. + /// + /// This attempts to allocate on the stack, if below StackAllocThreshold. + /// + /// + [System.Security.SecuritySafeCritical] + private unsafe void IntersectWithEnumerable(IEnumerable other) { + Debug.Assert(m_buckets != null, "m_buckets shouldn't be null; callers should check first"); + + // keep track of current last index; don't want to move past the end of our bit array + // (could happen if another thread is modifying the collection) + int originalLastIndex = m_lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + BitHelper bitHelper; + if (intArrayLength <= StackAllocThreshold) { + int* bitArrayPtr = stackalloc int[intArrayLength]; + bitHelper = new BitHelper(bitArrayPtr, intArrayLength); + } + else { + int[] bitArray = new int[intArrayLength]; + bitHelper = new BitHelper(bitArray, intArrayLength); + } + + // mark if contains: find index of in slots array and mark corresponding element in bit array + foreach (T item in other) { + int index = InternalIndexOf(item); + if (index >= 0) { + bitHelper.MarkBit(index); + } + } + + // if anything unmarked, remove it. Perf can be optimized here if BitHelper had a + // FindFirstUnmarked method. + for (int i = 0; i < originalLastIndex; i++) { + if (m_slots[i].hashCode >= 0 && !bitHelper.IsMarked(i)) { + Remove(m_slots[i].value); + } + } + } + + /// + /// Used internally by set operations which have to rely on bit array marking. This is like + /// Contains but returns index in slots array. + /// + /// + /// + private int InternalIndexOf(T item) { + Debug.Assert(m_buckets != null, "m_buckets was null; callers should check first"); + + int hashCode = InternalGetHashCode(item); + for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { + if ((m_slots[i].hashCode) == hashCode && m_comparer.Equals(m_slots[i].value, item)) { + return i; + } + } + // wasn't found + return -1; + } + + /// + /// if other is a set, we can assume it doesn't have duplicate elements, so use this + /// technique: if can't remove, then it wasn't present in this set, so add. + /// + /// As with other methods, callers take care of ensuring that other is a hashset using the + /// same equality comparer. + /// + /// + private void SymmetricExceptWithUniqueHashSet(RetrievableEntryHashSet other) { + foreach (T item in other) { + if (!Remove(item)) { + AddEvenIfPresent(item); + } + } + } + + /// + /// Implementation notes: + /// + /// Used for symmetric except when other isn't a HashSet. This is more tedious because + /// other may contain duplicates. HashSet technique could fail in these situations: + /// 1. Other has a duplicate that's not in this: HashSet technique would add then + /// remove it. + /// 2. Other has a duplicate that's in this: HashSet technique would remove then add it + /// back. + /// In general, its presence would be toggled each time it appears in other. + /// + /// This technique uses bit marking to indicate whether to add/remove the item. If already + /// present in collection, it will get marked for deletion. If added from other, it will + /// get marked as something not to remove. + /// + /// + /// + [System.Security.SecuritySafeCritical] + private unsafe void SymmetricExceptWithEnumerable(IEnumerable other) { + int originalLastIndex = m_lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + BitHelper itemsToRemove; + BitHelper itemsAddedFromOther; + if (intArrayLength <= StackAllocThreshold / 2) { + int* itemsToRemovePtr = stackalloc int[intArrayLength]; + itemsToRemove = new BitHelper(itemsToRemovePtr, intArrayLength); + + int* itemsAddedFromOtherPtr = stackalloc int[intArrayLength]; + itemsAddedFromOther = new BitHelper(itemsAddedFromOtherPtr, intArrayLength); + } + else { + int[] itemsToRemoveArray = new int[intArrayLength]; + itemsToRemove = new BitHelper(itemsToRemoveArray, intArrayLength); + + int[] itemsAddedFromOtherArray = new int[intArrayLength]; + itemsAddedFromOther = new BitHelper(itemsAddedFromOtherArray, intArrayLength); + } + + foreach (T item in other) { + int location = 0; + bool added = AddOrGetLocation(item, out location); + if (added) { + // wasn't already present in collection; flag it as something not to remove + // *NOTE* if location is out of range, we should ignore. BitHelper will + // detect that it's out of bounds and not try to mark it. But it's + // expected that location could be out of bounds because adding the item + // will increase m_lastIndex as soon as all the free spots are filled. + itemsAddedFromOther.MarkBit(location); + } + else { + // already there...if not added from other, mark for remove. + // *NOTE* Even though BitHelper will check that location is in range, we want + // to check here. There's no point in checking items beyond originalLastIndex + // because they could not have been in the original collection + if (location < originalLastIndex && !itemsAddedFromOther.IsMarked(location)) { + itemsToRemove.MarkBit(location); + } + } + } + + // if anything marked, remove it + for (int i = 0; i < originalLastIndex; i++) { + if (itemsToRemove.IsMarked(i)) { + Remove(m_slots[i].value); + } + } + } + + /// + /// Add if not already in hashset. Returns an out param indicating index where added. This + /// is used by SymmetricExcept because it needs to know the following things: + /// - whether the item was already present in the collection or added from other + /// - where it's located (if already present, it will get marked for removal, otherwise + /// marked for keeping) + /// + /// + /// + /// + private bool AddOrGetLocation(T value, out int location) { + Debug.Assert(m_buckets != null, "m_buckets is null, callers should have checked"); + + int hashCode = InternalGetHashCode(value); + int bucket = hashCode % m_buckets.Length; + for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { + if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, value)) { + location = i; + return false; //already present + } + } + int index; + if (m_freeList >= 0) { + index = m_freeList; + m_freeList = m_slots[index].next; + } + else { + if (m_lastIndex == m_slots.Length) { + IncreaseCapacity(); + // this will change during resize + bucket = hashCode % m_buckets.Length; + } + index = m_lastIndex; + m_lastIndex++; + } + m_slots[index].hashCode = hashCode; + m_slots[index].value = value; + m_slots[index].next = m_buckets[bucket] - 1; + m_buckets[bucket] = index + 1; + m_count++; + m_version++; + location = index; + return true; + } + + /// + /// Determines counts that can be used to determine equality, subset, and superset. This + /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet + /// these properties can be checked faster without use of marking because we can assume + /// other has no duplicates. + /// + /// The following count checks are performed by callers: + /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = m_count; i.e. everything + /// in other is in this and everything in this is in other + /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = m_count; i.e. other may + /// have elements not in this and everything in this is in other + /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = m_count; i.e + /// other must have at least one element not in this and everything in this is in other + /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less + /// than m_count; i.e. everything in other was in this and this had at least one element + /// not contained in other. + /// + /// An earlier implementation used delegates to perform these checks rather than returning + /// an ElementCount struct; however this was changed due to the perf overhead of delegates. + /// + /// + /// Allows us to finish faster for equals and proper superset + /// because unfoundCount must be 0. + /// + [System.Security.SecuritySafeCritical] + private unsafe ElementCount CheckUniqueAndUnfoundElements(IEnumerable other, bool returnIfUnfound) { + ElementCount result; + + // need special case in case this has no elements. + if (m_count == 0) { + int numElementsInOther = 0; + foreach (T item in other) { + numElementsInOther++; + // break right away, all we want to know is whether other has 0 or 1 elements + break; + } + result.uniqueCount = 0; + result.unfoundCount = numElementsInOther; + return result; + } + + + Debug.Assert((m_buckets != null) && (m_count > 0), "m_buckets was null but count greater than 0"); + + int originalLastIndex = m_lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + BitHelper bitHelper; + if (intArrayLength <= StackAllocThreshold) { + int* bitArrayPtr = stackalloc int[intArrayLength]; + bitHelper = new BitHelper(bitArrayPtr, intArrayLength); + } + else { + int[] bitArray = new int[intArrayLength]; + bitHelper = new BitHelper(bitArray, intArrayLength); + } + + // count of items in other not found in this + int unfoundCount = 0; + // count of unique items in other found in this + int uniqueFoundCount = 0; + + foreach (T item in other) { + int index = InternalIndexOf(item); + if (index >= 0) { + if (!bitHelper.IsMarked(index)) { + // item hasn't been seen yet + bitHelper.MarkBit(index); + uniqueFoundCount++; + } + } + else { + unfoundCount++; + if (returnIfUnfound) { + break; + } + } + } + + result.uniqueCount = uniqueFoundCount; + result.unfoundCount = unfoundCount; + return result; + } +#endif + /// + /// Copies this to an array. Used for DebugView + /// + /// + internal T[] ToArray() + { + T[] newArray = new T[Count]; + CopyTo(newArray); + return newArray; + } + +#if NEVER + /// + /// Internal method used for HashSetEqualityComparer. Compares set1 and set2 according + /// to specified comparer. + /// + /// Because items are hashed according to a specific equality comparer, we have to resort + /// to n^2 search if they're using different equality comparers. + /// + /// + /// + /// + /// + internal static bool HashSetEquals(RetrievableEntryHashSet set1, RetrievableEntryHashSet set2, IEqualityComparer comparer) { + // handle null cases first + if (set1 == null) { + return (set2 == null); + } + else if (set2 == null) { + // set1 != null + return false; + } + + // all comparers are the same; this is faster + if (AreEqualityComparersEqual(set1, set2)) { + if (set1.Count != set2.Count) { + return false; + } + // suffices to check subset + foreach (T item in set2) { + if (!set1.Contains(item)) { + return false; + } + } + return true; + } + else { // n^2 search because items are hashed according to their respective ECs + foreach (T set2Item in set2) { + bool found = false; + foreach (T set1Item in set1) { + if (comparer.Equals(set2Item, set1Item)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + } + + /// + /// Checks if equality comparers are equal. This is used for algorithms that can + /// speed up if it knows the other item has unique elements. I.e. if they're using + /// different equality comparers, then uniqueness assumption between sets break. + /// + /// + /// + /// + private static bool AreEqualityComparersEqual(RetrievableEntryHashSet set1, RetrievableEntryHashSet set2) { + return set1.Comparer.Equals(set2.Comparer); + } +#endif + /// + /// Workaround Comparers that throw ArgumentNullException for GetHashCode(null). + /// + /// + /// hash code + private int InternalGetHashCode(IKeyed item) + { + if (item == null) + { + return 0; + } + return _comparer.GetHashCode(item) & Lower31BitMask; + } + + #endregion + + // used for set checking operations (using enumerables) that rely on counting + internal struct ElementCount + { + internal int uniqueCount; + internal int unfoundCount; + } + + internal struct Slot + { + internal int hashCode; // Lower 31 bits of hash code, -1 if unused + internal T value; + internal int next; // Index of next entry, -1 if last + } + +#if !SILVERLIGHT + [Serializable()] + [System.Security.Permissions.HostProtection(MayLeakOnAbort = true)] +#endif + public struct Enumerator : IEnumerator, System.Collections.IEnumerator + { + private RetrievableEntryHashSet _set; + private int _index; + private int _version; + private T _current; + + internal Enumerator(RetrievableEntryHashSet set) + { + _set = set; + _index = 0; + _version = set._version; + _current = default(T); + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_version != _set._version) + { + throw new InvalidOperationException(); + } + + while (_index < _set._lastIndex) + { + if (_set._slots[_index].hashCode >= 0) + { + _current = _set._slots[_index].value; + _index++; + return true; + } + _index++; + } + _index = _set._lastIndex + 1; + _current = default(T); + return false; + } + + public T Current + { + get + { + return _current; + } + } + + Object System.Collections.IEnumerator.Current + { + get + { + if (_index == 0 || _index == _set._lastIndex + 1) + { + throw new InvalidOperationException(); + } + return Current; + } + } + + void System.Collections.IEnumerator.Reset() + { + if (_version != _set._version) + { + throw new InvalidOperationException(); + } + + _index = 0; + _current = default(T); + } + } + + /// + /// Wrapper is necessary because String doesn't implement IKeyed + /// + private struct KeyedObject : IKeyed + { + private string _name; + + internal KeyedObject(string name) + { + _name = name; + } + + string IKeyed.Key + { + get { return _name; } + } + + public override int GetHashCode() + { + ErrorUtilities.ThrowInternalError("should be using comparer"); + return -1; + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSetDebugView.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSetDebugView.cs new file mode 100644 index 00000000000..ce3ec5eb1bc --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/HashSetDebugView.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.Build.Collections +{ + /// + /// Debug view for HashSet + /// + /// + internal class HashSetDebugView where T : class, IKeyed + { + private RetrievableEntryHashSet _set; + + public HashSetDebugView(RetrievableEntryHashSet set) + { + if (set == null) + { + throw new ArgumentNullException("set"); + } + + _set = set; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + return _set.ToArray(); + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/BitHelper.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/BitHelper.cs new file mode 100644 index 00000000000..de0a9bedfd0 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/BitHelper.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections; +using System.Text; + +namespace System.Collections.Generic { + + /// + /// ABOUT: + /// Helps with operations that rely on bit marking to indicate whether an item in the + /// collection should be added, removed, visited already, etc. + /// + /// BitHelper doesn't allocate the array; you must pass in an array or ints allocated on the + /// stack or heap. ToIntArrayLength() tells you the int array size you must allocate. + /// + /// USAGE: + /// Suppose you need to represent a bit array of length (i.e. logical bit array length) + /// BIT_ARRAY_LENGTH. Then this is the suggested way to instantiate BitHelper: + /// *************************************************************************** + /// int intArrayLength = BitHelper.ToIntArrayLength(BIT_ARRAY_LENGTH); + /// BitHelper bitHelper; + /// if (intArrayLength less than stack alloc threshold) + /// int* m_arrayPtr = stackalloc int[intArrayLength]; + /// bitHelper = new BitHelper(m_arrayPtr, intArrayLength); + /// else + /// int[] m_arrayPtr = new int[intArrayLength]; + /// bitHelper = new BitHelper(m_arrayPtr, intArrayLength); + /// *************************************************************************** + /// + /// IMPORTANT: + /// The second ctor args, length, should be specified as the length of the int array, not + /// the logical bit array. Because length is used for bounds checking into the int array, + /// it's especially important to get this correct for the stackalloc version. See the code + /// samples above; this is the value gotten from ToIntArrayLength(). + /// + /// The length ctor argument is the only exception; for other methods -- MarkBit and + /// IsMarked -- pass in values as indices into the logical bit array, and it will be mapped + /// to the position within the array of ints. + /// + /// FUTURE OPTIMIZATIONS: + /// A method such as FindFirstMarked/Unmarked Bit would be useful for callers that operate + /// on a bit array and then need to loop over it. In particular, if it avoided visiting + /// every bit, it would allow good perf improvements when the bit array is sparse. + /// + unsafe internal class BitHelper { // should not be serialized + + private const byte MarkedBitFlag = 1; + private const byte IntSize = 32; + + // m_length of underlying int array (not logical bit array) + private int m_length; + + // ptr to stack alloc'd array of ints + [System.Security.SecurityCritical] + private int* m_arrayPtr; + + // array of ints + private int[] m_array; + + // whether to operate on stack alloc'd or heap alloc'd array + private bool useStackAlloc; + + /// + /// Instantiates a BitHelper with a heap alloc'd array of ints + /// + /// int array to hold bits + /// length of int array + [System.Security.SecurityCritical] + internal BitHelper(int* bitArrayPtr, int length) { + this.m_arrayPtr = bitArrayPtr; + this.m_length = length; + useStackAlloc = true; + } + + /// + /// Instantiates a BitHelper with a heap alloc'd array of ints + /// + /// int array to hold bits + /// length of int array + internal BitHelper(int[] bitArray, int length) { + this.m_array = bitArray; + this.m_length = length; + } + + /// + /// Mark bit at specified position + /// + /// + [System.Security.SecuritySafeCritical] + internal unsafe void MarkBit(int bitPosition) { + if (useStackAlloc) { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < m_length && bitArrayIndex >= 0) { + m_arrayPtr[bitArrayIndex] |= (MarkedBitFlag << (bitPosition % IntSize)); + } + } + else { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < m_length && bitArrayIndex >= 0) { + m_array[bitArrayIndex] |= (MarkedBitFlag << (bitPosition % IntSize)); + } + } + } + + /// + /// Is bit at specified position marked? + /// + /// + /// + [System.Security.SecuritySafeCritical] + internal unsafe bool IsMarked(int bitPosition) { + if (useStackAlloc) { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < m_length && bitArrayIndex >= 0) { + return ((m_arrayPtr[bitArrayIndex] & (MarkedBitFlag << (bitPosition % IntSize))) != 0); + } + return false; + } + else { + int bitArrayIndex = bitPosition / IntSize; + if (bitArrayIndex < m_length && bitArrayIndex >= 0) { + return ((m_array[bitArrayIndex] & (MarkedBitFlag << (bitPosition % IntSize))) != 0); + } + return false; + } + } + + /// + /// How many ints must be allocated to represent n bits. Returns (n+31)/32, but + /// avoids overflow + /// + /// + /// + internal static int ToIntArrayLength(int n) { + return n > 0 ? ((n - 1) / IntSize + 1) : 0; + } + + } +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashHelpers.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashHelpers.cs new file mode 100644 index 00000000000..1b1d758aa18 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashHelpers.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +#if !SILVERLIGHT +using System.Runtime.ConstrainedExecution; +#endif +using System.Text; + +namespace System.Collections.Generic { + + /// + /// Duplicated because internal to mscorlib + /// + internal static class HashHelpers { + // Table of prime numbers to use as hash table sizes. + // The entry used for capacity is the smallest prime number in this array + // that is larger than twice the previous capacity. + + internal static readonly int[] primes = { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369}; + +#if !SILVERLIGHT + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + internal static bool IsPrime(int candidate) { + if ((candidate & 1) != 0) { + int limit = (int)Math.Sqrt(candidate); + for (int divisor = 3; divisor <= limit; divisor += 2) { + if ((candidate % divisor) == 0) { + return false; + } + } + return true; + } + return (candidate == 2); + } + +#if !SILVERLIGHT + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + internal static int GetPrime(int min) { + Debug.Assert(min >= 0, "min less than zero; handle overflow checking before calling HashHelpers"); + + for (int i = 0; i < primes.Length; i++) { + int prime = primes[i]; + if (prime >= min) { + return prime; + } + } + + // Outside of our predefined table. Compute the hard way. + for (int i = (min | 1); i < Int32.MaxValue; i += 2) { + if (IsPrime(i)) { + return i; + } + } + return min; + } + + internal static int GetMinPrime() { + return primes[0]; + } + + // Returns size of hashtable to grow to. + internal static int ExpandPrime(int oldSize) + { + int newSize = 2 * oldSize; + + // Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newSize > MaxPrimeArrayLength) + return MaxPrimeArrayLength; + + return GetPrime(newSize); + } + + // This is the maximum prime smaller than Array.MaxArrayLength + internal const int MaxPrimeArrayLength = 0x7FEFFFFD; + } + +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSet.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSet.cs new file mode 100644 index 00000000000..7e0b8e2ddcf --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSet.cs @@ -0,0 +1,1461 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +#if !SILVERLIGHT +using System.Runtime.Serialization; +#endif +using System.Security.Permissions; +using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Security; +#if SILVERLIGHT +using System.Core; // for System.Core.SR +#endif + +namespace System.Collections.Generic { + + /// + /// Implementation notes: + /// This uses an array-based implementation similar to Dictionary, using a buckets array + /// to map hash values to the Slots array. Items in the Slots array that hash to the same value + /// are chained together through the "next" indices. + /// + /// The capacity is always prime; so during resizing, the capacity is chosen as the next prime + /// greater than double the last capacity. + /// + /// The underlying data structures are lazily initialized. Because of the observation that, + /// in practice, hashtables tend to contain only a few elements, the initial capacity is + /// set very small (3 elements) unless the ctor with a collection is used. + /// + /// The +/- 1 modifications in methods that add, check for containment, etc allow us to + /// distinguish a hash code of 0 from an uninitialized bucket. This saves us from having to + /// reset each bucket to -1 when resizing. See Contains, for example. + /// + /// Set methods such as UnionWith, IntersectWith, ExceptWith, and SymmetricExceptWith modify + /// this set. + /// + /// Some operations can perform faster if we can assume "other" contains unique elements + /// according to this equality comparer. The only times this is efficient to check is if + /// other is a hashset. Note that checking that it's a hashset alone doesn't suffice; we + /// also have to check that the hashset is using the same equality comparer. If other + /// has a different equality comparer, it will have unique elements according to its own + /// equality comparer, but not necessarily according to ours. Therefore, to go these + /// optimized routes we check that other is a hashset using the same equality comparer. + /// + /// A HashSet with no elements has the properties of the empty set. (See IsSubset, etc. for + /// special empty set checks.) + /// + /// A couple of methods have a special case if other is this (e.g. SymmetricExceptWith). + /// If we didn't have these checks, we could be iterating over the set and modifying at + /// the same time. + /// + /// + [DebuggerTypeProxy(typeof(System.Collections.Generic.HashSetDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [SuppressMessage("Microsoft.Naming","CA1710:IdentifiersShouldHaveCorrectSuffix", Justification="By design")] +#if SILVERLIGHT + public class HashSet : ICollection, ISet +#else + [Serializable()] + [System.Security.Permissions.HostProtection(MayLeakOnAbort = true)] + public class HashSet : ICollection, ISerializable, IDeserializationCallback, ISet +#endif + { + + // store lower 31 bits of hash code + private const int Lower31BitMask = 0x7FFFFFFF; + // cutoff point, above which we won't do stackallocs. This corresponds to 100 integers. + private const int StackAllocThreshold = 100; + // when constructing a hashset from an existing collection, it may contain duplicates, + // so this is used as the max acceptable excess ratio of capacity to count. Note that + // this is only used on the ctor and not to automatically shrink if the hashset has, e.g, + // a lot of adds followed by removes. Users must explicitly shrink by calling TrimExcess. + // This is set to 3 because capacity is acceptable as 2x rounded up to nearest prime. + private const int ShrinkThreshold = 3; + +#if !SILVERLIGHT + // constants for serialization + private const String CapacityName = "Capacity"; + private const String ElementsName = "Elements"; + private const String ComparerName = "Comparer"; + private const String VersionName = "Version"; +#endif + + private int[] m_buckets; + private Slot[] m_slots; + private int m_count; + private int m_lastIndex; + private int m_freeList; + private IEqualityComparer m_comparer; + private int m_version; + +#if !SILVERLIGHT + // temporary variable needed during deserialization + private SerializationInfo m_siInfo; +#endif + + #region Constructors + + public HashSet() + : this(EqualityComparer.Default) { } + + public HashSet(IEqualityComparer comparer) { + if (comparer == null) { + comparer = EqualityComparer.Default; + } + + this.m_comparer = comparer; + m_lastIndex = 0; + m_count = 0; + m_freeList = -1; + m_version = 0; + } + + public HashSet(IEnumerable collection) + : this(collection, EqualityComparer.Default) { } + + /// + /// Implementation Notes: + /// Since resizes are relatively expensive (require rehashing), this attempts to minimize + /// the need to resize by setting the initial capacity based on size of collection. + /// + /// + /// + public HashSet(IEnumerable collection, IEqualityComparer comparer) + : this(comparer) { + if (collection == null) { + throw new ArgumentNullException("collection"); + } + Contract.EndContractBlock(); + + // to avoid excess resizes, first set size based on collection's count. Collection + // may contain duplicates, so call TrimExcess if resulting hashset is larger than + // threshold + int suggestedCapacity = 0; + ICollection coll = collection as ICollection; + if (coll != null) { + suggestedCapacity = coll.Count; + } + Initialize(suggestedCapacity); + + this.UnionWith(collection); + if ((m_count == 0 && m_slots.Length > HashHelpers.GetMinPrime()) || + (m_count > 0 && m_slots.Length / m_count > ShrinkThreshold)) { + TrimExcess(); + } + } + +#if !SILVERLIGHT + protected HashSet(SerializationInfo info, StreamingContext context) { + // We can't do anything with the keys and values until the entire graph has been + // deserialized and we have a reasonable estimate that GetHashCode is not going to + // fail. For the time being, we'll just cache this. The graph is not valid until + // OnDeserialization has been called. + m_siInfo = info; + } +#endif + + #endregion + + #region ICollection methods + + /// + /// Add item to this hashset. This is the explicit implementation of the ICollection + /// interface. The other Add method returns bool indicating whether item was added. + /// + /// item to add + void ICollection.Add(T item) { + AddIfNotPresent(item); + } + + /// + /// Remove all items from this set. This clears the elements but not the underlying + /// buckets and slots array. Follow this call by TrimExcess to release these. + /// + public void Clear() { + if (m_lastIndex > 0) { + Debug.Assert(m_buckets != null, "m_buckets was null but m_lastIndex > 0"); + + // clear the elements so that the gc can reclaim the references. + // clear only up to m_lastIndex for m_slots + Array.Clear(m_slots, 0, m_lastIndex); + Array.Clear(m_buckets, 0, m_buckets.Length); + m_lastIndex = 0; + m_count = 0; + m_freeList = -1; + } + m_version++; + } + + /// + /// Checks if this hashset contains the item + /// + /// item to check for containment + /// true if item contained; false if not + public bool Contains(T item) { + if (m_buckets != null) { + int hashCode = InternalGetHashCode(item); + // see note at "HashSet" level describing why "- 1" appears in for loop + for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { + if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, item)) { + return true; + } + } + } + // either m_buckets is null or wasn't found + return false; + } + + /// + /// Copy items in this hashset to array, starting at arrayIndex + /// + /// array to add items to + /// index to start at + public void CopyTo(T[] array, int arrayIndex) { + CopyTo(array, arrayIndex, m_count); + } + + /// + /// Remove item from this hashset + /// + /// item to remove + /// true if removed; false if not (i.e. if the item wasn't in the HashSet) + public bool Remove(T item) { + if (m_buckets != null) { + int hashCode = InternalGetHashCode(item); + int bucket = hashCode % m_buckets.Length; + int last = -1; + for (int i = m_buckets[bucket] - 1; i >= 0; last = i, i = m_slots[i].next) { + if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, item)) { + if (last < 0) { + // first iteration; update buckets + m_buckets[bucket] = m_slots[i].next + 1; + } + else { + // subsequent iterations; update 'next' pointers + m_slots[last].next = m_slots[i].next; + } + m_slots[i].hashCode = -1; + m_slots[i].value = default(T); + m_slots[i].next = m_freeList; + + m_count--; + m_version++; + if (m_count == 0) { + m_lastIndex = 0; + m_freeList = -1; + } + else { + m_freeList = i; + } + return true; + } + } + } + // either m_buckets is null or wasn't found + return false; + } + + /// + /// Number of elements in this hashset + /// + public int Count { + get { return m_count; } + } + + /// + /// Whether this is readonly + /// + bool ICollection.IsReadOnly { + get { return false; } + } + + #endregion + + #region IEnumerable methods + + public Enumerator GetEnumerator() { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() { + return new Enumerator(this); + } + + #endregion + + #region ISerializable methods + +#if !SILVERLIGHT + [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] + [SecurityCritical] + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { + if (info == null) { + throw new ArgumentNullException("info"); + } + + // need to serialize version to avoid problems with serializing while enumerating + info.AddValue(VersionName, m_version); + info.AddValue(ComparerName, m_comparer, typeof(IEqualityComparer)); + info.AddValue(CapacityName, m_buckets == null ? 0 : m_buckets.Length); + if (m_buckets != null) { + T[] array = new T[m_count]; + CopyTo(array); + info.AddValue(ElementsName, array, typeof(T[])); + } + } +#endif + #endregion + + #region IDeserializationCallback methods + +#if !SILVERLIGHT + public virtual void OnDeserialization(Object sender) { + + if (m_siInfo == null) { + // It might be necessary to call OnDeserialization from a container if the + // container object also implements OnDeserialization. However, remoting will + // call OnDeserialization again. We can return immediately if this function is + // called twice. Note we set m_siInfo to null at the end of this method. + return; + } + + int capacity = m_siInfo.GetInt32(CapacityName); + m_comparer = (IEqualityComparer)m_siInfo.GetValue(ComparerName, typeof(IEqualityComparer)); + m_freeList = -1; + + if (capacity != 0) { + m_buckets = new int[capacity]; + m_slots = new Slot[capacity]; + + T[] array = (T[])m_siInfo.GetValue(ElementsName, typeof(T[])); + + if (array == null) { + throw new SerializationException(SR.GetString(SR.Serialization_MissingKeys)); + } + + // there are no resizes here because we already set capacity above + for (int i = 0; i < array.Length; i++) { + AddIfNotPresent(array[i]); + } + } + else { + m_buckets = null; + } + + m_version = m_siInfo.GetInt32(VersionName); + m_siInfo = null; + } +#endif + + #endregion + + #region HashSet methods + + /// + /// Add item to this HashSet. Returns bool indicating whether item was added (won't be + /// added if already present) + /// + /// + /// true if added, false if already present + public bool Add(T item) { + return AddIfNotPresent(item); + } + + /// + /// Take the union of this HashSet with other. Modifies this set. + /// + /// Implementation note: GetSuggestedCapacity (to increase capacity in advance avoiding + /// multiple resizes ended up not being useful in practice; quickly gets to the + /// point where it's a wasteful check. + /// + /// enumerable with items to add + public void UnionWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + foreach (T item in other) { + AddIfNotPresent(item); + } + } + + /// + /// Takes the intersection of this set with other. Modifies this set. + /// + /// Implementation Notes: + /// We get better perf if other is a hashset using same equality comparer, because we + /// get constant contains check in other. Resulting cost is O(n1) to iterate over this. + /// + /// If we can't go above route, iterate over the other and mark intersection by checking + /// contains in this. Then loop over and delete any unmarked elements. Total cost is n2+n1. + /// + /// Attempts to return early based on counts alone, using the property that the + /// intersection of anything with the empty set is the empty set. + /// + /// enumerable with items to add + public void IntersectWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // intersection of anything with empty set is empty set, so return if count is 0 + if (m_count == 0) { + return; + } + + // if other is empty, intersection is empty set; remove all elements and we're done + // can only figure this out if implements ICollection. (IEnumerable has no count) + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + if (otherAsCollection.Count == 0) { + Clear(); + return; + } + + HashSet otherAsSet = other as HashSet; + // faster if other is a hashset using same equality comparer; so check + // that other is a hashset using the same equality comparer. + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + IntersectWithHashSetWithSameEC(otherAsSet); + return; + } + } + + IntersectWithEnumerable(other); + } + + /// + /// Remove items in other from this set. Modifies this set. + /// + /// enumerable with items to remove + public void ExceptWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // this is already the enpty set; return + if (m_count == 0) { + return; + } + + // special case if other is this; a set minus itself is the empty set + if (other == this) { + Clear(); + return; + } + + // remove every element in other from this + foreach (T element in other) { + Remove(element); + } + } + + /// + /// Takes symmetric difference (XOR) with other and this set. Modifies this set. + /// + /// enumerable with items to XOR + public void SymmetricExceptWith(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // if set is empty, then symmetric difference is other + if (m_count == 0) { + UnionWith(other); + return; + } + + // special case this; the symmetric difference of a set with itself is the empty set + if (other == this) { + Clear(); + return; + } + + HashSet otherAsSet = other as HashSet; + // If other is a HashSet, it has unique elements according to its equality comparer, + // but if they're using different equality comparers, then assumption of uniqueness + // will fail. So first check if other is a hashset using the same equality comparer; + // symmetric except is a lot faster and avoids bit array allocations if we can assume + // uniqueness + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + SymmetricExceptWithUniqueHashSet(otherAsSet); + } + else { + SymmetricExceptWithEnumerable(other); + } + } + + /// + /// Checks if this is a subset of other. + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a subset of anything, including the empty set + /// 2. If other has unique elements according to this equality comparer, and this has more + /// elements than other, then it can't be a subset. + /// + /// Furthermore, if other is a hashset using the same equality comparer, we can use a + /// faster element-wise check. + /// + /// + /// true if this is a subset of other; false if not + public bool IsSubsetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // The empty set is a subset of any set + if (m_count == 0) { + return true; + } + + HashSet otherAsSet = other as HashSet; + // faster if other has unique elements according to this equality comparer; so check + // that other is a hashset using the same equality comparer. + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + // if this has more elements then it can't be a subset + if (m_count > otherAsSet.Count) { + return false; + } + + // already checked that we're using same equality comparer. simply check that + // each element in this is contained in other. + return IsSubsetOfHashSetWithSameEC(otherAsSet); + } + else { + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == m_count && result.unfoundCount >= 0); + } + } + + /// + /// Checks if this is a proper subset of other (i.e. strictly contained in) + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it's a proper subset of a set that contains at least + /// one element, but it's not a proper subset of the empty set. + /// 2. If other has unique elements according to this equality comparer, and this has >= + /// the number of elements in other, then this can't be a proper subset. + /// + /// Furthermore, if other is a hashset using the same equality comparer, we can use a + /// faster element-wise check. + /// + /// + /// true if this is a proper subset of other; false if not + public bool IsProperSubsetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // the empty set is a proper subset of anything but the empty set + if (m_count == 0) { + return otherAsCollection.Count > 0; + } + HashSet otherAsSet = other as HashSet; + // faster if other is a hashset (and we're using same equality comparer) + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + if (m_count >= otherAsSet.Count) { + return false; + } + // this has strictly less than number of items in other, so the following + // check suffices for proper subset. + return IsSubsetOfHashSetWithSameEC(otherAsSet); + } + } + + ElementCount result = CheckUniqueAndUnfoundElements(other, false); + return (result.uniqueCount == m_count && result.unfoundCount > 0); + + } + + /// + /// Checks if this is a superset of other + /// + /// Implementation Notes: + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If other has no elements (it's the empty set), then this is a superset, even if this + /// is also the empty set. + /// 2. If other has unique elements according to this equality comparer, and this has less + /// than the number of elements in other, then this can't be a superset + /// + /// + /// + /// true if this is a superset of other; false if not + public bool IsSupersetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // try to fall out early based on counts + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // if other is the empty set then this is a superset + if (otherAsCollection.Count == 0) { + return true; + } + HashSet otherAsSet = other as HashSet; + // try to compare based on counts alone if other is a hashset with + // same equality comparer + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + if (otherAsSet.Count > m_count) { + return false; + } + } + } + + return ContainsAllElements(other); + } + + /// + /// Checks if this is a proper superset of other (i.e. other strictly contained in this) + /// + /// Implementation Notes: + /// This is slightly more complicated than above because we have to keep track if there + /// was at least one element not contained in other. + /// + /// The following properties are used up-front to avoid element-wise checks: + /// 1. If this is the empty set, then it can't be a proper superset of any set, even if + /// other is the empty set. + /// 2. If other is an empty set and this contains at least 1 element, then this is a proper + /// superset. + /// 3. If other has unique elements according to this equality comparer, and other's count + /// is greater than or equal to this count, then this can't be a proper superset + /// + /// Furthermore, if other has unique elements according to this equality comparer, we can + /// use a faster element-wise check. + /// + /// + /// true if this is a proper superset of other; false if not + public bool IsProperSupersetOf(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + // the empty set isn't a proper superset of any set. + if (m_count == 0) { + return false; + } + + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // if other is the empty set then this is a superset + if (otherAsCollection.Count == 0) { + // note that this has at least one element, based on above check + return true; + } + HashSet otherAsSet = other as HashSet; + // faster if other is a hashset with the same equality comparer + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + if (otherAsSet.Count >= m_count) { + return false; + } + // now perform element check + return ContainsAllElements(otherAsSet); + } + } + // couldn't fall out in the above cases; do it the long way + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount < m_count && result.unfoundCount == 0); + + } + + /// + /// Checks if this set overlaps other (i.e. they share at least one item) + /// + /// + /// true if these have at least one common element; false if disjoint + public bool Overlaps(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + if (m_count == 0) { + return false; + } + + foreach (T element in other) { + if (Contains(element)) { + return true; + } + } + return false; + } + + /// + /// Checks if this and other contain the same elements. This is set equality: + /// duplicates and order are ignored + /// + /// + /// + public bool SetEquals(IEnumerable other) { + if (other == null) { + throw new ArgumentNullException("other"); + } + Contract.EndContractBlock(); + + HashSet otherAsSet = other as HashSet; + // faster if other is a hashset and we're using same equality comparer + if (otherAsSet != null && AreEqualityComparersEqual(this, otherAsSet)) { + // attempt to return early: since both contain unique elements, if they have + // different counts, then they can't be equal + if (m_count != otherAsSet.Count) { + return false; + } + + // already confirmed that the sets have the same number of distinct elements, so if + // one is a superset of the other then they must be equal + return ContainsAllElements(otherAsSet); + } + else { + ICollection otherAsCollection = other as ICollection; + if (otherAsCollection != null) { + // if this count is 0 but other contains at least one element, they can't be equal + if (m_count == 0 && otherAsCollection.Count > 0) { + return false; + } + } + ElementCount result = CheckUniqueAndUnfoundElements(other, true); + return (result.uniqueCount == m_count && result.unfoundCount == 0); + } + } + + public void CopyTo(T[] array) { CopyTo(array, 0, m_count); } + + public void CopyTo(T[] array, int arrayIndex, int count) { + if (array == null) { + throw new ArgumentNullException("array"); + } + Contract.EndContractBlock(); + + // check array index valid index into array + if (arrayIndex < 0) { + throw new ArgumentOutOfRangeException("arrayIndex", SR.GetString(SR.ArgumentOutOfRange_NeedNonNegNum)); + } + + // also throw if count less than 0 + if (count < 0) { + throw new ArgumentOutOfRangeException("count", SR.GetString(SR.ArgumentOutOfRange_NeedNonNegNum)); + } + + // will array, starting at arrayIndex, be able to hold elements? Note: not + // checking arrayIndex >= array.Length (consistency with list of allowing + // count of 0; subsequent check takes care of the rest) + if (arrayIndex > array.Length || count > array.Length - arrayIndex) { + throw new ArgumentException(SR.GetString(SR.Arg_ArrayPlusOffTooSmall)); + } + + int numCopied = 0; + for (int i = 0; i < m_lastIndex && numCopied < count; i++) { + if (m_slots[i].hashCode >= 0) { + array[arrayIndex + numCopied] = m_slots[i].value; + numCopied++; + } + } + } + + /// + /// Remove elements that match specified predicate. Returns the number of elements removed + /// + /// + /// + public int RemoveWhere(Predicate match) { + if (match == null) { + throw new ArgumentNullException("match"); + } + Contract.EndContractBlock(); + + int numRemoved = 0; + for (int i = 0; i < m_lastIndex; i++) { + if (m_slots[i].hashCode >= 0) { + // cache value in case delegate removes it + T value = m_slots[i].value; + if (match(value)) { + // check again that remove actually removed it + if (Remove(value)) { + numRemoved++; + } + } + } + } + return numRemoved; + } + + /// + /// Gets the IEqualityComparer that is used to determine equality of keys for + /// the HashSet. + /// + public IEqualityComparer Comparer { + get { + return m_comparer; + } + } + + /// + /// Sets the capacity of this list to the size of the list (rounded up to nearest prime), + /// unless count is 0, in which case we release references. + /// + /// This method can be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and release all + /// memory referenced by the list, execute the following statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + public void TrimExcess() { + Debug.Assert(m_count >= 0, "m_count is negative"); + + if (m_count == 0) { + // if count is zero, clear references + m_buckets = null; + m_slots = null; + m_version++; + } + else { + Debug.Assert(m_buckets != null, "m_buckets was null but m_count > 0"); + + // similar to IncreaseCapacity but moves down elements in case add/remove/etc + // caused fragmentation + int newSize = HashHelpers.GetPrime(m_count); + Slot[] newSlots = new Slot[newSize]; + int[] newBuckets = new int[newSize]; + + // move down slots and rehash at the same time. newIndex keeps track of current + // position in newSlots array + int newIndex = 0; + for (int i = 0; i < m_lastIndex; i++) { + if (m_slots[i].hashCode >= 0) { + newSlots[newIndex] = m_slots[i]; + + // rehash + int bucket = newSlots[newIndex].hashCode % newSize; + newSlots[newIndex].next = newBuckets[bucket] - 1; + newBuckets[bucket] = newIndex + 1; + + newIndex++; + } + } + + Debug.Assert(newSlots.Length <= m_slots.Length, "capacity increased after TrimExcess"); + + m_lastIndex = newIndex; + m_slots = newSlots; + m_buckets = newBuckets; + m_freeList = -1; + } + } + +#if !SILVERLIGHT || FEATURE_NETCORE + /// + /// Used for deep equality of HashSet testing + /// + /// + public static IEqualityComparer> CreateSetComparer() { + return new HashSetEqualityComparer(); + } +#endif + + #endregion + + #region Helper methods + + /// + /// Initializes buckets and slots arrays. Uses suggested capacity by finding next prime + /// greater than or equal to capacity. + /// + /// + private void Initialize(int capacity) { + Debug.Assert(m_buckets == null, "Initialize was called but m_buckets was non-null"); + + int size = HashHelpers.GetPrime(capacity); + + m_buckets = new int[size]; + m_slots = new Slot[size]; + } + + /// + /// Expand to new capacity. New capacity is next prime greater than or equal to suggested + /// size. This is called when the underlying array is filled. This performs no + /// defragmentation, allowing faster execution; note that this is reasonable since + /// AddIfNotPresent attempts to insert new elements in re-opened spots. + /// + /// + private void IncreaseCapacity() { + Debug.Assert(m_buckets != null, "IncreaseCapacity called on a set with no elements"); + + int newSize = HashHelpers.ExpandPrime(m_count); + if (newSize <= m_count) { + throw new ArgumentException(SR.GetString(SR.Arg_HSCapacityOverflow)); + } + + // Able to increase capacity; copy elements to larger array and rehash + Slot[] newSlots = new Slot[newSize]; + if (m_slots != null) { + Array.Copy(m_slots, 0, newSlots, 0, m_lastIndex); + } + + int[] newBuckets = new int[newSize]; + for (int i = 0; i < m_lastIndex; i++) { + int bucket = newSlots[i].hashCode % newSize; + newSlots[i].next = newBuckets[bucket] - 1; + newBuckets[bucket] = i + 1; + } + m_slots = newSlots; + m_buckets = newBuckets; + + } + + /// + /// Adds value to HashSet if not contained already + /// Returns true if added and false if already present + /// + /// value to find + /// + private bool AddIfNotPresent(T value) { + if (m_buckets == null) { + Initialize(0); + } + + int hashCode = InternalGetHashCode(value); + int bucket = hashCode % m_buckets.Length; + for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { + if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, value)) { + return false; + } + } + int index; + if (m_freeList >= 0) { + index = m_freeList; + m_freeList = m_slots[index].next; + } + else { + if (m_lastIndex == m_slots.Length) { + IncreaseCapacity(); + // this will change during resize + bucket = hashCode % m_buckets.Length; + } + index = m_lastIndex; + m_lastIndex++; + } + m_slots[index].hashCode = hashCode; + m_slots[index].value = value; + m_slots[index].next = m_buckets[bucket] - 1; + m_buckets[bucket] = index + 1; + m_count++; + m_version++; + return true; + } + + /// + /// Checks if this contains of other's elements. Iterates over other's elements and + /// returns false as soon as it finds an element in other that's not in this. + /// Used by SupersetOf, ProperSupersetOf, and SetEquals. + /// + /// + /// + private bool ContainsAllElements(IEnumerable other) { + foreach (T element in other) { + if (!Contains(element)) { + return false; + } + } + return true; + } + + /// + /// Implementation Notes: + /// If other is a hashset and is using same equality comparer, then checking subset is + /// faster. Simply check that each element in this is in other. + /// + /// Note: if other doesn't use same equality comparer, then Contains check is invalid, + /// which is why callers must take are of this. + /// + /// If callers are concerned about whether this is a proper subset, they take care of that. + /// + /// + /// + /// + private bool IsSubsetOfHashSetWithSameEC(HashSet other) { + + foreach (T item in this) { + if (!other.Contains(item)) { + return false; + } + } + return true; + } + + /// + /// If other is a hashset that uses same equality comparer, intersect is much faster + /// because we can use other's Contains + /// + /// + private void IntersectWithHashSetWithSameEC(HashSet other) { + for (int i = 0; i < m_lastIndex; i++) { + if (m_slots[i].hashCode >= 0) { + T item = m_slots[i].value; + if (!other.Contains(item)) { + Remove(item); + } + } + } + } + + /// + /// Iterate over other. If contained in this, mark an element in bit array corresponding to + /// its position in m_slots. If anything is unmarked (in bit array), remove it. + /// + /// This attempts to allocate on the stack, if below StackAllocThreshold. + /// + /// + [System.Security.SecuritySafeCritical] + private unsafe void IntersectWithEnumerable(IEnumerable other) { + Debug.Assert(m_buckets != null, "m_buckets shouldn't be null; callers should check first"); + + // keep track of current last index; don't want to move past the end of our bit array + // (could happen if another thread is modifying the collection) + int originalLastIndex = m_lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + BitHelper bitHelper; + if (intArrayLength <= StackAllocThreshold) { + int* bitArrayPtr = stackalloc int[intArrayLength]; + bitHelper = new BitHelper(bitArrayPtr, intArrayLength); + } + else { + int[] bitArray = new int[intArrayLength]; + bitHelper = new BitHelper(bitArray, intArrayLength); + } + + // mark if contains: find index of in slots array and mark corresponding element in bit array + foreach (T item in other) { + int index = InternalIndexOf(item); + if (index >= 0) { + bitHelper.MarkBit(index); + } + } + + // if anything unmarked, remove it. Perf can be optimized here if BitHelper had a + // FindFirstUnmarked method. + for (int i = 0; i < originalLastIndex; i++) { + if (m_slots[i].hashCode >= 0 && !bitHelper.IsMarked(i)) { + Remove(m_slots[i].value); + } + } + } + + /// + /// Used internally by set operations which have to rely on bit array marking. This is like + /// Contains but returns index in slots array. + /// + /// + /// + private int InternalIndexOf(T item) { + Debug.Assert(m_buckets != null, "m_buckets was null; callers should check first"); + + int hashCode = InternalGetHashCode(item); + for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { + if ((m_slots[i].hashCode) == hashCode && m_comparer.Equals(m_slots[i].value, item)) { + return i; + } + } + // wasn't found + return -1; + } + + /// + /// if other is a set, we can assume it doesn't have duplicate elements, so use this + /// technique: if can't remove, then it wasn't present in this set, so add. + /// + /// As with other methods, callers take care of ensuring that other is a hashset using the + /// same equality comparer. + /// + /// + private void SymmetricExceptWithUniqueHashSet(HashSet other) { + foreach (T item in other) { + if (!Remove(item)) { + AddIfNotPresent(item); + } + } + } + + /// + /// Implementation notes: + /// + /// Used for symmetric except when other isn't a HashSet. This is more tedious because + /// other may contain duplicates. HashSet technique could fail in these situations: + /// 1. Other has a duplicate that's not in this: HashSet technique would add then + /// remove it. + /// 2. Other has a duplicate that's in this: HashSet technique would remove then add it + /// back. + /// In general, its presence would be toggled each time it appears in other. + /// + /// This technique uses bit marking to indicate whether to add/remove the item. If already + /// present in collection, it will get marked for deletion. If added from other, it will + /// get marked as something not to remove. + /// + /// + /// + [System.Security.SecuritySafeCritical] + private unsafe void SymmetricExceptWithEnumerable(IEnumerable other) { + int originalLastIndex = m_lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + BitHelper itemsToRemove; + BitHelper itemsAddedFromOther; + if (intArrayLength <= StackAllocThreshold / 2) { + int* itemsToRemovePtr = stackalloc int[intArrayLength]; + itemsToRemove = new BitHelper(itemsToRemovePtr, intArrayLength); + + int* itemsAddedFromOtherPtr = stackalloc int[intArrayLength]; + itemsAddedFromOther = new BitHelper(itemsAddedFromOtherPtr, intArrayLength); + } + else { + int[] itemsToRemoveArray = new int[intArrayLength]; + itemsToRemove = new BitHelper(itemsToRemoveArray, intArrayLength); + + int[] itemsAddedFromOtherArray = new int[intArrayLength]; + itemsAddedFromOther = new BitHelper(itemsAddedFromOtherArray, intArrayLength); + } + + foreach (T item in other) { + int location = 0; + bool added = AddOrGetLocation(item, out location); + if (added) { + // wasn't already present in collection; flag it as something not to remove + // *NOTE* if location is out of range, we should ignore. BitHelper will + // detect that it's out of bounds and not try to mark it. But it's + // expected that location could be out of bounds because adding the item + // will increase m_lastIndex as soon as all the free spots are filled. + itemsAddedFromOther.MarkBit(location); + } + else { + // already there...if not added from other, mark for remove. + // *NOTE* Even though BitHelper will check that location is in range, we want + // to check here. There's no point in checking items beyond originalLastIndex + // because they could not have been in the original collection + if (location < originalLastIndex && !itemsAddedFromOther.IsMarked(location)) { + itemsToRemove.MarkBit(location); + } + } + } + + // if anything marked, remove it + for (int i = 0; i < originalLastIndex; i++) { + if (itemsToRemove.IsMarked(i)) { + Remove(m_slots[i].value); + } + } + } + + /// + /// Add if not already in hashset. Returns an out param indicating index where added. This + /// is used by SymmetricExcept because it needs to know the following things: + /// - whether the item was already present in the collection or added from other + /// - where it's located (if already present, it will get marked for removal, otherwise + /// marked for keeping) + /// + /// + /// + /// + private bool AddOrGetLocation(T value, out int location) { + Debug.Assert(m_buckets != null, "m_buckets is null, callers should have checked"); + + int hashCode = InternalGetHashCode(value); + int bucket = hashCode % m_buckets.Length; + for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { + if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, value)) { + location = i; + return false; //already present + } + } + int index; + if (m_freeList >= 0) { + index = m_freeList; + m_freeList = m_slots[index].next; + } + else { + if (m_lastIndex == m_slots.Length) { + IncreaseCapacity(); + // this will change during resize + bucket = hashCode % m_buckets.Length; + } + index = m_lastIndex; + m_lastIndex++; + } + m_slots[index].hashCode = hashCode; + m_slots[index].value = value; + m_slots[index].next = m_buckets[bucket] - 1; + m_buckets[bucket] = index + 1; + m_count++; + m_version++; + location = index; + return true; + } + + /// + /// Determines counts that can be used to determine equality, subset, and superset. This + /// is only used when other is an IEnumerable and not a HashSet. If other is a HashSet + /// these properties can be checked faster without use of marking because we can assume + /// other has no duplicates. + /// + /// The following count checks are performed by callers: + /// 1. Equals: checks if unfoundCount = 0 and uniqueFoundCount = m_count; i.e. everything + /// in other is in this and everything in this is in other + /// 2. Subset: checks if unfoundCount >= 0 and uniqueFoundCount = m_count; i.e. other may + /// have elements not in this and everything in this is in other + /// 3. Proper subset: checks if unfoundCount > 0 and uniqueFoundCount = m_count; i.e + /// other must have at least one element not in this and everything in this is in other + /// 4. Proper superset: checks if unfound count = 0 and uniqueFoundCount strictly less + /// than m_count; i.e. everything in other was in this and this had at least one element + /// not contained in other. + /// + /// An earlier implementation used delegates to perform these checks rather than returning + /// an ElementCount struct; however this was changed due to the perf overhead of delegates. + /// + /// + /// Allows us to finish faster for equals and proper superset + /// because unfoundCount must be 0. + /// + [System.Security.SecuritySafeCritical] + private unsafe ElementCount CheckUniqueAndUnfoundElements(IEnumerable other, bool returnIfUnfound) { + ElementCount result; + + // need special case in case this has no elements. + if (m_count == 0) { + int numElementsInOther = 0; + foreach (T item in other) { + numElementsInOther++; + // break right away, all we want to know is whether other has 0 or 1 elements + break; + } + result.uniqueCount = 0; + result.unfoundCount = numElementsInOther; + return result; + } + + + Debug.Assert((m_buckets != null) && (m_count > 0), "m_buckets was null but count greater than 0"); + + int originalLastIndex = m_lastIndex; + int intArrayLength = BitHelper.ToIntArrayLength(originalLastIndex); + + BitHelper bitHelper; + if (intArrayLength <= StackAllocThreshold) { + int* bitArrayPtr = stackalloc int[intArrayLength]; + bitHelper = new BitHelper(bitArrayPtr, intArrayLength); + } + else { + int[] bitArray = new int[intArrayLength]; + bitHelper = new BitHelper(bitArray, intArrayLength); + } + + // count of items in other not found in this + int unfoundCount = 0; + // count of unique items in other found in this + int uniqueFoundCount = 0; + + foreach (T item in other) { + int index = InternalIndexOf(item); + if (index >= 0) { + if (!bitHelper.IsMarked(index)) { + // item hasn't been seen yet + bitHelper.MarkBit(index); + uniqueFoundCount++; + } + } + else { + unfoundCount++; + if (returnIfUnfound) { + break; + } + } + } + + result.uniqueCount = uniqueFoundCount; + result.unfoundCount = unfoundCount; + return result; + } + + /// + /// Copies this to an array. Used for DebugView + /// + /// + internal T[] ToArray() { + T[] newArray = new T[Count]; + CopyTo(newArray); + return newArray; + } + + /// + /// Internal method used for HashSetEqualityComparer. Compares set1 and set2 according + /// to specified comparer. + /// + /// Because items are hashed according to a specific equality comparer, we have to resort + /// to n^2 search if they're using different equality comparers. + /// + /// + /// + /// + /// + internal static bool HashSetEquals(HashSet set1, HashSet set2, IEqualityComparer comparer) { + // handle null cases first + if (set1 == null) { + return (set2 == null); + } + else if (set2 == null) { + // set1 != null + return false; + } + + // all comparers are the same; this is faster + if (AreEqualityComparersEqual(set1, set2)) { + if (set1.Count != set2.Count) { + return false; + } + // suffices to check subset + foreach (T item in set2) { + if (!set1.Contains(item)) { + return false; + } + } + return true; + } + else { // n^2 search because items are hashed according to their respective ECs + foreach (T set2Item in set2) { + bool found = false; + foreach (T set1Item in set1) { + if (comparer.Equals(set2Item, set1Item)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + } + + /// + /// Checks if equality comparers are equal. This is used for algorithms that can + /// speed up if it knows the other item has unique elements. I.e. if they're using + /// different equality comparers, then uniqueness assumption between sets break. + /// + /// + /// + /// + private static bool AreEqualityComparersEqual(HashSet set1, HashSet set2) { + return set1.Comparer.Equals(set2.Comparer); + } + + /// + /// Workaround Comparers that throw ArgumentNullException for GetHashCode(null). + /// + /// + /// hash code + private int InternalGetHashCode(T item) { + if (item == null) { + return 0; + } + return m_comparer.GetHashCode(item) & Lower31BitMask; + } + + #endregion + + // used for set checking operations (using enumerables) that rely on counting + internal struct ElementCount { + internal int uniqueCount; + internal int unfoundCount; + } + + internal struct Slot { + internal int hashCode; // Lower 31 bits of hash code, -1 if unused + internal T value; + internal int next; // Index of next entry, -1 if last + } + +#if !SILVERLIGHT + [Serializable()] + [System.Security.Permissions.HostProtection(MayLeakOnAbort = true)] +#endif + public struct Enumerator : IEnumerator, System.Collections.IEnumerator { + private HashSet set; + private int index; + private int version; + private T current; + + internal Enumerator(HashSet set) { + this.set = set; + index = 0; + version = set.m_version; + current = default(T); + } + + public void Dispose() { + } + + public bool MoveNext() { + if (version != set.m_version) { + throw new InvalidOperationException(SR.GetString(SR.InvalidOperation_EnumFailedVersion)); + } + + while (index < set.m_lastIndex) { + if (set.m_slots[index].hashCode >= 0) { + current = set.m_slots[index].value; + index++; + return true; + } + index++; + } + index = set.m_lastIndex + 1; + current = default(T); + return false; + } + + public T Current { + get { + return current; + } + } + + Object System.Collections.IEnumerator.Current { + get { + if (index == 0 || index == set.m_lastIndex + 1) { + throw new InvalidOperationException(SR.GetString(SR.InvalidOperation_EnumOpCantHappen)); + } + return Current; + } + } + + void System.Collections.IEnumerator.Reset() { + if (version != set.m_version) { + throw new InvalidOperationException(SR.GetString(SR.InvalidOperation_EnumFailedVersion)); + } + + index = 0; + current = default(T); + } + } + } + +} diff --git a/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSetDebugView.cs b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSetDebugView.cs new file mode 100644 index 00000000000..e409ed204fe --- /dev/null +++ b/src/XMakeBuildEngine/Collections/RetrievableEntryHashSet/Originals/HashSetDebugView.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace System.Collections.Generic { + + /// + /// Debug view for HashSet + /// + /// + internal class HashSetDebugView { + private HashSet set; + + public HashSetDebugView(HashSet set) { + if (set == null) { + throw new ArgumentNullException("set"); + } + + this.set = set; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items { + get { + return set.ToArray(); + } + } + } + +} diff --git a/src/XMakeBuildEngine/Collections/Triple.cs b/src/XMakeBuildEngine/Collections/Triple.cs new file mode 100644 index 00000000000..e30594a4080 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/Triple.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A struct of three objects +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Collections +{ + /// + /// A struct containing three objects + /// + /// Type of first object + /// Type of second object + /// Type of third object + internal struct Triple + { + /// + /// First + /// + private A _first; + + /// + /// Second + /// + private B _second; + + /// + /// Third + /// + private C _third; + + /// + /// Constructor + /// + public Triple(A first, B second, C third) + { + _first = first; + _second = second; + _third = third; + } + + /// + /// First + /// + public A First + { + get { return _first; } + } + + /// + /// Second + /// + public B Second + { + get { return _second; } + } + + /// + /// Third + /// + public C Third + { + get { return _third; } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Collections/WeakDictionary.cs b/src/XMakeBuildEngine/Collections/WeakDictionary.cs new file mode 100644 index 00000000000..0a919ada19a --- /dev/null +++ b/src/XMakeBuildEngine/Collections/WeakDictionary.cs @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dictionary that does not hold strong references to either keys or values +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections.ObjectModel; +using System.Collections; +using Microsoft.Build.Shared; +using System.Diagnostics; + +namespace Microsoft.Build.Collections +{ + /// + /// Dictionary that does not prevent keys or values from being garbage collected. + /// + /// + /// Example scenarios: + /// - store wrappers V around K, where given a K can find the V, but K and V can still be collected + /// - store handles K to V, where given a K can find V, but K and V can still be collected + /// PERF: Like a regular dictionary, the collection will never reduce its underlying size. This dictionary may be more + /// prone to get overly capacious. If profiles show that is a problem, Scavenge could shrink the underlying dictionary + /// based on some policy, e.g., if the number of remaining entries are less than 10% of the size of the underlying dictionary. + /// + /// Type of key + /// Type of value + [DebuggerDisplay("#Entries={backingDictionary.Count}")] + internal class WeakDictionary + where K : class + where V : class + { + /// + /// Backing dictionary + /// + private Dictionary, WeakReference> _backingDictionary; + + /// + /// Improvised capacity. See comment in item setter. + /// + private int _capacity = 10; + + /// + /// Constructor for a collection using the default key comparer + /// + internal WeakDictionary() + : this(null) + { + } + + /// + /// Constructor taking a specified comparer for the keys + /// + internal WeakDictionary(IEqualityComparer keyComparer) + { + IEqualityComparer> equalityComparer = new WeakReferenceEqualityComparer(keyComparer); + _backingDictionary = new Dictionary, WeakReference>(equalityComparer); + } + + /// + /// Count of entries. + /// Some entries may represent keys or values that have already been garbage collected. + /// To clean these out call . + /// + internal int Count + { + get { return _backingDictionary.Count; } + } + + /// + /// Gets or sets the entry whose key equates to the specified key. + /// Getter throws KeyNotFoundException if key is not found. + /// Setter adds entry if key is not found. + /// + /// + /// If we find the entry but its target is null, we take the opportunity + /// to remove the entry, as if the GC had done it. + /// + internal V this[K key] + { + get + { + WeakReference wrappedKey = new WeakReference(key); + + WeakReference wrappedValue = _backingDictionary[wrappedKey]; + + V value = wrappedValue.Target; + + if (value == null) + { + Remove(key); + + // Trigger KeyNotFoundException + wrappedValue = _backingDictionary[wrappedKey]; + + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + return value; + } + + set + { + ErrorUtilities.VerifyThrowInternalNull(value, "value"); + + WeakReference wrappedKey = new WeakReference(key); + + // Make some attempt to prevent dictionary growing forever with + // entries whose underlying key or value has already been collected. + // We do not have access to the dictionary's true capacity or growth + // method, so we improvise with our own. + // So attempt to make room for the upcoming add before we do it. + if (_backingDictionary.Count == _capacity) + { + Scavenge(); + + // If that didn't do anything, raise the capacity at which + // we next scavenge. Note that we never shrink, but neither + // does the underlying dictionary. + if (_backingDictionary.Count == _capacity) + { + _capacity = _backingDictionary.Count * 2; + } + } + + _backingDictionary[wrappedKey] = new WeakReference(value); + } + } + + /// + /// Whether there is a key present with the specified key + /// + /// + /// As usual, don't just call Contained as the wrapped value may be null. + /// + internal bool Contains(K key) + { + V value; + bool contained = TryGetValue(key, out value); + + return contained; + } + + /// + /// Attempts to get the value for the provided key. + /// Returns true if the key is found, otherwise false. + /// + /// + /// If we find the entry but its target is null, we take the opportunity + /// to remove the entry, as if the GC had done it. + /// + internal bool TryGetValue(K key, out V value) + { + WeakReference wrappedKey = new WeakReference(key); + + WeakReference wrappedValue; + if (_backingDictionary.TryGetValue(wrappedKey, out wrappedValue)) + { + value = wrappedValue.Target; + + if (value == null) + { + Remove(key); + return false; + } + + return true; + } + + value = null; + return false; + } + + /// + /// Removes an entry with the specified key. + /// Returns true if found, false otherwise. + /// + internal bool Remove(K key) + { + return _backingDictionary.Remove(new WeakReference(key)); + } + + /// + /// Remove any entries from the dictionary that represent keys or values + /// that have been garbage collected. + /// Returns the number of entries removed. + /// + internal int Scavenge() + { + List> remove = null; + + foreach (KeyValuePair, WeakReference> pair in _backingDictionary) + { + // Get strong references to avoid GC races + K keyTarget = pair.Key.Target; + V valueTarget = pair.Value.Target; + + if (keyTarget == null || valueTarget == null) + { + remove = remove ?? new List>(); + + remove.Add(pair.Key); + } + } + + if (remove != null) + { + foreach (WeakReference entry in remove) + { + _backingDictionary.Remove(entry); + } + + return remove.Count; + } + + return 0; + } + + /// + /// Empty the collection + /// + internal void Clear() + { + _backingDictionary.Clear(); + } + + /// + /// Equality comparer for weak references that actually compares the + /// targets of the weak references + /// + /// Type of the targets of the weak references to be compared + private class WeakReferenceEqualityComparer : IEqualityComparer> + where T : class + { + /// + /// Comparer to use if specified, otherwise null + /// + private IEqualityComparer _underlyingComparer; + + /// + /// Constructor to use an explicitly specified comparer. + /// Comparer may be null, in which case the default comparer for the type + /// will be used. + /// + internal WeakReferenceEqualityComparer(IEqualityComparer comparer) + { + _underlyingComparer = comparer; + } + + /// + /// Gets the hashcode + /// + public int GetHashCode(WeakReference item) + { + return item.GetHashCode(); + } + + /// + /// Compares the weak references for equality + /// + public bool Equals(WeakReference left, WeakReference right) + { + if (Object.ReferenceEquals(left, right)) + { + return true; + } + + if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null)) + { + return false; + } + + // Get strong references to targets to avoid GC race + T leftTarget = left.Target; + T rightTarget = right.Target; + + // Target(s) may have been collected + if (Object.ReferenceEquals(leftTarget, rightTarget)) + { + return true; + } + + if (Object.ReferenceEquals(leftTarget, null) || Object.ReferenceEquals(rightTarget, null)) + { + return false; + } + + bool equals; + + if (_underlyingComparer != null) + { + equals = _underlyingComparer.Equals(leftTarget, rightTarget); + } + else + { + // Compare using target's own equality operator + equals = leftTarget.Equals(rightTarget); + } + + return equals; + } + } + } +} diff --git a/src/XMakeBuildEngine/Collections/WeakReference.cs b/src/XMakeBuildEngine/Collections/WeakReference.cs new file mode 100644 index 00000000000..6d40c4c0279 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/WeakReference.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Strongly typed weak reference +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections.ObjectModel; +using System.Collections; +using Microsoft.Build.Shared; +using System.Diagnostics; + +namespace Microsoft.Build.Collections +{ + /// + /// Strongly typed weak reference + /// + /// Type of the target of the weak reference + internal class WeakReference + where T : class + { + /// + /// Cache the hashcode so that it is still available even if the target has been + /// collected. This allows this object to be still found in a table so it can be removed. + /// + private int _hashcode; + + /// + /// Backing weak reference + /// + private WeakReference _weakReference; + + /// + /// Constructor. + /// Target may not be null. + /// + internal WeakReference(T target) + { + ErrorUtilities.VerifyThrowInternalNull(target, "target"); + + _weakReference = new WeakReference(target); + _hashcode = target.GetHashCode(); + } + + /// + /// Target wrapped by this weak reference. + /// If it returns null, its value may have been collected, or it may actually "wrap" null. + /// To distinguish these cases, compare the WeakReference object itself to WeakReference.Null. + /// + internal T Target + { + get { return (T)_weakReference.Target; } + } + + /// + /// Returns the hashcode of the wrapped target. + /// + public override int GetHashCode() + { + return _hashcode; + } + } +} diff --git a/src/XMakeBuildEngine/Collections/WeakValueDictionary.cs b/src/XMakeBuildEngine/Collections/WeakValueDictionary.cs new file mode 100644 index 00000000000..717970917c4 --- /dev/null +++ b/src/XMakeBuildEngine/Collections/WeakValueDictionary.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Dictionary that does not prevent values from being garbage collected. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Collections +{ + /// + /// Dictionary that does not prevent values from being garbage collected. + /// + /// Type of key + /// Type of value, without the WeakReference wrapper. + internal class WeakValueDictionary + where V : class + { + /// + /// The dictionary used internally to store the keys and values. + /// + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + private Dictionary> _dictionary; + + /// + /// Improvised capacity. See comment in item setter. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int _capacity = 10; + + /// + /// Constructor for a collection using the default key comparer + /// + public WeakValueDictionary() + : this(null) + { + } + + /// + /// Constructor taking a specified comparer for the keys + /// + public WeakValueDictionary(IEqualityComparer keyComparer) + { + _dictionary = new Dictionary>(_capacity, keyComparer); + } + + /// + /// Count of entries. + /// Some entries may represent keys or values that have already been garbage collected. + /// To clean these out call . + /// + public int Count + { + get { return _dictionary.Count; } + } + + /// + /// Return keys + /// + public IEnumerable Keys + { + get + { + List keys = new List(); + + foreach (KeyValuePair> pair in _dictionary) + { + if (pair.Value != null && pair.Value.Target != null) + { + keys.Add(pair.Key); + } + } + + return keys; + } + } + + /// + /// Obtains the value for a given key. + /// + /// + /// If we find the entry but its value's target is null, we take the opportunity + /// to remove the entry, as if the GC had done it. + /// + public V this[K key] + { + get + { + WeakReference wrappedValue = _dictionary[key]; + + if (wrappedValue == null) + { + // We use this to represent an actual value + // that is null, rather than a collected non-null value + return null; + } + + V value = wrappedValue.Target; + + if (value == null) + { + _dictionary.Remove(key); + + // Trigger KeyNotFoundException + wrappedValue = _dictionary[key]; + + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + return value; + } + + set + { + // Make some attempt to prevent dictionary growing forever with + // entries whose underlying key or value has already been collected. + // We do not have access to the dictionary's true capacity or growth + // method, so we improvise with our own. + // So attempt to make room for the upcoming add before we do it. + if (_dictionary.Count == _capacity) + { + Scavenge(); + + // If that didn't do anything, raise the capacity at which + // we next scavenge. Note that we never shrink, but neither + // does the underlying dictionary. + if (_dictionary.Count == _capacity) + { + _capacity = _dictionary.Count * 2; + } + } + + // Use a null value to represent real null, as opposed to a collected real value + WeakReference wrappedValue = (value == null) ? null : new WeakReference(value); + + _dictionary[key] = wrappedValue; + } + } + + /// + /// Whether there is a key present with the specified key + /// + /// + /// As usual, don't just call Contained as the wrapped value may be null. + /// + public bool Contains(K key) + { + V value; + bool contained = TryGetValue(key, out value); + return contained; + } + + /// + /// Attempts to get the value for the provided key. + /// Returns true if the key is found, otherwise false. + /// + /// + /// If we find the entry but its value's target is null, we take the opportunity + /// to remove the entry, as if the GC had done it. + /// + public bool TryGetValue(K key, out V value) + { + WeakReference wrappedValue; + bool result = _dictionary.TryGetValue(key, out wrappedValue); + + if (!result) + { + value = null; + return false; + } + + if (wrappedValue == null) + { + // We use this to represent an actual value + // that is null, rather than a collected non-null value + value = null; + return true; + } + + value = wrappedValue.Target; + + if (value == null) + { + _dictionary.Remove(key); + return false; + } + + return result; + } + + /// + /// Removes an entry with the specified key. + /// Returns true if found, false otherwise. + /// + public bool Remove(K key) + { + return _dictionary.Remove(key); + } + + /// + /// Remove any entries from the dictionary that represent keys + /// that have been garbage collected. + /// + /// The number of entries removed. + public int Scavenge() + { + List remove = null; + + foreach (KeyValuePair> entry in _dictionary) + { + if (entry.Value == null) + { + // We use this to represent an actual value + // that is null, rather than a collected non-null value + continue; + } + + V value = entry.Value.Target; + + if (value == null) + { + remove = remove ?? new List(); + remove.Add(entry.Key); + } + } + + if (remove != null) + { + foreach (K entry in remove) + { + _dictionary.Remove(entry); + } + + return remove.Count; + } + + return 0; + } + + /// + /// Empty the collection + /// + public void Clear() + { + _dictionary.Clear(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectChooseElement.cs b/src/XMakeBuildEngine/Construction/ProjectChooseElement.cs new file mode 100644 index 00000000000..4bfe657baf9 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectChooseElement.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectChooseElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectChooseElement represents the Choose element in the MSBuild project. + /// Currently it does not allow a Condition. + /// + [DebuggerDisplay("ProjectChooseElement (#Children={Count} HasOtherwise={OtherwiseElement != null})")] + public class ProjectChooseElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectChooseElement + /// + internal ProjectChooseElement(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectChooseElement + /// + private ProjectChooseElement(XmlElement xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + #region ChildEnumerators + /// + /// Get the When children. + /// Will contain at least one entry. + /// + public ICollection WhenElements + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get any Otherwise child. + /// May be null. + /// + public ProjectOtherwiseElement OtherwiseElement + { + get + { + ProjectOtherwiseElement otherwise = (LastChild == null) ? null : LastChild as ProjectOtherwiseElement; + + return otherwise; + } + } + #endregion + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + /// + /// Creates an unparented ProjectChooseElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectChooseElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.choose); + + return new ProjectChooseElement(element, containingProject); + } + + /// + /// Sets the parent of this element if it is a valid parent, + /// otherwise throws. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement || parent is ProjectWhenElement || parent is ProjectOtherwiseElement, "OM_CannotAcceptParent"); + + int nestingDepth = 0; + ProjectElementContainer immediateParent = parent; + + while (parent != null) + { + parent = parent.Parent; + + nestingDepth++; + + // This should really be an OM error, with no error number. But it's so obscure, it's not worth a new string. + ProjectErrorUtilities.VerifyThrowInvalidProject(nestingDepth <= ProjectParser.MaximumChooseNesting, immediateParent.Location, "ChooseOverflow", ProjectParser.MaximumChooseNesting); + } + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateChooseElement(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectElement.cs b/src/XMakeBuildEngine/Construction/ProjectElement.cs new file mode 100644 index 00000000000..2a96f31db9c --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectElement.cs @@ -0,0 +1,499 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An unevaluated element in MSBuild XML. +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Collections.ObjectModel; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// Abstract base class for MSBuild construction object model elements. + /// + public abstract class ProjectElement + { + /// + /// Parent container object. + /// + private ProjectElementContainer _parent; + + /// + /// Condition value cached for performance + /// + private string _condition; + + /// + /// Constructor called by ProjectRootElement only. + /// XmlElement is set directly after construction. + /// + /// + /// Should be protected+internal. + /// + internal ProjectElement() + { + } + + /// + /// Constructor called by derived classes, except from ProjectRootElement. + /// Parameters may not be null, except parent. + /// + internal ProjectElement(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(xmlElement, "xmlElement"); + ProjectXmlUtilities.VerifyThrowProjectValidNamespace((XmlElementWithLocation)xmlElement); + ErrorUtilities.VerifyThrowArgumentNull(containingProject, "containingProject"); + + this.XmlElement = (XmlElementWithLocation)xmlElement; + _parent = parent; + this.ContainingProject = containingProject; + } + + /// + /// Gets or sets the Condition value. + /// It will return empty string IFF a condition attribute is legal but it’s not present or has no value. + /// It will return null IFF a Condition attribute is illegal on that element. + /// Removes the attribute if the value to set is empty. + /// It is possible for derived classes to throw an if setting the condition is + /// not applicable for those elements. + /// + /// For the "ProjectExtensions" element, the getter returns null and the setter + /// throws an exception for any value. + public virtual string Condition + { + [DebuggerStepThrough] + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (_condition == null) + { + _condition = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.condition); + } + + return _condition; + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.condition, value); + _condition = value; + MarkDirty("Set condition {0}", _condition); + } + } + + /// + /// Gets or sets the Label value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string Label + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.label); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.label, value); + MarkDirty("Set label {0}", value); + } + } + + /// + /// Null if this is a ProjectRootElement. + /// Null if this has not been attached to a parent yet. + /// + /// + /// Parent should only be set by ProjectElementContainer. + /// + public ProjectElementContainer Parent + { + [DebuggerStepThrough] + get + { + if (_parent is WrapperForProjectRootElement) + { + // We hijacked the field to store the owning PRE. This element is actually unparented. + return null; + } + + return _parent; + } + + internal set + { + if (value == null) + { + // We're about to lose the parent. Hijack the field to store the owning PRE. + _parent = new WrapperForProjectRootElement(ContainingProject); + } + else + { + _parent = value; + } + + OnAfterParentChanged(value); + } + } + + /// + /// All parent elements of this element, going up to the ProjectRootElement. + /// None if this itself is a ProjectRootElement. + /// None if this itself has not been attached to a parent yet. + /// + public IEnumerable AllParents + { + get + { + ProjectElementContainer currentParent = Parent; + while (currentParent != null) + { + yield return currentParent; + currentParent = currentParent.Parent; + } + } + } + + /// + /// Previous sibling element. + /// May be null. + /// + /// + /// Setter should ideally be "protected AND internal" + /// + public ProjectElement PreviousSibling + { + [DebuggerStepThrough] + get; + [DebuggerStepThrough] + internal set; + } + + /// + /// Next sibling element. + /// May be null. + /// + /// + /// Setter should ideally be "protected AND internal" + /// + public ProjectElement NextSibling + { + [DebuggerStepThrough] + get; + [DebuggerStepThrough] + internal set; + } + + /// + /// ProjectRootElement (possibly imported) that contains this Xml. + /// Cannot be null. + /// + /// + /// Setter ideally would be "protected and internal" + /// There are some tricks here in order to save the space of a field: there are a lot of these objects. + /// + public ProjectRootElement ContainingProject + { + get + { + // If this element is unparented, we have hijacked the 'parent' field and stored the owning PRE in a special wrapper; get it from that. + var wrapper = _parent as WrapperForProjectRootElement; + if (wrapper != null) + { + return wrapper.ContainingProject; + } + + // If this element is parented, the parent field is the true parent, and we ask that for the PRE. + // It will call into this same getter on itself and figure it out. + return Parent.ContainingProject; + } + + // ContainingProject is set ONLY when an element is first constructed. + internal set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "ContainingProject"); + + if (_parent == null) + { + // Not parented yet, hijack the field to store the ContainingProject + _parent = new WrapperForProjectRootElement(value); + } + } + } + + /// + /// Location of the "Condition" attribute on this element, if any. + /// If there is no such attribute, returns null. + /// + public virtual ElementLocation ConditionLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.condition); } + } + + /// + /// Location of the "Label" attribute on this element, if any. + /// If there is no such attribute, returns null; + /// + public ElementLocation LabelLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.label); } + } + + /// + /// Location of the corresponding Xml element. + /// May not be correct if file is not saved, or + /// file has been edited since it was last saved. + /// In the case of an unsaved edit, the location only + /// contains the path to the file that the element originates from. + /// + public ElementLocation Location + { + get { return XmlElement.Location; } + } + + /// + /// Gets the name of the associated element. + /// Useful for display in some circumstances. + /// + internal string ElementName + { + get { return XmlElement.Name; } + } + + /// + /// Gets the XmlElement associated with this project element. + /// The setter is used when adding new elements. + /// Never null except during load or creation. + /// + /// + /// This should be protected, but "protected internal" means "OR" not "AND", + /// so this is not possible. + /// + internal XmlElementWithLocation XmlElement + { + get; + private set; + } + + /// + /// Gets the XmlDocument associated with this project element. + /// + /// + /// Never null except during load or creation. + /// This should be protected, but "protected internal" means "OR" not "AND", + /// so this is not possible. + /// + internal XmlDocumentWithLocation XmlDocument + { + [DebuggerStepThrough] + get + { + return (XmlElement == null) ? null : (XmlDocumentWithLocation)XmlElement.OwnerDocument; + } + } + + /// + /// Returns a shallow clone of this project element. + /// + /// The cloned element. + public ProjectElement Clone() + { + return this.Clone(this.ContainingProject); + } + + /// + /// Applies properties from the specified type to this instance. + /// + /// The element to act as a template to copy from. + public virtual void CopyFrom(ProjectElement element) + { + ErrorUtilities.VerifyThrowArgumentNull(element, "element"); + ErrorUtilities.VerifyThrowArgument(this.GetType().IsEquivalentTo(element.GetType()), "element"); + + if (this == element) + { + return; + } + + // Remove all the current attributes and textual content. + this.XmlElement.RemoveAllAttributes(); + if (this.XmlElement.ChildNodes.Count == 1 && this.XmlElement.FirstChild.NodeType == XmlNodeType.Text) + { + this.XmlElement.RemoveChild(this.XmlElement.FirstChild); + } + + // Ensure the element name itself matches. + this.ReplaceElement(XmlUtilities.RenameXmlElement(this.XmlElement, element.XmlElement.Name, XMakeAttributes.defaultXmlNamespace)); + + // Copy over the attributes from the template element. + foreach (XmlAttribute attribute in element.XmlElement.Attributes) + { + this.XmlElement.SetAttribute(attribute.LocalName, attribute.NamespaceURI, attribute.Value); + } + + // If this element has pure text content, copy that over. + if (element.XmlElement.ChildNodes.Count == 1 && element.XmlElement.FirstChild.NodeType == XmlNodeType.Text) + { + this.XmlElement.AppendChild(this.XmlElement.OwnerDocument.CreateTextNode(element.XmlElement.FirstChild.Value)); + } + + this.MarkDirty("CopyFrom", null); + } + + /// + /// Called only by the parser to tell the ProjectRootElement its backing XmlElement and its own parent project (itself) + /// This can't be done during construction, as it hasn't loaded the document at that point and it doesn't have a 'this' pointer either. + /// + internal void SetProjectRootElementFromParser(XmlElementWithLocation xmlElement, ProjectRootElement projectRootElement) + { + this.XmlElement = xmlElement; + this.ContainingProject = projectRootElement; + } + + /// + /// Called by ProjectElementContainer to clear the parent when + /// removing an element from its parent. + /// + internal void ClearParent() + { + Parent = null; + } + + /// + /// Called by a DERIVED CLASS to indicate its XmlElement has changed. + /// This normally shouldn't happen, so it's broken out into an explicit method. + /// An example of when it has to happen is when an item's type is changed. + /// We trust the caller to have fixed up the XmlDocument properly. + /// We ASSUME that attributes were copied verbatim. If this is not the case, + /// any cached attribute values would have to be cleared. + /// If the new element is actually the existing element, does nothing, and does + /// not mark the project dirty. + /// + /// + /// This should be protected, but "protected internal" means "OR" not "AND", + /// so this is not possible. + /// + internal void ReplaceElement(XmlElementWithLocation newElement) + { + if (Object.ReferenceEquals(newElement, XmlElement)) + { + return; + } + + XmlElement = newElement; + MarkDirty("Replace element {0}", newElement.Name); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal abstract void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer proposedParent, ProjectElement previousSibling, ProjectElement nextSibling); + + /// + /// Marks this element as dirty. + /// The default implementation simply marks the parent as dirty. + /// If there is no parent, because the element has not been parented, do nothing. The parent + /// will be dirtied when the element is added. + /// Accepts a reason for debugging purposes only, and optional reason parameter. + /// + /// + /// Should ideally be protected+internal. + /// + internal virtual void MarkDirty(string reason, string param) + { + if (Parent != null) + { + Parent.MarkDirty(reason, param); + } + } + + /// + /// Called after a new parent is set. Parent may be null. + /// By default does nothing. + /// + internal virtual void OnAfterParentChanged(ProjectElementContainer newParent) + { + } + + /// + /// Returns a shallow clone of this project element. + /// + /// The factory to use for creating the new instance. + /// The cloned element. + protected internal virtual ProjectElement Clone(ProjectRootElement factory) + { + var clone = this.CreateNewInstance(factory); + if (!clone.GetType().IsEquivalentTo(this.GetType())) + { + ErrorUtilities.ThrowInternalError("{0}.Clone() returned an instance of type {1}.", this.GetType().Name, clone.GetType().Name); + } + + clone.CopyFrom(this); + return clone; + } + + /// + /// Returns a new instance of this same type. + /// Any properties that cannot be set after creation should be set to copies of values + /// as set for this instance. + /// + /// The factory to use for creating the new instance. + protected abstract ProjectElement CreateNewInstance(ProjectRootElement owner); + + /// + /// Special derived variation of ProjectElementContainer used to wrap a ProjectRootElement. + /// This is part of a trick used in ProjectElement to avoid using a separate field for the containing PRE. + /// + private class WrapperForProjectRootElement : ProjectElementContainer + { + /// + /// Constructor + /// + internal WrapperForProjectRootElement(ProjectRootElement containingProject) + { + ErrorUtilities.VerifyThrowInternalNull(containingProject, "containingProject"); + this.ContainingProject = containingProject; + } + + /// + /// Wrapped ProjectRootElement + /// + internal new ProjectRootElement ContainingProject + { + get; + private set; + } + + /// + /// Dummy required implementation + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return new WrapperForProjectRootElement(this.ContainingProject); + } + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs b/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs new file mode 100644 index 00000000000..0ff7fa85041 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs @@ -0,0 +1,647 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A container for project elements. +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Collections.ObjectModel; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Construction +{ + /// + /// A container for project elements + /// + public abstract class ProjectElementContainer : ProjectElement + { + /// + /// Number of children of any kind + /// + private int _count; + + /// + /// Constructor called by ProjectRootElement only. + /// XmlElement is set directly after construction. + /// + /// + /// Should ideally be protected+internal. + /// + internal ProjectElementContainer() + : base() + { + } + + /// + /// Constructor called by derived classes, except from ProjectRootElement. + /// Parameters may not be null, except parent. + /// + /// + /// Should ideally be protected+internal. + /// + internal ProjectElementContainer(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + } + + /// + /// Get an enumerator over all children, gotten recursively. + /// Walks the children in a depth-first manner. + /// + public IEnumerable AllChildren + { + get { return GetChildrenRecursively(); } + } + + /// + /// Get enumerable over all the children + /// + public ICollection Children + { + [DebuggerStepThrough] + get + { + return new Microsoft.Build.Collections.ReadOnlyCollection + ( + new ProjectElementSiblingEnumerable(FirstChild) + ); + } + } + + /// + /// Get enumerable over all the children, starting from the last + /// + public ICollection ChildrenReversed + { + [DebuggerStepThrough] + get + { + return new Microsoft.Build.Collections.ReadOnlyCollection + ( + new ProjectElementSiblingEnumerable(LastChild, false /* reverse */) + ); + } + } + + /// + /// Number of children of any kind + /// + public int Count + { + [DebuggerStepThrough] + get + { return _count; } + } + + /// + /// First child, if any, otherwise null. + /// Cannot be set directly; use PrependChild(). + /// + public ProjectElement FirstChild + { + [DebuggerStepThrough] + get; + [DebuggerStepThrough] + private set; + } + + /// + /// Last child, if any, otherwise null. + /// Cannot be set directly; use AppendChild(). + /// + public ProjectElement LastChild + { + [DebuggerStepThrough] + get; + [DebuggerStepThrough] + private set; + } + + /// + /// Insert the child after the reference child. + /// Reference child if provided must be parented by this element. + /// Reference child may be null, in which case this is equivalent to PrependChild(child). + /// Throws if the parent is not itself parented. + /// Throws if the reference node does not have this node as its parent. + /// Throws if the node to add is already parented. + /// Throws if the node to add was created from a different project than this node. + /// + /// + /// Semantics are those of XmlNode.InsertAfterChild. + /// + public void InsertAfterChild(ProjectElement child, ProjectElement reference) + { + ErrorUtilities.VerifyThrowArgumentNull(child, "child"); + + if (reference == null) + { + PrependChild(child); + return; + } + + VerifyForInsertBeforeAfterFirst(child, reference); + + child.VerifyThrowInvalidOperationAcceptableLocation(this, reference, reference.NextSibling); + + child.Parent = this; + + if (LastChild == reference) + { + LastChild = child; + } + + child.PreviousSibling = reference; + child.NextSibling = reference.NextSibling; + + reference.NextSibling = child; + + if (child.NextSibling != null) + { + ErrorUtilities.VerifyThrow(child.NextSibling.PreviousSibling == reference, "Invalid structure"); + child.NextSibling.PreviousSibling = child; + } + + XmlElement.InsertAfter(child.XmlElement, reference.XmlElement); + + _count++; + MarkDirty("Insert element {0}", child.ElementName); + } + + /// + /// Insert the child before the reference child. + /// Reference child if provided must be parented by this element. + /// Reference child may be null, in which case this is equivalent to AppendChild(child). + /// Throws if the parent is not itself parented. + /// Throws if the reference node does not have this node as its parent. + /// Throws if the node to add is already parented. + /// Throws if the node to add was created from a different project than this node. + /// + /// + /// Semantics are those of XmlNode.InsertBeforeChild. + /// + public void InsertBeforeChild(ProjectElement child, ProjectElement reference) + { + ErrorUtilities.VerifyThrowArgumentNull(child, "child"); + + if (reference == null) + { + AppendChild(child); + return; + } + + VerifyForInsertBeforeAfterFirst(child, reference); + + child.VerifyThrowInvalidOperationAcceptableLocation(this, reference.PreviousSibling, reference); + + child.Parent = this; + + if (FirstChild == reference) + { + FirstChild = child; + } + + child.PreviousSibling = reference.PreviousSibling; + child.NextSibling = reference; + + reference.PreviousSibling = child; + + if (child.PreviousSibling != null) + { + ErrorUtilities.VerifyThrow(child.PreviousSibling.NextSibling == reference, "Invalid structure"); + child.PreviousSibling.NextSibling = child; + } + + XmlElement.InsertBefore(child.XmlElement, reference.XmlElement); + + _count++; + MarkDirty("Insert element {0}", child.ElementName); + } + + /// + /// Appends the provided element as the last child. + /// Throws if the parent is not itself parented. + /// Throws if the node to add is already parented. + /// Throws if the node to add was created from a different project than this node. + /// + public void AppendChild(ProjectElement child) + { + if (LastChild == null) + { + AddInitialChild(child); + } + else + { + ErrorUtilities.VerifyThrow(FirstChild != null, "Invalid structure"); + InsertAfterChild(child, LastChild); + } + } + + /// + /// Appends the provided element as the last child. + /// Throws if the parent is not itself parented. + /// Throws if the node to add is already parented. + /// Throws if the node to add was created from a different project than this node. + /// + public void PrependChild(ProjectElement child) + { + if (FirstChild == null) + { + AddInitialChild(child); + } + else + { + ErrorUtilities.VerifyThrow(LastChild != null, "Invalid structure"); + InsertBeforeChild(child, FirstChild); + return; + } + } + + /// + /// Removes the specified child. + /// Throws if the child is not currently parented by this object. + /// This is O(1). + /// May be safely called during enumeration of the children. + /// + /// + /// This is actually safe to call during enumeration of children, because it + /// doesn't bother to clear the child's NextSibling (or PreviousSibling) pointers. + /// To determine whether a child is unattached, check whether its parent is null, + /// or whether its NextSibling and PreviousSibling point back at it. + /// DO NOT BREAK THIS VERY USEFUL SAFETY CONTRACT. + /// + public void RemoveChild(ProjectElement child) + { + ErrorUtilities.VerifyThrowArgumentNull(child, "child"); + + ErrorUtilities.VerifyThrowArgument(child.Parent == this, "OM_NodeNotAlreadyParentedByThis"); + + child.ClearParent(); + + if (child.PreviousSibling != null) + { + child.PreviousSibling.NextSibling = child.NextSibling; + } + + if (child.NextSibling != null) + { + child.NextSibling.PreviousSibling = child.PreviousSibling; + } + + if (Object.ReferenceEquals(child, FirstChild)) + { + FirstChild = child.NextSibling; + } + + if (Object.ReferenceEquals(child, LastChild)) + { + LastChild = child.PreviousSibling; + } + + XmlElement.RemoveChild(child.XmlElement); + + _count--; + MarkDirty("Remove element {0}", child.ElementName); + } + + /// + /// Remove all the children, if any. + /// + /// + /// It is safe to modify the children in this way + /// during enumeration. See RemoveChild. + /// + public void RemoveAllChildren() + { + foreach (ProjectElement child in Children) + { + RemoveChild(child); + } + } + + /// + /// Applies properties from the specified type to this instance. + /// + /// The element to act as a template to copy from. + public virtual void DeepCopyFrom(ProjectElementContainer element) + { + ErrorUtilities.VerifyThrowArgumentNull(element, "element"); + ErrorUtilities.VerifyThrowArgument(this.GetType().IsEquivalentTo(element.GetType()), "element"); + + if (this == element) + { + return; + } + + this.RemoveAllChildren(); + this.CopyFrom(element); + + foreach (var child in element.Children) + { + var childContainer = child as ProjectElementContainer; + if (childContainer != null) + { + childContainer.DeepClone(this.ContainingProject, this); + } + else + { + this.AppendChild(child.Clone(this.ContainingProject)); + } + } + } + + /// + /// Appends the provided child. + /// Does not dirty the project, does not add an element, does not set the child's parent, + /// and does not check the parent's future siblings and parent are acceptable. + /// Called during project load, when the child can be expected to + /// already have a parent and its element is already connected to the + /// parent's element. + /// All that remains is to set FirstChild/LastChild and fix up the linked list. + /// + internal void AppendParentedChildNoChecks(ProjectElement child) + { + ErrorUtilities.VerifyThrow(child.Parent == this, "Expected parent already set"); + ErrorUtilities.VerifyThrow(child.PreviousSibling == null && child.NextSibling == null, "Invalid structure"); + + if (LastChild == null) + { + FirstChild = child; + } + else + { + child.PreviousSibling = LastChild; + LastChild.NextSibling = child; + } + + LastChild = child; + + _count++; + } + + /// + /// Returns a clone of this project element and all its children. + /// + /// The factory to use for creating the new instance. + /// The parent to append the cloned element to as a child. + /// The cloned element. + protected internal virtual ProjectElementContainer DeepClone(ProjectRootElement factory, ProjectElementContainer parent) + { + var clone = (ProjectElementContainer)this.Clone(factory); + if (parent != null) + { + parent.AppendChild(clone); + } + + foreach (var child in this.Children) + { + var childContainer = child as ProjectElementContainer; + if (childContainer != null) + { + childContainer.DeepClone(clone.ContainingProject, clone); + } + else + { + clone.AppendChild(child.Clone(clone.ContainingProject)); + } + } + + return clone; + } + + /// + /// Sets the first child in this container + /// + private void AddInitialChild(ProjectElement child) + { + ErrorUtilities.VerifyThrow(FirstChild == null && LastChild == null, "Expecting no children"); + + VerifyForInsertBeforeAfterFirst(child, null); + + child.VerifyThrowInvalidOperationAcceptableLocation(this, null, null); + + child.Parent = this; + + FirstChild = child; + LastChild = child; + + child.PreviousSibling = null; + child.NextSibling = null; + + XmlElement.AppendChild(child.XmlElement); + + _count++; + MarkDirty("Add child element named '{0}'", child.ElementName); + } + + /// + /// Common verification for insertion of an element. + /// Reference may be null. + /// + private void VerifyForInsertBeforeAfterFirst(ProjectElement child, ProjectElement reference) + { + ErrorUtilities.VerifyThrowInvalidOperation(this.Parent != null || this.ContainingProject == this, "OM_ParentNotParented"); + ErrorUtilities.VerifyThrowInvalidOperation(reference == null || reference.Parent == this, "OM_ReferenceDoesNotHaveThisParent"); + ErrorUtilities.VerifyThrowInvalidOperation(child.Parent == null, "OM_NodeAlreadyParented"); + ErrorUtilities.VerifyThrowInvalidOperation(child.ContainingProject == this.ContainingProject, "OM_MustBeSameProject"); + + // In RemoveChild() we do not update the victim's NextSibling (or PreviousSibling) to null, to allow RemoveChild to be + // called within an enumeration. So we can't expect these to be null if the child was previously removed. However, we + // can expect that what they point to no longer point back to it. They've been reconnected. + ErrorUtilities.VerifyThrow(child.NextSibling == null || child.NextSibling.PreviousSibling != this, "Invalid structure"); + ErrorUtilities.VerifyThrow(child.PreviousSibling == null || child.PreviousSibling.NextSibling != this, "Invalid structure"); + VerifyThrowInvalidOperationNotSelfAncestor(child); + } + + /// + /// Verifies that the provided element isn't this element or a parent of it. + /// If it is, throws InvalidOperationException. + /// + private void VerifyThrowInvalidOperationNotSelfAncestor(ProjectElement element) + { + ProjectElement ancestor = this; + + while (ancestor != null) + { + ErrorUtilities.VerifyThrowInvalidOperation(ancestor != element, "OM_SelfAncestor"); + ancestor = ancestor.Parent; + } + } + + /// + /// Recurses into the provided container (such as a choose) and finds all child elements, even if nested. + /// Result does NOT include the element passed in. + /// The caller could filter these. + /// + private IEnumerable GetChildrenRecursively() + { + ProjectElement child = FirstChild; + + while (child != null) + { + yield return child; + + ProjectElementContainer container = child as ProjectElementContainer; + + if (container != null) + { + foreach (ProjectElement grandchild in container.AllChildren) + { + yield return grandchild; + } + } + + child = child.NextSibling; + } + } + + /// + /// Enumerable over a series of sibling ProjectElement objects + /// + private struct ProjectElementSiblingEnumerable : IEnumerable + { + /// + /// The enumerator + /// + private ProjectElementSiblingEnumerator _enumerator; + + /// + /// Constructor + /// + internal ProjectElementSiblingEnumerable(ProjectElement initial) + : this(initial, true) + { + } + + /// + /// Constructor allowing reverse enumeration + /// + internal ProjectElementSiblingEnumerable(ProjectElement initial, bool forwards) + { + _enumerator = new ProjectElementSiblingEnumerator(initial, forwards); + } + + /// + /// Get enumerator + /// + public IEnumerator GetEnumerator() + { + return _enumerator; + } + + /// + /// Get non generic enumerator + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _enumerator; + } + + /// + /// Enumerator over a series of sibling ProjectElement objects + /// + private struct ProjectElementSiblingEnumerator : IEnumerator + { + /// + /// First element + /// + private ProjectElement _initial; + + /// + /// Current element + /// + private ProjectElement _current; + + /// + /// Whether enumeration should go forwards or backwards. + /// If backwards, the "initial" will be the first returned, then each previous + /// node in turn. + /// + private bool _forwards; + + /// + /// Constructor taking the first element + /// + internal ProjectElementSiblingEnumerator(ProjectElement initial, bool forwards) + { + _initial = initial; + _current = null; + _forwards = forwards; + } + + /// + /// Current element + /// Returns null if MoveNext() hasn't been called + /// + public ProjectElement Current + { + get { return _current; } + } + + /// + /// Current element. + /// Throws if MoveNext() hasn't been called + /// + object System.Collections.IEnumerator.Current + { + get + { + if (_current != null) + { + return _current; + } + + throw new InvalidOperationException(); + } + } + + /// + /// Dispose. Do nothing. + /// + public void Dispose() + { + } + + /// + /// Moves to the next item if any, otherwise returns false + /// + public bool MoveNext() + { + ProjectElement next; + + if (_current == null) + { + next = _initial; + } + else + { + next = _forwards ? _current.NextSibling : _current.PreviousSibling; + } + + if (next != null) + { + _current = next; + return true; + } + + return false; + } + + /// + /// Return to start + /// + public void Reset() + { + _current = null; + } + } + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectExtensionsElement.cs b/src/XMakeBuildEngine/Construction/ProjectExtensionsElement.cs new file mode 100644 index 00000000000..75fa33e3d1e --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectExtensionsElement.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents the ProjectExtensions element in the MSBuild project. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using Utilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectExtensionsElement represents the ProjectExtensions element in the MSBuild project. + /// ProjectExtensions can contain arbitrary XML content. + /// The ProjectExtensions element is deprecated and provided only for backward compatibility. + /// Use a property instead. Properties can also contain XML content. + /// + public class ProjectExtensionsElement : ProjectElement + { + /// + /// Initialize a parented ProjectExtensionsElement instance + /// + internal ProjectExtensionsElement(XmlElement xmlElement, ProjectRootElement parent, ProjectRootElement project) + : base(xmlElement, parent, project) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectExtensionsElement instance + /// + private ProjectExtensionsElement(XmlElement xmlElement, ProjectRootElement project) + : base(xmlElement, null, project) + { + } + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + /// + /// Gets and sets the raw XML content + /// + public string Content + { + [DebuggerStepThrough] + get + { + return XmlElement.InnerXml; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "Content"); + XmlElement.InnerXml = value; + MarkDirty("Set ProjectExtensions raw {0}", value); + } + } + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + /// + /// Get or set the content of the first sub-element + /// with the provided name. + /// + public string this[string name] + { + get + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + XmlElement idElement = XmlElement[name]; + + if (idElement == null) + { + return String.Empty; + } + + // remove the xmlns attribute, because the IDE's not expecting that + return Microsoft.Build.Internal.Utilities.RemoveXmlNamespace(idElement.InnerXml); + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(value, "value"); + + XmlElement idElement = XmlElement[name]; + + if (idElement == null) + { + if (value.Length == 0) + { + return; + } + + idElement = XmlDocument.CreateElement(name, XMakeAttributes.defaultXmlNamespace); + XmlElement.AppendChild(idElement); + } + + if (idElement.InnerXml != value) + { + if (value.Length == 0) + { + XmlElement.RemoveChild(idElement); + } + else + { + idElement.InnerXml = value; + } + + MarkDirty("Set ProjectExtensions content {0}", value); + } + } + } + + /// + public override void CopyFrom(ProjectElement element) + { + ErrorUtilities.VerifyThrowArgumentNull(element, "element"); + ErrorUtilities.VerifyThrowArgument(this.GetType().IsEquivalentTo(element.GetType()), "element"); + + if (this == element) + { + return; + } + + this.Label = element.Label; + + var other = (ProjectExtensionsElement)element; + this.Content = other.Content; + + this.MarkDirty("CopyFrom", null); + } + + /// + /// Creates a ProjectExtensionsElement parented by a project + /// + internal static ProjectExtensionsElement CreateParented(XmlElementWithLocation element, ProjectRootElement parent, ProjectRootElement containingProject) + { + return new ProjectExtensionsElement(element, parent, containingProject); + } + + /// + /// Creates an unparented ProjectExtensionsElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectExtensionsElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.projectExtensions); + + return new ProjectExtensionsElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateProjectExtensionsElement(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectImportElement.cs b/src/XMakeBuildEngine/Construction/ProjectImportElement.cs new file mode 100644 index 00000000000..bec6a598f1f --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectImportElement.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectImportElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// Initializes a ProjectImportElement instance. + /// + [DebuggerDisplay("Project={Project} Condition={Condition}")] + public class ProjectImportElement : ProjectElement + { + /// + /// Initialize a parented ProjectImportElement + /// + internal ProjectImportElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectImportElement + /// + private ProjectImportElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets or sets the Project value. + /// + public string Project + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.project); + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.project); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.project, value); + MarkDirty("Set Import Project {0}", value); + } + } + + /// + /// Location of the project attribute + /// + public ElementLocation ProjectLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.project); } + } + + /// + /// Creates an unparented ProjectImportElement, wrapping an unparented XmlElement. + /// Validates the project value. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectImportElement CreateDisconnected(string project, ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.import); + + ProjectImportElement import = new ProjectImportElement(element, containingProject); + + import.Project = project; + + return import; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement || parent is ProjectImportGroupElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateImportElement(this.Project); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectImportGroupElement.cs b/src/XMakeBuildEngine/Construction/ProjectImportGroupElement.cs new file mode 100644 index 00000000000..33ef5a3ff8a --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectImportGroupElement.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectImportGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectImportGroupElement represents the ImportGroup element in the MSBuild project. + /// + [DebuggerDisplay("#Imports={Count} Condition={Condition} Label={Label}")] + public class ProjectImportGroupElement : ProjectElementContainer + { + #region Constructors + + /// + /// Initialize a parented ProjectImportGroupElement + /// + internal ProjectImportGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectImportGroupElement + /// + private ProjectImportGroupElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + #endregion // Constructors + + #region Properties + + /// + /// Get any contained properties. + /// + public ICollection Imports + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + #endregion // Properties + + #region Methods + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new import after the last import in this import group. + /// + public ProjectImportElement AddImport(string project) + { + ErrorUtilities.VerifyThrowArgumentLength(project, "project"); + + ProjectImportElement newImport = ContainingProject.CreateImportElement(project); + AppendChild(newImport); + + return newImport; + } + + /// + /// Creates an unparented ProjectImportGroupElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectImportGroupElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.importGroup); + + return new ProjectImportGroupElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateImportGroupElement(); + } + + #endregion // Methods + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectItemDefinitionElement.cs b/src/XMakeBuildEngine/Construction/ProjectItemDefinitionElement.cs new file mode 100644 index 00000000000..83f8406f0d8 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectItemDefinitionElement.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectItemDefinitionElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectItemDefinitionElement class represents the Item Definition element in the MSBuild project. + /// + [DebuggerDisplay("{ItemType} #Metadata={Count} Condition={Condition}")] + public class ProjectItemDefinitionElement : ProjectElementContainer + { + /// + /// Initialize a ProjectItemDefinitionElement instance from a node read from a project file + /// + internal ProjectItemDefinitionElement(XmlElement xmlElement, ProjectItemDefinitionGroupElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize a ProjectItemDefinitionElement instance from a node read from a project file + /// + private ProjectItemDefinitionElement(XmlElement xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets the definition's type. + /// + public string ItemType + { + get { return XmlElement.Name; } + } + + /// + /// Get any child metadata definitions. + /// + public ICollection Metadata + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Convenience method to add a piece of metadata to this item definition. + /// Adds after any existing metadata. Does not modify any existing metadata. + /// + public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue, "unevaluatedValue"); + + ProjectMetadataElement metadata = ContainingProject.CreateMetadataElement(name); + metadata.Value = unevaluatedValue; + + AppendChild(metadata); + + return metadata; + } + + /// + /// Creates an unparented ProjectItemDefinitionElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectItemDefinitionElement CreateDisconnected(string itemType, ProjectRootElement containingProject) + { + XmlUtilities.VerifyThrowArgumentValidElementName(itemType); + + // Orcas inadvertently did not check for reserved item types (like "Choose") in item definitions, + // as we do for item types in item groups. So we do not have a check here. + // Although we could perhaps add one, as such item definitions couldn't be used + // since no items can have the reserved itemType. + XmlElementWithLocation element = containingProject.CreateElement(itemType); + + return new ProjectItemDefinitionElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectItemDefinitionGroupElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateItemDefinitionElement(this.ItemType); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Construction/ProjectItemDefinitionGroupElement.cs b/src/XMakeBuildEngine/Construction/ProjectItemDefinitionGroupElement.cs new file mode 100644 index 00000000000..d4d9ef0f5d5 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectItemDefinitionGroupElement.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectItemDefinitionGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectItemDefinitionGroupElement represents the ItemGroup element in the MSBuild project. + /// + [DebuggerDisplay("#ItemDefinitions={Count} Condition={Condition} Label={Label}")] + public class ProjectItemDefinitionGroupElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectItemDefinitionGroupElement + /// + internal ProjectItemDefinitionGroupElement(XmlElement xmlElement, ProjectRootElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectItemDefinitionGroupElement + /// + private ProjectItemDefinitionGroupElement(XmlElement xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Get a list of child item definitions. + /// + public ICollection ItemDefinitions + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new item definition after the last child. + /// + public ProjectItemDefinitionElement AddItemDefinition(string itemType) + { + ProjectItemDefinitionElement itemDefinition = ContainingProject.CreateItemDefinitionElement(itemType); + + AppendChild(itemDefinition); + + return itemDefinition; + } + + /// + /// Creates an unparented ProjectItemDefinitionGroupElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectItemDefinitionGroupElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.itemDefinitionGroup); + + return new ProjectItemDefinitionGroupElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateItemDefinitionGroupElement(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectItemElement.cs b/src/XMakeBuildEngine/Construction/ProjectItemElement.cs new file mode 100644 index 00000000000..9005441eb04 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectItemElement.cs @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectItemElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectItemElement class represents the Item element in the MSBuild project. + /// + [DebuggerDisplay("{ItemType} Include={Include} Exclude={Exclude} #Metadata={Count} Condition={Condition}")] + public class ProjectItemElement : ProjectElementContainer + { + /// + /// Include value cached for performance + /// + private string _include; + + /// + /// Exclude value cached for performance + /// + private string _exclude; + + /// + /// Remove value cached for performance + /// + private string _remove; + + /// + /// Whether the include value has wildcards, + /// cached for performance. + /// + private bool? _includeHasWildcards = null; + + /// + /// Initialize a parented ProjectItemElement instance + /// + internal ProjectItemElement(XmlElementWithLocation xmlElement, ProjectItemGroupElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectItemElement instance + /// + private ProjectItemElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets the item's type. + /// + public string ItemType + { + [DebuggerStepThrough] + get + { return XmlElement.Name; } + set { ChangeItemType(value); } + } + + /// + /// Gets or sets the Include value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty or null. + /// + public string Include + { + [DebuggerStepThrough] + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (_include == null) + { + _include = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.include); + } + + return _include; + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(value) || Remove.Length == 0, "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.include, XMakeAttributes.remove); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.include, value); + _include = value; + _includeHasWildcards = null; + MarkDirty("Set item Include {0}", value); + } + } + + /// + /// Gets or sets the Exclude value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty or null. + /// + public string Exclude + { + [DebuggerStepThrough] + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (_exclude == null) + { + _exclude = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.exclude); + } + + return _exclude; + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(value) || Remove.Length == 0, "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.exclude, XMakeAttributes.remove); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.exclude, value); + _exclude = value; + MarkDirty("Set item Exclude {0}", value); + } + } + + /// + /// Gets or sets the Remove value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty or null. + /// + public string Remove + { + [DebuggerStepThrough] + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (_remove == null) + { + _remove = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.remove); + } + + return _remove; + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(Parent == null || Parent.Parent is ProjectTargetElement, "OM_NoRemoveOutsideTargets"); + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(value) || Include.Length == 0, "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.include, XMakeAttributes.remove); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.remove, value); + _remove = value; + MarkDirty("Set item Remove {0}", value); + } + } + + /// + /// Gets or sets the KeepMetadata value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty or null. + /// + public string KeepMetadata + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.keepMetadata); + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(Parent == null || Parent.Parent is ProjectTargetElement, "OM_NoKeepMetadataOutsideTargets"); + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(value) || RemoveMetadata.Length == 0, "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.removeMetadata, XMakeAttributes.keepMetadata); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.keepMetadata, value); + MarkDirty("Set item KeepMetadata {0}", value); + } + } + + /// + /// Gets or sets the RemoveMetadata value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty or null. + /// + public string RemoveMetadata + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.removeMetadata); + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(Parent == null || Parent.Parent is ProjectTargetElement, "OM_NoRemoveMetadataOutsideTargets"); + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(value) || KeepMetadata.Length == 0, "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.keepMetadata, XMakeAttributes.removeMetadata); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.removeMetadata, value); + MarkDirty("Set item RemoveMetadata {0}", value); + } + } + + /// + /// Gets or sets the KeepDuplicates value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty or null. + /// + public string KeepDuplicates + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.keepDuplicates); + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(Parent == null || Parent.Parent is ProjectTargetElement, "OM_NoKeepDuplicatesOutsideTargets"); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.keepDuplicates, value); + MarkDirty("Set item KeepDuplicates {0}", value); + } + } + + /// + /// Whether there are any child metadata elements + /// + public bool HasMetadata + { + get { return FirstChild != null; } + } + + /// + /// Get any child metadata. + /// + public ICollection Metadata + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Location of the include attribute + /// + public ElementLocation IncludeLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.include); } + } + + /// + /// Location of the exclude attribute + /// + public ElementLocation ExcludeLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.exclude); } + } + + /// + /// Location of the remove attribute + /// + public ElementLocation RemoveLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.remove); } + } + + /// + /// Location of the keepMetadata attribute + /// + public ElementLocation KeepMetadataLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.keepMetadata); } + } + + /// + /// Location of the removeMetadata attribute + /// + public ElementLocation RemoveMetadataLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.removeMetadata); } + } + + /// + /// Location of the keepDuplicates attribute + /// + public ElementLocation KeepDuplicatesLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.keepDuplicates); } + } + + /// + /// Whether the include value has wildcards, + /// cached for performance. + /// + internal bool IncludeHasWildcards + { + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (!_includeHasWildcards.HasValue) + { + _includeHasWildcards = (Include == null) ? false : FileMatcher.HasWildcards(_include); + } + + return _includeHasWildcards.Value; + } + } + + /// + /// Internal helper to get the next ProjectItemElement sibling. + /// If there is none, returns null. + /// + internal ProjectItemElement NextItem + { + get + { + ProjectItemElement result = null; + ProjectElement sibling = NextSibling; + + while (sibling != null && result == null) + { + result = NextSibling as ProjectItemElement; + sibling = sibling.NextSibling; + } + + return result; + } + } + + /// + /// Convenience method to add a piece of metadata to this item. + /// Adds after any existing metadata. Does not modify any existing metadata. + /// + public ProjectMetadataElement AddMetadata(string name, string unevaluatedValue) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue, "unevaluatedValue"); + + ProjectMetadataElement metadata = ContainingProject.CreateMetadataElement(name); + metadata.Value = unevaluatedValue; + + AppendChild(metadata); + + return metadata; + } + + /// + public override void CopyFrom(ProjectElement element) + { + base.CopyFrom(element); + + // clear cached fields + _include = null; + _exclude = null; + _remove = null; + _includeHasWildcards = null; + } + + /// + /// Creates an unparented ProjectItemElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectItemElement CreateDisconnected(string itemType, ProjectRootElement containingProject) + { + XmlUtilities.VerifyThrowArgumentValidElementName(itemType); + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[itemType] == null, "CannotModifyReservedItem", itemType); + + XmlElementWithLocation element = containingProject.CreateElement(itemType); + + ProjectItemElement item = new ProjectItemElement(element, containingProject); + + return item; + } + + /// + /// Changes the item type. + /// + /// + /// The implementation has to actually replace the element to do this. + /// + internal void ChangeItemType(string newItemType) + { + ErrorUtilities.VerifyThrowArgumentLength(newItemType, "itemType"); + XmlUtilities.VerifyThrowArgumentValidElementName(newItemType); + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[newItemType] == null, "CannotModifyReservedItem", newItemType); + + // Because the element was created from our special XmlDocument, we know it's + // an XmlElementWithLocation. + XmlElementWithLocation newElement = (XmlElementWithLocation)XmlUtilities.RenameXmlElement(XmlElement, newItemType, XMakeAttributes.defaultXmlNamespace); + + ReplaceElement(newElement); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent.Parent is ProjectTargetElement || (Include.Length > 0 && Remove.Length == 0), "OM_ItemsOutsideTargetMustHaveIncludeNoRemove"); + ErrorUtilities.VerifyThrowInvalidOperation(parent.Parent is ProjectRootElement || parent.Parent is ProjectTargetElement || parent.Parent is ProjectWhenElement || parent.Parent is ProjectOtherwiseElement, "OM_CannotAcceptParent"); + } + + /// + /// Overridden to update the parent's children-have-no-wildcards flag. + /// + internal override void OnAfterParentChanged(ProjectElementContainer parent) + { + base.OnAfterParentChanged(parent); + + if (parent != null) + { + // This is our indication that we just got attached to a parent + // Update its children-with-wildcards flag + ProjectItemGroupElement groupParent = parent as ProjectItemGroupElement; + if (groupParent != null && groupParent.DefinitelyAreNoChildrenWithWildcards && IncludeHasWildcards) + { + groupParent.DefinitelyAreNoChildrenWithWildcards = false; + } + } + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateItemElement(this.ItemType, this.Include); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectItemGroupElement.cs b/src/XMakeBuildEngine/Construction/ProjectItemGroupElement.cs new file mode 100644 index 00000000000..8e3c7080af2 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectItemGroupElement.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectItemGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectItemGroupElement represents the ItemGroup element in the MSBuild project. + /// + [DebuggerDisplay("#Items={Count} Condition={Condition} Label={Label}")] + public class ProjectItemGroupElement : ProjectElementContainer + { + /// + /// True if it is known that no child items have wildcards in their + /// include. An optimization helping Project.AddItem. + /// Only reliable if it is true. + /// + private bool _definitelyAreNoChildrenWithWildcards; + + /// + /// Initialize a parented ProjectItemGroupElement + /// + internal ProjectItemGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectItemGroupElement + /// + private ProjectItemGroupElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Get any child items. + /// This is a live collection. + /// + public ICollection Items + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// True if it is known that no child items have wildcards in their + /// include. An optimization helping Project.AddItem. + /// Only reliable if it is true. + /// ONLY TO BE CALLED by ProjectItemElement. + /// Should be protected+internal. + /// + internal bool DefinitelyAreNoChildrenWithWildcards + { + get + { + if (Count == 0) + { + _definitelyAreNoChildrenWithWildcards = true; + } + + return _definitelyAreNoChildrenWithWildcards; + } + + set + { + _definitelyAreNoChildrenWithWildcards = value; + } + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new item ordered by include. + /// + public ProjectItemElement AddItem(string itemType, string include) + { + return AddItem(itemType, include, null); + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new item ordered by include. + /// Metadata may be null, indicating no metadata. + /// + public ProjectItemElement AddItem(string itemType, string include, IEnumerable> metadata) + { + ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); + ErrorUtilities.VerifyThrowArgumentLength(include, "include"); + + // If there are no items, or it turns out that there are only items with + // item types that sort earlier, then we should go after the last child + ProjectElement reference = LastChild; + + foreach (ProjectItemElement item in Items) + { + // If it's the same item type, and + if (MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, item.ItemType)) + { + // the include sorts after us, + if (String.Compare(include, item.Include, StringComparison.OrdinalIgnoreCase) < 0) + { + // then insert before it (ie. after the previous sibling) + reference = item.PreviousSibling; + break; + } + + // Otherwise go to the next item + continue; + } + + // If it's an item type that sorts after us, + if (String.Compare(itemType, item.ItemType, StringComparison.OrdinalIgnoreCase) < 0) + { + // then insert before it (ie. after the previous sibling) + reference = item.PreviousSibling; + break; + } + } + + ProjectItemElement newItem = ContainingProject.CreateItemElement(itemType, include); + + // If reference is null, this will prepend + InsertAfterChild(newItem, reference); + + if (metadata != null) + { + foreach (KeyValuePair metadatum in metadata) + { + newItem.AddMetadata(metadatum.Key, metadatum.Value); + } + } + + return newItem; + } + + /// + public override void CopyFrom(ProjectElement element) + { + base.CopyFrom(element); + + // clear out caching fields. + _definitelyAreNoChildrenWithWildcards = false; + } + + /// + /// Creates an unparented ProjectItemGroupElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to the XmlDocument in the appropriate location. + /// + internal static ProjectItemGroupElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.itemGroup); + + return new ProjectItemGroupElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement || parent is ProjectTargetElement || parent is ProjectWhenElement || parent is ProjectOtherwiseElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateItemGroupElement(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectMetadataElement.cs b/src/XMakeBuildEngine/Construction/ProjectMetadataElement.cs new file mode 100644 index 00000000000..d601b75250f --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectMetadataElement.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectMetadataElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Shared; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; +using Utilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectMetadataElement class represents a Metadata element in the MSBuild project. + /// + [DebuggerDisplay("{Name} Value={Value} Condition={Condition}")] + public class ProjectMetadataElement : ProjectElement + { + /// + /// Initialize a parented ProjectMetadataElement + /// + internal ProjectMetadataElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement project) + : base(xmlElement, parent, project) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectMetadataElement + /// + private ProjectMetadataElement(XmlElementWithLocation xmlElement, ProjectRootElement project) + : base(xmlElement, null, project) + { + } + + /// + /// Gets or sets the metadata's type. + /// + public string Name + { + get { return XmlElement.Name; } + set { ChangeName(value); } + } + + /// + /// Gets or sets the unevaluated value. + /// Returns empty string if it is not present. + /// + public string Value + { + get + { + return Microsoft.Build.Internal.Utilities.GetXmlNodeInnerContents(XmlElement); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "Value"); + Microsoft.Build.Internal.Utilities.SetXmlNodeInnerContents(XmlElement, value); + MarkDirty("Set metadata Value {0}", value); + } + } + + /// + /// Creates an unparented ProjectMetadataElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRootElement containingProject) + { + XmlUtilities.VerifyThrowArgumentValidElementName(name); + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); + ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[name] == null, "CannotModifyReservedItemMetadata", name); + + XmlElementWithLocation element = containingProject.CreateElement(name); + + return new ProjectMetadataElement(element, containingProject); + } + + /// + /// Changes the name. + /// + /// + /// The implementation has to actually replace the element to do this. + /// + internal void ChangeName(string newName) + { + ErrorUtilities.VerifyThrowArgumentLength(newName, "newName"); + XmlUtilities.VerifyThrowArgumentValidElementName(newName); + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[newName] == null, "CannotModifyReservedItemMetadata", newName); + + // Because the element was created from our special XmlDocument, we know it's + // an XmlElementWithLocation. + XmlElementWithLocation newElement = (XmlElementWithLocation)XmlUtilities.RenameXmlElement(XmlElement, newName, XMakeAttributes.defaultXmlNamespace); + + ReplaceElement(newElement); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectItemElement || parent is ProjectItemDefinitionElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateMetadataElement(this.Name); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectOnErrorElement.cs b/src/XMakeBuildEngine/Construction/ProjectOnErrorElement.cs new file mode 100644 index 00000000000..b5f8497eaa4 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectOnErrorElement.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectOnErrorElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectUsingTaskElement represents the Import element in the MSBuild project. + /// + [DebuggerDisplay("ExecuteTargets={ExecuteTargets}")] + public class ProjectOnErrorElement : ProjectElement + { + /// + /// Initialize a parented ProjectOnErrorElement + /// + internal ProjectOnErrorElement(XmlElementWithLocation xmlElement, ProjectTargetElement parent, ProjectRootElement project) + : base(xmlElement, parent, project) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectOnErrorElement + /// + private ProjectOnErrorElement(XmlElementWithLocation xmlElement, ProjectRootElement project) + : base(xmlElement, null, project) + { + } + + /// + /// Gets and sets the value of the ExecuteTargets attribute. + /// + /// + /// 'Attribute' suffix is for clarity. + /// + public string ExecuteTargetsAttribute + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.executeTargets); + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.executeTargets); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.executeTargets, value); + MarkDirty("Set OnError ExecuteTargets {0}", value); + } + } + + /// + /// Location of the "ExecuteTargets" attribute on this element, if any. + /// If there is no such attribute, returns null; + /// + public ElementLocation ExecuteTargetsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.executeTargets); } + } + + /// + /// Creates an unparented ProjectOnErrorElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectOnErrorElement CreateDisconnected(string executeTargets, ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.onError); + + ProjectOnErrorElement onError = new ProjectOnErrorElement(element, containingProject); + + onError.ExecuteTargetsAttribute = executeTargets; + + return onError; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectTargetElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateOnErrorElement(this.ExecuteTargetsAttribute); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectOtherwiseElement.cs b/src/XMakeBuildEngine/Construction/ProjectOtherwiseElement.cs new file mode 100644 index 00000000000..1b5384ed47b --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectOtherwiseElement.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectOtherwiseElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectOtherwiseElement represents the Otherwise element in the MSBuild project. + /// + [DebuggerDisplay("#Children={Count}")] + public class ProjectOtherwiseElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectOtherwiseElement + /// + internal ProjectOtherwiseElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement project) + : base(xmlElement, parent, project) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectOtherwiseElement + /// + private ProjectOtherwiseElement(XmlElementWithLocation xmlElement, ProjectRootElement project) + : base(xmlElement, null, project) + { + } + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + #region ChildEnumerators + /// + /// Get an enumerator over any child item groups + /// + public ICollection ItemGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child property groups + /// + public ICollection PropertyGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child chooses + /// + public ICollection ChooseElements + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + #endregion + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + /// + /// Creates an unparented ProjectOtherwiseElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectOtherwiseElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.otherwise); + + return new ProjectOtherwiseElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectChooseElement, "OM_CannotAcceptParent"); + ErrorUtilities.VerifyThrowInvalidOperation(!(nextSibling is ProjectWhenElement) && !(previousSibling is ProjectOtherwiseElement) && !(nextSibling is ProjectOtherwiseElement), "OM_NoOtherwiseBeforeWhenOrOtherwise"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateOtherwiseElement(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectOutputElement.cs b/src/XMakeBuildEngine/Construction/ProjectOutputElement.cs new file mode 100644 index 00000000000..e20773224f0 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectOutputElement.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectOutputElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectOutputElement represents the Output element in the MSBuild project. + /// + [DebuggerDisplay("Name={Name} TaskParameter={TaskParameter} ItemName={ItemName} PropertyName={PropertyName} Condition={Condition}")] + public class ProjectOutputElement : ProjectElement + { + /// + /// Initialize a parented ProjectOutputElement + /// + internal ProjectOutputElement(XmlElement xmlElement, ProjectTaskElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectOutputElement + /// + private ProjectOutputElement(XmlElement xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets or sets the TaskParameter value. + /// Returns empty string if it is not present. + /// + public string TaskParameter + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.taskParameter); + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, "value"); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.taskParameter, value); + MarkDirty("Set Output TaskParameter {0}", value); + } + } + + /// + /// Whether this represents an output item (as opposed to an output property) + /// + public bool IsOutputItem + { + get { return ItemType.Length > 0; } + } + + /// + /// Whether this represents an output property (as opposed to an output item) + /// + public bool IsOutputProperty + { + get { return PropertyName.Length > 0; } + } + + /// + /// Gets or sets the ItemType value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + /// + /// Unfortunately the attribute name chosen in Whidbey was "ItemName" not ItemType. + /// + public string ItemType + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.itemName); + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(PropertyName), "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.itemName, XMakeAttributes.propertyName); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.itemName, value); + MarkDirty("Set Output ItemType {0}", value); + } + } + + /// + /// Gets or sets the PropertyName value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string PropertyName + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.propertyName); + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(ItemType), "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.itemName, XMakeAttributes.propertyName); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.propertyName, value); + MarkDirty("Set Output PropertyName {0}", value); + } + } + + /// + /// Location of the task parameter attribute + /// + public ElementLocation TaskParameterLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.taskParameter); } + } + + /// + /// Location of the property name attribute, if any + /// + public ElementLocation PropertyNameLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.propertyName); } + } + + /// + /// Location of the item type attribute, if any + /// + public ElementLocation ItemTypeLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.itemName); } + } + + /// + /// Creates an unparented ProjectOutputElement, wrapping an unparented XmlElement. + /// Validates the parameters. + /// Exactly one of item name and property name must have a value. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectOutputElement CreateDisconnected(string taskParameter, string itemType, string propertyName, ProjectRootElement containingProject) + { + ErrorUtilities.VerifyThrowArgument + ( + (String.IsNullOrEmpty(itemType) ^ String.IsNullOrEmpty(propertyName)), + "OM_EitherAttributeButNotBoth", + XMakeElements.output, + XMakeAttributes.propertyName, + XMakeAttributes.itemName + ); + + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.output); + + ProjectOutputElement output = new ProjectOutputElement(element, containingProject); + + output.TaskParameter = taskParameter; + + if (!String.IsNullOrEmpty(itemType)) + { + output.ItemType = itemType; + } + else + { + output.PropertyName = propertyName; + } + + return output; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectTaskElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateOutputElement(this.TaskParameter, this.ItemType, this.PropertyName); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Construction/ProjectPropertyElement.cs b/src/XMakeBuildEngine/Construction/ProjectPropertyElement.cs new file mode 100644 index 00000000000..809628e046e --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectPropertyElement.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectPropertyElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectPropertyElement class represents the Property element in the MSBuild project. + /// + /// + /// We do not need to use or set the PropertyType enumeration in the CM. + /// The CM does not know about Environment or Global properties, and does not create Output properties. + /// We can just verify that we haven't read a PropertyType.Reserved property ourselves. + /// So the CM only represents Normal properties. + /// + [DebuggerDisplay("{Name} Value={Value} Condition={Condition}")] + public class ProjectPropertyElement : ProjectElement + { + /// + /// Initialize a parented ProjectPropertyElement + /// + internal ProjectPropertyElement(XmlElementWithLocation xmlElement, ProjectPropertyGroupElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectPropertyElement + /// + private ProjectPropertyElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets or sets the property name. + /// + public string Name + { + get { return XmlElement.Name; } + set { ChangeName(value); } + } + + /// + /// Gets or sets the unevaluated value. + /// Returns empty string if it is not present. + /// + public string Value + { + get + { + return Microsoft.Build.Internal.Utilities.GetXmlNodeInnerContents(XmlElement); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "Value"); + + // Visual Studio has a tendency to set properties to their existing value. + if (Value != value) + { + Microsoft.Build.Internal.Utilities.SetXmlNodeInnerContents(XmlElement, value); + MarkDirty("Set property Value {0}", value); + } + } + } + + /// + /// Creates an unparented ProjectPropertyElement, wrapping an unparented XmlElement. + /// Validates name. + /// Caller should then ensure the element is added to the XmlDocument in the appropriate location. + /// + internal static ProjectPropertyElement CreateDisconnected(string name, ProjectRootElement containingProject) + { + XmlUtilities.VerifyThrowArgumentValidElementName(name); + + ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[name] == null && !ReservedPropertyNames.IsReservedProperty(name), "OM_CannotCreateReservedProperty", name); + + XmlElementWithLocation element = containingProject.CreateElement(name); + + return new ProjectPropertyElement(element, containingProject); + } + + /// + /// Changes the name. + /// + /// + /// The implementation has to actually replace the element to do this. + /// + internal void ChangeName(string newName) + { + ErrorUtilities.VerifyThrowArgumentLength(newName, "newName"); + XmlUtilities.VerifyThrowArgumentValidElementName(newName); + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[newName] == null, "CannotModifyReservedProperty", newName); + + // Because the element was created from our special XmlDocument, we know it's + // an XmlElementWithLocation. + XmlElementWithLocation newElement = (XmlElementWithLocation)XmlUtilities.RenameXmlElement(XmlElement, newName, XMakeAttributes.defaultXmlNamespace); + + ReplaceElement(newElement); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectPropertyGroupElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreatePropertyElement(this.Name); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectPropertyGroupElement.cs b/src/XMakeBuildEngine/Construction/ProjectPropertyGroupElement.cs new file mode 100644 index 00000000000..1ea2f540d24 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectPropertyGroupElement.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectPropertyGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectPropertyGroupElement represents the PropertyGroup element in the MSBuild project. + /// + [DebuggerDisplay("#Properties={Count} Condition={Condition} Label={Label}")] + public class ProjectPropertyGroupElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectPropertyGroupElement + /// + internal ProjectPropertyGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectPropertyGroupElement + /// + private ProjectPropertyGroupElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Get any contained properties. + /// + public ICollection Properties + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get any contained properties. + /// + public ICollection PropertiesReversed + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(ChildrenReversed) + ); + } + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new property after the last property in this property group. + /// + public ProjectPropertyElement AddProperty(string name, string unevaluatedValue) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue, "unevaluatedValue"); + + ProjectPropertyElement newProperty = ContainingProject.CreatePropertyElement(name); + newProperty.Value = unevaluatedValue; + AppendChild(newProperty); + + return newProperty; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// If there is an existing property with the same name and no condition, + /// updates its value. Otherwise it adds a new property after the last property. + /// + public ProjectPropertyElement SetProperty(string name, string unevaluatedValue) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue, "unevaluatedValue"); + + foreach (ProjectPropertyElement property in Properties) + { + if (String.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase) && property.Condition.Length == 0) + { + property.Value = unevaluatedValue; + return property; + } + } + + return AddProperty(name, unevaluatedValue); + } + + /// + /// Creates an unparented ProjectPropertyGroupElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectPropertyGroupElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.propertyGroup); + + return new ProjectPropertyGroupElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement || parent is ProjectTargetElement || parent is ProjectWhenElement || parent is ProjectOtherwiseElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreatePropertyGroupElement(); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectRootElement.cs b/src/XMakeBuildEngine/Construction/ProjectRootElement.cs new file mode 100644 index 00000000000..46c38b9799a --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectRootElement.cs @@ -0,0 +1,2032 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectRootElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Xml; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#if MSBUILDENABLEVSPROFILING +using Microsoft.VisualStudio.Profiler; +#endif +#endif +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.Construction +{ + /// + /// Event handler for the event fired after this project file is named or renamed. + /// If the project file has not previously had a name, oldFullPath is null. + /// + internal delegate void RenameHandlerDelegate(string oldFullPath); + + /// + /// ProjectRootElement class represents an MSBuild project, an MSBuild targets file or any other file that conforms to MSBuild + /// project file schema. + /// This class and its related classes allow a complete MSBuild project or targets file to be read and written. + /// Comments and whitespace cannot be edited through this model at present. + /// + /// Each project root element is associated with exactly one ProjectCollection. This allows the owner of that project collection + /// to control its lifetime and not be surprised by edits via another project collection. + /// + [DebuggerDisplay("{FullPath} #Children={Count} DefaultTargets={DefaultTargets} ToolsVersion={ToolsVersion} InitialTargets={InitialTargets} ExplicitlyLoaded={IsExplicitlyLoaded}")] + public class ProjectRootElement : ProjectElementContainer + { + /// + /// Constant for default (empty) project file. + /// + private const string EmptyProjectFileContent = "\r\n\r\n"; + + /// + /// The singleton delegate that loads projects into the ProjectRootElement + /// + private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderDelegate = OpenLoader; + + /// + /// The default encoding to use / assume for a new project. + /// + private static readonly Encoding s_defaultEncoding = Encoding.UTF8; + + /// + /// A global counter used to ensure each project version is distinct from every other. + /// + /// + /// This number is static so that it is unique across the appdomain. That is so that a host + /// can know when a ProjectRootElement has been unloaded (perhaps after modification) and + /// reloaded -- the version won't reset to '0'. + /// + private static int s_globalVersionCounter = 0; + + /// + /// Version number of this object that was last saved to disk, or last loaded from disk. + /// Used to figure whether this object is dirty for saving. + /// Saving to or loading from a provided stream reader does not modify this value, only saving to or loading from disk. + /// The actual value is meaningless (since the counter is shared with all projects) -- + /// it should only be compared to a stored value. + /// Immediately after loading from disk, this has the same value as version. + /// + private int _versionOnDisk; + + /// + /// Current version number of this object. + /// Used to figure whether this object is dirty for saving, or projects evaluated from + /// this object need to be re-evaluated. + /// The actual value is meaningless (since the counter is shared with all projects) -- + /// it should only be compared to a stored value. + /// + /// + /// Set this only through . + /// + private int _version; + + /// + /// The encoding of the project that was (if applicable) loaded off disk, and that will be used to save the project. + /// + /// Defaults to UTF8 for new projects. + private Encoding _encoding; + + /// + /// The project file's location. It can be null if the project is not directly loaded from a file. + /// + private ElementLocation _projectFileLocation; + + /// + /// The directory that the project is in. + /// Essential for evaluting relative paths. + /// If the project is not loaded from disk, returns the current-directory from + /// the time the project was loaded - this is the same behavior as Whidbey/Orcas. + /// + private string _directory; + + /// + /// The time that this object was last changed. If it hasn't + /// been changed since being loaded or created, its value is . + /// Stored as UTC as this is faster when there are a large number of rapid edits. + /// + private DateTime _timeLastChangedUtc; + + /// + /// The last-write-time of the file that was read, when it was read. + /// This can be used to see whether the file has been changed on disk + /// by an external means. + /// + private DateTime _lastWriteTimeWhenRead; + + /// + /// The cache in which this project root element is stored. + /// + private ProjectRootElementCache _projectRootElementCache; + + /// + /// Reason it was last marked dirty; unlocalized, for debugging + /// + private string _dirtyReason = "first created project {0}"; + + /// + /// Parameter to be formatted into the dirty reason + /// + private string _dirtyParameter = String.Empty; + + /// + /// The build event context errors should be logged in. + /// + private BuildEventContext _buildEventContext; + + /// + /// Initialize a ProjectRootElement instance from a XmlReader. + /// May throw InvalidProjectFileException. + /// Leaves the project dirty, indicating there are unsaved changes. + /// Used to create a root element for solutions loaded by the 3.5 version of the solution wrapper. + /// + internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded) + : base() + { + ErrorUtilities.VerifyThrowArgumentNull(xmlReader, "xmlReader"); + ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); + + this.IsExplicitlyLoaded = isExplicitlyLoaded; + _projectRootElementCache = projectRootElementCache; + _directory = NativeMethodsShared.GetCurrentDirectory(); + IncrementVersion(); + + XmlDocumentWithLocation document = LoadDocument(xmlReader); + + ProjectParser.Parse(document, this); + } + + /// + /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. + /// Leaves the project dirty, indicating there are unsaved changes. + /// + private ProjectRootElement(ProjectRootElementCache projectRootElementCache) + { + ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); + + _projectRootElementCache = projectRootElementCache; + _directory = NativeMethodsShared.GetCurrentDirectory(); + IncrementVersion(); + + XmlDocumentWithLocation document = new XmlDocumentWithLocation(); + + XmlReaderSettings xrs = new XmlReaderSettings(); + xrs.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader xr = XmlReader.Create(new StringReader(ProjectRootElement.EmptyProjectFileContent), xrs)) + { + document.Load(xr); + } + + ProjectParser.Parse(document, this); + } + + /// + /// Initialize a ProjectRootElement instance over a project with the specified file path. + /// Assumes path is already normalized. + /// May throw InvalidProjectFileException. + /// + private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext) + : base() + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + ErrorUtilities.VerifyThrowInternalRooted(path); + ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); + ErrorUtilities.VerifyThrowArgumentNull(buildEventContext, "buildEventContext"); + _projectRootElementCache = projectRootElementCache; + _buildEventContext = buildEventContext; + + IncrementVersion(); + _versionOnDisk = _version; + _timeLastChangedUtc = DateTime.UtcNow; + + XmlDocumentWithLocation document = LoadDocument(path); + + ProjectParser.Parse(document, this); + + projectRootElementCache.AddEntry(this); + } + + /// + /// Initialize a ProjectRootElement instance from an existing document. + /// May throw InvalidProjectFileException. + /// Leaves the project dirty, indicating there are unsaved changes. + /// + /// + /// Do not make public: we do not wish to expose particular XML API's. + /// + private ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCache projectRootElementCache) + : base() + { + ErrorUtilities.VerifyThrowArgumentNull(document, "document"); + ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache"); + + _projectRootElementCache = projectRootElementCache; + _directory = NativeMethodsShared.GetCurrentDirectory(); + IncrementVersion(); + + ProjectParser.Parse(document, this); + } + + /// + /// Event raised after this project is renamed + /// + internal event RenameHandlerDelegate OnAfterProjectRename; + + /// + /// Event raised after the project XML is changed. + /// + internal event EventHandler OnProjectXmlChanged; + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + #region ChildEnumerators + /// + /// Get a read-only collection of the child chooses, if any + /// + /// + /// The name is inconsistent to make it more understandable, per API review. + /// + public ICollection ChooseElements + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get a read-only collection of the child item definition groups, if any + /// + public ICollection ItemDefinitionGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get a read-only collection of the child item definitions, if any, in all item definition groups anywhere in the project file. + /// + public ICollection ItemDefinitions + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(AllChildren) + ); + } + } + + /// + /// Get a read-only collection over the child item groups, if any. + /// Does not include any that may not be at the root, i.e. inside Choose elements. + /// + public ICollection ItemGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get a read-only collection of the child items, if any, in all item groups anywhere in the project file. + /// Not restricted to root item groups: traverses through Choose elements. + /// + public ICollection Items + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(AllChildren) + ); + } + } + + /// + /// Get a read-only collection of the child import groups, if any. + /// + public ICollection ImportGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get a read-only collection of the child imports + /// + public ICollection Imports + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(AllChildren) + ); + } + } + + /// + /// Get a read-only collection of the child property groups, if any. + /// Does not include any that may not be at the root, i.e. inside Choose elements. + /// + public ICollection PropertyGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Geta read-only collection of the child properties, if any, in all property groups anywhere in the project file. + /// Not restricted to root property groups: traverses through Choose elements. + /// + public ICollection Properties + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(AllChildren) + ); + } + } + + /// + /// Get a read-only collection of the child targets + /// + public ICollection Targets + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get a read-only collection of the child usingtasks, if any + /// + public ICollection UsingTasks + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get a read-only collection of the child item groups, if any, in reverse order + /// + public ICollection ItemGroupsReversed + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(ChildrenReversed) + ); + } + } + + /// + /// Get a read-only collection of the child item definition groups, if any, in reverse order + /// + public ICollection ItemDefinitionGroupsReversed + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(ChildrenReversed) + ); + } + } + + /// + /// Get a read-only collection of the child import groups, if any, in reverse order + /// + public ICollection ImportGroupsReversed + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(ChildrenReversed) + ); + } + } + + /// + /// Get a read-only collection of the child property groups, if any, in reverse order + /// + public ICollection PropertyGroupsReversed + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(ChildrenReversed) + ); + } + } + + #endregion + + /// + /// The directory that the project is in. + /// Essential for evaluting relative paths. + /// Is never null, even if the FullPath does not contain directory information. + /// If the project has not been loaded from disk and has not been given a path, returns the current-directory from + /// the time the project was loaded - this is the same behavior as Whidbey/Orcas. + /// If the project has not been loaded from disk but has been given a path, this path may not exist. + /// + public string DirectoryPath + { + [DebuggerStepThrough] + get + { return _directory ?? String.Empty; } + internal set { _directory = value; } // Used during solution load to ensure solutions which were created from a file have a location. + } + + /// + /// Full path to the project file. + /// If the project has not been loaded from disk and has not been given a path, returns null. + /// If the project has not been loaded from disk but has been given a path, this path may not exist. + /// Setter renames the project, if it already had a name. + /// + /// + /// Updates the ProjectRootElement cache. + /// + public string FullPath + { + get + { + return (_projectFileLocation != null) ? _projectFileLocation.File : null; + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, "value"); + + string oldFullPath = (_projectFileLocation != null) ? _projectFileLocation.File : null; + + // We do not control the current directory at this point, but assume that if we were + // passed a relative path, the caller assumes we will prepend the current directory. + string newFullPath = FileUtilities.NormalizePath(value); + + if (String.Equals(oldFullPath, newFullPath, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + _projectFileLocation = ElementLocation.Create(newFullPath); + _directory = Path.GetDirectoryName(newFullPath); + + if (XmlDocument != null) + { + XmlDocument.FullPath = newFullPath; + } + + if (oldFullPath == null) + { + _projectRootElementCache.AddEntry(this); + } + else + { + _projectRootElementCache.RenameEntry(oldFullPath, this); + } + + RenameHandlerDelegate rename = OnAfterProjectRename; + if (rename != null) + { + rename(oldFullPath); + } + + MarkDirty("Set project FullPath to '{0}'", FullPath); + } + } + + /// + /// Encoding that the project file is saved in, or will be saved in, unless + /// otherwise specified. + /// + /// + /// Returns the encoding from the Xml declaration if any, otherwise UTF8. + /// + public Encoding Encoding + { + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (_encoding == null) + { + XmlDeclaration declaration = XmlDocument.FirstChild as XmlDeclaration; + + if (declaration != null) + { + if (declaration.Encoding.Length > 0) + { + _encoding = Encoding.GetEncoding(declaration.Encoding); + } + } + } + + // Ensure we never return null, in case there was no xml declaration that we could find above. + return _encoding ?? s_defaultEncoding; + } + } + + /// + /// Gets or sets the value of DefaultTargets. If there is no DefaultTargets, returns empty string. + /// If the value is null or empty, removes the attribute. + /// + public string DefaultTargets + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.defaultTargets); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.defaultTargets, value); + MarkDirty("Set Project DefaultTargets to '{0}'", value); + } + } + + /// + /// Gets or sets the value of InitialTargets. If there is no InitialTargets, returns empty string. + /// If the value is null or empty, removes the attribute. + /// + public string InitialTargets + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.initialTargets); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.initialTargets, value); + MarkDirty("Set project InitialTargets to '{0}'", value); + } + } + + /// + /// Gets or sets the value of TreatAsLocalProperty. If there is no tag, returns empty string. + /// If the value being set is null or empty, removes the attribute. + /// + public string TreatAsLocalProperty + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.treatAsLocalProperty); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.treatAsLocalProperty, value); + MarkDirty("Set project TreatAsLocalProperty to '{0}'", value); + } + } + + /// + /// Gets or sets the value of ToolsVersion. If there is no ToolsVersion, returns empty string. + /// If the value is null or empty, removes the attribute. + /// + public string ToolsVersion + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.toolsVersion); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.toolsVersion, value); + MarkDirty("Set project ToolsVersion {0}", value); + } + } + + /// + /// Gets the XML representing this project as a string. + /// Does not remove any dirty flag. + /// + /// + /// Useful for debugging. + /// Note that we do not expose an XmlDocument or any other specific XML API. + /// + public string RawXml + { + get + { + using (StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture)) + { + using (ProjectWriter projectWriter = new ProjectWriter(stringWriter)) + { + projectWriter.Initialize(XmlDocument); + XmlDocument.Save(projectWriter); + } + + return stringWriter.ToString(); + } + } + } + + /// + /// Whether the XML has been modified since it was last loaded or saved. + /// + public bool HasUnsavedChanges + { + get + { + if (Version != _versionOnDisk) + { + return true; + } + + return false; + } + } + + /// + /// Version number of this object. + /// A host can compare this to a stored version number to determine whether + /// a project's XML has changed, even if it has also been saved since. + /// + /// The actual value is meaningless: an edit may increment it more than once, + /// so it should only be compared to a stored value. + /// + /// + /// Used by the Project class to figure whether changes have occurred that + /// it might want to pick up by reevaluation. + /// + /// Used by the ProjectRootElement class to determine whether it needs to save. + /// + /// This number is unique to the appdomain. That means that it is possible + /// to know when a ProjectRootElement has been unloaded (perhaps after modification) and + /// reloaded -- the version won't reset to '0'. + /// + /// We're assuming we don't have over 2 billion edits. + /// + public int Version + { + get + { + return _version; + } + } + + /// + /// The time that this object was last changed. If it hasn't + /// been changed since being loaded or created, its value is . + /// + /// + /// This is used by the VB/C# project system. + /// + public DateTime TimeLastChanged + { + [DebuggerStepThrough] + get + { return _timeLastChangedUtc.ToLocalTime(); } + } + + /// + /// The last-write-time of the file that was read, when it was read. + /// This can be used to see whether the file has been changed on disk + /// by an external means. + /// + public DateTime LastWriteTimeWhenRead + { + [DebuggerStepThrough] + get + { return _lastWriteTimeWhenRead; } + } + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + /// + /// Location of the originating file itself, not any specific content within it. + /// If the file has not been given a name, returns an empty location. + /// This is a case where it is legitimate to "not have a location". + /// + public ElementLocation ProjectFileLocation + { + get { return _projectFileLocation ?? ElementLocation.EmptyLocation; } + } + + /// + /// Location of the toolsversion attribute, if any + /// + public ElementLocation ToolsVersionLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.toolsVersion); } + } + + /// + /// Location of the defaulttargets attribute, if any + /// + public ElementLocation DefaultTargetsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.defaultTargets); } + } + + /// + /// Location of the initialtargets attribute, if any + /// + public ElementLocation InitialTargetsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.initialTargets); } + } + + /// + /// Location of the TreatAsLocalProperty attribute, if any + /// + public ElementLocation TreatAsLocalPropertyLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.treatAsLocalProperty); } + } + + /// + /// Has the project root element been explicitly loaded for a build or has it been implicitly loaded + /// as part of building another project. + /// + /// + /// Internal code that wants to set this to true should call . + /// The setter is private to make it more difficult to downgrade an existing PRE to an implicitly loaded state, which should never happen. + /// + internal bool IsExplicitlyLoaded + { + get; + private set; + } + + /// + /// Retrieves the root element cache with which this root element is associated. + /// + internal ProjectRootElementCache ProjectRootElementCache + { + [DebuggerStepThrough] + get + { return _projectRootElementCache; } + } + + /// + /// Gets a value indicating whether this PRE is known by its containing collection. + /// + internal bool IsMemberOfProjectCollection + { + get + { + // We call AddEntry on the ProjectRootElementCache when we first get our filename set. + return _projectFileLocation != null; + } + } + + /// + /// Indicates whether there are any targets in this project + /// that use the "Returns" attribute. If so, then this project file + /// is automatically assumed to be "Returns-enabled", and the default behaviour + /// for targets without Returns attributes changes from using the Outputs to + /// returning nothing by default. + /// + internal bool ContainsTargetsWithReturnsAttribute + { + get; + set; + } + + /// + /// Gets the ProjectExtensions child, if any, otherwise null. + /// + /// + /// Not public as we do not wish to encourage the use of ProjectExtensions. + /// + internal ProjectExtensionsElement ProjectExtensions + { + get + { + foreach (ProjectElement child in ChildrenReversed) + { + ProjectExtensionsElement extensions = child as ProjectExtensionsElement; + + if (extensions != null) + { + return extensions; + } + } + + return null; + } + } + + /// + /// Returns an unlocalized indication of how this file was last dirtied. + /// This is for debugging purposes only. + /// String formatting only occurs when retrieved. + /// + internal string LastDirtyReason + { + get + { + if (_dirtyReason == null) + { + return null; + } + + return String.Format(CultureInfo.InvariantCulture, _dirtyReason, _dirtyParameter); + } + } + + /// + /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. + /// Uses the global project collection. + /// + public static ProjectRootElement Create() + { + return Create(ProjectCollection.GlobalProjectCollection); + } + + /// + /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. + /// Uses the specified project collection. + /// + public static ProjectRootElement Create(ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + return Create(projectCollection.ProjectRootElementCache); + } + + /// + /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. + /// Uses the global project collection. + /// + public static ProjectRootElement Create(string path) + { + return Create(path, ProjectCollection.GlobalProjectCollection); + } + + /// + /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. + /// Uses the specified project collection. + /// + public static ProjectRootElement Create(string path, ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + ProjectRootElement projectRootElement = new ProjectRootElement(projectCollection.ProjectRootElementCache); + projectRootElement.FullPath = path; + + return projectRootElement; + } + + /// + /// Initialize a ProjectRootElement instance from an XmlReader. + /// Uses the global project collection. + /// May throw InvalidProjectFileException. + /// + public static ProjectRootElement Create(XmlReader xmlReader) + { + return Create(xmlReader, ProjectCollection.GlobalProjectCollection); + } + + /// + /// Initialize a ProjectRootElement instance from an XmlReader. + /// Uses the specified project collection. + /// May throw InvalidProjectFileException. + /// + public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/); + } + + /// + /// Initialize a ProjectRootElement instance by loading from the specified file path. + /// Uses the global project collection. + /// May throw InvalidProjectFileException. + /// + public static ProjectRootElement Open(string path) + { + return Open(path, ProjectCollection.GlobalProjectCollection); + } + + /// + /// Initialize a ProjectRootElement instance by loading from the specified file path. + /// Uses the specified project collection. + /// May throw InvalidProjectFileException. + /// + public static ProjectRootElement Open(string path, ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + path = FileUtilities.NormalizePath(path); + + return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/); + } + + /// + /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory. + /// Uses the global project collection. + /// + /// The path of the ProjectRootElement, cannot be null. + /// The loaded ProjectRootElement, or null if it is not currently in memory. + /// + /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore + /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory. + /// + public static ProjectRootElement TryOpen(string path) + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + + return TryOpen(path, ProjectCollection.GlobalProjectCollection); + } + + /// + /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory. + /// Uses the specified project collection. + /// + /// The path of the ProjectRootElement, cannot be null. + /// The loaded ProjectRootElement, or null if it is not currently in memory. + /// + /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore + /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory. + /// + public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentLength(path, "path"); + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + path = FileUtilities.NormalizePath(path); + + ProjectRootElement projectRootElement = projectCollection.ProjectRootElementCache.TryGet(path); + + return projectRootElement; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// If import groups exist, inserts into the last one without a condition on it. + /// Otherwise, creates an import at the end of the project. + /// + public ProjectImportElement AddImport(string project) + { + ErrorUtilities.VerifyThrowArgumentLength(project, "project"); + + ProjectImportGroupElement importGroupToAddTo = null; + + foreach (ProjectImportGroupElement importGroup in ImportGroupsReversed) + { + if (importGroup.Condition.Length > 0) + { + continue; + } + + importGroupToAddTo = importGroup; + break; + } + + ProjectImportElement import; + + if (importGroupToAddTo != null) + { + import = importGroupToAddTo.AddImport(project); + } + else + { + import = CreateImportElement(project); + AppendChild(import); + } + + return import; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Creates an import group at the end of the project. + /// + public ProjectImportGroupElement AddImportGroup() + { + ProjectImportGroupElement importGroup = CreateImportGroupElement(); + AppendChild(importGroup); + + return importGroup; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Finds item group with no condition with at least one item of same type, or else adds a new item group; + /// adds the item to that item group with items of the same type, ordered by include. + /// + /// + /// Per the previous implementation, it actually finds the last suitable item group, not the first. + /// + public ProjectItemElement AddItem(string itemType, string include) + { + return AddItem(itemType, include, null); + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Finds first item group with no condition with at least one item of same type, or else an empty item group; or else adds a new item group; + /// adds the item to that item group with items of the same type, ordered by include. + /// Does not attempt to check whether the item matches an existing wildcard expression; that is only possible + /// in the evaluated world. + /// + /// + /// Per the previous implementation, it actually finds the last suitable item group, not the first. + /// + public ProjectItemElement AddItem(string itemType, string include, IEnumerable> metadata) + { + ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); + ErrorUtilities.VerifyThrowArgumentLength(include, "include"); + + ProjectItemGroupElement itemGroupToAddTo = null; + + foreach (ProjectItemGroupElement itemGroup in ItemGroups) + { + if (itemGroup.Condition.Length > 0) + { + continue; + } + + if (itemGroupToAddTo == null && itemGroup.Count == 0) + { + itemGroupToAddTo = itemGroup; + } + + foreach (ProjectItemElement item in itemGroup.Items) + { + if (MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, item.ItemType)) + { + itemGroupToAddTo = itemGroup; + break; + } + } + + if (itemGroupToAddTo != null && itemGroupToAddTo.Count > 0) + { + break; + } + } + + if (itemGroupToAddTo == null) + { + itemGroupToAddTo = AddItemGroup(); + } + + ProjectItemElement newItem = itemGroupToAddTo.AddItem(itemType, include, metadata); + + return newItem; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds an item group after the last existing item group, if any; otherwise + /// adds an item group after the last existing property group, if any; otherwise + /// adds a new item group at the end of the project. + /// + public ProjectItemGroupElement AddItemGroup() + { + ProjectElement reference = null; + + foreach (ProjectItemGroupElement itemGroup in ItemGroupsReversed) + { + reference = itemGroup; + break; + } + + if (reference == null) + { + foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed) + { + reference = propertyGroup; + break; + } + } + + ProjectItemGroupElement newItemGroup = CreateItemGroupElement(); + + if (reference == null) + { + AppendChild(newItemGroup); + } + else + { + InsertAfterChild(newItemGroup, reference); + } + + return newItemGroup; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Finds first item definition group with no condition with at least one item definition of same item type, or else adds a new item definition group. + /// + public ProjectItemDefinitionElement AddItemDefinition(string itemType) + { + ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); + + ProjectItemDefinitionGroupElement itemDefinitionGroupToAddTo = null; + + foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroups) + { + if (itemDefinitionGroup.Condition.Length > 0) + { + continue; + } + + foreach (ProjectItemDefinitionElement itemDefinition in itemDefinitionGroup.ItemDefinitions) + { + if (MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, itemDefinition.ItemType)) + { + itemDefinitionGroupToAddTo = itemDefinitionGroup; + break; + } + } + + if (itemDefinitionGroupToAddTo != null) + { + break; + } + } + + if (itemDefinitionGroupToAddTo == null) + { + itemDefinitionGroupToAddTo = AddItemDefinitionGroup(); + } + + ProjectItemDefinitionElement newItemDefinition = CreateItemDefinitionElement(itemType); + + itemDefinitionGroupToAddTo.AppendChild(newItemDefinition); + + return newItemDefinition; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds an item definition group after the last existing item definition group, if any; otherwise + /// adds an item definition group after the last existing property group, if any; otherwise + /// adds a new item definition group at the end of the project. + /// + public ProjectItemDefinitionGroupElement AddItemDefinitionGroup() + { + ProjectElement reference = null; + + foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroupsReversed) + { + reference = itemDefinitionGroup; + break; + } + + if (reference == null) + { + foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed) + { + reference = propertyGroup; + break; + } + } + + ProjectItemDefinitionGroupElement newItemDefinitionGroup = CreateItemDefinitionGroupElement(); + + InsertAfterChild(newItemDefinitionGroup, reference); + + return newItemDefinitionGroup; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new property group after the last existing property group, if any; otherwise + /// at the start of the project. + /// + public ProjectPropertyGroupElement AddPropertyGroup() + { + ProjectPropertyGroupElement reference = null; + + foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed) + { + reference = propertyGroup; + break; + } + + ProjectPropertyGroupElement newPropertyGroup = CreatePropertyGroupElement(); + + InsertAfterChild(newPropertyGroup, reference); + + return newPropertyGroup; + } + + /// + /// Convenience method that picks a location based on a heuristic. + /// Updates the last existing property with the specified name that has no condition on itself or its property group, if any. + /// Otherwise, adds a new property in the first property group without a condition, creating a property group if necessary after + /// the last existing property group, else at the start of the project. + /// + public ProjectPropertyElement AddProperty(string name, string value) + { + ProjectPropertyGroupElement matchingPropertyGroup = null; + ProjectPropertyElement matchingProperty = null; + + foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroups) + { + if (propertyGroup.Condition.Length > 0) + { + continue; + } + + if (matchingPropertyGroup == null) + { + matchingPropertyGroup = propertyGroup; + } + + foreach (ProjectPropertyElement property in propertyGroup.Properties) + { + if (property.Condition.Length > 0) + { + continue; + } + + if (MSBuildNameIgnoreCaseComparer.Default.Equals(property.Name, name)) + { + matchingProperty = property; + } + } + } + + if (matchingProperty != null) + { + matchingProperty.Value = value; + + return matchingProperty; + } + + if (matchingPropertyGroup == null) + { + matchingPropertyGroup = AddPropertyGroup(); + } + + ProjectPropertyElement newProperty = matchingPropertyGroup.AddProperty(name, value); + + return newProperty; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Creates a target at the end of the project. + /// + public ProjectTargetElement AddTarget(string name) + { + ProjectTargetElement target = CreateTargetElement(name); + AppendChild(target); + + return target; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Creates a usingtask at the end of the project. + /// Exactly one of assemblyName or assemblyFile must be null. + /// + public ProjectUsingTaskElement AddUsingTask(string name, string assemblyFile, string assemblyName) + { + ProjectUsingTaskElement usingTask = CreateUsingTaskElement(name, assemblyFile, assemblyName); + AppendChild(usingTask); + + return usingTask; + } + + /// + /// Creates a choose. + /// Caller must add it to the location of choice in the project. + /// + public ProjectChooseElement CreateChooseElement() + { + return ProjectChooseElement.CreateDisconnected(this); + } + + /// + /// Creates an import. + /// Caller must add it to the location of choice in the project. + /// + public ProjectImportElement CreateImportElement(string project) + { + return ProjectImportElement.CreateDisconnected(project, this); + } + + /// + /// Creates an item node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectItemElement CreateItemElement(string itemType) + { + return ProjectItemElement.CreateDisconnected(itemType, this); + } + + /// + /// Creates an item node with an include. + /// Caller must add it to the location of choice in the project. + /// + public ProjectItemElement CreateItemElement(string itemType, string include) + { + ProjectItemElement item = ProjectItemElement.CreateDisconnected(itemType, this); + + item.Include = include; + + return item; + } + + /// + /// Creates an item definition. + /// Caller must add it to the location of choice in the project. + /// + public ProjectItemDefinitionElement CreateItemDefinitionElement(string itemType) + { + return ProjectItemDefinitionElement.CreateDisconnected(itemType, this); + } + + /// + /// Creates an item definition group. + /// Caller must add it to the location of choice in the project. + /// + public ProjectItemDefinitionGroupElement CreateItemDefinitionGroupElement() + { + return ProjectItemDefinitionGroupElement.CreateDisconnected(this); + } + + /// + /// Creates an item group. + /// Caller must add it to the location of choice in the project. + /// + public ProjectItemGroupElement CreateItemGroupElement() + { + return ProjectItemGroupElement.CreateDisconnected(this); + } + + /// + /// Creates an import group. + /// Caller must add it to the location of choice in the project. + /// + public ProjectImportGroupElement CreateImportGroupElement() + { + return ProjectImportGroupElement.CreateDisconnected(this); + } + + /// + /// Creates a metadata node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectMetadataElement CreateMetadataElement(string name) + { + return ProjectMetadataElement.CreateDisconnected(name, this); + } + + /// + /// Creates a metadata node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue) + { + ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this); + + metadatum.Value = unevaluatedValue; + + return metadatum; + } + + /// + /// Creates an on error node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectOnErrorElement CreateOnErrorElement(string executeTargets) + { + return ProjectOnErrorElement.CreateDisconnected(executeTargets, this); + } + + /// + /// Creates an otherwise node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectOtherwiseElement CreateOtherwiseElement() + { + return ProjectOtherwiseElement.CreateDisconnected(this); + } + + /// + /// Creates an output node. + /// Exactly one of itemType and propertyName must be specified. + /// Caller must add it to the location of choice in the project. + /// + public ProjectOutputElement CreateOutputElement(string taskParameter, string itemType, string propertyName) + { + return ProjectOutputElement.CreateDisconnected(taskParameter, itemType, propertyName, this); + } + + /// + /// Creates a project extensions node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectExtensionsElement CreateProjectExtensionsElement() + { + return ProjectExtensionsElement.CreateDisconnected(this); + } + + /// + /// Creates a property group. + /// Caller must add it to the location of choice in the project. + /// + public ProjectPropertyGroupElement CreatePropertyGroupElement() + { + return ProjectPropertyGroupElement.CreateDisconnected(this); + } + + /// + /// Creates a property. + /// Caller must add it to the location of choice in the project. + /// + public ProjectPropertyElement CreatePropertyElement(string name) + { + return ProjectPropertyElement.CreateDisconnected(name, this); + } + + /// + /// Creates a target. + /// Caller must add it to the location of choice in this project. + /// + public ProjectTargetElement CreateTargetElement(string name) + { + return ProjectTargetElement.CreateDisconnected(name, this); + } + + /// + /// Creates a task. + /// Caller must add it to the location of choice in this project. + /// + public ProjectTaskElement CreateTaskElement(string name) + { + return ProjectTaskElement.CreateDisconnected(name, this); + } + + /// + /// Creates a using task. + /// Caller must add it to the location of choice in the project. + /// Exactly one of assembly file and assembly name must be provided. + /// + public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName) + { + return CreateUsingTaskElement(taskName, assemblyFile, assemblyName, null, null); + } + + /// + /// Creates a using task. + /// Caller must add it to the location of choice in the project. + /// Exactly one of assembly file and assembly name must be provided. + /// Also allows providing optional runtime and architecture specifiers. Null is OK. + /// + public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture) + { + return ProjectUsingTaskElement.CreateDisconnected(taskName, assemblyFile, assemblyName, runtime, architecture, this); + } + + /// + /// Creates a ParameterGroup for use in a using task. + /// Caller must add it to the location of choice in the project under a using task. + /// + public UsingTaskParameterGroupElement CreateUsingTaskParameterGroupElement() + { + return UsingTaskParameterGroupElement.CreateDisconnected(this); + } + + /// + /// Creates a Parameter for use in a using ParameterGroup. + /// Caller must add it to the location of choice in the project under a using task. + /// + public ProjectUsingTaskParameterElement CreateUsingTaskParameterElement(string name, string output, string required, string parameterType) + { + return ProjectUsingTaskParameterElement.CreateDisconnected(name, output, required, parameterType, this); + } + + /// + /// Creates a Task element for use in a using task. + /// Caller must add it to the location of choice in the project under a using task. + /// + public ProjectUsingTaskBodyElement CreateUsingTaskBodyElement(string evaluate, string body) + { + return ProjectUsingTaskBodyElement.CreateDisconnected(evaluate, body, this); + } + + /// + /// Creates a when. + /// Caller must add it to the location of choice in this project. + /// + public ProjectWhenElement CreateWhenElement(string condition) + { + return ProjectWhenElement.CreateDisconnected(condition, this); + } + + /// + /// Save the project to the file system, if dirty. + /// Uses the Encoding returned by the Encoding property. + /// Creates any necessary directories. + /// May throw IO-related exceptions. + /// Clears the dirty flag. + /// + public void Save() + { + Save(Encoding); + } + + /// + /// Save the project to the file system, if dirty. + /// Creates any necessary directories. + /// May throw IO-related exceptions. + /// Clears the dirty flag. + /// + public void Save(Encoding saveEncoding) + { + ErrorUtilities.VerifyThrowInvalidOperation(_projectFileLocation != null, "OM_MustSetFileNameBeforeSave"); + +#if MSBUILDENABLEVSPROFILING + try + { + string beginProjectSave = String.Format(CultureInfo.CurrentCulture, "Save Project {0} To File - Begin", projectFileLocation.File); + DataCollection.CommentMarkProfile(8810, beginProjectSave); +#endif + + Directory.CreateDirectory(DirectoryPath); +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectSaveToFileBegin, CodeMarkerEvent.perfMSBuildProjectSaveToFileEnd)) +#endif + { + if (HasUnsavedChanges || saveEncoding != Encoding) + { + using (ProjectWriter projectWriter = new ProjectWriter(_projectFileLocation.File, saveEncoding)) + { + projectWriter.Initialize(XmlDocument); + XmlDocument.Save(projectWriter); + } + + _encoding = saveEncoding; + + FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(_projectFileLocation.File); + + // If the file was deleted by a race with someone else immediately after it was written above + // then we obviously can't read the write time. In this obscure case, we'll retain the + // older last write time, which at worst would cause the next load to unnecessarily + // come from disk. + if (fileInfo != null) + { + _lastWriteTimeWhenRead = fileInfo.LastWriteTime; + } + + _versionOnDisk = Version; + } + } +#if MSBUILDENABLEVSPROFILING + } + finally + { + string endProjectSave = String.Format(CultureInfo.CurrentCulture, "Save Project {0} To File - End", projectFileLocation.File); + DataCollection.CommentMarkProfile(8811, endProjectSave); + } +#endif + } + + /// + /// Save the project to the file system, if dirty or the path is different. + /// Creates any necessary directories. + /// May throw IO related exceptions. + /// Clears the Dirty flag. + /// + public void Save(string path) + { + Save(path, Encoding); + } + + /// + /// Save the project to the file system, if dirty or the path is different. + /// Creates any necessary directories. + /// May throw IO related exceptions. + /// Clears the Dirty flag. + /// + public void Save(string path, Encoding encoding) + { + FullPath = path; + + Save(encoding); + } + + /// + /// Save the project to the provided TextWriter, whether or not it is dirty. + /// Uses the encoding of the TextWriter. + /// Clears the Dirty flag. + /// + public void Save(TextWriter writer) + { + using (ProjectWriter projectWriter = new ProjectWriter(writer)) + { + projectWriter.Initialize(XmlDocument); + XmlDocument.Save(projectWriter); + } + + _versionOnDisk = Version; + } + + /// + /// Returns a clone of this project. + /// + /// The cloned element. + public ProjectRootElement DeepClone() + { + return (ProjectRootElement)this.DeepClone(this, null); + } + + /// + /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later. + /// Uses the specified project root element cache. + /// + internal static ProjectRootElement Create(ProjectRootElementCache projectRootElementCache) + { + return new ProjectRootElement(projectRootElementCache); + } + + /// + /// Initialize a ProjectRootElement instance by loading from the specified file path. + /// Assumes path is already normalized. + /// Uses the specified project root element cache. + /// May throw InvalidProjectFileException. + /// + internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded) + { + ErrorUtilities.VerifyThrowInternalRooted(path); + + ProjectRootElement projectRootElement = projectRootElementCache.Get(path, s_openLoaderDelegate, isExplicitlyLoaded); + + return projectRootElement; + } + + /// + /// Initialize a ProjectRootElement instance from an existing document. + /// Uses the global project collection. + /// May throw InvalidProjectFileException. + /// + /// + /// This is ultimately for unit testing. + /// Do not make public: we do not wish to expose particular XML API's. + /// + internal static ProjectRootElement Open(XmlDocumentWithLocation document) + { + ErrorUtilities.VerifyThrow(document.FullPath == null, "Only virtual documents supported"); + + return new ProjectRootElement(document, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); + } + + /// + /// Gets a ProjectRootElement representing an MSBuild file. + /// Path provided must be a canonicalized full path. + /// May throw InvalidProjectFileException or an IO-related exception. + /// + internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDictionary globalProperties, string toolsVersion, ILoggingService loggingService, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, bool isExplicitlyLoaded) + { + ErrorUtilities.VerifyThrowInternalRooted(fullPath); + + ProjectRootElement projectRootElement = projectRootElementCache.Get( + fullPath, + (path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, loggingService, cache, buildEventContext), + isExplicitlyLoaded); + + return projectRootElement; + } + + /// + /// Creates a XmlElement with the specified name in the document + /// containing this project. + /// + internal XmlElementWithLocation CreateElement(string name) + { + return (XmlElementWithLocation)XmlDocument.CreateElement(name, XMakeAttributes.defaultXmlNamespace); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotAcceptParent"); + } + + /// + /// Marks this project as dirty. + /// Typically called by child elements to indicate that they themselves have become dirty. + /// Accepts a reason for debugging purposes only, and optional reason parameter. + /// + /// + /// This is sealed because it is virtual and called in a constructor; by sealing it we + /// satisfy FXCop that nobody will override it to do something that would rely on + /// unconstructed state. + /// Should be protected+internal. + /// + internal sealed override void MarkDirty(string reason, string param) + { + IncrementVersion(); + + _dirtyReason = reason; + _dirtyParameter = param; + + _timeLastChangedUtc = DateTime.UtcNow; + + var changedEventArgs = new ProjectXmlChangedEventArgs(this, reason, param); + var projectXmlChanged = OnProjectXmlChanged; + if (projectXmlChanged != null) + { + projectXmlChanged(this, changedEventArgs); + } + + // Only bubble this event up if the cache knows about this PRE. + if (this.IsMemberOfProjectCollection) + { + _projectRootElementCache.OnProjectRootElementDirtied(this, changedEventArgs); + } + } + + /// + /// Bubbles a Project dirty notification up to the ProjectRootElementCache and ultimately to the ProjectCollection. + /// + /// The dirtied project. + internal void MarkProjectDirty(Project project) + { + ErrorUtilities.VerifyThrowArgumentNull(project, "project"); + + // Only bubble this event up if the cache knows about this PRE, which is equivalent to + // whether this PRE has a path. + if (_projectFileLocation != null) + { + _projectRootElementCache.OnProjectDirtied(project, new ProjectChangedEventArgs(project)); + } + } + + /// + /// Sets the property to true to indicate that this PRE + /// should not be removed from the cache until it is explicitly unloaded by some MSBuild client. + /// + internal void MarkAsExplicitlyLoaded() + { + IsExplicitlyLoaded = true; + } + + /// + /// Returns a new instance of ProjectRootElement that is affiliated with the same ProjectRootElementCache. + /// + /// The factory to use for creating the new instance. + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return ProjectRootElement.Create(owner._projectRootElementCache); + } + + /// + /// Creates a new ProjectRootElement for a specific PRE cache + /// + /// The path to the file to load. + /// The cache to load the PRE into. + private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache) + { + return new ProjectRootElement( + path, + projectRootElementCache, + new BuildEventContext(0, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId)); + } + + /// + /// Creates a ProjectRootElement representing a file, where the file may be a .sln instead of + /// an MSBuild format file. + /// Assumes path is already normalized. + /// If the file is in MSBuild format, may throw InvalidProjectFileException. + /// If the file is a solution, will throw an IO-related exception if the file cannot be read. + /// + private static ProjectRootElement CreateProjectFromPath + ( + string projectFile, + IDictionary globalProperties, + string toolsVersion, + ILoggingService loggingService, + ProjectRootElementCache projectRootElementCache, + BuildEventContext buildEventContext + ) + { + ErrorUtilities.VerifyThrowInternalRooted(projectFile); + + try + { + if (FileUtilities.IsVCProjFilename(projectFile)) + { + ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), "ProjectUpgradeNeededToVcxProj", projectFile); + } + + // OK it's a regular project file, load it normally. + return new ProjectRootElement(projectFile, projectRootElementCache, buildEventContext); + } + catch (InvalidProjectFileException) + { + throw; + } + catch (Exception ex) + { + if (!ExceptionHandling.NotExpectedException(ex)) + { + ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), ex, "InvalidProjectFile", ex.Message); + } + + throw; + } + } + + /// + /// Constructor helper to load an XmlDocumentWithLocation from a path. + /// Assumes path is already normalized. + /// May throw InvalidProjectFileException. + /// Never returns null. + /// Does NOT add to the ProjectRootElementCache. Caller should add after verifying subsequent MSBuild parsing succeeds. + /// + /// The full path to the document to load. + private XmlDocumentWithLocation LoadDocument(string fullPath) + { + ErrorUtilities.VerifyThrowInternalRooted(fullPath); + + XmlDocumentWithLocation document = new XmlDocumentWithLocation(); +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectLoadFromFileBegin, CodeMarkerEvent.perfMSBuildProjectLoadFromFileEnd)) +#endif + { + try + { +#if MSBUILDENABLEVSPROFILING + string beginProjectLoad = String.Format(CultureInfo.CurrentCulture, "Load Project {0} From File - Start", fullPath); + DataCollection.CommentMarkProfile(8806, beginProjectLoad); +#endif + using (XmlTextReader xtr = new XmlTextReader(fullPath)) + { + // Start the reader so it has an idea of what the encoding is. + xtr.DtdProcessing = DtdProcessing.Ignore; + xtr.Read(); + _encoding = xtr.Encoding; + document.Load(xtr); + } + + document.FullPath = fullPath; + _projectFileLocation = ElementLocation.Create(fullPath); + _directory = Path.GetDirectoryName(fullPath); + + if (XmlDocument != null) + { + XmlDocument.FullPath = fullPath; + } + + _lastWriteTimeWhenRead = FileUtilities.GetFileInfoNoThrow(fullPath).LastWriteTime; + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedIoOrXmlException(ex)) + { + throw; + } + + XmlException xmlException = ex as XmlException; + + BuildEventFileInfo fileInfo; + + if (xmlException != null) + { + fileInfo = new BuildEventFileInfo(xmlException); + } + else + { + fileInfo = new BuildEventFileInfo(fullPath); + } + + ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, ex, "InvalidProjectFile", ex.Message); + } +#if MSBUILDENABLEVSPROFILING + finally + { + string endProjectLoad = String.Format(CultureInfo.CurrentCulture, "Load Project {0} From File - End", fullPath); + DataCollection.CommentMarkProfile(8807, endProjectLoad); + } +#endif + } + + return document; + } + + /// + /// Constructor helper to load an XmlDocumentWithLocation from an XmlReader. + /// May throw InvalidProjectFileException. + /// Never returns null. + /// + private XmlDocumentWithLocation LoadDocument(XmlReader reader) + { + XmlDocumentWithLocation document = new XmlDocumentWithLocation(); + + try + { + document.Load(reader); + } + catch (XmlException ex) + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo(ex); + + ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, "InvalidProjectFile", ex.Message); + } + + return document; + } + + /// + /// Boost the appdomain-unique version counter for this object. + /// This is done when it is modified, and also when it is loaded. + /// + private void IncrementVersion() + { + _version = Interlocked.Increment(ref s_globalVersionCounter); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectTargetElement.cs b/src/XMakeBuildEngine/Construction/ProjectTargetElement.cs new file mode 100644 index 00000000000..38c7701ffcf --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectTargetElement.cs @@ -0,0 +1,493 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectTargetElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectTargetElement represents the Target element in the MSBuild project. + /// + [DebuggerDisplay("Name={Name} #Children={Count} Condition={Condition}")] + public class ProjectTargetElement : ProjectElementContainer + { + /// + /// Target name cached for performance + /// + private string _name; + + /// + /// Initialize a parented ProjectTargetElement + /// + internal ProjectTargetElement(XmlElementWithLocation xmlElement, ProjectRootElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectTargetElement + /// + private ProjectTargetElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + #region ChildEnumerators + /// + /// Get an enumerator over any child item groups + /// + public ICollection ItemGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child property groups + /// + public ICollection PropertyGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child tasks + /// + public ICollection Tasks + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child onerrors + /// + public ICollection OnErrors + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + #endregion + + /// + /// Gets and sets the name of the target element. + /// + public string Name + { + [DebuggerStepThrough] + get + { + // No thread-safety lock required here because many reader threads would set the same value to the field. + if (_name == null) + { + _name = EscapingUtilities.UnescapeAll(ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.name)); + } + + return _name; + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, "value"); + + string unescapedValue = EscapingUtilities.UnescapeAll(value); + + int indexOfSpecialCharacter = unescapedValue.IndexOfAny(XMakeElements.illegalTargetNameCharacters); + if (indexOfSpecialCharacter >= 0) + { + ErrorUtilities.ThrowArgument("OM_NameInvalid", unescapedValue, unescapedValue[indexOfSpecialCharacter]); + } + + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.name, unescapedValue); + _name = unescapedValue; + MarkDirty("Set target Name {0}", value); + } + } + + /// + /// Gets or sets the Inputs value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string Inputs + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.inputs); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.inputs); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.inputs, value); + MarkDirty("Set target Inputs {0}", value); + } + } + + /// + /// Gets or sets the Outputs value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string Outputs + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.outputs); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.outputs); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.outputs, value); + MarkDirty("Set target Outputs {0}", value); + } + } + + /// + /// Gets or sets the TrimDuplicateOutputs value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string KeepDuplicateOutputs + { + [DebuggerStepThrough] + get + { + string value = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.keepDuplicateOutputs); + if (String.IsNullOrEmpty(value) && !BuildParameters.KeepDuplicateOutputs) + { + // In 4.0, by default we do NOT keep duplicate outputs unless they user has either set the attribute + // explicitly or overridden it globally with MSBUILDKEEPDUPLICATEOUTPUTS set to a non-empty value. + value = "False"; + } + + return value; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.keepDuplicateOutputs); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.keepDuplicateOutputs, value); + MarkDirty("Set target KeepDuplicateOutputs {0}", value); + } + } + + /// + /// Gets or sets the DependsOnTargets value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string DependsOnTargets + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.dependsOnTargets); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.dependsOnTargets); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.dependsOnTargets, value); + MarkDirty("Set target DependsOnTargets {0}", value); + } + } + + /// + /// Gets or sets the BeforeTargets value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string BeforeTargets + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.beforeTargets); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.beforeTargets); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.beforeTargets, value); + MarkDirty("Set target BeforeTargets {0}", value); + } + } + + /// + /// Gets or sets the AfterTargets value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string AfterTargets + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.afterTargets); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, XMakeAttributes.afterTargets); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.afterTargets, value); + MarkDirty("Set target AfterTargets {0}", value); + } + } + + /// + /// Gets or sets the Returns value. + /// Returns null if the attribute is not present -- empty string is an allowable + /// value for both getting and setting. + /// Removes the attribute only if the value is set to null. + /// + public string Returns + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue + ( + XmlElement, + XMakeAttributes.returns, + true /* If the element is not there, return null */ + ); + } + + set + { + XmlAttributeWithLocation returnsAttribute = ProjectXmlUtilities.SetOrRemoveAttribute + ( + XmlElement, + XMakeAttributes.returns, + value, + true /* only remove the element if the value is null -- setting to empty string is OK */ + ); + + // if this target's Returns attribute is non-null, then there is at least one target in the + // parent project that has the returns attribute. + // NOTE: As things are currently, if a project is created that has targets with Returns, but then + // all of those targets are set to not have Returns anymore, the PRE will still claim that it + // contains targets with the Returns attribute. Do we care? + if (returnsAttribute != null) + { + ((ProjectRootElement)Parent).ContainsTargetsWithReturnsAttribute = true; + } + + MarkDirty("Set target Returns {0}", value); + } + } + + /// + /// Location of the Name attribute + /// + public ElementLocation NameLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.name); } + } + + /// + /// Location of the Inputs attribute + /// + public ElementLocation InputsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.inputs); } + } + + /// + /// Location of the Outputs attribute + /// + public ElementLocation OutputsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.outputs); } + } + + /// + /// Location of the TrimDuplicateOutputs attribute + /// + public ElementLocation KeepDuplicateOutputsLocation + { + get + { + ElementLocation location = XmlElement.GetAttributeLocation(XMakeAttributes.keepDuplicateOutputs); + if ((location == null) && !BuildParameters.KeepDuplicateOutputs) + { + // In 4.0, by default we do NOT keep duplicate outputs unless they user has either set the attribute + // explicitly or overridden it globally with MSBUILDKEEPDUPLICATEOUTPUTS set to a non-empty value. + location = NameLocation; + } + + return location; + } + } + + /// + /// Location of the DependsOnTargets attribute + /// + public ElementLocation DependsOnTargetsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.dependsOnTargets); } + } + + /// + /// Location of the BeforeTargets attribute + /// + public ElementLocation BeforeTargetsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.beforeTargets); } + } + + /// + /// Location of the Returns attribute + /// + public ElementLocation ReturnsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.returns); } + } + + /// + /// Location of the AfterTargets attribute + /// + public ElementLocation AfterTargetsLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.afterTargets); } + } + + /// + /// A cache of the last instance which was created from this target. + /// + internal ProjectTargetInstance TargetInstance + { + get; + set; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds an item group after the last child. + /// + public ProjectItemGroupElement AddItemGroup() + { + ProjectItemGroupElement itemGroup = ContainingProject.CreateItemGroupElement(); + + AppendChild(itemGroup); + + return itemGroup; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a property group after the last child. + /// + public ProjectPropertyGroupElement AddPropertyGroup() + { + ProjectPropertyGroupElement propertyGroup = ContainingProject.CreatePropertyGroupElement(); + + AppendChild(propertyGroup); + + return propertyGroup; + } + + /// + /// Convenience method to add a task to this target. + /// Adds after any existing task. + /// + public ProjectTaskElement AddTask(string taskName) + { + ErrorUtilities.VerifyThrowArgumentLength(taskName, "taskName"); + + ProjectTaskElement task = ContainingProject.CreateTaskElement(taskName); + + AppendChild(task); + + return task; + } + + /// + public override void CopyFrom(ProjectElement element) + { + base.CopyFrom(element); + + // Clear caching fields + _name = null; + } + + /// + /// Creates an unparented ProjectTargetElement, wrapping an unparented XmlElement. + /// Validates the name. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectTargetElement CreateDisconnected(string name, ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.target); + + ProjectTargetElement target = new ProjectTargetElement(element, containingProject); + + target.Name = name; + + return target; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent"); + } + + /// + /// Marks this element as dirty. + /// + internal override void MarkDirty(string reason, string param) + { + base.MarkDirty(reason, param); + TargetInstance = null; + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateTargetElement(this.Name); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectTaskElement.cs b/src/XMakeBuildEngine/Construction/ProjectTaskElement.cs new file mode 100644 index 00000000000..bac3b2b857c --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectTaskElement.cs @@ -0,0 +1,433 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectTaskElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectTaskElement represents the Task element in the MSBuild project. + /// + [DebuggerDisplay("{Name} Condition={Condition} ContinueOnError={ContinueOnError} MSBuildRuntime={MSBuildRuntime} MSBuildArchitecture={MSBuildArchitecture} #Outputs={Count}")] + public class ProjectTaskElement : ProjectElementContainer + { + /// + /// The parameters (excepting condition and continue-on-error) + /// + private CopyOnWriteDictionary> _parameters; + + /// + /// Protection for the parameters cache + /// + private Object _locker = new Object(); + + /// + /// Initialize a parented ProjectTaskElement + /// + internal ProjectTaskElement(XmlElementWithLocation xmlElement, ProjectTargetElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectTaskElement + /// + private ProjectTaskElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets or sets the continue on error value. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string ContinueOnError + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.continueOnError); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.continueOnError, value); + MarkDirty("Set task ContinueOnError {0}", value); + } + } + + /// + /// Gets or sets the runtime value for the task. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string MSBuildRuntime + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.msbuildRuntime); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.msbuildRuntime, value); + MarkDirty("Set task MSBuildRuntime {0}", value); + } + } + + /// + /// Gets or sets the architecture value for the task. + /// Returns empty string if it is not present. + /// Removes the attribute if the value to set is empty. + /// + public string MSBuildArchitecture + { + [DebuggerStepThrough] + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.msbuildArchitecture); + } + + [DebuggerStepThrough] + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.msbuildArchitecture, value); + MarkDirty("Set task MSBuildArchitecture {0}", value); + } + } + + /// + /// Gets the task name + /// + public string Name + { + get { return XmlElement.Name; } + } + + /// + /// Gets any output children. + /// + public ICollection Outputs + { + get + { + return new Microsoft.Build.Collections.ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Enumerable over the unevaluated parameters on the task. + /// Attributes with their own properties, such as ContinueOnError, are not included in this collection. + /// If parameters differ only by case only the last one will be returned. MSBuild uses only this one. + /// Hosts can still remove the other parameters by using RemoveAllParameters(). + /// + public IDictionary Parameters + { + get + { + lock (_locker) + { + EnsureParametersInitialized(); + + Dictionary parametersClone = new Dictionary(_parameters.Count, StringComparer.OrdinalIgnoreCase); + + foreach (var entry in _parameters) + { + parametersClone[entry.Key] = entry.Value.Item1; + } + + return new ReadOnlyDictionary(parametersClone); + } + } + } + + /// + /// Enumerable over the locations of parameters on the task. + /// Condition and ContinueOnError, which have their own properties, are not included in this collection. + /// If parameters differ only by case only the last one will be returned. MSBuild uses only this one. + /// Hosts can still remove the other parameters by using RemoveAllParameters(). + /// + public IEnumerable> ParameterLocations + { + get + { + lock (_locker) + { + EnsureParametersInitialized(); + + var parameterLocations = new List>(); + + foreach (var entry in _parameters) + { + parameterLocations.Add(new KeyValuePair(entry.Key, entry.Value.Item2)); + } + + return parameterLocations; + } + } + } + + /// + /// Location of the "ContinueOnError" attribute on this element, if any. + /// If there is no such attribute, returns null; + /// + public ElementLocation ContinueOnErrorLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.continueOnError); } + } + + /// + /// Location of the "MSBuildRuntime" attribute on this element, if any. + /// If there is no such attribute, returns null; + /// + public ElementLocation MSBuildRuntimeLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.msbuildRuntime); } + } + + /// + /// Location of the "MSBuildArchitecture" attribute on this element, if any. + /// If there is no such attribute, returns null; + /// + public ElementLocation MSBuildArchitectureLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.msbuildArchitecture); } + } + + /// + /// Retrieves a copy of the parameters as used during evaluation. + /// + internal CopyOnWriteDictionary> ParametersForEvaluation + { + get + { + lock (_locker) + { + EnsureParametersInitialized(); + + return _parameters.Clone(); // copy on write! + } + } + } + + /// + /// Convenience method to add an Output Item to this task. + /// Adds after the last child. + /// + public ProjectOutputElement AddOutputItem(string taskParameter, string itemType) + { + ErrorUtilities.VerifyThrowArgumentLength(taskParameter, "taskParameter"); + ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); + + return AddOutputItem(taskParameter, itemType, null); + } + + /// + /// Convenience method to add a conditioned Output Item to this task. + /// Adds after the last child. + /// + public ProjectOutputElement AddOutputItem(string taskParameter, string itemType, string condition) + { + ProjectOutputElement outputItem = ContainingProject.CreateOutputElement(taskParameter, itemType, null); + + if (condition != null) + { + outputItem.Condition = condition; + } + + AppendChild(outputItem); + + return outputItem; + } + + /// + /// Convenience method to add an Output Property to this task. + /// Adds after the last child. + /// + public ProjectOutputElement AddOutputProperty(string taskParameter, string propertyName) + { + ErrorUtilities.VerifyThrowArgumentLength(taskParameter, "taskParameter"); + ErrorUtilities.VerifyThrowArgumentLength(propertyName, "propertyName"); + + return AddOutputProperty(taskParameter, propertyName, null); + } + + /// + /// Convenience method to add a conditioned Output Property to this task. + /// Adds after the last child. + /// + public ProjectOutputElement AddOutputProperty(string taskParameter, string propertyName, string condition) + { + ProjectOutputElement outputProperty = ContainingProject.CreateOutputElement(taskParameter, null, propertyName); + + if (condition != null) + { + outputProperty.Condition = condition; + } + + AppendChild(outputProperty); + + return outputProperty; + } + + /// + /// Gets the value of the parameter with the specified name, + /// or empty string if it is not present. + /// + public string GetParameter(string name) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + EnsureParametersInitialized(); + + Tuple parameter; + if (_parameters.TryGetValue(name, out parameter)) + { + return parameter.Item1; + } + + return String.Empty; + } + } + + /// + /// Adds (or modifies the value of) a parameter on this task + /// + public void SetParameter(string name, string unevaluatedValue) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue, "unevaluatedValue"); + ErrorUtilities.VerifyThrowArgument(!XMakeAttributes.IsSpecialTaskAttribute(name), "CannotAccessKnownAttributes", name); + + _parameters = null; + XmlElement.SetAttribute(name, unevaluatedValue); + MarkDirty("Set task parameter {0}", name); + } + } + + /// + /// Removes any parameter on this task with the specified name. + /// If there is no such parameter, does nothing. + /// + public void RemoveParameter(string name) + { + lock (_locker) + { + _parameters = null; + XmlElement.RemoveAttribute(name); + MarkDirty("Remove task parameter {0}", name); + } + } + + /// + /// Removes all parameters from the task. + /// Does not remove any "special" parameters: ContinueOnError, Condition, etc. + /// + public void RemoveAllParameters() + { + lock (_locker) + { + _parameters = null; + foreach (XmlAttribute attribute in XmlElement.Attributes) + { + if (!XMakeAttributes.IsSpecialTaskAttribute(attribute.Name)) + { + XmlElement.RemoveAttributeNode(attribute); + } + } + + MarkDirty("Remove all task parameters on {0}", Name); + } + } + + /// + public override void CopyFrom(ProjectElement element) + { + base.CopyFrom(element); + + // Clear caching fields + _parameters = null; + } + + /// + /// Creates an unparented ProjectTaskElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to the XmlDocument in the appropriate location. + /// + /// + /// Any legal XML element name is allowed. We can't easily verify if the name is a legal XML element name, + /// so this will specifically throw XmlException if it isn't. + /// + internal static ProjectTaskElement CreateDisconnected(string name, ProjectRootElement containingProject) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + XmlElementWithLocation element = containingProject.CreateElement(name); + + return new ProjectTaskElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectTargetElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateTaskElement(this.Name); + } + + /// + /// Initialize parameters cache. + /// Must be called within the lock. + /// + private void EnsureParametersInitialized() + { + if (_parameters == null) + { + _parameters = new CopyOnWriteDictionary>(XmlElement.Attributes.Count, StringComparer.OrdinalIgnoreCase); + + foreach (XmlAttributeWithLocation attribute in XmlElement.Attributes) + { + if (!XMakeAttributes.IsSpecialTaskAttribute(attribute.Name)) + { + // By pulling off and caching the Location early here, it becomes frozen for the life of this object. + // That means that if the name of the file is changed after first load (possibly from null) it will + // remain the old value here. Correctly, this should cache the attribute not the location. Fixing + // that will need profiling, though, as this cache was added for performance. + _parameters[attribute.Name] = new Tuple(attribute.Value, attribute.Location); + } + } + } + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectUsingTaskBodyElement.cs b/src/XMakeBuildEngine/Construction/ProjectUsingTaskBodyElement.cs new file mode 100644 index 00000000000..f54a859ba6d --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectUsingTaskBodyElement.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectUsingTaskBodyElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Shared; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; +using Utilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectUsingTaskBodyElement class represents the Task element under the using task element in the MSBuild project. + /// + [DebuggerDisplay("Evaluate={Evaluate} TaskBody={TaskBody}")] + public class ProjectUsingTaskBodyElement : ProjectElement + { + /// + /// Initialize a parented ProjectUsingTaskBodyElement + /// + internal ProjectUsingTaskBodyElement(XmlElementWithLocation xmlElement, ProjectUsingTaskElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + VerifyCorrectParent(parent); + } + + /// + /// Initialize an unparented ProjectUsingTaskBodyElement + /// + private ProjectUsingTaskBodyElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + /// + /// Gets or sets the unevaluated value of the contents of the task xml + /// Returns empty string if it is not present. + /// + public string TaskBody + { + get + { + return Microsoft.Build.Internal.Utilities.GetXmlNodeInnerContents(XmlElement); + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "TaskBody"); + Microsoft.Build.Internal.Utilities.SetXmlNodeInnerContents(XmlElement, value); + MarkDirty("Set usingtask body {0}", value); + } + } + + /// + /// Gets the value of the Evaluate attribute. + /// Returns true if it is not present. + /// + public string Evaluate + { + get + { + string evaluateAttribute = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.evaluate); + + if (evaluateAttribute.Length == 0) + { + return bool.TrueString; + } + + return evaluateAttribute; + } + + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.evaluate, value); + MarkDirty("Set usingtask Evaluate {0}", value); + } + } + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + /// + /// Location of the "Condition" attribute on this element, if any. + /// If there is no such attribute, returns the location of the element, + /// in lieu of the default value it uses for the attribute. + /// + public ElementLocation EvaluateLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.evaluate) ?? Location; } + } + + /// + /// Creates an unparented ProjectUsingTaskBodyElement, wrapping an unparented XmlElement. + /// Validates name. + /// Caller should then ensure the element is added to the XmlDocument in the appropriate location. + /// + internal static ProjectUsingTaskBodyElement CreateDisconnected(string evaluate, string body, ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.usingTaskBody); + ProjectUsingTaskBodyElement taskElement = new ProjectUsingTaskBodyElement(element, containingProject); + taskElement.Evaluate = evaluate; + taskElement.TaskBody = body; + return taskElement; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + VerifyCorrectParent(parent); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateUsingTaskBodyElement(this.Evaluate, this.TaskBody); + } + + /// + /// Verify the parent is a usingTaskElement and that the taskFactory attribute is set + /// + private static void VerifyCorrectParent(ProjectElementContainer parent) + { + ProjectUsingTaskElement parentUsingTask = parent as ProjectUsingTaskElement; + ErrorUtilities.VerifyThrowInvalidOperation(parentUsingTask != null, "OM_CannotAcceptParent"); + + // Since there is not going to be a TaskElement on the using task we need to validate and make sure there is a TaskFactory attribute on the parent element and + // that it is not empty + if (parentUsingTask.TaskFactory.Length == 0) + { + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(parent.XmlElement, "TaskFactory"); + } + + // UNDONE: Do check to make sure the task body is the last child + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectUsingTaskElement.cs b/src/XMakeBuildEngine/Construction/ProjectUsingTaskElement.cs new file mode 100644 index 00000000000..a979859c283 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectUsingTaskElement.cs @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectUsingTaskElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectUsingTaskElement represents the Import element in the MSBuild project. + /// + [DebuggerDisplay("TaskName={TaskName} AssemblyName={AssemblyName} AssemblyFile={AssemblyFile} Condition={Condition} Runtime={Runtime} Architecture={Architecture}")] + public class ProjectUsingTaskElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectUsingTaskElement + /// + internal ProjectUsingTaskElement(XmlElementWithLocation xmlElement, ProjectRootElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectUsingTaskElement + /// + private ProjectUsingTaskElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Gets the value of the AssemblyFile attribute. + /// Returns empty string if it is not present. + /// + public string AssemblyFile + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.assemblyFile); + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.assemblyName); + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(AssemblyName), "OM_EitherAttributeButNotBoth", XmlElement.Name, XMakeAttributes.assemblyFile, XMakeAttributes.assemblyName); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.assemblyFile, value); + MarkDirty("Set usingtask AssemblyFile {0}", value); + } + } + + /// + /// Gets and sets the value of the AssemblyName attribute. + /// Returns empty string if it is not present. + /// + public string AssemblyName + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.assemblyName); + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.assemblyName); + ErrorUtilities.VerifyThrowInvalidOperation(String.IsNullOrEmpty(AssemblyFile), "OM_EitherAttributeButNotBoth", XMakeElements.usingTask, XMakeAttributes.assemblyFile, XMakeAttributes.assemblyName); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.assemblyName, value); + MarkDirty("Set usingtask AssemblyName {0}", value); + } + } + + /// + /// Gets and sets the value of the TaskName attribute. + /// + public string TaskName + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.taskName); + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.taskName); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.taskName, value); + MarkDirty("Set usingtask TaskName {0}", value); + } + } + + /// + /// Gets and sets the value of the TaskFactory attribute. + /// + public string TaskFactory + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.taskFactory); + } + + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.taskFactory, value); + MarkDirty("Set usingtask TaskFactory {0}", value); + } + } + + /// + /// Gets and sets the value of the Runtime attribute. + /// + public string Runtime + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.runtime); + } + + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.runtime, value); + MarkDirty("Set usingtask Runtime {0}", value); + } + } + + /// + /// Gets and sets the value of the Architecture attribute. + /// + public string Architecture + { + get + { + return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.architecture); + } + + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.architecture, value); + MarkDirty("Set usingtask Architecture {0}", value); + } + } + + /// + /// Get any contained TaskElement. + /// + public ProjectUsingTaskBodyElement TaskBody + { + get + { + ProjectUsingTaskBodyElement body = (LastChild == null) ? null : LastChild as ProjectUsingTaskBodyElement; + return body; + } + } + + /// + /// Get any contained ParameterGroup. + /// + public UsingTaskParameterGroupElement ParameterGroup + { + get + { + UsingTaskParameterGroupElement parameterGroup = (FirstChild == null) ? null : FirstChild as UsingTaskParameterGroupElement; + return parameterGroup; + } + } + + /// + /// Location of the task name attribute + /// + public ElementLocation TaskNameLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.taskName); } + } + + /// + /// Location of the assembly file attribute, if any + /// + public ElementLocation AssemblyFileLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.assemblyFile); } + } + + /// + /// Location of the assembly name attribute, if any + /// + public ElementLocation AssemblyNameLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.assemblyName); } + } + + /// + /// Location of the Runtime attribute, if any + /// + public ElementLocation RuntimeLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.runtime); } + } + + /// + /// Location of the Architecture attribute, if any + /// + public ElementLocation ArchitectureLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.architecture); } + } + + /// + /// Location of the TaskFactory attribute, if any + /// + public ElementLocation TaskFactoryLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.taskFactory); } + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new ParameterGroup to the using task to the end of the using task element + /// + public UsingTaskParameterGroupElement AddParameterGroup() + { + UsingTaskParameterGroupElement newParameterGroup = ContainingProject.CreateUsingTaskParameterGroupElement(); + PrependChild(newParameterGroup); + return newParameterGroup; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// Adds a new TaskBody to the using task to the end of the using task element + /// + public ProjectUsingTaskBodyElement AddUsingTaskBody(string evaluate, string taskBody) + { + ProjectUsingTaskBodyElement newTaskBody = ContainingProject.CreateUsingTaskBodyElement(evaluate, taskBody); + AppendChild(newTaskBody); + return newTaskBody; + } + + /// + /// Creates an unparented ProjectUsingTaskElement, wrapping an unparented XmlElement. + /// Validates the parameters. + /// Exactly one of assembly file and assembly name must have a value. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectUsingTaskElement CreateDisconnected(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture, ProjectRootElement containingProject) + { + ErrorUtilities.VerifyThrowArgument + ( + (String.IsNullOrEmpty(assemblyFile) ^ String.IsNullOrEmpty(assemblyName)), + "OM_EitherAttributeButNotBoth", + XMakeElements.usingTask, + XMakeAttributes.assemblyFile, + XMakeAttributes.assemblyName + ); + + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.usingTask); + + ProjectUsingTaskElement usingTask = new ProjectUsingTaskElement(element, containingProject); + + usingTask.TaskName = taskName; + usingTask.Runtime = runtime; + usingTask.Architecture = architecture; + + if (!String.IsNullOrEmpty(assemblyFile)) + { + usingTask.AssemblyFile = assemblyFile; + } + else + { + usingTask.AssemblyName = assemblyName; + } + + return usingTask; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateUsingTaskElement(this.TaskName, this.AssemblyFile, this.AssemblyName, this.Runtime, this.Architecture); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectUsingTaskParameterElement.cs b/src/XMakeBuildEngine/Construction/ProjectUsingTaskParameterElement.cs new file mode 100644 index 00000000000..a91f679f22a --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectUsingTaskParameterElement.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectUsingTaskParameterElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Shared; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// UsingTaskParameterElement class represents the Parameter element in the MSBuild project. + /// + [DebuggerDisplay("Name={Name} ParameterType={ParameterType} Output={Output} Required={Required}")] + public class ProjectUsingTaskParameterElement : ProjectElement + { + /// + /// Initialize a parented UsingTaskParameterElement instance + /// + internal ProjectUsingTaskParameterElement(XmlElementWithLocation xmlElement, UsingTaskParameterGroupElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented UsingTaskParameterElement instance + /// + private ProjectUsingTaskParameterElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + /// + /// Gets and sets the name of the parameter's name + /// + public string Name + { + [DebuggerStepThrough] + get + { + return XmlElement.Name; + } + + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, "Name"); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.name, value); + MarkDirty("Set usingtaskparameter {0}", value); + } + } + + /// + /// Gets or sets the Type attribute returns "System.String" if not set. + /// If null or empty is set the attribute will be removed from the element. + /// + public string ParameterType + { + get + { + string typeAttribute = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.parameterType); + + if (String.IsNullOrEmpty(typeAttribute)) + { + return typeof(String).FullName; + } + + return typeAttribute; + } + + set + { + // If null or empty is passed in remove the attribute from the element + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.parameterType, value); + MarkDirty("Set usingtaskparameter ParameterType {0}", value); + } + } + + /// + /// Gets or sets the output attribute + /// + public string Output + { + get + { + string outputAttribute = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.output); + if (String.IsNullOrEmpty(outputAttribute)) + { + return bool.FalseString; + } + + return outputAttribute; + } + + set + { + XmlAttribute typeAttribute = ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.output, value); + MarkDirty("Set usingtaskparameter Output {0}", value); + } + } + + /// + /// Gets or sets the required attribute + /// + public string Required + { + get + { + string requiredAttribute = ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.required); + if (String.IsNullOrEmpty(requiredAttribute)) + { + return bool.FalseString; + } + + return requiredAttribute; + } + + set + { + XmlAttribute typeAttribute = ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.required, value); + MarkDirty("Set usingtaskparameter Required {0}", value); + } + } + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + /// + /// Location of the Type attribute. + /// If there is no such attribute, returns the location of the element, + /// in lieu of the default value it uses for the attribute. + /// + public ElementLocation ParameterTypeLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.parameterType) ?? Location; } + } + + /// + /// Location of the Output attribute. + /// If there is no such attribute, returns the location of the element, + /// in lieu of the default value it uses for the attribute. + /// + public ElementLocation OutputLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.output) ?? Location; } + } + + /// + /// Location of the Required attribute. + /// If there is no such attribute, returns the location of the element, + /// in lieu of the default value it uses for the attribute. + /// + public ElementLocation RequiredLocation + { + get { return XmlElement.GetAttributeLocation(XMakeAttributes.required) ?? Location; } + } + + /// + /// Creates an unparented UsingTaskParameterElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent. + /// + internal static ProjectUsingTaskParameterElement CreateDisconnected(string parameterName, string output, string required, string parameterType, ProjectRootElement containingProject) + { + XmlUtilities.VerifyThrowArgumentValidElementName(parameterName); + XmlElementWithLocation element = containingProject.CreateElement(parameterName); + ProjectUsingTaskParameterElement parameter = new ProjectUsingTaskParameterElement(element, containingProject); + parameter.Output = output; + parameter.Required = required; + parameter.ParameterType = parameterType; + + return parameter; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is UsingTaskParameterGroupElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateUsingTaskParameterElement(this.Name, this.Output, this.Required, this.ParameterType); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/ProjectWhenElement.cs b/src/XMakeBuildEngine/Construction/ProjectWhenElement.cs new file mode 100644 index 00000000000..b8e38f3761f --- /dev/null +++ b/src/XMakeBuildEngine/Construction/ProjectWhenElement.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectWhenElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectWhenElement represents the When element in the MSBuild project. + /// + [DebuggerDisplay("#Children={Count} Condition={Condition}")] + public class ProjectWhenElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectWhenElement + /// + internal ProjectWhenElement(XmlElement xmlElement, ProjectChooseElement parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an unparented ProjectWhenElement + /// + private ProjectWhenElement(XmlElement xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + #region ChildEnumerators + /// + /// Get an enumerator over any child chooses + /// + public ICollection ChooseElements + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child item groups + /// + public ICollection ItemGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Get an enumerator over any child property groups + /// + public ICollection PropertyGroups + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + #endregion + + /// + /// Creates an unparented ProjectPropertyGroupElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectWhenElement CreateDisconnected(string condition, ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.when); + + ProjectWhenElement when = new ProjectWhenElement(element, containingProject); + when.Condition = condition; + + return when; + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectChooseElement, "OM_CannotAcceptParent"); + ErrorUtilities.VerifyThrowInvalidOperation(!(previousSibling is ProjectOtherwiseElement), "OM_NoOtherwiseBeforeWhenOrOtherwise"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateWhenElement(this.Condition); + } + } +} diff --git a/src/XMakeBuildEngine/Construction/Solution/ProjectConfigurationInSolution.cs b/src/XMakeBuildEngine/Construction/Solution/ProjectConfigurationInSolution.cs new file mode 100644 index 00000000000..c48e409102a --- /dev/null +++ b/src/XMakeBuildEngine/Construction/Solution/ProjectConfigurationInSolution.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a project configuration (e.g. "Debug|x86") +//----------------------------------------------------------------------- + +using System; + +namespace Microsoft.Build.Construction +{ + /// + /// This class represents an entry for a project configuration in a solution configuration. + /// + public sealed class ProjectConfigurationInSolution + { + /// + /// The configuration part of this configuration - e.g. "Debug", "Release" + /// + private string _configurationName; + + /// + /// The platform part of this configuration - e.g. "Any CPU", "Win32" + /// + private string _platformName; + + /// + /// The full name of this configuration - e.g. "Debug|Any CPU" + /// + private string _fullName; + + /// + /// True if this project configuration should be built as part of its parent solution configuration + /// + private bool _includeInBuild; + + /// + /// Constructor + /// + internal ProjectConfigurationInSolution(string configurationName, string platformName, bool includeInBuild) + { + _configurationName = configurationName; + _platformName = RemoveSpaceFromAnyCpuPlatform(platformName); + _includeInBuild = includeInBuild; + _fullName = SolutionConfigurationInSolution.ComputeFullName(_configurationName, _platformName); + } + + /// + /// The configuration part of this configuration - e.g. "Debug", "Release" + /// + public string ConfigurationName + { + get { return _configurationName; } + } + + /// + /// The platform part of this configuration - e.g. "Any CPU", "Win32" + /// + public string PlatformName + { + get { return _platformName; } + } + + /// + /// The full name of this configuration - e.g. "Debug|Any CPU" + /// + public string FullName + { + get { return _fullName; } + } + + /// + /// True if this project configuration should be built as part of its parent solution configuration + /// + public bool IncludeInBuild + { + get { return _includeInBuild; } + } + + /// + /// This is a hacky method to remove the space in the "Any CPU" platform in project configurations. + /// The problem is that this platform is stored as "AnyCPU" in project files, but the project system + /// reports it as "Any CPU" to the solution configuration manager. Because of that all solution configurations + /// contain the version with a space in it, and when we try and give that name to actual projects, + /// they have no clue what we're talking about. We need to remove the space in project platforms so that + /// the platform name matches the one used in projects. + /// + static private string RemoveSpaceFromAnyCpuPlatform(string platformName) + { + if (string.Compare(platformName, "Any CPU", StringComparison.OrdinalIgnoreCase) == 0) + { + return "AnyCPU"; + } + + return platformName; + } + } +} diff --git a/src/XMakeBuildEngine/Construction/Solution/ProjectInSolution.cs b/src/XMakeBuildEngine/Construction/Solution/ProjectInSolution.cs new file mode 100644 index 00000000000..5abb10e92ef --- /dev/null +++ b/src/XMakeBuildEngine/Construction/Solution/ProjectInSolution.cs @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Security; +using System.Text; +using System.Xml; + +using XMakeAttributes = Microsoft.Build.Shared.XMakeAttributes; +using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities; +using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo; +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; +using System.Collections.ObjectModel; + +namespace Microsoft.Build.Construction +{ + /// + /// An enumeration defining the different types of projects we might find in an SLN. + /// + public enum SolutionProjectType + { + Unknown, // Everything else besides the below well-known project types. + KnownToBeMSBuildFormat, // C#, VB, and VJ# projects + SolutionFolder, // Not really a project, but persisted as such in the .SLN file. + WebProject, // Venus projects + WebDeploymentProject, // Web Deployment (.wdproj) projects -- MSBuildFormat, but Whidbey-era ones specify ProjectReferences differently + EtpSubProject // Project inside an Enterprise Template project + } + + internal struct AspNetCompilerParameters + { + internal string aspNetVirtualPath; // For Venus projects only, Virtual path for web + internal string aspNetPhysicalPath; // For Venus projects only, Physical path for web + internal string aspNetTargetPath; // For Venus projects only, Target for output files + internal string aspNetForce; // For Venus projects only, Force overwrite of target + internal string aspNetUpdateable; // For Venus projects only, compiled web application is updateable + internal string aspNetDebug; // For Venus projects only, generate symbols, etc. + internal string aspNetKeyFile; // For Venus projects only, strong name key file. + internal string aspNetKeyContainer; // For Venus projects only, strong name key container. + internal string aspNetDelaySign; // For Venus projects only, delay sign strong name. + internal string aspNetAPTCA; // For Venus projects only, AllowPartiallyTrustedCallers. + internal string aspNetFixedNames; // For Venus projects only, generate fixed assembly names. + } + + /// + /// This class represents a project (or SLN folder) that is read in from a solution file. + /// + public sealed class ProjectInSolution + { + #region Constants + + /// + /// Characters that need to be cleansed from a project name. + /// + private static readonly char[] s_charsToCleanse = { '%', '$', '@', ';', '.', '(', ')', '\'' }; + + /// + /// Project names that need to be disambiguated when forming a target name + /// + internal static readonly string[] projectNamesToDisambiguate = { "Build", "Rebuild", "Clean", "Publish" }; + + /// + /// Character that will be used to replace 'unclean' ones. + /// + private const char cleanCharacter = '_'; + + #endregion + + #region Member data + + private SolutionProjectType _projectType; // For example, KnownToBeMSBuildFormat, VCProject, WebProject, etc. + private string _projectName; // For example, "WindowsApplication1" + private string _relativePath; // Relative from .SLN file. For example, "WindowsApplication1\WindowsApplication1.csproj" + private string _projectGuid; // The unique Guid assigned to this project or SLN folder. + private List _dependencies; // A list of strings representing the Guids of the dependent projects. + private ArrayList _projectReferences; // A list of strings representing the guids of referenced projects. + // This is only used for VC/Venus projects + private string _parentProjectGuid; // If this project (or SLN folder) is within a SLN folder, this is the Guid of the parent SLN folder. + private string _uniqueProjectName; // For example, "MySlnFolder\MySubSlnFolder\WindowsApplication1" + private Hashtable _aspNetConfigurations; // Key is configuration name, value is [struct] AspNetCompilerParameters + private SolutionFile _parentSolution; // The parent solution for this project + private string _targetFrameworkMoniker; // used for website projects, since they don't have a project file in which the + // target framework is stored. Defaults to .NETFX 3.5 + + /// + /// The project configuration in given solution configuration + /// K: full solution configuration name (cfg + platform) + /// V: project configuration + /// + private Dictionary _projectConfigurations; + + #endregion + + #region Constructors + + internal ProjectInSolution(SolutionFile solution) + { + _projectType = SolutionProjectType.Unknown; + _projectName = null; + _relativePath = null; + _projectGuid = null; + _dependencies = new List(); + _projectReferences = new ArrayList(); + _parentProjectGuid = null; + _uniqueProjectName = null; + _parentSolution = solution; + + // default to .NET Framework 3.5 if this is an old solution that doesn't explicitly say. + _targetFrameworkMoniker = ".NETFramework,Version=v3.5"; + + // This hashtable stores a AspNetCompilerParameters struct for each configuration name supported. + _aspNetConfigurations = new Hashtable(StringComparer.OrdinalIgnoreCase); + + _projectConfigurations = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + #endregion + + #region Properties + + /// + /// This project's name + /// + public string ProjectName + { + get { return _projectName; } + internal set { _projectName = value; } + } + + /// + /// The path to this project file, relative to the solution location + /// + public string RelativePath + { + get { return _relativePath; } + internal set { _relativePath = value; } + } + + /// + /// Returns the absolute path for this project + /// + public string AbsolutePath + { + get + { + return Path.Combine(this.ParentSolution.SolutionFileDirectory, this.RelativePath); + } + } + + /// + /// The unique guid associated with this project, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form + /// + public string ProjectGuid + { + get { return _projectGuid; } + internal set { _projectGuid = value; } + } + + /// + /// The guid, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, of this project's + /// parent project, if any. + /// + public string ParentProjectGuid + { + get { return _parentProjectGuid; } + internal set { _parentProjectGuid = value; } + } + + /// + /// List of guids, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, mapping to projects + /// that this project has a build order dependency on, as defined in the solution file. + /// + public IReadOnlyList Dependencies + { + get { return _dependencies.AsReadOnly(); } + } + + /// + /// Configurations for this project, keyed off the configuration's full name, e.g. "Debug|x86" + /// + public IReadOnlyDictionary ProjectConfigurations + { + get { return new ReadOnlyDictionary(_projectConfigurations); } + } + + /// + /// Extension of the project file, if any + /// + internal string Extension + { + get + { + return Path.GetExtension(_relativePath); + } + } + + /// + /// This project's type. + /// + public SolutionProjectType ProjectType + { + get { return _projectType; } + set { _projectType = value; } + } + + /// + /// Only applies to websites -- for other project types, references are + /// either specified as Dependencies above, or as ProjectReferences in the + /// project file, which the solution doesn't have insight into. + /// + internal ArrayList ProjectReferences + { + get { return _projectReferences; } + } + + internal SolutionFile ParentSolution + { + get { return _parentSolution; } + set { _parentSolution = value; } + } + + internal Hashtable AspNetConfigurations + { + get { return _aspNetConfigurations; } + set { _aspNetConfigurations = value; } + } + + internal string TargetFrameworkMoniker + { + get { return _targetFrameworkMoniker; } + set { _targetFrameworkMoniker = value; } + } + + #endregion + + #region Methods + + private bool _checkedIfCanBeMSBuildProjectFile = false; + private bool _canBeMSBuildProjectFile; + private string _canBeMSBuildProjectFileErrorMessage; + + /// + /// Add the guid of a referenced project to our dependencies list. + /// + internal void AddDependency(string referencedProjectGuid) + { + _dependencies.Add(referencedProjectGuid); + } + + /// + /// Set the requested project configuration. + /// + internal void SetProjectConfiguration(string configurationName, ProjectConfigurationInSolution configuration) + { + _projectConfigurations[configurationName] = configuration; + } + + /// + /// Looks at the project file node and determines (roughly) if the project file is in the MSBuild format. + /// The results are cached in case this method is called multiple times. + /// + /// Detailed error message in case we encounter critical problems reading the file + /// + internal bool CanBeMSBuildProjectFile(out string errorMessage) + { + if (_checkedIfCanBeMSBuildProjectFile) + { + errorMessage = _canBeMSBuildProjectFileErrorMessage; + return _canBeMSBuildProjectFile; + } + + _checkedIfCanBeMSBuildProjectFile = true; + _canBeMSBuildProjectFile = false; + errorMessage = null; + + try + { + // Read project thru a XmlReader with proper setting to avoid DTD processing + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + + XmlDocument projectDocument = new XmlDocument(); + + using (XmlReader xmlReader = XmlReader.Create(this.AbsolutePath, xrSettings)) + { + // Load the project file and get the first node + projectDocument.Load(xmlReader); + } + + XmlElement mainProjectElement = null; + + // The XML parser will guarantee that we only have one real root element, + // but we need to find it amongst the other types of XmlNode at the root. + foreach (XmlNode childNode in projectDocument.ChildNodes) + { + if (childNode.NodeType == XmlNodeType.Element) + { + mainProjectElement = (XmlElement)childNode; + break; + } + } + + if (mainProjectElement != null && mainProjectElement.LocalName == "Project") + { + if (String.Compare(mainProjectElement.NamespaceURI, XMakeAttributes.defaultXmlNamespace, StringComparison.OrdinalIgnoreCase) == 0) + { + _canBeMSBuildProjectFile = true; + return _canBeMSBuildProjectFile; + } + } + } + // catch all sorts of exceptions - if we encounter any problems here, we just assume the project file is not + // in the MSBuild format + + // handle errors in path resolution + catch (SecurityException e) + { + _canBeMSBuildProjectFileErrorMessage = e.Message; + } + // handle errors in path resolution + catch (NotSupportedException e) + { + _canBeMSBuildProjectFileErrorMessage = e.Message; + } + // handle errors in loading project file + catch (IOException e) + { + _canBeMSBuildProjectFileErrorMessage = e.Message; + } + // handle errors in loading project file + catch (UnauthorizedAccessException e) + { + _canBeMSBuildProjectFileErrorMessage = e.Message; + } + // handle XML parsing errors (when reading project file) + // this is not critical, since the project file doesn't have to be in XML formal + catch (XmlException) + { + } + + errorMessage = _canBeMSBuildProjectFileErrorMessage; + + return _canBeMSBuildProjectFile; + } + + /// + /// Find the unique name for this project, e.g. SolutionFolder\SubSolutionFolder\ProjectName + /// + internal string GetUniqueProjectName() + { + if (_uniqueProjectName == null) + { + // EtpSubProject and Venus projects have names that are already unique. No need to prepend the SLN folder. + if ((this.ProjectType == SolutionProjectType.WebProject) || (this.ProjectType == SolutionProjectType.EtpSubProject)) + { + _uniqueProjectName = CleanseProjectName(this.ProjectName); + } + else + { + // This is "normal" project, which in this context means anything non-Venus and non-EtpSubProject. + + // If this project has a parent SLN folder, first get the full unique name for the SLN folder, + // and tack on trailing backslash. + string uniqueName = String.Empty; + + if (this.ParentProjectGuid != null) + { + ProjectInSolution proj; + if (!this.ParentSolution.ProjectsByGuid.TryGetValue(this.ParentProjectGuid, out proj)) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(_parentSolution.FullPath), "SolutionParseNestedProjectError"); + } + + uniqueName = proj.GetUniqueProjectName() + "\\"; + } + + // Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access. + _uniqueProjectName = CleanseProjectName(uniqueName + this.ProjectName); + } + } + + return _uniqueProjectName; + } + + /// + /// Changes the unique name of the project. + /// + internal void UpdateUniqueProjectName(string newUniqueName) + { + ErrorUtilities.VerifyThrowArgumentLength(newUniqueName, "newUniqueName"); + + _uniqueProjectName = newUniqueName; + } + + /// + /// Cleanse the project name, by replacing characters like '@', '$' with '_' + /// + /// The name to be cleansed + /// string + static private string CleanseProjectName(string projectName) + { + ErrorUtilities.VerifyThrow(projectName != null, "Null strings not allowed."); + + // If there are no special chars, just return the original string immediately. + // Don't even instantiate the StringBuilder. + int indexOfChar = projectName.IndexOfAny(s_charsToCleanse); + if (indexOfChar == -1) + { + return projectName; + } + + // This is where we're going to work on the final string to return to the caller. + StringBuilder cleanProjectName = new StringBuilder(projectName); + + // Replace each unclean character with a clean one + foreach (char uncleanChar in s_charsToCleanse) + { + cleanProjectName.Replace(uncleanChar, cleanCharacter); + } + + return cleanProjectName.ToString(); + } + + /// + /// If the unique project name provided collides with one of the standard Solution project + /// entry point targets (Build, Rebuild, Clean, Publish), then disambiguate it by prepending the string "Solution:" + /// + /// The unique name for the project + /// string + static internal string DisambiguateProjectTargetName(string uniqueProjectName) + { + // Test our unique project name against those names that collide with Solution + // entry point targets + foreach (string projectName in projectNamesToDisambiguate) + { + if (String.Compare(uniqueProjectName, projectName, StringComparison.OrdinalIgnoreCase) == 0) + { + // Prepend "Solution:" so that the collision is resolved, but the + // log of the solution project still looks reasonable. + return "Solution:" + uniqueProjectName; + } + } + + return uniqueProjectName; + } + + #endregion + + #region Constants + + internal const int DependencyLevelUnknown = -1; + internal const int DependencyLevelBeingDetermined = -2; + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Construction/Solution/SolutionConfigurationInSolution.cs b/src/XMakeBuildEngine/Construction/Solution/SolutionConfigurationInSolution.cs new file mode 100644 index 00000000000..75a5e130a11 --- /dev/null +++ b/src/XMakeBuildEngine/Construction/Solution/SolutionConfigurationInSolution.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a solution configuration (e.g. "Debug|x86") +//----------------------------------------------------------------------- + +using System; +using System.Globalization; + +namespace Microsoft.Build.Construction +{ + /// + /// This represents an entry for a solution configuration + /// + public sealed class SolutionConfigurationInSolution + { + /// + /// Default seperator between configuration and platform in configuration + /// full names + /// + internal const char ConfigurationPlatformSeparator = '|'; + + /// + /// The configuration part of this configuration - e.g. "Debug", "Release" + /// + private string _configurationName; + + /// + /// The platform part of this configuration - e.g. "Any CPU", "Win32" + /// + private string _platformName; + + /// + /// The full name of this configuration - e.g. "Debug|Any CPU" + /// + private string _fullName; + + /// + /// Constructor + /// + internal SolutionConfigurationInSolution(string configurationName, string platformName) + { + _configurationName = configurationName; + _platformName = platformName; + _fullName = ComputeFullName(configurationName, platformName); + } + + /// + /// The configuration part of this configuration - e.g. "Debug", "Release" + /// + public string ConfigurationName + { + get { return _configurationName; } + } + + /// + /// The platform part of this configuration - e.g. "Any CPU", "Win32" + /// + public string PlatformName + { + get { return _platformName; } + } + + /// + /// The full name of this configuration - e.g. "Debug|Any CPU" + /// + public string FullName + { + get { return _fullName; } + } + + /// + /// Given a configuration name and a platform name, compute the full name + /// of this configuration + /// + internal static string ComputeFullName(string configurationName, string platformName) + { + // Some configurations don't have the platform part + if ((platformName != null) && (platformName.Length > 0)) + { + return String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", configurationName, ConfigurationPlatformSeparator, platformName); + } + else + { + return configurationName; + } + } + } +} diff --git a/src/XMakeBuildEngine/Construction/Solution/SolutionFile.cs b/src/XMakeBuildEngine/Construction/Solution/SolutionFile.cs new file mode 100644 index 00000000000..c9838e067bc --- /dev/null +++ b/src/XMakeBuildEngine/Construction/Solution/SolutionFile.cs @@ -0,0 +1,1601 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using System.IO; +using System.Text; +using System.Globalization; +using System.Security; +using System.Text.RegularExpressions; + +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; +using VisualStudioConstants = Microsoft.Build.Shared.VisualStudioConstants; +using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities; +using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo; +using ResourceUtilities = Microsoft.Build.Shared.ResourceUtilities; +using ExceptionUtilities = Microsoft.Build.Shared.ExceptionHandling; +using System.Collections.ObjectModel; + +namespace Microsoft.Build.Construction +{ + /// + /// This class contains the functionality to parse a solution file and return a corresponding + /// MSBuild project file containing the projects and dependencies defined in the solution. + /// + public sealed class SolutionFile + { + #region Solution specific constants + + // An example of a project line looks like this: + // Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{05A5AD00-71B5-4612-AF2F-9EA9121C4111}" + private static readonly Regex s_crackProjectLine = new Regex + ( + "^" // Beginning of line + + "Project\\(\"(?.*)\"\\)" + + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace + + "\"(?.*)\"" + + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace + + "\"(?.*)\"" + + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace + + "\"(?.*)\"" + + "$" // End-of-line + ); + + // An example of a property line looks like this: + // AspNetCompiler.VirtualPath = "/webprecompile" + // Because website projects now include the target framework moniker as + // one of their properties, may now have '=' in it. + + private static readonly Regex s_crackPropertyLine = new Regex + ( + "^" // Beginning of line + + "(?[^=]*)" + + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace + + "(?.*)" + + "$" // End-of-line + ); + + internal const int slnFileMinUpgradableVersion = 7; // Minimum version for MSBuild to give a nice message + internal const int slnFileMinVersion = 9; // Minimum version for MSBuild to actually do anything useful + internal const int slnFileMaxVersion = VisualStudioConstants.CurrentVisualStudioSolutionFileVersion; + + private const string vbProjectGuid = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"; + private const string csProjectGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; + private const string vjProjectGuid = "{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}"; + private const string vcProjectGuid = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; + private const string fsProjectGuid = "{F2A71F9B-5D33-465A-A702-920D77279786}"; + private const string dbProjectGuid = "{C8D11400-126E-41CD-887F-60BD40844F9E}"; + private const string wdProjectGuid = "{2CFEAB61-6A3B-4EB8-B523-560B4BEEF521}"; + private const string webProjectGuid = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; + private const string solutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"; + + #endregion + + #region Member data + + private int _slnFileActualVersion = 0; // The major version number of the .SLN file we're reading. + private string _solutionFile = null; // Could be absolute or relative path to the .SLN file. + private string _solutionFileDirectory = null; // Absolute path the solution file + private bool _solutionContainsWebProjects = false; // Does this SLN contain any web projects? + private bool _solutionContainsWebDeploymentProjects = false; // Does this SLN contain .wdproj projects? + private bool _parsingForConversionOnly = false; // Are we parsing this solution to get project reference data during + // conversion, or in preparation for actually building the solution? + + // The list of projects in this SLN, keyed by the project GUID. + private Dictionary _projects = null; + + // The list of projects in the SLN, in order of their appearance in the SLN. + private List _projectsInOrder = null; + + // The list of solution configurations in the solution + private List _solutionConfigurations; + + // cached default configuration name for GetDefaultConfigurationName + private string _defaultConfigurationName; + + // cached default platform name for GetDefaultPlatformName + private string _defaultPlatformName; + + //List of warnings that occured while parsing solution + private ArrayList _solutionParserWarnings = null; + + //List of comments that occured while parsing solution + private ArrayList _solutionParserComments = null; + + // unit-testing only + private ArrayList _solutionParserErrorCodes = null; + + // VisualStudionVersion specified in Dev12+ solutions + private Version _currentVisualStudioVersion = null; + + private StreamReader _reader = null; + private int _currentLineNumber = 0; + + #endregion + + #region Constructors + + /// + /// Constructor + /// + internal SolutionFile() + { + _solutionParserWarnings = new ArrayList(); + _solutionParserErrorCodes = new ArrayList(); + _solutionParserComments = new ArrayList(); + } + + #endregion + + #region Properties + + /// + /// This property returns the list of warnings that were generated during solution parsing + /// + internal ArrayList SolutionParserWarnings + { + get + { + return _solutionParserWarnings; + } + } + + /// + /// This property returns the list of comments that were generated during the solution parsing + /// + internal ArrayList SolutionParserComments + { + get + { + return _solutionParserComments; + } + } + + /// + /// This property returns the list of error codes for warnings/errors that were generated during solution parsing. + /// + internal ArrayList SolutionParserErrorCodes + { + get + { + return _solutionParserErrorCodes; + } + } + + /// + /// Returns the actual major version of the parsed solution file + /// + internal int Version + { + get + { + return _slnFileActualVersion; + } + } + + /// + /// Returns Visual Studio major version + /// + internal int VisualStudioVersion + { + get + { + if (_currentVisualStudioVersion != null) + { + return _currentVisualStudioVersion.Major; + } + else + { + return this.Version - 1; + } + } + } + + /// + /// Returns true if the solution contains any web projects + /// + internal bool ContainsWebProjects + { + get + { + return _solutionContainsWebProjects; + } + } + + /// + /// Returns true if the solution contains any .wdproj projects. Used to determine + /// whether we need to load up any projects to examine dependencies. + /// + internal bool ContainsWebDeploymentProjects + { + get + { + return _solutionContainsWebDeploymentProjects; + } + } + + /// + /// All projects in this solution, in the order they appeared in the solution file + /// + public IReadOnlyList ProjectsInOrder + { + get + { + return _projectsInOrder.AsReadOnly(); + } + } + + /// + /// The collection of projects in this solution, accessible by their guids as a + /// string in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form + /// + public IReadOnlyDictionary ProjectsByGuid + { + get + { + return new ReadOnlyDictionary(_projects); + } + } + + /// + /// This is the read/write accessor for the solution file which we will parse. This + /// must be set before calling any other methods on this class. + /// + /// + internal string FullPath + { + get + { + return _solutionFile; + } + + set + { + // Should already be canonicalized to a full path + ErrorUtilities.VerifyThrowInternalRooted(value); + _solutionFile = value; + } + } + + internal string SolutionFileDirectory + { + get + { + return _solutionFileDirectory; + } + // This setter is only used by the unit tests + set + { + _solutionFileDirectory = value; + } + } + + /// + /// For unit-testing only. + /// + /// + internal StreamReader SolutionReader + { + get + { + return _reader; + } + + set + { + _reader = value; + } + } + + /// + /// The list of all full solution configurations (configuration + platform) in this solution + /// + public IReadOnlyList SolutionConfigurations + { + get + { + return _solutionConfigurations.AsReadOnly(); + } + } + + #endregion + + #region Methods + + /// + /// This method takes a path to a solution file, parses the projects and project dependencies + /// in the solution file, and creates internal data structures representing the projects within + /// the SLN. + /// + public static SolutionFile Parse(string solutionFile) + { + SolutionFile parser = new SolutionFile(); + parser.FullPath = solutionFile; + + parser.ParseSolutionFile(); + + return parser; + } + + /// + /// Returns "true" if it's a project that's expected to be buildable, or false if it's + /// not (e.g. a solution folder) + /// + /// The project type + /// Whether the project is expected to be buildable + internal static bool IsBuildableProject(ProjectInSolution project) + { + return (project.ProjectType != SolutionProjectType.SolutionFolder && project.ProjectConfigurations.Count > 0); + } + + /// + /// Given a solution file, parses the header and returns the major version numbers of the solution file + /// and the visual studio. + /// Throws InvalidProjectFileException if the solution header is invalid, or if the version is less than + /// our minimum version. + /// + internal static void GetSolutionFileAndVisualStudioMajorVersions(string solutionFile, out int solutionVersion, out int visualStudioMajorVersion) + { + ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(solutionFile), "null solution file passed to GetSolutionFileMajorVersion!"); + ErrorUtilities.VerifyThrowInternalRooted(solutionFile); + + const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version "; + const string slnFileVSVLinePrefix = "VisualStudioVersion"; + FileStream fileStream = null; + StreamReader reader = null; + bool validVersionFound = false; + + solutionVersion = 0; + visualStudioMajorVersion = 0; + + try + { + // Open the file + fileStream = File.OpenRead(solutionFile); + reader = new StreamReader(fileStream, Encoding.Default); // HIGHCHAR: If solution files have no byte-order marks, then assume ANSI rather than ASCII. + + // Read first 4 lines of the solution file. + // The header is expected to be in line 1 or 2 + // VisualStudioVersion is expected to be in line 3 or 4. + for (int i = 0; i < 4; i++) + { + string line = reader.ReadLine(); + + if (line == null) + { + break; + } + + if (line.Trim().StartsWith(slnFileHeaderNoVersion, StringComparison.Ordinal)) + { + // Found it. Validate the version. + string fileVersionFromHeader = line.Substring(slnFileHeaderNoVersion.Length); + + Version version = null; + if (!System.Version.TryParse(fileVersionFromHeader, out version)) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + false /* just throw the exception */, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(solutionFile), + "SolutionParseVersionMismatchError", + slnFileMinUpgradableVersion, + slnFileMaxVersion + ); + } + + solutionVersion = version.Major; + + // Validate against our min & max + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + solutionVersion >= slnFileMinUpgradableVersion, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(solutionFile), + "SolutionParseVersionMismatchError", + slnFileMinUpgradableVersion, + slnFileMaxVersion + ); + + validVersionFound = true; + } + else if (line.Trim().StartsWith(slnFileVSVLinePrefix, StringComparison.Ordinal)) + { + Version visualStudioVersion = ParseVisualStudioVersion(line); + if (visualStudioVersion != null) + { + visualStudioMajorVersion = visualStudioVersion.Major; + } + } + } + } + finally + { + if (fileStream != null) + { + fileStream.Close(); + } + + if (reader != null) + { + reader.Close(); + } + } + + if (validVersionFound) + { + return; + } + + // Didn't find the header in lines 1-4, so the solution file is invalid. + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + false /* just throw the exception */, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(solutionFile), + "SolutionParseNoHeaderError" + ); + + return; /* UNREACHABLE */ + } + + /// + /// Adds a configuration to this solution + /// + internal void AddSolutionConfiguration(string configurationName, string platformName) + { + _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationName, platformName)); + } + + /// + /// Reads a line from the StreamReader, trimming leading and trailing whitespace. + /// + /// + private string ReadLine() + { + ErrorUtilities.VerifyThrow(_reader != null, "ParseFileHeader(): reader is null!"); + + string line = _reader.ReadLine(); + _currentLineNumber++; + + if (line != null) + { + line = line.Trim(); + } + + return line; + } + + /// + /// This method takes a path to a solution file, parses the projects and project dependencies + /// in the solution file, and creates internal data structures representing the projects within + /// the SLN. Used for conversion, which means it allows situations that we refuse to actually build. + /// + internal void ParseSolutionFileForConversion() + { + _parsingForConversionOnly = true; + ParseSolutionFile(); + } + + /// + /// This method takes a path to a solution file, parses the projects and project dependencies + /// in the solution file, and creates internal data structures representing the projects within + /// the SLN. + /// + internal void ParseSolutionFile() + { + ErrorUtilities.VerifyThrow((_solutionFile != null) && (_solutionFile.Length != 0), "ParseSolutionFile() got a null solution file!"); + ErrorUtilities.VerifyThrowInternalRooted(_solutionFile); + + FileStream fileStream = null; + _reader = null; + + try + { + // Open the file + fileStream = File.OpenRead(_solutionFile); + // Store the directory of the file as the current directory may change while we are processes the file + _solutionFileDirectory = Path.GetDirectoryName(_solutionFile); + _reader = new StreamReader(fileStream, Encoding.Default); // HIGHCHAR: If solution files have no byte-order marks, then assume ANSI rather than ASCII. + this.ParseSolution(); + } + catch (Exception e) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(!ExceptionUtilities.IsIoRelatedException(e), new BuildEventFileInfo(_solutionFile), "InvalidProjectFile", e.Message); + throw; + } + finally + { + if (fileStream != null) + { + fileStream.Close(); + } + + if (_reader != null) + { + _reader.Close(); + } + } + } + + /// + /// Parses the SLN file represented by the StreamReader in this.reader, and populates internal + /// data structures based on the SLN file contents. + /// + internal void ParseSolution() + { + _projects = new Dictionary(StringComparer.OrdinalIgnoreCase); + _projectsInOrder = new List(); + _solutionContainsWebProjects = false; + _slnFileActualVersion = 0; + _currentLineNumber = 0; + _solutionConfigurations = new List(); + _defaultConfigurationName = null; + _defaultPlatformName = null; + + // the raw list of project configurations in solution configurations, to be processed after it's fully read in. + Hashtable rawProjectConfigurationsEntries = null; + + ParseFileHeader(); + + string str; + while ((str = ReadLine()) != null) + { + if (str.StartsWith("Project(", StringComparison.Ordinal)) + { + ParseProject(str); + } + else if (str.StartsWith("GlobalSection(NestedProjects)", StringComparison.Ordinal)) + { + ParseNestedProjects(); + } + else if (str.StartsWith("GlobalSection(SolutionConfigurationPlatforms)", StringComparison.Ordinal)) + { + ParseSolutionConfigurations(); + } + else if (str.StartsWith("GlobalSection(ProjectConfigurationPlatforms)", StringComparison.Ordinal)) + { + rawProjectConfigurationsEntries = ParseProjectConfigurations(); + } + else if (str.StartsWith("VisualStudioVersion", StringComparison.Ordinal)) + { + _currentVisualStudioVersion = ParseVisualStudioVersion(str); + } + else + { + // No other section types to process at this point, so just ignore the line + // and continue. + } + } + + if (rawProjectConfigurationsEntries != null) + { + ProcessProjectConfigurationSection(rawProjectConfigurationsEntries); + } + + // Cache the unique name of each project, and check that we don't have any duplicates. + Hashtable projectsByUniqueName = new Hashtable(StringComparer.OrdinalIgnoreCase); + + foreach (ProjectInSolution proj in _projectsInOrder) + { + // Find the unique name for the project. This method also caches the unique name, + // so it doesn't have to be recomputed later. + string uniqueName = proj.GetUniqueProjectName(); + + if (proj.ProjectType == SolutionProjectType.WebProject) + { + // Examine port information and determine if we need to disambiguate similarly-named projects with different ports. + Uri uri; + if (Uri.TryCreate(proj.RelativePath, UriKind.Absolute, out uri)) + { + if (!uri.IsDefaultPort) + { + // If there are no other projects with the same name as this one, then we will keep this project's unique name, otherwise + // we will create a new unique name with the port added. + foreach (ProjectInSolution otherProj in _projectsInOrder) + { + if (Object.ReferenceEquals(proj, otherProj)) + { + continue; + } + + if (String.Equals(otherProj.ProjectName, proj.ProjectName, StringComparison.OrdinalIgnoreCase)) + { + uniqueName = String.Format(CultureInfo.InvariantCulture, "{0}:{1}", uniqueName, uri.Port); + proj.UpdateUniqueProjectName(uniqueName); + break; + } + } + } + } + } + + // Throw an error if there are any duplicates + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile( + projectsByUniqueName[uniqueName] == null, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath), + "SolutionParseDuplicateProject", + uniqueName); + + // Update the hash table with this unique name + projectsByUniqueName[uniqueName] = proj; + } + } // ParseSolutionFile() + + /// + /// This method searches the first two lines of the solution file opened by the specified + /// StreamReader for the solution file header. An exception is thrown if it is not found. + /// + /// The solution file header looks like this: + /// + /// Microsoft Visual Studio Solution File, Format Version 9.00 + /// + /// + private void ParseFileHeader() + { + ErrorUtilities.VerifyThrow(_reader != null, "ParseFileHeader(): reader is null!"); + + const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version "; + + // Read the file header. This can be on either of the first two lines. + for (int i = 1; i <= 2; i++) + { + string str = ReadLine(); + if (str == null) + { + break; + } + + if (str.StartsWith(slnFileHeaderNoVersion, StringComparison.Ordinal)) + { + // Found it. Validate the version. + ValidateSolutionFileVersion(str.Substring(slnFileHeaderNoVersion.Length)); + return; + } + } + + // Didn't find the header on either the first or second line, so the solution file + // is invalid. + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath), "SolutionParseNoHeaderError"); + } + + /// + /// This method parses the Visual Studio version in Dev 12 solution files + /// The version line looks like this: + /// + /// VisualStudioVersion = 12.0.20311.0 VSPRO_PLATFORM + /// + /// If such a line is found, the version is stored in this.currentVisualStudioVersion + /// + private static Version ParseVisualStudioVersion(string str) + { + Version currentVisualStudioVersion = null; + char[] delimiterChars = { ' ', '=' }; + string[] words = str.Split(delimiterChars, StringSplitOptions.RemoveEmptyEntries); + + if (words.Length >= 2) + { + string versionStr = words[1]; + if (!System.Version.TryParse(versionStr, out currentVisualStudioVersion)) + { + currentVisualStudioVersion = null; + } + } + + return currentVisualStudioVersion; + } + /// + /// This method extracts the whole part of the version number from the specified line + /// containing the solution file format header, and throws an exception if the version number + /// is outside of the valid range. + /// + /// The solution file header looks like this: + /// + /// Microsoft Visual Studio Solution File, Format Version 9.00 + /// + /// + /// + private void ValidateSolutionFileVersion(string versionString) + { + ErrorUtilities.VerifyThrow(versionString != null, "ValidateSolutionFileVersion() got a null line!"); + + Version version = null; + + if (!System.Version.TryParse(versionString, out version)) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseVersionMismatchError", + slnFileMinUpgradableVersion, slnFileMaxVersion); + } + + _slnFileActualVersion = version.Major; + + // Validate against our min & max + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile( + _slnFileActualVersion >= slnFileMinUpgradableVersion, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), + "SolutionParseVersionMismatchError", + slnFileMinUpgradableVersion, slnFileMaxVersion); + // If the solution file version is greater than the maximum one we will create a comment rather than warn + // as users such as blend opening a dev10 project cannot do anything about it. + if (_slnFileActualVersion > slnFileMaxVersion) + { + _solutionParserComments.Add(ResourceUtilities.FormatResourceString("UnrecognizedSolutionComment", _slnFileActualVersion)); + } + } + + /// + /// + /// This method processes a "Project" section in the solution file opened by the specified + /// StreamReader, and returns a populated ProjectInSolution instance, if successful. + /// An exception is thrown if the solution file is invalid. + /// + /// The format of the parts of a Project section that we care about is as follows: + /// + /// Project("{Project type GUID}") = "Project name", "Relative path to project file", "{Project GUID}" + /// ProjectSection(ProjectDependencies) = postProject + /// {Parent project unique name} = {Parent project unique name} + /// ... + /// EndProjectSection + /// EndProject + /// + /// + /// + /// + private void ParseProject(string firstLine) + { + ErrorUtilities.VerifyThrow((firstLine != null) && (firstLine.Length != 0), "ParseProject() got a null firstLine!"); + ErrorUtilities.VerifyThrow(_reader != null, "ParseProject() got a null reader!"); + + ProjectInSolution proj = new ProjectInSolution(this); + + // Extract the important information from the first line. + ParseFirstProjectLine(firstLine, proj); + + // Search for project dependencies. Keeping reading lines until we either 1.) reach + // the end of the file, 2.) see "ProjectSection(ProjectDependencies)" at the beginning + // of the line, or 3.) see "EndProject" at the beginning of the line. + string line; + while ((line = ReadLine()) != null) + { + // If we see an "EndProject", well ... that's the end of this project! + if (line == "EndProject") + { + break; + } + else if (line.StartsWith("ProjectSection(ProjectDependencies)", StringComparison.Ordinal)) + { + // We have a ProjectDependencies section. Each subsequent line should identify + // a dependency. + line = ReadLine(); + while ((line != null) && (!line.StartsWith("EndProjectSection", StringComparison.Ordinal))) + { + // This should be a dependency. The GUID identifying the parent project should + // be both the property name and the property value. + Match match = s_crackPropertyLine.Match(line); + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseProjectDepGuidError", proj.ProjectName); + + string parentGuid = match.Groups["PROPERTYNAME"].Value.Trim(); + proj.AddDependency(parentGuid); + + line = ReadLine(); + } + } + else if (line.StartsWith("ProjectSection(WebsiteProperties)", StringComparison.Ordinal)) + { + // We have a WebsiteProperties section. This section is present only in Venus + // projects, and contains properties that we'll need in order to call the + // AspNetCompiler task. + line = ReadLine(); + while ((line != null) && (!line.StartsWith("EndProjectSection", StringComparison.Ordinal))) + { + Match match = s_crackPropertyLine.Match(line); + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseWebProjectPropertiesError", proj.ProjectName); + + string propertyName = match.Groups["PROPERTYNAME"].Value.Trim(); + string propertyValue = match.Groups["PROPERTYVALUE"].Value.Trim(); + + ParseAspNetCompilerProperty(proj, propertyName, propertyValue); + + line = ReadLine(); + } + } + } + + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(line != null, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath), "SolutionParseProjectEofError", proj.ProjectName); + + if (proj != null) + { + // Add the project to the collection + AddProjectToSolution(proj); + // If the project is an etp project then parse the etp project file + // to get the projects contained in it. + if (IsEtpProjectFile(proj.RelativePath)) + { + ParseEtpProject(proj); + } + } + } // ParseProject() + + /// + /// This method will parse a .etp project recursively and + /// add all the projects found to projects and projectsInOrder + /// + /// ETP Project + internal void ParseEtpProject(ProjectInSolution etpProj) + { + XmlDocument etpProjectDocument = new XmlDocument(); + // Get the full path to the .etp project file + string fullPathToEtpProj = Path.Combine(_solutionFileDirectory, etpProj.RelativePath); + string etpProjectRelativeDir = Path.GetDirectoryName(etpProj.RelativePath); + try + { + /**************************************************************************** + * A Typical .etp project file will look like this + * + * + * + * Microsoft Visual Studio Application Template File + * 1.00 + * + * + * ClassLibrary2\ClassLibrary2.csproj + * + * + * + * + * ClassLibrary2\ClassLibrary2.csproj + * {73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE} + * + * + * + * + **********************************************************************************/ + // Make sure the XML reader ignores DTD processing + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.DtdProcessing = DtdProcessing.Ignore; + + // Load the .etp project file thru the XML reader + using (XmlReader xmlReader = XmlReader.Create(fullPathToEtpProj, readerSettings)) + { + etpProjectDocument.Load(xmlReader); + } + + // We need to parse the .etp project file to get the names of projects contained + // in the .etp Project. The projects are listed under /EFPROJECT/GENERAL/References/Reference node in the .etp project file. + // The /EFPROJECT/GENERAL/Views/ProjectExplorer node will not necessarily contain + // all the projects in the .etp project. Therefore, we need to look at + // /EFPROJECT/GENERAL/References/Reference. + // Find the /EFPROJECT/GENERAL/References/Reference node + // Note that this is case sensitive + XmlNodeList referenceNodes = etpProjectDocument.DocumentElement.SelectNodes("/EFPROJECT/GENERAL/References/Reference"); + // Do the right thing for each element + foreach (XmlNode referenceNode in referenceNodes) + { + // Get the relative path to the project file + string fileElementValue = referenceNode.SelectSingleNode("FILE").InnerText; + // If element is not present under then we don't do anything. + if (fileElementValue != null) + { + // Create and populate a ProjectInSolution for the project + ProjectInSolution proj = new ProjectInSolution(this); + proj.RelativePath = Path.Combine(etpProjectRelativeDir, fileElementValue); + + // Verify the relative path specified in the .etp proj file + ValidateProjectRelativePath(proj); + proj.ProjectType = SolutionProjectType.EtpSubProject; + proj.ProjectName = proj.RelativePath; + XmlNode projGuidNode = referenceNode.SelectSingleNode("GUIDPROJECTID"); + if (projGuidNode != null) + { + proj.ProjectGuid = projGuidNode.InnerText; + } + // It is ok for a project to not have a guid inside an etp project. + // If a solution file contains a project without a guid it fails to + // load in Everett. But if an etp project contains a project without + // a guid it loads well in Everett and p2p references to/from this project + // are preserved. So we should make sure that we don’t error in this + // situation while upgrading. + else + { + proj.ProjectGuid = String.Empty; + } + // Add the recently created proj to the collection of projects + AddProjectToSolution(proj); + // If the project is an etp project recurse + if (IsEtpProjectFile(fileElementValue)) + { + ParseEtpProject(proj); + } + } + } + } + // catch all sorts of exceptions - if we encounter any problems here, we just assume the .etp project file is not in the correct format + + // handle security errors + catch (SecurityException e) + { + // Log a warning + string errorCode, ignoredKeyword; + string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", + etpProj.RelativePath, e.Message); + _solutionParserWarnings.Add(warning); + _solutionParserErrorCodes.Add(errorCode); + } + // handle errors in path resolution + catch (NotSupportedException e) + { + // Log a warning + string errorCode, ignoredKeyword; + string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", + etpProj.RelativePath, e.Message); + _solutionParserWarnings.Add(warning); + _solutionParserErrorCodes.Add(errorCode); + } + // handle errors in loading project file + catch (IOException e) + { + // Log a warning + string errorCode, ignoredKeyword; + string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", + etpProj.RelativePath, e.Message); + _solutionParserWarnings.Add(warning); + _solutionParserErrorCodes.Add(errorCode); + } + // handle errors in loading project file + catch (UnauthorizedAccessException e) + { + // Log a warning + string errorCode, ignoredKeyword; + string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", + etpProj.RelativePath, e.Message); + _solutionParserWarnings.Add(warning); + _solutionParserErrorCodes.Add(errorCode); + } + // handle XML parsing errors + catch (XmlException e) + { + // Log a warning + string errorCode, ignoredKeyword; + string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.InvalidProjectFile", + etpProj.RelativePath, e.Message); + _solutionParserWarnings.Add(warning); + _solutionParserErrorCodes.Add(errorCode); + } + } + + /// + /// Adds a given project to the project collections of this class + /// + /// proj + private void AddProjectToSolution(ProjectInSolution proj) + { + if (!String.IsNullOrEmpty(proj.ProjectGuid)) + { + _projects[proj.ProjectGuid] = proj; + } + _projectsInOrder.Add(proj); + } + + /// + /// Checks whether a given project has a .etp extension. + /// + /// + private bool IsEtpProjectFile(string projectFile) + { + return projectFile.EndsWith(".etp", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Validate relative path of a project + /// + /// proj + private void ValidateProjectRelativePath(ProjectInSolution proj) + { + // Verify the relative path is not null + ErrorUtilities.VerifyThrow(proj.RelativePath != null, "Project relative path cannot be null."); + + // Verify the relative path does not contain invalid characters + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.IndexOfAny(Path.GetInvalidPathChars()) == -1, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), + "SolutionParseInvalidProjectFileNameCharacters", + proj.ProjectName, proj.RelativePath); + + // Verify the relative path is not empty string + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.Length > 0, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), + "SolutionParseInvalidProjectFileNameEmpty", + proj.ProjectName); + } + + /// + /// Takes a property name / value that comes from the SLN file for a Venus project, and + /// stores it appropriately in our data structures. + /// + /// + /// + /// + private void ParseAspNetCompilerProperty + ( + ProjectInSolution proj, + string propertyName, + string propertyValue + ) + { + // What we expect to find in the SLN file is something that looks like this: + // + // Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "c:\...\myfirstwebsite\", "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite", "{956CC04E-FD59-49A9-9099-96888CB6F366}" + // ProjectSection(WebsiteProperties) = preProject + // TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + // ProjectReferences = "{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSClassLibrary1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;" + // Debug.AspNetCompiler.VirtualPath = "/publishfirst" + // Debug.AspNetCompiler.PhysicalPath = "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite\" + // Debug.AspNetCompiler.TargetPath = "..\..\..\..\..\..\rajeev\temp\publishfirst\" + // Debug.AspNetCompiler.ForceOverwrite = "true" + // Debug.AspNetCompiler.Updateable = "true" + // Debug.AspNetCompiler.Enabled = "true" + // Debug.AspNetCompiler.Debug = "true" + // Debug.AspNetCompiler.KeyFile = "" + // Debug.AspNetCompiler.KeyContainer = "" + // Debug.AspNetCompiler.DelaySign = "true" + // Debug.AspNetCompiler.AllowPartiallyTrustedCallers = "true" + // Debug.AspNetCompiler.FixedNames = "true" + // Release.AspNetCompiler.VirtualPath = "/publishfirst" + // Release.AspNetCompiler.PhysicalPath = "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite\" + // Release.AspNetCompiler.TargetPath = "..\..\..\..\..\..\rajeev\temp\publishfirst\" + // Release.AspNetCompiler.ForceOverwrite = "true" + // Release.AspNetCompiler.Updateable = "true" + // Release.AspNetCompiler.Enabled = "true" + // Release.AspNetCompiler.Debug = "false" + // Release.AspNetCompiler.KeyFile = "" + // Release.AspNetCompiler.KeyContainer = "" + // Release.AspNetCompiler.DelaySign = "true" + // Release.AspNetCompiler.AllowPartiallyTrustedCallers = "true" + // Release.AspNetCompiler.FixedNames = "true" + // EndProjectSection + // EndProject + // + // This method is responsible for parsing each of the lines within the "WebsiteProperties" section. + // The first component of each property name is actually the configuration for which that + // property applies. + + int indexOfFirstDot = propertyName.IndexOf('.'); + if (indexOfFirstDot != -1) + { + // The portion before the first dot is the configuration name. + string configurationName = propertyName.Substring(0, indexOfFirstDot); + + // The rest of it is the actual property name. + string aspNetPropertyName = ((propertyName.Length - indexOfFirstDot) > 0) ? propertyName.Substring(indexOfFirstDot + 1, propertyName.Length - indexOfFirstDot - 1) : ""; + + // And the part after the sign is the property value (which was parsed out for us prior + // to calling this method). + propertyValue = TrimQuotes(propertyValue); + + // Grab the parameters for this specific configuration if they exist. + object aspNetCompilerParametersObject = proj.AspNetConfigurations[configurationName]; + AspNetCompilerParameters aspNetCompilerParameters; + + if (aspNetCompilerParametersObject == null) + { + // If it didn't exist, create a new one. + aspNetCompilerParameters = new AspNetCompilerParameters(); + aspNetCompilerParameters.aspNetVirtualPath = String.Empty; + aspNetCompilerParameters.aspNetPhysicalPath = String.Empty; + aspNetCompilerParameters.aspNetTargetPath = String.Empty; + aspNetCompilerParameters.aspNetForce = String.Empty; + aspNetCompilerParameters.aspNetUpdateable = String.Empty; + aspNetCompilerParameters.aspNetDebug = String.Empty; + aspNetCompilerParameters.aspNetKeyFile = String.Empty; + aspNetCompilerParameters.aspNetKeyContainer = String.Empty; + aspNetCompilerParameters.aspNetDelaySign = String.Empty; + aspNetCompilerParameters.aspNetAPTCA = String.Empty; + aspNetCompilerParameters.aspNetFixedNames = String.Empty; + } + else + { + // Otherwise just unbox it. + aspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParametersObject; + } + + // Update the appropriate field within the parameters struct. + if (aspNetPropertyName == "AspNetCompiler.VirtualPath") + { + aspNetCompilerParameters.aspNetVirtualPath = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.PhysicalPath") + { + aspNetCompilerParameters.aspNetPhysicalPath = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.TargetPath") + { + aspNetCompilerParameters.aspNetTargetPath = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.ForceOverwrite") + { + aspNetCompilerParameters.aspNetForce = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.Updateable") + { + aspNetCompilerParameters.aspNetUpdateable = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.Debug") + { + aspNetCompilerParameters.aspNetDebug = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.KeyFile") + { + aspNetCompilerParameters.aspNetKeyFile = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.KeyContainer") + { + aspNetCompilerParameters.aspNetKeyContainer = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.DelaySign") + { + aspNetCompilerParameters.aspNetDelaySign = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.AllowPartiallyTrustedCallers") + { + aspNetCompilerParameters.aspNetAPTCA = propertyValue; + } + else if (aspNetPropertyName == "AspNetCompiler.FixedNames") + { + aspNetCompilerParameters.aspNetFixedNames = propertyValue; + } + + // Store the updated parameters struct back into the hashtable by configuration name. + proj.AspNetConfigurations[configurationName] = aspNetCompilerParameters; + } + else + { + // ProjectReferences = "{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSClassLibrary1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;" + if (string.Compare(propertyName, "ProjectReferences", StringComparison.OrdinalIgnoreCase) == 0) + { + string[] projectReferenceEntries = propertyValue.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string projectReferenceEntry in projectReferenceEntries) + { + int indexOfBar = projectReferenceEntry.IndexOf('|'); + + // indexOfBar could be -1 if we had semicolons in the file names, so skip entries that + // don't contain a guid. File names may not contain the '|' character + if (indexOfBar != -1) + { + int indexOfOpeningBrace = projectReferenceEntry.IndexOf('{'); + if (indexOfOpeningBrace != -1) + { + int indexOfClosingBrace = projectReferenceEntry.IndexOf('}', indexOfOpeningBrace); + if (indexOfClosingBrace != -1) + { + string referencedProjectGuid = projectReferenceEntry.Substring(indexOfOpeningBrace, + indexOfClosingBrace - indexOfOpeningBrace + 1); + + proj.AddDependency(referencedProjectGuid); + proj.ProjectReferences.Add(referencedProjectGuid); + } + } + } + } + } + else if (String.Compare(propertyName, "TargetFrameworkMoniker", StringComparison.OrdinalIgnoreCase) == 0) + { + //Website project need to back support 3.5 msbuild parser for the Blend (it is not move to .Net4.0 yet.) + //However, 3.5 version of Solution parser can't handle a equal sign in the value. + //The "=" in targetframeworkMoniker was escaped to "%3D" for Orcas + string targetFrameworkMoniker = TrimQuotes(propertyValue); + proj.TargetFrameworkMoniker = Microsoft.Build.Shared.EscapingUtilities.UnescapeAll(targetFrameworkMoniker); + } + } + } + + /// + /// Strips a single pair of leading/trailing double-quotes from a string. + /// + /// + /// + private string TrimQuotes + ( + string property + ) + { + // If the incoming string starts and ends with a double-quote, strip the double-quotes. + if ((property != null) && (property.Length > 0) && (property[0] == '"') && (property[property.Length - 1] == '"')) + { + return property.Substring(1, property.Length - 2); + } + else + { + return property; + } + } + + /// + /// Parse the first line of a Project section of a solution file. This line should look like: + /// + /// Project("{Project type GUID}") = "Project name", "Relative path to project file", "{Project GUID}" + /// + /// + /// + /// + internal void ParseFirstProjectLine + ( + string firstLine, + ProjectInSolution proj + ) + { + Match match = s_crackProjectLine.Match(firstLine); + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseProjectError"); + + string projectTypeGuid = match.Groups["PROJECTTYPEGUID"].Value.Trim(); + proj.ProjectName = match.Groups["PROJECTNAME"].Value.Trim(); + proj.RelativePath = match.Groups["RELATIVEPATH"].Value.Trim(); + proj.ProjectGuid = match.Groups["PROJECTGUID"].Value.Trim(); + + // If the project name is empty (as in some bad solutions) set it to some generated generic value. + // This allows us to at least generate reasonable target names etc. instead of crashing. + if (String.IsNullOrEmpty(proj.ProjectName)) + { + proj.ProjectName = "EmptyProjectName." + Guid.NewGuid(); + } + + // Validate project relative path + ValidateProjectRelativePath(proj); + + // Figure out what type of project this is. + if ((String.Compare(projectTypeGuid, vbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(projectTypeGuid, csProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(projectTypeGuid, fsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(projectTypeGuid, dbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) || + (String.Compare(projectTypeGuid, vjProjectGuid, StringComparison.OrdinalIgnoreCase) == 0)) + { + proj.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat; + } + else if (String.Compare(projectTypeGuid, solutionFolderGuid, StringComparison.OrdinalIgnoreCase) == 0) + { + proj.ProjectType = SolutionProjectType.SolutionFolder; + } + // MSBuild format VC projects have the same project type guid as old style VC projects. + // If it's not an old-style VC project, we'll assume it's MSBuild format + else if (String.Compare(projectTypeGuid, vcProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) + { + if (String.Equals(proj.Extension, ".vcproj", StringComparison.OrdinalIgnoreCase)) + { + if (!_parsingForConversionOnly) + { + ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(FullPath), "ProjectUpgradeNeededToVcxProj", proj.RelativePath); + } + // otherwise, we're parsing this solution file because we want the P2P information during + // conversion, and it's perfectly valid for an unconverted solution file to still contain .vcprojs + } + else + { + proj.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat; + } + } + else if (String.Compare(projectTypeGuid, webProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) + { + proj.ProjectType = SolutionProjectType.WebProject; + _solutionContainsWebProjects = true; + } + else if (String.Compare(projectTypeGuid, wdProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) + { + proj.ProjectType = SolutionProjectType.WebDeploymentProject; + _solutionContainsWebDeploymentProjects = true; + } + else + { + proj.ProjectType = SolutionProjectType.Unknown; + } + } + + /// + /// Read nested projects section. + /// This is required to find a unique name for each project's target + /// + internal void ParseNestedProjects() + { + string str; + + do + { + str = ReadLine(); + if ((str == null) || (str == "EndGlobalSection")) + { + break; + } + + Match match = s_crackPropertyLine.Match(str); + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectError"); + + string projectGuid = match.Groups["PROPERTYNAME"].Value.Trim(); + string parentProjectGuid = match.Groups["PROPERTYVALUE"].Value.Trim(); + + ProjectInSolution proj; + if (!_projects.TryGetValue(projectGuid, out proj)) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectUndefinedError", projectGuid, parentProjectGuid); + } + + proj.ParentProjectGuid = parentProjectGuid; + } while (true); + } + + /// + /// Read solution configuration section. + /// + /// + /// A sample section: + /// + /// GlobalSection(SolutionConfigurationPlatforms) = preSolution + /// Debug|Any CPU = Debug|Any CPU + /// Release|Any CPU = Release|Any CPU + /// EndGlobalSection + /// + internal void ParseSolutionConfigurations() + { + string str; + char[] nameValueSeparators = new char[] { '=' }; + char[] configPlatformSeparators = new char[] { SolutionConfigurationInSolution.ConfigurationPlatformSeparator }; + + do + { + str = ReadLine(); + + if ((str == null) || (str == "EndGlobalSection")) + { + break; + } + + string[] configurationNames = str.Split(nameValueSeparators); + + // There should be exactly one '=' character, separating two names. + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationNames.Length == 2, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str); + + string fullConfigurationName = configurationNames[0].Trim(); + + //Fixing bug 555577: Solution file can have description information, in which case we ignore. + if (0 == String.Compare(fullConfigurationName, "DESCRIPTION", StringComparison.OrdinalIgnoreCase)) + continue; + + // Both names must be identical + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(fullConfigurationName == configurationNames[1].Trim(), "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str); + + string[] configurationPlatformParts = fullConfigurationName.Split(configPlatformSeparators); + + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationPlatformParts.Length == 2, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str); + + _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationPlatformParts[0], configurationPlatformParts[1])); + } while (true); + } + + /// + /// Read project configurations in solution configurations section. + /// + /// + /// A sample (incomplete) section: + /// + /// GlobalSection(ProjectConfigurationPlatforms) = postSolution + /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU + /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU + /// {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU + /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32 + /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32 + /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32 + /// {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32 + /// EndGlobalSection + /// + /// An unprocessed hashtable of entries in this section + internal Hashtable ParseProjectConfigurations() + { + Hashtable rawProjectConfigurationsEntries = new Hashtable(StringComparer.OrdinalIgnoreCase); + string str; + + do + { + str = ReadLine(); + + if ((str == null) || (str == "EndGlobalSection")) + { + break; + } + + string[] nameValue = str.Split(new char[] { '=' }); + + // There should be exactly one '=' character, separating the name and value. + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(nameValue.Length == 2, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidProjectSolutionConfigurationEntry", str); + + rawProjectConfigurationsEntries[nameValue[0].Trim()] = nameValue[1].Trim(); + } while (true); + + return rawProjectConfigurationsEntries; + } + + /// + /// Read the project configuration information for every project in the solution, using pre-cached + /// solution section data. + /// + /// Cached data from the project configuration section + internal void ProcessProjectConfigurationSection(Hashtable rawProjectConfigurationsEntries) + { + // Instead of parsing the data line by line, we parse it project by project, constructing the + // entry name (e.g. "{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg") and retrieving its + // value from the raw data. The reason for this is that the IDE does it this way, and as the result + // the '.' character is allowed in configuration names although it technically separates different + // parts of the entry name string. This could lead to ambiguous results if we tried to parse + // the entry name instead of constructing it and looking it up. Although it's pretty unlikely that + // this would ever be a problem, it's safer to do it the same way VS IDE does it. + char[] configPlatformSeparators = new char[] { SolutionConfigurationInSolution.ConfigurationPlatformSeparator }; + + foreach (ProjectInSolution project in _projectsInOrder) + { + // Solution folders don't have configurations + if (project.ProjectType != SolutionProjectType.SolutionFolder) + { + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionConfigurations) + { + // The "ActiveCfg" entry defines the active project configuration in the given solution configuration + // This entry must be present for every possible solution configuration/project combination. + string entryNameActiveConfig = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.ActiveCfg", + project.ProjectGuid, solutionConfiguration.FullName); + + // The "Build.0" entry tells us whether to build the project configuration in the given solution configuration. + // Technically, it specifies a configuration name of its own which seems to be a remnant of an initial, + // more flexible design of solution configurations (as well as the '.0' suffix - no higher values are ever used). + // The configuration name is not used, and the whole entry means "build the project configuration" + // if it's present in the solution file, and "don't build" if it's not. + string entryNameBuild = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.Build.0", + project.ProjectGuid, solutionConfiguration.FullName); + + if (rawProjectConfigurationsEntries.ContainsKey(entryNameActiveConfig)) + { + string[] configurationPlatformParts = ((string)(rawProjectConfigurationsEntries[entryNameActiveConfig])).Split(configPlatformSeparators); + + // Project configuration may not necessarily contain the platform part. Some project support only the configuration part. + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationPlatformParts.Length <= 2, "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(FullPath), "SolutionParseInvalidProjectSolutionConfigurationEntry", + string.Format(CultureInfo.InvariantCulture, "{0} = {1}", entryNameActiveConfig, rawProjectConfigurationsEntries[entryNameActiveConfig])); + + ProjectConfigurationInSolution projectConfiguration = new ProjectConfigurationInSolution( + configurationPlatformParts[0], + (configurationPlatformParts.Length > 1) ? configurationPlatformParts[1] : string.Empty, + rawProjectConfigurationsEntries.ContainsKey(entryNameBuild) + ); + + project.SetProjectConfiguration(solutionConfiguration.FullName, projectConfiguration); + } + } + } + } + } + + /// + /// Gets the default configuration name for this solution. Usually it's Debug, unless it's not present + /// in which case it's the first configuration name we find. + /// + public string GetDefaultConfigurationName() + { + // Have we done this already? Return the cached name + if (_defaultConfigurationName != null) + { + return _defaultConfigurationName; + } + + _defaultConfigurationName = string.Empty; + + // Pick the Debug configuration as default if present + foreach (SolutionConfigurationInSolution solutionConfiguration in this.SolutionConfigurations) + { + if (string.Compare(solutionConfiguration.ConfigurationName, "Debug", StringComparison.OrdinalIgnoreCase) == 0) + { + _defaultConfigurationName = solutionConfiguration.ConfigurationName; + break; + } + } + + // Failing that, just pick the first configuration name as default + if ((_defaultConfigurationName.Length == 0) && (this.SolutionConfigurations.Count > 0)) + { + _defaultConfigurationName = this.SolutionConfigurations[0].ConfigurationName; + } + + return _defaultConfigurationName; + } + + /// + /// Gets the default platform name for this solution. Usually it's Mixed Platforms, unless it's not present + /// in which case it's the first platform name we find. + /// + public string GetDefaultPlatformName() + { + // Have we done this already? Return the cached name + if (_defaultPlatformName != null) + { + return _defaultPlatformName; + } + + _defaultPlatformName = string.Empty; + + // Pick the Mixed Platforms platform as default if present + foreach (SolutionConfigurationInSolution solutionConfiguration in this.SolutionConfigurations) + { + if (string.Compare(solutionConfiguration.PlatformName, "Mixed Platforms", StringComparison.OrdinalIgnoreCase) == 0) + { + _defaultPlatformName = solutionConfiguration.PlatformName; + break; + } + // We would like this to be chosen if Mixed platforms does not exist. + else if (string.Compare(solutionConfiguration.PlatformName, "Any CPU", StringComparison.OrdinalIgnoreCase) == 0) + { + _defaultPlatformName = solutionConfiguration.PlatformName; + } + } + + // Failing that, just pick the first platform name as default + if ((_defaultPlatformName.Length == 0) && (this.SolutionConfigurations.Count > 0)) + { + _defaultPlatformName = this.SolutionConfigurations[0].PlatformName; + } + + return _defaultPlatformName; + } + + /// + /// This method takes a string representing one of the project's unique names (guid), and + /// returns the corresponding "friendly" name for this project. + /// + /// + /// + internal string GetProjectUniqueNameByGuid(string projectGuid) + { + ProjectInSolution proj; + if (_projects.TryGetValue(projectGuid, out proj)) + { + return proj.GetUniqueProjectName(); + } + + return null; + } + + /// + /// This method takes a string representing one of the project's unique names (guid), and + /// returns the corresponding relative path to this project. + /// + /// + /// + internal string GetProjectRelativePathByGuid(string projectGuid) + { + ProjectInSolution proj; + if (_projects.TryGetValue(projectGuid, out proj)) + { + return proj.RelativePath; + } + + return null; + } + + #endregion + } // class SolutionParser +} // namespace Microsoft.Build.Construction diff --git a/src/XMakeBuildEngine/Construction/Solution/SolutionProjectGenerator.cs b/src/XMakeBuildEngine/Construction/Solution/SolutionProjectGenerator.cs new file mode 100644 index 00000000000..5d65603558c --- /dev/null +++ b/src/XMakeBuildEngine/Construction/Solution/SolutionProjectGenerator.cs @@ -0,0 +1,2281 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Converts a solution file to a set of project instances which can be built. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using Project = Microsoft.Build.Evaluation.Project; +using ProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; +using ProjectItem = Microsoft.Build.Evaluation.ProjectItem; +using IProperty = Microsoft.Build.Evaluation.IProperty; + +using Constants = Microsoft.Build.Internal.Constants; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; + +using FrameworkName = System.Runtime.Versioning.FrameworkName; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.Construction +{ + /// + /// This class is used to generate an MSBuild wrapper project for a solution file. + /// + internal class SolutionProjectGenerator + { + #region Private Fields + + /// + /// The path node to add in when the output directory for a website is overridden. + /// + private const string WebProjectOverrideFolder = "_PublishedWebsites"; + + /// + /// The set of properties all projects in the solution should be built with + /// + private const string SolutionProperties = "BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)"; + + /// + /// The set of properties which identify the configuration and platform to build a project with + /// + private const string SolutionConfigurationAndPlatformProperties = "Configuration=$(Configuration); Platform=$(Platform)"; + + /// + /// Version 2.0 + /// + private readonly Version _version20 = new Version(2, 0); + + /// + /// Version 4.0 + /// + private readonly Version _version40 = new Version(4, 0); + + /// + /// The list of global properties we set on each metaproject and which get passed to each project when building. + /// + private Tuple[] _metaprojectGlobalProperties = new Tuple[] + { + new Tuple("Configuration", null), // This is the solution configuration in a metaproject, and project configuration on an actual project + new Tuple("Platform", null), // This is the solution platform in a metaproject, and project platform on an actual project + new Tuple("BuildingSolutionFile", "true"), + new Tuple("CurrentSolutionConfigurationContents", null), + new Tuple("SolutionDir", null), + new Tuple("SolutionExt", null), + new Tuple("SolutionFileName", null), + new Tuple("SolutionName", null), + new Tuple("SolutionPath", null) + }; + + /// + /// The SolutionFile containing information about the solution we're generating a wrapper for. + /// + private SolutionFile _solutionFile; + + /// + /// The global properties passed under which the project should be opened. + /// + private IDictionary _globalProperties; + + /// + /// The ToolsVersion passed on the commandline, if any. May be null. + /// + private string _toolsVersionOverride; + + /// + /// The context of this build (used for logging purposes). + /// + private BuildEventContext _projectBuildEventContext; + + /// + /// The LoggingService used to log messages. + /// + private ILoggingService _loggingService; + + /// + /// The solution configuration selected for this build. + /// + private string _selectedSolutionConfiguration; + + #endregion // Private Fields + + #region Constructors + + /// + /// Constructor. + /// + private SolutionProjectGenerator + ( + SolutionFile solution, + IDictionary globalProperties, + string toolsVersionOverride, + BuildEventContext projectBuildEventContext, + ILoggingService loggingService + ) + { + _solutionFile = solution; + _globalProperties = globalProperties ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + _toolsVersionOverride = toolsVersionOverride; + _projectBuildEventContext = projectBuildEventContext; + _loggingService = loggingService; + } + + #endregion // Constructors + + #region Methods + + /// + /// This method generates an MSBuild project file from the list of projects and project dependencies + /// that have been collected from the solution file. + /// + /// The parser which contains the solution file. + /// The global properties. + /// Tools Version override (may be null). This should be any tools version explicitly passed to the command-line or from an MSBuild ToolsVersion parameter. + /// The logging context for this project. + /// The logging service. + /// An array of ProjectInstances. The first instance is the traversal project, the remaining are the metaprojects for each project referenced in the solution. + internal static ProjectInstance[] Generate + ( + SolutionFile solution, + IDictionary globalProperties, + string toolsVersionOverride, + BuildEventContext projectBuildEventContext, + ILoggingService loggingService + ) + { + SolutionProjectGenerator projectGenerator = new SolutionProjectGenerator + ( + solution, + globalProperties, + toolsVersionOverride, + projectBuildEventContext, + loggingService + ); + + return projectGenerator.Generate(); + } + + /// + /// Adds a new property group with contents of the given solution configuration to the project + /// Internal for unit-testing. + /// + internal static void AddPropertyGroupForSolutionConfiguration(ProjectRootElement msbuildProject, SolutionFile solutionFile, SolutionConfigurationInSolution solutionConfiguration) + { + ProjectPropertyGroupElement solutionConfigurationProperties = msbuildProject.CreatePropertyGroupElement(); + msbuildProject.AppendChild(solutionConfigurationProperties); + solutionConfigurationProperties.Condition = GetConditionStringForConfiguration(solutionConfiguration); + + StringBuilder solutionConfigurationContents = new StringBuilder(1024); + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.OmitXmlDeclaration = true; + using (XmlWriter xw = XmlWriter.Create(solutionConfigurationContents, settings)) + { + xw.WriteStartElement("SolutionConfiguration"); + + // add a project configuration entry for each project in the solution + foreach (ProjectInSolution project in solutionFile.ProjectsInOrder) + { + ProjectConfigurationInSolution projectConfiguration = null; + + if (project.ProjectConfigurations.TryGetValue(solutionConfiguration.FullName, out projectConfiguration)) + { + xw.WriteStartElement("ProjectConfiguration"); + xw.WriteAttributeString("Project", project.ProjectGuid); + xw.WriteAttributeString("AbsolutePath", project.AbsolutePath); + xw.WriteAttributeString("BuildProjectInSolution", projectConfiguration.IncludeInBuild.ToString()); + xw.WriteString(projectConfiguration.FullName); + + foreach (string dependencyProjectGuid in project.Dependencies) + { + // This is a project that the current project depends *ON* (ie., it must build first) + ProjectInSolution dependencyProject; + if (!solutionFile.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out dependencyProject)) + { + // If it's not itself part of the solution, that's an invalid solution + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(dependencyProject != null, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo(solutionFile.FullPath), "SolutionParseProjectDepNotFoundError", project.ProjectGuid, dependencyProjectGuid); + } + + // Add it to the list of dependencies, but only if it should build in this solution configuration + // (If a project is not selected for build in the solution configuration, it won't build even if it's depended on by something that IS selected for build) + // .. and only if it's known to be MSBuild format, as projects can't use the information otherwise + if (dependencyProject.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat) + { + ProjectConfigurationInSolution dependencyProjectConfiguration = null; + if (dependencyProject.ProjectConfigurations.TryGetValue(solutionConfiguration.FullName, out dependencyProjectConfiguration) && + WouldProjectBuild(solutionFile, solutionConfiguration.FullName, dependencyProject, dependencyProjectConfiguration)) + { + xw.WriteStartElement("ProjectDependency"); + xw.WriteAttributeString("Project", dependencyProjectGuid); + xw.WriteEndElement(); + } + } + } + + xw.WriteEndElement(); // + } + } + + xw.WriteEndElement(); // + } + + var escapedSolutionConfigurationContents = EscapingUtilities.Escape(solutionConfigurationContents.ToString()); + + solutionConfigurationProperties.AddProperty("CurrentSolutionConfigurationContents", escapedSolutionConfigurationContents); + + msbuildProject.AddItem( + "SolutionConfiguration", + solutionConfiguration.FullName, + new Dictionary + { + { "Configuration", solutionConfiguration.ConfigurationName }, + { "Platform", solutionConfiguration.PlatformName }, + { "Content", escapedSolutionConfigurationContents }, + }); + } + + /// + /// Add a new error/warning/message tag into the given target + /// + internal static ProjectTaskElement AddErrorWarningMessageElement + ( + ProjectTargetElement target, + string elementType, + bool treatAsLiteral, + string textResourceName, + params object[] args + ) + { + string code = null; + string helpKeyword = null; + string text = ResourceUtilities.FormatResourceString(out code, out helpKeyword, textResourceName, args); + + if (treatAsLiteral) + { + text = EscapingUtilities.Escape(text); + } + + ProjectTaskElement task = target.AddTask(elementType); + task.SetParameter("Text", text); + + if ((elementType != XMakeElements.message) && (code != null)) + { + task.SetParameter("Code", EscapingUtilities.Escape(code)); + } + + if ((elementType != XMakeElements.message) && (helpKeyword != null)) + { + task.SetParameter("HelpKeyword", EscapingUtilities.Escape(helpKeyword)); + } + + return task; + } + + /// + /// Normally the active solution configuration/platform is determined when we build the solution + /// wrapper project, not when we create it. However, we need to know them to scan project references + /// for the right project configuration/platform. It's unlikely that references would be conditional, + /// but still possible and we want to get that case right. + /// + internal static string PredictActiveSolutionConfigurationName(SolutionFile solutionFile, IDictionary globalProperties) + { + string candidateFullSolutionConfigurationName = DetermineLikelyActiveSolutionConfiguration(solutionFile, globalProperties); + + // Now check if this solution configuration actually exists + string fullSolutionConfigurationName = null; + + foreach (SolutionConfigurationInSolution solutionConfiguration in solutionFile.SolutionConfigurations) + { + if (String.Equals(solutionConfiguration.FullName, candidateFullSolutionConfigurationName, StringComparison.OrdinalIgnoreCase)) + { + fullSolutionConfigurationName = solutionConfiguration.FullName; + break; + } + } + + return fullSolutionConfigurationName; + } + + /// + /// Returns the name of the metaproject for an actual project. + /// + /// The full path to the actual project + /// The metaproject path name + private static string GetMetaprojectName(string fullPathToProject) + { + return EscapingUtilities.Escape(fullPathToProject + ".metaproj"); + } + + /// + /// Figure out what tools version to build the solution wrapper project with. If a /tv + /// switch was passed in, use that; otherwise fall back to the default (12.0). + /// + private static string DetermineWrapperProjectToolsVersion(string toolsVersionOverride, out bool explicitToolsVersionSpecified) + { + string wrapperProjectToolsVersion = toolsVersionOverride; + + if (wrapperProjectToolsVersion == null) + { + explicitToolsVersionSpecified = false; + wrapperProjectToolsVersion = Constants.defaultSolutionWrapperProjectToolsVersion; + } + else + { + explicitToolsVersionSpecified = true; + } + + return wrapperProjectToolsVersion; + } + + /// + /// Add a call to the ResolveAssemblyReference task to crack the pre-resolved referenced + /// assemblies for the complete list of dependencies, PDBs, satellites, etc. The invoke + /// the Copy task to copy all these files (or at least the ones that RAR determined should + /// be copied local) into the web project's bin directory. + /// + private static void AddTasksToCopyAllDependenciesIntoBinDir + ( + ProjectTargetInstance target, + ProjectInSolution project, + string referenceItemName, + string conditionDescribingValidConfigurations + ) + { + string copyLocalFilesItemName = referenceItemName + "_CopyLocalFiles"; + string targetFrameworkDirectoriesName = GenerateSafePropertyName(project, "_TargetFrameworkDirectories"); + string fullFrameworkRefAssyPathName = GenerateSafePropertyName(project, "_FullFrameworkReferenceAssemblyPaths"); + string destinationFolder = String.Format(CultureInfo.InvariantCulture, @"$({0})\Bin\", GenerateSafePropertyName(project, "AspNetPhysicalPath")); + + // This is a bit of a hack. We're actually calling the "Copy" task on all of + // the *non-existent* files. Why? Because we want to emit a warning in the + // log for each non-existent file, and the Copy task does that nicely for us. + // I would have used the task except for the fact that we are in + // string-resource lockdown. + ProjectTaskInstance copyNonExistentReferencesTask = target.AddTask("Copy", String.Format(CultureInfo.InvariantCulture, "!Exists('%({0}.Identity)')", referenceItemName), "true"); + copyNonExistentReferencesTask.SetParameter("SourceFiles", "@(" + referenceItemName + "->'%(FullPath)')"); + copyNonExistentReferencesTask.SetParameter("DestinationFolder", destinationFolder); + + // We need to determine the appropriate TargetFrameworkMoniker to pass to GetReferenceAssemblyPaths, + // so that we will pass the appropriate target framework directories to RAR. + ProjectTaskInstance getRefAssembliesTask = target.AddTask("GetReferenceAssemblyPaths", null, null); + getRefAssembliesTask.SetParameter("TargetFrameworkMoniker", project.TargetFrameworkMoniker); + getRefAssembliesTask.SetParameter("RootPath", "$(TargetFrameworkRootPath)"); + getRefAssembliesTask.AddOutputProperty("ReferenceAssemblyPaths", targetFrameworkDirectoriesName, null); + getRefAssembliesTask.AddOutputProperty("FullFrameworkReferenceAssemblyPaths", fullFrameworkRefAssyPathName, null); + + // Call ResolveAssemblyReference on each of the .DLL files that were found on + // disk from the .REFRESH files as well as the P2P references. RAR will crack + // the dependencies, find PDBs, satellite assemblies, etc., and determine which + // files need to be copy-localed. + ProjectTaskInstance rarTask = target.AddTask("ResolveAssemblyReference", String.Format(CultureInfo.InvariantCulture, "Exists('%({0}.Identity)')", referenceItemName), null); + rarTask.SetParameter("Assemblies", "@(" + referenceItemName + "->'%(FullPath)')"); + rarTask.SetParameter("TargetFrameworkDirectories", "$(" + targetFrameworkDirectoriesName + ")"); + rarTask.SetParameter("FullFrameworkFolders", "$(" + fullFrameworkRefAssyPathName + ")"); + rarTask.SetParameter("SearchPaths", "{RawFileName};{TargetFrameworkDirectory};{GAC}"); + rarTask.SetParameter("FindDependencies", "true"); + rarTask.SetParameter("FindSatellites", "true"); + rarTask.SetParameter("FindSerializationAssemblies", "true"); + rarTask.SetParameter("FindRelatedFiles", "true"); + rarTask.SetParameter("TargetFrameworkMoniker", project.TargetFrameworkMoniker); + rarTask.AddOutputItem("CopyLocalFiles", copyLocalFilesItemName, null); + + // Copy all the copy-local files (reported by RAR) to the web project's "bin" + // directory. + ProjectTaskInstance copyTask = target.AddTask("Copy", conditionDescribingValidConfigurations, null); + copyTask.SetParameter("SourceFiles", "@(" + copyLocalFilesItemName + ")"); + copyTask.SetParameter + ( + "DestinationFiles", + String.Format(CultureInfo.InvariantCulture, @"@({0}->'{1}%(DestinationSubDirectory)%(Filename)%(Extension)')", copyLocalFilesItemName, destinationFolder) + ); + } + + /// + /// This code handles the *.REFRESH files that are in the "bin" subdirectory of + /// a web project. These .REFRESH files are just text files that contain absolute or + /// relative paths to the referenced assemblies. The goal of these tasks is to + /// search all *.REFRESH files and extract fully-qualified absolute paths for + /// each of the references. + /// + private static void AddTasksToResolveAutoRefreshFileReferences + ( + ProjectTargetInstance target, + ProjectInSolution project, + string referenceItemName + ) + { + string webRoot = "$(" + GenerateSafePropertyName(project, "AspNetPhysicalPath") + ")"; + + // Create an item list containing each of the .REFRESH files. + ProjectTaskInstance createItemTask = target.AddTask("CreateItem", null, null); + createItemTask.SetParameter("Include", webRoot + @"\Bin\*.refresh"); + createItemTask.AddOutputItem("Include", referenceItemName + "_RefreshFile", null); + + // Read the lines out of each .REFRESH file; they should be paths to .DLLs. Put these paths + // into an item list. + ProjectTaskInstance readLinesTask = target.AddTask("ReadLinesFromFile", String.Format(CultureInfo.InvariantCulture, @" '%({0}_RefreshFile.Identity)' != '' ", referenceItemName), null); + readLinesTask.SetParameter("File", String.Format(CultureInfo.InvariantCulture, @"%({0}_RefreshFile.Identity)", referenceItemName)); + readLinesTask.AddOutputItem("Lines", referenceItemName + "_ReferenceRelPath", null); + + // Take those paths and combine them with the root of the web project to form either + // an absolute path or a path relative to the .SLN file. These paths can be passed + // directly to RAR later. + ProjectTaskInstance combinePathTask = target.AddTask("CombinePath", null, null); + combinePathTask.SetParameter("BasePath", webRoot); + combinePathTask.SetParameter("Paths", String.Format(CultureInfo.InvariantCulture, @"@({0}_ReferenceRelPath)", referenceItemName)); + combinePathTask.AddOutputItem("CombinedPaths", referenceItemName, null); + } + + /// + /// Adds an MSBuild task to the specified target + /// + private static ProjectTaskInstance AddMSBuildTaskInstance + ( + ProjectTargetInstance target, + string projectPath, + string msbuildTargetName, + string configurationName, + string platformName, + bool specifyProjectToolsVersion + ) + { + ProjectTaskInstance msbuildTask = target.AddTask("MSBuild", null, null); + msbuildTask.SetParameter("Projects", EscapingUtilities.Escape(projectPath)); + + if (msbuildTargetName != null && msbuildTargetName.Length > 0) + { + msbuildTask.SetParameter("Targets", msbuildTargetName); + } + + string additionalProperties = string.Format( + CultureInfo.InvariantCulture, + "Configuration={0}; Platform={1}; BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)", + EscapingUtilities.Escape(configurationName), + EscapingUtilities.Escape(platformName) + ); + + msbuildTask.SetParameter("Properties", additionalProperties); + if (specifyProjectToolsVersion) + { + msbuildTask.SetParameter("ToolsVersion", "$(ProjectToolsVersion)"); + } + + return msbuildTask; + } + + /// + /// Takes a project in the solution and a base property name, and creates a new property name + /// that can safely be used as an XML element name, and is also unique to that project (by + /// embedding the project's GUID into the property name. + /// + private static string GenerateSafePropertyName + ( + ProjectInSolution proj, + string propertyName + ) + { + // XML element names cannot contain curly braces, so get rid of them from the project guid. + string projectGuid = proj.ProjectGuid.Substring(1, proj.ProjectGuid.Length - 2); + return "Project_" + projectGuid + "_" + propertyName; + } + + /// + /// Makes a legal item name from a given string by replacing invalid characters with '_' + /// + private static string MakeIntoSafeItemName(string name) + { + StringBuilder builder = new StringBuilder(name); + + if (name.Length > 0) + { + if (!XmlUtilities.IsValidInitialElementNameCharacter(name[0])) + { + builder[0] = '_'; + } + } + + for (int i = 1; i < builder.Length; i++) + { + if (!XmlUtilities.IsValidSubsequentElementNameCharacter(builder[i])) + { + builder[i] = '_'; + } + } + + return builder.ToString(); + } + + /// + /// Add a new error/warning/message tag into the given target + /// + private static ProjectTaskInstance AddErrorWarningMessageInstance + ( + ProjectTargetInstance target, + string condition, + string elementType, + bool treatAsLiteral, + string textResourceName, + params object[] args + ) + { + string code = null; + string helpKeyword = null; + string text = ResourceUtilities.FormatResourceString(out code, out helpKeyword, textResourceName, args); + + if (treatAsLiteral) + { + text = EscapingUtilities.Escape(text); + } + + ProjectTaskInstance task = target.AddTask(elementType, condition, null); + task.SetParameter("Text", text); + + if ((elementType != XMakeElements.message) && (code != null)) + { + task.SetParameter("Code", EscapingUtilities.Escape(code)); + } + + if ((elementType != XMakeElements.message) && (helpKeyword != null)) + { + task.SetParameter("HelpKeyword", EscapingUtilities.Escape(helpKeyword)); + } + + return task; + } + + /// + /// A helper method for constructing conditions for a solution configuration + /// + /// + /// Sample configuration condition: + /// '$(Configuration)' == 'Release' and '$(Platform)' == 'Any CPU' + /// + private static string GetConditionStringForConfiguration(SolutionConfigurationInSolution configuration) + { + return string.Format + ( + CultureInfo.InvariantCulture, + " ('$(Configuration)' == '{0}') and ('$(Platform)' == '{1}') ", + EscapingUtilities.Escape(configuration.ConfigurationName), + EscapingUtilities.Escape(configuration.PlatformName) + ); + } + + /// + /// Figure out what solution configuration we are going to build, whether or not it actually exists in the solution + /// file. + /// + private static string DetermineLikelyActiveSolutionConfiguration(SolutionFile solutionFile, IDictionary globalProperties) + { + string activeSolutionConfiguration = null; + string activeSolutionPlatform = null; + + globalProperties.TryGetValue("Configuration", out activeSolutionConfiguration); + globalProperties.TryGetValue("Platform", out activeSolutionPlatform); + + if (String.IsNullOrEmpty(activeSolutionConfiguration)) + { + activeSolutionConfiguration = solutionFile.GetDefaultConfigurationName(); + } + + if (String.IsNullOrEmpty(activeSolutionPlatform)) + { + activeSolutionPlatform = solutionFile.GetDefaultPlatformName(); + } + + SolutionConfigurationInSolution configurationInSolution = new SolutionConfigurationInSolution(activeSolutionConfiguration, activeSolutionPlatform); + + return configurationInSolution.FullName; + } + + /// + /// Returns true if the specified project will build in the currently selected solution configuration. + /// + private static bool WouldProjectBuild(SolutionFile solutionFile, string selectedSolutionConfiguration, ProjectInSolution project, ProjectConfigurationInSolution projectConfiguration) + { + if (projectConfiguration == null) + { + if (project.ProjectType == SolutionProjectType.WebProject) + { + // Sometimes web projects won't have the configuration we need (Release typically.) But they should still build if there is + // a solution configuration for it + foreach (SolutionConfigurationInSolution configuration in solutionFile.SolutionConfigurations) + { + if (String.Equals(configuration.FullName, selectedSolutionConfiguration, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + // No configuration, so it can't build. + return false; + } + + if (!projectConfiguration.IncludeInBuild) + { + // Not included in the build. + return false; + } + + return true; + } + + /// + /// Private method: generates an MSBuild wrapper project for the solution passed in; the MSBuild wrapper + /// project to be generated is the private variable "msbuildProject" and the SolutionFile containing information + /// about the solution is the private variable "solutionFile" + /// + private ProjectInstance[] Generate() + { + // Validate against our minimum for upgradable projects + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + (_solutionFile.Version >= SolutionFile.slnFileMinVersion), + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(_solutionFile.FullPath), + "SolutionParseUpgradeNeeded" + ); + + // This is needed in order to make decisions about tools versions such as whether to put a + // ToolsVersion parameter on task tags and what MSBuildToolsPath to use when + // scanning child projects for dependency information. + // The knowledge of whether it was explicitly specified is required because otherwise we + // don't know whether we need to pass the ToolsVersion on to the child projects or not. + bool explicitToolsVersionSpecified = false; + string wrapperProjectToolsVersion = DetermineWrapperProjectToolsVersion(_toolsVersionOverride, out explicitToolsVersionSpecified); + + return CreateSolutionProject(wrapperProjectToolsVersion, explicitToolsVersionSpecified); + } + + /// + /// Given a parsed solution, generate a top level traversal project and the metaprojects representing the dependencies for each real project + /// referenced in the solution. + /// + private ProjectInstance[] CreateSolutionProject(string wrapperProjectToolsVersion, bool explicitToolsVersionSpecified) + { + AddFakeReleaseSolutionConfigurationIfNecessary(); + + if (_solutionFile.ContainsWebDeploymentProjects) + { + // If there are Web Deployment projects, we need to scan those project files + // and specify the references explicitly. + // Other references are either ProjectReferences (taken care of by MSBuild) or + // explicit manual references in the solution file -- which get parsed out by + // the SolutionParser. + string childProjectToolsVersion = DetermineChildProjectToolsVersion(wrapperProjectToolsVersion); + string fullSolutionConfigurationName = PredictActiveSolutionConfigurationName(); + + ScanProjectDependencies(childProjectToolsVersion, fullSolutionConfigurationName); + } + + // Get a list of all actual projects in the solution + List projectsInOrder = new List(_solutionFile.ProjectsInOrder.Count); + foreach (ProjectInSolution project in _solutionFile.ProjectsInOrder) + { + if (SolutionFile.IsBuildableProject(project)) + { + projectsInOrder.Add(project); + } + } + + // Create the list of our generated projects. + List projectInstances = new List(projectsInOrder.Count + 1); + + // Create the project instance for the traversal project. + ProjectInstance traversalInstance = CreateTraversalInstance(wrapperProjectToolsVersion, explicitToolsVersionSpecified, projectsInOrder); + + // Compute the solution configuration which will be used for this build. We will use it later. + _selectedSolutionConfiguration = String.Format(CultureInfo.InvariantCulture, "{0}|{1}", traversalInstance.GetProperty("Configuration").EvaluatedValue, traversalInstance.GetProperty("Platform").EvaluatedValue); + projectInstances.Add(traversalInstance); + + // Now evaluate all of the projects in the solution and handle them appropriately. + EvaluateAndAddProjects(projectsInOrder, projectInstances, traversalInstance, _selectedSolutionConfiguration); + + // Special environment variable to allow people to see the in-memory MSBuild project generated + // to represent the SLN. + if (Environment.GetEnvironmentVariable("MSBuildEmitSolution") != null) + { + foreach (ProjectInstance instance in projectInstances) + { + instance.ToProjectRootElement().Save(instance.FullPath); + } + } + + return projectInstances.ToArray(); + } + + /// + /// Examine each project in the solution, add references and targets for it, and create metaprojects if necessary. + /// + private void EvaluateAndAddProjects(List projectsInOrder, List projectInstances, ProjectInstance traversalInstance, string selectedSolutionConfiguration) + { + // Now add all of the per-project items, targets and metaprojects. + foreach (ProjectInSolution project in projectsInOrder) + { + ProjectConfigurationInSolution projectConfiguration; + project.ProjectConfigurations.TryGetValue(selectedSolutionConfiguration, out projectConfiguration); + if (!WouldProjectBuild(_solutionFile, selectedSolutionConfiguration, project, projectConfiguration)) + { + // Project wouldn't build, so omit it from further processing. + continue; + } + + bool canBuildDirectly = CanBuildDirectly(traversalInstance, project, projectConfiguration); + + // Add an entry to @(ProjectReference) for the project. This will be either a reference directly to the project, or to the + // metaproject, as appropriate. + AddProjectReference(traversalInstance, traversalInstance, project, projectConfiguration, canBuildDirectly); + + // Add the targets to the traversal project for each standard target. These will either invoke the project directly or invoke the + // metaproject, as appropriate + AddTraversalTargetForProject(traversalInstance, project, projectConfiguration, null, "BuildOutput", canBuildDirectly); + AddTraversalTargetForProject(traversalInstance, project, projectConfiguration, "Clean", null, canBuildDirectly); + AddTraversalTargetForProject(traversalInstance, project, projectConfiguration, "Rebuild", "BuildOutput", canBuildDirectly); + AddTraversalTargetForProject(traversalInstance, project, projectConfiguration, "Publish", null, canBuildDirectly); + + // If we cannot build the project directly, then we need to generate a metaproject for it. + if (!canBuildDirectly) + { + ProjectInstance metaProject = CreateMetaproject(traversalInstance, project, projectConfiguration); + projectInstances.Add(metaProject); + } + } + } + + /// + /// Adds the standard targets to the traversal project. + /// + private void AddStandardTraversalTargets(ProjectInstance traversalInstance, List projectsInOrder) + { + // Add the initial target with some solution configuration validation/information + AddInitialTargets(traversalInstance, projectsInOrder); + + // Add the targets to traverse the metaprojects. + AddTraversalReferencesTarget(traversalInstance, null, "CollectedBuildOutput"); + AddTraversalReferencesTarget(traversalInstance, "Clean", null); + AddTraversalReferencesTarget(traversalInstance, "Rebuild", "CollectedBuildOutput"); + AddTraversalReferencesTarget(traversalInstance, "Publish", null); + } + + /// + /// Creates the traversal project instance. This has all of the properties against which we can perform evaluations for the remainder of the process. + /// + private ProjectInstance CreateTraversalInstance(string wrapperProjectToolsVersion, bool explicitToolsVersionSpecified, List projectsInOrder) + { + // Create the traversal project's root element. We will later instantiate this, and use it for evaluation of conditions on + // the metaprojects. + ProjectRootElement traversalProject = ProjectRootElement.Create(); + traversalProject.ToolsVersion = wrapperProjectToolsVersion; + traversalProject.DefaultTargets = "Build"; + traversalProject.InitialTargets = "ValidateSolutionConfiguration;ValidateToolsVersions;ValidateProjects"; + traversalProject.FullPath = _solutionFile.FullPath + ".metaproj"; + + // We don't use dependency levels any more - however this will find circular dependencies and throw for us. + Dictionary> projectsByDependencyLevel = new Dictionary>(); + + // Add default solution configuration/platform names in case the user doesn't specify them on the command line + AddConfigurationPlatformDefaults(traversalProject); + + // Add default Venus configuration names (for more details, see comments for this method) + AddVenusConfigurationDefaults(traversalProject); + + // Add solution related macros + AddGlobalProperties(traversalProject); + + // Add a property group for each solution configuration, each with one XML property containing the + // project configurations in this solution configuration. + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionFile.SolutionConfigurations) + { + AddPropertyGroupForSolutionConfiguration(traversalProject, solutionConfiguration); + } + + // Add our global extensibility points to the project representing the solution: + // Imported at the top: $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportBefore\* + // Imported at the bottom: $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportAfter\* + ProjectImportElement importBefore = traversalProject.CreateImportElement(@"$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportBefore\*"); + importBefore.Condition = @"'$(ImportByWildcardBeforeSolution)' != 'false' and exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportBefore')"; // Avoids wildcard perf problem + + ProjectImportElement importAfter = traversalProject.CreateImportElement(@"$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportAfter\*"); + importAfter.Condition = @"'$(ImportByWildcardBeforeSolution)' != 'false' and exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportAfter')"; // Avoids wildcard perf problem + + // Add our local extensibility points to the project representing the solution + // Imported at the top: before.mysolution.sln.targets + // Imported at the bottom: after.mysolution.sln.targets + string escapedSolutionFile = EscapingUtilities.Escape(Path.GetFileName(_solutionFile.FullPath)); + string escapedSolutionDirectory = EscapingUtilities.Escape(_solutionFile.SolutionFileDirectory); + string localFile = Path.Combine(escapedSolutionDirectory, "before." + escapedSolutionFile + ".targets"); + ProjectImportElement importBeforeLocal = traversalProject.CreateImportElement(localFile); + importBeforeLocal.Condition = @"exists('" + localFile + "')"; + + localFile = Path.Combine(escapedSolutionDirectory, "after." + escapedSolutionFile + ".targets"); + ProjectImportElement importAfterLocal = traversalProject.CreateImportElement(localFile); + importAfterLocal.Condition = @"exists('" + localFile + "')"; + + // Put locals second so they can override globals if they want + traversalProject.PrependChild(importBeforeLocal); + traversalProject.PrependChild(importBefore); + traversalProject.AppendChild(importAfter); + traversalProject.AppendChild(importAfterLocal); + + // These are just dummies necessary to make the evaluation into a project instance succeed when + // any custom imported targets have declarations like BeforeTargets="Build" + // They'll be replaced momentarily with the real ones. + traversalProject.AddTarget("Build"); + traversalProject.AddTarget("Rebuild"); + traversalProject.AddTarget("Clean"); + traversalProject.AddTarget("Publish"); + + // For debugging purposes: some information is lost when evaluating into a project instance, + // so make it possible to see what we have at this point. + if (Environment.GetEnvironmentVariable("MSBUILDEMITSOLUTION") != null) + { + string path = traversalProject.FullPath; + traversalProject.Save(_solutionFile.FullPath + ".metaproj.tmp"); + traversalProject.FullPath = path; + } + + // Create the instance. From this point forward we can evaluate conditions against the traversal project directly. + ProjectInstance traversalInstance = new ProjectInstance + ( + traversalProject, + _globalProperties, + explicitToolsVersionSpecified ? wrapperProjectToolsVersion : null, + _solutionFile.VisualStudioVersion, + new ProjectCollection() + ); + + // Make way for the real ones + traversalInstance.RemoveTarget("Build"); + traversalInstance.RemoveTarget("Rebuild"); + traversalInstance.RemoveTarget("Clean"); + traversalInstance.RemoveTarget("Publish"); + + AddStandardTraversalTargets(traversalInstance, projectsInOrder); + + return traversalInstance; + } + + /// + /// This method adds a new ProjectReference item to the specified instance. The reference will either be to its metaproject (if the project + /// is a web project or has reference of its own) or to the project itself (if it has no references and is a normal MSBuildable project.) + /// + private void AddProjectReference(ProjectInstance traversalProject, ProjectInstance projectInstance, ProjectInSolution projectToAdd, ProjectConfigurationInSolution projectConfiguration, bool direct) + { + ProjectItemInstance item; + + if (direct) + { + // We can build this project directly, so add its reference. + item = projectInstance.AddItem("ProjectReference", EscapingUtilities.Escape(projectToAdd.AbsolutePath), null); + item.SetMetadata("ToolsVersion", GetToolsVersionMetadataForDirectMSBuildTask(traversalProject)); + item.SetMetadata("SkipNonexistentProjects", "False"); // Skip if it doesn't exist on disk. + item.SetMetadata("AdditionalProperties", GetPropertiesMetadataForProjectReference(traversalProject, GetConfigurationAndPlatformPropertiesString(projectConfiguration))); + } + else + { + // We cannot build directly, add the metaproject reference instead. + item = projectInstance.AddItem("ProjectReference", GetMetaprojectName(projectToAdd), null); + item.SetMetadata("ToolsVersion", traversalProject.ToolsVersion); + item.SetMetadata("SkipNonexistentProjects", "Build"); // Instruct the MSBuild task to try to build even though the file doesn't exist on disk. + item.SetMetadata("AdditionalProperties", GetPropertiesMetadataForProjectReference(traversalProject, SolutionConfigurationAndPlatformProperties)); + } + + // Set raw config and platform for custom build steps to use if they wish + // Configuration is null for web projects + if (projectConfiguration != null) + { + item.SetMetadata("Configuration", projectConfiguration.ConfigurationName); + item.SetMetadata("Platform", projectConfiguration.PlatformName); + } + } + + /// + /// The value to be passed to the ToolsVersion attribute of the MSBuild task used to directly build a project. + /// + private string GetToolsVersionMetadataForDirectMSBuildTask(ProjectInstance traversalProject) + { + string directProjectToolsVersion = traversalProject.GetPropertyValue("ProjectToolsVersion"); + return directProjectToolsVersion; + } + + /// + /// The value to be passed to the ToolsVersion attribute of the MSBuild task used to directly build a project. + /// + private string GetToolsVersionAttributeForDirectMSBuildTask(ProjectInstance traversalProject) + { + return "$(ProjectToolsVersion)"; + } + + /// + /// The value to be assigned to the metadata for a particular project reference. Contains only configuration and platform specified in the project configuration, evaluated. + /// + private string GetPropertiesMetadataForProjectReference(ProjectInstance traversalProject, string configurationAndPlatformProperties) + { + string directProjectProperties = traversalProject.ExpandString(configurationAndPlatformProperties); + + if (traversalProject.SubToolsetVersion != null) + { + // Note: it is enough below to compare traversalProject.SubToolsetVersion with 4.0 as a means to verify if + // traversalProject.SubToolsetVersion < 12.0 since this path isn't followed for traversalProject.SubToolsetVersion values of 2.0 and 3.5 + if (traversalProject.SubToolsetVersion.Equals("4.0", StringComparison.OrdinalIgnoreCase)) + { + directProjectProperties = String.Format(CultureInfo.InvariantCulture, "{0}; {1}={2}", directProjectProperties, Constants.SubToolsetVersionPropertyName, traversalProject.SubToolsetVersion); + } + } + + return directProjectProperties; + } + + /// + /// Gets the project configuration and platform values as an attribute string for an MSBuild task used to build the project. + /// + private string GetConfigurationAndPlatformPropertiesString(ProjectConfigurationInSolution projectConfiguration) + { + string directProjectProperties = String.Format(CultureInfo.InvariantCulture, "Configuration={0}; Platform={1}", projectConfiguration.ConfigurationName, projectConfiguration.PlatformName); + return directProjectProperties; + } + + /// + /// The value to be passed to the Properties attribute of the MSBuild task to build a specific project. Contains reference to project configuration and + /// platform as well as the solution configuration bits. + /// + private string GetPropertiesAttributeForDirectMSBuildTask(ProjectConfigurationInSolution projectConfiguration) + { + string directProjectProperties = OpportunisticIntern.InternStringIfPossible(String.Join(";", GetConfigurationAndPlatformPropertiesString(projectConfiguration), SolutionProperties)); + return directProjectProperties; + } + + /// + /// Returns true if the specified project can be built directly, without using a metaproject. + /// + private bool CanBuildDirectly(ProjectInstance traversalProject, ProjectInSolution projectToAdd, ProjectConfigurationInSolution projectConfiguration) + { + // Can we build this project directly, without a metaproject? We can if it's MSBuild-able and has no references building in this configuration. + bool canBuildDirectly = false; + string unknownProjectTypeErrorMessage; + if ((projectToAdd.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat) || + (projectToAdd.CanBeMSBuildProjectFile(out unknownProjectTypeErrorMessage))) + { + canBuildDirectly = true; + foreach (string dependencyProjectGuid in projectToAdd.Dependencies) + { + ProjectInSolution dependencyProject; + if (!_solutionFile.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out dependencyProject)) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + false, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(traversalProject.FullPath), + "SolutionParseProjectDepNotFoundError", + projectToAdd.ProjectGuid, + dependencyProjectGuid + ); + } + + if (WouldProjectBuild(_solutionFile, _selectedSolutionConfiguration, dependencyProject, projectConfiguration)) + { + // This is a reference we would have to build, so we can't build the project directly. + canBuildDirectly = false; + break; + } + } + } + + return canBuildDirectly; + } + + /// + /// Produces a set of targets which allows the MSBuild scheduler to schedule projects in the order automatically by + /// following their dependencies without enforcing build levels. + /// + /// + /// We want MSBuild to be able to parallelize the builds of these projects where possible and still honor references. + /// Since the project files referenced by the solution do not (necessarily) themselves contain actual project references + /// to the projects they depend on, we need to synthesize this relationship ourselves. This is done by creating a target + /// which first invokes the project's dependencies, then invokes the actual project itself. However, invoking the + /// dependencies must also invoke their dependencies and so on down the line. + /// + /// Additionally, we do not wish to create a separate MSBuild project to contain this target yet we want to parallelize + /// calls to these targets. The way to do this is to pass in different global properties to the same project in the same + /// MSBuild call. MSBuild easily allows this using the AdditionalProperties metadata which can be specified on an Item. + /// + /// Assuming the solution project we are generating is called "foo.proj", we can accomplish this parallelism as follows: + /// + /// + /// + /// + /// + /// + /// We now have expressed the top level reference to all projects as @(SolutionReference) and each project's + /// set of references as @(PROJECTNAMEReference). We construct our target as: + /// + /// + /// + /// + /// + /// + /// The first MSBuild call re-invokes the solution project instructing it to build the reference projects for the + /// current project. The second MSBuild call invokes the actual project itself. Because all reference projects have + /// the same additional properties, MSBuild will only build the first one it comes across and the rest will be + /// satisfied from the cache. + /// + private ProjectInstance CreateMetaproject(ProjectInstance traversalProject, ProjectInSolution project, ProjectConfigurationInSolution projectConfiguration) + { + // Create a new project instance with global properties and tools version from the existing project + ProjectInstance metaprojectInstance = new ProjectInstance(EscapingUtilities.UnescapeAll(GetMetaprojectName(project)), traversalProject, GetMetaprojectGlobalProperties(traversalProject)); + + // Add the project references which must build before this one. + AddMetaprojectReferenceItems(traversalProject, metaprojectInstance, project); + + // This string holds the error message generated when we try to determine if a project is an MSBuild format + // project but it is not. + string unknownProjectTypeErrorMessage; + + if (project.ProjectType == SolutionProjectType.WebProject) + { + AddMetaprojectTargetForWebProject(traversalProject, metaprojectInstance, project, null); + AddMetaprojectTargetForWebProject(traversalProject, metaprojectInstance, project, "Clean"); + AddMetaprojectTargetForWebProject(traversalProject, metaprojectInstance, project, "Rebuild"); + AddMetaprojectTargetForWebProject(traversalProject, metaprojectInstance, project, "Publish"); + } + else if ((project.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat) || + (project.CanBeMSBuildProjectFile(out unknownProjectTypeErrorMessage))) + { + string safeItemNameFromProjectName = MakeIntoSafeItemName(project.ProjectName); + string targetOutputItemName = string.Format(CultureInfo.InvariantCulture, "{0}BuildOutput", safeItemNameFromProjectName); + + AddMetaprojectTargetForManagedProject(traversalProject, metaprojectInstance, project, projectConfiguration, "Clean", null); + AddMetaprojectTargetForManagedProject(traversalProject, metaprojectInstance, project, projectConfiguration, null, targetOutputItemName); + AddMetaprojectTargetForManagedProject(traversalProject, metaprojectInstance, project, projectConfiguration, "Rebuild", targetOutputItemName); + AddMetaprojectTargetForManagedProject(traversalProject, metaprojectInstance, project, projectConfiguration, "Publish", null); + } + else + { + AddMetaprojectTargetForUnknownProjectType(traversalProject, metaprojectInstance, project, null, unknownProjectTypeErrorMessage); + AddMetaprojectTargetForUnknownProjectType(traversalProject, metaprojectInstance, project, "Clean", unknownProjectTypeErrorMessage); + AddMetaprojectTargetForUnknownProjectType(traversalProject, metaprojectInstance, project, "Rebuild", unknownProjectTypeErrorMessage); + AddMetaprojectTargetForUnknownProjectType(traversalProject, metaprojectInstance, project, "Publish", unknownProjectTypeErrorMessage); + } + + return metaprojectInstance; + } + + /// + /// Returns the metaproject name for a given project. + /// + private string GetMetaprojectName(ProjectInSolution project) + { + string baseName; + if (project.ProjectType == SolutionProjectType.WebProject) + { + baseName = Path.Combine(_solutionFile.SolutionFileDirectory, MakeIntoSafeItemName(project.GetUniqueProjectName())); + } + else + { + baseName = project.AbsolutePath; + } + + if (String.IsNullOrEmpty(baseName)) + { + baseName = project.ProjectName; + } + + baseName = FileUtilities.EnsureNoTrailingSlash(baseName); + + return SolutionProjectGenerator.GetMetaprojectName(baseName); + } + + /// + /// Adds a set of items which describe the references for this project. + /// + private void AddMetaprojectReferenceItems(ProjectInstance traversalProject, ProjectInstance metaprojectInstance, ProjectInSolution project) + { + foreach (string dependencyProjectGuid in project.Dependencies) + { + ProjectInSolution dependencyProject; + if (!_solutionFile.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out dependencyProject)) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + false, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(traversalProject.FullPath), + "SolutionParseProjectDepNotFoundError", + project.ProjectGuid, + dependencyProjectGuid + ); + } + else + { + ProjectConfigurationInSolution dependencyProjectConfiguration = null; + if (dependencyProject.ProjectConfigurations.TryGetValue(_selectedSolutionConfiguration, out dependencyProjectConfiguration) && + WouldProjectBuild(_solutionFile, _selectedSolutionConfiguration, dependencyProject, dependencyProjectConfiguration)) + { + bool canBuildDirectly = CanBuildDirectly(traversalProject, dependencyProject, dependencyProjectConfiguration); + AddProjectReference(traversalProject, metaprojectInstance, dependencyProject, dependencyProjectConfiguration, canBuildDirectly); + } + } + } + } + + /// + /// Adds the targets which build the dependencies and actual project for a metaproject. + /// + private void AddMetaprojectTargetForManagedProject(ProjectInstance traversalProject, ProjectInstance metaprojectInstance, ProjectInSolution project, ProjectConfigurationInSolution projectConfiguration, string targetName, string outputItem) + { + ProjectTargetInstance target = metaprojectInstance.AddTarget(targetName ?? "Build", String.Empty, String.Empty, String.Empty, null, String.Empty, String.Empty, false /* legacy target returns behaviour */); + + AddReferencesBuildTask(metaprojectInstance, target, targetName, outputItem); + + // Add the task to build the actual project. + AddProjectBuildTask(traversalProject, project, projectConfiguration, target, targetName, EscapingUtilities.Escape(project.AbsolutePath), String.Empty, outputItem); + } + + /// + /// Adds an MSBuild task to a real project. + /// + private void AddProjectBuildTask(ProjectInstance traversalProject, ProjectInSolution project, ProjectConfigurationInSolution projectConfiguration, ProjectTargetInstance target, string targetToBuild, string sourceItems, string condition, string outputItem) + { + ProjectTaskInstance task = target.AddTask("MSBuild", condition, String.Empty); + task.SetParameter("Projects", sourceItems); + if (targetToBuild != null) + { + task.SetParameter("Targets", targetToBuild); + } + + task.SetParameter("BuildInParallel", "True"); + + task.SetParameter("ToolsVersion", GetToolsVersionAttributeForDirectMSBuildTask(traversalProject)); + task.SetParameter("Properties", GetPropertiesAttributeForDirectMSBuildTask(projectConfiguration)); + + if (outputItem != null) + { + task.AddOutputItem("TargetOutputs", outputItem, String.Empty); + } + } + + /// + /// Adds an MSBuild task to a single metaproject. This is used in the traversal project. + /// + private void AddMetaprojectBuildTask(ProjectInstance traversalProject, ProjectInSolution project, ProjectTargetInstance target, string targetToBuild, string outputItem) + { + ProjectTaskInstance task = target.AddTask("MSBuild", OpportunisticIntern.InternStringIfPossible("'%(ProjectReference.Identity)' == '" + GetMetaprojectName(project) + "'"), String.Empty); + task.SetParameter("Projects", "@(ProjectReference)"); + + if (targetToBuild != null) + { + task.SetParameter("Targets", targetToBuild); + } + + task.SetParameter("BuildInParallel", "True"); + task.SetParameter("ToolsVersion", MSBuildConstants.CurrentToolsVersion); + task.SetParameter("Properties", SolutionProperties); + task.SetParameter("SkipNonexistentProjects", "%(ProjectReference.SkipNonexistentProjects)"); + + if (outputItem != null) + { + task.AddOutputItem("TargetOutputs", outputItem, String.Empty); + } + } + + /// + /// Add a target for a Venus project into the XML doc that's being generated. This + /// target will call the AspNetCompiler task. + /// + private void AddMetaprojectTargetForWebProject(ProjectInstance traversalProject, ProjectInstance metaprojectInstance, ProjectInSolution project, string targetName) + { + // Add a supporting target called "GetFrameworkPathAndRedistList". + AddTargetForGetFrameworkPathAndRedistList(metaprojectInstance); + + ProjectTargetInstance newTarget = metaprojectInstance.AddTarget(targetName ?? "Build", ComputeTargetConditionForWebProject(project), null, null, null, null, "GetFrameworkPathAndRedistList", false /* legacy target returns behaviour */); + + // Build the references + AddReferencesBuildTask(metaprojectInstance, newTarget, targetName, null); + + if (targetName == "Clean") + { + // Well, hmmm. The AspNetCompiler task doesn't support any kind of + // a "Clean" operation. The best we can really do is offer up a + // message saying so. + AddErrorWarningMessageInstance(newTarget, null, XMakeElements.message, true, "SolutionVenusProjectNoClean"); + } + else if (targetName == "Publish") + { + // Well, hmmm. The AspNetCompiler task doesn't support any kind of + // a "Publish" operation. The best we can really do is offer up a + // message saying so. + AddErrorWarningMessageInstance(newTarget, null, XMakeElements.message, true, "SolutionVenusProjectNoPublish"); + } + else + { + // For normal build and "Rebuild", just call the AspNetCompiler task with the + // correct parameters. But before calling the AspNetCompiler task, we need to + // do a bunch of prep work regarding references. + + // We're going to build up an MSBuild condition string that represents the valid Configurations. + // We do this by OR'ing together individual conditions, each of which compares $(Configuration) + // with a valid configuration name. We init our condition string to "false", so we can easily + // OR together more stuff as we go, and also easily take the negation of the condition by putting + // a ! around the whole thing. + StringBuilder conditionDescribingValidConfigurations = new StringBuilder("(false)"); + + // Loop through all the valid configurations and add a PropertyGroup for each one. + foreach (DictionaryEntry aspNetConfiguration in project.AspNetConfigurations) + { + string configurationName = (string)aspNetConfiguration.Key; + AspNetCompilerParameters aspNetCompilerParameters = (AspNetCompilerParameters)aspNetConfiguration.Value; + + // We only add the PropertyGroup once per Venus project. Without the following "if", we would add + // the same identical PropertyGroup twice, once when AddTargetForWebProject is called with + // subTargetName=null and once when subTargetName="Rebuild". + if (targetName == null) + { + AddPropertyGroupForAspNetConfiguration(traversalProject, metaprojectInstance, project, configurationName, aspNetCompilerParameters, _solutionFile.FullPath); + } + + // Update our big condition string to include this configuration. + conditionDescribingValidConfigurations.Append(" or "); + conditionDescribingValidConfigurations.Append(String.Format(CultureInfo.InvariantCulture, "('$(AspNetConfiguration)' == '{0}')", EscapingUtilities.Escape(configurationName))); + } + + StringBuilder referenceItemName = new StringBuilder(GenerateSafePropertyName(project, "References")); + if (!string.IsNullOrEmpty(targetName)) + { + referenceItemName.Append('_'); + referenceItemName.Append(targetName); + } + + // Add tasks to resolve project references of this web project, if any + if (project.ProjectReferences.Count > 0) + { + // This is a bit tricky. Even though web projects don't use solution configurations, + // we want to use the current solution configuration to build the proper configurations + // of referenced projects. + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionFile.SolutionConfigurations) + { + string referenceProjectGuids = null; + + AddResolveProjectReferenceTasks + ( + traversalProject, + newTarget, + project, + solutionConfiguration, + referenceItemName.ToString(), + null /* don't care about native references */, + out referenceProjectGuids + ); + } + } + + // Add tasks to capture the auto-refreshed file references (those .REFRESH files). + AddTasksToResolveAutoRefreshFileReferences(newTarget, project, referenceItemName.ToString()); + + // Add a call to RAR (ResolveAssemblyReference) and the Copy task to put the referenced + // project outputs in the right place + AddTasksToCopyAllDependenciesIntoBinDir(newTarget, project, referenceItemName.ToString(), conditionDescribingValidConfigurations.ToString()); + + // Add a call to the AspNetCompiler task, conditioned on having a valid Configuration. + AddTaskForAspNetCompiler(newTarget, project, conditionDescribingValidConfigurations.ToString()); + + // Add a call to the task, conditioned on having an *invalid* Configuration. The + // message says that we're skipping the Venus project because it's either not enabled + // for precompilation, or doesn't support the given configuration. + ProjectTaskInstance skippingVenusProjectMessageTask = AddErrorWarningMessageInstance + ( + newTarget, + "!(" + conditionDescribingValidConfigurations.ToString() + ")", + XMakeElements.message, + false, + "SolutionVenusProjectSkipped" + ); + } + } + + /// + /// Helper method to add a call to the AspNetCompiler task into the given target. + /// + private void AddTaskForAspNetCompiler + ( + ProjectTargetInstance target, + ProjectInSolution project, + string conditionDescribingValidConfigurations + ) + { + // Add a call to the AspNetCompiler task, conditioned on having a valid Configuration. + ProjectTaskInstance newTask = target.AddTask("AspNetCompiler", conditionDescribingValidConfigurations, null); + newTask.SetParameter("VirtualPath", "$(" + GenerateSafePropertyName(project, "AspNetVirtualPath") + ")"); + newTask.SetParameter("PhysicalPath", "$(" + GenerateSafePropertyName(project, "AspNetPhysicalPath") + ")"); + newTask.SetParameter("TargetPath", "$(" + GenerateSafePropertyName(project, "AspNetTargetPath") + ")"); + newTask.SetParameter("Force", "$(" + GenerateSafePropertyName(project, "AspNetForce") + ")"); + newTask.SetParameter("Updateable", "$(" + GenerateSafePropertyName(project, "AspNetUpdateable") + ")"); + newTask.SetParameter("Debug", "$(" + GenerateSafePropertyName(project, "AspNetDebug") + ")"); + newTask.SetParameter("KeyFile", "$(" + GenerateSafePropertyName(project, "AspNetKeyFile") + ")"); + newTask.SetParameter("KeyContainer", "$(" + GenerateSafePropertyName(project, "AspNetKeyContainer") + ")"); + newTask.SetParameter("DelaySign", "$(" + GenerateSafePropertyName(project, "AspNetDelaySign") + ")"); + newTask.SetParameter("AllowPartiallyTrustedCallers", "$(" + GenerateSafePropertyName(project, "AspNetAPTCA") + ")"); + newTask.SetParameter("FixedNames", "$(" + GenerateSafePropertyName(project, "AspNetFixedNames") + ")"); + + bool isDotNetFramework = false; + + // generate the target .NET Framework version based on the passed in TargetFrameworkMoniker. + try + { + FrameworkName targetFramework = new FrameworkName(project.TargetFrameworkMoniker); + + if (String.Equals(targetFramework.Identifier, ".NETFramework", StringComparison.OrdinalIgnoreCase)) + { + isDotNetFramework = true; + + // As of .NET Framework 4.0, there are only two versions of aspnet_compiler.exe: 2.0 and 4.0. If + // the TargetFrameworkVersion is less than 4.0, use the 2.0 version. Otherwise, just use the 4.0 + // version of the executable, so that if say FV 4.1 is passed in, we don't throw an error. + if (targetFramework.Version.Major >= 4) + { + newTask.SetParameter + ( + "ToolPath", + FrameworkLocationHelper.GetPathToDotNetFramework(_version40) + ); + + if (targetFramework.Version > _version40) + { + _loggingService.LogComment(_projectBuildEventContext, MessageImportance.Low, "AspNetCompiler.TargetingHigherFrameworksDefaultsTo40", project.ProjectName, targetFramework.Version.ToString()); + } + } + else + { + string pathTo20 = FrameworkLocationHelper.GetPathToDotNetFramework(_version20); + + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(pathTo20 != null, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo(_solutionFile.FullPath), "AspNetCompiler.20NotInstalled"); + + newTask.SetParameter + ( + "ToolPath", + pathTo20 + ); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + else + { + ProjectFileErrorUtilities.ThrowInvalidProjectFile + ( + new BuildEventFileInfo(_solutionFile.FullPath), + e, + "AspNetCompiler.InvalidTargetFrameworkMonikerFromException", + project.ProjectName, + project.TargetFrameworkMoniker, + e.Message + ); + } + } + + if (!isDotNetFramework) + { + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + false, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(_solutionFile.FullPath), + "AspNetCompiler.InvalidTargetFrameworkMonikerNotDotNET", + project.ProjectName, + project.TargetFrameworkMoniker + ); + } + } + + /// + /// Adds MSBuild tasks to a project target to pre-resolve its project references + /// + private void AddResolveProjectReferenceTasks + ( + ProjectInstance traversalProject, + ProjectTargetInstance target, + ProjectInSolution project, + SolutionConfigurationInSolution solutionConfiguration, + string outputReferenceItemName, + string outputImportLibraryItemName, + out string addedReferenceGuids + ) + { + StringBuilder referenceGuids = new StringBuilder(); + + string message = null; + + // Suffix for the reference item name. Since we need to attach additional (different) metadata to every + // reference item, we need to have helper item lists each with only one item + int outputReferenceItemNameSuffix = 0; + + // Pre-resolve the MSBuild project references + foreach (string projectReferenceGuid in project.ProjectReferences) + { + ProjectInSolution referencedProject = (ProjectInSolution)_solutionFile.ProjectsByGuid[projectReferenceGuid]; + ProjectConfigurationInSolution referencedProjectConfiguration = null; + + if ((referencedProject != null) && + (referencedProject.ProjectConfigurations.TryGetValue(solutionConfiguration.FullName, out referencedProjectConfiguration)) && + (referencedProjectConfiguration != null)) + { + string outputReferenceItemNameWithSuffix = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", outputReferenceItemName, outputReferenceItemNameSuffix); + + if ((referencedProject.ProjectType == SolutionProjectType.KnownToBeMSBuildFormat) || + ((referencedProject.ProjectType == SolutionProjectType.Unknown) && (referencedProject.CanBeMSBuildProjectFile(out message)))) + { + string condition = GetConditionStringForConfiguration(solutionConfiguration); + if (traversalProject.EvaluateCondition(condition)) + { + bool specifyProjectToolsVersion = + String.Equals(traversalProject.ToolsVersion, "2.0", StringComparison.OrdinalIgnoreCase) ? false : true; + + ProjectTaskInstance msbuildTask = AddMSBuildTaskInstance + ( + target, + referencedProject.RelativePath, + "GetTargetPath", + referencedProjectConfiguration.ConfigurationName, + referencedProjectConfiguration.PlatformName, + specifyProjectToolsVersion + ); + msbuildTask.AddOutputItem("TargetOutputs", outputReferenceItemNameWithSuffix, null); + } + + if (referenceGuids.Length > 0) + { + referenceGuids.Append(';'); + } + + referenceGuids.Append(projectReferenceGuid); + + // This merges the one-item item list into the main list, adding the appropriate guid metadata + ProjectTaskInstance createItemTask = target.AddTask("CreateItem", null, null); + createItemTask.SetParameter("Include", "@(" + outputReferenceItemNameWithSuffix + ")"); + createItemTask.SetParameter("AdditionalMetadata", "Guid=" + projectReferenceGuid); + createItemTask.AddOutputItem("Include", outputReferenceItemName, null); + } + + outputReferenceItemNameSuffix++; + } + } + + addedReferenceGuids = referenceGuids.ToString(); + } + + /// + /// Add a PropertyGroup to the project for a particular Asp.Net configuration. This PropertyGroup + /// will have the correct values for all the Asp.Net properties for this project and this configuration. + /// + private void AddPropertyGroupForAspNetConfiguration + ( + ProjectInstance traversalProject, + ProjectInstance metaprojectInstance, + ProjectInSolution project, + string configurationName, + AspNetCompilerParameters aspNetCompilerParameters, + string solutionFile + ) + { + // If the configuration doesn't match, don't add the properties. + if (!traversalProject.EvaluateCondition(String.Format(CultureInfo.InvariantCulture, " '$(AspNetConfiguration)' == '{0}' ", EscapingUtilities.Escape(configurationName)))) + { + return; + } + + // Add properties into the property group for each of the AspNetCompiler properties. + // REVIEW: SetProperty takes an evaluated value. Are we doing the right thing here? + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetVirtualPath"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetVirtualPath)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetPhysicalPath"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetPhysicalPath)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetTargetPath"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetTargetPath)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetForce"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetForce)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetUpdateable"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetUpdateable)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetDebug"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetDebug)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetKeyFile"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetKeyFile)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetKeyContainer"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetKeyContainer)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetDelaySign"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetDelaySign)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetAPTCA"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetAPTCA)); + metaprojectInstance.SetProperty(GenerateSafePropertyName(project, "AspNetFixedNames"), EscapingUtilities.Escape(aspNetCompilerParameters.aspNetFixedNames)); + + string aspNetPhysicalPath = aspNetCompilerParameters.aspNetPhysicalPath; + if (!String.IsNullOrEmpty(aspNetPhysicalPath)) + { + // Trim the trailing slash if one exists. + if ( + (aspNetPhysicalPath[aspNetPhysicalPath.Length - 1] == Path.AltDirectorySeparatorChar) || + (aspNetPhysicalPath[aspNetPhysicalPath.Length - 1] == Path.DirectorySeparatorChar) + ) + { + aspNetPhysicalPath = aspNetPhysicalPath.Substring(0, aspNetPhysicalPath.Length - 1); + } + + // This gets us the last folder in the physical path. + string lastFolderInPhysicalPath = null; + + try + { + lastFolderInPhysicalPath = Path.GetFileName(aspNetPhysicalPath); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile + ( + false, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(solutionFile), + e, + "SolutionParseInvalidProjectFileName", + project.RelativePath, + e.Message + ); + } + + if (!String.IsNullOrEmpty(lastFolderInPhysicalPath)) + { + // If there is a global property called "OutDir" set, that means the caller is trying to + // override the AspNetTargetPath. What we want to do in this case is concatenate: + // $(OutDir) + "\_PublishedWebsites" + (the last portion of the folder in the AspNetPhysicalPath). + if (traversalProject.EvaluateCondition(" '$(OutDir)' != '' ")) + { + string outDirValue = String.Empty; + ProjectPropertyInstance outdir = metaprojectInstance.GetProperty("OutDir"); + + if (outdir != null) + { + outDirValue = ProjectInstance.GetPropertyValueEscaped(outdir); + } + + // Make sure the path we are appending to has no leading slash to prevent double slashes. + string publishWebsitePath = EscapingUtilities.Escape(WebProjectOverrideFolder) + Path.DirectorySeparatorChar + EscapingUtilities.Escape(lastFolderInPhysicalPath) + Path.DirectorySeparatorChar; + + metaprojectInstance.SetProperty + ( + GenerateSafePropertyName(project, "AspNetTargetPath"), + outDirValue + publishWebsitePath + ); + } + } + } + } + + /// + /// When adding a target to build a web project, we want to put a Condition on the Target node that + /// effectively says "Only build this target if the web project is active (marked for building) in the + /// current solution configuration. + /// + private string ComputeTargetConditionForWebProject(ProjectInSolution project) + { + StringBuilder condition = new StringBuilder(" ('$(CurrentSolutionConfigurationContents)' != '') and (false"); + + // Loop through all the solution configurations. + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionFile.SolutionConfigurations) + { + // Find out if the web project has a project configuration for this solution configuration. + // (Actually, web projects only have one project configuration, so the TryGetValue should + // pretty much always return "true". + ProjectConfigurationInSolution projectConfiguration = null; + if (project.ProjectConfigurations.TryGetValue(solutionConfiguration.FullName, out projectConfiguration)) + { + // See if the web project is marked as active for this solution configuration. If so, + // we'll build the target. Otherwise not. + if (projectConfiguration.IncludeInBuild) + { + condition.Append(" or ("); + condition.Append(GetConditionStringForConfiguration(solutionConfiguration)); + condition.Append(")"); + } + } + else if (String.Compare(solutionConfiguration.ConfigurationName, "Release", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(solutionConfiguration.ConfigurationName, "Debug", StringComparison.OrdinalIgnoreCase) == 0) + { + // we don't have a project configuration that matches the solution configuration but + // the solution configuration is called "Release" or "Debug" which are standard AspNetConfigurations + // so these should be available in the solution project + condition.Append(" or ("); + condition.Append(GetConditionStringForConfiguration(solutionConfiguration)); + condition.Append(")"); + } + } + + condition.Append(") "); + return condition.ToString(); + } + + /// + /// Add a target to the project called "GetFrameworkPathAndRedistList". This target calls the + /// GetFrameworkPath task and then CreateItem to populate @(_CombinedTargetFrameworkDirectoriesItem) and + /// @(InstalledAssemblyTables), so that we can pass these into the ResolveAssemblyReference task + /// when building web projects. + /// + private void AddTargetForGetFrameworkPathAndRedistList(ProjectInstance metaprojectInstance) + { + if (metaprojectInstance.Targets.ContainsKey("GetFrameworkPathAndRedistList")) + { + return; + } + + ProjectTargetInstance frameworkPathAndRedistListTarget = metaprojectInstance.AddTarget("GetFrameworkPathAndRedistList", String.Empty, null, null, null, null, null, false /* legacy target returns behaviour */); + + ProjectTaskInstance getFrameworkPathTask = frameworkPathAndRedistListTarget.AddTask("GetFrameworkPath", String.Empty, null); + + // Follow the same logic we use in Microsoft.Common.targets to choose the target framework + // directories (which are then used to find the set of redist lists). + getFrameworkPathTask.AddOutputItem( + "Path", + "_CombinedTargetFrameworkDirectoriesItem", + "'$(MSBuildToolsVersion)' == '2.0'"); + + // TFV v4.0 supported by TV 4.0+ + getFrameworkPathTask.AddOutputItem( + "FrameworkVersion40Path", + "_CombinedTargetFrameworkDirectoriesItem", + " '$(TargetFrameworkVersion)' == 'v4.0' and '$(MSBuildToolsVersion)' != '2.0' and '$(MSBuildToolsVersion)' != '3.5'"); + + // TFV v3.5 supported by TV 3.5+ + getFrameworkPathTask.AddOutputItem( + "FrameworkVersion35Path", + "_CombinedTargetFrameworkDirectoriesItem", + " ('$(TargetFrameworkVersion)' == 'v3.5' or '$(TargetFrameworkVersion)' == 'v4.0') and '$(MSBuildToolsVersion)' != '2.0'"); + + // TFV v3.0 supported by TV 3.5+ (there was no TV 3.0) + getFrameworkPathTask.AddOutputItem( + "FrameworkVersion30Path", + "_CombinedTargetFrameworkDirectoriesItem", + " ('$(TargetFrameworkVersion)' == 'v3.0' or '$(TargetFrameworkVersion)' == 'v3.5' or '$(TargetFrameworkVersion)' == 'v4.0') and '$(MSBuildToolsVersion)' != '2.0'"); + + // TFV v2.0 supported by TV 3.5+ (there was no TV 3.0). This property was not added until toolsversion 3.5 therefore it cannot be used for toolsversion 2.0 + getFrameworkPathTask.AddOutputItem( + "FrameworkVersion20Path", + "_CombinedTargetFrameworkDirectoriesItem", + "'$(MSBuildToolsVersion)' != '2.0'"); + + ProjectTaskInstance createItemTask = frameworkPathAndRedistListTarget.AddTask("CreateItem", null, null); + createItemTask.SetParameter("Include", @"@(_CombinedTargetFrameworkDirectoriesItem->'%(Identity)\RedistList\*.xml')"); + createItemTask.AddOutputItem("Include", "InstalledAssemblyTables", null); + } + + /// + /// Adds a target for a project whose type is unknown and we cannot build. We will emit an error or warning as appropriate. + /// + private void AddMetaprojectTargetForUnknownProjectType(ProjectInstance traversalProject, ProjectInstance metaprojectInstance, ProjectInSolution project, string targetName, string unknownProjectTypeErrorMessage) + { + ProjectTargetInstance newTarget = metaprojectInstance.AddTarget(targetName ?? "Build", "'$(CurrentSolutionConfigurationContents)' != ''", null, null, null, null, null, false /* legacy target returns behaviour */); + + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionFile.SolutionConfigurations) + { + ProjectConfigurationInSolution projectConfiguration = null; + ProjectTaskInstance newTask = null; + + if (project.ProjectConfigurations.TryGetValue(solutionConfiguration.FullName, out projectConfiguration)) + { + if (projectConfiguration.IncludeInBuild) + { + // Only add the task if it would run in this configuration. + if (!traversalProject.EvaluateCondition(GetConditionStringForConfiguration(solutionConfiguration))) + { + continue; + } + + if (unknownProjectTypeErrorMessage == null) + { + // we haven't encountered any problems accessing the project file in the past, but do not support + // building this project type + newTask = AddErrorWarningMessageInstance + ( + newTarget, + null, + XMakeElements.warning, + true, + "SolutionParseUnknownProjectType", + project.RelativePath + ); + } + else + { + // this project file may be of supported type, but we have encountered problems accessing it + newTask = AddErrorWarningMessageInstance + ( + newTarget, + null, + XMakeElements.warning, + true, + "SolutionParseErrorReadingProject", + project.RelativePath, + unknownProjectTypeErrorMessage + ); + } + } + else + { + newTask = AddErrorWarningMessageInstance + ( + newTarget, + null, + XMakeElements.message, + true, + "SolutionProjectSkippedForBuilding", + project.ProjectName, + solutionConfiguration.FullName + ); + } + } + else + { + newTask = AddErrorWarningMessageInstance + ( + newTarget, + null, + XMakeElements.warning, + true, + "SolutionProjectConfigurationMissing", + project.ProjectName, + solutionConfiguration.FullName + ); + } + } + } + + /// + /// Adds a target which verifies that all of the project references and configurations are valid. + /// + private void AddValidateProjectsTarget(ProjectInstance traversalProject, List projects) + { + ProjectTargetInstance newTarget = traversalProject.AddTarget("ValidateProjects", null, null, null, null, null, null, false /* legacy target returns behaviour */); + + foreach (ProjectInSolution project in projects) + { + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionFile.SolutionConfigurations) + { + ProjectConfigurationInSolution projectConfiguration = null; + string condition = GetConditionStringForConfiguration(solutionConfiguration); + + if (project.ProjectConfigurations.TryGetValue(solutionConfiguration.FullName, out projectConfiguration)) + { + if (!projectConfiguration.IncludeInBuild) + { + ProjectTaskInstance messageTask = AddErrorWarningMessageInstance + ( + newTarget, + condition, + XMakeElements.message, + true, + "SolutionProjectSkippedForBuilding", + project.ProjectName, + solutionConfiguration.FullName + ); + } + } + else + { + ProjectTaskInstance warningTask = AddErrorWarningMessageInstance + ( + newTarget, + condition, + XMakeElements.warning, + true, + "SolutionProjectConfigurationMissing", + project.ProjectName, + solutionConfiguration.FullName + ); + } + } + } + } + + /// + /// Creates the target used to build all of the references in the traversal project. + /// + private void AddTraversalReferencesTarget(ProjectInstance traversalProject, string targetName, string outputItem) + { + string outputItemAsItem = null; + if (!String.IsNullOrEmpty(outputItem)) + { + outputItemAsItem = "@(" + outputItem + ")"; + } + + ProjectTargetInstance target = traversalProject.AddTarget(targetName ?? "Build", String.Empty, String.Empty, outputItemAsItem, null, String.Empty, String.Empty, false /* legacy target returns behaviour */); + AddReferencesBuildTask(traversalProject, target, targetName, outputItem); + } + + /// + /// Adds a task which builds the @(ProjectReference) items. + /// + private void AddReferencesBuildTask(ProjectInstance projectInstance, ProjectTargetInstance target, string targetToBuild, string outputItem) + { + ProjectTaskInstance task = target.AddTask("MSBuild", String.Empty, String.Empty); + if (String.Equals(targetToBuild, "Clean", StringComparison.OrdinalIgnoreCase)) + { + task.SetParameter("Projects", "@(ProjectReference->Reverse())"); + } + else + { + task.SetParameter("Projects", "@(ProjectReference)"); // The references already have the tools versions and properties set on them. + } + + if (targetToBuild != null) + { + task.SetParameter("Targets", targetToBuild); + } + + task.SetParameter("BuildInParallel", "True"); + task.SetParameter("Properties", SolutionProperties); + + // We only want to build "nonexistent" projects if we're building metaprojects, since they don't exist on disk. Otherwise, + // we still want to error when the referenced project doesn't exist. + task.SetParameter("SkipNonexistentProjects", "%(ProjectReference.SkipNonexistentProjects)"); + + if (outputItem != null) + { + task.AddOutputItem("TargetOutputs", outputItem, String.Empty); + } + } + + /// + /// Adds a traversal target which invokes a specified target on a single project. This creates targets called "Project", "Project:Rebuild", "Project:Clean", "Project:Publish" etc. + /// + private void AddTraversalTargetForProject(ProjectInstance traversalProject, ProjectInSolution project, ProjectConfigurationInSolution projectConfiguration, string targetToBuild, string outputItem, bool canBuildDirectly) + { + string baseProjectName = ProjectInSolution.DisambiguateProjectTargetName(project.GetUniqueProjectName()); + string actualTargetName = baseProjectName; + if (targetToBuild != null) + { + actualTargetName += ":" + targetToBuild; + } + + // The output item name is the concatenation of the project name with the specified outputItem. In the typical case, if the + // project name is MyProject, the outputItemName will be MyProjectBuildOutput, and the outputItemAsItem will be @(MyProjectBuildOutput). + // In the case where the project contains characters not allowed as Xml element attribute values, those characters will + // be replaced with underscores. In the case where MyProject is actually unrepresentable in Xml, then the + // outputItemName would be _________BuildOutput. + string outputItemName = null; + string outputItemAsItem = null; + if (!String.IsNullOrEmpty(outputItem)) + { + outputItemName = MakeIntoSafeItemName(baseProjectName) + outputItem; + outputItemAsItem = "@(" + outputItemName + ")"; + } + + ProjectTargetInstance targetElement = traversalProject.AddTarget(actualTargetName, null, null, outputItemAsItem, null, null, null, false /* legacy target returns behaviour */); + if (canBuildDirectly) + { + AddProjectBuildTask(traversalProject, project, projectConfiguration, targetElement, targetToBuild, "@(ProjectReference)", "'%(ProjectReference.Identity)' == '" + EscapingUtilities.Escape(project.AbsolutePath) + "'", outputItemName); + } + else + { + AddMetaprojectBuildTask(traversalProject, project, targetElement, targetToBuild, outputItemName); + } + } + + /// + /// Retrieves a dictionary representing the global properties which should be transferred to a metaproject. + /// + /// The traversal from which the global properties should be obtained. + /// A dictionary of global properties. + private IDictionary GetMetaprojectGlobalProperties(ProjectInstance traversalProject) + { + Dictionary properties = new Dictionary(_metaprojectGlobalProperties.Length, StringComparer.OrdinalIgnoreCase); + foreach (Tuple property in _metaprojectGlobalProperties) + { + if (property.Item2 == null) + { + properties[property.Item1] = EscapingUtilities.Escape(traversalProject.GetPropertyValue(property.Item1)); + } + else + { + properties[property.Item1] = EscapingUtilities.Escape(property.Item2); + } + } + + // Now provide any which are explicitly set on the solution + foreach (ProjectPropertyInstance globalProperty in traversalProject.GlobalPropertiesDictionary) + { + properties[globalProperty.Name] = ((IProperty)globalProperty).EvaluatedValueEscaped; + } + + // If we have a sub-toolset version, it will be set on the P2P from the solution metaproj, so we need + // to make sure it's set here, too, so the global properties will match. + if (traversalProject.SubToolsetVersion != null) + { + if (traversalProject.SubToolsetVersion.Equals("4.0", StringComparison.OrdinalIgnoreCase)) + { + properties[Constants.SubToolsetVersionPropertyName] = traversalProject.SubToolsetVersion; + } + } + + return properties; + } + + /// + /// Figures out what the ToolsVersion should be for child projects (used when scanning + /// for dependencies) + /// + private string DetermineChildProjectToolsVersion(string wrapperProjectToolsVersion) + { + string childProjectToolsVersion = null; + + _globalProperties.TryGetValue("ProjectToolsVersion", out childProjectToolsVersion); + + if (childProjectToolsVersion == null) + { + childProjectToolsVersion = wrapperProjectToolsVersion; + } + + return childProjectToolsVersion; + } + + /// + /// Normally the active solution configuration/platform is determined when we build the solution + /// wrapper project, not when we create it. However, we need to know them to scan project references + /// for the right project configuration/platform. It's unlikely that references would be conditional, + /// but still possible and we want to get that case right. + /// + private string PredictActiveSolutionConfigurationName() + { + return PredictActiveSolutionConfigurationName(_solutionFile, _globalProperties); + } + + /// + /// Loads each MSBuild project in this solution and looks for its project-to-project references so that + /// we know what build order we should use when building the solution. + /// + private void ScanProjectDependencies(string childProjectToolsVersion, string fullSolutionConfigurationName) + { + // Don't bother with all this if the solution configuration doesn't even exist. + if (fullSolutionConfigurationName == null) + { + return; + } + + foreach (ProjectInSolution project in _solutionFile.ProjectsInOrder) + { + // We only need to scan .wdproj projects: Everything else is either MSBuildFormat or + // something we don't know how to do anything with anyway + if (project.ProjectType == SolutionProjectType.WebDeploymentProject) + { + // Skip the project if we don't have its configuration in this solution configuration + if (!project.ProjectConfigurations.ContainsKey(fullSolutionConfigurationName)) + { + continue; + } + + try + { + Project msbuildProject = new Project(project.AbsolutePath, _globalProperties, childProjectToolsVersion); + + // ProjectDependency items work exactly like ProjectReference items from the point of + // view of determining that project B depends on project A. This item must cause + // project A to be built prior to project B. + // + // This has the format + // + // {GUID} + // + IEnumerable references = msbuildProject.GetItems("ProjectDependency"); + + foreach (ProjectItem reference in references) + { + string referencedProjectGuid = reference.GetMetadataValue("Project"); + AddDependencyByGuid(project, referencedProjectGuid); + } + + // If this is a web deployment project, we have a reference specified as a property + // "SourceWebProject" rather than as a ProjectReference item. This has the format + // {GUID}|PATH_TO_CSPROJ + // where + // GUID is the project guid for the "source" project + // PATH_TO_CSPROJ is the solution-relative path to the csproj file. + // + // NOTE: This is obsolete and is intended only for backward compatability with + // Whidbey-generated web deployment projects. New projects should use the + // ProjectDependency item above. + string referencedWebProjectGuid = msbuildProject.GetPropertyValue("SourceWebProject"); + if (!string.IsNullOrEmpty(referencedWebProjectGuid)) + { + // Grab the guid with its curly braces... + referencedWebProjectGuid = referencedWebProjectGuid.Substring(0, 38); + AddDependencyByGuid(project, referencedWebProjectGuid); + } + } + catch (Exception e) + { + // We don't want any problems scanning the project file to result in aborting the build. + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + _loggingService.LogWarning + ( + _projectBuildEventContext, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(project.RelativePath), + "SolutionScanProjectDependenciesFailed", + project.RelativePath, + e.Message + ); + } + } + } + } + + /// + /// Adds a dependency to the project based on the specified guid string. + /// + /// + /// If the string is null or empty, no dependency is added and this is not considered an error. + /// + private void AddDependencyByGuid(ProjectInSolution project, string dependencyGuid) + { + if (!String.IsNullOrEmpty(dependencyGuid)) + { + if (_solutionFile.ProjectsByGuid.ContainsKey(dependencyGuid)) + { + project.AddDependency(dependencyGuid); + } + else + { + _loggingService.LogWarning + ( + _projectBuildEventContext, + "SubCategoryForSolutionParsingErrors", + new BuildEventFileInfo(_solutionFile.FullPath), + "SolutionParseProjectDepNotFoundError", + project.ProjectGuid, + dependencyGuid + ); + } + } + } + + /// + /// Creates default Configuration and Platform values based on solution configurations present in the solution + /// + private void AddConfigurationPlatformDefaults(ProjectRootElement traversalProject) + { + ProjectPropertyGroupElement configurationDefaultingPropertyGroup = traversalProject.CreatePropertyGroupElement(); + traversalProject.AppendChild(configurationDefaultingPropertyGroup); + + configurationDefaultingPropertyGroup.Condition = " '$(Configuration)' == '' "; + configurationDefaultingPropertyGroup.AddProperty("Configuration", EscapingUtilities.Escape(_solutionFile.GetDefaultConfigurationName())); + + ProjectPropertyGroupElement platformDefaultingPropertyGroup = traversalProject.CreatePropertyGroupElement(); + traversalProject.AppendChild(platformDefaultingPropertyGroup); + + platformDefaultingPropertyGroup.Condition = " '$(Platform)' == '' "; + platformDefaultingPropertyGroup.AddProperty("Platform", EscapingUtilities.Escape(_solutionFile.GetDefaultPlatformName())); + } + + /// + /// Adds a new property group with contents of the given solution configuration to the project. + /// + private void AddPropertyGroupForSolutionConfiguration(ProjectRootElement traversalProject, SolutionConfigurationInSolution solutionConfiguration) + { + AddPropertyGroupForSolutionConfiguration(traversalProject, _solutionFile, solutionConfiguration); + } + + /// + /// Creates the default Venus configuration property based on the selected solution configuration. + /// Unfortunately, Venus projects only expose one project configuration in the IDE (Debug) although + /// they allow building Debug and Release from command line. This means that if we wanted to use + /// the project configuration from the active solution configuration for Venus projects, we'd always + /// end up with Debug and there'd be no way to build the Release configuration. To work around this, + /// we use a special mechanism for choosing ASP.NET project configuration: we set it to Release if + /// we're building a Release solution configuration, and to Debug if we're building a Debug solution + /// configuration. The property is also settable from the command line, in which case it takes + /// precedence over this algorithm. + /// + private void AddVenusConfigurationDefaults(ProjectRootElement traversalProject) + { + ProjectPropertyGroupElement venusConfiguration = traversalProject.CreatePropertyGroupElement(); + traversalProject.AppendChild(venusConfiguration); + + venusConfiguration.Condition = " ('$(AspNetConfiguration)' == '') "; + venusConfiguration.AddProperty("AspNetConfiguration", "$(Configuration)"); + } + + /// + /// Adds solution related build event macros and other global properties to the wrapper project + /// + private void AddGlobalProperties(ProjectRootElement traversalProject) + { + ProjectPropertyGroupElement globalProperties = traversalProject.CreatePropertyGroupElement(); + traversalProject.AppendChild(globalProperties); + + string directoryName = _solutionFile.SolutionFileDirectory; + if (!directoryName.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) + { + directoryName += Path.DirectorySeparatorChar; + } + + globalProperties.AddProperty("SolutionDir", EscapingUtilities.Escape(directoryName)); + globalProperties.AddProperty("SolutionExt", EscapingUtilities.Escape(Path.GetExtension(_solutionFile.FullPath))); + globalProperties.AddProperty("SolutionFileName", EscapingUtilities.Escape(Path.GetFileName(_solutionFile.FullPath))); + globalProperties.AddProperty("SolutionName", EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(_solutionFile.FullPath))); + + globalProperties.AddProperty("SolutionPath", EscapingUtilities.Escape(Path.Combine(_solutionFile.SolutionFileDirectory, Path.GetFileName(_solutionFile.FullPath)))); + + // Add other global properties + ProjectPropertyGroupElement frameworkVersionProperties = traversalProject.CreatePropertyGroupElement(); + traversalProject.AppendChild(frameworkVersionProperties); + + // Set the property "TargetFrameworkVersion". This is needed for the GetFrameworkPath target. + // If TargetFrameworkVersion is already set by the user, use that value. + // Otherwise if MSBuildToolsVersion is 2.0, use "v2.0" + // Otherwise if MSBuildToolsVersion is 3.5, use "v3.5" + // Otherwise use "v4.0". + ProjectPropertyElement tfv20Property = frameworkVersionProperties.AddProperty("TargetFrameworkVersion", "v2.0"); + ProjectPropertyElement tfv35Property = frameworkVersionProperties.AddProperty("TargetFrameworkVersion", "v3.5"); + ProjectPropertyElement tfv40Property = frameworkVersionProperties.AddProperty("TargetFrameworkVersion", "v4.0"); + tfv20Property.Condition = "'$(TargetFrameworkVersion)' == '' and '$(MSBuildToolsVersion)' == '2.0'"; + tfv35Property.Condition = "'$(TargetFrameworkVersion)' == '' and ('$(MSBuildToolsVersion)' == '3.5' or '$(MSBuildToolsVersion)' == '3.0')"; + tfv40Property.Condition = "'$(TargetFrameworkVersion)' == '' and !('$(MSBuildToolsVersion)' == '3.5' or '$(MSBuildToolsVersion)' == '3.0' or '$(MSBuildToolsVersion)' == '2.0')"; + } + + /// + /// Special hack for web projects. It can happen that there is no Release configuration for solutions + /// containing web projects, yet we still want to be able to build the Release configuration for + /// those projects. Since the ASP.NET project configuration defaults to the solution configuration, + /// we allow Release even if it doesn't actually exist in the solution. + /// + private void AddFakeReleaseSolutionConfigurationIfNecessary() + { + if (_solutionFile.ContainsWebProjects) + { + bool solutionHasReleaseConfiguration = false; + foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionFile.SolutionConfigurations) + { + if (string.Compare(solutionConfiguration.ConfigurationName, "Release", StringComparison.OrdinalIgnoreCase) == 0) + { + solutionHasReleaseConfiguration = true; + break; + } + } + + if ((!solutionHasReleaseConfiguration) && (_solutionFile.SolutionConfigurations.Count > 0)) + { + _solutionFile.AddSolutionConfiguration("Release", _solutionFile.GetDefaultPlatformName()); + } + } + } + + /// + /// Adds the initial target to the solution wrapper project, necessary for a few message/error tags + /// + private void AddInitialTargets(ProjectInstance traversalProject, List projects) + { + AddValidateSolutionConfigurationTarget(traversalProject); + AddValidateToolsVersionsTarget(traversalProject); + AddValidateProjectsTarget(traversalProject, projects); + AddGetSolutionConfigurationContentsTarget(traversalProject); + } + + /// + /// Adds the target which validates that the solution configuration specified by the user is supported. + /// + private void AddValidateSolutionConfigurationTarget(ProjectInstance traversalProject) + { + ProjectTargetInstance initialTarget = traversalProject.AddTarget("ValidateSolutionConfiguration", null, null, null, null, null, null, false /* legacy target returns behaviour */); + + if (_solutionFile.SolutionConfigurations.Count > 0) + { + ProjectTaskInstance errorTask = AddErrorWarningMessageInstance + ( + initialTarget, + "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' != 'true')", + XMakeElements.error, + false /* do not treat as literal */, + "SolutionInvalidSolutionConfiguration", + "$(Configuration)|$(Platform)" + ); + + ProjectTaskInstance warningTask = AddErrorWarningMessageInstance + ( + initialTarget, + "('$(CurrentSolutionConfigurationContents)' == '') and ('$(SkipInvalidConfigurations)' == 'true')", + XMakeElements.warning, + false /* do not treat as literal */, + "SolutionInvalidSolutionConfiguration", + "$(Configuration)|$(Platform)" + ); + + ProjectTaskInstance messageTask = AddErrorWarningMessageInstance + ( + initialTarget, + "'$(CurrentSolutionConfigurationContents)' != ''", + XMakeElements.message, + false /* do not treat as literal */, + "SolutionBuildingSolutionConfiguration", + "$(Configuration)|$(Platform)" + ); + } + } + + /// + /// Adds the target which validates that the tools version is supported. + /// + private void AddValidateToolsVersionsTarget(ProjectInstance traversalProject) + { + ProjectTargetInstance validateToolsVersionsTarget = traversalProject.AddTarget("ValidateToolsVersions", null, null, null, null, null, null, false /* legacy target returns behaviour */); + ProjectTaskInstance toolsVersionErrorTask = AddErrorWarningMessageInstance + ( + validateToolsVersionsTarget, + "'$(MSBuildToolsVersion)' == '2.0' and ('$(ProjectToolsVersion)' != '2.0' and '$(ProjectToolsVersion)' != '')", + XMakeElements.error, + false /* do not treat as literal */, + "SolutionToolsVersionDoesNotSupportProjectToolsVersion", + "$(MSBuildToolsVersion)" + ); + } + + /// Adds the target to fetch solution configuration contents for given configuration|platform combo. + private void AddGetSolutionConfigurationContentsTarget(ProjectInstance traversalProject) + { + var initialTarget = traversalProject.AddTarget( + targetName: "GetSolutionConfigurationContents", + condition: null, + inputs: null, + outputs: "$(SolutionConfigurationContents)", + returns: null, + keepDuplicateOutputs: null, + dependsOnTargets: null, + parentProjectSupportsReturnsAttribute: false); + + var property = new ProjectPropertyGroupTaskPropertyInstance( + "SolutionConfigurationContents", + "@(SolutionConfiguration->WithMetadataValue('Identity', '$(Configuration)|$(Platform)')->'%(Content)')", + string.Empty, + initialTarget.Location, + initialTarget.Location); + + initialTarget.AddProjectTargetInstanceChild(new ProjectPropertyGroupTaskInstance( + string.Empty, + initialTarget.Location, + initialTarget.Location, + new ProjectPropertyGroupTaskPropertyInstance[] { property })); + } + + #endregion // Methods + } +} diff --git a/src/XMakeBuildEngine/Construction/UsingTaskParameterGroupElement.cs b/src/XMakeBuildEngine/Construction/UsingTaskParameterGroupElement.cs new file mode 100644 index 00000000000..57bdf36c3da --- /dev/null +++ b/src/XMakeBuildEngine/Construction/UsingTaskParameterGroupElement.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of UsingTaskParameterGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; + +namespace Microsoft.Build.Construction +{ + /// + /// UsingTaskParameterGroupElement represents a ParameterGroup under the using task. + /// + [DebuggerDisplay("#Parameters={Count}")] + public class UsingTaskParameterGroupElement : ProjectElementContainer + { + /// + /// Initialize a parented UsingTaskParameterGroupElement + /// + internal UsingTaskParameterGroupElement(XmlElementWithLocation xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + VerifyCorrectParent(parent); + } + + /// + /// Initialize an unparented UsingTaskParameterGroupElement + /// + private UsingTaskParameterGroupElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { + } + + /// + /// Condition should never be set, but the getter returns null instead of throwing + /// because a nonexistent condition is implicitly true + /// + public override string Condition + { + get + { + return null; + } + + set + { + ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition"); + } + } + + /// + /// Get any contained parameters. + /// + public ICollection Parameters + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// This does not allow conditions, so it should not be called. + /// + public override ElementLocation ConditionLocation + { + get + { + ErrorUtilities.ThrowInternalError("Should not evaluate this"); + return null; + } + } + + #region Add parameters + /// + /// Convenience method that picks a location based on a heuristic: + /// + public ProjectUsingTaskParameterElement AddParameter(string name, string output, string required, string parameterType) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + ProjectUsingTaskParameterElement newParameter = ContainingProject.CreateUsingTaskParameterElement(name, output, required, parameterType); + AppendChild(newParameter); + + return newParameter; + } + + /// + /// Convenience method that picks a location based on a heuristic: + /// + public ProjectUsingTaskParameterElement AddParameter(string name) + { + return AddParameter(name, String.Empty, String.Empty, String.Empty); + } + #endregion + + /// + /// Creates an unparented UsingTaskParameterGroupElement, wrapping an unparented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static UsingTaskParameterGroupElement CreateDisconnected(ProjectRootElement containingProject) + { + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.usingTaskParameterGroup); + + return new UsingTaskParameterGroupElement(element, containingProject); + } + + /// + /// Overridden to verify that the potential parent and siblings + /// are acceptable. Throws InvalidOperationException if they are not. + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling) + { + VerifyCorrectParent(parent); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateUsingTaskParameterGroupElement(); + } + + /// + /// Verify the parent is a usingTaskElement and that the taskFactory attribute is set + /// + private static void VerifyCorrectParent(ProjectElementContainer parent) + { + ProjectUsingTaskElement parentUsingTask = parent as ProjectUsingTaskElement; + ErrorUtilities.VerifyThrowInvalidOperation(parentUsingTask != null, "OM_CannotAcceptParent"); + + // Now since there is not goign to be a TaskElement on the using task we need to validate and make sure there is a TaskFactory attribute on the parent element and + // that it is not empty + if (parentUsingTask.TaskFactory.Length == 0) + { + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(parent.XmlElement, "TaskFactory"); + } + + // UNDONE: Do check to make sure the parameter group is the first child + } + } +} diff --git a/src/XMakeBuildEngine/Debugger/DebuggerLocalType.cs b/src/XMakeBuildEngine/Debugger/DebuggerLocalType.cs new file mode 100644 index 00000000000..0a609ea66b6 --- /dev/null +++ b/src/XMakeBuildEngine/Debugger/DebuggerLocalType.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Description of a local type for a debugger local. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Shared; +using System.Diagnostics; + +namespace Microsoft.Build.Debugging +{ + /// + /// Immutable class to describe the name and type for an early bound local + /// +#if JMC + [DebuggerNonUserCode] +#endif + internal struct DebuggerLocalType + { + /// + /// Name of the local variable. + /// + private string _name; + + /// + /// Type of the local variable. + /// + private Type _type; + + /// + /// Constructor + /// + internal DebuggerLocalType(string name, Type type) + { + ErrorUtilities.VerifyThrowInternalLength(name, "name"); + ErrorUtilities.VerifyThrowInternalNull(type, "type"); + + _name = name; + _type = type; + } + + /// + /// Name of the local variable. + /// + internal string Name + { + get { return _name; } + } + + /// + /// Type of the local variable. + /// + internal Type Type + { + get { return _type; } + } + } +} diff --git a/src/XMakeBuildEngine/Debugger/DebuggerManager.cs b/src/XMakeBuildEngine/Debugger/DebuggerManager.cs new file mode 100644 index 00000000000..5c908599f46 --- /dev/null +++ b/src/XMakeBuildEngine/Debugger/DebuggerManager.cs @@ -0,0 +1,908 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Provides debugging support for state machines. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.SymbolStore; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Debugging +{ + /// + /// Manager for supporting debugging a state machine. + /// This is for internal use by MSBuild, only. + /// + /// + /// This is using the theory described at: + /// http://blogs.msdn.com/jmstall/archive/2005/07/27/state-machine-theory.aspx. + /// The summary is that it emits IL snippets ("Islands") for each state in the machine. + /// The island serves as a spot to set a breakpoint. + /// + /// You should be able to set breakpoints on states and hit them. + /// To do stepping between states: + /// - ensure the interpreter is non-user code (perhaps by placing the [DebuggerNonUserCode] attribute + /// on all the classes, or not providing the symbols to the debugger) + /// - ensure Just-My-Code is turned on. In VS2005, + /// this is at: Tools > Options > Debugging > General, "Enable Just My Code". + /// - Use step-in (F11) between states. + /// + /// + /// The general usage is to call: + /// - DefineState() for each state + /// - Bake() once you've defined all the states you need to enter. + /// - EnterState() / LeaveState() for each state. + /// You can Define new states and bake them, such as if the script loads a new file. + /// Baking is expensive, so it's best to define as many states in each batch. + /// + /// UNDONE: Show proper state of items and properties set and modified within targets. + /// UNDONE: Characterization and fixing of debugging multiproc MSBuild, and MSBuild hosted by VS. + /// +#if JMC + [DebuggerNonUserCode] +#endif + public static class DebuggerManager + { + /// + /// Whether debugging should break on startup. + /// This is normally true, but setting it to false + /// might be useful in some situations, such as multiproc build. + /// + private static bool s_breakOnStartup; + + /// + /// Whether debugging is enabled. This is not normally + /// enabled as it makes everything slow. + /// + private static bool? s_debuggingEnabled; + + /// + /// The states that the debugger may be in, indexed + /// by their location. All baked states are in here. + /// + private static IDictionary s_allBakedStates = new Dictionary(); + + /// + /// Method that islands call back to. + /// + private static MethodInfo s_islandCallback; + + /// + /// Cached mapping of file path to symbol store documents + /// + private static Dictionary s_sources = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The single dynamic module used. + /// + private static ModuleBuilder s_dynamicModule; + + /// + /// Islands are executed on an auxiliary thread instead of the main thread. + /// This gives a better default stepping experience (allows Step-in, step-over, step-out), + /// and also allows unloading the islands (since the thread can be in a separate appdomain). + /// + private static IslandThread s_islandThread; + + /// + /// List of all state that have been created with DefineState + /// and are yet to be Baked into types. + /// We use a hashtable instead of a list so that we can find duplicate + /// state names immediately. + /// + private static Dictionary s_unbakedStates = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// In special cases, we ignore an EnterState, and increment this counter + /// so that we can ignore the matching LeaveState. + /// + private static int s_skippedEnters; + + /// + /// Whether the debugger manager has been initialized yet. + /// + private static bool s_initialized; + + /// + /// Type of delegate used by the debugger worker thread to call back to invoke an island + /// + internal delegate void InvokeIslandDelegate(object argument, VirtualStackFrame stackFrame); + + /// + /// Whether debugging of project files is enabled. + /// By default it is not. + /// + internal static bool DebuggingEnabled + { + get + { + if (!s_debuggingEnabled.HasValue) + { + s_debuggingEnabled = String.Equals(Environment.GetEnvironmentVariable("MSBUILDDEBUGGING"), "1", StringComparison.OrdinalIgnoreCase); + } + + return s_debuggingEnabled.Value; + } + } + + /// + /// Stop debugging thread. + /// This may not necessarily unload islands or dynamic modules that were created until the calling appdomain has exited. + /// UNDONE: Call this. Otherwise we still exit cleanly, just only when the process exits. + /// + internal static void Terminate() + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + + if (s_islandThread != null) + { + s_islandThread.Exit(); + s_islandThread = null; + } + } + + /// + /// Declare a new state associated with the given source location. + /// States should (probably) have disjoint source locations. + /// Define as many states as possible using this method before calling Bake(). + /// Location must map to a unique state within the type in which it is baked. + /// Name of the state will showup in the callstack as if it was a method name. Must be unique within the type in which it is baked. + /// Early-bound locals are arbitrary types whose values will be supplied on EnterState. May be null. + /// + internal static void DefineState(ElementLocation location, string name, ICollection earlyLocals) + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + + // Special case: elements added by editing, such as in a solution wrapper project, + // do not have line numbers. Such files cannot be debugged, so we special case + // such locations by doing nothing. + if (location.Line == 0) + { + return; + } + + ErrorUtilities.VerifyThrow(!s_unbakedStates.ContainsKey(name), "Need unique debug state name, already seen '{0}'", name); + + DebuggerState state = new DebuggerState(location, name, earlyLocals); + s_unbakedStates.Add(name, state); + } + + /// + /// Bake all unbaked states. States must be baked before calling EnterState(). + /// Islands are created in a type with the specified name. + /// File name is to show up on the callstack as the type name: "ASSEMBLYNAME!FILENAME.STATENAME(...LOCALS...)" + /// If the type name is not unique, it will be appended with a unique identifier. + /// + internal static void BakeStates(string fileName) + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + + // We may have baked no states if all states were in a file + // for which we did not have detailed location information + if (s_unbakedStates.Count == 0) + { + return; + } + + // Default assembly name, eg., for unnamed projects + fileName = fileName ?? "MSBuild"; + + int suffix = 0; + while (s_dynamicModule.GetType(fileName) != null) + { + fileName += suffix; + suffix++; + } + + TypeBuilder type = s_dynamicModule.DefineType(fileName, TypeAttributes.Public | TypeAttributes.Class); + + foreach (DebuggerState state in s_unbakedStates.Values) + { + if (s_allBakedStates.ContainsKey(state.Location)) + { + // This will happen if it is an import loaded by more than one project + continue; + } + + string methodName = CreateIsland(type, state); + + state.RecordMethodInfo(type, methodName); + + s_allBakedStates.Add(state.Location, state); + } + + s_unbakedStates = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Although type is going out of scope now, it will + // subsequently be accessed by its name + type.CreateType(); + } + + /// + /// Enter a state and push it onto the 'virtual callstack'. + /// If the user set a a breakpoint at the source location associated with + /// this state, this call will hit that breakpoint. + /// Call LeaveState when the interpreter is finished with this state. + /// State must have already been defined. + /// + /// + /// Location of state to enter, used to look up the state. + /// + /// + /// Local variables associated with this state, matching by index with the types + /// passed into DefineState. The debugger will show the names, types, and values. + /// + /// + /// EnterState can be called reentrantly. If code calls Enter(A); Enter(B); Enter(C); + /// Then on the call to Enter(C), the virtual callstack will be A-->B-->C. + /// Each call to Enter() will rebuild the virtual callstack. + /// + internal static void EnterState(ElementLocation location, IDictionary locals) + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + + // Special case: elements added by editing, such as in a solution wrapper project, + // do not have line numbers. Such files cannot be debugged, so we special case + // such locations by doing nothing. + if (location.Line == 0) + { + s_skippedEnters++; + return; + } + + DebuggerState state; + ErrorUtilities.VerifyThrow(s_allBakedStates.TryGetValue(location, out state), "No state defined and baked for location {0}", location.LocationString); + + s_islandThread.EnterState(state, locals); + } + + /// + /// Enter and immediately leave a state, so that any breakpoint can be hit. + /// + internal static void PulseState(ElementLocation location, IDictionary locals) + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + + EnterState(location, locals); + LeaveState(location); + } + + /// + /// Break in the current state last set by EnterState(). + /// An interpreter could call this to + /// implement a "data breakpoint". + /// + internal static void Break() + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + + s_islandThread.Break(); + } + + /// + /// Pop the state most recently pushed by EnterState. + /// The identifier (location) of a Leave must match the Enter at the top of the stack, + /// to catch mismatched Leaves. + /// + internal static void LeaveState(ElementLocation location) + { + ErrorUtilities.VerifyThrow(s_initialized, "Not initialized"); + ErrorUtilities.VerifyThrow(DebuggingEnabled, "Debugging not enabled"); + ErrorUtilities.VerifyThrow(s_skippedEnters >= 0, "Left too many"); + + // Special case: elements added by editing, such as in a solution wrapper project, + // do not have line numbers. Such files cannot be debugged, so we special case + // such locations by doing nothing. + if (s_skippedEnters > 0) + { + s_skippedEnters--; + return; + } + + s_islandThread.LeaveState(location); + } + + /// + /// Starts debugger worker thread immediately, if debugging is enabled. + /// This must not be called by a static constructor, as the + /// time at which it is called will then be undefined, and + /// the debugging environment variable might not have had a + /// chance to be set. + /// + internal static void Initialize() + { + if (s_islandThread == null) + { + if (DebuggingEnabled) + { + Trace.WriteLine("MSBuild debugging enabled"); + + s_breakOnStartup = !String.Equals(Environment.GetEnvironmentVariable("MSBUILDDONOTBREAKONSTARTUP"), "1", StringComparison.OrdinalIgnoreCase); + + CreateDynamicModule(); + + s_islandCallback = typeof(IslandThread).GetMethod("IslandWorker", BindingFlags.Static | BindingFlags.Public); + s_islandThread = new IslandThread(InvokeIsland /* delegate to invoke an island */, s_breakOnStartup); + } + } + + s_initialized = true; + } + + /// + /// Create the single dynamic module that will + /// contain all our types and states. + /// + /// + /// Emits the module into the current appdomain. + /// This could be improved to use another appdomain so that all + /// the types could be unloaded. All locals would have to be + /// marshalable in this case. + /// + private static void CreateDynamicModule() + { + // See http://blogs.msdn.com/jmstall/archive/2005/02/03/366429.aspx for a simple example + // of debuggable reflection-emit. + ErrorUtilities.VerifyThrow(s_dynamicModule == null, "Already emitted"); + + // In a later release, this could be changed to use LightweightCodeGen (DynamicMethod instead of AssemblyBuilder); + // currently they don't support sequence points, so they can't be debugged in the normal way + AssemblyBuilder assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("msbuild"), AssemblyBuilderAccess.Run); + + // Mark generated code as debuggable. + // See http://blogs.msdn.com/rmbyers/archive/2005/06/26/432922.aspx for explanation. + ConstructorInfo constructor = typeof(DebuggableAttribute).GetConstructor(new Type[] { typeof(DebuggableAttribute.DebuggingModes) }); + + DebuggableAttribute.DebuggingModes debuggingMode = DebuggableAttribute.DebuggingModes.DisableOptimizations | + DebuggableAttribute.DebuggingModes.Default; + + CustomAttributeBuilder attribute = new CustomAttributeBuilder(constructor, new object[] { debuggingMode }); + assembly.SetCustomAttribute(attribute); + + // Arbitrary but reasonable name + string name = Process.GetCurrentProcess().ProcessName; + + s_dynamicModule = assembly.DefineDynamicModule(name, true /* track debug information */); + } + + /// + /// Create the representation of a single state, known as an "island". + /// It is implemented as a small method in the type being baked into the dynamic module. + /// Returns the name of the method. + /// + private static string CreateIsland(TypeBuilder typeBuilder, DebuggerState state) + { + // Parameters to the islands: + // 1. Island thread + // 2 ... N. list of early bound locals. + Type[] parameterTypes = new Type[1 + state.EarlyLocalsTypes.Count]; + parameterTypes[0] = typeof(IslandThread); + + int i = 1; + foreach (DebuggerLocalType local in state.EarlyLocalsTypes) + { + parameterTypes[i] = local.Type; + i++; + } + + MethodBuilder method = typeBuilder.DefineMethod + ( + state.Name /* method name */, + MethodAttributes.Static | MethodAttributes.Public, + typeof(void) /* return type */, + parameterTypes + ); + + // Define the parameter names. + // Do not define a parameter for the first parameter, as this is an + // implementation detail and we want to hide it from VS. + // Parameter 0 is the return type. + int j = 2; + foreach (DebuggerLocalType local in state.EarlyLocalsTypes) + { + method.DefineParameter(j, ParameterAttributes.None, local.Name); + j++; + } + + // Note that the locals are ignored by the method, they are only for the debugger to display; + // only the thread parameter is passed on. + + // void MethodName(IslandThread thread, ... early locals ... ) + // { + // .line + // nop + // call Worker(thread) + // ret; + // } + ILGenerator generator = method.GetILGenerator(); + + ISymbolDocumentWriter source; + if (!s_sources.TryGetValue(state.Location.File, out source)) + { + source = s_dynamicModule.DefineDocument(state.Location.File, Guid.Empty, Guid.Empty, Guid.Empty); + s_sources.Add(state.Location.File, source); + } + + // Lines may not be zero, columns may be zero + int line = (state.Location.Line == 0) ? 1 : state.Location.Line; + + generator.MarkSequencePoint(source, line, state.Location.Column, line, Int32.MaxValue); // mapping to source file + generator.Emit(OpCodes.Nop); // Can help with setting a breakpoint + + generator.Emit(OpCodes.Ldarg_0); // Load argument 0 that went to this method back onto the stack to pass to the call + generator.EmitCall(OpCodes.Call, s_islandCallback /* method */, null /* no opt params */); + + generator.Emit(OpCodes.Ret); // Return from state + + return method.Name; + } + + /// + /// Invoke an "island", marshaling the arguments. + /// Called on debugger worker thread. + /// + private static void InvokeIsland(Object islandThread, VirtualStackFrame frame) + { + Object[] arguments = new Object[1 + frame.State.EarlyLocalsTypes.Count]; + arguments[0] = islandThread; + + int i = 1; + foreach (DebuggerLocalType localType in frame.State.EarlyLocalsTypes) + { + object value; + ErrorUtilities.VerifyThrow(frame.Locals.TryGetValue(localType.Name, out value), "Didn't define value for {0}", localType.Name); + + arguments[i] = value; + i++; + } + + // ReflectionPermission perm = new ReflectionPermission(ReflectionPermissionFlag.MemberAccess); + // perm.Assert(); + // frame.State.MethodInfo.Invoke(null /* no instance */, BindingFlags.NonPublic | BindingFlags.Static, null /* default binder */, args2, null /* default culture */); + frame.State.Method.Invoke(null /* no instance */, arguments); + } + + /// + /// This is for internal use by MSBuild, only. + /// + /// + /// Executes the islands on a dedicated worker thread. The worker thread's + /// physical callstack then maps to the interpreter's virtual callstack. + /// +#if JMC + [DebuggerNonUserCode] +#endif + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Working to avoid this being public")] + public sealed class IslandThread : IDisposable + { + /// + /// Callback used to enter an island + /// + private InvokeIslandDelegate _invokeIsland; + + /// + /// Set to true to notify to Break on first instruction. This helps the F11 on startup experience. + /// Since the islands are on a new thread, there may be no user code on the main thread and so + /// F11 doesn't work. Thus the new worker thread needs to fire some break event. + /// This gets reset after the 'startup breakpoint'. + /// The initial Properties can override this. + /// + private bool _breakOnStartup; + + /// + /// Wrapped worker thread + /// + private Thread _workerThread; + + /// + /// Signalled when the main thread wants to send an event to the debugger worker thread. + /// The main thread fills out the data first. + /// + private AutoResetEvent _workToDoEvent; + + /// + /// Signalled by the worker thread when it's finished handling the event and + /// the main thread can resume. + /// + private AutoResetEvent _workDoneEvent; + + /// + /// Slot for passing operation to the worker thread + /// + private DebugAction _debugAction = DebugAction.Invalid; + + /// + /// Parameter for EnterState. + /// Stored on a stack only for verification that enters and leaves are matched. + /// + private Stack _virtualStack; + + /// + /// Constructor + /// + internal IslandThread(InvokeIslandDelegate invokeIsland, bool breakOnStartup) + { + _invokeIsland = invokeIsland; + + _breakOnStartup = breakOnStartup; + + _virtualStack = new Stack(); + + _workToDoEvent = new AutoResetEvent(false); + _workDoneEvent = new AutoResetEvent(false); + + _workerThread = new Thread(new ThreadStart(WorkerThreadProc)); + _workerThread.Name = "DebuggerWorker"; + _workerThread.IsBackground = true; // Don't prevent process exit + _workerThread.Start(); + } + + /// + /// Action for the thread to take + /// + private enum DebugAction + { + /// + /// Uninitialized + /// + Invalid, + + /// + /// Enter a state + /// + Enter, + + /// + /// Leave the current state + /// + Leave, + + /// + /// Stop execution + /// + Break + } + + /// + /// This is for internal use by MSBuild, only. + /// + /// + /// Private Entry point called from islands. Must be public so that the islands can invoke it. + /// UNDONE: Make this internal somehow. + /// Called on debugger worker thread. + /// + public static void IslandWorker(IslandThread controller) + { + controller.Worker(true); + } + + /// + /// IDisposable implementation. + /// + void IDisposable.Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Worker thread loop. + /// Called on debugger worker thread. + /// + internal void Worker(bool withinCallback) + { + if (withinCallback) + { + // Fire the 1-time "startup" breakpoint + // the first time we are entered from an island + if (_breakOnStartup) + { + Debugger.Launch(); + + _breakOnStartup = false; + } + + _workDoneEvent.Set(); // Done entering state + } + + // The final terminator is when leave returns, but from a recursive call. + while (true) + { + _workToDoEvent.WaitOne(); + switch (_debugAction) + { + case DebugAction.Enter: + _invokeIsland(this, _virtualStack.Peek()); + + // LeaveState() caused a return back to here + _workDoneEvent.Set(); // Done leaving state + break; + + case DebugAction.Leave: + // Back up the stack, and if this is the + // top of the stack, return out of + // this method. In that case workDoneEvent + // must be set by the caller + return; + + case DebugAction.Break: + if (!Debugger.IsAttached) + { + Trace.WriteLine("Triggering debugger attach"); + Debugger.Break(); + } + + _workDoneEvent.Set(); + break; + + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + } + } + + /// + /// Posts an Enter instruction to the island thread. + /// Called by debugger manager thread + /// + internal void EnterState(DebuggerState state, IDictionary locals) + { + _debugAction = DebugAction.Enter; + _virtualStack.Push(new VirtualStackFrame(state, locals)); + _workToDoEvent.Set(); + + // Block until Island executes NOP, + // giving BPs a chance to be hit. + // Must block here if the island is stopped at a breakpoint. + _workDoneEvent.WaitOne(); + } + + /// + /// Posts a Leave instruction to the island thread. + /// Called by debugger manager thread + /// If location is provided, verifies that the state being left is the state that was entered. + /// Stack may already be empty, in which case it is not modified. + /// + internal void LeaveState(ElementLocation location) + { + ErrorUtilities.VerifyThrow(location == null || location == _virtualStack.Peek().State.Location, "Mismatched leave was {0} expected {1}", location.LocationString, _virtualStack.Peek().State.Location.LocationString); + + _debugAction = DebugAction.Leave; + + if (_virtualStack.Count > 0) // May be falling out of the first enter + { + _virtualStack.Pop(); + } + + _workToDoEvent.Set(); + _workDoneEvent.WaitOne(); + } + + /// + /// Posts a Break instruction to the island thread. + /// Called by debugger manager thread. + /// + internal void Break() + { + _debugAction = DebugAction.Break; + + _workToDoEvent.Set(); + _workDoneEvent.WaitOne(); + + ((IDisposable)this).Dispose(); + } + + /// + /// Exit debugging. + /// Called by debugger manager thread. + /// + internal void Exit() + { + // Pop out of any existing stack + while (_virtualStack.Count >= 0) + { + LeaveState(null /* don't know what was the state */); + } + + // Add an unbalanced leave to make + // the debugger worker thread leave the threadproc. + LeaveState(null /* unbalanced */); + + _workerThread.Join(); + + _workToDoEvent.Close(); + _workDoneEvent.Close(); + } + + /// + /// The real disposer. + /// + private void Dispose(bool disposing) + { + if (disposing) + { + _workToDoEvent.Dispose(); + _workDoneEvent.Dispose(); + } + } + + /// + /// Threadproc. + /// Called on debugger worker thread. + /// + private void WorkerThreadProc() + { + Worker(false /* not within callback */); + + _workDoneEvent.Set(); // Done leaving state the last time + } + } + + /// + /// Describes a state in the interpreter. A state is any source location that + /// a breakpoint could be set on or that could be stepped to, such + /// as a line of code or a statement. + /// +#if JMC + [DebuggerNonUserCode] +#endif + internal class DebuggerState + { + /// + /// Type to later call GetMethod on + /// + private Type _type; + + /// + /// Name to later call GetMethod with + /// + private string _methodName; + + /// + /// Cached MethodInfo for the method for this state + /// + private MethodInfo _methodInfo; + + /// + /// Constructor. + /// State is given arbitrary provided name, which will appear in the debugger callstack: "ASSEMBLYNAME!FILENAME.STATENAME(...LOCALS...)" + /// Early locals are any locals whose names and types available at the time the state was created. May be null. + /// "Calling Type.GetMethod() is slow (10,000 calls can take ~1 minute). So defer that to later." + /// CALLED ONLY FROM THE DEBUGGER MANAGER. + /// + internal DebuggerState(ElementLocation location, string name, ICollection earlyLocalsTypes) + { + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + ErrorUtilities.VerifyThrowInternalLength(name, "name"); + + this.Location = location; + this.Name = name; + this.EarlyLocalsTypes = earlyLocalsTypes ?? ReadOnlyEmptyList.Instance; + } + + /// + /// Location in source file associated with this state. + /// SourceLocations for all the states should be disjoint. + /// + internal ElementLocation Location + { + get; + private set; + } + + /// + /// Friendly name of the state, such as method name. + /// Never null. + /// + internal string Name + { + get; + private set; + } + + /// + /// Type definitions for early bound locals. This list is ordered. + /// Names should be unique. + /// + internal ICollection EarlyLocalsTypes + { + get; + private set; + } + + /// + /// Method to call into this state. + /// Must be gotten on the debugger thread, otherwise + /// "NotSupportedException: The invoked member is not supported in a dynamic module." + /// + internal MethodInfo Method + { + get + { + ErrorUtilities.VerifyThrow(_type != null, "Didn't bake state '{0}'", Name); + + if (_methodInfo == null) + { + _methodInfo = _type.GetMethod(_methodName); + } + + return _methodInfo; + } + } + + /// + /// Record information necessary to find the method info from + /// the debugger thread. + /// CALLED ONLY FROM THE DEBUGGER MANAGER. + /// + internal void RecordMethodInfo(Type typeToRecord, string methodNameToRecord) + { + ErrorUtilities.VerifyThrow(_type == null, "already recorded type"); + ErrorUtilities.VerifyThrowInternalNull(typeToRecord, "typeToRecord"); + ErrorUtilities.VerifyThrowInternalLength(methodNameToRecord, "methodNameToRecord"); + + _type = typeToRecord; + _methodName = methodNameToRecord; + } + } + + /// + /// A virtual callstack frame for the interpreter. + /// This is created by calls to EnterState and LeaveState. + /// +#if JMC + [DebuggerNonUserCode] +#endif + internal class VirtualStackFrame + { + /// + /// Construct a stack frame for the given state with the given locals (both early and late bound). + /// + /// state for this stackframe + /// collection of all locals (both early and late) for this frame. May be null. + internal VirtualStackFrame(DebuggerState state, IDictionary locals) + { + ErrorUtilities.VerifyThrowInternalNull(state, "state"); + + State = state; + Locals = locals; + } + + /// + /// State for this frame. + /// + internal DebuggerState State + { + get; + private set; + } + + /// + /// All locals (both early-bound and late-bound) for this frame. + /// + internal IDictionary Locals + { + get; + private set; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Definition/BuiltInMetadata.cs b/src/XMakeBuildEngine/Definition/BuiltInMetadata.cs new file mode 100644 index 00000000000..7f9936af785 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/BuiltInMetadata.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Container for built-in metadata. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This class encapsulates the behavior and collection of built-in metadata. These metadatum + /// are inferred from the content of the include and sometimes the context of the project or + /// current directory. + /// + internal static class BuiltInMetadata + { + /// + /// Retrieves the count of built-in metadata. + /// + static internal int MetadataCount + { + [DebuggerStepThrough] + get + { return FileUtilities.ItemSpecModifiers.All.Length; } + } + + /// + /// Retrieves the list of metadata names. + /// + static internal ICollection MetadataNames + { + [DebuggerStepThrough] + get + { return FileUtilities.ItemSpecModifiers.All; } + } + + /// + /// Retrieves a built-in metadata value and caches it. + /// Never returns null. + /// + /// + /// The current directory for evaluation. Null if this is being called from a task, otherwise + /// it should be the project's directory. + /// + /// The evaluated include prior to wildcard expansion. + /// The evaluated include for the item. + /// The path to the project that defined this item + /// The name of the metadata. + /// The generated full path, for caching + /// The unescaped metadata value. + internal static string GetMetadataValue(string currentDirectory, string evaluatedIncludeBeforeWildcardExpansionEscaped, string evaluatedIncludeEscaped, string definingProjectEscaped, string name, ref string fullPath) + { + return EscapingUtilities.UnescapeAll(GetMetadataValueEscaped(currentDirectory, evaluatedIncludeBeforeWildcardExpansionEscaped, evaluatedIncludeEscaped, definingProjectEscaped, name, ref fullPath)); + } + + /// + /// Retrieves a built-in metadata value and caches it. + /// If value is not available, returns empty string. + /// + /// + /// The current directory for evaluation. Null if this is being called from a task, otherwise + /// it should be the project's directory. + /// + /// The evaluated include prior to wildcard expansion. + /// The evaluated include for the item. + /// The path to the project that defined this item + /// The name of the metadata. + /// The generated full path, for caching + /// The escaped as necessary metadata value. + internal static string GetMetadataValueEscaped(string currentDirectory, string evaluatedIncludeBeforeWildcardExpansionEscaped, string evaluatedIncludeEscaped, string definingProjectEscaped, string name, ref string fullPath) + { + // This is an assert, not a VerifyThrow, because the caller should already have done this check, and it's slow/hot. + Debug.Assert(FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name)); + + string value = null; + + if (String.Equals(name, FileUtilities.ItemSpecModifiers.RecursiveDir, StringComparison.OrdinalIgnoreCase)) + { + value = GetRecursiveDirValue(evaluatedIncludeBeforeWildcardExpansionEscaped, evaluatedIncludeEscaped); + } + else + { + value = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(currentDirectory, evaluatedIncludeEscaped, definingProjectEscaped, name, ref fullPath); + } + + return value; + } + + /// + /// Extract the value for "RecursiveDir", if any, from the Include. + /// If there is none, returns an empty string. + /// + /// + /// Inputs to and outputs of this function are all escaped. + /// + private static string GetRecursiveDirValue(string evaluatedIncludeBeforeWildcardExpansionEscaped, string evaluatedIncludeEscaped) + { + // If there were no wildcards, the two strings will be the same, and there is no recursivedir part. + if (String.Equals(evaluatedIncludeBeforeWildcardExpansionEscaped, evaluatedIncludeEscaped, StringComparison.OrdinalIgnoreCase)) + { + return String.Empty; + } + + // we're going to the file system, so unescape the include value here: + string evaluatedIncludeBeforeWildcardExpansion = EscapingUtilities.UnescapeAll(evaluatedIncludeBeforeWildcardExpansionEscaped); + string evaluatedInclude = EscapingUtilities.UnescapeAll(evaluatedIncludeEscaped); + + FileMatcher.Result match = FileMatcher.FileMatch(evaluatedIncludeBeforeWildcardExpansion, evaluatedInclude); + + if (match.isLegalFileSpec && match.isMatch) + { + return EscapingUtilities.Escape(match.wildcardDirectoryPart); + } + + return String.Empty; + } + } +} diff --git a/src/XMakeBuildEngine/Definition/Project.cs b/src/XMakeBuildEngine/Definition/Project.cs new file mode 100644 index 00000000000..098a2449566 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/Project.cs @@ -0,0 +1,3145 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents an definition model project. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Xml; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using ObjectModel = System.Collections.ObjectModel; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Constants = Microsoft.Build.Internal.Constants; +using Utilities = Microsoft.Build.Internal.Utilities; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ProjectItemFactory = Microsoft.Build.Evaluation.ProjectItem.ProjectItemFactory; +using System.Globalization; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Flags for controlling the project load. + /// + /// + /// This is a "flags" enum, allowing future settings to be added + /// in an additive, non breaking fashion. + /// + [Flags] + [SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue", Justification = "Public API. 'Default' is roughly equivalent to 'None'. ")] + public enum ProjectLoadSettings + { + /// + /// Normal load. This is the default. + /// + Default = 0, + + /// + /// Ignore nonexistent targets files when evaluating the project + /// + IgnoreMissingImports = 1, + + /// + /// Record imports including duplicate, but not circular, imports on the ImportsIncludingDuplicates property + /// + RecordDuplicateButNotCircularImports = 2, + + /// + /// Throw an exception and stop the evaluation of a project if any circular imports are detected + /// + RejectCircularImports = 4 + } + + /// + /// Represents an evaluated project with design time semantics. + /// Always backed by XML; can be built directly, or an instance can be cloned off to add virtual items/properties and build. + /// Edits to this project always update the backing XML. + /// + /// + /// UNDONE: (Multiple configurations.) Protect against problems when attempting to edit, after edits were made to the same ProjectRootElement either directly or through other projects evaluated from that ProjectRootElement. + /// + [DebuggerDisplay("{FullPath} EffectiveToolsVersion={ToolsVersion} #GlobalProperties={data.globalProperties.Count} #Properties={data.Properties.Count} #ItemTypes={data.ItemTypes.Count} #ItemDefinitions={data.ItemDefinitions.Count} #Items={data.Items.Count} #Targets={data.Targets.Count}")] + public class Project + { + /// + /// Whether to write information about why we evaluate to debug output. + /// + private static readonly bool s_debugEvaluation = (Environment.GetEnvironmentVariable("MSBUILDDEBUGEVALUATION") != null); + + /// + /// Backing XML object. + /// Can never be null: projects must always be backed by XML + /// + private readonly ProjectRootElement _xml; + + /// + /// Project collection in which this Project is a member. + /// All Project's are a member of exactly one ProjectCollection. + /// Their backing ProjectRootElement may be shared with Projects in another ProjectCollection. + /// + private readonly ProjectCollection _projectCollection = ProjectCollection.GlobalProjectCollection; + + /// + /// Context to log messages and events in + /// + private static BuildEventContext s_buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + + /// + /// The last used evaluation counter anywhere in this appdomain. + /// Used such that even after unload and reload, the evaluation counter changes. + /// + private static int s_globalEvaluationCounter; + + /// + /// Locking object. + /// + private static object s_locker = new Object(); + + /// + /// Backing data; stored in a nested class so it can be passed to the Evaluator to fill + /// in on re-evaluation, without having to expose property setters for that purpose. + /// Also it makes it easy to re-evaluate this project without creating a new project object. + /// + private Data _data; + + /// + /// The highest version of the backing ProjectRootElements (including imports) that this object was last evaluated from. + /// Edits to the ProjectRootElement either by this Project or another Project increment the number. + /// If that number is different from this one a reevaluation is necessary at some point. + /// + private int _evaluatedVersion; + + /// + /// The version of the tools information in the project collection against we were last evaluated. + /// + private int _evaluatedToolsetCollectionVersion; + + /// + /// The number of evaluations that have occurred to this project object since it was created. + /// Hosts don't know whether an evaluation actually happened in an interval, but they can compare this number to + /// their previously stored value to find out, and if so perhaps decide to update their own state. + /// + private int _evaluationCounter; + + /// + /// Whether the project has been explicitly marked as dirty. Generally this is not necessary to set; all edits affecting + /// this project will automatically make it dirty. However there are potential corner cases where it is necessary to mark it dirty + /// directly. For example, if the project has an import conditioned on a file existing on disk, and the file did not exist at + /// evaluation time, then someone subsequently writes the file, the project will not know that reevaluation would be productive, + /// and would not dirty itself. In such a case the host should help us by setting the dirty flag explicitly. + /// + private bool _explicitlyMarkedDirty; + + /// + /// This controls whether or not the building of targets/tasks is enabled for this + /// project. This is for security purposes in case a host wants to closely + /// control which projects it allows to run targets/tasks. + /// + private BuildEnabledSetting _isBuildEnabled = BuildEnabledSetting.UseProjectCollectionSetting; + + /// + /// The load settings, such as to ignore missing imports. + /// This is retained after construction as it will be needed for reevaluation. + /// + private ProjectLoadSettings _loadSettings; + + /// + /// The delegate registered with the ProjectRootElement to be called if the file name + /// is changed. Retained so that ultimately it can be unregistered. + /// If it has been set to null, the project has been unloaded from its collection. + /// + private RenameHandlerDelegate _renameHandler; + + /// + /// Construct an empty project, evaluating with the global project collection's + /// global properties and default tools version. + /// Project will be added to the global project collection when it is named. + /// + public Project() + : this(ProjectRootElement.Create(ProjectCollection.GlobalProjectCollection)) + { + } + + /// + /// Construct an empty project, evaluating with the specified project collection's + /// global properties and default tools version. + /// Project will be added to the specified project collection when it is named. + /// + public Project(ProjectCollection projectCollection) + : this(ProjectRootElement.Create(projectCollection), null, null, projectCollection) + { + } + + /// + /// Construct an empty project, evaluating with the specified project collection and + /// the specified global properties and default tools version, either of which may be null. + /// Project will be added to the specified project collection when it is named. + /// + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + public Project(IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection) + : this(ProjectRootElement.Create(projectCollection), globalProperties, toolsVersion, projectCollection) + { + } + + /// + /// Construct over a ProjectRootElement object, evaluating with the global project collection's + /// global properties and default tools version. + /// Project is added to the global project collection if it has a name, or else when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// ProjectRootElement to use + public Project(ProjectRootElement xml) + : this(xml, null, null) + { + } + + /// + /// Construct over a ProjectRootElement object, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project is added to the global project collection if it has a name, or else when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// ProjectRootElement to use + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + public Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion) + : this(xml, globalProperties, toolsVersion, ProjectCollection.GlobalProjectCollection) + { + } + + /// + /// Construct over a ProjectRootElement object, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project is added to the global project collection if it has a name, or else when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// ProjectRootElement to use + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + public Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection) + : this(xml, globalProperties, toolsVersion, projectCollection, ProjectLoadSettings.Default) + { + } + + /// + /// Construct over a ProjectRootElement object, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project is added to the global project collection if it has a name, or else when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// ProjectRootElement to use + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + public Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) + : this(xml, globalProperties, toolsVersion, null /* no explicit sub-toolset version */, projectCollection, loadSettings) + { + } + + /// + /// Construct over a ProjectRootElement object, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project is added to the global project collection if it has a name, or else when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// ProjectRootElement to use + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + /// Sub-toolset version to explicitly evaluate the toolset with. May be null. + public Project(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) + { + ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + _xml = xml; + _projectCollection = projectCollection; + + Initialize(globalProperties, toolsVersion, subToolsetVersion, loadSettings); + } + + /// + /// Construct over a text reader over project xml, evaluating with the global project collection's + /// global properties and default tools version. + /// Project will be added to the global project collection when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// Xml reader to read project from + public Project(XmlReader xmlReader) + : this(xmlReader, null, null) + { + } + + /// + /// Construct over a text reader over project xml, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project will be added to the global project collection when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// Xml reader to read project from + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + public Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion) + : this(xmlReader, globalProperties, toolsVersion, ProjectCollection.GlobalProjectCollection) + { + } + + /// + /// Construct over a text reader over project xml, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project will be added to the specified project collection when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// Xml reader to read project from + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + /// The collection with which this project should be associated. May not be null. + public Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection) + : this(xmlReader, globalProperties, toolsVersion, projectCollection, ProjectLoadSettings.Default) + { + } + + /// + /// Construct over a text reader over project xml, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project will be added to the specified project collection when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// Xml reader to read project from + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + /// The collection with which this project should be associated. May not be null. + public Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) + : this(xmlReader, globalProperties, toolsVersion, null /* no explicit sub-toolset version */, projectCollection, loadSettings) + { + } + + /// + /// Construct over a text reader over project xml, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project will be added to the specified project collection when it is named. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// + /// Xml reader to read project from + /// Global properties to evaluate with. May be null in which case the containing project collection's global properties will be used. + /// Tools version to evaluate with. May be null + /// Sub-toolset version to explicitly evaluate the toolset with. May be null. + /// The collection with which this project should be associated. May not be null. + /// The load settings for this project. + public Project(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) + { + ErrorUtilities.VerifyThrowArgumentNull(xmlReader, "xmlReader"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + _projectCollection = projectCollection; + + try + { + _xml = ProjectRootElement.Create(xmlReader, projectCollection); + } + catch (InvalidProjectFileException ex) + { + LoggingService.LogInvalidProjectFileError(s_buildEventContext, ex); + throw; + } + + Initialize(globalProperties, toolsVersion, subToolsetVersion, loadSettings); + } + + /// + /// Construct over an existing project file, evaluating with the global project collection's + /// global properties and default tools version. + /// Project is added to the global project collection. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// May throw IO-related exceptions. + /// + /// If the evaluation fails. + public Project(string projectFile) + : this(projectFile, null, null) + { + } + + /// + /// Construct over an existing project file, evaluating with specified + /// global properties and toolset, either or both of which may be null. + /// Project is added to the global project collection. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// May throw IO-related exceptions. + /// + public Project(string projectFile, IDictionary globalProperties, string toolsVersion) + : this(projectFile, globalProperties, toolsVersion, ProjectCollection.GlobalProjectCollection) + { + } + + /// + /// Construct over an existing project file, evaluating with the specified global properties and + /// using the tools version provided, either or both of which may be null. + /// Project is added to the global project collection. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// May throw IO-related exceptions. + /// + /// The project file + /// The global properties. May be null. + /// The tools version. May be null. + /// The collection with which this project should be associated. May not be null. + public Project(string projectFile, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection) + : this(projectFile, globalProperties, toolsVersion, projectCollection, ProjectLoadSettings.Default) + { + } + + /// + /// Construct over an existing project file, evaluating with the specified global properties and + /// using the tools version provided, either or both of which may be null. + /// Project is added to the global project collection. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// May throw IO-related exceptions. + /// + /// The project file + /// The global properties. May be null. + /// The tools version. May be null. + /// The collection with which this project should be associated. May not be null. + public Project(string projectFile, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) + : this(projectFile, globalProperties, toolsVersion, null /* no explicitly specified sub-toolset version */, projectCollection, loadSettings) + { + } + + /// + /// Construct over an existing project file, evaluating with the specified global properties and + /// using the tools version provided, either or both of which may be null. + /// Project is added to the global project collection. + /// Throws InvalidProjectFileException if the evaluation fails. + /// Throws InvalidOperationException if there is already an equivalent project loaded in the project collection. + /// May throw IO-related exceptions. + /// + /// The project file + /// The global properties. May be null. + /// The tools version. May be null. + /// Sub-toolset version to explicitly evaluate the toolset with. May be null. + /// The collection with which this project should be associated. May not be null. + /// The load settings for this project. + public Project(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection, ProjectLoadSettings loadSettings) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); + + _projectCollection = projectCollection; + + // We do not control the current directory at this point, but assume that if we were + // passed a relative path, the caller assumes we will prepend the current directory. + projectFile = FileUtilities.NormalizePath(projectFile); + + try + { + _xml = ProjectRootElement.OpenProjectOrSolution(projectFile, globalProperties, toolsVersion, LoggingService, projectCollection.ProjectRootElementCache, s_buildEventContext, true /*Explicitly loaded*/); + } + catch (InvalidProjectFileException ex) + { + LoggingService.LogInvalidProjectFileError(s_buildEventContext, ex); + throw; + } + + try + { + Initialize(globalProperties, toolsVersion, subToolsetVersion, loadSettings); + } + catch (Exception ex) + { + // If possible, clear out the XML we just loaded into the XML cache: + // if we had loaded the XML from disk into the cache within this constructor, + // and then are are bailing out because there is a typo in the XML such that + // evaluation failed, we don't want to leave the bad XML in the cache; + // the user wouldn't be able to fix the XML file and try again. + if (!ExceptionHandling.IsCriticalException(ex)) + { + projectCollection.TryUnloadProject(_xml); + } + + throw; + } + } + + /// + /// Whether build is enabled for this project. + /// + private enum BuildEnabledSetting + { + /// + /// Explicitly enabled + /// + BuildEnabled, + + /// + /// Explicitly disabled + /// + BuildDisabled, + + /// + /// No explicit setting, uses the setting on the + /// project collection. + /// This is the default. + /// + UseProjectCollectionSetting + } + + /// + /// Gets or sets the project collection which contains this project. + /// Can never be null. + /// Cannot be modified. + /// + public ProjectCollection ProjectCollection + { + [DebuggerStepThrough] + get + { return _projectCollection; } + } + + /// + /// The backing Xml project. + /// Can never be null + /// + /// + /// There is no setter here as that doesn't make sense. If you have a new ProjectRootElement, evaluate it into a new Project. + /// + public ProjectRootElement Xml + { + [DebuggerStepThrough] + get + { return _xml; } + } + + /// + /// Whether this project is dirty such that it needs reevaluation. + /// This may be because its underlying XML has changed (either through this project or another) + /// either the XML of the main project or an imported file; + /// or because its toolset may have changed. + /// + public bool IsDirty + { + get + { + if (_explicitlyMarkedDirty) + { + if (s_debugEvaluation) + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Explicitly marked dirty, eg., because a global property was set, or an import, such as a .user file, was created on disk [{0}] [PC Hash {1}]", FullPath, _projectCollection.GetHashCode())); + } + + return true; + } + + if (_evaluatedVersion < _xml.Version) + { + if (s_debugEvaluation) + { + if (_xml.Count > 0) // don't log empty projects, evaluation is not interesting + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Is dirty because {0} [{1}] [PC Hash {2}]", _xml.LastDirtyReason, FullPath, _projectCollection.GetHashCode())); + } + } + + return true; + } + + if (_evaluatedToolsetCollectionVersion != ProjectCollection.ToolsetsVersion) + { + if (s_debugEvaluation) + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Is dirty because toolsets updated [{0}] [PC Hash {1}]", FullPath, _projectCollection.GetHashCode())); + } + + return true; + } + + foreach (Triple triple in _data.ImportClosure) + { + if (triple.Second.Version != triple.Third || _evaluatedVersion < triple.Third) + { + if (s_debugEvaluation) + { + string reason = triple.Second.LastDirtyReason; + + if (reason != null) + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Is dirty because {0} [{1} - {2}] [PC Hash {3}]", reason, FullPath, (triple.Second.FullPath == FullPath ? String.Empty : triple.Second.FullPath), _projectCollection.GetHashCode())); + } + } + + return true; + } + } + + return false; + } + } + + /// + /// An arbitrary number that changes when this project reevaluates. + /// Hosts don't know whether an evaluation actually happened in an interval, but they can compare this number to + /// their previously stored value to find out, and if so perhaps decide to update their own state. + /// Note that the number may not increase monotonically. + /// Unloading a project does not reset the number, so it does not break the guarantee. + /// + public int EvaluationCounter + { + get { return _evaluationCounter; } + } + + /// + /// Read only dictionary of the global properties used in the evaluation + /// of this project. + /// + /// + /// This is the publicly exposed getter, that translates into a read-only dead IDictionary<string, string>. + /// + /// In order to easily tell when we're dirtied, setting and removing global properties is done with + /// SetGlobalProperty and RemoveGlobalProperty. + /// + public IDictionary GlobalProperties + { + [DebuggerStepThrough] + get + { + if (_data.GlobalPropertiesDictionary.Count == 0) + { + return ReadOnlyEmptyDictionary.Instance; + } + + Dictionary dictionary = new Dictionary(_data.GlobalPropertiesDictionary.Count, MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectPropertyInstance property in _data.GlobalPropertiesDictionary) + { + dictionary[property.Name] = ((IProperty)property).EvaluatedValueEscaped; + } + + return new ObjectModel.ReadOnlyDictionary(dictionary); + } + } + + /// + /// Item types in this project. + /// This is an ordered collection. + /// + /// + /// data.ItemTypes is a KeyCollection, so it doesn't need any + /// additional read-only protection + /// + public ICollection ItemTypes + { + [DebuggerStepThrough] + get + { return _data.ItemTypes; } + } + + /// + /// Properties in this project. + /// Since evaluation has occurred, this is an unordered collection. + /// + public ICollection Properties + { + [DebuggerStepThrough] + get + { return new ReadOnlyCollection(_data.Properties); } + } + + /// + /// Collection of possible values implied for properties contained in the conditions found on properties, + /// property groups, imports, and whens. + /// + /// For example, if the following conditions existed on properties in a project: + /// + /// Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'" + /// Condition="'$(Configuration)' == 'Release'" + /// + /// the table would be populated with + /// + /// { "Configuration", { "Debug", "Release" }} + /// { "Platform", { "x86" }} + /// + /// This is used by Visual Studio to determine the configurations defined in the project. + /// + public IDictionary> ConditionedProperties + { + [DebuggerStepThrough] + get + { + if (_data.ConditionedProperties == null) + { + return ReadOnlyEmptyDictionary>.Instance; + } + + return new ObjectModel.ReadOnlyDictionary>(_data.ConditionedProperties); + } + } + + /// + /// Read-only dictionary of item definitions in this project. + /// Keyed by item type + /// + public IDictionary ItemDefinitions + { + [DebuggerStepThrough] + get + { return _data.ItemDefinitions; } + } + + /// + /// Items in this project, ordered within groups of item types + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public ICollection Items + { + [DebuggerStepThrough] + get + { return new ReadOnlyCollection(_data.Items); } + } + + /// + /// Items in this project, ordered within groups of item types, + /// including items whose conditions evaluated to false, or that were + /// contained within item groups who themselves had conditioned evaluated to false. + /// This is useful for hosts that wish to display all items, even if they might not be part + /// of the build in the current configuration. + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public ICollection ItemsIgnoringCondition + { + [DebuggerStepThrough] + get + { return new ReadOnlyCollection(_data.ItemsIgnoringCondition); } + } + + /// + /// All the files that during evaluation contributed to this project, as ProjectRootElements, + /// with the ProjectImportElement that caused them to be imported. + /// This does not include projects that were never imported because a condition on an Import element was false. + /// The outer ProjectRootElement that maps to this project itself is not included. + /// + /// + /// This can be used by the host to figure out what projects might be impacted by a change to a particular file. + /// It could also be used, for example, to find the .user file, and use its ProjectRootElement to modify properties in it. + /// + public IList Imports + { + get + { + List imports = new List(_data.ImportClosure.Count - 1 /* outer project */); + + foreach (Triple import in _data.ImportClosure) + { + if (import.First != null) // Exclude outer project itself + { + imports.Add(new ResolvedImport(this, import.First, import.Second)); + } + } + + return imports; + } + } + + /// + /// This list will contain duplicate imports if an import is imported multiple times. However, only the first import was used in evaluation. + /// + public IList ImportsIncludingDuplicates + { + get + { + ErrorUtilities.VerifyThrowInvalidOperation((_loadSettings & ProjectLoadSettings.RecordDuplicateButNotCircularImports) != 0, "OM_MustSetRecordDuplicateInputs"); + + List imports = new List(_data.ImportClosureWithDuplicates.Count - 1 /* outer project */); + + foreach (Triple import in _data.ImportClosureWithDuplicates) + { + if (import.First != null) // Exclude outer project itself + { + imports.Add(new ResolvedImport(this, import.First, import.Second)); + } + } + + return imports; + } + } + + /// + /// Targets in the project. The key to the dictionary is the target's name. + /// Overridden targets are not included in this collection. + /// This collection is read-only. + /// + public IDictionary Targets + { + [DebuggerStepThrough] + get + { + if (_data.Targets == null) + { + return ReadOnlyEmptyDictionary.Instance; + } + + return new ObjectModel.ReadOnlyDictionary(_data.Targets); + } + } + + /// + /// Properties encountered during evaluation. These are read during the first evaluation pass. + /// Unlike those returned by the Properties property, these are ordered, and includes any properties that + /// were subsequently overridden by others with the same name. It does not include any + /// properties whose conditions did not evaluate to true. + /// It does not include any properties added since the last evaluation. + /// + public ICollection AllEvaluatedProperties + { + get + { + ICollection allEvaluatedProperties = _data.AllEvaluatedProperties; + + if (allEvaluatedProperties == null) + { + return ReadOnlyEmptyCollection.Instance; + } + + return new ReadOnlyCollection(allEvaluatedProperties); + } + } + + /// + /// Item definition metadata encountered during evaluation. These are read during the second evaluation pass. + /// Unlike those returned by the ItemDefinitions property, these are ordered, and include any metadata that + /// were subsequently overridden by others with the same name and item type. It does not include any + /// elements whose conditions did not evaluate to true. + /// It does not include any item definition metadata added since the last evaluation. + /// + public ICollection AllEvaluatedItemDefinitionMetadata + { + get + { + ICollection allEvaluatedItemDefinitionMetadata = _data.AllEvaluatedItemDefinitionMetadata; + + if (allEvaluatedItemDefinitionMetadata == null) + { + return ReadOnlyEmptyCollection.Instance; + } + + return new ReadOnlyCollection(allEvaluatedItemDefinitionMetadata); + } + } + + /// + /// Items encountered during evaluation. These are read during the third evaluation pass. + /// Unlike those returned by the Items property, these are ordered with respect to all other items + /// encountered during evaluation, not just ordered with respect to items of the same item type. + /// In some applications, like the F# language, this complete mutual ordering is significant, and such hosts + /// can use this property. + /// It does not include any elements whose conditions did not evaluate to true. + /// It does not include any items added since the last evaluation. + /// + public ICollection AllEvaluatedItems + { + get + { + ICollection allEvaluatedItems = _data.AllEvaluatedItems; + + if (allEvaluatedItems == null) + { + return ReadOnlyEmptyCollection.Instance; + } + + return new ReadOnlyCollection(allEvaluatedItems); + } + } + + /// + /// The tools version this project was evaluated with, if any. + /// Not necessarily the same as the tools version on the Project tag, if any; + /// it may have been externally specified, for example with a /tv switch. + /// The actual tools version on the Project tag, can be gotten from Xml.ToolsVersion. + /// Cannot be changed once the project has been created. + /// + /// + /// Set by construction. + /// + public string ToolsVersion + { + get { return _data.Toolset.ToolsVersion; } + } + + /// + /// The sub-toolset version that, combined with the ToolsVersion, was used to determine + /// the toolset properties for this project. + /// + public string SubToolsetVersion + { + get { return _data.SubToolsetVersion; } + } + + /// + /// The root directory for this project. + /// Is never null: in-memory projects use the current directory from the time of load. + /// + public string DirectoryPath + { + [DebuggerStepThrough] + get + { return Xml.DirectoryPath; } + } + + /// + /// The full path to this project's file. + /// May be null, if the project was not loaded from disk. + /// Setter renames the project, if it already had a name. + /// + public string FullPath + { + [DebuggerStepThrough] + get + { return Xml.FullPath; } + [DebuggerStepThrough] + set + { Xml.FullPath = value; } + } + + /// + /// Whether ReevaluateIfNecessary is temporarily disabled. + /// This is useful when the host expects to make a number of reads and writes + /// to the project, and wants to temporarily sacrifice correctness for performance. + /// + public bool SkipEvaluation + { + get; + set; + } + + /// + /// Whether MarkDirty() is temporarily disabled. + /// This allows, for example, a global property to be set without the project getting + /// marked dirty for reevaluation as a consequence. + /// + public bool DisableMarkDirty + { + get; + set; + } + + /// + /// This controls whether or not the building of targets/tasks is enabled for this + /// project. This is for security purposes in case a host wants to closely + /// control which projects it allows to run targets/tasks. By default, for a newly + /// created project, we will use whatever setting is in the parent project collection. + /// When build is disabled, the Build method on this class will fail. However if + /// the host has already created a ProjectInstance, it can still build it. (It is + /// free to put a similar check around where it does this.) + /// + public bool IsBuildEnabled + { + get + { + switch (_isBuildEnabled) + { + case BuildEnabledSetting.BuildEnabled: + return true; + + case BuildEnabledSetting.BuildDisabled: + return false; + + case BuildEnabledSetting.UseProjectCollectionSetting: + return ProjectCollection.IsBuildEnabled; + + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + return false; + } + } + + set + { + _isBuildEnabled = value ? BuildEnabledSetting.BuildEnabled : BuildEnabledSetting.BuildDisabled; + } + } + + /// + /// Location of the originating file itself, not any specific content within it. + /// If the file has not been given a name, returns an empty location. + /// + public ElementLocation ProjectFileLocation + { + get { return _xml.ProjectFileLocation; } + } + + /// + /// List of names of the properties that, while global, are still treated as overridable + /// + internal ISet GlobalPropertiesToTreatAsLocal + { + [DebuggerStepThrough] + get + { return _data.GlobalPropertiesToTreatAsLocal; } + } + + /// + /// The logging service used for evaluation errors + /// + internal ILoggingService LoggingService + { + [DebuggerStepThrough] + get + { return ProjectCollection.LoggingService; } + } + + /// + /// Returns the evaluated, escaped value of the provided item's include. + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] + public static string GetEvaluatedItemIncludeEscaped(ProjectItem item) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).EvaluatedIncludeEscaped; + } + + /// + /// Returns the evaluated, escaped value of the provided item definition's include. + /// + public static string GetEvaluatedItemIncludeEscaped(ProjectItemDefinition item) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).EvaluatedIncludeEscaped; + } + + /// + /// Gets the escaped value of the provided metadatum. + /// + public static string GetMetadataValueEscaped(ProjectMetadata metadatum) + { + ErrorUtilities.VerifyThrowArgumentNull(metadatum, "metadatum"); + + return metadatum.EvaluatedValueEscaped; + } + + /// + /// Gets the escaped value of the metadatum with the provided name on the provided item. + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] + public static string GetMetadataValueEscaped(ProjectItem item, string name) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).GetMetadataValueEscaped(name); + } + + /// + /// Gets the escaped value of the metadatum with the provided name on the provided item definition. + /// + public static string GetMetadataValueEscaped(ProjectItemDefinition item, string name) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).GetMetadataValueEscaped(name); + } + + /// + /// Get the escaped value of the provided property + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IProperty is an internal interface; this is less confusing to outside customers. ")] + public static string GetPropertyValueEscaped(ProjectProperty property) + { + ErrorUtilities.VerifyThrowArgumentNull(property, "property"); + + return ((IProperty)property).EvaluatedValueEscaped; + } + + /// + /// Returns an iterator over the "logical project". The logical project is defined as + /// the unevaluated project obtained from the single MSBuild file that is the result + /// of inlining the text of all imports of the original MSBuild project manifest file. + /// + public IEnumerable GetLogicalProject() + { + IEnumerable enumerable = GetLogicalProject(Xml.AllChildren); + + return enumerable; + } + + /// + /// Get any property in the project that has the specified name, + /// otherwise returns null + /// + [DebuggerStepThrough] + public ProjectProperty GetProperty(string name) + { + return _data.Properties[name]; + } + + /// + /// Get the unescaped value of a property in this project, or + /// an empty string if it does not exist. + /// + /// + /// A property with a value of empty string and no property + /// at all are not distinguished between by this method. + /// That makes it easier to use. To find out if a property is set at + /// all in the project, use GetProperty(name). + /// + public string GetPropertyValue(string name) + { + return _data.GetPropertyValue(name); + } + + /// + /// Set or add a property with the specified name and value. + /// Overwrites the value of any property with the same name already in the collection if it did not originate in an imported file. + /// If there is no such existing property, uses this heuristic: + /// Updates the last existing property with the specified name that has no condition on itself or its property group, if any, + /// and is in this project file rather than an imported file. + /// Otherwise, adds a new property in the first property group without a condition, creating a property group if necessary after + /// the last existing property group, else at the start of the project. + /// Returns the property set. + /// Evaluates on a best-effort basis: + /// -expands with all properties. Properties that are defined in the XML below the new property may be used, even though in a real evaluation they would not be. + /// -only this property is evaluated. Anything else that would depend on its value is not affected. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state until reevaluation. + /// + public ProjectProperty SetProperty(string name, string unevaluatedValue) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue, "unevaluatedValue"); + + ProjectProperty property = _data.Properties[name]; + + ErrorUtilities.VerifyThrowInvalidOperation(property == null || !property.IsReservedProperty, "OM_ReservedName", name); + ErrorUtilities.VerifyThrowInvalidOperation(property == null || !property.IsGlobalProperty, "OM_GlobalProperty", name); + + // If there's an existing regular property, we can reuse it, unless it's not attached to its XML any more + if (property != null && + !property.IsEnvironmentProperty && + property.Xml.Parent != null && + property.Xml.Parent.Parent != null && + Object.ReferenceEquals(property.Xml.ContainingProject, _xml)) + { + property.UnevaluatedValue = unevaluatedValue; + } + else + { + ProjectPropertyElement propertyElement = _xml.AddProperty(name, unevaluatedValue); + + property = ProjectProperty.Create(this, propertyElement, unevaluatedValue, null /* predecessor unknown */); + + _data.Properties[name] = property; + } + + property.UpdateEvaluatedValue(ExpandPropertyValueBestEffortLeaveEscaped(unevaluatedValue, property.Xml.Location)); + + return property; + } + + /// + /// Change a global property after the project has been evaluated. + /// If the value changes, this makes the project require reevaluation. + /// If the value changes, returns true, otherwise false. + /// + public bool SetGlobalProperty(string name, string escapedValue) + { + ProjectPropertyInstance existing = _data.GlobalPropertiesDictionary[name]; + + if (existing == null || ((IProperty)existing).EvaluatedValueEscaped != escapedValue) + { + string originalValue = (existing == null) ? String.Empty : ((IProperty)existing).EvaluatedValueEscaped; + + _data.GlobalPropertiesDictionary.Set(ProjectPropertyInstance.Create(name, escapedValue)); + _data.Properties.Set(ProjectProperty.Create(this, name, escapedValue, true /* is global */, false /* may not be reserved name */)); + + ProjectCollection.AfterUpdateLoadedProjectGlobalProperties(this); + MarkDirty(); + + if (s_debugEvaluation) + { + string displayValue = escapedValue.Substring(0, Math.Min(escapedValue.Length, 75)) + ((escapedValue.Length > 75) ? "..." : String.Empty); + if (existing == null) + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Initially set global property {0} to '{1}' [{2}]", name, displayValue, FullPath)); + } + else + { + string displayOriginalValue = originalValue.Substring(0, Math.Min(originalValue.Length, 75)) + ((originalValue.Length > 75) ? "..." : String.Empty); + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Changed global property {0} from '{1}' to '{2}' [{3}]", name, displayOriginalValue, displayValue, FullPath)); + } + } + + return true; + } + + return false; + } + + /// + /// Adds an item with no metadata to the project. + /// Any metadata can be added subsequently. + /// Does not modify the XML if a wildcard expression would already include the new item. + /// Evaluates on a best-effort basis: + /// -expands with all items. Items that are defined in the XML below the new item may be used, even though in a real evaluation they would not be. + /// -only this item is evaluated. Other items that might depend on it is not affected. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state until reevaluation. + /// + public IList AddItem(string itemType, string unevaluatedInclude) + { + return AddItem(itemType, unevaluatedInclude, null); + } + + /// + /// Adds an item with metadata to the project. + /// Metadata may be null, indicating no metadata. + /// Does not modify the XML if a wildcard expression would already include the new item. + /// Evaluates on a best-effort basis: + /// -expands with all items. Items that are defined in the XML below the new item may be used, even though in a real evaluation they would not be. + /// -only this item is evaluated. Other items that might depend on it is not affected. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state until reevaluation. + /// + public IList AddItem(string itemType, string unevaluatedInclude, IEnumerable> metadata) + { + // For perf reasons, this method does several jobs in one. + // If it finds a suitable existing item element, it returns that as the out parameter, otherwise the out parameter returns null. + // Otherwise, if it finds an item element suitable to be just below our new element, it returns that. + // Otherwise, if it finds an item group at least that's suitable to put our element in somewhere, it returns that. + // Otherwise, it returns null. + ProjectItemElement itemElement; + ProjectElement element = GetAnySuitableExistingItemXml(itemType, unevaluatedInclude, metadata, out itemElement); + + if (itemElement == null) + { + // Didn't find a suitable existing item; maybe the hunt gave us a hint as + // to where to put a new one. + ProjectItemElement itemElementToAddBefore = element as ProjectItemElement; + + if (itemElementToAddBefore != null) + { + // It told us an item to add before + itemElement = _xml.CreateItemElement(itemType, unevaluatedInclude); + itemElementToAddBefore.Parent.InsertBeforeChild(itemElement, itemElementToAddBefore); + } + else + { + ProjectItemGroupElement itemGroupElement = element as ProjectItemGroupElement; + + if (itemGroupElement != null) + { + // It only told us an item group to add it somewhere within + itemElement = itemGroupElement.AddItem(itemType, unevaluatedInclude); + } + else + { + // It didn't give any hint at all + itemElement = _xml.AddItem(itemType, unevaluatedInclude); + } + } + } + + // Fix up the evaluated state to match + return AddItemHelper(itemElement, unevaluatedInclude, metadata); + } + + /// + /// Adds an item with no metadata to the project. + /// Makes no effort to see if an existing wildcard would already match the new item, unless it is the first item in an item group. + /// Makes no effort to locate the new item near similar items. + /// Appends the item to the first item group that does not have a condition and has either no children or whose first child is an item of the same type. + /// Evaluates on a best-effort basis: + /// -expands with all items. Items that are defined in the XML below the new item may be used, even though in a real evaluation they would not be. + /// -only this item is evaluated. Other items that might depend on it is not affected. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state until reevaluation. + /// + public IList AddItemFast(string itemType, string unevaluatedInclude) + { + return AddItemFast(itemType, unevaluatedInclude, null); + } + + /// + /// Adds an item with metadata to the project. + /// Metadata may be null, indicating no metadata. + /// Makes no effort to see if an existing wildcard would already match the new item, unless it is the first item in an item group. + /// Makes no effort to locate the new item near similar items. + /// Appends the item to the first item group that does not have a condition and has either no children or whose first child is an item of the same type. + /// Evaluates on a best-effort basis: + /// -expands with all items. Items that are defined in the XML below the new item may be used, even though in a real evaluation they would not be. + /// -only this item is evaluated. Other items that might depend on it is not affected. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state until reevaluation. + /// + public IList AddItemFast(string itemType, string unevaluatedInclude, IEnumerable> metadata) + { + ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); + ErrorUtilities.VerifyThrowArgumentLength(unevaluatedInclude, "unevalutedInclude"); + + ProjectItemGroupElement groupToAppendTo = null; + + foreach (ProjectItemGroupElement group in _xml.ItemGroups) + { + if (group.Condition.Length > 0) + { + continue; + } + + if (group.Count == 0 || MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, group.Items.First().ItemType)) + { + groupToAppendTo = group; + + break; + } + } + + if (groupToAppendTo == null) + { + groupToAppendTo = _xml.AddItemGroup(); + } + + ProjectItemElement itemElement; + + if (groupToAppendTo.Count == 0 || + FileMatcher.HasWildcardsSemicolonItemOrPropertyReferences(unevaluatedInclude) || + !IsSuitableExistingItemXml(groupToAppendTo.Items.First(), unevaluatedInclude, metadata)) + { + itemElement = _xml.CreateItemElement(itemType, unevaluatedInclude); + groupToAppendTo.AppendChild(itemElement); + } + else + { + itemElement = groupToAppendTo.Items.First(); + } + + return AddItemHelper(itemElement, unevaluatedInclude, metadata); + } + + /// + /// All the items in the project of the specified + /// type. + /// If there are none, returns an empty list. + /// Use AddItem or RemoveItem to modify items in this project. + /// + /// + /// data.GetItems returns a read-only collection, so no need to re-wrap it here. + /// + public ICollection GetItems(string itemType) + { + ICollection items = _data.GetItems(itemType); + return items; + } + + /// + /// All the items in the project of the specified + /// type, irrespective of whether the conditions on them evaluated to true. + /// This is a read-only list: use AddItem or RemoveItem to modify items in this project. + /// + /// + /// ItemDictionary[] returns a read only collection, so no need to wrap it. + /// + public ICollection GetItemsIgnoringCondition(string itemType) + { + ICollection items = _data.ItemsIgnoringCondition[itemType]; + return items; + } + + /// + /// Returns all items that have the specified evaluated include. + /// For example, all items that have the evaluated include "bar.cpp". + /// Typically there will be zero or one, but sometimes there are two items with the + /// same path and different item types, or even the same item types. This will return + /// them all. + /// + /// + /// data.GetItemsByEvaluatedInclude already returns a read-only collection, so no need + /// to wrap it further. + /// + public ICollection GetItemsByEvaluatedInclude(string evaluatedInclude) + { + ICollection items = _data.GetItemsByEvaluatedInclude(evaluatedInclude); + return items; + } + + /// + /// Removes the specified property. + /// Property must be associated with this project. + /// Property must not originate from an imported file. + /// Returns true if the property was in this evaluated project, otherwise false. + /// As a convenience, if the parent property group becomes empty, it is also removed. + /// Updates the evaluated project, but does not affect anything else in the project until reevaluation. For example, + /// if "p" is removed, it will be removed from the evaluated project, but "q" which is evaluated from "$(p)" will not be modified until reevaluation. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state. + /// + public bool RemoveProperty(ProjectProperty property) + { + ErrorUtilities.VerifyThrowArgumentNull(property, "property"); + ErrorUtilities.VerifyThrowInvalidOperation(!property.IsReservedProperty, "OM_ReservedName", property.Name); + ErrorUtilities.VerifyThrowInvalidOperation(!property.IsGlobalProperty, "OM_GlobalProperty", property.Name); + ErrorUtilities.VerifyThrowArgument(property.Xml.Parent != null, "OM_IncorrectObjectAssociation", "ProjectProperty", "Project"); + VerifyThrowInvalidOperationNotImported(property.Xml.ContainingProject); + + ProjectElementContainer parent = property.Xml.Parent; + + property.Xml.Parent.RemoveChild(property.Xml); + + if (parent.Count == 0) + { + parent.Parent.RemoveChild(parent); + } + + bool result = _data.Properties.Remove(property.Name); + + return result; + } + + /// + /// Removes a global property. + /// If it was set, returns true, and marks the project + /// as requiring reevaluation. + /// + public bool RemoveGlobalProperty(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + bool result = _data.GlobalPropertiesDictionary.Remove(name); + + if (result) + { + ProjectCollection.AfterUpdateLoadedProjectGlobalProperties(this); + MarkDirty(); + + if (s_debugEvaluation) + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Remove global property {0}", name)); + } + } + + return result; + } + + /// + /// Removes an item from the project. + /// Item must be associated with this project. + /// Item must not originate from an imported file. + /// Returns true if the item was in this evaluated project, otherwise false. + /// As a convenience, if the parent item group becomes empty, it is also removed. + /// If the item originated from a wildcard or semicolon separated expression, expands that expression into multiple items first. + /// Updates the evaluated project, but does not affect anything else in the project until reevaluation. For example, + /// if an item of type "i" is removed, "j" which is evaluated from "@(i)" will not be modified until reevaluation. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state until reevaluation. + /// + /// + /// Normally this will return true, since if the item isn't in the project, it will throw. + /// The exception is removing an item that was only in ItemsIgnoringCondition. + /// + public bool RemoveItem(ProjectItem item) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + ErrorUtilities.VerifyThrowArgument(item.Project == this, "OM_IncorrectObjectAssociation", "ProjectItem", "Project"); + + bool result = RemoveItemHelper(item); + + return result; + } + + /// + /// Removes all the specified items from the project. + /// Items that are not associated with this project are skipped. + /// + /// + /// Removing one item could cause the backing XML + /// to be expanded, which could zombie (disassociate) the next item. + /// To make this case easy for the caller, if an item + /// is not associated with this project it is simply skipped. + /// + public void RemoveItems(IEnumerable items) + { + ErrorUtilities.VerifyThrowArgumentNull(items, "items"); + + // Copying to a list makes it possible to remove + // all items of a particular type with + // RemoveItems(p.GetItems("mytype")) + // without modifying the collection during enumeration. + List itemsList = new List(items); + + foreach (ProjectItem item in itemsList) + { + RemoveItemHelper(item); + } + } + + /// + /// Evaluates the provided string by expanding items and properties, + /// as if it was found at the very end of the project file. + /// This is useful for some hosts for which this kind of best-effort + /// evaluation is sufficient. + /// Does not expand bare metadata expressions. + /// + public string ExpandString(string unexpandedValue) + { + ErrorUtilities.VerifyThrowArgumentNull(unexpandedValue, "unexpandedValue"); + + string result = _data.Expander.ExpandIntoStringAndUnescape(unexpandedValue, ExpanderOptions.ExpandPropertiesAndItems, ProjectFileLocation); + + return result; + } + + /// + /// Returns an instance based on this project, but completely disconnected. + /// This instance can be used to build independently. + /// Before creating the instance, this will reevaluate the project if necessary, so it will not be dirty. + /// + public ProjectInstance CreateProjectInstance() + { + return CreateProjectInstance(LoggingService, ProjectInstanceSettings.None); + } + + /// + /// Returns an instance based on this project, but completely disconnected. + /// This instance can be used to build independently. + /// Before creating the instance, this will reevaluate the project if necessary, so it will not be dirty. + /// The instance is immutable; none of the objects that form it can be modified. This makes it safe to + /// access concurrently from multiple threads. + /// + public ProjectInstance CreateProjectInstance(ProjectInstanceSettings settings) + { + return CreateProjectInstance(LoggingService, settings); + } + + /// + /// Called to forcibly mark the project as dirty requiring reevaluation. Generally this is not necessary to set; all edits affecting + /// this project will automatically make it dirty. However there are potential corner cases where it is necessary to mark the project dirty + /// directly. For example, if the project has an import conditioned on a file existing on disk, and the file did not exist at + /// evaluation time, then someone subsequently creates that file, the project cannot know that reevaluation would be productive. + /// In such a case the host can help us by setting the dirty flag explicitly so that ReevaluateIfNecessary() + /// will recognize an evaluation is indeed necessary. + /// Does not mark the underlying project file as requiring saving. + /// + public void MarkDirty() + { + if (!DisableMarkDirty && !_projectCollection.DisableMarkDirty) + { + _explicitlyMarkedDirty = true; + } + + // Pass up the MarkDirty call even when DisableMarkDirty is true. + _xml.MarkProjectDirty(this); + } + + /// + /// Reevaluate the project to get it into a queryable state, if it's dirty. + /// This incorporates all changes previously made to the backing XML by editing this project. + /// Throws InvalidProjectFileException if the evaluation fails. + /// + public void ReevaluateIfNecessary() + { + ReevaluateIfNecessary(LoggingService); + } + + /// + /// Save the project to the file system, if dirty. + /// Uses the default encoding. + /// + public void Save() + { + Xml.Save(); + } + + /// + /// Save the project to the file system, if dirty. + /// + public void Save(Encoding encoding) + { + Xml.Save(encoding); + } + + /// + /// Save the project to the file system, if dirty or the path is different. + /// Uses the default encoding. + /// + public void Save(string path) + { + Xml.Save(path); + } + + /// + /// Save the project to the file system, if dirty or the path is different. + /// + public void Save(string path, Encoding encoding) + { + Xml.Save(path, encoding); + } + + /// + /// Save the project to the provided TextWriter, whether or not it is dirty. + /// Uses the encoding of the TextWriter. + /// Clears the Dirty flag. + /// + public void Save(TextWriter writer) + { + Xml.Save(writer); + } + + /// + /// Saves a "logical" or "preprocessed" project file, that includes all the imported + /// files as if they formed a single file. + /// + public void SaveLogicalProject(TextWriter writer) + { + XmlDocument document = Preprocessor.GetPreprocessedDocument(this); + + using (ProjectWriter projectWriter = new ProjectWriter(writer)) + { + projectWriter.Initialize(document); + document.Save(projectWriter); + } + } + + /// + /// Starts a build using this project, building the default targets. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build() + { + return Build((string[])null); + } + + /// + /// Starts a build using this project, building the default targets and the specified logger. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(ILogger logger) + { + List loggers = new List(1); + loggers.Add(logger); + return Build((string[])null, loggers, null); + } + + /// + /// Starts a build using this project, building the default targets and the specified loggers. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(IEnumerable loggers) + { + return Build((string[])null, loggers, null); + } + + /// + /// Starts a build using this project, building the default targets and the specified loggers. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(IEnumerable loggers, IEnumerable remoteLoggers) + { + return Build((string[])null, loggers, remoteLoggers); + } + + /// + /// Starts a build using this project, building the specified target. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(string target) + { + return Build(target, null, null); + } + + /// + /// Starts a build using this project, building the specified target with the specified loggers. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(string target, IEnumerable loggers) + { + return Build(target, loggers, null); + } + + /// + /// Starts a build using this project, building the specified target with the specified loggers. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(string target, IEnumerable loggers, IEnumerable remoteLoggers) + { + // targets may be null, but not an entry within it + string[] targets = (target == null) ? null : new string[] { target }; + + return Build(targets, loggers, remoteLoggers); + } + + /// + /// Starts a build using this project, building the specified targets. + /// Returns true on success, false on failure. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(string[] targets) + { + return Build(targets, null, null); + } + + /// + /// Starts a build using this project, building the specified targets with the specified loggers. + /// Returns true on success, false on failure. + /// If build is disabled on this project, does not build, and returns false. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(string[] targets, IEnumerable loggers) + { + return Build(targets, loggers, null); + } + + /// + /// Starts a build using this project, building the specified targets with the specified loggers. + /// Returns true on success, false on failure. + /// If build is disabled on this project, does not build, and returns false. + /// Works on a privately cloned instance. To set or get + /// virtual items for build purposes, clone an instance explicitly and build that. + /// Does not modify the Project object. + /// + public bool Build(string[] targets, IEnumerable loggers, IEnumerable remoteLoggers) + { + bool result = false; + + if (!IsBuildEnabled) + { + LoggingService.LogError(s_buildEventContext, new BuildEventFileInfo(FullPath), "SecurityProjectBuildDisabled"); + return false; + } + + ProjectInstance instance = CreateProjectInstance(LoggingService, ProjectInstanceSettings.None); + IDictionary targetOutputs; + + if (loggers == null && ProjectCollection.Loggers != null) + { + loggers = ProjectCollection.Loggers; + } + + result = instance.Build(targets, loggers, remoteLoggers, null, ProjectCollection.MaxNodeCount, out targetOutputs); + + return result; + } + + /// + /// Tests whether a given project IS or IMPORTS some given project xml root element. + /// + /// The project xml root element in question. + /// True if this project is or imports the xml file; false otherwise. + internal bool UsesProjectRootElement(ProjectRootElement xmlRootElement) + { + if (Object.ReferenceEquals(this.Xml, xmlRootElement)) + { + return true; + } + + if (_data.ImportClosure.Any(triple => Object.ReferenceEquals(triple.Second, xmlRootElement))) + { + return true; + } + + return false; + } + + /// + /// If the ProjectItemElement evaluated to more than one ProjectItem, replaces it with a new ProjectItemElement for each one of them. + /// If the ProjectItemElement did not evaluate into more than one ProjectItem, does nothing. + /// Returns true if a split occurred, otherwise false. + /// + /// + /// A ProjectItemElement could have resulted in several items if it contains wildcards or item or property expressions. + /// Before any edit to a ProjectItem (remove, rename, set metadata, or remove metadata) this must be called to make + /// sure that the edit does not affect any other ProjectItems originating in the same ProjectItemElement. + /// + /// For example, an item xml with an include of "@(x)" could evaluate to items "a", "b", and "c". If "b" is removed, then the original + /// item xml must be removed and replaced with three, then the one corresponding to "b" can be removed. + /// + /// This is an unsophisticated approach; the best that can be said is that the result will likely be correct, if not ideal. + /// For example, perhaps the user would rather remove the item from the original list "x" instead of expanding the list. + /// Or, perhaps the user would rather the property in "$(p)\a;$(p)\b" not be expanded when "$(p)\b" is removed. + /// If that's important, the host can manipulate the ProjectItemElement's directly, instead, and it can be as fastidious as it wishes. + /// + internal bool SplitItemElementIfNecessary(ProjectItemElement itemElement) + { + if (!FileMatcher.HasWildcardsSemicolonItemOrPropertyReferences(itemElement.Include)) + { + return false; + } + + List relevantItems = new List(); + + foreach (ProjectItem item in Items) + { + if (item.Xml == itemElement) + { + relevantItems.Add(item); + } + } + + if (relevantItems.Count <= 1) + { + return false; + } + + foreach (ProjectItem item in relevantItems) + { + item.SplitOwnItemElement(); + } + + itemElement.Parent.RemoveChild(itemElement); + + return true; + } + + /// + /// Examines the provided ProjectItemElement to see if it has a wildcard that would match the + /// item we wish to add, and does not have a condition or an exclude. + /// Works conservatively - if there is anything that might cause doubt, considers the candidate to not be suitable. + /// Returns true if it is suitable, otherwise false. + /// + /// + /// Outside this class called ONLY from ProjectItem.Rename(string name). + /// + internal bool IsSuitableExistingItemXml(ProjectItemElement candidateExistingItemXml, string unevaluatedInclude, IEnumerable> metadata) + { + if (candidateExistingItemXml.Condition.Length != 0 || candidateExistingItemXml.Exclude.Length != 0 || !candidateExistingItemXml.IncludeHasWildcards) + { + return false; + } + + if ((metadata != null && metadata.Any()) || candidateExistingItemXml.Count > 0) + { + // Don't try to make sure the metadata are the same. + return false; + } + + string evaluatedExistingInclude = _data.Expander.ExpandIntoStringLeaveEscaped(candidateExistingItemXml.Include, ExpanderOptions.ExpandProperties, candidateExistingItemXml.IncludeLocation); + + string[] existingIncludePieces = evaluatedExistingInclude.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string existingIncludePiece in existingIncludePieces) + { + if (!FileMatcher.HasWildcards(existingIncludePiece)) + { + continue; + } + + FileMatcher.Result match = FileMatcher.FileMatch(existingIncludePiece, unevaluatedInclude); + + if (match.isLegalFileSpec && match.isMatch) + { + // The wildcard in the original item spec will match the new item that + // user is trying to add. + return true; + } + } + + return false; + } + + /// + /// Before an item changes its item type, it must be removed from + /// our datastructures, which key off item type. + /// This should be called ONLY by ProjectItems, in this situation. + /// + internal void RemoveItemBeforeItemTypeChange(ProjectItem item) + { + _data.RemoveItem(item); + } + + /// + /// After an item has changed its item type, it needs to be added back again, + /// since our data structures key off the item type. + /// This should be called ONLY by ProjectItems, in this situation. + /// + internal void ReAddExistingItemAfterItemTypeChange(ProjectItem item) + { + _data.AddItem(item); + _data.AddItemIgnoringCondition(item); + } + + /// + /// Provided a property that is already part of this project, does a best-effort expansion + /// of the unevaluated value provided and sets it as the evaluated value. + /// + /// + /// On project in order to keep Project's expander hidden. + /// + internal string ExpandPropertyValueBestEffortLeaveEscaped(string unevaluatedValue, ElementLocation propertyLocation) + { + string evaluatedValueEscaped = _data.Expander.ExpandIntoStringLeaveEscaped(unevaluatedValue, ExpanderOptions.ExpandProperties, propertyLocation); + + return evaluatedValueEscaped; + } + + /// + /// Provided an item element that has been renamed with a new unevaluated include, + /// returns a best effort guess at the evaluated include that results. + /// If the best effort expansion produces anything other than one item, it just + /// returns the unevaluated include. + /// This is not at all generalized, but useful for the majority case where an item is a very + /// simple file name with perhaps a property prefix. + /// + /// + /// On project in order to keep Project's expander hidden. + /// + internal string ExpandItemIncludeBestEffortLeaveEscaped(ProjectItemElement renamedItemElement) + { + if (renamedItemElement.Exclude.Length > 0) + { + return renamedItemElement.Include; + } + + ProjectItemFactory itemFactory = new ProjectItemFactory(this, renamedItemElement); + + List items = Evaluator.CreateItemsFromInclude(DirectoryPath, renamedItemElement, itemFactory, renamedItemElement.Include, _data.Expander); + + if (items.Count != 1) + { + return renamedItemElement.Include; + } + + return ((IItem)items[0]).EvaluatedIncludeEscaped; + } + + /// + /// Provided a metadatum that is already part of this project, does a best-effort expansion + /// of the unevaluated value provided and returns the resulting value. + /// This is a interim expansion only: it may not be the value that a full project reevaluation would produce. + /// The metadata table passed in is that of the parent item or item definition. + /// + /// + /// On project in order to keep Project's expander hidden. + /// + internal string ExpandMetadataValueBestEffortLeaveEscaped(IMetadataTable metadataTable, string unevaluatedValue, ElementLocation metadataLocation) + { + ErrorUtilities.VerifyThrow(_data.Expander.Metadata == null, "Should be null"); + + _data.Expander.Metadata = metadataTable; + string evaluatedValueEscaped = _data.Expander.ExpandIntoStringLeaveEscaped(unevaluatedValue, ExpanderOptions.ExpandAll, metadataLocation); + _data.Expander.Metadata = null; + + return evaluatedValueEscaped; + } + + /// + /// Called by the project collection to indicate to this project that it is no longer loaded. + /// + internal void Zombify() + { + _xml.OnAfterProjectRename -= _renameHandler; + _xml.OnProjectXmlChanged -= ProjectRootElement_ProjectXmlChangedHandler; + _xml.XmlDocument.ClearAnyCachedStrings(); + _renameHandler = null; + } + + /// + /// Verify that the project has not been unloaded from its collection. + /// Once it's been unloaded, it cannot be used. + /// + internal void VerifyThrowInvalidOperationNotZombie() + { + ErrorUtilities.VerifyThrow(_renameHandler != null, "OM_ProjectIsNoLongerActive"); + } + + /// + /// Verify that the provided object location is in the same file as the project. + /// If it is not, throws an InvalidOperationException indicating that imported evaluated objects should not be modified. + /// This prevents, for example, accidentally updating something like the OutputPath property, that you want be in the + /// main project, but for some reason was actually read in from an imported targets file. + /// + internal void VerifyThrowInvalidOperationNotImported(ProjectRootElement otherXml) + { + ErrorUtilities.VerifyThrowInternalNull(otherXml, "otherXml"); + ErrorUtilities.VerifyThrowInvalidOperation(Object.ReferenceEquals(Xml, otherXml), "OM_CannotModifyEvaluatedObjectInImportedFile", otherXml.Location.File); + } + + /// + /// Get the next global evaluation counter number + /// in a thread safe fashion. + /// + private static int GetNextEvaluationCounter() + { + // We build without /checked, so this + // will wrap, which is fine as it's incredibly unlikely + return Interlocked.Increment(ref s_globalEvaluationCounter); + } + + /// + /// Common code for the AddItem methods. + /// + private List AddItemHelper(ProjectItemElement itemElement, string unevaluatedInclude, IEnumerable> metadata) + { + ProjectItemFactory itemFactory = new ProjectItemFactory(this, itemElement); + + List items = Evaluator.CreateItemsFromInclude(DirectoryPath, itemElement, itemFactory, unevaluatedInclude, _data.Expander); + + foreach (ProjectItem item in items) + { + _data.AddItem(item); + _data.AddItemIgnoringCondition(item); + } + + if (metadata != null) + { + foreach (ProjectItem item in items) + { + foreach (KeyValuePair metadatum in metadata) + { + item.SetMetadataValue(metadatum.Key, metadatum.Value); + } + } + } + + // The old OM attempted to evaluate and return the resulting item, or if several then whatever was the "first" returned. + // This was rather arbitrary, and made it impossible for the caller to retrieve the whole set. + return items; + } + + /// + /// Helper for and . + /// If the item is not associated with a project, returns false. + /// If the item is not present in the evaluated project, returns false. + /// If the item is associated with another project, throws ArgumentException. + /// Otherwise removes the item and returns true. + /// + private bool RemoveItemHelper(ProjectItem item) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + if (item.Project == null || item.Xml.Parent == null) + { + // Return rather than throwing: this is to make it easier + // to enumerate over a list of items to remove. + return false; + } + + ErrorUtilities.VerifyThrowArgument(item.Project == this, "OM_IncorrectObjectAssociation", "ProjectItem", "Project"); + + VerifyThrowInvalidOperationNotImported(item.Xml.ContainingProject); + + SplitItemElementIfNecessary(item.Xml); + + ProjectElementContainer parent = item.Xml.Parent; + + item.Xml.Parent.RemoveChild(item.Xml); + + if (parent.Count == 0) + { + parent.Parent.RemoveChild(parent); + } + + bool result = _data.RemoveItem(item); + + return result; + } + + /// + /// Creates a project instance based on this project using the specified logging service. + /// + private ProjectInstance CreateProjectInstance(ILoggingService loggingServiceForEvaluation, ProjectInstanceSettings settings) + { + ReevaluateIfNecessary(loggingServiceForEvaluation); + + return new ProjectInstance(_data, DirectoryPath, FullPath, ProjectCollection.HostServices, _projectCollection.EnvironmentProperties, settings); + } + + /// + /// Re-evaluates the project using the specified logging service. + /// + private void ReevaluateIfNecessary(ILoggingService loggingServiceForEvaluation) + { + // We will skip the evaluation if the flag is set. This will give us better performance on scenarios + // that we know we don't have to reevaluate. One example is project conversion bulk addfiles and set attributes. + if (!SkipEvaluation && !_projectCollection.SkipEvaluation && IsDirty) + { + try + { + Evaluator.Evaluate(_data, _xml, _loadSettings, ProjectCollection.MaxNodeCount, ProjectCollection.EnvironmentProperties, loggingServiceForEvaluation, new ProjectItemFactory(this), _projectCollection as IToolsetProvider, _projectCollection.ProjectRootElementCache, s_buildEventContext, null /* no project instance for debugging */); + + // We have to do this after evaluation, because evaluation might have changed + // the imports being pulled in. + int highestXmlVersion = Xml.Version; + + if (_data.ImportClosure != null) + { + foreach (Triple triple in _data.ImportClosure) + { + highestXmlVersion = (highestXmlVersion < triple.Third) ? triple.Third : highestXmlVersion; + } + } + + _explicitlyMarkedDirty = false; + _evaluatedVersion = highestXmlVersion; + _evaluatedToolsetCollectionVersion = ProjectCollection.ToolsetsVersion; + _evaluationCounter = GetNextEvaluationCounter(); + _data.HasUnsavedChanges = false; + + ErrorUtilities.VerifyThrow(!IsDirty, "Should not be dirty now"); + } + catch (InvalidProjectFileException ex) + { + loggingServiceForEvaluation.LogInvalidProjectFileError(s_buildEventContext, ex); + throw; + } + } + } + + /// + /// Common code for the constructors. + /// Applies global properties that are on the collection. + /// Global properties provided for the project overwrite any global properties from the collection that have the same name. + /// Global properties may be null. + /// Tools version may be null. + /// + private void Initialize(IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectLoadSettings loadSettings) + { + _xml.MarkAsExplicitlyLoaded(); + + _evaluationCounter = GetNextEvaluationCounter(); + + PropertyDictionary globalPropertiesCollection = new PropertyDictionary(); + + foreach (ProjectPropertyInstance property in ProjectCollection.GlobalPropertiesCollection) + { + ProjectPropertyInstance clone = property.DeepClone(); + globalPropertiesCollection.Set(clone); + } + + if (globalProperties != null) + { + foreach (KeyValuePair pair in globalProperties) + { + if (String.Equals(pair.Key, Constants.SubToolsetVersionPropertyName, StringComparison.OrdinalIgnoreCase) && subToolsetVersion != null) + { + // if we have a sub-toolset version explicitly provided by the ProjectInstance constructor, AND a sub-toolset version provided as a global property, + // make sure that the one passed in with the constructor wins. If there isn't a matching global property, the sub-toolset version will be set at + // a later point. + globalPropertiesCollection.Set(ProjectPropertyInstance.Create(pair.Key, subToolsetVersion)); + } + else + { + globalPropertiesCollection.Set(ProjectPropertyInstance.Create(pair.Key, pair.Value)); + } + } + } + + _data = new Data(this, globalPropertiesCollection, toolsVersion, subToolsetVersion); + + _loadSettings = loadSettings; + + ReevaluateIfNecessary(); + + // Cause the project to be actually loaded into the collection, and register for + // rename notifications so we can subsequently update the collection. + _renameHandler = new RenameHandlerDelegate(delegate (string oldFullPath) + { + _projectCollection.OnAfterRenameLoadedProject(oldFullPath, this); + }); + + _xml.OnAfterProjectRename += _renameHandler; + _xml.OnProjectXmlChanged += ProjectRootElement_ProjectXmlChangedHandler; + + _renameHandler(null /* not previously named */); + } + + /// + /// Raised when any XML in the underlying ProjectRootElement has changed. + /// + private void ProjectRootElement_ProjectXmlChangedHandler(object sender, ProjectXmlChangedEventArgs args) + { + _xml.MarkProjectDirty(this); + } + + /// + /// Tries to find a ProjectItemElement already in the project file XML that has a wildcard that would match the + /// item we wish to add, does not have a condition or an exclude, and is within an itemgroup without a condition. + /// + /// For perf reasons, this method does several jobs in one. + /// If it finds a suitable existing item element, it returns that as the out parameter, otherwise the out parameter returns null. + /// Otherwise, if it finds an item element suitable to be just below our new element, it returns that. + /// Otherwise, if it finds an item group at least that's suitable to put our element in somewhere, it returns that. + /// + /// Returns null if the include of the item being added itself has wildcards, or semicolons, as the case is too difficult. + /// + private ProjectElement GetAnySuitableExistingItemXml(string itemType, string unevaluatedInclude, IEnumerable> metadata, out ProjectItemElement suitableExistingItemXml) + { + suitableExistingItemXml = null; + + if (FileMatcher.HasWildcardsSemicolonItemOrPropertyReferences(unevaluatedInclude)) + { + return null; + } + + if (metadata != null && metadata.Any()) + { + // Don't bother trying to match up metadata + return null; + } + + // In case we don't find a suitable existing item xml, at least find + // a good item group to add to. Either the first item group with at least one + // item of the same type, or else the first empty item group without a condition. + ProjectItemGroupElement itemGroupToAddTo = null; + + ProjectItemElement itemToAddBefore = null; + + foreach (ProjectItemGroupElement itemGroupXml in _xml.ItemGroups) + { + if (itemGroupXml.Condition.Length > 0) + { + continue; + } + + if (itemGroupXml.DefinitelyAreNoChildrenWithWildcards) + { + continue; + } + + if (itemGroupToAddTo == null && itemGroupXml.Count == 0) + { + itemGroupToAddTo = itemGroupXml; + } + + foreach (ProjectItemElement existingItemXml in itemGroupXml.Items) + { + if (!MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, existingItemXml.ItemType)) + { + continue; + } + + if (itemGroupToAddTo == null || itemGroupToAddTo.Count == 0) + { + itemGroupToAddTo = itemGroupXml; + } + + // if the include sorts after us, store this item, so we can add + // right after it if need be. For example if the item is "b.cs" and we are planning to add "a.cs" + // then we know that we will want to add it just above this item. We can avoid another scan to figure that out. + if (itemToAddBefore == null && String.Compare(unevaluatedInclude, existingItemXml.Include, StringComparison.OrdinalIgnoreCase) < 0) + { + itemToAddBefore = existingItemXml; + } + + if (IsSuitableExistingItemXml(existingItemXml, unevaluatedInclude, metadata)) + { + suitableExistingItemXml = existingItemXml; + return null; + } + } + } + + if (itemToAddBefore == null) + { + return itemGroupToAddTo; + } + + return itemToAddBefore; + } + + /// + /// Recursive helper for GetLogicalProject. + /// + private IEnumerable GetLogicalProject(IEnumerable projectElements) + { + foreach (ProjectElement element in projectElements) + { + ProjectImportElement import = element as ProjectImportElement; + + if (import == null) + { + yield return element; + } + else + { + // Get the project root elements of all the imports resulting from this import statement (there could be multiple if there is a wild card). + IEnumerable children = _data.ImportClosure.Where(triple => Object.ReferenceEquals(triple.First, import)).Select(triple => triple.Second); + + foreach (ProjectRootElement child in children) + { + if (child != null) + { + // The import's condition must have evaluated to true, to traverse into it + IEnumerable childElements = GetLogicalProject(child.AllChildren); + + foreach (ProjectElement childElement in childElements) + { + yield return childElement; + } + } + } + } + } + } + + /// + /// Encapsulates the backing data of a Project, so that it can be passed to the Evaluator to + /// fill in on a re-evaluation without having to expose property setters. + /// + /// + /// This object is only passed to the Evaluator. + /// + internal class Data : IItemProvider, IPropertyProvider, IEvaluatorData + { + /// + /// Project that owns this data + /// + private readonly Project _project; + + /// + /// The global properties to evaluate with, if any. + /// Can never be null. + /// + private readonly PropertyDictionary _globalProperties; + + /// + /// Almost always, projects have the same set of targets because they all import the same ones. + /// So we keep around the last set seen and if ours is the same at the end of evaluation, unify the references. + /// + private static System.WeakReference> s_typicalTargetsCollection; + + /// + /// Save off the contents of the environment variable that specifies whether we should treat higher toolsversions as the current + /// toolsversion. (Some hosts require this.) + /// + private static bool s_shouldTreatHigherToolsVersionsAsCurrent = (Environment.GetEnvironmentVariable("MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT") != null); + + /// + /// Save off the contents of the environment variable that specifies whether we should treat all toolsversions, regardless of + /// whether they are higher or lower, as the current toolsversion. (Some hosts require this.) + /// + private static bool s_shouldTreatOtherToolsVersionsAsCurrent = (Environment.GetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT") != null); + + /// + /// List of names of the properties that, while global, are still treated as overridable + /// + private ISet _globalPropertiesToTreatAsLocal; + + /// + /// List of items that link the XML items and evaluated items. + /// This is an ordered collection + /// + /// + /// Private so we can make sure that itemsByEvaluatedInclude is updated + /// on changes. + /// + private ItemDictionary _items; + + /// + /// Items indexed by their evaluated include value. + /// Useful for hosts to find an item again after reevaluation. + /// + /// + /// Include value is unescaped + /// + private MultiDictionary _itemsByEvaluatedInclude; + + /// + /// Whether when we read a ToolsVersion that does not match the current one from the Project tag, we treat it as though it + /// was current. + /// + private bool _usingDifferentToolsVersionFromProjectFile; + + /// + /// The toolsversion that was originally on the project's Project root element + /// + private string _originalProjectToolsVersion; + + /// + /// Constructor taking the immutable global properties and tools version. + /// Tools version may be null. + /// + internal Data(Project project, PropertyDictionary globalProperties, string explicitToolsVersion, string explicitSubToolsetVersion) + { + _project = project; + _globalProperties = globalProperties; + this.ExplicitToolsVersion = explicitToolsVersion; + this.ExplicitSubToolsetVersion = explicitSubToolsetVersion; + } + + /// + /// Whether evaluation should collect items ignoring condition, + /// as well as items respecting condition; and collect + /// conditioned properties, as well as regular properties + /// + bool IEvaluatorData.ShouldEvaluateForDesignTime + { + get { return true; } + } + + /// + /// Collection of all evaluated item definitions, one per item-type + /// + IEnumerable IEvaluatorData.ItemDefinitionsEnumerable + { + get { return ItemDefinitions.Values; } + } + + /// + /// DefaultTargets specified in the project, or + /// the logically first target if no DefaultTargets is + /// specified in the project. + /// + public List DefaultTargets + { + get; + set; + } + + /// + /// The global properties to evaluate with, if any. + /// Can never be null. + /// Read-only; to use different global properties, evaluate yourself a new project. + /// + public PropertyDictionary GlobalPropertiesDictionary + { + get { return _globalProperties; } + } + + /// + /// List of names of the properties that, while global, are still treated as overridable + /// + public ISet GlobalPropertiesToTreatAsLocal + { + get + { + if (_globalPropertiesToTreatAsLocal == null) + { + _globalPropertiesToTreatAsLocal = new HashSet(MSBuildNameIgnoreCaseComparer.Default); + } + + return _globalPropertiesToTreatAsLocal; + } + } + + /// + /// InitialTargets specified in the project, plus those + /// in all imports, gathered depth-first. + /// + public List InitialTargets + { + get; + set; + } + + /// + /// Sets or retrieves the list of targets which run before the keyed target. + /// + public IDictionary> BeforeTargets + { + get; + set; + } + + /// + /// Sets or retrieves the list of targets which run after the keyed target. + /// + public IDictionary> AfterTargets + { + get; + set; + } + + /// + /// The externally specified tools version, if any. + /// For example, the tools version from a /tv switch. + /// Not necessarily the same as the tools version from the project tag or of the toolset used. + /// May be null. + /// Flows through to called projects. + /// + public string ExplicitToolsVersion + { + get; + private set; + } + + /// + /// The toolset data used during evaluation. + /// + public Toolset Toolset + { + get; + private set; + } + + /// + /// The externally specified sub-toolset version that, combined with the ToolsVersion, is used to determine + /// the toolset properties for this project. + /// + public string ExplicitSubToolsetVersion + { + get; + private set; + } + + /// + /// The sub-toolset version that, combined with the ToolsVersion, was used to determine + /// the toolset properties for this project. + /// + public string SubToolsetVersion + { + get; + private set; + } + + /// + /// Items in this project, ordered within groups of item types. + /// Protected by an upcast to IEnumerable. + /// + public ItemDictionary Items + { + get { return _items; } + } + + /// + /// List of items that link the XML items and evaluated items, + /// evaluated as if their conditions were true. + /// This is useful for hosts that wish to display all items regardless of their condition. + /// This is an ordered collection. + /// + public ItemDictionary ItemsIgnoringCondition + { + get; + private set; + } + + /// + /// Collection of properties that link the XML properties and evaluated properties. + /// Since evaluation has occurred, this is an unordered collection. + /// Includes any global and reserved properties. + /// + public PropertyDictionary Properties + { + get; + private set; + } + + /// + /// Collection of possible values implied for properties contained in the conditions found on properties, + /// property groups, imports, and whens. + /// + /// For example, if the following conditions existed on properties in a project: + /// + /// Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'" + /// Condition="'$(Configuration)' == 'Release'" + /// + /// the table would be populated with + /// + /// { "Configuration", { "Debug", "Release" }} + /// { "Platform", { "x86" }} + /// + /// This is used by Visual Studio to determine the configurations defined in the project. + /// + public Dictionary> ConditionedProperties + { + get; + private set; + } + + /// + /// The root directory for this project + /// + public string Directory + { + get + { + return _project.DirectoryPath; + } + } + + /// + /// Registry of usingtasks, for build + /// + public Execution.TaskRegistry TaskRegistry + { + get; + set; + } + + /// + /// Get the item types that have at least one item. + /// Read only collection. + /// + /// + /// item.ItemTypes is a KeyCollection, so it doesn't need any + /// additional read-only protection + /// + public ICollection ItemTypes + { + get { return _items.ItemTypes; } + } + + /// + /// Properties encountered during evaluation. These are read during the first evaluation pass. + /// Unlike those returned by the Properties property, these are ordered, and includes any properties that + /// were subsequently overridden by others with the same name. It does not include any + /// properties whose conditions did not evaluate to true. + /// It does not include any properties added since the last evaluation. + /// + internal IList AllEvaluatedProperties + { + get; + private set; + } + + /// + /// Item definition metadata encountered during evaluation. These are read during the second evaluation pass. + /// Unlike those returned by the ItemDefinitions property, these are ordered, and include any metadata that + /// were subsequently overridden by others with the same name and item type. It does not include any + /// elements whose conditions did not evaluate to true. + /// It does not include any item definition metadata added since the last evaluation. + /// + internal IList AllEvaluatedItemDefinitionMetadata + { + get; + private set; + } + + /// + /// Items encountered during evaluation. These are read during the third evaluation pass. + /// Unlike those returned by the Items property, these are ordered. + /// It does not include any elements whose conditions did not evaluate to true. + /// It does not include any items added since the last evaluation. + /// + internal IList AllEvaluatedItems + { + get; + private set; + } + + /// + /// Expander to use to expand any expressions encountered after the project has been fully evaluated. + /// For example, to expand the values of any properties added at design time. + /// It's convenient to store it here. + /// + internal Expander Expander + { + get; + private set; + } + + /// + /// Whether something in this data has been modified since evaluation. + /// For example, a global property has been set. + /// + internal bool HasUnsavedChanges + { + get; + set; + } + + /// + /// Collection of all evaluated item definitions, one per item-type + /// + internal RetrievableEntryHashSet ItemDefinitions + { + get; + private set; + } + + /// + /// Project that owns this data + /// + internal Project Project + { + get { return _project; } + } + + /// + /// Targets in the project, used to build + /// + internal RetrievableEntryHashSet Targets + { + get; + set; + } + + /// + /// Complete list of all imports pulled in during evaluation. + /// This includes the outer project itself. + /// + internal List> ImportClosure + { + get; + private set; + } + + /// + /// Complete list of all imports pulled in during evaluation including duplicate imports. + /// This includes the outer project itself. + /// + internal List> ImportClosureWithDuplicates + { + get; + private set; + } + + /// + /// The toolsversion that was originally specified on the project's root element + /// + internal string OriginalProjectToolsVersion + { + get { return _originalProjectToolsVersion; } + } + + /// + /// Whether when we read a ToolsVersion other than the current one in the Project tag, we treat it as the current one. + /// + internal bool UsingDifferentToolsVersionFromProjectFile + { + get { return _usingDifferentToolsVersionFromProjectFile; } + } + + /// + /// expose mutable precalculated cache to outside so that other can take advantage of the cache as well. + /// + internal MultiDictionary ItemsByEvaluatedIncludeCache + { + get + { + return _itemsByEvaluatedInclude; + } + } + + /// + /// Prepares the data object for evaluation. + /// + public void InitializeForEvaluation(IToolsetProvider toolsetProvider) + { + this.DefaultTargets = null; + this.Properties = new PropertyDictionary(); + this.ConditionedProperties = new Dictionary>(MSBuildNameIgnoreCaseComparer.Default); + _items = new ItemDictionary(); + this.ItemsIgnoringCondition = new ItemDictionary(); + _itemsByEvaluatedInclude = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + this.Expander = new Expander(this.Properties, _items); + this.ItemDefinitions = new RetrievableEntryHashSet(MSBuildNameIgnoreCaseComparer.Default); + this.Targets = new RetrievableEntryHashSet(OrdinalIgnoreCaseKeyedComparer.Instance); + this.ImportClosure = new List>(); + this.ImportClosureWithDuplicates = new List>(); + this.AllEvaluatedProperties = new List(); + this.AllEvaluatedItemDefinitionMetadata = new List(); + this.AllEvaluatedItems = new List(); + + if (_globalPropertiesToTreatAsLocal != null) + { + _globalPropertiesToTreatAsLocal.Clear(); + } + + // Include the main project in the list of imports, as this list is + // used to figure out if any of them have changed. + RecordImport(null, _project._xml, _project._xml.Version); + + string toolsVersionToUse = ExplicitToolsVersion; + ElementLocation toolsVersionLocation = _project._xml.ProjectFileLocation; + bool explicitToolsVersionSpecified = (ExplicitToolsVersion != null); + + if (_project._xml.ToolsVersion.Length > 0) + { + _originalProjectToolsVersion = _project._xml.ToolsVersion; + toolsVersionLocation = _project._xml.ToolsVersionLocation; + } + + toolsVersionToUse = Utilities.GenerateToolsVersionToUse + ( + ExplicitToolsVersion, + _project._xml.ToolsVersion, + Project.ProjectCollection.GetToolset, + Project.ProjectCollection.DefaultToolsVersion + ); + + // Don't log the message if the toolsversion is different because an explicit toolsversion was specified -- + // in that case the user already knows what they're doing; the point of this warning is to give them a heads + // up if we're doing this ourselves for our own reasons. + if (!explicitToolsVersionSpecified && !String.Equals(_originalProjectToolsVersion, toolsVersionToUse, StringComparison.OrdinalIgnoreCase)) + { + _usingDifferentToolsVersionFromProjectFile = true; + } + + Toolset = toolsetProvider.GetToolset(toolsVersionToUse); + + if (Toolset == null) + { + string toolsVersionList = Microsoft.Build.Internal.Utilities.CreateToolsVersionListString(Project.ProjectCollection.Toolsets); + ProjectErrorUtilities.ThrowInvalidProject(toolsVersionLocation, "UnrecognizedToolsVersion", toolsVersionToUse, toolsVersionList); + } + + if (this.ExplicitSubToolsetVersion != null) + { + this.SubToolsetVersion = this.ExplicitSubToolsetVersion; + } + else + { + this.SubToolsetVersion = this.Toolset.GenerateSubToolsetVersion(_globalProperties); + } + + // Create a task registry which will fall back on the toolset task registry if necessary. + TaskRegistry = new TaskRegistry(Toolset, _project._projectCollection.ProjectRootElementCache); + } + + /// + /// Indicates to the data block that evaluation has completed, + /// so for example it can mark datastructures read-only. + /// + public void FinishEvaluation() + { + // We assume there will be no further changes to the targets collection + // This also makes sure that we are thread safe + Targets.MakeReadOnly(); + + if (s_typicalTargetsCollection == null) + { + Targets.TrimExcess(); + s_typicalTargetsCollection = new System.WeakReference>(Targets); + } + else + { + // Attempt to unify the references, to save space + RetrievableEntryHashSet candidate; + if (s_typicalTargetsCollection.TryGetTarget(out candidate) && candidate.EntriesAreReferenceEquals(Targets)) + { + // Reuse + Targets = candidate; + } + else + { + // Else we'll guess that this latest one is a potential match for the next, + // if it actually has any elements (eg., it's not a .user or .filters file) + if (Targets.Count > 0) + { + Targets.TrimExcess(); + s_typicalTargetsCollection.SetTarget(Targets); + } + } + } + } + + /// + /// Adds a new item. + /// + public void AddItem(ProjectItem item) + { + _items.Add(item); + _itemsByEvaluatedInclude.Add(item.EvaluatedInclude, item); + } + + /// + /// Adds a new item to the collection of all items ignoring condition + /// + public void AddItemIgnoringCondition(ProjectItem item) + { + ItemsIgnoringCondition.Add(item); + } + + /// + /// Properties encountered during evaluation. These are read during the first evaluation pass. + /// Unlike those returned by the Properties property, these are ordered, and includes any properties that + /// were subsequently overridden by others with the same name. It does not include any + /// properties whose conditions did not evaluate to true. + /// + public void AddToAllEvaluatedPropertiesList(ProjectProperty property) + { + ErrorUtilities.VerifyThrowInternalNull(property, "property"); + AllEvaluatedProperties.Add(property); + } + + /// + /// Item definition metadata encountered during evaluation. These are read during the second evaluation pass. + /// Unlike those returned by the ItemDefinitions property, these are ordered, and include any metadata that + /// were subsequently overridden by others with the same name and item type. It does not include any + /// elements whose conditions did not evaluate to true. + /// + public void AddToAllEvaluatedItemDefinitionMetadataList(ProjectMetadata itemDefinitionMetadatum) + { + ErrorUtilities.VerifyThrowInternalNull(itemDefinitionMetadatum, "itemDefinitionMetadatum"); + AllEvaluatedItemDefinitionMetadata.Add(itemDefinitionMetadatum); + } + + /// + /// Items encountered during evaluation. These are read during the third evaluation pass. + /// Unlike those returned by the Items property, these are ordered. + /// It does not include any elements whose conditions did not evaluate to true. + /// It does not include any items added since the last evaluation. + /// + public void AddToAllEvaluatedItemsList(ProjectItem item) + { + ErrorUtilities.VerifyThrowInternalNull(item, "item"); + AllEvaluatedItems.Add(item); + } + + /// + /// Adds a new item definition + /// + public IItemDefinition AddItemDefinition(string itemType) + { + ProjectItemDefinition newItemDefinition = new ProjectItemDefinition(this.Project, itemType); + + ItemDefinitions.Add(newItemDefinition); + + return newItemDefinition; + } + + /// + /// Gets an existing item definition, if any. + /// + public IItemDefinition GetItemDefinition(string itemType) + { + ProjectItemDefinition itemDefinition; + ItemDefinitions.TryGetValue(itemType, out itemDefinition); + + return itemDefinition; + } + + /// + /// Sets a property which is not derived from Xml. + /// + public ProjectProperty SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + { + ProjectProperty property = ProjectProperty.Create(this.Project, name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved); + Properties.Set(property); + + AddToAllEvaluatedPropertiesList(property); + + return property; + } + + /// + /// Sets a property derived from Xml. + /// Predecessor is any immediately previous property that was overridden by this one during evaluation. + /// This would include all properties with the same name that lie above in the logical + /// project file, and whose conditions evaluated to true. + /// If there are none above this is null. + /// + public ProjectProperty SetProperty(ProjectPropertyElement propertyElement, string evaluatedValueEscaped, ProjectProperty predecessor) + { + ProjectProperty property = ProjectProperty.Create(this.Project, propertyElement, evaluatedValueEscaped, predecessor); + Properties.Set(property); + + AddToAllEvaluatedPropertiesList(property); + + return property; + } + + /// + /// Retrieves an existing target, if any. + /// + public ProjectTargetInstance GetTarget(string targetName) + { + ProjectTargetInstance target; + + Targets.TryGetValue(targetName, out target); + + return target; + } + + /// + /// Adds the specified target, overwriting any existing target with the same name. + /// + public void AddTarget(ProjectTargetInstance target) + { + Targets[target.Name] = target; + } + + /// + /// Record an import opened during evaluation. + /// This is used to check later whether any of them have been changed. + /// + /// + /// This may include imported files that ended up contributing nothing to the evaluated project. + /// These might otherwise have no strong references to them at all. + /// If they are dirtied, though, they might affect the evaluated project; and that's why we record them. + /// Mostly these will be common imports, so they'll be shared anyway. + /// + public void RecordImport(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated) + { + ImportClosure.Add(new Triple(importElement, import, versionEvaluated)); + RecordImportWithDuplicates(importElement, import, versionEvaluated); + } + + /// + /// Record a duplicate import, possible a duplicate import opened during evaluation. + /// + public void RecordImportWithDuplicates(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated) + { + ImportClosureWithDuplicates.Add(new Triple(importElement, import, versionEvaluated)); + } + + /// + /// Evaluates the provided string by expanding items and properties, + /// using the current items and properties available. + /// This is useful for the immediate window. + /// Does not expand bare metadata expressions. + /// + /// + /// Not for internal use. + /// + string IEvaluatorData.ExpandString(string unexpandedValue) + { + return _project.ExpandString(unexpandedValue); + } + + /// + /// Evaluates the provided string as a condition by expanding items and properties, + /// using the current items and properties available, then doing a logical evaluation. + /// This is useful for the immediate window. + /// Does not expand bare metadata expressions. + /// + /// + /// Not for internal use. + /// + public bool EvaluateCondition(string condition) + { + // This is for the debugger, which should not get a live Project object, + // so this is not implemented. + ErrorUtilities.ThrowInternalErrorUnreachable(); + return false; + } + + #region IItemProvider Members + + /// + /// Returns a list of items of the specified type. + /// If there are none, returns an empty list. + /// + /// + /// ItemDictionary returns a read-only collection, so no need to wrap it here. + /// + /// The type of items to return. + /// A list of matching items. + public ICollection GetItems(string itemType) + { + return _items[itemType]; + } + + #endregion + + #region IPropertyProvider Members + + /// + /// Returns the property with the specified name or null if it was not present + /// + /// The property name. + /// The property. + public ProjectProperty GetProperty(string name) + { + return Properties[name]; + } + + /// + /// Returns the property with the specified name or null if it was not present + /// + /// The property name. + /// The property. + public ProjectProperty GetProperty(string name, int startIndex, int endIndex) + { + return Properties.GetProperty(name, startIndex, endIndex); + } + + #endregion + + /// + /// Clears out certain cached values. + /// FOR UNIT TESTING ONLY + /// + internal static void ClearCachedFlags() + { + s_shouldTreatHigherToolsVersionsAsCurrent = false; + s_shouldTreatOtherToolsVersionsAsCurrent = false; + } + + /// + /// Removes an item. + /// Returns true if it was previously present, otherwise false. + /// + internal bool RemoveItem(ProjectItem item) + { + bool result = _items.Remove(item); + + // This remove will not succeed if the item include was changed. + // If many items are modified and then removed, this will leak them + // until the next reevaluation. + _itemsByEvaluatedInclude.Remove(item.EvaluatedInclude, item); + + ItemsIgnoringCondition.Remove(item); + + return result; + } + + /// + /// Returns all items that have the specified evaluated include. + /// For example, all items that have the evaluated include "bar.cpp". + /// Typically there will be no more than one, but sometimes there are two items with the + /// same path and different item types, or even the same item types. This will return + /// them all. + /// + /// + /// Assumes that the evaluated include value is unescaped. + /// + internal ICollection GetItemsByEvaluatedInclude(string evaluatedInclude) + { + // Even if there are no items in itemsByEvaluatedInclude[], it will return an IEnumerable, which is non-null + ICollection items = new ReadOnlyCollection(_itemsByEvaluatedInclude[evaluatedInclude]); + + return items; + } + + /// + /// Get the value of a property in this project, or + /// an empty string if it does not exist. + /// Returns the unescaped value. + /// + /// + /// A property with a value of empty string and no property + /// at all are not distinguished between by this method. + /// That makes it easier to use. To find out if a property is set at + /// all in the project, use GetProperty(name). + /// + internal string GetPropertyValue(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + ProjectProperty property = Properties[name]; + string value = (property == null) ? String.Empty : property.EvaluatedValue; + return value; + } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ProjectCollection.cs b/src/XMakeBuildEngine/Definition/ProjectCollection.cs new file mode 100644 index 00000000000..432b8f3b18c --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ProjectCollection.cs @@ -0,0 +1,2416 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A collection of loaded projects. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Xml; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; + +using Constants = Microsoft.Build.Internal.Constants; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using LoggerMode = Microsoft.Build.BackEnd.Logging.LoggerMode; +using ObjectModel = System.Collections.ObjectModel; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using Utilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Flags for controlling the toolset initialization. + /// + [Flags] + public enum ToolsetDefinitionLocations + { + /// + /// Do not read toolset information from any external location. + /// + None = 0, + + /// + /// Read toolset information from the exe configuration file. + /// + ConfigurationFile = 1, + + /// + /// Read toolset information from the registry (HKLM\Software\Microsoft\MSBuild\ToolsVersions). + /// + Registry = 2 + } + + /// + /// This class encapsulates a set of related projects, their toolsets, a default set of global properties, + /// and the loggers that should be used to build them. + /// A global version of this class acts as the default ProjectCollection. + /// Multiple ProjectCollections can exist within an appdomain. However, these must not build concurrently. + /// + [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Justification = "This is a collection of projects API review has approved this")] + public class ProjectCollection : IToolsetProvider, IBuildComponent, IDisposable + { + /// + /// The object to synchronize with when accessing certain fields. + /// + private readonly object _locker = new object(); + + /// + /// The global singleton project collection used as a default for otherwise + /// unassociated projects. + /// + private static ProjectCollection s_globalProjectCollection; + + /// + /// Gets the file version of the file in which the Engine assembly lies. + /// + /// + /// This is the Windows file version (specifically the value of the ProductVersion + /// resource), not necessarily the assembly version. + /// If you want the assembly version, use Constants.AssemblyVersion. + /// + private static Version s_engineVersion; + + /// + /// The projects loaded into this collection. + /// + private LoadedProjectCollection _loadedProjects; + + /// + /// The component host for this collection. + /// + private IBuildComponentHost _host; + + /// + /// Single logging service used for all builds of projects in this project collection + /// + private ILoggingService _loggingService; + + /// + /// Any object exposing host services. + /// May be null. + /// + private HostServices _hostServices; + + /// + /// The locations where we look for toolsets. + /// + private ToolsetDefinitionLocations _toolsetDefinitionLocations; + + /// + /// A mapping of tools versions to Toolsets, which contain the public Toolsets. + /// This is the collection we use internally. + /// + private Dictionary _toolsets; + + /// + /// The default global properties. + /// + private PropertyDictionary _globalProperties; + + /// + /// The properties representing the environment. + /// + private PropertyDictionary _environmentProperties; + + /// + /// The default tools version obtained by examining all of the toolsets. + /// + private string _defaultToolsVersion; + + /// + /// A counter incremented every time the toolsets change which would necessitate a re-evaluation of + /// associated projects. + /// + private int _toolsetsVersion; + + /// + /// This is the default value used by newly created projects for whether or not the building + /// of projects is enabled. This is for security purposes in case a host wants to closely + /// control which projects it allows to run targets/tasks. + /// + private bool _isBuildEnabled = true; + + /// + /// We may only wish to log crtitical events, record that fact so we can apply it to build requests + /// + private bool _onlyLogCriticalEvents; + + /// + /// Whether reevaluation is temporarily disabled on projects in this collection. + /// This is useful when the host expects to make a number of reads and writes + /// to projects, and wants to temporarily sacrifice correctness for performance. + /// + private bool _skipEvaluation; + + /// + /// Whether MarkDirty() is temporarily disabled on + /// projects in this collection. + /// This allows, for example, global properties to be set without projects getting + /// marked dirty for reevaluation as a consequence. + /// + private bool _disableMarkDirty; + + /// + /// The maximum number of nodes which can be started during the build + /// + private int _maxNodeCount; + + /// + /// The cache of project root elements associated with this project collection. + /// Each is associated with a specific project collection for two reasons: + /// - To help protect one project collection from any XML edits through another one: + /// until a reload from disk - when it's ready to accept changes - it won't see the edits; + /// - So that the owner of this project collection can force the XML to be loaded again + /// from disk, by doing . + /// + private ProjectRootElementCache _projectRootElementCache; + + /// + /// Hook up last minute dumping of any exceptions bringing down the process + /// + static ProjectCollection() + { + AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(ExceptionHandling.UnhandledExceptionHandler); + } + + /// + /// Instantiates a project collection with no global properties or loggers that reads toolset + /// information from the configuration file and registry. + /// + public ProjectCollection() + : this((IDictionary)null) + { + } + + /// + /// Instantiates a project collection using toolsets from the specified locations, + /// and no global properties or loggers. + /// May throw InvalidToolsetDefinitionException. + /// + /// The locations from which to load toolsets. + public ProjectCollection(ToolsetDefinitionLocations toolsetLocations) + : this(null, null, toolsetLocations) + { + } + + /// + /// Instantiates a project collection with specified global properties, no loggers, + /// and that reads toolset information from the configuration file and registry. + /// May throw InvalidToolsetDefinitionException. + /// + /// The default global properties to use. May be null. + public ProjectCollection(IDictionary globalProperties) + : this(globalProperties, null, ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry) + { + } + + /// + /// Instantiates a project collection with specified global properties and loggers and using the + /// specified toolset locations. + /// May throw InvalidToolsetDefinitionException. + /// + /// The default global properties to use. May be null. + /// The loggers to register. May be null. + /// The locations from which to load toolsets. + public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, ToolsetDefinitionLocations toolsetDefinitionLocations) + : this(globalProperties, loggers, null, toolsetDefinitionLocations, 1 /* node count */, false /* do not only log critical events */) + { + } + + /// + /// Instantiates a project collection with specified global properties and loggers and using the + /// specified toolset locations, node count, and setting of onlyLogCriticalEvents. + /// Global properties and loggers may be null. + /// Throws InvalidProjectFileException if any of the global properties are reserved. + /// May throw InvalidToolsetDefinitionException. + /// + /// The default global properties to use. May be null. + /// The loggers to register. May be null and specified to any build instead. + /// Any remote loggers to register. May be null and specified to any build instead. + /// The locations from which to load toolsets. + /// The maximum number of nodes to use for building. + /// If set to true, only critical events will be logged. + public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents) + { + _loadedProjects = new LoadedProjectCollection(); + _toolsetDefinitionLocations = toolsetDefinitionLocations; + this.MaxNodeCount = maxNodeCount; + this.ProjectRootElementCache = new ProjectRootElementCache(false /* do not automatically reload changed files from disk */); + this.OnlyLogCriticalEvents = onlyLogCriticalEvents; + + try + { + CreateLoggingService(maxNodeCount, onlyLogCriticalEvents); + + RegisterLoggers(loggers); + RegisterForwardingLoggers(remoteLoggers); + + if (globalProperties != null) + { + _globalProperties = new PropertyDictionary(globalProperties.Count); + + foreach (KeyValuePair pair in globalProperties) + { + try + { + _globalProperties.Set(ProjectPropertyInstance.Create(pair.Key, pair.Value)); + } + catch (ArgumentException ex) + { + // Reserved or invalid property name + try + { + ProjectErrorUtilities.ThrowInvalidProject(ElementLocation.Create("MSBUILD"), "InvalidProperty", ex.Message); + } + catch (InvalidProjectFileException ex2) + { + BuildEventContext buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + LoggingService.LogInvalidProjectFileError(buildEventContext, ex2); + throw; + } + } + } + } + else + { + _globalProperties = new PropertyDictionary(); + } + + InitializeToolsetCollection(); + } + catch (Exception) + { + ShutDownLoggingService(); + throw; + } + + ProjectRootElementCache.ProjectRootElementAddedHandler += new Evaluation.ProjectRootElementCache.ProjectRootElementCacheAddEntryHandler(ProjectRootElementCache_ProjectRootElementAddedHandler); + ProjectRootElementCache.ProjectRootElementDirtied += ProjectRootElementCache_ProjectRootElementDirtiedHandler; + ProjectRootElementCache.ProjectDirtied += ProjectRootElementCache_ProjectDirtiedHandler; + } + + /// + /// Handler to recieve which project got added to the project collection. + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "This has been API reviewed")] + public delegate void ProjectAddedEventHandler(object sender, ProjectAddedToProjectCollectionEventArgs e); + + /// + /// Event that is fired when a project is added to the ProjectRootElementCache of this project collection. + /// + public event ProjectAddedEventHandler ProjectAdded; + + /// + /// Raised when state is changed on this instance. + /// + /// + /// This event is NOT raised for changes in individual projects. + /// + public event EventHandler ProjectCollectionChanged; + + /// + /// Raised when a contained by this instance is changed. + /// + /// + /// This event is NOT raised for changes to global properties, or any other change that doesn't actually dirty the XML. + /// + public event EventHandler ProjectXmlChanged; + + /// + /// Raised when a contained by this instance is directly changed. + /// + /// + /// This event is NOT raised for direct project XML changes via the construction model. + /// + public event EventHandler ProjectChanged; + + /// + /// Retrieves the global project collection object. + /// This is a singleton project collection with no global properties or loggers that reads toolset + /// information from the configuration file and registry. + /// May throw InvalidToolsetDefinitionException. + /// Thread safe. + /// + public static ProjectCollection GlobalProjectCollection + { + get + { + if (s_globalProjectCollection == null) + { + // Take care to ensure that there is never more than one value observed + // from this property even in the case of race conditions while lazily initializing. + var local = new ProjectCollection(); + Interlocked.CompareExchange(ref s_globalProjectCollection, local, null); + } + + return s_globalProjectCollection; + } + } + + /// + /// Gets the file version of the file in which the Engine assembly lies. + /// + /// + /// This is the Windows file version (specifically the value of the ProductVersion + /// resource), not necessarily the assembly version. + /// If you want the assembly version, use Constants.AssemblyVersion. + /// This is not the ToolsetCollectionVersion. + /// + public static Version Version + { + get + { + if (s_engineVersion == null) + { + // Get the file version from the currently executing assembly. + // Use .CodeBase instead of .Location, because .Location doesn't + // work when Microsoft.Build.dll has been shadow-copied, for example + // in scenarios where NUnit is loading Microsoft.Build. + s_engineVersion = new Version(FileVersionInfo.GetVersionInfo(FileUtilities.ExecutingAssemblyPath).ProductVersion); + } + + return s_engineVersion; + } + } + + /// + /// The default tools version of this project collection. Projects use this tools version if they + /// aren't otherwise told what tools version to use. + /// This value is gotten from the .exe.config file, or else in the registry, + /// or if neither specify a default tools version then it is hard-coded to the tools version "2.0". + /// Setter throws InvalidOperationException if a toolset with the provided tools version has not been defined. + /// Always defined. + /// + public string DefaultToolsVersion + { + get + { + lock (_locker) + { + ErrorUtilities.VerifyThrow(_defaultToolsVersion != null, "Should have a default"); + return _defaultToolsVersion; + } + } + + set + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentLength(value, "DefaultToolsVersion"); + + if (!_toolsets.ContainsKey(value)) + { + string toolsVersionList = Microsoft.Build.Internal.Utilities.CreateToolsVersionListString(Toolsets); + ErrorUtilities.ThrowInvalidOperation("UnrecognizedToolsVersion", value, toolsVersionList); + } + + if (_defaultToolsVersion != value) + { + _defaultToolsVersion = value; + + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.DefaultToolsVersion); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + } + + /// + /// Returns default global properties for all projects in this collection. + /// Read-only dead dictionary. + /// + /// + /// This is the publicly exposed getter, that translates into a read-only dead IDictionary<string, string>. + /// + /// To be consistent with Project, setting and removing global properties is done with + /// SetGlobalProperty and RemoveGlobalProperty. + /// + public IDictionary GlobalProperties + { + get + { + lock (_locker) + { + if (_globalProperties.Count == 0) + { + return ReadOnlyEmptyDictionary.Instance; + } + + Dictionary dictionary = new Dictionary(_globalProperties.Count, MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectPropertyInstance property in _globalProperties) + { + dictionary[property.Name] = ((IProperty)property).EvaluatedValueEscaped; + } + + return new ObjectModel.ReadOnlyDictionary(dictionary); + } + } + } + + /// + /// All the projects currently loaded into this collection. + /// Each has a unique combination of path, global properties, and tools version. + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public ICollection LoadedProjects + { + get + { + lock (_locker) + { + return new List(_loadedProjects); + } + } + } + + /// + /// Number of projects currently loaded into this collection. + /// + public int Count + { + get + { + lock (_locker) + { + return _loadedProjects.Count; + } + } + } + + /// + /// Loggers that all contained projects will use for their builds. + /// Loggers are added with the . + /// UNDONE: Currently they cannot be removed. + /// Returns an empty collection if there are no loggers. + /// + public ICollection Loggers + { + [DebuggerStepThrough] + get + { + lock (_locker) + { + return (_loggingService.Loggers == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new List(_loggingService.Loggers); + } + } + } + + /// + /// Returns the toolsets this ProjectCollection knows about. + /// + /// + /// ValueCollection is already read-only + /// + public ICollection Toolsets + { + get + { + lock (_locker) + { + return new List(_toolsets.Values); + } + } + } + + /// + /// Returns the locations used to find the toolsets. + /// + public ToolsetDefinitionLocations ToolsetLocations + { + [DebuggerStepThrough] + get + { + lock (_locker) + { + return _toolsetDefinitionLocations; + } + } + } + + /// + /// This is the default value used by newly created projects for whether or not the building + /// of projects is enabled. This is for security purposes in case a host wants to closely + /// control which projects it allows to run targets/tasks. + /// + public bool IsBuildEnabled + { + [DebuggerStepThrough] + get + { + lock (_locker) + { + return _isBuildEnabled; + } + } + + [DebuggerStepThrough] + set + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + if (_isBuildEnabled != value) + { + _isBuildEnabled = value; + + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.IsBuildEnabled); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + } + + /// + /// When true, only log critical events such as warnings and errors. Has to be in here for API compat + /// + public bool OnlyLogCriticalEvents + { + get + { + lock (_locker) + { + return _onlyLogCriticalEvents; + } + } + + set + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + if (_onlyLogCriticalEvents != value) + { + _onlyLogCriticalEvents = value; + + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.OnlyLogCriticalEvents); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + } + + /// + /// Object exposing host services to tasks during builds of projects + /// contained by this project collection. + /// By default, HostServices is used. + /// May be set to null, but the getter will create a new instance in that case. + /// + public HostServices HostServices + { + get + { + lock (_locker) + { + if (_hostServices == null) + { + _hostServices = new HostServices(); + } + + return _hostServices; + } + } + + set + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + if (_hostServices != value) + { + _hostServices = value; + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.HostServices); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + } + + /// + /// Whether reevaluation is temporarily disabled on projects in this collection. + /// This is useful when the host expects to make a number of reads and writes + /// to projects, and wants to temporarily sacrifice correctness for performance. + /// + public bool SkipEvaluation + { + get + { + lock (_locker) + { + return _skipEvaluation; + } + } + + set + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + if (_skipEvaluation != value) + { + _skipEvaluation = value; + + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.SkipEvaluation); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + } + + /// + /// Whether MarkDirty() is temporarily disabled on + /// projects in this collection. + /// This allows, for example, global properties to be set without projects getting + /// marked dirty for reevaluation as a consequence. + /// + public bool DisableMarkDirty + { + get + { + lock (_locker) + { + return _disableMarkDirty; + } + } + + set + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + if (_disableMarkDirty != value) + { + _disableMarkDirty = value; + + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.DisableMarkDirty); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + } + + /// + /// Logging service that should be used for project load and for builds + /// + internal ILoggingService LoggingService + { + [DebuggerStepThrough] + get + { + lock (_locker) + { + return _loggingService; + } + } + } + + /// + /// Gets default global properties for all projects in this collection. + /// Dead copy. + /// + internal PropertyDictionary GlobalPropertiesCollection + { + [DebuggerStepThrough] + get + { + lock (_locker) + { + var clone = new PropertyDictionary(); + + foreach (var property in _globalProperties) + { + clone.Set(property.DeepClone()); + } + + return clone; + } + } + } + + /// + /// Returns the property dictionary containing the properties representing the environment. + /// + internal PropertyDictionary EnvironmentProperties + { + get + { + lock (_locker) + { + // Retrieves the environment properties. + // This is only done once, when the project collection is created. Any subsequent + // environment changes will be ignored. Child nodes will be passed this set + // of properties in their build parameters. + if (null == _environmentProperties) + { + _environmentProperties = Microsoft.Build.Internal.Utilities.GetEnvironmentProperties(); + } + + return new PropertyDictionary(_environmentProperties); + } + } + } + + /// + /// Returns the internal version for this object's state. + /// Updated when toolsets change, indicating all contained projects are potentially invalid. + /// + internal int ToolsetsVersion + { + [DebuggerStepThrough] + get + { + lock (_locker) + { + return _toolsetsVersion; + } + } + } + + /// + /// The maximum number of nodes which can be started during the build + /// + internal int MaxNodeCount + { + get + { + lock (_locker) + { + return _maxNodeCount; + } + } + + set + { + lock (_locker) + { + _maxNodeCount = value; + } + } + } + + /// + /// The cache of project root elements associated with this project collection. + /// Each is associated with a specific project collection for two reasons: + /// - To help protect one project collection from any XML edits through another one: + /// until a reload from disk - when it's ready to accept changes - it won't see the edits; + /// - So that the owner of this project collection can force the XML to be loaded again + /// from disk, by doing . + /// + internal ProjectRootElementCache ProjectRootElementCache + { + get + { + // no locks required because this field is only set in the constructor. + return _projectRootElementCache; + } + + private set + { + // no locks required because this field is only set in the constructor. + _projectRootElementCache = value; + } + } + + /// + /// Escape a string using MSBuild escaping format. For example, "%3b" for ";". + /// Only characters that are especially significant to MSBuild parsing are escaped. + /// Callers can use this method to make a string safe to be parsed to other methods + /// that would otherwise expand it; or to make a string safe to be written to a project file. + /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Public API that has shipped")] + public static string Escape(string unescapedString) + { + return EscapingUtilities.Escape(unescapedString); + } + + /// + /// Unescape a string using MSBuild escaping format. For example, "%3b" for ";". + /// All escaped characters are unescaped. + /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Public API that has shipped")] + public static string Unescape(string escapedString) + { + return EscapingUtilities.UnescapeAll(escapedString); + } + + /// + /// Returns true if there is a toolset defined for the specified + /// tools version, otherwise false. + /// + public bool ContainsToolset(string toolsVersion) + { + lock (_locker) + { + bool result = GetToolset(toolsVersion) != null; + + return result; + } + } + + /// + /// Add a new toolset. + /// Replaces any existing toolset with the same tools version. + /// + public void AddToolset(Toolset toolset) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentNull(toolset, "toolset"); + + _toolsets[toolset.ToolsVersion] = toolset; + + _toolsetsVersion++; + } + + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets)); + } + + /// + /// Remove a toolset. + /// Returns true if it was present, otherwise false. + /// + public bool RemoveToolset(string toolsVersion) + { + bool changed; + lock (_locker) + { + changed = RemoveToolsetInternal(toolsVersion); + } + + if (changed) + { + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets)); + } + + return changed; + } + + /// + /// Removes all toolsets. + /// + public void RemoveAllToolsets() + { + bool changed = false; + lock (_locker) + { + List toolsets = new List(Toolsets); + + foreach (Toolset toolset in toolsets) + { + changed |= RemoveToolsetInternal(toolset.ToolsVersion); + } + } + + if (changed) + { + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets)); + } + } + + /// + /// Get the toolset with the specified tools version. + /// If it is not present, returns null. + /// + public Toolset GetToolset(string toolsVersion) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentLength(toolsVersion, "toolsVersion"); + + Toolset toolset; + _toolsets.TryGetValue(toolsVersion, out toolset); + + return toolset; + } + } + + /// + /// Figure out what ToolsVersion to use to actually build the project with. + /// + /// The user-specified ToolsVersion (through e.g. /tv: on the command line). May be null + /// The ToolsVersion from the project file. May be null + /// The ToolsVersion we should use to build this project. Should never be null. + public string GetEffectiveToolsVersion(string explicitToolsVersion, string toolsVersionFromProject) + { + return Utilities.GenerateToolsVersionToUse(explicitToolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion); + } + + /// + /// Returns any and all loaded projects with the provided path. + /// There may be more than one, if they are distinguished by global properties + /// and/or tools version. + /// + public ICollection GetLoadedProjects(string fullPath) + { + lock (_locker) + { + var loaded = new List(_loadedProjects.GetMatchingProjectsIfAny(fullPath)); + + return loaded; + } + } + + /// + /// Loads a project with the specified filename, using the collection's global properties and tools version. + /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded. + /// + /// The project file to load + /// A loaded project. + public Project LoadProject(string fileName) + { + return LoadProject(fileName, null); + } + + /// + /// Loads a project with the specified filename and tools version, using the collection's global properties. + /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded. + /// + /// The project file to load + /// The tools version to use. May be null. + /// A loaded project. + public Project LoadProject(string fileName, string toolsVersion) + { + return LoadProject(fileName, null /* use project collection's global properties */, toolsVersion); + } + + /// + /// Loads a project with the specified filename, tools version and global properties. + /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded. + /// + /// The project file to load + /// The global properties to use. May be null, in which case the containing project collection's global properties will be used. + /// The tools version. May be null. + /// A loaded project. + public Project LoadProject(string fileName, IDictionary globalProperties, string toolsVersion) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentLength(fileName, "fileName"); + BuildEventContext buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + + if (globalProperties == null) + { + globalProperties = this.GlobalProperties; + } + else + { + // We need to update the set of global properties to merge in the ProjectCollection global properties -- + // otherwise we might end up declaring "not matching" a project that actually does ... and then throw + // an exception when we go to actually add the newly created project to the ProjectCollection. + // BUT remember that project global properties win -- don't override a property that already exists. + foreach (KeyValuePair globalProperty in this.GlobalProperties) + { + if (!globalProperties.ContainsKey(globalProperty.Key)) + { + globalProperties.Add(globalProperty); + } + } + } + + // We do not control the current directory at this point, but assume that if we were + // passed a relative path, the caller assumes we will prepend the current directory. + fileName = FileUtilities.NormalizePath(fileName); + string toolsVersionFromProject = null; + + if (toolsVersion == null) + { + // Load the project XML to get any ToolsVersion attribute. + // If there isn't already an equivalent project loaded, the real load we'll do will be satisfied from the cache. + // If there is already an equivalent project loaded, we'll never need this XML -- but it'll already + // have been loaded by that project so it will have been satisfied from the ProjectRootElementCache. + // Either way, no time wasted. + try + { + ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(fileName, globalProperties, toolsVersion, LoggingService, ProjectRootElementCache, buildEventContext, true /*explicitlyloaded*/); + toolsVersionFromProject = (xml.ToolsVersion.Length > 0) ? xml.ToolsVersion : DefaultToolsVersion; + } + catch (InvalidProjectFileException ex) + { + LoggingService.LogInvalidProjectFileError(buildEventContext, ex); + throw; + } + } + + string effectiveToolsVersion = Utilities.GenerateToolsVersionToUse(toolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion); + Project project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion); + + if (project == null) + { + // The Project constructor adds itself to our collection, + // it is not done by us + project = new Project(fileName, globalProperties, effectiveToolsVersion, this); + } + + return project; + } + } + + /// + /// Loads a project with the specified reader, using the collection's global properties and tools version. + /// The project will be added to this project collection when it is named. + /// + /// Xml reader to read project from + /// A loaded project. + public Project LoadProject(XmlReader xmlReader) + { + return LoadProject(xmlReader, null); + } + + /// + /// Loads a project with the specified reader and tools version, using the collection's global properties. + /// The project will be added to this project collection when it is named. + /// + /// Xml reader to read project from + /// The tools version to use. May be null. + /// A loaded project. + public Project LoadProject(XmlReader xmlReader, string toolsVersion) + { + return LoadProject(xmlReader, null /* use project collection's global properties */, toolsVersion); + } + + /// + /// Loads a project with the specified reader, tools version and global properties. + /// The project will be added to this project collection when it is named. + /// + /// Xml reader to read project from + /// The global properties to use. May be null in which case the containing project collection's global properties will be used. + /// The tools version. May be null. + /// A loaded project. + public Project LoadProject(XmlReader xmlReader, IDictionary globalProperties, string toolsVersion) + { + return new Project(xmlReader, globalProperties, toolsVersion, this); + } + + /// + /// Adds a logger to the collection of loggers used for builds of projects in this collection. + /// If the logger object is already in the collection, does nothing. + /// + public void RegisterLogger(ILogger logger) + { + lock (_locker) + { + RegisterLoggerInternal(logger); + } + + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers)); + } + + /// + /// Adds some loggers to the collection of loggers used for builds of projects in this collection. + /// If any logger object is already in the collection, does nothing for that logger. + /// May be null. + /// + public void RegisterLoggers(IEnumerable loggers) + { + bool changed = false; + if (loggers != null) + { + lock (_locker) + { + foreach (ILogger logger in loggers) + { + RegisterLoggerInternal(logger); + changed = true; + } + } + } + + if (changed) + { + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers)); + } + } + + /// + /// Adds some remote loggers to the collection of remote loggers used for builds of projects in this collection. + /// May be null. + /// + public void RegisterForwardingLoggers(IEnumerable remoteLoggers) + { + lock (_locker) + { + if (remoteLoggers != null) + { + foreach (ForwardingLoggerRecord remoteLoggerRecord in remoteLoggers) + { + _loggingService.RegisterDistributedLogger(new ReusableLogger(remoteLoggerRecord.CentralLogger), remoteLoggerRecord.ForwardingLoggerDescription); + } + } + } + + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers)); + } + + /// + /// Removes all loggers from the collection of loggers used for builds of projects in this collection. + /// + public void UnregisterAllLoggers() + { + lock (_locker) + { + _loggingService.UnregisterAllLoggers(); + + // UNDONE: Logging service should not shut down when all loggers are unregistered. + // VS unregisters all loggers on the same project collection often. To workaround this, we have to create it again now! + CreateLoggingService(MaxNodeCount, OnlyLogCriticalEvents); + } + + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers)); + } + + /// + /// Unloads the specific project specified. + /// Host should call this when they are completely done with the project. + /// If project was not already loaded, throws InvalidOperationException. + /// + public void UnloadProject(Project project) + { + lock (_locker) + { + bool existed = _loadedProjects.RemoveProject(project); + + ErrorUtilities.VerifyThrowInvalidOperation(existed, "OM_ProjectWasNotLoaded"); + + project.Zombify(); + + // If we've removed the last entry for the given project full path + // then unregister any and all host objects for that project + if (_hostServices != null && _loadedProjects.GetMatchingProjectsIfAny(project.FullPath).Count == 0) + { + _hostServices.UnregisterProject(project.FullPath); + } + + // Release our own cache's strong references to try to help + // free memory. These may be the last references to the ProjectRootElements + // in the cache, so the cache shouldn't hold strong references to them of its own. + ProjectRootElementCache.DiscardStrongReferences(); + + // Aggressively release any strings from all the contributing documents. + // It's fine if we cache less (by now we likely did a lot of loading and got the benefits) + // If we don't do this, we could be releasing the last reference to a + // ProjectRootElement, causing it to fall out of the weak cache leaving its strings and XML + // behind in the string cache. + project.Xml.XmlDocument.ClearAnyCachedStrings(); + + foreach (var import in project.Imports) + { + import.ImportedProject.XmlDocument.ClearAnyCachedStrings(); + } + } + } + + /// + /// Unloads a project XML root element from the weak cache. + /// + /// The project XML root element to unload. + /// + /// Thrown if the project XML root element to unload is still in use by a loaded project or its imports. + /// + /// + /// This method is useful for the case where the host knows that all projects using this XML element + /// are unloaded, and desires to discard any unsaved changes. + /// + public void UnloadProject(ProjectRootElement projectRootElement) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentNull(projectRootElement, "projectRootElement"); + + Project conflictingProject = LoadedProjects.FirstOrDefault(project => project.UsesProjectRootElement(projectRootElement)); + + if (conflictingProject != null) + { + ErrorUtilities.ThrowInvalidOperation("OM_ProjectXmlCannotBeUnloadedDueToLoadedProjects", projectRootElement.FullPath, conflictingProject.FullPath); + } + + projectRootElement.XmlDocument.ClearAnyCachedStrings(); + ProjectRootElementCache.DiscardAnyWeakReference(projectRootElement); + } + } + + /// + /// Unloads all the projects contained by this ProjectCollection. + /// Host should call this when they are completely done with all the projects. + /// + public void UnloadAllProjects() + { + lock (_locker) + { + foreach (Project project in _loadedProjects) + { + project.Zombify(); + + // We're removing every entry from the project collection + // so unregister any and all host objects for each project + if (_hostServices != null) + { + _hostServices.UnregisterProject(project.FullPath); + } + } + + _loadedProjects.RemoveAllProjects(); + + ProjectRootElementCache.Clear(); + } + } + + /// + /// Get any global property on the project collection that has the specified name, + /// otherwise returns null. + /// + public ProjectPropertyInstance GetGlobalProperty(string name) + { + lock (_locker) + { + return _globalProperties[name]; + } + } + + /// + /// Set a global property at the collection-level, + /// and on all projects in the project collection. + /// + public void SetGlobalProperty(string name, string value) + { + ProjectCollectionChangedEventArgs eventArgs = null; + lock (_locker) + { + ProjectPropertyInstance propertyInGlobalProperties = _globalProperties.GetProperty(name); + bool changed = propertyInGlobalProperties == null || (!String.Equals(((IValued)propertyInGlobalProperties).EscapedValue, value, StringComparison.OrdinalIgnoreCase)); + + if (changed) + { + _globalProperties.Set(ProjectPropertyInstance.Create(name, value)); + eventArgs = new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.GlobalProperties); + } + + // Copy LoadedProjectCollection as modifying a project's global properties will cause it to re-add + List projects = new List(_loadedProjects); + foreach (Project project in projects) + { + project.SetGlobalProperty(name, value); + } + } + + OnProjectCollectionChangedIfNonNull(eventArgs); + } + + /// + /// Removes a global property from the collection-level set of global properties, + /// and all projects in the project collection. + /// If it was on this project collection, returns true. + /// + public bool RemoveGlobalProperty(string name) + { + bool set; + lock (_locker) + { + set = _globalProperties.Remove(name); + + // Copy LoadedProjectCollection as modifying a project's global properties will cause it to re-add + List projects = new List(_loadedProjects); + foreach (Project project in projects) + { + project.RemoveGlobalProperty(name); + } + } + + OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.GlobalProperties)); + + return set; + } + + /// + /// Called when a host is completely done with the project collection. + /// UNDONE: This is a hack to make sure the logging thread shuts down if the build used the loggingservice + /// off the ProjectCollection. After CTP we need to rationalize this and see if we can remove the logging service from + /// the project collection entirely so this isn't necessary. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region IBuildComponent Members + + /// + /// Initializes the component with the component host. + /// + /// The component host. + void IBuildComponent.InitializeComponent(IBuildComponentHost host) + { + _host = host; + } + + /// + /// Shuts down the component. + /// + void IBuildComponent.ShutdownComponent() + { + _host = null; + } + + #endregion + + /// + /// Unloads a project XML root element from the cache entirely, if it is not + /// in use by project loaded into this collection. + /// Returns true if it was unloaded successfully, or was not already loaded. + /// Returns false if it was not unloaded because it was still in use by a loaded . + /// + /// The project XML root element to unload. + public bool TryUnloadProject(ProjectRootElement projectRootElement) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentNull(projectRootElement, "projectRootElement"); + + ProjectRootElementCache.DiscardStrongReferences(); + + Project conflictingProject = LoadedProjects.FirstOrDefault(project => project.UsesProjectRootElement(projectRootElement)); + + if (conflictingProject == null) + { + ProjectRootElementCache.DiscardAnyWeakReference(projectRootElement); + projectRootElement.XmlDocument.ClearAnyCachedStrings(); + return true; + } + + return false; + } + } + + /// + /// Called by a Project object to load itself into this collection. + /// If the project was already loaded under a different name, it is unloaded. + /// Stores the project in the list of loaded projects if it has a name. + /// Does not store the project if it has no name because it has not been saved to disk yet. + /// If the project previously had a name, but was not in the collection already, throws InvalidOperationException. + /// If the project was not previously in the collection, sets the collection's global properties on it. + /// + internal void OnAfterRenameLoadedProject(string oldFullPathIfAny, Project project) + { + lock (_locker) + { + if (project.FullPath == null) + { + return; + } + + if (oldFullPathIfAny != null) + { + bool existed = _loadedProjects.RemoveProject(oldFullPathIfAny, project); + + ErrorUtilities.VerifyThrowInvalidOperation(existed, "OM_ProjectWasNotLoaded"); + } + + // The only time this ever gets called with a null full path is when the project is first being + // constructed. The mere fact that this method is being called means that this project will belong + // to this project collection. As such, it has already had all necessary global properties applied + // when being constructed -- we don't need to do anything special here. + // If we did add global properties here, we would just end up either duplicating work or possibly + // wiping out global properties set on the project meant to override the ProjectCollection copies. + _loadedProjects.AddProject(project); + + if (_hostServices != null) + { + HostServices.OnRenameProject(oldFullPathIfAny, project.FullPath); + } + } + } + + /// + /// Called after a loaded project's global properties are changed, so we can update + /// our loaded project table. + /// Project need not already be in the project collection yet, but it can't be in another one. + /// + /// + /// We have to remove and re-add so that there's an error if there's already an equivalent + /// project loaded. + /// + internal void AfterUpdateLoadedProjectGlobalProperties(Project project) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowInvalidOperation(Object.ReferenceEquals(project.ProjectCollection, this), "OM_IncorrectObjectAssociation", "Project", "ProjectCollection"); + + if (project.FullPath == null) + { + return; + } + + bool existed = _loadedProjects.RemoveProject(project); + + if (existed) + { + _loadedProjects.AddProject(project); + } + } + } + + /// + /// Following standard framework guideline dispose pattern. + /// Shut down logging service if the project collection owns one, in order + /// to shut down the logger thread and loggers. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources.. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + ShutDownLoggingService(); + Tracing.Dump(); + } + } + + /// + /// Remove a toolset and does not raise events. The caller should have acquired a lock on this method's behalf. + /// + /// The toolset to remove. + /// true if the toolset was found and removed; false otherwise. + private bool RemoveToolsetInternal(string toolsVersion) + { + ErrorUtilities.VerifyThrowArgumentLength(toolsVersion, "toolsVersion"); + Debug.Assert(Monitor.IsEntered(_locker)); + + if (!_toolsets.ContainsKey(toolsVersion)) + { + return false; + } + + _toolsets.Remove(toolsVersion); + + _toolsetsVersion++; + + return true; + } + + /// + /// Adds a logger to the collection of loggers used for builds of projects in this collection. + /// If the logger object is already in the collection, does nothing. + /// + private void RegisterLoggerInternal(ILogger logger) + { + ErrorUtilities.VerifyThrowArgumentNull(logger, "logger"); + Debug.Assert(Monitor.IsEntered(_locker)); + _loggingService.RegisterLogger(new ReusableLogger(logger)); + } + + /// + /// Handler which is called when a project is added to the RootElementCache of this project collection. We then fire an event indicating that a project was added to the collection itself. + /// + private void ProjectRootElementCache_ProjectRootElementAddedHandler(object sender, ProjectRootElementCache.ProjectRootElementCacheAddEntryEventArgs e) + { + if (ProjectAdded != null) + { + ProjectAdded(this, new ProjectAddedToProjectCollectionEventArgs(e.RootElement)); + } + } + + /// + /// Handler which is called when a project that is part of this collection is dirtied. We then fire an event indicating that a project has been dirtied. + /// + private void ProjectRootElementCache_ProjectRootElementDirtiedHandler(object sender, ProjectXmlChangedEventArgs e) + { + OnProjectXmlChanged(e); + } + + /// + /// Handler which is called when a project is dirtied. + /// + private void ProjectRootElementCache_ProjectDirtiedHandler(object sender, ProjectChangedEventArgs e) + { + OnProjectChanged(e); + } + + /// + /// Raises the event. + /// + /// The event arguments that indicate ProjectRootElement-specific details. + private void OnProjectXmlChanged(ProjectXmlChangedEventArgs e) + { + var projectXmlChanged = this.ProjectXmlChanged; + if (projectXmlChanged != null) + { + projectXmlChanged(this, e); + } + } + + /// + /// Raises the event. + /// + /// The event arguments that indicate Project-specific details. + private void OnProjectChanged(ProjectChangedEventArgs e) + { + var projectChanged = this.ProjectChanged; + if (projectChanged != null) + { + projectChanged(this, e); + } + } + + /// + /// Raises the event. + /// + /// The event arguments that indicate details on what changed on the collection. + private void OnProjectCollectionChanged(ProjectCollectionChangedEventArgs e) + { + Debug.Assert(!Monitor.IsEntered(_locker), "We should never raise events while holding a private lock."); + var projectCollectionChanged = this.ProjectCollectionChanged; + if (projectCollectionChanged != null) + { + projectCollectionChanged(this, e); + } + } + + /// + /// Raises the event if the args parameter is non-null. + /// + /// The event arguments that indicate details on what changed on the collection. + private void OnProjectCollectionChangedIfNonNull(ProjectCollectionChangedEventArgs e) + { + if (e != null) + { + OnProjectCollectionChanged(e); + } + } + + /// + /// Shutdown the logging service + /// + private void ShutDownLoggingService() + { + if (_loggingService != null) + { + try + { + ((IBuildComponent)LoggingService).ShutdownComponent(); + } + catch (LoggerException) + { + throw; + } + catch (InternalLoggerException) + { + throw; + } + catch (Exception ex) + { + // According to Framework Guidelines, Dispose methods should never throw except in dire circumstances. + // However if we throw at all, its a bug. Throw InternalErrorException to emphasize that. + ErrorUtilities.ThrowInternalError("Throwing from logger shutdown", ex); + throw; + } + + _loggingService = null; + } + } + + /// + /// Create a new logging service + /// + private void CreateLoggingService(int maxCPUCount, bool onlyLogCriticalEvents) + { + _loggingService = Microsoft.Build.BackEnd.Logging.LoggingService.CreateLoggingService(LoggerMode.Synchronous, 0 /*Evaluation can be done as if it was on node "0"*/); + _loggingService.MaxCPUCount = maxCPUCount; + _loggingService.OnlyLogCriticalEvents = onlyLogCriticalEvents; + } + + /// + /// Populate Toolsets with a dictionary of (toolset version, Toolset) + /// using information from the registry and config file, if any. + /// + private void InitializeToolsetCollection() + { + _toolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _defaultToolsVersion = ToolsetReader.ReadAllToolsets(_toolsets, EnvironmentProperties, _globalProperties, _toolsetDefinitionLocations); + + _toolsetsVersion++; + } + + /// + /// Event to provide information about what project just got added to the project collection. + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "This has been API reviewed")] + public class ProjectAddedToProjectCollectionEventArgs : EventArgs + { + /// + /// Root element which was added to the project collection. + /// + private ProjectRootElement _rootElement; + + /// + /// The root element which was added to the project collection. + /// + public ProjectAddedToProjectCollectionEventArgs(ProjectRootElement element) + { + _rootElement = element; + } + + /// + /// Root element which was added to the project collection. + /// + public ProjectRootElement ProjectRootElement + { + get { return _rootElement; } + } + } + + /// + /// The ReusableLogger wraps a logger and allows it to be used for both design-time and build-time. It internally swaps + /// between the design-time and build-time event sources in response to Initialize and Shutdown events. + /// + internal class ReusableLogger : INodeLogger, IEventSource + { + /// + /// The logger we are wrapping. + /// + private ILogger _originalLogger; + + /// + /// The design-time event source + /// + private IEventSource _designTimeEventSource; + + /// + /// The build-time event source + /// + private IEventSource _buildTimeEventSource; + + /// + /// The Any event handler + /// + private AnyEventHandler _anyEventHandler; + + /// + /// The BuildFinished event handler + /// + private BuildFinishedEventHandler _buildFinishedEventHandler; + + /// + /// The BuildStarted event handler + /// + private BuildStartedEventHandler _buildStartedEventHandler; + + /// + /// The Custom event handler + /// + private CustomBuildEventHandler _customBuildEventHandler; + + /// + /// The Error event handler + /// + private BuildErrorEventHandler _buildErrorEventHandler; + + /// + /// The Message event handler + /// + private BuildMessageEventHandler _buildMessageEventHandler; + + /// + /// The ProjectFinished event handler + /// + private ProjectFinishedEventHandler _projectFinishedEventHandler; + + /// + /// The ProjectStarted event handler + /// + private ProjectStartedEventHandler _projectStartedEventHandler; + + /// + /// The Status event handler + /// + private BuildStatusEventHandler _buildStatusEventHandler; + + /// + /// The TargetFinished event handler + /// + private TargetFinishedEventHandler _targetFinishedEventHandler; + + /// + /// The TargetStarted event handler + /// + private TargetStartedEventHandler _targetStartedEventHandler; + + /// + /// The TaskFinished event handler + /// + private TaskFinishedEventHandler _taskFinishedEventHandler; + + /// + /// The TaskStarted event handler + /// + private TaskStartedEventHandler _taskStartedEventHandler; + + /// + /// The Warning event handler + /// + private BuildWarningEventHandler _buildWarningEventHandler; + + /// + /// Constructor. + /// + public ReusableLogger(ILogger originalLogger) + { + ErrorUtilities.VerifyThrowArgumentNull(originalLogger, "originalLogger"); + _originalLogger = originalLogger; + } + + #region IEventSource Members + + /// + /// The Message logging event + /// + public event BuildMessageEventHandler MessageRaised; + + /// + /// The Error logging event + /// + public event BuildErrorEventHandler ErrorRaised; + + /// + /// The Warning logging event + /// + public event BuildWarningEventHandler WarningRaised; + + /// + /// The BuildStarted logging event + /// + public event BuildStartedEventHandler BuildStarted; + + /// + /// The BuildFinished logging event + /// + public event BuildFinishedEventHandler BuildFinished; + + /// + /// The ProjectStarted logging event + /// + public event ProjectStartedEventHandler ProjectStarted; + + /// + /// The ProjectFinished logging event + /// + public event ProjectFinishedEventHandler ProjectFinished; + + /// + /// The TargetStarted logging event + /// + public event TargetStartedEventHandler TargetStarted; + + /// + /// The TargetFinished logging event + /// + public event TargetFinishedEventHandler TargetFinished; + + /// + /// The TashStarted logging event + /// + public event TaskStartedEventHandler TaskStarted; + + /// + /// The TaskFinished logging event + /// + public event TaskFinishedEventHandler TaskFinished; + + /// + /// The Custom logging event + /// + public event CustomBuildEventHandler CustomEventRaised; + + /// + /// The Status logging event + /// + public event BuildStatusEventHandler StatusEventRaised; + + /// + /// The Any logging event + /// + public event AnyEventHandler AnyEventRaised; + + #endregion + + #region ILogger Members + + /// + /// The logger verbosity + /// + public LoggerVerbosity Verbosity + { + get + { + return _originalLogger.Verbosity; + } + + set + { + _originalLogger.Verbosity = value; + } + } + + /// + /// The logger parameters + /// + public string Parameters + { + get + { + return _originalLogger.Parameters; + } + + set + { + _originalLogger.Parameters = value; + } + } + + /// + /// If we haven't yet been initialized, we register for design time events and initialize the logger we are holding. + /// If we are in design-time mode + /// + public void Initialize(IEventSource eventSource, int nodeCount) + { + if (_designTimeEventSource == null) + { + _designTimeEventSource = eventSource; + RegisterForEvents(_designTimeEventSource); + + if (_originalLogger is INodeLogger) + { + ((INodeLogger)_originalLogger).Initialize(this, nodeCount); + } + else + { + _originalLogger.Initialize(this); + } + } + else + { + ErrorUtilities.VerifyThrow(_buildTimeEventSource == null, "Already registered for build-time."); + _buildTimeEventSource = eventSource; + UnregisterForEvents(_designTimeEventSource); + RegisterForEvents(_buildTimeEventSource); + } + } + + /// + /// If we haven't yet been initialized, we register for design time events and initialize the logger we are holding. + /// If we are in design-time mode + /// + public void Initialize(IEventSource eventSource) + { + Initialize(eventSource, 1); + } + + /// + /// If we are in build-time mode, we unregister for build-time events and re-register for design-time events. + /// If we are in design-time mode, we unregister for design-time events and shut down the logger we are holding. + /// + public void Shutdown() + { + if (_buildTimeEventSource != null) + { + UnregisterForEvents(_buildTimeEventSource); + RegisterForEvents(_designTimeEventSource); + _buildTimeEventSource = null; + } + else + { + ErrorUtilities.VerifyThrow(_designTimeEventSource != null, "Already unregistered for design-time."); + UnregisterForEvents(_designTimeEventSource); + _originalLogger.Shutdown(); + } + } + + #endregion + + /// + /// Registers for all of the events on the specified event source. + /// + private void RegisterForEvents(IEventSource eventSource) + { + // Create the handlers. + _anyEventHandler = new AnyEventHandler(AnyEventRaisedHandler); + _buildFinishedEventHandler = new BuildFinishedEventHandler(BuildFinishedHandler); + _buildStartedEventHandler = new BuildStartedEventHandler(BuildStartedHandler); + _customBuildEventHandler = new CustomBuildEventHandler(CustomEventRaisedHandler); + _buildErrorEventHandler = new BuildErrorEventHandler(ErrorRaisedHandler); + _buildMessageEventHandler = new BuildMessageEventHandler(MessageRaisedHandler); + _projectFinishedEventHandler = new ProjectFinishedEventHandler(ProjectFinishedHandler); + _projectStartedEventHandler = new ProjectStartedEventHandler(ProjectStartedHandler); + _buildStatusEventHandler = new BuildStatusEventHandler(StatusEventRaisedHandler); + _targetFinishedEventHandler = new TargetFinishedEventHandler(TargetFinishedHandler); + _targetStartedEventHandler = new TargetStartedEventHandler(TargetStartedHandler); + _taskFinishedEventHandler = new TaskFinishedEventHandler(TaskFinishedHandler); + _taskStartedEventHandler = new TaskStartedEventHandler(TaskStartedHandler); + _buildWarningEventHandler = new BuildWarningEventHandler(WarningRaisedHandler); + + // Register for the events. + eventSource.AnyEventRaised += _anyEventHandler; + eventSource.BuildFinished += _buildFinishedEventHandler; + eventSource.BuildStarted += _buildStartedEventHandler; + eventSource.CustomEventRaised += _customBuildEventHandler; + eventSource.ErrorRaised += _buildErrorEventHandler; + eventSource.MessageRaised += _buildMessageEventHandler; + eventSource.ProjectFinished += _projectFinishedEventHandler; + eventSource.ProjectStarted += _projectStartedEventHandler; + eventSource.StatusEventRaised += _buildStatusEventHandler; + eventSource.TargetFinished += _targetFinishedEventHandler; + eventSource.TargetStarted += _targetStartedEventHandler; + eventSource.TaskFinished += _taskFinishedEventHandler; + eventSource.TaskStarted += _taskStartedEventHandler; + eventSource.WarningRaised += _buildWarningEventHandler; + } + + /// + /// Unregisters for all events on the specified event source. + /// + private void UnregisterForEvents(IEventSource eventSource) + { + // Unregister for the events. + eventSource.AnyEventRaised -= _anyEventHandler; + eventSource.BuildFinished -= _buildFinishedEventHandler; + eventSource.BuildStarted -= _buildStartedEventHandler; + eventSource.CustomEventRaised -= _customBuildEventHandler; + eventSource.ErrorRaised -= _buildErrorEventHandler; + eventSource.MessageRaised -= _buildMessageEventHandler; + eventSource.ProjectFinished -= _projectFinishedEventHandler; + eventSource.ProjectStarted -= _projectStartedEventHandler; + eventSource.StatusEventRaised -= _buildStatusEventHandler; + eventSource.TargetFinished -= _targetFinishedEventHandler; + eventSource.TargetStarted -= _targetStartedEventHandler; + eventSource.TaskFinished -= _taskFinishedEventHandler; + eventSource.TaskStarted -= _taskStartedEventHandler; + eventSource.WarningRaised -= _buildWarningEventHandler; + + // Null out the handlers. + _anyEventHandler = null; + _buildFinishedEventHandler = null; + _buildStartedEventHandler = null; + _customBuildEventHandler = null; + _buildErrorEventHandler = null; + _buildMessageEventHandler = null; + _projectFinishedEventHandler = null; + _projectStartedEventHandler = null; + _buildStatusEventHandler = null; + _targetFinishedEventHandler = null; + _targetStartedEventHandler = null; + _taskFinishedEventHandler = null; + _taskStartedEventHandler = null; + _buildWarningEventHandler = null; + } + + /// + /// Handler for Warning events. + /// + private void WarningRaisedHandler(object sender, BuildWarningEventArgs e) + { + if (WarningRaised != null) + { + WarningRaised(sender, e); + } + } + + /// + /// Handler for TaskStartedevents. + /// + private void TaskStartedHandler(object sender, TaskStartedEventArgs e) + { + if (TaskStarted != null) + { + TaskStarted(sender, e); + } + } + + /// + /// Handler for TaskFinished events. + /// + private void TaskFinishedHandler(object sender, TaskFinishedEventArgs e) + { + if (TaskFinished != null) + { + TaskFinished(sender, e); + } + } + + /// + /// Handler for TargetStarted events. + /// + private void TargetStartedHandler(object sender, TargetStartedEventArgs e) + { + if (TargetStarted != null) + { + TargetStarted(sender, e); + } + } + + /// + /// Handler for TargetFinished events. + /// + private void TargetFinishedHandler(object sender, TargetFinishedEventArgs e) + { + if (TargetFinished != null) + { + TargetFinished(sender, e); + } + } + + /// + /// Handler for Status events. + /// + private void StatusEventRaisedHandler(object sender, BuildStatusEventArgs e) + { + if (StatusEventRaised != null) + { + StatusEventRaised(sender, e); + } + } + + /// + /// Handler for ProjectStarted events. + /// + private void ProjectStartedHandler(object sender, ProjectStartedEventArgs e) + { + if (ProjectStarted != null) + { + ProjectStarted(sender, e); + } + } + + /// + /// Handler for ProjectFinished events. + /// + private void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e) + { + if (ProjectFinished != null) + { + ProjectFinished(sender, e); + } + } + + /// + /// Handler for Message events. + /// + private void MessageRaisedHandler(object sender, BuildMessageEventArgs e) + { + if (MessageRaised != null) + { + MessageRaised(sender, e); + } + } + + /// + /// Handler for Error events. + /// + private void ErrorRaisedHandler(object sender, BuildErrorEventArgs e) + { + if (ErrorRaised != null) + { + ErrorRaised(sender, e); + } + } + + /// + /// Handler for Custom events. + /// + private void CustomEventRaisedHandler(object sender, CustomBuildEventArgs e) + { + if (CustomEventRaised != null) + { + CustomEventRaised(sender, e); + } + } + + /// + /// Handler for BuildStarted events. + /// + private void BuildStartedHandler(object sender, BuildStartedEventArgs e) + { + if (BuildStarted != null) + { + BuildStarted(sender, e); + } + } + + /// + /// Handler for BuildFinished events. + /// + private void BuildFinishedHandler(object sender, BuildFinishedEventArgs e) + { + if (BuildFinished != null) + { + BuildFinished(sender, e); + } + } + + /// + /// Handler for Any events. + /// + private void AnyEventRaisedHandler(object sender, BuildEventArgs e) + { + if (AnyEventRaised != null) + { + AnyEventRaised(sender, e); + } + } + } + + /// + /// Holder for the projects loaded into this collection. + /// + private class LoadedProjectCollection : IEnumerable + { + /// + /// The collection of all projects already loaded into this collection. + /// Key is the full path to the project, value is a list of projects with that path, each + /// with different global properties and/or tools version. + /// + /// + /// If hosts tend to load lots of projects with the same path, the value will have to be + /// changed to a more efficient type of collection. + /// + /// Lock on this object. Concurrent load must be thread safe. + /// Not using ConcurrentDictionary because some of the add/update + /// semantics would get convoluted. + /// + private Dictionary> _loadedProjects = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// Count of loaded projects + /// + private int _count; + + /// + /// Constructor + /// + internal LoadedProjectCollection() + { + } + + /// + /// Returns the number of projects currently loaded + /// + internal int Count + { + get + { + lock (_loadedProjects) + { + return _count; + } + } + } + + /// + /// Enumerate all the projects + /// + public IEnumerator GetEnumerator() + { + lock (_loadedProjects) + { + var projects = new List(); + + foreach (List projectList in _loadedProjects.Values) + { + foreach (Project project in projectList) + { + projects.Add(project); + } + } + + return projects.GetEnumerator(); + } + } + + /// + /// Enumerate all the projects. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Get all projects with the provided path. + /// Returns an empty list if there are none. + /// + internal IList GetMatchingProjectsIfAny(string fullPath) + { + lock (_loadedProjects) + { + List candidates; + + _loadedProjects.TryGetValue(fullPath, out candidates); + + return (candidates == null) ? (IList)ReadOnlyEmptyList.Instance : candidates; + } + } + + /// + /// Returns the project in the collection matching the path, global properties, and tools version provided. + /// There can be no more than one match. + /// If none is found, returns null. + /// + internal Project GetMatchingProjectIfAny(string fullPath, IDictionary globalProperties, string toolsVersion) + { + lock (_loadedProjects) + { + List candidates; + if (_loadedProjects.TryGetValue(fullPath, out candidates)) + { + foreach (Project candidate in candidates) + { + if (HasEquivalentGlobalPropertiesAndToolsVersion(candidate, globalProperties, toolsVersion)) + { + return candidate; + } + } + } + + return null; + } + } + + /// + /// Adds the provided project to the collection. + /// If there is already an equivalent project, throws InvalidOperationException. + /// + internal void AddProject(Project project) + { + lock (_loadedProjects) + { + List projectList; + if (!_loadedProjects.TryGetValue(project.FullPath, out projectList)) + { + projectList = new List(); + _loadedProjects.Add(project.FullPath, projectList); + } + + foreach (Project existing in projectList) + { + if (HasEquivalentGlobalPropertiesAndToolsVersion(existing, project.GlobalProperties, project.ToolsVersion)) + { + ErrorUtilities.ThrowInvalidOperation("OM_MatchingProjectAlreadyInCollection", existing.FullPath); + } + } + + projectList.Add(project); + _count++; + } + } + + /// + /// Removes the provided project from the collection. + /// If project was not loaded, returns false. + /// + internal bool RemoveProject(Project project) + { + return RemoveProject(project.FullPath, project); + } + + /// + /// Removes a project, using the specified full path to use as the key to find it. + /// This is specified separately in case the project was previously stored under a different path. + /// + internal bool RemoveProject(string projectFullPath, Project project) + { + lock (_loadedProjects) + { + List projectList; + if (!_loadedProjects.TryGetValue(projectFullPath, out projectList)) + { + return false; + } + + bool result = projectList.Remove(project); + + if (result) + { + _count--; + + if (projectList.Count == 0) + { + _loadedProjects.Remove(projectFullPath); + } + } + + return result; + } + } + + /// + /// Removes all projects from the collection. + /// + internal void RemoveAllProjects() + { + lock (_loadedProjects) + { + _loadedProjects = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _count = 0; + } + } + + /// + /// Returns true if the global properties and tools version provided are equivalent to + /// those in the provided project, otherwise false. + /// + private bool HasEquivalentGlobalPropertiesAndToolsVersion(Project project, IDictionary globalProperties, string toolsVersion) + { + if (!String.Equals(project.ToolsVersion, toolsVersion, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (project.GlobalProperties.Count != globalProperties.Count) + { + return false; + } + + foreach (KeyValuePair leftProperty in project.GlobalProperties) + { + string rightValue; + if (!globalProperties.TryGetValue(leftProperty.Key, out rightValue)) + { + return false; + } + + if (!String.Equals(leftProperty.Value, rightValue, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ProjectCollectionChangedEventArgs.cs b/src/XMakeBuildEngine/Definition/ProjectCollectionChangedEventArgs.cs new file mode 100644 index 00000000000..ba29f17c966 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ProjectCollectionChangedEventArgs.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectCollectionChangedEventArgs class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Properties or other mutable state associated with a . + /// + public enum ProjectCollectionChangedState + { + /// + /// The property changed. + /// + DefaultToolsVersion, + + /// + /// The toolsets changed. + /// + Toolsets, + + /// + /// The loggers changed. + /// + Loggers, + + /// + /// The global properties changed. + /// + GlobalProperties, + + /// + /// The property changed. + /// + IsBuildEnabled, + + /// + /// The property changed. + /// + OnlyLogCriticalEvents, + + /// + /// The property changed. + /// + HostServices, + + /// + /// The property changed. + /// + DisableMarkDirty, + + /// + /// The property changed. + /// + SkipEvaluation, + } + + /// + /// Event arguments for the event. + /// + public class ProjectCollectionChangedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + internal ProjectCollectionChangedEventArgs(ProjectCollectionChangedState changedState) + { + Changed = changedState; + } + + /// + /// Gets the nature of the change made to the . + /// + public ProjectCollectionChangedState Changed { get; private set; } + } +} diff --git a/src/XMakeBuildEngine/Definition/ProjectItem.cs b/src/XMakeBuildEngine/Definition/ProjectItem.cs new file mode 100644 index 00000000000..f7da55e181a --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ProjectItem.cs @@ -0,0 +1,1058 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents an evaluated item with a link to its source in the project file. +//----------------------------------------------------------------------- + +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System; +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An evaluated design-time item + /// + /// + /// Edits to this object will indirectly dirty the containing project because they will modify the backing XML. + /// + /// + /// We cannot use a copy-on-write table for the metadata, as ProjectMetadata objects are mutable. However, + /// we do use it for build-time items. + /// + [DebuggerDisplay("{ItemType}={EvaluatedInclude} [{UnevaluatedInclude}] #DirectMetadata={DirectMetadataCount}")] + public class ProjectItem : IKeyed, IItem, IMetadataTable, IProjectMetadataParent + { + /// + /// Project that this item lives in. + /// ProjectItems always live in a project. + /// Used to get item definitions and project directory. + /// + private readonly Project _project; + + /// + /// Fragment of the original include that led to this item, + /// with properties expanded but not wildcards. Escaped as necessary + /// + /// + /// This is ONLY used to figure out %(RecursiveDir) when it is requested. + /// It's likely too expensive to figure that out if it isn't needed, so we store + /// the necessary material here. + /// + private readonly string _evaluatedIncludeBeforeWildcardExpansionEscaped; + + /// + /// Item definitions are stored in one single table shared by all items of a particular item type. + /// + /// When an item is created from another item, such as by using an expression like Include="@(x)", + /// any item definition metadata those source items have must override any item definition metadata + /// associated with the new item type. + /// + /// Copying all those item definition metadata into real metadata on this item would be very inefficient, because + /// it would turn a single shared table into a separate table for every item. + /// + /// Instead, we get a reference to the item definition of the source items, and consult + /// that table before we consult our own item type's item definition. Since item definitions can't change at this point, + /// it's safe to reference their original table. + /// + /// If our item gets copied again, we need a reference to the inherited item definition and we need the real item + /// definition of the source items. Thus a list is created. On copying, a list is created, beginning with a clone + /// of any list the source item had, and ending with the item definition list of the source item type. + /// + /// When we look up a metadata value we look at + /// (1) directly associated metadata and built-in metadata + /// (2) the inherited item definition list, starting from the top + /// (3) the item definition associated with our item type + /// + private readonly List _inheritedItemDefinitions; + + /// + /// Backing XML item. + /// Can never be null + /// + private ProjectItemElement _xml; + + /// + /// Evaluated include. + /// The original XML may have evaluated to several of these items, + /// each with a different include. + /// May be empty, for example from expanding an empty list or from a transform with undefined metadata. + /// Escaped as necessary + /// + private string _evaluatedIncludeEscaped; + + /// + /// Collection of metadata that link the XML metadata and evaluated metadata. + /// Since evaluation has occurred, this is an unordered collection. + /// May be null. + /// + /// + /// Lazily created, as there are lots of items + /// that have no metadata at all. + /// + private PropertyDictionary _directMetadata; + + /// + /// Cached value of the fullpath metadata. All other metadata are computed on demand. + /// + private string _fullPath; + + /// + /// Called by the Evaluator during project evaluation. + /// Direct metadata may be null, indicating no metadata. It is assumed to have already been cloned. + /// Inherited item definition metadata may be null. It is assumed that its list has already been cloned. + /// ProjectMetadata objects may be shared with other items. + /// + internal ProjectItem( + Project project, + ProjectItemElement xml, + string evaluatedIncludeEscaped, + string evaluatedIncludeBeforeWildcardExpansionEscaped, + PropertyDictionary directMetadataCloned, + List inheritedItemDefinitionsCloned + ) + { + ErrorUtilities.VerifyThrowInternalNull(project, "project"); + ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); + + // Orcas accidentally allowed empty includes if they resulted from expansion: we preserve that bug + ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeEscaped, "evaluatedIncludeEscaped"); + ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeBeforeWildcardExpansionEscaped, "evaluatedIncludeBeforeWildcardExpansionEscaped"); + + _xml = xml; + _project = project; + _evaluatedIncludeEscaped = evaluatedIncludeEscaped; + _evaluatedIncludeBeforeWildcardExpansionEscaped = evaluatedIncludeBeforeWildcardExpansionEscaped; + _directMetadata = directMetadataCloned; + _inheritedItemDefinitions = inheritedItemDefinitionsCloned; + } + + /// + /// Backing XML item. + /// Can never be null. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public ProjectItemElement Xml + { + [DebuggerStepThrough] + get + { return _xml; } + } + + /// + /// Gets or sets the type of this item. + /// + public string ItemType + { + [DebuggerStepThrough] + get + { return _xml.ItemType; } + set { ChangeItemType(value); } + } + + /// + /// Gets or sets the unevaluated value of the Include. + /// + public string UnevaluatedInclude + { + [DebuggerStepThrough] + get + { return _xml.Include; } + set { Rename(value); } + } + + /// + /// Gets the evaluated value of the include, unescaped. + /// + public string EvaluatedInclude + { + [DebuggerStepThrough] + get + { return EscapingUtilities.UnescapeAll(_evaluatedIncludeEscaped); } + } + + /// + /// Gets the evaluated value of the include, escaped as necessary. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IItem.EvaluatedIncludeEscaped + { + [DebuggerStepThrough] + get + { return _evaluatedIncludeEscaped; } + } + + /// + /// The directory of the project being built + /// Never null: If there is no project filename yet, it will use the current directory + /// + string IItem.ProjectDirectory + { + get { return _project.DirectoryPath; } + } + + /// + /// Project that this item lives in. + /// ProjectItems always live in a project. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public Project Project + { + [DebuggerStepThrough] + get + { return _project; } + } + + /// + /// If the item originated in an imported file, returns true. + /// Otherwise returns false. + /// + public bool IsImported + { + get + { + bool isImported = !Object.ReferenceEquals(_xml.ContainingProject, _project.Xml); + + return isImported; + } + } + + /// + /// Metadata directly on the item, if any. + /// Does not include metadata from item definitions. + /// Does not include built-in metadata. + /// Never returns null. + /// + public IEnumerable DirectMetadata + { + get { return (IEnumerable)_directMetadata ?? (IEnumerable)ReadOnlyEmptyCollection.Instance; } + } + + /// + /// Count of direct metadata on this item, if any. + /// Does NOT count any metadata inherited from item definitions. + /// Does not count built-in metadata, such as "FullPath". + /// + public int DirectMetadataCount + { + [DebuggerStepThrough] + get + { return _directMetadata != null ? _directMetadata.Count : 0; } + } + + /// + /// Metadata on the item, if any. Includes metadata specified by the definition, if any. + /// If there is no metadata, returns an empty collection. + /// Does not include built-in metadata, such as "FullPath". + /// Get the values of built-in metadata using . + /// This is a read-only collection. To modify the metadata, use . + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public ICollection Metadata + { + [DebuggerStepThrough] + get + { return MetadataCollection; } + } + + /// + /// Count of metadata on this item, if any. + /// Includes any metadata inherited from item definitions. + /// Includes both custom and built-in metadata. + /// + public int MetadataCount + { + [DebuggerStepThrough] + get + { return MetadataCollection.Count + FileUtilities.ItemSpecModifiers.All.Length; } + } + + /// + /// Implementation of IKeyed exposing the item type, so items + /// can be put in a dictionary conveniently. + /// + string IKeyed.Key + { + [DebuggerStepThrough] + get + { return ItemType; } + } + + /// + /// Internal version of Metadata that returns + /// a full ICollection. + /// Unordered collection of evaluated metadata on the item. + /// If there is no metadata, returns an empty collection. + /// Does not include built-in metadata. + /// Includes any from item definitions not masked by directly set metadata. + /// This is a read-only collection. To modify the metadata, use . + /// + internal ICollection MetadataCollection + { + get + { + RetrievableEntryHashSet allMetadata = new RetrievableEntryHashSet(MSBuildNameIgnoreCaseComparer.Default); + + // Lowest priority: regular item definitions + ProjectItemDefinition itemDefinition = null; + if (_project.ItemDefinitions.TryGetValue(ItemType, out itemDefinition)) + { + foreach (ProjectMetadata metadataFromDefinition in itemDefinition.Metadata) + { + allMetadata[metadataFromDefinition.Name] = metadataFromDefinition; + } + } + + // Next, any inherited item definitions. Front of the list is highest priority, + // so walk backwards. + if (_inheritedItemDefinitions != null) + { + for (int i = _inheritedItemDefinitions.Count - 1; i >= 0; i--) + { + foreach (ProjectMetadata metadatum in _inheritedItemDefinitions[i].Metadata) + { + allMetadata[metadatum.Name] = metadatum; + } + } + } + + // Finally any direct metadata win. + if (null != _directMetadata) + { + foreach (ProjectMetadata metadatum in _directMetadata) + { + allMetadata[metadatum.Name] = metadatum; + } + } + + return allMetadata.Values; + } + } + + /// + /// Accesses the unescaped evaluated include prior to wildcard expansion + /// + internal string EvaluatedIncludeBeforeWildcardExpansion + { + [DebuggerStepThrough] + get + { return EscapingUtilities.UnescapeAll(_evaluatedIncludeBeforeWildcardExpansionEscaped); } + } + + /// + /// Accesses the evaluated include prior to wildcard expansion + /// + internal string EvaluatedIncludeBeforeWildcardExpansionEscaped + { + [DebuggerStepThrough] + get + { return _evaluatedIncludeBeforeWildcardExpansionEscaped; } + } + + /// + /// Accesses the inherited item definitions, if any. + /// Used ONLY by the ProjectInstance, when cloning a ProjectItem. + /// + internal List InheritedItemDefinitions + { + [DebuggerStepThrough] + get + { return _inheritedItemDefinitions; } + } + + /// + /// Gets an evaluated metadata on this item. + /// Potentially includes a metadata from an item definition. + /// Does not return built-in metadata, such as "FullPath". + /// Returns null if not found. + /// + public ProjectMetadata GetMetadata(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + ProjectMetadata result = null; + + if (_directMetadata != null) + { + result = _directMetadata[name]; + } + + if (result == null) + { + result = GetItemDefinitionMetadata(name); + } + + return result; + } + + /// + /// Get the evaluated value of a metadata on this item, possibly from an item definition. + /// Returns empty string if it does not exist. + /// To determine whether a piece of metadata does not exist vs. simply has no value, use HasMetadata. + /// May be used to access the value of built-in metadata, such as "FullPath". + /// Attempting to get built-in metadata on a value that is not a valid path throws InvalidOperationException. + /// + public string GetMetadataValue(string name) + { + return EscapingUtilities.UnescapeAll(((IItem)this).GetMetadataValueEscaped(name)); + } + + /// + /// Returns true if a particular piece of metadata is defined on this item, + /// otherwise false. + /// Includes built-in metadata and metadata inherited from item definitions. + /// + public bool HasMetadata(string name) + { + if (_directMetadata != null && _directMetadata.Contains(name)) + { + return true; + } + + if (FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name)) + { + return true; + } + + ProjectMetadata metadatum = GetItemDefinitionMetadata(name); + if (null != metadatum) + { + return true; + } + + return false; + } + + /// + /// See GetMetadataValue for a more detailed explanation. + /// Returns the escaped value of the metadatum requested. + /// + string IItem.GetMetadataValueEscaped(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + string value = null; + + if (_directMetadata != null) + { + ProjectMetadata metadatum = _directMetadata[name]; + if (metadatum != null) + { + value = metadatum.EvaluatedValueEscaped; + } + } + + if (value == null) + { + value = GetBuiltInMetadataEscaped(name); + } + + if (value == null) + { + ProjectMetadata metadatum = GetItemDefinitionMetadata(name); + + if (null != metadatum && Expander.ExpressionMayContainExpandableExpressions(metadatum.EvaluatedValueEscaped)) + { + Expander expander = new Expander(null, null, new BuiltInMetadataTable(this)); + + value = expander.ExpandIntoStringLeaveEscaped(metadatum.EvaluatedValueEscaped, ExpanderOptions.ExpandBuiltInMetadata, metadatum.Location); + } + else if (null != metadatum) + { + return metadatum.EvaluatedValueEscaped; + } + } + + return value ?? String.Empty; + } + + /// + /// Gets any existing ProjectMetadata on the item, or + /// else any on an applicable item definition. + /// This is ONLY called during evaluation. + /// Does not return built-in metadata, such as "FullPath". + /// Returns null if not found. + /// + ProjectMetadata IItem.GetMetadata(string name) + { + return GetMetadata(name); + } + + /// + /// Adds a ProjectMetadata to the item. + /// This is ONLY called during evaluation and does not affect the XML. + /// + ProjectMetadata IItem.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedInclude) + { + _directMetadata = _directMetadata ?? new PropertyDictionary(); + + ProjectMetadata predecessor = GetMetadata(metadataElement.Name); + + ProjectMetadata metadatum = new ProjectMetadata(this, metadataElement, evaluatedInclude, predecessor); + + _directMetadata.Set(metadatum); + + return metadatum; + } + + /// + /// Adds metadata with the specified name and value to the item. + /// Updates an existing metadata if one already exists with the same name on the item directly, as opposed to inherited from an item definition. + /// Updates the evaluated project, but does not affect anything else in the project until reevaluation. For example, + /// if a piece of metadata named "m" is added on item of type "i", it does not affect "j" which is evaluated from "@(j->'%(m)')" until reevaluation. + /// Also if the unevaluated value of "m" is set to something that is modified by evaluation, such as "$(p)", the evaluated value will be set to literally "$(p)" until reevaluation. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state. + /// Returns the new or existing metadatum. + /// + /// Unevaluated value is assumed to be escaped as necessary + public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue) + { + Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); + + XmlUtilities.VerifyThrowArgumentValidElementName(name); + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); + ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[name] == null, "CannotModifyReservedItemMetadata", name); + ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent != null && _xml.Parent.Parent != null, "OM_ObjectIsNoLongerActive"); + + _project.SplitItemElementIfNecessary(_xml); + + ProjectMetadata metadatum; + + if (_directMetadata != null) + { + metadatum = _directMetadata[name]; + + if (metadatum != null) + { + metadatum.UnevaluatedValue = unevaluatedValue; + return metadatum; + } + } + + ProjectMetadataElement metadatumXml = _xml.AddMetadata(name, unevaluatedValue); + + _directMetadata = _directMetadata ?? new PropertyDictionary(); + + string evaluatedValueEscaped = _project.ExpandMetadataValueBestEffortLeaveEscaped(this, unevaluatedValue, metadatumXml.Location); + + metadatum = new ProjectMetadata(this, metadatumXml, evaluatedValueEscaped, null /* predecessor unknown */); + + _directMetadata.Set(metadatum); + + return metadatum; + } + + /// + /// Removes any metadata with the specified name. + /// Returns true if the evaluated metadata existed, otherwise false. + /// If the metadata name is one of the built-in metadata, like "FullPath", throws InvalidArgumentException. + /// If the metadata originates in an item definition, and was not overridden, throws InvalidOperationException. + /// + public bool RemoveMetadata(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); + Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); + ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent != null && _xml.Parent.Parent != null, "OM_ObjectIsNoLongerActive"); + + ProjectMetadata metadatum = (_directMetadata == null) ? null : _directMetadata[name]; + + if (metadatum == null) + { + ProjectMetadata itemDefinitionMetadata = GetItemDefinitionMetadata(name); + ErrorUtilities.VerifyThrowInvalidOperation(itemDefinitionMetadata == null, "OM_CannotRemoveMetadataOriginatingFromItemDefinition", name); + return false; + } + + _project.SplitItemElementIfNecessary(_xml); + + // New metadata objects may have been created + metadatum = _directMetadata[name]; + + _xml.RemoveChild(metadatum.Xml); + _directMetadata.Remove(name); + + return true; + } + + /// + /// Renames the item. + /// Equivalent to setting the value. + /// Generally, no expansion occurs. This is because it would potentially result in several items, + /// which is not meaningful semantics when renaming a single item. + /// However if the item does not need to be split (which would invalidate its ProjectItemElement), + /// and the new value expands to exactly one item, then its evaluated include is updated + /// with the expanded value, rather than the unexpanded value. + /// + /// + /// Even if the new value expands to zero items, we do not expand it. + /// The common case we are interested in for expansion here is setting something + /// like "$(sourcesroot)\foo.cs" and expanding that to a single item. + /// If say "@(foo)" is set as the new name, and it expands to blank, that might + /// be surprising to the host and maybe even unhandled, if on full reevaluation + /// it wouldn’t expand to blank. That’s why we're being cautious and supporting + /// the most common scenario only. + /// Many hosts will do a ReevaluateIfNecessary before reading anyway. + /// + public void Rename(string name) + { + Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); + ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent != null && _xml.Parent.Parent != null, "OM_ObjectIsNoLongerActive"); + + if (String.Equals(UnevaluatedInclude, name, StringComparison.Ordinal)) + { + return; + } + + _fullPath = null; // Clear cached value + + if (_xml.Count == 0 /* no metadata */ && _project.IsSuitableExistingItemXml(_xml, name, null /* no metadata */) && !FileMatcher.HasWildcardsSemicolonItemOrPropertyReferences(name)) + { + _evaluatedIncludeEscaped = name; + + // Fast item lookup tables are invalid now. + // Make sure that when the caller invokes ReevaluateIfNecessary() that they'll be refreshed. + Project.MarkDirty(); + return; + } + + bool splitOccurred = _project.SplitItemElementIfNecessary(_xml); + + _xml.Include = name; + + if (splitOccurred) + { + _evaluatedIncludeEscaped = name; + } + else + { + _evaluatedIncludeEscaped = _project.ExpandItemIncludeBestEffortLeaveEscaped(_xml); + } + } + + #region IMetadataTable Members + + /// + /// Retrieves any value we have in our metadata table for the metadata name specified. + /// If no value is available, returns empty string. + /// Value, if escaped, remains escaped. + /// + string IMetadataTable.GetEscapedValue(string name) + { + string value = ((IMetadataTable)this).GetEscapedValue(null, name); + + return value; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If no value is available, returns empty string. + /// If item type is null, it is ignored, otherwise it must match. + /// Value, if escaped, remains escaped. + /// + string IMetadataTable.GetEscapedValue(string itemType, string name) + { + string value = ((IMetadataTable)this).GetEscapedValueIfPresent(itemType, name); + + return value ?? String.Empty; + } + + /// + /// Returns the value if it exists. + /// If no value is available, returns null. + /// If item type is null, it is ignored, otherwise it must match. + /// Value, if escaped, remains escaped. + /// + string IMetadataTable.GetEscapedValueIfPresent(string itemType, string name) + { + if (itemType == null || MSBuildNameIgnoreCaseComparer.Default.Equals(ItemType, itemType)) + { + string value = ((IItem)this).GetMetadataValueEscaped(name); + + if (value.Length > 0 || HasMetadata(name)) + { + return value; + } + } + + return null; + } + + #endregion + + /// + /// Changes the item type of this item. + /// Until reevaluation puts it in the correct place, it will be placed at + /// the end of the list of items of its new type. + /// + /// + /// This is a little involved, as it requires replacing + /// the XmlElement, and updating the project's datastructures. + /// + internal void ChangeItemType(string newItemType) + { + ErrorUtilities.VerifyThrowArgumentLength(newItemType, "ItemType"); + Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); + ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent != null && _xml.Parent.Parent != null, "OM_ObjectIsNoLongerActive"); + + if (String.Equals(ItemType, newItemType, StringComparison.Ordinal)) + { + return; + } + + _project.SplitItemElementIfNecessary(_xml); + + _project.RemoveItemBeforeItemTypeChange(this); + + // xml.ChangeItemType will throw if new item type is invalid. Make sure we re-add the item anyway + try + { + _xml.ChangeItemType(newItemType); + } + finally + { + _project.ReAddExistingItemAfterItemTypeChange(this); + } + } + + /// + /// Creates new xml objects for itself, disconnecting from the old xml objects. + /// Called ONLY by + /// + /// + /// Called when breaking up a single ProjectItemElement that evaluates into several ProjectItems. + /// + internal void SplitOwnItemElement() + { + ProjectItemElement oldXml = _xml; + + _xml = _xml.ContainingProject.CreateItemElement(ItemType, ((IItem)this).EvaluatedIncludeEscaped); + + oldXml.Parent.InsertBeforeChild(_xml, oldXml); + + if (_directMetadata == null) + { + return; + } + + // ProjectMetadata objects may be being shared with other ProjectItem objects, + // or originate from item definitions, so it is necessary to replace ours with + // new ones. + List temporary = new List(_directMetadata.Count); + + foreach (ProjectMetadata metadatum in _directMetadata) + { + temporary.Add(metadatum); + } + + _directMetadata = (_directMetadata == null ? null : new PropertyDictionary(_directMetadata.Count)); + + foreach (ProjectMetadata metadatum in temporary) + { + SetMetadataValue(metadatum.Name, metadatum.EvaluatedValueEscaped); + } + } + + /// + /// Helper to get the value of a built-in metadatum with + /// the specified name, if any. + /// + private string GetBuiltInMetadataEscaped(string name) + { + string value = null; + + if (FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name)) + { + value = BuiltInMetadata.GetMetadataValueEscaped(_project.DirectoryPath, _evaluatedIncludeBeforeWildcardExpansionEscaped, _evaluatedIncludeEscaped, this.Xml.ContainingProject.FullPath, name, ref _fullPath); + } + + return value; + } + + /// + /// Retrieves the named metadata from the item definition, if any. + /// If it is not present, returns null + /// + /// The metadata name. + /// The value if it exists, null otherwise. + private ProjectMetadata GetItemDefinitionMetadata(string name) + { + ProjectMetadata metadataFromDefinition = null; + + // Check any inherited item definition metadata first. It's more like + // direct metadata, but we didn't want to copy the tables. + if (_inheritedItemDefinitions != null) + { + foreach (ProjectItemDefinition inheritedItemDefinition in _inheritedItemDefinitions) + { + metadataFromDefinition = inheritedItemDefinition.GetMetadata(name); + + if (metadataFromDefinition != null) + { + return metadataFromDefinition; + } + } + } + + // Now try regular item definition metadata for this item type. + ProjectItemDefinition itemDefinition; + if (_project.ItemDefinitions.TryGetValue(ItemType, out itemDefinition)) + { + metadataFromDefinition = itemDefinition.GetMetadata(name); + } + + return metadataFromDefinition; + } + + /// + /// A class factory for ProjectItems. + /// + internal class ProjectItemFactory : IItemFactory + { + /// + /// The Project with which each item should be associated. + /// + private readonly Project _project; + + /// + /// The project item's XML + /// + private ProjectItemElement _xml; + + /// + /// Creates an item factory which does not specify an item xml. The item xml must + /// be specified later. + /// + /// The project for items generated. + internal ProjectItemFactory(Project project) + { + _project = project; + } + + /// + /// Constructor + /// + /// The project for items generated. + /// The xml for items generated. + internal ProjectItemFactory(Project project, ProjectItemElement xml) + { + _project = project; + _xml = xml; + } + + /// + /// Item type that items created by this factory will have. + /// + public string ItemType + { + get { return _xml.ItemType; } + set { ErrorUtilities.ThrowInternalError("Cannot change the item type on ProjectItem.ProjectItemFactory"); } + } + + /// + /// Set the item xml from which items will be created. + /// Used by the evaluator only. + /// + public ProjectItemElement ItemElement + { + set { _xml = value; } + } + + /// + /// Creates an item with the specified type and evaluated include. + /// Used for making items from "just strings" and from expressions like "@(Compile, ';')" + /// + /// The include. + /// A new project item. + /// + /// NOTE: defining project is ignored because we already know the ItemElement associated with + /// this item, and use that for where it is defined. + /// + public ProjectItem CreateItem(string include, string definingProject) + { + return CreateItem(include, include, definingProject); + } + + /// + /// Creates an item based on the provided item, but with + /// the project and xml of this factory. Metadata is cloned, + /// but continues to point to the original ProjectMetadataElement objects. + /// This is to support the scenario Include="@(i)" where we are copying + /// metadata, and are happy to see changes in the original metadata, but + /// setting metadata should create new XML. + /// + /// + /// NOTE: defining project is ignored because we already know the ItemElement associated with + /// this item, and use that for where it is defined. + /// + public ProjectItem CreateItem(ProjectItem source, string definingProject) + { + return CreateItem(source._evaluatedIncludeEscaped, source._evaluatedIncludeBeforeWildcardExpansionEscaped, source); + } + + /// + /// Creates an item based on the provided item, but with + /// the project and xml of this factory and the specified include. Metadata is cloned, + /// but continues to point to the original ProjectMetadataElement objects. + /// This is to support this scenario: Include="@(i->'xxx')" + /// + /// + /// If the item type of the source is the same as the item type of the destination, + /// then it's not necessary to copy metadata originating in an item definition. + /// If it's not, we have to clone that too. + /// + /// + /// NOTE: defining project is ignored because we already know the ItemElement associated with + /// this item, and use that for where it is defined. + /// + public ProjectItem CreateItem(string evaluatedIncludeEscaped, ProjectItem source, string definingProject) + { + return CreateItem(evaluatedIncludeEscaped, evaluatedIncludeEscaped, source); + } + + /// + /// Creates an item with the specified include and include before wildcard expansion. + /// This is to support creating items from an include that may have a wildcard expression in it. + /// + /// + /// NOTE: defining project is ignored because we already know the ItemElement associated with + /// this item, and use that for where it is defined. + /// + public ProjectItem CreateItem(string evaluatedIncludeEscaped, string evaluatedIncludeBeforeWildcardExpansion, string definingProject) + { + ErrorUtilities.VerifyThrowInternalNull(_xml, "xml"); + + return new ProjectItem(_project, _xml, evaluatedIncludeEscaped, evaluatedIncludeBeforeWildcardExpansion, null /* no metadata */, null /* no inherited definition metadata */); + } + + /// + /// Applies the supplied metadata to the destination item. + /// + public void SetMetadata(IEnumerable> metadata, IEnumerable destinationItems) + { + foreach (IItem item in destinationItems) + { + foreach (Pair metadatum in metadata) + { + item.SetMetadata(metadatum.Key, metadatum.Value); + } + } + } + + /// + /// Creates an item based on the provided item, with the specified include and item type. + /// + private ProjectItem CreateItem(string evaluatedIncludeEscaped, string evaluatedIncludeBeforeWildcardExpansionEscaped, ProjectItem source) + { + ErrorUtilities.VerifyThrowInternalNull(_xml, "xml"); + + // The new item inherits any metadata originating in item definitions, which + // takes precedence over its own item definition metadata. + // + // Order of precedence: + // (1) any directly defined metadata on the source item + // (2) any inherited item definition metadata the source item had accumulated, in order of accumulation + // (3) any item definition metadata associated with the source item's item type + // (4) any item definition metadata associated with the destination item's item type; none yet. + + // Clone for (1) + PropertyDictionary directMetadataClone = null; + + if (source.DirectMetadataCount > 0) + { + directMetadataClone = new PropertyDictionary(source.DirectMetadataCount); + + foreach (ProjectMetadata metadatum in source._directMetadata) + { + directMetadataClone.Set(metadatum.DeepClone()); + } + } + + // Combine (2) and (3) into a list, (2) on top. + int inheritedItemDefinitionsCount = (source._inheritedItemDefinitions == null) ? 0 : source._inheritedItemDefinitions.Count; + + List inheritedItemDefinitionsClone = null; + + if (source._inheritedItemDefinitions != null) + { + inheritedItemDefinitionsClone = inheritedItemDefinitionsClone ?? new List(inheritedItemDefinitionsCount + 1); + inheritedItemDefinitionsClone.AddRange(source._inheritedItemDefinitions); + } + + ProjectItemDefinition sourceItemDefinition; + if (_project.ItemDefinitions.TryGetValue(source.ItemType, out sourceItemDefinition)) + { + inheritedItemDefinitionsClone = inheritedItemDefinitionsClone ?? new List(inheritedItemDefinitionsCount + 1); + inheritedItemDefinitionsClone.Add(sourceItemDefinition); + } + + return new ProjectItem(_project, _xml, evaluatedIncludeEscaped, evaluatedIncludeBeforeWildcardExpansionEscaped, directMetadataClone, inheritedItemDefinitionsClone); + } + } + + /// + /// Implementation of IMetadataTable that can be passed to expander + /// to expose only built-in metadata on this item. + /// + private class BuiltInMetadataTable : IMetadataTable + { + /// + /// Backing item + /// + private ProjectItem _item; + + /// + /// Constructor. + /// + internal BuiltInMetadataTable(ProjectItem item) + { + _item = item; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name specified. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string name) + { + string value = _item.GetBuiltInMetadataEscaped(name); + return value; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If item type is null, it is ignored. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string itemType, string name) + { + string value = GetEscapedValueIfPresent(itemType, name); + + return value ?? String.Empty; + } + + /// + /// Returns the value if it exists, null otherwise. + /// If item type is null, it is ignored. + /// + public string GetEscapedValueIfPresent(string itemType, string name) + { + string value = null; + + if ((itemType == null) || String.Equals(_item.ItemType, itemType, StringComparison.OrdinalIgnoreCase)) + { + value = GetEscapedValue(name); + } + + return value; + } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ProjectItemDefinition.cs b/src/XMakeBuildEngine/Definition/ProjectItemDefinition.cs new file mode 100644 index 00000000000..5ba39335f05 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ProjectItemDefinition.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a set of evaluated item definitions all applying to the same item-type. +//----------------------------------------------------------------------- + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An evaluated item definition for a particular item-type. + /// + /// + /// Note that these are somewhat different to items. Like items, they can have metadata; like properties, the metadata + /// can override each other. So during evaluation all the item definitions for a type are rolled together (assuming + /// their conditions are true) to create one ProjectItemDefinition for each type. For this reason, the ProjectItemDefinition + /// often will not point to a single ProjectItemDefinitionElement. The metadata within, however, will each point to a single + /// ProjectMetadataElement, and these can be added, removed, and modified. + /// + [DebuggerDisplay("{itemType} #Metadata={MetadataCount}")] + public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition, IProjectMetadataParent + { + /// + /// Project that this item definition lives in. + /// ProjectItemDefinitions always live in a project. + /// Used to evaluate any updates to child metadata. + /// + private readonly Project _project; + + /// + /// Item type, for example "Compile", that this item definition applies to + /// + private readonly string _itemType; + + /// + /// Collection of metadata that link the XML metadata and instance metadata + /// Since evaluation has occurred, this is an unordered collection. + /// + private PropertyDictionary _metadata; + + /// + /// Called by the Evaluator during project evaluation. + /// + /// + /// Assumes that the itemType string originated in a ProjectItemDefinitionElement and therefore + /// was already validated. + /// + internal ProjectItemDefinition(Project project, string itemType) + { + ErrorUtilities.VerifyThrowInternalNull(project, "project"); + ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType"); + + _project = project; + _itemType = itemType; + _metadata = null; + } + + /// + /// Project that this item lives in. + /// ProjectDefinitions always live in a project. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public Project Project + { + [DebuggerStepThrough] + get + { return _project; } + } + + /// + /// Type of this item definition. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public string ItemType + { + [DebuggerStepThrough] + get + { return _itemType; } + } + + /// + /// Metadata on the item definition. + /// If there is no metadata, returns empty collection. + /// This is a read-only collection. + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public IEnumerable Metadata + { + get + { + return (_metadata == null) ? ReadOnlyEmptyList.Instance : (IEnumerable)_metadata; + } + } + + /// + /// Count of metadata on the item definition. + /// + public int MetadataCount + { + get { return (_metadata == null) ? 0 : _metadata.Count; } + } + + /// + /// Implementation of IKeyed exposing the item type, so these + /// can be put in a dictionary conveniently. + /// + string IKeyed.Key + { + get { return ItemType; } + } + + /// + /// Get any metadata in the item that has the specified name, + /// otherwise returns null + /// + [DebuggerStepThrough] + public ProjectMetadata GetMetadata(string name) + { + return (_metadata == null) ? null : _metadata[name]; + } + + /// + /// Get the value of any metadata in the item that has the specified + /// name, otherwise returns null + /// + public string GetMetadataValue(string name) + { + string escapedValue = (this as IMetadataTable).GetEscapedValue(name); + + return (escapedValue == null) ? null : EscapingUtilities.UnescapeAll(escapedValue); + } + + /// + /// Sets a new metadata value on the ItemDefinition. + /// + /// Unevaluated value is assumed to be escaped as necessary + public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue) + { + XmlUtilities.VerifyThrowArgumentValidElementName(name); + ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); + ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[name] == null, "CannotModifyReservedItemMetadata", name); + + ProjectMetadata metadatum; + + if (_metadata != null) + { + metadatum = _metadata[name]; + + if (metadatum != null) + { + Project.VerifyThrowInvalidOperationNotImported(metadatum.Xml.ContainingProject); + metadatum.UnevaluatedValue = unevaluatedValue; + return metadatum; + } + } + + // We can't use the item definition that this object came from as a root, as it doesn't map directly + // to a single XML element. Instead, add a new one to the project. Best we can do. + ProjectItemDefinitionElement itemDefinition = _project.Xml.AddItemDefinition(_itemType); + + ProjectMetadataElement metadatumXml = itemDefinition.AddMetadata(name, unevaluatedValue); + + _metadata = _metadata ?? new PropertyDictionary(); + + string evaluatedValueEscaped = _project.ExpandMetadataValueBestEffortLeaveEscaped(this, unevaluatedValue, metadatumXml.Location); + + metadatum = new ProjectMetadata(this, metadatumXml, evaluatedValueEscaped, null /* predecessor unknown */); + + _metadata.Set(metadatum); + + return metadatum; + } + + #region IItemDefinition Members + + /// + /// Sets a new metadata value on the ItemDefinition. + /// This is ONLY called during evaluation and does not affect the XML. + /// + ProjectMetadata IItemDefinition.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedValue, ProjectMetadata predecessor) + { + _metadata = _metadata ?? new PropertyDictionary(); + + ProjectMetadata metadatum = new ProjectMetadata(this, metadataElement, evaluatedValue, predecessor); + _metadata.Set(metadatum); + + return metadatum; + } + + #endregion + + #region IMetadataTable Members + + /// + /// Retrieves the value of the named metadatum. + /// + /// The metadatum to retrieve. + /// The value, or an empty string if there is none by that name. + string IMetadataTable.GetEscapedValue(string name) + { + return ((IMetadataTable)this).GetEscapedValue(null, name); + } + + /// + /// Retrieves the value of the named metadatum. + /// + /// The type of item. + /// The metadatum to retrieve. + /// The value, or an empty string if there is none by that name. + string IMetadataTable.GetEscapedValue(string specifiedItemType, string name) + { + return ((IMetadataTable)this).GetEscapedValueIfPresent(specifiedItemType, name) ?? String.Empty; + } + + /// + /// Retrieves the value of the named metadatum, or null if it doesn't exist + /// + /// The type of item. + /// The metadatum to retrieve. + /// The value, or null if there is none by that name. + string IMetadataTable.GetEscapedValueIfPresent(string specifiedItemType, string name) + { + if (_metadata == null) + { + return null; + } + + if (specifiedItemType == null || String.Equals(_itemType, specifiedItemType, StringComparison.OrdinalIgnoreCase)) + { + ProjectMetadata metadatum = GetMetadata(name); + if (metadatum != null) + { + return metadatum.EvaluatedValueEscaped; + } + } + + return null; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Definition/ProjectMetadata.cs b/src/XMakeBuildEngine/Definition/ProjectMetadata.cs new file mode 100644 index 00000000000..154e5015b01 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ProjectMetadata.cs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps a logical metadatum on an item. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An evaluated design-time metadatum. + /// Parented either by a ProjectItemDefinition or a ProjectItem. + /// + /// + /// Never used to represent built-in metadata, like %(Filename). There is always a backing XML object. + /// + [DebuggerDisplay("{Name}={EvaluatedValue} [{xml.Value}]")] + public class ProjectMetadata : IKeyed, IValued, IEquatable, IMetadatum + { + /// + /// Parent item or item definition that this metadatum lives in. + /// ProjectMetadata's always live in a project and always have a parent. + /// The project can be gotten from this parent. + /// Used to evaluate any updates. + /// + private readonly IProjectMetadataParent _parent; + + /// + /// Backing XML metadata. + /// Can never be null. + /// + private readonly ProjectMetadataElement _xml; + + /// + /// Evaluated value + /// + private string _evaluatedValueEscaped; + + /// + /// Any immediately previous metadatum (from item definition or item) that was overridden by this one during evaluation. + /// This would include all metadata with the same name that lie above in the logical + /// project file, who are on item definitions of the same type, and whose conditions evaluated to true. + /// If this metadatum is on an item, it would include any previous metadatum with the same name on the same item whose condition + /// evaluated to true, and following that any item definition metadata. + /// If there are none above this is null. + /// If the project has not been reevaluated since the last modification this value may be incorrect. + /// + private ProjectMetadata _predecessor; + + /// + /// Creates a metadata backed by XML. + /// Constructed during evaluation of a project. + /// + internal ProjectMetadata(IProjectMetadataParent parent, ProjectMetadataElement xml, string evaluatedValueEscaped, ProjectMetadata predecessor) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); + ErrorUtilities.VerifyThrowArgumentNull(evaluatedValueEscaped, "evaluatedValueEscaped"); + + _parent = parent; + _xml = xml; + _evaluatedValueEscaped = evaluatedValueEscaped; + _predecessor = predecessor; + } + + /// + /// Name of the metadata + /// + public string Name + { + [DebuggerStepThrough] + get + { return _xml.Name; } + } + + /// + /// Gets the evaluated metadata value. + /// Cannot be set directly: only the unevaluated value can be set. + /// Is never null. + /// + public string EvaluatedValue + { + [DebuggerStepThrough] + get + { return EscapingUtilities.UnescapeAll(_evaluatedValueEscaped); } + } + + /// + /// Gets or sets the unevaluated metadata value. + /// + /// As well as updating the unevaluated value, the setter updates the evaluated value, but does not affect anything else in the project until reevaluation. For example, + /// --if a piece of metadata named "m" is modified on item of type "i", it does not affect "j" which is evaluated from "@(j->'%(m)')" until reevaluation. + /// --if the unevaluated value of "m" is set to something that is modified by evaluation, such as "$(p)", the evaluated value will be set to "$(p)" until reevaluation. + /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state. + /// + /// Setting metadata through a ProjectItem may cause the underlying ProjectItemElement to be split, if it originated with an itemlist, wildcard, or semicolon expression, + /// because it was clear that the caller intended to only affect that particular item. + /// Setting metadata through a ProjectMetadata does not cause any splitting, because we assume the caller presumably intends to affect all items using the underlying + /// ProjectMetadataElement. At least, this seems a reasonable assumption, and it avoids the need for metadata to hold a pointer to their containing items. + /// + /// + /// The containing project will be dirtied by the XML modification. Unevaluated values are assumed to be passed in escaped as necessary. + /// + public string UnevaluatedValue + { + [DebuggerStepThrough] + get + { + return _xml.Value; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "value"); + Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); + ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent != null && _xml.Parent.Parent != null && _xml.Parent.Parent.Parent != null, "OM_ObjectIsNoLongerActive"); + + if (String.Equals(_xml.Value, value, StringComparison.Ordinal)) + { + return; + } + + _xml.Value = value; + + // Clear out the current value of this metadata, so the new value can't refer to the old one. + // The expansion call below otherwise passes in the parent item's metadata - including this one's + // current value. + _evaluatedValueEscaped = String.Empty; + + _evaluatedValueEscaped = _parent.Project.ExpandMetadataValueBestEffortLeaveEscaped(_parent, value, Location); + } + } + + /// + /// Backing XML metadata. + /// Can never be null. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public ProjectMetadataElement Xml + { + [DebuggerStepThrough] + get + { return _xml; } + } + + /// + /// Project that this metadatum lives in. + /// ProjectMetadata's always live in a project. + /// + public Project Project + { + [DebuggerStepThrough] + get + { return _parent.Project; } + } + + /// + /// The item type of the parent item definition or item. + /// + public string ItemType + { + get { return _parent.ItemType; } + } + + /// + /// Any immediately previous metadatum (from item definition or item) that was overridden by this one during evaluation. + /// This would include all metadata with the same name that lie above in the logical + /// project file, who are on item definitions of the same type, and whose conditions evaluated to true. + /// If this metadatum is on an item, it would include any previous metadatum with the same name on the same item whose condition + /// evaluated to true, and following that any item definition metadata. + /// If there are none above this is null. + /// If the project has not been reevaluated since the last modification this value may be incorrect. + /// + public ProjectMetadata Predecessor + { + [DebuggerStepThrough] + get + { return _predecessor; } + } + + /// + /// If the metadatum originated in an imported file, returns true. + /// Otherwise returns false. + /// + public bool IsImported + { + get + { + bool isImported = !Object.ReferenceEquals(_xml.ContainingProject, _parent.Project.Xml); + + return isImported; + } + } + + /// + /// Location of the element + /// + public ElementLocation Location + { + get { return _xml.Location; } + } + + /// + /// Location of the condition attribute + /// + public ElementLocation ConditionLocation + { + get { return _xml.ConditionLocation; } + } + + /// + /// Implementation of IKeyed exposing the metadata name, so metadata + /// can be put in a dictionary conveniently. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + [DebuggerStepThrough] + get + { return Name; } + } + + /// + /// Implementation of IValued + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IValued.EscapedValue + { + [DebuggerStepThrough] + get + { return EvaluatedValueEscaped; } + } + + /// + /// Gets the evaluated metadata value. + /// Cannot be set directly: only the unevaluated value can be set. + /// Is never null. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal string EvaluatedValueEscaped + { + [DebuggerStepThrough] + get + { return _evaluatedValueEscaped; } + } + + #region IEquatable Members + + /// + /// Compares this metadata to another for equivalence. + /// + /// The other metadata + /// True if they are equivalent, false otherwise. + bool IEquatable.Equals(ProjectMetadata other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (other == null) + { + return false; + } + + return (_xml == other._xml && + _evaluatedValueEscaped == other._evaluatedValueEscaped); + } + + #endregion + + /// + /// Deep clone a metadatum, retaining the same parent. + /// + internal ProjectMetadata DeepClone() + { + // The new metadatum's predecessor is the same as its original's predecessor, just as the XML is the same + // as its original's XML. Predecessors map to XML elements. + return new ProjectMetadata(_parent, this.Xml, this.EvaluatedValueEscaped, this.Predecessor); + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ProjectProperty.cs b/src/XMakeBuildEngine/Definition/ProjectProperty.cs new file mode 100644 index 00000000000..70bd6f746c1 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ProjectProperty.cs @@ -0,0 +1,629 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps a logical property on an item. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An evaluated design-time property + /// + [DebuggerDisplay("{Name}={EvaluatedValue} [{UnevaluatedValue}]")] + public abstract class ProjectProperty : IKeyed, IValued, IProperty, IEquatable + { + /// + /// Project that this property lives in. + /// ProjectProperty's always live in a project. + /// Used to evaluate any updates. + /// + private readonly Project _project; + + /// + /// Evaluated value of the property. Escaped as necessary. + /// + private string _evaluatedValueEscaped; + + /// + /// Creates a property. + /// + internal ProjectProperty(Project project, string evaluatedValueEscaped) + { + ErrorUtilities.VerifyThrowArgumentNull(project, "project"); + ErrorUtilities.VerifyThrowArgumentNull(evaluatedValueEscaped, "evaluatedValueEscaped"); + + _project = project; + _evaluatedValueEscaped = evaluatedValueEscaped; + } + + /// + /// Name of the property. + /// Cannot be set. + /// + /// + /// If this could be set, it would be necessary to have a callback + /// so that the containing collections could be updated, as they use the name as + /// their key. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public abstract string Name + { + [DebuggerStepThrough] + get; + } + + /// + /// Gets the evaluated property value. + /// Cannot be set directly: only the unevaluated value can be set. + /// Is never null. + /// + /// + /// Unescaped value of the evaluated property + /// + public string EvaluatedValue + { + [DebuggerStepThrough] + get + { return EscapingUtilities.UnescapeAll(_evaluatedValueEscaped); } + } + + /// + /// Gets the evaluated property value. + /// Cannot be set directly: only the unevaluated value can be set. + /// Is never null. + /// + /// + /// Evaluated property escaped as necessary + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IProperty.EvaluatedValueEscaped + { + [DebuggerStepThrough] + get + { return _evaluatedValueEscaped; } + } + + /// + /// Gets or sets the unevaluated property value. + /// Updates the evaluated value in the project, although this is not sure to be correct until re-evaluation. + /// + public abstract string UnevaluatedValue + { + [DebuggerStepThrough] + get; + set; + } + + /// + /// Whether the property originated from the environment (or the toolset) + /// + public abstract bool IsEnvironmentProperty + { + [DebuggerStepThrough] + get; + } + + /// + /// Whether the property is a global property + /// + public abstract bool IsGlobalProperty + { + [DebuggerStepThrough] + get; + } + + /// + /// Whether the property is a reserved property, + /// like 'MSBuildProjectFile'. + /// + public abstract bool IsReservedProperty + { + [DebuggerStepThrough] + get; + } + + /// + /// Backing XML property. + /// Null only if this is a global, environment, or built-in property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public abstract ProjectPropertyElement Xml + { + [DebuggerStepThrough] + get; + } + + /// + /// Project that this property lives in. + /// ProjectProperty's always live in a project. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public Project Project + { + [DebuggerStepThrough] + get + { return _project; } + } + + /// + /// Any immediately previous property that was overridden by this one during evaluation. + /// This would include all properties with the same name that lie above in the logical + /// project file, and whose conditions evaluated to true. + /// If there are none above this is null. + /// If the project has not been reevaluated since the last modification this value may be incorrect. + /// + public abstract ProjectProperty Predecessor + { + [DebuggerStepThrough] + get; + } + + /// + /// If the property originated in an imported file, returns true. + /// If the property originates from the environment, a global property, or is a built-in property, returns false. + /// Otherwise returns false. + /// + public abstract bool IsImported + { + get; + } + + /// + /// Implementation of IKeyed exposing the property name, so properties + /// can be put in a dictionary conveniently. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + [DebuggerStepThrough] + get + { return Name; } + } + + /// + /// Implementation of IValued + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IValued.EscapedValue + { + [DebuggerStepThrough] + get + { return _evaluatedValueEscaped; } + } + + #region IEquatable Members + + /// + /// Compares this property to another for equivalence. + /// + /// The other property. + /// True if the properties are equivalent, false otherwise. + bool IEquatable.Equals(ProjectProperty other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (null == other) + { + return false; + } + + return _project == other._project && + Xml == other.Xml && + _evaluatedValueEscaped == other._evaluatedValueEscaped && + Name == other.Name; + } + + #endregion + + /// + /// Creates a property without backing XML. + /// Property MAY BE global, and property MAY HAVE a reserved name (such as "MSBuildProjectDirectory") if indicated. + /// This is ONLY to be used by the Evaluator (and Project.SetGlobalProperty) and ONLY for Global, Environment, and Built-in properties. + /// All other properties originate in XML, and should have a backing XML object. + /// + internal static ProjectProperty Create(Project project, string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + { + return new ProjectPropertyNotXmlBacked(project, name, evaluatedValueEscaped, isGlobalProperty, mayBeReserved); + } + + /// + /// Creates a regular evaluated property, with backing XML. + /// Called by Project.SetProperty. + /// Property MAY NOT have reserved name and MAY NOT overwrite a global property. + /// Predecessor is any immediately previous property that was overridden by this one during evaluation and may be null. + /// + internal static ProjectProperty Create(Project project, ProjectPropertyElement xml, string evaluatedValueEscaped, ProjectProperty predecessor) + { + if (predecessor == null) + { + return new ProjectPropertyXmlBacked(project, xml, evaluatedValueEscaped); + } + else + { + return new ProjectPropertyXmlBackedWithPredecessor(project, xml, evaluatedValueEscaped, predecessor); + } + } + + /// + /// Called ONLY by the project in order to update the evaluated value + /// after a property set occurring between full evaluations. + /// + /// + /// Method instead of a setter on EvaluatedValue to try to make clear its limited purpose. + /// + internal void UpdateEvaluatedValue(string evaluatedValueEscaped) + { + _evaluatedValueEscaped = evaluatedValueEscaped; + } + + /// + /// Looks for a matching global property. + /// + /// + /// The reason we do this and not just look at project.GlobalProperties is + /// that when the project is being loaded, the GlobalProperties collection is already populated. When we do our + /// evaluation, we may attempt to add some properties, such as environment variables, to the master Properties + /// collection. As GlobalProperties are supposed to override these and thus be added last, we can't check against + /// the GlobalProperties collection as they are being added. The correct behavior is to always check against the + /// collection which is accumulating properties as we go, which is the Properties collection. Once the project has + /// been fully populated, this method will also ensure that further properties do not attempt to override global + /// properties, as those will have the global property flag set. + /// + /// The project to compare with. + /// The property name to look up + /// True if there is a matching global property, false otherwise. + private static bool ProjectHasMatchingGlobalProperty(Project project, string propertyName) + { + ProjectProperty property = project.GetProperty(propertyName); + if (property != null && property.IsGlobalProperty && !project.GlobalPropertiesToTreatAsLocal.Contains(propertyName)) + { + return true; + } + + return false; + } + + /// + /// Regular property, originating in an XML node, but with no predecessor (property with same name that it overrode during evaluation) + /// + private class ProjectPropertyXmlBacked : ProjectProperty + { + /// + /// Backing XML property. + /// Never null. + /// + private readonly ProjectPropertyElement _xml; + + /// + /// Creates a regular evaluated property, with backing XML. + /// Called by Project.SetProperty. + /// Property MAY NOT have reserved name and MAY NOT overwrite a global property. + /// Predecessor is any immediately previous property that was overridden by this one during evaluation and may be null. + /// + internal ProjectPropertyXmlBacked(Project project, ProjectPropertyElement xml, string evaluatedValueEscaped) + : base(project, evaluatedValueEscaped) + { + ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); + ErrorUtilities.VerifyThrowInvalidOperation(!ProjectHasMatchingGlobalProperty(project, xml.Name), "OM_GlobalProperty", xml.Name); + + _xml = xml; + } + + /// + /// Name of the property. + /// Cannot be set. + /// + /// + /// If this could be set, it would be necessary to have a callback + /// so that the containing collections could be updated, as they use the name as + /// their key. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override string Name + { + [DebuggerStepThrough] + get + { return _xml.Name; } + } + + /// + /// Gets or sets the unevaluated property value. + /// Updates the evaluated value in the project, although this is not sure to be correct until re-evaluation. + /// + /// + /// The containing project will be dirtied by the XML modification. + /// If there is no XML backing, the evaluated value returned is the value of the property that has been + /// escaped as necessary. + /// + public override string UnevaluatedValue + { + [DebuggerStepThrough] + get + { + return _xml.Value; + } + + set + { + Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject); + ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent != null && _xml.Parent.Parent != null, "OM_ObjectIsNoLongerActive"); + + _xml.Value = value; + + _evaluatedValueEscaped = _project.ExpandPropertyValueBestEffortLeaveEscaped(value, _xml.Location); + } + } + + /// + /// Whether the property originated from the environment (or the toolset) + /// + public override bool IsEnvironmentProperty + { + [DebuggerStepThrough] + get + { return false; } + } + + /// + /// Whether the property is a global property + /// + public override bool IsGlobalProperty + { + [DebuggerStepThrough] + get + { return false; } + } + + /// + /// Whether the property is a reserved property, + /// like 'MSBuildProjectFile'. + /// + public override bool IsReservedProperty + { + [DebuggerStepThrough] + get + { return false; } + } + + /// + /// Backing XML property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override ProjectPropertyElement Xml + { + [DebuggerStepThrough] + get + { return _xml; } + } + + /// + /// Any immediately previous property that was overridden by this one during evaluation. + /// This would include all properties with the same name that lie above in the logical + /// project file, and whose conditions evaluated to true. + /// In this class this is null. + /// If the project has not been reevaluated since the last modification this value may be incorrect. + /// + public override ProjectProperty Predecessor + { + [DebuggerStepThrough] + get + { return null; } + } + + /// + /// If the property originated in an imported file, returns true. + /// Otherwise returns false. + /// + public override bool IsImported + { + get + { + bool isImported = !Object.ReferenceEquals(_xml.ContainingProject, _project.Xml); + + return isImported; + } + } + } + + /// + /// Regular property, originating in an XML node, and with a predecessor (property with same name that was overridden during evaluation) + /// + private class ProjectPropertyXmlBackedWithPredecessor : ProjectPropertyXmlBacked + { + /// + /// Any immediately previous property that was overridden by this one during evaluation. + /// This would include all properties with the same name that lie above in the logical + /// project file, and whose conditions evaluated to true. + /// If there are none above this is null. + /// If the project has not been reevaluated since the last modification this value may be incorrect. + /// + private ProjectProperty _predecessor; + + /// + /// Creates a regular evaluated property, with backing XML. + /// Called by Project.SetProperty. + /// Property MAY NOT have reserved name and MAY NOT overwrite a global property. + /// Predecessor is any immediately previous property that was overridden by this one during evaluation and may be null. + /// + internal ProjectPropertyXmlBackedWithPredecessor(Project project, ProjectPropertyElement xml, string evaluatedValueEscaped, ProjectProperty predecessor) + : base(project, xml, evaluatedValueEscaped) + { + ErrorUtilities.VerifyThrowArgumentNull(predecessor, "predecessor"); + + _predecessor = predecessor; + } + + /// + /// Any immediately previous property that was overridden by this one during evaluation. + /// This would include all properties with the same name that lie above in the logical + /// project file, and whose conditions evaluated to true. + /// If there are none above this is null. + /// If the project has not been reevaluated since the last modification this value may be incorrect. + /// + public override ProjectProperty Predecessor + { + [DebuggerStepThrough] + get + { return _predecessor; } + } + } + + /// + /// Global/environment/toolset properties are the minority; + /// they don't originate with XML, so we must store their name (instead) + /// + private class ProjectPropertyNotXmlBacked : ProjectProperty + { + /// + /// Name of the property. + /// + private readonly string _name; + + /// + /// Creates a property without backing XML. + /// Property MAY BE global, and property MAY HAVE a reserved name (such as "MSBuildProjectDirectory") if indicated. + /// This is ONLY to be used by the Evaluator (and Project.SetGlobalProperty) and ONLY for Global, Environment, and Built-in properties. + /// All other properties originate in XML, and should have a backing XML object. + /// + internal ProjectPropertyNotXmlBacked(Project project, string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + : base(project, evaluatedValueEscaped) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowInvalidOperation(isGlobalProperty || !ProjectHasMatchingGlobalProperty(project, name), "OM_GlobalProperty", name); + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[name] == null, "OM_ReservedName", name); + ErrorUtilities.VerifyThrowArgument(mayBeReserved || !ReservedPropertyNames.IsReservedProperty(name), "OM_ReservedName", name); + + _name = name; + } + + /// + /// Name of the property. + /// Cannot be set. + /// + /// + /// If this could be set, it would be necessary to have a callback + /// so that the containing collections could be updated, as they use the name as + /// their key. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override string Name + { + [DebuggerStepThrough] + get + { return _name; } + } + + /// + /// Gets or sets the unevaluated property value. + /// Updates the evaluated value in the project, although this is not sure to be correct until re-evaluation. + /// + /// + /// The containing project will be dirtied. + /// As there is no XML backing, the evaluated value returned is the value of the property that has been + /// escaped as necessary. + /// + public override string UnevaluatedValue + { + [DebuggerStepThrough] + get + { + return ((IProperty)this).EvaluatedValueEscaped; + } + + set + { + ErrorUtilities.VerifyThrowInvalidOperation(!IsReservedProperty, "OM_ReservedName", _name); + ErrorUtilities.VerifyThrowInvalidOperation(!IsGlobalProperty, "OM_GlobalProperty", _name); + + if (IsEnvironmentProperty) + { + // Although this is an environment property, the user wants it + // to be persisted. So as well as updating this object, + // tell the project to add a real persisted property to match. + _evaluatedValueEscaped = value; + + _project.Xml.AddProperty(_name, value); + + return; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + + /// + /// Whether the property originated from the environment (or the toolset) + /// + public override bool IsEnvironmentProperty + { + get { return (!IsGlobalProperty && !IsReservedProperty); } + } + + /// + /// Whether the property is a global property + /// + public override bool IsGlobalProperty + { + [DebuggerStepThrough] + get + { return _project.GlobalProperties.ContainsKey(Name); } + } + + /// + /// Whether the property is a reserved property, + /// like 'MSBuildProjectFile'. + /// + public override bool IsReservedProperty + { + [DebuggerStepThrough] + get + { return ReservedPropertyNames.IsReservedProperty(Name); } + } + + /// + /// Backing XML property. + /// Null because this is a global, environment, or built-in property. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override ProjectPropertyElement Xml + { + [DebuggerStepThrough] + get + { return null; } + } + + /// + /// Any immediately previous property that was overridden by this one during evaluation. + /// Because these properties are not backed by XML, they cannot have precedessors. + /// + public override ProjectProperty Predecessor + { + [DebuggerStepThrough] + get + { return null; } + } + + /// + /// Whether the property originated in an imported file. + /// Because these properties did not originate in an XML file, this always returns null. + /// + public override bool IsImported + { + get { return false; } + } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ResolvedImport.cs b/src/XMakeBuildEngine/Definition/ResolvedImport.cs new file mode 100644 index 00000000000..2da297f82e3 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ResolvedImport.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A hack (leaking into public API) to prevent a certain case of Jitting in our NGen'd assemblies. +//----------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Encapsulates an import relationship in an evaluated project + /// between a ProjectImportElement and the ProjectRootElement of the + /// imported project. + /// + /// + /// This struct is functionally identical to KeyValuePair, but is necessary to avoid + /// CA908 warnings (types that in ngen images that will JIT). + /// It works because although this is a value type, it is not defined in mscorlib. + /// Essentially we would use KeyValuePair except for this technical reason. + /// + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Not possible as Equals cannot be implemented on the struct members")] + public struct ResolvedImport + { + /// + /// Element doing the import + /// + private ProjectImportElement _importingElement; + + /// + /// One of the files it causes to import + /// + private ProjectRootElement _importedProject; + + /// + /// Whether the importing element is itself imported. + /// + private bool _isImported; + + /// + /// Initializes a new instance of the struct. + /// + internal ResolvedImport(Project project, ProjectImportElement importingElement, ProjectRootElement importedProject) + { + ErrorUtilities.VerifyThrowInternalNull(importingElement, "parent"); + ErrorUtilities.VerifyThrowInternalNull(importedProject, "child"); + + _importingElement = importingElement; + _importedProject = importedProject; + _isImported = !ReferenceEquals(project.Xml, importingElement.ContainingProject); + } + + /// + /// Gets the element doing the import. + /// + public ProjectImportElement ImportingElement + { + get { return _importingElement; } + } + + /// + /// Gets one of the imported projects. + /// + public ProjectRootElement ImportedProject + { + get { return _importedProject; } + } + + /// + /// Whether the importing element is itself imported. + /// + public bool IsImported + { + get { return _isImported; } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/SubToolset.cs b/src/XMakeBuildEngine/Definition/SubToolset.cs new file mode 100644 index 00000000000..31bd9339187 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/SubToolset.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An object containing properties of a sub-toolset. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using ObjectModel = System.Collections.ObjectModel; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Aggregation of a set of properties that correspond to a particular sub-toolset. + /// + [DebuggerDisplay("SubToolsetVersion={SubToolsetVersion} #Properties={properties.Count}")] + public class SubToolset : INodePacketTranslatable + { + /// + /// VisualStudioVersion that corresponds to this subtoolset + /// + private string _subToolsetVersion; + + /// + /// The properties defined by the subtoolset. + /// + private PropertyDictionary _properties; + + /// + /// Constructor that associates a set of properties with a sub-toolset version. + /// + internal SubToolset(string subToolsetVersion, PropertyDictionary properties) + { + ErrorUtilities.VerifyThrowArgumentLength(subToolsetVersion, "subToolsetVersion"); + + _subToolsetVersion = subToolsetVersion; + _properties = properties; + } + + /// + /// Private constructor for translation + /// + private SubToolset(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// VisualStudioVersion that corresponds to this subtoolset + /// + public string SubToolsetVersion + { + get + { + return _subToolsetVersion; + } + } + + /// + /// The properties that correspond to this particular sub-toolset. + /// + public IDictionary Properties + { + get + { + if (_properties == null) + { + return ReadOnlyEmptyDictionary.Instance; + } + + return new ObjectModel.ReadOnlyDictionary(_properties); + } + } + + /// + /// Translates the sub-toolset. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _subToolsetVersion); + translator.TranslateProjectPropertyInstanceDictionary(ref _properties); + } + + /// + /// Factory for deserialization. + /// + internal static SubToolset FactoryForDeserialization(INodePacketTranslator translator) + { + SubToolset subToolset = new SubToolset(translator); + return subToolset; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Definition/Toolset.cs b/src/XMakeBuildEngine/Definition/Toolset.cs new file mode 100644 index 00000000000..97e79a7d785 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/Toolset.cs @@ -0,0 +1,1072 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An object containing properties of a toolset. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Xml; +using System.Linq; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using Microsoft.Win32; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using ObjectModel = System.Collections.ObjectModel; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Delegate for loading an Xml file, for unit testing. + /// + /// The path to load. + /// An Xml document. + internal delegate XmlDocumentWithLocation LoadXmlFromPath(string path); + + /// + /// Aggregation of a toolset version (eg. "2.0"), tools path, and optional set of associated properties. + /// Toolset is immutable. + /// + /// + /// UNDONE: Review immutability. If this is not immutable, add a mechanism to notify the project collection/s owning it to increment their toolsetVersion. + /// + [DebuggerDisplay("ToolsVersion={ToolsVersion} ToolsPath={ToolsPath} #Properties={properties.Count}")] + public class Toolset : INodePacketTranslatable + { + /// + /// these files list all default tasks and task assemblies that do not need to be explicitly declared by projects + /// + private const string DefaultTasksFilePattern = "*.tasks"; + + /// + /// these files list all Override tasks and task assemblies that do not need to be explicitly declared by projects + /// + private const string OverrideTasksFilePattern = "*.overridetasks"; + + /// + /// Regkey that we check to see whether Dev10 is installed. This should exist if any SKU of Dev10 is installed, + /// but is not removed even when the last version of Dev10 is uninstalled, due to 10.0\bsln sticking around. + /// + private const string Dev10OverallInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vs\Servicing\10.0"; + + /// + /// Regkey that we check to see whether Dev10 Ultimate is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10UltimateInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vs\Servicing\10.0\vstscore"; + + /// + /// Regkey that we check to see whether Dev10 Premium is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10PremiumInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vs\Servicing\10.0\vstdcore"; + + /// + /// Regkey that we check to see whether Dev10 Professional is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10ProfessionalInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vs\Servicing\10.0\procore"; + + /// + /// Regkey that we check to see whether C# Express 2010 is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10VCSExpressInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vcs\Servicing\10.0\xcor"; + + /// + /// Regkey that we check to see whether VB Express 2010 is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10VBExpressInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vb\Servicing\10.0\xcor"; + + /// + /// Regkey that we check to see whether VC Express 2010 is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10VCExpressInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vc\Servicing\10.0\xcor"; + + /// + /// Regkey that we check to see whether VWD Express 2010 is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10VWDExpressInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vns\Servicing\10.0\xcor"; + + /// + /// Regkey that we check to see whether LightSwitch 2010 is installed. This will exist if it is installed, and be + /// properly removed after it has been uninstalled. + /// + private const string Dev10LightSwitchInstallKeyRegistryPath = @"Software\Microsoft\DevDiv\vs\Servicing\10.0\vslscore"; + + /// + /// Null if it hasn't been figured out yet; true if (some variation of) Visual Studio 2010 is installed on + /// the current machine, false otherwise. + /// + private static bool? s_dev10IsInstalled = null; + + /// + /// Name of the tools version + /// + private string _toolsVersion; + + /// + /// The MSBuildBinPath (and ToolsPath) for this tools version + /// + private string _toolsPath; + + /// + /// The properties defined by the toolset. + /// + private PropertyDictionary _properties; + + /// + /// Path to look for msbuild override task files. + /// + private string _overrideTasksPath; + + /// + /// ToolsVersion to use as the default ToolsVersion for this version of MSBuild + /// + private string _defaultOverrideToolsVersion; + + /// + /// The environment properties + /// + private PropertyDictionary _environmentProperties; + + /// + /// The build-global properties + /// + private PropertyDictionary _globalProperties; + + /// + /// indicates if the default tasks file has already been scanned + /// + private bool _defaultTasksRegistrationAttempted; + + /// + /// indicates if the override tasks file has already been scanned + /// + private bool _overrideTasksRegistrationAttempted; + + /// + /// holds all the default tasks we know about and the assemblies they exist in + /// + private TaskRegistry _defaultTaskRegistry; + + /// + /// holds all the override tasks we know about and the assemblies they exist in + /// + private TaskRegistry _overrideTaskRegistry; + + /// + /// Delegate to retrieving files. For unit testing only. + /// + private DirectoryGetFiles _getFiles; + + /// + /// Delegate to check to see if a direcotry exists + /// + private DirectoryExists _directoryExists = null; + + /// + /// Delegate for loading Xml. For unit testing only. + /// + private LoadXmlFromPath _loadXmlFromPath; + + /// + /// Expander to expand the properties and items in the using tasks files + /// + private Expander _expander; + + /// + /// Bag of properties for the expander to expand the properties and items in the using tasks files + /// + private PropertyDictionary _propertyBag; + + /// + /// SubToolsets that map to this toolset. + /// + private Dictionary _subToolsets; + + /// + /// If no sub-toolset is specified, this is the default sub-toolset version. Null == no default + /// sub-toolset, just use the base toolset. + /// + private string _defaultSubToolsetVersion; + + /// + /// Constructor taking only tools version and a matching tools path + /// + /// Name of the toolset + /// Path to this toolset's tasks and targets + /// The project collection from which to obtain the properties. + /// The path to search for msbuild overridetasks files. + public Toolset(string toolsVersion, string toolsPath, ProjectCollection projectCollection, string msbuildOverrideTasksPath) + : this(toolsVersion, toolsPath, null, projectCollection, msbuildOverrideTasksPath) + { + } + + /// + /// Constructor that also associates a set of properties with the tools version + /// + /// Name of the toolset + /// Path to this toolset's tasks and targets + /// + /// Properties that should be associated with the Toolset. + /// May be null, in which case an empty property group will be used. + /// + public Toolset(string toolsVersion, string toolsPath, IDictionary buildProperties, ProjectCollection projectCollection, string msbuildOverrideTasksPath) + : this(toolsVersion, toolsPath, buildProperties, projectCollection, null, msbuildOverrideTasksPath) + { + } + + /// + /// Constructor that also associates a set of properties with the tools version + /// + /// Name of the toolset + /// Path to this toolset's tasks and targets + /// + /// Properties that should be associated with the Toolset. + /// May be null, in which case an empty property group will be used. + /// + /// The project collection that this toolset should inherit from + /// The set of sub-toolsets to add to this toolset + /// The override tasks path. + public Toolset(string toolsVersion, string toolsPath, IDictionary buildProperties, ProjectCollection projectCollection, IDictionary subToolsets, string msbuildOverrideTasksPath) + : this(toolsVersion, toolsPath, null, projectCollection.EnvironmentProperties, projectCollection.GlobalPropertiesCollection, subToolsets, msbuildOverrideTasksPath, defaultOverrideToolsVersion: null) + { + _properties = new PropertyDictionary(); + if (null != buildProperties) + { + foreach (KeyValuePair keyValuePair in buildProperties) + { + _properties.Set(ProjectPropertyInstance.Create(keyValuePair.Key, keyValuePair.Value, true)); + } + } + } + + /// + /// Constructor taking only tools version and a matching tools path + /// + /// Name of the toolset + /// Path to this toolset's tasks and targets + internal Toolset(string toolsVersion, string toolsPath, PropertyDictionary environmentProperties, PropertyDictionary globalProperties, string msbuildOverrideTasksPath, string defaultOverrideToolsVersion) + { + ErrorUtilities.VerifyThrowArgumentLength(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentLength(toolsPath, "toolsPath"); + ErrorUtilities.VerifyThrowArgumentNull(environmentProperties, "environmentProperties"); + ErrorUtilities.VerifyThrowArgumentNull(globalProperties, "globalProperties"); + + _toolsVersion = toolsVersion; + this.ToolsPath = toolsPath; + _globalProperties = globalProperties; + _environmentProperties = environmentProperties; + _overrideTasksPath = msbuildOverrideTasksPath; + _defaultOverrideToolsVersion = defaultOverrideToolsVersion; + } + + /// + /// Constructor that also associates a set of properties with the tools version + /// + /// Name of the toolset + /// Path to this toolset's tasks and targets + /// + /// Properties that should be associated with the Toolset. + /// May be null, in which case an empty property group will be used. + /// + internal Toolset(string toolsVersion, string toolsPath, PropertyDictionary buildProperties, PropertyDictionary environmentProperties, PropertyDictionary globalProperties, IDictionary subToolsets, string msbuildOverrideTasksPath, string defaultOverrideToolsVersion) + : this(toolsVersion, toolsPath, environmentProperties, globalProperties, msbuildOverrideTasksPath, defaultOverrideToolsVersion) + { + if (_properties == null) + { + if (null != buildProperties) + { + _properties = new PropertyDictionary(buildProperties); + } + else + { + _properties = new PropertyDictionary(); + } + } + + if (subToolsets != null) + { + Dictionary subToolsetsAsDictionary = subToolsets as Dictionary; + + if (subToolsetsAsDictionary != null) + { + _subToolsets = subToolsetsAsDictionary; + } + else + { + _subToolsets = new Dictionary(subToolsets); + } + } + } + + /// + /// Additional constructor to make unit testing the TaskRegistry support easier + /// + /// + /// Internal for unit test purposes only. + /// + /// Name of the toolset + /// Path to this toolset's tasks and targets + /// + /// Properties that should be associated with the Toolset. + /// May be null, in which case an empty property group will be used. + /// + /// The project collection. + /// A delegate to intercept GetFiles calls. For unit testing. + /// A delegate to intercept Xml load calls. For unit testing. + internal Toolset(string toolsVersion, string toolsPath, PropertyDictionary buildProperties, ProjectCollection projectCollection, DirectoryGetFiles getFiles, LoadXmlFromPath loadXmlFromPath, string msbuildOverrideTasksPath, DirectoryExists directoryExists) + : this(toolsVersion, toolsPath, buildProperties, projectCollection.EnvironmentProperties, projectCollection.GlobalPropertiesCollection, null, msbuildOverrideTasksPath, null) + { + ErrorUtilities.VerifyThrowInternalNull(getFiles, "getFiles"); + ErrorUtilities.VerifyThrowInternalNull(loadXmlFromPath, "loadXmlFromPath"); + + _directoryExists = directoryExists; + _getFiles = getFiles; + _loadXmlFromPath = loadXmlFromPath; + } + + /// + /// Private constructor for serialization. + /// + private Toolset(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Name of this toolset + /// + public string ToolsVersion + { + get { return _toolsVersion; } + } + + /// + /// Path to this toolset's tasks and targets. Corresponds to $(MSBuildToolsPath) in a project or targets file. + /// + public string ToolsPath + { + get + { + return _toolsPath; + } + + private set + { + // Strip the trailing backslash if it exists. This way, when somebody + // concatenates does something like "$(MSBuildToolsPath)\CSharp.targets", + // they don't end up with a double-backslash in the middle. (It doesn't + // technically hurt anything, but it doesn't look nice.) + string toolsPathToUse = value; + + if (FileUtilities.EndsWithSlash(toolsPathToUse)) + { + string rootPath = Path.GetPathRoot(Path.GetFullPath(toolsPathToUse)); + + // Only if $(MSBuildBinPath) is *NOT* the root of a drive should we strip trailing slashes + if (!String.Equals(rootPath, toolsPathToUse, StringComparison.OrdinalIgnoreCase)) + { + // Trim off one trailing slash + toolsPathToUse = toolsPathToUse.Substring(0, toolsPathToUse.Length - 1); + } + } + + _toolsPath = toolsPathToUse; + } + } + + /// + /// Properties associated with the toolset + /// + public IDictionary Properties + { + get + { + if (_properties == null) + { + return ReadOnlyEmptyDictionary.Instance; + } + + return new ObjectModel.ReadOnlyDictionary(_properties); + } + } + + /// + /// The set of sub-toolsets associated with this toolset. + /// + public IDictionary SubToolsets + { + get + { + if (_subToolsets == null) + { + return ReadOnlyEmptyDictionary.Instance; + } + + return new ObjectModel.ReadOnlyDictionary(_subToolsets); + } + } + + /// + /// Returns the default sub-toolset version for this sub-toolset. Heuristic used is: + /// 1) If Visual Studio 2010 is installed and our ToolsVersion is "4.0", use the base toolset, and return + /// a sub-toolset version of "10.0", to be set as a publicly visible property so that e.g. targets can + /// consume it. This is to handle the fact that Visual Studio 2010 did not have any concept of sub-toolsets. + /// 2) Otherwise, use the highest-versioned sub-toolset found. Sub-toolsets with numbered versions will + /// be ordered numerically; any additional sub-toolsets will be prepended to the beginning of the list in + /// the order found. We use the highest-versioned sub-toolset because, in the absence of any other information, + /// we assume that higher-versioned tools will be more likely to be able to generate something more correct. + /// + /// Will return null if there is no sub-toolset available (and Dev10 is not installed). + /// + public string DefaultSubToolsetVersion + { + get + { + if (_defaultSubToolsetVersion == null) + { + // 1) Workaround for ToolsVersion 4.0 + VS 2010 + if (String.Equals(ToolsVersion, "4.0", StringComparison.OrdinalIgnoreCase) && Dev10IsInstalled) + { + return Constants.Dev10SubToolsetValue; + } + + // 2) Otherwise, just pick the highest available. + SortedDictionary subToolsetsWithVersion = new SortedDictionary(); + List additionalSubToolsetNames = new List(); + + foreach (string subToolsetName in SubToolsets.Keys) + { + Version subToolsetVersion = VersionUtilities.ConvertToVersion(subToolsetName); + + if (subToolsetVersion != null) + { + subToolsetsWithVersion.Add(subToolsetVersion, subToolsetName); + } + else + { + // if it doesn't parse to an actual version number, shrug and just add it to the end. + additionalSubToolsetNames.Add(subToolsetName); + } + } + + List orderedSubToolsetList = new List(additionalSubToolsetNames); + orderedSubToolsetList.AddRange(subToolsetsWithVersion.Values); + + if (orderedSubToolsetList.Count > 0) + { + _defaultSubToolsetVersion = orderedSubToolsetList[orderedSubToolsetList.Count - 1]; + } + } + + return _defaultSubToolsetVersion; + } + } + + /// + /// Null if it hasn't been figured out yet; true if (some variation of) Visual Studio 2010 is installed on + /// the current machine, false otherwise. + /// + /// + /// Internal so that unit tests can use it too. + /// + internal static bool Dev10IsInstalled + { + get + { + if (s_dev10IsInstalled == null) + { + try + { + // Figure out whether Dev10 is currently installed using the following heuristic: + // - Check whether the overall key (installed if any version of Dev10 is installed) is there. + // - If it's not, no version of Dev10 exists or has ever existed on this machine, so return 'false'. + // - If it is, we know that some version of Dev10 has been installed at some point, but we don't know + // for sure whether it's still there or not. Check the inndividual keys for {Pro, Premium, Ultimate, + // C# Express, VB Express, C++ Express, VWD Express, LightSwitch} 2010 + // - If even one of them exists, return 'true'. + // - Otherwise, return 'false. + if (!RegistryKeyWrapper.KeyExists(Dev10OverallInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32)) + { + s_dev10IsInstalled = false; + } + else if ( + RegistryKeyWrapper.KeyExists(Dev10UltimateInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10PremiumInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10ProfessionalInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10VCSExpressInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10VBExpressInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10VCExpressInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10VWDExpressInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) || + RegistryKeyWrapper.KeyExists(Dev10LightSwitchInstallKeyRegistryPath, RegistryHive.LocalMachine, RegistryView.Registry32) + ) + { + s_dev10IsInstalled = true; + } + else + { + s_dev10IsInstalled = false; + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedRegistryException(e)) + { + throw; + } + + // if it's a registry exception, just shrug, eat it, and move on with life on the assumption that whatever + // went wrong, it's pretty clear that Dev10 probably isn't installed. + s_dev10IsInstalled = false; + } + } + + return s_dev10IsInstalled.Value; + } + } + + /// + /// Path to look for msbuild override task files. + /// + internal string OverrideTasksPath + { + get { return _overrideTasksPath; } + } + + /// + /// ToolsVersion to use as the default ToolsVersion for this version of MSBuild + /// + internal string DefaultOverrideToolsVersion + { + get { return _defaultOverrideToolsVersion; } + } + + /// + /// Function for serialization. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _toolsVersion); + translator.Translate(ref _toolsPath); + translator.TranslateProjectPropertyInstanceDictionary(ref _properties); + translator.TranslateProjectPropertyInstanceDictionary(ref _environmentProperties); + translator.TranslateProjectPropertyInstanceDictionary(ref _globalProperties); + translator.TranslateDictionary(ref _subToolsets, StringComparer.OrdinalIgnoreCase, SubToolset.FactoryForDeserialization); + translator.Translate(ref _overrideTasksPath); + translator.Translate(ref _defaultOverrideToolsVersion); + } + + /// + /// Generates the sub-toolset version to be used with this toolset. Sub-toolset version is based on: + /// 1. If "VisualStudioVersion" is set as a property on the toolset itself (global or environment), + /// use that. + /// 2. Otherwise, use the default sub-toolset version for this toolset. + /// + /// The sub-toolset version returned may be null; if so, that means that no sub-toolset should be used, + /// just the base toolset on its own. The sub-toolset version returned may not map to an existing + /// sub-toolset. + /// + public string GenerateSubToolsetVersion() + { + string subToolsetVersion = GenerateSubToolsetVersion(0 /* user doesn't care about solution version */); + return subToolsetVersion; + } + + /// + /// Generates the sub-toolset version to be used with this toolset. Sub-toolset version is based on: + /// 1. If the "VisualStudioVersion" global property exists in the set of properties passed to us, use it. + /// 2. Otherwise, if "VisualStudioVersion" is set as a property on the toolset itself (global or environment), + /// use that. + /// 3. Otherwise, use Visual Studio version from solution file if it maps to an existing sub-toolset. + /// 4. Otherwise, use the default sub-toolset version for this toolset. + /// + /// The sub-toolset version returned may be null; if so, that means that no sub-toolset should be used, + /// just the base toolset on its own. The sub-toolset version returned may not map to an existing + /// sub-toolset. + /// + /// The global properties dictionary may be null. + /// + [SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "solutionVersion-1", Justification = "Method called in restricted places. Checks done by the callee and inside the method.")] + public string GenerateSubToolsetVersion(IDictionary overrideGlobalProperties, int solutionVersion) + { + return GenerateSubToolsetVersionUsingVisualStudioVersion(overrideGlobalProperties, solutionVersion - 1); + } + + /// + /// Given a property name and a sub-toolset version, searches for that property first in the + /// sub-toolset, then falls back to the base toolset if necessary, and returns the property + /// if it was found. + /// + public ProjectPropertyInstance GetProperty(string propertyName, string subToolsetVersion) + { + SubToolset subToolset; + ProjectPropertyInstance property = null; + + if (SubToolsets.TryGetValue(subToolsetVersion, out subToolset)) + { + property = subToolset.Properties[propertyName]; + } + + if (property == null) + { + property = Properties[propertyName]; + } + + return property; + } + + /// + /// Factory for deserialization. + /// + static internal Toolset FactoryForDeserialization(INodePacketTranslator translator) + { + Toolset toolset = new Toolset(translator); + return toolset; + } + + /// + /// Given a search path and a task pattern get a list of task or override task files. + /// + internal static string[] GetTaskFiles(DirectoryGetFiles getFiles, ILoggingService loggingServices, BuildEventContext buildEventContext, string taskPattern, string searchPath, string taskFileWarning) + { + string[] defaultTasksFiles = { }; + + try + { + if (null != getFiles) + { + defaultTasksFiles = getFiles(searchPath, taskPattern); + } + else + { + // The order of the returned file names is not guaranteed per msdn + defaultTasksFiles = Directory.GetFiles(searchPath, taskPattern); + } + + if (defaultTasksFiles.Length == 0) + { + loggingServices.LogWarning + ( + buildEventContext, + null, + new BuildEventFileInfo(/* this warning truly does not involve any file */ String.Empty), + taskFileWarning, + taskPattern, + searchPath, + String.Empty + ); + } + } + catch (Exception e) + { + // handle problems when reading the default tasks files + if (ExceptionHandling.NotExpectedException(e)) + { + // Catching Exception, but rethrowing unless it's an IO related exception. + throw; + } + + loggingServices.LogWarning + ( + buildEventContext, + null, + new BuildEventFileInfo(/* this warning truly does not involve any file */ String.Empty), + taskFileWarning, + taskPattern, + searchPath, + e.Message + ); + } + + // Sort the file names to give a deterministic order + Array.Sort(defaultTasksFiles, StringComparer.OrdinalIgnoreCase); + return defaultTasksFiles; + } + + /// + /// Generates the sub-toolset version to be used with this toolset. Sub-toolset version is based on: + /// 1. If the "VisualStudioVersion" global property exists in the set of properties passed to us, use it. + /// 2. Otherwise, if "VisualStudioVersion" is set as a property on the toolset itself (global or environment), + /// use that. + /// 3. Otherwise, use Visual Studio version from solution file if it maps to an existing sub-toolset. + /// 4. Otherwise, use the default sub-toolset version for this toolset. + /// + /// The sub-toolset version returned may be null; if so, that means that no sub-toolset should be used, + /// just the base toolset on its own. The sub-toolset version returned may not map to an existing + /// sub-toolset. + /// + /// The global properties dictionary may be null. + /// + internal string GenerateSubToolsetVersion(PropertyDictionary overrideGlobalProperties) + { + ProjectPropertyInstance subToolsetProperty = null; + string visualStudioVersion = null; + if (overrideGlobalProperties != null) + { + subToolsetProperty = overrideGlobalProperties[Constants.SubToolsetVersionPropertyName]; + + if (subToolsetProperty != null) + { + visualStudioVersion = subToolsetProperty.EvaluatedValue; + return visualStudioVersion; + } + } + + visualStudioVersion = GenerateSubToolsetVersion(0 /* don't care about solution version */); + return visualStudioVersion; + } + + /// + /// Generates the sub-toolset version to be used with this toolset. Sub-toolset version is based on: + /// 1. If the "VisualStudioVersion" global property exists in the set of properties passed to us, use it. + /// 2. Otherwise, if "VisualStudioVersion" is set as a property on the toolset itself (global or environment), + /// use that. + /// 3. Otherwise, use Visual Studio version from solution file if it maps to an existing sub-toolset. + /// 4. Otherwise, use the default sub-toolset version for this toolset. + /// + /// The sub-toolset version returned may be null; if so, that means that no sub-toolset should be used, + /// just the base toolset on its own. The sub-toolset version returned may not map to an existing + /// sub-toolset. + /// + /// The global properties dictionary may be null. + /// + internal string GenerateSubToolsetVersion(int visualStudioVersionFromSolution) + { + // Next, try the toolset global properties (before environment properties because if there's a clash between the + // two, global should win) + if (_globalProperties != null) + { + ProjectPropertyInstance visualStudioVersionProperty = _globalProperties[Constants.SubToolsetVersionPropertyName]; + + if (visualStudioVersionProperty != null) + { + return visualStudioVersionProperty.EvaluatedValue; + } + } + + // Next, try the toolset environment properties + if (_environmentProperties != null) + { + ProjectPropertyInstance visualStudioVersionProperty = _environmentProperties[Constants.SubToolsetVersionPropertyName]; + + if (visualStudioVersionProperty != null) + { + return visualStudioVersionProperty.EvaluatedValue; + } + } + + // The VisualStudioVersion derived from parsing the solution version in the solution file + string subToolsetVersion = null; + if (visualStudioVersionFromSolution > 0) + { + Version visualStudioVersionFromSolutionAsVersion = new Version(visualStudioVersionFromSolution, 0); + subToolsetVersion = SubToolsets.Keys.FirstOrDefault(version => visualStudioVersionFromSolutionAsVersion.Equals(VersionUtilities.ConvertToVersion(version))); + } + + // Solution version also didn't work out, so fall back to default. + // If subToolsetVersion is null, there simply wasn't a matching solution version. + if (subToolsetVersion == null) + { + subToolsetVersion = DefaultSubToolsetVersion; + } + + return subToolsetVersion; + } + + /// + /// Return a task registry stub for the tasks in the *.tasks file for this toolset + /// + /// The logging services used to log during task registration. + /// The build event context used to log during task registration. + /// The task registry + internal TaskRegistry GetTaskRegistry(ILoggingService loggingServices, BuildEventContext buildEventContext, ProjectRootElementCache projectRootElementCache) + { + RegisterDefaultTasks(loggingServices, buildEventContext, projectRootElementCache); + return _defaultTaskRegistry; + } + + /// + /// Get SubToolset version using Visual Studio version from Dev 12 solution file + /// + internal string GenerateSubToolsetVersionUsingVisualStudioVersion(IDictionary overrideGlobalProperties, int visualStudioVersionFromSolution) + { + string visualStudioVersion = null; + if (overrideGlobalProperties != null && overrideGlobalProperties.TryGetValue(Constants.SubToolsetVersionPropertyName, out visualStudioVersion)) + { + return visualStudioVersion; + } + + visualStudioVersion = GenerateSubToolsetVersion(visualStudioVersionFromSolution); + return visualStudioVersion; + } + + /// + /// Return a task registry for the override tasks in the *.overridetasks file for this toolset + /// + /// The logging services used to log during task registration. + /// The build event context used to log during task registration. + /// The task registry + internal TaskRegistry GetOverrideTaskRegistry(ILoggingService loggingServices, BuildEventContext buildEventContext, ProjectRootElementCache projectRootElementCache) + { + RegisterOverrideTasks(loggingServices, buildEventContext, projectRootElementCache); + return _overrideTaskRegistry; + } + + /// + /// Used to load information about default MSBuild tasks i.e. tasks that do not need to be explicitly declared in projects + /// with the <UsingTask> element. Default task information is read from special files, which are located in the same + /// directory as the MSBuild binaries. + /// + /// + /// 1) a default tasks file needs the <Project> root tag in order to be well-formed + /// 2) the XML declaration tag <?xml ...> is ignored + /// 3) comment tags are always ignored regardless of their placement + /// 4) the rest of the tags are expected to be <UsingTask> tags + /// + /// The logging services to use to log during this registration. + /// The build event context to use to log during this registration. + private void RegisterDefaultTasks(ILoggingService loggingServices, BuildEventContext buildEventContext, ProjectRootElementCache projectRootElementCache) + { + if (!_defaultTasksRegistrationAttempted) + { + try + { + _defaultTaskRegistry = new TaskRegistry(projectRootElementCache); + + InitializeProperties(loggingServices, buildEventContext); + + string[] defaultTasksFiles = GetTaskFiles(_getFiles, loggingServices, buildEventContext, DefaultTasksFilePattern, ToolsPath, "DefaultTasksFileLoadFailureWarning"); + LoadAndRegisterFromTasksFile(ToolsPath, defaultTasksFiles, loggingServices, buildEventContext, DefaultTasksFilePattern, "DefaultTasksFileFailure", projectRootElementCache, _defaultTaskRegistry); + } + finally + { + _defaultTasksRegistrationAttempted = true; + } + } + } + + /// + /// Initialize the properties which are used to evaluate the tasks files. + /// + private void InitializeProperties(ILoggingService loggingServices, BuildEventContext buildEventContext) + { + try + { + if (_propertyBag == null) + { + List reservedProperties = new List(); + + reservedProperties.Add(ProjectPropertyInstance.Create(ReservedPropertyNames.binPath, EscapingUtilities.Escape(ToolsPath), mayBeReserved: true)); + reservedProperties.Add(ProjectPropertyInstance.Create(ReservedPropertyNames.toolsVersion, ToolsVersion, mayBeReserved: true)); + + reservedProperties.Add(ProjectPropertyInstance.Create(ReservedPropertyNames.toolsPath, EscapingUtilities.Escape(ToolsPath), mayBeReserved: true)); + reservedProperties.Add(ProjectPropertyInstance.Create(ReservedPropertyNames.assemblyVersion, Constants.AssemblyVersion, mayBeReserved: true)); + + // Add one for the subtoolset version property -- it may or may not be set depending on whether it has already been set by the + // environment or global properties, but it's better to create a dictionary that's one too big than one that's one too small. + int count = _environmentProperties.Count + reservedProperties.Count + Properties.Values.Count + _globalProperties.Count + 1; + + // GenerateSubToolsetVersion checks the environment and global properties, so it's safe to go ahead and gather the + // subtoolset properties here without fearing that we'll have somehow come up with the wrong subtoolset version. + string subToolsetVersion = this.GenerateSubToolsetVersion(); + SubToolset subToolset; + ICollection subToolsetProperties = null; + + if (subToolsetVersion != null) + { + if (SubToolsets.TryGetValue(subToolsetVersion, out subToolset)) + { + subToolsetProperties = subToolset.Properties.Values; + count += subToolsetProperties.Count; + } + } + + _propertyBag = new PropertyDictionary(count); + + // Should be imported in the same order as in the evaluator: + // - Environment + // - Toolset + // - Subtoolset (if any) + // - Global + _propertyBag.ImportProperties(_environmentProperties); + + _propertyBag.ImportProperties(reservedProperties); + + _propertyBag.ImportProperties(Properties.Values); + + if (subToolsetVersion != null) + { + _propertyBag.Set(ProjectPropertyInstance.Create(Constants.SubToolsetVersionPropertyName, subToolsetVersion)); + } + + if (subToolsetProperties != null) + { + _propertyBag.ImportProperties(subToolsetProperties); + } + + _propertyBag.ImportProperties(_globalProperties); + } + + if (_expander == null) + { + _expander = new Expander(_propertyBag); + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + // Catching Exception, but rethrowing unless it's an IO related exception. + throw; + } + + loggingServices.LogError(buildEventContext, new BuildEventFileInfo(/* this warning truly does not involve any file it is just gathering properties */String.Empty), "TasksPropertyBagError", e.Message); + } + } + + /// + /// Used to load information about MSBuild override tasks i.e. tasks that override tasks declared in tasks or project files. + /// + private void RegisterOverrideTasks(ILoggingService loggingServices, BuildEventContext buildEventContext, ProjectRootElementCache projectRootElementCache) + { + if (!_overrideTasksRegistrationAttempted) + { + try + { + _overrideTaskRegistry = new TaskRegistry(projectRootElementCache); + bool overrideDirectoryExists = false; + + try + { + // Make sure the override directory exists and is not empty before trying to find the files + if (!String.IsNullOrEmpty(_overrideTasksPath)) + { + if (Path.IsPathRooted(_overrideTasksPath)) + { + if (null != _directoryExists) + { + overrideDirectoryExists = _directoryExists(_overrideTasksPath); + } + else + { + overrideDirectoryExists = Directory.Exists(_overrideTasksPath); + } + } + + if (!overrideDirectoryExists) + { + string rootedPathMessage = ResourceUtilities.FormatResourceString("OverrideTaskNotRootedPath", _overrideTasksPath); + loggingServices.LogWarning(buildEventContext, null, new BuildEventFileInfo(String.Empty /* this warning truly does not involve any file*/), "OverrideTasksFileFailure", rootedPathMessage); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + // Catching Exception, but rethrowing unless it's an IO related exception. + throw; + } + + string rootedPathMessage = ResourceUtilities.FormatResourceString("OverrideTaskProblemWithPath", _overrideTasksPath, e.Message); + loggingServices.LogWarning(buildEventContext, null, new BuildEventFileInfo(String.Empty /* this warning truly does not involve any file*/), "OverrideTasksFileFailure", rootedPathMessage); + } + + if (overrideDirectoryExists) + { + InitializeProperties(loggingServices, buildEventContext); + string[] overrideTasksFiles = GetTaskFiles(_getFiles, loggingServices, buildEventContext, OverrideTasksFilePattern, _overrideTasksPath, "OverrideTasksFileLoadFailureWarning"); + + // Load and register any override tasks + LoadAndRegisterFromTasksFile(_overrideTasksPath, overrideTasksFiles, loggingServices, buildEventContext, OverrideTasksFilePattern, "OverrideTasksFileFailure", projectRootElementCache, _overrideTaskRegistry); + } + } + finally + { + _overrideTasksRegistrationAttempted = true; + } + } + } + + /// + /// Do the actual loading of the tasks or override tasks file and register the tasks in the task registry + /// + private void LoadAndRegisterFromTasksFile(string searchPath, string[] defaultTaskFiles, ILoggingService loggingServices, BuildEventContext buildEventContext, string defaultTasksFilePattern, string taskFileError, ProjectRootElementCache projectRootElementCache, TaskRegistry registry) + { + foreach (string defaultTasksFile in defaultTaskFiles) + { + try + { + // Important to keep the following line since unit tests use the delegate. + ProjectRootElement projectRootElement; + if (_loadXmlFromPath != null) + { + XmlDocumentWithLocation defaultTasks = _loadXmlFromPath(defaultTasksFile); + projectRootElement = ProjectRootElement.Open(defaultTasks); + } + else + { + projectRootElement = ProjectRootElement.Open(defaultTasksFile, projectRootElementCache, false /*The tasks file is not a explicitly loaded file*/); + } + + foreach (ProjectElement elementXml in projectRootElement.Children) + { + ProjectUsingTaskElement usingTask = elementXml as ProjectUsingTaskElement; + + if (null == usingTask) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + elementXml.Location, + "UnrecognizedElement", + elementXml.XmlElement.Name + ); + } + + TaskRegistry.RegisterTasksFromUsingTaskElement + ( + loggingServices, + buildEventContext, + Path.GetDirectoryName(defaultTasksFile), + usingTask, + registry, + _expander, + ExpanderOptions.ExpandProperties + ); + } + } + catch (XmlException e) + { + // handle XML errors in the default tasks file + ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, new BuildEventFileInfo(e), taskFileError, e.Message); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + // Catching Exception, but rethrowing unless it's an IO related exception. + throw; + } + + loggingServices.LogError(buildEventContext, new BuildEventFileInfo(defaultTasksFile), taskFileError, e.Message); + break; + } + } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ToolsetConfigurationReader.cs b/src/XMakeBuildEngine/Definition/ToolsetConfigurationReader.cs new file mode 100644 index 00000000000..89975c06078 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ToolsetConfigurationReader.cs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A class used to read the Toolset configuration. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Text; +using System.Globalization; +using System.Reflection; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; +using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Delegate for unit test purposes only + /// + internal delegate Configuration ReadApplicationConfiguration(); + + /// + /// Class used to read toolset configurations. + /// + internal class ToolsetConfigurationReader : ToolsetReader + { + /// + /// A section of a toolset configuration + /// + private ToolsetConfigurationSection _configurationSection = null; + + /// + /// Delegate used to read application configurations + /// + private ReadApplicationConfiguration _readApplicationConfiguration = null; + + /// + /// Flag indicating that an attempt has been made to read the configuration + /// + private bool _configurationReadAttempted = false; + + /// + /// Default constructor + /// + internal ToolsetConfigurationReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties) + : this(environmentProperties, globalProperties, new ReadApplicationConfiguration(ToolsetConfigurationReader.ReadApplicationConfiguration)) + { + } + + /// + /// Constructor taking a delegate for unit test purposes only + /// + internal ToolsetConfigurationReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties, ReadApplicationConfiguration readApplicationConfiguration) + : base(environmentProperties, globalProperties) + { + ErrorUtilities.VerifyThrowArgumentNull(readApplicationConfiguration, "readApplicationConfiguration"); + _readApplicationConfiguration = readApplicationConfiguration; + } + + /// + /// Returns the list of tools versions + /// + protected override IEnumerable ToolsVersions + { + get + { + if (ConfigurationSection != null) + { + foreach (ToolsetElement toolset in ConfigurationSection.Toolsets) + { + ElementLocation location = ElementLocation.Create(toolset.ElementInformation.Source, toolset.ElementInformation.LineNumber, 0); + + if (toolset.toolsVersion != null && toolset.toolsVersion.Length == 0) + { + InvalidToolsetDefinitionException.Throw("InvalidToolsetValueInConfigFileValue", location.LocationString); + } + + yield return new ToolsetPropertyDefinition(toolset.toolsVersion, string.Empty, location); + } + } + else + { + yield break; + } + } + } + + /// + /// Returns the default tools version, or null if none was specified + /// + protected override string DefaultToolsVersion + { + get + { + return (ConfigurationSection == null ? null : ConfigurationSection.Default); + } + } + + /// + /// Returns the path to find overridetasks, or null if none was specified + /// + protected override string MSBuildOverrideTasksPath + { + get + { + return (ConfigurationSection == null ? null : ConfigurationSection.MSBuildOverrideTasksPath); + } + } + + /// + /// DefaultOverrideToolsVersion attribute on msbuildToolsets element, specifying the toolsversion that should be used by + /// default to build projects with this version of MSBuild. + /// + protected override string DefaultOverrideToolsVersion + { + get + { + return (ConfigurationSection == null ? null : ConfigurationSection.DefaultOverrideToolsVersion); + } + } + + /// + /// Lazy getter for the ToolsetConfigurationSection + /// Returns null if the section is not present + /// + private ToolsetConfigurationSection ConfigurationSection + { + get + { + if (null == _configurationSection && !_configurationReadAttempted) + { + try + { + Configuration configuration = _readApplicationConfiguration(); + _configurationSection = ToolsetConfigurationReaderHelpers.ReadToolsetConfigurationSection(configuration); + } + catch (ConfigurationException ex) + { + // ConfigurationException is obsolete, but we catch it rather than + // ConfigurationErrorsException (which is what we throw below) because it is more + // general and we don't want to miss catching some other derived exception. + InvalidToolsetDefinitionException.Throw(ex, "ConfigFileReadError", ElementLocation.Create(ex.Source, ex.Line, 0).LocationString, ex.BareMessage); + } + finally + { + _configurationReadAttempted = true; + } + } + + return _configurationSection; + } + } + + /// + /// Provides an enumerator over property definitions for a specified tools version + /// + protected override IEnumerable GetPropertyDefinitions(string toolsVersion) + { + ToolsetElement toolsetElement = ConfigurationSection.Toolsets.GetElement(toolsVersion); + + if (toolsetElement == null) + { + yield break; + } + + foreach (ToolsetElement.PropertyElement propertyElement in toolsetElement.PropertyElements) + { + ElementLocation location = ElementLocation.Create(propertyElement.ElementInformation.Source, propertyElement.ElementInformation.LineNumber, 0); + + if (propertyElement.Name != null && propertyElement.Name.Length == 0) + { + InvalidToolsetDefinitionException.Throw("InvalidToolsetValueInConfigFileValue", location.LocationString); + } + + yield return new ToolsetPropertyDefinition(propertyElement.Name, propertyElement.Value, location); + } + } + + /// + /// Provides an enumerator over the set of sub-toolset names available to a particular + /// toolsversion. MSBuild config files do not currently support sub-toolsets, so + /// we return nothing. + /// + /// The tools version. + /// An enumeration of the sub-toolsets that belong to that toolsversion. + protected override IEnumerable GetSubToolsetVersions(string toolsVersion) + { + yield break; + } + + /// + /// Provides an enumerator over property definitions for a specified sub-toolset version + /// under a specified toolset version. In the ToolsetConfigurationReader case, breaks + /// immediately because we do not currently support sub-toolsets in the configuration file. + /// + /// The tools version. + /// The sub-toolset version. + /// An enumeration of property definitions. + protected override IEnumerable GetSubToolsetPropertyDefinitions(string toolsVersion, string subToolsetVersion) + { + yield break; + } + + /// + /// Reads the application configuration file. + /// NOTE: this is abstracted into a method to support unit testing GetToolsetDataFromConfiguration(). + /// Unit tests wish to avoid reading (nunit.exe) application configuration file. + /// + private static Configuration ReadApplicationConfiguration() + { + return ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Definition/ToolsetPropertyDefinition.cs b/src/XMakeBuildEngine/Definition/ToolsetPropertyDefinition.cs new file mode 100644 index 00000000000..e3ace3d9981 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ToolsetPropertyDefinition.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A property class used internally by the Toolset readers. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// A class representing a property. Used internally by the toolset readers. + /// + [DebuggerDisplay("Name={Name} Value={Value}")] + internal class ToolsetPropertyDefinition + { + /// + /// The property name + /// + private string _name; + + /// + /// The property value + /// + private string _value; + + /// + /// The property source + /// + private IElementLocation _source; + + /// + /// Creates a new property + /// + /// The property name + /// The property value + /// The property source + public ToolsetPropertyDefinition(string name, string value, IElementLocation source) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + ErrorUtilities.VerifyThrowArgumentNull(source, "source"); + + // value can be the empty string but not null + ErrorUtilities.VerifyThrowArgumentNull(value, "value"); + + _name = name; + _value = value; + _source = source; + } + + /// + /// The name of the property + /// + public string Name + { + get + { + return _name; + } + } + + /// + /// The value of the property + /// + public string Value + { + get + { + return _value; + } + + set + { + ErrorUtilities.VerifyThrowInternalNull(value, "Value"); + _value = value; + } + } + + /// + /// A description of the location where the property was defined, + /// such as a registry key path or a path to a config file and + /// line number. + /// + public IElementLocation Source + { + get + { + return _source; + } + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ToolsetReader.cs b/src/XMakeBuildEngine/Definition/ToolsetReader.cs new file mode 100644 index 00000000000..b7920eb041e --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ToolsetReader.cs @@ -0,0 +1,533 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Abstract base class for objects reading toolsets. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Generic; + +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Win32; + +using error = Microsoft.Build.Shared.ErrorUtilities; +using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Evaluation +{ + /// + /// The abstract base class for all Toolset readers. + /// + internal abstract class ToolsetReader + { + /// + /// The global properties used to read the toolset. + /// + private PropertyDictionary _globalProperties; + + /// + /// The environment properties used to read the toolset. + /// + private PropertyDictionary _environmentProperties; + + /// + /// Constructor + /// + protected ToolsetReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties) + { + _environmentProperties = environmentProperties; + _globalProperties = globalProperties; + } + + /// + /// Returns the list of tools versions + /// + protected abstract IEnumerable ToolsVersions + { + get; + } + + /// + /// Returns the default tools version, or null if none was specified + /// + protected abstract string DefaultToolsVersion + { + get; + } + + /// + /// Returns the path to find override tasks, or null if none was specified + /// + protected abstract string MSBuildOverrideTasksPath + { + get; + } + + /// + /// ToolsVersion to use as the default ToolsVersion for this version of MSBuild + /// + protected abstract string DefaultOverrideToolsVersion + { + get; + } + + /// + /// Gathers toolset data from the registry and configuration file, if any: + /// allows you to specify which of the registry and configuration file to + /// read from by providing ToolsetInitialization + /// + internal static string ReadAllToolsets(Dictionary toolsets, PropertyDictionary environmentProperties, PropertyDictionary globalProperties, ToolsetDefinitionLocations locations) + { + return ReadAllToolsets(toolsets, null, null, environmentProperties, globalProperties, locations); + } + + /// + /// Gathers toolset data from the registry and configuration file, if any. + /// NOTE: this method is internal for unit testing purposes only. + /// + internal static string ReadAllToolsets + ( + Dictionary toolsets, + ToolsetRegistryReader registryReader, + ToolsetConfigurationReader configurationReader, + PropertyDictionary environmentProperties, + PropertyDictionary globalProperties, + ToolsetDefinitionLocations locations + ) + { + PropertyDictionary initialProperties = new PropertyDictionary(environmentProperties); + + initialProperties.ImportProperties(globalProperties); + + // The ordering here is important because the configuration file should have greater precedence + // than the registry, and we do a check and don't read in the new toolset if there's already one. + string defaultToolsVersionFromConfiguration = null; + string overrideTasksPathFromConfiguration = null; + string defaultOverrideToolsVersionFromConfiguration = null; + + ToolsetConfigurationReader configurationReaderToUse = null; + if ((locations & ToolsetDefinitionLocations.ConfigurationFile) == ToolsetDefinitionLocations.ConfigurationFile) + { + if (configurationReader == null && ToolsetConfigurationReaderHelpers.ConfigurationFileMayHaveToolsets()) + { + // We haven't been passed in a fake configuration reader by a unit test, + // and it looks like we have a .config file to read, so create a real + // configuration reader + configurationReader = new ToolsetConfigurationReader(environmentProperties, globalProperties); + } + + if (configurationReader != null) + { + configurationReaderToUse = configurationReader == null ? new ToolsetConfigurationReader(environmentProperties, globalProperties) : configurationReader; + + // Accumulation of properties is okay in the config file because it's deterministically ordered + defaultToolsVersionFromConfiguration = + configurationReaderToUse.ReadToolsets(toolsets, globalProperties, initialProperties, true /* accumulate properties */, out overrideTasksPathFromConfiguration, out defaultOverrideToolsVersionFromConfiguration); + } + } + + string defaultToolsVersionFromRegistry = null; + string overrideTasksPathFromRegistry = null; + string defaultOverrideToolsVersionFromRegistry = null; + + ToolsetRegistryReader registryReaderToUse = null; + if ((locations & ToolsetDefinitionLocations.Registry) == ToolsetDefinitionLocations.Registry) + { + registryReaderToUse = registryReader == null ? new ToolsetRegistryReader(environmentProperties, globalProperties) : registryReader; + + // We do not accumulate properties when reading them from the registry, because the order + // in which values are returned to us is essentially random: so we disallow one property + // in the registry to refer to another also in the registry + defaultToolsVersionFromRegistry = + registryReaderToUse.ReadToolsets(toolsets, globalProperties, initialProperties, false /* do not accumulate properties */, out overrideTasksPathFromRegistry, out defaultOverrideToolsVersionFromRegistry); + } + + // The 2.0 .NET Framework installer did not write a ToolsVersion key for itself in the registry. + // The 3.5 installer writes one for 2.0, but 3.5 might not be installed. + // The 4.0 and subsequent installers can't keep writing the 2.0 one, because (a) it causes SxS issues and (b) we + // don't want it unless 2.0 is installed. + // So if the 2.0 framework is actually installed, we're reading the registry, and either the registry or the config + // file have not already created the 2.0 toolset, mock up a fake one. + if ( + ((locations & ToolsetDefinitionLocations.Registry) != 0) && + !toolsets.ContainsKey("2.0") && + FrameworkLocationHelper.PathToDotNetFrameworkV20 != null + ) + { + Toolset synthetic20Toolset = new Toolset("2.0", FrameworkLocationHelper.PathToDotNetFrameworkV20, environmentProperties, globalProperties, null /* 2.0 did not have override tasks */, null /* 2.0 did not have a default override toolsversion */); + toolsets.Add("2.0", synthetic20Toolset); + } + + // We'll use the path from the configuration file if it was specified, otherwise we'll try + // the one from the registry. It's possible (and valid) that neither the configuration file + // nor the registry specify a override in which case we'll just return null. + string overrideTasksPath = overrideTasksPathFromConfiguration ?? overrideTasksPathFromRegistry; + + // We'll use the path from the configuration file if it was specified, otherwise we'll try + // the one from the registry. It's possible (and valid) that neither the configuration file + // nor the registry specify a override in which case we'll just return null. + string defaultOverrideToolsVersion = defaultOverrideToolsVersionFromConfiguration ?? defaultOverrideToolsVersionFromRegistry; + + // We'll use the default from the configuration file if it was specified, otherwise we'll try + // the one from the registry. It's possible (and valid) that neither the configuration file + // nor the registry specify a default, in which case we'll just return null. + string defaultToolsVersion = defaultToolsVersionFromConfiguration ?? defaultToolsVersionFromRegistry; + + // If we got a default version from the registry or config file, and it + // actually exists, fine. + // Otherwise we have to come up with one. + if (defaultToolsVersion == null || !toolsets.ContainsKey(defaultToolsVersion)) + { + // We're going to choose a hard coded default tools version of 2.0. + defaultToolsVersion = Constants.defaultToolsVersion; + + // But don't overwrite any existing tools path for this default we're choosing. + if (!toolsets.ContainsKey(Constants.defaultToolsVersion)) + { + // There's no tools path already for 2.0, so use the path to the v2.0 .NET Framework. + // If an old-fashioned caller sets BinPath property, or passed a BinPath to the constructor, + // that will overwrite what we're setting here. + ErrorUtilities.VerifyThrow(Constants.defaultToolsVersion == "2.0", "Getting 2.0 FX path so default should be 2.0"); + string pathToFramework = FrameworkLocationHelper.PathToDotNetFrameworkV20; + + // We could not find the default toolsversion because it was not installed on the machine. Fallback to the + // one we expect to always be there when running msbuild 4.0. + if (pathToFramework == null) + { + pathToFramework = FrameworkLocationHelper.PathToDotNetFrameworkV40; + defaultToolsVersion = Constants.defaultFallbackToolsVersion; + } + + // Again don't overwrite any existing tools path for this default we're choosing. + if (!toolsets.ContainsKey(defaultToolsVersion)) + { + Toolset defaultToolset = new Toolset(defaultToolsVersion, pathToFramework, environmentProperties, globalProperties, overrideTasksPath, defaultOverrideToolsVersion); + toolsets.Add(defaultToolsVersion, defaultToolset); + } + } + } + + return defaultToolsVersion; + } + + /// + /// Populates the toolset collection passed in with the toolsets read from some location. + /// + /// Internal for unit testing only + /// the default tools version if available, or null otherwise + internal string ReadToolsets + ( + Dictionary toolsets, + PropertyDictionary globalProperties, + PropertyDictionary initialProperties, + bool accumulateProperties, + out string msBuildOverrideTasksPath, + out string defaultOverrideToolsVersion + ) + { + error.VerifyThrowArgumentNull(toolsets, "Toolsets"); + + ReadEachToolset(toolsets, globalProperties, initialProperties, accumulateProperties); + + string defaultToolsVersion = DefaultToolsVersion; + + msBuildOverrideTasksPath = MSBuildOverrideTasksPath; + + defaultOverrideToolsVersion = DefaultOverrideToolsVersion; + + // We don't check whether the default tools version actually + // corresponds to a toolset definition. That's because our default for + // the indefinite future is 2.0, and 2.0 might not be installed, which is fine. + // If a project tries to use 2.0 (or whatever the default is) in these circumstances + // they'll get a nice error saying that toolset isn't available and listing those that are. + return defaultToolsVersion; + } + + /// + /// Provides an enumerator over property definitions for a specified tools version + /// + /// The tools version. + /// An enumeration of property definitions. + protected abstract IEnumerable GetPropertyDefinitions(string toolsVersion); + + /// + /// Provides an enumerator over the set of sub-toolset names available to a particular + /// toolsversion + /// + /// The tools version. + /// An enumeration of the sub-toolsets that belong to that toolsversion. + protected abstract IEnumerable GetSubToolsetVersions(string toolsVersion); + + /// + /// Provides an enumerator over property definitions for a specified sub-toolset version + /// under a specified toolset version. + /// + /// The tools version. + /// The sub-toolset version. + /// An enumeration of property definitions. + protected abstract IEnumerable GetSubToolsetPropertyDefinitions(string toolsVersion, string subToolsetVersion); + + /// + /// Reads all the toolsets and populates the given ToolsetCollection with them + /// + private void ReadEachToolset + ( + Dictionary toolsets, + PropertyDictionary globalProperties, + PropertyDictionary initialProperties, + bool accumulateProperties + ) + { + foreach (ToolsetPropertyDefinition toolsVersion in ToolsVersions) + { + // If there's already an existing toolset, it's of higher precedence, so + // don't even bother to read this toolset in. + if (!toolsets.ContainsKey(toolsVersion.Name)) + { + // We clone here because we don't want to interfere with the evaluation + // of subsequent Toolsets; otherwise, properties found during the evaluation + // of this Toolset would be persisted in initialProperties and appear + // to later Toolsets as Global or Environment properties from the Engine. + PropertyDictionary initialPropertiesClone = new PropertyDictionary(initialProperties); + + Toolset toolset = ReadToolset(toolsVersion, globalProperties, initialPropertiesClone, accumulateProperties); + + if (toolset != null) + { + toolsets[toolset.ToolsVersion] = toolset; + } + } + } + } + + /// + /// Reads the settings for a specified tools version + /// + private Toolset ReadToolset + ( + ToolsetPropertyDefinition toolsVersion, + PropertyDictionary globalProperties, + PropertyDictionary initialProperties, + bool accumulateProperties + ) + { + // Initial properties is the set of properties we're going to use to expand property expressions like $(foo) + // in the values we read out of the registry or config file. We'll add to it as we pick up properties (including binpath) + // from the registry or config file, so that properties there can be referenced in values below them. + // After processing all the properties, we don't need initialProperties anymore. + string toolsPath = null; + string binPath = null; + PropertyDictionary properties = new PropertyDictionary(); + + IEnumerable rawProperties = GetPropertyDefinitions(toolsVersion.Name); + + Expander expander = new Expander(initialProperties); + + foreach (ToolsetPropertyDefinition property in rawProperties) + { + EvaluateAndSetProperty(property, properties, globalProperties, initialProperties, accumulateProperties, ref toolsPath, ref binPath, ref expander); + } + + Dictionary subToolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + IEnumerable subToolsetVersions = GetSubToolsetVersions(toolsVersion.Name); + + foreach (string subToolsetVersion in subToolsetVersions) + { + string subToolsetToolsPath = null; + string subToolsetBinPath = null; + IEnumerable rawSubToolsetProperties = GetSubToolsetPropertyDefinitions(toolsVersion.Name, subToolsetVersion); + PropertyDictionary subToolsetProperties = new PropertyDictionary(); + + // If we have a sub-toolset, any values defined here will override the toolset properties. + foreach (ToolsetPropertyDefinition property in rawSubToolsetProperties) + { + EvaluateAndSetProperty(property, subToolsetProperties, globalProperties, initialProperties, false /* do not ever accumulate sub-toolset properties */, ref subToolsetToolsPath, ref subToolsetBinPath, ref expander); + } + + if (subToolsetToolsPath != null || subToolsetBinPath != null) + { + InvalidToolsetDefinitionException.Throw("MSBuildToolsPathNotSupportedInSubToolsets", toolsVersion.Name, toolsVersion.Source.LocationString, subToolsetVersion); + } + + subToolsets[subToolsetVersion] = new SubToolset(subToolsetVersion, subToolsetProperties); + } + + // All tools versions must specify a value for MSBuildToolsPath (or MSBuildBinPath) + if (String.IsNullOrEmpty(toolsPath) && String.IsNullOrEmpty(binPath)) + { + return null; + } + + // If both MSBuildBinPath and MSBuildToolsPath are present, they must be the same + if (toolsPath != null && binPath != null && !toolsPath.Equals(binPath, StringComparison.OrdinalIgnoreCase)) + { + InvalidToolsetDefinitionException.Throw("ConflictingValuesOfMSBuildToolsPath", toolsVersion.Name, toolsVersion.Source.LocationString); + } + + Toolset toolset = null; + + try + { + toolset = new Toolset(toolsVersion.Name, toolsPath == null ? binPath : toolsPath, properties, _environmentProperties, globalProperties, subToolsets, MSBuildOverrideTasksPath, DefaultOverrideToolsVersion); + } + catch (ArgumentException e) + { + InvalidToolsetDefinitionException.Throw("ErrorCreatingToolset", toolsVersion.Name, e.Message); + } + + return toolset; + } + + /// + /// Processes a particular ToolsetPropertyDefinition into the correct value and location in the initial and/or final property set. + /// + /// The ToolsetPropertyDefinition being analyzed. + /// The final set of properties that we wish this toolset property to be added to. + /// The global properties, used for expansion and to make sure none are overridden. + /// The initial properties, used for expansion and added to if "accumulateProperties" is true. + /// If "true", we add this property to the initialProperties dictionary, as well, so that properties later in the toolset can use this value. + /// If this toolset property is the "MSBuildToolsPath" property, we will return the value in this parameter. + /// If this toolset property is the "MSBuildBinPath" property, we will return the value in this parameter. + /// The expander used to expand the value of the properties. Ref because if we are accumulating the properties, we need to re-create the expander to account for the new property value. + private void EvaluateAndSetProperty(ToolsetPropertyDefinition property, PropertyDictionary properties, PropertyDictionary globalProperties, PropertyDictionary initialProperties, bool accumulateProperties, ref string toolsPath, ref string binPath, ref Expander expander) + { + if (0 == String.Compare(property.Name, ReservedPropertyNames.toolsPath, StringComparison.OrdinalIgnoreCase)) + { + toolsPath = ExpandPropertyLeaveEscaped(property, expander); + toolsPath = ExpandRelativePathsRelativeToExeLocation(toolsPath); + + if (accumulateProperties) + { + SetProperty + ( + new ToolsetPropertyDefinition(ReservedPropertyNames.toolsPath, toolsPath, property.Source), + initialProperties, + globalProperties + ); + } + } + else if (0 == String.Compare(property.Name, ReservedPropertyNames.binPath, StringComparison.OrdinalIgnoreCase)) + { + binPath = ExpandPropertyLeaveEscaped(property, expander); + binPath = ExpandRelativePathsRelativeToExeLocation(binPath); + + if (accumulateProperties) + { + SetProperty + ( + new ToolsetPropertyDefinition(ReservedPropertyNames.binPath, binPath, property.Source), + initialProperties, + globalProperties + ); + } + } + else if (ReservedPropertyNames.IsReservedProperty(property.Name)) + { + // We don't allow toolsets to define reserved properties + string baseMessage = ResourceUtilities.FormatResourceString("CannotModifyReservedProperty", property.Name); + InvalidToolsetDefinitionException.Throw("InvalidPropertyNameInToolset", property.Name, property.Source.LocationString, baseMessage); + } + else + { + // It's an arbitrary property + property.Value = ExpandPropertyLeaveEscaped(property, expander); + + SetProperty(property, properties, globalProperties); + + if (accumulateProperties) + { + SetProperty(property, initialProperties, globalProperties); + } + } + + if (accumulateProperties) + { + expander = new Expander(initialProperties); + } + } + + /// + /// Expands the given unexpanded property expression using the properties in the + /// given expander. + /// + private string ExpandPropertyLeaveEscaped(ToolsetPropertyDefinition property, Expander expander) + { + try + { + return expander.ExpandIntoStringLeaveEscaped(property.Value, ExpanderOptions.ExpandProperties, property.Source); + } + catch (InvalidProjectFileException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "ErrorEvaluatingToolsetPropertyExpression", property.Value, property.Source.LocationString, ex.BaseMessage); + } + + return string.Empty; + } + + /// + /// Sets the given property in the given property group. + /// + private void SetProperty(ToolsetPropertyDefinition property, PropertyDictionary propertyGroup, PropertyDictionary globalProperties) + { + try + { + // Global properties cannot be overwritten + if (globalProperties[property.Name] == null) + { + propertyGroup.Set(ProjectPropertyInstance.Create(property.Name, EscapingUtilities.UnescapeAll(property.Value), true /* may be reserved */, false /* not immutable */)); + } + } + catch (ArgumentException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "InvalidPropertyNameInToolset", property.Name, property.Source.LocationString, ex.Message); + } + } + + /// + /// Given a path, de-relativizes it using the location of the currently + /// executing .exe as the base directory. For example, the path "..\foo" + /// becomes "c:\windows\microsoft.net\framework\foo" if the current exe is + /// "c:\windows\microsoft.net\framework\v3.5.1234\msbuild.exe". + /// If the path is not relative, it is returned without modification. + /// If the path is invalid, it is returned without modification. + /// + private string ExpandRelativePathsRelativeToExeLocation(string path) + { + try + { + // Trim, because we don't want to do anything with empty values + // (those should cause an error) + string trimmedValue = path.Trim(); + if (trimmedValue.Length > 0 && !Path.IsPathRooted(trimmedValue)) + { + path = Path.GetFullPath( + Path.Combine(FileUtilities.CurrentExecutableDirectory, trimmedValue)); + } + } + catch (Exception e) + { + // Catching Exception, but rethrowing unless it's an IO related exception. + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + // This means that the path looked relative, but was an invalid path. In this case, we'll + // just not expand it, and carry on - to be consistent with what happens when there's a + // non-relative bin path with invalid characters. The problem will be detected later when + // it's used in a project file. + } + + return path; + } + } +} diff --git a/src/XMakeBuildEngine/Definition/ToolsetRegistryReader.cs b/src/XMakeBuildEngine/Definition/ToolsetRegistryReader.cs new file mode 100644 index 00000000000..eeb7e9c27d4 --- /dev/null +++ b/src/XMakeBuildEngine/Definition/ToolsetRegistryReader.cs @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Object which reads toolset information from the registry. +//----------------------------------------------------------------------- + +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; +using System.Security; + +using Microsoft.Build.Shared; +using error = Microsoft.Build.Shared.ErrorUtilities; +using RegistryKeyWrapper = Microsoft.Build.Internal.RegistryKeyWrapper; +using RegistryException = Microsoft.Build.Exceptions.RegistryException; +using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; +using Constants = Microsoft.Build.Internal.Constants; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Reads registry at the base key and returns a Dictionary keyed on ToolsVersion. + /// Dictionary contains another dictionary of (property name, property value) pairs. + /// If a registry value is not a string, this will throw a InvalidToolsetDefinitionException. + /// An example of how the registry will look (note that the DefaultToolsVersion is per-MSBuild-version) + /// [HKLM]\SOFTWARE\Microsoft + /// msbuild + /// 3.5 + /// @DefaultToolsVersion = 2.0 + /// ToolsVersions + /// 2.0 + /// @MSBuildToolsPath = D:\SomeFolder + /// 3.5 + /// @MSBuildToolsPath = D:\SomeOtherFolder + /// @MSBuildBinPath = D:\SomeOtherFolder + /// @SomePropertyName = PropertyOtherValue + /// + internal class ToolsetRegistryReader : ToolsetReader + { + /// + /// Registry location for storing tools version dependent data for msbuild + /// + private const string MSBuildRegistryPath = @"SOFTWARE\Microsoft\MSBuild"; + + /// + /// Cached registry wrapper at root of the msbuild entries + /// + private RegistryKeyWrapper _msbuildRegistryWrapper; + + /// + /// Default constructor + /// + internal ToolsetRegistryReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties) + : this(environmentProperties, globalProperties, new RegistryKeyWrapper(MSBuildRegistryPath)) + { + } + + /// + /// Constructor overload accepting a registry wrapper for unit testing purposes only + /// + internal ToolsetRegistryReader(PropertyDictionary environmentProperties, PropertyDictionary globalProperties, RegistryKeyWrapper msbuildRegistryWrapper) + : base(environmentProperties, globalProperties) + { + error.VerifyThrowArgumentNull(msbuildRegistryWrapper, "msbuildRegistryWrapper"); + + _msbuildRegistryWrapper = msbuildRegistryWrapper; + } + + /// + /// Returns the list of tools versions + /// + protected override IEnumerable ToolsVersions + { + get + { + string[] toolsVersionNames = new string[] { }; + try + { + RegistryKeyWrapper subKey = null; + using (subKey = _msbuildRegistryWrapper.OpenSubKey("ToolsVersions")) + { + toolsVersionNames = subKey.GetSubKeyNames(); + } + } + catch (RegistryException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "RegistryReadError", ex.Source, ex.Message); + } + + foreach (string toolsVersionName in toolsVersionNames) + { + // For the purposes of error location, use the registry path instead of a file name + IElementLocation location = new RegistryLocation(_msbuildRegistryWrapper.Name + "\\ToolsVersions\\" + toolsVersionName); + + yield return new ToolsetPropertyDefinition(toolsVersionName, string.Empty, location); + } + } + } + + /// + /// Returns the default tools version, or null if none was specified + /// + protected override string DefaultToolsVersion + { + get + { + string defaultToolsVersion = null; + + // We expect to find the DefaultToolsVersion value under a registry key named for our + // version, e.g., "3.5" + using (RegistryKeyWrapper defaultToolsVersionKey = _msbuildRegistryWrapper.OpenSubKey(Constants.AssemblyVersion)) + { + if (defaultToolsVersionKey != null) + { + defaultToolsVersion = GetValue(defaultToolsVersionKey, "DefaultToolsVersion"); + } + } + + return defaultToolsVersion; + } + } + + /// + /// Returns the path to find override tasks, or null if none was specified + /// + protected override string MSBuildOverrideTasksPath + { + get + { + string defaultToolsVersion = null; + + // We expect to find the MsBuildOverrideTasksPath value under a registry key named for our + // version, e.g., "4.0" + using (RegistryKeyWrapper defaultToolsVersionKey = _msbuildRegistryWrapper.OpenSubKey(Constants.AssemblyVersion)) + { + if (defaultToolsVersionKey != null) + { + defaultToolsVersion = GetValue(defaultToolsVersionKey, ReservedPropertyNames.overrideTasksPath); + } + } + + return defaultToolsVersion; + } + } + + /// + /// ToolsVersion to use as the default ToolsVersion for this version of MSBuild + /// + protected override string DefaultOverrideToolsVersion + { + get + { + string defaultOverrideToolsVersion = null; + + // We expect to find the MsBuildOverrideTasksPath value under a registry key named for our + // version, e.g., "12.0" + using (RegistryKeyWrapper defaultOverrideToolsVersionKey = _msbuildRegistryWrapper.OpenSubKey(Constants.AssemblyVersion)) + { + if (defaultOverrideToolsVersionKey != null) + { + defaultOverrideToolsVersion = GetValue(defaultOverrideToolsVersionKey, ReservedPropertyNames.defaultOverrideToolsVersion); + } + } + + return defaultOverrideToolsVersion; + } + } + + /// + /// Provides an enumerator over property definitions for a specified tools version + /// + /// The tools version + /// An enumeration of property definitions + protected override IEnumerable GetPropertyDefinitions(string toolsVersion) + { + RegistryKeyWrapper toolsVersionWrapper = null; + try + { + try + { + toolsVersionWrapper = _msbuildRegistryWrapper.OpenSubKey("ToolsVersions\\" + toolsVersion); + } + catch (RegistryException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "RegistryReadError", ex.Source, ex.Message); + } + + foreach (string propertyName in toolsVersionWrapper.GetValueNames()) + { + yield return CreatePropertyFromRegistry(toolsVersionWrapper, propertyName); + } + } + finally + { + if (toolsVersionWrapper != null) + { + toolsVersionWrapper.Dispose(); + } + } + } + + /// + /// Provides an enumerator over the set of sub-toolset names available to a particular + /// toolsversion + /// + /// The tools version. + /// An enumeration of the sub-toolsets that belong to that toolsversion. + protected override IEnumerable GetSubToolsetVersions(string toolsVersion) + { + RegistryKeyWrapper toolsVersionWrapper = null; + try + { + try + { + toolsVersionWrapper = _msbuildRegistryWrapper.OpenSubKey("ToolsVersions\\" + toolsVersion); + } + catch (RegistryException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "RegistryReadError", ex.Source, ex.Message); + } + + return toolsVersionWrapper.GetSubKeyNames(); + } + finally + { + if (toolsVersionWrapper != null) + { + toolsVersionWrapper.Dispose(); + } + } + } + + /// + /// Provides an enumerator over property definitions for a specified sub-toolset version + /// under a specified toolset version. + /// + /// The tools version. + /// The sub-toolset version. + /// An enumeration of property definitions. + protected override IEnumerable GetSubToolsetPropertyDefinitions(string toolsVersion, string subToolsetVersion) + { + ErrorUtilities.VerifyThrowArgumentLength(subToolsetVersion, "subToolsetVersion"); + + RegistryKeyWrapper toolsVersionWrapper = null; + RegistryKeyWrapper subToolsetWrapper = null; + + try + { + try + { + toolsVersionWrapper = _msbuildRegistryWrapper.OpenSubKey("ToolsVersions\\" + toolsVersion); + } + catch (RegistryException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "RegistryReadError", ex.Source, ex.Message); + } + + try + { + subToolsetWrapper = toolsVersionWrapper.OpenSubKey(subToolsetVersion); + } + catch (RegistryException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "RegistryReadError", ex.Source, ex.Message); + } + + foreach (string propertyName in subToolsetWrapper.GetValueNames()) + { + yield return CreatePropertyFromRegistry(subToolsetWrapper, propertyName); + } + } + finally + { + if (toolsVersionWrapper != null) + { + toolsVersionWrapper.Dispose(); + } + + if (subToolsetWrapper != null) + { + subToolsetWrapper.Dispose(); + } + } + } + + /// + /// Given a registry location containing a property name and value, create the ToolsetPropertyDefinition that maps to it + /// + /// Wrapper for the key that we're getting values from + /// The name of the property whose value we wish to generate a ToolsetPropertyDefinition for. + /// A ToolsetPropertyDefinition instance corresponding to the property name requested. + private static ToolsetPropertyDefinition CreatePropertyFromRegistry(RegistryKeyWrapper toolsetWrapper, string propertyName) + { + string propertyValue = null; + + if (propertyName != null && propertyName.Length == 0) + { + InvalidToolsetDefinitionException.Throw("PropertyNameInRegistryHasZeroLength", toolsetWrapper.Name); + } + + try + { + propertyValue = GetValue(toolsetWrapper, propertyName); + } + catch (RegistryException ex) + { + InvalidToolsetDefinitionException.Throw(ex, "RegistryReadError", ex.Source, ex.Message); + } + + // For the purposes of error location, use the registry path instead of a file name + IElementLocation location = new RegistryLocation(toolsetWrapper.Name + "@" + propertyName); + + return new ToolsetPropertyDefinition(propertyName, propertyValue, location); + } + + /// + /// Reads a string value from the specified registry key + /// + /// wrapper around key + /// name of the value + /// string data in the value + private static string GetValue(RegistryKeyWrapper wrapper, string valueName) + { + if (wrapper.Exists()) + { + object result = wrapper.GetValue(valueName); + + // RegistryKey.GetValue returns null if the value is not present + // and String.Empty if the value is present and no data is defined. + // We preserve this distinction, because a string property in the registry with + // no value really has an empty string for a value (which is a valid property value) + // rather than null for a value (which is an invalid property value) + if (result != null) + { + // Must be a value of string type + if (!(result is string)) + { + InvalidToolsetDefinitionException.Throw("NonStringDataInRegistry", wrapper.Name + "@" + valueName); + } + + return result.ToString(); + } + } + + return null; + } + } +} diff --git a/src/XMakeBuildEngine/ElementLocation/ElementLocation.cs b/src/XMakeBuildEngine/ElementLocation/ElementLocation.cs new file mode 100644 index 00000000000..1e1cfb6cd97 --- /dev/null +++ b/src/XMakeBuildEngine/ElementLocation/ElementLocation.cs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The location of an XML node in a file +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using System; +using System.Diagnostics; + +namespace Microsoft.Build.Construction +{ + /// + /// The location of an XML node in a file. + /// Any editing of the project XML through the MSBuild API's will invalidate locations in that XML until the XML is reloaded. + /// + /// + /// This object is IMMUTABLE, so that it can be passed around arbitrarily. + /// DO NOT make these objects any larger. There are huge numbers of them and they are transmitted between nodes. + /// + [Serializable] + public abstract class ElementLocation : IElementLocation, INodePacketTranslatable, IImmutable + { + /// + /// The singleton empty element location. + /// + private static ElementLocation s_emptyElementLocation = new SmallElementLocation(null, 0, 0); + + /// + /// The file from which this particular element originated. It may + /// differ from the ProjectFile if, for instance, it was part of + /// an import or originated in a targets file. + /// If not known, returns empty string. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public abstract string File + { + get; + } + + /// + /// The line number where this element exists in its file. + /// The first line is numbered 1. + /// Zero indicates "unknown location". + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public abstract int Line + { + get; + } + + /// + /// The column number where this element exists in its file. + /// The first column is numbered 1. + /// Zero indicates "unknown location". + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public abstract int Column + { + get; + } + + /// + /// The location in a form suitable for replacement + /// into a message. + /// Example: "c:\foo\bar.csproj (12,34)" + /// Calling this creates and formats a new string. + /// PREFER TO PUT THE LOCATION INFORMATION AT THE START OF THE MESSAGE INSTEAD. + /// Only in rare cases should the location go within the message itself. + /// + public string LocationString + { + get { return GetLocationString(File, Line, Column); } + } + + /// + /// Gets the empty element location. + /// This is not to be used when something is "missing": that should have a null location. + /// It is to be used for the project location when the project has not been given a name. + /// In that case, it exists, but can't have a specific location. + /// + internal static ElementLocation EmptyLocation + { + get { return s_emptyElementLocation; } + } + + /// + /// Get reasonable hash code. + /// + public override int GetHashCode() + { + // Line and column are good enough + return Line.GetHashCode() ^ Column.GetHashCode(); + } + + /// + /// Override Equals so that identical + /// fields imply equal objects. + /// + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + IElementLocation that = obj as IElementLocation; + + if (that == null) + { + return false; + } + + if (this.Line != that.Line || this.Column != that.Column) + { + return false; + } + + if (!String.Equals(this.File, that.File, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + /// + /// Location of element. + /// + public override string ToString() + { + return LocationString; + } + + /// + /// Writes the packet to the serializer. + /// Always send as ints, even if ushorts are being used: otherwise it'd + /// need a byte to discriminate and the savings would be microscopic. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + + string file = File; + int line = Line; + int column = Column; + translator.Translate(ref file); + translator.Translate(ref line); + translator.Translate(ref column); + } + + /// + /// Factory for serialization. + /// Custom factory is needed because this class is abstract and uses a factory pattern. + /// + internal static IElementLocation FactoryForDeserialization(INodePacketTranslator translator) + { + string file = null; + int line = 0; + int column = 0; + translator.Translate(ref file); + translator.Translate(ref line); + translator.Translate(ref column); + + return Create(file, line, column); + } + + /// + /// Constructor for when we only know the file and nothing else. + /// This is the case when we are creating a new item, for example, and it has + /// not been evaluated from some XML. + /// + internal static ElementLocation Create(string file) + { + return Create(file, 0, 0); + } + + /// + /// Constructor for the case where we have most or all information. + /// Numerical values must be 1-based, non-negative; 0 indicates unknown + /// File may be null, indicating the file was not loaded from disk. + /// + /// + /// In AG there are 600 locations that have a file but zero line and column. + /// In theory yet another derived class could be made for these to save 4 bytes each. + /// + internal static ElementLocation Create(string file, int line, int column) + { + if (file == null && line == 0 && column == 0) + { + return EmptyLocation; + } + + if (line <= 65535 && column <= 65535) + { + return new ElementLocation.SmallElementLocation(file, line, column); + } + + return new ElementLocation.RegularElementLocation(file, line, column); + } + + /// + /// The location in a form suitable for replacement + /// into a message. + /// Example: "c:\foo\bar.csproj (12,34)" + /// Calling this creates and formats a new string. + /// PREFER TO PUT THE LOCATION INFORMATION AT THE START OF THE MESSAGE INSTEAD. + /// Only in rare cases should the location go within the message itself. + /// + private static string GetLocationString(string file, int line, int column) + { + string locationString = String.Empty; + if (line != 0 && column != 0) + { + locationString = ResourceUtilities.FormatResourceString("FileLocation", file, line, column); + } + else if (line != 0) + { + locationString = file + " (" + line + ")"; + } + else + { + locationString = file; + } + + return locationString; + } + + /// + /// Rarer variation for when the line and column won't each fit in a ushort. + /// + private class RegularElementLocation : ElementLocation + { + /// + /// The source file. + /// + private string _file; + + /// + /// The source line. + /// + private int _line; + + /// + /// The source column. + /// + private int _column; + + /// + /// Constructor for the case where we have most or all information. + /// Numerical values must be 1-based, non-negative; 0 indicates unknown + /// File may be null, indicating the file was not loaded from disk. + /// + internal RegularElementLocation(string file, int line, int column) + { + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(file, "file"); + ErrorUtilities.VerifyThrow(line > -1 && column > -1, "Use zero for unknown"); + + _file = file ?? String.Empty; + _line = line; + _column = column; + } + + /// + /// The file from which this particular element originated. It may + /// differ from the ProjectFile if, for instance, it was part of + /// an import or originated in a targets file. + /// If not known, returns empty string. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override string File + { + get { return _file; } + } + + /// + /// The line number where this element exists in its file. + /// The first line is numbered 1. + /// Zero indicates "unknown location". + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override int Line + { + get { return _line; } + } + + /// + /// The column number where this element exists in its file. + /// The first column is numbered 1. + /// Zero indicates "unknown location". + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override int Column + { + get { return _column; } + } + } + + /// + /// For when the line and column each fit in a short - under 65536 + /// (almost always will: microsoft.common.targets is less than 5000 lines long) + /// When loading Australian Government, for example, there are over 31,000 ElementLocation + /// objects so this saves 4 bytes each = 123KB + /// + /// A "very small" variation that used two bytes (or halves of a short) would fit about half of them + /// and save 4 more bytes each, but the CLR packs each field to 4 bytes, so it isn't actually any smaller. + /// + private class SmallElementLocation : ElementLocation + { + /// + /// The source file. + /// + private string _file; + + /// + /// The source line. + /// + private ushort _line; + + /// + /// The source column. + /// + private ushort _column; + + /// + /// Constructor for the case where we have most or all information. + /// Numerical values must be 1-based, non-negative; 0 indicates unknown + /// File may be null, indicating the file was not loaded from disk. + /// + internal SmallElementLocation(string file, int line, int column) + { + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(file, "file"); + ErrorUtilities.VerifyThrow(line > -1 && column > -1, "Use zero for unknown"); + ErrorUtilities.VerifyThrow(line <= 65535 && column <= 65535, "Use ElementLocation instead"); + + _file = file ?? String.Empty; + _line = Convert.ToUInt16(line); + _column = Convert.ToUInt16(column); + } + + /// + /// The file from which this particular element originated. It may + /// differ from the ProjectFile if, for instance, it was part of + /// an import or originated in a targets file. + /// If not known, returns empty string. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override string File + { + get { return _file; } + } + + /// + /// The line number where this element exists in its file. + /// The first line is numbered 1. + /// Zero indicates "unknown location". + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override int Line + { + get { return (int)_line; } + } + + /// + /// The column number where this element exists in its file. + /// The first column is numbered 1. + /// Zero indicates "unknown location". + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public override int Column + { + get { return (int)_column; } + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ElementLocation/RegistryLocation.cs b/src/XMakeBuildEngine/ElementLocation/RegistryLocation.cs new file mode 100644 index 00000000000..e8a00138a9e --- /dev/null +++ b/src/XMakeBuildEngine/ElementLocation/RegistryLocation.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The location something in the registry +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using System; + +namespace Microsoft.Build.Construction +{ + /// + /// The location something in the registry + /// + /// + /// This object is IMMUTABLE, so that it can be passed around arbitrarily. + /// This is not public because the current implementation only provides correct data for unedited projects. + /// DO NOT make it public without considering a solution to this problem. + /// + [Serializable] + internal class RegistryLocation : IElementLocation, INodePacketTranslatable + { + /// + /// The location. + /// + private string _registryPath; + + /// + /// Constructor taking the registry location. + /// + internal RegistryLocation(string registryPath) + { + ErrorUtilities.VerifyThrowInternalLength(registryPath, "registryPath"); + + _registryPath = registryPath; + } + + /// + /// Private constructor for deserialization + /// + private RegistryLocation(INodePacketTranslator translator) + { + Translate(translator); + } + + /// + /// Not relevant, returns empty string. + /// + public string File + { + get { return String.Empty; } + } + + /// + /// Not relevant, returns 0. + /// + public int Line + { + get { return 0; } + } + + /// + /// Not relevant, returns 0. + /// + public int Column + { + get { return 0; } + } + + /// + /// The location in a form suitable for replacement + /// into a message. + /// + public string LocationString + { + get { return _registryPath; } + } + + #region INodePacketTranslatable Members + + /// + /// Reads or writes the packet to the serializer. + /// + public void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _registryPath); + } + + /// + /// Factory for serialization. + /// + static internal RegistryLocation FactoryForDeserialization(INodePacketTranslator translator) + { + return new RegistryLocation(translator); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/ElementLocation/XmlAttributeWithLocation.cs b/src/XMakeBuildEngine/ElementLocation/XmlAttributeWithLocation.cs new file mode 100644 index 00000000000..7e1118bc69e --- /dev/null +++ b/src/XMakeBuildEngine/ElementLocation/XmlAttributeWithLocation.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Improvement to XmlAttribute that during load attaches location information to itself. +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using System; +using System.Xml; +using System.Diagnostics; + +namespace Microsoft.Build.Construction +{ + /// + /// Derivation of XmlAttribute to implement IXmlLineInfo + /// + internal class XmlAttributeWithLocation : XmlAttribute, IXmlLineInfo + { + /// + /// Line, column, file information + /// + private ElementLocation _elementLocation; + + /// + /// Constructor without location information + /// + public XmlAttributeWithLocation(string prefix, string localName, string namespaceURI, XmlDocument document) + : this(prefix, localName, namespaceURI, document, 0, 0) + { + } + + /// + /// Constructor with location information + /// + public XmlAttributeWithLocation(string prefix, string localName, string namespaceURI, XmlDocument document, int lineNumber, int columnNumber) + : base(prefix, localName, namespaceURI, document) + { + XmlDocumentWithLocation documentWithLocation = (XmlDocumentWithLocation)document; + + _elementLocation = ElementLocation.Create(documentWithLocation.FullPath, lineNumber, columnNumber); + } + + /// + /// Returns the line number if available, else 0. + /// IXmlLineInfo member. + /// + public int LineNumber + { + [DebuggerStepThrough] + get + { return Location.Line; } + } + + /// + /// Returns the column number if available, else 0. + /// IXmlLineInfo member. + /// + public int LinePosition + { + [DebuggerStepThrough] + get + { return Location.Column; } + } + + /// + /// Provides an ElementLocation for this attribute. + /// + /// + /// Should have at least the file name if the containing project has been given a file name, + /// even if it wasn't loaded from disk, or has been edited since. That's because we set that + /// path on our XmlDocumentWithLocation wrapper class. + /// + internal ElementLocation Location + { + get + { + // Caching the element location object saves significant memory + XmlDocumentWithLocation ownerDocumentWithLocation = (XmlDocumentWithLocation)OwnerDocument; + if (!String.Equals(_elementLocation.File, ownerDocumentWithLocation.FullPath, StringComparison.OrdinalIgnoreCase)) + { + _elementLocation = ElementLocation.Create(ownerDocumentWithLocation.FullPath, _elementLocation.Line, _elementLocation.Column); + } + + return _elementLocation; + } + } + + /// + /// Whether location is available. + /// IXmlLineInfo member. + /// + public bool HasLineInfo() + { + return Location.Line != 0; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ElementLocation/XmlDocumentWithLocation.cs b/src/XMakeBuildEngine/ElementLocation/XmlDocumentWithLocation.cs new file mode 100644 index 00000000000..761b7f02075 --- /dev/null +++ b/src/XMakeBuildEngine/ElementLocation/XmlDocumentWithLocation.cs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Improvement to XmlDocument that during load attaches location information to all elements and attributes. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Construction +{ + /// + /// Improvement to XmlDocument that during load attaches location information to all elements and attributes. + /// We don't need a real XmlDocument, as we are careful not to expose Xml types in our public API. + /// + /// + /// XmlDocument has many members, and this can't substitute for all of them. Location finding probably won't work if + /// certain XmlDocument members are used. So for extra robustness, this could wrap an XmlDocument instead, + /// and expose the small number of members that the MSBuild code actually uses. + /// + internal class XmlDocumentWithLocation : XmlDocument + { + /// + /// Used to cache strings used in attribute values and comments. + /// + private static ProjectStringCache s_globalStringCache = new ProjectStringCache(); + + /// + /// Used to cache tag names in loaded files. + /// + private static NameTable s_nameTable = new XmlNameTableThreadSafe(); + + /// + /// Whether we can selectively load as read-only (eg just when in program files directory) + /// + private static ReadOnlyLoadFlags s_readOnlyFlags; + + /// + /// Reader we've hooked + /// + private IXmlLineInfo _reader; + + /// + /// Path to the file loaded, if any, otherwise null. + /// Easier to intercept and store than to derive it from the XmlDocument.BaseUri property. + /// + private string _fullPath; + + /// + /// Local cache of strings for attribute values and comments. Used for testing. + /// + private ProjectStringCache _stringCache; + + /// + /// Whether we can expect to never save this file. + /// In such a case, we can discard as much as possible on load, like comments and whitespace. + /// + private bool? _loadAsReadOnly; + + /// + /// Constructor + /// + internal XmlDocumentWithLocation() + : base(s_nameTable) + { + } + + /// + /// Constructor + /// + internal XmlDocumentWithLocation(bool loadAsReadOnly) + : this() + { + _loadAsReadOnly = loadAsReadOnly; + } + + /// + /// Whether to load files read only + /// + private enum ReadOnlyLoadFlags + { + /// + /// Not determined + /// + Undefined, + + /// + /// Always load writeable + /// + LoadAllWriteable, + + /// + /// Always load read-only, to save memory + /// + LoadAllReadOnly, + + /// + /// Load read only selectively, Eg., just when file names begin with "Microsoft." + /// + LoadReadOnlyIfAppropriate + } + + /// + /// Path to the file loaded if any, otherwise null. + /// If the XmlDocument hasn't been loaded from a file, we wouldn't have a full path. + /// However the project might actually have been given a path - it might even have been saved. + /// In order to allow created elements to be able to provide a location with at least + /// that path, the setter here should be called when the project is given a path. + /// It may be set to null. + /// + internal string FullPath + { + get { return _fullPath; } + set { _fullPath = value; } + } + + /// + /// Sets or gets the string cache used by this XmlDocument. + /// + /// + /// When a particular instance has not been set will use the global string cache. The ability + /// to use a particular instance is useful for tests. + /// + internal ProjectStringCache StringCache + { + get { return _stringCache ?? s_globalStringCache; } + set { _stringCache = value; } + } + + /// + /// Loads from an XmlReader, intercepting the reader. + /// + /// + /// This method is called within XmlDocument by all other + /// Load(..) overloads, and by LoadXml(..), so however the client loads XML, + /// we will grab the reader. + /// + public override void Load(XmlReader reader) + { + if (reader.BaseURI.Length > 0) + { + DetermineWhetherToLoadReadOnly(new Uri(reader.BaseURI).LocalPath); + } + + // Set the line info source if it is available given the specific implementation of XmlReader + // we've been given. + _reader = reader as IXmlLineInfo; + + // This call results in calls to our CreateElement and CreateAttribute methods, + // which use this.reader within themselves. + base.Load(reader); + + // After load, the reader is no use for location information; it isn't updated when + // the document is edited. So null it out, so that elements and attributes created by subsequent + // editing don't have meaningless location information. + _reader = null; + } + + /// + /// Grab the path to the file, for use in our location information. + /// + public override void Load(string fullPath) + { + DetermineWhetherToLoadReadOnly(fullPath); + + _fullPath = fullPath; + + // For security purposes we need to disable DTD processing when loading an XML file + XmlReaderSettings rs = new XmlReaderSettings(); + rs.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader xmlReader = XmlReader.Create(fullPath, rs)) + { + this.Load(xmlReader); + } + } + + /// + /// Called during load, to add an element. + /// + /// + /// We create our own kind of element, that we can give location information to. + /// + public override XmlElement CreateElement(string prefix, string localName, string namespaceURI) + { + if (_reader != null) + { + return new XmlElementWithLocation(prefix, localName, namespaceURI, this, _reader.LineNumber, _reader.LinePosition); + } + + // Must be a subsequent edit; we can't provide location information + return new XmlElementWithLocation(prefix, localName, namespaceURI, this); + } + + /// + /// Called during load, to add an attribute. + /// + /// + /// We create our own kind of attribute, that we can give location information to. + /// + public override XmlAttribute CreateAttribute(string prefix, string localName, string namespaceURI) + { + if (_reader != null) + { + return new XmlAttributeWithLocation(prefix, localName, namespaceURI, this, _reader.LineNumber, _reader.LinePosition); + } + + // Must be a subsequent edit; we can't provide location information + return new XmlAttributeWithLocation(prefix, localName, namespaceURI, this); + } + + /// + /// Create a whitespace node. + /// Overridden to cache attribute values. + /// + public override XmlWhitespace CreateWhitespace(string text) + { + if (_loadAsReadOnly.HasValue && _loadAsReadOnly.Value) + { + text = String.Empty; + } + + string interned = StringCache.Add(text, this); + return base.CreateWhitespace(interned); + } + + /// + /// Create a whitespace node. The definition of "significant" whitespace is obscure + /// and does not include whitespace in text values in element content, which we always want to keep. + /// Overridden to cache attribute values. + /// + public override XmlSignificantWhitespace CreateSignificantWhitespace(string text) + { + if (_loadAsReadOnly.HasValue && _loadAsReadOnly.Value) + { + text = String.Empty; + } + + string interned = StringCache.Add(text, this); + return base.CreateSignificantWhitespace(interned); + } + + /// + /// Create a text node. + /// Overridden to cache attribute values. + /// + public override XmlText CreateTextNode(string text) + { + string textNode = StringCache.Add(text, this); + return base.CreateTextNode(textNode); + } + + /// + /// Create a comment node. + /// Overridden in order to cache comment strings. + /// + public override XmlComment CreateComment(string data) + { + if (_loadAsReadOnly.HasValue && _loadAsReadOnly.Value) + { + data = String.Empty; + } + + string interned = StringCache.Add(data, this); + return base.CreateComment(interned); + } + + /// + /// Override Save to verify file was not loaded as readonly + /// + public override void Save(Stream outStream) + { + VerifyThrowNotReadOnly(); + base.Save(outStream); + } + + /// + /// Override Save to verify file was not loaded as readonly + /// + public override void Save(string filename) + { + VerifyThrowNotReadOnly(); + base.Save(filename); + } + + /// + /// Override Save to verify file was not loaded as readonly + /// + public override void Save(TextWriter writer) + { + VerifyThrowNotReadOnly(); + base.Save(writer); + } + + /// + /// Override Save to verify file was not loaded as readonly + /// + public override void Save(XmlWriter writer) + { + VerifyThrowNotReadOnly(); + base.Save(writer); + } + + /// + /// Reset state for unit tests that want to set the env var + /// + internal static void ClearReadOnlyFlags_UnitTestsOnly() + { + s_readOnlyFlags = ReadOnlyLoadFlags.Undefined; + } + + /// + /// Called when the XmlDocument is unloaded to remove this XML's + /// contribution to the string interning cache. + /// Does NOT zombie the ProjectRootElement or anything else. + /// + internal void ClearAnyCachedStrings() + { + StringCache.Clear(this); + } + + /// + /// Determine whether we should load this file read only. + /// We decide yes if it is in program files or the OS directory, and the file name starts with "microsoft", else no. + /// We are very selective because we don't want to load files read only that the host might want to save, nor + /// any files in which comments within property/metadata values might be significant - MSBuild does not discard those, normally. + /// + private void DetermineWhetherToLoadReadOnly(string fullPath) + { + if (_loadAsReadOnly == null) + { + DetermineWhetherToLoadReadOnlyIfPossible(); + + if (s_readOnlyFlags == ReadOnlyLoadFlags.LoadAllReadOnly) + { + _loadAsReadOnly = true; + } + else if (s_readOnlyFlags == ReadOnlyLoadFlags.LoadReadOnlyIfAppropriate) + { + // Only files from Microsoft + if (Path.GetFileName(fullPath).StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase)) + { + // If we are loading devdiv targets, we're in razzle + if (Path.GetFileName(fullPath).StartsWith("Microsoft.DevDiv", StringComparison.OrdinalIgnoreCase)) + { + _loadAsReadOnly = true; + } + else // Else, only load if they're in program files or windows directories + { + ErrorUtilities.VerifyThrow(Path.IsPathRooted(fullPath), "should be full path"); + string directory = Path.GetDirectoryName(fullPath); + + if (directory.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.Windows), StringComparison.OrdinalIgnoreCase) || + (directory.StartsWith(FrameworkLocationHelper.programFiles32, StringComparison.OrdinalIgnoreCase)) || + (!String.IsNullOrEmpty(FrameworkLocationHelper.programFiles64) && directory.StartsWith(FrameworkLocationHelper.programFiles64, StringComparison.OrdinalIgnoreCase))) + { + _loadAsReadOnly = true; + } + } + } + } + } + } + + /// + /// Determine whether we would ever load read only + /// + private void DetermineWhetherToLoadReadOnlyIfPossible() + { + if (s_readOnlyFlags == ReadOnlyLoadFlags.Undefined) + { + s_readOnlyFlags = ReadOnlyLoadFlags.LoadAllWriteable; + + if (String.Equals(Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly"), "true", StringComparison.OrdinalIgnoreCase)) + { + s_readOnlyFlags = ReadOnlyLoadFlags.LoadReadOnlyIfAppropriate; + } + + if (String.Equals(Environment.GetEnvironmentVariable("MSBUILDLOADALLFILESASREADONLY"), "1", StringComparison.OrdinalIgnoreCase)) + { + s_readOnlyFlags = ReadOnlyLoadFlags.LoadAllReadOnly; + } + + // "Escape hatch" should someone really need to edit these - since we'll be switching it on in VS and msbuild.exe wholesale. + if (String.Equals(Environment.GetEnvironmentVariable("MSBUILDLOADALLFILESASWRITEABLE"), "1", StringComparison.OrdinalIgnoreCase)) + { + s_readOnlyFlags = ReadOnlyLoadFlags.LoadAllWriteable; + } + } + } + + /// + /// Throw if this was loaded read only + /// + private void VerifyThrowNotReadOnly() + { + ErrorUtilities.VerifyThrowInvalidOperation(!_loadAsReadOnly.HasValue || !_loadAsReadOnly.Value, "OM_CannotSaveFileLoadedAsReadOnly", _fullPath); + } + } +} diff --git a/src/XMakeBuildEngine/ElementLocation/XmlElementWithLocation.cs b/src/XMakeBuildEngine/ElementLocation/XmlElementWithLocation.cs new file mode 100644 index 00000000000..59b3feb9286 --- /dev/null +++ b/src/XMakeBuildEngine/ElementLocation/XmlElementWithLocation.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Improvement to XmlElement that during load attaches location information to itself. +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using System; +using System.Xml; +using System.Diagnostics; + +namespace Microsoft.Build.Construction +{ + /// + /// Derivation of XmlElement to implement IXmlLineInfo + /// + /// + /// It would be nice to add some helper overloads of base class members that + /// downcast their return values to XmlElement/AttributeWithLocation. However + /// C# doesn't currently allow covariance in method overloading, only on delegates. + /// The caller must bravely downcast. + /// + internal class XmlElementWithLocation : XmlElement, IXmlLineInfo + { + /// + /// Line, column, file information + /// + private ElementLocation _elementLocation; + + /// + /// Constructor without location information + /// + public XmlElementWithLocation(string prefix, string localName, string namespaceURI, XmlDocumentWithLocation document) + : this(prefix, localName, namespaceURI, document, 0, 0) + { + } + + /// + /// Constructor with location information + /// + public XmlElementWithLocation(string prefix, string localName, string namespaceURI, XmlDocumentWithLocation document, int lineNumber, int columnNumber) + : base(prefix, localName, namespaceURI, document) + { + // Subtract one, just to give the same value as the old code did. + // In the past we pointed to the column of the open angle bracket whereas the XmlTextReader points to the first character of the element name. + // In well formed XML these are always adjacent on the same line, so it's safe to subtract one. + // If we're loading from a stream it's zero, so don't subtract one. + XmlDocumentWithLocation documentWithLocation = (XmlDocumentWithLocation)document; + + int adjustedColumn = (columnNumber == 0) ? columnNumber : columnNumber - 1; + _elementLocation = ElementLocation.Create(documentWithLocation.FullPath, lineNumber, adjustedColumn); + } + + /// + /// Returns the line number if available, else 0. + /// IXmlLineInfo member. + /// + public int LineNumber + { + [DebuggerStepThrough] + get + { return Location.Line; } + } + + /// + /// Returns the column number if available, else 0. + /// IXmlLineInfo member. + /// + public int LinePosition + { + [DebuggerStepThrough] + get + { return Location.Column; } + } + + /// + /// Provides an ElementLocation for this element, using the path to the file containing + /// this element as the project file entry. + /// Element location may be incorrect, if it was not loaded from disk. + /// Does not return null. + /// + /// + /// Should have at least the file name if the containing project has been given a file name, + /// even if it wasn't loaded from disk, or has been edited since. That's because we set that + /// path on our XmlDocumentWithLocation wrapper class. + /// + internal ElementLocation Location + { + get + { + // Caching the element location object saves significant memory + XmlDocumentWithLocation ownerDocumentWithLocation = (XmlDocumentWithLocation)OwnerDocument; + if (!String.Equals(_elementLocation.File, ownerDocumentWithLocation.FullPath, StringComparison.OrdinalIgnoreCase)) + { + _elementLocation = ElementLocation.Create(ownerDocumentWithLocation.FullPath, _elementLocation.Line, _elementLocation.Column); + } + + return _elementLocation; + } + } + + /// + /// Whether location is available. + /// IXmlLineInfo member. + /// + public bool HasLineInfo() + { + return Location.Line != 0; + } + + /// + /// Returns the XmlAttribute with the specified name or null if a matching attribute was not found. + /// + public XmlAttributeWithLocation GetAttributeWithLocation(string name) + { + XmlAttribute attribute = GetAttributeNode(name); + + if (attribute == null) + { + return null; + } + + return (XmlAttributeWithLocation)attribute; + } + + /// + /// Overridden to convert the display of the element from open form (separate open and closed tags) to closed form + /// (single closed tag) if the last child is being removed. This is simply for tidiness of the project file. + /// For example, removing the only piece of metadata from an item will leave behind one tag instead of two. + /// + public override XmlNode RemoveChild(XmlNode oldChild) + { + XmlNode result = base.RemoveChild(oldChild); + + if (!HasChildNodes) + { + IsEmpty = true; + } + + return result; + } + + /// + /// Gets the location of any attribute on this element with the specified name. + /// If there is no such attribute, returns null. + /// + internal ElementLocation GetAttributeLocation(string name) + { + XmlAttributeWithLocation attributeWithLocation = GetAttributeWithLocation(name); + + return (attributeWithLocation != null) ? attributeWithLocation.Location : null; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/ElementLocation/XmlNameTableThreadSafe.cs b/src/XMakeBuildEngine/ElementLocation/XmlNameTableThreadSafe.cs new file mode 100644 index 00000000000..5ef959e3081 --- /dev/null +++ b/src/XMakeBuildEngine/ElementLocation/XmlNameTableThreadSafe.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// XmlNameTable that is thread safe for concurrent users. +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using System; +using System.Xml; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.Build.Construction +{ + /// + /// XmlNameTable that is thread safe for concurrent users. + /// + /// + /// Fortunately the standard implementation has only four accessible members + /// and all of them are virtual so we can easily add locks. + /// + internal class XmlNameTableThreadSafe : NameTable + { + /// + /// Synchronization object. + /// + private object _locker = new Object(); + + /// + /// Add a string to the table. + /// + public override string Add(string key) + { + lock (_locker) + { + return base.Add(key); + } + } + + /// + /// Add a string to the table, passed in as + /// an extent in a char array. + /// + public override string Add(char[] key, int start, int len) + { + lock (_locker) + { + return base.Add(key, start, len); + } + } + + /// + /// Get a string from the table. + /// + public override string Get(string value) + { + lock (_locker) + { + return base.Get(value); + } + } + + /// + /// Get a string from the table, passed in as + /// an extent in a char array. + /// + public override string Get(char[] key, int start, int len) + { + lock (_locker) + { + return base.Get(key, start, len); + } + } + } +} diff --git a/src/XMakeBuildEngine/Errors/InternalLoggerException.cs b/src/XMakeBuildEngine/Errors/InternalLoggerException.cs new file mode 100644 index 00000000000..fc707fc69da --- /dev/null +++ b/src/XMakeBuildEngine/Errors/InternalLoggerException.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Exceptions +{ + /// + /// This exception is used to wrap an unhandled exception from a logger. This exception aborts the build, and it can only be + /// thrown by the MSBuild engine. + /// + /// + /// WARNING: marking a type [Serializable] without implementing ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both forward and backward compatibility + /// + [Serializable] + public sealed class InternalLoggerException : Exception + { + #region Unusable constructors + + /// + /// Default constructor. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use the rich constructor instead. + /// + /// + public InternalLoggerException() + { + ErrorUtilities.VerifyThrowInvalidOperation(false, "InternalLoggerExceptionOnlyThrownByEngine"); + } + + /// + /// Creates an instance of this exception using the specified error message. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use the rich constructor instead. + /// + /// + /// + public InternalLoggerException(string message) + : base(message) + { + ErrorUtilities.VerifyThrowInvalidOperation(false, "InternalLoggerExceptionOnlyThrownByEngine"); + } + + /// + /// Creates an instance of this exception using the specified error message and inner exception. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use the rich constructor instead. + /// + /// + /// + /// + public InternalLoggerException(string message, Exception innerException) + : base(message, innerException) + { + ErrorUtilities.VerifyThrowInvalidOperation(false, "InternalLoggerExceptionOnlyThrownByEngine"); + } + + #endregion + + /// + /// Creates an instance of this exception using rich error information. + /// Internal for unit testing + /// + /// This is the only usable constructor. + /// + /// + /// Can be null. + /// + /// + internal InternalLoggerException + ( + string message, + Exception innerException, + BuildEventArgs e, + string errorCode, + string helpKeyword, + bool initializationException + ) + : base(message, innerException) + { + ErrorUtilities.VerifyThrow((message != null) && (message.Length > 0), "Need error message."); + ErrorUtilities.VerifyThrow(innerException != null || initializationException == true, "Need the logger exception."); + ErrorUtilities.VerifyThrow((errorCode != null) && (errorCode.Length > 0), "Must specify the error message code."); + ErrorUtilities.VerifyThrow((helpKeyword != null) && (helpKeyword.Length > 0), "Must specify the help keyword for the IDE."); + + _e = e; + _errorCode = errorCode; + _helpKeyword = helpKeyword; + _initializationException = initializationException; + } + + #region Serialization (update when adding new class members) + + /// + /// Protected constructor used for (de)serialization. + /// If we ever add new members to this class, we'll need to update this. + /// + /// + /// + private InternalLoggerException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _e = (BuildEventArgs)info.GetValue("e", typeof(BuildEventArgs)); + _errorCode = info.GetString("errorCode"); + _helpKeyword = info.GetString("helpKeyword"); + _initializationException = info.GetBoolean("initializationException"); + } + + /// + /// ISerializable method which we must override since Exception implements this interface + /// If we ever add new members to this class, we'll need to update this. + /// + /// + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + override public void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("e", _e); + info.AddValue("errorCode", _errorCode); + info.AddValue("helpKeyword", _helpKeyword); + info.AddValue("initializationException", _initializationException); + } + + /// + /// Provide default values for optional members + /// + [OnDeserializing] // Will happen before the object is deserialized + private void SetDefaultsBeforeSerialization(StreamingContext sc) + { + _initializationException = false; + } + + /// + /// Dont actually have anything to do in the method, but the method is required when implementing an optional field + /// + [OnDeserialized] + private void SetValueAfterDeserialization(StreamingContext sx) + { + // Have nothing to do + } + #endregion + + #region Properties + + /// + /// Gets the details of the build event (if any) that was being logged. + /// + /// The build event args, or null. + public BuildEventArgs BuildEventArgs + { + get + { + return _e; + } + } + + /// + /// Gets the error code associated with this exception's message (not the inner exception). + /// + /// The error code string. + public string ErrorCode + { + get + { + return _errorCode; + } + } + + /// + /// Gets the F1-help keyword associated with this error, for the host IDE. + /// + /// The keyword string. + public string HelpKeyword + { + get + { + return _helpKeyword; + } + } + + /// + /// True if the exception occured during logger initialization + /// + public bool InitializationException + { + get + { + return _initializationException; + } + } + + #endregion + + /// + /// Throws an instance of this exception using rich error information. + /// + /// + /// Can be null. + /// + /// + internal static void Throw + ( + Exception innerException, + BuildEventArgs e, + string messageResourceName, + bool initializationException, + params string[] messageArgs + ) + { + ErrorUtilities.VerifyThrow(messageResourceName != null, "Need error message."); + + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, messageResourceName, messageArgs); + + throw new InternalLoggerException(message, innerException, e, errorCode, helpKeyword, initializationException); + } + + // the event that was being logged when a logger failed (can be null) + private BuildEventArgs _e; + // the error code for this exception's message (not the inner exception) + private string _errorCode; + // the F1-help keyword for the host IDE + private string _helpKeyword; + + // This flag is set to indicate that the exception occured during logger initialization + [OptionalField(VersionAdded = 2)] + private bool _initializationException; + } +} diff --git a/src/XMakeBuildEngine/Errors/InvalidProjectFileException.cs b/src/XMakeBuildEngine/Errors/InvalidProjectFileException.cs new file mode 100644 index 00000000000..ca816e8ddc8 --- /dev/null +++ b/src/XMakeBuildEngine/Errors/InvalidProjectFileException.cs @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Runtime.Serialization; +using System.Security.Permissions; + +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.Exceptions +{ + /// + /// This exception is thrown whenever there is a problem with the user's XML project file. The problem might be semantic or + /// syntactical. The latter would be of a type typically caught by XSD validation (if it was performed by the project writer). + /// + /// + /// WARNING: marking a type [Serializable] without implementing ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both forward and backward compatibility + /// + [Serializable] + public sealed class InvalidProjectFileException : Exception + { + #region Basic constructors + + /// + /// Default constructor. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use a rich constructor whenever possible. + /// + public InvalidProjectFileException() + : base() + { + // do nothing + } + + /// + /// Creates an instance of this exception using the specified error message. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use a rich constructor whenever possible. + /// + /// + public InvalidProjectFileException(string message) + : base(message) + { + // do nothing + } + + /// + /// Creates an instance of this exception using the specified error message and inner exception. + /// + /// + /// This constructor only exists to satisfy .NET coding guidelines. Use a rich constructor whenever possible. + /// + /// + /// + public InvalidProjectFileException(string message, Exception innerException) + : base(message, innerException) + { + // do nothing + } + + /// + /// Creates an instance of this exception using the specified error message and inner invalid project file exception. + /// This is used in order to wrap and exception rather than rethrow it verbatim, which would reset the callstack. + /// The assumption is that all the metadata for the outer exception comes from the inner exception, eg., they have the same error code. + /// + internal InvalidProjectFileException(string message, InvalidProjectFileException innerException) + : this(innerException.ProjectFile, innerException.LineNumber, innerException.ColumnNumber, innerException.EndLineNumber, innerException.EndColumnNumber, message, innerException.ErrorSubcategory, innerException.ErrorCode, innerException.HelpKeyword) + { + } + + #endregion + + #region Serialization (update when adding new class members) + + /// + /// Protected constructor used for (de)serialization. + /// If we ever add new members to this class, we'll need to update this. + /// + /// + /// + private InvalidProjectFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _file = info.GetString("projectFile"); + _lineNumber = info.GetInt32("lineNumber"); + _columnNumber = info.GetInt32("columnNumber"); + _endLineNumber = info.GetInt32("endLineNumber"); + _endColumnNumber = info.GetInt32("endColumnNumber"); + _errorSubcategory = info.GetString("errorSubcategory"); + _errorCode = info.GetString("errorCode"); + _helpKeyword = info.GetString("helpKeyword"); + _hasBeenLogged = info.GetBoolean("hasBeenLogged"); + } + + /// + /// ISerializable method which we must override since Exception implements this interface + /// If we ever add new members to this class, we'll need to update this. + /// + /// + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + override public void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("projectFile", _file); + info.AddValue("lineNumber", _lineNumber); + info.AddValue("columnNumber", _columnNumber); + info.AddValue("endLineNumber", _endLineNumber); + info.AddValue("endColumnNumber", _endColumnNumber); + info.AddValue("errorSubcategory", _errorSubcategory); + info.AddValue("errorCode", _errorCode); + info.AddValue("helpKeyword", _helpKeyword); + info.AddValue("hasBeenLogged", _hasBeenLogged); + } + + #endregion + + #region Rich constructors + + /// + /// Creates an instance of this exception using rich error information. + /// + /// This constructor is preferred over the basic constructors. + /// The invalid project file (can be empty string). + /// The invalid line number in the project (set to zero if not available). + /// The invalid column number in the project (set to zero if not available). + /// The end of a range of invalid lines in the project (set to zero if not available). + /// The end of a range of invalid columns in the project (set to zero if not available). + /// Error message for exception. + /// Error sub-category that describes the error (can be null). + /// The error code (can be null). + /// The F1-help keyword for the host IDE (can be null). + public InvalidProjectFileException + ( + string projectFile, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string errorSubcategory, + string errorCode, + string helpKeyword + ) : + this(projectFile, lineNumber, columnNumber, endLineNumber, endColumnNumber, message, errorSubcategory, errorCode, helpKeyword, null) + { + } + + /// + /// Creates an instance of this exception using rich error information. + /// + /// This constructor is preferred over the basic constructors. + /// The invalid project file (can be empty string). + /// The invalid line number in the project (set to zero if not available). + /// The invalid column number in the project (set to zero if not available). + /// The end of a range of invalid lines in the project (set to zero if not available). + /// The end of a range of invalid columns in the project (set to zero if not available). + /// Error message for exception. + /// Error sub-category that describes the error (can be null). + /// The error code (can be null). + /// The F1-help keyword for the host IDE (can be null). + /// Any inner exception. May be null. + internal InvalidProjectFileException + ( + string projectFile, + int lineNumber, + int columnNumber, + int endLineNumber, + int endColumnNumber, + string message, + string errorSubcategory, + string errorCode, + string helpKeyword, + Exception innerException + ) : base(message, innerException) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentLength(message, "message"); + + // Try to helpfully provide a full path if possible, but do so robustly. + // This exception might be because the path was invalid! + // Also don't consider "MSBUILD" a path: that's what msbuild.exe uses when there's no project associated. + if (projectFile.Length > 0 && !String.Equals(projectFile, "MSBUILD", StringComparison.Ordinal)) + { + string fullPath = FileUtilities.GetFullPathNoThrow(projectFile); + + projectFile = (fullPath == null) ? projectFile : fullPath; + } + + _file = projectFile; + _lineNumber = lineNumber; + _columnNumber = columnNumber; + _endLineNumber = endLineNumber; + _endColumnNumber = endColumnNumber; + _errorSubcategory = errorSubcategory; + _errorCode = errorCode; + _helpKeyword = helpKeyword; + } + + #endregion + + #region Properties + + /// + /// Gets the exception message including the affected project file (if any). + /// + /// The complete message string. + public override string Message + { + get + { + return base.Message + ((!String.IsNullOrEmpty(ProjectFile)) + ? (" " + ProjectFile) + : null); + } + } + + /// + /// Gets the exception message not including the project file. + /// + /// The error message string only. + public string BaseMessage + { + get + { + return base.Message; + } + } + + /// + /// Gets the file (if any) associated with this exception. + /// This may be an imported file. + /// + /// + /// The name is poorly chosen as this may be a targets file, + /// but the name has shipped now. + /// + /// Project filename/path string, or null. + public string ProjectFile + { + get + { + return _file; + } + } + + /// + /// Gets the invalid line number (if any) in the project. + /// + /// The invalid line number, or zero. + public int LineNumber + { + get + { + return _lineNumber; + } + } + + /// + /// Gets the invalid column number (if any) in the project. + /// + /// The invalid column number, or zero. + public int ColumnNumber + { + get + { + return _columnNumber; + } + } + + /// + /// Gets the last line number (if any) of a range of invalid lines in the project. + /// + /// The last invalid line number, or zero. + public int EndLineNumber + { + get + { + return _endLineNumber; + } + } + + /// + /// Gets the last column number (if any) of a range of invalid columns in the project. + /// + /// The last invalid column number, or zero. + public int EndColumnNumber + { + get + { + return _endColumnNumber; + } + } + + /// + /// Gets the error sub-category (if any) that describes the type of this error. + /// + /// The sub-category string, or null. + public string ErrorSubcategory + { + get + { + return _errorSubcategory; + } + } + + /// + /// Gets the error code (if any) associated with the exception message. + /// + /// Error code string, or null. + public string ErrorCode + { + get + { + return _errorCode; + } + } + + /// + /// Gets the F1-help keyword (if any) associated with this error, for the host IDE. + /// + /// The keyword string, or null. + public string HelpKeyword + { + get + { + return _helpKeyword; + } + } + + /// + /// Whether the exception has already been logged. Allows the exception to be logged at the + /// most appropriate location, but continue to be propagated. + /// + public bool HasBeenLogged + { + get + { + return _hasBeenLogged; + } + internal set + { + _hasBeenLogged = value; + } + } + + #endregion + + // the project file that caused this exception + private string _file; + // the invalid line number in the project + private int _lineNumber; + // the invalid column number in the project + private int _columnNumber; + // the end of a range of invalid lines in the project + private int _endLineNumber; + // the end of a range of invalid columns in the project + private int _endColumnNumber; + // the error sub-category that describes the type of this error + private string _errorSubcategory; + // the error code for the exception message + private string _errorCode; + // the F1-help keyword for the host IDE + private string _helpKeyword; + // Has this errors been sent to the loggers? + private bool _hasBeenLogged = false; + } +} diff --git a/src/XMakeBuildEngine/Errors/InvalidToolsetDefinitionException.cs b/src/XMakeBuildEngine/Errors/InvalidToolsetDefinitionException.cs new file mode 100644 index 00000000000..14b4014a73e --- /dev/null +++ b/src/XMakeBuildEngine/Errors/InvalidToolsetDefinitionException.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; + +using Microsoft.Build.Shared; +using System.Runtime.Serialization; +using System.Security.Permissions; + +namespace Microsoft.Build.Exceptions +{ + /// + /// Exception subclass that ToolsetReaders should throw. + /// + [Serializable] + public class InvalidToolsetDefinitionException : Exception + { + /// + /// The MSBuild error code corresponding with this exception. + /// + private string _errorCode = null; + + /// + /// Basic constructor. + /// + public InvalidToolsetDefinitionException() + : base() + { + } + + /// + /// Basic constructor. + /// + /// + public InvalidToolsetDefinitionException(string message) + : base(message) + { + } + + /// + /// Basic constructor. + /// + /// + /// + public InvalidToolsetDefinitionException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Basic constructor. + /// + /// + /// + protected InvalidToolsetDefinitionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + _errorCode = info.GetString("errorCode"); + } + + /// + /// Constructor that takes an MSBuild error code + /// + /// + /// + public InvalidToolsetDefinitionException(string message, string errorCode) + : base(message) + { + _errorCode = errorCode; + } + + /// + /// Constructor that takes an MSBuild error code + /// + /// + /// + /// + public InvalidToolsetDefinitionException(string message, string errorCode, Exception innerException) + : base(message, innerException) + { + _errorCode = errorCode; + } + + /// + /// ISerializable method which we must override since Exception implements this interface + /// If we ever add new members to this class, we'll need to update this. + /// + /// + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + override public void GetObjectData(SerializationInfo info, StreamingContext context) + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + base.GetObjectData(info, context); + + info.AddValue("errorCode", _errorCode); + } + + /// + /// The MSBuild error code corresponding with this exception, or + /// null if none was specified. + /// + public string ErrorCode + { + get + { + return _errorCode; + } + } + + #region Static Throw Helpers + + /// + /// Throws an InvalidToolsetDefinitionException. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments + /// is expensive, because memory is allocated for the array of arguments -- do + /// not call this method repeatedly in performance-critical scenarios + /// + /// + /// + internal static void Throw + ( + string resourceName, + params string[] args + ) + { + Throw(null, resourceName, args); + } + + /// + /// Throws an InvalidToolsetDefinitionException including a specified inner exception, + /// which may be interesting to hosts. + /// + /// PERF WARNING: calling a method that takes a variable number of arguments + /// is expensive, because memory is allocated for the array of arguments -- do + /// not call this method repeatedly in performance-critical scenarios + /// + /// + /// + internal static void Throw + ( + Exception innerException, + string resourceName, + params string[] args + ) + { +#if DEBUG + ResourceUtilities.VerifyResourceStringExists(resourceName); +#endif + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, resourceName, (object[])args); + + throw new InvalidToolsetDefinitionException(message, errorCode, innerException); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Errors/RegistryException.cs b/src/XMakeBuildEngine/Errors/RegistryException.cs new file mode 100644 index 00000000000..c792b526c6a --- /dev/null +++ b/src/XMakeBuildEngine/Errors/RegistryException.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Runtime.Serialization; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Exceptions +{ + /// + /// Generic exception used to wrap exceptions thrown during Registry access. + /// + [Serializable] + internal class RegistryException : Exception + { + /// + /// Basic constructor. + /// + public RegistryException() + : base() + { + } + + /// + /// Basic constructor. + /// + /// + public RegistryException(string message) + : base(message) + { + } + + /// + /// Basic constructor. + /// + /// + /// + public RegistryException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Constructor that takes a string description of the registry + /// key or value causing the error. + /// + /// + /// + public RegistryException(string message, string source) + : base(message) + { + base.Source = source; + } + + /// + /// Since this class implements Iserializable this constructor is required to be implemented. + /// + protected RegistryException(SerializationInfo info, StreamingContext context) : base(info, context) + { + // We don't have any reason at the moment to do any custom serizlization or deserialization, this methods was added + // to conform to the implementation of the standard constructors for ISerializable classes + } + + /// + /// Constructor that takes a string description of the registry + /// key or value causing the error. + /// + /// + /// + /// + public RegistryException(string message, string source, Exception innerException) + : base(message, innerException) + { + base.Source = source; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ConditionEvaluator.cs b/src/XMakeBuildEngine/Evaluation/ConditionEvaluator.cs new file mode 100644 index 00000000000..950366f756d --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ConditionEvaluator.cs @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.Evaluation +{ + using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; + using BuildEventContext = Microsoft.Build.Framework.BuildEventContext; + using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + using ElementLocation = Microsoft.Build.Construction.ElementLocation; + using Microsoft.Build.Execution; + using Microsoft.Build.Shared; + + internal static class ConditionEvaluator + { + private readonly static Regex s_singlePropertyRegex = new Regex(@"^\$\(([^\$\(\)]*)\)$"); + + /// + /// Update our table which keeps track of all the properties that are referenced + /// inside of a condition and the string values that they are being tested against. + /// So, for example, if the condition was " '$(Configuration)' == 'Debug' ", we + /// would get passed in leftValue="$(Configuration)" and rightValueExpanded="Debug". + /// This call would add the string "Debug" to the list of possible values for the + /// "Configuration" property. + /// + /// This method also handles the case when two or more properties are being + /// concatenated together with a vertical bar, as in ' + /// $(Configuration)|$(Platform)' == 'Debug|x86' + /// + /// + /// + /// + internal static void UpdateConditionedPropertiesTable + ( + Dictionary> conditionedPropertiesTable, // List of possible values, keyed by property name + + string leftValue, // The raw value on the left side of the operator + + string rightValueExpanded // The fully expanded value on the right side + // of the operator. + ) + { + if ((conditionedPropertiesTable != null) && (rightValueExpanded.Length > 0)) + { + // The left side should be exactly "$(propertyname)" or "$(propertyname1)|$(propertyname2)" + // or "$(propertyname1)|$(propertyname2)|$(propertyname3)", etc. Anything else, + // and we don't touch the table. + + // Split up the leftValue into pieces based on the vertical bar character. + // PERF: Avoid allocations from string.Split by forming spans between 'pieceStart' and 'pieceEnd' + int pieceStart = 0; + + // Loop through each of the pieces. + while (true) + { + int pieceSeparator = leftValue.IndexOf('|', pieceStart); + bool lastPiece = pieceSeparator < 0; + int pieceEnd = lastPiece ? leftValue.Length : pieceSeparator; + + Match singlePropertyMatch = s_singlePropertyRegex.Match(leftValue, pieceStart, pieceEnd - pieceStart); + + if (singlePropertyMatch.Success) + { + // Find the first vertical bar on the right-hand-side expression. + int indexOfVerticalBar = rightValueExpanded.IndexOf('|'); + string rightValueExpandedPiece; + + // If there was no vertical bar, then just use the remainder of the right-hand-side + // expression as the value of the property, and terminate the loop after this iteration. + // Also, if we're on the last segment of the left-hand-side, then use the remainder + // of the right-hand-side expression as the value of the property. + if ((indexOfVerticalBar == -1) || lastPiece) + { + rightValueExpandedPiece = rightValueExpanded; + lastPiece = true; + } + else + { + // If we found a vertical bar, then the portion before the vertical bar is the + // property value which we will store in our table. Then remove that portion + // from the original string so that the next iteration of the loop can easily search + // for the first vertical bar again. + rightValueExpandedPiece = rightValueExpanded.Substring(0, indexOfVerticalBar); + rightValueExpanded = rightValueExpanded.Substring(indexOfVerticalBar + 1); + } + + // Capture the property name out of the regular expression. + string propertyName = singlePropertyMatch.Groups[1].ToString(); + + // Get the string collection for this property name, if one already exists. + List conditionedPropertyValues; + + // If this property is not already represented in the table, add a new entry + // for it. + if (!conditionedPropertiesTable.TryGetValue(propertyName, out conditionedPropertyValues)) + { + conditionedPropertyValues = new List(); + conditionedPropertiesTable[propertyName] = conditionedPropertyValues; + } + + // If the "rightValueExpanded" is not already in the string collection + // for this property name, add it now. + if (!conditionedPropertyValues.Contains(rightValueExpandedPiece)) + { + conditionedPropertyValues.Add(rightValueExpandedPiece); + } + } + + if (lastPiece) + { + break; + } + + pieceStart = pieceSeparator + 1; + } + } + } + + // An array of hashtables with cached expression trees for all the combinations of condition strings + // and parser options + private static volatile Hashtable[] s_cachedExpressionTrees = new Hashtable[(int)ParserOptions.AllowAll + 1]; + + /// + /// For debugging leaks, a way to disable caching expression trees, to reduce noise + /// + private static bool s_disableExpressionCaching = (Environment.GetEnvironmentVariable("MSBUILDDONOTCACHEEXPRESSIONS") == "1"); + + /// + /// Evaluates a string representing a condition from a "condition" attribute. + /// If the condition is a malformed string, it throws an InvalidProjectFileException. + /// This method uses cached expression trees to avoid generating them from scratch every time it's called. + /// This method is thread safe and is called from engine and task execution module threads + /// + internal static bool EvaluateCondition + ( + string condition, + ParserOptions options, + Expander expander, + ExpanderOptions expanderOptions, + string evaluationDirectory, + ElementLocation elementLocation, + ILoggingService loggingServices, + BuildEventContext buildEventContext, + ProjectRootElementCache projectRootElementCache = null + ) + where P : class, IProperty + where I : class, IItem + { + return EvaluateConditionCollectingConditionedProperties(condition, options, expander, expanderOptions, null /* do not collect conditioned properties */, evaluationDirectory, elementLocation, loggingServices, buildEventContext, projectRootElementCache); + } + + /// + /// Evaluates a string representing a condition from a "condition" attribute. + /// If the condition is a malformed string, it throws an InvalidProjectFileException. + /// This method uses cached expression trees to avoid generating them from scratch every time it's called. + /// This method is thread safe and is called from engine and task execution module threads + /// Logging service may be null. + /// + internal static bool EvaluateConditionCollectingConditionedProperties + ( + string condition, + ParserOptions options, + Expander expander, + ExpanderOptions expanderOptions, + Dictionary> conditionedPropertiesTable, + string evaluationDirectory, + ElementLocation elementLocation, + ILoggingService loggingServices, + BuildEventContext buildEventContext, + ProjectRootElementCache projectRootElementCache = null + ) + where P : class, IProperty + where I : class, IItem + { + ErrorUtilities.VerifyThrowArgumentNull(condition, "condition"); + ErrorUtilities.VerifyThrowArgumentNull(expander, "expander"); + ErrorUtilities.VerifyThrowArgumentLength(evaluationDirectory, "evaluationDirectory"); + ErrorUtilities.VerifyThrowArgumentNull(buildEventContext, "buildEventContext"); + + // An empty condition is equivalent to a "true" condition. + if (condition.Length == 0) + { + return true; + } + + // If the condition wasn't empty, there must be a location for it + ErrorUtilities.VerifyThrowArgumentNull(elementLocation, "elementLocation"); + + Hashtable cachedExpressionTreesForCurrentOptions = s_cachedExpressionTrees[(int)options]; + + // We only need to lock on writes to the table + if (cachedExpressionTreesForCurrentOptions == null) + { + // Given property functions, casing in conditional expressions isn't necessarily ignored. + cachedExpressionTreesForCurrentOptions = new Hashtable(50, StringComparer.Ordinal); + + lock (s_cachedExpressionTrees) + { + s_cachedExpressionTrees[(int)options] = cachedExpressionTreesForCurrentOptions; + } + } + + // VS stress tests could fill up this cache without end, for example if they use + // random configuration names - those appear in conditional expressions. + // So if we hit a limit that we should never hit in normal circumstances in VS, + // and rarely, periodically hit in normal circumstances in large tree builds, + // just clear out the cache. It can start repopulating again. Some kind of prioritized + // aging isn't worth it: although the hit rate of these caches is excellent (nearly 100%) + // the cost of reparsing expressions should the cache be cleared is not particularly large. + // Loading Australian Government in VS, there are 3 of these tables, two with about 50 + // entries and one with about 650 entries. So 3000 seems large enough. + if (cachedExpressionTreesForCurrentOptions.Count > 3000) // threadsafe + { + lock (cachedExpressionTreesForCurrentOptions) + { + cachedExpressionTreesForCurrentOptions.Clear(); + } + } + + // Try and see if we have an expression tree for this condition already + GenericExpressionNode parsedExpression = (GenericExpressionNode)cachedExpressionTreesForCurrentOptions[condition]; + + if (parsedExpression == null) + { + Parser conditionParser = new Parser(); + + #region REMOVE_COMPAT_WARNING + conditionParser.LoggingServices = loggingServices; + conditionParser.LogBuildEventContext = buildEventContext; + #endregion + + parsedExpression = conditionParser.Parse(condition, options, elementLocation); + + // It's possible two threads will add a different tree to the same entry in the hashtable, + // but it should be rare and it's not a problem - the previous entry will be thrown away. + // We could ensure no dupes with double check locking but it's not really necessary here. + // Also, we don't want to lock on every read. + lock (cachedExpressionTreesForCurrentOptions) + { + if (!s_disableExpressionCaching) + { + cachedExpressionTreesForCurrentOptions[condition] = parsedExpression; + } + } + } + + bool result; + + ConditionEvaluationState state = new ConditionEvaluationState(condition, expander, expanderOptions, conditionedPropertiesTable, evaluationDirectory, elementLocation, projectRootElementCache); + + // We are evaluating this expression now and it can cache some state for the duration, + // so we don't want multiple threads working on the same expression + lock (parsedExpression) + { + try + { + result = parsedExpression.Evaluate(state); + } + finally + { + parsedExpression.ResetState(); + } + } + + return result; + } + + internal interface IConditionEvaluationState + { + string Condition + { + get; + } + + string EvaluationDirectory + { + get; + } + + ElementLocation ElementLocation + { + get; + } + + /// + /// Table of conditioned properties and their values. + /// Used to populate configuration lists in some project systems. + /// If this is null, as it is for command line builds, conditioned properties + /// are not recorded. + /// + Dictionary> ConditionedPropertiesInProject + { + get; + } + + /// + /// May return null if the expression would expand to non-empty and it broke out early. + /// Otherwise, returns the correctly expanded expression. + /// + string ExpandIntoStringBreakEarly(string expression); + + /// + /// Expands the specified expression into a list of TaskItem's. + /// + IList ExpandIntoTaskItems(string expression); + + /// + /// Expands the specified expression into a string. + /// + string ExpandIntoString(string expression); + + /// + /// PRE cache + /// + ProjectRootElementCache LoadedProjectsCache + { + get; + } + } + + /// + /// All the state necessary for the evaluation of conditionals so that the expression tree + /// is stateless and reusable + /// + internal class ConditionEvaluationState : IConditionEvaluationState + where P : class, IProperty + where I : class, IItem + { + private string _condition; + private string _evaluationDirectory; + private Expander _expander; + private ExpanderOptions _expanderOptions; + private Dictionary> _conditionedPropertiesInProject; + private ElementLocation _elementLocation; + private ProjectRootElementCache _projectRootElementCache; + + /// + /// Condition that was parsed. This does not belong here, + /// it belongs to the expression tree, not the condition evaluation state. + /// + public string Condition + { + get { return _condition; } + } + + public string EvaluationDirectory + { + get { return _evaluationDirectory; } + } + + public ElementLocation ElementLocation + { + get { return _elementLocation; } + } + + /// + /// Table of conditioned properties and their values. + /// Used to populate configuration lists in some project systems. + /// If this is null, as it is for command line builds, conditioned properties + /// are not recorded. + /// + public Dictionary> ConditionedPropertiesInProject + { + get { return _conditionedPropertiesInProject; } + } + + /// + /// PRE collection. + /// + public ProjectRootElementCache LoadedProjectsCache + { + get { return _projectRootElementCache; } + } + + + internal ConditionEvaluationState + ( + string condition, + Expander expander, + ExpanderOptions expanderOptions, + Dictionary> conditionedPropertiesInProject, + string evaluationDirectory, + ElementLocation elementLocation, + ProjectRootElementCache projectRootElementCache = null + ) + { + ErrorUtilities.VerifyThrowArgumentNull(condition, "condition"); + ErrorUtilities.VerifyThrowArgumentNull(expander, "expander"); + ErrorUtilities.VerifyThrowArgumentNull(evaluationDirectory, "evaluationDirectory"); + ErrorUtilities.VerifyThrowArgumentNull(elementLocation, "elementLocation"); + + _condition = condition; + _expander = expander; + _expanderOptions = expanderOptions; + _conditionedPropertiesInProject = conditionedPropertiesInProject; // May be null + _evaluationDirectory = evaluationDirectory; + _elementLocation = elementLocation; + _projectRootElementCache = projectRootElementCache; + } + + /// + /// May return null if the expression would expand to non-empty and it broke out early. + /// Otherwise, returns the correctly expanded expression. + /// + public string ExpandIntoStringBreakEarly(string expression) + { + bool originalValue = _expander.WarnForUninitializedProperties; + + expression = _expander.ExpandIntoStringAndUnescape(expression, _expanderOptions | ExpanderOptions.BreakOnNotEmpty, _elementLocation); + + _expander.WarnForUninitializedProperties = originalValue; + + return expression; + } + + /// + /// Expands the properties and items in the specified expression into a list of taskitems. + /// + /// The expression to expand. + /// The element location context for the expression, used for error reporting. + /// A list of items. + public IList ExpandIntoTaskItems(string expression) + { + bool originalValue = _expander.WarnForUninitializedProperties; + + IList items = _expander.ExpandIntoTaskItemsLeaveEscaped(expression, _expanderOptions, _elementLocation); + + _expander.WarnForUninitializedProperties = originalValue; + + return items; + } + + /// + /// Expands the specified expression into a string. + /// + /// The expression to expand. + /// The element location context for the expression, used for error reporting. + /// The expanded string. + public string ExpandIntoString(string expression) + { + bool originalValue = _expander.WarnForUninitializedProperties; + + expression = _expander.ExpandIntoStringAndUnescape(expression, _expanderOptions, _elementLocation); + + _expander.WarnForUninitializedProperties = originalValue; + + return expression; + } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/AndExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/AndExpressionNode.cs new file mode 100644 index 00000000000..89edd84c56a --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/AndExpressionNode.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Performs logical AND on children + /// Does not update conditioned properties table + /// + internal sealed class AndExpressionNode : OperatorExpressionNode + { + /// + /// Evaluate as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + (LeftChild.CanBoolEvaluate(state), + state.ElementLocation, + "ExpressionDoesNotEvaluateToBoolean", + LeftChild.GetUnexpandedValue(state), + LeftChild.GetExpandedValue(state), + state.Condition); + + if (!LeftChild.BoolEvaluate(state)) + { + // Short circuit + return false; + } + else + { + ProjectErrorUtilities.VerifyThrowInvalidProject + (RightChild.CanBoolEvaluate(state), + state.ElementLocation, + "ExpressionDoesNotEvaluateToBoolean", + RightChild.GetUnexpandedValue(state), + RightChild.GetExpandedValue(state), + state.Condition); + + return RightChild.BoolEvaluate(state); + } + } + + #region REMOVE_COMPAT_WARNING + private bool _possibleAndCollision = true; + internal override bool PossibleAndCollision + { + set { _possibleAndCollision = value; } + get { return _possibleAndCollision; } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/CharacterUtilities.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/CharacterUtilities.cs new file mode 100644 index 00000000000..8e44cb99184 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/CharacterUtilities.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; + +namespace Microsoft.Build.Evaluation +{ + internal static class CharacterUtilities + { + static internal bool IsNumberStart(char candidate) + { + return (candidate == '+' || candidate == '-' || candidate == '.' || char.IsDigit(candidate)); + } + + static internal bool IsSimpleStringStart(char candidate) + { + return (candidate == '_' || char.IsLetter(candidate)); + } + + static internal bool IsSimpleStringChar(char candidate) + { + return (IsSimpleStringStart(candidate) || char.IsDigit(candidate)); + } + + static internal bool IsHexAlphabetic(char candidate) + { + return (candidate == 'a' || candidate == 'b' || candidate == 'c' || candidate == 'd' || candidate == 'e' || candidate == 'f' || + candidate == 'A' || candidate == 'B' || candidate == 'C' || candidate == 'D' || candidate == 'E' || candidate == 'F'); + } + + static internal bool IsHexDigit(char candidate) + { + return (char.IsDigit(candidate) || IsHexAlphabetic(candidate)); + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/EqualExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/EqualExpressionNode.cs new file mode 100644 index 00000000000..4317057d27b --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/EqualExpressionNode.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Compares for equality + /// + internal sealed class EqualExpressionNode : MultipleComparisonNode + { + /// + /// Compare numbers + /// + protected override bool Compare(double left, double right) + { + return left == right; + } + + /// + /// Compare booleans + /// + protected override bool Compare(bool left, bool right) + { + return left == right; + } + + /// + /// Compare strings + /// + protected override bool Compare(string left, string right) + { + return String.Equals(left, right, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/FunctionCallExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/FunctionCallExpressionNode.cs new file mode 100644 index 00000000000..20c59ee549c --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/FunctionCallExpressionNode.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Evaluates a function expression, such as "Exists('foo')" + /// + internal sealed class FunctionCallExpressionNode : OperatorExpressionNode + { + private ArrayList _arguments; + private string _functionName; + + private FunctionCallExpressionNode() { } + + internal FunctionCallExpressionNode(string functionName, ArrayList arguments) + { + _functionName = functionName; + _arguments = arguments; + } + + /// + /// Evaluate node as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + if (String.Compare(_functionName, "exists", StringComparison.OrdinalIgnoreCase) == 0) + { + // Check we only have one argument + VerifyArgumentCount(1, state); + + // Expand properties and items, and verify the result is an appropriate scalar + string expandedValue = ExpandArgumentForScalarParameter("exists", (GenericExpressionNode)_arguments[0], state); + + if (String.IsNullOrEmpty(expandedValue)) + { + return false; + } + + try + { + if (state.EvaluationDirectory != null && !Path.IsPathRooted(expandedValue)) + { + expandedValue = Path.GetFullPath(Path.Combine(state.EvaluationDirectory, expandedValue)); + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + // Ignore invalid characters or path related exceptions + + // We will ignore the PathTooLong exception caused by GetFullPath becasue in single proc this code + // is not executed and the condition is just evaluated to false as File.Exists and Directory.Exists does not throw in this situation. + // To be consistant with that we will return a false in this case also. + // DevDiv Bugs: 46035 + + return false; + } + + if (state.LoadedProjectsCache != null && state.LoadedProjectsCache.TryGet(expandedValue) != null) + { + return true; + } + + bool exists = FileUtilities.FileOrDirectoryExistsNoThrow(expandedValue); + + return exists; + } + else if (String.Compare(_functionName, "HasTrailingSlash", StringComparison.OrdinalIgnoreCase) == 0) + { + // Check we only have one argument + VerifyArgumentCount(1, state); + + // Expand properties and items, and verify the result is an appropriate scalar + string expandedValue = ExpandArgumentForScalarParameter("HasTrailingSlash", (GenericExpressionNode)_arguments[0], state); + + // Is the last character a backslash? + if (expandedValue.Length != 0) + { + char lastCharacter = expandedValue[expandedValue.Length - 1]; + // Either back or forward slashes satisfy the function: this is useful for URL's + return (lastCharacter == Path.DirectorySeparatorChar || lastCharacter == Path.AltDirectorySeparatorChar); + } + else + { + return false; + } + } + // We haven't implemented any other "functions" + else + { + ProjectErrorUtilities.VerifyThrowInvalidProject( + false, + state.ElementLocation, + "UndefinedFunctionCall", + state.Condition, + _functionName); + + return false; + } + } + + /// + /// Expands properties and items in the argument, and verifies that the result is consistent + /// with a scalar parameter type. + /// + /// Function name for errors + /// Argument to be expanded + /// Scalar result + private string ExpandArgumentForScalarParameter(string function, GenericExpressionNode argumentNode, ConditionEvaluator.IConditionEvaluationState state) + { + string argument = argumentNode.GetUnexpandedValue(state); + + IList items; + + items = state.ExpandIntoTaskItems(argument); + + string expandedValue = String.Empty; + + if (items.Count == 0) + { + // Empty argument, that's fine. + } + else if (items.Count == 1) + { + expandedValue = items[0].ItemSpec; + } + else // too many items for the function + { + // We only allow a single item to be passed into a scalar parameter. + ProjectErrorUtilities.ThrowInvalidProject( + state.ElementLocation, + "CannotPassMultipleItemsIntoScalarFunction", function, argument, + state.ExpandIntoString(argument)); + } + + return expandedValue; + } + + /// + /// Check that the number of function arguments is correct. + /// + /// + private void VerifyArgumentCount(int expected, ConditionEvaluator.IConditionEvaluationState state) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + (_arguments.Count == expected, + state.ElementLocation, + "IncorrectNumberOfFunctionArguments", + state.Condition, + _arguments.Count, + expected); + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/GenericExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/GenericExpressionNode.cs new file mode 100644 index 00000000000..359261a8d54 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/GenericExpressionNode.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Base class for all expression nodes. + /// + internal abstract class GenericExpressionNode + { + internal abstract bool CanBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state); + internal abstract bool CanNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state); + internal abstract bool CanVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state); + internal abstract bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state); + internal abstract double NumericEvaluate(ConditionEvaluator.IConditionEvaluationState state); + internal abstract Version VersionEvaluate(ConditionEvaluator.IConditionEvaluationState state); + + /// + /// Returns true if this node evaluates to an empty string, + /// otherwise false. + /// (It may be cheaper to determine whether an expression will evaluate + /// to empty than to fully evaluate it.) + /// Implementations should cache the result so that calls after the first are free. + /// + internal virtual bool EvaluatesToEmpty(ConditionEvaluator.IConditionEvaluationState state) + { + return false; + } + + /// + /// Value after any item and property expressions are expanded + /// + /// + internal abstract string GetExpandedValue(ConditionEvaluator.IConditionEvaluationState state); + + /// + /// Value before any item and property expressions are expanded + /// + /// + internal abstract string GetUnexpandedValue(ConditionEvaluator.IConditionEvaluationState state); + + /// + /// If any expression nodes cache any state for the duration of evaluation, + /// now's the time to clean it up + /// + internal abstract void ResetState(); + + /// + /// The main evaluate entry point for expression trees + /// + /// + /// + internal bool Evaluate(ConditionEvaluator.IConditionEvaluationState state) + { + ProjectErrorUtilities.VerifyThrowInvalidProject( + CanBoolEvaluate(state), + state.ElementLocation, + "ConditionNotBooleanDetail", + state.Condition, + GetExpandedValue(state)); + + return BoolEvaluate(state); + } + + #region REMOVE_COMPAT_WARNING + internal virtual bool PossibleAndCollision + { + set { /* do nothing */ } + get { return false; } + } + + internal virtual bool PossibleOrCollision + { + set { /* do nothing */ } + get { return false; } + } + + internal bool PotentialAndOrConflict() + { + // The values of the functions are assigned to boolean locals + // in order to force evaluation of the functions even when the + // first one returns false + bool detectOr = DetectOr(); + bool detectAnd = DetectAnd(); + return (detectOr && detectAnd); + } + + internal abstract bool DetectOr(); + internal abstract bool DetectAnd(); + #endregion + + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanExpressionNode.cs new file mode 100644 index 00000000000..00e12fe24f2 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanExpressionNode.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Compares for left > right + /// + internal sealed class GreaterThanExpressionNode : NumericComparisonExpressionNode + { + /// + /// Compare numerically + /// + protected override bool Compare(double left, double right) + { + return left > right; + } + + /// + /// Compare Versions. This is only intended to compare version formats like "A.B.C.D" which can otherwise not be compared numerically + /// + /// + protected override bool Compare(Version left, Version right) + { + return left > right; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(Version left, double right) + { + if (left.Major != right) + { + return left.Major > right; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return true; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(double left, Version right) + { + if (right.Major != left) + { + return left > right.Major; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return false; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanOrEqualExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanOrEqualExpressionNode.cs new file mode 100644 index 00000000000..7d446be1a60 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/GreaterThanOrEqualExpressionNode.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Compares for left >= right + /// + internal sealed class GreaterThanOrEqualExpressionNode : NumericComparisonExpressionNode + { + /// + /// Compare numerically + /// + protected override bool Compare(double left, double right) + { + return left >= right; + } + + /// + /// Compare Versions. This is only intended to compare version formats like "A.B.C.D" which can otherwise not be compared numerically + /// + /// + protected override bool Compare(Version left, Version right) + { + return left >= right; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(Version left, double right) + { + if (left.Major != right) + { + return left.Major >= right; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return true; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(double left, Version right) + { + if (right.Major != left) + { + return left >= right.Major; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return false; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/IItem.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/IItem.cs new file mode 100644 index 00000000000..5f7500336d1 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/IItem.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface representing an item without exposing its type. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This interface represents an item without exposing its type. + /// It's convenient to not genericise the base interface, to make it easier to use + /// for the majority of code that doesn't call these methods. + /// + /// Type of metadata object. + internal interface IItem : IItem + where M : class, IMetadatum + { + /// + /// Gets any existing metadatum on the item, or + /// else any on an applicable item definition. + /// + M GetMetadata(string name); + + /// + /// Sets the specified metadata. + /// Predecessor is any preceding overridden metadata + /// + M SetMetadata(ProjectMetadataElement metadataElement, string evaluatedValue); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/LessThanExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/LessThanExpressionNode.cs new file mode 100644 index 00000000000..1e6d11e37a9 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/LessThanExpressionNode.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Compares for left < right + /// + internal sealed class LessThanExpressionNode : NumericComparisonExpressionNode + { + /// + /// Compare numerically + /// + protected override bool Compare(double left, double right) + { + return left < right; + } + + /// + /// Compare Versions. This is only intended to compare version formats like "A.B.C.D" which can otherwise not be compared numerically + /// + /// + protected override bool Compare(Version left, Version right) + { + return left < right; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(Version left, double right) + { + if (left.Major != right) + { + return left.Major < right; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return false; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(double left, Version right) + { + if (right.Major != left) + { + return left < right.Major; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return true; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/LessThanOrEqualExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/LessThanOrEqualExpressionNode.cs new file mode 100644 index 00000000000..4c1b623e229 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/LessThanOrEqualExpressionNode.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Compares for left <= right + /// + internal sealed class LessThanOrEqualExpressionNode : NumericComparisonExpressionNode + { + /// + /// Compare numerically + /// + protected override bool Compare(double left, double right) + { + return left <= right; + } + + /// + /// Compare Versions. This is only intended to compare version formats like "A.B.C.D" which can otherwise not be compared numerically + /// + /// + protected override bool Compare(Version left, Version right) + { + return left <= right; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(Version left, double right) + { + if (left.Major != right) + { + return left.Major <= right; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return false; + } + + /// + /// Compare mixed numbers and Versions + /// + protected override bool Compare(double left, Version right) + { + if (right.Major != left) + { + return left <= right.Major; + } + + // If they have same "major" number, then that means we are comparing something like "6.X.Y.Z" to "6". Version treats the objects with more dots as + // "larger" regardless of what those dots are (e.g. 6.0.0.0 > 6 is a true statement) + return true; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/MultipleComparisonExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/MultipleComparisonExpressionNode.cs new file mode 100644 index 00000000000..0f4d3c0aa4a --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/MultipleComparisonExpressionNode.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Evaluates as boolean and evaluates children as boolean, numeric, or string. + /// Order in which comparisons are attempted is numeric, boolean, then string. + /// Updates conditioned properties table. + /// + internal abstract class MultipleComparisonNode : OperatorExpressionNode + { + private bool _conditionedPropertiesUpdated = false; + + /// + /// Compare numbers + /// + protected abstract bool Compare(double left, double right); + + /// + /// Compare booleans + /// + protected abstract bool Compare(bool left, bool right); + + /// + /// Compare strings + /// + protected abstract bool Compare(string left, string right); + + /// + /// Evaluates as boolean and evaluates children as boolean, numeric, or string. + /// Order in which comparisons are attempted is numeric, boolean, then string. + /// Updates conditioned properties table. + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + (LeftChild != null && RightChild != null, + state.ElementLocation, + "IllFormedCondition", + state.Condition); + + // It's sometimes possible to bail out of expansion early if we just need to know whether + // the result is empty string. + // If at least one of the left or the right hand side will evaluate to empty, + // and we know which do, then we already have enough information to evaluate this expression. + // That means we don't have to fully expand a condition like " '@(X)' == '' " + // which is a performance advantage if @(X) is a huge item list. + if (LeftChild.EvaluatesToEmpty(state) || RightChild.EvaluatesToEmpty(state)) + { + UpdateConditionedProperties(state); + + return Compare(LeftChild.EvaluatesToEmpty(state), RightChild.EvaluatesToEmpty(state)); + } + + if (LeftChild.CanNumericEvaluate(state) && RightChild.CanNumericEvaluate(state)) + { + return Compare(LeftChild.NumericEvaluate(state), RightChild.NumericEvaluate(state)); + } + else if (LeftChild.CanBoolEvaluate(state) && RightChild.CanBoolEvaluate(state)) + { + return Compare(LeftChild.BoolEvaluate(state), RightChild.BoolEvaluate(state)); + } + else // string comparison + { + string leftExpandedValue = LeftChild.GetExpandedValue(state); + string rightExpandedValue = RightChild.GetExpandedValue(state); + + ProjectErrorUtilities.VerifyThrowInvalidProject + (leftExpandedValue != null && rightExpandedValue != null, + state.ElementLocation, + "IllFormedCondition", + state.Condition); + + UpdateConditionedProperties(state); + + return Compare(leftExpandedValue, rightExpandedValue); + } + } + + /// + /// Reset temporary state + /// + internal override void ResetState() + { + base.ResetState(); + _conditionedPropertiesUpdated = false; + } + + /// + /// Updates the conditioned properties table if it hasn't already been done. + /// + private void UpdateConditionedProperties(ConditionEvaluator.IConditionEvaluationState state) + { + if (!_conditionedPropertiesUpdated && state.ConditionedPropertiesInProject != null) + { + string leftUnexpandedValue = LeftChild.GetUnexpandedValue(state); + string rightUnexpandedValue = RightChild.GetUnexpandedValue(state); + + if (leftUnexpandedValue != null) + { + ConditionEvaluator.UpdateConditionedPropertiesTable + (state.ConditionedPropertiesInProject, + leftUnexpandedValue, + RightChild.GetExpandedValue(state)); + } + + if (rightUnexpandedValue != null) + { + ConditionEvaluator.UpdateConditionedPropertiesTable + (state.ConditionedPropertiesInProject, + rightUnexpandedValue, + LeftChild.GetExpandedValue(state)); + } + + _conditionedPropertiesUpdated = true; + } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/NotEqualExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/NotEqualExpressionNode.cs new file mode 100644 index 00000000000..7277a89a3f4 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/NotEqualExpressionNode.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Compares for inequality + /// + internal sealed class NotEqualExpressionNode : MultipleComparisonNode + { + /// + /// Compare numbers + /// + protected override bool Compare(double left, double right) + { + return left != right; + } + + /// + /// Compare booleans + /// + protected override bool Compare(bool left, bool right) + { + return left != right; + } + + /// + /// Compare strings + /// + protected override bool Compare(string left, string right) + { + return !String.Equals(left, right, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/NotExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/NotExpressionNode.cs new file mode 100644 index 00000000000..5f5b328101e --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/NotExpressionNode.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Performs logical NOT on left child + /// Does not update conditioned properties table + /// + internal sealed class NotExpressionNode : OperatorExpressionNode + { + /// + /// Evaluate as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return !LeftChild.BoolEvaluate(state); + } + + internal override bool CanBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return LeftChild.CanBoolEvaluate(state); + } + + /// + /// Returns unexpanded value with '!' prepended. Useful for error messages. + /// + internal override string GetUnexpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return "!" + LeftChild.GetUnexpandedValue(state); + } + + /// + /// Returns expanded value with '!' prepended. Useful for error messages. + /// + internal override string GetExpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return "!" + LeftChild.GetExpandedValue(state); + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/NumericComparisonExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/NumericComparisonExpressionNode.cs new file mode 100644 index 00000000000..33bcefd2607 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/NumericComparisonExpressionNode.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Evaluates a numeric comparison, such as less-than, or greater-or-equal-than + /// Does not update conditioned properties table. + /// + internal abstract class NumericComparisonExpressionNode : OperatorExpressionNode + { + /// + /// Compare numbers + /// + protected abstract bool Compare(double left, double right); + + /// + /// Compare Versions. This is only intended to compare version formats like "A.B.C.D" which can otherwise not be compared numerically + /// + protected abstract bool Compare(Version left, Version right); + + /// + /// Compare mixed numbers and Versions + /// + protected abstract bool Compare(Version left, double right); + + /// + /// Compare mixed numbers and Versions + /// + protected abstract bool Compare(double left, Version right); + + /// + /// Evaluate as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + bool isLeftNum = LeftChild.CanNumericEvaluate(state); + bool isLeftVersion = LeftChild.CanVersionEvaluate(state); + bool isRightNum = RightChild.CanNumericEvaluate(state); + bool isRightVersion = RightChild.CanVersionEvaluate(state); + bool isNumeric = isLeftNum && isRightNum; + bool isVersion = isLeftVersion && isRightVersion; + bool isValidComparison = isNumeric || isVersion || (isLeftNum && isRightVersion) || (isLeftVersion && isRightNum); + + ProjectErrorUtilities.VerifyThrowInvalidProject + (isValidComparison, + state.ElementLocation, + "ComparisonOnNonNumericExpression", + state.Condition, + /* helpfully display unexpanded token and expanded result in error message */ + (LeftChild.CanNumericEvaluate(state) ? RightChild.GetUnexpandedValue(state) : LeftChild.GetUnexpandedValue(state)), + (LeftChild.CanNumericEvaluate(state) ? RightChild.GetExpandedValue(state) : LeftChild.GetExpandedValue(state))); + + // If the values identify as numeric, make that comparison instead of the Version comparison since numeric has a stricter definition + if (isNumeric) + { + return Compare(LeftChild.NumericEvaluate(state), RightChild.NumericEvaluate(state)); + } + else if (isVersion) + { + return Compare(LeftChild.VersionEvaluate(state), RightChild.VersionEvaluate(state)); + } + + // If the numbers are of a mixed type, call that specific Compare method + if (isLeftNum && isRightVersion) + { + return Compare(LeftChild.NumericEvaluate(state), RightChild.VersionEvaluate(state)); + } + else if (isLeftVersion && isRightNum) + { + return Compare(LeftChild.VersionEvaluate(state), RightChild.NumericEvaluate(state)); + } + + // Throw error here as this code should be unreachable + ErrorUtilities.ThrowInternalErrorUnreachable(); + return false; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/NumericExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/NumericExpressionNode.cs new file mode 100644 index 00000000000..fc14beb9d19 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/NumericExpressionNode.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Represents a number - evaluates as numeric. + /// + internal sealed class NumericExpressionNode : OperandExpressionNode + { + private string _value; + + private NumericExpressionNode() { } + + internal NumericExpressionNode(string value) + { + _value = value; + } + + /// + /// Evaluate as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + // Should be unreachable: all calls check CanBoolEvaluate() first + ErrorUtilities.VerifyThrow(false, "Can't evaluate a numeric expression as boolean."); + return false; + } + + /// + /// Evaluate as numeric + /// + internal override double NumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return ConversionUtilities.ConvertDecimalOrHexToDouble(_value); + } + + /// + /// Evaluate as a Version + /// + internal override Version VersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return Version.Parse(_value); + } + + /// + /// Whether it can be evaluated as a boolean: never allowed for numerics + /// + internal override bool CanBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + // Numeric expressions are never allowed to be treated as booleans. + return false; + } + + /// + /// Whether it can be evaluated as numeric + /// + internal override bool CanNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + // It is not always possible to numerically evaluate even a numerical expression - + // for example, it may overflow a double. So check here. + return ConversionUtilities.ValidDecimalOrHexNumber(_value); + } + + /// + /// Whether it can be evaluated as a Version + /// + internal override bool CanVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + // Check if the value can be formatted as a Version number + // This is needed for nodes that identify as Numeric but can't be parsed as numbers (e.g. 8.1.1.0 vs 8.1) + Version unused; + return Version.TryParse(_value, out unused); + } + + /// + /// Get the unexpanded value + /// + internal override string GetUnexpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return _value; + } + + /// + /// Get the expanded value + /// + internal override string GetExpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return _value; + } + + /// + /// If any expression nodes cache any state for the duration of evaluation, + /// now's the time to clean it up + /// + internal override void ResetState() + { + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/OperandExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/OperandExpressionNode.cs new file mode 100644 index 00000000000..f7b42422605 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/OperandExpressionNode.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Base class for all nodes that are operands (are leaves in the parse tree) + /// + internal abstract class OperandExpressionNode : GenericExpressionNode + { + #region REMOVE_COMPAT_WARNING + + internal override bool DetectAnd() + { + return false; + } + + internal override bool DetectOr() + { + return false; + } + #endregion + + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/OperatorExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/OperatorExpressionNode.cs new file mode 100644 index 00000000000..77cc0a05d84 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/OperatorExpressionNode.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Base class for nodes that are operators (have children in the parse tree) + /// + internal abstract class OperatorExpressionNode : GenericExpressionNode + { + /// + /// Storage for the left and right children of the operator + /// + private GenericExpressionNode _leftChild, _rightChild; + + /// + /// Numeric evaluation is never allowed for operators + /// + internal override double NumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + // Should be unreachable: all calls check CanNumericEvaluate() first + ErrorUtilities.VerifyThrow(false, "Cannot numeric evaluate an operator"); + return 0.0D; + } + + /// + /// Version evaluation is never allowed for operators + /// + internal override Version VersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + ErrorUtilities.VerifyThrow(false, "Cannot version evaluate an operator"); + return null; + } + + /// + /// Whether boolean evaluation is allowed: always allowed for operators + /// + internal override bool CanBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return true; + } + + /// + /// Whether the node can be evaluated as a numeric: by default, + /// this is not allowed + /// + internal override bool CanNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return false; + } + + /// + /// Whether the node can be evaluated as a version: by default, + /// this is not allowed + /// + internal override bool CanVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return false; + } + + /// + /// Value after any item and property expressions are expanded + /// + /// + internal override string GetExpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return null; + } + + /// + /// Value before any item and property expressions are expanded + /// + /// + internal override string GetUnexpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return null; + } + + /// + /// If any expression nodes cache any state for the duration of evaluation, + /// now's the time to clean it up + /// + internal override void ResetState() + { + if (_leftChild != null) + { + _leftChild.ResetState(); + } + + if (_rightChild != null) + { + _rightChild.ResetState(); + } + } + + /// + /// Storage for the left child + /// + internal GenericExpressionNode LeftChild + { + set { _leftChild = value; } + get { return _leftChild; } + } + + /// + /// Storage for the right child + /// + internal GenericExpressionNode RightChild + { + set { _rightChild = value; } + get { return _rightChild; } + } + + #region REMOVE_COMPAT_WARNING + internal override bool DetectAnd() + { + // Read the state of the current node + bool detectedAnd = this.PossibleAndCollision; + // Reset the flags on the current node + this.PossibleAndCollision = false; + // Process the children of the node if preset + bool detectAndRChild = false; + bool detectAndLChild = false; + if (RightChild != null) + { + detectAndRChild = RightChild.DetectAnd(); + } + if (LeftChild != null) + { + detectAndLChild = LeftChild.DetectAnd(); + } + return detectedAnd || detectAndRChild || detectAndLChild; + } + + internal override bool DetectOr() + { + // Read the state of the current node + bool detectedOr = this.PossibleOrCollision; + // Reset the flags on the current node + this.PossibleOrCollision = false; + // Process the children of the node if preset + bool detectOrRChild = false; + bool detectOrLChild = false; + if (RightChild != null) + { + detectOrRChild = RightChild.DetectOr(); + } + if (LeftChild != null) + { + detectOrLChild = LeftChild.DetectOr(); + } + return detectedOr || detectOrRChild || detectOrLChild; + } + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/OrExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/OrExpressionNode.cs new file mode 100644 index 00000000000..ff18a5e1965 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/OrExpressionNode.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Performs logical OR on children + /// Does not update conditioned properties table + /// + internal sealed class OrExpressionNode : OperatorExpressionNode + { + /// + /// Evaluate as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + (LeftChild.CanBoolEvaluate(state), + state.ElementLocation, + "ExpressionDoesNotEvaluateToBoolean", + LeftChild.GetUnexpandedValue(state), + LeftChild.GetExpandedValue(state), + state.Condition); + + if (LeftChild.BoolEvaluate(state)) + { + // Short circuit + return true; + } + else + { + ProjectErrorUtilities.VerifyThrowInvalidProject + (RightChild.CanBoolEvaluate(state), + state.ElementLocation, + "ExpressionDoesNotEvaluateToBoolean", + RightChild.GetUnexpandedValue(state), + RightChild.GetExpandedValue(state), + state.Condition); + + return RightChild.BoolEvaluate(state); + } + } + + #region REMOVE_COMPAT_WARNING + private bool _possibleOrCollision = true; + internal override bool PossibleOrCollision + { + set { _possibleOrCollision = value; } + get { return _possibleOrCollision; } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/Parser.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/Parser.cs new file mode 100644 index 00000000000..502d832621b --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/Parser.cs @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using System.Collections; +using System.Xml; +using System; +using Microsoft.Build.Shared; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Evaluation +{ + using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; + + [Flags] + internal enum ParserOptions + { + None = 0x0, + AllowProperties = 0x1, + AllowItemLists = 0x2, + AllowPropertiesAndItemLists = AllowProperties | AllowItemLists, + AllowBuiltInMetadata = 0x4, + AllowCustomMetadata = 0x8, + AllowItemMetadata = AllowBuiltInMetadata | AllowCustomMetadata, + AllowPropertiesAndItemMetadata = AllowProperties | AllowItemMetadata, + AllowPropertiesAndCustomMetadata = AllowProperties | AllowCustomMetadata, + AllowAll = AllowProperties | AllowItemLists | AllowItemMetadata + }; + + /// + /// This class implements the grammar for complex conditionals. + /// + /// The usage is: + /// Parser p = new Parser(CultureInfo); + /// ExpressionTree t = p.Parse(expression, XmlNode); + /// + /// The expression tree can then be evaluated and re-evaluated as needed. + /// + /// + /// UNDONE: When we copied over the conditionals code, we didn't copy over the unit tests for scanner, parser, and expression tree. + /// + internal sealed class Parser + { + private Scanner _lexer; + private ParserOptions _options; + private ElementLocation _elementLocation; + internal int errorPosition = 0; // useful for unit tests + + #region REMOVE_COMPAT_WARNING + + private bool _warnedForExpression = false; + + private BuildEventContext _logBuildEventContext; + /// + /// Location contextual information which are attached to logging events to + /// say where they are in relation to the process, engine, project, target,task which is executing + /// + internal BuildEventContext LogBuildEventContext + { + get + { + return _logBuildEventContext; + } + set + { + _logBuildEventContext = value; + } + } + private ILoggingService _loggingServices; + /// + /// Engine Logging Service reference where events will be logged to + /// + internal ILoggingService LoggingServices + { + set + { + _loggingServices = value; + } + + get + { + return _loggingServices; + } + } + #endregion + + internal Parser() + { + // nothing to see here, move along. + } + + // + // Main entry point for parser. + // You pass in the expression you want to parse, and you get an + // ExpressionTree out the back end. + // + internal GenericExpressionNode Parse(string expression, ParserOptions optionSettings, ElementLocation elementLocation) + { + // We currently have no support (and no scenarios) for disallowing property references + // in Conditions. + ErrorUtilities.VerifyThrow(0 != (optionSettings & ParserOptions.AllowProperties), + "Properties should always be allowed."); + + _options = optionSettings; + _elementLocation = elementLocation; + + _lexer = new Scanner(expression, _options); + if (!_lexer.Advance()) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, _lexer.GetErrorResource(), expression, errorPosition, _lexer.UnexpectedlyFound); + } + GenericExpressionNode node = Expr(expression); + if (!_lexer.IsNext(Token.TokenType.EndOfInput)) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + return node; + } + + // + // Top node of grammar + // See grammar for how the following methods relate to each + // other. + // + private GenericExpressionNode Expr(string expression) + { + GenericExpressionNode node = BooleanTerm(expression); + if (!_lexer.IsNext(Token.TokenType.EndOfInput)) + { + node = ExprPrime(expression, node); + } + + + #region REMOVE_COMPAT_WARNING + // Check for potential change in behavior + if (LoggingServices != null && !_warnedForExpression && + node.PotentialAndOrConflict()) + { + // We only want to warn once even if there multiple () sub expressions + _warnedForExpression = true; + + // Log a warning regarding the fact the expression may have been evaluated + // incorrectly in earlier version of MSBuild + LoggingServices.LogWarning(_logBuildEventContext, null, new BuildEventFileInfo(_elementLocation), "ConditionMaybeEvaluatedIncorrectly", expression); + } + #endregion + + return node; + } + + private GenericExpressionNode ExprPrime(string expression, GenericExpressionNode lhs) + { + if (Same(expression, Token.TokenType.EndOfInput)) + { + return lhs; + } + else if (Same(expression, Token.TokenType.Or)) + { + OperatorExpressionNode orNode = new OrExpressionNode(); + GenericExpressionNode rhs = BooleanTerm(expression); + orNode.LeftChild = lhs; + orNode.RightChild = rhs; + return ExprPrime(expression, orNode); + } + else + { + // I think this is ok. ExprPrime always shows up at + // the rightmost side of the grammar rhs, the EndOfInput case + // takes care of things + return lhs; + } + } + + private GenericExpressionNode BooleanTerm(string expression) + { + GenericExpressionNode node = RelationalExpr(expression); + if (null == node) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + + if (!_lexer.IsNext(Token.TokenType.EndOfInput)) + { + node = BooleanTermPrime(expression, node); + } + return node; + } + + private GenericExpressionNode BooleanTermPrime(string expression, GenericExpressionNode lhs) + { + if (_lexer.IsNext(Token.TokenType.EndOfInput)) + { + return lhs; + } + else if (Same(expression, Token.TokenType.And)) + { + GenericExpressionNode rhs = RelationalExpr(expression); + if (null == rhs) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + + OperatorExpressionNode andNode = new AndExpressionNode(); + andNode.LeftChild = lhs; + andNode.RightChild = rhs; + return BooleanTermPrime(expression, andNode); + } + else + { + // Should this be error case? + return lhs; + } + } + + private GenericExpressionNode RelationalExpr(string expression) + { + { + GenericExpressionNode lhs = Factor(expression); + if (null == lhs) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + + OperatorExpressionNode node = RelationalOperation(expression); + if (node == null) + { + return lhs; + } + GenericExpressionNode rhs = Factor(expression); + node.LeftChild = lhs; + node.RightChild = rhs; + return node; + } + } + + + private OperatorExpressionNode RelationalOperation(string expression) + { + OperatorExpressionNode node = null; + if (Same(expression, Token.TokenType.LessThan)) + { + node = new LessThanExpressionNode(); + } + else if (Same(expression, Token.TokenType.GreaterThan)) + { + node = new GreaterThanExpressionNode(); + } + else if (Same(expression, Token.TokenType.LessThanOrEqualTo)) + { + node = new LessThanOrEqualExpressionNode(); + } + else if (Same(expression, Token.TokenType.GreaterThanOrEqualTo)) + { + node = new GreaterThanOrEqualExpressionNode(); + } + else if (Same(expression, Token.TokenType.EqualTo)) + { + node = new EqualExpressionNode(); + } + else if (Same(expression, Token.TokenType.NotEqualTo)) + { + node = new NotEqualExpressionNode(); + } + return node; + } + + private GenericExpressionNode Factor(string expression) + { + // Checks for TokenTypes String, Numeric, Property, ItemMetadata, and ItemList. + GenericExpressionNode arg = this.Arg(expression); + + // If it's one of those, return it. + if (arg != null) + { + return arg; + } + + // If it's not one of those, check for other TokenTypes. + Token current = _lexer.CurrentToken; + if (Same(expression, Token.TokenType.Function)) + { + if (!Same(expression, Token.TokenType.LeftParenthesis)) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", _lexer.IsNextString(), errorPosition); + return null; + } + ArrayList arglist = new ArrayList(); + Arglist(expression, arglist); + if (!Same(expression, Token.TokenType.RightParenthesis)) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + return null; + } + return new FunctionCallExpressionNode(current.String, arglist); + } + else if (Same(expression, Token.TokenType.LeftParenthesis)) + { + GenericExpressionNode child = Expr(expression); + if (Same(expression, Token.TokenType.RightParenthesis)) + return child; + else + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + } + else if (Same(expression, Token.TokenType.Not)) + { + OperatorExpressionNode notNode = new NotExpressionNode(); + GenericExpressionNode expr = Factor(expression); + if (expr == null) + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + notNode.LeftChild = expr; + return notNode; + } + else + { + errorPosition = _lexer.GetErrorPosition(); + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, "UnexpectedTokenInCondition", expression, _lexer.IsNextString(), errorPosition); + } + return null; + } + + private void Arglist(string expression, ArrayList arglist) + { + if (!_lexer.IsNext(Token.TokenType.RightParenthesis)) + Args(expression, arglist); + } + + private void Args(string expression, ArrayList arglist) + { + GenericExpressionNode arg = Arg(expression); + arglist.Add(arg); + if (Same(expression, Token.TokenType.Comma)) + { + Args(expression, arglist); + } + } + + private GenericExpressionNode Arg(string expression) + { + Token current = _lexer.CurrentToken; + if (Same(expression, Token.TokenType.String)) + { + return new StringExpressionNode(current.String, current.Expandable); + } + else if (Same(expression, Token.TokenType.Numeric)) + { + return new NumericExpressionNode(current.String); + } + else if (Same(expression, Token.TokenType.Property)) + { + return new StringExpressionNode(current.String, true /* requires expansion */); + } + else if (Same(expression, Token.TokenType.ItemMetadata)) + { + return new StringExpressionNode(current.String, true /* requires expansion */); + } + else if (Same(expression, Token.TokenType.ItemList)) + { + return new StringExpressionNode(current.String, true /* requires expansion */); + } + else + { + return null; + } + } + + private bool Same(string expression, Token.TokenType token) + { + if (_lexer.IsNext(token)) + { + if (!_lexer.Advance()) + { + errorPosition = _lexer.GetErrorPosition(); + if (null != _lexer.UnexpectedlyFound) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, _lexer.GetErrorResource(), expression, errorPosition, _lexer.UnexpectedlyFound); + } + else + { + ProjectErrorUtilities.VerifyThrowInvalidProject(false, _elementLocation, _lexer.GetErrorResource(), expression, errorPosition); + } + } + return true; + } + else + { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/Scanner.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/Scanner.cs new file mode 100644 index 00000000000..8f5a8c48381 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/Scanner.cs @@ -0,0 +1,716 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using System; +using System.Diagnostics; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Class: Scanner + /// This class does the scanning of the input and returns tokens. + /// The usage pattern is: + /// Scanner s = new Scanner(expression, CultureInfo) + /// do { + /// s.Advance(); + /// while (s.IsNext(Token.EndOfInput)); + /// + /// After Advance() is called, you can get the current token (s.CurrentToken), + /// check it's type (s.IsNext()), get the string for it (s.NextString()). + /// + internal sealed class Scanner + { + private string _expression; + private int _parsePoint; + private Token _lookahead; + private bool _errorState; + private int _errorPosition; + // What we found instead of what we were looking for + private string _unexpectedlyFound = null; + private ParserOptions _options; + private string _errorResource = null; + private static string s_endOfInput = null; + + /// + /// Lazily format resource string to help avoid (in some perf critical cases) even loading + /// resources at all. + /// + private string EndOfInput + { + get + { + if (s_endOfInput == null) + { + s_endOfInput = ResourceUtilities.FormatResourceString("EndOfInputTokenName"); + } + + return s_endOfInput; + } + } + + private Scanner() { } + // + // Constructor takes the string to parse and the culture. + // + internal Scanner(string expressionToParse, ParserOptions options) + { + // We currently have no support (and no scenarios) for disallowing property references + // in Conditions. + ErrorUtilities.VerifyThrow(0 != (options & ParserOptions.AllowProperties), + "Properties should always be allowed."); + + _expression = expressionToParse; + _parsePoint = 0; + _errorState = false; + _errorPosition = -1; // invalid + _options = options; + } + + /// + /// If the lexer errors, it has the best knowledge of the error message to show. For example, + /// 'unexpected character' or 'illformed operator'. This method returns the name of the resource + /// string that the parser should display. + /// + /// Intentionally not a property getter to avoid the debugger triggering the Assert dialog + /// + internal string GetErrorResource() + { + if (_errorResource == null) + { + // I do not believe this is reachable, but provide a reasonable default. + Debug.Assert(false, "What code path did not set an appropriate error resource? Expression: " + _expression); + _unexpectedlyFound = EndOfInput; + return "UnexpectedCharacterInCondition"; + } + else + { + return _errorResource; + } + } + + internal bool IsNext(Token.TokenType type) + { + return _lookahead.IsToken(type); + } + + internal string IsNextString() + { + return _lookahead.String; + } + + internal Token CurrentToken + { + get { return _lookahead; } + } + + internal int GetErrorPosition() + { + Debug.Assert(-1 != _errorPosition); // We should have set it + return _errorPosition; + } + + // The string (usually a single character) we found unexpectedly. + // We might want to show it in the error message, to help the user spot the error. + internal string UnexpectedlyFound + { + get + { + return _unexpectedlyFound; + } + } + + /// + /// Advance + /// returns true on successful advance + /// and false on an erroneous token + /// + /// Doesn't return error until the bogus input is encountered. + /// Advance() returns true even after EndOfInput is encountered. + /// + internal bool Advance() + { + if (_errorState) + return false; + + if (_lookahead != null && _lookahead.IsToken(Token.TokenType.EndOfInput)) + return true; + + SkipWhiteSpace(); + + // Update error position after skipping whitespace + _errorPosition = _parsePoint + 1; + + if (_parsePoint >= _expression.Length) + { + _lookahead = Token.EndOfInput; + } + else + { + switch (_expression[_parsePoint]) + { + case ',': + _lookahead = Token.Comma; + _parsePoint++; + break; + case '(': + _lookahead = Token.LeftParenthesis; + _parsePoint++; + break; + case ')': + _lookahead = Token.RightParenthesis; + _parsePoint++; + break; + case '$': + if (!ParseProperty()) + return false; + break; + case '%': + if (!ParseItemMetadata()) + return false; + break; + case '@': + int start = _parsePoint; + // If the caller specified that he DOESN'T want to allow item lists ... + if ((_options & ParserOptions.AllowItemLists) == 0) + { + if ((_parsePoint + 1) < _expression.Length && _expression[_parsePoint + 1] == '(') + { + _errorPosition = start + 1; + _errorState = true; + _errorResource = "ItemListNotAllowedInThisConditional"; + return false; + } + } + if (!ParseItemList()) + return false; + break; + case '!': + // negation and not-equal + if ((_parsePoint + 1) < _expression.Length && _expression[_parsePoint + 1] == '=') + { + _lookahead = Token.NotEqualTo; + _parsePoint += 2; + } + else + { + _lookahead = Token.Not; + _parsePoint++; + } + break; + case '>': + // gt and gte + if ((_parsePoint + 1) < _expression.Length && _expression[_parsePoint + 1] == '=') + { + _lookahead = Token.GreaterThanOrEqualTo; + _parsePoint += 2; + } + else + { + _lookahead = Token.GreaterThan; + _parsePoint++; + } + break; + case '<': + // lt and lte + if ((_parsePoint + 1) < _expression.Length && _expression[_parsePoint + 1] == '=') + { + _lookahead = Token.LessThanOrEqualTo; + _parsePoint += 2; + } + else + { + _lookahead = Token.LessThan; + _parsePoint++; + } + break; + case '=': + if ((_parsePoint + 1) < _expression.Length && _expression[_parsePoint + 1] == '=') + { + _lookahead = Token.EqualTo; + _parsePoint += 2; + } + else + { + _errorPosition = _parsePoint + 2; // expression[parsePoint + 1], counting from 1 + _errorResource = "IllFormedEqualsInCondition"; + if ((_parsePoint + 1) < _expression.Length) + { + // store the char we found instead + _unexpectedlyFound = Convert.ToString(_expression[_parsePoint + 1], CultureInfo.InvariantCulture); + } + else + { + _unexpectedlyFound = EndOfInput; + } + _parsePoint++; + _errorState = true; + return false; + } + break; + case '\'': + if (!ParseQuotedString()) + return false; + break; + default: + // Simple strings, function calls, decimal numbers, hex numbers + if (!ParseRemaining()) + return false; + break; + } + } + return true; + } + + /// + /// Parses either the $(propertyname) syntax or the %(metadataname) syntax, + /// and returns the parsed string beginning with the '$' or '%', and ending with the + /// closing parenthesis. + /// + /// + private string ParsePropertyOrItemMetadata() + { + int start = _parsePoint; // set start so that we include "$(" or "%(" + _parsePoint++; + + if (_parsePoint < _expression.Length && _expression[_parsePoint] != '(') + { + _errorState = true; + _errorPosition = start + 1; + _errorResource = "IllFormedPropertyOpenParenthesisInCondition"; + _unexpectedlyFound = Convert.ToString(_expression[_parsePoint], CultureInfo.InvariantCulture); + return null; + } + + _parsePoint = ScanForPropertyExpressionEnd(_expression, _parsePoint++); + + // Maybe we need to generate an error for invalid characters in property/metadata name? + // For now, just wait and let the property/metadata evaluation handle the error case. + if (_parsePoint >= _expression.Length) + { + _errorState = true; + _errorPosition = start + 1; + _errorResource = "IllFormedPropertyCloseParenthesisInCondition"; + _unexpectedlyFound = EndOfInput; + return null; + } + + _parsePoint++; + return _expression.Substring(start, _parsePoint - start); + } + + /// + /// Scan for the end of the property expression + /// + private static int ScanForPropertyExpressionEnd(string expression, int index) + { + int nestLevel = 0; + + unsafe + { + fixed (char* pchar = expression) + { + while (index < expression.Length) + { + char character = pchar[index]; + if (character == '(') + { + nestLevel++; + } + else if (character == ')') + { + nestLevel--; + } + + // We have reached the end of the parenthesis nesting + // this should be the end of the property expression + // If it is not then the calling code will determine that + if (nestLevel == 0) + { + return index; + } + else + { + index++; + } + } + } + } + return index; + } + + /// + /// Parses a string of the form $(propertyname). + /// + /// + private bool ParseProperty() + { + string propertyExpression = this.ParsePropertyOrItemMetadata(); + + if (propertyExpression == null) + { + return false; + } + else + { + _lookahead = new Token(Token.TokenType.Property, propertyExpression); + return true; + } + } + + /// + /// Parses a string of the form %(itemmetadataname). + /// + /// + private bool ParseItemMetadata() + { + string itemMetadataExpression = this.ParsePropertyOrItemMetadata(); + + if (itemMetadataExpression == null) + { + // The ParsePropertyOrItemMetadata method returns the correct error resources + // for parsing properties such as $(propertyname). At this stage in the Whidbey + // cycle, we're not allowed to add new string resources, so I can't add a new + // resource specific to item metadata, so here, we just change the error to + // the generic "UnexpectedCharacter". + _errorResource = "UnexpectedCharacterInCondition"; + return false; + } + + _lookahead = new Token(Token.TokenType.ItemMetadata, itemMetadataExpression); + + if (!CheckForUnexpectedMetadata(itemMetadataExpression)) + { + return false; + } + + return true; + } + + /// + /// Helper to verify that any AllowBuiltInMetadata or AllowCustomMetadata + /// specifications are not respected. + /// Returns true if it is ok, otherwise false. + /// + private bool CheckForUnexpectedMetadata(string expression) + { + if ((_options & ParserOptions.AllowItemMetadata) == ParserOptions.AllowItemMetadata) + { + return true; + } + + // Take off %( and ) + if (expression.Length > 3 && expression[0] == '%' && expression[1] == '(' && expression[expression.Length - 1] == ')') + { + expression = expression.Substring(2, expression.Length - 1 - 2); + } + + // If it's like %(a.b) find 'b' + int period = expression.IndexOf('.'); + if (period > 0 && period < expression.Length - 1) + { + expression = expression.Substring(period + 1); + } + + bool isItemSpecModifier = FileUtilities.ItemSpecModifiers.IsItemSpecModifier(expression); + + if (((_options & ParserOptions.AllowBuiltInMetadata) == 0) && + isItemSpecModifier) + { + _errorPosition = _parsePoint; + _errorState = true; + _errorResource = "BuiltInMetadataNotAllowedInThisConditional"; + _unexpectedlyFound = expression; + return false; + } + + if (((_options & ParserOptions.AllowCustomMetadata) == 0) && + !isItemSpecModifier) + { + _errorPosition = _parsePoint; + _errorState = true; + _errorResource = "CustomMetadataNotAllowedInThisConditional"; + _unexpectedlyFound = expression; + return false; + } + + return true; + } + + private bool ParseInternalItemList() + { + int start = _parsePoint; + _parsePoint++; + + if (_parsePoint < _expression.Length && _expression[_parsePoint] != '(') + { + // @ was not followed by ( + _errorPosition = start + 1; + _errorResource = "IllFormedItemListOpenParenthesisInCondition"; + // Not useful to set unexpectedlyFound here. The message is going to be detailed enough. + _errorState = true; + return false; + } + _parsePoint++; + // Maybe we need to generate an error for invalid characters in itemgroup name? + // For now, just let item evaluation handle the error. + bool fInReplacement = false; + while (_parsePoint < _expression.Length) + { + if (_expression[_parsePoint] == '\'') + { + fInReplacement = !fInReplacement; + } + else if (_expression[_parsePoint] == ')' && !fInReplacement) + { + break; + } + _parsePoint++; + } + if (_parsePoint >= _expression.Length) + { + _errorPosition = start + 1; + if (fInReplacement) + { + // @( ... ' was never followed by a closing quote before the closing parenthesis + _errorResource = "IllFormedItemListQuoteInCondition"; + } + else + { + // @( was never followed by a ) + _errorResource = "IllFormedItemListCloseParenthesisInCondition"; + } + // Not useful to set unexpectedlyFound here. The message is going to be detailed enough. + _errorState = true; + return false; + } + _parsePoint++; + return true; + } + + private bool ParseItemList() + { + int start = _parsePoint; + if (!ParseInternalItemList()) + { + return false; + } + _lookahead = new Token(Token.TokenType.ItemList, _expression.Substring(start, _parsePoint - start)); + return true; + } + + /// + /// Parse any part of the conditional expression that is quoted. It may contain a property, item, or + /// metadata element that needs expansion during evaluation. + /// + private bool ParseQuotedString() + { + _parsePoint++; + int start = _parsePoint; + bool expandable = false; + while (_parsePoint < _expression.Length && _expression[_parsePoint] != '\'') + { + // Standalone percent-sign must be allowed within a condition because it's + // needed to escape special characters. However, percent-sign followed + // by open-parenthesis is an indication of an item metadata reference, and + // that is only allowed in certain contexts. + if ((_expression[_parsePoint] == '%') && ((_parsePoint + 1) < _expression.Length) && (_expression[_parsePoint + 1] == '(')) + { + expandable = true; + string name = String.Empty; + + int endOfName = _expression.IndexOf(')', _parsePoint) - 1; + if (endOfName < 0) + { + endOfName = _expression.Length - 1; + } + + // If it's %(a.b) the name is just 'b' + if (_parsePoint + 3 < _expression.Length) + { + name = _expression.Substring(_parsePoint + 2, (endOfName - _parsePoint - 2 + 1)); + } + + if (!CheckForUnexpectedMetadata(name)) + { + return false; + } + } + else if (_expression[_parsePoint] == '@' && ((_parsePoint + 1) < _expression.Length) && (_expression[_parsePoint + 1] == '(')) + { + expandable = true; + + // If the caller specified that he DOESN'T want to allow item lists ... + if ((_options & ParserOptions.AllowItemLists) == 0) + { + _errorPosition = start + 1; + _errorState = true; + _errorResource = "ItemListNotAllowedInThisConditional"; + return false; + } + + // Item lists have to be parsed because of the replacement syntax e.g. @(Foo,'_'). + // I have to know how to parse those so I can skip over the tic marks. I don't + // have to do that with other things like propertygroups, hence itemlists are + // treated specially. + + ParseInternalItemList(); + continue; + } + else if (_expression[_parsePoint] == '$' && ((_parsePoint + 1) < _expression.Length) && (_expression[_parsePoint + 1] == '(')) + { + expandable = true; + } + else if (_expression[_parsePoint] == '%') + { + // There may be some escaped characters in the expression + expandable = true; + } + _parsePoint++; + } + + if (_parsePoint >= _expression.Length) + { + // Quoted string wasn't closed + _errorState = true; + _errorPosition = start; // The message is going to say "expected after position n" so don't add 1 here. + _errorResource = "IllFormedQuotedStringInCondition"; + // Not useful to set unexpectedlyFound here. By definition it got to the end of the string. + return false; + } + string originalTokenString = _expression.Substring(start, _parsePoint - start); + + _lookahead = new Token(Token.TokenType.String, originalTokenString, expandable); + _parsePoint++; + return true; + } + + private bool ParseRemaining() + { + int start = _parsePoint; + if (CharacterUtilities.IsNumberStart(_expression[_parsePoint])) // numeric + { + if (!ParseNumeric(start)) + return false; + } + else if (CharacterUtilities.IsSimpleStringStart(_expression[_parsePoint])) // simple string (handle 'and' and 'or') + { + if (!ParseSimpleStringOrFunction(start)) + return false; + } + else + { + // Something that wasn't a number or a letter, like a newline (%0a) + _errorState = true; + _errorPosition = start + 1; + _errorResource = "UnexpectedCharacterInCondition"; + _unexpectedlyFound = Convert.ToString(_expression[_parsePoint], CultureInfo.InvariantCulture); + return false; + } + return true; + } + + // There is a bug here that spaces are not required around 'and' and 'or'. For example, + // this works perfectly well: + // Condition="%(a.Identity)!=''and%(a.m)=='1'" + // Since people now depend on this behavior, we must not change it. + private bool ParseSimpleStringOrFunction(int start) + { + SkipSimpleStringChars(); + if (0 == string.Compare(_expression.Substring(start, _parsePoint - start), "and", StringComparison.OrdinalIgnoreCase)) + { + _lookahead = Token.And; + } + else if (0 == string.Compare(_expression.Substring(start, _parsePoint - start), "or", StringComparison.OrdinalIgnoreCase)) + { + _lookahead = Token.Or; + } + else + { + int end = _parsePoint; + SkipWhiteSpace(); + if (_parsePoint < _expression.Length && _expression[_parsePoint] == '(') + { + _lookahead = new Token(Token.TokenType.Function, _expression.Substring(start, end - start)); + } + else + { + string tokenValue = _expression.Substring(start, end - start); + _lookahead = new Token(Token.TokenType.String, tokenValue); + } + } + return true; + } + private bool ParseNumeric(int start) + { + if ((_expression.Length - _parsePoint) > 2 && _expression[_parsePoint] == '0' && (_expression[_parsePoint + 1] == 'x' || _expression[_parsePoint + 1] == 'X')) + { + // Hex number + _parsePoint += 2; + SkipHexDigits(); + _lookahead = new Token(Token.TokenType.Numeric, _expression.Substring(start, _parsePoint - start)); + } + else if (CharacterUtilities.IsNumberStart(_expression[_parsePoint])) + { + // Decimal number + if (_expression[_parsePoint] == '+') + { + _parsePoint++; + } + else if (_expression[_parsePoint] == '-') + { + _parsePoint++; + } + do + { + SkipDigits(); + if (_parsePoint < _expression.Length && _expression[_parsePoint] == '.') + { + _parsePoint++; + } + if (_parsePoint < _expression.Length) + { + SkipDigits(); + } + } while (_parsePoint < _expression.Length && _expression[_parsePoint] == '.'); + // Do we need to error on malformed input like 0.00.00)? or will the conversion handle it? + // For now, let the conversion generate the error. + _lookahead = new Token(Token.TokenType.Numeric, _expression.Substring(start, _parsePoint - start)); + } + else + { + // Unreachable + _errorState = true; + _errorPosition = start + 1; + return false; + } + return true; + } + private void SkipWhiteSpace() + { + while (_parsePoint < _expression.Length && char.IsWhiteSpace(_expression[_parsePoint])) + _parsePoint++; + return; + } + private void SkipDigits() + { + while (_parsePoint < _expression.Length && char.IsDigit(_expression[_parsePoint])) + _parsePoint++; + return; + } + private void SkipHexDigits() + { + while (_parsePoint < _expression.Length && CharacterUtilities.IsHexDigit(_expression[_parsePoint])) + _parsePoint++; + return; + } + private void SkipSimpleStringChars() + { + while (_parsePoint < _expression.Length && CharacterUtilities.IsSimpleStringChar(_expression[_parsePoint])) + _parsePoint++; + return; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/StringExpressionNode.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/StringExpressionNode.cs new file mode 100644 index 00000000000..7d9f832d041 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/StringExpressionNode.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.IO; +using System; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Node representing a string + /// + internal sealed class StringExpressionNode : OperandExpressionNode + { + private string _value; + private string _cachedExpandedValue; + + /// + /// Whether the string potentially has expandable content, + /// such as a property expression or escaped character. + /// + private bool _expandable; + + internal StringExpressionNode(string value, bool expandable) + { + _value = value; + _expandable = expandable; + } + + /// + /// Evaluate as boolean + /// + internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return ConversionUtilities.ConvertStringToBool(GetExpandedValue(state)); + } + + /// + /// Evaluate as numeric + /// + internal override double NumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return ConversionUtilities.ConvertDecimalOrHexToDouble(GetExpandedValue(state)); + } + + internal override Version VersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return Version.Parse(GetExpandedValue(state)); + } + + internal override bool CanBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return ConversionUtilities.CanConvertStringToBool(GetExpandedValue(state)); + } + + internal override bool CanNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + return ConversionUtilities.ValidDecimalOrHexNumber(GetExpandedValue(state)); + } + + internal override bool CanVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) + { + Version unused; + return Version.TryParse(GetExpandedValue(state), out unused); + } + + /// + /// Returns true if this node evaluates to an empty string, + /// otherwise false. + /// It may be cheaper to determine whether an expression will evaluate + /// to empty than to fully evaluate it. + /// Implementations should cache the result so that calls after the first are free. + /// + internal override bool EvaluatesToEmpty(ConditionEvaluator.IConditionEvaluationState state) + { + if (_cachedExpandedValue == null) + { + if (_expandable) + { + string expandBreakEarly = state.ExpandIntoStringBreakEarly(_value); + + if (expandBreakEarly == null) + { + // It broke early: we can't store the value, we just + // know it's non empty + return false; + } + + // It didn't break early, the result is accurate, + // so store it so the work isn't done again. + _cachedExpandedValue = expandBreakEarly; + } + else + { + _cachedExpandedValue = _value; + } + } + + return (_cachedExpandedValue.Length == 0); + } + + + /// + /// Value before any item and property expressions are expanded + /// + /// + internal override string GetUnexpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + return _value; + } + + /// + /// Value after any item and property expressions are expanded + /// + /// + internal override string GetExpandedValue(ConditionEvaluator.IConditionEvaluationState state) + { + if (_cachedExpandedValue == null) + { + if (_expandable) + { + _cachedExpandedValue = state.ExpandIntoString(_value); + } + else + { + _cachedExpandedValue = _value; + } + } + + return _cachedExpandedValue; + } + + /// + /// If any expression nodes cache any state for the duration of evaluation, + /// now's the time to clean it up + /// + internal override void ResetState() + { + _cachedExpandedValue = null; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Conditionals/Token.cs b/src/XMakeBuildEngine/Evaluation/Conditionals/Token.cs new file mode 100644 index 00000000000..9276a9b5316 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Conditionals/Token.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This class represents a token in the Complex Conditionals grammar. It's + /// really just a bag that contains the type of the token and the string that + /// was parsed into the token. This isn't very useful for operators, but + /// is useful for strings and such. + /// + internal sealed class Token + { + internal readonly static Token Comma = new Token(TokenType.Comma); + internal readonly static Token LeftParenthesis = new Token(TokenType.LeftParenthesis); + internal readonly static Token RightParenthesis = new Token(TokenType.RightParenthesis); + internal readonly static Token LessThan = new Token(TokenType.LessThan); + internal readonly static Token GreaterThan = new Token(TokenType.GreaterThan); + internal readonly static Token LessThanOrEqualTo = new Token(TokenType.LessThanOrEqualTo); + internal readonly static Token GreaterThanOrEqualTo = new Token(TokenType.GreaterThanOrEqualTo); + internal readonly static Token And = new Token(TokenType.And); + internal readonly static Token Or = new Token(TokenType.Or); + internal readonly static Token EqualTo = new Token(TokenType.EqualTo); + internal readonly static Token NotEqualTo = new Token(TokenType.NotEqualTo); + internal readonly static Token Not = new Token(TokenType.Not); + internal readonly static Token EndOfInput = new Token(TokenType.EndOfInput); + + /// + /// Valid tokens + /// + internal enum TokenType + { + Comma, LeftParenthesis, RightParenthesis, + LessThan, GreaterThan, LessThanOrEqualTo, GreaterThanOrEqualTo, + And, Or, + EqualTo, NotEqualTo, Not, + Property, String, Numeric, ItemList, ItemMetadata, Function, + EndOfInput + }; + + private TokenType _tokenType; + private string _tokenString; + + /// + /// Constructor for types that don't have values + /// + /// + private Token(TokenType tokenType) + { + _tokenType = tokenType; + _tokenString = null; + } + + /// + /// Constructor takes the token type and the string that + /// represents the token + /// + /// + /// + internal Token(TokenType type, string tokenString) + : this(type, tokenString, false /* not expandable */) + { } + + /// + /// Constructor takes the token type and the string that + /// represents the token. + /// If the string may contain content that needs expansion, expandable is set. + /// + /// + /// + internal Token(TokenType type, string tokenString, bool expandable) + { + ErrorUtilities.VerifyThrow + ( + type == TokenType.Property || + type == TokenType.String || + type == TokenType.Numeric || + type == TokenType.ItemList || + type == TokenType.ItemMetadata || + type == TokenType.Function, + "Unexpected token type" + ); + + ErrorUtilities.VerifyThrowInternalNull(tokenString, "tokenString"); + + _tokenType = type; + _tokenString = tokenString; + this.Expandable = expandable; + } + + /// + /// Whether the content potentially has expandable content, + /// such as a property expression or escaped character. + /// + internal bool Expandable + { + get; + set; + } + + /// + /// + /// + /// + /// + internal bool IsToken(TokenType type) + { + return _tokenType == type; + } + + internal string String + { + get + { + if (_tokenString != null) + { + return _tokenString; + } + + // Return a token string for + // an error message. + switch (_tokenType) + { + case TokenType.Comma: + return ","; + case TokenType.LeftParenthesis: + return "("; + case TokenType.RightParenthesis: + return ")"; + case TokenType.LessThan: + return "<"; + case TokenType.GreaterThan: + return ">"; + case TokenType.LessThanOrEqualTo: + return "<="; + case TokenType.GreaterThanOrEqualTo: + return ">="; + case TokenType.And: + return "and"; + case TokenType.Or: + return "or"; + case TokenType.EqualTo: + return "=="; + case TokenType.NotEqualTo: + return "!="; + case TokenType.Not: + return "!"; + case TokenType.EndOfInput: + return null; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/Evaluator.cs b/src/XMakeBuildEngine/Evaluation/Evaluator.cs new file mode 100644 index 00000000000..9908d978c67 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Evaluator.cs @@ -0,0 +1,2252 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Evaluates a ProjectRootElement into a Project. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Debugging; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using ObjectModel = System.Collections.ObjectModel; +using Microsoft.Build.Collections; +using Microsoft.Build.BackEnd; +using System.Globalization; +#if MSBUILDENABLEVSPROFILING +using Microsoft.VisualStudio.Profiler; +#endif + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using Constants = Microsoft.Build.Internal.Constants; +using EngineFileUtilities = Microsoft.Build.Internal.EngineFileUtilities; +using Microsoft.Build.Framework; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Evaluates a ProjectRootElement, updating the fresh Project.Data passed in. + /// Handles evaluating conditions, expanding expressions, and building up the + /// lists of applicable properties, items, and itemdefinitions, as well as gathering targets and tasks + /// and creating a TaskRegistry from the using tasks. + /// + /// The type of properties to produce. + /// The type of items to produce. + /// The type of metadata on those items. + /// The type of item definitions to be produced. + /// + /// This class could be improved to do partial (minimal) reevaluation: at present we wipe all state and start over. + /// + internal class Evaluator + where P : class, IProperty, IEquatable

, IValued + where I : class, IItem, IMetadataTable + where M : class, IMetadatum + where D : class, IItemDefinition + { + ///

+ /// Character used to split InitialTargets and DefaultTargets lists + /// + private static readonly char[] s_splitter = new char[] { ';' }; + + /// + /// Whether to write information about why we evaluate to debug output. + /// + private static readonly bool s_debugEvaluation = (Environment.GetEnvironmentVariable("MSBUILDDEBUGEVALUATION") != null); + + /// + /// Whether to to respect the TreatAsLocalProperty parameter on the Project tag. + /// + private static readonly bool s_ignoreTreatAsLocalProperty = (Environment.GetEnvironmentVariable("MSBUILDIGNORETREATASLOCALPROPERTY") != null); + + /// + /// Locals types names. We only have these because 'Built In' has a space, + /// else we would use LocalsTypes enum names. + /// Note: This should match LocalsTypes enum. + /// + private static readonly string[] s_localsTypesNames = new string[] + { + "Project", + "Built In", + "Environment", + "Toolset", + "SubToolset", + "Global", + "EvaluateExpression", + "EvaluateCondition", + "ToolsVersion", + "Properties", + "ItemDefinitions", + "Items" + }; + + /// + /// Expander for evaluating conditions + /// + private readonly Expander _expander; + + /// + /// Data containing the ProjectRootElement to evaluate and the slots for + /// items, properties, etc originating from the evaluation. + /// + private readonly IEvaluatorData _data; + + /// + /// List of ProjectItemElement's traversing into imports. + /// Gathered during the first pass to avoid traversing again. + /// + private readonly IList _itemGroupElements; + + /// + /// List of ProjectItemDefinitionElement's traversing into imports. + /// Gathered during the first pass to avoid traversing again. + /// + private readonly IList _itemDefinitionGroupElements; + + /// + /// List of ProjectUsingTaskElement's traversing into imports. + /// Gathered during the first pass to avoid traversing again. + /// Key is the directory of the file importing the usingTask, which is needed + /// to handle any relative paths in the usingTask. + /// + private readonly IList> _usingTaskElements; + + /// + /// List of ProjectTargetElement's traversing into imports. + /// Gathered during the first pass to avoid traversing again. + /// + private readonly IList _targetElements; + + /// + /// Paths to imports already seen and where they were imported from; used to flag duplicate imports + /// + private readonly Dictionary _importsSeen; + + /// + /// Depth first collection of InitialTargets strings declared in the main + /// Project and all its imported files, split on semicolons. + /// + private readonly List _initialTargetsList; + + /// + /// Dictionary of project full paths and a boolean that indicates whether at least one + /// of their targets has the "Returns" attribute set. + /// + private readonly Dictionary> _projectSupportsReturnsAttribute; + + /// + /// The Project Xml to be evaluated. + /// + private readonly ProjectRootElement _projectRootElement; + + /// + /// The logging service for use during evaluation + /// + private readonly ILoggingService _loggingService; + + /// + /// The item factory used to create items from Xml. + /// + private readonly IItemFactory _itemFactory; + + /// + /// Load settings, such as whether to ignore missing imports. + /// + private readonly ProjectLoadSettings _loadSettings; + + /// + /// The maximum number of nodes to report for evaluation. + /// + private readonly int _maxNodeCount; + + /// + /// This optional ProjectInstance is only exposed when doing debugging. It is not used by the evaluator. + /// + private readonly ProjectInstance _projectInstanceIfAnyForDebuggerOnly; + + /// + /// The environment properties with which evaluation should take place. + /// + private readonly PropertyDictionary _environmentProperties; + + /// + /// The cache to consult for any imports that need loading. + /// + private readonly ProjectRootElementCache _projectRootElementCache; + + /// + /// Types of locals pulled in at the start - environment, global, toolset, and built-in properties + /// + private static IList s_initialLocalsTypes; + + /// + /// Types of locals relevant to the property pass + /// + private static IList s_propertyPassLocalsTypes; + + /// + /// Types of locals relevant to the item definition pass + /// + private static IList s_itemDefinitionPassLocalsTypes; + + /// + /// Types of locals relevant to the item pass + /// + private static IList s_itemPassLocalsTypes; + + /// + /// Build event context to log evaluator events in. + /// + private BuildEventContext _buildEventContext = null; + + /// + /// List of values and names available initially + /// + private IDictionary _initialLocals; + + /// + /// List of values and names available in the property pass of evaluation + /// + private IDictionary _propertyPassLocals; + + /// + /// List of values and names available in the item definition pass of evaluation + /// + private IDictionary _itemDefinitionPassLocals; + + /// + /// List of values and names available in the item pass of evaluation + /// + private IDictionary _itemPassLocals; + + /// + /// Dictionary of {child, parent} import relationships. + /// + private IDictionary _importRelationships; + + /// + /// This is passed back so it can go to the build for debugger display while executing targets + /// + private IDictionary _projectLevelLocalsForBuild; + + /// + /// Private constructor called by the static Evaluate method. + /// + private Evaluator(IEvaluatorData data, ProjectRootElement projectRootElement, ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, ProjectInstance projectInstanceIfAnyForDebuggerOnly) + { + ErrorUtilities.VerifyThrowInternalNull(data, "data"); + ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, "projectRootElementCache"); + + // Create containers for the evaluation results + data.InitializeForEvaluation(toolsetProvider); + + _expander = new Expander(data, data); + + // This setting may change after the build has started, therefore if the user has not set the property to true on the build parameters we need to check to see if it is set to true on the environment variable. + _expander.WarnForUninitializedProperties = BuildParameters.WarnOnUninitializedProperty || !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY")); + _data = data; + _itemGroupElements = new List(); + _itemDefinitionGroupElements = new List(); + _usingTaskElements = new List>(); + _targetElements = new List(); + _importsSeen = new Dictionary(StringComparer.OrdinalIgnoreCase); + _initialTargetsList = new List(); + _projectSupportsReturnsAttribute = new Dictionary>(); + _projectRootElement = projectRootElement; + _loadSettings = loadSettings; + _maxNodeCount = maxNodeCount; + _environmentProperties = environmentProperties; + _loggingService = loggingService; + _itemFactory = itemFactory; + _projectRootElementCache = projectRootElementCache; + _buildEventContext = buildEventContext; + _projectInstanceIfAnyForDebuggerOnly = projectInstanceIfAnyForDebuggerOnly; + } + + /// + /// Delegate passed to methods to provide basic expression evaluation + /// ability, without having a language service. + /// + internal delegate string ExpandExpression(string unexpandedString); + + /// + /// Delegate passed to methods to provide basic expression evaluation + /// ability, without having a language service. + /// + internal delegate bool EvaluateConditionalExpression(string unexpandedExpression); + + /// + /// Enumeration for locals types + /// Note: This should match LocalsTypesNames + /// + private enum LocalsTypes : int + { + /// + /// Project, + /// + Project, + + /// + /// BuiltIn, + /// + BuiltIn, + + /// + /// Environment, + /// + Environment, + + /// + /// Toolset, + /// + Toolset, + + /// + /// SubToolset, + /// + SubToolset, + + /// + /// Global, + /// + Global, + + /// + /// EvaluateExpression, + /// + EvaluateExpression, + + /// + /// EvaluateCondition, + /// + EvaluateCondition, + + /// + /// ToolsVersion, + /// + ToolsVersion, + + /// + /// Properties, + /// + Properties, + + /// + /// ItemDefinitions, + /// + ItemDefinitions, + + /// + /// Items + /// + Items + } + + /// + /// Whether to write information about why we evaluate to debug output. + /// + internal static bool DebugEvaluation + { + get { return s_debugEvaluation; } + } + + /// + /// Evaluates the project data passed in. + /// If debugging is enabled, returns a dictionary of name/value pairs such as properties, for debugger display. + /// + /// + /// This is the only non-private member of this class. + /// This is a helper static method so that the caller can just do "Evaluator.Evaluate(..)" without + /// newing one up, yet the whole class need not be static. + /// The optional ProjectInstance is only exposed when doing debugging. It is not used by the evaluator. + /// + internal static IDictionary Evaluate(IEvaluatorData data, ProjectRootElement root, ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, ProjectInstance projectInstanceIfAnyForDebuggerOnly) + { +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectEvaluateBegin, CodeMarkerEvent.perfMSBuildProjectEvaluateEnd)) +#endif + { +#if MSBUILDENABLEVSPROFILING + try + { + string projectFile = String.IsNullOrEmpty(root.ProjectFileLocation.File) ? "(null)" : root.ProjectFileLocation.File; + string beginProjectEvaluate = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - Begin", projectFile); + DataCollection.CommentMarkProfile(8812, beginProjectEvaluate); +#endif + Evaluator evaluator = new Evaluator(data, root, loadSettings, maxNodeCount, environmentProperties, loggingService, itemFactory, toolsetProvider, projectRootElementCache, buildEventContext, projectInstanceIfAnyForDebuggerOnly); + IDictionary projectLevelLocalsForBuild = evaluator.Evaluate(); + return projectLevelLocalsForBuild; +#if MSBUILDENABLEVSPROFILING + } + finally + { + string projectFile = String.IsNullOrEmpty(root.ProjectFileLocation.File) ? "(null)" : root.ProjectFileLocation.File; + string beginProjectEvaluate = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - End", projectFile); + DataCollection.CommentMarkProfile(8813, beginProjectEvaluate); + } +#endif + } + } + + /// + /// Helper that creates a list of ProjectItem's given an unevaluated Include and a ProjectRootElement. + /// Used by both Evaluator.EvaluateItemElement and by Project.AddItem. + /// + internal static List CreateItemsFromInclude(string rootDirectory, ProjectItemElement itemElement, IItemFactory itemFactory, string unevaluatedIncludeEscaped, Expander expander) + { + ErrorUtilities.VerifyThrowArgumentLength(unevaluatedIncludeEscaped, "unevaluatedIncludeEscaped"); + + List items = new List(); + itemFactory.ItemElement = itemElement; + + // STEP 1: Expand properties in Include + string evaluatedIncludeEscaped = expander.ExpandIntoStringLeaveEscaped(unevaluatedIncludeEscaped, ExpanderOptions.ExpandProperties, itemElement.IncludeLocation); + + // STEP 2: Split Include on any semicolons, and take each split in turn + if (evaluatedIncludeEscaped.Length > 0) + { + IList includeSplitsEscaped = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedIncludeEscaped); + + foreach (string includeSplitEscaped in includeSplitsEscaped) + { + // STEP 3: If expression is "@(x)" copy specified list with its metadata, otherwise just treat as string + bool throwaway; + IList itemsFromSplit = expander.ExpandSingleItemVectorExpressionIntoItems(includeSplitEscaped, itemFactory, ExpanderOptions.ExpandItems, false /* do not include null expansion results */, out throwaway, itemElement.IncludeLocation); + + if (itemsFromSplit != null) + { + // Expression is in form "@(X)" + foreach (I item in itemsFromSplit) + { + items.Add(item); + } + } + else + { + // The expression is not of the form "@(X)". Treat as string + string[] includeSplitFilesEscaped = EngineFileUtilities.GetFileListEscaped(rootDirectory, includeSplitEscaped); + + if (includeSplitFilesEscaped.Length > 0) + { + foreach (string includeSplitFileEscaped in includeSplitFilesEscaped) + { + items.Add(itemFactory.CreateItem(includeSplitFileEscaped, includeSplitEscaped, itemElement.ContainingProject.FullPath)); + } + } + } + } + } + + return items; + } + + /// + /// Initializes DebuggerManager. + /// Initialize definitions of locals types. + /// This must not be called by a static constructor, as the + /// time at which it is called will then be undefined, and + /// the debugging environment variable might not have had a + /// chance to be set. + /// + private static void InitializeForDebugging() + { + DebuggerManager.Initialize(); + + if (DebuggerManager.DebuggingEnabled) + { + s_initialLocalsTypes = new List(6); + s_initialLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.Project], typeof(ProjectInstance))); + s_initialLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.BuiltIn], typeof(ICollection

))); + s_initialLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.Environment], typeof(ICollection

))); + s_initialLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.Toolset], typeof(ICollection

))); + s_initialLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.SubToolset], typeof(ICollection

))); + s_initialLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.Global], typeof(ICollection

))); + + s_propertyPassLocalsTypes = new List(s_initialLocalsTypes); + s_propertyPassLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.EvaluateExpression], typeof(ExpandExpression))); + s_propertyPassLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.EvaluateCondition], typeof(EvaluateConditionalExpression))); + s_propertyPassLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.ToolsVersion], typeof(string))); + s_propertyPassLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.Properties], typeof(PropertyDictionary

))); + + s_itemDefinitionPassLocalsTypes = new List(s_propertyPassLocalsTypes); + s_itemDefinitionPassLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.ItemDefinitions], typeof(IEnumerable))); + + s_itemPassLocalsTypes = new List(s_itemDefinitionPassLocalsTypes); + s_itemPassLocalsTypes.Add(new DebuggerLocalType(Evaluator.s_localsTypesNames[(int)LocalsTypes.Items], typeof(ItemDictionary))); + } + } + + ///

+ /// Read the task into an instance. + /// Do not evaluate anything: this occurs during build. + /// + private static ProjectTaskInstance ReadTaskElement(ProjectTaskElement taskElement) + { + List taskOutputs = new List(); + + foreach (ProjectOutputElement output in taskElement.Outputs) + { + if (output.IsOutputItem) + { + ProjectTaskOutputItemInstance outputItem = new ProjectTaskOutputItemInstance + ( + output.ItemType, + output.TaskParameter, + output.Condition, + output.Location, + output.ItemTypeLocation, + output.TaskParameterLocation, + output.ConditionLocation + ); + + taskOutputs.Add(outputItem); + } + else + { + ProjectTaskOutputPropertyInstance outputItem = new ProjectTaskOutputPropertyInstance + ( + output.PropertyName, + output.TaskParameter, + output.Condition, + output.Location, + output.PropertyNameLocation, + output.TaskParameterLocation, + output.ConditionLocation + ); + + taskOutputs.Add(outputItem); + } + } + + ProjectTaskInstance task = new ProjectTaskInstance(taskElement, taskOutputs); + return task; + } + + /// + /// Read the property-group-under-target into an instance. + /// Do not evaluate anything: this occurs during build. + /// + private static ProjectPropertyGroupTaskInstance ReadPropertyGroupUnderTargetElement(ProjectPropertyGroupElement propertyGroupElement) + { + List properties = new List(); + + foreach (ProjectPropertyElement propertyElement in propertyGroupElement.Properties) + { + ProjectPropertyGroupTaskPropertyInstance property = new ProjectPropertyGroupTaskPropertyInstance(propertyElement.Name, propertyElement.Value, propertyElement.Condition, propertyElement.Location, propertyElement.ConditionLocation); + properties.Add(property); + } + + ProjectPropertyGroupTaskInstance propertyGroup = new ProjectPropertyGroupTaskInstance(propertyGroupElement.Condition, propertyGroupElement.Location, propertyGroupElement.ConditionLocation, properties); + + return propertyGroup; + } + + /// + /// Read an onError tag. + /// Do not evaluate anything: this occurs during build. + /// + private static ProjectOnErrorInstance ReadOnErrorElement(ProjectOnErrorElement projectOnErrorElement) + { + ProjectOnErrorInstance onError = new ProjectOnErrorInstance(projectOnErrorElement.ExecuteTargetsAttribute, projectOnErrorElement.Condition, projectOnErrorElement.Location, projectOnErrorElement.ExecuteTargetsLocation, projectOnErrorElement.ConditionLocation); + + return onError; + } + + /// + /// Read the item-group-under-target into an instance. + /// Do not evaluate anything: this occurs during build. + /// + private static ProjectItemGroupTaskInstance ReadItemGroupUnderTargetElement(ProjectItemGroupElement itemGroupElement) + { + List items = new List(); + + foreach (ProjectItemElement itemElement in itemGroupElement.Items) + { + List metadata = null; + + foreach (ProjectMetadataElement metadataElement in itemElement.Metadata) + { + if (metadata == null) + { + metadata = new List(); + } + + ProjectItemGroupTaskMetadataInstance metadatum = new ProjectItemGroupTaskMetadataInstance + ( + metadataElement.Name, + metadataElement.Value, + metadataElement.Condition, + metadataElement.Location, + metadataElement.ConditionLocation + ); + + metadata.Add(metadatum); + } + + ProjectItemGroupTaskItemInstance item = new ProjectItemGroupTaskItemInstance + ( + itemElement.ItemType, + itemElement.Include, + itemElement.Exclude, + itemElement.Remove, + itemElement.KeepMetadata, + itemElement.RemoveMetadata, + itemElement.KeepDuplicates, + itemElement.Condition, + itemElement.Location, + itemElement.IncludeLocation, + itemElement.ExcludeLocation, + itemElement.RemoveLocation, + itemElement.KeepMetadataLocation, + itemElement.RemoveMetadataLocation, + itemElement.KeepDuplicatesLocation, + itemElement.ConditionLocation, + metadata + ); + + items.Add(item); + } + + ProjectItemGroupTaskInstance itemGroup = new ProjectItemGroupTaskInstance(itemGroupElement.Condition, itemGroupElement.Location, itemGroupElement.ConditionLocation, items); + + return itemGroup; + } + + /// + /// Read the provided target into a target instance. + /// Do not evaluate anything: this occurs during build. + /// + private static ProjectTargetInstance ReadNewTargetElement(ProjectTargetElement targetElement, bool parentProjectSupportsReturnsAttribute) + { + List targetChildren = new List(); + List targetOnErrorChildren = new List(); + + foreach (ProjectElement targetChildElement in targetElement.Children) + { + ProjectTaskElement task = targetChildElement as ProjectTaskElement; + + if (task != null) + { + ProjectTaskInstance taskInstance = ReadTaskElement(task); + + targetChildren.Add(taskInstance); + continue; + } + + ProjectPropertyGroupElement propertyGroup = targetChildElement as ProjectPropertyGroupElement; + + if (propertyGroup != null) + { + ProjectPropertyGroupTaskInstance propertyGroupInstance = ReadPropertyGroupUnderTargetElement(propertyGroup); + + targetChildren.Add(propertyGroupInstance); + continue; + } + + ProjectItemGroupElement itemGroup = targetChildElement as ProjectItemGroupElement; + + if (itemGroup != null) + { + ProjectItemGroupTaskInstance itemGroupInstance = ReadItemGroupUnderTargetElement(itemGroup); + + targetChildren.Add(itemGroupInstance); + continue; + } + + ProjectOnErrorElement onError = targetChildElement as ProjectOnErrorElement; + + if (onError != null) + { + ProjectOnErrorInstance onErrorInstance = ReadOnErrorElement(onError); + + targetOnErrorChildren.Add(onErrorInstance); + continue; + } + + ErrorUtilities.ThrowInternalError("Unexpected child"); + } + + // ObjectModel.ReadOnlyCollection is actually a poorly named ReadOnlyList + + // UNDONE: (Cloning.) This should be cloning these collections, but it isn't. ProjectTargetInstance will be able to see modifications. + ObjectModel.ReadOnlyCollection readOnlyTargetChildren = new ObjectModel.ReadOnlyCollection(targetChildren); + ObjectModel.ReadOnlyCollection readOnlyTargetOnErrorChildren = new ObjectModel.ReadOnlyCollection(targetOnErrorChildren); + + ProjectTargetInstance targetInstance = new ProjectTargetInstance + ( + targetElement.Name, + targetElement.Condition, + targetElement.Inputs, + targetElement.Outputs, + targetElement.Returns, + targetElement.KeepDuplicateOutputs, + targetElement.DependsOnTargets, + targetElement.Location, + targetElement.ConditionLocation, + targetElement.InputsLocation, + targetElement.OutputsLocation, + targetElement.ReturnsLocation, + targetElement.KeepDuplicateOutputsLocation, + targetElement.DependsOnTargetsLocation, + targetElement.BeforeTargetsLocation, + targetElement.AfterTargetsLocation, + readOnlyTargetChildren, + readOnlyTargetOnErrorChildren, + parentProjectSupportsReturnsAttribute + ); + + targetElement.TargetInstance = targetInstance; + return targetInstance; + } + + /// + /// Do the evaluation. + /// Called by the static helper method. + /// If debugging is enabled, returns a dictionary of name/value pairs such as properties, for debugger display. + /// + private IDictionary Evaluate() + { + InitializeForDebugging(); + + // Pass0: load initial properties + // Follow the order of precedence so that Global properties overwrite Environment properties + ICollection

builtInProperties = AddBuiltInProperties(); + ICollection

environmentProperties = AddEnvironmentProperties(); + ICollection

toolsetProperties = AddToolsetProperties(); + ICollection

subToolsetProperties = AddSubToolsetProperties(); + ICollection

globalProperties = AddGlobalProperties(); + + // Create a state for the root project node to show initial properties + if (DebuggerManager.DebuggingEnabled) + { + _initialLocals = new Dictionary(); + _initialLocals.Add(new KeyValuePair(s_initialLocalsTypes[(int)LocalsTypes.Project].Name, _projectInstanceIfAnyForDebuggerOnly)); + _initialLocals.Add(new KeyValuePair(s_initialLocalsTypes[(int)LocalsTypes.BuiltIn].Name, builtInProperties)); + _initialLocals.Add(new KeyValuePair(s_initialLocalsTypes[(int)LocalsTypes.Environment].Name, environmentProperties)); + _initialLocals.Add(new KeyValuePair(s_initialLocalsTypes[(int)LocalsTypes.Toolset].Name, toolsetProperties)); + _initialLocals.Add(new KeyValuePair(s_initialLocalsTypes[(int)LocalsTypes.SubToolset].Name, subToolsetProperties)); + _initialLocals.Add(new KeyValuePair(s_initialLocalsTypes[(int)LocalsTypes.Global].Name, globalProperties)); + + DebuggerManager.DefineState(_projectRootElement.Location, _projectRootElement.ElementName, s_initialLocalsTypes); + + DebuggerManager.BakeStates(Path.GetFileNameWithoutExtension(_projectRootElement.FullPath)); + + DebuggerManager.PulseState(_projectRootElement.Location, _initialLocals); + + _propertyPassLocals = new Dictionary(_initialLocals); + _propertyPassLocals.Add(new KeyValuePair(s_propertyPassLocalsTypes[(int)LocalsTypes.EvaluateExpression].Name, (ExpandExpression)_data.ExpandString)); + _propertyPassLocals.Add(new KeyValuePair(s_propertyPassLocalsTypes[(int)LocalsTypes.EvaluateCondition].Name, (EvaluateConditionalExpression)_data.EvaluateCondition)); + _propertyPassLocals.Add(new KeyValuePair(s_propertyPassLocalsTypes[(int)LocalsTypes.ToolsVersion].Name, _data.Toolset.ToolsVersion)); + _propertyPassLocals.Add(new KeyValuePair(s_propertyPassLocalsTypes[(int)LocalsTypes.Properties].Name, _data.Properties)); + + _itemDefinitionPassLocals = new Dictionary(_propertyPassLocals); + _itemDefinitionPassLocals.Add(new KeyValuePair(s_itemDefinitionPassLocalsTypes[(int)LocalsTypes.ItemDefinitions].Name, _data.ItemDefinitionsEnumerable)); + + _itemPassLocals = new Dictionary(_itemDefinitionPassLocals); + _itemPassLocals.Add(new KeyValuePair(s_itemPassLocalsTypes[(int)LocalsTypes.Items].Name, _data.Items)); + + // This is currently only needed when debugging + _importRelationships = new Dictionary(); + + // This is passed back to the build, so locals are visible during the build + _projectLevelLocalsForBuild = _itemPassLocals; + } +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildProjectEvaluatePass0End); +#endif + string projectFile = String.IsNullOrEmpty(_projectRootElement.ProjectFileLocation.File) ? "(null)" : _projectRootElement.ProjectFileLocation.File; + +#if MSBUILDENABLEVSPROFILING + string endPass0 = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - End Pass 0 (Initial properties)", projectFile); + DataCollection.CommentMarkProfile(8816, endPass0); +#endif + + // Pass1: evaluate properties, load imports, and gather everything else + PerformDepthFirstPass(_projectRootElement); + + List initialTargets = new List(_initialTargetsList.Count); + for (int i = 0; i < _initialTargetsList.Count; i++) + { + initialTargets.Add(EscapingUtilities.UnescapeAll(_initialTargetsList[i].Trim())); + } + + _data.InitialTargets = initialTargets; +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildProjectEvaluatePass1End); +#endif +#if MSBUILDENABLEVSPROFILING + string endPass1 = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - End Pass 1 (Properties and Imports)", projectFile); + DataCollection.CommentMarkProfile(8817, endPass1); +#endif + // Pass2: evaluate item definitions + foreach (ProjectItemDefinitionGroupElement itemDefinitionGroupElement in _itemDefinitionGroupElements) + { + EvaluateItemDefinitionGroupElement(itemDefinitionGroupElement); + } +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildProjectEvaluatePass2End); +#endif +#if MSBUILDENABLEVSPROFILING + string endPass2 = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - End Pass 2 (Item Definitions)", projectFile); + DataCollection.CommentMarkProfile(8818, endPass2); +#endif + // Pass3: evaluate project items + foreach (ProjectItemGroupElement itemGroupElement in _itemGroupElements) + { + EvaluateItemGroupElement(itemGroupElement); + } +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildProjectEvaluatePass3End); +#endif +#if MSBUILDENABLEVSPROFILING + string endPass3 = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - End Pass 3 (Items)", projectFile); + DataCollection.CommentMarkProfile(8819, endPass3); +#endif + // Pass4: evaluate using-tasks + foreach (Pair entry in _usingTaskElements) + { + EvaluateUsingTaskElement(entry.Key, entry.Value); + } + + // If there was no DefaultTargets attribute found in the depth first pass, + // use the name of the first target. If there isn't any target, don't error until build time. + if (_data.DefaultTargets == null || _data.DefaultTargets.Count == 0) + { + List defaultTargets = new List(_targetElements.Count); + if (_targetElements.Count > 0) + { + defaultTargets.Add(_targetElements[0].Name); + } + + _data.DefaultTargets = defaultTargets; + } + + Dictionary> targetsWhichRunBeforeByTarget = new Dictionary>(StringComparer.OrdinalIgnoreCase); + Dictionary> targetsWhichRunAfterByTarget = new Dictionary>(StringComparer.OrdinalIgnoreCase); + LinkedList activeTargetsByEvaluationOrder = new LinkedList(); + Dictionary> activeTargets = new Dictionary>(StringComparer.OrdinalIgnoreCase); +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildProjectEvaluatePass4End); +#endif +#if MSBUILDENABLEVSPROFILING + string endPass4 = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - End Pass 4 (UsingTasks)", projectFile); + DataCollection.CommentMarkProfile(8820, endPass4); +#endif + + // Pass5: read targets (but don't evaluate them: that happens during build) + foreach (ProjectTargetElement targetElement in _targetElements) + { + ReadTargetElement(targetElement, activeTargetsByEvaluationOrder, activeTargets); + } + + foreach (ProjectTargetElement target in activeTargetsByEvaluationOrder) + { + AddBeforeAndAfterTargetMappings(target, activeTargets, targetsWhichRunBeforeByTarget, targetsWhichRunAfterByTarget); + } + + _data.BeforeTargets = targetsWhichRunBeforeByTarget; + _data.AfterTargets = targetsWhichRunAfterByTarget; + + if (s_debugEvaluation) + { + // This is so important for VS performance it's worth always tracing; accidentally having + // inconsistent sets of global properties will cause reevaluations, which are wasteful and incorrect + if (_projectRootElement.Count > 0) // VB/C# will new up empty projects; they aren't worth recording + { + ProjectPropertyInstance configurationData = _data.GlobalPropertiesDictionary["currentsolutionconfigurationcontents"]; + int hash = (configurationData != null) ? configurationData.EvaluatedValue.GetHashCode() : 0; + string propertyDump = null; + + foreach (var entry in _data.GlobalPropertiesDictionary) + { + if (!String.Equals(entry.Name, "currentsolutionconfigurationcontents", StringComparison.OrdinalIgnoreCase)) + { + propertyDump += entry.Name + "=" + entry.EvaluatedValue + "\n"; + } + } + + string line = new string('#', 100) + "\n"; + + string output = String.Format(CultureInfo.CurrentUICulture, "###: MSBUILD: Evaluating or reevaluating project {0} with {1} global properties and {2} tools version, child count {3}, CurrentSolutionConfigurationContents hash {4} other properties:\n{5}", _projectRootElement.FullPath, globalProperties.Count, _data.Toolset.ToolsVersion, _projectRootElement.Count, hash, propertyDump); + + Trace.WriteLine(line + output + line); + } + } + + _data.FinishEvaluation(); + + return _projectLevelLocalsForBuild; + } + + ///

+ /// Evaluate the properties in the passed in XML, into the project. + /// Does a depth first traversal into Imports. + /// In the process, populates the item, itemdefinition, target, and usingtask lists as well. + /// + private void PerformDepthFirstPass(ProjectRootElement currentProjectOrImport) + { + // We accumulate InitialTargets from the project and each import + IList initialTargets = _expander.ExpandIntoStringListLeaveEscaped(currentProjectOrImport.InitialTargets, ExpanderOptions.ExpandProperties, currentProjectOrImport.InitialTargetsLocation); + _initialTargetsList.AddRange(initialTargets); + + if (!s_ignoreTreatAsLocalProperty) + { + IList globalPropertiesToTreatAsLocals = _expander.ExpandIntoStringListLeaveEscaped(currentProjectOrImport.TreatAsLocalProperty, ExpanderOptions.ExpandProperties, currentProjectOrImport.TreatAsLocalPropertyLocation); + + foreach (string propertyName in globalPropertiesToTreatAsLocals) + { + XmlUtilities.VerifyThrowProjectValidElementName(propertyName, currentProjectOrImport.Location); + _data.GlobalPropertiesToTreatAsLocal.Add(propertyName); + } + } + + UpdateDefaultTargets(currentProjectOrImport); + + if (DebuggerManager.DebuggingEnabled) + { + // Create a state for every element processed during the properties pass + foreach (ProjectElement element in currentProjectOrImport.AllChildren) + { + if ( + element is ProjectPropertyGroupElement || + element is ProjectPropertyElement || + element is ProjectImportGroupElement || + element is ProjectImportElement || + element is ProjectChooseElement || + element is ProjectWhenElement || // although Whens are encountered again during the item pass, the condition is only evaluated on the first pass, hence, property locals only + element is ProjectOtherwiseElement + ) + { + // Skip any that are somewhere below targets; those will be defined later + if (!(element is ProjectTargetElement) && + element.AllParents.FirstOrDefault(delegate (ProjectElementContainer current) { return (current != null && current is ProjectTargetElement); }) == null) + { + DebuggerManager.DefineState(element.Location, element.Location.LocationString, s_propertyPassLocalsTypes); + } + } + } + + // Bake the property pass states so we can enter them + DebuggerManager.BakeStates(Path.GetFileNameWithoutExtension(currentProjectOrImport.FullPath)); + } + + foreach (ProjectElement element in currentProjectOrImport.Children) + { + ProjectPropertyGroupElement propertyGroup = element as ProjectPropertyGroupElement; + + if (propertyGroup != null) + { + EvaluatePropertyGroupElement(propertyGroup); + continue; + } + + ProjectItemGroupElement itemGroup = element as ProjectItemGroupElement; + + if (itemGroup != null) + { + _itemGroupElements.Add(itemGroup); + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.DefineState(element.Location, element.Location.LocationString, s_itemPassLocalsTypes); + + foreach (ProjectItemElement item in itemGroup.Items) + { + DebuggerManager.DefineState(item.Location, item.Location.LocationString, s_itemPassLocalsTypes); + + foreach (ProjectMetadataElement metadatum in item.Metadata) + { + DebuggerManager.DefineState(metadatum.Location, metadatum.Location.LocationString, s_itemPassLocalsTypes); + } + } + } + + continue; + } + + ProjectItemDefinitionGroupElement itemDefinitionGroup = element as ProjectItemDefinitionGroupElement; + + if (itemDefinitionGroup != null) + { + _itemDefinitionGroupElements.Add(itemDefinitionGroup); + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.DefineState(element.Location, element.Location.LocationString, s_itemDefinitionPassLocalsTypes); + + foreach (ProjectItemDefinitionElement itemDefinition in itemDefinitionGroup.ItemDefinitions) + { + DebuggerManager.DefineState(itemDefinition.Location, itemDefinition.Location.LocationString, s_itemDefinitionPassLocalsTypes); + + foreach (ProjectMetadataElement metadatum in itemDefinition.Metadata) + { + DebuggerManager.DefineState(metadatum.Location, metadatum.Location.LocationString, s_itemDefinitionPassLocalsTypes); + } + } + } + + continue; + } + + ProjectTargetElement target = element as ProjectTargetElement; + + if (target != null) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.DefineState(element.Location, element.Location.LocationString, s_itemPassLocalsTypes); + + foreach (ProjectElement child in (target.AllChildren)) + { + DebuggerManager.DefineState(child.Location, child.Location.LocationString, s_itemPassLocalsTypes); + } + } + + if (_projectSupportsReturnsAttribute.ContainsKey(currentProjectOrImport)) + { + _projectSupportsReturnsAttribute[currentProjectOrImport] |= (target.Returns != null); + } + else + { + _projectSupportsReturnsAttribute[currentProjectOrImport] = (target.Returns != null); + } + + _targetElements.Add(target); + + continue; + } + + ProjectImportElement import = element as ProjectImportElement; + + if (import != null) + { + EvaluateImportElement(currentProjectOrImport.DirectoryPath, import); + continue; + } + + ProjectImportGroupElement importGroup = element as ProjectImportGroupElement; + + if (importGroup != null) + { + EvaluateImportGroupElement(currentProjectOrImport.DirectoryPath, importGroup); + continue; + } + + ProjectUsingTaskElement usingTask = element as ProjectUsingTaskElement; + + if (usingTask != null) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.DefineState(element.Location, element.Location.LocationString, s_itemPassLocalsTypes); + } + + _usingTaskElements.Add(new Pair(currentProjectOrImport.DirectoryPath, usingTask)); + continue; + } + + ProjectChooseElement choose = element as ProjectChooseElement; + + if (choose != null) + { + if (DebuggerManager.DebuggingEnabled) + { + // Already defined states for all choose children that were relevant to the + // property pass; now the ones relevant to the item pass, which get the item pass locals + foreach (ProjectElement child in choose.AllChildren) + { + if (child is ProjectItemGroupElement || + child is ProjectItemElement || + child is ProjectMetadataElement) + { + DebuggerManager.DefineState(child.Location, child.Location.LocationString, s_itemPassLocalsTypes); + } + } + } + + EvaluateChooseElement(choose); + continue; + } + + if (element is ProjectExtensionsElement) + { + continue; + } + + ErrorUtilities.ThrowInternalError("Unexpected child type"); + } + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.BakeStates(Path.GetFileNameWithoutExtension(currentProjectOrImport.FullPath)); + } + } + + /// + /// Update the default targets value. + /// We only take the first DefaultTargets value we encounter in a project or import. + /// + private void UpdateDefaultTargets(ProjectRootElement currentProjectOrImport) + { + if (_data.DefaultTargets == null) + { + string expanded = _expander.ExpandIntoStringLeaveEscaped(currentProjectOrImport.DefaultTargets, ExpanderOptions.ExpandProperties, currentProjectOrImport.DefaultTargetsLocation); + + if (expanded.Length > 0) + { + SetBuiltInProperty(ReservedPropertyNames.projectDefaultTargets, EscapingUtilities.UnescapeAll(expanded)); + + List temp = new List(expanded.Split(s_splitter, StringSplitOptions.RemoveEmptyEntries)); + + for (int i = 0; i < temp.Count; i++) + { + string target = EscapingUtilities.UnescapeAll(temp[i].Trim()); + if (target.Length > 0) + { + _data.DefaultTargets = _data.DefaultTargets ?? new List(temp.Count); + _data.DefaultTargets.Add(target); + } + } + } + } + } + + /// + /// Evaluate the properties in the propertygroup and set the applicable ones on the data passed in + /// + private void EvaluatePropertyGroupElement(ProjectPropertyGroupElement propertyGroupElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(propertyGroupElement.Location, _propertyPassLocals); + } + + if (EvaluateConditionCollectingConditionedProperties(propertyGroupElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties)) + { + foreach (ProjectPropertyElement propertyElement in propertyGroupElement.Properties) + { + EvaluatePropertyElement(propertyElement); + } + } + } + + /// + /// Evaluate the itemdefinitiongroup and update the definitions library + /// + private void EvaluateItemDefinitionGroupElement(ProjectItemDefinitionGroupElement itemDefinitionGroupElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(itemDefinitionGroupElement.Location, _itemDefinitionPassLocals); + } + + if (EvaluateCondition(itemDefinitionGroupElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties)) + { + foreach (ProjectItemDefinitionElement itemDefinitionElement in itemDefinitionGroupElement.ItemDefinitions) + { + EvaluateItemDefinitionElement(itemDefinitionElement); + } + } + } + + /// + /// Evaluate the items in the itemgroup and add the applicable ones to the data passed in + /// + private void EvaluateItemGroupElement(ProjectItemGroupElement itemGroupElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(itemGroupElement.Location, _itemPassLocals); + } + + bool itemGroupConditionResult = EvaluateCondition(itemGroupElement, ExpanderOptions.ExpandPropertiesAndItems, ParserOptions.AllowPropertiesAndItemLists); + + if (itemGroupConditionResult || _data.ShouldEvaluateForDesignTime) + { + foreach (ProjectItemElement itemElement in itemGroupElement.Items) + { + EvaluateItemElement(itemGroupConditionResult, itemElement); + } + } + } + + /// + /// Evaluate the usingtask and add the result into the data passed in + /// + private void EvaluateUsingTaskElement(string directoryOfImportingFile, ProjectUsingTaskElement projectUsingTaskElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(projectUsingTaskElement.Location, _itemPassLocals); + } + + TaskRegistry.RegisterTasksFromUsingTaskElement + ( + _loggingService, + _buildEventContext, + directoryOfImportingFile, + projectUsingTaskElement, + _data.TaskRegistry, + _expander, + ExpanderOptions.ExpandPropertiesAndItems + ); + } + + /// + /// Retrieve the matching ProjectTargetInstance from the cache and add it to the provided collection. + /// If it is not cached already, read it and cache it. + /// Do not evaluate anything: this occurs during build. + /// + private void ReadTargetElement(ProjectTargetElement targetElement, LinkedList activeTargetsByEvaluationOrder, Dictionary> activeTargets) + { + ProjectTargetInstance targetInstance = null; + + // If we already have read a target instance for this element, use that. + targetInstance = targetElement.TargetInstance; + + if (targetInstance == null) + { + targetInstance = ReadNewTargetElement(targetElement, _projectSupportsReturnsAttribute[(ProjectRootElement)targetElement.Parent]); + } + + string targetName = targetElement.Name; + ProjectTargetInstance otherTarget = _data.GetTarget(targetName); + if (otherTarget != null) + { + _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "OverridingTarget", otherTarget.Name, otherTarget.Location.File, targetName, targetElement.Location.File); + } + + LinkedListNode node; + if (activeTargets.TryGetValue(targetName, out node)) + { + activeTargetsByEvaluationOrder.Remove(node); + } + + activeTargets[targetName] = activeTargetsByEvaluationOrder.AddLast(targetElement); + _data.AddTarget(targetInstance); + } + + /// + /// Updates the evaluation maps for BeforeTargets and AfterTargets + /// + private void AddBeforeAndAfterTargetMappings(ProjectTargetElement targetElement, Dictionary> activeTargets, Dictionary> targetsWhichRunBeforeByTarget, Dictionary> targetsWhichRunAfterByTarget) + { + IList beforeTargets = _expander.ExpandIntoStringListLeaveEscaped(targetElement.BeforeTargets, ExpanderOptions.ExpandPropertiesAndItems, targetElement.BeforeTargetsLocation); + IList afterTargets = _expander.ExpandIntoStringListLeaveEscaped(targetElement.AfterTargets, ExpanderOptions.ExpandPropertiesAndItems, targetElement.AfterTargetsLocation); + + foreach (string beforeTarget in beforeTargets) + { + string unescapedBeforeTarget = EscapingUtilities.UnescapeAll(beforeTarget); + + if (activeTargets.ContainsKey(unescapedBeforeTarget)) + { + List beforeTargetsForTarget = null; + if (!targetsWhichRunBeforeByTarget.TryGetValue(unescapedBeforeTarget, out beforeTargetsForTarget)) + { + beforeTargetsForTarget = new List(); + targetsWhichRunBeforeByTarget[unescapedBeforeTarget] = beforeTargetsForTarget; + } + + beforeTargetsForTarget.Add(new TargetSpecification(targetElement.Name, targetElement.BeforeTargetsLocation)); + } + else + { + // This is a message, not a warning, because that enables people to speculatively extend the build of a project + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, "TargetDoesNotExistBeforeTargetMessage", unescapedBeforeTarget, targetElement.BeforeTargetsLocation.LocationString); + } + } + + foreach (string afterTarget in afterTargets) + { + string unescapedAfterTarget = EscapingUtilities.UnescapeAll(afterTarget); + + if (activeTargets.ContainsKey(unescapedAfterTarget)) + { + List afterTargetsForTarget = null; + if (!targetsWhichRunAfterByTarget.TryGetValue(unescapedAfterTarget, out afterTargetsForTarget)) + { + afterTargetsForTarget = new List(); + targetsWhichRunAfterByTarget[unescapedAfterTarget] = afterTargetsForTarget; + } + + afterTargetsForTarget.Add(new TargetSpecification(targetElement.Name, targetElement.AfterTargetsLocation)); + } + else + { + // This is a message, not a warning, because that enables people to speculatively extend the build of a project + _loggingService.LogComment(_buildEventContext, MessageImportance.Normal, "TargetDoesNotExistAfterTargetMessage", unescapedAfterTarget, targetElement.AfterTargetsLocation.LocationString); + } + } + } + + /// + /// Set the built-in properties, most of which are read-only + /// + private ICollection

AddBuiltInProperties() + { + string startupDirectory = BuildParameters.StartupDirectory; + + List

builtInProperties = new List

(12); + + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.toolsVersion, _data.Toolset.ToolsVersion)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.toolsPath, _data.Toolset.ToolsPath)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.binPath, _data.Toolset.ToolsPath)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.startupDirectory, startupDirectory)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.buildNodeCount, _maxNodeCount.ToString(CultureInfo.CurrentCulture))); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.programFiles32, FrameworkLocationHelper.programFiles32)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.assemblyVersion, Constants.AssemblyVersion)); + + if (String.IsNullOrEmpty(_projectRootElement.FullPath)) + { + // If this is an un-saved project, this is as far as we can go + if (String.IsNullOrEmpty(_projectRootElement.DirectoryPath)) + { + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectDirectory, startupDirectory)); + } + else + { + // Solution files based on the old OM end up here. But they do have a location, which is where the solution was loaded from. + // We need to set this here otherwise we can't locate any projects the solution refers to. + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectDirectory, _projectRootElement.DirectoryPath)); + } + } + else + { + // Add the MSBuildProjectXXXXX properties, but not the MSBuildFileXXXX ones. Those + // vary according to the file they're evaluated in, so they have to be dealt with + // specially in the Expander. + string projectFile = EscapingUtilities.Escape(Path.GetFileName(_projectRootElement.FullPath)); + string projectFileWithoutExtension = EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(_projectRootElement.FullPath)); + string projectExtension = EscapingUtilities.Escape(Path.GetExtension(_projectRootElement.FullPath)); + string projectFullPath = EscapingUtilities.Escape(_projectRootElement.FullPath); + string projectDirectory = EscapingUtilities.Escape(_projectRootElement.DirectoryPath); + + int rootLength = Path.GetPathRoot(projectDirectory).Length; + string projectDirectoryNoRoot = projectDirectory.Substring(rootLength); + projectDirectoryNoRoot = FileUtilities.EnsureNoTrailingSlash(projectDirectoryNoRoot); + projectDirectoryNoRoot = EscapingUtilities.Escape(FileUtilities.EnsureNoLeadingSlash(projectDirectoryNoRoot)); + + // ReservedPropertyNames.projectDefaultTargets is already set + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectFile, projectFile)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectName, projectFileWithoutExtension)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectExtension, projectExtension)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectFullPath, projectFullPath)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectDirectory, projectDirectory)); + builtInProperties.Add(SetBuiltInProperty(ReservedPropertyNames.projectDirectoryNoRoot, projectDirectoryNoRoot)); + } + + return builtInProperties; + } + + ///

+ /// Pull in all the environment into our property bag + /// + private ICollection

AddEnvironmentProperties() + { + List

environmentPropertiesList = new List

(_environmentProperties.Count); + + foreach (ProjectPropertyInstance environmentProperty in _environmentProperties) + { + P property = _data.SetProperty(environmentProperty.Name, ((IProperty)environmentProperty).EvaluatedValueEscaped, false /* NOT global property */, false /* may NOT be a reserved name */); + environmentPropertiesList.Add(property); + } + + return environmentPropertiesList; + } + + ///

+ /// Put all the toolset's properties into our property bag + /// + private ICollection

AddToolsetProperties() + { + List

toolsetProperties = new List

(_data.Toolset.Properties.Count); + + foreach (ProjectPropertyInstance toolsetProperty in _data.Toolset.Properties.Values) + { + P property = _data.SetProperty(toolsetProperty.Name, ((IProperty)toolsetProperty).EvaluatedValueEscaped, false /* NOT global property */, false /* may NOT be a reserved name */); + toolsetProperties.Add(property); + } + + return toolsetProperties; + } + + ///

+ /// Put all the sub-toolset's properties into our property bag. Run after + /// AddToolsetProperties to ensure that, if there are any overlaps, the sub-toolset wins. + /// + private ICollection

AddSubToolsetProperties() + { + List

subToolsetProperties = new List

(); + + if (_data.SubToolsetVersion != null) + { + SubToolset subToolset = null; + + // Make the subtoolset version itself available as a property -- but only if it's not already set. + // Because some people may be depending on this value even if there isn't a matching sub-toolset, + // set the property even if there is no matching sub-toolset. + if (!_data.Properties.Contains(Constants.SubToolsetVersionPropertyName)) + { + P subToolsetVersionProperty = _data.SetProperty(Constants.SubToolsetVersionPropertyName, _data.SubToolsetVersion, false /* NOT global property */, false /* may NOT be a reserved name */); + subToolsetProperties.Add(subToolsetVersionProperty); + } + + if (_data.Toolset.SubToolsets.TryGetValue(_data.SubToolsetVersion, out subToolset)) + { + foreach (ProjectPropertyInstance subToolsetProperty in subToolset.Properties.Values) + { + P property = _data.SetProperty(subToolsetProperty.Name, ((IProperty)subToolsetProperty).EvaluatedValueEscaped, false /* NOT global property */, false /* may NOT be a reserved name */); + subToolsetProperties.Add(property); + } + } + } + + return subToolsetProperties; + } + + ///

+ /// Put all the global properties into our property bag + /// + private ICollection

AddGlobalProperties() + { + if (_data.GlobalPropertiesDictionary == null) + { + return ReadOnlyEmptyList

.Instance; + } + + List

globalProperties = new List

(_data.GlobalPropertiesDictionary.Count); + + foreach (ProjectPropertyInstance globalProperty in _data.GlobalPropertiesDictionary) + { + P property = _data.SetProperty(globalProperty.Name, ((IProperty)globalProperty).EvaluatedValueEscaped, true /* IS global property */, false /* may NOT be a reserved name */); + globalProperties.Add(property); + } + + return globalProperties; + } + + ///

+ /// Set a built-in property in the supplied bag. + /// NOT to be used for properties originating in XML. + /// NOT to be used for global properties. + /// NOT to be used for environment properties. + /// + private P SetBuiltInProperty(string name, string evaluatedValueEscaped) + { + P property = _data.SetProperty(name, evaluatedValueEscaped, false /* NOT global property */, true /* OK to be a reserved name */); + return property; + } + + /// + /// Evaluate a single ProjectPropertyElement and update the data as appropriate + /// + private void EvaluatePropertyElement(ProjectPropertyElement propertyElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.EnterState(propertyElement.Location, _propertyPassLocals); + } + + // Global properties cannot be overridden. We silently ignore them if we try. Legacy behavior. + // That is, unless this global property has been explicitly labeled as one that we want to treat as overridable for the duration + // of this project (or import). + if ( + ((IDictionary)_data.GlobalPropertiesDictionary).ContainsKey(propertyElement.Name) && + !_data.GlobalPropertiesToTreatAsLocal.Contains(propertyElement.Name) + ) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(propertyElement.Location); + } + + return; + } + + if (!EvaluateConditionCollectingConditionedProperties(propertyElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties)) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(propertyElement.Location); + } + + return; + } + + // Set the name of the property we are currently evaluating so when we are checking to see if we want to add the property to the list of usedUninitialized properties we can not add the property if + // it is the same as what we are setting the value on. Note: This needs to be set before we expand the property we are currently setting. + _expander.UsedUninitializedProperties.CurrentlyEvaluatingPropertyElementName = propertyElement.Name; + + string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(propertyElement.Value, ExpanderOptions.ExpandProperties, propertyElement.Location); + + // If we are goign to set a property to a value other than null or empty we need to check to see if it has been used + // during evaluation. + if (evaluatedValue.Length > 0 && _expander.WarnForUninitializedProperties) + { + // Is the property we are currently setting in the list of properties which have been used but not initialized + IElementLocation elementWhichUsedProperty = null; + bool isPropertyInList = _expander.UsedUninitializedProperties.Properties.TryGetValue(propertyElement.Name, out elementWhichUsedProperty); + + if (isPropertyInList) + { + // Once we are going to warn for a property once, remove it from the list so we do not add it again. + _expander.UsedUninitializedProperties.Properties.Remove(propertyElement.Name); + _loggingService.LogWarning(_buildEventContext, null, new BuildEventFileInfo(propertyElement.Location), "UsedUninitializedProperty", propertyElement.Name, elementWhichUsedProperty.LocationString); + } + } + + _expander.UsedUninitializedProperties.CurrentlyEvaluatingPropertyElementName = null; + + P predecessor = _data.GetProperty(propertyElement.Name); + + P property = _data.SetProperty(propertyElement, evaluatedValue, predecessor); + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(propertyElement.Location); + } + } + + /// + /// Evaluate a single ProjectItemElement into zero or more items. + /// If specified, or if the condition on the item itself is false, only gathers the result into the list of items-ignoring-condition, + /// and not into the real list of items. + /// + private void EvaluateItemElement(bool itemGroupConditionResult, ProjectItemElement itemElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.EnterState(itemElement.Location, _itemPassLocals); + } + + bool itemConditionResult = EvaluateCondition(itemElement, ExpanderOptions.ExpandPropertiesAndItems, ParserOptions.AllowPropertiesAndItemLists); + + if (!itemConditionResult && !_data.ShouldEvaluateForDesignTime) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(itemElement.Location); + } + + return; + } + + // Paths in items are evaluated relative to the outer project file, rather than relative to any targets file they may be contained in + IList items = CreateItemsFromInclude(_projectRootElement.DirectoryPath, itemElement, _itemFactory, itemElement.Include, _expander); + + // STEP 4: Evaluate, split, expand and subtract any Exclude + if (itemElement.Exclude.Length > 0) + { + string evaluatedExclude = _expander.ExpandIntoStringLeaveEscaped(itemElement.Exclude, ExpanderOptions.ExpandPropertiesAndItems, itemElement.ExcludeLocation); + + if (evaluatedExclude.Length > 0) + { + IList excludeSplits = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedExclude); + + HashSet excludes = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string excludeSplit in excludeSplits) + { + string[] excludeSplitFiles = EngineFileUtilities.GetFileListEscaped(_projectRootElement.DirectoryPath, excludeSplit); + + foreach (string excludeSplitFile in excludeSplitFiles) + { + excludes.Add(EscapingUtilities.UnescapeAll(excludeSplitFile)); + } + } + + List remainingItems = new List(); + + for (int i = 0; i < items.Count; i++) + { + if (!excludes.Contains(items[i].EvaluatedInclude)) + { + remainingItems.Add(items[i]); + } + } + + items = remainingItems; + } + } + + // STEP 5: Evaluate each metadata XML and apply them to each item we have so far + if (itemElement.HasMetadata) + { + //////////////////////////////////////////////////// + // UNDONE: Implement batching here. + // + // We want to allow built-in metadata in metadata values here. + // For example, so that an Idl file can specify that its Tlb output should be named %(Filename).tlb. + // + // In other words, we want batching. However, we won't need to go to the trouble of using the regular batching code! + // That's because that code is all about grouping into buckets of similar items. In this context, we're not + // invoking a task, and it's fine to process each item individually, which will always give the correct results. + // + // For the CTP, to make the minimal change, we will not do this quite correctly. + // + // We will do this: + // -- check whether any metadata values or their conditions contain any bare built-in metadata expressions, + // or whether they contain any custom metadata && the Include involved an @(itemlist) expression. + // -- if either case is found, we go ahead and evaluate all the metadata separately for each item. + // -- otherwise we can do the old thing (evaluating all metadata once then applying to all items) + // + // This algorithm gives the correct results except when: + // -- batchable expressions exist on the include, exclude, or condition on the item element itself + // + // It means that 99% of cases still go through the old code, which is best for the CTP. + // When we ultimately implement this correctly, we should make sure we optimize for the case of very many items + // and little metadata, none of which varies between items. + List values = new List(itemElement.Count); + + foreach (ProjectMetadataElement metadatumElement in itemElement.Metadata) + { + values.Add(metadatumElement.Value); + values.Add(metadatumElement.Condition); + } + + ItemsAndMetadataPair itemsAndMetadataFound = ExpressionShredder.GetReferencedItemNamesAndMetadata(values); + + bool needToProcessItemsIndividually = false; + + if (itemsAndMetadataFound.Metadata != null && itemsAndMetadataFound.Metadata.Values.Count > 0) + { + // If there is bare metadata of any kind, and the Include involved an item list, we should + // run items individually, as even non-built-in metadata might differ between items + List include = new List(); + include.Add(itemElement.Include); + ItemsAndMetadataPair itemsAndMetadataFromInclude = ExpressionShredder.GetReferencedItemNamesAndMetadata(include); + + if (itemsAndMetadataFromInclude.Items != null && itemsAndMetadataFromInclude.Items.Count > 0) + { + needToProcessItemsIndividually = true; + } + else + { + // If there is bare built-in metadata, we must always run items individually, as that almost + // always differs between items. + + // UNDONE: When batching is implemented for real, we need to make sure that + // item definition metadata is included in all metadata operations during evaluation + if (itemsAndMetadataFound.Metadata.Values.Count > 0) + { + needToProcessItemsIndividually = true; + } + } + } + + if (needToProcessItemsIndividually) + { + foreach (I item in items) + { + _expander.Metadata = item; + + foreach (ProjectMetadataElement metadatumElement in itemElement.Metadata) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(metadatumElement.Location, _itemPassLocals); + } + + if (!EvaluateCondition(metadatumElement, ExpanderOptions.ExpandAll, ParserOptions.AllowAll)) + { + continue; + } + + string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadatumElement.Value, ExpanderOptions.ExpandAll, metadatumElement.Location); + + item.SetMetadata(metadatumElement, evaluatedValue); + } + } + + // End of legal area for metadata expressions. + _expander.Metadata = null; + } + + // End of pseudo batching + //////////////////////////////////////////////////// + // Start of old code + else + { + // Metadata expressions are allowed here. + // Temporarily gather and expand these in a table so they can reference other metadata elements above. + EvaluatorMetadataTable metadataTable = new EvaluatorMetadataTable(itemElement.ItemType); + _expander.Metadata = metadataTable; + + // Also keep a list of everything so we can get the predecessor objects correct. + List> metadataList = new List>(); + + foreach (ProjectMetadataElement metadatumElement in itemElement.Metadata) + { + // Because of the checking above, it should be safe to expand metadata in conditions; the condition + // will be true for either all the items or none + if (!EvaluateCondition(metadatumElement, ExpanderOptions.ExpandAll, ParserOptions.AllowAll)) + { + continue; + } + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(metadatumElement.Location, _itemPassLocals); + } + + string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadatumElement.Value, ExpanderOptions.ExpandAll, metadatumElement.Location); + + metadataTable.SetValue(metadatumElement, evaluatedValue); + metadataList.Add(new Pair(metadatumElement, evaluatedValue)); + } + + // Apply those metadata to each item + // Note that several items could share the same metadata objects + + // Set all the items at once to make a potential copy-on-write optimization possible. + // This is valuable in the case where one item element evaluates to + // many items (either by semicolon or wildcards) + // and that item also has the same piece/s of metadata for each item. + _itemFactory.SetMetadata(metadataList, items); + + // End of legal area for metadata expressions. + _expander.Metadata = null; + } + } + + // FINALLY: Add the items to the project + if (itemConditionResult && itemGroupConditionResult) + { + foreach (I item in items) + { + _data.AddItem(item); + + if (_data.ShouldEvaluateForDesignTime) + { + _data.AddToAllEvaluatedItemsList(item); + } + } + } + + if (_data.ShouldEvaluateForDesignTime) + { + foreach (I item in items) + { + _data.AddItemIgnoringCondition(item); + } + } + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(itemElement.Location); + } + } + + /// + /// Evaluates an itemdefinition element, updating the definitions library. + /// + private void EvaluateItemDefinitionElement(ProjectItemDefinitionElement itemDefinitionElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(itemDefinitionElement.Location, _itemDefinitionPassLocals); + } + + // Get matching existing item definition, if any. + IItemDefinition itemDefinition = _data.GetItemDefinition(itemDefinitionElement.ItemType); + + // The expander should use the metadata from this item definition for further expansion, if any. + // Otherwise, use a temporary, empty table. + if (itemDefinition != null) + { + _expander.Metadata = itemDefinition; + } + else + { + _expander.Metadata = new EvaluatorMetadataTable(itemDefinitionElement.ItemType); + } + + if (EvaluateCondition(itemDefinitionElement, ExpanderOptions.ExpandPropertiesAndMetadata, ParserOptions.AllowPropertiesAndCustomMetadata)) + { + if (itemDefinition == null) + { + itemDefinition = _data.AddItemDefinition(itemDefinitionElement.ItemType); + _expander.Metadata = itemDefinition; + } + + foreach (ProjectMetadataElement metadataElement in itemDefinitionElement.Metadata) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(metadataElement.Location, _itemDefinitionPassLocals); + } + + if (EvaluateCondition(metadataElement, ExpanderOptions.ExpandPropertiesAndMetadata, ParserOptions.AllowPropertiesAndCustomMetadata)) + { + string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadataElement.Value, ExpanderOptions.ExpandPropertiesAndCustomMetadata, itemDefinitionElement.Location); + + M predecessor = itemDefinition.GetMetadata(metadataElement.Name); + + M metadatum = itemDefinition.SetMetadata(metadataElement, evaluatedValue, predecessor); + + if (_data.ShouldEvaluateForDesignTime) + { + _data.AddToAllEvaluatedItemDefinitionMetadataList(metadatum); + } + } + } + } + + // End of valid area for metadata expansion. + _expander.Metadata = null; + } + + /// + /// Evaluates an import element. + /// If the condition is true, loads the import and continues the pass. + /// + /// + /// UNDONE: Protect against overflowing the stack by having too many nested imports. + /// + private void EvaluateImportElement(string directoryOfImportingFile, ProjectImportElement importElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.EnterState(importElement.Location, _propertyPassLocals); + } + + if (EvaluateConditionCollectingConditionedProperties(importElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties, _projectRootElementCache)) + { + string importExpressionEscaped = _expander.ExpandIntoStringLeaveEscaped(importElement.Project, ExpanderOptions.ExpandProperties, importElement.ProjectLocation); + + List importedProjectRootElements = ExpandAndLoadImports(directoryOfImportingFile, importExpressionEscaped, importElement); + + foreach (ProjectRootElement importedProjectRootElement in importedProjectRootElements) + { + _data.RecordImport(importElement, importedProjectRootElement, importedProjectRootElement.Version); + + // This key should be unique, as duplicate imports were already discarded + if (DebuggerManager.DebuggingEnabled) + { + _importRelationships.Add(importedProjectRootElement, importElement.ContainingProject); + } + + PerformDepthFirstPass(importedProjectRootElement); + } + } + + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.LeaveState(importElement.Location); + } + } + + /// + /// Evaluates an ImportGroup element. + /// If the condition is true, evaluates the contained imports and continues the pass. + /// + /// + /// UNDONE: Protect against overflowing the stack by having too many nested imports. + /// + private void EvaluateImportGroupElement(string directoryOfImportingFile, ProjectImportGroupElement importGroupElement) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(importGroupElement.Location, _propertyPassLocals); + } + + if (EvaluateConditionCollectingConditionedProperties(importGroupElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties, _projectRootElementCache)) + { + foreach (ProjectImportElement importElement in importGroupElement.Imports) + { + EvaluateImportElement(directoryOfImportingFile, importElement); + } + } + } + + /// + /// Choose does not accept a condition. + /// + /// + /// We enter here in both the property and item passes, since Chooses can contain both. + /// However, we only evaluate the When conditions on the first pass, so we only pulse + /// those states on that pass. On the other pass, it's as if they're not there. + /// + private void EvaluateChooseElement(ProjectChooseElement chooseElement) + { + foreach (ProjectWhenElement whenElement in chooseElement.WhenElements) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(whenElement.Location, _propertyPassLocals); + } + + if (EvaluateConditionCollectingConditionedProperties(whenElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties)) + { + EvaluateWhenOrOtherwiseChildren(whenElement.Children); + return; + } + } + + // "Otherwise" elements never have a condition + if (chooseElement.OtherwiseElement != null) + { + if (DebuggerManager.DebuggingEnabled) + { + DebuggerManager.PulseState(chooseElement.OtherwiseElement.Location, _propertyPassLocals); + } + + EvaluateWhenOrOtherwiseChildren(chooseElement.OtherwiseElement.Children); + } + } + + /// + /// Evaluates the children of a When or Choose. + /// Returns true if the condition was true, so subsequent + /// WhenElements and Otherwise can be skipped. + /// + private bool EvaluateWhenOrOtherwiseChildren(IEnumerable children) + { + foreach (ProjectElement element in children) + { + ProjectPropertyGroupElement propertyGroup = element as ProjectPropertyGroupElement; + + if (propertyGroup != null) + { + EvaluatePropertyGroupElement(propertyGroup); + continue; + } + + ProjectItemGroupElement itemGroup = element as ProjectItemGroupElement; + + if (itemGroup != null) + { + _itemGroupElements.Add(itemGroup); + continue; + } + + ProjectChooseElement choose = element as ProjectChooseElement; + + if (choose != null) + { + EvaluateChooseElement(choose); + continue; + } + + ErrorUtilities.ThrowInternalError("Unexpected child type"); + } + + return true; + } + + /// + /// Load and parse the specified project import, which may have wildcards, + /// into one or more ProjectRootElements. + /// Caches the parsed import into the provided collection, so future + /// requests can be satisfied without re-parsing it. + /// + private List ExpandAndLoadImports(string directoryOfImportingFile, string importExpressionEscaped, ProjectImportElement importElement) + { + ElementLocation importLocationInProject = importElement.Location; + + List imports = new List(); + string[] importFilesEscaped = null; + + try + { + // Handle the case of an expression expanding to nothing specially; + // force an exception here to give a nicer message, that doesn't show the project directory in it. + if (importExpressionEscaped.Length == 0 || importExpressionEscaped.Trim().Length == 0) + { + FileUtilities.NormalizePath(EscapingUtilities.UnescapeAll(importExpressionEscaped)); + } + + // Expand the wildcards and provide an alphabetical order list of import statements. + importFilesEscaped = EngineFileUtilities.GetFileListEscaped(directoryOfImportingFile, importExpressionEscaped); + } + catch (Exception ex) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(importLocationInProject, "InvalidAttributeValueWithException", EscapingUtilities.UnescapeAll(importExpressionEscaped), XMakeAttributes.project, XMakeElements.import, ex.Message); + } + + foreach (string importFileEscaped in importFilesEscaped) + { + string importFileUnescaped = EscapingUtilities.UnescapeAll(importFileEscaped); + + // GetFileListEscaped may not return a rooted path, we need to root it. Also if there are no wild cards we still need to get the full path on the filespec. + try + { + if (directoryOfImportingFile != null && !Path.IsPathRooted(importFileUnescaped)) + { + importFileUnescaped = Path.Combine(directoryOfImportingFile, importFileUnescaped); + } + + // Canonicalize to eg., eliminate "\..\" + importFileUnescaped = FileUtilities.NormalizePath(importFileUnescaped); + } + catch (Exception ex) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(importLocationInProject, "InvalidAttributeValueWithException", importFileUnescaped, XMakeAttributes.project, XMakeElements.import, ex.Message); + } + + // If a file is included twice, or there is a cycle of imports, we ignore all but the first import + // and issue a warning to that effect. + if (String.Equals(_projectRootElement.FullPath, importFileUnescaped, StringComparison.OrdinalIgnoreCase) /* We are trying to import ourselves */) + { + _loggingService.LogWarning(_buildEventContext, null, new BuildEventFileInfo(importLocationInProject), "SelfImport", importFileUnescaped); + + continue; + } + + // Circular dependencies (e.g. t0.targets imports t1.targets, t1.targets imports t2.targets and t2.targets imports t0.targets) will be + // caught by the check for duplicate imports which is done later in the method. However, if the project load setting requires throwing + // on circular imports or recording duplicate-but-not-circular imports, then we need to do exclusive check for circular imports here. + if ((_loadSettings & ProjectLoadSettings.RejectCircularImports) != 0 || (_loadSettings & ProjectLoadSettings.RecordDuplicateButNotCircularImports) != 0) + { + // Check if this import introduces circularity. + if (IntroducesCircularity(importFileUnescaped, importElement)) + { + // Get the full path of the MSBuild file that has this import. + string importedBy = importElement.ContainingProject.FullPath ?? String.Empty; + + _loggingService.LogWarning(_buildEventContext, null, new BuildEventFileInfo(importLocationInProject), "ImportIntroducesCircularity", importFileUnescaped, importedBy); + + // Throw exception if the project load settings requires us to stop the evaluation of a project when circular imports are detected. + if ((_loadSettings & ProjectLoadSettings.RejectCircularImports) != 0) + { + ProjectErrorUtilities.ThrowInvalidProject(importLocationInProject, "ImportIntroducesCircularity", importFileUnescaped, importedBy); + } + + // Ignore this import and no more further processing on it. + continue; + } + } + + ProjectImportElement previouslyImportedAt; + bool duplicateImport = false; + + if (_importsSeen.TryGetValue(importFileUnescaped, out previouslyImportedAt)) + { + string parenthesizedProjectLocation = String.Empty; + + // If neither file involved is the project itself, append its path in square brackets + if (previouslyImportedAt.ContainingProject != _projectRootElement && importElement.ContainingProject != _projectRootElement) + { + parenthesizedProjectLocation = "[" + _projectRootElement.FullPath + "]"; + } + + _loggingService.LogWarning(_buildEventContext, null, new BuildEventFileInfo(importLocationInProject), "DuplicateImport", importFileUnescaped, previouslyImportedAt.Location.LocationString, parenthesizedProjectLocation); + duplicateImport = true; + } + + ProjectRootElement importedProjectElement; + + try + { + // We take the explicit loaded flag from the project ultimately being evaluated. The goal being that + // if a project system loaded a user's project, all imports (which would include property sheets and .user file) + // may impact evaluation and should be included in the weak cache without ever being cleared out to avoid + // the project system being exposed to multiple PRE instances for the same file. We only want to consider + // clearing the weak cache (and therefore setting explicitload=false) for projects the project system never + // was directly interested in (i.e. the ones that were reached for purposes of building a P2P.) + bool explicitlyLoaded = importElement.ContainingProject.IsExplicitlyLoaded; + importedProjectElement = _projectRootElementCache.Get( + importFileUnescaped, + (p, c) => ProjectRootElement.OpenProjectOrSolution( + importFileUnescaped, + new ReadOnlyConvertingDictionary( + _data.GlobalPropertiesDictionary, + instance => ((IProperty)instance).EvaluatedValueEscaped), + _data.ExplicitToolsVersion, + _loggingService, + _projectRootElementCache, + _buildEventContext, + explicitlyLoaded), + explicitlyLoaded); + + if (duplicateImport) + { + // Only record the data if we want to record duplicate imports + if ((_loadSettings & ProjectLoadSettings.RecordDuplicateButNotCircularImports) != 0) + { + _data.RecordImportWithDuplicates(importElement, importedProjectElement, importedProjectElement.Version); + } + + // Since we have already seen this we need to not continue on in the processing. + continue; + } + else + { + imports.Add(importedProjectElement); + } + } + catch (InvalidProjectFileException ex) + { + if (ExceptionHandling.IsIoRelatedException(ex.InnerException)) + { + // The import couldn't be read from disk, or something similar. In that case, + // the error message would be more useful if it pointed to the location in the importing project file instead. + // Perhaps the import tag has a typo in, for example. + + // There's a specific message for file not existing + if (!File.Exists(importFileUnescaped)) + { + if ((_loadSettings & ProjectLoadSettings.IgnoreMissingImports) != 0) + { + continue; + } + + ProjectErrorUtilities.ThrowInvalidProject(importLocationInProject, "ImportedProjectNotFound", importFileUnescaped); + } + else + { + // Otherwise a more generic message, still pointing to the location of the import tag + ProjectErrorUtilities.ThrowInvalidProject(importLocationInProject, "InvalidImportedProjectFile", importFileUnescaped, ex.InnerException.Message); + } + } + + throw; + } + + // Because these expressions will never be expanded again, we + // can store the unescaped value. The only purpose of escaping is to + // avoid undesired splitting or expansion. + _importsSeen.Add(importFileUnescaped, importElement); + } + + return imports; + } + + /// + /// Checks if an import matches with another import in its ancestor line of imports. + /// + /// The import that is being added. + /// The importing element for this import. + /// True, if and only if this import introduces a circularity. + private bool IntroducesCircularity(string importFileUnescaped, ProjectImportElement importElement) + { + bool foundMatchingAncestor = false; + + // While we haven't found a matching ancestor haven't reach the project node, + // keep climbing the import chain and checking for matches. + while (importElement != null) + { + // Get the full path of the MSBuild file that imports this file. + string importedBy = importElement.ContainingProject.FullPath; + + if (String.Equals(importFileUnescaped, importedBy, StringComparison.OrdinalIgnoreCase)) + { + // Circular dependency found! + foundMatchingAncestor = true; + break; + } + + if (!String.IsNullOrEmpty(importedBy)) // The full path of a project loaded from memory can be null. + { + // Set the "counter" to the importing project. + _importsSeen.TryGetValue(importedBy, out importElement); + } + else + { + importElement = null; + } + } + + return foundMatchingAncestor; + } + + /// + /// Evaluate a given condition + /// + private bool EvaluateCondition(ProjectElement element, ExpanderOptions expanderOptions, ParserOptions parserOptions) + { + if (element.Condition.Length == 0) + { + return true; + } + + bool result = ConditionEvaluator.EvaluateCondition + ( + element.Condition, + parserOptions, + _expander, + expanderOptions, + GetCurrentDirectoryForConditionEvaluation(element), + element.ConditionLocation, + _loggingService, + _buildEventContext + ); + + return result; + } + + /// + /// Evaluate a given condition, collecting conditioned properties. + /// + private bool EvaluateConditionCollectingConditionedProperties(ProjectElement element, ExpanderOptions expanderOptions, ParserOptions parserOptions, ProjectRootElementCache projectRootElementCache = null) + { + if (element.Condition.Length == 0) + { + return true; + } + + if (!_data.ShouldEvaluateForDesignTime) + { + return EvaluateCondition(element, expanderOptions, parserOptions); + } + + bool result = ConditionEvaluator.EvaluateConditionCollectingConditionedProperties + ( + element.Condition, + parserOptions, + _expander, + expanderOptions, + _data.ConditionedProperties, + GetCurrentDirectoryForConditionEvaluation(element), + element.ConditionLocation, + _loggingService, + _buildEventContext, + projectRootElementCache + ); + + return result; + } + + /// + /// COMPAT: Whidbey used the "current project file/targets" directory for evaluating Import and PropertyGroup conditions + /// Orcas broke this by using the current root project file for all conditions + /// For Dev10+, we'll fix this, and use the current project file/targets directory for Import, ImportGroup and PropertyGroup + /// but the root project file for the rest. Inside of targets will use the root project file as always. + /// + private string GetCurrentDirectoryForConditionEvaluation(ProjectElement element) + { + if (element is ProjectPropertyGroupElement || element is ProjectImportElement || element is ProjectImportGroupElement) + { + return element.ContainingProject.DirectoryPath; + } + else + { + return _data.Directory; + } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/EvaluatorMetadataTable.cs b/src/XMakeBuildEngine/Evaluation/EvaluatorMetadataTable.cs new file mode 100644 index 00000000000..3562c8c2406 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/EvaluatorMetadataTable.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a set of metadata for the evaluator. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; + +using EscapingUtilities = Microsoft.Build.Shared.EscapingUtilities; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Implementation of a metadata table for use by the evaluator. + /// Accumulates ProjectMetadataElement objects and their evaluated value, + /// overwriting any previous metadata with that name. + /// + internal class EvaluatorMetadataTable : IMetadataTable + { + /// + /// The actual metadata dictionary. + /// + private Dictionary _metadata; + + /// + /// The type of item the metadata should be considered to apply to. + /// + private string _implicitItemType; + + /// + /// Creates a new table using the specified item type. + /// + public EvaluatorMetadataTable(string implicitItemType) + { + _implicitItemType = implicitItemType; + } + + /// + /// Enumerator over the entries in this table + /// + internal IEnumerable Entries + { + get + { + if (_metadata == null) + { + return ReadOnlyEmptyList.Instance; + } + + return _metadata.Values; + } + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name specified, + /// whatever the item type. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string name) + { + return GetEscapedValue(null, name); + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string itemType, string name) + { + return GetEscapedValueIfPresent(itemType, name) ?? String.Empty; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If no value is available, returns null. + /// + public string GetEscapedValueIfPresent(string itemType, string name) + { + if (_metadata == null) + { + return null; + } + + string value = null; + + if (itemType == null || String.Equals(_implicitItemType, itemType, StringComparison.OrdinalIgnoreCase)) + { + EvaluatorMetadata metadatum; + _metadata.TryGetValue(name, out metadatum); + + if (metadatum != null) + { + value = metadatum.EvaluatedValueEscaped; + } + } + + return value; + } + + /// + /// Adds a metadata entry to the table + /// + internal void SetValue(ProjectMetadataElement xml, string evaluatedValueEscaped) + { + if (_metadata == null) + { + _metadata = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + } + + _metadata[xml.Name] = new EvaluatorMetadata(xml, evaluatedValueEscaped); + } + + /// + /// An entry in the evaluator's metadata table. + /// + public class EvaluatorMetadata + { + /// + /// Construct a new EvaluatorMetadata + /// + public EvaluatorMetadata(ProjectMetadataElement xml, string evaluatedValueEscaped) + { + this.Xml = xml; + this.EvaluatedValueEscaped = evaluatedValueEscaped; + } + + /// + /// Gets or sets the metadata Xml + /// + public ProjectMetadataElement Xml + { + get; + private set; + } + + /// + /// Gets or sets the evaluated value, unescaped + /// + public string EvaluatedValue + { + get + { + return EscapingUtilities.UnescapeAll(EvaluatedValueEscaped); + } + } + + /// + /// Gets or sets the evaluated value, escaped as necessary + /// + internal string EvaluatedValueEscaped + { + get; + private set; + } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Expander.cs b/src/XMakeBuildEngine/Evaluation/Expander.cs new file mode 100644 index 00000000000..c498366fcf4 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Expander.cs @@ -0,0 +1,3553 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Expands item/property/metadata in expressions. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Linq; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Win32; +using AvailableStaticMethods = Microsoft.Build.Internal.AvailableStaticMethods; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; +using Microsoft.Build.Collections; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using TaskItemFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.TaskItemFactory; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Indicates to the expander what exactly it should expand. + /// + [Flags] + internal enum ExpanderOptions + { + /// + /// Invalid + /// + Invalid = 0x0, + + /// + /// Expand bare custom metadata, like %(foo), but not built-in + /// metadata, such as %(filename) or %(identity) + /// + ExpandCustomMetadata = 0x1, + + /// + /// Expand bare built-in metadata, such as %(filename) or %(identity) + /// + ExpandBuiltInMetadata = 0x2, + + /// + /// Expand all bare metadata + /// + ExpandMetadata = ExpandCustomMetadata | ExpandBuiltInMetadata, + + /// + /// Expand only properties + /// + ExpandProperties = 0x4, + + /// + /// Expand only item list expressions + /// + ExpandItems = 0x8, + + /// + /// If the expression is going to not be an empty string, break + /// out early + /// + BreakOnNotEmpty = 0x10, + + /// + /// Expand only properties and then item lists + /// + ExpandPropertiesAndItems = ExpandProperties | ExpandItems, + + /// + /// Expand only bare metadata and then properties + /// + ExpandPropertiesAndMetadata = ExpandMetadata | ExpandProperties, + + /// + /// Expand only bare custom metadata and then properties + /// + ExpandPropertiesAndCustomMetadata = ExpandCustomMetadata | ExpandProperties, + + /// + /// Expand bare metadata, then properties, then item expressions + /// + ExpandAll = ExpandMetadata | ExpandProperties | ExpandItems + } + + /// + /// Expands item/property/metadata in expressions. + /// Encapsulates the data necessary for expansion. + /// + /// + /// Requires the caller to explicitly state what they wish to expand at the point of expansion (explicitly does not have a field for ExpanderOptions). + /// Callers typically use a single expander in many locations, and this forces the caller to make explicit what they wish to expand at the point of expansion. + /// + /// Requires the caller to have previously provided the necessary material for the expansion requested. + /// For example, if the caller requests ExpanderOptions.ExpandItems, the Expander will throw if it was not given items. + /// + /// Type of the properties used + /// Type of the items used. + internal class Expander + where P : class, IProperty + where I : class, IItem + { + /// + /// Those characters which indicate that an expression may contain expandable + /// expressions + /// + private static char[] s_expandableChars = { '$', '%', '@' }; + + /// + /// The CultureInfo from the invariant culture. Used to avoid allocations for + /// perfoming IndexOf etc. + /// + private static CompareInfo s_invariantCompareInfo = CultureInfo.InvariantCulture.CompareInfo; + + /// + /// Properties to draw on for expansion + /// + private IPropertyProvider

_properties; + + ///

+ /// Items to draw on for expansion + /// + private IItemProvider _items; + + /// + /// Metadata to draw on for expansion + /// + private IMetadataTable _metadata; + + /// + /// Set of properties which are null during expansion + /// + private UsedUninitializedProperties _usedUninitializedProperties; + + /// + /// Creates an expander passing it some properties to use. + /// Properties may be null. + /// + internal Expander(IPropertyProvider

properties) + { + _properties = properties; + _usedUninitializedProperties = new UsedUninitializedProperties(); + } + + ///

+ /// Creates an expander passing it some properties and items to use. + /// Either or both may be null. + /// + internal Expander(IPropertyProvider

properties, IItemProvider items) + : this(properties) + { + _items = items; + } + + ///

+ /// Creates an expander passing it some properties, items, and/or metadata to use. + /// Any or all may be null. + /// + internal Expander(IPropertyProvider

properties, IItemProvider items, IMetadataTable metadata) + : this(properties, items) + { + _metadata = metadata; + } + + ///

+ /// Whether to warn when we set a property for the first time, after it was previously used. + /// Default is false, unless MSBUILDWARNONUNINITIALIZEDPROPERTY is set. + /// + internal bool WarnForUninitializedProperties + { + get { return _usedUninitializedProperties.Warn; } + set { _usedUninitializedProperties.Warn = value; } + } + + /// + /// Accessor for the metadata. + /// Set temporarily during item metadata evaluation. + /// + internal IMetadataTable Metadata + { + get { return _metadata; } + set { _metadata = value; } + } + + /// + /// If a property is expanded but evaluates to null then it is consisered to be un-initialized. + /// We want to keep track of these properties so that we can warn if the property gets set later on. + /// + internal UsedUninitializedProperties UsedUninitializedProperties + { + get { return _usedUninitializedProperties; } + set { _usedUninitializedProperties = value; } + } + + /// + /// Tests to see if the expression may contain expandable expressions, i.e. + /// contains $, % or @ + /// + internal static bool ExpressionMayContainExpandableExpressions(string expression) + { + return expression.IndexOfAny(s_expandableChars) > -1; + } + + /// + /// Returns true if the expression contains an item vector pattern, else returns false. + /// Used to flag use of item expressions where they are illegal. + /// + internal static bool ExpressionContainsItemVector(string expression) + { + List transforms = ExpressionShredder.GetReferencedItemExpressions(expression); + + return (transforms != null); + } + + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options. + /// This is the standard form. Before using the expanded value, it must be unescaped, and this does that for you. + /// + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal string ExpandIntoStringAndUnescape(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + string result = ExpandIntoStringLeaveEscaped(expression, options, elementLocation); + + result = (result == null) ? null : EscapingUtilities.UnescapeAll(result); + + return result; + } + + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options. + /// Use this form when the result is going to be processed further, for example by matching against the file system, + /// so literals must be distinguished, and you promise to unescape after that. + /// + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal string ExpandIntoStringLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + if (expression.Length == 0) + { + return String.Empty; + } + + ErrorUtilities.VerifyThrowInternalNull(elementLocation, "elementLocation"); + + string result = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options); + result = PropertyExpander

.ExpandPropertiesLeaveEscaped(result, _properties, options, elementLocation, _usedUninitializedProperties); + result = ItemExpander.ExpandItemVectorsIntoString(this, result, _items, options, elementLocation); + + return result; + } + + ///

+ /// Used only for unit tests. Expands the property expression (including any metadata expressions) and returns + /// the result typed (i.e. not converted into a string if the result is a function return) + /// + internal object ExpandPropertiesLeaveTypedAndEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + if (expression.Length == 0) + { + return String.Empty; + } + + ErrorUtilities.VerifyThrowInternalNull(elementLocation, "elementLocation"); + + string metaExpanded = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options); + return PropertyExpander

.ExpandPropertiesLeaveTypedAndEscaped(metaExpanded, _properties, options, elementLocation, _usedUninitializedProperties); + } + + ///

+ /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options, + /// then splits on semi-colons into a list of strings. + /// Use this form when the result is going to be processed further, for example by matching against the file system, + /// so literals must be distinguished, and you promise to unescape after that. + /// + internal IList ExpandIntoStringListLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + ErrorUtilities.VerifyThrow((options & ExpanderOptions.BreakOnNotEmpty) == 0, "not supported"); + + return ExpressionShredder.SplitSemiColonSeparatedList(ExpandIntoStringLeaveEscaped(expression, options, elementLocation)); + } + + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options + /// and produces a list of TaskItems. + /// If the expression is empty, returns an empty list. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + internal IList ExpandIntoTaskItemsLeaveEscaped(string expression, ExpanderOptions options, IElementLocation elementLocation) + { + return ExpandIntoItemsLeaveEscaped(expression, (IItemFactory)TaskItemFactory.Instance, options, elementLocation); + } + + /// + /// Expands embedded item metadata, properties, and embedded item lists (in that order) as specified in the provided options + /// and produces a list of items of the type for which it was specialized. + /// If the expression is empty, returns an empty list. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// Use this form when the result is going to be processed further, for example by matching against the file system, + /// so literals must be distinguished, and you promise to unescape after that. + /// + /// Type of items to return + internal IList ExpandIntoItemsLeaveEscaped(string expression, IItemFactory itemFactory, ExpanderOptions options, IElementLocation elementLocation) + where T : class, IItem + { + if (expression.Length == 0) + { + return ReadOnlyEmptyList.Instance; + } + + ErrorUtilities.VerifyThrowInternalNull(elementLocation, "elementLocation"); + + expression = MetadataExpander.ExpandMetadataLeaveEscaped(expression, _metadata, options); + expression = PropertyExpander

.ExpandPropertiesLeaveEscaped(expression, _properties, options, elementLocation, _usedUninitializedProperties); + + List result = new List(); + + if (expression.Length == 0) + { + return result; + } + + IList splits = ExpressionShredder.SplitSemiColonSeparatedList(expression); + + foreach (string split in splits) + { + bool isTransformExpression; + IList itemsToAdd = ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, split, _items, itemFactory, options, false /* do not include null items */, out isTransformExpression, elementLocation); + + if ((itemsToAdd == null /* broke out early non empty */ || (itemsToAdd.Count > 0)) && (options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return null; + } + + if (itemsToAdd != null) + { + result.AddRange(itemsToAdd); + } + else + { + // The expression is not of the form @(itemName). Therefore, just + // treat it as a string, and create a new item from that string. + T itemToAdd = itemFactory.CreateItem(split, elementLocation.File); + + result.Add(itemToAdd); + } + } + + return result; + } + + ///

+ /// This is a specialized method for the use of TargetUpToDateChecker and Evaluator.EvaluateItemXml only. + /// + /// Extracts the items in the given SINGLE item vector. + /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list. + /// + /// If there is in fact more than one vector in the expression, throws InvalidProjectFileException. + /// + /// If there are no item expressions in the expression (for example a literal "foo.cpp"), returns null. + /// If expression expands to no items, returns an empty list. + /// If item expansion is not allowed by the provided options, returns null. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because + /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means + /// that the length of the returned list is always the same as the length of the referenced item list in the input string. + /// That's important for any correlation the caller wants to do. + /// + /// If expression was a transform, 'isTransformExpression' is true, otherwise false. + /// + /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not + /// have an item type set on it, it will be given the item type of the item vector to use. + /// + /// Type of the items that should be returned + internal IList ExpandSingleItemVectorExpressionIntoItems(string expression, IItemFactory itemFactory, ExpanderOptions options, bool includeNullItems, out bool isTransformExpression, IElementLocation elementLocation) + where T : class, IItem + { + if (expression.Length == 0) + { + isTransformExpression = false; + return ReadOnlyEmptyList.Instance; + } + + ErrorUtilities.VerifyThrowInternalNull(elementLocation, "elementLocation"); + + return ItemExpander.ExpandSingleItemVectorExpressionIntoItems(this, expression, _items, itemFactory, options, includeNullItems, out isTransformExpression, elementLocation); + } + + /// + /// Returns true if the supplied string contains a valid property name + /// + private static bool IsValidPropertyName(string propertyName) + { + if (propertyName.Length == 0 || !XmlUtilities.IsValidInitialElementNameCharacter(propertyName[0])) + { + return false; + } + + for (int n = 1; n < propertyName.Length; n++) + { + if (!XmlUtilities.IsValidSubsequentElementNameCharacter(propertyName[n])) + { + return false; + } + } + + return true; + } + + /// + /// Scan for the closing bracket that matches the one we've already skipped; + /// essentially, pushes and pops on a stack of parentheses to do this. + /// Takes the expression and the index to start at. + /// Returns the index of the matching parenthesis, or -1 if it was not found. + /// + private static int ScanForClosingParenthesis(string expression, int index) + { + bool potentialPropertyFunction = false; + bool potentialRegistryFunction = false; + return ScanForClosingParenthesis(expression, index, out potentialPropertyFunction, out potentialRegistryFunction); + } + + /// + /// Scan for the closing bracket that matches the one we've already skipped; + /// essentially, pushes and pops on a stack of parentheses to do this. + /// Takes the expression and the index to start at. + /// Returns the index of the matching parenthesis, or -1 if it was not found. + /// Also returns flags to indicate if a propertyfunction or registry property is likely + /// to be found in the expression + /// + private static int ScanForClosingParenthesis(string expression, int index, out bool potentialPropertyFunction, out bool potentialRegistryFunction) + { + int nestLevel = 1; + int length = expression.Length; + + potentialPropertyFunction = false; + potentialRegistryFunction = false; + + unsafe + { + fixed (char* pchar = expression) + { + // Scan for our closing ')' + while (index < length && nestLevel > 0) + { + char character = pchar[index]; + + if (character == '\'' || character == '`' || character == '"') + { + index++; + index = ScanForClosingQuote(character, expression, index); + + if (index < 0) + { + return -1; + } + } + else if (character == '(') + { + nestLevel++; + } + else if (character == ')') + { + nestLevel--; + } + else if (character == '.' || character == '[' || character == '$') + { + potentialPropertyFunction = true; + } + else if (character == ':') + { + potentialRegistryFunction = true; + } + + index++; + } + } + } + + // We will have parsed past the ')', so step back one character + index--; + + return (nestLevel == 0) ? index : -1; + } + + /// + /// Skip all characters until we find the matching quote character + /// + private static int ScanForClosingQuote(char quoteChar, string expression, int index) + { + unsafe + { + fixed (char* pchar = expression) + { + // Scan for our closing quoteChar + while (index < expression.Length) + { + if (pchar[index] == quoteChar) + { + return index; + } + + index++; + } + } + } + + return -1; + } + + /// + /// Add the argument in the StringBuilder to the arguments list, handling nulls + /// appropriately + /// + private static void AddArgument(List arguments, ReuseableStringBuilder argumentBuilder) + { + // If we don't have something that can be treated as an argument + // then we should treat it as a null so that passing nulls + // becomes possible through an empty argument between commas. + ErrorUtilities.VerifyThrowArgumentNull(argumentBuilder, "argumentBuilder"); + + // we reached the end of an argument, add the builder's final result + // to our arguments. + string argValue = OpportunisticIntern.InternableToString(argumentBuilder).Trim(); + + // We support passing of null through the argument constant value null + if (String.Compare("null", argValue, StringComparison.OrdinalIgnoreCase) == 0) + { + arguments.Add(null); + } + else + { + if (argValue.Length > 0) + { + if (argValue[0] == '\'' && argValue[argValue.Length - 1] == '\'') + { + arguments.Add(argValue.Trim('\'')); + } + else if (argValue[0] == '`' && argValue[argValue.Length - 1] == '`') + { + arguments.Add(argValue.Trim('`')); + } + else if (argValue[0] == '"' && argValue[argValue.Length - 1] == '"') + { + arguments.Add(argValue.Trim('"')); + } + else + { + arguments.Add(argValue); + } + } + else + { + arguments.Add(argValue); + } + } + } + + /// + /// Extract the first level of arguments from the content. + /// Splits the content passed in at commas. + /// Returns an array of unexpanded arguments. + /// If there are no arguments, returns an empty array. + /// + private static string[] ExtractFunctionArguments(IElementLocation elementLocation, string expressionFunction, string argumentsString) + { + int argumentsContentLength = argumentsString.Length; + + List arguments = new List(); + + // With the reuseable string builder, there's no particular need to initialize the length as it will already have grown. + using (var argumentBuilder = new ReuseableStringBuilder()) + { + unsafe + { + fixed (char* argumentsContent = argumentsString) + { + // Iterate over the contents of the arguments extracting the + // the individual arguments as we go + for (int n = 0; n < argumentsContentLength; n++) + { + // We found a property expression.. skip over all of it. + if ((n < argumentsContentLength - 1) && (argumentsContent[n] == '$' && argumentsContent[n + 1] == '(')) + { + int nestedPropertyStart = n; + n += 2; // skip over the opening '$(' + + // Scan for the matching closing bracket, skipping any nested ones + n = ScanForClosingParenthesis(argumentsString, n); + + if (n == -1) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis")); + } + + argumentBuilder.Append(argumentsString.Substring(nestedPropertyStart, (n - nestedPropertyStart) + 1)); + } + else if (argumentsContent[n] == '`' || argumentsContent[n] == '"' || argumentsContent[n] == '\'') + { + int quoteStart = n; + n += 1; // skip over the opening quote + + n = ScanForClosingQuote(argumentsString[quoteStart], argumentsString, n); + + if (n == -1) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedQuote")); + } + + argumentBuilder.Append(argumentsString.Substring(quoteStart, (n - quoteStart) + 1)); + } + else if (argumentsContent[n] == ',') + { + // We have reached the end of the current argument, go ahead and add it + // to our list + AddArgument(arguments, argumentBuilder); + + // Clear out the argument builder ready for the next argument + argumentBuilder.Remove(0, argumentBuilder.Length); + } + else + { + argumentBuilder.Append(argumentsContent[n]); + } + } + } + } + + // This will either be the one and only argument, or the last one + // so add it to our list + AddArgument(arguments, argumentBuilder); + } + + return arguments.ToArray(); + } + + /// + /// Expands bare metadata expressions, like %(Compile.WarningLevel), or unqualified, like %(Compile). + /// + /// + /// This is a private nested class, exposed only through the Expander class. + /// That allows it to hide its private methods even from Expander. + /// + private static class MetadataExpander + { + /// + /// Expands all embedded item metadata in the given string, using the bucketed items. + /// Metadata may be qualified, like %(Compile.WarningLevel), or unqualified, like %(Compile) + /// + /// The expression containing item metadata references + /// The string with item metadata expanded in-place, escaped. + internal static string ExpandMetadataLeaveEscaped(string expression, IMetadataTable metadata, ExpanderOptions options) + { + if (((options & ExpanderOptions.ExpandMetadata) == 0)) + { + return expression; + } + + if (expression.Length == 0) + { + return expression; + } + + ErrorUtilities.VerifyThrow(metadata != null, "Cannot expand metadata without providing metadata"); + + // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item metadata references, just bail + // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! + if (s_invariantCompareInfo.IndexOf(expression, "%(", CompareOptions.Ordinal) == -1) + { + return expression; + } + + string result = null; + + if (s_invariantCompareInfo.IndexOf(expression, "@(", CompareOptions.Ordinal) == -1) + { + // if there are no item vectors in the string + // run a simpler Regex to find item metadata references + MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options); + result = RegularExpressions.ItemMetadataPattern.Replace(expression, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata)); + } + else + { + List itemVectorExpressions = ExpressionShredder.GetReferencedItemExpressions(expression); + + // The most common case is where the transform is the whole expression + // Also if there were no valid item vector expressions found, then go ahead and do the replacement on + // the whole expression (which is what Orcas did). + if (itemVectorExpressions != null && itemVectorExpressions.Count == 1 && itemVectorExpressions[0].Value == expression && itemVectorExpressions[0].Separator == null) + { + return expression; + } + + // otherwise, run the more complex Regex to find item metadata references not contained in transforms + // With the reuseable string builder, there's no particular need to initialize the length as it will already have grown. + using (var finalResultBuilder = new ReuseableStringBuilder()) + { + int start = 0; + MetadataMatchEvaluator matchEvaluator = new MetadataMatchEvaluator(metadata, options); + + if (itemVectorExpressions != null) + { + // Move over the expression, skipping those that have been recognized as an item vector expression + // Anything other than an item vector expression we want to expand bare metadata in. + for (int n = 0; n < itemVectorExpressions.Count; n++) + { + string vectorExpression = itemVectorExpressions[n].Value; + + // Extract the part of the expression that appears before the item vector expression + // e.g. the ABC in ABC@(foo->'%(FullPath)') + string subExpressionToReplaceIn = expression.Substring(start, itemVectorExpressions[n].Index - start); + string replacementResult = RegularExpressions.NonTransformItemMetadataPattern.Replace(subExpressionToReplaceIn, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata)); + + // Append the metadata replacement + finalResultBuilder.Append(replacementResult); + + // Expand any metadata that appears in the item vector expression's separator + if (itemVectorExpressions[n].Separator != null) + { + vectorExpression = RegularExpressions.NonTransformItemMetadataPattern.Replace(itemVectorExpressions[n].Value, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata), -1, itemVectorExpressions[n].SeparatorStart); + } + + // Append the item vector expression as is + // e.g. the @(foo->'%(FullPath)') in ABC@(foo->'%(FullPath)') + finalResultBuilder.Append(vectorExpression); + + // Move onto the next part of the expression that isn't an item vector expression + start = (itemVectorExpressions[n].Index + itemVectorExpressions[n].Length); + } + } + + // If there's anything left after the last item vector expression + // then we need to metadata replace and then append that + if (start < expression.Length) + { + string subExpressionToReplaceIn = expression.Substring(start); + string replacementResult = RegularExpressions.NonTransformItemMetadataPattern.Replace(subExpressionToReplaceIn, new MatchEvaluator(matchEvaluator.ExpandSingleMetadata)); + + finalResultBuilder.Append(replacementResult); + } + + result = OpportunisticIntern.InternableToString(finalResultBuilder); + } + } + + // Don't create more strings + if (String.Equals(result, expression, StringComparison.Ordinal)) + { + result = expression; + } + + return result; + } + + /// + /// A functor that returns the value of the metadata in the match + /// that is contained in the metadata dictionary it was created with. + /// + private class MetadataMatchEvaluator + { + /// + /// Source of the metadata + /// + private IMetadataTable _metadata; + + /// + /// Whether to expand built-in metadata, custom metadata, or both kinds. + /// + private ExpanderOptions _options; + + /// + /// Constructor taking a source of metadata + /// + internal MetadataMatchEvaluator(IMetadataTable metadata, ExpanderOptions options) + { + _metadata = metadata; + _options = (options & ExpanderOptions.ExpandMetadata); + + ErrorUtilities.VerifyThrow(options != ExpanderOptions.Invalid, "Must be expanding metadata of some kind"); + } + + /// + /// Expands a single item metadata, which may be qualified with an item type. + /// + internal string ExpandSingleMetadata(Match itemMetadataMatch) + { + ErrorUtilities.VerifyThrow(itemMetadataMatch.Success, "Need a valid item metadata."); + + string metadataName = itemMetadataMatch.Groups[RegularExpressions.NameGroup].Value; + string itemType = null; + + // check if the metadata is qualified with the item type + if (itemMetadataMatch.Groups[RegularExpressions.ItemSpecificationGroup].Length > 0) + { + itemType = itemMetadataMatch.Groups[RegularExpressions.ItemTypeGroup].Value; + } + + // look up the metadata - we may not have a value for it + string metadataValue = itemMetadataMatch.Value; + + bool isBuiltInMetadata = FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName); + + if ( + (isBuiltInMetadata && ((_options & ExpanderOptions.ExpandBuiltInMetadata) != 0)) || + (!isBuiltInMetadata && ((_options & ExpanderOptions.ExpandCustomMetadata) != 0)) + ) + { + metadataValue = _metadata.GetEscapedValue(itemType, metadataName); + } + + return metadataValue; + } + } + } + + /// + /// Expands property expressions, like $(Configuration) and $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation) + /// + /// + /// This is a private nested class, exposed only through the Expander class. + /// That allows it to hide its private methods even from Expander. + /// + /// Type of the properties used to expand the expression + private static class PropertyExpander + where T : class, IProperty + { + /// + /// This method takes a string which may contain any number of + /// "$(propertyname)" tags in it. It replaces all those tags with + /// the actual property values, and returns a new string. For example, + /// + /// string processedString = + /// propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo)."); + /// + /// This code might produce: + /// + /// processedString = "Value of NoLogo is true." + /// + /// If the sourceString contains an embedded property which doesn't + /// have a value, then we replace that tag with an empty string. + /// + /// This method leaves the result escaped. Callers may need to unescape on their own as appropriate. + /// + internal static string ExpandPropertiesLeaveEscaped(string expression, IPropertyProvider properties, ExpanderOptions options, IElementLocation elementLocation, UsedUninitializedProperties usedUninitializedProperties) + { + return ConvertToString(ExpandPropertiesLeaveTypedAndEscaped(expression, properties, options, elementLocation, usedUninitializedProperties)); + } + + /// + /// This method takes a string which may contain any number of + /// "$(propertyname)" tags in it. It replaces all those tags with + /// the actual property values, and returns a new string. For example, + /// + /// string processedString = + /// propertyBag.ExpandProperties("Value of NoLogo is $(NoLogo)."); + /// + /// This code might produce: + /// + /// processedString = "Value of NoLogo is true." + /// + /// If the sourceString contains an embedded property which doesn't + /// have a value, then we replace that tag with an empty string. + /// + /// This method leaves the result typed and escaped. Callers may need to convert to string, and unescape on their own as appropriate. + /// + internal static object ExpandPropertiesLeaveTypedAndEscaped(string expression, IPropertyProvider properties, ExpanderOptions options, IElementLocation elementLocation, UsedUninitializedProperties usedUninitializedProperties) + { + if (((options & ExpanderOptions.ExpandProperties) == 0) || String.IsNullOrEmpty(expression)) + { + return expression; + } + + ErrorUtilities.VerifyThrow(properties != null, "Cannot expand properties without providing properties"); + + // These are also zero-based indices into the expression, but + // these tell us where the current property tag begins and ends. + int propertyStartIndex, propertyEndIndex; + + // If there are no substitutions, then just return the string. + propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", CompareOptions.Ordinal); + if (propertyStartIndex == -1) + { + return expression; + } + + // We will build our set of results as object components + // so that we can either maintain the object's type in the event + // that we have a single component, or convert to a string + // if concatenation is required. + List results = null; + object lastResult = null; + + // The sourceIndex is the zero-based index into the expression, + // where we've essentially read up to and copied into the target string. + int sourceIndex = 0; + int expressionLength = expression.Length; + + // Search for "$(" in the expression. Loop until we don't find it + // any more. + while (propertyStartIndex != -1) + { + if (lastResult != null) + { + if (results == null) + { + results = new List(4); + } + + results.Add(lastResult); + } + + bool tryExtractPropertyFunction = false; + bool tryExtractRegistryFunction = false; + + // Append the result with the portion of the expression up to + // (but not including) the "$(", and advance the sourceIndex pointer. + if (propertyStartIndex - sourceIndex > 0) + { + if (results == null) + { + results = new List(4); + } + + results.Add(expression.Substring(sourceIndex, propertyStartIndex - sourceIndex)); + } + + sourceIndex = propertyStartIndex; + + // Following the "$(" we need to locate the matching ')' + // Scan for the matching closing bracket, skipping any nested ones + // This is a very complete, fast validation of parenthesis matching including for nested + // function calls. + propertyEndIndex = ScanForClosingParenthesis(expression, propertyStartIndex + 2, out tryExtractPropertyFunction, out tryExtractRegistryFunction); + + if (propertyEndIndex == -1) + { + // If we didn't find the closing parenthesis, that means this + // isn't really a well-formed property tag. Just literally + // copy the remainder of the expression (starting with the "$(" + // that we found) into the result, and quit. + lastResult = expression.Substring(propertyStartIndex, expression.Length - propertyStartIndex); + sourceIndex = expression.Length; + } + else + { + // Aha, we found the closing parenthesis. All the stuff in + // between the "$(" and the ")" constitutes the property body. + // Note: Current propertyStartIndex points to the "$", and + // propertyEndIndex points to the ")". That's why we have to + // add 2 for the start of the substring, and subtract 2 for + // the length. + string propertyBody; + + // A property value of null will indicate that we're calling a static function on a type + object propertyValue = null; + + // Compat: $() should return String.Empty + if (propertyStartIndex + 2 == propertyEndIndex) + { + propertyValue = String.Empty; + } + else if ((expression.Length - (propertyStartIndex + 2)) > 9 && tryExtractRegistryFunction && s_invariantCompareInfo.IndexOf(expression, "Registry:", propertyStartIndex + 2, 9, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2) + { + propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2); + + // If the property body starts with any of our special objects, then deal with them + // This is a registry reference, like $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation) + propertyValue = ExpandRegistryValue(propertyBody, elementLocation); + } + + // Compat hack: as a special case, $(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory) should return String.Empty + // In this case, tryExtractRegistryFunction will be false. Note that very few properties are exactly 77 chars, so this check should be fast. + else if ((propertyEndIndex - (propertyStartIndex + 2)) == 77 && s_invariantCompareInfo.IndexOf(expression, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory", propertyStartIndex + 2, 77, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2) + { + propertyValue = String.Empty; + } + + // Compat hack: WebProjects may have an import with a condition like: + // Condition=" '$(Solutions.VSVersion)' == '8.0'" + // These would have been '' in prior versions of msbuild but would be treated as a possible string function in current versions. + // Be compatible by returning an empty string here. + else if ((propertyEndIndex - (propertyStartIndex + 2)) == 19 && String.Equals(expression, "$(Solutions.VSVersion)", StringComparison.Ordinal)) + { + propertyValue = String.Empty; + } + else if (tryExtractPropertyFunction) + { + propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2); + + // This is likely to be a function expression + propertyValue = ExpandPropertyBody(propertyBody, propertyValue, properties, options, elementLocation, usedUninitializedProperties); + } + else // This is a regular property + { + propertyValue = LookupProperty(properties, expression, propertyStartIndex + 2, propertyEndIndex - 1, elementLocation, usedUninitializedProperties); + } + + // Record our result, and advance + // our sourceIndex pointer to the character just after the closing + // parenthesis. + lastResult = propertyValue; + sourceIndex = propertyEndIndex + 1; + } + + propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", sourceIndex, CompareOptions.Ordinal); + } + + // If we have only a single result, then just return it + if (results == null && expression.Length == sourceIndex) + { + return lastResult; + } + else + { + // The expression is constant, return it as is + if (sourceIndex == 0) + { + return expression; + } + + // We have more than one result collected, therefore we need to concatenate + // into the final result string. This does mean that we will lose type information. + // However since the user wanted contatenation, then they clearly wanted that to happen. + + // Initialize our output string to empty string. + // This method is called very often - of the order of 3,000 times per project. + // With the reuseable string builder, there's no particular need to initialize the length as it will already have grown. + using (var result = new ReuseableStringBuilder()) + { + // Append our collected results + if (results != null) + { + // Create a combined result string from the result components that we've gathered + foreach (object component in results) + { + result.Append(component.ToString()); + } + } + + // Append the last result we collected (it wasn't added to the list) + if (lastResult != null) + { + result.Append(lastResult.ToString()); + } + + // And if we couldn't find anymore property tags in the expression, + // so just literally copy the remainder into the result. + if (expression.Length - sourceIndex > 0) + { + result.Append(expression, sourceIndex, expression.Length - sourceIndex); + } + + return OpportunisticIntern.InternableToString(result); + } + } + } + + /// + /// Expand the body of the property, including any functions that it may contain + /// + internal static object ExpandPropertyBody(string propertyBody, object propertyValue, IPropertyProvider properties, ExpanderOptions options, IElementLocation elementLocation, UsedUninitializedProperties usedUninitializedProperties) + { + Function function = null; + string propertyName = propertyBody; + + // Trim the body for compatibility reasons: + // Spaces are not valid property name chars, but $( Foo ) is allowed, and should always expand to BLANK. + // Do a very fast check for leading and trailing whitespace, and trim them from the property body if we have any. + // But we will do a property name lookup on the propertyName that we held onto. + if (Char.IsWhiteSpace(propertyBody[0]) || Char.IsWhiteSpace(propertyBody[propertyBody.Length - 1])) + { + propertyBody = propertyBody.Trim(); + } + + // If we don't have a clean propertybody then we'll do deeper checks to see + // if what we have is a function + if (!IsValidPropertyName(propertyBody)) + { + if (propertyBody.Contains(".") || propertyBody[0] == '[') + { + if (BuildParameters.DebugExpansion) + { + Console.WriteLine("Expanding: {0}", propertyBody); + } + + // This is a function + function = Function.ExtractPropertyFunction(propertyBody, elementLocation, propertyValue, usedUninitializedProperties); + + // We may not have been able to parse out a function + if (function != null) + { + // We will have either extracted the actual property name + // or realised that there is none (static function), and have recorded a null + propertyName = function.ExpressionRootName; + } + else + { + // In the event that we have been handed an unrecognized property body, throw + // an invalid function property exception. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty); + return null; + } + } + else if (propertyValue == null && propertyBody.Contains("[")) // a single property indexer + { + int indexerStart = propertyBody.IndexOf('['); + int indexerEnd = propertyBody.IndexOf(']'); + + if (indexerStart < 0 || indexerEnd < 0) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets")); + } + else + { + propertyValue = LookupProperty(properties, propertyBody, 0, indexerStart - 1, elementLocation, usedUninitializedProperties); + propertyBody = propertyBody.Substring(indexerStart); + + // recurse so that the function representing the indexer can be executed on the property value + return ExpandPropertyBody(propertyBody, propertyValue, properties, options, elementLocation, usedUninitializedProperties); + } + } + else + { + // In the event that we have been handed an unrecognized property body, throw + // an invalid function property exception. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", propertyBody, String.Empty); + return null; + } + } + + // Find the property value in our property collection. This + // will automatically return "" (empty string) if the property + // doesn't exist in the collection, and we're not executing a static function + if (!String.IsNullOrEmpty(propertyName)) + { + propertyValue = LookupProperty(properties, propertyName, elementLocation, usedUninitializedProperties); + } + + if (function != null) + { + // Because of the rich expansion capabilities of MSBuild, we need to keep things + // as strings, since property expansion & string embedding can happen anywhere + // propertyValue can be null here, when we're invoking a static function + propertyValue = function.Execute(propertyValue, properties, options, elementLocation); + } + + return propertyValue; + } + + /// + /// Convert the object into an MSBuild friendly string + /// Arrays are supported. + /// Will not return NULL + /// + internal static string ConvertToString(object valueToConvert) + { + if (valueToConvert != null) + { + Type valueType = valueToConvert.GetType(); + string convertedString; + + // If the type is a string, then there is nothing to do + if (valueType == typeof(string)) + { + convertedString = (string)valueToConvert; + } + else if (valueToConvert is IDictionary) + { + // If the return type is an IDictionary, then we convert this to + // a semi-colon delimited set of A=B pairs. + // Key and Value are converted to string and escaped + IDictionary dictionary = valueToConvert as IDictionary; + using (var builder = new ReuseableStringBuilder()) + { + foreach (DictionaryEntry entry in dictionary) + { + if (builder.Length > 0) + { + builder.Append(';'); + } + + // convert and escape each key and value in the dictionary entry + builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Key))); + builder.Append('='); + builder.Append(EscapingUtilities.Escape(ConvertToString(entry.Value))); + } + + convertedString = OpportunisticIntern.InternableToString(builder); + } + } + else if (valueToConvert is IEnumerable) + { + // If the return is enumerable, then we'll convert to semi-colon delimted elements + // each of which must be converted, so we'll recurse for each element + using (var builder = new ReuseableStringBuilder()) + { + IEnumerable enumerable = (IEnumerable)valueToConvert; + + foreach (object element in enumerable) + { + if (builder.Length > 0) + { + builder.Append(';'); + } + + // we need to convert and escape each element of the array + builder.Append(EscapingUtilities.Escape(ConvertToString(element))); + } + + convertedString = OpportunisticIntern.InternableToString(builder); + } + } + else + { + // The fall back is always to just convert to a string directly. + convertedString = valueToConvert.ToString(); + } + + return convertedString; + } + else + { + return String.Empty; + } + } + + /// + /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo) + /// + private static object LookupProperty(IPropertyProvider properties, string propertyName, IElementLocation elementLocation, UsedUninitializedProperties usedUninitializedProperties) + { + return LookupProperty(properties, propertyName, 0, propertyName.Length - 1, elementLocation, usedUninitializedProperties); + } + + /// + /// Look up a simple property reference by the name of the property, e.g. "Foo" when expanding $(Foo) + /// + private static object LookupProperty(IPropertyProvider properties, string propertyName, int startIndex, int endIndex, IElementLocation elementLocation, UsedUninitializedProperties usedUninitializedProperties) + { + T property = properties.GetProperty(propertyName, startIndex, endIndex); + + object propertyValue; + + if (property == null && MSBuildNameIgnoreCaseComparer.Equals("MSBuild", propertyName, startIndex, 7)) + { + // It could be one of the MSBuildThisFileXXXX properties, + // whose values vary according to the file they are in. + if (startIndex != 0 || endIndex != propertyName.Length) + { + propertyValue = ExpandMSBuildThisFileProperty(propertyName.Substring(startIndex, endIndex - startIndex + 1), elementLocation); + } + else + { + propertyValue = ExpandMSBuildThisFileProperty(propertyName, elementLocation); + } + } + else if (property == null) + { + // We have evaluated a property to null. We now need to see if we need to add it to the list of properties which are used before they have been initialized + // + // We also do not want to add the property to the list if the environment variable is not set, also we do not want to add the property to the list if we are currently + // evaluating a condition because a common pattern for msbuild projects is to see if the property evaluates to empty and then set a value as this would cause a considerable number of false positives. default + // + // Another pattern used is where a property concatonates with other values, $(a);something however we do not want to add the a element to the list because again this would make a number of + // false positives. Therefore we check to see what element we are currently evaluating and if it is the same as our property we do not add the property to the list. + if (usedUninitializedProperties.Warn && usedUninitializedProperties.CurrentlyEvaluatingPropertyElementName != null) + { + // Check to see if the property name does not match the property we are currently evaluating, note the property we are currently evaluating in the element name, this means no $( or ) + if (!MSBuildNameIgnoreCaseComparer.Equals(usedUninitializedProperties.CurrentlyEvaluatingPropertyElementName, propertyName, startIndex, endIndex - startIndex + 1)) + { + string propertyTrimed = propertyName.Substring(startIndex, endIndex - startIndex + 1); + if (!usedUninitializedProperties.Properties.ContainsKey(propertyTrimed)) + { + usedUninitializedProperties.Properties.Add(propertyTrimed, elementLocation); + } + } + } + + propertyValue = String.Empty; + } + else + { + propertyValue = property.EvaluatedValueEscaped; + } + + return propertyValue; + } + + /// + /// If the property name provided is one of the special + /// per file properties named "MSBuildThisFileXXXX" then returns the value of that property. + /// If the location provided does not have a path (eg., if it comes from a file that has + /// never been saved) then returns empty string. + /// If the property name is not one of those properties, returns empty string. + /// + private static object ExpandMSBuildThisFileProperty(string propertyName, IElementLocation elementLocation) + { + if (!ReservedPropertyNames.IsReservedProperty(propertyName)) + { + return String.Empty; + } + + if (elementLocation.File.Length == 0) + { + return String.Empty; + } + + string value = String.Empty; + + // Because String.Equals checks the length first, and these strings are almost + // all different lengths, this sequence is efficient. + if (String.Equals(propertyName, ReservedPropertyNames.thisFile, StringComparison.OrdinalIgnoreCase)) + { + value = Path.GetFileName(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileName, StringComparison.OrdinalIgnoreCase)) + { + value = Path.GetFileNameWithoutExtension(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileFullPath, StringComparison.OrdinalIgnoreCase)) + { + value = FileUtilities.NormalizePath(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileExtension, StringComparison.OrdinalIgnoreCase)) + { + value = Path.GetExtension(elementLocation.File); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectory, StringComparison.OrdinalIgnoreCase)) + { + value = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(elementLocation.File)); + } + else if (String.Equals(propertyName, ReservedPropertyNames.thisFileDirectoryNoRoot, StringComparison.OrdinalIgnoreCase)) + { + string directory = Path.GetDirectoryName(elementLocation.File); + int rootLength = Path.GetPathRoot(directory).Length; + string directoryNoRoot = directory.Substring(rootLength); + directoryNoRoot = FileUtilities.EnsureTrailingSlash(directoryNoRoot); + directoryNoRoot = FileUtilities.EnsureNoLeadingSlash(directoryNoRoot); + value = directoryNoRoot; + } + + return value; + } + + /// + /// Given a string like "Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation", return the value at that location + /// in the registry. If the value isn't found, returns String.Empty. + /// Properties may refer to a registry location by using the syntax for example + /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and + /// "TaskLocation" is the name of the value. The name of the value and the preceding "@" may be omitted if + /// the default value is desired. + /// + private static string ExpandRegistryValue(string registryExpression, IElementLocation elementLocation) + { + // Remove "Registry:" prefix + string registryLocation = registryExpression.Substring(9); + + // Split off the value name -- the part after the "@" sign. If there's no "@" sign, then it's the default value name + // we want. + int firstAtSignOffset = registryLocation.IndexOf('@'); + int lastAtSignOffset = registryLocation.LastIndexOf('@'); + + ProjectErrorUtilities.VerifyThrowInvalidProject(firstAtSignOffset == lastAtSignOffset, elementLocation, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", String.Empty); + + string valueName = lastAtSignOffset == -1 || lastAtSignOffset == registryLocation.Length - 1 + ? null : registryLocation.Substring(lastAtSignOffset + 1); + + // If there's no '@', or '@' is first, then we'll use null or String.Empty for the location; otherwise + // the location is the part before the '@' + string registryKeyName = lastAtSignOffset != -1 ? registryLocation.Substring(0, lastAtSignOffset) : registryLocation; + + string result = String.Empty; + if (registryKeyName != null) + { + // We rely on the '@' character to delimit the key and its value, but the registry + // allows this character to be used in the names of keys and the names of values. + // Hence we use our standard escaping mechanism to allow users to access such keys + // and values. + registryKeyName = EscapingUtilities.UnescapeAll(registryKeyName); + + if (valueName != null) + { + valueName = EscapingUtilities.UnescapeAll(valueName); + } + + try + { + object valueFromRegistry = Registry.GetValue(registryKeyName, valueName, null /* default if key or value name is not found */); + + if (null != valueFromRegistry) + { + // Convert the result to a string that is reasonable for MSBuild + result = ConvertToString(valueFromRegistry); + } + else + { + // This means either the key or value was not found in the registry. In this case, + // we simply expand the property value to String.Empty to imitate the behavior of + // normal properties. + result = String.Empty; + } + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidRegistryPropertyExpression", "$(" + registryExpression + ")", ex.Message); + } + } + + return result; + } + } + + /// + /// Expands item expressions, like @(Compile), possibly with transforms and/or separators. + /// + /// Item vectors are composed of a name, an optional transform, and an optional separator i.e. + /// + /// @(<name>->'<transform>','<separator>') + /// + /// If a separator is not specified it defaults to a semi-colon. The transform expression is also optional, but if + /// specified, it allows each item in the vector to have its item-spec converted to a different form. The transform + /// expression can reference any custom metadata defined on the item, as well as the pre-defined item-spec modifiers. + /// + /// NOTE: + /// 1) white space between <name>, <transform> and <separator> is ignored + /// i.e. @(<name>, '<separator>') is valid + /// 2) the separator is not restricted to be a single character, it can be a string + /// 3) the separator can be an empty string i.e. @(<name>,'') + /// 4) specifying an empty transform is NOT the same as specifying no transform -- the former will reduce all item-specs + /// to empty strings + /// + /// if @(files) is a vector for the files a.txt and b.txt, then: + /// + /// "my list: @(files)" expands to string "my list: a.txt;b.txt" + /// + /// "my list: @(files,' ')" expands to string "my list: a.txt b.txt" + /// + /// "my list: @(files, '')" expands to string "my list: a.txtb.txt" + /// + /// "my list: @(files, '; ')" expands to string "my list: a.txt; b.txt" + /// + /// "my list: @(files->'%(Filename)')" expands to string "my list: a;b" + /// + /// "my list: @(files -> 'temp\%(Filename).xml', ' ') expands to string "my list: temp\a.xml temp\b.xml" + /// + /// "my list: @(files->'') expands to string "my list: ;" + /// + /// + /// This is a private nested class, exposed only through the Expander class. + /// That allows it to hide its private methods even from Expander. + /// + private static class ItemExpander + { + /// + /// Execute the list of transform functions + /// + /// class, IItem + internal static IEnumerable> Transform(Expander expander, bool includeNullEntries, Stack> transformFunctionStack, IEnumerable> itemsOfType) + where S : class, IItem + { + // If we have transforms on our stack, then we'll execute those first + // This effectively runs backwards through the set + if (transformFunctionStack.Count > 0) + { + TransformFunction function = transformFunctionStack.Pop(); + + foreach (Tuple item in Transform(expander, includeNullEntries, transformFunctionStack, function.Execute(expander, includeNullEntries, itemsOfType))) + { + yield return item; + } + } + else + { + // When we have no more tranforms on the stack, iterate over the items + // that we have to return them + foreach (Tuple item in itemsOfType) + { + yield return item; + } + } + } + + /// + /// Expands any item vector in the expression into items. + /// + /// For example, expands @(Compile->'%(foo)') to a set of items derived from the items in the "Compile" list. + /// + /// If there is no item vector in the expression (for example a literal "foo.cpp"), returns null. + /// If the item vector expression expands to no items, returns an empty list. + /// If item expansion is not allowed by the provided options, returns null. + /// If there is an item vector but concatenated with something else, throws InvalidProjectFileException. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// If the expression is a transform, any transformations to an expression that evaluates to nothing (i.e., because + /// an item has no value for a piece of metadata) are optionally indicated with a null entry in the list. This means + /// that the length of the returned list is always the same as the length of the referenced item list in the input string. + /// That's important for any correlation the caller wants to do. + /// + /// If expression was a transform, 'isTransformExpression' is true, otherwise false. + /// + /// Item type of the items returned is determined by the IItemFactory passed in; if the IItemFactory does not + /// have an item type set on it, it will be given the item type of the item vector to use. + /// + /// Type of the items provided by the item source used for expansion + /// Type of the items that should be returned + internal static IList ExpandSingleItemVectorExpressionIntoItems(Expander expander, string expression, IItemProvider items, IItemFactory itemFactory, ExpanderOptions options, bool includeNullEntries, out bool isTransformExpression, IElementLocation elementLocation) + where S : class, IItem + where T : class, IItem + { + isTransformExpression = false; + + if (((options & ExpanderOptions.ExpandItems) == 0) || (expression.Length == 0)) + { + return null; + } + + ErrorUtilities.VerifyThrow(items != null, "Cannot expand items without providing items"); + + List matches = null; + + if (s_invariantCompareInfo.IndexOf(expression, '@') == -1) + { + return null; + } + else + { + matches = ExpressionShredder.GetReferencedItemExpressions(expression); + + if (matches == null) + { + return null; + } + } + + ExpressionShredder.ItemExpressionCapture match = matches[0]; + + // We have a single valid @(itemlist) reference in the given expression. + // If the passed-in expression contains exactly one item list reference, + // with nothing else concatenated to the beginning or end, then proceed + // with itemizing it, otherwise error. + ProjectErrorUtilities.VerifyThrowInvalidProject(match.Value == expression, elementLocation, "EmbeddedItemVectorCannotBeItemized", expression); + ErrorUtilities.VerifyThrow(matches.Count == 1, "Expected just one item vector"); + + // If the incoming factory doesn't have an item type that it can use to + // create items, it's our indication that the caller wants its items to have the type of the + // expression being expanded. For example, items from expanding "@(Compile") should + // have the item type "Compile". + if (itemFactory.ItemType == null) + { + itemFactory.ItemType = match.ItemType; + } + + IList result = null; + + if (match.Separator != null) + { + // Reference contains a separator, for example @(Compile, ';'). + // We need to flatten the list into + // a scalar and then create a single item. Basically we need this + // to be able to convert item lists with user specified separators into properties. + string expandedItemVector; + using (var builder = new ReuseableStringBuilder()) + { + bool brokeEarlyNonEmpty = ExpandItemVectorMatchIntoStringBuilder(expander, match, items, elementLocation, builder, options); + + if (brokeEarlyNonEmpty) + { + return null; + } + + expandedItemVector = OpportunisticIntern.InternableToString(builder); + } + + result = new List(1); + + if (expandedItemVector.Length > 0) + { + T newItem = itemFactory.CreateItem(expandedItemVector, elementLocation.File); + + result.Add(newItem); + } + + return result; + } + + // No separator. Expand to a list of items + // + // eg.,: + // expands @(Compile) to a set of items cloned from the items in the "Compile" list + // expands @(Compile->'%(foo)') to a set of items with the Include value of items in the "Compile" list. + if (match.Captures != null) + { + isTransformExpression = true; + } + + ICollection itemsOfType = items.GetItems(match.ItemType); + + // If there are no items of the given type, then bail out early + if (itemsOfType.Count == 0) + { + // .. but only if there isn't a function "Count()", since that will want to return something (zero) for an empty list + if (match.Captures == null || !match.Captures.Any(capture => String.Equals(capture.FunctionName, "Count", StringComparison.OrdinalIgnoreCase))) + { + return new List(); + } + } + + result = new List(itemsOfType.Count); + + if (!isTransformExpression) + { + // No transform: expression is like @(Compile), so copy the items + foreach (S item in itemsOfType) + { + if ((options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return null; + } + + T newItem = itemFactory.CreateItem(item, elementLocation.File); + + result.Add(newItem); + } + + return result; + } + + Stack> transformFunctionStack = PrepareTransformStackFromMatch(elementLocation, match); + + // iterate over our tranform chain, creating the final items from its results + foreach (Tuple itemTuple in Transform(expander, includeNullEntries, transformFunctionStack, IntrinsicItemFunctions.GetItemTupleEnumerator(itemsOfType))) + { + if (itemTuple.Item1 != null && itemTuple.Item2 == null) + { + // We have an itemspec, but no base item + result.Add(itemFactory.CreateItem(itemTuple.Item1, elementLocation.File)); + } + else if (itemTuple.Item1 != null && itemTuple.Item2 != null) + { + // We have both an itemspec, and a base item + result.Add(itemFactory.CreateItem(itemTuple.Item1, itemTuple.Item2, elementLocation.File)); + } + else if (includeNullEntries) + { + // The itemspec is null and the base item doesn't matter + result.Add(null); + } + } + + return result; + } + + /// + /// Expands all item vectors embedded in the given expression into a single string. + /// If the expression is empty, returns empty string. + /// If ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and it broke out early, returns null. Otherwise the result can be trusted. + /// + /// Type of the items provided + internal static string ExpandItemVectorsIntoString(Expander expander, string expression, IItemProvider items, ExpanderOptions options, IElementLocation elementLocation) + where T : class, IItem + { + if (((options & ExpanderOptions.ExpandItems) == 0) || (expression.Length == 0)) + { + return expression; + } + + ErrorUtilities.VerifyThrow(items != null, "Cannot expand items without providing items"); + + List matches = ExpressionShredder.GetReferencedItemExpressions(expression); + + if (matches == null) + { + return expression; + } + + using (var builder = new ReuseableStringBuilder()) + { + // As we walk through the matches, we need to copy out the original parts of the string which + // are not covered by the match. This preserves original behavior which did not trim whitespace + // from between separators. + int lastStringIndex = 0; + for (int i = 0; i < matches.Count; i++) + { + if (matches[i].Index > lastStringIndex) + { + if ((options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return null; + } + + builder.Append(expression, lastStringIndex, matches[i].Index - lastStringIndex); + } + + bool brokeEarlyNonEmpty = ExpandItemVectorMatchIntoStringBuilder(expander, matches[i], items, elementLocation, builder, options); + + if (brokeEarlyNonEmpty) + { + return null; + } + + lastStringIndex = matches[i].Index + matches[i].Length; + } + + builder.Append(expression, lastStringIndex, expression.Length - lastStringIndex); + + return OpportunisticIntern.InternableToString(builder); + } + } + + /// + /// Prepare the stack of transforms that will be executed on a given set of items + /// + /// class, IItem + private static Stack> PrepareTransformStackFromMatch(IElementLocation elementLocation, ExpressionShredder.ItemExpressionCapture match) + where S : class, IItem + { + // There's something wrong with the expression, and we ended up with no function names + ProjectErrorUtilities.VerifyThrowInvalidProject(match.Captures.Count > 0, elementLocation, "InvalidFunctionPropertyExpression"); + + Stack> transformFunctionStack = new Stack>(match.Captures.Count); + + // Create a TransformFunction for each transform in the chain by extracting the relevant information + // from the regex parsing results + // Each will be pushed onto a stack in right to left order (i.e. the inner/right most will be on the + // bottom of the stack, the outer/left most will be on the top + for (int n = match.Captures.Count - 1; n >= 0; n--) + { + string function = match.Captures[n].Value; + string functionName = match.Captures[n].FunctionName; + string argumentsExpression = match.Captures[n].FunctionArguments; + + string[] arguments = null; + + if (functionName == null) + { + functionName = "ExpandQuotedExpressionFunction"; + arguments = new string[] { function }; + } + else if (argumentsExpression != null) + { + arguments = ExtractFunctionArguments(elementLocation, argumentsExpression, argumentsExpression); + } + + IntrinsicItemFunctions.ItemTransformFunction transformFunction = IntrinsicItemFunctions.GetItemTransformFunction(elementLocation, functionName, typeof(S)); + + // Push our tranform on to the stack + transformFunctionStack.Push(new TransformFunction(elementLocation, functionName, transformFunction, arguments)); + } + + return transformFunctionStack; + } + + /// + /// Expand the match provided into a string, and append that to the provided string builder. + /// Returns true if ExpanderOptions.BreakOnNotEmpty was passed, expression was going to be non-empty, and so it broke out early. + /// + /// Type of source items + private static bool ExpandItemVectorMatchIntoStringBuilder(Expander expander, ExpressionShredder.ItemExpressionCapture match, IItemProvider items, IElementLocation elementLocation, ReuseableStringBuilder builder, ExpanderOptions options) + where S : class, IItem + { + string itemType = match.ItemType; + string separator = (match.Separator != null) ? match.Separator : ";"; + + // There's something wrong with the expression, and we ended up with a blank item type + ProjectErrorUtilities.VerifyThrowInvalidProject(!String.IsNullOrEmpty(itemType), elementLocation, "InvalidFunctionPropertyExpression"); + + ICollection itemsOfType = items.GetItems(itemType); + + // If there are no items of the given type, then bail out early + if (itemsOfType.Count == 0) + { + // .. but only if there isn't a function "Count()", since that will want to return something (zero) for an empty list + if (match.Captures == null || !match.Captures.Any(capture => String.Equals(capture.FunctionName, "Count", StringComparison.OrdinalIgnoreCase))) + { + return false; // false because the result is reliable + } + } + + if (match.Captures == null) + { + foreach (S item in itemsOfType) + { + // No transform: expression is like @(Compile), so append the item spec + if ((item.EvaluatedIncludeEscaped.Length > 0) && (options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return true; + } + + builder.Append(item.EvaluatedIncludeEscaped); + builder.Append(separator); + } + } + else + { + Stack> transformFunctionStack = PrepareTransformStackFromMatch(elementLocation, match); + + // iterate over our tranform chain, creating the final items from its results + foreach (Tuple itemTuple in Transform(expander, true /* including null items */, transformFunctionStack, IntrinsicItemFunctions.GetItemTupleEnumerator(itemsOfType))) + { + if (itemTuple.Item1 != null && itemTuple.Item1.Length > 0 && (options & ExpanderOptions.BreakOnNotEmpty) != 0) + { + return true; // broke out early; result cannot be trusted + } + else if (itemTuple.Item1 != null) + { + builder.Append(itemTuple.Item1); + } + + builder.Append(separator); + } + } + + // Remove the extra separator + if (builder.Length > 0) + { + builder.Remove(builder.Length - separator.Length, separator.Length); + } + + return false; // did not break early + } + + /// + /// The set of functions that called during an item transformation, e.g. @(CLCompile->ContainsMetadata('MetaName', 'metaValue')) + /// + /// class, IItem + internal static class IntrinsicItemFunctions + where S : class, IItem + { + /// + /// A cache of previously created item function delegates + /// + private static ConcurrentDictionary s_transformFunctionDelegateCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Delegate that represents the signature of all item transformation functions + /// This is used to support calling the functions by name + /// + public delegate IEnumerable> ItemTransformFunction(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments); + + /// + /// Get a delegate to the given item transformation function by supplying the name and the + /// Item type that should be used + /// + internal static ItemTransformFunction GetItemTransformFunction(IElementLocation elementLocation, string functionName, Type itemType) + { + ItemTransformFunction transformFunction = null; + string qualifiedFunctionName = itemType.FullName + "::" + functionName; + + // We may have seen this delegate before, if so grab the one we already created + if (!s_transformFunctionDelegateCache.TryGetValue(qualifiedFunctionName, out transformFunction)) + { + if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(functionName)) + { + // Create a delegate to the function we're going to call + transformFunction = new ItemTransformFunction(ItemSpecModifierFunction); + } + else + { + MethodInfo itemFunctionInfo = typeof(IntrinsicItemFunctions).GetMethod(functionName, BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Static); + + if (itemFunctionInfo == null) + { + functionName = "ExecuteStringFunction"; + itemFunctionInfo = typeof(IntrinsicItemFunctions).GetMethod(functionName, BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Static); + if (itemFunctionInfo == null) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName); + return null; + } + + transformFunction = (ItemTransformFunction)Delegate.CreateDelegate(typeof(ItemTransformFunction), itemFunctionInfo, false); + } + else + { + // Create a delegate to the function we're going to call + transformFunction = (ItemTransformFunction)Delegate.CreateDelegate(typeof(ItemTransformFunction), itemFunctionInfo, false); + } + } + + if (transformFunction == null) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "UnknownItemFunction", functionName); + return null; + } + + // record our delegate for future use + s_transformFunctionDelegateCache[qualifiedFunctionName] = transformFunction; + } + + return transformFunction; + } + + /// + /// Create an enumerator from a base IEnumerable of items into an enumerable + /// of transformation result which includes the new itemspec and the base item + /// + internal static IEnumerable> GetItemTupleEnumerator(IEnumerable itemsOfType) + { + // iterate over the items, and yield out items in the tuple format + foreach (S item in itemsOfType) + { + yield return new Tuple(item.EvaluatedIncludeEscaped, item); + } + } + + /// + /// Intrinsic function that returns the number of items in the list + /// + internal static IEnumerable> Count(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + yield return new Tuple(Convert.ToString(itemsOfType.Count(), CultureInfo.InvariantCulture), null /* no base item */); + } + + /// + /// Intrinsic function that returns the specified built-in modifer value of the items in itemsOfType + /// Tuple is {current item include, item under transformation} + /// + internal static IEnumerable> ItemSpecModifierFunction(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + foreach (Tuple item in itemsOfType) + { + // If the item include has become empty, + // this is the end of the pipeline for this item + if (String.IsNullOrEmpty(item.Item1)) + { + continue; + } + + string result = null; + + try + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + string directoryToUse = item.Item2.ProjectDirectory ?? Directory.GetCurrentDirectory(); + string definingProjectEscaped = item.Item2.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + + result = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(directoryToUse, item.Item1, definingProjectEscaped, functionName); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + // InvalidOperationException is how GetItemSpecModifier communicates invalid conditions upwards, so + // we do not want to rethrow in that case. + if (ExceptionHandling.NotExpectedException(e) && !(e is InvalidOperationException)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Item1, e.Message); + } + + if (!String.IsNullOrEmpty(result)) + { + // GetItemSpecModifier will have returned us an escaped string + // there is nothing more to do than yield it into the pipeline + yield return new Tuple(result, item.Item2); + } + else if (includeNullEntries) + { + yield return new Tuple(null, item.Item2); + } + } + } + + /// + /// Intrinsic function that returns the DirectoryName of the items in itemsOfType + /// UNDONE: This can be removed in favor of a built-in %(DirectoryName) metadata in future. + /// + internal static IEnumerable> DirectoryName(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + Dictionary directoryNameTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (Tuple item in itemsOfType) + { + // If the item include has become empty, + // this is the end of the pipeline for this item + if (String.IsNullOrEmpty(item.Item1)) + { + continue; + } + + string directoryName = null; + + if (!directoryNameTable.TryGetValue(item.Item1, out directoryName)) + { + // Unescape as we are passing to the file system + string unescapedPath = EscapingUtilities.UnescapeAll(item.Item1); + + try + { + string rootedPath; + + // If we're a projectitem instance then we need to get + // the project directory and be relative to that + if (Path.IsPathRooted(unescapedPath)) + { + rootedPath = unescapedPath; + } + else + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + string baseDirectoryToUse = item.Item2.ProjectDirectory ?? String.Empty; + rootedPath = Path.Combine(baseDirectoryToUse, unescapedPath); + } + + directoryName = Path.GetDirectoryName(rootedPath); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidItemFunctionExpression", functionName, item.Item1, e.Message); + } + + // Escape as this is going back into the engine + directoryName = EscapingUtilities.Escape(directoryName); + directoryNameTable[unescapedPath] = directoryName; + } + + if (!String.IsNullOrEmpty(directoryName)) + { + // return a result through the enumerator + yield return new Tuple(directoryName, item.Item2); + } + else if (includeNullEntries) + { + yield return new Tuple(null, item.Item2); + } + } + } + + /// + /// Intrinsic function that returns the contents of the metadata in specified in argument[0] + /// + internal static IEnumerable> Metadata(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments != null && arguments.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + string metadataName = arguments[0]; + + foreach (Tuple item in itemsOfType) + { + if (item.Item2 != null) + { + string metadataValue = null; + + try + { + metadataValue = item.Item2.GetMetadataValueEscaped(metadataName); + } + catch (ArgumentException ex) // Blank metadata name + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (!String.IsNullOrEmpty(metadataValue)) + { + // It may be that the itemspec has unescaped ';'s in it so we need to split here to handle + // that case. + if (s_invariantCompareInfo.IndexOf(metadataValue, ';') >= 0) + { + IList splits = ExpressionShredder.SplitSemiColonSeparatedList(metadataValue); + + foreach (string itemSpec in splits) + { + // return a result through the enumerator + yield return new Tuple(itemSpec, item.Item2); + } + } + else + { + // return a result through the enumerator + yield return new Tuple(metadataValue, item.Item2); + } + } + else if (metadataValue != String.Empty && includeNullEntries) + { + yield return new Tuple(metadataValue, item.Item2); + } + } + } + } + + /// + /// Intrinsic function that returns only the items from itemsOfType that have distinct Item1 in the Tuple + /// Using a case sensitive comparison + /// + internal static IEnumerable> DistinctWithCase(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + return DistinctWithComparer(expander, elementLocation, includeNullEntries, functionName, itemsOfType, arguments, StringComparer.Ordinal); + } + + /// + /// Intrinsic function that returns only the items from itemsOfType that have distinct Item1 in the Tuple + /// Using a case insensitive comparison + /// + internal static IEnumerable> Distinct(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + return DistinctWithComparer(expander, elementLocation, includeNullEntries, functionName, itemsOfType, arguments, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Intrinsic function that returns only the items from itemsOfType that have distinct Item1 in the Tuple + /// Using a case insensitive comparison + /// + internal static IEnumerable> DistinctWithComparer(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments, StringComparer comparer) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + // This dictionary will ensure that we only return one result per unique itemspec + Dictionary seenItems = new Dictionary(comparer); + + foreach (Tuple item in itemsOfType) + { + if (item.Item1 != null && !seenItems.ContainsKey(item.Item1)) + { + seenItems[item.Item1] = item.Item2; + + yield return new Tuple(item.Item1, item.Item2); + } + } + } + + /// + /// Intrinsic function reverses the item list. + /// + internal static IEnumerable> Reverse(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + foreach (Tuple item in itemsOfType.Reverse()) + { + yield return new Tuple(item.Item1, item.Item2); + } + } + + /// + /// Intrinsic function that transforms expressions like the %(foo) in @(Compile->'%(foo)') + /// + internal static IEnumerable> ExpandQuotedExpressionFunction(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments != null && arguments.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + foreach (Tuple item in itemsOfType) + { + MetadataMatchEvaluator matchEvaluator; + string include = null; + + // If we've been handed a null entry by an uptream tranform + // then we don't want to try to tranform it with an itempec modification. + // Simply allow the null to be passed along (if, we are including nulls as specified by includeNullEntries + if (item.Item1 != null) + { + matchEvaluator = new MetadataMatchEvaluator(item.Item1, item.Item2, elementLocation); + + include = RegularExpressions.ItemMetadataPattern.Replace(arguments[0], matchEvaluator.GetMetadataValueFromMatch); + } + + // Include may be empty. Historically we have created items with empty include + // and ultimately set them on tasks, but we don't do that anymore as it's broken. + // Instead we optionally add a null, so that input and output lists are the same length; this allows + // the caller to possibly do correlation. + + // We pass in the existing item so we can copy over its metadata + if (include != null && include.Length > 0) + { + yield return new Tuple(include, item.Item2); + } + else if (includeNullEntries) + { + yield return new Tuple(null, item.Item2); + } + } + } + + /// + /// Intrinsic function that transforms expressions by invoking methods of System.String on the itemspec + /// of the item in the pipeline + /// + internal static IEnumerable> ExecuteStringFunction(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + // Transform: expression is like @(Compile->'%(foo)'), so create completely new items, + // using the Include from the source items + foreach (Tuple item in itemsOfType) + { + Function

function = new Expander.Function

(typeof(string), item.Item1, item.Item1, functionName, arguments, BindingFlags.Public | BindingFlags.InvokeMethod, String.Empty, expander.UsedUninitializedProperties); + + object result = function.Execute(item.Item1, expander._properties, ExpanderOptions.ExpandAll, elementLocation); + + string include = Expander.PropertyExpander

.ConvertToString(result); + + // We pass in the existing item so we can copy over its metadata + if (include.Length > 0) + { + yield return new Tuple(include, item.Item2); + } + else if (includeNullEntries) + { + yield return new Tuple(null, item.Item2); + } + } + } + + ///

+ /// Intrinsic function that returns the items from itemsOfType with their metadata cleared, i.e. only the itemspec is retained + /// + internal static IEnumerable> ClearMetadata(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments == null || arguments.Length == 0, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + foreach (Tuple item in itemsOfType) + { + if (includeNullEntries || item.Item1 != null) + { + yield return new Tuple(item.Item1, null); + } + } + } + + /// + /// Intrinsic function that returns only those items that have a not-blank value for the metadata specified + /// Using a case insensitive comparison + /// + internal static IEnumerable> HasMetadata(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments != null && arguments.Length == 1, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + string metadataName = arguments[0]; + + foreach (Tuple item in itemsOfType) + { + string metadataValue = null; + + try + { + metadataValue = item.Item2.GetMetadataValueEscaped(metadataName); + } + catch (ArgumentException ex) // Blank metadata name + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + // GetMetadataValueEscaped returns empty string for missing metadata, + // but IItem specifies it should return null + if (metadataValue != null && metadataValue.Length > 0) + { + // return a result through the enumerator + yield return new Tuple(item.Item1, item.Item2); + } + } + } + + /// + /// Intrinsic function that returns only those items have the given metadata value + /// Using a case insensitive comparison + /// + internal static IEnumerable> WithMetadataValue(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments != null && arguments.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + string metadataName = arguments[0]; + string metadataValueToFind = arguments[1]; + + foreach (Tuple item in itemsOfType) + { + string metadataValue = null; + + try + { + metadataValue = item.Item2.GetMetadataValueEscaped(metadataName); + } + catch (ArgumentException ex) // Blank metadata name + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) + { + // return a result through the enumerator + yield return new Tuple(item.Item1, item.Item2); + } + } + } + + /// + /// Intrinsic function that returns a boolean to indicate if any of the items have the given metadata value + /// Using a case insensitive comparison + /// + internal static IEnumerable> AnyHaveMetadataValue(Expander expander, IElementLocation elementLocation, bool includeNullEntries, string functionName, IEnumerable> itemsOfType, string[] arguments) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(arguments != null && arguments.Length == 2, elementLocation, "InvalidItemFunctionSyntax", functionName, (arguments == null ? 0 : arguments.Length)); + + string metadataName = arguments[0]; + string metadataValueToFind = arguments[1]; + bool metadataFound = false; + + foreach (Tuple item in itemsOfType) + { + if (item.Item2 != null) + { + string metadataValue = null; + + try + { + metadataValue = item.Item2.GetMetadataValueEscaped(metadataName); + } + catch (ArgumentException ex) // Blank metadata name + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CannotEvaluateItemMetadata", metadataName, ex.Message); + } + + if (metadataValue != null && String.Equals(metadataValue, metadataValueToFind, StringComparison.OrdinalIgnoreCase)) + { + metadataFound = true; + + // return a result through the enumerator + yield return new Tuple("true", item.Item2); + + // break out as soon as we found a match + yield break; + } + } + } + + if (!metadataFound) + { + // We did not locate an item with the required metadata + yield return new Tuple("false", null); + } + } + } + + /// + /// Represents all the components of a transform function, including the ability to execute it + /// + /// class, IItem + internal class TransformFunction + where S : class, IItem + { + /// + /// The delegate that points to the transform function + /// + private IntrinsicItemFunctions.ItemTransformFunction _transform; + + /// + /// Arguments to pass to the transform function as parsed out of the project file + /// + private string[] _arguments; + + /// + /// The element location of the transform expression + /// + private IElementLocation _elementLocation; + + /// + /// The name of the function that this class will call + /// + private string _functionName; + + /// + /// TransformFunction constructor + /// + public TransformFunction(IElementLocation elementLocation, string functionName, IntrinsicItemFunctions.ItemTransformFunction transform, string[] arguments) + { + _elementLocation = elementLocation; + _functionName = functionName; + _transform = transform; + _arguments = arguments; + } + + /// + /// Arguments to pass to the transform function as parsed out of the project file + /// + public string[] Arguments + { + get { return _arguments; } + } + + /// + /// The element location of the transform expression + /// + public IElementLocation ElementLocation + { + get { return _elementLocation; } + } + + /// + /// Execute this transform function with the arguments contained within this TransformFunction instance + /// + public IEnumerable> Execute(Expander expander, bool includeNullEntries, IEnumerable> itemsOfType) + { + // Execute via the delegate + return _transform(expander, _elementLocation, includeNullEntries, _functionName, itemsOfType, _arguments); + } + } + + /// + /// A functor that returns the value of the metadata in the match + /// that is on the item it was created with. + /// + private class MetadataMatchEvaluator + { + /// + /// The current ItemSpec of the item being matched + /// + private string _itemSpec; + + /// + /// Item used as the source of metadata + /// + private IItem _sourceOfMetadata; + + /// + /// Location of the match + /// + private IElementLocation _elementLocation; + + /// + /// Constructor + /// + internal MetadataMatchEvaluator(string itemSpec, IItem sourceOfMetadata, IElementLocation elementLocation) + { + _itemSpec = itemSpec; + _sourceOfMetadata = sourceOfMetadata; + _elementLocation = elementLocation; + } + + /// + /// Expands the metadata in the match provided into a string result. + /// The match is expected to be the content of a transform. + /// For example, representing "%(Filename.obj)" in the original expression "@(Compile->'%(Filename.obj)')" + /// + internal string GetMetadataValueFromMatch(Match match) + { + string name = match.Groups[RegularExpressions.NameGroup].Value; + + ProjectErrorUtilities.VerifyThrowInvalidProject(match.Groups[RegularExpressions.ItemSpecificationGroup].Length == 0, _elementLocation, "QualifiedMetadataInTransformNotAllowed", match.Value, name); + + string value = null; + try + { + if (FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(name)) + { + // If we're not a ProjectItem or ProjectItemInstance, then ProjectDirectory will be null. + // In that case, we're safe to get the current directory as we'll be running on TaskItems which + // only exist within a target where we can trust the current directory + string directoryToUse = _sourceOfMetadata.ProjectDirectory ?? Directory.GetCurrentDirectory(); + string definingProjectEscaped = _sourceOfMetadata.GetMetadataValueEscaped(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + + value = FileUtilities.ItemSpecModifiers.GetItemSpecModifier(directoryToUse, _itemSpec, definingProjectEscaped, name); + } + else + { + value = _sourceOfMetadata.GetMetadataValueEscaped(name); + } + } + catch (InvalidOperationException ex) + { + ProjectErrorUtilities.ThrowInvalidProject(_elementLocation, "CannotEvaluateItemMetadata", name, ex.Message); + } + + return value; + } + } + } + + /// + /// Regular expressions used by the expander. + /// The expander currently uses regular expressions rather than a parser to do its work. + /// + private static class RegularExpressions + { + /************************************************************************************************************************** + * WARNING: The regular expressions below MUST be kept in sync with the expressions in the ProjectWriter class -- if the + * description of an item vector changes, the expressions must be updated in both places. + *************************************************************************************************************************/ + + /// + /// Regular expression used to match item metadata references embedded in strings. + /// For example, %(Compile.DependsOn) or %(DependsOn). + /// + internal static readonly Regex ItemMetadataPattern = new Regex(ItemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + + /// + /// Name of the group matching the "name" of a metadatum. + /// + internal const string NameGroup = "NAME"; + + /// + /// Name of the group matching the prefix on a metadata expression, for example "Compile." in "%(Compile.Object)" + /// + internal const string ItemSpecificationGroup = "ITEM_SPECIFICATION"; + + /// + /// Name of the group matching the item type in an item expression or metadata expression. + /// + internal const string ItemTypeGroup = "ITEM_TYPE"; + + /// + /// regular expression used to match item metadata references outside of item vector transforms + /// + /// PERF WARNING: this Regex is complex and tends to run slowly + internal static readonly Regex NonTransformItemMetadataPattern = + new Regex + ( + @"((?<=" + ItemVectorWithTransformLHS + @")" + ItemMetadataSpecification + @"(?!" + ItemVectorWithTransformRHS + @")) | ((? + /// Complete description of an item metadata reference, including the optional qualifying item type. + /// For example, %(Compile.DependsOn) or %(DependsOn). + /// + private const string ItemMetadataSpecification = @"%\(\s* (?(?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") \s*\)"; + + /// + /// description of an item vector with a transform, left hand side + /// + private const string ItemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*"; + + /// + /// description of an item vector with a transform, right hand side + /// + private const string ItemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)"; + + /************************************************************************************************************************** + * WARNING: The regular expressions above MUST be kept in sync with the expressions in the ProjectWriter class. + *************************************************************************************************************************/ + } + + /// + /// This class represents the function as extracted from an expression + /// It is also responsible for executing the function + /// + /// Type of the properties used to expand the expression + private class Function + where T : class, IProperty + { + /// + /// The type that this function will act on + /// + private Type _objectType; + + /// + /// The name of the function + /// + private string _name; + + /// + /// The arguments for the function + /// + private string[] _arguments; + + /// + /// The expression that constitutes this function + /// + private string _expression; + + /// + /// The property name that is the context for this function + /// + private string _expressionRootName; + + /// + /// The binding flags that will be used during invocation of this function + /// + private BindingFlags _bindingFlags; + + /// + /// The remainder of the body once the function and arguments have been extracted + /// + private string _remainder; + + /// + /// List of properties which have been used but have not been initialized yet. + /// + private UsedUninitializedProperties _usedUninitializedProperties; + + /// + /// Construct a function that will be executed during property evaluation + /// + internal Function(Type objectType, string expression, string expressionRootName, string name, string[] arguments, BindingFlags bindingFlags, string remainder, UsedUninitializedProperties usedUninitializedProperties) + { + _name = name; + if (arguments == null) + { + _arguments = new string[0]; + } + else + { + _arguments = arguments; + } + + _expressionRootName = expressionRootName; + _expression = expression; + _objectType = objectType; + _bindingFlags = bindingFlags; + _remainder = remainder; + _usedUninitializedProperties = usedUninitializedProperties; + } + + /// + /// Part of the extraction may result in the name of the property + /// This accessor is used by the Expander + /// Examples of expression root: + /// [System.Diagnostics.Process]::Start + /// SomeMSBuildProperty + /// + internal string ExpressionRootName + { + get { return _expressionRootName; } + } + + /// + /// Extract the function details from the given property function expression + /// + internal static Function ExtractPropertyFunction(string expressionFunction, IElementLocation elementLocation, object propertyValue, UsedUninitializedProperties usedUnInitializedProperties) + { + // If this a expression function rather than a static, then we'll capture the name of the property referenced + string propertyName = null; + + // The type of the object that this function is part + Type objectType = null; + + // By default the expression root is the whole function expression + string expressionRoot = expressionFunction; + + // The arguments for this function start at the first '(' + // If there are no arguments, then we're a property getter + int argumentStartIndex = expressionFunction.IndexOf('('); + + // If we have arguments, then we only want the content up to but not including the '(' + if (argumentStartIndex > -1) + { + expressionRoot = expressionFunction.Substring(0, argumentStartIndex); + } + + // In case we ended up with something we don't understand + ProjectErrorUtilities.VerifyThrowInvalidProject(!String.IsNullOrEmpty(expressionRoot), elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + + // First we'll see if there is a static function being called + // A static method is the content that follows the last "::", the rest being + // the type + int methodStartIndex = -1; + + // This is a static method call + if (propertyValue == null && expressionRoot[0] == '[') + { + int typeEndIndex = expressionRoot.IndexOf(']', 1); + + if (typeEndIndex < 1) + { + // We ended up with something other than a function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty); + } + + string typeName = expressionRoot.Substring(1, typeEndIndex - 1); + methodStartIndex = typeEndIndex + 1; + + // Make an attempt to locate a type that matches the body of the expression. + // We won't throw on error here + objectType = GetTypeForStaticMethod(typeName); + + if (objectType == null) + { + // We ended up with something other than a type + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionTypeUnavailable", expressionFunction, typeName); + } + + if (expressionRoot.Length > methodStartIndex + 2 && expressionRoot[methodStartIndex] == ':' && expressionRoot[methodStartIndex + 1] == ':') + { + // skip over the "::" + methodStartIndex += 2; + } + else + { + // We ended up with something other than a static function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", expressionFunction, String.Empty); + } + } + else if (expressionFunction[0] == '[') // We have an indexer + { + objectType = propertyValue.GetType(); + int indexerEndIndex = expressionFunction.IndexOf(']', 1); + + if (indexerEndIndex < 1) + { + // We ended up with something other than a function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedSquareBrackets")); + } + + string argumentsContent = expressionFunction.Substring(1, indexerEndIndex - 1); + methodStartIndex = indexerEndIndex + 1; + Function indexerFunction = ConstructIndexerFunction(expressionFunction, elementLocation, propertyValue, propertyName, objectType, methodStartIndex, argumentsContent, usedUnInitializedProperties); + + return indexerFunction; + } + else + { + // No static function call was found, look for an instance function call next, such as in SomeStuff.ToLower() + methodStartIndex = expressionRoot.IndexOf('.'); + if (methodStartIndex == -1) + { + // We don't have a function invocation in the expression root, return null + return null; + } + else + { + // skip over the '.'; + methodStartIndex++; + } + } + + // No type matched, therefore the content must be a property reference, or a recursive call as functions + // are chained together + if (objectType == null) + { + int rootEndIndex = expressionRoot.IndexOf('.'); + propertyName = expressionRoot.Substring(0, rootEndIndex); + + // If propertyValue is null (we're not recursing), then we're expecting a valid property name + if (propertyValue == null && !IsValidPropertyName(propertyName)) + { + // We extracted something that wasn't a valid property name, fail. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + } + + objectType = typeof(string); + } + + // If we are recursively acting on a type that has been already produced + // then pass that type inwards + if (propertyValue != null) + { + objectType = propertyValue.GetType(); + } + + Function function = ConstructFunction(elementLocation, expressionFunction, propertyName, objectType, argumentStartIndex, methodStartIndex, usedUnInitializedProperties); + + return function; + } + + /// + /// Execute the function on the given instance + /// + internal object Execute(object objectInstance, IPropertyProvider properties, ExpanderOptions options, IElementLocation elementLocation) + { + object functionResult = String.Empty; + object[] args = null; + + try + { + // If there is no object instance, then the method invocation will be a static + if (objectInstance == null) + { + // Check that the function that we're going to call is valid to call + if (!IsStaticMethodAvailable(_objectType, _name)) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionMethodUnavailable", _name, _objectType.FullName); + } + + _bindingFlags |= BindingFlags.Static; + + // For our intrinsic function we need to support calling of internal methods + // since we don't want them to be public + if (_objectType == typeof(Microsoft.Build.Evaluation.IntrinsicFunctions)) + { + _bindingFlags |= BindingFlags.NonPublic; + } + } + else + { + _bindingFlags |= BindingFlags.Instance; + + // The object that we're about to call methods on may have escaped characters + // in it, we want to operate on the unescaped string in the function, just as we + // want to pass arguments that are unescaped (see below) + if (objectInstance is string) + { + objectInstance = EscapingUtilities.UnescapeAll((string)objectInstance); + } + } + + // We have a methodinfo match, need to plug in the arguments + args = new object[_arguments.Length]; + + // Assemble our arguments ready for passing to our method + for (int n = 0; n < _arguments.Length; n++) + { + object argument = PropertyExpander.ExpandPropertiesLeaveTypedAndEscaped(_arguments[n], properties, options, elementLocation, _usedUninitializedProperties); + string argumentValue = argument as string; + + if (argumentValue != null) + { + // Unescape the value since we're about to send it out of the engine and into + // the function being called + args[n] = EscapingUtilities.UnescapeAll(argumentValue); + } + else + { + args[n] = argument; + } + } + + // Handle special cases where the object type needs to affect the choice of method + // The default binder and method invoke, often chooses the incorrect Equals and CompareTo and + // fails the comparison, because what we have on the right is generally a string. + // This special casing is to realize that its a comparison that is taking place and handle the + // argument type coercion accordingly; effectively pre-preparing the argument type so + // that it matches the left hand side ready for the default binder’s method invoke. + if (objectInstance != null && args.Length == 1 && (String.Equals("Equals", _name, StringComparison.OrdinalIgnoreCase) || String.Equals("CompareTo", _name, StringComparison.OrdinalIgnoreCase))) + { + // change the type of the final unescaped string into the destination + args[0] = Convert.ChangeType(args[0], objectInstance.GetType(), CultureInfo.InvariantCulture); + } + + // If we've been asked for and instance to be constructed, then we + // need to locate an appropriate constructor and invoke it + if (String.Equals("new", _name, StringComparison.OrdinalIgnoreCase)) + { + functionResult = LateBindExecute(null /* no previous exception */, BindingFlags.Public | BindingFlags.Instance, null /* no instance for a constructor */, args, true /* is constructor */); + } + else + { + // Execute the function given converted arguments + // The only exception that we should catch to try a late bind here is missing method + // otherwise there is the potential of running a function twice! + try + { + // First use InvokeMember using the standard binder - this will match and coerce as needed + functionResult = _objectType.InvokeMember(_name, _bindingFlags, Type.DefaultBinder, objectInstance, args, CultureInfo.InvariantCulture); + } + catch (MissingMethodException ex) // Don't catch and retry on any other exception + { + // If we're invoking a method, then there are deeper attempts that + // can be made to invoke the method + if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) + { + // The standard binder failed, so do our best to coerce types into the arguments for the function + // This may happen if the types need coercion, but it may also happen if the object represents a type that contains open type parameters, that is, ContainsGenericParameters returns true. + functionResult = LateBindExecute(ex, _bindingFlags, objectInstance, args, false /* is not constructor */); + } + else + { + // We were asked to get a property or field, and we found that we cannot + // locate it. Since there is no further argument coersion possible + // we'll throw right now. + throw; + } + } + } + + // If the result of the function call is a string, then we need to escape the result + // so that we maintain the "engine contains escaped data" state. + // The exception is that the user is explicitly calling MSBuild::Unescape or MSBuild::Escape + if (functionResult is string && !String.Equals("Unescape", _name, StringComparison.OrdinalIgnoreCase) && !String.Equals("Escape", _name, StringComparison.OrdinalIgnoreCase)) + { + functionResult = EscapingUtilities.Escape((string)functionResult); + } + + // We have nothing left to parse, so we'll return what we have + if (String.IsNullOrEmpty(_remainder)) + { + return functionResult; + } + + // Recursively expand the remaining property body after execution + return PropertyExpander.ExpandPropertyBody(_remainder, functionResult, properties, options, elementLocation, _usedUninitializedProperties); + } + + // Exceptions coming from the actual function called are wrapped in a TargetInvocationException + catch (TargetInvocationException ex) + { + // We ended up with something other than a function expression + string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _name, args); + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.InnerException.Message.Replace("\r\n", " ")); + return null; + } + + // Any other exception was thrown by trying to call it + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedFunctionException(ex)) + { + throw; + } + + // If there's a :: in the expression, they were probably trying for a static function + // invocation. Give them some more relevant info in that case + if (s_invariantCompareInfo.IndexOf(_expression, "::", CompareOptions.OrdinalIgnoreCase) > -1) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionStaticMethodSyntax", _expression, ex.Message.Replace("Microsoft.Build.Evaluation.IntrinsicFunctions.", "[MSBuild]::")); + } + else + { + // We ended up with something other than a function expression + string partiallyEvaluated = GenerateStringOfMethodExecuted(_expression, objectInstance, _name, args); + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", partiallyEvaluated, ex.Message); + } + + return null; + } + } + + /// + /// Return a Type object for the type we're trying to call static methods on + /// + private static Type GetTypeForStaticMethod(string typeName) + { + // Ultimately this will be a more in-depth lookup, including assembly name etc. + // for now, we're only supporting a subset of what's in mscorlib + specific additional types + // If the env var MSBUILDENABLEALLPROPERTYFUNCTIONS=1 then we'll allow pretty much anything + Type objectType; + Tuple functionType; + + // If we don't have a type name, we already know that we won't be able to find a type. + // Go ahead and return here -- otherwise the Type.GetType() calls below will throw. + if (String.IsNullOrEmpty(typeName)) + { + return null; + } + + // For whole types we support them being in different assemblies than mscorlib + // Get the assembly qualified type name if one exists + if (AvailableStaticMethods.TryGetValue(typeName, out functionType) && functionType != null) + { + // We need at least one of these set + ErrorUtilities.VerifyThrow(functionType.Item1 != null || functionType.Item2 != null, "Function type information needs either string or type represented."); + + // If we have the type information in Type form, then just return that + if (functionType.Item2 != null) + { + return functionType.Item2; + } + else if (functionType.Item1 != null) + { + // This is a case where the Type is not available at compile time, so + // we are forced to bind by name instead + typeName = functionType.Item1; + + // Get the type from the assembly qualified type name from AvailableStaticMethods + objectType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); + + // If we've used it once, chances are that we'll be using it again + // We can record the type here since we know it's available for calling from the fact that is was in the AvailableStaticMethods table + AvailableStaticMethods.TryAdd(typeName, new Tuple(typeName, objectType)); + + return objectType; + } + } + + // Get the type from mscorlib (or the currently running assembly) + objectType = Type.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); + + if (objectType != null) + { + // DO NOT CACHE THE TYPE HERE! + // We don't add the resolved type here in the AvailableStaticMethods table. This is because that table is used + // during function parse, but only later during execution do we check for the ability to call specific methods on specific types. + return objectType; + } + + // Note the following code path is only entered when MSBUILDENABLEALLPROPERTYFUNCTIONS == 1. + // This environment variable must not be cached - it should be dynamically settable while the application is executing. + if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1") + { + // We didn't find the type, so go probing. First in System + if (objectType == null) + { + objectType = GetTypeFromAssembly(typeName, "System"); + } + + // Next in System.Core + if (objectType == null) + { + objectType = GetTypeFromAssembly(typeName, "System.Core"); + } + + // We didn't find the type, so try to find it using the namespace + if (objectType == null) + { + objectType = GetTypeFromAssemblyUsingNamespace(typeName); + } + + if (objectType != null) + { + // If we've used it once, chances are that we'll be using it again + // We can cache the type here, since all functions are enabled + AvailableStaticMethods.TryAdd(typeName, new Tuple(typeName, objectType)); + } + } + + return objectType; + } + + /// + /// Gets the specified type using the namespace to guess the assembly that its in + /// + private static Type GetTypeFromAssemblyUsingNamespace(string typeName) + { + string baseName = typeName; + int assemblyNameEnd = baseName.LastIndexOf('.'); + Type foundType = null; + + // If the string has no dot, or is nothing but a dot, we have no + // namespace to look for, so we can't help. + if (assemblyNameEnd <= 0) + { + return null; + } + + // We will work our way up the namespace looking for an assembly that matches + while (assemblyNameEnd > 0) + { + string candidateAssemblyName = null; + + candidateAssemblyName = baseName.Substring(0, assemblyNameEnd); + + // Try to load the assembly with the computed name + foundType = GetTypeFromAssembly(typeName, candidateAssemblyName); + + if (foundType != null) + { + // We have a match, so get the type from that assembly + return foundType; + } + else + { + // Keep looking as we haven't found a match yet + baseName = candidateAssemblyName; + assemblyNameEnd = baseName.LastIndexOf('.'); + } + } + + // We didn't find it, so we need to give up + return null; + } + + /// + /// Get the specified type from the assembly partial name supplied + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] + private static Type GetTypeFromAssembly(string typeName, string candidateAssemblyName) + { + Type objectType = null; + + // Try to load the assembly with the computed name +#pragma warning disable 618 + // Unfortunately Assembly.Load is not an alternative to LoadWithPartialName, since + // Assembly.Load requires the full assembly name to be passed to it. + // Therefore we must ignore the deprecated warning. + Assembly candidateAssembly = Assembly.LoadWithPartialName(candidateAssemblyName); +#pragma warning restore 618 + + if (candidateAssembly != null) + { + objectType = candidateAssembly.GetType(typeName, false /* do not throw TypeLoadException if not found */, true /* ignore case */); + } + + return objectType; + } + + /// + /// Factory method to construct an indexer function for property evaluation + /// + private static Function ConstructIndexerFunction(string expressionFunction, IElementLocation elementLocation, object propertyValue, string propertyName, Type objectType, int methodStartIndex, string argumentsContent, UsedUninitializedProperties usedUnInitializedProperties) + { + string remainder = expressionFunction.Substring(methodStartIndex); + string functionToInvoke; + string[] functionArguments; + + // If there are no arguments, then just create an empty array + if (String.IsNullOrEmpty(argumentsContent)) + { + functionArguments = new string[0]; + } + else + { + // We will keep empty entries so that we can treat them as null + functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent); + } + + // choose the name of the function based on the type of the object that we + // are using. + if (propertyValue is Array) + { + functionToInvoke = "GetValue"; + } + else if (propertyValue is string) + { + functionToInvoke = "get_Chars"; + } + else // a regular indexer + { + functionToInvoke = "get_Item"; + } + + Function indexerFunction; + + indexerFunction = new Function(objectType, expressionFunction, propertyName, functionToInvoke, functionArguments, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.InvokeMethod, remainder, usedUnInitializedProperties); + return indexerFunction; + } + + /// + /// Factory method to construct a function for property evaluation + /// + private static Function ConstructFunction(IElementLocation elementLocation, string expressionFunction, string expressionRootName, Type objectType, int argumentStartIndex, int methodStartIndex, UsedUninitializedProperties usedUninitializedProperties) + { + // The unevaluated and unexpanded arguments for this function + string[] functionArguments; + + // The name of the function that will be invoked + string functionToInvoke; + + // What's left of the expression once the function has been constructed + string remainder = String.Empty; + + // The binding flags that we will use for this function's execution + BindingFlags defaultBindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public; + + // There are arguments that need to be passed to the function + if (argumentStartIndex > -1 && !expressionFunction.Substring(methodStartIndex, argumentStartIndex - methodStartIndex).Contains(".")) + { + string argumentsContent; + + // separate the function and the arguments + functionToInvoke = expressionFunction.Substring(methodStartIndex, argumentStartIndex - methodStartIndex).Trim(); + + // Skip the '(' + argumentStartIndex++; + + // Scan for the matching closing bracket, skipping any nested ones + int argumentsEndIndex = ScanForClosingParenthesis(expressionFunction, argumentStartIndex); + + if (argumentsEndIndex == -1) + { + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, AssemblyResources.GetString("InvalidFunctionPropertyExpressionDetailMismatchedParenthesis")); + } + + // We have been asked for a method invocation + defaultBindingFlags |= BindingFlags.InvokeMethod; + + // It may be that there are '()' but no actual arguments content + if (argumentStartIndex == expressionFunction.Length - 1) + { + argumentsContent = String.Empty; + functionArguments = new string[0]; + } + else + { + // we have content within the '()' so let's extract and deal with it + argumentsContent = expressionFunction.Substring(argumentStartIndex, argumentsEndIndex - argumentStartIndex); + + // If there are no arguments, then just create an empty array + if (String.IsNullOrEmpty(argumentsContent)) + { + functionArguments = new string[0]; + } + else + { + // We will keep empty entries so that we can treat them as null + functionArguments = ExtractFunctionArguments(elementLocation, expressionFunction, argumentsContent); + } + + remainder = expressionFunction.Substring(argumentsEndIndex + 1); + } + } + else + { + int nextMethodIndex = expressionFunction.IndexOf('.', methodStartIndex); + int methodLength = expressionFunction.Length - methodStartIndex; + int indexerIndex = expressionFunction.IndexOf('[', methodStartIndex); + + // We don't want to consume the indexer + if (indexerIndex >= 0 && indexerIndex < nextMethodIndex) + { + nextMethodIndex = indexerIndex; + } + + functionArguments = new string[0]; + + if (nextMethodIndex > 0) + { + methodLength = nextMethodIndex - methodStartIndex; + remainder = expressionFunction.Substring(nextMethodIndex); + } + + string netPropertyName = expressionFunction.Substring(methodStartIndex, methodLength).Trim(); + + ProjectErrorUtilities.VerifyThrowInvalidProject(netPropertyName.Length > 0, elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + + // We have been asked for a property or a field + defaultBindingFlags |= (BindingFlags.GetProperty | BindingFlags.GetField); + + functionToInvoke = netPropertyName; + } + + // either there are no functions left or what we have is another function or an indexer + if (String.IsNullOrEmpty(remainder) || remainder[0] == '.' || remainder[0] == '[') + { + // Construct a FunctionInfo will all the content that we just gathered + return new Function(objectType, expressionFunction, expressionRootName, functionToInvoke, functionArguments, defaultBindingFlags, remainder, usedUninitializedProperties); + } + else + { + // We ended up with something other than a function expression + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "InvalidFunctionPropertyExpression", expressionFunction, String.Empty); + return null; + } + } + + /// + /// Coerce the arguments according to the parameter types + /// Will only return null if the coercion didn't work due to an InvalidCastException + /// + private static object[] CoerceArguments(object[] args, ParameterInfo[] parameters) + { + object[] coercedArguments = new object[args.Length]; + + try + { + // Do our best to coerce types into the arguments for the function + for (int n = 0; n < parameters.Length; n++) + { + if (args[n] == null) + { + // We can't coerce (object)null -- that's as general + // as it can get! + continue; + } + + // Here we have special case conversions on a type basis + if (parameters[n].ParameterType == typeof(char[])) + { + coercedArguments[n] = args[n].ToString().ToCharArray(); + } + else if (parameters[n].ParameterType.IsEnum && args[n] is string && ((string)args[n]).Contains(".")) + { + Type enumType = parameters[n].ParameterType; + string typeLeafName = enumType.Name + "."; + string typeFullName = enumType.FullName + "."; + + // Enum.parse expects commas between enum components + // We'll support the C# type | syntax too + // We'll also allow the user to specify the leaf or full type name on the enum + string argument = args[n].ToString().Replace('|', ',').Replace(typeFullName, "").Replace(typeLeafName, ""); + + // Parse the string representation of the argument into the destination enum + coercedArguments[n] = Enum.Parse(enumType, argument); + } + else + { + // change the type of the final unescaped string into the destination + coercedArguments[n] = Convert.ChangeType(args[n], parameters[n].ParameterType, CultureInfo.InvariantCulture); + } + } + } + catch (InvalidCastException) + { + // The coercion failed therefore we return null + return null; + } + + return coercedArguments; + } + + /// + /// Make an attempt to create a string showing what we were trying to execute when we failed. + /// This will show any intermediate evaluation which may help the user figure out what happened. + /// + private string GenerateStringOfMethodExecuted(string expression, object objectInstance, string name, object[] args) + { + string parameters = String.Empty; + if (args != null) + { + foreach (object arg in args) + { + if (arg == null) + { + parameters += "null"; + } + else + { + string argString = arg.ToString(); + if (arg is string && argString.Length == 0) + { + parameters += "''"; + } + else + { + parameters += arg.ToString(); + } + } + + parameters += ", "; + } + + if (parameters.Length > 2) + { + parameters = parameters.Substring(0, parameters.Length - 2); + } + } + + if (objectInstance == null) + { + string typeName = _objectType.FullName; + + // We don't want to expose the real type name of our intrinsics + // so we'll replace it with "MSBuild" + if (_objectType == typeof(Microsoft.Build.Evaluation.IntrinsicFunctions)) + { + typeName = "MSBuild"; + } + + if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) + { + return "[" + typeName + "]::" + name + "(" + parameters + ")"; + } + else + { + return "[" + typeName + "]::" + name; + } + } + else + { + string propertyValue = "\"" + objectInstance as string + "\""; + + if ((_bindingFlags & BindingFlags.InvokeMethod) == BindingFlags.InvokeMethod) + { + return propertyValue + "." + name + "(" + parameters + ")"; + } + else + { + return propertyValue + "." + name; + } + } + } + + /// + /// For this initial implementation of inline functions, only very specific static methods on specific types are + /// available + /// + private bool IsStaticMethodAvailable(Type objectType, string methodName) + { + if (objectType == typeof(Microsoft.Build.Evaluation.IntrinsicFunctions)) + { + // These are our intrinsic functions, so we're OK with those + return true; + } + else + { + string typeMethod = objectType.FullName + "::" + methodName; + + if (AvailableStaticMethods.ContainsKey(objectType.FullName)) + { + // Check our set for the type name + // This enables all statics on the given type + return true; + } + else if (AvailableStaticMethods.ContainsKey(typeMethod)) + { + // Check for specific methods on types + return true; + } + else if (Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS") == "1") + { + // If MSBUILDENABLEALLPROPERTYFUNCTION == 1, then anything goes + return true; + } + } + + return false; + } + + /// + /// Construct and instance of objectType based on the constructor or method arguments provided. + /// Arguments must never be null. + /// + private object LateBindExecute(Exception ex, BindingFlags bindingFlags, object objectInstance /* null unless instance method */, object[] args, bool isConstructor) + { + ParameterInfo[] parameters = null; + MethodBase[] members = null; + MethodBase memberInfo = null; + + // First let's try for a method where all arguments are strings.. + Type[] types = new Type[_arguments.Length]; + for (int n = 0; n < _arguments.Length; n++) + { + types[n] = typeof(string); + } + + if (isConstructor) + { + memberInfo = _objectType.GetConstructor(bindingFlags, null, types, null); + } + else + { + memberInfo = _objectType.GetMethod(_name, bindingFlags, null, types, null); + } + + // If we didn't get a match on all string arguments, + // search for a method with the right number of arguments + if (memberInfo == null) + { + // Gather all methods that may match + if (isConstructor) + { + members = _objectType.GetConstructors(bindingFlags); + } + else + { + members = _objectType.GetMethods(bindingFlags); + } + + // Try to find a method with the right name, number of arguments and + // compatible argument types + object[] coercedArguments = null; + foreach (MethodBase member in members) + { + parameters = member.GetParameters(); + + // Simple match on name and number of params, we will be case insensitive + if (parameters.Length == _arguments.Length) + { + if (isConstructor || String.Equals(member.Name, _name, StringComparison.OrdinalIgnoreCase)) + { + // we have a match on the name and argument number + // now let's try to coerce the arguments we have + // into the arguments on the matching method + coercedArguments = CoerceArguments(args, parameters); + + if (coercedArguments != null) + { + // We have a complete match + memberInfo = member; + args = coercedArguments; + break; + } + } + } + } + } + + object functionResult = null; + + // We have a match and coerced arguments, let's construct.. + if (memberInfo != null && args != null) + { + if (isConstructor) + { + functionResult = ((ConstructorInfo)memberInfo).Invoke(args); + } + else + { + functionResult = ((MethodInfo)memberInfo).Invoke(objectInstance /* null if static method */, args); + } + } + else if (!isConstructor) + { + throw ex; + } + + if (functionResult == null && isConstructor) + { + throw new TargetInvocationException(new MissingMethodException()); + } + + return functionResult; + } + } + } + + /// + /// This class wraps information about properties which have been used before they are initialized + /// + internal class UsedUninitializedProperties + { + /// + /// This class wraps information about properties which have been used before they are initialized + /// + internal UsedUninitializedProperties() + { + Properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Hash set of properties which have been used before being initialized + /// + internal IDictionary Properties + { + get; + set; + } + + /// + /// Are we currently supposed to warn if we used an uninitialized property. + /// + internal bool Warn + { + get; + set; + } + + /// + /// What is the currently evaluating property element, this is so that we do not add a un initialized property if we are evaluating that property + /// + internal string CurrentlyEvaluatingPropertyElementName + { + get; + set; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ExpressionShredder.cs b/src/XMakeBuildEngine/Evaluation/ExpressionShredder.cs new file mode 100644 index 00000000000..14bae367684 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ExpressionShredder.cs @@ -0,0 +1,946 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation a class which splits expressions to MSBuild rules. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Security.Permissions; +using System.Diagnostics; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Evaluation +{ + /// + /// What the shredder should be looking for. + /// + [Flags] + internal enum ShredderOptions + { + /// + /// Don't use + /// + Invalid = 0x0, + + /// + /// Shred item types + /// + ItemTypes = 0x1, + + /// + /// Shred metadata not contained inside of a transform. + /// + MetadataOutsideTransforms = 0x2, + + /// + /// Shred both items and metadata not contained in a transform. + /// + All = ItemTypes | MetadataOutsideTransforms + } + + /// + /// A class which interprets and splits MSBuild expressions + /// + internal static class ExpressionShredder + { + /// + /// Splits an expression into fragments at semi-colons, except where the + /// semi-colons are in a macro or separator expression. + /// Fragments are trimmed and empty fragments discarded. + /// + /// + /// These complex cases prevent us from doing a simple split on ';': + /// (1) Macro expression: @(foo->'xxx;xxx') + /// (2) Separator expression: @(foo, 'xxx;xxx') + /// (3) Combination: @(foo->'xxx;xxx', 'xxx;xxx') + /// We must not split on semicolons in macro or separator expressions like these. + /// + /// List expression to split + /// Array of non-empty strings from split list. + internal static IList SplitSemiColonSeparatedList(string expression) + { + expression = expression.Trim(); + + if (expression.Length == 0) + { + return ReadOnlyEmptyList.Instance; + } + + List splitList = new List(1); + int segmentStart = 0; + bool insideItemList = false; + bool insideQuotedPart = false; + string segment; + + // Walk along the string, keeping track of whether we are in an item list expression. + // If we hit a semi-colon or the end of the string and we aren't in an item list, + // add the segment to the list. + for (int current = 0; current < expression.Length; current++) + { + switch (expression[current]) + { + case ';': + if (!insideItemList) + { + // End of segment, so add it to the list + segment = expression.Substring(segmentStart, current - segmentStart).Trim(); + if (segment.Length > 0) + { + splitList.Add(segment); + } + + // Move past this semicolon + segmentStart = current + 1; + } + + break; + case '@': + // An '@' immediately followed by a '(' is the start of an item list + if (expression.Length > current + 1 && expression[current + 1] == '(') + { + // Start of item expression + insideItemList = true; + } + + break; + case ')': + if (insideItemList && !insideQuotedPart) + { + // End of item expression + insideItemList = false; + } + + break; + case '\'': + if (insideItemList) + { + // Start or end of quoted expression in item expression + insideQuotedPart = !insideQuotedPart; + } + + break; + } + } + + // Reached the end of the string: what's left is another segment + segment = expression.Substring(segmentStart, expression.Length - segmentStart).Trim(); + if (segment.Length > 0) + { + splitList.Add(segment); + } + + return splitList; + } + + /// + /// Given a list of expressions that may contain item list expressions, + /// returns a pair of tables of all item names found, as K=Name, V=String.Empty; + /// and all metadata not in transforms, as K=Metadata key, V=MetadataReference, + /// where metadata key is like "itemname.metadataname" or "metadataname". + /// PERF: Tables are null if there are no entries, because this is quite a common case. + /// + internal static ItemsAndMetadataPair GetReferencedItemNamesAndMetadata(List expressions) + { + ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null); + + foreach (string expression in expressions) + { + GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.All); + } + + return pair; + } + + /// + /// Returns true if there is a metadata expression (outside of a transform) in the expression. + /// + internal static bool ContainsMetadataExpressionOutsideTransform(string expression) + { + ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null); + + GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.MetadataOutsideTransforms); + + bool result = (pair.Metadata != null && pair.Metadata.Count > 0); + + return result; + } + + /// + /// Given a subexpression, finds referenced sub transform expressions + /// itemName and separator will be null if they are not found + /// return value will be null if no transform expressions are found + /// + internal static List GetReferencedItemExpressions(string expression) + { + return GetReferencedItemExpressions(expression, 0, expression.Length); + } + + /// + /// Given a subexpression, finds referenced sub transform expressions + /// itemName and separator will be null if they are not found + /// return value will be null if no transform expressions are found + /// + internal static List GetReferencedItemExpressions(string expression, int start, int end) + { + List subExpressions = null; + + if (expression.IndexOf('@') < 0) + { + return null; + } + + for (int i = start; i < end; i++) + { + int restartPoint; + int startPoint; + + if (Sink(expression, ref i, end, '@', '(')) + { + List transformExpressions = null; + string itemName = null; + string separator = null; + int separatorStart = -1; + int separatorLength = -1; + + // Start of a possible item list expression + + // Store the index to backtrack to if this doesn't turn out to be a well + // formed expression. (Subtract one for the increment when we loop around.) + restartPoint = i - 1; + + // Store the expression's start point + startPoint = i - 2; + + SinkWhitespace(expression, ref i); + + int startOfName = i; + + if (!SinkValidName(expression, ref i, end)) + { + i = restartPoint; + continue; + } + + // '-' is a legitimate char in an item name, but we should match '->' as an arrow + // in '@(foo->'x')' rather than as the last char of the item name. + // The old regex accomplished this by being "greedy" + if (end > i && expression[i - 1] == '-' && expression[i] == '>') + { + i--; + } + + // Grab the name, but continue to verify it's a well-formed expression + // before we store it. + string name = expression.Substring(startOfName, i - startOfName); + + // return the item that we're working with + itemName = name; + + SinkWhitespace(expression, ref i); + bool transformOrFunctionFound = true; + + // If there's an '->' eat it and the subsequent quoted expression or transform function + while (Sink(expression, ref i, end, '-', '>') && transformOrFunctionFound) + { + SinkWhitespace(expression, ref i); + int startTransform = i; + + bool isQuotedTransform = SinkSingleQuotedExpression(expression, ref i, end); + if (isQuotedTransform) + { + int startQuoted = startTransform + 1; + int endQuoted = i - 1; + if (transformExpressions == null) + { + transformExpressions = new List(); + } + + transformExpressions.Add(new ItemExpressionCapture(startQuoted, endQuoted - startQuoted, expression.Substring(startQuoted, endQuoted - startQuoted))); + continue; + } + + startTransform = i; + ItemExpressionCapture functionCapture = SinkItemFunctionExpression(expression, startTransform, ref i, end); + if (functionCapture != null) + { + if (transformExpressions == null) + { + transformExpressions = new List(); + } + + transformExpressions.Add(functionCapture); + continue; + } + + if (!isQuotedTransform && functionCapture == null) + { + i = restartPoint; + transformOrFunctionFound = false; + } + } + + if (!transformOrFunctionFound) + { + continue; + } + + SinkWhitespace(expression, ref i); + + // If there's a ',', eat it and the subsequent quoted expression + if (Sink(expression, ref i, ',')) + { + SinkWhitespace(expression, ref i); + + if (!Sink(expression, ref i, '\'')) + { + i = restartPoint; + continue; + } + + int closingQuote = expression.IndexOf('\'', i); + if (closingQuote == -1) + { + i = restartPoint; + continue; + } + + separatorStart = i - startPoint; + separatorLength = closingQuote - i; + separator = expression.Substring(i, separatorLength); + + i = closingQuote + 1; + } + + SinkWhitespace(expression, ref i); + + if (!Sink(expression, ref i, ')')) + { + i = restartPoint; + continue; + } + + int endPoint = i; + i--; + + if (subExpressions == null) + { + subExpressions = new List(); + } + + // Create an expression capture that encompases the entire expression between the @( and the ) + // with the item name and any separator contained within it + // and each transform expression contained within it (i.e. each ->XYZ) + ItemExpressionCapture expressionCapture = new ItemExpressionCapture(startPoint, endPoint - startPoint, expression.Substring(startPoint, endPoint - startPoint), itemName, separator, separatorStart, separatorLength, transformExpressions); + subExpressions.Add(expressionCapture); + + continue; + } + } + + return subExpressions; + } + + /// + /// Given a subexpression, finds referenced item names and inserts them into the table + /// as K=Name, V=String.Empty. + /// + /// + /// We can ignore any semicolons in the expression, since we're not itemizing it. + /// + private static void GetReferencedItemNamesAndMetadata(string expression, int start, int end, ref ItemsAndMetadataPair pair, ShredderOptions whatToShredFor) + { + for (int i = start; i < end; i++) + { + int restartPoint; + + if (Sink(expression, ref i, end, '@', '(')) + { + // Start of a possible item list expression + + // Store the index to backtrack to if this doesn't turn out to be a well + // formed metadata expression. (Subtract one for the increment when we loop around.) + restartPoint = i - 1; + + SinkWhitespace(expression, ref i); + + int startOfName = i; + + if (!SinkValidName(expression, ref i, end)) + { + i = restartPoint; + continue; + } + + // '-' is a legitimate char in an item name, but we should match '->' as an arrow + // in '@(foo->'x')' rather than as the last char of the item name. + // The old regex accomplished this by being "greedy" + if (end > i && expression[i - 1] == '-' && expression[i] == '>') + { + i--; + } + + // Grab the name, but continue to verify it's a well-formed expression + // before we store it. + string name = expression.Substring(startOfName, i - startOfName); + + SinkWhitespace(expression, ref i); + + bool transformOrFunctionFound = true; + + // If there's an '->' eat it and the subsequent quoted expression or transform function + while (Sink(expression, ref i, end, '-', '>') && transformOrFunctionFound) + { + SinkWhitespace(expression, ref i); + int startTransform = i; + + bool isQuotedTransform = SinkSingleQuotedExpression(expression, ref i, end); + if (isQuotedTransform) + { + continue; + } + + ItemExpressionCapture functionCapture = SinkItemFunctionExpression(expression, startTransform, ref i, end); + if (functionCapture != null) + { + continue; + } + + if (!isQuotedTransform && functionCapture == null) + { + i = restartPoint; + transformOrFunctionFound = false; + } + } + + if (!transformOrFunctionFound) + { + continue; + } + + SinkWhitespace(expression, ref i); + + // If there's a ',', eat it and the subsequent quoted expression + if (Sink(expression, ref i, ',')) + { + SinkWhitespace(expression, ref i); + + if (!Sink(expression, ref i, '\'')) + { + i = restartPoint; + continue; + } + + int closingQuote = expression.IndexOf('\'', i); + if (closingQuote == -1) + { + i = restartPoint; + continue; + } + + // Look for metadata in the separator expression + // e.g., @(foo, '%(bar)') contains batchable metadata 'bar' + GetReferencedItemNamesAndMetadata(expression, i, closingQuote, ref pair, ShredderOptions.MetadataOutsideTransforms); + + i = closingQuote + 1; + } + + SinkWhitespace(expression, ref i); + + if (!Sink(expression, ref i, ')')) + { + i = restartPoint; + continue; + } + + // If we've got this far, we know the item expression was + // well formed, so make sure the name's in the table + if ((whatToShredFor & ShredderOptions.ItemTypes) != 0) + { + pair.Items = pair.Items ?? new HashSet(MSBuildNameIgnoreCaseComparer.Default); + pair.Items.Add(name); + } + + i--; + + continue; + } + + if (Sink(expression, ref i, end, '%', '(')) + { + // Start of a possible metadata expression + + // Store the index to backtrack to if this doesn't turn out to be a well + // formed metadata expression. (Subtract one for the increment when we loop around.) + restartPoint = i - 1; + + SinkWhitespace(expression, ref i); + + int startOfText = i; + + if (!SinkValidName(expression, ref i, end)) + { + i = restartPoint; + continue; + } + + // Grab this, but we don't know if it's an item or metadata name yet + string firstPart = expression.Substring(startOfText, i - startOfText); + string itemName = null; + string metadataName; + string qualifiedMetadataName; + + SinkWhitespace(expression, ref i); + + bool qualified = Sink(expression, ref i, '.'); + + if (qualified) + { + SinkWhitespace(expression, ref i); + + startOfText = i; + + if (!SinkValidName(expression, ref i, end)) + { + i = restartPoint; + continue; + } + + itemName = firstPart; + metadataName = expression.Substring(startOfText, i - startOfText); + qualifiedMetadataName = itemName + "." + metadataName; + } + else + { + metadataName = firstPart; + qualifiedMetadataName = metadataName; + } + + SinkWhitespace(expression, ref i); + + if (!Sink(expression, ref i, ')')) + { + i = restartPoint; + continue; + } + + if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) + { + pair.Metadata = pair.Metadata ?? new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); + } + + i--; + } + } + } + + /// + /// Returns true if a single quoted subexpression begins at the specified index + /// and ends before the specified end index. + /// Leaves index one past the end of the second quote. + /// + private static bool SinkSingleQuotedExpression(string expression, ref int i, int end) + { + if (!Sink(expression, ref i, '\'')) + { + return false; + } + + while (i < end && expression[i] != '\'') + { + i++; + } + + i++; + + if (end <= i) + { + return false; + } + + return true; + } + + /// + /// Scan for the closing bracket that matches the one we've already skipped; + /// essentially, pushes and pops on a stack of parentheses to do this. + /// Takes the expression and the index to start at. + /// Returns the index of the matching parenthesis, or -1 if it was not found. + /// + private static bool SinkArgumentsInParentheses(string expression, ref int i, int end) + { + int nestLevel = 0; + int length = expression.Length; + int restartPoint; + + unsafe + { + fixed (char* pchar = expression) + { + if (pchar[i] == '(') + { + nestLevel++; + i++; + } + else + { + return false; + } + + // Scan for our closing ')' + while (i < length && i < end && nestLevel > 0) + { + char character = pchar[i]; + + if (character == '\'' || character == '`' || character == '"') + { + restartPoint = i; + if (!SinkUntilClosingQuote(character, expression, ref i, end)) + { + i = restartPoint; + return false; + } + } + else if (character == '(') + { + nestLevel++; + } + else if (character == ')') + { + nestLevel--; + } + + i++; + } + } + } + + if (nestLevel == 0) + { + return true; + } + else + { + return false; + } + } + + /// + /// Skip all characters until we find the matching quote character + /// + private static bool SinkUntilClosingQuote(char quoteChar, string expression, ref int i, int end) + { + unsafe + { + fixed (char* pchar = expression) + { + // We have already checked the first quote + i++; + + // Scan for our closing quoteChar + while (i < expression.Length && i < end) + { + if (pchar[i] == quoteChar) + { + return true; + } + + i++; + } + } + } + + return false; + } + + /// + /// Returns true if a item function subexpression begins at the specified index + /// and ends before the specified end index. + /// Leaves index one past the end of the closing paren. + /// + private static ItemExpressionCapture SinkItemFunctionExpression(string expression, int startTransform, ref int i, int end) + { + if (SinkValidName(expression, ref i, end)) + { + int endFunctionName = i; + + // Eat any whitespace between the function name and its arguments + SinkWhitespace(expression, ref i); + int startFunctionArguments = i + 1; + + if (SinkArgumentsInParentheses(expression, ref i, end)) + { + int endFunctionArguments = i - 1; + + ItemExpressionCapture capture = new ItemExpressionCapture(startTransform, i - startTransform, expression.Substring(startTransform, i - startTransform)); + capture.FunctionName = expression.Substring(startTransform, endFunctionName - startTransform); + + if (endFunctionArguments > startFunctionArguments) + { + capture.FunctionArguments = expression.Substring(startFunctionArguments, endFunctionArguments - startFunctionArguments); + } + + return capture; + } + + return null; + } + else + { + return null; + } + } + + /// + /// Returns true if a valid name begins at the specified index. + /// Leaves index one past the end of the name. + /// + private static bool SinkValidName(string expression, ref int i, int end) + { + if (end <= i || !XmlUtilities.IsValidInitialElementNameCharacter(expression[i])) + { + return false; + } + + i++; + + while (end > i && XmlUtilities.IsValidSubsequentElementNameCharacter(expression[i])) + { + i++; + } + + return true; + } + + /// + /// Returns true if the character at the specified index + /// is the specified char. + /// Leaves index one past the character. + /// + private static bool Sink(string expression, ref int i, char c) + { + if (i < expression.Length && expression[i] == c) + { + i++; + return true; + } + + return false; + } + + /// + /// Returns true if the next two characters at the specified index + /// are the specified sequence. + /// Leaves index one past the second character. + /// + private static bool Sink(string expression, ref int i, int end, char c1, char c2) + { + if (i < end - 1 && expression[i] == c1 && expression[i + 1] == c2) + { + i = i + 2; + return true; + } + + return false; + } + + /// + /// Moves past all whitespace starting at the specified index. + /// Returns the next index, possibly the string length. + /// + /// + /// Char.IsWhitespace() is not identical in behavior to regex's \s character class, + /// but it's extremely close, and it's what we use in conditional expressions. + /// + /// The expression to process. + /// The start location for skipping whitespace, contains the next non-whitespace character on exit. + private static void SinkWhitespace(string expression, ref int i) + { + while (i < expression.Length && Char.IsWhiteSpace(expression[i])) + { + i++; + } + } + + /// + /// Represents one substring for a single successful capture. + /// + internal class ItemExpressionCapture + { + /// + /// Captures within this capture + /// + private List _captures; + + /// + /// The position in the original string where the first character of the captured + /// substring was found. + /// + private int _index; + + /// + /// The length of the captured substring. + /// + private int _length; + + /// + /// The captured substring from the input string. + /// + private string _value; + + /// + /// The type of the item within this expression + /// + private string _itemType; + + /// + /// The separator, if any, within this expression + /// + private string _separator; + + /// + /// The starting character of the separator within the expression + /// + private int _separatorStart; + + /// + /// The length of the separator + /// + private int _separatorLength; + + /// + /// The function name, if any, within this expression + /// + private string _functionName; + + /// + /// The function arguments, if any, within this expression + /// + private string _functionArguments; + + /// + /// Create an Expression Capture instance + /// Represents a sub expression, shredded from a larger expression + /// + public ItemExpressionCapture(int index, int length, string subExpression) : this(index, length, subExpression, null, null, -1, -1, null) + { + } + + /// + /// Create an Expression Capture instance + /// Represents a sub expression, shredded from a larger expression + /// + public ItemExpressionCapture(int index, int length, string subExpression, string itemType, string separator, int separatorStart, int separatorLength, List captures) + { + _index = index; + _length = length; + _value = subExpression; + _itemType = itemType; + _separator = separator; + _separatorStart = separatorStart; + _separatorLength = separatorLength; + _captures = captures; + } + + /// + /// Captures within this capture + /// + public List Captures + { + get { return _captures; } + set { _captures = value; } + } + + /// + /// The position in the original string where the first character of the captured + /// substring was found. + /// + public int Index + { + get { return _index; } + } + + /// + /// The length of the captured substring. + /// + public int Length + { + get { return _length; } + } + + /// + /// Gets the captured substring from the input string. + /// + public string Value + { + get { return _value; } + } + + /// + /// Gets the captured itemtype. + /// + public string ItemType + { + get { return _itemType; } + set { _itemType = value; } + } + + /// + /// Gets the captured itemtype. + /// + public string Separator + { + get { return _separator; } + set { _separator = value; } + } + + /// + /// The starting character of the separator. + /// + public int SeparatorStart + { + get { return _separatorStart; } + } + + /// + /// The length of the separator. + /// + public int SeparatorLength + { + get { return _separatorLength; } + } + + /// + /// The function name, if any, within this expression + /// + public string FunctionName + { + get { return _functionName; } + set { _functionName = value; } + } + + /// + /// The function arguments, if any, within this expression + /// + public string FunctionArguments + { + get { return _functionArguments; } + set { _functionArguments = value; } + } + + /// + /// Gets the captured substring from the input string. + /// + public override string ToString() + { + return _value; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/IEvaluatorData.cs b/src/XMakeBuildEngine/Evaluation/IEvaluatorData.cs new file mode 100644 index 00000000000..8bafc5848aa --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IEvaluatorData.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// An interface for objects which can have project xml evaluated into them. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An interface for objects which the Evaluator can use as a destination for evaluation of ProjectRootElement. + /// + /// The type of properties to be produced. + /// The type of items to be produced. + /// The type of metadata on those items. + /// The type of item definitions to be produced. + internal interface IEvaluatorData : IPropertyProvider

, IItemProvider + where P : class, IProperty, IEquatable

, IValued + where I : class, IItem + where M : class, IMetadatum + where D : class, IItemDefinition + { + ///

+ /// The (project) directory that should be used during evaluation + /// + string Directory + { + get; + } + + /// + /// Task classes and locations known to this project. + /// This is the project-specific task registry, which is consulted before + /// the toolset's task registry. + /// + TaskRegistry TaskRegistry + { + get; + set; + } + + /// + /// The toolset data used during evaluation, and which should be used for build. + /// + Toolset Toolset + { + get; + } + + /// + /// The sub-toolset version that should be used with this toolset to determine + /// the full set of properties to be used by the build. + /// + string SubToolsetVersion + { + get; + } + + /// + /// The externally specified tools version to evaluate with, if any. + /// For example, the tools version from a /tv switch. + /// This is not the tools version specified on the Project tag, if any. + /// May be null. + /// + string ExplicitToolsVersion + { + get; + } + + /// + /// Gets the global properties + /// + PropertyDictionary GlobalPropertiesDictionary + { + get; + } + + /// + /// List of names of the properties that, while global, are still treated as overridable + /// + ISet GlobalPropertiesToTreatAsLocal + { + get; + } + + /// + /// Sets the initial targets + /// + List InitialTargets + { + get; + set; + } + + /// + /// Sets the default targets + /// + List DefaultTargets + { + get; + set; + } + + /// + /// Sets or retrieves the list of targets which run before the keyed target. + /// + IDictionary> BeforeTargets + { + get; + set; + } + + /// + /// Sets or retrieves the list of targets which run after the keyed target. + /// + IDictionary> AfterTargets + { + get; + set; + } + + /// + /// List of possible values for properties inferred from certain conditions, + /// keyed by the property name. + /// + Dictionary> ConditionedProperties + { + get; + } + + /// + /// Whether evaluation should collect items ignoring condition, + /// as well as items respecting condition; and collect + /// conditioned properties, as well as regular properties + /// + bool ShouldEvaluateForDesignTime + { + get; + } + + /// + /// Enumerator over properties in this project. + /// Exposed for debugging display. + /// + PropertyDictionary

Properties + { + get; + } + + ///

+ /// Enumerator over all item definitions. + /// Exposed for debugging display. + /// Ideally the dictionary would be exposed, but there are + /// covariance problems. (A dictionary of Key, Value cannot be upcast + /// to a Dictionary of Key, IValue). + /// + IEnumerable ItemDefinitionsEnumerable + { + get; + } + + /// + /// Enumerator over all items. + /// Exposed for debugging display. + /// Ideally the dictionary would be exposed, but there are + /// covariance problems. (A dictionary of Key, Value cannot be upcast + /// to a Dictionary of Key, IValue). + /// + ItemDictionary Items + { + get; + } + + /// + /// Prepares the data block for a new evaluation pass + /// + void InitializeForEvaluation(IToolsetProvider toolsetProvider); + + /// + /// Indicates to the data block that evaluation has completed, + /// so for example it can mark datastructures read-only. + /// + void FinishEvaluation(); + + /// + /// Adds a new item + /// + void AddItem(I item); + + /// + /// Adds a new item to the collection of all items ignoring condition + /// + void AddItemIgnoringCondition(I item); + + /// + /// Adds a new item definition + /// + IItemDefinition AddItemDefinition(string itemType); + + /// + /// Properties encountered during evaluation. These are read during the first evaluation pass. + /// Unlike those returned by the Properties property, these are ordered, and include any properties that + /// were subsequently overridden by others with the same name. It does not include any + /// properties whose conditions did not evaluate to true. + /// + void AddToAllEvaluatedPropertiesList(P property); + + /// + /// Item definition metadata encountered during evaluation. These are read during the second evaluation pass. + /// Unlike those returned by the ItemDefinitions property, these are ordered, and include any metadata that + /// were subsequently overridden by others with the same name and item type. It does not include any + /// elements whose conditions did not evaluate to true. + /// + void AddToAllEvaluatedItemDefinitionMetadataList(M itemDefinitionMetadatum); + + /// + /// Items encountered during evaluation. These are read during the third evaluation pass. + /// Unlike those returned by the Items property, these are ordered. + /// It does not include any elements whose conditions did not evaluate to true. + /// It does not include any items added since the last evaluation. + /// + void AddToAllEvaluatedItemsList(I item); + + /// + /// Retrieves an existing item definition, if any. + /// + IItemDefinition GetItemDefinition(string itemType); + + /// + /// Sets a property which does not come from the Xml. + /// + P SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved); + + /// + /// Sets a property which comes from the Xml. + /// Predecessor is any immediately previous property that was overridden by this one during evaluation. + /// This would include all properties with the same name that lie above in the logical + /// project file, and whose conditions evaluated to true. + /// If there are none above this is null. + /// + P SetProperty(ProjectPropertyElement propertyElement, string evaluatedValueEscaped, P predecessor); + + /// + /// Retrieves an existing target, if any. + /// + ProjectTargetInstance GetTarget(string targetName); + + /// + /// Adds a new target, overwriting any existing target with the same name. + /// + void AddTarget(ProjectTargetInstance target); + + /// + /// Record an import opened during evaluation, if appropriate. + /// + void RecordImport(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated); + + /// + /// Record an import opened during evaluation, if appropriate. + /// + void RecordImportWithDuplicates(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated); + + /// + /// Evaluates the provided string by expanding items and properties, + /// using the current items and properties available. + /// This is useful for the immediate window. + /// Does not expand bare metadata expressions. + /// + /// + /// Not for internal use. + /// + string ExpandString(string unexpandedValue); + + /// + /// Evaluates the provided string as a condition by expanding items and properties, + /// using the current items and properties available, then doing a logical evaluation. + /// This is useful for the immediate window. + /// Does not expand bare metadata expressions. + /// + /// + /// Not for internal use. + /// + bool EvaluateCondition(string condition); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IItem.cs b/src/XMakeBuildEngine/Evaluation/IItem.cs new file mode 100644 index 00000000000..f277e98e62d --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IItem.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface representing an item without exposing its type. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This interface represents an item without exposing its type. + /// + internal interface IItem : IKeyed + { + /// + /// Gets the evaluated include value for this item, unescaped. + /// + string EvaluatedInclude + { + get; + } + + /// + /// Gets the evaluated include value for this item, escaped as necessary. + /// + string EvaluatedIncludeEscaped + { + get; + } + + /// + /// The directory of the project being built + /// If there is no project filename defined, returns null. + /// + string ProjectDirectory + { + get; + } + + /// + /// Returns the metadata with the specified key. + /// Returns null if it does not exist. + /// Attempting to get built-in metadata on a value that is not a valid path throws InvalidOperationException. + /// Metadata value is unescaped. + /// + string GetMetadataValue(string name); + + /// + /// Returns the metadata with the specified key. + /// Returns null if it does not exist. + /// Attempting to get built-in metadata on a value that is not a valid path throws InvalidOperationException. + /// Metadata value is the escaped value initially set. + /// + string GetMetadataValueEscaped(string name); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IItemDefinition.cs b/src/XMakeBuildEngine/Evaluation/IItemDefinition.cs new file mode 100644 index 00000000000..e495cfdb407 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IItemDefinition.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface representing item definition objects for use by the Evaulator. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Interface representing item definition objects for use by the Evaulator. + /// + /// Type of metadata objects. + internal interface IItemDefinition : IMetadataTable + where M : class, IMetadatum + { + /// + /// Gets any metadatum on this item definition with the specified name. + /// + M GetMetadata(string name); + + /// + /// Adds the specified metadata to the item definition. + /// + M SetMetadata(ProjectMetadataElement metadataElement, string evaluatedValue, M predecessor); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IItemFactory.cs b/src/XMakeBuildEngine/Evaluation/IItemFactory.cs new file mode 100644 index 00000000000..17b32071b7a --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IItemFactory.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for an object which can create items for the Expander +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This interface is used to describe a class which can act as a factory for creating + /// items when the Expander expands certain expressions. + /// + /// The type of items this factory can clone from. + /// The type of items this factory will create. + internal interface IItemFactory + where S : class, IItem + where T : class, IItem + { + /// + /// The item type of the items that this factory will create. + /// May be null, if the items will not have an itemtype (ie., for ITaskItems) + /// May not be settable (eg., for ITaskItems and for ProjectItems) + /// + string ItemType + { + get; + set; + } + + /// + /// Used in the evaluator + /// + ProjectItemElement ItemElement + { + set; + } + + /// + /// Creates an item with the specified evaluated include and defining project. + /// Include must not be zero length. + /// + /// The include + /// The project from which this item was created + /// A new item instance + T CreateItem(string include, string definingProject); + + /// + /// Creates an item based off the provided item, with cloning semantics. + /// New item is associated with the passed in defining project, not that of the original item. + /// + T CreateItem(S source, string definingProject); + + /// + /// Creates an item with the specified include and the metadata from the specified base item + /// New item is associated with the passed in defining project, not that of the original item. + /// + T CreateItem(string include, S baseItem, string definingProject); + + /// + /// Creates an item using the specified evaluated include, include before wildcard expansion, + /// and defining project. + /// + T CreateItem(string include, string includeBeforeWildcardExpansion, string definingProject); + + /// + /// Applies the supplied metadata to the destination items. + /// + void SetMetadata(IEnumerable> metadata, IEnumerable destinationItems); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IItemProvider.cs b/src/XMakeBuildEngine/Evaluation/IItemProvider.cs new file mode 100644 index 00000000000..9d6a500ac7b --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IItemProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for an item provider which can be used with the Expander +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This interface represents an object which can act as a source of items for the Expander. + /// + /// The type of items provided by the implementation. + internal interface IItemProvider where T : IItem + { + /// + /// Returns a list of items with the specified item type. + /// + /// If there are no items of this type, returns an empty list. + /// + /// The item type of items to return. + /// A list of matching items. + ICollection GetItems(string itemType); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IMetadataTable.cs b/src/XMakeBuildEngine/Evaluation/IMetadataTable.cs new file mode 100644 index 00000000000..70964320bf9 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IMetadataTable.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for a table of metadata. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Table of metadata useable to expand expressions + /// + internal interface IMetadataTable + { + /// + /// Retrieves any value we have in our metadata table for the metadata name specified. + /// If no value is available, returns empty string. + /// + string GetEscapedValue(string name); + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If item type is null, it is ignored. + /// If no value is available, returns empty string. + /// + string GetEscapedValue(string itemType, string name); + + /// + /// Returns the value if it exists, null otherwise. + /// If item type is null, it is ignored. + /// + string GetEscapedValueIfPresent(string itemType, string name); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IMetadatum.cs b/src/XMakeBuildEngine/Evaluation/IMetadatum.cs new file mode 100644 index 00000000000..4449818587f --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IMetadatum.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface representing a piece of metadata. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This interface represents a metadata object. + /// + internal interface IMetadatum + { + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IProjectMetadataParent.cs b/src/XMakeBuildEngine/Evaluation/IProjectMetadataParent.cs new file mode 100644 index 00000000000..16a8da6816d --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IProjectMetadataParent.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for the parent of a ProjectMetadata object. +//----------------------------------------------------------------------- + +namespace Microsoft.Build.Evaluation +{ + /// + /// Represents the parent of a ProjectMetadata object - + /// either a ProjectItem or a ProjectItemDefinition. + /// + internal interface IProjectMetadataParent : IMetadataTable + { + /// + /// The owning project + /// + Project Project + { + get; + } + + /// + /// The item type of the parent item definition or item. + /// + string ItemType + { + get; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IProperty.cs b/src/XMakeBuildEngine/Evaluation/IProperty.cs new file mode 100644 index 00000000000..b00a4cf07f8 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IProperty.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for properties +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An interface representing an object which can act as a property. + /// + internal interface IProperty : IKeyed + { + /// + /// Name of the property + /// + string Name + { + get; + } + + /// + /// Returns the evaluated, unescaped value for the property. + /// + string EvaluatedValue + { + get; + } + + /// + /// Returns the evaluated, escaped value for the property + /// + string EvaluatedValueEscaped + { + get; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IPropertyProvider.cs b/src/XMakeBuildEngine/Evaluation/IPropertyProvider.cs new file mode 100644 index 00000000000..d5e2aadde16 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IPropertyProvider.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Generic Interface for a property provider usable with the expander +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.Evaluation +{ + /// + /// An interface representing an object which can provide properties to the Expander. + /// + /// The type of properties provided. + internal interface IPropertyProvider where T : class + { + /// + /// Returns a property with the specified name, or null if it was not found. + /// + /// The property name. + /// The property. + T GetProperty(string name); + + /// + /// Returns a property with the specified name, or null if it was not found. + /// Name is the segment of the provided string with the provided start and end indexes. + /// + T GetProperty(string name, int startIndex, int endIndex); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IToolsetProvider.cs b/src/XMakeBuildEngine/Evaluation/IToolsetProvider.cs new file mode 100644 index 00000000000..a11a71bef06 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IToolsetProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface for classes providing access to toolsets. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Interface for an object which can provide toolsets for evaulation. + /// + internal interface IToolsetProvider + { + /// + /// Gets an enumeration of all toolsets in the provider. + /// + ICollection Toolsets + { + get; + } + + /// + /// Retrieves a specific toolset. + /// + /// The tools version for the toolset. + /// The requested toolset. + Toolset GetToolset(string toolsVersion); + } +} diff --git a/src/XMakeBuildEngine/Evaluation/IntrinsicFunctions.cs b/src/XMakeBuildEngine/Evaluation/IntrinsicFunctions.cs new file mode 100644 index 00000000000..4f14f002df6 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/IntrinsicFunctions.cs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of functions which can be accessed from MSBuild files. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; + +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using System.Collections.Concurrent; +using Microsoft.Win32; + +// Needed for DoesTaskHostExistForParameters +using NodeProviderOutOfProcTaskHost = Microsoft.Build.BackEnd.NodeProviderOutOfProcTaskHost; + +namespace Microsoft.Build.Evaluation +{ + /// + /// The Intrinsic class provides static methods that can be accessed from MSBuild's + /// property functions using $([MSBuild]::Function(x,y)) + /// + internal static class IntrinsicFunctions + { + /// + /// Add two doubles + /// + internal static double Add(double a, double b) + { + return a + b; + } + + /// + /// Add two longs + /// + internal static long Add(long a, long b) + { + return a + b; + } + + /// + /// Subtract two doubles + /// + internal static double Subtract(double a, double b) + { + return a - b; + } + + /// + /// Subtract two longs + /// + internal static long Subtract(long a, long b) + { + return a - b; + } + + /// + /// Multiply two doubles + /// + internal static double Multiply(double a, double b) + { + return a * b; + } + + /// + /// Multiply two longs + /// + internal static long Multiply(long a, long b) + { + return a * b; + } + + /// + /// Divide two doubles + /// + internal static double Divide(double a, double b) + { + return a / b; + } + + /// + /// Divide two longs + /// + internal static long Divide(long a, long b) + { + return a / b; + } + + /// + /// Modulo two doubles + /// + internal static double Modulo(double a, double b) + { + return a % b; + } + + /// + /// Modulo two longs + /// + internal static long Modulo(long a, long b) + { + return a % b; + } + + /// + /// Escape the string according to MSBuild's escaping rules + /// + internal static string Escape(string unescaped) + { + return EscapingUtilities.Escape(unescaped); + } + + /// + /// Unescape the string according to MSBuild's escaping rules + /// + internal static string Unescape(string escaped) + { + return EscapingUtilities.UnescapeAll(escaped); + } + + /// + /// Perform a bitwise OR on the first and second (first | second) + /// + internal static int BitwiseOr(int first, int second) + { + return first | second; + } + + /// + /// Perform a bitwise AND on the first and second (first & second) + /// + internal static int BitwiseAnd(int first, int second) + { + return first & second; + } + + /// + /// Perform a bitwise XOR on the first and second (first ^ second) + /// + internal static int BitwiseXor(int first, int second) + { + return first ^ second; + } + + /// + /// Perform a bitwise NOT on the first and second (~first) + /// + internal static int BitwiseNot(int first) + { + return ~first; + } + + /// + /// Get the value of the registry key and value, default value is null + /// + internal static object GetRegistryValue(string keyName, string valueName) + { + return Registry.GetValue(keyName, valueName, null /* null to match the $(Regsitry:XYZ@ZBC) behaviour */); + } + + /// + /// Get the value of the registry key and value + /// + internal static object GetRegistryValue(string keyName, string valueName, object defaultValue) + { + return Registry.GetValue(keyName, valueName, defaultValue); + } + + /// + /// Get the value of the registry key from one of the RegistryView's specified + /// + internal static object GetRegistryValueFromView(string keyName, string valueName, object defaultValue, params object[] views) + { + string subKeyName; + + // We will take on handing of default value + // A we need to act on the null return from the GetValue call below + // so we can keep searching other registry views + object result = defaultValue; + + // If we haven't been passed any views, then we'll just use the default view + if (views == null || views.Length == 0) + { + views = new object[] { RegistryView.Default }; + } + + foreach (object viewObject in views) + { + string viewAsString = viewObject as string; + + if (viewAsString != null) + { + string typeLeafName = typeof(RegistryView).Name + "."; + string typeFullName = typeof(RegistryView).FullName + "."; + + // We'll allow the user to specify the leaf or full type name on the RegistryView enum + viewAsString = viewAsString.Replace(typeFullName, "").Replace(typeLeafName, ""); + + // This may throw - and that's fine as the user will receive a controlled version + // of that error. + RegistryView view = (RegistryView)Enum.Parse(typeof(RegistryView), viewAsString, true); + + using (RegistryKey key = GetBaseKeyFromKeyName(keyName, view, out subKeyName)) + { + if (key != null) + { + using (RegistryKey subKey = key.OpenSubKey(subKeyName, false)) + { + // If we managed to retrieve the subkey, then move onto locating the value + if (subKey != null) + { + result = subKey.GetValue(valueName); + } + + // We've found a value, so stop looking + if (result != null) + { + break; + } + } + } + } + } + } + + // We will have either found a result or defaultValue if one wasn't found at this point + return result; + } + + /// + /// Given the absolute location of a file, and a disc location, returns relative file path to that disk location. + /// Throws UriFormatException. + /// + /// + /// The base path we want to relativize to. Must be absolute. + /// Should not include a filename as the last segment will be interpreted as a directory. + /// + /// + /// The path we need to make relative to basePath. The path can be either absolute path or a relative path in which case it is relative to the base path. + /// If the path cannot be made relative to the base path (for example, it is on another drive), it is returned verbatim. + /// + /// relative path (can be the full path) + internal static string MakeRelative(string basePath, string path) + { + string result = FileUtilities.MakeRelative(basePath, path); + + return result; + } + + /// + /// Locate a file in either the directory specified or a location in the + /// direcorty structure above that directory. + /// + internal static string GetDirectoryNameOfFileAbove(string startingDirectory, string fileName) + { + // Canonicalize our starting location + string lookInDirectory = Path.GetFullPath(startingDirectory); + + do + { + // Construct the path that we will use to test against + string possibleFileDirectory = Path.Combine(lookInDirectory, fileName); + + // If we successfully locate the file in the directory that we're + // looking in, simply return that location. Otherwise we'll + // keep moving up the tree. + if (File.Exists(possibleFileDirectory)) + { + // We've found the file, return the directory we found it in + return lookInDirectory; + } + else + { + // GetDirectoryName will return null when we reach the root + // terminating our search + lookInDirectory = Path.GetDirectoryName(lookInDirectory); + } + } + while (lookInDirectory != null); + + // When we didn't find the location, then return an empty string + return String.Empty; + } + + /// + /// Return the string in parameter 'defaultValue' only if parameter 'conditionValue' is empty + /// else, return the value conditionValue + /// + internal static string ValueOrDefault(string conditionValue, string defaultValue) + { + if (String.IsNullOrEmpty(conditionValue)) + { + return defaultValue; + } + else + { + return conditionValue; + } + } + + /// + /// Returns true if a task host exists that can service the requested runtime and architecture + /// values, and false otherwise. + /// + internal static bool DoesTaskHostExist(string runtime, string architecture) + { + if (runtime != null) + { + runtime = runtime.Trim(); + } + + if (architecture != null) + { + architecture = architecture.Trim(); + } + + if (!XMakeAttributes.IsValidMSBuildRuntimeValue(runtime)) + { + ErrorUtilities.ThrowArgument("InvalidTaskHostFactoryParameter", runtime, "Runtime", XMakeAttributes.MSBuildRuntimeValues.clr2, XMakeAttributes.MSBuildRuntimeValues.clr4, XMakeAttributes.MSBuildRuntimeValues.currentRuntime, XMakeAttributes.MSBuildRuntimeValues.any); + } + + if (!XMakeAttributes.IsValidMSBuildArchitectureValue(architecture)) + { + ErrorUtilities.ThrowArgument("InvalidTaskHostFactoryParameter", architecture, "Architecture", XMakeAttributes.MSBuildArchitectureValues.x86, XMakeAttributes.MSBuildArchitectureValues.x64, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, XMakeAttributes.MSBuildArchitectureValues.any); + } + + runtime = XMakeAttributes.GetExplicitMSBuildRuntime(runtime); + architecture = XMakeAttributes.GetExplicitMSBuildArchitecture(architecture); + + IDictionary parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + parameters.Add(XMakeAttributes.runtime, runtime); + parameters.Add(XMakeAttributes.architecture, architecture); + + TaskHostContext desiredContext = CommunicationsUtilities.GetTaskHostContext(parameters); + string taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationFromHostContext(desiredContext); + + if (taskHostLocation != null && FileUtilities.FileExistsNoThrow(taskHostLocation)) + { + return true; + } + + return false; + } + + #region Debug only intrinsics + + /// + /// returns if the string contains escaped wildcards + /// + internal static List __GetListTest() + { + return new List { "A", "B", "C", "D" }; + } + + #endregion + + /// + /// Following function will parse a keyName and returns the basekey for it. + /// It will also store the subkey name in the out parameter. + /// If the keyName is not valid, we will throw ArgumentException. + /// The return value shouldn't be null. + /// Taken from: \ndp\clr\src\BCL\Microsoft\Win32\Registry.cs + /// + private static RegistryKey GetBaseKeyFromKeyName(string keyName, RegistryView view, out string subKeyName) + { + if (keyName == null) + { + throw new ArgumentNullException("keyName"); + } + + string basekeyName; + int i = keyName.IndexOf('\\'); + if (i != -1) + { + basekeyName = keyName.Substring(0, i).ToUpper(System.Globalization.CultureInfo.InvariantCulture); + } + else + { + basekeyName = keyName.ToUpper(System.Globalization.CultureInfo.InvariantCulture); + } + + RegistryKey basekey = null; + + switch (basekeyName) + { + case "HKEY_CURRENT_USER": + basekey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, view); + break; + case "HKEY_LOCAL_MACHINE": + basekey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); + break; + case "HKEY_CLASSES_ROOT": + basekey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, view); + break; + case "HKEY_USERS": + basekey = RegistryKey.OpenBaseKey(RegistryHive.Users, view); + break; + case "HKEY_PERFORMANCE_DATA": + basekey = RegistryKey.OpenBaseKey(RegistryHive.PerformanceData, view); + break; + case "HKEY_CURRENT_CONFIG": + basekey = RegistryKey.OpenBaseKey(RegistryHive.CurrentConfig, view); + break; + case "HKEY_DYN_DATA": + basekey = RegistryKey.OpenBaseKey(RegistryHive.DynData, view); + break; + default: + ErrorUtilities.ThrowArgument(keyName); + break; + } + + if (i == -1 || i == keyName.Length) + { + subKeyName = string.Empty; + } + else + { + subKeyName = keyName.Substring(i + 1, keyName.Length - i - 1); + } + + return basekey; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ItemsAndMetadataPair.cs b/src/XMakeBuildEngine/Evaluation/ItemsAndMetadataPair.cs new file mode 100644 index 00000000000..2803103f1c8 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ItemsAndMetadataPair.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation a class which splits expressions to MSBuild rules. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Wrapper of two tables for a convenient method return value. + /// + internal struct ItemsAndMetadataPair + { + /// + /// The item set + /// + private HashSet _items; + + /// + /// The metadata dictionary. + /// The key is the possibly qualified metadata name, for example + /// "EmbeddedResource.Culture" or "Culture" + /// + private Dictionary _metadata; + + /// + /// Constructs a pair from an item set and metadata + /// + /// The item set + /// The metadata dictionary + internal ItemsAndMetadataPair(HashSet items, Dictionary metadata) + { + _items = items; + _metadata = metadata; + } + + /// + /// Gets or sets the item set + /// + internal HashSet Items + { + get + { + return _items; + } + + set + { + _items = value; + } + } + + /// + /// Gets or sets the metadata dictionary + /// The key is the possibly qualified metadata name, for example + /// "EmbeddedResource.Culture" or "Culture" + /// + internal Dictionary Metadata + { + get + { + return _metadata; + } + + set + { + _metadata = value; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/MetadataReference.cs b/src/XMakeBuildEngine/Evaluation/MetadataReference.cs new file mode 100644 index 00000000000..7525e6c61ff --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/MetadataReference.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// This struct represents a reference to a piece of item metadata. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Evaluation +{ + /// + /// This struct represents a reference to a piece of item metadata. For example, + /// %(EmbeddedResource.Culture) or %(Culture) in the project file. In this case, + /// "EmbeddedResource" is the item name, and "Culture" is the metadata name. + /// The item name is optional. + /// + internal struct MetadataReference + { + /// + /// The item name + /// + internal string ItemName; // Could be null if the %(...) is not qualified with an item name. + + /// + /// The metadata name + /// + internal string MetadataName; + + /// + /// Constructor + /// + /// Name of the item + /// Name of the metadata + internal MetadataReference + ( + string itemName, + string metadataName + ) + { + this.ItemName = itemName; + this.MetadataName = metadataName; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/Preprocessor.cs b/src/XMakeBuildEngine/Evaluation/Preprocessor.cs new file mode 100644 index 00000000000..998698306cc --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/Preprocessor.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Helper to create a "preprocessed" or "logical" view of an evaluated Project. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Creates a view of an evaluated project's XML as if it had all been loaded from + /// a single file, instead of being assembled by pulling in imported files as it actually was. + /// + /// + /// Ideally the result would be buildable on its own, and *usually* this should be the case. + /// Known cases where it wouldn't be buildable: + /// -- $(MSBuildThisFile) and similar properties aren't corrected + /// -- relative path in exists(..) conditions is relative to the imported file + /// -- same for AssemblyFile on UsingTask + /// Paths in item includes are relative to the importing project, though. + /// + internal class Preprocessor + { + /// + /// Project to preprocess + /// + private Project _project; + + /// + /// Table to resolve import tags + /// + private Dictionary> _importTable; + + /// + /// Stack of file paths pushed as we follow imports + /// + private Stack _filePaths = new Stack(); + + /// + /// Constructor + /// + private Preprocessor(Project project) + { + _project = project; + + IList imports = project.Imports; + + _importTable = new Dictionary>(imports.Count); + + foreach (ResolvedImport entry in imports) + { + IList list; + if (!_importTable.TryGetValue(entry.ImportingElement.XmlElement, out list)) + { + list = new List(); + _importTable[entry.ImportingElement.XmlElement] = list; + } + + list.Add(entry.ImportedProject); + } + } + + /// + /// Returns an XmlDocument representing the evaluated project's XML as if it all had + /// been loaded from a single file, instead of being assembled by pulling in imported files. + /// + internal static XmlDocument GetPreprocessedDocument(Project project) + { + Preprocessor preprocessor = new Preprocessor(project); + + XmlDocument result = preprocessor.Preprocess(); + + return result; + } + + /// + /// Root of the preprocessing. + /// + private XmlDocument Preprocess() + { + XmlDocument outerDocument = _project.Xml.XmlDocument; + + XmlDocument destinationDocument = (XmlDocument)outerDocument.CloneNode(false /* shallow */); + + _filePaths.Push(_project.FullPath); + + if (!String.IsNullOrEmpty(_project.FullPath)) // Ignore in-memory projects + { + destinationDocument.AppendChild(destinationDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n" + _project.FullPath + "\r\n" + new String('=', 140) + "\r\n")); + } + + CloneChildrenResolvingImports(outerDocument, destinationDocument); + + return destinationDocument; + } + + /// + /// Recursively called method that clones source nodes into nodes in the destination + /// document. + /// + private void CloneChildrenResolvingImports(XmlNode source, XmlNode destination) + { + XmlDocument sourceDocument = source.OwnerDocument ?? (XmlDocument)source; + XmlDocument destinationDocument = destination.OwnerDocument ?? (XmlDocument)destination; + + foreach (XmlNode child in source.ChildNodes) + { + // Only one of and we got it automatically already + if (child.NodeType == XmlNodeType.XmlDeclaration) + { + continue; + } + + // If this is not the first tag + if ( + child.NodeType == XmlNodeType.Element && + sourceDocument.DocumentElement == child && // This is the root element, not some random element named 'Project' + destinationDocument.DocumentElement != null && // Skip tag from the outer project + String.Equals(XMakeElements.project, child.Name, StringComparison.Ordinal) + ) + { + // But suffix any InitialTargets attribute + string outerInitialTargets = destinationDocument.DocumentElement.GetAttribute(XMakeAttributes.initialTargets).Trim(); + string innerInitialTargets = ((XmlElement)child).GetAttribute(XMakeAttributes.initialTargets).Trim(); + + if (innerInitialTargets.Length > 0) + { + if (outerInitialTargets.Length > 0) + { + outerInitialTargets += ";"; + } + + destinationDocument.DocumentElement.SetAttribute(XMakeAttributes.initialTargets, outerInitialTargets + innerInitialTargets); + } + + // Also gather any DefaultTargets value if none has been encountered already; put it on the outer tag + string outerDefaultTargets = destinationDocument.DocumentElement.GetAttribute(XMakeAttributes.defaultTargets).Trim(); + + if (outerDefaultTargets.Length == 0) + { + string innerDefaultTargets = ((XmlElement)child).GetAttribute(XMakeAttributes.defaultTargets).Trim(); + + if (innerDefaultTargets.Trim().Length > 0) + { + destinationDocument.DocumentElement.SetAttribute(XMakeAttributes.defaultTargets, innerDefaultTargets); + } + } + + CloneChildrenResolvingImports(child, destination); + continue; + } + + // Resolve to 0-n documents and walk into them + if (child.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.import, child.Name, StringComparison.Ordinal)) + { + // To display what the tag looked like + string importCondition = ((XmlElement)child).GetAttribute(XMakeAttributes.condition); + string importProject = ((XmlElement)child).GetAttribute(XMakeAttributes.project); + + IList resolvedList; + if (!_importTable.TryGetValue(child as XmlElement, out resolvedList)) + { + // Import didn't resolve to anything; just display as a comment and move on + string closedImportTag = " 0) ? " Condition=\"" + importCondition + "\"" : String.Empty) + " />"; + destination.AppendChild(destinationDocument.CreateComment(closedImportTag)); + + continue; + } + + for (int i = 0; i < resolvedList.Count; i++) + { + ProjectRootElement resolved = resolvedList[i]; + XmlDocument innerDocument = resolved.XmlDocument; + + string importTag = " 0) ? " Condition=\"" + importCondition + "\"" : String.Empty) + ">"; + destination.AppendChild(destination.OwnerDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n" + importTag + "\r\n\r\n" + resolved.FullPath + "\r\n" + new String('=', 140) + "\r\n")); + + _filePaths.Push(resolved.FullPath); + CloneChildrenResolvingImports(innerDocument, destination); + _filePaths.Pop(); + + if (i < resolvedList.Count - 1) + { + destination.AppendChild(destination.OwnerDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n \r\n" + new String('=', 140) + "\r\n")); + } + else + { + destination.AppendChild(destination.OwnerDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n \r\n\r\n" + _filePaths.Peek() + "\r\n" + new String('=', 140) + "\r\n")); + } + } + + continue; + } + + // Skip over into its children + if (child.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.importGroup, child.Name, StringComparison.Ordinal)) + { + // To display what the tag looked like + string importGroupCondition = ((XmlElement)child).GetAttribute(XMakeAttributes.condition); + string importGroupTag = " 0) ? " Condition=\"" + importGroupCondition + "\"" : String.Empty) + ">"; + destination.AppendChild(destinationDocument.CreateComment(importGroupTag)); + + CloneChildrenResolvingImports(child, destination); + + destination.AppendChild(destinationDocument.CreateComment("")); + + continue; + } + + // Node doesn't need special treatment, clone and append + XmlNode clone = destinationDocument.ImportNode(child, false /* shallow */); // ImportNode does a clone but unlike CloneNode it works across XmlDocuments + + destination.AppendChild(clone); + + CloneChildrenResolvingImports(child, clone); + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Evaluation/ProjectChangedEventArgs.cs b/src/XMakeBuildEngine/Evaluation/ProjectChangedEventArgs.cs new file mode 100644 index 00000000000..9d5b5a8d932 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ProjectChangedEventArgs.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectCollectionDirtiedEventArgs class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Event arguments for the event. + /// + public class ProjectChangedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The changed project. + internal ProjectChangedEventArgs(Project project) + { + ErrorUtilities.VerifyThrowArgumentNull(project, "project"); + + Project = project; + } + + /// + /// Gets the project that was marked dirty. + /// + /// Never null. + public Project Project { get; private set; } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ProjectParser.cs b/src/XMakeBuildEngine/Evaluation/ProjectParser.cs new file mode 100644 index 00000000000..a8a62e1d023 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ProjectParser.cs @@ -0,0 +1,846 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Parses a project from raw XML into strongly typed objects +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using System; +using System.Xml; +using System.Collections.Generic; +using System.Globalization; +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; + +#if MSBUILDENABLEVSPROFILING +using Microsoft.VisualStudio.Profiler; +#endif +#endif +using Expander = Microsoft.Build.Evaluation.Expander; +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace Microsoft.Build.Construction +{ + /// + /// Parses a project from raw XML into strongly typed objects + /// + internal class ProjectParser + { + /// + /// Maximum nesting level of Choose elements. No reasonable project needs more than this + /// + internal const int MaximumChooseNesting = 50; + + /// + /// Valid attribute list when only Condition and Label are valid + /// + private readonly static string[] s_validAttributesOnlyConditionAndLabel = new string[] { XMakeAttributes.condition, XMakeAttributes.label }; + + /// + /// Valid attribute list for item + /// + private readonly static string[] s_validAttributesOnItem = new string[] { XMakeAttributes.condition, XMakeAttributes.label, XMakeAttributes.include, XMakeAttributes.exclude, XMakeAttributes.remove, XMakeAttributes.keepMetadata, XMakeAttributes.removeMetadata, XMakeAttributes.keepDuplicates }; + + /// + /// Valid attributes on import element + /// + private readonly static string[] s_validAttributesOnImport = new string[] { XMakeAttributes.condition, XMakeAttributes.label, XMakeAttributes.project }; + + /// + /// Valid attributes on usingtask element + /// + private readonly static string[] s_validAttributesOnUsingTask = new string[] { XMakeAttributes.condition, XMakeAttributes.label, XMakeAttributes.taskName, XMakeAttributes.assemblyFile, XMakeAttributes.assemblyName, XMakeAttributes.taskFactory, XMakeAttributes.architecture, XMakeAttributes.runtime, XMakeAttributes.requiredPlatform, XMakeAttributes.requiredRuntime }; + + /// + /// Valid attributes on target element + /// + private readonly static string[] s_validAttributesOnTarget = new string[] { XMakeAttributes.condition, XMakeAttributes.label, XMakeAttributes.name, XMakeAttributes.inputs, XMakeAttributes.outputs, XMakeAttributes.keepDuplicateOutputs, XMakeAttributes.dependsOnTargets, XMakeAttributes.beforeTargets, XMakeAttributes.afterTargets, XMakeAttributes.returns }; + + /// + /// Valid attributes on on error element + /// + private readonly static string[] s_validAttributesOnOnError = new string[] { XMakeAttributes.condition, XMakeAttributes.label, XMakeAttributes.executeTargets }; + + /// + /// Valid attributes on output element + /// + private readonly static string[] s_validAttributesOnOutput = new string[] { XMakeAttributes.condition, XMakeAttributes.label, XMakeAttributes.taskParameter, XMakeAttributes.itemName, XMakeAttributes.propertyName }; + + /// + /// Valid attributes on UsingTaskParameter element + /// + private readonly static string[] s_validAttributesOnUsingTaskParameter = new string[] { XMakeAttributes.parameterType, XMakeAttributes.output, XMakeAttributes.required }; + + /// + /// Valid attributes on UsingTaskTask element + /// + private readonly static string[] s_validAttributesOnUsingTaskBody = new string[] { XMakeAttributes.evaluate }; + + /// + /// The ProjectRootElement to parse into + /// + private ProjectRootElement _project; + + /// + /// The document to parse from + /// + private XmlDocumentWithLocation _document; + + /// + /// Whether a ProjectExtensions node has been encountered already. + /// It's not supposed to appear more than once. + /// + private bool _seenProjectExtensions; + + /// + /// Private constructor to give static semantics + /// + private ProjectParser(XmlDocumentWithLocation document, ProjectRootElement project) + { + ErrorUtilities.VerifyThrowInternalNull(project, "project"); + ErrorUtilities.VerifyThrowInternalNull(document, "document"); + + _document = document; + _project = project; + } + + /// + /// Parses the document into the provided ProjectRootElement. + /// Throws InvalidProjectFileExceptions for syntax errors. + /// + /// + /// The code markers here used to be around the Project class constructor in the old code. + /// In the new code, that's not very interesting; we are repurposing to wrap parsing the XML. + /// + internal static void Parse(XmlDocumentWithLocation document, ProjectRootElement projectRootElement) + { +#if MSBUILDENABLEVSPROFILING + try + { + string projectFile = String.IsNullOrEmpty(projectRootElement.ProjectFileLocation.File) ? "(null)" : projectRootElement.ProjectFileLocation.File; + string projectParseBegin = String.Format(CultureInfo.CurrentCulture, "Parse Project {0} - Begin", projectFile); + DataCollection.CommentMarkProfile(8808, projectParseBegin); +#endif +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectConstructBegin, CodeMarkerEvent.perfMSBuildProjectConstructEnd)) +#endif + { + ProjectParser parser = new ProjectParser(document, projectRootElement); + parser.Parse(); + } +#if MSBUILDENABLEVSPROFILING + } + finally + { + string projectFile = String.IsNullOrEmpty(projectRootElement.ProjectFileLocation.File) ? "(null)" : projectRootElement.ProjectFileLocation.File; + string projectParseEnd = String.Format(CultureInfo.CurrentCulture, "Parse Project {0} - End", projectFile); + DataCollection.CommentMarkProfile(8809, projectParseEnd); + } +#endif + } + + /// + /// Parses the project into the ProjectRootElement + /// + private void Parse() + { + // XML guarantees exactly one root element + XmlElementWithLocation element = null; + foreach (XmlNode childNode in _document.ChildNodes) + { + if (childNode.NodeType == XmlNodeType.Element) + { + element = (XmlElementWithLocation)childNode; + break; + } + } + + ProjectErrorUtilities.VerifyThrowInvalidProject(element != null, ElementLocation.Create(_document.FullPath), "NoRootProjectElement", XMakeElements.project); + ProjectErrorUtilities.VerifyThrowInvalidProject(element.Name != XMakeElements.visualStudioProject, element.Location, "ProjectUpgradeNeeded", _project.FullPath); + ProjectErrorUtilities.VerifyThrowInvalidProject(element.LocalName == XMakeElements.project, element.Location, "UnrecognizedElement", element.Name); + + if (element.Prefix.Length > 0 || !String.Equals(element.NamespaceURI, XMakeAttributes.defaultXmlNamespace, StringComparison.OrdinalIgnoreCase)) + { + ProjectErrorUtilities.ThrowInvalidProject(element.Location, "ProjectMustBeInMSBuildXmlNamespace", XMakeAttributes.defaultXmlNamespace); + } + + ParseProjectElement(element); + } + + /// + /// Parse a ProjectRootElement from an element + /// + private void ParseProjectElement(XmlElementWithLocation element) + { + // Historically, we allow any attribute on the Project element + + // The element wasn't available to the ProjectRootElement constructor, + // so we have to set it now + _project.SetProjectRootElementFromParser(element, _project); + + ParseProjectRootElementChildren(element); + } + + /// + /// Parse the child of a ProjectRootElement + /// + private void ParseProjectRootElementChildren(XmlElementWithLocation element) + { + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectElement child = null; + + switch (childElement.Name) + { + case XMakeElements.propertyGroup: + child = ParseProjectPropertyGroupElement(childElement, _project); + break; + + case XMakeElements.itemGroup: + child = ParseProjectItemGroupElement(childElement, _project); + break; + + case XMakeElements.importGroup: + child = ParseProjectImportGroupElement(childElement, _project); + break; + + case XMakeElements.import: + child = ParseProjectImportElement(childElement, _project); + break; + + case XMakeElements.usingTask: + child = ParseProjectUsingTaskElement(childElement); + break; + + case XMakeElements.target: + child = ParseProjectTargetElement(childElement); + break; + + case XMakeElements.itemDefinitionGroup: + child = ParseProjectItemDefinitionGroupElement(childElement); + break; + + case XMakeElements.choose: + child = ParseProjectChooseElement(childElement, _project, 0 /* nesting depth */); + break; + + case XMakeElements.projectExtensions: + child = ParseProjectExtensionsElement(childElement); + break; + + // Obsolete + case XMakeElements.error: + case XMakeElements.warning: + case XMakeElements.message: + ProjectErrorUtilities.ThrowInvalidProject(childElement.Location, "ErrorWarningMessageNotSupported", childElement.Name); + break; + + default: + ProjectXmlUtilities.ThrowProjectInvalidChildElement(childElement.Name, childElement.ParentNode.Name, childElement.Location); + break; + } + + _project.AppendParentedChildNoChecks(child); + } + } + + /// + /// Parse a ProjectPropertyGroupElement from the element + /// + private ProjectPropertyGroupElement ParseProjectPropertyGroupElement(XmlElementWithLocation element, ProjectElementContainer parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + ProjectPropertyGroupElement propertyGroup = new ProjectPropertyGroupElement(element, parent, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectPropertyElement property = ParseProjectPropertyElement(childElement, propertyGroup); + + propertyGroup.AppendParentedChildNoChecks(property); + } + + return propertyGroup; + } + + /// + /// Parse a ProjectPropertyElement from the element + /// + private ProjectPropertyElement ParseProjectPropertyElement(XmlElementWithLocation element, ProjectPropertyGroupElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + XmlUtilities.VerifyThrowProjectValidElementName(element); + ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[element.Name] == null && !ReservedPropertyNames.IsReservedProperty(element.Name), element.Location, "CannotModifyReservedProperty", element.Name); + + // All children inside a property are ignored, since they are only part of its value + return new ProjectPropertyElement(element, parent, _project); + } + + /// + /// Parse a ProjectItemGroupElement + /// + private ProjectItemGroupElement ParseProjectItemGroupElement(XmlElementWithLocation element, ProjectElementContainer parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + ProjectItemGroupElement itemGroup = new ProjectItemGroupElement(element, parent, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectItemElement item = ParseProjectItemElement(childElement, itemGroup); + + itemGroup.AppendParentedChildNoChecks(item); + } + + return itemGroup; + } + + /// + /// Parse a ProjectItemElement + /// + private ProjectItemElement ParseProjectItemElement(XmlElementWithLocation element, ProjectItemGroupElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnItem); + + bool belowTarget = parent.Parent is ProjectTargetElement; + + string itemType = element.Name; + string include = element.GetAttribute(XMakeAttributes.include); + string exclude = element.GetAttribute(XMakeAttributes.exclude); + string remove = element.GetAttribute(XMakeAttributes.remove); + + // Remove must be missing, unless inside a target and Include is missing + ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute((remove.Length == 0 || (belowTarget && include.Length == 0)), (XmlAttributeWithLocation)element.Attributes[XMakeAttributes.remove]); + + // Include must be present, unless inside a target + ProjectErrorUtilities.VerifyThrowInvalidProject(include.Length > 0 || belowTarget, element.Location, "MissingRequiredAttribute", XMakeAttributes.include, itemType); + + // Exclude must be missing, unless Include exists + ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute(exclude.Length == 0 || include.Length > 0, (XmlAttributeWithLocation)element.Attributes[XMakeAttributes.exclude]); + + // If we have an Include attribute at all, it must have non-zero length + ProjectErrorUtilities.VerifyThrowInvalidProject(include.Length > 0 || element.Attributes[XMakeAttributes.include] == null, element.Location, "MissingRequiredAttribute", XMakeAttributes.include, itemType); + + XmlUtilities.VerifyThrowProjectValidElementName(element); + ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[itemType] == null, element.Location, "CannotModifyReservedItem", itemType); + + ProjectItemElement item = new ProjectItemElement(element, parent, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectMetadataElement metadatum = ParseProjectMetadataElement(childElement, item); + + item.AppendParentedChildNoChecks(metadatum); + } + + return item; + } + + /// + /// Parse a ProjectMetadataElement + /// + private ProjectMetadataElement ParseProjectMetadataElement(XmlElementWithLocation element, ProjectElementContainer parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + XmlUtilities.VerifyThrowProjectValidElementName(element); + + ProjectErrorUtilities.VerifyThrowInvalidProject(!(parent is ProjectItemElement) || ((ProjectItemElement)parent).Remove.Length == 0, element.Location, "ChildElementsBelowRemoveNotAllowed", element.Name); + ProjectErrorUtilities.VerifyThrowInvalidProject(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(element.Name), element.Location, "ItemSpecModifierCannotBeCustomMetadata", element.Name); + ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[element.Name] == null, element.Location, "CannotModifyReservedItemMetadata", element.Name); + + ProjectMetadataElement metadatum = new ProjectMetadataElement(element, parent, _project); + + // If the parent is an item definition, we don't allow expressions like @(foo) in the value, as no items exist at that point + if (parent is ProjectItemDefinitionElement) + { + bool containsItemVector = Expander.ExpressionContainsItemVector(metadatum.Value); + ProjectErrorUtilities.VerifyThrowInvalidProject(!containsItemVector, element.Location, "MetadataDefinitionCannotContainItemVectorExpression", metadatum.Value, metadatum.Name); + } + + return metadatum; + } + + /// + /// Parse a ProjectImportGroupElement + /// + /// The XML element to parse + /// A ProjectImportGroupElement derived from the XML element passed in + private ProjectImportGroupElement ParseProjectImportGroupElement(XmlElementWithLocation element, ProjectRootElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + ProjectImportGroupElement importGroup = new ProjectImportGroupElement(element, parent, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + childElement.Name == XMakeElements.import, + childElement.Location, + "UnrecognizedChildElement", + childElement.Name, + element.Name + ); + + ProjectImportElement item = ParseProjectImportElement(childElement, importGroup); + + importGroup.AppendParentedChildNoChecks(item); + } + + return importGroup; + } + + /// + /// Parse a ProjectImportElement that is contained in an ImportGroup + /// + private ProjectImportElement ParseProjectImportElement(XmlElementWithLocation element, ProjectElementContainer parent) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + parent is ProjectRootElement || parent is ProjectImportGroupElement, + element.Location, + "UnrecognizedParentElement", + parent, + element + ); + + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnImport); + + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(element, XMakeAttributes.project); + + ProjectXmlUtilities.VerifyThrowProjectNoChildElements(element); + + return new ProjectImportElement(element, parent, _project); + } + + /// + /// Parse a UsingTaskParameterGroupElement from the element + /// + private UsingTaskParameterGroupElement ParseUsingTaskParameterGroupElement(XmlElementWithLocation element, ProjectElementContainer parent) + { + // There should be no attributes + ProjectXmlUtilities.VerifyThrowProjectNoAttributes(element); + + UsingTaskParameterGroupElement parameterGroup = new UsingTaskParameterGroupElement(element, parent, _project); + + HashSet listOfChildElementNames = new HashSet(); + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + // The parameter already exists this means there is a duplicate child item. Throw an exception. + if (listOfChildElementNames.Contains(childElement.Name)) + { + ProjectXmlUtilities.ThrowProjectInvalidChildElementDueToDuplicate(childElement); + } + else + { + ProjectUsingTaskParameterElement parameter = ParseUsingTaskParameterElement(childElement, parameterGroup); + parameterGroup.AppendParentedChildNoChecks(parameter); + + // Add the name of the child element to the hashset so we can check for a duplicate child element + listOfChildElementNames.Add(childElement.Name); + } + } + + return parameterGroup; + } + + /// + /// Parse a UsingTaskBodyElement from the element + /// + private ProjectUsingTaskBodyElement ParseUsingTaskBodyElement(XmlElementWithLocation element, ProjectUsingTaskElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnUsingTaskBody); + XmlUtilities.VerifyThrowProjectValidElementName(element); + return new ProjectUsingTaskBodyElement(element, parent, _project); + } + + /// + /// Parse a UsingTaskParameterElement from the element + /// + private ProjectUsingTaskParameterElement ParseUsingTaskParameterElement(XmlElementWithLocation element, UsingTaskParameterGroupElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnUsingTaskParameter); + XmlUtilities.VerifyThrowProjectValidElementName(element); + return new ProjectUsingTaskParameterElement(element, parent, _project); + } + + /// + /// Parse a ProjectUsingTaskElement + /// + private ProjectUsingTaskElement ParseProjectUsingTaskElement(XmlElementWithLocation element) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnUsingTask); + ProjectErrorUtilities.VerifyThrowInvalidProject(element.GetAttribute(XMakeAttributes.taskName).Length > 0, element.Location, "ProjectTaskNameEmpty"); + + string assemblyName = element.GetAttribute(XMakeAttributes.assemblyName); + string assemblyFile = element.GetAttribute(XMakeAttributes.assemblyFile); + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + ((assemblyName.Length > 0) ^ (assemblyFile.Length > 0)), + element.Location, + "UsingTaskAssemblySpecification", + XMakeElements.usingTask, + XMakeAttributes.assemblyName, + XMakeAttributes.assemblyFile + ); + + ProjectXmlUtilities.VerifyThrowProjectAttributeEitherMissingOrNotEmpty(element, XMakeAttributes.assemblyName); + ProjectXmlUtilities.VerifyThrowProjectAttributeEitherMissingOrNotEmpty(element, XMakeAttributes.assemblyFile); + + ProjectUsingTaskElement usingTask = new ProjectUsingTaskElement(element, _project, _project); + + bool foundTaskElement = false; + bool foundParameterGroup = false; + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectElement child = null; + string childElementName = childElement.Name; + switch (childElementName) + { + case XMakeElements.usingTaskParameterGroup: + if (foundParameterGroup) + { + ProjectXmlUtilities.ThrowProjectInvalidChildElementDueToDuplicate(childElement); + } + + child = ParseUsingTaskParameterGroupElement(childElement, usingTask); + foundParameterGroup = true; + break; + case XMakeElements.usingTaskBody: + if (foundTaskElement) + { + ProjectXmlUtilities.ThrowProjectInvalidChildElementDueToDuplicate(childElement); + } + + child = ParseUsingTaskBodyElement(childElement, usingTask); + foundTaskElement = true; + break; + default: + ProjectXmlUtilities.ThrowProjectInvalidChildElement(childElement.Name, element.Name, element.Location); + break; + } + + usingTask.AppendParentedChildNoChecks(child); + } + + return usingTask; + } + + /// + /// Parse a ProjectTargetElement + /// + private ProjectTargetElement ParseProjectTargetElement(XmlElementWithLocation element) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnTarget); + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(element, XMakeAttributes.name); + + string targetName = ProjectXmlUtilities.GetAttributeValue(element, XMakeAttributes.name); + + // Orcas compat: all target names are automatically unescaped + targetName = EscapingUtilities.UnescapeAll(targetName); + + int indexOfSpecialCharacter = targetName.IndexOfAny(XMakeElements.illegalTargetNameCharacters); + if (indexOfSpecialCharacter >= 0) + { + ProjectErrorUtilities.ThrowInvalidProject(element.GetAttributeLocation(XMakeAttributes.name), "NameInvalid", targetName, targetName[indexOfSpecialCharacter]); + } + + ProjectTargetElement target = new ProjectTargetElement(element, _project, _project); + ProjectOnErrorElement onError = null; + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectElement child = null; + + switch (childElement.Name) + { + case XMakeElements.propertyGroup: + if (onError != null) + { + ProjectErrorUtilities.ThrowInvalidProject(onError.Location, "NodeMustBeLastUnderElement", XMakeElements.onError, XMakeElements.target, childElement.Name); + } + + child = ParseProjectPropertyGroupElement(childElement, target); + break; + + case XMakeElements.itemGroup: + if (onError != null) + { + ProjectErrorUtilities.ThrowInvalidProject(onError.Location, "NodeMustBeLastUnderElement", XMakeElements.onError, XMakeElements.target, childElement.Name); + } + + child = ParseProjectItemGroupElement(childElement, target); + break; + + case XMakeElements.onError: + onError = ParseProjectOnErrorElement(childElement, target); + child = onError; + break; + + case XMakeElements.itemDefinitionGroup: + ProjectErrorUtilities.ThrowInvalidProject(childElement.Location, "ItemDefinitionGroupNotLegalInsideTarget", childElement.Name); + break; + + default: + if (onError != null) + { + ProjectErrorUtilities.ThrowInvalidProject(onError.Location, "NodeMustBeLastUnderElement", XMakeElements.onError, XMakeElements.target, childElement.Name); + } + + child = ParseProjectTaskElement(childElement, target); + break; + } + + target.AppendParentedChildNoChecks(child); + } + + return target; + } + + /// + /// Parse a ProjectTaskElement + /// + private ProjectTaskElement ParseProjectTaskElement(XmlElementWithLocation element, ProjectTargetElement parent) + { + foreach (XmlAttributeWithLocation attribute in element.Attributes) + { + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + !XMakeAttributes.IsBadlyCasedSpecialTaskAttribute(attribute.Name), + attribute.Location, + "BadlyCasedSpecialTaskAttribute", + attribute.Name, + element.Name, + element.Name + ); + } + + ProjectTaskElement task = new ProjectTaskElement(element, parent, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(childElement.Name == XMakeElements.output, childElement.Location, "UnrecognizedChildElement", childElement.Name, task.Name); + + ProjectOutputElement output = ParseProjectOutputElement(childElement, task); + + task.AppendParentedChildNoChecks(output); + } + + return task; + } + + /// + /// Parse a ProjectOutputElement + /// + private ProjectOutputElement ParseProjectOutputElement(XmlElementWithLocation element, ProjectTaskElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnOutput); + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(element, XMakeAttributes.taskParameter); + ProjectXmlUtilities.VerifyThrowProjectNoChildElements(element); + + string taskParameter = element.GetAttribute(XMakeAttributes.taskParameter); + string itemName = element.GetAttribute(XMakeAttributes.itemName); + string propertyName = element.GetAttribute(XMakeAttributes.propertyName); + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + (itemName.Length > 0 || propertyName.Length > 0) && (itemName.Length == 0 || propertyName.Length == 0), + element.Location, + "InvalidTaskOutputSpecification", + parent.Name + ); + + ProjectXmlUtilities.VerifyThrowProjectAttributeEitherMissingOrNotEmpty(element, XMakeAttributes.itemName); + ProjectXmlUtilities.VerifyThrowProjectAttributeEitherMissingOrNotEmpty(element, XMakeAttributes.propertyName); + + ProjectErrorUtilities.VerifyThrowInvalidProject(!ReservedPropertyNames.IsReservedProperty(propertyName), element.Location, "CannotModifyReservedProperty", propertyName); + + return new ProjectOutputElement(element, parent, _project); + } + + /// + /// Parse a ProjectOnErrorElement + /// + private ProjectOnErrorElement ParseProjectOnErrorElement(XmlElementWithLocation element, ProjectTargetElement parent) + { + // Previous OM accidentally didn't verify ExecuteTargets on parse, + // but we do, as it makes no sense + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnOnError); + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(element, XMakeAttributes.executeTargets); + ProjectXmlUtilities.VerifyThrowProjectNoChildElements(element); + + return new ProjectOnErrorElement(element, parent, _project); + } + + /// + /// Parse a ProjectItemDefinitionGroupElement + /// + private ProjectItemDefinitionGroupElement ParseProjectItemDefinitionGroupElement(XmlElementWithLocation element) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + ProjectItemDefinitionGroupElement itemDefinitionGroup = new ProjectItemDefinitionGroupElement(element, _project, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectItemDefinitionElement itemDefinition = ParseProjectItemDefinitionXml(childElement, itemDefinitionGroup); + + itemDefinitionGroup.AppendParentedChildNoChecks(itemDefinition); + } + + return itemDefinitionGroup; + } + + /// + /// Pasre a ProjectItemDefinitionElement + /// + private ProjectItemDefinitionElement ParseProjectItemDefinitionXml(XmlElementWithLocation element, ProjectItemDefinitionGroupElement parent) + { + ProjectXmlUtilities.VerifyThrowProjectAttributes(element, s_validAttributesOnlyConditionAndLabel); + + // Orcas inadvertently did not check for reserved item types (like "Choose") in item definitions, + // as we do for item types in item groups. So we do not have a check here. + // Although we could perhaps add one, as such item definitions couldn't be used + // since no items can have the reserved itemType. + ProjectItemDefinitionElement itemDefinition = new ProjectItemDefinitionElement(element, parent, _project); + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectMetadataElement metadatum = ParseProjectMetadataElement(childElement, itemDefinition); + + itemDefinition.AppendParentedChildNoChecks(metadatum); + } + + return itemDefinition; + } + + /// + /// Parse a ProjectChooseElement + /// + private ProjectChooseElement ParseProjectChooseElement(XmlElementWithLocation element, ProjectElementContainer parent, int nestingDepth) + { + ProjectXmlUtilities.VerifyThrowProjectNoAttributes(element); + + ProjectChooseElement choose = new ProjectChooseElement(element, parent, _project); + + nestingDepth++; + ProjectErrorUtilities.VerifyThrowInvalidProject(nestingDepth <= MaximumChooseNesting, element.Location, "ChooseOverflow", MaximumChooseNesting); + + bool foundWhen = false; + bool foundOtherwise = false; + + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectElement child = null; + + switch (childElement.Name) + { + case XMakeElements.when: + ProjectErrorUtilities.VerifyThrowInvalidProject(!foundOtherwise, childElement.Location, "WhenNotAllowedAfterOtherwise"); + child = ParseProjectWhenElement(childElement, choose, nestingDepth); + foundWhen = true; + break; + + case XMakeElements.otherwise: + ProjectErrorUtilities.VerifyThrowInvalidProject(!foundOtherwise, childElement.Location, "MultipleOtherwise"); + foundOtherwise = true; + child = ParseProjectOtherwiseElement(childElement, choose, nestingDepth); + break; + + default: + ProjectXmlUtilities.ThrowProjectInvalidChildElement(childElement.Name, element.Name, element.Location); + break; + } + + choose.AppendParentedChildNoChecks(child); + } + + nestingDepth--; + ProjectErrorUtilities.VerifyThrowInvalidProject(foundWhen, element.Location, "ChooseMustContainWhen"); + + return choose; + } + + /// + /// Parse a ProjectWhenElement + /// + private ProjectWhenElement ParseProjectWhenElement(XmlElementWithLocation element, ProjectChooseElement parent, int nestingDepth) + { + ProjectXmlUtilities.VerifyThrowProjectRequiredAttribute(element, XMakeAttributes.condition); + + ProjectWhenElement when = new ProjectWhenElement(element, parent, _project); + + ParseWhenOtherwiseChildren(element, when, nestingDepth); + + return when; + } + + /// + /// Parse a ProjectOtherwiseElement + /// + private ProjectOtherwiseElement ParseProjectOtherwiseElement(XmlElementWithLocation element, ProjectChooseElement parent, int nestingDepth) + { + ProjectXmlUtilities.VerifyThrowProjectNoAttributes(element); + + ProjectOtherwiseElement otherwise = new ProjectOtherwiseElement(element, parent, _project); + + ParseWhenOtherwiseChildren(element, otherwise, nestingDepth); + + return otherwise; + } + + /// + /// Parse the children of a When or Otherwise + /// + private void ParseWhenOtherwiseChildren(XmlElementWithLocation element, ProjectElementContainer parent, int nestingDepth) + { + foreach (XmlElementWithLocation childElement in ProjectXmlUtilities.GetVerifyThrowProjectChildElements(element)) + { + ProjectElement child = null; + + switch (childElement.Name) + { + case XMakeElements.propertyGroup: + child = ParseProjectPropertyGroupElement(childElement, parent); + break; + + case XMakeElements.itemGroup: + child = ParseProjectItemGroupElement(childElement, parent); + break; + + case XMakeElements.choose: + child = ParseProjectChooseElement(childElement, parent, nestingDepth); + break; + + default: + ProjectXmlUtilities.ThrowProjectInvalidChildElement(childElement.Name, element.Name, element.Location); + break; + } + + parent.AppendParentedChildNoChecks(child); + } + } + + /// + /// Parse a ProjectExtensionsElement + /// + private ProjectExtensionsElement ParseProjectExtensionsElement(XmlElementWithLocation element) + { + // ProjectExtensions are only found in the main project file - in fact, the code used to ignore them in imported + // files. We don't. + ProjectXmlUtilities.VerifyThrowProjectNoAttributes(element); + + ProjectErrorUtilities.VerifyThrowInvalidProject(!_seenProjectExtensions, element.Location, "DuplicateProjectExtensions"); + _seenProjectExtensions = true; + + // All children inside ProjectExtensions are ignored, since they are only part of its value + return new ProjectExtensionsElement(element, _project, _project); + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs b/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs new file mode 100644 index 00000000000..ae6e8b4ccaa --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs @@ -0,0 +1,643 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Holds weak references to ProjectRootElement's for design time sharing purposes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; + +using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.BackEnd; + +using OutOfProcNode = Microsoft.Build.Execution.OutOfProcNode; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Maintains a cache of all loaded ProjectRootElement's for design time purposes. + /// Weak references are held to add added ProjectRootElement's. + /// Strong references are held to a limited number of added ProjectRootElement's. + /// + /// 1. Loads of a ProjectRootElement will share any existing loaded ProjectRootElement, rather + /// than loading and parsing a new one. This is the case whether the ProjectRootElement + /// is loaded directly or imported. + /// + /// 2. For design time, only a weak reference needs to be held, because all users have a strong reference. + /// + /// 3. Because all loads of a ProjectRootElement consult this cache, they can be assured that any + /// entries in this cache are up to date. For example, if a ProjectRootElement is modified and saved, + /// the cached ProjectRootElement will be the loaded one that was saved, so it will be up to date. + /// + /// 4. If, after a project has been loaded, an external app changes the project file content on disk, it is + /// important that a subsequent load of that project does not return stale ProjectRootElement. To avoid this, the + /// timestamp of the file on disk is compared to the timestamp of the file at the time that the ProjectRootElement loaded it. + /// + /// 5. For build time, some strong references need to be held, as otherwise the ProjectRootElement's for reuseable + /// imports will be collected, and time will be wasted reparsing them. However we do not want to hold strong references + /// to all ProjectRootElement's, consuming memory without end. So a simple priority queue is used. All Adds and Gets boost their + /// entry to the top. As the queue gets too big, low priority entries are dropped. + /// + /// No guesses are made at which files are more interesting to cache, beyond the most-recently-used list. For example, ".targets" files + /// or imported files are not treated specially, as this is a potentially unreliable heuristic. Besides, caching a project file itself could + /// be useful, if for example you want to build it twice with different sets of properties. + /// + /// Because of the strongly typed list, some ProjectRootElement's will be held onto indefinitely. This is an acceptable price to pay for + /// being able to provide a commonly used ProjectRootElement immediately it's needed. It is mitigated by the list being finite and small, and + /// because we allow ProjectCollection.UnloadAllProjects to hint to us to clear the list. + /// + /// Implicit references are those which were loaded as a result of a build, and not explicitly loaded through, for instance, the project + /// collection. + /// + /// + internal class ProjectRootElementCache + { + /// + /// The maximum number of entries to keep strong references to. + /// This has to be strong enough to make sure that key .targets files aren't pushed + /// off by transient loads of non-reusable files like .user files. + /// + /// Made this as large as 50 because VC has a large number of + /// regularly used property sheets and other imports. + /// If you change this, update the unit tests. + /// + /// + /// If this number is increased much higher, the datastructure may + /// need to be changed from a linked list, since it's currently O(n). + /// + private static readonly int s_maximumStrongCacheSize = 50; + + /// + /// Whether the cache should log activity to the Debug.Out stream + /// + private static bool s_debugLogCacheActivity; + + /// + /// The map of weakly-held ProjectRootElement's + /// + /// + /// Be sure that the string keys are strongly held, or unpredictable bad + /// behavior will ensue. + /// + private WeakValueDictionary _weakCache; + + /// + /// The list of strongly-held ProjectRootElement's + /// + private LinkedList _strongCache; + + /// + /// Whether the cache should check the timestamp of the file on disk + /// whenever it is requested, and update with the latest content of that + /// file if it has changed. + /// + private bool _autoReloadFromDisk; + + /// + /// Locking object for this shared cache + /// + private Object _locker = new Object(); + + /// + /// Static constructor to choose cache size. + /// + static ProjectRootElementCache() + { + // Configurable in case a customer has related perf problems after shipping and so that + // we can measure different values for perf easily. + string userSpecifiedSize = Environment.GetEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE"); + if (!String.IsNullOrEmpty(userSpecifiedSize)) + { + // Not catching as this is an undocumented setting + s_maximumStrongCacheSize = Convert.ToInt32(userSpecifiedSize, NumberFormatInfo.InvariantInfo); + } + + s_debugLogCacheActivity = Environment.GetEnvironmentVariable("MSBUILDDEBUGXMLCACHE") == "1"; + } + + /// + /// Creates an empty cache. + /// + internal ProjectRootElementCache(bool autoReloadFromDisk) + { + DebugTraceCache("Constructing with autoreload from disk: ", autoReloadFromDisk); + + _weakCache = new WeakValueDictionary(StringComparer.OrdinalIgnoreCase); + _strongCache = new LinkedList(); + _autoReloadFromDisk = autoReloadFromDisk; + } + + /// + /// Handler for which project root element just got added to the cache + /// + internal delegate void ProjectRootElementCacheAddEntryHandler(object sender, ProjectRootElementCacheAddEntryEventArgs e); + + /// + /// Delegate for StrongCacheEntryRemoved event + /// + internal delegate void StrongCacheEntryRemovedDelegate(object sender, ProjectRootElement projectRootElement); + + /// + /// Callback to create a ProjectRootElement if need be + /// + internal delegate ProjectRootElement OpenProjectRootElement(string path, ProjectRootElementCache cache); + + /// + /// Event that is fired when an entry in the Strong Cache is removed. + /// + internal static event StrongCacheEntryRemovedDelegate StrongCacheEntryRemoved; + + /// + /// Event which is fired when a project root element is added to this cache. + /// + internal event ProjectRootElementCacheAddEntryHandler ProjectRootElementAddedHandler; + + /// + /// Event which is fired when a project root element in this cache is dirtied. + /// + internal event EventHandler ProjectRootElementDirtied; + + /// + /// Event which is fired when a project is marked dirty. + /// + internal event EventHandler ProjectDirtied; + + /// + /// Returns an existing ProjectRootElement for the specified file path, if any. + /// If none exists, calls the provided delegate to load one, and adds that to the cache. + /// The reason that it calls back to do this is so that the cache is locked between determining + /// that the entry does not exist and adding the entry. + /// + /// If was set to true, and the file on disk has changed since it was cached, + /// it will be reloaded before being returned. + /// + /// Thread safe. + /// + /// + /// Never needs to consult the strong cache as well, since if the item is in there, it will + /// not have left the weak cache. + /// If item is found, boosts it to the top of the strong cache. + /// + /// The project file which contains the ProjectRootElement. Must be a full path. + /// The delegate to use to load if necessary. May be null. + /// The ProjectRootElement instance if one exists. Null otherwise. + internal ProjectRootElement Get(string projectFile, OpenProjectRootElement openProjectRootElement, bool isExplicitlyLoaded) + { + // Should already have been canonicalized + ErrorUtilities.VerifyThrowInternalRooted(projectFile); + + lock (_locker) + { + ProjectRootElement projectRootElement; + _weakCache.TryGetValue(projectFile, out projectRootElement); + + if (projectRootElement != null && _autoReloadFromDisk) + { + FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(projectFile); + + // If the file doesn't exist on disk, go ahead and use the cached version. + // It's an in-memory project that hasn't been saved yet. + if (fileInfo != null) + { + bool forgetEntry = false; + + if (fileInfo.LastWriteTime != projectRootElement.LastWriteTimeWhenRead) + { + // File was changed on disk by external means. Cached version is no longer reliable. + // We could throw here or ignore the problem, but it is a common and reasonable pattern to change a file + // externally and load a new project over it to see the new content. So we dump it from the cache + // to force a load from disk. There might then exist more than one ProjectRootElement with the same path, + // but clients ought not get themselves into such a state - and unless they save them to disk, + // it may not be a problem. + forgetEntry = true; + } + else if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDCACHECHECKFILECONTENT"))) + { + // QA tests run too fast for the timestamp check to work. This environment variable is for their + // use: it checks the file content as well as the timestamp. That's better than completely disabling + // the cache as we get test coverage of the rest of the cache code. + XmlDocument document = new XmlDocument(); + using (XmlTextReader xtr = new XmlTextReader(projectRootElement.FullPath)) + { + xtr.DtdProcessing = DtdProcessing.Ignore; + document.Load(xtr); + } + + string diskContent = document.OuterXml; + string cacheContent = projectRootElement.XmlDocument.OuterXml; + + if (diskContent != cacheContent) + { + forgetEntry = true; + } + } + + if (forgetEntry) + { + ForgetEntry(projectRootElement); + + DebugTraceCache("Out of date dropped from XML cache: ", projectFile); + projectRootElement = null; + } + } + } + + if (projectRootElement == null && openProjectRootElement != null) + { + projectRootElement = openProjectRootElement(projectFile, this); + + ErrorUtilities.VerifyThrowInternalNull(projectRootElement, "projectRootElement"); + ErrorUtilities.VerifyThrow(projectRootElement.FullPath == projectFile, "Got project back with incorrect path"); + ErrorUtilities.VerifyThrow(_weakCache.Contains(projectFile), "Open should have renamed into cache and boosted"); + } + else if (projectRootElement != null) + { + DebugTraceCache("Satisfied from XML cache: ", projectFile); + BoostEntryInStrongCache(projectRootElement); + } + + // An implicit load will never reset the explicit flag. + if (projectRootElement != null && isExplicitlyLoaded) + { + projectRootElement.MarkAsExplicitlyLoaded(); + } + + return projectRootElement; + } + } + + /// + /// Add an entry to the cache. + /// + internal void AddEntry(ProjectRootElement projectRootElement) + { + lock (_locker) + { + RenameEntryInternal(null, projectRootElement); + + RaiseProjectRootElementAddedToCacheEvent(projectRootElement); + } + } + + /// + /// Raises the event. + /// + /// The dirtied project root element. + /// Details on the PRE and the nature of the change. + internal void OnProjectRootElementDirtied(ProjectRootElement sender, ProjectXmlChangedEventArgs e) + { + var cacheDirtied = this.ProjectRootElementDirtied; + if (cacheDirtied != null) + { + cacheDirtied(sender, e); + } + } + + /// + /// Raises the event. + /// + /// The dirtied project. + /// Details on the Project and the change. + internal void OnProjectDirtied(Project sender, ProjectChangedEventArgs e) + { + var projectDirtied = this.ProjectDirtied; + if (projectDirtied != null) + { + projectDirtied(sender, e); + } + } + + /// + /// Rename an entry in the cache. + /// Entry must already be in the cache. + /// + internal void RenameEntry(string oldFullPath, ProjectRootElement projectRootElement) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentLength(oldFullPath, "oldFullPath"); + RenameEntryInternal(oldFullPath, projectRootElement); + } + } + + /// + /// Returns any a ProjectRootElement in the cache with the provided full path, + /// otherwise null. + /// + internal ProjectRootElement TryGet(string projectFile) + { + ProjectRootElement result = Get(projectFile, null /* no delegate to load it */, false /*Since we are not creating a PRE this can be true or false*/); + + return result; + } + + /// + /// Discards strong references held by the cache. + /// + /// + /// The weak cache is never cleared, as we need it to guarantee that the appdomain never + /// has two ProjectRootElement's for a particular file. Attempts to clear out the weak cache + /// resulted in this guarantee being broken and subtle bugs popping up everywhere. + /// + internal void DiscardStrongReferences() + { + lock (_locker) + { + DebugTraceCache("Clearing strong refs: ", _strongCache.Count); + + LinkedList oldStrongCache = _strongCache; + _strongCache = new LinkedList(); + + foreach (ProjectRootElement projectRootElement in oldStrongCache) + { + RaiseProjectRootElementRemovedFromStrongCache(projectRootElement); + } + + // A scavenge of the weak cache is probably not worth it as + // the GC would have had to run immediately after the line above. + } + } + + /// + /// Clears out the cache. + /// Called when all projects are unloaded and possibly when a build is done. + /// + internal void Clear() + { + lock (_locker) + { + LinkedList oldStrongCache = _strongCache; + _weakCache = new WeakValueDictionary(StringComparer.OrdinalIgnoreCase); + _strongCache = new LinkedList(); + + foreach (ProjectRootElement projectRootElement in oldStrongCache) + { + RaiseProjectRootElementRemovedFromStrongCache(projectRootElement); + } + } + } + + /// + /// Discard any entries (weak and strong) which do not have the explicitlyLoaded flag set. + /// + internal void DiscardImplicitReferences() + { + lock (_locker) + { + // Make a new Weak cache only with items that have been explicitly loaded, this will be a small number, there will most likely + // be many items which were not explicitly loaded (ie p2p references). + WeakValueDictionary oldWeakCache = _weakCache; + _weakCache = new WeakValueDictionary(StringComparer.OrdinalIgnoreCase); + + LinkedList oldStrongCache = _strongCache; + _strongCache = new LinkedList(); + + foreach (string projectPath in oldWeakCache.Keys) + { + ProjectRootElement rootElement; + + if (oldWeakCache.TryGetValue(projectPath, out rootElement)) + { + if (rootElement.IsExplicitlyLoaded) + { + _weakCache[projectPath] = rootElement; + } + + if (rootElement.IsExplicitlyLoaded && oldStrongCache.Contains(rootElement)) + { + _strongCache.AddFirst(rootElement); + } + else + { + _strongCache.Remove(rootElement); + RaiseProjectRootElementRemovedFromStrongCache(rootElement); + } + } + } + } + } + + /// + /// Forces a removal of a project root element from the weak cache if it is present. + /// + /// The project root element to remove. + /// + /// No exception is thrown if this project root element is in use by currently loaded projects + /// by this method. The calling method must know that this is a safe operation. + /// There may of course be strong references to the project root element from customer code. + /// The assumption is that when they instruct the project collection to unload it, which + /// leads to this being called, they are releasing their strong references too (or it doesn't matter) + /// + internal void DiscardAnyWeakReference(ProjectRootElement projectRootElement) + { + ErrorUtilities.VerifyThrowArgumentNull(projectRootElement, "projectRootElement"); + + // A PRE may be unnamed if it was only used in memory. + if (projectRootElement.FullPath != null) + { + lock (_locker) + { + _weakCache.Remove(projectRootElement.FullPath); + } + } + } + + /// + /// Raises an event which is raised when a project root element is added to the cache. + /// + private void RaiseProjectRootElementAddedToCacheEvent(ProjectRootElement rootElement) + { + if (ProjectRootElementAddedHandler != null) + { + ProjectRootElementAddedHandler(this, new ProjectRootElementCacheAddEntryEventArgs(rootElement)); + } + } + + /// + /// Raises an event which is raised when a project root element is removed from the strong cache. + /// + private void RaiseProjectRootElementRemovedFromStrongCache(ProjectRootElement projectRootElement) + { + StrongCacheEntryRemovedDelegate removedEvent = StrongCacheEntryRemoved; + if (null != removedEvent) + { + removedEvent(this, projectRootElement); + } + } + + /// + /// Add or rename an entry in the cache. + /// Old full path may be null iff it was not already in the cache. + /// + /// + /// Must be called within the cache lock. + /// + private void RenameEntryInternal(string oldFullPathIfAny, ProjectRootElement projectRootElement) + { + ErrorUtilities.VerifyThrowInternalNull(projectRootElement.FullPath, "FullPath"); + + if (oldFullPathIfAny != null) + { + ErrorUtilities.VerifyThrowInternalRooted(oldFullPathIfAny); + ErrorUtilities.VerifyThrow(_weakCache[oldFullPathIfAny] == projectRootElement, "Should already be present"); + _weakCache.Remove(oldFullPathIfAny); + } + + // There may already be a ProjectRootElement in the cache with the new name. In this case we cannot throw an exception; + // we must merely replace it. This is because it may be an unrooted entry + // (and thus gone from the client's point of view) that merely remains + // in the cache because we still have a reference to it from our strong cache. + // Another possibility is that there are two, unrelated, un-saved, in-memory projects that were given the same path. + // Replacing the cache entry does not in itself cause a problem -- if there are any actual users of the old + // entry they will not be affected. There would then exist more than one ProjectRootElement with the same path, + // but clients ought not get themselves into such a state - and unless they save them to disk, + // it may not be a problem. Replacing also doesn't cause a problem for the strong cache, + // as it is never consulted by us, but it is reasonable for us to remove the old entry in that case. + ProjectRootElement existingWeakEntry; + _weakCache.TryGetValue(projectRootElement.FullPath, out existingWeakEntry); + + if (existingWeakEntry != null && !Object.ReferenceEquals(existingWeakEntry, projectRootElement)) + { + _strongCache.Remove(existingWeakEntry); + RaiseProjectRootElementRemovedFromStrongCache(existingWeakEntry); + } + + DebugTraceCache("Adding: ", projectRootElement.FullPath); + _weakCache[projectRootElement.FullPath] = projectRootElement; + + BoostEntryInStrongCache(projectRootElement); + } + + /// + /// Update the strong cache. + /// If the item is already a member of the list, move it to the top. + /// Otherwise, just add it to the top. + /// If the list is too large, remove an entry from the bottom. + /// + /// + /// Must be called within the cache lock. + /// If the size of strong cache gets large, this needs a faster data structure + /// than a linked list. It's currently O(n). + /// + private void BoostEntryInStrongCache(ProjectRootElement projectRootElement) + { + LinkedListNode node = _strongCache.First; + + while (node != null) + { + if (Object.ReferenceEquals(node.Value, projectRootElement)) + { + // DebugTraceCache("Boosting: ", projectRootElement.FullPath); + _strongCache.Remove(node); + _strongCache.AddFirst(node); + + return; + } + + node = node.Next; + } + + _strongCache.AddFirst(projectRootElement); + + if (_strongCache.Count > s_maximumStrongCacheSize) + { + node = _strongCache.Last; + + DebugTraceCache("Shedding: ", node.Value.FullPath); + _strongCache.Remove(node); + RaiseProjectRootElementRemovedFromStrongCache(node.Value); + } + } + + /// + /// Completely remove an entry from this cache + /// + /// + /// Must be called within the cache lock. + /// + private void ForgetEntry(ProjectRootElement projectRootElement) + { + DebugTraceCache("Forgetting: ", projectRootElement.FullPath); + + _weakCache.Remove(projectRootElement.FullPath); + + LinkedListNode strongCacheEntry = _strongCache.Find(projectRootElement); + if (strongCacheEntry != null) + { + _strongCache.Remove(strongCacheEntry); + RaiseProjectRootElementRemovedFromStrongCache(strongCacheEntry.Value); + } + } + + /// + /// Write debugging messages to the Debug.Out stream. + /// + private void DebugTraceCache(string message, bool param1) + { + if (s_debugLogCacheActivity) + { + DebugTraceCache(message, Convert.ToString(param1, CultureInfo.InvariantCulture)); + } + } + + /// + /// Write debugging messages to the Debug.Out stream. + /// + private void DebugTraceCache(string message, int param1) + { + if (s_debugLogCacheActivity) + { + DebugTraceCache(message, Convert.ToString(param1, CultureInfo.InvariantCulture)); + } + } + + /// + /// Write debugging messages to the Debug.Out stream. + /// + private void DebugTraceCache(string message, string param1) + { + if (s_debugLogCacheActivity) + { + string prefix = OutOfProcNode.IsOutOfProcNode ? "C" : "P"; + Trace.WriteLine(prefix + " " + Process.GetCurrentProcess().Id + " | " + message + param1); + } + } + + /// + /// This class is an event that holds which ProjectRootElement was added to the root element cache. + /// + internal class ProjectRootElementCacheAddEntryEventArgs : EventArgs + { + /// + /// Root element which was just added to the cache. + /// + private ProjectRootElement _rootElement; + + /// + /// Takes the root element which was added to the results cache. + /// + internal ProjectRootElementCacheAddEntryEventArgs(ProjectRootElement element) + { + _rootElement = element; + } + + /// + /// Root element which was just added to the cache. + /// + public ProjectRootElement RootElement + { + get { return _rootElement; } + } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ProjectStringCache.cs b/src/XMakeBuildEngine/Evaluation/ProjectStringCache.cs new file mode 100644 index 00000000000..716dfbb16f8 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ProjectStringCache.cs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Holds references to strings contained in ProjectRootElement in +// order to save memory when duplicate files are loaded. +// +// Most of the code comes from XmlNameTable implementation: +// ndp\fx\src\Xml\System\Xml\NameTable.cs +// We are adding functionality to remove cache entries when xml documents +// are unloaded. +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.Construction +{ + /// + /// This class will cache string values for loaded Xml files. + /// + [DebuggerDisplay("#Strings={Count} #Documents={documents.Count}")] + internal class ProjectStringCache + { + /// + /// Start off with a large size as there are very many strings in common scenarios and resizing is expensive. + /// Note that there is a single instance of this cache for the lifetime of the process (albeit cleared out on XML unload) + /// Australian Govt has about 3000 strings; a single VC project with all its various XML files has about 4000 strings. + /// + private const int InitialSize = 5000; + + /// + /// Store interned strings, and also a ref count, one per document using them. + /// + private RetrievableEntryHashSet _strings = new RetrievableEntryHashSet(InitialSize, OrdinalKeyedComparer.Instance); + + /// + /// Store all the strings a document is using, so their ref count can be decremented. + /// + private Dictionary> _documents = new Dictionary>(); + + /// + /// Locking object for this shared cache + /// + private Object _locker = new Object(); + + /// + /// Public constructor. + /// + public ProjectStringCache() + { + ProjectRootElementCache.StrongCacheEntryRemoved += OnStrongCacheEntryRemoved; + } + + /// + /// Obtain the number of entries contained in the cache. + /// + internal int Count + { + get + { + lock (_locker) + { + return _strings.Count; + } + } + } + + /// + /// Add the given string to the cache or return the existing string if it is already + /// in the cache. + /// Constant time operation. + /// + public string Add(string key, XmlDocument document) + { + if (key.Length == 0) + { + return String.Empty; + } + + lock (_locker) + { + VerifyState(); + + StringCacheEntry entry; + HashSet entries; + + bool seenString = _strings.TryGetValue(key, out entry); + bool seenDocument = _documents.TryGetValue(document, out entries); + + if (!seenString) + { + entry = new StringCacheEntry(key); + _strings.Add(entry); + } + + if (!seenDocument) + { + entries = new HashSet(); + _documents.Add(document, entries); + } + + bool seenStringInThisDocument = seenString && seenDocument && entries.Contains(entry); + + if (!seenStringInThisDocument) + { + entries.Add(entry); + + // We've been referred to by a new document, so increment our ref count. + entry.Increment(); + } + + VerifyState(); + + return entry.CachedString; + } + } + + /// + /// Find the matching string in the cache. + /// Constant time operation. + /// + /// String to find in the cache. + /// Existing string in the cache, or null if it is not contained. + public string Get(string key) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentNull(key, "key"); + + if (key.Length == 0) + { + return String.Empty; + } + + StringCacheEntry entry; + if (_strings.TryGetValue(key, out entry)) + { + return entry.CachedString; + } + + return null; + } + } + + /// + /// Indicates that a document's entries should be removed. + /// If document is unknown, does nothing. + /// Complexity proportional to the number of strings in the document, + /// if the document is anywhere in the cache, otherwise O(1). + /// + public void Clear(XmlDocument document) + { + lock (_locker) + { + ErrorUtilities.VerifyThrowArgumentNull(document, "document"); + + VerifyState(); + + HashSet entries; + if (_documents.TryGetValue(document, out entries)) + { + foreach (var entry in entries) + { + string str = entry.CachedString; + entry.Decrement(); + + if (entry.RefCount == 0) + { + _strings.Remove(str); + } + } + + _documents.Remove(document); + } + + VerifyState(); + } + } + + /// + /// Verifies that each string entry has only one instance in the system. + /// Enable the conditional if and while you make any modifications to the class, then disable as it is very slow. + /// + [Conditional("NEVER")] + private void VerifyState() + { + HashSet uniqueEntries = new HashSet(); + foreach (var entries in _documents.Values) + { + foreach (var entry in entries) + { + uniqueEntries.Add(entry); + ErrorUtilities.VerifyThrow(entry.RefCount > 0, "extra deref"); + + // We only ever create one StringCacheEntry instance per unique string, and that instance should be + // the same in both collections. + ErrorUtilities.VerifyThrow(Object.ReferenceEquals(entry, _strings[entry.CachedString]), "bad state"); + } + } + + ErrorUtilities.VerifyThrow(uniqueEntries.Count == _strings.Count, "bad state"); + } + + /// + /// Handle event that is fired when an entry in the project root element cache is removed + /// from its strong cache. + /// + /// + /// When an entry is removed from a project root element cache's strong cache, we will remove + /// its entries from our string cache. Otherwise the string cache ends up being the only one + /// holding references to the Xml documents that have already been dropped. + /// + private void OnStrongCacheEntryRemoved(object sender, ProjectRootElement projectRootElement) + { + ErrorUtilities.VerifyThrowArgumentNull(projectRootElement, "projectRootElement"); + Clear(projectRootElement.XmlDocument); + } + + /// + /// Represents an entry in the ProjectStringCache. + /// Can't be a struct because the copy-by-value and the ref counting don't go well together. + /// + [DebuggerDisplay("Count={refCount} String={cachedString}")] + private class StringCacheEntry : IKeyed + { + /// + /// Cached string + /// + private string _cachedString; + + /// + /// Number of XmlDocuments where this string is included. + /// + private int _refCount; + + /// + /// Constructor. + /// Caller must then do Increment(). + /// + internal StringCacheEntry(string str) + { + _cachedString = str; + _refCount = 0; + } + + /// + /// Key to find it + /// + public string Key + { + get { return _cachedString; } + } + + /// + /// Number of documents using this string + /// + internal int RefCount + { + get { return _refCount; } + } + + /// + /// Get the cached string. + /// + internal string CachedString + { + get + { + ErrorUtilities.VerifyThrow(_refCount > 0, "extra deref"); + return _cachedString; + } + } + + /// + /// Indicates that this entry is included in the given document. + /// Callers must verify that we were not already adreffed for this document. + /// + internal void Increment() + { + _refCount++; + } + + /// + /// Removes a container for this entry. + /// Callers must verify that this was not already reffed and not subsequently dereffed. + /// + internal void Decrement() + { + ErrorUtilities.VerifyThrow(_refCount > 0, "extra deref"); + _refCount--; + } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ProjectXmlChangedEventArgs.cs b/src/XMakeBuildEngine/Evaluation/ProjectXmlChangedEventArgs.cs new file mode 100644 index 00000000000..0176aa334b3 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ProjectXmlChangedEventArgs.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectCollectionDirtiedEventArgs class. +//----------------------------------------------------------------------- + +using System; +using System.Globalization; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Event arguments for the event. + /// + public class ProjectXmlChangedEventArgs : EventArgs + { + /// + /// The unformatted reason for dirtying the project collection. + /// + private readonly string _unformattedReason; + + /// + /// The formatting parameter. + /// + private readonly string _formattingParameter; + + /// + /// Initializes a new instance of the class + /// that represents a change to a specific project root element. + /// + /// The ProjectRootElement whose content was actually changed. + /// The unformatted (may contain {0}) reason for the dirty event. + /// The formatting parameter to use with . + internal ProjectXmlChangedEventArgs(ProjectRootElement projectXml, string unformattedReason, string formattingParameter) + { + ErrorUtilities.VerifyThrowArgumentNull(projectXml, "projectXml"); + + this.ProjectXml = projectXml; + _unformattedReason = unformattedReason; + _formattingParameter = formattingParameter; + } + + /// + /// Gets the project root element which was just changed.. + /// + /// Never null. + public ProjectRootElement ProjectXml { get; private set; } + + /// + /// Gets the reason for the change. + /// + /// May be null. + public string Reason + { + get { return _unformattedReason != null ? String.Format(CultureInfo.CurrentCulture, _unformattedReason, _formattingParameter) : null; } + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/StringMetadataTable.cs b/src/XMakeBuildEngine/Evaluation/StringMetadataTable.cs new file mode 100644 index 00000000000..39c341f95e0 --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/StringMetadataTable.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of IMetadataTable wrapping a table of string values +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Wraps a table of metadata values in which keys + /// may be qualified ("itemtype.name") or unqualified ("name"). + /// + internal class StringMetadataTable : IMetadataTable + { + /// + /// Table of metadata values. + /// Each key may be qualified ("itemtype.name") or unqualified ("name"). + /// Unqualified are considered to apply to all item types. + /// May be null, if empty. + /// + private Dictionary _metadata; + + /// + /// Constructor taking a table of metadata in which keys + /// may be a mixture of qualified ("itemtype.name") and unqualified ("name"). + /// Unqualified keys are considered to apply to all item types. + /// Metadata may be null, indicating it is empty. + /// + internal StringMetadataTable(Dictionary metadata) + { + _metadata = metadata; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name specified. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string name) + { + return GetEscapedValue(null, name); + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string itemType, string name) + { + return GetEscapedValueIfPresent(itemType, name) ?? String.Empty; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If no value is available, returns null. + /// + public string GetEscapedValueIfPresent(string itemType, string name) + { + if (_metadata == null) + { + return null; + } + + string key; + if (itemType == null) + { + key = name; + } + else + { + key = itemType + "." + name; + } + + string value = null; + _metadata.TryGetValue(key, out value); + + return value; + } + } +} diff --git a/src/XMakeBuildEngine/Evaluation/ToolsetProvider.cs b/src/XMakeBuildEngine/Evaluation/ToolsetProvider.cs new file mode 100644 index 00000000000..244ee0c6b9a --- /dev/null +++ b/src/XMakeBuildEngine/Evaluation/ToolsetProvider.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class which can load and hold toolsets. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; + +using Constants = Microsoft.Build.Internal.Constants; + +namespace Microsoft.Build.Evaluation +{ + /// + /// Class which provides access to toolsets. + /// + internal class ToolsetProvider : IToolsetProvider, INodePacketTranslatable + { + /// + /// A mapping of tools versions to Toolsets, which contain the public Toolsets. + /// This is the collection we use internally. + /// + private Dictionary _toolsets; + + /// + /// Constructor which will load toolsets from the specified locations. + /// + public ToolsetProvider(string defaultToolsVersion, PropertyDictionary environmentProperties, PropertyDictionary globalProperties, ToolsetDefinitionLocations toolsetDefinitionLocations) + { + InitializeToolsetCollection(defaultToolsVersion, environmentProperties, globalProperties, toolsetDefinitionLocations); + } + + /// + /// Constructor from an existing collection of toolsets. + /// + public ToolsetProvider(IEnumerable toolsets) + { + _toolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (Toolset toolset in toolsets) + { + _toolsets[toolset.ToolsVersion] = toolset; + } + } + + /// + /// Private constructor for deserialization + /// + private ToolsetProvider(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + #region IToolsetProvider Members + + /// + /// Retrieves the toolsets. + /// + /// + /// ValueCollection is already read-only. + /// + public ICollection Toolsets + { + get { return _toolsets.Values; } + } + + /// + /// Gets the specified toolset. + /// + public Toolset GetToolset(string toolsVersion) + { + ErrorUtilities.VerifyThrowArgumentLength(toolsVersion, "toolsVersion"); + + Toolset toolset; + _toolsets.TryGetValue(toolsVersion, out toolset); + + return toolset; + } + + #endregion + + #region INodePacketTranslatable Members + + /// + /// Translates to and from binary form. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.TranslateDictionary(ref _toolsets, StringComparer.OrdinalIgnoreCase, Toolset.FactoryForDeserialization); + } + + /// + /// Factory for deserialization. + /// + static internal ToolsetProvider FactoryForDeserialization(INodePacketTranslator translator) + { + ToolsetProvider provider = new ToolsetProvider(translator); + return provider; + } + + #endregion + + /// + /// Populate Toolsets with a dictionary of (toolset version, Toolset) + /// using information from the registry and config file, if any. + /// + private void InitializeToolsetCollection(string defaultToolsVersion, PropertyDictionary environmentProperties, PropertyDictionary globalProperties, ToolsetDefinitionLocations toolsetDefinitionLocations) + { + _toolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + defaultToolsVersion = ToolsetReader.ReadAllToolsets(_toolsets, environmentProperties, globalProperties, toolsetDefinitionLocations); + } + } +} diff --git a/src/XMakeBuildEngine/FxCopExclusions/Microsoft.Build.Suppressions.cs b/src/XMakeBuildEngine/FxCopExclusions/Microsoft.Build.Suppressions.cs new file mode 100644 index 00000000000..e56b47796d3 --- /dev/null +++ b/src/XMakeBuildEngine/FxCopExclusions/Microsoft.Build.Suppressions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// FxCop Suppression file +// To Use: +// Add module level suppressions to this file to have them suppressed in the assembly +// + +using System.Diagnostics.CodeAnalysis; + +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Design","CA1020:AvoidNamespacesWithFewTypes", Scope="namespace", Target="Microsoft.Build.Debugging", Justification="This deserves its own namespace")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Resources.Strings.resources", MessageId="itemname", Justification="itemname is spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Resources.Strings.resources", MessageId="sln", Justification="sln is the extension for a solution")] +[module: SuppressMessage("Microsoft.Design","CA1032:ImplementStandardExceptionConstructors",Justification="We require this constructor for deserialization")] +[module: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification="We delay sign our assemblies.")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Resources.Strings.resources", MessageId="precompilation", Justification="precompilation is correctly spelled.")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Resources.Strings.resources", MessageId="devenv", Justification="devenv is correctly spelled.")] +[module: SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", Scope="namespace", Target="Microsoft.Build.Shared", MessageId="Shared")] +[module: SuppressMessage("Microsoft.MSInternal", "CA905:SystemAndMicrosoftNamespacesRequireApproval", Scope="namespace", Target="Microsoft.Build.Shared")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="aspnetcompiler", Scope="resource", Target="Microsoft.Build.Resources.Strings.resources", Justification="AspNetCompiler is the name of the task")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="vcproj", Scope="resource", Target="Microsoft.Build.Resources.Strings.resources", Justification="vcproj is an extension and is spelled corectly")] + +[module: SuppressMessage("Microsoft.Security","CA2119:SealMethodsThatSatisfyPrivateInterfaces", Scope="member", Target="Microsoft.Build.Construction.ElementLocation.#get_Line()", Justification="This must be overridable. SmallElementLocation and RegularElementLocation already override it")] +[module: SuppressMessage("Microsoft.Security","CA2119:SealMethodsThatSatisfyPrivateInterfaces", Scope="member", Target="Microsoft.Build.Construction.ElementLocation.#get_File()", Justification="This must be overridable. SmallElementLocation and RegularElementLocation already override it")] +[module: SuppressMessage("Microsoft.Security","CA2119:SealMethodsThatSatisfyPrivateInterfaces", Scope="member", Target="Microsoft.Build.Construction.ElementLocation.#get_Column()", Justification="This must be overridable. SmallElementLocation and RegularElementLocation already override it")] + +#endif diff --git a/src/XMakeBuildEngine/Instance/HostServices.cs b/src/XMakeBuildEngine/Instance/HostServices.cs new file mode 100644 index 00000000000..5830e2380ee --- /dev/null +++ b/src/XMakeBuildEngine/Instance/HostServices.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of HostServices that mediates access from the build to the host. +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using System.Collections.Generic; +using System; +using System.Collections; +using System.Diagnostics; + +namespace Microsoft.Build.Execution +{ + /// + /// Controls where projects must be built. + /// + public enum NodeAffinity + { + /// + /// The project may only be scheduled on the in-proc node. This happens automatically if there is a host object or if a ProjectInstance + /// was specified. A host may wish to specify it if they know a task depends explicitly on shared static data or other host-provided + /// objects. + /// + InProc, + + /// + /// The project may only be scheduled on an out-of-proc node. A host may wish to specify this if it is known the project being built + /// could contaminate the host environment (or the host contaminates the environment while a build is proceeding.) + /// + OutOfProc, + + /// + /// The project may be scheduled anywhere. + /// + Any + } + + /// + /// Implementation of HostServices that + /// mediates access from the build to the host. + /// + [DebuggerDisplay("#Entries={hostObjectMap.Count}")] + public class HostServices + { + /// + /// Collection storing host objects for particular project/task/target combinations. + /// + private Dictionary _hostObjectMap; + + /// + /// A mapping of project file names to their node affinities. An entry for String.Empty means that + /// all projects which don't otherwise have an affinity should use that affinity. + /// + private Dictionary _projectAffinities; + + /// + /// Gets any host object applicable to this task name + /// where the task appears within a target and project with the specified names. + /// If no host object exists, returns null. + /// + public ITaskHost GetHostObject(string projectFile, string targetName, string taskName) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentNull(targetName, "targetName"); + ErrorUtilities.VerifyThrowArgumentNull(taskName, "taskName"); + + HostObjects hostObjects; + if (_hostObjectMap == null || !_hostObjectMap.TryGetValue(projectFile, out hostObjects)) + { + return null; + } + + ITaskHost hostObject = hostObjects.GetAnyMatchingHostObject(targetName, taskName); + + return hostObject; + } + + /// + /// Register a host object for a particular task/target pair. + /// Overwrites any existing host object. + /// + public void RegisterHostObject(string projectFile, string targetName, string taskName, ITaskHost hostObject) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentNull(targetName, "targetName"); + ErrorUtilities.VerifyThrowArgumentNull(taskName, "taskName"); + + // We can only set the host object to a non-null value if the affinity for the project is not out of proc, or if it is, it is only implicitly + // out of proc, in which case it will become in-proc after this call completes. See GetNodeAffinity. + bool isExplicit; + bool hasExplicitOutOfProcAffinity = (GetNodeAffinity(projectFile, out isExplicit) == NodeAffinity.OutOfProc) && (isExplicit == true); + ErrorUtilities.VerifyThrowInvalidOperation(!hasExplicitOutOfProcAffinity || hostObject == null, "InvalidHostObjectOnOutOfProcProject"); + _hostObjectMap = _hostObjectMap ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + HostObjects hostObjects; + if (!_hostObjectMap.TryGetValue(projectFile, out hostObjects)) + { + hostObjects = new HostObjects(); + _hostObjectMap[projectFile] = hostObjects; + } + + hostObjects.RegisterHostObject(targetName, taskName, hostObject); + } + + /// + /// Unregister the project's host objects, if any and remove any node affinities associated with it. + /// + public void UnregisterProject(string projectFullPath) + { + if (projectFullPath != null) + { + if (_hostObjectMap != null && _hostObjectMap.ContainsKey(projectFullPath)) + { + _hostObjectMap.Remove(projectFullPath); + } + + if (_projectAffinities != null && _projectAffinities.ContainsKey(projectFullPath)) + { + _projectAffinities.Remove(projectFullPath); + } + } + } + + /// + /// Retrieves the node affinity for a particular project file. + /// + public NodeAffinity GetNodeAffinity(string projectFile) + { + bool isExplicit; + return GetNodeAffinity(projectFile, out isExplicit); + } + + /// + /// Sets the node affinity for a particular project file. + /// + /// + /// The project file. If set to String.Empty, all projects will use the specified affinity. If set to null, all affinities will be cleared. + /// + public void SetNodeAffinity(string projectFile, NodeAffinity nodeAffinity) + { + if (projectFile == null) + { + _projectAffinities = null; + } + else + { + if (HasHostObject(projectFile)) + { + ErrorUtilities.VerifyThrowInvalidOperation(nodeAffinity == NodeAffinity.InProc, "InvalidAffinityForProjectWithHostObject"); + } + + if (_projectAffinities == null) + { + _projectAffinities = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + _projectAffinities[projectFile] = nodeAffinity; + } + } + + /// + /// Updates the host object table when a project is renamed. + /// Old full path may be null. + /// + public void OnRenameProject(string oldFullPath, string newFullPath) + { + HostObjects hostObjects; + if (oldFullPath != null && _hostObjectMap != null && _hostObjectMap.TryGetValue(oldFullPath, out hostObjects)) + { + _hostObjectMap.Remove(oldFullPath); + _hostObjectMap[newFullPath] = hostObjects; + } + } + + /// + /// Returns true if there is any host object registered for this project file. + /// + internal bool HasHostObject(string projectFile) + { + if (_hostObjectMap == null) + { + return false; + } + + HostObjects hostObjects; + + if (!_hostObjectMap.TryGetValue(projectFile, out hostObjects)) + { + return false; + } + + return hostObjects.HasRegisteredHostObjects; + } + + /// + /// Retrieves the node affinity for a particular project file. + /// + private NodeAffinity GetNodeAffinity(string projectFile, out bool isExplicit) + { + isExplicit = false; + + // Projects with a registered host object must build in-proc + if (HasHostObject(projectFile)) + { + return NodeAffinity.InProc; + } + + // Now see if a specific affinity has been provided. + if (_projectAffinities != null) + { + NodeAffinity affinity = NodeAffinity.Any; + + if (_projectAffinities.TryGetValue(projectFile, out affinity)) + { + isExplicit = true; + return affinity; + } + + if (_projectAffinities.TryGetValue(String.Empty, out affinity)) + { + return affinity; + } + } + + // Attempts to find a specific affinity failed, so just go with Any. + return NodeAffinity.Any; + } + + /// + /// Bag holding host object information for a single project file. + /// + [DebuggerDisplay("#HostObjects={hostObjects.Count}")] + private class HostObjects + { + /// + /// The mapping of targets and tasks to host objects. + /// + private Dictionary _hostObjects; + + /// + /// Constructor + /// + internal HostObjects() + { + _hostObjects = new Dictionary(1); + } + + /// + /// Accessor which indicates if there are any registered host objects. + /// + internal bool HasRegisteredHostObjects + { + get + { + return _hostObjects.Count > 0; + } + } + + /// + /// Registers a host object for this project file + /// + internal void RegisterHostObject(string targetName, string taskName, ITaskHost hostObject) + { + if (hostObject == null) + { + _hostObjects.Remove(new TargetTaskKey(targetName, taskName)); + } + else + { + _hostObjects[new TargetTaskKey(targetName, taskName)] = hostObject; + } + } + + /// + /// Gets any host object for this project file matching the task and target names specified. + /// + internal ITaskHost GetAnyMatchingHostObject(string targetName, string taskName) + { + ITaskHost hostObject; + _hostObjects.TryGetValue(new TargetTaskKey(targetName, taskName), out hostObject); + + return hostObject; + } + + /// + /// Equatable key for the table + /// + private struct TargetTaskKey : IEquatable + { + /// + /// Target name + /// + private string _targetName; + + /// + /// Task name + /// + private string _taskName; + + /// + /// Constructor + /// + public TargetTaskKey(string targetName, string taskName) + { + _targetName = targetName; + _taskName = taskName; + } + + /// + /// Implementation of IEquatable. + /// + public bool Equals(TargetTaskKey other) + { + bool result = (String.Equals(_targetName, other._targetName, StringComparison.OrdinalIgnoreCase) && + String.Equals(_taskName, other._taskName, StringComparison.OrdinalIgnoreCase)); + + return result; + } + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Instance/ProjectInstance.cs b/src/XMakeBuildEngine/Instance/ProjectInstance.cs new file mode 100644 index 00000000000..15f4c3c2d40 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectInstance.cs @@ -0,0 +1,2399 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Definition of ProjectInstance class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Evaluation = Microsoft.Build.Evaluation; +using ObjectModel = System.Collections.ObjectModel; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Internal; +using Utilities = Microsoft.Build.Internal.Utilities; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory; +using Microsoft.Build.Debugging; +using System.Xml; +using System.IO; +using System.Collections; +using System.Runtime.CompilerServices; +using System.Linq; + +namespace Microsoft.Build.Execution +{ + /// + /// Enum for controlling project instance creation + /// + [Flags] + [SuppressMessage("Microsoft.Usage", "CA2217:DoNotMarkEnumsWithFlags", Justification = "ImmutableWithFastItemLookup is a variation on Immutable")] + public enum ProjectInstanceSettings + { + /// + /// no options + /// + None = 0x0, + + /// + /// create immutable version of project instance + /// + Immutable = 0x1, + + /// + /// create project instance with some look up table that improves performance + /// + ImmutableWithFastItemLookup = Immutable | 0x2 + } + + /// + /// What the user gets when they clone off a ProjectInstance. + /// They can hold onto this, change/query items and properties, + /// and call it several times to build it. + /// + /// + /// Neither this class nor none of its constituents are allowed to have + /// references to any of the Construction or Evaluation objects. + /// This class is immutable except for adding instance items and setting instance properties. + /// It only exposes items and properties: targets, host services, and the task registry are not exposed as they are only the concern of build. + /// Constructors are internal in order to direct users to Project class instead; these are only createable via Project objects. + /// + [DebuggerDisplay(@"{FullPath} #Targets={TargetsCount} DefaultTargets={(DefaultTargets == null) ? System.String.Empty : System.String.Join("";"", DefaultTargets.ToArray())} ToolsVersion={Toolset.ToolsVersion} InitialTargets={(InitialTargets == null) ? System.String.Empty : System.String.Join("";"", InitialTargets.ToArray())} #GlobalProperties={globalProperties.Count} #Properties={properties.Count} #ItemTypes={items.ItemTypes.Count} #Items={items.Count}")] + public class ProjectInstance : IPropertyProvider, IItemProvider, IEvaluatorData, INodePacketTranslatable + { + /// + /// Targets in the project after overrides have been resolved. + /// This is an unordered collection keyed by target name. + /// Only the wrapper around this collection is exposed. + /// + private RetrievableEntryHashSet _actualTargets; + + /// + /// Targets in the project after overrides have been resolved. + /// This is an immutable, unordered collection keyed by target name. + /// It is just a wrapper around actualTargets. + /// + private IDictionary _targets; + + /// + /// The global properties evaluation occurred with. + /// Needed by the build as they traverse between projects. + /// + private PropertyDictionary _globalProperties; + + /// + /// List of names of the properties that, while global, are still treated as overridable + /// + private ISet _globalPropertiesToTreatAsLocal; + + /// + /// Whether the tools version used originated from an explicit specification, + /// for example from an MSBuild task or /tv switch. + /// + private bool _explicitToolsVersionSpecified; + + /// + /// Properties in the project. This is a dictionary of name, value pairs. + /// + private PropertyDictionary _properties; + + /// + /// Properties originating from environment variables, gotten from the project collection + /// + private PropertyDictionary _environmentVariableProperties; + + /// + /// Items in the project. This is a dictionary of ordered lists of a single type of items keyed by item type. + /// + private ItemDictionary _items; + + /// + /// Items organized by evaluatedInclude value + /// + private MultiDictionary _itemsByEvaluatedInclude; + + /// + /// Items, properties and other project level values to display + /// in the debugger, if we are being debugged. + /// If not debugging, this is null. + /// + private IDictionary _initialGlobalsForDebugging; + + /// + /// The project's root directory, for evaluation of relative paths and + /// setting the current directory during build. + /// Is never null. + /// If the project has not been loaded from disk and has not been given a path, returns the current directory from + /// the time the project was loaded - this is the same behavior as Whidbey/Orcas. + /// If the project has not been loaded from disk but has been given a path, this path may not exist. + /// + private string _directory; + + /// + /// The project file location, for logging. + /// If the project has not been loaded from disk and has not been given a path, returns null. + /// If the project has not been loaded from disk but has been given a path, this path may not exist. + /// + private ElementLocation _projectFileLocation; + + /// + /// The item definitions from the parent Project. + /// + private RetrievableEntryHashSet _itemDefinitions; + + /// + /// The HostServices to use during a build. + /// + private HostServices _hostServices; + + /// + /// Whether when we read a ToolsVersion that is not equivalent to the current one on the Project tag, we + /// treat it as the current one. + /// + private bool _usingDifferentToolsVersionFromProjectFile; + + /// + /// The toolsversion that was originally on the project's Project root element + /// + private string _originalProjectToolsVersion; + + /// + /// Whether the instance is immutable. + /// The object is always mutable during evaluation. + /// + private bool _isImmutable; + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Uses the default project collection. + /// + /// The name of the project file. + /// A new project instance + public ProjectInstance(string projectFile) + : this(projectFile, null, (string)null) + { + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Uses the default project collection. + /// + /// The name of the project file. + /// The global properties to use. + /// The tools version. + /// A new project instance + public ProjectInstance(string projectFile, IDictionary globalProperties, string toolsVersion) + : this(projectFile, globalProperties, toolsVersion, ProjectCollection.GlobalProjectCollection) + { + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Global properties may be null. + /// Tools version may be null. + /// + /// The name of the project file. + /// The global properties to use. + /// The tools version. + /// Project collection + /// A new project instance + public ProjectInstance(string projectFile, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection) + : this(projectFile, globalProperties, toolsVersion, null /* no sub-toolset version */, projectCollection) + { + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Global properties may be null. + /// Tools version may be null. + /// + /// The name of the project file. + /// The global properties to use. + /// The tools version. + /// The sub-toolset version, used in tandem with the ToolsVersion to determine the set of toolset properties. + /// Project collection + /// A new project instance + public ProjectInstance(string projectFile, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection) + { + ErrorUtilities.VerifyThrowArgumentLength(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + + // We do not control the current directory at this point, but assume that if we were + // passed a relative path, the caller assumes we will prepend the current directory. + projectFile = FileUtilities.NormalizePath(projectFile); + + BuildParameters buildParameters = new BuildParameters(projectCollection); + + BuildEventContext buildEventContext = new BuildEventContext(buildParameters.NodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile, globalProperties, toolsVersion, projectCollection.LoggingService, buildParameters.ProjectRootElementCache, buildEventContext, true /*Explicitly Loaded*/); + + Initialize(xml, globalProperties, toolsVersion, subToolsetVersion, 0 /* no solution version provided */, buildParameters, projectCollection.LoggingService, buildEventContext); + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Uses the default project collection. + /// + /// The project root element + /// A new project instance + public ProjectInstance(ProjectRootElement xml) + : this(xml, null, null, ProjectCollection.GlobalProjectCollection) + { + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Global properties may be null. + /// Tools version may be null. + /// + /// The project root element + /// The global properties to use. + /// The tools version. + /// Project collection + /// A new project instance + public ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, ProjectCollection projectCollection) + : this(xml, globalProperties, toolsVersion, null, projectCollection) + { + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Global properties may be null. + /// Tools version may be null. + /// Sub-toolset version may be null, but if specified will override all other methods of determining the sub-toolset. + /// + /// The project root element + /// The global properties to use. + /// The tools version. + /// The sub-toolset version, used in tandem with the ToolsVersion to determine the set of toolset properties. + /// Project collection + /// A new project instance + public ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection) + { + BuildEventContext buildEventContext = new BuildEventContext(0, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + Initialize(xml, globalProperties, toolsVersion, subToolsetVersion, 0 /* no solution version specified */, new BuildParameters(projectCollection), projectCollection.LoggingService, buildEventContext); + } + + /// + /// Creates a ProjectInstance directly. Used to generate solution metaprojects. + /// + /// The full path to give to this project. + /// The traversal project from which global properties and tools version will be inherited. + internal ProjectInstance(string projectFile, ProjectInstance projectToInheritFrom, IDictionary globalProperties) + { + _projectFileLocation = ElementLocation.Create(projectFile); + _globalProperties = new PropertyDictionary(globalProperties.Count); + this.Toolset = projectToInheritFrom.Toolset; + this.SubToolsetVersion = projectToInheritFrom.SubToolsetVersion; + _explicitToolsVersionSpecified = projectToInheritFrom._explicitToolsVersionSpecified; + _properties = new PropertyDictionary(projectToInheritFrom._properties); // This brings along the reserved properties, which are important. + _items = new ItemDictionary(); // We don't want any of the items. That would include things like ProjectReferences, which would just pollute our own. + _actualTargets = new RetrievableEntryHashSet(OrdinalIgnoreCaseKeyedComparer.Instance); + _targets = new ObjectModel.ReadOnlyDictionary(_actualTargets); + _environmentVariableProperties = projectToInheritFrom._environmentVariableProperties; + _itemDefinitions = new RetrievableEntryHashSet((IEnumerable)projectToInheritFrom._itemDefinitions, MSBuildNameIgnoreCaseComparer.Default); + _hostServices = projectToInheritFrom._hostServices; + this.ProjectRootElementCache = projectToInheritFrom.ProjectRootElementCache; + _explicitToolsVersionSpecified = projectToInheritFrom._explicitToolsVersionSpecified; + this.InitialTargets = new List(); + this.DefaultTargets = new List(); + this.DefaultTargets.Add("Build"); + this.TaskRegistry = projectToInheritFrom.TaskRegistry; + _isImmutable = projectToInheritFrom._isImmutable; + + IEvaluatorData thisAsIEvaluatorData = (IEvaluatorData)this; + thisAsIEvaluatorData.AfterTargets = new Dictionary>(); + thisAsIEvaluatorData.BeforeTargets = new Dictionary>(); + + foreach (KeyValuePair property in globalProperties) + { + _globalProperties[property.Key] = ProjectPropertyInstance.Create(property.Key, property.Value, false /* may not be reserved */, _isImmutable); + } + } + + /// + /// Creates a ProjectInstance directly. + /// No intermediate Project object is created. + /// This is ideal if the project is simply going to be built, and not displayed or edited. + /// Global properties may be null. + /// Tools version may be null. + /// Used by SolutionProjectGenerator so that it can explicitly pass the vsVersionFromSolution in for use in + /// determining the sub-toolset version. + /// + /// The project root element + /// The global properties to use. + /// The tools version. + /// The version of the solution, used to help determine which sub-toolset to use. + /// Project collection + /// A new project instance + internal ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, int visualStudioVersionFromSolution, ProjectCollection projectCollection) + { + BuildEventContext buildEventContext = new BuildEventContext(0, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + Initialize(xml, globalProperties, toolsVersion, null, visualStudioVersionFromSolution, new BuildParameters(projectCollection), projectCollection.LoggingService, buildEventContext); + } + + /// + /// Creates a mutable ProjectInstance directly, using the specified logging service. + /// Assumes the project path is already normalized. + /// Used by the RequestBuilder. + /// + internal ProjectInstance(string projectFile, IDictionary globalProperties, string toolsVersion, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext) + { + ErrorUtilities.VerifyThrowArgumentLength(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(buildParameters, "buildParameters"); + + ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile, globalProperties, toolsVersion, loggingService, buildParameters.ProjectRootElementCache, buildEventContext, false /*Not explicitly loaded*/); + + Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext); + } + + /// + /// Creates a mutable ProjectInstance directly, using the specified logging service. + /// Assumes the project path is already normalized. + /// Used by this class when generating legacy solution wrappers. + /// + internal ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext) + { + ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(buildParameters, "buildParameters"); + Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext); + } + + /// + /// Constructor called by Project's constructor to create a fresh instance. + /// Properties and items are cloned immediately and only the instance data is stored. + /// + internal ProjectInstance(Evaluation.Project.Data data, string directory, string fullPath, HostServices hostServices, PropertyDictionary environmentVariableProperties, ProjectInstanceSettings settings) + { + ErrorUtilities.VerifyThrowInternalNull(data, "data"); + ErrorUtilities.VerifyThrowInternalLength(directory, "directory"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(fullPath, "fullPath"); + + _directory = directory; + _projectFileLocation = ElementLocation.Create(fullPath); + _hostServices = hostServices; + + var immutable = (settings & ProjectInstanceSettings.Immutable) == ProjectInstanceSettings.Immutable; + this.CreatePropertiesSnapshot(data, immutable); + + this.CreateItemDefinitionsSnapshot(data); + + var keepEvaluationCache = (settings & ProjectInstanceSettings.ImmutableWithFastItemLookup) == ProjectInstanceSettings.ImmutableWithFastItemLookup; + var projectItemToInstanceMap = this.CreateItemsSnapshot(data, keepEvaluationCache); + + this.CreateEvaluatedIncludeSnapshotIfRequested(keepEvaluationCache, data, projectItemToInstanceMap); + this.CreateGlobalPropertiesSnapshot(data); + this.CreateEnvironmentVariablePropertiesSnapshot(environmentVariableProperties); + this.CreateTargetsSnapshot(data); + + this.Toolset = data.Toolset; // UNDONE: This isn't immutable, should be cloned or made immutable; it currently has a pointer to project collection + this.SubToolsetVersion = data.SubToolsetVersion; + this.TaskRegistry = data.TaskRegistry; + + this.ProjectRootElementCache = data.Project.ProjectCollection.ProjectRootElementCache; + + _usingDifferentToolsVersionFromProjectFile = data.UsingDifferentToolsVersionFromProjectFile; + _originalProjectToolsVersion = data.OriginalProjectToolsVersion; + _explicitToolsVersionSpecified = data.ExplicitToolsVersion != null; + + _isImmutable = immutable; + } + + /// + /// Constructor for deserialization. + /// + private ProjectInstance(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Deep clone of this object. + /// Useful for compiling a single file; or for keeping resolved assembly references between builds + /// Mutability is same as original. + /// + private ProjectInstance(ProjectInstance that) + : this(that, that._isImmutable) + { + } + + /// + /// Deep clone of this object. + /// Useful for compiling a single file; or for keeping resolved assembly references between builds. + /// + private ProjectInstance(ProjectInstance that, bool isImmutable) + { + _directory = that._directory; + _projectFileLocation = that._projectFileLocation; + _hostServices = that._hostServices; + _isImmutable = isImmutable; + + _properties = new PropertyDictionary(that._properties.Count); + + foreach (ProjectPropertyInstance property in that.Properties) + { + _properties.Set(property.DeepClone(_isImmutable)); + } + + _items = new ItemDictionary(that._items.ItemTypes.Count); + + foreach (ProjectItemInstance item in that.Items) + { + _items.Add(item.DeepClone(this)); + } + + _globalProperties = new PropertyDictionary(that._globalProperties.Count); + + foreach (ProjectPropertyInstance globalProperty in that.GlobalPropertiesDictionary) + { + _globalProperties.Set(globalProperty.DeepClone(_isImmutable)); + } + + _environmentVariableProperties = new PropertyDictionary(that._environmentVariableProperties.Count); + + foreach (ProjectPropertyInstance environmentProperty in that._environmentVariableProperties) + { + _environmentVariableProperties.Set(environmentProperty.DeepClone(_isImmutable)); + } + + this.DefaultTargets = new List(that.DefaultTargets); + this.InitialTargets = new List(that.InitialTargets); + ((IEvaluatorData)this).BeforeTargets = CreateCloneDictionary(((IEvaluatorData)that).BeforeTargets, StringComparer.OrdinalIgnoreCase); + ((IEvaluatorData)this).AfterTargets = CreateCloneDictionary(((IEvaluatorData)that).AfterTargets, StringComparer.OrdinalIgnoreCase); + this.TaskRegistry = that.TaskRegistry; // UNDONE: This isn't immutable, should be cloned or made immutable; it currently has a pointer to project collection + + // These are immutable so we don't need to clone them: + this.Toolset = that.Toolset; + this.SubToolsetVersion = that.SubToolsetVersion; + _targets = that._targets; + _itemDefinitions = that._itemDefinitions; + _explicitToolsVersionSpecified = that._explicitToolsVersionSpecified; + + this.ProjectRootElementCache = that.ProjectRootElementCache; + } + + /// + /// Global properties this project was evaluated with, if any. + /// Read only collection. + /// Traverses project references. + /// + /// + /// This is the publicly exposed getter, that translates into a read-only dead IDictionary<string, string>. + /// + public IDictionary GlobalProperties + { + [DebuggerStepThrough] + get + { + if (_globalProperties == null /* cached */ || _globalProperties.Count == 0) + { + return ReadOnlyEmptyDictionary.Instance; + } + + Dictionary dictionary = new Dictionary(_globalProperties.Count, MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectPropertyInstance property in _globalProperties) + { + dictionary[property.Name] = ((IProperty)property).EvaluatedValueEscaped; + } + + return new ObjectModel.ReadOnlyDictionary(dictionary); + } + } + + /// + /// The tools version this project was evaluated with, if any. + /// Not necessarily the same as the tools version on the Project tag, if any; + /// it may have been externally specified, for example with a /tv switch. + /// + public string ToolsVersion + { + get { return Toolset.ToolsVersion; } + } + + /// + /// Enumerator over item types of the items in this project + /// + public ICollection ItemTypes + { + [DebuggerStepThrough] + get + { + // KeyCollection, which is already read-only + return _items.ItemTypes; + } + } + + /// + /// Enumerator over properties in this project + /// + public ICollection Properties + { + [DebuggerStepThrough] + get + { + return (_properties == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new ReadOnlyCollection(_properties); + } + } + + /// + /// Enumerator over items in this project. + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public ICollection Items + { + [DebuggerStepThrough] + get + { + return (_items == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new ReadOnlyCollection(_items); + } + } + + /// + /// The project's root directory, for evaluation of relative paths and + /// setting the current directory during build. + /// Is never null: projects not loaded from disk use the current directory from + /// the time the build started. + /// + public string Directory + { + [DebuggerStepThrough] + get + { return _directory; } + } + + /// + /// The full path to the project, for logging. + /// If the project was never given a path, returns empty string. + /// + public string FullPath + { + [DebuggerStepThrough] + get + { return _projectFileLocation.File; } + } + + /// + /// Read-only dictionary of item definitions in this project. + /// Keyed by item type + /// + public IDictionary ItemDefinitions + { + [DebuggerStepThrough] + get + { return _itemDefinitions; } + } + + /// + /// DefaultTargets specified in the project, or + /// the logically first target if no DefaultTargets is + /// specified in the project. + /// The build builds these if no targets are explicitly specified + /// to build. + /// + public List DefaultTargets + { + get; + private set; + } + + /// + /// InitialTargets specified in the project, plus those + /// in all imports, gathered depth-first. + /// The build runs these before anything else. + /// + public List InitialTargets + { + get; + private set; + } + + /// + /// Targets in the project. The build process can find one by looking for its name + /// in the dictionary. + /// This collection is read-only. + /// + public IDictionary Targets + { + [DebuggerStepThrough] + get + { return _targets; } + } + + /// + /// Whether the instance is immutable. + /// This is set permanently when the instance is created. + /// + public bool IsImmutable + { + get { return _isImmutable; } + } + + /// + /// Task classes and locations known to this project. + /// This is the project-specific task registry, which is consulted before + /// the toolset's task registry. + /// Only set during evaluation, so does not check for immutability. + /// + TaskRegistry IEvaluatorData.TaskRegistry + { + [DebuggerStepThrough] + get + { return TaskRegistry; } + set { TaskRegistry = value; } + } + + /// + /// Gets the Toolset + /// + Toolset IEvaluatorData.Toolset + { + [DebuggerStepThrough] + get + { return Toolset; } + } + + /// + /// The sub-toolset version we should use during the build, used to determine which set of sub-toolset + /// properties we should merge into this toolset. + /// + string IEvaluatorData.SubToolsetVersion + { + [DebuggerStepThrough] + get + { return SubToolsetVersion; } + } + + /// + /// The externally specified tools version, if any. + /// For example, the tools version from a /tv switch. + /// Not necessarily the same as the tools version from the project tag or of the toolset used. + /// May be null. + /// Flows through to called projects. + /// + string IEvaluatorData.ExplicitToolsVersion + { + [DebuggerStepThrough] + get + { return ExplicitToolsVersion; } + } + + /// + /// Gets the global properties + /// + PropertyDictionary IEvaluatorData.GlobalPropertiesDictionary + { + [DebuggerStepThrough] + get + { return _globalProperties; } + } + + /// + /// List of names of the properties that, while global, are still treated as overridable + /// + ISet IEvaluatorData.GlobalPropertiesToTreatAsLocal + { + get + { + if (_globalPropertiesToTreatAsLocal == null) + { + _globalPropertiesToTreatAsLocal = new HashSet(MSBuildNameIgnoreCaseComparer.Default); + } + + return _globalPropertiesToTreatAsLocal; + } + } + + /// + /// Gets the global properties + /// + PropertyDictionary IEvaluatorData.Properties + { + [DebuggerStepThrough] + get + { return _properties; } + } + + /// + /// Gets the global properties + /// + IEnumerable IEvaluatorData.ItemDefinitionsEnumerable + { + [DebuggerStepThrough] + get + { return _itemDefinitions.Values; } + } + + /// + /// Gets the items + /// + ItemDictionary IEvaluatorData.Items + { + [DebuggerStepThrough] + get + { return _items; } + } + + /// + /// Sets the initial targets + /// Only set during evaluation, so does not check for immutability. + /// + List IEvaluatorData.InitialTargets + { + [DebuggerStepThrough] + get + { return InitialTargets; } + set { InitialTargets = value; } + } + + /// + /// Gets or sets the default targets + /// Only set during evaluation, so does not check for immutability. + /// + List IEvaluatorData.DefaultTargets + { + [DebuggerStepThrough] + get + { return DefaultTargets; } + set { DefaultTargets = value; } + } + + /// + /// Gets or sets the before targets + /// Only set during evaluation, so does not check for immutability. + /// + IDictionary> IEvaluatorData.BeforeTargets + { + get; + set; + } + + /// + /// Gets or sets the after targets + /// Only set during evaluation, so does not check for immutability. + /// + IDictionary> IEvaluatorData.AfterTargets + { + get; + set; + } + + /// + /// List of possible values for properties inferred from certain conditions, + /// keyed by the property name. + /// + /// + /// Because ShouldEvaluateForDesignTime returns false, this should not be called. + /// + Dictionary> IEvaluatorData.ConditionedProperties + { + get + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + } + + /// + /// Whether evaluation should collect items ignoring condition, + /// as well as items respecting condition; and collect + /// conditioned properties, as well as regular properties + /// + bool IEvaluatorData.ShouldEvaluateForDesignTime + { + get { return false; } + } + + /// + /// Location of the originating file itself, not any specific content within it. + /// Never returns null, even if the file has not got a path yet. + /// + public ElementLocation ProjectFileLocation + { + get { return _projectFileLocation; } + } + + /// + /// Gets the global properties this project was evaluated with, if any. + /// Traverses project references. + /// + internal PropertyDictionary GlobalPropertiesDictionary + { + [DebuggerStepThrough] + get + { return _globalProperties; } + } + + /// + /// The tools version we should use during the build, used to determine which toolset we should access. + /// + internal Toolset Toolset + { + get; + private set; + } + + /// + /// If we are treating a missing toolset as the current ToolsVersion + /// + internal bool UsingDifferentToolsVersionFromProjectFile + { + get { return _usingDifferentToolsVersionFromProjectFile; } + } + + /// + /// The toolsversion that was originally specified on the project's root element + /// + internal string OriginalProjectToolsVersion + { + get { return _originalProjectToolsVersion; } + } + + /// + /// The externally specified tools version, if any. + /// For example, the tools version from a /tv switch. + /// Not necessarily the same as the tools version from the project tag or of the toolset used. + /// May be null. + /// Flows through to called projects. + /// + internal string ExplicitToolsVersion + { + get { return _explicitToolsVersionSpecified ? Toolset.ToolsVersion : null; } + } + + /// + /// Whether the tools version used originated from an explicit specification, + /// for example from an MSBuild task or /tv switch. + /// + internal bool ExplicitToolsVersionSpecified + { + get { return _explicitToolsVersionSpecified; } + } + + /// + /// The sub-toolset version we should use during the build, used to determine which set of sub-toolset + /// properties we should merge into this toolset. + /// + internal string SubToolsetVersion + { + get; + private set; + } + + /// + /// Actual collection of properties in this project, + /// for the build to start with. + /// + internal PropertyDictionary PropertiesToBuildWith + { + [DebuggerStepThrough] + get + { return _properties; } + } + + /// + /// Actual collection of items in this project, + /// for the build to start with. + /// + internal ItemDictionary ItemsToBuildWith + { + [DebuggerStepThrough] + get + { return _items; } + } + + /// + /// Items, properties and other project level values to display + /// in the debugger, if we are being debugged. + /// If not debugging, this is null. + /// + internal IDictionary InitialGlobalsForDebugging + { + get { return _initialGlobalsForDebugging; } + } + + /// + /// Task classes and locations known to this project. + /// This is the project-specific task registry, which is consulted before + /// the toolset's task registry. + /// + /// + /// UsingTask tags have already been evaluated and entered into this task registry. + /// + internal TaskRegistry TaskRegistry + { + get; + private set; + } + + /// + /// Number of targets in the project. + /// + internal int TargetsCount + { + get { return _targets.Count; } + } + + /// + /// The project root element cache from the project collection + /// that began the build. This is a thread-safe object. + /// It's held here so it can get passed to the build. + /// + internal ProjectRootElementCache ProjectRootElementCache + { + get; + private set; + } + + /// + /// Returns the evaluated, escaped value of the provided item's include. + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] + public static string GetEvaluatedItemIncludeEscaped(ProjectItemInstance item) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).EvaluatedIncludeEscaped; + } + + /// + /// Returns the evaluated, escaped value of the provided item definition's include. + /// + public static string GetEvaluatedItemIncludeEscaped(ProjectItemDefinitionInstance item) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).EvaluatedIncludeEscaped; + } + + /// + /// Gets the escaped value of the provided metadatum. + /// + public static string GetMetadataValueEscaped(ProjectMetadataInstance metadatum) + { + ErrorUtilities.VerifyThrowArgumentNull(metadatum, "metadatum"); + + return metadatum.EvaluatedValueEscaped; + } + + /// + /// Gets the escaped value of the metadatum with the provided name on the provided item. + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IItem is an internal interface; this is less confusing to outside customers. ")] + public static string GetMetadataValueEscaped(ProjectItemInstance item, string name) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).GetMetadataValueEscaped(name); + } + + /// + /// Gets the escaped value of the metadatum with the provided name on the provided item definition. + /// + public static string GetMetadataValueEscaped(ProjectItemDefinitionInstance item, string name) + { + ErrorUtilities.VerifyThrowArgumentNull(item, "item"); + + return ((IItem)item).GetMetadataValueEscaped(name); + } + + /// + /// Get the escaped value of the provided property + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "IProperty is an internal interface; this is less confusing to outside customers. ")] + public static string GetPropertyValueEscaped(ProjectPropertyInstance property) + { + ErrorUtilities.VerifyThrowArgumentNull(property, "property"); + + return ((IProperty)property).EvaluatedValueEscaped; + } + + /// + /// Gets items of the specified type. + /// For internal use. + /// + /// + /// Already a readonly collection + /// + ICollection IItemProvider.GetItems(string itemType) + { + return _items[itemType]; + } + + /// + /// Initializes the object for evaluation. + /// Only called during evaluation, so does not check for immutability. + /// + void IEvaluatorData.InitializeForEvaluation(IToolsetProvider toolsetProvider) + { + // All been done in the constructor. We don't allow re-evaluation of project instances. + } + + /// + /// Indicates to the data block that evaluation has completed, + /// so for example it can mark datastructures read-only. + /// + void IEvaluatorData.FinishEvaluation() + { + // Ideally we would unify targets collections here (they are almost all the same) as Project.FinishEvaluation() does. + // However it's trickier as the target collections here are in a few cases mutated: they would have to be copy on write. + } + + /// + /// Adds a new item + /// Only called during evaluation, so does not check for immutability. + /// + void IEvaluatorData.AddItem(ProjectItemInstance item) + { + _items.Add(item); + } + + /// + /// Adds a new item to the collection of all items ignoring condition + /// + /// + /// Because ShouldEvaluateForDesignTime returns false, this should not be called. + /// + void IEvaluatorData.AddItemIgnoringCondition(ProjectItemInstance item) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Adds a new item definition + /// Only called during evaluation, so does not check for immutability. + /// + IItemDefinition IEvaluatorData.AddItemDefinition(string itemType) + { + ProjectItemDefinitionInstance itemDefinitionInstance = new ProjectItemDefinitionInstance(this, itemType); + + _itemDefinitions.Add(itemDefinitionInstance); + + return itemDefinitionInstance; + } + + /// + /// Properties encountered during evaluation. These are read during the first evaluation pass. + /// Unlike those returned by the Properties property, these are ordered, and include any properties that + /// were subsequently overridden by others with the same name. It does not include any + /// properties whose conditions did not evaluate to true. + /// + /// + /// Because ShouldEvaluateForDesignTime returns false, this should not be called. + /// + void IEvaluatorData.AddToAllEvaluatedPropertiesList(ProjectPropertyInstance property) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Item definition metadata encountered during evaluation. These are read during the second evaluation pass. + /// Unlike those returned by the ItemDefinitions property, these are ordered, and include any metadata that + /// were subsequently overridden by others with the same name and item type. It does not include any + /// elements whose conditions did not evaluate to true. + /// + /// + /// Because ShouldEvaluateForDesignTime returns false, this should not be called. + /// + void IEvaluatorData.AddToAllEvaluatedItemDefinitionMetadataList(ProjectMetadataInstance itemDefinitionMetadatum) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Items encountered during evaluation. These are read during the third evaluation pass. + /// Unlike those returned by the Items property, these are ordered. + /// It does not include any elements whose conditions did not evaluate to true. + /// It does not include any items added since the last evaluation. + /// + /// + /// Because ShouldEvaluateForDesignTime returns false, this should not be called. + /// + void IEvaluatorData.AddToAllEvaluatedItemsList(ProjectItemInstance item) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + + /// + /// Retrieves an existing item definition, if any. + /// + IItemDefinition IEvaluatorData.GetItemDefinition(string itemType) + { + ProjectItemDefinitionInstance itemDefinitionInstance; + + _itemDefinitions.TryGetValue(itemType, out itemDefinitionInstance); + + return itemDefinitionInstance; + } + + /// + /// Sets a property which does not come from the Xml. + /// This is where global, environment, and toolset properties are added to the project instance by the evaluator, and we mark them + /// immutable if we are immutable. + /// Only called during evaluation, so does not check for immutability. + /// + ProjectPropertyInstance IEvaluatorData.SetProperty(string name, string evaluatedValueEscaped, bool isGlobalProperty, bool mayBeReserved) + { + // Mutability not verified as this is being populated during evaluation + ProjectPropertyInstance property = ProjectPropertyInstance.Create(name, evaluatedValueEscaped, mayBeReserved, _isImmutable); + _properties.Set(property); + return property; + } + + /// + /// Sets a property which comes from the Xml. + /// Predecessor is discarded as it is a design time only artefact. + /// Only called during evaluation, so does not check for immutability. + /// + ProjectPropertyInstance IEvaluatorData.SetProperty(ProjectPropertyElement propertyElement, string evaluatedValueEscaped, ProjectPropertyInstance predecessor) + { + // Mutability not verified as this is being populated during evaluation + ProjectPropertyInstance property = ProjectPropertyInstance.Create(propertyElement.Name, evaluatedValueEscaped, false /* may not be reserved */, _isImmutable); + _properties.Set(property); + return property; + } + + /// + /// Retrieves an existing target, if any. + /// + ProjectTargetInstance IEvaluatorData.GetTarget(string targetName) + { + ProjectTargetInstance targetInstance; + + _targets.TryGetValue(targetName, out targetInstance); + + return targetInstance; + } + + /// + /// Adds a new target. + /// Only called during evaluation, so does not check for immutability. + /// + void IEvaluatorData.AddTarget(ProjectTargetInstance target) + { + _actualTargets[target.Name] = target; + } + + /// + /// Record an import opened during evaluation. + /// Does nothing: not needed for project instances. + /// + void IEvaluatorData.RecordImport(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated) + { + } + + /// + /// Record an import opened during evaluation. Include duplicates + /// Does nothing: not needed for project instances. + /// + void IEvaluatorData.RecordImportWithDuplicates(ProjectImportElement importElement, ProjectRootElement import, int versionEvaluated) + { + } + + /// + /// Get any property in the item that has the specified name, + /// otherwise returns null + /// + [DebuggerStepThrough] + public ProjectPropertyInstance GetProperty(string name) + { + return _properties[name]; + } + + /// + /// Get any property in the item that has the specified name, + /// otherwise returns null. + /// Name is the segment of the provided string with the provided start and end indexes. + /// + [DebuggerStepThrough] + ProjectPropertyInstance IPropertyProvider.GetProperty(string name, int startIndex, int endIndex) + { + return _properties.GetProperty(name, startIndex, endIndex); + } + + /// + /// Get the value of a property in this project, or + /// an empty string if it does not exist. + /// + /// + /// A property with a value of empty string and no property + /// at all are not distinguished between by this method. + /// This is because the build does not distinguish between the two. + /// The reason this method exists when users can simply do GetProperty(..).EvaluatedValue, + /// is that the caller would have to check for null every time. For properties, empty and undefined are + /// not distinguished, so it much more useful to also have a method that returns empty string in + /// either case. + /// This function returns the unescaped value. + /// + public string GetPropertyValue(string name) + { + ProjectPropertyInstance property = _properties[name]; + string value = (property == null) ? String.Empty : property.EvaluatedValue; + + return value; + } + + /// + /// Add a property with the specified name and value. + /// Overwrites any property with the same name already in the collection. + /// + /// + /// We don't take a ProjectPropertyInstance to make sure we don't have one that's already + /// in use by another ProjectPropertyInstance. + /// + public ProjectPropertyInstance SetProperty(string name, string evaluatedValue) + { + VerifyThrowNotImmutable(); + + ProjectPropertyInstance property = ProjectPropertyInstance.Create(name, evaluatedValue, false /* may not be reserved */, _isImmutable); + _properties.Set(property); + + return property; + } + + /// + /// Adds an item with no metadata to the project + /// + /// + /// We don't take a ProjectItemInstance to make sure we don't have one that's already + /// in use by another ProjectInstance. + /// + /// + /// For purposes of declaring the project that defined this item (for use with e.g. the + /// DeclaringProject* metadata), the entrypoint project is used for synthesized items + /// like those added by this API. + /// + public ProjectItemInstance AddItem(string itemType, string evaluatedInclude) + { + VerifyThrowNotImmutable(); + + ProjectItemInstance item = new ProjectItemInstance(this, itemType, evaluatedInclude, this.FullPath); + _items.Add(item); + + return item; + } + + /// + /// Adds an item with metadata to the project. + /// Metadata may be null. + /// + /// + /// We don't take a ProjectItemInstance to make sure we don't have one that's already + /// in use by another ProjectInstance. + /// + /// + /// For purposes of declaring the project that defined this item (for use with e.g. the + /// DeclaringProject* metadata), the entrypoint project is used for synthesized items + /// like those added by this API. + /// + public ProjectItemInstance AddItem(string itemType, string evaluatedInclude, IEnumerable> metadata) + { + VerifyThrowNotImmutable(); + + ProjectItemInstance item = new ProjectItemInstance(this, itemType, evaluatedInclude, metadata, this.FullPath); + _items.Add(item); + + return item; + } + + /// + /// Get a list of all the items in the project of the specified + /// type, or an empty list if there are none. + /// This is a read-only list. + /// + public ICollection GetItems(string itemType) + { + // GetItems already returns a readonly collection + return ((IItemProvider)this).GetItems(itemType); + } + + /// + /// get items by item type and evaluated include value + /// + public IEnumerable GetItemsByItemTypeAndEvaluatedInclude(string itemType, string evaluatedInclude) + { + if (_itemsByEvaluatedInclude == null) + { + return this.GetItems(itemType).Where(item => String.Equals(item.EvaluatedInclude, evaluatedInclude, StringComparison.OrdinalIgnoreCase)); + } + + return this.GetItemsByEvaluatedInclude(evaluatedInclude).Where(item => String.Equals(item.ItemType, itemType, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Removes an item from the project, if present. + /// Returns true if it was present, false otherwise. + /// + public bool RemoveItem(ProjectItemInstance item) + { + VerifyThrowNotImmutable(); + + return _items.Remove(item); + } + + /// + /// Removes any property with the specified name. + /// Returns true if the property had a value (possibly empty string), otherwise false. + /// + public bool RemoveProperty(string name) + { + VerifyThrowNotImmutable(); + + return _properties.Remove(name); + } + + /// + /// Create an independent, deep clone of this object and everything in it. + /// Useful for compiling a single file; or for keeping build results between builds. + /// Clone has the same mutability as the original. + /// + public ProjectInstance DeepCopy() + { + return DeepCopy(_isImmutable); + } + + /// + /// Create an independent, deep clone of this object and everything in it, with + /// specified mutability. + /// Useful for compiling a single file; or for keeping build results between builds. + /// + public ProjectInstance DeepCopy(bool isImmutable) + { + if (isImmutable && _isImmutable) + { + // No need to clone + return this; + } + + return new ProjectInstance(this, isImmutable); + } + + /// + /// Build default target/s with loggers of the project collection. + /// Returns true on success, false on failure. + /// Only valid if mutable. + /// + public bool Build() + { + return Build(null); + } + + /// + /// Build default target/s with specified loggers. + /// Returns true on success, false on failure. + /// Loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(IEnumerable loggers) + { + return Build((string[])null, loggers, null); + } + + /// + /// Build default target/s with specified loggers. + /// Returns true on success, false on failure. + /// Loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(IEnumerable loggers, IEnumerable remoteLoggers) + { + return Build((string[])null, loggers, remoteLoggers); + } + + /// + /// Build a target with specified loggers. + /// Returns true on success, false on failure. + /// Target may be null. + /// Loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(string target, IEnumerable loggers) + { + return Build(target, loggers, null); + } + + /// + /// Build a target with specified loggers. + /// Returns true on success, false on failure. + /// Target may be null. + /// Loggers may be null. + /// Remote loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(string target, IEnumerable loggers, IEnumerable remoteLoggers) + { + string[] targets = (target == null) ? new string[] { } : new string[] { target }; + + return Build(targets, loggers, remoteLoggers); + } + + /// + /// Build a list of targets with specified loggers. + /// Returns true on success, false on failure. + /// Targets may be null. + /// Loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(string[] targets, IEnumerable loggers) + { + return Build(targets, loggers, null); + } + + /// + /// Build a list of targets with specified loggers. + /// Returns true on success, false on failure. + /// Targets may be null. + /// Loggers may be null. + /// Remote loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(string[] targets, IEnumerable loggers, IEnumerable remoteLoggers) + { + IDictionary targetOutputs; + + return Build(targets, loggers, remoteLoggers, out targetOutputs); + } + + /// + /// Build a list of targets with specified loggers. + /// Returns true on success, false on failure. + /// Targets may be null. + /// Loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(string[] targets, IEnumerable loggers, out IDictionary targetOutputs) + { + return Build(targets, loggers, null, null, out targetOutputs); + } + + /// + /// Build a list of targets with specified loggers. + /// Returns true on success, false on failure. + /// Targets may be null. + /// Loggers may be null. + /// Remote loggers may be null. + /// Only valid if mutable. + /// + /// + /// If any of the loggers supplied are already attached to the logging service we + /// were passed, throws InvalidOperationException. + /// + public bool Build(string[] targets, IEnumerable loggers, IEnumerable remoteLoggers, out IDictionary targetOutputs) + { + return Build(targets, loggers, remoteLoggers, null, out targetOutputs); + } + + /// + /// Evaluates the provided string by expanding items and properties, + /// using the current items and properties available. + /// This is useful for some hosts, or for the debugger immediate window. + /// Does not expand bare metadata expressions. + /// + /// + /// Not for internal use. + /// + public string ExpandString(string unexpandedValue) + { + Expander expander = new Expander(this, this); + + string result = expander.ExpandIntoStringAndUnescape(unexpandedValue, ExpanderOptions.ExpandPropertiesAndItems, ProjectFileLocation); + + return result; + } + + /// + /// Evaluates the provided string as a condition by expanding items and properties, + /// using the current items and properties available, then doing a logical evaluation. + /// This is useful for the immediate window. + /// Does not expand bare metadata expressions. + /// + /// + /// Not for internal use. + /// + public bool EvaluateCondition(string condition) + { + Expander expander = new Expander(this, this); + + bool result = ConditionEvaluator.EvaluateCondition(condition, ParserOptions.AllowPropertiesAndItemLists, expander, ExpanderOptions.ExpandPropertiesAndItems, Directory, ProjectFileLocation, null /* no logging service */, BuildEventContext.Invalid); + + return result; + } + + /// + /// Creates a ProjectRootElement from the contents of this ProjectInstance. + /// + /// A ProjectRootElement which represents this instance. + public ProjectRootElement ToProjectRootElement() + { + ProjectRootElement rootElement = ProjectRootElement.Create(); + + rootElement.InitialTargets = String.Join(";", InitialTargets.ToArray()); + rootElement.DefaultTargets = String.Join(";", DefaultTargets.ToArray()); + rootElement.ToolsVersion = ToolsVersion; + + // Add all of the item definitions. + ProjectItemDefinitionGroupElement itemDefinitionGroupElement = rootElement.AddItemDefinitionGroup(); + foreach (ProjectItemDefinitionInstance itemDefinitionInstance in _itemDefinitions.Values) + { + itemDefinitionInstance.ToProjectItemDefinitionElement(itemDefinitionGroupElement); + } + + // Add all of the items. + foreach (string itemType in _items.ItemTypes) + { + ProjectItemGroupElement itemGroupElement = rootElement.AddItemGroup(); + foreach (ProjectItemInstance item in _items.GetItems(itemType)) + { + item.ToProjectItemElement(itemGroupElement); + } + } + + // Add all of the properties. + ProjectPropertyGroupElement propertyGroupElement = rootElement.AddPropertyGroup(); + foreach (ProjectPropertyInstance property in _properties) + { + if (!ReservedPropertyNames.IsReservedProperty(property.Name)) + { + // Only emit the property if it does not exist in the global or environment properties dictionaries or differs from them. + if (!_globalProperties.Contains(property.Name) || !String.Equals(_globalProperties[property.Name].EvaluatedValue, property.EvaluatedValue, StringComparison.OrdinalIgnoreCase)) + { + if (!_environmentVariableProperties.Contains(property.Name) || !String.Equals(_environmentVariableProperties[property.Name].EvaluatedValue, property.EvaluatedValue, StringComparison.OrdinalIgnoreCase)) + { + property.ToProjectPropertyElement(propertyGroupElement); + } + } + } + } + + // Add all of the targets. + foreach (ProjectTargetInstance target in Targets.Values) + { + target.ToProjectTargetElement(rootElement); + } + + return rootElement; + } + + /// + /// Replaces the project state (, and ) with that + /// from the provided. + /// + /// with the state to use. + public void UpdateStateFrom(ProjectInstance projectState) + { + _globalProperties = new PropertyDictionary(projectState._globalProperties); + _properties = new PropertyDictionary(projectState._properties); + _items = new ItemDictionary(projectState._items); + } + + #region INodePacketTranslatable Members + + /// + /// Translate the project instance to or from a stream. + /// Only translates global properties, properties, items, and mutability. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.TranslateDictionary, ProjectPropertyInstance>(ref _globalProperties, ProjectPropertyInstance.FactoryForDeserialization); + translator.TranslateDictionary, ProjectPropertyInstance>(ref _properties, ProjectPropertyInstance.FactoryForDeserialization); + translator.Translate(ref _isImmutable); + + if (translator.Mode == TranslationDirection.ReadFromStream) + { + int typeCount = default(int); + translator.Translate(ref typeCount); + _items = new ItemDictionary(typeCount); + for (int typeIndex = 0; typeIndex < typeCount; typeIndex++) + { + int itemCount = default(int); + translator.Translate(ref itemCount); + for (int i = 0; i < itemCount; i++) + { + ProjectItemInstance item = null; + translator.Translate(ref item, delegate (INodePacketTranslator outerTranslator) { return ProjectItemInstance.FactoryForDeserialization(translator, this); }); + _items.Add(item); + } + } + } + else + { + int typeCount = _items.ItemTypes.Count; + translator.Translate(ref typeCount); + + foreach (string itemType in _items.ItemTypes) + { + ICollection itemList = _items[itemType]; + int itemCount = itemList.Count; + translator.Translate(ref itemCount); + foreach (ProjectItemInstance item in itemList) + { + ProjectItemInstance temp = item; + translator.Translate(ref temp, delegate (INodePacketTranslator outerTranslator) { return ProjectItemInstance.FactoryForDeserialization(translator, this); }); + } + } + } + } + + #endregion + + /// + /// Creates a set of project instances which represent the project dependency graph for a solution build. + /// + internal static ProjectInstance[] LoadSolutionForBuild(string projectFile, PropertyDictionary globalPropertiesInstances, string toolsVersion, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext projectBuildEventContext, bool isExplicitlyLoaded) + { + ErrorUtilities.VerifyThrowArgumentLength(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentNull(globalPropertiesInstances, "globalPropertiesInstances"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(buildParameters, "buildParameters"); + ErrorUtilities.VerifyThrow(FileUtilities.IsSolutionFilename(projectFile), "Project file {0} is not a solution.", projectFile); + + ProjectInstance[] projectInstances = null; + + Dictionary globalProperties = new Dictionary(globalPropertiesInstances.Count, StringComparer.OrdinalIgnoreCase); + foreach (ProjectPropertyInstance propertyInstance in globalPropertiesInstances) + { + globalProperties[propertyInstance.Name] = ((IProperty)propertyInstance).EvaluatedValueEscaped; + } + + // If a ToolsVersion has been passed in using the /tv:xx switch, we want to generate an + // old-style solution wrapper project if it's < 4.0, to work around ordering issues. + if (toolsVersion != null) + { + if ( + String.Equals(toolsVersion, "2.0", StringComparison.OrdinalIgnoreCase) || + String.Equals(toolsVersion, "3.0", StringComparison.OrdinalIgnoreCase) || + String.Equals(toolsVersion, "3.5", StringComparison.OrdinalIgnoreCase) + ) + { + // Spawn the Orcas SolutionWrapperProject generator. + loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedExplicitToolsVersion", toolsVersion); + projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, toolsVersion, buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded); + } + else + { + projectInstances = GenerateSolutionWrapper(projectFile, globalProperties, toolsVersion, loggingService, projectBuildEventContext); + } + } + + // If the user didn't pass in a ToolsVersion, still try to make a best-effort guess as to whether + // we should be generating a 4.0+ or a 3.5-style wrapper project based on the version of the solution. + else if (toolsVersion == null) + { + int solutionVersion; + int visualStudioVersion; + SolutionFile.GetSolutionFileAndVisualStudioMajorVersions(projectFile, out solutionVersion, out visualStudioVersion); + + // If we get to this point, it's because it's a valid version. Map the solution version + // to the equivalent MSBuild ToolsVersion, and unless it's Dev10 or newer, spawn the old + // engine to generate the solution wrapper. + if (solutionVersion <= 9) /* Whidbey or before */ + { + loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedOldSolutionVersion", "2.0", solutionVersion); + projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, "2.0", buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded); + } + else if (solutionVersion == 10) /* Orcas */ + { + loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedOldSolutionVersion", "3.5", solutionVersion); + projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, "3.5", buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded); + } + else + { + if ((solutionVersion == 11) || (solutionVersion == 12 && visualStudioVersion == 0)) /* Dev 10 and Dev 11 */ + { + toolsVersion = "4.0"; + } + else /* Dev 12 and above */ + { + toolsVersion = visualStudioVersion.ToString(CultureInfo.InvariantCulture) + ".0"; + } + + string toolsVersionToUse = Utilities.GenerateToolsVersionToUse(explicitToolsVersion: null, toolsVersionFromProject: toolsVersion, getToolset: buildParameters.GetToolset, defaultToolsVersion: Constants.defaultSolutionWrapperProjectToolsVersion); + projectInstances = GenerateSolutionWrapper(projectFile, globalProperties, toolsVersionToUse, loggingService, projectBuildEventContext); + } + } + + return projectInstances; + } + + /// + /// Factory for deserialization. + /// + internal static ProjectInstance FactoryForDeserialization(INodePacketTranslator translator) + { + return new ProjectInstance(translator); + } + + /// + /// Throws invalid operation exception if the project instance is immutable. + /// Called before an edit. + /// + internal static void VerifyThrowNotImmutable(bool isImmutable) + { + if (isImmutable) + { + ErrorUtilities.ThrowInvalidOperation("OM_ProjectInstanceImmutable"); + } + } + + /// + /// Builds a list of targets with the specified loggers. + /// + internal bool Build(string[] targets, IEnumerable loggers, IEnumerable remoteLoggers, ILoggingService loggingService, int maxNodeCount, out IDictionary targetOutputs) + { + VerifyThrowNotImmutable(); + + if (null == targets) + { + targets = new string[] { }; + } + + BuildResult results; + + BuildManager buildManager = BuildManager.DefaultBuildManager; + + BuildRequestData data = new BuildRequestData(this, targets, _hostServices); + BuildParameters parameters = new BuildParameters(); + + if (loggers != null) + { + parameters.Loggers = (loggers is ICollection) ? ((ICollection)loggers) : new List(loggers); + } + + if (remoteLoggers != null) + { + parameters.ForwardingLoggers = (remoteLoggers is ICollection) ? + ((ICollection)remoteLoggers) : + new List(remoteLoggers); + } + + parameters.EnvironmentPropertiesInternal = _environmentVariableProperties; + parameters.ProjectRootElementCache = ProjectRootElementCache; + parameters.MaxNodeCount = maxNodeCount; + + results = buildManager.Build(parameters, data); + + targetOutputs = results.ResultsByTarget; + + // UNDONE: Does this need to happen in EndBuild? +#if false + Exception exception = results.Exception; + if (exception != null) + { + BuildEventContext buildEventContext = new BuildEventContext(1 /* UNDONE: NodeID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); + + InvalidProjectFileException projectException = exception as InvalidProjectFileException; + + if (projectException != null) + { + loggingService.LogInvalidProjectFileError(buildEventContext, projectException); + } + else + { + loggingService.LogFatalBuildError(buildEventContext, exception, new BuildEventFileInfo(projectFileLocation)); + } + } +#endif + + return results.OverallResult == BuildResultCode.Success; + } + + /// + /// Builds a list of targets with the specified loggers. + /// + internal bool Build(string[] targets, IEnumerable loggers, IEnumerable remoteLoggers, ILoggingService loggingService, out IDictionary targetOutputs) + { + return Build(targets, loggers, remoteLoggers, loggingService, 1, out targetOutputs); + } + + /// + /// Retrieves the list of targets which should run before the specified target. + /// Never returns null. + /// + internal IList GetTargetsWhichRunBefore(string target) + { + List beforeTargetsForTarget; + if (((IEvaluatorData)this).BeforeTargets.TryGetValue(target, out beforeTargetsForTarget)) + { + return beforeTargetsForTarget; + } + else + { + return ReadOnlyEmptyList.Instance; + } + } + + /// + /// Retrieves the list of targets which should run after the specified target. + /// Never returns null. + /// + internal IList GetTargetsWhichRunAfter(string target) + { + List afterTargetsForTarget; + if (((IEvaluatorData)this).AfterTargets.TryGetValue(target, out afterTargetsForTarget)) + { + return afterTargetsForTarget; + } + else + { + return ReadOnlyEmptyList.Instance; + } + } + + /// + /// Cache the contents of this project instance to the translator. + /// The object is retained, but the bulk of its content is released. + /// + internal void Cache(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + + if (translator.Mode == TranslationDirection.WriteToStream) + { + _globalProperties = null; + _properties = null; + _items = null; + } + } + + /// + /// Retrieve the contents of this project from the translator. + /// + internal void RetrieveFromCache(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Adds the specified target to the instance. + /// + internal ProjectTargetInstance AddTarget(string targetName, string condition, string inputs, string outputs, string returns, string keepDuplicateOutputs, string dependsOnTargets, bool parentProjectSupportsReturnsAttribute) + { + VerifyThrowNotImmutable(); + + ErrorUtilities.VerifyThrowInternalLength(targetName, "targetName"); + ErrorUtilities.VerifyThrow(!_actualTargets.ContainsKey(targetName), "Target {0} already exists.", targetName); + + ProjectTargetInstance target = new ProjectTargetInstance + ( + targetName, + condition ?? String.Empty, + inputs ?? String.Empty, + outputs ?? String.Empty, + returns, // returns may be null + keepDuplicateOutputs ?? String.Empty, + dependsOnTargets ?? String.Empty, + _projectFileLocation, + String.IsNullOrEmpty(condition) ? null : ElementLocation.EmptyLocation, + String.IsNullOrEmpty(inputs) ? null : ElementLocation.EmptyLocation, + String.IsNullOrEmpty(outputs) ? null : ElementLocation.EmptyLocation, + String.IsNullOrEmpty(returns) ? null : ElementLocation.EmptyLocation, + String.IsNullOrEmpty(keepDuplicateOutputs) ? null : ElementLocation.EmptyLocation, + String.IsNullOrEmpty(dependsOnTargets) ? null : ElementLocation.EmptyLocation, + null, + null, + new ObjectModel.ReadOnlyCollection(new List()), + new ObjectModel.ReadOnlyCollection(new List()), + parentProjectSupportsReturnsAttribute + ); + + _actualTargets[target.Name] = target; + + return target; + } + + /// + /// Removes the specified target from the instance. + /// + internal void RemoveTarget(string targetName) + { + VerifyThrowNotImmutable(); + + _actualTargets.Remove(targetName); + } + + /// + /// Throws invalid operation exception if the project instance is immutable. + /// Called before an edit. + /// + internal void VerifyThrowNotImmutable() + { + VerifyThrowNotImmutable(_isImmutable); + } + + /// + /// Generate a 4.0+-style solution wrapper project. + /// + /// The solution file to generate a wrapper for. + /// The global properties of this solution. + /// The ToolsVersion to use when generating the wrapper. + /// The logging service used to log messages etc. from the solution wrapper generator. + /// The build event context in which this project is being constructed. + /// The ProjectRootElement for the root traversal and each of the metaprojects. + private static ProjectInstance[] GenerateSolutionWrapper + ( + string projectFile, + IDictionary globalProperties, + string toolsVersion, + ILoggingService loggingService, + BuildEventContext projectBuildEventContext + ) + { + SolutionFile sp = SolutionFile.Parse(projectFile); + + // Log any comments from the solution parser + if (sp.SolutionParserComments.Count > 0) + { + foreach (string comment in sp.SolutionParserComments) + { + loggingService.LogCommentFromText(projectBuildEventContext, MessageImportance.Low, comment); + } + } + + // Pass the toolsVersion of this project through, which will be not null if there was a /tv:nn switch + // It's needed to determine which tags to put in, whether to put a ToolsVersion parameter + // on the task tags, and what MSBuildToolsPath to use when scanning child projects + // for dependency information. + ProjectInstance[] instances = SolutionProjectGenerator.Generate(sp, globalProperties, toolsVersion, projectBuildEventContext, loggingService); + return instances; + } + + /// + /// Spawn the old engine to generate a solution wrapper project, so that our build ordering is somewhat more correct + /// when solutions with toolsVersions < 4.0 are passed to us. + /// + /// + /// ############################################################################################# + /// #### Segregated into another method to avoid loading the old Engine in the regular case. #### + /// ####################### Do not move back in to the main code path! ########################## + /// ############################################################################################# + /// We have marked this method as NoInlining because we do not want Microsoft.Build.Engine.dll to be loaded unless we really execute this code path + /// + /// The solution file to generate a wrapper for. + /// The global properties of this solution. + /// The ToolsVersion to use when generating the wrapper. + /// The root element cache which should be used for the generated project. + /// The build parameters. + /// The logging service used to log messages etc. from the solution wrapper generator. + /// The build event context in which this project is being constructed. + /// An appropriate ProjectRootElement + [MethodImpl(MethodImplOptions.NoInlining)] + private static ProjectInstance[] GenerateSolutionWrapperUsingOldOM + ( + string projectFile, + IDictionary globalProperties, + string toolsVersion, + ProjectRootElementCache projectRootElementCache, + BuildParameters buildParameters, + ILoggingService loggingService, + BuildEventContext projectBuildEventContext, + bool isExplicitlyLoaded + ) + { + // Pass the toolsVersion of this project through, which will never be null -- either we passed the /tv:nn + // switch straight through, or we fabricated a ToolsVersion based on the solution version. + // It's needed to determine which tags to put in, whether to put a ToolsVersion parameter + // on the task tags, and what MSBuildToolsPath to use when scanning child projects + // for dependency information. + string wrapperProjectXml; + + List clearedVariables = null; + try + { + // We need to make sure we unset any enviroment variable which is a reserved property or has an illegal name before we call the oldOM as it may crash it. + foreach (DictionaryEntry environmentVariable in Environment.GetEnvironmentVariables()) + { + // We're going to just skip environment variables that contain names + // with characters we can't handle. There's no logger registered yet + // when this method is called, so we can't really log anything. + string environmentVariableName = environmentVariable.Key as string; + + if (environmentVariableName != null && + (!XmlUtilities.IsValidElementName(environmentVariableName) + || XMakeElements.IllegalItemPropertyNames[environmentVariableName] != null + || ReservedPropertyNames.IsReservedProperty(environmentVariableName)) + ) + { + if (clearedVariables == null) + { + clearedVariables = new List(); + } + + Environment.SetEnvironmentVariable(environmentVariableName, null); + clearedVariables.Add(environmentVariable); + } + } +#if (!STANDALONEBUILD) + wrapperProjectXml = Microsoft.Build.BuildEngine.SolutionWrapperProject.Generate(projectFile, toolsVersion, projectBuildEventContext); +#else + wrapperProjectXml = ""; +#endif + } +#if (!STANDALONEBUILD) + catch (Microsoft.Build.BuildEngine.InvalidProjectFileException ex) + { + // Whenever calling the old engine, we must translate its exception types into ours + throw new InvalidProjectFileException(ex.ProjectFile, ex.LineNumber, ex.ColumnNumber, ex.EndLineNumber, ex.EndColumnNumber, ex.Message, ex.ErrorSubcategory, ex.ErrorCode, ex.HelpKeyword, ex.InnerException); + } +#endif + finally + { + // Set the cleared environment variables back to what they were. + if (clearedVariables != null) + { + foreach (DictionaryEntry clearedVariable in clearedVariables) + { + Environment.SetEnvironmentVariable(clearedVariable.Key as string, clearedVariable.Value as string); + } + } + } + + XmlReaderSettings xrs = new XmlReaderSettings(); + xrs.DtdProcessing = DtdProcessing.Ignore; + + ProjectRootElement projectRootElement = new ProjectRootElement(XmlReader.Create(new StringReader(wrapperProjectXml), xrs), projectRootElementCache, isExplicitlyLoaded); + projectRootElement.DirectoryPath = Path.GetDirectoryName(projectFile); + ProjectInstance instance = new ProjectInstance(projectRootElement, globalProperties, toolsVersion, buildParameters, loggingService, projectBuildEventContext); + return new ProjectInstance[] { instance }; + } + + /// + /// Creates a copy of a dictionary and returns a read-only dictionary around the results. + /// + /// The value stored in the dictionary + /// Dictionary to clone. + private static ObjectModel.ReadOnlyDictionary CreateCloneDictionary(IDictionary dictionary, StringComparer strComparer) + { + Dictionary clone; + if (dictionary == null) + { + clone = new Dictionary(0); + } + else + { + clone = new Dictionary(dictionary, strComparer); + } + + return new ObjectModel.ReadOnlyDictionary(clone); + } + + /// + /// Creates a copy of a dictionary and returns a read-only dictionary around the results. + /// + /// The value stored in the dictionary + /// Dictionary to clone. + private static IDictionary CreateCloneDictionary(IDictionary dictionary) where TValue : class, IKeyed + { + if (dictionary == null) + { + return ReadOnlyEmptyDictionary.Instance; + } + else + { + return new RetrievableEntryHashSet(dictionary, OrdinalIgnoreCaseKeyedComparer.Instance, readOnly: true); + } + } + + /// + /// Common code for the constructors that evaluate directly. + /// Global properties may be null. + /// Tools version may be null. + /// Does not set mutability. + /// + private void Initialize(ProjectRootElement xml, IDictionary globalProperties, string explicitToolsVersion, string explicitSubToolsetVersion, int visualStudioVersionFromSolution, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext) + { + ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); + ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(explicitToolsVersion, "toolsVersion"); + ErrorUtilities.VerifyThrowArgumentNull(buildParameters, "buildParameters"); + + _directory = xml.DirectoryPath; + _projectFileLocation = (xml.ProjectFileLocation != null) ? xml.ProjectFileLocation : ElementLocation.EmptyLocation; + _properties = new PropertyDictionary(); + _items = new ItemDictionary(); + _actualTargets = new RetrievableEntryHashSet(OrdinalIgnoreCaseKeyedComparer.Instance); + _targets = new ObjectModel.ReadOnlyDictionary(_actualTargets); + _globalProperties = new PropertyDictionary((globalProperties == null) ? 0 : globalProperties.Count); + _environmentVariableProperties = buildParameters.EnvironmentPropertiesInternal; + _itemDefinitions = new RetrievableEntryHashSet(MSBuildNameIgnoreCaseComparer.Default); + _hostServices = buildParameters.HostServices; + this.ProjectRootElementCache = buildParameters.ProjectRootElementCache; + + string toolsVersionToUse = explicitToolsVersion; + _explicitToolsVersionSpecified = (explicitToolsVersion != null); + ElementLocation toolsVersionLocation = xml.Location; + + if (xml.ToolsVersion.Length > 0) + { + _originalProjectToolsVersion = xml.ToolsVersion; + toolsVersionLocation = xml.ToolsVersionLocation; + } + + toolsVersionToUse = Utilities.GenerateToolsVersionToUse + ( + explicitToolsVersion, + xml.ToolsVersion, + buildParameters.GetToolset, + buildParameters.DefaultToolsVersion + ); + + // Don't log the message if the toolsversion is different because an explicit toolsversion was specified -- + // in that case the user already knows what they're doing; the point of this warning is to give them a heads + // up if we're doing this ourselves for our own reasons. + if (!_explicitToolsVersionSpecified && !String.Equals(_originalProjectToolsVersion, toolsVersionToUse, StringComparison.OrdinalIgnoreCase)) + { + _usingDifferentToolsVersionFromProjectFile = true; + } + + this.Toolset = buildParameters.GetToolset(toolsVersionToUse); + + if (this.Toolset == null) + { + string toolsVersionList = Utilities.CreateToolsVersionListString(buildParameters.Toolsets); + ProjectErrorUtilities.ThrowInvalidProject(toolsVersionLocation, "UnrecognizedToolsVersion", toolsVersionToUse, toolsVersionList); + } + + if (explicitSubToolsetVersion != null) + { + this.SubToolsetVersion = explicitSubToolsetVersion; + } + else + { + this.SubToolsetVersion = this.Toolset.GenerateSubToolsetVersionUsingVisualStudioVersion(globalProperties, visualStudioVersionFromSolution); + } + + // Create a task registry which will fall back on the toolset task registry if necessary. + this.TaskRegistry = new TaskRegistry(this.Toolset, ProjectRootElementCache); + + if (globalProperties != null) + { + foreach (KeyValuePair globalProperty in globalProperties) + { + if (String.Equals(globalProperty.Key, Constants.SubToolsetVersionPropertyName, StringComparison.OrdinalIgnoreCase) && explicitSubToolsetVersion != null) + { + // if we have a sub-toolset version explicitly provided by the ProjectInstance constructor, AND a sub-toolset version provided as a global property, + // make sure that the one passed in with the constructor wins. If there isn't a matching global property, the sub-toolset version will be set at + // a later point. + _globalProperties.Set(ProjectPropertyInstance.Create(globalProperty.Key, explicitSubToolsetVersion, false /* may not be reserved */, _isImmutable)); + } + else + { + _globalProperties.Set(ProjectPropertyInstance.Create(globalProperty.Key, globalProperty.Value, false /* may not be reserved */, _isImmutable)); + } + } + } + + if (Evaluator.DebugEvaluation) + { + Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Creating a ProjectInstance from an unevaluated state [{0}]", FullPath)); + } + + _initialGlobalsForDebugging = Evaluator.Evaluate(this, xml, ProjectLoadSettings.Default, buildParameters.MaxNodeCount, buildParameters.EnvironmentPropertiesInternal, loggingService, new ProjectItemInstanceFactory(this), buildParameters.ToolsetProvider, ProjectRootElementCache, buildEventContext, this /* for debugging only */); + } + + /// + /// Get items by evaluatedInclude value + /// + private ICollection GetItemsByEvaluatedInclude(string evaluatedInclude) + { + // Even if there are no items in itemsByEvaluatedInclude[], it will return an IEnumerable, which is non-null + return new ReadOnlyCollection(_itemsByEvaluatedInclude[evaluatedInclude]); + } + + /// + /// Create various target snapshots + /// + private void CreateTargetsSnapshot(Evaluation.Project.Data data) + { + this.DefaultTargets = new List(data.DefaultTargets); + this.InitialTargets = new List(data.InitialTargets); + ((IEvaluatorData)this).BeforeTargets = CreateCloneDictionary(data.BeforeTargets, StringComparer.OrdinalIgnoreCase); + ((IEvaluatorData)this).AfterTargets = CreateCloneDictionary(data.AfterTargets, StringComparer.OrdinalIgnoreCase); + + // ProjectTargetInstances are immutable so only the dictionary must be cloned + _targets = CreateCloneDictionary(data.Targets); + } + + /// + /// Create environment variable properties snapshot + /// + private void CreateEnvironmentVariablePropertiesSnapshot(PropertyDictionary environmentVariableProperties) + { + _environmentVariableProperties = new PropertyDictionary(environmentVariableProperties.Count); + + foreach (ProjectPropertyInstance environmentProperty in environmentVariableProperties) + { + _environmentVariableProperties.Set(environmentProperty.DeepClone()); + } + } + + /// + /// Create global properties snapshot + /// + private void CreateGlobalPropertiesSnapshot(Evaluation.Project.Data data) + { + _globalProperties = new PropertyDictionary(data.GlobalPropertiesDictionary.Count); + + foreach (ProjectPropertyInstance globalProperty in data.GlobalPropertiesDictionary) + { + _globalProperties.Set(globalProperty.DeepClone()); + } + } + + /// + /// Create evaluated include cache snapshot + /// + private void CreateEvaluatedIncludeSnapshotIfRequested(bool keepEvaluationCache, Evaluation.Project.Data data, Dictionary projectItemToInstanceMap) + { + if (!keepEvaluationCache) + { + return; + } + + _itemsByEvaluatedInclude = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + foreach (var key in data.ItemsByEvaluatedIncludeCache.Keys) + { + var projectItems = data.ItemsByEvaluatedIncludeCache[key]; + foreach (var projectItem in projectItems) + { + _itemsByEvaluatedInclude.Add(key, projectItemToInstanceMap[projectItem]); + } + } + } + + /// + /// Create Items snapshot + /// + private Dictionary CreateItemsSnapshot(Evaluation.Project.Data data, bool keepEvaluationCache) + { + _items = new ItemDictionary(data.ItemTypes.Count); + + var projectItemToInstanceMap = keepEvaluationCache ? new Dictionary(data.Items.Count) : null; + + foreach (ProjectItem item in data.Items) + { + List inheritedItemDefinitions = null; + + if (item.InheritedItemDefinitions != null) + { + inheritedItemDefinitions = new List(item.InheritedItemDefinitions.Count); + + foreach (ProjectItemDefinition inheritedItemDefinition in item.InheritedItemDefinitions) + { + // All item definitions in this list should be present in the collection of item definitions + // on the project we are cloning. + inheritedItemDefinitions.Add(_itemDefinitions[inheritedItemDefinition.ItemType]); + } + } + + CopyOnWritePropertyDictionary directMetadata = null; + + if (item.DirectMetadata != null) + { + directMetadata = new CopyOnWritePropertyDictionary(item.DirectMetadataCount); + foreach (ProjectMetadata directMetadatum in item.DirectMetadata) + { + ProjectMetadataInstance directMetadatumInstance = new ProjectMetadataInstance(directMetadatum); + directMetadata.Set(directMetadatumInstance); + } + } + + ProjectItemInstance instance = new ProjectItemInstance(this, item.ItemType, ((IItem)item).EvaluatedIncludeEscaped, item.EvaluatedIncludeBeforeWildcardExpansionEscaped, directMetadata, inheritedItemDefinitions, ProjectCollection.Escape(item.Xml.ContainingProject.FullPath)); + + _items.Add(instance); + + if (projectItemToInstanceMap != null) + { + projectItemToInstanceMap.Add(item, instance); + } + } + + return projectItemToInstanceMap; + } + + /// + /// Create ItemDefinitions snapshot + /// + private void CreateItemDefinitionsSnapshot(Evaluation.Project.Data data) + { + _itemDefinitions = new RetrievableEntryHashSet(MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectItemDefinition definition in data.ItemDefinitions.Values) + { + _itemDefinitions.Add(new ProjectItemDefinitionInstance(this, definition)); + } + } + + /// + /// create property snapshot + /// + private void CreatePropertiesSnapshot(Evaluation.Project.Data data, bool isImmutable) + { + _properties = new PropertyDictionary(data.Properties.Count); + + foreach (ProjectProperty property in data.Properties) + { + // Allow reserved property names, since this is how they are added to the project instance. + // The caller has prevented users setting them themselves. + ProjectPropertyInstance instance = ProjectPropertyInstance.Create(property.Name, ((IProperty)property).EvaluatedValueEscaped, true /* MAY be reserved name */, isImmutable); + _properties.Set(instance); + } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectItemDefinitionInstance.cs b/src/XMakeBuildEngine/Instance/ProjectItemDefinitionInstance.cs new file mode 100644 index 00000000000..b1e3554e522 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectItemDefinitionInstance.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a set of item definitions all applying to the same item-type. +//----------------------------------------------------------------------- + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System; + +namespace Microsoft.Build.Execution +{ + /// + /// An evaluated item definition for a particular item-type, divested of all references to XML. + /// Immutable. + /// + [DebuggerDisplay("{itemType} #Metadata={MetadataCount}")] + public class ProjectItemDefinitionInstance : IKeyed, IMetadataTable, IItemDefinition + { + /// + /// Item type, for example "Compile", that this item definition applies to + /// + private readonly string _itemType; + + /// + /// Collection of metadata that link the XML metadata and instance metadata + /// Since evaluation has occurred, this is an unordered collection. + /// Is never null or empty. + /// + private CopyOnWritePropertyDictionary _metadata; + + /// + /// Constructs an empty project item definition instance. + /// + /// The project instance to which this item definition belongs. + /// The type of item this definition object represents. + internal ProjectItemDefinitionInstance(ProjectInstance projectInstance, string itemType) + { + ErrorUtilities.VerifyThrowArgumentNull(itemType, "itemType"); + + _itemType = itemType; + } + + /// + /// Called when a ProjectInstance is created. + /// + /// + /// Assumes that the itemType string originated in a ProjectItemDefinitionElement and therefore + /// was already validated. + /// + internal ProjectItemDefinitionInstance(ProjectInstance projectInstance, ProjectItemDefinition itemDefinition) + : this(projectInstance, itemDefinition.ItemType) + { + if (itemDefinition.MetadataCount > 0) + { + _metadata = new CopyOnWritePropertyDictionary(itemDefinition.MetadataCount); + } + + foreach (ProjectMetadata originalMetadata in itemDefinition.Metadata) + { + _metadata.Set(new ProjectMetadataInstance(originalMetadata)); + } + } + + /// + /// Type of this item definition. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public string ItemType + { + [DebuggerStepThrough] + get + { return _itemType; } + } + + /// + /// Metadata on the item definition. + /// If there is no metadata, returns empty collection. + /// This is a read-only collection. + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public ICollection Metadata + { + get + { + if (_metadata == null) + { + return ReadOnlyEmptyCollection.Instance; + } + + return new ReadOnlyCollection(_metadata); + } + } + + /// + /// Number of pieces of metadata on this item definition. + /// + public int MetadataCount + { + get { return (_metadata == null) ? 0 : _metadata.Count; } + } + + /// + /// Names of all metadata on this item definition + /// + public IEnumerable MetadataNames + { + get + { + if (_metadata == null) + { + yield break; + } + + foreach (ProjectMetadataInstance metadatum in _metadata) + { + yield return metadatum.Name; + } + } + } + + /// + /// Implementation of IKeyed exposing the item type, so these + /// can be put in a dictionary conveniently. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + get { return ItemType; } + } + + /// + /// Get any metadata in the item that has the specified name, + /// otherwise returns null + /// + [DebuggerStepThrough] + public ProjectMetadataInstance GetMetadata(string name) + { + return (_metadata == null) ? null : _metadata[name]; + } + + #region IMetadataTable Members + + /// + /// Returns the specified metadata. + /// + /// The metadata name. + /// The metadata value, or an empty string if none exists. + string IMetadataTable.GetEscapedValue(string name) + { + return ((IMetadataTable)this).GetEscapedValue(null, name); + } + + /// + /// Returns the metadata for the specified item type. + /// + /// The item type. + /// The metadata name. + /// The metadata value, or an empty string if none exists. + string IMetadataTable.GetEscapedValue(string specifiedItemType, string name) + { + return ((IMetadataTable)this).GetEscapedValueIfPresent(specifiedItemType, name) ?? String.Empty; + } + + /// + /// Returns the metadata for the specified item type. + /// + /// The item type. + /// The metadata name. + /// The metadata value, or an null if none exists. + string IMetadataTable.GetEscapedValueIfPresent(string specifiedItemType, string name) + { + if (specifiedItemType == null || String.Equals(_itemType, specifiedItemType, StringComparison.OrdinalIgnoreCase)) + { + ProjectMetadataInstance metadatum = GetMetadata(name); + if (metadatum != null) + { + return metadatum.EvaluatedValueEscaped; + } + } + + return null; + } + + #endregion + + /// + /// Sets a new metadata value. Called by the evaluator only. + /// Discards predecessor as this information is only useful at design time. + /// + ProjectMetadataInstance IItemDefinition.SetMetadata(ProjectMetadataElement xml, string evaluatedValue, ProjectMetadataInstance predecessor) + { + // No mutability check as this is used during creation (evaluation) + _metadata = _metadata ?? new CopyOnWritePropertyDictionary(); + + ProjectMetadataInstance metadatum = new ProjectMetadataInstance(xml.Name, evaluatedValue); + _metadata[xml.Name] = metadatum; + + return metadatum; + } + + /// + /// Creates a ProjectItemDefinitionElement representing this instance. + /// + internal ProjectItemDefinitionElement ToProjectItemDefinitionElement(ProjectElementContainer parent) + { + ProjectItemDefinitionElement element = parent.ContainingProject.CreateItemDefinitionElement(ItemType); + parent.AppendChild(element); + foreach (ProjectMetadataInstance metadataInstance in _metadata) + { + element.AddMetadata(metadataInstance.Name, metadataInstance.EvaluatedValue); + } + + return element; + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskInstance.cs b/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskInstance.cs new file mode 100644 index 00000000000..fdc4731a5fc --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskInstance.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an unevaluated itemgroup under a target. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an unevaluated itemgroup under a target. + /// Immutable. + /// + [DebuggerDisplay("Condition={condition}")] + public class ProjectItemGroupTaskInstance : ProjectTargetInstanceChild + { + /// + /// Condition, if any + /// + private readonly string _condition; + + /// + /// Child items. + /// Not ProjectItemInstances, as these are evaluated during the build. + /// + private readonly ICollection _items; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Constructor called by the Evaluator. + /// Assumes ProjectItemGroupTaskItemInstance is an immutable type. + /// + internal ProjectItemGroupTaskInstance + ( + string condition, + ElementLocation location, + ElementLocation conditionLocation, + IEnumerable items + ) + { + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + ErrorUtilities.VerifyThrowInternalNull(items, "items"); + + _condition = condition; + _location = location; + _conditionLocation = conditionLocation; + + if (items != null) + { + _items = (items is ICollection) ? + ((ICollection)items) : + new List(items); + } + } + + /// + /// Cloning constructor + /// + private ProjectItemGroupTaskInstance(ProjectItemGroupTaskInstance that) + { + // All members are immutable + _condition = that._condition; + _items = that._items; + } + + /// + /// Condition, if any. + /// May be empty string. + /// + public override string Condition + { + [DebuggerStepThrough] + get + { return _condition; } + } + + /// + /// Child items + /// + public ICollection Items + { + [DebuggerStepThrough] + get + { + return (_items == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new ReadOnlyCollection(_items); + } + } + + /// + /// Location of the original element + /// + public override ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public override ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Deep clone + /// + internal ProjectItemGroupTaskInstance DeepClone() + { + return new ProjectItemGroupTaskInstance(this); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskItemInstance.cs b/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskItemInstance.cs new file mode 100644 index 00000000000..b772e1f46f5 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskItemInstance.cs @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an unevaluated item under an itemgroup in a target. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an unevaluated item under an itemgroup in a target. + /// Immutable. + /// + [DebuggerDisplay("{itemType} Include={include} Exclude={exclude} Remove={remove} Condition={condition}")] + public class ProjectItemGroupTaskItemInstance + { + /// + /// Item type, for example "Compile" + /// + private readonly string _itemType; + + /// + /// Unevaluated include + /// + private readonly string _include; + + /// + /// Unevaluated exclude + /// + private readonly string _exclude; + + /// + /// Unevaluated remove + /// + private readonly string _remove; + + /// + /// The list of metadata to keep. + /// + private readonly string _keepMetadata; + + /// + /// The list of metadata to remove. + /// + private readonly string _removeMetadata; + + /// + /// True to remove duplicates during the add. + /// + private readonly string _keepDuplicates; + + /// + /// Unevaluated condition + /// + private readonly string _condition; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the include, if any + /// + private readonly ElementLocation _includeLocation; + + /// + /// Location of the exclude, if any + /// + private readonly ElementLocation _excludeLocation; + + /// + /// Location of the remove, if any + /// + private readonly ElementLocation _removeLocation; + + /// + /// Location of keepMetadata, if any + /// + private readonly ElementLocation _keepMetadataLocation; + + /// + /// Location of removeMetadata, if any + /// + private readonly ElementLocation _removeMetadataLocation; + + /// + /// Location of keepDuplicates, if any + /// + private readonly ElementLocation _keepDuplicatesLocation; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Ordered collection of unevaluated metadata. + /// May be null. + /// + /// + /// There is no need for a PropertyDictionary here as the build always + /// walks through all metadata sequentially. + /// Lazily created, as so many items have no metadata at all. + /// + private ICollection _metadata; + + /// + /// Constructor called by the Evaluator. + /// Metadata may be null, indicating no metadata. + /// Metadata collection is ordered. + /// Assumes ProjectItemGroupTaskMetadataInstance is an immutable type. + /// + internal ProjectItemGroupTaskItemInstance + ( + string itemType, + string include, + string exclude, + string remove, + string keepMetadata, + string removeMetadata, + string keepDuplicates, + string condition, + ElementLocation location, + ElementLocation includeLocation, + ElementLocation excludeLocation, + ElementLocation removeLocation, + ElementLocation keepMetadataLocation, + ElementLocation removeMetadataLocation, + ElementLocation keepDuplicatesLocation, + ElementLocation conditionLocation, + IEnumerable metadata + ) + { + ErrorUtilities.VerifyThrowInternalNull(itemType, "itemType"); + ErrorUtilities.VerifyThrowInternalNull(include, "include"); + ErrorUtilities.VerifyThrowInternalNull(exclude, "exclude"); + ErrorUtilities.VerifyThrowInternalNull(remove, "remove"); + ErrorUtilities.VerifyThrowInternalNull(keepMetadata, "keepMetadata"); + ErrorUtilities.VerifyThrowInternalNull(removeMetadata, "removeMetadata"); + ErrorUtilities.VerifyThrowInternalNull(keepDuplicates, "keepDuplicates"); + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + + _itemType = itemType; + _include = include; + _exclude = exclude; + _remove = remove; + _keepMetadata = keepMetadata; + _removeMetadata = removeMetadata; + _keepDuplicates = keepDuplicates; + _condition = condition; + _location = location; + _includeLocation = includeLocation; + _excludeLocation = excludeLocation; + _removeLocation = removeLocation; + _keepMetadataLocation = keepMetadataLocation; + _removeMetadataLocation = removeMetadataLocation; + _keepDuplicatesLocation = keepDuplicatesLocation; + _conditionLocation = conditionLocation; + + if (metadata != null) + { + _metadata = (metadata is ICollection) ? + ((ICollection)metadata) : + new List(metadata); + } + } + + /// + /// Cloning constructor + /// + private ProjectItemGroupTaskItemInstance(ProjectItemGroupTaskItemInstance that) + { + // All fields are immutable + _itemType = that._itemType; + _include = that._include; + _exclude = that._exclude; + _remove = that._remove; + _keepMetadata = that._keepMetadata; + _removeMetadata = that._removeMetadata; + _keepDuplicates = that._keepDuplicates; + _condition = that._condition; + _metadata = that._metadata; + } + + /// + /// Item type, for example "Compile" + /// + public string ItemType + { + [DebuggerStepThrough] + get + { return _itemType; } + } + + /// + /// Unevaluated include value + /// + public string Include + { + [DebuggerStepThrough] + get + { return _include; } + } + + /// + /// Unevaluated exclude value + /// + public string Exclude + { + [DebuggerStepThrough] + get + { return _exclude; } + } + + /// + /// Unevaluated remove value + /// + public string Remove + { + [DebuggerStepThrough] + get + { return _remove; } + } + + /// + /// Unevaluated keepMetadata value + /// + public string KeepMetadata + { + [DebuggerStepThrough] + get + { return _keepMetadata; } + } + + /// + /// Unevaluated removeMetadata value + /// + public string RemoveMetadata + { + [DebuggerStepThrough] + get + { return _removeMetadata; } + } + + /// + /// Unevaluated keepDuplicates value + /// + public string KeepDuplicates + { + [DebuggerStepThrough] + get + { return _keepDuplicates; } + } + + /// + /// Unevaluated condition value + /// + public string Condition + { + [DebuggerStepThrough] + get + { return _condition; } + } + + /// + /// Ordered collection of unevaluated metadata on the item. + /// If there is no metadata, returns an empty collection. + /// IEnumerable + public ICollection Metadata + { + [DebuggerStepThrough] + get + { + return (_metadata == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new ReadOnlyCollection(_metadata); + } + } + + /// + /// Location of the element + /// + public ElementLocation Location + { + [DebuggerStepThrough] + get + { return _location; } + } + + /// + /// Location of the include attribute, if any + /// + public ElementLocation IncludeLocation + { + [DebuggerStepThrough] + get + { return _includeLocation; } + } + + /// + /// Location of the exclude attribute, if any + /// + public ElementLocation ExcludeLocation + { + [DebuggerStepThrough] + get + { return _excludeLocation; } + } + + /// + /// Location of the remove attribute, if any + /// + public ElementLocation RemoveLocation + { + [DebuggerStepThrough] + get + { return _removeLocation; } + } + + /// + /// Location of the keepMetadata attribute, if any + /// + public ElementLocation KeepMetadataLocation + { + [DebuggerStepThrough] + get + { return _keepMetadataLocation; } + } + + /// + /// Location of the removeMetadata attribute, if any + /// + public ElementLocation RemoveMetadataLocation + { + [DebuggerStepThrough] + get + { return _removeMetadataLocation; } + } + + /// + /// Location of the keepDuplicates attribute, if any + /// + public ElementLocation KeepDuplicatesLocation + { + [DebuggerStepThrough] + get + { return _keepDuplicatesLocation; } + } + + /// + /// Location of the condition attribute if any + /// + public ElementLocation ConditionLocation + { + [DebuggerStepThrough] + get + { return _conditionLocation; } + } + + /// + /// Deep clone + /// + internal ProjectItemGroupTaskItemInstance DeepClone() + { + return new ProjectItemGroupTaskItemInstance(this); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskMetadataInstance.cs b/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskMetadataInstance.cs new file mode 100644 index 00000000000..eef9cd90511 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectItemGroupTaskMetadataInstance.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an unevaluated metadatum under an item in an itemgroup in a target. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an unevaluated metadatum under an item in an itemgroup in a target + /// Immutable. + /// + [DebuggerDisplay("{name} Value={value} Condition={condition}")] + public class ProjectItemGroupTaskMetadataInstance + { + /// + /// Name of the metadatum + /// + private readonly string _name; + + /// + /// Unevaluated value + /// + private readonly string _value; + + /// + /// Unevaluated condition + /// + private readonly string _condition; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Constructor called by the Evaluator. + /// + internal ProjectItemGroupTaskMetadataInstance(string name, string value, string condition, ElementLocation location, ElementLocation conditionLocation) + { + ErrorUtilities.VerifyThrowInternalNull(name, "name"); + ErrorUtilities.VerifyThrowInternalNull(value, "value"); + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + + _name = name; + _value = value; + _condition = condition; + _location = location; + _conditionLocation = conditionLocation; + } + + /// + /// Cloning constructor + /// + private ProjectItemGroupTaskMetadataInstance(ProjectItemGroupTaskMetadataInstance that) + { + // All fields are immutable + _name = that._name; + _value = that._value; + _condition = that._condition; + } + + /// + /// Name of the metadatum + /// + public string Name + { + [DebuggerStepThrough] + get + { return _name; } + } + + /// + /// Unevaluated value + /// + public string Value + { + [DebuggerStepThrough] + get + { return _value; } + } + + /// + /// Unevaluated condition value + /// + public string Condition + { + [DebuggerStepThrough] + get + { return _condition; } + } + + /// + /// Location of the element + /// + public ElementLocation Location + { + [DebuggerStepThrough] + get + { return _location; } + } + + /// + /// Location of the condition attribute if any + /// + public ElementLocation ConditionLocation + { + [DebuggerStepThrough] + get + { return _conditionLocation; } + } + + /// + /// Deep clone + /// + internal ProjectItemGroupTaskMetadataInstance DeepClone() + { + return new ProjectItemGroupTaskMetadataInstance(this); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectItemInstance.cs b/src/XMakeBuildEngine/Instance/ProjectItemInstance.cs new file mode 100644 index 00000000000..efd1c5ee5b1 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectItemInstance.cs @@ -0,0 +1,2143 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an evaluated item. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Construction; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an evaluated item for build purposes + /// + /// + /// Does not store XML location information. That is not needed by the build process as all correctness checks + /// and evaluation has already been performed, so it is unnecessary bulk. + /// + [DebuggerDisplay("{ItemType}={EvaluatedInclude} #DirectMetadata={DirectMetadataCount})")] + public class ProjectItemInstance : IKeyed, IItem, ITaskItem, ITaskItem2, IMetadataTable, INodePacketTranslatable, IDeepCloneable + { + /// + /// The project instance to which this item belongs. + /// Never null. + /// + private ProjectInstance _project; + + /// + /// Item type, for example "Compile" + /// Never null. + /// + private string _itemType; + + /// + /// Backing task item holding the other data. + /// Never null. + /// + private TaskItem _taskItem; + + /// + /// Constructor for items with no metadata. + /// Include may be empty. + /// Called before the build when virtual items are added, + /// and during the build when tasks emit items. + /// Mutability follows the project. + /// + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string definingFileEscaped) + : this(project, itemType, includeEscaped, includeEscaped, definingFileEscaped) + { + } + + /// + /// Constructor for items with no metadata. + /// Include may be empty. + /// Called before the build when virtual items are added, + /// and during the build when tasks emit items. + /// Mutability follows the project. + /// + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string includeBeforeWildcardExpansionEscaped, string definingFileEscaped) + : this(project, itemType, includeEscaped, includeBeforeWildcardExpansionEscaped, null /* no direct metadata */, null /* need to add item definition metadata */, definingFileEscaped) + { + } + + /// + /// Constructor for items with metadata. + /// Called before the build when virtual items are added, + /// and during the build when tasks emit items. + /// Include may be empty. + /// Direct metadata may be null, indicating no metadata. It will be cloned. + /// Builtin metadata may be null, indicating it has not been populated. It will be cloned. + /// Inherited item definition metadata may be null. It is assumed to ALREADY HAVE BEEN CLONED. + /// Mutability follows the project. + /// + /// + /// Not public since the only creation scenario is setting on a project. + /// + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, string includeBeforeWildcardExpansionEscaped, CopyOnWritePropertyDictionary directMetadata, List itemDefinitions, string definingFileEscaped) + { + CommonConstructor(project, itemType, includeEscaped, includeBeforeWildcardExpansionEscaped, directMetadata, itemDefinitions, definingFileEscaped); + } + + /// + /// Constructor for items with metadata. + /// Called when a ProjectInstance is created. + /// Include may be empty. + /// Direct metadata may be null, indicating no metadata. It will be cloned. + /// Metadata collection provided is cloned. + /// Mutability follows the project. + /// + /// + /// Not public since the only creation scenario is setting on a project. + /// + internal ProjectItemInstance(ProjectInstance project, string itemType, string includeEscaped, IEnumerable> directMetadata, string definingFileEscaped) + { + CopyOnWritePropertyDictionary metadata = null; + + if (directMetadata != null && directMetadata.GetEnumerator().MoveNext()) + { + metadata = new CopyOnWritePropertyDictionary(directMetadata.FastCountOrZero()); + foreach (KeyValuePair metadatum in directMetadata) + { + metadata.Set(new ProjectMetadataInstance(metadatum.Key, metadatum.Value)); + } + } + + CommonConstructor(project, itemType, includeEscaped, includeEscaped, metadata, null /* need to add item definition metadata */, definingFileEscaped); + } + + /// + /// Cloning constructor, retaining same parentage. + /// + private ProjectItemInstance(ProjectItemInstance that) + : this(that, that._project) + { + } + + /// + /// Cloning constructor. + /// + private ProjectItemInstance(ProjectItemInstance that, ProjectInstance newProject) + { + _project = newProject; + _itemType = that._itemType; + _taskItem = that._taskItem.DeepClone(newProject.IsImmutable); + } + + /// + /// Constructor for serialization + /// + private ProjectItemInstance(ProjectInstance projectInstance) + { + _project = projectInstance; + + // Deserialization continues + } + + /// + /// Owning project + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public ProjectInstance Project + { + get { return _project; } + } + + /// + /// Item type, for example "Compile" + /// + /// + /// This cannot be set, as it is used as the key into + /// the project's items table. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public string ItemType + { + [DebuggerStepThrough] + get + { return _itemType; } + } + + /// + /// Evaluated include value. + /// May be empty string. + /// + public string EvaluatedInclude + { + [DebuggerStepThrough] + get + { + return _taskItem.ItemSpec; + } + + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, "EvaluatedInclude"); + _project.VerifyThrowNotImmutable(); + + _taskItem.ItemSpec = value; + } + } + + /// + /// Evaluated include value, escaped as necessary. + /// May be empty string. + /// + string IItem.EvaluatedIncludeEscaped + { + [DebuggerStepThrough] + get + { return _taskItem.IncludeEscaped; } + } + + /// + /// Evaluated include value, escaped as necessary. + /// May be empty string. + /// + string ITaskItem2.EvaluatedIncludeEscaped + { + [DebuggerStepThrough] + get + { + return _taskItem.IncludeEscaped; + } + + set + { + _project.VerifyThrowNotImmutable(); + + _taskItem.IncludeEscaped = value; + } + } + + /// + /// Unordered collection of evaluated metadata on the item. + /// If there is no metadata, returns an empty collection. + /// Does not include built-in metadata. + /// Includes any from item definitions. + /// This is a read-only collection. To modify the metadata, use . + /// + /// + /// Computed, not necessarily fast. + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")] + public IEnumerable Metadata + { + get { return _taskItem.MetadataCollection; } + } + + /// + /// Number of pieces of metadata on this item + /// + public int DirectMetadataCount + { + get { return _taskItem.DirectMetadataCount; } + } + + /// + /// Implementation of IKeyed exposing the item type + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + get { return ItemType; } + } + + /// + /// Returns all the metadata names on this item. + /// Includes names from any applicable item definitions. + /// Includes names of built-in metadata. + /// + /// + /// Computed, not necessarily fast. + /// + public ICollection MetadataNames + { + get { return new ReadOnlyCollection(_taskItem.MetadataNames.Cast()); } + } + + /// + /// ITaskItem implementation + /// + string ITaskItem.ItemSpec + { + get + { + return EvaluatedInclude; + } + + set + { + _project.VerifyThrowNotImmutable(); + + EvaluatedInclude = value; + } + } + + /// + /// ITaskItem implementation + /// + /// + /// Computed, not necessarily fast. + /// + ICollection ITaskItem.MetadataNames + { + get { return new List(MetadataNames); } + } + + /// + /// Returns the number of metadata entries. + /// Includes any from applicable item definitions. + /// Includes both custom and built-in metadata. + /// + /// + /// Computed, not necessarily fast. + /// + public int MetadataCount + { + get { return _taskItem.MetadataCount; } + } + + /// + /// The directory of the project being built + /// Never null: If there is no project filename yet, it will use the current directory + /// + string IItem.ProjectDirectory + { + get { return _project.Directory; } + } + + /// + /// Retrieves the comparer used for determining equality between ProjectItemInstances. + /// + internal static IEqualityComparer EqualityComparer + { + get { return ProjectItemInstanceEqualityComparer.Default; } + } + + /// + /// The full path to the project file being built + /// Can be null: if the project hasn't been saved yet it will be null + /// + internal string ProjectFullPath + { + get { return _project.FullPath; } + } + + /// + /// Get any metadata in the item that has the specified name, + /// otherwise returns null. + /// Includes any metadata inherited from item definitions. + /// Includes any built-in metadata. + /// + public ProjectMetadataInstance GetMetadata(string name) + { + return _taskItem.GetMetadataObject(name); + } + + /// + /// Get the value of a metadata on this item, or + /// String.Empty if it does not exist or has no value. + /// Includes any metadata inherited from item definitions and any built-in metadata. + /// To determine whether a piece of metadata is actually present + /// but with an empty value, use HasMetadata. + /// + public string GetMetadataValue(string name) + { + return _taskItem.GetMetadata(name); + } + + /// + /// Returns true if a particular piece of metadata is defined on this item (even if + /// its value is empty string) otherwise false. + /// This includes built-in metadata and metadata from item definitions. + /// + /// + /// It has to include all of these because it's used for batching, which doesn't + /// care where the metadata originated. + /// + public bool HasMetadata(string name) + { + return _taskItem.HasMetadata(name); + } + + /// + /// Add a metadata with the specified name and value. + /// Overwrites any metadata with the same name already in the collection. + /// + public ProjectMetadataInstance SetMetadata(string name, string evaluatedValue) + { + _project.VerifyThrowNotImmutable(); + + return _taskItem.SetMetadataObject(name, evaluatedValue, false /* built-in metadata not allowed */); + } + + /// + /// Add a metadata with the specified names and values. + /// Overwrites any metadata with the same name already in the collection. + /// + public void SetMetadata(IEnumerable> metadataDictionary) + { + _project.VerifyThrowNotImmutable(); + + _taskItem.SetMetadata(metadataDictionary); + } + + /// + /// Removes a metadatum with the specified name. + /// Used by TaskItem + /// + public void RemoveMetadata(string metadataName) + { + _project.VerifyThrowNotImmutable(); + + _taskItem.RemoveMetadata(metadataName); + } + + /// + /// Produce a string representation. + /// + public override string ToString() + { + return _taskItem.ToString(); + } + + /// + /// Get the value of a metadata on this item, or + /// String.Empty if it does not exist or has no value. + /// Includes any metadata inherited from item definitions and any built-in metadata. + /// To determine whether a piece of metadata is actually present + /// but with an empty value, use HasMetadata. + /// + string IItem.GetMetadataValueEscaped(string name) + { + return _taskItem.GetMetadataEscaped(name); + } + + /// + /// Sets the specified metadata. Discards the xml part except for the name. + /// Discards the location of the original element. This is not interesting in the Execution world + /// as it should never be needed for any messages, and is just extra bulk. + /// Predecessor is discarded as it is only needed for design time. + /// + ProjectMetadataInstance IItem.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedInclude) + { + _project.VerifyThrowNotImmutable(); + + return SetMetadata(metadataElement.Name, evaluatedInclude); + } + + /// + /// ITaskItem implementation. + /// + /// + /// ITaskItem should not return null if metadata is not present. + /// + string ITaskItem.GetMetadata(string metadataName) + { + return GetMetadataValue(metadataName); + } + + /// + /// ITaskItem2 implementation. + /// + /// + /// ITaskItem2 should not return null if metadata is not present. + /// + string ITaskItem2.GetMetadataValueEscaped(string name) + { + return _taskItem.GetMetadataEscaped(name); + } + + /// + /// ITaskItem implementation + /// + /// + /// MetadataValue is assumed to be in its escaped form. + /// + void ITaskItem.SetMetadata(string metadataName, string metadataValue) + { + SetMetadata(metadataName, metadataValue); + } + + /// + /// ITaskItem2 implementation + /// + /// + /// Assumes metadataValue is unescaped. + /// + void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) + { + _project.VerifyThrowNotImmutable(); + + ((ITaskItem2)_taskItem).SetMetadataValueLiteral(metadataName, metadataValue); + } + + /// + /// ITaskItem implementation + /// + void ITaskItem.CopyMetadataTo(ITaskItem destinationItem) + { + _taskItem.CopyMetadataTo(destinationItem); + } + + /// + /// ITaskItem implementation + /// + /// + /// Returns a dictionary of the UNESCAPED values of the metadata + /// + IDictionary ITaskItem.CloneCustomMetadata() + { + return _taskItem.CloneCustomMetadata(); + } + + /// + /// ITaskItem2 implementation + /// + /// + /// Returns a dictionary of the ESCAPED values of the metadata + /// + IDictionary ITaskItem2.CloneCustomMetadataEscaped() + { + return ((ITaskItem2)_taskItem).CloneCustomMetadataEscaped(); + } + + #region IMetadataTable Members + + /// + /// Retrieves any value we have in our metadata table for the metadata name specified. + /// If no value is available, returns empty string. + /// + string IMetadataTable.GetEscapedValue(string name) + { + return _taskItem.GetMetadataEscaped(name); + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If no value is available, returns empty string. + /// If item type is null, it is ignored, otherwise it must match. + /// + string IMetadataTable.GetEscapedValue(string itemType, string name) + { + string value = ((IMetadataTable)this).GetEscapedValueIfPresent(itemType, name); + + return value ?? String.Empty; + } + + /// + /// Returns the value if it exists. + /// If no value is available, returns null. + /// If item type is null, it is ignored, otherwise it must match. + /// + string IMetadataTable.GetEscapedValueIfPresent(string itemType, string name) + { + if (itemType == null || String.Equals(itemType, _itemType, StringComparison.OrdinalIgnoreCase)) + { + string value = _taskItem.GetMetadataEscaped(name); + + if (value.Length > 0 || HasMetadata(name)) + { + return value; + } + } + + return null; + } + + #endregion + + #region INodePacketTranslatable Members + + /// + /// Translation method. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _itemType); + translator.Translate(ref _taskItem, TaskItem.FactoryForDeserialization); + } + + #endregion + + #region IDeepCloneable + + /// + /// Deep clone the item. + /// Any metadata inherited from item definitions are also copied. + /// + ProjectItemInstance IDeepCloneable.DeepClone() + { + return DeepClone(); + } + + #endregion + + /// + /// Set all the supplied metadata on all the supplied items. + /// + internal static void SetMetadata(IEnumerable> metadataList, IEnumerable items) + { + // Set up a single dictionary that can be applied to all the items + CopyOnWritePropertyDictionary metadata = new CopyOnWritePropertyDictionary(metadataList.FastCountOrZero()); + foreach (KeyValuePair metadatum in metadataList) + { + metadata.Set(new ProjectMetadataInstance(metadatum.Key, metadatum.Value)); + } + + foreach (ProjectItemInstance item in items) + { + item._taskItem.SetMetadata(metadata); // Potential copy on write + } + } + + /// + /// Factory for deserialization. + /// + static internal ProjectItemInstance FactoryForDeserialization(INodePacketTranslator translator, ProjectInstance projectInstance) + { + ProjectItemInstance newItem = new ProjectItemInstance(projectInstance); + ((INodePacketTranslatable)newItem).Translate(translator); + return newItem; + } + + /// + /// Add a metadata with the specified names and values. + /// Overwrites any metadata with the same name already in the collection. + /// + internal void SetMetadata(CopyOnWritePropertyDictionary metadataDictionary) + { + _project.VerifyThrowNotImmutable(); + + _taskItem.SetMetadata(metadataDictionary); + } + + /// + /// Sets metadata where one built-in metadata is allowed to be set: RecursiveDir. + /// This is not normally legal to set outside of evaluation. However, the CreateItem + /// needs to be able to set it as a task output, because it supports wildcards. So as a special exception we allow + /// tasks to set this particular metadata as a task output. + /// Other built in metadata names are ignored. That's because often task outputs are items that were passed in, + /// which legally have built-in metadata. If necessary we can calculate it on the new items we're making if requested. + /// We don't copy them too because tasks shouldn't set them (they might become inconsistent) + /// + internal void SetMetadataOnTaskOutput(string name, string evaluatedValueEscaped) + { + _project.VerifyThrowNotImmutable(); + + _taskItem.SetMetadataOnTaskOutput(name, evaluatedValueEscaped); + } + + /// + /// Deep clone the item. + /// Any metadata inherited from item definitions are also copied. + /// + internal ProjectItemInstance DeepClone() + { + return new ProjectItemInstance(this); + } + + /// + /// Deep clone the item. + /// Any metadata inherited from item definitions are also copied. + /// + internal ProjectItemInstance DeepClone(ProjectInstance newProject) + { + return new ProjectItemInstance(this, newProject); + } + + /// + /// Generates a ProjectItemElement representing this instance. + /// + /// The root element to which the element will belong. + /// The new element. + internal ProjectItemElement ToProjectItemElement(ProjectElementContainer parent) + { + ProjectItemElement item = parent.ContainingProject.CreateItemElement(ItemType); + item.Include = EvaluatedInclude; + parent.AppendChild(item); + + foreach (ProjectMetadataInstance metadataInstance in Metadata) + { + item.AddMetadata(metadataInstance.Name, metadataInstance.EvaluatedValue); + } + + return item; + } + + /// + /// Common constructor code. + /// Direct metadata may be null, indicating no metadata. It will be cloned. + /// Builtin metadata may be null, indicating it has not been populated. It will be cloned. + /// Inherited item definition metadata may be null. It is assumed to ALREADY HAVE BEEN CLONED. + /// Mutability follows the project. + /// + private void CommonConstructor(ProjectInstance projectToUse, string itemTypeToUse, string includeEscaped, string includeBeforeWildcardExpansionEscaped, CopyOnWritePropertyDictionary directMetadata, List itemDefinitions, string definingFileEscaped) + { + ErrorUtilities.VerifyThrowArgumentNull(projectToUse, "project"); + ErrorUtilities.VerifyThrowArgumentLength(itemTypeToUse, "itemType"); + XmlUtilities.VerifyThrowArgumentValidElementName(itemTypeToUse); + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[itemTypeToUse] == null, "OM_ReservedName", itemTypeToUse); + + // TaskItems don't have an item type. So for their benefit, we have to lookup and add the regular item definition. + List inheritedItemDefinitions = (itemDefinitions == null) ? null : new List(itemDefinitions); + + ProjectItemDefinitionInstance itemDefinition; + if (projectToUse.ItemDefinitions.TryGetValue(itemTypeToUse, out itemDefinition)) + { + inheritedItemDefinitions = inheritedItemDefinitions ?? new List(); + inheritedItemDefinitions.Add(itemDefinition); + } + + _project = projectToUse; + _itemType = itemTypeToUse; + _taskItem = new TaskItem( + includeEscaped, + includeBeforeWildcardExpansionEscaped, + (directMetadata == null) ? null : directMetadata.DeepClone(), // copy on write! + inheritedItemDefinitions, + _project.Directory, + _project.IsImmutable, + definingFileEscaped + ); + } + + /// + /// An item without an item type. Cast to an ITaskItem, this is + /// what is given to tasks. It is also used for target outputs. + /// + internal sealed class TaskItem : MarshalByRefObject, ITaskItem, ITaskItem2, IItem, INodePacketTranslatable, IEquatable + { + /// + /// The source file that defined this item. + /// + private string _definingFileEscaped; + + /// + /// Evaluated include, escaped as necessary. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string _includeEscaped; + + /// + /// The evaluated (escaped) include prior to wildcard expansion. Used to determine the + /// RecursiveDir build-in metadata value. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string _includeBeforeWildcardExpansionEscaped; + + /// + /// Evaluated metadata. + /// May be null. + /// + /// + /// Lazily created, as there are huge numbers of items generated in + /// a build that have no metadata at all. + /// + private CopyOnWritePropertyDictionary _directMetadata; + + /// + /// Cached value of the fullpath metadata. All other metadata are computed on demand. + /// + private string _fullPath; + + /// + /// All the item definitions that apply to this item, in order of + /// decreasing precedence. At the bottom will be an item definition + /// that directly applies to the item type that produced this item. The others will + /// be item definitions inherited from items that were + /// used to create this item. + /// + private List _itemDefinitions; + + /// + /// Directory of the associated project. If this is available, + /// it is used to calculate built-in metadata. Otherwise, + /// the current directory is used. + /// + private string _projectDirectory; + + /// + /// Whether the task item is immutable. + /// + private bool _isImmutable; + + /// + /// Creates an instance of this class given the item-spec. + /// + internal TaskItem(string includeEscaped, string definingFileEscaped) + : this(includeEscaped, includeEscaped, null, null, null, /* mutable */ false, definingFileEscaped) + { + } + + /// + /// Creates an instance of this class given the item-spec and a built-in metadata collection. + /// Parameters are assumed to be ALREADY CLONED. + /// + internal TaskItem( + string includeEscaped, + string includeBeforeWildcardExpansionEscaped, + CopyOnWritePropertyDictionary directMetadata, + List itemDefinitions, + string projectDirectory, + bool immutable, + string definingFileEscaped // the actual project file (or import) that defines this item. + ) + { + ErrorUtilities.VerifyThrowArgumentLength(includeEscaped, "includeEscaped"); + ErrorUtilities.VerifyThrowArgumentLength(includeBeforeWildcardExpansionEscaped, "includeBeforeWildcardExpansionEscaped"); + + _includeEscaped = includeEscaped; + _includeBeforeWildcardExpansionEscaped = includeBeforeWildcardExpansionEscaped; + _directMetadata = (directMetadata == null || directMetadata.Count == 0) ? null : directMetadata; // If the metadata was all removed, toss the dictionary + _itemDefinitions = itemDefinitions; + _projectDirectory = projectDirectory; + _isImmutable = immutable; + _definingFileEscaped = definingFileEscaped; + } + + /// + /// Creates a task item by copying the information from a . + /// Parameters are cloned. + /// + internal TaskItem(ProjectItemInstance item) + : this(item._taskItem, false /* no original itemspec */) + { + } + + /// + /// Constructor for deserialization only. + /// + private TaskItem() + { + } + + /// + /// Creates an instance of this class given the backing item. + /// Does not copy immutability, since there is no connection with the original. + /// + private TaskItem(TaskItem source, bool addOriginalItemSpec) + { + _includeEscaped = source._includeEscaped; + _includeBeforeWildcardExpansionEscaped = source._includeBeforeWildcardExpansionEscaped; + source.CopyMetadataTo(this, addOriginalItemSpec); + _fullPath = source._fullPath; + _definingFileEscaped = source._definingFileEscaped; + } + + /// + /// Private constructor used for serialization. + /// + private TaskItem(INodePacketTranslator translator) + { + ((INodePacketTranslatable)this).Translate(translator); + } + + /// + /// Private constructor used for serialization. + /// + private TaskItem(INodePacketTranslator translator, LookasideStringInterner interner) + { + this.TranslateWithInterning(translator, interner); + } + + /// + /// Gets or sets the unescaped include, or "name", for the item. + /// + /// + /// This one is a bit tricky. Orcas assumed that the value being set was escaped, but + /// that the value being returned was unescaped. Maintain that behaviour here. To get + /// the escaped value, use ITaskItem2.EvaluatedIncludeEscaped. + /// + public string ItemSpec + { + get + { + return EscapingUtilities.UnescapeAll(_includeEscaped); + } + + set + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + // Historically empty string was allowed + ErrorUtilities.VerifyThrowArgumentNull(value, "ItemSpec"); + + _includeEscaped = value; + _fullPath = null; // Clear cached value + } + } + + /// + /// Gets or sets the escaped include, or "name", for the item. + /// + /// + /// Taking the opportunity to fix the property name, although this doesn't + /// make it obvious it's an improvement on ItemSpec. + /// + string ITaskItem2.EvaluatedIncludeEscaped + { + get + { + return _includeEscaped; + } + + set + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + // setter on ItemSpec already expects an escaped value. + ItemSpec = value; + } + } + + /// + /// Gets the names of metadata on the item. + /// Includes all built-in metadata. + /// Computed, not necessarily fast. + /// + public ICollection MetadataNames + { + get + { + List names = new List((List)CustomMetadataNames); + + foreach (string name in FileUtilities.ItemSpecModifiers.All) + { + names.Add(name); + } + + return names; + } + } + + /// + /// Gets the number of metadata set on the item. + /// Computed, not necessarily fast. + /// + public int MetadataCount + { + get { return MetadataNames.Count; } + } + + /// + /// Gets the names of custom metadata on the item. + /// If there is none, returns an empty collection. + /// Does not include built-in metadata. + /// Computed, not necessarily fast. + /// + public ICollection CustomMetadataNames + { + get + { + List names = new List(); + + foreach (ProjectMetadataInstance metadatum in MetadataCollection) + { + names.Add(metadatum.Name); + } + + return names; + } + } + + /// + /// Gets the number of custom metadata set on the item. + /// Does not include built-in metadata. + /// Computed, not necessarily fast. + /// + public int CustomMetadataCount + { + get { return CustomMetadataNames.Count; } + } + + /// + /// Gets the evaluated include for this item, unescaped. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IItem.EvaluatedInclude + { + get { return EscapingUtilities.UnescapeAll(_includeEscaped); } + } + + /// + /// Gets the evaluated include for this item, escaped. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IItem.EvaluatedIncludeEscaped + { + get { return _includeEscaped; } + } + + /// + /// The directory of the project owning this TaskItem. + /// May be null if this is not well defined. + /// + string IItem.ProjectDirectory + { + get { return _projectDirectory; } + } + + #region IKeyed Members + + /// + /// Returns some value useful for a key in a dictionary + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string Microsoft.Build.Collections.IKeyed.Key + { + get { return _includeEscaped; } + } + + #endregion + + /// + /// The escaped include for this item + /// + internal string IncludeEscaped + { + get + { + return _includeEscaped; + } + + set + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + ErrorUtilities.VerifyThrowArgumentLength(value, "IncludeEscaped"); + _includeEscaped = value; + _fullPath = null; // Clear cached value + } + } + + /// + /// The value of the include after evaluation but before wildcard expansion. + /// Used to determine %(RecursiveDir) + /// + internal string IncludeBeforeWildcardExpansionEscaped + { + get { return _includeBeforeWildcardExpansionEscaped; } + } + + /// + /// Number of pieces of metadata directly on this item + /// + internal int DirectMetadataCount + { + get { return (_directMetadata == null) ? 0 : _directMetadata.Count; } + } + + /// + /// Unordered collection of evaluated metadata on the item. + /// If there is no metadata, returns an empty collection. + /// Does not include built-in metadata. + /// Includes any from item definitions not masked by directly set metadata. + /// This is a read-only collection. To modify the metadata, use . + /// Computed, not necessarily fast. + /// + internal CopyOnWritePropertyDictionary MetadataCollection + { + get + { + // The new item inherits any metadata originating in item definitions, which + // takes precedence over its own item definition metadata. + // + // Order of precedence: + // (1) any directly defined metadata on the source item + // (2) any item definition metadata the item had accumulated, in order of accumulation + // (last of which is any item definition metadata associated with the destination item's item type) + if (_itemDefinitions == null) + { + return (_directMetadata == null) ? new CopyOnWritePropertyDictionary() : _directMetadata.DeepClone(); // copy on write! + } + + CopyOnWritePropertyDictionary allMetadata = new CopyOnWritePropertyDictionary(); + + // Next, any inherited item definitions. Front of the list is highest priority, + // so walk backwards. + for (int i = _itemDefinitions.Count - 1; i >= 0; i--) + { + foreach (ProjectMetadataInstance metadatum in _itemDefinitions[i].Metadata) + { + allMetadata.Set(metadatum); + } + } + + // Finally any direct metadata win. + if (_directMetadata != null) + { + foreach (ProjectMetadataInstance metadatum in _directMetadata) + { + allMetadata.Set(metadatum); + } + } + + return allMetadata; + } + } + + #region Operators + + /// + /// This allows an explicit typecast from a "TaskItem" to a "string", returning the ItemSpec for this item. + /// + public static explicit operator string (TaskItem that) + { + return that._includeEscaped; + } + + /// + /// The equivalence operator. + /// + /// The left hand operand. + /// The right hand operand. + /// True if the items are equivalent, false otherwise. + public static bool operator ==(TaskItem left, TaskItem right) + { + if (!Object.ReferenceEquals(left, null)) + { + return left.Equals(right); + } + else if (!Object.ReferenceEquals(right, null)) + { + return right.Equals(left); + } + + return true; + } + + /// + /// The non-equivalence operator. + /// + /// The left hand operand. + /// The right hand operand. + /// False if the items are equivalent, true otherwise. + public static bool operator !=(TaskItem left, TaskItem right) + { + return !(left == right); + } + + #endregion + + /// + /// Produce a string representation. + /// + public override string ToString() + { + return _includeEscaped; + } + + /// + /// Overridden to give this class infinite lease time. Otherwise we end up with a limited + /// lease (5 minutes I think) and instances can expire if they take long time processing. + /// + public override object InitializeLifetimeService() + { + // null means infinite lease time + return null; + } + + #region IItem and ITaskItem2 Members + + /// + /// Returns the metadata with the specified key. + /// + string IItem.GetMetadataValue(string name) + { + return GetMetadata(name); + } + + /// + /// Returns the escaped value of the metadata with the specified key. + /// + string IItem.GetMetadataValueEscaped(string name) + { + return GetMetadataEscaped(name); + } + + /// + /// Returns the escaped value of the metadata with the specified key. + /// + string ITaskItem2.GetMetadataValueEscaped(string name) + { + return GetMetadataEscaped(name); + } + + /// + /// Gets any existing ProjectMetadata on the item, or + /// else any on an applicable item definition. + /// This is ONLY called during evaluation. + /// + /// + /// Evaluation never creates ITaskItems, so this should never be called. + /// + ProjectMetadataInstance IItem.GetMetadata(string name) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + /// + /// Set metadata + /// + ProjectMetadataInstance IItem.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedInclude) + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + /// + /// ITaskItem implementation which returns the specified metadata value, unescaped. + /// If metadata is not defined, returns empty string. + /// + public string GetMetadata(string metadataName) + { + return EscapingUtilities.UnescapeAll(GetMetadataEscaped(metadataName)); + } + + /// + /// Returns the specified metadata value, escaped. + /// If metadata is not defined, returns empty string. + /// + public string GetMetadataEscaped(string metadataName) + { + if (metadataName == null || metadataName.Length == 0) + { + ErrorUtilities.VerifyThrowArgumentLength(metadataName, "metadataName"); + } + + string value = null; + ProjectMetadataInstance metadatum = null; + + if (_directMetadata != null) + { + Debug.Assert(_directMetadata.Count != 0, "We should not waste a dictionary for no metadata"); + metadatum = _directMetadata[metadataName]; + if (metadatum != null) + { + return metadatum.EvaluatedValueEscaped; + } + } + + metadatum = GetItemDefinitionMetadata(metadataName); + + if (null != metadatum && Expander.ExpressionMayContainExpandableExpressions(metadatum.EvaluatedValueEscaped)) + { + Expander expander = new Expander(null, null, new BuiltInMetadataTable(null, this)); + + // We don't have a location to use, but this is very unlikely to error + value = expander.ExpandIntoStringLeaveEscaped(metadatum.EvaluatedValueEscaped, ExpanderOptions.ExpandBuiltInMetadata, ElementLocation.EmptyLocation); + + return value; + } + else if (null != metadatum) + { + return metadatum.EvaluatedValueEscaped; + } + + value = GetBuiltInMetadataEscaped(metadataName); + + return value ?? String.Empty; + } + + /// + /// ITaskItem implementation which sets metadata. + /// + /// + /// The value is assumed to be escaped. + /// + public void SetMetadata(string metadataName, string metadataValueEscaped) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + SetMetadataObject(metadataName, metadataValueEscaped, true /* built-in metadata allowed */); + } + + /// + /// ITaskItem2 implementation which sets the literal value of metadata -- it is escaped + /// internally as necessary. + /// + void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + SetMetadata(metadataName, EscapingUtilities.Escape(metadataValue)); + } + + /// + /// ITaskItem implementation which removed the named piece of metadata. + /// If the metadata is not present, does nothing. + /// + public void RemoveMetadata(string metadataName) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + if (_directMetadata != null) + { + _directMetadata.Remove(metadataName); + + // If the metadata was all removed, toss the dictionary + if (_directMetadata.Count == 0) + { + _directMetadata = null; + } + } + } + + /// + /// ITaskItem implementation which copies the metadata on this item to the specified item. + /// Does not copy built-in metadata, and will not overwrite existing, non-empty metadata. + /// If the destination implements ITaskItem2, this avoids losing the escaped nature of values. + /// + public void CopyMetadataTo(ITaskItem destinationItem) + { + CopyMetadataTo(destinationItem, true /* add original itemspec metadata */); + } + + /// + /// ITaskItem implementation which copies the metadata on this item to the specified item. + /// Copies direct and item definition metadata. + /// Does not copy built-in metadata, and will not overwrite existing, non-empty metadata. + /// If the destination implements ITaskItem2, this avoids losing the escaped nature of values. + /// + /// When copying metadata to a task item which can be accessed from a task (Utilities task item) + /// this method will merge and expand any metadata originating with item definitions. + /// + /// destination item to copy the metadata from this to + /// Whether the OriginalItemSpec should be added as a piece + /// of magic metadata. For copying of items this is useful but for cloning of items this adds + /// additional metadata which is not useful because the OriginalItemSpec will always be identical + /// to the ItemSpec, and the addition will and will cause copy-on-write to trigger. + /// + public void CopyMetadataTo(ITaskItem destinationItem, bool addOriginalItemSpec) + { + ErrorUtilities.VerifyThrowArgumentNull(destinationItem, "destinationItem"); + + string originalItemSpec = null; + if (addOriginalItemSpec) + { + // also copy the original item-spec under a "magic" metadata -- this is useful for tasks that forward metadata + // between items, and need to know the source item where the metadata came from. + // Get it before the clone, as it will get overwritten on the destination otherwise + originalItemSpec = destinationItem.GetMetadata("OriginalItemSpec"); + } + + TaskItem destinationAsTaskItem = destinationItem as TaskItem; + + if (destinationAsTaskItem != null && destinationAsTaskItem._directMetadata == null) + { + ProjectInstance.VerifyThrowNotImmutable(destinationAsTaskItem._isImmutable); + + // This optimized path is hit most often + Debug.Assert(_directMetadata == null || _directMetadata.Count != 0, "We should not waste a dictionary for no metadata"); + destinationAsTaskItem._directMetadata = (_directMetadata == null) ? null : _directMetadata.DeepClone(); // copy on write! + + // If the destination item already has item definitions then we want to maintain them + // But ours will be of less precedence than those already on the item + if (destinationAsTaskItem._itemDefinitions == null) + { + destinationAsTaskItem._itemDefinitions = (_itemDefinitions == null) ? null : new List(_itemDefinitions); + } + else if (_itemDefinitions != null) + { + destinationAsTaskItem._itemDefinitions.AddRange(_itemDefinitions); + } + } + else + { + // OK, most likely the destination item was a Microsoft.Build.Utilities.TaskItem. + foreach (ProjectMetadataInstance metadatum in MetadataCollection) + { + // When copying metadata, we do NOT overwrite metadata already on the destination item. + string destinationValue = destinationItem.GetMetadata(metadatum.Name); + if (String.IsNullOrEmpty(destinationValue)) + { + // Utilities.TaskItem's don't know about item definition metadata. So merge that into the values. + destinationItem.SetMetadata(metadatum.Name, GetMetadataEscaped(metadatum.Name)); + } + } + } + + if (addOriginalItemSpec) + { + if (String.IsNullOrEmpty(originalItemSpec)) + { + // This does not appear to significantly cause a copy-on-write; otherwise, it could go in its own slot. + destinationItem.SetMetadata("OriginalItemSpec", _includeEscaped); + } + } + } + + /// + /// ITaskItem implementation which returns a clone of the metadata on this object. + /// Values returned are unescaped. To get the original escaped values, use ITaskItem2.CloneCustomMetadataEscaped instead. + /// + /// The cloned metadata. + public IDictionary CloneCustomMetadata() + { + Dictionary clonedMetadata = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectMetadataInstance metadatum in MetadataCollection) + { + clonedMetadata[metadatum.Name] = metadatum.EvaluatedValue; + } + + return clonedMetadata; + } + + /// + /// ITaskItem2 implementation which returns a clone of the metadata on this object. + /// Values returned are in their original escaped form. + /// + /// The cloned metadata. + IDictionary ITaskItem2.CloneCustomMetadataEscaped() + { + Dictionary clonedMetadata = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + + foreach (ProjectMetadataInstance metadatum in MetadataCollection) + { + clonedMetadata[metadatum.Name] = metadatum.EvaluatedValueEscaped; + } + + return clonedMetadata; + } + + #endregion + + #region INodePacketTranslatable Members + + /// + /// Reads or writes the packet to the serializer. + /// Built-in metadata is not transmitted, but other metadata is. + /// Does not lose escaped nature. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _includeEscaped); + translator.Translate(ref _includeBeforeWildcardExpansionEscaped); + translator.Translate(ref _isImmutable); + translator.Translate(ref _definingFileEscaped); + + CopyOnWritePropertyDictionary temp = (translator.Mode == TranslationDirection.WriteToStream) ? MetadataCollection : null; + translator.TranslateDictionary, ProjectMetadataInstance>(ref temp, ProjectMetadataInstance.FactoryForDeserialization); + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream || _directMetadata == null, "Should be null"); + _directMetadata = (temp.Count == 0) ? null : temp; // If the metadata was all removed, toss the dictionary + } + + #endregion + + #region IEquatable Members + + /// + /// Override of GetHashCode. + /// + public override int GetHashCode() + { + // We need to change this to upper case to ensure that task items whose item specs differ only by + // casing still have the same hash code, since this is used to determine if we have duplicates when + // we do duplicate removal. + return ItemSpec.ToUpper(CultureInfo.InvariantCulture).GetHashCode(); + } + + /// + /// Override of Equals + /// + public override bool Equals(object obj) + { + return this.Equals(obj as TaskItem); + } + + /// + /// Test for item equivalence. Items are equivalent if their item specs are the same, + /// and they have the same custom metadata, case insensitive. + /// + /// + /// The metadata value check has to be case insensitive as batching bucketing is case + /// insensitive. + /// + /// The item against which to compare. + /// True if the items are equivalent, false otherwise. + public bool Equals(TaskItem other) + { + if (Object.ReferenceEquals(other, null)) + { + return false; + } + + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + // Since both sides are this class, we know both sides support ITaskItem2. + ITaskItem2 thisAsITaskItem2 = this as ITaskItem2; + ITaskItem2 otherAsITaskItem2 = other as ITaskItem2; + + // This is case-insensitive. See GetHashCode(). + if (!MSBuildNameIgnoreCaseComparer.Default.Equals(thisAsITaskItem2.EvaluatedIncludeEscaped, otherAsITaskItem2.EvaluatedIncludeEscaped)) + { + return false; + } + + if (this.CustomMetadataCount != other.CustomMetadataCount) + { + return false; + } + + foreach (string name in this.CustomMetadataNames) + { + // This is case-insensitive, so that for example "en-US" and "en-us" match and are bucketed together. + // In this respect, therefore, we have to consider item metadata value case as not significant. + if (!String.Equals + ( + thisAsITaskItem2.GetMetadataValueEscaped(name), + otherAsITaskItem2.GetMetadataValueEscaped(name), + StringComparison.OrdinalIgnoreCase + ) + ) + { + return false; + } + } + + // Do not consider mutability for equality comparison + return true; + } + + #endregion + + /// + /// Returns true if a particular piece of metadata is defined on this item (even if + /// its value is empty string) otherwise false. + /// This includes built-in metadata and metadata from item definitions. + /// + /// + /// It has to include all of these because it's used for batching, which doesn't + /// care where the metadata originated. + /// + public bool HasMetadata(string name) + { + if ((_directMetadata != null && _directMetadata.Contains(name)) || + FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name) || + GetItemDefinitionMetadata(name) != null) + { + return true; + } + + return false; + } + + /// + /// Add a metadata with the specified names and values. + /// Overwrites any metadata with the same name already in the collection. + /// + /// + /// Assumes that metadataDictionary contains escaped values + /// + public void SetMetadata(IEnumerable> metadataDictionary) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + foreach (KeyValuePair metadataEntry in metadataDictionary) + { + SetMetadata(metadataEntry.Key, metadataEntry.Value); + } + } + + /// + /// Factory for serialization. + /// + internal static TaskItem FactoryForDeserialization(INodePacketTranslator translator) + { + return new TaskItem(translator); + } + + /// + /// Factory for serialization. + /// + internal static TaskItem FactoryForDeserialization(INodePacketTranslator translator, LookasideStringInterner interner) + { + return new TaskItem(translator, interner); + } + + /// + /// Reads or writes the task item to the translator using an interner for metadata. + /// + internal void TranslateWithInterning(INodePacketTranslator translator, LookasideStringInterner interner) + { + translator.Translate(ref _includeEscaped); + translator.Translate(ref _includeBeforeWildcardExpansionEscaped); + + if (translator.Mode == TranslationDirection.WriteToStream) + { + CopyOnWritePropertyDictionary temp = MetadataCollection; + + // Intern the metadata + if (translator.TranslateNullable(temp)) + { + int count = temp.Count; + translator.Writer.Write(count); + foreach (ProjectMetadataInstance metadatum in temp) + { + int key = interner.Intern(metadatum.Name); + int value = interner.Intern(metadatum.EvaluatedValueEscaped); + translator.Writer.Write(key); + translator.Writer.Write(value); + } + } + } + else + { + if (translator.TranslateNullable(_directMetadata)) + { + int count = translator.Reader.ReadInt32(); + _directMetadata = (count == 0) ? null : new CopyOnWritePropertyDictionary(count); + for (int i = 0; i < count; i++) + { + int key = translator.Reader.ReadInt32(); + int value = translator.Reader.ReadInt32(); + _directMetadata.Set(new ProjectMetadataInstance(interner.GetString(key), interner.GetString(value))); + } + } + } + } + + /// + /// Gets any metadata with the specified name. + /// Does not include built-in metadata. + /// + internal ProjectMetadataInstance GetMetadataObject(string name) + { + ProjectMetadataInstance value = null; + + if (_directMetadata != null) + { + value = _directMetadata[name]; + } + + if (value == null) + { + value = GetItemDefinitionMetadata(name); + } + + return value; + } + + /// + /// Add a metadata with the specified name and value. + /// Overwrites any metadata with the same name already in the collection. + /// + internal void SetMetadata(CopyOnWritePropertyDictionary metadata) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + if (metadata.Count == 0) + { + return; + } + + if (_directMetadata == null) + { + _directMetadata = metadata.DeepClone(); // Copy on write ! + } + else + { + _directMetadata.ImportProperties(metadata); + } + } + + /// + /// Add a metadata with the specified name and value. + /// Overwrites any metadata with the same name already in the collection. + /// Does not allow built-in metadata unless allowItemSpecModifiers is set. + /// + internal ProjectMetadataInstance SetMetadataObject(string name, string metadataValueEscaped, bool allowItemSpecModifiers) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + _directMetadata = _directMetadata ?? new CopyOnWritePropertyDictionary(); + ProjectMetadataInstance metadatum = new ProjectMetadataInstance(name, metadataValueEscaped, allowItemSpecModifiers /* may not be built-in metadata name */); + _directMetadata.Set(metadatum); + + return metadatum; + } + + /// + /// Sets metadata where one built-in metadata is allowed to be set: RecursiveDir. + /// This is not normally legal to set outside of evaluation. However, the CreateItem + /// needs to be able to set it as a task output, because it supports wildcards. So as a special exception we allow + /// tasks to set this particular metadata as a task output. + /// Other built in metadata names are ignored. That's because often task outputs are items that were passed in, + /// which legally have built-in metadata. If necessary we can calculate it on the new items we're making if requested. + /// We don't copy them too because tasks shouldn't set them (they might become inconsistent) + /// + internal void SetMetadataOnTaskOutput(string name, string evaluatedValueEscaped) + { + ProjectInstance.VerifyThrowNotImmutable(_isImmutable); + + if (!FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(name)) + { + _directMetadata = _directMetadata ?? new CopyOnWritePropertyDictionary(); + ProjectMetadataInstance metadatum = new ProjectMetadataInstance(name, evaluatedValueEscaped, true /* may be built-in metadata name */); + _directMetadata.Set(metadatum); + } + } + + /// + /// Deep clone this into another TaskItem + /// + internal TaskItem DeepClone() + { + // When making a deep clone we do not want to add the OriginalItemSpec because it is the same as ItemSpec + return new TaskItem(this, false); + } + + /// + /// Deep clone this into another TaskItem + /// + internal TaskItem DeepClone(bool isImmutable) + { + // When making a deep clone we do not want to add the OriginalItemSpec because it is the same as ItemSpec + var clone = new TaskItem(this, false); + clone._isImmutable = isImmutable; + + return clone; + } + + /// + /// Helper to get the value of a built-in metadatum with + /// the specified name, if any. + /// If value is not available, returns empty string. + /// + private string GetBuiltInMetadataEscaped(string name) + { + string value = String.Empty; + + if (FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name)) + { + value = BuiltInMetadata.GetMetadataValueEscaped(_projectDirectory, _includeBeforeWildcardExpansionEscaped, _includeEscaped, _definingFileEscaped, name, ref _fullPath); + } + + return value; + } + + /// + /// Retrieves the named metadata from the item definition, if any. + /// If it is not present, returns null. + /// + private ProjectMetadataInstance GetItemDefinitionMetadata(string metadataName) + { + ProjectMetadataInstance metadataFromDefinition = null; + + // Check any inherited item definition metadata first. It's more like + // direct metadata, but we didn't want to copy the tables. + if (_itemDefinitions != null) + { + foreach (ProjectItemDefinitionInstance itemDefinition in _itemDefinitions) + { + metadataFromDefinition = itemDefinition.GetMetadata(metadataName); + + if (metadataFromDefinition != null) + { + return metadataFromDefinition; + } + } + } + + return null; + } + + /// + /// A class factory for instance model items. + /// + internal class ProjectItemInstanceFactory : IItemFactory + { + /// + /// The project to which item instances created by this factory will belong. + /// + private ProjectInstance _project; + + /// + /// Constructor not taking an item type. + /// This indicates that the user of this factory should set the item type + /// on it before using it to create items. + /// + internal ProjectItemInstanceFactory(ProjectInstance project) + { + _project = project; + } + + /// + /// Constructor taking the itemtype for the generated items. + /// + internal ProjectItemInstanceFactory(ProjectInstance project, string itemType) + : this(project) + { + ErrorUtilities.VerifyThrowInternalLength(itemType, "itemType"); + this.ItemType = itemType; + } + + /// + /// The item type that generated items should have + /// + public string ItemType + { + get; + set; + } + + /// + /// Sets the item type via the item xml. + /// Used by the evaluator only. + /// + public ProjectItemElement ItemElement + { + set { ItemType = value.ItemType; } + } + + /// + /// Creates an instance-model item. + /// + /// The include. + /// A new instance item. + public ProjectItemInstance CreateItem(string include, string definingProject) + { + ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); + + ProjectItemInstance item = new ProjectItemInstance(_project, ItemType, include, definingProject); + + return item; + } + + /// + /// Create a ProjectItemInstance, changing the item type but keeping the include. + /// This is to support the scenario Include="@(i)" where we are copying + /// metadata. + /// + public ProjectItemInstance CreateItem(ProjectItemInstance source, string definingProject) + { + return CreateItem(source._taskItem.IncludeEscaped, source._taskItem.IncludeBeforeWildcardExpansionEscaped, source, definingProject); + } + + /// + /// Create a ProjectItemInstance, changing the item type and include but retaining the + /// metadata of the original item. + /// This is to support this scenario: Include="@(i->'xxx')" + /// + public ProjectItemInstance CreateItem(string includeEscaped, ProjectItemInstance source, string definingProject) + { + return CreateItem(includeEscaped, includeEscaped, source, definingProject); + } + + /// + /// Create a new item from the specified include and include before wildcard expansion. + /// This is to support the scenario Include="@(i)" where we are creating new items before adding metadata. + /// + public ProjectItemInstance CreateItem(string evaluatedInclude, string evaluatedIncludeBeforeWildcardExpansion, string definingProject) + { + ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); + + return new ProjectItemInstance(_project, ItemType, evaluatedInclude, evaluatedIncludeBeforeWildcardExpansion, definingProject); + } + + /// + /// Applies the supplied metadata to the destination item. + /// + public void SetMetadata(IEnumerable> metadataList, IEnumerable destinationItems) + { + // Set up a single dictionary that can be applied to all the items + CopyOnWritePropertyDictionary metadata = new CopyOnWritePropertyDictionary(metadataList.FastCountOrZero()); + foreach (Pair metadatum in metadataList) + { + metadata.Set(new ProjectMetadataInstance(metadatum.Key.Name, metadatum.Value)); + } + + foreach (ProjectItemInstance item in destinationItems) + { + item._taskItem.SetMetadata(metadata); + } + } + + /// + /// Create a ProjectItemInstance from another item, changing the item type and include. + /// + private ProjectItemInstance CreateItem(string includeEscaped, string includeBeforeWildcardExpansionEscaped, ProjectItemInstance source, string definingProject) + { + ErrorUtilities.VerifyThrowInternalLength(ItemType, "ItemType"); + ErrorUtilities.VerifyThrowInternalNull(source, "source"); + + // The new item inherits any metadata originating in item definitions, which + // takes precedence over its own item definition metadata. + // + // Order of precedence: + // (1) any directly defined metadata on the source item (passed through) + // (2) any item definition metadata the source item had accumulated, in order of accumulation + // (3) any item definition metadata associated with the source item's item type + // (4) any item definition metadata associated with the destination item's item type (none at this point) + // For (2) and (3) combine into a list, (2) on top. + List itemDefinitionsClone = null; + if (source._taskItem._itemDefinitions != null) + { + itemDefinitionsClone = itemDefinitionsClone ?? new List(source._taskItem._itemDefinitions.Count + 1); + itemDefinitionsClone.AddRange(source._taskItem._itemDefinitions); + } + + ProjectItemDefinitionInstance sourceItemDefinition; + if (_project.ItemDefinitions.TryGetValue(source.ItemType, out sourceItemDefinition)) + { + itemDefinitionsClone = itemDefinitionsClone ?? new List(); + itemDefinitionsClone.Add(sourceItemDefinition); + } + + return new ProjectItemInstance(_project, ItemType, includeEscaped, includeBeforeWildcardExpansionEscaped, source._taskItem._directMetadata, itemDefinitionsClone, definingProject); + } + } + + /// + /// A class factory for task items. + /// + internal class TaskItemFactory : IItemFactory, IItemFactory + { + /// + /// The singleton instance. + /// + private static TaskItemFactory s_instance = new TaskItemFactory(); + + /// + /// Private constructor for singleton creation. + /// + private TaskItemFactory() + { + } + + /// + /// The item type of items created by this factory. + /// Since TaskItems don't have an item type, this returns null, and cannot be set. + /// + public string ItemType + { + get { return null; } + set { /* ignore */ } + } + + /// + /// The item xml for items in this factory. + /// + public ProjectItemElement ItemElement + { + set { /* ignore */ } + } + + /// + /// The singleton instance. Can be cast to the interface required. + /// + internal static TaskItemFactory Instance + { + get { return s_instance; } + } + + /// + /// Creates a taskitem. + /// + /// The include. + /// A new instance item. + public TaskItem CreateItem(string includeEscaped, string definingProject) + { + return new TaskItem(includeEscaped, definingProject); + } + + /// + /// Creates a task item from a ProjectItem + /// + public TaskItem CreateItem(ProjectItem source, string definingProject) + { + TaskItem item = CreateItem(((IItem)source).EvaluatedIncludeEscaped, source, definingProject); + + return item; + } + + /// + /// Creates a task item from a ProjectItem but changing the itemspec + /// + public TaskItem CreateItem(string includeEscaped, ProjectItem baseItem, string definingProject) + { + TaskItem item = new TaskItem(includeEscaped, definingProject); + + foreach (ProjectMetadata metadatum in baseItem.Metadata) + { + item.SetMetadata(metadatum.Name, metadatum.EvaluatedValueEscaped); + } + + return item; + } + + /// + /// Create a task item from a ProjectItemInstance. + /// + public TaskItem CreateItem(ProjectItemInstance source, string definingProject) + { + TaskItem item = CreateItem(((IItem)source).EvaluatedIncludeEscaped, source, definingProject); + + return item; + } + + /// + /// Creates a task item from a ProjectItem + /// + public TaskItem CreateItem(string includeEscaped, ProjectItemInstance baseItem, string definingProject) + { + TaskItem item = new TaskItem(baseItem); + item.IncludeEscaped = includeEscaped; + + return item; + } + + /// + /// Creates a task item using the specified include and include before wildcard expansion. + /// + public TaskItem CreateItem(string includeEscaped, string includeBeforeWildcardExpansionEscaped, string definingProject) + { + return CreateItem(includeEscaped, definingProject); + } + + /// + /// Applies the supplied metadata to the destination item. + /// + public void SetMetadata(IEnumerable> metadata, IEnumerable destinationItems) + { + // Not difficult to implement, but we do not expect to go here. + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + + /// + /// Implementation of IMetadataTable that can be passed to expander to expose only built-in metadata on this item. + /// Built-in metadata is stored in a separate table so it can be cleared out when the item is renamed, as this invalidates the values. + /// Also, more importantly, because typically the same regular metadata values can be shared by many items, + /// and keeping item-specific metadata out of it could allow it to be implemented as a copy-on-write table. + /// + private class BuiltInMetadataTable : IMetadataTable + { + /// + /// Item type + /// + private string _itemType; + + /// + /// Backing item + /// + private TaskItem _item; + + /// + /// Constructor. + /// + internal BuiltInMetadataTable(string itemType, TaskItem item) + { + _itemType = itemType; + _item = item; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name specified. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string name) + { + string value = _item.GetBuiltInMetadataEscaped(name); + return value; + } + + /// + /// Retrieves any value we have in our metadata table for the metadata name and item type specified. + /// If item type is null, it is ignored. + /// If no value is available, returns empty string. + /// + public string GetEscapedValue(string requiredItemType, string name) + { + string value = GetEscapedValueIfPresent(requiredItemType, name); + + return value ?? String.Empty; + } + + /// + /// Returns the value if it exists, null otherwise. + /// If item type is null, it is ignored. + /// + public string GetEscapedValueIfPresent(string requiredItemType, string name) + { + string value = null; + + if ((requiredItemType == null) || MSBuildNameIgnoreCaseComparer.Default.Equals(_itemType, requiredItemType)) + { + value = GetEscapedValue(name); + } + + return value; + } + } + } + + /// + /// Implementation of a comparer that determines equality between ProjectItemInstances + /// + private class ProjectItemInstanceEqualityComparer : IEqualityComparer + { + /// + /// The singleton comparer. + /// + private static ProjectItemInstanceEqualityComparer s_comparer = new ProjectItemInstanceEqualityComparer(); + + /// + /// Constructor. + /// + private ProjectItemInstanceEqualityComparer() + { + } + + /// + /// Returns the default comparer instance. + /// + public static IEqualityComparer Default + { + get { return s_comparer; } + } + + /// + /// Implemtnation of IEqualityComparer.Equals. + /// + /// The left hand side. + /// The right hand side. + /// True of the instances are equivalent, false otherwise. + public bool Equals(ProjectItemInstance x, ProjectItemInstance y) + { + return x._taskItem.Equals(y._taskItem); + } + + /// + /// Implementation of IEqualityComparer.GetHashCode. + /// + /// The item instance. + /// The hash code of the instance. + public int GetHashCode(ProjectItemInstance obj) + { + return obj._taskItem.GetHashCode(); + } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectMetadataInstance.cs b/src/XMakeBuildEngine/Instance/ProjectMetadataInstance.cs new file mode 100644 index 00000000000..bb1c06567b6 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectMetadataInstance.cs @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an evaluated piece of metadata for build purposes. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Construction; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an evaluated piece of metadata for build purposes + /// Added and removed via methods on the ProjectItemInstance object. + /// IMMUTABLE OBJECT. + /// + [DebuggerDisplay("{name}={EvaluatedValue}")] + public class ProjectMetadataInstance : IKeyed, IValued, IEquatable, INodePacketTranslatable, IMetadatum, IDeepCloneable, IImmutable + { + /// + /// Name of the metadatum + /// + private readonly string _name; + + /// + /// Evaluated value + /// Never null. + /// + private readonly string _escapedValue; + + /// + /// Constructor for metadata. + /// Does not allow item spec modifiers. + /// Discards the location of the original element. This is not interesting in the Execution world + /// as it should never be needed for any subsequent messages, and is just extra bulk. + /// IMMUTABLE OBJECT. + /// + /// + /// Not public since the only creation scenario is setting on an item + /// + internal ProjectMetadataInstance(string name, string escapedValue) + : this(name, escapedValue, false) + { + } + + /// + /// Constructor for metadata. + /// Called when a ProjectInstance is created, before the build + /// when virtual items are added, and during the build when tasks + /// emit items. + /// Discards the location of the original element. This is not interesting in the Execution world + /// as it should never be needed for any subsequent messages, and is just extra bulk. + /// IMMUTABLE OBJECT. + /// If the value passed in is null, will be changed to String.Empty. + /// + /// + /// Not public since the only creation scenario is setting on an item + /// + internal ProjectMetadataInstance(string name, string escapedValue, bool allowItemSpecModifiers) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + if (allowItemSpecModifiers) + { + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[name] == null, "OM_ReservedName", name); + } + else + { + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[name] == null && !FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "OM_ReservedName", name); + } + + _name = name; + _escapedValue = escapedValue ?? String.Empty; + } + + /// + /// Constructor for metadata from a ProjectMetadata. + /// Called when a ProjectInstance is created. + /// IMMUTABLE OBJECT. + /// + internal ProjectMetadataInstance(ProjectMetadata metadatum) + : this(metadatum.Name, metadatum.EvaluatedValueEscaped, false) + { + } + + /// + /// Private constructor used for serialization + /// + private ProjectMetadataInstance(INodePacketTranslator translator) + { + translator.Translate(ref _name); + translator.Translate(ref _escapedValue); + } + + /// + /// Name of the metadata + /// + /// + /// This cannot be set, as it is used as the key into + /// the item's metadata table. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public string Name + { + [DebuggerStepThrough] + get + { return _name; } + } + + /// + /// Evaluated value of the metadatum. + /// Never null. + /// + public string EvaluatedValue + { + [DebuggerStepThrough] + get + { + return EscapingUtilities.UnescapeAll(_escapedValue); + } + } + + /// + /// Implementation of IKeyed exposing the metadatum name + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + [DebuggerStepThrough] + get + { return Name; } + } + + /// + /// Implementation of IValued + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IValued.EscapedValue + { + [DebuggerStepThrough] + get + { return EvaluatedValueEscaped; } + } + + /// + /// Evaluated and escaped value of the metadata. + /// Never null. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal string EvaluatedValueEscaped + { + [DebuggerStepThrough] + get + { + return _escapedValue; + } + } + + /// + /// String representation handy for tracing + /// + public override string ToString() + { + return _name + "=" + _escapedValue; + } + + #region INodePacketTranslatable Members + + /// + /// Reads or writes the packet to the serializer. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + // Read implementation is directly in the constructor so that fields can be read-only + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + + string mutableName = _name; + string mutableValue = _escapedValue; + translator.Translate(ref mutableName); + translator.Translate(ref mutableValue); + } + + #endregion + + #region IEquatable Members + + /// + /// Compares this metadata to another for equivalence. + /// + /// The other metadata + /// True if they are equivalent, false otherwise. + bool IEquatable.Equals(ProjectMetadataInstance other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (other == null) + { + return false; + } + + return (_escapedValue == other._escapedValue && + String.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)); + } + + #endregion + + /// + /// Deep clone the metadata + /// Strings are immutable (copy on write) so there is no work to do. + /// Allows built-in metadata names, as they are still valid on the new metadatum. + /// + /// A new metadata instance. + public ProjectMetadataInstance DeepClone() + { + return new ProjectMetadataInstance(_name, _escapedValue, true /* allow built-in metadata names */); + } + + /// + /// Factory for serialization. + /// + static internal ProjectMetadataInstance FactoryForDeserialization(INodePacketTranslator translator) + { + return new ProjectMetadataInstance(translator); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectOnErrorInstance.cs b/src/XMakeBuildEngine/Instance/ProjectOnErrorInstance.cs new file mode 100644 index 00000000000..46512414cb2 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectOnErrorInstance.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an onerror element. +//----------------------------------------------------------------------- + +using System.Diagnostics; +using Microsoft.Build.Shared; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an onerror element + /// + /// + /// This is an immutable class + /// + [DebuggerDisplay("ExecuteTargets={executeTargets} Condition={condition}")] + public sealed class ProjectOnErrorInstance : ProjectTargetInstanceChild + { + /// + /// Unevaluated executetargets value. + /// + private readonly string _executeTargets; + + /// + /// Condition on the element. + /// + private readonly string _condition; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Location of the executeTargets attribute + /// + private readonly ElementLocation _executeTargetsLocation; + + /// + /// Constructor called by Evaluator. + /// All parameters are in the unevaluated state. + /// + internal ProjectOnErrorInstance + ( + string executeTargets, + string condition, + ElementLocation location, + ElementLocation executeTargetsLocation, + ElementLocation conditionLocation + ) + { + ErrorUtilities.VerifyThrowInternalLength(executeTargets, "executeTargets"); + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + + _executeTargets = executeTargets; + _condition = condition; + _location = location; + _executeTargetsLocation = executeTargetsLocation; + _conditionLocation = conditionLocation; + } + + /// + /// Unevaluated condition. + /// May be empty string. + /// + public override string Condition + { + get { return _condition; } + } + + /// + /// Unevaluated ExecuteTargets value. + /// May be empty string. + /// + public string ExecuteTargets + { + get { return _executeTargets; } + } + + /// + /// Location of the element + /// + public override ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public override ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Location of the execute targets attribute, if any + /// + public ElementLocation ExecuteTargetsLocation + { + get { return _executeTargetsLocation; } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskInstance.cs b/src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskInstance.cs new file mode 100644 index 00000000000..12858ba6b80 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskInstance.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an unevaluated propertygroup under a target. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an unevaluated propertygroup under a target. + /// Immutable. + /// + [DebuggerDisplay("Condition={condition}")] + public class ProjectPropertyGroupTaskInstance : ProjectTargetInstanceChild + { + /// + /// Condition, if any + /// + private readonly string _condition; + + /// + /// Child properties. + /// Not ProjectPropertyInstances, as these are evaluated during the build. + /// + private readonly ICollection _properties; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Constructor called by the Evaluator. + /// Assumes ProjectPropertyGroupTaskPropertyInstance is an immutable type. + /// + internal ProjectPropertyGroupTaskInstance + ( + string condition, + ElementLocation location, + ElementLocation conditionLocation, + IEnumerable properties + ) + { + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + ErrorUtilities.VerifyThrowInternalNull(properties, "properties"); + + _condition = condition; + _location = location; + _conditionLocation = conditionLocation; + + if (properties != null) + { + _properties = (properties is ICollection) ? + ((ICollection)properties) : + new List(properties); + } + } + + /// + /// Cloning constructor + /// + private ProjectPropertyGroupTaskInstance(ProjectPropertyGroupTaskInstance that) + { + // All members are immutable + _condition = that._condition; + _properties = that._properties; + } + + /// + /// Condition, if any. + /// May be empty string. + /// + public override string Condition + { + [DebuggerStepThrough] + get + { return _condition; } + } + + /// + /// Child properties + /// + public ICollection Properties + { + [DebuggerStepThrough] + get + { + return (_properties == null) ? + (ICollection)ReadOnlyEmptyCollection.Instance : + new ReadOnlyCollection(_properties); + } + } + + /// + /// Location of the element itself + /// + public override ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public override ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Deep clone + /// + internal ProjectPropertyGroupTaskInstance DeepClone() + { + return new ProjectPropertyGroupTaskInstance(this); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskPropertyInstance.cs b/src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskPropertyInstance.cs new file mode 100644 index 00000000000..d570ac4b2ff --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectPropertyGroupTaskPropertyInstance.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an unevaluated property under an propertygroup in a target. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an unevaluated property under an propertygroup in a target. + /// Immutable. + /// + [DebuggerDisplay("{name}={Value} Condition={condition}")] + public class ProjectPropertyGroupTaskPropertyInstance + { + /// + /// Name of the property + /// + private readonly string _name; + + /// + /// Unevaluated value + /// + private readonly string _value; + + /// + /// Unevaluated condition + /// + private readonly string _condition; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Constructor called by the Evaluator. + /// + internal ProjectPropertyGroupTaskPropertyInstance(string name, string value, string condition, ElementLocation location, ElementLocation conditionLocation) + { + ErrorUtilities.VerifyThrowInternalNull(name, "name"); + ErrorUtilities.VerifyThrowInternalNull(value, "value"); + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + + _name = name; + _value = value; + _condition = condition; + _location = location; + _conditionLocation = conditionLocation; + } + + /// + /// Cloning constructor + /// + private ProjectPropertyGroupTaskPropertyInstance(ProjectPropertyGroupTaskPropertyInstance that) + { + // All fields are immutable + _name = that._name; + _value = that._value; + _condition = that._condition; + _location = that._location; + _conditionLocation = that._conditionLocation; + } + + /// + /// Property name + /// + public string Name + { + [DebuggerStepThrough] + get + { return _name; } + } + + /// + /// Unevaluated value + /// + public string Value + { + [DebuggerStepThrough] + get + { return _value; } + } + + /// + /// Unevaluated condition value + /// + public string Condition + { + [DebuggerStepThrough] + get + { return _condition; } + } + + /// + /// Location of the original element + /// + public ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Deep clone + /// + internal ProjectPropertyGroupTaskPropertyInstance DeepClone() + { + return new ProjectPropertyGroupTaskPropertyInstance(this); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectPropertyInstance.cs b/src/XMakeBuildEngine/Instance/ProjectPropertyInstance.cs new file mode 100644 index 00000000000..bcf844dac28 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectPropertyInstance.cs @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps an evaluated property for build purposes. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; + +using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an evaluated property for build purposes. + /// Added and removed via methods on the ProjectInstance object. + /// + [DebuggerDisplay("{name}={escapedValue}")] + public class ProjectPropertyInstance : IKeyed, IValued, IProperty, IEquatable, INodePacketTranslatable, IDeepCloneable + { + /// + /// Name of the property + /// + private string _name; + + /// + /// Evaluated value: stored escaped. + /// + private string _escapedValue; + + /// + /// Private constructor + /// + private ProjectPropertyInstance(string name, string escapedValue, bool mayBeReserved) + { + _name = name; + _escapedValue = escapedValue; + } + + /// + /// Name of the property + /// + /// + /// This cannot be set, as it is used as the key into + /// the project's properties table. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public string Name + { + [DebuggerStepThrough] + get + { return _name; } + } + + /// + /// Evaluated value of the property. + /// Setter assumes caller has protected global properties, if necessary + /// SETTER ASSUMES CALLER ONLY CALLS IF PROJECTINSTANCE IS MUTABLE because it cannot always be verified. + /// + public string EvaluatedValue + { + [DebuggerStepThrough] + get + { + return EscapingUtilities.UnescapeAll(_escapedValue); + } + + [DebuggerStepThrough] + set + { + ProjectInstance.VerifyThrowNotImmutable(IsImmutable); + ErrorUtilities.VerifyThrowArgumentNull(value, "value"); + _escapedValue = EscapingUtilities.Escape(value); + } + } + + /// + /// Whether this object is immutable. + /// An immutable object can not be made mutable. + /// + public virtual bool IsImmutable + { + get { return false; } + } + + /// + /// Evaluated value of the property, escaped as necessary. + /// Setter assumes caller has protected global properties, if necessary. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IProperty.EvaluatedValueEscaped + { + [DebuggerStepThrough] + get + { + return _escapedValue; + } + } + + /// + /// Implementation of IKeyed exposing the property name + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + [DebuggerStepThrough] + get + { return Name; } + } + + /// + /// Implementation of IValued + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IValued.EscapedValue + { + [DebuggerStepThrough] + get + { return _escapedValue; } + } + + #region IEquatable Members + + /// + /// Compares this property to another for equivalence. + /// + /// The other property. + /// True if the properties are equivalent, false otherwise. + bool IEquatable.Equals(ProjectPropertyInstance other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + if (other == null) + { + return false; + } + + // Do not consider mutability for equality comparison + return (_escapedValue == other._escapedValue && String.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)); + } + + #endregion + + #region INodePacketTranslatable Members + + /// + /// Reads or writes the packet to the serializer. + /// + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.WriteToStream, "write only"); + + translator.Translate(ref _name); + translator.Translate(ref _escapedValue); + bool isImmutable = IsImmutable; + translator.Translate(ref isImmutable); + } + + #endregion + + #region IDeepCloneable + + /// + /// Performs a deep clone + /// + ProjectPropertyInstance IDeepCloneable.DeepClone() + { + return DeepClone(); + } + + #endregion + + /// + /// String representation handy for tracing + /// + public override string ToString() + { + return _name + "=" + _escapedValue; + } + + /// + /// Called before the build when virtual properties are added, + /// and during the build when tasks emit properties. + /// If name is invalid or reserved, throws ArgumentException. + /// Creates mutable object. + /// + /// + /// Not public since the only creation scenario is setting on a project. + /// + internal static ProjectPropertyInstance Create(string name, string escapedValue) + { + return Create(name, escapedValue, mayBeReserved: false, isImmutable: false); + } + + /// + /// Called before the build when virtual properties are added, + /// and during the build when tasks emit properties. + /// If name is invalid or reserved, throws ArgumentException. + /// Creates mutable object. + /// + /// + /// Not public since the only creation scenario is setting on a project. + /// + internal static ProjectPropertyInstance Create(string name, string escapedValue, bool mayBeReserved) + { + return Create(name, escapedValue, mayBeReserved, isImmutable: false); + } + + /// + /// Called by the Evaluator during creation of the ProjectInstance. + /// Reserved properties can be set with this constructor using the appropriate flag. + /// This flags should ONLY be set by the evaluator or by cloning; after the ProjectInstance is created, they must be illegal. + /// If name is invalid or reserved, throws ArgumentException. + /// + internal static ProjectPropertyInstance Create(string name, string escapedValue, bool mayBeReserved, bool isImmutable) + { + return Create(name, escapedValue, mayBeReserved, null, isImmutable); + } + + /// + /// Called during project build time to create a property. Reserved properties will cause + /// an invalid project file exception. + /// Creates mutable object. + /// + internal static ProjectPropertyInstance Create(string name, string escapedValue, ElementLocation location) + { + return Create(name, escapedValue, false, location, isImmutable: false); + } + + /// + /// Called during project build time to create a property. Reserved properties will cause + /// an invalid project file exception. + /// + internal static ProjectPropertyInstance Create(string name, string escapedValue, ElementLocation location, bool isImmutable) + { + return Create(name, escapedValue, false, location, isImmutable); + } + + /// + /// Cloning constructor. + /// Strings are immutable (copy on write) so there is no work to do + /// + internal static ProjectPropertyInstance Create(ProjectPropertyInstance that) + { + return Create(that._name, that._escapedValue, mayBeReserved: true /* already validated */, isImmutable: that.IsImmutable); + } + + /// + /// Cloning constructor. + /// Strings are immutable (copy on write) so there is no work to do + /// + internal static ProjectPropertyInstance Create(ProjectPropertyInstance that, bool isImmutable) + { + return Create(that._name, that._escapedValue, mayBeReserved: true /* already validated */, isImmutable: isImmutable); + } + + /// + /// Factory for serialization + /// + internal static ProjectPropertyInstance FactoryForDeserialization(INodePacketTranslator translator) + { + ErrorUtilities.VerifyThrow(translator.Mode == TranslationDirection.ReadFromStream, "read only"); + + string name = null; + string escapedValue = null; + bool isImmutable = false; + translator.Translate(ref name); + translator.Translate(ref escapedValue); + translator.Translate(ref isImmutable); + + return Create(name, escapedValue, mayBeReserved: true, isImmutable: isImmutable); + } + + /// + /// Performs a deep clone + /// + internal ProjectPropertyInstance DeepClone() + { + return Create(this); + } + + /// + /// Performs a deep clone, optionally changing mutability + /// + internal ProjectPropertyInstance DeepClone(bool isImmutable) + { + return Create(this, isImmutable); + } + + /// + /// Creates a ProjectPropertyElement representing this instance. + /// + /// The root element to which this element will belong. + /// The new element. + internal ProjectPropertyElement ToProjectPropertyElement(ProjectElementContainer parent) + { + ProjectPropertyElement property = parent.ContainingProject.CreatePropertyElement(Name); + property.Value = EvaluatedValue; + parent.AppendChild(property); + + return property; + } + + /// + /// Private constructor which throws the right sort of exception depending on whether it is invoked as a result of + /// a design-time or build-time call. + /// Discards the location of the original element after error checking. This is not interesting in the Execution world + /// as it should never be needed for any subsequent messages, and is just extra bulk. + /// Inherits mutability from project if any. + /// + private static ProjectPropertyInstance Create(string name, string escapedValue, bool mayBeReserved, ElementLocation location, bool isImmutable) + { + // Does not check immutability as this is only called during build (which is already protected) or evaluation + ErrorUtilities.VerifyThrowArgumentNull(escapedValue, "escapedValue"); + if (location == null) + { + ErrorUtilities.VerifyThrowArgument(XMakeElements.IllegalItemPropertyNames[name] == null, "OM_ReservedName", name); + ErrorUtilities.VerifyThrowArgument(mayBeReserved || !ReservedPropertyNames.IsReservedProperty(name), "OM_CannotCreateReservedProperty", name); + XmlUtilities.VerifyThrowArgumentValidElementName(name); + } + else + { + ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[name] == null, location, "CannotModifyReservedProperty", name); + ProjectErrorUtilities.VerifyThrowInvalidProject(mayBeReserved || !ReservedPropertyNames.IsReservedProperty(name), location, "CannotModifyReservedProperty", name); + XmlUtilities.VerifyThrowProjectValidElementName(name, location); + } + + if (isImmutable) + { + return new ProjectPropertyInstance.ProjectPropertyInstanceImmutable(name, escapedValue, mayBeReserved); + } + else + { + return new ProjectPropertyInstance(name, escapedValue, mayBeReserved); + } + } + + /// + /// Version of the class that's immutable. + /// Could have a single class with a boolean field, but there are large numbers of these + /// so it's important to avoid adding another field. Both types of objects are 16 bytes instead of 20. + /// + private class ProjectPropertyInstanceImmutable : ProjectPropertyInstance + { + /// + /// Private constructor. + /// Called by outer class factory method. + /// + internal ProjectPropertyInstanceImmutable(string name, string escapedValue, bool mayBeReserved) + : base(name, escapedValue, mayBeReserved) + { + } + + /// + /// Whether this object can be changed. + /// An immutable object can not be made mutable. + /// + /// + /// Usually gotten from the parent ProjectInstance. + /// + public override bool IsImmutable + { + get { return true; } + } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectTargetInstance.cs b/src/XMakeBuildEngine/Instance/ProjectTargetInstance.cs new file mode 100644 index 00000000000..03e833bbe41 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectTargetInstance.cs @@ -0,0 +1,513 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a target for build purposes. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; +using ObjectModel = System.Collections.ObjectModel; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps a target element + /// + /// + /// This is an immutable class. + /// + [DebuggerDisplay("Name={name} Count={children.Count} Condition={condition} Inputs={inputs} Outputs={outputs} DependsOnTargets={dependsOnTargets}")] + public sealed class ProjectTargetInstance : IImmutable, IKeyed + { + /// + /// Name of the target + /// + private readonly string _name; + + /// + /// Condition on the target. + /// Evaluated during the build. + /// + private readonly string _condition; + + /// + /// Inputs on the target + /// + private readonly string _inputs; + + /// + /// Outputs on the target + /// + private readonly string _outputs; + + /// + /// Return values on the target. + /// + private readonly string _returns; + + /// + /// Semicolon separated list of targets it depends on + /// + private readonly string _dependsOnTargets; + + /// + /// Condition for whether to trim duplicate outputs + /// + private readonly string _keepDuplicateOutputs; + + /// + /// Child entries of the target which refer to OnError targets + /// + private readonly ObjectModel.ReadOnlyCollection _onErrorChildren; + + /// + /// Whether the project file that this target lives in has at least one target + /// with a Returns attribute on it. If so, the default behaviour for all targets + /// in the file without Returns attributes changes from returning the Outputs, to + /// returning nothing. + /// + private readonly bool _parentProjectSupportsReturnsAttribute; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Location of the inputs attribute, if any + /// + private readonly ElementLocation _inputsLocation; + + /// + /// Location of the outputs attribute, if any + /// + private readonly ElementLocation _outputsLocation; + + /// + /// Location of the returns attribute, if any + /// + private readonly ElementLocation _returnsLocation; + + /// + /// Location of KeepDuplicateOutputs attribute, if any + /// + private readonly ElementLocation _keepDuplicateOutputsLocation; + + /// + /// Location of the DependsOnTargets attribute ,if any + /// + private readonly ElementLocation _dependsOnTargetsLocation; + + /// + /// Location of the BeforeTargets attribute ,if any + /// + private readonly ElementLocation _beforeTargetsLocation; + + /// + /// Location of the AfterTargets attribute ,if any + /// + private readonly ElementLocation _afterTargetsLocation; + + /// + /// Child tasks below the target (both regular tasks and "intrinsic tasks" like ItemGroup and PropertyGroup). + /// This is a read-only list unless the instance has been modified using AddTask. + /// + private IList _children; + + /// + /// Constructor called by Evaluator. + /// All parameters are in the unevaluated state. + /// All location parameters may be null if not applicable, except for the main location parameter. + /// + internal ProjectTargetInstance + ( + string name, + string condition, + string inputs, + string outputs, + string returns, + string keepDuplicateOutputs, + string dependsOnTargets, + ElementLocation location, + ElementLocation conditionLocation, + ElementLocation inputsLocation, + ElementLocation outputsLocation, + ElementLocation returnsLocation, + ElementLocation keepDuplicateOutputsLocation, + ElementLocation dependsOnTargetsLocation, + ElementLocation beforeTargetsLocation, + ElementLocation afterTargetsLocation, + ObjectModel.ReadOnlyCollection children, + ObjectModel.ReadOnlyCollection onErrorChildren, + bool parentProjectSupportsReturnsAttribute + ) + { + ErrorUtilities.VerifyThrowInternalLength(name, "name"); + ErrorUtilities.VerifyThrowInternalNull(condition, "condition"); + ErrorUtilities.VerifyThrowInternalNull(inputs, "inputs"); + ErrorUtilities.VerifyThrowInternalNull(outputs, "outputs"); + ErrorUtilities.VerifyThrowInternalNull(keepDuplicateOutputs, "keepDuplicateOutputs"); + ErrorUtilities.VerifyThrowInternalNull(dependsOnTargets, "dependsOnTargets"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + ErrorUtilities.VerifyThrowInternalNull(children, "children"); + ErrorUtilities.VerifyThrowInternalNull(onErrorChildren, "onErrorChildren"); + + _name = name; + _condition = condition; + _inputs = inputs; + _outputs = outputs; + _returns = returns; + _keepDuplicateOutputs = keepDuplicateOutputs; + _dependsOnTargets = dependsOnTargets; + _location = location; + _conditionLocation = conditionLocation; + _inputsLocation = inputsLocation; + _outputsLocation = outputsLocation; + _returnsLocation = returnsLocation; + _keepDuplicateOutputsLocation = keepDuplicateOutputsLocation; + _dependsOnTargetsLocation = dependsOnTargetsLocation; + _beforeTargetsLocation = beforeTargetsLocation; + _afterTargetsLocation = afterTargetsLocation; + _children = children; + _onErrorChildren = onErrorChildren; + _parentProjectSupportsReturnsAttribute = parentProjectSupportsReturnsAttribute; + } + + /// + /// Name of the target + /// + public string Name + { + [DebuggerStepThrough] + get + { return _name; } + } + + /// + /// Unevaluated condition on the task. + /// May be empty string. + /// + public string Condition + { + [DebuggerStepThrough] + get + { return _condition; } + } + + /// + /// Unevaluated inputs on the target element. + /// May be empty string. + /// + public string Inputs + { + [DebuggerStepThrough] + get + { return _inputs; } + } + + /// + /// Unevaluated outputs on the target element + /// May be empty string. + /// + public string Outputs + { + [DebuggerStepThrough] + get + { return _outputs; } + } + + /// + /// Unevaluated return values on the target element + /// May be empty string or null, if no return value is specified. + /// + public string Returns + { + [DebuggerStepThrough] + get + { return _returns; } + } + + /// + /// Unevaluated condition on which we will trim duplicate outputs from the target outputs + /// May be empty string. + /// + public string KeepDuplicateOutputs + { + [DebuggerStepThrough] + get + { return _keepDuplicateOutputs; } + } + + /// + /// Unevaluated semicolon separated list of targets it depends on. + /// May be empty string. + /// + public string DependsOnTargets + { + [DebuggerStepThrough] + get + { return _dependsOnTargets; } + } + + /// + /// Children below the target. The build iterates through this to get each task to execute. + /// This is an ordered collection. + /// This is a read-only list; the ProjectTargetInstance class is immutable. + /// This collection does not contain the OnError target references. + /// + public IList Children + { + [DebuggerStepThrough] + get + { return _children; } + } + + /// + /// The children below the target which refer to OnError targets. + /// This is an ordered collection. + /// This is a read-only list; the ProjectTargetInstance class is immutable. + /// + public IList OnErrorChildren + { + [DebuggerStepThrough] + get + { return _onErrorChildren; } + } + + /// + /// Just the tasks below this target, if any. + /// Other kinds of children are not included. + /// + public ICollection Tasks + { + get + { + return new ReadOnlyCollection + ( + new FilteringEnumerable(Children) + ); + } + } + + /// + /// Full path to the file from which this target originated. + /// If it originated in a project that was not loaded and has never been + /// given a path, returns an empty string. + /// + public string FullPath + { + get { return _location.File; } + } + + /// + /// Location of the original element + /// + public ElementLocation Location + { + [DebuggerStepThrough] + get + { return _location; } + } + + /// + /// Location of the condition, if any + /// + public ElementLocation ConditionLocation + { + [DebuggerStepThrough] + get + { return _conditionLocation; } + } + + /// + /// Location of the inputs + /// + public ElementLocation InputsLocation + { + [DebuggerStepThrough] + get + { return _inputsLocation; } + } + + /// + /// Location of the outputs + /// + public ElementLocation OutputsLocation + { + [DebuggerStepThrough] + get + { return _outputsLocation; } + } + + /// + /// Location of the returns + /// + public ElementLocation ReturnsLocation + { + [DebuggerStepThrough] + get + { return _returnsLocation; } + } + + /// + /// Location of the KeepDuplicatOutputs attribute + /// + public ElementLocation KeepDuplicateOutputsLocation + { + [DebuggerStepThrough] + get + { return _keepDuplicateOutputsLocation; } + } + + /// + /// Location of the dependsOnTargets + /// + public ElementLocation DependsOnTargetsLocation + { + [DebuggerStepThrough] + get + { return _dependsOnTargetsLocation; } + } + + /// + /// Location of the beforeTargets + /// + public ElementLocation BeforeTargetsLocation + { + [DebuggerStepThrough] + get + { return _beforeTargetsLocation; } + } + + /// + /// Location of the afterTargets + /// + public ElementLocation AfterTargetsLocation + { + [DebuggerStepThrough] + get + { return _afterTargetsLocation; } + } + + /// + /// Implementation of IKeyed exposing the target name + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string IKeyed.Key + { + [DebuggerStepThrough] + get + { return Name; } + } + + /// + /// Whether the project file that this target lives in has at least one target + /// with a Returns attribute on it. If so, the default behaviour for all targets + /// in the file without Returns attributes changes from returning the Outputs, to + /// returning nothing. + /// + internal bool ParentProjectSupportsReturnsAttribute + { + [DebuggerStepThrough] + get + { return _parentProjectSupportsReturnsAttribute; } + } + + /// + /// Creates a ProjectTargetElement representing this instance. Attaches it to the specified root element. + /// + /// The root element to which the new element will belong. + /// The new element. + internal ProjectTargetElement ToProjectTargetElement(ProjectRootElement rootElement) + { + ProjectTargetElement target = rootElement.CreateTargetElement(Name); + rootElement.AppendChild(target); + + target.Condition = Condition; + target.DependsOnTargets = DependsOnTargets; + target.Inputs = Inputs; + target.Outputs = Outputs; + target.Returns = Returns; + + foreach (ProjectTaskInstance taskInstance in Tasks) + { + ProjectTaskElement taskElement = target.AddTask(taskInstance.Name); + taskElement.Condition = taskInstance.Condition; + taskElement.ContinueOnError = taskInstance.ContinueOnError; + taskElement.MSBuildArchitecture = taskInstance.MSBuildArchitecture; + taskElement.MSBuildRuntime = taskInstance.MSBuildRuntime; + + foreach (KeyValuePair taskParameterEntry in taskInstance.Parameters) + { + taskElement.SetParameter(taskParameterEntry.Key, taskParameterEntry.Value); + } + + foreach (ProjectTaskInstanceChild outputInstance in taskInstance.Outputs) + { + if (outputInstance is ProjectTaskOutputItemInstance) + { + ProjectTaskOutputItemInstance outputItemInstance = outputInstance as ProjectTaskOutputItemInstance; + taskElement.AddOutputItem(outputItemInstance.TaskParameter, outputItemInstance.ItemType, outputItemInstance.Condition); + } + else if (outputInstance is ProjectTaskOutputPropertyInstance) + { + ProjectTaskOutputPropertyInstance outputPropertyInstance = outputInstance as ProjectTaskOutputPropertyInstance; + taskElement.AddOutputItem(outputPropertyInstance.TaskParameter, outputPropertyInstance.PropertyName, outputPropertyInstance.Condition); + } + } + } + + return target; + } + + /// Adds new child instance. + /// Child instance. + internal void AddProjectTargetInstanceChild(ProjectTargetInstanceChild projectTargetInstanceChild) + { + if (!(_children is List)) + { + _children = new List(_children); + } + + _children.Add(projectTargetInstanceChild); + } + + /// + /// Creates a new task and adds it to the end of the list of tasks. + /// + /// The name of the task to create. + /// The task's condition. + /// The continue on error flag. + /// The new task instance. + internal ProjectTaskInstance AddTask(string taskName, string condition, string continueOnError) + { + ProjectTaskInstance task = AddTask(taskName, condition, continueOnError, String.Empty, String.Empty); + return task; + } + + /// + /// Creates a new task and adds it to the end of the list of tasks. + /// + /// The name of the task to create. + /// The task's condition. + /// The continue on error flag. + /// The new task instance. + internal ProjectTaskInstance AddTask(string taskName, string condition, string continueOnError, string msbuildRuntime, string msbuildArchitecture) + { + ErrorUtilities.VerifyThrowInternalLength(taskName, "taskName"); + ProjectTaskInstance task = new ProjectTaskInstance(taskName, _location, condition ?? String.Empty, continueOnError ?? String.Empty, msbuildRuntime ?? String.Empty, msbuildArchitecture ?? String.Empty); + this.AddProjectTargetInstanceChild(task); + return task; + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectTargetInstanceChild.cs b/src/XMakeBuildEngine/Instance/ProjectTargetInstanceChild.cs new file mode 100644 index 00000000000..3fb3f384768 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectTargetInstanceChild.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Type for TaskInstance and ProjectPropertyGroupTaskInstance and ProjectItemGroupTaskInstance. +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Type for ProjectTaskInstance and ProjectPropertyGroupTaskInstance and ProjectItemGroupTaskInstance + /// allowing them to be used in a single collection of target children + /// + public abstract class ProjectTargetInstanceChild + { + /// + /// Condition on the element + /// + public abstract string Condition + { + get; + } + + /// + /// Full path to the file in which the originating element was originally + /// defined. + /// If it originated in a project that was not loaded and has never been + /// given a path, returns an empty string. + /// + public string FullPath + { + get { return Location.File; } + } + + /// + /// Location of the original element + /// + public abstract ElementLocation Location + { + get; + } + + /// + /// Location of the original condition attribute + /// if any + /// + public abstract ElementLocation ConditionLocation + { + get; + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectTaskInstance.cs b/src/XMakeBuildEngine/Instance/ProjectTaskInstance.cs new file mode 100644 index 00000000000..e6f8c7f975b --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectTaskInstance.cs @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps a task element. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using System.Diagnostics; +using System; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps a task element + /// + /// + /// This is an immutable class + /// + [DebuggerDisplay("Name={name} Condition={condition} ContinueOnError={continueOnError} MSBuildRuntime={MSBuildRuntime} MSBuildArchitecture={MSBuildArchitecture} #Parameters={parameters.Count} #Outputs={outputs.Count}")] + public sealed class ProjectTaskInstance : ProjectTargetInstanceChild + { + /// + /// Name of the task, possibly qualified, as it appears in the project + /// + private readonly string _name; + + /// + /// Condition on the task, if any + /// May be empty string + /// + private readonly string _condition; + + /// + /// Continue on error on the task, if any + /// May be empty string + /// + private readonly string _continueOnError; + + /// + /// Runtime on the task, if any + /// May be empty string + /// + private readonly string _msbuildRuntime; + + /// + /// Architecture on the task, if any + /// May be empty string + /// + private readonly string _msbuildArchitecture; + + /// + /// Unordered set of task parameter names and unevaluated values. + /// This is a dead, read-only collection. + /// + private readonly CopyOnWriteDictionary> _parameters; + + /// + /// Output properties and items below this task. This is an ordered collection + /// as one may depend on another. + /// This is a dead, read-only collection. + /// + private readonly IList _outputs; + + /// + /// Location of this element + /// + private readonly ElementLocation _location; + + /// + /// Location of the condition, if any + /// + private readonly ElementLocation _conditionLocation; + + /// + /// Location of the continueOnError attribute, if any + /// + private readonly ElementLocation _continueOnErrorLocation; + + /// + /// Location of the MSBuildRuntime attribute, if any + /// + private readonly ElementLocation _msbuildRuntimeLocation; + + /// + /// Location of the MSBuildArchitecture attribute, if any + /// + private readonly ElementLocation _msbuildArchitectureLocation; + + /// + /// Constructor called by Evaluator. + /// All parameters are in the unevaluated state. + /// Locations other than the main location may be null. + /// + internal ProjectTaskInstance + ( + ProjectTaskElement element, + IList outputs + ) + { + ErrorUtilities.VerifyThrowInternalNull(element, "element"); + ErrorUtilities.VerifyThrowInternalNull(outputs, "outputs"); + + // These are all immutable + _name = element.Name; + _condition = element.Condition; + _continueOnError = element.ContinueOnError; + _msbuildArchitecture = element.MSBuildArchitecture; + _msbuildRuntime = element.MSBuildRuntime; + _location = element.Location; + _conditionLocation = element.ConditionLocation; + _continueOnErrorLocation = element.ContinueOnErrorLocation; + _msbuildRuntimeLocation = element.MSBuildRuntimeLocation; + _msbuildArchitectureLocation = element.MSBuildArchitectureLocation; + _parameters = element.ParametersForEvaluation; + _outputs = new List(outputs); + } + + /// + /// Creates a new task instance directly. Used for generating instances on-the-fly. + /// + /// The task name. + /// The location for this task. + /// The unevaluated condition. + /// The unevaluated continue on error. + internal ProjectTaskInstance + ( + string name, + ElementLocation taskLocation, + string condition, + string continueOnError, + string msbuildRuntime, + string msbuildArchitecture + ) + { + ErrorUtilities.VerifyThrowArgumentLength("name", "name"); + ErrorUtilities.VerifyThrowArgumentNull(condition, "condition"); + ErrorUtilities.VerifyThrowArgumentNull(continueOnError, "continueOnError"); + _name = name; + _condition = condition; + _continueOnError = continueOnError; + _msbuildRuntime = msbuildRuntime; + _msbuildArchitecture = msbuildArchitecture; + _location = taskLocation; + _conditionLocation = (condition == String.Empty) ? null : ElementLocation.EmptyLocation; + _continueOnErrorLocation = (continueOnError == String.Empty) ? null : ElementLocation.EmptyLocation; + _msbuildArchitectureLocation = (msbuildArchitecture == String.Empty) ? null : ElementLocation.EmptyLocation; + _msbuildRuntimeLocation = (msbuildRuntime == String.Empty) ? null : ElementLocation.EmptyLocation; + _parameters = new CopyOnWriteDictionary>(8, StringComparer.OrdinalIgnoreCase); + _outputs = new List(); + } + + /// + /// Name of the task, possibly qualified, as it appears in the project + /// + public string Name + { + get { return _name; } + } + + /// + /// Unevaluated condition on the task + /// May be empty string. + /// + public override string Condition + { + get { return _condition; } + } + + /// + /// Unevaluated ContinueOnError on the task. + /// May be empty string. + /// + public string ContinueOnError + { + get { return _continueOnError; } + } + + /// + /// Unevaluated MSBuildRuntime on the task. + /// May be empty string. + /// + public string MSBuildRuntime + { + get { return _msbuildRuntime; } + } + + /// + /// Unevaluated MSBuildArchitecture on the task. + /// May be empty string. + /// + public string MSBuildArchitecture + { + get { return _msbuildArchitecture; } + } + + /// + /// Read-only dead unordered set of task parameter names and unevaluated values. + /// Condition and ContinueOnError, which have their own properties, are not included in this collection. + /// + public IDictionary Parameters + { + get + { + Dictionary filteredParameters = new Dictionary(_parameters.Count, StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair> parameter in _parameters) + { + filteredParameters[parameter.Key] = parameter.Value.Item1; + } + + return filteredParameters; + } + } + + /// + /// Ordered set of output property and item objects. + /// This is a read-only dead collection. + /// + public IList Outputs + { + get { return _outputs; } + } + + /// + /// Location of the ContinueOnError attribute, if any + /// + public ElementLocation ContinueOnErrorLocation + { + get { return _continueOnErrorLocation; } + } + + /// + /// Location of the MSBuildRuntime attribute, if any + /// + public ElementLocation MSBuildRuntimeLocation + { + get { return _msbuildRuntimeLocation; } + } + + /// + /// Location of the MSBuildArchitecture attribute, if any + /// + public ElementLocation MSBuildArchitectureLocation + { + get { return _msbuildArchitectureLocation; } + } + + /// + /// Location of the original element + /// + public override ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public override ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Retrieves the parameters dictionary as used during the build. + /// + internal IDictionary> ParametersForBuild + { + get { return _parameters; } + } + + /// + /// Returns the value of a named parameter, or null if there is no such parameter. + /// + /// The name of the parameter to retrieve. + /// The parameter value, or null if it does not exist. + internal string GetParameter(string parameterName) + { + Tuple parameterValue = null; + if (_parameters.TryGetValue(parameterName, out parameterValue)) + { + return parameterValue.Item1; + } + + return null; + } + + /// + /// Sets the unevaluated value for the specified parameter. + /// + /// The name of the parameter to set. + /// The unevaluated value for the parameter. + internal void SetParameter(string parameterName, string unevaluatedValue) + { + _parameters[parameterName] = new Tuple(unevaluatedValue, ElementLocation.EmptyLocation); + } + + /// + /// Adds an output item to the task. + /// + /// The name of the parameter on the task which produces the output. + /// The item which will receive the output. + /// The condition. + internal void AddOutputItem(string taskOutputParameterName, string itemName, string condition) + { + ErrorUtilities.VerifyThrowArgumentLength(taskOutputParameterName, "taskOutputParameterName"); + ErrorUtilities.VerifyThrowArgumentLength(itemName, "itemName"); + _outputs.Add(new ProjectTaskOutputItemInstance(itemName, taskOutputParameterName, condition ?? String.Empty, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, condition == null ? null : ElementLocation.EmptyLocation)); + } + + /// + /// Adds an output property to the task. + /// + /// The name of the parameter on the task which produces the output. + /// The property which will receive the output. + /// The condition. + internal void AddOutputProperty(string taskOutputParameterName, string propertyName, string condition) + { + ErrorUtilities.VerifyThrowArgumentLength(taskOutputParameterName, "taskOutputParameterName"); + ErrorUtilities.VerifyThrowArgumentLength(propertyName, "propertyName"); + _outputs.Add(new ProjectTaskOutputPropertyInstance(propertyName, taskOutputParameterName, condition ?? String.Empty, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, ElementLocation.EmptyLocation, condition == null ? null : ElementLocation.EmptyLocation)); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectTaskInstanceChild.cs b/src/XMakeBuildEngine/Instance/ProjectTaskInstanceChild.cs new file mode 100644 index 00000000000..92d8d002c39 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectTaskInstanceChild.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Type for TaskOutputItem and TaskOutputProperty. +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Type for TaskOutputItem and TaskOutputProperty + /// allowing them to be used in a single collection + /// + public abstract class ProjectTaskInstanceChild + { + /// + /// Condition on the element + /// + public abstract string Condition + { + get; + } + + /// + /// Location of the original element + /// + public abstract ElementLocation Location + { + get; + } + + /// + /// Location of the TaskParameter attribute + /// + public abstract ElementLocation TaskParameterLocation + { + get; + } + + /// + /// Location of the original condition attribute, if any + /// + public abstract ElementLocation ConditionLocation + { + get; + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectTaskOutputItemInstance.cs b/src/XMakeBuildEngine/Instance/ProjectTaskOutputItemInstance.cs new file mode 100644 index 00000000000..60d311f9458 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectTaskOutputItemInstance.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents a task output item tag for build purposes. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Wraps an output item element under a task element + /// + /// + /// Immutable. + /// + public sealed class ProjectTaskOutputItemInstance : ProjectTaskInstanceChild + { + /// + /// Name of the property to put the output in + /// + private readonly string _itemType; + + /// + /// Property on the task class to retrieve the output from + /// + private readonly string _taskParameter; + + /// + /// Condition on the output element + /// + private readonly string _condition; + + /// + /// Location of the original element + /// + private ElementLocation _location; + + /// + /// Location of the original item type attribute + /// + private ElementLocation _itemTypeLocation; + + /// + /// Location of the original task parameter attribute + /// + private ElementLocation _taskParameterLocation; + + /// + /// Location of the original condition attribute + /// + private ElementLocation _conditionLocation; + + /// + /// Constructor called by evaluator + /// + internal ProjectTaskOutputItemInstance(string itemType, string taskParameter, string condition, ElementLocation location, ElementLocation itemTypeLocation, ElementLocation taskParameterLocation, ElementLocation conditionLocation) + { + ErrorUtilities.VerifyThrowInternalLength(itemType, "itemType"); + ErrorUtilities.VerifyThrowInternalLength(taskParameter, "taskParameter"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + ErrorUtilities.VerifyThrowInternalNull(itemTypeLocation, "itemTypeLocation"); + ErrorUtilities.VerifyThrowInternalNull(taskParameterLocation, "taskParameterLocation"); + + _itemType = itemType; + _taskParameter = taskParameter; + _condition = condition; + _location = location; + _itemTypeLocation = itemTypeLocation; + _taskParameterLocation = taskParameterLocation; + _conditionLocation = conditionLocation; + } + + /// + /// Name of the item type that the outputs go into + /// + public string ItemType + { + get { return _itemType; } + } + + /// + /// Property on the task class to retrieve the outputs from + /// + public string TaskParameter + { + get { return _taskParameter; } + } + + /// + /// Condition on the element. + /// If there is no condition, returns empty string. + /// + public override string Condition + { + get { return _condition; } + } + + /// + /// Location of the original element + /// + public override ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public override ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Location of the TaskParameter attribute + /// + public override ElementLocation TaskParameterLocation + { + get { return _taskParameterLocation; } + } + + /// + /// Location of the ItemType attribute + /// + public ElementLocation ItemTypeLocation + { + get { return _itemTypeLocation; } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ProjectTaskOutputPropertyInstance.cs b/src/XMakeBuildEngine/Instance/ProjectTaskOutputPropertyInstance.cs new file mode 100644 index 00000000000..11a70089d24 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ProjectTaskOutputPropertyInstance.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Represents an output property tag on a task for build purposes +//----------------------------------------------------------------------- + +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Execution +{ + /// + /// Represents an output property element beneath a task element + /// + /// + /// Immutable. + /// + public sealed class ProjectTaskOutputPropertyInstance : ProjectTaskInstanceChild + { + /// + /// Name of the property to put the output in + /// + private readonly string _propertyName; + + /// + /// Property on the task class to retrieve the output from + /// + private readonly string _taskParameter; + + /// + /// Condition on the output element + /// + private readonly string _condition; + + /// + /// Location of the original element + /// + private ElementLocation _location; + + /// + /// Location of the original property name attribute + /// + private ElementLocation _propertyNameLocation; + + /// + /// Location of the original task parameter attribute + /// + private ElementLocation _taskParameterLocation; + + /// + /// Location of the original condition attribute + /// + private ElementLocation _conditionLocation; + + /// + /// Constructor called by Evaluator + /// + internal ProjectTaskOutputPropertyInstance(string propertyName, string taskParameter, string condition, ElementLocation location, ElementLocation propertyNameLocation, ElementLocation taskParameterLocation, ElementLocation conditionLocation) + { + ErrorUtilities.VerifyThrowInternalLength(propertyName, "propertyName"); + ErrorUtilities.VerifyThrowInternalLength(taskParameter, "taskParameter"); + ErrorUtilities.VerifyThrowInternalNull(location, "location"); + ErrorUtilities.VerifyThrowInternalNull(propertyNameLocation, "propertyNameLocation"); + ErrorUtilities.VerifyThrowInternalNull(taskParameterLocation, "taskParameterLocation"); + + _propertyName = propertyName; + _taskParameter = taskParameter; + _condition = condition; + _location = location; + _propertyNameLocation = propertyNameLocation; + _taskParameterLocation = taskParameterLocation; + _conditionLocation = conditionLocation; + } + + /// + /// Name of the property to put the output in + /// + public string PropertyName + { + get { return _propertyName; } + } + + /// + /// Property on the task class to retrieve the output from + /// + public string TaskParameter + { + get { return _taskParameter; } + } + + /// + /// Condition on the output element. + /// If there is no condition, returns empty string. + /// + public override string Condition + { + get { return _condition; } + } + + /// + /// Location of the original PropertyName attribute + /// + public ElementLocation PropertyNameLocation + { + get { return _propertyNameLocation; } + } + + /// + /// Location of the original element + /// + public override ElementLocation Location + { + get { return _location; } + } + + /// + /// Location of the condition, if any + /// + public override ElementLocation ConditionLocation + { + get { return _conditionLocation; } + } + + /// + /// Location of the TaskParameter attribute + /// + public override ElementLocation TaskParameterLocation + { + get { return _taskParameterLocation; } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/ReflectableTaskPropertyInfo.cs b/src/XMakeBuildEngine/Instance/ReflectableTaskPropertyInfo.cs new file mode 100644 index 00000000000..eb165b94720 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/ReflectableTaskPropertyInfo.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A reflection-generated TaskPropertyInfo instance. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Execution +{ + /// + /// A reflection-generated TaskPropertyInfo instance. + /// + internal class ReflectableTaskPropertyInfo : TaskPropertyInfo + { + /// + /// The reflection-produced PropertyInfo. + /// + private PropertyInfo _propertyInfo; + + /// + /// The type of the generated tasks. + /// + private Type _taskType; + + /// + /// Initializes a new instance of the class. + /// + /// The original property info that generated this instance. + /// The type to reflect over to get the reflection propertyinfo later. + internal ReflectableTaskPropertyInfo(TaskPropertyInfo taskPropertyInfo, Type taskType) + : base(taskPropertyInfo.Name, taskPropertyInfo.PropertyType, taskPropertyInfo.Output, taskPropertyInfo.Required) + { + ErrorUtilities.VerifyThrowArgumentNull(taskType, "taskType"); + _taskType = taskType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The PropertyInfo used to discover this task property. + internal ReflectableTaskPropertyInfo(PropertyInfo propertyInfo) + : base( + propertyInfo.Name, + propertyInfo.PropertyType, + propertyInfo.GetCustomAttributes(typeof(OutputAttribute), true).Length > 0, + propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), true).Length > 0) + { + _propertyInfo = propertyInfo; + } + + /// + /// Gets or sets the reflection-produced PropertyInfo. + /// + internal PropertyInfo Reflection + { + get + { + if (_propertyInfo == null) + { + _propertyInfo = _taskType.GetProperty(Name, BindingFlags.ExactBinding | BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); + ErrorUtilities.VerifyThrow(_propertyInfo != null, "Could not find property {0} on type {1} that the task factory indicated should exist.", Name, _taskType.FullName); + } + + return _propertyInfo; + } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactory.cs new file mode 100644 index 00000000000..b612400343f --- /dev/null +++ b/src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -0,0 +1,653 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The assembly task factory is used to wrap and construct tasks +// which are from .net assemblies, and can also be used to launch the +// MSBuild task host. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; +using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The assembly task factory is used to wrap and construct tasks which are from .net assemblies. + /// + internal class AssemblyTaskFactory : ITaskFactory2 + { + #region Data + + /// + /// The type loader to load types which derrive from ITask or ITask2 + /// + private readonly TypeLoader _typeLoader = new TypeLoader(new TypeFilter(TaskLoader.IsTaskClass)); + + /// + /// Name of the task wrapped by the task factory + /// + private string _taskName = null; + + /// + /// The loaded type (type, assembly name / file) of the task wrapped by the factory + /// + private LoadedType _loadedType; + + /// + /// A cache of tasks and the AppDomains they are loaded in. + /// + private Dictionary _tasksAndAppDomains = new Dictionary(); + + /// + /// the set of parameters owned by this particular task host + /// + private IDictionary _factoryIdentityParameters; + + /// + /// Tracks whether, in the UsingTask invocation, we were specifically asked to use + /// the task host. If so, that overrides all other concerns, and we will launch + /// the task host even if the requested runtime / architecture match that of the + /// current MSBuild process. + /// + private bool _taskHostFactoryExplicitlyRequested; + + /// + /// Need to store away the taskloggingcontext used by CreateTaskInstance so that + /// TaskLoader will be able to call back with errors. + /// + private TaskLoggingContext _taskLoggingContext; + + #endregion + + /// + /// Initializes a new instance of the class. + /// + internal AssemblyTaskFactory() + { + } + + #region Public Members + + /// + /// Name of the factory. In this case the name is the assembly name which is wrapped by the factory + /// + public string FactoryName + { + get + { + return _loadedType.Assembly.AssemblyLocation; + } + } + + /// + /// Gets the type of task this factory creates. + /// + public Type TaskType + { + get { return _loadedType.Type; } + } + + /// + /// Initializes this factory for instantiating tasks with a particular inline task block. + /// + /// Name of the task. + /// The parameter group. + /// The task body. + /// The task factory logging host. + /// A value indicating whether initialization was successful. + /// + /// MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the factory can be asked + /// whether or not task names can be created by the factory. + /// + /// The taskFactoryLoggingHost will log messages in the context of the target where the task is first used. + /// + /// + public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) + { + ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory"); + return false; + } + + /// + /// Initializes this factory for instantiating tasks with a particular inline task block and a set of UsingTask parameters. + /// + /// Name of the task. + /// Special parameters that the task factory can use to modify how it executes tasks, + /// such as Runtime and Architecture. The key is the name of the parameter and the value is the parameter's value. This + /// is the set of parameters that was set on the UsingTask using e.g. the UsingTask Runtime and Architecture parameters. + /// The parameter group. + /// The task body. + /// The task factory logging host. + /// A value indicating whether initialization was successful. + /// + /// MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the + /// factory can be asked whether or not task names can be created by the factory. If a task factory implements ITaskFactory2, + /// this Initialize method will be called in place of ITaskFactory.Initialize. + /// + /// The taskFactoryLoggingHost will log messages in the context of the target where the task is first used. + /// + /// + public bool Initialize(string taskName, IDictionary factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) + { + ErrorUtilities.ThrowInternalError("Use internal call to properly initialize the assembly task factory"); + return false; + } + + /// + /// Get a list of parameters for the task. + /// + public TaskPropertyInfo[] GetTaskParameters() + { + PropertyInfo[] infos = _loadedType.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var propertyInfos = new TaskPropertyInfo[infos.Length]; + for (int i = 0; i < infos.Length; i++) + { + propertyInfos[i] = new ReflectableTaskPropertyInfo(infos[i]); + } + + return propertyInfos; + } + + /// + /// Create an instance of the task to be used. + /// The task factory logging host will log messages in the context of the task. + /// + /// + /// The task factory logging host will log messages in the context of the task. + /// + /// + /// The generated task, or null if the task failed to be created. + /// + public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) + { + ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory"); + return null; + } + + /// + /// Create an instance of the task to be used. + /// + /// + /// The task factory logging host will log messages in the context of the task. + /// + /// + /// Special parameters that the task factory can use to modify how it executes tasks, such as Runtime and Architecture. + /// The key is the name of the parameter and the value is the parameter's value. This is the set of parameters that was + /// set to the task invocation itself, via e.g. the special MSBuildRuntime and MSBuildArchitecture parameters. + /// + /// + /// If a task factory implements ITaskFactory2, MSBuild will call this method instead of ITaskFactory.CreateTask. + /// + /// + /// The generated task, or null if the task failed to be created. + /// + public ITask CreateTask(IBuildEngine taskFactoryLoggingHost, IDictionary taskIdentityParameters) + { + ErrorUtilities.ThrowInternalError("Use internal call to properly create a task instance from the assembly task factory"); + return null; + } + + /// + /// Cleans up any context or state that may have been built up for a given task. + /// + /// The task to clean up. + /// + /// For many factories, this method is a no-op. But some factories may have built up + /// an AppDomain as part of an individual task instance, and this is their opportunity + /// to shutdown the AppDomain. + /// + public void CleanupTask(ITask task) + { + ErrorUtilities.VerifyThrowArgumentNull(task, "task"); + AppDomain appDomain; + if (_tasksAndAppDomains.TryGetValue(task, out appDomain)) + { + _tasksAndAppDomains.Remove(task); + + if (appDomain != null) + { + // Unload the AppDomain asynchronously to avoid a deadlock that can happen because + // AppDomain.Unload blocks for the process's one Finalizer thread to finalize all + // objects. Some objects are RCWs for STA COM objects and as such would need the + // VS main thread to be processing messages in order to finalize. But if the main thread + // is blocked in a non-pumping wait waiting for this build request to complete, we would + // deadlock. By unloading asynchronously, the AppDomain unload can block till the main + // thread is available, even if it isn't available until after this MSBuild Task has + // finished executing. + Task.Run(() => AppDomain.Unload(appDomain)); + } + } + + TaskHostTask taskAsTaskHostTask = task as TaskHostTask; + if (taskAsTaskHostTask != null) + { + taskAsTaskHostTask.Cleanup(); + } + else + { + // It's really not necessary to do it for TaskHostTasks + TaskLoader.RemoveAssemblyResolver(); + } + } + + #endregion + + #region Internal Members + + /// + /// Initialize the factory from the task registry + /// + internal LoadedType InitializeFactory + ( + AssemblyLoadInfo loadInfo, + string taskName, + IDictionary taskParameters, + string taskElementContents, + IDictionary taskFactoryIdentityParameters, + bool taskHostFactoryExplicitlyRequested, + TargetLoggingContext targetLoggingContext, + ElementLocation elementLocation, + string taskProjectFile + ) + { + ErrorUtilities.VerifyThrowArgumentNull(loadInfo, "loadInfo"); + VerifyThrowIdentityParametersValid(taskFactoryIdentityParameters, elementLocation, taskName, "Runtime", "Architecture"); + + if (taskFactoryIdentityParameters != null) + { + _factoryIdentityParameters = new Dictionary(taskFactoryIdentityParameters, StringComparer.OrdinalIgnoreCase); + } + + _taskHostFactoryExplicitlyRequested = taskHostFactoryExplicitlyRequested; + + try + { + ErrorUtilities.VerifyThrowArgumentLength(taskName, "taskName"); + _taskName = taskName; + _loadedType = _typeLoader.Load(taskName, loadInfo); + ProjectErrorUtilities.VerifyThrowInvalidProject(_loadedType != null, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, String.Empty); + } + catch (TargetInvocationException e) + { + // Exception thrown by the called code itself + // Log the stack, so the task vendor can fix their code + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, Environment.NewLine + e.InnerException.ToString()); + } + catch (ReflectionTypeLoadException e) + { + // ReflectionTypeLoadException.LoaderExceptions may contain nulls + foreach (Exception exception in e.LoaderExceptions) + { + if (exception != null) + { + targetLoggingContext.LogError(new BuildEventFileInfo(taskProjectFile), "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, exception.Message); + } + } + + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, e.Message); + } + catch (ArgumentNullException e) + { + // taskName may be null + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, e.Message); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + ProjectErrorUtilities.VerifyThrowInvalidProject(false, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, e.Message); + } + + return _loadedType; + } + + /// + /// Create an instance of the wrapped ITask for a batch run of the task. + /// + internal ITask CreateTaskInstance(ElementLocation taskLocation, TaskLoggingContext taskLoggingContext, IBuildComponentHost buildComponentHost, IDictionary taskIdentityParameters, AppDomainSetup appDomainSetup, bool isOutOfProc) + { + bool useTaskFactory = false; + IDictionary mergedParameters = null; + _taskLoggingContext = taskLoggingContext; + + // Optimization for the common (vanilla AssemblyTaskFactory) case -- only calculate + // the task factory parameters if we have any to calculate; otherwise even if we + // still launch the task factory, it will be with parameters corresponding to the + // current process. + if ((_factoryIdentityParameters != null && _factoryIdentityParameters.Count > 0) || (taskIdentityParameters != null && taskIdentityParameters.Count > 0)) + { + VerifyThrowIdentityParametersValid(taskIdentityParameters, taskLocation, _taskName, "MSBuildRuntime", "MSBuildArchitecture"); + + mergedParameters = MergeTaskFactoryParameterSets(_factoryIdentityParameters, taskIdentityParameters); + useTaskFactory = _taskHostFactoryExplicitlyRequested || !TaskHostParametersMatchCurrentProcess(mergedParameters); + } + else + { + // if we don't have any task host parameters specified on either the using task or the + // task invocation, then we will run in-proc UNLESS "TaskHostFactory" is explicitly specified + // as the task factory. + useTaskFactory = _taskHostFactoryExplicitlyRequested; + } + + if (useTaskFactory) + { + ErrorUtilities.VerifyThrowInternalNull(buildComponentHost, "buildComponentHost"); + + mergedParameters = mergedParameters ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + string runtime = null; + string architecture = null; + + if (!mergedParameters.TryGetValue(XMakeAttributes.runtime, out runtime)) + { + mergedParameters[XMakeAttributes.runtime] = XMakeAttributes.MSBuildRuntimeValues.clr4; + } + + if (!mergedParameters.TryGetValue(XMakeAttributes.architecture, out architecture)) + { + mergedParameters[XMakeAttributes.architecture] = XMakeAttributes.GetCurrentMSBuildArchitecture(); + } + + TaskHostTask task = new TaskHostTask(taskLocation, taskLoggingContext, buildComponentHost, mergedParameters, _loadedType, appDomainSetup); + return task; + } + else + { + AppDomain taskAppDomain = null; + + ITask taskInstance = TaskLoader.CreateTask(_loadedType, _taskName, taskLocation.File, taskLocation.Line, taskLocation.Column, new TaskLoader.LogError(ErrorLoggingDelegate), appDomainSetup, isOutOfProc, out taskAppDomain); + + if (taskAppDomain != null) + { + _tasksAndAppDomains[taskInstance] = taskAppDomain; + } + + return taskInstance; + } + } + + /// + /// Is the given task name able to be created by the task factory. In the case of an assembly task factory + /// this question is answered by checking the assembly wrapped by the task factory to see if it exists. + /// + internal bool TaskNameCreatableByFactory(string taskName, IDictionary taskIdentityParameters, string taskProjectFile, TargetLoggingContext targetLoggingContext, ElementLocation elementLocation) + { + if (!TaskIdentityParametersMatchFactory(_factoryIdentityParameters, taskIdentityParameters)) + { + return false; + } + + // Parameters match, so now we check to see if the task exists. + LoadedType taskClass = null; + try + { + ErrorUtilities.VerifyThrowArgumentLength(taskName, "TaskName"); + taskClass = _typeLoader.ReflectionOnlyLoad(taskName, _loadedType.Assembly); + if (taskClass != null) + { + return true; + } + else + { + return false; + } + } + catch (TargetInvocationException e) + { + // Exception thrown by the called code itself + // Log the stack, so the task vendor can fix their code + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, Environment.NewLine + e.InnerException.ToString()); + } + catch (ReflectionTypeLoadException e) + { + // ReflectionTypeLoadException.LoaderExceptions may contain nulls + foreach (Exception exception in e.LoaderExceptions) + { + if (exception != null) + { + targetLoggingContext.LogError(new BuildEventFileInfo(taskProjectFile), "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, exception.Message); + } + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, e.Message); + } + catch (ArgumentNullException e) + { + // taskName may be null + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, e.Message); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _loadedType.Assembly.AssemblyLocation, e.Message); + } + + return false; + } + + #endregion + + #region Private members + + /// + /// Validates the given set of parameters, logging the appropriate errors as necessary. + /// + private static void VerifyThrowIdentityParametersValid(IDictionary identityParameters, IElementLocation errorLocation, string taskName, string runtimeName, string architectureName) + { + // validate the task factory parameters + if (identityParameters != null && identityParameters.Count > 0) + { + string runtime = null; + if (identityParameters.TryGetValue(XMakeAttributes.runtime, out runtime)) + { + if (!XMakeAttributes.IsValidMSBuildRuntimeValue(runtime)) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + errorLocation, + "TaskLoadFailureInvalidTaskHostFactoryParameter", + taskName, + runtime, + runtimeName, + XMakeAttributes.MSBuildRuntimeValues.clr2, + XMakeAttributes.MSBuildRuntimeValues.clr4, + XMakeAttributes.MSBuildRuntimeValues.currentRuntime, + XMakeAttributes.MSBuildRuntimeValues.any + ); + } + } + + string architecture = null; + if (identityParameters.TryGetValue(XMakeAttributes.architecture, out architecture)) + { + if (!XMakeAttributes.IsValidMSBuildArchitectureValue(architecture)) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + errorLocation, + "TaskLoadFailureInvalidTaskHostFactoryParameter", + taskName, + architecture, + architectureName, + XMakeAttributes.MSBuildArchitectureValues.x86, + XMakeAttributes.MSBuildArchitectureValues.x64, + XMakeAttributes.MSBuildArchitectureValues.currentArchitecture, + XMakeAttributes.MSBuildArchitectureValues.any + ); + } + } + } + } + + /// + /// Given the set of parameters that are set to the factory, and the set of parameters coming from the task invocation that we're searching for + /// a matching record to, determine whether the parameters match this record. + /// + private static bool TaskIdentityParametersMatchFactory(IDictionary factoryIdentityParameters, IDictionary taskIdentityParameters) + { + if (taskIdentityParameters == null || taskIdentityParameters.Count == 0 || factoryIdentityParameters == null || factoryIdentityParameters.Count == 0) + { + // either the task or the using task doesn't care about anything, in which case we match by default. + return true; + } + + string taskRuntime = null; + string taskArchitecture = null; + string usingTaskRuntime = null; + string usingTaskArchitecture = null; + + taskIdentityParameters.TryGetValue(XMakeAttributes.runtime, out taskRuntime); + factoryIdentityParameters.TryGetValue(XMakeAttributes.runtime, out usingTaskRuntime); + + if (XMakeAttributes.RuntimeValuesMatch(taskRuntime, usingTaskRuntime)) + { + taskIdentityParameters.TryGetValue(XMakeAttributes.architecture, out taskArchitecture); + factoryIdentityParameters.TryGetValue(XMakeAttributes.architecture, out usingTaskArchitecture); + + if (XMakeAttributes.ArchitectureValuesMatch(taskArchitecture, usingTaskArchitecture)) + { + // both match + return true; + } + } + + // one or more does not match, so we don't match. + return false; + } + + /// + /// Given a set of task parameters from the UsingTask and from the task invocation, generate a dictionary that combines the two, or throws if the merge + /// is impossible (we shouldn't ever get to this point if it is ...) + /// + private static IDictionary MergeTaskFactoryParameterSets(IDictionary factoryIdentityParameters, IDictionary taskIdentityParameters) + { + IDictionary mergedParameters = null; + string mergedRuntime = null; + string mergedArchitecture = null; + + if (factoryIdentityParameters == null || factoryIdentityParameters.Count == 0) + { + mergedParameters = new Dictionary(taskIdentityParameters, StringComparer.OrdinalIgnoreCase); + } + else if (taskIdentityParameters == null || taskIdentityParameters.Count == 0) + { + mergedParameters = new Dictionary(factoryIdentityParameters, StringComparer.OrdinalIgnoreCase); + } + + if (mergedParameters != null) + { + mergedParameters.TryGetValue(XMakeAttributes.runtime, out mergedRuntime); + mergedParameters.TryGetValue(XMakeAttributes.architecture, out mergedArchitecture); + + mergedParameters[XMakeAttributes.runtime] = XMakeAttributes.GetExplicitMSBuildRuntime(mergedRuntime); + mergedParameters[XMakeAttributes.architecture] = XMakeAttributes.GetExplicitMSBuildArchitecture(mergedArchitecture); + } + else + { + string taskRuntime = null; + string taskArchitecture = null; + string usingTaskRuntime = null; + string usingTaskArchitecture = null; + + mergedParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + taskIdentityParameters.TryGetValue(XMakeAttributes.runtime, out taskRuntime); + factoryIdentityParameters.TryGetValue(XMakeAttributes.runtime, out usingTaskRuntime); + + if (!XMakeAttributes.TryMergeRuntimeValues(taskRuntime, usingTaskRuntime, out mergedRuntime)) + { + ErrorUtilities.ThrowInternalError("How did we get two runtime values that were unmergeable?"); + } + else + { + mergedParameters.Add(XMakeAttributes.runtime, mergedRuntime); + } + + taskIdentityParameters.TryGetValue(XMakeAttributes.architecture, out taskArchitecture); + factoryIdentityParameters.TryGetValue(XMakeAttributes.architecture, out usingTaskArchitecture); + + if (!XMakeAttributes.TryMergeArchitectureValues(taskArchitecture, usingTaskArchitecture, out mergedArchitecture)) + { + ErrorUtilities.ThrowInternalError("How did we get two runtime values that were unmergeable?"); + } + else + { + mergedParameters.Add(XMakeAttributes.architecture, mergedArchitecture); + } + } + + return mergedParameters; + } + + /// + /// Returns true if the provided set of task host parameters matches the current process, + /// and false otherwise. + /// + private static bool TaskHostParametersMatchCurrentProcess(IDictionary mergedParameters) + { + if (mergedParameters == null || mergedParameters.Count == 0) + { + // We don't care, so they match by default. + return true; + } + + string runtime; + if (mergedParameters.TryGetValue(XMakeAttributes.runtime, out runtime)) + { + string currentRuntime = XMakeAttributes.GetExplicitMSBuildRuntime(XMakeAttributes.MSBuildRuntimeValues.currentRuntime); + + if (!currentRuntime.Equals(XMakeAttributes.GetExplicitMSBuildRuntime(runtime), StringComparison.OrdinalIgnoreCase)) + { + // runtime doesn't match + return false; + } + } + + string architecture; + if (mergedParameters.TryGetValue(XMakeAttributes.architecture, out architecture)) + { + string currentArchitecture = XMakeAttributes.GetCurrentMSBuildArchitecture(); + + if (!currentArchitecture.Equals(XMakeAttributes.GetExplicitMSBuildArchitecture(architecture), StringComparison.OrdinalIgnoreCase)) + { + // architecture doesn't match + return false; + } + } + + // if it doesn't not match, then it matches + return true; + } + + /// + /// Log errors from TaskLoader. + /// + private void ErrorLoggingDelegate(string taskLocation, int taskLine, int taskColumn, string message, params object[] messageArgs) + { + _taskLoggingContext.LogError(new BuildEventFileInfo(taskLocation, taskLine, taskColumn), message, messageArgs); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactoryInstance.cs b/src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactoryInstance.cs new file mode 100644 index 00000000000..7391dc01f9d --- /dev/null +++ b/src/XMakeBuildEngine/Instance/TaskFactories/AssemblyTaskFactoryInstance.cs @@ -0,0 +1,213 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// The assembly task factory Instance is used to wrap and construct tasks which are from .net assemblies. +//----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Reflection; +using System.IO; +using System.Xml; + +using TaskLoggingContext = Microsoft.Build.BackEnd.Logging.TaskLoggingContext; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The assembly task factory is used to wrap and construct tasks which are from .net assemblies. + /// + internal class AssemblyTaskFactoryInstance : ITaskFactory + { + #region Data + + /// + /// Name of the task wrapped by the task factory + /// + private string taskName = null; + + /// + /// The appdomain the task will run in if it requires its own appdomain + /// + private AppDomain taskAppDomain = null; + + /// + /// Does the task need to run in its own appdomain + /// + private bool separateAppDomain = false; + + /// + /// Instance of the task which is wrapped by the task factory + /// + private ITask taskInstance = null; + + /// + /// The loaded type (type, assembly name / file) of the task wrapped by the factory + /// + private LoadedType loadedType; + #endregion + + /// + /// We just need the loaded type information because the task factory which is not instance specific should + /// already have loaded the assembly and got the information. + /// + public AssemblyTaskFactoryInstance(string taskName, LoadedType loadedType) + { + ErrorUtilities.VerifyThrowArgumentLength(taskName, "taskName"); + ErrorUtilities.VerifyThrowArgumentNull(loadedType, "loadedType"); + + this.taskName = taskName; + this.loadedType = loadedType; + } + + #region Properties + + /// + /// Name of the factory. In this case the name is the assembly name which is wrapped by the factory + /// + public string FactoryName + { + get + { + return loadedType.Assembly.AssemblyLocation; + } + } + + #endregion + + #region Public Members + + /// + /// Create an instance of the wrapped ITask for a run of the task in a batch + /// + public ITask CreateTask(IBuildEngine taskfactoryLoggingHost) + { + throw new NotImplementedException("Use Internal call for AssemblyFactories instead"); + } + + /// + /// Create an instance of the wrapped ITask for a batch run of the task. + /// + public ITask CreateTaskInstance(ElementLocation taskLocation, TaskLoggingContext taskLoggingContext, AppDomainSetup appDomainSetup, bool isOutOfProc) + { + separateAppDomain = false; + separateAppDomain = loadedType.HasLoadInSeparateAppDomainAttribute(); + + taskAppDomain = null; + + if (separateAppDomain) + { + if (!loadedType.Type.IsMarshalByRef) + { + taskLoggingContext.LogError + ( + new BuildEventFileInfo(taskLocation), + "TaskNotMarshalByRef", + taskName + ); + + return null; + } + else + { + // Our task depend on this name to be precisely that, so if you change it make sure + // you also change the checks in the tasks run in separate AppDomains. Better yet, just don't change it. + + // Make sure we copy the appdomain configuration and send it to the appdomain we create so that if the creator of the current appdomain + // has done the binding redirection in code, that we will get those settings as well. + AppDomainSetup appDomainInfo = new AppDomainSetup(); + + // Get the current app domain setup settings + byte[] currentAppdomainBytes = appDomainSetup.GetConfigurationBytes(); + + // Apply the appdomain settings to the new appdomain before creating it + appDomainInfo.SetConfigurationBytes(currentAppdomainBytes); + taskAppDomain = AppDomain.CreateDomain(isOutOfProc ? "taskAppDomain (out-of-proc)" : "taskAppDomain (in-proc)", null, appDomainInfo); + + // Hook up last minute dumping of any exceptions + taskAppDomain.UnhandledException += new UnhandledExceptionEventHandler(ExceptionHandling.UnhandledExceptionHandler); + } + } + + // instantiate the task in given domain + if (taskAppDomain == null || taskAppDomain == AppDomain.CurrentDomain) + { + // perf improvement for the same appdomain case - we already have the type object + // and don't want to go through reflection to recreate it from the name. + taskInstance = (ITask)Activator.CreateInstance(loadedType.Type); + + return taskInstance; + } + + if (loadedType.Assembly.AssemblyFile != null) + { + taskInstance = (ITask)taskAppDomain.CreateInstanceFromAndUnwrap(loadedType.Assembly.AssemblyFile, loadedType.Type.FullName); + + // this will force evaluation of the task class type and try to load the task assembly + Type taskType = taskInstance.GetType(); + + // If the types don't match, we have a problem. It means that our AppDomain was able to load + // a task assembly using Load, and loaded a different one. I don't see any other choice than + // to fail here. + if (taskType != loadedType.Type) + { + taskLoggingContext.LogError + ( + new BuildEventFileInfo(taskLocation), + "ConflictingTaskAssembly", + loadedType.Assembly.AssemblyFile, + loadedType.Type.Assembly.Location + ); + + taskInstance = null; + } + } + else + { + taskInstance = (ITask)taskAppDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName); + } + + return taskInstance; + } + + /// + /// Given an instantiated task, this helper method sets the specified parameter. + /// All exceptions from this method will be caught in the taskExecution host and logged as a fatal task error + /// + /// true, if successful + public bool SetTaskParameterValue + ( + PropertyInfo parameter, + object parameterValue + ) + { + parameter.SetValue(taskInstance, parameterValue, null); + return true; + } + + /// + /// Given a task parameter get its value; + /// + public object GetTaskParameterValue(PropertyInfo parameter) + { + return parameter.GetValue(taskInstance, null); + } + + /// + /// Release any context information related to CreateInstanceForBatch. The task should hold no state + /// between batches. + /// + public void CleanupTask() + { + if (separateAppDomain && taskAppDomain != null) + { + AppDomain.Unload(taskAppDomain); + taskAppDomain = null; + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/Instance/TaskFactories/TaskHostTask.cs b/src/XMakeBuildEngine/Instance/TaskFactories/TaskHostTask.cs new file mode 100644 index 00000000000..b3d72706d0c --- /dev/null +++ b/src/XMakeBuildEngine/Instance/TaskFactories/TaskHostTask.cs @@ -0,0 +1,595 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The wrapper task for tasks that wish to take advantage of the +// task host factory feature. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Text; + +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd.Logging; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The wrapper task for tasks that wish to take advantage of the + /// task host factory feature. Generated by AssemblyTaskFactory + /// when it wants to run the loaded task in the task host. + /// + internal class TaskHostTask : IGeneratedTask, ICancelableTask, INodePacketFactory, INodePacketHandler + { + /// + /// The IBuildEngine callback object. + /// + private IBuildEngine _buildEngine; + + /// + /// The host object that can be passed to this task. + /// + private ITaskHost _hostObject; + + /// + /// Logging context for logging errors / issues + /// encountered in the TaskHostTask itself. + /// + private TaskLoggingContext _taskLoggingContext; + + /// + /// Location of the task in the project file. + /// + private IElementLocation _taskLocation; + + /// + /// The provider for the task host nodes. + /// + private IBuildComponentHost _buildComponentHost; + + /// + /// The packet factory. + /// + private NodePacketFactory _packetFactory; + + /// + /// The event which is set when we receive packets. + /// + private AutoResetEvent _packetReceivedEvent; + + /// + /// The packet that is the end result of the task host task execution process + /// + private Queue _receivedPackets; + + /// + /// The set of parameters used to decide which host to launch. + /// + private IDictionary _taskHostParameters; + + /// + /// The type of the task that we are wrapping. + /// + private LoadedType _taskType; + + /// + /// The AppDomainSetup we'll want to apply to the AppDomain that we may + /// want to load the OOP task into. + /// + private AppDomainSetup _appDomainSetup; + + /// + /// The task host context of the task host we're launching -- used to + /// communicate with the task host. + /// + private TaskHostContext _requiredContext = TaskHostContext.Invalid; + + /// + /// True if currently connected to the task host; false otherwise. + /// + private bool _connectedToTaskHost = false; + + /// + /// The provider for task host nodes. + /// + private NodeProviderOutOfProcTaskHost _taskHostProvider; + + /// + /// Lock object to serialize access to the task host. + /// + private Object _taskHostLock; + + /// + /// Keeps track of whether the wrapped task has had cancel called against it. + /// + private bool _taskCancelled; + + /// + /// The set of parameters that has been set to this wrapped task -- save them + /// here so that we can forward them on to the task host. + /// + private IDictionary _setParameters; + + /// + /// Did the task succeed? + /// + private bool _taskExecutionSucceeded = false; + + /// + /// Constructor + /// + public TaskHostTask(IElementLocation taskLocation, TaskLoggingContext taskLoggingContext, IBuildComponentHost buildComponentHost, IDictionary taskHostParameters, LoadedType taskType, AppDomainSetup appDomainSetup) + { + ErrorUtilities.VerifyThrowInternalNull(taskType, "taskType"); + + _taskLocation = taskLocation; + _taskLoggingContext = taskLoggingContext; + _buildComponentHost = buildComponentHost; + _taskType = taskType; + _appDomainSetup = appDomainSetup; + _taskHostParameters = taskHostParameters; + + _packetFactory = new NodePacketFactory(); + + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostTaskComplete, TaskHostTaskComplete.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this); + + _packetReceivedEvent = new AutoResetEvent(false); + _receivedPackets = new Queue(); + _taskHostLock = new Object(); + + _setParameters = new Dictionary(); + } + + /// + /// THe IBuildEngine callback object + /// + public IBuildEngine BuildEngine + { + get + { + return _buildEngine; + } + + set + { + _buildEngine = value; + } + } + + /// + /// The host object that can be passed to this task. + /// + public ITaskHost HostObject + { + get + { + return _hostObject; + } + + set + { + _hostObject = value; + } + } + + /// + /// Sets the requested task parameter to the requested value. + /// + public void SetPropertyValue(TaskPropertyInfo property, object value) + { + _setParameters[property.Name] = value; + } + + /// + /// Returns the value of the requested task parameter + /// + public object GetPropertyValue(TaskPropertyInfo property) + { + if (_setParameters.ContainsKey(property.Name)) + { + object value = _setParameters[property.Name]; + + // If we returned an exception, then we want to throw it when we + // do the get. + if (value != null && (value as Exception) != null) + { + throw (Exception)value; + } + + return _setParameters[property.Name]; + } + else + { + PropertyInfo parameter = _taskType.Type.GetProperty(property.Name, BindingFlags.Instance | BindingFlags.Public); + return parameter.GetValue(this, null); + } + } + + /// + /// Cancels the currently executing task + /// + public void Cancel() + { + if (!_taskCancelled) + { + lock (_taskHostLock) + { + if (_taskHostProvider != null && _connectedToTaskHost) + { + _taskHostProvider.SendData(_requiredContext, new TaskHostTaskCancelled()); + } + } + + _taskCancelled = true; + } + } + + /// + /// Executes the task. + /// + public bool Execute() + { + // log that we are about to spawn the task host + string runtime = _taskHostParameters[XMakeAttributes.runtime]; + string architecture = _taskHostParameters[XMakeAttributes.architecture]; + _taskLoggingContext.LogComment(MessageImportance.Low, "ExecutingTaskInTaskHost", _taskType.Type.Name, _taskType.Assembly.AssemblyLocation, runtime, architecture); + + // set up the node + lock (_taskHostLock) + { + _taskHostProvider = (NodeProviderOutOfProcTaskHost)_buildComponentHost.GetComponent(BuildComponentType.OutOfProcTaskHostNodeProvider); + ErrorUtilities.VerifyThrowInternalNull(_taskHostProvider, "taskHostProvider"); + } + + TaskHostConfiguration hostConfiguration = + new TaskHostConfiguration + ( + _buildComponentHost.BuildParameters.NodeId, + NativeMethodsShared.GetCurrentDirectory(), + CommunicationsUtilities.GetEnvironmentVariables(), + _buildComponentHost.BuildParameters.Culture, + _buildComponentHost.BuildParameters.UICulture, + _appDomainSetup, + BuildEngine.LineNumberOfTaskNode, + BuildEngine.ColumnNumberOfTaskNode, + BuildEngine.ProjectFileOfTaskNode, + BuildEngine.ContinueOnError, + _taskType.Type.FullName, + _taskType.Type.Assembly.Location, + _setParameters + ); + + try + { + lock (_taskHostLock) + { + _requiredContext = CommunicationsUtilities.GetTaskHostContext(_taskHostParameters); + _connectedToTaskHost = _taskHostProvider.AcquireAndSetUpHost(_requiredContext, this, this, hostConfiguration); + } + + if (_connectedToTaskHost) + { + try + { + bool taskFinished = false; + + while (!taskFinished) + { + _packetReceivedEvent.WaitOne(); + + INodePacket packet = null; + + int packetCount = _receivedPackets.Count; + + // Handle the packet that's coming in + while (packetCount > 0) + { + lock (_receivedPackets) + { + if (_receivedPackets.Count > 0) + { + packet = _receivedPackets.Dequeue(); + } + else + { + break; + } + } + + if (packet != null) + { + HandlePacket(packet, out taskFinished); + } + } + } + } + finally + { + lock (_taskHostLock) + { + _taskHostProvider.DisconnectFromHost(_requiredContext); + _connectedToTaskHost = false; + } + } + } + else + { + LogErrorUnableToCreateTaskHost(_requiredContext, runtime, architecture, null); + } + } + catch (BuildAbortedException) + { + LogErrorUnableToCreateTaskHost(_requiredContext, runtime, architecture, null); + } + catch (NodeFailedToLaunchException e) + { + LogErrorUnableToCreateTaskHost(_requiredContext, runtime, architecture, e); + } + + return _taskExecutionSucceeded; + } + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _packetFactory.RegisterPacketHandler(packetType, factory, handler); + } + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + public void UnregisterPacketHandler(NodePacketType packetType) + { + _packetFactory.UnregisterPacketHandler(packetType); + } + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + } + + /// + /// Routes the specified packet + /// + /// The node from which the packet was received. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + { + _packetFactory.RoutePacket(nodeId, packet); + } + + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + public void PacketReceived(int node, INodePacket packet) + { + lock (_receivedPackets) + { + _receivedPackets.Enqueue(packet); + _packetReceivedEvent.Set(); + } + } + + /// + /// Called by TaskHostFactory to let the task know that if it needs to do any additional cleanup steps, + /// now would be the time. + /// + internal void Cleanup() + { + // for now, do nothing. + } + + /// + /// Handles the packets received from the task host. + /// + private void HandlePacket(INodePacket packet, out bool taskFinished) + { + Debug.WriteLine("[TaskHostTask] Handling packet {0} at {1}", packet.Type, DateTime.Now); + taskFinished = false; + + switch (packet.Type) + { + case NodePacketType.TaskHostTaskComplete: + HandleTaskHostTaskComplete(packet as TaskHostTaskComplete); + taskFinished = true; + break; + case NodePacketType.NodeShutdown: + HandleNodeShutdown(packet as NodeShutdown); + taskFinished = true; + break; + case NodePacketType.LogMessage: + HandleLoggedMessage(packet as LogMessagePacket); + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + } + + /// + /// Task completed executing in the task host + /// + private void HandleTaskHostTaskComplete(TaskHostTaskComplete taskHostTaskComplete) + { + // If it crashed, or if it failed, it didn't succeed. + _taskExecutionSucceeded = taskHostTaskComplete.TaskResult == TaskCompleteType.Success ? true : false; + + // reset the environment, as though the task were executed in this process all along. + CommunicationsUtilities.SetEnvironment(taskHostTaskComplete.BuildProcessEnvironment); + + // If it crashed during the execution phase, then we can effectively replicate the inproc task execution + // behaviour by just throwing here and letting the taskbuilder code take care of it the way it would + // have normally. + // We will also replicate the same behaviour if the TaskHost caught some exceptions after execution of the task. + if ((taskHostTaskComplete.TaskResult == TaskCompleteType.CrashedDuringExecution) || + (taskHostTaskComplete.TaskResult == TaskCompleteType.CrashedAfterExecution)) + { + throw new TargetInvocationException(taskHostTaskComplete.TaskException); + } + + // On the other hand, if it crashed during initialization, there's not really a way to effectively replicate + // the inproc behavior -- in the inproc case, the task would have failed to load and crashed long before now. + // Furthermore, if we were just to throw here like in the execution case, we'd lose the ability to log + // different messages based on the circumstances of the initialization failure -- whether it was a setter failure, + // the task just could not be loaded, etc. + + // So instead, when we catch the exception in the task host, we'll also record what message we want it to use + // when the error is logged; and given that information, log that error here. This has the effect of differing + // from the inproc case insofar as ContinueOnError is now respected, instead of forcing a stop here. + if (taskHostTaskComplete.TaskResult == TaskCompleteType.CrashedDuringInitialization) + { + string exceptionMessage; + string[] exceptionMessageArgs; + + if (taskHostTaskComplete.TaskExceptionMessage != null) + { + exceptionMessage = taskHostTaskComplete.TaskExceptionMessage; + exceptionMessageArgs = taskHostTaskComplete.TaskExceptionMessageArgs; + } + else + { + exceptionMessage = "TaskInstantiationFailureError"; + exceptionMessageArgs = new string[] { _taskType.Type.Name, _taskType.Type.Assembly.Location, String.Empty }; + } + + _taskLoggingContext.LogFatalError(new BuildEventFileInfo(_taskLocation), taskHostTaskComplete.TaskException, taskHostTaskComplete.TaskExceptionMessage, taskHostTaskComplete.TaskExceptionMessageArgs); + } + + // Set the output parameters for later + foreach (KeyValuePair outputParam in taskHostTaskComplete.TaskOutputParameters) + { + _setParameters[outputParam.Key] = (outputParam.Value == null) ? null : outputParam.Value.WrappedParameter; + } + } + + /// + /// The task host node failed for some reason + /// + private void HandleNodeShutdown(NodeShutdown nodeShutdown) + { + // if the task was canceled, it may send the shutdown packet before the task itself has exited -- + // in this case, the shutdown is expected, so don't log errors. Also don't update taskExecutionSucceeded, + // as it has already been set properly (likely also to false) when we dealt with the TaskComplete + // packet that was sent immediately prior to this. + if (!_taskCancelled) + { + // nothing much else to say. + _taskExecutionSucceeded = false; + + _taskLoggingContext.LogError(new BuildEventFileInfo(_taskLocation), "TaskHostExitedPrematurely", (nodeShutdown.Exception == null) ? String.Empty : nodeShutdown.Exception.ToString()); + } + } + + /// + /// Handle logged messages from the task host. + /// + private void HandleLoggedMessage(LogMessagePacket logMessagePacket) + { + switch (logMessagePacket.EventType) + { + case LoggingEventType.BuildErrorEvent: + this.BuildEngine.LogErrorEvent((BuildErrorEventArgs)logMessagePacket.NodeBuildEvent.Value.Value); + break; + case LoggingEventType.BuildWarningEvent: + this.BuildEngine.LogWarningEvent((BuildWarningEventArgs)logMessagePacket.NodeBuildEvent.Value.Value); + break; + case LoggingEventType.TaskCommandLineEvent: + case LoggingEventType.BuildMessageEvent: + this.BuildEngine.LogMessageEvent((BuildMessageEventArgs)logMessagePacket.NodeBuildEvent.Value.Value); + break; + case LoggingEventType.CustomEvent: + BuildEventArgs buildEvent = logMessagePacket.NodeBuildEvent.Value.Value; + + // "Custom events" in terms of the communications infrastructure can also be, e.g. custom error events, + // in which case they need to be dealt with in the same way as their base type of event. + if (buildEvent is BuildErrorEventArgs) + { + this.BuildEngine.LogErrorEvent((BuildErrorEventArgs)buildEvent); + } + else if (buildEvent is BuildWarningEventArgs) + { + this.BuildEngine.LogWarningEvent((BuildWarningEventArgs)buildEvent); + } + else if (buildEvent is BuildMessageEventArgs) + { + this.BuildEngine.LogMessageEvent((BuildMessageEventArgs)buildEvent); + } + else if (buildEvent is CustomBuildEventArgs) + { + this.BuildEngine.LogCustomEvent((CustomBuildEventArgs)buildEvent); + } + else + { + ErrorUtilities.ThrowInternalError("Unknown event args type."); + } + + break; + } + } + + /// + /// Since we log that we weren't able to connect to the task host in a couple of different places, + /// extract it out into a separate method. + /// + private void LogErrorUnableToCreateTaskHost(TaskHostContext requiredContext, string runtime, string architecture, NodeFailedToLaunchException e) + { + string msbuildLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationFromHostContext(requiredContext); + + if (msbuildLocation == null) + { + // We don't know the path -- probably we're trying to get a 64-bit assembly on a + // 32-bit machine. At least give them the exe name to look for, though ... + switch (requiredContext) + { + case TaskHostContext.X32CLR2: + case TaskHostContext.X64CLR2: + msbuildLocation = "MSBuildTaskHost.exe"; + break; + case TaskHostContext.X32CLR4: + case TaskHostContext.X64CLR4: + msbuildLocation = "MSBuild.exe"; + break; + case TaskHostContext.Invalid: + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + } + + if (e == null) + { + _taskLoggingContext.LogError(new BuildEventFileInfo(_taskLocation), "TaskHostAcquireFailed", _taskType.Type.Name, runtime, architecture, msbuildLocation); + } + else + { + _taskLoggingContext.LogError(new BuildEventFileInfo(_taskLocation), "TaskHostNodeFailedToLaunch", _taskType.Type.Name, runtime, architecture, msbuildLocation, e.ErrorCode, e.Message); + } + } + } +} diff --git a/src/XMakeBuildEngine/Instance/TaskFactoryLoggingHost.cs b/src/XMakeBuildEngine/Instance/TaskFactoryLoggingHost.cs new file mode 100644 index 00000000000..6e4c8149015 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/TaskFactoryLoggingHost.cs @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The host allows task factories access to method to allow them to log message during the construction of the task factories. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Diagnostics; +using BaseLoggingContext = Microsoft.Build.BackEnd.Logging.BaseLoggingContext; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using System.Runtime.Remoting.Lifetime; +using System.Runtime.Remoting; + +namespace Microsoft.Build.BackEnd +{ + /// + /// The host allows task factories access to method to allow them to log message during the construction of the task factories. + /// + internal class TaskFactoryLoggingHost : MarshalByRefObject, IBuildEngine + { + /// + /// Location of the task node in the original file + /// + private ElementLocation _elementLocation; + + /// + /// The task factory logging context + /// + private BaseLoggingContext _loggingContext; + + /// + /// Is the system running in multi-process mode and requires events to be serializable + /// + private bool _isRunningWithMultipleNodes; + + /// + /// A client sponsor is a class + /// which will respond to a lease renewal request and will + /// increase the lease time allowing the object to stay in memory + /// + private ClientSponsor _sponsor; + + /// + /// True if the task connected to this proxy is alive + /// + private bool _activeProxy; + + /// + /// Constructor + /// + public TaskFactoryLoggingHost(bool isRunningWithMultipleNodes, ElementLocation elementLocation, BaseLoggingContext loggingContext) + { + ErrorUtilities.VerifyThrowArgumentNull(loggingContext, "loggingContext"); + ErrorUtilities.VerifyThrowInternalNull(elementLocation, "elementLocation"); + + _activeProxy = true; + _isRunningWithMultipleNodes = isRunningWithMultipleNodes; + _loggingContext = loggingContext; + _elementLocation = elementLocation; + } + + /// + /// Returns true in the multiproc case + /// REVIEW: Should this mean the same thing in the distributed build case? If we have + /// a build which happens to be on a distributed cluster, but the build manager has only + /// alotted a single machine to this build, is this true? Because the build manager + /// could later decide to add more nodes to this build. + /// UNDONE: This means we are building with multiple processes. If we are building on + /// one machine then I think the maxcpu-count is still 1. In my mind this means multiple nodes either distributed or on the same machine. + /// + public bool IsRunningMultipleNodes + { + get + { + VerifyActiveProxy(); + return _isRunningWithMultipleNodes; + } + } + + /// + /// Reflects the value of the ContinueOnError attribute. + /// + public bool ContinueOnError + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// The line number this task is on + /// + public int LineNumberOfTaskNode + { + get + { + VerifyActiveProxy(); + return _elementLocation.Line; + } + } + + /// + /// The column number this task is on + /// + public int ColumnNumberOfTaskNode + { + get + { + VerifyActiveProxy(); + return _elementLocation.Column; + } + } + + /// + /// The project file this task is in. + /// Typically this is an imported .targets file. + /// Unfortunately the interface has shipped with a poor name, so we cannot change it. + /// + public string ProjectFileOfTaskNode + { + get + { + VerifyActiveProxy(); + return _elementLocation.File; + } + } + + /// + /// Sets or retrieves the logging context + /// + internal BaseLoggingContext LoggingContext + { + [DebuggerStepThrough] + get + { return _loggingContext; } + } + + #region IBuildEngine Members + + /// + /// Logs an error event for the current task + /// + /// The event args + public void LogErrorEvent(Microsoft.Build.Framework.BuildErrorEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + VerifyActiveProxy(); + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _loggingContext.BuildEventContext; + _loggingContext.LoggingService.LogBuildEvent(e); + } + + /// + /// Logs a warning event for the current task + /// + /// The event args + public void LogWarningEvent(Microsoft.Build.Framework.BuildWarningEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + VerifyActiveProxy(); + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _loggingContext.BuildEventContext; + _loggingContext.LoggingService.LogBuildEvent(e); + } + + /// + /// Logs a message event for the current task + /// + /// The event args + public void LogMessageEvent(Microsoft.Build.Framework.BuildMessageEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + VerifyActiveProxy(); + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _loggingContext.BuildEventContext; + _loggingContext.LoggingService.LogBuildEvent(e); + } + + /// + /// Logs a custom event for the current task + /// + /// The event args + public void LogCustomEvent(Microsoft.Build.Framework.CustomBuildEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e, "e"); + VerifyActiveProxy(); + + // If we are in building across process we need the events to be serializable. This method will + // check to see if we are building with multiple process and if the event is serializable. It will + // also log a warning if the event is not serializable and drop the logging message. + if (IsRunningMultipleNodes && !IsEventSerializable(e)) + { + return; + } + + e.BuildEventContext = _loggingContext.BuildEventContext; + _loggingContext.LoggingService.LogBuildEvent(e); + } + + /// + /// Builds a single project file + /// + /// The project file name + /// The set of targets to build. + /// The global properties to use + /// The outputs from the targets + /// True on success, false otherwise. + public bool BuildProjectFile(string projectFileName, string[] targetNames, System.Collections.IDictionary globalProperties, System.Collections.IDictionary targetOutputs) + { + throw new NotImplementedException(); + } + + #endregion + + /// + /// InitializeLifetimeService is called when the remote object is activated. + /// This method will determine how long the lifetime for the object will be. + /// + /// The lease object to control this object's lifetime. + public override object InitializeLifetimeService() + { + VerifyActiveProxy(); + + // Each MarshalByRef object has a reference to the service which + // controls how long the remote object will stay around + ILease lease = (ILease)base.InitializeLifetimeService(); + + // Set how long a lease should be initially. Once a lease expires + // the remote object will be disconnected and it will be marked as being availiable + // for garbage collection + int initialLeaseTime = 1; + + string initialLeaseTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDENGINEPROXYINITIALLEASETIME"); + + if (!String.IsNullOrEmpty(initialLeaseTimeFromEnvironment)) + { + int leaseTimeFromEnvironment; + if (int.TryParse(initialLeaseTimeFromEnvironment, out leaseTimeFromEnvironment) && leaseTimeFromEnvironment > 0) + { + initialLeaseTime = leaseTimeFromEnvironment; + } + } + + lease.InitialLeaseTime = TimeSpan.FromMinutes(initialLeaseTime); + + // Make a new client sponsor. A client sponsor is a class + // which will respond to a lease renewal request and will + // increase the lease time allowing the object to stay in memory + _sponsor = new ClientSponsor(); + + // When a new lease is requested lets make it last 1 minutes longer. + int leaseExtensionTime = 1; + + string leaseExtensionTimeFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDENGINEPROXYLEASEEXTENSIONTIME"); + if (!String.IsNullOrEmpty(leaseExtensionTimeFromEnvironment)) + { + int leaseExtensionFromEnvironment; + if (int.TryParse(leaseExtensionTimeFromEnvironment, out leaseExtensionFromEnvironment) && leaseExtensionFromEnvironment > 0) + { + leaseExtensionTime = leaseExtensionFromEnvironment; + } + } + + _sponsor.RenewalTime = TimeSpan.FromMinutes(leaseExtensionTime); + + // Register the sponsor which will increase lease timeouts when the lease expires + lease.Register(_sponsor); + + return lease; + } + + /// + /// Indicates to the TaskHost that it is no longer needed. + /// Called by TaskBuilder when the task using the EngineProxy is done. + /// + internal void MarkAsInactive() + { + VerifyActiveProxy(); + _activeProxy = false; + + _loggingContext = null; + _elementLocation = null; + + // Clear out the sponsor (who is responsible for keeping the EngineProxy remoting lease alive until the task is done) + // this will be null if the engineproxy was never sent across an appdomain boundry. + if (_sponsor != null) + { + ILease lease = (ILease)RemotingServices.GetLifetimeService(this); + + if (lease != null) + { + lease.Unregister(_sponsor); + } + + _sponsor.Close(); + _sponsor = null; + } + } + + /// + /// Determine if the event is serializable. If we are running with multiple nodes we need to make sure the logging events are serializable. If not + /// we need to log a warning. + /// + internal bool IsEventSerializable(BuildEventArgs e) + { + if (!e.GetType().IsSerializable) + { + _loggingContext.LogWarning(new BuildEventFileInfo(string.Empty), null, "ExpectedEventToBeSerializable", e.GetType().Name); + return false; + } + + return true; + } + + /// + /// Verify the task host is active or not + /// + private void VerifyActiveProxy() + { + ErrorUtilities.VerifyThrow(_activeProxy == true, "Attempted to use an inactive task factory logging host."); + } + } +} diff --git a/src/XMakeBuildEngine/Instance/TaskFactoryWrapper.cs b/src/XMakeBuildEngine/Instance/TaskFactoryWrapper.cs new file mode 100644 index 00000000000..143a0076e73 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/TaskFactoryWrapper.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Wraps a task factory and provides helper methods to gather the parameters +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Execution +{ + /// + /// This class packages information about task which has been loaded from a task factory. + /// + internal sealed class TaskFactoryWrapper + { + #region Data + + /// + /// Factory which is wrapped by the wrapper + /// + private ITaskFactory _taskFactory; + + /// + /// Cache of names of required properties on this type + /// + private IDictionary _namesOfPropertiesWithRequiredAttribute; + + /// + /// Cache of names of output properties on this type + /// + private IDictionary _namesOfPropertiesWithOutputAttribute; + + /// + /// Cache of names of properties on this type whose names are ambiguous + /// + private IDictionary _namesOfPropertiesWithAmbiguousMatches; + + /// + /// Cache of PropertyInfos for this type + /// + private IDictionary _propertyInfoCache; + + /// + /// The name of the task this factory can create. + /// + private string _taskName; + + /// + /// The set of special parameters that, along with the name, contribute to the identity of + /// this factory. + /// + private IDictionary _factoryIdentityParameters; + + #endregion + + #region Constructors + + /// + /// Creates an instance of this class for the given type. + /// + internal TaskFactoryWrapper(ITaskFactory taskFactory, LoadedType taskFactoryLoadInfo, string taskName, IDictionary factoryIdentityParameters) + { + ErrorUtilities.VerifyThrowArgumentNull(taskFactory, "taskFactory"); + ErrorUtilities.VerifyThrowArgumentLength(taskName, "taskName"); + _taskFactory = taskFactory; + _taskName = taskName; + TaskFactoryLoadedType = taskFactoryLoadInfo; + _factoryIdentityParameters = factoryIdentityParameters; + } + + #endregion + + #region Properties + + /// + /// Load information about the task factory itself + /// + public LoadedType TaskFactoryLoadedType + { + get; + private set; + } + + /// + /// The task factory wrapped by the wrapper + /// + public ITaskFactory TaskFactory + { + get + { + return _taskFactory; + } + } + + /// + /// Gets the list of names of public instance properties that have the required attribute applied. + /// Caches the result - since it can't change during the build. + /// + /// + public IDictionary GetNamesOfPropertiesWithRequiredAttribute + { + get + { + PopulatePropertyInfoCacheIfNecessary(); + + return _namesOfPropertiesWithRequiredAttribute; + } + } + + /// + /// Gets the list of names of public instance properties that have the output attribute applied. + /// Caches the result - since it can't change during the build. + /// + /// + public IDictionary GetNamesOfPropertiesWithOutputAttribute + { + get + { + PopulatePropertyInfoCacheIfNecessary(); + + return _namesOfPropertiesWithOutputAttribute; + } + } + + /// + /// Get the name of the factory wrapped by the wrapper + /// + public string Name + { + get + { + return _taskFactory.FactoryName; + } + } + + /// + /// The set of task identity parameters that were set on + /// this particular factory's UsingTask statement. + /// + public IDictionary FactoryIdentityParameters + { + get + { + return _factoryIdentityParameters; + } + } + + #endregion + + #region Methods. + + /// + /// Get the cached propertyinfo of the given name + /// + /// property name + /// PropertyInfo + public TaskPropertyInfo GetProperty(string propertyName) + { + PopulatePropertyInfoCacheIfNecessary(); + + TaskPropertyInfo propertyInfo; + if (!_propertyInfoCache.TryGetValue(propertyName, out propertyInfo)) + { + return null; + } + else + { + if (_namesOfPropertiesWithAmbiguousMatches.ContainsKey(propertyName)) + { + // See comment in PopulatePropertyInfoCache + throw new AmbiguousMatchException(); + } + + return propertyInfo; + } + } + + /// + /// Sets the given property on the task. + /// + internal void SetPropertyValue(ITask task, TaskPropertyInfo property, object value) + { + ErrorUtilities.VerifyThrowArgumentNull(task, "task"); + ErrorUtilities.VerifyThrowArgumentNull(property, "property"); + + IGeneratedTask generatedTask = task as IGeneratedTask; + if (generatedTask != null) + { + generatedTask.SetPropertyValue(property, value); + } + else + { + ReflectableTaskPropertyInfo propertyInfo = (ReflectableTaskPropertyInfo)property; + propertyInfo.Reflection.SetValue(task, value, null); + } + } + + /// + /// Gets the value of a given property on the given task. + /// + internal object GetPropertyValue(ITask task, TaskPropertyInfo property) + { + ErrorUtilities.VerifyThrowArgumentNull(task, "task"); + ErrorUtilities.VerifyThrowArgumentNull(property, "property"); + + IGeneratedTask generatedTask = task as IGeneratedTask; + if (generatedTask != null) + { + return generatedTask.GetPropertyValue(property); + } + else + { + ReflectableTaskPropertyInfo propertyInfo = property as ReflectableTaskPropertyInfo; + if (propertyInfo != null) + { + return propertyInfo.Reflection.GetValue(task, null); + } + else + { + ErrorUtilities.ThrowInternalError("Task does not implement IGeneratedTask and we don't have {0} either.", typeof(ReflectableTaskPropertyInfo).Name); + throw new InternalErrorException(); // unreachable + } + } + } + + /// + /// Determines whether a task with the given name is instantiable by this factory. + /// + /// Name of the task. + /// + /// true if this factory can instantiate such a task; otherwise, false. + /// + internal bool IsCreatableByFactory(string taskName) + { + return String.Equals(_taskName, taskName, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Populate the cache of PropertyInfos for this type + /// + private void PopulatePropertyInfoCacheIfNecessary() + { + if (_propertyInfoCache == null) + { + _propertyInfoCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Use a HybridDictionary because these are usually very small + _namesOfPropertiesWithRequiredAttribute = new HybridDictionary(StringComparer.OrdinalIgnoreCase); + _namesOfPropertiesWithOutputAttribute = new HybridDictionary(StringComparer.OrdinalIgnoreCase); + _namesOfPropertiesWithAmbiguousMatches = new HybridDictionary(StringComparer.OrdinalIgnoreCase); + + bool taskTypeImplementsIGeneratedTask = typeof(IGeneratedTask).IsAssignableFrom(_taskFactory.TaskType); + TaskPropertyInfo[] propertyInfos = _taskFactory.GetTaskParameters(); + + for (int i = 0; i < propertyInfos.Length; i++) + { + // If the task implements IGeneratedTask, we must use the TaskPropertyInfo the factory gives us. + // Otherwise, we never have to hand the TaskPropertyInfo back to the task or factory, so we replace + // theirs with one of our own that will allow us to cache reflection data per-property. + TaskPropertyInfo propertyInfo = propertyInfos[i]; + if (!taskTypeImplementsIGeneratedTask) + { + propertyInfo = new ReflectableTaskPropertyInfo(propertyInfo, _taskFactory.TaskType); + } + + try + { + _propertyInfoCache.Add(propertyInfo.Name, propertyInfo); + } + catch (ArgumentException) + { + // We have encountered a duplicate entry in our hashtable; if we had used BindingFlags.IgnoreCase this + // would have produced an AmbiguousMatchException. In the old code, before this cache existed, + // that wouldn't have been thrown unless and until the project actually tried to set this ambiguous parameter. + // So rather than fail here, we store a list of ambiguous names and throw later, when one of them + // is requested. + _namesOfPropertiesWithAmbiguousMatches[propertyInfo.Name] = String.Empty; + } + + if (propertyInfos[i].Required) + { + // we have a require attribute defined, keep a record of that + _namesOfPropertiesWithRequiredAttribute[propertyInfo.Name] = String.Empty; + } + + if (propertyInfos[i].Output) + { + // we have a output attribute defined, keep a record of that + _namesOfPropertiesWithOutputAttribute[propertyInfo.Name] = String.Empty; + } + } + + // Toss the dictionaries if we can as often they are empty (at least the last three are) + _propertyInfoCache = (_propertyInfoCache.Count == 0) ? ReadOnlyEmptyDictionary.Instance : _propertyInfoCache; + _namesOfPropertiesWithRequiredAttribute = (_namesOfPropertiesWithRequiredAttribute.Count == 0) ? ReadOnlyEmptyDictionary.Instance : _namesOfPropertiesWithRequiredAttribute; + _namesOfPropertiesWithOutputAttribute = (_namesOfPropertiesWithOutputAttribute.Count == 0) ? ReadOnlyEmptyDictionary.Instance : _namesOfPropertiesWithOutputAttribute; + _namesOfPropertiesWithAmbiguousMatches = (_namesOfPropertiesWithAmbiguousMatches.Count == 0) ? ReadOnlyEmptyDictionary.Instance : _namesOfPropertiesWithAmbiguousMatches; + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/Instance/TaskRegistry.cs b/src/XMakeBuildEngine/Instance/TaskRegistry.cs new file mode 100644 index 00000000000..5ad7c40d4a2 --- /dev/null +++ b/src/XMakeBuildEngine/Instance/TaskRegistry.cs @@ -0,0 +1,1632 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Consults stored task declarations (from UsingTask tags) to return the appropriate Type for a requested task name. +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Xml; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.BackEnd; + +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using TaskEngineAssemblyResolver = Microsoft.Build.BackEnd.Logging.TaskEngineAssemblyResolver; +using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; +using System.Collections.ObjectModel; + +namespace Microsoft.Build.Execution +{ + /// + /// 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. + /// + /// + /// 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 + /// + /// + /// <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)\"/> + /// + internal sealed class TaskRegistry + { + /// + /// The fallback task registry + /// + private readonly Toolset _toolset; + + /// + /// 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. + /// + private static bool s_forceTaskHostLaunch = (Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"); + + /// + /// Simple name for the MSBuild tasks (v4), used for shimming in loading + /// task factory UsingTasks + /// + private static string s_tasksV4SimpleName = "Microsoft.Build.Tasks.v4.0"; + + /// + /// Filename for the MSBuild tasks (v4), used for shimming in loading + /// task factory UsingTasks + /// + private static string s_tasksV4Filename = s_tasksV4SimpleName + ".dll"; + + /// + /// 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 + /// + private static string s_potentialTasksV4Location = Path.Combine(FileUtilities.CurrentExecutableDirectory, s_tasksV4Filename); + + /// + /// Simple name for the MSBuild tasks (v12), used for shimming in loading + /// task factory UsingTasks + /// + private static string s_tasksV12SimpleName = "Microsoft.Build.Tasks.v12.0"; + + /// + /// Filename for the MSBuild tasks (v12), used for shimming in loading + /// task factory UsingTasks + /// + private static string s_tasksV12Filename = s_tasksV12SimpleName + ".dll"; + + /// + /// 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 + /// + private static string s_potentialTasksV12Location = Path.Combine(FileUtilities.CurrentExecutableDirectory, s_tasksV12Filename); + + /// + /// Simple name for the MSBuild tasks (v14+), used for shimming in loading + /// task factory UsingTasks + /// + private static string s_tasksCoreSimpleName = "Microsoft.Build.Tasks.Core"; + + /// + /// Filename for the MSBuild tasks (v14+), used for shimming in loading + /// task factory UsingTasks + /// + private static string s_tasksCoreFilename = s_tasksCoreSimpleName + ".dll"; + + /// + /// 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 + /// + private static string s_potentialTasksCoreLocation = Path.Combine(FileUtilities.CurrentExecutableDirectory, s_tasksCoreFilename); + + /// + /// Cache of tasks already found using exact matching, + /// keyed by the task identity requested. + /// + private Dictionary _cachedTaskRecordsWithExactMatch; + + /// + /// 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. + /// + private Dictionary> _cachedTaskRecordsWithFuzzyMatch; + + /// + /// 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. + /// + private Dictionary> _taskRegistrations; + + /// + /// The cache to load the *.tasks files into + /// + private ProjectRootElementCache _projectRootElementCache; + + /// + /// 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 + /// + internal TaskRegistry(ProjectRootElementCache projectRootElementCache) + { + ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, "projectRootElementCache"); + + _projectRootElementCache = projectRootElementCache; + } + + /// + /// 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. + /// + /// The Toolset containing the toolser task registry + internal TaskRegistry(Toolset toolset, ProjectRootElementCache projectRootElementCache) + { + ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, "projectRootElementCache"); + ErrorUtilities.VerifyThrowInternalNull(toolset, "toolset"); + + _projectRootElementCache = projectRootElementCache; + _toolset = toolset; + } + + /// + /// Returns the toolset state used to initialize this registry, if any. + /// + internal Toolset Toolset + { + [DebuggerStepThrough] + get + { return _toolset; } + } + + /// + /// Access list of task registrations. + /// FOR UNIT TESTING ONLY. + /// + internal IDictionary> TaskRegistrations + { + get + { + if (null == _taskRegistrations) + { + _taskRegistrations = new Dictionary>(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact); + } + + return _taskRegistrations; + } + } + + /// + /// Evaluate the usingtask and add the result into the data passed in + /// + /// A type derived from IProperty + /// A type derived from IItem + internal static void RegisterTasksFromUsingTaskElement + ( + ILoggingService loggingService, + BuildEventContext buildEventContext, + string directoryOfImportingFile, + ProjectUsingTaskElement projectUsingTaskXml, + TaskRegistry taskRegistry, + Expander expander, + ExpanderOptions expanderOptions + ) + where P : class, IProperty + where I : class, IItem + { + ErrorUtilities.VerifyThrowInternalNull(directoryOfImportingFile, "directoryOfImportingFile"); + + if (!ConditionEvaluator.EvaluateCondition + ( + projectUsingTaskXml.Condition, + ParserOptions.AllowPropertiesAndItemLists, + expander, + expanderOptions, + projectUsingTaskXml.ContainingProject.DirectoryPath, + projectUsingTaskXml.ConditionLocation, + loggingService, + buildEventContext + )) + { + 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 node was defined -- we + // don't want paths from imported projects being interpreted relative to the main project file. + try + { + if (assemblyFile != null && !Path.IsPathRooted(assemblyFile)) + { + assemblyFile = 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) + ) + { + string replacedAssemblyFile = Path.Combine(Path.GetDirectoryName(assemblyFile), s_tasksCoreFilename); + + if (FileUtilities.FileExistsNoThrow(replacedAssemblyFile)) + { + 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) && + FileUtilities.FileExistsNoThrow(s_potentialTasksCoreLocation) + ) + { + assemblyName = s_tasksCoreSimpleName; + } + else if + ( + assemblyName.Equals(s_tasksV12SimpleName, StringComparison.OrdinalIgnoreCase) && + !FileUtilities.FileExistsNoThrow(s_potentialTasksV12Location) && + FileUtilities.FileExistsNoThrow(s_potentialTasksCoreLocation) + ) + { + 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(projectUsingTaskXml, expander, expanderOptions); + } + + IDictionary taskFactoryParameters = null; + string runtime = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.Runtime, expanderOptions, projectUsingTaskXml.RuntimeLocation); + string architecture = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.Architecture, expanderOptions, projectUsingTaskXml.ArchitectureLocation); + + if ((runtime != String.Empty) || (architecture != String.Empty)) + { + taskFactoryParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + 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); + } + + /// + /// 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. + /// + internal TaskFactoryWrapper GetRegisteredTask + ( + string taskName, + string taskProjectFile, + IDictionary taskIdentityParameters, + bool exactMatchRequired, + TargetLoggingContext targetLoggingContext, + ElementLocation elementLocation + ) + { + TaskFactoryWrapper taskFactory = null; + bool retrievedFromCache = false; + + // 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 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; + } + + /// + /// Retrieves the task registration record for the specified task. + /// + /// The name of the task to retrieve. + /// The task's project file. + /// The set of task identity parameters to be used to identify the + /// correct task record match. + /// True if an exact name match is required. + /// The logging context. + /// The location of the task element in the project file. + /// True if the record was retrieved from the cache. + /// The task registration record, or null if none was found. + internal RegisteredTaskRecord GetTaskRegistrationRecord + ( + string taskName, + string taskProjectFile, + IDictionary taskIdentityParameters, + bool exactMatchRequired, + TargetLoggingContext targetLoggingContext, + ElementLocation elementLocation, + out bool retrievedFromCache + ) + { + RegisteredTaskRecord taskRecord = null; + retrievedFromCache = false; + RegisteredTaskIdentity taskIdentity = new RegisteredTaskIdentity(taskName, taskIdentityParameters); + + // Try the override task registry first + if (_toolset != null) + { + TaskRegistry toolsetRegistry = _toolset.GetOverrideTaskRegistry(targetLoggingContext.LoggingService, targetLoggingContext.BuildEventContext, _projectRootElementCache); + taskRecord = toolsetRegistry.GetTaskRegistrationRecord(taskName, taskProjectFile, taskIdentityParameters, exactMatchRequired, targetLoggingContext, elementLocation, out retrievedFromCache); + } + + // Try the current task registry + if (taskRecord == null && _taskRegistrations != null && _taskRegistrations.Count > 0) + { + if (exactMatchRequired) + { + if (_cachedTaskRecordsWithExactMatch != null && _cachedTaskRecordsWithExactMatch.TryGetValue(taskIdentity, out taskRecord)) + { + retrievedFromCache = true; + return taskRecord; + } + } + else + { + HybridDictionary taskRecords; + + if (_cachedTaskRecordsWithFuzzyMatch != null && _cachedTaskRecordsWithFuzzyMatch.TryGetValue(taskIdentity.Name, out 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. + } + } + + Dictionary> registrations = GetRelevantRegistrations(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 + foreach (KeyValuePair> registration in registrations) + { + // 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 > registration.Key.Name.Length) ? taskName : registration.Key.Name; + + taskRecord = GetMatchingRegistration(mostSpecificTaskName, registration.Value, taskProjectFile, taskIdentityParameters, targetLoggingContext, elementLocation); + + if (taskRecord != null) + { + break; + } + } + } + + // 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.LoggingService, targetLoggingContext.BuildEventContext, _projectRootElementCache); + 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 = _cachedTaskRecordsWithExactMatch ?? new Dictionary(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact); + _cachedTaskRecordsWithExactMatch[taskIdentity] = taskRecord; + } + else + { + _cachedTaskRecordsWithFuzzyMatch = _cachedTaskRecordsWithFuzzyMatch ?? new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // 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) + HybridDictionary taskRecords; + if (!_cachedTaskRecordsWithFuzzyMatch.TryGetValue(taskIdentity.Name, out taskRecords)) + { + taskRecords = new HybridDictionary(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact); + } + + taskRecords[taskIdentity] = taskRecord; + _cachedTaskRecordsWithFuzzyMatch[taskIdentity.Name] = taskRecords; + } + + return taskRecord; + } + + /// + /// Is the class being loaded a task factory class + /// + private static bool IsTaskFactoryClass(Type type, object unused) + { + return (type.IsClass && + !type.IsAbstract && + typeof(Microsoft.Build.Framework.ITaskFactory).IsAssignableFrom(type)); + } + + /// + /// 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. + /// + private Dictionary> GetRelevantRegistrations(RegisteredTaskIdentity taskIdentity, bool exactMatchRequired) + { + Dictionary> relevantTaskRegistrations = + new Dictionary>(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact); + + List taskAssemblies; + + // if we find an exact match + if (_taskRegistrations.TryGetValue(taskIdentity, out taskAssemblies)) + { + // we're done + relevantTaskRegistrations[taskIdentity] = taskAssemblies; + return relevantTaskRegistrations; + } + + if (exactMatchRequired) + { + return relevantTaskRegistrations; + } + + // look through all task declarations for partial matches + foreach (KeyValuePair> taskRegistration in _taskRegistrations) + { + if (RegisteredTaskIdentity.RegisteredTaskIdentityComparer.IsPartialMatch(taskIdentity, taskRegistration.Key)) + { + relevantTaskRegistrations[taskRegistration.Key] = taskRegistration.Value; + } + } + + return relevantTaskRegistrations; + } + + /// + /// Registers an evaluated using task tag for future + /// consultation + /// + private void RegisterTask(string taskName, AssemblyLoadInfo assemblyLoadInfo, string taskFactory, IDictionary taskFactoryParameters, RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord) + { + ErrorUtilities.VerifyThrowInternalLength(taskName, "taskName"); + ErrorUtilities.VerifyThrowInternalNull(assemblyLoadInfo, "assemblyLoadInfo"); + + // Lazily allocate the hashtable + if (_taskRegistrations == null) + { + _taskRegistrations = new Dictionary>(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact); + } + + // 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 registeredTaskEntries; + RegisteredTaskIdentity taskIdentity = new RegisteredTaskIdentity(taskName, taskFactoryParameters); + if (!_taskRegistrations.TryGetValue(taskIdentity, out registeredTaskEntries)) + { + registeredTaskEntries = new List(); + _taskRegistrations[taskIdentity] = registeredTaskEntries; + } + + registeredTaskEntries.Add(new RegisteredTaskRecord(taskName, assemblyLoadInfo, taskFactory, taskFactoryParameters, inlineTaskRecord)); + } + + /// + /// 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). + /// + private RegisteredTaskRecord GetMatchingRegistration + ( + string taskName, + List taskRecords, + string taskProjectFile, + IDictionary taskIdentityParameters, + TargetLoggingContext targetLoggingContext, + ElementLocation elementLocation + ) + { + foreach (RegisteredTaskRecord record in taskRecords) + { + if (record.CanTaskBeCreatedByFactory(taskName, taskProjectFile, taskIdentityParameters, targetLoggingContext, elementLocation)) + { + return record; + } + } + + // Cannot find the task in any of the records + return null; + } + + /// + /// An object representing the identity of a task -- not just task name, but also + /// the set of identity parameters + /// + [DebuggerDisplay("{Name} ParameterCount = {TaskIdentityParameters.Count}")] + internal class RegisteredTaskIdentity + { + /// + /// Constructor + /// + internal RegisteredTaskIdentity(string name, IDictionary taskIdentityParameters) + { + Name = name; + + // The ReadOnlyDictionary is a *wrapper*, the Dictionary is the copy. + TaskIdentityParameters = taskIdentityParameters == null ? null : new ReadOnlyDictionary(new Dictionary(taskIdentityParameters, StringComparer.OrdinalIgnoreCase)); + } + + /// + /// The name of the task + /// + public string Name + { + get; + private set; + } + + /// + /// The identity parameters + /// + public IDictionary TaskIdentityParameters + { + get; + private set; + } + + /// + /// Comparer used to figure out whether two RegisteredTaskIdentities are equal or not. + /// + internal class RegisteredTaskIdentityComparer : IEqualityComparer + { + /// + /// The singleton comparer to use when an exact match is desired + /// + private static RegisteredTaskIdentityComparer s_exact = new RegisteredTaskIdentityComparer(true /* exact match */); + + /// + /// 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. + /// + private static RegisteredTaskIdentityComparer s_fuzzy = new RegisteredTaskIdentityComparer(false /* fuzzy match */); + + /// + /// Keeps track of whether we're doing exact or fuzzy equivalency + /// + private bool _exactMatchRequired; + + /// + /// Constructor + /// + private RegisteredTaskIdentityComparer(bool exactMatchRequired) + { + _exactMatchRequired = exactMatchRequired; + } + + /// + /// The singleton comparer to use for when an exact match is desired + /// + public static RegisteredTaskIdentityComparer Exact + { + get { return s_exact; } + } + + /// + /// The singleton comparer to use for when a fuzzy match is desired + /// + public static RegisteredTaskIdentityComparer Fuzzy + { + get { return s_fuzzy; } + } + + /// + /// 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. + /// + 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; + } + } + + /// + /// Returns true if the two task identities are equal; false otherwise. + /// + 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; + } + } + + /// + /// Returns a hash code for the given task identity + /// + public int GetHashCode(RegisteredTaskIdentity obj) + { + if (obj == null) + { + return 0; + } + + int nameHash = String.IsNullOrEmpty(obj.Name) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name); + int paramHash = 0; + + // 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); + } + + 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; + } + + /// + /// 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. + /// + private static bool IdentityParametersMatch(IDictionary x, IDictionary 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 param in x) + { + string value = null; + + if (y.TryGetValue(param.Key, out value)) + { + if (!String.Equals(param.Value, value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + else + { + return false; + } + } + } + else + { + // we need to fuzzy-match the parameters as well + string runtimeX = null; + string runtimeY = null; + string architectureX = null; + string architectureY = null; + + if (x != null) + { + x.TryGetValue(XMakeAttributes.runtime, out runtimeX); + x.TryGetValue(XMakeAttributes.architecture, out architectureX); + } + + if (y != null) + { + y.TryGetValue(XMakeAttributes.runtime, out runtimeY); + y.TryGetValue(XMakeAttributes.architecture, out architectureY); + } + + // null is OK -- it's treated as a "don't care" + if (!XMakeAttributes.RuntimeValuesMatch(runtimeX, runtimeY)) + { + return false; + } + + if (!XMakeAttributes.ArchitectureValuesMatch(architectureX, architectureY)) + { + return false; + } + } + + // if we didn't return before now, all parameters were found and matched. + return true; + } + } + } + + /// + /// A record for a task registration which also contains the factory which matches the record + /// + internal class RegisteredTaskRecord + { + /// + /// Default task factory to use if one is not specified + /// + internal const string AssemblyTaskFactory = "AssemblyTaskFactory"; + + /// + /// Default task factory to use if one is not specified and runtime or architecture is specified + /// + internal const string TaskHostFactory = "TaskHostFactory"; + + /// + /// Task factory used to create CodeDom-based inline tasks. Special-cased as one of two officially + /// supported task factories in Microsoft.Build.Tasks.vX.Y.dll to deal with versioning issue. + /// + internal const string CodeTaskFactory = "CodeTaskFactory"; + + /// + /// Task factory used to create CodeDom-based inline tasks. Special-cased as one of two officially + /// supported task factories in Microsoft.Build.Tasks.vX.Y.dll to deal with versioning issue. + /// + internal const string XamlTaskFactory = "XamlTaskFactory"; + + /// + /// Lock for the taskFactoryTypeLoader + /// + private static readonly Object s_taskFactoryTypeLoaderLock = new Object(); + +#if DEBUG + /// + /// Inform users that this is a problem from a task factory, a bug should be opened against the factory user + /// + private const string UnhandledFactoryError = "\nThis is an unhandled exception from a task factory-- PLEASE OPEN A BUG AGAINST THE TASK FACTORY OWNER. "; +#endif + + /// + /// Type filter to make sure we only look for taskFactoryClasses + /// + private static readonly TypeFilter s_taskFactoryTypeFilter = new TypeFilter(IsTaskFactoryClass); + + /// + /// Identity of this task. + /// + private readonly RegisteredTaskIdentity _taskIdentity; + + /// + /// Typeloader for taskFactories + /// + private static TypeLoader s_taskFactoryTypeLoader; + + /// + /// The task name this record was registered with from the using task element + /// + private string _registeredName; + + /// + /// The assembly information about the task factory to be instantiated. For + /// AssemblyTaskFactories this is the task assembly which should be loaded + /// + private AssemblyLoadInfo _taskFactoryAssemblyLoadInfo; + + /// + /// The task factory class name which will be used to lookup the task factory from the assembly specified in the assemblyName or assemblyFile. + /// + private string _taskFactory; + + /// + /// A task factory wrapper which caches and combines information related to the parameters of the task. + /// + private TaskFactoryWrapper _taskFactoryWrapperInstance; + + /// + /// Cache of task names which can be created by the factory. + /// When ever a taskName is checked against the factory we cache the result so we do not have to + /// make possibly expensive calls over and over again. + /// + private Dictionary _taskNamesCreatableByFactory; + + /// + /// Set of parameters that can be used by the task factory specifically. + /// + private IDictionary _taskFactoryParameters; + + /// + /// Encapsulates the parameters and the body of the task element for the inline task. + /// + private ParameterGroupAndTaskElementRecord _parameterGroupAndTaskBody; + + /// + /// Constructor + /// + internal RegisteredTaskRecord(string registeredName, AssemblyLoadInfo assemblyLoadInfo, string taskFactory, IDictionary taskFactoryParameters, ParameterGroupAndTaskElementRecord inlineTask) + { + ErrorUtilities.VerifyThrowArgumentNull(assemblyLoadInfo, "AssemblyLoadInfo"); + _registeredName = registeredName; + _taskFactoryAssemblyLoadInfo = assemblyLoadInfo; + _taskFactoryParameters = taskFactoryParameters; + _taskIdentity = new RegisteredTaskIdentity(registeredName, taskFactoryParameters); + _parameterGroupAndTaskBody = inlineTask; + + if (String.IsNullOrEmpty(taskFactory)) + { + if (taskFactoryParameters != null) + { + ErrorUtilities.VerifyThrow(taskFactoryParameters.Count == 2, "if the parameter dictionary is non-null, it should contain both parameters when we get here!"); + } + + _taskFactory = AssemblyTaskFactory; + } + else + { + _taskFactory = taskFactory; + } + + if (inlineTask == null) + { + _parameterGroupAndTaskBody = new ParameterGroupAndTaskElementRecord(); + } + } + + /// + /// Gets the task name this record was registered with. + /// + internal string RegisteredName + { + [DebuggerStepThrough] + get + { return _registeredName; } + } + + /// + /// Gets the assembly load information. + /// + internal AssemblyLoadInfo TaskFactoryAssemblyLoadInfo + { + [DebuggerStepThrough] + get + { return _taskFactoryAssemblyLoadInfo; } + } + + /// + /// Gets the task factory attribute value. + /// + internal string TaskFactoryAttributeName + { + [DebuggerStepThrough] + get + { return _taskFactory; } + } + + /// + /// Gets the set of parameters for the task factory + /// + internal IDictionary TaskFactoryParameters + { + [DebuggerStepThrough] + get + { return _taskFactoryParameters; } + } + + /// + /// Gets the inline task record + /// + internal ParameterGroupAndTaskElementRecord ParameterGroupAndTaskBody + { + [DebuggerStepThrough] + get + { return _parameterGroupAndTaskBody; } + } + + /// + /// Identity of this task. + /// + private RegisteredTaskIdentity TaskIdentity + { + get { return _taskIdentity; } + } + + /// + /// Ask the question, whether or not the task name can be created by the task factory. + /// To answer this question we need to instantiate and initialize the task factory and ask it if it can create the given task name. + /// This question is useful for assembly tasks where the task may or may not be in an assembly, this can also be useful if the task factory + /// loads an external file and uses that to generate the tasks. + /// + /// true if the task can be created by the factory, false if it cannot be created + internal bool CanTaskBeCreatedByFactory(string taskName, string taskProjectFile, IDictionary taskIdentityParameters, TargetLoggingContext targetLoggingContext, ElementLocation elementLocation) + { + // Keep a cache of task identities which have been checked against the factory, this is useful because we ask this question everytime we get a registered task record or a taskFactory wrapper. + if (_taskNamesCreatableByFactory == null) + { + _taskNamesCreatableByFactory = new Dictionary(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact); + } + + RegisteredTaskIdentity taskIdentity = new RegisteredTaskIdentity(taskName, taskIdentityParameters); + + // See if the task name as already been checked against the factory, return the value if it has + object creatableByFactory = null; + if (!_taskNamesCreatableByFactory.TryGetValue(taskIdentity, out creatableByFactory)) + { + try + { + bool haveTaskFactory = GetTaskFactory(targetLoggingContext, elementLocation, taskProjectFile); + + // Create task Factory will only actually create a factory once. + if (haveTaskFactory) + { + // If we are an AssemblyTaskFactory we can use the fact we are internal to the engine assembly to do some logging / exception throwing that regular factories cannot do, + // this is requried to remain compatible with orcas in terms of exceptions thrown / messages logged when a task cannot be found in an assembly. + if (TaskFactoryAttributeName == AssemblyTaskFactory || TaskFactoryAttributeName == TaskHostFactory) + { + // Also we only need to check to see if the task name can be created by the factory if the taskName does not equal the Registered name + // and the identity parameters don't match the factory's declared parameters. + // This is because when the task factory is instantiated we try and load the Registered name from the task factory and fail it it cannot be loaded + // therefore the fact that we have a factory means the Registered type and parameters can be created by the factory. + if (RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Fuzzy.Equals(this.TaskIdentity, taskIdentity)) + { + creatableByFactory = this; + } + else + { + // The method will handle exceptions related to asking if a task can be created and will throw an Invalid project file exception if there is a problem + bool createable = ((AssemblyTaskFactory)_taskFactoryWrapperInstance.TaskFactory).TaskNameCreatableByFactory(taskName, taskIdentityParameters, taskProjectFile, targetLoggingContext, elementLocation); + + if (createable) + { + creatableByFactory = this; + } + else + { + creatableByFactory = null; + } + } + } + else + { + // Wrap arbitrary task factory calls because we do not know what kind of error handling they are doing. + try + { + bool createable = _taskFactoryWrapperInstance.IsCreatableByFactory(taskName); + + if (createable) + { + creatableByFactory = this; + } + else + { + creatableByFactory = null; + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Log e.ToString to give as much information about the failure of a "third party" call as possible. + string message = String.Empty; +#if DEBUG + message += UnhandledFactoryError; +#endif + message += e.ToString(); + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskLoadFailure", taskName, _taskFactoryWrapperInstance.Name, message); + } + } + } + } + finally + { + _taskNamesCreatableByFactory[taskIdentity] = creatableByFactory; + } + } + + return creatableByFactory != null; + } + + /// + /// Given a Registered task record and a task name. Check create an instance of the task factory using the record. + /// If the factory is a assembly task factory see if the assemblyFile has the correct task inside of it. + /// + internal TaskFactoryWrapper GetTaskFactoryFromRegistrationRecord(string taskName, string taskProjectFile, IDictionary taskIdentityParameters, TargetLoggingContext targetLoggingContext, ElementLocation elementLocation) + { + if (CanTaskBeCreatedByFactory(taskName, taskProjectFile, taskIdentityParameters, targetLoggingContext, elementLocation)) + { + return _taskFactoryWrapperInstance; + } + + return null; + } + + /// + /// Create an instance of the task factory and load it from the assembly. + /// + /// If the task factory could not be properly created an InvalidProjectFileException will be thrown + private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLocation elementLocation, string taskProjectFile) + { + // see if we have already created the factory before, only create it once + if (_taskFactoryWrapperInstance == null) + { + AssemblyLoadInfo taskFactoryLoadInfo = TaskFactoryAssemblyLoadInfo; + ErrorUtilities.VerifyThrow(taskFactoryLoadInfo != null, "TaskFactoryLoadInfo should never be null"); + ITaskFactory factory = null; + LoadedType loadedType = null; + + bool isAssemblyTaskFactory = String.Equals(TaskFactoryAttributeName, AssemblyTaskFactory, StringComparison.OrdinalIgnoreCase); + bool isTaskHostFactory = String.Equals(TaskFactoryAttributeName, TaskHostFactory, StringComparison.OrdinalIgnoreCase); + + if (isAssemblyTaskFactory || isTaskHostFactory) + { + bool explicitlyLaunchTaskHost = + isTaskHostFactory || + ( + s_forceTaskHostLaunch && + !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "MSBuild") && + !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "CallTarget") + ); + + // Create an instance of the internal assembly task factory, it has the error handling built into its methods. + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + loadedType = taskFactory.InitializeFactory(taskFactoryLoadInfo, RegisteredName, ParameterGroupAndTaskBody.UsingTaskParameters, ParameterGroupAndTaskBody.InlineTaskXmlBody, TaskFactoryParameters, explicitlyLaunchTaskHost, targetLoggingContext, elementLocation, taskProjectFile); + factory = taskFactory; + } + else + { + // We are not one of the default factories. + TaskEngineAssemblyResolver resolver = null; + + try + { + // Add a resolver to allow us to resolve types from the assembly when loading into the current appdomain. + resolver = new TaskEngineAssemblyResolver(); + resolver.Initialize(taskFactoryLoadInfo.AssemblyFile); + resolver.InstallHandler(); + + try + { + lock (s_taskFactoryTypeLoaderLock) + { + if (s_taskFactoryTypeLoader == null) + { + s_taskFactoryTypeLoader = new TypeLoader(s_taskFactoryTypeFilter); + } + } + + // Make sure we only look for task factory classes when loading based on the name + loadedType = s_taskFactoryTypeLoader.Load(TaskFactoryAttributeName, taskFactoryLoadInfo); + + if (loadedType == null) + { + // We could not find the type (this is what null means from the Load method) but there is no reason given so we can only log the fact that + // we could not find the name given in the task factory attribute in the class specified in the assembly File or assemblyName fields. + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "CouldNotFindFactory", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation); + } + + targetLoggingContext.LogComment(MessageImportance.Low, "InitializingTaskFactory", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation); + } + catch (TargetInvocationException e) + { + // Exception thrown by the called code itself + // Log the stack, so the task vendor can fix their code + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskFactoryLoadFailure", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation, Environment.NewLine + e.InnerException.ToString()); + } + catch (ReflectionTypeLoadException e) + { + // ReflectionTypeLoadException.LoaderExceptions may contain nulls + foreach (Exception exception in e.LoaderExceptions) + { + if (exception != null) + { + targetLoggingContext.LogError(new BuildEventFileInfo(taskProjectFile), "TaskFactoryLoadFailure", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation, exception.Message); + } + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskFactoryLoadFailure", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation, e.Message); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedReflectionException(e)) + { + throw; + } + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskFactoryLoadFailure", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation, e.Message); + } + + try + { + // We have loaded the type, lets now try and construct it + // Any exceptions from the constructor of the task factory will be caught lower down and turned into an InvalidProjectFileExceptions + factory = (ITaskFactory)AppDomain.CurrentDomain.CreateInstanceAndUnwrap(loadedType.Type.Assembly.FullName, loadedType.Type.FullName); + TaskFactoryLoggingHost taskFactoryLoggingHost = new TaskFactoryLoggingHost(true /*I dont have the data at this point, the safest thing to do is make sure events are serializable*/, elementLocation, targetLoggingContext); + + bool initialized = false; + try + { + ITaskFactory2 factory2 = factory as ITaskFactory2; + if (factory2 != null) + { + initialized = factory2.Initialize(RegisteredName, TaskFactoryParameters, ParameterGroupAndTaskBody.UsingTaskParameters, ParameterGroupAndTaskBody.InlineTaskXmlBody, taskFactoryLoggingHost); + } + else + { + initialized = factory.Initialize(RegisteredName, ParameterGroupAndTaskBody.UsingTaskParameters, ParameterGroupAndTaskBody.InlineTaskXmlBody, taskFactoryLoggingHost); + + // TaskFactoryParameters will always be null unless specifically created to have runtime and architecture parameters. + if (TaskFactoryParameters != null) + { + targetLoggingContext.LogWarning + ( + new BuildEventFileInfo(elementLocation), + null, + "TaskFactoryWillIgnoreTaskFactoryParameters", + factory.FactoryName, + XMakeAttributes.runtime, + XMakeAttributes.architecture, + RegisteredName + ); + } + } + } + finally + { + taskFactoryLoggingHost.MarkAsInactive(); + } + + if (!initialized) + { + _taskFactoryWrapperInstance = null; + return false; + } + } + catch (InvalidCastException e) + { + string message = String.Empty; +#if DEBUG + message += UnhandledFactoryError; +#endif + message += e.Message; + + // Could get an invalid cast when Creating Instance and UnWrap due to the framework assembly not being the same. + targetLoggingContext.LogError + ( + new BuildEventFileInfo(elementLocation.File, elementLocation.Line, elementLocation.Column), + "TaskFactoryInstantiationFailureErrorInvalidCast", + TaskFactoryAttributeName, + taskFactoryLoadInfo.AssemblyLocation, + message + ); + + return false; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + string message = String.Empty; +#if DEBUG + message += UnhandledFactoryError; +#endif + // message += e.ToString(); + message += e.Message; + + ProjectErrorUtilities.ThrowInvalidProject(elementLocation, "TaskFactoryLoadFailure", TaskFactoryAttributeName, taskFactoryLoadInfo.AssemblyLocation, message); + } + } + finally + { + if (resolver != null) + { + resolver.RemoveHandler(); + resolver = null; + } + } + } + + _taskFactoryWrapperInstance = new TaskFactoryWrapper(factory, loadedType, RegisteredName, TaskFactoryParameters); + } + + return true; + } + + /// + /// Keep track of the xml which will be sent to the inline task factory and the parameters if any which will also be passed in + /// + internal class ParameterGroupAndTaskElementRecord + { + /// + /// The list of parameters found in the using task along with a corosponding UsingTaskParameterInfo which contains the specific information about it + /// Populated lazily as it is often empty. + /// + private IDictionary _usingTaskParameters; + + /// + /// The body of the task element which will be passed to the task factory. + /// + private string _inlineTaskXmlBody; + + /// + /// Was the task body evaluated or not + /// + private bool _taskBodyEvaluated; + + /// + /// Create an empty ParameterGroupAndTaskElementRecord + /// + internal ParameterGroupAndTaskElementRecord() + { + } + + /// + /// The parameters from the ParameterGroup from the using task element which will be passed to the task factory. + /// + internal IDictionary UsingTaskParameters + { + get { return _usingTaskParameters ?? ReadOnlyEmptyDictionary.Instance; } + } + + /// + /// The body of the task element which will be passed to the task factory. + /// + internal string InlineTaskXmlBody + { + get { return _inlineTaskXmlBody; } + } + + /// + /// Has the task body been passed to the expander to be expanded + /// + internal bool TaskBodyEvaluated + { + get { return _taskBodyEvaluated; } + } + + /// + /// Keep track of the xml which will be sent to the inline task factory and the parameters if any which will also be passed in + /// + /// Property type + /// Item Type + internal void ExpandUsingTask(ProjectUsingTaskElement projectUsingTaskXml, Expander expander, ExpanderOptions expanderOptions) + where P : class, IProperty + where I : class, IItem + { + ErrorUtilities.VerifyThrowArgumentNull(projectUsingTaskXml, "projectUsingTaskXml"); + ErrorUtilities.VerifyThrowArgumentNull(expander, "expander"); + + ProjectUsingTaskBodyElement taskElement = projectUsingTaskXml.TaskBody; + if (taskElement != null) + { + EvaluateTaskBody(expander, taskElement, expanderOptions); + } + + UsingTaskParameterGroupElement parameterGroupElement = projectUsingTaskXml.ParameterGroup; + + if (parameterGroupElement != null) + { + ParseUsingTaskParameterGroupElement(parameterGroupElement, expander, expanderOptions); + } + } + + /// + /// Evaluate the task body of the using task + /// + /// IProperttyTypes + /// IItems + private void EvaluateTaskBody(Expander expander, ProjectUsingTaskBodyElement taskElement, ExpanderOptions expanderOptions) + where P : class, IProperty + where I : class, IItem + { + bool evaluate; + string expandedType = expander.ExpandIntoStringLeaveEscaped(taskElement.Evaluate, expanderOptions, taskElement.EvaluateLocation); + + if (!bool.TryParse(expandedType, out evaluate)) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + taskElement.EvaluateLocation, + "InvalidEvaluatedAttributeValue", + expandedType, + taskElement.Evaluate, + XMakeAttributes.evaluate, + XMakeElements.usingTaskBody + ); + } + + _taskBodyEvaluated = evaluate; + + // If we need to evaluate then expand and evaluate the next inside of the body + if (evaluate) + { + _inlineTaskXmlBody = expander.ExpandIntoStringLeaveEscaped(taskElement.TaskBody, expanderOptions, taskElement.Location); + } + else + { + _inlineTaskXmlBody = taskElement.TaskBody; + } + } + + /// + /// Convert the UsingTaskParameterGroupElement into a list of parameter names and UsingTaskParameters + /// + /// Property type + /// Item types + private void ParseUsingTaskParameterGroupElement(UsingTaskParameterGroupElement usingTaskParameterGroup, Expander expander, ExpanderOptions expanderOptions) + where P : class, IProperty + where I : class, IItem + { + _usingTaskParameters = _usingTaskParameters ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Go through each of the parameters and create new ParameterInfo objects from them + foreach (ProjectUsingTaskParameterElement parameter in usingTaskParameterGroup.Parameters) + { + // Expand the type value before parsing it because it could be a property or item which needs to be expanded before it make sense + string expandedType = expander.ExpandIntoStringLeaveEscaped(parameter.ParameterType, expanderOptions, parameter.ParameterTypeLocation); + + // Cannot have a null or empty name for the type after expansion. + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + !String.IsNullOrEmpty(expandedType), + parameter.ParameterTypeLocation, + "InvalidEvaluatedAttributeValue", + expandedType, + parameter.ParameterType, + XMakeAttributes.parameterType, + XMakeElements.usingTaskParameter + ); + + // Try and get the type directly + Type paramType = Type.GetType(expandedType); + + // The type could not be got directly try and see if the type can be found by appending the FrameworkAssemblyName to it. + if (paramType == null) + { + paramType = Type.GetType(expandedType + "," + typeof(ITaskItem).Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */); + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + paramType != null, + parameter.ParameterTypeLocation, + "InvalidEvaluatedAttributeValue", + expandedType, + parameter.ParameterType, + XMakeAttributes.parameterType, + XMakeElements.usingTaskParameter + ); + } + + bool output; + string expandedOutput = expander.ExpandIntoStringLeaveEscaped(parameter.Output, expanderOptions, parameter.OutputLocation); + + if (!bool.TryParse(expandedOutput, out output)) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + parameter.OutputLocation, + "InvalidEvaluatedAttributeValue", + expandedOutput, + parameter.Output, + XMakeAttributes.output, + XMakeElements.usingTaskParameter + ); + } + + if ( + (!output && (!TaskParameterTypeVerifier.IsValidInputParameter(paramType))) || + (output && !(TaskParameterTypeVerifier.IsValidOutputParameter(paramType))) + ) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + parameter.Location, + "UnsupportedTaskParameterTypeError", + paramType.FullName, + parameter.ParameterType, + parameter.Name + ); + } + + bool required; + string expandedRequired = expander.ExpandIntoStringLeaveEscaped(parameter.Required, expanderOptions, parameter.RequiredLocation); + + if (!bool.TryParse(expandedRequired, out required)) + { + ProjectErrorUtilities.ThrowInvalidProject + ( + parameter.RequiredLocation, + "InvalidEvaluatedAttributeValue", + expandedRequired, + parameter.Required, + XMakeAttributes.required, + XMakeElements.usingTaskParameter + ); + } + + UsingTaskParameters.Add(parameter.Name, new TaskPropertyInfo(parameter.Name, paramType, output, required)); + } + } + } + } + } +} diff --git a/src/XMakeBuildEngine/Logging/BaseConsoleLogger.cs b/src/XMakeBuildEngine/Logging/BaseConsoleLogger.cs new file mode 100644 index 00000000000..34187689126 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/BaseConsoleLogger.cs @@ -0,0 +1,1277 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Collections; +using System.Globalization; +using System.IO; + +using ColorSetter = Microsoft.Build.Logging.ColorSetter; +using ColorResetter = Microsoft.Build.Logging.ColorResetter; +using WriteHandler = Microsoft.Build.Logging.WriteHandler; + +namespace Microsoft.Build.BackEnd.Logging +{ + #region Delegates + internal delegate void WriteLinePrettyFromResourceDelegate(int indentLevel, string resourceString, params object[] args); + #endregion + + abstract internal class BaseConsoleLogger : INodeLogger + { + #region Properties + /// + /// Gets or sets the level of detail to show in the event log. + /// + /// Verbosity level. + public LoggerVerbosity Verbosity + { + get + { + return verbosity; + } + + set + { + verbosity = value; + } + } + + public int NumberOfProcessors + { + get + { + return numberOfProcessors; + } + + set + { + numberOfProcessors = value; + } + } + + /// + /// The console logger takes a single parameter to suppress the output of the errors + /// and warnings summary at the end of a build. + /// + /// null + public string Parameters + { + get + { + return loggerParameters; + } + + set + { + loggerParameters = value; + } + } + + /// + /// Suppresses the display of project headers. Project headers are + /// displayed by default unless this property is set. + /// + /// This is only needed by the IDE logger. + internal bool SkipProjectStartedText + { + get + { + return skipProjectStartedText; + } + + set + { + skipProjectStartedText = value; + } + } + + /// + /// Suppresses the display of error and warnings summary. + /// + internal bool ShowSummary + { + get + { + return _showSummary ?? false; + } + + set + { + _showSummary = value; + } + } + + /// + /// Provide access to the write hander delegate so that it can be redirected + /// if necessary (e.g. to a file) + /// + protected WriteHandler WriteHandler + { + get + { + return write; + } + + set + { + write = value; + } + } + #endregion + + /// + /// Parses out the logger parameters from the Parameters string. + /// + public void ParseParameters() + { + if (loggerParameters != null) + { + string[] parameterComponents; + + parameterComponents = loggerParameters.Split(parameterDelimiters); + for (int param = 0; param < parameterComponents.Length; param++) + { + if (parameterComponents[param].Length > 0) + { + string[] parameterAndValue = parameterComponents[param].Split(s_parameterValueSplitCharacter); + + if (parameterAndValue.Length > 1) + { + ApplyParameter(parameterAndValue[0], parameterAndValue[1]); + } + else + { + ApplyParameter(parameterAndValue[0], null); + } + } + } + } + } + + /// + /// An implementation of IComparer useful for comparing the keys + /// on DictionaryEntry's + /// + /// Uses CurrentCulture for display purposes + internal class DictionaryEntryKeyComparer : IComparer + { + public int Compare(Object a, Object b) + { + return String.Compare( + (string)(((DictionaryEntry)a).Key), + (string)(((DictionaryEntry)b).Key), + true /*case insensitive*/, CultureInfo.CurrentCulture); + } + } + + /// + /// An implementation of IComparer useful for comparing the ItemSpecs + /// on ITaskItem's + /// + /// Uses CurrentCulture for display purposes + internal class ITaskItemItemSpecComparer : IComparer + { + public int Compare(Object a, Object b) + { + return String.Compare( + (string)(((ITaskItem)a).ItemSpec), + (string)(((ITaskItem)b).ItemSpec), + true /*case insensitive*/, CultureInfo.CurrentCulture); + } + } + + /// + /// Indents the given string by the specified number of spaces. + /// + /// String to indent. + /// Depth to indent. + internal string IndentString(string s, int indent) + { + // It's possible the event has a null message + if (null == s) + { + s = String.Empty; + } + + // This will never return an empty array. The returned array will always + // have at least one non-null element, even if "s" is totally empty. + String[] subStrings = SplitStringOnNewLines(s); + + StringBuilder result = new StringBuilder( + (subStrings.Length * indent) + + (subStrings.Length * Environment.NewLine.Length) + + s.Length); + + for (int i = 0; i < subStrings.Length; i++) + { + result.Append(' ', indent).Append(subStrings[i]); + result.AppendLine(); + } + + return result.ToString(); + } + + /// + /// Splits strings on 'newLines' with tollerance for Everett and Dogfood builds. + /// + /// String to split. + internal static string[] SplitStringOnNewLines(string s) + { + string[] subStrings = s.Split(newLines, StringSplitOptions.None); + return subStrings; + } + + /// + /// Writes a newline to the log. + /// + internal void WriteNewLine() + { + write(Environment.NewLine); + } + + /// + /// Writes a line from a resource string to the log, using the default indentation. + /// + /// + /// + internal void WriteLinePrettyFromResource(string resourceString, params object[] args) + { + int indentLevel = IsVerbosityAtLeast(LoggerVerbosity.Normal) ? this.currentIndentLevel : 0; + WriteLinePrettyFromResource(indentLevel, resourceString, args); + } + + /// + /// Writes a line from a resource string to the log, using the specified indentation. + /// + internal void WriteLinePrettyFromResource(int indentLevel, string resourceString, params object[] args) + { + string formattedString = ResourceUtilities.FormatResourceString(resourceString, args); + WriteLinePretty(indentLevel, formattedString); + } + + /// + /// Writes to the log, using the default indentation. Does not + /// terminate with a newline. + /// + internal void WritePretty(string formattedString) + { + int indentLevel = IsVerbosityAtLeast(LoggerVerbosity.Normal) ? this.currentIndentLevel : 0; + WritePretty(indentLevel, formattedString); + } + + /// + /// If requested, display a performance summary at the end of the build. This + /// shows how much time (and # hits) were spent inside of each project, target, + /// and task. + /// + internal void ShowPerfSummary() + { + // Show project performance summary. + if (projectPerformanceCounters != null) + { + setColor(ConsoleColor.Green); + WriteNewLine(); + WriteLinePrettyFromResource("ProjectPerformanceSummary"); + + setColor(ConsoleColor.Gray); + DisplayCounters(projectPerformanceCounters); + } + + // Show target performance summary. + if (targetPerformanceCounters != null) + { + setColor(ConsoleColor.Green); + WriteNewLine(); + WriteLinePrettyFromResource("TargetPerformanceSummary"); + + setColor(ConsoleColor.Gray); + DisplayCounters(targetPerformanceCounters); + } + + // Show task performance summary. + if (taskPerformanceCounters != null) + { + setColor(ConsoleColor.Green); + WriteNewLine(); + WriteLinePrettyFromResource("TaskPerformanceSummary"); + + setColor(ConsoleColor.Gray); + DisplayCounters(taskPerformanceCounters); + } + + resetColor(); + } + + /// + /// Writes to the log, using the specified indentation. Does not + /// terminate with a newline. + /// + internal void WritePretty(int indentLevel, string formattedString) + { + StringBuilder result = new StringBuilder(); + result.Append(' ', indentLevel * tabWidth).Append(formattedString); + write(result.ToString()); + } + + /// + /// Writes a line to the log, using the default indentation. + /// + /// + internal void WriteLinePretty(string formattedString) + { + int indentLevel = IsVerbosityAtLeast(LoggerVerbosity.Normal) ? this.currentIndentLevel : 0; + WriteLinePretty(indentLevel, formattedString); + } + + /// + /// Writes a line to the log, using the specified indentation. + /// + internal void WriteLinePretty(int indentLevel, string formattedString) + { + indentLevel = indentLevel > 0 ? indentLevel : 0; + write(IndentString(formattedString, indentLevel * tabWidth)); + } + + /// + /// Check to see what kind of device we are outputting the log to, is it a character device, a file, or something else + /// this can be used by loggers to modify their outputs based on the device they are writing to + /// + internal void IsRunningWithCharacterFileType() + { + // Get the std out handle + IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + + if (stdHandle != Microsoft.Build.BackEnd.NativeMethods.InvalidHandle) + { + uint fileType = NativeMethodsShared.GetFileType(stdHandle); + + // The std out is a char type(LPT or Console) + runningWithCharacterFileType = (fileType == NativeMethodsShared.FILE_TYPE_CHAR); + } + else + { + runningWithCharacterFileType = false; + } + } + + /// + /// Determines whether the current verbosity setting is at least the value + /// passed in. + /// + internal bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity) + { + return (this.verbosity >= checkVerbosity); + } + + /// + /// Sets foreground color to color specified + /// + internal static void SetColor(ConsoleColor c) + { + try + { + Console.ForegroundColor = + TransformColor(c, Console.BackgroundColor); + } + catch (IOException) + { + // Does not matter if we cannot set the color + } + } + + /// + /// Resets the color + /// + internal static void ResetColor() + { + try + { + Console.ResetColor(); + } + catch (IOException) + { + // The color could not be reset, no reason to crash + } + } + + /// + /// Changes the foreground color to black if the foreground is the + /// same as the background. Changes the foreground to white if the + /// background is black. + /// + /// foreground color for black + /// current background + internal static ConsoleColor TransformColor(ConsoleColor foreground, + ConsoleColor background) + { + ConsoleColor result = foreground; //typically do nothing ... + + if (foreground == background) + { + if (background != ConsoleColor.Black) + { + result = ConsoleColor.Black; + } + else + { + result = ConsoleColor.Gray; + } + } + + return result; + } + + /// + /// Does nothing, meets the ColorSetter delegate type + /// + /// foreground color (is ignored) + internal static void DontSetColor(ConsoleColor c) + { + // do nothing... + } + + /// + /// Does nothing, meets the ColorResetter delegate type + /// + internal static void DontResetColor() + { + // do nothing... + } + + internal void InitializeConsoleMethods(LoggerVerbosity logverbosity, WriteHandler logwriter, ColorSetter colorSet, ColorResetter colorReset) + { + this.verbosity = logverbosity; + this.write = logwriter; + IsRunningWithCharacterFileType(); + // This is a workaround, because the Console class provides no way to check that a color + // can actually be set or not. Color cannot be set if the console has been redirected + // in certain ways (e.g. how BUILD.EXE does it) + bool canSetColor = true; + + try + { + ConsoleColor c = Console.BackgroundColor; + } + catch (IOException) + { + // If the attempt to set a color fails with an IO exception then it is + // likely that the console has been redirected in a way that cannot + // cope with color (e.g. BUILD.EXE) so don't try to do color again. + canSetColor = false; + } + + if ((colorSet != null) && canSetColor) + { + this.setColor = colorSet; + } + else + { + this.setColor = new ColorSetter(DontSetColor); + } + + if ((colorReset != null) && canSetColor) + { + this.resetColor = colorReset; + } + else + { + this.resetColor = new ColorResetter(DontResetColor); + } + } + + /// + /// Writes out the list of property names and their values. + /// This could be done at any time during the build to show the latest + /// property values, using the cached reference to the list from the + /// appropriate ProjectStarted event. + /// + /// List of properties + internal void WriteProperties(ArrayList properties) + { + if (Verbosity == LoggerVerbosity.Diagnostic && showItemAndPropertyList) + { + if (properties.Count == 0) + { + return; + } + + OutputProperties(properties); + // Add a blank line + WriteNewLine(); + } + } + + + /// + /// Writes out the environment as seen on build started. + /// + internal void WriteEnvironment(IDictionary environment) + { + if (environment == null || environment.Count == 0) + { + return; + } + + if (Verbosity == LoggerVerbosity.Diagnostic || showEnvironment) + { + OutputEnvironment(environment); + + // Add a blank line + WriteNewLine(); + } + } + + + /// + /// Generate an arraylist which contains the properties referenced + /// by the properties enumerable object + /// + internal ArrayList ExtractPropertyList(IEnumerable properties) + { + // Gather a sorted list of all the properties. + ArrayList list = new ArrayList(); + foreach (DictionaryEntry prop in properties) + { + list.Add(prop); + } + list.Sort(new DictionaryEntryKeyComparer()); + return list; + } + + /// + /// Write the environment of the build as was captured on the build started event. + /// + internal virtual void OutputEnvironment(IDictionary environment) + { + // Write the banner + setColor(ConsoleColor.Green); + WriteLinePretty(currentIndentLevel, ResourceUtilities.FormatResourceString("EnvironmentHeader")); + + if (environment != null) + { + // Write each environment value one per line + foreach (KeyValuePair entry in environment) + { + setColor(ConsoleColor.Gray); + WritePretty(String.Format(CultureInfo.CurrentCulture, "{0,-30} = ", entry.Key)); + setColor(ConsoleColor.DarkGray); + WriteLinePretty((entry.Value)); + } + } + + resetColor(); + } + + internal virtual void OutputProperties(ArrayList list) + { + // Write the banner + setColor(ConsoleColor.Green); + WriteLinePretty(currentIndentLevel, ResourceUtilities.FormatResourceString("PropertyListHeader")); + // Write each property name and its value, one per line + foreach (DictionaryEntry prop in list) + { + setColor(ConsoleColor.Gray); + WritePretty(String.Format(CultureInfo.CurrentCulture, "{0,-30} = ", prop.Key)); + setColor(ConsoleColor.DarkGray); + WriteLinePretty(EscapingUtilities.UnescapeAll((string)prop.Value)); + } + resetColor(); + } + + /// + /// Writes out the list of item specs and their metadata. + /// This could be done at any time during the build to show the latest + /// items, using the cached reference to the list from the + /// appropriate ProjectStarted event. + /// + /// List of items + internal void WriteItems(SortedList itemTypes) + { + if (Verbosity == LoggerVerbosity.Diagnostic && showItemAndPropertyList) + { + if (itemTypes.Count == 0) + { + return; + } + + // Write the banner + setColor(ConsoleColor.Green); + WriteLinePretty(currentIndentLevel, ResourceUtilities.FormatResourceString("ItemListHeader")); + + // Write each item type and its itemspec, one per line + foreach (DictionaryEntry entry in itemTypes) + { + string itemType = (string)entry.Key; + ArrayList itemTypeList = (ArrayList)entry.Value; + + if (itemTypeList.Count == 0) + { + continue; + } + + // Sort the list by itemSpec + itemTypeList.Sort(new ITaskItemItemSpecComparer()); + OutputItems(itemType, itemTypeList); + } + + // Add a blank line + WriteNewLine(); + } + } + + /// + /// Extract the Items from the enumerable object and return a sorted list containing these items + /// + internal SortedList ExtractItemList(IEnumerable items) + { + // The "items" list is a flat list of itemtype-ITaskItem pairs. + // We would like to organize the ITaskItems into groups by itemtype. + + // Use a SortedList instead of an ArrayList (because we need to lookup fast) + // and instead of a Hashtable (because we need to sort it) + SortedList itemTypes = new SortedList(CaseInsensitiveComparer.Default); + foreach (DictionaryEntry item in items) + { + // Create a new list for this itemtype, if we haven't already + if (itemTypes[(string)item.Key] == null) + { + itemTypes[(string)item.Key] = new ArrayList(); + } + + // Add the item to the list for its itemtype + ArrayList itemsOfAType = (ArrayList)itemTypes[(string)item.Key]; + itemsOfAType.Add(item.Value); + } + + return itemTypes; + } + + /// + /// Dump the initial items provided. + /// Overridden in ParallelConsoleLogger. + /// + internal virtual void OutputItems(string itemType, ArrayList itemTypeList) + { + // Write each item, one per line + bool haveWrittenItemType = false; + setColor(ConsoleColor.DarkGray); + foreach (ITaskItem item in itemTypeList) + { + if (!haveWrittenItemType) + { + setColor(ConsoleColor.Gray); + WriteLinePretty(itemType); + haveWrittenItemType = true; + setColor(ConsoleColor.DarkGray); + } + WriteLinePretty(" " /* indent slightly*/ + item.ItemSpec); + + IDictionary metadata = item.CloneCustomMetadata(); + + foreach (DictionaryEntry metadatum in metadata) + { + // A metadatum's "value" is its escaped value, since that's how we represent them internally. + // So unescape before returning to the world at large. + WriteLinePretty(" " + metadatum.Key + " = " + item.GetMetadata(metadatum.Key as string)); + } + } + resetColor(); + } + + + /// + /// Returns a performance counter for a given scope (either task name or target name) + /// from the given table. + /// + /// Task name or target name. + /// Table that has tasks or targets. + /// + internal static PerformanceCounter GetPerformanceCounter(string scopeName, ref Hashtable table) + { + // Lazily construct the performance counter table. + if (table == null) + { + table = new Hashtable(StringComparer.OrdinalIgnoreCase); + } + + PerformanceCounter counter = (PerformanceCounter)table[scopeName]; + + // And lazily construct the performance counter itself. + if (counter == null) + { + counter = new PerformanceCounter(scopeName); + table[scopeName] = counter; + } + + return counter; + } + + /// + /// Display the timings for each counter in the hashtable + /// + /// + internal void DisplayCounters(Hashtable counters) + { + ArrayList perfCounters = new ArrayList(counters.Values.Count); + perfCounters.AddRange(counters.Values); + + perfCounters.Sort(PerformanceCounter.DescendingByElapsedTimeComparer); + + bool reentrantCounterExists = false; + + WriteLinePrettyFromResourceDelegate lineWriter = new WriteLinePrettyFromResourceDelegate(WriteLinePrettyFromResource); + + foreach (PerformanceCounter counter in perfCounters) + { + if (counter.ReenteredScope) + { + reentrantCounterExists = true; + } + + counter.PrintCounterMessage(lineWriter, setColor, resetColor); + } + + if (reentrantCounterExists) + { + // display an explanation of why there was no value displayed + WriteLinePrettyFromResource(4, "PerformanceReentrancyNote"); + } + } + + /// + /// Records performance information consumed by a task or target. + /// + internal class PerformanceCounter + { + protected string scopeName = String.Empty; + protected int calls = 0; + protected TimeSpan elapsedTime = new TimeSpan(0); + protected bool inScope = false; + protected DateTime scopeStartTime; + protected bool reenteredScope = false; + + /// + /// Construct. + /// + /// + internal PerformanceCounter(string scopeName) + { + this.scopeName = scopeName; + } + + /// + /// Name of the scope. + /// + internal string ScopeName + { + get { return scopeName; } + } + + /// + /// Total number of calls so far. + /// + internal int Calls + { + get { return calls; } + } + + /// + /// Total accumalated time so far. + /// + internal TimeSpan ElapsedTime + { + get { return elapsedTime; } + } + + /// + /// Whether or not this scope was reentered. Timing information is not recorded in these cases. + /// + internal bool ReenteredScope + { + get { return reenteredScope; } + } + + /// + /// Whether or not this task or target is executing right now. + /// + internal bool InScope + { + get { return inScope; } + set + { + if (!reenteredScope) + { + if (InScope && !value) + { + // Edge meaning scope is finishing. + inScope = false; + + elapsedTime += (System.DateTime.Now - scopeStartTime); + } + else if (!InScope && value) + { + // Edge meaning scope is starting. + inScope = true; + + ++calls; + scopeStartTime = System.DateTime.Now; + } + else + { + // Should only happen when a scope is reentrant. + // We don't track these numbers. + reenteredScope = true; + } + } + } + } + + internal virtual void PrintCounterMessage(WriteLinePrettyFromResourceDelegate WriteLinePrettyFromResource, ColorSetter setColor, ColorResetter resetColor) + { + string time; + if (!reenteredScope) + { + // round: submillisecond values are not meaningful + time = String.Format(CultureInfo.CurrentCulture, + "{0,5}", Math.Round(elapsedTime.TotalMilliseconds, 0)); + } + else + { + // no value available; instead display an asterisk + time = " *"; + } + + WriteLinePrettyFromResource + ( + 2, + "PerformanceLine", + time, + String.Format(CultureInfo.CurrentCulture, + "{0,-40}" /* pad to 40 align left */, scopeName), + String.Format(CultureInfo.CurrentCulture, + "{0,3}", calls) + ); + } + + /// + /// Returns an IComparer that will put erformance counters + /// in descending order by elapsed time. + /// + static internal IComparer DescendingByElapsedTimeComparer + { + get { return new DescendingByElapsedTime(); } + } + + /// + /// Private IComparer class for sorting performance counters + /// in descending order by elapsed time. + /// + internal class DescendingByElapsedTime : IComparer + { + /// + /// Compare two PerformanceCounters. + /// + /// + /// + /// + public int Compare(object o1, object o2) + { + PerformanceCounter p1 = (PerformanceCounter)o1; + PerformanceCounter p2 = (PerformanceCounter)o2; + + // don't compare reentrant counters, time is incorrect + // and we want to sort them first + if (!p1.reenteredScope && !p2.reenteredScope) + { + return TimeSpan.Compare(p1.ElapsedTime, p2.ElapsedTime); + } + else if (p1.Equals(p2)) + { + return 0; + } + else if (p1.reenteredScope) + { + // p1 was reentrant; sort first + return -1; + } + else + { + // p2 was reentrant; sort first + return 1; + } + } + } + } + + #region eventHandlers + + public virtual void Shutdown() + { + // do nothing + } + + internal abstract void ResetConsoleLoggerState(); + + public virtual void Initialize(IEventSource eventSource, int nodeCount) + { + numberOfProcessors = nodeCount; + Initialize(eventSource); + } + + /// + /// Signs up the console logger for all build events. + /// + /// Available events. + public virtual void Initialize(IEventSource eventSource) + { + ParseParameters(); + + // Always show perf summary for diagnostic verbosity. + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)) + { + this.showPerfSummary = true; + } + + showTargetOutputs = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING")); + + // If not specifically instructed otherwise, show a summary in normal + // and higher verbosities. + if (_showSummary == null && IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + _showSummary = true; + } + + if (showOnlyWarnings || showOnlyErrors) + { + _showSummary = false; + this.showPerfSummary = false; + } + + // Put this after reading the parameters, since it may want to initialize something + // specially based on some parameter value. For example, choose whether to have a summary, based + // on the verbosity. + ResetConsoleLoggerState(); + + // Event source is allowed to be null; this allows the logger to be wrapped by a class that wishes + // to call its event handlers directly. The VS HostLogger does this. + if (eventSource != null) + { + eventSource.BuildStarted += + new BuildStartedEventHandler(BuildStartedHandler); + eventSource.BuildFinished += + new BuildFinishedEventHandler(BuildFinishedHandler); + eventSource.ProjectStarted += + new ProjectStartedEventHandler(ProjectStartedHandler); + eventSource.ProjectFinished += + new ProjectFinishedEventHandler(ProjectFinishedHandler); + eventSource.TargetStarted += + new TargetStartedEventHandler(TargetStartedHandler); + eventSource.TargetFinished += + new TargetFinishedEventHandler(TargetFinishedHandler); + eventSource.TaskStarted += + new TaskStartedEventHandler(TaskStartedHandler); + eventSource.TaskFinished += + new TaskFinishedEventHandler(TaskFinishedHandler); + + eventSource.ErrorRaised += + new BuildErrorEventHandler(ErrorHandler); + eventSource.WarningRaised += + new BuildWarningEventHandler(WarningHandler); + eventSource.MessageRaised += + new BuildMessageEventHandler(MessageHandler); + + eventSource.CustomEventRaised += + new CustomBuildEventHandler(CustomEventHandler); + } + } + + /// + /// Apply a logger parameter. + /// parameterValue may be null, if there is no parameter value. + /// + internal virtual bool ApplyParameter(string parameterName, string parameterValue) + { + ErrorUtilities.VerifyThrowArgumentNull(parameterName, "parameterName"); + + switch (parameterName.ToUpperInvariant()) + { + case "PERFORMANCESUMMARY": + showPerfSummary = true; + return true; + case "NOSUMMARY": + _showSummary = false; + return true; + case "SUMMARY": + _showSummary = true; + return true; + case "NOITEMANDPROPERTYLIST": + showItemAndPropertyList = false; + return true; + case "WARNINGSONLY": + showOnlyWarnings = true; + return true; + case "ERRORSONLY": + showOnlyErrors = true; + return true; + case "SHOWENVIRONMENT": + showEnvironment = true; + return true; + case "SHOWPROJECTFILE": + if (parameterValue == null) + { + showProjectFile = true; + } + else + { + if (parameterValue.Length == 0) + { + showProjectFile = true; + } + else + { + switch (parameterValue.ToUpperInvariant()) + { + case "TRUE": + showProjectFile = true; + break; + + default: + showProjectFile = false; + break; + } + } + } + + return true; + case "V": + case "VERBOSITY": + { + return ApplyVerbosityParameter(parameterValue); + } + } + + return false; + } + + /// + /// Apply the verbosity value + /// + private bool ApplyVerbosityParameter(string parameterValue) + { + switch (parameterValue.ToUpperInvariant()) + { + case "Q": + case "QUIET": + verbosity = LoggerVerbosity.Quiet; + return true; + case "M": + case "MINIMAL": + verbosity = LoggerVerbosity.Minimal; + return true; + case "N": + case "NORMAL": + verbosity = LoggerVerbosity.Normal; + return true; + case "D": + case "DETAILED": + verbosity = LoggerVerbosity.Detailed; + return true; + case "DIAG": + case "DIAGNOSTIC": + verbosity = LoggerVerbosity.Diagnostic; + return true; + default: + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "InvalidVerbosity", parameterValue); + throw new LoggerException(message, null, errorCode, helpKeyword); + } + } + + public abstract void BuildStartedHandler(object sender, BuildStartedEventArgs e); + + public abstract void BuildFinishedHandler(object sender, BuildFinishedEventArgs e); + + public abstract void ProjectStartedHandler(object sender, ProjectStartedEventArgs e); + + public abstract void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e); + + public abstract void TargetStartedHandler(object sender, TargetStartedEventArgs e); + + public abstract void TargetFinishedHandler(object sender, TargetFinishedEventArgs e); + + public abstract void TaskStartedHandler(object sender, TaskStartedEventArgs e); + + public abstract void TaskFinishedHandler(object sender, TaskFinishedEventArgs e); + + public abstract void ErrorHandler(object sender, BuildErrorEventArgs e); + + public abstract void WarningHandler(object sender, BuildWarningEventArgs e); + + public abstract void MessageHandler(object sender, BuildMessageEventArgs e); + + public abstract void CustomEventHandler(object sender, CustomBuildEventArgs e); + + #endregion + + #region Internal member data + + /// + /// Controls the amount of text displayed by the logger + /// + internal LoggerVerbosity verbosity = LoggerVerbosity.Normal; + + /// + /// Time the build started + /// + internal DateTime buildStarted; + + /// + /// Delegate used to write text + /// + internal WriteHandler write = null; + + /// + /// Delegate used to change text color. + /// + internal ColorSetter setColor = null; + + /// + /// Delegate used to reset text color + /// + internal ColorResetter resetColor = null; + + /// + /// Indicates if project header should not be displayed. + /// + internal bool skipProjectStartedText = false; + + /// + /// Number of spaces that each level of indentation is worth + /// + internal const int tabWidth = 2; + + /// + /// Keeps track of the current indentation level. + /// + internal int currentIndentLevel = 0; + + /// + /// The kinds of newline breaks we expect. + /// + /// Currently we're not supporting "\r". + internal static readonly string[] newLines = { "\r\n", "\n" }; + + /// + /// Visual separator for projects. Line length was picked arbitrarily. + /// + internal const string projectSeparatorLine = + "__________________________________________________"; + + /// + /// Console logger parameters. + /// + internal string loggerParameters = null; + + /// + /// Console logger parameters delimiters. + /// + internal static readonly char[] parameterDelimiters = { ';' }; + + /// + /// Console logger parameter value split character. + /// + private static readonly char[] s_parameterValueSplitCharacter = { '=' }; + + /// + /// Console logger should show error and warning summary at the end of build? + /// If null, user has made no indication. + /// + private bool? _showSummary; + + /// + /// When true, accumulate performance numbers. + /// + internal bool showPerfSummary = false; + + /// + /// When true, show the list of item and property values at the start of each project + /// + internal bool showItemAndPropertyList = true; + + /// + /// Should the target output items be displayed + /// + internal bool showTargetOutputs = false; + + /// + /// When true, suppresses all messages except for warnings. (And possibly errors, if showOnlyErrors is true.) + /// + protected bool showOnlyWarnings; + + /// + /// When true, suppresses all messages except for errors. (And possibly warnings, if showOnlyWarnings is true.) + /// + protected bool showOnlyErrors; + + /// + /// When true the environment block supplied by the build started event should be printed out at the start of the build + /// + protected bool showEnvironment; + + /// + /// When true, indicates that the logger should tack the project file onto the end of errors and warnings. + /// + protected bool showProjectFile = false; + + internal bool ignoreLoggerErrors = true; + + internal bool runningWithCharacterFileType = false; + + + #region Per-build Members + internal int numberOfProcessors = 1; + /// + /// Number of errors enountered in this build + /// + internal int errorCount = 0; + + /// + /// Number of warnings enountered in this build + /// + internal int warningCount = 0; + + /// + /// A list of the errors that have occured during this build. + /// + internal ArrayList errorList; + + /// + /// A list of the warnings that have occured during this build. + /// + internal ArrayList warningList; + + /// + /// Accumulated project performance information. + /// + internal Hashtable projectPerformanceCounters; + + /// + /// Accumulated target performance information. + /// + internal Hashtable targetPerformanceCounters; + + /// + /// Accumulated task performance information. + /// + internal Hashtable taskPerformanceCounters; + + #endregion + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/ConsoleLogger.cs b/src/XMakeBuildEngine/Logging/ConsoleLogger.cs new file mode 100644 index 00000000000..adcce71b9e7 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/ConsoleLogger.cs @@ -0,0 +1,542 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using BaseConsoleLogger = Microsoft.Build.BackEnd.Logging.BaseConsoleLogger; +using SerialConsoleLogger = Microsoft.Build.BackEnd.Logging.SerialConsoleLogger; +using ParallelConsoleLogger = Microsoft.Build.BackEnd.Logging.ParallelConsoleLogger; + +namespace Microsoft.Build.Logging +{ + #region Delegates + + /// + /// Delegate to use for writing a string to some location like + /// the console window or the IDE build window. + /// + /// + public delegate void WriteHandler(string message); + + /// + /// Type of delegate used to set console color. + /// + /// Text color + public delegate void ColorSetter(ConsoleColor color); + + /// + /// Type of delegate used to reset console color. + /// + public delegate void ColorResetter(); + + #endregion + + /// + /// This class implements the default logger that outputs event data + /// to the console (stdout). + /// It is a facade: it creates, wraps and delegates to a kind of BaseConsoleLogger, + /// either SerialConsoleLogger or ParallelConsoleLogger. + /// + /// This class is not thread safe. + public class ConsoleLogger : INodeLogger + { + private BaseConsoleLogger _consoleLogger; + private int _numberOfProcessors = 1; + private LoggerVerbosity _verbosity; + private WriteHandler _write; + private ColorSetter _colorSet; + private ColorResetter _colorReset; + private string _parameters; + private bool _skipProjectStartedText = false; + private bool? _showSummary; + + + #region Constructors + + /// + /// Default constructor. + /// + public ConsoleLogger() + : this(LoggerVerbosity.Normal) + { + // do nothing + } + + /// + /// Create a logger instance with a specific verbosity. This logs to + /// the default console. + /// + /// Verbosity level. + public ConsoleLogger(LoggerVerbosity verbosity) + : + this + ( + verbosity, + new WriteHandler(Console.Out.Write), + new ColorSetter(SetColor), + new ColorResetter(ResetColor) + ) + { + // do nothing + } + + /// + /// Initializes the logger, with alternate output handlers. + /// + /// + /// + /// + /// + public ConsoleLogger + ( + LoggerVerbosity verbosity, + WriteHandler write, + ColorSetter colorSet, + ColorResetter colorReset + ) + { + ErrorUtilities.VerifyThrowArgumentNull(write, "write"); + _verbosity = verbosity; + _write = write; + _colorSet = colorSet; + _colorReset = colorReset; + } + + /// + /// This is called by every event handler for compat reasons -- see DDB #136924 + /// However it will skip after the first call + /// + private void InitializeBaseConsoleLogger() + { + if (_consoleLogger == null) + { + bool useMPLogger = false; + bool disableConsoleColor = false; + if (!string.IsNullOrEmpty(_parameters)) + { + string[] parameterComponents = _parameters.Split(BaseConsoleLogger.parameterDelimiters); + for (int param = 0; param < parameterComponents.Length; param++) + { + if (parameterComponents[param].Length > 0) + { + if (0 == String.Compare(parameterComponents[param], "ENABLEMPLOGGING", StringComparison.OrdinalIgnoreCase)) + { + useMPLogger = true; + } + if (0 == String.Compare(parameterComponents[param], "DISABLEMPLOGGING", StringComparison.OrdinalIgnoreCase)) + { + useMPLogger = false; + } + if (0 == String.Compare(parameterComponents[param], "DISABLECONSOLECOLOR", StringComparison.OrdinalIgnoreCase)) + { + disableConsoleColor = true; + } + } + } + } + + if (disableConsoleColor) + { + _colorSet = new ColorSetter(BaseConsoleLogger.DontSetColor); + _colorReset = new ColorResetter(BaseConsoleLogger.DontResetColor); + } + + if (_numberOfProcessors == 1 && !useMPLogger) + { + _consoleLogger = new SerialConsoleLogger(_verbosity, _write, _colorSet, _colorReset); + } + else + { + _consoleLogger = new ParallelConsoleLogger(_verbosity, _write, _colorSet, _colorReset); + } + + if (!string.IsNullOrEmpty(_parameters)) + { + _consoleLogger.Parameters = _parameters; + _parameters = null; + } + + if (_showSummary != null) + { + _consoleLogger.ShowSummary = (bool)_showSummary; + } + + _consoleLogger.SkipProjectStartedText = _skipProjectStartedText; + } + } + + #endregion + + #region Properties + + /// + /// Gets or sets the level of detail to show in the event log. + /// + /// Verbosity level. + public LoggerVerbosity Verbosity + { + get + { + return _consoleLogger == null ? _verbosity : _consoleLogger.Verbosity; + } + + set + { + if (_consoleLogger == null) + { + _verbosity = value; + } + else + { + _consoleLogger.Verbosity = value; + } + } + } + + /// + /// A semi-colon delimited list of "key[=value]" parameter pairs. + /// + /// null + public string Parameters + { + get + { + return _consoleLogger == null ? _parameters : _consoleLogger.Parameters; + } + + set + { + if (_consoleLogger == null) + { + _parameters = value; + } + else + { + _consoleLogger.Parameters = value; + } + } + } + + /// + /// Suppresses the display of project headers. Project headers are + /// displayed by default unless this property is set. + /// + /// This is only needed by the IDE logger. + public bool SkipProjectStartedText + { + get + { + return _consoleLogger == null ? _skipProjectStartedText : _consoleLogger.SkipProjectStartedText; + } + + set + { + if (_consoleLogger == null) + { + _skipProjectStartedText = value; + } + else + { + _consoleLogger.SkipProjectStartedText = value; + } + } + } + + /// + /// Suppresses the display of error and warnings summary. + /// + public bool ShowSummary + { + get + { + return (_consoleLogger == null ? _showSummary : _consoleLogger.ShowSummary) ?? false; + } + + set + { + if (_consoleLogger == null) + { + _showSummary = value; + } + else + { + _consoleLogger.ShowSummary = value; + } + } + } + + /// + /// Provide access to the write hander delegate so that it can be redirected + /// if necessary (e.g. to a file) + /// + protected WriteHandler WriteHandler + { + get + { + return _consoleLogger == null ? _write : _consoleLogger.write; + } + + set + { + if (_consoleLogger == null) + { + _write = value; + } + else + { + _consoleLogger.write = value; + } + } + } + + #endregion + + #region Methods + + /// + /// Apply a parameter. + /// NOTE: This method was public by accident in Whidbey, so it cannot be made internal now. It has + /// no good reason for being public. + /// + public void ApplyParameter(string parameterName, string parameterValue) + { + ErrorUtilities.VerifyThrowInvalidOperation(_consoleLogger != null, "MustCallInitializeBeforeApplyParameter"); + _consoleLogger.ApplyParameter(parameterName, parameterValue); + } + + /// + /// Signs up the console logger for all build events. + /// + /// Available events. + public virtual void Initialize(IEventSource eventSource) + { + InitializeBaseConsoleLogger(); + _consoleLogger.Initialize(eventSource); + } + + public virtual void Initialize(IEventSource eventSource, int nodeCount) + { + _numberOfProcessors = nodeCount; + InitializeBaseConsoleLogger(); + _consoleLogger.Initialize(eventSource, nodeCount); + } + + /// + /// The console logger does not need to release any resources. + /// This method does nothing. + /// + public virtual void Shutdown() + { + if (_consoleLogger != null) + { + _consoleLogger.Shutdown(); + } + } + + /// + /// Handler for build started events + /// + /// sender (should be null) + /// event arguments + public void BuildStartedHandler(object sender, BuildStartedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.BuildStartedHandler(sender, e); + } + + /// + /// Handler for build finished events + /// + /// sender (should be null) + /// event arguments + public void BuildFinishedHandler(object sender, BuildFinishedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.BuildFinishedHandler(sender, e); + } + + /// + /// Handler for project started events + /// + /// sender (should be null) + /// event arguments + public void ProjectStartedHandler(object sender, ProjectStartedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.ProjectStartedHandler(sender, e); + } + + /// + /// Handler for project finished events + /// + /// sender (should be null) + /// event arguments + public void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.ProjectFinishedHandler(sender, e); + } + + /// + /// Handler for target started events + /// + /// sender (should be null) + /// event arguments + public void TargetStartedHandler(object sender, TargetStartedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.TargetStartedHandler(sender, e); + } + + /// + /// Handler for target finished events + /// + /// sender (should be null) + /// event arguments + public void TargetFinishedHandler(object sender, TargetFinishedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.TargetFinishedHandler(sender, e); + } + + /// + /// Handler for task started events + /// + /// sender (should be null) + /// event arguments + public void TaskStartedHandler(object sender, TaskStartedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.TaskStartedHandler(sender, e); + } + + /// + /// Handler for task finished events + /// + /// sender (should be null) + /// event arguments + public void TaskFinishedHandler(object sender, TaskFinishedEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.TaskFinishedHandler(sender, e); + } + + /// + /// Prints an error event + /// + public void ErrorHandler(object sender, BuildErrorEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.ErrorHandler(sender, e); + } + + /// + /// Prints a warning event + /// + public void WarningHandler(object sender, BuildWarningEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.WarningHandler(sender, e); + } + + /// + /// Prints a message event + /// + public void MessageHandler(object sender, BuildMessageEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.MessageHandler(sender, e); + } + + /// + /// Prints a custom event + /// + public void CustomEventHandler(object sender, CustomBuildEventArgs e) + { + InitializeBaseConsoleLogger(); // for compat: see DDB#136924 + + _consoleLogger.CustomEventHandler(sender, e); + } + + /// + /// Sets foreground color to color specified + /// + /// foreground color + internal static void SetColor(ConsoleColor c) + { + try + { + Console.ForegroundColor = + TransformColor(c, Console.BackgroundColor); + } + catch (IOException) + { + // The color could not be set, no reason to crash + } + } + + /// + /// Resets the color + /// + internal static void ResetColor() + { + try + { + Console.ResetColor(); + } + catch (IOException) + { + // The color could not be reset, no reason to crash + } + } + + + /// + /// Changes the foreground color to black if the foreground is the + /// same as the background. Changes the foreground to white if the + /// background is black. + /// + /// foreground color for black + /// current background + private static ConsoleColor TransformColor(ConsoleColor foreground, + ConsoleColor background) + { + ConsoleColor result = foreground; //typically do nothing ... + + if (foreground == background) + { + if (background != ConsoleColor.Black) + { + result = ConsoleColor.Black; + } + else + { + result = ConsoleColor.Gray; + } + } + + return result; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs b/src/XMakeBuildEngine/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs new file mode 100644 index 00000000000..5b08f251411 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/DistributedLoggers/ConfigurableForwardingLogger.cs @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Text; +using System.Collections.Generic; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Logging +{ + /// + /// Logger that forwards events to a central logger (e.g ConsoleLogger) + /// residing on the parent node. + /// + public class ConfigurableForwardingLogger : IForwardingLogger + { + #region Constructors + /// + /// Default constructor. + /// + public ConfigurableForwardingLogger() + { + InitializeForwardingTable(); + } + #endregion + + #region Properties + + /// + /// Gets or sets the level of detail to show in the event log. + /// + /// Verbosity level. + public LoggerVerbosity Verbosity + { + get { return _verbosity; } + set { _verbosity = value; } + } + + /// + /// The console logger takes a single parameter to suppress the output of the errors + /// and warnings summary at the end of a build. + /// + /// null + public string Parameters + { + get { return _loggerParameters; } + set { _loggerParameters = value; } + } + + /// + /// This property is set by the build engine to allow a node loggers to forward messages to the + /// central logger + /// + public IEventRedirector BuildEventRedirector + { + get { return _buildEventRedirector; } + set { _buildEventRedirector = value; } + } + + public int NodeId + { + get { return _nodeId; } + set { _nodeId = value; } + } + #endregion + + #region Methods + + /// + /// Initialize the Forwarding Table with the default values + /// + private void InitializeForwardingTable() + { + _forwardingTable = new Dictionary(15, StringComparer.OrdinalIgnoreCase); + _forwardingTable[BuildStartedEventDescription] = 0; + _forwardingTable[BuildFinishedEventDescription] = 0; + _forwardingTable[ProjectStartedEventDescription] = 0; + _forwardingTable[ProjectFinishedEventDescription] = 0; + _forwardingTable[TargetStartedEventDescription] = 0; + _forwardingTable[TargetFinishedEventDescription] = 0; + _forwardingTable[TaskStartedEventDescription] = 0; + _forwardingTable[TaskFinishedEventDescription] = 0; + _forwardingTable[ErrorEventDescription] = 0; + _forwardingTable[WarningEventDescription] = 0; + _forwardingTable[HighMessageEventDescription] = 0; + _forwardingTable[NormalMessageEventDescription] = 0; + _forwardingTable[LowMessageEventDescription] = 0; + _forwardingTable[CustomEventDescription] = 0; + _forwardingTable[CommandLineDescription] = 0; + _forwardingSetFromParameters = false; + } + + /// + /// Parses out the logger parameters from the Parameters string. + /// + private void ParseParameters() + { + if (_loggerParameters != null) + { + string[] parameterComponents; + + parameterComponents = _loggerParameters.Split(s_parameterDelimiters); + for (int param = 0; param < parameterComponents.Length; param++) + { + if (parameterComponents[param].Length > 0) + { + ApplyParameter(parameterComponents[param]); + } + } + // Setting events to forward on the commandline will override the verbosity and other switches such as + // showPerfSummand and ShowSummary + if (_forwardingSetFromParameters) + { + _showPerfSummary = false; + _showSummary = true; + } + } + } + + /// + /// Logger parameters can be used to enable and disable specific event types. + /// Otherwise, the verbosity is used to choose which events to forward. + /// + private void ApplyParameter(string parameterName) + { + ErrorUtilities.VerifyThrowArgumentNull(parameterName, "parameterName"); + + if (_forwardingTable.ContainsKey(parameterName)) + { + _forwardingSetFromParameters = true; + _forwardingTable[parameterName] = 1; + } + + // If any of the following parameters are set, we will make sure we forward the events + // necessary for the central logger to emit the requested information + if (0 == String.Compare(parameterName, PerformanceSummaryDescription, StringComparison.OrdinalIgnoreCase)) + { + _showPerfSummary = true; + } + else if (0 == String.Compare(parameterName, NoSummaryDescription, StringComparison.OrdinalIgnoreCase)) + { + _showSummary = false; + } + else if (0 == String.Compare(parameterName, ShowCommandLineDescription, StringComparison.OrdinalIgnoreCase)) + { + _showCommandLine = true; + } + } + + /// + /// Signs up the console logger for all build events. + /// + public virtual void Initialize(IEventSource eventSource) + { + ErrorUtilities.VerifyThrowArgumentNull(eventSource, "eventSource"); + + ParseParameters(); + + ResetLoggerState(); + + if (!_forwardingSetFromParameters) + { + SetForwardingBasedOnVerbosity(); + } + + eventSource.BuildStarted += new BuildStartedEventHandler(BuildStartedHandler); + eventSource.BuildFinished += new BuildFinishedEventHandler(BuildFinishedHandler); + eventSource.ProjectStarted += new ProjectStartedEventHandler(ProjectStartedHandler); + eventSource.ProjectFinished += new ProjectFinishedEventHandler(ProjectFinishedHandler); + eventSource.TargetStarted += new TargetStartedEventHandler(TargetStartedHandler); + eventSource.TargetFinished += new TargetFinishedEventHandler(TargetFinishedHandler); + eventSource.TaskStarted += new TaskStartedEventHandler(TaskStartedHandler); + eventSource.TaskFinished += new TaskFinishedEventHandler(TaskFinishedHandler); + eventSource.ErrorRaised += new BuildErrorEventHandler(ErrorHandler); + eventSource.WarningRaised += new BuildWarningEventHandler(WarningHandler); + eventSource.MessageRaised += new BuildMessageEventHandler(MessageHandler); + eventSource.CustomEventRaised += new CustomBuildEventHandler(CustomEventHandler); + } + + /// + /// Signs up the console logger for all build events. + /// + public void Initialize(IEventSource eventSource, int nodeCount) + { + Initialize(eventSource); + } + + private void SetForwardingBasedOnVerbosity() + { + _forwardingTable[BuildStartedEventDescription] = 1; + _forwardingTable[BuildFinishedEventDescription] = 1; + + if (IsVerbosityAtLeast(LoggerVerbosity.Quiet)) + { + _forwardingTable[ErrorEventDescription] = 1; + _forwardingTable[WarningEventDescription] = 1; + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Minimal)) + { + _forwardingTable[HighMessageEventDescription] = 1; + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + _forwardingTable[NormalMessageEventDescription] = 1; + _forwardingTable[ProjectStartedEventDescription] = 1; + _forwardingTable[ProjectFinishedEventDescription] = 1; + _forwardingTable[TargetStartedEventDescription] = 1; + _forwardingTable[TargetFinishedEventDescription] = 1; + _forwardingTable[CommandLineDescription] = 1; + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + _forwardingTable[TargetStartedEventDescription] = 1; + _forwardingTable[TargetFinishedEventDescription] = 1; + _forwardingTable[TaskStartedEventDescription] = 1; + _forwardingTable[TaskFinishedEventDescription] = 1; + _forwardingTable[LowMessageEventDescription] = 1; + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)) + { + _forwardingTable[CustomEventDescription] = 1; + } + + if (_showSummary) + { + _forwardingTable[ErrorEventDescription] = 1; + _forwardingTable[WarningEventDescription] = 1; + } + + if (_showPerfSummary) + { + _forwardingTable[TargetStartedEventDescription] = 1; + _forwardingTable[TargetFinishedEventDescription] = 1; + _forwardingTable[TaskStartedEventDescription] = 1; + _forwardingTable[TaskFinishedEventDescription] = 1; + _forwardingTable[TargetStartedEventDescription] = 1; + _forwardingTable[TargetFinishedEventDescription] = 1; + _forwardingTable[ProjectStartedEventDescription] = 1; + _forwardingTable[ProjectFinishedEventDescription] = 1; + } + + if (_showCommandLine) + { + _forwardingTable[CommandLineDescription] = 1; + } + } + + + /// + /// Reset the states of per-build member variables. + /// Used when a build is finished, but the logger might be needed for the next build. + /// + private void ResetLoggerState() + { + // No state needs resetting + } + + /// + /// Called when Engine is done with this logger + /// + public virtual void Shutdown() + { + // Nothing to do + } + + /// + /// Handler for build started events + /// + /// sender (should be null) + /// event arguments + private void BuildStartedHandler(object sender, BuildStartedEventArgs e) + { + // This is false by default + if (_forwardingTable[BuildStartedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Handler for build finished events + /// + /// sender (should be null) + /// event arguments + private void BuildFinishedHandler(object sender, BuildFinishedEventArgs e) + { + // This is false by default + if (_forwardingTable[BuildFinishedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + ResetLoggerState(); + } + + /// + /// Handler for project started events + /// + /// sender (should be null) + /// event arguments + private void ProjectStartedHandler(object sender, ProjectStartedEventArgs e) + { + if (_forwardingTable[ProjectStartedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Handler for project finished events + /// + /// sender (should be null) + /// event arguments + private void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e) + { + if (_forwardingTable[ProjectFinishedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Handler for target started events + /// + /// sender (should be null) + /// event arguments + private void TargetStartedHandler(object sender, TargetStartedEventArgs e) + { + if (_forwardingTable[TargetStartedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Handler for target finished events + /// + /// sender (should be null) + /// event arguments + private void TargetFinishedHandler(object sender, TargetFinishedEventArgs e) + { + if (_forwardingTable[TargetFinishedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Handler for task started events + /// + /// sender (should be null) + /// event arguments + private void TaskStartedHandler(object sender, TaskStartedEventArgs e) + { + if (_forwardingTable[TaskStartedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Handler for task finished events + /// + /// sender (should be null) + /// event arguments + private void TaskFinishedHandler(object sender, TaskFinishedEventArgs e) + { + if (_forwardingTable[TaskFinishedEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Prints an error event + /// + private void ErrorHandler(object sender, BuildErrorEventArgs e) + { + if (_forwardingTable[ErrorEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Prints a warning event + /// + private void WarningHandler(object sender, BuildWarningEventArgs e) + { + if (_forwardingTable[WarningEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Prints a message event + /// + private void MessageHandler(object sender, BuildMessageEventArgs e) + { + bool forwardEvent = false; + + if (_forwardingTable[LowMessageEventDescription] == 1 && e.Importance == MessageImportance.Low) + { + forwardEvent = true; + } + else if (_forwardingTable[NormalMessageEventDescription] == 1 && e.Importance == MessageImportance.Normal) + { + forwardEvent = true; + } + else if (_forwardingTable[HighMessageEventDescription] == 1 && e.Importance == MessageImportance.High) + { + forwardEvent = true; + } + else if (_forwardingTable[CommandLineDescription] == 1 && e is TaskCommandLineEventArgs) + { + forwardEvent = true; + } + + if (forwardEvent) + { + ForwardToCentralLogger(e); + } + } + + /// + /// Prints a custom event + /// + private void CustomEventHandler(object sender, CustomBuildEventArgs e) + { + if (_forwardingTable[CustomEventDescription] == 1) + { + ForwardToCentralLogger(e); + } + } + + protected virtual void ForwardToCentralLogger(BuildEventArgs e) + { + _buildEventRedirector.ForwardEvent(e); + } + + /// + /// Determines whether the current verbosity setting is at least the value + /// passed in. + /// + private bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity) + { + return (_verbosity >= checkVerbosity); + } + #endregion + + #region Private member data + + /// + /// Controls the amount of text displayed by the logger + /// + private LoggerVerbosity _verbosity = LoggerVerbosity.Normal; + + /// + /// Console logger parameters. + /// + private string _loggerParameters = null; + + /// + /// Console logger parameters delimiters. + /// + private static readonly char[] s_parameterDelimiters = { ';' }; + + /// + /// Strings that users of this logger can pass in to enable specific events or logger output. + /// Also used as keys into our dictionary. + /// + private const string BuildStartedEventDescription = "BUILDSTARTEDEVENT"; + private const string BuildFinishedEventDescription = "BUILDFINISHEDEVENT"; + private const string ProjectStartedEventDescription = "PROJECTSTARTEDEVENT"; + private const string ProjectFinishedEventDescription = "PROJECTFINISHEDEVENT"; + private const string TargetStartedEventDescription = "TARGETSTARTEDEVENT"; + private const string TargetFinishedEventDescription = "TARGETFINISHEDEVENT"; + private const string TaskStartedEventDescription = "TASKSTARTEDEVENT"; + private const string TaskFinishedEventDescription = "TASKFINISHEDEVENT"; + private const string ErrorEventDescription = "ERROREVENT"; + private const string WarningEventDescription = "WARNINGEVENT"; + private const string HighMessageEventDescription = "HIGHMESSAGEEVENT"; + private const string NormalMessageEventDescription = "NORMALMESSAGEEVENT"; + private const string LowMessageEventDescription = "LOWMESSAGEEVENT"; + private const string CustomEventDescription = "CUSTOMEVENT"; + private const string CommandLineDescription = "COMMANDLINE"; + private const string PerformanceSummaryDescription = "PERFORMANCESUMMARY"; + private const string NoSummaryDescription = "NOSUMMARY"; + private const string ShowCommandLineDescription = "SHOWCOMMANDLINE"; + + #region Per-build Members + + /// + /// A table indicating if a particular event type should be forwarded + /// The value is type int rather than bool to avoid the problem of JITting generics. + /// Dictionary is already compiled into mscorlib. + /// + private Dictionary _forwardingTable; + + /// + /// A pointer to the central logger + /// + private IEventRedirector _buildEventRedirector; + + /// + /// Indicates if the events to forward are being set by the parameters sent to the logger + /// if this is false the events to forward are based on verbosity else verbosity settings will be ignored + /// + private bool _forwardingSetFromParameters; + + /// + /// Console logger should show error and warning summary at the end of build? + /// + private bool _showSummary = true; + + /// + /// When true, accumulate performance numbers. + /// + private bool _showPerfSummary = false; + + /// + /// When true the commandline message is sent + /// + private bool _showCommandLine = false; + + /// + /// Id of the node the logger is attached to + /// + private int _nodeId; + + #endregion + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/DistributedLoggers/DistributedFileLogger.cs b/src/XMakeBuildEngine/Logging/DistributedLoggers/DistributedFileLogger.cs new file mode 100644 index 00000000000..760cad44967 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/DistributedLoggers/DistributedFileLogger.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Text; +using System.IO; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Logging +{ + /// + /// This class will create a text file which will contain the build log for that node + /// + public class DistributedFileLogger : IForwardingLogger + { + #region Constructors + /// + /// Default constructor. + /// + public DistributedFileLogger() + : base() + { + } + #endregion + + #region Methods + + public void Initialize(IEventSource eventSource, int nodeCount) + { + Initialize(eventSource); + } + + /// + /// Parses out the logger parameters from the Parameters string. + /// + private void ParseFileLoggerParameters() + { + if (this.Parameters != null) + { + string[] parameterComponents; + + parameterComponents = this.Parameters.Split(s_fileLoggerParameterDelimiters); + for (int param = 0; param < parameterComponents.Length; param++) + { + if (parameterComponents[param].Length > 0) + { + string[] parameterAndValue = parameterComponents[param].Split(s_fileLoggerParameterValueSplitCharacter); + + if (parameterAndValue.Length > 1) + { + ApplyFileLoggerParameter(parameterAndValue[0], parameterAndValue[1]); + } + else + { + ApplyFileLoggerParameter(parameterAndValue[0], null); + } + } + } + } + } + + /// + /// Apply a parameter + /// + private void ApplyFileLoggerParameter(string parameterName, string parameterValue) + { + if (String.Compare("LOGFILE", parameterName, StringComparison.OrdinalIgnoreCase) == 0) + { + if (string.IsNullOrEmpty(parameterValue)) + { + string message = ResourceUtilities.FormatResourceString("InvalidFileLoggerFile", string.Empty, ResourceUtilities.FormatResourceString("logfilePathNullOrEmpty")); + throw new LoggerException(message); + } + + // Set log file to the right half of the parameter string and then remove it as it is going to be replaced in Initialize + _logFile = parameterValue; + int indexOfParameter = _parameters.IndexOf(parameterName + s_fileLoggerParameterValueSplitCharacter[0] + parameterValue, 0, StringComparison.OrdinalIgnoreCase); + int length = ((string)(parameterName + s_fileLoggerParameterValueSplitCharacter[0] + parameterValue)).Length; + // Check to see if the next char is a ; if so remove that as well + if ((indexOfParameter + length) < _parameters.Length && _parameters[indexOfParameter + length] == ';') + { + length++; + } + _parameters = _parameters.Remove(indexOfParameter, length); + } + } + + public void Initialize(IEventSource eventSource) + { + ErrorUtilities.VerifyThrowArgumentNull(eventSource, "eventSource"); + ParseFileLoggerParameters(); + string fileName = _logFile; + try + { + // Create a new file logger and pass it some parameters to make the build log very detailed + _nodeFileLogger = new FileLogger(); + string extension = Path.GetExtension(_logFile); + // If there is no extension add a default of .log to it + if (String.IsNullOrEmpty(extension)) + { + _logFile += ".log"; + extension = ".log"; + } + // Log 0-based node id's, where 0 is the parent. This is a little unnatural for the reader, + // but avoids confusion by being consistent with the Engine and any error messages it may produce. + fileName = _logFile.Replace(extension, _nodeId + extension); + _nodeFileLogger.Verbosity = LoggerVerbosity.Detailed; + _nodeFileLogger.Parameters = "ShowEventId;ShowCommandLine;logfile=" + fileName + ";" + _parameters; + } + catch (ArgumentException e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (_nodeFileLogger != null) + { + _nodeFileLogger.Shutdown(); + } + + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "InvalidFileLoggerFile", fileName, e.Message); + throw new LoggerException(message, e, errorCode, helpKeyword); + } + + // Say we are operating on 2 processors so we can get the multiproc output + _nodeFileLogger.Initialize(eventSource, 2); + } + + public void Shutdown() + { + if (_nodeFileLogger != null) + { + _nodeFileLogger.Shutdown(); + } + } + #endregion + + #region Properties + + // Need to access this for testing purposes + internal FileLogger InternalFilelogger + { + get + { + return _nodeFileLogger; + } + } + public IEventRedirector BuildEventRedirector + { + get + { + return _buildEventRedirector; + } + set + { + _buildEventRedirector = value; + } + } + + // Node Id of the node which the forwarding logger is attached to + public int NodeId + { + get + { + return _nodeId; + } + set + { + _nodeId = value; + } + } + + // The verbosity for now is set at detailed + public LoggerVerbosity Verbosity + { + get + { + ErrorUtilities.VerifyThrow(false, "Should not be getting verbosity from distributed file logger"); + return LoggerVerbosity.Detailed; + } + set + { + // Dont really care about verbosity at this point, but dont want to throw exception as it is set for all distributed loggers + } + } + + public string Parameters + { + get + { + return _parameters; + } + set + { + _parameters = value; + } + } + + #endregion + + #region Data + // The file logger which will do the actual logging of the node's build output + private FileLogger _nodeFileLogger; + // Reference for the central logger + private IEventRedirector _buildEventRedirector; + // The Id of the node the forwardingLogger is attached to + private int _nodeId; + // Directory to place the log files, by default this will be in the current directory when the node is created + private string _logFile = "msbuild.log"; + // Logger parameters + private string _parameters; + // File logger parameters delimiters. + private static readonly char[] s_fileLoggerParameterDelimiters = { ';' }; + // File logger parameter value split character. + private static readonly char[] s_fileLoggerParameterValueSplitCharacter = { '=' }; + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Logging/FileLogger.cs b/src/XMakeBuildEngine/Logging/FileLogger.cs new file mode 100644 index 00000000000..8645f260931 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/FileLogger.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Globalization; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Logging +{ + /// + /// A specialization of the ConsoleLogger that logs to a file instead of the console. + /// The output in terms of what is written and how it looks is identical. For example you can + /// log verbosely to a file using the FileLogger while simultaneously logging only high priority events + /// to the console using a ConsoleLogger. + /// + /// + /// It's unfortunate that this is derived from ConsoleLogger, which is itself a facade; it makes things more + /// complex -- for example, there is parameter parsing in this class, plus in BaseConsoleLogger. However we have + /// to derive FileLogger from ConsoleLogger because it shipped that way in Whidbey. + /// + public class FileLogger : ConsoleLogger + { + #region Constructors + + /// + /// Default constructor. + /// + public FileLogger() : base(LoggerVerbosity.Normal) + { + this.WriteHandler = new WriteHandler(Write); + } + + #endregion + + /// + /// Signs up the console file logger for all build events. + /// This is the backward-compatible overload. + /// + /// Available events. + public override void Initialize(IEventSource eventSource) + { + ErrorUtilities.VerifyThrowArgumentNull(eventSource, "eventSource"); + eventSource.BuildFinished += new BuildFinishedEventHandler(FileLoggerBuildFinished); + InitializeFileLogger(eventSource, 1); + } + + private void FileLoggerBuildFinished(object sender, BuildFinishedEventArgs e) + { + if (_fileWriter != null) + { + _fileWriter.Flush(); + } + } + + /// + /// Creates new file for logging + /// + private void InitializeFileLogger(IEventSource eventSource, int nodeCount) + { + // Prepend the default setting of "forcenoalign": no alignment is needed as we're + // writing to a file + string parameters = Parameters; + if (parameters != null) + { + Parameters = "FORCENOALIGN;" + parameters; + } + else + { + Parameters = "FORCENOALIGN;"; + } + + this.ParseFileLoggerParameters(); + + // Finally, ask the base console logger class to initialize. It may + // want to make decisions based on our verbosity, so we do this last. + base.Initialize(eventSource, nodeCount); + + try + { + _fileWriter = new StreamWriter(_logFileName, _append, _encoding); + + _fileWriter.AutoFlush = _autoFlush; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "InvalidFileLoggerFile", _logFileName, e.Message); + if (_fileWriter != null) + { + _fileWriter.Close(); + } + throw new LoggerException(message, e.InnerException, errorCode, helpKeyword); + } + } + + /// + /// Multiproc aware initialization + /// + public override void Initialize(IEventSource eventSource, int nodeCount) + { + InitializeFileLogger(eventSource, nodeCount); + } + + /// + /// The handler for the write delegate of the console logger we are deriving from. + /// + /// The text to write to the log + private void Write(string text) + { + try + { + _fileWriter.Write(text); + } + catch (Exception ex) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(ex)) + throw; + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "InvalidFileLoggerFile", _logFileName, ex.Message); + if (_fileWriter != null) + { + _fileWriter.Close(); + } + throw new LoggerException(message, ex.InnerException, errorCode, helpKeyword); + } + } + + /// + /// Shutdown method implementation of ILogger - we need to flush and close our logfile. + /// + public override void Shutdown() + { + try + { + // Do, or do not, there is no try. + } + finally + { + // Keep FxCop happy by closing in a Finally. + if (_fileWriter != null) + { + _fileWriter.Close(); + } + } + } + + /// + /// Parses out the logger parameters from the Parameters string. + /// + private void ParseFileLoggerParameters() + { + if (this.Parameters != null) + { + string[] parameterComponents; + + parameterComponents = this.Parameters.Split(s_fileLoggerParameterDelimiters); + for (int param = 0; param < parameterComponents.Length; param++) + { + if (parameterComponents[param].Length > 0) + { + string[] parameterAndValue = parameterComponents[param].Split(s_fileLoggerParameterValueSplitCharacter); + + if (parameterAndValue.Length > 1) + { + ApplyFileLoggerParameter(parameterAndValue[0], parameterAndValue[1]); + } + else + { + ApplyFileLoggerParameter(parameterAndValue[0], null); + } + } + } + } + } + + /// + /// Apply a parameter parsed by the file logger. + /// + private void ApplyFileLoggerParameter(string parameterName, string parameterValue) + { + switch (parameterName.ToUpperInvariant()) + { + case "LOGFILE": + _logFileName = parameterValue; + break; + case "APPEND": + _append = true; + break; + case "NOAUTOFLUSH": + _autoFlush = false; + break; + case "ENCODING": + try + { + _encoding = Encoding.GetEncoding(parameterValue); + } + catch (ArgumentException ex) + { + // Can't change strings at this point, so for now we are using the exception string + // verbatim, and supplying a error code directly. + // This should move into the .resx later. + throw new LoggerException(ex.Message, ex.InnerException, "MSB4128", null); + } + break; + default: + // We will not error for unrecognized parameters, since someone may wish to + // extend this class and call this base method before theirs. + break; + } + } + + #region Private member data + + /// + /// logFileName is the name of the log file that we will generate + /// the default value is msbuild.log + /// + private string _logFileName = "msbuild.log"; + + /// + /// fileWriter is the stream that has been opened on our log file. + /// + private StreamWriter _fileWriter = null; + + /// + /// Whether the logger should append to any existing file. + /// Default is to overwrite. + /// + private bool _append = false; + + /// + /// Whether the logger should flush aggressively to disk. + /// Default is true. This preserves the most information in the case + /// of a crash, but may slow the logger down. + /// + private bool _autoFlush = true; + + /// + /// Encoding for the output. Defaults to ANSI. + /// + private Encoding _encoding = Encoding.Default; + + /// + /// File logger parameters delimiters. + /// + private static readonly char[] s_fileLoggerParameterDelimiters = { ';' }; + + /// + /// File logger parameter value split character. + /// + private static readonly char[] s_fileLoggerParameterValueSplitCharacter = { '=' }; + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/LogFormatter.cs b/src/XMakeBuildEngine/Logging/LogFormatter.cs new file mode 100644 index 00000000000..bd3c78c65a8 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/LogFormatter.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Text; +using System.IO; +using Microsoft.Build.Framework; +using System.Globalization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Utility helper functions for formatting logger output. + /// + static internal class LogFormatter + { + /// + /// Formats the timestamp in the log as Hours:Minutes:Seconds.Milliseconds + /// + internal static string FormatLogTimeStamp(DateTime timeStamp) + { + // From http://msdn2.microsoft.com/en-us/library/8kb3ddd4.aspx + // Custom DateTime Format Strings + // + // HH Represents the hour as a number from 00 through 23, that is, + // the hour as represented by a zero-based 24-hour clock that counts the hours since midnight. + // A single-digit hour is formatted with a leading zero. + // + // mm Represents the minute as a number from 00 through 59. The minute represents whole minutes + // passed since the last hour. A single-digit minute is formatted with a leading zero. + // + // ss Represents the seconds as a number from 00 through 59. The second represents whole seconds passed + // since the last minute. A single-digit second is formatted with a leading zero. + // + // fff Represents the three most significant digits of the seconds fraction. Trailing zeros are displayed. + // Since milliseconds are 1 / 1000 of a second we need to display 3 digits. + + // Debug-only because a customer could fire a custom event that has an invalid timestamp + Debug.Assert(timeStamp != DateTime.MinValue, "Timestamp missing"); + + return timeStamp.ToString("HH:mm:ss.fff", CultureInfo.CurrentCulture); + } + + /// + /// Formats a timespan for logger output. + /// + /// + /// String representation of time-span. + internal static string FormatTimeSpan(TimeSpan t) + { + string rawTime = t.ToString(); // Timespan is a value type and can't be null. + int rawTimeLength = rawTime.Length; + int prettyLength = System.Math.Min(11, rawTimeLength); + return t.ToString().Substring(0, prettyLength); + } + } +} diff --git a/src/XMakeBuildEngine/Logging/LoggerDescription.cs b/src/XMakeBuildEngine/Logging/LoggerDescription.cs new file mode 100644 index 00000000000..e2d20770658 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/LoggerDescription.cs @@ -0,0 +1,420 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Text; +using System.IO; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; + +using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException; + +namespace Microsoft.Build.Logging +{ + /// + /// This class is used to contain information about a logger as a collection of values that + /// can be used to instantiate the logger and can be serialized to be passed between different + /// processes. + /// + public class LoggerDescription : INodePacketTranslatable + { + #region Constructor + + internal LoggerDescription() + { + } + + /// + /// Creates a logger description from given data + /// + public LoggerDescription + ( + string loggerClassName, + string loggerAssemblyName, + string loggerAssemblyFile, + string loggerSwitchParameters, + LoggerVerbosity verbosity + ) + { + _loggerClassName = loggerClassName; + + if (loggerAssemblyFile != null && !Path.IsPathRooted(loggerAssemblyFile)) + { + loggerAssemblyFile = FileUtilities.NormalizePath(loggerAssemblyFile); + } + + _loggerAssembly = AssemblyLoadInfo.Create(loggerAssemblyName, loggerAssemblyFile); + _loggerSwitchParameters = loggerSwitchParameters; + _verbosity = verbosity; + } + + #endregion + + #region Properties + + /// + /// This property exposes the logger id which identifies each distributed logger uniquiely + /// + internal int LoggerId + { + get + { + return _loggerId; + } + set + { + _loggerId = value; + } + } + + /// + /// This property generates the logger name by appending together the class name and assembly name + /// + internal string Name + { + get + { + if (!string.IsNullOrEmpty(_loggerClassName) && + !string.IsNullOrEmpty(_loggerAssembly.AssemblyFile)) + { + return _loggerClassName + ":" + _loggerAssembly.AssemblyFile; + } + else if (!string.IsNullOrEmpty(_loggerClassName)) + { + return _loggerClassName; + } + else + { + return _loggerAssembly.AssemblyFile; + } + } + } + + /// + /// Returns the string of logger parameters, null if there are none + /// + public string LoggerSwitchParameters + { + get + { + return _loggerSwitchParameters; + } + } + + /// + /// Return the verbosity for this logger (from command line all loggers get same verbosity) + /// + public LoggerVerbosity Verbosity + { + get + { + return _verbosity; + } + } + + #endregion + + #region Methods + + /// + /// Create an IForwardingLogger out of the data in this description. This method may throw a variety of + /// reflection exceptions if the data is invalid. It is the resposibility of the caller to handle these + /// exceptions if desired. + /// + /// + internal IForwardingLogger CreateForwardingLogger() + { + IForwardingLogger forwardingLogger = null; + try + { + forwardingLogger = (IForwardingLogger)CreateLogger(true); + + // Check if the class was not found in the assembly + if (forwardingLogger == null) + { + InternalLoggerException.Throw(null, null, "LoggerNotFoundError", true, this.Name); + } + } + catch (Exception e /* Wrap all other exceptions in a more meaningful exception*/) + { + // Two of the possible exceptions are already in reasonable exception types + if (e is LoggerException /* Polite logger Failure*/ || e is InternalLoggerException /* LoggerClass not found*/) + { + throw; + } + else + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + InternalLoggerException.Throw(e, null, "LoggerCreationError", true, Name); + } + } + + return forwardingLogger; + } + + /// + /// Create an ILogger out of the data in this description. This method may throw a variety of + /// reflection exceptions if the data is invalid. It is the resposibility of the caller to handle these + /// exceptions if desired. + /// + /// + public ILogger CreateLogger() + { + return CreateLogger(false); + } + + /// + /// Loads a logger from its assembly, instantiates it, and handles errors. + /// + /// Instantiated logger. + private ILogger CreateLogger(bool forwardingLogger) + { + ILogger logger = null; + + try + { + if (forwardingLogger) + { + // load the logger from its assembly + LoadedType loggerClass = (new TypeLoader(s_forwardingLoggerClassFilter)).Load(_loggerClassName, _loggerAssembly); + + if (loggerClass != null) + { + // instantiate the logger + logger = (IForwardingLogger)Activator.CreateInstance(loggerClass.Type); + } + } + else + { + // load the logger from its assembly + LoadedType loggerClass = (new TypeLoader(s_loggerClassFilter)).Load(_loggerClassName, _loggerAssembly); + + if (loggerClass != null) + { + // instantiate the logger + logger = (ILogger)Activator.CreateInstance(loggerClass.Type); + } + } + } + catch (InvalidCastException e) + { + // The logger when trying to load has hit an invalid case, this is usually due to the framework assembly being a different version + string message = ResourceUtilities.FormatResourceString("LoggerInstantiationFailureErrorInvalidCast", _loggerClassName, _loggerAssembly.AssemblyLocation, e.Message); + throw new LoggerException(message, e.InnerException); + } + catch (TargetInvocationException e) + { + // At this point, the interesting stack is the internal exception; + // the outer exception is System.Reflection stuff that says nothing + // about the nature of the logger failure. + Exception innerException = e.InnerException; + + if (innerException is LoggerException) + { + // Logger failed politely during construction. In order to preserve + // the stack trace at which the error occured we wrap the original + // exception instead of throwing. + LoggerException l = ((LoggerException)innerException); + throw new LoggerException(l.Message, innerException, l.ErrorCode, l.HelpKeyword); + } + else + { + throw; + } + } + + return logger; + } + + /// + /// Used for finding loggers when reflecting through assemblies. + /// + private static readonly TypeFilter s_forwardingLoggerClassFilter = new TypeFilter(IsForwardingLoggerClass); + + /// + /// Used for finding loggers when reflecting through assemblies. + /// + private static readonly TypeFilter s_loggerClassFilter = new TypeFilter(IsLoggerClass); + + /// + /// Checks if the given type is a logger class. + /// + /// This method is used as a TypeFilter delegate. + /// true, if specified type is a logger + private static bool IsForwardingLoggerClass(Type type, object unused) + { + return (type.IsClass && + !type.IsAbstract && + (type.GetInterface("IForwardingLogger") != null)); + } + + /// + /// Checks if the given type is a logger class. + /// + /// This method is used as a TypeFilter delegate. + /// true, if specified type is a logger + private static bool IsLoggerClass(Type type, object unused) + { + return (type.IsClass && + !type.IsAbstract && + (type.GetInterface("ILogger") != null)); + } + + /// + /// Converts the path to the logger assembly to a full path + /// + internal void ConvertPathsToFullPaths() + { + if (_loggerAssembly.AssemblyFile != null) + { + _loggerAssembly = + AssemblyLoadInfo.Create(_loggerAssembly.AssemblyName, Path.GetFullPath(_loggerAssembly.AssemblyFile)); + } + } + + #endregion + + #region Data + private string _loggerClassName; + private string _loggerSwitchParameters; + private AssemblyLoadInfo _loggerAssembly; + private LoggerVerbosity _verbosity; + private int _loggerId; + #endregion + + #region CustomSerializationToStream + internal void WriteToStream(BinaryWriter writer) + { + #region LoggerClassName + if (_loggerClassName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_loggerClassName); + } + #endregion + #region LoggerSwitchParameters + if (_loggerSwitchParameters == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_loggerSwitchParameters); + } + #endregion + #region LoggerAssembly + if (_loggerAssembly == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + if (_loggerAssembly.AssemblyFile == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_loggerAssembly.AssemblyFile); + } + + if (_loggerAssembly.AssemblyName == null) + { + writer.Write((byte)0); + } + else + { + writer.Write((byte)1); + writer.Write(_loggerAssembly.AssemblyName); + } + } + #endregion + writer.Write((Int32)_verbosity); + writer.Write((Int32)_loggerId); + } + + internal void CreateFromStream(BinaryReader reader) + { + #region LoggerClassName + if (reader.ReadByte() == 0) + { + _loggerClassName = null; + } + else + { + _loggerClassName = reader.ReadString(); + } + #endregion + #region LoggerSwitchParameters + if (reader.ReadByte() == 0) + { + _loggerSwitchParameters = null; + } + else + { + _loggerSwitchParameters = reader.ReadString(); + } + #endregion + #region LoggerAssembly + if (reader.ReadByte() == 0) + { + _loggerAssembly = null; + } + else + { + string assemblyName = null; + string assemblyFile = null; + + if (reader.ReadByte() != 0) + { + assemblyFile = reader.ReadString(); + } + + if (reader.ReadByte() != 0) + { + assemblyName = reader.ReadString(); + } + + _loggerAssembly = AssemblyLoadInfo.Create(assemblyName, assemblyFile); + } + #endregion + _verbosity = (LoggerVerbosity)reader.ReadInt32(); + _loggerId = reader.ReadInt32(); + } + #endregion + + #region INodePacketTranslatable Members + + void INodePacketTranslatable.Translate(INodePacketTranslator translator) + { + translator.Translate(ref _loggerClassName); + translator.Translate(ref _loggerSwitchParameters); + translator.Translate(ref _loggerAssembly, AssemblyLoadInfo.FactoryForTranslation); + translator.TranslateEnum(ref _verbosity, (int)_verbosity); + translator.Translate(ref _loggerId); + } + + static internal LoggerDescription FactoryForTranslation(INodePacketTranslator translator) + { + LoggerDescription description = new LoggerDescription(); + ((INodePacketTranslatable)description).Translate(translator); + return description; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/NullCentralLogger.cs b/src/XMakeBuildEngine/Logging/NullCentralLogger.cs new file mode 100644 index 00000000000..a23f373fbdc --- /dev/null +++ b/src/XMakeBuildEngine/Logging/NullCentralLogger.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// This class will throw an exception when it recieves any event except for the build started or build finished event + /// this logger is good to use if a distributed logger is attached but does not want to forward any events + /// + internal class NullCentralLogger : INodeLogger + { + #region Data + private string _parameters; + private LoggerVerbosity _verbosity; + #endregion + + #region Properties + public LoggerVerbosity Verbosity + { + get + { + return _verbosity; + } + set + { + _verbosity = value; + } + } + + public string Parameters + { + get + { + return _parameters; + } + set + { + _parameters = value; + } + } + #endregion + + #region Methods + public void Initialize(IEventSource eventSource, int nodeCount) + { + eventSource.AnyEventRaised += new AnyEventHandler(AnyEventRaisedHandler); + } + + public void AnyEventRaisedHandler(object sender, BuildEventArgs e) + { + if (!(e is BuildStartedEventArgs) && !(e is BuildFinishedEventArgs)) + { + ErrorUtilities.VerifyThrowInvalidOperation(false, "Should not recieve any events other than build started or finished"); + } + } + + public void Initialize(IEventSource eventSource) + { + Initialize(eventSource, 1); + } + + public void Shutdown() + { + // do nothing + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/ParallelLogger/ParallelConsoleLogger.cs b/src/XMakeBuildEngine/Logging/ParallelLogger/ParallelConsoleLogger.cs new file mode 100644 index 00000000000..8f24bfa623f --- /dev/null +++ b/src/XMakeBuildEngine/Logging/ParallelLogger/ParallelConsoleLogger.cs @@ -0,0 +1,1709 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Collections.Generic; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using ColorSetter = Microsoft.Build.Logging.ColorSetter; +using ColorResetter = Microsoft.Build.Logging.ColorResetter; +using WriteHandler = Microsoft.Build.Logging.WriteHandler; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// This class implements the default logger that outputs event data + /// to the console (stdout). + /// + /// This class is not thread safe. + internal class ParallelConsoleLogger : BaseConsoleLogger + { + #region Constructors + + /// + /// Default constructor. + /// + public ParallelConsoleLogger() + : this(LoggerVerbosity.Normal) + { + // do nothing + } + + /// + /// Create a logger instance with a specific verbosity. This logs to + /// the default console. + /// + public ParallelConsoleLogger(LoggerVerbosity verbosity) + : + this + ( + verbosity, + new WriteHandler(Console.Out.Write), + new ColorSetter(SetColor), + new ColorResetter(ResetColor) + ) + { + // do nothing + } + + /// + /// Initializes the logger, with alternate output handlers. + /// + public ParallelConsoleLogger + ( + LoggerVerbosity verbosity, + WriteHandler write, + ColorSetter colorSet, + ColorResetter colorReset + ) + { + InitializeConsoleMethods(verbosity, write, colorSet, colorReset); + _deferredMessages = new Dictionary>(s_compareContextNodeId); + _buildEventManager = new BuildEventManager(); + } + + /// + /// Check to see if the console is going to a char output such as a console,printer or com port, or if it going to a file + /// + private void CheckIfOutputSupportsAlignment() + { + _alignMessages = false; + _bufferWidth = -1; + + // If forceNoAlign is set there is no point getting the console width as there will be no aligning of the text + if (!_forceNoAlign) + { + if (runningWithCharacterFileType) + { + // Get the size of the console buffer so messages can be formatted to the console width + try + { + _bufferWidth = Console.BufferWidth; + _alignMessages = true; + } + catch (Exception) + { + // on Win8 machines while in IDE Console.BufferWidth will throw (while it talks to native console it gets "operation aborted" native error) + // this is probably temporary workaround till we understand what is the reason for that exception + _alignMessages = false; + } + } + else + { + _alignMessages = false; + } + } + } + + #endregion + + #region Methods + + /// + /// Allows the logger to take action based on a parameter passed on when initializing the logger + /// + internal override bool ApplyParameter(string parameterName, string parameterValue) + { + if (base.ApplyParameter(parameterName, parameterValue)) + { + return true; + } + + if (0 == String.Compare(parameterName, "SHOWCOMMANDLINE", StringComparison.OrdinalIgnoreCase)) + { + if (String.IsNullOrEmpty(parameterValue)) + { + _showCommandLine = true; + } + else + { + try + { + _showCommandLine = ConversionUtilities.ConvertStringToBool(parameterValue); + } + catch (ArgumentException) + { + // For compatibility, if the string is not a boolean, just ignore it + _showCommandLine = false; + } + } + + return true; + } + else if (0 == String.Compare(parameterName, "SHOWTIMESTAMP", StringComparison.OrdinalIgnoreCase)) + { + _showTimeStamp = true; + return true; + } + else if (0 == String.Compare(parameterName, "SHOWEVENTID", StringComparison.OrdinalIgnoreCase)) + { + _showEventId = true; + return true; + } + else if (0 == String.Compare(parameterName, "FORCENOALIGN", StringComparison.OrdinalIgnoreCase)) + { + _forceNoAlign = true; + _alignMessages = false; + return true; + } + return false; + } + + public override void Initialize(IEventSource eventSource) + { + // If the logger is being used in singleproc do not show EventId after each message unless it is set as part of a console parameter + if (numberOfProcessors == 1) + { + _showEventId = false; + } + + // Parameters are parsed in Initialize + base.Initialize(eventSource); + CheckIfOutputSupportsAlignment(); + } + + /// + /// Keep track of the last event displayed so target names can be displayed at the correct time + /// + private void ShownBuildEventContext(BuildEventContext e) + { + _lastDisplayedBuildEventContext = e; + } + + /// + /// Reset the states of per-build member variables + /// VSW#516376 + /// + internal override void ResetConsoleLoggerState() + { + if (ShowSummary) + { + errorList = new ArrayList(); + warningList = new ArrayList(); + } + else + { + errorList = null; + warningList = null; + } + + errorCount = 0; + warningCount = 0; + projectPerformanceCounters = null; + targetPerformanceCounters = null; + taskPerformanceCounters = null; + _hasBuildStarted = false; + + // Reset the two data structures created when the logger was created + _buildEventManager = new BuildEventManager(); + _deferredMessages = new Dictionary>(s_compareContextNodeId); + _prefixWidth = 0; + _lastDisplayedBuildEventContext = null; + } + + /// + /// Handler for build started events + /// + /// sender (should be null) + /// event arguments + public override void BuildStartedHandler(object sender, BuildStartedEventArgs e) + { + buildStarted = e.Timestamp; + _hasBuildStarted = true; + + if (showOnlyErrors || showOnlyWarnings) return; + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + WriteLinePrettyFromResource("BuildStartedWithTime", e.Timestamp); + } + + WriteEnvironment(e.BuildEnvironment); + } + + /// + /// Handler for build finished events + /// + /// sender (should be null) + /// event arguments + public override void BuildFinishedHandler(object sender, BuildFinishedEventArgs e) + { + if (!showOnlyErrors && !showOnlyWarnings) + { + // If for some reason we have deferred messages at the end of the build they should be displayed + // so that the reason why they are still buffered can be determined + if (_deferredMessages.Count > 0) + { + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + // Print out all of the deferred messages + WriteLinePrettyFromResource("DeferredMessages"); + foreach (List messageList in _deferredMessages.Values) + { + foreach (BuildMessageEventArgs message in messageList) + { + PrintMessage(message, false); + } + } + } + } + + // Show the performance summary iff the verbosity is diagnostic or the user specifically asked for it + // with a logger parameter. + if (this.showPerfSummary) + { + ShowPerfSummary(); + } + + // if verbosity is normal, detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + if (e.Succeeded) + { + setColor(ConsoleColor.Green); + } + + // Write the "Build Finished" event. + WriteNewLine(); + WriteLinePretty(e.Message); + resetColor(); + } + + // The decision whether or not to show a summary at this verbosity + // was made during initalization. We just do what we're told. + if (ShowSummary) + { + // We can't display a nice nested summary unless we're at Normal or above, + // since we need to have gotten TargetStarted events, which aren't forwarded otherwise. + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + ShowNestedErrorWarningSummary(); + + // Emit text like: + // 1 Warning(s) + // 0 Error(s) + // Don't color the line if it's zero. (Per Whidbey behavior.) + if (warningCount > 0) + { + setColor(ConsoleColor.Yellow); + } + WriteLinePrettyFromResource(2, "WarningCount", warningCount); + resetColor(); + + if (errorCount > 0) + { + setColor(ConsoleColor.Red); + } + WriteLinePrettyFromResource(2, "ErrorCount", errorCount); + resetColor(); + } + else + { + ShowFlatErrorWarningSummary(); + } + } + + // if verbosity is normal, detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + // The time elapsed is the difference between when the BuildStartedEventArg + // was created and when the BuildFinishedEventArg was created + string timeElapsed = LogFormatter.FormatTimeSpan(e.Timestamp - buildStarted); + + WriteNewLine(); + WriteLinePrettyFromResource("TimeElapsed", timeElapsed); + } + } + + ResetConsoleLoggerState(); + CheckIfOutputSupportsAlignment(); + } + + /// + /// At the end of the build, repeats the errors and warnings that occurred + /// during the build, and displays the error count and warning count. + /// Does this in a "flat" style, without context. + /// + private void ShowFlatErrorWarningSummary() + { + if (warningList.Count == 0 && errorList.Count == 0) return; + + // If we're showing only warnings and/or errors, don't summarize. + // This is the buildc.err case. There's no point summarizing since we'd just + // repeat the entire log content again. + if (showOnlyErrors || showOnlyWarnings) return; + + // Make some effort to distinguish this summary from the output above, since otherwise + // it's not clear in lower verbosities + WriteNewLine(); + + if (warningList.Count > 0) + { + setColor(ConsoleColor.Yellow); + foreach (BuildWarningEventArgs warning in warningList) + { + WriteMessageAligned(EventArgsFormatting.FormatEventMessage(warning, runningWithCharacterFileType, showProjectFile), true); + } + } + + if (errorList.Count > 0) + { + setColor(ConsoleColor.Red); + foreach (BuildErrorEventArgs error in errorList) + { + WriteMessageAligned(EventArgsFormatting.FormatEventMessage(error, runningWithCharacterFileType, showProjectFile), true); + } + } + + resetColor(); + } + + /// + /// At the end of the build, repeats the errors and warnings that occurred + /// during the build, and displays the error count and warning count. + /// Does this in a "nested" style. + /// + private void ShowNestedErrorWarningSummary() + { + if (warningList.Count == 0 && errorList.Count == 0) return; + + // If we're showing only warnings and/or errors, don't summarize. + // This is the buildc.err case. There's no point summarizing since we'd just + // repeat the entire log content again. + if (showOnlyErrors || showOnlyWarnings) return; + + if (warningCount > 0) + { + setColor(ConsoleColor.Yellow); + ShowErrorWarningSummary(warningList); + } + + if (errorCount > 0) + { + setColor(ConsoleColor.Red); + ShowErrorWarningSummary(errorList); + } + + resetColor(); + } + + private void ShowErrorWarningSummary(ArrayList listToProcess) where T : BuildEventArgs + { + // Group the build warning event args based on the entry point and the target in which the warning occurred + Dictionary> groupByProjectEntryPoint = new Dictionary>(); + + // Loop through each of the warnings and put them into the correct buckets + for (int listCount = 0; listCount < listToProcess.Count; listCount++) + { + T errorWarningEventArgs = (T)listToProcess[listCount]; + + // Target event may be null for a couple of reasons: + // 1) If the event was from a project load, or engine + // 2) If the flushing of the event queue for each request and result is turned off + // as this could cause errors and warnings to be seen by the logger after the target finished event + // which would cause the error or warning to have no matching target started event as they are removed + // when a target finished event is logged. + // 3) On NORMAL verbosity if the error or warning occurres in a project load then the error or warning and the target started event will be forwarded to + // different forwarding loggers which cannot communicate to each other, meaning there will be no matching target started event logged + // as the forwarding logger did not know to forward the target started event + string targetName = null; + TargetStartedEventMinimumFields targetEvent = _buildEventManager.GetTargetStartedEvent(errorWarningEventArgs.BuildEventContext); + + if (targetEvent != null) + { + targetName = targetEvent.TargetName; + } + + // Create a new key from the error event context and the target where the error happened + ErrorWarningSummaryDictionaryKey key = new ErrorWarningSummaryDictionaryKey(errorWarningEventArgs.BuildEventContext, targetName); + + // Check to see if there is a bucket for the warning + if (!groupByProjectEntryPoint.ContainsKey(key)) + { + // If there is no bucket create a new one which contains a list of all the errors which + // happened for a given buildEventContext / target + List errorWarningEventListByTarget = new List(); + groupByProjectEntryPoint.Add(key, errorWarningEventListByTarget); + } + + // Add the error event to the correct bucket + groupByProjectEntryPoint[key].Add(errorWarningEventArgs); + } + + BuildEventContext previousEntryPoint = null; + string previousTarget = null; + // Loop through each of the bucket and print out the stack trace information for the errors + foreach (KeyValuePair> valuePair in groupByProjectEntryPoint) + { + //If the project entrypoint where the error occurred is the same as the previous message do not print the + // stack trace again + if (previousEntryPoint != valuePair.Key.EntryPointContext) + { + WriteNewLine(); + foreach (string s in _buildEventManager.ProjectCallStackFromProject(valuePair.Key.EntryPointContext)) + { + WriteMessageAligned(s, false); + } + previousEntryPoint = valuePair.Key.EntryPointContext; + } + + //If the target where the error occurred is the same as the previous message do not print the location + // where the error occurred again + if (String.Compare(previousTarget, valuePair.Key.TargetName, StringComparison.OrdinalIgnoreCase) != 0) + { + //If no targetName was specified then do not show the target where the error occurred + if (!string.IsNullOrEmpty(valuePair.Key.TargetName)) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ErrorWarningInTarget", valuePair.Key.TargetName), false); + } + previousTarget = valuePair.Key.TargetName; + } + + // Print out all of the errors under the ProjectEntryPoint / target + foreach (T errorWarningEvent in valuePair.Value) + { + if (errorWarningEvent is BuildErrorEventArgs) + { + WriteMessageAligned(" " + EventArgsFormatting.FormatEventMessage(errorWarningEvent as BuildErrorEventArgs, runningWithCharacterFileType, showProjectFile), false); + } + else if (errorWarningEvent is BuildWarningEventArgs) + { + WriteMessageAligned(" " + EventArgsFormatting.FormatEventMessage(errorWarningEvent as BuildWarningEventArgs, runningWithCharacterFileType, showProjectFile), false); + } + } + WriteNewLine(); + } + } + + /// + /// Handler for project started events + /// + /// sender (should be null) + /// event arguments + public override void ProjectStartedHandler(object sender, ProjectStartedEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + ErrorUtilities.VerifyThrowArgumentNull(e.ParentProjectBuildEventContext, "ParentProjectBuildEventContext"); + + // Add the project to the BuildManager so we can use the start information later in the build process + _buildEventManager.AddProjectStartedEvent(e, _showTimeStamp || IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + + + if (this.showPerfSummary) + { + // Create a new project performance counter for this project + MPPerformanceCounter counter = GetPerformanceCounter(e.ProjectFile, ref projectPerformanceCounters); + counter.AddEventStarted(e.TargetNames, e.BuildEventContext, e.Timestamp, s_compareContextNodeId); + } + + // If there were deferred messages then we should show them now, this will cause the project started event to be shown properly + if (_deferredMessages.ContainsKey(e.BuildEventContext)) + { + if (!showOnlyErrors && !showOnlyWarnings) + { + foreach (BuildMessageEventArgs message in _deferredMessages[e.BuildEventContext]) + { + // This will display the project started event before the messages is shown + this.MessageHandler(sender, message); + } + } + _deferredMessages.Remove(e.BuildEventContext); + } + + //If we are in diagnostic and are going to show items, show the project started event + // along with the items. The project started event will only be shown if it has not been shown before + if (Verbosity == LoggerVerbosity.Diagnostic && showItemAndPropertyList) + { + //Show the deferredProjectStartedEvent + if (!showOnlyErrors && !showOnlyWarnings) + { + DisplayDeferredProjectStartedEvent(e.BuildEventContext); + } + if (null != e.Properties) + { + WriteProperties(e, e.Properties); + } + + if (null != e.Items) + { + WriteItems(e, e.Items); + } + } + } + + /// + /// Handler for project finished events + /// + /// sender (should be null) + /// event arguments + public override void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + + + //Get the project started event so we can use its information to properly display a project finished event + ProjectStartedEventMinimumFields startedEvent = _buildEventManager.GetProjectStartedEvent(e.BuildEventContext); + ErrorUtilities.VerifyThrow(startedEvent != null, "Project finished event for {0} received without matching start event", e.ProjectFile); + + if (this.showPerfSummary) + { + // Stop the performance counter which was created in the project started event handler + MPPerformanceCounter counter = GetPerformanceCounter(e.ProjectFile, ref projectPerformanceCounters); + counter.AddEventFinished(startedEvent.TargetNames, e.BuildEventContext, e.Timestamp); + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + // Only want to show the project finished event if a project started event has been shown + if (startedEvent.ShowProjectFinishedEvent) + { + _lastProjectFullKey = GetFullProjectKey(e.BuildEventContext); + + if (!showOnlyErrors && !showOnlyWarnings) + { + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + setColor(ConsoleColor.Cyan); + + // In the project finished message the targets which were built and the project which was built + // should be shown + string targets = startedEvent.TargetNames; + + string projectName = string.Empty; + + projectName = startedEvent.ProjectFile == null ? string.Empty : startedEvent.ProjectFile; + // Show which targets were built as part of this project + if (string.IsNullOrEmpty(targets)) + { + if (e.Succeeded) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithDefaultTargetsMultiProc", projectName), true); + } + else + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithDefaultTargetsMultiProcFailed", projectName), true); + } + } + else + { + if (e.Succeeded) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithTargetNamesMultiProc", projectName, targets), true); + } + else + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithTargetNamesMultiProcFailed", projectName, targets), true); + } + } + + // In single proc only make a space between the project done event and the next line, this + // is to increase the readability on the single proc log when there are a number of done events + // or a mix of done events and project started events. Also only do this on the console and not any log file. + if (numberOfProcessors == 1 && runningWithCharacterFileType) + { + WriteNewLine(); + } + } + + ShownBuildEventContext(e.BuildEventContext); + resetColor(); + } + } + // We are done with the project started event if the project has finished building, remove its reference + _buildEventManager.RemoveProjectStartedEvent(e.BuildEventContext); + } + + /// + /// Writes out the list of property names and their values. + /// This could be done at any time during the build to show the latest + /// property values, using the cached reference to the list from the + /// appropriate ProjectStarted event. + /// + /// List of properties + internal void WriteProperties(BuildEventArgs e, IEnumerable properties) + { + if (showOnlyErrors || showOnlyWarnings) return; + ArrayList propertyList = ExtractPropertyList(properties); + + // if there are no properties to display return out of the method and dont print out anything related to displaying + // the properties, this includes the multiproc prefix information or the Initial properties header + if (propertyList.Count == 0) + { + return; + } + + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + WriteProperties(propertyList); + ShownBuildEventContext(e.BuildEventContext); + } + + internal override void OutputProperties(ArrayList list) + { + // Write the banner + setColor(ConsoleColor.Green); + WriteMessageAligned(ResourceUtilities.FormatResourceString("PropertyListHeader"), true); + // Write each property name and its value, one per line + foreach (DictionaryEntry prop in list) + { + setColor(ConsoleColor.Gray); + string propertyString = String.Format(CultureInfo.CurrentCulture, "{0} = {1}", prop.Key, EscapingUtilities.UnescapeAll((string)(prop.Value))); + WriteMessageAligned(propertyString, false); + } + resetColor(); + } + + /// + /// Write the environment strings to the console. + /// + internal override void OutputEnvironment(IDictionary environment) + { + // Write the banner + setColor(ConsoleColor.Green); + WriteMessageAligned(ResourceUtilities.FormatResourceString("EnvironmentHeader"), true); + + if (environment != null) + { + // Write each property name and its value, one per line + foreach (KeyValuePair entry in environment) + { + setColor(ConsoleColor.Gray); + string environmentMessage = String.Format(CultureInfo.CurrentCulture, "{0} = {1}", entry.Key, entry.Value); + WriteMessageAligned(environmentMessage, false); + } + } + + resetColor(); + } + + /// + /// Writes out the list of item specs and their metadata. + /// This could be done at any time during the build to show the latest + /// items, using the cached reference to the list from the + /// appropriate ProjectStarted event. + /// + /// List of items + internal void WriteItems(BuildEventArgs e, IEnumerable items) + { + if (showOnlyErrors || showOnlyWarnings) return; + SortedList itemList = ExtractItemList(items); + + // if there are no Items to display return out of the method and dont print out anything related to displaying + // the items, this includes the multiproc prefix information or the Initial items header + if (itemList.Count == 0) + { + return; + } + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + WriteItems(itemList); + ShownBuildEventContext(e.BuildEventContext); + } + + internal override void OutputItems(string itemType, ArrayList itemTypeList) + { + // Write each item, one per line + bool haveWrittenItemType = false; + foreach (ITaskItem item in itemTypeList) + { + string itemString = null; + if (!haveWrittenItemType) + { + itemString = itemType; + setColor(ConsoleColor.DarkGray); + WriteMessageAligned(itemType, false); + haveWrittenItemType = true; + } + setColor(ConsoleColor.Gray); + + // Indent the text by two tab lengths + StringBuilder result = new StringBuilder(); + result.Append(' ', 2 * tabWidth).Append(item.ItemSpec); + WriteMessageAligned(result.ToString(), false); + + IDictionary metadata = item.CloneCustomMetadata(); + + foreach (DictionaryEntry metadatum in metadata) + { + WriteMessageAligned(new String(' ', 4 * tabWidth) + metadatum.Key + " = " + item.GetMetadata(metadatum.Key as string), false); + } + } + resetColor(); + } + /// + /// Handler for target started events + /// + /// sender (should be null) + /// event arguments + public override void TargetStartedHandler(object sender, TargetStartedEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + + // Add the target started information to the buildEventManager so its information can be used + // later in the build + _buildEventManager.AddTargetStartedEvent(e, _showTimeStamp || IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + + + if (this.showPerfSummary) + { + // Create a new performance counter for this target + MPPerformanceCounter counter = GetPerformanceCounter(e.TargetName, ref targetPerformanceCounters); + counter.AddEventStarted(null, e.BuildEventContext, e.Timestamp, s_compareContextNodeIdTargetId); + } + } + + /// + /// Handler for target finished events + /// + /// sender (should be null) + /// event arguments + public override void TargetFinishedHandler(object sender, TargetFinishedEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + + if (this.showPerfSummary) + { + // Stop the performance counter started in the targetStartedEventHandler + MPPerformanceCounter counter = GetPerformanceCounter(e.TargetName, ref targetPerformanceCounters); + counter.AddEventFinished(null, e.BuildEventContext, e.Timestamp); + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + // Display the target started event arg if we have got to the target finished but have not displayed it yet. + DisplayDeferredTargetStartedEvent(e.BuildEventContext); + + // Get the target started event so we can determine whether or not to show the targetFinishedEvent + // as we only want to show target finished events if a target started event has been shown + TargetStartedEventMinimumFields startedEvent = _buildEventManager.GetTargetStartedEvent(e.BuildEventContext); + ErrorUtilities.VerifyThrow(startedEvent != null, "Started event should not be null in the finished event handler"); + if (startedEvent.ShowTargetFinishedEvent) + { + if (showTargetOutputs) + { + IEnumerable targetOutputs = e.TargetOutputs; + + if (targetOutputs != null) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetOutputItemsHeader"), false); + foreach (ITaskItem item in targetOutputs) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetOutputItem", item.ItemSpec), false); + + IDictionary metadata = item.CloneCustomMetadata(); + + foreach (DictionaryEntry metadatum in metadata) + { + WriteMessageAligned(new String(' ', 4 * tabWidth) + metadatum.Key + " = " + item.GetMetadata(metadatum.Key as string), false); + } + } + } + } + + if (!showOnlyErrors && !showOnlyWarnings) + { + _lastProjectFullKey = GetFullProjectKey(e.BuildEventContext); + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + setColor(ConsoleColor.Cyan); + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic) || _showEventId) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetMessageWithId", e.Message, e.BuildEventContext.TargetId), true); + } + else + { + WriteMessageAligned(e.Message, true); + } + resetColor(); + } + + ShownBuildEventContext(e.BuildEventContext); + } + } + + //We no longer need this target started event, it can be removed + _buildEventManager.RemoveTargetStartedEvent(e.BuildEventContext); + } + + /// + /// Handler for task started events + /// + /// sender (should be null) + /// event arguments + public override void TaskStartedHandler(object sender, TaskStartedEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + + // if verbosity is detailed or diagnostic + + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + DisplayDeferredStartedEvents(e.BuildEventContext); + + if (!showOnlyErrors && !showOnlyWarnings) + { + bool prefixAlreadyWritten = WriteTargetMessagePrefix(e, e.BuildEventContext, e.Timestamp); + setColor(ConsoleColor.DarkCyan); + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic) || _showEventId) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TaskMessageWithId", e.Message, e.BuildEventContext.TaskId), prefixAlreadyWritten); + } + else + { + WriteMessageAligned(e.Message, prefixAlreadyWritten); + } + resetColor(); + } + + ShownBuildEventContext(e.BuildEventContext); + } + + if (this.showPerfSummary) + { + // Create a new performance counter for this task + MPPerformanceCounter counter = GetPerformanceCounter(e.TaskName, ref taskPerformanceCounters); + counter.AddEventStarted(null, e.BuildEventContext, e.Timestamp, null); + } + } + + /// + /// Handler for task finished events + /// + /// sender (should be null) + /// event arguments + public override void TaskFinishedHandler(object sender, TaskFinishedEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + if (this.showPerfSummary) + { + // Stop the task performance counter which was started in the task started event + MPPerformanceCounter counter = GetPerformanceCounter(e.TaskName, ref taskPerformanceCounters); + counter.AddEventFinished(null, e.BuildEventContext, e.Timestamp); + } + + // if verbosity is detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + if (!showOnlyErrors && !showOnlyWarnings) + { + bool prefixAlreadyWritten = WriteTargetMessagePrefix(e, e.BuildEventContext, e.Timestamp); + setColor(ConsoleColor.DarkCyan); + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic) || _showEventId) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TaskMessageWithId", e.Message, e.BuildEventContext.TaskId), prefixAlreadyWritten); + } + else + { + WriteMessageAligned(e.Message, prefixAlreadyWritten); + } + resetColor(); + } + ShownBuildEventContext(e.BuildEventContext); + } + } + + /// + /// Prints an error event + /// + public override void ErrorHandler(object sender, BuildErrorEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + // Keep track of the number of error events raisd + errorCount++; + + // If there is an error we need to walk up the call stack and make sure that + // the project started events back to the root project know an error has occurred + // and are not removed when they finish + _buildEventManager.SetErrorWarningFlagOnCallStack(e.BuildEventContext); + + TargetStartedEventMinimumFields targetStartedEvent = _buildEventManager.GetTargetStartedEvent(e.BuildEventContext); + // Can be null if the error occurred outside of a target, or the error occurres before the targetStartedEvent + if (targetStartedEvent != null) + { + targetStartedEvent.ErrorInTarget = true; + } + + DisplayDeferredStartedEvents(e.BuildEventContext); + + // Display only if showOnlyWarnings is false; + // unless showOnlyErrors is true, which trumps it. + if (!showOnlyWarnings || showOnlyErrors) + { + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + } + + setColor(ConsoleColor.Red); + WriteMessageAligned(EventArgsFormatting.FormatEventMessage(e, runningWithCharacterFileType, showProjectFile), true); + ShownBuildEventContext(e.BuildEventContext); + if (ShowSummary) + { + if (!errorList.Contains(e)) + { + errorList.Add(e); + } + } + resetColor(); + } + } + + /// + /// Prints a warning event + /// + public override void WarningHandler(object sender, BuildWarningEventArgs e) + { + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + // Keep track of the number of warning events raised during the build + warningCount++; + + // If there is a warning we need to walk up the call stack and make sure that + // the project started events back to the root project know a warning has ocured + // and are not removed when they finish + _buildEventManager.SetErrorWarningFlagOnCallStack(e.BuildEventContext); + TargetStartedEventMinimumFields targetStartedEvent = _buildEventManager.GetTargetStartedEvent(e.BuildEventContext); + + // Can be null if the error occurred outside of a target, or the error occurres before the targetStartedEvent + if (targetStartedEvent != null) + { + targetStartedEvent.ErrorInTarget = true; + } + + DisplayDeferredStartedEvents(e.BuildEventContext); + + // Display only if showOnlyErrors is false; + // unless showOnlyWarnings is true, which trumps it. + if (!showOnlyErrors || showOnlyWarnings) + { + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + } + + setColor(ConsoleColor.Yellow); + WriteMessageAligned(EventArgsFormatting.FormatEventMessage(e, runningWithCharacterFileType, showProjectFile), true); + } + + ShownBuildEventContext(e.BuildEventContext); + + if (ShowSummary) + { + if (!warningList.Contains(e)) + { + warningList.Add(e); + } + } + resetColor(); + } + + /// + /// Prints a message event + /// + public override void MessageHandler(object sender, BuildMessageEventArgs e) + { + if (showOnlyErrors || showOnlyWarnings) return; + + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + bool print = false; + bool lightenText = false; + + if (e is TaskCommandLineEventArgs) + { + if ((_showCommandLine.HasValue && !_showCommandLine.Value) || (!_showCommandLine.HasValue && !IsVerbosityAtLeast(LoggerVerbosity.Normal))) + { + return; + } + print = true; + } + else + { + switch (e.Importance) + { + case MessageImportance.High: + print = IsVerbosityAtLeast(LoggerVerbosity.Minimal); + break; + case MessageImportance.Normal: + print = IsVerbosityAtLeast(LoggerVerbosity.Normal); + lightenText = true; + break; + case MessageImportance.Low: + print = IsVerbosityAtLeast(LoggerVerbosity.Detailed); + lightenText = true; + break; + default: + ErrorUtilities.VerifyThrow(false, "Impossible"); + break; + } + } + + if (print) + { + // If the event has a valid Project contextId but the project started event has not been fired, the message needs to be + // buffered until the project started event is fired + if ( + _hasBuildStarted + && e.BuildEventContext.ProjectContextId != BuildEventContext.InvalidProjectContextId + && _buildEventManager.GetProjectStartedEvent(e.BuildEventContext) == null + && IsVerbosityAtLeast(LoggerVerbosity.Normal) + ) + { + List messageList = null; + if (_deferredMessages.ContainsKey(e.BuildEventContext)) + { + messageList = _deferredMessages[e.BuildEventContext]; + } + else + { + messageList = new List(); + _deferredMessages.Add(e.BuildEventContext, messageList); + } + messageList.Add(e); + return; + } + + DisplayDeferredStartedEvents(e.BuildEventContext); + + // Print the message event out to the console + PrintMessage(e, lightenText); + ShownBuildEventContext(e.BuildEventContext); + } + } + + private void DisplayDeferredStartedEvents(BuildEventContext e) + { + if (showOnlyErrors || showOnlyWarnings) return; + + // Display any project started events which were deferred until a visible + // message from their project is displayed + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + DisplayDeferredProjectStartedEvent(e); + } + + // Display any target started events which were deferred until a visible + // message from their target is displayed + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + DisplayDeferredTargetStartedEvent(e); + } + } + + /// + /// Prints out a message event to the console + /// + private void PrintMessage(BuildMessageEventArgs e, bool lightenText) + { + string nonNullMessage = null; + + // Include file information if present. + if (e.File != null) + { + nonNullMessage = EventArgsFormatting.FormatEventMessage(e, runningWithCharacterFileType, showProjectFile); + } + else + { + nonNullMessage = (e.Message == null) ? String.Empty : e.Message; + } + + int prefixAdjustment = 0; + + // Do not include prefixAdjustment if TaskId is invalid or file information is present. + // We want messages with file information to appear aligned with warning and error messages. + if (e.BuildEventContext.TaskId != BuildEventContext.InvalidTaskId && e.File == null) + { + prefixAdjustment = 2; + } + + if (lightenText) + { + setColor(ConsoleColor.DarkGray); + } + + PrintTargetNamePerMessage(e, lightenText); + + // On diagnostic or if showEventId is set the task message should also display the taskId to assist debugging + if ((IsVerbosityAtLeast(LoggerVerbosity.Diagnostic) || _showEventId) && e.BuildEventContext.TaskId != BuildEventContext.InvalidTaskId) + { + bool prefixAlreadyWritten = WriteTargetMessagePrefix(e, e.BuildEventContext, e.Timestamp); + WriteMessageAligned(ResourceUtilities.FormatResourceString("TaskMessageWithId", nonNullMessage, e.BuildEventContext.TaskId), prefixAlreadyWritten, prefixAdjustment); + } + else + { + //A time stamp may be shown on verbosities lower than diagnostic + if (_showTimeStamp || IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + bool prefixAlreadyWritten = WriteTargetMessagePrefix(e, e.BuildEventContext, e.Timestamp); + WriteMessageAligned(nonNullMessage, prefixAlreadyWritten, prefixAdjustment); + } + else + { + WriteMessageAligned(nonNullMessage, false, prefixAdjustment); + } + } + + if (lightenText) + { + resetColor(); + } + } + + private void PrintTargetNamePerMessage(BuildMessageEventArgs e, bool lightenText) + { + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + // Event Context of the current message + BuildEventContext currentBuildEventContext = e.BuildEventContext; + + // Should the target name be written before the message + bool writeTargetName = false; + string targetName = string.Empty; + + // Does the context (Project, Node, Context, Target, NOT task) of the previous event match the current message + bool contextAreEqual = s_compareContextNodeIdTargetId.Equals(currentBuildEventContext, _lastDisplayedBuildEventContext == null ? null : _lastDisplayedBuildEventContext); + + TargetStartedEventMinimumFields targetStartedEvent = null; + // If the previous event does not have the same target context information, the target name needs to be printed to the console + // to give the message some more contextual information + if (!contextAreEqual) + { + targetStartedEvent = _buildEventManager.GetTargetStartedEvent(currentBuildEventContext); + // Some messages such as engine messages will not have a target started event, in their case, dont print the targetName + if (targetStartedEvent != null) + { + targetName = targetStartedEvent.TargetName; + writeTargetName = true; + } + } + else + { + writeTargetName = false; + } + + if (writeTargetName) + { + bool prefixAlreadyWritten = WriteTargetMessagePrefix(e, targetStartedEvent.ProjectBuildEventContext, targetStartedEvent.TimeStamp); + + setColor(ConsoleColor.Cyan); + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic) || _showEventId) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetMessageWithId", targetName, e.BuildEventContext.TargetId), prefixAlreadyWritten); + } + else + { + WriteMessageAligned(targetName + ":", prefixAlreadyWritten); + } + + if (lightenText) + { + setColor(ConsoleColor.DarkGray); + } + else + { + resetColor(); + } + } + } + } + + private bool WriteTargetMessagePrefix(BuildEventArgs e, BuildEventContext context, DateTime timeStamp) + { + bool prefixAlreadyWritten = true; + ProjectFullKey currentProjectFullKey = GetFullProjectKey(e.BuildEventContext); + if (!(_lastProjectFullKey.Equals(currentProjectFullKey))) + { + // Write the prefix information about the target for the message + WriteLinePrefix(context, timeStamp, false); + _lastProjectFullKey = currentProjectFullKey; + } + else + { + prefixAlreadyWritten = false; + } + return prefixAlreadyWritten; + } + + /// + /// Writes a message to the console, aligned and formatted to fit within the console width + /// + /// Message to be formatted to fit on the console + /// Has the prefix(timestamp or key been written) + private void WriteMessageAligned(string message, bool prefixAlreadyWritten) + { + WriteMessageAligned(message, prefixAlreadyWritten, 0); + } + + /// + /// Writes a message to the console, aligned and formatted to fit within the console width + /// + /// Message to be formatted to fit on the console + /// Has the prefix(timestamp or key been written) + private void WriteMessageAligned(string message, bool prefixAlreadyWritten, int prefixAdjustment) + { + // This method may require the splitting of lines inorder to format them to the console, this must be an atomic operation + lock (_lockObject) + { + int adjustedPrefixWidth = _prefixWidth + prefixAdjustment; + + // The string may contain new lines, treat each new line as a different string to format and send to the console + string[] nonNullMessages = SplitStringOnNewLines(message); + for (int i = 0; i < nonNullMessages.Length; i++) + { + string nonNullMessage = nonNullMessages[i]; + // Take into account the new line char which will be added to the end or each reformatted string + int bufferWidthMinusNewLine = _bufferWidth - 1; + + // If the buffer is larger then the prefix information (timestamp and key) then reformat the messages. + // If there is not enough room just print the message out and let the console do the formatting + bool bufferIsLargerThanPrefix = bufferWidthMinusNewLine > adjustedPrefixWidth; + bool messageAndPrefixTooLargeForBuffer = (nonNullMessage.Length + adjustedPrefixWidth) > bufferWidthMinusNewLine; + if (bufferIsLargerThanPrefix && messageAndPrefixTooLargeForBuffer && _alignMessages) + { + // Our message may have embedded tab characters, so expand those to their space + // equivalent so that wrapping works as expected. + nonNullMessage = nonNullMessage.Replace("\t", consoleTab); + + // If the message and the prefix are too large for one line in the console, split the string to fit + int index = 0; + int messageLength = nonNullMessage.Length; + int amountToCopy = 0; + // Loop until all the string has been sent to the console + while (index < messageLength) + { + // Calculate how many chars will fit on the console buffer + amountToCopy = (messageLength - index) < (bufferWidthMinusNewLine - adjustedPrefixWidth) ? (messageLength - index) : (bufferWidthMinusNewLine - adjustedPrefixWidth); + WriteBasedOnPrefix(nonNullMessage.Substring(index, amountToCopy), (prefixAlreadyWritten && index == 0 && i == 0), adjustedPrefixWidth); + index = index + amountToCopy; + } + } + else + { + //there is not enough room just print the message out and let the console do the formatting + WriteBasedOnPrefix(nonNullMessage, prefixAlreadyWritten, adjustedPrefixWidth); + } + } + } + } + + /// + /// Write message takinginto account whether or not the prefix (timestamp and key) have already been written on the line + /// + /// + /// + private void WriteBasedOnPrefix(string nonNullMessage, bool prefixAlreadyWritten, int adjustedPrefixWidth) + { + if (prefixAlreadyWritten) + { + write(nonNullMessage + Environment.NewLine); + } + else + { + // No prefix info has been written, indent the line to the proper location + write(IndentString(nonNullMessage, adjustedPrefixWidth)); + } + } + + /// + /// Will display the target started event which was deferred until the first visible message for the target is ready to be displayed + /// + private void DisplayDeferredTargetStartedEvent(BuildEventContext e) + { + if (showOnlyErrors || showOnlyWarnings) return; + + // Get the deferred target started event + TargetStartedEventMinimumFields targetStartedEvent = _buildEventManager.GetTargetStartedEvent(e); + + //Make sure we have not shown the event before + if (targetStartedEvent != null && !targetStartedEvent.ShowTargetFinishedEvent) + { + //Since the target started event has been shows, the target finished event should also be shown + targetStartedEvent.ShowTargetFinishedEvent = true; + + // If there are any other started events waiting and we are the first message, show them + DisplayDeferredStartedEvents(targetStartedEvent.ProjectBuildEventContext); + + WriteLinePrefix(targetStartedEvent.ProjectBuildEventContext, targetStartedEvent.TimeStamp, false); + + setColor(ConsoleColor.Cyan); + + ProjectStartedEventMinimumFields startedEvent = _buildEventManager.GetProjectStartedEvent(e); + ErrorUtilities.VerifyThrow(startedEvent != null, "Project Started should not be null in deferred target started"); + string currentProjectFile = startedEvent.ProjectFile == null ? string.Empty : startedEvent.ProjectFile; + + string targetName = null; + if (IsVerbosityAtLeast(LoggerVerbosity.Diagnostic) || _showEventId) + { + targetName = ResourceUtilities.FormatResourceString("TargetMessageWithId", targetStartedEvent.TargetName, targetStartedEvent.ProjectBuildEventContext.TargetId); + } + else + { + targetName = targetStartedEvent.TargetName; + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + if (String.Equals(currentProjectFile, targetStartedEvent.TargetFile, StringComparison.OrdinalIgnoreCase)) + { + if (!String.IsNullOrEmpty(targetStartedEvent.ParentTarget)) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetStartedProjectDepends", targetName, currentProjectFile, targetStartedEvent.ParentTarget), true); + } + else + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetStartedProjectEntry", targetName, currentProjectFile), true); + } + } + else + { + if (!String.IsNullOrEmpty(targetStartedEvent.ParentTarget)) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetStartedFileProjectDepends", targetName, targetStartedEvent.TargetFile, currentProjectFile, targetStartedEvent.ParentTarget), true); + } + else + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetStartedFileProjectEntry", targetName, targetStartedEvent.TargetFile, currentProjectFile), true); + } + } + } + else + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("TargetStartedFileProjectEntry", targetName, targetStartedEvent.TargetFile, currentProjectFile), true); + } + + resetColor(); + ShownBuildEventContext(e); + } + } + + /// + /// Will display the project started event which was deferred until the first visible message for the project is ready to be displayed + /// + private void DisplayDeferredProjectStartedEvent(BuildEventContext e) + { + if (showOnlyErrors || showOnlyWarnings) return; + + if (!SkipProjectStartedText) + { + // Get the project started event which matched the passed in event context + ProjectStartedEventMinimumFields projectStartedEvent = _buildEventManager.GetProjectStartedEvent(e); + + // Make sure the project started event has not been show yet + if (projectStartedEvent != null && !projectStartedEvent.ShowProjectFinishedEvent) + { + projectStartedEvent.ShowProjectFinishedEvent = true; + + ProjectStartedEventMinimumFields parentStartedEvent = projectStartedEvent.ParentProjectStartedEvent; + if (parentStartedEvent != null) + { + //Make sure that if there are any events deferred on this event to show them first + DisplayDeferredStartedEvents(parentStartedEvent.ProjectBuildEventContext); + } + + string current = projectStartedEvent.ProjectFile == null ? string.Empty : projectStartedEvent.ProjectFile; + string previous = parentStartedEvent == null ? null : parentStartedEvent.ProjectFile; + string targetNames = projectStartedEvent.TargetNames; + + // Log 0-based node id's, where 0 is the parent. This is a little unnatural for the reader, + // but avoids confusion by being consistent with the Engine and any error messages it may produce. + int currentProjectNodeId = (projectStartedEvent.ProjectBuildEventContext.NodeId); + if (previous == null) + { + WriteLinePrefix(projectStartedEvent.FullProjectKey, projectStartedEvent.TimeStamp, false); + setColor(ConsoleColor.Cyan); + string message = string.Empty; + if ((targetNames == null) || (targetNames.Length == 0)) + { + message = ResourceUtilities.FormatResourceString("ProjectStartedTopLevelProjectWithDefaultTargets", current, currentProjectNodeId); + } + else + { + message = ResourceUtilities.FormatResourceString("ProjectStartedTopLevelProjectWithTargetNames", current, currentProjectNodeId, targetNames); + } + + WriteMessageAligned(message, true); + resetColor(); + } + else + { + WriteLinePrefix(parentStartedEvent.FullProjectKey, parentStartedEvent.TimeStamp, false); + setColor(ConsoleColor.Cyan); + if ((targetNames == null) || (targetNames.Length == 0)) + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ProjectStartedWithDefaultTargetsMultiProc", previous, parentStartedEvent.FullProjectKey, current, projectStartedEvent.FullProjectKey, currentProjectNodeId), true); + } + else + { + WriteMessageAligned(ResourceUtilities.FormatResourceString("ProjectStartedWithTargetsMultiProc", previous, parentStartedEvent.FullProjectKey, current, projectStartedEvent.FullProjectKey, currentProjectNodeId, targetNames), true); + } + resetColor(); + } + + // Make the last shown buildevent context to be null so that the next message will always print out the target name. If this is not null + // then the first message after the project started event will not have the target name printed out which was causing some confusion. + ShownBuildEventContext(null); + } + } + } + + /// + /// Prints a custom event + /// + public override void CustomEventHandler(object sender, CustomBuildEventArgs e) + { + if (showOnlyErrors || showOnlyWarnings) return; + + ErrorUtilities.VerifyThrowArgumentNull(e.BuildEventContext, "BuildEventContext"); + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + // ignore custom events with null messages -- some other + // logger will handle them appropriately + if (e.Message != null) + { + DisplayDeferredStartedEvents(e.BuildEventContext); + WriteLinePrefix(e.BuildEventContext, e.Timestamp, false); + WriteMessageAligned(e.Message, true); + ShownBuildEventContext(e.BuildEventContext); + } + } + } + + /// + /// Writes message contextual information for each message displayed on the console + /// + private void WriteLinePrefix(BuildEventContext e, DateTime eventTimeStamp, bool isMessagePrefix) + { + WriteLinePrefix(GetFullProjectKey(e).ToString(verbosity), eventTimeStamp, isMessagePrefix); + } + + private void WriteLinePrefix(string key, DateTime eventTimeStamp, bool isMessagePrefix) + { + // Dont want any prefix for single proc + if (numberOfProcessors == 1) + { + return; + } + + setColor(ConsoleColor.Cyan); + + string context = string.Empty; + if (_showTimeStamp || IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)) + { + context = LogFormatter.FormatLogTimeStamp(eventTimeStamp); + } + + string prefixString = string.Empty; + + if (!isMessagePrefix || IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + prefixString = ResourceUtilities.FormatResourceString("BuildEventContext", context, key) + ">"; + } + else + { + prefixString = ResourceUtilities.FormatResourceString("BuildEventContext", context, string.Empty) + " "; + } + + WritePretty(prefixString); + resetColor(); + + if (_prefixWidth == 0) + { + _prefixWidth = prefixString.Length; + } + } + + /// + /// Extract the full project key from the BuildEventContext + /// + private ProjectFullKey GetFullProjectKey(BuildEventContext e) + { + ProjectStartedEventMinimumFields startedEvent = null; + + if (e != null) + { + startedEvent = _buildEventManager.GetProjectStartedEvent(e); + } + + //Project started event can be null, if the message has come before the project started event + // or the message is not part of a project such as if the message came from the engine + if (startedEvent == null) + { + return new ProjectFullKey(0, 0); + } + else + { + return new ProjectFullKey(startedEvent.ProjectKey, startedEvent.EntryPointKey); + } + } + + /// + /// Returns a performance counter for a given scope (either task name or target name) + /// from the given table. + /// + /// Task name or target name. + /// Table that has tasks or targets. + internal new static MPPerformanceCounter GetPerformanceCounter(string scopeName, ref Hashtable table) + { + // Lazily construct the performance counter table. + if (table == null) + { + table = new Hashtable(StringComparer.OrdinalIgnoreCase); + } + + MPPerformanceCounter counter = (MPPerformanceCounter)table[scopeName]; + + // And lazily construct the performance counter itself. + if (counter == null) + { + counter = new MPPerformanceCounter(scopeName); + table[scopeName] = counter; + } + + return counter; + } + #endregion + + #region InternalClass + /// + /// Stores and calculates the performance numbers for the different events + /// + internal class MPPerformanceCounter : PerformanceCounter + { + // Set of performance counters for a project + private Hashtable _internalPerformanceCounters; + // Dictionary mapping event context to the start number of ticks, this will be used to calculate the amount + // of time between the start of the performance counter and the end + // An object is being used to box the start time long value to prevent jitting when this code path is executed. + private Dictionary _startedEvent; + private int _messageIdentLevel = 2; + + internal int MessageIdentLevel + { + get { return _messageIdentLevel; } + set { _messageIdentLevel = value; } + } + + internal MPPerformanceCounter(string scopeName) + : base(scopeName) + { + // Do Nothing + } + + /// + /// Add a started event to the performance counter, by adding the event this sets the start time of the performance counter + /// + internal void AddEventStarted(string projectTargetNames, BuildEventContext buildEventContext, DateTime eventTimeStamp, IEqualityComparer comparer) + { + //If the projectTargetNames are set then we should be a projectstarted event + if (!string.IsNullOrEmpty(projectTargetNames)) + { + // Create a new performance counter for the project entry point to calculate how much time and how many calls + // were made to the entry point + MPPerformanceCounter entryPoint = GetPerformanceCounter(projectTargetNames, ref _internalPerformanceCounters); + entryPoint.AddEventStarted(null, buildEventContext, eventTimeStamp, s_compareContextNodeIdTargetId); + // Indent the output so it is intented with respect to its parent project + entryPoint._messageIdentLevel = 7; + } + + if (_startedEvent == null) + { + if (comparer == null) + { + _startedEvent = new Dictionary(); + } + else + { + _startedEvent = new Dictionary(comparer); + } + } + + if (!_startedEvent.ContainsKey(buildEventContext)) + { + _startedEvent.Add(buildEventContext, (object)eventTimeStamp.Ticks); + ++calls; + } + } + + /// + /// Add a finished event to the performance counter, so perf numbers can be calculated + /// + internal void AddEventFinished(string projectTargetNames, BuildEventContext buildEventContext, DateTime eventTimeStamp) + { + if (!string.IsNullOrEmpty(projectTargetNames)) + { + MPPerformanceCounter entryPoint = GetPerformanceCounter(projectTargetNames, ref _internalPerformanceCounters); + entryPoint.AddEventFinished(null, buildEventContext, eventTimeStamp); + } + + ErrorUtilities.VerifyThrow(_startedEvent != null, "Cannot have finished counter without started counter. "); + + if (_startedEvent.ContainsKey(buildEventContext)) + { + // Calculate the amount of time spent in the event based on the time stamp of when + // the started event was created and when the finished event was created + elapsedTime += (TimeSpan.FromTicks(eventTimeStamp.Ticks - (long)_startedEvent[buildEventContext])); + _startedEvent.Remove(buildEventContext); + } + } + + /// + /// Print out the performance counter message + /// + internal override void PrintCounterMessage(WriteLinePrettyFromResourceDelegate WriteLinePrettyFromResource, ColorSetter setColor, ColorResetter resetColor) + { + // round: submillisecond values are not meaningful + string time = String.Format(CultureInfo.CurrentCulture, + "{0,5}", Math.Round(elapsedTime.TotalMilliseconds, 0)); + + WriteLinePrettyFromResource + ( + _messageIdentLevel, + "PerformanceLine", + time, + String.Format(CultureInfo.CurrentCulture, + "{0,-40}" /* pad to 40 align left */, scopeName), + String.Format(CultureInfo.CurrentCulture, + "{0,3}", calls) + ); + + if (_internalPerformanceCounters != null && _internalPerformanceCounters.Count > 0) + { + // For each of the entry points in the project print out the performance numbers for them + foreach (MPPerformanceCounter counter in _internalPerformanceCounters.Values) + { + setColor(ConsoleColor.White); + counter.PrintCounterMessage(WriteLinePrettyFromResource, setColor, resetColor); + resetColor(); + } + } + } + } + #endregion + + #region internal MemberData + private static ComparerContextNodeId s_compareContextNodeId = new ComparerContextNodeId(); + private static ComparerContextNodeIdTargetId s_compareContextNodeIdTargetId = new ComparerContextNodeIdTargetId(); + private BuildEventContext _lastDisplayedBuildEventContext; + private int _bufferWidth = -1; + private object _lockObject = new Object(); + private int _prefixWidth = 0; + private ProjectFullKey _lastProjectFullKey = new ProjectFullKey(-1, -1); + private bool _alignMessages; + private bool _forceNoAlign; + private bool _showEventId; + // According to the documentaion for ENABLE_PROCESSED_OUTPUT tab width for the console is 8 characters + private const string consoleTab = " "; + #endregion + + #region Per-build Members + //Holds messages that were going to be shown before the project started event, buffer them until the project started event is shown + private Dictionary> _deferredMessages; + private BuildEventManager _buildEventManager; + // Has the build started + private bool _hasBuildStarted; + private bool? _showCommandLine; + private bool _showTimeStamp; + #endregion + } +} diff --git a/src/XMakeBuildEngine/Logging/ParallelLogger/ParallelLoggerHelpers.cs b/src/XMakeBuildEngine/Logging/ParallelLogger/ParallelLoggerHelpers.cs new file mode 100644 index 00000000000..7b5714531ee --- /dev/null +++ b/src/XMakeBuildEngine/Logging/ParallelLogger/ParallelLoggerHelpers.cs @@ -0,0 +1,709 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.IO; +using System.Diagnostics; +using System.Threading; +using System.Globalization; + +namespace Microsoft.Build.BackEnd.Logging +{ + /// + /// Stores and manages projects and targets events for logging purposes + /// + internal class BuildEventManager + { + #region Data + private Dictionary _projectStartedEvents; + private Dictionary _targetStartedEvents; + private Dictionary _projectTargetKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + private Dictionary _projectKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static ComparerContextNodeId s_compareContextNodeId = new ComparerContextNodeId(); + private static ComparerContextNodeIdTargetId s_compareContextNodeIdTargetId = new ComparerContextNodeIdTargetId(); + private int _projectIncrementKey; + #endregion + + #region Constructors + internal BuildEventManager() + { + _projectStartedEvents = new Dictionary(s_compareContextNodeId); + _targetStartedEvents = new Dictionary(s_compareContextNodeIdTargetId); + _projectIncrementKey = 0; + } + #endregion + + #region Methods + /// + /// Adds a new project to the list of project started events which have been fired + /// + internal void AddProjectStartedEvent(ProjectStartedEventArgs e, bool requireTimestamp) + { //Parent event can be null if this is the root project + ProjectStartedEventMinimumFields parentEvent = GetProjectStartedEvent(e.ParentProjectBuildEventContext); + lock (_projectStartedEvents) + { + if (!_projectStartedEvents.ContainsKey(e.BuildEventContext)) + { + int projectTargetKeyLocal = 1; + int projectIncrementKeyLocal = 1; + // If we haven't seen this project before (by full path) then + // allocate a new key for it and save it away + if (!_projectKey.ContainsKey(e.ProjectFile)) + { + _projectIncrementKey += 1; + + _projectKey[e.ProjectFile] = _projectIncrementKey; + projectIncrementKeyLocal = _projectIncrementKey; + } + else + { + // We've seen this project before, so retrieve it + projectIncrementKeyLocal = _projectKey[e.ProjectFile]; + } + + // If we haven't seen any entrypoint for the current project (by full path) then + // allocate a new entry point key + if (!_projectTargetKey.ContainsKey(e.ProjectFile)) + { + _projectTargetKey[e.ProjectFile] = projectTargetKeyLocal; + } + else + { + // We've seen this project before, but not this entrypoint, so increment + // the entrypoint key that we have. + projectTargetKeyLocal = _projectTargetKey[e.ProjectFile] + 1; + _projectTargetKey[e.ProjectFile] = projectTargetKeyLocal; + } + + _projectStartedEvents.Add(e.BuildEventContext, new ProjectStartedEventMinimumFields(projectIncrementKeyLocal, projectTargetKeyLocal, e, parentEvent, requireTimestamp)); + } + } + } + + /// + /// Adds a new target to the list of project started events which have been fired + /// + internal void AddTargetStartedEvent(TargetStartedEventArgs e, bool requireTimeStamp) + { + if (!_targetStartedEvents.ContainsKey(e.BuildEventContext)) + { + _targetStartedEvents.Add(e.BuildEventContext, new TargetStartedEventMinimumFields(e, requireTimeStamp)); + } + } + + /// + /// Get a call stack of event contexts for a starting point event context + /// + internal List GetProjectCallStack(BuildEventContext e) + { + List stackTrace = new List(); + + ProjectStartedEventMinimumFields currentKey = GetProjectStartedEvent(e); + + // currentKey can be null if the stack trace is requested before the project started event has been seen + // or if the call stack is requested by an event which is not associated with a project such as an event + // from the engine itself + if (currentKey != null) + { + //Add the event where the stack should start + stackTrace.Add(currentKey); + + // Loop through the call tree until the root project started event has been found + while (currentKey.ParentProjectStartedEvent != null) + { + currentKey = currentKey.ParentProjectStartedEvent; + stackTrace.Add(currentKey); + } + } + return stackTrace; + } + + /// + /// Set an error flag on all projects in the call stack of a given event context + /// + internal void SetErrorWarningFlagOnCallStack(BuildEventContext e) + { + List projectStackTrace = GetProjectCallStack(e); + foreach (ProjectStartedEventMinimumFields startedEvent in projectStackTrace) + { + // Can be null if the event occures before the project startedEvent or outside of a project + if (startedEvent != null) + { + startedEvent.ErrorInProject = true; + } + } + } + + /// + /// Retrieve the project call stack based on the starting point of buildEventContext e + /// + internal string[] ProjectCallStackFromProject(BuildEventContext e) + { + BuildEventContext currentKey = e; + + ProjectStartedEventMinimumFields startedEvent = GetProjectStartedEvent(currentKey); + + List stackTrace = new List(); + // If there is no started event then there should be no stack trace + // this is a valid situation if the event occures in the engine or outside the context of a project + // or the event is raised before the project started event + if (startedEvent == null) + { + return new string[0]; + } + + List projectStackTrace = GetProjectCallStack(e); + foreach (ProjectStartedEventMinimumFields projectStartedEvent in projectStackTrace) + { + if (!string.IsNullOrEmpty(projectStartedEvent.TargetNames)) + { + stackTrace.Add(ResourceUtilities.FormatResourceString("ProjectStackWithTargetNames", projectStartedEvent.ProjectFile, projectStartedEvent.TargetNames, projectStartedEvent.FullProjectKey)); + } + else + { + stackTrace.Add(ResourceUtilities.FormatResourceString("ProjectStackWithDefaultTargets", projectStartedEvent.ProjectFile, projectStartedEvent.FullProjectKey)); + } + } + stackTrace.Reverse(); + return stackTrace.ToArray(); + } + + /// + /// Get a deferred project started event based on a given event context + /// + internal ProjectStartedEventMinimumFields GetProjectStartedEvent(BuildEventContext e) + { + ProjectStartedEventMinimumFields buildEvent; + if (_projectStartedEvents.ContainsKey(e)) + { + buildEvent = _projectStartedEvents[e]; + } + else + { + buildEvent = null; + } + return buildEvent; + } + + /// + /// Get a deferred target started event based on a given event context + /// + internal TargetStartedEventMinimumFields GetTargetStartedEvent(BuildEventContext e) + { + TargetStartedEventMinimumFields buildEvent; + if (_targetStartedEvents.ContainsKey(e)) + { + buildEvent = _targetStartedEvents[e]; + } + else + { + buildEvent = null; + } + return buildEvent; + } + + /// + /// Will remove a project started event from the list of deferred project started events + /// + internal void RemoveProjectStartedEvent(BuildEventContext e) + { + ProjectStartedEventMinimumFields startedEvent = GetProjectStartedEvent(e); + // Only remove the project from the event list if it is in the list, and no errors have occured in the project + if (startedEvent != null && !startedEvent.ErrorInProject) + { + _projectStartedEvents.Remove(e); + } + } + + /// + /// Will remove a project started event from the list of deferred project started events + /// + internal void RemoveTargetStartedEvent(BuildEventContext e) + { + TargetStartedEventMinimumFields startedEvent = GetTargetStartedEvent(e); + // Only remove the project from the event list if it is in the list, and no errors have occured in the project + if (startedEvent != null && !startedEvent.ErrorInTarget) + { + _targetStartedEvents.Remove(e); + } + } + #endregion + } + + /// + /// Compares two event contexts on ProjectContextId and NodeId only + /// + internal class ComparerContextNodeId : IEqualityComparer + { + #region Methods + public bool Equals(T x, T y) + { + BuildEventContext contextX = x as BuildEventContext; + BuildEventContext contextY = y as BuildEventContext; + + if (contextX == null || contextY == null) + { + return false; + } + + // Return true if the fields match: + return (contextX.NodeId == contextY.NodeId) + && (contextX.ProjectContextId == contextY.ProjectContextId); + } + + public int GetHashCode(T x) + { + BuildEventContext context = x as BuildEventContext; + return (context.ProjectContextId + (context.NodeId << 24)); + } + #endregion + } + + /// + /// Compares two event contexts based on the ProjectContextId, NodeId, and TargetId only + /// + internal class ComparerContextNodeIdTargetId : IEqualityComparer + { + #region Methods + public bool Equals(T x, T y) + { + BuildEventContext contextX = x as BuildEventContext; + BuildEventContext contextY = y as BuildEventContext; + + if (contextX == null || contextY == null) + { + return false; + } + + // Return true if the fields match: + return (contextX.NodeId == contextY.NodeId) + && (contextX.ProjectContextId == contextY.ProjectContextId) + && (contextX.TargetId == contextY.TargetId); + } + + public int GetHashCode(T x) + { + BuildEventContext context = x as BuildEventContext; + return (context.ProjectContextId + (context.NodeId << 24)); + } + + #endregion + } + + /// + /// This class stands in for a full project started event because it contains only the + /// minimum amount of inforomation needed for the logger + /// + internal class ProjectStartedEventMinimumFields + { + #region Data + private DateTime _timeStamp; + private string _targetNames; + private string _projectFile; + private bool _showProjectFinishedEvent; + private bool _errorInProject; + private int _projectId; + private ProjectFullKey _projectFullKey; + private BuildEventContext _buildEventContext; + private ProjectStartedEventMinimumFields _parentProjectStartedEvent; + #endregion + + #region Properties + + internal DateTime TimeStamp + { + get + { + return _timeStamp; + } + } + + internal int ProjectKey + { + get + { + return _projectFullKey.ProjectKey; + } + } + + internal int EntryPointKey + { + get + { + return _projectFullKey.EntryPointKey; + } + } + + internal string FullProjectKey + { + get + { + return _projectFullKey.ToString(); + } + } + + internal ProjectStartedEventMinimumFields ParentProjectStartedEvent + { + get + { + return _parentProjectStartedEvent; + } + } + + internal string TargetNames + { + get + { + return _targetNames; + } + } + + internal int ProjectId + { + get + { + return _projectId; + } + } + + internal string ProjectFile + { + get + { + return _projectFile; + } + } + + internal bool ShowProjectFinishedEvent + { + get + { + return _showProjectFinishedEvent; + } + + set + { + _showProjectFinishedEvent = value; + } + } + + internal bool ErrorInProject + { + get + { + return _errorInProject; + } + + set + { + _errorInProject = value; + } + } + + internal BuildEventContext ProjectBuildEventContext + { + get + { + return _buildEventContext; + } + } + #endregion + + #region Constructors + internal ProjectStartedEventMinimumFields(int projectKey, int entryPointKey, ProjectStartedEventArgs startedEvent, ProjectStartedEventMinimumFields parentProjectStartedEvent, bool requireTimeStamp) + { + _targetNames = startedEvent.TargetNames; + _projectFile = startedEvent.ProjectFile; + _showProjectFinishedEvent = false; + _errorInProject = false; + _projectId = startedEvent.ProjectId; + _buildEventContext = startedEvent.BuildEventContext; + _parentProjectStartedEvent = parentProjectStartedEvent; + _projectFullKey = new ProjectFullKey(projectKey, entryPointKey); + if (requireTimeStamp) + { + _timeStamp = startedEvent.Timestamp; + } + } + #endregion + } + + /// + /// This class stands in for a full target started event because it contains only the + /// minimum amount of inforomation needed for the logger + /// + internal class TargetStartedEventMinimumFields + { + #region Data + private DateTime _timeStamp; + private string _targetName; + private string _targetFile; + private string _projectFile; + private string _parentTarget; + private bool _showTargetFinishedEvent; + private bool _errorInTarget; + private string _message; + private BuildEventContext _buildEventContext; + #endregion + + #region Properties + internal DateTime TimeStamp + { + get + { + return _timeStamp; + } + } + + internal string TargetName + { + get + { + return _targetName; + } + } + + internal string TargetFile + { + get + { + return _targetFile; + } + } + + internal string ProjectFile + { + get + { + return _projectFile; + } + } + + internal string Message + { + get + { + return _message; + } + } + + internal bool ShowTargetFinishedEvent + { + get + { + return _showTargetFinishedEvent; + } + + set + { + _showTargetFinishedEvent = value; + } + } + + internal bool ErrorInTarget + { + get + { + return _errorInTarget; + } + + set + { + _errorInTarget = value; + } + } + internal BuildEventContext ProjectBuildEventContext + { + get + { + return _buildEventContext; + } + } + + internal string ParentTarget + { + get + { + return _parentTarget; + } + } + #endregion + + #region Constructors + internal TargetStartedEventMinimumFields(TargetStartedEventArgs startedEvent, bool requireTimeStamp) + { + _targetName = startedEvent.TargetName; + _targetFile = startedEvent.TargetFile; + _projectFile = startedEvent.ProjectFile; + this.ShowTargetFinishedEvent = false; + _errorInTarget = false; + _message = startedEvent.Message; + _buildEventContext = startedEvent.BuildEventContext; + if (requireTimeStamp) + { + _timeStamp = startedEvent.Timestamp; + } + _parentTarget = startedEvent.ParentTarget; + } + #endregion + } + + /// + /// This class is used as a key to group warnings and errors by the project entry point and the target they + /// error or warning was in + /// + internal class ErrorWarningSummaryDictionaryKey + { + #region Data + private BuildEventContext _entryPointContext; + private string _targetName; + private static ComparerContextNodeId s_eventComparer = new ComparerContextNodeId(); + #endregion + + #region Constructor + internal ErrorWarningSummaryDictionaryKey(BuildEventContext entryPoint, string targetName) + { + _entryPointContext = entryPoint; + _targetName = targetName == null ? string.Empty : targetName; + } + #endregion + + #region Properties + internal BuildEventContext EntryPointContext + { + get + { + return _entryPointContext; + } + } + + internal string TargetName + { + get + { + return _targetName; + } + } + + #endregion + + #region Equality + + public override bool Equals(object obj) + { + ErrorWarningSummaryDictionaryKey key = obj as ErrorWarningSummaryDictionaryKey; + if (key == null) + { + return false; + } + return s_eventComparer.Equals(_entryPointContext, key.EntryPointContext) && (String.Compare(_targetName, key.TargetName, StringComparison.OrdinalIgnoreCase) == 0); + } + + public override int GetHashCode() + { + return (_entryPointContext.GetHashCode() + _targetName.GetHashCode()); + } + #endregion + + } + + /// + /// Structure that holds both project and entrypoint keys + /// + internal class ProjectFullKey + { + #region Data + private int _projectKey; + private int _entryPointKey; + #endregion + + #region Properties + internal int ProjectKey + { + get { return _projectKey; } + set { _projectKey = value; } + } + + internal int EntryPointKey + { + get { return _entryPointKey; } + set { _entryPointKey = value; } + } + #endregion + + #region Constructor + internal ProjectFullKey(int projectKey, int entryPointKey) + { + _projectKey = projectKey; + _entryPointKey = entryPointKey; + } + #endregion + + #region ToString + /// + /// Output the projectKey or the projectKey and the entrypointKey depending on the verbosity level of the logger + /// + + public string ToString(LoggerVerbosity verbosity) + { + string fullProjectKey; + + if (verbosity > LoggerVerbosity.Normal) + { + fullProjectKey = this.ToString(); + } + else + { + fullProjectKey = String.Format(CultureInfo.InvariantCulture, "{0}", _projectKey); + } + + return fullProjectKey; + } + /// + /// The default of he ToString method should be to output the projectKey or the projectKey and the entrypointKey depending if a + /// entry point key exists or not + /// + /// + public override string ToString() + { + string fullProjectKey; + + if (_entryPointKey > 1) + { + fullProjectKey = String.Format(CultureInfo.InvariantCulture, "{0}:{1}", _projectKey, _entryPointKey); + } + else + { + fullProjectKey = String.Format(CultureInfo.InvariantCulture, "{0}", _projectKey); + } + + return fullProjectKey; + } + #endregion + + #region Equality + public override bool Equals(object obj) + { + ProjectFullKey compareKey = obj as ProjectFullKey; + if (compareKey != null) + { + return ((compareKey._projectKey == _projectKey) && (compareKey._entryPointKey == _entryPointKey)); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return (_projectKey + (_entryPointKey << 16)); + } + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/Logging/SerialConsoleLogger.cs b/src/XMakeBuildEngine/Logging/SerialConsoleLogger.cs new file mode 100644 index 00000000000..31d39813cc4 --- /dev/null +++ b/src/XMakeBuildEngine/Logging/SerialConsoleLogger.cs @@ -0,0 +1,970 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using System.IO; +using System.Globalization; +using System.Diagnostics; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using ColorSetter = Microsoft.Build.Logging.ColorSetter; +using ColorResetter = Microsoft.Build.Logging.ColorResetter; +using WriteHandler = Microsoft.Build.Logging.WriteHandler; + +namespace Microsoft.Build.BackEnd.Logging +{ + internal class SerialConsoleLogger : BaseConsoleLogger + { + #region Constructors + + /// + /// Default constructor. + /// + public SerialConsoleLogger() + : this(LoggerVerbosity.Normal) + { + // do nothing + } + + /// + /// Create a logger instance with a specific verbosity. This logs to + /// the default console. + /// + /// Verbosity level. + public SerialConsoleLogger(LoggerVerbosity verbosity) + : + this + ( + verbosity, + new WriteHandler(Console.Out.Write), + new ColorSetter(SetColor), + new ColorResetter(ResetColor) + ) + { + // do nothing + } + + /// + /// Initializes the logger, with alternate output handlers. + /// + /// + /// + /// + /// + public SerialConsoleLogger + ( + LoggerVerbosity verbosity, + WriteHandler write, + ColorSetter colorSet, + ColorResetter colorReset + ) + { + InitializeConsoleMethods(verbosity, write, colorSet, colorReset); + } + + #endregion + + #region Methods + + /// + /// Reset the states of per-build member variables + /// VSW#516376 + /// + internal override void ResetConsoleLoggerState() + { + if (ShowSummary) + { + errorList = new ArrayList(); + warningList = new ArrayList(); + } + else + { + errorList = null; + warningList = null; + } + + errorCount = 0; + warningCount = 0; + + projectPerformanceCounters = null; + targetPerformanceCounters = null; + taskPerformanceCounters = null; + } + + /// + /// Handler for build started events + /// + /// sender (should be null) + /// event arguments + public override void BuildStartedHandler(object sender, BuildStartedEventArgs e) + { + buildStarted = e.Timestamp; + + // if verbosity is detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + WriteLinePrettyFromResource("BuildStartedWithTime", e.Timestamp); + } + + WriteEnvironment(e.BuildEnvironment); + } + + /// + /// Handler for build finished events + /// + /// sender (should be null) + /// event arguments + public override void BuildFinishedHandler(object sender, BuildFinishedEventArgs e) + { + // Show the performance summary iff the verbosity is diagnostic or the user specifically asked for it + // with a logger parameter. + if (this.showPerfSummary) + { + ShowPerfSummary(); + } + + // if verbosity is normal, detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + if (e.Succeeded) + { + setColor(ConsoleColor.Green); + } + + // Write the "Build Finished" event. + WriteNewLine(); + WriteLinePretty(e.Message); + + resetColor(); + } + + // The decision whether or not to show a summary at this verbosity + // was made during initalization. We just do what we're told. + if (ShowSummary) + { + ShowErrorWarningSummary(); + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + // Emit text like: + // 1 Warning(s) + // 0 Error(s) + // Don't color the line if it's zero. (Per Whidbey behavior.) + if (warningCount > 0) + { + setColor(ConsoleColor.Yellow); + } + WriteLinePrettyFromResource(2, "WarningCount", warningCount); + resetColor(); + + if (errorCount > 0) + { + setColor(ConsoleColor.Red); + } + WriteLinePrettyFromResource(2, "ErrorCount", errorCount); + resetColor(); + } + } + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + string timeElapsed = LogFormatter.FormatTimeSpan(e.Timestamp - buildStarted); + + WriteNewLine(); + WriteLinePrettyFromResource("TimeElapsed", timeElapsed); + } + + ResetConsoleLoggerState(); + } + + /// + /// At the end of the build, repeats the errors and warnings that occurred + /// during the build, and displays the error count and warning count. + /// + private void ShowErrorWarningSummary() + { + if (warningCount == 0 && errorCount == 0) return; + + // Make some effort to distinguish the summary from the previous output + WriteNewLine(); + + if (warningCount > 0) + { + setColor(ConsoleColor.Yellow); + foreach (BuildWarningEventArgs warningEventArgs in warningList) + { + WriteLinePretty(EventArgsFormatting.FormatEventMessage(warningEventArgs, runningWithCharacterFileType, showProjectFile)); + } + } + + if (errorCount > 0) + { + setColor(ConsoleColor.Red); + foreach (BuildErrorEventArgs errorEventArgs in errorList) + { + WriteLinePretty(EventArgsFormatting.FormatEventMessage(errorEventArgs, runningWithCharacterFileType, showProjectFile)); + } + } + + resetColor(); + } + + /// + /// Handler for project started events + /// + /// sender (should be null) + /// event arguments + public override void ProjectStartedHandler(object sender, ProjectStartedEventArgs e) + { + if (!contextStack.IsEmpty()) + { + this.VerifyStack(contextStack.Peek().type == FrameType.Target, "Bad stack -- Top is project {0}", contextStack.Peek().ID); + } + + // if verbosity is normal, detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + ShowDeferredMessages(); + + // check for stack corruption + if (!contextStack.IsEmpty()) + { + this.VerifyStack(contextStack.Peek().type == FrameType.Target, "Bad stack -- Top is target {0}", contextStack.Peek().ID); + } + + contextStack.Push(new Frame(FrameType.Project, + false, // message not yet displayed + this.currentIndentLevel, + e.ProjectFile, + e.TargetNames, + null, + GetCurrentlyBuildingProjectFile())); + WriteProjectStarted(); + } + else + { + contextStack.Push(new Frame(FrameType.Project, + false, // message not yet displayed + this.currentIndentLevel, + e.ProjectFile, + e.TargetNames, + null, + GetCurrentlyBuildingProjectFile())); + } + + if (this.showPerfSummary) + { + PerformanceCounter counter = GetPerformanceCounter(e.ProjectFile, ref projectPerformanceCounters); + + // Place the counter "in scope" meaning the project is executing right now. + counter.InScope = true; + } + + if (Verbosity == LoggerVerbosity.Diagnostic && showItemAndPropertyList) + { + if (null != e.Properties) + { + ArrayList propertyList = ExtractPropertyList(e.Properties); + WriteProperties(propertyList); + } + + if (null != e.Items) + { + SortedList itemList = ExtractItemList(e.Items); + WriteItems(itemList); + } + } + } + + /// + /// Handler for project finished events + /// + /// sender (should be null) + /// event arguments + public override void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e) + { + if (this.showPerfSummary) + { + PerformanceCounter counter = GetPerformanceCounter(e.ProjectFile, ref projectPerformanceCounters); + + // Place the counter "in scope" meaning the project is done executing right now. + counter.InScope = false; + } + + // if verbosity is detailed or diagnostic, + // or there was an error or warning + if (contextStack.Peek().hasErrorsOrWarnings + || (IsVerbosityAtLeast(LoggerVerbosity.Detailed))) + { + setColor(ConsoleColor.Cyan); + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + WriteNewLine(); + } + + WriteLinePretty(e.Message); + + resetColor(); + } + + Frame top = contextStack.Pop(); + + this.VerifyStack(top.type == FrameType.Project, "Unexpected project frame {0}", top.ID); + this.VerifyStack(top.ID == e.ProjectFile, "Project frame {0} expected, but was {1}.", e.ProjectFile, top.ID); + } + + /// + /// Handler for target started events + /// + /// sender (should be null) + /// event arguments + public override void TargetStartedHandler(object sender, TargetStartedEventArgs e) + { + contextStack.Push(new Frame(FrameType.Target, + false, + this.currentIndentLevel, + e.TargetName, + null, + e.TargetFile, + GetCurrentlyBuildingProjectFile())); + + // if verbosity is detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + WriteTargetStarted(); + } + + if (this.showPerfSummary) + { + PerformanceCounter counter = GetPerformanceCounter(e.TargetName, ref targetPerformanceCounters); + + // Place the counter "in scope" meaning the target is executing right now. + counter.InScope = true; + } + + // Bump up the overall number of indents, so that anything within this target will show up + // indented. + this.currentIndentLevel++; + } + + /// + /// Handler for target finished events + /// + /// sender (should be null) + /// event arguments + public override void TargetFinishedHandler(object sender, TargetFinishedEventArgs e) + { + // Done with the target, so shift everything left again. + this.currentIndentLevel--; + + if (this.showPerfSummary) + { + PerformanceCounter counter = GetPerformanceCounter(e.TargetName, ref targetPerformanceCounters); + + // Place the counter "in scope" meaning the target is done executing right now. + counter.InScope = false; + } + + bool targetHasErrorsOrWarnings = contextStack.Peek().hasErrorsOrWarnings; + + // if verbosity is diagnostic, + // or there was an error or warning and verbosity is normal or detailed + if ((targetHasErrorsOrWarnings && (IsVerbosityAtLeast(LoggerVerbosity.Normal))) + || Verbosity == LoggerVerbosity.Diagnostic) + { + setColor(ConsoleColor.Cyan); + + if (showTargetOutputs) + { + IEnumerable targetOutputs = e.TargetOutputs; + + if (targetOutputs != null) + { + WriteLinePretty(ResourceUtilities.FormatResourceString("TargetOutputItemsHeader")); + foreach (ITaskItem item in targetOutputs) + { + WriteLinePretty(ResourceUtilities.FormatResourceString("TargetOutputItem", item.ItemSpec)); + } + } + } + + WriteLinePretty(e.Message); + resetColor(); + } + + Frame top = contextStack.Pop(); + this.VerifyStack(top.type == FrameType.Target, "bad stack frame type"); + this.VerifyStack(top.ID == e.TargetName, "bad stack frame id"); + + // set the value on the Project frame, for the ProjectFinished handler + if (targetHasErrorsOrWarnings) + { + SetErrorsOrWarningsOnCurrentFrame(); + } + } + + /// + /// Handler for task started events + /// + /// sender (should be null) + /// event arguments + public override void TaskStartedHandler(object sender, TaskStartedEventArgs e) + { + // if verbosity is detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + setColor(ConsoleColor.Cyan); + WriteLinePretty(e.Message); + resetColor(); + } + + if (this.showPerfSummary) + { + PerformanceCounter counter = GetPerformanceCounter(e.TaskName, ref taskPerformanceCounters); + + // Place the counter "in scope" meaning the task is executing right now. + counter.InScope = true; + } + + // Bump up the overall number of indents, so that anything within this task will show up + // indented. + this.currentIndentLevel++; + } + + /// + /// Handler for task finished events + /// + /// sender (should be null) + /// event arguments + public override void TaskFinishedHandler(object sender, TaskFinishedEventArgs e) + { + // Done with the task, so shift everything left again. + this.currentIndentLevel--; + + if (this.showPerfSummary) + { + PerformanceCounter counter = GetPerformanceCounter(e.TaskName, ref taskPerformanceCounters); + + // Place the counter "in scope" meaning the task is done executing. + counter.InScope = false; + } + + // if verbosity is detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + setColor(ConsoleColor.Cyan); + WriteLinePretty(e.Message); + resetColor(); + } + } + + /// + /// Prints an error event + /// + public override void ErrorHandler(object sender, BuildErrorEventArgs e) + { + errorCount++; + SetErrorsOrWarningsOnCurrentFrame(); + ShowDeferredMessages(); + setColor(ConsoleColor.Red); + WriteLinePretty(EventArgsFormatting.FormatEventMessage(e, runningWithCharacterFileType, showProjectFile)); + if (ShowSummary) + { + errorList.Add(e); + } + resetColor(); + } + + /// + /// Prints a warning event + /// + public override void WarningHandler(object sender, BuildWarningEventArgs e) + { + warningCount++; + SetErrorsOrWarningsOnCurrentFrame(); + ShowDeferredMessages(); + setColor(ConsoleColor.Yellow); + WriteLinePretty(EventArgsFormatting.FormatEventMessage(e, runningWithCharacterFileType, showProjectFile)); + if (ShowSummary) + { + warningList.Add(e); + } + resetColor(); + } + + /// + /// Prints a message event + /// + public override void MessageHandler(object sender, BuildMessageEventArgs e) + { + bool print = false; + bool lightenText = false; + switch (e.Importance) + { + case MessageImportance.High: + print = IsVerbosityAtLeast(LoggerVerbosity.Minimal); + break; + + case MessageImportance.Normal: + print = IsVerbosityAtLeast(LoggerVerbosity.Normal); + lightenText = true; + break; + + case MessageImportance.Low: + print = IsVerbosityAtLeast(LoggerVerbosity.Detailed); + lightenText = true; + break; + + default: + ErrorUtilities.VerifyThrow(false, "Impossible"); + break; + } + + if (print) + { + ShowDeferredMessages(); + + if (lightenText) + { + setColor(ConsoleColor.DarkGray); + } + + string nonNullMessage = null; + + // Include file information if present. + if (e.File != null) + { + nonNullMessage = EventArgsFormatting.FormatEventMessage(e, runningWithCharacterFileType, showProjectFile); + } + else + { + // null messages are ok -- treat as blank line + nonNullMessage = (e.Message == null) ? String.Empty : e.Message; + } + + WriteLinePretty(nonNullMessage); + + if (lightenText) + { + resetColor(); + } + } + } + + /// + /// Prints a custom event + /// + public override void CustomEventHandler(object sender, CustomBuildEventArgs e) + { + // if verbosity is detailed or diagnostic + if (IsVerbosityAtLeast(LoggerVerbosity.Detailed)) + { + // ignore custom events with null messages -- some other + // logger will handle them appropriately + if (e.Message != null) + { + ShowDeferredMessages(); + WriteLinePretty(e.Message); + } + } + } + + /// + /// Writes project started messages. + /// + internal void WriteProjectStarted() + { + this.VerifyStack(!contextStack.IsEmpty(), "Bad project stack"); + + //Pop the current project + Frame outerMost = contextStack.Pop(); + + this.VerifyStack(!outerMost.displayed, "Bad project stack on {0}", outerMost.ID); + this.VerifyStack(outerMost.type == FrameType.Project, "Bad project stack"); + + outerMost.displayed = true; + contextStack.Push(outerMost); + + WriteProjectStartedText(outerMost.ID, outerMost.targetNames, outerMost.parentProjectFile, + this.IsVerbosityAtLeast(LoggerVerbosity.Normal) ? outerMost.indentLevel : 0); + } + + /// + /// Displays the text for a project started message. + /// + /// current project file + /// previous project file + /// targets that are being invoked + /// indentation level + private void WriteProjectStartedText(string current, string targetNames, string previous, int indentLevel) + { + if (!SkipProjectStartedText) + { + setColor(ConsoleColor.Cyan); + + this.VerifyStack((current != null), "Unexpected null project stack"); + + WriteLinePretty(projectSeparatorLine); + + if (previous == null) + { + if ((targetNames == null) || (targetNames.Length == 0)) + { + WriteLinePrettyFromResource(indentLevel, "ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", current); + } + else + { + WriteLinePrettyFromResource(indentLevel, "ProjectStartedPrefixForTopLevelProjectWithTargetNames", current, targetNames); + } + } + else + { + if ((targetNames == null) || (targetNames.Length == 0)) + { + WriteLinePrettyFromResource(indentLevel, "ProjectStartedPrefixForNestedProjectWithDefaultTargets", previous, current); + } + else + { + WriteLinePrettyFromResource(indentLevel, "ProjectStartedPrefixForNestedProjectWithTargetNames", previous, current, targetNames); + } + } + + // add a little bit of extra space + WriteNewLine(); + + resetColor(); + } + } + + /// + /// Writes target started messages. + /// + private void WriteTargetStarted() + { + Frame f = contextStack.Pop(); + f.displayed = true; + contextStack.Push(f); + + setColor(ConsoleColor.Cyan); + + if (this.Verbosity == LoggerVerbosity.Diagnostic) + { + WriteLinePrettyFromResource(f.indentLevel, "TargetStartedFromFile", f.ID, f.file); + } + else + { + WriteLinePrettyFromResource(this.IsVerbosityAtLeast(LoggerVerbosity.Normal) ? f.indentLevel : 0, + "TargetStartedPrefix", f.ID); + } + + resetColor(); + } + + /// + /// Determines the currently building project file. + /// + /// name of project file currently being built + private string GetCurrentlyBuildingProjectFile() + { + if (contextStack.IsEmpty()) + { + return null; + } + + Frame topOfStack = contextStack.Peek(); + + // If the top of the stack is a TargetStarted event, then its parent project + // file is the one we want. + if (topOfStack.type == FrameType.Target) + { + return topOfStack.parentProjectFile; + } + // If the top of the stack is a ProjectStarted event, then its ID is the project + // file we want. + else if (topOfStack.type == FrameType.Project) + { + return topOfStack.ID; + } + else + { + ErrorUtilities.VerifyThrow(false, "Unexpected frame type."); + return null; + } + } + + /// + /// Displays project started and target started messages that + /// are shown only when the associated project or target produces + /// output. + /// + private void ShowDeferredMessages() + { + if (contextStack.IsEmpty()) + { + return; + } + + if (!contextStack.Peek().displayed) + { + Frame f = contextStack.Pop(); + + ShowDeferredMessages(); + + //push now, so that the stack is in a good state + //for WriteProjectStarted() and WriteLinePretty() + //because we use the stack to control indenting + contextStack.Push(f); + + switch (f.type) + { + case FrameType.Project: + WriteProjectStarted(); + break; + + case FrameType.Target: + // Only do things if we're at normal verbosity. If + // we're at a higher verbosity, we can assume that all + // targets have already be printed. If we're at lower + // verbosity we don't need to print at all. + ErrorUtilities.VerifyThrow(this.Verbosity < LoggerVerbosity.Detailed, + "This target should have already been printed at a higher verbosity."); + + if (IsVerbosityAtLeast(LoggerVerbosity.Normal)) + { + WriteTargetStarted(); + } + + break; + + default: + ErrorUtilities.VerifyThrow(false, "Unexpected frame type."); + break; + } + } + } + + /// + /// Marks the current frame to indicate that an error or warning + /// occurred during it. + /// + private void SetErrorsOrWarningsOnCurrentFrame() + { + // under unit test, there may not be frames on the stack + if (contextStack.Count == 0) + { + return; + } + + Frame frame = contextStack.Pop(); + frame.hasErrorsOrWarnings = true; + contextStack.Push(frame); + } + + /// + /// Checks the condition passed in. If it's false, it emits an error message to the console + /// indicating that there's a problem with the console logger. These "problems" should + /// never occur in the real world after we ship, unless there's a bug in the MSBuild + /// engine such that events aren't getting paired up properly. So the messages don't + /// really need to be localized here, since they're only for our own benefit, and have + /// zero value to a customer. + /// + /// + /// + /// + private void VerifyStack + ( + bool condition, + string unformattedMessage, + params object[] args + ) + { + if (!condition && !ignoreLoggerErrors) + { + string errorMessage = "INTERNAL CONSOLE LOGGER ERROR. " + ResourceUtilities.FormatString(unformattedMessage, args); + BuildErrorEventArgs errorEvent = new BuildErrorEventArgs(null, null, null, 0, 0, 0, 0, + errorMessage, null, null); + + ErrorUtilities.ThrowInternalError(errorMessage); + } + } + #endregion + + #region Supporting classes + + /// + /// This enumeration represents the kinds of context that can be + /// stored in the context stack. + /// + internal enum FrameType + { + Project, + Target + } + + /// + /// This struct represents context information about a single + /// target or project. + /// + internal struct Frame + { + /// + /// Creates a new instance of frame with all fields specified. + /// + /// the type of the this frame + /// display state. true indicates this frame has been displayed to the user + /// indentation level for this frame + /// frame id + /// targets to execute, in the case of a project frame + /// the file name where the target is defined + /// parent project file + internal Frame + ( + FrameType t, + bool d, + int indent, + string s, + string targets, + string fileOfTarget, + string parent + ) + { + type = t; + displayed = d; + indentLevel = indent; + ID = s; + targetNames = targets; + file = fileOfTarget; + hasErrorsOrWarnings = false; + parentProjectFile = parent; + } + + /// + /// Indicates if project or target frame. + /// + internal FrameType type; + + /// + /// Set to true to indicate the user has seen a message about this frame. + /// + internal bool displayed; + + /// + /// The number of tabstops to indent this event when it is eventually displayed. + /// + internal int indentLevel; + + /// + /// A string associated with this frame -- should be a target name + /// or a project file. + /// + internal string ID; + + /// + /// For a TargetStarted or a ProjectStarted event, this field tells us + /// the name of the *parent* project file that was responsible. + /// + internal string parentProjectFile; + + /// + /// Stores the TargetNames from the ProjectStarted event. Null for Target frames. + /// + internal string targetNames; + + /// + /// For TargetStarted events, this stores the filename where the Target is defined + /// (e.g., Microsoft.Common.targets). This is different than the project that is + /// being built. + /// For ProjectStarted events, this is null. + /// + internal string file; + + /// + /// True if there were errors/warnings during the project or target frame. + /// + internal bool hasErrorsOrWarnings; + } + + /// + /// The FrameStack class represents a (lifo) stack of Frames. + /// + internal class FrameStack + { + /// + /// The frames member is contained by FrameStack and does + /// all the heavy lifting for FrameStack. + /// + private System.Collections.Stack _frames; + + /// + /// Create a new, empty, FrameStack. + /// + internal FrameStack() + { + _frames = new System.Collections.Stack(); + } + + /// + /// Remove and return the top element in the stack. + /// + /// Thrown when stack is empty. + internal Frame Pop() + { + return (Frame)(_frames.Pop()); + } + + /// + /// Returns, but does not remove, the top of the stack. + /// + internal Frame Peek() + { + return (Frame)(_frames.Peek()); + } + + /// + /// Push(f) adds f to the top of the stack. + /// + /// a frame to push + internal void Push(Frame f) + { + _frames.Push(f); + } + + /// + /// Constant property that indicates the number of elements + /// in the stack. + /// + internal int Count + { + get + { + return _frames.Count; + } + } + + /// + /// s.IsEmpty() is true iff s.Count == 0 + /// + internal bool IsEmpty() + { + return (_frames.Count == 0); + } + } + #endregion + + #region Private member data + + /// + /// contextStack is the only interesting state in the console + /// logger. The context stack contains a sequence of frames + /// denoting current and previous containing projects and targets + /// + internal FrameStack contextStack = new FrameStack(); + #endregion + } +} diff --git a/src/XMakeBuildEngine/Microsoft.Build.csproj b/src/XMakeBuildEngine/Microsoft.Build.csproj new file mode 100644 index 00000000000..fab0d0ee9e4 --- /dev/null +++ b/src/XMakeBuildEngine/Microsoft.Build.csproj @@ -0,0 +1,664 @@ + + + + + Debug + AnyCPU + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58} + Library + Properties + Microsoft.Build + Microsoft.Build + true + + + + TRACE;DEBUG;STANDALONEBUILD;BUILD_ENGINE + + + TRACE;STANDALONEBUILD;BUILD_ENGINE + + + + true + + + SharedUtilities\AssemblyNameComparer.cs + + + SharedUtilities\AwaitExtensions + + + SharedUtilities\AssemblyNameReverseVersionComparer.cs + + + BackEnd\Components\RequestBuilder\IntrinsicTasks\CanonicalError.cs + True + + + BackEnd\Components\RequestBuilder\IntrinsicTasks\Constants.cs + True + + + Collections\HybridDictionary.cs + + + SharedUtilities\NGen.cs + + + SharedUtilities\Pair.cs + + + BackEnd\Components\RequestBuilder\IntrinsicTasks\PropertyParser.cs + True + + + Collections\ReadOnlyEmptyCollection.cs + + + True + + + + + + + + + + + + + + + + + + + + + + BackEnd\Components\RequestBuilder\IntrinsicTasks\TaskLoggingHelper.cs + True + + + BackEnd\Components\RequestBuilder\IntrinsicTasks\TaskLoggingHelperExtension.cs + True + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + true + + + + + + + + + + + + + + true + + + true + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + true + + + true + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Collections\CopyOnWriteDictionary.cs + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + true + + + true + + + true + + + true + + + SharedUtilities\AssemblyLoadInfo.cs + true + + + SharedUtilities\ReadOnlyEmptyDictionary.cs + + + SharedUtilities\ReadOnlyEmptyList.cs + + + SharedUtilities\AssemblyNameExtension.cs + true + + + SharedUtilities\BuildEventFileInfo.cs + true + + + SharedUtilities\ConversionUtilities.cs + true + + + SharedUtilities\FileDelegates.cs + true + + + Errors\ErrorUtilities.cs + true + + + SharedUtilities\EscapingUtilities.cs + true + + + SharedUtilities\VersionUtilities.cs + true + + + SharedUtilities\EventArgsFormatting.cs + true + + + SharedUtilities\ExceptionHandling.cs + + + SharedUtilities\FileMatcher.cs + true + + + SharedUtilities\FileUtilities.cs + true + + + + true + + + SharedUtilities\FileUtilitiesRegex.cs + true + + + SharedUtilities\FrameworkLocationHelper.cs + true + + + SharedUtilities\IElementLocation.cs + + + Errors\InternalErrorException.cs + + + SharedUtilities\LoadedType.cs + true + + + SharedUtilities\NativeMethodsShared.cs + true + + + InprocTrackingNativeMethods.cs + true + + + Errors\ProjectErrorUtilities.cs + true + + + Errors\ProjectFileErrorUtilities.cs + true + + + SharedUtilities\ProjectWriter.cs + true + + + SharedUtilities\ResourceUtilities.cs + true + + + + SharedUtilities\TypeLoader.cs + + + VisualStudioConstants.cs + + + Resources\XMakeAttributes.cs + true + + + Resources\XMakeElements.cs + true + + + SharedUtilities\XmlUtilities.cs + true + + + true + + + + + Designer + + + Resources\Strings.shared.resx + Designer + $(AssemblyName).Resources.Strings.shared.resources + + + + + + + + + + $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1\Facades\System.Runtime.dll + + + $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1\Facades\System.Threading.Tasks.dll + + + False + ..\..\packages\Microsoft.Tpl.Dataflow.4.5.24\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/Resources/AssemblyResources.cs b/src/XMakeBuildEngine/Resources/AssemblyResources.cs new file mode 100644 index 00000000000..a2e48833100 --- /dev/null +++ b/src/XMakeBuildEngine/Resources/AssemblyResources.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Resources; +using System.Reflection; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// This class provides access to the assembly's resources. + /// + internal static class AssemblyResources + { + /// + /// A slot for msbuild.exe to add a resource manager over its own resources, that can also be consulted. + /// + private static ResourceManager s_msbuildExeResourceManager; + + /// + /// The internals of the Engine are exposed to MSBuild.exe, so they must share the same AssemblyResources class and + /// ResourceUtilities class that uses it. To make this possible, MSBuild.exe registers its resources here and they are + /// normally consulted last. This assumes that there are no duplicated resource ID's between the Engine and MSBuild.exe. + /// (Actually there are currently two: LoggerCreationError and LoggerNotFoundError. + /// We can't change the resource ID's this late in the cycle and we sometimes want to load the MSBuild.exe ones, + /// because they're a little different. So for that purpose we call GetStringLookingInMSBuildExeResourcesFirst() ) + /// + internal static void RegisterMSBuildExeResources(ResourceManager manager) + { + ErrorUtilities.VerifyThrow(s_msbuildExeResourceManager == null, "Only one extra resource manager"); + + s_msbuildExeResourceManager = manager; + } + + /// + /// Loads the specified resource string, either from the assembly's primary resources, or its shared resources. + /// + /// This method is thread-safe. + /// + /// The resource string, or null if not found. + internal static string GetString(string name) + { + // NOTE: the ResourceManager.GetString() method is thread-safe + string resource = GetStringFromEngineResources(name); + + if (resource == null) + { + resource = GetStringFromMSBuildExeResources(name); + } + + return resource; + } + + /// + /// Loads the specified resource string. + /// + /// The resource string, or null if not found. + internal static string GetStringLookingInMSBuildExeResourcesFirst(string name) + { + string resource = GetStringFromMSBuildExeResources(name); + + if (resource == null) + { + resource = GetStringFromEngineResources(name); + } + + return resource; + } + + /// + /// Loads the specified resource string, from the Engine or else Shared resources. + /// + /// The resource string, or null if not found. + private static string GetStringFromEngineResources(string name) + { + string resource = s_resources.GetString(name, CultureInfo.CurrentUICulture); + + if (resource == null) + { + resource = s_sharedResources.GetString(name, CultureInfo.CurrentUICulture); + } + + ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); + + return resource; + } + + /// + /// Loads the specified resource string, from the MSBuild.exe resources. + /// + /// The resource string, or null if not found. + private static string GetStringFromMSBuildExeResources(string name) + { + string resource = null; + + if (s_msbuildExeResourceManager != null) + { + // Try MSBuild.exe's resources + resource = s_msbuildExeResourceManager.GetString(name, CultureInfo.CurrentUICulture); + } + + return resource; + } + + internal static ResourceManager PrimaryResources + { + get { return s_resources; } + } + + internal static ResourceManager SharedResources + { + get { return s_sharedResources; } + } + + // assembly resources + private static readonly ResourceManager s_resources = new ResourceManager("Microsoft.Build.Resources.Strings", Assembly.GetExecutingAssembly()); + // shared resources + private static readonly ResourceManager s_sharedResources = new ResourceManager("Microsoft.Build.Resources.Strings.shared", Assembly.GetExecutingAssembly()); + } +} diff --git a/src/XMakeBuildEngine/Resources/Constants.cs b/src/XMakeBuildEngine/Resources/Constants.cs new file mode 100644 index 00000000000..826f05b4e2b --- /dev/null +++ b/src/XMakeBuildEngine/Resources/Constants.cs @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Xml; +using System.Globalization; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Collections.Concurrent; + +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Internal +{ + /// + /// Contains a list of the special (reserved) properties that are settable by MSBuild code only. + /// + internal static class ReservedPropertyNames + { + // NOTE: if you add to this list, update the ReservedProperties hashtable below + internal const string projectDirectory = "MSBuildProjectDirectory"; + internal const string projectDirectoryNoRoot = "MSBuildProjectDirectoryNoRoot"; + internal const string projectFile = "MSBuildProjectFile"; + internal const string projectExtension = "MSBuildProjectExtension"; + internal const string projectFullPath = "MSBuildProjectFullPath"; + internal const string projectName = "MSBuildProjectName"; + internal const string thisFileDirectory = "MSBuildThisFileDirectory"; + internal const string thisFileDirectoryNoRoot = "MSBuildThisFileDirectoryNoRoot"; + internal const string thisFile = "MSBuildThisFile"; // "MSBuildThisFileFile" sounds silly! + internal const string thisFileExtension = "MSBuildThisFileExtension"; + internal const string thisFileFullPath = "MSBuildThisFileFullPath"; + internal const string thisFileName = "MSBuildThisFileName"; + internal const string binPath = "MSBuildBinPath"; + internal const string projectDefaultTargets = "MSBuildProjectDefaultTargets"; + internal const string extensionsPath = "MSBuildExtensionsPath"; + internal const string extensionsPath32 = "MSBuildExtensionsPath32"; + internal const string extensionsPath64 = "MSBuildExtensionsPath64"; + internal const string userExtensionsPath = "MSBuildUserExtensionsPath"; + internal const string toolsPath = MSBuildConstants.ToolsPath; + internal const string toolsVersion = "MSBuildToolsVersion"; + internal const string overrideTasksPath = "MSBuildOverrideTasksPath"; + internal const string defaultOverrideToolsVersion = "DefaultOverrideToolsVersion"; + internal const string startupDirectory = "MSBuildStartupDirectory"; + internal const string buildNodeCount = "MSBuildNodeCount"; + internal const string lastTaskResult = "MSBuildLastTaskResult"; + internal const string extensionsPathSuffix = "MSBuild"; + internal const string userExtensionsPathSuffix = "Microsoft\\MSBuild"; + internal const string programFiles32 = "MSBuildProgramFiles32"; + internal const string localAppData = "LocalAppData"; + internal const string assemblyVersion = "MSBuildAssemblyVersion"; + + /// + /// Lookup for reserved property names + /// + private static HashSet s_reservedProperties; + + /// + /// Lock object, since this is a shared table, and concurrent evaluation must be safe + /// + private static Object s_locker = new Object(); + + /// + /// Intentionally do not include MSBuildExtensionsPath* or MSBuildUserExtensionsPath in this list. We need tasks to be able to override those. + /// + private static HashSet ReservedProperties + { + get + { + lock (s_locker) + { + if (s_reservedProperties == null) + { + s_reservedProperties = new HashSet(MSBuildNameIgnoreCaseComparer.Default); + + s_reservedProperties.Add(projectDirectory); + s_reservedProperties.Add(projectDirectoryNoRoot); + s_reservedProperties.Add(projectFile); + s_reservedProperties.Add(projectExtension); + s_reservedProperties.Add(projectFullPath); + s_reservedProperties.Add(projectName); + + s_reservedProperties.Add(thisFileDirectory); + s_reservedProperties.Add(thisFileDirectoryNoRoot); + s_reservedProperties.Add(thisFile); + s_reservedProperties.Add(thisFileExtension); + s_reservedProperties.Add(thisFileFullPath); + s_reservedProperties.Add(thisFileName); + + s_reservedProperties.Add(binPath); + s_reservedProperties.Add(projectDefaultTargets); + s_reservedProperties.Add(toolsPath); + s_reservedProperties.Add(toolsVersion); + s_reservedProperties.Add(startupDirectory); + s_reservedProperties.Add(buildNodeCount); + s_reservedProperties.Add(lastTaskResult); + s_reservedProperties.Add(programFiles32); + s_reservedProperties.Add(assemblyVersion); + } + } + + return s_reservedProperties; + } + } + + /// + /// Indicates if the given property is a reserved property. + /// + /// true, if specified property is reserved + internal static bool IsReservedProperty(string property) + { + return ReservedProperties.Contains(property); + } + } + + /// + /// Constants used by the Engine + /// + internal static class Constants + { + /// + /// If no default tools version is specified in the config file or registry, we'll use 2.0. + /// The engine will use its binpath for the matching toolset path. + /// + internal const string defaultToolsVersion = "2.0"; + + /// + /// The toolsversion we will fall back to as a last resort if the default one cannot be found, this fallback should be the most current toolsversion known + /// + internal const string defaultFallbackToolsVersion = MSBuildConstants.CurrentToolsVersion; + + /// + /// The toolsversion we will use when we construct the solution wrapper metaprojects; this should be the most current toolsversion known + /// + internal const string defaultSolutionWrapperProjectToolsVersion = MSBuildConstants.CurrentToolsVersion; + + /// + /// Name of the property used to select which sub-toolset to use. + /// + internal const string SubToolsetVersionPropertyName = "VisualStudioVersion"; + + /// + /// Value we should be setting VisualStudioVersion as the ultimate fallback when Dev10 is installed. + /// + internal const string Dev10SubToolsetValue = "10.0"; + + /// + /// Number representing the current assembly's timestamp + /// + internal static long assemblyTimestamp; + + /// Current version of this MSBuild Engine assembly in the + /// form, e.g, "4.0" + /// + internal static string AssemblyVersion + { + get + { +#if STANDALONEBUILD + return MSBuildConstants.CurrentToolsVersion; +#else + Version version = new Version(ThisAssembly.Version); + + // "4.0.0.0" --> "4.0" + return version.Major + "." + version.Minor; +#endif + } + } + + + /// + /// Number representing the current assembly's timestamp + /// + internal static long AssemblyTimestamp + { + get + { + if (assemblyTimestamp == 0) + { + // Get the file version from the currently executing assembly. + // Use .CodeBase instead of .Location, because .Location doesn't + // work when Microsoft.Build.dll has been shadow-copied, for example + // in scenarios where NUnit is loading Microsoft.Build. + string path = FileUtilities.ExecutingAssemblyPath; + + assemblyTimestamp = new FileInfo(path).LastWriteTime.Ticks; + } + + return assemblyTimestamp; + } + } + + // Name of the environment variable that always points to 32-bit program files. + internal const string programFilesx86 = "ProgramFiles(x86)"; + } + + /// + /// The set of available static methods. + /// NOTE: Do not allow methods here that could do "bad" things under any circumstances. + /// These must be completely benign operations, as they run during project load, which must be safe in VS. + /// Key = Type or Type::Method, Value = AssemblyQualifiedTypeName (where null = mscorlib) + /// + /// + /// Placed here to avoid StyleCop error. + /// + internal static class AvailableStaticMethods + { + /// + /// Static methods that are allowed in constants. Key = Type or Type::Method, Value = AssemblyQualifiedTypeName (where null = mscorlib) + /// + private static ConcurrentDictionary> s_availableStaticMethods; + + /// + /// Locker to protect initialization + /// + private static Object s_locker = new Object(); + + /// + /// Whether a key is present + /// + internal static bool ContainsKey(string key) + { + InitializeAvailableMethods(); + + return s_availableStaticMethods.ContainsKey(key); + } + /// + /// Add an entry if not already present + /// + internal static bool TryAdd(string key, Tuple value) + { + InitializeAvailableMethods(); + + return s_availableStaticMethods.TryAdd(key, value); + } + + /// + /// Get an entry if present + /// + internal static bool TryGetValue(string key, out Tuple value) + { + InitializeAvailableMethods(); + + return s_availableStaticMethods.TryGetValue(key, out value); + } + + /// + /// Re-initialize. + /// Unit tests need this when they enable "unsafe" methods -- which will then go in the collection, + /// and mess up subsequent tests. + /// + internal static void Reset_ForUnitTestsOnly() + { + InitializeAvailableMethods(); + } + + /// + /// Fill up the dictionary for first use + /// + private static void InitializeAvailableMethods() + { + lock (s_locker) + { + if (s_availableStaticMethods == null) + { + s_availableStaticMethods = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + + // Pre declare our common type Tuples + Tuple environmentType = new Tuple(null, typeof(System.Environment)); + Tuple directoryType = new Tuple(null, typeof(System.IO.Directory)); + Tuple fileType = new Tuple(null, typeof(System.IO.File)); + + // Make specific static methods available (Assembly qualified type names are *NOT* supported, only null which means mscorlib): + s_availableStaticMethods.TryAdd("System.Environment::ExpandEnvironmentVariables", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::GetEnvironmentVariable", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::GetEnvironmentVariables", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::GetFolderPath", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::GetLogicalDrives", environmentType); + + // All the following properties only have getters + s_availableStaticMethods.TryAdd("System.Environment::CommandLine", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::Is64BitOperatingSystem", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::Is64BitProcess", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::MachineName", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::OSVersion", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::ProcessorCount", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::StackTrace", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::SystemDirectory", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::SystemPageSize", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::TickCount", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::UserDomainName", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::UserInteractive", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::UserName", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::Version", environmentType); + s_availableStaticMethods.TryAdd("System.Environment::WorkingSet", environmentType); + + s_availableStaticMethods.TryAdd("System.IO.Directory::GetDirectories", directoryType); + s_availableStaticMethods.TryAdd("System.IO.Directory::GetFiles", directoryType); + s_availableStaticMethods.TryAdd("System.IO.Directory::GetLastAccessTime", directoryType); + s_availableStaticMethods.TryAdd("System.IO.Directory::GetLastWriteTime", directoryType); + s_availableStaticMethods.TryAdd("System.IO.Directory::GetParent", directoryType); + s_availableStaticMethods.TryAdd("System.IO.File::Exists", fileType); + s_availableStaticMethods.TryAdd("System.IO.File::GetCreationTime", fileType); + s_availableStaticMethods.TryAdd("System.IO.File::GetAttributes", fileType); + s_availableStaticMethods.TryAdd("System.IO.File::GetLastAccessTime", fileType); + s_availableStaticMethods.TryAdd("System.IO.File::GetLastWriteTime", fileType); + s_availableStaticMethods.TryAdd("System.IO.File::ReadAllText", fileType); + + s_availableStaticMethods.TryAdd("System.Globalization.CultureInfo::GetCultureInfo", new Tuple(null, typeof(System.Globalization.CultureInfo))); // user request + s_availableStaticMethods.TryAdd("System.Globalization.CultureInfo::CurrentUICulture", new Tuple(null, typeof(System.Globalization.CultureInfo))); // user request + + // All static methods of the following are available (Assembly qualified type names are supported): + s_availableStaticMethods.TryAdd("MSBuild", new Tuple(null, typeof(Microsoft.Build.Evaluation.IntrinsicFunctions))); + s_availableStaticMethods.TryAdd("System.Byte", new Tuple(null, typeof(System.Byte))); + s_availableStaticMethods.TryAdd("System.Char", new Tuple(null, typeof(System.Char))); + s_availableStaticMethods.TryAdd("System.Convert", new Tuple(null, typeof(System.Convert))); + s_availableStaticMethods.TryAdd("System.DateTime", new Tuple(null, typeof(System.DateTime))); + s_availableStaticMethods.TryAdd("System.Decimal", new Tuple(null, typeof(System.Decimal))); + s_availableStaticMethods.TryAdd("System.Double", new Tuple(null, typeof(System.Double))); + s_availableStaticMethods.TryAdd("System.Enum", new Tuple(null, typeof(System.Enum))); + s_availableStaticMethods.TryAdd("System.Guid", new Tuple(null, typeof(System.Guid))); + s_availableStaticMethods.TryAdd("System.Int16", new Tuple(null, typeof(System.Int16))); + s_availableStaticMethods.TryAdd("System.Int32", new Tuple(null, typeof(System.Int32))); + s_availableStaticMethods.TryAdd("System.Int64", new Tuple(null, typeof(System.Int64))); + s_availableStaticMethods.TryAdd("System.IO.Path", new Tuple(null, typeof(System.IO.Path))); + s_availableStaticMethods.TryAdd("System.Math", new Tuple(null, typeof(System.Math))); + s_availableStaticMethods.TryAdd("System.UInt16", new Tuple(null, typeof(System.UInt16))); + s_availableStaticMethods.TryAdd("System.UInt32", new Tuple(null, typeof(System.UInt32))); + s_availableStaticMethods.TryAdd("System.UInt64", new Tuple(null, typeof(System.UInt64))); + s_availableStaticMethods.TryAdd("System.SByte", new Tuple(null, typeof(System.SByte))); + s_availableStaticMethods.TryAdd("System.Single", new Tuple(null, typeof(System.Single))); + s_availableStaticMethods.TryAdd("System.String", new Tuple(null, typeof(System.String))); + s_availableStaticMethods.TryAdd("System.StringComparer", new Tuple(null, typeof(System.StringComparer))); + s_availableStaticMethods.TryAdd("System.TimeSpan", new Tuple(null, typeof(System.TimeSpan))); + s_availableStaticMethods.TryAdd("System.Text.RegularExpressions.Regex", new Tuple(null, typeof(System.Text.RegularExpressions.Regex))); + s_availableStaticMethods.TryAdd("System.UriBuilder", new Tuple(null, typeof(System.UriBuilder))); + s_availableStaticMethods.TryAdd("System.Version", new Tuple(null, typeof(System.Version))); +#if (!STANDALONEBUILD) + availableStaticMethods.TryAdd("Microsoft.Build.Utilities.ToolLocationHelper", new Tuple("Microsoft.Build.Utilities.ToolLocationHelper, Microsoft.Build.Utilities.Core, Version=" + MSBuildConstants.CurrentAssemblyVersion + ", Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", null)); +#else + s_availableStaticMethods.TryAdd("Microsoft.Build.Utilities.ToolLocationHelper", new Tuple("Microsoft.Build.Utilities.ToolLocationHelper, Microsoft.Build.Utilities.Core, Version=" + MSBuildConstants.CurrentAssemblyVersion, null)); +#endif + } + } + } + } +} diff --git a/src/XMakeBuildEngine/Resources/Strings.resx b/src/XMakeBuildEngine/Resources/Strings.resx new file mode 100644 index 00000000000..590531cb264 --- /dev/null +++ b/src/XMakeBuildEngine/Resources/Strings.resx @@ -0,0 +1,1559 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + MSB4001: The "{0}" task has more than one parameter called "{1}". + {StrBegin="MSB4001: "}UE: This message is shown when a task has more than one .NET property with the same name -- it's unclear which of + those properties the task wants to use as a parameter in project files. + + + MSB4002: There was a failure retrieving the attributes for parameters in the "{0}" task. {1} + {StrBegin="MSB4002: "}UE: This message is shown when the .NET attributes that a task's .NET properties are decorated with, cannot be + retrieved -- this is typically because the .NET classes that define the .NET attributes cannot be loaded because the assembly + they are defined in cannot be found, or the classes themselves cannot be found. + + + MSB4003: "{0}" is a reserved attribute of the <{1}> element, and must be spelled with the correct casing. This attribute cannot be used as a parameter to the "{2}" task. + {StrBegin="MSB4003: "}UE: Tasks are not allowed to use incorrect case for reserved attributes on the task nodes e.g. "continueonerror" + instead of the "ContinueOnError". + + + The operation cannot be completed because a build is already in progress. + + + The operation cannot be completed because BeginBuild has not yet been called. + + + The operation cannot be completed because EndBuild has already been called but existing submissions have not yet completed. + + + The operation cannot be completed because the submission has already been executed. + + + Cannot dispose the build manager because it is not idle. + + + MSB4197: Build was canceled. {0} + {StrBegin="MSB4197: "} Error when the build stops suddenly for some reason. For example, because a child node died. + + + Build FAILED. + + + Build succeeded. + + + Build started {0}. + + + Building target "{0}" completely. + {0} is the name of the target. + + + No input files were specified. + + + Input file "{0}" is newer than output file "{1}". + {0} and {1} are filenames on disk. + + + Output file "{0}" does not exist. + {0} is a filename on disk. + + + Input file "{0}" does not exist. + {0} is a filename on disk. + + + Building target "{0}" partially, because some output files are out of date with respect to their input files. + {0} is the name of the target. + + + [{0}: Input={1}, Output={2}] Input file is newer than output file. + {0} is the name of an MSBuild item. {1} and {2} are filenames on disk. + + + [{0}: Input={1}, Output={2}] Output file does not exist. + {0} is the name of an MSBuild item. {1} and {2} are filenames on disk. + + + [{0}: Input={1}, Output={2}] Input file does not exist. + {0} is the name of an MSBuild item. {1} and {2} are filenames on disk. + + + The attribute "{0}" is a known MSBuild attribute, and cannot be accessed using this method. + + + MSB4023: Cannot evaluate the item metadata "%({0})". {1} + {StrBegin="MSB4023: "}UE: This message is shown when the value of an item metadata cannot be computed for some reason e.g. trying to apply + %(RootDir) to an item-spec that's not a valid path, would result in this error. + LOCALIZATION: "{1}" is a localized message explaining the problem. + + + MSB4193: MSBuild.exe could not be launched as a child node as it could not be found at the location "{0}". If necessary, specify the correct location in the BuildParameters, or with the MSBUILD_EXE_PATH environment variable. + {StrBegin="MSB4193: "} + + + MSB4218: Failed to successfully launch or connect to a child MSBuild.exe process. Verify that the MSBuild.exe "{0}" launches successfully, and that it is loading the same microsoft.build.dll that the launching process loaded. If the location seems incorrect, try specifying the correct location in the BuildParameters object, or with the MSBUILD_EXE_PATH environment variable. + {StrBegin="MSB4218: "} + + + MSB4117: The "{0}" item name is reserved, and cannot be used. + {StrBegin="MSB4117: "}UE: This message is shown when the user tries to redefine one of the reserved MSBuild items e.g. @(Choose) + + + MSB4118: The "{0}" item metadata name is reserved, and cannot be used. + {StrBegin="MSB4118: "}UE: This message is shown when the user tries to redefine one of the reserved MSBuild item metadata names e.g. %(FullPath). Only MSBuild can set those. + + + MSB4004: The "{0}" property is reserved, and cannot be modified. + {StrBegin="MSB4004: "}UE: This message is shown when the user tries to redefine one of the reserved MSBuild properties e.g. $(MSBuildProjectFile) + + + MSB4094: "{0}" is an invalid value for the "{1}" parameter of the "{3}" task. Multiple items cannot be passed into a parameter of type "{2}". + {StrBegin="MSB4094: "} + UE: This error is shown when a project tries to pass multiple items into a task parameter of type ITaskItem (singular). + + + + MSB4115: The "{0}" function only accepts a scalar value, but its argument "{1}" evaluates to "{2}" which is not a scalar value. + {StrBegin="MSB4115: "} + UE: This error is shown when a project tries to pass multiple items into a function in a conditional expression, that can only accept a scalar value (such as the "exists()" function). + + + + MSB4095: The item metadata %({0}) is being referenced without an item name. Specify the item name by using %(itemname.{0}). + {StrBegin="MSB4095: "} + + + MSB4162: <{0}> is not valid. Child elements are not allowed below a item remove element. + {StrBegin="MSB4162: "} + + + MSB4166: Child node "{0}" exited prematurely. Shutting down. Diagnostic information may be found in files in the temporary files directory named MSBuild_*.failure.txt. + {StrBegin="MSB4166: "} + + + MSB4085: A <Choose> must contain at least one <When>. + {StrBegin="MSB4085: "} + + + MSB4114: <Choose> elements cannot be nested more than {0} levels deep. + {StrBegin="MSB4114: "}UE: This message appears if the project file contains unreasonably nested Choose elements. + LOCALIZATION: Do not localize "Choose" as it is an XML element name. + + + MSB4006: There is a circular dependency in the target dependency graph involving target "{0}". + {StrBegin="MSB4006: "}UE: This message is shown when the build engine detects a target referenced in a circular manner -- a project cannot + request a target to build itself (perhaps via a chain of other targets). + + + MSB4086: A numeric comparison was attempted on "{1}" that evaluates to "{2}" instead of a number, in condition "{0}". + {StrBegin="MSB4086: "} + + + MSB4130: The condition "{0}" may have been evaluated incorrectly in an earlier version of MSBuild. Please verify that the order of the AND and OR clauses is written as intended. To avoid this warning, add parentheses to make the evaluation order explicit. + {StrBegin="MSB4130: "} + + + MSB4087: Specified condition "{0}" does not evaluate to a boolean. + {StrBegin="MSB4087: "} + + + MSB4113: Specified condition "{0}" evaluates to "{1}" instead of a boolean. + {StrBegin="MSB4113: "} + + + MSB4136: Error reading the toolset information from the configuration file "{0}". {1} + {StrBegin="MSB4136: "} + + + MSB4142: MSBuildToolsPath is not the same as MSBuildBinPath for the ToolsVersion "{0}" defined at "{1}". If both are present they must have the same value. + {StrBegin="MSB4142: "} + + + MSB4097: The element <{0}> beneath element <{1}> may not have a custom XML namespace. + {StrBegin="MSB4097: "} + + + MSB4009: The default tasks file could not be successfully loaded. {0} + {StrBegin="MSB4009: "}UE: This message is shown when one of the default tasks file (*.tasks) located alongside the MSBuild binaries cannot + be opened/parsed. "{0}" contains a message explaining why. The filename itself is not part of the message but is provided + separately to loggers. + LOCALIZATION: "{0}" is a message from some FX method and is already localized. + + + MSB4010: The "{0}" files could not be successfully loaded from their expected location "{1}". Default tasks will not be available. {2} + {StrBegin="MSB4010: "}UE: This message is shown when the default tasks files that are located alongside the MSBuild binaries cannot be + found, either because they don't exist, or because of lack of permissions. "{2}" contains a message explaining why. + LOCALIZATION: "{2}" is a message from some FX method and is already localized. + + + Importing the file "{0}" into the file "{1}" results in a circular dependency. + + {0} is a file imported into the file "{1}" such that it results in a circular dependency. For e.g. if t1.targets imports + t2.targets and t2.targets tries to import t1.targets, then it results in a circular dependency. + + + + MSB4194: The override tasks file could not be successfully loaded. {0} + + {StrBegin="MSB4194: "}UE: This message is shown when one of the override tasks file (*.overridetasks) located alongside the MSBuild binaries cannot + be opened/parsed. "{0}" contains a message explaining why. The filename itself is not part of the message but is provided + separately to loggers. + LOCALIZATION: "{0}" is a message from some FX method and is already localized. + + + + The override tasks path "{0}" must not be a relative path and must exist on disk. Default tasks will not be overridden. + + UE: This message is shown when the override tasks path in the registry or passed to the toolset is not a full path. + + + + A problem occurred loading the override tasks path "{0}". {1} + + UE: This message is shown when the override tasks path in the registry or passed to the toolset is not a full path. + + + + MSB4196: The "{0}" files could not be successfully loaded from their expected location "{1}". Default tasks will not be overridden. {2} + + {StrBegin="MSB4196: "}UE: This message is shown when the override tasks files that are located alongside the MSBuild binaries cannot be + found, either because they don't exist, or because of lack of permissions. "{2}" contains a message explaining why. + LOCALIZATION: "{2}" is a message from some FX method and is already localized. + + + + MSB4195: There was an error gathering properties for tasks file evaluation. {0} + + {StrBegin="MSB4195: "}UE: This message is shown when the gathering of properties for the evaluation of override and defaults tasks has an exception. "{0"} will be the exception message + + + MSB4133: A default tools version "{0}" was specified, but its definition could not be found. + {StrBegin="MSB4133: "} + + + MSB4011: "{0}" cannot be imported again. It was already imported at "{1}". This is most likely a build authoring error. This subsequent import will be ignored. {2} + {StrBegin="MSB4011: "} + + + MSB4211: The property "{0}" is being set to a value for the first time, but it was already consumed at "{1}". + {StrBegin="MSB4211: "} + + + MSB4210: "{0}" is attempting to import itself, directly or indirectly. This is most likely a build authoring error. The import will be ignored. + {StrBegin="MSB4210: "} + + + MSB4079: The <ProjectExtensions> element occurs more than once. + {StrBegin="MSB4079: "} + + + MSB4012: The expression "{0}" cannot be used in this context. Item lists cannot be concatenated with other strings where an item list is expected. Use a semicolon to separate multiple item lists. + {StrBegin="MSB4012: "}UE: This message is shown when the user does not properly specify an item list when an item list is expected + e.g. "badprefix@(foo)badsuffix" instead of "prefix; @(foo); suffix" + + + end of input + This is the name of the "EndOfInput" token. It is displayed in quotes as the + unexpected char or token when the end of a conditional was unexpectedly reached. + + + The previous error was converted to a warning because the task was called with ContinueOnError=true. + + + {0} Error(s) + + + MSB4159: Error creating the toolset "{0}". {1} + {StrBegin="MSB4159: "} + + + MSB4146: Cannot evaluate the property expression "{0}" found at "{1}". {2} + {StrBegin="MSB4146: "} + + + The <{0}> tag is no longer supported as a child of the <Project> element. Place this tag within a target, and add the name of the target to the "InitialTargets" attribute of the <Project> element. + + + Launching task "{0}" from assembly "{1}" in an external task host with a runtime of "{2}" and a process architecture of "{3}". + + + MSB4100: Expected "{0}" to evaluate to a boolean instead of "{1}", in condition "{2}". + {StrBegin="MSB4100: "} + + + MSB4028: The "{0}" task's outputs could not be retrieved from the "{1}" parameter. {2} + {StrBegin="MSB4028: "} + + + MSB4014: The build stopped unexpectedly because of an internal failure. + {StrBegin="MSB4014: "}UE: This message is shown when an unhandled exception terminates the build. The cause is most likely a programming + error in the build engine. + + + MSB4015: The build stopped unexpectedly because the "{0}" logger failed unexpectedly during shutdown. + {StrBegin="MSB4015: "}UE: This message is used for a special exception that is thrown when a logger fails while shutting down (most likely + because of a programming error in the logger). When a logger dies, we cannot proceed with the build, and we throw a special + exception to abort the build. + + + MSB4016: The build stopped unexpectedly because the "{0}" logger failed unexpectedly during initialization. + {StrBegin="MSB4016: "}UE: This message is used for a special exception that is thrown when a logger fails while initializing itself (most + likely because of a programming error in the logger). When a logger dies, we cannot proceed with the build, and we throw a + special exception to abort the build. + + + MSB4017: The build stopped unexpectedly because of an unexpected logger failure. + {StrBegin="MSB4017: "}UE: This message is used for a special exception that is thrown when a logger fails while logging an event (most + likely because of a programming error in the logger). When a logger dies, we cannot proceed with the build, and we throw a + special exception to abort the build. + + + MSB4018: The "{0}" task failed unexpectedly. + {StrBegin="MSB4018: "}UE: This message is shown when a task terminates because of an unhandled exception. The cause is most likely a + programming error in the task; however, it is also possible that the unhandled exception originated in the engine, and was + surfaced through the task when the task called into the engine. + + + MSB4187: Failed to receive a response from the task thread in the timeout period "{0}" ms. Shutting down. + {StrBegin="MSB4187: "} + + + MSB4088: Condition "{0}" is improperly constructed. + {StrBegin="MSB4088: "} + + + MSB4105: Found an unexpected character '{2}' at position {1} in condition "{0}". Did you intend to use "=="? + {StrBegin="MSB4105: "} + + + MSB4106: Expected an item list at position {1} in condition "{0}". Did you forget the closing parenthesis? + {StrBegin="MSB4106: "} + + + MSB4107: Expected an item list at position {1} in condition "{0}". Did you forget the opening parenthesis after the '@'? To use a literal '@', use '%40' instead. + {StrBegin="MSB4107: "} + + + MSB4108: Expected an item list at position {1} in condition "{0}". Did you forget to close a quote inside the item list expression? + {StrBegin="MSB4108: "} + + + MSB4109: Expected a property at position {1} in condition "{0}". Did you forget the closing parenthesis? + {StrBegin="MSB4109: "} + + + MSB4110: Expected a property at position {1} in condition "{0}". Did you forget the opening parenthesis after the '$'? To use a literal '$', use '%24' instead. + {StrBegin="MSB4110: "} + + + MSB4101: Expected a closing quote after position {1} in condition "{0}". + {StrBegin="MSB4101: "} + + + MSB4019: The imported project "{0}" was not found. Confirm that the path in the <Import> declaration is correct, and that the file exists on disk. + {StrBegin="MSB4019: "}LOCALIZATION: <Import> should not be localized. + + + MSB4089: Incorrect number of arguments to function in condition "{0}". Found {1} argument(s) when expecting {2}. + {StrBegin="MSB4089: "} + + + MSB4020: The value "{0}" of the "{1}" attribute in element <{2}> is invalid. + {StrBegin="MSB4020: "}UE: This is a generic message that is displayed when we find a project element with an incorrect value for one of its + attributes e.g. <Import Project=""> -- the value of Project should not be an empty string. + + + MSB4102: The value "{0}" of the "{1}" attribute in element <{2}> is invalid. {3} + {StrBegin="MSB4102: "}UE: This is a generic message that is displayed when we find a project element with an incorrect value for one of its + attributes. At the end of the message we show the exception text we got trying to use the value. + + + MSB4021: The "ContinueOnError" attribute of the "{0}" task is not valid. {1} + {StrBegin="MSB4021: "}LOCALIZATION: "ContinueOnError" should not be localized. "{1}" is a message from another exception explaining the problem. + + + MSB4022: The result "{0}" of evaluating the value "{1}" of the "{2}" attribute in element <{3}> is not valid. + {StrBegin="MSB4022: "}UE: This message is shown when the engine is checking the correctness of the value (after evaluating embedded + properties/items) assigned to an XML attribute of an XML element in the project file. + + + MSB4104: Failed to write to log file "{0}". {1} + {StrBegin="MSB4104: "}UE: This is shown when the File Logger can't create or write to the file it was instructed to log to. + + + MSB4024: The imported project file "{0}" could not be loaded. {1} + {StrBegin="MSB4024: "}UE: This message is shown when an imported project file cannot be loaded because of incorrect XML. The project + filename is not part of the message because it is provided separately to loggers. + LOCALIZATION: {0} is a localized message from the CLR/FX explaining why the project is invalid. + + + MSB4147: The property "{0}" at "{1}" is invalid. {2} + {StrBegin="MSB4147: "} + + + MSB4177: Invalid property. {0} + {StrBegin="MSB4177: "} + UE: {0} is a localized message indicating what the problem was. + + + MSB4143: The registry expression "{0}" cannot be evaluated. {1} + {StrBegin="MSB4143: "} + UE: This message is shown when the user attempts to provide an expression like "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)" + LOCALIZATION: "{0}" is the expression that was bad. "{1}" is a message from an FX exception that describes why the expression is bad. + + + + MSB4184: The expression "{0}" cannot be evaluated. {1} + {StrBegin="MSB4184: "} + Double quotes as the expression will typically have single quotes in it. + UE: This message is shown when the user attempts to provide an expression like "$(SomeProperty.ToLower())" or "@(Foo->Bar())" + LOCALIZATION: "{0}" is the expression that was bad. "{1}" is a message from an FX exception that describes why the expression is bad. + + + + The quotes were mismatched. + This is a potential suffix to "InvalidFunctionPropertyExpression" so it has no error code. + + + The parentheses were mismatched. + This is a potential suffix to "InvalidFunctionPropertyExpression" so it has no error code. + + + The square brackets were mismatched. + This is a potential suffix to "InvalidFunctionPropertyExpression" so it has no error code. + + + MSB4185: The function "{0}" on type "{1}" is not available for execution as an MSBuild property function. + + {StrBegin="MSB4185: "} + UE: This message is shown when the user attempts to provide an expression like "$([System.DateTime]::Now)", but the expression has not been enabled + LOCALIZATION: "{0}" is the static function name, "{1}" is the .NET Framework type name + + + + MSB4212: Invalid static method invocation syntax: "{0}". The type "{1}" is either not available for execution in an MSBuild property function or could not be found. + + {StrBegin="MSB4212: "} + UE: This message is shown when the user attempts to provide an expression like "$([System.DateTime]::Now)", but the expression has not been enabled + LOCALIZATION: "{0}" is the function expression which is in error. "{1}" is the .NET Framework type name + + + + MSB4186: Invalid static method invocation syntax: "{0}". {1} Static method invocation should be of the form: $([FullTypeName]::Method()), e.g. $([System.IO.Path]::Combine(`a`, `b`)). + {StrBegin="MSB4186: "} + UE: This message is shown when the user attempts to call a static method on a type, but has used the incorrect syntax + LOCALIZATION: "{0}" is the function expression which is in error. "{1}" is a message from an FX exception that describes why the expression is bad. + + + + MSB4198: The expression "{0}" cannot be evaluated on item "{1}". {2} + + {StrBegin="MSB4198: "} + Double quotes as the expression will typically have single quotes in it. + UE: This message is shown when the user attempts to provide an expression like "@(SomeItem->DirectoryName())" + LOCALIZATION: "{0}" is the expression that was bad, "{1}" is the item or file that was being worked on. "{2}" is a message from an FX exception that describes why the expression is bad. + + + + MSB4199: Invalid transformation syntax "{0}". An item function was not found with that name and {1} parameters. + + {StrBegin="MSB4199: "} + UE: This message is shown when the user attempts to call a transformation on an item, but has used the incorrect syntax + LOCALIZATION: "{0}" is the function which is in error + + + + MSB4200: Unknown item transformation function "{0}". + + {StrBegin="MSB4200: "} + UE: This message is shown when the user attempts to provide an expression like @(Item->SomeTransform()), but SomeTransform is unknown + LOCALIZATION: "{0}" is the function name + + + + MSB4026: The "{0}={1}" parameter for the "{2}" task is invalid. + {StrBegin="MSB4026: "}UE: This message is displayed when a task has an invalid parameter that cannot be initialized. + + + MSB4027: The "{0}" task generated invalid items from the "{1}" output parameter. {2} + {StrBegin="MSB4027: "} + + + MSB4029: The "{0}" task has an invalid output specification. The "TaskParameter" attribute is required, and either the "ItemName" or "PropertyName" attribute must be specified (but not both). + {StrBegin="MSB4029: "}LOCALIZATION: "TaskParameter", "ItemName" and "PropertyName" should not be localized. + + + MSB4030: "{0}" is an invalid value for the "{1}" parameter of the "{3}" task. The "{1}" parameter is of type "{2}". + {StrBegin="MSB4030: "}UE: This error is shown when a type mis-match occurs between the value assigned to task parameter in the project file + and the type of the .NET property that corresponds to the task parameter. For example, if an int task parameter called "Count" + is assigned the value "x", this error would be displayed: <MyTask Count="x" /> + + + MSB4137: Invalid value specified in the configuration file at "{0}". Property name or tools version name is an empty string. + {StrBegin="MSB4137: "} + + + MSB4163: <ItemDefinitionGroup> is not allowed inside a target. + {StrBegin="MSB4163: "} + + + MSB4096: The item "{0}" in item list "{1}" does not define a value for metadata "{2}". In order to use this metadata, either qualify it by specifying %({1}.{2}), or ensure that all items in this list define a value for this metadata. + {StrBegin="MSB4096: "} + + + MSB4099: A reference to an item list at position {1} is not allowed in this condition "{0}". + {StrBegin="MSB4099: "} + + + MSB4191: The reference to custom metadata "{2}" at position {1} is not allowed in this condition "{0}". + {StrBegin="MSB4191: "} + + + MSB4190: The reference to the built-in metadata "{2}" at position {1} is not allowed in this condition "{0}". + {StrBegin="MSB4190: "} + + + MSB4033: "{0}" is a reserved item metadata, and cannot be redefined as a custom metadata on the item. + {StrBegin="MSB4033: "} + + + An InternalLoggerException can only be thrown by the MSBuild engine. The public constructors of this class cannot be used to create an instance of the exception. + UE: This message is shown when a user tries to instantiate a special exception called InternalLoggerException through the OM -- + only the engine is allowed to create and throw this exception. + LOCALIZATION: "InternalLoggerException" and "MSBuild" should not be localized. + + + Initial Items: + + + Environment at start of build: + + + MSB4164: The value "{0}" of metadata "{1}" contains an item list expression. Item list expressions are not allowed on default metadata values. + {StrBegin="MSB4164: "} + + + MSB4035: The required attribute "{0}" is empty or missing from the element <{1}>. + {StrBegin="MSB4035: "}UE: This message is shown when a user leaves off a required attribute from a project element + e.g. <UsingTask AssemblyName="foo"> -- this is missing the "TaskName" attribute. + + + MSB4036: The "{0}" task was not found. Check the following: 1.) The name of the task in the project file is the same as the name of the task class. 2.) The task class is "public" and implements the Microsoft.Build.Framework.ITask interface. 3.) The task is correctly declared with <UsingTask> in the project file, or in the *.tasks files located in the "{1}" directory. + {StrBegin="MSB4036: "}LOCALIZATION: <UsingTask> and "*.tasks" should not be localized. + + + MSB4141: MSBuildToolsPath is not specified for the ToolsVersion "{0}" defined at "{1}", or the value specified evaluates to the empty string. + {StrBegin="MSB4141: "} + + + MSB4222: ToolsVersion "{0}", defined at "{1}", contains sub-toolset "{2}" which sets MSBuildBinPath or MSBuildToolsPath. This is not supported in sub-toolsets. + + + MSB4144: Multiple definitions were found for the toolset "{0}". + {StrBegin="MSB4144: "} + + + MSB4145: Multiple definitions were found for the property "{0}". + {StrBegin="MSB4145: "} + + + MSB4082: Choose has more than one <Otherwise> element. + {StrBegin="MSB4082: "} + + + MSB4038: The element <{0}> must be last under element <{1}>. Found element <{2}> instead. + {StrBegin="MSB4038: "} + + + MSB4138: Non-string data was specified at the registry location "{0}". + {StrBegin="MSB4138: "} + + + MSB4039: No "{0}" element was found in the project file. + {StrBegin="MSB4039: "} + + + MSB4040: There is no target in the project. + {StrBegin="MSB4040: "} + + + A null entry was found in the collection of loggers. + + + MaxNodeCount may only be assigned a value greater than zero. + + + Overriding target "{0}" in project "{1}" with target "{2}" from project "{3}". + + + {0} ms {1} {2} calls + + + (* = timing was not recorded because of reentrancy) + + + The project file "{0}" was not found. + UE: This message is shown when the user calls into the OM to build a project that doesn't exist on disk. + + + Done building project "{0}" -- FAILED. + + + Done building project "{0}". + + + Done Building Project "{0}" ({1} target(s)). + + + Done Building Project "{0}" (default targets). + + + Done Building Project "{0}" ({1} target(s)) -- FAILED. + + + Done Building Project "{0}" (default targets) -- FAILED. + + + MSB4041: The default XML namespace of the project must be the MSBuild XML namespace. If the project is authored in the MSBuild 2003 format, please add xmlns="{0}" to the <Project> element. If the project has been authored in the old 1.0 or 1.2 format, please convert it to MSBuild 2003 format. + {StrBegin="MSB4041: "}UE: This is a Beta 1 message only. + LOCALIZATION: <Project>, "MSBuild" and "xmlns" should not be localized. + + + Project Performance Summary: + + + Project "{0}" is building "{1}" ({2} target(s)): + + + Project "{0}" is building "{1}" (default targets): + + + Project "{0}" ({1} target(s)): + + + Project "{0}" (default targets): + + + Task name cannot be empty. + + + MSB4075: The project file "{0}" must be opened in the Visual Studio IDE and converted to the latest version before it can be built by MSBuild. + {StrBegin="MSB4075: "} + + + MSB4192: The project file "{0}" is in the ".vcproj" file format, which MSBuild no longer supports. Please convert the project by opening it in the Visual Studio IDE or running the conversion tool, or use MSBuild 3.5 or earlier to build it. + {StrBegin="MSB4192: "} LOC: ".vcproj" should not be localized + + + Initial Properties: + + + MSB4148: The name of a property stored under the registry key "{0}" has zero length. + {StrBegin="MSB4148: "} + + + MSB4043: The item metadata reference "{0}" is invalid because it is qualified with an item name. Item metadata referenced in transforms do not need to be qualified, because the item name is automatically deduced from the items being transformed. Change "{0}" to "%({1})". + {StrBegin="MSB4043: "}UE: This message is shown when the user does something like this: @(foo->'%(foo.metadata)'). There is no need to specify + "foo.metadata", because "foo" is automatically deduced. In corollary, "bar.metadata" is not allowed either, where "bar" is a different + item list type. + + + MSB4135: Error reading the toolset information from the registry location "{0}". {1} + {StrBegin="MSB4135: "} + + + MSB4044: The "{0}" task was not given a value for the required parameter "{1}". + {StrBegin="MSB4044: "}UE: This message is shown when a task parameter designated as "required" is not set in the project file. + + + MSB4112: The targets in this project have been disabled by the host and therefore cannot be built at this time. This may have been done for security reasons. To enable the targets, the host must set Project.BuildEnabled to "true". + {StrBegin="MSB4112: "} + + + MSB4093: The "{0}" parameter of the "{1}" task cannot be written to because it does not have a "set" accessor. + {StrBegin="MSB4093: "}UE: This error is shown when a project tries to assign a value to a task parameter that does not have a "set" + accessor on the corresponding .NET property on the task class. + + + Skipping target "{0}" because it has no inputs. + + + Though the target has declared its inputs, the input specification only references empty properties and/or empty item lists. + + + Skipping target "{0}" because it has no outputs. + + + Though the target has declared its outputs, the output specification only references empty properties and/or empty item lists. + + + Skipping target "{0}" because all output files are up-to-date with respect to the input files. + + + Input files: {0} + {0} is a semicolon-separated list of filenames. + + + Output files: {0} + {0} is a semicolon-separated list of filenames. + + + {0}: Defaulting .NET Framework v{1} to the .NET Framework v4.0 version of aspnet_compiler.exe. To change the version of the tool used, please set the "ToolPath" parameter with the correct path to the tool. + + + MSB4203: {0}: Invalid TargetFrameworkMoniker {1}. The AspNetCompiler task only supports targeting the .NET Framework. + {StrBegin="MSB4203: "} + + + MSB4205: The website project in this solution is targeting the v2.0 runtime, but it is not installed. + {StrBegin="MSB4205: "} + + + MSB4204: {0}: Invalid TargetFrameworkMoniker {1}. {2}. + {StrBegin="MSB4204: "} + + + Using the MSBuild v3.5 solution wrapper generator because the tools version was set to {0}. + + + Using the MSBuild v3.5 solution wrapper generator with a tools version of {0} because the solution file format was version {1} and no tools version was supplied. + + + Building solution configuration "{0}". + UE: This is not an error, so doesn't need an error code. + + + MSB4160: A circular dependency involving project "{0}" has been detected. + {StrBegin="MSB4160: "} + + + MSB4126: The specified solution configuration "{0}" is invalid. Please specify a valid solution configuration using the Configuration and Platform properties (e.g. MSBuild.exe Solution.sln /p:Configuration=Debug /p:Platform="Any CPU") or leave those properties blank to use the default solution configuration. + {StrBegin="MSB4126: "}UE: The solution filename is provided separately to loggers. + + + MSB4046: Error reading project file "{0}": {1} + {StrBegin="MSB4046: "} + + + MSB4125: The project file name "{0}" is invalid. {1} + {StrBegin="MSB4125: "}UE: The solution filename is provided separately to loggers. + + + MSB4051: Project {0} is referencing a project with GUID {1}, but a project with this GUID was not found in the .SLN file. + {StrBegin="MSB4051: "}UE: The solution filename is provided separately to loggers. + + + MSB4078: The project file "{0}" is not supported by MSBuild and cannot be built. + {StrBegin="MSB4078: "} + + + MSB4054: The solution file must be opened in the Visual Studio IDE and converted to the latest version before it can be built by MSBuild. + {StrBegin="MSB4054: "}UE: The solution filename is provided separately to loggers. + + + MSB4121: The project configuration for project "{0}" was not specified in the solution file for the solution configuration "{1}". + {StrBegin="MSB4121: "} + + + The project "{0}" is not selected for building in solution configuration "{1}". + + UE: This is not an error, so doesn't need an error code. + + + + MSB4122: Scanning project dependencies for project "{0}" failed. {1} + {StrBegin="MSB4122: "} + + + MSB4149: The tools version "{0}" of the solution does not support building projects with a different tools version. + {StrBegin="MSB4149: "} + + + Web projects do not support the "Clean" target. Continuing with remaining projects ... + UE: This is not an error, so doesn't need an error code. + LOCALIZATION: Do not localize "Clean". + + + Web projects do not support the "Publish" target. Continuing with remaining projects ... + UE: This is not an error, so doesn't need an error code. + LOCALIZATION: Do not localize "Publish". + + + Skipping because the "$(AspNetConfiguration)" configuration is not supported for this web project. You can use the AspNetConfiguration property to override the configuration used for building web projects, by adding /p:AspNetConfiguration=<value> to the command line. Currently web projects only support Debug and Release configurations. + + UE: This is not an error, so doesn't need an error code. + LOCALIZATION: Do NOT localize "AspNetConfiguration", "Debug", "Release". + + + + Target "{0}" skipped. Previously built unsuccessfully. + + + Target "{0}" skipped. Previously built successfully. + + + MSB4116: The condition "{1}" on the "{0}" target has a reference to item metadata. References to item metadata are not allowed in target conditions unless they are part of an item transform. + {StrBegin="MSB4116: "} + + + MSB4057: The target "{0}" does not exist in the project. + {StrBegin="MSB4057: "} + + + The target "{0}" listed in a BeforeTargets attribute at "{1}" does not exist in the project, and will be ignored. + + + The target "{0}" listed in an AfterTargets attribute at "{1}" does not exist in the project, and will be ignored. + + + Done building target "{0}" in project "{1}" -- FAILED. + + + Done building target "{0}" in project "{1}". + + + {0}: (TargetId:{1}) + + + Target output items: + + + {0} + + + MSB4058: The "{0}" target is missing its output specification. If a target declares inputs, it must also declare outputs. + {StrBegin="MSB4058: "} + + + Target Performance Summary: + + + Target "{0}" skipped, due to false condition; ({1}) was evaluated as ({2}). + + + + + + + Target "{0}" in file "{1}" from project "{2}": + + + Target "{0}" in file "{1}" from project "{2}" (entry point): + + + Target "{0}" in file "{1}" from project "{2}" (target "{3}" depends on it): + + + Target "{0}" in file "{1}" from project "{2}" (designated to run before target "{3}"): + + + Target "{0}" in file "{1}" from project "{2}" (designated to run after target "{3}"): + + + + + Target "{0}" in project "{1}": + + + Target "{0}" in project "{1}" (entry point): + + + Target "{0}" in project "{1}" (target "{2}" depends on it): + + + Target "{0}" in project "{1}" (designated to run before target "{2}"): + + + Target "{0}" in project "{1}" (designated to run after target "{2}"): + + + + + Target {0}: + + + + + Target "{0}" in file "{1}": + + + + + Target {0} from project "{1}": + + + + + Target "{0}" in file "{1}" from project "{2}": + + + + + Added Item(s): + + + Removed Item(s): + + + Set Property: {0}={1} + + + + + Output Item(s): + + + Output Property: {0}={1} + + + + + + Build continuing because "{0}" on the task "{1}" is set to "{2}". + + + MSB4060: The "{0}" task has been declared or used incorrectly, or failed during construction. Check the spelling of the task name and the assembly name. + {StrBegin="MSB4060: "} + + + MSB4214: The "{0}" task has been defined, but cannot be used due to the fact that the identity defined in the UsingTask declaration (Runtime="{1}", Architecture="{2}") does not match the identity specified by the task invocation (MSBuildRuntime="{3}", MSBuildArchitecture="{4}"). + {StrBegin="MSB4214: "}LOCALIZATION: Runtime, Architecture, MSBuildRuntime, and MSBuildArchitecture should not be localized. + + + Done executing task "{0}" -- FAILED. + + + Done executing task "{0}". + + + {0} (TaskId:{1}) + + + Using "{0}" task from assembly "{1}". + UE: This informational message helps users determine which assemblies their tasks were loaded from. + + + Using "{0}" task from the task factory "{1}". + UE: This informational message helps users determine which assemblies their tasks were loaded from. + + + Task "{0}" will be run in a single-threaded apartment thread. + UE: This informational message helps users determine if their tasks are run in the STA or MTA. + + + MSB4061: The "{0}" task could not be instantiated from "{1}". {2} + {StrBegin="MSB4061: "}LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4127: The "{0}" task could not be instantiated from the assembly "{1}". Please verify the task assembly has been built using the same version of the Microsoft.Build.Framework assembly as the one installed on your computer and that your host application is not missing a binding redirect for Microsoft.Build.Framework. {2} + {StrBegin="MSB4127: "}UE: This message is a specialized version of the TaskInstantiationFailureError message and can probably reuse some of its docs. + LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. Also, Microsoft.Build.Framework should not be localized + + + MSB4180: The "{0}" logger could not be instantiated from the assembly "{1}". Please verify the logger assembly has been built using the same version of the Microsoft.Build.Framework assembly as the one installed on your computer and that your host application is not missing a binding redirect for Microsoft.Build.Framework. {2} + + {StrBegin="MSB4180: "} + LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. Also, Microsoft.Build.Framework should not be localized + + + + MSB1021: Cannot create an instance of the logger. {0} + {StrBegin="MSB1021: "} + UE: This error is shown when a logger cannot be loaded and instantiated from its assembly. + LOCALIZATION: The prefix "MSBxxxx: " should not be localized. {0} contains a message explaining why the + logger could not be created -- this message comes from the CLR/FX and is localized. + + + MSB1020: The logger was not found. Check the following: 1.) The logger name specified is the same as the name of the logger class. 2.) The logger class is "public" and implements the Microsoft.Build.Framework.ILogger interface. 3.) The path to the logger assembly is correct, or the logger can be loaded using only the assembly name provided. + + {StrBegin="MSB1020: "}UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies an logger that does not exist e.g. "msbuild /logger:FooLoggerClass,FooAssembly". The + logger class must exist in the given assembly. + LOCALIZATION: The prefix "MSBxxxx: " should not be localized. + + + + MSB4179: The task factory instance for the "{0}" task could not be instantiated from the task factory "{1}". {2} + {StrBegin="MSB4179: "} + LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. + + + MSB4176: The "{0}" task factory could not be instantiated from the assembly "{1}". Please verify the task assembly has been built using the same version of the Microsoft.Build.Framework assembly as the one installed on your computer and that your host application is not missing a binding redirect for Microsoft.Build.Framework. {2} + {StrBegin="MSB4176: "}UE: This message is a specialized version of the TaskFactoryInstantiationFailureError message and can probably reuse some of its docs. + LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. Also, Microsoft.Build.Framework should not be localized + + + MSB4219: The TaskFactory "{0}" does not implement ITaskFactory2. The attributes "{1}" and/or "{2}" on the UsingTask declaration for task "{3}" will be be ignored. + {StrBegin="MSB4219: "} + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSB4062: The "{0}" task could not be loaded from the assembly {1}. {2} Confirm that the <UsingTask> declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask. + {StrBegin="MSB4062: "}UE: This message is shown when a task cannot be loaded from its assembly for various reasons e.g. corrupt assembly, + invalid task declaration, disk error, etc. "{2}" contains a message explaining what happened. + LOCALIZATION: "{2}" is a message from the CLR loader and is already localized. Also, <UsingTask> should not be localized. + + + MSB4215: The "{0}" task could not be loaded. "{1}" is an invalid value for the task host parameter "{2}". Valid values are: "{3}", "{4}", "{5}", and "{6}"; not specifying a value is also valid. + {StrBegin="MSB4215: "} + + + "{0}" is an invalid value for the task host parameter "{1}". Valid values are: "{2}", "{3}", "{4}", and "{5}"; not specifying a value is also valid. + + + MSB4216: Could not run the "{0}" task because MSBuild could not create or connect to a task host with runtime "{1}" and architecture "{2}". Please ensure that (1) the requested runtime and/or architecture are available on the machine, and (2) that the required executable "{3}" exists and can be run. + {StrBegin="MSB4216: "} + + + MSB4221: Could not run the "{0}" task because MSBuild could not create or connect to a task host with runtime "{1}" and architecture "{2}". Please ensure that (1) the requested runtime and/or architecture are available on the machine, and (2) that the required executable "{3}" exists and can be run. Error {4} {5}. + {StrBegin="MSB4221: "} + + + A task requested launch of the .NET 3.5 version of the MSBuild task host, but .NET 3.5 is not installed on this machine so the task host could not be launched. To fix this error, please either install .NET 3.5 or retarget your project. + + + MSB4217: Task host node exited prematurely. Diagnostic information may be found in files in the temporary files directory named MSBuild_*.failure.txt. {0} + {StrBegin="MSB4217: "} + + + MSB4063: The "{0}" task could not be initialized with its input parameters. {1} + {StrBegin="MSB4063: "} + + + Task Parameter: + + + Task Performance Summary: + + + Task "{0}" skipped, due to false condition; ({1}) was evaluated as ({2}). + + + Task "{0}" + + + Time Elapsed {0} + + + Building with tools version "{0}". + + + Project file contains ToolsVersion="{0}". This toolset may be unknown or missing, in which case you may be able to resolve this by installing the appropriate version of MSBuild, or the build may have been forced to a particular ToolsVersion for policy reasons. Treating the project as if it had ToolsVersion="{1}". For more information, please see http://go.microsoft.com/fwlink/?LinkId=293424. + + + Deferred Messages + + + MSB4090: Found an unexpected character '{2}' at position {1} in condition "{0}". + {StrBegin="MSB4090: "} + + + MSB4091: Found a call to an undefined function "{1}" in condition "{0}". + {StrBegin="MSB4091: "} + + + MSB4064: The "{0}" parameter is not supported by the "{1}" task. Verify the parameter exists on the task, and it is a settable public instance property. + {StrBegin="MSB4064: "} + + + MSB4131: The "{0}" parameter is not supported by the "{1}" task. Verify the parameter exists on the task, and it is a gettable public instance property. + {StrBegin="MSB4131: "} + + + MSB4092: An unexpected token "{1}" was found at character position {2} in condition "{0}". + {StrBegin="MSB4092: "} + + + MSB4065: The "{0}" parameter is not marked for output by the "{1}" task. + {StrBegin="MSB4065: "} + + + MSB4066: The attribute "{0}" in element <{1}> is unrecognized. + {StrBegin="MSB4066: "} + + + MSB4067: The element <{0}> beneath element <{1}> is unrecognized. + {StrBegin="MSB4067: "} + + + MSB4173: The element <{0}> beneath element <{1}> is invalid because a child element with that name already exists + {StrBegin="MSB4173: "} + + + MSB4189: <{1}> is not a valid child of the <{0}> element. + {StrBegin="MSB4189: "} + + + MSB4068: The element <{0}> is unrecognized, or not supported in this context. + {StrBegin="MSB4068: "} + + + MSB4069: The "{0}" type of the "{1}" parameter of the "{2}" task is not supported by MSBuild. + {StrBegin="MSB4069: "}LOCALIZATION: "MSBuild" should not be localized. + + + MSB4072: A <{0}> element must contain either the "{1}" attribute or the "{2}" attribute (but not both). + {StrBegin="MSB4072: "} + + + {0} Warning(s) + + + MSB4084: A <When> element may not follow an <Otherwise> element in a <Choose>. + {StrBegin="MSB4084: "} + + + MSB4150: Must initialize the console logger using the initialize method before using ApplyParameter + {StrBegin="MSB4150: "} + + + MSB4206: A non-null host object cannot be specified for a project which has an affinity set to out-of-process. + {StrBegin="MSB4206: "} + + + MSB4207: Only an in-process affinity may be specified for projects which have registered host objects. + {StrBegin="MSB4207: "} + + + MSB4208: Only an in-process affinity may be specified for projects which have registered host objects. + {StrBegin="MSB4208: "} + + + MSB4209: The specified project instance conflicts with the affinity specified in the HostServices. + {StrBegin="MSB4209: "} + + + MSB4213: The specified request affinity {0} conflicts with a previous affinity {1} specified for this project. + {StrBegin="MSB4213: "} + + + MSB4223: A node of the required type {0} could not be created. + {StrBegin="MSB4223: "} + + + MSB4224: KeepMetadata and RemoveMetadata are mutually exclusive. + {StrBegin="MSB4224: "} + + + "{0}" ({1} target) ({2}) -> + + + "{0}" (default target) ({1}) -> + + + {0} {1,5} + + + Project "{0}" on node {1} ({2} target(s)). + + + Project "{0}" on node {1} (default targets). + + + Project "{0}" ({1}) is building "{2}" ({3}) on node {4} ({5} target(s)). + + + Project "{0}" ({1}) is building "{2}" ({3}) on node {4} (default targets). + + + ({0} target) -> + + + The property "{0}" with value "{1}" is being overridden by another batch. The property is now: "{2}" + + + The log file path cannot be null or empty. + + + [default] + + + MSB4174: The task factory "{0}" could not be found in the assembly "{1}". + {StrBegin="MSB4174: "} + + + MSB4175: The task factory "{0}" could not be loaded from the assembly "{1}". {2} + {StrBegin="MSB4175: "} + + + MSB4201: The cancel operation was unable to complete because the currently executing task is not responding. + {StrBegin="MSB4201: "} + + + MSB4220: Waiting for the currently executing task "{0}" to cancel. + {StrBegin="MSB4220: "} + + + MSB4202: The request to build submission {0} cannot proceed because submission {1} is already using the main thread. + {StrBegin="MSB4202: "} + + + The request to build this submission cannot proceed because a project with the same configuration is already building. + + + Initializing task factory "{0}" from assembly "{1}". + + + Initializing task host factory for task "{0}" from assembly "{1}" + + + The build plan could not be written. Check that the build process has permissions to write to {0}. + + + The MSBuild cache file "{0}" is inaccessible. Ensure you have access to the directory and that this file is not deleted during the build. {1} + + LOCALIZATION: "{1}" is a localized message from a CLR/FX exception. + + + + The build plan could not be read. Check that the build process has permissions to read {0}. + + + The build plan at {0} appears to be corrupt and cannot be used. + + + +Detailed Build Summary +====================== + + + + +============================== Build Hierarchy (IDs represent configurations) ===================================================== +Id : Exclusive Time Total Time Path (Targets) +----------------------------------------------------------------------------------------------------------------------------------- + + + {0}{1,-3}{2}: {3:0.000}s {4:0.000}s {5} ({6}) + Fields 3 and 4 represent seconds, and the 's' following them should reflect the localized abbreviation for seconds. + + + +============================== Node Utilization (IDs represent configurations) ==================================================== +Timestamp: {0} Duration Cumulative +----------------------------------------------------------------------------------------------------------------------------------- + Spacing is important. Preserve the number of characters between the start of each word. + + + {0} {1:0.000}s {2:0.000}s {3} + Fields 1 and 2 represents seconds, and the 's' following it should reflect the localized abbreviation for seconds. + + + ----------------------------------------------------------------------------------------------------------------------------------- +Utilization: {0} Average Utilization: {1:###.0} + Spacing is important. Preserve the number of characters between the start of each word. + + + + + + + This collection cannot convert from the specified value to the backing value. + + + The "{0}" property name is reserved. + UE: This message is shown when the user tries to redefine one of the reserved MSBuild properties e.g. $(MSBuildProjectFile) through the object model + + + + + + + The <{0}> element must have either the "{1}" attribute or the "{2}" attribute but not both. + + + The "Remove" attribute is not permitted on an item outside of a <Target>. + + + The "KeepMetadata" attribute is not permitted on an item outside of a <Target>. + + + The "RemoveMetadata" attribute is not permitted on an item outside of a <Target>. + + + The "KeepDuplicates" attribute is not permitted on an item outside of a <Target>. + + + Elements of this type do not have a condition. + For example, it is not legal for a <ProjectExtensions> element to have a condition attribute. + + + The node already has a parent. Remove it from its parent before moving it. + + + The node is not parented by this object so it cannot be removed from it. + + + The reference node is not a child of this node. + + + Cannot make a node or an ancestor of that node a child of itself. + + + Cannot create a relationship between nodes that were created from different projects. + + + The parent node is not itself parented. + + + This parent is not a valid parent for the item. + + + An item not parented under a Target must have a value for Include and no value for Remove. + LOCALIZATION: Please do not localize "Target", "Include", and "Remove". + + + The name "{0}" contains an invalid character "{1}". + + + An <Otherwise> element cannot be located before a <When> or <Otherwise> element. + + + Project has not been given a path to save to. + + + Project was not loaded with the ProjectLoadSettings.RecordDuplicateImports flag. + + + Project or targets file "{0}" was loaded in read-only mode, and cannot be saved. + + + + + + + The "{0}" object specified does not belong to the correct "{1}" object. + + + The "{0}" name is reserved, and cannot be used. + + + The "{0}" property is a global property, and cannot be modified. + + + An equivalent project (a project with the same global properties and tools version) is already present in the project collection, with the path "{0}". To load an equivalent into this project collection, unload this project first. + + + The project provided was not loaded in the collection. + + + Cannot modify an evaluated object originating in an imported file "{0}". + + + Cannot remove the metadata named "{0}" as it originates from an item definition, rather than being directly defined on this item. + + + The project cannot be used as it is no longer loaded into a project collection. + + + The operation is not allowed because currently this object is not parented. + + + The project XML file "{0}" cannot be unloaded because at least one project "{1}" is still loaded which references that project XML. + + + Instance object was created as immutable. + + + + + + + All build submissions in a build must use project instances originating from the same project collection. + This occurs if a client tries to add two BuildSubmissions to a build, each involving a ProjectInstance originating in different ProjectCollection. This is not allowed. + + + + + + + + + + Build started. + + + {0} ({1},{2}) + A file location to be embedded in a string. + + + + + + MSB3203: The output path "{0}" cannot be rebased. {1} + {StrBegin="MSB3203: "}UE: This message is shown when the user asks the "MSBuild" task to rebase the paths of its output items relative to the project from where the "MSBuild" task is called (as opposed to the project(s) on which the "MSBuild" task is called), and one of the output item paths is invalid. LOCALIZATION: "{1}" is a localized message from a CLR/FX exception explaining the problem. + + + MSB3202: The project file "{0}" was not found. + + {StrBegin="MSB3202: "}UE: This message is shown when the user passes a non-existent project file to the MSBuild task, in the "Projects" parameter. + and they have not specified the SkipNonexistentProjects parameter, or it is set to false. + + + + Skipping project "{0}" because it was not found. + UE: This message is shown when the user passes a non-existent project file to the MSBuild task, in the "Projects" parameter, and they have specified the SkipNonexistentProjects parameter. + + + MSB3204: The project file "{0}" is in the ".vcproj" file format, which MSBuild no longer supports. Please convert the project by opening it in the Visual Studio IDE or running the conversion tool, or use MSBuild 3.5 or earlier to build it. + {StrBegin="MSB3204: "} LOC: ".vcproj" should not be localized + + + MSB3205: SkipNonexistentProject can only accept values of "True", "False" and "Build". + {StrBegin="MSB3205: "} LOC: "SkipNonexistentProject", "True", "False" and "Build" should not be localized + + + The MSBuild task is skipping the remaining projects because the StopOnFirstFailure parameter was set to true. + LOCALIZATION: Do not localize the words "MSBuild" or "StopOnFirstFailure". + + + The MSBuild task is skipping the remaining targets because the StopOnFirstFailure parameter was set to true. + LOCALIZATION: Do not localize the words "MSBuild" or "StopOnFirstFailure". + + + Overriding the BuildingInParallel property by setting it to false. This is due to the system being run in single process mode with StopOnFirstFailure set to true. + LOCALIZATION: Do not localize the words "MSBuild", "BuildingInParallel", or "StopOnFirstFailure". + + + StopOnFirstFailure will have no effect when the following conditions are all present: 1) The system is running in multiple process mode 2) The BuildInParallel property is true. 3) The RunEachTargetSeparately property is false. + LOCALIZATION: Do not localize the words "RunEachTargetSeparately", "BuildingInParallel", or "StopOnFirstFailure". + + + + + MSB3100: Syntax for "{0}" parameter is not valid ({1}). Correct syntax is {0}="<name>=<value>". + {StrBegin="MSB3100: "}This error is shown if the user does any of the following: + Properties="foo" (missing property value) + Properties="=4" (missing property name) + The user must pass in an actual property name and value, as in Properties="Configuration=Debug". + + + Global Properties: + + + Removing Properties: + + + Overriding Global Properties for project "{0}" with: + + + Additional Properties for project "{0}": + + + Removing Properties for project "{0}": + + + + diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs new file mode 100644 index 00000000000..78eb44b0bee --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/AssemblyTaskFactory_Tests.cs @@ -0,0 +1,697 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the AssemblyTaskFactory +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using System.Reflection; +using Microsoft.Build.Utilities; +using Microsoft.Build.Construction; + +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using LoggingService = Microsoft.Build.BackEnd.Logging.LoggingService; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for the assembly task factory + /// + [TestClass] + public class AssemblyTaskFactory_Tests + { + /// + /// A well instantiated task factory + /// + private AssemblyTaskFactory _taskFactory; + + /// + /// The load info about a task to wrap in the assembly task factory + /// + private AssemblyLoadInfo _loadInfo; + + /// + /// The loaded type from the initialized task factory. + /// + private LoadedType _loadedType; + + /// + /// Initialize a task factory + /// + [TestInitialize] + public void Setup() + { + SetupTaskFactory(null, false); + } + + /// + /// Tear down what was created in setup + /// + [TestCleanup] + public void TearDownAttribute() + { + _taskFactory = null; + _loadInfo = null; + _loadedType = null; + } + + #region AssemblyTaskFactory + #region ExpectExceptions + /// + /// Make sure we get an invalid project file exception when a null load info is passed to the factory + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullLoadInfo() + { + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + taskFactory.InitializeFactory(null, "TaskToTestFactories", new Dictionary(), string.Empty, null, false, null, ElementLocation.Create("NONE"), String.Empty); + } + + /// + /// Make sure we get an invalid project file exception when a null task name is passed to the factory + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void NullTaskName() + { + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + taskFactory.InitializeFactory(_loadInfo, null, new Dictionary(), string.Empty, null, false, null, ElementLocation.Create("NONE"), String.Empty); + } + + /// + /// Make sure we get an invalid project file exception when an empty task name is passed to the factory + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void EmptyTaskName() + { + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + taskFactory.InitializeFactory(_loadInfo, String.Empty, new Dictionary(), string.Empty, null, false, null, ElementLocation.Create("NONE"), String.Empty); + } + + /// + /// Make sure we get an invalid project file exception when the task is not in the info + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void GoodTaskNameButNotInInfo() + { + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + taskFactory.InitializeFactory(_loadInfo, "RandomTask", new Dictionary(), string.Empty, null, false, null, ElementLocation.Create("NONE"), String.Empty); + } + + /// + /// Make sure we get an internal error when we call the initialize factory on the public method. + /// This is done because we cannot properly initialize the task factory using the public interface and keep + /// backwards compatibility with orcas and whidbey. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void CallPublicInitializeFactory() + { + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + taskFactory.Initialize(String.Empty, new Dictionary(), String.Empty, null); + } + + /// + /// Make sure we get an internal error when we call the ITaskFactory2 version of initialize factory. + /// This is done because we cannot properly initialize the task factory using the public interface and keep + /// backwards compatibility with orcas and whidbey. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void CallPublicInitializeFactory2() + { + AssemblyTaskFactory taskFactory = new AssemblyTaskFactory(); + taskFactory.Initialize(String.Empty, null, new Dictionary(), String.Empty, null); + } + + #endregion + + /// + /// Verify that we can ask the factory if a given task is in the factory and get the correct result back + /// + [TestMethod] + public void CreatableByTaskFactoryGoodName() + { + Assert.IsTrue(_taskFactory.TaskNameCreatableByFactory("TaskToTestFactories", null, String.Empty, null, ElementLocation.Create(".", 1, 1))); + } + + /// + /// Expect a false answer when we ask for a task which is not in the factory. + /// + [TestMethod] + public void CreatableByTaskFactoryNotInAssembly() + { + Assert.IsFalse(_taskFactory.TaskNameCreatableByFactory("NotInAssembly", null, String.Empty, null, ElementLocation.Create(".", 1, 1))); + } + + /// + /// Expect a false answer when we ask for a task which is not in the factory. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void CreatableByTaskFactoryNotInAssemblyEmptyTaskName() + { + Assert.IsFalse(_taskFactory.TaskNameCreatableByFactory(String.Empty, null, String.Empty, null, ElementLocation.Create(".", 1, 1))); + } + + /// + /// Expect a false answer when we ask for a task which is not in the factory. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void CreatableByTaskFactoryNullTaskName() + { + Assert.IsFalse(_taskFactory.TaskNameCreatableByFactory(null, null, String.Empty, null, ElementLocation.Create(".", 1, 1))); + } + + /// + /// Make sure that when an explicitly matching identity is specified (e.g. the identity is non-empty), + /// it still counts as correct. + /// + [TestMethod] + public void CreatableByTaskFactoryMatchingIdentity() + { + IDictionary factoryIdentityParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + factoryIdentityParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.currentRuntime); + factoryIdentityParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); + + SetupTaskFactory(factoryIdentityParameters, false /* don't want task host */); + + IDictionary taskIdentityParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskIdentityParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + taskIdentityParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + Assert.IsTrue(_taskFactory.TaskNameCreatableByFactory("TaskToTestFactories", taskIdentityParameters, String.Empty, null, ElementLocation.Create(".", 1, 1))); + } + + /// + /// Verify that if the task identity parameters don't match the factory identity, TaskNameCreatableByFactory + /// returns false. + /// + [TestMethod] + public void CreatableByTaskFactoryMismatchedIdentity() + { + IDictionary factoryIdentityParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + factoryIdentityParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr2); + factoryIdentityParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); + + SetupTaskFactory(factoryIdentityParameters, false /* don't want task host */); + + IDictionary taskIdentityParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskIdentityParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + taskIdentityParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); + + Assert.IsFalse(_taskFactory.TaskNameCreatableByFactory("TaskToTestFactories", taskIdentityParameters, String.Empty, null, ElementLocation.Create(".", 1, 1))); + } + + /// + /// Make sure the number of properties retreived from the task factory are the same number retreived from the type directly. + /// + [TestMethod] + public void VerifyGetTaskParameters() + { + TaskPropertyInfo[] propertyInfos = _taskFactory.GetTaskParameters(); + LoadedType comparisonType = new LoadedType(typeof(TaskToTestFactories), _loadInfo); + PropertyInfo[] comparisonInfo = comparisonType.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + Assert.AreEqual(comparisonInfo.Length, propertyInfos.Length); + + bool foundExpectedParameter = false; + bool foundNotExpectedParameter = false; + + for (int i = 0; i < propertyInfos.Length; i++) + { + if (propertyInfos[i].Name.Equals("ExpectedParameter", StringComparison.OrdinalIgnoreCase)) + { + foundExpectedParameter = true; + } + + if (propertyInfos[i].Name.Equals("NotExpectedParameter", StringComparison.OrdinalIgnoreCase)) + { + foundNotExpectedParameter = true; + } + } + + Assert.IsTrue(foundExpectedParameter); + Assert.IsFalse(foundNotExpectedParameter); + } + + /// + /// Verify a good task can be created. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyGoodTaskInstantiation() + { + ITask createdTask = null; + try + { + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that does not use the task host can be created when passed "don't care" + /// for the task invocation task host parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyMatchingTaskParametersDontLaunchTaskHost1() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.any); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that does not use the task host can be created when passed task host + /// parameters that explicitly match the current process. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyMatchingTaskParametersDontLaunchTaskHost2() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.GetCurrentMSBuildArchitecture()); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that does not use the task host can be created when passed "don't care" + /// for the task invocation task host parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyMatchingUsingTaskParametersDontLaunchTaskHost1() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.any); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + SetupTaskFactory(taskParameters, false /* don't want task host */); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that does not use the task host can be created when passed task host + /// parameters that explicitly match the current process. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyMatchingUsingTaskParametersDontLaunchTaskHost2() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.any); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.GetCurrentMSBuildArchitecture()); + + SetupTaskFactory(taskParameters, false /* don't want task host */); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when passed task host + /// parameters that explicitly do not match the current process. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifyMatchingParametersDontLaunchTaskHost() + { + ITask createdTask = null; + try + { + IDictionary factoryParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + factoryParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + + SetupTaskFactory(factoryParameters, false /* don't want task host */); + + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when passed task host + /// parameters that explicitly do not match the current process. + /// + [TestMethod] + public void VerifyNonmatchingUsingTaskParametersLaunchTaskHost() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr2); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + SetupTaskFactory(taskParameters, false /* don't want task host */); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when passed task host + /// parameters that explicitly do not match the current process. + /// + [TestMethod] + public void VerifyNonmatchingTaskParametersLaunchTaskHost() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr2); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when passed task host + /// parameters that explicitly do not match the current process. + /// + [TestMethod] + public void VerifyNonmatchingParametersLaunchTaskHost() + { + ITask createdTask = null; + try + { + IDictionary factoryParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + factoryParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr2); + + SetupTaskFactory(factoryParameters, false /* don't want task host */); + + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when the task factory is + /// explicitly instructed to launch the task host. + /// + [TestMethod] + public void VerifyExplicitlyLaunchTaskHost() + { + ITask createdTask = null; + try + { + SetupTaskFactory(null, true /* want task host */); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when the task factory is + /// explicitly instructed to launch the task host. + /// + [TestMethod] + public void VerifyExplicitlyLaunchTaskHostEvenIfParametersMatch1() + { + ITask createdTask = null; + try + { + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.any); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + SetupTaskFactory(taskParameters, true /* want task host */); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when the task factory is + /// explicitly instructed to launch the task host. + /// + [TestMethod] + public void VerifyExplicitlyLaunchTaskHostEvenIfParametersMatch2() + { + ITask createdTask = null; + try + { + SetupTaskFactory(null, true /* want task host */); + + IDictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.any); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Verify a good task that uses the task host can be created when the task factory is + /// explicitly instructed to launch the task host. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void VerifySameFactoryCanGenerateDifferentTaskInstances() + { + ITask createdTask = null; + IDictionary factoryParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + factoryParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.any); + factoryParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.any); + + SetupTaskFactory(factoryParameters, explicitlyLaunchTaskHost: false); + + try + { + // #1: don't launch task host + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), null, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsNotInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + + try + { + // #2: launch task host + var taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr2); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.currentArchitecture); + + createdTask = _taskFactory.CreateTaskInstance(ElementLocation.Create("MSBUILD"), null, new MockHost(), taskParameters, new AppDomainSetup(), false); + Assert.IsNotNull(createdTask); + Assert.IsInstanceOfType(createdTask, typeof(TaskHostTask)); + } + finally + { + if (createdTask != null) + { + _taskFactory.CleanupTask(createdTask); + } + } + } + + /// + /// Abstract out the creation of the new AssemblyTaskFactory with default task, and + /// with some basic validation. + /// + private void SetupTaskFactory(IDictionary factoryParameters, bool explicitlyLaunchTaskHost) + { + _taskFactory = new AssemblyTaskFactory(); + _loadInfo = AssemblyLoadInfo.Create(null, Assembly.GetAssembly(typeof(TaskToTestFactories)).Location); + _loadedType = _taskFactory.InitializeFactory(_loadInfo, "TaskToTestFactories", new Dictionary(), string.Empty, factoryParameters, explicitlyLaunchTaskHost, null, ElementLocation.Create("NONE"), String.Empty); + Assert.IsTrue(_loadedType.Assembly.Equals(_loadInfo), "Expected the AssemblyLoadInfo to be equal"); + } + + #endregion + + #region InternalClasses + /// + /// Create a task which can be used to test the factories + /// + public class TaskToTestFactories : AppDomainIsolatedTask + { + /// + /// Give a parameter which can be considered expected + /// + public string ExpectedParameter + { + get; + set; + } + + /// + /// Expect not to find this parameter as it is internal + /// + internal string NotExpected + { + get; + set; + } + + /// + /// Execute the test + /// + public override bool Execute() + { + return true; + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BatchingEngine_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BatchingEngine_Tests.cs new file mode 100644 index 00000000000..a276fd88af0 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BatchingEngine_Tests.cs @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Collections; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using System.Collections.Generic; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class BatchingEngine_Tests + { + [TestMethod] + public void GetBuckets() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + List parameters = new List(); + parameters.Add("@(File);$(unittests)"); + parameters.Add("$(obj)\\%(Filename).ext"); + parameters.Add("@(File->'%(extension)')"); // attributes in transforms don't affect batching + + ItemDictionary itemsByType = new ItemDictionary(); + + IList items = new List(); + items.Add(new ProjectItemInstance(project, "File", "a.foo", project.FullPath)); + items.Add(new ProjectItemInstance(project, "File", "b.foo", project.FullPath)); + items.Add(new ProjectItemInstance(project, "File", "c.foo", project.FullPath)); + items.Add(new ProjectItemInstance(project, "File", "d.foo", project.FullPath)); + items.Add(new ProjectItemInstance(project, "File", "e.foo", project.FullPath)); + itemsByType.ImportItems(items); + + items = new List(); + items.Add(new ProjectItemInstance(project, "Doc", "a.doc", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Doc", "b.doc", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Doc", "c.doc", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Doc", "d.doc", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Doc", "e.doc", project.FullPath)); + itemsByType.ImportItems(items); + + PropertyDictionary properties = new PropertyDictionary(); + properties.Set(ProjectPropertyInstance.Create("UnitTests", "unittests.foo")); + properties.Set(ProjectPropertyInstance.Create("OBJ", "obj")); + + List buckets = BatchingEngine.PrepareBatchingBuckets(parameters, CreateLookup(itemsByType, properties), MockElementLocation.Instance); + + Assert.AreEqual(5, buckets.Count); + + foreach (ItemBucket bucket in buckets) + { + // non-batching data -- same for all buckets + XmlAttribute tempXmlAttribute = (new XmlDocument()).CreateAttribute("attrib"); + tempXmlAttribute.Value = "'$(Obj)'=='obj'"; + + Assert.IsTrue(ConditionEvaluator.EvaluateCondition(tempXmlAttribute.Value, ParserOptions.AllowAll, bucket.Expander, ExpanderOptions.ExpandAll, Environment.CurrentDirectory, MockElementLocation.Instance, null, new BuildEventContext(1, 2, 3, 4))); + Assert.AreEqual("a.doc;b.doc;c.doc;d.doc;e.doc", bucket.Expander.ExpandIntoStringAndUnescape("@(doc)", ExpanderOptions.ExpandItems, MockElementLocation.Instance)); + Assert.AreEqual("unittests.foo", bucket.Expander.ExpandIntoStringAndUnescape("$(bogus)$(UNITTESTS)", ExpanderOptions.ExpandPropertiesAndMetadata, MockElementLocation.Instance)); + } + + Assert.AreEqual("a.foo", buckets[0].Expander.ExpandIntoStringAndUnescape("@(File)", ExpanderOptions.ExpandItems, MockElementLocation.Instance)); + Assert.AreEqual(".foo", buckets[0].Expander.ExpandIntoStringAndUnescape("@(File->'%(Extension)')", ExpanderOptions.ExpandItems, MockElementLocation.Instance)); + Assert.AreEqual("obj\\a.ext", buckets[0].Expander.ExpandIntoStringAndUnescape("$(obj)\\%(Filename).ext", ExpanderOptions.ExpandPropertiesAndMetadata, MockElementLocation.Instance)); + + // we weren't batching on this attribute, so it has no value + Assert.AreEqual(String.Empty, buckets[0].Expander.ExpandIntoStringAndUnescape("%(Extension)", ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + ProjectItemInstanceFactory factory = new ProjectItemInstanceFactory(project, "i"); + items = buckets[0].Expander.ExpandIntoItemsLeaveEscaped("@(file)", factory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + Assert.IsNotNull(items); + Assert.AreEqual(1, items.Count); + + int invalidProjectFileExceptions = 0; + try + { + // This should throw because we don't allow item lists to be concatenated + // with other strings. + bool throwAway; + items = buckets[0].Expander.ExpandSingleItemVectorExpressionIntoItems("@(file)$(unitests)", factory, ExpanderOptions.ExpandItems, false /* no nulls */, out throwAway, MockElementLocation.Instance); + } + catch (InvalidProjectFileException ex) + { + // check we don't lose error codes from IPFE's during build + Assert.AreEqual(ex.ErrorCode, "MSB4012"); + invalidProjectFileExceptions++; + } + + // We do allow separators in item vectors, this results in an item group with a single flattened item + items = buckets[0].Expander.ExpandIntoItemsLeaveEscaped("@(file, ',')", factory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + Assert.IsNotNull(items); + Assert.AreEqual(1, items.Count); + Assert.AreEqual("a.foo", items[0].EvaluatedInclude); + + Assert.AreEqual(1, invalidProjectFileExceptions); + } + + /// + /// Tests the real simple case of using an unqualified metadata reference %(Culture), + /// where there are only two items and both of them have a value for Culture, but they + /// have different values. + /// + [TestMethod] + public void ValidUnqualifiedMetadataReference() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + List parameters = new List(); + parameters.Add("@(File)"); + parameters.Add("%(Culture)"); + + ItemDictionary itemsByType = new ItemDictionary(); + + List items = new List(); + + ProjectItemInstance a = new ProjectItemInstance(project, "File", "a.foo", project.FullPath); + ProjectItemInstance b = new ProjectItemInstance(project, "File", "b.foo", project.FullPath); + a.SetMetadata("Culture", "fr-fr"); + b.SetMetadata("Culture", "en-en"); + items.Add(a); + items.Add(b); + itemsByType.ImportItems(items); + + PropertyDictionary properties = new PropertyDictionary(); + + List buckets = BatchingEngine.PrepareBatchingBuckets(parameters, CreateLookup(itemsByType, properties), null); + Assert.AreEqual(2, buckets.Count); + } + + /// + /// Tests the case where an unqualified metadata reference is used illegally. + /// It's illegal because not all of the items consumed contain a value for + /// that metadata. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidUnqualifiedMetadataReference() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + List parameters = new List(); + parameters.Add("@(File)"); + parameters.Add("%(Culture)"); + + ItemDictionary itemsByType = new ItemDictionary(); + + List items = new List(); + + ProjectItemInstance a = new ProjectItemInstance(project, "File", "a.foo", project.FullPath); + items.Add(a); + ProjectItemInstance b = new ProjectItemInstance(project, "File", "b.foo", project.FullPath); + items.Add(b); + a.SetMetadata("Culture", "fr-fr"); + itemsByType.ImportItems(items); + + PropertyDictionary properties = new PropertyDictionary(); + + // This is expected to throw because not all items contain a value for metadata "Culture". + // Only a.foo has a Culture metadata. b.foo does not. + List buckets = BatchingEngine.PrepareBatchingBuckets(parameters, CreateLookup(itemsByType, properties), MockElementLocation.Instance); + } + + /// + /// Tests the case where an unqualified metadata reference is used illegally. + /// It's illegal because not all of the items consumed contain a value for + /// that metadata. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void NoItemsConsumed() + { + List parameters = new List(); + parameters.Add("$(File)"); + parameters.Add("%(Culture)"); + + ItemDictionary itemsByType = new ItemDictionary(); + PropertyDictionary properties = new PropertyDictionary(); + + // This is expected to throw because we have no idea what item list %(Culture) refers to. + List buckets = BatchingEngine.PrepareBatchingBuckets(parameters, CreateLookup(itemsByType, properties), MockElementLocation.Instance); + } + + /// + /// Missing unittest found by mutation testing. + /// REASON TEST WASN'T ORIGINALLY PRESENT: Missed test. + /// + /// This test ensures that two items with duplicate attributes end up in exactly one batching + /// bucket. + /// + [TestMethod] + public void Regress_Mutation_DuplicateBatchingBucketsAreFoldedTogether() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + List parameters = new List(); + parameters.Add("%(File.Culture)"); + + ItemDictionary itemsByType = new ItemDictionary(); + + List items = new List(); + items.Add(new ProjectItemInstance(project, "File", "a.foo", project.FullPath)); + items.Add(new ProjectItemInstance(project, "File", "b.foo", project.FullPath)); // Need at least two items for this test case to ensure multiple buckets might be possible + itemsByType.ImportItems(items); + + PropertyDictionary properties = new PropertyDictionary(); + + List buckets = BatchingEngine.PrepareBatchingBuckets(parameters, CreateLookup(itemsByType, properties), null); + + // If duplicate buckes have been folded correctly, then there will be exactly one bucket here + // containing both a.foo and b.foo. + Assert.AreEqual(1, buckets.Count); + } + + [TestMethod] + public void Simple() + { + string content = @" + + + + + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[AToBBatched: a;b]"); + } + + + /// + /// When removing an item in a target which is batched and called by call target there was an exception thrown + /// due to us adding the same item instance to the remove item lists when merging the lookups between the two batches. + /// The fix was to not add the item to the remove list if it already exists. + /// + [TestMethod] + public void Regress72803() + { + string content = @" + + + + + + + + + + + + + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("Item Before:dev 1"); + log.AssertLogContains("Item Before:prod 1"); + log.AssertLogDoesntContain("Item After:dev 1"); + log.AssertLogDoesntContain("Item After:prod 1"); + } + + /// + /// Regress a bug where batching over an item list seemed to have + /// items for that list even in buckets where there should be none, because + /// it was batching over metadata that only other list/s had. + /// + [TestMethod] + public void BucketsWithEmptyListForBatchedItemList() + { + string content = @" + + + + + x + + + + + + '%(Filename).obj');%(i.foo)""/> + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogDoesntContain("a.obj"); + } + + /// + /// Bug for Targets instead of Tasks. + /// + [TestMethod] + public void BucketsWithEmptyListForTargetBatchedItemList() + { + string content = @" + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[a=a1 b=]"); + log.AssertLogContains("[a= b=b1]"); + } + + /// + /// A batching target that has no outputs should still run. + /// This is how we shipped before, although Jay pointed out it's odd. + /// + [TestMethod] + public void BatchOnEmptyOutput() + { + string content = @" + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[]"); + } + + /// + /// Every batch should get its own new task object. + /// We verify this by using the Warning class. If the same object is being reused, + /// the second warning would have the code from the first use of the task. + /// + [TestMethod] + public void EachBatchGetsASeparateTaskObject() + { + string content = @" + + + + high + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + Assert.AreEqual("high", log.Warnings[0].Code); + Assert.AreEqual(null, log.Warnings[1].Code); + } + + + /// + /// It is important that the batching engine invokes the different batches in the same + /// order as the items are declared in the project, especially when batching is simply + /// being used as a "for loop". + /// + [TestMethod] + public void BatcherPreservesItemOrderWithinASingleItemList() + { + string content = @" + + + + + + + + + + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("AToZBatched: a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;w;x;y;z"); + log.AssertLogContains("ZToABatched: z;y;x;w;v;u;t;s;r;q;p;o;n;m;l;k;j;i;h;g;f;e;d;c;b;a"); + } + + /// + /// Undefined and empty metadata values should not be distinguished when bucketing. + /// This is the same as previously shipped. + /// + [TestMethod] + public void UndefinedAndEmptyMetadataValues() + { + string content = @" + + + + + + + + m1 + + + + + + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(content)))); + MockLogger logger = new MockLogger(); + project.Build(logger); + + logger.AssertLogContains("[i1;i2 ]", "[i3 m1]"); + } + + private static Lookup CreateLookup(ItemDictionary itemsByType, PropertyDictionary properties) + { + return new Lookup(itemsByType, properties, null); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildEventArgTransportSink_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildEventArgTransportSink_Tests.cs new file mode 100644 index 00000000000..d8a1ead86d9 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildEventArgTransportSink_Tests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Transport sink correctly takes a buildEventArg and sends to the transport layer +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + /// Test the central forwarding logger by initializing a new one and sending events through it. + /// + [TestClass] + public class BuildEventArgTransportSink_Tests + { + /// + /// Verify the properties on BuildEventArgTransportSink properly work + /// + [TestMethod] + public void PropertyTests() + { + BuildEventArgTransportSink sink = new BuildEventArgTransportSink(PacketProcessor); + Assert.IsNull(sink.Name); + + string name = "Test Name"; + sink.Name = name; + Assert.IsTrue(string.Compare(sink.Name, name, StringComparison.OrdinalIgnoreCase) == 0); + } + + /// + /// Make sure we throw an exception if the transport delegate is null + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestConstructorNullSendDataDelegate() + { + BuildEventArgTransportSink transportSink = new BuildEventArgTransportSink(null); + } + + /// + /// Verify consume throws the correct exception when a null build event is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestConsumeNullBuildEvent() + { + BuildEventArgTransportSink transportSink = new BuildEventArgTransportSink(PacketProcessor); + transportSink.Consume(null, 0); + } + + /// + /// Verify consume properly packages up the message event into a packet and send it to the + /// transport delegate + /// + [TestMethod] + public void TestConsumeMessageBuildEvent() + { + bool wentInHandler = false; + BuildMessageEventArgs messageEvent = new BuildMessageEventArgs("My message", "Help me keyword", "Sender", MessageImportance.High); + SendDataDelegate transportDelegate = delegate (INodePacket packet) + { + wentInHandler = true; + LogMessagePacket loggingPacket = packet as LogMessagePacket; + Assert.IsNotNull(loggingPacket); + BuildMessageEventArgs messageEventFromPacket = loggingPacket.NodeBuildEvent.Value.Value as BuildMessageEventArgs; + Assert.IsTrue(messageEventFromPacket == messageEvent); + }; + + BuildEventArgTransportSink transportSink = new BuildEventArgTransportSink(transportDelegate); + transportSink.Consume(messageEvent, 0); + Assert.IsTrue(wentInHandler, "Expected to go into transport delegate"); + } + + /// + /// Verify consume ignores BuildStarted events + /// + [TestMethod] + public void TestConsumeBuildStartedEvent() + { + bool wentInHandler = false; + BuildStartedEventArgs buildStarted = new BuildStartedEventArgs("Start", "Help"); + SendDataDelegate transportDelegate = delegate (INodePacket packet) + { + wentInHandler = true; + }; + + BuildEventArgTransportSink transportSink = new BuildEventArgTransportSink(transportDelegate); + transportSink.Consume(buildStarted, 0); + Assert.IsTrue(transportSink.HaveLoggedBuildStartedEvent); + Assert.IsFalse(transportSink.HaveLoggedBuildFinishedEvent); + Assert.IsFalse(wentInHandler, "Expected not to go into transport delegate"); + } + + /// + /// Verify consume ignores BuildFinished events + /// + [TestMethod] + public void TestConsumeBuildFinishedEvent() + { + bool wentInHandler = false; + BuildFinishedEventArgs buildFinished = new BuildFinishedEventArgs("Finished", "Help", true); + SendDataDelegate transportDelegate = delegate (INodePacket packet) + { + wentInHandler = true; + }; + + BuildEventArgTransportSink transportSink = new BuildEventArgTransportSink(transportDelegate); + transportSink.Consume(buildFinished, 0); + Assert.IsFalse(transportSink.HaveLoggedBuildStartedEvent); + Assert.IsTrue(transportSink.HaveLoggedBuildFinishedEvent); + Assert.IsFalse(wentInHandler, "Expected not to go into transport delegate"); + } + + /// + /// Make sure shutdown will correctly null out the send data delegate + /// + [TestMethod] + public void TestShutDown() + { + SendDataDelegate transportDelegate = PacketProcessor; + WeakReference weakTransportDelegateReference = new WeakReference(transportDelegate); + BuildEventArgTransportSink transportSink = new BuildEventArgTransportSink(transportDelegate); + + transportSink.ShutDown(); + + Assert.IsNotNull(weakTransportDelegateReference.Target); + transportDelegate = null; + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // Expected shutdown to null out the sendData delegate, the two garbage collections + // should have collected the sendDataDelegate causing the weak reference to die. + Assert.IsNull(weakTransportDelegateReference.Target, " Expected delegate to be dead"); + } + + /// + /// Create a method which will be a fake method to process a packet. + /// This needs to be done because using an anonymous method does not work. + /// Using an anonymous method does not work because when the delegate is created + /// it seems that a field is created which creates a strong reference + /// between the delegate and the class it was created in. This means the delegate is not + /// garbage collected until the class it was instantiated in is collected itself. + /// + /// Packet to process + private void PacketProcessor(INodePacket packet) + { + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildManager_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildManager_Tests.cs new file mode 100644 index 00000000000..33dded39dea --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildManager_Tests.cs @@ -0,0 +1,3632 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for the BuildManager object. +//----------------------------------------------------------------------- + +using System; +using System.CodeDom.Compiler; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Xml; +using System.Security; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.Build.Logging; + +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Diagnostics; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// The test fixture for the BuildManager + /// + [TestClass] + public class BuildManager_Tests + { + /// + /// The mock logger for testing. + /// + private MockLogger _logger; + + /// + /// The standard build manager for each test. + /// + private BuildManager _buildManager; + + /// + /// The build parameters. + /// + private BuildParameters _parameters; + + /// + /// The project collection used. + /// + private ProjectCollection _projectCollection; + + /// + /// SetUp + /// + [TestInitialize] + public void SetUp() + { + // Ensure that any previous tests which may have been using the default BuildManager do not conflict with us. + BuildManager.DefaultBuildManager.Dispose(); + + _logger = new MockLogger(); + _parameters = new BuildParameters(); + _parameters.ShutdownInProcNodeOnBuildFinish = true; + _parameters.Loggers = new ILogger[] { _logger }; + _parameters.EnableNodeReuse = false; + _buildManager = new BuildManager(); + _projectCollection = new ProjectCollection(); + Environment.SetEnvironmentVariable("MSBUILDINPROCENVCHECK", "1"); + } + + /// + /// TearDown + /// + [TestCleanup] + public void TearDown() + { + Environment.SetEnvironmentVariable("MSBUILDINPROCENVCHECK", null); + if (_buildManager != null) + { + _buildManager.Dispose(); + _buildManager = null; + } + } + + /// + /// Check that we behave reasonably when passed a null ProjectCollection + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void BuildParametersWithNullCollection() + { + BuildParameters parameters = new BuildParameters(null); + } + + /// + /// A simple successful build. + /// + [TestMethod] + public void SimpleBuild() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + + string propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty1", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty1", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty2", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty2", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty3", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty3", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify that the environment between two msbuild calls to the same project are stored + /// so that on the next call we get access to them + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void VerifyEnvironmentSavedBetweenCalls() + { + string contents1 = ObjectModelHelpers.CleanupFileContents(@" + + + + + System.Environment.SetEnvironmentVariable(""MOO"", ""When the dawn comes, tonight will be a memory too""); + + + + + + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(contents1)), (IDictionary)null, null, _projectCollection); + project.FullPath = FileUtilities.GetTemporaryFile(); + project.Save(); + try + { + string contents2 = ObjectModelHelpers.CleanupFileContents(@" + + + " + + "" + + @" + +"); + + ProjectInstance instance = CreateProjectInstance(contents2, null, _projectCollection, true); + BuildRequestData data = new BuildRequestData(instance, new string[] { "Build" }, _projectCollection.HostServices); + + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("What does a cat say : When the dawn comes, tonight will be a memory too"); + } + finally + { + if (File.Exists(project.FullPath)) + { + File.Delete(project.FullPath); + } + } + } + + /// + /// Verify if idle nodes are shutdown when BuildManager.ShutdownAllNodes is evoked. + /// The final number of nodes has to be less or equal the number of nodes already in + /// the system before this method was called. + /// + [TestMethod] + public void ShutdownNodesAfterParallelBuild() + { + ProjectCollection projectCollection = new ProjectCollection(); + + // Get number of MSBuild processes currently instantiated + int numberProcsOriginally = (new List(Process.GetProcessesByName("MSBuild"))).Count; + + // Generate a theoretically unique directory to put our dummy projects in. + string shutdownProjectDirectory = Path.Combine(Path.GetTempPath(), String.Format(CultureInfo.InvariantCulture, "VSNodeShutdown_{0}_UnitTest", Process.GetCurrentProcess().Id)); + + // Create the dummy projects we'll be "building" as our excuse to connect to and shut down + // all the nodes. + ProjectInstance rootProject = this.GenerateDummyProjects(shutdownProjectDirectory, numberProcsOriginally + 4, projectCollection); + + // Build the projects. + BuildParameters buildParameters = new BuildParameters(projectCollection); + + buildParameters.OnlyLogCriticalEvents = true; + buildParameters.MaxNodeCount = numberProcsOriginally + 4; + buildParameters.EnableNodeReuse = true; + buildParameters.DisableInProcNode = true; + + // Tell the build manager to not disturb process wide state + buildParameters.SaveOperatingEnvironment = false; + + BuildRequestData requestData = new BuildRequestData(rootProject, new string[] { "Build" }, null); + + // Use a separate BuildManager for the node shutdown build, so that we don't have + // to worry about taking dependencies on whether or not the existing ones have already + // disappeared. + BuildManager shutdownManager = new BuildManager("IdleNodeShutdown"); + shutdownManager.Build(buildParameters, requestData); + + // Number of nodes after the build has to be greater than the original number + int numberProcsAfterBuild = (new List(Process.GetProcessesByName("MSBuild"))).Count; + Assert.IsTrue(numberProcsOriginally < numberProcsAfterBuild); + + // Shutdown all nodes + shutdownManager.ShutdownAllNodes(); + + // Wait until all processes shut down + Thread.Sleep(3000); + + // Number of nodes after the shutdown has to be smaller or equal the original number + int numberProcsAfterShutdown = (new List(Process.GetProcessesByName("MSBuild"))).Count; + Assert.IsTrue(numberProcsAfterShutdown <= numberProcsOriginally); + + // Delete directory with the dummy project + if (Directory.Exists(shutdownProjectDirectory)) + { + Directory.Delete(shutdownProjectDirectory, true /* recursive delete */); + } + } + + /// + /// A simple successful build, out of process only. + /// + [TestMethod] + public void SimpleBuildOutOfProcess() + { + this.RunOutOfProcBuild(_ => Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1")); + } + + /// + /// A simple successful build, out of process only. Triggered by setting build parameters' DisableInProcNode to true. + /// + [TestMethod] + public void DisableInProcNode() + { + this.RunOutOfProcBuild(buildParameters => buildParameters.DisableInProcNode = true); + } + + /// + /// Runs a build and verifies it happenes out of proc by checking the process ID. + /// + /// Runs a test out of proc. + public void RunOutOfProcBuild(Action buildParametersModifier) + { + const string Contents = @" + + + + + + + + + + + + +"; + + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string originalMsBuildEnableAllPropertyFunctions = Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS"); + string projectFullPath = null; + try + { + // Need to set this env variable to enable Process.GetCurrentProcess().Id in the project file. + Environment.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", "1"); + + Project project = CreateProject(ObjectModelHelpers.CleanupFileContents(Contents), ObjectModelHelpers.MSBuildDefaultToolsVersion, _projectCollection, false); + projectFullPath = project.FullPath; + + BuildRequestData data = new BuildRequestData(project.CreateProjectInstance(), new string[0], _projectCollection.HostServices); + BuildParameters customparameters = new BuildParameters(); + customparameters.EnableNodeReuse = false; + buildParametersModifier(customparameters); + + BuildResult result = _buildManager.Build(customparameters, data); + TargetResult targetresult = result.ResultsByTarget["test"]; + ITaskItem[] item = targetresult.Items; + + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + Assert.AreEqual(3, item.Length); + int processId; + Assert.IsTrue(int.TryParse(item[2].ItemSpec, out processId), "Process ID passed from the 'test' target is not a valid integer (actual is '{0}')", item[2].ItemSpec); + Assert.AreNotEqual(System.Diagnostics.Process.GetCurrentProcess().Id, processId, "Build is expected to be out-of-proc. In fact it was in-proc."); + } + finally + { + if (projectFullPath != null && File.Exists(projectFullPath)) + { + File.Delete(projectFullPath); + } + + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + Environment.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", originalMsBuildEnableAllPropertyFunctions); + } + } + + /// + /// Make sure when we are doign an inprocess build that even if the environment variable msbuildforwardpropertiesfromchild is set that we still + /// get all of the initial properties. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void InProcForwardPropertiesFromChild() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MSBuildForwardPropertiesFromChild"); + + try + { + Environment.SetEnvironmentVariable("MSBuildForwardPropertiesFromChild", "InitialProperty2;IAMNOTREAL"); + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + + string propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty1", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty1", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty2", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty2", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty3", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty3", StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("MSBuildForwardPropertiesFromChild", originalEnvironmentValue); + } + } + + /// + /// Make sure when we are doign an inprocess build that even if the environment variable MsBuildForwardAllPropertiesFromChild is set that we still + /// get all of the initial properties. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void InProcMsBuildForwardAllPropertiesFromChild() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild"); + + try + { + Environment.SetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild", "InitialProperty2;IAMNOTREAL"); + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + + string propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty1", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty1", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty2", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty2", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty3", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty3", StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild", originalEnvironmentValue); + } + } + + /// + /// Make sure when we launch a child node and set MsBuildForwardAllPropertiesFromChild that we get all of our properties. This needs to happen + /// even if the msbuildforwardpropertiesfromchild is set to something. + /// + [TestMethod] + public void MsBuildForwardAllPropertiesFromChildLaunchChildNode() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild"); + string originalForwardPropertiesFromChild = Environment.GetEnvironmentVariable("MsBuildForwardPropertiesFromChild"); + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string tempFile = null; + try + { + Environment.SetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild", "InitialProperty2;IAMNOTREAL"); + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "Something"); + + Project project = CreateProject(contents, null, _projectCollection, false); + tempFile = project.FullPath; + BuildRequestData data = new BuildRequestData(tempFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + + string propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty1", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty1", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty2", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty2", StringComparison.OrdinalIgnoreCase)); + + propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty3", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty3", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + Environment.SetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild", originalEnvironmentValue); + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", originalForwardPropertiesFromChild); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + } + } + + /// + /// Make sure when if the environment variable MsBuildForwardPropertiesFromChild is set to a value and + /// we launch a child node that we get only that value. + /// + [TestMethod] + public void OutOfProcNodeForwardCertainproperties() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MsBuildForwardPropertiesFromChild"); + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string tempFile = null; + try + { + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "InitialProperty3;IAMNOTREAL"); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + Project project = CreateProject(contents, null, _projectCollection, false); + tempFile = project.FullPath; + BuildRequestData data = new BuildRequestData(tempFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + + Assert.IsTrue(properties.Count == 1); + + string propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty3", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty3", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", originalEnvironmentValue); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + } + } + + /// + /// Make sure when if the environment variable MsBuildForwardPropertiesFromChild is set to a value and + /// we launch a child node that we get only that value. Also, make sure that when a project is pulled from the results cache + /// and we have a list of properties to serialize that we do not crash. This is to prevent a regression of 826594 + /// + [TestMethod] + public void OutOfProcNodeForwardCertainpropertiesAlsoGetResultsFromCache() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MsBuildForwardPropertiesFromChild"); + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string tempFile = null; + string tempProject = Path.Combine(Path.GetTempPath(), "OutOfProcNodeForwardCertainpropertiesAlsoGetResultsFromCache.proj"); + + try + { + File.WriteAllText(tempProject, projectContents); + + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "InitialProperty3;IAMNOTREAL"); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + Project project = CreateProject(contents, null, _projectCollection, false); + tempFile = project.FullPath; + BuildRequestData data = new BuildRequestData(tempFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 3); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[1]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + + Assert.IsTrue(properties.Count == 1); + + string propertyValue = null; + Assert.IsTrue(properties.TryGetValue("InitialProperty3", out propertyValue)); + Assert.IsTrue(String.Equals(propertyValue, "InitialProperty3", StringComparison.OrdinalIgnoreCase)); + + projectStartedEvent = _logger.ProjectStartedEvents[2]; + Assert.IsNull(projectStartedEvent.Properties); + } + finally + { + if (File.Exists(tempProject)) + { + File.Delete(tempProject); + } + + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", originalEnvironmentValue); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + } + } + + /// + /// Make sure when if the environment variable MsBuildForwardPropertiesFromChild is set to empty and + /// we launch a child node that we get no properties + /// + [TestMethod] + public void ForwardNoPropertiesLaunchChildNode() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MsBuildForwardPropertiesFromChild"); + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string tempFile = null; + try + { + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", ""); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + Project project = CreateProject(contents, null, _projectCollection, false); + tempFile = project.FullPath; + BuildRequestData data = new BuildRequestData(tempFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + Assert.IsNull(properties); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", originalEnvironmentValue); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + } + } + + /// + /// We want to pass the toolsets from the parent to the child nodes so that any custom toolsets + /// defined on the parent are also availiable on the child nodes for tasks which use the global project + /// collection + /// + [TestMethod] + public void VerifyCustomToolSetsPropigated() + { + string netFrameworkDirectory = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45); + + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + "); + + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string tempFile = null; + try + { + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + + ProjectCollection projectCollection = new ProjectCollection(); + Toolset newToolSet = new Toolset("CustomToolSet", "c:\\SomePath", projectCollection, null); + projectCollection.AddToolset(newToolSet); + + Project project = CreateProject(contents, null, projectCollection, false); + tempFile = project.FullPath; + + BuildRequestData data = new BuildRequestData(tempFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + + BuildParameters customParameters = new BuildParameters(projectCollection); + customParameters.Loggers = new ILogger[] { _logger }; + BuildResult result = _buildManager.Build(customParameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + } + } + + /// + /// When a child node is launched by default we should not send any properties. + /// we launch a child node that we get no properties + /// + [TestMethod] + public void ForwardNoPropertiesLaunchChildNodeDefault() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + string originalEnvironmentValue = Environment.GetEnvironmentVariable("MsBuildForwardPropertiesFromChild"); + string originalMsBuildNoInProcNode = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE"); + string tempFile = null; + try + { + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", null); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + Project project = CreateProject(contents, null, _projectCollection, false); + tempFile = project.FullPath; + + BuildRequestData data = new BuildRequestData(tempFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + + ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0]; + Dictionary properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties); + Assert.IsNull(properties); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + Environment.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", originalEnvironmentValue); + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", originalMsBuildNoInProcNode); + } + } + + /// + /// A simple failing build. + /// + [TestMethod] + public void SimpleBuildWithFailure() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertLogContains("[fail]"); + } + + /// + /// A build with a message, error and warning, verify that + /// we only get errors, warnings, and project started and finished when OnlyLogCriticalEvents is true + /// + [TestMethod] + public void SimpleBuildWithFailureAndWarningOnlyLogCriticalEventsTrue() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + + BuildRequestData data = GetBuildRequestData(contents); + _parameters.OnlyLogCriticalEvents = true; + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertLogContains("[fail]"); + _logger.AssertLogContains("[warn]"); + _logger.AssertLogDoesntContain("[message]"); + Assert.IsTrue(_logger.BuildStartedEvents.Count == 1); + Assert.IsTrue(_logger.BuildFinishedEvents.Count == 1); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + Assert.IsTrue(_logger.ProjectFinishedEvents.Count == 1); + Assert.IsTrue(_logger.TargetStartedEvents.Count == 0); + Assert.IsTrue(_logger.TargetFinishedEvents.Count == 0); + Assert.IsTrue(_logger.TaskStartedEvents.Count == 0); + Assert.IsTrue(_logger.TaskFinishedEvents.Count == 0); + } + + /// + /// A build with a message, error and warning, verify that + /// we only get errors, warnings, messages, task and target messages OnlyLogCriticalEvents is false + /// + [TestMethod] + public void SimpleBuildWithFailureAndWarningOnlyLogCriticalEventsFalse() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + + BuildRequestData data = GetBuildRequestData(contents); + _parameters.OnlyLogCriticalEvents = false; + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertLogContains("[fail]"); + _logger.AssertLogContains("[warn]"); + _logger.AssertLogContains("[message]"); + Assert.IsTrue(_logger.BuildStartedEvents.Count == 1); + Assert.IsTrue(_logger.BuildFinishedEvents.Count == 1); + Assert.IsTrue(_logger.ProjectStartedEvents.Count == 1); + Assert.IsTrue(_logger.ProjectFinishedEvents.Count == 1); + Assert.IsTrue(_logger.TargetStartedEvents.Count == 1); + Assert.IsTrue(_logger.TargetFinishedEvents.Count == 1); + Assert.IsTrue(_logger.TaskStartedEvents.Count == 3); + Assert.IsTrue(_logger.TaskFinishedEvents.Count == 3); + } + + /// + /// Submitting a synchronous build request before calling BeginBuild yields an InvalidOperationException. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void BuildRequestWithoutBegin() + { + BuildRequestData data = new BuildRequestData("foo", new Dictionary(), "2.0", new string[0], null); + BuildResult result = _buildManager.BuildRequest(data); + } + + /// + /// Pending a build request before calling BeginBuild yields an InvalidOperationException. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void PendBuildRequestWithoutBegin() + { + BuildRequestData data = new BuildRequestData("foo", new Dictionary(), "2.0", new string[0], null); + BuildSubmission submission = _buildManager.PendBuildRequest(data); + } + + /// + /// Calling EndBuild before BeginBuild yields an InvalidOperationException. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void EndWithoutBegin() + { + _buildManager.EndBuild(); + } + + [TestMethod] + public void DisposeAfterUse() + { + string project = FrameworkLocationHelper.PathToDotNetFrameworkV45 + "\\microsoft.common.targets"; + var globalProperties = new Dictionary(); + var targets = new string[0]; + var brd = new BuildRequestData(project, globalProperties, null, targets, new HostServices()); + using (var bm = new BuildManager()) + { + bm.Build(new BuildParameters(), brd); + } + } + + [TestMethod] + public void DisposeWithoutUse() + { + var bm = new BuildManager(); + bm.Dispose(); + } + + /// + /// Calling BeginBuild after BeginBuild has already been called yields an InvalidOperationException. + /// + [TestMethod] + public void OverlappingBegin() + { + try + { + bool exceptionCaught = false; + + try + { + _buildManager.BeginBuild(new BuildParameters()); + _buildManager.BeginBuild(new BuildParameters()); + } + catch (InvalidOperationException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught); + } + finally + { + // Call EndBuild to get us back into a state that approximates reasonable + _buildManager.EndBuild(); + } + } + + /// + /// Starting and ending a build without submitting any requests is valid. + /// + [TestMethod] + public void EmptyBuild() + { + _buildManager.BeginBuild(_parameters); + _buildManager.EndBuild(); + + Assert.AreEqual(0, _logger.ErrorCount); + Assert.AreEqual(0, _logger.WarningCount); + } + + /// + /// Calling EndBuild after it has already been called yields an InvalidOperationException. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ExtraEnds() + { + _buildManager.BeginBuild(new BuildParameters()); + _buildManager.EndBuild(); + _buildManager.EndBuild(); + } + + /// + /// Pending a request after EndBuild has been called yields an InvalidOperationException. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void PendBuildRequestAfterEnd() + { + BuildRequestData data = new BuildRequestData("foo", new Dictionary(), "2.0", new string[0], null); + _buildManager.BeginBuild(new BuildParameters()); + _buildManager.EndBuild(); + + BuildSubmission submission = _buildManager.PendBuildRequest(data); + } + + /// + /// Attempting a synchronous build when a build is in progress yields an InvalidOperationException. + /// + [TestMethod] + public void BuildDuringBuild() + { + try + { + bool exceptionCaught = false; + try + { + BuildRequestData data = new BuildRequestData("foo", new Dictionary(), "2.0", new string[0], null); + _buildManager.BeginBuild(new BuildParameters()); + _buildManager.Build(new BuildParameters(), data); + } + catch (InvalidOperationException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught); + } + finally + { + // Call EndBuild to get us back into a state that approximates reasonable + _buildManager.EndBuild(); + } + } + + /// + /// A sequential build. + /// + [TestMethod] + public void EndBuildBlocks() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + BuildRequestData data = GetBuildRequestData(contents); + _buildManager.BeginBuild(_parameters); + BuildSubmission submission1 = _buildManager.PendBuildRequest(data); + submission1.ExecuteAsync(null, null); + Assert.IsFalse(submission1.IsCompleted); + _buildManager.EndBuild(); + Assert.IsTrue(submission1.IsCompleted); + _logger.AssertLogContains("[success 1]"); + } + + /// + /// Validate that EndBuild can be called during a submission completion callback. + /// + [TestMethod] + public void EndBuildCalledWithinSubmissionCallback() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + BuildRequestData data = GetBuildRequestData(contents); + _buildManager.BeginBuild(_parameters); + BuildSubmission submission1 = _buildManager.PendBuildRequest(data); + AutoResetEvent callbackFinished = new AutoResetEvent(false); + submission1.ExecuteAsync + ( + delegate (BuildSubmission submission) + { + _buildManager.EndBuild(); + callbackFinished.Set(); + }, + null); + + // Wait for the build to finish + Assert.IsTrue(callbackFinished.WaitOne(5000), "Build is hung."); + + // EndBuild should now have been called, so invoking it again should give us an invalid operation error. + bool invalidOperationReceived = false; + try + { + _buildManager.EndBuild(); + } + catch (InvalidOperationException) + { + invalidOperationReceived = true; + } + + Assert.IsTrue(invalidOperationReceived); + } + + /// + /// A sequential build. + /// + [TestMethod] + public void SequentialBuild() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + string contents2 = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + BuildRequestData data = GetBuildRequestData(contents); + BuildRequestData data2 = GetBuildRequestData(contents2); + _buildManager.BeginBuild(_parameters); + BuildResult result = _buildManager.BuildRequest(data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + BuildResult result2 = _buildManager.BuildRequest(data2); + Assert.AreEqual(BuildResultCode.Success, result2.OverallResult); + _buildManager.EndBuild(); + + _logger.AssertLogContains("[success 1]"); + _logger.AssertLogContains("[success 2]"); + } + + /// + /// A sequential build. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void OverlappingBuildSubmissions() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + string contents2 = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + BuildRequestData data = GetBuildRequestData(contents); + BuildRequestData data2 = GetBuildRequestData(contents2); + _buildManager.BeginBuild(_parameters); + BuildSubmission submission1 = _buildManager.PendBuildRequest(data); + submission1.ExecuteAsync(null, null); + BuildResult result2 = _buildManager.BuildRequest(data2); + submission1.WaitHandle.WaitOne(); + BuildResult result = submission1.BuildResult; + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Success, result2.OverallResult); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + _logger.AssertLogContains("[success 1]"); + _logger.AssertLogContains("[success 2]"); + } + + /// + /// If two overlapping submissions are executed against the same project, with at least one + /// target involved that skipped, make sure that the second one successfully completes + /// (retrieved from the cache). + /// + [TestMethod] + [Timeout(10000)] // this initially caused a hang + public void OverlappingIdenticalBuildSubmissions() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + +"); + + BuildRequestData data = GetBuildRequestData(contents); + BuildRequestData data2 = new BuildRequestData(data.ProjectInstance, data.TargetNames.ToArray(), data.HostServices); + + _buildManager.BeginBuild(_parameters); + BuildSubmission submission1 = _buildManager.PendBuildRequest(data); + BuildSubmission submission2 = _buildManager.PendBuildRequest(data2); + + submission2.ExecuteAsync(null, null); + submission1.ExecuteAsync(null, null); + + submission1.WaitHandle.WaitOne(); + submission2.WaitHandle.WaitOne(); + + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Success, submission1.BuildResult.OverallResult); + Assert.AreEqual(BuildResultCode.Success, submission2.BuildResult.OverallResult); + } + + /// + /// With two overlapping submissions, the first of which skips a target and the second + /// of which should not, ensure that the second submission does not, in fact, skip + /// the target. (E.g. despite the fact that the target results are in the cache already + /// as 'skipped', ensure that we retry execution in case conditions have changed.) + /// + [TestMethod] + public void OverlappingBuildSubmissions_OnlyOneSucceeds() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + true + + + + + + + + + + false + + + + +"); + + BuildRequestData data = GetBuildRequestData(contents, new string[] { "A" }); + BuildRequestData data2 = new BuildRequestData(data.ProjectInstance, new string[] { "MaySkip" }, data.HostServices); + + _buildManager.BeginBuild(_parameters); + BuildSubmission submission1 = _buildManager.PendBuildRequest(data); + BuildSubmission submission2 = _buildManager.PendBuildRequest(data2); + + submission1.ExecuteAsync(null, null); + submission2.ExecuteAsync(null, null); + + submission1.WaitHandle.WaitOne(); + submission2.WaitHandle.WaitOne(); + + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Success, submission1.BuildResult.OverallResult); + Assert.AreEqual(BuildResultCode.Failure, submission2.BuildResult.OverallResult); + } + + /// + /// Calling EndBuild with an unexecuted submission. + /// + [TestMethod] + public void EndWithUnexecutedSubmission() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents, new string[] { }, ObjectModelHelpers.MSBuildDefaultToolsVersion); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + _buildManager.EndBuild(); + } + + /// + /// A cancelled build with a submssion which is not executed yet. + /// + [TestMethod] + public void CancelledBuildWithUnexecutedSubmission() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents, new string[] { }, ObjectModelHelpers.MSBuildDefaultToolsVersion); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + _buildManager.CancelAllSubmissions(); + _buildManager.EndBuild(); + } + + /// + /// A cancelled build + /// + [TestMethod] + public void CancelledBuild() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents, new string[] { }, ObjectModelHelpers.MSBuildDefaultToolsVersion); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + + asyncResult.ExecuteAsync(null, null); + DateTime startTime = DateTime.Now; + _buildManager.CancelAllSubmissions(); + asyncResult.WaitHandle.WaitOne(); + BuildResult result = asyncResult.BuildResult; + DateTime endTime = DateTime.Now; + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult, "Build should have failed."); + _logger.AssertLogDoesntContain("[fail]"); + } + + /// + /// A cancelled build which waits for the task to get started before cancelling. Because it is a 2.0 task, we should + /// wait until the task finishes normally (cancellation not supported.) + /// + [TestMethod] + public void CancelledBuildWithDelay20() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 != null) + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + asyncResult.ExecuteAsync(null, null); + + System.Threading.Thread.Sleep(500); + _buildManager.CancelAllSubmissions(); + asyncResult.WaitHandle.WaitOne(); + BuildResult result = asyncResult.BuildResult; + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult, "Build should have failed."); + _logger.AssertLogDoesntContain("[fail]"); + } + } + + /// + /// A cancelled build which waits for the task to get started before cancelling. Because it is a 2.0 task, we should + /// wait until the task finishes normally (cancellation not supported.) + /// + [TestMethod] + public void CancelledBuildInTaskHostWithDelay20() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 != null) + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents, new string[] { }, ObjectModelHelpers.MSBuildDefaultToolsVersion); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + asyncResult.ExecuteAsync(null, null); + + System.Threading.Thread.Sleep(500); + _buildManager.CancelAllSubmissions(); + asyncResult.WaitHandle.WaitOne(); + BuildResult result = asyncResult.BuildResult; + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult, "Build should have failed."); + _logger.AssertLogDoesntContain("[fail]"); + + // Task host should not have exited prematurely + _logger.AssertLogDoesntContain("MSB4217"); + } + } + + /// + /// A cancelled build which waits for the task to get started before cancelling. Because it is a 12.. task, we should + /// cancel the task and exit out after a short period wherein we wait for the task to exit cleanly. + /// + [TestMethod] + public void CancelledBuildWithDelay40() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents, new string[] { }, ObjectModelHelpers.MSBuildDefaultToolsVersion); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + asyncResult.ExecuteAsync(null, null); + + System.Threading.Thread.Sleep(500); + _buildManager.CancelAllSubmissions(); + asyncResult.WaitHandle.WaitOne(); + BuildResult result = asyncResult.BuildResult; + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult, "Build should have failed."); + _logger.AssertLogDoesntContain("[fail]"); + } + + /// + /// A cancelled build which waits for the task to get started before cancelling. Because it is a 12.0 task, we should + /// cancel the task and exit out after a short period wherein we wait for the task to exit cleanly. + /// + [TestMethod] + public void CancelledBuildInTaskHostWithDelay40() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents, new string[] { }, ObjectModelHelpers.MSBuildDefaultToolsVersion); + _buildManager.BeginBuild(_parameters); + BuildSubmission asyncResult = _buildManager.PendBuildRequest(data); + asyncResult.ExecuteAsync(null, null); + + System.Threading.Thread.Sleep(500); + _buildManager.CancelAllSubmissions(); + asyncResult.WaitHandle.WaitOne(); + BuildResult result = asyncResult.BuildResult; + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult, "Build should have failed."); + _logger.AssertLogDoesntContain("[fail]"); + + // Task host should not have exited prematurely + _logger.AssertLogDoesntContain("MSB4217"); + } + + /// + /// This test verifies that builds of the same project instance in sequence are permitted. + /// + [TestMethod] + public void SequentialBuildsOfTheSameProjectAllowed() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + +"); + Project project = CreateProject(contents, ObjectModelHelpers.MSBuildDefaultToolsVersion, _projectCollection, true); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + _buildManager.BeginBuild(_parameters); + BuildResult result1 = _buildManager.BuildRequest(new BuildRequestData(instance, new string[] { "target1" })); + BuildResult result2 = _buildManager.BuildRequest(new BuildRequestData(instance, new string[] { "target2" })); + _buildManager.EndBuild(); + + Assert.AreEqual(BuildResultCode.Success, result1.OverallResult); + Assert.IsTrue(result1.HasResultsForTarget("target1"), "Results for target1 missing"); + Assert.AreEqual(BuildResultCode.Success, result2.OverallResult); + Assert.IsTrue(result2.HasResultsForTarget("target2"), "Results for target2 missing"); + } + + /// + /// This test verifies that overlapping builds of the same project are allowed. + /// + [TestMethod] + public void OverlappingBuildsOfTheSameProjectDifferentTargetsAreAllowed() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + +"); + Project project = CreateProject(contents, ObjectModelHelpers.MSBuildDefaultToolsVersion, _projectCollection, true); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + _buildManager.BeginBuild(_parameters); + try + { + BuildSubmission submission = _buildManager.PendBuildRequest(new BuildRequestData(instance, new string[] { "target1" })); + submission.ExecuteAsync(null, null); + BuildResult result2 = _buildManager.BuildRequest(new BuildRequestData(project.CreateProjectInstance(), new string[] { "target2" })); + + submission.WaitHandle.WaitOne(); + var result1 = submission.BuildResult; + + Assert.AreEqual(BuildResultCode.Success, result1.OverallResult); + Assert.IsTrue(result1.HasResultsForTarget("target1"), "Results for target1 missing"); + Assert.AreEqual(BuildResultCode.Success, result2.OverallResult); + Assert.IsTrue(result2.HasResultsForTarget("target2"), "Results for target2 missing"); + } + finally + { + _buildManager.EndBuild(); + } + } + + /// + /// This test verifies that overlapping builds of the same project are allowed. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void OverlappingBuildsOfTheSameProjectSameTargetsAreAllowed() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + +"); + Project project = CreateProject(contents, ObjectModelHelpers.MSBuildDefaultToolsVersion, _projectCollection, true); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + _buildManager.BeginBuild(_parameters); + try + { + BuildSubmission submission = _buildManager.PendBuildRequest(new BuildRequestData(instance, new string[] { "target1" })); + submission.ExecuteAsync(null, null); + BuildResult result2 = _buildManager.BuildRequest(new BuildRequestData(project.CreateProjectInstance(), new string[] { "target1" })); + + submission.WaitHandle.WaitOne(); + var result1 = submission.BuildResult; + + Assert.AreEqual(BuildResultCode.Success, result1.OverallResult); + Assert.IsTrue(result1.HasResultsForTarget("target1"), "Results for target1 missing"); + Assert.AreEqual(BuildResultCode.Success, result2.OverallResult); + Assert.IsTrue(result2.HasResultsForTarget("target1"), "Results for target1 (second call) missing"); + } + finally + { + _buildManager.EndBuild(); + } + } + + /// + /// This test verifies that the out-of-proc node won't lock the directory containing the target project. + /// + [TestMethod] + public void OutOfProcNodeDoesntLockWorkingDirectory() + { + string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + string projectFile = Path.Combine(tempDir, "foo.proj"); + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + Directory.CreateDirectory(tempDir); + File.WriteAllText(projectFile, contents); + + try + { + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"); + BuildRequestData data = new BuildRequestData(projectFile, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[] { }, null); + _buildManager.Build(_parameters, data); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDNOINPROCNODE", String.Empty); + } + + Directory.Delete(tempDir, true); + Assert.IsFalse(Directory.Exists(tempDir), "Temp directory should no longer exist."); + } + + /// + /// Retrieving a ProjectInstance from the BuildManager stores it in the cache + /// + [TestMethod] + public void ProjectInstanceStoredInCache() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + Project project = CreateProject(contents, ObjectModelHelpers.MSBuildDefaultToolsVersion, _projectCollection, true); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + ProjectInstance instance2 = _buildManager.GetProjectInstanceForBuild(project); + + Assert.IsTrue(Object.ReferenceEquals(instance, instance2), "Instances don't match"); + } + + /// + /// Retrieving a ProjectInstance from the BuildManager after a build. + /// + [TestMethod] + public void ProjectInstanceRetrievedAfterBuildMatchesSourceProject() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + bar + + + + +"); + IBuildComponentHost host = _buildManager as IBuildComponentHost; + IConfigCache cache = host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + + Project project = _projectCollection.LoadProject(data.ProjectFullPath); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + Assert.AreEqual(instance.GetPropertyValue("Foo"), "bar"); + } + + /// + /// Retrieving a ProjectInstance after resetting the cache clears the instances. + /// + [TestMethod] + public void ResetCacheClearsInstances() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + bar + + + + +"); + IBuildComponentHost host = _buildManager as IBuildComponentHost; + IConfigCache cache = host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + + Project project = _projectCollection.LoadProject(data.ProjectFullPath); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + Assert.AreEqual("bar", instance.GetPropertyValue("Foo")); + + _buildManager.BeginBuild(_parameters); + _buildManager.EndBuild(); + + instance = _buildManager.GetProjectInstanceForBuild(project); + Assert.IsNull(instance.GetProperty("Foo")); + } + + /// + /// Retrieving a ProjectInstance after another build without resetting the cache keeps the existing instance + /// + [TestMethod] + public void DisablingCacheResetKeepsInstance() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + bar + + + + +"); + IBuildComponentHost host = _buildManager as IBuildComponentHost; + IConfigCache cache = host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + + Project project = _projectCollection.LoadProject(data.ProjectFullPath); + ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project); + Assert.AreEqual(instance.GetPropertyValue("Foo"), "bar"); + + _logger.ClearLog(); + _parameters.ResetCaches = false; + _buildManager.BeginBuild(_parameters); + result = _buildManager.BuildRequest(data); + _buildManager.EndBuild(); + + // We should have built the same instance, with the same results, so the target will be skipped. + string skippedMessage = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteSuccess", "test"); + Assert.AreEqual(true, _logger.FullLog.Contains(skippedMessage)); + + ProjectInstance instance2 = _buildManager.GetProjectInstanceForBuild(project); + Assert.IsTrue(Object.ReferenceEquals(instance, instance2), "Instances are not the same"); + } + + /// + /// Retrieving a ProjectInstance after another build without resetting the cache keeps the existing instance + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void GhostProjectRootElementCache() + { + string contents1 = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + +"); + + string contents2 = ObjectModelHelpers.CleanupFileContents(@" + + + Baz + + + + +"); + IBuildComponentHost host = _buildManager as IBuildComponentHost; + IConfigCache cache = host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache; + + // Create Project 1 + ProjectInstance projectInstance = CreateProjectInstance(contents1, null, _projectCollection, false); + BuildRequestData data = new BuildRequestData(projectInstance, new string[0]); + + _logger.ClearLog(); + string p2pProject = Path.Combine(Path.GetDirectoryName(data.ProjectFullPath), "Project2.proj"); + + try + { + // Write the second project to disk and load it into its own project collection + ProjectCollection projectCollection2 = new ProjectCollection(); + File.WriteAllText(p2pProject, contents2); + + Project project2 = projectCollection2.LoadProject(p2pProject); + + _parameters.ResetCaches = false; + + // Build the first project to make sure we get the expected default values out for the p2p call. + _parameters.ProjectRootElementCache = _projectCollection.ProjectRootElementCache; + _buildManager.BeginBuild(_parameters); + BuildResult result = _buildManager.BuildRequest(data); + _buildManager.EndBuild(); + + _logger.AssertLogContains("Value:Baz"); + _logger.ClearLog(); + + // Modify the property in the second project and save it to disk. + project2.SetProperty("Bar", "FOO"); + project2.Save(); + + // Create a new build. + ProjectInstance projectInstance2 = CreateProjectInstance(contents1, null, _projectCollection, false); + BuildRequestData data2 = new BuildRequestData(projectInstance2, new string[0]); + + // Build again. + _parameters.ResetCaches = false; + _buildManager.BeginBuild(_parameters); + result = _buildManager.BuildRequest(data2); + _buildManager.EndBuild(); + _logger.AssertLogContains("Value:FOO"); + } + finally + { + if (File.Exists(p2pProject)) + { + File.Delete(p2pProject); + } + } + } + + /// + /// Verifies that explicitly loaded projects' imports are all marked as also explicitly loaded. + /// + [TestMethod] + public void VerifyImportedProjectRootElementsInheritExplicitLoadFlag() + { + string contents1 = ObjectModelHelpers.CleanupFileContents(@" + + + + +"); + + string contents2 = ObjectModelHelpers.CleanupFileContents(@" + + + ImportedValue + + +"); + + using (TempFileCollection tfc = new TempFileCollection()) + { + string importedProjectPath = FileUtilities.GetTemporaryFile(); + string rootProjectPath = FileUtilities.GetTemporaryFile(); + tfc.AddFile(importedProjectPath, false); + tfc.AddFile(rootProjectPath, false); + File.WriteAllText(importedProjectPath, contents2); + File.WriteAllText(rootProjectPath, String.Format(CultureInfo.InvariantCulture, contents1, importedProjectPath)); + + var projectCollection = new ProjectCollection(); + + // Run a simple build just to prove that nothing is left in the cache. + BuildRequestData data = new BuildRequestData(rootProjectPath, ReadOnlyEmptyDictionary.Instance, null, new[] { "test" }, null); + _parameters.ResetCaches = true; + _parameters.ProjectRootElementCache = projectCollection.ProjectRootElementCache; + _buildManager.BeginBuild(_parameters); + BuildResult result = _buildManager.BuildRequest(data); + _buildManager.EndBuild(); + _buildManager.ResetCaches(); + + // The semantic of TryOpen is to only retrieve the PRE if it is already in the weak cache. + Assert.IsNull(Microsoft.Build.Construction.ProjectRootElement.TryOpen(rootProjectPath, projectCollection), "The built project shouldn't be in the cache anymore."); + Assert.IsNull(Microsoft.Build.Construction.ProjectRootElement.TryOpen(importedProjectPath, projectCollection), "The built project's import shouldn't be in the cache anymore."); + + Project project = projectCollection.LoadProject(rootProjectPath); + Microsoft.Build.Construction.ProjectRootElement preRoot, preImported; + Assert.IsNotNull(preRoot = Microsoft.Build.Construction.ProjectRootElement.TryOpen(rootProjectPath, projectCollection), "The root project file should be in the weak cache."); + Assert.IsNotNull(preImported = Microsoft.Build.Construction.ProjectRootElement.TryOpen(importedProjectPath, projectCollection), "The imported project file should be in the weak cache."); + Assert.IsTrue(preRoot.IsExplicitlyLoaded); + Assert.IsTrue(preImported.IsExplicitlyLoaded); + + // Run a simple build just to prove that it doesn't impact what is in the cache. + data = new BuildRequestData(rootProjectPath, ReadOnlyEmptyDictionary.Instance, null, new[] { "test" }, null); + _parameters.ResetCaches = true; + _parameters.ProjectRootElementCache = projectCollection.ProjectRootElementCache; + _buildManager.BeginBuild(_parameters); + result = _buildManager.BuildRequest(data); + _buildManager.EndBuild(); + _buildManager.ResetCaches(); + + // Now make sure they are still in the weak cache. Since they were loaded explictly before the build, the build shouldn't have unloaded them from the cache. + Assert.AreSame(preRoot, Microsoft.Build.Construction.ProjectRootElement.TryOpen(rootProjectPath, projectCollection), "The root project file should be in the weak cache after a build."); + Assert.AreSame(preImported, Microsoft.Build.Construction.ProjectRootElement.TryOpen(importedProjectPath, projectCollection), "The imported project file should be in the weak cache after a build."); + Assert.IsTrue(preRoot.IsExplicitlyLoaded); + Assert.IsTrue(preImported.IsExplicitlyLoaded); + + projectCollection.UnloadProject(project); + projectCollection.UnloadAllProjects(); + Assert.IsNull(Microsoft.Build.Construction.ProjectRootElement.TryOpen(rootProjectPath, projectCollection), "The unloaded project shouldn't be in the cache anymore."); + Assert.IsNull(Microsoft.Build.Construction.ProjectRootElement.TryOpen(importedProjectPath, projectCollection), "The unloaded project's import shouldn't be in the cache anymore."); + } + } + + /// + /// Verify that using a second BuildManager doesn't cause the system to crash. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void Regress251333() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + + // First a normal build + BuildRequestData data = GetBuildRequestData(contents); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(result.OverallResult, BuildResultCode.Success); + + // Now a build using a different build manager. + using (BuildManager newBuildManager = new BuildManager()) + { + BuildRequestData data2 = GetBuildRequestData(contents); + BuildResult result2 = newBuildManager.Build(_parameters, data); + Assert.AreEqual(result2.OverallResult, BuildResultCode.Success); + } + } + + /// + /// Verify that disabling the in-proc node doesn't cause projects which don't require it to fail. + /// + [TestMethod] + public void Regress239661() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + + string fileName = Path.GetTempFileName(); + try + { + File.WriteAllText(fileName, contents); + BuildRequestData data = new BuildRequestData(fileName, _projectCollection.GlobalProperties, ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[0], null); + _parameters.DisableInProcNode = true; + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + } + finally + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + } + + /// + /// Verify that disabling the in-proc node when a project requires it will cause the build to fail, but not crash. + /// + [TestMethod] + public void Regress239661_NodeUnavailable() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + InitialProperty1 + InitialProperty2 + InitialProperty3 + + + + + +"); + BuildRequestData data = GetBuildRequestData(contents); + _parameters.DisableInProcNode = true; + + // Require that this project build on the in-proc node, which will not be available. + data.HostServices.SetNodeAffinity(data.ProjectFullPath, NodeAffinity.InProc); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertLogDoesntContain("[success]"); + _logger.AssertLogContains("MSB4223"); + } + + /// + /// Ensures that properties and items are transferred to the out-of-proc node when an instance is used to start the build. + /// + [TestMethod] + public void ProjectInstanceTransfersToOOPNode() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + deleteme + unmodified + original + + + + + + + + + + + + + + + +"); + + string fileName = Path.GetTempFileName(); + File.WriteAllText(fileName, contents); + try + { + Project project = new Project(fileName); + ProjectInstance instance = project.CreateProjectInstance(); + instance.RemoveProperty("DeleteMe"); + instance.SetProperty("VirtualProp", "overridden"); + instance.SetProperty("NewProp", "new"); + instance.AddItem("Baz", "baz"); + instance.AddItem("Foo2", "foo21"); + foreach (var item in instance.Items) + { + if (item.EvaluatedInclude == "foo") + { + instance.RemoveItem(item); + break; + } + } + + BuildRequestData data = new BuildRequestData(instance, new string[0]); + + // Force this to build out-of-proc + _parameters.DisableInProcNode = true; + _buildManager.Build(_parameters, data); + _logger.AssertLogDoesntContain("[deleteme]"); + _logger.AssertLogContains("[overridden]"); + _logger.AssertLogContains("[unmodified]"); + _logger.AssertLogContains("[new]"); + _logger.AssertLogDoesntContain("[foo]"); + _logger.AssertLogContains("[foo2;foo21]"); + _logger.AssertLogContains("[baz]"); + } + finally + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + } + + /// + /// Ensures that a limited set of properties are transferred from a project instance to an OOP node. + /// + [TestMethod] + public void ProjectInstanceLimitedTransferToOOPNode() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + unmodified + original + + + + + + +"); + + string fileName = Path.GetTempFileName(); + File.WriteAllText(fileName, contents); + try + { + Project project = new Project(fileName); + ProjectInstance instance = project.CreateProjectInstance(); + instance.SetProperty("VirtualProp", "overridden"); + instance.SetProperty("Unmodified", "changed"); + + BuildRequestData data = new BuildRequestData(instance, new string[0], null, BuildRequestDataFlags.None, new string[] { "VirtualProp" }); + + // Force this to build out-of-proc + _parameters.DisableInProcNode = true; + _buildManager.Build(_parameters, data); + _logger.AssertLogContains("[overridden]"); + _logger.AssertLogContains("[unmodified]"); + _logger.AssertLogDoesntContain("[changed]"); + } + finally + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + } + + /// + /// Tests that cache files are created as expected and their lifetime is controlled appropriately. + /// + [TestMethod] + public void CacheLifetime() + { + const string ForceCaching = "MSBUILDDEBUGFORCECACHING"; + FileUtilities.ClearCacheDirectory(); + string forceCachingValue = Environment.GetEnvironmentVariable(ForceCaching); + try + { + Environment.SetEnvironmentVariable(ForceCaching, "1"); + string outerBuildCacheDirectory; + string innerBuildCacheDirectory; + + // Do a build with one build manager. + using (var outerBuildManager = new BuildManager()) + { + outerBuildCacheDirectory = BuildAndCheckCache(outerBuildManager, new string[] { }); + + // Do another build with a second build manager while the first still exists. Since both BuildManagers + // share a process-wide cache directory, we want to verify that they don't stomp on each other, either + // by accidentally sharing results, or by clearing them away. + using (var innerBuildManager = new BuildManager()) + { + innerBuildCacheDirectory = BuildAndCheckCache(innerBuildManager, new string[] { outerBuildCacheDirectory }); + + // Force the cache for this build manager (and only this build manager) to be cleared. It should leave + // behind the results from the other one. + innerBuildManager.ResetCaches(); + } + + Assert.IsFalse(Directory.Exists(innerBuildCacheDirectory), "Inner build cache directory still exists after inner build manager was disposed."); + Assert.IsTrue(Directory.Exists(outerBuildCacheDirectory), "Outer build cache directory doesn't exist after inner build manager was disposed."); + + // Force the cache for this build manager to be cleared. + outerBuildManager.ResetCaches(); + } + + Assert.IsFalse(Directory.Exists(outerBuildCacheDirectory), "Outer build cache directory still exists after outer build manager was disposed."); + } + finally + { + Environment.SetEnvironmentVariable(ForceCaching, forceCachingValue); + } + } + + /// + /// If there's a P2P that otherwise succeeds, but has an AfterTarget that errors out, the + /// overall build result -- and thus the return value of the MSBuild task -- should reflect + /// that failure. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void FailedAfterTargetInP2PShouldCauseOverallBuildFailure() + { + string projA = null; + string projB = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + + + +"; + + string contentsB = @" + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertNoWarnings(); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If there's a P2P that otherwise succeeds, but has an AfterTarget that errors out, the + /// overall build result -- and thus the return value of the MSBuild task -- should reflect + /// that failure. Specifically tests where there are multiple entrypoint targets with + /// AfterTargets, only one of which fails. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void FailedAfterTargetInP2PShouldCauseOverallBuildFailure_MultipleEntrypoints() + { + string projA = null; + string projB = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertNoWarnings(); + _logger.AssertLogContains("[Build]"); + _logger.AssertLogContains("[Build2]"); + _logger.AssertLogContains("[AT1]"); + _logger.AssertLogContains("[AT2]"); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If there's a P2P that otherwise succeeds, but has an AfterTarget that errors out, the + /// overall build result -- and thus the return value of the MSBuild task -- should reflect + /// that failure. This should also be true if the AfterTarget is an AfterTarget of the + /// entrypoint target. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void FailedNestedAfterTargetInP2PShouldCauseOverallBuildFailure() + { + string projA = null; + string projB = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + _logger.AssertNoWarnings(); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If a project is called into twice, with two different entrypoint targets that + /// depend on non-overlapping sets of targets, and the first fails, the second + /// should not inherit that failure if all the targets it calls succeed. + /// + [TestMethod] + public void NonOverlappingEntrypointTargetsShouldNotInfluenceEachOthersResults() + { + string projA = null; + string projB = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + + + + + +"; + + string contentsB = @" + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + Assert.AreEqual(1, _logger.ErrorCount); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + _buildManager.EndBuild(); + } + } + + /// + /// In a situation where we have two requests calling into the same project, with different entry point + /// targets, one of which depends on "A;B", the other of which depends on "B", which has a dependency of + /// its own on "A", that we still properly build. + /// + [TestMethod] + public void Regress473114() + { + string projA = null; + string projB = null; + string projC = null; + string projD = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + projC = FileUtilities.GetTemporaryFile(".proj"); + projD = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + + + + + + + +"; + + string contentsB = @" + + + + + + +"; + + string contentsC = @" + + + + + + + + + + + + + + + + + + +"; + + string contentsD = @" + + + + + + + + + +"; + + File.WriteAllText(projA, contentsA); + File.WriteAllText(projB, contentsB); + File.WriteAllText(projC, contentsC); + File.WriteAllText(projD, contentsD); + + _parameters.MaxNodeCount = 3; + _parameters.EnableNodeReuse = false; + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), "4.0", new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + if (projC != null) + { + FileUtilities.DeleteNoThrow(projC); + } + + if (projD != null) + { + FileUtilities.DeleteNoThrow(projD); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If two requests are made for the same project, and they call in with + /// just the right timing such that: + /// - request 1 builds for a while, reaches a P2P, and blocks + /// - request 2 starts building, skips for a while, reaches the above P2P, and + /// blocks waiting for request 1's results + /// - request 1 resumes building, errors, and exits + /// - request 2 resumes building + /// + /// Then request 2 should end up exiting in the same fashion. + /// + /// This simple test verifies that if there are two error targets in a row, the + /// second request will bail out where the first request did, as though it had + /// executed the target, rather than skipping and continuing. + /// + [TestMethod] + public void VerifyMultipleRequestForSameProjectWithErrors_Simple() + { + string projA = null; + string projB = null; + string projC = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + projC = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + +"; + + string contentsC = @" + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + File.WriteAllText(projC, ObjectModelHelpers.CleanupFileContents(contentsC)); + + _parameters.MaxNodeCount = 2; + _parameters.EnableNodeReuse = false; + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + + // We should never get to Error2, because it's supposed to execute after Error1, which failed. + _logger.AssertLogDoesntContain("Error 2"); + + // We should, however, end up skipping Error1 on the second call to B. + string skippedMessage = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteFailure", "Error1"); + _logger.AssertLogContains(skippedMessage); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + if (projC != null) + { + FileUtilities.DeleteNoThrow(projC); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If two requests are made for the same project, and they call in with + /// just the right timing such that: + /// - request 1 builds for a while, reaches a P2P, and blocks + /// - request 2 starts building, skips for a while, reaches the above P2P, and + /// blocks waiting for request 1's results + /// - request 1 resumes building, errors, and exits + /// - request 2 resumes building + /// + /// Then request 2 should end up exiting in the same fashion. + /// + /// This simple test verifies that if there are two error targets in a row, and the + /// first has a chain of OnError targets, the OnError targets will all execute as + /// expected in the first request, but be skipped by the second (since if it's "skipping + /// unsuccessful", it can assume that all other OnError targets have also already been run) + /// + [TestMethod] + public void VerifyMultipleRequestForSameProjectWithErrors_OnErrorChain() + { + string projA = null; + string projB = null; + string projC = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + projC = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + string contentsC = @" + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + File.WriteAllText(projC, ObjectModelHelpers.CleanupFileContents(contentsC)); + + _parameters.MaxNodeCount = 2; + _parameters.EnableNodeReuse = false; + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + + // We should never get to Error2, because it's supposed to execute after Error1, which failed. + _logger.AssertLogDoesntContain("Error 2"); + + // We should, however, get to Target2, Target3, and Target4, since they're part of the OnError + // chain for Error1 + _logger.AssertLogContains("Error in Target2"); + _logger.AssertLogContains("Target 3"); + _logger.AssertLogContains("Target 4"); + + // We should end up skipping Error1 on the second call to B. + string skippedMessage1 = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteFailure", "Error1"); + _logger.AssertLogContains(skippedMessage1); + + // We shouldn't, however, see skip messages for the OnError targets + string skippedMessage2 = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteFailure", "Target2"); + _logger.AssertLogDoesntContain(skippedMessage2); + + string skippedMessage3 = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteSuccess", "Target3"); + _logger.AssertLogDoesntContain(skippedMessage3); + + string skippedMessage4 = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteSuccess", "Target4"); + _logger.AssertLogDoesntContain(skippedMessage4); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + if (projC != null) + { + FileUtilities.DeleteNoThrow(projC); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If two requests are made for the same project, and they call in with + /// just the right timing such that: + /// - request 1 builds for a while, reaches a P2P, and blocks + /// - request 2 starts building, skips for a while, reaches the above P2P, and + /// blocks waiting for request 1's results + /// - request 1 resumes building, errors, and exits + /// - request 2 resumes building + /// + /// Then request 2 should end up exiting in the same fashion. + /// + /// This simple test verifies that if there are two error targets in a row, AND + /// they're marked as ContinueOnError=ErrorAndContinue, then we won't bail, but + /// will continue executing (on the first request) or skipping (on the second) + /// + [TestMethod] + public void VerifyMultipleRequestForSameProjectWithErrors_ErrorAndContinue() + { + string projA = null; + string projB = null; + string projC = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + projC = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + +"; + + string contentsC = @" + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + File.WriteAllText(projC, ObjectModelHelpers.CleanupFileContents(contentsC)); + + _parameters.MaxNodeCount = 2; + _parameters.EnableNodeReuse = false; + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + + // We should see both Error1 and Error2 + _logger.AssertLogContains("Error 1"); + _logger.AssertLogContains("Error 2"); + + // We should also end up skipping them both. + string skippedMessage1 = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteFailure", "Error1"); + _logger.AssertLogContains(skippedMessage1); + + string skippedMessage2 = ResourceUtilities.FormatResourceString("TargetAlreadyCompleteFailure", "Error2"); + _logger.AssertLogContains(skippedMessage2); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + if (projC != null) + { + FileUtilities.DeleteNoThrow(projC); + } + + _buildManager.EndBuild(); + } + } + + /// + /// If two requests are made for the same project, and they call in with + /// just the right timing such that: + /// - request 1 builds for a while, reaches a P2P, and blocks + /// - request 2 starts building, skips for a while, reaches the above P2P, and + /// blocks waiting for request 1's results + /// - request 1 resumes building, errors, and exits + /// - request 2 resumes building + /// + /// Then request 2 should end up exiting in the same fashion. + /// + /// This test verifies that if the errors are in AfterTargets, we still + /// exit as though the target that those targets run after has already run. + /// + [TestMethod] + public void VerifyMultipleRequestForSameProjectWithErrors_AfterTargets() + { + string projA = null; + string projB = null; + string projC = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + projC = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + +"; + + string contentsC = @" + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + File.WriteAllText(projC, ObjectModelHelpers.CleanupFileContents(contentsC)); + + _parameters.MaxNodeCount = 2; + _parameters.EnableNodeReuse = false; + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + + // We should never get to Error2, because we should never run its AfterTarget, after + // the AfterTarget with Error1 failed + _logger.AssertLogDoesntContain("Error 2"); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + if (projC != null) + { + FileUtilities.DeleteNoThrow(projC); + } + + _buildManager.EndBuild(); + } + } + + /// + /// Related to the two tests above, if two requests are made for the same project, but + /// for different entry targets, and a target fails in the first request, if the second + /// request also runs that target, its skip-unsuccessful should behave in the same + /// way as if the target had actually errored. + /// + [TestMethod] + public void VerifyMultipleRequestForSameProjectWithErrors_DifferentEntrypoints() + { + string projA = null; + string projB = null; + + try + { + projA = FileUtilities.GetTemporaryFile(".proj"); + projB = FileUtilities.GetTemporaryFile(".proj"); + + string contentsA = @" + + + + Build + + + Build2 + + + + + + + +"; + + string contentsB = @" + + + + + + + + + + + + + + + + + + + + +"; + + File.WriteAllText(projA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projB, ObjectModelHelpers.CleanupFileContents(contentsB)); + + _buildManager.BeginBuild(_parameters); + BuildRequestData data = new BuildRequestData(projA, new Dictionary(), null, new[] { "Build" }, new HostServices()); + BuildResult result = _buildManager.PendBuildRequest(data).Execute(); + + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + // We should never get to Error2, because it's only ever executed in the second + // request after Error1, which should skip-unsuccessful and exit + _logger.AssertLogDoesntContain("[Error2]"); + } + finally + { + if (projA != null) + { + FileUtilities.DeleteNoThrow(projA); + } + + if (projB != null) + { + FileUtilities.DeleteNoThrow(projB); + } + + _buildManager.EndBuild(); + } + } + + /// + /// Verify that we can submit multiple simultaneous submissions with + /// legacy threading mode active and successfully build. + /// + [TestMethod] + public void TestSimultaneousSubmissionsWithLegacyThreadingData() + { + string projectPath1 = null; + string projectPath2 = null; + + try + { + string projectContent = @" + + + + + + +"; + projectPath1 = Path.GetTempFileName(); + File.WriteAllText(projectPath1, ObjectModelHelpers.CleanupFileContents(projectContent)); + + Project project1 = new Project(projectPath1); + + projectPath2 = Path.GetTempFileName(); + File.WriteAllText(projectPath2, ObjectModelHelpers.CleanupFileContents(projectContent)); + + Project project2 = new Project(projectPath2); + + ConsoleLogger cl = new ConsoleLogger(); + BuildParameters buildParameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + buildParameters.Loggers = new ILogger[] { cl }; + buildParameters.LegacyThreadingSemantics = true; + BuildManager.DefaultBuildManager.BeginBuild(buildParameters); + + AutoResetEvent project1DoneEvent = new AutoResetEvent(false); + ThreadPool.QueueUserWorkItem(delegate + { + ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project1); + + BuildRequestData requestData = new BuildRequestData(pi, new string[] { "Build" }); + BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData); + BuildResult br = submission.Execute(); + project1DoneEvent.Set(); + }); + + AutoResetEvent project2DoneEvent = new AutoResetEvent(false); + ThreadPool.QueueUserWorkItem(delegate + { + ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project2); + BuildRequestData requestData = new BuildRequestData(pi, new string[] { "Build" }); + BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData); + BuildResult br = submission.Execute(); + project2DoneEvent.Set(); + }); + + project1DoneEvent.WaitOne(); + project2DoneEvent.WaitOne(); + + BuildManager.DefaultBuildManager.EndBuild(); + } + finally + { + if (projectPath1 != null) + { + File.Delete(projectPath1); + } + + if (projectPath2 != null) + { + File.Delete(projectPath2); + } + } + } + + /// + /// Verify that we can submit multiple simultaneous submissions with + /// legacy threading mode active and successfully build, and that one of those + /// submissions can P2P to the other. (See Dev14 969114) + /// + [TestMethod] + public void TestSimultaneousSubmissionsWithLegacyThreadingData_P2P() + { + string projectPath1 = null; + string projectPath2 = null; + + try + { + string projectContent1 = @" + + + + + + + + + + + +"; + + projectPath1 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(projectPath1, ObjectModelHelpers.CleanupFileContents(projectContent1)); + + Project project1 = new Project(projectPath1); + + string projectContent2 = @" + + + + + + + +"; + + projectPath2 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(projectPath2, ObjectModelHelpers.CleanupFileContents(projectContent2)); + + Project project2 = new Project(projectPath2); + + ConsoleLogger cl = new ConsoleLogger(); + BuildParameters buildParameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + buildParameters.Loggers = new ILogger[] { cl }; + buildParameters.LegacyThreadingSemantics = true; + BuildManager.DefaultBuildManager.BeginBuild(buildParameters); + + AutoResetEvent project1DoneEvent = new AutoResetEvent(false); + ThreadPool.QueueUserWorkItem(delegate + { + // need to kick off project 2 first so that it project 1 can get submitted before the P2P happens + ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project2); + + BuildRequestData requestData = new BuildRequestData(pi, new string[] { "MSDeployPublish" }); + BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData); + BuildResult br = submission.Execute(); + Assert.AreEqual(BuildResultCode.Success, br.OverallResult); + project1DoneEvent.Set(); + }); + + AutoResetEvent project2DoneEvent = new AutoResetEvent(false); + ThreadPool.QueueUserWorkItem(delegate + { + ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project1); + BuildRequestData requestData = new BuildRequestData(pi, new string[] { "CopyRunEnvironmentFiles" }); + BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData); + BuildResult br = submission.Execute(); + Assert.AreEqual(BuildResultCode.Success, br.OverallResult); + project2DoneEvent.Set(); + }); + + project1DoneEvent.WaitOne(); + project2DoneEvent.WaitOne(); + + BuildManager.DefaultBuildManager.EndBuild(); + } + finally + { + if (projectPath1 != null) + { + File.Delete(projectPath1); + } + + if (projectPath2 != null) + { + File.Delete(projectPath2); + } + } + } + + /// + /// Verify that we can submit multiple simultaneous submissions with + /// legacy threading mode active and successfully build, and that one of those + /// submissions can P2P to the other. (See Dev14 969114) + /// + /// A variation of the above test, where multiple nodes are avaiable, so the + /// submissions aren't restricted to running strictly serially by the single in-proc + /// node. + /// + [TestMethod] + public void TestSimultaneousSubmissionsWithLegacyThreadingData_P2P_MP() + { + string projectPath1 = null; + string projectPath2 = null; + + try + { + string projectContent1 = @" + + + + + + + + + + + +"; + + projectPath1 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(projectPath1, ObjectModelHelpers.CleanupFileContents(projectContent1)); + + Project project1 = new Project(projectPath1); + + string projectContent2 = @" + + + + + + + +"; + + projectPath2 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(projectPath2, ObjectModelHelpers.CleanupFileContents(projectContent2)); + + Project project2 = new Project(projectPath2); + + ConsoleLogger cl = new ConsoleLogger(); + BuildParameters buildParameters = new BuildParameters(ProjectCollection.GlobalProjectCollection); + buildParameters.Loggers = new ILogger[] { cl }; + buildParameters.LegacyThreadingSemantics = true; + buildParameters.MaxNodeCount = 2; + BuildManager.DefaultBuildManager.BeginBuild(buildParameters); + + AutoResetEvent project1DoneEvent = new AutoResetEvent(false); + ThreadPool.QueueUserWorkItem(delegate + { + // need to kick off project 2 first so that it project 1 can get submitted before the P2P happens + ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project2); + + BuildRequestData requestData = new BuildRequestData(pi, new string[] { "MSDeployPublish" }); + BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData); + BuildResult br = submission.Execute(); + Assert.AreEqual(BuildResultCode.Success, br.OverallResult); + project1DoneEvent.Set(); + }); + + AutoResetEvent project2DoneEvent = new AutoResetEvent(false); + ThreadPool.QueueUserWorkItem(delegate + { + ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project1); + BuildRequestData requestData = new BuildRequestData(pi, new string[] { "CopyRunEnvironmentFiles" }); + BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData); + BuildResult br = submission.Execute(); + Assert.AreEqual(BuildResultCode.Success, br.OverallResult); + project2DoneEvent.Set(); + }); + + project1DoneEvent.WaitOne(); + project2DoneEvent.WaitOne(); + + BuildManager.DefaultBuildManager.EndBuild(); + } + finally + { + if (projectPath1 != null) + { + File.Delete(projectPath1); + } + + if (projectPath2 != null) + { + File.Delete(projectPath2); + } + } + } + + /// + /// Ensures that properties and items are transferred from an out-of-proc project to an in-proc project. + /// + /// + /// This differs from transferring a project instance to an out-of-proc node because in this case the project + /// was loaded by MSBuild, not supplied directly by the user. + /// + [TestMethod] + public void Regress265010() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + BaseValue + + + + + + + + + NewValue + + + + + + + + + + + + +"); + + string fileName = Path.GetTempFileName(); + File.WriteAllText(fileName, contents); + _buildManager.BeginBuild(_parameters); + try + { + HostServices services = new HostServices(); + services.SetNodeAffinity(fileName, NodeAffinity.OutOfProc); + BuildRequestData data = new BuildRequestData(fileName, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new[] { "BaseTest" }, services); + _buildManager.PendBuildRequest(data).Execute(); + _logger.AssertLogContains("[BaseValue]"); + _logger.AssertLogContains("[BaseItem]"); + _logger.ClearLog(); + + _parameters.ResetCaches = false; + services.SetNodeAffinity(fileName, NodeAffinity.InProc); + data = new BuildRequestData(fileName, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new[] { "MovedTest" }, services); + _buildManager.PendBuildRequest(data).Execute(); + _logger.AssertLogContains("[NewValue]"); + _logger.AssertLogContains("[BaseItem;NewItem]"); + _logger.AssertLogDoesntContain("[BaseValue]"); + } + finally + { + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + _buildManager.EndBuild(); + } + } + + /// + /// Helper for cache tests. Builds a project and verifies the right cache files are created. + /// + private string BuildAndCheckCache(BuildManager localBuildManager, IEnumerable exceptCacheDirectories) + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + +"); + string fileName = Path.GetTempFileName(); + File.WriteAllText(fileName, contents); + + string cacheDirectory = FileUtilities.GetCacheDirectory(); + + BuildParameters parameters = new BuildParameters(); + localBuildManager.BeginBuild(parameters); + try + { + var services = new HostServices(); + BuildRequestData data = new BuildRequestData(fileName, new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new[] { "One", "Two", "Three" }, services); + var result = localBuildManager.PendBuildRequest(data).Execute(); + Assert.IsTrue(result.OverallResult == BuildResultCode.Success, "Test project failed to build correctly."); + } + finally + { + localBuildManager.EndBuild(); + } + + // Ensure that we got the cache files we expected. There should be one set of results in there, once we exclude + // any of the specified directories from previous builds in the same test. + string directory = Directory.EnumerateDirectories(cacheDirectory).Except(exceptCacheDirectories).First(); + + // Within this directory should be a set of target results files, one for each of the targets we invoked. + var resultsFiles = Directory.EnumerateFiles(directory).Select(path => Path.GetFileName(path)); + Assert.IsTrue(resultsFiles.Count() == 3, "Expected 3 results, got {0}", resultsFiles.Count()); + Assert.IsTrue(resultsFiles.Contains("One.cache")); + Assert.IsTrue(resultsFiles.Contains("Two.cache")); + Assert.IsTrue(resultsFiles.Contains("Three.cache")); + + // Return the cache directory created for this build. + return directory; + } + + /// + /// Extract a string dictionary from the property enumeration on a project started event. + /// + private Dictionary ExtractProjectStartedPropertyList(IEnumerable properties) + { + // Gather a sorted list of all the properties. + Dictionary list = null; + + if (properties != null) + { + foreach (DictionaryEntry prop in properties) + { + if (list == null) + { + list = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + list.Add((string)prop.Key, (string)prop.Value); + } + } + + return list; + } + + /// + /// Retrieves a BuildRequestData using the specified contents, default targets and an empty project collection. + /// + private BuildRequestData GetBuildRequestData(string projectContents) + { + return GetBuildRequestData(projectContents, new string[] { }); + } + + /// + /// Retrieves a BuildRequestData using the specified contents and targets with an empty project collection. + /// + private BuildRequestData GetBuildRequestData(string projectContents, string[] targets) + { + return GetBuildRequestData(projectContents, targets, null); + } + + /// + /// Retrieves a BuildRequestData using the specified contents, targets and project collection. + /// + private BuildRequestData GetBuildRequestData(string projectContents, string[] targets, string toolsVersion) + { + BuildRequestData data = new BuildRequestData(CreateProjectInstance(projectContents, toolsVersion, _projectCollection, true), targets, _projectCollection.HostServices); + return data; + } + + /// + /// Retrieve a ProjectInstance evaluated with the specified contents using the specified projectCollection + /// + private ProjectInstance CreateProjectInstance(string contents, string toolsVersion, ProjectCollection projectCollection, bool deleteTempProject) + { + Project project = CreateProject(contents, toolsVersion, projectCollection, deleteTempProject); + return project.CreateProjectInstance(); + } + + /// + /// Retrieve a Project with the specified contents using the specified projectCollection + /// + private Project CreateProject(string contents, string toolsVersion, ProjectCollection projectCollection, bool deleteTempProject) + { + Project project = new Project(XmlReader.Create(new StringReader(contents)), (IDictionary)null, toolsVersion, projectCollection); + project.FullPath = FileUtilities.GetTemporaryFile(); + + if (!deleteTempProject) + { + project.Save(); + } + + if (deleteTempProject) + { + File.Delete(project.FullPath); + } + + return project; + } + + /// + /// Generate dummy projects + /// + private ProjectInstance GenerateDummyProjects(string shutdownProjectDirectory, int parallelProjectCount, ProjectCollection projectCollection) + { + Directory.CreateDirectory(shutdownProjectDirectory); + + // Generate the project. It will have the following format. Setting the AdditionalProperties + // causes the projects to be built to be separate configs, which allows us to build the same project + // a bunch of times in parallel. + // + // + // + // + // p={incremented value} + // + // ... + // + // + // + // + // + // + // + // + string rootProjectPath = Path.Combine(shutdownProjectDirectory, String.Format(CultureInfo.InvariantCulture, "RootProj_{0}.proj", Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))); + ProjectRootElement rootProject = ProjectRootElement.Create(rootProjectPath, projectCollection); + + ProjectTargetElement buildTarget = rootProject.AddTarget("Build"); + ProjectTaskElement buildTask = buildTarget.AddTask("MSBuild"); + buildTask.SetParameter("Projects", "@(ProjectReference)"); + buildTask.SetParameter("BuildInParallel", "true"); + buildTask.SetParameter("Targets", "ChildBuild"); + + rootProject.AddTarget("ChildBuild"); + + IDictionary metadata = new Dictionary(1); + for (int i = 0; i < parallelProjectCount; i++) + { + // Add the ProjectReference item for this actual config. + metadata["AdditionalProperties"] = String.Format(CultureInfo.InvariantCulture, "p={0}", i); + rootProject.AddItem("ProjectReference", rootProjectPath, metadata); + } + + rootProject.Save(); + return new ProjectInstance(rootProject); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfigurationResponse_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfigurationResponse_Tests.cs new file mode 100644 index 00000000000..88c0b2cd010 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfigurationResponse_Tests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the BuildRequestConfigurationResponse class. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using System.IO; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for the BuildRequestConfigurationResponse class. + /// + [TestClass] + public class BuildRequestConfigurationResponse_Tests + { + /// + /// Validate the constructor takes any combination of arguments. It is not the purpose of this class to enforce + /// rules on configuration IDs. + /// + [TestMethod] + public void TestConstructor() + { + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(0, 0, 0); + BuildRequestConfigurationResponse response2 = new BuildRequestConfigurationResponse(0, 1, 0); + BuildRequestConfigurationResponse response3 = new BuildRequestConfigurationResponse(0, -1, 0); + BuildRequestConfigurationResponse response4 = new BuildRequestConfigurationResponse(1, 0, 0); + BuildRequestConfigurationResponse response5 = new BuildRequestConfigurationResponse(1, 1, 0); + BuildRequestConfigurationResponse response6 = new BuildRequestConfigurationResponse(1, -1, 0); + BuildRequestConfigurationResponse response7 = new BuildRequestConfigurationResponse(-1, 0, 0); + BuildRequestConfigurationResponse response8 = new BuildRequestConfigurationResponse(-1, 1, 0); + BuildRequestConfigurationResponse response9 = new BuildRequestConfigurationResponse(-1, -1, 0); + BuildRequestConfigurationResponse response10 = new BuildRequestConfigurationResponse(0, 0, 1); + BuildRequestConfigurationResponse response11 = new BuildRequestConfigurationResponse(0, 1, 0); + BuildRequestConfigurationResponse response12 = new BuildRequestConfigurationResponse(0, -1, -1); + BuildRequestConfigurationResponse response13 = new BuildRequestConfigurationResponse(1, 0, 1); + BuildRequestConfigurationResponse response14 = new BuildRequestConfigurationResponse(1, 1, 0); + BuildRequestConfigurationResponse response15 = new BuildRequestConfigurationResponse(1, -1, -1); + BuildRequestConfigurationResponse response16 = new BuildRequestConfigurationResponse(-1, 0, 1); + BuildRequestConfigurationResponse response17 = new BuildRequestConfigurationResponse(-1, 1, 0); + BuildRequestConfigurationResponse response18 = new BuildRequestConfigurationResponse(-1, -1, -1); + } + + /// + /// Test the NodeConfigurationId property + /// + [TestMethod] + public void TestNodeConfigurationId() + { + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(1, 0, 0); + Assert.AreEqual(1, response.NodeConfigurationId); + } + + /// + /// Test the GlobalConfigurationId property + /// + [TestMethod] + public void TestGlobalConfigurationId() + { + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(0, 1, 0); + Assert.AreEqual(1, response.GlobalConfigurationId); + } + + /// + /// Test the ResultsNodeId property + /// + [TestMethod] + public void TestResultsNodeId() + { + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(0, 1, 2); + Assert.AreEqual(2, response.ResultsNodeId); + } + + /// + /// Test the Serialize method + /// + [TestMethod] + public void TestTranslation() + { + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(1, 2, 3); + Assert.AreEqual(response.Type, NodePacketType.BuildRequestConfigurationResponse); + + ((INodePacketTranslatable)response).Translate(TranslationHelpers.GetWriteTranslator()); + + INodePacket deserializedPacket = BuildRequestConfigurationResponse.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + BuildRequestConfigurationResponse deserializedResponse = deserializedPacket as BuildRequestConfigurationResponse; + Assert.AreEqual(response.NodeConfigurationId, deserializedResponse.NodeConfigurationId); + Assert.AreEqual(response.GlobalConfigurationId, deserializedResponse.GlobalConfigurationId); + Assert.AreEqual(response.ResultsNodeId, deserializedResponse.ResultsNodeId); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs new file mode 100644 index 00000000000..ef6c41b064a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestConfiguration_Tests.cs @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using System.IO; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class BuildRequestConfiguration_Tests + { + [TestInitialize] + public void SetUp() + { + } + + [TestCleanup] + public void TearDown() + { + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorNullFile() + { + BuildRequestData config1 = new BuildRequestData(null, new Dictionary(), "toolsVersion", new string[0], null); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorNullProps() + { + BuildRequestData config1 = new BuildRequestData("file", null, "toolsVersion", new string[0], null); + } + + [TestMethod] + public void TestConstructor1() + { + BuildRequestData config1 = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestConstructorInvalidConfigId() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(1, data, "2.0"); + BuildRequestConfiguration config2 = config1.ShallowCloneWithNewId(0); + } + + [TestMethod] + public void TestConstructor2PositiveConfigId() + { + BuildRequestData config1 = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + new BuildRequestConfiguration(1, config1, "2.0"); + } + + [TestMethod] + public void TestConstructor2NegativeConfigId() + { + BuildRequestData config1 = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + new BuildRequestConfiguration(-1, config1, "2.0"); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructor2NullFile() + { + BuildRequestData config1 = new BuildRequestData(null, new Dictionary(), "toolsVersion", new string[0], null); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructor2NullProps() + { + BuildRequestData config1 = new BuildRequestData("file", null, "toolsVersion", new string[0], null); + } + + [TestMethod] + public void TestWasGeneratedByNode() + { + BuildRequestData data1 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(-1, data1, "2.0"); + Assert.IsTrue(config1.WasGeneratedByNode); + + BuildRequestData data2 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config2 = new BuildRequestConfiguration(1, data2, "2.0"); + Assert.IsFalse(config2.WasGeneratedByNode); + + BuildRequestData data3 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config3 = new BuildRequestConfiguration(data3, "2.0"); + Assert.IsFalse(config3.WasGeneratedByNode); + } + + [TestMethod] + public void TestDefaultConfigurationId() + { + BuildRequestData data1 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(-1, data1, "2.0"); + Assert.AreEqual(config1.ConfigurationId, -1); + + BuildRequestData data2 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config2 = new BuildRequestConfiguration(1, data2, "2.0"); + Assert.AreEqual(config2.ConfigurationId, 1); + + BuildRequestData data3 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config3 = new BuildRequestConfiguration(0, data3, "2.0"); + Assert.AreEqual(config3.ConfigurationId, 0); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestSetConfigurationIdBad() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(-1, data, "2.0"); + config1.ConfigurationId = -2; + } + + [TestMethod] + public void TestSetConfigurationIdGood() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(data, "2.0"); + Assert.AreEqual(config1.ConfigurationId, 0); + config1.ConfigurationId = 1; + Assert.AreEqual(config1.ConfigurationId, 1); + } + + [TestMethod] + public void TestGetFileName() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(data, "2.0"); + Assert.AreEqual(config1.ProjectFullPath, Path.GetFullPath("file")); + } + + [TestMethod] + public void TestGetToolsVersion() + { + BuildRequestData data1 = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(data1, "2.0"); + Assert.AreEqual(config1.ToolsVersion, "toolsVersion"); + } + + [TestMethod] + public void TestGetProperties() + { + Dictionary props = new Dictionary(); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(new BuildRequestData("file", props, "toolsVersion", new string[0], null), "2.0"); + + Assert.AreEqual(props.Count, Helpers.MakeList((IEnumerable)(config1.Properties)).Count); + } + + [TestMethod] + public void TestSetProjectGood() + { + BuildRequestData data1 = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(data1, "2.0"); + Assert.IsNull(config1.Project); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@"")))); + + ProjectInstance projectInstance = project.CreateProjectInstance(); + config1.Project = projectInstance; + Assert.AreSame(config1.Project, projectInstance); + } + + [TestMethod] + public void TestPacketType() + { + BuildRequestData data1 = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + BuildRequestConfiguration config1 = new BuildRequestConfiguration(data1, "2.0"); + Assert.AreEqual(config1.Type, NodePacketType.BuildRequestConfiguration); + } + + [TestMethod] + public void TestGetHashCode() + { + BuildRequestConfiguration config1 = new BuildRequestConfiguration(new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null), "2.0"); + BuildRequestConfiguration config2 = new BuildRequestConfiguration(new BuildRequestData("File", new Dictionary(), "ToolsVersion", new string[0], null), "2.0"); + BuildRequestConfiguration config3 = new BuildRequestConfiguration(new BuildRequestData("file2", new Dictionary(), "toolsVersion", new string[0], null), "2.0"); + BuildRequestConfiguration config4 = new BuildRequestConfiguration(new BuildRequestData("file2", new Dictionary(), "toolsVersion2", new string[0], null), "2.0"); + BuildRequestConfiguration config5 = new BuildRequestConfiguration(new BuildRequestData("file", new Dictionary(), "toolsVersion2", new string[0], null), "2.0"); + + Assert.AreEqual(config1.GetHashCode(), config2.GetHashCode()); + Assert.AreNotEqual(config1.GetHashCode(), config3.GetHashCode()); + Assert.AreNotEqual(config1.GetHashCode(), config5.GetHashCode()); + Assert.AreNotEqual(config4.GetHashCode(), config5.GetHashCode()); + } + + [TestMethod] + public void TestEquals() + { + BuildRequestConfiguration config1 = new BuildRequestConfiguration(new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null), "2.0"); + Assert.AreEqual(config1, config1); + BuildRequestConfiguration config2 = new BuildRequestConfiguration(new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null), "2.0"); + Assert.AreEqual(config1, config2); + + BuildRequestConfiguration config3 = new BuildRequestConfiguration(new BuildRequestData("file2", new Dictionary(), "toolsVersion", new string[0], null), "2.0"); + Assert.AreNotEqual(config1, config3); + + BuildRequestConfiguration config4 = new BuildRequestConfiguration(new BuildRequestData("file", new Dictionary(), "toolsVersion2", new string[0], null), "2.0"); + Assert.AreNotEqual(config1, config4); + + PropertyDictionary props = new PropertyDictionary(); + props.Set(ProjectPropertyInstance.Create("prop1", "value1")); + BuildRequestData data = new BuildRequestData("file", props.ToDictionary(), "toolsVersion", new string[0], null); + BuildRequestConfiguration config5 = new BuildRequestConfiguration(data, "2.0"); + Assert.AreNotEqual(config1, config5); + + Assert.IsTrue(config1 == config2); + Assert.IsTrue(config1 != config3); + } + + [TestMethod] + public void TestTranslation() + { + PropertyDictionary properties = new PropertyDictionary(); + properties.Set(ProjectPropertyInstance.Create("this", "that")); + properties.Set(ProjectPropertyInstance.Create("foo", "bar")); + + BuildRequestData data = new BuildRequestData("file", properties.ToDictionary(), "4.0", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(data, "2.0"); + + Assert.AreEqual(NodePacketType.BuildRequestConfiguration, config.Type); + + ((INodePacketTranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = BuildRequestConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + BuildRequestConfiguration deserializedConfig = packet as BuildRequestConfiguration; + + Assert.AreEqual(config, deserializedConfig); + } + + [TestMethod] + public void TestProperties() + { + BuildRequestConfiguration configuration = new BuildRequestConfiguration(new BuildRequestData("path", new Dictionary(), "2.0", new string[] { }, null), "2.0"); + Assert.IsTrue(configuration.IsCacheable); + Assert.IsFalse(configuration.IsLoaded); + Assert.IsFalse(configuration.IsCached); + Assert.IsFalse(configuration.IsActivelyBuilding); + } + + [TestMethod] + public void TestCache() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + 1 + 2 + $(ThreeIn) + + + + + 1 + + + + + + + + + + + + + + + + + + +"); + + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties["ThreeIn"] = "3"; + globalProperties["BazIn"] = "bazfile"; + + Project project = new Project(XmlReader.Create(new StringReader(projectBody)), globalProperties, "4.0", new ProjectCollection()); + project.FullPath = "foo"; + ProjectInstance instance = project.CreateProjectInstance(); + BuildRequestConfiguration configuration = new BuildRequestConfiguration(new BuildRequestData(instance, new string[] { }, null), "2.0"); + configuration.ConfigurationId = 1; + + string originalValue = Environment.GetEnvironmentVariable("MSBUILDCACHE"); + try + { + Environment.SetEnvironmentVariable("MSBUILDCACHE", "1"); + Assert.AreEqual("3", instance.GlobalProperties["ThreeIn"]); + Assert.AreEqual("bazfile", instance.GlobalProperties["BazIn"]); + Assert.AreEqual("1", instance.PropertiesToBuildWith["One"].EvaluatedValue); + Assert.AreEqual("2", instance.PropertiesToBuildWith["Two"].EvaluatedValue); + Assert.AreEqual("3", instance.PropertiesToBuildWith["Three"].EvaluatedValue); + + int fooCount = instance.ItemsToBuildWith["Foo"].Count; + Assert.IsTrue(fooCount > 0); + Assert.AreEqual(1, instance.ItemsToBuildWith["Bar"].Count); + Assert.AreEqual(1, instance.ItemsToBuildWith["Baz"].Count); + Assert.AreEqual("bazfile", instance.ItemsToBuildWith["Baz"].First().EvaluatedInclude); + + Lookup lookup = configuration.BaseLookup; + + Assert.IsNotNull(lookup); + Assert.AreEqual(fooCount, lookup.GetItems("Foo").Count); + + // Configuration initialized with a ProjectInstance should not be cacheable by default. + Assert.IsFalse(configuration.IsCacheable); + configuration.IsCacheable = true; + configuration.CacheIfPossible(); + + Assert.IsNull(instance.GlobalPropertiesDictionary); + Assert.IsNull(instance.ItemsToBuildWith); + Assert.IsNull(instance.PropertiesToBuildWith); + + configuration.RetrieveFromCache(); + + Assert.AreEqual("3", instance.GlobalProperties["ThreeIn"]); + Assert.AreEqual("bazfile", instance.GlobalProperties["BazIn"]); + Assert.AreEqual("1", instance.PropertiesToBuildWith["One"].EvaluatedValue); + Assert.AreEqual("2", instance.PropertiesToBuildWith["Two"].EvaluatedValue); + Assert.AreEqual("3", instance.PropertiesToBuildWith["Three"].EvaluatedValue); + Assert.AreEqual(fooCount, instance.ItemsToBuildWith["Foo"].Count); + Assert.AreEqual(1, instance.ItemsToBuildWith["Bar"].Count); + Assert.AreEqual(1, instance.ItemsToBuildWith["Baz"].Count); + Assert.AreEqual("bazfile", instance.ItemsToBuildWith["Baz"].First().EvaluatedInclude); + + lookup = configuration.BaseLookup; + + Assert.IsNotNull(lookup); + Assert.AreEqual(fooCount, lookup.GetItems("Foo").Count); + } + finally + { + configuration.ClearCacheFile(); + Environment.SetEnvironmentVariable("MSBUILDCACHE", originalValue); + } + } + + [TestMethod] + public void TestCache2() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + 1 + 2 + $(ThreeIn) + + + + + 1 + + + + + + + + + + + + + + + + + + + "); + + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties["ThreeIn"] = "3"; + globalProperties["BazIn"] = "bazfile"; + + Project project = new Project(XmlReader.Create(new StringReader(projectBody)), globalProperties, "4.0", new ProjectCollection()); + project.FullPath = "foo"; + ProjectInstance instance = project.CreateProjectInstance(); + BuildRequestConfiguration configuration = new BuildRequestConfiguration(new BuildRequestData(instance, new string[] { }, null), "2.0"); + + string originalTmp = Environment.GetEnvironmentVariable("TMP"); + string originalTemp = Environment.GetEnvironmentVariable("TEMP"); + + try + { + string problematicTmpPath = @"C:\Users\}\blabla\temp"; + Environment.SetEnvironmentVariable("TMP", problematicTmpPath); + Environment.SetEnvironmentVariable("TEMP", problematicTmpPath); + + FileUtilities.ClearCacheDirectoryPath(); + string cacheFilePath = configuration.GetCacheFile(); + Assert.IsTrue(cacheFilePath.StartsWith(problematicTmpPath, StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("TMP", originalTmp); + Environment.SetEnvironmentVariable("TEMP", originalTemp); + FileUtilities.ClearCacheDirectoryPath(); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEngine_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEngine_Tests.cs new file mode 100644 index 00000000000..0c28bf9d9a8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEngine_Tests.cs @@ -0,0 +1,612 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.IO; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Unittest; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; + using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; + + [TestClass] + public class BuildRequestEngine_Tests + { + private delegate void EndpointOperationDelegate(NodeEndpointInProc endpoint); + + internal class MockRequestBuilder : IRequestBuilder, IBuildComponent + { + public bool ThrowExceptionOnRequest + { + get; + set; + } + public bool ThrowExceptionOnContinue + { + get; + set; + } + public bool ThrowExceptionOnCancel + { + get; + set; + } + public bool CompleteRequestSuccessfully + { + get; + set; + } + + public List NewRequests + { + get; + set; + } + + + private IBuildComponentHost _host; + private Thread _builderThread; + private BuildRequestEntry _entry; + private AutoResetEvent _continueEvent; + private AutoResetEvent _cancelEvent; + + public MockRequestBuilder() + { + ThrowExceptionOnRequest = false; + ThrowExceptionOnContinue = false; + ThrowExceptionOnCancel = false; + CompleteRequestSuccessfully = true; + NewRequests = new List(); + } + + #region IRequestBuilder Members + + public event NewBuildRequestsDelegate OnNewBuildRequests; + + public event BuildRequestCompletedDelegate OnBuildRequestCompleted; + + public event BuildRequestBlockedDelegate OnBuildRequestBlocked; + + public void BuildRequest(NodeLoggingContext context, BuildRequestEntry entry) + { + Assert.IsNull(_builderThread, "Received BuildRequest while one was in progress"); + + _continueEvent = new AutoResetEvent(false); + _cancelEvent = new AutoResetEvent(false); + _entry = entry; + entry.Continue(); + + _builderThread = new Thread(BuilderThreadProc); + _builderThread.Start(); + } + + private void Delay() + { + Thread.Sleep(1000); + } + + private void BuilderThreadProc() + { + _entry.RequestConfiguration.Project = CreateStandinProject(); + + if (ThrowExceptionOnRequest) + { + BuildResult errorResult = new BuildResult(_entry.Request, new InvalidOperationException("ContinueRequest not received in time.")); + _entry.Complete(errorResult); + RaiseRequestComplete(_entry); + return; + } + + bool completeSuccess = CompleteRequestSuccessfully; + + if (_cancelEvent.WaitOne(1000, false)) + { + BuildResult res = new BuildResult(_entry.Request, new BuildAbortedException()); + _entry.Complete(res); + RaiseRequestComplete(_entry); + return; + } + + for (int i = 0; i < NewRequests.Count; ++i) + { + OnNewBuildRequests(_entry, NewRequests[i]); + WaitHandle[] handles = new WaitHandle[2] { _cancelEvent, _continueEvent }; + int evt = WaitHandle.WaitAny(handles, 5000, false); + if (evt == 0) + { + BuildResult res = new BuildResult(_entry.Request, new BuildAbortedException()); + _entry.Complete(res); + RaiseRequestComplete(_entry); + return; + } + else if (evt == 1) + { + IDictionary results = _entry.Continue(); + foreach (BuildResult configResult in results.Values) + { + if (configResult.OverallResult == BuildResultCode.Failure) + { + completeSuccess = false; + break; + } + } + } + else + { + BuildResult errorResult = new BuildResult(_entry.Request, new InvalidOperationException("ContinueRequest not received in time.")); + _entry.Complete(errorResult); + RaiseRequestComplete(_entry); + return; + } + if (!completeSuccess) + { + break; + } + Delay(); + } + + BuildResult result = new BuildResult(_entry.Request); + + foreach (string target in _entry.Request.Targets) + { + result.AddResultsForTarget(target, new TargetResult(new TaskItem[1] { new TaskItem("include", _entry.RequestConfiguration.ProjectFullPath) }, completeSuccess ? TestUtilities.GetSuccessResult() : TestUtilities.GetStopWithErrorResult())); + } + _entry.Complete(result); + } + + public void RaiseRequestComplete(BuildRequestEntry entry) + { + if (null != OnBuildRequestCompleted) + { + OnBuildRequestCompleted(entry); + } + } + + public void RaiseRequestBlocked(BuildRequestEntry entry, int blockingId, string blockingTarget) + { + if (null != OnBuildRequestBlocked) + { + OnBuildRequestBlocked(entry, blockingId, blockingTarget); + } + } + + public void ContinueRequest() + { + if (ThrowExceptionOnContinue) + { + throw new InvalidOperationException("ThrowExceptionOnContinue set."); + } + _continueEvent.Set(); + } + + public void CancelRequest() + { + this.BeginCancel(); + this.WaitForCancelCompletion(); + } + + public void BeginCancel() + { + if (ThrowExceptionOnCancel) + { + throw new InvalidOperationException("ThrowExceptionOnCancel set."); + } + _cancelEvent.Set(); + } + + public void WaitForCancelCompletion() + { + if (!_builderThread.Join(5000)) + { + Assert.Fail("Builder thread did not terminate on cancel."); + _builderThread.Abort(); + } + } + + #endregion + + #region IBuildComponent Members + + public void InitializeComponent(IBuildComponentHost host) + { + _host = host; + } + + public void ShutdownComponent() + { + _host = null; + } + + #endregion + + private ProjectInstance CreateStandinProject() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + return project.CreateProjectInstance(); + } + } + + private MockHost _host; + + private AutoResetEvent _requestCompleteEvent; + private BuildRequest _requestComplete_Request; + private BuildResult _requestComplete_Result; + + private AutoResetEvent _requestResumedEvent; + private BuildRequest _requestResumed_Request; + + private AutoResetEvent _newRequestEvent; + private BuildRequestBlocker _newRequest_Request; + + private AutoResetEvent _engineStatusChangedEvent; + private BuildRequestEngineStatus _engineStatusChanged_Status; + + private AutoResetEvent _newConfigurationEvent; + private BuildRequestConfiguration _newConfiguration_Config; + + private AutoResetEvent _engineExceptionEvent; + private Exception _engineException_Exception; + + private IBuildRequestEngine _engine; + private IConfigCache _cache; + private int _nodeRequestId; + private int _globalRequestId; + + [TestInitialize] + public void SetUp() + { + _host = new MockHost(); + _nodeRequestId = 1; + _globalRequestId = 1; + _engineStatusChangedEvent = new AutoResetEvent(false); + _requestCompleteEvent = new AutoResetEvent(false); + _requestResumedEvent = new AutoResetEvent(false); + _newRequestEvent = new AutoResetEvent(false); + _newConfigurationEvent = new AutoResetEvent(false); + _engineExceptionEvent = new AutoResetEvent(false); + + _engine = (IBuildRequestEngine)_host.GetComponent(BuildComponentType.RequestEngine); + _cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + + ConfigureEngine(_engine); + } + + [TestCleanup] + public void TearDown() + { + if (_engine.Status != BuildRequestEngineStatus.Uninitialized) + { + _engine.CleanupForBuild(); + } + + ((IBuildComponent)_engine).ShutdownComponent(); + _engineStatusChangedEvent.Close(); + _requestCompleteEvent.Close(); + _requestResumedEvent.Close(); + _newRequestEvent.Close(); + _newConfigurationEvent.Close(); + _engineExceptionEvent.Close(); + + _host = null; + } + + private void ConfigureEngine(IBuildRequestEngine engine) + { + engine.OnNewConfigurationRequest += this.Engine_NewConfigurationRequest; + engine.OnRequestBlocked += this.Engine_NewRequest; + engine.OnRequestComplete += this.Engine_RequestComplete; + engine.OnRequestResumed += this.Engine_RequestResumed; + engine.OnStatusChanged += this.Engine_EngineStatusChanged; + engine.OnEngineException += this.Engine_Exception; + } + + /// + /// This test verifies that the engine properly shuts down even if there is an active build request. + /// This should cause that request to cancel and fail. + /// + [TestMethod] + public void TestEngineShutdownWhileActive() + { + BuildRequestData data = new BuildRequestData("TestFile", new Dictionary(), "TestToolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, "2.0"); + _cache.AddConfiguration(config); + + string[] targets = new string[3] { "target1", "target2", "target3" }; + BuildRequest request = CreateNewBuildRequest(1, targets); + + VerifyEngineStatus(BuildRequestEngineStatus.Uninitialized); + _engine.InitializeForBuild(new NodeLoggingContext(_host.LoggingService, 0, false)); + _engine.SubmitBuildRequest(request); + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Active); + + _engine.CleanupForBuild(); + + WaitForEvent(_requestCompleteEvent, "RequestComplete"); + Assert.AreEqual(request, _requestComplete_Request); + Assert.AreEqual(BuildResultCode.Failure, _requestComplete_Result.OverallResult); + VerifyEngineStatus(BuildRequestEngineStatus.Uninitialized); + } + + + /// + /// This test verifies that issuing a simple request results in a successful completion. + /// + [TestMethod] + public void TestSimpleBuildScenario() + { + BuildRequestData data = new BuildRequestData("TestFile", new Dictionary(), "TestToolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, "2.0"); + _cache.AddConfiguration(config); + + string[] targets = new string[3] { "target1", "target2", "target3" }; + BuildRequest request = CreateNewBuildRequest(1, targets); + + VerifyEngineStatus(BuildRequestEngineStatus.Uninitialized); + _engine.InitializeForBuild(new NodeLoggingContext(_host.LoggingService, 0, false)); + _engine.SubmitBuildRequest(request); + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Active); + + WaitForEvent(_requestCompleteEvent, "RequestComplete"); + Assert.AreEqual(request, _requestComplete_Request); + Assert.AreEqual(BuildResultCode.Success, _requestComplete_Result.OverallResult); + + VerifyEngineStatus(BuildRequestEngineStatus.Idle); + } + + /// + /// This test verifies that a project which has project dependencies can issue and consume them through the + /// engine interface. + /// + [TestMethod] + public void TestBuildWithChildren() + { + BuildRequestData data = new BuildRequestData("TestFile", new Dictionary(), "TestToolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, "2.0"); + _cache.AddConfiguration(config); + + // Configure the builder to spawn build requests + MockRequestBuilder builder = (MockRequestBuilder)_host.GetComponent(BuildComponentType.RequestBuilder); + builder.NewRequests.Add(new FullyQualifiedBuildRequest[1] { new FullyQualifiedBuildRequest(config, new string[1] { "requiredTarget1" }, true) }); + + // Create the initial build request + string[] targets = new string[3] { "target1", "target2", "target3" }; + BuildRequest request = CreateNewBuildRequest(1, targets); + + // Kick it off + VerifyEngineStatus(BuildRequestEngineStatus.Uninitialized); + _engine.InitializeForBuild(new NodeLoggingContext(_host.LoggingService, 0, false)); + _engine.SubmitBuildRequest(request); + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Active); + + // Wait for the new requests to be spawned by the builder + WaitForEvent(_newRequestEvent, "NewRequestEvent"); + Assert.AreEqual(1, _newRequest_Request.BuildRequests[0].ConfigurationId); + Assert.AreEqual(1, _newRequest_Request.BuildRequests[0].Targets.Count); + Assert.AreEqual("requiredTarget1", _newRequest_Request.BuildRequests[0].Targets[0]); + + // Wait for a moment, because the build request engine thread may not have gotten around + // to going to the waiting state. + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Waiting); + + // Report a result to satisfy the build request + BuildResult result = new BuildResult(_newRequest_Request.BuildRequests[0]); + result.AddResultsForTarget("requiredTarget1", TestUtilities.GetEmptySucceedingTargetResult()); + _engine.UnblockBuildRequest(new BuildRequestUnblocker(result)); + + // Continue the request. + _engine.UnblockBuildRequest(new BuildRequestUnblocker(request.GlobalRequestId)); + + // Wait for the original request to complete + WaitForEvent(_requestCompleteEvent, "RequestComplete"); + Assert.AreEqual(request, _requestComplete_Request); + Assert.AreEqual(BuildResultCode.Success, _requestComplete_Result.OverallResult); + + VerifyEngineStatus(BuildRequestEngineStatus.Idle); + } + + /// + /// This test verifies that a project can issue a build request with an unresolved configuration and that if we resolve it, + /// the build will continue and complete successfully. + /// + [TestMethod] + public void TestBuildWithNewConfiguration() + { + BuildRequestData data = new BuildRequestData(Path.GetFullPath("TestFile"), new Dictionary(), "TestToolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, "2.0"); + _cache.AddConfiguration(config); + + // Configure the builder to spawn build requests + MockRequestBuilder builder = (MockRequestBuilder)_host.GetComponent(BuildComponentType.RequestBuilder); + BuildRequestData data2 = new BuildRequestData(Path.GetFullPath("OtherFile"), new Dictionary(), "TestToolsVersion", new string[0], null); + BuildRequestConfiguration unresolvedConfig = new BuildRequestConfiguration(data2, "2.0"); + builder.NewRequests.Add(new FullyQualifiedBuildRequest[1] { new FullyQualifiedBuildRequest(unresolvedConfig, new string[1] { "requiredTarget1" }, true) }); + + // Create the initial build request + string[] targets = new string[3] { "target1", "target2", "target3" }; + BuildRequest request = CreateNewBuildRequest(1, targets); + + // Kick it off + VerifyEngineStatus(BuildRequestEngineStatus.Uninitialized); + _engine.InitializeForBuild(new NodeLoggingContext(_host.LoggingService, 0, false)); + _engine.SubmitBuildRequest(request); + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Active); + + // Wait for the request to generate the child request with the unresolved configuration + WaitForEvent(_newConfigurationEvent, "NewConfigurationEvent"); + Assert.AreEqual(Path.GetFullPath("OtherFile"), _newConfiguration_Config.ProjectFullPath); + Assert.AreEqual("TestToolsVersion", _newConfiguration_Config.ToolsVersion); + Assert.IsTrue(_newConfiguration_Config.WasGeneratedByNode); + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Waiting); + + // Resolve the configuration + BuildRequestConfigurationResponse response = new BuildRequestConfigurationResponse(_newConfiguration_Config.ConfigurationId, 2, 0); + _engine.ReportConfigurationResponse(response); + + // Now wait for the actual requests to be issued. + WaitForEvent(_newRequestEvent, "NewRequestEvent"); + Assert.AreEqual(2, _newRequest_Request.BuildRequests[0].ConfigurationId); + Assert.AreEqual(2, _newRequest_Request.BuildRequests[0].ConfigurationId); + Assert.AreEqual(1, _newRequest_Request.BuildRequests[0].Targets.Count); + Assert.AreEqual("requiredTarget1", _newRequest_Request.BuildRequests[0].Targets[0]); + + // Report a result to satisfy the build request + BuildResult result = new BuildResult(_newRequest_Request.BuildRequests[0]); + result.AddResultsForTarget("requiredTarget1", TestUtilities.GetEmptySucceedingTargetResult()); + _engine.UnblockBuildRequest(new BuildRequestUnblocker(result)); + + // Continue the request + _engine.UnblockBuildRequest(new BuildRequestUnblocker(request.GlobalRequestId)); + + // Wait for the original request to complete + WaitForEvent(_requestCompleteEvent, "RequestComplete"); + Assert.AreEqual(request, _requestComplete_Request); + Assert.AreEqual(BuildResultCode.Success, _requestComplete_Result.OverallResult); + Thread.Sleep(250); + VerifyEngineStatus(BuildRequestEngineStatus.Idle); + } + + [TestMethod] + public void TestShutdown() + { + } + + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + BuildRequest request = new BuildRequest(1 /* submission id */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + request.GlobalRequestId = _globalRequestId++; + return request; + } + + private void WaitForEngineStatus(BuildRequestEngineStatus expectedStatus) + { + DateTime time = DateTime.Now; + while (DateTime.Now - time > new TimeSpan(0, 0, 5)) + { + WaitForEvent(_engineStatusChangedEvent, "EngineStatusChanged"); + if (expectedStatus == _engineStatusChanged_Status) + { + return; + } + } + Assert.Fail("Engine failed to change to status " + expectedStatus); + } + + private void VerifyEngineStatus(BuildRequestEngineStatus expectedStatus) + { + IBuildRequestEngine engine = (IBuildRequestEngine)_host.GetComponent(BuildComponentType.RequestEngine); + + if (engine.Status == expectedStatus) + { + return; + } + + WaitForEvent(_engineStatusChangedEvent, "EngineStatusChanged"); + BuildRequestEngineStatus engineStatus = engine.Status; + Assert.IsTrue(expectedStatus == engineStatus, "Unexpected engine status " + engineStatus + ". Expected " + expectedStatus); + } + + private void WaitForEvent(WaitHandle evt, string eventName) + { + WaitHandle[] events = new WaitHandle[2] { _engineExceptionEvent, evt }; + int index = WaitHandle.WaitAny(events, 5000, false); + if (WaitHandle.WaitTimeout == index) + { + Assert.Fail("Did not receive " + eventName + " callback before the timeout expired."); + } + else if (index == 0) + { + Assert.Fail("Received engine exception " + _engineException_Exception); + } + } + + /// + /// Callback for event raised when a build request is completed + /// + /// The request which completed + /// The result for the request + private void Engine_RequestComplete(BuildRequest request, BuildResult result) + { + _requestComplete_Request = request; + _requestComplete_Result = result; + _requestCompleteEvent.Set(); + } + + /// + /// Callback for event raised when a request is resumed + /// + /// The request being resumed + private void Engine_RequestResumed(BuildRequest request) + { + _requestResumed_Request = request; + _requestResumedEvent.Set(); + } + + /// + /// Callback for event raised when a new build request is generated by an MSBuild callback + /// + /// The new build request + private void Engine_NewRequest(BuildRequestBlocker blocker) + { + _newRequest_Request = blocker; + _newRequestEvent.Set(); + } + + /// + /// Callback for event raised when the build request engine's status changes. + /// + /// The new status for the engine + private void Engine_EngineStatusChanged(BuildRequestEngineStatus newStatus) + { + _engineStatusChanged_Status = newStatus; + _engineStatusChangedEvent.Set(); + } + + /// + /// Callback for event raised when a new configuration needs an ID resolved. + /// + /// The configuration needing an ID + private void Engine_NewConfigurationRequest(BuildRequestConfiguration config) + { + _newConfiguration_Config = config; + _newConfigurationEvent.Set(); + } + + /// + /// Callback for event raised when a new configuration needs an ID resolved. + /// + /// The configuration needing an ID + private void Engine_Exception(Exception e) + { + _engineException_Exception = e; + _engineExceptionEvent.Set(); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEntry_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEntry_Tests.cs new file mode 100644 index 00000000000..ed3726ff422 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequestEntry_Tests.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using Microsoft.Build.Unittest; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class BuildRequestEntry_Tests + { + private int _nodeRequestId; + + [TestInitialize] + public void SetUp() + { + _nodeRequestId++; + } + + [TestCleanup] + public void TearDown() + { + } + + [TestMethod] + public void TestConstructorGood() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0] { }); + BuildRequestData data = new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + Assert.AreEqual(entry.Request, request); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorBad() + { + BuildRequestEntry entry = new BuildRequestEntry(null, null); + } + + [TestMethod] + public void TestSimpleStateProgression() + { + // Start in Ready + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + Assert.AreEqual(entry.Request, request); + Assert.IsNull(entry.Result); + + // Move to active. Should not be any results yet. + IDictionary results = entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + Assert.IsNull(entry.Result); + Assert.IsNull(results); + + // Wait for results, move to waiting. + BuildRequest waitingRequest = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + Assert.AreEqual(entry.Request, request); + Assert.IsNull(entry.Result); + + // Provide the results, move to ready. + BuildResult requiredResult = new BuildResult(waitingRequest); + requiredResult.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + Assert.AreEqual(entry.Request, request); + Assert.IsNull(entry.Result); + + // Continue the build, move to active. + results = entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + Assert.IsNull(entry.Result); + Assert.AreEqual(results.Count, 1); + Assert.IsTrue(results.ContainsKey(requiredResult.NodeRequestId)); + Assert.AreEqual(results[requiredResult.NodeRequestId], requiredResult); + + // Complete the build, move to completed. + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + entry.Complete(result); + Assert.AreEqual(entry.State, BuildRequestEntryState.Complete); + Assert.IsNotNull(entry.Result); + Assert.AreEqual(entry.Result, result); + } + + [TestMethod] + public void TestResolveConfiguration() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestData data1 = new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data1, "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + + entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + + BuildRequest waitingRequest = CreateNewBuildRequest(-1, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest); + + entry.ResolveConfigurationRequest(-1, 2); + + BuildResult requiredResult = new BuildResult(waitingRequest); + requiredResult.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + } + + [TestMethod] + public void TestMultipleWaitingRequests() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestData data1 = new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data1, "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + + entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + + BuildRequest waitingRequest1 = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest1); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildRequest waitingRequest2 = CreateNewBuildRequest(2, new string[1] { "xor" }); + entry.WaitForResult(waitingRequest2); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildResult requiredResult1 = new BuildResult(waitingRequest1); + requiredResult1.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult1); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildResult requiredResult2 = new BuildResult(waitingRequest2); + requiredResult2.AddResultsForTarget("xor", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult2); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + } + + [TestMethod] + public void TestMixedWaitingRequests() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + + entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + + BuildRequest waitingRequest1 = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest1); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildRequest waitingRequest2 = CreateNewBuildRequest(-1, new string[1] { "xor" }); + entry.WaitForResult(waitingRequest2); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + Assert.IsNull(entry.GetRequestsToIssueIfReady(), "Entry should not be ready to issue because there are unresolved configurations"); + + entry.ResolveConfigurationRequest(-1, 3); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildResult requiredResult1 = new BuildResult(waitingRequest1); + requiredResult1.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult1); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildResult requiredResult2 = new BuildResult(waitingRequest2); + requiredResult2.AddResultsForTarget("xor", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult2); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestNoReadyToWaiting() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestData data1 = new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data1, "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + + BuildRequest waitingRequest1 = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest1); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestNoReadyToComplete() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestData data1 = new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data1, "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + + BuildResult requiredResult = new BuildResult(request); + requiredResult.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + entry.Complete(requiredResult); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestNoWaitingToComplete() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestData data1 = new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data1, "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + + entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + + BuildRequest waitingRequest1 = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest1); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildResult requiredResult = new BuildResult(request); + requiredResult.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + entry.Complete(requiredResult); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestNoCompleteToWaiting() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + + entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + + BuildResult requiredResult = new BuildResult(request); + requiredResult.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + entry.Complete(requiredResult); + Assert.AreEqual(entry.State, BuildRequestEntryState.Complete); + + BuildRequest waitingRequest1 = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest1); + } + + [TestMethod] + public void TestResultsWithNoMatch1() + { + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "foo" }); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry entry = new BuildRequestEntry(request, config); + Assert.AreEqual(entry.State, BuildRequestEntryState.Ready); + + entry.Continue(); + Assert.AreEqual(entry.State, BuildRequestEntryState.Active); + + BuildRequest waitingRequest1 = CreateNewBuildRequest(2, new string[1] { "bar" }); + entry.WaitForResult(waitingRequest1); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + + BuildRequest randomRequest = CreateNewBuildRequest(3, new string[0]); + BuildResult requiredResult = new BuildResult(randomRequest); + requiredResult.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + entry.ReportResult(requiredResult); + Assert.AreEqual(entry.State, BuildRequestEntryState.Waiting); + } + + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequest_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequest_Tests.cs new file mode 100644 index 00000000000..d444e305231 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildRequest_Tests.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class BuildRequest_Tests + { + private int _nodeRequestId; + + [TestInitialize] + public void SetUp() + { + _nodeRequestId = 1; + } + + [TestCleanup] + public void TearDown() + { + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorBad() + { + BuildRequest request = CreateNewBuildRequest(0, null); + } + + [TestMethod] + public void TestConstructorGood() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + } + + [TestMethod] + public void TestConfigurationId() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + Assert.AreEqual(0, request.ConfigurationId); + + BuildRequest request2 = CreateNewBuildRequest(1, new string[0] { }); + Assert.AreEqual(1, request2.ConfigurationId); + + BuildRequest request3 = CreateNewBuildRequest(-1, new string[0] { }); + Assert.AreEqual(-1, request3.ConfigurationId); + } + + [TestMethod] + public void TestConfigurationResolved() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + Assert.IsFalse(request.IsConfigurationResolved); + + BuildRequest request2 = CreateNewBuildRequest(1, new string[0] { }); + Assert.IsTrue(request2.IsConfigurationResolved); + + BuildRequest request3 = CreateNewBuildRequest(-1, new string[0] { }); + Assert.IsFalse(request3.IsConfigurationResolved); + } + + [TestMethod] + public void TestTargets() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + Assert.IsNotNull(request.Targets); + Assert.AreEqual(0, request.Targets.Count); + + BuildRequest request2 = CreateNewBuildRequest(1, new string[1] { "a" }); + Assert.IsNotNull(request2.Targets); + Assert.AreEqual(1, request2.Targets.Count); + Assert.AreEqual("a", request2.Targets[0]); + } + + [TestMethod] + public void TestPacketType() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + Assert.AreEqual(NodePacketType.BuildRequest, request.Type); + } + + [TestMethod] + public void TestResolveConfigurationGood() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + request.ResolveConfiguration(1); + Assert.IsTrue(request.IsConfigurationResolved); + Assert.AreEqual(1, request.ConfigurationId); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestResolveConfigurationBad() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0] { }); + request.ResolveConfiguration(2); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestResolveConfigurationBad2() + { + BuildRequest request = CreateNewBuildRequest(0, new string[0] { }); + request.ResolveConfiguration(-1); + } + + [TestMethod] + public void TestTranslation() + { + BuildRequest request = CreateNewBuildRequest(1, new string[] { "alpha", "omega" }); + + Assert.AreEqual(NodePacketType.BuildRequest, request.Type); + + ((INodePacketTranslatable)request).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = BuildRequest.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + BuildRequest deserializedRequest = packet as BuildRequest; + + Assert.AreEqual(request.BuildEventContext, deserializedRequest.BuildEventContext); + Assert.AreEqual(request.ConfigurationId, deserializedRequest.ConfigurationId); + Assert.AreEqual(request.GlobalRequestId, deserializedRequest.GlobalRequestId); + Assert.AreEqual(request.IsConfigurationResolved, deserializedRequest.IsConfigurationResolved); + Assert.AreEqual(request.NodeRequestId, deserializedRequest.NodeRequestId); + Assert.AreEqual(request.ParentBuildEventContext, deserializedRequest.ParentBuildEventContext); + Assert.AreEqual(request.Targets.Count, deserializedRequest.Targets.Count); + for (int i = 0; i < request.Targets.Count; i++) + { + Assert.AreEqual(request.Targets[i], deserializedRequest.Targets[i]); + } + } + + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/BuildResult_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildResult_Tests.cs new file mode 100644 index 00000000000..79e73942ba8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/BuildResult_Tests.cs @@ -0,0 +1,363 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Evaluation; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using Microsoft.Build.Unittest; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class BuildResult_Tests + { + private int _nodeRequestId; + + [TestInitialize] + public void SetUp() + { + _nodeRequestId = 1; + } + + [TestCleanup] + public void TearDown() + { + } + + [TestMethod] + public void TestConstructorGood() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result2 = new BuildResult(request); + } + + [TestMethod] + public void Clone() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result1 = new BuildResult(request); + result1.ResultsByTarget.Add("FOO", TestUtilities.GetEmptySucceedingTargetResult()); + Assert.IsTrue(result1.ResultsByTarget.ContainsKey("foo")); // test comparer + + BuildResult result2 = result1.Clone(); + + result1.ResultsByTarget.Add("BAR", TestUtilities.GetEmptySucceedingTargetResult()); + Assert.IsTrue(result1.ResultsByTarget.ContainsKey("foo")); // test comparer + Assert.IsTrue(result1.ResultsByTarget.ContainsKey("bar")); + + Assert.AreEqual(result1.SubmissionId, result2.SubmissionId); + Assert.AreEqual(result1.ConfigurationId, result2.ConfigurationId); + Assert.AreEqual(result1.GlobalRequestId, result2.GlobalRequestId); + Assert.AreEqual(result1.ParentGlobalRequestId, result2.ParentGlobalRequestId); + Assert.AreEqual(result1.NodeRequestId, result2.NodeRequestId); + Assert.AreEqual(result1.CircularDependency, result2.CircularDependency); + Assert.AreEqual(result1.ResultsByTarget["foo"], result2.ResultsByTarget["foo"]); + Assert.AreEqual(result1.OverallResult, result2.OverallResult); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestConstructorBad() + { + BuildResult result = new BuildResult(null); + } + + [TestMethod] + public void TestConfigurationId() + { + BuildRequest request = CreateNewBuildRequest(-1, new string[0]); + BuildResult result = new BuildResult(request); + Assert.AreEqual(-1, result.ConfigurationId); + + BuildRequest request2 = CreateNewBuildRequest(1, new string[0]); + BuildResult result2 = new BuildResult(request2); + Assert.AreEqual(1, result2.ConfigurationId); + } + + [TestMethod] + public void TestExceptionGood() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + Assert.IsNull(result.Exception); + + AccessViolationException e = new AccessViolationException(); + result = new BuildResult(request, e); + + Assert.AreEqual(e, result.Exception); + } + + [TestMethod] + public void TestOverallResult() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + result.AddResultsForTarget("bar", new TargetResult(new TaskItem[0] { }, new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, new Exception()))); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + + result.AddResultsForTarget("baz", new TargetResult(new TaskItem[0] { }, TestUtilities.GetStopWithErrorResult(new Exception()))); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + + BuildRequest request2 = CreateNewBuildRequest(2, new string[0]); + BuildResult result2 = new BuildResult(request2); + result2.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + result2.AddResultsForTarget("bar", TestUtilities.GetEmptyFailingTargetResult()); + Assert.AreEqual(BuildResultCode.Failure, result2.OverallResult); + } + + [TestMethod] + public void TestPacketType() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + Assert.AreEqual(NodePacketType.BuildResult, ((INodePacket)result).Type); + } + + [TestMethod] + public void TestAddAndRetrieve() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + result.AddResultsForTarget("bar", TestUtilities.GetEmptyFailingTargetResult()); + + Assert.AreEqual(TargetResultCode.Success, result["foo"].ResultCode); + Assert.AreEqual(TargetResultCode.Failure, result["bar"].ResultCode); + } + + [ExpectedException(typeof(KeyNotFoundException))] + [TestMethod] + public void TestIndexerBad1() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + ITargetResult targetResult = result["foo"]; + } + + [ExpectedException(typeof(KeyNotFoundException))] + [TestMethod] + public void TestIndexerBad2() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + ITargetResult targetResult = result["bar"]; + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestAddResultsInvalid1() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget(null, TestUtilities.GetEmptySucceedingTargetResult()); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestAddResultsInvalid2() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", null); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestAddResultsInvalid3() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget(null, TestUtilities.GetEmptySucceedingTargetResult()); + } + + [TestMethod] + public void TestMergeResults() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + + BuildResult result2 = new BuildResult(request); + result.AddResultsForTarget("bar", TestUtilities.GetEmptyFailingTargetResult()); + + result.MergeResults(result2); + Assert.AreEqual(TargetResultCode.Success, result["foo"].ResultCode); + Assert.AreEqual(TargetResultCode.Failure, result["bar"].ResultCode); + + BuildResult result3 = new BuildResult(request); + result.MergeResults(result3); + + BuildResult result4 = new BuildResult(request); + result4.AddResultsForTarget("xor", TestUtilities.GetEmptySucceedingTargetResult()); + result.MergeResults(result4); + Assert.AreEqual(TargetResultCode.Success, result["foo"].ResultCode); + Assert.AreEqual(TargetResultCode.Failure, result["bar"].ResultCode); + Assert.AreEqual(TargetResultCode.Success, result["xor"].ResultCode); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestMergeResultsBad1() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + + result.MergeResults(null); + } + + // See the implementation of BuildResult.MergeResults for an explanation of why this + // test is disabled. +#if false + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestMergeResultsBad2() + { + BuildResult result = new BuildResult(1); + result["foo"] = new TargetResult(new BuildItem[0] { }, BuildResultCode.Success); + + BuildResult result2 = new BuildResult(1); + result2["foo"] = new TargetResult(new BuildItem[0] { }, BuildResultCode.Success); + + result.MergeResults(result2); + } +#endif + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestMergeResultsBad3() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + + BuildRequest request2 = CreateNewBuildRequest(2, new string[0]); + BuildResult result2 = new BuildResult(request2); + result2.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + + result.MergeResults(result2); + } + + [TestMethod] + public void TestHasResultsForTarget() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + + Assert.IsTrue(result.HasResultsForTarget("foo")); + Assert.IsFalse(result.HasResultsForTarget("bar")); + } + + [TestMethod] + public void TestEnumerator() + { + BuildRequest request = CreateNewBuildRequest(1, new string[0]); + BuildResult result = new BuildResult(request); + int countFound = 0; + foreach (KeyValuePair resultPair in result.ResultsByTarget) + { + countFound++; + } + Assert.AreEqual(countFound, 0); + + result.AddResultsForTarget("foo", TestUtilities.GetEmptySucceedingTargetResult()); + bool foundFoo = false; + countFound = 0; + foreach (KeyValuePair resultPair in result.ResultsByTarget) + { + if (resultPair.Key == "foo") + { + foundFoo = true; + } + countFound++; + } + Assert.AreEqual(countFound, 1); + Assert.IsTrue(foundFoo); + + result.AddResultsForTarget("bar", TestUtilities.GetEmptySucceedingTargetResult()); + foundFoo = false; + bool foundBar = false; + countFound = 0; + foreach (KeyValuePair resultPair in result.ResultsByTarget) + { + if (resultPair.Key == "foo") + { + Assert.IsFalse(foundFoo); + foundFoo = true; + } + if (resultPair.Key == "bar") + { + Assert.IsFalse(foundBar); + foundBar = true; + } + countFound++; + } + Assert.AreEqual(countFound, 2); + Assert.IsTrue(foundFoo); + Assert.IsTrue(foundBar); + } + + + [TestMethod] + public void TestTranslation() + { + BuildRequest request = new BuildRequest(1, 1, 2, new string[] { "alpha", "omega" }, null, new BuildEventContext(1, 1, 2, 3, 4, 5), null); + BuildResult result = new BuildResult(request, new BuildAbortedException()); + + TaskItem fooTaskItem = new TaskItem("foo", "asdf.proj"); + fooTaskItem.SetMetadata("meta1", "metavalue1"); + fooTaskItem.SetMetadata("meta2", "metavalue2"); + + result.InitialTargets = new List { "a", "b" }; + result.DefaultTargets = new List { "c", "d" }; + + result.AddResultsForTarget("alpha", new TargetResult(new TaskItem[] { fooTaskItem }, TestUtilities.GetSuccessResult())); + result.AddResultsForTarget("omega", new TargetResult(new TaskItem[] { }, TestUtilities.GetStopWithErrorResult(new ArgumentException("The argument was invalid")))); + + Assert.AreEqual(NodePacketType.BuildResult, (result as INodePacket).Type); + ((INodePacketTranslatable)result).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = BuildResult.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + BuildResult deserializedResult = packet as BuildResult; + + Assert.AreEqual(result.ConfigurationId, deserializedResult.ConfigurationId); + Assert.IsTrue(TranslationHelpers.CompareCollections(result.DefaultTargets, deserializedResult.DefaultTargets, StringComparer.Ordinal)); + Assert.IsTrue(TranslationHelpers.CompareExceptions(result.Exception, deserializedResult.Exception)); + Assert.AreEqual(result.Exception.Message, deserializedResult.Exception.Message); + Assert.AreEqual(result.GlobalRequestId, deserializedResult.GlobalRequestId); + Assert.IsTrue(TranslationHelpers.CompareCollections(result.InitialTargets, deserializedResult.InitialTargets, StringComparer.Ordinal)); + Assert.AreEqual(result.NodeRequestId, deserializedResult.NodeRequestId); + Assert.AreEqual(result["alpha"].ResultCode, deserializedResult["alpha"].ResultCode); + Assert.IsTrue(TranslationHelpers.CompareExceptions(result["alpha"].Exception, deserializedResult["alpha"].Exception)); + Assert.IsTrue(TranslationHelpers.CompareCollections(result["alpha"].Items, deserializedResult["alpha"].Items, TaskItemComparer.Instance)); + Assert.AreEqual(result["omega"].ResultCode, deserializedResult["omega"].ResultCode); + Assert.IsTrue(TranslationHelpers.CompareExceptions(result["omega"].Exception, deserializedResult["omega"].Exception)); + Assert.IsTrue(TranslationHelpers.CompareCollections(result["omega"].Items, deserializedResult["omega"].Items, TaskItemComparer.Instance)); + } + + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/CentralForwardingLogger_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/CentralForwardingLogger_Tests.cs new file mode 100644 index 00000000000..ca11eec994d --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/CentralForwardingLogger_Tests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Test the central forwarding logger +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + /// Test the central forwarding logger by initializing a new one and sending events through it. + /// + [TestClass] + public class CentralForwardingLogger_Tests + { + /// + /// Tests the basic getting and setting of the logger parameters + /// + [TestMethod] + public void GetandSetLoggerParameters() + { + CentralForwardingLogger centralLogger = new CentralForwardingLogger(); + + // Verify NodeId can be get and set properly + Assert.AreEqual(0, centralLogger.NodeId); + centralLogger.NodeId = 4; + Assert.AreEqual(4, centralLogger.NodeId); + + // Verify Parameters can be get and set properly + Assert.IsTrue(string.IsNullOrEmpty(centralLogger.Parameters), "Expected parameters to be null or empty"); + centralLogger.Parameters = "MyParameters"; + Assert.IsTrue(string.Compare(centralLogger.Parameters, "MyParameters", StringComparison.OrdinalIgnoreCase) == 0, "Expected parameters equal MyParameters"); + + // Verify Verbosity can be get and set properly + Assert.IsTrue(centralLogger.Verbosity == LoggerVerbosity.Quiet, "Expected default to be Quiet"); + centralLogger.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue(centralLogger.Verbosity == LoggerVerbosity.Detailed, "Expected default to be Detailed"); + + // Verify BuildEventRedirector can be get and set properly + Assert.IsNull(centralLogger.BuildEventRedirector, "Expected BuildEventRedirector to be null"); + TestEventRedirector eventRedirector = new TestEventRedirector(null); + centralLogger.BuildEventRedirector = eventRedirector; + Assert.IsTrue(centralLogger.BuildEventRedirector == eventRedirector, "Expected the BuildEventRedirector to match the passed in eventRedirector"); + } + + /// + /// Verify the correct exception is thrown when the logger is initialized with a null + /// event source. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InitializeWithNullEventSourceILogger() + { + CentralForwardingLogger centralLogger = new CentralForwardingLogger(); + centralLogger.Initialize(null); + } + + /// + /// Verify the correct exception is thrown when the logger is initialized with a null + /// event source. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InitializeWithNullEventSourceINodeLogger() + { + CentralForwardingLogger centralLogger = new CentralForwardingLogger(); + centralLogger.Initialize(null, 4); + } + + /// + /// Verify the shutdown method will null out the event redirector + /// + [TestMethod] + public void TestShutDown() + { + CentralForwardingLogger centralLogger = new CentralForwardingLogger(); + centralLogger.BuildEventRedirector = new TestEventRedirector(null); + + Assert.IsNotNull(centralLogger.BuildEventRedirector); + + centralLogger.Shutdown(); + Assert.IsNull(centralLogger.BuildEventRedirector); + } + + /// + /// Verify that the forwarding logger correctly forwards events when passed to it. + /// + [TestMethod] + public void ForwardEvents() + { + BuildStartedEventArgs buildStarted = new BuildStartedEventArgs("Message", "Help"); + BuildFinishedEventArgs buildFinished = new BuildFinishedEventArgs("Message", "Keyword", true); + BuildMessageEventArgs normalMessage = new BuildMessageEventArgs("Message2", "help", "sender", MessageImportance.Normal); + + EventSourceSink loggerSource = AttachForwardingLoggerAndRedirector(buildStarted); + loggerSource.Consume(buildStarted); + + loggerSource = AttachForwardingLoggerAndRedirector(buildFinished); + loggerSource.Consume(buildFinished); + + loggerSource = AttachForwardingLoggerAndRedirector(normalMessage); + loggerSource.Consume(normalMessage); + } + + /// + /// Verify no exception is thrown when an event is raised but no + /// event redirector is registered on the logger. This could happen + /// if no central logger is registered with the system. + /// + [TestMethod] + public void RaiseEventWithNoBuildEventRedirector() + { + BuildMessageEventArgs normalMessage = new BuildMessageEventArgs("Message2", "help", "sender", MessageImportance.Normal); + EventSourceSink loggerSource = new EventSourceSink(); + CentralForwardingLogger forwardingLogger = new CentralForwardingLogger(); + forwardingLogger.Initialize(loggerSource); + loggerSource.Consume(normalMessage); + } + + /// + /// Create a new forwarding logger, event redirector, and event source. + /// The returned event source can then have and event raised on it and it can + /// check to see if the event raised matches the one we were expecting. + /// + /// A build event we are expecting to be forwarded by the forwarding logger + /// An event source on which one can raise an event. + private static EventSourceSink AttachForwardingLoggerAndRedirector(BuildEventArgs buildEventToCheck) + { + EventSourceSink loggerEventSource = new EventSourceSink(); + CentralForwardingLogger forwardingLogger = new CentralForwardingLogger(); + TestEventRedirector eventRedirector = new TestEventRedirector(buildEventToCheck); + forwardingLogger.BuildEventRedirector = eventRedirector; + forwardingLogger.Initialize(loggerEventSource); + return loggerEventSource; + } + + /// + /// An event redirector which takes in an expected event + /// and when the forwarding logger forwards and event + /// we check to see if the events match. This allows + /// us to check to see if the forwarding logger is + /// sending us the events we send in. + /// + private class TestEventRedirector : IEventRedirector + { + #region Data + + /// + /// Event we expect to see in the ForwardEvent method. + /// This helps us verify that a logger is correctly forwarding + /// an event. + /// + private BuildEventArgs _expectedEvent; + + #endregion + + /// + /// Take in an expected event and when the event is forwarded make sure + /// the events are the same. + /// + /// Event we expect to see in the ForwardEvent method + public TestEventRedirector(BuildEventArgs eventToExpect) + { + _expectedEvent = eventToExpect; + } + + #region Members + + /// + /// When a forwarding logger forwards an event we need to check to see + /// if the event the logger sent us is the same one we sent in. + /// + /// Build event to forward + public void ForwardEvent(BuildEventArgs buildEvent) + { + Assert.IsTrue(_expectedEvent == buildEvent, "Expected the forwarded event to match the expected event"); + } + + #endregion + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/ConfigurationMetadata_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/ConfigurationMetadata_Tests.cs new file mode 100644 index 00000000000..bfbf7a4ac13 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/ConfigurationMetadata_Tests.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the ConfigurationMetadata class. +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using System.IO; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Unit tests for ConfigurationMetadata + /// + [TestClass] + public class ConfigurationMetadata_Tests + { + /// + /// Prepares to run the test + /// + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Tears down after the test. + /// + [TestCleanup] + public void TearDown() + { + } + + /// + /// Verify that a null config throws an ArgumentNullException. + /// + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorNullConfiguration() + { + BuildRequestConfiguration config = null; + ConfigurationMetadata metadata = new ConfigurationMetadata(config); + } + + /// + /// Verify that a null project thrown an ArgumentNullException + /// + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorNullProject() + { + Project project = null; + ConfigurationMetadata metadata = new ConfigurationMetadata(project); + } + + /// + /// Verify that we get the project path and tools version from the configuration + /// + [TestMethod] + public void TestValidConfiguration() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(), "toolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, "2.0"); + ConfigurationMetadata metadata = new ConfigurationMetadata(config); + Assert.AreEqual(data.ProjectFullPath, metadata.ProjectFullPath); + Assert.AreEqual(data.ExplicitlySpecifiedToolsVersion, metadata.ToolsVersion); + } + + /// + /// Verify that we get the project path and tools version from the project. + /// + [TestMethod] + public void TestValidProject() + { + Project project = CreateProject(); + + ConfigurationMetadata metadata = new ConfigurationMetadata(project); + Assert.AreEqual(project.FullPath, metadata.ProjectFullPath); + Assert.AreEqual(project.ToolsVersion, metadata.ToolsVersion); + } + + /// + /// Verify that we get the same hash code from equivalent metadatas even if they come from different sources. + /// + [TestMethod] + public void TestGetHashCode() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, ObjectModelHelpers.MSBuildDefaultToolsVersion); + + Project project = CreateProject(); + + ConfigurationMetadata metadata1 = new ConfigurationMetadata(config); + ConfigurationMetadata metadata2 = new ConfigurationMetadata(project); + Assert.AreEqual(metadata1.GetHashCode(), metadata2.GetHashCode()); + } + + /// + /// Verify that the Equals method works correctly. + /// + [TestMethod] + public void TestEquals() + { + BuildRequestData data = new BuildRequestData("file", new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, data, ObjectModelHelpers.MSBuildDefaultToolsVersion); + + Project project = CreateProject(); + + ConfigurationMetadata metadata1 = new ConfigurationMetadata(config); + ConfigurationMetadata metadata2 = new ConfigurationMetadata(project); + Assert.IsTrue(metadata1.Equals(metadata2)); + + data = new BuildRequestData("file2", new Dictionary(), ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[0], null); + BuildRequestConfiguration config2 = new BuildRequestConfiguration(1, data, ObjectModelHelpers.MSBuildDefaultToolsVersion); + ConfigurationMetadata metadata3 = new ConfigurationMetadata(config2); + Assert.IsFalse(metadata1.Equals(metadata3)); + + data = new BuildRequestData("file", new Dictionary(), "3.0", new string[0], null); + BuildRequestConfiguration config3 = new BuildRequestConfiguration(1, data, "3.0"); + ConfigurationMetadata metadata4 = new ConfigurationMetadata(config3); + Assert.IsFalse(metadata1.Equals(metadata4)); + } + + /// + /// Creates a test project. + /// + private Project CreateProject() + { + string projectBody = ObjectModelHelpers.CleanupFileContents(@" + + + +"); + + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + Project project = new Project(XmlReader.Create(new StringReader(projectBody)), globalProperties, ObjectModelHelpers.MSBuildDefaultToolsVersion); + project.FullPath = "file"; + + return project; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/CustomTaskHelper.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/CustomTaskHelper.cs new file mode 100644 index 00000000000..1cf3fcb0e03 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/CustomTaskHelper.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper task for creating custom tasks for unit tests. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Reflection; +using System.CodeDom.Compiler; + +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// This class provides methods for creating custom tasks for unit tests. + /// + internal static class CustomTaskHelper + { + /// + /// Create a task assembly given the specified task code. + /// + /// The text of the C# code. + /// The name of the assembly. + public static string GetAssemblyForTask(string taskContents) + { + string referenceAssembliesPath = ToolLocationHelper.GetPathToBuildTools(ToolLocationHelper.CurrentToolsVersion); + + string[] referenceAssemblies = new string[] { "System.dll", Path.Combine(referenceAssembliesPath, "Microsoft.Build.Framework.dll"), Path.Combine(referenceAssembliesPath, "Microsoft.Build.Utilities.Core.dll"), Path.Combine(referenceAssembliesPath, "Microsoft.Build.Tasks.Core.dll") }; + return GetAssemblyForTask(taskContents, referenceAssemblies); + } + + /// + /// Create a task assembly given the specified task code. + /// + /// The text of the C# code. + /// The reference assemblies to pass to the task + /// The name of the assembly. + public static string GetAssemblyForTask(string taskContents, string[] referenceAssembliesForTask) + { + CompilerParameters compilerParameters = new CompilerParameters(referenceAssembliesForTask); + compilerParameters.GenerateInMemory = false; + compilerParameters.TreatWarningsAsErrors = false; + + CodeDomProvider codegenerator = CodeDomProvider.CreateProvider("cs"); + CompilerResults results = codegenerator.CompileAssemblyFromSource(compilerParameters, taskContents); + try + { + Assembly taskAssembly = results.CompiledAssembly; + if (taskAssembly == null) + { + StringBuilder builder = new StringBuilder(); + foreach (CompilerError error in results.Errors) + { + if (!error.IsWarning) + { + builder.AppendLine(error.ToString()); + } + } + + throw new ArgumentException(builder.ToString()); + } + + return taskAssembly.Location; + } + catch (FileNotFoundException) + { + // This occurs if there is a failure to compile the assembly. We just pass through because we will take care of the failure below. + } + + return null; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/EventRedirectorToSink_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/EventRedirectorToSink_Tests.cs new file mode 100644 index 00000000000..4b09a5c0997 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/EventRedirectorToSink_Tests.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Verify the event redirector to sink properly forwards message to the attached sink +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + /// Test the central forwarding logger by initializing a new one and sending events through it. + /// + [TestClass] + public class EventRedirectorToSink_Tests + { + /// + /// Tests the basic getting and setting of the logger parameters + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestConstructorNegativeLoggerId() + { + EventSourceSink testSink = new EventSourceSink(); + EventRedirectorToSink eventRedirector = new EventRedirectorToSink(-10, testSink); + } + + /// + /// Verify the correct exception is thrown when the logger is initialized with a null + /// event source. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestConstructorNullSink() + { + EventRedirectorToSink eventRedirector = new EventRedirectorToSink(0, null); + } + + /// + /// Verify an valid inputs work and do not produce an exception + /// + [TestMethod] + public void TestConstructorValidInputs() + { + EventSourceSink testSink = new EventSourceSink(); + EventRedirectorToSink eventRedirector = new EventRedirectorToSink(5, testSink); + Assert.IsNotNull(eventRedirector, "eventRedirector was not supposed to be null"); + } + + /// + /// Verify when an event is forwarded, the event that was put in is the same event that was recieved on the event source + /// also make sure the sinkId has been updated by the event redirector. + /// + [TestMethod] + public void TestForwardingNotNullEvent() + { + EventSourceSink testSink = new EventSourceSink(); + EventRedirectorToSink eventRedirector = new EventRedirectorToSink(5, testSink); + BuildMessageEventArgs messageEvent = new BuildMessageEventArgs("My message", "Help me keyword", "Sender", MessageImportance.High); + bool wentInHandler = false; + testSink.AnyEventRaised += new AnyEventHandler + ( + delegate + ( + object sender, + BuildEventArgs buildEvent + ) + { + wentInHandler = true; + BuildMessageEventArgs messageEventFromPacket = buildEvent as BuildMessageEventArgs; + Assert.IsTrue(messageEvent == messageEventFromPacket, "Expected messageEvent to be forwarded to match actually forwarded event"); + } + + ); + + ((IEventRedirector)eventRedirector).ForwardEvent(messageEvent); + Assert.IsTrue(wentInHandler, "Expected to go into event handler"); + } + + /// + /// Verify when a null event is forwarded we get a null argument exception + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestForwardingNullEvent() + { + EventSourceSink testSink = new EventSourceSink(); + EventRedirectorToSink eventRedirector = new EventRedirectorToSink(5, testSink); + ((IEventRedirector)eventRedirector).ForwardEvent(null); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/EventSourceSink_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/EventSourceSink_Tests.cs new file mode 100644 index 00000000000..bbc801d3df2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/EventSourceSink_Tests.cs @@ -0,0 +1,984 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Test that events are properly raised and consumed +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + /// Verify the event source sink functions correctly. + /// + [TestClass] + public class EventSourceSink_Tests + { + /// + /// Verify the properties on EventSourceSink properly work + /// + [TestMethod] + public void PropertyTests() + { + EventSourceSink sink = new EventSourceSink(); + Assert.IsNull(sink.Name); + string name = "Test Name"; + sink.Name = name; + Assert.IsTrue(string.Compare(sink.Name, name, StringComparison.OrdinalIgnoreCase) == 0); + } + + /// + /// Test out events + /// + [TestMethod] + public void ConsumeEventsGoodEvents() + { + EventSourceSink sink = new EventSourceSink(); + RaiseEventHelper eventHelper = new RaiseEventHelper(sink); + EventHandlerHelper testHandlers = new EventHandlerHelper(sink, null); + VerifyRegisteredHandlers(RaiseEventHelper.BuildStarted, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.BuildFinished, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.NormalMessage, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.TaskFinished, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.CommandLine, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.Warning, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.Error, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.TargetStarted, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.TargetFinished, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.ProjectStarted, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.ProjectFinished, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.ExternalStartedEvent, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.BuildStarted, eventHelper, testHandlers); + VerifyRegisteredHandlers(RaiseEventHelper.GenericStatusEvent, eventHelper, testHandlers); + } + + /// + /// Test out events when no event handlers are registered + /// + [TestMethod] + public void ConsumeEventsGoodEventsNoHandlers() + { + EventSourceSink sink = new EventSourceSink(); + RaiseEventHelper eventHelper = new RaiseEventHelper(sink); + eventHelper.RaiseBuildEvent(RaiseEventHelper.BuildStarted); + eventHelper.RaiseBuildEvent(RaiseEventHelper.BuildFinished); + eventHelper.RaiseBuildEvent(RaiseEventHelper.NormalMessage); + eventHelper.RaiseBuildEvent(RaiseEventHelper.TaskFinished); + eventHelper.RaiseBuildEvent(RaiseEventHelper.CommandLine); + eventHelper.RaiseBuildEvent(RaiseEventHelper.Warning); + eventHelper.RaiseBuildEvent(RaiseEventHelper.Error); + eventHelper.RaiseBuildEvent(RaiseEventHelper.TargetStarted); + eventHelper.RaiseBuildEvent(RaiseEventHelper.TargetFinished); + eventHelper.RaiseBuildEvent(RaiseEventHelper.ProjectStarted); + eventHelper.RaiseBuildEvent(RaiseEventHelper.ProjectFinished); + eventHelper.RaiseBuildEvent(RaiseEventHelper.ExternalStartedEvent); + eventHelper.RaiseBuildEvent(RaiseEventHelper.ExternalStartedEvent); + eventHelper.RaiseBuildEvent(RaiseEventHelper.GenericStatusEvent); + } + + #region TestsThrowingLoggingExceptions + + /// + /// Verify when exceptions are thrown in the event handler, they are properly handled + /// + [TestMethod] + public void LoggerExceptionInEventHandler() + { + List exceptionList = new List(); + exceptionList.Add(new LoggerException()); + exceptionList.Add(new ArgumentException()); + exceptionList.Add(new StackOverflowException()); + + foreach (Exception exception in exceptionList) + { + RaiseExceptionInEventHandler(RaiseEventHelper.BuildStarted, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.BuildFinished, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.NormalMessage, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.TaskFinished, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.CommandLine, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.Warning, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.Error, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.TargetStarted, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.TargetFinished, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.ProjectStarted, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.ProjectFinished, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.ExternalStartedEvent, exception); + RaiseExceptionInEventHandler(RaiseEventHelper.GenericStatusEvent, exception); + } + } + + /// + /// Verify raising a generic event derrived from BuildEventArgs rather than CustomBuildEventArgs causes an internalErrorException + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void RaiseGenericBuildEventArgs() + { + EventSourceSink sink = new EventSourceSink(); + RaiseEventHelper eventHelper = new RaiseEventHelper(sink); + eventHelper.RaiseBuildEvent(RaiseEventHelper.GenericBuildEvent); + } + + /// + /// Verify that shutdown un registers all of the event handlers + /// + [TestMethod] + public void VerifyShutdown() + { + EventSourceSink sink = new EventSourceSink(); + + // Registers event handlers onto the event source + EventHandlerHelper handlerHelper = new EventHandlerHelper(sink, null); + RaiseEventHelper raiseEventHelper = new RaiseEventHelper(sink); + + raiseEventHelper.RaiseBuildEvent(RaiseEventHelper.ProjectStarted); + Assert.IsTrue(handlerHelper.EnteredEventHandler); + Assert.IsTrue(handlerHelper.EnteredAnyEventHandler); + Assert.IsTrue(handlerHelper.EnteredStatusEventHandler); + Assert.IsTrue(handlerHelper.RaisedEvent == RaiseEventHelper.ProjectStarted); + Assert.IsTrue(handlerHelper.RaisedAnyEvent == RaiseEventHelper.ProjectStarted); + Assert.IsTrue(handlerHelper.RaisedStatusEvent == RaiseEventHelper.ProjectStarted); + + sink.ShutDown(); + + handlerHelper.ResetRaisedEvent(); + raiseEventHelper.RaiseBuildEvent(RaiseEventHelper.ProjectStarted); + Assert.IsFalse(handlerHelper.EnteredEventHandler); + Assert.IsFalse(handlerHelper.EnteredAnyEventHandler); + Assert.IsFalse(handlerHelper.EnteredStatusEventHandler); + Assert.IsNull(handlerHelper.RaisedEvent); + Assert.IsNull(handlerHelper.RaisedAnyEvent); + Assert.IsNull(handlerHelper.RaisedStatusEvent); + } + + /// + /// Verify aggregate exceptions are caught as critical if they contain critical exceptions + /// + [TestMethod] + public void VerifyAggregateExceptionHandling() + { + try + { + // A simple non-critical exception + throw new Exception(); + } + catch (Exception e) + { + Assert.IsFalse(ExceptionHandling.IsCriticalException(e)); + } + + try + { + // An empty aggregate exception - non-critical + throw new AggregateException(); + } + catch (Exception e) + { + Assert.IsFalse(ExceptionHandling.IsCriticalException(e)); + } + + try + { + // Aggregate exception containing two non-critical exceptions - non-critical + Exception[] exceptionArray = new Exception[] { new IndexOutOfRangeException(), new Exception() }; //// two non-critical exceptions + throw new AggregateException(exceptionArray); + } + catch (Exception e) + { + Assert.IsFalse(ExceptionHandling.IsCriticalException(e)); + } + + try + { + // Nested aggregate exception containing non-critical exceptions - non-critical + Exception[] exceptionArray1 = new Exception[] { new IndexOutOfRangeException(), new Exception() }; //// two non-critical exceptions + AggregateException ae1 = new AggregateException(exceptionArray1); + + Exception[] exceptionArray2 = new Exception[] { ae1, new Exception() }; //// two non-critical exceptions (ae1 contains nested exceptions) + throw new AggregateException(exceptionArray2); + } + catch (Exception e) + { + Assert.IsFalse(ExceptionHandling.IsCriticalException(e)); + } + + try + { + // A simple critical exception + throw new OutOfMemoryException(); + } + catch (Exception e) + { + Assert.IsTrue(ExceptionHandling.IsCriticalException(e)); + } + + try + { + // An aggregate exception containing one critical exception - critical + Exception[] exceptionArray = new Exception[] { new OutOfMemoryException(), new IndexOutOfRangeException(), new Exception() }; //// two non-critical exceptions, one critical + throw new AggregateException(exceptionArray); + } + catch (Exception e) + { + Assert.IsTrue(ExceptionHandling.IsCriticalException(e)); + } + + try + { + // Nested aggregate exception containing non-critical exceptions - non-critical + Exception[] exceptionArray1 = new Exception[] { new OutOfMemoryException(), new IndexOutOfRangeException(), new Exception() }; //// two non-critical exceptions, one critical + AggregateException ae1 = new AggregateException(exceptionArray1); + + Exception[] exceptionArray2 = new Exception[] { ae1, new Exception() }; //// one critical one non-critical (ae1 contains nested critical exception) + throw new AggregateException(exceptionArray2); + } + catch (Exception e) + { + Assert.IsTrue(ExceptionHandling.IsCriticalException(e)); + } + } + + #region Private methods + /// + /// Take an event and an exception to raise, create a new sink and raise the event on it. + /// In the event handler registered on the sink, the exception will be thrown. + /// + /// BuildEvent to raise on the + /// Exception to throw in the event handler + private static void RaiseExceptionInEventHandler(BuildEventArgs buildEventToRaise, Exception exceptionToRaise) + { + EventSourceSink sink = new EventSourceSink(); + RaiseEventHelper eventHelper = new RaiseEventHelper(sink); + EventHandlerHelper testHandlers = new EventHandlerHelper(sink, exceptionToRaise); + try + { + eventHelper.RaiseBuildEvent(buildEventToRaise); + } + catch (Exception e) + { + // Logger exceptions should be rethrown as is with no wrapping + if (exceptionToRaise is LoggerException) + { + Assert.IsTrue(e == exceptionToRaise, "Expected Logger exception to be raised in event handler and re-thrown by event source"); + } + else + { + if (ExceptionHandling.IsCriticalException(e)) + { + Assert.IsTrue(e == exceptionToRaise, "Expected Logger exception to be raised in event handler and re-thrown by event source"); + } + else + { + // All other exceptions should be wrapped in an InternalLoggerException, with the original exception as the inner exception + Assert.IsTrue(e is InternalLoggerException, "Expected general exception to be raised in event handler and re-thrown by event source as a InternalLoggerException"); + } + } + } + } + + /// + /// Verify when an is raised the handlers which are registered to handle the event should handle them + /// + /// A buildEventArgs to raise on the event source + /// Helper class which events are raised on + /// Class which contains a set of event handlers registered on the event source + private static void VerifyRegisteredHandlers(BuildEventArgs buildEventToRaise, RaiseEventHelper eventHelper, EventHandlerHelper testHandlers) + { + try + { + eventHelper.RaiseBuildEvent(buildEventToRaise); + if (buildEventToRaise.GetType() != typeof(GenericBuildStatusEventArgs)) + { + Assert.IsTrue(testHandlers.RaisedEvent == buildEventToRaise, "Expected buildevent in handler to match buildevent raised on event source"); + Assert.IsTrue(testHandlers.RaisedEvent == testHandlers.RaisedAnyEvent, "Expected RaisedEvent and RaisedAnyEvent to match"); + Assert.IsTrue(testHandlers.EnteredEventHandler, "Expected to enter into event handler"); + } + + Assert.IsTrue(testHandlers.RaisedAnyEvent == buildEventToRaise, "Expected buildEvent in any event handler to match buildevent raised on event source"); + Assert.IsTrue(testHandlers.EnteredAnyEventHandler, "Expected to enter into AnyEvent handler"); + + if (buildEventToRaise is BuildStatusEventArgs) + { + Assert.IsTrue(testHandlers.RaisedStatusEvent == buildEventToRaise, "Expected buildevent in handler to match buildevent raised on event source"); + Assert.IsTrue(testHandlers.EnteredStatusEventHandler, "Expected to enter into Status event handler"); + } + else + { + Assert.IsNull(testHandlers.RaisedStatusEvent); + Assert.IsFalse(testHandlers.EnteredStatusEventHandler); + } + } + finally + { + testHandlers.ResetRaisedEvent(); + } + } + + #endregion + + #region HelperClasses + + /// + /// Generic class derived from BuildEventArgs which is used to test the case + /// where the event is not a well known event, or a custom event + /// + internal class GenericBuildEventArgs : BuildEventArgs + { + /// + /// Default constructor + /// + internal GenericBuildEventArgs() + : base() + { + } + } + + /// + /// Generic class derived from BuildStatusEvent which is used to test the case + /// where a status event is raised but it is not a well known status event (build started ...) + /// + internal class GenericBuildStatusEventArgs : BuildStatusEventArgs + { + /// + /// Default constructor + /// + internal GenericBuildStatusEventArgs() + : base() + { + } + } + + /// + /// Create a test class which will register to the event source and have event handlers + /// which can act normally or throw exceptions. + /// + internal class EventHandlerHelper + { + #region Data + /// + /// When an event handler raises an event it will + /// set this to the event which was raised + /// This can then be asserted upon to verify the event + /// which was raised on the sink was the one received + /// by the event handler + /// + private BuildEventArgs _raisedEvent; + + /// + /// The any event handler will get all events, even if they are raised to another event handler + /// We need to verify that both the event handler and the any event handler both get the events + /// + private BuildEventArgs _raisedAnyEvent; + + /// + /// A status event message, this is set when status events are raised on the event handler + /// + private BuildEventArgs _raisedStatusEvent; + + /// + /// To test the exception mechinism of the event source, we may want to + /// thow certain exceptions in the event handlers. This can be null if + /// no exception is to be thrown. + /// + private Exception _exceptionInHandlers; + + /// + /// Was the event handler entered into, this tells us whether or not the event + /// was actually raised + /// + private bool _enteredEventHandler; + + /// + /// The any event handler will get all events, even if they are raised to another event handler + /// We need to verify that both the event handler and the any event handler both get the events + /// + private bool _enteredAnyEventHandler; + + /// + /// Events such as BuildStarted, ProjectStarted/Finished, ... are status events. + /// In addition to being raised on their own events, they are also raised on the status event and any event. + /// + private bool _enteredStatusEventHandler; + #endregion + + #region Constructors + /// + /// Default Constructor, registered event handlers for all the well know event types on the passed in event source + /// + /// Event source to register to for events + /// What exception should be thrown from the event handler, this can be null + internal EventHandlerHelper(IEventSource source, Exception exceptionToThrow) + { + _exceptionInHandlers = exceptionToThrow; + source.AnyEventRaised += new AnyEventHandler(Source_AnyEventRaised); + source.BuildFinished += new BuildFinishedEventHandler(Source_BuildFinished); + source.BuildStarted += new BuildStartedEventHandler(Source_BuildStarted); + source.CustomEventRaised += new CustomBuildEventHandler(Source_CustomEventRaised); + source.ErrorRaised += new BuildErrorEventHandler(Source_ErrorRaised); + source.MessageRaised += new BuildMessageEventHandler(Source_MessageRaised); + source.ProjectFinished += new ProjectFinishedEventHandler(Source_ProjectFinished); + source.ProjectStarted += new ProjectStartedEventHandler(Source_ProjectStarted); + source.StatusEventRaised += new BuildStatusEventHandler(Source_StatusEventRaised); + source.TargetFinished += new TargetFinishedEventHandler(Source_TargetFinished); + source.TargetStarted += new TargetStartedEventHandler(Source_TargetStarted); + source.TaskFinished += new TaskFinishedEventHandler(Source_TaskFinished); + source.TaskStarted += new TaskStartedEventHandler(Source_TaskStarted); + source.WarningRaised += new BuildWarningEventHandler(Source_WarningRaised); + } + #endregion + + #region Properties + /// + /// Was an event handler entered into + /// + public bool EnteredEventHandler + { + get { return _enteredEventHandler; } + } + + /// + /// Was the Any event handler + /// + public bool EnteredAnyEventHandler + { + get + { + return _enteredAnyEventHandler; + } + } + + /// + /// Was the Status event handler + /// + public bool EnteredStatusEventHandler + { + get + { + return _enteredStatusEventHandler; + } + } + + /// + /// Which event was raised on the event source, this can be asserted upon + /// to verify the event passed to the event source is the same one which was + /// recieved by the event handlers + /// + public BuildEventArgs RaisedEvent + { + get + { + return _raisedEvent; + } + } + + /// + /// Check the event raised by the AnyEventHandler + /// + public BuildEventArgs RaisedAnyEvent + { + get + { + return _raisedAnyEvent; + } + } + + /// + /// Check the event raised by the StatusEventHandler + /// + public BuildEventArgs RaisedStatusEvent + { + get + { + return _raisedStatusEvent; + } + } + #endregion + + #region Public Methods + + /// + /// Reset the per event variables so that we can raise another + /// event and capture the information for it. + /// + public void ResetRaisedEvent() + { + _raisedEvent = null; + _raisedAnyEvent = null; + _raisedStatusEvent = null; + _enteredAnyEventHandler = false; + _enteredEventHandler = false; + _enteredStatusEventHandler = false; + } + #endregion + + #region EventHandlers + + /// + /// Do the test work for all of the event handlers. + /// + /// Event which was raised by an event source this class was listening to + private void HandleEvent(BuildEventArgs e) + { + _enteredEventHandler = true; + _raisedEvent = e; + + if (_exceptionInHandlers != null) + { + throw _exceptionInHandlers; + } + } + + /// + /// Handle a warning event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_WarningRaised(object sender, BuildWarningEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a task started event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_TaskStarted(object sender, TaskStartedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a task finished event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_TaskFinished(object sender, TaskFinishedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a target started event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_TargetStarted(object sender, TargetStartedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a target finished event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_TargetFinished(object sender, TargetFinishedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a status event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_StatusEventRaised(object sender, BuildStatusEventArgs e) + { + _enteredStatusEventHandler = true; + _raisedStatusEvent = e; + + if (_exceptionInHandlers != null) + { + throw _exceptionInHandlers; + } + } + + /// + /// Handle a project started event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_ProjectStarted(object sender, ProjectStartedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a project finished event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_ProjectFinished(object sender, ProjectFinishedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a message event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_MessageRaised(object sender, BuildMessageEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a error event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_ErrorRaised(object sender, BuildErrorEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a custom event, these are mostly user created events + /// + /// Who sent the event + /// Event raised on the event source + private void Source_CustomEventRaised(object sender, CustomBuildEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a build started event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_BuildStarted(object sender, BuildStartedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a build finished event + /// + /// Who sent the event + /// Event raised on the event source + private void Source_BuildFinished(object sender, BuildFinishedEventArgs e) + { + HandleEvent(e); + } + + /// + /// Handle a events raised from the any event source. This source will + /// raise all events no matter the type. + /// + /// Who sent the event + /// Event raised on the event source + private void Source_AnyEventRaised(object sender, BuildEventArgs e) + { + _enteredAnyEventHandler = true; + _raisedAnyEvent = e; + } + #endregion + } + + /// + /// Helper for the test, this class has methods + /// individual types of events or a set of all well known events. + /// The Events can be raised in multiple tests. The helper class keeps the code cleaner + /// by not having to instantiate new objects everywhere and + /// all the fields are set in one place which makes it more maintainable + /// + internal class RaiseEventHelper + { + #region Data + /// + /// Build Started Event + /// + private static BuildStartedEventArgs s_buildStarted = new BuildStartedEventArgs("Message", "Help"); + + /// + /// Generic Build Event + /// + private static GenericBuildEventArgs s_genericBuild = new GenericBuildEventArgs(); + + /// + /// Generic Build Status Evemt + /// + private static GenericBuildStatusEventArgs s_genericBuildStatus = new GenericBuildStatusEventArgs(); + + /// + /// Build Finished Event + /// + private static BuildFinishedEventArgs s_buildFinished = new BuildFinishedEventArgs("Message", "Keyword", true); + + /// + /// Build Message Event + /// + private static BuildMessageEventArgs s_buildMessage = new BuildMessageEventArgs("Message2", "help", "sender", MessageImportance.Normal); + + /// + /// Task Started Event + /// + private static TaskStartedEventArgs s_taskStarted = new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"); + + /// + /// Task Finished Event + /// + private static TaskFinishedEventArgs s_taskFinished = new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true); + + /// + /// Task Command Line Event + /// + private static TaskCommandLineEventArgs s_taskCommandLine = new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low); + + /// + /// Build Warning Event + /// + private static BuildWarningEventArgs s_buildWarning = new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + + /// + /// Build Error Event + /// + private static BuildErrorEventArgs s_buildError = new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + + /// + /// Target Started Event + /// + private static TargetStartedEventArgs s_targetStarted = new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"); + + /// + /// Target Finished Event + /// + private static TargetFinishedEventArgs s_targetFinished = new TargetFinishedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile", true); + + /// + /// Project Started Event + /// + private static ProjectStartedEventArgs s_projectStarted = new ProjectStartedEventArgs(-1, "message", "help", "ProjectFile", "targetNames", null, null, null); + + /// + /// Project Finished Event + /// + private static ProjectFinishedEventArgs s_projectFinished = new ProjectFinishedEventArgs("message", "help", "ProjectFile", true); + + /// + /// External Project Started Event + /// + private static ExternalProjectStartedEventArgs s_externalProjectStarted = new ExternalProjectStartedEventArgs("message", "help", "senderName", "projectFile", "targetNames"); + + /// + /// Event source on which the events will be raised. + /// + private EventSourceSink _sourceForEvents; + + #endregion + + #region Constructor + /// + /// Constructor + /// + /// Event source on which the events will be raised + internal RaiseEventHelper(EventSourceSink eventSource) + { + _sourceForEvents = eventSource; + } + + #endregion + + #region Properties + + /// + /// Event which can be raised in multiple tests. + /// + internal static BuildStartedEventArgs BuildStarted + { + get + { + return s_buildStarted; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static GenericBuildEventArgs GenericBuildEvent + { + get + { + return s_genericBuild; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static GenericBuildStatusEventArgs GenericStatusEvent + { + get + { + return s_genericBuildStatus; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static BuildFinishedEventArgs BuildFinished + { + get + { + return s_buildFinished; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static BuildMessageEventArgs NormalMessage + { + get + { + return s_buildMessage; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static TaskStartedEventArgs TaskStarted + { + get + { + return s_taskStarted; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static TaskFinishedEventArgs TaskFinished + { + get + { + return s_taskFinished; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static TaskCommandLineEventArgs CommandLine + { + get + { + return s_taskCommandLine; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static BuildWarningEventArgs Warning + { + get + { + return s_buildWarning; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static BuildErrorEventArgs Error + { + get + { + return s_buildError; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static TargetStartedEventArgs TargetStarted + { + get + { + return s_targetStarted; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static TargetFinishedEventArgs TargetFinished + { + get + { + return s_targetFinished; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static ProjectStartedEventArgs ProjectStarted + { + get + { + return s_projectStarted; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static ProjectFinishedEventArgs ProjectFinished + { + get + { + return s_projectFinished; + } + } + + /// + /// Event which can be raised in multiple tests. + /// + internal static ExternalProjectStartedEventArgs ExternalStartedEvent + { + get + { + return s_externalProjectStarted; + } + } + #endregion + + /// + /// Raise a build event on the event source + /// + internal void RaiseBuildEvent(BuildEventArgs buildEvent) + { + _sourceForEvents.Consume(buildEvent); + if (buildEvent is BuildStartedEventArgs) + { + Assert.IsTrue(_sourceForEvents.HaveLoggedBuildStartedEvent); + _sourceForEvents.HaveLoggedBuildStartedEvent = false; + Assert.IsFalse(_sourceForEvents.HaveLoggedBuildStartedEvent); + } + else if (buildEvent is BuildFinishedEventArgs) + { + Assert.IsTrue(_sourceForEvents.HaveLoggedBuildFinishedEvent); + _sourceForEvents.HaveLoggedBuildFinishedEvent = false; + Assert.IsFalse(_sourceForEvents.HaveLoggedBuildFinishedEvent); + } + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/FullyQualifiedBuildRequest_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/FullyQualifiedBuildRequest_Tests.cs new file mode 100644 index 00000000000..6005ab2e39a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/FullyQualifiedBuildRequest_Tests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class FullyQualifiedBuildRequest_Tests + { + [TestInitialize] + public void SetUp() + { + } + + [TestCleanup] + public void TearDown() + { + } + + [TestMethod] + public void TestConstructorGood() + { + BuildRequestData data1 = new BuildRequestData("foo", new Dictionary(), "tools", new string[0], null); + FullyQualifiedBuildRequest request = new FullyQualifiedBuildRequest(new BuildRequestConfiguration(data1, "2.0"), new string[1] { "foo" }, true); + + request = new FullyQualifiedBuildRequest(new BuildRequestConfiguration(data1, "2.0"), new string[0] { }, true); + + BuildRequestData data3 = new BuildRequestData("foo", new Dictionary(), "tools", new string[0], null); + request = new FullyQualifiedBuildRequest(new BuildRequestConfiguration(data1, "2.0"), new string[0] { }, false); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorBad1() + { + FullyQualifiedBuildRequest request = new FullyQualifiedBuildRequest(null, new string[1] { "foo" }, true); + } + + [ExpectedException(typeof(ArgumentNullException))] + [TestMethod] + public void TestConstructorBad2() + { + FullyQualifiedBuildRequest request = new FullyQualifiedBuildRequest(new BuildRequestConfiguration(new BuildRequestData("foo", new Dictionary(), "tools", new string[0], null), "2.0"), null, true); + } + + [TestMethod] + public void TestProperties() + { + BuildRequestData data = new BuildRequestData("foo", new Dictionary(), "tools", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(data, "2.0"); + FullyQualifiedBuildRequest request = new FullyQualifiedBuildRequest(config, new string[1] { "foo" }, true); + Assert.AreEqual(request.Config, config); + Assert.AreEqual(request.Targets.Length, 1); + Assert.AreEqual(request.Targets[0], "foo"); + Assert.AreEqual(request.ResultsNeeded, true); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/ITestTaskHost.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/ITestTaskHost.cs new file mode 100644 index 00000000000..5a4896f5e00 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/ITestTaskHost.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Interface used by the test task to communicate what the TaskExecutionHost did to it. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Interface used by the test task to communicate what the TaskExecutionHost did to it. + /// + internal interface ITestTaskHost : ITaskHost + { + /// + /// Called when a parameter is set on the task. + /// + void ParameterSet(string parameterName, object valueSet); + + /// + /// Called when an output is read from the task. + /// + void OutputRead(string parameterName, object actualValue); + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/IntegrationTests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/IntegrationTests.cs new file mode 100644 index 00000000000..c2e88153213 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/IntegrationTests.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.Unittest.BackEnd +{ + class IntegrationTests + { + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/IntrinsicTask_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/IntrinsicTask_Tests.cs new file mode 100644 index 00000000000..40d1dd78c5e --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/IntrinsicTask_Tests.cs @@ -0,0 +1,3538 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class IntrinsicTask_Tests + { + [TestMethod] + public void PropertyGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v1 + v2 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + ExecuteTask(task, LookupHelpers.CreateLookup(properties)); + + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("v1", properties["p1"].EvaluatedValue); + Assert.AreEqual("v2", properties["p2"].EvaluatedValue); + } + + [TestMethod] + public void PropertyGroupWithComments() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v1 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + ExecuteTask(task, LookupHelpers.CreateLookup(properties)); + + Assert.AreEqual(1, properties.Count); + Assert.AreEqual("v1", properties["p1"].EvaluatedValue); + } + + [TestMethod] + public void PropertyGroupEmpty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + ExecuteTask(task, LookupHelpers.CreateLookup(properties)); + + Assert.AreEqual(0, properties.Count); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyGroupWithReservedProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task); + } + + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyGroupWithInvalidPropertyName() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + " + ); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task); + } + + [TestMethod] + public void BlankProperty() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + " + ); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + ExecuteTask(task, LookupHelpers.CreateLookup(properties)); + + Assert.AreEqual(1, properties.Count); + Assert.AreEqual("", properties["p1"].EvaluatedValue); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyGroupWithInvalidSyntax1() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + x + + " + ); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyGroupWithInvalidSyntax2() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + +

+ + + " + ); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + public void PropertyGroupWithConditionOnGroup() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + v1 + v2 + + + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogDoesntContain("[v1][v2]"); + logger.ClearLog(); + + p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + v1 + v2 + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogContains("[v1][v2]"); + } + + [TestMethod] + public void PropertyGroupWithConditionOnGroupUsingMetadataErrors() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + + @(i0) + %(i0.m) + + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogContains("MSB4191"); // Metadata not allowed + } + + [TestMethod] + public void ItemGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + Assert.AreEqual("a1", i1Group.First().EvaluatedInclude); + Assert.AreEqual("b1", i2Group.First().EvaluatedInclude); + } + + [TestMethod] + public void ItemKeepDuplicatesEmptySameAsTrue() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i1"); + Assert.AreEqual(2, group.Count); + } + + [TestMethod] + public void ItemKeepDuplicatesFalse() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + } + + [TestMethod] + public void ItemKeepDuplicatesAsCondition() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + } + + [TestMethod] + public void ItemKeepDuplicatesFalseKeepsExistingDuplicates() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i1"); + Assert.AreEqual(2, group.Count); + } + + [TestMethod] + public void ItemKeepDuplicatesFalseDuringCopyEliminatesDuplicates() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i1"); + Assert.AreEqual(2, group.Count); + + group = lookup.GetItems("i2"); + Assert.AreEqual(1, group.Count); + } + + [TestMethod] + public void ItemKeepDuplicatesFalseWithMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + + m1 + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i1"); + Assert.AreEqual(2, group.Count); + } + + [TestMethod] + public void ItemKeepMetadataEmptySameAsKeepAll() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + } + + [TestMethod] + public void ItemKeepMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual("m2", group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + } + + + [TestMethod] + public void ItemKeepMetadataNotExistant() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + } + + [TestMethod] + public void ItemKeepMetadataList() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + Assert.AreEqual("m2", group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + } + + [TestMethod] + public void ItemKeepMetadataListExpansion() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + var scope = lookup.EnterScope("test"); + lookup.SetProperty(ProjectPropertyInstance.Create("Keep", "m1;m2")); + ExecuteTask(task, lookup); + scope.LeaveScope(); + + var group = lookup.GetItems("i2"); + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + Assert.AreEqual("m2", group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + } + + [TestMethod] + public void ItemRemoveMetadataEmptySameAsKeepAll() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + } + + [TestMethod] + public void ItemRemoveMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + } + + [TestMethod] + public void ItemRemoveMetadataList() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + var group = lookup.GetItems("i2"); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + } + + [TestMethod] + public void ItemRemoveMetadataListExpansion() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + var scope = lookup.EnterScope("test"); + lookup.SetProperty(ProjectPropertyInstance.Create("Remove", "m1;m2")); + ExecuteTask(task, lookup); + scope.LeaveScope(); + + var group = lookup.GetItems("i2"); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemKeepMetadataAndRemoveMetadataMutuallyExclusive() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + m2 + m3 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + } + + ///

+ /// Should not make items with an empty include. + /// + [TestMethod] + public void ItemGroupWithPropertyExpandingToNothing() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + Assert.AreEqual(0, i1Group.Count); + } + + [TestMethod] + public void ItemGroupWithComments() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + m1 + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + Assert.AreEqual("a1", i1Group.First().EvaluatedInclude); + Assert.AreEqual("m1", i1Group.First().GetMetadataValue("m")); + } + + /// + /// This is something that used to be done by CreateItem + /// + [TestMethod] + public void ItemGroupTrims() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + properties.Set(ProjectPropertyInstance.Create("p0", " v0 ")); + Lookup lookup = LookupHelpers.CreateLookup(properties); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + Assert.AreEqual("v0", i1Group.First().EvaluatedInclude); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemGroupWithInvalidSyntax1() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + x + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemGroupWithInvalidSyntax2() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + x + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemGroupWithInvalidSyntax3() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + public void ItemGroupWithTransform() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + '%(filename).obj')""/> + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + Assert.AreEqual("a.cpp", i1Group.First().EvaluatedInclude); + Assert.AreEqual("a.obj", i2Group.First().EvaluatedInclude); + } + + [TestMethod] + public void ItemGroupWithTransformInMetadataValue() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + @(i1->'%(filename).obj') + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i2Group = lookup.GetItems("i2"); + Assert.AreEqual("a.cpp", i2Group.First().EvaluatedInclude); + Assert.AreEqual("a.obj", i2Group.First().GetMetadataValue("m")); + } + + [TestMethod] + public void ItemGroupWithExclude() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + Assert.AreEqual("a1", i1Group.First().EvaluatedInclude); + Assert.AreEqual("b2", i2Group.First().EvaluatedInclude); + } + + [TestMethod] + public void ItemGroupWithMetadataInExclude() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + a1 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + Assert.AreEqual(1, i1Group.Count); + Assert.AreEqual(1, i2Group.Count); + Assert.AreEqual("a1", i1Group.First().EvaluatedInclude); + Assert.AreEqual("b1", i2Group.First().EvaluatedInclude); + } + + [TestMethod] + public void ItemGroupWithConditionOnGroup() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogDoesntContain("[a1][b1]"); + logger.ClearLog(); + + p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogContains("[a1][b1]"); + } + + [TestMethod] + public void ItemGroupWithConditionOnGroupUsingMetadataErrors() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogContains("MSB4191"); // Metadata not allowed + } + + [TestMethod] + public void PropertyGroupWithExternalPropertyReferences() + { + // + // v0 + // + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + $(p0) + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = GeneratePropertyGroup(); + ExecuteTask(task, LookupHelpers.CreateLookup(properties)); + + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("v0", properties["p0"].EvaluatedValue); + Assert.AreEqual("v0", properties["p1"].EvaluatedValue); + } + + [TestMethod] + public void ItemGroupWithPropertyReferences() + { + // + // v0 + // + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = GeneratePropertyGroup(); + Lookup lookup = LookupHelpers.CreateLookup(properties); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + Assert.AreEqual("v0", i1Group.First().EvaluatedInclude); + Assert.AreEqual("a2", i2Group.First().EvaluatedInclude); + } + + [TestMethod] + public void ItemGroupWithMetadataReferences() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + m2 + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + + Assert.AreEqual("a1", i1Group.First().EvaluatedInclude); + Assert.AreEqual("a2", i1Group.ElementAt(1).EvaluatedInclude); + Assert.AreEqual("m1", i2Group.First().EvaluatedInclude); + Assert.AreEqual("m2", i2Group.ElementAt(1).EvaluatedInclude); + + Assert.AreEqual("m1", i1Group.First().GetMetadataValue("m")); + Assert.AreEqual("m2", i1Group.ElementAt(1).GetMetadataValue("m")); + } + + [TestMethod] + public void ItemGroupWithMetadataReferencesOnMetadataConditions() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + + + n1 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ICollection i2Group = lookup.GetItems("i2"); + + Assert.AreEqual(2, i2Group.Count); + Assert.AreEqual("a1", i2Group.First().EvaluatedInclude); + Assert.AreEqual("a2", i2Group.ElementAt(1).EvaluatedInclude); + + Assert.AreEqual("n1", i2Group.First().GetMetadataValue("n")); + Assert.AreEqual(String.Empty, i2Group.ElementAt(1).GetMetadataValue("n")); + } + + [TestMethod] + public void ItemGroupWithMetadataReferencesOnItemGroupAndItemConditionsErrors() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogContains("MSB4191"); // Metadata not allowed + } + + [TestMethod] + public void ItemGroupWithExternalMetadataReferences() + { + // + // + // m1 + // + // + // m2 + // + // + // m3 + // + // + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + %(i0.m) + + + + "); + + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = GenerateLookup(task.Project); + ExecuteTask(task, lookup); + + ICollection i1Group = lookup.GetItems("i1"); + ICollection i2Group = lookup.GetItems("i2"); + + Assert.AreEqual("b1", i1Group.First().EvaluatedInclude); + Assert.AreEqual("b1", i1Group.ElementAt(1).EvaluatedInclude); + Assert.AreEqual("b1", i1Group.ElementAt(2).EvaluatedInclude); + Assert.AreEqual("m1", i1Group.First().GetMetadataValue("m")); + Assert.AreEqual("m2", i1Group.ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("m3", i1Group.ElementAt(2).GetMetadataValue("m")); + + Assert.AreEqual("m1", i2Group.First().EvaluatedInclude); + Assert.AreEqual("m2", i2Group.ElementAt(1).EvaluatedInclude); + Assert.AreEqual("m3", i2Group.ElementAt(2).EvaluatedInclude); + } + + [TestMethod] + public void PropertyGroupWithCumulativePropertyReferences() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + v1 + #$(p1)# + v2 + + "); + + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + ExecuteTask(task, LookupHelpers.CreateLookup(properties)); + + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("v2", properties["p1"].EvaluatedValue); + Assert.AreEqual("#v1#", properties["p2"].EvaluatedValue); + } + + [TestMethod] + public void PropertyGroupWithMetadataReferencesOnGroupErrors() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + %(i0.m) + + ")))); + + p.Build(new string[] { "t" }, new ILogger[] { logger }); + logger.AssertLogContains("MSB4191"); + } + + [TestMethod] + public void PropertyGroupWithMetadataReferencesOnProperty() + { + // + // + // m1 + // n1 + // + // + // m2 + // n2 + // + // + // m3 + // n3 + // + // + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + %(i0.n) + + "); + + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = GenerateLookup(task.Project); + ExecuteTask(task, lookup); + + Assert.AreEqual("n2", lookup.GetProperty("p1").EvaluatedValue); + } + + [TestMethod] + public void PropertiesCanReferenceItemsInSameTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + +

@(i1->'#%(identity)#', '*')

+
+ +
+
+ ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[#a1#*#a2#]"); + } + + [TestMethod] + public void ItemsCanReferencePropertiesInSameTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[v0]"); + } + + [TestMethod] + public void PropertyGroupInTargetCanOverwriteGlobalProperties() + { + MockLogger logger = new MockLogger(); + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("global", "v0"); + + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + v1 + + + + + + + + v2 + + + + + "))), globalProperties, "4.0"); + + ProjectInstance p = project.CreateProjectInstance(); + + Assert.AreEqual("v0", p.GetProperty("global").EvaluatedValue); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + // PropertyGroup outside of target can't overwrite global property, + // but PropertyGroup inside of target can overwrite it + logger.AssertLogContains("start:[v0]", "end:[v2]", "final:[v2]"); + Assert.AreEqual("v2", p.GetProperty("global").EvaluatedValue); + + // Resetting the project goes back to the old value + p = project.CreateProjectInstance(); + Assert.AreEqual("v0", p.GetProperty("global").EvaluatedValue); + } + + + [TestMethod] + public void PropertiesAreRevertedAfterBuild() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + +

p0

+
+ + +

p1

+
+
+
+ ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + string value = p.GetProperty("p").EvaluatedValue; + Assert.AreEqual("p1", value); + + p = project.CreateProjectInstance(); + + value = p.GetProperty("p").EvaluatedValue; + Assert.AreEqual("p0", value); + } + + [TestMethod] + public void PropertiesVisibleToSubsequentTask() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + +

p1

+
+ +
+
+ ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[p1]"); + } + + [TestMethod] + public void PropertiesVisibleToSubsequentTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + +

p1

+
+
+
+ ")))); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + logger.AssertLogContains("[p1]"); + } + + [TestMethod] + public void ItemsVisibleToSubsequentTask() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[i1]"); + } + + [TestMethod] + public void ItemsVisibleToSubsequentTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + ")))); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + logger.AssertLogContains("[i1]"); + } + + [TestMethod] + public void ItemsNotVisibleToParallelTargetBatches() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + 1.out + 2.out + + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "start:[1.in]", "end:[1.in]", "start:[2.in]", "end:[2.in]" }); + } + + [TestMethod] + public void PropertiesNotVisibleToParallelTargetBatches() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + 1.out + 2.out + + + + +

p1

+
+ +
+
+ ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "start:[]", "end:[p1]", "start:[]", "end:[p1]" }); + } + + // One input is built, the other is inferred + [TestMethod] + public void ItemsInPartialBuild() + { + string[] oldFiles = null, newFiles = null; + try + { + oldFiles = ObjectModelHelpers.GetTempFiles(2, new DateTime(2005, 1, 1)); + newFiles = ObjectModelHelpers.GetTempFiles(2, new DateTime(2006, 1, 1)); + + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + " + newFiles.First() + @" + " + oldFiles.ElementAt(1) + @" + + + + + + + + + + + + + ")))); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + // We should only see messages for the out of date inputs, but the itemgroup should do its work for both inputs + logger.AssertLogContains(new string[] { "start:[]", "end:[" + newFiles.ElementAt(1) + "]", "final:[" + oldFiles.First() + ";" + newFiles.ElementAt(1) + "]" }); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(oldFiles); + ObjectModelHelpers.DeleteTempFiles(newFiles); + } + } + + // One input is built, the other input is inferred + [TestMethod] + public void PropertiesInPartialBuild() + { + string[] oldFiles = null, newFiles = null; + try + { + oldFiles = ObjectModelHelpers.GetTempFiles(2, new DateTime(2005, 1, 1)); + newFiles = ObjectModelHelpers.GetTempFiles(2, new DateTime(2006, 1, 1)); + + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + " + newFiles.First() + @" + " + oldFiles.ElementAt(1) + @" + + + + + + + +

@(i)

+
+ +
+
+ ")))); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + // We should only see messages for the out of date inputs, but the propertygroup should do its work for both inputs + // Finally, execution wins over inferral, as the code chooses to do it that way + logger.AssertLogContains(new string[] { "start:[]", "end:[" + newFiles.ElementAt(1) + "]", "final:[" + newFiles.ElementAt(1) + "]" }); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(oldFiles); + ObjectModelHelpers.DeleteTempFiles(newFiles); + } + } + + // One input is built, the other is inferred + [TestMethod] + public void ItemsInPartialBuildVisibleToSubsequentlyInferringTasks() + { + string[] oldFiles = null, newFiles = null; + try + { + oldFiles = ObjectModelHelpers.GetTempFiles(2, new DateTime(2005, 1, 1)); + newFiles = ObjectModelHelpers.GetTempFiles(2, new DateTime(2006, 1, 1)); + string oldInput = oldFiles.First(); + string newInput = newFiles.ElementAt(1); + string oldOutput = oldFiles.ElementAt(1); + string newOutput = newFiles.First(); + + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + " + newOutput + @" + " + oldOutput + @" + + + + + + + + + + + + + + + + + ")))); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + // We should only see messages for the out of date inputs, but the itemgroup should do its work for both inputs; + // The final result should include the out of date inputs (twice) and the up to date inputs (twice). + // NOTE: outputs from regular tasks, like CreateItem, are gathered up and included in the project in the order (1) inferred (2) executed. + // Intrinsic tasks, because they affect the project directly, don't do this. So the final order we see is + // two inputs (old, new) from the ItemGroup; followed by the inferred CreateItem output, then the executed CreateItem output. + // I suggest this ordering isn't important: it's a new feature, so nobody will get broken. + logger.AssertLogContains(new string[] { "start:[" + newInput + "]", + "middle:[" + newInput + "][" + newInput + "]", + "end:[" + newInput + ";" + newInput + "]", + "final:[" + oldInput + ";" + newInput + ";" + oldInput + ";" + newInput + "]" }); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(oldFiles); + ObjectModelHelpers.DeleteTempFiles(newFiles); + } + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void IncludeNoOp() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + public void RemoveNoOp() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + [TestMethod] + public void RemoveItemInTarget() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + /// + /// Removes in one batch should never affect adds in a parallel batch, even if that + /// parallel batch ran first. + /// + [TestMethod] + public void RemoveOfItemAddedInTargetByParallelTargetBatchDoesNothing() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + 1.out + 2.out + + + + + + + + + + + + + + + + + + + + + + + + ")))); + p.Build(new string[] { "t", "t2" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "final:[a;b;d]" }); + } + + [TestMethod] + public void RemoveItemInTargetWithTransform() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + '%(filename).obj')""/> + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + [TestMethod] + public void RemoveWithMultipleIncludes() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + [TestMethod] + public void RemoveAllItemsInList() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + [TestMethod] + public void RemoveItemOutsideTarget() + { + // + // + // m1 + // n1 + // + // + // m2 + // n2 + // + // + // m3 + // n3 + // + // + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = GenerateLookup(task.Project); + + task.ExecuteTask(lookup); + + ICollection i0Group = lookup.GetItems("i0"); + + Assert.AreEqual(3, i0Group.Count); + Assert.AreEqual("a1", i0Group.First().EvaluatedInclude); + Assert.AreEqual("a3", i0Group.ElementAt(1).EvaluatedInclude); + Assert.AreEqual("a4", i0Group.ElementAt(2).EvaluatedInclude); + } + + /// + /// Bare (batchable) metadata is prohibited on IG/PG conditions -- all other expressions + /// should be allowed + /// + [TestMethod] + public void ConditionOnPropertyGroupUsingPropertiesAndItemListsAndTransforms() + { + // + // + // m1 + // + // + // m2 + // + // + // m3 + // + // + // + // v0 + // + string content = ObjectModelHelpers.CleanupFileContents(@" + + + '%(identity).x','|')'=='a1.x|a2.x|a3.x|a4.x'""> + v1 + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + + Lookup lookup = GenerateLookupWithItemsAndProperties(task.Project); + + task.ExecuteTask(lookup); + + string p1 = lookup.GetProperty("p1").EvaluatedValue; + + Assert.AreEqual("v1", p1); + } + + /// + /// Bare (batchable) metadata is prohibited on IG/PG conditions -- all other expressions + /// should be allowed + /// + [TestMethod] + public void ConditionOnItemGroupUsingPropertiesAndItemListsAndTransforms() + { + // + // + // m1 + // + // + // m2 + // + // + // m3 + // + // + // + // v0 + // + string content = ObjectModelHelpers.CleanupFileContents(@" + + + '%(identity).x','|')'=='a1.x|a2.x|a3.x|a4.x'""> + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + + Lookup lookup = GenerateLookupWithItemsAndProperties(task.Project); + + task.ExecuteTask(lookup); + + ICollection i1Group = lookup.GetItems("i1"); + + Assert.AreEqual(1, i1Group.Count); + Assert.AreEqual("x", i1Group.First().EvaluatedInclude); + } + + /// + /// This bug was caused by batching over the ItemGroup as well as over each child. + /// If the condition on a child did not exclude it, an unwitting child could be included multiple times, + /// once for each outer batch. The fix was to abandon the idea of outer batching and just + /// prohibit batchable expressions on the ItemGroup conditions. It's just too hard to write such expressions + /// in a comprehensible way. + /// + [TestMethod] + public void RegressPCHBug() + { + // + // + // m1 + // + // + // m2 + // + // + // m3 + // + // + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + '%(m).obj')"" Condition=""'%(i0.m)' == 'm1'""/> + '%(m)')"" Condition=""'%(i0.m)' == 'm2'""/> + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + + Lookup lookup = GenerateLookup(task.Project); + + task.ExecuteTask(lookup); + + ICollection linkGroup = lookup.GetItems("link"); + + Assert.AreEqual(4, linkGroup.Count); + Assert.AreEqual("A_PCH", linkGroup.First().EvaluatedInclude); + Assert.AreEqual("m1.obj", linkGroup.ElementAt(1).EvaluatedInclude); + Assert.AreEqual("m2", linkGroup.ElementAt(2).EvaluatedInclude); + Assert.AreEqual("m2", linkGroup.ElementAt(3).EvaluatedInclude); + } + + [TestMethod] + public void RemovesOfPersistedItemsAreReversed() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + // The item was removed during the build + logger.AssertLogContains("[]"); + Assert.AreEqual(0, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(0, p.ItemsToBuildWith.ItemTypes.Count); + + p = project.CreateProjectInstance(); + // We should still have the item left + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(1, p.ItemsToBuildWith.ItemTypes.Count); + } + + [TestMethod] + public void RemovesOfPersistedItemsAreReversed1() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[]"); + Assert.AreEqual(0, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(0, p.ItemsToBuildWith.ItemTypes.Count); + + p = project.CreateProjectInstance(); + // We should still have the item left + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(1, p.ItemsToBuildWith.ItemTypes.Count); + } + + [TestMethod] + public void RemovesOfPersistedItemsAreReversed2() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[a2;a1;a3][b1]"); + Assert.AreEqual(3, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(1, p.ItemsToBuildWith["i1"].Count); + Assert.AreEqual(2, p.ItemsToBuildWith.ItemTypes.Count); + + p = project.CreateProjectInstance(); + Assert.AreEqual(2, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(1, p.ItemsToBuildWith["i1"].Count); + Assert.AreEqual(2, p.ItemsToBuildWith.ItemTypes.Count); + } + + [TestMethod] + public void RemovesOfPersistedItemsAreReversed3() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + m1 + + + + + + m2 + + + + + + + ")))); + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[]"); + Assert.AreEqual(0, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(0, p.ItemsToBuildWith.ItemTypes.Count); + + p = project.CreateProjectInstance(); + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual("m1", p.ItemsToBuildWith["i0"].First().GetMetadataValue("m")); + Assert.AreEqual(1, p.ItemsToBuildWith.ItemTypes.Count); + } + + /// + /// Persisted item is copied into another item list by an ItemGroup -- the copy + /// should be reversed + /// + [TestMethod] + public void RemovesOfPersistedItemsAreReversed4() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[a1;a1][a1;a1]"); + Assert.AreEqual(2, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(2, p.ItemsToBuildWith["i1"].Count); + Assert.AreEqual(2, p.ItemsToBuildWith.ItemTypes.Count); + + p = project.CreateProjectInstance(); + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual("a1", p.ItemsToBuildWith["i0"].First().EvaluatedInclude); + Assert.AreEqual(0, p.ItemsToBuildWith["i1"].Count); + Assert.AreEqual(1, p.ItemsToBuildWith.ItemTypes.Count); + } + + [TestMethod] + public void RemovesOfItemsOnlyWithMetadataValue() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + m1 + + + + + + m2 + + + + + + + ")))); + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[m2]"); + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + } + + [TestMethod] + public void RemoveBatchingOnRemoveValue() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + m2 + + + + + + + + + + ")))); + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[m3]"); + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + } + + [TestMethod] + public void RemoveWithWildcards() + { + string[] files = null; + + try + { + files = ObjectModelHelpers.GetTempFiles(2, DateTime.Now); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + PropertyDictionary properties = new PropertyDictionary(); + properties.Set(ProjectPropertyInstance.Create("TEMP", Environment.GetEnvironmentVariable("TEMP"))); + Lookup lookup = LookupHelpers.CreateLookup(properties); + ExecuteTask(task, lookup); + + Assert.AreEqual(1, lookup.GetItems("i1").Count); + Assert.AreEqual("other", lookup.GetItems("i1").First().EvaluatedInclude); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(files); + } + } + + [TestMethod] + public void RemovesNotVisibleToParallelTargetBatches() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + 1.out + 2.out + + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "start:[1.in]", "end:[]", "start:[2.in]", "end:[]" }); + } + + [TestMethod] + public void RemovesNotVisibleToParallelTargetBatches2() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + 1.out + 2.out + + + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "start:[j1]", "end:[]", "start:[j1]", "end:[]" }); + } + +#if false // Not implemented yet: this was working when we were cloning, but now needs some thought. + + /// + /// The historical task output publishing model prevents a called target seeing outputs + /// from tasks in the same target that have already run. We choose to not follow this model + /// for itemgroups in targets. + /// + [TestMethod] + public void RemovesAreVisibleToCalledTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + + + + + + + + + + + + "))); + p.Build(new string[] { "t" }); + + logger.AssertLogContains(new string[] { "a:[i1;i2]", "b:[i1]", "c:[i1]", "d:[i1]" }); + } +#endif + + /// + /// Whidbey behavior was that items/properties emitted by a target being called, were + /// not visible to subsequent tasks in the calling target. (That was because the project + /// items and properties had been cloned for the target batches.) We must match that behavior. + /// + [TestMethod] + public void CalledTargetItemsAreNotVisibleToCallerTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + +

a

+
+ + + + + + + + + + + + + + + + +

$(p);$(q);c

+
+
+
+ ")))); + p.Build(new string[] { "t3" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "in target:[a][a]", "after target:[a;b;c][a;b;c]" }); + } + + /// + /// Items and properties should be visible within a CallTarget, even if the CallTargets are seperate tasks + /// + [TestMethod] + public void CalledTargetItemsAreVisibleWhenTargetsRunFromSeperateTasks() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + prop + + + + + + + + + + + + + + ")))); + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "Props During t1:[prop]", "Props During t2:[prop]", "Props After t1;t2:[]", "Props During Build:[prop]" }); + logger.AssertLogContains(new string[] { "Items During t1:[item]", "Items During t2:[item]", "Items After t1;t2:[]", "Items During Build:[item]" }); + } + + /// + /// Items and properties should be visible within a CallTarget, even if the targets + /// are Run Seperately + /// + [TestMethod] + public void CalledTargetItemsAreVisibleWhenTargetsRunSeperately() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + prop + + + + + + + + + + + + + + ")))); + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "Props During t1:[prop]", "Props During t2:[prop]", "Props After t1;t2:[]", "Props During Build:[prop]" }); + logger.AssertLogContains(new string[] { "Items During t1:[item]", "Items During t2:[item]", "Items After t1;t2:[]", "Items During Build:[item]" }); + } + + /// + /// Items and properties should be visible within a CallTarget, even if the targets + /// are Run Together + /// + [TestMethod] + public void CalledTargetItemsAreVisibleWhenTargetsRunTogether() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + prop + + + + + + + + + + + + + + ")))); + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "Props During t1:[prop]", "Props During t2:[prop]", "Props After t1;t2:[]", "Props During Build:[prop]" }); + logger.AssertLogContains(new string[] { "Items During t1:[item]", "Items During t2:[item]", "Items After t1;t2:[]", "Items During Build:[item]" }); + } + + /// + /// Whidbey behavior was that items/properties emitted by a target calling another target, were + /// not visible to the calling target. (That was because the project items and properties had been cloned for the target batches.) + /// We must match that behavior. (For now) + /// + [TestMethod] + public void CallerTargetItemsAreNotVisibleToCalledTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + +

a

+
+ + + + + + + + + + + + +

$(p);$(q);c

+
+ +
+ + + +
+ ")))); + p.Build(new string[] { "t3" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "in target:[a][a]", "after target:[a;b;c][a;b;c]" }); + } + + [TestMethod] + public void ModifyNoOp() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + [TestMethod] + public void ModifyItemInTarget() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ProjectItemInstance item = lookup.GetItems("i1").First(); + Assert.AreEqual("m2", item.GetMetadataValue("m")); + } + + [TestMethod] + public void ModifyItemInTargetComplex() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + true + v3 + + + + + + + + + + v1 + v2 + $(p2) + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(@"[item1|v1|v2|v3]"); + } + + [TestMethod] + public void ModifyItemInTargetLastMetadataWins() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + m3 + m4 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ProjectItemInstance item = lookup.GetItems("i1").First(); + Assert.AreEqual("m3", item.GetMetadataValue("m")); + } + + [TestMethod] + public void ModifyItemEmittedByTask() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + m2 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "[m2][n1]" }); + } + + [TestMethod] + public void ModifyItemInTargetWithCondition() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + + + m3 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ProjectItemInstance item1 = lookup.GetItems("i1").First(); + ProjectItemInstance item2 = lookup.GetItems("i1").ElementAt(1); + Assert.AreEqual("a1", item1.EvaluatedInclude); + Assert.AreEqual("a2", item2.EvaluatedInclude); + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("m3", item2.GetMetadataValue("m")); + } + + [TestMethod] + public void ModifyItemInTargetWithConditionOnMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + + + m3 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ProjectItemInstance item1 = lookup.GetItems("i1").First(); + ProjectItemInstance item2 = lookup.GetItems("i1").ElementAt(1); + Assert.AreEqual("a1", item1.EvaluatedInclude); + Assert.AreEqual("a2", item2.EvaluatedInclude); + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("m3", item2.GetMetadataValue("m")); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ModifyItemWithUnqualifiedMetadataError() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + 2 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + ExecuteTask(task, null); + } + + [TestMethod] + public void ModifyItemInTargetWithConditionWithoutItemTypeOnMetadataInCondition() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + + + m3 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ProjectItemInstance item1 = lookup.GetItems("i1").First(); + ProjectItemInstance item2 = lookup.GetItems("i1").ElementAt(1); + Assert.AreEqual("a1", item1.EvaluatedInclude); + Assert.AreEqual("a2", item2.EvaluatedInclude); + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("m3", item2.GetMetadataValue("m")); + } + + + [TestMethod] + public void ModifyItemInTargetWithConditionOnMetadataWithoutItemTypeOnMetadataInCondition() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + m1 + + + m2 + + + m3 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + ExecuteTask(task, lookup); + + ProjectItemInstance item1 = lookup.GetItems("i1").First(); + ProjectItemInstance item2 = lookup.GetItems("i1").ElementAt(1); + Assert.AreEqual("a1", item1.EvaluatedInclude); + Assert.AreEqual("a2", item2.EvaluatedInclude); + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("m3", item2.GetMetadataValue("m")); + } + + [TestMethod] + public void ModifyItemOutsideTarget() + { + // + // + // m1 + // n1 + // + // + // m2 + // n2 + // + // + // m3 + // n3 + // + // + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m4 + + + "); + IntrinsicTask task = CreateIntrinsicTask(content); + + Lookup lookup = GenerateLookup(task.Project); + + task.ExecuteTask(lookup); + + ICollection i0Group = lookup.GetItems("i0"); + + Assert.AreEqual(4, i0Group.Count); + foreach (ProjectItemInstance item in i0Group) + { + Assert.AreEqual("m4", item.GetMetadataValue("m")); + } + } + + [TestMethod] + public void RemoveComplexMidlExample() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + true + dlldata.c + dlldatadir + headerdir + tlbdir + proxydir + interfacedir + + + + + + mydlldata.c + + + myheader.h + + + + + + + + $(MidlDllDataDir)\%(Filename)_dlldata.c + $(MidlDllDataFileName) + $(MidlHeaderDir)\%(Idl.Filename).h + $(MidlTlbDir)\%(Filename).tlb + $(MidlProxyDir)\%(Filename)_p.c + $(MidlInterfaceDir)\%(Filename)_i.c + + + + + + + ")))); + p.Build(new string[] { "MIDL" }, new ILogger[] { logger }); + + logger.AssertLogContains(@"[a.idl|dlldatadir\a_dlldata.c|headerdir\a.h|tlbdir\a.tlb|proxydir\a_p.c|interfacedir\a_i.c]", + @"[b.idl|mydlldata.c|headerdir\b.h|tlbdir\b.tlb|proxydir\b_p.c|interfacedir\b_i.c]", + @"[c.idl|dlldatadir\c_dlldata.c|myheader.h|tlbdir\c.tlb|proxydir\c_p.c|interfacedir\c_i.c]"); + } + + [TestMethod] + public void ModifiesOfPersistedItemsAreReversed1() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + m0 + + + + + + m1 + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t", "t2" }, new ILogger[] { logger }); + + logger.AssertLogContains("[m1]"); + + ProjectItemInstance item = p.ItemsToBuildWith["i0"].First(); + Assert.AreEqual("m1", item.GetMetadataValue("m")); + + p = project.CreateProjectInstance(); + item = p.ItemsToBuildWith["i0"].First(); + Assert.AreEqual("m0", item.GetMetadataValue("m")); + } + + /// + /// Modify of an item copied during the build + /// + [TestMethod] + public void ModifiesOfPersistedItemsAreReversed2() + { + MockLogger logger = new MockLogger(); + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + m0 + n0 + + + + + + m1 + + + n1 + + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "t", "t2" }, new ILogger[] { logger }); + + logger.AssertLogContains("[m0][n0]", "[m1][n1]"); + + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(1, p.ItemsToBuildWith["i1"].Count); + Assert.AreEqual("m0", p.ItemsToBuildWith["i0"].First().GetMetadataValue("m")); + Assert.AreEqual("n0", p.ItemsToBuildWith["i0"].First().GetMetadataValue("n")); + Assert.AreEqual("m1", p.ItemsToBuildWith["i1"].First().GetMetadataValue("m")); + Assert.AreEqual("n1", p.ItemsToBuildWith["i1"].First().GetMetadataValue("n")); + + p = project.CreateProjectInstance(); + Assert.AreEqual(1, p.ItemsToBuildWith["i0"].Count); + Assert.AreEqual(0, p.ItemsToBuildWith["i1"].Count); + Assert.AreEqual("m0", p.ItemsToBuildWith["i0"].First().GetMetadataValue("m")); + Assert.AreEqual("n0", p.ItemsToBuildWith["i0"].First().GetMetadataValue("n")); + } + + + /// + /// The case is where a transform is done on an item to generate a pdb file name when the extension of an item is dll + /// the resulting items is expected to have an extension metadata of pdb but instead has an extension of dll + /// + [TestMethod] + public void IncludeCheckOnMetadata() + { + MockLogger logger = new MockLogger(); + + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + '%(FileName).pdb')"" Condition=""'%(Content.Extension)' == '.dll'""/> + + + + + ")))); + bool success = p.Build(new string[] { "a" }, new ILogger[] { logger }); + Assert.IsTrue(success); + logger.AssertLogContains("[a.dll]->[.dll]"); + logger.AssertLogContains("[a.pdb]->[.pdb]"); + } + + /// + /// The case is where a transform is done on an item to generate a pdb file name the batching is done on the identity. + /// If the identity was also copied over then we would only get one bucket instead of two buckets + /// + [TestMethod] + public void IncludeCheckOnMetadata2() + { + MockLogger logger = new MockLogger(); + + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + '%(FileName)%(Extension).pdb')""/> + '%(FileName)%(Extension).pdb')"" Condition=""'%(Content.Identity)' != ''""/> + + + + + ")))); + bool success = p.Build(new string[] { "a" }, new ILogger[] { logger }); + Assert.IsTrue(success); + logger.AssertLogContains("[a.dll]->[.dll]"); + logger.AssertLogContains("[a.dll.pdb]->[.pdb]"); + logger.AssertLogContains("[a.dll.pdb.pdb]->[.pdb]"); + } + + /// + /// Make sure that recursive dir still gets the right file + /// + /// + [TestMethod] + public void IncludeCheckOnMetadata_3() + { + MockLogger logger = new MockLogger(); + + string tempPath = Path.GetTempPath(); + string directoryForTest = Path.Combine(tempPath, "IncludeCheckOnMetadata_3\\Test"); + string fileForTest = Path.Combine(directoryForTest, "a.dll"); + + try + { + if (Directory.Exists(directoryForTest)) + { + Directory.Delete(directoryForTest, true); + } + else + { + Directory.CreateDirectory(directoryForTest); + } + + File.WriteAllText(fileForTest, fileForTest); + + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + ")))); + bool success = p.Build(new string[] { "a" }, new ILogger[] { logger }); + Assert.IsTrue(success); + logger.AssertLogContains("[a.dll]->[.dll]->[]"); + logger.AssertLogContains("[" + directoryForTest + @"\..\Test\a.dll]->[.dll]->[Test\]"); + } + finally + { + if (Directory.Exists(directoryForTest)) + { + Directory.Delete(directoryForTest, true); + } + } + } + + [TestMethod] + public void RemoveItemInImportedFile() + { + MockLogger logger = new MockLogger(); + string importedFile = null; + + try + { + importedFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(importedFile, ObjectModelHelpers.CleanupFileContents(@" + + + + + + ")); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[imported]", "[]"); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(new string[] { importedFile }); + } + } + + [TestMethod] + public void ModifyItemInImportedFile() + { + MockLogger logger = new MockLogger(); + string importedFile = null; + + try + { + importedFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(importedFile, ObjectModelHelpers.CleanupFileContents(@" + + + + + + ")); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + m1 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[m1]"); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(new string[] { importedFile }); + } + } + + /// + /// Properties produced in one target batch are not visible to another + /// + [TestMethod] + public void OutputPropertiesInTargetBatchesCreateItem() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + 1.out + 2.out + + + + + + + + + + + + + ")))); + p.Build(new string[] { "t", "t2" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "start:[]", "end:[--1.in]", "start:[]", "end:[--2.in]", "final:[--2.in]" }); + } + + /// + /// Properties produced in one task batch are not visible to another + /// + [TestMethod] + public void OutputPropertiesInTaskBatchesCreateItem() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains(new string[] { "end:[--2.in]" }); + } + + /// + /// In this case gen.cpp was getting ObjectFile of def.obj. + /// + [TestMethod] + public void PhoenixBatchingIssue() + { + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + def.obj + + + + + + + true + + + %(Filename).obj + + + + + ")))); + ProjectInstance instance = new ProjectInstance(xml); + instance.Build(); + + Assert.AreEqual(2, instance.Items.Count()); + Assert.AreEqual("gen.obj", instance.GetItems("CppCompile").First().GetMetadataValue("ObjectFile")); + Assert.AreEqual("def.obj", instance.GetItems("CppCompile").Last().GetMetadataValue("ObjectFile")); + } + + [TestMethod] + public void PropertiesInInferredBuildCreateProperty() + { + string[] files = null; + try + { + files = ObjectModelHelpers.GetTempFiles(2, new DateTime(2005, 1, 1)); + + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + " + files.ElementAt(1) + @" + + + + + + + + + + + + + ")))); + p.Build(new string[] { "t2" }, new ILogger[] { logger }); + + // We should only see messages from the second target, as the first is only inferred + logger.AssertLogDoesntContain("start:"); + logger.AssertLogDoesntContain("end:"); + logger.AssertLogContains(new string[] { "final:[" + files.First() + "]" }); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(files); + } + } + + [TestMethod] + public void ModifyItemPreviouslyModified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + 1 + + + 2 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogDoesntContain("[1]"); + logger.AssertLogContains("[2]"); + } + + [TestMethod] + public void ModifyItemPreviouslyModified2() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + 1 + + + + + 2 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogDoesntContain("[1]"); + logger.AssertLogContains("[2]"); + } + + [TestMethod] + public void RemoveItemPreviouslyModified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + 1 + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogDoesntContain("[1]"); + logger.AssertLogDoesntContain("[2]"); + } + + [TestMethod] + public void RemoveItemPreviouslyModified2() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + 1 + + + + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogDoesntContain("[1]"); + logger.AssertLogDoesntContain("[2]"); + } + + [TestMethod] + public void FilterItemPreviouslyModified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + 1 + + + 2 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogDoesntContain("[1]"); + logger.AssertLogContains("[2]"); + } + + [TestMethod] + public void FilterItemPreviouslyModified2() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + 1 + + + 2 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogDoesntContain("[1]"); + logger.AssertLogContains("[2]"); + } + + [TestMethod] + public void FilterItemPreviouslyModified3() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + +
+ m1 + + + + + + m2 + + + + + m3 + + + + + m4 + + + + +
+ ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[a;b;c = m4]"); + } + + [TestMethod] + public void FilterItemPreviouslyModified4() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + m2 + + + m3 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[b = m1]"); + logger.AssertLogContains("[a;c = m3]"); + } + + [TestMethod] + public void FilterItemPreviouslyModified5() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + m2 + + + m3 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[a = m3]"); + logger.AssertLogContains("[b = m1]"); + logger.AssertLogContains("[c = m2]"); + } + + [TestMethod] + public void FilterItemPreviouslyModified6() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + m1 + + + + + + m2 + + + + + + + + + + m3 + + + + + + ")))); + p.Build(new string[] { "t" }, new ILogger[] { logger }); + + logger.AssertLogContains("[a;b;c=]"); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + + #region Helpers + + private static PropertyDictionary GeneratePropertyGroup() + { + PropertyDictionary properties = new PropertyDictionary(); + properties.Set(ProjectPropertyInstance.Create("p0", "v0")); + return properties; + } + + private static Lookup GenerateLookupWithItemsAndProperties(ProjectInstance project) + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("p0", "v0")); + + Lookup lookup = GenerateLookup(project, pg); + return lookup; + } + + private static Lookup GenerateLookup(ProjectInstance project) + { + return GenerateLookup(project, new PropertyDictionary()); + } + + private static Lookup GenerateLookup(ProjectInstance project, PropertyDictionary properties) + { + List items = new List(); + ProjectItemInstance item1 = new ProjectItemInstance(project, "i0", "a1", project.FullPath); + ProjectItemInstance item2 = new ProjectItemInstance(project, "i0", "a2", project.FullPath); + ProjectItemInstance item3 = new ProjectItemInstance(project, "i0", "a3", project.FullPath); + ProjectItemInstance item4 = new ProjectItemInstance(project, "i0", "a4", project.FullPath); + item1.SetMetadata("m", "m1"); + item1.SetMetadata("n", "n1"); + item2.SetMetadata("m", "m2"); + item2.SetMetadata("n", "n2"); + item3.SetMetadata("m", "m2"); + item3.SetMetadata("n", "n2"); + item4.SetMetadata("m", "m3"); + item4.SetMetadata("n", "n3"); + items.Add(item1); + items.Add(item2); + items.Add(item3); + items.Add(item4); + ItemDictionary itemsByName = new ItemDictionary(); + itemsByName.ImportItems(items); + + Lookup lookup = LookupHelpers.CreateLookup(properties, itemsByName); + + return lookup; + } + + private static IntrinsicTask CreateIntrinsicTask(string content) + { + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectTargetInstanceChild targetChild = projectInstance.Targets["t"].Children.First(); + + NodeLoggingContext nodeContext = new NodeLoggingContext(new MockLoggingService(), 1, false); + BuildRequestEntry entry = new BuildRequestEntry(new BuildRequest(1 /* submissionId */, 0, 1, new string[] { "t" }, null, BuildEventContext.Invalid, null), new BuildRequestConfiguration(1, new BuildRequestData("projectFile", new Dictionary(), "3.5", new string[0], null), "2.0")); + entry.RequestConfiguration.Project = projectInstance; + IntrinsicTask task = IntrinsicTask.InstantiateTask( + targetChild, + nodeContext.LogProjectStarted(entry).LogTargetBatchStarted(projectInstance.FullPath, projectInstance.Targets["t"], null), + projectInstance, + false); + + return task; + } + + private void ExecuteTask(IntrinsicTask task) + { + ExecuteTask(task, null); + } + + private void ExecuteTask(IntrinsicTask task, Lookup lookup) + { + if (lookup == null) + { + lookup = LookupHelpers.CreateEmptyLookup(); + } + + task.ExecuteTask(lookup); + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingContext_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingContext_Tests.cs new file mode 100644 index 00000000000..d235ae8a00d --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingContext_Tests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for logging contexts. +//----------------------------------------------------------------------- + +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for logging contexts. + /// + [TestClass] + public class LoggingContext_Tests + { + /// + /// A few simple tests for NodeLoggingContexts. + /// + [TestMethod] + public void CreateValidNodeLoggingContexts() + { + NodeLoggingContext context = new NodeLoggingContext(new MockLoggingService(), 1, true); + Assert.AreEqual(true, context.IsInProcNode); + Assert.IsTrue(context.IsValid); + + context.LogBuildFinished(true); + Assert.IsFalse(context.IsValid); + + Assert.AreEqual(1, context.BuildEventContext.NodeId); + + NodeLoggingContext context2 = new NodeLoggingContext(new MockLoggingService(), 2, false); + Assert.AreEqual(false, context2.IsInProcNode); + Assert.IsTrue(context2.IsValid); + + context2.LogBuildFinished(true); + Assert.IsFalse(context2.IsValid); + + Assert.AreEqual(2, context2.BuildEventContext.NodeId); + } + + /// + /// Verifies that if an invalid node ID is passed to the NodeLoggingContext, it throws + /// an exception -- this is to guarantee that if we're passing around invalid node IDs, + /// we'll know about it. + /// + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void InvalidNodeIdOnNodeLoggingContext() + { + NodeLoggingContext context = new NodeLoggingContext(new MockLoggingService(), -2, true); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServiceFactory_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServiceFactory_Tests.cs new file mode 100644 index 00000000000..d3fa0a6cd0a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServiceFactory_Tests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Verify the LoggingService Factory +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + ///Test the Factory to create components of the type LoggingService + /// + [TestClass] + public class LoggingServiceFactory_Tests + { + /// + /// Verify we can create a synchronous LoggingService + /// + [TestMethod] + public void TestCreateSynchronousLogger() + { + LoggingServiceFactory factory = new LoggingServiceFactory(LoggerMode.Synchronous, 1); + LoggingService loggingService = (LoggingService)factory.CreateInstance(BuildComponentType.LoggingService); + Assert.IsTrue(loggingService.LoggingMode == LoggerMode.Synchronous, "Expected to create a Synchronous LoggingService"); + } + + /// + /// Verify we can create a Asynchronous LoggingService + /// + [TestMethod] + public void TestCreateAsynchronousLogger() + { + LoggingServiceFactory factory = new LoggingServiceFactory(LoggerMode.Asynchronous, 1); + LoggingService loggingService = (LoggingService)factory.CreateInstance(BuildComponentType.LoggingService); + Assert.IsTrue(loggingService.LoggingMode == LoggerMode.Asynchronous, "Expected to create an Asynchronous LoggingService"); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingService_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingService_Tests.cs new file mode 100644 index 00000000000..8b828453224 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingService_Tests.cs @@ -0,0 +1,1141 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Test the logging service component +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Logging; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests.BackEnd; +using System.IO; +using System.Threading; +using System.Reflection; +using System.Collections.Generic; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + /// Test the logging service component + /// + [TestClass] + public class LoggingService_Tests + { + #region Data + /// + /// An already instantiated and initialized service. + /// This is used so the host object does not need to be + /// used in every test method. + /// + private LoggingService _initializedService; + + /// + /// The event signalled when shutdown is complete. + /// + private ManualResetEvent _shutdownComplete = new ManualResetEvent(false); + + #endregion + + #region Setup + + /// + /// This method is run before each test case is run. + /// We instantiate and initialize a new logging service each time + /// + [TestInitialize] + public void Setup() + { + InitializeLoggingService(); + } + + #endregion + + #region Test BuildComponent Methods + + /// + /// Verify the CreateLogger method create a LoggingService in both Synchronous mode + /// and Asynchronous mode. + /// + [TestMethod] + public void CreateLogger() + { + // Generic host which has some default properties set inside of it + IBuildComponent logServiceComponent = (IBuildComponent)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + + // Create a synchronous logging service and do some quick checks + Assert.IsNotNull(logServiceComponent); + LoggingService logService = (LoggingService)logServiceComponent; + Assert.IsTrue(logService.LoggingMode == LoggerMode.Synchronous); + Assert.IsTrue(logService.ServiceState == LoggingServiceState.Instantiated); + + // Create an asynchronous logging service + logServiceComponent = (IBuildComponent)LoggingService.CreateLoggingService(LoggerMode.Asynchronous, 1); + Assert.IsNotNull(logServiceComponent); + logService = (LoggingService)logServiceComponent; + Assert.IsTrue(logService.LoggingMode == LoggerMode.Asynchronous); + Assert.IsTrue(logService.ServiceState == LoggingServiceState.Instantiated); + + // Shutdown logging thread + logServiceComponent.InitializeComponent(new MockHost()); + logServiceComponent.ShutdownComponent(); + Assert.IsTrue(logService.ServiceState == LoggingServiceState.Shutdown); + } + + /// + /// Test the IBuildComponent method InitializeComponent, make sure the component gets the parameters it expects + /// + [TestMethod] + public void InitializeComponent() + { + IBuildComponent logServiceComponent = (IBuildComponent)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + + BuildParameters parameters = new BuildParameters(); + parameters.MaxNodeCount = 4; + parameters.OnlyLogCriticalEvents = true; + + IBuildComponentHost loggingHost = new MockHost(parameters); + + // Make sure we are in the Instantiated state before initializing + Assert.IsTrue(((LoggingService)logServiceComponent).ServiceState == LoggingServiceState.Instantiated); + + logServiceComponent.InitializeComponent(loggingHost); + + // Makesure that the parameters in the host are set in the logging service + LoggingService service = (LoggingService)logServiceComponent; + Assert.IsTrue(service.ServiceState == LoggingServiceState.Initialized); + Assert.IsTrue(service.MaxCPUCount == 4); + Assert.IsTrue(service.OnlyLogCriticalEvents == true); + } + + /// + /// Verify the correct exception is thrown when a null Component host is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InitializeComponentNullHost() + { + IBuildComponent logServiceComponent = (IBuildComponent)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + logServiceComponent.InitializeComponent(null); + } + + /// + /// Verify an exception is thrown if in itialized is called after the service has been shutdown + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InitializeComponentAfterShutdown() + { + _initializedService.ShutdownComponent(); + _initializedService.InitializeComponent(new MockHost()); + } + + /// + /// Verify the correct exceptions are thrown if the loggers crash + /// when they are shutdown + /// + [TestMethod] + public void ShutDownComponentExceptionsInForwardingLogger() + { + // Cause a logger exception in the shutdown of the logger + string className = "Microsoft.Build.UnitTests.Logging.LoggingService_Tests+ShutdownLoggerExceptionFL"; + Type exceptionType = typeof(LoggerException); + VerifyShutdownExceptions(null, className, exceptionType); + Assert.IsTrue(_initializedService.ServiceState == LoggingServiceState.Shutdown); + + // Cause a general exception which should result in an InternalLoggerException + className = "Microsoft.Build.UnitTests.Logging.LoggingService_Tests+ShutdownGeneralExceptionFL"; + exceptionType = typeof(InternalLoggerException); + VerifyShutdownExceptions(null, className, exceptionType); + Assert.IsTrue(_initializedService.ServiceState == LoggingServiceState.Shutdown); + + // Cause a StackOverflow exception in the shutdown of the logger + // this kind of exception should not be caught + className = "Microsoft.Build.UnitTests.Logging.LoggingService_Tests+ShutdownStackoverflowExceptionFL"; + exceptionType = typeof(StackOverflowException); + VerifyShutdownExceptions(null, className, exceptionType); + + Assert.IsTrue(_initializedService.ServiceState == LoggingServiceState.Shutdown); + } + + /// + /// Verify the correct exceptions are thrown when ILoggers + /// throw exceptions during shutdown + /// + [TestMethod] + public void ShutDownComponentExceptionsInLogger() + { + LoggerThrowException logger = new LoggerThrowException(true, false, new LoggerException("Hello")); + VerifyShutdownExceptions(logger, null, typeof(LoggerException)); + + logger = new LoggerThrowException(true, false, new Exception("boo")); + VerifyShutdownExceptions(logger, null, typeof(InternalLoggerException)); + + logger = new LoggerThrowException(true, false, new StackOverflowException()); + VerifyShutdownExceptions(logger, null, typeof(StackOverflowException)); + + Assert.IsTrue(_initializedService.ServiceState == LoggingServiceState.Shutdown); + } + + /// + /// Log some events on one thread and verify that even + /// when events are being logged while shutdown is occuring + /// that the shutdown still completes. + /// + [TestMethod] + public void ShutdownWaitForEvents() + { + // LoggingBuildComponentHost loggingHost = new LoggingBuildComponentHost(); + // loggingHost.NumberOfNodes = 2; + // IBuildComponent logServiceComponent = LoggingService.CreateLogger(LoggerMode.Asynchronous); + // initializedService = logServiceComponent as LoggingService; + // shutdownComplete = new ManualResetEvent(false); + // Thread loggingThread = new Thread(new ThreadStart(TightLoopLogEvents)); + // loggingThread.Start(); + // Give it time to log some events + // Thread.Sleep(100); + // initializedService.ShutdownComponent(); + // Assert.IsFalse(initializedService.LoggingQueueHasEvents); + // shutdownComplete.Set(); + // if (initializedService.ServiceState != LoggingServiceState.Shutdown) + // { + // Assert.Fail(); + // } + } + + /// + /// Make sure an exception is thrown if shutdown is called + /// more than once + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void DoubleShutdown() + { + _initializedService.ShutdownComponent(); + _initializedService.ShutdownComponent(); + } + + #endregion + + #region RegisterLogger + /// + /// Verify we get an exception when a null logger is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NullLogger() + { + _initializedService.RegisterLogger(null); + } + + /// + /// Verify we get an exception when we try and register a logger + /// and the system has already shutdown + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void RegisterLoggerServiceShutdown() + { + _initializedService.ShutdownComponent(); + RegularILogger regularILogger = new RegularILogger(); + _initializedService.RegisterLogger(regularILogger); + } + + /// + /// Verify a logger exception when initializing a logger is rethrown + /// as a logger exception + /// + [TestMethod] + [ExpectedException(typeof(LoggerException))] + public void LoggerExceptionInInitialize() + { + LoggerThrowException exceptionLogger = new LoggerThrowException(false, true, new LoggerException()); + _initializedService.RegisterLogger(exceptionLogger); + } + + /// + /// Verify a general exception when initializing a logger is wrapped + /// as a InternalLogger exception + /// + [TestMethod] + [ExpectedException(typeof(InternalLoggerException))] + public void GeneralExceptionInInitialize() + { + LoggerThrowException exceptionLogger = new LoggerThrowException(false, true, new Exception()); + _initializedService.RegisterLogger(exceptionLogger); + } + + /// + /// Verify a critical exception is not wrapped + /// + [TestMethod] + [ExpectedException(typeof(StackOverflowException))] + public void ILoggerExceptionInInitialize() + { + LoggerThrowException exceptionLogger = new LoggerThrowException(false, true, new StackOverflowException()); + _initializedService.RegisterLogger(exceptionLogger); + } + + /// + /// Register an good Logger and verify it was registered. + /// + [TestMethod] + public void RegisterILoggerAndINodeLoggerGood() + { + ConsoleLogger consoleLogger = new ConsoleLogger(); + RegularILogger regularILogger = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterLogger(consoleLogger)); + Assert.IsTrue(_initializedService.RegisterLogger(regularILogger)); + Assert.IsNotNull(_initializedService.RegisteredLoggerTypeNames); + + // Should have 2 central loggers and 1 forwarding logger + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Count == 3); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.BackEnd.Logging.CentralForwardingLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.UnitTests.Logging.LoggingService_Tests+RegularILogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.ConsoleLogger")); + + // Should have 1 event sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.IsTrue(_initializedService.RegisteredSinkNames.Count == 1); + } + + /// + /// Try and register the same logger multiple times + /// + [TestMethod] + public void RegisterDuplicateLogger() + { + ConsoleLogger consoleLogger = new ConsoleLogger(); + RegularILogger regularILogger = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterLogger(consoleLogger)); + Assert.IsFalse(_initializedService.RegisterLogger(consoleLogger)); + Assert.IsTrue(_initializedService.RegisterLogger(regularILogger)); + Assert.IsFalse(_initializedService.RegisterLogger(regularILogger)); + Assert.IsNotNull(_initializedService.RegisteredLoggerTypeNames); + + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Count == 3); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.BackEnd.Logging.CentralForwardingLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.UnitTests.Logging.LoggingService_Tests+RegularILogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.ConsoleLogger")); + + // Should have 1 event sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.IsTrue(_initializedService.RegisteredSinkNames.Count == 1); + } + + #endregion + + #region RegisterDistributedLogger + /// + /// Verify we get an exception when a null logger forwarding logger is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NullForwardingLogger() + { + _initializedService.RegisterDistributedLogger(null, null); + } + + /// + /// Verify we get an exception when we try and register a distributed logger + /// and the system has already shutdown + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void RegisterDistributedLoggerServiceShutdown() + { + _initializedService.ShutdownComponent(); + string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + _initializedService.RegisterDistributedLogger(null, description); + } + + /// + /// Register both a good central logger and a good forwarding logger + /// + [TestMethod] + public void RegisterGoodDistributedAndCentralLogger() + { + string configurableClassName = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + string distributedClassName = "Microsoft.Build.Logging.DistributedFileLogger"; + LoggerDescription configurableDescription = CreateLoggerDescription(configurableClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + LoggerDescription distributedDescription = CreateLoggerDescription(distributedClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + + DistributedFileLogger fileLogger = new DistributedFileLogger(); + RegularILogger regularILogger = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(regularILogger, configurableDescription)); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(null, distributedDescription)); + Assert.IsNotNull(_initializedService.RegisteredLoggerTypeNames); + + // Should have 2 central loggers and 2 forwarding logger + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Count == 4); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.ConfigurableForwardingLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.UnitTests.Logging.LoggingService_Tests+RegularILogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.DistributedFileLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.BackEnd.Logging.NullCentralLogger")); + + // Should have 2 event sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.AreEqual(2, _initializedService.RegisteredSinkNames.Count); + Assert.AreEqual(2, _initializedService.LoggerDescriptions.Count); + } + + /// + /// Have a one forwarding logger which forwards build started and finished and have one which does not and a regular logger. Expect the central loggers to all get + /// one build started and one build finished event only. + /// + [TestMethod] + public void RegisterGoodDistributedAndCentralLoggerTestBuildStartedFinished() + { + string configurableClassNameA = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + string configurableClassNameB = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + + LoggerDescription configurableDescriptionA = CreateLoggerDescription(configurableClassNameA, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + LoggerDescription configurableDescriptionB = CreateLoggerDescription(configurableClassNameB, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, false); + + RegularILogger regularILoggerA = new RegularILogger(); + RegularILogger regularILoggerB = new RegularILogger(); + RegularILogger regularILoggerC = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(regularILoggerA, configurableDescriptionA)); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(regularILoggerB, configurableDescriptionB)); + Assert.IsTrue(_initializedService.RegisterLogger(regularILoggerC)); + Assert.IsNotNull(_initializedService.RegisteredLoggerTypeNames); + + _initializedService.LogBuildStarted(); + Assert.IsTrue(regularILoggerA.BuildStartedCount == 1); + Assert.IsTrue(regularILoggerB.BuildStartedCount == 1); + Assert.IsTrue(regularILoggerC.BuildStartedCount == 1); + + _initializedService.LogBuildFinished(true); + Assert.IsTrue(regularILoggerA.BuildFinishedCount == 1); + Assert.IsTrue(regularILoggerB.BuildFinishedCount == 1); + Assert.IsTrue(regularILoggerC.BuildFinishedCount == 1); + + // Make sure if we call build started again we only get one other build started event. + _initializedService.LogBuildStarted(); + Assert.IsTrue(regularILoggerA.BuildStartedCount == 2); + Assert.IsTrue(regularILoggerB.BuildStartedCount == 2); + Assert.IsTrue(regularILoggerC.BuildStartedCount == 2); + + // Make sure if we call build started again we only get one other build started event. + _initializedService.LogBuildFinished(true); + Assert.IsTrue(regularILoggerA.BuildFinishedCount == 2); + Assert.IsTrue(regularILoggerB.BuildFinishedCount == 2); + Assert.IsTrue(regularILoggerC.BuildFinishedCount == 2); + } + + /// + /// Try and register a duplicate central logger + /// + [TestMethod] + public void RegisterDuplicateCentralLogger() + { + string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + + RegularILogger regularILogger = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(regularILogger, description)); + Assert.IsFalse(_initializedService.RegisterDistributedLogger(regularILogger, description)); + + // Should have 2 central loggers and 1 forwarding logger + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Count == 2); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.ConfigurableForwardingLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.UnitTests.Logging.LoggingService_Tests+RegularILogger")); + + // Should have 1 sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.AreEqual(1, _initializedService.RegisteredSinkNames.Count); + Assert.AreEqual(1, _initializedService.LoggerDescriptions.Count); + } + + /// + /// Try and register a duplicate Forwarding logger + /// + [TestMethod] + public void RegisterDuplicateForwardingLoggerLogger() + { + string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + + RegularILogger regularILogger = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(regularILogger, description)); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(null, description)); + Assert.AreEqual(4, _initializedService.RegisteredLoggerTypeNames.Count); + + // Verify there are two versions in the type names, one for each description + int countForwardingLogger = 0; + foreach (string loggerName in _initializedService.RegisteredLoggerTypeNames) + { + if (String.Compare("Microsoft.Build.Logging.ConfigurableForwardingLogger", loggerName, StringComparison.OrdinalIgnoreCase) == 0) + { + countForwardingLogger++; + } + } + + Assert.AreEqual(2, countForwardingLogger); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.BackEnd.Logging.NullCentralLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.UnitTests.Logging.LoggingService_Tests+RegularILogger")); + + // Should have 2 sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.AreEqual(2, _initializedService.RegisteredSinkNames.Count); + Assert.AreEqual(2, _initializedService.LoggerDescriptions.Count); + } + + #endregion + + #region RegisterLoggerDescriptions + /// + /// Verify we get an exception when a null description collection is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NullDescriptionCollection() + { + _initializedService.InitializeNodeLoggers(null, new EventSourceSink(), 3); + } + + /// + /// Verify we get an exception when an empty description collection is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void EmptyDescriptionCollection() + { + _initializedService.InitializeNodeLoggers(new List(), new EventSourceSink(), 3); + } + + /// + /// Verify we get an exception when we try and register a description and the component has already shutdown + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NullForwardingLoggerSink() + { + string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + _initializedService.ShutdownComponent(); + List tempList = new List(); + tempList.Add(description); + _initializedService.InitializeNodeLoggers(tempList, new EventSourceSink(), 2); + } + + /// + /// Register both a good central logger and a good forwarding logger + /// + [TestMethod] + public void RegisterGoodDiscriptions() + { + string configurableClassName = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + string distributedClassName = "Microsoft.Build.BackEnd.Logging.CentralForwardingLogger"; + EventSourceSink sink = new EventSourceSink(); + EventSourceSink sink2 = new EventSourceSink(); + List loggerDescriptions = new List(); + loggerDescriptions.Add(CreateLoggerDescription(configurableClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true)); + loggerDescriptions.Add(CreateLoggerDescription(distributedClassName, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true)); + + // Register some descriptions with a sink + _initializedService.InitializeNodeLoggers(loggerDescriptions, sink, 1); + + // Register the same descriptions with another sink (so we can see that another sink was added) + _initializedService.InitializeNodeLoggers(loggerDescriptions, sink2, 1); + + // Register the descriptions again with the same sink so we can verify that another sink was not created + _initializedService.InitializeNodeLoggers(loggerDescriptions, sink, 1); + + Assert.IsNotNull(_initializedService.RegisteredLoggerTypeNames); + + // Should have 6 forwarding logger. three of each type + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Count == 6); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.ConfigurableForwardingLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.BackEnd.Logging.CentralForwardingLogger")); + + int countForwardingLogger = 0; + foreach (string loggerName in _initializedService.RegisteredLoggerTypeNames) + { + if (String.Compare("Microsoft.Build.Logging.ConfigurableForwardingLogger", loggerName, StringComparison.OrdinalIgnoreCase) == 0) + { + countForwardingLogger++; + } + } + + // Should be 3, one for each call to RegisterLoggerDescriptions + Assert.AreEqual(3, countForwardingLogger); + + countForwardingLogger = 0; + foreach (string loggerName in _initializedService.RegisteredLoggerTypeNames) + { + if (String.Compare("Microsoft.Build.BackEnd.Logging.CentralForwardingLogger", loggerName, StringComparison.OrdinalIgnoreCase) == 0) + { + countForwardingLogger++; + } + } + + // Should be 3, one for each call to RegisterLoggerDescriptions + Assert.AreEqual(3, countForwardingLogger); + + // Should have 2 event sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.IsTrue(_initializedService.RegisteredSinkNames.Count == 2); + + // There should not be any (this method is to be called on a child node) + Assert.AreEqual(0, _initializedService.LoggerDescriptions.Count); + } + + /// + /// Try and register a duplicate central logger + /// + [TestMethod] + public void RegisterDuplicateDistributedCentralLogger() + { + string className = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + LoggerDescription description = CreateLoggerDescription(className, Assembly.GetAssembly(typeof(ProjectCollection)).FullName, true); + + RegularILogger regularILogger = new RegularILogger(); + Assert.IsTrue(_initializedService.RegisterDistributedLogger(regularILogger, description)); + Assert.IsFalse(_initializedService.RegisterDistributedLogger(regularILogger, description)); + + // Should have 2 central loggers and 1 forwarding logger + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Count == 2); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.Logging.ConfigurableForwardingLogger")); + Assert.IsTrue(_initializedService.RegisteredLoggerTypeNames.Contains("Microsoft.Build.UnitTests.Logging.LoggingService_Tests+RegularILogger")); + + // Should have 1 sink + Assert.IsNotNull(_initializedService.RegisteredSinkNames); + Assert.AreEqual(1, _initializedService.RegisteredSinkNames.Count); + Assert.AreEqual(1, _initializedService.LoggerDescriptions.Count); + } + #endregion + + #region Test Properties + /// + /// Verify the getters and setters for the properties work. + /// + [TestMethod] + public void Properties() + { + // Test OnlyLogCriticalEvents + LoggingService loggingService = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + Assert.IsFalse(loggingService.OnlyLogCriticalEvents, "Expected only log critical events to be false"); + loggingService.OnlyLogCriticalEvents = true; + Assert.IsTrue(loggingService.OnlyLogCriticalEvents, "Expected only log critical events to be true"); + + // Test LoggingMode + Assert.IsTrue(loggingService.LoggingMode == LoggerMode.Synchronous, "Expected Logging mode to be Synchronous"); + + // Test LoggerDescriptions + Assert.AreEqual(0, loggingService.LoggerDescriptions.Count, "Expected LoggerDescriptions to be empty"); + + // Test Number of InitialNodes + Assert.AreEqual(1, loggingService.MaxCPUCount); + loggingService.MaxCPUCount = 5; + Assert.AreEqual(5, loggingService.MaxCPUCount); + } + + #endregion + + #region PacketHandling Tests + /// + /// Verify how a null packet is handled. There should be an InternalErrorException thrown + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NullPacketReceived() + { + LoggingService loggingService = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + loggingService.PacketReceived(1, null); + } + + /// + /// Verify when a non logging packet is received. + /// An invalid operation should be thrown + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NonLoggingPacketPacketReceived() + { + LoggingService loggingService = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + NonLoggingPacket packet = new NonLoggingPacket(); + loggingService.PacketReceived(1, packet); + } + + /// + /// Verify when a logging packet is received the build event is + /// properly passed to ProcessLoggingEvent + /// An invalid operation should be thrown + /// + [TestMethod] + public void LoggingPacketReceived() + { + LoggingServicesLogMethod_Tests.ProcessBuildEventHelper loggingService = (LoggingServicesLogMethod_Tests.ProcessBuildEventHelper)LoggingServicesLogMethod_Tests.ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + BuildMessageEventArgs messageEvent = new BuildMessageEventArgs("MyMessage", "HelpKeyword", "Sender", MessageImportance.High); + LogMessagePacket packet = new LogMessagePacket(new KeyValuePair(1, messageEvent)); + loggingService.PacketReceived(1, packet); + + BuildMessageEventArgs messageEventFromPacket = loggingService.ProcessedBuildEvent as BuildMessageEventArgs; + Assert.IsNotNull(messageEventFromPacket); + Assert.IsTrue(messageEventFromPacket == messageEvent, "Expected messages to match"); + } + + #endregion + + #region PrivateMethods + + /// + /// Instantiate and Initialize a new loggingService. + /// This is used by the test setup method to create + /// a new logging service before each test. + /// + private void InitializeLoggingService() + { + BuildParameters parameters = new BuildParameters(); + parameters.MaxNodeCount = 2; + MockHost mockHost = new MockHost(parameters); + + IBuildComponent logServiceComponent = (IBuildComponent)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + logServiceComponent.InitializeComponent(mockHost); + _initializedService = logServiceComponent as LoggingService; + } + + /// + /// Log a message every 10ms, this is used to verify + /// the shutdown is not waiting forever for events as it + /// shutsdown. + /// + private void TightLoopLogEvents() + { + while (!_shutdownComplete.WaitOne(10, false)) + { + _initializedService.LogBuildEvent(new BuildMessageEventArgs("Message", "Help", "Sender", MessageImportance.High)); + } + } + + /// + /// Register the correct logger and then call the shutdownComponent method. + /// This will call shutdown on the loggers, we should expect to see certain exceptions. + /// + /// Logger to register, this will only be used if className is null + /// ClassName to instantiate a new distributed logger + /// Exception type which is expected to be thrown + private void VerifyShutdownExceptions(ILogger logger, string className, Type expectedExceptionType) + { + InitializeLoggingService(); + if (className != null) + { + Assembly thisAssembly = Assembly.GetAssembly(typeof(LoggingService_Tests)); + string loggerAssemblyName = thisAssembly.FullName; + LoggerDescription centralLoggerDescrption = CreateLoggerDescription(className, loggerAssemblyName, true); + _initializedService.RegisterDistributedLogger(null, centralLoggerDescrption); + } + else + { + _initializedService.RegisterLogger(logger); + } + + try + { + _initializedService.ShutdownComponent(); + Assert.Fail("No Exceptions Generated"); + } + catch (Exception e) + { + if (e.GetType() != expectedExceptionType) + { + Assert.Fail("Expected a " + expectedExceptionType + " but got a " + e.GetType() + " Stack:" + e.ToString()); + } + } + } + + /// + /// Create a logger description from the class name and logger assembly + /// This is used in any test which needs to register a distributed logger. + /// + /// Fully qualified class name (dont for get ParentClass+Nestedclass, if nested) + /// Assembly name which contains class + /// A logger description which can be registered + private LoggerDescription CreateLoggerDescription(string loggerClassName, string loggerAssemblyName, bool forwardAllEvents) + { + string eventsToForward = "CustomEvent"; + + if (forwardAllEvents == true) + { + eventsToForward = "BuildStartedEvent;BuildFinishedEvent;ProjectStartedEvent;ProjectFinishedEvent;TargetStartedEvent;TargetFinishedEvent;TaskStartedEvent;TaskFinishedEvent;ErrorEvent;WarningEvent;HighMessageEvent;NormalMessageEvent;LowMessageEvent;CustomEvent;CommandLine"; + } + + LoggerDescription centralLoggerDescrption = new LoggerDescription + ( + loggerClassName, + loggerAssemblyName, + null /*Not needed as we are loading from current assembly*/, + eventsToForward, + LoggerVerbosity.Diagnostic /*Not used, but the spirit of the logger is to forward everything so this is the most appropriate verbosity */ + ); + return centralLoggerDescrption; + } + #endregion + + #region HelperClasses + + /// + /// A forwarding logger which will throw an exception + /// + public class BaseFLThrowException : LoggerThrowException, IForwardingLogger + { + #region Constructor + + /// + /// Create a forwarding logger which will throw an exception on initialize or shutdown + /// + /// Throw exception on shutdown + /// Throw exception on initialize + /// Exception to throw + internal BaseFLThrowException(bool throwOnShutdown, bool throwOnInitialize, Exception exception) + : base(throwOnShutdown, throwOnInitialize, exception) + { + } + #endregion + + #region IForwardingLogger Members + + /// + /// Not used, implmented due to interface + /// + /// Notused + public IEventRedirector BuildEventRedirector + { + get; + set; + } + + /// + /// Not used, implemented due to interface + /// + /// Not used + public int NodeId + { + get; + set; + } + + #endregion + } + + /// + /// Forwarding logger which throws a logger exception in the shutdown method. + /// This is to test the logging service exception handling. + /// + public class ShutdownLoggerExceptionFL : BaseFLThrowException + { + /// + /// Create a logger which will throw a logger exception + /// in the shutdown method + /// + public ShutdownLoggerExceptionFL() + : base(true, false, new LoggerException("Hello")) + { + } + } + + /// + /// Forwarding logger which will throw a general exception in the shutdown method + /// This is used to test the logging service shutdown handling method. + /// + public class ShutdownGeneralExceptionFL : BaseFLThrowException + { + /// + /// Create a logger which logs a general exception in the shutdown method + /// + public ShutdownGeneralExceptionFL() + : base(true, false, new Exception("Hello")) + { + } + } + + /// + /// Forwarding logger which will throw a StackOverflowException + /// in the shutdown method. This is to test the shutdown exception handling + /// + public class ShutdownStackoverflowExceptionFL : BaseFLThrowException + { + /// + /// Create a logger which will throw a StackOverflow exception + /// in the shutdown method. + /// + public ShutdownStackoverflowExceptionFL() + : base(true, false, new StackOverflowException()) + { + } + } + + /// + /// Logger which can throw a defined exception in the initialize or shutdown methods + /// + public class LoggerThrowException : INodeLogger + { + #region Constructor + + /// + /// Constructor to tell the logger when to throw an exception and what excetption + /// to throw + /// + /// True, throw the exception when shutdown is called + /// True, throw the exception when Initialize is called + /// The exception to throw + internal LoggerThrowException(bool throwOnShutdown, bool throwOnInitialize, Exception exception) + { + ExceptionToThrow = exception; + ThrowExceptionOnShutdown = throwOnShutdown; + ThrowExceptionOnInitialize = throwOnInitialize; + } + #endregion + + #region Propeties + + /// + /// Not used, implemented due to ILoggerInterface + /// + /// Not used + public LoggerVerbosity Verbosity + { + get; + set; + } + + /// + /// Not used, implemented due to ILoggerInterface + /// + /// Not used + public string Parameters + { + get; + set; + } + + /// + /// Should the exception be thrown on the call to shutdown + /// + /// Not used + protected bool ThrowExceptionOnShutdown + { + get; + set; + } + + /// + /// Should the exception be thrown on the call to initalize + /// + /// Not used + protected bool ThrowExceptionOnInitialize + { + get; + set; + } + + /// + /// The exception which will be thrown in shutdown or initialize + /// + /// Not used + protected Exception ExceptionToThrow + { + get; + set; + } + #endregion + + #region ILogger Members + + /// + /// Initialize the logger, throw an exception + /// if ThrowExceptionOnInitialize is set + /// + /// Not used + public void Initialize(IEventSource eventSource) + { + if (ThrowExceptionOnInitialize && ExceptionToThrow != null) + { + throw ExceptionToThrow; + } + } + + /// + /// Shutdown the logger, throw an exception if + /// ThrowExceptionOnShutdown is set + /// + public void Shutdown() + { + if (ThrowExceptionOnShutdown && ExceptionToThrow != null) + { + throw ExceptionToThrow; + } + } + + /// + /// Initialize using the INodeLogger Interface + /// + /// Not used + /// Not used + public void Initialize(IEventSource eventSource, int nodeCount) + { + Initialize(eventSource); + } + #endregion + } + + /// + /// Create a regular ILogger to test Registering ILoggers. + /// + public class RegularILogger : ILogger + { + #region Properties + + /// + /// ParametersForTheLogger + /// + public string Parameters + { + get; + set; + } + + /// + /// Verbosity + /// + public LoggerVerbosity Verbosity + { + get; + set; + } + + /// + /// Number of times build started was logged + /// + internal int BuildStartedCount + { + get; + set; + } + + /// + /// Number of times build finished was logged + /// + internal int BuildFinishedCount + { + get; + set; + } + + /// + /// Initialize + /// + public void Initialize(IEventSource eventSource) + { + eventSource.AnyEventRaised += + new AnyEventHandler(LoggerEventHandler); + } + + /// + /// DoNothing + /// + public void Shutdown() + { + // do nothing + } + + /// + /// Log the event + /// + internal void LoggerEventHandler(object sender, BuildEventArgs eventArgs) + { + if (eventArgs is BuildStartedEventArgs) + { + ++BuildStartedCount; + } + + if (eventArgs is BuildFinishedEventArgs) + { + ++BuildFinishedCount; + } + } + } + + /// + /// Create a regular ILogger which keeps track of how many of each event were logged + /// + public class TestLogger : ILogger + { + /// + /// Not Used + /// + /// Not used + public LoggerVerbosity Verbosity + { + get; + set; + } + + /// + /// Do Nothing + /// + /// Not Used + public string Parameters + { + get; + set; + } + + /// + /// Do Nothing + /// + /// Not Used + public void Initialize(IEventSource eventSource) + { + } + + /// + /// Do Nothing + /// + public void Shutdown() + { + } + + #endregion + } + + /// + /// Create a non logging packet to test the packet handling code + /// + internal class NonLoggingPacket : INodePacket + { + #region Members + + /// + /// Inform users of the class, this class is a BuildRequest packet + /// + public NodePacketType Type + { + get + { + return NodePacketType.BuildRequest; + } + } + + /// + /// Serialize the packet + /// + public void Translate(INodePacketTranslator translator) + { + throw new NotImplementedException(); + } + + #endregion + } + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs new file mode 100644 index 00000000000..99422d9c5e1 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/LoggingServicesLogMethod_Tests.cs @@ -0,0 +1,1655 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Test the logging service component +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using System.IO; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using Microsoft.Build.Execution; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using System.Collections; +using System.Collections.Generic; +using System.Xml; + +using MockHost = Microsoft.Build.UnitTests.BackEnd.MockHost; + +namespace Microsoft.Build.UnitTests.Logging +{ + /// + /// Contain the logging services tests which deal with the logging methods themselves + /// + [TestClass] + public class LoggingServicesLogMethod_Tests + { + #region Data + /// + /// A generic valid build event context which can be used in the tests. + /// + private static BuildEventContext s_buildEventContext = new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4); + + /// + /// buildevent context for target events, note the invalid taskId, target started and finished events have this. + /// + private static BuildEventContext s_targetBuildEventContext = new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, -1); + #endregion + + #region Event based logging method tests + + /// + /// Make sure an InternalErrorExcetpionis thrown when a null event is attempted to be logged + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogBuildEventNullEvent() + { + LoggingService loggingService = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + loggingService.LogBuildEvent(null); + } + + /// + /// Test LogBuildevent by logging a number of events with both OnlyLogCriticalEvents On and Off + /// + [TestMethod] + public void LogBuildEvents() + { + // This event should only be logged when OnlyLogCriticalEvents is off + BuildMessageEventArgs messageEvent = new BuildMessageEventArgs("MyMessage", "HelpKeyword", "Sender", MessageImportance.High); + + // These three should be logged when OnlyLogCritical Events is on or off + BuildWarningEventArgs warning = new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + BuildErrorEventArgs error = new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + ExternalProjectStartedEventArgs externalStartedEvent = new ExternalProjectStartedEventArgs("message", "help", "senderName", "projectFile", "targetNames"); + + ProcessBuildEventHelper loggingService = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + + // Verify when OnlyLogCriticalEvents is false + LogandVerifyBuildEvent(messageEvent, loggingService); + LogandVerifyBuildEvent(warning, loggingService); + LogandVerifyBuildEvent(error, loggingService); + LogandVerifyBuildEvent(externalStartedEvent, loggingService); + + // Verify when OnlyLogCriticalEvents is true + loggingService.OnlyLogCriticalEvents = true; + loggingService.LogBuildEvent(messageEvent); + Assert.IsNull(loggingService.ProcessedBuildEvent, "Expected ProcessedBuildEvent to be null"); + LogandVerifyBuildEvent(warning, loggingService); + LogandVerifyBuildEvent(error, loggingService); + LogandVerifyBuildEvent(externalStartedEvent, loggingService); + } + + #endregion + + #region TestErrors + + #region LogError + + /// + /// Verify an InternalErrorException is thrown when MessageResourceName is null. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogErrorNullMessageResource() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogError(s_buildEventContext, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo("foo.cs"), null, "MyTask"); + } + + /// + /// Verify an InternlErrorException is thrown when an empty MessageResourceName is passed in. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogErrorEmptyMessageResource() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogError(s_buildEventContext, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo("foo.cs"), string.Empty, "MyTask"); + } + + /// + /// Verify a message is logged when all of the parameters are filled out correctly. + /// + [TestMethod] + public void LogErrorGoodParameters() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + string errorCode; + string helpKeyword; + string taskName = "TaskName"; + string subcategoryKey = "SubCategoryForSolutionParsingErrors"; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, "FatalTaskError", taskName); + string subcategory = AssemblyResources.GetString(subcategoryKey); + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + + service.LogError(s_buildEventContext, subcategoryKey, fileInfo, "FatalTaskError", taskName); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, subcategory); + } + + #endregion + + #region LogInvalidProjectFileError + + /// + /// Verify an exception is thrown when a null buildevent context is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogInvalidProjectFileErrorNullEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogInvalidProjectFileError(null, new InvalidProjectFileException()); + } + + /// + /// Verify an exception is thrown when a null Invalid ProjectFile exception is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogInvalidProjectFileErrorNullException() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogInvalidProjectFileError(s_buildEventContext, null); + } + + /// + /// Verify a message is logged when both parameters are good and + /// the exception has not been logged yet. Verify with and without OnlyLogCriticalEvents. + /// In Both cases we expect the event to be logged + /// + [TestMethod] + public void LogInvalidProjectFileError() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + InvalidProjectFileException exception = new InvalidProjectFileException("ProjectFile", 1, 2, 3, 4, "Message", "errorSubCategory", "ErrorCode", "HelpKeyword"); + + // Log the exception for the first time + Assert.IsFalse(exception.HasBeenLogged); + service.LogInvalidProjectFileError(s_buildEventContext, exception); + Assert.IsTrue(exception.HasBeenLogged); + BuildEventFileInfo fileInfo = new BuildEventFileInfo(exception.ProjectFile, exception.LineNumber, exception.ColumnNumber, exception.EndLineNumber, exception.EndColumnNumber); + VerifyBuildErrorEventArgs(fileInfo, exception.ErrorCode, exception.HelpKeyword, exception.BaseMessage, service, exception.ErrorSubcategory); + + // Verify when the exception is logged again that it does not actually get logged due to it already being logged + service.ResetProcessedBuildEvent(); + service.LogInvalidProjectFileError(s_buildEventContext, exception); + Assert.IsNull(service.ProcessedBuildEvent); + + // Reset the HasLogged field and verify OnlyLogCriticalEvents does not effect the logging of the message + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + exception.HasBeenLogged = false; + service.LogInvalidProjectFileError(s_buildEventContext, exception); + VerifyBuildErrorEventArgs(fileInfo, exception.ErrorCode, exception.HelpKeyword, exception.BaseMessage, service, exception.ErrorSubcategory); + } + + #endregion + + #region LogFatalError + + /// + /// Verify an InternalErrorException is thrown when a null build event context is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogFatalErrorNullContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalError(null, new Exception("SuperException"), new BuildEventFileInfo("foo.cs"), "FatalTaskError", "TaskName"); + } + + /// + /// Verify an InternalErrorException is thrown when fileInfo is null + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogFatalErrorNullFileInfo() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalError(s_buildEventContext, new Exception("SuperException"), null, "FatalTaskError", "TaskName"); + } + + /// + /// Verify a error message is correctly logged when the exception is null. + /// + [TestMethod] + public void LogFatalErrorNullException() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + string errorCode; + string helpKeyword; + string resourceName = "FatalTaskError"; + string parameters = "TaskName"; + string message = null; + + GenerateMessageFromExceptionAndResource(null, resourceName, out errorCode, out helpKeyword, out message, parameters); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalError(s_buildEventContext, null, fileInfo, resourceName, parameters); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, null); + } + + /// + /// Verify an InternalErrorException is thrown when messageResourceName is null + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogFatalErrorNullMessageResourceName() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalError(s_buildEventContext, new Exception("SuperException"), fileInfo, null); + } + + /// + /// Verify an InternalErrorException is thrown when messageResourceName is empty + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogFatalErrorEmptyMessageResourceName() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalError(s_buildEventContext, new Exception("SuperException"), fileInfo, string.Empty, null); + } + + /// + /// Verify a error message is correctly logged when all of the inputs are valid. + /// + [TestMethod] + public void LogFatalErrorAllGoodInput() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + Exception exception = new Exception("SuperException"); + string resourceName = "FatalTaskError"; + string parameter = "TaskName"; + string errorCode; + string helpKeyword; + string message; + GenerateMessageFromExceptionAndResource(exception, resourceName, out errorCode, out helpKeyword, out message, parameter); + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + + service.LogFatalError(s_buildEventContext, exception, fileInfo, resourceName, parameter); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, null); + } + #endregion + + #region LogFatalBuildError + + /// + /// Verify a error message is correctly logged when all of the inputs are valid. + /// + [TestMethod] + public void LogFatalBuildErrorGoodInput() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + Exception exception = new Exception("SuperException"); + string resourceName = "FatalBuildError"; + string errorCode; + string helpKeyword; + string message; + GenerateMessageFromExceptionAndResource(exception, resourceName, out errorCode, out helpKeyword, out message); + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalBuildError(s_buildEventContext, exception, fileInfo); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, null); + } + #endregion + + #region LogFatalTaskError + + /// + /// Verify an InternalErrorException is thrown when taskName is null + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogFatalTaskErrorNullTaskNameName() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalTaskError(s_buildEventContext, new Exception("SuperException"), fileInfo, null); + } + + /// + /// Verify a error message is correctly logged when all of the inputs are valid. + /// + [TestMethod] + public void LogFatalTaskError() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + Exception exception = new Exception("SuperException"); + string errorCode; + string helpKeyword; + string resourceName = "FatalTaskError"; + string parameters = "TaskName"; + string message = null; + + GenerateMessageFromExceptionAndResource(exception, resourceName, out errorCode, out helpKeyword, out message, parameters); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogFatalTaskError(s_buildEventContext, exception, fileInfo, parameters); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, null); + + // Test when the task name is empty + GenerateMessageFromExceptionAndResource(exception, resourceName, out errorCode, out helpKeyword, out message, String.Empty); + service.ResetProcessedBuildEvent(); + service.LogFatalTaskError(s_buildEventContext, exception, fileInfo, string.Empty); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, null); + } + #endregion + + #region LogErrorFromText + /// + /// Verify an InternalErrorException is thrown when BuildEventContext is null. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogErrorFromTextNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogErrorFromText(null, "SubCategoryForSolutionParsingErrors", "WarningCode", "HelpKeyword", new BuildEventFileInfo("foo.cs"), "Message"); + } + + /// + /// Verify an InternalErrorException when a null FileInfo is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogErrorFromTextNullFileInfo() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogErrorFromText(s_buildEventContext, "SubCategoryForSolutionParsingErrors", "WarningCode", "HelpKeyword", null, "Message"); + } + + /// + /// Verify an InternalErrorException is thrown when a null message is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogErrorFromTextNullMessage() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogErrorFromText(null, "SubCategoryForSolutionParsingErrors", "WarningCode", "HelpKeyword", new BuildEventFileInfo("foo.cs"), null); + } + + /// + /// Test LogErrorFromText with a number of different inputs + /// + [TestMethod] + public void LogErrorFromTextTests() + { + string warningCode; + string helpKeyword; + string subcategoryKey = "SubCategoryForSolutionParsingErrors"; + string message = ResourceUtilities.FormatResourceString(out warningCode, out helpKeyword, "FatalTaskError", "MyTask"); + + // Test ErrorCode + TestLogErrorFromText(null, helpKeyword, subcategoryKey, message); + TestLogErrorFromText(String.Empty, helpKeyword, subcategoryKey, message); + + // Test HelpKeyword + TestLogErrorFromText(warningCode, null, subcategoryKey, message); + TestLogErrorFromText(warningCode, String.Empty, subcategoryKey, message); + + // Test subcategory (we use the key, the actual one is generated in TestLogFromText + TestLogErrorFromText(warningCode, helpKeyword, null, message); + + // Test empty message + TestLogErrorFromText(warningCode, helpKeyword, subcategoryKey, String.Empty); + + // Test Good + TestLogErrorFromText(warningCode, helpKeyword, subcategoryKey, message); + } + + /// + /// Make sure if an imported project has an invalid project file exception say by trying to run a nonexistent task that we properly get + /// the [projectfile] post fix information. + /// + [TestMethod] + public void VerifyErrorPostfixForInvalidProjectFileException() + { + MockLogger mockLogger = new MockLogger(); + string tempPath = Path.GetTempPath(); + string testTempPath = Path.Combine(tempPath, "VerifyErrorPostfixForInvalidProjectFileException"); + string projectFile = Path.Combine(testTempPath, "a.proj"); + string targetsFile = Path.Combine(testTempPath, "x.targets"); + string projectfileContent = + @" + + + + "; + + string targetsfileContent = @" + + + + + + + "; + try + { + Directory.CreateDirectory(testTempPath); + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectfileContent))); + project.Save(projectFile); + project = ProjectRootElement.Create(XmlReader.Create(new StringReader(targetsfileContent))); + project.Save(targetsFile); + Project msbuildProject = new Project(projectFile); + msbuildProject.Build(mockLogger); + + List errors = mockLogger.Errors; + Assert.IsTrue(errors.Count == 1); + BuildErrorEventArgs error = errors[0]; + Assert.IsTrue(String.Equals(error.File, targetsFile)); + Assert.IsTrue(String.Equals(error.ProjectFile, projectFile)); + } + finally + { + if (Directory.Exists(testTempPath)) + { + Directory.Delete(testTempPath, true); + } + + if (File.Exists(targetsFile)) + { + File.Delete(targetsFile); + } + + if (File.Exists(projectFile)) + { + File.Delete(projectFile); + } + } + } + #endregion + #endregion + + #region TestWarnings + #region Test LogTaskWarningFromException + /// + /// Verify an InternalErrorException is thrown when taskName is null + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogTaskWarningFromExceptionNullTaskName() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTaskWarningFromException(s_buildEventContext, null, fileInfo, null); + } + + /// + /// Verify an InternalErrorException is thrown when taskName is empty + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogTaskWarningFromExceptionEmptyTaskName() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTaskWarningFromException(s_buildEventContext, null, fileInfo, null); + } + + /// + /// Verify a LogTaskWarningFromException with a null exception and a non null exception + /// with all of the other fields properly filled out. + /// + [TestMethod] + public void LogTaskWarningFromException() + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + string resourceName = "FatalTaskError"; + string parameters = "TaskName"; + string warningCode; + string helpKeyword; + string message; + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + + // Check with a null exception + GenerateMessageFromExceptionAndResource(null, resourceName, out warningCode, out helpKeyword, out message, parameters); + service.LogTaskWarningFromException(s_buildEventContext, null, fileInfo, parameters); + VerifyBuildWarningEventArgs(fileInfo, warningCode, helpKeyword, message, service, null); + + // Check when the exception is not null + service.ResetProcessedBuildEvent(); + Exception exception = new Exception("SuperException"); + GenerateMessageFromExceptionAndResource(exception, resourceName, out warningCode, out helpKeyword, out message, parameters); + service.LogTaskWarningFromException(s_buildEventContext, exception, fileInfo, parameters); + VerifyBuildWarningEventArgs(fileInfo, warningCode, helpKeyword, message, service, null); + } + #endregion + + #region LogWarning + /// + /// Verify an exception is when a null MessageResourceName is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogWarningNullMessageResource() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogWarning(s_buildEventContext, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo("foo.cs"), null, "MyTask"); + } + + /// + /// Verify an exception is when a empty MessageResourceName is passed in. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogWarningEmptyMessageResource() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogWarning(s_buildEventContext, "SubCategoryForSolutionParsingErrors", new BuildEventFileInfo("foo.cs"), string.Empty, "MyTask"); + } + + /// + /// Verify a message is logged when all of the parameters are filled out + /// + [TestMethod] + public void LogWarningTests() + { + TestLogWarning(null, "SubCategoryForSolutionParsingErrors"); + TestLogWarning(String.Empty, "SubCategoryForSolutionParsingErrors"); + TestLogWarning("MyTask", "SubCategoryForSolutionParsingErrors"); + } + + #endregion + + #region LogWarningErrorFromText + /// + /// Verify an InternalErrorException is thown when a null BuildEventContext is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogWarningFromTextNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogWarningFromText(null, "SubCategoryForSolutionParsingErrors", "WarningCode", "HelpKeyword", new BuildEventFileInfo("foo.cs"), "Message"); + } + + /// + /// Verify an InternalErrorException is thrown when a null fileInfo is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogWarningFromTextNullFileInfo() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogWarningFromText(s_buildEventContext, "SubCategoryForSolutionParsingErrors", "WarningCode", "HelpKeyword", null, "Message"); + } + + /// + /// Verify an InternalErrorException is thrown when a null message is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogWarningFromTextNullMessage() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogWarningFromText(null, "SubCategoryForSolutionParsingErrors", "WarningCode", "HelpKeyword", new BuildEventFileInfo("foo.cs"), null); + } + + /// + /// Test LogWarningFromText with a number of different inputs + /// + [TestMethod] + public void LogWarningFromTextTests() + { + string warningCode; + string helpKeyword; + string subcategoryKey = "SubCategoryForSolutionParsingErrors"; + string message = ResourceUtilities.FormatResourceString(out warningCode, out helpKeyword, "FatalTaskError", "MyTask"); + + TestLogWarningFromText(null, helpKeyword, subcategoryKey, message); + TestLogWarningFromText(String.Empty, helpKeyword, subcategoryKey, message); + + TestLogWarningFromText(warningCode, null, subcategoryKey, message); + TestLogWarningFromText(warningCode, String.Empty, subcategoryKey, message); + + TestLogWarningFromText(warningCode, null, subcategoryKey, message); + TestLogWarningFromText(warningCode, String.Empty, subcategoryKey, message); + + TestLogWarningFromText(warningCode, helpKeyword, null, message); + + TestLogWarningFromText(warningCode, helpKeyword, subcategoryKey, String.Empty); + TestLogWarningFromText(warningCode, helpKeyword, subcategoryKey, message); + } + + #endregion + #endregion + + #region LogCommentTests + + /// + /// Verify an InternalErrorException is thrown when a null messageResource name is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogCommentNullMessageResourceName() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogComment(s_buildEventContext, MessageImportance.Low, null, null); + } + + /// + /// Verify an InternalErrorException is thrown when a empty messageResource name is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogCommentEmptyMessageResourceName() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogComment(s_buildEventContext, MessageImportance.Low, String.Empty, null); + } + + /// + /// Verify LogComment by testing it with OnlyLogCriticalEvents On and Off when the rest of the fields are + /// valid inputs. + /// + [TestMethod] + public void LogCommentGoodMessage() + { + MessageImportance messageImportance = MessageImportance.Normal; + string message = ResourceUtilities.FormatResourceString("BuildFinishedSuccess"); + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + + // Verify message is logged when OnlyLogCriticalEvents is false + service.LogComment(s_buildEventContext, messageImportance, "BuildFinishedSuccess"); + VerityBuildMessageEventArgs(service, messageImportance, message); + + // Verify no message is logged when OnlyLogCriticalEvents is true + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogComment(s_buildEventContext, MessageImportance.Normal, "BuildFinishedSuccess"); + Assert.IsNull(service.ProcessedBuildEvent); + } + + #endregion + + #region LogCommentFromTextTests + + /// + /// Verify an InternalErrorException is thrown when a null message is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogCommentFromTextNullMessage() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogCommentFromText(s_buildEventContext, MessageImportance.Low, null); + } + + /// + /// Verify a message is logged when an empty message is passed in + /// + [TestMethod] + public void LogCommentFromTextEmptyMessage() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogCommentFromText(s_buildEventContext, MessageImportance.Low, string.Empty); + } + + /// + /// Verify an InternalErrorException is thrown when a null build event context is passed in + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogCommentFromTextNullBuildEventContextMessage() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogCommentFromText(null, MessageImportance.Low, "Hello"); + } + + /// + /// Make sure we can log a comment when everything should be working correctly + /// + [TestMethod] + public void LogCommentFromTextGoodMessage() + { + MessageImportance messageImportance = MessageImportance.Normal; + string message = ResourceUtilities.FormatResourceString("BuildFinishedSuccess"); + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogCommentFromText(s_buildEventContext, messageImportance, ResourceUtilities.FormatResourceString("BuildFinishedSuccess")); + VerityBuildMessageEventArgs(service, messageImportance, message); + + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogCommentFromText(s_buildEventContext, MessageImportance.Normal, ResourceUtilities.FormatResourceString("BuildFinishedSuccess")); + Assert.IsNull(service.ProcessedBuildEvent); + } + #endregion + + #region LogStatusMessages + + #region ProjectEvents + + #region ProjectStarted + + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ProjectStartedNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogProjectStarted(null, 1, 2, s_buildEventContext, "ProjectFile", "TargetNames", null, null); + } + + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ProjectStartedNullParentBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogProjectStarted(s_buildEventContext, 1, 2, null, "ProjectFile", "TargetNames", null, null); + } + + /// + /// Test the case where ProjectFile is good and TargetNames is null. + /// Expect an event to be logged + /// + [TestMethod] + public void ProjectStartedEventTests() + { + // Good project File and null target names + LogProjectStartedTestHelper("ProjectFile", null); + + // Good project File and empty target names + LogProjectStartedTestHelper("ProjectFile", string.Empty); + + // Null project file and null target names + LogProjectStartedTestHelper(null, null); + + // Empty project file null target Names + LogProjectStartedTestHelper(string.Empty, null); + + // Empty project File and Empty target Names + LogProjectStartedTestHelper(string.Empty, string.Empty); + + // TestGoodInputs + LogProjectStartedTestHelper("ProjectFile", "TargetNames"); + } + + #endregion + + #region ProjectFinished + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ProjectFinishedNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogProjectFinished(null, "ProjectFile", true); + } + + /// + /// Test the project finished event + /// + [TestMethod] + public void ProjectFinished() + { + TestProjectFinishedEvent(null, true); + TestProjectFinishedEvent(String.Empty, true); + TestProjectFinishedEvent("ProjectFile", true); + TestProjectFinishedEvent("ProjectFile", false); + } + + #endregion + #endregion + + #region BuildStartedFinishedEvents + + /// + /// Make sure we can log a build started event correctly. + /// Test both the LogOnlyCriticalEvents true and false + /// + [TestMethod] + public void LogBuildStarted() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogBuildStarted(); + BuildStartedEventArgs buildEvent = new BuildStartedEventArgs(ResourceUtilities.FormatResourceString("BuildStarted"), null /* no help keyword */, service.ProcessedBuildEvent.Timestamp); + Assert.IsTrue(((BuildStartedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogBuildStarted(); + buildEvent = new BuildStartedEventArgs(string.Empty, null /* no help keyword */); + Assert.IsTrue(((BuildStartedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + } + + /// + /// Make sure we can log a build finished event correctly. + /// Verify the success cases as well as OnlyLogCriticalEvents + /// + [TestMethod] + public void LogBuildFinished() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogBuildFinished(true); + BuildFinishedEventArgs buildEvent = new BuildFinishedEventArgs(ResourceUtilities.FormatResourceString("BuildFinishedSuccess"), null /* no help keyword */, true, service.ProcessedBuildEvent.Timestamp); + Assert.IsTrue(((BuildFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + + service.ResetProcessedBuildEvent(); + service.LogBuildFinished(false); + buildEvent = new BuildFinishedEventArgs(ResourceUtilities.FormatResourceString("BuildFinishedFailure"), null /* no help keyword */, false, service.ProcessedBuildEvent.Timestamp); + Assert.IsTrue(((BuildFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogBuildFinished(true); + buildEvent = new BuildFinishedEventArgs(string.Empty, null /* no help keyword */, true, service.ProcessedBuildEvent.Timestamp); + Assert.IsTrue(((BuildFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + } + + /// + /// Exercise Asynchronous code path, this method should return right away as there are no events to process. + /// This will be further tested in the LoggingService_Tests class. + /// + [TestMethod] + public void TestBuildFinishedWaitForEvents() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Asynchronous, 1); + service.LogBuildFinished(true); + } + + #endregion + + #region LogTaskEvents + + #region TaskStarted + + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TaskStartedNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTaskStarted(null, "MyTask", "ProjectFile", "ProjectFileOfTask"); + } + + /// + /// Test the case where TaskName + /// + [TestMethod] + public void TaskStartedEvent() + { + TestTaskStartedEvent(null, "ProjectFile", "ProjectFileOfTaskNode"); + TestTaskStartedEvent(String.Empty, "ProjectFile", "ProjectFileOfTaskNode"); + + TestTaskStartedEvent("TaskName", null, "ProjectFileOfTaskNode"); + TestTaskStartedEvent("TaskName", String.Empty, "ProjectFileOfTaskNode"); + + TestTaskStartedEvent("TaskName", "ProjectFile", null); + TestTaskStartedEvent("TaskName", "ProjectFile", String.Empty); + + TestTaskStartedEvent("TaskName", "ProjectFile", "ProjectFileOfTaskNode"); + } + + #endregion + + #region TaskFinished + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TaskFinishedNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTaskFinished(null, "MyTask", "ProjectFile", "ProjectFileOfTask", true); + } + + /// + /// Test the case where TaskName is null. + /// + [TestMethod] + public void TaskFinishedNullTaskName() + { + TestTaskFinished(null, "ProjectFile", "ProjectFileOfTaskNode", true); + TestTaskFinished(String.Empty, "ProjectFile", "ProjectFileOfTaskNode", true); + + TestTaskFinished("TaskName", null, "ProjectFileOfTaskNode", true); + TestTaskFinished("TaskName", String.Empty, "ProjectFileOfTaskNode", true); + + TestTaskFinished("TaskName", "ProjectFile", null, true); + TestTaskFinished("TaskName", "ProjectFile", String.Empty, true); + + TestTaskFinished("TaskName", "ProjectFile", "ProjectFileOfTaskNode", true); + TestTaskFinished("TaskName", "ProjectFile", "ProjectFileOfTaskNode", false); + } + + #endregion + + #endregion + + #region LogTargetEvents + + #region TargetStarted + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TargetStartedNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTargetStarted(null, "MyTarget", "ProjectFile", "ProjectFileOfTarget", null); + } + + /// + /// Test the target started event with a null target name. + /// + [TestMethod] + public void TargetStartedNullTargetName() + { + TestTargetStartedEvent(null, "ProjectFile", "projectFileOfTarget"); + TestTargetStartedEvent(String.Empty, "ProjectFile", "projectFileOfTarget"); + TestTargetStartedEvent("TargetName", null, "projectFileOfTarget"); + TestTargetStartedEvent("TargetName", String.Empty, "projectFileOfTarget"); + TestTargetStartedEvent("Good", "ProjectFile", null); + TestTargetStartedEvent("Good", "ProjectFile", String.Empty); + TestTargetStartedEvent("Good", "ProjectFile", "projectFileOfTarget"); + TestTargetStartedEvent("Good", "ProjectFile", "ProjectFile"); + } + + /// + /// Test the target started event with different values being null. + /// + [TestMethod] + public void TargetStartedWithParentTarget() + { + TestTargetStartedWithParentTargetEvent(null, "ProjectFile", "projectFileOfTarget"); + TestTargetStartedWithParentTargetEvent(String.Empty, "ProjectFile", "projectFileOfTarget"); + TestTargetStartedWithParentTargetEvent("TargetName", null, "projectFileOfTarget"); + TestTargetStartedWithParentTargetEvent("TargetName", String.Empty, "projectFileOfTarget"); + TestTargetStartedWithParentTargetEvent("Good", "ProjectFile", null); + TestTargetStartedWithParentTargetEvent("Good", "ProjectFile", String.Empty); + TestTargetStartedWithParentTargetEvent("Good", "ProjectFile", "projectFileOfTarget"); + TestTargetStartedWithParentTargetEvent("Good", "ProjectFile", "ProjectFile"); + } + #endregion + + #region TargetFinished + /// + /// Expect an exception to be thrown if a null build event context is passed in + /// and OnlyLogCriticalEvents is false + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TargetFinishedNullBuildEventContext() + { + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTargetFinished(null, "MyTarget", "ProjectFile", "ProjectFileOfTarget", true, null); + } + + /// + /// Test the case where TargetName is null. + /// + [TestMethod] + public void TargetFinishedNullTargetName() + { + TestTargetFinished(null, "ProjectFile", "ProjectFileOfTarget", true); + TestTargetFinished(String.Empty, "ProjectFile", "ProjectFileOfTarget", true); + + TestTargetFinished("TargetName", null, "ProjectFileOfTarget", true); + TestTargetFinished("TargetName", String.Empty, "ProjectFileOfTarget", true); + + TestTargetFinished("TargetName", "ProjectFile", null, true); + TestTargetFinished("TargetName", "ProjectFile", String.Empty, true); + + TestTargetFinished("TargetName", "ProjectFile", "ProjectFileOfTarget", true); + TestTargetFinished("TargetName", "ProjectFile", "ProjectFileOfTarget", false); + } + #endregion + + #endregion + + #endregion + + #region Private methods + + /// + /// Generate a message from an exception and a resource string. This is used for both errors and warnings. + /// + /// Exception to add to end of message + /// Resource name to generate message from + /// Error or Warning code which is output from FormatResourceString + /// output HelpKeyword + /// output message + /// parameters to use in format resource string + private void GenerateMessageFromExceptionAndResource(Exception exception, string resourceName, out string code, out string helpKeyword, out string message, params string[] parameters) + { + message = ResourceUtilities.FormatResourceString(out code, out helpKeyword, resourceName, parameters); +#if DEBUG + message += Environment.NewLine + "This is an unhandled exception from a task -- PLEASE OPEN A BUG AGAINST THE TASK OWNER."; +#endif + if (exception != null) + { + message += Environment.NewLine + exception.ToString(); + } + } + + /// + /// Verify LogErrorFromText + /// + /// ErrorCode to test + /// HelpKeyword to test + /// SubCategory which will be used to get the Subcategory + /// Message to test + private void TestLogErrorFromText(string errorCode, string helpKeyword, string subcategoryKey, string message) + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + string subcategory = null; + if (subcategoryKey != null) + { + subcategory = AssemblyResources.GetString(subcategoryKey); + } + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogErrorFromText(s_buildEventContext, subcategoryKey, errorCode, helpKeyword, fileInfo, message); + VerifyBuildErrorEventArgs(fileInfo, errorCode, helpKeyword, message, service, subcategory); + } + + /// + /// Verify LogWarningFromText + /// + /// WarningCode to test + /// HelpKeyword to test + /// SubCategory which will be used to get the Subcategory + /// Message to test + private void TestLogWarningFromText(string warningCode, string helpKeyword, string subcategoryKey, string message) + { + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + string subcategory = null; + if (subcategoryKey != null) + { + subcategory = AssemblyResources.GetString(subcategoryKey); + } + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogWarningFromText(s_buildEventContext, subcategoryKey, warningCode, helpKeyword, fileInfo, message); + VerifyBuildWarningEventArgs(fileInfo, warningCode, helpKeyword, message, service, subcategory); + } + + /// + /// Test LogWarning + /// + /// TaskName to test + /// SubCategoryKey to test + private void TestLogWarning(string taskName, string subCategoryKey) + { + string subcategory = AssemblyResources.GetString(subCategoryKey); + BuildEventFileInfo fileInfo = new BuildEventFileInfo("foo.cs", 1, 2, 3, 4); + string warningCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out warningCode, out helpKeyword, "FatalTaskError", taskName); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + + service.LogWarning(s_buildEventContext, subCategoryKey, fileInfo, "FatalTaskError", taskName); + VerifyBuildWarningEventArgs(fileInfo, warningCode, helpKeyword, message, service, subcategory); + } + + /// + /// Test ProjectFinishedEvent + /// + /// Project File to Test + /// Success value to test + private void TestProjectFinishedEvent(string projectFile, bool success) + { + string message = ResourceUtilities.FormatResourceString((success ? "ProjectFinishedSuccess" : "ProjectFinishedFailure"), Path.GetFileName(projectFile)); + MockHost componentHost = new MockHost(); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1, componentHost); + try + { + service.LogProjectFinished(s_buildEventContext, projectFile, success); + } + catch (InternalErrorException ex) + { + Assert.IsTrue(ex.Message.Contains("ContextID " + s_buildEventContext.ProjectContextId)); + } + finally + { + service.ResetProcessedBuildEvent(); + } + + ConfigCache cache = (ConfigCache)componentHost.GetComponent(BuildComponentType.ConfigCache); + + BuildRequestData data = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(2, data, "4.0"); + cache.AddConfiguration(config); + + // Now do it the right way -- with a matching ProjectStarted. + BuildEventContext projectContext = service.LogProjectStarted + ( + new BuildEventContext(1, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId), + 1, + 2, + s_buildEventContext, + projectFile, + null, + null, + null + ); + + service.LogProjectFinished(projectContext, projectFile, success); + + VerifyProjectFinishedEvent(service, projectContext, message, projectFile, success); + + service.ResetProcessedBuildEvent(); + } + + /// + /// Test TaskStartedEvent + /// + /// TaskName to test + /// ProjectFile to test + /// ProjectFileOfTask to test + private void TestTaskStartedEvent(string taskName, string projectFile, string projectFileOfTask) + { + string message = ResourceUtilities.FormatResourceString("TaskStarted", taskName); + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTaskStarted(s_buildEventContext, taskName, projectFile, projectFileOfTask); + VerifyTaskStartedEvent(taskName, projectFile, projectFileOfTask, message, service); + + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogTaskStarted(s_buildEventContext, taskName, projectFile, projectFileOfTask); + Assert.IsNull(service.ProcessedBuildEvent); + } + + /// + /// Test task Finished event + /// + /// TaskName to test + /// ProjectFile to test + /// ProjectFileOfTask to test + /// Succeeded value to test + private void TestTaskFinished(string taskName, string projectFile, string projectFileOfTask, bool succeeded) + { + string message = ResourceUtilities.FormatResourceString((succeeded ? "TaskFinishedSuccess" : "TaskFinishedFailure"), taskName); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTaskFinished(s_buildEventContext, taskName, projectFile, projectFileOfTask, succeeded); + VerifyTaskFinishedEvent(taskName, projectFile, projectFileOfTask, succeeded, message, service); + + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogTaskFinished(s_buildEventContext, taskName, projectFile, projectFileOfTask, succeeded); + Assert.IsNull(service.ProcessedBuildEvent); + } + + /// + /// Test the TargetFinished event + /// + /// TargetName to test + /// ProjectFile to test + /// ProjectFileOftarget to test + /// Succeeded value to test + private void TestTargetFinished(string targetName, string projectFile, string projectFileOfTarget, bool succeeded) + { + string message = ResourceUtilities.FormatResourceString((succeeded ? "TargetFinishedSuccess" : "TargetFinishedFailure"), targetName, Path.GetFileName(projectFile)); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + List outputs = new List(); + outputs.Add(new TaskItem("ItemInclude", projectFile)); + service.LogTargetFinished(s_targetBuildEventContext, targetName, projectFile, projectFileOfTarget, succeeded, outputs); + VerifyTargetFinishedEvent(targetName, projectFile, projectFileOfTarget, succeeded, message, service, outputs); + + // Test OnlyLogCriticalEvents + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogTargetFinished(s_targetBuildEventContext, targetName, projectFile, projectFileOfTarget, succeeded, outputs); + Assert.IsNull(service.ProcessedBuildEvent); + } + + /// + /// Test the targetStarted event + /// + /// TargetName to test + /// Project file to test + /// ProjectFileOfTarget to test + private void TestTargetStartedEvent(string targetName, string projectFile, string projectFileOfTarget) + { + string message = String.Empty; + + if (String.Equals(projectFile, projectFileOfTarget, StringComparison.OrdinalIgnoreCase)) + { + message = ResourceUtilities.FormatResourceString("TargetStartedProjectEntry", targetName, projectFile); + } + else + { + message = ResourceUtilities.FormatResourceString("TargetStartedFileProjectEntry", targetName, projectFileOfTarget, projectFile); + } + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTargetStarted(s_targetBuildEventContext, targetName, projectFile, projectFileOfTarget, String.Empty); + VerifyTargetStartedEvent(targetName, projectFile, projectFileOfTarget, message, service); + + // Do not expect to have any event logged when OnlyLogCriticalEvents is true + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogTargetStarted(s_targetBuildEventContext, targetName, projectFile, projectFileOfTarget, null); + Assert.IsNull(service.ProcessedBuildEvent); + } + + /// + /// Test the targetStarted event when there is a parent target + /// + private void TestTargetStartedWithParentTargetEvent(string targetName, string projectFile, string projectFileOfTarget) + { + string parentTargetName = "MyParentTarget"; + string message = String.Empty; + if (String.Equals(projectFile, projectFileOfTarget, StringComparison.OrdinalIgnoreCase)) + { + message = ResourceUtilities.FormatResourceString("TargetStartedProjectDepends", targetName, projectFile, parentTargetName); + } + else + { + message = ResourceUtilities.FormatResourceString("TargetStartedFileProjectDepends", targetName, projectFileOfTarget, projectFile, parentTargetName); + } + + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1); + service.LogTargetStarted(s_targetBuildEventContext, targetName, projectFile, projectFileOfTarget, parentTargetName); + VerifyTargetStartedEvent(targetName, projectFile, projectFileOfTarget, message, service); + + // Do not expect to have any event logged when OnlyLogCriticalEvents is true + service.ResetProcessedBuildEvent(); + service.OnlyLogCriticalEvents = true; + service.LogTargetStarted(s_targetBuildEventContext, targetName, projectFile, projectFileOfTarget, parentTargetName); + Assert.IsNull(service.ProcessedBuildEvent); + } + + /// + /// Test LogProjectStarted + /// + private void LogProjectStartedTestHelper(string projectFile, string targetNames) + { + string message = string.Empty; + if (!String.IsNullOrEmpty(targetNames)) + { + message = ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithTargetNames", Path.GetFileName(projectFile), targetNames); + } + else + { + message = ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", Path.GetFileName(projectFile)); + } + + MockHost componentHost = new MockHost(); + ProcessBuildEventHelper service = (ProcessBuildEventHelper)ProcessBuildEventHelper.CreateLoggingService(LoggerMode.Synchronous, 1, componentHost); + ConfigCache cache = (ConfigCache)componentHost.GetComponent(BuildComponentType.ConfigCache); + + BuildRequestData data = new BuildRequestData("file", new Dictionary(StringComparer.OrdinalIgnoreCase), "toolsVersion", new string[0], null); + BuildRequestConfiguration config = new BuildRequestConfiguration(2, data, "4.0"); + cache.AddConfiguration(config); + + BuildEventContext context = service.LogProjectStarted(s_buildEventContext, 1, 2, s_buildEventContext, projectFile, targetNames, null, null); + BuildEventContext parentBuildEventContext = s_buildEventContext; + VerifyProjectStartedEventArgs(service, context.ProjectContextId, message, projectFile, targetNames, parentBuildEventContext, context); + + service.ResetProcessedBuildEvent(); + } + + /// + /// Create a TargetFinished event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// TargetName to create the comparison event with. + /// ProjectFile to create the comparison event with. + /// ProjectFileOfTarget to create the comparison event with. + /// Succeeded value to create the comparison event with. + /// Message to create the comparison event with. + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + private void VerifyTargetFinishedEvent(string targetName, string projectFile, string projectFileOfTarget, bool succeeded, string message, ProcessBuildEventHelper service, IEnumerable targetOutputs) + { + TargetFinishedEventArgs targetEvent = new TargetFinishedEventArgs + ( + message, + null, + targetName, + projectFile, + projectFileOfTarget, + succeeded, + service.ProcessedBuildEvent.Timestamp, + targetOutputs + ); + targetEvent.BuildEventContext = s_targetBuildEventContext; + Assert.IsTrue(((TargetFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(targetEvent)); + } + + /// + /// Create a TargetStarted event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// TaskName to create the comparison event with. + /// ProjectFile to create the comparison event with. + /// ProjectFileOfTarget to create the comparison event with. + /// Message to create the comparison event with. + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + private void VerifyTargetStartedEvent(string targetName, string projectFile, string projectFileOfTarget, string message, ProcessBuildEventHelper service) + { + TargetStartedEventArgs buildEvent = new TargetStartedEventArgs + ( + message, + null, // no help keyword + targetName, + projectFile, + projectFileOfTarget, + String.Empty, + service.ProcessedBuildEvent.Timestamp + ); + buildEvent.BuildEventContext = s_targetBuildEventContext; + Assert.IsTrue(((TargetStartedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + } + + /// + /// Create a TaskFinished event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// TaskName to create the comparison event with. + /// ProjectFile to create the comparison event with. + /// ProjectFileOfTask to create the comparison event with. + /// Succeeded value to create the comparison event with. + /// Message to create the comparison event with. + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + private void VerifyTaskFinishedEvent(string taskName, string projectFile, string projectFileOfTask, bool succeeded, string message, ProcessBuildEventHelper service) + { + TaskFinishedEventArgs taskEvent = new TaskFinishedEventArgs + ( + message, + null, + projectFile, + projectFileOfTask, + taskName, + succeeded, + service.ProcessedBuildEvent.Timestamp + ); + taskEvent.BuildEventContext = s_buildEventContext; + Assert.IsTrue(((TaskFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(taskEvent)); + } + + /// + /// Create a taskStarted event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// TaskName to create the comparison event with. + /// ProjectFile to create the comparison event with. + /// ProjectFileOfTask to create the comparison event with. + /// Message to create the comparison event with. + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + private void VerifyTaskStartedEvent(string taskName, string projectFile, string projectFileOfTask, string message, ProcessBuildEventHelper service) + { + TaskStartedEventArgs taskEvent = new TaskStartedEventArgs + ( + message, + null, // no help keyword + projectFile, + projectFileOfTask, + taskName, + service.ProcessedBuildEvent.Timestamp + ); + taskEvent.BuildEventContext = s_buildEventContext; + Assert.IsTrue(((TaskStartedEventArgs)service.ProcessedBuildEvent).IsEquivalent(taskEvent)); + } + + /// + /// Create a projectFinished event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + /// The build event context that this ProjectFinished event should contain + /// Message to create the comparison event with. + /// ProjectFile to create the comparison event with. + /// Success value to create the comparison event with + private void VerifyProjectFinishedEvent(ProcessBuildEventHelper service, BuildEventContext projectContext, string message, string projectFile, bool success) + { + ProjectFinishedEventArgs projectEvent = new ProjectFinishedEventArgs + ( + message, + null, + projectFile, + success, + service.ProcessedBuildEvent.Timestamp + ); + projectEvent.BuildEventContext = projectContext; + Assert.IsTrue(((ProjectFinishedEventArgs)service.ProcessedBuildEvent).IsEquivalent(projectEvent)); + } + + /// + /// Create a projectStarted event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + /// ProjectId to create the comparison event with. + /// Message to create the comparison event with. + /// ProjectFile to create the comparison event with. + /// TargetNames to create the comparison event with. + /// ParentBuildEventContext to create the comparison event with. + private void VerifyProjectStartedEventArgs(ProcessBuildEventHelper service, int projectId, string message, string projectFile, string targetNames, BuildEventContext parentBuildEventContext, BuildEventContext generatedContext) + { + ProjectStartedEventArgs buildEvent = new ProjectStartedEventArgs + ( + projectId, + message, + null, // no help keyword + projectFile, + targetNames, + null, + null, + parentBuildEventContext, + service.ProcessedBuildEvent.Timestamp + ); + buildEvent.BuildEventContext = generatedContext; + Assert.IsTrue(((ProjectStartedEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildEvent)); + } + + /// + /// Create a buildMessage event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + /// Importance level create the comparison event with + /// Message to create the comparison event with + private void VerityBuildMessageEventArgs(ProcessBuildEventHelper service, MessageImportance messageImportance, string message) + { + BuildMessageEventArgs buildMessageEvent = new BuildMessageEventArgs + ( + message, + null, + "MSBuild", + messageImportance, + service.ProcessedBuildEvent.Timestamp + ); + + buildMessageEvent.BuildEventContext = s_buildEventContext; + Assert.IsTrue(((BuildMessageEventArgs)service.ProcessedBuildEvent).IsEquivalent(buildMessageEvent)); + } + + /// + /// Create a buildWarning event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// FileInfo to create the comparison event with + /// Warningcode to create the comparison event with c + /// helpKeyword to create the comparison event with + /// message to create the comparison event with + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + /// Subcategory to create the comparison event with + private void VerifyBuildWarningEventArgs(BuildEventFileInfo fileInfo, string warningCode, string helpKeyword, string message, ProcessBuildEventHelper service, string subcategory) + { + BuildWarningEventArgs buildEvent = new BuildWarningEventArgs + ( + subcategory, + warningCode, + fileInfo.File, + fileInfo.Line, + fileInfo.Column, + fileInfo.EndLine, + fileInfo.EndColumn, + message, + helpKeyword, + "MSBuild", + service.ProcessedBuildEvent.Timestamp + ); + buildEvent.BuildEventContext = s_buildEventContext; + Assert.IsTrue(buildEvent.IsEquivalent((BuildWarningEventArgs)service.ProcessedBuildEvent)); + } + + /// + /// Create a buildError event to compare to the one which was passed into the ProcessedBuildEvent method. + /// + /// FileInfo to create the comparison event with + /// Errorcode to create the comparison event with c + /// helpKeyword to create the comparison event with + /// message to create the comparison event with + /// LoggingService mock object which overrides ProcessBuildEvent and can provide a ProcessedBuildEvent (the event which would have been sent to the loggers) + /// Subcategory to create the comparison event with + private void VerifyBuildErrorEventArgs(BuildEventFileInfo fileInfo, string errorCode, string helpKeyword, string message, ProcessBuildEventHelper service, string subcategory) + { + BuildErrorEventArgs buildEvent = new BuildErrorEventArgs + ( + subcategory, + errorCode, + fileInfo.File, + fileInfo.Line, + fileInfo.Column, + fileInfo.EndLine, + fileInfo.EndColumn, + message, + helpKeyword, + "MSBuild", + service.ProcessedBuildEvent.Timestamp + ); + buildEvent.BuildEventContext = s_buildEventContext; + Assert.IsTrue(buildEvent.IsEquivalent((BuildErrorEventArgs)service.ProcessedBuildEvent)); + } + + /// + /// Log a given build event and verify it is sent to ProcessLoggingEvent + /// + /// BuildEvent to log and expect from ProcessLoggingEvent + /// LoggingService to log event to + private void LogandVerifyBuildEvent(BuildEventArgs expectedBuildEvent, ProcessBuildEventHelper loggingService) + { + loggingService.LogBuildEvent(expectedBuildEvent); + Assert.IsTrue(loggingService.ProcessedBuildEvent.IsEquivalent(expectedBuildEvent), "Expected ProcessedBuildEvent to equal expected build event"); + loggingService.ResetProcessedBuildEvent(); + } + #endregion + + #region Helper Classes + /// + /// Create a derrived class which overrides ProcessLoggingEvent so + /// we can test most of the logging methods without relying on the + /// exact implementation of process logging events. + /// + internal class ProcessBuildEventHelper : LoggingService + { + #region Data + /// + /// Event processed by ProcessLoggingEvent. This can be asserted in a test + /// to verify that a buildEvent was sent to ProcessLoggingEvent. + /// + private BuildEventArgs _processedBuildEvent; + #endregion + #region Constructor + /// + /// Create a constructor which calls the base class constructor + /// + /// Is the logging service supposed to be Synchronous or Asynchronous + protected ProcessBuildEventHelper(LoggerMode loggerMode, int nodeId, IBuildComponentHost componentHost) + : base(loggerMode, nodeId) + { + if (componentHost == null) + { + componentHost = new MockHost(); + } + + InitializeComponent(componentHost); + } + #endregion + + #region Properties + /// + /// Accessor for the event processed by ProcessLoggingEvent + /// + public BuildEventArgs ProcessedBuildEvent + { + get + { + return _processedBuildEvent; + } + } + #endregion + + #region Methods + + /// + /// Create a new instance of a LoggingServiceOverrideProcessBuildEvent class + /// + /// Logger mode, this is not used + /// Instantiated LoggingServiceOverrideProcessBuildEvent + public new static IBuildComponent CreateLoggingService(LoggerMode mode, int nodeId) + { + return new ProcessBuildEventHelper(mode, nodeId, null); + } + + /// + /// Create a new instance of a LoggingServiceOverrideProcessBuildEvent class + /// + /// Logger mode, this is not used + /// Instantiated LoggingServiceOverrideProcessBuildEvent + public static IBuildComponent CreateLoggingService(LoggerMode mode, int nodeId, IBuildComponentHost componentHost) + { + return new ProcessBuildEventHelper(mode, nodeId, componentHost); + } + + /// + /// Override the method to log which event was p1446 + /// rocessed so it can be verified in a test + /// + /// Build event which was asked to be processed + internal override void ProcessLoggingEvent(object buildEvent, bool allowThrottling = false) + { + if (buildEvent is BuildEventArgs) + { + _processedBuildEvent = buildEvent as BuildEventArgs; + } + else if (buildEvent is KeyValuePair) + { + _processedBuildEvent = ((KeyValuePair)buildEvent).Value; + } + else + { + _processedBuildEvent = null; + } + } + + /// + /// Reset the event processed by ProcessLoggingEvent. + /// This is done so another event can be logged. + /// + internal void ResetProcessedBuildEvent() + { + _processedBuildEvent = null; + } + #endregion + } + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/Lookup_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/Lookup_Tests.cs new file mode 100644 index 00000000000..18d507ad13a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/Lookup_Tests.cs @@ -0,0 +1,1460 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using System.Xml; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; +using System.Diagnostics; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class Lookup_Tests + { + /// + /// Primary group contains an item for a type and secondary does; + /// primary item should be returned instead of the secondary item. + /// + [TestMethod] + public void SecondaryItemShadowedByPrimaryItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath)); + table1.Add(new ProjectItemInstance(project, "i2", "a%3b1", project.FullPath)); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + lookup.PopulateWithItem(new ProjectItemInstance(project, "i1", "a2", project.FullPath)); + lookup.PopulateWithItem(new ProjectItemInstance(project, "i2", "a%282", project.FullPath)); + + // Should return the item from the primary, not the secondary table + Assert.AreEqual("a2", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual("a(2", lookup.GetItems("i2").First().EvaluatedInclude); + } + + /// + /// Primary group does not contain an item for a type but secondary does; + /// secondary item should be returned. + /// + [TestMethod] + public void SecondaryItemNotShadowedByPrimaryItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath)); + table1.Add(new ProjectItemInstance(project, "i2", "a%3b1", project.FullPath)); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Should return item from the secondary table. + Assert.AreEqual("a1", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual("a;1", lookup.GetItems("i2").First().EvaluatedInclude); + } + + /// + /// No items of that type: should return empty group rather than null + /// + [TestMethod] + public void UnknownItemType() + { + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); // Doesn't matter really + + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + /// + /// Adds accumulate as we lookup in the tables + /// + [TestMethod] + public void AddsAreCombinedWithPopulates() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + // One item in the project + ItemDictionary table1 = new ItemDictionary(); + table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath)); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // We see the one item + Assert.AreEqual("a1", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual(1, lookup.GetItems("i1").Count); + + // One item in the project + Assert.AreEqual("a1", table1["i1"].First().EvaluatedInclude); + Assert.AreEqual(1, table1["i1"].Count); + + // Start a target + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // We see the one item + Assert.AreEqual("a1", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual(1, lookup.GetItems("i1").Count); + + // One item in the project + Assert.AreEqual("a1", table1["i1"].First().EvaluatedInclude); + Assert.AreEqual(1, table1["i1"].Count); + + // Start a task (eg) and add a new item + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + lookup.AddNewItem(new ProjectItemInstance(project, "i1", "a2", project.FullPath)); + + // Now we see two items + Assert.AreEqual("a1", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual("a2", lookup.GetItems("i1").ElementAt(1).EvaluatedInclude); + Assert.AreEqual(2, lookup.GetItems("i1").Count); + + // But there's still one item in the project + Assert.AreEqual("a1", table1["i1"].First().EvaluatedInclude); + Assert.AreEqual(1, table1["i1"].Count); + + // Finish the task + enteredScope2.LeaveScope(); + + // We still see two items + Assert.AreEqual("a1", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual("a2", lookup.GetItems("i1").ElementAt(1).EvaluatedInclude); + Assert.AreEqual(2, lookup.GetItems("i1").Count); + + // But there's still one item in the project + Assert.AreEqual("a1", table1["i1"].First().EvaluatedInclude); + Assert.AreEqual(1, table1["i1"].Count); + + // Finish the target + enteredScope.LeaveScope(); + + // We still see two items + Assert.AreEqual("a1", lookup.GetItems("i1").First().EvaluatedInclude); + Assert.AreEqual("a2", lookup.GetItems("i1").ElementAt(1).EvaluatedInclude); + Assert.AreEqual(2, lookup.GetItems("i1").Count); + + // And now the items have gotten put into the global group + Assert.AreEqual("a1", table1["i1"].First().EvaluatedInclude); + Assert.AreEqual("a2", table1["i1"].ElementAt(1).EvaluatedInclude); + Assert.AreEqual(2, table1["i1"].Count); + } + + /// + /// Adds when duplicate removal is enabled removes only duplicates. Tests only item specs, not metadata differences + /// + [TestMethod] + public void AddsWithDuplicateRemovalItemSpecsOnly() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + // One item in the project + ItemDictionary table1 = new ItemDictionary(); + table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath)); + + // Add an existing duplicate + table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath)); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + var scope = lookup.EnterScope("test"); + + // This one should not get added + ProjectItemInstance[] newItems = new ProjectItemInstance[] + { + new ProjectItemInstance(project, "i1", "a1", project.FullPath), // Should not get added + new ProjectItemInstance(project, "i1", "a2", project.FullPath), // Should get added + }; + + // Perform the addition + lookup.AddNewItemsOfItemType("i1", newItems, doNotAddDuplicates: true); + + var group = lookup.GetItems("i1"); + + // We should have the original two duplicates plus one new addition. + Assert.AreEqual(3, group.Count); + + // Only two of the items should have the 'a1' include. + Assert.AreEqual(2, group.Where(item => item.EvaluatedInclude == "a1").Count()); + // And ensure the other item got added. + Assert.AreEqual(1, group.Where(item => item.EvaluatedInclude == "a2").Count()); + + scope.LeaveScope(); + + group = lookup.GetItems("i1"); + + // We should have the original two duplicates plus one new addition. + Assert.AreEqual(3, group.Count); + + // Only two of the items should have the 'a1' include. + Assert.AreEqual(2, group.Where(item => item.EvaluatedInclude == "a1").Count()); + // And ensure the other item got added. + Assert.AreEqual(1, group.Where(item => item.EvaluatedInclude == "a2").Count()); + } + + /// + /// Adds when duplicate removal is enabled removes only duplicates. Tests only item specs, not metadata differences + /// + [TestMethod] + public void AddsWithDuplicateRemovalWithMetadata() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + + ItemDictionary table1 = new ItemDictionary(); + + // Two items, differ only by metadata + table1.Add(new ProjectItemInstance(project, "i1", "a1", new KeyValuePair[] { new KeyValuePair("m1", "m1") }, project.FullPath)); + table1.Add(new ProjectItemInstance(project, "i1", "a1", new KeyValuePair[] { new KeyValuePair("m1", "m2") }, project.FullPath)); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + var scope = lookup.EnterScope("test"); + + // This one should not get added + ProjectItemInstance[] newItems = new ProjectItemInstance[] + { + new ProjectItemInstance(project, "i1", "a1", project.FullPath), // Should get added + new ProjectItemInstance(project, "i1", "a2", new KeyValuePair[] { new KeyValuePair( "m1", "m1" ) }, project.FullPath), // Should get added + new ProjectItemInstance(project, "i1", "a1", new KeyValuePair[] { new KeyValuePair( "m1", "m1" ) }, project.FullPath), // Should not get added + new ProjectItemInstance(project, "i1", "a1", new KeyValuePair[] { new KeyValuePair( "m1", "m3" ) }, project.FullPath), // Should get added + }; + + // Perform the addition + lookup.AddNewItemsOfItemType("i1", newItems, doNotAddDuplicates: true); + + var group = lookup.GetItems("i1"); + + // We should have the original two duplicates plus one new addition. + Assert.AreEqual(5, group.Count); + + // Four of the items will have the a1 include + Assert.AreEqual(4, group.Where(item => item.EvaluatedInclude == "a1").Count()); + + // One item will have the a2 include + Assert.AreEqual(1, group.Where(item => item.EvaluatedInclude == "a2").Count()); + + scope.LeaveScope(); + + group = lookup.GetItems("i1"); + + // We should have the original two duplicates plus one new addition. + Assert.AreEqual(5, group.Count); + + // Four of the items will have the a1 include + Assert.AreEqual(4, group.Where(item => item.EvaluatedInclude == "a1").Count()); + + // One item will have the a2 include + Assert.AreEqual(1, group.Where(item => item.EvaluatedInclude == "a2").Count()); + } + + [TestMethod] + public void Removes() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + // One item in the project + ItemDictionary table1 = new ItemDictionary(); + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a1", project.FullPath); + table1.Add(item1); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Start a target + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Start a task (eg) and add a new item + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + ProjectItemInstance item2 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + lookup.AddNewItem(item2); + + // Remove one item + lookup.RemoveItem(item1); + + // We see one item + Assert.AreEqual(1, lookup.GetItems("i1").Count); + Assert.AreEqual("a2", lookup.GetItems("i1").First().EvaluatedInclude); + + // Remove the other item + lookup.RemoveItem(item2); + + // We see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // Finish the task + enteredScope2.LeaveScope(); + + // We still see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // But there's still one item in the project + Assert.AreEqual("a1", table1["i1"].First().EvaluatedInclude); + Assert.AreEqual(1, table1["i1"].Count); + + // Finish the target + enteredScope.LeaveScope(); + + // We still see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // And now there are no items in the project either + Assert.AreEqual(0, table1["i1"].Count); + } + + [TestMethod] + public void RemoveItemPopulatedInLowerScope() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + + // Start a target + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // There's one item in this batch + lookup.PopulateWithItem(item1); + + // We see it + Assert.AreEqual(1, lookup.GetItems("i1").Count); + + // Make a clone so we can keep an eye on that item + Lookup lookup2 = lookup.Clone(); + + // We can see the item in the clone + Assert.AreEqual(1, lookup2.GetItems("i1").Count); + + // Start a task (eg) + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // We see the item below + Assert.AreEqual(1, lookup.GetItems("i1").Count); + + // Remove that item + lookup.RemoveItem(item1); + + // We see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // The clone is unaffected so far + Assert.AreEqual(1, lookup2.GetItems("i1").Count); + + // Finish the task + enteredScope2.LeaveScope(); + + // We still see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // But now the clone doesn't either + Assert.AreEqual(0, lookup2.GetItems("i1").Count); + + // Finish the target + enteredScope.LeaveScope(); + + // We still see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + Assert.AreEqual(0, lookup2.GetItems("i1").Count); + } + + [TestMethod] + public void RemoveItemAddedInLowerScope() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Start a target + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + lookup.AddNewItem(item1); + + // Start a task (eg) + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // We see the item below + Assert.AreEqual(1, lookup.GetItems("i1").Count); + + // Remove that item + lookup.RemoveItem(item1); + + // We see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // Finish the task + enteredScope2.LeaveScope(); + + // We still see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + + // Finish the target + enteredScope.LeaveScope(); + + // We still see no items + Assert.AreEqual(0, lookup.GetItems("i1").Count); + } + + /// + /// Ensure that once keepOnlySpecified is set to true, it remains in effect. + /// + [TestMethod] + public void KeepMetadataOnlySpecifiedPropagate1() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m1", "m1"); + item1.SetMetadata("m2", "m2"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Get rid of all of the metadata. + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true); + ICollection group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + enteredScope2.LeaveScope(); + + // Add metadata m3. + Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata2.Add("m3", "m3"); + group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata2); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + // m3 is still there. + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + + enteredScope.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + // m3 is still there. + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + } + + /// + /// Ensure that if keepOnlySpecified is specified after some metadata have been set in a higher scope that it will + /// eliminate that metadata are the current scope and beyond. + /// + [TestMethod] + public void KeepMetadataOnlySpecifiedPropagate2() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m1", "m1"); + item1.SetMetadata("m2", "m2"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Add m3 metadata + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m3", "m3"); + ICollection group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // All metadata are present + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + Assert.AreEqual("m2", group.First().GetMetadataValue("m2")); + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + + enteredScope2.LeaveScope(); + + // Now clear metadata + Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: true); + group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata2); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // All metadata are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + + enteredScope.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // All metadata are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + } + + /// + /// Ensure that once keepOnlySpecified is set to true, it remains in effect, but that metadata explicitly added at subsequent levels is still retained. + /// + [TestMethod] + public void KeepMetadataOnlySpecifiedPropagate3() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m1", "m1"); + item1.SetMetadata("m2", "m2"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Get rid of all of the metadata, then add m3 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true); + newMetadata.Add("m3", "m3"); + ICollection group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + // m3 is still there. + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + + enteredScope2.LeaveScope(); + + // Add metadata m4. + Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: true); + newMetadata2.Add("m4", "m4"); + group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata2); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1, m2 and m3 are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + + // m4 is still there. + Assert.AreEqual("m4", group.First().GetMetadataValue("m4")); + + enteredScope.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1, m2 and m3 are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m3")); + + // m4 is still there. + Assert.AreEqual("m4", group.First().GetMetadataValue("m4")); + } + + + /// + /// Ensure that once keepOnlySpecified is set to true, it remains in effect, and that if a metadata modification is declared as 'keep value' that + /// the value as lower scopes is retained. + [TestMethod] + public void KeepMetadataOnlySpecifiedPropagate4() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m1", "m1"); + item1.SetMetadata("m2", "m2"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Get rid of all of the metadata, then add m3 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true); + newMetadata.Add("m3", "m3"); + ICollection group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + // m3 is still there. + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + + enteredScope2.LeaveScope(); + + // Keep m3. + Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: true); + newMetadata2["m3"] = Lookup.MetadataModification.CreateFromNoChange(); + group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata2); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + // m3 is still there + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + + enteredScope.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + // m3 is still there. + Assert.AreEqual("m3", group.First().GetMetadataValue("m3")); + } + + /// + /// Ensure that when keepOnlySpecified is true, we will clear all metadata unless it is retained using the 'NoChange' modification type. + /// + [TestMethod] + public void KeepMetadataOnlySpecified() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m1", "m1"); + item1.SetMetadata("m2", "m2"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Test keeping only specified metadata + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true); + newMetadata["m1"] = Lookup.MetadataModification.CreateFromNoChange(); + ICollection group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 is still here. + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + + // m2 is gone + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + enteredScope2.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 should still be here + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + + // m2 is gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + enteredScope.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 should still be here + Assert.AreEqual("m1", group.First().GetMetadataValue("m1")); + + // m2 should not persist here either + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + } + + [TestMethod] + public void KeepMetadataOnlySpecifiedNoneSpecified() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m1", "m1"); + item1.SetMetadata("m2", "m2"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Test keeping only specified metadata + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true); + ICollection group = lookup.GetItems(item1.ItemType); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + enteredScope2.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + + enteredScope.LeaveScope(); + + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + + // m1 and m2 are gone. + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, group.First().GetMetadataValue("m2")); + } + + [TestMethod] + public void ModifyItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + lookup.AddNewItem(item1); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Change the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + ICollection group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Now it has m=m2 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m2", group.First().GetMetadataValue("m")); + + // But the original item hasn't changed yet + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + + enteredScope2.LeaveScope(); + + // It still has m=m2 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m2", group.First().GetMetadataValue("m")); + + // The original item still hasn't changed + // even though it was added in this scope + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + + enteredScope.LeaveScope(); + + // It still has m=m2 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m2", group.First().GetMetadataValue("m")); + + // But now the original item has changed + Assert.AreEqual("m2", item1.GetMetadataValue("m")); + } + + /// + /// Modifications should be merged + /// + [TestMethod] + public void ModifyItemModifiedInPreviousScope() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Add an item with m=m1 and n=n1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + lookup.PopulateWithItem(item1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Make a modification to the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + newMetadata.Add("n", "n2"); + ICollection group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Make another modification to the item + newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m3"); + newMetadata.Add("o", "o3"); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // It's now m=m3, n=n2, o=o3 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m3", group.First().GetMetadataValue("m")); + Assert.AreEqual("n2", group.First().GetMetadataValue("n")); + Assert.AreEqual("o3", group.First().GetMetadataValue("o")); + } + + /// + /// Modifications should be merged + /// + [TestMethod] + public void ModifyItemTwiceInSameScope1() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Add an item with m=m1 and n=n1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + lookup.PopulateWithItem(item1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Make a modification to the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + ICollection group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Make an unrelated modification to the item + newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("n", "n1"); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // It's now m=m2 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m2", group.First().GetMetadataValue("m")); + } + + /// + /// Modifications should be merged + /// + [TestMethod] + public void ModifyItemTwiceInSameScope2() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Add an item with m=m1 and n=n1 and o=o1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + item1.SetMetadata("n", "n1"); + item1.SetMetadata("o", "o1"); + lookup.PopulateWithItem(item1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // It's still m=m1, n=n1, o=o1 + ICollection group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m1", group.First().GetMetadataValue("m")); + Assert.AreEqual("n1", group.First().GetMetadataValue("n")); + Assert.AreEqual("o1", group.First().GetMetadataValue("o")); + + // Make a modification to the item to be m=m2 and n=n2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + newMetadata.Add("n", "n2"); + group = new List(); + group.Add(item1); + lookup.ModifyItems("i1", group, newMetadata); + + // It's now m=m2, n=n2, o=o1 + ICollection foundGroup = lookup.GetItems("i1"); + Assert.AreEqual(1, foundGroup.Count); + Assert.AreEqual("m2", foundGroup.First().GetMetadataValue("m")); + Assert.AreEqual("n2", foundGroup.First().GetMetadataValue("n")); + Assert.AreEqual("o1", foundGroup.First().GetMetadataValue("o")); + + // Make a modification to the item to be n=n3 + newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("n", "n3"); + lookup.ModifyItems("i1", group, newMetadata); + + // It's now m=m2, n=n3, o=o1 + foundGroup = lookup.GetItems("i1"); + Assert.AreEqual(1, foundGroup.Count); + Assert.AreEqual("m2", foundGroup.First().GetMetadataValue("m")); + Assert.AreEqual("n3", foundGroup.First().GetMetadataValue("n")); + Assert.AreEqual("o1", foundGroup.First().GetMetadataValue("o")); + + // But the original item hasn't changed yet + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("n1", item1.GetMetadataValue("n")); + Assert.AreEqual("o1", item1.GetMetadataValue("o")); + + enteredScope.LeaveScope(); + + // It's still m=m2, n=n3, o=o1 + foundGroup = lookup.GetItems("i1"); + Assert.AreEqual(1, foundGroup.Count); + Assert.AreEqual("m2", foundGroup.First().GetMetadataValue("m")); + Assert.AreEqual("n3", foundGroup.First().GetMetadataValue("n")); + Assert.AreEqual("o1", foundGroup.First().GetMetadataValue("o")); + + // And the original item has changed + Assert.AreEqual("m2", item1.GetMetadataValue("m")); + Assert.AreEqual("n3", item1.GetMetadataValue("n")); + Assert.AreEqual("o1", item1.GetMetadataValue("o")); + } + + + [TestMethod] + public void ModifyItemThatWasAddedInSameScope() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Add an item with m=m1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + lookup.AddNewItem(item1); + + // Change the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + ICollection group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Now it has m=m2 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m2", group.First().GetMetadataValue("m")); + + // But the original item hasn't changed yet + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + + enteredScope.LeaveScope(); + + // It still has m=m2 + group = lookup.GetItems("i1"); + Assert.AreEqual(1, group.Count); + Assert.AreEqual("m2", group.First().GetMetadataValue("m")); + + // But now the original item has changed as well + Assert.AreEqual("m2", item1.GetMetadataValue("m")); + } + + /// + /// Modifying an item in the outside scope is prohibited- + /// purely because we don't need to do it in our code + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ModifyItemInOutsideScope() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Lookup lookup = LookupHelpers.CreateLookup(new ItemDictionary()); + lookup.AddNewItem(new ProjectItemInstance(project, "x", "y", project.FullPath)); + } + + /// + /// After modification, should be able to GetItem and then modify it again + /// + [TestMethod] + public void ModifyItemPreviouslyModifiedAndGottenThroughGetItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Add an item with m=m1 and n=n1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + lookup.PopulateWithItem(item1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Make a modification to the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + ICollection group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Get the item (under the covers, it cloned it in order to apply the modification) + ICollection group2 = lookup.GetItems(item1.ItemType); + Assert.AreEqual(1, group2.Count); + ProjectItemInstance item1b = group2.First(); + + // Modify to m=m3 + Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata2.Add("m", "m3"); + ICollection group3 = new List(); + group3.Add(item1b); + lookup.ModifyItems(item1b.ItemType, group3, newMetadata2); + + // Modifications are visible + ICollection group4 = lookup.GetItems(item1b.ItemType); + Assert.AreEqual(1, group4.Count); + Assert.AreEqual("m3", group4.First().GetMetadataValue("m")); + + // Leave scope + enteredScope.LeaveScope(); + + // Still visible + ICollection group5 = lookup.GetItems(item1b.ItemType); + Assert.AreEqual(1, group5.Count); + Assert.AreEqual("m3", group5.First().GetMetadataValue("m")); + } + + + /// + /// After modification, should be able to GetItem and then modify it again + /// + [TestMethod] + public void ModifyItemInProjectPreviouslyModifiedAndGottenThroughGetItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + // Create some project state with an item with m=m1 and n=n1 + ItemDictionary table1 = new ItemDictionary(); + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + table1.Add(item1); + + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Make a modification to the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + List group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Get the item (under the covers, it cloned it in order to apply the modification) + ICollection group2 = lookup.GetItems(item1.ItemType); + Assert.AreEqual(1, group2.Count); + ProjectItemInstance item1b = group2.First(); + + // Modify to m=m3 + Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata2.Add("m", "m3"); + List group3 = new List(); + group3.Add(item1b); + lookup.ModifyItems(item1b.ItemType, group3, newMetadata2); + + // Modifications are visible + ICollection group4 = lookup.GetItems(item1b.ItemType); + Assert.AreEqual(1, group4.Count); + Assert.AreEqual("m3", group4.First().GetMetadataValue("m")); + + // Leave scope + enteredScope.LeaveScope(); + + // Still visible + ICollection group5 = lookup.GetItems(item1b.ItemType); + Assert.AreEqual(1, group5.Count); + Assert.AreEqual("m3", group5.First().GetMetadataValue("m")); + + // And the one in the project is changed + Assert.AreEqual("m3", item1.GetMetadataValue("m")); + } + + /// + /// After modification, should be able to GetItem and then remove it + /// + [TestMethod] + public void RemoveItemPreviouslyModifiedAndGottenThroughGetItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + ItemDictionary table1 = new ItemDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(table1); + + // Add an item with m=m1 and n=n1 + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + lookup.PopulateWithItem(item1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Make a modification to the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + List group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Get the item (under the covers, it cloned it in order to apply the modification) + ICollection group2 = lookup.GetItems(item1.ItemType); + Assert.AreEqual(1, group2.Count); + ProjectItemInstance item1b = group2.First(); + + // Remove the item + lookup.RemoveItem(item1b); + + // There's now no items at all + ICollection group3 = lookup.GetItems(item1.ItemType); + Assert.AreEqual(0, group3.Count); + } + + /// + /// After modification, should be able to GetItem and then remove it + /// + [TestMethod] + public void RemoveItemFromProjectPreviouslyModifiedAndGottenThroughGetItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + // Create some project state with an item with m=m1 and n=n1 + ItemDictionary table1 = new ItemDictionary(); + ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath); + item1.SetMetadata("m", "m1"); + table1.Add(item1); + + Lookup lookup = LookupHelpers.CreateLookup(table1); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Make a modification to the item to be m=m2 + Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false); + newMetadata.Add("m", "m2"); + List group = new List(); + group.Add(item1); + lookup.ModifyItems(item1.ItemType, group, newMetadata); + + // Get the item (under the covers, it cloned it in order to apply the modification) + ICollection group2 = lookup.GetItems(item1.ItemType); + Assert.AreEqual(1, group2.Count); + ProjectItemInstance item1b = group2.First(); + + // Remove the item + lookup.RemoveItem(item1b); + + // There's now no items at all + ICollection group3 = lookup.GetItems(item1.ItemType); + Assert.AreEqual(0, group3.Count); + + // Leave scope + enteredScope.LeaveScope(); + + // And now none left in the project either + Assert.AreEqual(0, table1["i1"].Count); + } + + /// + /// If the property isn't modified, the initial property + /// should be returned + /// + [TestMethod] + public void UnmodifiedProperty() + { + PropertyDictionary group = new PropertyDictionary(); + ProjectPropertyInstance property = ProjectPropertyInstance.Create("p1", "v1"); + group.Set(property); + Lookup lookup = LookupHelpers.CreateLookup(group); + + Assert.AreEqual(property, lookup.GetProperty("p1")); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + Assert.AreEqual(property, lookup.GetProperty("p1")); + } + + /// + /// If the property isn't found, should return null + /// + [TestMethod] + public void NonexistentProperty() + { + PropertyDictionary group = new PropertyDictionary(); + Lookup lookup = LookupHelpers.CreateLookup(group); + + Assert.AreEqual(null, lookup.GetProperty("p1")); + + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + Assert.AreEqual(null, lookup.GetProperty("p1")); + } + + /// + /// If the property is modified, the updated value should be returned, + /// both before and after leaving scope. + /// + [TestMethod] + public void ModifiedProperty() + { + PropertyDictionary group = new PropertyDictionary(); + group.Set(ProjectPropertyInstance.Create("p1", "v1")); + Lookup lookup = LookupHelpers.CreateLookup(group); + // Enter scope so that property sets are allowed on it + Lookup.Scope enteredScope = lookup.EnterScope("x"); + + // Change the property value + lookup.SetProperty(ProjectPropertyInstance.Create("p1", "v2")); + + // Lookup is updated, but not original item group + Assert.AreEqual("v2", lookup.GetProperty("p1").EvaluatedValue); + Assert.AreEqual("v1", group["p1"].EvaluatedValue); + + Lookup.Scope enteredScope2 = lookup.EnterScope("x"); + + // Change the value again in the new scope + lookup.SetProperty(ProjectPropertyInstance.Create("p1", "v3")); + + // Lookup is updated, but not the original item group + Assert.AreEqual("v3", lookup.GetProperty("p1").EvaluatedValue); + Assert.AreEqual("v1", group["p1"].EvaluatedValue); + + Lookup.Scope enteredScope3 = lookup.EnterScope("x"); + + // Change the value again in the new scope + lookup.SetProperty(ProjectPropertyInstance.Create("p1", "v4")); + + Assert.AreEqual("v4", lookup.GetProperty("p1").EvaluatedValue); + + enteredScope3.LeaveScope(); + + Assert.AreEqual("v4", lookup.GetProperty("p1").EvaluatedValue); + + // Leave to the outer scope + enteredScope2.LeaveScope(); + enteredScope.LeaveScope(); + + // Now the lookup and original group are updated + Assert.AreEqual("v4", lookup.GetProperty("p1").EvaluatedValue); + Assert.AreEqual("v4", group["p1"].EvaluatedValue); + } + +#if false + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LeaveTooMuch() + { + Lookup lookup = LookupHelpers.CreateEmptyLookup(); + Lookup.Scope enteredScope = lookup.EnterScope("x"); + enteredScope.LeaveScope(); + enteredScope.LeaveScope(); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void RemoveScopeOnDifferentThread() + { + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + scopePassedBetweenThreads.LeaveScope(); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void PopulateWithItemOnDifferentThread() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.PopulateWithItem(new ProjectItemInstance(project, "x", "y")); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void PopulateWithItemsOnDifferentThread() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.PopulateWithItems("x", new List()); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void AddNewItemOnDifferentThread() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.AddNewItem(new ProjectItemInstance(project, "x", "y")); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void AddNewItemsOnDifferentThread() + { + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.AddNewItemsOfItemType("x", new List()); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void RemoveItemOnDifferentThread() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.RemoveItem(new ProjectItemInstance(project, "x", "y")); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void RemoveItemsOnDifferentThread() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + List list = new List(); + list.Add(new ProjectItemInstance(project, "x", "y")); + lookupPassedBetweenThreads.RemoveItems(list); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ModifyItemOnDifferentThread() + { + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.ModifyItems("x", new List(), new Dictionary()); + } + + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void SetPropertyOnDifferentThread() + { + Thread thread = new Thread(CreateLookupAndEnterScope); + thread.Start(); + thread.Join(); + + Assert.IsNotNull(lookupPassedBetweenThreads); + lookupPassedBetweenThreads.SetProperty(ProjectPropertyInstance.Create("x", "y")); + } +#endif + + /// + /// No ideal but simple way to get the lookup from another thread + /// + private static Lookup s_lookupPassedBetweenThreads; + + /// + /// Pass scope to other thread + /// + private static Lookup.Scope s_scopePassedBetweenThreads; + + private void CreateLookupAndEnterScope() + { + s_lookupPassedBetweenThreads = LookupHelpers.CreateEmptyLookup(); + s_scopePassedBetweenThreads = s_lookupPassedBetweenThreads.EnterScope("x"); + } + } + + internal class LookupHelpers + { + internal static Lookup CreateEmptyLookup() + { + Lookup lookup = new Lookup(new ItemDictionary(), new PropertyDictionary(), null); + return lookup; + } + + internal static Lookup CreateLookup(ItemDictionary items) + { + Lookup lookup = new Lookup(items, new PropertyDictionary(), null); + return lookup; + } + + internal static Lookup CreateLookup(PropertyDictionary properties) + { + Lookup lookup = new Lookup(new ItemDictionary(), properties, null); + return lookup; + } + + internal static Lookup CreateLookup(PropertyDictionary properties, ItemDictionary items) + { + Lookup lookup = new Lookup(items, properties, null); + return lookup; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/MSBuild_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/MSBuild_Tests.cs new file mode 100644 index 00000000000..e0c0be3eb70 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/MSBuild_Tests.cs @@ -0,0 +1,1658 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class MSBuildTask_Tests + { + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + + + /// + /// If we pass in an item spec that is over the max path but it can be normalized down to something under the max path, we should still work and not + /// throw a path too long exception + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ProjectItemSpecTooLong() + { + string currentDirectory = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = Path.GetTempPath(); + + string tempPath = Path.GetTempPath(); + + string tempProject = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + + + "); + + string fileName = Path.GetFileName(tempProject); + + string projectFile1 = null; + for (int i = 0; i < 250; i++) + { + projectFile1 += "..\\"; + } + + int rootLength = Path.GetPathRoot(tempPath).Length; + string tempPathNoRoot = tempPath.Substring(rootLength); + + projectFile1 += Path.Combine(tempPathNoRoot, fileName); + + string parentProjectContents = @" + + + + + + "; + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + + bool success = p.Build(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(tempProject); + } + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + + /// + /// Ensure that the MSBuild task tags any output items with two pieces of metadata -- MSBuildSourceProjectFile and + /// MSBuildSourceTargetName -- that give an indication of where the items came from. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void OutputItemsAreTaggedWithProjectFileAndTargetName() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + a1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetA + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + b2.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + c1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetC + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + g2.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(7, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Ensures that it is possible to call the MSBuild task with an empty Projects parameter, and it + /// shouldn't error, and it shouldn't try to build itself. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void EmptyProjectsParameterResultsInNoop() + { + string projectContents = @" + + + + + + "; + + MockLogger logger = new MockLogger(); + Project project = ObjectModelHelpers.CreateInMemoryProject(projectContents, logger); + + bool success = project.Build(); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + } + + /// + /// Verifies that nonexistent projects aren't normally skipped + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void NormallyDoNotSkipNonexistentProjects() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"SkipNonexistentProjectsMain.csproj"); + string error = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFound"), "this_project_does_not_exist.csproj"); + Assert.IsTrue(logger.FullLog.Contains(error)); + } + + /// + /// Verifies that nonexistent projects aren't normally skipped + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void NormallyDoNotSkipNonexistentProjectsBuildInParallel() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"SkipNonexistentProjectsMain.csproj"); + string error = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFound"), "this_project_does_not_exist.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 1); + Assert.IsTrue(logger.FullLog.Contains(error)); + } + + /// + /// Verifies that nonexistent projects are skipped when requested + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SkipNonexistentProjects() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "foo.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"SkipNonexistentProjectsMain.csproj"); + + logger.AssertLogContains("Hello from foo.csproj"); + string message = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFoundMessage"), "this_project_does_not_exist.csproj"); + string error = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFound"), "this_project_does_not_exist.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 0); + Assert.IsTrue(logger.FullLog.Contains(message)); // for the missing project + Assert.IsTrue(!logger.FullLog.Contains(error)); + } + + /// + /// Verifies that nonexistent projects are skipped when requested when building in parallel. + /// DDB # 125831 + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SkipNonexistentProjectsBuildingInParallel() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "foo.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"SkipNonexistentProjectsMain.csproj"); + + logger.AssertLogContains("Hello from foo.csproj"); + string message = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFoundMessage"), "this_project_does_not_exist.csproj"); + string error = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFound"), "this_project_does_not_exist.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 0); + Assert.IsTrue(logger.FullLog.Contains(message)); // for the missing project + Assert.IsTrue(!logger.FullLog.Contains(error)); + } + + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void LogErrorWhenBuildingVCProj() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "BuildingVCProjMain.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "foo.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "blah.vcproj", + @" + + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"BuildingVCProjMain.csproj"); + + logger.AssertLogContains("Hello from foo.csproj"); + string error = String.Format(AssemblyResources.GetString("MSBuild.ProjectUpgradeNeededToVcxProj"), "blah.vcproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 1); + Assert.IsTrue(logger.FullLog.Contains(error)); + } + + /// + /// Regression test for bug 533369. Calling the MSBuild task, passing in a property + /// in the Properties parameter that has a special character in its value, such as semicolon. + /// However, it's a situation where the project author doesn't have control over the + /// property value and so he can't escape it himself. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void PropertyOverridesContainSemicolon() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // ------------------------------------------------------- + // ConsoleApplication1.csproj + // ------------------------------------------------------- + + // Just a normal console application project. + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"bug'533'369\Sub;Dir\ConsoleApplication1\ConsoleApplication1.csproj", @" + + + + Debug + AnyCPU + Exe + ConsoleApplication1 + + + true + full + false + bin\Debug\ + + + pdbonly + true + bin\Release\ + + + + + + + + + + + + "); + + // ------------------------------------------------------- + // Program.cs + // ------------------------------------------------------- + + // Just a normal console application project. + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"bug'533'369\Sub;Dir\ConsoleApplication1\Program.cs", @" + using System; + + namespace ConsoleApplication32 + { + class Program + { + static void Main(string[] args) + { + Console.WriteLine(`Hello world`); + } + } + } + "); + + + // ------------------------------------------------------- + // TeamBuild.proj + // ------------------------------------------------------- + // Attempts to build the above ConsoleApplication1.csproj by calling the MSBuild task, + // and overriding the OutDir property. However, the value being passed into OutDir + // is coming from another property which is produced by CreateProperty and has + // some special characters in it. + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"bug'533'369\Sub;Dir\TeamBuild.proj", @" + + + + + + + + + + + + + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"bug'533'369\Sub;Dir\TeamBuild.proj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bug'533'369\Sub;Dir\binaries\ConsoleApplication1.exe"); + } + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DifferentGlobalPropertiesWithDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + + MyProp=1 + + + + MyProp=1 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + a1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetA + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(4, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DifferentGlobalPropertiesWithoutDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + + MyProp=1 + + + + MyProp=1 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(2, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Check trailing semicolons are ignored + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void VariousPropertiesToMSBuildTask() + { + string projectFile = null; + + try + { + projectFile = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + a=a;b=b; + e=e;g=1;f=f; + g;h; + + + + + + + + + + + + + + + + "); + + var logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(projectFile); + + Console.WriteLine(logger.FullLog); + + logger.AssertLogContains("a=[a]"); + logger.AssertLogContains("b=[b]"); + logger.AssertLogContains("c=[]"); + logger.AssertLogContains("d=[]"); + logger.AssertLogContains("e=[e]"); + logger.AssertLogContains("f=[f]"); + logger.AssertLogContains("g=[]"); + } + finally + { + File.Delete(projectFile); + } + } + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + public void DifferentGlobalPropertiesWithBlanks() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + + + + + + MyProp=1 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(1, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + public void DifferentGlobalPropertiesInvalid() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + + =1 + + + + =;1 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + bool success = p.Build(); + Assert.IsFalse(success, "Build succeeded. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Check if passing additional global properites via metadata works + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DifferentAdditionalPropertiesWithDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + MyPropA=1 + + + MyPropA=0 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + a1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetA + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(3, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing additional global properites via metadata works + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DifferentAdditionalPropertiesWithGlobalProperties() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + MyPropG=1 + MyPropA=1 + + + MyPropG=0 + MyPropA=1 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(3, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing additional global properites via metadata works + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DifferentAdditionalPropertiesWithoutDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + string parentProjectContents = @" + + + + MyPropA=1 + + + MyPropA=1 + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(2, targetOutputs["Build"].Items.Length); + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, targetOutputs["Build"].Items, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Properties and Targets that use non-standard separation chars + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TargetsWithSeparationChars() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + Clean%3BBuild%3CBuildAgain + + + + + + + + + + + "); + + string parentProjectContents = @" + + + + + + + + + + + "; + + try + { + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + bool success = p.Build(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Verify stopOnFirstFailure with BuildInParallel override message are correctly logged + /// Also verify stop on first failure will not build the second project if the first one failed + /// The Aardvark tests which also test StopOnFirstFailure are at: + /// qa\md\wd\DTP\MSBuild\ShippingExtensions\ShippingTasks\MSBuild\_Tst\MSBuild.StopOnFirstFailure + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void StopOnFirstFailureandBuildInParallelSingleNode() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string project2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(project1), new TaskItem(project2) + }; + + // Test the various combinations of BuildInParallel and StopOnFirstFailure when the msbuild task is told there are not multiple nodes + // running in the system + for (int i = 0; i < 4; i++) + { + bool buildInParallel = false; + bool stopOnFirstFailure = false; + + // first set up the project being built. + switch (i) + { + case 0: + buildInParallel = true; + stopOnFirstFailure = true; + break; + case 1: + buildInParallel = true; + stopOnFirstFailure = false; + break; + case 2: + buildInParallel = false; + stopOnFirstFailure = true; + break; + case 3: + buildInParallel = false; + stopOnFirstFailure = false; + break; + } + + string parentProjectContents = @" + + + + + + + + + + + + "; + + MockLogger logger = new MockLogger(); + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents, logger); + bool success = p.Build(logger); + switch (i) + { + case 0: + // Verify setting BuildInParallel and StopOnFirstFailure to + // true will cause the msbuild task to set BuildInParallel to false during the execute + // Verify build did not build second project which has the message SecondProject + logger.AssertLogDoesntContain("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogContains(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogContains(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + case 1: + // Verify setting BuildInParallel to true and StopOnFirstFailure to + // false will cause no change in BuildInParallel + // Verify build did build second project which has the message SecondProject + logger.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + case 2: + // Verify build did not build second project which has the message SecondProject + logger.AssertLogDoesntContain("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogContains(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + + case 3: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // false will cause no change in BuildInParallel + // Verify build did build second project which has the message SecondProject + logger.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + } + // The build should fail as the first project has an error + Assert.IsFalse(success, "Iteration of i " + i + " Build Succeded. See 'Standard Out' tab for details."); + } + } + finally + { + File.Delete(project1); + File.Delete(project2); + } + } + + /// + /// Verify stopOnFirstFailure with BuildInParallel override message are correctly logged when there are multiple nodes + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void StopOnFirstFailureandBuildInParallelMultipleNode() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string project2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + try + { + // Test the various combinations of BuildInParallel and StopOnFirstFailure when the msbuild task is told there are multiple nodes + // running in the system + for (int i = 0; i < 4; i++) + { + bool buildInParallel = false; + bool stopOnFirstFailure = false; + + // first set up the project being built. + switch (i) + { + case 0: + buildInParallel = true; + stopOnFirstFailure = true; + break; + case 1: + buildInParallel = true; + stopOnFirstFailure = false; + break; + case 2: + buildInParallel = false; + stopOnFirstFailure = true; + break; + case 3: + buildInParallel = false; + stopOnFirstFailure = false; + break; + } + + string parentProjectContents = @" + + + + + + + + + + + + "; + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(null, new List { logger }, null, ToolsetDefinitionLocations.Registry | ToolsetDefinitionLocations.ConfigurationFile, 2, false); + Project p = ObjectModelHelpers.CreateInMemoryProject(pc, parentProjectContents, logger); + bool success = p.Build(); + switch (i) + { + case 0: + // Verify build did build second project which has the message SecondProject + logger.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogContains(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + case 1: + // Verify setting BuildInParallel to true and StopOnFirstFailure to + // false will cause no change in BuildInParallel + // Verify build did build second project which has the message SecondProject + logger.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + case 2: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // true will cause no change in BuildInParallel + // Verify build did not build second project which has the message SecondProject + logger.AssertLogDoesntContain("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogContains(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + + case 3: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // false will cause no change in BuildInParallel + // Verify build did build second project which has the message SecondProject + logger.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NoStopOnFirstFailure")); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.NotBuildingInParallel")); + break; + } + // The build should fail as the first project has an error + Assert.IsFalse(success, "Iteration of i " + i + " Build Succeded. See 'Standard Out' tab for details."); + } + } + finally + { + File.Delete(project1); + File.Delete(project2); + } + } + + /// + /// Test the skipping of the remaining projects. Verify the skip message is only displayed when there are projects to skip. + /// + [TestMethod] + public void SkipRemainingProjects() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string project2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + try + { + string parentProjectContents = @" + + + + + + + + + + + "; + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(null, new List { logger }, null, ToolsetDefinitionLocations.Registry | ToolsetDefinitionLocations.ConfigurationFile, 2, false); + Project p = ObjectModelHelpers.CreateInMemoryProject(pc, parentProjectContents, logger); + bool success = p.Build(); + + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + Assert.IsFalse(success, "Build Succeded. See 'Standard Out' tab for details."); + + parentProjectContents = @" + + + + + + + + + + + + "; + + MockLogger logger2 = new MockLogger(); + Project p2 = ObjectModelHelpers.CreateInMemoryProject(pc, parentProjectContents, logger2); + bool success2 = p2.Build(); + logger2.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingProjects")); + Assert.IsFalse(success2, "Build Succeded. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(project1); + File.Delete(project2); + } + } + + /// + /// Verify the behavior of Target execution with StopOnFirstFailure + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TargetStopOnFirstFailureBuildInParallel() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(project1) + }; + for (int i = 0; i < 6; i++) + { + bool stopOnFirstFailure = false; + bool runEachTargetSeparately = false; + string target1 = String.Empty; + string target2 = String.Empty; + string target3 = String.Empty; + + switch (i) + { + case 0: + stopOnFirstFailure = true; + runEachTargetSeparately = true; + target1 = "T1"; + target2 = "T2"; + target3 = "T3"; + break; + case 1: + stopOnFirstFailure = true; + runEachTargetSeparately = true; + target1 = "T1"; + target2 = "T3"; + target3 = "T2"; + break; + case 2: + stopOnFirstFailure = false; + runEachTargetSeparately = true; + target1 = "T1"; + target2 = "T3"; + target3 = "T2"; + break; + case 3: + stopOnFirstFailure = true; + target1 = "T1"; + target2 = "T2"; + target3 = "T3"; + break; + case 4: + stopOnFirstFailure = true; + target1 = "T1"; + target2 = "T3"; + target3 = "T2"; + break; + case 5: + stopOnFirstFailure = false; + target1 = "T1"; + target2 = "T3"; + target3 = "T2"; + break; + } + + string parentProjectContents = @" + + + + + + + + + + + + + + + + + "; + + MockLogger logger = new MockLogger(); + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents, logger); + bool success = p.Build(); + + switch (i) + { + case 0: + // Test the case where the error is in the last project and RunEachTargetSeparately = true + logger.AssertLogContains("Proj2 T1 message"); + logger.AssertLogContains("Proj2 T2 message"); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingTargets")); + break; + case 1: + // Test the case where the error is in the second target out of 3. + logger.AssertLogContains("Proj2 T1 message"); + logger.AssertLogContains(AssemblyResources.GetString("MSBuild.SkippingRemainingTargets")); + logger.AssertLogDoesntContain("Proj2 T2 message"); + // The build should fail as the first project has an error + break; + case 2: + // Test case where error is in second last target but stopOnFirstFailure is false + logger.AssertLogContains("Proj2 T1 message"); + logger.AssertLogContains("Proj2 T2 message"); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingTargets")); + break; + // Test the cases where RunEachTargetSeparately is false. In these cases all of the targets should be submitted at once + case 3: + // Test the case where the error is in the last project and RunEachTargetSeparately = true + logger.AssertLogContains("Proj2 T1 message"); + logger.AssertLogContains("Proj2 T2 message"); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingTargets")); + // The build should fail as the first project has an error + break; + case 4: + // Test the case where the error is in the second target out of 3. + logger.AssertLogContains("Proj2 T1 message"); + logger.AssertLogDoesntContain("Proj2 T2 message"); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingTargets")); + // The build should fail as the first project has an error + break; + case 5: + // Test case where error is in second last target but stopOnFirstFailure is false + logger.AssertLogContains("Proj2 T1 message"); + logger.AssertLogDoesntContain("Proj2 T2 message"); + logger.AssertLogDoesntContain(AssemblyResources.GetString("MSBuild.SkippingRemainingTargets")); + break; + } + + // The build should fail as the first project has an error + Assert.IsFalse(success, "Iteration of i:" + i + "Build Succeded. See 'Standard Out' tab for details."); + } + } + finally + { + File.Delete(project1); + } + } + + /// + /// Properties and Targets that use non-standard separation chars + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void PropertiesWithSeparationChars() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + a%3BA + b;B + c;C + d%3BD + + + + + C=$(CValues)%3BD=$(DValues) + + + + + + + + + + "); + + string parentProjectContents = @" + + + + + + + + + + + "; + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile2) + }; + + Project p = ObjectModelHelpers.CreateInMemoryProject(parentProjectContents); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary targetOutputs; + bool success = pi.Build(null, null, null, out targetOutputs); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + Assert.IsTrue(targetOutputs.ContainsKey("Build")); + Assert.AreEqual(5, targetOutputs["Build"].Items.Length); + Assert.AreEqual("|a", targetOutputs["Build"].Items[0].ItemSpec); + Assert.AreEqual("A|b", targetOutputs["Build"].Items[1].ItemSpec); + Assert.AreEqual("B|c", targetOutputs["Build"].Items[2].ItemSpec); + Assert.AreEqual("C|d", targetOutputs["Build"].Items[3].ItemSpec); + Assert.AreEqual("D|", targetOutputs["Build"].Items[4].ItemSpec); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Orcas had a bug that if the target casing specified was not correct, we would still build it, + /// but not return any target outputs! + /// + [TestMethod] + public void TargetNameIsCaseInsensitive() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + "); + + try + { + Project project = new Project(projectFile2); + MockLogger logger = new MockLogger(); + + project.Build(logger); + + logger.AssertLogContains("[foo.out]"); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/MockHost.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/MockHost.cs new file mode 100644 index 00000000000..a3614e792b8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/MockHost.cs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A mock host used for unit testing +//----------------------------------------------------------------------- + +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.UnitTests.BackEnd; +using System; +using System.Threading; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using LegacyThreadingData = Microsoft.Build.Execution.LegacyThreadingData; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Mock host which is used duing tests which need a host object + /// + internal class MockHost : MockLoggingService, IBuildComponentHost, IBuildComponent + { + /// + /// Configuration cache + /// + private IConfigCache _configCache; + + /// + /// Logging service which will do the actual logging + /// + private ILoggingService _loggingService; + + /// + /// Request engine to process the build requests + /// + private IBuildRequestEngine _requestEngine; + + /// + /// Target Builder + /// + private ITargetBuilder _targetBuilder; + + /// + /// The build parameters. + /// + private BuildParameters _buildParameters; + + /// + /// Cache of requests + /// + private IResultsCache _resultsCache; + + /// + /// Builder which will do the actual building of the requests + /// + private IRequestBuilder _requestBuilder; + + /// + /// Holds the data for legacy threading semantics + /// + private LegacyThreadingData _legacyThreadingData; + + #region SystemParameterFields + + #endregion; + + /// + /// Constructor + /// + public MockHost() + : this(new BuildParameters()) + { + } + + /// + /// Constructor + /// + public MockHost(BuildParameters buildParameters) + { + _buildParameters = buildParameters; + + _buildParameters.ProjectRootElementCache = new ProjectRootElementCache(false); + + _configCache = new ConfigCache(); + ((IBuildComponent)_configCache).InitializeComponent(this); + + // We are a logging service + _loggingService = this; + + _legacyThreadingData = new LegacyThreadingData(); + + _requestEngine = new BuildRequestEngine(); + ((IBuildComponent)_requestEngine).InitializeComponent(this); + + _resultsCache = new ResultsCache(); + ((IBuildComponent)_resultsCache).InitializeComponent(this); + + _requestBuilder = new Microsoft.Build.UnitTests.BackEnd.BuildRequestEngine_Tests.MockRequestBuilder(); + ((IBuildComponent)_requestBuilder).InitializeComponent(this); + + _targetBuilder = new TestTargetBuilder(); + ((IBuildComponent)_targetBuilder).InitializeComponent(this); + } + + /// + /// Able to modify the loggingService this is required for testing + /// + public ILoggingService LoggingService + { + get { return _loggingService; } + internal set { _loggingService = value; } + } + + /// + /// Retrieves the name of the host. + /// + public string Name + { + get + { + return "BackEnd.MockHost"; + } + } + + /// + /// Retrieve the build parameters. + /// + /// + public BuildParameters BuildParameters + { + get + { + return _buildParameters; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + /// + /// Able to modify the request builder this is required for testing + /// + internal IRequestBuilder RequestBuilder + { + get { return _requestBuilder; } + set { _requestBuilder = value; } + } + + /// + /// Get the a component based on the request component type + /// + public IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.ConfigCache: + return (IBuildComponent)_configCache; + + case BuildComponentType.LoggingService: + return (IBuildComponent)_loggingService; + + case BuildComponentType.RequestEngine: + return (IBuildComponent)_requestEngine; + + case BuildComponentType.TargetBuilder: + return (IBuildComponent)_targetBuilder; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)_resultsCache; + + case BuildComponentType.RequestBuilder: + return (IBuildComponent)_requestBuilder; + + default: + throw new ArgumentException("Unexpected type " + type); + } + } + + /// + /// Register a new build component factory with the host. + /// + public void RegisterFactory(BuildComponentType type, BuildComponentFactoryDelegate factory) + { + throw new NotImplementedException(); + } + + #region INodePacketFactory Members + + /// + /// Deserialize a packet + /// + public INodePacket DeserializePacket(NodePacketType type, byte[] serializedPacket) + { + throw new NotImplementedException(); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Initialize this component using the provided host + /// + public void InitializeComponent(IBuildComponentHost host) + { + throw new NotImplementedException(); + } + + /// + /// Clean up any state + /// + public void ShutdownComponent() + { + throw new NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/MockLoggingService.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/MockLoggingService.cs new file mode 100644 index 00000000000..f3a9773ad0f --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/MockLoggingService.cs @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A mock implementation of ILoggingService used for testing. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// A class providing a mock implementation of ILoggingService. + /// + internal class MockLoggingService : ILoggingService + { + #region ILoggingService Members + + /// + /// The event to raise when there is a logging exception + /// + public event LoggingExceptionDelegate OnLoggingThreadException; + + /// + /// The event to raise when ProjectStarted is processed. + /// + public event ProjectStartedEventHandler OnProjectStarted; + + /// + /// The event to raise when ProjectFinished is processed + /// + public event ProjectFinishedEventHandler OnProjectFinished; + + /// + /// Enumerator over all registered loggers. + /// + public ICollection Loggers + { + get { throw new NotImplementedException(); } + } + + /// + /// The logging service state + /// + public LoggingServiceState ServiceState + { + get + { + OnLoggingThreadException(null); + OnProjectStarted(null, null); + OnProjectFinished(null, null); + throw new NotImplementedException(); + } + } + + /// + /// The logging mode. + /// + public LoggerMode LoggingMode + { + get + { + return LoggerMode.Synchronous; + } + } + + /// + /// Whether to log critical events + /// + public bool OnlyLogCriticalEvents + { + get + { + return false; + } + + set + { + } + } + + /// + /// Returns the number of initial nodes. + /// + public int MaxCPUCount + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + /// + /// Gets the logger descriptions + /// + public ICollection LoggerDescriptions + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Gets the registered logger type names. + /// + public ICollection RegisteredLoggerTypeNames + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Retrieves the registered sink names. + /// + public ICollection RegisteredSinkNames + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Properties to serialize from the child node to the parent node + /// + public string[] PropertiesToSerialize + { + get; + set; + } + + /// + /// Is the logging service on a remote node, this is used to determine if properties need to be serialized + /// + public bool RunningOnRemoteNode + { + get; + set; + } + + /// + /// Should all properties be serialized from the child to the parent process + /// + public bool SerializeAllProperties + { + get; + set; + } + + /// + /// Registers a distributed logger. + /// + /// The central logger, which resides on the build manager. + /// The forwarding logger, which resides on the node. + /// True if successful. + public bool RegisterDistributedLogger(ILogger centralLogger, LoggerDescription forwardingLogger) + { + throw new NotImplementedException(); + } + + /// + /// Registers a logger + /// + /// The logger + /// True if successful. + public bool RegisterLogger(ILogger logger) + { + throw new NotImplementedException(); + } + + /// + /// Clear out all registered loggers so that none are registered. + /// + public void UnregisterAllLoggers() + { + throw new NotImplementedException(); + } + + /// + /// Initializes the loggers on a node + /// + /// The descriptions received from the Build Manager + /// The sink used to transmit messages to the manager. + /// The id of the node. + public void InitializeNodeLoggers(ICollection loggerDescriptions, IBuildEventSink forwardingLoggerSink, int nodeId) + { + throw new NotImplementedException(); + } + + /// + /// Logs a comment based on a message resource + /// + /// The context + /// The importance + /// The resource for the message + /// The args for the message + public void LogComment(BuildEventContext buildEventContext, MessageImportance importance, string messageResourceName, params object[] messageArgs) + { + Console.WriteLine(messageResourceName); + foreach (object o in messageArgs) + { + Console.WriteLine((string)o); + } + } + + /// + /// Logs a text comment + /// + /// The context + /// The importance + /// The message + public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message) + { + Console.WriteLine(message); + } + + /// + /// Logs a pre-formed build event + /// + /// The event to log + public void LogBuildEvent(BuildEventArgs buildEvent) + { + } + + /// + /// Logs an error + /// + /// The event context + /// The file from which the error is logged + /// The message resource + /// The message args + public void LogError(BuildEventContext buildEventContext, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + Console.WriteLine(messageResourceName); + foreach (object o in messageArgs) + { + Console.WriteLine((string)o); + } + } + + /// + /// Logs an error with a subcategory + /// + /// The build event context + /// The subcategory resource + /// The file + /// The message resource + /// The message args + public void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + Console.WriteLine(messageResourceName); + foreach (object o in messageArgs) + { + Console.WriteLine((string)o); + } + } + + /// + /// Logs a text error + /// + /// The event context + /// The subcategory resource + /// The error code + /// A help keyword + /// The file + /// The message + public void LogErrorFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string errorCode, string helpKeyword, BuildEventFileInfo file, string message) + { + Console.WriteLine(message); + } + + /// + /// Logs an invalid project file error + /// + /// The event context + /// The exception + public void LogInvalidProjectFileError(BuildEventContext buildEventContext, InvalidProjectFileException invalidProjectFileException) + { + } + + /// + /// Logs a fatal build error + /// + /// The event context + /// The exception + /// The file + public void LogFatalBuildError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file) + { + } + + /// + /// Logs a fatal task error + /// + /// The event context + /// The exception + /// The file + /// The name of the task + public void LogFatalTaskError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) + { + } + + /// + /// Logs a generic fatal error + /// + /// The build context + /// The exception + /// The file + /// The message resource + /// The message args + public void LogFatalError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + } + + /// + /// Logs a task warning + /// + /// The build context + /// The exception + /// The file + /// The name of the task + public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) + { + } + + /// + /// Logs a warning + /// + /// The event context + /// The subcategory resource + /// The file + /// The message resource + /// The message args + public void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + Console.WriteLine(messageResourceName); + foreach (object o in messageArgs) + { + Console.WriteLine((string)o); + } + } + + /// + /// Logs a text warning + /// + /// The build context + /// The subcategory resource + /// The warning code + /// A help keyword + /// The file + /// The message + public void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message) + { + Console.WriteLine(message); + } + + /// + /// Logs a build started event + /// + public void LogBuildStarted() + { + } + + /// + /// Logs a build finished event + /// + /// Set to true if the build was successful + public void LogBuildFinished(bool success) + { + } + + /// + /// Logs a project started event + /// + public BuildEventContext LogProjectStarted(BuildEventContext nodeBuildEventContext, int submissionId, int projectId, BuildEventContext parentBuildEventContext, string projectFile, string targetNames, IEnumerable properties, IEnumerable items) + { + return new BuildEventContext(0, 0, 0, 0); + } + + /// + /// Logs a project finished event + /// + /// The project build event context + /// The project filename + /// Whether it was successful or not. + public void LogProjectFinished(BuildEventContext projectBuildEventContext, string projectFile, bool success) + { + } + + /// + /// Logs a target started event + /// + /// The build event context of the project + /// The name of the target + /// The project file + /// The project file containing the target element + /// The build event context for the target + public BuildEventContext LogTargetStarted(BuildEventContext projectBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, string parentTargetName) + { + return new BuildEventContext(0, 0, 0, 0); + } + + /// + /// Logs a target finished event + /// + /// The target's build event context + /// The name of the target + /// The project file + /// The project file containing the target element + /// Whether it was successful or not. + public void LogTargetFinished(BuildEventContext targetBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, bool success, IEnumerable targetOutputs) + { + } + + /// + /// Logs a task started event + /// + /// The target's build event context + /// The name of the task + /// The project file + /// The project file containing the task node. + public void LogTaskStarted(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode) + { + } + + /// + /// Logs a task started event + /// + /// The target's build event context + /// The name of the task + /// The project file + /// The project file containing the task node. + /// The task logging context + public BuildEventContext LogTaskStarted2(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode) + { + return new BuildEventContext(0, 0, 0, 0); + } + + /// + /// Logs a task finished event + /// + /// The task's build event context + /// The name of the task + /// The project file + /// The project file of the task node + /// Whether the task was successful or not. + public void LogTaskFinished(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, bool success) + { + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/MockTaskBuilder.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/MockTaskBuilder.cs new file mode 100644 index 00000000000..69f59b800d7 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/MockTaskBuilder.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A fake task builder used for testing other components. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext; +using System.Threading.Tasks; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// The mock task builder. + /// + internal class MockTaskBuilder : ITaskBuilder, IBuildComponent + { + /// + /// The component host. + /// + private IBuildComponentHost _host; + + /// + /// The current task number. + /// + private int _taskNumber; + + /// + /// Constructor. + /// + public MockTaskBuilder() + { + Reset(); + } + + /// + /// The list of tasks executed. + /// + public List ExecutedTasks + { + get; + set; + } + + /// + /// The list of OnError tasks. + /// + public List ErrorTasks + { + get; + set; + } + + /// + /// The task ordinal to fail on. + /// + public int FailTaskNumber + { + get; + set; + } + + /// + /// Resets the mock task builder to its clean state. + /// + public void Reset() + { + ErrorTasks = new List(); + ExecutedTasks = new List(); + FailTaskNumber = -1; + _taskNumber = 0; + } + + #region ITaskBuilder Members + + /// + /// Executes the task. + /// + public Task ExecuteTask(TargetLoggingContext targetLoggingContext, BuildRequestEntry requestEntry, ITargetBuilderCallback targetBuilderCallback, ProjectTargetInstanceChild task, TaskExecutionMode mode, Lookup lookupForInference, Lookup lookupForExecution, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromResult(new WorkUnitResult(WorkUnitResultCode.Canceled, WorkUnitActionCode.Stop, null)); + } + + ProjectOnErrorInstance errorTask = task as ProjectOnErrorInstance; + if (null != errorTask) + { + ErrorTasks.Add(errorTask); + } + else + { + ProjectTaskInstance taskInstance = task as ProjectTaskInstance; + ExecutedTasks.Add(taskInstance); + + if ((mode & TaskExecutionMode.InferOutputsOnly) == TaskExecutionMode.InferOutputsOnly) + { + lookupForInference.AddNewItem(new ProjectItemInstance(requestEntry.RequestConfiguration.Project, taskInstance.Name + "_Item", "Item", task.Location.File)); + } + else if ((mode & TaskExecutionMode.ExecuteTaskAndGatherOutputs) == TaskExecutionMode.ExecuteTaskAndGatherOutputs) + { + lookupForExecution.AddNewItem(new ProjectItemInstance(requestEntry.RequestConfiguration.Project, taskInstance.Name + "_Item", "Item", task.Location.File)); + } + + if (String.Equals(taskInstance.Name, "CallTarget", StringComparison.OrdinalIgnoreCase)) + { + taskInstance.GetParameter("Targets"); + char[] splitter = new char[] { ';' }; + targetBuilderCallback.LegacyCallTarget(taskInstance.GetParameter("Targets").Split(splitter), false, taskInstance.Location); + } + + _taskNumber++; + if (FailTaskNumber == _taskNumber) + { + if (taskInstance.ContinueOnError == "True") + { + return Task.FromResult(new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Continue, null)); + } + + return Task.FromResult(new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null)); + } + } + + return Task.FromResult(new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null)); + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host. + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + _host = host; + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + _host = null; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/NodeEndpointInProc_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/NodeEndpointInProc_Tests.cs new file mode 100644 index 00000000000..c7b57615ea8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/NodeEndpointInProc_Tests.cs @@ -0,0 +1,463 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using LegacyThreadingData = Microsoft.Build.Execution.LegacyThreadingData; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class NodeEndpointInProc_Tests + { + private delegate void EndpointOperationDelegate(NodeEndpointInProc endpoint); + + private class MockHost : IBuildComponentHost, INodePacketFactory + { + private DataReceivedContext _dataReceivedContext; + private AutoResetEvent _dataReceivedEvent; + private BuildParameters _buildParameters; + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + private LegacyThreadingData _legacyThreadingData; + + + public MockHost() + { + _buildParameters = new BuildParameters(); + _dataReceivedEvent = new AutoResetEvent(false); + _legacyThreadingData = new LegacyThreadingData(); + } + + public ILoggingService LoggingService + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + public string Name + { + get + { + return "NodeEndpointInProc_Tests.MockHost"; + } + } + + public BuildParameters BuildParameters + { + get + { + return _buildParameters; + } + } + + #region IBuildComponentHost Members + + public IBuildComponent GetComponent(BuildComponentType type) + { + throw new NotImplementedException(); + } + + public void RegisterFactory(BuildComponentType type, BuildComponentFactoryDelegate factory) + { + } + + #endregion + + #region INodePacketFactory Members + + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + throw new NotImplementedException(); + } + + public void UnregisterPacketHandler(NodePacketType packetType) + { + throw new NotImplementedException(); + } + + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + throw new NotImplementedException(); + } + + public void RoutePacket(int nodeId, INodePacket packet) + { + _dataReceivedContext = new DataReceivedContext(Thread.CurrentThread, packet); + _dataReceivedEvent.Set(); + } + + public DataReceivedContext DataReceivedContext + { + get { return _dataReceivedContext; } + } + + public WaitHandle DataReceivedEvent + { + get { return _dataReceivedEvent; } + } + + #endregion + } + private class TestPacket : INodePacket + { + #region INodePacket Members + + public NodePacketType Type + { + get { throw new NotImplementedException(); } + } + + public void Translate(INodePacketTranslator translator) + { + throw new NotImplementedException(); + } + + #endregion + } + + private struct LinkStatusContext + { + public readonly Thread thread; + public readonly LinkStatus status; + + public LinkStatusContext(Thread thread, LinkStatus status) + { + this.thread = thread; + this.status = status; + } + } + + private struct DataReceivedContext + { + public readonly Thread thread; + public readonly INodePacket packet; + + public DataReceivedContext(Thread thread, INodePacket packet) + { + this.thread = thread; + this.packet = packet; + } + } + + private Dictionary _linkStatusTable; + private MockHost _host; + + [TestMethod] + public void ConstructionWithValidHost() + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Synchronous, _host); + Assert.IsNotNull(endpoints); + + endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Asynchronous, _host); + Assert.IsNotNull(endpoints); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ConstructionSynchronousWithInvalidHost() + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Synchronous, null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ConstructionAsynchronousWithInvalidHost() + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Asynchronous, null); + } + + /// + /// Verify that the links: + /// 1. are marked inactive + /// 2. and that attempting to send data while they are + /// inactive throws the expected exception. + /// + [TestMethod] + public void InactiveLinkTestSynchronous() + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Synchronous, _host); + + CallOpOnEndpoints(endpoints, VerifyLinkInactive); + CallOpOnEndpoints(endpoints, VerifySendDataInvalidOperation); + CallOpOnEndpoints(endpoints, VerifyDisconnectInvalidOperation); + + // The following should not throw + endpoints.ManagerEndpoint.Listen(_host); + endpoints.NodeEndpoint.Connect(_host); + } + + /// + /// Verify that the links are marked inactive and that attempting to send data while they are + /// inactive throws the expected exception. + /// + [TestMethod] + public void InactiveLinkTestAsynchronous() + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Asynchronous, _host); + + CallOpOnEndpoints(endpoints, VerifyLinkInactive); + CallOpOnEndpoints(endpoints, VerifySendDataInvalidOperation); + CallOpOnEndpoints(endpoints, VerifyDisconnectInvalidOperation); + + // The following should not throw + endpoints.ManagerEndpoint.Listen(_host); + endpoints.NodeEndpoint.Connect(_host); + } + + [TestMethod] + public void ConnectionTestSynchronous() + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Synchronous, _host); + + endpoints.ManagerEndpoint.OnLinkStatusChanged += LinkStatusChanged; + endpoints.NodeEndpoint.OnLinkStatusChanged += LinkStatusChanged; + + // Call listen. This shouldn't have any effect on the link statuses. + endpoints.ManagerEndpoint.Listen(_host); + CallOpOnEndpoints(endpoints, VerifyLinkInactive); + // No link status callback should have occurred. + Assert.IsFalse(_linkStatusTable.ContainsKey(endpoints.NodeEndpoint)); + Assert.IsFalse(_linkStatusTable.ContainsKey(endpoints.ManagerEndpoint)); + + // Now call connect on the node side. This should activate the link on both ends. + endpoints.NodeEndpoint.Connect(_host); + CallOpOnEndpoints(endpoints, VerifyLinkActive); + + // We should have received callbacks informing us of the link change. + Assert.IsTrue(_linkStatusTable[endpoints.NodeEndpoint].status == LinkStatus.Active); + Assert.IsTrue(_linkStatusTable[endpoints.ManagerEndpoint].status == LinkStatus.Active); + } + + [TestMethod] + public void DisconnectionTestSynchronous() + { + DisconnectionTestHelper(NodeEndpointInProc.EndpointMode.Synchronous); + } + + [TestMethod] + public void DisconnectionTestAsynchronous() + { + DisconnectionTestHelper(NodeEndpointInProc.EndpointMode.Asynchronous); + } + + + [TestMethod] + public void SynchronousData() + { + // Create the endpoints + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Synchronous, _host); + + // Connect the endpoints + endpoints.ManagerEndpoint.Listen(_host); + endpoints.NodeEndpoint.Connect(_host); + + // Create our test packets + INodePacket managerPacket = new TestPacket(); + INodePacket nodePacket = new TestPacket(); + + // Send data from the manager. We expect to receive it from the node endpoint, and it should + // be on the same thread. + endpoints.ManagerEndpoint.SendData(managerPacket); + Assert.IsTrue(_host.DataReceivedContext.packet == managerPacket); + Assert.IsTrue(_host.DataReceivedContext.thread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId); + + // Send data from the node. We expect to receive it from the manager endpoint, and it should + // be on the same thread. + endpoints.NodeEndpoint.SendData(nodePacket); + Assert.IsTrue(_host.DataReceivedContext.packet == nodePacket); + Assert.IsTrue(_host.DataReceivedContext.thread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId); + } + + [TestMethod] + public void AsynchronousData() + { + // Create the endpoints + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints( + NodeEndpointInProc.EndpointMode.Asynchronous, _host); + + // Connect the endpoints + endpoints.ManagerEndpoint.Listen(_host); + endpoints.NodeEndpoint.Connect(_host); + + // Create our test packets + INodePacket managerPacket = new TestPacket(); + INodePacket nodePacket = new TestPacket(); + + // Send data from the manager. We expect to receive it from the node endpoint, and it should + // be on the same thread. + endpoints.ManagerEndpoint.SendData(managerPacket); + if (!_host.DataReceivedEvent.WaitOne(1000, false)) + { + Assert.Fail("Data not received before timeout expired."); + } + Assert.IsTrue(_host.DataReceivedContext.packet == managerPacket); + Assert.IsTrue(_host.DataReceivedContext.thread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId); + + // Send data from the node. We expect to receive it from the manager endpoint, and it should + // be on the same thread. + endpoints.NodeEndpoint.SendData(nodePacket); + if (!_host.DataReceivedEvent.WaitOne(1000, false)) + { + Assert.Fail("Data not received before timeout expired."); + } + Assert.IsTrue(_host.DataReceivedContext.packet == nodePacket); + Assert.IsTrue(_host.DataReceivedContext.thread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId); + } + + [TestInitialize] + public void SetUp() + { + _linkStatusTable = new Dictionary(); + _host = new MockHost(); + } + + [TestCleanup] + public void TearDown() + { + _linkStatusTable = null; + _host = null; + } + + + private void CallOpOnEndpoints(NodeEndpointInProc.EndpointPair pair, EndpointOperationDelegate opDelegate) + { + opDelegate(pair.NodeEndpoint); + opDelegate(pair.ManagerEndpoint); + } + + private void VerifyLinkInactive(NodeEndpointInProc endpoint) + { + Assert.IsTrue(endpoint.LinkStatus == LinkStatus.Inactive, "Expected LinkStatus to be Inactive"); + } + + private void VerifyLinkActive(NodeEndpointInProc endpoint) + { + Assert.IsTrue(endpoint.LinkStatus == LinkStatus.Active, "Expected LinkStatus to be Active"); + } + + private void VerifySendDataInvalidOperation(NodeEndpointInProc endpoint) + { + bool caught = false; + try + { + endpoint.SendData(new TestPacket()); + } + catch (InternalErrorException) + { + caught = true; + } + + Assert.IsTrue(caught, "Did not receive InternalErrorException."); + } + + private void VerifyDisconnectInvalidOperation(NodeEndpointInProc endpoint) + { + bool caught = false; + try + { + endpoint.Disconnect(); + } + catch (InternalErrorException) + { + caught = true; + } + Assert.IsTrue(caught, "Did not receive InternalErrorException."); + } + + private void VerifyListenCallSuccess(NodeEndpointInProc endpoint) + { + endpoint.Listen(_host); + } + + private void VerifyConnectCallSuccess(NodeEndpointInProc endpoint) + { + endpoint.Connect(_host); + Assert.IsTrue(endpoint.LinkStatus == LinkStatus.Active); + } + + private void DisconnectionTestHelper(NodeEndpointInProc.EndpointMode mode) + { + NodeEndpointInProc.EndpointPair endpoints = SetupConnection(mode); + endpoints.ManagerEndpoint.Disconnect(); + VerifyLinksAndCallbacksInactive(endpoints); + + endpoints = SetupConnection(mode); + endpoints.NodeEndpoint.Disconnect(); + VerifyLinksAndCallbacksInactive(endpoints); + } + + private void VerifyLinksAndCallbacksInactive(NodeEndpointInProc.EndpointPair endpoints) + { + CallOpOnEndpoints(endpoints, VerifyLinkInactive); + Assert.IsTrue(_linkStatusTable[endpoints.NodeEndpoint].status == LinkStatus.Inactive); + Assert.IsTrue(_linkStatusTable[endpoints.ManagerEndpoint].status == LinkStatus.Inactive); + } + + private NodeEndpointInProc.EndpointPair SetupConnection(NodeEndpointInProc.EndpointMode mode) + { + NodeEndpointInProc.EndpointPair endpoints = + NodeEndpointInProc.CreateInProcEndpoints(mode, _host); + + endpoints.ManagerEndpoint.OnLinkStatusChanged += LinkStatusChanged; + endpoints.NodeEndpoint.OnLinkStatusChanged += LinkStatusChanged; + + // Call listen. This shouldn't have any effect on the link statuses. + endpoints.ManagerEndpoint.Listen(_host); + endpoints.NodeEndpoint.Connect(_host); + + return endpoints; + } + + private void LinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + lock (_linkStatusTable) + { + _linkStatusTable[endpoint] = new LinkStatusContext(Thread.CurrentThread, status); + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/NodePacketTranslator_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/NodePacketTranslator_Tests.cs new file mode 100644 index 00000000000..5242b8f9123 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/NodePacketTranslator_Tests.cs @@ -0,0 +1,705 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the NodePacketTranslator class. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using System.IO; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for the NodePacketTranslators + /// + [TestClass] + public class NodePacketTranslator_Tests + { + /// + /// Tests the SerializationMode property + /// + [TestMethod] + public void TestSerializationMode() + { + MemoryStream stream = new MemoryStream(); + INodePacketTranslator translator = NodePacketTranslator.GetReadTranslator(stream, null); + Assert.AreEqual(TranslationDirection.ReadFromStream, translator.Mode); + + translator = NodePacketTranslator.GetWriteTranslator(stream); + Assert.AreEqual(TranslationDirection.WriteToStream, translator.Mode); + } + + /// + /// Tests serializing bools. + /// + [TestMethod] + public void TestSerializeBool() + { + HelperTestSimpleType(false, true); + HelperTestSimpleType(true, false); + } + + /// + /// Tests serializing bytes. + /// + [TestMethod] + public void TestSerializeByte() + { + byte val = 0x55; + HelperTestSimpleType((byte)0, val); + HelperTestSimpleType(val, (byte)0); + } + + /// + /// Tests serializing shorts. + /// + [TestMethod] + public void TestSerializeShort() + { + short val = 0x55AA; + HelperTestSimpleType((short)0, val); + HelperTestSimpleType(val, (short)0); + } + + /// + /// Tests serializing ints. + /// + [TestMethod] + public void TestSerializeInt() + { + int val = 0x55AA55AA; + HelperTestSimpleType((int)0, val); + HelperTestSimpleType(val, (int)0); + } + + /// + /// Tests serializing strings. + /// + [TestMethod] + public void TestSerializeString() + { + HelperTestSimpleType("foo", null); + HelperTestSimpleType("", null); + HelperTestSimpleType(null, null); + } + + /// + /// Tests serializing string arrays. + /// + [TestMethod] + public void TestSerializeStringArray() + { + HelperTestArray(new string[] { }, StringComparer.Ordinal); + HelperTestArray(new string[] { "foo", "bar" }, StringComparer.Ordinal); + HelperTestArray(null, StringComparer.Ordinal); + } + + /// + /// Tests serializing string arrays. + /// + [TestMethod] + public void TestSerializeStringList() + { + HelperTestList(new List(), StringComparer.Ordinal); + List twoItems = new List(2); + twoItems.Add("foo"); + twoItems.Add("bar"); + HelperTestList(twoItems, StringComparer.Ordinal); + HelperTestList(null, StringComparer.Ordinal); + } + + /// + /// Tests serializing DateTimes. + /// + [TestMethod] + public void TestSerializeDateTime() + { + HelperTestSimpleType(new DateTime(), DateTime.Now); + HelperTestSimpleType(DateTime.Now, new DateTime()); + } + + /// + /// Tests serializing enums. + /// + public void TestSerializeEnum() + { + TranslationDirection value = TranslationDirection.ReadFromStream; + TranslationHelpers.GetWriteTranslator().TranslateEnum(ref value, (int)value); + + TranslationDirection deserializedValue = TranslationDirection.WriteToStream; + TranslationHelpers.GetReadTranslator().TranslateEnum(ref deserializedValue, (int)deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Tests serializing using the DotNet serializer. + /// + [TestMethod] + public void TestSerializeDotNet() + { + ArgumentNullException value = new ArgumentNullException("The argument was null", new InsufficientMemoryException()); + TranslationHelpers.GetWriteTranslator().TranslateDotNet(ref value); + + ArgumentNullException deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDotNet(ref deserializedValue); + + Assert.IsTrue(TranslationHelpers.CompareExceptions(value, deserializedValue)); + } + + /// + /// Tests serializing using the DotNet serializer passing in null. + /// + [TestMethod] + public void TestSerializeDotNetNull() + { + ArgumentNullException value = null; + TranslationHelpers.GetWriteTranslator().TranslateDotNet(ref value); + + ArgumentNullException deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDotNet(ref deserializedValue); + + Assert.IsTrue(TranslationHelpers.CompareExceptions(value, deserializedValue)); + } + + /// + /// Tests serializing an object with a default constructor. + /// + [TestMethod] + public void TestSerializeINodePacketSerializable() + { + DerivedClass value = new DerivedClass(1, 2); + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + DerivedClass deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value.BaseValue, deserializedValue.BaseValue); + Assert.AreEqual(value.DerivedValue, deserializedValue.DerivedValue); + } + + /// + /// Tests serializing an object with a default constructor passed as null. + /// + [TestMethod] + public void TestSerializeINodePacketSerializableNull() + { + DerivedClass value = null; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + DerivedClass deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Tests serializing an object requiring a factory to construct. + /// + [TestMethod] + public void TestSerializeWithFactory() + { + BaseClass value = new BaseClass(1); + TranslationHelpers.GetWriteTranslator().Translate(ref value, BaseClass.FactoryForDeserialization); + + BaseClass deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue, BaseClass.FactoryForDeserialization); + + Assert.AreEqual(value.BaseValue, deserializedValue.BaseValue); + } + + /// + /// Tests serializing an object requiring a factory to construct, passing null for the value. + /// + [TestMethod] + public void TestSerializeWithFactoryNull() + { + BaseClass value = null; + TranslationHelpers.GetWriteTranslator().Translate(ref value, BaseClass.FactoryForDeserialization); + + BaseClass deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue, BaseClass.FactoryForDeserialization); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Tests serializing an array of objects with default constructors. + /// + [TestMethod] + public void TestSerializeArray() + { + DerivedClass[] value = new DerivedClass[] { new DerivedClass(1, 2), new DerivedClass(3, 4) }; + TranslationHelpers.GetWriteTranslator().TranslateArray(ref value); + + DerivedClass[] deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateArray(ref deserializedValue); + + Assert.IsTrue(TranslationHelpers.CompareCollections(value, deserializedValue, DerivedClass.Comparer)); + } + + /// + /// Tests serializing an array of objects with default constructors, passing null for the array. + /// + [TestMethod] + public void TestSerializeArrayNull() + { + DerivedClass[] value = null; + TranslationHelpers.GetWriteTranslator().TranslateArray(ref value); + + DerivedClass[] deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateArray(ref deserializedValue); + + Assert.IsTrue(TranslationHelpers.CompareCollections(value, deserializedValue, DerivedClass.Comparer)); + } + + /// + /// Tests serializing an array of objects requiring factories to construct. + /// + [TestMethod] + public void TestSerializeArrayWithFactory() + { + BaseClass[] value = new BaseClass[] { new BaseClass(1), new BaseClass(2) }; + TranslationHelpers.GetWriteTranslator().TranslateArray(ref value, BaseClass.FactoryForDeserialization); + + BaseClass[] deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateArray(ref deserializedValue, BaseClass.FactoryForDeserialization); + + Assert.IsTrue(TranslationHelpers.CompareCollections(value, deserializedValue, BaseClass.Comparer)); + } + + /// + /// Tests serializing an array of objects requiring factories to construct, passing null for the array. + /// + [TestMethod] + public void TestSerializeArrayWithFactoryNull() + { + BaseClass[] value = null; + TranslationHelpers.GetWriteTranslator().TranslateArray(ref value, BaseClass.FactoryForDeserialization); + + BaseClass[] deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateArray(ref deserializedValue, BaseClass.FactoryForDeserialization); + + Assert.IsTrue(TranslationHelpers.CompareCollections(value, deserializedValue, BaseClass.Comparer)); + } + + /// + /// Tests serializing a dictionary of { string, string } + /// + [TestMethod] + public void TestSerializeDictionaryStringString() + { + Dictionary value = new Dictionary(StringComparer.OrdinalIgnoreCase); + value["foo"] = "bar"; + value["alpha"] = "omega"; + + TranslationHelpers.GetWriteTranslator().TranslateDictionary(ref value, StringComparer.OrdinalIgnoreCase); + + Dictionary deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary(ref deserializedValue, StringComparer.OrdinalIgnoreCase); + + Assert.AreEqual(value.Count, deserializedValue.Count); + Assert.AreEqual(value["foo"], deserializedValue["foo"]); + Assert.AreEqual(value["alpha"], deserializedValue["alpha"]); + Assert.AreEqual(value["FOO"], deserializedValue["FOO"]); + } + + /// + /// Tests serializing a dictionary of { string, string }, passing null. + /// + [TestMethod] + public void TestSerializeDictionaryStringStringNull() + { + Dictionary value = null; + + TranslationHelpers.GetWriteTranslator().TranslateDictionary(ref value, StringComparer.OrdinalIgnoreCase); + + Dictionary deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary(ref deserializedValue, StringComparer.OrdinalIgnoreCase); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Tests serializing a dictionary of { string, T } where T requires a factory to construct and the dictionary + /// requires a KeyComparer initializer. + /// + [TestMethod] + public void TestSerializeDictionaryStringT() + { + Dictionary value = new Dictionary(StringComparer.OrdinalIgnoreCase); + value["foo"] = new BaseClass(1); + value["alpha"] = new BaseClass(2); + + TranslationHelpers.GetWriteTranslator().TranslateDictionary(ref value, StringComparer.OrdinalIgnoreCase, BaseClass.FactoryForDeserialization); + + Dictionary deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary(ref deserializedValue, StringComparer.OrdinalIgnoreCase, BaseClass.FactoryForDeserialization); + + Assert.AreEqual(value.Count, deserializedValue.Count); + Assert.AreEqual(BaseClass.Comparer.Compare(value["foo"], deserializedValue["foo"]), 0); + Assert.AreEqual(BaseClass.Comparer.Compare(value["alpha"], deserializedValue["alpha"]), 0); + Assert.AreEqual(BaseClass.Comparer.Compare(value["FOO"], deserializedValue["FOO"]), 0); + } + + /// + /// Tests serializing a dictionary of { string, T } where T requires a factory to construct and the dictionary + /// requires a KeyComparer initializer, passing null for the dictionary. + /// + [TestMethod] + public void TestSerializeDictionaryStringTNull() + { + Dictionary value = null; + + TranslationHelpers.GetWriteTranslator().TranslateDictionary(ref value, StringComparer.OrdinalIgnoreCase, BaseClass.FactoryForDeserialization); + + Dictionary deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary(ref deserializedValue, StringComparer.OrdinalIgnoreCase, BaseClass.FactoryForDeserialization); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Tests serializing a dictionary of { string, T } where T requires a factory to construct and the dictionary + /// has a default constructor. + /// + [TestMethod] + public void TestSerializeDictionaryStringTNoComparer() + { + Dictionary value = new Dictionary(); + value["foo"] = new BaseClass(1); + value["alpha"] = new BaseClass(2); + + TranslationHelpers.GetWriteTranslator().TranslateDictionary, BaseClass>(ref value, BaseClass.FactoryForDeserialization); + + Dictionary deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary, BaseClass>(ref deserializedValue, BaseClass.FactoryForDeserialization); + + Assert.AreEqual(value.Count, deserializedValue.Count); + Assert.AreEqual(BaseClass.Comparer.Compare(value["foo"], deserializedValue["foo"]), 0); + Assert.AreEqual(BaseClass.Comparer.Compare(value["alpha"], deserializedValue["alpha"]), 0); + Assert.IsFalse(deserializedValue.ContainsKey("FOO")); + } + + /// + /// Tests serializing a dictionary of { string, T } where T requires a factory to construct and the dictionary + /// has a default constructor, passing null for the dictionary. + /// + [TestMethod] + public void TestSerializeDictionaryStringTNoComparerNull() + { + Dictionary value = null; + + TranslationHelpers.GetWriteTranslator().TranslateDictionary, BaseClass>(ref value, BaseClass.FactoryForDeserialization); + + Dictionary deserializedValue = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary, BaseClass>(ref deserializedValue, BaseClass.FactoryForDeserialization); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for bool serialization. + /// + private void HelperTestSimpleType(bool initialValue, bool deserializedInitialValue) + { + bool value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + bool deserializedValue = deserializedInitialValue; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for byte serialization. + /// + private void HelperTestSimpleType(byte initialValue, byte deserializedInitialValue) + { + byte value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + byte deserializedValue = deserializedInitialValue; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for short serialization. + /// + private void HelperTestSimpleType(short initialValue, short deserializedInitialValue) + { + short value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + short deserializedValue = deserializedInitialValue; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for int serialization. + /// + private void HelperTestSimpleType(int initialValue, int deserializedInitialValue) + { + int value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + int deserializedValue = deserializedInitialValue; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for string serialization. + /// + private void HelperTestSimpleType(string initialValue, string deserializedInitialValue) + { + string value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + string deserializedValue = deserializedInitialValue; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for DateTime serialization. + /// + private void HelperTestSimpleType(DateTime initialValue, DateTime deserializedInitialValue) + { + DateTime value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + DateTime deserializedValue = deserializedInitialValue; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.AreEqual(value, deserializedValue); + } + + /// + /// Helper for array serialization. + /// + private void HelperTestArray(string[] initialValue, IComparer comparer) + { + string[] value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + string[] deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.IsTrue(TranslationHelpers.CompareCollections(value, deserializedValue, comparer)); + } + + /// + /// Helper for list serialization. + /// + private void HelperTestList(List initialValue, IComparer comparer) + { + List value = initialValue; + TranslationHelpers.GetWriteTranslator().Translate(ref value); + + List deserializedValue = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedValue); + + Assert.IsTrue(TranslationHelpers.CompareCollections(value, deserializedValue, comparer)); + } + + /// + /// Base class for testing + /// + private class BaseClass : INodePacketTranslatable + { + /// + /// A field. + /// + private int _baseValue; + + /// + /// Constructor with value. + /// + public BaseClass(int val) + { + _baseValue = val; + } + + /// + /// Constructor + /// + protected BaseClass() + { + } + + /// + /// Gets a comparer. + /// + static public IComparer Comparer + { + get { return new BaseClassComparer(); } + } + + /// + /// Gets the value. + /// + public int BaseValue + { + get { return _baseValue; } + } + + #region INodePacketTranslatable Members + + /// + /// Factory for serialization. + /// + public static BaseClass FactoryForDeserialization(INodePacketTranslator translator) + { + BaseClass packet = new BaseClass(); + packet.Translate(translator); + return packet; + } + + /// + /// Serializes the class. + /// + public virtual void Translate(INodePacketTranslator translator) + { + translator.Translate(ref _baseValue); + } + + #endregion + + /// + /// Comparer for BaseClass. + /// + private class BaseClassComparer : IComparer + { + /// + /// Constructor. + /// + public BaseClassComparer() + { + } + + #region IComparer Members + + /// + /// Compare two BaseClass objects. + /// + public int Compare(BaseClass x, BaseClass y) + { + if (x._baseValue == y._baseValue) + { + return 0; + } + + return -1; + } + #endregion + } + } + + /// + /// Derived class for testing. + /// + private class DerivedClass : BaseClass + { + /// + /// A field. + /// + private int _derivedValue; + + /// + /// Default constructor. + /// + public DerivedClass() + { + } + + /// + /// Constructor taking two values. + /// + public DerivedClass(int derivedValue, int baseValue) + : base(baseValue) + { + _derivedValue = derivedValue; + } + + /// + /// Gets a comparer. + /// + static new public IComparer Comparer + { + get { return new DerivedClassComparer(); } + } + + /// + /// Returns the value. + /// + public int DerivedValue + { + get { return _derivedValue; } + } + + #region INodePacketTranslatable Members + + /// + /// Serializes the class. + /// + public override void Translate(INodePacketTranslator translator) + { + base.Translate(translator); + translator.Translate(ref _derivedValue); + } + + #endregion + + /// + /// Comparer for DerivedClass. + /// + private class DerivedClassComparer : IComparer + { + /// + /// Constructor + /// + public DerivedClassComparer() + { + } + + #region IComparer Members + + /// + /// Compares two DerivedClass objects. + /// + public int Compare(DerivedClass x, DerivedClass y) + { + if (x._derivedValue == y._derivedValue) + { + return BaseClass.Comparer.Compare(x, y); + } + + return -1; + } + #endregion + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/NodePackets_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/NodePackets_Tests.cs new file mode 100644 index 00000000000..e27bb2edcdc --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/NodePackets_Tests.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Test the node packets are created properly +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using System.Collections.Generic; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Each packet is split up into a region, the region contains the tests for + /// a given packet type. + /// + [TestClass] + public class NodePackets_Tests + { + #region LogMessagePacket Tests + + /// + /// Verify a null build event throws an exception + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void LogMessageConstructorNullBuildEvent() + { + LogMessagePacket packet = new LogMessagePacket(null); + } + + /// + /// Verify when creating a LogMessagePacket + /// that the correct Event Type is set. + /// + [TestMethod] + public void VerifyEventType() + { + BuildFinishedEventArgs buildFinished = new BuildFinishedEventArgs("Message", "Keyword", true); + BuildStartedEventArgs buildStarted = new BuildStartedEventArgs("Message", "Help"); + BuildMessageEventArgs lowMessage = new BuildMessageEventArgs("Message", "help", "sender", MessageImportance.Low); + TaskStartedEventArgs taskStarted = new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"); + TaskFinishedEventArgs taskFinished = new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true); + TaskCommandLineEventArgs commandLine = new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low); + BuildWarningEventArgs warning = new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + BuildErrorEventArgs error = new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + TargetStartedEventArgs targetStarted = new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"); + TargetFinishedEventArgs targetFinished = new TargetFinishedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile", true); + ProjectStartedEventArgs projectStarted = new ProjectStartedEventArgs(-1, "message", "help", "ProjectFile", "targetNames", null, null, null); + ProjectFinishedEventArgs projectFinished = new ProjectFinishedEventArgs("message", "help", "ProjectFile", true); + ExternalProjectStartedEventArgs externalStartedEvent = new ExternalProjectStartedEventArgs("message", "help", "senderName", "projectFile", "targetNames"); + + VerifyLoggingPacket(buildFinished, LoggingEventType.BuildFinishedEvent); + VerifyLoggingPacket(buildStarted, LoggingEventType.BuildStartedEvent); + VerifyLoggingPacket(lowMessage, LoggingEventType.BuildMessageEvent); + VerifyLoggingPacket(taskStarted, LoggingEventType.TaskStartedEvent); + VerifyLoggingPacket(taskFinished, LoggingEventType.TaskFinishedEvent); + VerifyLoggingPacket(commandLine, LoggingEventType.TaskCommandLineEvent); + VerifyLoggingPacket(warning, LoggingEventType.BuildWarningEvent); + VerifyLoggingPacket(error, LoggingEventType.BuildErrorEvent); + VerifyLoggingPacket(targetStarted, LoggingEventType.TargetStartedEvent); + VerifyLoggingPacket(targetFinished, LoggingEventType.TargetFinishedEvent); + VerifyLoggingPacket(projectStarted, LoggingEventType.ProjectStartedEvent); + VerifyLoggingPacket(projectFinished, LoggingEventType.ProjectFinishedEvent); + VerifyLoggingPacket(externalStartedEvent, LoggingEventType.CustomEvent); + } + + /// + /// Tests serialization of LogMessagePacket with each kind of event type. + /// + [TestMethod] + public void TestTranslation() + { + TaskItem item = new TaskItem("Hello", "my.proj"); + List targetOutputs = new List(); + targetOutputs.Add(item); + + Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", "1"); + BuildEventArgs[] testArgs = new BuildEventArgs[] + { + new BuildFinishedEventArgs("Message", "Keyword", true), + new BuildStartedEventArgs("Message", "Help"), + new BuildMessageEventArgs("Message", "help", "sender", MessageImportance.Low), + new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"), + new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true), + new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low), + new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"), + new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"), + new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"), + new TargetFinishedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile", true, targetOutputs), + new ProjectStartedEventArgs(-1, "message", "help", "ProjectFile", "targetNames", null, null, null), + new ProjectFinishedEventArgs("message", "help", "ProjectFile", true), + new ExternalProjectStartedEventArgs("message", "help", "senderName", "projectFile", "targetNames") + }; + + foreach (BuildEventArgs arg in testArgs) + { + LogMessagePacket packet = new LogMessagePacket(new KeyValuePair(0, arg)); + + ((INodePacketTranslatable)packet).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket tempPacket = LogMessagePacket.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()) as LogMessagePacket; + + LogMessagePacket deserializedPacket = tempPacket as LogMessagePacket; + + CompareLogMessagePackets(packet, deserializedPacket); + } + + Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", null); + } + + /// + /// Verify the LoggingMessagePacket is properly created from a build event. + /// This includes the packet type and the event type depending on which build event arg is passed in. + /// + /// Build event to put into a packet, and verify after packet creation + /// What is the expected logging event type + private static void VerifyLoggingPacket(BuildEventArgs buildEvent, LoggingEventType logEventType) + { + LogMessagePacket packet = new LogMessagePacket(new KeyValuePair(0, buildEvent)); + Assert.AreEqual(logEventType, packet.EventType); + Assert.AreEqual(NodePacketType.LogMessage, packet.Type); + Assert.IsTrue(Object.ReferenceEquals(buildEvent, packet.NodeBuildEvent.Value.Value), "Expected buildEvent to have the same object reference as packet.BuildEvent"); + } + + /// + /// Compares two BuildEventArgs objects for equivalence. + /// + private void CompareNodeBuildEventArgs(KeyValuePair leftTuple, KeyValuePair rightTuple, bool expectInvalidBuildEventContext) + { + BuildEventArgs left = leftTuple.Value; + BuildEventArgs right = rightTuple.Value; + + if (expectInvalidBuildEventContext) + { + Assert.AreEqual(BuildEventContext.Invalid, right.BuildEventContext); + } + else + { + Assert.AreEqual(left.BuildEventContext, right.BuildEventContext); + } + + Assert.AreEqual(leftTuple.Key, rightTuple.Key); + Assert.AreEqual(left.HelpKeyword, right.HelpKeyword); + Assert.AreEqual(left.Message, right.Message); + Assert.AreEqual(left.SenderName, right.SenderName); + Assert.AreEqual(left.ThreadId, right.ThreadId); + Assert.AreEqual(left.Timestamp, right.Timestamp); + } + + /// + /// Compares two LogMessagePacket objects for equivalence. + /// + private void CompareLogMessagePackets(LogMessagePacket left, LogMessagePacket right) + { + Assert.AreEqual(left.EventType, right.EventType); + Assert.AreEqual(left.NodeBuildEvent.Value.Value.GetType(), right.NodeBuildEvent.Value.Value.GetType()); + + CompareNodeBuildEventArgs(left.NodeBuildEvent.Value, right.NodeBuildEvent.Value, left.EventType == LoggingEventType.CustomEvent /* expectInvalidBuildEventContext */); + + switch (left.EventType) + { + case LoggingEventType.BuildErrorEvent: + BuildErrorEventArgs leftError = left.NodeBuildEvent.Value.Value as BuildErrorEventArgs; + BuildErrorEventArgs rightError = right.NodeBuildEvent.Value.Value as BuildErrorEventArgs; + Assert.IsNotNull(leftError); + Assert.IsNotNull(rightError); + Assert.AreEqual(leftError.Code, rightError.Code); + Assert.AreEqual(leftError.ColumnNumber, rightError.ColumnNumber); + Assert.AreEqual(leftError.EndColumnNumber, rightError.EndColumnNumber); + Assert.AreEqual(leftError.EndLineNumber, rightError.EndLineNumber); + Assert.AreEqual(leftError.File, rightError.File); + Assert.AreEqual(leftError.LineNumber, rightError.LineNumber); + Assert.AreEqual(leftError.Message, rightError.Message); + Assert.AreEqual(leftError.Subcategory, rightError.Subcategory); + break; + + case LoggingEventType.BuildFinishedEvent: + BuildFinishedEventArgs leftFinished = left.NodeBuildEvent.Value.Value as BuildFinishedEventArgs; + BuildFinishedEventArgs rightFinished = right.NodeBuildEvent.Value.Value as BuildFinishedEventArgs; + Assert.IsNotNull(leftFinished); + Assert.IsNotNull(rightFinished); + Assert.AreEqual(leftFinished.Succeeded, rightFinished.Succeeded); + break; + + case LoggingEventType.BuildMessageEvent: + BuildMessageEventArgs leftMessage = left.NodeBuildEvent.Value.Value as BuildMessageEventArgs; + BuildMessageEventArgs rightMessage = right.NodeBuildEvent.Value.Value as BuildMessageEventArgs; + Assert.IsNotNull(leftMessage); + Assert.IsNotNull(rightMessage); + Assert.AreEqual(leftMessage.Importance, rightMessage.Importance); + break; + + case LoggingEventType.BuildStartedEvent: + BuildStartedEventArgs leftBuildStart = left.NodeBuildEvent.Value.Value as BuildStartedEventArgs; + BuildStartedEventArgs rightBuildStart = right.NodeBuildEvent.Value.Value as BuildStartedEventArgs; + Assert.IsNotNull(leftBuildStart); + Assert.IsNotNull(rightBuildStart); + break; + + case LoggingEventType.BuildWarningEvent: + BuildWarningEventArgs leftBuildWarn = left.NodeBuildEvent.Value.Value as BuildWarningEventArgs; + BuildWarningEventArgs rightBuildWarn = right.NodeBuildEvent.Value.Value as BuildWarningEventArgs; + Assert.IsNotNull(leftBuildWarn); + Assert.IsNotNull(rightBuildWarn); + Assert.AreEqual(leftBuildWarn.Code, rightBuildWarn.Code); + Assert.AreEqual(leftBuildWarn.ColumnNumber, rightBuildWarn.ColumnNumber); + Assert.AreEqual(leftBuildWarn.EndColumnNumber, rightBuildWarn.EndColumnNumber); + Assert.AreEqual(leftBuildWarn.EndLineNumber, rightBuildWarn.EndLineNumber); + Assert.AreEqual(leftBuildWarn.File, rightBuildWarn.File); + Assert.AreEqual(leftBuildWarn.LineNumber, rightBuildWarn.LineNumber); + Assert.AreEqual(leftBuildWarn.Subcategory, rightBuildWarn.Subcategory); + break; + + case LoggingEventType.CustomEvent: + ExternalProjectStartedEventArgs leftCustom = left.NodeBuildEvent.Value.Value as ExternalProjectStartedEventArgs; + ExternalProjectStartedEventArgs rightCustom = right.NodeBuildEvent.Value.Value as ExternalProjectStartedEventArgs; + Assert.IsNotNull(leftCustom); + Assert.IsNotNull(rightCustom); + Assert.AreEqual(leftCustom.ProjectFile, rightCustom.ProjectFile); + Assert.AreEqual(leftCustom.TargetNames, rightCustom.TargetNames); + break; + + case LoggingEventType.ProjectFinishedEvent: + ProjectFinishedEventArgs leftProjectFinished = left.NodeBuildEvent.Value.Value as ProjectFinishedEventArgs; + ProjectFinishedEventArgs rightProjectFinished = right.NodeBuildEvent.Value.Value as ProjectFinishedEventArgs; + Assert.IsNotNull(leftProjectFinished); + Assert.IsNotNull(rightProjectFinished); + Assert.AreEqual(leftProjectFinished.ProjectFile, rightProjectFinished.ProjectFile); + Assert.AreEqual(leftProjectFinished.Succeeded, rightProjectFinished.Succeeded); + break; + + case LoggingEventType.ProjectStartedEvent: + ProjectStartedEventArgs leftProjectStarted = left.NodeBuildEvent.Value.Value as ProjectStartedEventArgs; + ProjectStartedEventArgs rightProjectStarted = right.NodeBuildEvent.Value.Value as ProjectStartedEventArgs; + Assert.IsNotNull(leftProjectStarted); + Assert.IsNotNull(rightProjectStarted); + Assert.AreEqual(leftProjectStarted.ParentProjectBuildEventContext, rightProjectStarted.ParentProjectBuildEventContext); + Assert.AreEqual(leftProjectStarted.ProjectFile, rightProjectStarted.ProjectFile); + Assert.AreEqual(leftProjectStarted.ProjectId, rightProjectStarted.ProjectId); + Assert.AreEqual(leftProjectStarted.TargetNames, rightProjectStarted.TargetNames); + + // UNDONE: (Serialization.) We don't actually serialize the items at this time. + // Assert.AreEqual(leftProjectStarted.Items, rightProjectStarted.Items); + // UNDONE: (Serialization.) We don't actually serialize properties at this time. + // Assert.AreEqual(leftProjectStarted.Properties, rightProjectStarted.Properties); + break; + + case LoggingEventType.TargetFinishedEvent: + TargetFinishedEventArgs leftTargetFinished = left.NodeBuildEvent.Value.Value as TargetFinishedEventArgs; + TargetFinishedEventArgs rightTargetFinished = right.NodeBuildEvent.Value.Value as TargetFinishedEventArgs; + Assert.IsNotNull(leftTargetFinished); + Assert.IsNotNull(rightTargetFinished); + Assert.AreEqual(leftTargetFinished.ProjectFile, rightTargetFinished.ProjectFile); + Assert.AreEqual(leftTargetFinished.Succeeded, rightTargetFinished.Succeeded); + Assert.AreEqual(leftTargetFinished.TargetFile, rightTargetFinished.TargetFile); + Assert.AreEqual(leftTargetFinished.TargetName, rightTargetFinished.TargetName); + break; + + case LoggingEventType.TargetStartedEvent: + TargetStartedEventArgs leftTargetStarted = left.NodeBuildEvent.Value.Value as TargetStartedEventArgs; + TargetStartedEventArgs rightTargetStarted = right.NodeBuildEvent.Value.Value as TargetStartedEventArgs; + Assert.IsNotNull(leftTargetStarted); + Assert.IsNotNull(rightTargetStarted); + Assert.AreEqual(leftTargetStarted.ProjectFile, rightTargetStarted.ProjectFile); + Assert.AreEqual(leftTargetStarted.TargetFile, rightTargetStarted.TargetFile); + Assert.AreEqual(leftTargetStarted.TargetName, rightTargetStarted.TargetName); + break; + + case LoggingEventType.TaskCommandLineEvent: + TaskCommandLineEventArgs leftCommand = left.NodeBuildEvent.Value.Value as TaskCommandLineEventArgs; + TaskCommandLineEventArgs rightCommand = right.NodeBuildEvent.Value.Value as TaskCommandLineEventArgs; + Assert.IsNotNull(leftCommand); + Assert.IsNotNull(rightCommand); + Assert.AreEqual(leftCommand.CommandLine, rightCommand.CommandLine); + Assert.AreEqual(leftCommand.Importance, rightCommand.Importance); + Assert.AreEqual(leftCommand.TaskName, rightCommand.TaskName); + break; + + case LoggingEventType.TaskFinishedEvent: + TaskFinishedEventArgs leftTaskFinished = left.NodeBuildEvent.Value.Value as TaskFinishedEventArgs; + TaskFinishedEventArgs rightTaskFinished = right.NodeBuildEvent.Value.Value as TaskFinishedEventArgs; + Assert.IsNotNull(leftTaskFinished); + Assert.IsNotNull(rightTaskFinished); + Assert.AreEqual(leftTaskFinished.ProjectFile, rightTaskFinished.ProjectFile); + Assert.AreEqual(leftTaskFinished.Succeeded, rightTaskFinished.Succeeded); + Assert.AreEqual(leftTaskFinished.TaskFile, rightTaskFinished.TaskFile); + Assert.AreEqual(leftTaskFinished.TaskName, rightTaskFinished.TaskName); + break; + + case LoggingEventType.TaskStartedEvent: + TaskStartedEventArgs leftTaskStarted = left.NodeBuildEvent.Value.Value as TaskStartedEventArgs; + TaskStartedEventArgs rightTaskStarted = right.NodeBuildEvent.Value.Value as TaskStartedEventArgs; + Assert.IsNotNull(leftTaskStarted); + Assert.IsNotNull(rightTaskStarted); + Assert.AreEqual(leftTaskStarted.ProjectFile, rightTaskStarted.ProjectFile); + Assert.AreEqual(leftTaskStarted.TaskFile, rightTaskStarted.TaskFile); + Assert.AreEqual(leftTaskStarted.TaskName, rightTaskStarted.TaskName); + break; + + default: + Assert.Fail("Unexpected logging event type {0}", left.EventType); + break; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/OnError_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/OnError_Tests.cs new file mode 100644 index 00000000000..8a190e0729c --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/OnError_Tests.cs @@ -0,0 +1,790 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using System.Threading; +using System.Collections; +using System.Xml; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /* + * Class: OnErrorHandling + * + * Tests that exercise the tag. + */ + [TestClass] + sealed public class OnError_Tests + { + /* + * Method: Basic + * + * Construct a simple OnError tag. + */ + [TestMethod] + public void Basic() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + } + + /// + /// Target items and properties should be published to the project level even when a task that + /// outputs them fails. (Of course the task must have populated its property before it errors.) + /// Then these items and properties should be visible to the onerror targets. + /// + [TestMethod] + public void FailingTaskStillPublishesOutputs() + { + MockLogger l = new MockLogger(); + + string resx = Path.Combine(Path.GetTempPath(), "FailingTaskStillPublishesOutputs.resx"); + + try + { + File.WriteAllText(resx, @" + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + aa + + + bb + + "); + + Project project = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + ")))); + + ProjectInstance p = project.CreateProjectInstance(); + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + string resource = Path.ChangeExtension(resx, ".resources"); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + l.AssertLogContains("[" + resource + "]", "[" + resource + "]"); + + // And outputs are visible at the project level + Assert.AreEqual(resource, Helpers.MakeList(p.GetItems("FilesWrittenItem"))[0].EvaluatedInclude); + Assert.AreEqual(resource, p.GetPropertyValue("FilesWrittenProperty")); + + p = project.CreateProjectInstance(); + + // But are gone after resetting of course + Assert.AreEqual(0, Helpers.MakeList(p.GetItems("FilesWrittenItem")).Count); + Assert.AreEqual(String.Empty, p.GetPropertyValue("FilesWrittenProperty")); + } + finally + { + File.Delete(resx); + } + } + + /// + /// Target items and properties should be published to the project level when an OnError + /// target runs, and those items and properties should be visible to the OnError targets. + /// + [TestMethod] + public void OnErrorSeesPropertiesAndItemsFromFirstTarget() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + v2 + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + l.AssertLogContains("[a1;a2][v1][v2]"); + } + + /* + * Method: TwoExecuteTargets + * + * Make sure two execute targets can be called. + */ + [TestMethod] + public void TwoExecuteTargets() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp2-was-called") != -1), "The CleanUp2 target should have been called."); + } + + /* + * Method: TwoOnErrorClauses + * + * Make sure two OnError clauses can be used. + */ + [TestMethod] + public void TwoOnErrorClauses() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp2-was-called") != -1), "The CleanUp2 target should have been called."); + } + + /* + * Method: DependentTarget + * + * Make sure that a target that is a dependent of a target called because of an + * OnError clause is called + */ + [TestMethod] + public void DependentTarget() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp2-was-called") != -1), "The CleanUp2 target should have been called."); + } + + /* + * Method: ErrorInChildIsHandledInParent + * + * If a target is dependent on a child target and that child target errors, + * then the parent's OnError clauses should fire. + */ + [TestMethod] + public void ErrorInChildIsHandledInParent() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'BuildStep1' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Error-in-build-step-1") != -1), "The BuildStep1 target should have been called."); + } + + + /* + * Method: NonExistentExecuteTarget + * + * Construct a simple OnError tag. + */ + [TestMethod] + public void NonExistentExecuteTarget() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 2, "Expected at least one error because 'Build' failed and one error because 'CleanUp' didn't exist."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") == -1), "The CleanUp target should not have been called."); + } + + /* + * Method: TrueCondition + * + * Test the case when the result of the condition is 'true' + */ + [TestMethod] + public void TrueCondition() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + } + + /* + * Method: FalseCondition + * + * Test the case when the result of the condition is 'false' + */ + [TestMethod] + public void FalseCondition() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + ")))); + + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") == -1), "The CleanUp target should not have been called."); + } + + /* + * Method: PropertiesInExecuteTargets + * + * Make sure that properties in ExecuteTargets are properly expanded. + */ + [TestMethod] + public void PropertiesInExecuteTargets() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + Clean + Up + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + } + + /* + * Method: ErrorTargetsContinueAfterErrorsInErrorHandler + * + * If an error occurs in an error handling target, then continue processing + * remaining error targets + */ + [TestMethod] + public void ErrorTargetsContinueAfterErrorsInErrorHandler() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 4, "Four build errors expect: One from CoreBuild and on each from the error handlers."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp1-was-called") != -1), "The CleanUp1 target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp2-was-called") != -1), "The CleanUp2 target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp3-was-called") != -1), "The CleanUp3 target should have been called."); + } + + /* + * Method: ExecuteTargetIsMissing + * + * If an OnError specifies an ExecuteTarget that is missing, that's an error + */ + [TestMethod] + public void ExecuteTargetIsMissing() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 2, "Expected one error because 'Build' failed and one because 'CleanUp' doesn't exist."); + } + + /* + * Method: CommentsAroundOnError + * + * Since there is special-case code to ignore comments around OnError blocks, + * let's test this case. + */ + [TestMethod] + public void CommentsAroundOnError() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + ")))); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("CleanUp-was-called") != -1), "The CleanUp target should have been called."); + } + + /* + * Method: CircularDependency + * + * Detect circular dependencies and break out. + */ + [TestMethod] + public void CircularDependency() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + ")))); + + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 2, "Expected one error because 'Build' failed and one error because of the circular dependency."); + } + + /* + * Method: OutOfOrderOnError + * + * OnError clauses must come at the end of a Target, it can't be sprinkled in-between tasks. Catch this case. + */ + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void OutOfOrderOnError() + { + MockLogger l = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + ")))); + + /* No build required */ + } + + + #region Postbuild + /* + * Method: PostBuildBasic + * + * Handle the basic post-build case where the user has asked for 'On_Success' and + * none of the build steps fail. + */ + [TestMethod] + public void PostBuildBasic() + { + MockLogger l = new MockLogger(); + Project p = new Project + ( + XmlReader.Create(new StringReader(PostBuildBuilder("On_Success", FailAt.Nowhere))) + ); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 0, "Expected no error because 'Build' succeeded."); + Assert.IsTrue((l.FullLog.IndexOf("ResGen-was-called") != -1), "The ResGen target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-was-called") != -1), "The Compile target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-was-called") != -1), "The GenerateSatellites target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("PostBuild-was-called") != -1), "The PostBuild target should have been called."); + } + + /* + * Method: PostBuildOnSuccessWhereCompileFailed + * + * User asked for 'On_Success' but the compile step failed. We don't expect post-build + * to be called. + */ + [TestMethod] + public void PostBuildOnSuccessWhereCompileFailed() + { + MockLogger l = new MockLogger(); + Project p = new Project + ( + XmlReader.Create(new StringReader(PostBuildBuilder("On_Success", FailAt.Compile))) + ); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("ResGen-was-called") != -1), "The ResGen target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-was-called") != -1), "The Compile target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-step-failed") != -1), "The Compile target should have failed."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-was-called") == -1), "The GenerateSatellites target should not have been called."); + Assert.IsTrue((l.FullLog.IndexOf("PostBuild-was-called") == -1), "The PostBuild target should not have been called."); + } + + /* + * Method: PostBuildOnSuccessWhereGenerateSatellitesFailed + * + * User asked for 'On_Success' but the PostBuildOnSuccessWhereGenerateSatellitesFailed step + * failed. We don't expect post-build to be called. + */ + [TestMethod] + public void PostBuildOnSuccessWhereGenerateSatellitesFailed() + { + MockLogger l = new MockLogger(); + Project p = new Project + ( + XmlReader.Create(new StringReader(PostBuildBuilder("On_Success", FailAt.GenerateSatellites))) + ); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("ResGen-was-called") != -1), "The ResGen target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-was-called") != -1), "The Compile target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-was-called") != -1), "The GenerateSatellites target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-step-failed") != -1), "The GenerateSatellites target should have failed."); + Assert.IsTrue((l.FullLog.IndexOf("PostBuild-was-called") == -1), "The PostBuild target should not have been called."); + } + + /* + * Method: PostBuildAlwaysWhereCompileFailed + * + * User asked for 'Always' but the compile step failed. We expect the post-build + * to be called. + */ + [TestMethod] + public void PostBuildAlwaysWhereCompileFailed() + { + MockLogger l = new MockLogger(); + Project p = new Project + ( + XmlReader.Create(new StringReader(PostBuildBuilder("Always", FailAt.Compile))) + ); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("ResGen-was-called") != -1), "The ResGen target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-was-called") != -1), "The Compile target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-step-failed") != -1), "The Compile target should have failed."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-was-called") == -1), "The GenerateSatellites target should not have been called."); + Assert.IsTrue((l.FullLog.IndexOf("PostBuild-was-called") != -1), "The PostBuild target should have been called."); + } + + /* + * Method: PostBuildFinalOutputChangedWhereCompileFailed + * + * User asked for 'Final_Output_Changed' but the Compile step failed. + * We expect post-build to be called. + */ + [TestMethod] + public void PostBuildFinalOutputChangedWhereCompileFailed() + { + MockLogger l = new MockLogger(); + Project p = new Project + ( + XmlReader.Create(new StringReader(PostBuildBuilder("Final_Output_Changed", FailAt.Compile))) + ); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("ResGen-was-called") != -1), "The ResGen target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-was-called") != -1), "The Compile target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-step-failed") != -1), "The Compile target should have failed."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-was-called") == -1), "The GenerateSatellites target should not have been called."); + Assert.IsTrue((l.FullLog.IndexOf("PostBuild-was-called") == -1), "The PostBuild target should not have been called."); + } + + /* + * Method: PostBuildFinalOutputChangedWhereGenerateSatellitesFailed + * + * User asked for 'Final_Output_Changed' but the GenerateSatellites step failed. + * We expect post-build to be called because Compile succeeded (and wrote to the output). + */ + [TestMethod] + public void PostBuildFinalOutputChangedWhereGenerateSatellitesFailed() + { + MockLogger l = new MockLogger(); + Project p = new Project + ( + XmlReader.Create(new StringReader(PostBuildBuilder("Final_Output_Changed", FailAt.GenerateSatellites))) + ); + + p.Build(new string[] { "Build" }, new ILogger[] { l }); + + Assert.IsTrue(l.ErrorCount == 1, "Expected one error because 'Build' failed."); + Assert.IsTrue((l.FullLog.IndexOf("ResGen-was-called") != -1), "The ResGen target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("Compile-was-called") != -1), "The Compile target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-was-called") != -1), "The GenerateSatellites target should have been called."); + Assert.IsTrue((l.FullLog.IndexOf("GenerateSatellites-step-failed") != -1), "The GenerateSatellites target should have failed."); + Assert.IsTrue((l.FullLog.IndexOf("PostBuild-was-called") != -1), "The PostBuild target should have been called."); + } + + + /* + * The different places that PostBuildBuilder might be instructed to fail at + */ + private enum FailAt + { + Compile, + GenerateSatellites, + Nowhere + } + + /* + * Method: PostBuildBuilder + * + * Build a project file that mimics the fairly complex way we plan to use OnError + * to handle all the different combinations of project failures and post-build + * conditions. + * + */ + private static string PostBuildBuilder + ( + string controlFlag, // On_Success, Always, Final_Output_Changed + FailAt failAt + ) + { + string compileStep = ""; + if (FailAt.Compile == failAt) + { + compileStep = ""; + } + + string generateSatellites = ""; + if (FailAt.GenerateSatellites == failAt) + { + generateSatellites = ""; + } + + return String.Format(ObjectModelHelpers.CleanupFileContents(@" + + + {0} + + + + + + + + {1} + + + + + + + {2} + + + + + + + + + + + + + + + + + + + + + + + "), controlFlag, compileStep, generateSatellites); + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/RequestBuilder_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/RequestBuilder_Tests.cs new file mode 100644 index 00000000000..9c8b9603cb2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/RequestBuilder_Tests.cs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Unittest; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + using System.Threading.Tasks; + + [TestClass] + public class RequestBuilder_Tests + { + private AutoResetEvent _newBuildRequestsEvent; + private BuildRequestEntry _newBuildRequests_Entry; + private FullyQualifiedBuildRequest[] _newBuildRequests_FQRequests; + private BuildRequest[] _newBuildRequests_BuildRequests; + private AutoResetEvent _buildRequestCompletedEvent; + private BuildRequestEntry _buildRequestCompleted_Entry; + + private MockHost _host; + private IRequestBuilder _requestBuilder; + private int _nodeRequestId; + + public void LoggingException(Exception e) + { + } + + [TestInitialize] + public void SetUp() + { + _nodeRequestId = 1; + _host = new MockHost(); + _host.RequestBuilder = new RequestBuilder(); + ((IBuildComponent)_host.RequestBuilder).InitializeComponent(_host); + + _host.OnLoggingThreadException += this.LoggingException; + + _newBuildRequestsEvent = new AutoResetEvent(false); + _buildRequestCompletedEvent = new AutoResetEvent(false); + + _requestBuilder = (IRequestBuilder)_host.GetComponent(BuildComponentType.RequestBuilder); + _requestBuilder.OnBuildRequestCompleted += this.BuildRequestCompletedCallback; + _requestBuilder.OnNewBuildRequests += this.NewBuildRequestsCallback; + } + + [TestCleanup] + public void TearDown() + { + ((IBuildComponent)_requestBuilder).ShutdownComponent(); + _host = null; + } + + [TestMethod] + public void TestConstructor() + { + // The call to Setup will test this. + } + + [TestMethod] + public void TestSimpleBuildRequest() + { + BuildRequestConfiguration configuration = CreateTestProject(1); + try + { + TestTargetBuilder targetBuilder = (TestTargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "target1" }); + BuildRequestEntry entry = new BuildRequestEntry(request, configuration); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("target1", GetEmptySuccessfulTargetResult()); + targetBuilder.SetResultsToReturn(result); + + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + Assert.AreEqual(BuildRequestEntryState.Complete, entry.State); + Assert.AreEqual(entry, _buildRequestCompleted_Entry); + Assert.AreEqual(BuildResultCode.Success, _buildRequestCompleted_Entry.Result.OverallResult); + } + finally + { + DeleteTestProject(configuration); + } + } + + [TestMethod] + public void TestSimpleBuildRequestCancelled() + { + BuildRequestConfiguration configuration = CreateTestProject(1); + try + { + TestTargetBuilder targetBuilder = (TestTargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "target1" }); + BuildRequestEntry entry = new BuildRequestEntry(request, configuration); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("target1", GetEmptySuccessfulTargetResult()); + targetBuilder.SetResultsToReturn(result); + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + + Thread.Sleep(500); + _requestBuilder.CancelRequest(); + + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + Assert.AreEqual(BuildRequestEntryState.Complete, entry.State); + Assert.AreEqual(entry, _buildRequestCompleted_Entry); + Assert.AreEqual(BuildResultCode.Failure, _buildRequestCompleted_Entry.Result.OverallResult); + } + finally + { + DeleteTestProject(configuration); + } + } + + [TestMethod] + public void TestRequestWithReference() + { + BuildRequestConfiguration configuration = CreateTestProject(1); + try + { + TestTargetBuilder targetBuilder = (TestTargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + FullyQualifiedBuildRequest[] newRequest = new FullyQualifiedBuildRequest[1] { new FullyQualifiedBuildRequest(configuration, new string[1] { "testTarget2" }, true) }; + targetBuilder.SetNewBuildRequests(newRequest); + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "target1" }); + BuildRequestEntry entry = new BuildRequestEntry(request, configuration); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("target1", GetEmptySuccessfulTargetResult()); + targetBuilder.SetResultsToReturn(result); + + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + WaitForEvent(_newBuildRequestsEvent, "New Build Requests"); + Assert.AreEqual(_newBuildRequests_Entry, entry); + ObjectModelHelpers.AssertArrayContentsMatch(_newBuildRequests_FQRequests, newRequest); + + BuildResult newResult = new BuildResult(_newBuildRequests_BuildRequests[0]); + newResult.AddResultsForTarget("testTarget2", GetEmptySuccessfulTargetResult()); + entry.ReportResult(newResult); + _requestBuilder.ContinueRequest(); + + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + Assert.AreEqual(BuildRequestEntryState.Complete, entry.State); + Assert.AreEqual(entry, _buildRequestCompleted_Entry); + Assert.AreEqual(BuildResultCode.Success, _buildRequestCompleted_Entry.Result.OverallResult); + } + finally + { + DeleteTestProject(configuration); + } + } + + [TestMethod] + public void TestRequestWithReferenceCancelled() + { + BuildRequestConfiguration configuration = CreateTestProject(1); + try + { + TestTargetBuilder targetBuilder = (TestTargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + FullyQualifiedBuildRequest[] newRequest = new FullyQualifiedBuildRequest[1] { new FullyQualifiedBuildRequest(configuration, new string[1] { "testTarget2" }, true) }; + targetBuilder.SetNewBuildRequests(newRequest); + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "target1" }); + BuildRequestEntry entry = new BuildRequestEntry(request, configuration); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("target1", GetEmptySuccessfulTargetResult()); + targetBuilder.SetResultsToReturn(result); + + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + WaitForEvent(_newBuildRequestsEvent, "New Build Requests"); + Assert.AreEqual(_newBuildRequests_Entry, entry); + ObjectModelHelpers.AssertArrayContentsMatch(_newBuildRequests_FQRequests, newRequest); + + BuildResult newResult = new BuildResult(_newBuildRequests_BuildRequests[0]); + newResult.AddResultsForTarget("testTarget2", GetEmptySuccessfulTargetResult()); + entry.ReportResult(newResult); + + _requestBuilder.ContinueRequest(); + Thread.Sleep(500); + _requestBuilder.CancelRequest(); + + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + Assert.AreEqual(BuildRequestEntryState.Complete, entry.State); + Assert.AreEqual(entry, _buildRequestCompleted_Entry); + Assert.AreEqual(BuildResultCode.Failure, _buildRequestCompleted_Entry.Result.OverallResult); + } + finally + { + DeleteTestProject(configuration); + } + } + + [TestMethod] + public void TestMissingProjectFile() + { + TestTargetBuilder targetBuilder = (TestTargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestConfiguration configuration = new BuildRequestConfiguration(1, new BuildRequestData("testName", new Dictionary(), "3.5", new string[0], null), "2.0"); + configCache.AddConfiguration(configuration); + + BuildRequest request = CreateNewBuildRequest(1, new string[1] { "target1" }); + BuildRequestEntry entry = new BuildRequestEntry(request, configuration); + _requestBuilder.BuildRequest(GetNodeLoggingContext(), entry); + WaitForEvent(_buildRequestCompletedEvent, "Build Request Completed"); + Assert.AreEqual(BuildRequestEntryState.Complete, entry.State); + Assert.AreEqual(entry, _buildRequestCompleted_Entry); + Assert.AreEqual(BuildResultCode.Failure, _buildRequestCompleted_Entry.Result.OverallResult); + Assert.AreEqual(typeof(InvalidProjectFileException), _buildRequestCompleted_Entry.Result.Exception.GetType()); + } + + private BuildRequestConfiguration CreateTestProject(int configId) + { + string projectFileContents = @" + + + + + + + + + + + + + + + "; + + string projectFile = GetTestProjectFile(configId); + File.WriteAllText(projectFile, projectFileContents.Replace('`', '"')); + + string defaultToolsVersion = null; + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + defaultToolsVersion = "4.0"; + } + else + { + defaultToolsVersion = "2.0"; + } + + + BuildRequestConfiguration config = new BuildRequestConfiguration(configId, new BuildRequestData(projectFile, new Dictionary(), "4.0", new string[0], null), defaultToolsVersion); + return config; + } + + private void DeleteTestProject(BuildRequestConfiguration config) + { + string fileName = GetTestProjectFile(config.ConfigurationId); + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + + private string GetTestProjectFile(int configId) + { + return Path.GetTempPath() + "testProject" + configId + ".proj"; + } + + private void NewBuildRequestsCallback(BuildRequestEntry entry, FullyQualifiedBuildRequest[] requests) + { + _newBuildRequests_FQRequests = requests; + _newBuildRequests_BuildRequests = new BuildRequest[requests.Length]; + _newBuildRequests_Entry = entry; + + int index = 0; + foreach (FullyQualifiedBuildRequest request in requests) + { + IConfigCache configCache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestConfiguration matchingConfig = configCache.GetMatchingConfiguration(request.Config); + BuildRequest newRequest = CreateNewBuildRequest(matchingConfig.ConfigurationId, request.Targets); + + entry.WaitForResult(newRequest); + _newBuildRequests_BuildRequests[index++] = newRequest; + } + _newBuildRequestsEvent.Set(); + } + + private void BuildRequestCompletedCallback(BuildRequestEntry entry) + { + _buildRequestCompleted_Entry = entry; + _buildRequestCompletedEvent.Set(); + } + + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + } + + private TargetResult GetEmptySuccessfulTargetResult() + { + return new TargetResult(new TaskItem[0] { }, new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null)); + } + + private void WaitForEvent(WaitHandle evt, string eventName) + { + if (!evt.WaitOne(5000, false)) + { + Assert.Fail("Did not receive " + eventName + " callback before the timeout expired."); + } + } + + private NodeLoggingContext GetNodeLoggingContext() + { + return new NodeLoggingContext(_host, 1, false); + } + } + + internal class TestTargetBuilder : ITargetBuilder, IBuildComponent + { + private IBuildComponentHost _host; + private IResultsCache _cache; + private FullyQualifiedBuildRequest[] _newRequests; + private IRequestBuilderCallback _requestBuilderCallback; + + internal void SetResultsToReturn(BuildResult result) + { + _cache.AddResult(result); + } + + internal void SetNewBuildRequests(FullyQualifiedBuildRequest[] requests) + { + _newRequests = requests; + } + + #region ITargetBuilder Members + + public Task BuildTargets(ProjectLoggingContext loggingContext, BuildRequestEntry entry, IRequestBuilderCallback callback, string[] targets, Lookup baseLookup, CancellationToken cancellationToken) + { + _requestBuilderCallback = callback; + + if (cancellationToken.WaitHandle.WaitOne(1500, false)) + { + BuildResult result = new BuildResult(entry.Request); + foreach (string target in targets) + { + result.AddResultsForTarget(target, TestUtilities.GetEmptyFailingTargetResult()); + } + return Task.FromResult(result); + } + + if (null != _newRequests) + { + string[] projectFiles = new string[_newRequests.Length]; + PropertyDictionary[] properties = new PropertyDictionary[_newRequests.Length]; + string[] toolsVersions = new string[_newRequests.Length]; + + for (int i = 0; i < projectFiles.Length; ++i) + { + projectFiles[i] = _newRequests[i].Config.ProjectFullPath; + properties[i] = new PropertyDictionary(_newRequests[i].Config.Properties); + toolsVersions[i] = _newRequests[i].Config.ToolsVersion; + } + + _requestBuilderCallback.BuildProjects(projectFiles, properties, toolsVersions, _newRequests[0].Targets, _newRequests[0].ResultsNeeded); + + if (cancellationToken.WaitHandle.WaitOne(1500, false)) + { + BuildResult result = new BuildResult(entry.Request); + foreach (string target in targets) + { + result.AddResultsForTarget(target, TestUtilities.GetEmptyFailingTargetResult()); + } + return Task.FromResult(result); + } + } + + return Task.FromResult(_cache.GetResultForRequest(entry.Request)); + } + + #endregion + + #region IBuildComponent Members + + public void InitializeComponent(IBuildComponentHost host) + { + _host = host; + _cache = new ResultsCache(); + } + + public void ShutdownComponent() + { + _host = null; + _cache = null; + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/ResultsCache_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/ResultsCache_Tests.cs new file mode 100644 index 00000000000..3a2f6980b17 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/ResultsCache_Tests.cs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using Microsoft.Build.Unittest; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class ResultsCache_Tests + { + [TestInitialize] + public void SetUp() + { + } + + [TestCleanup] + public void TearDown() + { + } + + [TestMethod] + public void TestConstructor() + { + ResultsCache cache = new ResultsCache(); + } + + [TestMethod] + public void TestAddAndRetrieveResults() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null); BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + BuildResult retrievedResult = cache.GetResultForRequest(request); + + Assert.IsTrue(AreResultsIdentical(result, retrievedResult)); + } + + [TestMethod] + public void TestAddAndRetrieveResultsByConfiguration() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "otherTarget" }, null, BuildEventContext.Invalid, null); + result = new BuildResult(request); + result.AddResultsForTarget("otherTarget", TestUtilities.GetEmptySucceedingTargetResult()); + cache.AddResult(result); + + BuildResult retrievedResult = cache.GetResultsForConfiguration(1); + + Assert.IsTrue(retrievedResult.HasResultsForTarget("testTarget")); + Assert.IsTrue(retrievedResult.HasResultsForTarget("otherTarget")); + } + + [TestMethod] + public void TestMissingResults() + { + ResultsCache cache = new ResultsCache(); + + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget" }, null, BuildEventContext.Invalid, null); + BuildResult retrievedResult = cache.GetResultForRequest(request); + Assert.IsNull(retrievedResult); + } + + [TestMethod] + public void TestRetrieveMergedResults() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[2] { "testTarget", "testTarget2" }, null, BuildEventContext.Invalid, null); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + BuildResult result2 = new BuildResult(request); + result2.AddResultsForTarget("testTarget2", TestUtilities.GetEmptySucceedingTargetResult()); + cache.AddResult(result2); + + BuildResult retrievedResult = cache.GetResultForRequest(request); + + Assert.IsTrue(AreResultsIdenticalForTarget(result, retrievedResult, "testTarget")); + Assert.IsTrue(AreResultsIdenticalForTarget(result2, retrievedResult, "testTarget2")); + } + + [TestMethod] + public void TestMergeResultsWithException() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[] { "testTarget" }, null, BuildEventContext.Invalid, null); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + BuildResult result2 = new BuildResult(request, new Exception("Test exception")); + cache.AddResult(result2); + + BuildResult retrievedResult = cache.GetResultForRequest(request); + + Assert.IsNotNull(retrievedResult.Exception); + } + + [ExpectedException(typeof(InternalErrorException))] + [TestMethod] + public void TestRetrieveIncompleteResults() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[2] { "testTarget", "testTarget2" }, null, BuildEventContext.Invalid, null); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + BuildResult retrievedResult = cache.GetResultForRequest(request); + } + + [TestMethod] + public void TestRetrieveSubsetResults() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget2" }, null, BuildEventContext.Invalid, null); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + BuildResult result2 = new BuildResult(request); + result2.AddResultsForTarget("testTarget2", TestUtilities.GetEmptySucceedingTargetResult()); + cache.AddResult(result2); + + BuildResult retrievedResult = cache.GetResultForRequest(request); + + Assert.IsTrue(AreResultsIdenticalForTarget(result2, retrievedResult, "testTarget2")); + } + + /// + /// If a result had multiple targets associated with it and we only requested some of their + /// results, the returned result should only contain the targets we asked for, BUT the overall + /// status of the result should remain the same. + /// + [TestMethod] + public void TestRetrieveSubsetTargetsFromResult() + { + ResultsCache cache = new ResultsCache(); + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget2" }, null, BuildEventContext.Invalid, null); + + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + result.AddResultsForTarget("testTarget2", TestUtilities.GetEmptySucceedingTargetResult()); + cache.AddResult(result); + + ResultsCacheResponse response = cache.SatisfyRequest(request, new List(), new List(new string[] { "testTarget2" }), new List(new string[] { "testTarget" }), skippedResultsAreOK: false); + + Assert.AreEqual(ResultsCacheResponseType.Satisfied, response.Type); + + Assert.IsTrue(AreResultsIdenticalForTarget(result, response.Results, "testTarget2")); + Assert.IsFalse(response.Results.HasResultsForTarget("testTarget")); + Assert.AreEqual(BuildResultCode.Failure, response.Results.OverallResult); + } + + [TestMethod] + public void TestClearResultsCache() + { + ResultsCache cache = new ResultsCache(); + cache.ClearResults(); + + BuildRequest request = new BuildRequest(1 /* submissionId */, 0, 1, new string[1] { "testTarget2" }, null, BuildEventContext.Invalid, null); + BuildResult result = new BuildResult(request); + result.AddResultsForTarget("testTarget", TestUtilities.GetEmptyFailingTargetResult()); + cache.AddResult(result); + + cache.ClearResults(); + + Assert.IsNull(cache.GetResultForRequest(request)); + } + + #region Helper Methods + + static internal bool AreResultsIdentical(BuildResult a, BuildResult b) + { + if (a.ConfigurationId != b.ConfigurationId) + { + return false; + } + + if ((a.Exception == null) ^ (b.Exception == null)) + { + return false; + } + + if (a.Exception != null) + { + if (a.Exception.GetType() != b.Exception.GetType()) + { + return false; + } + } + + if (a.OverallResult != b.OverallResult) + { + return false; + } + + foreach (KeyValuePair targetResult in a.ResultsByTarget) + { + if (!AreResultsIdenticalForTarget(a, b, targetResult.Key)) + { + return false; + } + } + + foreach (KeyValuePair targetResult in b.ResultsByTarget) + { + if (!AreResultsIdenticalForTarget(a, b, targetResult.Key)) + { + return false; + } + } + + return true; + } + + static internal bool AreResultsIdenticalForTargets(BuildResult a, BuildResult b, string[] targets) + { + foreach (string target in targets) + { + if (!AreResultsIdenticalForTarget(a, b, target)) + { + return false; + } + } + + return true; + } + + static private bool AreResultsIdenticalForTarget(BuildResult a, BuildResult b, string target) + { + if (!a.HasResultsForTarget(target) || !b.HasResultsForTarget(target)) + { + return false; + } + + if (a[target].ResultCode != b[target].ResultCode) + { + return false; + } + + if (!AreItemsIdentical(a[target].Items, b[target].Items)) + { + return false; + } + + return true; + } + + static private bool AreItemsIdentical(IList a, IList b) + { + // Exhaustive comparison of items should not be necessary since we don't merge on the item level. + return a.Count == b.Count; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/Scheduler_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/Scheduler_Tests.cs new file mode 100644 index 00000000000..f7e6748e504 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/Scheduler_Tests.cs @@ -0,0 +1,804 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the basic scheduler. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; + using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + using Microsoft.Build.Unittest; + + /// + /// Tests of the scheduler. + /// + //[TestClass] + // Ignore: Causing issues with other tests + public class Scheduler_Tests + { + /// + /// The host object. + /// + private MockHost _host; + + /// + /// The scheduler used in each test. + /// + private Scheduler _scheduler; + + /// + /// The default parent request + /// + private BuildRequest _defaultParentRequest; + + /// + /// The mock logger for testing. + /// + private MockLogger _logger; + + /// + /// The standard build manager for each test. + /// + private BuildManager _buildManager; + + /// + /// The build parameters. + /// + private BuildParameters _parameters; + + /// + /// Set up + /// + [TestInitialize] + public void SetUp() + { + // Since we're creating our own BuildManager, we need to make sure that the default + // one has properly relinquished the inproc node + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)BuildManager.DefaultBuildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + if (nodeProviderInProc != null) + { + nodeProviderInProc.Dispose(); + } + + _host = new MockHost(); + _scheduler = new Scheduler(); + _scheduler.InitializeComponent(_host); + CreateConfiguration(99, "parent.proj"); + _defaultParentRequest = CreateBuildRequest(99, 99, new string[] { }, null); + + // Set up the scheduler with one node to start with. + _scheduler.ReportNodesCreated(new NodeInfo[] { new NodeInfo(1, NodeProviderType.InProc) }); + _scheduler.ReportRequestBlocked(1, new BuildRequestBlocker(-1, new string[] { }, new BuildRequest[] { _defaultParentRequest })); + + _logger = new MockLogger(); + _parameters = new BuildParameters(); + _parameters.Loggers = new ILogger[] { _logger }; + _parameters.ShutdownInProcNodeOnBuildFinish = true; + _buildManager = new BuildManager(); + } + + /// + /// Tear down + /// + [TestCleanup] + public void TearDown() + { + if (_buildManager != null) + { + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)_buildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + nodeProviderInProc.Dispose(); + + _buildManager.Dispose(); + } + } + + /// + /// Verify that when a single request is submitted, we get a request assigned back out. + /// + [TestMethod] + public void TestSimpleRequest() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request = CreateBuildRequest(1, 1); + BuildRequestBlocker blocker = new BuildRequestBlocker(request.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request, response[0].BuildRequest); + } + + /// + /// Verify that when we submit a request and we already have results, we get the results back. + /// + [TestMethod] + public void TestSimpleRequestWithCachedResultsSuccess() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildResult result = CacheBuildResult(request, "foo", TestUtilities.GetSuccessResult()); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + + // First response tells the parent of the results. + Assert.AreEqual(ScheduleActionType.ReportResults, response[0].Action); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result, response[0].Unblocker.Result)); + + // Second response tells the parent to continue. + Assert.AreEqual(ScheduleActionType.ResumeExecution, response[1].Action); + Assert.IsNull(response[1].Unblocker.Result); + } + + /// + /// Verify that when we submit a request with failing results, we get the results back. + /// + [TestMethod] + public void TestSimpleRequestWithCachedResultsFail() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildResult result = CacheBuildResult(request, "foo", TestUtilities.GetStopWithErrorResult()); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + + // First response tells the parent of the results. + Assert.AreEqual(ScheduleActionType.ReportResults, response[0].Action); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result, response[0].Unblocker.Result)); + + // Second response tells the parent to continue. + Assert.AreEqual(ScheduleActionType.ResumeExecution, response[1].Action); + Assert.IsNull(response[1].Unblocker.Result); + } + + /// + /// Verify that when we submit a child request with results cached, we get those results back. + /// + [TestMethod] + public void TestChildRequest() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request = CreateBuildRequest(1, 1, new string[] { "foo" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(-1, new string[] { }, new BuildRequest[] { request }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + CreateConfiguration(2, "bar.proj"); + BuildRequest childRequest = CreateBuildRequest(2, 2, new string[] { "foo" }, request); + BuildResult childResult = CacheBuildResult(childRequest, "foo", TestUtilities.GetSuccessResult()); + + blocker = new BuildRequestBlocker(0, new string[] { "foo" }, new BuildRequest[] { childRequest }); + response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + + // The first response will be to report the results back to the node. + Assert.AreEqual(ScheduleActionType.ReportResults, response[0].Action); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(childResult, response[0].Unblocker.Result)); + + // The second response will be to continue building the original request. + Assert.AreEqual(ScheduleActionType.ResumeExecution, response[1].Action); + Assert.IsNull(response[1].Unblocker.Result); + } + + /// + /// Verify that when multiple requests are submitted, the first one in is the first one out. + /// + [TestMethod] + public void TestMultipleRequests() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request1, response[0].BuildRequest); + } + + /// + /// Verify that when multiple requests are submitted with results cached, we get the results back. + /// + [TestMethod] + public void TestMultipleRequestsWithSomeResults() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + CreateConfiguration(2, "bar.proj"); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }); + BuildResult result2 = CacheBuildResult(request2, "bar", TestUtilities.GetSuccessResult()); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.AreEqual(ScheduleActionType.ReportResults, response[0].Action); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result2, response[0].Unblocker.Result)); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[1].Action); + Assert.AreEqual(request1, response[1].BuildRequest); + } + + /// + /// Verify that when multiple requests are submitted with results cached, we get the results back. + /// + [TestMethod] + public void TestMultipleRequestsWithAllResults() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildResult result1 = CacheBuildResult(request1, "foo", TestUtilities.GetSuccessResult()); + CreateConfiguration(2, "bar.proj"); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }); + BuildResult result2 = CacheBuildResult(request2, "bar", TestUtilities.GetSuccessResult()); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(3, response.Count); + + // First two are the results which were cached. + Assert.AreEqual(ScheduleActionType.ReportResults, response[0].Action); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result1, response[0].Unblocker.Result)); + Assert.AreEqual(ScheduleActionType.ReportResults, response[1].Action); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result2, response[1].Unblocker.Result)); + + // Last response is to continue the parent. + Assert.AreEqual(ScheduleActionType.ResumeExecution, response[2].Action); + Assert.AreEqual(request1.ParentGlobalRequestId, response[2].Unblocker.BlockedRequestId); + Assert.IsNull(response[2].Unblocker.Result); + } + + /// + /// Verify that if the affinity of one of the requests is out-of-proc, we create an out-of-proc node (but only one) + /// even if the max node count = 1. + /// + [TestMethod] + public void TestOutOfProcNodeCreatedWhenAffinityIsOutOfProc() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }, NodeAffinity.OutOfProc, _defaultParentRequest); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + // Parent request is blocked by the fact that both child requests require the out-of-proc node that doesn't + // exist yet. + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.CreateNode, response[0].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, response[0].RequiredNodeType); + Assert.AreEqual(1, response[0].NumberOfNodesToCreate); + } + + /// + /// Verify that if the affinity of our requests is out-of-proc, that many out-of-proc nodes will + /// be made (assuming it does not exceed MaxNodeCount) + /// + [TestMethod] + public void TestOutOfProcNodesCreatedWhenAffinityIsOutOfProc() + { + _host.BuildParameters.MaxNodeCount = 4; + + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }, NodeAffinity.OutOfProc, _defaultParentRequest); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + // Parent request is blocked by the fact that both child requests require the out-of-proc node that doesn't + // exist yet. + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.CreateNode, response[0].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, response[0].RequiredNodeType); + Assert.AreEqual(2, response[0].NumberOfNodesToCreate); + } + + /// + /// Verify that if we have multiple requests and the max node count to fulfill them, + /// we still won't create any new nodes if they're all for the same configuration -- + /// they'd end up all being assigned to the same node. + /// + [TestMethod] + public void TestNoNewNodesCreatedForMultipleRequestsWithSameConfiguration() + { + _host.BuildParameters.MaxNodeCount = 3; + + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }); + BuildRequest request3 = CreateBuildRequest(3, 1, new string[] { "baz" }); + BuildRequest request4 = CreateBuildRequest(4, 1, new string[] { "qux" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2, request3, request4 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request1, response[0].BuildRequest); + } + + /// + /// Verify that if the affinity of our requests is "any", we will not create more than + /// MaxNodeCount nodes (1 IP node + MaxNodeCount - 1 OOP nodes) + /// + [TestMethod] + public void TestMaxNodeCountNotExceededWithRequestsOfAffinityAny() + { + _host.BuildParameters.MaxNodeCount = 3; + + CreateConfiguration(1, "foo.proj"); + CreateConfiguration(2, "bar.proj"); + CreateConfiguration(3, "baz.proj"); + CreateConfiguration(4, "quz.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }); + BuildRequest request3 = CreateBuildRequest(3, 3, new string[] { "baz" }); + BuildRequest request4 = CreateBuildRequest(4, 4, new string[] { "qux" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2, request3, request4 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request1, response[0].BuildRequest); + Assert.AreEqual(ScheduleActionType.CreateNode, response[1].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, response[1].RequiredNodeType); + Assert.AreEqual(2, response[1].NumberOfNodesToCreate); + } + + /// + /// Verify that if we get 2 Any and 2 inproc requests, in that order, we will only create 2 nodes, since the initial inproc + /// node will service an Any request instead of an inproc request, leaving only one non-inproc request for the second round + /// of node creation. + /// + [TestMethod] + public void VerifyRequestOrderingDoesNotAffectNodeCreationCountWithInProcAndAnyRequests() + { + // Since we're creating our own BuildManager, we need to make sure that the default + // one has properly relinquished the inproc node + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)BuildManager.DefaultBuildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + if (nodeProviderInProc != null) + { + nodeProviderInProc.Dispose(); + } + + _host = new MockHost(); + _host.BuildParameters.MaxNodeCount = 3; + + _scheduler = new Scheduler(); + _scheduler.InitializeComponent(_host); + + _logger = new MockLogger(); + _parameters = new BuildParameters(); + _parameters.Loggers = new ILogger[] { _logger }; + _parameters.ShutdownInProcNodeOnBuildFinish = true; + _buildManager = new BuildManager(); + + CreateConfiguration(99, "parent.proj"); + _defaultParentRequest = CreateBuildRequest(99, 99, new string[] { }, null); + + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.Any, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }, NodeAffinity.InProc, _defaultParentRequest); + BuildRequest request3 = CreateBuildRequest(3, 1, new string[] { "bar" }, NodeAffinity.InProc, _defaultParentRequest); + + List response = new List(_scheduler.ReportRequestBlocked(1, new BuildRequestBlocker(-1, new string[] { }, new BuildRequest[] { _defaultParentRequest, request1, request2, request3 }))); + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.CreateNode, response[0].Action); + Assert.AreEqual(NodeAffinity.InProc, response[0].RequiredNodeType); + Assert.AreEqual(1, response[0].NumberOfNodesToCreate); + + List nodeInfos = new List(new NodeInfo[] { new NodeInfo(1, NodeProviderType.InProc) }); + List moreResponses = new List(_scheduler.ReportNodesCreated(nodeInfos)); + + Assert.AreEqual(2, moreResponses.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, moreResponses[0].Action); + Assert.AreEqual(ScheduleActionType.CreateNode, moreResponses[1].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, moreResponses[1].RequiredNodeType); + Assert.AreEqual(1, moreResponses[1].NumberOfNodesToCreate); + } + + /// + /// Verify that if the affinity of our requests is out-of-proc, we will create as many as + /// MaxNodeCount out-of-proc nodes + /// + [TestMethod] + public void TestMaxNodeCountOOPNodesCreatedForOOPAffinitizedRequests() + { + _host.BuildParameters.MaxNodeCount = 3; + + CreateConfiguration(1, "foo.proj"); + CreateConfiguration(2, "bar.proj"); + CreateConfiguration(3, "baz.proj"); + CreateConfiguration(4, "quz.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request3 = CreateBuildRequest(3, 3, new string[] { "baz" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request4 = CreateBuildRequest(4, 4, new string[] { "qux" }, NodeAffinity.OutOfProc, _defaultParentRequest); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2, request3, request4 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + // Parent request is blocked by the fact that both child requests require the out-of-proc node that doesn't + // exist yet. + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.CreateNode, response[0].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, response[0].RequiredNodeType); + Assert.AreEqual(3, response[0].NumberOfNodesToCreate); + } + + /// + /// Verify that if only some of our requests are explicitly affinitized to out-of-proc, and that number + /// is less than MaxNodeCount, that we only create MaxNodeCount - 1 OOP nodes (for a total of MaxNodeCount + /// nodes, when the inproc node is included) + /// + [TestMethod] + public void TestMaxNodeCountNodesNotExceededWithSomeOOPRequests1() + { + _host.BuildParameters.MaxNodeCount = 3; + + CreateConfiguration(1, "foo.proj"); + CreateConfiguration(2, "bar.proj"); + CreateConfiguration(3, "baz.proj"); + CreateConfiguration(4, "quz.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }); + BuildRequest request3 = CreateBuildRequest(3, 3, new string[] { "baz" }); + BuildRequest request4 = CreateBuildRequest(4, 4, new string[] { "qux" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2, request3, request4 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request2, response[0].BuildRequest); + Assert.AreEqual(ScheduleActionType.CreateNode, response[1].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, response[1].RequiredNodeType); + Assert.AreEqual(2, response[1].NumberOfNodesToCreate); + } + + /// + /// Verify that if only some of our requests are explicitly affinitized to out-of-proc, and that number + /// is less than MaxNodeCount, that we only create MaxNodeCount - 1 OOP nodes (for a total of MaxNodeCount + /// nodes, when the inproc node is included) + /// + [TestMethod] + public void TestMaxNodeCountNodesNotExceededWithSomeOOPRequests2() + { + _host.BuildParameters.MaxNodeCount = 3; + + CreateConfiguration(1, "foo.proj"); + CreateConfiguration(2, "bar.proj"); + CreateConfiguration(3, "baz.proj"); + CreateConfiguration(4, "quz.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }, NodeAffinity.OutOfProc, _defaultParentRequest); + BuildRequest request3 = CreateBuildRequest(3, 3, new string[] { "baz" }); + BuildRequest request4 = CreateBuildRequest(4, 4, new string[] { "qux" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2, request3, request4 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request3, response[0].BuildRequest); + Assert.AreEqual(ScheduleActionType.CreateNode, response[1].Action); + Assert.AreEqual(NodeAffinity.OutOfProc, response[1].RequiredNodeType); + Assert.AreEqual(2, response[1].NumberOfNodesToCreate); + } + + /// + /// Make sure that traversal projects are marked with an affinity of "InProc", which means that + /// even if multiple are available, we should still only have the single inproc node. + /// + [TestMethod] + public void TestTraversalAffinityIsInProc() + { + _host.BuildParameters.MaxNodeCount = 3; + + CreateConfiguration(1, "dirs.proj"); + CreateConfiguration(2, "abc.metaproj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, _defaultParentRequest); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }, _defaultParentRequest); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + // There will be no request to create a new node, because both of the above requests are traversals, + // which have an affinity of "inproc", and the inproc node already exists. + Assert.AreEqual(1, response.Count); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[0].Action); + Assert.AreEqual(request1, response[0].BuildRequest); + } + + /// + /// With something approximating the BuildManager's build loop, make sure that we don't end up + /// trying to create more nodes than we can actually support. + /// + [TestMethod] + public void VerifyNoOverCreationOfNodesWithBuildLoop() + { + // Since we're creating our own BuildManager, we need to make sure that the default + // one has properly relinquished the inproc node + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)BuildManager.DefaultBuildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + if (nodeProviderInProc != null) + { + nodeProviderInProc.Dispose(); + } + + _host = new MockHost(); + _host.BuildParameters.MaxNodeCount = 3; + + _scheduler = new Scheduler(); + _scheduler.InitializeComponent(_host); + + _parameters = new BuildParameters(); + _parameters.ShutdownInProcNodeOnBuildFinish = true; + _buildManager = new BuildManager(); + + CreateConfiguration(99, "parent.proj"); + _defaultParentRequest = CreateBuildRequest(99, 99, new string[] { }, null); + + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }, NodeAffinity.OutOfProc, _defaultParentRequest); + CreateConfiguration(2, "foo2.proj"); + BuildRequest request2 = CreateBuildRequest(2, 2, new string[] { "bar" }, NodeAffinity.OutOfProc, _defaultParentRequest); + CreateConfiguration(3, "foo3.proj"); + BuildRequest request3 = CreateBuildRequest(3, 3, new string[] { "bar" }, NodeAffinity.InProc, _defaultParentRequest); + + List responses = new List(_scheduler.ReportRequestBlocked(1, new BuildRequestBlocker(-1, new string[] { }, new BuildRequest[] { _defaultParentRequest, request1, request2, request3 }))); + + int nextNodeId = 1; + bool inProcNodeExists = false; + MockPerformSchedulingActions(responses, ref nextNodeId, ref inProcNodeExists); + Assert.AreEqual(4, nextNodeId); // 3 nodes + } + + /// + /// Verify that if we get two requests but one of them is a failure, we only get the failure result back. + /// + [TestMethod] + public void TestTwoRequestsWithFirstFailure() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildResult result1 = CacheBuildResult(request1, "foo", TestUtilities.GetStopWithErrorResult()); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result1, response[0].Unblocker.Result)); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[1].Action); + } + + /// + /// Verify that if we get two requests but one of them is a failure, we only get the failure result back. + /// + [TestMethod] + public void TestTwoRequestsWithSecondFailure() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }); + BuildResult result2 = CacheBuildResult(request2, "bar", TestUtilities.GetStopWithErrorResult()); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result2, response[0].Unblocker.Result)); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[1].Action); + } + + /// + /// Verify that if we get three requests but one of them is a failure, we only get the failure result back. + /// + [TestMethod] + public void TestThreeRequestsWithOneFailure() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request1 = CreateBuildRequest(1, 1, new string[] { "foo" }); + BuildRequest request2 = CreateBuildRequest(2, 1, new string[] { "bar" }); + BuildResult result2 = CacheBuildResult(request2, "bar", TestUtilities.GetStopWithErrorResult()); + BuildRequest request3 = CreateBuildRequest(3, 1, new string[] { "baz" }); + + BuildRequestBlocker blocker = new BuildRequestBlocker(request1.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request1, request2, request3 }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + Assert.AreEqual(2, response.Count); + Assert.IsTrue(ResultsCache_Tests.AreResultsIdentical(result2, response[0].Unblocker.Result)); + Assert.AreEqual(ScheduleActionType.ScheduleWithConfiguration, response[1].Action); + } + + /// + /// Verify that providing a result to the only ourstanding request results in build complete. + /// + [TestMethod] + public void TestResult() + { + CreateConfiguration(1, "foo.proj"); + BuildRequest request = CreateBuildRequest(1, 1); + BuildRequestBlocker blocker = new BuildRequestBlocker(request.ParentGlobalRequestId, new string[] { }, new BuildRequest[] { request }); + List response = new List(_scheduler.ReportRequestBlocked(1, blocker)); + + BuildResult result = CreateBuildResult(request, "foo", TestUtilities.GetSuccessResult()); + response = new List(_scheduler.ReportResult(1, result)); + + Assert.AreEqual(2, response.Count); + + // First response is reporting the results for this request to the parent + Assert.AreEqual(ScheduleActionType.ReportResults, response[0].Action); + + // Second response is continuing execution of the parent + Assert.AreEqual(ScheduleActionType.ResumeExecution, response[1].Action); + Assert.AreEqual(request.ParentGlobalRequestId, response[1].Unblocker.BlockedRequestId); + } + + /// + /// Tests that the detailed summary setting causes the summary to be produced. + /// + [TestMethod] + public void TestDetailedSummary() + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + _parameters.DetailedSummary = true; + Project project = new Project(new XmlTextReader(new StringReader(contents))); + BuildRequestData data = new BuildRequestData(project.CreateProjectInstance(), new string[] { "test" }); + BuildResult result = _buildManager.Build(_parameters, data); + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + _logger.AssertLogContains("[success]"); + + // Verify the existence of the first line of the header. + StringReader reader = new StringReader(ResourceUtilities.FormatResourceString("BuildHierarchyHeader")); + _logger.AssertLogContains(reader.ReadLine()); + } + + /// + /// Creates a configuration and stores it in the cache. + /// + private void CreateConfiguration(int configId, string file) + { + BuildRequestData data = new BuildRequestData(file, new Dictionary(), "4.0", new string[] { }, null); + BuildRequestConfiguration config = new BuildRequestConfiguration(configId, data, "4.0"); + config.ProjectInitialTargets = new List(); + config.ProjectDefaultTargets = new List(); + + (_host.GetComponent(BuildComponentType.ConfigCache) as IConfigCache).AddConfiguration(config); + } + + /// + /// Creates and caches a built result. + /// + private BuildResult CacheBuildResult(BuildRequest request, string target, WorkUnitResult workUnitResult) + { + BuildResult result = CreateBuildResult(request, target, workUnitResult); + IResultsCache resultsCache = _host.GetComponent(BuildComponentType.ResultsCache) as IResultsCache; + resultsCache.AddResult(result); + return result; + } + + /// + /// Creates a build result for a request + /// + private BuildResult CreateBuildResult(BuildRequest request, string target, WorkUnitResult workUnitResult) + { + BuildResult result = new BuildResult(request); + result.AddResultsForTarget(target, new TargetResult(new TaskItem[] { }, workUnitResult)); + return result; + } + + /// + /// Creates a build request. + /// + private BuildRequest CreateBuildRequest(int nodeRequestId, int configId) + { + return CreateBuildRequest(nodeRequestId, configId, new string[] { }); + } + + /// + /// Creates a build request. + /// + private BuildRequest CreateBuildRequest(int nodeRequestId, int configId, string[] targets) + { + return CreateBuildRequest(nodeRequestId, configId, targets, _defaultParentRequest); + } + + /// + /// Creates a build request. + /// + private BuildRequest CreateBuildRequest(int nodeRequestId, int configId, string[] targets, BuildRequest parentRequest) + { + return CreateBuildRequest(nodeRequestId, configId, targets, NodeAffinity.Any, parentRequest); + } + + /// + /// Creates a build request. + /// + private BuildRequest CreateBuildRequest(int nodeRequestId, int configId, string[] targets, NodeAffinity nodeAffinity, BuildRequest parentRequest) + { + HostServices hostServices = null; + + if (nodeAffinity != NodeAffinity.Any) + { + hostServices = new HostServices(); + hostServices.SetNodeAffinity(String.Empty, nodeAffinity); + } + + BuildRequest request = new BuildRequest(1 /* submissionId */, nodeRequestId, configId, targets, hostServices, BuildEventContext.Invalid, parentRequest); + return request; + } + + /// + /// Method that fakes the actions done by BuildManager.PerformSchedulingActions + /// + private void MockPerformSchedulingActions(IEnumerable responses, ref int nodeId, ref bool inProcNodeExists) + { + List nodeInfos = new List(); + foreach (ScheduleResponse response in responses) + { + if (response.Action == ScheduleActionType.CreateNode) + { + NodeProviderType nodeType; + if (response.RequiredNodeType == NodeAffinity.InProc || (response.RequiredNodeType == NodeAffinity.Any && !inProcNodeExists)) + { + nodeType = NodeProviderType.InProc; + inProcNodeExists = true; + } + else + { + nodeType = NodeProviderType.OutOfProc; + } + + for (int i = 0; i < response.NumberOfNodesToCreate; i++) + { + nodeInfos.Add(new NodeInfo(nodeId, nodeType)); + nodeId++; + } + } + } + + if (nodeInfos.Count > 0) + { + List moreResponses = new List(_scheduler.ReportNodesCreated(nodeInfos)); + MockPerformSchedulingActions(moreResponses, ref nodeId, ref inProcNodeExists); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TargetBuilder_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetBuilder_Tests.cs new file mode 100644 index 00000000000..d44931c8c33 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetBuilder_Tests.cs @@ -0,0 +1,1609 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for the TargetBuilder with a mock task builder. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext; +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; +using LegacyThreadingData = Microsoft.Build.Execution.LegacyThreadingData; +using System.Threading.Tasks; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// This is the unit test for the TargetBuilder. This particular test is confined to just using the + /// actual TargetBuilder, and uses a mock TaskBuilder on which TargetBuilder depends. + /// + [TestClass] + public class TargetBuilder_Tests : IRequestBuilderCallback + { + /// + /// The component host. + /// + private MockHost _host; + + /// + /// A mock logger for scenario tests. + /// + private MockLogger _mockLogger; + + /// + /// The node request id counter + /// + private int _nodeRequestId; + + /// + /// Callback used to receive exceptions from loggers. Unused here. + /// + /// The exception + public void LoggingException(Exception e) + { + } + + /// + /// Sets up to run tests. Creates the host object. + /// + [TestInitialize] + public void SetUp() + { + _nodeRequestId = 1; + _host = new MockHost(); + _mockLogger = new MockLogger(); + _host.OnLoggingThreadException += this.LoggingException; + } + + /// + /// Executed after all tests are run. + /// + [TestCleanup] + public void TearDown() + { + File.Delete("testProject.proj"); + _mockLogger = null; + _host = null; + } + + /// + /// Runs the constructor. + /// + [TestMethod] + public void TestConstructor() + { + TargetBuilder builder = new TargetBuilder(); + } + + /// + /// Runs a "simple" build with no dependencies and no outputs. + /// + [TestMethod] + public void TestSimpleBuild() + { + ProjectInstance project = CreateTestProject(); + + // The Empty target has no inputs or outputs. + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Empty" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + Assert.IsTrue(result.HasResultsForTarget("Empty")); + Assert.AreEqual(TargetResultCode.Success, result["Empty"].ResultCode); + Assert.AreEqual(0, result["Empty"].Items.Length); + } + + /// + /// Runs a build with a target which depends on one other target. + /// + [TestMethod] + public void TestDependencyBuild() + { + ProjectInstance project = CreateTestProject(); + + // The Baz project depends on the Bar target. Both should succeed. + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Baz" }), cache[1]); + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + + // The result returned from the builder includes only those for the specified targets. + Assert.IsTrue(result.HasResultsForTarget("Baz")); + Assert.IsFalse(result.HasResultsForTarget("Bar")); + Assert.AreEqual(TargetResultCode.Success, result["Baz"].ResultCode); + + // The results cache should have ALL of the results. + IResultsCache resultsCache = (IResultsCache)_host.GetComponent(BuildComponentType.ResultsCache); + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Bar")); + Assert.AreEqual(TargetResultCode.Success, resultsCache.GetResultForRequest(entry.Request)["Bar"].ResultCode); + } + + /// + /// Tests a project with a dependency which will be skipped because its up-to-date. + /// + [TestMethod] + public void TestDependencyBuildWithSkip() + { + ProjectInstance project = CreateTestProject(); + + // DepSkip depends on Skip (which skips) but should succeed itself. + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "DepSkip" }), cache[1]); + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + Assert.IsTrue(result.HasResultsForTarget("DepSkip")); + Assert.IsFalse(result.HasResultsForTarget("Skip")); + Assert.AreEqual(TargetResultCode.Success, result["DepSkip"].ResultCode); + + IResultsCache resultsCache = (IResultsCache)_host.GetComponent(BuildComponentType.ResultsCache); + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("SkipCondition")); + Assert.AreEqual(TargetResultCode.Skipped, resultsCache.GetResultForRequest(entry.Request)["SkipCondition"].ResultCode); + } + + /// + /// This test is currently ignored because the error tasks aren't implemented yet (due to needing the task builder.) + /// + [TestMethod] + public void TestDependencyBuildWithError() + { + ProjectInstance project = CreateTestProject(); + + // The DepError target builds Foo (which succeeds), Skip (which skips) and Error (which fails), and Baz2 + // Baz2 should not run since it came after Error. + // Error tries to build Foo again as an error (which is already built) and Bar, which produces outputs. + // DepError builds Baz as an error, which produces outputs + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + taskBuilder.FailTaskNumber = 3; // Succeed on Foo's one task, and Error's first task, and fail the second. + + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "DepError" }), cache[1]); + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + Assert.IsTrue(result.HasResultsForTarget("DepError")); + Assert.IsFalse(result.HasResultsForTarget("Foo")); + Assert.IsFalse(result.HasResultsForTarget("Skip")); + Assert.IsFalse(result.HasResultsForTarget("Error")); + Assert.IsFalse(result.HasResultsForTarget("Baz2")); + Assert.IsFalse(result.HasResultsForTarget("Bar")); + Assert.IsFalse(result.HasResultsForTarget("Baz")); + + IResultsCache resultsCache = (IResultsCache)_host.GetComponent(BuildComponentType.ResultsCache); + + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Foo")); + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Skip")); + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Error")); + Assert.IsFalse(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Baz2")); + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Bar")); + Assert.IsTrue(resultsCache.GetResultForRequest(entry.Request).HasResultsForTarget("Baz")); + Assert.AreEqual(TargetResultCode.Failure, resultsCache.GetResultForRequest(entry.Request)["DepError"].ResultCode); + Assert.AreEqual(TargetResultCode.Success, resultsCache.GetResultForRequest(entry.Request)["Foo"].ResultCode); + Assert.AreEqual(TargetResultCode.Success, resultsCache.GetResultForRequest(entry.Request)["Skip"].ResultCode); + Assert.AreEqual(TargetResultCode.Failure, resultsCache.GetResultForRequest(entry.Request)["Error"].ResultCode); + Assert.AreEqual(TargetResultCode.Success, resultsCache.GetResultForRequest(entry.Request)["Bar"].ResultCode); + Assert.AreEqual(TargetResultCode.Success, resultsCache.GetResultForRequest(entry.Request)["Baz"].ResultCode); + } + + /// + /// Ensure that skipped targets only infer outputs once + /// + [TestMethod] + public void SkippedTargetsShouldOnlyInferOutputsOnce() + { + MockLogger logger = new MockLogger(); + + string path = FileUtilities.GetTemporaryFile(); + + Thread.Sleep(100); + + string content = String.Format + ( +@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ", + path + ); + + Project p = new Project(XmlReader.Create(new StringReader(content))); + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + + // There should be no duplicates in the list - if there are, then skipped targets are being inferred multiple times + logger.AssertLogContains("[GFA;GFT;DFTA;GAFT]"); + + File.Delete(path); + } + + /// + /// Test empty before targets + /// + [TestMethod] + public void TestLegacyCallTarget() + { + string projectBody = @" + + + + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "CallTarget", "Foo2Target", "FooTarget", "GooTarget" }); + } + + /// + /// BeforeTargets specifies a missing target. Should not warn or error. + /// + [TestMethod] + public void TestBeforeTargetsMissing() + { + string content = @" + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[t]"); + log.AssertLogDoesntContain("MSB4057"); // missing target + log.AssertNoErrors(); + log.AssertNoWarnings(); + } + + /// + /// BeforeTargets specifies a missing target. Should not warn or error. + /// + [TestMethod] + public void TestBeforeTargetsMissingRunsOthers() + { + string content = @" + + + + + + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[t]", "[a]", "[c]"); + log.AssertLogDoesntContain("MSB4057"); // missing target + log.AssertNoErrors(); + log.AssertNoWarnings(); + } + + /// + /// AfterTargets specifies a missing target. Should not warn or error. + /// + [TestMethod] + public void TestAfterTargetsMissing() + { + string content = @" + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[t]"); + log.AssertLogDoesntContain("MSB4057"); // missing target + log.AssertNoErrors(); + log.AssertNoWarnings(); + } + + /// + /// AfterTargets specifies a missing target. Should not warn or error. + /// + [TestMethod] + public void TestAfterTargetsMissingRunsOthers() + { + string content = @" + + + + + + + + + + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[a]", "[t]", "[c]", "[t2]"); + log.AssertLogDoesntContain("MSB4057"); // missing target + log.AssertNoErrors(); + log.AssertNoWarnings(); + } + + /// + /// Test empty before targets + /// + [TestMethod] + public void TestBeforeTargetsEmpty() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask" }); + } + + /// + /// Test single before targets + /// + [TestMethod] + public void TestBeforeTargetsSingle() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "BuildTask" }); + } + + /// + /// Test single before targets on an escaped target + /// + [TestMethod] + public void TestBeforeTargetsEscaped() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build;Me" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "BuildTask" }); + } + + /// + /// Test single before targets + /// + [TestMethod] + public void TestBeforeTargetsSingleWithError() + { + string projectBody = @" + + + + + + + +"; + + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + taskBuilder.FailTaskNumber = 2; // Succeed on BeforeTask, fail on BuildTask + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "BuildTask" }); + } + + /// + /// Test single before targets + /// + [TestMethod] + public void TestBeforeTargetsSingleWithErrorAndParent() + { + string projectBody = @" + + + + + + + + + + + + +"; + + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + taskBuilder.FailTaskNumber = 2; // Succeed on BeforeTask, fail on BuildTask + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "BuildTask", "Error" }); + } + + /// + /// Test multiple before targets + /// + [TestMethod] + public void TestBeforeTargetsWithTwoReferringToOne() + { + string projectBody = @" + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "BeforeTask2", "BuildTask" }); + } + + /// + /// Test multiple before targets + /// + [TestMethod] + public void TestBeforeTargetsWithOneReferringToTwo() + { + string projectBody = @" + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Foo" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "FooTask" }); + } + + /// + /// Test before target on a skipped target + /// + [TestMethod] + public void TestBeforeTargetsSkip() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask" }); + } + + /// + /// Test before target on a skipped target + /// + [TestMethod] + public void TestBeforeTargetsDependencyOrdering() + { + string projectBody = @" + + + + + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildDepTask", "BeforeDepTask", "BeforeTask", "BuildTask" }); + } + + /// + /// Test after target on a skipped target + /// + [TestMethod] + public void TestAfterTargetsEmpty() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask" }); + } + + /// + /// Test after target on a skipped target + /// + [TestMethod] + public void TestAfterTargetsSkip() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "AfterTask" }); + } + + /// + /// Test single before targets + /// + [TestMethod] + public void TestAfterTargetsSingleWithError() + { + string projectBody = @" + + + + + + +"; + + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + taskBuilder.FailTaskNumber = 1; // Fail on BuildTask + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask" }); + } + + /// + /// Test single before targets + /// + [TestMethod] + public void TestAfterTargetsSingleWithErrorAndParent() + { + string projectBody = @" + + + + + + + + + + + + + + + + + + + + +"; + + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + taskBuilder.FailTaskNumber = 2; // Succeed on BuildTask, fail on AfterTask + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "PostBuild" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask", "AfterTask", "Error2" }); + } + + /// + /// Test after target on a normal target + /// + [TestMethod] + public void TestAfterTargetsSingle() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask", "AfterTask" }); + } + + /// + /// Test after target on a target name which needs escaping + /// + [TestMethod] + public void TestAfterTargetsEscaped() + { + string projectBody = @" + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build;Me" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask", "AfterTask" }); + } + + /// + /// Test after target on a skipped target + /// + [TestMethod] + public void TestAfterTargetsWithTwoReferringToOne() + { + string projectBody = @" + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask", "AfterTask", "AfterTask2" }); + } + + /// + /// Test after target on a skipped target + /// + [TestMethod] + public void TestAfterTargetsWithOneReferringToTwo() + { + string projectBody = @" + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Foo" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "FooTask", "AfterTask" }); + } + + /// + /// Test after target on a skipped target + /// + [TestMethod] + public void TestAfterTargetsWithDependencyOrdering() + { + string projectBody = @" + + + + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildDepTask", "BuildTask", "AfterDepTask", "AfterTask" }); + } + + /// + /// Test a complex ordering with depends, before and after targets + /// + [TestMethod] + public void TestComplexOrdering() + { + string projectBody = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildDepTask", "BeforeDepDepTask", "BeforeDepTask", "BeforeTask", "BuildTask", "AfterDepDepTask", "AfterDepTask", "AfterTask" }); + } + + /// + /// Test a complex ordering with depends, before and after targets + /// + [TestMethod] + public void TestComplexOrdering2() + { + string projectBody = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildDepTask", "BeforeDepDepTask", "BeforeBeforeDepTask", "AfterBeforeBeforeDepTask", "BeforeDepTask", "BeforeTask", "AfterBeforeDepDepTask", "AfterBeforeDepTask", "AfterBeforeTask", "BuildTask" }); + } + + /// + /// Test a complex ordering with depends, before and after targets + /// + [TestMethod] + public void TestBeforeAndAfterWithErrorTargets() + { + string projectBody = @" + + + + + + + + + + + + + + + + + + + +"; + + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + taskBuilder.FailTaskNumber = 1; // Fail on BuildTask + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask", "BeforeErrorTargetTask", "ErrorTargetTask", "AfterErrorTargetTask" }); + } + + /// + /// Test after target on a skipped target + /// + [TestMethod] + public void TestBeforeAndAfterOverrides() + { + string projectBody = @" + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BeforeTask", "BuildDepTask", "AfterTask", "BuildTask" }); + } + + /// + /// Test that if before and after targets skip, the main target still runs (bug 476908) + /// + [TestMethod] + public void TestSkippingBeforeAndAfterTargets() + { + string projectBody = @" + + + + + + + + + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), CancellationToken.None).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask" }); + } + + /// + /// Tests that a circular dependency within a CallTarget call correctly propogates the failure. Bug 502570. + /// + [TestMethod] + public void TestCircularDependencyInCallTarget() + { + string projectContents = @" + + + + + + + + + + "; + StringReader reader = new StringReader(projectContents); + Project project = new Project(new XmlTextReader(reader), null, null); + bool success = project.Build(_mockLogger); + Assert.IsFalse(success); + } + + /// + /// Tests that cancel with no entries after building does not fail. + /// + [TestMethod] + public void TestCancelWithNoEntriesAfterBuild() + { + string projectBody = @" + + + +"; + + ProjectInstance project = CreateTestProject(projectBody); + + TargetBuilder builder = (TargetBuilder)_host.GetComponent(BuildComponentType.TargetBuilder); + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestEntry entry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "Build" }), cache[1]); + using (CancellationTokenSource source = new CancellationTokenSource()) + { + BuildResult result = builder.BuildTargets(GetProjectLoggingContext(entry), entry, this, entry.Request.Targets.ToArray(), CreateStandardLookup(project), source.Token).Result; + AssertTaskExecutionOrder(new string[] { "BuildTask" }); + + // This simply should not fail. + source.Cancel(); + } + } + + #region IRequestBuilderCallback Members + + /// + /// We have to have this interface, but it won't be used in this test because we aren't doing MSBuild callbacks. + /// + /// N/A + /// N/A + /// N/A + /// N/A + /// N/A + /// N/A + Task IRequestBuilderCallback.BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented + /// + Task IRequestBuilderCallback.BlockOnTargetInProgress(int blockingRequestId, string blockingTarget) + { + throw new NotImplementedException(); + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.Yield() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.Reacquire() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.EnterMSBuildCallbackState() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.ExitMSBuildCallbackState() + { + } + + #endregion + + /// + /// Verifies the order in which tasks executed. + /// + private void AssertTaskExecutionOrder(string[] tasks) + { + MockTaskBuilder mockBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + Assert.AreEqual(tasks.Length, mockBuilder.ExecutedTasks.Count); + + int currentTask = 0; + foreach (ProjectTaskInstance task in mockBuilder.ExecutedTasks) + { + Assert.IsTrue(String.Equals(task.Name, tasks[currentTask])); + currentTask++; + } + } + + /// + /// Creates a new build request + /// + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + } + + /// + /// Creates a 'Lookup' used to deal with projects. + /// + /// The project for which to create the lookup + /// The lookup + private Lookup CreateStandardLookup(ProjectInstance project) + { + Lookup lookup = new Lookup(new ItemDictionary(project.Items), new PropertyDictionary(project.Properties), null); + return lookup; + } + + /// + /// Creates a test project. + /// + /// The project. + private ProjectInstance CreateTestProject() + { + string projectBodyContents = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + + return CreateTestProject(projectBodyContents); + } + + /// + /// Creates a test project. + /// + private ProjectInstance CreateTestProject(string projectBodyContents) + { + return CreateTestProject(projectBodyContents, String.Empty, String.Empty); + } + + /// + /// Creates a test project. + /// + private ProjectInstance CreateTestProject(string projectBodyContents, string initialTargets, string defaultTargets) + { + string projectFileContents = String.Format("{2}", initialTargets, defaultTargets, projectBodyContents); + + // retries to deal with occasional locking issues where the file can't be written to initially + for (int retries = 0; retries < 5; retries++) + { + try + { + File.Create("testProject.proj").Close(); + break; + } + catch (Exception ex) + { + if (retries < 4) + { + Console.WriteLine(ex.ToString()); + } + else + { + // All the retries have failed. We will now fail with the + // actual problem now instead of with some more difficult-to-understand + // issue later. + throw ex; + } + } + } + + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("testFile", new Dictionary(), "3.5", new string[0], null), "2.0"); + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + + config.Project = project.CreateProjectInstance(); + cache.AddConfiguration(config); + + return config.Project; + } + + /// + /// Creates a project logging context. + /// + /// The entry on which to base the logging context. + /// The context + private ProjectLoggingContext GetProjectLoggingContext(BuildRequestEntry entry) + { + return new ProjectLoggingContext(new NodeLoggingContext(_host, 1, false), entry, null); + } + + /// + /// The mock component host object. + /// + private class MockHost : MockLoggingService, IBuildComponentHost, IBuildComponent + { + #region IBuildComponentHost Members + + /// + /// The config cache + /// + private IConfigCache _configCache; + + /// + /// The logging service + /// + private ILoggingService _loggingService; + + /// + /// The results cache + /// + private IResultsCache _resultsCache; + + /// + /// The request builder + /// + private IRequestBuilder _requestBuilder; + + /// + /// The mock task builder + /// + private ITaskBuilder _taskBuilder; + + /// + /// The target builder + /// + private ITargetBuilder _targetBuilder; + + /// + /// The build parameters + /// + private BuildParameters _buildParameters; + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + private LegacyThreadingData _legacyThreadingData; + + /// + /// Constructor + /// + public MockHost() + { + _buildParameters = new BuildParameters(); + _legacyThreadingData = new LegacyThreadingData(); + + _configCache = new ConfigCache(); + ((IBuildComponent)_configCache).InitializeComponent(this); + + _loggingService = this; + + _resultsCache = new ResultsCache(); + ((IBuildComponent)_resultsCache).InitializeComponent(this); + + _requestBuilder = new RequestBuilder(); + ((IBuildComponent)_requestBuilder).InitializeComponent(this); + + _taskBuilder = new MockTaskBuilder(); + ((IBuildComponent)_taskBuilder).InitializeComponent(this); + + _targetBuilder = new TargetBuilder(); + ((IBuildComponent)_targetBuilder).InitializeComponent(this); + } + + /// + /// Returns the node logging service. We don't distinguish here. + /// + /// The logging service. + public ILoggingService LoggingService + { + get + { + return _loggingService; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + /// + /// Retrieves the name of thoe host. + /// + public string Name + { + get + { + return "TargetBuilder_Tests.MockHost"; + } + } + + /// + /// Returns the build parameters. + /// + public BuildParameters BuildParameters + { + get + { + return _buildParameters; + } + } + + /// + /// Constructs and returns a component of the specified type. + /// + /// The type of component to return + /// The component + public IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.ConfigCache: + return (IBuildComponent)_configCache; + + case BuildComponentType.LoggingService: + return (IBuildComponent)_loggingService; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)_resultsCache; + + case BuildComponentType.RequestBuilder: + return (IBuildComponent)_requestBuilder; + + case BuildComponentType.TaskBuilder: + return (IBuildComponent)_taskBuilder; + + case BuildComponentType.TargetBuilder: + return (IBuildComponent)_targetBuilder; + + default: + throw new ArgumentException("Unexpected type " + type); + } + } + + /// + /// Registers a component factory + /// + public void RegisterFactory(BuildComponentType type, BuildComponentFactoryDelegate factory) + { + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + throw new NotImplementedException(); + } + + /// + /// Shuts down the component + /// + public void ShutdownComponent() + { + throw new NotImplementedException(); + } + + #endregion + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TargetEntry_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetEntry_Tests.cs new file mode 100644 index 00000000000..c461aa02a04 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetEntry_Tests.cs @@ -0,0 +1,1336 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for the TargetEntry with a mock task builder. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd.Logging; +using System.Threading.Tasks; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Test for the TargetEntry class used by the TargetBuilder. This class does most of the + /// actual work to build a target. + /// + [TestClass] + public class TargetEntry_Tests : ITargetBuilderCallback + { + /// + /// The component host. + /// + private MockHost _host; + + /// + /// The node request id counter + /// + private int _nodeRequestId; + + /// + /// Handles exceptions from the logging system. + /// + /// The exception + public void LoggingException(Exception e) + { + } + + /// + /// Called prior to each test. + /// + [TestInitialize] + public void SetUp() + { + _nodeRequestId = 1; + _host = new MockHost(); + _host.OnLoggingThreadException += this.LoggingException; + } + + /// + /// Called after each test is run. + /// + [TestCleanup] + public void TearDown() + { + File.Delete("testProject.proj"); + _host = null; + } + + /// + /// Tests a constructor with a null target. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestConstructorNullTarget() + { + ProjectInstance project = CreateTestProject(true /* Returns enabled */); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry requestEntry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "foo" }), config); + Lookup lookup = new Lookup(new ItemDictionary(project.Items), new PropertyDictionary(project.Properties), null); + TargetEntry entry = new TargetEntry(requestEntry, this, null, lookup, null, _host, false); + } + + /// + /// Tests a constructor with a null lookup. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestConstructorNullLookup() + { + ProjectInstance project = CreateTestProject(true /* Returns enabled */); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry requestEntry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "foo" }), config); + TargetEntry entry = new TargetEntry(requestEntry, this, new TargetSpecification("Empty", null), null, null, _host, false); + } + + /// + /// Tests a constructor with a null host. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestConstructorNullHost() + { + ProjectInstance project = CreateTestProject(true /* Returns enabled */); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + BuildRequestEntry requestEntry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "foo" }), config); + + Lookup lookup = new Lookup(new ItemDictionary(project.Items), new PropertyDictionary(project.Properties), null); + TargetEntry entry = new TargetEntry(requestEntry, this, new TargetSpecification("Empty", null), lookup, null, null, false); + } + + /// + /// Tests a valid constructor call. + /// + [TestMethod] + public void TestConstructorValid() + { + ProjectInstance project = CreateTestProject(true /* Returns enabled */); + TargetEntry entry = CreateStandardTargetEntry(project, "Empty"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + } + + /// + /// Tests incorrect invocation of ExecuteTarget + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestInvalidState_Execution() + { + ProjectInstance project = CreateTestProject(true /* Returns enabled */); + TargetEntry entry = CreateStandardTargetEntry(project, "Empty"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + ExecuteEntry(project, entry); + } + + /// + /// Tests incorrect invocation of GatherResults. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void TestInvalidState_Completed() + { + ProjectInstance project = CreateTestProject(true /* Returns enabled */); + TargetEntry entry = CreateStandardTargetEntry(project, "Empty"); + Assert.AreEqual(entry.State, TargetEntryState.Dependencies); + entry.GatherResults(); + } + + /// + /// Verifies that the dependencies specified for a target are returned by the GetDependencies call. + /// + [TestMethod] + public void TestDependencies() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + TargetEntry entry = CreateStandardTargetEntry(project, "Empty"); + + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + Assert.AreEqual(0, deps.Count); + + entry = CreateStandardTargetEntry(project, "Baz"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + Assert.AreEqual(1, deps.Count); + IEnumerator depsEnum = deps.GetEnumerator(); + depsEnum.MoveNext(); + Assert.AreEqual("Bar", depsEnum.Current.TargetName); + + entry = CreateStandardTargetEntry(project, "Baz2"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + Assert.AreEqual(2, deps.Count); + depsEnum = deps.GetEnumerator(); + depsEnum.MoveNext(); + Assert.AreEqual("Bar", depsEnum.Current.TargetName); + depsEnum.MoveNext(); + Assert.AreEqual("Foo", depsEnum.Current.TargetName); + } + } + + /// + /// Tests normal target execution and verifies the tasks expected to be executed are. + /// + [TestMethod] + public void TestExecution() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + taskBuilder.Reset(); + TargetEntry entry = CreateStandardTargetEntry(project, "Empty"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + ExecuteEntry(project, entry); + Assert.AreEqual(0, taskBuilder.ExecutedTasks.Count); + + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Baz"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + ExecuteEntry(project, entry); + Assert.AreEqual(2, taskBuilder.ExecutedTasks.Count); + Assert.AreEqual("BazTask1", taskBuilder.ExecutedTasks[0].Name); + Assert.AreEqual("BazTask2", taskBuilder.ExecutedTasks[1].Name); + + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Baz2"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + ExecuteEntry(project, entry); + Assert.AreEqual(3, taskBuilder.ExecutedTasks.Count); + Assert.AreEqual("Baz2Task1", taskBuilder.ExecutedTasks[0].Name); + Assert.AreEqual("Baz2Task2", taskBuilder.ExecutedTasks[1].Name); + Assert.AreEqual("Baz2Task3", taskBuilder.ExecutedTasks[2].Name); + } + } + + /// + /// Executes various cases where tasks cause an error. Verifies that the expected tasks + /// executed. + /// + [TestMethod] + public void TestExecutionWithErrors() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + taskBuilder.Reset(); + TargetEntry entry = CreateStandardTargetEntry(project, "Error"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + taskBuilder.FailTaskNumber = 1; + ExecuteEntry(project, entry); + Assert.AreEqual(3, taskBuilder.ExecutedTasks.Count); + Assert.AreEqual("ErrorTask1", taskBuilder.ExecutedTasks[0].Name); + Assert.AreEqual("ErrorTask2", taskBuilder.ExecutedTasks[1].Name); + Assert.AreEqual("ErrorTask3", taskBuilder.ExecutedTasks[2].Name); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Error"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + taskBuilder.FailTaskNumber = 2; + ExecuteEntry(project, entry); + Assert.AreEqual(2, taskBuilder.ExecutedTasks.Count); + Assert.AreEqual("ErrorTask1", taskBuilder.ExecutedTasks[0].Name); + Assert.AreEqual("ErrorTask2", taskBuilder.ExecutedTasks[1].Name); + Assert.AreEqual(TargetEntryState.ErrorExecution, entry.State); + Assert.AreEqual(2, entry.GetErrorTargets(GetProjectLoggingContext(entry.RequestEntry)).Count); + + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Error"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + taskBuilder.FailTaskNumber = 3; + ExecuteEntry(project, entry); + Assert.AreEqual(3, taskBuilder.ExecutedTasks.Count); + Assert.AreEqual("ErrorTask1", taskBuilder.ExecutedTasks[0].Name); + Assert.AreEqual("ErrorTask2", taskBuilder.ExecutedTasks[1].Name); + Assert.AreEqual("ErrorTask3", taskBuilder.ExecutedTasks[2].Name); + Assert.AreEqual(TargetEntryState.ErrorExecution, entry.State); + Assert.AreEqual(2, entry.GetErrorTargets(GetProjectLoggingContext(entry.RequestEntry)).Count); + + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Error"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + ExecuteEntry(project, entry); + Assert.AreEqual(3, taskBuilder.ExecutedTasks.Count); + Assert.AreEqual("ErrorTask1", taskBuilder.ExecutedTasks[0].Name); + Assert.AreEqual("ErrorTask2", taskBuilder.ExecutedTasks[1].Name); + Assert.AreEqual("ErrorTask3", taskBuilder.ExecutedTasks[2].Name); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + } + } + + /// + /// Tests that the dependencies returned can also be built and that their entries in the lookup + /// are appropriately aggregated into the parent target entry. + /// + [TestMethod] + public void TestBuildDependencies() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + // Empty target doesn't produce any items of its own, the Compile items should be in it. + TargetEntry entry = CreateStandardTargetEntry(project, "Baz2"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + foreach (TargetSpecification target in deps) + { + TargetEntry depEntry = CreateStandardTargetEntry(project, target.TargetName, entry); + depEntry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, depEntry); + depEntry.GatherResults(); + } + + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + Assert.AreEqual(2, entry.Lookup.GetItems("Compile").Count); + Assert.AreEqual(1, entry.Lookup.GetItems("FooTask1_Item").Count); + Assert.AreEqual(1, entry.Lookup.GetItems("BarTask1_Item").Count); + } + } + + /// + /// Tests a variety of situations returning various results + /// + [TestMethod] + public void TestGatherResults() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + // Empty target doesn't produce any items of its own, the Compile items should be in it. + // This target has no outputs. + TargetEntry entry = CreateStandardTargetEntry(project, "Empty"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + TargetResult results = entry.GatherResults(); + Assert.AreEqual(2, entry.Lookup.GetItems("Compile").Count); + Assert.AreEqual(0, results.Items.Length); + Assert.AreEqual(TargetResultCode.Success, results.ResultCode); + + // Foo produces one item of its own and has an output + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Foo"); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + results = entry.GatherResults(); + Assert.AreEqual(2, entry.Lookup.GetItems("Compile").Count); + Assert.AreEqual(1, entry.Lookup.GetItems("FooTask1_Item").Count); + + if (returnsEnabledForThisProject) + { + // If returns are enabled, since this is a target with "Outputs", they won't + // be returned. + Assert.AreEqual(0, results.Items.Length); + } + else + { + Assert.AreEqual(1, results.Items.Length); + Assert.AreEqual("foo.o", results.Items[0].ItemSpec); + } + + Assert.AreEqual(TargetResultCode.Success, results.ResultCode); + + // Skip produces outputs but is up to date, so should record success + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Skip"); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + results = entry.GatherResults(); + + if (returnsEnabledForThisProject) + { + Assert.AreEqual(0, results.Items.Length); + } + else + { + Assert.AreEqual(1, results.Items.Length); + Assert.AreEqual("testProject.proj", results.Items[0].ItemSpec); + } + + Assert.AreEqual(TargetResultCode.Success, results.ResultCode); + + // SkipCondition is skipped due to condition. + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "SkipCondition"); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + results = entry.GatherResults(); + Assert.AreEqual(TargetResultCode.Skipped, results.ResultCode); + + // DepSkip produces no outputs and calls Empty and Skip. The result should be success + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "DepSkip"); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + results = entry.GatherResults(); + Assert.AreEqual(TargetResultCode.Success, results.ResultCode); + + // DepSkip2 calls Skip. The result should be success because both DepSkip and Skip are up-to-date. + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "DepSkip2"); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.Completed, entry.State); + results = entry.GatherResults(); + Assert.AreEqual(TargetResultCode.Success, results.ResultCode); + + // Error target should produce error results + taskBuilder.Reset(); + entry = CreateStandardTargetEntry(project, "Error"); + Assert.AreEqual(TargetEntryState.Dependencies, entry.State); + deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + Assert.AreEqual(TargetEntryState.Execution, entry.State); + taskBuilder.FailTaskNumber = 2; + ExecuteEntry(project, entry); + Assert.AreEqual(TargetEntryState.ErrorExecution, entry.State); + entry.GetErrorTargets(GetProjectLoggingContext(entry.RequestEntry)); + results = entry.GatherResults(); + Assert.AreEqual(TargetResultCode.Failure, results.ResultCode); + } + } + + /// + /// Tests that multiple outputs are allowed + /// + [TestMethod] + public void TestMultipleOutputs() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "MultipleOutputsNoReturns"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + + if (returnsEnabledForThisProject) + { + // If returns are enabled, since this is a target with "Outputs", they won't + // be returned. + Assert.AreEqual(0, results.Items.Length); + } + else + { + Assert.AreEqual(2, results.Items.Length); + } + } + } + + /// + /// Tests that multiple return values are still passed through, even when there is no Outputs specified. + /// + [TestMethod] + public void TestMultipleReturnsNoOutputs() + { + ProjectInstance project = CreateTestProject(true /* returns are enabled */); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "MultipleReturnsNoOutputs"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + + Assert.AreEqual(3, results.Items.Length); + } + + /// + /// Tests that multiple return values are still passed through, and verifies that when both Outputs and Returns + /// are specified, Returns is what controls the return value of the target. + /// + [TestMethod] + public void TestMultipleReturnsWithOutputs() + { + ProjectInstance project = CreateTestProject(true /* returns are enabled */); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "MultipleReturnsWithOutputs"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + + Assert.AreEqual(3, results.Items.Length); + } + + /// + /// Tests that duplicate outputs are allowed + /// + [TestMethod] + public void TestDuplicateOutputs() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "DupeOutputs"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + Assert.AreEqual(1, results.Items.Length); + } + } + + /// + /// Tests that duplicate outputs are not trimmed under the false trim condition + /// + [TestMethod] + public void TestKeepDuplicateOutputsTrue() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "DupeOutputsKeep"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + Assert.AreEqual(2, results.Items.Length); + } + } + + /// + /// Tests that duplicate outputs are trimmed under the false keep condition + /// + [TestMethod] + public void TestKeepDuplicateOutputsFalse() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "DupeOutputsNoKeep"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + Assert.AreEqual(1, results.Items.Length); + } + } + + /// + /// Tests that duplicate outputs are trimmed if they have the same metadata + /// + [TestMethod] + public void TestKeepDuplicateOutputsSameMetadata() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "DupeOutputsSameMetadata"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + Assert.AreEqual(1, results.Items.Length); + } + } + + /// + /// Tests that duplicate outputs are not trimmed if they have different metadata + /// + [TestMethod] + public void TestKeepDuplicateOutputsDiffMetadata() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + ProjectInstance project = CreateTestProject(returnsEnabledForThisProject); + MockTaskBuilder taskBuilder = (MockTaskBuilder)_host.GetComponent(BuildComponentType.TaskBuilder); + + TargetEntry entry = CreateStandardTargetEntry(project, "DupeOutputsDiffMetadata"); + ICollection deps = entry.GetDependencies(GetProjectLoggingContext(entry.RequestEntry)); + ExecuteEntry(project, entry); + TargetResult results = entry.GatherResults(); + Assert.AreEqual(4, results.Items.Length); + } + } + + /// + /// Tests that metadata references in target outputs are correctly expanded + /// + [TestMethod] + public void TestMetadataReferenceInTargetOutputs() + { + bool[] returnsEnabled = new bool[] { true, false }; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + string content = @" + + + + + + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + if (returnsEnabledForThisProject) + { + log.AssertLogContains("item2"); + log.AssertLogDoesntContain("item1;item2"); + } + else + { + log.AssertLogContains("item1;item2"); + } + } + } + + /// + /// Tests that we get the target ouputs correctly. + /// + [TestMethod] + public void TestTargetOutputsOnFinishedEvent() + { + bool[] returnsEnabled = new bool[] { false, true }; + + string loggingVariable = Environment.GetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING"); + Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", "1"); + try + { + TargetLoggingContext.EnableTargetOutputLogging = true; + + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + string content = @" + + + + + + + + + + + + + + + "; + + // Only log critical event is false by default + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + Assert.IsTrue(log.TargetFinishedEvents.Count == 3); + + TargetFinishedEventArgs targeta = log.TargetFinishedEvents[2]; + TargetFinishedEventArgs targetb = log.TargetFinishedEvents[0]; + TargetFinishedEventArgs targetc = log.TargetFinishedEvents[1]; + + Assert.IsNotNull(targeta); + Assert.IsNotNull(targetb); + Assert.IsNotNull(targetc); + + Assert.IsTrue(targeta.TargetName.Equals("a", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetb.TargetName.Equals("b", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetc.TargetName.Equals("c", StringComparison.OrdinalIgnoreCase)); + + IEnumerable targetOutputsA = targeta.TargetOutputs; + IEnumerable targetOutputsB = targetb.TargetOutputs; + IEnumerable targetOutputsC = targetc.TargetOutputs; + + Assert.IsNull(targetOutputsA); + Assert.IsNotNull(targetOutputsB); + + if (returnsEnabledForThisProject) + { + Assert.IsNull(targetOutputsC); + } + else + { + Assert.IsNotNull(targetOutputsC); + } + + List outputListB = new List(); + foreach (ITaskItem item in targetOutputsB) + { + outputListB.Add(item); + } + + Assert.IsTrue(outputListB.Count == 1); + Assert.IsTrue(outputListB[0].ItemSpec.Equals("item1", StringComparison.OrdinalIgnoreCase)); + + if (!returnsEnabledForThisProject) + { + List outputListC = new List(); + foreach (ITaskItem item in targetOutputsC) + { + outputListC.Add(item); + } + + Assert.IsTrue(outputListC.Count == 1); + + Assert.IsTrue(outputListC[0].ItemSpec.Equals("item2", StringComparison.OrdinalIgnoreCase)); + } + } + } + finally + { + TargetLoggingContext.EnableTargetOutputLogging = false; + Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", loggingVariable); + } + } + + /// + /// Tests that we get no target outputs when the environment variable is not set + /// + [TestMethod] + public void TestTargetOutputsOnFinishedEventNoVariableSet() + { + bool[] returnsEnabled = new bool[] { true, false }; + + string loggingVariable = Environment.GetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING"); + Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", null); + try + { + foreach (bool returnsEnabledForThisProject in returnsEnabled) + { + string content = @" + + + + + + + + + + + + + + + "; + + // Only log critical event is false by default + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + Assert.IsTrue(log.TargetFinishedEvents.Count == 3); + + TargetFinishedEventArgs targeta = log.TargetFinishedEvents[2]; + TargetFinishedEventArgs targetb = log.TargetFinishedEvents[0]; + TargetFinishedEventArgs targetc = log.TargetFinishedEvents[1]; + + Assert.IsNotNull(targeta); + Assert.IsNotNull(targetb); + Assert.IsNotNull(targetc); + + Assert.IsTrue(targeta.TargetName.Equals("a", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetb.TargetName.Equals("b", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(targetc.TargetName.Equals("c", StringComparison.OrdinalIgnoreCase)); + + IEnumerable targetOutputsA = targeta.TargetOutputs; + IEnumerable targetOutputsB = targetb.TargetOutputs; + IEnumerable targetOutputsC = targetc.TargetOutputs; + + Assert.IsNull(targetOutputsA); + Assert.IsNull(targetOutputsB); + Assert.IsNull(targetOutputsC); + } + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDTARGETOUTPUTLOGGING", loggingVariable); + } + } + + /// + /// Make sure that if an after target fails that the build result is reported as failed. + /// + [TestMethod] + public void AfterTargetsShouldReportFailedBuild() + { + // Since we're creating our own BuildManager, we need to make sure that the default + // one has properly relinquished the inproc node + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)BuildManager.DefaultBuildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + if (nodeProviderInProc != null) + { + nodeProviderInProc.Dispose(); + } + + string content = @" + + + + + + + + + + "; + BuildManager manager = null; + try + { + MockLogger logger = new MockLogger(); + List loggers = new List(); + loggers.Add(logger); + + ProjectCollection collection = new ProjectCollection(); + Project project = new Project(XmlReader.Create(new StringReader(content)), (IDictionary)null, "4.0", collection); + project.FullPath = FileUtilities.GetTemporaryFile(); + project.Save(); + File.Delete(project.FullPath); + + BuildParameters parameters = new BuildParameters(collection); + parameters.Loggers = loggers; + parameters.ShutdownInProcNodeOnBuildFinish = true; + + BuildRequestData data = new BuildRequestData(project.FullPath, new Dictionary(), "4.0", new string[] { }, null); + manager = new BuildManager(); + BuildResult result = manager.Build(parameters, data); + + // Make sure the overall result is failed + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + + // Expect the build target to pass + Assert.IsTrue(result.ResultsByTarget["Build"].ResultCode == TargetResultCode.Success); + } + finally + { + // and we should clean up after ourselves, too. + if (manager != null) + { + NodeProviderInProc inProcNodeProvider = ((IBuildComponentHost)manager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + + if (inProcNodeProvider != null) + { + inProcNodeProvider.Dispose(); + } + } + } + } + + /// + /// Tests that with an invalid target specification (inputs but no outputs) we + /// still raise the TargetFinished event. + /// + [TestMethod] + public void TestTargetFinishedRaisedOnInvalidTarget() + { + string content = @" + + + + + + "; + + // Only log critical event is false by default + MockLogger log = Helpers.BuildProjectWithNewOMExpectFailure(content, allowTaskCrash: true); + + Assert.AreEqual(1, log.TargetFinishedEvents.Count); + } + + #region ITargetBuilderCallback Members + + /// + /// Empty impl + /// + Task ITargetBuilderCallback.LegacyCallTarget(string[] targets, bool continueOnError, ElementLocation referenceLocation) + { + throw new NotImplementedException(); + } + + #endregion + + #region IRequestBuilderCallback Members + + /// + /// Empty impl + /// + Task IRequestBuilderCallback.BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + Task IRequestBuilderCallback.BlockOnTargetInProgress(int blockingRequestId, string blockingTarget) + { + throw new NotImplementedException(); + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.Yield() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.Reacquire() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.EnterMSBuildCallbackState() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.ExitMSBuildCallbackState() + { + } + + #endregion + + /// + /// Executes the specified entry with the specified project. + /// + private void ExecuteEntry(ProjectInstance project, TargetEntry entry) + { + ITaskBuilder taskBuilder = _host.GetComponent(BuildComponentType.TaskBuilder) as ITaskBuilder; + + // GetAwaiter().GetResult() will flatten any AggregateException throw by the task. + entry.ExecuteTarget(taskBuilder, entry.RequestEntry, GetProjectLoggingContext(entry.RequestEntry), CancellationToken.None).GetAwaiter().GetResult(); + ((IBuildComponent)taskBuilder).ShutdownComponent(); + } + + /// + /// Creates a new build request + /// + private BuildRequest CreateNewBuildRequest(int configurationId, string[] targets) + { + return new BuildRequest(1 /* submissionId */, _nodeRequestId++, configurationId, targets, null, BuildEventContext.Invalid, null); + } + + /// + /// Creates a TargetEntry from a project and the specified target name. + /// + /// The project object. + /// The name of a target within the specified project. + /// The new target entry + private TargetEntry CreateStandardTargetEntry(ProjectInstance project, string targetName) + { + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + config.Project = project; + BuildRequestEntry requestEntry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[] { "foo" }), config); + + Lookup lookup = new Lookup(new ItemDictionary(project.Items), new PropertyDictionary(project.Properties), null); + TargetEntry entry = new TargetEntry(requestEntry, this, new TargetSpecification(targetName, project.Targets[targetName].Location), lookup, null, _host, false); + return entry; + } + + /// + /// Creates a target entry object. + /// + /// The project object + /// The target object + /// The parent entry. + /// The new target entry + private TargetEntry CreateStandardTargetEntry(ProjectInstance project, string target, TargetEntry baseEntry) + { + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("foo", new Dictionary(), "foo", new string[0], null), "2.0"); + config.Project = project; + BuildRequestEntry requestEntry = new BuildRequestEntry(CreateNewBuildRequest(1, new string[1] { "foo" }), config); + TargetEntry entry = new TargetEntry(requestEntry, this, new TargetSpecification(target, project.Targets[target].Location), baseEntry.Lookup, baseEntry, _host, false); + return entry; + } + + /// + /// Creates the test project + /// + /// The project object. + private ProjectInstance CreateTestProject(bool returnsAttributeEnabled) + { + string returnsAttributeName = returnsAttributeEnabled ? "Returns" : "Outputs"; + + string projectFileContents = @" + + + + + + + + + + + + + + + + + + + 1 + + + 2 + + + 1 + + + 2 + + + + + + 1 + + + 1 + + + + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + + if (returnsAttributeEnabled) + { + projectFileContents += @" + + + "; + } + + projectFileContents += @" + + + + + + + + + + + + + + + + "; + + FileStream stream = File.Create("testProject.proj"); + stream.Close(); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + return project.CreateProjectInstance(); + } + + /// + /// Returns a project logging context. + /// + /// The build request entry. + /// The project logging context. + private ProjectLoggingContext GetProjectLoggingContext(BuildRequestEntry entry) + { + return new ProjectLoggingContext(new NodeLoggingContext(_host, 1, false), entry, null); + } + + /// + /// The mock component host. + /// + private class MockHost : MockLoggingService, IBuildComponentHost, IBuildComponent + { + #region IBuildComponentHost Members + + /// + /// The configuration cache. + /// + private IConfigCache _configCache; + + /// + /// The logging service. + /// + private ILoggingService _loggingService; + + /// + /// The results cache. + /// + private IResultsCache _resultsCache; + + /// + /// The request builder. + /// + private IRequestBuilder _requestBuilder; + + /// + /// The mock task builder + /// + private ITaskBuilder _taskBuilder; + + /// + /// The build parameters. + /// + private BuildParameters _buildParameters; + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + private LegacyThreadingData _legacyThreadingData; + + /// + /// Constructor + /// + public MockHost() + { + _buildParameters = new BuildParameters(); + _legacyThreadingData = new LegacyThreadingData(); + + _configCache = new ConfigCache(); + ((IBuildComponent)_configCache).InitializeComponent(this); + + _loggingService = this; + + _resultsCache = new ResultsCache(); + ((IBuildComponent)_resultsCache).InitializeComponent(this); + + _requestBuilder = new RequestBuilder(); + ((IBuildComponent)_requestBuilder).InitializeComponent(this); + + _taskBuilder = new MockTaskBuilder(); + ((IBuildComponent)_taskBuilder).InitializeComponent(this); + } + + /// + /// Gets the build-specific logging service. + /// + /// The logging service + public ILoggingService LoggingService + { + get + { + return _loggingService; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + /// + /// Retrieves the name of the host. + /// + public string Name + { + get + { + return "TargetEntry_Tests.MockHost"; + } + } + + /// + /// Gets the build parameters. + /// + public BuildParameters BuildParameters + { + get + { + return _buildParameters; + } + } + + /// + /// Gets the component of the specified type. + /// + /// The type of component to return. + /// The component + public IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.ConfigCache: + return (IBuildComponent)_configCache; + + case BuildComponentType.LoggingService: + return (IBuildComponent)_loggingService; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)_resultsCache; + + case BuildComponentType.RequestBuilder: + return (IBuildComponent)_requestBuilder; + + case BuildComponentType.TaskBuilder: + return (IBuildComponent)_taskBuilder; + + default: + throw new ArgumentException("Unexpected type " + type); + } + } + + /// + /// Register a component factory. + /// + public void RegisterFactory(BuildComponentType type, BuildComponentFactoryDelegate factory) + { + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + throw new NotImplementedException(); + } + + /// + /// Shuts down the component + /// + public void ShutdownComponent() + { + throw new NotImplementedException(); + } + + #endregion + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TargetResult_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetResult_Tests.cs new file mode 100644 index 00000000000..8fb7336e4c0 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetResult_Tests.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the TargetResult class. +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Unittest; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for the target result test. + /// + [TestClass] + public class TargetResult_Tests + { + /// + /// Tests a constructor with no items. + /// + [TestMethod] + public void TestConstructorNoItems() + { + TargetResult result = new TargetResult(new TaskItem[] { }, TestUtilities.GetStopWithErrorResult()); + Assert.AreEqual(0, result.Items.Length); + Assert.IsNull(result.Exception); + Assert.AreEqual(TargetResultCode.Failure, result.ResultCode); + } + + /// + /// Tests a constructor with items. + /// + [TestMethod] + public void TestConstructorWithItems() + { + TaskItem item = new TaskItem("foo", "bar.proj"); + TargetResult result = new TargetResult(new TaskItem[] { item }, TestUtilities.GetStopWithErrorResult()); + Assert.AreEqual(1, result.Items.Length); + Assert.AreEqual(item.ItemSpec, result.Items[0].ItemSpec); + Assert.AreEqual(TargetResultCode.Failure, result.ResultCode); + } + + /// + /// Tests a constructor with a null item array passed. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestConstructorNullItems() + { + TargetResult result = new TargetResult(null, TestUtilities.GetStopWithErrorResult()); + } + + /// + /// Tests a constructor with an exception passed. + /// + [TestMethod] + public void TestConstructorWithException() + { + TaskItem item = new TaskItem("foo", "bar.proj"); + TargetResult result = new TargetResult(new TaskItem[] { item }, TestUtilities.GetStopWithErrorResult(new ArgumentException())); + Assert.AreEqual(1, result.Items.Length); + Assert.IsNotNull(result.Exception); + Assert.AreEqual(typeof(ArgumentException), result.Exception.GetType()); + Assert.AreEqual(TargetResultCode.Failure, result.ResultCode); + } + + /// + /// Tests a constructor with a null exception passed. + /// + [TestMethod] + public void TestConstructorWithExceptionNull() + { + TaskItem item = new TaskItem("foo", "bar.proj"); + TargetResult result = new TargetResult(new TaskItem[] { item }, TestUtilities.GetStopWithErrorResult()); + Assert.AreEqual(1, result.Items.Length); + Assert.IsNull(result.Exception); + Assert.AreEqual(TargetResultCode.Failure, result.ResultCode); + } + + /// + /// Tests serialization with no exception in the result. + /// + [TestMethod] + public void TestTranslationNoException() + { + TaskItem item = new TaskItem("foo", "bar.proj"); + item.SetMetadata("a", "b"); + + TargetResult result = new TargetResult(new TaskItem[] { item }, TestUtilities.GetStopWithErrorResult()); + + ((INodePacketTranslatable)result).Translate(TranslationHelpers.GetWriteTranslator()); + TargetResult deserializedResult = TargetResult.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(result.ResultCode, deserializedResult.ResultCode); + Assert.IsTrue(TranslationHelpers.CompareCollections(result.Items, deserializedResult.Items, TaskItemComparer.Instance)); + Assert.IsTrue(TranslationHelpers.CompareExceptions(result.Exception, deserializedResult.Exception)); + } + + /// + /// Tests serialization with an exception in the result. + /// + [TestMethod] + public void TestTranslationWithException() + { + TaskItem item = new TaskItem("foo", "bar.proj"); + item.SetMetadata("a", "b"); + + TargetResult result = new TargetResult(new TaskItem[] { item }, TestUtilities.GetStopWithErrorResult(new BuildAbortedException())); + + ((INodePacketTranslatable)result).Translate(TranslationHelpers.GetWriteTranslator()); + TargetResult deserializedResult = TargetResult.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(result.ResultCode, deserializedResult.ResultCode); + Assert.IsTrue(TranslationHelpers.CompareCollections(result.Items, deserializedResult.Items, TaskItemComparer.Instance)); + Assert.IsTrue(TranslationHelpers.CompareExceptions(result.Exception, deserializedResult.Exception)); + } + + /// + /// Test GetCacheDirectory is resilient to paths with strings that would normally make string.format to throw a FormatException + /// + [TestMethod] + public void TestGetCacheDirectory() + { + string oldTmp = Environment.GetEnvironmentVariable("TMP"); + + try + { + Environment.SetEnvironmentVariable("TMP", "C:\\}"); + string path1 = TargetResult.GetCacheDirectory(2, "Blah"); + + Environment.SetEnvironmentVariable("TMP", "C:\\{"); + string path2 = TargetResult.GetCacheDirectory(2, "Blah"); + } + finally + { + Environment.SetEnvironmentVariable("TMP", oldTmp); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs new file mode 100644 index 00000000000..790c4ee2034 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs @@ -0,0 +1,912 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using System.Text; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using System.Threading; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + [TestClass] + public class TargetUpToDateChecker_Tests + { + private MockHost _mockHost; + + [TestInitialize] + public void SetUp() + { + _mockHost = new MockHost(); + } + + [TestCleanup] + public void TearDown() + { + // Remove any temp files that have been created by each test + ObjectModelHelpers.DeleteTempProjectDirectory(); + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + [TestMethod] + public void EmptyItemSpecInTargetInputs() + { + MockLogger ml = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + + + + '%(NonExistentMetadata)')"" + Outputs='foo.exe'> + + + ")))); + + bool success = p.Build(new string[] { "Build" }, new ILogger[] { ml }); + + Assert.IsTrue(success); + + // It should have actually skipped the "Build" target since there were no inputs. + ml.AssertLogDoesntContain("Running Build target"); + } + + /// + /// Verify missing output metadata does not cause errors. + /// + [TestMethod] + public void EmptyItemSpecInTargetOutputs() + { + MockLogger ml = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + '%(OutputFile)');@(TASKXML->'%(PasFile)');""> + + + + + bcc32task.cs + + + ccc32task.pas + + + ")))); + + bool success = p.Build("Build", new ILogger[] { ml }); + + Assert.IsTrue(success); + + // It should have actually skipped the "Build" target since some output metadata was missing + ml.AssertLogContains("Running Build target"); + + ml = new MockLogger(); + p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( + @" + '%(OutputFile)');@(TASKXML->'%(PasFile)');""> + + + + + + + + + ")))); + + success = p.Build("Build", new ILogger[] { ml }); + + Assert.IsTrue(success); + + // It should have actually skipped the "Build" target since some output metadata was missing + ml.AssertLogDoesntContain("Running Build target"); + } + + + /// + /// Tests this case: + /// + /// + /// + /// If Items = [a.cs;b.cs], and only b.cs is out of date w/r/t its + /// correlated output b.dll, then we should only build "b" incrementally. + /// + [TestMethod] + public void MetaInputAndInputItemThatCorrelatesWithOutputItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + string inputs = "@(Items);c.cs"; + string outputs = "@(Items->'%(Filename).dll')"; + FileWriteInfo[] filesToAnalyze = new FileWriteInfo[] + { + new FileWriteInfo("a.cs", _yesterday), + new FileWriteInfo("a.dll", _today), + new FileWriteInfo("b.cs", _today), + new FileWriteInfo("b.dll", _yesterday), + new FileWriteInfo("c.cs", _twoDaysAgo) + }; + + List items = new List(); + items.Add(new ProjectItemInstance(project, "Items", "a.cs", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Items", "b.cs", project.FullPath)); + + ItemDictionary itemsByName = new ItemDictionary(); + itemsByName.ImportItems(items); + + DependencyAnalysisResult result = PerformDependencyAnalysisTestHelper(filesToAnalyze, itemsByName, inputs, outputs); + + Assert.AreEqual(DependencyAnalysisResult.IncrementalBuild, result, "Should only build partially."); + } + + /// + /// Tests this case: + /// + /// + /// + /// If Items = [a.cs;b.cs;c.cs], and only b.cs is out of date w/r/t its + /// correlated outputs (dll or xml), then we should only build "b" incrementally. + /// + [TestMethod] + public void InputItemThatCorrelatesWithMultipleTransformOutputItems() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + string inputs = "@(Items)"; + string outputs = "@(Items->'%(Filename).dll');@(Items->'%(Filename).xml')"; + + FileWriteInfo[] filesToAnalyze = new FileWriteInfo[] + { + new FileWriteInfo("a.cs", _yesterday), + new FileWriteInfo("a.dll", _today), + new FileWriteInfo("a.xml", _today), + new FileWriteInfo("b.cs", _yesterday), + new FileWriteInfo("b.dll", _twoDaysAgo), + new FileWriteInfo("b.xml", _today), + new FileWriteInfo("c.cs", _yesterday), + new FileWriteInfo("c.dll", _today), + new FileWriteInfo("c.xml", _today) + }; + + List items = new List(); + items.Add(new ProjectItemInstance(project, "Items", "a.cs", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Items", "b.cs", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Items", "c.cs", project.FullPath)); + + ItemDictionary itemsByName = new ItemDictionary(); + itemsByName.ImportItems(items); + + DependencyAnalysisResult result = PerformDependencyAnalysisTestHelper(filesToAnalyze, itemsByName, inputs, outputs); + + Assert.AreEqual(DependencyAnalysisResult.IncrementalBuild, result, "Should only build partially."); + } + + /// + /// Tests this case: + /// + /// + /// + /// If Items = [a.cs;b.cs;c.cs], and only b.cs is out of date w/r/t its + /// correlated outputs (dll or xml), then we should only build "b" incrementally. + /// + [TestMethod] + public void MultiInputItemsThatCorrelatesWithMultipleTransformOutputItems() + { + Console.WriteLine("MultiInputItemsThatCorrelatesWithMultipleTransformOutputItems"); + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + string inputs = "@(Items);@(MoreItems)"; + string outputs = "@(Items->'%(Filename).dll');@(MoreItems->'%(Filename).xml')"; + + FileWriteInfo[] filesToAnalyze = new FileWriteInfo[] + { + new FileWriteInfo("a.cs", _yesterday), + new FileWriteInfo("a.txt", _yesterday), + new FileWriteInfo("a.dll", _today), + new FileWriteInfo("a.xml", _today), + new FileWriteInfo("b.cs", _yesterday), + new FileWriteInfo("b.txt", _yesterday), + new FileWriteInfo("b.dll", _twoDaysAgo), + new FileWriteInfo("b.xml", _today), + new FileWriteInfo("c.cs", _yesterday), + new FileWriteInfo("c.txt", _yesterday), + new FileWriteInfo("c.dll", _today), + new FileWriteInfo("c.xml", _today) + }; + + List items = new List(); + items.Add(new ProjectItemInstance(project, "Items", "a.cs", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Items", "b.cs", project.FullPath)); + items.Add(new ProjectItemInstance(project, "Items", "c.cs", project.FullPath)); + items.Add(new ProjectItemInstance(project, "MoreItems", "a.txt", project.FullPath)); + items.Add(new ProjectItemInstance(project, "MoreItems", "b.txt", project.FullPath)); + items.Add(new ProjectItemInstance(project, "MoreItems", "c.txt", project.FullPath)); + + ItemDictionary itemsByName = new ItemDictionary(); + itemsByName.ImportItems(items); + + ItemDictionary changedTargetInputs = new ItemDictionary(); + ItemDictionary upToDateTargetInputs = new ItemDictionary(); + DependencyAnalysisResult result = PerformDependencyAnalysisTestHelper(filesToAnalyze, itemsByName, inputs, outputs, out changedTargetInputs, out upToDateTargetInputs); + + foreach (ProjectItemInstance itemInstance in changedTargetInputs) + { + Console.WriteLine("Changed: {0}:{1}", itemInstance.ItemType, itemInstance.EvaluatedInclude); + } + + Assert.AreEqual(DependencyAnalysisResult.IncrementalBuild, result, "Should only build partially."); + + // Even though they were all up to date, we still expect to see an empty marker + // so that lookups can correctly *not* find items of that type + Assert.IsTrue(changedTargetInputs.HasEmptyMarker("MoreItems")); + } + + [TestMethod] + public void InputItemsTransformedToDifferentNumberOfOutputsFewer() + { + Console.WriteLine("InputItemsTransformedToDifferentNumberOfOutputsFewer"); + MockLogger logger = new MockLogger(); + string projectText = ObjectModelHelpers.CleanupFileContents(@" + + + + SomeMetaThing + SomeMetaThing + + + Metadata('Bar')->Distinct())`> + + + + + "); + Project p = new Project(XmlReader.Create(new StringReader(projectText.Replace("`", "\"")))); + + Assert.IsTrue(p.Build(new string[] { "Build" }, new ILogger[] { logger })); + + logger.AssertLogContains("SomeMetaThing"); + } + + [TestMethod] + public void InputItemsTransformedToDifferentNumberOfOutputsFewer1() + { + Console.WriteLine("InputItemsTransformedToDifferentNumberOfOutputsFewer1"); + MockLogger logger = new MockLogger(); + string projectText = ObjectModelHelpers.CleanupFileContents(@" + + + + SomeMetaThing + SomeMetaThing + + + Metadata('Bar')->Distinct())` + Outputs=`@(Foo)`> + + + + + "); + Project p = new Project(XmlReader.Create(new StringReader(projectText.Replace("`", "\"")))); + + Assert.IsTrue(p.Build(new string[] { "Build" }, new ILogger[] { logger })); + + logger.AssertLogContains("SomeMetaThing"); + } + + [TestMethod] + public void InputItemsTransformedToDifferentNumberOfOutputsMore() + { + Console.WriteLine("InputItemsTransformedToDifferentNumberOfOutputsMore"); + MockLogger logger = new MockLogger(); + string projectText = ObjectModelHelpers.CleanupFileContents(@" + + + + 1;2;3;4;5;6;7;8;9 + a;b;c;d;e;f;g + + + Metadata('Bar')->Distinct())`> + + + + + "); + Project p = new Project(XmlReader.Create(new StringReader(projectText.Replace("`", "\"")))); + + Assert.IsTrue(p.Build(new string[] { "Build" }, new ILogger[] { logger })); + + logger.AssertLogContains("1;2;3;4;5;6;7;8;9"); + logger.AssertLogContains("a;b;c;d;e;f;g"); + } + + [TestMethod] + public void InputItemsTransformedToDifferentNumberOfOutputsMore1() + { + Console.WriteLine("InputItemsTransformedToDifferentNumberOfOutputsMore1"); + MockLogger logger = new MockLogger(); + string projectText = ObjectModelHelpers.CleanupFileContents(@" + + + + 1;2;3;4;5;6;7;8;9 + a;b;c;d;e;f;g + + + Metadata('Bar')->Distinct())` + Outputs=`@(Foo)`> + + + + + "); + Project p = new Project(XmlReader.Create(new StringReader(projectText.Replace("`", "\"")))); + + Assert.IsTrue(p.Build(new string[] { "Build" }, new ILogger[] { logger })); + + logger.AssertLogContains("1;2;3;4;5;6;7;8;9"); + logger.AssertLogContains("a;b;c;d;e;f;g"); + } + + [TestMethod] + public void InputItemsTransformedToDifferentNumberOfOutputsTwoWays() + { + Console.WriteLine("InputItemsTransformedToDifferentNumberOfOutputsTwoWays"); + MockLogger logger = new MockLogger(); + File.WriteAllText("foo1.txt", ""); + File.WriteAllText("foo.txt", ""); + Thread.Sleep(100); + File.WriteAllText("1111", ""); + File.WriteAllText("a", ""); + string projectText = ObjectModelHelpers.CleanupFileContents(@" + + + + 1111 + a + + + Metadata('Bar'));@(Foo->'%(Filename).goo')`> + + + + + "); + Project p = new Project(XmlReader.Create(new StringReader(projectText.Replace("`", "\"")))); + + Assert.IsTrue(p.Build(new string[] { "Build" }, new ILogger[] { logger })); + + logger.AssertLogContains("foo.goo"); + logger.AssertLogContains("foo1.goo"); + + File.Delete("foo1.txt"); + File.Delete("foo.txt"); + File.Delete("a"); + File.Delete("1111"); + } + + /// + /// Ensure that items not involved in the incremental build are explicitly empty + /// + [TestMethod] + public void MultiInputItemsThatCorrelatesWithMultipleTransformOutputItems2() + { + Console.WriteLine("MultiInputItemsThatCorrelatesWithMultipleTransformOutputItems2"); + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + '%(Filename).out');@(B->'%(Filename).out')""> + '%(Filename).out')""> + + + + '%(Filename).out')""> + + + + + + ")))); + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + + // If the log contains B.out twice, then there is leakage from the parent lookup + logger.AssertLogDoesntContain("B.out;B.out"); + } + + private readonly DateTime _today = DateTime.Today; + private readonly DateTime _yesterday = DateTime.Today.AddTicks(-TimeSpan.TicksPerDay); + private readonly DateTime _twoDaysAgo = DateTime.Today.AddTicks(-2 * TimeSpan.TicksPerDay); + + private class FileWriteInfo + { + public string Path; + public DateTime LastWriteTime; + + private FileWriteInfo() { } + + public FileWriteInfo(string path, DateTime lastWriteTime) + { + this.Path = path; + this.LastWriteTime = lastWriteTime; + } + } + + /// + /// Helper method for tests of PerformDependencyAnalysis. + /// The setup required here suggests that the TargetDependencyAnalyzer + /// class should be refactored. + /// + private DependencyAnalysisResult PerformDependencyAnalysisTestHelper + ( + FileWriteInfo[] filesToAnalyze, + ItemDictionary itemsByName, + string inputs, + string outputs + ) + { + ItemDictionary h1 = new ItemDictionary(); + ItemDictionary h2 = new ItemDictionary(); + return PerformDependencyAnalysisTestHelper(filesToAnalyze, itemsByName, inputs, outputs, out h1, out h2); + } + + private DependencyAnalysisResult PerformDependencyAnalysisTestHelper + ( + FileWriteInfo[] filesToAnalyze, + ItemDictionary itemsByName, + string inputs, + string outputs, + out ItemDictionary changedTargetInputs, + out ItemDictionary upToDateTargetInputs + ) + { + List filesToDelete = new List(); + + try + { + // first set the disk up + for (int i = 0; i < filesToAnalyze.Length; ++i) + { + string path = ObjectModelHelpers.CreateFileInTempProjectDirectory(filesToAnalyze[i].Path, ""); + File.SetCreationTime(path, filesToAnalyze[i].LastWriteTime); + File.SetLastWriteTime(path, filesToAnalyze[i].LastWriteTime); + filesToDelete.Add(path); + } + + // Wait + Thread.Sleep(50); + + // now create the project + string unformattedProjectXml = ObjectModelHelpers.CleanupFileContents( + @" + + + "); + + string projectFile = Path.Combine(ObjectModelHelpers.TempProjectDir, "temp.proj"); + string formattedProjectXml = String.Format(unformattedProjectXml, inputs, outputs); + File.WriteAllText(projectFile, formattedProjectXml); + + // Wait + Thread.Sleep(50); + + filesToDelete.Add(projectFile); + + Project project = new Project(projectFile); + ProjectInstance p = project.CreateProjectInstance(); + + // now do the dependency analysis + ItemBucket itemBucket = new ItemBucket(null, null, new Lookup(itemsByName, new PropertyDictionary(), null), 0); + TargetUpToDateChecker analyzer = new TargetUpToDateChecker(p, p.Targets["Build"], _mockHost, BuildEventContext.Invalid); + + return analyzer.PerformDependencyAnalysis(itemBucket, out changedTargetInputs, out upToDateTargetInputs); + } + finally + { + // finally clean up + foreach (string path in filesToDelete) + { + if (File.Exists(path)) File.Delete(path); + } + + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + } + + /// + /// Test comparison of inputs/outputs: up to date + /// + [TestMethod] + public void TestIsAnyOutOfDate1() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + new DateTime(2001, 1, 1), /* output1 */ + new DateTime(2001, 1, 1), /* output2 */ + false /* none out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: first input out of date wrt second output + /// + [TestMethod] + public void TestIsAnyOutOfDate2() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2002, 1, 1), /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + new DateTime(2003, 1, 1), /* output1 */ + new DateTime(2001, 1, 1), /* output2 */ + true /* some out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: second input out of date wrt first output + /// + [TestMethod] + public void TestIsAnyOutOfDate3() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2002, 1, 1), /* input2 */ + new DateTime(2001, 1, 1), /* output1 */ + new DateTime(2003, 1, 1), /* output2 */ + true /* some out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: inputs and outputs have same dates + /// + [TestMethod] + public void TestIsAnyOutOfDate4() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + new DateTime(2000, 1, 1), /* output1 */ + new DateTime(2000, 1, 1), /* output2 */ + false /* none out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: first input missing + /// + [TestMethod] + public void TestIsAnyOutOfDate5() + { + IsAnyOutOfDateTestHelper + ( + null, /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + new DateTime(2002, 1, 1), /* output1 */ + new DateTime(2002, 1, 1), /* output2 */ + true /* some out of date */ + ); + } + + + /// + /// Test comparison of inputs/outputs: second input missing + /// + [TestMethod] + public void TestIsAnyOutOfDate6() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + null, /* input2 */ + new DateTime(2002, 1, 1), /* output1 */ + new DateTime(2002, 1, 1), /* output2 */ + true /* some out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: second output missing + /// + [TestMethod] + public void TestIsAnyOutOfDate7() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + new DateTime(2002, 1, 1), /* output1 */ + null, /* output2 */ + true /* some out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: first output missing + /// + [TestMethod] + public void TestIsAnyOutOfDate8() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + null, /* output1 */ + new DateTime(2002, 1, 1), /* output2 */ + true /* some out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: first input and first output missing + /// + [TestMethod] + public void TestIsAnyOutOfDate9() + { + IsAnyOutOfDateTestHelper + ( + null, /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + null, /* output1 */ + new DateTime(2002, 1, 1), /* output2 */ + true /* some out of date */ + ); + } + + /// + /// Test comparison of inputs/outputs: one input, two outputs, input out of date + /// + [TestMethod] + public void TestIsAnyOutOfDate10() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2002, 1, 1), /* input1 */ + null, /* input2 */ + new DateTime(2000, 1, 1), /* output1 */ + new DateTime(2002, 1, 1), /* output2 */ + true, /* some out of date */ + true, /* include input1 */ + false, /* do not include input2 */ + true, /* include output1 */ + true /* include output2 */ + ); + } + + /// + /// Test comparison of inputs/outputs: one input, two outputs, input up to date + /// + [TestMethod] + public void TestIsAnyOutOfDate11() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + null, /* input2 */ + new DateTime(2002, 1, 1), /* output1 */ + new DateTime(2002, 1, 1), /* output2 */ + false, /* none out of date */ + true, /* include input1 */ + false, /* do not include input2 */ + true, /* include output1 */ + true /* include output2 */ + ); + } + + /// + /// Test comparison of inputs/outputs: two inputs, one output, inputs up to date + /// + [TestMethod] + public void TestIsAnyOutOfDate12() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2000, 1, 1), /* input2 */ + new DateTime(2002, 1, 1), /* output1 */ + null, /* output2 */ + false, /* none out of date */ + true, /* include input1 */ + true, /* include input2 */ + true, /* include output1 */ + false /* do not include output2 */ + ); + } + + /// + /// Test comparison of inputs/outputs: two inputs, one output, second input out of date + /// + [TestMethod] + public void TestIsAnyOutOfDate13() + { + IsAnyOutOfDateTestHelper + ( + new DateTime(2000, 1, 1), /* input1 */ + new DateTime(2003, 1, 1), /* input2 */ + new DateTime(2002, 1, 1), /* output1 */ + null, /* output2 */ + true, /* some out of date */ + true, /* include input1 */ + true, /* include input2 */ + true, /* include output1 */ + false /* do not include output2 */ + ); + } + + /// + /// Helper method for tests of IsAnyOutOfDate. + /// The setup required here suggests that the TargetDependencyAnalyzer + /// class should be refactored. + /// + /// + /// + /// + /// + /// + private void IsAnyOutOfDateTestHelper + ( + DateTime? input1Time, + DateTime? input2Time, + DateTime? output1Time, + DateTime? output2Time, + bool isUpToDate + ) + { + IsAnyOutOfDateTestHelper(input1Time, input2Time, output1Time, output2Time, isUpToDate, true, true, true, true); + } + + /// + /// Helper method for tests of IsAnyOutOfDate. + /// The setup required here suggests that the TargetDependencyAnalyzer + /// class should be refactored. + /// + /// + /// + /// + /// + /// + private void IsAnyOutOfDateTestHelper + ( + DateTime? input1Time, + DateTime? input2Time, + DateTime? output1Time, + DateTime? output2Time, + bool expectedAnyOutOfDate, + bool includeInput1, + bool includeInput2, + bool includeOutput1, + bool includeOutput2 + ) + { + List inputs = new List(); + List outputs = new List(); + + string input1 = "NONEXISTENT_FILE"; + string input2 = "NONEXISTENT_FILE"; + string output1 = "NONEXISTENT_FILE"; + string output2 = "NONEXISTENT_FILE"; + + try + { + if (input1Time != null) + { + input1 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(input1, String.Empty); + File.SetLastWriteTime(input1, (DateTime)input1Time); + } + + if (input2Time != null) + { + input2 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(input2, String.Empty); + File.SetLastWriteTime(input2, (DateTime)input2Time); + } + + if (output1Time != null) + { + output1 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(output1, String.Empty); + File.SetLastWriteTime(output1, (DateTime)output1Time); + } + + if (output2Time != null) + { + output2 = FileUtilities.GetTemporaryFile(); + File.WriteAllText(output2, String.Empty); + File.SetLastWriteTime(output2, (DateTime)output2Time); + } + + if (includeInput1) inputs.Add(input1); + if (includeInput2) inputs.Add(input2); + if (includeOutput1) outputs.Add(output1); + if (includeOutput2) outputs.Add(output2); + + DependencyAnalysisLogDetail detail; + Assert.AreEqual(expectedAnyOutOfDate, TargetUpToDateChecker.IsAnyOutOfDate(out detail, Directory.GetCurrentDirectory(), inputs, outputs)); + } + finally + { + if (File.Exists(input1)) File.Delete(input1); + if (File.Exists(input2)) File.Delete(input2); + if (File.Exists(output1)) File.Delete(output1); + if (File.Exists(output2)) File.Delete(output2); + } + } + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilderTestTask.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilderTestTask.cs new file mode 100644 index 00000000000..455ecb683d2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilderTestTask.cs @@ -0,0 +1,582 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A task used to test the TaskExecutionHost. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using System.Reflection; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// A task used for testing the TaskExecutionHost, which reports what the TaskExecutionHost does to it. + /// + internal class TaskBuilderTestTask : IGeneratedTask + { + /// + /// The task host. + /// + private ITestTaskHost _testTaskHost; + + /// + /// The value to return from Execute + /// + private bool _executeReturnValue; + + /// + /// The value for the BoolOutput + /// + private bool _boolOutput; + + /// + /// The value for the BoolArrayOutput + /// + private bool[] _boolArrayOutput; + + /// + /// The value for the IntOutput + /// + private int _intOutput; + + /// + /// The value for the IntArrayOutput + /// + private int[] _intArrayOutput; + + /// + /// The value for the StringOutput + /// + private string _stringOutput; + + /// + /// The value for the StringArrayOutput + /// + private string[] _stringArrayOutput; + + /// + /// The value for the ItemOutput + /// + private ITaskItem _itemOutput; + + /// + /// The value for the ItemArrayOutput + /// + private ITaskItem[] _itemArrayOutput; + + /// + /// Property determining if Execute() should throw or not. + /// + public bool ThrowOnExecute + { + internal get; + set; + } + + /// + /// A boolean parameter + /// + public bool BoolParam + { + set + { + _boolOutput = value; + _testTaskHost.ParameterSet("BoolParam", value); + } + } + + /// + /// A boolean array parameter + /// + public bool[] BoolArrayParam + { + set + { + _boolArrayOutput = value; + _testTaskHost.ParameterSet("BoolArrayParam", value); + } + } + + /// + /// An integer parameter + /// + public int IntParam + { + set + { + _intOutput = value; + _testTaskHost.ParameterSet("IntParam", value); + } + } + + /// + /// An integer array parameter. + /// + public int[] IntArrayParam + { + set + { + _intArrayOutput = value; + _testTaskHost.ParameterSet("IntArrayParam", value); + } + } + + /// + /// A string parameter. + /// + public string StringParam + { + set + { + _stringOutput = value; + _testTaskHost.ParameterSet("StringParam", value); + } + } + + /// + /// A string array parameter. + /// + public string[] StringArrayParam + { + set + { + _stringArrayOutput = value; + _testTaskHost.ParameterSet("StringArrayParam", value); + } + } + + /// + /// An item parameter. + /// + public ITaskItem ItemParam + { + set + { + _itemOutput = value; + _testTaskHost.ParameterSet("ItemParam", value); + } + } + + /// + /// An item array parameter. + /// + public ITaskItem[] ItemArrayParam + { + set + { + _itemArrayOutput = value; + _testTaskHost.ParameterSet("ItemArrayParam", value); + } + } + + /// + /// The Execute return value parameter. + /// + [Required] + public bool ExecuteReturnParam + { + set + { + _executeReturnValue = value; + _testTaskHost.ParameterSet("ExecuteReturnParam", value); + } + } + + /// + /// A boolean output. + /// + [Output] + public bool BoolOutput + { + get + { + _testTaskHost.OutputRead("BoolOutput", _boolOutput); + return _boolOutput; + } + } + + /// + /// A boolean array output + /// + [Output] + public bool[] BoolArrayOutput + { + get + { + _testTaskHost.OutputRead("BoolArrayOutput", _boolArrayOutput); + return _boolArrayOutput; + } + } + + /// + /// An integer output + /// + [Output] + public int IntOutput + { + get + { + _testTaskHost.OutputRead("IntOutput", _intOutput); + return _intOutput; + } + } + + /// + /// An integer array output + /// + [Output] + public int[] IntArrayOutput + { + get + { + _testTaskHost.OutputRead("IntArrayOutput", _intArrayOutput); + return _intArrayOutput; + } + } + + /// + /// A string output + /// + [Output] + public string StringOutput + { + get + { + _testTaskHost.OutputRead("StringOutput", _stringOutput); + return _stringOutput; + } + } + + /// + /// An empty string output + /// + [Output] + public string EmptyStringOutput + { + get + { + _testTaskHost.OutputRead("EmptyStringOutput", null); + return String.Empty; + } + } + + /// + /// An empty string array output + /// + [Output] + public string[] EmptyStringArrayOutput + { + get + { + _testTaskHost.OutputRead("EmptyStringArrayOutput", null); + return new string[0]; + } + } + + /// + /// A null string output + /// + [Output] + public string NullStringOutput + { + get + { + _testTaskHost.OutputRead("NullStringOutput", null); + return null; + } + } + + /// + /// A null ITaskItem output + /// + [Output] + public ITaskItem NullITaskItemOutput + { + get + { + _testTaskHost.OutputRead("NullITaskItemOutput", null); + return null; + } + } + + /// + /// A null string array output + /// + [Output] + public string[] NullStringArrayOutput + { + get + { + _testTaskHost.OutputRead("NullStringArrayOutput", null); + return null; + } + } + + /// + /// A null ITaskItem array output + /// + [Output] + public ITaskItem[] NullITaskItemArrayOutput + { + get + { + _testTaskHost.OutputRead("NullITaskItemArrayOutput", null); + return null; + } + } + + /// + /// A string array output + /// + [Output] + public string[] StringArrayOutput + { + get + { + _testTaskHost.OutputRead("StringArrayOutput", _stringArrayOutput); + return _stringArrayOutput; + } + } + + /// + /// A task item output + /// + [Output] + public ITaskItem ItemOutput + { + get + { + _testTaskHost.OutputRead("ItemOutput", _itemOutput); + return _itemOutput; + } + } + + /// + /// A task item array output + /// + [Output] + public ITaskItem[] ItemArrayOutput + { + get + { + _testTaskHost.OutputRead("ItemArrayOutput", _itemArrayOutput); + return _itemArrayOutput; + } + } + + /// + /// A task item array output that is null + /// + [Output] + public ITaskItem[] ItemArrayNullOutput + { + get + { + _testTaskHost.OutputRead("ItemArrayNullOutput", _itemArrayOutput); + return null; + } + } + + /// + /// An object output + /// + [Output] + public object ObjectOutput + { + get + { + object output = new object(); + _testTaskHost.OutputRead("ObjectOutput", output); + return output; + } + } + + /// + /// An object array output + /// + [Output] + public object[] ObjectArrayOutput + { + get + { + object[] output = new object[] { new object(), new object() }; + _testTaskHost.OutputRead("ObjectArrayOutput", output); + return output; + } + } + + /// + /// An arraylist output + /// + [Output] + public ArrayList ArrayListOutput + { + get + { + ArrayList output = new ArrayList(); + _testTaskHost.OutputRead("ArrayListOutput", output); + return output; + } + } + + #region ITask Members + + /// + /// The build engine property + /// + public IBuildEngine BuildEngine + { + get; + set; + } + + /// + /// The host object property + /// + public ITaskHost HostObject + { + get + { + return _testTaskHost; + } + + set + { + _testTaskHost = value as ITestTaskHost; + } + } + + /// + /// The Execute() method for ITask. + /// + public bool Execute() + { + if (ThrowOnExecute) + { + throw new IndexOutOfRangeException(); + } + + return _executeReturnValue; + } + + #endregion + + #region IGeneratedTask Members + + /// + /// Gets the property value. + /// + /// The property to get. + /// + /// The value of the property, the value's type will match the type given by . + /// + /// + /// MSBuild calls this method after executing the task to get output parameters. + /// All exceptions from this method will be caught in the taskExecution host and logged as a fatal task error + /// + public object GetPropertyValue(TaskPropertyInfo property) + { + return GetType().GetProperty(property.Name).GetValue(this, null); + } + + /// + /// Sets a value on a property of this task instance. + /// + /// The property to set. + /// The value to set. The caller is responsible to type-coerce this value to match the property's . + /// + /// All exceptions from this method will be caught in the taskExecution host and logged as a fatal task error + /// + public void SetPropertyValue(TaskPropertyInfo property, object value) + { + GetType().GetProperty(property.Name).SetValue(this, value, null); + } + + #endregion + + /// + /// Task factory which wraps a test task, this is used for unit testing + /// + internal class TaskBuilderTestTaskFactory : ITaskFactory + { + /// + /// Type of the task wrapped by the task factory + /// + private Type _type = typeof(TaskBuilderTestTask); + + /// + /// Should the task throw on execution + /// + public bool ThrowOnExecute + { + get; + set; + } + + /// + /// Name of the factory + /// + public string FactoryName + { + get { return typeof(TaskBuilderTestTask).ToString(); } + } + + /// + /// Gets the type of task generated. + /// + public Type TaskType + { + get { return _type; } + } + + /// + /// There is nothing to initalize + /// + public bool Initialize(string taskName, IDictionary taskParameters, string taskElementContents, IBuildEngine taskLoggingHost) + { + return true; + } + + /// + /// Get a list of parameters for the task. + /// + public TaskPropertyInfo[] GetTaskParameters() + { + PropertyInfo[] infos = _type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var propertyInfos = new TaskPropertyInfo[infos.Length]; + for (int i = 0; i < infos.Length; i++) + { + propertyInfos[i] = new TaskPropertyInfo( + infos[i].Name, + infos[i].PropertyType, + infos[i].GetCustomAttributes(typeof(OutputAttribute), false).Length > 0, + infos[i].GetCustomAttributes(typeof(RequiredAttribute), false).Length > 0); + } + + return propertyInfos; + } + + /// + /// Create a new instance + /// + public ITask CreateTask(IBuildEngine loggingHost) + { + var task = new TaskBuilderTestTask(); + task.ThrowOnExecute = ThrowOnExecute; + return task; + } + + /// + /// Cleans up a task that is finished. + /// + public void CleanupTask(ITask task) + { + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilder_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilder_Tests.cs new file mode 100644 index 00000000000..c3426589d33 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskBuilder_Tests.cs @@ -0,0 +1,1346 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for the task builder object. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using ElementLocation = Microsoft.Build.Construction.ElementLocation; +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using LegacyThreadingData = Microsoft.Build.Execution.LegacyThreadingData; +using TargetDotNetFrameworkVersion = Microsoft.Build.Utilities.TargetDotNetFrameworkVersion; +using ToolLocationHelper = Microsoft.Build.Utilities.ToolLocationHelper; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Unit tests for the TaskBuilder component + /// + [TestClass] + public class TaskBuilder_Tests : ITargetBuilderCallback + { + /// + /// Task definition for a task that outputs items containing null metadata. + /// + private static string s_nullMetadataTaskContents = +@"using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections; +using System.Collections.Generic; + +namespace NullMetadataTask +{ + public class NullMetadataTask : Task + { + [Output] + public ITaskItem[] OutputItems + { + get; + set; + } + + public override bool Execute() + { + OutputItems = new ITaskItem[1]; + + IDictionary metadata = new Dictionary(); + metadata.Add(""a"", null); + + OutputItems[0] = new TaskItem(""foo"", (IDictionary)metadata); + + return true; + } + } +} +"; + + /// + /// Task definition for task that outputs items in a variety of ways, used to + /// test definition of the DefiningProject* metadata for task outputs. + /// + private static string s_itemCreationTaskContents = + @"using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections; +using System.Collections.Generic; + +namespace ItemCreationTask +{ + public class ItemCreationTask : Task + { + public ITaskItem[] InputItemsToPassThrough + { + get; + set; + } + + public ITaskItem[] InputItemsToCopy + { + get; + set; + } + + [Output] + public ITaskItem[] PassedThroughOutputItems + { + get; + set; + } + + [Output] + public ITaskItem[] CreatedOutputItems + { + get; + set; + } + + [Output] + public ITaskItem[] CopiedOutputItems + { + get; + set; + } + + [Output] + public string OutputString + { + get; + set; + } + + public override bool Execute() + { + PassedThroughOutputItems = InputItemsToPassThrough; + + CopiedOutputItems = new ITaskItem[InputItemsToCopy.Length]; + + for (int i = 0; i < InputItemsToCopy.Length; i++) + { + CopiedOutputItems[i] = new TaskItem(InputItemsToCopy[i]); + } + + CreatedOutputItems = new ITaskItem[2]; + CreatedOutputItems[0] = new TaskItem(""Foo""); + CreatedOutputItems[1] = new TaskItem(""Bar""); + + OutputString = ""abc;def;ghi""; + + return true; + } + } +} +"; + + /// + /// The mock component host and logger + /// + private MockHost _host; + + /// + /// The temporary project we use to run the test + /// + private ProjectInstance _testProject; + + /// + /// Prepares the environment for the test. + /// + [TestInitialize] + public void SetUp() + { + _host = new MockHost(); + _testProject = CreateTestProject(); + } + + /// + /// Cleans up after the test + /// + [TestCleanup] + public void TearDown() + { + _testProject = null; + _host = null; + } + + /********************************************************************************* + * + * OUTPUT PARAMS + * + *********************************************************************************/ + + /// + /// Verifies that we do look up the task during execute when the condition is true. + /// + [TestMethod] + public void TasksAreDiscoveredWhenTaskConditionTrue() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents( + @" + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + project.Build("t", loggers); + + logger.AssertLogContains("MSB4036"); + logger.AssertLogDoesntContain("Made it"); + } + + /// + /// Tests that when the task condition is false, Execute still returns true even though we never loaded + /// the task. We verify that we never loaded the task because if we did try, the task load itself would + /// have failed, resulting in an error. + /// + [TestMethod] + public void TasksNotDiscoveredWhenTaskConditionFalse() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents( + @" + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + project.Build("t", loggers); + + logger.AssertLogContains("Made it"); + } + + /// + /// Verify when task outputs are overridden the override messages are correctly displayed + /// + [TestMethod] + public void OverridePropertiesInCreateProperty() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents( + @" + + + foo + + + bar + + + barz + + + + '/assemblyresource:%(Identity),%(LogicalName)', ' ')"" + Condition=""'%(LogicalName)' != '' ""> + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + project.Build("t", loggers); + + logger.AssertLogContains(new string[] { "final:[/assemblyresource:c.resx,barz]" }); + logger.AssertLogContains(new string[] { ResourceUtilities.FormatResourceString("TaskStarted", "CreateProperty") }); + logger.AssertLogContains(new string[] { ResourceUtilities.FormatResourceString("PropertyOutputOverridden", "LinkSwitches", "/assemblyresource:a.resx,foo", "/assemblyresource:b.resx,bar") }); + logger.AssertLogContains(new string[] { ResourceUtilities.FormatResourceString("PropertyOutputOverridden", "LinkSwitches", "/assemblyresource:b.resx,bar", "/assemblyresource:c.resx,barz") }); + } + + /// + /// Verify that when a task outputs are inferred the override messages are displayed + /// + [TestMethod] + public void OverridePropertiesInInferredCreateProperty() + { + string[] files = null; + try + { + files = ObjectModelHelpers.GetTempFiles(2, new DateTime(2005, 1, 1)); + + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents( + @" + + " + files[1] + @" + + + + foo + + + bar + + + barz + + + + + + + + '/assemblyresource:%(Identity),%(LogicalName)', ' ')"" + Condition=""'%(LogicalName)' != '' ""> + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + project.Build("t2", loggers); + + // We should only see messages from the second target, as the first is only inferred + logger.AssertLogDoesntContain("start:"); + logger.AssertLogDoesntContain("end:"); + + logger.AssertLogContains(new string[] { "final:[/assemblyresource:c.resx,barz]" }); + logger.AssertLogDoesntContain(ResourceUtilities.FormatResourceString("TaskStarted", "CreateProperty")); + logger.AssertLogContains(new string[] { ResourceUtilities.FormatResourceString("PropertyOutputOverridden", "LinkSwitches", "/assemblyresource:a.resx,foo", "/assemblyresource:b.resx,bar") }); + logger.AssertLogContains(new string[] { ResourceUtilities.FormatResourceString("PropertyOutputOverridden", "LinkSwitches", "/assemblyresource:b.resx,bar", "/assemblyresource:c.resx,barz") }); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(files); + } + } + + /// + /// Tests that tasks batch on outputs correctly. + /// + [TestMethod] + public void TaskOutputBatching() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + Value + Include + MetadataProperty + MetadataItem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + project.Build(loggers); + + logger.AssertLogContains("Property1=[foo]"); + logger.AssertLogContains("Property2=[foo]"); + logger.AssertLogContains("MetadataProperty=[foo]"); + logger.AssertLogContains("TestItem1=[foo]"); + logger.AssertLogContains("TestItem2=[foo]"); + logger.AssertLogContains("MetadataItem=[foo]"); + } + + /// + /// MSbuildLastTaskResult property contains true or false indicating + /// the success or failure of the last task. + /// + [TestMethod] + public void MSBuildLastTaskResult() + { + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + +

$(MSBuildLastTaskResult)

+
+ + + +
+ + + + + + + + + + + + +
"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + MockLogger logger = new MockLogger(); + loggers.Add(logger); + project.Build("t2", loggers); + + logger.AssertLogContains("[start:]"); + logger.AssertLogContains("[0:true]"); + logger.AssertLogContains("[1:false]"); + logger.AssertLogContains("[p:false]"); + logger.AssertLogContains("[2:true]"); + logger.AssertLogContains("[3:true]"); + logger.AssertLogContains("[4:false]"); + logger.AssertLogContains("[4:false]"); + } + + /// + /// Verifies that we can add "recursivedir" built-in metadata as target outputs. + /// This is to support wildcards in CreateItem. Allowing anything + /// else could let the item get corrupt (inconsistent values for Filename and FullPath, for example) + /// + [TestMethod] + public void TasksCanAddRecursiveDirBuiltInMetadata() + { + MockLogger logger = new MockLogger(); + + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + bool result = project.Build("t", loggers); + + Assert.AreEqual(true, result); + logger.AssertLogDoesntContain("[]"); + logger.AssertLogDoesntContain("MSB4118"); + logger.AssertLogDoesntContain("MSB3031"); + } + + /// + /// Verify CreateItem prevents adding any built-in metadata explicitly, even recursivedir. + /// + [TestMethod] + public void OtherBuiltInMetadataErrors() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + bool result = project.Build("t", loggers); + + Assert.AreEqual(false, result); + logger.AssertLogContains("MSB3031"); + } + + /// + /// Verify CreateItem prevents adding any built-in metadata explicitly, even recursivedir. + /// + [TestMethod] + public void OtherBuiltInMetadataErrors2() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + bool result = project.Build("t", loggers); + + Assert.AreEqual(false, result); + logger.AssertLogContains("MSB3031"); + } + + /// + /// Verify that properties can be passed in to a task and out as items, despite the + /// built-in metadata restrictions. + /// + [TestMethod] + public void PropertiesInItemsOutOfTask() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + +

c:\a.ext

+
+ + + + +
+
"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + bool result = project.Build("t", loggers); + + Assert.AreEqual(true, result); + logger.AssertLogContains("[.ext]"); + } + + /// + /// Verify that properties can be passed in to a task and out as items, despite + /// having illegal characters for a file name + /// + [TestMethod] + public void IllegalFileCharsInItemsOutOfTask() + { + MockLogger logger = new MockLogger(); + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + +

||illegal||

+
+ + + + +
+
"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + List loggers = new List(); + loggers.Add(logger); + bool result = project.Build("t", loggers); + + Assert.AreEqual(true, result); + logger.AssertLogContains("[||illegal||]"); + } + + /// + /// If an item being output from a task has null metadata, we shouldn't crash. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void NullMetadataOnOutputItems() + { + string customTaskPath = CustomTaskHelper.GetAssemblyForTask(s_nullMetadataTaskContents); + + string projectContents = @" + + + + + + + + + +"; + + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents); + logger.AssertLogContains("[foo: ]"); + } + + /// + /// If an item being output from a task has null metadata, we shouldn't crash. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void NullMetadataOnLegacyOutputItems() + { + string referenceAssembliesPath = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.VersionLatest); + + if (String.IsNullOrEmpty(referenceAssembliesPath)) + { + // fall back to the .NET Framework -- they should always exist there. + referenceAssembliesPath = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.VersionLatest); + } + + string[] referenceAssemblies = new string[] { Path.Combine(referenceAssembliesPath, "System.dll"), Path.Combine(referenceAssembliesPath, "Microsoft.Build.Framework.dll"), Path.Combine(referenceAssembliesPath, "Microsoft.Build.Utilities.v4.0.dll") }; + + string customTaskPath = CustomTaskHelper.GetAssemblyForTask(s_nullMetadataTaskContents, referenceAssemblies); + + string projectContents = @" + + + + + + + + + +"; + + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents); + logger.AssertLogContains("[foo: ]"); + } + + /// + /// If an item being output from a task has null metadata, we shouldn't crash. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void NullMetadataOnOutputItems_InlineTask() + { + string projectContents = @" + + + + + + + + metadata = new Dictionary(); + metadata.Add(`a`, null); + + OutputItems[0] = new TaskItem(`foo`, (IDictionary)metadata); + + return true; + ]]> + + + + + + + + + + + "; + + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents); + logger.AssertLogContains("[foo: ]"); + } + + /// + /// If an item being output from a task has null metadata, we shouldn't crash. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void NullMetadataOnLegacyOutputItems_InlineTask() + { + string projectContents = @" + + + + + + + + metadata = new Dictionary(); + metadata.Add(`a`, null); + + OutputItems[0] = new TaskItem(`foo`, (IDictionary)metadata); + + return true; + ]]> + + + + + + + + + + + "; + + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents); + logger.AssertLogContains("[foo: ]"); + } + + /// + /// Validates that the defining project metadata is set (or not set) as expected in + /// various task output-related operations, using a task built against the current + /// version of MSBuild. + /// + public void ValidateDefiningProjectMetadataOnTaskOutputs() + { + string customTaskPath = CustomTaskHelper.GetAssemblyForTask(s_itemCreationTaskContents); + ValidateDefiningProjectMetadataOnTaskOutputsHelper(customTaskPath); + } + + /// + /// Validates that the defining project metadata is set (or not set) as expected in + /// various task output-related operations, using a task built against V4 MSBuild, + /// which didn't support the defining project metadata. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ValidateDefiningProjectMetadataOnTaskOutputs_LegacyItems() + { + string referenceAssembliesPath = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.VersionLatest); + + if (String.IsNullOrEmpty(referenceAssembliesPath)) + { + referenceAssembliesPath = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.VersionLatest); + } + + string[] referenceAssemblies = new string[] { Path.Combine(referenceAssembliesPath, "System.dll"), Path.Combine(referenceAssembliesPath, "Microsoft.Build.Framework.dll"), Path.Combine(referenceAssembliesPath, "Microsoft.Build.Utilities.v4.0.dll") }; + + string customTaskPath = CustomTaskHelper.GetAssemblyForTask(s_itemCreationTaskContents, referenceAssemblies); + ValidateDefiningProjectMetadataOnTaskOutputsHelper(customTaskPath); + } + + /// + /// Tests that putting the RunInSTA attribute on a task causes it to run in the STA thread. + /// + [TestMethod] + public void TestSTAThreadRequired() + { + TestSTATask(true, false, false); + } + + /// + /// Tests an STA task with an exception + /// + [TestMethod] + public void TestSTAThreadRequiredWithException() + { + TestSTATask(true, false, true); + } + + /// + /// Tests an STA task with failure. + /// + [TestMethod] + public void TestSTAThreadRequiredWithFailure() + { + TestSTATask(true, true, false); + } + + /// + /// Tests an MTA task. + /// + [TestMethod] + public void TestSTAThreadNotRequired() + { + TestSTATask(false, false, false); + } + + /// + /// Tests an MTA task with an exception. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TestSTAThreadNotRequiredWithException() + { + TestSTATask(false, false, true); + } + + /// + /// Tests an MTA task with failure. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TestSTAThreadNotRequiredWithFailure() + { + TestSTATask(false, true, false); + } + + #region ITargetBuilderCallback Members + + /// + /// Empty impl + /// + Task ITargetBuilderCallback.LegacyCallTarget(string[] targets, bool continueOnError, ElementLocation referenceLocation) + { + throw new NotImplementedException(); + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.Yield() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.Reacquire() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.EnterMSBuildCallbackState() + { + } + + /// + /// Empty impl + /// + void IRequestBuilderCallback.ExitMSBuildCallbackState() + { + } + + #endregion + + #region IRequestBuilderCallback Members + + /// + /// Empty impl + /// + Task IRequestBuilderCallback.BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + Task IRequestBuilderCallback.BlockOnTargetInProgress(int blockingRequestId, string blockingTarget) + { + throw new NotImplementedException(); + } + + #endregion + + /********************************************************************************* + * + * Helpers + * + *********************************************************************************/ + + /// + /// Helper method for validating the setting of defining project metadata on items + /// coming from task outputs + /// + private void ValidateDefiningProjectMetadataOnTaskOutputsHelper(string customTaskPath) + { + string projectAPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "a.proj"); + string projectBPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "b.proj"); + + string projectAContents = @" + + + + + + + + + + + + + + + + + + +"; + + string projectBContents = @" + + + + + + + +"; + + try + { + File.WriteAllText(projectAPath, ObjectModelHelpers.CleanupFileContents(projectAContents)); + File.WriteAllText(projectBPath, ObjectModelHelpers.CleanupFileContents(projectBContents)); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("a.proj"); + logger.AssertNoWarnings(); + } + finally + { + if (File.Exists(projectAPath)) + { + File.Delete(projectAPath); + } + + if (File.Exists(projectBPath)) + { + File.Delete(projectBPath); + } + } + } + + /// + /// Executes an STA task test. + /// + private void TestSTATask(bool requireSTA, bool failTask, bool throwException) + { + MockLogger logger = new MockLogger(); + logger.AllowTaskCrashes = throwException; + + string taskAssemblyName = null; + Project project = CreateSTATestProject(requireSTA, failTask, throwException, out taskAssemblyName); + + List loggers = new List(); + loggers.Add(logger); + + BuildParameters parameters = new BuildParameters(); + parameters.Loggers = new ILogger[] { logger }; + BuildResult result = BuildManager.DefaultBuildManager.Build(parameters, new BuildRequestData(project.CreateProjectInstance(), new string[] { "Foo" })); + if (requireSTA) + { + logger.AssertLogContains("STA"); + } + else + { + logger.AssertLogContains("MTA"); + } + + if (throwException) + { + logger.AssertLogContains("EXCEPTION"); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + return; + } + else + { + logger.AssertLogDoesntContain("EXCEPTION"); + } + + if (failTask) + { + logger.AssertLogContains("FAIL"); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + } + else + { + logger.AssertLogDoesntContain("FAIL"); + } + + if (!throwException && !failTask) + { + Assert.AreEqual(BuildResultCode.Success, result.OverallResult); + } + } + + /// + /// Helper to create a project which invokes the STA helper task. + /// + private Project CreateSTATestProject(bool requireSTA, bool failTask, bool throwException, out string assemblyToDelete) + { + assemblyToDelete = GenerateSTATask(requireSTA); + + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + + return project; + } + + /// + /// Helper to create the STA test task. + /// + private string GenerateSTATask(bool requireSTA) + { + string taskContents = + @" +using System; +using Microsoft.Build.Framework; +namespace ClassLibrary2 +{" + (requireSTA ? "[RunInSTA]" : String.Empty) + @" + public class ThreadTask : ITask + { + #region ITask Members + + public IBuildEngine BuildEngine + { + get; + set; + } + + public bool ThrowException + { + get; + set; + } + + public bool Fail + { + get; + set; + } + + public bool Execute() + { + string message; + if (System.Threading.Thread.CurrentThread.GetApartmentState() == System.Threading.ApartmentState.STA) + { + message = ""STA""; + } + else + { + message = ""MTA""; + } + + BuildEngine.LogMessageEvent(new BuildMessageEventArgs(message, """", ""ThreadTask"", MessageImportance.High)); + + if (ThrowException) + { + throw new InvalidOperationException(""EXCEPTION""); + } + + if (Fail) + { + BuildEngine.LogMessageEvent(new BuildMessageEventArgs(""FAIL"", """", ""ThreadTask"", MessageImportance.High)); + } + + return !Fail; + } + + public ITaskHost HostObject + { + get; + set; + } + + #endregion + } +}"; + return CustomTaskHelper.GetAssemblyForTask(taskContents); + } + + /// + /// Creates a test project. + /// + /// The project. + private ProjectInstance CreateTestProject() + { + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "); + + IConfigCache cache = (IConfigCache)_host.GetComponent(BuildComponentType.ConfigCache); + BuildRequestConfiguration config = new BuildRequestConfiguration(1, new BuildRequestData("testfile", new Dictionary(), "3.5", new string[0], null), "2.0"); + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + config.Project = project.CreateProjectInstance(); + cache.AddConfiguration(config); + + return config.Project; + } + + /// + /// The mock component host object. + /// + private class MockHost : MockLoggingService, IBuildComponentHost, IBuildComponent + { + #region IBuildComponentHost Members + + /// + /// The config cache + /// + private IConfigCache _configCache; + + /// + /// The logging service + /// + private ILoggingService _loggingService; + + /// + /// The results cache + /// + private IResultsCache _resultsCache; + + /// + /// The request builder + /// + private IRequestBuilder _requestBuilder; + + /// + /// The target builder + /// + private ITargetBuilder _targetBuilder; + + /// + /// The build parameters. + /// + private BuildParameters _buildParameters; + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + private LegacyThreadingData _legacyThreadingData; + + /// + /// Constructor + /// + /// UNDONE: Refactor this, and the other MockHosts, to use a common base implementation. The duplication of the + /// logging implementation alone is unfortunate. + /// + public MockHost() + { + _buildParameters = new BuildParameters(); + _legacyThreadingData = new LegacyThreadingData(); + + _configCache = new ConfigCache(); + ((IBuildComponent)_configCache).InitializeComponent(this); + + _loggingService = this; + + _resultsCache = new ResultsCache(); + ((IBuildComponent)_resultsCache).InitializeComponent(this); + + _requestBuilder = new RequestBuilder(); + ((IBuildComponent)_requestBuilder).InitializeComponent(this); + + _targetBuilder = new TargetBuilder(); + ((IBuildComponent)_targetBuilder).InitializeComponent(this); + } + + /// + /// Returns the node logging service. We don't distinguish here. + /// + public ILoggingService LoggingService + { + get + { + return _loggingService; + } + } + + /// + /// Retrieves the name of the host. + /// + public string Name + { + get + { + return "TaskBuilder_Tests.MockHost"; + } + } + + /// + /// Returns the build parameters. + /// + public BuildParameters BuildParameters + { + get + { + return _buildParameters; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return _legacyThreadingData; + } + } + + /// + /// Constructs and returns a component of the specified type. + /// + /// The type of component to return + /// The component + public IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.ConfigCache: + return (IBuildComponent)_configCache; + + case BuildComponentType.LoggingService: + return (IBuildComponent)_loggingService; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)_resultsCache; + + case BuildComponentType.RequestBuilder: + return (IBuildComponent)_requestBuilder; + + case BuildComponentType.TargetBuilder: + return (IBuildComponent)_targetBuilder; + + default: + throw new ArgumentException("Unexpected type " + type); + } + } + + /// + /// Register a component factory. + /// + public void RegisterFactory(BuildComponentType type, BuildComponentFactoryDelegate factory) + { + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + throw new NotImplementedException(); + } + + /// + /// Shuts down the component + /// + public void ShutdownComponent() + { + throw new NotImplementedException(); + } + + #endregion + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskExecutionHost_Tests.cs new file mode 100644 index 00000000000..e14eb9103f7 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskExecutionHost_Tests.cs @@ -0,0 +1,1470 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for the task execution host object. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Xml; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// The test class for the TaskExecutionHost + /// + [TestClass] + public class TaskExecutionHost_Tests : ITestTaskHost, IBuildEngine2 + { + /// + /// The set of parameters which have been initialized on the task. + /// + private Dictionary _parametersSetOnTask; + + /// + /// The set of outputs which were read from the task. + /// + private Dictionary _outputsReadFromTask; + + /// + /// The task execution host + /// + private ITaskExecutionHost _host; + + /// + /// The mock logging service + /// + private ILoggingService _loggingService; + + /// + /// The mock logger + /// + private MockLogger _logger; + + /// + /// Array containing one item, used for ITaskItem tests. + /// + private ITaskItem[] _oneItem; + + /// + /// Array containing two items, used for ITaskItem tests. + /// + private ITaskItem[] _twoItems; + + /// + /// The bucket which receives outputs. + /// + private ItemBucket _bucket; + + /// + /// Unused. + /// + public bool IsRunningMultipleNodes + { + get { return false; } + } + + /// + /// Unused. + /// + public bool ContinueOnError + { + get { throw new NotImplementedException(); } + } + + /// + /// Unused. + /// + public int LineNumberOfTaskNode + { + get { throw new NotImplementedException(); } + } + + /// + /// Unused. + /// + public int ColumnNumberOfTaskNode + { + get { throw new NotImplementedException(); } + } + + /// + /// Unused. + /// + public string ProjectFileOfTaskNode + { + get { throw new NotImplementedException(); } + } + + /// + /// Prepares the environment for the test. + /// + [TestInitialize] + public void SetUp() + { + InitializeHost(false); + } + + /// + /// Cleans up after the test + /// + [TestCleanup] + public void TearDown() + { + if (_host != null) + { + ((IDisposable)_host).Dispose(); + } + + _host = null; + } + + /// + /// Validate that setting parameters with only the required parameters works. + /// + [TestMethod] + public void ValidateNoParameters() + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + parameters["ExecuteReturnParam"] = new Tuple("true", ElementLocation.Create("foo.proj")); + + Assert.IsTrue(_host.SetTaskParameters(parameters)); + Assert.AreEqual(1, _parametersSetOnTask.Count); + Assert.IsTrue(_parametersSetOnTask.ContainsKey("ExecuteReturnParam")); + } + + /// + /// Validate that setting no parameters when a required parameter exists fails and throws an exception. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ValidateNoParameters_MissingRequired() + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _host.SetTaskParameters(parameters); + } + + /// + /// Validate that setting a non-existant parameter fails, but does not throw an exception. + /// + [TestMethod] + public void ValidateNonExistantParameter() + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + parameters["NonExistantParam"] = new Tuple("foo", ElementLocation.Create("foo.proj")); + Assert.IsFalse(_host.SetTaskParameters(parameters)); + } + + #region Bool Params + + /// + /// Validate that setting a bool param works and sets the right value. + /// + [TestMethod] + public void TestSetBoolParam() + { + ValidateTaskParameter("BoolParam", "true", true); + } + + /// + /// Validate that setting a bool param works and sets the right value. + /// + [TestMethod] + public void TestSetBoolParamFalse() + { + ValidateTaskParameter("BoolParam", "false", false); + } + + /// + /// Validate that setting a bool param with an empty value does not cause the parameter to get set. + /// + [TestMethod] + public void TestSetBoolParamEmptyAttribute() + { + ValidateTaskParameterNotSet("BoolParam", ""); + } + + /// + /// Validate that setting a bool param with a property which evaluates to nothing does not cause the parameter to get set. + /// + [TestMethod] + public void TestSetBoolParamEmptyProperty() + { + ValidateTaskParameterNotSet("BoolParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting a bool param with an item which evaluates to nothing does not cause the parameter to get set. + /// + [TestMethod] + public void TestSetBoolParamEmptyItem() + { + ValidateTaskParameterNotSet("BoolParam", "@(NonExistantItem)"); + } + + #endregion + + #region Bool Array Params + + /// + /// Validate that setting a bool array with a single true sets the array to one 'true' value. + /// + [TestMethod] + public void TestSetBoolArrayParamOneItem() + { + ValidateTaskParameterArray("BoolArrayParam", "true", new bool[] { true }); + } + + /// + /// Validate that setting a bool array with a list of two values sets them appropriately. + /// + [TestMethod] + public void TestSetBoolArrayParamTwoItems() + { + ValidateTaskParameterArray("BoolArrayParam", "false;true", new bool[] { false, true }); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetBoolArrayParamEmptyAttribute() + { + ValidateTaskParameterNotSet("BoolArrayParam", ""); + } + + /// + /// Validate that setting the parameter with a property which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetBoolArrayParamEmptyProperty() + { + ValidateTaskParameterNotSet("BoolArrayParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetBoolArrayParamEmptyItem() + { + ValidateTaskParameterNotSet("BoolArrayParam", "@(NonExistantItem)"); + } + + #endregion + + #region Int Params + + /// + /// Validate that setting an int param with a value of 0 causes it to get the correct value. + /// + [TestMethod] + public void TestSetIntParamZero() + { + ValidateTaskParameter("IntParam", "0", 0); + } + + /// + /// Validate that setting an int param with a value of 1 causes it to get the correct value. + /// + [TestMethod] + public void TestSetIntParamOne() + { + ValidateTaskParameter("IntParam", "1", 1); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetIntParamEmptyAttribute() + { + ValidateTaskParameterNotSet("IntParam", ""); + } + + /// + /// Validate that setting the parameter with a property which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetIntParamEmptyProperty() + { + ValidateTaskParameterNotSet("IntParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetIntParamEmptyItem() + { + ValidateTaskParameterNotSet("IntParam", "@(NonExistantItem)"); + } + + #endregion + + #region Int Array Params + + /// + /// Validate that setting an int array with a single value causes it to get a single value. + /// + [TestMethod] + public void TestSetIntArrayParamOneItem() + { + ValidateTaskParameterArray("IntArrayParam", "0", new int[] { 0 }); + } + + /// + /// Validate that setting an int array with a list of values causes it to get the correct values. + /// + [TestMethod] + public void TestSetIntArrayParamTwoItems() + { + SetTaskParameter("IntArrayParam", "1;0"); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey("IntArrayParam")); + + Assert.AreEqual(1, ((int[])_parametersSetOnTask["IntArrayParam"])[0]); + Assert.AreEqual(0, ((int[])_parametersSetOnTask["IntArrayParam"])[1]); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetIntArrayParamEmptyAttribute() + { + ValidateTaskParameterNotSet("IntArrayParam", ""); + } + + /// + /// Validate that setting the parameter with a property which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetIntArrayParamEmptyProperty() + { + ValidateTaskParameterNotSet("IntArrayParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetIntArrayParamEmptyItem() + { + ValidateTaskParameterNotSet("IntArrayParam", "@(NonExistantItem)"); + } + + #endregion + + #region String Params + + /// + /// Test that setting a string param sets the correct value. + /// + [TestMethod] + public void TestSetStringParam() + { + ValidateTaskParameter("StringParam", "0", "0"); + } + + /// + /// Test that setting a string param sets the correct value. + /// + [TestMethod] + public void TestSetStringParamOne() + { + ValidateTaskParameter("StringParam", "1", "1"); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetStringParamEmptyAttribute() + { + ValidateTaskParameterNotSet("StringParam", ""); + } + + /// + /// Validate that setting the parameter with a property which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetStringParamEmptyProperty() + { + ValidateTaskParameterNotSet("StringParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetStringParamEmptyItem() + { + ValidateTaskParameterNotSet("StringParam", "@(NonExistantItem)"); + } + + #endregion + + #region String Array Params + + /// + /// Validate that setting a string array with a single value sets the correct value. + /// + [TestMethod] + public void TestSetStringArrayParam() + { + ValidateTaskParameterArray("StringArrayParam", "0", new string[] { "0" }); + } + + /// + /// Validate that setting a string array with a list of two values sets the correct values. + /// + [TestMethod] + public void TestSetStringArrayParamOne() + { + ValidateTaskParameterArray("StringArrayParam", "1;0", new string[] { "1", "0" }); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetStringArrayParamEmptyAttribute() + { + ValidateTaskParameterNotSet("StringArrayParam", ""); + } + + /// + /// Validate that setting the parameter with a property which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetStringArrayParamEmptyProperty() + { + ValidateTaskParameterNotSet("StringArrayParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetStringArrayParamEmptyItem() + { + ValidateTaskParameterNotSet("StringArrayParam", "@(NonExistantItem)"); + } + + #endregion + + #region ITaskItem Params + + /// + /// Validate that setting an item with an item list evaluating to one item sets the value appropriately, including metadata. + /// + [TestMethod] + public void TestSetItemParamSingle() + { + ValidateTaskParameterItem("ItemParam", "@(ItemListContainingOneItem)", _oneItem[0]); + } + + /// + /// Validate that setting an item with an item list evaluating to two items sets the value appropriately, including metadata. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void TestSetItemParamDouble() + { + ValidateTaskParameterItems("ItemParam", "@(ItemListContainingTwoItems)", _twoItems); + } + + /// + /// Validate that setting an item with a string results in an item with the evaluated include set to the string. + /// + [TestMethod] + public void TestSetItemParamString() + { + ValidateTaskParameterItem("ItemParam", "MyItemName"); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetItemParamEmptyAttribute() + { + ValidateTaskParameterNotSet("ItemParam", ""); + } + + /// + /// Validate that setting the parameter with a property which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetItemParamEmptyProperty() + { + ValidateTaskParameterNotSet("ItemParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetItemParamEmptyItem() + { + ValidateTaskParameterNotSet("ItemParam", "@(NonExistantItem)"); + } + + #endregion + + #region ITaskItem Array Params + + /// + /// Validate that setting an item array using an item list containing one item sets a single item. + /// + [TestMethod] + public void TestSetItemArrayParamSingle() + { + ValidateTaskParameterItems("ItemArrayParam", "@(ItemListContainingOneItem)", _oneItem); + } + + /// + /// Validate that setting an item array using an item list containing two items sets both items. + /// + [TestMethod] + public void TestSetItemArrayParamDouble() + { + ValidateTaskParameterItems("ItemArrayParam", "@(ItemListContainingTwoItems)", _twoItems); + } + + /// + /// Validate that setting an item array with + /// + [TestMethod] + public void TestSetItemArrayParamString() + { + ValidateTaskParameterItems("ItemArrayParam", "MyItemName"); + } + + /// + /// Validate that setting an item array with a list with multiple values creates multiple items. + /// + [TestMethod] + public void TestSetItemArrayParamTwoStrings() + { + ValidateTaskParameterItems("ItemArrayParam", "MyItemName;MyOtherItemName", new string[] { "MyItemName", "MyOtherItemName" }); + } + + /// + /// Validate that setting the parameter with an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetItemArrayParamEmptyAttribute() + { + ValidateTaskParameterNotSet("ItemArrayParam", ""); + } + + /// + /// Validate that setting the parameter with a parameter which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetItemArrayParamEmptyProperty() + { + ValidateTaskParameterNotSet("ItemArrayParam", "$(NonExistantProperty)"); + } + + /// + /// Validate that setting the parameter with an item which evaluates to an empty value does not cause it to be set. + /// + [TestMethod] + public void TestSetItemArrayParamEmptyItem() + { + ValidateTaskParameterNotSet("ItemArrayParam", "@(NonExistantItem)"); + } + + #endregion + + #region Execute Tests + + /// + /// Tests that successful execution returns true. + /// + [TestMethod] + public void TestExecuteTrue() + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + parameters["ExecuteReturnParam"] = new Tuple("true", ElementLocation.Create("foo.proj")); + + Assert.IsTrue(_host.SetTaskParameters(parameters)); + + bool executeValue = _host.Execute(); + + Assert.AreEqual(true, executeValue); + } + + /// + /// Tests that unsuccessful execution returns false. + /// + [TestMethod] + public void TestExecuteFalse() + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + parameters["ExecuteReturnParam"] = new Tuple("false", ElementLocation.Create("foo.proj")); + + Assert.IsTrue(_host.SetTaskParameters(parameters)); + + bool executeValue = _host.Execute(); + + Assert.AreEqual(false, executeValue); + } + + /// + /// Tests that when Execute throws, the exception bubbles up. + /// + [TestMethod] + [ExpectedException(typeof(IndexOutOfRangeException))] + public void TestExecuteThrow() + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + parameters["ExecuteReturnParam"] = new Tuple("false", ElementLocation.Create("foo.proj")); + + TearDown(); + InitializeHost(true); + + Assert.IsTrue(_host.SetTaskParameters(parameters)); + + bool executeValue = _host.Execute(); + } + + #endregion + + #region Bool Outputs + + /// + /// Validate that boolean output to an item produces the correct evaluated include. + /// + [TestMethod] + public void TestOutputBoolToItem() + { + SetTaskParameter("BoolParam", "true"); + ValidateOutputItem("BoolOutput", "True"); + } + + /// + /// Validate that boolean output to a property produces the correct evaluated value. + /// + [TestMethod] + public void TestOutputBoolToProperty() + { + SetTaskParameter("BoolParam", "true"); + ValidateOutputProperty("BoolOutput", "True"); + } + + /// + /// Validate that boolean array output to an item array produces the correct evaluated includes. + /// + [TestMethod] + public void TestOutputBoolArrayToItems() + { + SetTaskParameter("BoolArrayParam", "false;true"); + ValidateOutputItems("BoolArrayOutput", new string[] { "False", "True" }); + } + + /// + /// Validate that boolean array output to an item produces the correct semi-colon-delimited evaluated value. + /// + [TestMethod] + public void TestOutputBoolArrayToProperty() + { + SetTaskParameter("BoolArrayParam", "false;true"); + ValidateOutputProperty("BoolArrayOutput", "False;True"); + } + + #endregion + + #region Int Outputs + + /// + /// Validate that an int output to an item produces the correct evaluated include + /// + [TestMethod] + public void TestOutputIntToItem() + { + SetTaskParameter("IntParam", "42"); + ValidateOutputItem("IntOutput", "42"); + } + + /// + /// Validate that an int output to an property produces the correct evaluated value. + /// + [TestMethod] + public void TestOutputIntToProperty() + { + SetTaskParameter("IntParam", "42"); + ValidateOutputProperty("IntOutput", "42"); + } + + /// + /// Validate that an int array output to an item produces the correct evaluated includes. + /// + [TestMethod] + public void TestOutputIntArrayToItems() + { + SetTaskParameter("IntArrayParam", "42;99"); + ValidateOutputItems("IntArrayOutput", new string[] { "42", "99" }); + } + + /// + /// Validate that an int array output to a property produces the correct semi-colon-delimiated evaluated value. + /// + [TestMethod] + public void TestOutputIntArrayToProperty() + { + SetTaskParameter("IntArrayParam", "42;99"); + ValidateOutputProperty("IntArrayOutput", "42;99"); + } + + #endregion + + #region String Outputs + + /// + /// Validate that a string output to an item produces the correct evaluated include. + /// + [TestMethod] + public void TestOutputStringToItem() + { + SetTaskParameter("StringParam", "FOO"); + ValidateOutputItem("StringOutput", "FOO"); + } + + /// + /// Validate that a string output to a property produces the correct evaluated value. + /// + [TestMethod] + public void TestOutputStringToProperty() + { + SetTaskParameter("StringParam", "FOO"); + ValidateOutputProperty("StringOutput", "FOO"); + } + + /// + /// Validate that an empty string output overwrites the property value + /// + [TestMethod] + public void TestOutputEmptyStringToProperty() + { + _bucket.Lookup.SetProperty(ProjectPropertyInstance.Create("output", "initialvalue")); + ValidateOutputProperty("EmptyStringOutput", String.Empty); + } + + /// + /// Validate that an empty string array output overwrites the property value + /// + [TestMethod] + public void TestOutputEmptyStringArrayToProperty() + { + _bucket.Lookup.SetProperty(ProjectPropertyInstance.Create("output", "initialvalue")); + ValidateOutputProperty("EmptyStringArrayOutput", String.Empty); + } + + /// + /// A string output returning null should not cause any property set. + /// + [TestMethod] + public void TestOutputNullStringToProperty() + { + _bucket.Lookup.SetProperty(ProjectPropertyInstance.Create("output", "initialvalue")); + ValidateOutputProperty("NullStringOutput", "initialvalue"); + } + + /// + /// A string output returning null should not cause any property set. + /// + [TestMethod] + public void TestOutputNullITaskItemToProperty() + { + _bucket.Lookup.SetProperty(ProjectPropertyInstance.Create("output", "initialvalue")); + ValidateOutputProperty("NullITaskItemOutput", "initialvalue"); + } + + /// + /// A string output returning null should not cause any property set. + /// + [TestMethod] + public void TestOutputNullStringArrayToProperty() + { + _bucket.Lookup.SetProperty(ProjectPropertyInstance.Create("output", "initialvalue")); + ValidateOutputProperty("NullStringArrayOutput", "initialvalue"); + } + + /// + /// A string output returning null should not cause any property set. + /// + [TestMethod] + public void TestOutputNullITaskItemArrayToProperty() + { + _bucket.Lookup.SetProperty(ProjectPropertyInstance.Create("output", "initialvalue")); + ValidateOutputProperty("NullITaskItemArrayOutput", "initialvalue"); + } + + /// + /// Validate that a string array output to an item produces the correct evaluated includes. + /// + [TestMethod] + public void TestOutputStringArrayToItems() + { + SetTaskParameter("StringArrayParam", "FOO;bar"); + ValidateOutputItems("StringArrayOutput", new string[] { "FOO", "bar" }); + } + + /// + /// Validate that a string array output to a property produces the correct semi-colon-delimited evaluated value. + /// + [TestMethod] + public void TestOutputStringArrayToProperty() + { + SetTaskParameter("StringArrayParam", "FOO;bar"); + ValidateOutputProperty("StringArrayOutput", "FOO;bar"); + } + + #endregion + + #region Item Outputs + + /// + /// Validate that an item output to an item replicates the item, with metadata + /// + [TestMethod] + public void TestOutputItemToItem() + { + SetTaskParameter("ItemParam", "@(ItemListContainingOneItem)"); + ValidateOutputItems("ItemOutput", _oneItem); + } + + /// + /// Validate than an item output to a property produces the correct evaluated value. + /// + [TestMethod] + public void TestOutputItemToProperty() + { + SetTaskParameter("ItemParam", "@(ItemListContainingOneItem)"); + ValidateOutputProperty("ItemOutput", _oneItem[0].ItemSpec); + } + + /// + /// Validate that an item array output to an item replicates the items, with metadata. + /// + [TestMethod] + public void TestOutputItemArrayToItems() + { + SetTaskParameter("ItemArrayParam", "@(ItemListContainingTwoItems)"); + ValidateOutputItems("ItemArrayOutput", _twoItems); + } + + /// + /// Validate that an item array output to a property produces the correct semi-colon-demlimited evaluated value. + /// + [TestMethod] + public void TestOutputItemArrayToProperty() + { + SetTaskParameter("ItemArrayParam", "@(ItemListContainingTwoItems)"); + ValidateOutputProperty("ItemArrayOutput", String.Concat(_twoItems[0].ItemSpec, ";", _twoItems[1].ItemSpec)); + } + + #endregion + + #region Other Output Tests + + /// + /// Attempts to gather outputs into an item list from an string task parameter that + /// returns an empty string. This should be a no-op. + /// + [TestMethod] + public void TestEmptyStringInStringArrayParameterIntoItemList() + { + SetTaskParameter("StringArrayParam", ""); + ValidateOutputItems("StringArrayOutput", new ITaskItem[] { }); + } + + /// + /// Attempts to gather outputs into an item list from an string task parameter that + /// returns an empty string. This should be a no-op. + /// + [TestMethod] + public void TestEmptyStringParameterIntoItemList() + { + SetTaskParameter("StringParam", ""); + ValidateOutputItems("StringOutput", new ITaskItem[] { }); + } + + /// + /// Attempts to gather outputs from a null task parameter of type "ITaskItem[]". This should succeed. + /// + [TestMethod] + public void TestNullITaskItemArrayParameter() + { + ValidateOutputItems("ItemArrayNullOutput", new ITaskItem[] { }); + } + + /// + /// Attempts to gather outputs from a task parameter of type "ArrayList". This should fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void TestArrayListParameter() + { + ValidateOutputItems("ArrayListOutput", new ITaskItem[] { }); + } + + /// + /// Attempts to gather outputs from a non-existant output. This should fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void TestNonexistantOutput() + { + Assert.IsFalse(_host.GatherTaskOutputs("NonExistantOutput", ElementLocation.Create(".", 1, 1), true, "output")); + } + + /// + /// object[] should not be a supported output type. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void TestOutputObjectArrayToProperty() + { + ValidateOutputProperty("ObjectArrayOutput", ""); + } + + #endregion + + #region Other Tests + + /// + /// Test that cleanup for task clears out the task instance. + /// + [TestMethod] + public void TestCleanupForTask() + { + _host.CleanupForBatch(); + Assert.IsNotNull((_host as TaskExecutionHost)._UNITTESTONLY_TaskFactoryWrapper); + _host.CleanupForTask(); + Assert.IsNull((_host as TaskExecutionHost)._UNITTESTONLY_TaskFactoryWrapper); + } + + /// + /// Test that a using task which specifies an invalid assembly produces an exception. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void TestTaskResolutionFailureWithUsingTask() + { + _loggingService = new MockLoggingService(); + TearDown(); + _host = new TaskExecutionHost(); + TargetLoggingContext tlc = new TargetLoggingContext(_loggingService, new BuildEventContext(1, 1, BuildEventContext.InvalidProjectContextId, 1)); + + ProjectInstance project = CreateTestProject(); + _host.InitializeForTask + ( + this, + tlc, + project, + "TaskWithMissingAssembly", + ElementLocation.Create("none", 1, 1), + this, + false, + null, + false, + CancellationToken.None + ); + _host.FindTask(null); + _host.InitializeForBatch(new TaskLoggingContext(_loggingService, tlc.BuildEventContext), _bucket, null); + } + + /// + /// Test that specifying a task with no using task logs an error, but does not throw. + /// + [TestMethod] + public void TestTaskResolutionFailureWithNoUsingTask() + { + TearDown(); + _host = new TaskExecutionHost(); + TargetLoggingContext tlc = new TargetLoggingContext(_loggingService, new BuildEventContext(1, 1, BuildEventContext.InvalidProjectContextId, 1)); + + ProjectInstance project = CreateTestProject(); + _host.InitializeForTask + ( + this, + tlc, + project, + "TaskWithNoUsingTask", + ElementLocation.Create("none", 1, 1), + this, + false, + null, + false, + CancellationToken.None + ); + + _host.FindTask(null); + _host.InitializeForBatch(new TaskLoggingContext(_loggingService, tlc.BuildEventContext), _bucket, null); + _logger.AssertLogContains("MSB4036"); + } + + #endregion + + #region ITestTaskHost Members + + /// + /// Records that a parameter was set on the task. + /// + public void ParameterSet(string parameterName, object valueSet) + { + _parametersSetOnTask[parameterName] = valueSet; + } + + /// + /// Records that an output was read from the task. + /// + public void OutputRead(string parameterName, object actualValue) + { + _outputsReadFromTask[parameterName] = actualValue; + } + + #endregion + + #region IBuildEngine2 Members + + /// + /// Unused. + /// + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion) + { + throw new NotImplementedException(); + } + + /// + /// Unused. + /// + public bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) + { + throw new NotImplementedException(); + } + + #endregion + + #region IBuildEngine Members + + /// + /// Unused. + /// + public void LogErrorEvent(BuildErrorEventArgs e) + { + throw new NotImplementedException(); + } + + /// + /// Unused. + /// + public void LogWarningEvent(BuildWarningEventArgs e) + { + throw new NotImplementedException(); + } + + /// + /// Unused. + /// + public void LogMessageEvent(BuildMessageEventArgs e) + { + throw new NotImplementedException(); + } + + /// + /// Unused. + /// + public void LogCustomEvent(CustomBuildEventArgs e) + { + throw new NotImplementedException(); + } + + /// + /// Unused. + /// + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + throw new NotImplementedException(); + } + + #endregion + + #region Validation Routines + + /// + /// Is the class a task factory + /// + private static bool IsTaskFactoryClass(Type type, object unused) + { + return (type.IsClass && + !type.IsAbstract && + (type.GetInterface("Microsoft.Build.Framework.ITaskFactory") != null)); + } + + /// + /// Initialize the host object + /// + /// Should the task throw when executed + private void InitializeHost(bool throwOnExecute) + { + _loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1) as ILoggingService; + _logger = new MockLogger(); + _loggingService.RegisterLogger(_logger); + _host = new TaskExecutionHost(); + TargetLoggingContext tlc = new TargetLoggingContext(_loggingService, new BuildEventContext(1, 1, BuildEventContext.InvalidProjectContextId, 1)); + + // Set up a temporary project and add some items to it. + ProjectInstance project = CreateTestProject(); + + TypeLoader typeLoader = new TypeLoader(new TypeFilter(IsTaskFactoryClass)); + AssemblyLoadInfo loadInfo = AssemblyLoadInfo.Create(Assembly.GetAssembly(typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory)).FullName, null); + LoadedType loadedType = new LoadedType(typeof(TaskBuilderTestTask.TaskBuilderTestTaskFactory), loadInfo); + + TaskBuilderTestTask.TaskBuilderTestTaskFactory taskFactory = new TaskBuilderTestTask.TaskBuilderTestTaskFactory(); + taskFactory.ThrowOnExecute = throwOnExecute; + string taskName = "TaskBuilderTestTask"; + (_host as TaskExecutionHost)._UNITTESTONLY_TaskFactoryWrapper = new TaskFactoryWrapper(taskFactory, loadedType, taskName, null); + _host.InitializeForTask + ( + this, + tlc, + project, + taskName, + ElementLocation.Create("none", 1, 1), + this, + false, + null, + false, + CancellationToken.None + ); + + ProjectTaskInstance taskInstance = project.Targets["foo"].Tasks.First(); + TaskLoggingContext talc = tlc.LogTaskBatchStarted(".", taskInstance); + + ItemDictionary itemsByName = new ItemDictionary(); + + ProjectItemInstance item = new ProjectItemInstance(project, "ItemListContainingOneItem", "a.cs", "."); + item.SetMetadata("Culture", "fr-fr"); + itemsByName.Add(item); + _oneItem = new ITaskItem[] { new TaskItem(item) }; + + item = new ProjectItemInstance(project, "ItemListContainingTwoItems", "b.cs", "."); + ProjectItemInstance item2 = new ProjectItemInstance(project, "ItemListContainingTwoItems", "c.cs", "."); + item.SetMetadata("HintPath", "c:\\foo"); + item2.SetMetadata("HintPath", "c:\\bar"); + itemsByName.Add(item); + itemsByName.Add(item2); + _twoItems = new ITaskItem[] { new TaskItem(item), new TaskItem(item2) }; + + _bucket = new ItemBucket(new string[0], new Dictionary(), new Lookup(itemsByName, new PropertyDictionary(), null), 0); + _host.FindTask(null); + _host.InitializeForBatch(talc, _bucket, null); + _parametersSetOnTask = new Dictionary(StringComparer.OrdinalIgnoreCase); + _outputsReadFromTask = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Helper method for tests + /// + private void ValidateOutputItem(string outputName, string value) + { + Assert.IsTrue(_host.GatherTaskOutputs(outputName, ElementLocation.Create(".", 1, 1), true, "output")); + Assert.IsTrue(_outputsReadFromTask.ContainsKey(outputName)); + + Assert.AreEqual(1, _bucket.Lookup.GetItems("output").Count); + Assert.AreEqual(value, _bucket.Lookup.GetItems("output").First().EvaluatedInclude); + } + + /// + /// Helper method for tests + /// + private void ValidateOutputItem(string outputName, ITaskItem value) + { + Assert.IsTrue(_host.GatherTaskOutputs(outputName, ElementLocation.Create(".", 1, 1), true, "output")); + Assert.IsTrue(_outputsReadFromTask.ContainsKey(outputName)); + + Assert.AreEqual(1, _bucket.Lookup.GetItems("output").Count); + Assert.AreEqual(0, TaskItemComparer.Instance.Compare(value, new TaskItem(_bucket.Lookup.GetItems("output").First()))); + } + + /// + /// Helper method for tests + /// + private void ValidateOutputItems(string outputName, string[] values) + { + Assert.IsTrue(_host.GatherTaskOutputs(outputName, ElementLocation.Create(".", 1, 1), true, "output")); + Assert.IsTrue(_outputsReadFromTask.ContainsKey(outputName)); + + Assert.AreEqual(values.Length, _bucket.Lookup.GetItems("output").Count); + for (int i = 0; i < values.Length; i++) + { + Assert.AreEqual(values[i], _bucket.Lookup.GetItems("output").ElementAt(i).EvaluatedInclude); + } + } + + /// + /// Helper method for tests + /// + private void ValidateOutputItems(string outputName, ITaskItem[] values) + { + Assert.IsTrue(_host.GatherTaskOutputs(outputName, ElementLocation.Create(".", 1, 1), true, "output")); + Assert.IsTrue(_outputsReadFromTask.ContainsKey(outputName)); + + Assert.AreEqual(values.Length, _bucket.Lookup.GetItems("output").Count); + for (int i = 0; i < values.Length; i++) + { + Assert.AreEqual(0, TaskItemComparer.Instance.Compare(values[i], new TaskItem(_bucket.Lookup.GetItems("output").ElementAt(i)))); + } + } + + /// + /// Helper method for tests + /// + private void ValidateOutputProperty(string outputName, string value) + { + Assert.IsTrue(_host.GatherTaskOutputs(outputName, ElementLocation.Create(".", 1, 1), false, "output")); + Assert.IsTrue(_outputsReadFromTask.ContainsKey(outputName)); + + Assert.IsNotNull(_bucket.Lookup.GetProperty("output")); + Assert.AreEqual(value, _bucket.Lookup.GetProperty("output").EvaluatedValue); + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameter(string parameterName, string value, object expectedValue) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + Assert.AreEqual(expectedValue, _parametersSetOnTask[parameterName]); + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterItem(string parameterName, string value) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + + ITaskItem actualItem = _parametersSetOnTask[parameterName] as ITaskItem; + Assert.AreEqual(value, actualItem.ItemSpec); + Assert.AreEqual(BuiltInMetadata.MetadataCount, actualItem.MetadataCount); + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterItem(string parameterName, string value, ITaskItem expectedItem) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + + ITaskItem actualItem = _parametersSetOnTask[parameterName] as ITaskItem; + Assert.AreEqual(0, TaskItemComparer.Instance.Compare(expectedItem, actualItem)); + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterItems(string parameterName, string value) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + + ITaskItem[] actualItems = _parametersSetOnTask[parameterName] as ITaskItem[]; + Assert.AreEqual(1, actualItems.Length); + Assert.AreEqual(value, actualItems[0].ItemSpec); + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterItems(string parameterName, string value, ITaskItem[] expectedItems) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + + ITaskItem[] actualItems = _parametersSetOnTask[parameterName] as ITaskItem[]; + Assert.AreEqual(expectedItems.Length, actualItems.Length); + + for (int i = 0; i < expectedItems.Length; i++) + { + Assert.AreEqual(0, TaskItemComparer.Instance.Compare(expectedItems[i], actualItems[i])); + } + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterItems(string parameterName, string value, string[] expectedItems) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + + ITaskItem[] actualItems = _parametersSetOnTask[parameterName] as ITaskItem[]; + Assert.AreEqual(expectedItems.Length, actualItems.Length); + + for (int i = 0; i < expectedItems.Length; i++) + { + Assert.AreEqual(expectedItems[i], actualItems[i].ItemSpec); + } + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterArray(string parameterName, string value, object expectedValue) + { + SetTaskParameter(parameterName, value); + + Assert.IsTrue(_parametersSetOnTask.ContainsKey(parameterName)); + + Array expectedArray = expectedValue as Array; + Array actualArray = _parametersSetOnTask[parameterName] as Array; + + Assert.AreEqual(expectedArray.Length, actualArray.Length); + for (int i = 0; i < expectedArray.Length; i++) + { + Assert.AreEqual(expectedArray.GetValue(i), actualArray.GetValue(i)); + } + } + + /// + /// Helper method for tests + /// + private void ValidateTaskParameterNotSet(string parameterName, string value) + { + SetTaskParameter(parameterName, value); + Assert.IsFalse(_parametersSetOnTask.ContainsKey(parameterName)); + } + + #endregion + + /// + /// Helper method for tests + /// + private void SetTaskParameter(string parameterName, string value) + { + Dictionary> parameters = GetStandardParametersDictionary(true); + parameters[parameterName] = new Tuple(value, ElementLocation.Create("foo.proj")); + bool success = _host.SetTaskParameters(parameters); + Assert.IsTrue(success); + } + + /// + /// Helper method for tests + /// + private Dictionary> GetStandardParametersDictionary(bool returnParam) + { + Dictionary> parameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + parameters["ExecuteReturnParam"] = new Tuple(returnParam ? "true" : "false", ElementLocation.Create("foo.proj")); + return parameters; + } + + /// + /// Helper method for tests + /// + private IElementLocation GetParameterLocation(string name) + { + return ElementLocation.Create(".", 1, 1); + } + + /// + /// Creates a test project. + /// + /// The project. + private ProjectInstance CreateTestProject() + { + string projectFileContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectFileContents))); + return project.CreateProjectInstance(); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostConfiguration_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostConfiguration_Tests.cs new file mode 100644 index 00000000000..c61455034a4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostConfiguration_Tests.cs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit Tests for TaskHostConfiguration packet. +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Unit Tests for TaskHostConfiguration packet. + /// + [TestClass] + public class TaskHostConfiguration_Tests + { + /// + /// Override for ContinueOnError + /// + private bool _continueOnErrorDefault = true; + + /// + /// Test that an exception is thrown when the task name is null. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ConstructorWithNullName() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, null, @"c:\my tasks\mytask.dll", null); + } + + /// + /// Test that an exception is thrown when the task name is empty. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ConstructorWithEmptyName() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, String.Empty, @"c:\my tasks\mytask.dll", null); + } + + /// + /// Test that an exception is thrown when the path to the task assembly is null + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ConstructorWithNullLocation() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", null, null); + } + + /// + /// Test that an exception is thrown when the path to the task assembly is empty + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ConstructorWithEmptyLocation() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", String.Empty, null); + } + + /// + /// Test the valid constructors. + /// + [TestMethod] + public void TestValidConstructors() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", null); + TaskHostConfiguration config2 = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", null); + + IDictionary parameters = new Dictionary(); + TaskHostConfiguration config3 = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", parameters); + + IDictionary parameters2 = new Dictionary(); + parameters2.Add("Text", "Hello!"); + parameters2.Add("MyBoolValue", true); + parameters2.Add("MyITaskItem", new TaskItem("ABC")); + parameters2.Add("ItemArray", new ITaskItem[] { new TaskItem("DEF"), new TaskItem("GHI"), new TaskItem("JKL") }); + + TaskHostConfiguration config4 = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", parameters2); + } + + /// + /// Test serialization / deserialization when the parameter dictionary is null. + /// + [TestMethod] + public void TestTranslationWithNullDictionary() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", null); + + ((INodePacketTranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration; + + Assert.AreEqual(config.TaskName, deserializedConfig.TaskName); + Assert.AreEqual(config.TaskLocation, config.TaskLocation); + Assert.IsNull(deserializedConfig.TaskParameters); + } + + /// + /// Test serialization / deserialization when the parameter dictionary is empty. + /// + [TestMethod] + public void TestTranslationWithEmptyDictionary() + { + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", new Dictionary()); + + ((INodePacketTranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration; + + Assert.AreEqual(config.TaskName, deserializedConfig.TaskName); + Assert.AreEqual(config.TaskLocation, config.TaskLocation); + Assert.IsNotNull(deserializedConfig.TaskParameters); + Assert.AreEqual(config.TaskParameters.Count, deserializedConfig.TaskParameters.Count); + } + + /// + /// Test serialization / deserialization when the parameter dictionary contains just value types. + /// + [TestMethod] + public void TestTranslationWithValueTypesInDictionary() + { + IDictionary parameters = new Dictionary(); + parameters.Add("Text", "Foo"); + parameters.Add("BoolValue", false); + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", parameters); + + ((INodePacketTranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration; + + Assert.AreEqual(config.TaskName, deserializedConfig.TaskName); + Assert.AreEqual(config.TaskLocation, config.TaskLocation); + Assert.IsNotNull(deserializedConfig.TaskParameters); + Assert.AreEqual(config.TaskParameters.Count, deserializedConfig.TaskParameters.Count); + Assert.AreEqual(config.TaskParameters["Text"].WrappedParameter, deserializedConfig.TaskParameters["Text"].WrappedParameter); + Assert.AreEqual(config.TaskParameters["BoolValue"].WrappedParameter, deserializedConfig.TaskParameters["BoolValue"].WrappedParameter); + } + + /// + /// Test serialization / deserialization when the parameter dictionary contains an ITaskItem. + /// + [TestMethod] + public void TestTranslationWithITaskItemInDictionary() + { + IDictionary parameters = new Dictionary(); + parameters.Add("TaskItemValue", new TaskItem("Foo")); + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", parameters); + + ((INodePacketTranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration; + + Assert.AreEqual(config.TaskName, deserializedConfig.TaskName); + Assert.AreEqual(config.TaskLocation, config.TaskLocation); + Assert.IsNotNull(deserializedConfig.TaskParameters); + Assert.AreEqual(config.TaskParameters.Count, deserializedConfig.TaskParameters.Count); + TaskHostPacketHelpers.AreEqual((ITaskItem)config.TaskParameters["TaskItemValue"].WrappedParameter, (ITaskItem)deserializedConfig.TaskParameters["TaskItemValue"].WrappedParameter); + } + + /// + /// Test serialization / deserialization when the parameter dictionary contains an ITaskItem array. + /// + [TestMethod] + public void TestTranslationWithITaskItemArrayInDictionary() + { + IDictionary parameters = new Dictionary(); + parameters.Add("TaskItemArrayValue", new ITaskItem[] { new TaskItem("Foo"), new TaskItem("Baz") }); + TaskHostConfiguration config = new TaskHostConfiguration(1, Environment.CurrentDirectory, null, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture, null, 1, 1, @"c:\my project\myproj.proj", _continueOnErrorDefault, "TaskName", @"c:\MyTasks\MyTask.dll", parameters); + + ((INodePacketTranslatable)config).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostConfiguration.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostConfiguration deserializedConfig = packet as TaskHostConfiguration; + + Assert.AreEqual(config.TaskName, deserializedConfig.TaskName); + Assert.AreEqual(config.TaskLocation, config.TaskLocation); + Assert.IsNotNull(deserializedConfig.TaskParameters); + Assert.AreEqual(config.TaskParameters.Count, deserializedConfig.TaskParameters.Count); + + ITaskItem[] itemArray = (ITaskItem[])config.TaskParameters["TaskItemArrayValue"].WrappedParameter; + ITaskItem[] deserializedItemArray = (ITaskItem[])deserializedConfig.TaskParameters["TaskItemArrayValue"].WrappedParameter; + + TaskHostPacketHelpers.AreEqual(itemArray, deserializedItemArray); + } + + /// + /// Helper methods for testing the task host-related packets. + /// + internal static class TaskHostPacketHelpers + { + /// + /// Asserts the equality (or lack thereof) of two arrays of ITaskItems. + /// + internal static void AreEqual(ITaskItem[] x, ITaskItem[] y) + { + if (x == null && y == null) + { + return; + } + + if (x == null || y == null) + { + Assert.Fail("The two item lists are not equal -- one of them is null"); + } + + if (x.Length != y.Length) + { + Assert.Fail("The two item lists have different lengths, so they cannot be equal"); + } + + for (int i = 0; i < x.Length; i++) + { + AreEqual(x[i], y[i]); + } + } + + /// + /// Asserts the equality (or lack thereof) of two ITaskItems. + /// + internal static void AreEqual(ITaskItem x, ITaskItem y) + { + if (x == null && y == null) + { + return; + } + + if (x == null || y == null) + { + Assert.Fail("The two items are not equal -- one of them is null"); + } + + Assert.AreEqual(x.ItemSpec, y.ItemSpec); + + IDictionary metadataFromX = x.CloneCustomMetadata(); + IDictionary metadataFromY = y.CloneCustomMetadata(); + + if (x == null && y == null) + { + return; + } + + if (x == null || y == null) + { + Assert.Fail("The two items are not equal -- one of them is null"); + } + + Assert.AreEqual(metadataFromX.Count, metadataFromY.Count); + + foreach (object metadataName in metadataFromX.Keys) + { + if (!metadataFromY.Contains(metadataName)) + { + Assert.Fail("Only one item contains the '{0}' metadata", metadataName.ToString()); + } + else + { + Assert.AreEqual(metadataFromX[metadataName], metadataFromY[metadataName]); + } + } + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskCancelled_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskCancelled_Tests.cs new file mode 100644 index 00000000000..d39c47e6556 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskCancelled_Tests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit Tests for TaskHostTaskCancelled packet. +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Unit Tests for TaskHostTaskCancelled packet. + /// + [TestClass] + public class TaskHostTaskCancelled_Tests + { + /// + /// Basic test of the constructor. + /// + [TestMethod] + public void TestConstructor() + { + TaskHostTaskCancelled cancelled = new TaskHostTaskCancelled(); + } + + /// + /// Basic test of serialization / deserialization. + /// + [TestMethod] + public void TestTranslation() + { + TaskHostTaskCancelled cancelled = new TaskHostTaskCancelled(); + + ((INodePacketTranslatable)cancelled).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostTaskCancelled.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostTaskCancelled deserializedCancelled = packet as TaskHostTaskCancelled; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs new file mode 100644 index 00000000000..312cf6f42d9 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit Tests for TaskHostTaskComplete packet. +//----------------------------------------------------------------------- + +using System; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +using TaskHostPacketHelpers = Microsoft.Build.UnitTests.BackEnd.TaskHostConfiguration_Tests.TaskHostPacketHelpers; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Unit Tests for TaskHostTaskComplete packet. + /// + [TestClass] + public class TaskHostTaskComplete_Tests + { + /// + /// Tests various valid ways to construct this packet. + /// + [TestMethod] + public void TestConstructors() + { + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success), null); + TaskHostTaskComplete complete2 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure), null); + TaskHostTaskComplete complete3 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringInitialization, new ArgumentOutOfRangeException()), null); + TaskHostTaskComplete complete4 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, new ArgumentNullException()), null); + + IDictionary parameters = new Dictionary(); + TaskHostTaskComplete complete5 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null); + + IDictionary parameters2 = new Dictionary(); + parameters2.Add("Text", "Hello!"); + parameters2.Add("MyBoolValue", true); + parameters2.Add("MyITaskItem", new TaskItem("ABC")); + parameters2.Add("ItemArray", new ITaskItem[] { new TaskItem("DEF"), new TaskItem("GHI"), new TaskItem("JKL") }); + + TaskHostTaskComplete complete6 = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters2), null); + } + + /// + /// Test invalid constructor permutations. + /// + [TestMethod] + public void TestInvalidConstructors() + { + AssertInvalidConstructorThrows(typeof(InternalErrorException), TaskCompleteType.CrashedDuringExecution, null, "ExceptionlessErrorMessage", null, null, null); + AssertInvalidConstructorThrows(typeof(InternalErrorException), TaskCompleteType.CrashedDuringInitialization, null, null, null, null, null); + AssertInvalidConstructorThrows(typeof(InternalErrorException), TaskCompleteType.Success, new ArgumentNullException(), "ExceptionlessErrorMessage", null, null, null); + AssertInvalidConstructorThrows(typeof(InternalErrorException), TaskCompleteType.CrashedDuringExecution, null, null, new string[1] { "Foo" }, null, null); + } + + /// + /// Test serialization / deserialization when the parameter dictionary is null + /// + [TestMethod] + public void TestTranslationWithNullDictionary() + { + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success), null); + + ((INodePacketTranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostTaskComplete deserializedComplete = packet as TaskHostTaskComplete; + + Assert.AreEqual(complete.TaskResult, deserializedComplete.TaskResult); + Assert.IsNotNull(deserializedComplete.TaskOutputParameters); + Assert.AreEqual(0, deserializedComplete.TaskOutputParameters.Count); + } + + /// + /// Test serialization / deserialization when the parameter dictionary is empty + /// + [TestMethod] + public void TestTranslationWithEmptyDictionary() + { + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, new Dictionary()), null); + + ((INodePacketTranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostTaskComplete deserializedComplete = packet as TaskHostTaskComplete; + + Assert.AreEqual(complete.TaskResult, deserializedComplete.TaskResult); + Assert.IsNotNull(deserializedComplete.TaskOutputParameters); + Assert.AreEqual(complete.TaskOutputParameters.Count, deserializedComplete.TaskOutputParameters.Count); + } + + /// + /// Test serialization / deserialization when the parameter dictionary contains only value types + /// + [TestMethod] + public void TestTranslationWithValueTypesInDictionary() + { + IDictionary parameters = new Dictionary(); + parameters.Add("Text", "Foo"); + parameters.Add("BoolValue", false); + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null); + + ((INodePacketTranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostTaskComplete deserializedComplete = packet as TaskHostTaskComplete; + + Assert.AreEqual(complete.TaskResult, deserializedComplete.TaskResult); + Assert.IsNotNull(deserializedComplete.TaskOutputParameters); + Assert.AreEqual(complete.TaskOutputParameters.Count, deserializedComplete.TaskOutputParameters.Count); + Assert.AreEqual(complete.TaskOutputParameters["Text"].WrappedParameter, deserializedComplete.TaskOutputParameters["Text"].WrappedParameter); + Assert.AreEqual(complete.TaskOutputParameters["BoolValue"].WrappedParameter, deserializedComplete.TaskOutputParameters["BoolValue"].WrappedParameter); + } + + /// + /// Test serialization / deserialization when the parameter dictionary contains an ITaskItem. + /// + [TestMethod] + public void TestTranslationWithITaskItemInDictionary() + { + IDictionary parameters = new Dictionary(); + parameters.Add("TaskItemValue", new TaskItem("Foo")); + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null); + + ((INodePacketTranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostTaskComplete deserializedComplete = packet as TaskHostTaskComplete; + + Assert.AreEqual(complete.TaskResult, deserializedComplete.TaskResult); + Assert.IsNotNull(deserializedComplete.TaskOutputParameters); + Assert.AreEqual(complete.TaskOutputParameters.Count, deserializedComplete.TaskOutputParameters.Count); + TaskHostPacketHelpers.AreEqual((ITaskItem)complete.TaskOutputParameters["TaskItemValue"].WrappedParameter, (ITaskItem)deserializedComplete.TaskOutputParameters["TaskItemValue"].WrappedParameter); + } + + /// + /// Test serialization / deserialization when the parameter dictionary contains an ITaskItem array. + /// + [TestMethod] + public void TestTranslationWithITaskItemArrayInDictionary() + { + IDictionary parameters = new Dictionary(); + parameters.Add("TaskItemArrayValue", new ITaskItem[] { new TaskItem("Foo"), new TaskItem("Baz") }); + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.Success, parameters), null); + + ((INodePacketTranslatable)complete).Translate(TranslationHelpers.GetWriteTranslator()); + INodePacket packet = TaskHostTaskComplete.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + TaskHostTaskComplete deserializedComplete = packet as TaskHostTaskComplete; + + Assert.AreEqual(complete.TaskResult, deserializedComplete.TaskResult); + Assert.IsNotNull(deserializedComplete.TaskOutputParameters); + Assert.AreEqual(complete.TaskOutputParameters.Count, deserializedComplete.TaskOutputParameters.Count); + + ITaskItem[] itemArray = (ITaskItem[])complete.TaskOutputParameters["TaskItemArrayValue"].WrappedParameter; + ITaskItem[] deserializedItemArray = (ITaskItem[])deserializedComplete.TaskOutputParameters["TaskItemArrayValue"].WrappedParameter; + + TaskHostPacketHelpers.AreEqual(itemArray, deserializedItemArray); + } + + /// + /// Helper method for testing invalid constructors + /// + private void AssertInvalidConstructorThrows(Type expectedExceptionType, TaskCompleteType taskResult, Exception taskException, string taskExceptionMessage, string[] taskExceptionMessageArgs, IDictionary taskOutputParameters, IDictionary buildProcessEnvironment) + { + bool exceptionCaught = false; + + try + { + TaskHostTaskComplete complete = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(taskResult, taskOutputParameters, taskException, taskExceptionMessage, taskExceptionMessageArgs), buildProcessEnvironment); + } + catch (Exception e) + { + exceptionCaught = true; + Assert.IsInstanceOfType(e, expectedExceptionType, "Wrong exception was thrown!"); + } + + Assert.IsTrue(exceptionCaught, "No exception was caught when one was expected!"); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHost_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHost_Tests.cs new file mode 100644 index 00000000000..6e96fc61297 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskHost_Tests.cs @@ -0,0 +1,1306 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the TaskHost +//----------------------------------------------------------------------- + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd.Logging; +using System.Collections.Generic; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using System.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Unittest; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using System.Threading.Tasks; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Test the task host class which acts as a communication mechanism between tasks and the msbuild engine. + /// + [TestClass] + public class TaskHost_Tests + { + /// + /// Task host for the test + /// + private TaskHost _taskHost; + + /// + /// Mock host for the tests + /// + private MockHost _mockHost; + + /// + /// Custom logger for the tests + /// + private MyCustomLogger _customLogger; + + /// + /// Element location for the tests + /// + private ElementLocation _elementLocation; + + /// + /// Logging service for the tests + /// + private ILoggingService _loggingService; + + /// + /// Mock request callback that provides the build results. + /// + private MockIRequestBuilderCallback _mockRequestCallback; + + /// + /// Set up and initialize before each test is run + /// + [TestInitialize] + public void SetUp() + { + LoggingServiceFactory loggingFactory = new LoggingServiceFactory(LoggerMode.Synchronous, 1); + + _loggingService = loggingFactory.CreateInstance(BuildComponentType.LoggingService) as LoggingService; + + _customLogger = new MyCustomLogger(); + _mockHost = new MockHost(); + _mockHost.LoggingService = _loggingService; + + _loggingService.RegisterLogger(_customLogger); + _elementLocation = ElementLocation.Create("MockFile", 5, 5); + + BuildRequest buildRequest = new BuildRequest(1 /* submissionId */, 1, 1, new List(), null, BuildEventContext.Invalid, null); + BuildRequestConfiguration configuration = new BuildRequestConfiguration(1, new BuildRequestData("Nothing", new Dictionary(), "4.0", new string[0], null), "2.0"); + + configuration.Project = new ProjectInstance(ProjectRootElement.Create()); + + BuildRequestEntry entry = new BuildRequestEntry(buildRequest, configuration); + + BuildResult buildResult = new BuildResult(buildRequest, false); + buildResult.AddResultsForTarget("Build", new TargetResult(new TaskItem[] { new TaskItem("IamSuper", configuration.ProjectFullPath) }, TestUtilities.GetSkippedResult())); + _mockRequestCallback = new MockIRequestBuilderCallback(new BuildResult[] { buildResult }); + entry.Builder = (IRequestBuilder)_mockRequestCallback; + + _taskHost = new TaskHost(_mockHost, entry, _elementLocation, null /*Dont care about the callback either unless doing a build*/); + _taskHost.LoggingContext = new TaskLoggingContext(_loggingService, BuildEventContext.Invalid); + } + + /// + /// Clean up after each tests is run + /// + [TestCleanup] + public void TearDown() + { + _customLogger = null; + _mockHost = null; + _elementLocation = null; + _taskHost = null; + } + + /// + /// Verify when pulling target outputs out that we do not get the lives ones which are in the cache. + /// This is to prevent changes to the target outputs from being reflected in the cache if the changes are made in the task which calls the msbuild callback. + /// + [TestMethod] + public void TestLiveTargetOutputs() + { + IDictionary targetOutputs = new Hashtable(); + IDictionary projectProperties = new Hashtable(); + + _taskHost.BuildProjectFile("ProjectFile", new string[] { "Build" }, projectProperties, targetOutputs); + + Assert.IsNotNull(((ITaskItem[])targetOutputs["Build"])[0]); + + TaskItem targetOutputItem = ((ITaskItem[])targetOutputs["Build"])[0] as TaskItem; + TaskItem mockItemInCache = _mockRequestCallback.BuildResultsToReturn[0].ResultsByTarget["Build"].Items[0] as TaskItem; + + // Assert the contents are the same + Assert.IsTrue(targetOutputItem.Equals(mockItemInCache)); + + // Assert they are different instances. + Assert.IsTrue(!object.ReferenceEquals(targetOutputItem, mockItemInCache)); + } + + /// + /// Makes sure that if a task tries to log a custom error event that subclasses our own + /// BuildErrorEventArgs, that the subclass makes it all the way to the logger. In other + /// words, the engine should not try to read data out of the event args and construct + /// its own. + /// + [TestMethod] + public void CustomBuildErrorEventIsPreserved() + { + // Create a custom build event args that derives from MSBuild's BuildErrorEventArgs. + // Set a custom field on this event (FXCopRule). + MyCustomBuildErrorEventArgs fxcopError = new MyCustomBuildErrorEventArgs("Your code failed."); + fxcopError.FXCopRule = "CodeViolation"; + + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogErrorEvent(fxcopError); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastError is MyCustomBuildErrorEventArgs, "Expected Custom Error Event"); + + // Make sure the special fields in the custom event match what we originally logged. + fxcopError = _customLogger.LastError as MyCustomBuildErrorEventArgs; + Assert.AreEqual("Your code failed.", fxcopError.Message); + Assert.AreEqual("CodeViolation", fxcopError.FXCopRule); + } + + /// + /// Makes sure that if a task tries to log a custom warning event that subclasses our own + /// BuildWarningEventArgs, that the subclass makes it all the way to the logger. In other + /// words, the engine should not try to read data out of the event args and construct + /// its own. + /// + [TestMethod] + public void CustomBuildWarningEventIsPreserved() + { + // Create a custom build event args that derives from MSBuild's BuildWarningEventArgs. + // Set a custom field on this event (FXCopRule). + MyCustomBuildWarningEventArgs fxcopWarning = new MyCustomBuildWarningEventArgs("Your code failed."); + fxcopWarning.FXCopRule = "CodeViolation"; + + _taskHost.LogWarningEvent(fxcopWarning); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastWarning is MyCustomBuildWarningEventArgs, "Expected Custom Warning Event"); + + // Make sure the special fields in the custom event match what we originally logged. + fxcopWarning = _customLogger.LastWarning as MyCustomBuildWarningEventArgs; + Assert.AreEqual("Your code failed.", fxcopWarning.Message); + Assert.AreEqual("CodeViolation", fxcopWarning.FXCopRule); + } + + /// + /// Makes sure that if a task tries to log a custom message event that subclasses our own + /// BuildMessageEventArgs, that the subclass makes it all the way to the logger. In other + /// words, the engine should not try to read data out of the event args and construct + /// its own. + /// + [TestMethod] + public void CustomBuildMessageEventIsPreserved() + { + // Create a custom build event args that derives from MSBuild's BuildMessageEventArgs. + // Set a custom field on this event (FXCopRule). + MyCustomMessageEvent customMessage = new MyCustomMessageEvent("I am a message"); + customMessage.CustomMessage = "CodeViolation"; + + _taskHost.LogMessageEvent(customMessage); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastMessage is MyCustomMessageEvent, "Expected Custom message Event"); + + customMessage = _customLogger.LastMessage as MyCustomMessageEvent; + Assert.AreEqual("I am a message", customMessage.Message); + Assert.AreEqual("CodeViolation", customMessage.CustomMessage); + } + + /// + /// Test that error events are correctly logged and take into account continue on error + /// + [TestMethod] + public void TestLogErrorEventWithContinueOnError() + { + _taskHost.ContinueOnError = false; + + _taskHost.LogErrorEvent(new BuildErrorEventArgs("SubCategory", "code", null, 0, 1, 2, 3, "message", "Help", "Sender")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastError is BuildErrorEventArgs, "Expected Error Event"); + Assert.IsTrue(_customLogger.LastError.LineNumber == 0, "Expected line number to be 0"); + + _taskHost.ContinueOnError = true; + _taskHost.ConvertErrorsToWarnings = true; + + Assert.IsNull(_customLogger.LastWarning, "Expected no Warning Event at this point"); + + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogErrorEvent(new BuildErrorEventArgs("SubCategory", "code", null, 0, 1, 2, 3, "message", "Help", "Sender")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastWarning is BuildWarningEventArgs, "Expected Warning Event"); + Assert.IsTrue(_customLogger.LastWarning.LineNumber == 0, "Expected line number to be 0"); + + _taskHost.ContinueOnError = true; + _taskHost.ConvertErrorsToWarnings = false; + + Assert.IsTrue(_customLogger.NumberOfWarning == 1, "Expected one Warning Event at this point"); + Assert.IsTrue(_customLogger.NumberOfError == 1, "Expected one Warning Event at this point"); + + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogErrorEvent(new BuildErrorEventArgs("SubCategory", "code", null, 0, 1, 2, 3, "message", "Help", "Sender")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastError is BuildErrorEventArgs, "Expected Error Event"); + Assert.IsTrue(_customLogger.LastWarning.LineNumber == 0, "Expected line number to be 0"); + } + + /// + /// Test that a null error event will cause an exception + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestLogErrorEventNull() + { + _taskHost.LogErrorEvent(null); + } + + /// + /// Test that a null warning event will cause an exception + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestLogWarningEventNull() + { + _taskHost.LogWarningEvent(null); + } + + /// + /// Test that a null message event will cause an exception + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestLogMessageEventNull() + { + _taskHost.LogMessageEvent(null); + } + + /// + /// Test that a null custom event will cause an exception + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestLogCustomEventNull() + { + _taskHost.LogCustomEvent(null); + } + + /// + /// Test that errors are logged properly + /// + [TestMethod] + public void TestLogErrorEvent() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogErrorEvent(new BuildErrorEventArgs("SubCategory", "code", null, 0, 1, 2, 3, "message", "Help", "Sender")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastError is BuildErrorEventArgs, "Expected Error Event"); + Assert.IsTrue(_customLogger.LastError.LineNumber == 0, "Expected line number to be 0"); + } + + /// + /// Test that warnings are logged properly + /// + [TestMethod] + public void TestLogWarningEvent() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogWarningEvent(new BuildWarningEventArgs("SubCategory", "code", null, 0, 1, 2, 3, "message", "Help", "Sender")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastWarning is BuildWarningEventArgs, "Expected Warning Event"); + Assert.IsTrue(_customLogger.LastWarning.LineNumber == 0, "Expected line number to be 0"); + } + + /// + /// Test that messages are logged properly + /// + [TestMethod] + public void TestLogMessageEvent() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogMessageEvent(new BuildMessageEventArgs("message", "HelpKeyword", "senderName", MessageImportance.High)); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastMessage is BuildMessageEventArgs, "Expected Message Event"); + Assert.IsTrue(_customLogger.LastMessage.Importance == MessageImportance.High, "Expected Message importance to be high"); + } + + /// + /// Test that custom events are logged properly + /// + [TestMethod] + public void TestLogCustomEvent() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogCustomEvent(new MyCustomBuildEventArgs("testCustomBuildEvent")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastCustom is CustomBuildEventArgs, "Expected custom build Event"); + Assert.AreEqual("testCustomBuildEvent", _customLogger.LastCustom.Message); + } + + #region NotSerializableEvents + + /// + /// Test that errors are logged properly + /// + [TestMethod] + public void TestLogErrorEventNotSerializableSP() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogErrorEvent(new MyCustomBuildErrorEventArgsNotSerializable("SubCategory")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastError is BuildErrorEventArgs, "Expected Error Event"); + Assert.IsTrue(_customLogger.LastError.Message.Contains("SubCategory"), "Expected line number to be 0"); + } + + /// + /// Test that warnings are logged properly + /// + [TestMethod] + public void TestLogWarningEventNotSerializableSP() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogWarningEvent(new MyCustomBuildWarningEventArgsNotSerializable("SubCategory")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastWarning is MyCustomBuildWarningEventArgsNotSerializable, "Expected Warning Event"); + Assert.IsTrue(_customLogger.LastWarning.Message.Contains("SubCategory"), "Expected line number to be 0"); + } + + /// + /// Test that messages are logged properly + /// + [TestMethod] + public void TestLogMessageEventNotSerializableSP() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogMessageEvent(new MyCustomMessageEventNotSerializable("message")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastMessage is MyCustomMessageEventNotSerializable, "Expected Message Event"); + Assert.IsTrue(_customLogger.LastMessage.Message.Contains("message"), "Expected Message importance to be high"); + } + + /// + /// Test that custom events are logged properly + /// + [TestMethod] + public void TestLogCustomEventNotSerializableSP() + { + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogCustomEvent(new MyCustomBuildEventArgsNotSerializable("testCustomBuildEvent")); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastCustom is MyCustomBuildEventArgsNotSerializable, "Expected custom build Event"); + Assert.AreEqual(_customLogger.LastCustom.Message, "testCustomBuildEvent"); + } + + /// + /// Test that errors are logged properly + /// + [TestMethod] + public void TestLogErrorEventNotSerializableMP() + { + MyCustomBuildErrorEventArgsNotSerializable e = new MyCustomBuildErrorEventArgsNotSerializable("SubCategory"); + + _mockHost.BuildParameters.MaxNodeCount = 4; + Assert.IsTrue(_taskHost.IsRunningMultipleNodes); + + // Log the custom event args. (Pretend that the task actually did this.) + _taskHost.LogErrorEvent(e); + + Assert.IsNull(_customLogger.LastError, "Expected no error Event"); + Assert.IsTrue(_customLogger.LastWarning is BuildWarningEventArgs, "Expected Warning Event"); + + string message = ResourceUtilities.FormatResourceString("ExpectedEventToBeSerializable", e.GetType().Name); + Assert.IsTrue(_customLogger.LastWarning.Message.Contains(message), "Expected line to contain NotSerializable message but it did not"); + } + + /// + /// Test that warnings are logged properly + /// + [TestMethod] + public void TestLogWarningEventNotSerializableMP() + { + MyCustomBuildWarningEventArgsNotSerializable e = new MyCustomBuildWarningEventArgsNotSerializable("SubCategory"); + + _mockHost.BuildParameters.MaxNodeCount = 4; + _taskHost.LogWarningEvent(e); + Assert.IsTrue(_taskHost.IsRunningMultipleNodes); + + Assert.IsTrue(_customLogger.LastWarning is BuildWarningEventArgs, "Expected Warning Event"); + Assert.IsTrue(_customLogger.NumberOfWarning == 1, "Expected there to be only one warning"); + + string message = ResourceUtilities.FormatResourceString("ExpectedEventToBeSerializable", e.GetType().Name); + Assert.IsTrue(_customLogger.LastWarning.Message.Contains(message), "Expected line to contain NotSerializable message but it did not"); + } + + /// + /// Test that messages are logged properly + /// + [TestMethod] + public void TestLogMessageEventNotSerializableMP() + { + MyCustomMessageEventNotSerializable e = new MyCustomMessageEventNotSerializable("Message"); + + _mockHost.BuildParameters.MaxNodeCount = 4; + _taskHost.LogMessageEvent(e); + Assert.IsTrue(_taskHost.IsRunningMultipleNodes); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastWarning is BuildWarningEventArgs, "Expected Warning Event"); + Assert.IsTrue(_customLogger.NumberOfWarning == 1, "Expected there to be only one warning"); + + string message = ResourceUtilities.FormatResourceString("ExpectedEventToBeSerializable", e.GetType().Name); + Assert.IsTrue(_customLogger.LastWarning.Message.Contains(message), "Expected line to contain NotSerializable message but it did not"); + } + + /// + /// Test that custom events are logged properly + /// + [TestMethod] + public void TestLogCustomEventNotSerializableMP() + { + MyCustomBuildEventArgsNotSerializable e = new MyCustomBuildEventArgsNotSerializable("testCustomBuildEvent"); + + _mockHost.BuildParameters.MaxNodeCount = 4; + _taskHost.LogCustomEvent(e); + Assert.IsTrue(_taskHost.IsRunningMultipleNodes); + Assert.IsNull(_customLogger.LastCustom as MyCustomBuildEventArgsNotSerializable, "Expected no custom Event"); + + // Make sure our custom logger received the actual custom event and not some fake. + Assert.IsTrue(_customLogger.LastWarning is BuildWarningEventArgs, "Expected Warning Event"); + Assert.IsTrue(_customLogger.NumberOfWarning == 1, "Expected there to be only one warning"); + string message = ResourceUtilities.FormatResourceString("ExpectedEventToBeSerializable", e.GetType().Name); + Assert.IsTrue(_customLogger.LastWarning.Message.Contains(message), "Expected line to contain NotSerializable message but it did not"); + } + #endregion + + /// + /// Verify IsRunningMultipleNodes + /// + [TestMethod] + public void IsRunningMultipleNodes1Node() + { + _mockHost.BuildParameters.MaxNodeCount = 1; + Assert.IsFalse(_taskHost.IsRunningMultipleNodes, "Expect IsRunningMultipleNodes to be false with 1 node"); + } + + /// + /// Verify IsRunningMultipleNodes + /// + [TestMethod] + public void IsRunningMultipleNodes4Nodes() + { + _mockHost.BuildParameters.MaxNodeCount = 4; + Assert.IsTrue(_taskHost.IsRunningMultipleNodes, "Expect IsRunningMultipleNodes to be true with 4 nodes"); + } + + /// + /// Task logging after it's done should not crash us. + /// + [TestMethod] + public void LogCustomAfterTaskIsDone() + { + string projectFileContents = @" + + + + + + + + { + Thread.Sleep(100); + Log.LogExternalProjectStarted(""a"", ""b"", ""c"", ""d""); // this logs a custom event + }); + + ]]> + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("[1]"); + mockLogger.AssertLogContains("[3]"); // [2] may or may not appear. + } + + /// + /// Task logging after it's done should not crash us. + /// + [TestMethod] + public void LogCommentAfterTaskIsDone() + { + string projectFileContents = @" + + + + + + + + { + Thread.Sleep(100); + Log.LogMessage(""[2]""); + }); + + ]]> + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("[1]"); + mockLogger.AssertLogContains("[3]"); // [2] may or may not appear. + } + + /// + /// Task logging after it's done should not crash us. + /// + [TestMethod] + public void LogWarningAfterTaskIsDone() + { + string projectFileContents = @" + + + + + + + + { + Thread.Sleep(100); + Log.LogWarning(""[2]""); + }); + + ]]> + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("[1]"); + mockLogger.AssertLogContains("[3]"); // [2] may or may not appear. + } + + /// + /// Task logging after it's done should not crash us. + /// + [TestMethod] + public void LogErrorAfterTaskIsDone() + { + string projectFileContents = @" + + + + + + + + { + Thread.Sleep(100); + Log.LogError(""[2]""); + }); + + ]]> + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("[1]"); + mockLogger.AssertLogContains("[3]"); // [2] may or may not appear. + } + + #region Helper Classes + + /// + /// Create a custom message event to make sure it can get sent correctly + /// + [Serializable] + internal class MyCustomMessageEvent : BuildMessageEventArgs + { + /// + /// Some custom data for the custom event. + /// + private string _customMessage; + + /// + /// Constructor + /// + internal MyCustomMessageEvent + ( + string message + ) + : base(message, null, null, MessageImportance.High) + { + } + + /// + /// Some data which can be set on the custom message event to make sure it makes it to the logger. + /// + internal string CustomMessage + { + get + { + return _customMessage; + } + + set + { + _customMessage = value; + } + } + } + + /// + /// Create a custom build event to test the logging of custom build events against the task host + /// + [Serializable] + internal class MyCustomBuildEventArgs : CustomBuildEventArgs + { + /// + /// Constructor + /// + public MyCustomBuildEventArgs() : base() + { + } + + /// + /// Constructor which adds a message + /// + public MyCustomBuildEventArgs(string message) : base(message, "HelpKeyword", "SenderName") + { + } + } + + /// + /// Class which implements a simple custom build error + /// + [Serializable] + internal class MyCustomBuildErrorEventArgs : BuildErrorEventArgs + { + /// + /// Some custom data for the custom event. + /// + private string _fxcopRule; + + /// + /// Constructor + /// + internal MyCustomBuildErrorEventArgs + ( + string message + ) + : base(null, null, null, 0, 0, 0, 0, message, null, null) + { + } + + /// + /// Some data which can be set on the custom error event to make sure it makes it to the logger. + /// + internal string FXCopRule + { + get + { + return _fxcopRule; + } + + set + { + _fxcopRule = value; + } + } + } + + /// + /// Class which implements a simple custom build warning + /// + [Serializable] + internal class MyCustomBuildWarningEventArgs : BuildWarningEventArgs + { + /// + /// Custom data for the custom event + /// + private string _fxcopRule; + + /// + /// Constructor + /// + internal MyCustomBuildWarningEventArgs + ( + string message + ) + : base(null, null, null, 0, 0, 0, 0, message, null, null) + { + } + + /// + /// Getter for the custom data in the custom event. + /// + internal string FXCopRule + { + get + { + return _fxcopRule; + } + + set + { + _fxcopRule = value; + } + } + } + + /// + /// Create a custom message event to make sure it can get sent correctly + /// + internal class MyCustomMessageEventNotSerializable : BuildMessageEventArgs + { + /// + /// Some custom data for the custom event. + /// + private string _customMessage; + + /// + /// Constructor + /// + internal MyCustomMessageEventNotSerializable + ( + string message + ) + : base(message, null, null, MessageImportance.High) + { + } + + /// + /// Some data which can be set on the custom message event to make sure it makes it to the logger. + /// + internal string CustomMessage + { + get + { + return _customMessage; + } + + set + { + _customMessage = value; + } + } + } + + /// + /// Custom build event which is not marked serializable. This is used to make sure we warn if we try and log a not serializable type in multiproc. + /// + internal class MyCustomBuildEventArgsNotSerializable : CustomBuildEventArgs + { + /// + /// Default constructor + /// + public MyCustomBuildEventArgsNotSerializable() : base() + { + } + + /// + /// Constructor which takes a message + /// + public MyCustomBuildEventArgsNotSerializable(string message) : base(message, "HelpKeyword", "SenderName") + { + } + } + + /// + /// Class which implements a simple custom build error which is not serializable + /// + internal class MyCustomBuildErrorEventArgsNotSerializable : BuildErrorEventArgs + { + /// + /// Custom data for the custom event + /// + private string _fxcopRule; + + /// + /// Constructor + /// + internal MyCustomBuildErrorEventArgsNotSerializable + ( + string message + ) + : base(null, null, null, 0, 0, 0, 0, message, null, null) + { + } + + /// + /// Getter and setter for the custom data + /// + internal string FXCopRule + { + get + { + return _fxcopRule; + } + + set + { + _fxcopRule = value; + } + } + } + + /// + /// Class which implements a simple custom build warning which is not serializable + /// + internal class MyCustomBuildWarningEventArgsNotSerializable : BuildWarningEventArgs + { + /// + /// Custom data for the custom event + /// + private string _fxcopRule; + + /// + /// Constructor + /// + internal MyCustomBuildWarningEventArgsNotSerializable + ( + string message + ) + : base(null, null, null, 0, 0, 0, 0, message, null, null) + { + } + + /// + /// Getter and setter for the custom data + /// + internal string FXCopRule + { + get + { + return _fxcopRule; + } + + set + { + _fxcopRule = value; + } + } + } + + /// + /// Custom logger which will be used for testing + /// + internal class MyCustomLogger : ILogger + { + /// + /// Last error event the logger encountered + /// + private BuildErrorEventArgs _lastError = null; + + /// + /// Last warning event the logger encountered + /// + private BuildWarningEventArgs _lastWarning = null; + + /// + /// Last message event the logger encountered + /// + private BuildMessageEventArgs _lastMessage = null; + + /// + /// Last custom build event the logger encountered + /// + private CustomBuildEventArgs _lastCustom = null; + + /// + /// Number of errors + /// + private int _numberOfError = 0; + + /// + /// Number of warnings + /// + private int _numberOfWarning = 0; + + /// + /// Number of messages + /// + private int _numberOfMessage = 0; + + /// + /// Number of custom build events + /// + private int _numberOfCustom = 0; + + /// + /// Last error logged + /// + public BuildErrorEventArgs LastError + { + get { return _lastError; } + set { _lastError = value; } + } + + /// + /// Last warning logged + /// + public BuildWarningEventArgs LastWarning + { + get { return _lastWarning; } + set { _lastWarning = value; } + } + + /// + /// Last message logged + /// + public BuildMessageEventArgs LastMessage + { + get { return _lastMessage; } + set { _lastMessage = value; } + } + + /// + /// Last custom event logged + /// + public CustomBuildEventArgs LastCustom + { + get { return _lastCustom; } + set { _lastCustom = value; } + } + + /// + /// Number of errors logged + /// + public int NumberOfError + { + get { return _numberOfError; } + set { _numberOfError = value; } + } + + /// + /// Number of warnings logged + /// + public int NumberOfWarning + { + get { return _numberOfWarning; } + set { _numberOfWarning = value; } + } + + /// + /// Number of message logged + /// + public int NumberOfMessage + { + get { return _numberOfMessage; } + set { _numberOfMessage = value; } + } + + /// + /// Number of custom events logged + /// + public int NumberOfCustom + { + get { return _numberOfCustom; } + set { _numberOfCustom = value; } + } + + /// + /// Verbosity of the log; + /// + public LoggerVerbosity Verbosity + { + get + { + return LoggerVerbosity.Normal; + } + + set + { + } + } + + /// + /// Parameters for the logger + /// + public string Parameters + { + get + { + return String.Empty; + } + + set + { + } + } + + /// + /// Initialize the logger against the event source + /// + public void Initialize(IEventSource eventSource) + { + eventSource.ErrorRaised += new BuildErrorEventHandler(MyCustomErrorHandler); + eventSource.WarningRaised += new BuildWarningEventHandler(MyCustomWarningHandler); + eventSource.MessageRaised += new BuildMessageEventHandler(MyCustomMessageHandler); + eventSource.CustomEventRaised += new CustomBuildEventHandler(MyCustomBuildHandler); + eventSource.AnyEventRaised += new AnyEventHandler(EventSource_AnyEventRaised); + } + + /// + /// Do any cleanup and shutdown once the logger is done. + /// + public void Shutdown() + { + } + + /// + /// Log if we have recieved any event. + /// + internal void EventSource_AnyEventRaised(object sender, BuildEventArgs e) + { + if (e.Message != null) + { + Console.Out.WriteLine("AnyEvent:" + e.Message.ToString()); + } + } + + /// + /// Log and record the number of errors. + /// + internal void MyCustomErrorHandler(object s, BuildErrorEventArgs e) + { + _numberOfError++; + _lastError = e; + if (e.Message != null) + { + Console.Out.WriteLine("CustomError:" + e.Message.ToString()); + } + } + + /// + /// Log and record the number of warnings. + /// + internal void MyCustomWarningHandler(object s, BuildWarningEventArgs e) + { + _numberOfWarning++; + _lastWarning = e; + if (e.Message != null) + { + Console.Out.WriteLine("CustomWarning:" + e.Message.ToString()); + } + } + + /// + /// Log and record the number of messages. + /// + internal void MyCustomMessageHandler(object s, BuildMessageEventArgs e) + { + _numberOfMessage++; + _lastMessage = e; + if (e.Message != null) + { + Console.Out.WriteLine("CustomMessage:" + e.Message.ToString()); + } + } + + /// + /// Log and record the number of custom build events. + /// + internal void MyCustomBuildHandler(object s, CustomBuildEventArgs e) + { + _numberOfCustom++; + _lastCustom = e; + if (e.Message != null) + { + Console.Out.WriteLine("CustomEvent:" + e.Message.ToString()); + } + } + } + + /// + /// Mock this class so that we can determine if build results are being cloned or if the live copies are being returned to the callers of the msbuild callback. + /// + internal class MockIRequestBuilderCallback : IRequestBuilderCallback, IRequestBuilder + { + /// + /// BuildResults to return from the BuildProjects method. + /// + private BuildResult[] _buildResultsToReturn; + + /// + /// Constructor which takes an array of build results to return from the BuildProjects method when it is called. + /// + internal MockIRequestBuilderCallback(BuildResult[] buildResultsToReturn) + { + _buildResultsToReturn = buildResultsToReturn; + OnNewBuildRequests += new NewBuildRequestsDelegate(MockIRequestBuilderCallback_OnNewBuildRequests); + OnBuildRequestCompleted += new BuildRequestCompletedDelegate(MockIRequestBuilderCallback_OnBuildRequestCompleted); + OnBuildRequestBlocked += new BuildRequestBlockedDelegate(MockIRequestBuilderCallback_OnBuildRequestBlocked); + } + +#pragma warning disable 0067 // not used + /// + /// Not Implemented + /// + public event NewBuildRequestsDelegate OnNewBuildRequests; + + /// + /// Not Implemented + /// + public event BuildRequestCompletedDelegate OnBuildRequestCompleted; + + /// + /// Not Implemented + /// + public event BuildRequestBlockedDelegate OnBuildRequestBlocked; +#pragma warning restore + + /// + /// BuildResults to return from the BuildProjects method. + /// + public BuildResult[] BuildResultsToReturn + { + get { return _buildResultsToReturn; } + set { _buildResultsToReturn = value; } + } + + /// + /// Mock of the BuildProjects method on the callback. + /// + public Task BuildProjects(string[] projectFiles, PropertyDictionary[] properties, string[] toolsVersions, string[] targets, bool waitForResults) + { + return Task.FromResult(_buildResultsToReturn); + } + + /// + /// Mock of Yield + /// + public void Yield() + { + } + + /// + /// Mock of Reacquire + /// + public void Reacquire() + { + } + + /// + /// Mock + /// + public void EnterMSBuildCallbackState() + { + } + + /// + /// Mock + /// + public void ExitMSBuildCallbackState() + { + } + + /// + /// Mock of the Block on target in progress. + /// + public Task BlockOnTargetInProgress(int blockingRequestId, string blockingTarget) + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + public void BuildRequest(NodeLoggingContext nodeLoggingContext, BuildRequestEntry entry) + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + public void ContinueRequest() + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + public void CancelRequest() + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + public void BeginCancel() + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + public void WaitForCancelCompletion() + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + private void MockIRequestBuilderCallback_OnBuildRequestBlocked(BuildRequestEntry sourceEntry, int blockingGlobalRequestId, string blockingTarget) + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + private void MockIRequestBuilderCallback_OnBuildRequestCompleted(BuildRequestEntry completedEntry) + { + throw new NotImplementedException(); + } + + /// + /// Not Implemented + /// + private void MockIRequestBuilderCallback_OnNewBuildRequests(BuildRequestEntry sourceEntry, FullyQualifiedBuildRequest[] requests) + { + throw new NotImplementedException(); + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskItemComparer.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskItemComparer.cs new file mode 100644 index 00000000000..aa7aeb3f9d6 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskItemComparer.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of IComparer on ITaskItems used for testing. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Implementation of IComparer on ITaskItems used for testing. + /// + public class TaskItemComparer : IComparer + { + /// + /// Constructor. + /// + private TaskItemComparer() + { + } + + /// + /// Retrieves a new instance of the class. + /// + public static IComparer Instance + { + get { return new TaskItemComparer(); } + } + + #region IComparer Members + + /// + /// Compares two task items. + /// Built-in derivable metadata is ignored as it might not be copied. + /// + /// 0 if they are equal, -1 otherwise. + public int Compare(ITaskItem x, ITaskItem y) + { + if (x.ItemSpec != y.ItemSpec) + { + return -1; + } + + if (x.CloneCustomMetadata().Count != x.CloneCustomMetadata().Count) + { + return -1; + } + + foreach (string metadataName in x.MetadataNames) + { + if (!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName) || + FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName)) + { + if (x.GetMetadata(metadataName) != y.GetMetadata(metadataName)) + { + return -1; + } + } + } + + foreach (string metadataName in y.MetadataNames) + { + if (!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(metadataName) || + FileUtilities.ItemSpecModifiers.IsDerivableItemSpecModifier(metadataName)) + { + if (x.GetMetadata(metadataName) != y.GetMetadata(metadataName)) + { + return -1; + } + } + } + + return 0; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TaskRegistry_Tests.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskRegistry_Tests.cs new file mode 100644 index 00000000000..46c9e1ce984 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TaskRegistry_Tests.cs @@ -0,0 +1,2271 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Unit tests for the task execution host object. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using System.Reflection; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.Build.UnitTests; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Test the task registry + /// + [TestClass] + public class TaskRegistry_Tests + { + /// + /// Name of the test task built into the test + /// assembly at testTaskLocation. + /// + private const string TestTaskName = "TestTask"; + + /// + /// Location of the generated test task DLL. + /// + private static string s_testTaskLocation; + + /// + /// Logging service to use in for the task registry + /// + private static ILoggingService s_loggingService; + + /// + /// Mock logger which is attached to the logging service. + /// + private static MockLogger s_logger; + + /// + /// Target logging context to use when logging. + /// + private static TargetLoggingContext s_targetLoggingContext; + + /// + /// Build event context to use when logging + /// + private static BuildEventContext s_loggerContext = new BuildEventContext(2, 2, 2, 2); + + /// + /// Element location to use when logging + /// + private static ElementLocation s_elementLocation = ElementLocation.Create("c:\\project.proj", 0, 0); + + /// + /// Set up this test class -- generate the test task assembly used by + /// several of the tests. + /// + [ClassInitialize] + public static void SetupClass(TestContext context) + { + s_testTaskLocation = GetTestTaskAssemblyLocation(); + } + + /// + /// Clean this test class up -- make sure the test task assembly we + /// generated has been deleted. + /// + [ClassCleanup] + public static void CleanupClass() + { + if (File.Exists(s_testTaskLocation)) + { + FileUtilities.DeleteNoThrow(s_testTaskLocation); + } + } + + /// + /// Setup some logging services so we can see what is goign on. + /// + [TestInitialize] + public void SetUp() + { + s_loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1) as ILoggingService; + s_logger = new MockLogger(); + s_targetLoggingContext = new TargetLoggingContext(s_loggingService, s_loggerContext); + + s_loggingService.RegisterLogger(s_logger); + } + + #region UsingTaskTests + /// + /// Try and register a simple task + /// Expect: + /// One task to be registered and that it has the correct assembly information registered. + /// + [TestMethod] + public void RegisterTaskSimple() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("CustomTask", null, "CustomTask, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(1, registeredTaskCount, "Expected one registered tasks in TaskRegistry.AllTaskDeclarations!"); + + foreach (ProjectUsingTaskElement taskElement in elementList) + { + List registrationRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(taskElement.TaskName, null)]; + Assert.IsNotNull(registrationRecords, "Task registrationrecord not found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registrationRecords.Count, "Expected only one record registered under this TaskName!"); + + AssemblyLoadInfo taskAssemblyLoadInfo = registrationRecords[0].TaskFactoryAssemblyLoadInfo; + string assemblyName = String.IsNullOrEmpty(taskElement.AssemblyName) ? null : taskElement.AssemblyName; + string assemblyFile = String.IsNullOrEmpty(taskElement.AssemblyFile) ? null : taskElement.AssemblyFile; + Assert.AreEqual(taskAssemblyLoadInfo, AssemblyLoadInfo.Create(assemblyName, assemblyFile), "Task record was not properly registered by TaskRegistry.RegisterTask!"); + } + } + + /// + /// Register many tasks with different names + /// Expect: + /// Three tasks to be regisered + /// Expect only one task to be regisered under each task name + /// Expect the correct assembly information to be registered + /// + [TestMethod] + public void RegisterMultipleTasksWithDifferentNames() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("CustomTask", null, "CustomTask, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + element = project.AddUsingTask("YetAnotherCustomTask", "bin\\Assemblies\\YetAnotherCustomTask.dll", null); + elementList.Add(element); + + element = project.AddUsingTask("AnotherCustomTask", null, "AnotherCustomTask, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(3, registeredTaskCount, "Expected three registered tasks in TaskRegistry.AllTaskDeclarations!"); + + foreach (ProjectUsingTaskElement taskElement in elementList) + { + List registrationRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(taskElement.TaskName, null)]; + Assert.IsNotNull(registrationRecords, "Task registrationrecord not found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registrationRecords.Count, "Expected only one record registered under this TaskName!"); + + AssemblyLoadInfo taskAssemblyLoadInfo = registrationRecords[0].TaskFactoryAssemblyLoadInfo; + + string assemblyName = String.IsNullOrEmpty(taskElement.AssemblyName) ? null : taskElement.AssemblyName; + string assemblyFile = String.IsNullOrEmpty(taskElement.AssemblyFile) ? null : taskElement.AssemblyFile; + + Assert.AreEqual(taskAssemblyLoadInfo, AssemblyLoadInfo.Create(assemblyName, assemblyFile == null ? null : Path.GetFullPath(assemblyFile)), "Task record was not properly registered by TaskRegistry.RegisterTask!"); + } + } + + /// + /// Register the same task multiple times with the same name + /// Expect: + /// Three three tasks to be registered + /// Expect two of the tasks to be under the same task name bucket + /// Expect the correct assembly information to be registered for each of the tasks + /// + [TestMethod] + public void RegisterMultipleTasksSomeWithSameName() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("CustomTask", null, "CustomTask, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + element = project.AddUsingTask("YetAnotherCustomTask", null, "YetAnotherCustomTask, Version=9.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + element = project.AddUsingTask("CustomTask", null, "CustomTask, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(3, registeredTaskCount, "Expected three registered tasks in TaskRegistry.AllTaskDeclarations!"); + + // First assert that there are two unique buckets + Assert.AreEqual(2, registry.TaskRegistrations.Count, "Expected only two buckets since two of three tasks have the same name!"); + + // Now let's look at the bucket with only one task + List singletonBucket = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(elementList[1].TaskName, null)]; + Assert.IsNotNull(singletonBucket, "Record not found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, singletonBucket.Count, "Expected only Record registered under this TaskName!"); + AssemblyLoadInfo singletonAssemblyLoadInfo = singletonBucket[0].TaskFactoryAssemblyLoadInfo; + string assemblyName = String.IsNullOrEmpty(elementList[1].AssemblyName) ? null : elementList[1].AssemblyName; + string assemblyFile = String.IsNullOrEmpty(elementList[1].AssemblyFile) ? null : elementList[1].AssemblyFile; + Assert.AreEqual(singletonAssemblyLoadInfo, AssemblyLoadInfo.Create(assemblyName, assemblyFile), "Task record was not properly registered by TaskRegistry.RegisterTask!"); + + // Now let's look at the bucket with two tasks + List duplicateBucket = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(elementList[0].TaskName, null)]; + Assert.IsNotNull(duplicateBucket, "Records not found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(2, duplicateBucket.Count, "Expected two Records registered under this TaskName!"); + + bool foundFirstLoadInfo = false; + bool foundSecondLoadInfo = false; + foreach (TaskRegistry.RegisteredTaskRecord record in duplicateBucket) + { + assemblyName = String.IsNullOrEmpty(elementList[0].AssemblyName) ? null : elementList[0].AssemblyName; + assemblyFile = String.IsNullOrEmpty(elementList[0].AssemblyFile) ? null : elementList[0].AssemblyFile; + if (record.TaskFactoryAssemblyLoadInfo.Equals(AssemblyLoadInfo.Create(assemblyName, assemblyFile))) + { + foundFirstLoadInfo = true; + } + + assemblyName = String.IsNullOrEmpty(elementList[2].AssemblyName) ? null : elementList[2].AssemblyName; + assemblyFile = String.IsNullOrEmpty(elementList[2].AssemblyFile) ? null : elementList[2].AssemblyFile; + if (record.TaskFactoryAssemblyLoadInfo.Equals(AssemblyLoadInfo.Create(assemblyName, assemblyFile))) + { + foundSecondLoadInfo = true; + } + } + + Assert.IsTrue(foundFirstLoadInfo, "Expected first task to be registered in this bucket!"); + Assert.IsTrue(foundSecondLoadInfo, "Expected second task to be registered in this bucket!"); + } + + /// + /// Register multiple tasks with different names in the same assembly + /// Expect: + /// Three tasks to be registered + /// + [TestMethod] + public void RegisterMultipleTasksWithDifferentNamesFromSameAssembly() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("CustomTask", null, "CustomTasks, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + element = project.AddUsingTask("YetAnotherCustomTask", "bin\\Assemblies\\YetAnotherCustomTask.dll", null); + elementList.Add(element); + + element = project.AddUsingTask("AnotherCustomTask", null, "CustomTasks, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(3, registeredTaskCount, "Expected three registered tasks in TaskRegistry.AllTaskDeclarations!"); + + foreach (ProjectUsingTaskElement taskElement in elementList) + { + List registrationRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(taskElement.TaskName, null)]; + Assert.IsNotNull(registrationRecords, "Task registrationrecord not found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registrationRecords.Count, "Expected only one record registered under this TaskName!"); + + AssemblyLoadInfo taskAssemblyLoadInfo = registrationRecords[0].TaskFactoryAssemblyLoadInfo; + string assemblyName = String.IsNullOrEmpty(taskElement.AssemblyName) ? null : taskElement.AssemblyName; + string assemblyFile = String.IsNullOrEmpty(taskElement.AssemblyFile) ? null : taskElement.AssemblyFile; + Assert.AreEqual(taskAssemblyLoadInfo, AssemblyLoadInfo.Create(assemblyName, assemblyFile == null ? null : Path.GetFullPath(assemblyFile)), "Task record was not properly registered by TaskRegistry.RegisterTask!"); + } + } + + /// + /// Register multiple tasks with the same name in the same assembly + /// Expect: + /// Three tasks to be registered + /// Two of the tasks should be in the same name bucket + /// + [TestMethod] + public void RegisterMultipleTasksWithSameNameAndSameAssembly() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("CustomTask", "Some\\Relative\\Path\\CustomTasks.dll", null); + elementList.Add(element); + + element = project.AddUsingTask("YetAnotherCustomTask", null, "YetAnotherCustomTask, Version=9.0.0.0, Culture=neutral, PublicKeyToken=null"); + elementList.Add(element); + + element = project.AddUsingTask("CustomTask", "Some\\Relative\\Path\\CustomTasks.dll", null); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // two unique buckets + Assert.AreEqual(2, registry.TaskRegistrations.Count, "Expected only two buckets since two of three tasks have the same name!"); + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(3, registeredTaskCount, "Expected three registered tasks in TaskRegistry.TaskRegistrations!"); + } + + /// + /// Validate registration of tasks with different combinations of task parameters. + /// Expected that an otherwise equivalent task will be recognized as a separate task if it has + /// different task parameters set. + /// + [TestMethod] + public void RegisterTasksWithFactoryParameters() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Task", "c:\\TaskLocation\\Tasks.dll", null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + element = project.AddUsingTask("Task", "c:\\TaskLocation\\Tasks.dll", null); + element.Runtime = "CLR4"; + element.Architecture = "x64"; + elementList.Add(element); + + element = project.AddUsingTask("Task", "c:\\TaskLocation\\Tasks.dll", null); + element.Runtime = "CLR4"; + element.Architecture = "*"; + elementList.Add(element); + + element = project.AddUsingTask("Task", "c:\\TaskLocation\\Tasks.dll", null); + element.Runtime = "CLR4"; + element.Architecture = "x64"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + Assert.AreEqual(3, registry.TaskRegistrations.Count, "Should have three buckets, since two of the tasks are the same."); + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(4, registeredTaskCount); + } + + #region Cache read tests + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheTaskDoesNotExist_ExactMatch() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("UnrelatedTask", s_testTaskLocation, null); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Not in registry, so shouldn't match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: null, + architecture: null, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // Still not in registry, so shouldn't match this time either -- and we should pull from the cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: null, + architecture: null, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: true + ); + } + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheTaskDoesNotExist_FuzzyMatch() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("UnrelatedTask", s_testTaskLocation, null); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Not in registry, so shouldn't match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: null, + architecture: null, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // Still not in registry, so shouldn't match this time either -- and we should pull from the cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: null, + architecture: null, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: true + ); + } + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheMatchingTaskDoesNotExist_FuzzyMatch() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Not in registry, so shouldn't match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: "CLR2", + architecture: "*", + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // Still not in registry, so shouldn't match this time either -- and we should pull from the cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: "CLR2", + architecture: "*", + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: true + ); + } + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheMatchingTaskDoesNotExistOnFirstCallButDoesOnSecond() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Not in registry, so shouldn't match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: "CLR2", + architecture: "*", + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // Still not in registry, so shouldn't match this time either -- and we should pull from the cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: null, + architecture: null, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false + ); + } + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheMatchingExactParameters() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // no parameters - no match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: null, + architecture: null, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // parameters that would be a successful fuzzy match - no match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.any, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // parameters that are a successful exact match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false + ); + + // parameters that do not match - should not retrieve + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr2, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // exact match #2 -- should get it from the cache this time + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: true, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true + ); + } + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters beyond just runtime and architecture. Hint: it shouldn't + /// ever work, since we don't currently have a way to create a using task with + /// parameters other than those two. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheMatchingExactParameters_AdditionalParameters() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Runtime and architecture match the using task exactly, but since there is an additional parameter, it still + // doesn't match when doing exact matching. + Dictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.x86); + taskParameters.Add("Foo", "Bar"); + + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + true /* exact match */, + taskParameters, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // However, it should still match itself -- so if we try again, we should get the "no match" + // back from the cache this time. + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + true /* exact match */, + taskParameters, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: true + ); + } + + /// + /// Test retrieving a matching task record using various parameter combinations when allowing + /// fuzzy matches. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheFuzzyMatchingParameters() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // no parameters + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: null, + architecture: null, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that are a successful exact match - should retrieve from cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that would be a successful fuzzy match - should still be retrieved from the cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.any, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that do not match -- but would match the previous fuzzy match request. Should NOT retrieve anything + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // and another fuzzy match -- should still be pulling from the cache. + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.any, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + } + + /// + /// Test retrieving a matching task record using various parameter combinations when allowing + /// fuzzy matches. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheFuzzyMatchingParameters_RecoverFromFailure() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // no parameters - should retrieve the record + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: null, + architecture: null, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that do not match at all - shouldn't retrieve anything + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr2, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // parameters that are a successful match - should retrieve from the cache this time + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + } + + /// + /// Test fuzzy matching of parameters when retrieving task records when there are + /// multiple using tasks registered for the same task, just with different parameter + /// sets. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheFuzzyMatchingParameters_MultipleUsingTasks() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "*"; + element.Architecture = "x64"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // no parameters -- gets the first one (CLR4|x86) + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: null, + architecture: null, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that are a successful exact match for CLR4|x86 -- should come from cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that would be a successful fuzzy match for either, so should get the one in the cache (CLR4|x86) + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.any, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // parameters that match *|x64 - should retrieve that + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.any, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x64 + ); + + // match CLR4|x86 again - comes from the cache + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.any, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // match *|x64 again + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr2, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.any, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x64 + ); + + // CLR2|x86 should not match either task record + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr2, + architecture: XMakeAttributes.MSBuildArchitectureValues.x86, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // match *|x64 again -- should still be a cache hit + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr2, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.any, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x64 + ); + } + + /// + /// Test fuzzy matching of parameters when retrieving task records when there are + /// multiple using tasks registered for the same task, just with different parameter + /// sets. Specific sub-test: although we generally pick the first available record if + /// there are multiple matches, if we are doing fuzzy matching, we should prefer the + /// record that's in the cache, even if it wasn't the original first record. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheFuzzyMatchingParameters_MultipleUsingTasks_PreferCache() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "*"; + element.Architecture = "x64"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // CLR4|x64 -- should be fulfilled by *|x64 + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.any, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x64 + ); + + // CLR4|* -- could be filled by either, would normally be filled by CLR4|x86 (since it was registered first), + // but since *|x64 is in the cache already, we return that one. + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.any, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.any, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x64 + ); + } + + /// + /// Test retrieving a matching task record using various parameter combinations when allowing + /// fuzzy matches. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheFuzzyMatchingParameters_ExactMatches() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // CLR4|* should match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.any, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // CLR4|x64 should not match + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: false + ); + + // try CLR4|* again -- should resolve correctly from the cache. + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.any, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // try CLR4|x64 again -- should also come from the catch (but needless to say, still not be a match) + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + exactMatchRequired: false, + runtime: XMakeAttributes.MSBuildRuntimeValues.clr4, + architecture: XMakeAttributes.MSBuildArchitectureValues.x64, + shouldBeRetrieved: false, + shouldBeRetrievedFromCache: true + ); + } + + /// + /// Validate task retrieval and exact cache retrieval when attempting to load + /// a task with parameters beyond just runtime and architecture. Hint: it shouldn't + /// ever work, since we don't currently have a way to create a using task with + /// parameters other than those two. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void RetrieveFromCacheFuzzyMatchingParameters_AdditionalParameters() + { + Assert.IsNotNull(s_testTaskLocation, "Need a test task to run this test"); + + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask(TestTaskName, s_testTaskLocation, null); + element.Runtime = "CLR4"; + element.Architecture = "x86"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Runtime and architecture match, so even though we have the extra parameter, it should still match + Dictionary taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.x86); + taskParameters.Add("Foo", "Bar"); + + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + false /* fuzzy match */, + taskParameters, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: false, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + // And if we try again, we should get it from the cache this time. + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + false /* fuzzy match */, + taskParameters, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + + taskParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + taskParameters.Add(XMakeAttributes.runtime, XMakeAttributes.MSBuildRuntimeValues.clr4); + taskParameters.Add(XMakeAttributes.architecture, XMakeAttributes.MSBuildArchitectureValues.x86); + taskParameters.Add("Baz", "Qux"); + + // Even with a different value to the additional parameter, because it's a fuzzy equals and because all + // our equivalency check looks for is runtime and architecture, it still successfully retrieves the + // existing record from the cache. + RetrieveAndValidateRegisteredTaskRecord + ( + registry, + false /* fuzzy match */, + taskParameters, + shouldBeRetrieved: true, + shouldBeRetrievedFromCache: true, + expectedRuntime: XMakeAttributes.MSBuildRuntimeValues.clr4, + expectedArchitecture: XMakeAttributes.MSBuildArchitectureValues.x86 + ); + } + + #endregion + + /// + /// Verify the using task attributes are expanded correctly + /// Expect: + /// Expanded property and item values to be correct for each of the attributes + /// + [TestMethod] + public void AllUsingTaskAttributesAreExpanded() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("$(Property1)@(ThirdItem)$(Property2)", "Some\\$(Property3)\\Path\\CustomTasks.dll", null); + element.TaskFactory = "$(Property1)@(ThirdItem)$(Property2)"; + elementList.Add(element); + + element = project.AddUsingTask("YetAnotherCustomTask", null, "$(Property4)@(ThirdItem), Version=9.0.0.0, Culture=neutral, PublicKeyToken=null"); + element.TaskFactory = ""; + elementList.Add(element); + + element = project.AddUsingTask("Custom$(Property5)Task", "Some\\Relative\\Path\\CustomTasks.dll", null); + element.TaskFactory = null; + element.Condition = "'@(ThirdItem)$(Property1)' == 'ThirdValue1Value1'"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(3, registeredTaskCount, "Expected three registered tasks in TaskRegistry.TaskRegistrations!"); + + IDictionary> registeredTasks = registry.TaskRegistrations; + + foreach (ProjectUsingTaskElement taskElement in elementList) + { + string expandedtaskName = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.TaskName, ExpanderOptions.ExpandPropertiesAndItems, taskElement.TaskNameLocation); + string expandedAssemblyName = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.AssemblyName, ExpanderOptions.ExpandPropertiesAndItems, taskElement.AssemblyNameLocation); + string expandedAssemblyFile = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.AssemblyFile, ExpanderOptions.ExpandPropertiesAndItems, taskElement.AssemblyFileLocation); + string expandedTaskFactory = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.TaskFactory, ExpanderOptions.ExpandPropertiesAndItems, taskElement.TaskFactoryLocation); + + expandedAssemblyName = String.IsNullOrEmpty(expandedAssemblyName) ? null : expandedAssemblyName; + expandedAssemblyFile = String.IsNullOrEmpty(expandedAssemblyFile) ? null : expandedAssemblyFile; + expandedTaskFactory = String.IsNullOrEmpty(expandedTaskFactory) ? "AssemblyTaskFactory" : expandedTaskFactory; + + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(expandedtaskName, null)]; + Assert.IsNotNull(registeredTaskRecords, "Task to be found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + + Assert.AreEqual(expandedTaskFactory, registeredTaskRecords[0].TaskFactoryAttributeName); + + AssemblyLoadInfo taskAssemblyLoadInfo = registeredTaskRecords[0].TaskFactoryAssemblyLoadInfo; + Assert.AreEqual(taskAssemblyLoadInfo, AssemblyLoadInfo.Create(expandedAssemblyName, expandedAssemblyFile == null ? null : Path.GetFullPath(expandedAssemblyFile)), "Task record was not properly registered by TaskRegistry.RegisterTask!"); + } + } + + /// + /// Verify tasks are registered only if the condition on the using task is true + /// Expect: + /// Expect two of the conditions to evaluate to false causing two of the tasks to not be registered + /// + [TestMethod] + public void TaskRegisteredOnlyIfConditionIsTrue() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("$(Property1)@(ThirdItem)$(Property2)", "Some\\$(Property3)\\Path\\CustomTasks.dll", null); + element.Condition = "'true' != 'false'"; + elementList.Add(element); + + element = project.AddUsingTask("YetAnotherCustomTask", null, "$(Property4)@(ThirdItem), Version=9.0.0.0, Culture=neutral, PublicKeyToken=null"); + element.Condition = "false"; + elementList.Add(element); + + element = project.AddUsingTask("Custom$(Property5)Task", "Some\\Relative\\Path\\CustomTasks.dll", null); + element.Condition = "'@(ThirdItem)$(Property1)' == 'ThirdValue1Value1'"; + elementList.Add(element); + + element = project.AddUsingTask("MyTask", "TasksAssembly.dll", null); + element.Condition = "'@(ThirdItem)$(Property1)' == 'ThirdValue1'"; + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(2, registeredTaskCount, "Expected two registered tasks in TaskRegistry.TaskRegistrations!"); + + IDictionary> registeredTasks = registry.TaskRegistrations; + + for (int i = 0; i <= 2; i += 2) + { + ProjectUsingTaskElement taskElement = elementList[i]; + string expandedtaskName = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.TaskName, ExpanderOptions.ExpandPropertiesAndItems, taskElement.TaskNameLocation); + string expandedAssemblyName = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.AssemblyName, ExpanderOptions.ExpandPropertiesAndItems, taskElement.AssemblyNameLocation); + string expandedAssemblyFile = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(taskElement.AssemblyFile, ExpanderOptions.ExpandPropertiesAndItems, taskElement.AssemblyFileLocation); + + expandedAssemblyName = String.IsNullOrEmpty(expandedAssemblyName) ? null : expandedAssemblyName; + expandedAssemblyFile = String.IsNullOrEmpty(expandedAssemblyFile) ? null : expandedAssemblyFile; + + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity(expandedtaskName, null)]; + Assert.IsNotNull(registeredTaskRecords, "Task to be found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + + AssemblyLoadInfo taskAssemblyLoadInfo = registeredTaskRecords[0].TaskFactoryAssemblyLoadInfo; + Assert.AreEqual(taskAssemblyLoadInfo, AssemblyLoadInfo.Create(expandedAssemblyName, Path.GetFullPath(expandedAssemblyFile)), "Task record was not properly registered by TaskRegistry.RegisterTask!"); + } + } + + /// + /// Verify that when there are no child elements on the using task that there are no ParameterGroupAndTaskBody + /// + [TestMethod] + public void NoChildrenElements() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Hello", "File", null); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(1, registeredTaskCount, "Expected three registered tasks in TaskRegistry.TaskRegistrations!"); + + IDictionary> registeredTasks = registry.TaskRegistrations; + + ProjectUsingTaskElement taskElement = elementList[0]; + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Hello", null)]; + Assert.IsNotNull(registeredTaskRecords, "Task to be found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + Assert.AreEqual(0, registeredTaskRecords[0].ParameterGroupAndTaskBody.UsingTaskParameters.Count); + Assert.IsNull(registeredTaskRecords[0].ParameterGroupAndTaskBody.InlineTaskXmlBody); + } + #endregion + + #region ParameterGroupTests + /// + /// Verify that when there is a parametergroup that there is a ParameterGroupAndTaskBody but that there are no parameters in it. + /// + [TestMethod] + public void EmptyParameterGroup() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperDuperFactory"; + + // Add empty parameterGroup + element.AddParameterGroup(); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(1, registeredTaskCount, "Expected three registered tasks in TaskRegistry.TaskRegistrations!"); + IDictionary> registeredTasks = registry.TaskRegistrations; + + ProjectUsingTaskElement taskElement = elementList[0]; + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)]; + Assert.IsNotNull(registeredTaskRecords, "Task to be found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + TaskRegistry.RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord = registeredTaskRecords[0].ParameterGroupAndTaskBody; + Assert.IsNotNull(inlineTaskRecord); + Assert.IsNull(inlineTaskRecord.InlineTaskXmlBody); + Assert.IsTrue(inlineTaskRecord.UsingTaskParameters.Count == 0); + } + + /// + /// Verify that when when multiple parameters are set that they show up in the parametergroup object + /// + [TestMethod] + public void MultipleGoodParameters() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperFactory"; + + // Add empty parameterGroup + UsingTaskParameterGroupElement parameterGroup = element.AddParameterGroup(); + ProjectUsingTaskParameterElement defaultParameter = parameterGroup.AddParameter("ParameterWithNoAttributes"); + + ProjectUsingTaskParameterElement filledOutAttributesParameter = parameterGroup.AddParameter("ParameterWithAllAttributesHardCoded", bool.TrueString, bool.TrueString, typeof(Int32).FullName); + + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(1, registeredTaskCount, "Expected three registered tasks in TaskRegistry.TaskRegistrations!"); + IDictionary> registeredTasks = registry.TaskRegistrations; + + ProjectUsingTaskElement taskElement = elementList[0]; + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)]; + Assert.IsNotNull(registeredTaskRecords, "Task to be found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + + TaskRegistry.RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord = registeredTaskRecords[0].ParameterGroupAndTaskBody; + Assert.IsNotNull(inlineTaskRecord); + Assert.IsNull(inlineTaskRecord.InlineTaskXmlBody); + Assert.IsTrue(inlineTaskRecord.UsingTaskParameters.Count == 2); + + TaskPropertyInfo parameterInfo = inlineTaskRecord.UsingTaskParameters[defaultParameter.Name] as TaskPropertyInfo; + Assert.IsNotNull(parameterInfo); + Assert.AreEqual(parameterInfo.Name, defaultParameter.Name); + Assert.AreEqual(parameterInfo.Output, false); + Assert.AreEqual(parameterInfo.Required, false); + Assert.AreEqual(parameterInfo.PropertyType, typeof(System.String)); + + parameterInfo = inlineTaskRecord.UsingTaskParameters[filledOutAttributesParameter.Name] as TaskPropertyInfo; + Assert.IsNotNull(parameterInfo); + Assert.AreEqual(parameterInfo.Name, filledOutAttributesParameter.Name); + Assert.AreEqual(parameterInfo.Output, true); + Assert.AreEqual(parameterInfo.Required, true); + Assert.AreEqual(parameterInfo.PropertyType, typeof(Int32)); + } + + /// + /// Verify passing a empty type parameter results in the default type of String being registered + /// + [TestMethod] + public void EmptyTypeOnParameter() + { + string output = bool.TrueString; + string required = bool.TrueString; + string type = ""; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"].PropertyType.Equals(typeof(String))); + } + + /// + /// Verify passing a null as a type parameter results in the default type of String being registered + /// + [TestMethod] + public void NullTypeOnParameter() + { + string output = bool.TrueString; + string required = bool.TrueString; + string type = null; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"].PropertyType.Equals(typeof(String))); + } + + /// + /// Verify when registering a randon type which is not allowed that we get an InvalidProjectFileException + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RandomTypeOnParameter() + { + string output = bool.TrueString; + string required = bool.TrueString; + string type = "ISomethingItem"; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify the following types work when registered as input parameters + /// ValueTypeArray + /// StringArray + /// + [TestMethod] + public void GoodValueTypeArrayInputOnInputParameter() + { + // Note output is false so these are only input parameters + string output = bool.FalseString; + string required = bool.TrueString; + + string type = typeof(int[]).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(String[]).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(ITaskItem[]).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(DateTime[]).FullName; + VerifyTypeParameter(output, required, type); + } + + /// + /// Verify when a class (other than string or ITaskItem) is attempted to be registered as an input parameter we get an invalid project file exception. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void BadArrayInputOnInputParameter() + { + // Note output is false so these are only input parameters + string output = bool.FalseString; + string required = bool.TrueString; + string type = typeof(ArrayList[]).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify that value types and (string and ITaskItem classes) can be registered as input parameters + /// + [TestMethod] + public void GoodScalarTypeArrayInputOnInputParameter() + { + // Note output is false so these are only input parameters + string output = bool.FalseString; + string required = bool.TrueString; + + string type = typeof(int).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(String).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(ITaskItem).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(DateTime).FullName; + VerifyTypeParameter(output, required, type); + } + + /// + /// Verify when a class which derives from ITask is attempted to be registered that we get an InvalidProjectFileException. + /// We only support ITaskItems and not their derrived types as input parameters. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void BadScalarInputOnInputParameterDerrivedFromITask() + { + // Note output is false so these are only input parameters + string output = bool.FalseString; + string required = bool.TrueString; + string type = type = typeof(DerrivedFromITaskItem).FullName + "," + typeof(DerrivedFromITaskItem).Assembly.FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify when a random scalar input class is attempted to be registered that we get an invalid proejct file exceptions. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void BadScalarInputOnInputParameter() + { + // Note output is false so these are only input parameters + string output = bool.FalseString; + string required = bool.TrueString; + string type = typeof(ArrayList).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify the expected outparameters are supported + /// String + /// String[] + /// ValueType + /// ValueType[] + /// ItaskItem + /// ItaskItem[] + /// Types which are assignable to ITaskItem or ITaskItem[] + /// + [TestMethod] + public void GoodOutPutParameters() + { + // Notice output is true + string output = bool.TrueString; + string required = bool.TrueString; + + string type = typeof(int).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(String).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(ITaskItem).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(DerrivedFromITaskItem).FullName + "," + typeof(DerrivedFromITaskItem).Assembly.FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(ITaskItem[]).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(DateTime).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(String[]).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(DateTime[]).FullName; + VerifyTypeParameter(output, required, type); + + type = typeof(DerrivedFromITaskItem[]).FullName + "," + typeof(DerrivedFromITaskItem).Assembly.FullName; + VerifyTypeParameter(output, required, type); + } + + /// + /// Verify that an arbitrary output type class which is not derrived from ITaskItem is not allowed + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void BadOutputParameter() + { + // Notice output is true + string output = bool.TrueString; + string required = bool.TrueString; + string type = typeof(ArrayList).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify when the output parameter is not set that it defaults to false + /// + [TestMethod] + public void EmptyOutput() + { + string output = ""; + string required = bool.TrueString; + string type = typeof(String).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(((TaskPropertyInfo)registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"]).Output == false); + } + + /// + /// Verify when the output parameter is empty that it defaults to false + /// + [TestMethod] + public void NullOutput() + { + string output = null; + string required = bool.TrueString; + string type = typeof(String).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(((TaskPropertyInfo)registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"]).Output == false); + } + + /// + /// Verify that a random string which is not a boolean causes an invalid project file exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RandomOutput() + { + string output = "RandomStuff"; + string required = bool.TrueString; + string type = typeof(String).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify an empty required value results in a default value of false + /// + [TestMethod] + public void EmptyRequired() + { + string output = bool.TrueString; + string required = ""; + string type = typeof(String).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(((TaskPropertyInfo)registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"]).Required == false); + } + + /// + /// Verify a null required value results in a default value of false + /// + [TestMethod] + public void NullRequired() + { + string output = bool.TrueString; + string required = null; + string type = typeof(String).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(((TaskPropertyInfo)registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"]).Required == false); + } + + /// + /// Verify a value which cannot be parsed to a boolean results in a invalidprojectfileexception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RandomRequired() + { + string output = bool.TrueString; + string required = "RANDOM"; + string type = typeof(String).FullName; + + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify that expansion of the attributes works. + /// + [TestMethod] + public void ExpandedGoodParameters() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperFactory"; + + // Add empty parameterGroup + UsingTaskParameterGroupElement parameterGroup = element.AddParameterGroup(); + ProjectUsingTaskParameterElement defaultParameter = parameterGroup.AddParameter("ParameterWithNoAttributes"); + + ProjectUsingTaskParameterElement filledOutAttributesParameter = parameterGroup.AddParameter("ParameterWithAllAttributesHardCoded"); + filledOutAttributesParameter.Output = "$(TrueString)"; + filledOutAttributesParameter.Required = "@(ItemWithTrueItem)"; + filledOutAttributesParameter.ParameterType = "$(ITaskItem)"; + + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + int registeredTaskCount = TaskRegistryHelperMethods.GetDeepCountOfRegisteredTasks(registry.TaskRegistrations); + Assert.AreEqual(1, registeredTaskCount, "Expected three registered tasks in TaskRegistry.TaskRegistrations!"); + IDictionary> registeredTasks = registry.TaskRegistrations; + + ProjectUsingTaskElement taskElement = elementList[0]; + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)]; + Assert.IsNotNull(registeredTaskRecords, "Task to be found in TaskRegistry.TaskRegistrations!"); + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + + TaskRegistry.RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord = registeredTaskRecords[0].ParameterGroupAndTaskBody; + Assert.IsNotNull(inlineTaskRecord); + Assert.IsNull(inlineTaskRecord.InlineTaskXmlBody); + Assert.IsTrue(inlineTaskRecord.UsingTaskParameters.Count == 2); + + string expandedOutput = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(filledOutAttributesParameter.Output, ExpanderOptions.ExpandPropertiesAndItems, filledOutAttributesParameter.OutputLocation); + string expandedRequired = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(filledOutAttributesParameter.Required, ExpanderOptions.ExpandPropertiesAndItems, filledOutAttributesParameter.RequiredLocation); + string expandedType = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(filledOutAttributesParameter.ParameterType, ExpanderOptions.ExpandPropertiesAndItems, filledOutAttributesParameter.ParameterTypeLocation); + + TaskPropertyInfo parameterInfo = inlineTaskRecord.UsingTaskParameters[filledOutAttributesParameter.Name] as TaskPropertyInfo; + Assert.IsNotNull(parameterInfo); + Assert.AreEqual(parameterInfo.Name, filledOutAttributesParameter.Name); + Assert.AreEqual(parameterInfo.Output, bool.Parse(expandedOutput)); + Assert.AreEqual(parameterInfo.Required, bool.Parse(expandedRequired)); + Assert.AreEqual(parameterInfo.PropertyType, Type.GetType(expandedType + "," + typeof(ITaskItem).Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */)); + } + #endregion + + #region TaskBodyTests + + /// + /// Verify that expansion of the evaluate attribute. + /// + [TestMethod] + public void ExpandedPropertyEvaluate() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperFactory"; + element.AddUsingTaskBody("$(FalseString)", String.Empty); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)]; + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + + TaskRegistry.RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord = registeredTaskRecords[0].ParameterGroupAndTaskBody; + Assert.IsNotNull(inlineTaskRecord); + Assert.IsFalse(inlineTaskRecord.TaskBodyEvaluated); + } + + /// + /// Verify that expansion of the evaluate attribute. + /// + [TestMethod] + public void ExpandedItemEvaluate() + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperFactory"; + element.AddUsingTaskBody("@(ItemWithTrueItem)", String.Empty); + elementList.Add(element); + + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + List registeredTaskRecords = registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)]; + Assert.AreEqual(1, registeredTaskRecords.Count, "Expected only one task registered under this TaskName!"); + + TaskRegistry.RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord = registeredTaskRecords[0].ParameterGroupAndTaskBody; + Assert.IsNotNull(inlineTaskRecord); + Assert.IsTrue(inlineTaskRecord.TaskBodyEvaluated); + } + + /// + /// Verify when false is passed to evaluate value results in a false value being set + /// + [TestMethod] + public void FalseEvaluateWithBody() + { + string body = "$(Property1)@(ThirdItem)$(Property2)"; + List elementList = CreateTaskBodyElementWithAttributes(bool.FalseString, body); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Make sure when evaluate is false the string passed in is not expanded + Assert.IsFalse(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.TaskBodyEvaluated.Equals(body)); + } + + /// + /// Verify when false is passed to evaluate value results in a false value being set + /// + [TestMethod] + public void EvaluateWithBody() + { + string body = "$(Property1)@(ThirdItem)$(Property2)"; + List elementList = CreateTaskBodyElementWithAttributes(bool.TrueString, body); + ProjectUsingTaskElement taskElement = elementList[0]; + ProjectUsingTaskBodyElement bodyElement = taskElement.TaskBody; + + string expandedBody = TaskRegistryHelperMethods.RegistryExpander.ExpandIntoStringAndUnescape(body, ExpanderOptions.ExpandPropertiesAndItems, bodyElement.Location); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + // Make sure when evaluate is false the string passed in is not expanded + Assert.IsFalse(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.TaskBodyEvaluated.Equals(expandedBody)); + } + + /// + /// Verify that a random string which is not a boolean causes an invalid project file exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RandomEvaluate() + { + string evaluate = "RandomStuff"; + List elementList = CreateTaskBodyElementWithAttributes(evaluate, ""); + TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.Fail(); + } + + /// + /// Verify when false is passed to evaluate value results in a false value being set + /// + [TestMethod] + public void FalseEvaluate() + { + string evaluate = bool.FalseString; + List elementList = CreateTaskBodyElementWithAttributes(evaluate, ""); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsFalse(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.TaskBodyEvaluated); + } + + /// + /// Verify an empty evaluate value results in a default value of true + /// + [TestMethod] + public void EmptyEvaluate() + { + string evaluate = ""; + List elementList = CreateTaskBodyElementWithAttributes(evaluate, ""); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.TaskBodyEvaluated); + } + + /// + /// Verify a null evaluate value results in a default value of true + /// + [TestMethod] + public void NullEvaluate() + { + string evaluate = null; + List elementList = CreateTaskBodyElementWithAttributes(evaluate, ""); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + Assert.IsTrue(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.TaskBodyEvaluated); + } + #endregion + + #region Helper Methods + + /// + /// With the given task registry, retrieve a copy of the test task with the given runtime and + /// architecture and verify: + /// - that it was retrieved (or not) as expected + /// - that it was retrieved from the cache (or not) as expected + /// - that the record that was retrieved had the expected runtime and architecture + /// values as its factory parameters. + /// + private static void RetrieveAndValidateRegisteredTaskRecord + ( + TaskRegistry registry, + bool exactMatchRequired, + Dictionary taskParameters, + bool shouldBeRetrieved, + bool shouldBeRetrievedFromCache, + string expectedRuntime, + string expectedArchitecture + ) + { + bool retrievedFromCache = false; + var record = registry.GetTaskRegistrationRecord(TestTaskName, null, taskParameters, exactMatchRequired, s_targetLoggingContext, s_elementLocation, out retrievedFromCache); + + if (shouldBeRetrieved) + { + Assert.IsNotNull(record, "Should have retrieved a match."); + + if (expectedRuntime != null) + { + Assert.AreEqual(expectedRuntime, record.TaskFactoryParameters[XMakeAttributes.runtime]); + } + + if (expectedArchitecture != null) + { + Assert.AreEqual(expectedArchitecture, record.TaskFactoryParameters[XMakeAttributes.architecture]); + } + } + else + { + Assert.IsNull(record, "Should not have been a match."); + } + + Assert.AreEqual(shouldBeRetrievedFromCache, retrievedFromCache); + } + + /// + /// With the given task registry, retrieve a copy of the test task with the given runtime and + /// architecture and verify: + /// - that it was retrieved (or not) as expected + /// - that it was retrieved from the cache (or not) as expected + /// - that the record that was retrieved had the expected runtime and architecture + /// values as its factory parameters. + /// + private static void RetrieveAndValidateRegisteredTaskRecord + ( + TaskRegistry registry, + bool exactMatchRequired, + string runtime, + string architecture, + bool shouldBeRetrieved, + bool shouldBeRetrievedFromCache, + string expectedRuntime, + string expectedArchitecture + ) + { + Dictionary parameters = null; + if (runtime != null || architecture != null) + { + parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + parameters.Add(XMakeAttributes.runtime, runtime ?? XMakeAttributes.MSBuildRuntimeValues.any); + parameters.Add(XMakeAttributes.architecture, architecture ?? XMakeAttributes.MSBuildArchitectureValues.any); + } + + RetrieveAndValidateRegisteredTaskRecord(registry, exactMatchRequired, parameters, shouldBeRetrieved, shouldBeRetrievedFromCache, expectedRuntime, expectedArchitecture); + } + + /// + /// With the given task registry, retrieve a copy of the test task with the given runtime and + /// architecture and verify: + /// - that it was retrieved (or not) as expected + /// - that it was retrieved from the cache (or not) as expected + /// + private static void RetrieveAndValidateRegisteredTaskRecord(TaskRegistry registry, bool exactMatchRequired, Dictionary taskParameters, bool shouldBeRetrieved, bool shouldBeRetrievedFromCache) + { + // if we're requiring an exact match, we can cheat and figure out what the expected runtime / architecture should be. + // if not, then if the user didn't pass us an expected runtime, we can't really check it, so just pass + // null (which will be treated as "don't validate"). + string expectedRuntime = null; + string expectedArchitecture = null; + if (exactMatchRequired) + { + taskParameters.TryGetValue(XMakeAttributes.runtime, out expectedRuntime); + taskParameters.TryGetValue(XMakeAttributes.architecture, out expectedArchitecture); + } + + RetrieveAndValidateRegisteredTaskRecord(registry, exactMatchRequired, taskParameters, shouldBeRetrieved, shouldBeRetrievedFromCache, expectedRuntime, expectedArchitecture); + } + + /// + /// With the given task registry, retrieve a copy of the test task with the given runtime and + /// architecture and verify: + /// - that it was retrieved (or not) as expected + /// - that it was retrieved from the cache (or not) as expected + /// + private static void RetrieveAndValidateRegisteredTaskRecord(TaskRegistry registry, bool exactMatchRequired, string runtime, string architecture, bool shouldBeRetrieved, bool shouldBeRetrievedFromCache) + { + // if we're requiring an exact match, we can cheat and figure out what the expected runtime / architecture should be. + // if not, then if the user didn't pass us an expected runtime, we can't really check it, so just pass + // null (which will be treated as "don't validate"). + string expectedRuntime = exactMatchRequired ? runtime : null; + string expectedArchitecture = exactMatchRequired ? architecture : null; + + RetrieveAndValidateRegisteredTaskRecord(registry, exactMatchRequired, runtime, architecture, shouldBeRetrieved, shouldBeRetrievedFromCache, expectedRuntime, expectedArchitecture); + } + + /// + /// Make sure the type passed in is the same type which is parsed out. + /// + private static void VerifyTypeParameter(string output, string required, string type) + { + List elementList = CreateParameterElementWithAttributes(output, required, type); + TaskRegistry registry = TaskRegistryHelperMethods.CreateTaskRegistryAndRegisterTasks(elementList); + + Type paramType = Type.GetType(type); + + // The type may be in the Microsoft.Build.Framework Assembly + if (paramType == null) + { + paramType = Type.GetType(type + "," + typeof(ITaskItem).Assembly.FullName, false /* don't throw on error */, true /* case-insensitive */); + } + + Assert.IsTrue(registry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("Name", null)][0].ParameterGroupAndTaskBody.UsingTaskParameters["ParameterWithAllAttributesHardCoded"].PropertyType.Equals(paramType)); + } + + /// + /// Create a parameter element with the passed in attributes, this method will help with testing. + /// + private static List CreateParameterElementWithAttributes(string output, string required, string type) + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperFactory"; + + // Add empty parameterGroup + UsingTaskParameterGroupElement parameterGroup = element.AddParameterGroup(); + ProjectUsingTaskParameterElement filledOutAttributesParameter = parameterGroup.AddParameter("ParameterWithAllAttributesHardCoded", output, required, type); + elementList.Add(element); + return elementList; + } + + /// + /// Create a task body element with the passed in attributes, this method will help with testing. + /// + private static List CreateTaskBodyElementWithAttributes(string evaluate, string body) + { + List elementList = new List(); + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectUsingTaskElement element = project.AddUsingTask("Name", "File", null); + element.TaskFactory = "SuperFactory"; + element.AddUsingTaskBody(evaluate, body); + elementList.Add(element); + return elementList; + } + + /// + /// Generates a test task assembly containing a single task named "TestTask" + /// and returns the path to that assembly. + /// + private static string GetTestTaskAssemblyLocation() + { + string codeFile = null; + string outputFile = Path.Combine(Path.GetTempPath(), "TaskRegistryTests_TestTask.dll"); + string codeContent = @" +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace TestTask +{ + public class TestTask : Task + { + public override bool Execute() + { + return true; + } + } +}"; + + File.Delete(outputFile); + bool succeeded = true; + + try + { + codeFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(codeFile, codeContent); + Csc csc = new Csc(); + csc.BuildEngine = new MockEngine(); + csc.Sources = new ITaskItem[] { new TaskItem(codeFile) }; + csc.OutputAssembly = new TaskItem(outputFile); + csc.References = new ITaskItem[] { new TaskItem("Microsoft.Build.Framework.dll"), new TaskItem("Microsoft.Build.Utilities.Core.dll") }; + csc.Platform = "AnyCPU"; + csc.TargetType = "Library"; + csc.Prefer32Bit = false; + succeeded = csc.Execute(); + } + catch (Exception) + { + if (File.Exists(outputFile)) + { + FileUtilities.DeleteNoThrow(outputFile); + } + + // now rethrow + } + finally + { + File.Delete(codeFile); + } + + if (succeeded) + { + return outputFile; + } + else + { + return null; + } + } + + /// + /// Create a custom class derrived from ITaskItem to test input and output parameters work using this item. + /// + internal class DerrivedFromITaskItem : ITaskItem + { + /// + /// The ItemSpec of the item + /// + public string ItemSpec + { + get; + set; + } + + /// + /// Collection of metadataNames on the item + /// + public ICollection MetadataNames + { + get { throw new NotImplementedException(); } + } + + /// + /// Number of metadata items on the item + /// + public int MetadataCount + { + get { throw new NotImplementedException(); } + } + + /// + /// Get the meatadata on the item based on the metadataName + /// + public string GetMetadata(string metadataName) + { + throw new NotImplementedException(); + } + + /// + /// Set some metadata on the item + /// + public void SetMetadata(string metadataName, string metadataValue) + { + throw new NotImplementedException(); + } + + /// + /// Remove some metadata from the item + /// + public void RemoveMetadata(string metadataName) + { + throw new NotImplementedException(); + } + + /// + /// Copy the metadata from this item to another item. + /// + public void CopyMetadataTo(ITaskItem destinationItem) + { + throw new NotImplementedException(); + } + + /// + /// Clone the custom metadata from this item + /// + public IDictionary CloneCustomMetadata() + { + throw new NotImplementedException(); + } + } + + /// + /// Helper class to assist in using the task registry + /// + /// ProjectPropertyInstance + /// ProjectItemInstance + internal class TaskRegistryHelperMethods + where P : class, IProperty + where I : class, IItem + { + /// + /// Expander to expand the registry entires + /// + private static Expander s_registryExpander; + + /// + /// Accessor to the expander + /// + internal static Expander RegistryExpander + { + get + { + if (s_registryExpander == null) + { + s_registryExpander = GetExpander(); + } + + return s_registryExpander; + } + } + + /// + /// Count the number of registry records which exist in the task registry + /// + internal static int GetDeepCountOfRegisteredTasks(IDictionary> registryRecords) + { + if (registryRecords == null) + { + return 0; + } + + int count = 0; + foreach (List recordList in registryRecords.Values) + { + count += recordList.Count; + } + + return count; + } + + /// + /// Create and fill a task registry based on some using task elements. + /// + internal static TaskRegistry CreateTaskRegistryAndRegisterTasks(List usingTaskElements) + { + TaskRegistry registry = new TaskRegistry(ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); + + foreach (ProjectUsingTaskElement projectUsingTaskElement in usingTaskElements) + { + TaskRegistry.RegisterTasksFromUsingTaskElement + ( + s_loggingService, + s_loggerContext, + Directory.GetCurrentDirectory(), + projectUsingTaskElement, + registry, + RegistryExpander, + ExpanderOptions.ExpandPropertiesAndItems + ); + } + + return registry; + } + + /// + /// Create an expander with some property values which can be used for testing. + /// + internal static Expander GetExpander() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + for (int i = 1; i < 6; i++) + { + pg.Set(ProjectPropertyInstance.Create("Property" + i, "Value" + i)); + } + + pg.Set(ProjectPropertyInstance.Create("TrueString", "True")); + pg.Set(ProjectPropertyInstance.Create("FalseString", "False")); + pg.Set(ProjectPropertyInstance.Create("ItaskItem", "Microsoft.Build.Framework.ItaskItem[]")); + + List intermediateAssemblyItemGroup = new List(); + ProjectItemInstance iag = new ProjectItemInstance(project, "IntermediateAssembly", @"subdir1\engine.dll", project.FullPath); + intermediateAssemblyItemGroup.Add(iag); + iag.SetMetadata("aaa", "111"); + + iag = new ProjectItemInstance(project, "IntermediateAssembly", @"subdir2\tasks.dll", project.FullPath); + intermediateAssemblyItemGroup.Add(iag); + iag.SetMetadata("bbb", "222"); + + List firstItemGroup = new List(); + for (int i = 0; i < 3; i++) + { + ProjectItemInstance fig = new ProjectItemInstance(project, "FirstItem" + i, "FirstValue" + i, project.FullPath); + firstItemGroup.Add(fig); + } + + List secondItemGroup = new List(); + for (int i = 0; i < 3; i++) + { + ProjectItemInstance sig = new ProjectItemInstance(project, "SecondItem" + i, "SecondValue" + i, project.FullPath); + secondItemGroup.Add(sig); + } + + List thirdItemGroup = new List(); + ProjectItemInstance tig = new ProjectItemInstance(project, "ThirdItem", "ThirdValue1", project.FullPath); + thirdItemGroup.Add(tig); + + List trueItemGroup = new List(); + ProjectItemInstance trig = new ProjectItemInstance(project, "ItemWithTrueItem", "true", project.FullPath); + trueItemGroup.Add(trig); + + ItemDictionary secondaryItemsByName = new ItemDictionary(); + secondaryItemsByName.ImportItems(intermediateAssemblyItemGroup); + secondaryItemsByName.ImportItems(firstItemGroup); + secondaryItemsByName.ImportItems(secondItemGroup); + secondaryItemsByName.ImportItems(thirdItemGroup); + secondaryItemsByName.ImportItems(trueItemGroup); + + Expander expander = new Expander(pg, secondaryItemsByName); + return expander; + } + } + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/BackEnd/TranslationHelpers.cs b/src/XMakeBuildEngine/UnitTests/BackEnd/TranslationHelpers.cs new file mode 100644 index 00000000000..78a960a9aa7 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/BackEnd/TranslationHelpers.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper class for serialization tests. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Class containing methods used to assist in testing serialization methods. + /// + static internal class TranslationHelpers + { + /// + /// The stream backing the serialization classes. + /// + static private MemoryStream s_serializationStream; + + /// + /// Gets a serializer used to write data. Note that only one such serializer may be used from this class at a time. + /// + static internal INodePacketTranslator GetWriteTranslator() + { + s_serializationStream = new MemoryStream(); + return NodePacketTranslator.GetWriteTranslator(s_serializationStream); + } + + /// + /// Gets a serializer used to read data. Note that only one such serializer may be used from this class at a time, + /// and this must be called after GetWriteTranslator() has been called. + /// + static internal INodePacketTranslator GetReadTranslator() + { + s_serializationStream.Seek(0, SeekOrigin.Begin); + return NodePacketTranslator.GetReadTranslator(s_serializationStream, null); + } + + /// + /// Compares two collections. + /// + /// The collections element type. + /// The left collections. + /// The right collections. + /// The comparer to use on each element. + /// True if the collections are equivalent. + static internal bool CompareCollections(ICollection left, ICollection right, IComparer comparer) + { + if (Object.ReferenceEquals(left, right)) + { + return true; + } + + if ((left == null) ^ (right == null)) + { + return false; + } + + if (left.Count != right.Count) + { + return false; + } + + T[] leftArray = left.ToArray(); + T[] rightArray = right.ToArray(); + + for (int i = 0; i < leftArray.Length; i++) + { + if (comparer.Compare(leftArray[i], rightArray[i]) != 0) + { + return false; + } + } + + return true; + } + + /// + /// Compares two exceptions. + /// + static internal bool CompareExceptions(Exception left, Exception right) + { + if (Object.ReferenceEquals(left, right)) + { + return true; + } + + if ((left == null) ^ (right == null)) + { + return false; + } + + if (left.Message != right.Message) + { + return false; + } + + if (left.StackTrace != right.StackTrace) + { + return false; + } + + return CompareExceptions(left.InnerException, right.InnerException); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Collections/CopyOnReadEnumerable_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/CopyOnReadEnumerable_Tests.cs new file mode 100644 index 00000000000..ac5662f6d70 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/CopyOnReadEnumerable_Tests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests the CopyOnReadEnumerable utility class +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for CopyOnReadEnumerable + /// + [TestClass] + public class CopyOnReadEnumerable_Tests + { + /// + /// Verify basic case + /// + [TestMethod] + public void NonCloneableBackingCollection() + { + List values = new List(new int[] { 1, 2, 3 }); + + CopyOnReadEnumerable enumerable = new CopyOnReadEnumerable(values, values); + + using (IEnumerator enumerator = values.GetEnumerator()) + { + foreach (int i in enumerable) + { + enumerator.MoveNext(); + Assert.AreEqual(i, enumerator.Current); + } + } + } + + /// + /// Verify cloning case + /// + [TestMethod] + public void CloneableBackingCollection() + { + List values = new List(new Cloneable[] { new Cloneable(), new Cloneable(), new Cloneable() }); + + CopyOnReadEnumerable enumerable = new CopyOnReadEnumerable(values, values); + + using (IEnumerator enumerator = values.GetEnumerator()) + { + foreach (Cloneable i in enumerable) + { + enumerator.MoveNext(); + Assert.IsFalse(Object.ReferenceEquals(i, enumerator.Current), "Enumerator copied references."); + } + } + } + + /// + /// A class used for testing cloneable backing collections. + /// + private class Cloneable : IDeepCloneable + { + #region IDeepCloneable Members + + /// + /// Clones the object. + /// + /// The new instance. + public Cloneable DeepClone() + { + return new Cloneable(); + } + + #endregion + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Collections/FilteringEnumerable_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/FilteringEnumerable_Tests.cs new file mode 100644 index 00000000000..c9e8daac7e8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/FilteringEnumerable_Tests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests the FilteringEnumerable utility class +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for FilteringEnumerable + /// + [TestClass] + public class FilteringEnumerable_Tests + { + /// + /// Verify basic case + /// + [TestMethod] + public void FilteringEnumerableBasic() + { + A a1 = new A(); + B b1 = new B(); + A a2 = new A(); + B b2 = new B(); + + List
list = new List(); + list.Add(a1); + list.Add(b1); + list.Add(a2); + list.Add(b2); + var collection = new FilteringEnumerable(list); + + List result = new List(collection); + Assert.AreEqual(2, result.Count); + } + + /// + /// Null collection should be like an empty collection + /// (Seems useful for a general purpose class) + /// + [TestMethod] + public void FilteringEnumerableNullBacking() + { + IEnumerable enumerable = new FilteringEnumerable(null); + + Assert.AreEqual(false, enumerable.GetEnumerator().MoveNext()); + } + + /// + /// Test class + /// + private class A + { + } + + /// + /// Test class + /// + private class B : A + { + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Collections/LookasideStringInterner_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/LookasideStringInterner_Tests.cs new file mode 100644 index 00000000000..716562aa0a9 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/LookasideStringInterner_Tests.cs @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the lookaside string interner used for serialization. +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using System.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; +using Microsoft.Build.BackEnd; +using System.IO; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + [TestClass] + public class LookasideStringInterner_Tests + { + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void Empty() + { + var interner = new LookasideStringInterner(StringComparer.OrdinalIgnoreCase, 1); + interner.GetString(0); + } + + [TestMethod] + public void BasicInterning() + { + var interner = new LookasideStringInterner(StringComparer.OrdinalIgnoreCase, 1); + int nullIndex = interner.Intern(null); + int emptyIndex = interner.Intern(String.Empty); + int strIndex = interner.Intern("abc123def456"); + + Assert.AreEqual(interner.Intern(null), nullIndex); + Assert.AreEqual(interner.Intern(String.Empty), emptyIndex); + Assert.AreEqual(interner.Intern("abc123def456"), strIndex); + + Assert.AreEqual(interner.GetString(nullIndex), null); + Assert.AreEqual(interner.GetString(emptyIndex), String.Empty); + Assert.AreEqual(interner.GetString(strIndex), "abc123def456"); + } + + [TestMethod] + public void Serialization() + { + var interner = new LookasideStringInterner(StringComparer.OrdinalIgnoreCase, 1); + int nullIndex = interner.Intern(null); + int emptyIndex = interner.Intern(String.Empty); + int strIndex = interner.Intern("abc123def456"); + + MemoryStream stream = new MemoryStream(); + INodePacketTranslator writetranslator = NodePacketTranslator.GetWriteTranslator(stream); + + interner.Translate(writetranslator); + + INodePacketTranslator readtranslator = NodePacketTranslator.GetReadTranslator(stream, null); + var newInterner = new LookasideStringInterner(readtranslator); + Assert.AreEqual(newInterner.GetString(nullIndex), null); + Assert.AreEqual(newInterner.GetString(emptyIndex), String.Empty); + Assert.AreEqual(newInterner.GetString(strIndex), "abc123def456"); + } + + [TestMethod] + public void ReuseOfDeserializedInternerNotAllowed() + { + var interner = new LookasideStringInterner(StringComparer.OrdinalIgnoreCase, 1); + int strIndex = interner.Intern("abc123def456"); + + MemoryStream stream = new MemoryStream(); + INodePacketTranslator writetranslator = NodePacketTranslator.GetWriteTranslator(stream); + + interner.Translate(writetranslator); + + INodePacketTranslator readtranslator = NodePacketTranslator.GetReadTranslator(stream, null); + var newInterner = new LookasideStringInterner(readtranslator); + + bool gotException = false; + try + { + newInterner.Intern("foo"); + } + catch (Exception) + { + gotException = true; + } + + Assert.IsTrue(gotException); + } + + [TestMethod] + public void ComparerIsObeyed() + { + var interner = new LookasideStringInterner(StringComparer.OrdinalIgnoreCase, 1); + int strIndex = interner.Intern("abc123def456"); + Assert.AreEqual(interner.Intern("ABC123DEF456"), strIndex); + + var interner2 = new LookasideStringInterner(StringComparer.Ordinal, 1); + int strIndex2 = interner2.Intern("abc123def456"); + Assert.AreNotEqual(interner.Intern("ABC123DEF456"), strIndex2); + } + + } + +} diff --git a/src/XMakeBuildEngine/UnitTests/Collections/MSBuildNameIgnoreCaseComparer_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/MSBuildNameIgnoreCaseComparer_Tests.cs new file mode 100644 index 00000000000..aa5293e6ea8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/MSBuildNameIgnoreCaseComparer_Tests.cs @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests the MSBuildNameIgnoreCaseComparer +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for MSBuildNameIgnoreCaseComparer + /// + [TestClass] + public class MSBuildNameIgnoreCaseComparer_Tests + { + /// + /// Verify default comparer works on the whole string + /// + [TestMethod] + public void DefaultEquals() + { + Assert.AreEqual(true, MSBuildNameIgnoreCaseComparer.Default.Equals("FOO", "foo")); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("FOO", " FOO")); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("FOOA", "FOOB")); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("AFOO", "BFOO")); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("FOO", "FOO ")); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("a", "b")); + Assert.AreEqual(true, MSBuildNameIgnoreCaseComparer.Default.Equals("", "")); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("x", null)); + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals(null, "x")); + Assert.AreEqual(true, MSBuildNameIgnoreCaseComparer.Default.Equals(null, null)); + } + + /// + /// Compare real expressions + /// + [TestMethod] + public void MatchProperty() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p = ProjectPropertyInstance.Create("foo", "bar"); + + dictionary.Set(p); + + string s = "$(foo)"; + ProjectPropertyInstance value = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, s, 2, 4); + + Assert.IsTrue(Object.ReferenceEquals(p, value), "Should have returned the same object as was inserted"); + + try + { + MSBuildNameIgnoreCaseComparer.Mutable.SetConstraintsForUnitTestingOnly(s, 2, 4); + Assert.AreEqual(MSBuildNameIgnoreCaseComparer.Default.GetHashCode("foo"), MSBuildNameIgnoreCaseComparer.Mutable.GetHashCode(s)); + } + finally + { + MSBuildNameIgnoreCaseComparer.Mutable.RemoveConstraintsForUnitTestingOnly(); + } + } + + /// + /// Default comparer is immutable + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void Immutable() + { + Dictionary dictionary = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); + + MSBuildNameIgnoreCaseComparer.Default.GetValueWithConstraints(dictionary, "x", 0, 1); + } + + /// + /// Objects work + /// + [TestMethod] + public void NonString() + { + Object o = new Object(); + Assert.AreEqual(true, ((IEqualityComparer)MSBuildNameIgnoreCaseComparer.Default).Equals(o, o)); + } + + /// + /// Null + /// + [TestMethod] + public void Null1() + { + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals("x", null)); + } + + /// + /// Null + /// + [TestMethod] + public void Null2() + { + Assert.AreEqual(false, MSBuildNameIgnoreCaseComparer.Default.Equals(null, "x")); + } + + /// + /// Make sure we can handle the case where the dictionary is null. + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void NullDictionary() + { + MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(null, "s", 0, 1); + } + + /// + /// Invalid start + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InvalidValue2() + { + PropertyDictionary dictionary = new PropertyDictionary(); + MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "x", -1, 0); + } + + /// + /// Invalid small end + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InvalidValue4() + { + PropertyDictionary dictionary = new PropertyDictionary(); + MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "x", 0, -1); + } + + /// + /// Invalid large end + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void InvalidValue5() + { + PropertyDictionary dictionary = new PropertyDictionary(); + MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "x", 0, 2); + } + + /// + /// End past the end of other string + /// + [TestMethod] + public void EqualsEndPastEnd1() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p = ProjectPropertyInstance.Create("bbb", "value"); + dictionary.Set(p); + + ProjectPropertyInstance value = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "abbbaaa", 1, 3); + + Assert.IsTrue(Object.ReferenceEquals(p, value), "Should have returned the same object as was inserted"); + } + + /// + /// Same values means one char + /// + [TestMethod] + public void EqualsSameStartEnd1() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p1 = ProjectPropertyInstance.Create("A", "value1"); + ProjectPropertyInstance p2 = ProjectPropertyInstance.Create("B", "value2"); + + dictionary.Set(p1); + dictionary.Set(p2); + + ProjectPropertyInstance value = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "babbbb", 1, 1); + + Assert.IsTrue(Object.ReferenceEquals(p1, value), "Should have returned the 'A' value"); + } + + /// + /// Same values means one char + /// + [TestMethod] + public void EqualsSameStartEnd2() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p1 = ProjectPropertyInstance.Create("a", "value1"); + ProjectPropertyInstance p2 = ProjectPropertyInstance.Create("b", "value2"); + + dictionary.Set(p1); + dictionary.Set(p2); + + ProjectPropertyInstance value = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "aabaa", 2, 2); + + Assert.IsTrue(Object.ReferenceEquals(p2, value), "Should have returned the 'b' value"); + } + + /// + /// Same values means one char + /// + [TestMethod] + public void EqualsSameStartEnd3() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p1 = ProjectPropertyInstance.Create("a", "value1"); + ProjectPropertyInstance p2 = ProjectPropertyInstance.Create("b", "value2"); + + dictionary.Set(p1); + dictionary.Set(p2); + + ProjectPropertyInstance value = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "ab", 0, 0); + + Assert.IsTrue(Object.ReferenceEquals(p1, value), "Should have returned the 'a' value"); + } + + /// + /// Start at 0 + /// + [TestMethod] + public void EqualsStartZero() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p1 = ProjectPropertyInstance.Create("aab", "value1"); + ProjectPropertyInstance p2 = ProjectPropertyInstance.Create("aba", "value2"); + + dictionary.Set(p1); + dictionary.Set(p2); + + ProjectPropertyInstance value = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, "aabaa", 0, 2); + + Assert.IsTrue(Object.ReferenceEquals(p1, value), "Should have returned the 'aab' value"); + } + + /// + /// End at end + /// + [TestMethod] + public void EqualsEndEnd() + { + PropertyDictionary dictionary = new PropertyDictionary(); + + ProjectPropertyInstance p1 = ProjectPropertyInstance.Create("aabaaaa", "value1"); + ProjectPropertyInstance p2 = ProjectPropertyInstance.Create("baaaa", "value2"); + dictionary.Set(p1); + dictionary.Set(p2); + + string constraint = "aabaaa"; + + ProjectPropertyInstance p3 = ProjectPropertyInstance.Create("abaaa", "value3"); + dictionary.Set(p3); + + // Should match o3 + ProjectPropertyInstance value1 = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, constraint, 1, 5); + + Assert.IsTrue(Object.ReferenceEquals(p3, value1), "Should have returned the 'abaaa' value"); + + dictionary.Remove("abaaa"); // get rid of o3 + + ProjectPropertyInstance value2 = MSBuildNameIgnoreCaseComparer.Mutable.GetValueWithConstraints(dictionary, constraint, 1, 5); + + Assert.IsNull(value2, "Should not have been a match in the dictionary"); + + // Even if the string is exactly the same, if only a substring is being compared, then although it + // will be judged equal, the hash codes will NOT be the same, and for that reason, a lookup in the + // dictionary will fail. + int originalHashCode = MSBuildNameIgnoreCaseComparer.Mutable.GetHashCode("aabaaa"); + try + { + MSBuildNameIgnoreCaseComparer.Mutable.SetConstraintsForUnitTestingOnly(constraint, 1, 5); + + Assert.IsTrue(MSBuildNameIgnoreCaseComparer.Mutable.Equals("aabaaa", constraint)); // same on both sides + Assert.AreNotEqual(originalHashCode, MSBuildNameIgnoreCaseComparer.Mutable.GetHashCode(constraint)); + } + finally + { + MSBuildNameIgnoreCaseComparer.Mutable.RemoveConstraintsForUnitTestingOnly(); + } + } + + /// + /// Default get hash code + /// + [TestMethod] + public void DefaultGetHashcode() + { + Assert.AreEqual(true, 0 == MSBuildNameIgnoreCaseComparer.Default.GetHashCode(null)); + + MSBuildNameIgnoreCaseComparer.Default.GetHashCode(""); // doesn't throw + Assert.AreEqual(MSBuildNameIgnoreCaseComparer.Default.GetHashCode("aBc"), MSBuildNameIgnoreCaseComparer.Default.GetHashCode("AbC")); + } + + /// + /// Indexed get hashcode + /// + [TestMethod] + public void IndexedGetHashcode1() + { + MSBuildNameIgnoreCaseComparer comparer = MSBuildNameIgnoreCaseComparer.Mutable; + string s = "xyz"; + + try + { + comparer.SetConstraintsForUnitTestingOnly(s, 0, 0); + + comparer.GetHashCode(""); // does not crash + + Assert.AreEqual(true, 0 == comparer.GetHashCode(null)); + Assert.AreEqual(comparer.GetHashCode("aBc"), comparer.GetHashCode("AbC")); + Assert.AreEqual(comparer.GetHashCode(s), comparer.GetHashCode("x")); + } + finally + { + comparer.RemoveConstraintsForUnitTestingOnly(); + } + } + + /// + /// Indexed get hashcode + /// + [TestMethod] + public void IndexedGetHashcode2() + { + MSBuildNameIgnoreCaseComparer comparer = MSBuildNameIgnoreCaseComparer.Mutable; + string s = "xyz"; + + try + { + comparer.SetConstraintsForUnitTestingOnly(s, 1, 2); + + Assert.AreEqual(comparer.GetHashCode(s), comparer.GetHashCode("YZ")); + } + finally + { + comparer.RemoveConstraintsForUnitTestingOnly(); + } + } + + /// + /// Indexed get hashcode + /// + [TestMethod] + public void IndexedGetHashcode3() + { + MSBuildNameIgnoreCaseComparer comparer = MSBuildNameIgnoreCaseComparer.Mutable; + string s = "abcd"; + + try + { + comparer.SetConstraintsForUnitTestingOnly(s, 0, 2); + + Assert.AreEqual(comparer.GetHashCode(s), comparer.GetHashCode("abc")); + } + finally + { + comparer.RemoveConstraintsForUnitTestingOnly(); + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/Collections/MultiDictionary_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/MultiDictionary_Tests.cs new file mode 100644 index 00000000000..b0d051861b7 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/MultiDictionary_Tests.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the multi-dictionary class +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using System.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for the multi-dictionary class + /// + [TestClass] + public class MultiDictionary_Tests + { + /// + /// Empty dictionary + /// + [TestMethod] + public void Empty() + { + MultiDictionary dictionary = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + + Assert.AreEqual(0, dictionary.KeyCount); + Assert.AreEqual(0, dictionary.ValueCount); + + Assert.AreEqual(false, dictionary.Remove("x", "y")); + + foreach (string value in dictionary["x"]) + { + Assert.Fail(); + } + } + + /// + /// Remove stuff that is there + /// + [TestMethod] + public void Remove() + { + MultiDictionary dictionary = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + + dictionary.Add("x", "x1"); + dictionary.Add("x", "x2"); + dictionary.Add("y", "y1"); + + Assert.AreEqual(true, dictionary.Remove("x", "x1")); + + Assert.AreEqual(2, dictionary.KeyCount); + Assert.AreEqual(2, dictionary.ValueCount); + + Assert.AreEqual(true, dictionary.Remove("x", "x2")); + + Assert.AreEqual(1, dictionary.KeyCount); + Assert.AreEqual(1, dictionary.ValueCount); + + Assert.AreEqual(true, dictionary.Remove("y", "y1")); + + Assert.AreEqual(0, dictionary.KeyCount); + Assert.AreEqual(0, dictionary.ValueCount); + + dictionary.Add("x", "x1"); + dictionary.Add("x", "x2"); + + Assert.AreEqual(true, dictionary.Remove("x", "x2")); + + Assert.AreEqual(1, dictionary.KeyCount); + Assert.AreEqual(1, dictionary.ValueCount); + } + + /// + /// Remove stuff that isn't there + /// + [TestMethod] + public void RemoveNonExistent() + { + MultiDictionary dictionary = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + + dictionary.Add("x", "x1"); + dictionary.Add("x", "x2"); + dictionary.Add("y", "y1"); + + Assert.AreEqual(false, dictionary.Remove("z", "y1")); + Assert.AreEqual(false, dictionary.Remove("x", "y1")); + Assert.AreEqual(false, dictionary.Remove("y", "y2")); + + Assert.AreEqual(2, dictionary.KeyCount); + Assert.AreEqual(3, dictionary.ValueCount); + } + + /// + /// Enumerate over all values for a key + /// + [TestMethod] + public void Enumerate() + { + MultiDictionary dictionary = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + + dictionary.Add("x", "x1"); + dictionary.Add("x", "x2"); + dictionary.Add("y", "y1"); + + List values = Helpers.MakeList(dictionary["x"]); + values.Sort(); + + Assert.AreEqual(2, values.Count); + Assert.AreEqual("x1", values[0]); + Assert.AreEqual("x2", values[1]); + + values = Helpers.MakeList(dictionary["y"]); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual("y1", values[0]); + + values = Helpers.MakeList(dictionary["z"]); + + Assert.AreEqual(0, values.Count); + } + + /// + /// Mixture of adds and removes + /// + [TestMethod] + public void MixedAddRemove() + { + MultiDictionary dictionary = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + + dictionary.Add("x", "x1"); + dictionary.Remove("x", "x1"); + dictionary.Add("x", "x1"); + dictionary.Add("x", "x1"); + dictionary.Add("x", "x1"); + dictionary.Remove("x", "x1"); + dictionary.Remove("x", "x1"); + dictionary.Remove("x", "x1"); + dictionary.Add("x", "x2"); + + Assert.AreEqual(1, dictionary.KeyCount); + Assert.AreEqual(1, dictionary.ValueCount); + + List values = Helpers.MakeList(dictionary["x"]); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual("x2", values[0]); + } + + /// + /// Clearing out + /// + [TestMethod] + public void Clear() + { + MultiDictionary dictionary = new MultiDictionary(StringComparer.OrdinalIgnoreCase); + + dictionary.Add("x", "x1"); + dictionary.Add("x", "x2"); + dictionary.Add("y", "y1"); + + dictionary.Clear(); + + Assert.AreEqual(0, dictionary.KeyCount); + Assert.AreEqual(0, dictionary.ValueCount); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/Collections/OMcollections_tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/OMcollections_tests.cs new file mode 100644 index 00000000000..79fc65141db --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/OMcollections_tests.cs @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for several of the collections classes +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Framework; +using System.Collections; +using System.Linq; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; +using Microsoft.Build.UnitTests.BackEnd; +using ObjectModel = System.Collections.ObjectModel; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for several of the collections classes + /// + [TestClass] + public class OMcollections_Tests + { + /// + /// End to end test of PropertyDictionary + /// + [TestMethod] + public void BasicPropertyDictionary() + { + PropertyDictionary properties = new PropertyDictionary(); + + ProjectPropertyInstance p1 = GetPropertyInstance("p1", "v1"); + ProjectPropertyInstance p2 = GetPropertyInstance("p2", "v2"); + ProjectPropertyInstance p3 = GetPropertyInstance("p1", "v1"); + ProjectPropertyInstance p4 = GetPropertyInstance("p2", "v3"); + + properties.Set(p1); + properties.Set(p2); + properties.Set(p3); + properties.Set(p1); + properties.Set(p4); + + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("v1", properties["p1"].EvaluatedValue); + Assert.AreEqual("v3", properties["p2"].EvaluatedValue); + + Assert.AreEqual(true, properties.Remove("p1")); + Assert.IsNull(properties["p1"]); + + Assert.AreEqual(false, properties.Remove("x")); + + properties.Clear(); + + Assert.AreEqual(0, properties.Count); + } + + /// + /// Test dictionary serialization with properties + /// + [TestMethod] + public void PropertyDictionarySerialization() + { + PropertyDictionary properties = new PropertyDictionary(); + + ProjectPropertyInstance p1 = GetPropertyInstance("p1", "v1"); + ProjectPropertyInstance p2 = GetPropertyInstance("p2", "v2"); + ProjectPropertyInstance p3 = GetPropertyInstance("p1", "v1"); + ProjectPropertyInstance p4 = GetPropertyInstance("p2", "v3"); + + properties.Set(p1); + properties.Set(p2); + properties.Set(p3); + properties.Set(p1); + properties.Set(p4); + + TranslationHelpers.GetWriteTranslator().TranslateDictionary, ProjectPropertyInstance>(ref properties, ProjectPropertyInstance.FactoryForDeserialization); + PropertyDictionary deserializedProperties = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary, ProjectPropertyInstance>(ref deserializedProperties, ProjectPropertyInstance.FactoryForDeserialization); + + Assert.AreEqual(properties.PropertyNames.Count, deserializedProperties.PropertyNames.Count); + foreach (string propertyName in properties.PropertyNames) + { + Assert.AreEqual(properties[propertyName].EvaluatedValue, deserializedProperties[propertyName].EvaluatedValue); + } + } + + /// + /// Test dictionary serialization with no properties + /// + [TestMethod] + public void PropertyDictionarySerializationEmpty() + { + PropertyDictionary properties = new PropertyDictionary(); + + TranslationHelpers.GetWriteTranslator().TranslateDictionary, ProjectPropertyInstance>(ref properties, ProjectPropertyInstance.FactoryForDeserialization); + PropertyDictionary deserializedProperties = null; + TranslationHelpers.GetReadTranslator().TranslateDictionary, ProjectPropertyInstance>(ref deserializedProperties, ProjectPropertyInstance.FactoryForDeserialization); + + Assert.AreEqual(properties.PropertyNames.Count, deserializedProperties.PropertyNames.Count); + } + + /// + /// End to end test of ItemDictionary + /// + [TestMethod] + public void BasicItemDictionary() + { + ItemDictionary items = new ItemDictionary(); + + // Clearing empty collection + items.Clear(); + + // Enumeration of empty collection + using (IEnumerator enumerator = items.GetEnumerator()) + { + Assert.AreEqual(false, enumerator.MoveNext()); + ObjectModelHelpers.AssertThrows(typeof(InvalidOperationException), delegate { object o = ((IEnumerator)enumerator).Current; }); + Assert.AreEqual(null, enumerator.Current); + } + + List list = new List(); + foreach (ProjectItemInstance item in items) + { + list.Add(item); + } + + Assert.AreEqual(0, list.Count); + + // Cause an empty list for type 'x' to be added + ICollection itemList = items["x"]; + + // Enumerate empty collection, with an empty list in it + foreach (ProjectItemInstance item in items) + { + list.Add(item); + } + + Assert.AreEqual(0, list.Count); + + // Add and remove some items + ProjectItemInstance item1 = GetItemInstance("i", "i1"); + Assert.AreEqual(false, items.Remove(item1)); + Assert.AreEqual(0, items["j"].Count); + + items.Add(item1); + Assert.AreEqual(1, items["i"].Count); + Assert.AreEqual(item1, items["i"].First()); + + ProjectItemInstance item2 = GetItemInstance("i", "i2"); + items.Add(item2); + ProjectItemInstance item3 = GetItemInstance("j", "j1"); + items.Add(item3); + + // Enumerate to verify contents + list = new List(); + foreach (ProjectItemInstance item in items) + { + list.Add(item); + } + + list.Sort(ProjectItemInstanceComparer); + Assert.AreEqual(item1, list[0]); + Assert.AreEqual(item2, list[1]); + Assert.AreEqual(item3, list[2]); + + // Direct operations on the enumerator + using (IEnumerator enumerator = items.GetEnumerator()) + { + Assert.AreEqual(null, enumerator.Current); + Assert.AreEqual(true, enumerator.MoveNext()); + Assert.IsNotNull(enumerator.Current); + enumerator.Reset(); + Assert.AreEqual(null, enumerator.Current); + Assert.AreEqual(true, enumerator.MoveNext()); + Assert.IsNotNull(enumerator.Current); + } + } + + /// + /// Null backing collection should be like empty collection + /// + [TestMethod] + public void ReadOnlyDictionaryNullBackingClone() + { + var dictionary = CreateCloneDictionary(null, StringComparer.OrdinalIgnoreCase); + Assert.AreEqual(0, dictionary.Count); + } + + /// + /// Null backing collection should be like empty collection + /// + [TestMethod] + public void ReadOnlyDictionaryNullBackingWrapper() + { + var dictionary = new ObjectModel.ReadOnlyDictionary(new Dictionary(0)); + Assert.AreEqual(0, dictionary.Count); + } + + /// + /// Cloning constructor should not see subsequent changes + /// + [TestMethod] + public void ReadOnlyDictionaryClone() + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + dictionary.Add("p", "v"); + + var readOnlyDictionary = CreateCloneDictionary(dictionary, StringComparer.OrdinalIgnoreCase); + dictionary.Add("p2", "v2"); + + Assert.AreEqual(1, readOnlyDictionary.Count); + Assert.AreEqual(true, readOnlyDictionary.ContainsKey("P")); + Assert.AreEqual(false, readOnlyDictionary.ContainsKey("p2")); + } + + /// + /// Wrapping constructor should be "live" + /// + [TestMethod] + public void ReadOnlyDictionaryWrapper() + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + dictionary.Add("p", "v"); + + var readOnlyDictionary = new ObjectModel.ReadOnlyDictionary(dictionary); + dictionary.Add("p2", "v2"); + + Assert.AreEqual(2, dictionary.Count); + Assert.AreEqual(true, dictionary.ContainsKey("p2")); + } + + /// + /// Null backing collection should be an error + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ReadOnlyCollectionNullBacking() + { + new ReadOnlyCollection(null); + } + + /// + /// Verify non generic enumeration does not recurse + /// ie., GetEnumerator() does not call itself + /// + [TestMethod] + public void ReadOnlyDictionaryNonGenericEnumeration() + { + var backing = new Dictionary(); + var collection = new ObjectModel.ReadOnlyDictionary(backing); + IEnumerable enumerable = (IEnumerable)collection; + + // Does not overflow stack: + foreach (object o in enumerable) + { + } + } + + /// + /// Verify that the converting collection functions. + /// + [TestMethod] + public void ReadOnlyConvertingCollection() + { + string[] source = { "1", "2", "3" }; + ReadOnlyConvertingCollection convertingCollection = new ReadOnlyConvertingCollection(source, delegate (string x) { return Convert.ToInt32(x); }); + Assert.AreEqual(3, convertingCollection.Count); + Assert.IsTrue(convertingCollection.IsReadOnly); + + int index = 1; + foreach (int value in convertingCollection) + { + Assert.AreEqual(index++, value); + } + } + + /// + /// Verify that the converting dictionary functions. + /// + [TestMethod] + public void ReadOnlyConvertingDictionary() + { + Dictionary values = new Dictionary(); + values["one"] = "1"; + values["two"] = "2"; + values["three"] = "3"; + + Dictionary convertedValues = new Dictionary(); + convertedValues["one"] = 1; + convertedValues["two"] = 2; + convertedValues["three"] = 3; + + ReadOnlyConvertingDictionary convertingCollection = new ReadOnlyConvertingDictionary(values, delegate (string x) { return Convert.ToInt32(x); }); + Assert.AreEqual(3, convertingCollection.Count); + Assert.IsTrue(convertingCollection.IsReadOnly); + + foreach (KeyValuePair value in convertingCollection) + { + Assert.AreEqual(convertedValues[value.Key], value.Value); + } + } + + /// + /// Verify non generic enumeration does not recurse + /// ie., GetEnumerator() does not call itself + /// + [TestMethod] + public void ReadOnlyCollectionNonGenericEnumeration() + { + var backing = new List(); + var collection = new ReadOnlyCollection(backing); + IEnumerable enumerable = (IEnumerable)collection; + + // Does not overflow stack: + foreach (object o in enumerable) + { + } + } + + /// + /// Helper to make a ProjectPropertyInstance. + /// + private static ProjectPropertyInstance GetPropertyInstance(string name, string value) + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectPropertyInstance property = projectInstance.SetProperty(name, value); + + return property; + } + + /// + /// Helper to make a ProjectItemInstance. + /// + private static ProjectItemInstance GetItemInstance(string itemType, string evaluatedInclude) + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectItemInstance item = projectInstance.AddItem(itemType, evaluatedInclude); + + return item; + } + + /// + /// Creates a copy of a dictionary and returns a read-only dictionary around the results. + /// + /// The value stored in the dictionary + /// Dictionary to clone. + private static ObjectModel.ReadOnlyDictionary CreateCloneDictionary(IDictionary dictionary, StringComparer strComparer) + { + Dictionary clone; + if (dictionary == null) + { + clone = new Dictionary(0); + } + else + { + clone = new Dictionary(dictionary, strComparer); + } + + return new ObjectModel.ReadOnlyDictionary(clone); + } + + /// + /// Simple comparer for ProjectItemInstances. Ought to compare metadata etc. + /// + private int ProjectItemInstanceComparer(ProjectItemInstance one, ProjectItemInstance two) + { + return String.Compare(one.EvaluatedInclude, two.EvaluatedInclude); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Collections/WeakDictionary_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/WeakDictionary_Tests.cs new file mode 100644 index 00000000000..2767f5969b4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/WeakDictionary_Tests.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the weak dictionary class +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using System.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for the weak dictionary class + /// + [TestClass] + public class WeakDictionary_Tests + { + /// + /// Find with the same key inserted using the indexer + /// + [TestMethod] + public void Indexer_ReferenceFound() + { + object k1 = new Object(); + object v1 = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + object v2 = dictionary[k1]; + + Assert.AreEqual(true, Object.ReferenceEquals(v1, v2)); + Assert.AreEqual(true, dictionary.Contains(k1)); + } + + /// + /// Find something not present with the indexer + /// + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void Indexer_NotFound() + { + var dictionary = new WeakDictionary(); + object value = dictionary[new Object()]; + } + + /// + /// Find with the same key inserted using TryGetValue + /// + [TestMethod] + public void TryGetValue_ReferenceFound() + { + object k1 = new Object(); + object v1 = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + object v2; + bool result = dictionary.TryGetValue(k1, out v2); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// Find something not present with TryGetValue + /// + [TestMethod] + public void TryGetValue_ReferenceNotFound() + { + var dictionary = new WeakDictionary(); + + object v; + bool result = dictionary.TryGetValue(new Object(), out v); + + Assert.AreEqual(false, result); + Assert.AreEqual(null, v); + Assert.AreEqual(false, dictionary.Contains(new Object())); + } + + /// + /// Find a key that wasn't inserted but is equal + /// + [TestMethod] + public void EqualityComparer() + { + string k1 = String.Concat("ke", "y"); + object v1 = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k1] = v1; + + // Now look for a different but equatable key + // Don't create it with a literal or the compiler will intern it! + string k2 = String.Concat("k", "ey"); + + Assert.AreEqual(false, Object.ReferenceEquals(k1, k2)); + + object v2 = dictionary[k2]; + + Assert.AreEqual(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// If value target has been collected, key should not be present. + /// (When accessed, if target is null, entry is removed instead of returned.) + /// + [TestMethod] + [ExpectedException(typeof(KeyNotFoundException))] + public void IndexerRemovesDeadValue() + { + object k = new Object(); + object v = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k] = v; + + v = null; + GC.Collect(); + + object value = dictionary[k]; // throws + } + + /// + /// If value target has been collected, key should not be present. + /// (When accessed, if target is null, entry is removed instead of returned.) + /// + [TestMethod] + public void ContainsRemovesDeadValue() + { + Console.WriteLine("Fixed contains test .."); + + Object k = new Object(); + object v = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k] = v; + + // Do not put an assert here! It will cause the test to mysteriously fail + // as somehow an NUnit Assert can hold onto the value! + v = null; + GC.Collect(); + + Assert.AreEqual(false, dictionary.Contains(k)); + } + + /// + /// If value target has been collected, key should not be present. + /// (When accessed, if target is null, entry is removed instead of returned.) + /// + [TestMethod] + public void TryGetRemovesDeadValue() + { + object k = new Object(); + object v = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k] = v; + + // Do not put an assert here! It will cause the test to mysteriously fail + // as somehow an NUnit Assert can hold onto the value! + v = null; + GC.Collect(); + + object value; + Assert.AreEqual(false, dictionary.TryGetValue(k, out value)); + Assert.AreEqual(0, dictionary.Count); + } + + /// + /// Verify dictionary doesn't hold onto keys + /// + [TestMethod] + public void KeysCollectable() + { + string k1 = new string('k', 1000000); + string v1 = new string('v', 1000000); + + // Each character is 2 bytes, so about 4MB of this should be the strings + long memory1 = GC.GetTotalMemory(true); + + var dictionary = new WeakDictionary(); + dictionary[k1] = v1; + + k1 = null; + + long memory2 = GC.GetTotalMemory(true); + + // Key collected, sould be about 2MB less + long difference = memory1 - memory2; + + Console.WriteLine("Start {0}, end {1}, diff {2}", memory1, memory2, difference); + Assert.AreEqual(true, difference > 1500000); // 2MB minus big noise allowance + + // This line is VERY important, as it keeps the GC from being too smart and collecting + // the dictionary and its large strings because we never use them again. + GC.KeepAlive(dictionary); + } + + /// + /// Verify dictionary doesn't hold onto values + /// + [TestMethod] + public void ValuesCollectable() + { + string k1 = new string('k', 1000000); + string v1 = new string('v', 1000000); + + // Each character is 2 bytes, so about 4MB of this should be the strings + long memory1 = GC.GetTotalMemory(true); + + var dictionary = new WeakDictionary(); + dictionary[k1] = v1; + + v1 = null; + + long memory2 = GC.GetTotalMemory(true); + + // Value collected, sould be about 2MB less + long difference = memory1 - memory2; + + Console.WriteLine("Start {0}, end {1}, diff {2}", memory1, memory2, difference); + Assert.AreEqual(true, difference > 1500000); // 2MB minus big noise allowance + + // This line is VERY important, as it keeps the GC from being too smart and collecting + // the dictionary and its large strings because we never use them again. + GC.KeepAlive(dictionary); + } + + /// + /// Call Scavenge explicitly + /// + [TestMethod] + public void ExplicitScavenge() + { + object k1 = new Object(); + object v1 = new Object(); + + var dictionary = new WeakDictionary(); + dictionary[k1] = v1; + + Assert.AreEqual(1, dictionary.Count); + + k1 = null; + GC.Collect(); + + dictionary.Scavenge(); + + Assert.AreEqual(0, dictionary.Count); + } + + /// + /// Growing should invoke Scavenge + /// + [TestMethod] + public void ScavengeOnGrow() + { + var dictionary = new WeakDictionary(); + + for (int i = 0; i < 100; i++) + { + dictionary[new Object()] = new Object(); + + // Randomly collect some + if (i == 15) + { + GC.Collect(); + } + } + + // We should have scavenged at least once + Console.WriteLine("Count {0}", dictionary.Count); + Assert.AreEqual(true, dictionary.Count < 100); + + // Finish with explicit scavenge + int count1 = dictionary.Count; + int removed = dictionary.Scavenge(); + int count2 = dictionary.Count; + + Console.WriteLine("Removed {0}", removed); + Assert.AreEqual(removed, count1 - count2); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/Collections/WeakValueDictionary_Tests.cs b/src/XMakeBuildEngine/UnitTests/Collections/WeakValueDictionary_Tests.cs new file mode 100644 index 00000000000..a00e367735a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Collections/WeakValueDictionary_Tests.cs @@ -0,0 +1,278 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the weak value dictionary class +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.OM.Collections +{ + /// + /// Tests for the weak value dictionary class + /// + [TestFixture] + public class WeakValueDictionaryTests + { + /// + /// Magic number size of strings to allocate for GC tests. + /// + private const int BigMemoryFootprintTest = 1 * 1024 * 1024; + + /// + /// Find with the same key inserted using the indexer + /// + [Test] + public void Indexer_ReferenceFound() + { + string k1 = "key"; + string v1 = "value"; + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + string v2 = dictionary[k1]; + + Assertion.AssertEquals(true, Object.ReferenceEquals(v1, v2)); + Assertion.AssertEquals(true, dictionary.Contains(k1)); + } + + /// + /// Find where the value is truly null + /// + [Test] + public void Indexer_NullValue_ReferenceFound() + { + string k1 = "key"; + string v1 = null; + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + string v2 = dictionary[k1]; + + Assertion.AssertEquals(true, Object.ReferenceEquals(v1, v2)); + Assertion.AssertEquals(true, dictionary.Contains(k1)); + + // Should not scavenge values that are null, rather than collected + dictionary.Scavenge(); + Assertion.AssertEquals(1, dictionary.Count); + } + + /// + /// Find something not present with the indexer + /// + [Test] + [ExpectedException(typeof(KeyNotFoundException))] + public void Indexer_NotFound() + { + var dictionary = new WeakValueDictionary(); + string value = dictionary["x"]; + } + + /// + /// Find with the same key inserted using TryGetValue + /// + [Test] + public void TryGetValue_ReferenceFound() + { + string k1 = "key"; + string v1 = "value"; + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + string v2; + bool result = dictionary.TryGetValue(k1, out v2); + + Assertion.AssertEquals(true, result); + Assertion.AssertEquals(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// Find true null value + /// + [Test] + public void TryGetNullValue_ReferenceFound() + { + string k1 = "key"; + string v1 = null; + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + // Now look for the same key we inserted + string v2; + bool result = dictionary.TryGetValue(k1, out v2); + + Assertion.AssertEquals(true, result); + Assertion.AssertEquals(true, Object.ReferenceEquals(v1, v2)); + + // Should not scavenge values that are null, rather than collected + dictionary.Scavenge(); + Assertion.AssertEquals(1, dictionary.Count); + } + + /// + /// Find something not present with TryGetValue + /// + [Test] + public void TryGetValue_ReferenceNotFound() + { + var dictionary = new WeakValueDictionary(); + + string v; + bool result = dictionary.TryGetValue("x", out v); + + Assert.IsFalse(result); + Assert.IsNull(v); + Assert.IsFalse(dictionary.Contains("x")); + } + + /// + /// Find a key that wasn't inserted but is equal + /// + [Test] + public void EqualityComparer() + { + string k1 = "key"; + string v1 = "value"; + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + // Now look for a different but equatable key + // Don't create it with a literal or the compiler will intern it! + string k2 = String.Concat("k", "ey"); + + Assert.IsFalse(Object.ReferenceEquals(k1, k2)); + + string v2 = dictionary[k2]; + + Assertion.AssertEquals(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// Find a key that wasn't inserted but is equal + /// + [Test] + public void SpecifiedEqualityComparer() + { + string k1 = "key"; + string v1 = "value"; + + var dictionary = new WeakValueDictionary(StringComparer.OrdinalIgnoreCase); + dictionary[k1] = v1; + + string v2 = dictionary["KEY"]; + + Assertion.AssertEquals(true, Object.ReferenceEquals(v1, v2)); + } + + /// + /// Verify dictionary holds onto keys, but not values. + /// + [Test] + public void OnlyValuesCollectable() + { + long memory0 = GC.GetTotalMemory(true); + + string k1 = new string('k', BigMemoryFootprintTest); + string v1 = new string('v', BigMemoryFootprintTest); + + // Each character is 2 bytes, so about 4MB of this should be the strings + long memory1 = GC.GetTotalMemory(true); + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + k1 = null; + + long memory2 = GC.GetTotalMemory(true); + + // Key not collected, should be about the same + long difference = memory1 - memory2; + + Console.WriteLine("Before {0} Start {1}, end {2}, diff {3}, {4} more than start", memory0, memory1, memory2, difference, memory2 - memory0); + Assertion.AssertEquals(true, difference < 500000); // big noise allowance + + v1 = null; + + memory2 = GC.GetTotalMemory(true); + + // Value collected, should be about 2MB less + difference = memory1 - memory2; + + Console.WriteLine("Before {0} Start {1}, end {2}, diff {3}, {4} more than start", memory0, memory1, memory2, difference, memory2 - memory0); + Assertion.AssertEquals(true, difference > 1500000); // 2MB minus big noise allowance + + // This line is VERY important, as it keeps the GC from being too smart and collecting + // the dictionary and its large strings because we never use them again. + GC.KeepAlive(dictionary); + } + + /// + /// Call Scavenge explicitly + /// + [Test] + public void ExplicitScavenge() + { + object k1 = new object(); + object v1 = new object(); + + var dictionary = new WeakValueDictionary(); + dictionary[k1] = v1; + + Assertion.AssertEquals(1, dictionary.Count); + + v1 = null; + GC.Collect(); + + dictionary.Scavenge(); + + Assertion.AssertEquals(0, dictionary.Count); + } + + /// + /// Growing should invoke Scavenge + /// + [Test] + public void ScavengeOnGrow() + { + var dictionary = new WeakValueDictionary(); + + for (int i = 0; i < 100; i++) + { + dictionary[new Object()] = new Object(); + + // Randomly collect some + if (i == 15) + { + GC.Collect(); + } + } + + // We should have scavenged at least once + Console.WriteLine("Count {0}", dictionary.Count); + Assertion.AssertEquals(true, dictionary.Count < 100); + + // Finish with explicit scavenge + int count1 = dictionary.Count; + int removed = dictionary.Scavenge(); + int count2 = dictionary.Count; + + Console.WriteLine("Removed {0}", removed); + Assertion.AssertEquals(removed, count1 - count2); + } + } +} + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/ConfigureableForwardingLogger_Tests.cs b/src/XMakeBuildEngine/UnitTests/ConfigureableForwardingLogger_Tests.cs new file mode 100644 index 00000000000..82c0595c8b0 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/ConfigureableForwardingLogger_Tests.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.BackEnd.Logging; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ConfigureableForwardingLogger_Tests + { + private static BuildFinishedEventArgs s_buildFinished = new BuildFinishedEventArgs("Message", "Keyword", true); + private static BuildStartedEventArgs s_buildStarted = new BuildStartedEventArgs("Message", "Help"); + private static BuildMessageEventArgs s_lowMessage = new BuildMessageEventArgs("Message", "help", "sender", MessageImportance.Low); + private static BuildMessageEventArgs s_normalMessage = new BuildMessageEventArgs("Message2", "help", "sender", MessageImportance.Normal); + private static BuildMessageEventArgs s_highMessage = new BuildMessageEventArgs("Message3", "help", "sender", MessageImportance.High); + private static TaskStartedEventArgs s_taskStarted = new TaskStartedEventArgs("message", "help", "projectFile", "taskFile", "taskName"); + private static TaskFinishedEventArgs s_taskFinished = new TaskFinishedEventArgs("message", "help", "projectFile", "taskFile", "taskName", true); + private static TaskCommandLineEventArgs s_commandLine = new TaskCommandLineEventArgs("commandLine", "taskName", MessageImportance.Low); + private static BuildWarningEventArgs s_warning = new BuildWarningEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + private static BuildErrorEventArgs s_error = new BuildErrorEventArgs("SubCategoryForSchemaValidationErrors", "MSB4000", "file", 1, 2, 3, 4, "message", "help", "sender"); + private static TargetStartedEventArgs s_targetStarted = new TargetStartedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile"); + private static TargetFinishedEventArgs s_targetFinished = new TargetFinishedEventArgs("message", "help", "targetName", "ProjectFile", "targetFile", true); + private static ProjectStartedEventArgs s_projectStarted = new ProjectStartedEventArgs(-1, "message", "help", "ProjectFile", "targetNames", null, null, null); + private static ProjectFinishedEventArgs s_projectFinished = new ProjectFinishedEventArgs("message", "help", "ProjectFile", true); + private static ExternalProjectStartedEventArgs s_externalStartedEvent = new ExternalProjectStartedEventArgs("message", "help", "senderName", "projectFile", "targetNames"); + + internal class TestForwardingLogger : ConfigurableForwardingLogger + { + internal TestForwardingLogger() + { + forwardedEvents = new List(); + } + internal List forwardedEvents; + protected override void ForwardToCentralLogger(BuildEventArgs e) + { + forwardedEvents.Add(e); + } + } + + [ClassInitialize] + public static void FixtureSetup(TestContext testContext) + { + BuildEventContext context = new BuildEventContext(1, 2, 3, 4); + s_error.BuildEventContext = context; + s_warning.BuildEventContext = context; + s_targetStarted.BuildEventContext = context; + s_targetFinished.BuildEventContext = context; + } + + [TestMethod] + public void ForwardingLoggingEventsBasedOnVerbosity() + { + EventSourceSink source = new EventSourceSink(); + TestForwardingLogger logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Parameters = "BUILDSTARTEDEVENT"; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 1); + + logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Quiet; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 4); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + + logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Minimal; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 5); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_highMessage)); + + logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Normal; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 11); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_highMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_normalMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_commandLine)); + + logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Detailed; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 14); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_highMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_lowMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_normalMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_taskStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_taskFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_commandLine)); + + logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Diagnostic; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 15); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_highMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_lowMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_normalMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_taskStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_taskFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_externalStartedEvent)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_commandLine)); + } + + [TestMethod] + public void ForwardingLoggingPerformanceSummary() + { + EventSourceSink source = new EventSourceSink(); + TestForwardingLogger logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Parameters = "PERFORMANCESUMMARY"; + logger.Verbosity = LoggerVerbosity.Quiet; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 10); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_taskStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_taskFinished)); + } + + [TestMethod] + public void ForwardingLoggingNoSummary() + { + EventSourceSink source = new EventSourceSink(); + TestForwardingLogger logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Normal; + logger.Parameters = "NOSUMMARY"; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 11); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_highMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_normalMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_commandLine)); + } + + [TestMethod] + public void ForwardingLoggingShowCommandLine() + { + EventSourceSink source = new EventSourceSink(); + TestForwardingLogger logger = new TestForwardingLogger(); + logger.BuildEventRedirector = null; + logger.Verbosity = LoggerVerbosity.Normal; + logger.Parameters = "SHOWCOMMANDLINE"; + logger.Initialize(source, 4); + RaiseEvents(source); + Assert.IsTrue(logger.forwardedEvents.Count == 11); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_buildFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_error)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_warning)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_highMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_normalMessage)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_projectFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetStarted)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_targetFinished)); + Assert.IsTrue(logger.forwardedEvents.Contains(s_commandLine)); + } + + private void RaiseEvents(EventSourceSink source) + { + source.Consume(s_buildStarted); + source.Consume(s_projectStarted); + source.Consume(s_targetStarted); + source.Consume(s_taskStarted); + source.Consume(s_lowMessage); + source.Consume(s_normalMessage); + source.Consume(s_highMessage); + source.Consume(s_commandLine); + source.Consume(s_externalStartedEvent); + source.Consume(s_warning); + source.Consume(s_error); + source.Consume(s_taskFinished); + source.Consume(s_targetFinished); + source.Consume(s_projectFinished); + source.Consume(s_buildFinished); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/ConsoleLogger_Tests.cs b/src/XMakeBuildEngine/UnitTests/ConsoleLogger_Tests.cs new file mode 100644 index 00000000000..c027ad5db8a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/ConsoleLogger_Tests.cs @@ -0,0 +1,2589 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ConsoleLoggerTest + { + /// + /// For the environment writing test + /// + private Dictionary _environment; + + private static string s_dummyProjectContents = @" + + + + + + + + + + "; + + + private class SimulatedConsole + { + private StringBuilder _simulatedConsole; + + internal SimulatedConsole() + { + _simulatedConsole = new StringBuilder(); + } + + internal void Clear() + { + _simulatedConsole = new StringBuilder(); + } + + public override string ToString() + { + return _simulatedConsole.ToString(); + } + + internal void Write(string s) + { + _simulatedConsole.Append(s); + } + + internal void WriteLine(string s) + { + Write(s); + Write(Environment.NewLine); + } + + internal void SetColor(ConsoleColor c) + { + switch (c) + { + case ConsoleColor.Red: + _simulatedConsole.Append(""); + break; + + case ConsoleColor.Yellow: + _simulatedConsole.Append(""); + break; + + case ConsoleColor.Cyan: + _simulatedConsole.Append(""); + break; + + case ConsoleColor.DarkGray: + _simulatedConsole.Append(""); + break; + + case ConsoleColor.Green: + _simulatedConsole.Append(""); + break; + + default: + _simulatedConsole.Append(""); + break; + } + } + + internal void ResetColor() + { + _simulatedConsole.Append(""); + } + + public static implicit operator string (SimulatedConsole sc) + { + return sc.ToString(); + } + } + + private static void SingleMessageTest(LoggerVerbosity v, MessageImportance j, bool shouldPrint) + { + for (int i = 1; i <= 2; i++) + { + SimulatedConsole sc = new SimulatedConsole(); + EventSourceSink es = new EventSourceSink(); + ConsoleLogger L = new ConsoleLogger(v, + sc.Write, null, null); + L.Initialize(es, i); + string msg = "my 1337 message"; + + BuildMessageEventArgs be = new BuildMessageEventArgs(msg, "help", "sender", j); + be.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + es.Consume(be); + + if (i == 2 && v == LoggerVerbosity.Diagnostic) + { + string context = ResourceUtilities.FormatResourceString("BuildEventContext", LogFormatter.FormatLogTimeStamp(be.Timestamp), 0) + ">"; + msg = context + ResourceUtilities.FormatResourceString("TaskMessageWithId", "my 1337 message", be.BuildEventContext.TaskId); + } + else if (i == 2 && v == LoggerVerbosity.Detailed) + { + string context = ResourceUtilities.FormatResourceString("BuildEventContext", string.Empty, 0) + ">"; + msg = context + "my 1337 message"; + } + else if (i == 2) + { + msg = " " + msg; + } + + Assert.AreEqual(shouldPrint ? msg + Environment.NewLine : String.Empty, sc.ToString()); + } + } + + private sealed class MyCustomBuildEventArgs : CustomBuildEventArgs + { + internal MyCustomBuildEventArgs() + : base() + { + // do nothing + } + + internal MyCustomBuildEventArgs(string message) + : base(message, null, null) + { + // do nothing + } + } + + private class MyCustomBuildEventArgs2 : CustomBuildEventArgs { } + + [TestInitialize] + public void SuiteSetup() + { + _environment = new Dictionary(); + + foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) + { + _environment.Add((string)entry.Key, (string)entry.Value); + } + } + + + /// + /// Verify when the project has not been named that we correctly get the same placeholder + /// project name for for project started event and the target started event. + /// Test for BUG 579935 + /// + [TestMethod] + public void TestEmptyProjectNameForTargetStarted() + { + Microsoft.Build.Evaluation.Project project = new Microsoft.Build.Evaluation.Project(); + + ProjectTargetElement target = project.Xml.AddTarget("T"); + ProjectTaskElement task = target.AddTask("Message"); + + System.Xml.XmlAttribute attribute = task.XmlDocument.CreateAttribute("Text"); + attribute.Value = "HELLO"; + + attribute = task.XmlDocument.CreateAttribute("MessageImportance"); + attribute.Value = "High"; + + MockLogger mockLogger = new MockLogger(); + List loggerList = new List(); + loggerList.Add(mockLogger); + project.Build(loggerList); + + List projectStartedEvents = mockLogger.ProjectStartedEvents; + Assert.IsTrue(projectStartedEvents.Count == 1); + string projectStartedName = projectStartedEvents[0].ProjectFile; + Assert.IsFalse(String.IsNullOrEmpty(projectStartedName), "Expected project started name to not be null or empty"); + + List targetStartedEvents = mockLogger.TargetStartedEvents; + Assert.IsTrue(targetStartedEvents.Count == 1); + Assert.IsTrue(projectStartedName.Equals(targetStartedEvents[0].ProjectFile, StringComparison.OrdinalIgnoreCase), "Expected the project started and target started target names to match"); + } + + + /// + /// Make sure the first message after a project started event prints out the target name. This was annoying a lot of people when there were messages right after the project + /// started event but there was no target printed out. + /// + [TestMethod] + public void TestTargetAfterProjectStarted() + { + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + string log = sc.ToString(); + Assert.IsTrue(log.IndexOf("XXX:", StringComparison.OrdinalIgnoreCase) != -1); + } + + /// + /// Verify that on minimal verbosity the console logger does not log the target names. + /// + [TestMethod] + public void TestNoTargetNameOnMinimal() + { + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger logger = new ConsoleLogger(LoggerVerbosity.Minimal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + string log = sc.ToString(); + Assert.IsTrue(log.IndexOf("XXX:", StringComparison.OrdinalIgnoreCase) == -1); + Assert.IsTrue(log.IndexOf("YYY:", StringComparison.OrdinalIgnoreCase) == -1); + Assert.IsTrue(log.IndexOf("GGG:", StringComparison.OrdinalIgnoreCase) == -1); + } + + /// + /// Make sure if a target has no messages logged that its started and finished events show up on detailed but not normal. + /// + [TestMethod] + public void EmptyTargetsOnDetailedButNotNotmal() + { + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + string log = sc.ToString(); + Assert.IsTrue(log.IndexOf("YYY:", StringComparison.OrdinalIgnoreCase) == -1); + + sc = new SimulatedConsole(); + logger = new ConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + logger.Parameters = "EnableMPLogging"; + string tempProjectDir = Path.Combine(Path.GetTempPath(), "EmptyTargetsOnDetailedButNotNotmal"); + string tempProjectPath = Path.Combine(tempProjectDir, "test.proj"); + + try + { + if (FileUtilities.DirectoryExistsNoThrow(tempProjectDir)) + { + FileUtilities.DeleteDirectoryNoThrow(tempProjectDir, true); + } + + Directory.CreateDirectory(tempProjectDir); + File.WriteAllText(tempProjectPath, s_dummyProjectContents); + + ObjectModelHelpers.BuildTempProjectFileWithTargets(tempProjectPath, null, null, logger); + + log = sc.ToString(); + string targetStartedMessage = ResourceUtilities.FormatResourceString("TargetStartedProjectEntry", "YYY", tempProjectPath); + + // it's a console, so it cuts off, so only look for the existence of the first bit (which should contains the "YYY") + targetStartedMessage = targetStartedMessage.Substring(0, 60); + Assert.IsTrue(log.IndexOf(targetStartedMessage, StringComparison.OrdinalIgnoreCase) != -1); + } + finally + { + if (FileUtilities.DirectoryExistsNoThrow(tempProjectDir)) + { + FileUtilities.DeleteDirectoryNoThrow(tempProjectDir, true); + } + } + } + + /// + /// Test a number of cases where difference values from showcommandline are used with normal verbosity + /// + [TestMethod] + public void ShowCommandLineWithNormalVerbosity() + { + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging;ShowCommandLine"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + string log = sc.ToString(); + Assert.IsTrue(log.IndexOf("where.exe where", StringComparison.OrdinalIgnoreCase) != -1); + + sc = new SimulatedConsole(); + logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging;ShowCommandLine=true"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + log = sc.ToString(); + Assert.IsTrue(log.IndexOf("where.exe where", StringComparison.OrdinalIgnoreCase) != -1); + + sc = new SimulatedConsole(); + logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging;ShowCommandLine=false"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + log = sc.ToString(); + Assert.IsTrue(log.IndexOf("where.exe where", StringComparison.OrdinalIgnoreCase) == -1); + + sc = new SimulatedConsole(); + logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging;ShowCommandLine=NotAbool"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + log = sc.ToString(); + Assert.IsTrue(log.IndexOf("where.exe where", StringComparison.OrdinalIgnoreCase) == -1); + + sc = new SimulatedConsole(); + logger = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + logger.Parameters = "EnableMPLogging"; + ObjectModelHelpers.BuildProjectExpectSuccess(s_dummyProjectContents, logger); + + log = sc.ToString(); + Assert.IsTrue(log.IndexOf("where.exe where", StringComparison.OrdinalIgnoreCase) != -1); + } + + /// + /// We should not crash when given a null message, etc. + /// + [TestMethod] + public void NullEventFields() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Diagnostic, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es); + + // Not all parameters are null here, but that's fine, we assume the engine will never + // fire a ProjectStarted without a project name, etc. + es.Consume(new BuildStartedEventArgs(null, null)); + es.Consume(new ProjectStartedEventArgs(null, null, "p", null, null, null)); + es.Consume(new TargetStartedEventArgs(null, null, "t", null, null)); + es.Consume(new TaskStartedEventArgs(null, null, null, null, "task")); + es.Consume(new BuildMessageEventArgs(null, null, null, MessageImportance.High)); + es.Consume(new BuildWarningEventArgs(null, null, null, 0, 0, 0, 0, null, null, null)); + es.Consume(new BuildErrorEventArgs(null, null, null, 0, 0, 0, 0, null, null, null)); + es.Consume(new TaskFinishedEventArgs(null, null, null, null, "task", true)); + es.Consume(new TargetFinishedEventArgs(null, null, "t", null, null, true)); + es.Consume(new ProjectFinishedEventArgs(null, null, "p", true)); + es.Consume(new BuildFinishedEventArgs(null, null, true)); + es.Consume(new BuildFinishedEventArgs(null, null, true)); + es.Consume(new BuildFinishedEventArgs(null, null, true)); + es.Consume(new MyCustomBuildEventArgs2()); + // No exception raised + } + + [TestMethod] + public void NullEventFieldsParallel() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Diagnostic, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es, 2); + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs(null, null); + bse.BuildEventContext = buildEventContext; + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(-1, null, null, "p", null, null, null, buildEventContext); + pse.BuildEventContext = buildEventContext; + TargetStartedEventArgs trse = new TargetStartedEventArgs(null, null, "t", null, null); + trse.BuildEventContext = buildEventContext; + TaskStartedEventArgs tase = new TaskStartedEventArgs(null, null, null, null, "task"); + tase.BuildEventContext = buildEventContext; + BuildMessageEventArgs bmea = new BuildMessageEventArgs(null, null, null, MessageImportance.High); + bmea.BuildEventContext = buildEventContext; + BuildWarningEventArgs bwea = new BuildWarningEventArgs(null, null, null, 0, 0, 0, 0, null, null, null); + bwea.BuildEventContext = buildEventContext; + BuildErrorEventArgs beea = new BuildErrorEventArgs(null, null, null, 0, 0, 0, 0, null, null, null); + beea.BuildEventContext = buildEventContext; + TaskFinishedEventArgs trfea = new TaskFinishedEventArgs(null, null, null, null, "task", true); + trfea.BuildEventContext = buildEventContext; + TargetFinishedEventArgs tafea = new TargetFinishedEventArgs(null, null, "t", null, null, true); + tafea.BuildEventContext = buildEventContext; + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs(null, null, "p", true); + pfea.BuildEventContext = buildEventContext; + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs(null, null, true); + bfea.BuildEventContext = buildEventContext; + MyCustomBuildEventArgs2 mcea = new MyCustomBuildEventArgs2(); + mcea.BuildEventContext = buildEventContext; + + + // Not all parameters are null here, but that's fine, we assume the engine will never + // fire a ProjectStarted without a project name, etc. + es.Consume(bse); + es.Consume(pse); + es.Consume(trse); + es.Consume(tase); + es.Consume(bmea); + es.Consume(bwea); + es.Consume(beea); + es.Consume(trfea); + es.Consume(tafea); + es.Consume(pfea); + es.Consume(bfea); + es.Consume(bfea); + es.Consume(bfea); + es.Consume(mcea); + // No exception raised + } + + [TestMethod] + public void TestVerbosityLessThan() + { + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new SerialConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(true, + (new SerialConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Quiet)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Minimal)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Normal)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(false, + (new ParallelConsoleLogger(LoggerVerbosity.Detailed)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Quiet)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Minimal)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Normal)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Detailed)); + Assert.AreEqual(true, + (new ParallelConsoleLogger(LoggerVerbosity.Diagnostic)).IsVerbosityAtLeast(LoggerVerbosity.Diagnostic)); + } + + /// + /// Test of single message printing + /// + [TestMethod] + public void SingleMessageTests_quiet_low() + { + SingleMessageTest(LoggerVerbosity.Quiet, + MessageImportance.Low, false); + } + + [TestMethod] + public void SingleMessageTests_quiet_medium() + { + SingleMessageTest(LoggerVerbosity.Quiet, + MessageImportance.Normal, false); + } + + [TestMethod] + public void SingleMessageTests_quiet_high() + { + SingleMessageTest(LoggerVerbosity.Quiet, + MessageImportance.High, false); + } + + [TestMethod] + public void SingleMessageTests_medium_low() + { + SingleMessageTest(LoggerVerbosity.Minimal, + MessageImportance.Low, false); + } + + [TestMethod] + public void SingleMessageTests_medium_medium() + { + SingleMessageTest(LoggerVerbosity.Minimal, + MessageImportance.Normal, false); + } + + [TestMethod] + public void SingleMessageTests_medium_high() + { + SingleMessageTest(LoggerVerbosity.Minimal, + MessageImportance.High, true); + } + + [TestMethod] + public void SingleMessageTests_normal_low() + { + SingleMessageTest(LoggerVerbosity.Normal, + MessageImportance.Low, false); + } + + [TestMethod] + public void SingleMessageTests_normal_medium() + { + SingleMessageTest(LoggerVerbosity.Normal, + MessageImportance.Normal, true); + } + + [TestMethod] + public void SingleMessageTests_normal_high() + { + SingleMessageTest(LoggerVerbosity.Normal, + MessageImportance.High, true); + } + + [TestMethod] + public void SingleMessageTests_detailed_low() + { + SingleMessageTest(LoggerVerbosity.Detailed, + MessageImportance.Low, true); + } + + [TestMethod] + public void SingleMessageTests_detailed_medium() + { + SingleMessageTest(LoggerVerbosity.Detailed, + MessageImportance.Normal, true); + } + + [TestMethod] + public void SingleMessageTests_detailed_high() + { + SingleMessageTest(LoggerVerbosity.Detailed, + MessageImportance.High, true); + } + + [TestMethod] + public void SingleMessageTests_diagnostic_low() + { + SingleMessageTest(LoggerVerbosity.Diagnostic, + MessageImportance.Low, true); + } + + [TestMethod] + public void SingleMessageTests_diagnostic_medium() + { + SingleMessageTest(LoggerVerbosity.Diagnostic, + MessageImportance.Normal, true); + } + + [TestMethod] + public void SingleMessageTests_diagnostic_high() + { + SingleMessageTest(LoggerVerbosity.Diagnostic, + MessageImportance.High, true); + } + + [TestMethod] + public void ErrorColorTest() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es); + + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", "31415", "file.vb", 42, 0, 0, 0, "Some long message", "help", "sender"); + es.Consume(beea); + Assert.AreEqual("file.vb(42): VBC error 31415: Some long message" + Environment.NewLine + "", sc.ToString()); + } + + [TestMethod] + public void ErrorColorTestParallel() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es, 4); + + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + beea.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + + es.Consume(beea); + + Assert.AreEqual( + "file.vb(42): VBC error 31415: Some long message" + + Environment.NewLine + "", + sc.ToString()); + } + + [TestMethod] + public void WarningColorTest() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es); + + BuildWarningEventArgs bwea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + es.Consume(bwea); + + Assert.AreEqual( + "file.vb(42): VBC warning 31415: Some long message" + + Environment.NewLine + "", + sc.ToString()); + } + + [TestMethod] + public void WarningColorTestParallel() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es, 2); + + BuildWarningEventArgs bwea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + bwea.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + es.Consume(bwea); + + Assert.AreEqual( + "file.vb(42): VBC warning 31415: Some long message" + + Environment.NewLine + "", + sc.ToString()); + } + + [TestMethod] + public void LowMessageColorTest() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Diagnostic, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es); + + BuildMessageEventArgs msg = + new BuildMessageEventArgs("text", "help", "sender", + MessageImportance.Low); + + es.Consume(msg); + + Assert.AreEqual( + "text" + + Environment.NewLine + "", + sc.ToString()); + } + + [TestMethod] + public void TestQuietWithHighMessage() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + es.Consume(bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 1, 1, 1)); + pse.BuildEventContext = buildEventContext; + es.Consume(pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + es.Consume(trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + es.Consume(tase); + + BuildMessageEventArgs bmea = new BuildMessageEventArgs("foo!", null, "sender", MessageImportance.High); + bmea.BuildEventContext = buildEventContext; + es.Consume(bmea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + es.Consume(tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + es.Consume(trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + es.Consume(pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + es.Consume(bfea); + + Assert.AreEqual(String.Empty, sc.ToString()); + } + } + + [TestMethod] + public void TestQuietWithError() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + es.Consume(bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(-1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 2, 3, 4)); + pse.BuildEventContext = buildEventContext; + es.Consume(pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + es.Consume(trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + es.Consume(tase); + + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + beea.BuildEventContext = buildEventContext; + es.Consume(beea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + es.Consume(tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + es.Consume(trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + es.Consume(pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + es.Consume(bfea); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + if (i == 1) + { + Assert.AreEqual( + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname") + Environment.NewLine + Environment.NewLine + + "file.vb(42): VBC error 31415: Some long message" + Environment.NewLine + + "pf" + Environment.NewLine + + "", + sc.ToString()); + } + else + { + Assert.AreEqual( + "file.vb(42): VBC error 31415: Some long message" + Environment.NewLine + "", + sc.ToString()); + } + } + } + + /// + /// Quiet build with a warning; project finished should appear + /// but not target finished + /// + [TestMethod] + public void TestQuietWithWarning() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + es.Consume(bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(-1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 2, 3, 4)); + pse.BuildEventContext = buildEventContext; + es.Consume(pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + es.Consume(trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + es.Consume(tase); + + BuildWarningEventArgs beea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + + beea.BuildEventContext = buildEventContext; + es.Consume(beea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + es.Consume(tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + es.Consume(trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + es.Consume(pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + es.Consume(bfea); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + if (i == 1) + { + Assert.AreEqual( + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname") + Environment.NewLine + Environment.NewLine + + "file.vb(42): VBC warning 31415: Some long message" + Environment.NewLine + + "pf" + Environment.NewLine + + "", + sc.ToString()); + } + else + { + Assert.AreEqual( + "file.vb(42): VBC warning 31415: Some long message" + Environment.NewLine + "", + sc.ToString()); + } + } + } + + /// + /// Minimal with no errors or warnings should emit nothing. + /// + [TestMethod] + public void TestMinimalWithNormalMessage() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Minimal, + sc.Write, sc.SetColor, + sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + es.Consume(bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 1, 1, 1)); + pse.BuildEventContext = buildEventContext; + es.Consume(pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + es.Consume(trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + es.Consume(tase); + + BuildMessageEventArgs bmea = new BuildMessageEventArgs("foo!", null, "sender", MessageImportance.Normal); + bmea.BuildEventContext = buildEventContext; + es.Consume(bmea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + es.Consume(tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + es.Consume(trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + es.Consume(pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + es.Consume(bfea); + + Assert.AreEqual(String.Empty, sc.ToString()); + } + } + + /// + /// Minimal with error should emit project started, the error, and project finished + /// + [TestMethod] + public void TestMinimalWithError() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Minimal, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + es.Consume(bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(-1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 2, 3, 4)); + pse.BuildEventContext = buildEventContext; + es.Consume(pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + es.Consume(trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + es.Consume(tase); + + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + beea.BuildEventContext = buildEventContext; + es.Consume(beea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + es.Consume(tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + es.Consume(trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + es.Consume(pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + es.Consume(bfea); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + if (i == 1) + { + Assert.AreEqual( + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname") + Environment.NewLine + Environment.NewLine + + "file.vb(42): VBC error 31415: Some long message" + Environment.NewLine + + "pf" + Environment.NewLine + + "", + sc.ToString()); + } + else + { + Assert.AreEqual( + "file.vb(42): VBC error 31415: Some long message" + Environment.NewLine + "", + sc.ToString()); + } + } + } + + /// + /// Minimal with warning should emit project started, the warning, and project finished + /// + [TestMethod] + public void TestMinimalWithWarning() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + es.Consume(bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(-1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 2, 3, 4)); + pse.BuildEventContext = buildEventContext; + es.Consume(pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + es.Consume(trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + es.Consume(tase); + + BuildWarningEventArgs beea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + + beea.BuildEventContext = buildEventContext; + es.Consume(beea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + es.Consume(tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + es.Consume(trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + es.Consume(pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + es.Consume(bfea); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + if (i == 1) + { + Assert.AreEqual( + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname") + Environment.NewLine + Environment.NewLine + + "file.vb(42): VBC warning 31415: Some long message" + Environment.NewLine + + "pf" + Environment.NewLine + + "", + sc.ToString()); + } + else + { + Assert.AreEqual( + "file.vb(42): VBC warning 31415: Some long message" + Environment.NewLine + "", + sc.ToString()); + } + } + } + + /// + /// Minimal with warning should emit project started, the warning, and project finished + /// + [TestMethod] + public void TestDirectEventHandlers() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Quiet, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, i); + + BuildEventContext buildEventContext = new BuildEventContext(1, 2, 3, 4); + + BuildStartedEventArgs bse = new BuildStartedEventArgs("bs", null); + bse.BuildEventContext = buildEventContext; + L.BuildStartedHandler(null, bse); + + ProjectStartedEventArgs pse = new ProjectStartedEventArgs(-1, "ps", null, "fname", "", null, null, new BuildEventContext(1, 2, 3, 4)); + pse.BuildEventContext = buildEventContext; + L.ProjectStartedHandler(null, pse); + + TargetStartedEventArgs trse = new TargetStartedEventArgs("ts", null, "trname", "pfile", "tfile"); + trse.BuildEventContext = buildEventContext; + L.TargetStartedHandler(null, trse); + + TaskStartedEventArgs tase = new TaskStartedEventArgs("tks", null, "tname", "tfname", "tsname"); + tase.BuildEventContext = buildEventContext; + L.TaskStartedHandler(null, tase); + + BuildWarningEventArgs beea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + + beea.BuildEventContext = buildEventContext; + L.WarningHandler(null, beea); + + TaskFinishedEventArgs tafea = new TaskFinishedEventArgs("tkf", null, "fname", "tsname", "tfname", true); + tafea.BuildEventContext = buildEventContext; + L.TaskFinishedHandler(null, tafea); + + TargetFinishedEventArgs trfea = new TargetFinishedEventArgs("tf", null, "trname", "fname", "tfile", true); + trfea.BuildEventContext = buildEventContext; + L.TargetFinishedHandler(null, trfea); + + ProjectFinishedEventArgs pfea = new ProjectFinishedEventArgs("pf", null, "fname", true); + pfea.BuildEventContext = buildEventContext; + L.ProjectFinishedHandler(null, pfea); + + BuildFinishedEventArgs bfea = new BuildFinishedEventArgs("bf", null, true); + bfea.BuildEventContext = buildEventContext; + L.BuildFinishedHandler(null, bfea); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + if (i == 1) + { + Assert.AreEqual( + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname") + Environment.NewLine + Environment.NewLine + + "file.vb(42): VBC warning 31415: Some long message" + Environment.NewLine + + "pf" + Environment.NewLine + + "", + sc.ToString()); + } + else + { + Assert.AreEqual( + "file.vb(42): VBC warning 31415: Some long message" + Environment.NewLine + "", + sc.ToString()); + } + } + } + + [TestMethod] + public void SingleLineFormatNoop() + { + string s = "foo"; + SerialConsoleLogger cl = new SerialConsoleLogger(); + + string ss = cl.IndentString(s, 0); + + //should be a no-op + Assert.AreEqual("foo" + Environment.NewLine, ss); + } + + [TestMethod] + public void MultilineFormatWindowsLineEndings() + { + string newline = "\r\n"; + string s = "foo" + newline + "bar" + + newline + "baz" + newline; + SerialConsoleLogger cl = new SerialConsoleLogger(); + + string ss = cl.IndentString(s, 4); + + //should convert lines to system format + Assert.AreEqual(" foo" + Environment.NewLine + + " bar" + Environment.NewLine + + " baz" + Environment.NewLine + + " " + Environment.NewLine, ss); + } + + [TestMethod] + public void MultilineFormatUnixLineEndings() + { + string s = "foo\nbar\nbaz\n"; + SerialConsoleLogger cl = new SerialConsoleLogger(); + + string ss = cl.IndentString(s, 0); + + //should convert lines to system format + Assert.AreEqual("foo" + Environment.NewLine + + "bar" + Environment.NewLine + + "baz" + Environment.NewLine + Environment.NewLine, ss); + } + + [TestMethod] + public void MultilineFormatMixedLineEndings() + { + string s = "foo" + "\r\n\r\n" + "bar" + "\n" + "baz" + "\n\r\n\n" + + "jazz" + "\r\n" + "razz" + "\n\n" + "matazz" + "\n" + "end"; + + SerialConsoleLogger cl = new SerialConsoleLogger(); + + string ss = cl.IndentString(s, 0); + + //should convert lines to system format + Assert.AreEqual("foo" + Environment.NewLine + Environment.NewLine + + "bar" + Environment.NewLine + + "baz" + Environment.NewLine + Environment.NewLine + Environment.NewLine + + "jazz" + Environment.NewLine + + "razz" + Environment.NewLine + Environment.NewLine + + "matazz" + Environment.NewLine + + "end" + Environment.NewLine, ss); + } + + [TestMethod] + public void NestedProjectMinimal() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Minimal, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, 1); + + es.Consume(new BuildStartedEventArgs("bs", null)); + + //Clear time dependant build started message + sc.Clear(); + + es.Consume(new ProjectStartedEventArgs("ps1", null, "fname1", "", null, null)); + + es.Consume(new TargetStartedEventArgs("ts", null, + "trname", "fname", "tfile")); + + es.Consume(new ProjectStartedEventArgs("ps2", null, "fname2", "", null, null)); + + Assert.AreEqual(string.Empty, sc.ToString()); + + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + es.Consume(beea); + + Assert.AreEqual( + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname1") + Environment.NewLine + + Environment.NewLine + "" + + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForNestedProjectWithDefaultTargets", "fname1", "fname2") + Environment.NewLine + + Environment.NewLine + "" + + "" + "file.vb(42): VBC error 31415: Some long message" + + Environment.NewLine + "", + sc.ToString()); + } + + [TestMethod] + public void NestedProjectNormal() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, + sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es); + + es.Consume(new BuildStartedEventArgs("bs", null)); + + + //Clear time dependant build started message + string expectedOutput = null; + string actualOutput = null; + sc.Clear(); + + es.Consume(new ProjectStartedEventArgs("ps1", null, "fname1", "", null, null)); + + #region Check + expectedOutput = + "" + BaseConsoleLogger.projectSeparatorLine + Environment.NewLine + + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForTopLevelProjectWithDefaultTargets", "fname1") + Environment.NewLine + + Environment.NewLine + ""; + actualOutput = sc.ToString(); + + Assert.AreEqual(expectedOutput, actualOutput); + Console.WriteLine("1 [" + expectedOutput + "] [" + actualOutput + "]"); + sc.Clear(); + #endregion + + es.Consume(new TargetStartedEventArgs("ts", null, + "tarname", "fname", "tfile")); + #region Check + expectedOutput = String.Empty; + actualOutput = sc.ToString(); + + Console.WriteLine("2 [" + expectedOutput + "] [" + actualOutput + "]"); + Assert.AreEqual(expectedOutput, actualOutput); + sc.Clear(); + #endregion + + es.Consume(new TaskStartedEventArgs("", "", "", "", "Exec")); + es.Consume(new ProjectStartedEventArgs("ps2", null, "fname2", "", null, null)); + + #region Check + expectedOutput = + "" + ResourceUtilities.FormatResourceString("TargetStartedPrefix", "tarname") + Environment.NewLine + "" + + "" + " " + BaseConsoleLogger.projectSeparatorLine + + Environment.NewLine + + " " + ResourceUtilities.FormatResourceString("ProjectStartedPrefixForNestedProjectWithDefaultTargets", "fname1", "fname2") + Environment.NewLine + + Environment.NewLine + ""; + actualOutput = sc.ToString(); + + Console.WriteLine("3 [" + expectedOutput + "] [" + actualOutput + "]"); + Assert.AreEqual(expectedOutput, actualOutput); + sc.Clear(); + #endregion + + es.Consume(new ProjectFinishedEventArgs("pf2", null, "fname2", true)); + es.Consume(new TaskFinishedEventArgs("", "", "", "", "Exec", true)); + + #region Check + expectedOutput = String.Empty; + actualOutput = sc.ToString(); + + Console.WriteLine("4 [" + expectedOutput + "] [" + actualOutput + "]"); + Assert.AreEqual(expectedOutput, actualOutput); + sc.Clear(); + #endregion + + es.Consume(new TargetFinishedEventArgs("tf", null, "tarname", "fname", "tfile", true)); + + #region Check + expectedOutput = String.Empty; + actualOutput = sc.ToString(); + + Console.WriteLine("5 [" + expectedOutput + "] [" + actualOutput + "]"); + Assert.AreEqual(expectedOutput, actualOutput); + sc.Clear(); + #endregion + + es.Consume(new ProjectFinishedEventArgs("pf1", null, "fname1", true)); + + #region Check + expectedOutput = String.Empty; + actualOutput = sc.ToString(); + + Console.WriteLine("6 [" + expectedOutput + "] [" + actualOutput + "]"); + Assert.AreEqual(expectedOutput, actualOutput); + sc.Clear(); + #endregion + + es.Consume(new BuildFinishedEventArgs("bf", null, true)); + + #region Check + expectedOutput = "" + Environment.NewLine + "bf" + + Environment.NewLine + "" + + " " + ResourceUtilities.FormatResourceString("WarningCount", 0) + + Environment.NewLine + "" + + " " + ResourceUtilities.FormatResourceString("ErrorCount", 0) + + Environment.NewLine + "" + + Environment.NewLine; + + // Would like to add... + // + ResourceUtilities.FormatResourceString("TimeElapsed", String.Empty); + // ...but this assumes that the time goes on the far right in every locale. + + actualOutput = sc.ToString().Substring(0, expectedOutput.Length); + + Console.WriteLine("7 [" + expectedOutput + "] [" + actualOutput + "]"); + Assert.AreEqual(expectedOutput, actualOutput); + sc.Clear(); + #endregion + + } + + [TestMethod] + public void CustomDisplayedAtDetailed() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Detailed, + sc.Write, null, null); + L.Initialize(es); + + MyCustomBuildEventArgs c = + new MyCustomBuildEventArgs("msg"); + + es.Consume(c); + + Assert.AreEqual("msg" + Environment.NewLine, + sc.ToString()); + } + + [TestMethod] + public void CustomDisplayedAtDiagnosticMP() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Diagnostic, + sc.Write, null, null); + L.Initialize(es, 2); + + MyCustomBuildEventArgs c = + new MyCustomBuildEventArgs("msg"); + c.BuildEventContext = new BuildEventContext(1, 1, 1, 1); + es.Consume(c); + + Assert.IsTrue(sc.ToString().Contains("msg")); + } + + [TestMethod] + public void CustomNotDisplayedAtNormal() + { + EventSourceSink es = new EventSourceSink(); + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, + sc.Write, null, null); + L.Initialize(es); + + MyCustomBuildEventArgs c = + new MyCustomBuildEventArgs("msg"); + + es.Consume(c); + + Assert.AreEqual(String.Empty, sc.ToString()); + } + + /// + /// Create some properties and log them + /// + /// + /// + private void WriteAndValidateProperties(BaseConsoleLogger cl, SimulatedConsole sc, bool expectToSeeLogging) + { + Hashtable properties = new Hashtable(); + properties.Add("prop1", "val1"); + properties.Add("prop2", "val2"); + properties.Add("pro(p3)", "va%3b%253b%3bl3"); + string prop1 = string.Empty; + string prop2 = string.Empty; + string prop3 = string.Empty; + + if (cl is SerialConsoleLogger) + { + ArrayList propertyList = ((SerialConsoleLogger)cl).ExtractPropertyList(properties); + ((SerialConsoleLogger)cl).WriteProperties(propertyList); + prop1 = String.Format(CultureInfo.CurrentCulture, "{0,-30} = {1}", "prop1", "val1"); + prop2 = String.Format(CultureInfo.CurrentCulture, "{0,-30} = {1}", "prop2", "val2"); + prop3 = String.Format(CultureInfo.CurrentCulture, "{0,-30} = {1}", "pro(p3)", "va;%3b;l3"); + } + else + { + BuildEventArgs buildEvent = new BuildErrorEventArgs("", "", "", 0, 0, 0, 0, "", "", ""); + buildEvent.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + ((ParallelConsoleLogger)cl).WriteProperties(buildEvent, properties); + prop1 = String.Format(CultureInfo.CurrentCulture, "{0} = {1}", "prop1", "val1"); + prop2 = String.Format(CultureInfo.CurrentCulture, "{0} = {1}", "prop2", "val2"); + prop3 = String.Format(CultureInfo.CurrentCulture, "{0} = {1}", "pro(p3)", "va;%3b;l3"); + } + string log = sc.ToString(); + + Console.WriteLine("[" + log + "]"); + + + // Being careful not to make locale assumptions here, eg about sorting + if (expectToSeeLogging) + { + Assert.IsTrue(log.Contains(prop1)); + Assert.IsTrue(log.Contains(prop2)); + Assert.IsTrue(log.Contains(prop3)); + } + else + { + Assert.IsFalse(log.Contains(prop1)); + Assert.IsFalse(log.Contains(prop2)); + Assert.IsFalse(log.Contains(prop3)); + } + } + + /// + /// Basic test of properties list display + /// + [TestMethod] + public void DisplayPropertiesList() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + + WriteAndValidateProperties(cl, sc, true); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + + WriteAndValidateProperties(cl2, sc, true); + } + + /// + /// Basic test of properties list not being displayed except in Diagnostic + /// + [TestMethod] + public void DoNotDisplayPropertiesListInDetailed() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + + WriteAndValidateProperties(cl, sc, false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + + WriteAndValidateProperties(cl2, sc, false); + } + + + /// + /// Basic test of environment list not being displayed except in Diagnostic or if the showenvironment flag is set + /// + [TestMethod] + public void DoNotDisplayEnvironmentInDetailed() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + + WriteEnvironment(cl, sc, false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + + WriteEnvironment(cl2, sc, false); + } + + + + /// + /// Basic test of environment list not being displayed except in Diagnostic or if the showenvironment flag is set + /// + [TestMethod] + public void DisplayEnvironmentInDetailed() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + cl.Parameters = "ShowEnvironment"; + cl.ParseParameters(); + WriteEnvironment(cl, sc, true); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + cl2.Parameters = "ShowEnvironment"; + cl2.ParseParameters(); + + WriteEnvironment(cl2, sc, true); + } + + /// + /// Basic test of environment list not being displayed except in Diagnostic or if the showenvironment flag is set + /// + [TestMethod] + public void DisplayEnvironmentInDiagnostic() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + WriteEnvironment(cl, sc, true); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + WriteEnvironment(cl2, sc, true); + } + + /// + /// Basic test of environment list not being displayed except in Diagnostic or if the showenvironment flag is set + /// + [TestMethod] + public void DoNotDisplayEnvironmentInMinimal() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Minimal, sc.Write, null, null); + + WriteEnvironment(cl, sc, false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Minimal, sc.Write, null, null); + + WriteEnvironment(cl2, sc, false); + } + + + + /// + /// Basic test of environment list not being displayed except in Diagnostic or if the showenvironment flag is set + /// + [TestMethod] + public void DisplayEnvironmentInMinimal() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Minimal, sc.Write, null, null); + cl.Parameters = "ShowEnvironment"; + cl.ParseParameters(); + WriteEnvironment(cl, sc, true); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Minimal, sc.Write, null, null); + cl2.Parameters = "ShowEnvironment"; + cl2.ParseParameters(); + + WriteEnvironment(cl2, sc, true); + } + + /// + /// Basic test of properties list not being displayed when disabled + /// + [TestMethod] + public void DoNotDisplayPropertiesListIfDisabled() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + cl.Parameters = "noitemandpropertylist"; + cl.ParseParameters(); + + WriteAndValidateProperties(cl, sc, false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + cl2.Parameters = "noitemandpropertylist"; + cl2.ParseParameters(); + + WriteAndValidateProperties(cl, sc, false); + } + + + /// + /// Create some items and log them + /// + private void WriteEnvironment(BaseConsoleLogger cl, SimulatedConsole sc, bool expectToSeeLogging) + { + cl.WriteEnvironment(_environment); + string log = sc.ToString(); + Console.WriteLine("[" + log + "]"); + + // Being careful not to make locale assumptions here, eg about sorting + foreach (KeyValuePair kvp in _environment) + { + string message = String.Empty; + if (cl is ParallelConsoleLogger) + { + message = String.Format(CultureInfo.CurrentCulture, "{0} = {1}", kvp.Key, kvp.Value); + } + else + { + message = String.Format(CultureInfo.CurrentCulture, "{0,-30} = {1}", kvp.Key, kvp.Value); + } + + if (expectToSeeLogging) + { + Assert.IsTrue(log.Contains(message)); + } + else + { + Assert.IsFalse(log.Contains(message)); + } + } + } + + /// + /// Create some items and log them + /// + /// + private void WriteAndValidateItems(BaseConsoleLogger cl, SimulatedConsole sc, bool expectToSeeLogging) + { + Hashtable items = new Hashtable(); + items.Add("type", (ITaskItem2)new TaskItem("spec", String.Empty)); + items.Add("type2", (ITaskItem2)new TaskItem("spec2", String.Empty)); + + // ItemSpecs are expected to be escaped coming in + ITaskItem2 taskItem3 = new TaskItem("%28spec%3b3", String.Empty); + + // As are metadata, when set with "SetMetadata" + taskItem3.SetMetadata("f)oo", "%21%40%23"); + + items.Add("type(3)", taskItem3); + + string item1type = string.Empty; + string item2type = string.Empty; + string item3type = string.Empty; + string item1spec = string.Empty; + string item2spec = string.Empty; + string item3spec = string.Empty; + string item3metadatum = string.Empty; + + if (cl is SerialConsoleLogger) + { + SortedList itemList = ((SerialConsoleLogger)cl).ExtractItemList(items); + ((SerialConsoleLogger)cl).WriteItems(itemList); + item1spec = "spec" + Environment.NewLine; + item2spec = "spec2" + Environment.NewLine; + item3spec = "(spec;3" + Environment.NewLine; + item3metadatum = "f)oo = !@#" + Environment.NewLine; + } + else + { + BuildEventArgs buildEvent = new BuildErrorEventArgs("", "", "", 0, 0, 0, 0, "", "", ""); + buildEvent.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + ((ParallelConsoleLogger)cl).WriteItems(buildEvent, items); + item1spec = Environment.NewLine + " spec" + Environment.NewLine; + item2spec = Environment.NewLine + " spec2" + Environment.NewLine; + item3spec = Environment.NewLine + " (spec;3" + Environment.NewLine; + } + + item1type = "type" + Environment.NewLine; + item2type = "type2" + Environment.NewLine; + item3type = "type(3)" + Environment.NewLine; + + string log = sc.ToString(); + + Console.WriteLine("[" + log + "]"); + + + + // Being careful not to make locale assumptions here, eg about sorting + if (expectToSeeLogging) + { + Assert.IsTrue(log.Contains(item1type)); + Assert.IsTrue(log.Contains(item2type)); + Assert.IsTrue(log.Contains(item3type)); + Assert.IsTrue(log.Contains(item1spec)); + Assert.IsTrue(log.Contains(item2spec)); + Assert.IsTrue(log.Contains(item3spec)); + + if (!String.Equals(item3metadatum, String.Empty, StringComparison.OrdinalIgnoreCase)) + { + Assert.IsTrue(log.Contains(item3metadatum)); + } + } + else + { + Assert.IsFalse(log.Contains(item1type)); + Assert.IsFalse(log.Contains(item2type)); + Assert.IsFalse(log.Contains(item3type)); + Assert.IsFalse(log.Contains(item1spec)); + Assert.IsFalse(log.Contains(item2spec)); + Assert.IsFalse(log.Contains(item3type)); + + if (!String.Equals(item3metadatum, String.Empty, StringComparison.OrdinalIgnoreCase)) + { + Assert.IsFalse(log.Contains(item3metadatum)); + } + } + } + + /// + /// Verify passing in an empty item list does not print anything out + /// + /// + [TestMethod] + public void WriteItemsEmptyList() + { + Hashtable items = new Hashtable(); + + for (int i = 0; i < 2; i++) + { + BaseConsoleLogger cl = null; + SimulatedConsole sc = new SimulatedConsole(); + if (i == 0) + { + cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + } + else + { + cl = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + } + + if (cl is SerialConsoleLogger) + { + SortedList itemList = ((SerialConsoleLogger)cl).ExtractItemList(items); + ((SerialConsoleLogger)cl).WriteItems(itemList); + } + else + { + BuildEventArgs buildEvent = new BuildErrorEventArgs("", "", "", 0, 0, 0, 0, "", "", ""); + buildEvent.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + ((ParallelConsoleLogger)cl).WriteItems(buildEvent, items); + } + + string log = sc.ToString(); + + // There should be nothing in the log + Assert.IsTrue(log.Length == 0, "Iteration of I: " + i); + Console.WriteLine("Iteration of i: " + i + "[" + log + "]"); + } + } + + /// + /// Verify passing in an empty item list does not print anything out + /// + /// + [TestMethod] + public void WritePropertiesEmptyList() + { + Hashtable properties = new Hashtable(); + + + for (int i = 0; i < 2; i++) + { + BaseConsoleLogger cl = null; + SimulatedConsole sc = new SimulatedConsole(); + if (i == 0) + { + cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + } + else + { + cl = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + } + + if (cl is SerialConsoleLogger) + { + ArrayList propertyList = ((SerialConsoleLogger)cl).ExtractPropertyList(properties); + ((SerialConsoleLogger)cl).WriteProperties(propertyList); + } + else + { + BuildEventArgs buildEvent = new BuildErrorEventArgs("", "", "", 0, 0, 0, 0, "", "", ""); + buildEvent.BuildEventContext = new BuildEventContext(1, 2, 3, 4); + ((ParallelConsoleLogger)cl).WriteProperties(buildEvent, properties); + } + + string log = sc.ToString(); + + // There should be nothing in the log + Assert.IsTrue(log.Length == 0, "Iteration of I: " + i); + Console.WriteLine("Iteration of i: " + i + "[" + log + "]"); + } + } + + /// + /// Basic test of item list display + /// + [TestMethod] + public void DisplayItemsList() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + + WriteAndValidateItems(cl, sc, true); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + + WriteAndValidateItems(cl2, sc, true); + } + + /// + /// Basic test of item list not being displayed except in Diagnostic + /// + [TestMethod] + public void DoNotDisplayItemListInDetailed() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + + WriteAndValidateItems(cl, sc, false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Detailed, sc.Write, null, null); + + WriteAndValidateItems(cl2, sc, false); + } + + /// + /// Basic test of item list not being displayed when disabled + /// + [TestMethod] + public void DoNotDisplayItemListIfDisabled() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger cl = new SerialConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + cl.Parameters = "noitemandpropertylist"; + cl.ParseParameters(); + + WriteAndValidateItems(cl, sc, false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + cl2.Parameters = "noitemandpropertylist"; + cl2.ParseParameters(); + + WriteAndValidateItems(cl2, sc, false); + } + + [TestMethod] + public void ParametersEmptyTests() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger L = new SerialConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + + L.Parameters = ""; + L.ParseParameters(); + Assert.IsTrue(L.ShowSummary == false); + + L.Parameters = null; + L.ParseParameters(); + Assert.IsTrue(L.ShowSummary == false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger cl2 = new ParallelConsoleLogger(LoggerVerbosity.Diagnostic, sc.Write, null, null); + cl2.Parameters = "noitemandpropertylist"; + cl2.ParseParameters(); + + WriteAndValidateItems(cl2, sc, false); + } + + [TestMethod] + public void ParametersParsingTests() + { + SimulatedConsole sc = new SimulatedConsole(); + SerialConsoleLogger L = new SerialConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + + L.Parameters = "NoSuMmaRy"; + L.ParseParameters(); + Assert.IsTrue(L.ShowSummary == false); + + L.Parameters = ";;NoSuMmaRy;"; + L.ParseParameters(); + Assert.IsTrue(L.ShowSummary == false); + + sc = new SimulatedConsole(); + ParallelConsoleLogger L2 = new ParallelConsoleLogger(LoggerVerbosity.Normal, sc.Write, null, null); + + L2.Parameters = "NoSuMmaRy"; + L2.ParseParameters(); + Assert.IsTrue(L2.ShowSummary == false); + + L2.Parameters = ";;NoSuMmaRy;"; + L2.ParseParameters(); + Assert.IsTrue(L2.ShowSummary == false); + } + + /// + /// ResetConsoleLoggerState should reset the state of the console logger + /// + [TestMethod] + public void ResetConsoleLoggerStateTestBasic() + { + // Create an event source + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + + // error and warning string for 1 error and 1 warning + // errorString = 1 Error(s) + // warningString = 1 Warning(s) + string errorString = ResourceUtilities.FormatResourceString("ErrorCount", 1); + string warningString = ResourceUtilities.FormatResourceString("WarningCount", 1); + + // Create a ConsoleLogger with Normal verbosity + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, + sc.Write, sc.SetColor, sc.ResetColor); + // Initialize ConsoleLogger + L.Initialize(es); + + // BuildStarted Event + es.Consume(new BuildStartedEventArgs("bs", null)); + + // Introduce a warning + BuildWarningEventArgs bwea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + es.Consume(bwea); + + // Introduce an error + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + es.Consume(beea); + + // BuildFinished Event + es.Consume(new BuildFinishedEventArgs("bf", + null, true)); + + // Log so far + string actualLog = sc.ToString(); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + // Verify that the log has correct error and warning string + Assert.IsTrue(actualLog.Contains(errorString)); + Assert.IsTrue(actualLog.Contains(warningString)); + Assert.IsTrue(actualLog.Contains("")); + Assert.IsTrue(actualLog.Contains("")); + + // Clear the log obtained so far + sc.Clear(); + + // BuildStarted event + es.Consume(new BuildStartedEventArgs("bs", null)); + + // BuildFinished + es.Consume(new BuildFinishedEventArgs("bf", + null, true)); + // Log so far + actualLog = sc.ToString(); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + // Verify that the error and warning from the previous build is not + // reported in the subsequent build + Assert.IsFalse(actualLog.Contains(errorString)); + Assert.IsFalse(actualLog.Contains(warningString)); + Assert.IsFalse(actualLog.Contains("")); + Assert.IsFalse(actualLog.Contains("")); + + // errorString = 0 Error(s) + // warningString = 0 Warning(s) + errorString = ResourceUtilities.FormatResourceString("ErrorCount", 0); + warningString = ResourceUtilities.FormatResourceString("WarningCount", 0); + + // Verify that the log has correct error and warning string + Assert.IsTrue(actualLog.Contains(errorString)); + Assert.IsTrue(actualLog.Contains(warningString)); + } + + /// + /// ConsoleLogger::Initialize() should reset the state of the console logger + /// + [TestMethod] + public void ResetConsoleLoggerState_Initialize() + { + // Create an event source + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + + // error and warning string for 1 error and 1 warning + // errorString = 1 Error(s) + // warningString = 1 Warning(s) + string errorString = ResourceUtilities.FormatResourceString("ErrorCount", 1); + string warningString = ResourceUtilities.FormatResourceString("WarningCount", 1); + + // Create a ConsoleLogger with Normal verbosity + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, + sc.Write, sc.SetColor, sc.ResetColor); + // Initialize ConsoleLogger + L.Initialize(es); + + // BuildStarted Event + es.Consume(new BuildStartedEventArgs("bs", null)); + + // Introduce a warning + BuildWarningEventArgs bwea = new BuildWarningEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + es.Consume(bwea); + + // Introduce an error + BuildErrorEventArgs beea = new BuildErrorEventArgs("VBC", + "31415", "file.vb", 42, 0, 0, 0, + "Some long message", "help", "sender"); + + es.Consume(beea); + + // NOTE: We don't call the es.RaiseBuildFinishedEvent(...) here as this + // would call ResetConsoleLoggerState and we will fail to detect if Initialize() + // is not calling it. + + // Log so far + string actualLog = sc.ToString(); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + // Verify that the log has correct error and warning string + Assert.IsTrue(actualLog.Contains("")); + Assert.IsTrue(actualLog.Contains("")); + + // Clear the log obtained so far + sc.Clear(); + + //Initilialize (This should call ResetConsoleLoggerState(...)) + L.Initialize(es); + + // BuildStarted event + es.Consume(new BuildStartedEventArgs("bs", null)); + + // BuildFinished + es.Consume(new BuildFinishedEventArgs("bf", + null, true)); + // Log so far + actualLog = sc.ToString(); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + // Verify that the error and warning from the previous build is not + // reported in the subsequent build + Assert.IsFalse(actualLog.Contains("")); + Assert.IsFalse(actualLog.Contains("")); + + // errorString = 0 Error(s) + errorString = ResourceUtilities.FormatResourceString("ErrorCount", 0); + // warningString = 0 Warning(s) + warningString = ResourceUtilities.FormatResourceString("WarningCount", 0); + + // Verify that the log has correct error and warning string + Assert.IsTrue(actualLog.Contains(errorString)); + Assert.IsTrue(actualLog.Contains(warningString)); + } + + /// + /// ResetConsoleLoggerState should reset PerformanceCounters + /// + [TestMethod] + public void ResetConsoleLoggerState_PerformanceCounters() + { + for (int i = 1; i <= 2; i++) + { + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + // Create a ConsoleLogger with Normal verbosity + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, sc.SetColor, sc.ResetColor); + // Initialize ConsoleLogger + L.Parameters = "Performancesummary"; + L.Initialize(es, i); + // prjPerfString = Project Performance Summary: + string prjPerfString = ResourceUtilities.FormatResourceString("ProjectPerformanceSummary", null); + // targetPerfString = Target Performance Summary: + string targetPerfString = ResourceUtilities.FormatResourceString("TargetPerformanceSummary", null); + // taskPerfString = Task Performance Summary: + string taskPerfString = ResourceUtilities.FormatResourceString("TaskPerformanceSummary", null); + + // BuildStarted Event + es.Consume(new BuildStartedEventArgs("bs", null)); + //Project Started Event + ProjectStartedEventArgs project1Started = new ProjectStartedEventArgs(1, null, null, "p", "t", null, null, new BuildEventContext(BuildEventContext.InvalidNodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId)); + project1Started.BuildEventContext = new BuildEventContext(1, 1, 1, 1); + es.Consume(project1Started); + TargetStartedEventArgs targetStarted1 = new TargetStartedEventArgs(null, null, "t", null, null); + targetStarted1.BuildEventContext = project1Started.BuildEventContext; + // TargetStarted Event + es.Consume(targetStarted1); + + TaskStartedEventArgs taskStarted1 = new TaskStartedEventArgs(null, null, null, null, "task"); + taskStarted1.BuildEventContext = project1Started.BuildEventContext; + // TaskStarted Event + es.Consume(taskStarted1); + + BuildMessageEventArgs messsage1 = new BuildMessageEventArgs(null, null, null, MessageImportance.High); + messsage1.BuildEventContext = project1Started.BuildEventContext; + // Message Event + es.Consume(messsage1); + TaskFinishedEventArgs taskFinished1 = new TaskFinishedEventArgs(null, null, null, null, "task", true); + taskFinished1.BuildEventContext = project1Started.BuildEventContext; + // TaskFinished Event + es.Consume(taskFinished1); + + TargetFinishedEventArgs targetFinished1 = new TargetFinishedEventArgs(null, null, "t", null, null, true); + targetFinished1.BuildEventContext = project1Started.BuildEventContext; + // TargetFinished Event + es.Consume(targetFinished1); + + ProjectStartedEventArgs project2Started = new ProjectStartedEventArgs(2, null, null, "p2", "t2", null, null, project1Started.BuildEventContext); + //Project Started Event + project2Started.BuildEventContext = new BuildEventContext(2, 2, 2, 2); + es.Consume(project2Started); + TargetStartedEventArgs targetStarted2 = new TargetStartedEventArgs(null, null, "t2", null, null); + targetStarted2.BuildEventContext = project2Started.BuildEventContext; + // TargetStarted Event + es.Consume(targetStarted2); + + TaskStartedEventArgs taskStarted2 = new TaskStartedEventArgs(null, null, null, null, "task2"); + taskStarted2.BuildEventContext = project2Started.BuildEventContext; + // TaskStarted Event + es.Consume(taskStarted2); + + BuildMessageEventArgs messsage2 = new BuildMessageEventArgs(null, null, null, MessageImportance.High); + messsage2.BuildEventContext = project2Started.BuildEventContext; + // Message Event + es.Consume(messsage2); + TaskFinishedEventArgs taskFinished2 = new TaskFinishedEventArgs(null, null, null, null, "task2", true); + taskFinished2.BuildEventContext = project2Started.BuildEventContext; + // TaskFinished Event + es.Consume(taskFinished2); + + TargetFinishedEventArgs targetFinished2 = new TargetFinishedEventArgs(null, null, "t2", null, null, true); + targetFinished2.BuildEventContext = project2Started.BuildEventContext; + // TargetFinished Event + es.Consume(targetFinished2); + + ProjectFinishedEventArgs finished2 = new ProjectFinishedEventArgs(null, null, "p2", true); + finished2.BuildEventContext = project2Started.BuildEventContext; + // ProjectFinished Event + es.Consume(finished2); // BuildFinished Event + + ProjectFinishedEventArgs finished1 = new ProjectFinishedEventArgs(null, null, "p", true); + finished1.BuildEventContext = project1Started.BuildEventContext; + // ProjectFinished Event + es.Consume(finished1); // BuildFinished Event + es.Consume(new BuildFinishedEventArgs("bf", + null, true)); + // Log so far + string actualLog = sc.ToString(); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + // Verify that the log has perf summary + // Project perf summary + Assert.IsTrue(actualLog.Contains(prjPerfString)); + // Target perf summary + Assert.IsTrue(actualLog.Contains(targetPerfString)); + // Task Perf summary + Assert.IsTrue(actualLog.Contains(taskPerfString)); + + // Clear the log obtained so far + sc.Clear(); + + // BuildStarted event + es.Consume(new BuildStartedEventArgs("bs", null)); + // BuildFinished + es.Consume(new BuildFinishedEventArgs("bf", + null, true)); + // Log so far + actualLog = sc.ToString(); + + Console.WriteLine("=="); + Console.WriteLine(sc.ToString()); + Console.WriteLine("=="); + + // Verify that the log doesn't have perf summary + Assert.IsFalse(actualLog.Contains(prjPerfString)); + Assert.IsFalse(actualLog.Contains(targetPerfString)); + Assert.IsFalse(actualLog.Contains(taskPerfString)); + } + } + + + [TestMethod] + public void DeferredMessages() + { + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + // Create a ConsoleLogger with Detailed verbosity + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Detailed, sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, 2); + es.Consume(new BuildStartedEventArgs("bs", null)); + TaskCommandLineEventArgs messsage1 = new TaskCommandLineEventArgs("Message", null, MessageImportance.High); + messsage1.BuildEventContext = new BuildEventContext(1, 1, 1, 1); + // Message Event + es.Consume(messsage1); + es.Consume(new BuildFinishedEventArgs("bf", null, true)); + string actualLog = sc.ToString(); + Assert.IsTrue(actualLog.Contains(ResourceUtilities.FormatResourceString("DeferredMessages"))); + + es = new EventSourceSink(); + sc = new SimulatedConsole(); + // Create a ConsoleLogger with Normal verbosity + L = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, 2); + es.Consume(new BuildStartedEventArgs("bs", null)); + BuildMessageEventArgs messsage2 = new BuildMessageEventArgs("Message", null, null, MessageImportance.High); + messsage2.BuildEventContext = new BuildEventContext(1, 1, 1, 1); + // Message Event + es.Consume(messsage2); + es.Consume(new BuildFinishedEventArgs("bf", null, true)); + actualLog = sc.ToString(); + Assert.IsTrue(actualLog.Contains(ResourceUtilities.FormatResourceString("DeferredMessages"))); + + es = new EventSourceSink(); + sc = new SimulatedConsole(); + // Create a ConsoleLogger with Normal verbosity + L = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, 2); + es.Consume(new BuildStartedEventArgs("bs", null)); + messsage2 = new BuildMessageEventArgs("Message", null, null, MessageImportance.High); + messsage2.BuildEventContext = new BuildEventContext(1, 1, 1, 1); + // Message Event + es.Consume(messsage2); + ProjectStartedEventArgs project = new ProjectStartedEventArgs(1, "Hello,", "HI", "None", "Build", null, null, messsage1.BuildEventContext); + project.BuildEventContext = messsage1.BuildEventContext; + es.Consume(project); + es.Consume(new BuildFinishedEventArgs("bf", null, true)); + actualLog = sc.ToString(); + Assert.IsTrue(actualLog.Contains("Message")); + } + + [TestMethod] + public void VerifyMPLoggerSwitch() + { + for (int i = 0; i < 2; i++) + { + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + // Create a ConsoleLogger with Normal verbosity + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, sc.SetColor, sc.ResetColor); + //Make sure the MPLogger switch will property work on both Initialize methods + L.Parameters = "EnableMPLogging"; + if (i == 0) + { + L.Initialize(es, 1); + } + else + { + L.Initialize(es); + } + es.Consume(new BuildStartedEventArgs("bs", null)); + BuildEventContext context = new BuildEventContext(1, 1, 1, 1); + BuildEventContext context2 = new BuildEventContext(2, 2, 2, 2); + + ProjectStartedEventArgs project = new ProjectStartedEventArgs(1, "Hello,", "HI", "None", "Build", null, null, context); + project.BuildEventContext = context; + es.Consume(project); + + TargetStartedEventArgs targetStarted1 = new TargetStartedEventArgs(null, null, "t", null, null); + targetStarted1.BuildEventContext = context; + es.Consume(targetStarted1); + + BuildMessageEventArgs messsage1 = new BuildMessageEventArgs("Message", null, null, MessageImportance.High); + messsage1.BuildEventContext = context; + es.Consume(messsage1); + string actualLog = sc.ToString(); + string resourceString = ResourceUtilities.FormatResourceString("ProjectStartedTopLevelProjectWithTargetNames", "None", 1, "Build"); + Assert.IsTrue(actualLog.Contains(resourceString)); + } + } + + [TestMethod] + public void TestPrintTargetNamePerMessage() + { + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + // Create a ConsoleLogger with Normal verbosity + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, sc.SetColor, sc.ResetColor); + L.Initialize(es, 2); + es.Consume(new BuildStartedEventArgs("bs", null)); + BuildEventContext context = new BuildEventContext(1, 1, 1, 1); + BuildEventContext context2 = new BuildEventContext(2, 2, 2, 2); + + ProjectStartedEventArgs project = new ProjectStartedEventArgs(1, "Hello,", "HI", "None", "Build", null, null, context); + project.BuildEventContext = context; + es.Consume(project); + + ProjectStartedEventArgs project2 = new ProjectStartedEventArgs(2, "Hello,", "HI", "None", "Build", null, null, context2); + project2.BuildEventContext = context2; + es.Consume(project2); + + TargetStartedEventArgs targetStarted1 = new TargetStartedEventArgs(null, null, "t", null, null); + targetStarted1.BuildEventContext = context; + es.Consume(targetStarted1); + + TargetStartedEventArgs targetStarted2 = new TargetStartedEventArgs(null, null, "t2", null, null); + targetStarted2.BuildEventContext = context2; + es.Consume(targetStarted2); + + BuildMessageEventArgs messsage1 = new BuildMessageEventArgs("Message", null, null, MessageImportance.High); + messsage1.BuildEventContext = context; + BuildMessageEventArgs messsage2 = new BuildMessageEventArgs("Message2", null, null, MessageImportance.High); + messsage2.BuildEventContext = context2; + BuildMessageEventArgs messsage3 = new BuildMessageEventArgs("Message3", null, null, MessageImportance.High); + messsage3.BuildEventContext = context; + es.Consume(messsage1); + es.Consume(messsage2); + es.Consume(messsage3); + string actualLog = sc.ToString(); + Assert.IsTrue(actualLog.Contains("t:")); + } + + /// + /// Verify that in the MP case and the older serial logger that there is no extra newline after the project done event. + /// We cannot verify there is a newline after the project done event for the MP single proc log because + /// nunit is showing up as an unknown output type, this causes us to not print the newline because we think it may be to a + /// text file. + /// + [TestMethod] + public void TestNewLineAfterProjectFinished() + { + bool runningWithCharDevice = IsRunningWithCharacterFileType(); + for (int i = 0; i < 3; i++) + { + Console.Out.WriteLine("Iteration of I is {" + i + "}"); + + + EventSourceSink es = new EventSourceSink(); + //Create a simulated console + SimulatedConsole sc = new SimulatedConsole(); + ConsoleLogger L = new ConsoleLogger(LoggerVerbosity.Normal, sc.Write, sc.SetColor, sc.ResetColor); + + if (i < 2) + { + // On the second pass through use the MP single proc logger + if (i == 1) + { + L.Parameters = "EnableMPLogging"; + } + // Use the old single proc logger + L.Initialize(es, 1); + } + else + { + // Use the parallel logger + L.Initialize(es, 2); + } + + es.Consume(new BuildStartedEventArgs("bs", null)); + BuildEventContext context = new BuildEventContext(1, 1, 1, 1); + + ProjectStartedEventArgs project = new ProjectStartedEventArgs(1, "Hello,", "HI", "None", "Build", null, null, context); + project.BuildEventContext = context; + es.Consume(project); + + TargetStartedEventArgs targetStarted1 = new TargetStartedEventArgs(null, null, "t", null, null); + targetStarted1.BuildEventContext = context; + es.Consume(targetStarted1); + + BuildMessageEventArgs messsage1 = new BuildMessageEventArgs("Message", null, null, MessageImportance.High); + messsage1.BuildEventContext = context; + es.Consume(messsage1); + + ProjectFinishedEventArgs projectFinished = new ProjectFinishedEventArgs("Finished,", "HI", "projectFile", true); + projectFinished.BuildEventContext = context; + es.Consume(projectFinished); + + string actualLog = sc.ToString(); + + switch (i) + { + case 0: + // There is no project finished event printed in normal verbosity + Assert.IsFalse(actualLog.Contains(projectFinished.Message)); + break; + // We are in single proc but logging with multiproc logging add an extra new line to make the log more readable. + case 1: + Assert.IsTrue(actualLog.Contains(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithTargetNamesMultiProc", "None", "Build") + Environment.NewLine)); + if (runningWithCharDevice) + { + Assert.IsTrue(actualLog.Contains(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithTargetNamesMultiProc", "None", "Build") + Environment.NewLine + Environment.NewLine)); + } + else + { + Assert.IsFalse(actualLog.Contains(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithTargetNamesMultiProc", "None", "Build") + Environment.NewLine + Environment.NewLine)); + } + break; + case 2: + Assert.IsFalse(actualLog.Contains(ResourceUtilities.FormatResourceString("ProjectFinishedPrefixWithTargetNamesMultiProc", "None", "Build") + Environment.NewLine + Environment.NewLine)); + break; + } + } + } + + /// + /// Check to see what kind of device we are outputting the log to, is it a character device, a file, or something else + /// this can be used by loggers to modify their outputs based on the device they are writing to + /// + internal bool IsRunningWithCharacterFileType() + { + // Get the std out handle + IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + + if (stdHandle != Microsoft.Build.BackEnd.NativeMethods.InvalidHandle) + { + uint fileType = NativeMethodsShared.GetFileType(stdHandle); + + // The std out is a char type(LPT or Console) + return fileType == NativeMethodsShared.FILE_TYPE_CHAR; + } + else + { + return false; + } + } + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/Construction/ElementLocation_Tests.cs b/src/XMakeBuildEngine/UnitTests/Construction/ElementLocation_Tests.cs new file mode 100644 index 00000000000..a8ca7a55387 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Construction/ElementLocation_Tests.cs @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the ElementLocation class +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Framework; +using System.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; +using Microsoft.Build.UnitTests.BackEnd; +using System.Xml; +using System.IO; +using System.Reflection; + +namespace Microsoft.Build.UnitTests.Construction +{ + /// + /// Tests for the ElementLocation class + /// + [TestClass] + public class ElementLocation_Tests + { + /// + /// Path to the common targets + /// + private string _pathToCommonTargets = Path.Combine(FrameworkLocationHelper.PathToDotNetFrameworkV45, "microsoft.common.targets"); + + /// + /// Tests constructor specifying only file. + /// + [TestMethod] + public void ConstructorTest1() + { + IElementLocation location = ElementLocation.Create("file", 65536, 0); + Assert.AreEqual("file", location.File); + Assert.AreEqual(65536, location.Line); + Assert.AreEqual(0, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("RegularElementLocation")); + } + + /// + /// Tests constructor specifying only file. + /// + [TestMethod] + public void ConstructorTest2() + { + IElementLocation location = ElementLocation.Create("file", 0, 65536); + Assert.AreEqual("file", location.File); + Assert.AreEqual(0, location.Line); + Assert.AreEqual(65536, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("RegularElementLocation")); + } + + /// + /// Tests constructor specifying only file. + /// + [TestMethod] + public void ConstructorTest3() + { + IElementLocation location = ElementLocation.Create("file", 65536, 65537); + Assert.AreEqual("file", location.File); + Assert.AreEqual(65536, location.Line); + Assert.AreEqual(65537, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("RegularElementLocation")); + } + + /// + /// Test equality + /// + [TestMethod] + public void Equality() + { + IElementLocation location1 = ElementLocation.Create("file", 65536, 65537); + IElementLocation location2 = ElementLocation.Create("file", 0, 1); + IElementLocation location3 = ElementLocation.Create("file", 0, 65537); + IElementLocation location4 = ElementLocation.Create("file", 65536, 1); + IElementLocation location5 = ElementLocation.Create("file", 0, 1); + IElementLocation location6 = ElementLocation.Create("file", 65536, 65537); + + Assert.AreEqual(true, location1.Equals(location6)); + Assert.AreEqual(true, location2.Equals(location5)); + Assert.AreEqual(false, location3.Equals(location1)); + Assert.AreEqual(false, location4.Equals(location2)); + Assert.AreEqual(false, location4.Equals(location6)); + } + + /// + /// Check it will use large element location when it should. + /// Using file as BIZARRELY XmlTextReader+StringReader crops or trims. + /// + [TestMethod] + public void TestLargeElementLocationUsedLargeColumn() + { + string file = null; + + try + { + file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents("\r\n") + new string(' ', 70000) + @""); + + ProjectRootElement projectXml = ProjectRootElement.Open(file); + } + catch (InvalidProjectFileException ex) + { + Assert.AreEqual(70012, ex.ColumnNumber); + Assert.AreEqual(2, ex.LineNumber); + } + finally + { + File.Delete(file); + } + } + + /// + /// Check it will use large element location when it should. + /// Using file as BIZARRELY XmlTextReader+StringReader crops or trims. + /// + [TestMethod] + public void TestLargeElementLocationUsedLargeLine() + { + string file = null; + + try + { + string longstring = String.Empty; + + for (int i = 0; i < 7000; i++) + { + longstring += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; + } + + file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents("\r\n") + longstring + @" "); + + ProjectRootElement projectXml = ProjectRootElement.Open(file); + } + catch (InvalidProjectFileException ex) + { + Assert.AreEqual(70002, ex.LineNumber); + Assert.AreEqual(2, ex.ColumnNumber); + } + finally + { + File.Delete(file); + } + } + + /// + /// Tests serialization. + /// + [TestMethod] + public void SerializationTest() + { + IElementLocation location = ElementLocation.Create("file", 65536, 65537); + + TranslationHelpers.GetWriteTranslator().Translate(ref location, ElementLocation.FactoryForDeserialization); + IElementLocation deserializedLocation = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedLocation, ElementLocation.FactoryForDeserialization); + + Assert.AreEqual(location.File, deserializedLocation.File); + Assert.AreEqual(location.Line, deserializedLocation.Line); + Assert.AreEqual(location.Column, deserializedLocation.Column); + Assert.IsTrue(location.GetType().FullName.Contains("RegularElementLocation")); + } + + /// + /// Tests constructor specifying file, line and column. + /// + [TestMethod] + public void ConstructorWithIndicesTest_SmallElementLocation() + { + IElementLocation location = ElementLocation.Create("file", 65535, 65534); + Assert.AreEqual("file", location.File); + Assert.AreEqual(65535, location.Line); + Assert.AreEqual(65534, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("SmallElementLocation")); + } + + /// + /// Tests constructor specifying file, negative line, column + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ConstructorWithNegativeIndicesTest1() + { + IElementLocation location = ElementLocation.Create("file", -1, 2); + } + + /// + /// Tests constructor specifying file, line, negative column + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void ConstructorWithNegativeIndicesTest2n() + { + IElementLocation location = ElementLocation.Create("file", 1, -2); + } + + /// + /// Tests constructor with invalid null file. + /// + [TestMethod] + public void ConstructorTestNullFile() + { + IElementLocation location = ElementLocation.Create(null); + Assert.AreEqual(location.File, String.Empty); + } + + /// + /// Tests constructor specifying only file. + /// + [TestMethod] + public void ConstructorTest1_SmallElementLocation() + { + IElementLocation location = ElementLocation.Create("file", 65535, 0); + Assert.AreEqual("file", location.File); + Assert.AreEqual(65535, location.Line); + Assert.AreEqual(0, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("SmallElementLocation")); + } + + /// + /// Tests constructor specifying only file. + /// + [TestMethod] + public void ConstructorTest2_SmallElementLocation() + { + IElementLocation location = ElementLocation.Create("file", 0, 65535); + Assert.AreEqual("file", location.File); + Assert.AreEqual(0, location.Line); + Assert.AreEqual(65535, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("SmallElementLocation")); + } + + /// + /// Tests constructor specifying only file. + /// + [TestMethod] + public void ConstructorTest3_SmallElementLocation() + { + IElementLocation location = ElementLocation.Create("file", 65535, 65534); + Assert.AreEqual("file", location.File); + Assert.AreEqual(65535, location.Line); + Assert.AreEqual(65534, location.Column); + Assert.IsTrue(location.GetType().FullName.Contains("SmallElementLocation")); + } + + /// + /// Tests serialization. + /// + [TestMethod] + public void SerializationTest_SmallElementLocation() + { + IElementLocation location = ElementLocation.Create("file", 65535, 2); + + TranslationHelpers.GetWriteTranslator().Translate(ref location, ElementLocation.FactoryForDeserialization); + IElementLocation deserializedLocation = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedLocation, ElementLocation.FactoryForDeserialization); + + Assert.AreEqual(location.File, deserializedLocation.File); + Assert.AreEqual(location.Line, deserializedLocation.Line); + Assert.AreEqual(location.Column, deserializedLocation.Column); + Assert.IsTrue(location.GetType().FullName.Contains("SmallElementLocation")); + } + + /// + /// Test many of the getters + /// + [TestMethod] + public void LocationStringsMedleyReadOnlyLoad() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + foo bar + + + + + + + + +

+ + + + + + + + + + +

+ + + + + + "); + + string readWriteLoadLocations = GetLocations(content, readOnly: false); + string readOnlyLoadLocations = GetLocations(content, readOnly: true); + + Console.WriteLine(readWriteLoadLocations); + + Helpers.VerifyAssertLineByLine(readWriteLoadLocations, readOnlyLoadLocations); + } + + ///

+ /// Save read only fails + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SaveReadOnly1() + { + var doc = new XmlDocumentWithLocation(loadAsReadOnly: true); + doc.Load(_pathToCommonTargets); + doc.Save(FileUtilities.GetTemporaryFile()); + } + + /// + /// Save read only fails + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SaveReadOnly2() + { + var doc = new XmlDocumentWithLocation(loadAsReadOnly: true); + doc.Load(_pathToCommonTargets); + doc.Save(new MemoryStream()); + } + + /// + /// Save read only fails + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SaveReadOnly3() + { + var doc = new XmlDocumentWithLocation(loadAsReadOnly: true); + doc.Load(_pathToCommonTargets); + doc.Save(new StringWriter()); + } + + /// + /// Save read only fails + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SaveReadOnly4() + { + var doc = new XmlDocumentWithLocation(loadAsReadOnly: true); + doc.Load(_pathToCommonTargets); + doc.Save(XmlWriter.Create(FileUtilities.GetTemporaryFile())); + } + + /// + /// Get location strings for the content, loading as readonly if specified + /// + private string GetLocations(string content, bool readOnly) + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + File.WriteAllText(file, content); + var doc = new XmlDocumentWithLocation(loadAsReadOnly: readOnly); + doc.Load(file); + var allNodes = doc.SelectNodes("//*|//@*"); + + string locations = String.Empty; + foreach (var node in allNodes) + { + foreach (var property in node.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + if (property.Name.Equals("Location")) + { + var value = ((ElementLocation)property.GetValue(node, null)); + + if (value != null) // null means attribute is not present + { + locations += ((XmlNode)node).Name + "==" + ((XmlNode)node).Value ?? String.Empty + ": " + value.LocationString + "\r\n"; + } + } + } + } + + locations = locations.Replace(file, "c:\\foo\\bar.csproj"); + + return locations; + } + finally + { + File.Delete(file); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Construction/SolutionFile_Tests.cs b/src/XMakeBuildEngine/UnitTests/Construction/SolutionFile_Tests.cs new file mode 100644 index 00000000000..a10e34ab631 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Construction/SolutionFile_Tests.cs @@ -0,0 +1,1748 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; +using ResourceUtilities = Microsoft.Build.Shared.ResourceUtilities; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.Construction +{ + [TestClass] + public class SolutionFile_Tests + { + /// + /// Test just the most basic, plain vanilla first project line. + /// + [TestMethod] + public void BasicParseFirstProjectLine() + { + SolutionFile p = new SolutionFile(); + p.FullPath = "c:\\foo.sln"; + ProjectInSolution proj = new ProjectInSolution(p); + + p.ParseFirstProjectLine + ( + "Project(\"{Project GUID}\") = \"Project name\", \"Relative path to project file\", \"Unique name-GUID\"", + proj + ); + Assert.AreEqual(SolutionProjectType.Unknown, proj.ProjectType); + Assert.AreEqual("Project name", proj.ProjectName); + Assert.AreEqual("Relative path to project file", proj.RelativePath); + Assert.AreEqual("Unique name-GUID", proj.ProjectGuid); + } + + /// + /// Test that the first project line of a project with the C++ project guid and an + /// extension of vcproj is seen as invalid. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ParseFirstProjectLine_VC() + { + SolutionFile p = new SolutionFile(); + p.FullPath = "c:\\foo.sln"; + ProjectInSolution proj = new ProjectInSolution(p); + + p.ParseFirstProjectLine + ( + "Project(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = \"Project name.vcproj\", \"Relative path\\to\\Project name.vcproj\", \"Unique name-GUID\"", + proj + ); + + Assert.Fail("Should not get here"); + } + + /// + /// Test that the first project line of a project with the C++ project guid and an + /// arbitrary extension is seen as valid -- we assume that all C++ projects except + /// .vcproj are MSBuild format. + /// + [TestMethod] + public void ParseFirstProjectLine_VC2() + { + SolutionFile p = new SolutionFile(); + p.FullPath = "c:\\foo.sln"; + ProjectInSolution proj = new ProjectInSolution(p); + + p.ParseFirstProjectLine + ( + "Project(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = \"Project name.myvctype\", \"Relative path\\to\\Project name.myvctype\", \"Unique name-GUID\"", + proj + ); + Assert.AreEqual(SolutionProjectType.KnownToBeMSBuildFormat, proj.ProjectType); + Assert.AreEqual("Project name.myvctype", proj.ProjectName); + Assert.AreEqual("Relative path\\to\\Project name.myvctype", proj.RelativePath); + Assert.AreEqual("Unique name-GUID", proj.ProjectGuid); + } + + /// + /// A slightly more complicated test where there is some different whitespace. + /// + [TestMethod] + public void ParseFirstProjectLineWithDifferentSpacing() + { + SolutionFile p = new SolutionFile(); + p.FullPath = "c:\\foo.sln"; + ProjectInSolution proj = new ProjectInSolution(p); + + p.ParseFirstProjectLine + ( + "Project(\" {Project GUID} \") = \" Project name \", \" Relative path to project file \" , \" Unique name-GUID \"", + proj + ); + Assert.AreEqual(SolutionProjectType.Unknown, proj.ProjectType); + Assert.AreEqual("Project name", proj.ProjectName); + Assert.AreEqual("Relative path to project file", proj.RelativePath); + Assert.AreEqual("Unique name-GUID", proj.ProjectGuid); + } + + /// + /// First project line with an empty project name. This is somewhat malformed, but we should + /// still behave reasonably instead of crashing. + /// + [TestMethod] + public void ParseFirstProjectLine_InvalidProject() + { + SolutionFile p = new SolutionFile(); + p.FullPath = "c:\\foo.sln"; + ProjectInSolution proj = new ProjectInSolution(p); + + p.ParseFirstProjectLine + ( + "Project(\"{Project GUID}\") = \"\", \"src\\.proj\", \"Unique name-GUID\"", + proj + ); + Assert.AreEqual(SolutionProjectType.Unknown, proj.ProjectType); + Assert.IsTrue(proj.ProjectName.StartsWith("EmptyProjectName")); + Assert.AreEqual("src\\.proj", proj.RelativePath); + Assert.AreEqual("Unique name-GUID", proj.ProjectGuid); + } + + /// + /// Test ParseEtpProject function. + /// + [TestMethod] + public void ParseEtpProject() + { + string proj1Path = Path.Combine(Path.GetTempPath(), "someproj.etp"); + try + { + // Create the first .etp project file + string etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + ClassLibrary2.csproj + + + + + ClassLibrary2.csproj + {73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE} + + + + "; + + File.WriteAllText(proj1Path, etpProjContent); + + // Create the SolutionFile object + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 8.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + //Project should get added to the solution + Assert.AreEqual(solution.ProjectsInOrder[0].RelativePath, @"someproj.etp"); + Assert.AreEqual(solution.ProjectsInOrder[1].RelativePath, @"ClassLibrary2.csproj"); + } + // Delete the files created during the test + finally + { + File.Delete(proj1Path); + } + } + + /// + /// Test CanBeMSBuildFile + /// + [TestMethod] + public void CanBeMSBuildFile() + { + string proj1Path = Path.Combine(Path.GetTempPath(), "someproj.etp"); + string proj2Path = Path.Combine(Path.GetTempPath(), "someproja.proj"); + try + { + // Create the first .etp project file + string etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + ClassLibrary2.csproj + + + + + ClassLibrary2.csproj + {73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE} + + + + "; + + string genericProj = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + "); + + File.WriteAllText(proj1Path, etpProjContent); + File.WriteAllText(proj2Path, genericProj); + + // Create the SolutionFile object + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 8.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject + Project('{NNNNNNNN-9925-4D57-9DAF-E0A9D936ABDB}') = 'someproja', 'someproja.proj', '{CCCCCCCC-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + ProjectInSolution project = (ProjectInSolution)solution.ProjectsByGuid["{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}"]; + ProjectInSolution project2 = (ProjectInSolution)solution.ProjectsByGuid["{CCCCCCCC-9925-4D57-9DAF-E0A9D936ABDB}"]; + string error = null; + Assert.IsFalse(project.CanBeMSBuildProjectFile(out error)); + Assert.IsTrue(project2.CanBeMSBuildProjectFile(out error)); + } + // Delete the files created during the test + finally + { + File.Delete(proj1Path); + File.Delete(proj2Path); + } + } + + /// + /// Test ParseEtpProject function. + /// + [TestMethod] + public void ParseNestedEtpProjectSingleLevel() + { + string proj1Path = Path.Combine(Path.GetTempPath(), "someproj.etp"); + string proj2Path = Path.Combine(Path.GetTempPath(), "someproj2.etp"); + try + { + // Create the first .etp project file + string etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + someproj2.etp + {73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE} + + + + "; + + File.WriteAllText(proj1Path, etpProjContent); + + // Create the second .etp project file + etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + ClassLibrary1.csproj + {83D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FF} + + + + "; + + File.WriteAllText(proj2Path, etpProjContent); + + // Create the SolutionFile object + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 8.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + //Project should get added to the solution + Assert.AreEqual(solution.ProjectsInOrder[0].RelativePath, @"someproj.etp"); + Assert.AreEqual(solution.ProjectsInOrder[1].RelativePath, @"someproj2.etp"); + Assert.AreEqual(solution.ProjectsInOrder[2].RelativePath, @"ClassLibrary1.csproj"); + } + // Delete the files created during the test + finally + { + File.Delete(proj1Path); + File.Delete(proj2Path); + } + } + + [TestMethod] + public void TestVSAndSolutionVersionParsing() + { + // Create the SolutionFile object + string solutionFileContentsPriorToDev12 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionPriorToDev12 = ParseSolutionHelper(solutionFileContentsPriorToDev12); + + Assert.AreEqual(solutionPriorToDev12.Version, 11); + Assert.AreEqual(solutionPriorToDev12.VisualStudioVersion, 10); + + // Create the SolutionFile object + string solutionFileContentsDev12 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = 12.0.20311.0 VSPRO_PLATFORM + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12 = ParseSolutionHelper(solutionFileContentsDev12); + + Assert.AreEqual(solutionDev12.Version, 11); + Assert.AreEqual(solutionDev12.VisualStudioVersion, 12); + + // Test parsing of corrupted VisualStudioVersion lines + + // Version number deleted + string solutionFileContentsDev12Corrupted1 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = VSPRO_PLATFORM + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12Corrupted1 = ParseSolutionHelper(solutionFileContentsDev12Corrupted1); + Assert.AreEqual(solutionDev12Corrupted1.Version, 11); + Assert.AreEqual(solutionDev12Corrupted1.VisualStudioVersion, 10); + + // Remove version number and VSPRO_PLATFORM tag + string solutionFileContentsDev12Corrupted2 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12Corrupted2 = ParseSolutionHelper(solutionFileContentsDev12Corrupted2); + Assert.AreEqual(solutionDev12Corrupted2.Version, 11); + Assert.AreEqual(solutionDev12Corrupted2.VisualStudioVersion, 10); + + // Switch positions between VSPRO_PLATFORM tag and version number + string solutionFileContentsDev12Corrupted3 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = VSPRO_PLATFORM 12.0.20311.0 + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12Corrupted3 = ParseSolutionHelper(solutionFileContentsDev12Corrupted3); + Assert.AreEqual(solutionDev12Corrupted3.Version, 11); + Assert.AreEqual(solutionDev12Corrupted3.VisualStudioVersion, 10); + + // Add a number of spaces before version number and glue it together with VSPRO_PLATFORM + string solutionFileContentsDev12Corrupted4 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = 12.0.20311.0VSPRO_PLATFORM + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12Corrupted4 = ParseSolutionHelper(solutionFileContentsDev12Corrupted4); + Assert.AreEqual(solutionDev12Corrupted4.Version, 11); + Assert.AreEqual(solutionDev12Corrupted4.VisualStudioVersion, 10); + + // Corrupted version number + string solutionFileContentsDev12Corrupted5 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = ...12..0,.20311.0 VSPRO_PLATFORM + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12Corrupted5 = ParseSolutionHelper(solutionFileContentsDev12Corrupted5); + Assert.AreEqual(solutionDev12Corrupted5.Version, 11); + Assert.AreEqual(solutionDev12Corrupted5.VisualStudioVersion, 10); + + // Add a number of spaces before version number + string solutionFileContentsDev12Corrupted6 = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + VisualStudioVersion = 12.0.20311.0 VSPRO_PLATFORM + MinimumVisualStudioVersion = 10.0.40219.1 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + + SolutionFile solutionDev12Corrupted6 = ParseSolutionHelper(solutionFileContentsDev12Corrupted6); + Assert.AreEqual(solutionDev12Corrupted6.Version, 11); + Assert.AreEqual(solutionDev12Corrupted6.VisualStudioVersion, 12); + } + + /// + /// Test ParseEtpProject function. + /// + [TestMethod] + public void ParseNestedEtpProjectMultipleLevel() + { + string proj1Path = Path.Combine(Path.GetTempPath(), "someproj.etp"); + string proj2Path = Path.Combine(Path.GetTempPath(), "someproj2.etp"); + string proj3Path = Path.Combine(Path.GetTempPath(), "ETPProjUpgradeTest" + Path.DirectorySeparatorChar + "someproj3.etp"); + try + { + // Create the first .etp project file + string etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + someproj2.etp + {73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE} + + + + "; + + File.WriteAllText(proj1Path, etpProjContent); + + // Create the second .etp project file + etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + ETPProjUpgradeTest\someproj3.etp + {83D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FF} + + + + "; + + File.WriteAllText(proj2Path, etpProjContent); + + // Create the thirsd .etp project file + etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + ..\SomeFolder\ClassLibrary1.csproj + {83D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FF} + + + + "; + //Create the directory for the third project + Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "ETPProjUpgradeTest")); + File.WriteAllText(proj3Path, etpProjContent); + + // Create the SolutionFile object + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 8.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + //Project should get added to the solution + Assert.AreEqual(solution.ProjectsInOrder[0].RelativePath, @"someproj.etp"); + Assert.AreEqual(solution.ProjectsInOrder[1].RelativePath, @"someproj2.etp"); + Assert.AreEqual(solution.ProjectsInOrder[2].RelativePath, @"ETPProjUpgradeTest\someproj3.etp"); + Assert.AreEqual(solution.ProjectsInOrder[3].RelativePath, @"ETPProjUpgradeTest\..\SomeFolder\ClassLibrary1.csproj"); + } + // Delete the files created during the test + finally + { + File.Delete(proj1Path); + File.Delete(proj2Path); + File.Delete(proj3Path); + } + } + + /// + /// Ensure that a malformed .etp proj file listed in the .SLN file results in an + /// InvalidProjectFileException. + /// + [TestMethod] + public void MalformedEtpProjFile() + { + string proj1Path = Path.Combine(Path.GetTempPath(), "someproj.etp"); + try + { + // Create the .etp project file + // Note the is missing + string etpProjContent = @" + + + Microsoft Visual Studio Application Template File + 1.00 + + + ClassLibrary2\ClassLibrary2.csproj + + + + + ClassLibrary2\ClassLibrary2.csproj + {73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE} + + + + "; + + File.WriteAllText(proj1Path, etpProjContent); + + // Create the SolutionFile object + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 8.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + string errCode, ignoredKeyword; + ResourceUtilities.FormatResourceString(out errCode, out ignoredKeyword, "Shared.InvalidProjectFile", + "someproj.etp", String.Empty); + foreach (string warningString in solution.SolutionParserWarnings) + { + Console.WriteLine(warningString.ToString()); + } + Assert.IsTrue(solution.SolutionParserErrorCodes[0].ToString().Contains(errCode)); + } + // Delete the files created suring the test + finally + { + File.Delete(proj1Path); + } + } + + /// + /// Ensure that a missing .etp proj file listed in the .SLN file results in an + /// InvalidProjectFileException. + /// + [TestMethod] + public void MissingEtpProjFile() + { + string proj1Path = Path.Combine(Path.GetTempPath(), "someproj.etp"); + // Create the solution file + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 8.00 + Project('{FE3BBBB6-72D5-11D2-9ACE-00C04F79A2A4}') = 'someproj', 'someproj.etp', '{AD0F3D02-9925-4D57-9DAF-E0A9D936ABDB}' + ProjectSection(ProjectDependencies) = postProject + EndProjectSection + EndProject"; + // Delete the someproj.etp file if it exists + File.Delete(proj1Path); + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + string errCode, ignoredKeyword; + ResourceUtilities.FormatResourceString(out errCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded", + "someproj.etp", String.Empty); + Assert.IsTrue(solution.SolutionParserErrorCodes[0].ToString().Contains(errCode)); + } + + /// + /// Test some characters that are valid in a file name but that also could be + /// considered a delimiter by a parser. Does quoting work for special characters? + /// + [TestMethod] + public void ParseFirstProjectLineWhereProjectNameHasSpecialCharacters() + { + SolutionFile p = new SolutionFile(); + p.FullPath = "c:\\foo.sln"; + ProjectInSolution proj = new ProjectInSolution(p); + + p.ParseFirstProjectLine + ( + "Project(\"{Project GUID}\") = \"MyProject,(=IsGreat)\", \"Relative path to project file\" , \"Unique name-GUID\"", + proj + ); + Assert.AreEqual(SolutionProjectType.Unknown, proj.ProjectType); + Assert.AreEqual("MyProject,(=IsGreat)", proj.ProjectName); + Assert.AreEqual("Relative path to project file", proj.RelativePath); + Assert.AreEqual("Unique name-GUID", proj.ProjectGuid); + } + + /// + /// Helper method to create a SolutionFile object, and call it to parse the SLN file + /// represented by the string contents passed in. + /// + /// + /// + static internal SolutionFile ParseSolutionHelper + ( + string solutionFileContents + ) + { + solutionFileContents = solutionFileContents.Replace('\'', '"'); + StreamReader sr = StreamHelpers.StringToStreamReader(solutionFileContents); + + SolutionFile sp = new SolutionFile(); + sp.SolutionFileDirectory = Path.GetTempPath(); + sp.SolutionReader = sr; + string tmpFileName = FileUtilities.GetTemporaryFile(); + sp.FullPath = tmpFileName + ".sln"; + // This file is not expected to exist at this point, so make sure it doesn't + File.Delete(sp.FullPath); + sp.ParseSolution(); + // Clean up the temporary file that got created with this call + File.Delete(tmpFileName); + return sp; + } + + /// + /// Ensure that a bogus version stamp in the .SLN file results in an + /// InvalidProjectFileException. + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void BadVersionStamp() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version a.b + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Expected version numbers less than 7 to cause an invalid project file exception. + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void VersionTooLow() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 6.0 + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Ensure that an unsupported version greater than the current maximum (10) in the .SLN file results in a + /// comment indicating we will try and continue + /// + [TestMethod] + public void UnsupportedVersion() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 999.0 + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + Assert.IsTrue(solution.SolutionParserComments.Count == 1, "Expected the solution parser to contain one comment"); + Assert.IsTrue(String.Equals((string)solution.SolutionParserComments[0], ResourceUtilities.FormatResourceString("UnrecognizedSolutionComment", "999"), StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void Version9() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.0 + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(9, solution.Version); + } + + [TestMethod] + public void Version10() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 10.0 + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(10, solution.Version); + } + + /// + /// Test to parse a very basic .sln file to validate that description property in a solution file + /// is properly handled. + /// + [TestMethod] + public void ParseSolutionFileWithDescriptionInformation() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'AnyProject', 'AnyProject\AnyProject.csproj', '{2CAB0FBD-15D8-458B-8E63-1B5B840E9798}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + Description = Some description of this solution + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + try + { + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + catch (Exception ex) + { + Assert.Fail("Failed to parse solution containing description information. Error: " + ex.Message); + } + } + + /// + /// Tests the parsing of a very basic .SLN file with three independent projects. + /// + [TestMethod] + public void BasicSolution() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'ConsoleApplication1', 'ConsoleApplication1\ConsoleApplication1.vbproj', '{AB3413A6-D689-486D-B7F0-A095371B3F13}' + EndProject + Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'vbClassLibrary', 'vbClassLibrary\vbClassLibrary.vbproj', '{BA333A76-4511-47B8-8DF4-CA51C303AD0B}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{DEBCE986-61B9-435E-8018-44B9EF751655}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.Build.0 = Release|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Release|AnyCPU.Build.0 = Release|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(3, solution.ProjectsInOrder.Count); + + Assert.AreEqual(SolutionProjectType.KnownToBeMSBuildFormat, solution.ProjectsInOrder[0].ProjectType); + Assert.AreEqual("ConsoleApplication1", solution.ProjectsInOrder[0].ProjectName); + Assert.AreEqual(@"ConsoleApplication1\ConsoleApplication1.vbproj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{AB3413A6-D689-486D-B7F0-A095371B3F13}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + Assert.AreEqual("ConsoleApplication1", solution.ProjectsInOrder[0].GetUniqueProjectName()); + + Assert.AreEqual(SolutionProjectType.KnownToBeMSBuildFormat, solution.ProjectsInOrder[1].ProjectType); + Assert.AreEqual("vbClassLibrary", solution.ProjectsInOrder[1].ProjectName); + Assert.AreEqual(@"vbClassLibrary\vbClassLibrary.vbproj", solution.ProjectsInOrder[1].RelativePath); + Assert.AreEqual("{BA333A76-4511-47B8-8DF4-CA51C303AD0B}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[1].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[1].ParentProjectGuid); + Assert.AreEqual("vbClassLibrary", solution.ProjectsInOrder[1].GetUniqueProjectName()); + + Assert.AreEqual(SolutionProjectType.KnownToBeMSBuildFormat, solution.ProjectsInOrder[2].ProjectType); + Assert.AreEqual("ClassLibrary1", solution.ProjectsInOrder[2].ProjectName); + Assert.AreEqual(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[2].RelativePath); + Assert.AreEqual("{DEBCE986-61B9-435E-8018-44B9EF751655}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[2].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[2].ParentProjectGuid); + Assert.AreEqual("ClassLibrary1", solution.ProjectsInOrder[2].GetUniqueProjectName()); + } + + /// + /// Exercises solution folders, and makes sure that samely named projects in different + /// solution folders will get correctly uniquified. + /// + [TestMethod] + public void SolutionFolders() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{34E0D07D-CF8F-459D-9449-C4188D8C5564}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{E0F97730-25D2-418A-A7BD-02CAFDC6E470}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj', '{A5EE8128-B08E-4533-86C5-E46714981680}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySubSlnFolder', 'MySubSlnFolder', '{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary2', 'ClassLibrary2\ClassLibrary2.csproj', '{6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.Build.0 = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.Build.0 = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A5EE8128-B08E-4533-86C5-E46714981680} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4} = {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(5, solution.ProjectsInOrder.Count); + + Assert.AreEqual(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{34E0D07D-CF8F-459D-9449-C4188D8C5564}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + Assert.AreEqual("ClassLibrary1", solution.ProjectsInOrder[0].GetUniqueProjectName()); + + Assert.AreEqual(SolutionProjectType.SolutionFolder, solution.ProjectsInOrder[1].ProjectType); + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[1].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[1].ParentProjectGuid); + Assert.AreEqual("MySlnFolder", solution.ProjectsInOrder[1].GetUniqueProjectName()); + + Assert.AreEqual(@"MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[2].RelativePath); + Assert.AreEqual("{A5EE8128-B08E-4533-86C5-E46714981680}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[2].Dependencies.Count); + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[2].ParentProjectGuid); + Assert.AreEqual(@"MySlnFolder\ClassLibrary1", solution.ProjectsInOrder[2].GetUniqueProjectName()); + + Assert.AreEqual(SolutionProjectType.SolutionFolder, solution.ProjectsInOrder[3].ProjectType); + Assert.AreEqual("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}", solution.ProjectsInOrder[3].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[3].Dependencies.Count); + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[3].ParentProjectGuid); + Assert.AreEqual(@"MySlnFolder\MySubSlnFolder", solution.ProjectsInOrder[3].GetUniqueProjectName()); + + Assert.AreEqual(@"ClassLibrary2\ClassLibrary2.csproj", solution.ProjectsInOrder[4].RelativePath); + Assert.AreEqual("{6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}", solution.ProjectsInOrder[4].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[4].Dependencies.Count); + Assert.AreEqual("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}", solution.ProjectsInOrder[4].ParentProjectGuid); + Assert.AreEqual(@"MySlnFolder\MySubSlnFolder\ClassLibrary2", solution.ProjectsInOrder[4].GetUniqueProjectName()); + } + + /// + /// Tests situation where there's a nonexistent project listed in the solution folders. We should + /// error with a useful message. + /// + [TestMethod] + public void MissingNestedProject() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{E0F97730-25D2-418A-A7BD-02CAFDC6E470}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj', '{A5EE8128-B08E-4533-86C5-E46714981680}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.Build.0 = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.Build.0 = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A5EE8128-B08E-4533-86C5-E46714981680} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + EndGlobalSection + EndGlobal + "; + + try + { + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + catch (InvalidProjectFileException e) + { + Assert.AreEqual("MSB5023", e.ErrorCode); + Assert.IsTrue(e.Message.Contains("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}")); + return; + } + + // Should not get here + Assert.Fail(); + } + + + /// + /// Verifies that we correctly identify solution folders and mercury non-buildable projects both as + /// "non-building" + /// + [TestMethod] + public void BuildableProjects() + { + string solutionFileContents = + @" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21119.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project('{D954291E-2A0B-460D-934E-DC6B0785DB48}') = 'HubApp2', 'HubApp2\HubApp2.scproj', '{892B5932-9AA8-46F9-A857-8967DCDBE4F5}' +EndProject +Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'HubApp2.Store', 'HubApp2\Store\HubApp2.Store.vcxproj', '{A5526AEA-E0A2-496D-94B7-2BBE835C83F8}' +EndProject +Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'Shared', 'HubApp2\Shared\Shared.vcxitems', '{FF6AEDF3-950A-46DD-910B-52BC69B9C99A}' +EndProject +Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'HubApp2.Phone', 'HubApp2\Phone\HubApp2.Phone.vcxproj', '{024E8607-06B0-440D-8741-5A888DC4B176}' +EndProject +Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{E0F97730-25D2-418A-A7BD-02CAFDC6E470}' +EndProject +Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{A5EE8128-B08E-4533-86C5-E46714981680}' +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|ARM.ActiveCfg = Debug|ARM + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|ARM.Build.0 = Debug|ARM + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|ARM.Deploy.0 = Debug|ARM + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Mixed Platforms.Deploy.0 = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Win32.ActiveCfg = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Win32.Build.0 = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|Win32.Deploy.0 = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|x64.ActiveCfg = Debug|x64 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|x64.Build.0 = Debug|x64 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|x64.Deploy.0 = Debug|x64 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|x86.ActiveCfg = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|x86.Build.0 = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Debug|x86.Deploy.0 = Debug|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Any CPU.ActiveCfg = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|ARM.ActiveCfg = Release|ARM + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|ARM.Build.0 = Release|ARM + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|ARM.Deploy.0 = Release|ARM + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Mixed Platforms.Build.0 = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Mixed Platforms.Deploy.0 = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Win32.ActiveCfg = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Win32.Build.0 = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|Win32.Deploy.0 = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|x64.ActiveCfg = Release|x64 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|x64.Build.0 = Release|x64 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|x64.Deploy.0 = Release|x64 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|x86.ActiveCfg = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|x86.Build.0 = Release|Win32 + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8}.Release|x86.Deploy.0 = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|ARM.ActiveCfg = Debug|ARM + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|ARM.Build.0 = Debug|ARM + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|ARM.Deploy.0 = Debug|ARM + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Mixed Platforms.Deploy.0 = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Win32.ActiveCfg = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Win32.Build.0 = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|Win32.Deploy.0 = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|x64.ActiveCfg = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|x86.ActiveCfg = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|x86.Build.0 = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Debug|x86.Deploy.0 = Debug|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Any CPU.ActiveCfg = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|ARM.ActiveCfg = Release|ARM + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|ARM.Build.0 = Release|ARM + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|ARM.Deploy.0 = Release|ARM + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Mixed Platforms.Build.0 = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Mixed Platforms.Deploy.0 = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Win32.ActiveCfg = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Win32.Build.0 = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|Win32.Deploy.0 = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|x64.ActiveCfg = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|x86.ActiveCfg = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|x86.Build.0 = Release|Win32 + {024E8607-06B0-440D-8741-5A888DC4B176}.Release|x86.Deploy.0 = Release|Win32 + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|x86.ActiveCfg = Debug|Win32 + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|x86.Build.0 = Debug|Win32 + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|x86.Deploy.0 = Debug|Win32 + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|x86.ActiveCfg = Release|Win32 + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|x86.Build.0 = Release|Win32 + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|x86.Deploy.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A5526AEA-E0A2-496D-94B7-2BBE835C83F8} = {892B5932-9AA8-46F9-A857-8967DCDBE4F5} + {FF6AEDF3-950A-46DD-910B-52BC69B9C99A} = {892B5932-9AA8-46F9-A857-8967DCDBE4F5} + {024E8607-06B0-440D-8741-5A888DC4B176} = {892B5932-9AA8-46F9-A857-8967DCDBE4F5} + EndGlobalSection +EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(6, solution.ProjectsInOrder.Count); + + Assert.AreEqual("{892B5932-9AA8-46F9-A857-8967DCDBE4F5}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual("HubApp2", solution.ProjectsInOrder[0].ProjectName); + Assert.IsFalse(SolutionFile.IsBuildableProject(solution.ProjectsInOrder[0])); + + Assert.AreEqual("{A5526AEA-E0A2-496D-94B7-2BBE835C83F8}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual("HubApp2.Store", solution.ProjectsInOrder[1].ProjectName); + Assert.IsTrue(SolutionFile.IsBuildableProject(solution.ProjectsInOrder[1])); + + Assert.AreEqual("{FF6AEDF3-950A-46DD-910B-52BC69B9C99A}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual("Shared", solution.ProjectsInOrder[2].ProjectName); + Assert.IsFalse(SolutionFile.IsBuildableProject(solution.ProjectsInOrder[2])); + + Assert.AreEqual("{024E8607-06B0-440D-8741-5A888DC4B176}", solution.ProjectsInOrder[3].ProjectGuid); + Assert.AreEqual("HubApp2.Phone", solution.ProjectsInOrder[3].ProjectName); + Assert.IsTrue(SolutionFile.IsBuildableProject(solution.ProjectsInOrder[3])); + + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[4].ProjectGuid); + Assert.AreEqual("MySlnFolder", solution.ProjectsInOrder[4].ProjectName); + Assert.IsFalse(SolutionFile.IsBuildableProject(solution.ProjectsInOrder[4])); + + // Even though it doesn't have project configurations mapped for all solution configurations, + // it at least has some, so this project should still be marked as "buildable" + Assert.AreEqual("{A5EE8128-B08E-4533-86C5-E46714981680}", solution.ProjectsInOrder[5].ProjectGuid); + Assert.AreEqual("ClassLibrary1", solution.ProjectsInOrder[5].ProjectName); + Assert.IsTrue(SolutionFile.IsBuildableProject(solution.ProjectsInOrder[5])); + } + + /// + /// Verifies that hand-coded project-to-project dependencies listed in the .SLN file + /// are correctly recognized by our solution parser. + /// + [TestMethod] + public void SolutionDependencies() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{05A5AD00-71B5-4612-AF2F-9EA9121C4111}' + ProjectSection(ProjectDependencies) = postProject + {FAB4EE06-6E01-495A-8926-5514599E3DD9} = {FAB4EE06-6E01-495A-8926-5514599E3DD9} + EndProjectSection + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary2', 'ClassLibrary2\ClassLibrary2.csproj', '{7F316407-AE3E-4F26-BE61-2C50D30DA158}' + ProjectSection(ProjectDependencies) = postProject + {FAB4EE06-6E01-495A-8926-5514599E3DD9} = {FAB4EE06-6E01-495A-8926-5514599E3DD9} + {05A5AD00-71B5-4612-AF2F-9EA9121C4111} = {05A5AD00-71B5-4612-AF2F-9EA9121C4111} + EndProjectSection + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary3', 'ClassLibrary3\ClassLibrary3.csproj', '{FAB4EE06-6E01-495A-8926-5514599E3DD9}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Release|Any CPU.Build.0 = Release|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Release|Any CPU.Build.0 = Release|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(3, solution.ProjectsInOrder.Count); + + Assert.AreEqual(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{05A5AD00-71B5-4612-AF2F-9EA9121C4111}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(1, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", (string)solution.ProjectsInOrder[0].Dependencies[0]); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + Assert.AreEqual("ClassLibrary1", solution.ProjectsInOrder[0].GetUniqueProjectName()); + + Assert.AreEqual(@"ClassLibrary2\ClassLibrary2.csproj", solution.ProjectsInOrder[1].RelativePath); + Assert.AreEqual("{7F316407-AE3E-4F26-BE61-2C50D30DA158}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual(2, solution.ProjectsInOrder[1].Dependencies.Count); + Assert.AreEqual("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", (string)solution.ProjectsInOrder[1].Dependencies[0]); + Assert.AreEqual("{05A5AD00-71B5-4612-AF2F-9EA9121C4111}", (string)solution.ProjectsInOrder[1].Dependencies[1]); + Assert.AreEqual(null, solution.ProjectsInOrder[1].ParentProjectGuid); + Assert.AreEqual("ClassLibrary2", solution.ProjectsInOrder[1].GetUniqueProjectName()); + + Assert.AreEqual(@"ClassLibrary3\ClassLibrary3.csproj", solution.ProjectsInOrder[2].RelativePath); + Assert.AreEqual("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[2].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[2].ParentProjectGuid); + Assert.AreEqual("ClassLibrary3", solution.ProjectsInOrder[2].GetUniqueProjectName()); + } + + /// + /// Tests to see that all the data/properties are correctly parsed out of a Venus + /// project in a .SLN. + /// + [TestMethod] + public void VenusProject() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project(`{E24C65DC-7377-472B-9ABA-BC803B73C61A}`) = `C:\WebSites\WebApplication3\`, `C:\WebSites\WebApplication3\`, `{464FD0B9-E335-4677-BE1E-6B2F982F4D86}` + ProjectSection(WebsiteProperties) = preProject + ProjectReferences = `{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSCla;ssLibra;ry1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;` + Frontpage = false + Debug.AspNetCompiler.VirtualPath = `/publishfirst` + Debug.AspNetCompiler.PhysicalPath = `..\rajeev\temp\websites\myfirstwebsite\` + Debug.AspNetCompiler.TargetPath = `..\rajeev\temp\publishfirst\` + Debug.AspNetCompiler.ForceOverwrite = `true` + Debug.AspNetCompiler.Updateable = `false` + Debug.AspNetCompiler.Debug = `true` + Debug.AspNetCompiler.KeyFile = `debugkeyfile.snk` + Debug.AspNetCompiler.KeyContainer = `12345.container` + Debug.AspNetCompiler.DelaySign = `true` + Debug.AspNetCompiler.AllowPartiallyTrustedCallers = `false` + Debug.AspNetCompiler.FixedNames = `debugfixednames` + Release.AspNetCompiler.VirtualPath = `/publishfirst_release` + Release.AspNetCompiler.PhysicalPath = `..\rajeev\temp\websites\myfirstwebsite_release\` + Release.AspNetCompiler.TargetPath = `..\rajeev\temp\publishfirst_release\` + Release.AspNetCompiler.ForceOverwrite = `true` + Release.AspNetCompiler.Updateable = `true` + Release.AspNetCompiler.Debug = `false` + VWDPort = 63496 + EndProjectSection + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|.NET = Debug|.NET + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {464FD0B9-E335-4677-BE1E-6B2F982F4D86}.Debug|.NET.ActiveCfg = Debug|.NET + {464FD0B9-E335-4677-BE1E-6B2F982F4D86}.Debug|.NET.Build.0 = Debug|.NET + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents.Replace('`', '"')); + + Assert.AreEqual(1, solution.ProjectsInOrder.Count); + + Assert.AreEqual(SolutionProjectType.WebProject, solution.ProjectsInOrder[0].ProjectType); + Assert.AreEqual(@"C:\WebSites\WebApplication3\", solution.ProjectsInOrder[0].ProjectName); + Assert.AreEqual(@"C:\WebSites\WebApplication3\", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{464FD0B9-E335-4677-BE1E-6B2F982F4D86}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(2, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + Assert.AreEqual(@"C:\WebSites\WebApplication3\", solution.ProjectsInOrder[0].GetUniqueProjectName()); + + Hashtable aspNetCompilerParameters = solution.ProjectsInOrder[0].AspNetConfigurations; + AspNetCompilerParameters debugAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Debug"]; + AspNetCompilerParameters releaseAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Release"]; + + Assert.AreEqual(@"/publishfirst", debugAspNetCompilerParameters.aspNetVirtualPath); + Assert.AreEqual(@"..\rajeev\temp\websites\myfirstwebsite\", debugAspNetCompilerParameters.aspNetPhysicalPath); + Assert.AreEqual(@"..\rajeev\temp\publishfirst\", debugAspNetCompilerParameters.aspNetTargetPath); + Assert.AreEqual(@"true", debugAspNetCompilerParameters.aspNetForce); + Assert.AreEqual(@"false", debugAspNetCompilerParameters.aspNetUpdateable); + Assert.AreEqual(@"true", debugAspNetCompilerParameters.aspNetDebug); + Assert.AreEqual(@"debugkeyfile.snk", debugAspNetCompilerParameters.aspNetKeyFile); + Assert.AreEqual(@"12345.container", debugAspNetCompilerParameters.aspNetKeyContainer); + Assert.AreEqual(@"true", debugAspNetCompilerParameters.aspNetDelaySign); + Assert.AreEqual(@"false", debugAspNetCompilerParameters.aspNetAPTCA); + Assert.AreEqual(@"debugfixednames", debugAspNetCompilerParameters.aspNetFixedNames); + + Assert.AreEqual(@"/publishfirst_release", releaseAspNetCompilerParameters.aspNetVirtualPath); + Assert.AreEqual(@"..\rajeev\temp\websites\myfirstwebsite_release\", releaseAspNetCompilerParameters.aspNetPhysicalPath); + Assert.AreEqual(@"..\rajeev\temp\publishfirst_release\", releaseAspNetCompilerParameters.aspNetTargetPath); + Assert.AreEqual(@"true", releaseAspNetCompilerParameters.aspNetForce); + Assert.AreEqual(@"true", releaseAspNetCompilerParameters.aspNetUpdateable); + Assert.AreEqual(@"false", releaseAspNetCompilerParameters.aspNetDebug); + Assert.AreEqual("", releaseAspNetCompilerParameters.aspNetKeyFile); + Assert.AreEqual("", releaseAspNetCompilerParameters.aspNetKeyContainer); + Assert.AreEqual("", releaseAspNetCompilerParameters.aspNetDelaySign); + Assert.AreEqual("", releaseAspNetCompilerParameters.aspNetAPTCA); + Assert.AreEqual("", releaseAspNetCompilerParameters.aspNetFixedNames); + + ArrayList aspNetProjectReferences = solution.ProjectsInOrder[0].ProjectReferences; + Assert.AreEqual(2, aspNetProjectReferences.Count); + Assert.AreEqual("{FD705688-88D1-4C22-9BFF-86235D89C2FC}", aspNetProjectReferences[0]); + Assert.AreEqual("{F0726D09-042B-4A7A-8A01-6BED2422BD5D}", aspNetProjectReferences[1]); + } + + /// + /// Tests to see that our solution parser correctly recognizes a Venus project that + /// sits inside a solution folder. + /// + [TestMethod] + public void VenusProjectInASolutionFolder() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\WebSites\WebApplication3\', 'C:\WebSites\WebApplication3\', '{464FD0B9-E335-4677-BE1E-6B2F982F4D86}' + ProjectSection(WebsiteProperties) = preProject + Frontpage = false + AspNetCompiler.VirtualPath = '/webprecompile3' + AspNetCompiler.PhysicalPath = '..\..\WebSites\WebApplication3\' + AspNetCompiler.TargetPath = '..\..\..\rajeev\temp\webprecompile3\' + VWDPort = 63496 + EndProjectSection + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{092FE6E5-71F8-43F7-9C92-30E3124B8A22}' + EndProject + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\WebSites\WebApplication4\', 'C:\WebSites\WebApplication4\', '{947DB39C-77BA-4F7F-A667-0BCD59CE853F}' + ProjectSection(WebsiteProperties) = preProject + Frontpage = false + AspNetCompiler.VirtualPath = '/webprecompile4' + AspNetCompiler.PhysicalPath = '..\..\WebSites\WebApplication4\' + AspNetCompiler.TargetPath = '..\..\..\rajeev\temp\webprecompile4\' + EndProjectSection + EndProject + Global + GlobalSection(NestedProjects) = preSolution + {947DB39C-77BA-4F7F-A667-0BCD59CE853F} = {092FE6E5-71F8-43F7-9C92-30E3124B8A22} + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(3, solution.ProjectsInOrder.Count); + + Assert.AreEqual(SolutionProjectType.WebProject, solution.ProjectsInOrder[0].ProjectType); + Assert.AreEqual(@"C:\WebSites\WebApplication3\", solution.ProjectsInOrder[0].GetUniqueProjectName()); + + Assert.AreEqual(SolutionProjectType.SolutionFolder, solution.ProjectsInOrder[1].ProjectType); + Assert.AreEqual("{092FE6E5-71F8-43F7-9C92-30E3124B8A22}", solution.ProjectsInOrder[1].ProjectGuid); + + Assert.AreEqual(SolutionProjectType.WebProject, solution.ProjectsInOrder[2].ProjectType); + Assert.AreEqual(@"C:\WebSites\WebApplication4\", solution.ProjectsInOrder[2].GetUniqueProjectName()); + Assert.AreEqual("{092FE6E5-71F8-43F7-9C92-30E3124B8A22}", solution.ProjectsInOrder[2].ParentProjectGuid); + } + + /// + /// Make sure the solution configurations get parsed correctly for a simple mixed C#/VC solution + /// + [TestMethod] + public void ParseSolutionConfigurations() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.ActiveCfg = Debug|ARM + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.Build.0 = Debug|ARM + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Win32.ActiveCfg = Release|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(7, solution.SolutionConfigurations.Count); + + List configurationNames = new List(6); + foreach (SolutionConfigurationInSolution configuration in solution.SolutionConfigurations) + { + configurationNames.Add(configuration.FullName); + } + + Assert.IsTrue(configurationNames.Contains("Debug|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Debug|Mixed Platforms")); + Assert.IsTrue(configurationNames.Contains("Debug|Win32")); + Assert.IsTrue(configurationNames.Contains("Release|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Release|Mixed Platforms")); + Assert.IsTrue(configurationNames.Contains("Release|Win32")); + + Assert.AreEqual("Debug", solution.GetDefaultConfigurationName(), "Default solution configuration"); + Assert.AreEqual("Mixed Platforms", solution.GetDefaultPlatformName(), "Default solution platform"); + } + + /// + /// Make sure the solution configurations get parsed correctly for a simple C# application + /// + [TestMethod] + public void ParseSolutionConfigurationsNoMixedPlatform() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|ARM = Release|ARM + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|x86.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|ARM.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|ARM.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(6, solution.SolutionConfigurations.Count); + + List configurationNames = new List(6); + foreach (SolutionConfigurationInSolution configuration in solution.SolutionConfigurations) + { + configurationNames.Add(configuration.FullName); + } + + Assert.IsTrue(configurationNames.Contains("Debug|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Debug|ARM")); + Assert.IsTrue(configurationNames.Contains("Debug|x86")); + Assert.IsTrue(configurationNames.Contains("Release|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Release|ARM")); + Assert.IsTrue(configurationNames.Contains("Release|x86")); + + Assert.AreEqual("Debug", solution.GetDefaultConfigurationName(), "Default solution configuration"); + Assert.AreEqual("Any CPU", solution.GetDefaultPlatformName(), "Default solution platform"); + } + + /// + /// Test some invalid cases for solution configuration parsing. + /// There can be only one '=' character in a sln cfg entry, separating two identical names + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void ParseInvalidSolutionConfigurations1() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any=CPU = Debug|Any=CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Test some invalid cases for solution configuration parsing + /// There can be only one '=' character in a sln cfg entry, separating two identical names + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void ParseInvalidSolutionConfigurations2() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Something|Else + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Test some invalid cases for solution configuration parsing + /// Solution configurations must include the platform part + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void ParseInvalidSolutionConfigurations3() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug = Debug + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Make sure the project configurations in solution configurations get parsed correctly + /// for a simple mixed C#/VC solution + /// + [TestMethod] + public void ParseProjectConfigurationsInSolutionConfigurations1() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Win32.ActiveCfg = Release|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + ProjectInSolution csProject = (ProjectInSolution)solution.ProjectsByGuid["{6185CC21-BE89-448A-B3C0-D1C27112E595}"]; + ProjectInSolution vcProject = (ProjectInSolution)solution.ProjectsByGuid["{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}"]; + + Assert.AreEqual(6, csProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug|AnyCPU", csProject.ProjectConfigurations["Debug|Any CPU"].FullName); + Assert.AreEqual(true, csProject.ProjectConfigurations["Debug|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csProject.ProjectConfigurations["Debug|Mixed Platforms"].FullName); + Assert.AreEqual(true, csProject.ProjectConfigurations["Debug|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Debug|AnyCPU", csProject.ProjectConfigurations["Debug|Win32"].FullName); + Assert.AreEqual(false, csProject.ProjectConfigurations["Debug|Win32"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csProject.ProjectConfigurations["Release|Any CPU"].FullName); + Assert.AreEqual(true, csProject.ProjectConfigurations["Release|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csProject.ProjectConfigurations["Release|Mixed Platforms"].FullName); + Assert.AreEqual(true, csProject.ProjectConfigurations["Release|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csProject.ProjectConfigurations["Release|Win32"].FullName); + Assert.AreEqual(false, csProject.ProjectConfigurations["Release|Win32"].IncludeInBuild); + + Assert.AreEqual(6, vcProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug|Win32", vcProject.ProjectConfigurations["Debug|Any CPU"].FullName); + Assert.AreEqual(false, vcProject.ProjectConfigurations["Debug|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Debug|Win32", vcProject.ProjectConfigurations["Debug|Mixed Platforms"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Debug|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Debug|Win32", vcProject.ProjectConfigurations["Debug|Win32"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Debug|Win32"].IncludeInBuild); + + Assert.AreEqual("Release|Win32", vcProject.ProjectConfigurations["Release|Any CPU"].FullName); + Assert.AreEqual(false, vcProject.ProjectConfigurations["Release|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Release|Win32", vcProject.ProjectConfigurations["Release|Mixed Platforms"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Release|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Release|Win32", vcProject.ProjectConfigurations["Release|Win32"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Release|Win32"].IncludeInBuild); + } + + /// + /// Make sure the project configurations in solution configurations get parsed correctly + /// for a more tricky solution + /// + [TestMethod] + public void ParseProjectConfigurationsInSolutionConfigurations2() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\solutions\WebSite1\', '..\WebSite1\', '{E8E75132-67E4-4D6F-9CAE-8DA4C883F418}' + EndProject + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\solutions\WebSite2\', '..\WebSite2\', '{E8E75132-67E4-4D6F-9CAE-8DA4C883F419}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'NewFolder1', 'NewFolder1', '{54D20FFE-84BE-4066-A51E-B25D040A4235}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'NewFolder2', 'NewFolder2', '{D2633E4D-46FF-4C4E-8340-4BC7CDF78615}' + EndProject + Project('{8BC9CEB9-8B4A-11D0-8D11-00A0C91BC942}') = 'MSBuild.exe', '..\..\dd\binaries.x86dbg\bin\i386\MSBuild.exe', '{25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|.NET = Debug|.NET + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E8E75132-67E4-4D6F-9CAE-8DA4C883F418}.Debug|.NET.ActiveCfg = Debug|.NET + {E8E75132-67E4-4D6F-9CAE-8DA4C883F418}.Debug|.NET.Build.0 = Debug|.NET + {25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0}.Debug|.NET.ActiveCfg = Debug + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0} = {D2633E4D-46FF-4C4E-8340-4BC7CDF78615} + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + ProjectInSolution webProject = (ProjectInSolution)solution.ProjectsByGuid["{E8E75132-67E4-4D6F-9CAE-8DA4C883F418}"]; + ProjectInSolution exeProject = (ProjectInSolution)solution.ProjectsByGuid["{25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0}"]; + ProjectInSolution missingWebProject = (ProjectInSolution)solution.ProjectsByGuid["{E8E75132-67E4-4D6F-9CAE-8DA4C883F419}"]; + + Assert.AreEqual(1, webProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug|.NET", webProject.ProjectConfigurations["Debug|.NET"].FullName); + Assert.AreEqual(true, webProject.ProjectConfigurations["Debug|.NET"].IncludeInBuild); + + Assert.AreEqual(1, exeProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug", exeProject.ProjectConfigurations["Debug|.NET"].FullName); + Assert.AreEqual(false, exeProject.ProjectConfigurations["Debug|.NET"].IncludeInBuild); + + Assert.AreEqual(0, missingWebProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug", solution.GetDefaultConfigurationName(), "Default solution configuration"); + Assert.AreEqual(".NET", solution.GetDefaultPlatformName(), "Default solution platform"); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Construction/SolutionProjectGenerator_Tests.cs b/src/XMakeBuildEngine/UnitTests/Construction/SolutionProjectGenerator_Tests.cs new file mode 100644 index 00000000000..3a3d9f2cb49 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Construction/SolutionProjectGenerator_Tests.cs @@ -0,0 +1,1989 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.IO; +using System.Xml; +using System.Linq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Unittest; + +using Project = Microsoft.Build.Evaluation.Project; +using ProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; +using Toolset = Microsoft.Build.Evaluation.Toolset; + +using InternalUtilities = Microsoft.Build.Internal.Utilities; + +using XMakeElements = Microsoft.Build.Shared.XMakeElements; +using ResourceUtilities = Microsoft.Build.Shared.ResourceUtilities; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using FrameworkLocationHelper = Microsoft.Build.Shared.FrameworkLocationHelper; + +namespace Microsoft.Build.UnitTests.Construction +{ + [TestClass] + public class SolutionProjectGenerator_Tests + { + private string _originalVisualStudioVersion = null; + + [TestInitialize] + public void Setup() + { + // Save off the value for use during cleanup + _originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + } + + [TestCleanup] + public void Cleanup() + { + // Need to make sure the environment is cleared up for later tests + Environment.SetEnvironmentVariable("VisualStudioVersion", _originalVisualStudioVersion); + } + + /// + /// Verify the AddNewErrorWarningMessageElement method + /// + [TestMethod] + public void AddNewErrorWarningMessageElement() + { + MockLogger logger = new MockLogger(); + + /** + * + * + * + * + /// Test to make sure we properly set the ToolsVersion attribute on the in-memory project based + /// on the Solution File Format Version. + /// + [TestMethod] + public void EmitToolsVersionAttributeToInMemoryProject9() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV35 == null) + { + // ".NET Framework 3.5 is required to be installed for this test, but it is not installed."); + return; + } + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + Other|Any CPU = Other|Any CPU + Other|Win32 = Other|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, "3.5", new BuildEventContext(0, 0, 0, 0), null); + + Assert.AreEqual("3.5", instances[0].ToolsVersion); + } + + /// + /// Test to make sure we properly set the ToolsVersion attribute on the in-memory project based + /// on the Solution File Format Version. + /// + [TestMethod] + public void EmitToolsVersionAttributeToInMemoryProject10() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV35 == null) + { + // ".NET Framework 3.5 is required to be installed for this test, but it is not installed."); + return; + } + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 10.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + Other|Any CPU = Other|Any CPU + Other|Win32 = Other|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, "3.5", new BuildEventContext(0, 0, 0, 0), null); + + Assert.AreEqual("3.5", instances[0].ToolsVersion); + } + + /// + /// Test to make sure that if the solution file version doesn't map to a sub-toolset version, we won't try + /// to force it to be used. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DefaultSubToolsetIfSolutionVersionSubToolsetDoesntExist() + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 10.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + Other|Any CPU = Other|Any CPU + Other|Win32 = Other|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, new BuildEventContext(0, 0, 0, 0), null); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, instances[0].ToolsVersion); + + Toolset t = ProjectCollection.GlobalProjectCollection.GetToolset(instances[0].ToolsVersion); + + Assert.AreEqual(t.DefaultSubToolsetVersion, instances[0].SubToolsetVersion); + Assert.AreEqual(t.DefaultSubToolsetVersion, instances[0].GetPropertyValue("VisualStudioVersion")); + } + + /// + /// Test to make sure that if the solution version corresponds to an existing sub-toolset version, + /// barring other factors that might override, the sub-toolset will be based on the solution version. + /// + [TestMethod] + public void SubToolsetSetBySolutionVersion() + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 12.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + Other|Any CPU = Other|Any CPU + Other|Win32 = Other|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, new BuildEventContext(0, 0, 0, 0), null); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, instances[0].ToolsVersion); + + // being cautious -- we can't expect the sub-toolset to be picked if it doesn't exist in the first place + if (instances[0].Toolset.SubToolsets.ContainsKey("11.0")) + { + Assert.AreEqual("11.0", instances[0].SubToolsetVersion); + Assert.AreEqual("11.0", instances[0].GetPropertyValue("VisualStudioVersion")); + } + } + + /// + /// Test to make sure that even if the solution version corresponds to an existing sub-toolset version, + /// + [TestMethod] + public void SolutionBasedSubToolsetVersionOverriddenByEnvironment() + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "ABC"); + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 12.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + Other|Any CPU = Other|Any CPU + Other|Win32 = Other|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, new BuildEventContext(0, 0, 0, 0), null); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, instances[0].ToolsVersion); + Assert.AreEqual("ABC", instances[0].SubToolsetVersion); + Assert.AreEqual("ABC", instances[0].GetPropertyValue("VisualStudioVersion")); + } + + + /// + /// Test to make sure that even if the solution version corresponds to an existing sub-toolset version + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SolutionPassesSubToolsetToChildProjects2() + { + string classLibraryContentsToolsV4 = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + "); + + string classLibraryContentsToolsV12 = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + "); + + string solutionFilePreambleV11 = + @" + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio Dev11 + "; + + string solutionFilePreambleV12 = + @" + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio Dev11 + VisualStudioVersion = 12.0.20311.0 VSPRO_PLATFORM + MinimumVisualStudioVersion = 10.0.40219.1 + "; + + string solutionBodySingleProjectContents = + @" + + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ClassLibrary1"", ""ClassLibrary1.csproj"", ""{6185CC21-BE89-448A-B3C0-D1C27112E595}"" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = CSConfig2|Any CPU + EndGlobalSection + EndGlobal + "; + + string solutionBodyMultipleProjectsContents = + @" + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ClassLibrary1"", ""ClassLibrary1.csproj"", ""{A437DBE9-DCAA-46D8-9D80-A50EDB2244FD}"" + EndProject + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ClassLibrary2"", ""ClassLibrary2.csproj"", ""{84AA5584-4B0F-41DE-95AA-589E1447EDA0}"" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A437DBE9-DCAA-46D8-9D80-A50EDB2244FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A437DBE9-DCAA-46D8-9D80-A50EDB2244FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A437DBE9-DCAA-46D8-9D80-A50EDB2244FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A437DBE9-DCAA-46D8-9D80-A50EDB2244FD}.Release|Any CPU.Build.0 = Release|Any CPU + {84AA5584-4B0F-41DE-95AA-589E1447EDA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84AA5584-4B0F-41DE-95AA-589E1447EDA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84AA5584-4B0F-41DE-95AA-589E1447EDA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84AA5584-4B0F-41DE-95AA-589E1447EDA0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + + string solutionFileContentsDev11 = solutionFilePreambleV11 + solutionBodySingleProjectContents; + string solutionFileContentsDev12 = solutionFilePreambleV12 + solutionBodySingleProjectContents; + + string[] solutions = { solutionFileContentsDev11, solutionFileContentsDev12, solutionFileContentsDev12 }; + string[] projects = { classLibraryContentsToolsV4, classLibraryContentsToolsV4, classLibraryContentsToolsV12 }; + string[] logoutputs = { ".[11.0]. .[4.0].", ".[11.0]. .[4.0].", String.Format(".[{0}]. .[{0}].", ObjectModelHelpers.MSBuildDefaultToolsVersion) }; + + string previousLegacyEnvironmentVariable = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + for (int i = 0; i < solutions.Length; i++) + { + string solutionFile = ObjectModelHelpers.CreateFileInTempProjectDirectory("Foo.sln", solutions[i]); + string projectFile = ObjectModelHelpers.CreateFileInTempProjectDirectory("ClassLibrary1.csproj", projects[i]); + SolutionFile sp = new SolutionFile(); + + sp.FullPath = solutionFile; + sp.ParseSolutionFile(); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(sp, null, null, new BuildEventContext(0, 0, 0, 0), null); + + MockLogger logger = new MockLogger(); + List loggers = new List(1); + loggers.Add(logger); + + instances[0].Build(loggers); + logger.AssertLogContains(logoutputs[i]); + } + + // Test Dev 12 sln and mixed v4.0 and v12.0 projects + string solutionFileContentsDev12MultipleProjects = solutionFilePreambleV12 + solutionBodyMultipleProjectsContents; + + string solutionFileMultipleProjects = ObjectModelHelpers.CreateFileInTempProjectDirectory("Foo.sln", solutionFileContentsDev12MultipleProjects); + string projectFileV4 = ObjectModelHelpers.CreateFileInTempProjectDirectory("ClassLibrary1.csproj", classLibraryContentsToolsV4); + string projectFileV12 = ObjectModelHelpers.CreateFileInTempProjectDirectory("ClassLibrary2.csproj", classLibraryContentsToolsV12); + + SolutionFile sp1 = new SolutionFile(); + + sp1.FullPath = solutionFileMultipleProjects; + sp1.ParseSolutionFile(); + ProjectInstance[] instances1 = SolutionProjectGenerator.Generate(sp1, null, null, new BuildEventContext(0, 0, 0, 0), null); + + MockLogger logger1 = new MockLogger(); + List loggers1 = new List(1); + loggers1.Add(logger1); + + instances1[0].Build(loggers1); + logger1.AssertLogContains(".[11.0]. .[4.0]."); + logger1.AssertLogContains(String.Format(".[{0}]. .[{0}].", ObjectModelHelpers.MSBuildDefaultToolsVersion)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", previousLegacyEnvironmentVariable); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// Test to make sure that, when we're not TV 4.0 -- which even for Dev11 solutions we are not by default -- that we + /// do not pass VisualStudioVersion down to the child projects. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SolutionDoesntPassSubToolsetToChildProjects() + { + try + { + string classLibraryContents = + @" + + + + + + + "; + + string projectFile = ObjectModelHelpers.CreateFileInTempProjectDirectory("ClassLibrary1.csproj", classLibraryContents); + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio Dev11 + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ClassLibrary1"", ""ClassLibrary1.csproj"", ""{6185CC21-BE89-448A-B3C0-D1C27112E595}"" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = CSConfig2|Any CPU + EndGlobalSection + EndGlobal + "; + + string solutionFile = ObjectModelHelpers.CreateFileInTempProjectDirectory("Foo.sln", solutionFileContents); + + SolutionFile sp = new SolutionFile(); + + sp.FullPath = solutionFile; + sp.ParseSolutionFile(); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(sp, null, null, new BuildEventContext(0, 0, 0, 0), null); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, instances[0].ToolsVersion); + Assert.AreEqual("11.0", instances[0].SubToolsetVersion); + Assert.AreEqual("11.0", instances[0].GetPropertyValue("VisualStudioVersion")); + + MockLogger logger = new MockLogger(); + List loggers = new List(1); + loggers.Add(logger); + + + instances[0].Build(loggers); + logger.AssertLogContains(String.Format(".[{0}].", ObjectModelHelpers.MSBuildDefaultToolsVersion)); + } + finally + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + } + + + /// + /// Verify that we throw the appropriate error if the solution declares a dependency + /// on a project that doesn't exist. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void SolutionWithMissingDependencies() + { + string solutionFileContents = + @" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 11 +Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `B`, `Project2\B.csproj`, `{881C1674-4ECA-451D-85B6-D7C59B7F16FA}` + ProjectSection(ProjectDependencies) = postProject + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167} = {4A727FF8-65F2-401E-95AD-7C8BBFBE3167} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = preSolution + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|x64.Build.0 = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|Any CPU.Build.0 = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|x64.ActiveCfg = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal +".Replace("`", "\""); + + SolutionFile sp = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + ProjectInstance[] instances = SolutionProjectGenerator.Generate(sp, null, null, new BuildEventContext(0, 0, 0, 0), null); + } + + /// + /// Blob should contain dependency info + /// Here B depends on C + /// + [TestMethod] + public void SolutionConfigurationWithDependencies() + { + string solutionFileContents = + @" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 11 +Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `A`, `Project1\A.csproj`, `{786E302A-96CE-43DC-B640-D6B6CC9BF6C0}` +EndProject +Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `B`, `Project2\B.csproj`, `{881C1674-4ECA-451D-85B6-D7C59B7F16FA}` + ProjectSection(ProjectDependencies) = postProject + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167} = {4A727FF8-65F2-401E-95AD-7C8BBFBE3167} + EndProjectSection +EndProject +Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `C`, `Project3\C.csproj`, `{4A727FF8-65F2-401E-95AD-7C8BBFBE3167}` +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = preSolution + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Debug|x64.Build.0 = Debug|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Release|Any CPU.Build.0 = Release|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Release|x64.ActiveCfg = Release|Any CPU + {4A727FF8-65F2-401E-95AD-7C8BBFBE3167}.Release|x64.Build.0 = Release|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Debug|x64.Build.0 = Debug|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Release|Any CPU.Build.0 = Release|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Release|x64.ActiveCfg = Release|Any CPU + {786E302A-96CE-43DC-B640-D6B6CC9BF6C0}.Release|x64.Build.0 = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Debug|x64.Build.0 = Debug|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|Any CPU.Build.0 = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|x64.ActiveCfg = Release|Any CPU + {881C1674-4ECA-451D-85B6-D7C59B7F16FA}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal +".Replace("`", "\""); + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectRootElement projectXml = ProjectRootElement.Create(); + + foreach (SolutionConfigurationInSolution solutionConfiguration in solution.SolutionConfigurations) + { + SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration(projectXml, solution, solutionConfiguration); + } + + Project msbuildProject = new Project(projectXml); + + // Both projects configurations should be present for solution configuration "Debug|Mixed Platforms" + msbuildProject.SetGlobalProperty("Configuration", "Debug"); + msbuildProject.SetGlobalProperty("Platform", "Any CPU"); + msbuildProject.ReevaluateIfNecessary(); + + string solutionConfigurationContents = msbuildProject.GetPropertyValue("CurrentSolutionConfigurationContents"); + + // Only the specified solution configuration is represented in THE BLOB: nothing for x64 in this case + string expected = @" + Debug|AnyCPU + Debug|AnyCPU + Debug|AnyCPU +".Replace("`", "\"").Replace("##temp##", Path.GetTempPath()); + + Helpers.VerifyAssertLineByLine(expected, solutionConfigurationContents); + } + + /// + /// Test the SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration method + /// + [TestMethod] + public void TestAddPropertyGroupForSolutionConfiguration() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = CSConfig2|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = VCConfig1|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = VCConfig1|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectRootElement projectXml = ProjectRootElement.Create(); + + foreach (SolutionConfigurationInSolution solutionConfiguration in solution.SolutionConfigurations) + { + SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration(projectXml, solution, solutionConfiguration); + } + + Project msbuildProject = new Project(projectXml); + + // Both projects configurations should be present for solution configuration "Debug|Mixed Platforms" + msbuildProject.SetGlobalProperty("Configuration", "Debug"); + msbuildProject.SetGlobalProperty("Platform", "Mixed Platforms"); + msbuildProject.ReevaluateIfNecessary(); + + string solutionConfigurationContents = msbuildProject.GetPropertyValue("CurrentSolutionConfigurationContents"); + string tempProjectPath = Path.Combine(Path.GetTempPath(), "ClassLibrary1\\ClassLibrary1.csproj"); + + Assert.IsTrue(solutionConfigurationContents.Contains("{6185CC21-BE89-448A-B3C0-D1C27112E595}")); + tempProjectPath = Path.GetFullPath(tempProjectPath); + Assert.IsTrue(solutionConfigurationContents.IndexOf(tempProjectPath, StringComparison.OrdinalIgnoreCase) > 0); + Assert.IsTrue(solutionConfigurationContents.Contains("CSConfig1|AnyCPU")); + + tempProjectPath = Path.Combine(Path.GetTempPath(), "MainApp\\MainApp.vcxproj"); + tempProjectPath = Path.GetFullPath(tempProjectPath); + Assert.IsTrue(solutionConfigurationContents.Contains("{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}")); + Assert.IsTrue(solutionConfigurationContents.IndexOf(tempProjectPath, StringComparison.OrdinalIgnoreCase) > 0); + Assert.IsTrue(solutionConfigurationContents.Contains("VCConfig1|Win32")); + + // Only the C# project should be present for solution configuration "Release|Any CPU", since the VC project + // is missing + msbuildProject.SetGlobalProperty("Configuration", "Release"); + msbuildProject.SetGlobalProperty("Platform", "Any CPU"); + msbuildProject.ReevaluateIfNecessary(); + + solutionConfigurationContents = msbuildProject.GetPropertyValue("CurrentSolutionConfigurationContents"); + + Assert.IsTrue(solutionConfigurationContents.Contains("{6185CC21-BE89-448A-B3C0-D1C27112E595}")); + Assert.IsTrue(solutionConfigurationContents.Contains("CSConfig2|AnyCPU")); + + Assert.IsFalse(solutionConfigurationContents.Contains("{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}")); + } + + /// + /// Make sure that BuildProjectInSolution is set to true of the Build.0 entry is in the solution configuration. + /// + [TestMethod] + public void TestAddPropertyGroupForSolutionConfigurationBuildProjectInSolutionSet() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectRootElement projectXml = ProjectRootElement.Create(); + + foreach (SolutionConfigurationInSolution solutionConfiguration in solution.SolutionConfigurations) + { + SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration(projectXml, solution, solutionConfiguration); + } + + Project msbuildProject = new Project(projectXml); + + // Both projects configurations should be present for solution configuration "Debug|Mixed Platforms" + msbuildProject.SetGlobalProperty("Configuration", "Debug"); + msbuildProject.SetGlobalProperty("Platform", "Mixed Platforms"); + msbuildProject.ReevaluateIfNecessary(); + + string solutionConfigurationContents = msbuildProject.GetPropertyValue("CurrentSolutionConfigurationContents"); + Assert.IsTrue(solutionConfigurationContents.Contains(@"BuildProjectInSolution=""" + bool.TrueString + @"""")); + } + + /// + /// Make sure that BuildProjectInSolution is set to false of the Build.0 entry is in the solution configuration. + /// + [TestMethod] + public void TestAddPropertyGroupForSolutionConfigurationBuildProjectInSolutionNotSet() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectRootElement projectXml = ProjectRootElement.Create(); + + foreach (SolutionConfigurationInSolution solutionConfiguration in solution.SolutionConfigurations) + { + SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration(projectXml, solution, solutionConfiguration); + } + + Project msbuildProject = new Project(projectXml); + + // Both projects configurations should be present for solution configuration "Debug|Mixed Platforms" + msbuildProject.SetGlobalProperty("Configuration", "Debug"); + msbuildProject.SetGlobalProperty("Platform", "Mixed Platforms"); + msbuildProject.ReevaluateIfNecessary(); + + string solutionConfigurationContents = msbuildProject.GetPropertyValue("CurrentSolutionConfigurationContents"); + Assert.IsTrue(solutionConfigurationContents.Contains(@"BuildProjectInSolution=""" + bool.FalseString + @"""")); + } + + /// + /// In this bug, SkipNonexistentProjects was always set to 'Build'. It should be 'Build' for metaprojects and 'True' for everything else. + /// The repro below has one of each case. WebProjects can't build so they are set as SkipNonexistentProjects='Build' + /// + [TestMethod] + public void Regress751742_SkipNonexistentProjects() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // ".NET Framework 2.0 is required to be installed for this test, but it is not installed." + return; + } + + var solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'MainApp', 'MainApp\MainApp.webproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = CSConfig2|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = VCConfig1|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = VCConfig1|Win32 + EndGlobalSection + EndGlobal + "; + + // We're not passing in a /tv:xx switch, so the solution project will have tools version 2.0 + var solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + var buildEventContext = new BuildEventContext(0, 0, 0, 0); + + var instance = SolutionProjectGenerator.Generate(solution, null, ObjectModelHelpers.MSBuildDefaultToolsVersion, buildEventContext, null)[0]; + + foreach (ITaskItem item in instance.Items) + { + string skipNonexistentProjects = item.GetMetadata("SkipNonexistentProjects"); + if (item.ItemSpec.EndsWith("ClassLibrary1.csproj")) + { + Assert.AreEqual("False", skipNonexistentProjects); + } + else if (item.ItemSpec.EndsWith("MainApp.metaproj")) + { + Assert.AreEqual("Build", skipNonexistentProjects); + } + else if (item.ItemSpec == "Debug|Mixed Platforms") + { + Assert.AreEqual("Debug", item.GetMetadata("Configuration")); + Assert.AreEqual("Mixed Platforms", item.GetMetadata("Platform")); + Assert.IsTrue(item.GetMetadata("Content").Contains("")); + } + else if (item.ItemSpec == "Release|Any CPU") + { + Assert.AreEqual("Release", item.GetMetadata("Configuration")); + Assert.AreEqual("Any CPU", item.GetMetadata("Platform")); + Assert.IsTrue(item.GetMetadata("Content").Contains("")); + } + else + { + Assert.Fail("Unexpected project seen:" + item.ItemSpec); + } + } + } + + + + /// + /// Test that the in memory project created from a solution file exposes an MSBuild property which, + /// if set when building a solution, will be specified as the ToolsVersion on the MSBuild task when + /// building the projects contained within the solution. + /// + [TestMethod] + public void ToolsVersionOverrideShouldBeSpecifiedOnMSBuildTaskInvocations() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = CSConfig2|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = VCConfig1|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = VCConfig1|Win32 + EndGlobalSection + EndGlobal + "; + + // We're not passing in a /tv:xx switch, so the solution project will have tools version 2.0 + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + BuildEventContext buildEventContext = new BuildEventContext(0, 0, 0, 0); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, ObjectModelHelpers.MSBuildDefaultToolsVersion, buildEventContext, null); + + int i = 0; + foreach (ProjectInstance instance in instances) + { + if (i == 0) + { + continue; + } + + foreach (ProjectTargetInstance target in instance.Targets.Values) + { + foreach (ProjectTaskInstance childNode in target.Tasks) + { + if (0 == String.Compare(childNode.Name, "MSBuild", StringComparison.OrdinalIgnoreCase)) + { + string projectsParameter = childNode.GetParameter("Projects"); + if (projectsParameter != "@(ProjectReference)") + { + // we found an MSBuild task invocation, now let's verify that it has the correct + // ToolsVersion parameter set + string toolsVersionParameter = childNode.GetParameter("ToolsVersion"); + + Assert.AreEqual(0, String.Compare(toolsVersionParameter, instances[0].GetPropertyValue("ProjectToolsVersion"), StringComparison.OrdinalIgnoreCase)); + } + } + } + } + + i++; + } + } + + /// + /// Make sure that whatever the solution ToolsVersion is, it gets mapped to all its metaprojs, too. + /// + [TestMethod] + public void SolutionWithDependenciesHasCorrectToolsVersionInMetaprojs() + { + string solutionFileContents = + @" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 12 +Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ConsoleApplication2', 'ConsoleApplication2\ConsoleApplication2.csproj', '{5B97A3C7-3DEE-47A4-870F-5CB6384FE6A4}' + ProjectSection(ProjectDependencies) = postProject + {E0D295A1-CAFA-4E68-9929-468657DAAC6C} = {E0D295A1-CAFA-4E68-9929-468657DAAC6C} + EndProjectSection +EndProject +Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'ConsoleApplication1', 'ConsoleApplication1\ConsoleApplication1.vbproj', '{E0D295A1-CAFA-4E68-9929-468657DAAC6C}' +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5B97A3C7-3DEE-47A4-870F-5CB6384FE6A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B97A3C7-3DEE-47A4-870F-5CB6384FE6A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B97A3C7-3DEE-47A4-870F-5CB6384FE6A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B97A3C7-3DEE-47A4-870F-5CB6384FE6A4}.Release|Any CPU.Build.0 = Release|Any CPU + {E0D295A1-CAFA-4E68-9929-468657DAAC6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0D295A1-CAFA-4E68-9929-468657DAAC6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0D295A1-CAFA-4E68-9929-468657DAAC6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0D295A1-CAFA-4E68-9929-468657DAAC6C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal + "; + + // We're not passing in a /tv:xx switch, so the solution project will have tools version 2.0 + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + BuildEventContext buildEventContext = new BuildEventContext(0, 0, 0, 0); + + string[] solutionToolsVersions = { "4.0", ObjectModelHelpers.MSBuildDefaultToolsVersion }; + + foreach (string solutionToolsVersion in solutionToolsVersions) + { + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, solutionToolsVersion, buildEventContext, null); + + Assert.AreEqual(2, instances.Length); + + // Solution metaproj + Assert.AreEqual(solutionToolsVersion, instances[0].ToolsVersion); + + ICollection projectReferences = instances[0].GetItems("ProjectReference"); + + foreach (ProjectItemInstance projectReference in projectReferences) + { + // If this is the reference to the metaproj, its ToolsVersion metadata needs to match + // the solution ToolsVersion -- that's how the build knows which ToolsVersion to use. + if (projectReference.EvaluatedInclude.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase)) + { + Assert.AreEqual(solutionToolsVersion, projectReference.GetMetadataValue("ToolsVersion")); + } + } + + // Project metaproj for project with dependencies + Assert.AreEqual(solutionToolsVersion, instances[1].ToolsVersion); + } + } + + /// + /// Test the SolutionProjectGenerator.Generate method has its toolset redirected correctly. + /// + [TestMethod] + public void ToolsVersionOverrideCausesToolsetRedirect() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = CSConfig1|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = CSConfig2|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = VCConfig1|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = VCConfig1|Win32 + EndGlobalSection + EndGlobal + "; + + ProjectInstance[] instances = null; + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + BuildEventContext buildEventContext = new BuildEventContext(0, 0, 0, 0); + bool caughtException = false; + + try + { + // SolutionProjectGenerator.Generate() is used at build-time, and creates evaluation- and + // execution-model projects; as such it will throw if fed an explicitly invalid toolsversion + instances = SolutionProjectGenerator.Generate(solution, null, "invalid", buildEventContext, null); + } + catch (InvalidProjectFileException) + { + caughtException = true; + } + + Assert.IsTrue(caughtException, "Passing an invalid ToolsVersion should have caused an InvalidProjectFileException to be thrown."); + } + + /// + /// Test the SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration method + /// + [TestMethod] + public void TestDisambiguateProjectTargetName() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'Build', 'Build\Build.csproj', '{21397922-C38F-4A0E-B950-77B3FBD51881}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {21397922-C38F-4A0E-B950-77B3FBD51881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21397922-C38F-4A0E-B950-77B3FBD51881}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21397922-C38F-4A0E-B950-77B3FBD51881}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21397922-C38F-4A0E-B950-77B3FBD51881}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, null); + + Assert.AreEqual(1, instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Build", StringComparison.OrdinalIgnoreCase) == 0).Count()); + Assert.AreEqual(1, instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Clean", StringComparison.OrdinalIgnoreCase) == 0).Count()); + Assert.AreEqual(1, instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Rebuild", StringComparison.OrdinalIgnoreCase) == 0).Count()); + Assert.AreEqual(1, instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Publish", StringComparison.OrdinalIgnoreCase) == 0).Count()); + + ProjectTargetInstance buildTarget = instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Build", StringComparison.OrdinalIgnoreCase) == 0).First().Value; + ProjectTargetInstance cleanTarget = instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Clean", StringComparison.OrdinalIgnoreCase) == 0).First().Value; + ProjectTargetInstance rebuildTarget = instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Rebuild", StringComparison.OrdinalIgnoreCase) == 0).First().Value; + ProjectTargetInstance publishTarget = instances[0].Targets.Where(target => String.Compare(target.Value.Name, "Publish", StringComparison.OrdinalIgnoreCase) == 0).First().Value; + + // Check that the appropriate target is being passed to the child projects + Assert.AreEqual(null, buildTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Targets")); + + Assert.AreEqual("Clean", cleanTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Targets")); + + Assert.AreEqual("Rebuild", rebuildTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Targets")); + + Assert.AreEqual("Publish", publishTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Targets")); + + // Check that the child projects in question are the members of the "ProjectReference" item group + Assert.AreEqual("@(ProjectReference)", buildTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Projects")); + + Assert.AreEqual("@(ProjectReference->Reverse())", cleanTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Projects")); + + Assert.AreEqual("@(ProjectReference)", rebuildTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Projects")); + + Assert.AreEqual("@(ProjectReference)", publishTarget.Tasks.Where + ( + task => String.Compare(task.Name, "MSBuild", StringComparison.OrdinalIgnoreCase) == 0 + ).First().GetParameter("Projects")); + + // We should have only the four standard targets plus the two validation targets (ValidateSolutionConfiguration and ValidateToolsVersions). + } + + /// + /// Tests the algorithm for choosing default configuration/platform values for solutions + /// + [TestMethod] + public void TestConfigurationPlatformDefaults1() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + // These used to exist on the engine, but now need to be passed in explicitly + IDictionary globalProperties = new Dictionary(); + + globalProperties.Add(new KeyValuePair("Configuration", "Debug")); + globalProperties.Add(new KeyValuePair("Platform", "Mixed Platforms")); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, null); + + // Default for Configuration is "Debug", if present + Assert.AreEqual("Debug", instances[0].GetPropertyValue("Configuration")); + + // Default for Platform is "Mixed Platforms", if present + Assert.AreEqual("Mixed Platforms", instances[0].GetPropertyValue("Platform")); + } + + /// + /// Tests the algorithm for choosing default configuration/platform values for solutions + /// + [TestMethod] + public void TestConfigurationPlatformDefaults2() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + Other|Any CPU = Other|Any CPU + Other|Win32 = Other|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, null); + + // If "Debug" is not present, just pick the first configuration name + Assert.AreEqual("Release", instances[0].GetPropertyValue("Configuration")); + + // if "Mixed Platforms" is not present, just pick the first platform name + Assert.AreEqual("Any CPU", instances[0].GetPropertyValue("Platform")); + } + + /// + /// Tests the algorithm for choosing default Venus configuration values for solutions + /// + [TestMethod] + public void TestVenusConfigurationDefaults() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // ".NET Framework 2.0 is required to be installed for this test, but it is not installed." + return; + } + + Dictionary globalProperties = new Dictionary(); + globalProperties["Configuration"] = "Debug"; + ProjectInstance msbuildProject = CreateVenusSolutionProject(globalProperties); + + // ASP.NET configuration should match the selected solution configuration + Assert.AreEqual("Debug", msbuildProject.GetPropertyValue("AspNetConfiguration")); + + globalProperties["Configuration"] = "Release"; + msbuildProject = CreateVenusSolutionProject(globalProperties); + Assert.AreEqual("Release", msbuildProject.GetPropertyValue("AspNetConfiguration")); + + // Check that the two standard Asp.net configurations are represented on the targets + Assert.IsTrue(msbuildProject.Targets["Build"].Condition.Contains("'$(Configuration)' == 'Release'")); + Assert.IsTrue(msbuildProject.Targets["Build"].Condition.Contains("'$(Configuration)' == 'Debug'")); + } + + /// + /// Tests that the correct value for TargetFrameworkVersion gets set when creating Venus solutions + /// + [TestMethod] + public void VenusSolutionDefaultTargetFrameworkVersion() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // ".NET Framework 2.0 is required to be installed for this test, but it is not installed." + return; + } + + // v4.0 by default + ProjectInstance msbuildProject = CreateVenusSolutionProject(); + Assert.AreEqual("v4.0", msbuildProject.GetPropertyValue("TargetFrameworkVersion")); + + if (FrameworkLocationHelper.PathToDotNetFrameworkV35 == null) + { + // ".NET Framework 3.5 is required to be installed for this test, but it is not installed." + return; + } + + // v3.5 if MSBuildToolsVersion is 3.5 + msbuildProject = CreateVenusSolutionProject("3.5"); + Assert.AreEqual("v3.5", msbuildProject.GetPropertyValue("TargetFrameworkVersion")); + + // v2.0 if MSBuildToolsVersion is 2.0 + msbuildProject = CreateVenusSolutionProject("2.0"); + Assert.AreEqual("v2.0", msbuildProject.GetPropertyValue("TargetFrameworkVersion")); + + // may be user defined + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("TargetFrameworkVersion", "userdefined"); + msbuildProject = CreateVenusSolutionProject(globalProperties); + Assert.AreEqual("userdefined", msbuildProject.GetPropertyValue("TargetFrameworkVersion")); + } + + /// + /// Tests the algorithm for choosing target framework paths for ResolveAssemblyReferences for Venus + /// + [TestMethod] + public void TestTargetFrameworkPaths0() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkSdkV20 != null) + { + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("TargetFrameworkVersion", "v2.0"); + + ProjectInstance msbuildProject = CreateVenusSolutionProject("2.0"); + + // ToolsVersion is 2.0, TargetFrameworkVersion is v2.0 --> one item pointing to v2.0 + Assert.AreEqual("2.0", msbuildProject.ToolsVersion); + + bool success = msbuildProject.Build("GetFrameworkPathAndRedistList", null); + Assert.AreEqual(true, success); + AssertProjectContainsItem(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", FrameworkLocationHelper.PathToDotNetFrameworkV20); + AssertProjectItemNameCount(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", 1); + } + } + + /// + /// Tests the algorithm for choosing target framework paths for ResolveAssemblyReferences for Venus + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TestTargetFrameworkPaths1() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // ".NET Framework 2.0 is required to be installed for this test, but it is not installed." + return; + } + + ProjectInstance msbuildProject = CreateVenusSolutionProject(); + + // ToolsVersion is 4.0, TargetFrameworkVersion is v2.0 --> one item pointing to v2.0 + msbuildProject.SetProperty("TargetFrameworkVersion", "v2.0"); + MockLogger logger = new MockLogger(); + bool success = msbuildProject.Build("GetFrameworkPathAndRedistList", new ILogger[] { logger }); + Assert.AreEqual(true, success); + + AssertProjectContainsItem(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", FrameworkLocationHelper.PathToDotNetFrameworkV20); + AssertProjectItemNameCount(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", 1); + } + + /// + /// Tests the algorithm for choosing target framework paths for ResolveAssemblyReferences for Venus + /// + [TestMethod] + public void TestTargetFrameworkPaths2() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // ".NET Framework 2.0 is required to be installed for this test, but it is not installed." + return; + } + + ProjectInstance msbuildProject = CreateVenusSolutionProject(); + + // ToolsVersion is 4.0, TargetFrameworkVersion is v4.0 --> items for v2.0 and v4.0 + msbuildProject.SetProperty("TargetFrameworkVersion", "v4.0"); + // ProjectInstance projectToBuild = msbuildProject.CreateProjectInstance(); + bool success = msbuildProject.Build("GetFrameworkPathAndRedistList", null); + Assert.AreEqual(true, success); + + int expectedCount = 0; + + // 2.0 must be installed for us to have come this far + AssertProjectContainsItem(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", FrameworkLocationHelper.PathToDotNetFrameworkV20); + expectedCount++; + + if (FrameworkLocationHelper.PathToDotNetFrameworkV30 != null) + { + AssertProjectContainsItem(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", FrameworkLocationHelper.PathToDotNetFrameworkV30); + expectedCount++; + } + + if (FrameworkLocationHelper.PathToDotNetFrameworkV35 != null) + { + AssertProjectContainsItem(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", FrameworkLocationHelper.PathToDotNetFrameworkV35); + expectedCount++; + } + + if (FrameworkLocationHelper.PathToDotNetFrameworkV40 != null) + { + AssertProjectContainsItem(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", FrameworkLocationHelper.PathToDotNetFrameworkV40); + expectedCount++; + } + + AssertProjectItemNameCount(msbuildProject, "_CombinedTargetFrameworkDirectoriesItem", expectedCount); + } + + /// + /// Test the PredictActiveSolutionConfigurationName method + /// + [TestMethod] + public void TestPredictSolutionConfigurationName() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + IDictionary globalProperties = new Dictionary(); + + Assert.AreEqual("Debug|Mixed Platforms", SolutionProjectGenerator.PredictActiveSolutionConfigurationName(solution, globalProperties)); + + globalProperties.Add("Configuration", "Release"); + Assert.AreEqual("Release|Mixed Platforms", SolutionProjectGenerator.PredictActiveSolutionConfigurationName(solution, globalProperties)); + + globalProperties.Add("Platform", "Win32"); + Assert.AreEqual("Release|Win32", SolutionProjectGenerator.PredictActiveSolutionConfigurationName(solution, globalProperties)); + + globalProperties["Configuration"] = "Nonexistent"; + Assert.AreEqual(null, SolutionProjectGenerator.PredictActiveSolutionConfigurationName(solution, globalProperties)); + } + + + /// + /// Verifies that the SolutionProjectGenerator will correctly escape project file paths + /// + [TestMethod] + public void SolutionGeneratorEscapingProjectFilePaths() + { + string oldValueForMSBuildEmitSolution = Environment.GetEnvironmentVariable("MSBuildEmitSolution"); + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'ConsoleApplication1', '%abtest\ConsoleApplication1.vbproj', '{AB3413A6-D689-486D-B7F0-A095371B3F13}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = null; + + solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + // Creating a ProjectRootElement shouldn't affect the ProjectCollection at all + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.LoadedProjects.Count()); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, null); + + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.LoadedProjects.Count()); + + // Ensure that the value has been correctly stored in the ProjectReference item list + // Since there is only one project in the solution, there will be only one project reference + Assert.IsTrue(instances[0].GetItems("ProjectReference").ElementAt(0).EvaluatedInclude.Contains("%abtest")); + } + + /// + /// Verifies that the SolutionProjectGenerator will emit a solution file. + /// + [TestMethod] + public void SolutionGeneratorCanEmitSolutions() + { + string oldValueForMSBuildEmitSolution = Environment.GetEnvironmentVariable("MSBuildEmitSolution"); + + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'ConsoleApplication1', 'ConsoleApplication1\ConsoleApplication1.vbproj', '{AB3413A6-D689-486D-B7F0-A095371B3F13}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = null; + + try + { + Environment.SetEnvironmentVariable("MSBuildEmitSolution", "1"); + + solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + // Creating a ProjectRootElement shouldn't affect the ProjectCollection at all + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.LoadedProjects.Count()); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, null); + + // Instantiating the + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.LoadedProjects.Count()); + } + finally + { + // reset the environment variable first so that it doesn't get ignored by the assert. + Environment.SetEnvironmentVariable("MSBuildEmitSolution", oldValueForMSBuildEmitSolution); + + // Clean up. Delete temp files and reset environment variables. + if (solution != null) + { + Assert.IsTrue(File.Exists(solution.FullPath + ".metaproj"), "Solution parser should have written in-memory project to disk"); + File.Delete(solution.FullPath + ".metaproj"); + } + else + { + Assert.Fail("Something went really wrong! The SolutionFile wasn't even created!"); + } + } + } + + /// + /// Make sure that we output a warning and don't build anything when we're given an invalid + /// solution configuration and SkipInvalidConfigurations is set to true. + /// + [TestMethod] + public void TestSkipInvalidConfigurationsCase() + { + string tmpFileName = FileUtilities.GetTemporaryFile(); + File.Delete(tmpFileName); + string projectFilePath = tmpFileName + ".sln"; + + string solutionContents = + @" + Microsoft Visual Studio Solution File, Format Version 11.00 + # Visual Studio 2005 + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\solutions\WebSite2\', '..\..\solutions\WebSite2\', '{F90528C4-6989-4D33-AFE8-F53173597CC2}' + ProjectSection(WebsiteProperties) = preProject + Debug.AspNetCompiler.VirtualPath = '/WebSite2' + Debug.AspNetCompiler.PhysicalPath = '..\..\solutions\WebSite2\' + Debug.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite2\' + Debug.AspNetCompiler.Updateable = 'true' + Debug.AspNetCompiler.ForceOverwrite = 'true' + Debug.AspNetCompiler.FixedNames = 'true' + Debug.AspNetCompiler.Debug = 'True' + Release.AspNetCompiler.VirtualPath = '/WebSite2' + Release.AspNetCompiler.PhysicalPath = '..\..\solutions\WebSite2\' + Release.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite2\' + Release.AspNetCompiler.Updateable = 'true' + Release.AspNetCompiler.ForceOverwrite = 'true' + Release.AspNetCompiler.FixedNames = 'true' + Release.AspNetCompiler.Debug = 'False' + VWDPort = '2776' + DefaultWebSiteLanguage = 'Visual C#' + EndProjectSection + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F90528C4-6989-4D33-AFE8-F53173597CC2}.Debug|Any CPU.ActiveCfg = Debug|.NET + {F90528C4-6989-4D33-AFE8-F53173597CC2}.Debug|Any CPU.Build.0 = Debug|.NET + EndGlobalSection + EndGlobal"; + + try + { + MockLogger logger = new MockLogger(); + + Dictionary globalProperties = new Dictionary(); + globalProperties["Configuration"] = "Nonexistent"; + globalProperties["SkipInvalidConfigurations"] = "true"; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionContents.Replace('\'', '"')); + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, globalProperties, null, BuildEventContext.Invalid, null); + ProjectInstance msbuildProject = instances[0]; + + // Build should complete successfully even with an invalid solution config if SkipInvalidConfigurations is true + Assert.AreEqual(true, msbuildProject.Build(new ILogger[] { logger })); + + // We should get the invalid solution configuration warning + Assert.AreEqual(1, logger.Warnings.Count); + BuildWarningEventArgs warning = logger.Warnings[0]; + + // Don't look at warning.Code here -- it may be null if PseudoLoc has messed + // with our resource strings. The code will still be in the log -- it just wouldn't get + // pulled out into the code field. + logger.AssertLogContains("MSB4126"); + + // No errors expected + Assert.AreEqual(0, logger.Errors.Count); + } + finally + { + File.Delete(projectFilePath); + } + } + + /// + /// When we have a bad framework moniker we expect the build to fail. + /// + [TestMethod] + public void BadFrameworkMonkierExpectBuildToFail() + { + string tmpFileName = FileUtilities.GetTemporaryFile(); + File.Delete(tmpFileName); + string projectFilePath = tmpFileName + ".sln"; + + string solutionFileContents = + @"Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'WebSite1', '..\WebSite1\', '{6B8F98F2-C976-4029-9321-5CCD73A174DA}' + ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = 'SuperCoolReallyAwesomeFramework,Version=v1.0' + Debug.AspNetCompiler.VirtualPath = '/WebSite1' + Debug.AspNetCompiler.PhysicalPath = '..\WebSite1\' + Debug.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite1\' + Debug.AspNetCompiler.Updateable = 'true' + Debug.AspNetCompiler.ForceOverwrite = 'true' + Debug.AspNetCompiler.FixedNames = 'false' + Debug.AspNetCompiler.Debug = 'True' + Release.AspNetCompiler.VirtualPath = '/WebSite1' + Release.AspNetCompiler.PhysicalPath = '..\WebSite1\' + Release.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite1\' + Release.AspNetCompiler.Updateable = 'true' + Release.AspNetCompiler.ForceOverwrite = 'true' + Release.AspNetCompiler.FixedNames = 'false' + Release.AspNetCompiler.Debug = 'False' + VWDPort = '45602' + DefaultWebSiteLanguage = 'Visual Basic' + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6B8F98F2-C976-4029-9321-5CCD73A174DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B8F98F2-C976-4029-9321-5CCD73A174DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal + "; + + BuildManager buildManager = null; + + try + { + // Since we're creating our own BuildManager, we need to make sure that the default + // one has properly relinquished the inproc node + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)BuildManager.DefaultBuildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + if (nodeProviderInProc != null) + { + nodeProviderInProc.Dispose(); + } + + File.WriteAllText(projectFilePath, solutionFileContents.Replace('\'', '"')); + MockLogger logger = new MockLogger(); + + BuildParameters parameters = new BuildParameters(); + parameters.Loggers = new ILogger[] { logger }; + parameters.EnableNodeReuse = false; + parameters.ShutdownInProcNodeOnBuildFinish = true; + buildManager = new BuildManager(); + + + Dictionary globalProperties = new Dictionary(); + globalProperties["Configuration"] = "Release"; + + BuildRequestData request = new BuildRequestData(projectFilePath, globalProperties, ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[0], null); + BuildResult result = buildManager.Build(parameters, request); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + // Build should complete successfully even with an invalid solution config if SkipInvalidConfigurations is true + logger.AssertLogContains("MSB4203"); + } + finally + { + File.Delete(projectFilePath); + + if (buildManager != null) + { + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)buildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + nodeProviderInProc.Dispose(); + } + } + } + + /// + /// When we have a bad framework moniker we expect the build to fail. In this case we are passing a poorly formatted framework moniker. + /// This will test the exception path where the framework name is invalid rather than just not .netFramework + /// + [TestMethod] + public void BadFrameworkMonkierExpectBuildToFail2() + { + string tmpFileName = FileUtilities.GetTemporaryFile(); + File.Delete(tmpFileName); + string projectFilePath = tmpFileName + ".sln"; + + string solutionFileContents = + @"Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'WebSite1', '..\WebSite1\', '{6B8F98F2-C976-4029-9321-5CCD73A174DA}' + ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = 'Oscar the grouch' + Debug.AspNetCompiler.VirtualPath = '/WebSite1' + Debug.AspNetCompiler.PhysicalPath = '..\WebSite1\' + Debug.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite1\' + Debug.AspNetCompiler.Updateable = 'true' + Debug.AspNetCompiler.ForceOverwrite = 'true' + Debug.AspNetCompiler.FixedNames = 'false' + Debug.AspNetCompiler.Debug = 'True' + Release.AspNetCompiler.VirtualPath = '/WebSite1' + Release.AspNetCompiler.PhysicalPath = '..\WebSite1\' + Release.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite1\' + Release.AspNetCompiler.Updateable = 'true' + Release.AspNetCompiler.ForceOverwrite = 'true' + Release.AspNetCompiler.FixedNames = 'false' + Release.AspNetCompiler.Debug = 'False' + VWDPort = '45602' + DefaultWebSiteLanguage = 'Visual Basic' + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6B8F98F2-C976-4029-9321-5CCD73A174DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B8F98F2-C976-4029-9321-5CCD73A174DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal + "; + + BuildManager buildManager = null; + + try + { + // Since we're creating our own BuildManager, we need to make sure that the default + // one has properly relinquished the inproc node + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)BuildManager.DefaultBuildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + if (nodeProviderInProc != null) + { + nodeProviderInProc.Dispose(); + } + + File.WriteAllText(projectFilePath, solutionFileContents.Replace('\'', '"')); + MockLogger logger = new MockLogger(); + + BuildParameters parameters = new BuildParameters(); + parameters.Loggers = new ILogger[] { logger }; + parameters.EnableNodeReuse = false; + parameters.ShutdownInProcNodeOnBuildFinish = true; + buildManager = new BuildManager(); + + + Dictionary globalProperties = new Dictionary(); + globalProperties["Configuration"] = "Release"; + + BuildRequestData request = new BuildRequestData(projectFilePath, globalProperties, ObjectModelHelpers.MSBuildDefaultToolsVersion, new string[0], null); + BuildResult result = buildManager.Build(parameters, request); + Assert.AreEqual(BuildResultCode.Failure, result.OverallResult); + // Build should complete successfully even with an invalid solution config if SkipInvalidConfigurations is true + logger.AssertLogContains("MSB4204"); + } + finally + { + File.Delete(projectFilePath); + + if (buildManager != null) + { + NodeProviderInProc nodeProviderInProc = ((IBuildComponentHost)buildManager).GetComponent(BuildComponentType.InProcNodeProvider) as NodeProviderInProc; + nodeProviderInProc.Dispose(); + } + } + } + + /// + /// Bug indicated that when a target framework version greater than 4.0 was used then the solution project generator would crash. + /// this test is to make sure the fix is not regressed. + /// + [TestMethod] + public void TestTargetFrameworkVersionGreaterThan4() + { + string tmpFileName = FileUtilities.GetTemporaryFile(); + File.Delete(tmpFileName); + string projectFilePath = tmpFileName + ".sln"; + + string solutionFileContents = + @" +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'WebSite1', '..\WebSite1\', '{6B8F98F2-C976-4029-9321-5CCD73A174DA}' + ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = '.NETFramework,Version=v4.34' + Debug.AspNetCompiler.VirtualPath = '/WebSite1' + Debug.AspNetCompiler.PhysicalPath = '..\WebSite1\' + Debug.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite1\' + Debug.AspNetCompiler.Updateable = 'true' + Debug.AspNetCompiler.ForceOverwrite = 'true' + Debug.AspNetCompiler.FixedNames = 'false' + Debug.AspNetCompiler.Debug = 'True' + Release.AspNetCompiler.VirtualPath = '/WebSite1' + Release.AspNetCompiler.PhysicalPath = '..\WebSite1\' + Release.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite1\' + Release.AspNetCompiler.Updateable = 'true' + Release.AspNetCompiler.ForceOverwrite = 'true' + Release.AspNetCompiler.FixedNames = 'false' + Release.AspNetCompiler.Debug = 'False' + VWDPort = '45602' + DefaultWebSiteLanguage = 'Visual Basic' + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6B8F98F2-C976-4029-9321-5CCD73A174DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B8F98F2-C976-4029-9321-5CCD73A174DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal + "; + + try + { + MockLogger logger = new MockLogger(); + + Dictionary globalProperties = new Dictionary(); + globalProperties["Configuration"] = "Release"; + globalProperties["SkipInvalidConfigurations"] = "true"; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents.Replace('\'', '"')); + ProjectCollection collection = new ProjectCollection(); + collection.RegisterLogger(logger); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, globalProperties, null, BuildEventContext.Invalid, collection.LoggingService); + + Version ver = new Version("4.34"); + string message = ResourceUtilities.FormatResourceString("AspNetCompiler.TargetingHigherFrameworksDefaultsTo40", solution.ProjectsInOrder[0].ProjectName, ver.ToString()); + logger.AssertLogContains(message); + } + finally + { + File.Delete(projectFilePath); + } + } + + #region Helper Functions + + /// + /// Convert passed in solution file to an MSBuild project. This method is used by Sln2Proj + /// + public bool ConvertSLN2Proj(string nameSolutionFile) + { + // Set the environment variable to cause the SolutionProjectGenerator to emit the project to disk + string oldValueForMSBuildEmitSolution = Environment.GetEnvironmentVariable("MSBuildEmitSolution"); + Environment.SetEnvironmentVariable("MSBuildEmitSolution", "1"); + + if (nameSolutionFile == null || !File.Exists(nameSolutionFile)) + { + return false; + } + + // Parse the solution + SolutionFile solution = new SolutionFile(); + solution.FullPath = nameSolutionFile; + solution.ParseSolutionFile(); + + // Generate the in-memory MSBuild project and output it to disk + ProjectInstance[] instance = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, null); + + + //Reset the environment variable + Environment.SetEnvironmentVariable("MSBuildEmitSolution", oldValueForMSBuildEmitSolution); + + return true; + } + + /// + /// Create a Project derived from a Venus solution + /// + private static ProjectInstance CreateVenusSolutionProject() + { + return CreateVenusSolutionProject(null, null); + } + + /// + /// Create a Project derived from a Venus solution + /// + private static ProjectInstance CreateVenusSolutionProject(IDictionary globalProperties) + { + return CreateVenusSolutionProject(globalProperties, null); + } + + /// + /// Create a Project derived from a Venus solution + /// + private static ProjectInstance CreateVenusSolutionProject(string toolsVersion) + { + return CreateVenusSolutionProject(null, toolsVersion); + } + + /// + /// Create a Project derived from a Venus solution, given a set of global properties and a ToolsVersion + /// to use as the override value + /// + /// The dictionary of global properties. May be null. + /// The ToolsVersion override value. May be null. + private static ProjectInstance CreateVenusSolutionProject(IDictionary globalProperties, string toolsVersion) + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\solutions\WebSite2\', '..\..\solutions\WebSite2\', '{F90528C4-6989-4D33-AFE8-F53173597CC2}' + ProjectSection(WebsiteProperties) = preProject + Debug.AspNetCompiler.VirtualPath = '/WebSite2' + Debug.AspNetCompiler.PhysicalPath = '..\..\solutions\WebSite2\' + Debug.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite2\' + Debug.AspNetCompiler.Updateable = 'true' + Debug.AspNetCompiler.ForceOverwrite = 'true' + Debug.AspNetCompiler.FixedNames = 'true' + Debug.AspNetCompiler.Debug = 'True' + Release.AspNetCompiler.VirtualPath = '/WebSite2' + Release.AspNetCompiler.PhysicalPath = '..\..\solutions\WebSite2\' + Release.AspNetCompiler.TargetPath = 'PrecompiledWeb\WebSite2\' + Release.AspNetCompiler.Updateable = 'true' + Release.AspNetCompiler.ForceOverwrite = 'true' + Release.AspNetCompiler.FixedNames = 'true' + Release.AspNetCompiler.Debug = 'False' + VWDPort = '2776' + DefaultWebSiteLanguage = 'Visual C#' + EndProjectSection + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F90528C4-6989-4D33-AFE8-F53173597CC2}.Debug|Any CPU.ActiveCfg = Debug|.NET + {F90528C4-6989-4D33-AFE8-F53173597CC2}.Debug|Any CPU.Build.0 = Debug|.NET + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(solutionFileContents); + + ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, globalProperties, toolsVersion, BuildEventContext.Invalid, null); + + // Index 0 is the traversal project, which will reference the sole Venus project. + return instances[1]; + } + + /// + /// Checks the provided project for a matching itemtype and include value. If it + /// does not exist, asserts. + /// + private void AssertProjectContainsItem(ProjectInstance msbuildProject, string itemType, string include) + { + IEnumerable itemGroup = msbuildProject.GetItems(itemType); + Assert.IsNotNull(itemGroup); + + foreach (ProjectItemInstance item in itemGroup) + { + if (item.ItemType == itemType && item.EvaluatedInclude == include) + { + return; + } + } + + Assert.IsTrue(false); + } + + /// + /// Counts the number of items with a particular itemtype in the provided project, and + /// asserts if it doesn't match the provided count. + /// + private void AssertProjectItemNameCount(ProjectInstance msbuildProject, string itemType, int count) + { + IEnumerable itemGroup = msbuildProject.GetItems(itemType); + Assert.IsNotNull(itemGroup); + Assert.AreEqual(count, itemGroup.Count()); + } + + #endregion // Helper Functions + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs b/src/XMakeBuildEngine/UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs new file mode 100644 index 00000000000..021abcd7e27 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Construction/XmlReaderWithoutLocation_Tests.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the ElementLocation class +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.BackEnd; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Xml; +using System.IO; + +namespace Microsoft.Build.UnitTests.Construction +{ + [TestClass] + public class XmlReaderWithoutLocation_Tests + { + private class XmlReaderNoIXmlLineInfo : XmlReader + { + private XmlReader _wrappedReader; + + public XmlReaderNoIXmlLineInfo(XmlReader wrappedReader) + { + _wrappedReader = wrappedReader; + } + + public override int AttributeCount + { + get { return _wrappedReader.AttributeCount; } + } + + public override string BaseURI + { + get { return _wrappedReader.BaseURI; } + } + + public override void Close() + { + _wrappedReader.Close(); + } + + public override int Depth + { + get { return _wrappedReader.Depth; } + } + + public override bool EOF + { + get { return _wrappedReader.EOF; } + } + + public override string GetAttribute(int i) + { + return _wrappedReader.GetAttribute(i); + } + + public override string GetAttribute(string name, string namespaceURI) + { + return _wrappedReader.GetAttribute(name, namespaceURI); + } + + public override string GetAttribute(string name) + { + return _wrappedReader.GetAttribute(name); + } + + public override bool HasValue + { + get { return _wrappedReader.HasValue; } + } + + public override bool IsEmptyElement + { + get { return _wrappedReader.IsEmptyElement; } + } + + public override string LocalName + { + get { return _wrappedReader.LocalName; } + } + + public override string LookupNamespace(string prefix) + { + return _wrappedReader.LookupNamespace(prefix); + } + + public override bool MoveToAttribute(string name, string ns) + { + return _wrappedReader.MoveToAttribute(name, ns); + } + + public override bool MoveToAttribute(string name) + { + return _wrappedReader.MoveToAttribute(name); + } + + public override bool MoveToElement() + { + return _wrappedReader.MoveToElement(); + } + + public override bool MoveToFirstAttribute() + { + return _wrappedReader.MoveToFirstAttribute(); + } + + public override bool MoveToNextAttribute() + { + return _wrappedReader.MoveToNextAttribute(); + } + + public override XmlNameTable NameTable + { + get { return _wrappedReader.NameTable; } + } + + public override string NamespaceURI + { + get { return _wrappedReader.NamespaceURI; } + } + + public override XmlNodeType NodeType + { + get { return _wrappedReader.NodeType; } + } + + public override string Prefix + { + get { return _wrappedReader.Prefix; } + } + + public override bool Read() + { + return _wrappedReader.Read(); + } + + public override bool ReadAttributeValue() + { + return _wrappedReader.ReadAttributeValue(); + } + + public override ReadState ReadState + { + get { return _wrappedReader.ReadState; } + } + + public override void ResolveEntity() + { + _wrappedReader.ResolveEntity(); + } + + public override string Value + { + get { return _wrappedReader.Value; } + } + } + + [TestMethod] + public void CreateProjectWithoutLineInfo() + { + XmlReader reader = XmlReader.Create(new StringReader + ( + @" + + " + )); + XmlReader noLineInfoReader = new XmlReaderNoIXmlLineInfo(reader); + Project project = new Project(noLineInfoReader); + Assert.AreEqual(1, project.Targets.Count); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ItemDefinitionGroup_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/ItemDefinitionGroup_Tests.cs new file mode 100644 index 00000000000..d18a8f9b6e1 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ItemDefinitionGroup_Tests.cs @@ -0,0 +1,1860 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using System.Text; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Threading; + +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.Definition +{ + /// + /// Class containing tests for the ProjectItemDefinition and related functionality. + /// + [TestClass] + public class ItemDefinitionGroup_Tests + { + /// + /// Test for item definition group definitions showing up in project. + /// + [TestMethod] + public void ItemDefinitionGroupExistsInProject() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + 2nd + + + + "))); + + Assert.IsTrue(ContainsMetadata(p.ItemDefinitions["Compile"].Metadata, "First", "1st")); + Assert.IsTrue(ContainsMetadata(p.ItemDefinitions["Compile"].Metadata, "Second", "2nd")); + } + + /// + /// Test for multiple item definition group definitions showing up in project. + /// + [TestMethod] + public void MultipleItemDefinitionGroupExistsInProject() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + 2nd + + + + + 3rd + 4th + + + + "))); + + Assert.IsTrue(ContainsMetadata(p.ItemDefinitions["Compile"].Metadata, "First", "1st")); + Assert.IsTrue(ContainsMetadata(p.ItemDefinitions["Compile"].Metadata, "Second", "2nd")); + Assert.IsTrue(ContainsMetadata(p.ItemDefinitions["Link"].Metadata, "Third", "3rd")); + Assert.IsTrue(ContainsMetadata(p.ItemDefinitions["Link"].Metadata, "Fourth", "4th")); + } + + /// + /// Tests that items with no metadata inherit from item definition groups + /// + [TestMethod] + public void EmptyItemsInheritValues() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + 2nd + + + + + 3rd + 4th + + + + + + + "))); + + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "b.cs", "First", "1st")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Second", "2nd")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "b.cs", "Second", "2nd")); + Assert.IsFalse(ItemContainsMetadata(p, "Compile", "a.cs", "Third", "3rd")); + } + + /// + /// Tests that items with metadata override inherited metadata of the same name + /// + [TestMethod] + public void ItemMetadataOverridesInheritedValues() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + 2nd + + + + + 3rd + 4th + + + + + Bar + Not1st + + + Bar + + + + + + + "))); + + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "First", "Not1st")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Second", "2nd")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "b.cs", "First", "1st")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "b.cs", "Second", "2nd")); + Assert.IsTrue(ItemContainsMetadata(p, "Link", "a.o", "Third", "3rd")); + Assert.IsTrue(ItemContainsMetadata(p, "Link", "a.o", "Fourth", "4th")); + } + + /// + /// Tests that item definition doesn't allow item expansion for the conditional. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemDefinitionDoesntAllowItemExpansion() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + Bar + Not1st + + + Bar + + + + + 1st + 2nd + + + + "))); + } + + /// + /// Tests that item definition metadata doesn't allow item expansion for the conditional. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemDefinitionMetadataConditionDoesntAllowItemExpansion() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + Bar + Not1st + + + Bar + + + + + 1st + + + + "))); + } + + /// + /// Tests that item definition metadata doesn't allow item expansion for the value. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemDefinitionMetadataDoesntAllowItemExpansion() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + Bar + Not1st + + + Bar + + + + + @(Compile) + + + + "))); + } + + /// + /// Tests that item metadata which contains a metadata expansion referring to an item type other + /// than the one this item definition refers to expands to blank. + /// + [TestMethod] + public void ItemMetadataReferringToDifferentItemGivesEmptyValue() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + 2nd + + + + + --%(Compile.First)-- + 4th + + + + + Bar + Not1st + + + Bar + + + + + + + "))); + + Assert.IsTrue(ItemContainsMetadata(p, "Link", "a.o", "Third", "----")); + } + + /// + /// Tests that empty item definition groups are OK. + /// + [TestMethod] + public void EmptyItemDefinitionGroup() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + + + + + "))); + } + + /// + /// Tests that item definition groups with empty item definitions are OK. + /// + [TestMethod] + public void EmptyItemDefinitions() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + + + + Bar + + + + "))); + + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "b.cs", "Foo", "Bar")); + } + + [TestMethod] + public void SelfReferencingMetadataReferencesUseItemDefinition() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + DEBUG + + + + + + %(Defines);CODEANALYSIS + + + + + + + "))); + + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + logger.AssertLogContains("[{a.cpp}{DEBUG;CODEANALYSIS}]"); // Unexpected value after evaluation + } + + + [TestMethod] + public void SelfReferencingMetadataReferencesUseItemDefinitionInTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + DEBUG + + + + + + + %(Defines);CODEANALYSIS + + + + + + "))); + + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + logger.AssertLogContains("[{a.cpp}{DEBUG;CODEANALYSIS}]"); // Unexpected value after evaluation + } + + [TestMethod] + public void SelfReferencingMetadataReferencesUseItemDefinitionInTargetModify() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + DEBUG + + + + + + + + + + %(Defines);CODEANALYSIS + + + + + + "))); + + p.Build(new string[] { "Build" }, new ILogger[] { logger }); + logger.AssertLogContains("[{a.cpp}{DEBUG;CODEANALYSIS}]"); // Unexpected value after evaluation + } + + /// + /// Tests that item definition groups with false conditions don't produce definitions + /// + [TestMethod] + public void ItemDefinitionGroupWithFalseCondition() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + + + + + Bar + + + + "))); + + Assert.IsFalse(p.ItemDefinitions.ContainsKey("Compile")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsFalse(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + } + + /// + /// Tests that item definition groups with true conditions produce definitions + /// + [TestMethod] + public void ItemDefinitionGroupWithTrueCondition() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + + + + + Bar + + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("Compile")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + } + + /// + /// Tests that item definition with false conditions don't produce definitions + /// + [TestMethod] + public void ItemDefinitionWithFalseCondition() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + + + + + Bar + + + + "))); + + Assert.IsFalse(p.ItemDefinitions.ContainsKey("Compile")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsFalse(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + } + + /// + /// Tests that item definition with true conditions produce definitions + /// + [TestMethod] + public void ItemDefinitionWithTrueCondition() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + + + + + Bar + + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("Compile")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + } + + /// + /// Tests that item definition metadata with false conditions don't produce definitions + /// + [TestMethod] + public void ItemDefinitionMetadataWithFalseCondition() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + + + + + Bar + + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("Compile")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsFalse(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + } + + /// + /// Tests that item definition metadata with true conditions produce definitions + /// + [TestMethod] + public void ItemDefinitionMetadataWithTrueCondition() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + 1st + + + + + Bar + + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("Compile")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "Foo", "Bar")); + Assert.IsTrue(ItemContainsMetadata(p, "Compile", "a.cs", "First", "1st")); + } + + /// + /// Tests that item definition metadata is correctly copied to a destination item + /// + [TestMethod] + public void ItemDefinitionMetadataCopiedToTaskItem() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + M-A(b) + M-B(b) + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("ItemA")); + + ProjectInstance pi = p.CreateProjectInstance(); + ITaskItem noMetaItem = null; + ITaskItem withMetaItem; + + List itemdefs = new List(); + itemdefs.Add(pi.ItemDefinitions["ItemA"]); + + noMetaItem = new TaskItem("NoMetaItem", pi.FullPath); + withMetaItem = new TaskItem("WithMetaItem", "WithMetaItem", null, itemdefs, ".", false, pi.FullPath); + + // Copy the metadata on the item with no metadata onto the item with metadata + // from an item definition. The destination item's metadata should be maintained + noMetaItem.CopyMetadataTo(withMetaItem); + + Assert.AreEqual("M-A(b)", withMetaItem.GetMetadata("MetaA")); + Assert.AreEqual("M-B(b)", withMetaItem.GetMetadata("MetaB")); + } + + /// + /// Tests that item definition metadata is correctly copied to a destination item + /// + [TestMethod] + public void ItemDefinitionMetadataCopiedToTaskItem2() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + M-A(b) + M-B(b) + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("ItemA")); + + ProjectInstance pi = p.CreateProjectInstance(); + ITaskItem noMetaItem = null; + ITaskItem withMetaItem; + + List itemdefs = new List(); + itemdefs.Add(pi.ItemDefinitions["ItemA"]); + + noMetaItem = new TaskItem("NoMetaItem", pi.FullPath); + noMetaItem.SetMetadata("MetaA", "NEWMETA_A"); + + withMetaItem = new TaskItem("WithMetaItem", "WithMetaItem", null, itemdefs, ".", false, pi.FullPath); + + // Copy the metadata on the item with no metadata onto the item with metadata + // from an item definition. The destination item's metadata should be maintained + noMetaItem.CopyMetadataTo(withMetaItem); + + // New direct metadata takes precedence over item definitions on the destination item + Assert.AreEqual("NEWMETA_A", withMetaItem.GetMetadata("MetaA")); + Assert.AreEqual("M-B(b)", withMetaItem.GetMetadata("MetaB")); + } + + /// + /// Tests that item definition metadata is correctly copied to a destination item + /// + [TestMethod] + public void ItemDefinitionMetadataCopiedToTaskItem3() + { + Project p = new Project(XmlReader.Create(new StringReader( + @" + + + M-A(b) + M-B(b) + + + + + + "))); + + Assert.IsTrue(p.ItemDefinitions.ContainsKey("ItemA")); + + ProjectInstance pi = p.CreateProjectInstance(); + ITaskItem noMetaItem = null; + ITaskItem withMetaItem = null; + + List itemdefs = new List(); + itemdefs.Add(pi.ItemDefinitions["ItemA"]); + + noMetaItem = new TaskItem("NoMetaItem", pi.FullPath); + + // No the ideal way to get the first item, but there is no other way since GetItems returns an IEnumerable :( + foreach (ProjectItemInstance item in pi.GetItems("ItemA")) + { + withMetaItem = item; + } + + // Copy the metadata on the item with no metadata onto the item with metadata + // from an item definition. The destination item's metadata should be maintained + noMetaItem.CopyMetadataTo(withMetaItem); + + Assert.AreEqual("M-A(b)", withMetaItem.GetMetadata("MetaA")); + Assert.AreEqual("M-B(b)", withMetaItem.GetMetadata("MetaB")); + } + + #region Project tests + + [TestMethod] + public void BasicItemDefinitionInProject() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + DEBUG + + + + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[a.cpp==DEBUG]", "[b.cpp==DEBUG]"); + } + + [TestMethod] + public void EscapingInItemDefinitionInProject() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + %24(xyz) + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[$(xyz)]"); + } + + + [TestMethod] + public void ItemDefinitionForOtherItemType() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[]"); + } + + [TestMethod] + public void RedefinitionLastOneWins() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + n1 + + + + + m2 + o1 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2-n1-o1]"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemExpressionInDefaultMetadataValueErrors() + { + // We don't allow item expressions on an ItemDefinitionGroup because there are no items when IDG is evaluated. + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + @(x) + + + + "))); + p.Build("t", new ILogger[] { logger }); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void UnqualifiedMetadataConditionOnItemDefinitionGroupErrors() + { + // We don't allow unqualified metadata on an ItemDefinitionGroup because we don't know what item type it refers to. + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + "))); + p.Build("t", new ILogger[] { logger }); + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void QualifiedMetadataConditionOnItemDefinitionGroupErrors() + { + // We don't allow qualified metadata because it's not worth distinguishing from unqualified, when you can just move the condition to the child. + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + "))); + p.Build("t", new ILogger[] { logger }); + } + + [TestMethod] + public void MetadataConditionOnItemDefinition() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + + m1 + + + n1 + + + + + m2 + + + + n2 + + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2]", "[n2]"); + } + + [TestMethod] + public void QualifiedMetadataConditionOnItemDefinitionBothQualifiedAndUnqualified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + m2 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2]"); + } + + [TestMethod] + public void FalseMetadataConditionOnItemDefinitionBothQualifiedAndUnqualified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + m3 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1]"); + } + + [TestMethod] + public void MetadataConditionOnItemDefinitionChildBothQualifiedAndUnqualified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + n1 + + + + + m2 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2]"); + } + + [TestMethod] + public void FalseMetadataConditionOnItemDefinitionChildBothQualifiedAndUnqualified() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + n1 + + + + + m3 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1]"); + } + + [TestMethod] + public void MetadataConditionOnItemDefinitionAndChildQualifiedWithUnrelatedItemType() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + m2 + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2]"); + } + + /// + /// Make ItemDefinitionGroup inside a target produce a nice error. + /// It will normally produce an error due to the invalid child tag, but + /// we want to error even if there's no child tag. This will make it + /// easier to support it inside targets in a future version. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ItemDefinitionInTargetErrors() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + "))); + bool result = p.Build("t", new ILogger[] { logger }); + } + + // Verify that anyone with a task named "ItemDefinitionGroup" can still + // use it by fully qualifying the name. + [TestMethod] + public void ItemDefinitionGroupTask() + { + MockLogger ml = Helpers.BuildProjectWithNewOMExpectSuccess(String.Format(@" + + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + Assert.IsTrue(ml.FullLog.Contains("In ItemDefinitionGroup task.")); + } + + [TestMethod] + public void MetadataOnItemWins() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + RETAIL + + + + + + DEBUG + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[a.cpp==RETAIL]", "[b.cpp==DEBUG]"); + } + + [TestMethod] + public void MixtureOfItemAndDefaultMetadata() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + 4 + + + + + DEBUG + + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[a.cpp==DEBUG]", "[a.cpp==4]"); + } + + [TestMethod] + public void IntrinsicTaskModifyingDefaultMetadata() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + + m2 + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2]"); + } + + [TestMethod] + public void IntrinsicTaskConsumingDefaultMetadata() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + + n2 + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[n2]"); + } + + [TestMethod] + public void DefinitionInImportedFile() + { + MockLogger logger = new MockLogger(); + string importedFile = null; + + try + { + importedFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(importedFile, @" + + + + DEBUG + + + + "); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + + + + "))); + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[a.cpp==DEBUG]"); + } + finally + { + ObjectModelHelpers.DeleteTempFiles(new string[] { importedFile }); + } + } + + /// + /// Item added to project should pick up the item + /// definitions that project has. + [TestMethod] + public void ProjectAddNewItemPicksUpProjectItemDefinitions() + { + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + + + + "))); + + p.AddItem("i", "i1"); + p.ReevaluateIfNecessary(); + + Assert.IsTrue(ItemContainsMetadata(p, "i", "i1", "m", "m1")); + } + + /// + /// Item added to project should pick up the item + /// definitions that project has. + [TestMethod] + public void ProjectAddNewItemExistingGroupPicksUpProjectItemDefinitions() + { + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + + + + + m2 + + + + "))); + + p.AddItem("i", "i1"); + p.ReevaluateIfNecessary(); + + Assert.IsTrue(ItemContainsMetadata(p, "i", "i1", "m", "m1")); + Assert.IsTrue(ItemContainsMetadata(p, "i", "i2", "m", "m2")); + } + + [TestMethod] + public void ItemsEmittedByTaskPickUpItemDefinitions() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + n1 + + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1][n2]"); + } + + [TestMethod] + public void ItemsEmittedByIntrinsicTaskPickUpItemDefinitions() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + n1 + + + + + + n2 + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1][n2]"); + } + + /// + /// When items are passed with an item list expression, default metadata values on the source + /// items should become regular metadata values on the new items, unless overridden. + /// + [TestMethod] + public void ItemsEmittedByIntrinsicTaskConsumingItemExpression_SourceDefaultMetadataPassed() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + + + + + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1]"); + } + + /// + /// Default metadata on the source item list is overridden by matching metadata explicitly on the destination + /// + [TestMethod] + public void ItemsEmittedByIntrinsicTaskConsumingItemExpression_DestinationExplicitMetadataBeatsSourceDefaultMetadata() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + + + + + + + + + m2 + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m2]"); + } + + /// + /// When items of type X are copied into a list of type Y, default metadata applicable to type X override + /// any matching default metadata applicable to type Y. + /// + /// + /// Either behavior here is fairly reasonable. We decided on this way around based on feedback from VC. + /// Note: this differs from how Orcas did it. + /// + [TestMethod] + public void ItemsEmittedByIntrinsicTaskConsumingItemExpression_DestinationDefaultMetadataOverriddenBySourceDefaultMetadata() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + + + m2 + + + + + + + + + + + + + + + "))); + + Assert.AreEqual("m1", p.GetItems("j").First().GetMetadataValue("m")); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1]"); + logger.AssertLogDoesntContain("[m2]"); + } + + /// + /// Default and explicit metadata on both source and destination. + /// Item definition metadata from the source override item definition on the destination. + /// + [TestMethod] + public void ItemsEmittedByIntrinsicTaskConsumingItemExpression_Combination_OutsideTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + im1 + in1 + io1 +

ip1

+
+ + jm3 + jn3 + jq3 + + + km4 + kq4 + kr4 + +
+ + + io2 + is2 + + + + km5 + + + + + jm6 + + + ks3 + + +
+ "))); + + Assert.AreEqual("im1", p.GetItems("i").First().GetMetadataValue("m")); + Assert.AreEqual("in1", p.GetItems("i").First().GetMetadataValue("n")); + Assert.AreEqual("io2", p.GetItems("i").First().GetMetadataValue("o")); + Assert.AreEqual("ip1", p.GetItems("i").First().GetMetadataValue("p")); + Assert.AreEqual("", p.GetItems("i").First().GetMetadataValue("q")); + + Assert.AreEqual("jm3", p.GetItems("j").First().GetMetadataValue("m")); + Assert.AreEqual("jn3", p.GetItems("j").First().GetMetadataValue("n")); + Assert.AreEqual("", p.GetItems("j").First().GetMetadataValue("o")); + Assert.AreEqual("", p.GetItems("j").First().GetMetadataValue("p")); + Assert.AreEqual("jq3", p.GetItems("j").First().GetMetadataValue("q")); + + Assert.AreEqual("jm6", p.GetItems("j").ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("in1", p.GetItems("j").ElementAt(1).GetMetadataValue("n")); + Assert.AreEqual("io2", p.GetItems("j").ElementAt(1).GetMetadataValue("o")); + Assert.AreEqual("ip1", p.GetItems("j").ElementAt(1).GetMetadataValue("p")); + Assert.AreEqual("jq3", p.GetItems("j").ElementAt(1).GetMetadataValue("q")); + + Assert.AreEqual("km5", p.GetItems("k").ElementAt(0).GetMetadataValue("m")); + Assert.AreEqual("", p.GetItems("k").ElementAt(0).GetMetadataValue("n")); + Assert.AreEqual("", p.GetItems("k").ElementAt(0).GetMetadataValue("o")); + Assert.AreEqual("", p.GetItems("k").ElementAt(0).GetMetadataValue("p")); + Assert.AreEqual("kq4", p.GetItems("k").ElementAt(0).GetMetadataValue("q")); + Assert.AreEqual("kr4", p.GetItems("k").ElementAt(0).GetMetadataValue("r")); + Assert.AreEqual("", p.GetItems("k").ElementAt(0).GetMetadataValue("s")); + + Assert.AreEqual("jm3", p.GetItems("k").ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("jn3", p.GetItems("k").ElementAt(1).GetMetadataValue("n")); + Assert.AreEqual("", p.GetItems("k").ElementAt(1).GetMetadataValue("o")); + Assert.AreEqual("", p.GetItems("k").ElementAt(1).GetMetadataValue("p")); + Assert.AreEqual("jq3", p.GetItems("k").ElementAt(1).GetMetadataValue("q")); + Assert.AreEqual("kr4", p.GetItems("k").ElementAt(1).GetMetadataValue("r")); + Assert.AreEqual("ks3", p.GetItems("k").ElementAt(1).GetMetadataValue("s")); + + Assert.AreEqual("jm6", p.GetItems("k").ElementAt(2).GetMetadataValue("m")); + Assert.AreEqual("in1", p.GetItems("k").ElementAt(2).GetMetadataValue("n")); + Assert.AreEqual("io2", p.GetItems("k").ElementAt(2).GetMetadataValue("o")); + Assert.AreEqual("ip1", p.GetItems("k").ElementAt(2).GetMetadataValue("p")); + Assert.AreEqual("jq3", p.GetItems("k").ElementAt(2).GetMetadataValue("q")); + Assert.AreEqual("kr4", p.GetItems("k").ElementAt(2).GetMetadataValue("r")); + Assert.AreEqual("ks3", p.GetItems("k").ElementAt(1).GetMetadataValue("s")); + } + + /// + /// Default and explicit metadata on both source and destination. + /// Item definition metadata from the source override item definition on the destination. + /// + [TestMethod] + public void ItemsEmittedByIntrinsicTaskConsumingItemExpression_Combination_InsideTarget() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + im1 + in1 + io1 +

ip1

+
+ + jm3 + jn3 + jq3 + + + km4 + kq4 + +
+ + + io2 + + + + km5 + + + + + + jm6 + + + + + + + +
+ "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("i:1 [im1][in1][io2][ip1][]"); + logger.AssertLogContains("j:2 [jm3][jn3][][][jq3]"); + logger.AssertLogContains("j:1 [jm6][in1][io2][ip1][jq3]"); + logger.AssertLogContains("k:3 [km5][][][][kq4]"); + logger.AssertLogContains("k:2 [jm3][jn3][][][jq3]"); + logger.AssertLogContains("k:1 [jm6][in1][io2][ip1][jq3]"); + } + + [TestMethod] + public void MutualReferenceToDefinition1() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + ~%(m)~ + + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1][~m1~]"); + } + + [TestMethod] + public void MutualReferenceToDefinition2() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + ~%(n)~ + n1 + + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[~~][n1]"); + } + + [TestMethod] + public void MutualReferenceToDefinition3() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + m1 + %(i.m) + %(j.m) + + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[m1][m1][]"); + } + + [TestMethod] + public void ProjectReevaluationReevaluatesItemDefinitions() + { + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + CODEANALYSIS + + + + + + + $(Defines);RETAIL + $(Defines);DEBUG + + + + + + + "))); + + p.SetProperty("BuildFlavor", "ret"); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[a.cpp==CODEANALYSIS;RETAIL]"); + + Assert.IsTrue(ItemContainsMetadata(p, "CppCompile", "a.cpp", "Defines", "CODEANALYSIS;RETAIL")); + + p.SetProperty("BuildFlavor", "chk"); + p.ReevaluateIfNecessary(); + + Assert.IsTrue(ItemContainsMetadata(p, "CppCompile", "a.cpp", "Defines", "CODEANALYSIS;DEBUG")); + } + + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void MSBuildCallDoesNotAffectCallingProjectsDefinitions() + { + string otherProject = null; + + try + { + otherProject = FileUtilities.GetTemporaryFile(); + string otherProjectContent = @" + + + + + + m2 + + + + + + "; + + using (StreamWriter writer = new StreamWriter(otherProject)) + { + writer.Write(otherProjectContent); + } + + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + m1 + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[PARENT-before:m1]", "[CHILD:m2]", "[PARENT-after:m1]"); + } + finally + { + File.Delete(otherProject); + } + } + + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DefaultMetadataTravelWithTargetOutputs() + { + string otherProject = null; + + try + { + otherProject = FileUtilities.GetTemporaryFile(); + string otherProjectContent = @" + + + m1 + + + + + n1 + + + + + + "; + + using (StreamWriter writer = new StreamWriter(otherProject)) + { + writer.Write(otherProjectContent); + } + + MockLogger logger = new MockLogger(); + Project p = new Project(XmlReader.Create(new StringReader(@" + + + + + + + + + "))); + + p.Build("t", new ILogger[] { logger }); + + logger.AssertLogContains("[CHILD:i1:m=m1,n=n1]", "[PARENT:i1:m=m1,n=n1]"); + } + finally + { + File.Delete(otherProject); + } + } + + #endregion + /// + /// Determines if the specified item contains the specified metadata + /// + /// The project. + /// The item type. + /// The item include. + /// The metadata name. + /// The metadata value. + /// True if the item contains the metadata, false otherwise. + private bool ItemContainsMetadata(Project project, string itemType, string itemInclude, string name, string value) + { + foreach (ProjectItem item in project.GetItems(itemType)) + { + if (item.EvaluatedInclude == itemInclude) + { + return ContainsMetadata(item.Metadata, name, value); + } + } + + return false; + } + + /// + /// Determines if the metadata collection contains the named metadata with the specified value + /// + /// The collection. + /// The metadata name. + /// The metadata value. + /// True if the collection contains the metadata, false otherwise. + private bool ContainsMetadata(IEnumerable metadata, string name, string value) + { + foreach (ProjectMetadata metadataEntry in metadata) + { + if (metadataEntry.Name == name && metadataEntry.EvaluatedValue == value) + { + return true; + } + } + + return false; + } + } + + public class ItemDefinitionGroup : Microsoft.Build.Utilities.Task + { + public override bool Execute() + { + Log.LogMessage("In ItemDefinitionGroup task."); + return true; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ProjectHelpers.cs b/src/XMakeBuildEngine/UnitTests/Definition/ProjectHelpers.cs new file mode 100644 index 00000000000..492a106bda4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ProjectHelpers.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helper class to create projects for testing.. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Contains helper methods for creating projects for testing. + /// + internal static class ProjectHelpers + { + /// + /// Creates a project instance with a single empty target named 'foo' + /// + /// A project instance. + internal static ProjectInstance CreateEmptyProjectInstance() + { + XmlReader reader = XmlReader.Create(new StringReader + ( + @" + + " + )); + + Project project = new Project(reader); + ProjectInstance instance = project.CreateProjectInstance(); + + return instance; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ProjectItem_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/ProjectItem_Tests.cs new file mode 100644 index 00000000000..575791338f2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ProjectItem_Tests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for ProjectInstance internal members +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Linq; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using System.Text; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using System.Threading; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ProjectItemFactory = Microsoft.Build.Evaluation.ProjectItem.ProjectItemFactory; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.UnitTests.Definition +{ + /// + /// Class containing tests for the ProjectItem and related functionality. + /// + [TestClass] + public class ProjectItem_Tests + { + /// + /// Make sure the the CopyFrom actually does a clone. + /// + [TestMethod] + public void CopyFromClonesMetadata() + { + ProjectItem item1 = GetOneItemFromFragment(@"m1"); + ProjectItemFactory factory = new ProjectItemFactory(item1.Project, item1.Xml); + ProjectItem item2 = factory.CreateItem(item1, item1.Project.FullPath); + + item1.SetMetadataValue("m", "m2"); + item1.SetMetadataValue("n", "n1"); + + Assert.AreEqual(1, Helpers.MakeList(item2.Metadata).Count); + Assert.AreEqual(String.Empty, item2.GetMetadataValue("n")); + Assert.AreEqual(1 + 15 /* built-in metadata */, item2.MetadataCount); + + // Should still point at the same XML items + Assert.AreEqual(true, Object.ReferenceEquals(item1.DirectMetadata.First().Xml, item2.DirectMetadata.First().Xml)); + } + + /// + /// Get items of item type "i" with using the item xml fragment passed in + /// + private static IList GetItemsFromFragment(string fragment) + { + string content = String.Format + ( + ObjectModelHelpers.CleanupFileContents(@" + + + {0} + + + "), + fragment + ); + + IList items = GetItems(content); + return items; + } + + /// + /// Get the item of type "i" using the item Xml fragment provided. + /// If there is more than one, fail. + /// + private static ProjectItem GetOneItemFromFragment(string fragment) + { + IList items = GetItemsFromFragment(fragment); + + Assert.AreEqual(1, items.Count); + return items[0]; + } + + /// + /// Get the items of type "i" in the project provided + /// + private static IList GetItems(string content) + { + ProjectRootElement projectXml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(projectXml); + IList item = Helpers.MakeList(project.GetItems("i")); + + return item; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/Definition/Project_Internal_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/Project_Internal_Tests.cs new file mode 100644 index 00000000000..fcb5b6cabda --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/Project_Internal_Tests.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for Project that involve some reference to internal code +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.IO; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InternalUtilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.UnitTests.Definition +{ + /// + /// Tests some manipulations of Project and ProjectCollection that require dealing with internal data. + /// + [TestClass] + public class Project_Internal_Tests + { + /// + /// Set default tools version; subsequent projects should use it + /// + [TestMethod] + public void SetDefaultToolsVersion() + { + string oldValue = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + // In the new world of figuring out the ToolsVersion to use, we completely ignore the default + // ToolsVersion in the ProjectCollection. However, this test explicitly depends on modifying + // that, so we need to turn the new defaulting behavior off in order to verify that this still works. + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection collection = new ProjectCollection(); + collection.AddToolset(new Toolset("x", @"c:\y", collection, null)); + + collection.DefaultToolsVersion = "x"; + + Assert.AreEqual("x", collection.DefaultToolsVersion); + + string content = @" + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content)), null, null, collection); + + Assert.AreEqual(project.ToolsVersion, "x"); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldValue); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// If the ToolsVersion in the project file is bogus, we'll default to the current ToolsVersion and successfully + /// load it. Make sure we can RE-load it, too, and successfully pick up the correct copy of the loaded project. + /// + /// ... Make sure we can do this even if we're not using the "always default everything to current anyway" codepath. + /// + [TestMethod] + public void ReloadProjectWithInvalidToolsVersionInFile() + { + string oldValue = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + // In the new world of figuring out the ToolsVersion to use, we completely ignore the default + // ToolsVersion in the ProjectCollection. However, this test explicitly depends on modifying + // that, so we need to turn the new defaulting behavior off in order to verify that this still works. + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + string content = @" + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + project.FullPath = "c:\\123.proj"; + + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject("c:\\123.proj", null, null); + + Assert.IsTrue(Object.ReferenceEquals(project, project2)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldValue); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// Project.ToolsVersion should be set to ToolsVersion evaluated with, + /// even if it is subsequently changed on the XML (without reevaluation) + /// + [TestMethod] + public void ProjectToolsVersion20Present() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // "Requires 2.0 to be installed" + return; + } + + string oldValue = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + // In the new world of figuring out the ToolsVersion to use, we completely ignore what + // is written in the project file. However, this test explicitly depends on effectively + // modifying the "project file" (through the construction model OM), so we need to turn + // that behavior off in order to verify that it still works. + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + Project project = new Project(); + project.Xml.ToolsVersion = "2.0"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual("2.0", project.ToolsVersion); + + project.Xml.ToolsVersion = "4.0"; + + Assert.AreEqual("2.0", project.ToolsVersion); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldValue); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// $(MSBuildToolsVersion) should be set to ToolsVersion evaluated with, + /// even if it is subsequently changed on the XML (without reevaluation) + /// + [TestMethod] + public void MSBuildToolsVersionProperty() + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + // "Requires 2.0 to be installed" + return; + } + + string oldValue = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + // In the new world of figuring out the ToolsVersion to use, we completely ignore what + // is written in the project file. However, this test explicitly depends on effectively + // modifying the "project file" (through the construction model OM), so we need to turn + // that behavior off in order to verify that it still works. + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + Project project = new Project(); + project.Xml.ToolsVersion = "2.0"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual("2.0", project.GetPropertyValue("msbuildtoolsversion")); + + project.Xml.ToolsVersion = "4.0"; + Assert.AreEqual("2.0", project.GetPropertyValue("msbuildtoolsversion")); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual("4.0", project.GetPropertyValue("msbuildtoolsversion")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldValue); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ToolsVersion_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/ToolsVersion_Tests.cs new file mode 100644 index 00000000000..786055b9bcb --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ToolsVersion_Tests.cs @@ -0,0 +1,1000 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Xml; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; + +using LoggingService = Microsoft.Build.BackEnd.Logging.LoggingService; +using LoggerMode = Microsoft.Build.BackEnd.Logging.LoggerMode; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using InternalUtilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.UnitTests.Definition +{ + [TestClass] + public class ToolsetState_Tests + { + [TestMethod] + public void OverrideTasksAreFoundInOverridePath() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + ProjectCollection e = new ProjectCollection(); + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), "c:\\msbuildoverridetasks", new DirectoryExists(this.directoryExists)); + + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + TaskRegistry taskRegistry = (TaskRegistry)t.GetTaskRegistry(service, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), e.ProjectRootElementCache); + TaskRegistry taskoverrideRegistry = (TaskRegistry)t.GetOverrideTaskRegistry(service, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), e.ProjectRootElementCache); + + string[] expectedRegisteredTasks = { "a1", "a2", "a3", "a4", "b1", "e1", "g1", "g2", "g3" }; + string[] expectedOverrideTasks = { "a1" /* special because it is in the override tasks file as well as in the tasks file*/, "oa1", "oa2", "og1", "ooo" }; + + string[] unexpectedRegisteredTasks = { "c1", "d1", "f1", "11", "12", "13", "21", "oa1", "oa2", "og1", "ooo" }; + string[] unexpectedOverrideRegisteredTasks = { "c1", "d1", "f1", "11", "12", "13", "21", "a2", "a3", "a4", "b1", "e1", "g1", "g2", "g3" }; + + foreach (string expectedRegisteredTask in expectedRegisteredTasks) + { + Assert.IsTrue(taskRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(expectedRegisteredTask, null)), + String.Format("Expected task '{0}' registered!", expectedRegisteredTask)); + } + + foreach (string expectedRegisteredTask in expectedOverrideTasks) + { + Assert.IsTrue(taskoverrideRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(expectedRegisteredTask, null)), + String.Format("Expected task '{0}' registered!", expectedRegisteredTask)); + } + + foreach (string unexpectedRegisteredTask in unexpectedRegisteredTasks) + { + Assert.IsFalse(taskRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(unexpectedRegisteredTask, null)), + String.Format("Unexpected task '{0}' registered!", unexpectedRegisteredTask)); + } + + foreach (string unexpectedRegisteredTask in unexpectedOverrideRegisteredTasks) + { + Assert.IsFalse(taskoverrideRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(unexpectedRegisteredTask, null)), + String.Format("Unexpected task '{0}' registered!", unexpectedRegisteredTask)); + } + } + + [TestMethod] + public void OverrideTaskPathIsRelative() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + ProjectCollection e = new ProjectCollection(); + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), "msbuildoverridetasks", new DirectoryExists(this.directoryExists)); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + TaskRegistry taskoverrideRegistry = (TaskRegistry)t.GetOverrideTaskRegistry(service, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), e.ProjectRootElementCache); + Assert.IsNotNull(taskoverrideRegistry); + Assert.IsTrue(taskoverrideRegistry.TaskRegistrations.Count == 0); + string rootedPathMessage = ResourceUtilities.FormatResourceString("OverrideTaskNotRootedPath", "msbuildoverridetasks"); + mockLogger.AssertLogContains(ResourceUtilities.FormatResourceString("OverrideTasksFileFailure", rootedPathMessage)); + } + + [TestMethod] + public void OverrideTaskPathHasInvalidChars() + { + ProjectCollection e = new ProjectCollection(); + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), "k:\\||^%$#*msbuildoverridetasks", new DirectoryExists(this.directoryExists)); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + TaskRegistry taskoverrideRegistry = (TaskRegistry)t.GetOverrideTaskRegistry(service, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), e.ProjectRootElementCache); + Assert.IsNotNull(taskoverrideRegistry); + Assert.IsTrue(taskoverrideRegistry.TaskRegistrations.Count == 0); + mockLogger.AssertLogContains("MSB4194"); + } + + [TestMethod] + public void OverrideTaskPathHasTooLongOfAPath() + { + string tooLong = "c:\\" + new string('C', 6000); + ProjectCollection e = new ProjectCollection(); + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), tooLong, new DirectoryExists(this.directoryExists)); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + TaskRegistry taskoverrideRegistry = (TaskRegistry)t.GetOverrideTaskRegistry(service, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), e.ProjectRootElementCache); + Assert.IsNotNull(taskoverrideRegistry); + Assert.IsTrue(taskoverrideRegistry.TaskRegistrations.Count == 0); + string rootedPathMessage = ResourceUtilities.FormatResourceString("OverrideTaskNotRootedPath", tooLong); + mockLogger.AssertLogContains(ResourceUtilities.FormatResourceString("OverrideTasksFileFailure", rootedPathMessage)); + } + + [TestMethod] + public void OverrideTaskPathIsNotFound() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + ProjectCollection e = new ProjectCollection(); + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), "k:\\Thecatinthehat", new DirectoryExists(this.directoryExists)); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + TaskRegistry taskoverrideRegistry = (TaskRegistry)t.GetOverrideTaskRegistry(service, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), e.ProjectRootElementCache); + Assert.IsNotNull(taskoverrideRegistry); + Assert.IsTrue(taskoverrideRegistry.TaskRegistrations.Count == 0); + string rootedPathMessage = ResourceUtilities.FormatResourceString("OverrideTaskNotRootedPath", "k:\\Thecatinthehat"); + mockLogger.AssertLogContains(ResourceUtilities.FormatResourceString("OverrideTasksFileFailure", rootedPathMessage)); + } + + [TestMethod] + public void DefaultTasksAreFoundInToolsPath() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), null, new DirectoryExists(this.directoryExists)); + + TaskRegistry taskRegistry = (TaskRegistry)t.GetTaskRegistry(null, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); + + string[] expectedRegisteredTasks = { "a1", "a2", "a3", "a4", "b1", "e1", "g1", "g2", "g3" }; + string[] unexpectedRegisteredTasks = { "c1", "d1", "f1", "11", "12", "13", "21" }; + + foreach (string expectedRegisteredTask in expectedRegisteredTasks) + { + Assert.IsTrue(taskRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(expectedRegisteredTask, null)), + String.Format("Expected task '{0}' registered!", expectedRegisteredTask)); + } + foreach (string unexpectedRegisteredTask in unexpectedRegisteredTasks) + { + Assert.IsFalse(taskRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(unexpectedRegisteredTask, null)), + String.Format("Unexpected task '{0}' registered!", unexpectedRegisteredTask)); + } + } + + [TestMethod] + public void WarningLoggedIfNoDefaultTasksFound() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + Toolset t = new Toolset("toolsversionname", "c:\\directory1\\directory2\\doesntexist", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), null, new DirectoryExists(this.directoryExists)); + + TaskRegistry taskRegistry = (TaskRegistry)t.GetTaskRegistry(service, BuildEventContext.Invalid, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); + + string[] unexpectedRegisteredTasks = { "a1", "a2", "a3", "a4", "b1", "c1", "d1", "e1", "f1", "g1", "g2", "g3", "11", "12", "13", "21" }; + + Assert.AreEqual(1, mockLogger.WarningCount, "Expected 1 warning logged!"); + foreach (string unexpectedRegisteredTask in unexpectedRegisteredTasks) + { + Assert.IsFalse(taskRegistry.TaskRegistrations.ContainsKey(new TaskRegistry.RegisteredTaskIdentity(unexpectedRegisteredTask, null)), + String.Format("Unexpected task '{0}' registered!", unexpectedRegisteredTask)); + } + } + + [TestMethod] + public void InvalidToolPath() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + Toolset t = new Toolset("toolsversionname", "invalid||path", new PropertyDictionary(), p, new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), null, new DirectoryExists(this.directoryExists)); + + TaskRegistry taskRegistry = (TaskRegistry)t.GetTaskRegistry(service, BuildEventContext.Invalid, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); + + Console.WriteLine(mockLogger.FullLog); + Assert.AreEqual(1, mockLogger.WarningCount, "Expected a warning for invalid character in toolpath"); + } + + /// + /// Make sure when we read in the tasks files off disk that they come in in a sorted order so that there is a deterministric way of + /// figurting out the order the files were read in. + /// + [TestMethod] + public void VerifyTasksFilesAreInSortedOrder() + { + //Note Engine's BinPath is distinct from the ToolsVersion's ToolsPath + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + string[] foundFiles = Toolset.GetTaskFiles(new DirectoryGetFiles(this.getFiles), service, BuildEventContext.Invalid, "*.tasks", "c:\\directory1\\directory2", String.Empty); + string[] foundoverrideFiles = Toolset.GetTaskFiles(new DirectoryGetFiles(this.getFiles), service, BuildEventContext.Invalid, "*.overridetasks", "c:\\msbuildoverridetasks", String.Empty); + + List sortedTasksExpectedPaths = new List(); + List sortedOverrideExpectedPaths = new List(); + + foreach (DefaultTasksFile file in _defaultTasksFileCandidates) + { + if (Path.GetDirectoryName(file.Path).Equals("c:\\directory1\\directory2", StringComparison.OrdinalIgnoreCase) && file.Path.EndsWith(".tasks", StringComparison.OrdinalIgnoreCase)) + { + sortedTasksExpectedPaths.Add(file.Path); + } + + if (Path.GetDirectoryName(file.Path).Equals("c:\\msbuildoverridetasks", StringComparison.OrdinalIgnoreCase) && file.Path.EndsWith(".overridetasks", StringComparison.OrdinalIgnoreCase)) + { + sortedOverrideExpectedPaths.Add(file.Path); + } + } + + sortedTasksExpectedPaths.Sort(StringComparer.OrdinalIgnoreCase); + sortedOverrideExpectedPaths.Sort(StringComparer.OrdinalIgnoreCase); + + Assert.IsTrue(sortedTasksExpectedPaths.Count == foundFiles.Length); + for (int i = 0; i < foundFiles.Length; i++) + { + Assert.IsTrue(sortedTasksExpectedPaths[i].Equals(foundFiles[i], StringComparison.OrdinalIgnoreCase)); + } + + + Assert.IsTrue(sortedOverrideExpectedPaths.Count == foundoverrideFiles.Length); + for (int i = 0; i < foundoverrideFiles.Length; i++) + { + Assert.IsTrue(sortedOverrideExpectedPaths[i].Equals(foundoverrideFiles[i], StringComparison.OrdinalIgnoreCase)); + } + } + + [TestMethod] + public void InvalidToolsVersionTooHighMappedToCurrent() + { + string oldLegacyToolsVersion = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + string oldTreatHigherToolsVersions = Environment.GetEnvironmentVariable("MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT", "1"); + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + + mockLogger.AssertLogContains("ToolsVersion=\"98.6\""); + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT", oldTreatHigherToolsVersions); + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldLegacyToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + [TestMethod] + public void InvalidToolsVersionMissingLowMappedToCurrent() + { + string oldLegacyToolsVersion = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"0.1\""); + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldLegacyToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + [TestMethod] + public void InvalidToolsVersionMissingMappedToCurrent() + { + string oldLegacyToolsVersion = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"invalidToolsVersion\""); + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldLegacyToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidToolsVersion() + { + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, "goober", p); + success = project.Build(mockLogger); + // BANG! + } + + /// + /// Even a valid toolsversion should be forced to the current ToolsVersion if MSBUILDTREATALLTOOLSVERSIONSASCURRENT + /// is set. + /// + [TestMethod] + public void ToolsVersionMappedToCurrent() + { + string oldLegacyToolsVersion = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + string oldForceToolsVersionToCurrent = Environment.GetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldLegacyToolsVersion); + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", oldForceToolsVersionToCurrent); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// Validate that a custom defined toolset is honored + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void CustomToolsVersionIsHonored() + { + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", String.Empty); + try + { + string content = @" + + + + +"; + string projectPath = Path.GetTempFileName(); + File.WriteAllText(projectPath, content); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + Toolset source = p.GetToolset("14.0"); + Toolset potato = new Toolset("potato", source.ToolsPath, ProjectCollection.GlobalProjectCollection, source.ToolsPath); + p.AddToolset(potato); + + bool success = false; + Project project = p.LoadProject(projectPath, "potato"); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("[potato]"); + } + finally + { + // Nothing + } + } + + /// + /// If the current ToolsVersion doesn't exist, we should fall back to what's in the project file. + /// + [TestMethod] + public void ToolsVersionFallbackIfCurrentToolsVersionDoesNotExist() + { + ProjectCollection p = new ProjectCollection(); + p.RemoveToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + Assert.AreEqual("4.0", project.ToolsVersion); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("\"4.0\""); + mockLogger.AssertLogDoesntContain(ObjectModelHelpers.CleanupFileContents("\"msbuilddefaulttoolsversion\"")); + } + + /// + /// If MSBUILDTREATALLTOOLSVERSIONSASCURRENT is not set, and there is not an explicit ToolsVersion passed to the project, + /// then if MSBUILDDEFAULTTOOLSVERSION is set and exists, use that ToolsVersion. + /// + [TestMethod] + public void ToolsVersionFromEnvironmentVariable() + { + string oldDefaultToolsVersion = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", "foo"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + p.AddToolset(new Toolset("foo", @"c:\foo", p, @"c:\foo\override")); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + mockLogger.AssertLogContains("ToolsVersion=\"foo\""); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", oldDefaultToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// If MSBUILDTREATALLTOOLSVERSIONSASCURRENT is not set, and there is not an explicit ToolsVersion passed to the project, + /// and if MSBUILDDEFAULTTOOLSVERSION is set but to an invalid ToolsVersion, fall back to current. + /// + [TestMethod] + public void InvalidToolsVersionFromEnvironmentVariable() + { + string oldDefaultToolsVersion = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", "foo"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = project.Build(mockLogger); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + // falls back to the current ToolsVersion + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", oldDefaultToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// Even a valid toolsversion should be forced to the current ToolsVersion if MSBUILDTREATALLTOOLSVERSIONSASCURRENT + /// is set. + /// + [TestMethod] + public void ToolsVersionMappedToCurrent_CreateProjectInstance() + { + string oldLegacyToolsVersion = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + string oldForceToolsVersionToCurrent = Environment.GetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = project.CreateProjectInstance(); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldLegacyToolsVersion); + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", oldForceToolsVersionToCurrent); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// If the current ToolsVersion doesn't exist, we should fall back to what's in the project file. + /// + [TestMethod] + public void ToolsVersionFallbackIfCurrentToolsVersionDoesNotExist_CreateProjectInstance() + { + ProjectCollection p = new ProjectCollection(); + p.RemoveToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = project.CreateProjectInstance(); + Assert.AreEqual("4.0", pi.ToolsVersion); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("\"4.0\""); + mockLogger.AssertLogDoesntContain(ObjectModelHelpers.CleanupFileContents("\"msbuilddefaulttoolsversion\"")); + } + + /// + /// If MSBUILDTREATALLTOOLSVERSIONSASCURRENT is not set, and there is not an explicit ToolsVersion passed to the project, + /// then if MSBUILDDEFAULTTOOLSVERSION is set and exists, use that ToolsVersion. + /// + [TestMethod] + public void ToolsVersionFromEnvironmentVariable_CreateProjectInstance() + { + string oldDefaultToolsVersion = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", "foo"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + p.AddToolset(new Toolset("foo", @"c:\foo", p, @"c:\foo\override")); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = project.CreateProjectInstance(); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + mockLogger.AssertLogContains("ToolsVersion=\"foo\""); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", oldDefaultToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// If MSBUILDTREATALLTOOLSVERSIONSASCURRENT is not set, and there is not an explicit ToolsVersion passed to the project, + /// and if MSBUILDDEFAULTTOOLSVERSION is set but to an invalid ToolsVersion, fall back to current. + /// + [TestMethod] + public void InvalidToolsVersionFromEnvironmentVariable_CreateProjectInstance() + { + string oldDefaultToolsVersion = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", "foo"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = project.CreateProjectInstance(); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + // falls back to the current ToolsVersion + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", oldDefaultToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + + /// + /// Even a valid toolsversion should be forced to the current ToolsVersion if MSBUILDTREATALLTOOLSVERSIONSASCURRENT + /// is set. + /// + [TestMethod] + public void ToolsVersionMappedToCurrent_ProjectInstance() + { + string oldLegacyToolsVersion = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + string oldForceToolsVersionToCurrent = Environment.GetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = new ProjectInstance(project.Xml, null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldLegacyToolsVersion); + Environment.SetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT", oldForceToolsVersionToCurrent); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// If the current ToolsVersion doesn't exist, we should fall back to what's in the project file. + /// + [TestMethod] + public void ToolsVersionFallbackIfCurrentToolsVersionDoesNotExist_ProjectInstance() + { + ProjectCollection p = new ProjectCollection(); + p.RemoveToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion); + + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = new ProjectInstance(project.Xml, null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + Assert.AreEqual("4.0", pi.ToolsVersion); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("\"4.0\""); + mockLogger.AssertLogDoesntContain(ObjectModelHelpers.CleanupFileContents("\"msbuilddefaulttoolsversion\"")); + } + + /// + /// If MSBUILDTREATALLTOOLSVERSIONSASCURRENT is not set, and there is not an explicit ToolsVersion passed to the project, + /// then if MSBUILDDEFAULTTOOLSVERSION is set and exists, use that ToolsVersion. + /// + [TestMethod] + public void ToolsVersionFromEnvironmentVariable_ProjectInstance() + { + string oldDefaultToolsVersion = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", "foo"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + p.AddToolset(new Toolset("foo", @"c:\foo", p, @"c:\foo\override")); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = new ProjectInstance(project.Xml, null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + mockLogger.AssertLogContains("ToolsVersion=\"foo\""); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", oldDefaultToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// If MSBUILDTREATALLTOOLSVERSIONSASCURRENT is not set, and there is not an explicit ToolsVersion passed to the project, + /// and if MSBUILDDEFAULTTOOLSVERSION is set but to an invalid ToolsVersion, fall back to current. + /// + [TestMethod] + public void InvalidToolsVersionFromEnvironmentVariable_ProjectInstance() + { + string oldDefaultToolsVersion = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", "foo"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection p = new ProjectCollection(); + MockLogger mockLogger = new MockLogger(); + LoggingService service = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1); + service.RegisterLogger(mockLogger); + + bool success = false; + Project project = new Project(XmlReader.Create(new StringReader(@" + + + ")), null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + + ProjectInstance pi = new ProjectInstance(project.Xml, null /* no global properties */, null /* don't explicitly set the toolsversion */, p); + success = pi.Build(new ILogger[] { mockLogger }); + + Assert.IsTrue(success); + mockLogger.AssertLogContains("ToolsVersion=\"4.0\""); + // falls back to the current ToolsVersion + mockLogger.AssertLogContains(ObjectModelHelpers.CleanupFileContents("ToolsVersion=\"msbuilddefaulttoolsversion\"")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION", oldDefaultToolsVersion); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// Inline tasks found in a .tasks file only have properties expanded. + /// (When they are in a regular MSBuild file, items are also expanded.) + /// + [TestMethod] + public void InlineTasksInDotTasksFile() + { + Toolset t = new Toolset("t", "c:\\inline", new PropertyDictionary(), new ProjectCollection(), new DirectoryGetFiles(this.getFiles), new LoadXmlFromPath(this.loadXmlFromPath), null, new DirectoryExists(directoryExists)); + + TaskRegistry taskRegistry = (TaskRegistry)t.GetTaskRegistry(null, new BuildEventContext(1, 2, BuildEventContext.InvalidProjectContextId, 4), ProjectCollection.GlobalProjectCollection.ProjectRootElementCache); + + // Did not crash due to trying to expand items without having items + } + + public ToolsetState_Tests() + { + _defaultTasksFileMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DefaultTasksFile defaultTasksFileCandidate in _defaultTasksFileCandidates) + { + _defaultTasksFileMap.Add(defaultTasksFileCandidate.Path, defaultTasksFileCandidate.XmlContents); + } + } + + private bool directoryExists(string path) + { + // run throught directory exits to throw the correct exceptions if there are any + Directory.Exists(path); + return path.Contains("msbuildoverridetasks"); + } + + private string[] getFiles(string path, string pattern) + { + // Cause an exception if the path is invalid + Path.GetFileName(path); + + string pathWithoutTrailingSlash = path.EndsWith("\\") ? path.Substring(0, path.Length - 1) : path; + //NOTE: the Replace calls below are a very minimal attempt to convert a basic, cmd.exe-style wildcard + //into something Regex.IsMatch will know how to use. + string finalPattern = "^" + pattern.Replace(".", "\\.").Replace("*", "[\\w\\W]*") + "$"; + + List matches = new List(_defaultTasksFileMap.Keys); + matches.RemoveAll( + delegate (string candidate) + { + bool sameFolder = (0 == String.Compare(Path.GetDirectoryName(candidate), + pathWithoutTrailingSlash, + StringComparison.OrdinalIgnoreCase)); + return !sameFolder || !Regex.IsMatch(Path.GetFileName(candidate), finalPattern); + }); + return matches.ToArray(); + } + + private XmlDocumentWithLocation loadXmlFromPath(string path) + { + string xmlContents = _defaultTasksFileMap[path]; + XmlDocumentWithLocation xmlDocument = new XmlDocumentWithLocation(); + xmlDocument.LoadXml(xmlContents); + return xmlDocument; + } + + private readonly Dictionary _defaultTasksFileMap; + + private DefaultTasksFile[] _defaultTasksFileCandidates = + { new DefaultTasksFile( + "c:\\directory1\\directory2\\a.tasks", + @" + + + + + "), + new DefaultTasksFile("c:\\directory1\\directory2\\b.tasks", + @" + + "), + new DefaultTasksFile("c:\\directory1\\directory2\\c.tasksfile", + @" + + "), + new DefaultTasksFile("c:\\directory1\\directory2\\directory3\\d.tasks", + @" + + "), + new DefaultTasksFile("c:\\directory1\\directory2\\e.tasks", + @" + + "), + new DefaultTasksFile("d:\\directory1\\directory2\\f.tasks", + @" + + "), + new DefaultTasksFile("c:\\directory1\\directory2\\g.custom.tasks", + @" + + + + "), + new DefaultTasksFile("c:\\somepath\\1.tasks", + @" + + + + "), + new DefaultTasksFile("c:\\somepath\\2.tasks", + @" + + "), + new DefaultTasksFile("c:\\inline\\inlinetasks.tasks", + @" + + + + + + x + + + "), + new DefaultTasksFile("c:\\msbuildoverridetasks\\1.overridetasks", + @" + + + + + "), + new DefaultTasksFile("c:\\msbuildoverridetasks\\2.overridetasks", + @" + + ") +}; + + public struct DefaultTasksFile + { + public string Path; + public string XmlContents; + public DefaultTasksFile(string path, string xmlContents) + { + this.Path = path; + this.XmlContents = xmlContents; + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReaderTestHelper.cs b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReaderTestHelper.cs new file mode 100644 index 00000000000..c361472a07c --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReaderTestHelper.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Configuration; +using Microsoft.Win32; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Helper class to simulate application configuration read + /// + internal class ToolsetConfigurationReaderTestHelper + { + private static ExeConfigurationFileMap s_configFile; + private static string s_testFolderFullPath = null; + private static Exception s_exceptionToThrow = null; + + internal static string WriteConfigFile(string content) + { + return WriteConfigFile(ObjectModelHelpers.CleanupFileContents(content), null); + } + + internal static string WriteConfigFile(string content, Exception exception) + { + s_exceptionToThrow = exception; + s_testFolderFullPath = Path.Combine(Path.GetTempPath(), "configFileTests"); + Directory.CreateDirectory(s_testFolderFullPath); + string configFilePath = Path.Combine(s_testFolderFullPath, "test.exe.config"); + + if (File.Exists(configFilePath)) + { + File.Delete(configFilePath); + } + + File.WriteAllText(configFilePath, content); + s_configFile = new ExeConfigurationFileMap(); + s_configFile.ExeConfigFilename = configFilePath; + return configFilePath; + } + + internal static void CleanUp() + { + try + { + if (s_testFolderFullPath != null && Directory.Exists(s_testFolderFullPath)) + { + for (int i = 0; i < 5; i++) + { + try + { + Directory.Delete(s_testFolderFullPath, true /* recursive */); + break; + } + catch (Exception) + { + Thread.Sleep(1000); + // Eat exceptions from the delete + } + } + } + } + finally + { + s_exceptionToThrow = null; + } + } + + /// + /// Creates a config file and loads a Configuration from it + /// + /// configuration object + internal static Configuration ReadApplicationConfigurationTest() + { + if (s_exceptionToThrow != null) + { + throw s_exceptionToThrow; + } + + return ConfigurationManager.OpenMappedExeConfiguration(s_configFile, ConfigurationUserLevel.None); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReader_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReader_Tests.cs new file mode 100644 index 00000000000..d2dcfa7b13d --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetConfigurationReader_Tests.cs @@ -0,0 +1,485 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Configuration; +using Microsoft.Win32; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; + +using ToolsetConfigurationSection = Microsoft.Build.Evaluation.ToolsetConfigurationSection; + +namespace Microsoft.Build.UnitTests.Definition +{ + /// + /// Unit tests for ToolsetConfigurationReader class + /// + [TestClass] + public class ToolsetConfigurationReaderTests + { + private static string s_msbuildToolsets = "msbuildToolsets"; + + [TestInitialize] + public void Setup() + { + } + + [TestCleanup] + public void Teardown() + { + ToolsetConfigurationReaderTestHelper.CleanUp(); + } + + #region "msbuildToolsets element tests" + + /// + /// msbuildToolsets element is empty + /// + [TestMethod] + public void MSBuildToolsetsTest_EmptyElement() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.MSBuildOverrideTasksPath, null); + Assert.IsNotNull(msbuildToolsetSection); + Assert.AreEqual(null, msbuildToolsetSection.Default); + Assert.IsNotNull(msbuildToolsetSection.Toolsets); + Assert.AreEqual(0, msbuildToolsetSection.Toolsets.Count); + } + + /// + /// tests if ToolsetConfigurationReaderTests is successfully initialized from the config file + /// + [TestMethod] + public void MSBuildToolsetsTest_Basic() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + ConfigurationSection section = config.GetSection(s_msbuildToolsets); + ToolsetConfigurationSection msbuildToolsetSection = section as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.MSBuildOverrideTasksPath, null); + Assert.AreEqual(msbuildToolsetSection.Default, "2.0"); + Assert.AreEqual(1, msbuildToolsetSection.Toolsets.Count); + + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement(0).toolsVersion, "2.0"); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.Count, 1); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.GetElement("MSBuildBinPath").Value, + @"D:\windows\Microsoft.NET\Framework\v2.0.x86ret\"); + } + + /// + /// Tests if ToolsetConfigurationReaderTests is successfully initialized from the config file when msbuildOVerrideTasksPath is set. + /// Also verify the msbuildOverrideTasksPath is properly read in. + /// + [TestMethod] + public void MSBuildToolsetsTest_Basic2() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + ConfigurationSection section = config.GetSection(s_msbuildToolsets); + ToolsetConfigurationSection msbuildToolsetSection = section as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.MSBuildOverrideTasksPath, "c:\\foo"); + } + + /// + /// Tests if ToolsetConfigurationReaderTests is successfully initialized from the config file and that msbuildOVerrideTasksPath + /// is correctly read in when the value is empty. + /// + [TestMethod] + public void MSBuildToolsetsTest_Basic3() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + ConfigurationSection section = config.GetSection(s_msbuildToolsets); + ToolsetConfigurationSection msbuildToolsetSection = section as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.MSBuildOverrideTasksPath, null); + } + + /// + /// tests if ToolsetConfigurationReaderTests is successfully initialized from the config file + /// + [TestMethod] + public void MSBuildToolsetsTest_BasicWithOtherConfigEntries() + { + // NOTE: for some reason, MUST be the first element under + // for the API to read it. The docs don't make this clear. + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + + + + + + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.Default, "2.0"); + Assert.AreEqual(1, msbuildToolsetSection.Toolsets.Count); + + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement(0).toolsVersion, "2.0"); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.Count, 1); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.GetElement("MSBuildBinPath").Value, + @"D:\windows\Microsoft.NET\Framework\v2.0.x86ret\"); + } + #endregion + + #region "toolsVersion element tests" + + #region "Invalid cases (exception is expected to be thrown)" + + /// + /// name attribute is missing from toolset element + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void ToolsVersionTest_NameNotSpecified() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + + /// + /// More than 1 toolset element with the same name + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void ToolsVersionTest_MultipleElementsWithSameName() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + + /// + /// empty toolset element + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void ToolsVersionTest_EmptyElement() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + + #endregion + + #region "Valid cases (No exception expected)" + + /// + /// only 1 toolset is specified + /// + [TestMethod] + public void ToolsVersionTest_SingleElement() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.Default, "4.0"); + Assert.AreEqual(1, msbuildToolsetSection.Toolsets.Count); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement(0).toolsVersion, "4.0"); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("4.0").PropertyElements.Count, 1); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("4.0").PropertyElements.GetElement("MSBuildBinPath").Value, + @"D:\windows\Microsoft.NET\Framework\v3.5.x86ret\"); + } + #endregion + #endregion + + #region "Property" + + #region "Invalid cases (exception is expected to be thrown)" + + /// + /// name attribute is missing + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void PropertyTest_NameNotSpecified() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + + /// + /// value attribute is missing + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void PropertyTest_ValueNotSpecified() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + + /// + /// more than 1 property element with the same name + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void PropertyTest_MultipleElementsWithSameName() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + + /// + /// property element is an empty element + /// + [TestMethod] + [ExpectedException(typeof(ConfigurationErrorsException))] + public void PropertyTest_EmptyElement() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + } + #endregion + + #region "Valid cases" + + /// + /// more than 1 property element specified + /// + [TestMethod] + public void PropertyTest_MultipleElement() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + + Assert.AreEqual(msbuildToolsetSection.Default, "2.0"); + Assert.AreEqual(1, msbuildToolsetSection.Toolsets.Count); + Assert.AreEqual(2, msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.Count); + + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.GetElement("MSBuildBinPath").Value, + @"D:\windows\Microsoft.NET\Framework\v2.0.x86ret\"); + Assert.AreEqual(msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.GetElement("SomeOtherPropertyName").Value, + @"SomeOtherPropertyValue"); + } + + /// + /// tests GetElement(string name) function in propertycollection class + /// + [TestMethod] + public void PropertyTest_GetValueByName() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(ObjectModelHelpers.CleanupFileContents(@" + + +
+ + + + + + + + ")); + + Configuration config = ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest(); + + ToolsetConfigurationSection msbuildToolsetSection = config.GetSection(s_msbuildToolsets) as ToolsetConfigurationSection; + + // Verifications + Assert.AreEqual(msbuildToolsetSection.Default, "2.0"); + Assert.AreEqual(1, msbuildToolsetSection.Toolsets.Count); + Assert.AreEqual(2, msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.Count); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v2.0.x86ret\", + msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.GetElement("MSBuildBinPath").Value); + Assert.AreEqual(@"SomeOtherPropertyValue", + msbuildToolsetSection.Toolsets.GetElement("2.0").PropertyElements.GetElement("SomeOtherPropertyName").Value); + } + + #endregion + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ToolsetReader_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetReader_Tests.cs new file mode 100644 index 00000000000..fc2fa77eb88 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetReader_Tests.cs @@ -0,0 +1,2701 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections.Generic; +using System.Configuration; +using Microsoft.Win32; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Collections; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +using RegistryKeyWrapper = Microsoft.Build.Internal.RegistryKeyWrapper; +using RegistryException = Microsoft.Build.Exceptions.RegistryException; +using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; +using InternalUtilities = Microsoft.Build.Internal.Utilities; + +namespace Microsoft.Build.UnitTests.Definition +{ + /// + /// Unit tests for ToolsetReader class and its derived classes + /// + [TestClass] + public class ToolsetReaderTests + { + // The registry key that is passed as the baseKey parameter to the ToolsetRegistryReader class + private RegistryKey _testRegistryKey = null; + // Subkey "4.0" + private RegistryKey _currentVersionRegistryKey = null; + // Subkey "ToolsVersions" + private RegistryKey _toolsVersionsRegistryKey = null; + + // Path to the registry key under HKCU + // Note that this is a test registry key created solely for unit testing. + private const string testRegistryPath = @"msbuildUnitTests"; + + /// + /// Store the value of the "VisualStudioVersion" environment variable here so that + /// we can unset it for the duration of the test. + /// + private string _oldVisualStudioVersion; + + /// + /// Reset the testRegistryKey + /// + [TestInitialize] + public void Setup() + { + TearDown(); + _testRegistryKey = Registry.CurrentUser.CreateSubKey(testRegistryPath); + _currentVersionRegistryKey = Registry.CurrentUser.CreateSubKey(testRegistryPath + "\\" + Constants.AssemblyVersion); + _toolsVersionsRegistryKey = Registry.CurrentUser.CreateSubKey(testRegistryPath + "\\ToolsVersions"); + + _oldVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + } + + [TestCleanup] + public void TearDown() + { + ToolsetConfigurationReaderTestHelper.CleanUp(); + + DeleteTestRegistryKey(); + + Environment.SetEnvironmentVariable("VisualStudioVersion", _oldVisualStudioVersion); + } + + /// + /// Callback for toolset collection + /// + public void ToolsetAdded(Toolset toolset) + { + // Do nothing + } + + /// + /// Helper class to delete the testRegistryKey tree. + /// + private void DeleteTestRegistryKey() + { + if (Registry.CurrentUser.OpenSubKey(testRegistryPath) != null) + { + Registry.CurrentUser.DeleteSubKeyTree(testRegistryPath); + } + } + + /// + /// Test to make sure machine.config file has the section registered + /// and we are picking it up from there. + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_SectionNotRegisteredInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, msbuildOverrideTasksPath); + Assert.AreEqual(null, defaultToolsVersion); + Assert.AreEqual(0, values.Count); + } + + #region "Reading from application configuration file tests" + + /// + /// Tests that the data is correctly populated using function GetToolsetDataFromConfiguration + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_Basic() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual("c:\\Cat", msbuildOverrideTasksPath); + Assert.AreEqual("4.0", defaultOverrideToolsVersion); + Assert.AreEqual("2.0", defaultToolsVersion); + Assert.AreEqual(2, values.Count); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v2.0.x86ret", values["2.0"].ToolsPath); + Assert.AreEqual(0, values["4.0"].Properties.Count); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v4.0.x86ret", values["4.0"].ToolsPath); + } + + /// + /// Relative paths can be used in a config file value + /// + [TestMethod] + public void RelativePathInValue() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("DotDotSlash", @"..\")); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), pg, true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + string expected1 = Path.GetFullPath(Path.Combine(FileUtilities.CurrentExecutableDirectory, @"..\foo")); + string expected2 = Path.GetFullPath(Path.Combine(FileUtilities.CurrentExecutableDirectory, @"..\bar")); + Console.WriteLine(values["2.0"].ToolsPath); + Assert.AreEqual(expected1, values["2.0"].ToolsPath); + Assert.AreEqual(expected2, values["3.0"].ToolsPath); + Assert.AreEqual("..\\Foo", msbuildOverrideTasksPath); + } + + /// + /// Invalid relative path in msbuildbinpath value + /// + [TestMethod] + public void InvalidRelativePath() + { + string invalidRelativePath = @"..\|invalid|"; + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + // Don't crash (consistent with invalid absolute path) + Assert.AreEqual(invalidRelativePath, values["2.0"].ToolsPath); + Assert.AreEqual(null, msbuildOverrideTasksPath); + } + + /// + /// Tests the case where application configuration file is invalid + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_InvalidXmlFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@""); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case where application configuration file is invalid + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_InvalidConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case where application configuration file is empty + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_FileEmpty() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@""); + + ToolsetReader reader = new ToolsetConfigurationReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary(), new ReadApplicationConfiguration( + ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest)); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case when ReadConfiguration throws exception + /// Make sure that we don't eat it and always throw ConfigurationErrorsException + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_ConfigurationExceptionThrown() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@"", new ConfigurationErrorsException()); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // this should throw ... + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case when ReadConfiguration throws exception + /// Make sure that we don't eat it and always throw ConfigurationErrorsException + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_ConfigurationErrorsExceptionThrown() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@"", new ConfigurationErrorsException()); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // this should throw ... + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case where default attribute is not specified in the config file + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_DefaultAttributeNotSpecified() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultToolsVersion); + Assert.AreEqual(1, values.Count); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v2.0.x86ret", values["2.0"].ToolsPath); + Assert.AreEqual("C:\\Cat", msbuildOverrideTasksPath); + } + + /// + /// Default toolset has no toolsVersion element definition + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_DefaultToolsetUndefined() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should not throw + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case where msbuildToolsets is not specified in the config file + /// Basically in the code we should be checking if config.GetSection("msbuildToolsets") returns a null + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_MSBuildToolsetsNodeNotPresent() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultToolsVersion); + Assert.AreEqual(0, values.Count); + } + + /// + /// Tests that we handle empty MSBuildToolsets element correctly + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_EmptyMSBuildToolsetsNode() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultToolsVersion); + Assert.AreEqual(0, values.Count); + } + + /// + /// Tests the case where only default ToolsVersion is specified in the application configuration file + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_OnlyDefaultSpecified() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should not throw + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(0, values.Count); + } + + /// + /// Tests the case where only one ToolsVersion data is specified in the application configuration file + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_OneToolsVersionNode() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual("2.0", defaultToolsVersion); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v2.0.x86ret", values["2.0"].ToolsPath); + Assert.AreEqual(1, values.Count); + } + + /// + /// Tests the case when an invalid value of ToolsVersion is specified + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_ToolsVersionIsEmptyString() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + //this should throw ... + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// If both MSBuildToolsPath and MSBuildBinPath are present, they must match + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetDataFromConfiguration_ToolsPathAndBinPathDiffer() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case when a blank value of PropertyName is specified in the config file + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void BlankPropertyNameInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + //this should throw ... + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case when a blank property name is specified in the registry + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void BlankPropertyNameInRegistry() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "someBinPath"); + rk.SetValue("", "foo"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Tests the case when a blank property name is specified in the registry in a + /// sub-toolset. + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void BlankPropertyNameInRegistrySubToolset() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "someBinPath"); + + RegistryKey subToolsetKey = rk.CreateSubKey("11.0"); + subToolsetKey.SetValue("", "foo"); + + PropertyDictionary globalProperties = new PropertyDictionary(); + globalProperties.Set(ProjectPropertyInstance.Create("VisualStudioVersion", "11.0")); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + globalProperties, + ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Tests the case when a blank property value is specified in the config file + /// + [TestMethod] + public void BlankPropertyValueInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + //this should not throw ... + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case when a blank property value is specified in the registry + /// + [TestMethod] + public void BlankPropertyValueInRegistry() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "someBinPath"); + rk.SetValue("foo", ""); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should not throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Tests the case when a blank property value is specified in the registry + /// + [TestMethod] + public void BlankPropertyValueInRegistrySubToolset() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", @"c:\someBinPath"); + + RegistryKey subToolsetKey = rk.CreateSubKey("11.0"); + subToolsetKey.SetValue("foo", ""); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should not throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("2.0", defaultToolsVersion); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"c:\someBinPath", values["2.0"].ToolsPath); + Assert.AreEqual(1, values["2.0"].SubToolsets.Count); + Assert.AreEqual("", values["2.0"].SubToolsets["11.0"].Properties["foo"].EvaluatedValue); + } + + /// + /// Tests the case when an invalid value of PropertyName is specified in the config file + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void InvalidPropertyNameInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + //this should throw ... + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Tests the case when an invalid value of PropertyName is specified in the registry + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void InvalidPropertyNameInRegistry() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "someBinPath"); + rk.SetValue("foo|bar", "x"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Tests the case when an invalid value of PropertyName is specified in the registry + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void InvalidPropertyNameInRegistrySubToolset() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + + RegistryKey subToolsetKey = rk.CreateSubKey("10.0"); + subToolsetKey.SetValue("MSBuildBinPath", "someBinPath"); + subToolsetKey.SetValue("foo|bar", "x"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + + /// + /// Tests that empty string is an invalid value for MSBuildBinPath + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_PropertyValueIsEmptyString1() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(0, values.Count); + } + + /// + /// Tests that empty string is a valid property value for an arbitrary property + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_PropertyValueIsEmptyString2() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(1, values["2.0"].Properties.Count); + Assert.AreEqual(String.Empty, values["2.0"].Properties["foo"].EvaluatedValue); + } + + /// + /// Tests that any escaped xml in config file, is treated well + /// Note that this comes for free with the current implementation using the + /// framework api to access section in the config file + /// + [TestMethod] + public void GetToolsetDataFromConfiguration_XmlEscapedCharacters() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + ToolsetReader reader = GetStandardConfigurationReader(); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), true, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual("2>.0", defaultToolsVersion); + Assert.AreEqual(1, values.Count); + Assert.AreEqual(@"some>value", values["2>.0"].Properties["foo"].EvaluatedValue); + } + #endregion + + #region "GetToolsetData tests" + + /// + /// Tests the case where registry and config file contains different toolsVersion + /// + [TestMethod] + public void GetToolsetData_NoConflict() + { + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "2.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + // Verifications + Assert.AreEqual(4, values.Count); + Assert.AreEqual("4.5", defaultToolsVersion); + Assert.AreEqual(@"D:\somepath", values["2.0"].ToolsPath); + Assert.AreEqual(@"D:\somepath2", values["4.0"].ToolsPath); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v2.0.x86ret", values["4.5"].ToolsPath); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v4.0.x86ret", values["5.0"].ToolsPath); + } + + /// + /// Tests that ToolsetInitialization are respected. + /// + [TestMethod] + public void ToolsetInitializationFlagsSetToNone() + { + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "22.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("33.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + RegistryKey key2 = _testRegistryKey.CreateSubKey("55.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.None + ); + + // Verifications + Assert.AreEqual(1, values.Count); + + string expectedDefault = "2.0"; + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + expectedDefault = ObjectModelHelpers.MSBuildDefaultToolsVersion; + } + + Assert.AreEqual(expectedDefault, defaultToolsVersion); + } + + /// + /// Tests that ToolsetInitialization are respected. + /// + [TestMethod] + public void ToolsetInitializationFlagsSetToRegistry() + { + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "2.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + + // Verifications + Assert.AreEqual(2, values.Count); + Assert.AreEqual("2.0", defaultToolsVersion); + Assert.AreEqual(@"D:\somepath", values["2.0"].ToolsPath); + Assert.AreEqual(@"D:\somepath2", values["4.0"].ToolsPath); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void ThrowOnNonStringRegistryValueTypes() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "someBinPath"); + + // Non-string + rk.SetValue("QuadWordValue", 42, RegistryValueKind.QWord); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should throw ... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + [TestMethod] + public void PropertiesInRegistryCannotReferToOtherPropertiesInRegistry() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "c:\\x$(p1)"); + rk.SetValue("p0", "$(p1)"); + rk.SetValue("p1", "v"); + rk.SetValue("p2", "$(p1)"); + rk.SetValue("MSBuildToolsPath", "c:\\x$(p1)"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("", values["2.0"].Properties["p0"].EvaluatedValue); + Assert.AreEqual("v", values["2.0"].Properties["p1"].EvaluatedValue); + Assert.AreEqual("", values["2.0"].Properties["p2"].EvaluatedValue); + Assert.AreEqual("c:\\x", values["2.0"].ToolsPath); + } + + [TestMethod] + public void SubToolsetPropertiesInRegistryCannotReferToOtherPropertiesInRegistry() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("MSBuildBinPath", "c:\\x$(p1)"); + rk.SetValue("p0", "$(p1)"); + rk.SetValue("p1", "v"); + + RegistryKey subToolsetKey = rk.CreateSubKey("dogfood"); + subToolsetKey.SetValue("p2", "$(p1)"); + subToolsetKey.SetValue("p3", "c:\\x$(p1)"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("", values["2.0"].Properties["p0"].EvaluatedValue); + Assert.AreEqual("v", values["2.0"].Properties["p1"].EvaluatedValue); + Assert.AreEqual("", values["2.0"].SubToolsets["dogfood"].Properties["p2"].EvaluatedValue); + Assert.AreEqual("c:\\x", values["2.0"].SubToolsets["dogfood"].Properties["p3"].EvaluatedValue); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void SubToolsetsCannotDefineMSBuildToolsPath() + { + RegistryKey rk = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + rk.SetValue("p0", "$(p1)"); + rk.SetValue("p1", "v"); + + RegistryKey subToolsetKey = rk.CreateSubKey("dogfood"); + subToolsetKey.SetValue("p2", "$(p1)"); + subToolsetKey.SetValue("MSBuildToolsPath", "c:\\x$(p1)"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // throws + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Tests that ToolsetInitialization are respected. + /// + [TestMethod] + public void ToolsetInitializationFlagsSetToConfigurationFile() + { + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "2.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + + // Verifications + Assert.AreEqual(2, values.Count); + Assert.AreEqual("4.5", defaultToolsVersion); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v2.0.x86ret", values["4.5"].ToolsPath); + Assert.AreEqual(@"D:\windows\Microsoft.NET\Framework\v4.0.x86ret", values["5.0"].ToolsPath); + } + + /// + /// Properties in the configuration file may refer to a registry location by using the syntax for example + /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and + /// "TaskLocation" is the name of the value. The name of the value and the preceding "@" may be omitted if + /// the default value is desired. + /// + [TestMethod] + public void PropertyInConfigurationFileReferencesRegistryLocation() + { + // Registry Read + RegistryKey key1 = Registry.CurrentUser.CreateSubKey(@"Software\Vendor\Tools"); + key1.SetValue("TaskLocation", @"somePathToTasks"); + key1.SetValue("TargetsLocation", @"D:\somePathToTargets"); + key1.SetValue("SchemaLocation", @"Schemas"); + key1.SetValue(null, @"D:\somePathToDefault"); //this sets the default value for this key + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(@"D:\somePathToTasks", values["2.0"].ToolsPath); + Assert.AreEqual(2, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\somePathToDefault", values["2.0"].Properties["p1"].EvaluatedValue); + Assert.AreEqual(@"D:\somePathToDefault\somePathToTasks\Schemas\2.0", values["2.0"].Properties["p2"].EvaluatedValue); + + Registry.CurrentUser.DeleteSubKeyTree(@"Software\Vendor"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void ToolsPathInRegistryHasInvalidPathChars() + { + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "2.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\some\foo|bar\path\"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // should throw... + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + null, + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void SamePropertyDefinedMultipleTimesForSingleToolsVersionInConfigurationFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + null, + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void SamePropertyDifferentCaseDefinedMultipleTimesForSingleToolsVersionInConfigurationFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + null, + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void SameToolsVersionDefinedMultipleTimesInConfigurationFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + null, + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void SameToolsVersionDifferentCaseDefinedMultipleTimesInConfigurationFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + null, + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void CannotSetReservedPropertyInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + null, + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void CannotSetReservedPropertyInRegistry() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + key1.SetValue("MSBuildProjectFile", @"SomeRegistryValue"); + + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + null, + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void CannotSetReservedPropertyInRegistrySubToolset() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + RegistryKey subKey1 = key1.CreateSubKey("Foo"); + + subKey1.SetValue("MSBuildBinPath", @"D:\somepath"); + subKey1.SetValue("MSBuildProjectFile", @"SomeRegistryValue"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + null, + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Properties defined in previously processed toolset definitions should + /// not affect the evaluation of subsequent toolset definitions. + /// + [TestMethod] + public void NoInterferenceBetweenToolsetDefinitions() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + null, + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + + ToolsetDefinitionLocations.ConfigurationFile + ); + + Assert.AreEqual(2, values.Count); + + Assert.AreEqual(@"D:\20\some\folder\on\disk", values["2.0"].ToolsPath); + Assert.AreEqual(2, values["2.0"].Properties.Count); + Assert.AreEqual(@"another", values["2.0"].Properties["p1"].EvaluatedValue); + Assert.AreEqual(@"fourthValue", values["2.0"].Properties["p4"].EvaluatedValue); + + Assert.AreEqual(@"D:\35\some\folder\on\disk", values["4.0"].ToolsPath); + Assert.AreEqual(2, values["4.0"].Properties.Count); + Assert.AreEqual(@"somevalue", values["4.0"].Properties["p2"].EvaluatedValue); + Assert.AreEqual(@"propertyValue", values["4.0"].Properties["p3"].EvaluatedValue); + } + + /// + /// Properties in the configuration file may refer to a registry location by using the syntax for example + /// "$(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)", where "HKEY_LOCAL_MACHINE\Software\Vendor\Tools" is the key and + /// "TaskLocation" is the name of the value. The name of the value and the preceding "@" may be omitted if + /// the default value is desired. + /// + + [TestMethod] + public void ConfigFileInvalidRegistryExpression1() + { + // No location + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileInvalidRegistryExpression2() + { + // Bogus key expression + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileInvalidRegistryExpression3() + { + // No registry location just @ + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileInvalidRegistryExpression4() + { + // Double @ + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileInvalidRegistryExpression5() + { + // Trailing @ + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileInvalidRegistryExpression6() + { + // Leading @ + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileInvalidRegistryExpression7() + { + // Bogus hive + ConfigFileInvalidRegistryExpressionHelper(@""); + } + + [TestMethod] + public void ConfigFileStringEmptyRegistryExpression1() + { + // Regular undefined property beginning with "Registry" + ConfigFileValidRegistryExpressionHelper(@"", + String.Empty); + } + + [TestMethod] + public void ConfigFileStringEmptyRegistryExpression2() + { + // Nonexistent key + ConfigFileValidRegistryExpressionHelper(@"", + String.Empty); + } + + [TestMethod] + public void ConfigFileNonPropertyRegistryExpression1() + { + // Property not terminated with paren, does not look like property + ConfigFileValidRegistryExpressionHelper(@"", + @"$(Registry:HKEY_CURRENT_USER\Software\Vendor\Tools@TaskLocation"); + } + + [TestMethod] + public void ConfigFileNonPropertyRegistryExpression2() + { + // Missing colon, looks like regular property (but with invalid property name chars, we will return blank as a result) + ConfigFileValidRegistryExpressionHelper(@"", + String.Empty); + } + + [TestMethod] + public void ConfigFileItemExpressionsDoNotExpandInConfigurationProperties() + { + // Expect that item expressions such as '@(SomeItem)' are not evaluated in any way, e.g., they are treated literally + ConfigFileValidRegistryExpressionHelper(@"", + @"@(SomeItem)"); + } + + [TestMethod] + public void RegistryInvalidRegistryExpression1() + { + // Bogus key expression + RegistryInvalidRegistryExpressionHelper("$(Registry:__bogus__)"); + } + + [TestMethod] + public void RegistryValidRegistryExpression1() + { + // Regular undefined property beginning with "Registry" + RegistryValidRegistryExpressionHelper("$(Registry)", String.Empty); + } + + private void RegistryInvalidRegistryExpressionHelper(string propertyExpression) + { + bool caught = false; + try + { + // this should throw... + RegistryValidRegistryExpressionHelper(propertyExpression, String.Empty); + } + catch (InvalidToolsetDefinitionException ex) + { + Console.WriteLine(ex.Message); + caught = true; + } + + Assert.AreEqual(true, caught); + } + + private void RegistryValidRegistryExpressionHelper(string propertyExpression, string expectedValue) + { + // Registry Read + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "2.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", "xxxx"); + key1.SetValue("p", propertyExpression); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(expectedValue, values["2.0"].Properties["p"].EvaluatedValue); + } + + /// + /// Tests that an invalid registry property expression causes an exception (resulting in a project load error) + /// + /// + private void ConfigFileInvalidRegistryExpressionHelper(string propertyExpression) + { + bool caught = false; + try + { + // this should throw... + ConfigFileValidRegistryExpressionHelper(propertyExpression, String.Empty); + } + catch (InvalidToolsetDefinitionException ex) + { + Console.WriteLine(ex.Message); + caught = true; + } + + Assert.AreEqual(true, caught); + } + + /// + /// Tests that a specified registry property expression evaluates to specified value + /// + /// + private void ConfigFileValidRegistryExpressionHelper(string propertyExpression, string expectedValue) + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + " + propertyExpression + @" + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(expectedValue, values["2.0"].Properties["p"].EvaluatedValue); + } + + /// + /// Tests the case where application configuration file overrides a value already specified in the registry + /// + [TestMethod] + public void GetToolsetData_ConflictingPropertyValuesSameCase() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\somedifferentpath", values["2.0"].ToolsPath); + } + + /// + /// Tests the case where application configuration file overrides a value already specified in the registry, + /// where that registry value is bogus and would otherwise throw. However, since the config file also + /// contains an entry for that toolset, the registry toolset never gets read, and thus never throws. + /// + [TestMethod] + public void GetToolsetData_ConflictingPropertyValuesRegistryThrows() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"$([MSBuild]::SomeNonexistentPropertyFunction())"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\somepath", values["2.0"].ToolsPath); + } + + /// + /// Tests when properties are defined in the registry as + /// well as in the config file for the same tools version. + /// We should not merge them; we should take the config file ones only + /// + [TestMethod] + public void GetToolsetData_NoMerging() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + key1.SetValue("SomeRegistryProperty", @"SomeRegistryValue"); + + // Set the config file contents as needed + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(1, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\someotherpath", values["2.0"].ToolsPath); + Assert.AreEqual(null, values["2.0"].Properties["SomeRegistryProperty"]); // Was zapped + Assert.AreEqual(@"SomeConfigValue", values["2.0"].Properties["SomeConfigProperty"].EvaluatedValue); + } + + /// + /// The absence of the ToolsVersion attribute on the main Project element in a project file means + /// that the engine's default tools version should be used. + /// + [TestMethod] + public void ToolsVersionAttributeNotSpecifiedOnProjectElementAndDefaultVersionSpecifiedInRegistry() + { + string oldValue = Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION"); + + try + { + // In the new world of figuring out the ToolsVersion to use, we completely ignore the default + // ToolsVersion in the ProjectCollection. However, this test explicitly depends on modifying + // that, so we need to turn the new defaulting behavior off in order to verify that this still works. + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", "1"); + InternalUtilities.RefreshInternalEnvironmentValues(); + + ProjectCollection projectCollection = new ProjectCollection(); + + string msbuildOverrideTasksPath = null; + projectCollection.AddToolset(new Toolset("2.0", "20toolsPath", projectCollection, msbuildOverrideTasksPath)); + projectCollection.AddToolset(new Toolset(ObjectModelHelpers.MSBuildDefaultToolsVersion, "120toolsPath", projectCollection, msbuildOverrideTasksPath)); + + string projectPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("x.proj", @""); + + Project project = projectCollection.LoadProject(projectPath); + + string defaultExpected = "14.1"; + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + defaultExpected = ObjectModelHelpers.MSBuildDefaultToolsVersion; + } + + Assert.AreEqual(defaultExpected, project.ToolsVersion); + Assert.AreEqual(defaultExpected, projectCollection.DefaultToolsVersion); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION", oldValue); + InternalUtilities.RefreshInternalEnvironmentValues(); + } + } + + /// + /// Tests the case when no values are specified in the registry + /// + [TestMethod] + public void GetToolsetData_RegistryNotPresent() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\somedifferentpath", values["2.0"].ToolsPath); + } + + /// + /// Test the case where nothing is specified in the config file + /// Note that config file not present is same as config file + /// with no MSBuildToolsets Section + /// + [TestMethod] + public void GetToolsetData_ConfigFileNotPresent() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(0, values["2.0"].Properties.Count); + Assert.AreEqual(@"D:\somepath", values["2.0"].ToolsPath); + } + + /// + /// Tests the case where nothing is specified in registry and config file + /// + [TestMethod] + public void GetToolsetData_RegistryAndConfigNotPresent() + { + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + // Should either be the last-ditch 2.0 toolset, or if 2.0 is not installed, then the last-last-ditch of 4.0 + Assert.AreEqual(1, values.Count); + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 != null) + { + Assert.AreEqual("2.0", defaultToolsVersion); + } + else + { + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, defaultToolsVersion); + } + } + + /// + /// Tests the case when reading config file throws an exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetData_ReadConfigThrowsException() + { + // Registry Read + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + + // Set the config helper to throw exception + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@"", new ConfigurationErrorsException()); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + } + + /// + /// Tests the case where reading from registry throws exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetToolsetData_ReadRegistryOpenSubKeyThrowsException() + { + RegistryKeyWrapper mockRegistryKey = + new MockRegistryKey(testRegistryPath, MockRegistryKey.WhereToThrow.OpenSubKey); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + new ToolsetRegistryReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary(), mockRegistryKey), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + } + + #endregion + + #region "SetDefaultToolsetVersion tests" + + /// + /// Tests that the default ToolsVersion is correctly resolved when specified + /// in registry and config file + /// + [TestMethod] + public void SetDefaultToolsetVersion_SpecifiedInRegistryAndConfigFile() + { + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "2.0"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("2.0"); + key1.SetValue("MSBuildBinPath", @"D:\somepath"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("5.0", defaultToolsVersion); + } + + /// + /// Tests that the default ToolsVersion is correctly resolved when specified in registry only + /// + [TestMethod] + public void SetDefaultToolsetVersion_SpecifiedOnlyInRegistry() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "4.0"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("4.0", defaultToolsVersion); + } + + /// + /// Tests that the override task path is correctly resolved when specified in registry only + /// + [TestMethod] + public void SetOverrideTasks_SpecifiedOnlyInRegistry() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "4.0"); + _currentVersionRegistryKey.SetValue("msbuildOverrideTasksPath", "c:\\TaskOverridePath"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath2"); + key2.SetValue("MSBuildBinPath", @"D:\OtherTaskOverridePath"); + RegistryKey key3 = _toolsVersionsRegistryKey.CreateSubKey("5.0"); + key3.SetValue("MSBuildBinPath", @"D:\somepath3"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("4.0", defaultToolsVersion); + Assert.AreEqual("c:\\TaskOverridePath", values["4.0"].OverrideTasksPath); + // Assert.AreEqual("c:\\OtherTaskOverridePath", values["5.0"].OverrideTasksPath); // UNDONE: Per-toolset override paths don't work. + } + + /// + /// Tests that the override default toolsversion is correctly resolved when specified in registry only + /// + [TestMethod] + public void SetDefaultOverrideToolsVersion_SpecifiedOnlyInRegistry() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + // Set up registry with two tools versions and one property each + _currentVersionRegistryKey.SetValue("DefaultOverrideToolsVersion", "13.0"); + RegistryKey key = _toolsVersionsRegistryKey.CreateSubKey("13.0"); + key.SetValue("MSBuildBinPath", @"D:\somepath2"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("4.0"); + key2.SetValue("MSBuildBinPath", @"D:\somepath3"); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("13.0", values["4.0"].DefaultOverrideToolsVersion); + } + + /// + /// Tests that the default ToolsVersion is correctly resolved + /// when specified in config file only + /// + [TestMethod] + public void SetDefaultToolsetVersion_SpecifiedOnlyInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + + Assert.AreEqual("5.0", defaultToolsVersion); + } + + /// + /// Tests that the override tasks path ToolsVersion is correctly resolved + /// when specified in config file only. + /// Also, that MSBuildOverrideTasksPath can be overridden. + /// + [TestMethod] + public void SetOverrideTaskPath_SpecifiedOnlyInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + + Assert.AreEqual("5.0", defaultToolsVersion); + Assert.AreEqual("C:\\TaskOverride", values["4.0"].OverrideTasksPath); + // Assert.AreEqual("C:\\OtherTaskOverride", values["5.0"].OverrideTasksPath); // UNDONE: Per-toolset override paths aren't working + } + + /// + /// Tests that the override default ToolsVersion is correctly resolved + /// when specified in config file only. + /// + [TestMethod] + public void SetDefaultOverrideToolsVersion_SpecifiedOnlyInConfigFile() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + + Assert.AreEqual("5.0", defaultToolsVersion); + Assert.AreEqual("3.0", values["4.0"].DefaultOverrideToolsVersion); + } + + /// + /// Tests that the default ToolsVersion is correctly resolved when specified nowhere + /// + [TestMethod] + public void SetDefaultToolsetVersion_SpecifiedNowhere() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new PropertyDictionary(), + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + string expectedDefault = "2.0"; + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null) + { + expectedDefault = ObjectModelHelpers.MSBuildDefaultToolsVersion; + } + + Assert.AreEqual(expectedDefault, defaultToolsVersion); // built-in default + Assert.AreEqual(null, values[expectedDefault].OverrideTasksPath); + Assert.AreEqual(null, values[expectedDefault].DefaultOverrideToolsVersion); + } + + /// + /// Tests that properties are properly expanded when reading them from the config file + /// + [TestMethod] + public void PropertiesInToolsetsFromConfigFileAreExpanded() + { + // $(COMPUTERNAME) is just a convenient env var. $(NUMBER_OF_PROCESSORS) isn't defined on Longhorn + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new ProjectCollection().GlobalPropertiesCollection, + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("v1", values["4.0"].Properties["p1"].EvaluatedValue); + // Properties can refer to other properties also defined in the config file + Assert.AreEqual("__v1__", values["4.0"].Properties["p2"].EvaluatedValue); + Assert.AreEqual(Environment.MachineName, values["4.0"].Properties["p3"].EvaluatedValue); + } + + /// + /// Tests that properties in MSBuildToolsPath are properly expanded when reading them from the config file + /// + [TestMethod] + public void PropertiesInToolsetsFromConfigFileAreExpandedInToolsPath() + { + // $(COMPUTERNAME) is just a convenient env var. $(NUMBER_OF_PROCESSORS) isn't defined on Longhorn + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + + "); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + new ProjectCollection().EnvironmentProperties, + new ProjectCollection().GlobalPropertiesCollection, + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("Microsoft.NET", values["4.0"].Properties["p1"].EvaluatedValue); + Assert.AreEqual("windows", values["4.0"].Properties["p2"].EvaluatedValue); + string expectedToolsPath = @"D:\windows\Microsoft.NET\Framework\v2.0.x86ret\" + Environment.MachineName; + Assert.AreEqual(expectedToolsPath, values["4.0"].ToolsPath); + Assert.AreEqual("v3" + expectedToolsPath, values["4.0"].Properties["p3"].EvaluatedValue); + } + + /// + /// Global properties are available, but they cannot be overwritten by other toolset properties, just as they cannot + /// be overwritten by project file properties. + /// + [TestMethod] + public void GlobalPropertiesInToolsetsAreExpandedButAreNotOverwritten() + { + ToolsetConfigurationReaderTestHelper.WriteConfigFile(@" + + +
+ + + + + + + + + + "); + + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties["gp1"] = "gv1"; + ProjectCollection e = new ProjectCollection(globalProperties, null, ToolsetDefinitionLocations.None); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = ToolsetReader.ReadAllToolsets + ( + values, + GetStandardRegistryReader(), + GetStandardConfigurationReader(), + e.EnvironmentProperties, + e.GlobalPropertiesCollection, + ToolsetDefinitionLocations.ConfigurationFile | ToolsetDefinitionLocations.Registry + ); + + Assert.AreEqual("gv1", values["4.0"].Properties["p1"].EvaluatedValue); + Assert.AreEqual("gv1", values["4.0"].Properties["p2"].EvaluatedValue); + } + + #endregion + + private ToolsetRegistryReader GetStandardRegistryReader() + { + return new ToolsetRegistryReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary(), new MockRegistryKey(testRegistryPath)); + } + + private ToolsetConfigurationReader GetStandardConfigurationReader() + { + return new ToolsetConfigurationReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary(), new ReadApplicationConfiguration( + ToolsetConfigurationReaderTestHelper.ReadApplicationConfigurationTest)); + } + } + + internal class MockRegistryKey : RegistryKeyWrapper + { + public enum WhereToThrow + { + None, + Name, + GetValue, + GetValueNames, + GetSubKeyNames, + OpenSubKey + } + + private WhereToThrow _whereToThrow = WhereToThrow.None; + private string _subKeyThatDoesNotExist = null; + + /// + /// Construct the mock key with a specified key + /// + /// + protected MockRegistryKey(RegistryKey wrappedKey, RegistryKey registryHive) + : base(wrappedKey, registryHive) + { } + + /// + /// Construct the mock key with a wrapper + /// + /// + public MockRegistryKey(string path) + : base(path, Registry.CurrentUser) + { } + + /// + /// Construct the mock key with a wrapper and a designated method + /// to throw from + /// + /// + /// + public MockRegistryKey(string path, WhereToThrow whereToThrow) + : base(path, Registry.CurrentUser) + { + _whereToThrow = whereToThrow; + } + + /// + /// Construct the mock key with a wrapper and a designated subkey + /// to refuse to open + /// + /// + /// + public MockRegistryKey(string path, string subKeyThatDoesNotExist) + : base(path, Registry.CurrentUser) + { + _subKeyThatDoesNotExist = subKeyThatDoesNotExist; + } + + /// + /// Name of the registry key + /// + public override string Name + { + get + { + if (_whereToThrow == WhereToThrow.Name) + { + throw new RegistryException("registryException", "registry"); + } + return base.Name; + } + } + + /// + /// Gets the value with name "name" stored under this registry key + /// + public override object GetValue(string name) + { + if (_whereToThrow == WhereToThrow.GetValue) + { + throw new RegistryException("registryException", "registry"); + } + return base.GetValue(name); + } + + /// + /// Gets the names of all values underneath this registry key + /// + public override string[] GetValueNames() + { + if (_whereToThrow == WhereToThrow.GetValueNames) + { + throw new RegistryException("registryException", "registry"); + } + return base.GetValueNames(); + } + + /// + /// Gets the names of all sub keys immediately below this registry key + /// + /// + public override string[] GetSubKeyNames() + { + if (_whereToThrow == WhereToThrow.GetSubKeyNames) + { + throw new RegistryException("registryException", "registry"); + } + return base.GetSubKeyNames(); + } + + /// + /// Returns the sub key with name "name" as a read only key + /// + public override RegistryKeyWrapper OpenSubKey(string name) + { + if (_whereToThrow == WhereToThrow.OpenSubKey) + { + throw new RegistryException("registryException", "registry"); + } + + if (_subKeyThatDoesNotExist == name) + { + // Return wrapper around null key + return new MockRegistryKey((RegistryKey)null, Registry.LocalMachine); + } + + return base.OpenSubKey(name); + } + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/Definition/ToolsetRegistryReader_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetRegistryReader_Tests.cs new file mode 100644 index 00000000000..2eb81fa6264 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/ToolsetRegistryReader_Tests.cs @@ -0,0 +1,597 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Configuration; +using Microsoft.Win32; +using Microsoft.Build.Evaluation; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.Definition +{ + /// + /// Unit test for ToolsetRegistryReader class + /// + [TestClass] + public class ToolsetRegistryReader_Tests + { + // The registry key that is passed as the baseKey parameter to the ToolsetRegistryReader class + private RegistryKey _testRegistryKey = null; + // Subkey "3.5" + private RegistryKey _currentVersionRegistryKey = null; + // Subkey "ToolsVersions" + private RegistryKey _toolsVersionsRegistryKey = null; + + // Path to the registry key under HKCU + // Note that this is a test registry key created solely for unit testing. + private const string testRegistryPath = @"msbuildUnitTests"; + + /// + /// Store the value of the "VisualStudioVersion" environment variable here so that + /// we can unset it for the duration of the test. + /// + private string _oldVisualStudioVersion; + + /// + /// Reset the testRegistryKey + /// + [TestInitialize] + public void Setup() + { + DeleteTestRegistryKey(); + _testRegistryKey = Registry.CurrentUser.CreateSubKey(testRegistryPath); + _currentVersionRegistryKey = Registry.CurrentUser.CreateSubKey(testRegistryPath + "\\" + Constants.AssemblyVersion); + _toolsVersionsRegistryKey = Registry.CurrentUser.CreateSubKey(testRegistryPath + "\\ToolsVersions"); + + _oldVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + } + + [TestCleanup] + public void TearDown() + { + DeleteTestRegistryKey(); + + Environment.SetEnvironmentVariable("VisualStudioVersion", _oldVisualStudioVersion); + } + + /// + /// Callback for toolset collection + /// + public void ToolsetAdded(Toolset toolset) + { + // Do nothing + } + + /// + /// Helper class to delete the testRegistryKey tree. + /// + private void DeleteTestRegistryKey() + { + if (Registry.CurrentUser.OpenSubKey(testRegistryPath) != null) + { + Registry.CurrentUser.DeleteSubKeyTree(testRegistryPath); + } + } + + /// + /// If the base key has been deleted, then we just don't get any information (no exception) + /// + [TestMethod] + public void ReadRegistry_DeletedKey() + { + DeleteTestRegistryKey(); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + Assert.AreEqual(0, values.Count); + } + + /// + /// Tests the tools version 4.0 is written to the the registry at install time + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void DefaultValuesInRegistryCreatedBySetup() + { + ToolsetReader reader = new ToolsetRegistryReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary()); //we don't use the test registry key because we want to verify the install + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + // Check the values in the data + Assert.IsTrue(values.ContainsKey("4.0"), "Tools version 4.0 should be defined by default"); + Assert.IsTrue(values.ContainsKey(ObjectModelHelpers.MSBuildDefaultToolsVersion), String.Format("Tools version {0} should be defined by default", ObjectModelHelpers.MSBuildDefaultToolsVersion)); + Assert.AreEqual("2.0", defaultToolsVersion, "Default tools version should be 2.0"); + } + + /// + /// Tests we handle no default toolset specified in the registry + /// + [TestMethod] + public void DefaultValueInRegistryDoesNotExist() + { + ToolsetReader reader = new ToolsetRegistryReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary(), new MockRegistryKey(testRegistryPath, "3.5" /* fail to find subkey 3.5 */)); + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Should not throw + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultToolsVersion); + } + + /// + /// The base key exists but contains no subkey or values: this is okay + /// + [TestMethod] + public void ReadRegistry_NoSubkeyNoValues() + { + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(0, values.Count); + Assert.AreEqual(null, defaultToolsVersion); + } + + /// + /// Here we validate that MSBuild does not fail when there are unrecognized values underneath + /// the ToolsVersion key. + /// + [TestMethod] + public void ReadRegistry_NoSubkeysOnlyValues() + { + _toolsVersionsRegistryKey.SetValue("Name1", "Value1"); + _toolsVersionsRegistryKey.SetValue("Name2", "Value2"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(0, values.Count); + Assert.AreEqual(null, defaultToolsVersion); + } + + /// + /// Basekey has only 1 subkey + /// + [TestMethod] + public void ReadRegistry_OnlyOneSubkey() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultToolsVersion); + Assert.AreEqual(1, values.Count); + Assert.AreEqual(0, values["tv1"].Properties.Count); + Assert.IsTrue(0 == String.Compare("c:\\xxx", values["tv1"].ToolsPath, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Basic case + /// + [TestMethod] + public void ReadRegistry_Basic() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("tv2"); + key2.SetValue("name2", "value2"); + key2.SetValue("msbuildtoolspath", "c:\\yyy"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(2, values.Count); + Assert.AreEqual(1, values["tv1"].Properties.Count); + Assert.IsTrue(0 == String.Compare("c:\\xxx", values["tv1"].ToolsPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(0 == String.Compare("value1", values["tv1"].Properties["name1"].EvaluatedValue, StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual(1, values["tv2"].Properties.Count); + Assert.IsTrue(0 == String.Compare("c:\\yyy", values["tv2"].ToolsPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(0 == String.Compare("value2", values["tv2"].Properties["name2"].EvaluatedValue, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// baseKey contains some non-String data + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void ReadRegistry_NonStringData() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("tv2"); + key2.SetValue("msbuildtoolspath", "c:\\xxx"); + key2.SetValue("name2", new String[] { "value2a", "value2b" }, RegistryValueKind.MultiString); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Registry has the following structure + /// [HKCU]\basekey\ + /// Key1 + /// SubKey1 + /// Key2 + /// SubKey2 + /// SubKey3 + /// + [TestMethod] + public void ReadRegistry_HasSubToolsets() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + RegistryKey subKey1 = key1.CreateSubKey("SubKey1"); + subKey1.SetValue("name1a", "value1a"); + subKey1.SetValue("name2a", "value2a"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("tv2"); + key2.SetValue("msbuildtoolspath", "c:\\yyy"); + key2.SetValue("name2", "value2"); + RegistryKey subKey2 = key2.CreateSubKey("SubKey2"); + subKey2.SetValue("name3a", "value3a"); + subKey2.SetValue("name2a", "value2a"); + RegistryKey subKey3 = key2.CreateSubKey("SubKey3"); + subKey3.SetValue("name4a", "value4a"); + subKey3.SetValue("name5a", "value5a"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(2, values.Count); + Assert.AreEqual(1, values["tv1"].Properties.Count); + Assert.AreEqual("c:\\xxx", values["tv1"].ToolsPath); + Assert.AreEqual("value1", values["tv1"].Properties["name1"].EvaluatedValue); + Assert.AreEqual(1, values["tv1"].SubToolsets.Count); + Assert.AreEqual(2, values["tv1"].SubToolsets["SubKey1"].Properties.Count); + Assert.AreEqual("value1a", values["tv1"].SubToolsets["SubKey1"].Properties["name1a"].EvaluatedValue); + Assert.AreEqual("value2a", values["tv1"].SubToolsets["SubKey1"].Properties["name2a"].EvaluatedValue); + + Assert.AreEqual(1, values["tv2"].Properties.Count); + Assert.AreEqual("c:\\yyy", values["tv2"].ToolsPath); + Assert.AreEqual("value2", values["tv2"].Properties["name2"].EvaluatedValue); + Assert.AreEqual(2, values["tv2"].SubToolsets.Count); + Assert.AreEqual(2, values["tv2"].SubToolsets["SubKey2"].Properties.Count); + Assert.AreEqual("value3a", values["tv2"].SubToolsets["SubKey2"].Properties["name3a"].EvaluatedValue); + Assert.AreEqual("value2a", values["tv2"].SubToolsets["SubKey2"].Properties["name2a"].EvaluatedValue); + Assert.AreEqual(2, values["tv2"].SubToolsets["SubKey3"].Properties.Count); + Assert.AreEqual("value4a", values["tv2"].SubToolsets["SubKey3"].Properties["name4a"].EvaluatedValue); + Assert.AreEqual("value5a", values["tv2"].SubToolsets["SubKey3"].Properties["name5a"].EvaluatedValue); + } + + /// + /// Registry has the following structure + /// [HKCU]\basekey\ + /// Key1 + /// SubKey1 + /// SubSubKey1 + /// + [TestMethod] + public void ReadRegistry_IgnoreSubToolsetSubKeys() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + RegistryKey subKey1 = key1.CreateSubKey("SubKey1"); + subKey1.SetValue("name1a", "value1a"); + subKey1.SetValue("name2a", "value2a"); + RegistryKey subSubKey1 = subKey1.CreateSubKey("SubSubKey1"); + subSubKey1.SetValue("name2b", "value2b"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(1, values["tv1"].Properties.Count); + Assert.AreEqual("c:\\xxx", values["tv1"].ToolsPath); + Assert.AreEqual("value1", values["tv1"].Properties["name1"].EvaluatedValue); + Assert.AreEqual(1, values["tv1"].SubToolsets.Count); + Assert.AreEqual(2, values["tv1"].SubToolsets["SubKey1"].Properties.Count); + Assert.AreEqual("value1a", values["tv1"].SubToolsets["SubKey1"].Properties["name1a"].EvaluatedValue); + Assert.AreEqual("value2a", values["tv1"].SubToolsets["SubKey1"].Properties["name2a"].EvaluatedValue); + } + + /// + /// Verifies that if a value is defined in both the base toolset and the + /// selected subtoolset, the subtoolset value overrides -- even if that + /// value is empty. + /// + [TestMethod] + public void ReadRegistry_SubToolsetOverridesBaseToolsetEntries() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + key1.SetValue("name2", "value2"); + RegistryKey subKey1 = key1.CreateSubKey("Foo"); + subKey1.SetValue("name1", "value1a"); + subKey1.SetValue("name2", ""); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(2, values["tv1"].Properties.Count); + Assert.AreEqual("c:\\xxx", values["tv1"].ToolsPath); + Assert.AreEqual("value1", values["tv1"].Properties["name1"].EvaluatedValue); + Assert.AreEqual("value2", values["tv1"].Properties["name2"].EvaluatedValue); + Assert.AreEqual(1, values["tv1"].SubToolsets.Count); + Assert.AreEqual(2, values["tv1"].SubToolsets["Foo"].Properties.Count); + Assert.AreEqual("value1a", values["tv1"].SubToolsets["Foo"].Properties["name1"].EvaluatedValue); + Assert.AreEqual("", values["tv1"].SubToolsets["Foo"].Properties["name2"].EvaluatedValue); + + // Check when requesting the final evaluated value of the property in the context of its sub-toolset + // that the sub-toolset overrides + Assert.AreEqual("value1a", values["tv1"].GetProperty("name1", "Foo").EvaluatedValue); + Assert.AreEqual("", values["tv1"].GetProperty("name2", "Foo").EvaluatedValue); + } + + /// + /// Verifies that if a value is defined in both the base toolset and the + /// selected subtoolset, the subtoolset value overrides -- even if that + /// value is empty. + /// + [TestMethod] + public void ReadRegistry_UnselectedSubToolsetIsIgnored() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + key1.SetValue("name2", "value2"); + RegistryKey subKey1 = key1.CreateSubKey("Foo"); + subKey1.SetValue("name1", "value1a"); + subKey1.SetValue("name2", ""); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(1, values.Count); + Assert.AreEqual(2, values["tv1"].Properties.Count); + Assert.AreEqual("c:\\xxx", values["tv1"].ToolsPath); + Assert.AreEqual("value1", values["tv1"].Properties["name1"].EvaluatedValue); + Assert.AreEqual("value2", values["tv1"].Properties["name2"].EvaluatedValue); + } + + /// + /// Regular case of getting default tools version + /// + [TestMethod] + public void GetDefaultToolsVersionFromRegistry_Basic() + { + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", "tv1"); + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); // Need matching tools version + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual("tv1", defaultToolsVersion); + } + + /// + /// Default value is not set + /// + [TestMethod] + public void GetDefaultToolsVersionFromRegistry_DefaultValueNotSet() + { + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultToolsVersion); + } + + /// + /// "DefaultToolsVersion" has non-String data + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetDefaultToolsVersionFromRegistry_NonStringData() + { + _currentVersionRegistryKey.SetValue("DefaultToolsVersion", new String[] { "2.0.xxxx.a", "2.0.xxxx.b" }, RegistryValueKind.MultiString); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + private ToolsetRegistryReader GetStandardRegistryReader() + { + return new ToolsetRegistryReader(new ProjectCollection().EnvironmentProperties, new PropertyDictionary(), new MockRegistryKey(testRegistryPath)); + } + + /// + /// Regular case of getting overridetaskspath + /// + [TestMethod] + public void GetOverrideTasksPathFromRegistry_Basic() + { + _currentVersionRegistryKey.SetValue("MsBuildOverrideTasksPath", "c:\\Foo"); + + ToolsetReader reader = GetStandardRegistryReader(); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual("c:\\Foo", msbuildOverrideTasksPath); + } + + /// + /// OverrideTasksPath is not set + /// + [TestMethod] + public void GetOverrideTasksPathFromRegistry_ValueNotSet() + { + ToolsetReader reader = GetStandardRegistryReader(); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, msbuildOverrideTasksPath); + } + + /// + /// "OverrideTasksPath" has non-String data + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetOverrideTasksPathFromRegistry_NonStringData() + { + _currentVersionRegistryKey.SetValue("MsBuildOverrideTasksPath", new String[] { "2938304894", "3948394.2.3.3.3" }, RegistryValueKind.MultiString); + + ToolsetReader reader = GetStandardRegistryReader(); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + /// + /// Regular case of getting the default override toolsversion + /// + [TestMethod] + public void GetDefaultOverrideToolsVersionFromRegistry_Basic() + { + _currentVersionRegistryKey.SetValue("DefaultOverrideToolsVersion", "15.0"); + + ToolsetReader reader = GetStandardRegistryReader(); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual("15.0", defaultOverrideToolsVersion); + } + + /// + /// DefaultOverrideToolsVersion is not set + /// + [TestMethod] + public void GetDefaultOverrideToolsVersionFromRegistry_ValueNotSet() + { + ToolsetReader reader = GetStandardRegistryReader(); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.AreEqual(null, defaultOverrideToolsVersion); + } + + /// + /// "DefaultOverrideToolsVersion" has non-String data + /// + [TestMethod] + [ExpectedException(typeof(InvalidToolsetDefinitionException))] + public void GetDefaultOverrideToolsVersionFromRegistry_NonStringData() + { + _currentVersionRegistryKey.SetValue("DefaultOverrideToolsVersion", new String[] { "2938304894", "3948394.2.3.3.3" }, RegistryValueKind.MultiString); + + ToolsetReader reader = GetStandardRegistryReader(); + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + } + + [TestMethod] + public void ReadToolsets_NoBinPathOrToolsPath() + { + RegistryKey key1 = _toolsVersionsRegistryKey.CreateSubKey("tv1"); + key1.SetValue("msbuildtoolspath", "c:\\xxx"); + key1.SetValue("name1", "value1"); + RegistryKey key2 = _toolsVersionsRegistryKey.CreateSubKey("tv2"); + key2.SetValue("name2", "value2"); + RegistryKey key3 = _toolsVersionsRegistryKey.CreateSubKey("tv3"); + key3.SetValue("msbuildtoolspath", "c:\\zzz"); + key3.SetValue("name3", "value3"); + + ToolsetReader reader = GetStandardRegistryReader(); + string msbuildOverrideTasksPath = null; + string defaultOverrideToolsVersion = null; + + Dictionary values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + //should not throw + string defaultToolsVersion = reader.ReadToolsets(values, new PropertyDictionary(), new PropertyDictionary(), false, out msbuildOverrideTasksPath, out defaultOverrideToolsVersion); + + Assert.IsTrue(values.ContainsKey("tv1")); + + //should not contain the second toolset because it does not define a tools/bin path + Assert.IsFalse(values.ContainsKey("tv2")); + + Assert.IsTrue(values.ContainsKey("tv3")); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Definition/Toolset_Tests.cs b/src/XMakeBuildEngine/UnitTests/Definition/Toolset_Tests.cs new file mode 100644 index 00000000000..d90f095e632 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Definition/Toolset_Tests.cs @@ -0,0 +1,520 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Xml; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Internal; +using Microsoft.Build.UnitTests.BackEnd; +using Microsoft.Build.Unittest; + +namespace Microsoft.Build.UnitTests.Definition +{ + [TestClass] + public class Toolset_Tests + { + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ToolsetCtorErrors1() + { + Toolset t = new Toolset(null, "x", new ProjectCollection(), null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ToolsetCtorErrors2() + { + Toolset t = new Toolset("x", null, new ProjectCollection(), null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ToolsetCtorErrors3() + { + Toolset t = new Toolset(String.Empty, "x", new ProjectCollection(), null); + } + + [TestMethod] + public void Regress27993_TrailingSlashTrimmedFromMSBuildToolsPath() + { + Toolset t; + + t = new Toolset("x", "C:", new ProjectCollection(), null); + Assert.AreEqual(@"C:", t.ToolsPath); + t = new Toolset("x", @"C:\", new ProjectCollection(), null); + Assert.AreEqual(@"C:\", t.ToolsPath); + t = new Toolset("x", @"C:\\", new ProjectCollection(), null); + Assert.AreEqual(@"C:\", t.ToolsPath); + + t = new Toolset("x", @"C:\foo", new ProjectCollection(), null); + Assert.AreEqual(@"C:\foo", t.ToolsPath); + t = new Toolset("x", @"C:\foo\", new ProjectCollection(), null); + Assert.AreEqual(@"C:\foo", t.ToolsPath); + t = new Toolset("x", @"C:\foo\\", new ProjectCollection(), null); + Assert.AreEqual(@"C:\foo\", t.ToolsPath); // trim at most one slash + + t = new Toolset("x", @"\\foo\share", new ProjectCollection(), null); + Assert.AreEqual(@"\\foo\share", t.ToolsPath); + t = new Toolset("x", @"\\foo\share\", new ProjectCollection(), null); + Assert.AreEqual(@"\\foo\share", t.ToolsPath); + t = new Toolset("x", @"\\foo\share\\", new ProjectCollection(), null); + Assert.AreEqual(@"\\foo\share\", t.ToolsPath); // trim at most one slash + } + + [TestMethod] + public void ValidateToolsetTranslation() + { + PropertyDictionary buildProperties = new PropertyDictionary(); + buildProperties.Set(ProjectPropertyInstance.Create("a", "a1")); + + PropertyDictionary environmentProperties = new PropertyDictionary(); + environmentProperties.Set(ProjectPropertyInstance.Create("b", "b1")); + + PropertyDictionary globalProperties = new PropertyDictionary(); + globalProperties.Set(ProjectPropertyInstance.Create("c", "c1")); + + PropertyDictionary subToolsetProperties = new PropertyDictionary(); + subToolsetProperties.Set(ProjectPropertyInstance.Create("d", "d1")); + + Dictionary subToolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + subToolsets.Add("dogfood", new SubToolset("dogfood", subToolsetProperties)); + + Toolset t = new Toolset("4.0", "c:\\bar", buildProperties, environmentProperties, globalProperties, subToolsets, "c:\\foo", "4.0"); + + ((INodePacketTranslatable)t).Translate(TranslationHelpers.GetWriteTranslator()); + Toolset t2 = Toolset.FactoryForDeserialization(TranslationHelpers.GetReadTranslator()); + + Assert.AreEqual(t.ToolsVersion, t2.ToolsVersion); + Assert.AreEqual(t.ToolsPath, t2.ToolsPath); + Assert.AreEqual(t.OverrideTasksPath, t2.OverrideTasksPath); + Assert.AreEqual(t.Properties.Count, t2.Properties.Count); + + foreach (string key in t.Properties.Keys) + { + Assert.AreEqual(t.Properties[key].Name, t2.Properties[key].Name); + Assert.AreEqual(t.Properties[key].EvaluatedValue, t2.Properties[key].EvaluatedValue); + } + + Assert.AreEqual(t.SubToolsets.Count, t2.SubToolsets.Count); + + foreach (string key in t.SubToolsets.Keys) + { + SubToolset subToolset1 = t.SubToolsets[key]; + SubToolset subToolset2 = null; + + if (t2.SubToolsets.TryGetValue(key, out subToolset2)) + { + Assert.AreEqual(subToolset1.SubToolsetVersion, subToolset2.SubToolsetVersion); + Assert.AreEqual(subToolset1.Properties.Count, subToolset2.Properties.Count); + + foreach (string subToolsetPropertyKey in subToolset1.Properties.Keys) + { + Assert.AreEqual(subToolset1.Properties[subToolsetPropertyKey].Name, subToolset2.Properties[subToolsetPropertyKey].Name); + Assert.AreEqual(subToolset1.Properties[subToolsetPropertyKey].EvaluatedValue, subToolset2.Properties[subToolsetPropertyKey].EvaluatedValue); + } + } + else + { + Assert.Fail("Sub-toolset {0} was lost in translation.", key); + } + } + + Assert.AreEqual(t.DefaultOverrideToolsVersion, t2.DefaultOverrideToolsVersion); + } + + [TestMethod] + public void TestDefaultSubToolset() + { + Toolset t = GetFakeToolset(null /* no global properties */); + + // The highest one numerically -- in this case, v13. + Assert.AreEqual("v13.0", t.DefaultSubToolsetVersion); + } + + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TestDefaultSubToolsetFor40() + { + Toolset t = ProjectCollection.GlobalProjectCollection.GetToolset("4.0"); + + if (Toolset.Dev10IsInstalled) + { + // If Dev10 is installed, the default sub-toolset = no sub-toolset + Assert.AreEqual(Constants.Dev10SubToolsetValue, t.DefaultSubToolsetVersion); + } + else + { + // Otherwise, it's the highest one numerically. Since by definition if Dev10 isn't + // installed and subtoolsets exists we must be at least Dev11, it should be "11.0" + Assert.AreEqual("11.0", t.DefaultSubToolsetVersion); + } + } + + [TestMethod] + public void TestDefaultWhenNoSubToolset() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + ProjectCollection projectCollection = new ProjectCollection(); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + if (Toolset.Dev10IsInstalled) + { + Assert.AreEqual(Constants.Dev10SubToolsetValue, t.DefaultSubToolsetVersion); + } + else + { + Assert.IsNull(t.DefaultSubToolsetVersion); + } + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGenerateSubToolsetVersionWhenNoSubToolset() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + ProjectCollection projectCollection = new ProjectCollection(); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + string subToolsetVersion = t.GenerateSubToolsetVersion(); + + if (Toolset.Dev10IsInstalled) + { + Assert.AreEqual(Constants.Dev10SubToolsetValue, subToolsetVersion); + } + else + { + Assert.IsNull(subToolsetVersion); + } + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestNoSubToolset_GlobalPropertyOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "99.0"); + + ProjectCollection projectCollection = new ProjectCollection(globalProperties); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + Assert.AreEqual("99.0", t.GenerateSubToolsetVersion()); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestNoSubToolset_EnvironmentOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "foo"); + + ProjectCollection projectCollection = new ProjectCollection(); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + Assert.AreEqual("foo", t.GenerateSubToolsetVersion()); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestNoSubToolset_ExplicitlyPassedGlobalPropertyOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + ProjectCollection projectCollection = new ProjectCollection(); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "v14.0"); + + Assert.AreEqual("v14.0", t.GenerateSubToolsetVersion(globalProperties, 0)); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestNoSubToolset_ExplicitlyPassedGlobalPropertyWins() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "foo"); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "v13.0"); + + ProjectCollection projectCollection = new ProjectCollection(globalProperties); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + IDictionary explicitGlobalProperties = new Dictionary(); + explicitGlobalProperties.Add("VisualStudioVersion", "baz"); + + Assert.AreEqual("baz", t.GenerateSubToolsetVersion(explicitGlobalProperties, 0)); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGenerateSubToolsetVersion_GlobalPropertyOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", ObjectModelHelpers.CurrentVisualStudioVersion); + + Toolset t = GetFakeToolset(globalProperties); + + Assert.AreEqual(ObjectModelHelpers.CurrentVisualStudioVersion, t.GenerateSubToolsetVersion()); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGenerateSubToolsetVersion_EnvironmentOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "FakeSubToolset"); + + Toolset t = GetFakeToolset(null); + + Assert.AreEqual("FakeSubToolset", t.GenerateSubToolsetVersion()); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGenerateSubToolsetVersion_ExplicitlyPassedGlobalPropertyOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + Toolset t = GetFakeToolset(null); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "v13.0"); + + Assert.AreEqual("v13.0", t.GenerateSubToolsetVersion(globalProperties, 0)); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGenerateSubToolsetVersion_SolutionVersionOverrides() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + Toolset t = GetFakeToolset(null); + + // VisualStudioVersion = SolutionVersion - 1 + Assert.AreEqual("12.0", t.GenerateSubToolsetVersion(null, 13)); + Assert.AreEqual("v13.0", t.GenerateSubToolsetVersion(null, 14)); + + // however, if there is no matching solution version, we just fall back to the + // default sub-toolset. + Assert.AreEqual(t.DefaultSubToolsetVersion, t.GenerateSubToolsetVersion(null, 55)); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGenerateSubToolsetVersion_ExplicitlyPassedGlobalPropertyWins() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", ObjectModelHelpers.CurrentVisualStudioVersion); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "v13.0"); + + ProjectCollection projectCollection = new ProjectCollection(globalProperties); + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset t = new Toolset("Fake", parentToolset.ToolsPath, null, projectCollection, null, parentToolset.OverrideTasksPath); + + IDictionary explicitGlobalProperties = new Dictionary(); + explicitGlobalProperties.Add("VisualStudioVersion", "FakeSubToolset"); + + Assert.AreEqual("FakeSubToolset", t.GenerateSubToolsetVersion(explicitGlobalProperties, 0)); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + [TestMethod] + public void TestGetPropertyFromSubToolset() + { + Toolset t = GetFakeToolset(null); + + Assert.AreEqual("a1", t.GetProperty("a", "v11.0").EvaluatedValue); // property in base toolset + Assert.AreEqual("c2", t.GetProperty("c", "v11.0").EvaluatedValue); // property in sub-toolset + Assert.AreEqual("b2", t.GetProperty("b", "v11.0").EvaluatedValue); // property in sub-toolset that overrides base toolset + Assert.IsNull(t.GetProperty("d", "v11.0")); // property in a different sub-toolset + } + + /// + /// Creates a standard ProjectCollection and adds a fake toolset with the following contents to it: + /// + /// ToolsVersion = Fake + /// Base Properties: + /// a = a1 + /// b = b1 + /// + /// SubToolset "12.0": + /// d = d4 + /// e = e5 + /// + /// SubToolset "v11.0": + /// b = b2 + /// c = c2 + /// + /// SubToolset "FakeSubToolset": + /// a = a3 + /// c = c3 + /// + /// SubToolset "v13.0": + /// f = f6 + /// g = g7 + /// + private Toolset GetFakeToolset(IDictionary globalPropertiesForProjectCollection) + { + ProjectCollection projectCollection = new ProjectCollection(globalPropertiesForProjectCollection); + + IDictionary properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + properties.Add("a", "a1"); + properties.Add("b", "b1"); + + Dictionary subToolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // SubToolset 12.0 properties + PropertyDictionary subToolset12Properties = new PropertyDictionary(); + subToolset12Properties.Set(ProjectPropertyInstance.Create("d", "d4")); + subToolset12Properties.Set(ProjectPropertyInstance.Create("e", "e5")); + + // SubToolset v11.0 properties + PropertyDictionary subToolset11Properties = new PropertyDictionary(); + subToolset11Properties.Set(ProjectPropertyInstance.Create("b", "b2")); + subToolset11Properties.Set(ProjectPropertyInstance.Create("c", "c2")); + + // FakeSubToolset properties + PropertyDictionary fakeSubToolsetProperties = new PropertyDictionary(); + fakeSubToolsetProperties.Set(ProjectPropertyInstance.Create("a", "a3")); + fakeSubToolsetProperties.Set(ProjectPropertyInstance.Create("c", "c3")); + + // SubToolset v13.0 properties + PropertyDictionary subToolset13Properties = new PropertyDictionary(); + subToolset13Properties.Set(ProjectPropertyInstance.Create("f", "f6")); + subToolset13Properties.Set(ProjectPropertyInstance.Create("g", "g7")); + + subToolsets.Add("12.0", new SubToolset("12.0", subToolset12Properties)); + subToolsets.Add("v11.0", new SubToolset("v11.0", subToolset11Properties)); + subToolsets.Add("FakeSubToolset", new SubToolset("FakeSubToolset", fakeSubToolsetProperties)); + subToolsets.Add("v13.0", new SubToolset("v13.0", subToolset13Properties)); + + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset fakeToolset = new Toolset("Fake", parentToolset.ToolsPath, properties, projectCollection, subToolsets, parentToolset.OverrideTasksPath); + + projectCollection.AddToolset(fakeToolset); + + return fakeToolset; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/DualQueue_Tests.cs b/src/XMakeBuildEngine/UnitTests/DualQueue_Tests.cs new file mode 100644 index 00000000000..6269c9c730a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/DualQueue_Tests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Collections; +using System.Text; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class DualQueue_Tests + { + /// + /// Test the dual queue with multiple writers and only one reader + /// + [TestMethod] + public void TestQueueEnqueueMultipleWriterOneReader() + { + // Queue which will contain elements added using a single Enqueue per item + DualQueue stringQueue = new DualQueue(); + // Queue which will contain elements added using an EnqueueArray for a group of Items + DualQueue stringQueueTwo = new DualQueue(); + // List of strings which are supposed to be in the queue + List stringsSupposedToBeInQueue = new List(); + // List of strings which are supposed to be in the queue which uses EnQueueArray + List stringsSupposedToBeInQueueTwo = new List(); + + // Array containing our set of ManualResetEvents which is the number of threads we are going to use + ManualResetEvent[] waitHandles = new ManualResetEvent[50]; + for (int i = 0; i < waitHandles.Length; i++) + { + waitHandles[i] = new ManualResetEvent(false); + ThreadPool.QueueUserWorkItem( + delegate(object state) + { + // Create three non repeating strings to put in the the different queues + string string1 = System.Guid.NewGuid().ToString(); + string string2 = System.Guid.NewGuid().ToString(); + string string3 = System.Guid.NewGuid().ToString(); + + stringQueue.Enqueue(string1); + lock (stringsSupposedToBeInQueue) + { + stringsSupposedToBeInQueue.Add(string1); + } + + stringQueueTwo.EnqueueArray(new string[] { string2, string3 }); + lock (stringsSupposedToBeInQueueTwo) + { + stringsSupposedToBeInQueueTwo.Add(string2); + stringsSupposedToBeInQueueTwo.Add(string3); + } + + // Say we are done the thread + ((ManualResetEvent)state).Set(); + }, waitHandles[i]); + } + + // Wait for all of the threads to complete + foreach (ManualResetEvent resetEvent in waitHandles) + { + resetEvent.WaitOne(); + } + + // Pop off items from the queue and make ture that we got all of out items back out + int numberOfItemsInQueue = 0; + string result = null; + while ((result = stringQueue.Dequeue()) != null) + { + Assert.IsTrue(stringsSupposedToBeInQueue.Contains(result),string.Format("Expected {0} to be in the queue but it was not",result)); + stringsSupposedToBeInQueue.Remove(result); + numberOfItemsInQueue++; + } + Assert.IsTrue(stringsSupposedToBeInQueue.Count == 0, "Expected all strings to be removed but they were not"); + // The number of items we processed should be the same as the number of EnQueues we did + Assert.IsTrue(numberOfItemsInQueue == waitHandles.Length,"Expected the number of items in the queue to be the same as the number of Enqueues but it was not"); + + // Pop off items from the queue and make ture that we got all of out items back out + int numberOfItemsInQueueTwo = 0; + string result2 = null; + while ((result2 = stringQueueTwo.Dequeue()) != null) + { + Assert.IsTrue(stringsSupposedToBeInQueueTwo.Contains(result2), string.Format("Expected {0} to be in the queue number 2 but it was not", result2)); + stringsSupposedToBeInQueueTwo.Remove(result2); + numberOfItemsInQueueTwo++; + } + Assert.IsTrue(stringsSupposedToBeInQueueTwo.Count == 0, "Expected all strings to be removed in queue 2 but they were not"); + // The number of items we processed should be the same as the number of EnQueues we did + Assert.IsTrue(numberOfItemsInQueueTwo == waitHandles.Length*2, "Expected the number of items in the queue 2 to be the same as the number of Enqueues but it was not"); + + // Clear the queue + stringQueue.Clear(); + Assert.IsTrue(stringQueue.Count == 0, "The count should be zero after clearing the queue"); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/EscapingInProjects_Tests.cs b/src/XMakeBuildEngine/UnitTests/EscapingInProjects_Tests.cs new file mode 100644 index 00000000000..4a50cef6361 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/EscapingInProjects_Tests.cs @@ -0,0 +1,1876 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Reflection; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.UnitTests; + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using FileUtilities = Microsoft.Build.Shared.FileUtilities; +using EscapingUtilities = Microsoft.Build.Shared.EscapingUtilities; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ResourceUtilities = Microsoft.Build.Shared.ResourceUtilities; + +namespace Microsoft.Build.UnitTests.EscapingInProjects_Tests +{ + /// + /// Test task that just logs the parameters it receives. + /// + public class MyTestTask : Task + { + private ITaskItem _taskItemParam; + public ITaskItem TaskItemParam + { + get + { + return _taskItemParam; + } + + set + { + _taskItemParam = value; + } + } + + override public bool Execute() + { + if (TaskItemParam != null) + { + Log.LogMessageFromText("Received TaskItemParam: " + TaskItemParam.ItemSpec, MessageImportance.High); + } + + return true; + } + } + + [TestClass] + public class SimpleScenarios + { + /// + /// Since we create a project with the same name in many of these tests, and two projects with + /// the same name cannot be loaded in a ProjectCollection at the same time, we should unload the + /// GlobalProjectCollection (into which all of these projects are placed by default) after each test. + /// + [TestCleanup] + public void UnloadGlobalProjectCollection() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Make sure I can define a property with escaped characters and pass it into + /// a string parameter of a task, in this case the Message task. + /// + [TestMethod] + public void SemicolonInPropertyPassedIntoStringParam() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + abc %3b def %3b ghi + + + + + + "); + + logger.AssertLogContains("Property value is 'abc ; def ; ghi'"); + } + + /// + /// Make sure I can define a property with escaped characters and pass it into + /// a string parameter of a task, in this case the Message task. + /// + [TestMethod] + public void SemicolonInPropertyPassedIntoStringParam_UsingTaskHost() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + abc %3b def %3b ghi + + + + + + "); + + logger.AssertLogContains("Property value is 'abc ; def ; ghi'"); + } + + /// + /// Make sure I can define a property with escaped characters and pass it into + /// an ITaskItem[] task parameter. + /// + [TestMethod] + public void SemicolonInPropertyPassedIntoITaskItemParam() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(String.Format(@" + + + + + + + abc %3b def %3b ghi + + + + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + logger.AssertLogContains("Received TaskItemParam: 123 abc ; def ; ghi 789"); + } + + /// + /// Make sure I can define a property with escaped characters and pass it into + /// an ITaskItem[] task parameter. + /// + [TestMethod] + public void SemicolonInPropertyPassedIntoITaskItemParam_UsingTaskHost() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(String.Format(@" + + + + + + + abc %3b def %3b ghi + + + + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + logger.AssertLogContains("Received TaskItemParam: 123 abc ; def ; ghi 789"); + } + + /// + /// If I try to add a new item to a project, and my new item's Include has an unescaped semicolon + /// in it, then we shouldn't try to match it up against any existing wildcards. This is a really + /// bizarre scenario ... the caller probably meant to escape the semicolon. + /// + [TestMethod] + public void AddNewItemWithSemicolon() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + + + + + "; + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + IEnumerable newItems = project.AddItem("MyWildCard", "foo;bar.weirdo"); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + } + + /// + /// If I try to add a new item to a project, and my new item's Include has a property that + /// contains an unescaped semicolon in it, then we shouldn't try to match it up against any existing + /// wildcards. + /// + [TestMethod] + public void AddNewItemWithPropertyContainingSemicolon() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + foo;bar + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + foo;bar + + + + + + + "; + + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + IEnumerable newItems = project.AddItem("MyWildCard", "$(FilenameWithSemicolon).weirdo"); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + } + + /// + /// If I try to modify an item in a project, and my new item's Include has an unescaped semicolon + /// in it, then we shouldn't try to match it up against any existing wildcards. This is a really + /// bizarre scenario ... the caller probably meant to escape the semicolon. + /// + [TestMethod] + public void ModifyItemIncludeSemicolon() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + + + + + + + + "; + + try + { + // Populate the project directory with three physical files on disk -- a.weirdo, b.weirdo, c.weirdo. + EscapingInProjectsHelper.CreateThreeWeirdoFiles(); + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + + EscapingInProjectsHelper.ModifyItemOfTypeInProject(project, "MyWildcard", "b.weirdo", "foo;bar.weirdo"); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + } + finally + { + EscapingInProjectsHelper.CleanupWeirdoFiles(); + } + } + + /// + /// If I try to modify an item in a project, and my new item's Include has an escaped semicolon + /// in it, and it matches the existing wildcard, then we shouldn't need to modify the project file. + /// + [TestMethod] + public void ModifyItemIncludeEscapedSemicolon() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + + + + + + "; + + try + { + // Populate the project directory with three physical files on disk -- a.weirdo, b.weirdo, c.weirdo. + EscapingInProjectsHelper.CreateThreeWeirdoFiles(); + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + + IEnumerable newItems = EscapingInProjectsHelper.ModifyItemOfTypeInProject(project, "MyWildcard", "b.weirdo", "foo%253Bbar.weirdo"); + + Assert.AreEqual(1, newItems.Count()); + Assert.AreEqual("*.weirdo", newItems.First().UnevaluatedInclude); + Assert.AreEqual("foo%3Bbar.weirdo", newItems.First().EvaluatedInclude); + Assert.AreEqual("foo%253Bbar.weirdo", Project.GetEvaluatedItemIncludeEscaped(newItems.First())); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + } + finally + { + EscapingInProjectsHelper.CleanupWeirdoFiles(); + } + } + + /// + /// If I try to modify an item in a project, and my new item's Include has a property that + /// contains an unescaped semicolon in it, then we shouldn't try to match it up against any existing + /// wildcards. + /// + [TestMethod] + public void ModifyItemAddPropertyContainingSemicolon() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + + foo;bar + + + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + + foo;bar + + + + + + + + + + "; + + try + { + // Populate the project directory with three physical files on disk -- a.weirdo, b.weirdo, c.weirdo. + EscapingInProjectsHelper.CreateThreeWeirdoFiles(); + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + + EscapingInProjectsHelper.ModifyItemOfTypeInProject(project, "MyWildcard", "b.weirdo", "$(FilenameWithSemicolon).weirdo"); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + } + finally + { + EscapingInProjectsHelper.CleanupWeirdoFiles(); + } + } + + /// + /// Make sure that character escaping works as expected when adding a new item that matches + /// an existing wildcarded item in the project file. + /// + [TestMethod] + public void AddNewItemThatMatchesWildcard1() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + + + + "; + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + IEnumerable newItems = project.AddItem("MyWildCard", "foo%253bbar.weirdo"); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + + Assert.AreEqual(1, newItems.Count()); + Assert.AreEqual("MyWildCard", newItems.First().ItemType, "Newly added item should have correct ItemType"); + Assert.AreEqual("*.weirdo", newItems.First().UnevaluatedInclude, "Newly added item should have correct UnevaluatedInclude"); + Assert.AreEqual("foo%253bbar.weirdo", Project.GetEvaluatedItemIncludeEscaped(newItems.First()), "Newly added item should have correct EvaluatedIncludeEscaped"); + Assert.AreEqual("foo%3bbar.weirdo", newItems.First().EvaluatedInclude, "Newly added item should have correct EvaluatedInclude"); + } + + /// + /// Make sure that character escaping works as expected when adding a new item that matches + /// an existing wildcarded item in the project file. + /// + [TestMethod] + public void AddNewItemThatMatchesWildcard2() + { + // ************************************ + // BEFORE + // ************************************ + string projectOriginalContents = @" + + + + + + "; + + + // ************************************ + // AFTER + // ************************************ + string projectNewExpectedContents = @" + + + + + + "; + + Project project = ObjectModelHelpers.CreateInMemoryProject(projectOriginalContents); + IEnumerable newItems = project.AddItem("MyWildCard", "foo.AAA%253bBBB"); + + Helpers.CompareProjectXml(projectNewExpectedContents, project.Xml.RawXml); + + Assert.AreEqual(1, newItems.Count()); + Assert.AreEqual("MyWildCard", newItems.First().ItemType, "Newly added item should have correct ItemType"); + Assert.AreEqual("*.AAA%253bBBB", newItems.First().UnevaluatedInclude, "Newly added item should have correct UnevaluatedInclude"); + Assert.AreEqual("foo.AAA%253bBBB", Project.GetEvaluatedItemIncludeEscaped(newItems.First()), "Newly added item should have correct EvaluatedIncludeEscaped"); + Assert.AreEqual("foo.AAA%3bBBB", newItems.First().EvaluatedInclude, "Newly added item should have correct EvaluatedInclude"); + } + + /// + /// Make sure that all inferred task outputs (those that are determined without actually + /// executing the task) are left escaped when they become real items in the engine, and + /// they only get unescaped when fed into a subsequent task. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void InferEscapedOutputsFromTask() + { + string inputFile = null; + string outputFile = null; + + try + { + inputFile = FileUtilities.GetTemporaryFile(); + outputFile = FileUtilities.GetTemporaryFile(); + + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(String.Format(@" + + + + + + + + + + + + + + + + ", inputFile, outputFile)); + + logger.AssertLogContains("Resources = aaa%3bbbb.resx;ccc%3bddd.resx"); + } + finally + { + if (inputFile != null) File.Delete(inputFile); + if (outputFile != null) File.Delete(outputFile); + } + } + + /// + /// Do an item transform, where the transform expression contains an unescaped semicolon as well + /// as an escaped percent sign. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ItemTransformContainingSemicolon() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + + + + + + '%(FileName);%(FileName)%253b%(FileName)%(Extension)',' ')'` /> + + + + "); + + logger.AssertLogContains("Transformed item list: 'X;X%3bX.txt Y;Y%3bY.txt Z;Z%3bZ.txt'"); + } + + /// + /// Do an item transform, where the transform expression contains an unescaped semicolon as well + /// as an escaped percent sign. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ItemTransformContainingSemicolon_InTaskHost() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + + + + + + + + '%(FileName);%(FileName)%253b%(FileName)%(Extension)',' ')'` /> + + + + "); + + logger.AssertLogContains("Transformed item list: 'X;X%3bX.txt Y;Y%3bY.txt Z;Z%3bZ.txt'"); + } + + /// + /// Tests that when we add an item and are in a directory with characters in need of escaping, and the + /// item's FullPath metadata is retrieved, that a properly un-escaped version of the path is returned + /// + [TestMethod] + public void FullPathMetadataOnItemUnescaped() + { + string projectName = "foo.proj"; + string projectRelativePath = "(jay's parens test)"; + string path = Path.Combine(Path.GetTempPath(), projectRelativePath); + string projectAbsolutePath = Path.Combine(path, projectName); + + try + { + Directory.CreateDirectory(path); + + ProjectRootElement projectElement = ProjectRootElement.Create(projectAbsolutePath); + ProjectItemGroupElement itemgroup = projectElement.AddItemGroup(); + itemgroup.AddItem("ProjectFile", projectName); + + Project project = new Project(projectElement, null, null, new ProjectCollection()); + ProjectInstance projectInstance = project.CreateProjectInstance(); + + IEnumerable items = projectInstance.GetItems("ProjectFile"); + Assert.AreEqual(projectAbsolutePath, items.First().GetMetadataValue("FullPath")); + } + finally + { + if (File.Exists(projectAbsolutePath)) File.Delete(projectAbsolutePath); + if (Directory.Exists(path)) Directory.Delete(path); + } + } + + + /// + /// Test that we can pass in global properties containing escaped characters and they + /// won't be unescaped. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void GlobalPropertyWithEscapedCharacters() + { + MockLogger logger = new MockLogger(); + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + "); + + project.SetGlobalProperty("MyGlobalProperty", "foo%253bbar"); + + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + + logger.AssertLogContains("MyGlobalProperty = 'foo%3bbar'"); + } + + /// + /// If %2A (escaped '*') or %3F (escaped '?') is in an item's Include, it should be treated + /// literally, not as a wildcard + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void EscapedWildcardsShouldNotBeExpanded() + { + MockLogger logger = new MockLogger(); + + try + { + // Populate the project directory with three physical files on disk -- a.weirdo, b.weirdo, c.weirdo. + EscapingInProjectsHelper.CreateThreeWeirdoFiles(); + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + + + + "); + + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + logger.AssertLogContains("[*]"); + } + finally + { + EscapingInProjectsHelper.CleanupWeirdoFiles(); + } + } + + /// + /// If %2A (escaped '*') or %3F (escaped '?') is in an item's Include, it should be treated + /// literally, not as a wildcard + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void EscapedWildcardsShouldNotBeExpanded_InTaskHost() + { + MockLogger logger = new MockLogger(); + + try + { + // Populate the project directory with three physical files on disk -- a.weirdo, b.weirdo, c.weirdo. + EscapingInProjectsHelper.CreateThreeWeirdoFiles(); + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + + + + + + "); + + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + logger.AssertLogContains("[*]"); + } + finally + { + EscapingInProjectsHelper.CleanupWeirdoFiles(); + } + } + + /// + /// Parity with Orcas: Target names are always unescaped, and in fact, if there are two targets, + /// one the escaped version of the other, the second will override the first as though they had the + /// same name. + /// + [TestMethod] + public void TargetNamesAlwaysUnescaped() + { + bool exceptionCaught = false; + + try + { + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + "); + } + catch (InvalidProjectFileException ex) + { + string expectedErrorMessage = ResourceUtilities.FormatResourceString("NameInvalid", "$", "$"); + Assert.IsTrue(String.Equals(ex.Message, expectedErrorMessage, StringComparison.OrdinalIgnoreCase), "Wrong error message"); + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught, "Expected an InvalidProjectFileException"); + } + + /// + /// Parity with Orcas: Target names are always unescaped, and in fact, if there are two targets, + /// one the escaped version of the other, the second will override the first as though they had the + /// same name. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TargetNamesAlwaysUnescaped_Override() + { + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + + + + "); + MockLogger logger = new MockLogger(); + + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + logger.AssertLogContains("[OVERRIDE]"); + } + + /// + /// Tests that when we set metadata through the evaluation model, we do the right thing + /// + [TestMethod] + public void SpecialCharactersInMetadataValueConstruction() + { + string projectString = @" + + + + %3B + %24 + + + "; + System.Xml.XmlReader reader = new System.Xml.XmlTextReader(new StringReader(projectString)); + Project project = new Project(reader); + ProjectItem item = project.GetItems("None").Single(); + + EscapingInProjectsHelper.SpecialCharactersInMetadataValueTests(item); + } + + /// + /// Tests that when we set metadata through the evaluation model, we do the right thing + /// + [TestMethod] + public void SpecialCharactersInMetadataValueEvaluation() + { + Project project = new Project(); + ProjectItem item = project.AddItem("None", "MetadataTests", new Dictionary { + {"EscapedSemicolon", "%3B"}, //Microsoft.Build.Evaluation.ProjectCollection.Escape(";") + {"EscapedDollarSign", "%24"}, //Microsoft.Build.Evaluation.ProjectCollection.Escape("$") + }).Single(); + + EscapingInProjectsHelper.SpecialCharactersInMetadataValueTests(item); + project.ReevaluateIfNecessary(); + EscapingInProjectsHelper.SpecialCharactersInMetadataValueTests(item); + } + + /// + /// Say you have a scenario where a user is allowed to specify an arbitrary set of files (or + /// any sort of items) and expects to be able to get them back out as they were sent in. In addition, + /// the user can specify a macro (property) that can resolve to yet another arbitrary set of items. + /// We want to make sure that we do the right thing (assuming that the user escaped the information + /// correctly coming in) and don't mess up their set of items + /// + [TestMethod] + public void CanGetCorrectListOfItemsWithSemicolonsInThem() + { + string projectString = @" + + + foo%3bbar + + + + + + + "; + + System.Xml.XmlReader reader = new System.Xml.XmlTextReader(new StringReader(projectString)); + Project project = new Project(reader); + IEnumerable items = project.GetItems("CrazyList"); + + Assert.AreEqual(3, items.Count()); + Assert.AreEqual(items.ElementAt(0).EvaluatedInclude, "a"); + Assert.AreEqual(items.ElementAt(1).EvaluatedInclude, "b;c"); + Assert.AreEqual(items.ElementAt(2).EvaluatedInclude, "foo;bar"); + } + + /// + /// Say you have a scenario where a user is allowed to specify an arbitrary set of files (or + /// any sort of items) and expects to be able to get them back out as they were sent in. In addition, + /// the user can specify a macro (property) that can resolve to yet another arbitrary set of items. + /// We want to make sure that we do the right thing (assuming that the user escaped the information + /// correctly coming in) and don't mess up their set of items + /// + [TestMethod] + public void CanGetCorrectListOfItemsWithSemicolonsInThem2() + { + string projectString = @" + + + foo;bar + + + + + + + "; + + System.Xml.XmlReader reader = new System.Xml.XmlTextReader(new StringReader(projectString)); + Project project = new Project(reader); + IEnumerable items = project.GetItems("CrazyList"); + + Assert.AreEqual(4, items.Count()); + Assert.AreEqual(items.ElementAt(0).EvaluatedInclude, "a"); + Assert.AreEqual(items.ElementAt(1).EvaluatedInclude, "b;c"); + Assert.AreEqual(items.ElementAt(2).EvaluatedInclude, "foo"); + Assert.AreEqual(items.ElementAt(3).EvaluatedInclude, "bar"); + } + } + + [TestClass] + public class FullProjectsUsingMicrosoftCommonTargets + { + /// + /// ESCAPING: Escaping in conditionals is broken. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SemicolonInConfiguration() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + ClassLibrary16 + + + bin\a%3bb%27c\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + // Create a logger. + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory("foo.csproj"); + + // Build the default targets using the Configuration "a;b'c". + project.SetGlobalProperty("Configuration", EscapingUtilities.Escape("a;b'c")); + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\a;b'c\ClassLibrary16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\a;b'c\ClassLibrary16.dll"); + + logger.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\a;b'c\ClassLibrary16.dll"))); + } + + /// + /// ESCAPING: Escaping in conditionals is broken. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void SemicolonInConfiguration_UsingTaskHost() + { + string originalOverrideTaskHostVariable = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + ClassLibrary16 + + + bin\a%3bb%27c\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + // Create a logger. + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.LoadProjectFileInTempProjectDirectory("foo.csproj"); + + // Build the default targets using the Configuration "a;b'c". + project.SetGlobalProperty("Configuration", EscapingUtilities.Escape("a;b'c")); + bool success = project.Build(logger); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\a;b'c\ClassLibrary16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\a;b'c\ClassLibrary16.dll"); + + logger.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\a;b'c\ClassLibrary16.dll"))); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", originalOverrideTaskHostVariable); + } + } + + /// + /// ESCAPING: CopyBuildTarget target fails if the output assembly name contains a semicolon or single-quote + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void SemicolonInAssemblyName() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + Class%3bLibrary16 + + + bin\Debug\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class;Library16.dll", @"Did not find expected file obj\debug\Class;Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class;Library16.pdb", @"Did not find expected file obj\debug\Class;Library16.pdb"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\Class;Library16.dll", @"Did not find expected file bin\debug\Class;Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class;Library16.pdb", @"Did not find expected file obj\debug\Class;Library16.pdb"); + + log.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\Debug\Class;Library16.dll"))); + } + + /// + /// ESCAPING: CopyBuildTarget target fails if the output assembly name contains a semicolon or single-quote + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SemicolonInAssemblyName_UsingTaskHost() + { + string originalOverrideTaskHostVariable = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + Class%3bLibrary16 + + + bin\Debug\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class;Library16.dll", @"Did not find expected file obj\debug\Class;Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class;Library16.pdb", @"Did not find expected file obj\debug\Class;Library16.pdb"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\Class;Library16.dll", @"Did not find expected file bin\debug\Class;Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class;Library16.pdb", @"Did not find expected file obj\debug\Class;Library16.pdb"); + + log.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\Debug\Class;Library16.dll"))); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", originalOverrideTaskHostVariable); + } + } + + /// + /// ESCAPING: Conversion Issue: Properties with $(xxx) as literals are not being converted correctly + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DollarSignInAssemblyName() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + Class%24%28prop%29Library16 + + + bin\Debug\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class$(prop)Library16.dll", @"Did not find expected file obj\debug\Class$(prop)Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class$(prop)Library16.pdb", @"Did not find expected file obj\debug\Class$(prop)Library16.pdb"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\Class$(prop)Library16.dll", @"Did not find expected file bin\debug\Class$(prop)Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\Class$(prop)Library16.pdb", @"Did not find expected file bin\debug\Class$(prop)Library16.pdb"); + + log.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\Debug\Class$(prop)Library16.dll"))); + } + + /// + /// ESCAPING: Conversion Issue: Properties with $(xxx) as literals are not being converted correctly + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void DollarSignInAssemblyName_UsingTaskHost() + { + string originalOverrideTaskHostVariable = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + Class%24%28prop%29Library16 + + + bin\Debug\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class$(prop)Library16.dll", @"Did not find expected file obj\debug\Class$(prop)Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\Class$(prop)Library16.pdb", @"Did not find expected file obj\debug\Class$(prop)Library16.pdb"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\Class$(prop)Library16.dll", @"Did not find expected file bin\debug\Class$(prop)Library16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\Class$(prop)Library16.pdb", @"Did not find expected file bin\debug\Class$(prop)Library16.pdb"); + + log.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\Debug\Class$(prop)Library16.dll"))); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", originalOverrideTaskHostVariable); + } + } + + /// + /// This is the case when one of the source code files in the project has a filename containing a semicolon. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void SemicolonInSourceCodeFilename() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + ClassLibrary16 + + + bin\Debug\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class;1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\ClassLibrary16.dll", @"Did not find expected file obj\debug\ClassLibrary16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\ClassLibrary16.pdb", @"Did not find expected file obj\debug\ClassLibrary16.pdb"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\ClassLibrary16.dll", @"Did not find expected file bin\debug\ClassLibrary16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\ClassLibrary16.pdb", @"Did not find expected file bin\debug\ClassLibrary16.pdb"); + + log.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\Debug\ClassLibrary16.dll"))); + } + + /// + /// This is the case when one of the source code files in the project has a filename containing a semicolon. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SemicolonInSourceCodeFilename_UsingTaskHost() + { + string originalOverrideTaskHostVariable = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + Library + ClassLibrary16 + + + bin\Debug\ + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class;1.cs", @" + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\ClassLibrary16.dll", @"Did not find expected file obj\debug\ClassLibrary16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"obj\debug\ClassLibrary16.pdb", @"Did not find expected file obj\debug\ClassLibrary16.pdb"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\ClassLibrary16.dll", @"Did not find expected file bin\debug\ClassLibrary16.dll"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bin\debug\ClassLibrary16.pdb", @"Did not find expected file bin\debug\ClassLibrary16.pdb"); + + log.AssertLogContains(String.Format("foo -> {0}", Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\Debug\ClassLibrary16.dll"))); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", originalOverrideTaskHostVariable); + } + } + + /// + /// Build a .SLN file using MSBuild. The .SLN and the projects contained within + /// have all sorts of crazy characters in their name. There + /// is even a P2P reference between the two projects in the .SLN. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void SolutionWithLotsaCrazyCharacters() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------------------------------------------------------- + // Console;!@(foo)'^(Application1.sln + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1.sln", + + @"Microsoft Visual Studio Solution File, Format Version 11.00 + # Visual Studio 2005 + Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `Cons.ole;!@(foo)'^(Application1`, `Console;!@(foo)'^(Application1\Cons.ole;!@(foo)'^(Application1.csproj`, `{770F2381-8C39-49E9-8C96-0538FA4349A7}` + EndProject + Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `Class;!@(foo)'^(Library1`, `Class;!@(foo)'^(Library1\Class;!@(foo)'^(Library1.csproj`, `{0B4B78CC-C752-43C2-BE9A-319D20216129}` + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Release|Any CPU.Build.0 = Release|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "); + + // --------------------------------------------------------------------- + // Console;!@(foo)'^(Application1.csproj + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1\Cons.ole;!@(foo)'^(Application1.csproj", + + @" + + + Debug + AnyCPU + 8.0.50510 + 2.0 + {770F2381-8C39-49E9-8C96-0538FA4349A7} + Exe + Properties + Console____foo____Application1 + Console%3b!%40%28foo%29%27^%28Application1 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + {0B4B78CC-C752-43C2-BE9A-319D20216129} + Class%3b!%40%28foo%29%27^%28Library1 + + + + + "); + + // --------------------------------------------------------------------- + // Program.cs + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1\Program.cs", + + @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace Console____foo____Application1 + { + class Program + { + static void Main(string[] args) + { + Class____foo____Library1.Class1 foo = new Class____foo____Library1.Class1(); + } + } + } + "); + + // --------------------------------------------------------------------- + // Class;!@(foo)'^(Library1.csproj + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Class;!@(foo)'^(Library1\Class;!@(foo)'^(Library1.csproj", + + @" + + + Debug + AnyCPU + 8.0.50510 + 2.0 + {0B4B78CC-C752-43C2-BE9A-319D20216129} + Library + Properties + Class____foo____Library1 + Class%3b!%40%28foo%29%27^%28Library1 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + "); + + // --------------------------------------------------------------------- + // Class1.cs + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Class;!@(foo)'^(Library1\Class1.cs", + + @" + namespace Class____foo____Library1 + { + public class Class1 + { + } + } + "); + + // Cons.ole;!@(foo)'^(Application1 + string targetForFirstProject = "Cons_ole_!__foo__^_Application1"; + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1.sln", new string[] { targetForFirstProject }, null); + + Assert.IsTrue + ( + File.Exists(Path.Combine(ObjectModelHelpers.TempProjectDir, @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1\bin\debug\Console;!@(foo)'^(Application1.exe")), + @"Did not find expected file Console;!@(foo)'^(Application1.exe" + ); + } + + /// + /// Build a .SLN file using MSBuild. The .SLN and the projects contained within + /// have all sorts of crazy characters in their name. There + /// is even a P2P reference between the two projects in the .SLN. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SolutionWithLotsaCrazyCharacters_UsingTaskHost() + { + string originalOverrideTaskHostVariable = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------------------------------------------------------- + // Console;!@(foo)'^(Application1.sln + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1.sln", + + @"Microsoft Visual Studio Solution File, Format Version 11.00 + # Visual Studio 2005 + Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `Cons.ole;!@(foo)'^(Application1`, `Console;!@(foo)'^(Application1\Cons.ole;!@(foo)'^(Application1.csproj`, `{770F2381-8C39-49E9-8C96-0538FA4349A7}` + EndProject + Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `Class;!@(foo)'^(Library1`, `Class;!@(foo)'^(Library1\Class;!@(foo)'^(Library1.csproj`, `{0B4B78CC-C752-43C2-BE9A-319D20216129}` + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {770F2381-8C39-49E9-8C96-0538FA4349A7}.Release|Any CPU.Build.0 = Release|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B4B78CC-C752-43C2-BE9A-319D20216129}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "); + + // --------------------------------------------------------------------- + // Console;!@(foo)'^(Application1.csproj + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1\Cons.ole;!@(foo)'^(Application1.csproj", + + @" + + + Debug + AnyCPU + 8.0.50510 + 2.0 + {770F2381-8C39-49E9-8C96-0538FA4349A7} + Exe + Properties + Console____foo____Application1 + Console%3b!%40%28foo%29%27^%28Application1 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + {0B4B78CC-C752-43C2-BE9A-319D20216129} + Class%3b!%40%28foo%29%27^%28Library1 + + + + + "); + + // --------------------------------------------------------------------- + // Program.cs + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1\Program.cs", + + @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace Console____foo____Application1 + { + class Program + { + static void Main(string[] args) + { + Class____foo____Library1.Class1 foo = new Class____foo____Library1.Class1(); + } + } + } + "); + + // --------------------------------------------------------------------- + // Class;!@(foo)'^(Library1.csproj + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Class;!@(foo)'^(Library1\Class;!@(foo)'^(Library1.csproj", + + @" + + + Debug + AnyCPU + 8.0.50510 + 2.0 + {0B4B78CC-C752-43C2-BE9A-319D20216129} + Library + Properties + Class____foo____Library1 + Class%3b!%40%28foo%29%27^%28Library1 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + "); + + // --------------------------------------------------------------------- + // Class1.cs + // --------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"SLN;!@(foo)'^1\Class;!@(foo)'^(Library1\Class1.cs", + + @" + namespace Class____foo____Library1 + { + public class Class1 + { + } + } + "); + + // Cons.ole;!@(foo)'^(Application1 + string targetForFirstProject = "Cons_ole_!__foo__^_Application1"; + + MockLogger log = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1.sln", new string[] { targetForFirstProject }, null); + + Assert.IsTrue + ( + File.Exists(Path.Combine(ObjectModelHelpers.TempProjectDir, @"SLN;!@(foo)'^1\Console;!@(foo)'^(Application1\bin\debug\Console;!@(foo)'^(Application1.exe")), + @"Did not find expected file Console;!@(foo)'^(Application1.exe" + ); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", originalOverrideTaskHostVariable); + } + } + } + + internal class EscapingInProjectsHelper + { + /// + /// Deletes all *.weirdo files from the temp path, and dumps 3 files there -- + /// a.weirdo, b.weirdo, c.weirdo. This is so that we can exercise our wildcard + /// matching a little bit without having to plumb mock objects all the way through + /// the engine. + /// + internal static void CreateThreeWeirdoFiles() + { + CleanupWeirdoFiles(); + + string tempPath = Path.GetTempPath(); + + // Create 3 files in the temp path -- a.weirdo, b.weirdo, and c.weirdo. + File.WriteAllText(Path.Combine(tempPath, "a.weirdo"), String.Empty); + File.WriteAllText(Path.Combine(tempPath, "b.weirdo"), String.Empty); + File.WriteAllText(Path.Combine(tempPath, "c.weirdo"), String.Empty); + } + + /// + /// Delete all *.weirdo files from the temp directory. + /// + internal static void CleanupWeirdoFiles() + { + // Delete all *.weirdo files from the temp path. + string[] filesEndingWithWeirdo = Directory.GetFiles(Path.GetTempPath(), "*.weirdo"); + foreach (string fileEndingWithWeirdo in filesEndingWithWeirdo) + { + File.Delete(fileEndingWithWeirdo); + } + } + + /// + /// Given a project and an item type, gets the items of that type, and renames an item + /// with the old evaluated include to have the new evaluated include instead. + /// + /// + /// + /// + /// + internal static IEnumerable ModifyItemOfTypeInProject(Project project, string itemType, string oldEvaluatedInclude, string newEvaluatedInclude) + { + IEnumerable itemsToMatch = project.GetItems(itemType); + List matchingItems = new List(); + + foreach (ProjectItem item in itemsToMatch) + { + if (String.Equals(item.EvaluatedInclude, oldEvaluatedInclude, StringComparison.OrdinalIgnoreCase)) + { + matchingItems.Add(item); + } + } + + for (int i = 0; i < matchingItems.Count; i++) + { + matchingItems[i].Rename(newEvaluatedInclude); + } + + return matchingItems; + } + + /// + /// Helper for SpecialCharactersInMetadataValue tests + /// + internal static void SpecialCharactersInMetadataValueTests(ProjectItem item) + { + Assert.AreEqual("%3B", item.GetMetadata("EscapedSemicolon").UnevaluatedValue); + Assert.AreEqual("%3B", item.GetMetadata("EscapedSemicolon").EvaluatedValueEscaped); + Assert.AreEqual(";", item.GetMetadata("EscapedSemicolon").EvaluatedValue); + Assert.AreEqual("%3B", Project.GetMetadataValueEscaped(item, "EscapedSemicolon")); + Assert.AreEqual(";", item.GetMetadataValue("EscapedSemicolon")); + + Assert.AreEqual("%24", item.GetMetadata("EscapedDollarSign").UnevaluatedValue); + Assert.AreEqual("%24", item.GetMetadata("EscapedDollarSign").EvaluatedValueEscaped); + Assert.AreEqual("$", item.GetMetadata("EscapedDollarSign").EvaluatedValue); + Assert.AreEqual("%24", Project.GetMetadataValueEscaped(item, "EscapedDollarSign")); + Assert.AreEqual("$", item.GetMetadataValue("EscapedDollarSign")); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/Evaluator_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/Evaluator_Tests.cs new file mode 100644 index 00000000000..1b0015fadc2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/Evaluator_Tests.cs @@ -0,0 +1,4165 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for evaluation +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text.RegularExpressions; +using System.Globalization; +using System.IO; +using System.Net; +using System.Threading; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using ProjectHelpers = Microsoft.Build.UnitTests.BackEnd.ProjectHelpers; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.Evaluation +{ + /// + /// Tests mainly for project evaluation + /// + [TestClass] + public class Evaluator_Tests + { + /// + /// Cleanup + /// + [TestInitialize] + public void Setup() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + /// + /// Cleanup + /// + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + /// + /// Verify Exist condition used in Import or ImportGroup elements will succeed when in-memory project is available inside projectCollection. + /// + [TestMethod] + public void VerifyExistsInMemoryProjecs() + { + string projXml = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + + string fooXml = ObjectModelHelpers.CleanupFileContents(@" + + + yes + + "); + string barXml = ObjectModelHelpers.CleanupFileContents(@" + + + yes + + "); + + // no imports should be loaded + Project project = new Project(XmlReader.Create(new StringReader(projXml))); + project.ReevaluateIfNecessary(); + + Assert.IsNull(project.GetProperty("foo")); + Assert.IsNull(project.GetProperty("bar")); + + // add in-memory project c:\temp\foo.import + Project fooImport = new Project(XmlReader.Create(new StringReader(fooXml))); + fooImport.FullPath = @"c:\temp\foo.import"; + + // force reevaluation + project.MarkDirty(); + project.ReevaluateIfNecessary(); + + // foo should be available now via fooImport + Assert.IsNotNull(project.GetProperty("foo")); + Assert.IsNull(project.GetProperty("bar")); + + // add in-memory project c:\temp\bar.import + Project barImport = new Project(XmlReader.Create(new StringReader(barXml))); + barImport.FullPath = @"c:\temp\bar.import"; + + // force reevaluation + project.MarkDirty(); + project.ReevaluateIfNecessary(); + + // both foo and bar should be available + Assert.IsNotNull(project.GetProperty("foo")); + Assert.IsNotNull(project.GetProperty("bar")); + + // remove the imports from PRE + fooImport.FullPath = @"c:\temp\alteredfoo.import"; + barImport.FullPath = @"c:\temp\alteredbar.import"; + + // force reevaluation + project.MarkDirty(); + project.ReevaluateIfNecessary(); + + // both foo and bar should be gone + Assert.IsNull(project.GetProperty("foo")); + Assert.IsNull(project.GetProperty("bar")); + } + + /// + /// Verify when the conditions are evaluated outside of a target that they are evaluated relative to the file they are physically contained in, + /// in the case of Imports, and ImportGroups, and PropertyGroups, but that property conditions are evaluated relative to the project file. + /// When conditions are evaluated inside of a target they are evaluated relative to the project file. + /// + /// File Structure + /// test.targets + /// test.tx, file to check for existance + /// subdir\test.proj + /// + [TestMethod] + public void VerifyConditionsInsideOutsideTargets() + { + string testtargets = @" + + + test.txt + ..\test.txt + + + test.txt + + + ..\test.txt + + + + test.txt + ..\test.txt + + + test.txt + + + ..\test.txt + + + + + + + + + + + + + + + "; + + string projDirTargets = @" + + + + + "; + + string projDirTargets2 = @" + + + + + "; + + string targetDirTargets = @" + + + + + "; + + string targetDirTargets2 = @" + + + + + "; + + string subdirTestProj = @" + + + "; + + string testTxt = @"Hello"; + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "VerifyConditionsInsideOutsideTargets"); + string subDirectory = Path.Combine(targetDirectory, "subdir"); + + string testTargetPath = Path.Combine(targetDirectory, "test.targets"); + string targetDirectoryTargetsPath = Path.Combine(targetDirectory, "targetdir.targets"); + string targetDirectoryTargetsPath2 = Path.Combine(targetDirectory, "targetdir2.targets"); + string subdirProjPath = Path.Combine(subDirectory, "test.proj"); + string projectDirectoryTargetsPath = Path.Combine(subDirectory, "projdir.targets"); + string projectDirectoryTargetsPath2 = Path.Combine(subDirectory, "projdir2.targets"); + string textTextPath = Path.Combine(targetDirectory, "test.txt"); + + try + { + Directory.CreateDirectory(subDirectory); + File.WriteAllText(testTargetPath, ObjectModelHelpers.CleanupFileContents(testtargets)); + File.WriteAllText(subdirProjPath, ObjectModelHelpers.CleanupFileContents(subdirTestProj)); + File.WriteAllText(textTextPath, testTxt); + File.WriteAllText(targetDirectoryTargetsPath, ObjectModelHelpers.CleanupFileContents(targetDirTargets)); + File.WriteAllText(targetDirectoryTargetsPath2, ObjectModelHelpers.CleanupFileContents(targetDirTargets2)); + File.WriteAllText(projectDirectoryTargetsPath, ObjectModelHelpers.CleanupFileContents(projDirTargets)); + File.WriteAllText(projectDirectoryTargetsPath2, ObjectModelHelpers.CleanupFileContents(projDirTargets2)); + + Project project = new Project(subdirProjPath); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.IsTrue(result); + logger.AssertLogContains("PropertyOutsideTarget: ..\\test.txt"); + logger.AssertLogContains("PropertyGroupOutsideTarget: test.txt"); + logger.AssertLogContains("PropertyInsideTarget: ..\\test.txt"); + logger.AssertLogContains("PropertyGroupInsideTarget: ..\\test.txt"); + logger.AssertLogContains("[TargetDirectoryTargetsImport]"); + logger.AssertLogDoesntContain("[ProjectDirectoryTargetsImport]"); + logger.AssertLogContains("[TargetDirectoryTargetsImportGroup]"); + logger.AssertLogDoesntContain("[ProjectDirectoryTargetsImportGroup]"); + } + finally + { + FileUtilities.DeleteDirectoryNoThrow(targetDirectory, true); + } + } + + /// + /// Verify when the conditions are evaluated outside of a target that they are evaluated relative to the file they are physically contained in, + /// in the case of Imports, and ImportGroups, and PropertyGroups, but that property conditions are evaluated relative to the project file. + /// When conditions are evaluated inside of a target they are evaluated relative to the project file. + /// + /// File Structure + /// test.targets + /// test.tx, file to check for existance + /// subdir\test.proj + /// + [TestMethod] + public void VerifyConditionsInsideOutsideTargets_ProjectInstance() + { + string testtargets = @" + + + test.txt + ..\test.txt + + + test.txt + + + ..\test.txt + + + + test.txt + ..\test.txt + + + test.txt + + + ..\test.txt + + + + + + + + + + + + + + + "; + + string projDirTargets = @" + + + + + "; + + string projDirTargets2 = @" + + + + + "; + + string targetDirTargets = @" + + + + + "; + + string targetDirTargets2 = @" + + + + + "; + + string subdirTestProj = @" + + + "; + + string testTxt = @"Hello"; + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "VerifyConditionsInsideOutsideTargets"); + string subDirectory = Path.Combine(targetDirectory, "subdir"); + + string testTargetPath = Path.Combine(targetDirectory, "test.targets"); + string targetDirectoryTargetsPath = Path.Combine(targetDirectory, "targetdir.targets"); + string targetDirectoryTargetsPath2 = Path.Combine(targetDirectory, "targetdir2.targets"); + string subdirProjPath = Path.Combine(subDirectory, "test.proj"); + string projectDirectoryTargetsPath = Path.Combine(subDirectory, "projdir.targets"); + string projectDirectoryTargetsPath2 = Path.Combine(subDirectory, "projdir2.targets"); + string textTextPath = Path.Combine(targetDirectory, "test.txt"); + + try + { + Directory.CreateDirectory(subDirectory); + File.WriteAllText(testTargetPath, ObjectModelHelpers.CleanupFileContents(testtargets)); + File.WriteAllText(subdirProjPath, ObjectModelHelpers.CleanupFileContents(subdirTestProj)); + File.WriteAllText(textTextPath, testTxt); + File.WriteAllText(targetDirectoryTargetsPath, ObjectModelHelpers.CleanupFileContents(targetDirTargets)); + File.WriteAllText(targetDirectoryTargetsPath2, ObjectModelHelpers.CleanupFileContents(targetDirTargets2)); + File.WriteAllText(projectDirectoryTargetsPath, ObjectModelHelpers.CleanupFileContents(projDirTargets)); + File.WriteAllText(projectDirectoryTargetsPath2, ObjectModelHelpers.CleanupFileContents(projDirTargets2)); + + ProjectInstance project = new ProjectInstance(subdirProjPath); + + MockLogger logger = new MockLogger(); + bool result = project.Build(new ILogger[] { logger }); + Assert.IsTrue(result); + logger.AssertLogContains("PropertyOutsideTarget: ..\\test.txt"); + logger.AssertLogContains("PropertyGroupOutsideTarget: test.txt"); + logger.AssertLogContains("PropertyInsideTarget: ..\\test.txt"); + logger.AssertLogContains("PropertyGroupInsideTarget: ..\\test.txt"); + logger.AssertLogContains("[TargetDirectoryTargetsImport]"); + logger.AssertLogDoesntContain("[ProjectDirectoryTargetsImport]"); + logger.AssertLogContains("[TargetDirectoryTargetsImportGroup]"); + logger.AssertLogDoesntContain("[ProjectDirectoryTargetsImportGroup]"); + } + finally + { + FileUtilities.DeleteDirectoryNoThrow(targetDirectory, true); + } + } + + /// + /// When properties are consumed and set in imports make sure that we get the correct warnigns. + /// + [TestMethod] + public void VerifyUsedUnInitializedPropertyInImports() + { + string targetA = ObjectModelHelpers.CleanupFileContents(@" + + + $(bar) + + + "); + + string targetB = ObjectModelHelpers.CleanupFileContents(@" + + + Something + + + "); + + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "VerifyUsedUnInitializedPropertyInImports"); + + string targetAPath = Path.Combine(targetDirectory, "targetA.targets"); + string targetBPath = Path.Combine(targetDirectory, "targetB.targets"); + string projectPath = Path.Combine(targetDirectory, "test.proj"); + bool originalValue = BuildParameters.WarnOnUninitializedProperty; + try + { + BuildParameters.WarnOnUninitializedProperty = true; + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(targetAPath, targetA); + File.WriteAllText(targetBPath, targetB); + File.WriteAllText(projectPath, projectContents); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(projectPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogContains("MSB4211"); + } + finally + { + BuildParameters.WarnOnUninitializedProperty = originalValue; + Directory.Delete(targetDirectory, true); + } + } + + /// + /// If a property is set to an empty value and then set to a non empty value we do not expect a warning. + /// + [TestMethod] + public void EmptyPropertyIsThenSet() + { + string testtargets = ObjectModelHelpers.CleanupFileContents(@" + + + + $(bar) + Something + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "EmptyPropertyIsThenSet"); + string testTargetPath = Path.Combine(targetDirectory, "test.proj"); + + string originalValue = Environment.GetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY"); + try + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", "true"); + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(testTargetPath, testtargets); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(testTargetPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogDoesntContain("MSB4211"); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", originalValue); + Directory.Delete(targetDirectory, true); + } + } + + /// + /// If a property is set to an empty value and the environment variable is not set then we do not expect an error + /// + [TestMethod] + public void EmptyPropertyIsThenSetEnvironmentVariableNotSet() + { + string testtargets = ObjectModelHelpers.CleanupFileContents(@" + + + $(bar) + Something + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "EmptyPropertyIsThenSet"); + string testTargetPath = Path.Combine(targetDirectory, "test.proj"); + + string originalValue = Environment.GetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY"); + try + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", null); + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(testTargetPath, testtargets); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(testTargetPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogDoesntContain("MSB4211"); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", originalValue); + Directory.Delete(targetDirectory, true); + } + } + + /// + /// If a property has not been set yet and we consume the property we are setting in order to set it, do not warn + /// + [TestMethod] + public void SetPropertyToItself() + { + string testtargets = ObjectModelHelpers.CleanupFileContents(@" + + + $(baz);I am some text + STUFF $(baz) STUFF + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "SetPropertyToItself"); + string testTargetPath = Path.Combine(targetDirectory, "test.proj"); + + string originalValue = Environment.GetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY"); + try + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", "true"); + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(testTargetPath, testtargets); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(testTargetPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogDoesntContain("MSB4211"); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", originalValue); + Directory.Delete(targetDirectory, true); + } + } + + /// + /// If we consume a property which has not been initialized in a property we do not expect a warning because we are explicitly ignoring conditions. + /// This is done because it is a very common scenario to use uninitialized properties in conditions to set default values. + /// + [TestMethod] + public void UsePropertyInCondition() + { + string testtargets = ObjectModelHelpers.CleanupFileContents(@" + + + Something + + + Something + + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "UsePropertyInCondition"); + string testTargetPath = Path.Combine(targetDirectory, "test.proj"); + + string originalValue = Environment.GetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY"); + try + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", "true"); + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(testTargetPath, testtargets); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(testTargetPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogDoesntContain("MSB4211"); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDWARNONUNINITIALIZEDPROPERTY", originalValue); + Directory.Delete(targetDirectory, true); + } + } + + /// + /// If a property is consumed before it is initialized for the first time log a warning. + /// + [TestMethod] + public void UsePropertyBeforeSet() + { + string testtargets = ObjectModelHelpers.CleanupFileContents(@" + + + $(baz) $(bar) + Something + Something + + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "UsePropertyBeforeSet"); + string testTargetPath = Path.Combine(targetDirectory, "test.proj"); + + bool originalValue = BuildParameters.WarnOnUninitializedProperty; + try + { + BuildParameters.WarnOnUninitializedProperty = true; + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(testTargetPath, testtargets); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(testTargetPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogContains("MSB4211"); + Assert.IsTrue(logger.WarningCount == 2, "Expected two warnings"); + } + finally + { + BuildParameters.WarnOnUninitializedProperty = originalValue; + Directory.Delete(targetDirectory, true); + } + } + + /// + /// If we use a property twice make sure we warn and dont crash due to the dictionary which is holding the used but uninitialized variables.. + /// + [TestMethod] + public void UsePropertyBeforeSetDuplicates() + { + string testtargets = ObjectModelHelpers.CleanupFileContents(@" + + + $(baz) $(bar) + $(baz) $(bar) + Something + Something + + + + "); + + string tempPath = Path.GetTempPath(); + string targetDirectory = Path.Combine(tempPath, "UsePropertyBeforeSetDuplicates"); + string testTargetPath = Path.Combine(targetDirectory, "test.proj"); + + bool originalValue = BuildParameters.WarnOnUninitializedProperty; + try + { + BuildParameters.WarnOnUninitializedProperty = true; + Directory.CreateDirectory(targetDirectory); + File.WriteAllText(testTargetPath, testtargets); + + MockLogger logger = new MockLogger(); + ProjectCollection pc = new ProjectCollection(); + pc.RegisterLogger(logger); + Project project = pc.LoadProject(testTargetPath); + + bool result = project.Build(); + Assert.AreEqual(true, result); + logger.AssertLogContains("MSB4211"); + Assert.IsTrue(logger.WarningCount == 2, "Expected two warnings"); + } + finally + { + BuildParameters.WarnOnUninitializedProperty = originalValue; + Directory.Delete(targetDirectory, true); + } + } + + /// + /// Imports should only be included once. + /// A second import should give a warning, and then be ignored. + /// If it is imported twice, subtle problems will occur: because typically the 2nd import will + /// have no effect, but occasionally it won't. + /// + [TestMethod] + public void ImportsOnlyIncludedOnce() + { + string importPath = null; + + try + { + importPath = FileUtilities.GetTemporaryFile(); + + string import = ObjectModelHelpers.CleanupFileContents(@" + + +

$(q)

+
+
+ "); + + File.WriteAllText(importPath, import); + + // If the import is pulled in again, the property 'q' will be defined when it is + // assigned to 'p' in the second inclusion + string content = ObjectModelHelpers.CleanupFileContents(@" + + + foo + + + + + + foo_bar + + + + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains("foo"); + logger.AssertLogDoesntContain("foo_bar"); + } + finally + { + File.Delete(importPath); + } + } + + /// + /// Imports should only be included once. However we should be able to see them in the ImportsIncludingDuplicates property + /// A second import should give a warning, and still not be added to the Imports property. + /// + [TestMethod] + public void MultipleImportsVerifyImportsIncludingDuplicates() + { + string importPath = null; + string importPath2 = null; + string importPath3 = null; + + try + { + importPath = FileUtilities.GetTemporaryFile(); + importPath2 = FileUtilities.GetTemporaryFile(); + importPath3 = FileUtilities.GetTemporaryFile(); + + string import = ObjectModelHelpers.CleanupFileContents(@" + + +

Hello

+
+
+ "); + + string import2 = ObjectModelHelpers.CleanupFileContents(@" + + +

Hello

+
+ +
+ "); + + File.WriteAllText(importPath, import2); + File.WriteAllText(importPath2, import2); + File.WriteAllText(importPath3, import); + + // If the import is pulled in again, the property 'q' will be defined when it is + // assigned to 'p' in the second inclusion + string content = ObjectModelHelpers.CleanupFileContents(@" + + + foo + + + + + + foo_bar + + + + + + + + + "); + + ProjectCollection pc = new ProjectCollection(); + Project project = new Project(XmlReader.Create(new StringReader(content)), null, null, pc, ProjectLoadSettings.RecordDuplicateButNotCircularImports); + IList imports = project.Imports; + IList importsIncludingDuplicates = project.ImportsIncludingDuplicates; + Assert.IsTrue(imports.Count == 3); + Assert.IsTrue(importsIncludingDuplicates.Count == 5); + Assert.IsTrue(!imports[0].IsImported); + Assert.IsTrue(imports[1].IsImported); + Assert.IsTrue(!imports[2].IsImported); + } + finally + { + File.Delete(importPath); + File.Delete(importPath2); + File.Delete(importPath3); + } + } + + /// + /// RecordDuplicateButNotCircularImports should not record circular imports (which do come under the category of "duplicate imports". + /// + [TestMethod] + public void RecordDuplicateButNotCircularImportsWithCircularImports() + { + string importPath1 = null; + string importPath2 = null; + + try + { + importPath1 = FileUtilities.GetTemporaryFile(); + importPath2 = FileUtilities.GetTemporaryFile(); + + // "import1" imports "import2" and vice versa. + string import1 = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + // "import1" imports "import2" and vice versa. + string import2 = ObjectModelHelpers.CleanupFileContents(@" + + +

Hello

+
+ +
+ "); + + File.WriteAllText(importPath1, import1); + File.WriteAllText(importPath2, import2); + + // The project file contents. + string manifest = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + ProjectCollection pc = new ProjectCollection(); + Project project = new Project(XmlReader.Create(new StringReader(manifest)), null, null, pc, ProjectLoadSettings.RecordDuplicateButNotCircularImports); + + // In the list returned by ImportsIncludingDuplicates, check if there are any imports that are imported by importPath2. + bool circularImportsAreRecorded = project.ImportsIncludingDuplicates.Any(resolvedImport => String.Equals(resolvedImport.ImportingElement.ContainingProject.FullPath, importPath2, StringComparison.OrdinalIgnoreCase)); + + // Even though, the text in importPath2 contains exactly one import, namely importPath1, it should not be recorded since + // importPath1 introduces a circular dependency when traversing depth-first from the project. + Assert.IsFalse(circularImportsAreRecorded); + } + finally + { + File.Delete(importPath1); + File.Delete(importPath2); + } + } + + /// + /// RecordDuplicateButNotCircularImports should not record circular imports (which do come under the category of "duplicate imports". + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RejectCircularImportsWithCircularImports() + { + string importPath1 = null; + string importPath2 = null; + + try + { + importPath1 = FileUtilities.GetTemporaryFile(); + importPath2 = FileUtilities.GetTemporaryFile(); + + // "import1" imports "import2" and vice versa. + string import1 = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + // "import1" imports "import2" and vice versa. + string import2 = ObjectModelHelpers.CleanupFileContents(@" + + +

Hello

+
+ +
+ "); + + File.WriteAllText(importPath1, import1); + File.WriteAllText(importPath2, import2); + + // The project file contents. + string manifest = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + ProjectCollection pc = new ProjectCollection(); + + // Because import1 and import2 import each other, there is a circular dependency. And since RejectCircularImports flag is set, the below call + // should throw. + Project project = new Project(XmlReader.Create(new StringReader(manifest)), null, null, pc, ProjectLoadSettings.RejectCircularImports); + } + finally + { + File.Delete(importPath1); + File.Delete(importPath2); + } + } + + /// + /// MSBuildDefaultTargets was not getting cleared out between reevaluations. + /// + [TestMethod] + public void MSBuildDefaultTargets() + { + Project project = new Project(); + project.Xml.DefaultTargets = "dt"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual("dt", project.GetPropertyValue("msbuildprojectdefaulttargets")); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual("dt", project.GetPropertyValue("msbuildprojectdefaulttargets")); + + project.MarkDirty(); + project.ReevaluateIfNecessary(); + + Assert.AreEqual("dt", project.GetPropertyValue("msbuildprojectdefaulttargets")); + + project.Xml.DefaultTargets = "dt2"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual("dt2", project.GetPropertyValue("msbuildprojectdefaulttargets")); + } + + /// + /// Something like a $ in an import's path should work. + /// + [TestMethod] + public void EscapableCharactersInImportPath() + { + string importPath1 = null; + string importPath2 = null; + string projectPath = null; + string directory = null; + string directory2 = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), "fol$der"); + directory2 = Path.Combine(Path.GetTempPath(), "fol$der\\fol$der2"); + Directory.CreateDirectory(directory2); + + string importPathRelativeEscaped = "fol$(x)$der2\\Escap%3beab$(x)leChar$ac;tersInI*tPa?h"; + string importRelative1 = "fol$der2\\Escap;eableChar$ac;tersInImportPath"; + string importRelative2 = "fol$der2\\Escap;eableChar$ac;tersInI_XXXX_tPath"; + importPath1 = Path.Combine(directory, importRelative1); + importPath2 = Path.Combine(directory, importRelative2); + + string import1 = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + + string import2 = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + + File.WriteAllText(importPath1, import1); + File.WriteAllText(importPath2, import2); + + projectPath = Path.Combine(directory, "my.proj"); // project path has $ in too + Project project = new Project(); + project.Save(projectPath); + project.Xml.AddImport(importPathRelativeEscaped); + Console.WriteLine(project.Xml.RawXml); + MockLogger logger = new MockLogger(); + bool result = project.Build("t2", new ILogger[] { logger }); + + Assert.AreEqual(true, result); + + logger.AssertLogContains("[imported1]"); + logger.AssertLogContains("[imported2]"); + } + finally + { + File.Delete(importPath1); + File.Delete(importPath2); + File.Delete(projectPath); + Directory.Delete(directory2); + Directory.Delete(directory); + } + } + + /// + /// There are several built-in MSBuildProjectXXXX properties like MSBuildProjectFile. + /// These always refer to the outer project whereever they are evaluated. + /// We also want MSBuildFileXXXX properties that are similar but have special behavior: + /// their values vary according to the file they are evaluated in. + /// + [TestMethod] + public void MSBuildThisFileProperties() + { + ProjectRootElement main = ProjectRootElement.Create(@"c:\a\p.proj"); + main.AddImport(@"c:\a\t1.targets"); + main.AddImport(@"c:\a\b\t2.targets"); + main.AddImport(@"c:\t3.targets"); + ProjectTargetElement target0 = main.AddTarget("t0"); + AddPropertyDumpTasks(@"c:\a\p.proj", target0); + main.InitialTargets = "t0"; + + ProjectRootElement import1 = ProjectRootElement.Create(@"c:\a\t1.targets"); + ProjectTargetElement target1 = import1.AddTarget("t1"); + AddPropertyDumpTasks(@"c:\a\t1.targets", target1); + import1.InitialTargets = "t1"; + + ProjectRootElement import2 = ProjectRootElement.Create(@"c:\a\b\t2.targets"); + ProjectTargetElement target2 = import2.AddTarget("t2"); + AddPropertyDumpTasks(@"c:\a\b\t2.targets", target2); + import2.InitialTargets = "t2"; + + ProjectRootElement import3 = ProjectRootElement.Create(@"c:\t3.targets"); + ProjectTargetElement target3 = import3.AddTarget("t3"); + AddPropertyDumpTasks(@"c:\t3.targets", target3); + import3.InitialTargets = "t3"; + + Project project = new Project(main); + MockLogger logger = new MockLogger(); + project.Build(logger); + + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + + // For comparison: + // + // Project "C:\a\p.proj" on node 1 (default targets). + // MSBuildProjectDirectory= C:\a + // MSBuildProjectDirectoryNoRoot= a + // MSBuildProjectFile= p.proj + // MSBuildProjectExtension= .proj + // MSBuildProjectFullPath= C:\a\p.proj + // MSBuildProjectName= p + // + // and at the root, c:\p.proj: + // + // MSBuildProjectDirectory= C:\ + // MSBuildProjectDirectoryNoRoot= + // MSBuildProjectFile= a.proj + // MSBuildProjectExtension= .proj + // MSBuildProjectFullPath= C:\a.proj + // MSBuildProjectName= a + logger.AssertLogContains(@"c:\a\p.proj: MSBuildThisFileDirectory=c:\a\"); + logger.AssertLogContains(@"c:\a\p.proj: MSBuildThisFileDirectoryNoRoot=a\"); + logger.AssertLogContains(@"c:\a\p.proj: MSBuildThisFile=p.proj"); + logger.AssertLogContains(@"c:\a\p.proj: MSBuildThisFileExtension=.proj"); + logger.AssertLogContains(@"c:\a\p.proj: MSBuildThisFileFullPath=c:\a\p.proj"); + logger.AssertLogContains(@"c:\a\p.proj: MSBuildThisFileName=p"); + + logger.AssertLogContains(@"c:\a\t1.targets: MSBuildThisFileDirectory=c:\a\"); + logger.AssertLogContains(@"c:\a\t1.targets: MSBuildThisFileDirectoryNoRoot=a\"); + logger.AssertLogContains(@"c:\a\t1.targets: MSBuildThisFile=t1.targets"); + logger.AssertLogContains(@"c:\a\t1.targets: MSBuildThisFileExtension=.targets"); + logger.AssertLogContains(@"c:\a\t1.targets: MSBuildThisFileFullPath=c:\a\t1.targets"); + logger.AssertLogContains(@"c:\a\t1.targets: MSBuildThisFileName=t1"); + + logger.AssertLogContains(@"c:\a\b\t2.targets: MSBuildThisFileDirectory=c:\a\b\"); + logger.AssertLogContains(@"c:\a\b\t2.targets: MSBuildThisFileDirectoryNoRoot=a\b\"); + logger.AssertLogContains(@"c:\a\b\t2.targets: MSBuildThisFile=t2.targets"); + logger.AssertLogContains(@"c:\a\b\t2.targets: MSBuildThisFileExtension=.targets"); + logger.AssertLogContains(@"c:\a\b\t2.targets: MSBuildThisFileFullPath=c:\a\b\t2.targets"); + logger.AssertLogContains(@"c:\a\b\t2.targets: MSBuildThisFileName=t2"); + + logger.AssertLogContains(@"c:\t3.targets: MSBuildThisFileDirectory=c:\"); + logger.AssertLogContains(@"c:\t3.targets: MSBuildThisFileDirectoryNoRoot="); + logger.AssertLogContains(@"c:\t3.targets: MSBuildThisFile=t3.targets"); + logger.AssertLogContains(@"c:\t3.targets: MSBuildThisFileExtension=.targets"); + logger.AssertLogContains(@"c:\t3.targets: MSBuildThisFileFullPath=c:\t3.targets"); + logger.AssertLogContains(@"c:\t3.targets: MSBuildThisFileName=t3"); + } + + /// + /// Per Orcas/Whidbey, if there are several task parameters that only differ + /// by case, we just silently take the last one. + /// + [TestMethod] + public void RepeatedTaskParameters() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectInstance instance = project.CreateProjectInstance(); + + Assert.AreEqual("3", (Helpers.GetFirst(instance.Targets["t"].Tasks)).GetParameter("Text")); + } + + /// + /// Simple override + /// + [TestMethod] + public void PropertyPredecessors() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + +

1

+

2

+

3

+

$(p);2

+
+
"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectProperty property = project.GetProperty("p"); + + ProjectPropertyElement xml1 = project.Xml.Properties.First(); + Assert.AreEqual("2;2", property.EvaluatedValue); + Assert.AreEqual("1", property.Predecessor.Predecessor.EvaluatedValue); + Assert.AreEqual(true, Object.ReferenceEquals(xml1, property.Predecessor.Predecessor.Xml)); + Assert.AreEqual(null, property.Predecessor.Predecessor.Predecessor); + } + + /// + /// Predecessors and imports + /// + [TestMethod] + [Ignore] + // Ignore: In-line Utilities function not found. + + public void PropertyPredecessorsAndImports() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + 1 + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + // Verify the predecessor is the one in the import document + ProjectRootElement importXml = ProjectRootElement.Open(project.Items.ElementAt(0).Xml.ContainingProject.FullPath); + ProjectRootElement predecessorXmlRoot = project.GetProperty("outdir").Predecessor.Xml.ContainingProject; + + Assert.AreEqual(true, Object.ReferenceEquals(importXml, predecessorXmlRoot)); + } + + /// + /// New properties get a null predecessor until reevaluation + /// + [TestMethod] + public void PropertyPredecessorsSetProperty() + { + // Need an existing property with the same name in an import + // so there's a potential predecessor but it's not just overwritten + string content = ObjectModelHelpers.CleanupFileContents(@" + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectProperty property = project.SetProperty("outdir", "x"); // Outdir is set in microsoft.common.targets + + Assert.AreEqual(null, property.Predecessor); + } + + /// + /// Predecessor of item definition is item definition + /// + [TestMethod] + public void ItemDefinitionPredecessorToItemDefinition() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + 1 + 2 + 3 + %(m);2 + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = project.ItemDefinitions["i"].GetMetadata("m"); + + Assert.AreEqual("2;2", metadatum.EvaluatedValue); + Assert.AreEqual("1", metadatum.Predecessor.Predecessor.EvaluatedValue); + + ProjectMetadataElement xml1 = project.Xml.ItemDefinitions.ElementAt(0).Metadata.ElementAt(0); + Assert.AreEqual(true, Object.ReferenceEquals(xml1, metadatum.Predecessor.Predecessor.Xml)); + Assert.AreEqual(null, metadatum.Predecessor.Predecessor.Predecessor); + } + + /// + /// Newly added item's metadata always has null predecessor until reevaluation + /// + [TestMethod] + public void NewItemPredecessor() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + m1 + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.AddItem("i", "i1")[0]; + ProjectMetadata metadatum = item.SetMetadataValue("m", "m2"); + + Assert.AreEqual(null, metadatum.Predecessor); + } + + /// + /// Predecessor of item is item definition + /// + [TestMethod] + public void ItemDefinitionPredecessorToItem() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + 1 + + + + + 2;%(m) + x + 3;%(m) + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = project.GetItems("i").ElementAt(0).GetMetadata("m"); + + Assert.AreEqual("3;2;1", metadatum.EvaluatedValue); + Assert.AreEqual("2;1", metadatum.Predecessor.EvaluatedValue); + Assert.AreEqual("1", metadatum.Predecessor.Predecessor.EvaluatedValue); + + ProjectMetadataElement xml1 = project.Xml.ItemDefinitions.ElementAt(0).Metadata.ElementAt(0); + Assert.AreEqual(true, Object.ReferenceEquals(xml1, metadatum.Predecessor.Predecessor.Xml)); + + ProjectMetadataElement xml2 = project.Xml.Items.ElementAt(0).Metadata.ElementAt(0); + Assert.AreEqual(true, Object.ReferenceEquals(xml2, metadatum.Predecessor.Xml)); + + Assert.AreEqual(null, metadatum.Predecessor.Predecessor.Predecessor); + } + + /// + /// Predecessor of item is on the same item. + /// + [TestMethod] + public void PredecessorOnSameItem() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + 0 + + + 1 + 2 + 3 + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = project.GetItems("i").ElementAt(1).GetMetadata("m"); + + Assert.AreEqual("3", metadatum.EvaluatedValue); + Assert.AreEqual("1", metadatum.Predecessor.EvaluatedValue); + + ProjectMetadataElement xml1 = project.Xml.Items.ElementAt(1).Metadata.ElementAt(0); + Assert.AreEqual(true, Object.ReferenceEquals(xml1, metadatum.Predecessor.Xml)); + + Assert.AreEqual(null, metadatum.Predecessor.Predecessor); + } + + /// + /// Predecessor of item is item + /// + [TestMethod] + public void ItemPredecessorToItem() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + 1 + + + 2;%(m) + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = project.GetItems("i").ElementAt(0).GetMetadata("m"); + + Assert.AreEqual("2;1", metadatum.EvaluatedValue); + Assert.AreEqual("1", metadatum.Predecessor.EvaluatedValue); + + ProjectMetadataElement xml1 = project.Xml.Items.ElementAt(0).Metadata.ElementAt(0); + Assert.AreEqual(true, Object.ReferenceEquals(xml1, metadatum.Predecessor.Xml)); + + Assert.AreEqual(null, metadatum.Predecessor.Predecessor); + } + + /// + /// Predecessor of item is item via transform + /// + [TestMethod] + public void ItemPredecessorToItemViaTransform() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + 1 + + '%(identity))""> + 2;%(m) + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = project.GetItems("i").ElementAt(0).GetMetadata("m"); + + Assert.AreEqual("2;", metadatum.EvaluatedValue); + Assert.AreEqual(null, metadatum.Predecessor); + } + + /// + /// Item predecessors and imports + /// + [TestMethod] + public void ItemPredecessorsAndImports() + { + string file = null; + + try + { + ProjectRootElement import = ProjectRootElement.Create(); + import.AddItemDefinition("i").AddMetadata("m", "%(m);m1"); + + file = FileUtilities.GetTemporaryFile(); + import.Save(file); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + m0 + + + + + + %(m);m2 + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata predecessor = project.GetItems("i").ElementAt(0).GetMetadata("m").Predecessor; + + Assert.AreEqual(true, Object.ReferenceEquals(import, predecessor.Xml.ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(project.Xml, predecessor.Predecessor.Xml.ContainingProject)); + } + finally + { + File.Delete(file); + } + } + + /// + /// Cases where there are no predecessors at all + /// + [TestMethod] + public void NoPredecessors() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + +

1

+
+ + + m1 + + + + + $(p) + + +
"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual(null, project.GetProperty("p").Predecessor); + Assert.AreEqual(null, project.ItemDefinitions["i"].GetMetadata("m").Predecessor); + Assert.AreEqual(null, project.GetItems("j").ElementAt(0).GetMetadata("m").Predecessor); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /// + /// Simple override + /// + [TestMethod] + public void AllEvaluatedProperties() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + +

1

+

2

+

3

+

$(p);2

+
+ +

3

+
+ + 4 + +
"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + IDictionary allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Get all those properties from project.AllEvaluatedProperties which don't have a backing xml. As project.AllEvaluatedProperties + // is an ordered collection and since such properties necessarily should occur before other properties, we don't need to scan + // the whole list. + // We have to dump it into a dictionary because AllEvaluatedProperties contains duplicates, but we're preparing to Properties, + // which doesn't, so we need to make sure that the final value in AllEvaluatedProperties is the one that matches. + foreach (ProjectProperty property in project.AllEvaluatedProperties.TakeWhile(property => property.Xml == null)) + { + allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates[property.Name] = property; + } + + // All those properties which aren't defined in any file. Examples are global properties, environment properties, etc. + IEnumerable nonImportedProperties = project.Properties.Where(property => property.Xml == null); + + Assert.AreEqual(allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates.Count, nonImportedProperties.Count()); + + // Now check and make sure they all match. If we get through the entire foreach without triggering an Assert.Fail(), then + // they do. + foreach (ProjectProperty property in nonImportedProperties) + { + ProjectProperty propertyFromAllEvaluated = null; + + if (!allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates.TryGetValue(property.Name, out propertyFromAllEvaluated)) + { + Assert.Fail(String.Format("project.Properties contained property {0}, but AllEvaluatedProperties did not.", property.Name)); + } + else if (!property.Equals(propertyFromAllEvaluated)) + { + Assert.Fail(String.Format("The properties in project.Properties and AllEvaluatedProperties for property {0} were different.", property.Name)); + } + } + + // These are the properties which are defined in some file. + IEnumerable restOfAllEvaluatedProperties = project.AllEvaluatedProperties.SkipWhile(property => property.Xml == null); + + Assert.AreEqual(4, restOfAllEvaluatedProperties.Count()); + Assert.AreEqual("1", restOfAllEvaluatedProperties.ElementAt(0).EvaluatedValue); + Assert.AreEqual("2", restOfAllEvaluatedProperties.ElementAt(1).EvaluatedValue); + Assert.AreEqual("2;2", restOfAllEvaluatedProperties.ElementAt(2).EvaluatedValue); + Assert.AreEqual("4", restOfAllEvaluatedProperties.ElementAt(3).EvaluatedValue); + + // Verify lists reset on reevaluation + project.MarkDirty(); + project.ReevaluateIfNecessary(); + + restOfAllEvaluatedProperties = project.AllEvaluatedProperties.SkipWhile(property => property.Xml == null); + Assert.AreEqual(4, restOfAllEvaluatedProperties.Count()); + } + + /// + /// All evaluated items + /// + [TestMethod] + public void AllEvaluatedItems() + { + string file = null; + + try + { + // Should include imported items + file = FileUtilities.GetTemporaryFile(); + ProjectRootElement import = ProjectRootElement.Create(file); + import.AddItem("i", "i10"); + import.Save(); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + m1 + + + + + + + + + + + + + + + m2 + + + + + + + + + + + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual(6, project.AllEvaluatedItems.Count()); + Assert.AreEqual("i1", project.AllEvaluatedItems.ElementAt(0).EvaluatedInclude); + Assert.AreEqual(String.Empty, project.AllEvaluatedItems.ElementAt(0).GetMetadataValue("m")); + Assert.AreEqual("j1", project.AllEvaluatedItems.ElementAt(1).EvaluatedInclude); + Assert.AreEqual("m1", project.AllEvaluatedItems.ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("i3", project.AllEvaluatedItems.ElementAt(2).EvaluatedInclude); + Assert.AreEqual("i1", project.AllEvaluatedItems.ElementAt(3).EvaluatedInclude); + Assert.AreEqual("m2", project.AllEvaluatedItems.ElementAt(3).GetMetadataValue("m")); + Assert.AreEqual("i5", project.AllEvaluatedItems.ElementAt(4).EvaluatedInclude); + Assert.AreEqual("i10", project.AllEvaluatedItems.ElementAt(5).EvaluatedInclude); + + // Adds aren't applied until reevaluation + project.AddItem("i", "i6"); + project.AddItem("i", "i7"); + project.RemoveItem(project.AllEvaluatedItems.ElementAt(1)); + + Assert.AreEqual(6, project.AllEvaluatedItems.Count()); + + project.MarkDirty(); + project.ReevaluateIfNecessary(); + + Assert.AreEqual(7, project.AllEvaluatedItems.Count()); + } + finally + { + File.Delete(file); + } + } + + /// + /// Evaluated properties list and imports + /// + [TestMethod] + public void AllEvaluatedPropertiesAndImports() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + ProjectRootElement import = ProjectRootElement.Create(file); + import.AddProperty("p", "0").Condition = "false"; + import.AddProperty("p", "1"); + import.AddProperty("q", "2"); + import.Save(); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + +

3

+
+
"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + IDictionary allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Get all those properties from project.AllEvaluatedProperties which don't have a backing xml. As project.AllEvaluatedProperties + // is an ordered collection and since such properties necessarily should occur before other properties, we don't need to scan + // the whole list. + // We have to dump it into a dictionary because AllEvaluatedProperties contains duplicates, but we're preparing to Properties, + // which doesn't, so we need to make sure that the final value in AllEvaluatedProperties is the one that matches. + foreach (ProjectProperty property in project.AllEvaluatedProperties.TakeWhile(property => property.Xml == null)) + { + allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates[property.Name] = property; + } + + // All those properties which aren't defined in any file. Examples are global properties, environment properties, etc. + IEnumerable nonImportedProperties = project.Properties.Where(property => property.Xml == null); + + Assert.AreEqual(allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates.Count, nonImportedProperties.Count()); + + // Now check and make sure they all match. If we get through the entire foreach without triggering an Assert.Fail(), then + // they do. + foreach (ProjectProperty property in nonImportedProperties) + { + ProjectProperty propertyFromAllEvaluated = null; + + if (!allEvaluatedPropertiesWithNoBackingXmlAndNoDuplicates.TryGetValue(property.Name, out propertyFromAllEvaluated)) + { + Assert.Fail(String.Format("project.Properties contained property {0}, but AllEvaluatedProperties did not.", property.Name)); + } + else if (!property.Equals(propertyFromAllEvaluated)) + { + Assert.Fail(String.Format("The properties in project.Properties and AllEvaluatedProperties for property {0} were different.", property.Name)); + } + } + + // These are the properties which are defined in some file. + IEnumerable restOfAllEvaluatedProperties = project.AllEvaluatedProperties.SkipWhile(property => property.Xml == null); + + Assert.AreEqual(3, restOfAllEvaluatedProperties.Count()); + Assert.AreEqual("1", restOfAllEvaluatedProperties.ElementAt(0).EvaluatedValue); + Assert.AreEqual("2", restOfAllEvaluatedProperties.ElementAt(1).EvaluatedValue); + Assert.AreEqual("3", restOfAllEvaluatedProperties.ElementAt(2).EvaluatedValue); + } + finally + { + File.Delete(file); + } + } + + /// + /// New properties do not appear in the evaluated properties list until reevaluation + /// + [TestMethod] + public void AllEvaluatedPropertiesSetProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + int initial = project.AllEvaluatedProperties.Count(); + + ProjectProperty property = project.SetProperty("p", "1"); + + Assert.AreEqual(initial, project.AllEvaluatedProperties.Count()); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual(initial + 1, project.AllEvaluatedProperties.Count()); + } + + /// + /// Two item definitions + /// + [TestMethod] + public void AllEvaluatedItemDefinitionMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + 1 + 2 + + + + + 1 + 3 + %(m);2 + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual(4, project.AllEvaluatedItemDefinitionMetadata.Count()); + + Assert.AreEqual("2", project.AllEvaluatedItemDefinitionMetadata.ElementAt(1).EvaluatedValue); + Assert.AreEqual("1;2", project.AllEvaluatedItemDefinitionMetadata.ElementAt(3).EvaluatedValue); + + // Verify lists are cleared on reevaluation + Assert.AreEqual(4, project.AllEvaluatedItemDefinitionMetadata.Count()); + } + + /// + /// Item's metadata does not appear in AllEvaluatedItemDefinitionMetadata + /// + [TestMethod] + public void AllEvaluatedItemDefinitionItem() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + m1 + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual(0, project.AllEvaluatedItemDefinitionMetadata.Count()); + } + + /// + /// Cases where there are no AllEvaluated* at all + /// + [TestMethod] + public void AllEvaluatedListsExceptPropertiesAreEmpty() + { + Project project = new Project(); + + // All those properties which aren't defined in any file. Examples are global properties, environment properties, etc. + IEnumerable nonImportedProperties = project.Properties.Where(property => property.Xml == null); + + // AllEvaluatedProperties intentionally includes duplicates; but if there are any among the non-imported properties, then + // our count won't match the above. + HashSet allProjectPropertiesNoDuplicateNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ProjectProperty property in project.AllEvaluatedProperties) + { + allProjectPropertiesNoDuplicateNames.Add(property.Name); + } + + Assert.AreEqual(nonImportedProperties.Count(), allProjectPropertiesNoDuplicateNames.Count); + Assert.AreEqual(0, project.AllEvaluatedItemDefinitionMetadata.Count()); + Assert.AreEqual(0, project.AllEvaluatedItems.Count()); + } + + /// + /// Test imports with wildcards and a relative path. + /// + [TestMethod] + public void ImportWildcardsRelative() + { + string directory = Path.Combine(Path.GetTempPath(), "ImportWildcardsRelative"); + string directory2 = Path.Combine(directory, "sub"); + Directory.CreateDirectory(directory2); + VerifyImportTargetRelativePath(directory, directory2, new string[] { @"**\*.targets" }); + } + + /// + /// Test imports with wildcards and a relative path. + /// + [TestMethod] + public void ImportWildcardsRelative2() + { + string directory = Path.Combine(Path.GetTempPath(), "ImportWildcardsRelative2"); + string directory2 = Path.Combine(directory, "sub"); + Directory.CreateDirectory(directory2); + VerifyImportTargetRelativePath(directory, directory2, new string[] { directory2 + "\\*.targets", directory + "\\*.targets" }); + } + + /// + /// Test imports with wildcards and a relative path. + /// + [TestMethod] + public void ImportWildcardsRelative3() + { + string directory = Path.Combine(Path.GetTempPath(), "ImportWildcardsRelative3"); + string directory2 = Path.Combine(directory, "sub"); + Directory.CreateDirectory(directory2); + VerifyImportTargetRelativePath(directory, directory2, new string[] { directory2 + "\\..\\*.targets", directory + "\\.\\sub\\*.targets" }); + } + + /// + /// Test imports with wildcards and a full path + /// + [TestMethod] + public void ImportWildcardsFullPath() + { + string directory = Path.Combine(Path.GetTempPath(), "ImportWildcardsFullPath"); + string directory2 = Path.Combine(directory, "sub"); + Directory.CreateDirectory(directory2); + + string file1 = Path.Combine(directory, "1.targets"); + string file2 = Path.Combine(directory2, "2.targets"); + string file3 = Path.Combine(directory2, "3.cpp.targets"); + + VerifyImportTargetRelativePath(directory, directory2, new string[] { file1, file2, file3 }); + } + + /// + /// Don't crash on a particular bad conditional. + /// + [TestMethod] + public void BadConditional() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + "); + + MockLogger mockLogger = new MockLogger(); + List loggerList = new List(); + loggerList.Add(mockLogger); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectInstance instance = project.CreateProjectInstance(); + instance.Build(loggerList); + + // Expect an error from the bad condition + Assert.IsTrue(mockLogger.ErrorCount == 1); + mockLogger.AssertLogContains("MSB4092"); + } + + /// + /// Default targets with empty entries doesn't break + /// + [TestMethod] + public void DefaultTargetsWithBlanks() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + "); + + MockLogger mockLogger = new MockLogger(); + List loggerList = new List(); + loggerList.Add(mockLogger); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectInstance instance = project.CreateProjectInstance(); + Assert.AreEqual(instance.DefaultTargets.Count, 2); + Assert.AreEqual(instance.DefaultTargets[0], "t"); + Assert.AreEqual(instance.DefaultTargets[1], "q"); + } + + /// + /// Initial targets with empty entries doesn't break + /// + [TestMethod] + public void InitialTargetsWithBlanks() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + MockLogger mockLogger = new MockLogger(); + List loggerList = new List(); + loggerList.Add(mockLogger); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectInstance instance = project.CreateProjectInstance(); + Assert.AreEqual(instance.InitialTargets.Count, 2); + Assert.AreEqual(instance.InitialTargets[0], "t"); + Assert.AreEqual(instance.InitialTargets[1], "q"); + } + + /// + /// Test that the default value for $(MSBuildExtensionsPath) points to "c:\program files\msbuild" in a 64-bit process + /// or on a 32-bit machine and "c:\program files (x86)\msbuild" in a 32-bit process on a 64-bit machine. + /// + [TestMethod] + public void MSBuildExtensionsPathDefault_Legacy() + { + string specialPropertyName = "MSBuildExtensionsPath"; + + // Save the old copy of the MSBuildExtensionsPath, so we can restore it when the unit test is done. + string backupMSBuildExtensionsPath = Environment.GetEnvironmentVariable(specialPropertyName); + string backupMagicSwitch = Environment.GetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH"); + string targetVar = Environment.GetEnvironmentVariable("Target"); + string numberVar = Environment.GetEnvironmentVariable("0env"); + string msbuildVar = Environment.GetEnvironmentVariable("msbuildtoolsversion"); + + try + { + // Set an environment variable called MSBuildExtensionsPath to some value, for the purpose + // of seeing whether our value wins. + Environment.SetEnvironmentVariable(specialPropertyName, null); + Environment.SetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH", "1"); + + // Need to create a new project collection object in order to pick up the new environment variables. + Project project = new Project(new ProjectCollection()); + + Assert.AreEqual(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + @"\MSBuild", project.GetPropertyValue(specialPropertyName)); + } + finally + { + // Restore the original value of the MSBuildExtensionsPath environment variable. + Environment.SetEnvironmentVariable(specialPropertyName, backupMSBuildExtensionsPath); + Environment.SetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH", backupMagicSwitch); + Environment.SetEnvironmentVariable("Target", targetVar); + Environment.SetEnvironmentVariable("0env", numberVar); + Environment.SetEnvironmentVariable("msbuildtoolsversion", msbuildVar); + } + } + + /// + /// Test that the default value for $(MSBuildExtensionsPath) points to the 32-bit Program Files always + /// (ie. it should have the same value as MSBuildExtensionsPath32). + /// + [TestMethod] + public void MSBuildExtensionsPathDefault() + { + string specialPropertyName = "MSBuildExtensionsPath"; + string specialPropertyName32 = "MSBuildExtensionsPath32"; + + // Save the old copy of the MSBuildExtensionsPath, so we can restore it when the unit test is done. + string backupMSBuildExtensionsPath = Environment.GetEnvironmentVariable(specialPropertyName); + string backupMSBuildExtensionsPath32 = Environment.GetEnvironmentVariable(specialPropertyName32); + string backupMagicSwitch = Environment.GetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH"); + string targetVar = Environment.GetEnvironmentVariable("Target"); + string numberVar = Environment.GetEnvironmentVariable("0env"); + string msbuildVar = Environment.GetEnvironmentVariable("msbuildtoolsversion"); + + try + { + // Set any pre-existing environment variables to null, just in case someone had set + // MSBuildExtensionsPath or MSBuildExtensionsPath32 explicitly in the environment. + Environment.SetEnvironmentVariable(specialPropertyName, null); + Environment.SetEnvironmentVariable(specialPropertyName32, null); + Environment.SetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH", null); + + // Need to create a new project collection object in order to pick up the new environment variables. + Project project = new Project(new ProjectCollection()); + + Assert.AreEqual(project.GetPropertyValue(specialPropertyName32), project.GetPropertyValue(specialPropertyName)); + } + finally + { + // Restore the original value of the MSBuildExtensionsPath environment variable. + Environment.SetEnvironmentVariable(specialPropertyName, backupMSBuildExtensionsPath); + Environment.SetEnvironmentVariable(specialPropertyName32, backupMSBuildExtensionsPath32); + Environment.SetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH", backupMagicSwitch); + Environment.SetEnvironmentVariable("Target", targetVar); + Environment.SetEnvironmentVariable("0env", numberVar); + Environment.SetEnvironmentVariable("msbuildtoolsversion", msbuildVar); + } + } + + /// + /// Test that if I set an environment variable called "MSBuildExtensionPath", that my env var + /// should win over whatever MSBuild thinks the default is. + /// + [TestMethod] + public void MSBuildExtensionsPathWithEnvironmentOverride() + { + // Save the old copy of the MSBuildExtensionsPath, so we can restore it when the unit test is done. + string backupMSBuildExtensionsPath = Environment.GetEnvironmentVariable("MSBuildExtensionsPath"); + + try + { + // Set an environment variable called MSBuildExtensionsPath to some value, for the purpose + // of seeing whether our value wins. + Environment.SetEnvironmentVariable("MSBuildExtensionsPath", @"c:\foo\bar"); + + // Need to create a new project collection object in order to pick up the new environment variables. + Project project = new Project(new ProjectCollection()); + + Assert.AreEqual(@"c:\foo\bar", project.GetPropertyValue("MSBuildExtensionsPath")); + } + finally + { + Environment.SetEnvironmentVariable("MSBuildExtensionsPath", backupMSBuildExtensionsPath); + } + } + + /// + /// Test that if I set a global property called "MSBuildExtensionPath", that my global property + /// should win over whatever MSBuild thinks the default is. + /// + [TestMethod] + public void MSBuildExtensionsPathWithGlobalOverride() + { + Project project = new Project(new ProjectCollection()); + + // Set a global property called MSBuildExtensionsPath to some value, for the purpose + // of seeing whether our value wins. + project.SetGlobalProperty("MSBuildExtensionsPath", @"c:\devdiv\vscore\msbuild"); + project.ReevaluateIfNecessary(); + + Assert.AreEqual(@"c:\devdiv\vscore\msbuild", project.GetPropertyValue("MSBuildExtensionsPath")); + } + + /// + /// The default value for $(MSBuildExtensionsPath32) should point to "c:\program files (x86)\msbuild" on a 64 bit machine. + /// We can't test that unless we are on a 64 bit box, but this test will work on either + /// + [TestMethod] + public void MSBuildExtensionsPath32Default() + { + // On a 64 bit machine we always want to use the program files x86. If we are running as a 64 bit process then this variable will be set correctly + // If we are on a 32 bit machine or running as a 32 bit process then this variable will be null and the programFiles variable will be correct. + string expected = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); + if (String.IsNullOrEmpty(expected)) + { + // 32 bit box + expected = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + } + + string extensionsPath32Env = Environment.GetEnvironmentVariable("MSBuildExtensionsPath32"); + + try + { + Environment.SetEnvironmentVariable("MSBuildExtensionsPath32", null); + Project project = new Project(new ProjectCollection()); + + Assert.AreEqual(expected + @"\MSBuild", project.GetPropertyValue("MSBuildExtensionsPath32")); + } + finally + { + Environment.SetEnvironmentVariable("MSBuildExtensionsPath32", extensionsPath32Env); + } + } + + /// + /// Set an env var called MSBuildExtensionsPath32 to some value, for the purpose + /// of seeing whether our value wins. + /// + [TestMethod] + public void MSBuildExtensionsPath32WithEnvironmentOverride() + { + string originalMSBuildExtensionsPath32Value = Environment.GetEnvironmentVariable("MSBuildExtensionsPath32"); + + try + { + Environment.SetEnvironmentVariable("MSBuildExtensionsPath32", @"c:\devdiv\vscore\msbuild"); + Project project = new Project(new ProjectCollection()); + string msbuildExtensionsPath32Value = project.GetPropertyValue("MSBuildExtensionsPath32"); + Assert.AreEqual(@"c:\devdiv\vscore\msbuild", msbuildExtensionsPath32Value); + } + finally + { + // And restore the old value + Environment.SetEnvironmentVariable("MSBuildExtensionsPath32", originalMSBuildExtensionsPath32Value); + } + } + + /// + /// Set a global property called MSBuildExtensionsPath32 to some value, for the purpose + /// of seeing whether our value wins. + /// + [TestMethod] + public void MSBuildExtensionsPath32WithGlobalOverride() + { + Project project = new Project(new ProjectCollection()); + + project.SetGlobalProperty("MSBuildExtensionsPath32", @"c:\devdiv\vscore\msbuild"); + string msbuildExtensionsPath32Value = project.GetPropertyValue("MSBuildExtensionsPath32"); + Assert.AreEqual(@"c:\devdiv\vscore\msbuild", msbuildExtensionsPath32Value); + } + + /// + /// The default value for $(MSBuildExtensionsPath64) should point to "c:\program files\msbuild" on a 64 bit machine, + /// and should be empty on a 32-bit machine. + /// We can't test that unless we are on a 64 bit box, but this test will work on either + /// + [TestMethod] + public void MSBuildExtensionsPath64Default() + { + string expected = String.Empty; + + // If we are on a 32 bit machine then this variable will be null. + string programFiles32 = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); + if (!String.IsNullOrEmpty(programFiles32)) + { + // only set in 32-bit windows on 64-bit machines + expected = Environment.GetEnvironmentVariable("ProgramW6432"); + + if (String.IsNullOrEmpty(expected)) + { + // 64-bit window on a 64-bit machine -- ProgramFiles is correct + expected = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + } + } + + if (!String.IsNullOrEmpty(expected)) + { + expected = expected + @"\MSBuild"; + } + + Project project = new Project(); + + Assert.AreEqual(expected, project.GetPropertyValue("MSBuildExtensionsPath64")); + } + + /// + /// Set an env var called MSBuildExtensionsPath64 to some value, for the purpose + /// of seeing whether our value wins. + /// + [TestMethod] + public void MSBuildExtensionsPath64WithEnvironmentOverride() + { + string originalMSBuildExtensionsPath64Value = Environment.GetEnvironmentVariable("MSBuildExtensionsPath64"); + + try + { + Environment.SetEnvironmentVariable("MSBuildExtensionsPath64", @"c:\devdiv\vscore\msbuild"); + Project project = new Project(new ProjectCollection()); + string msbuildExtensionsPath64Value = project.GetPropertyValue("MSBuildExtensionsPath64"); + Assert.AreEqual(@"c:\devdiv\vscore\msbuild", msbuildExtensionsPath64Value); + } + finally + { + // And restore the old value + Environment.SetEnvironmentVariable("MSBuildExtensionsPath64", originalMSBuildExtensionsPath64Value); + } + } + + /// + /// Set a global property called MSBuildExtensionsPath64 to some value, for the purpose + /// of seeing whether our value wins. + /// + [TestMethod] + public void MSBuildExtensionsPath64WithGlobalOverride() + { + Project project = new Project(new ProjectCollection()); + + project.SetGlobalProperty("MSBuildExtensionsPath64", @"c:\devdiv\vscore\msbuild"); + string msbuildExtensionsPath64Value = project.GetPropertyValue("MSBuildExtensionsPath64"); + Assert.AreEqual(@"c:\devdiv\vscore\msbuild", msbuildExtensionsPath64Value); + } + + /// + /// Verify whether LocalAppData property is set by default in msbuild + /// with the path of the OS special LocalApplicationData or ApplicationData folders. + /// + [TestMethod] + public void LocalAppDataDefault() + { + string expected = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (String.IsNullOrEmpty(expected)) + { + expected = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + + Project project = new Project(); + + Assert.AreEqual(expected, project.GetPropertyValue("LocalAppData")); + } + + /// + /// Set an env var called LocalAppData to some value, for the purpose + /// of seeing whether our value wins. + /// + [TestMethod] + public void LocalAppDataWithEnvironmentOverride() + { + string originalLocalAppDataValue = Environment.GetEnvironmentVariable("LocalAppData"); + + try + { + Environment.SetEnvironmentVariable("LocalAppData", @"c:\AppData\Local"); + Project project = new Project(new ProjectCollection()); + string localAppDataValue = project.GetPropertyValue("LocalAppData"); + Assert.AreEqual(@"c:\AppData\Local", localAppDataValue); + } + finally + { + // And restore the old value + Environment.SetEnvironmentVariable("LocalAppData", originalLocalAppDataValue); + } + } + + /// + /// Set a global property called LocalAppData to some value, for the purpose + /// of seeing whether our value wins. + /// + [TestMethod] + public void LocalAppDataWithGlobalOverride() + { + Project project = new Project(new ProjectCollection()); + + project.SetGlobalProperty("LocalAppData", @"c:\AppData\Local"); + string localAppDataValue = project.GetPropertyValue("LocalAppData"); + Assert.AreEqual(@"c:\AppData\Local", localAppDataValue); + } + + /// + /// Test standard reserved properties + /// + [TestMethod] + public void ReservedProjectProperties() + { + string file = @"c:\foo\bar.csproj"; + ProjectRootElement xml = ProjectRootElement.Create(file); + xml.DefaultTargets = "Build"; + Project project = new Project(xml); + + Assert.AreEqual(@"c:\foo", project.GetPropertyValue("MSBuildProjectDirectory")); + Assert.AreEqual(@"foo", project.GetPropertyValue("MSBuildProjectDirectoryNoRoot")); + Assert.AreEqual("bar.csproj", project.GetPropertyValue("MSBuildProjectFile")); + Assert.AreEqual(".csproj", project.GetPropertyValue("MSBuildProjectExtension")); + Assert.AreEqual(@"c:\foo\bar.csproj", project.GetPropertyValue("MSBuildProjectFullPath")); + Assert.AreEqual("bar", project.GetPropertyValue("MSBuildProjectName")); + } + + /// + /// Test standard reserved properties + /// + [TestMethod] + public void ReservedProjectPropertiesAtRoot() + { + string file = @"c:\bar.csproj"; + ProjectRootElement xml = ProjectRootElement.Create(file); + Project project = new Project(xml); + + Assert.AreEqual(@"c:\", project.GetPropertyValue("MSBuildProjectDirectory")); + Assert.AreEqual(String.Empty, project.GetPropertyValue("MSBuildProjectDirectoryNoRoot")); + Assert.AreEqual("bar.csproj", project.GetPropertyValue("MSBuildProjectFile")); + Assert.AreEqual(".csproj", project.GetPropertyValue("MSBuildProjectExtension")); + Assert.AreEqual(@"c:\bar.csproj", project.GetPropertyValue("MSBuildProjectFullPath")); + Assert.AreEqual("bar", project.GetPropertyValue("MSBuildProjectName")); + } + + /// + /// Test standard reserved properties on UNC at root + /// + [TestMethod] + public void ReservedProjectPropertiesOnUNCRoot() + { + string uncFile = @"\\foo\bar\baz.csproj"; + ProjectRootElement xml = ProjectRootElement.Create(uncFile); + Project project = new Project(xml); + + Assert.AreEqual(@"\\foo\bar", project.GetPropertyValue("MSBuildProjectDirectory")); + Assert.AreEqual(String.Empty, project.GetPropertyValue("MSBuildProjectDirectoryNoRoot")); + Assert.AreEqual("baz.csproj", project.GetPropertyValue("MSBuildProjectFile")); + Assert.AreEqual(".csproj", project.GetPropertyValue("MSBuildProjectExtension")); + Assert.AreEqual(@"\\foo\bar\baz.csproj", project.GetPropertyValue("MSBuildProjectFullPath")); + Assert.AreEqual("baz", project.GetPropertyValue("MSBuildProjectName")); + } + + /// + /// Test standard reserved properties on UNC + /// + [TestMethod] + public void ReservedProjectPropertiesOnUNC() + { + string uncFile = @"\\foo\bar\baz\biz.csproj"; + ProjectRootElement xml = ProjectRootElement.Create(uncFile); + Project project = new Project(xml); + + Assert.AreEqual(@"\\foo\bar\baz", project.GetPropertyValue("MSBuildProjectDirectory")); + Assert.AreEqual(@"baz", project.GetPropertyValue("MSBuildProjectDirectoryNoRoot")); + Assert.AreEqual("biz.csproj", project.GetPropertyValue("MSBuildProjectFile")); + Assert.AreEqual(".csproj", project.GetPropertyValue("MSBuildProjectExtension")); + Assert.AreEqual(@"\\foo\bar\baz\biz.csproj", project.GetPropertyValue("MSBuildProjectFullPath")); + Assert.AreEqual("biz", project.GetPropertyValue("MSBuildProjectName")); + } + + /// + /// Verify when a node count is passed through on the project collection that the correct number is used to evaluate the msbuildNodeCount + /// + [TestMethod] + public void VerifyMsBuildNodeCountReservedProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(MSBuildNodeCount) + + + + + + "); + + // Setup a project collection which asks for 4 nodes + ProjectCollection collection = new ProjectCollection + ( + ProjectCollection.GlobalProjectCollection.GlobalProperties, + ProjectCollection.GlobalProjectCollection.Loggers, + null, + ProjectCollection.GlobalProjectCollection.ToolsetLocations, + 4, + false + ); + + Project project = new Project(XmlReader.Create(new StringReader(content)), new Dictionary(), "4.0", collection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(String.Format("[{0}]", 4)); + } + + /// + /// Verify when no node count is passed that we evaluate MsBuildNodeCount to 1 + /// + [TestMethod] + public void VerifyMsBuildNodeCountReservedPropertyDefault() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(MSBuildNodeCount) + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(String.Format("[{0}]", 1)); + } + + /// + /// Verify that the programfiles32 property points to the correct location + /// + [TestMethod] + public void VerifyMsbuildProgramFiles32ReservedProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(MsBuildProgramFiles32) + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(content))); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(String.Format("[{0}]", FrameworkLocationHelper.programFiles32)); + } + + /// + /// Basic verification -- adding the tag to the ProjectRootElement on its own does nothing. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertyTagDoesNothingIfNoGlobalProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Bar + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(content))); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[Bar]"); + } + + /// + /// Basic verification -- with no TreatAsLocalProperty, but with a global property specified, the global property + /// overrides the local property. + /// + [TestMethod] + public void VerifyGlobalPropertyOverridesIfNoTreatAsLocalProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Bar + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[Baz]"); + } + + /// + /// Basic verification -- with TreatAsLocalProperty, and with a global property specified, the local property + /// overrides the global property. + /// + [TestMethod] + public void VerifyLocalPropertyOverridesIfTreatAsLocalPropertySet() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Bar + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[Bar]"); + } + + /// + /// Basic verification -- with TreatAsLocalProperty set, but to a different property than is being passed as a global, the + /// global property overrides the local property. + /// + [TestMethod] + public void VerifyGlobalPropertyOverridesNonSpecifiedLocalProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Bar + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[Baz]"); + } + + /// + /// Basic verification -- with TreatAsLocalProperty set, but to a different property than is being passed as a global, the + /// global property overrides the local property. + /// + [TestMethod] + public void VerifyLocalPropertyInheritsFromOverriddenGlobalProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Bar + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[BazBar]"); + } + + /// + /// Basic verification -- with TreatAsLocalProperty set, but to a different property than is being passed as a global, the + /// global property overrides the local property. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertySpecificationWorksIfSpecificationIsItselfAProperty() + { + string oldEnvironmentValue = Environment.GetEnvironmentVariable("EnvironmentProperty"); + + try + { + Environment.SetEnvironmentVariable("EnvironmentProperty", "Bar;Baz"); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo2 + $(Bar)Bar2 + $(Baz)Baz2 + + + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Foo1"); + globalProperties.Add("Bar", "Bar1"); + globalProperties.Add("Baz", "Baz1"); + globalProperties.Add("GlobalProperty", "Foo"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null, new ProjectCollection()); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[Foo1Foo2]."); + logger.AssertLogContains(".[[Bar1Bar2]]."); + logger.AssertLogContains(".[[[Baz1Baz2]]]."); + } + finally + { + Environment.SetEnvironmentVariable("EnvironmentProperty", oldEnvironmentValue); + } + } + + /// + /// Basic verification -- setting an invalid TreatAsLocalProperty should be an evaluation error. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void VerifyInvalidTreatAsLocalProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Bar + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + + // Should not reach this point. + Assert.Fail(); + } + + /// + /// Basic verification -- whitespace in the TreatAsLocalProperty definition should be trimmed. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertyTrimmed() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo + $(Goo)Goo + + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + globalProperties.Add("Goo", "Foo"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[BazFoo]."); + logger.AssertLogContains(".[[FooGoo]]."); + } + + /// + /// Basic verification -- if there are empty entries in the split of the properties for TreatAsLocalProperty, + /// they should be ignored. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertyEmptySplits() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Goo + $(Goo)Goo + + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + globalProperties.Add("Goo", "Foo"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[BazGoo]."); + logger.AssertLogContains(".[[FooGoo]]."); + } + + /// + /// Basic verification -- if looking at the project in the OM, verify that while looking at the property + /// value returns the mutable version, looking explicitly at the global properties dictionary still returns + /// the original global property value. + /// + [TestMethod] + public void VerifyGlobalPropertyRetainsOriginalValue() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Bar + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + Assert.AreEqual("BazBar", project.GetPropertyValue("Foo")); + Assert.AreEqual("Baz", project.GlobalProperties["Foo"]); + } + + /// + /// Basic verification -- if TreatAsLocalProperty is modified on the project XML and then the project is + /// re-evaluated, it should be re-evaluated in the context of that modified value. + /// + [TestMethod] + public void VerifyModificationsToTreatAsLocalPropertyRespected() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Goo + $(Goo)Goo + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + globalProperties.Add("Goo", "Foo"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + Assert.AreEqual("Foo;Goo", project.Xml.TreatAsLocalProperty); + + Assert.AreEqual("BazGoo", project.GetPropertyValue("Foo")); + Assert.AreEqual("Baz", project.GlobalProperties["Foo"]); + Assert.AreEqual("FooGoo", project.GetPropertyValue("Goo")); + Assert.AreEqual("Foo", project.GlobalProperties["Goo"]); + + project.Xml.TreatAsLocalProperty = "Foo"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual("BazGoo", project.GetPropertyValue("Foo")); + Assert.AreEqual("Baz", project.GlobalProperties["Foo"]); + Assert.AreEqual("Foo", project.GetPropertyValue("Goo")); + Assert.AreEqual("Foo", project.GlobalProperties["Goo"]); + } + + /// + /// Basic verification -- if TreatAsLocalProperty is modified on the project XML and then the project is + /// re-evaluated, it should be re-evaluated in the context of that modified value. + /// + [TestMethod] + public void VerifyModificationsToGlobalPropertiesRespected() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Bar + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("Bar", project.GetPropertyValue("Foo")); + Assert.IsFalse(project.GlobalProperties.ContainsKey("Foo")); + + project.SetGlobalProperty("Foo", "Baz"); + project.ReevaluateIfNecessary(); + + Assert.AreEqual("BazBar", project.GetPropertyValue("Foo")); + Assert.AreEqual("Baz", project.GlobalProperties["Foo"]); + + project.RemoveGlobalProperty("Foo"); + project.ReevaluateIfNecessary(); + + Assert.AreEqual("Bar", project.GetPropertyValue("Foo")); + Assert.IsFalse(project.GlobalProperties.ContainsKey("Foo")); + } + + /// + /// Basic verification -- with TreatAsLocalProperty set to multiple global properties, and with multiple global properties + /// passed in, only the ones that are marked TALP are overridable. + /// + [TestMethod] + public void VerifyOnlySpecifiedPropertiesOverridden() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Foo2 + Bar2 + Baz2 + + + + + + + + "); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Foo1"); + globalProperties.Add("Bar", "Bar1"); + globalProperties.Add("Baz", "Baz1"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[Foo2]."); + logger.AssertLogContains(".[[Bar2]]."); + logger.AssertLogContains(".[[[Baz1]]]."); + } + + /// + /// If TreatAsLocalProperty is set in a parent project, that property is still treated as overridable + /// when defined in an imported project. + /// + [TestMethod] + public void VerifyPropertySetInImportStillOverrides() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + "); + + string importContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Bar + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyPropertySetInImportStillOverrides"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string import = Path.Combine(projectDirectory, "import.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(import, importContents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[BazBar]"); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// If TreatAsLocalProperty is set in an imported project, any instances of that property in the parent + /// project before the import are ignored and the global property value is used instead. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertyInImportDoesntAffectParentProjectAboveIt() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + Bar + + + + + + + + "); + + string importContents = ObjectModelHelpers.CleanupFileContents(@" + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyTreatAsLocalPropertyInImportDoesntAffectParentProjectAboveIt"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string import = Path.Combine(projectDirectory, "import.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(import, importContents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[Baz]"); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// If TreatAsLocalProperty is set in an imported project, any instances of that property in the parent + /// project after the import recognize the TreatAsLocalProperty flag and override the global property value. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertyInImportAffectsParentProjectBelowIt() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + + Bar + + + + + + "); + + string importContents = ObjectModelHelpers.CleanupFileContents(@" + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyTreatAsLocalPropertyInImportAffectsParentProjectBelowIt"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string import = Path.Combine(projectDirectory, "import.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(import, importContents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Baz"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains("[Bar]"); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// If TreatAsLocalProperty is set in both the parent and imported project, the end result + /// set of overridable properties is the union of the two sets, though of course you cannot + /// override a property until you reach the import that mentions it in its TreatAsLocalProperty + /// parameter. + /// + [TestMethod] + public void VerifyTreatAsLocalPropertyUnionBetweenImports() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo1 + $(Bar)Bar1 + $(Baz)Baz1 + + + + + + + + + + "); + + string importContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo2 + $(Bar)Bar2 + $(Baz)Baz2 + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyTreatAsLocalPropertyUnionBetweenImports"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string import = Path.Combine(projectDirectory, "import.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(import, importContents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Foo3"); + globalProperties.Add("Bar", "Bar3"); + globalProperties.Add("Baz", "Baz3"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[Foo3Foo1Foo2]."); + logger.AssertLogContains(".[[Bar3Bar2]]."); + logger.AssertLogContains(".[[[Baz3Baz2]]]."); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// If a property is set to TreatAsLocalProperty in both the parent project and the import, this is + /// silently acknowledged. + /// + [TestMethod] + public void VerifyDuplicateTreatAsLocalProperty() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo1 + $(Bar)Bar1 + $(Baz)Baz1 + + + + + + + + + "); + + string importContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo2 + $(Bar)Bar2 + $(Baz)Baz2 + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyDuplicateTreatAsLocalProperty"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string import = Path.Combine(projectDirectory, "import.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(import, importContents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Foo3"); + globalProperties.Add("Bar", "Bar3"); + globalProperties.Add("Baz", "Baz3"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[Foo3Foo1Foo2]."); + logger.AssertLogContains(".[[Bar3Bar2]]."); + logger.AssertLogContains(".[[[Baz3Baz2]]]."); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// If TreatAsLocalProperty is set in a parent project, a project that is P2P'ed to will + /// still receive the original value of the global property. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void VerifyGlobalPropertyPassedToP2P() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo2 + + + + + + "); + + string project2Contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyGlobalPropertyPassedToP2P"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string project2 = Path.Combine(projectDirectory, "project2.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(project2, project2Contents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Foo1"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[Foo1]."); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// If TreatAsLocalProperty is set in a parent project, a project that is P2P'ed who is explicitly + /// passed the property, will get the mutable local value rather than the original value of the + /// global property. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void VerifyLocalPropertyPropagatesIfExplicitlyPassedToP2P() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + $(Foo)Foo2 + + + + + + "); + + string project2Contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyLocalPropertyPropagatesIfExplicitlyPassedToP2P"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string primaryProject = Path.Combine(projectDirectory, "project.proj"); + string project2 = Path.Combine(projectDirectory, "project2.proj"); + + File.WriteAllText(primaryProject, projectContents); + File.WriteAllText(project2, project2Contents); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Foo", "Foo1"); + + Project project = new Project(primaryProject, globalProperties, null); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + logger.AssertLogContains(".[Foo1Foo2]."); + } + finally + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + } + } + + /// + /// Verify that when we don't specify the sub-toolset version, we get the correct sub-toolset properties + /// based on the default sub-toolset version -- base toolset if Dev10 is installed, or lowest (numerically + /// sorted) toolset if it's not. + /// + [TestMethod] + public void VerifyDefaultSubToolsetPropertiesAreEvaluated() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no global properties */); + Project project = new Project(XmlReader.Create(new StringReader(content)), null, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + if (Toolset.Dev10IsInstalled) + { + // if Dev10 is installed, the default sub-toolset is nothing == base toolset. + logger.AssertLogContains(".[a1]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[]]]."); + logger.AssertLogContains(".[[[[10.0]]]]."); + } + else + { + // if Dev10 is not installed, the default sub-toolset is the numerical least -- in our case, "11.0" -- + // so the toolset properties are a combination of that + the base toolset. + logger.AssertLogContains(".[a1]."); + logger.AssertLogContains(".[[b2]]."); + logger.AssertLogContains(".[[[c2]]]."); + logger.AssertLogContains(".[[[[11.0]]]]."); + } + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a1||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that when we specify an invalid sub-toolset version, we just get the properties from the base + /// toolset ... but that invalid version is still reflected as a project property. + /// + [TestMethod] + public void VerifyNoSubToolsetPropertiesAreEvaluatedWithInvalidSubToolset() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "ABCDE"); + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no global properties */); + Project project = new Project(XmlReader.Create(new StringReader(content)), null, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a1]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[]]]."); + logger.AssertLogContains(".[[[[ABCDE]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a1||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that if a sub-toolset is explicitly specified, its properties are evaluated into the project properly. + /// + [TestMethod] + public void VerifyExplicitSubToolsetPropertiesAreEvaluated() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "FakeSubToolset"); + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no global properties */); + Project project = new Project(XmlReader.Create(new StringReader(content)), null, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a3]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[c3]]]."); + logger.AssertLogContains(".[[[[FakeSubToolset]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a3||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that if a non-existent sub-toolset is specified, we simply ignore it and just use the base toolset properties. + /// + [TestMethod] + public void VerifyExplicitNonExistentSubToolsetPropertiesAreEvaluated() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "abcdef"); + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no global properties */); + Project project = new Project(XmlReader.Create(new StringReader(content)), null, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a1]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[]]]."); + logger.AssertLogContains(".[[[[abcdef]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a1||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that if there is a conflict between sub-toolset and environment properties, the sub-toolset properties win. + /// + [TestMethod] + public void VerifySubToolsetPropertiesOverrideEnvironment() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + string originalC = Environment.GetEnvironmentVariable("C"); + string originalD = Environment.GetEnvironmentVariable("D"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "FakeSubToolset"); + Environment.SetEnvironmentVariable("C", "c4"); // not explosive :) + Environment.SetEnvironmentVariable("D", "d4"); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no global properties */); + Project project = new Project(XmlReader.Create(new StringReader(content)), null, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a3]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[c3]]]."); + logger.AssertLogContains(".[[[[d4]]]]."); + logger.AssertLogContains(".[[[[[FakeSubToolset]]]]]"); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a3||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + Environment.SetEnvironmentVariable("C", originalC); + Environment.SetEnvironmentVariable("D", originalD); + } + } + + /// + /// Verify that if there is a conflict between sub-toolset and global properties, the global properties win. + /// + [TestMethod] + public void VerifyGlobalPropertiesOverrideSubToolset() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "FakeSubToolset"); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no project collection global properties */); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("c", "c5"); + globalProperties.Add("d", "d5"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a3]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[c5]]]."); + logger.AssertLogContains(".[[[[d5]]]]."); + logger.AssertLogContains(".[[[[[FakeSubToolset]]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a3||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that even if the sub-toolset was set by a global property, it can be overridden from within the project + /// + [TestMethod] + public void VerifySubToolsetVersionSetByGlobalPropertyStillOverridable() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVerson"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "FakeSubToolset"); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no project collection global properties */); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "11.0"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, "Fake", fakeProjectCollection); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a1]."); + logger.AssertLogContains(".[[b2]]."); + logger.AssertLogContains(".[[[c2]]]."); + logger.AssertLogContains(".[[[[11.0]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a1||"); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that if the sub-toolset was set by a global property, it cannot be overridden from within the project + /// + [TestMethod] + public void VerifySubToolsetVersionSetByConstructorOverridable_OverridesGlobalProperty() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no project collection global properties */); + + IDictionary globalProperties = new Dictionary(); + globalProperties.Add("VisualStudioVersion", "11.0"); + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, "Fake", "FakeSubToolset", fakeProjectCollection, ProjectLoadSettings.Default); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a3]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[c3]]]."); + logger.AssertLogContains(".[[[[FakeSubToolset]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a3||"); + } + + /// + /// Verify that if the sub-toolset was set by a global property, it cannot be overridden from within the project + /// + [TestMethod] + public void VerifySubToolsetVersionSetByConstructorOverridable() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + changed + + + + + + "); + + ProjectCollection fakeProjectCollection = GetProjectCollectionWithFakeToolset(null /* no project collection global properties */); + + Project project = new Project(XmlReader.Create(new StringReader(content)), null, "Fake", "FakeSubToolset", fakeProjectCollection, ProjectLoadSettings.Default); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + Assert.AreEqual(true, result); + + logger.AssertLogContains(".[a3]."); + logger.AssertLogContains(".[[b1]]."); + logger.AssertLogContains(".[[[c3]]]."); + logger.AssertLogContains(".[[[[FakeSubToolset]]]]."); + + // whatever the initial value of VisualStudioVersion, we should be able to change it, but it doesn't affect + // the value of any of the sub-toolset properties. + logger.AssertLogContains("|changed|"); + logger.AssertLogContains("||a3||"); + } + + /// + /// Verify that DTD processing is disabled when loading a project + /// We add some invalid DTD code to a MSBuild project, if such code is ever parsed a XmlException will be thrown + /// If DTD parsing is desabled (desired behavior), no XmlException should be caught + /// + [TestMethod] + public void VerifyDTDProcessingIsDisabled() + { + string projectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + ]> + + + + + "); + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyDTDProcessingIsDisabled"); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string projectFilename = Path.Combine(projectDirectory, "project.proj"); + + File.WriteAllText(projectFilename, projectContents); + + Project project = new Project(projectFilename); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + } + catch (XmlException) + { + // XmlException thrown when invalid DTD statement is parsed: it means DTD processing was enabled + Assert.IsTrue(false); + } + } + + /// + /// Verify that DTD processing is disabled when loading a project + /// We create an HTTP server that waits for a request and load a project containing DTD code making reference to a ficticious file in the server. + /// This test emulates a scenario where some malicious DTD code could upload user data to a malicious website + /// If DTD processing is disabled, the server should not receive any connection request. + /// + [TestMethod] + public void VerifyDTDProcessingIsDisabled2() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + %external; + %param1; + %test; + ]> + + + + + "); + + string projectDirectory = Path.Combine(Path.GetTempPath(), "VerifyDTDProcessingIsDisabled"); + + Thread t = new Thread(HttpServerThread); + t.IsBackground = true; + t.Start(); + + try + { + if (Directory.Exists(projectDirectory)) + { + Directory.Delete(projectDirectory, true /* recursive delete */); + } + + Directory.CreateDirectory(projectDirectory); + + string projectFilename = Path.Combine(projectDirectory, "project.proj"); + + File.WriteAllText(projectFilename, projectContents); + + Project project = new Project(projectFilename); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + } + finally + { + Thread.Sleep(500); + + // Expect server to be alive and hung up unless a request originating from DTD processing was sent + Assert.IsTrue(t.IsAlive); + } + } + + /// + /// Verify that Condition Evaluator does reset the cached state when the evaluation throws an exception. + /// + [TestMethod] + public void VerifyConditionEvaluatorResetStateOnFailure() + { + PropertyDictionary propertyBag = new PropertyDictionary(); + Expander expander = new Expander(propertyBag); + string condition = " '$(TargetOSFamily)' >= '3' "; + + // Give an incorrect value for the property "TargetOSFamily", and then the evaluation should throw an exception. + propertyBag.Set(ProjectPropertyInstance.Create("TargetOSFamily", "*")); + try + { + ConditionEvaluator.EvaluateCondition( + condition, + ParserOptions.AllowAll, + expander, + ExpanderOptions.ExpandProperties, + Environment.CurrentDirectory, + MockElementLocation.Instance, + null, + new BuildEventContext(1, 2, 3, 4)); + Assert.Fail("Expect exception due to the value of property \"TargetOSFamily\" is not a number."); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine("Expect exception: " + e.Message); + } + + // Correct the property "TargetOSFamily", and then the evaluation should succeed. + propertyBag.Set(ProjectPropertyInstance.Create("TargetOSFamily", "3")); + Assert.IsTrue(ConditionEvaluator.EvaluateCondition( + condition, + ParserOptions.AllowAll, + expander, + ExpanderOptions.ExpandProperties, + Environment.CurrentDirectory, + MockElementLocation.Instance, + null, + new BuildEventContext(1, 2, 3, 4))); + } + + /// + /// HTTP server code running on a separate thread that expects a connection request + /// The test "VerifyDTDProcessingIsDisabled" creates a project with a url reference to this server from a DTD tag + /// If a connection request is received, this thread will terminate, if not, the server will remain alive until + /// "VerifyDTDProcessingIsDisabled" returns. + /// + static private void HttpServerThread() + { + HttpListener listener = new HttpListener(); + listener.Prefixes.Add("http://localhost:51111/"); + listener.Start(); + + HttpListenerContext context = listener.GetContext(); + + // if reached this point it means the server answered a request triggered during DTD processing + listener.Stop(); + } + + /// + /// Creates a standard ProjectCollection and adds a fake toolset with the following contents to it: + /// + /// ToolsVersion = Fake + /// Base Properties: + /// a = a1 + /// b = b1 + /// + /// SubToolset "11.0": + /// b = b2 + /// c = c2 + /// + /// SubToolset "FakeSubToolset": + /// a = a3 + /// c = c3 + /// + private ProjectCollection GetProjectCollectionWithFakeToolset(IDictionary globalProperties) + { + ProjectCollection projectCollection = new ProjectCollection(globalProperties); + + IDictionary properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + properties.Add("a", "a1"); + properties.Add("b", "b1"); + + Dictionary subToolsets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // SubToolset 11.0 properties + PropertyDictionary subToolset11Properties = new PropertyDictionary(); + subToolset11Properties.Set(ProjectPropertyInstance.Create("b", "b2")); + subToolset11Properties.Set(ProjectPropertyInstance.Create("c", "c2")); + + // FakeSubToolset properties + PropertyDictionary fakeSubToolsetProperties = new PropertyDictionary(); + fakeSubToolsetProperties.Set(ProjectPropertyInstance.Create("a", "a3")); + fakeSubToolsetProperties.Set(ProjectPropertyInstance.Create("c", "c3")); + + subToolsets.Add("FakeSubToolset", new SubToolset("FakeSubToolset", fakeSubToolsetProperties)); + subToolsets.Add("11.0", new SubToolset("11.0", subToolset11Properties)); + + Toolset parentToolset = projectCollection.GetToolset("4.0"); + + Toolset fakeToolset = new Toolset("Fake", parentToolset.ToolsPath, properties, projectCollection, subToolsets, parentToolset.OverrideTasksPath); + + projectCollection.AddToolset(fakeToolset); + + return projectCollection; + } + + /// + /// To the target provided add messages to dump all the MSBuildThisFileXXXX properties. + /// + private void AddPropertyDumpTasks(string prefix, ProjectTargetElement target) + { + target.AddTask("Message").SetParameter("Text", prefix + ": MSBuildThisFileDirectory=$(MSBuildThisFileDirectory)"); + target.AddTask("Message").SetParameter("Text", prefix + ": MSBuildThisFileDirectoryNoRoot=$(MSBuildThisFileDirectoryNoRoot)"); + target.AddTask("Message").SetParameter("Text", prefix + ": MSBuildThisFile=$(MSBuildThisFile)"); + target.AddTask("Message").SetParameter("Text", prefix + ": MSBuildThisFileExtension=$(MSBuildThisFileExtension)"); + target.AddTask("Message").SetParameter("Text", prefix + ": MSBuildThisFileFullPath=$(MSBuildThisFileFullPath)"); + target.AddTask("Message").SetParameter("Text", prefix + ": MSBuildThisFileName=$(MSBuildThisFileName)"); + } + + /// + /// Creates a file on disk that logs [$(MSBuildThisFile)] + /// + private void CreateTargetsFileWithMessage(string path, string targetName, string dependsOn) + { + ProjectRootElement import = ProjectRootElement.Create(path); + ProjectTargetElement target = import.AddTarget(targetName); + target.AddTask("Message").SetParameter("Text", "[$(MSBuildThisFile)]"); + target.DependsOnTargets = dependsOn; + import.Save(); + } + + /// + /// Verifies that the import path. + /// + private void VerifyImportTargetRelativePath(string directory, string directory2, string[] imports) + { + string file0 = null; + string file1 = null; + string file2 = null; + string file3 = null; + string file4 = null; + + try + { + if (File.Exists(directory)) + { + Directory.Delete(directory); + } + + file0 = Path.Combine(directory, "my.proj"); + file1 = Path.Combine(directory, "1.targets"); + file2 = Path.Combine(directory2, "2.targets"); + file3 = Path.Combine(directory2, "3.cpp.targets"); + file4 = Path.Combine(directory2, "4.nottargets"); + + ProjectRootElement projectXml = ProjectRootElement.Create(file0); + projectXml.DefaultTargets = "t1"; + foreach (string import in imports) + { + projectXml.AddImport(import); + } + + CreateTargetsFileWithMessage(file1, "t1", "t3"); + CreateTargetsFileWithMessage(file2, "t2", ""); + CreateTargetsFileWithMessage(file3, "t3", "t2"); + CreateTargetsFileWithMessage(file4, "t4", "t3"); + + Project project = new Project(projectXml); + + MockLogger logger = new MockLogger(); + bool result = project.Build(logger); + + Assert.AreEqual(true, result); + + logger.AssertLogContains(new string[] { "[2.targets]", "[3.cpp.targets]", "[1.targets]" }); + logger.AssertLogDoesntContain("4.nottargets"); + + logger.ClearLog(); + + result = project.Build("t4"); + + Assert.AreEqual(false, result); + } + finally + { + File.Delete(file1); + File.Delete(file2); + File.Delete(file3); + File.Delete(file4); + Directory.Delete(directory, true); + } + } + + #region ProjectPropertyComparer + + /// + /// Checks two ProjectProperty objects belonging to the same project for equality. + /// + private class ProjectPropertyComparer : IEqualityComparer + { + /// + /// Checks if two ProjectProperty objects are semantically equal. + /// + /// The first object. + /// The second object. + /// If they are semantically equal. + public bool Equals(ProjectProperty x, ProjectProperty y) + { + bool areEqual = false; + + if (Object.ReferenceEquals(x, y)) + { + // If they point to the same object or are both null, they are equal. + areEqual = true; + } + else if (x == null ^ y == null) + { + // If only one of them is null, they are NOT equal. + areEqual = false; + } + else if (!Object.ReferenceEquals(x.Project, y.Project)) + { + // If they don't belong to the same project, they are not equal. + areEqual = false; + } + else if (x.Xml != null && y.Xml != null && Object.ReferenceEquals(x.Xml, y.Xml)) + { + // If their underlying construction model elements are the same, they are equal. + // Note that certain properties such as global/environment/toolset properties + // do not have a backing xml. + areEqual = true; + } + else if (x.Xml == null && y.Xml == null) // both are global/environment/toolset properties + { + // If both their unevaluated values as well as their evaluated values are same, then they are equal. + areEqual = String.Equals(x.UnevaluatedValue, y.UnevaluatedValue, StringComparison.OrdinalIgnoreCase); + } + + return areEqual; + } + + /// + /// Returns the hash code for a ProjectProperty object. + /// + /// A ProjectProperty object. + /// The has code. + public int GetHashCode(ProjectProperty obj) + { + int hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.UnevaluatedValue); + + return hashCode; + } + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/Expander_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/Expander_Tests.cs new file mode 100644 index 00000000000..175f3c148f4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/Expander_Tests.cs @@ -0,0 +1,3131 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using System.Xml; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; + +using ProjectHelpers = Microsoft.Build.UnitTests.BackEnd.ProjectHelpers; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using Microsoft.Win32; +using System.Text; +using System.IO; +using Microsoft.Build.Internal; +using System.Globalization; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests.Evaluation +{ + [TestClass] + public class Expander_Tests + { + private string _dateToParse = new DateTime(2010, 12, 25).ToString(CultureInfo.CurrentCulture); + [TestMethod] + public void ExpandAllIntoTaskItems0() + { + PropertyDictionary pg = new PropertyDictionary(); + Expander expander = new Expander(pg); + + IList itemsOut = expander.ExpandIntoTaskItemsLeaveEscaped("", ExpanderOptions.ExpandProperties, null); + + ObjectModelHelpers.AssertItemsMatch("", GetTaskArrayFromItemList(itemsOut)); + } + + [TestMethod] + public void ExpandAllIntoTaskItems1() + { + PropertyDictionary pg = new PropertyDictionary(); + Expander expander = new Expander(pg); + + IList itemsOut = expander.ExpandIntoTaskItemsLeaveEscaped("foo", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + ObjectModelHelpers.AssertItemsMatch(@"foo", GetTaskArrayFromItemList(itemsOut)); + } + + [TestMethod] + public void ExpandAllIntoTaskItems2() + { + PropertyDictionary pg = new PropertyDictionary(); + Expander expander = new Expander(pg); + + IList itemsOut = expander.ExpandIntoTaskItemsLeaveEscaped("foo;bar", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + ObjectModelHelpers.AssertItemsMatch(@" + foo + bar + ", GetTaskArrayFromItemList(itemsOut)); + } + + [TestMethod] + public void ExpandAllIntoTaskItems3() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + + List ig = new List(); + ig.Add(new ProjectItemInstance(project, "Compile", "foo.cs", project.FullPath)); + ig.Add(new ProjectItemInstance(project, "Compile", "bar.cs", project.FullPath)); + + List ig2 = new List(); + ig2.Add(new ProjectItemInstance(project, "Resource", "bing.resx", project.FullPath)); + + ItemDictionary itemsByType = new ItemDictionary(); + itemsByType.ImportItems(ig); + itemsByType.ImportItems(ig2); + + Expander expander = new Expander(pg, itemsByType); + + IList itemsOut = expander.ExpandIntoTaskItemsLeaveEscaped("foo;bar;@(compile);@(resource)", ExpanderOptions.ExpandPropertiesAndItems, MockElementLocation.Instance); + + ObjectModelHelpers.AssertItemsMatch(@" + foo + bar + foo.cs + bar.cs + bing.resx + ", GetTaskArrayFromItemList(itemsOut)); + } + + [TestMethod] + public void ExpandAllIntoTaskItems4() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("a", "aaa")); + pg.Set(ProjectPropertyInstance.Create("b", "bbb")); + pg.Set(ProjectPropertyInstance.Create("c", "cc;dd")); + + Expander expander = new Expander(pg); + + IList itemsOut = expander.ExpandIntoTaskItemsLeaveEscaped("foo$(a);$(b);$(c)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + ObjectModelHelpers.AssertItemsMatch(@" + fooaaa + bbb + cc + dd + ", GetTaskArrayFromItemList(itemsOut)); + } + + /// + /// Expand property expressions into ProjectPropertyInstance itmes + /// + [TestMethod] + public void ExpandPropertiesIntoProjectPropertyInstances() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("a", "aaa")); + pg.Set(ProjectPropertyInstance.Create("b", "bbb")); + pg.Set(ProjectPropertyInstance.Create("c", "cc;dd")); + + Expander expander = new Expander(pg); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + IList itemsOut = expander.ExpandIntoItemsLeaveEscaped("foo$(a);$(b);$(c);$(d", itemFactory, ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(5, itemsOut.Count); + } + + /// + /// Expand property expressions into ProjectPropertyInstance items + /// + [TestMethod] + public void ExpandEmptyPropertyExpressionToEmpty() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$()", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + Assert.AreEqual(String.Empty, result); + } + + /// + /// Expand an item vector into items of the specified type + /// + [TestMethod] + public void ExpandItemVectorsIntoProjectItemInstancesSpecifyingItemType() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "j"); + + IList items = expander.ExpandIntoItemsLeaveEscaped("@(i)", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(2, items.Count); + Assert.AreEqual("j", items[0].ItemType); + Assert.AreEqual("j", items[1].ItemType); + Assert.AreEqual("i0", items[0].EvaluatedInclude); + Assert.AreEqual("i1", items[1].EvaluatedInclude); + } + + /// + /// Expand an item vector into items of the type of the item vector + /// + [TestMethod] + public void ExpandItemVectorsIntoProjectItemInstancesWithoutSpecifyingItemType() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project); + + IList items = expander.ExpandIntoItemsLeaveEscaped("@(i)", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(2, items.Count); + Assert.AreEqual("i", items[0].ItemType); + Assert.AreEqual("i", items[1].ItemType); + Assert.AreEqual("i0", items[0].EvaluatedInclude); + Assert.AreEqual("i1", items[1].EvaluatedInclude); + } + + /// + /// Expand an item vector function AnyHaveMetadataValue + /// + [TestMethod] + public void ExpandItemVectorFunctionsAnyHaveMetadataValue() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->AnyHaveMetadataValue('Even', 'true'))", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(1, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[0].ItemType); + Assert.AreEqual("true", itemsTrue[0].EvaluatedInclude); + + IList itemsFalse = expander.ExpandIntoItemsLeaveEscaped("@(i->AnyHaveMetadataValue('Even', 'goop'))", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(1, itemsFalse.Count); + Assert.AreEqual("i", itemsFalse[0].ItemType); + Assert.AreEqual("false", itemsFalse[0].EvaluatedInclude); + } + + /// + /// Expand an item vector function Metadata()->DirectoryName()->Distinct() + /// + [TestMethod] + public void ExpandItemVectorFunctionsGetDirectoryNameOfMetadataValueDistinct() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta0')->DirectoryName()->Distinct())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(1, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[0].ItemType); + Assert.AreEqual(@"c:\firstdirectory\seconddirectory", itemsTrue[0].EvaluatedInclude); + + IList itemsDir = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta9')->DirectoryName()->Distinct())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(1, itemsDir.Count); + Assert.AreEqual("i", itemsDir[0].ItemType); + Assert.AreEqual(Path.Combine(Directory.GetCurrentDirectory(), @"seconddirectory"), itemsDir[0].EvaluatedInclude); + } + + /// + /// /// Expand an item vector function that is an itemspec modifier + /// + [TestMethod] + public void ExpandItemVectorFunctionsItemSpecModifier() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta0')->Directory())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@"firstdirectory\seconddirectory\", itemsTrue[5].EvaluatedInclude); + + itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta0')->Filename())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@"file0", itemsTrue[5].EvaluatedInclude); + + itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta0')->Extension())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@".ext", itemsTrue[5].EvaluatedInclude); + } + + /// + /// Expand an item expression (that isn't a real expression) but includes a property reference nested within a metadata reference + /// + [TestMethod] + public void ExpandItemVectorFunctionsInvalid1() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + string result = expander.ExpandIntoStringLeaveEscaped("[@(type->'%($(a)), '%'')]", ExpanderOptions.ExpandAll, MockElementLocation.Instance); + + Assert.AreEqual(@"[@(type->'%(filename), '%'')]", result); + } + + /// + /// Expand an item expression (that isn't a real expression) but includes a metadata reference that till needs to be expanded + /// + [TestMethod] + public void ExpandItemVectorFunctionsInvalid2() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + string result = expander.ExpandIntoStringLeaveEscaped("[@(i->'%(Meta9))']", ExpanderOptions.ExpandAll, MockElementLocation.Instance); + + Assert.AreEqual(@"[@(i->')']", result); + } + + /// + /// Expand an item vector function that is chained into a string + /// + [TestMethod] + public void ExpandItemVectorFunctionsChained1() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + string result = expander.ExpandIntoStringLeaveEscaped("@(i->'%(Meta0)'->'%(Directory)'->Distinct())", ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(@"firstdirectory\seconddirectory\", result); + } + + /// + /// Expand an item vector function that is chained and has constants into a string + /// + [TestMethod] + public void ExpandItemVectorFunctionsChained2() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + string result = expander.ExpandIntoStringLeaveEscaped("[@(i->'%(Meta0)'->'%(Directory)'->Distinct())]", ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(@"[firstdirectory\seconddirectory\]", result); + } + + /// + /// Expand an item vector function that is chained and has constants into a string + /// + [TestMethod] + public void ExpandItemVectorFunctionsChained3() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + string result = expander.ExpandIntoStringLeaveEscaped("@(i->'%(MetaBlank)'->'%(Directory)'->Distinct())", ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(@"", result); + } + + [TestMethod] + public void ExpandItemVectorFunctionsChainedProject1() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + + C:\Value1\file1.txt + |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ## + + + C:\Value2\file2.txt + |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ## + + + C:\Value2\file3.txt + |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ## + + + C:\Value2\file3.txt + |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ## + + + + + '%(SomeMeta)'->'%(Directory)'->Distinct())`/> + '%(SomeMeta)'->'%(Directory)'->Distinct(), '%(A)')`/> + '%(SomeMeta)'->'%(Directory)'->Distinct(), '%(A)%(B)')`/> + '%(SomeMeta)'->'%(Directory)'->Distinct(), '%(A)$%(B)')`/> + '%(SomeMeta)'->'%(Directory)'->Distinct(), '$%(A)$%(B)')`/> + '%(SomeMeta)'->'%(Directory)'->Distinct(), '$%(A)$%(B)$')`/> + + + "); + + logger.AssertLogContains(@"DirChain0: Value1\;Value2\"); + logger.AssertLogContains(@"DirChain1: Value1\||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||Value2\"); + logger.AssertLogContains(@"DirChain2: Value1\||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||##Value2\"); + logger.AssertLogContains(@"DirChain3: Value1\||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||$##Value2\"); + logger.AssertLogContains(@"DirChain4: Value1\$||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||$##Value2\"); + logger.AssertLogContains(@"DirChain5: Value1\$||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||$##$Value2\"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsCount1() + { + string content = @" + + + + + + + + + Count())][@(J->Count())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[2][0]"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsCount2() + { + string content = @" + + + + + + + Count());@(J->Count())`/> + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("2;0"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsCountOperatingOnEmptyResult1() + { + string content = @" + + + + + + + + + Metadata('foo')->Count())][@(J->Metadata('foo')->Count())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[0][0]"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsCountOperatingOnEmptyResult2() + { + string content = @" + + + + + + + Metadata('foo')->Count());@(J->Metadata('foo')->Count())`/> + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("0;0"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsBuiltIn1() + { + string content = @" + + + + + + + + FullPath())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + var current = Directory.GetCurrentDirectory(); + log.AssertLogContains(String.Format(@"[{0}\foo;{0}\bar]", current)); + } + + [TestMethod] + public void ExpandItemVectorFunctionsBuiltIn2() + { + string content = @" + + + + + + + + FullPath()->Distinct())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + var current = Directory.GetCurrentDirectory(); + log.AssertLogContains(String.Format(@"[{0}\foo;{0}\bar]", current)); + } + + [TestMethod] + public void ExpandItemVectorFunctionsBuiltIn3() + { + string content = @" + + + + + + + + FullPath()->Distinct())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + var current = Directory.GetCurrentDirectory(); + log.AssertLogContains(String.Format(@"[{0}\foo;{0}\bar]", current)); + } + + [TestMethod] + public void ExpandItemVectorFunctionsBuiltIn4() + { + string content = @" + + + + + + + + Identity()->Distinct())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[foo;bar]"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsBuiltIn_PathTooLongError() + { + string content = @" + + + + + + + + FullPath())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectFailure(content, false /* no crashes */); + log.AssertLogContains("MSB4198"); + } + + [TestMethod] + public void ExpandItemVectorFunctionsBuiltIn_InvalidCharsError() + { + string content = @" + + + + + + + + Directory())]` /> + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectFailure(content, false /* no crashes */); + log.AssertLogContains("MSB4198"); + } + + /// + /// /// Expand an item vector function that is an itemspec modifier + /// + [TestMethod] + public void ExpandItemVectorFunctionsItemSpecModifier2() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->'%(Meta0)'->'%(Directory)')", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@"firstdirectory\seconddirectory\", itemsTrue[5].EvaluatedInclude); + + itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->'%(Meta0)'->'%(Filename)')", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@"file0", itemsTrue[5].EvaluatedInclude); + + itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->'%(Meta0)'->'%(Extension)'->Distinct())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(1, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[0].ItemType); + Assert.AreEqual(@".ext", itemsTrue[0].EvaluatedInclude); + + itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->'%(Meta0)'->'%(Filename)'->Substring($(Val)))", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@"le0", itemsTrue[5].EvaluatedInclude); + } + + /// + /// Expand an item vector function Metadata()->DirectoryName() + /// + [TestMethod] + public void ExpandItemVectorFunctionsGetDirectoryNameOfMetadataValue() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList itemsTrue = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta0')->DirectoryName())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, itemsTrue.Count); + Assert.AreEqual("i", itemsTrue[5].ItemType); + Assert.AreEqual(@"c:\firstdirectory\seconddirectory", itemsTrue[5].EvaluatedInclude); + } + + /// + /// Expand an item vector function Metadata() that contains semi-colon delimited sub-items + /// + [TestMethod] + public void ExpandItemVectorFunctionsMetadataValueMultiItem() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList items = expander.ExpandIntoItemsLeaveEscaped("@(i->Metadata('Meta10')->DirectoryName())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(20, items.Count); + Assert.AreEqual("i", items[5].ItemType); + Assert.AreEqual("i", items[6].ItemType); + Assert.AreEqual(Path.Combine(Directory.GetCurrentDirectory(), @"secondd;rectory"), items[5].EvaluatedInclude); + Assert.AreEqual(Path.Combine(Directory.GetCurrentDirectory(), @"someo;herplace"), items[6].EvaluatedInclude); + } + + /// + /// Expand an item vector function Items->ClearMetadata() + /// + [TestMethod] + public void ExpandItemVectorFunctionsClearMetadata() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + var expander = CreateItemFunctionExpander(); + + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(project, "i"); + + IList items = expander.ExpandIntoItemsLeaveEscaped("@(i->ClearMetadata())", itemFactory, ExpanderOptions.ExpandItems, MockElementLocation.Instance); + + Assert.AreEqual(10, items.Count); + Assert.AreEqual("i", items[5].ItemType); + Assert.AreEqual(0, items[5].Metadata.Count()); + } + + /// + /// Creates an expander populated with some ProjectPropertyInstances and ProjectPropertyItems. + /// + /// + private Expander CreateItemFunctionExpander() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("p", "v0")); + pg.Set(ProjectPropertyInstance.Create("p", "v1")); + pg.Set(ProjectPropertyInstance.Create("Val", "2")); + pg.Set(ProjectPropertyInstance.Create("a", "filename")); + + ItemDictionary ig = new ItemDictionary(); + + for (int n = 0; n < 10; n++) + { + ProjectItemInstance pi = new ProjectItemInstance(project, "i", "i" + n.ToString(), project.FullPath); + for (int m = 0; m < 5; m++) + { + pi.SetMetadata("Meta" + m.ToString(), @"c:\firstdirectory\seconddirectory\file" + m.ToString() + ".ext"); + } + pi.SetMetadata("Meta9", @"seconddirectory\file.ext"); + pi.SetMetadata("Meta10", @";someo%3bherplace\foo.txt;secondd%3brectory\file.ext;"); + pi.SetMetadata("MetaBlank", @""); + + if (n % 2 > 0) + { + pi.SetMetadata("Even", "true"); + pi.SetMetadata("Odd", "false"); + } + else + { + pi.SetMetadata("Even", "false"); + pi.SetMetadata("Odd", "true"); + } + ig.Add(pi); + } + + Dictionary itemMetadataTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + itemMetadataTable["Culture"] = "abc%253bdef;$(Gee_Aych_Ayee)"; + itemMetadataTable["Language"] = "english"; + IMetadataTable itemMetadata = new StringMetadataTable(itemMetadataTable); + + Expander expander = new Expander(pg, ig, itemMetadata); + + return expander; + } + + /// + /// Creates an expander populated with some ProjectPropertyInstances and ProjectPropertyItems. + /// + /// + private Expander CreateExpander() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("p", "v0")); + pg.Set(ProjectPropertyInstance.Create("p", "v1")); + + ItemDictionary ig = new ItemDictionary(); + ProjectItemInstance i0 = new ProjectItemInstance(project, "i", "i0", project.FullPath); + ProjectItemInstance i1 = new ProjectItemInstance(project, "i", "i1", project.FullPath); + ig.Add(i0); + ig.Add(i1); + + Expander expander = new Expander(pg, ig); + + return expander; + } + + /// + /// Regression test for bug when there are literally zero items declared + /// in the project, we should continue to expand item list references to empty-string + /// rather than not expand them at all. + /// + [TestMethod] + public void ZeroItemsInProjectExpandsToEmpty() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + + + + + "); + + logger.AssertLogDoesntContain("This target should NOT run."); + + logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + + + + + + + + + "); + + logger.AssertLogContains("Item list foo contains abc"); + } + + [TestMethod] + public void ItemIncludeContainsMultipleItemReferences() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + Library + + + + '%(filename).obj')`/> + '%(filename).obj')`/> + + + + + + + + + "); + + logger.AssertLogContains("Property OutputType=Library"); + logger.AssertLogContains("Item ObjFiles=foo.obj;bar.obj"); + logger.AssertLogContains("Item CleanFiles=foo.obj;bar.obj"); + } + + /// + /// Bad path when getting metadata through ->Metadata function + /// + [TestMethod] + public void InvalidPathAndMetadataItemFunction() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + + + + Metadata('FullPath'))"" /> + + ", false); + + logger.AssertLogContains("MSB4023"); + } + + /// + /// Asking for blank metadata + /// + [TestMethod] + public void InvalidMetadataName() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + + + + Metadata(''))"" /> + + ", false); + + logger.AssertLogContains("MSB4023"); + } + + /// + /// Bad path when getting metadata through ->WithMetadataValue function + /// + [TestMethod] + public void InvalidPathAndMetadataItemFunction2() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + + + + WithMetadataValue('FullPath', 'x'))"" /> + + ", false); + + logger.AssertLogContains("MSB4023"); + } + + /// + /// Asking for blank metadata with ->WithMetadataValue + /// + [TestMethod] + public void InvalidMetadataName2() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + + + + WithMetadataValue('', 'x'))"" /> + + ", false); + + logger.AssertLogContains("MSB4023"); + } + + /// + /// Bad path when getting metadata through ->AnyHaveMetadataValue function + /// + [TestMethod] + public void InvalidPathAndMetadataItemFunction3() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + + + + AnyHaveMetadataValue('FullPath', 'x'))"" /> + + ", false); + + logger.AssertLogContains("MSB4023"); + } + + /// + /// Asking for blank metadata with ->AnyHaveMetadataValue + /// + [TestMethod] + public void InvalidMetadataName3() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + + + + AnyHaveMetadataValue('', 'x'))"" /> + + ", false); + + logger.AssertLogContains("MSB4023"); + } + + /// + /// Filter by metadata presence + /// + [TestMethod] + public void HasMetadata() + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@" + + + + <_Item Include=""One""> + aa + bb + cc + + <_Item Include=""Two""> + bb + cc + + <_Item Include=""Three""> + aa + cc + + <_Item Include=""Four""> + aa + bb + cc + + <_Item Include=""Five""> + + + + + + HasMetadata('a'), '|')]""/> + + + +"); + + logger.AssertLogContains("[One|Three|Four]"); + } + + /// + /// Verify when there is an error due to an attempt to use a static method that we report the method name + /// + [TestMethod] + public void StaticMethodErrorMessageHaveMethodName() + { + try + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + $([System.IO.Path]::Combine(null,'')) + + + + + ", false); + } + catch (Microsoft.Build.Exceptions.InvalidProjectFileException e) + { + Assert.IsTrue(e.Message.IndexOf("[System.IO.Path]::Combine(null, '')", StringComparison.OrdinalIgnoreCase) != -1); + return; + } + + Assert.Fail(); + } + + /// + /// Verify when there is an error due to an attempt to use a static method that we report the method name + /// + [TestMethod] + public void StaticMethodErrorMessageHaveMethodName1() + { + try + { + MockLogger logger = Helpers.BuildProjectWithNewOMExpectFailure(@" + + + $(System.IO.Path::Combine('a','b')) + + + + + ", false); + } + catch (Microsoft.Build.Exceptions.InvalidProjectFileException e) + { + Assert.IsTrue(e.Message.IndexOf("System.IO.Path::Combine('a','b')", StringComparison.OrdinalIgnoreCase) != -1); + return; + } + + Assert.Fail(); + } + /// + /// Creates a set of complicated item metadata and properties, and items to exercise + /// the Expander class. The data here contains escaped characters, metadata that + /// references properties, properties that reference items, and other complex scenarios. + /// + /// + /// + /// + /// + private void CreateComplexPropertiesItemsMetadata + ( + out ReadOnlyLookup readOnlyLookup, + out StringMetadataTable itemMetadata + ) + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + Dictionary itemMetadataTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + itemMetadataTable["Culture"] = "abc%253bdef;$(Gee_Aych_Ayee)"; + itemMetadataTable["Language"] = "english"; + itemMetadata = new StringMetadataTable(itemMetadataTable); + + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Gee_Aych_Ayee", "ghi")); + pg.Set(ProjectPropertyInstance.Create("OutputPath", @"\jk ; l\mno%253bpqr\stu")); + pg.Set(ProjectPropertyInstance.Create("TargetPath", "@(IntermediateAssembly->'%(RelativeDir)')")); + + List intermediateAssemblyItemGroup = new List(); + ProjectItemInstance i1 = new ProjectItemInstance(project, "IntermediateAssembly", @"subdir1\engine.dll", project.FullPath); + intermediateAssemblyItemGroup.Add(i1); + i1.SetMetadata("aaa", "111"); + ProjectItemInstance i2 = new ProjectItemInstance(project, "IntermediateAssembly", @"subdir2\tasks.dll", project.FullPath); + intermediateAssemblyItemGroup.Add(i2); + i2.SetMetadata("bbb", "222"); + + List contentItemGroup = new List(); + ProjectItemInstance i3 = new ProjectItemInstance(project, "Content", "splash.bmp", project.FullPath); + contentItemGroup.Add(i3); + i3.SetMetadata("ccc", "333"); + + List resourceItemGroup = new List(); + ProjectItemInstance i4 = new ProjectItemInstance(project, "Resource", "string$(p).resx", project.FullPath); + resourceItemGroup.Add(i4); + i4.SetMetadata("ddd", "444"); + ProjectItemInstance i5 = new ProjectItemInstance(project, "Resource", "dialogs%253b.resx", project.FullPath); + resourceItemGroup.Add(i5); + i5.SetMetadata("eee", "555"); + + List contentItemGroup2 = new List(); + ProjectItemInstance i6 = new ProjectItemInstance(project, "Content", "about.bmp", project.FullPath); + contentItemGroup2.Add(i6); + i6.SetMetadata("fff", "666"); + + ItemDictionary secondaryItemsByName = new ItemDictionary(); + secondaryItemsByName.ImportItems(resourceItemGroup); + secondaryItemsByName.ImportItems(contentItemGroup2); + + Lookup lookup = new Lookup(secondaryItemsByName, pg, null); + + // Add primary items + lookup.EnterScope("x"); + lookup.PopulateWithItems("IntermediateAssembly", intermediateAssemblyItemGroup); + lookup.PopulateWithItems("Content", contentItemGroup); + + readOnlyLookup = new ReadOnlyLookup(lookup); + } + + /// + /// Exercises ExpandAllIntoTaskItems with a complex set of data. + /// + [TestMethod] + public void ExpandAllIntoTaskItemsComplex() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + IList taskItems = expander.ExpandIntoTaskItemsLeaveEscaped( + "@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; $(NonExistent) ; %(NonExistent) ; " + + "$(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)", + ExpanderOptions.ExpandAll, MockElementLocation.Instance); + + // the following items are passed to the TaskItem constructor, and thus their ItemSpecs should be + // in escaped form. + ObjectModelHelpers.AssertItemsMatch(@" + string$(p): ddd=444 + dialogs%253b: eee=555 + splash.bmp: ccc=333 + \jk + l\mno%253bpqr\stu + subdir1\: aaa=111 + subdir2\: bbb=222 + english_abc%253bdef + ghi + ", GetTaskArrayFromItemList(taskItems)); + } + + /// + /// Exercises ExpandAllIntoString with a complex set of data but in a piecemeal fashion + /// + [TestMethod] + public void ExpandAllIntoStringComplexPiecemeal() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + string stringToExpand = "@(Resource->'%(Filename)') ;"; + Assert.AreEqual( + @"string$(p);dialogs%3b ;", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "@(Content)"; + Assert.AreEqual( + @"splash.bmp", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "@(NonExistent)"; + Assert.AreEqual( + @"", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "$(NonExistent)"; + Assert.AreEqual( + @"", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "%(NonExistent)"; + Assert.AreEqual( + @"", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "$(OutputPath)"; + Assert.AreEqual( + @"\jk ; l\mno%3bpqr\stu", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "$(TargetPath)"; + Assert.AreEqual( + @"subdir1\;subdir2\", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + stringToExpand = "%(Language)_%(Culture)"; + Assert.AreEqual( + @"english_abc%3bdef;ghi", + expander.ExpandIntoStringAndUnescape(stringToExpand, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + } + + /// + /// Exercises ExpandAllIntoString with an item list using a transform that is empty + /// + [TestMethod] + public void ExpandAllIntoStringEmpty() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + XmlAttribute xmlattribute = (new XmlDocument()).CreateAttribute("dummy"); + xmlattribute.Value = "@(IntermediateAssembly->'')"; + + Assert.AreEqual( + @";", + expander.ExpandIntoStringAndUnescape(xmlattribute.Value, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + xmlattribute.Value = "@(IntermediateAssembly->'%(goop)')"; + + Assert.AreEqual( + @";", + expander.ExpandIntoStringAndUnescape(xmlattribute.Value, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + } + + /// + /// Exercises ExpandAllIntoString with a complex set of data. + /// + [TestMethod] + public void ExpandAllIntoStringComplex() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + XmlAttribute xmlattribute = (new XmlDocument()).CreateAttribute("dummy"); + xmlattribute.Value = "@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; $(NonExistent) ; %(NonExistent) ; " + + "$(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)"; + + Assert.AreEqual( + @"string$(p);dialogs%3b ; splash.bmp ; ; ; ; \jk ; l\mno%3bpqr\stu ; subdir1\;subdir2\ ; english_abc%3bdef;ghi", + expander.ExpandIntoStringAndUnescape(xmlattribute.Value, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + } + + /// + /// Exercises ExpandAllIntoString with a complex set of data. + /// + [TestMethod] + public void ExpandAllIntoStringLeaveEscapedComplex() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + XmlAttribute xmlattribute = (new XmlDocument()).CreateAttribute("dummy"); + xmlattribute.Value = "@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; $(NonExistent) ; %(NonExistent) ; " + + "$(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)"; + + Assert.AreEqual( + @"string$(p);dialogs%253b ; splash.bmp ; ; ; ; \jk ; l\mno%253bpqr\stu ; subdir1\;subdir2\ ; english_abc%253bdef;ghi", + expander.ExpandIntoStringLeaveEscaped(xmlattribute.Value, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + } + + /// + /// Exercises ExpandAllIntoString with a string that does not need expanding. + /// In this case the expanded string should be reference identical to the passed in string. + /// + [TestMethod] + public void ExpandAllIntoStringExpectIdenticalReference() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + XmlAttribute xmlattribute = (new XmlDocument()).CreateAttribute("dummy"); + + // Create a *non-literal* string. If we used a literal string, the CLR might (would) intern + // it, which would mean that Expander would inevitably return a reference to the same string. + // In real builds, the strings will never be literals, and we want to test the behavior in + // that situation. + xmlattribute.Value = "abc123" + new Random().Next(); + string expandedString = expander.ExpandIntoStringLeaveEscaped(xmlattribute.Value, ExpanderOptions.ExpandAll, MockElementLocation.Instance); + + // Verify neither string got interned, so that this test is meaningful + Assert.IsTrue(null == string.IsInterned(xmlattribute.Value)); + Assert.IsTrue(null == string.IsInterned(expandedString)); + + // Finally verify Expander indeed didn't create a new string. + Assert.IsTrue(Object.ReferenceEquals(xmlattribute.Value, expandedString)); + } + + /// + /// Exercises ExpandAllIntoString with a complex set of data and various expander options + /// + [TestMethod] + public void ExpandAllIntoStringExpanderOptions() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + string value = @"@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; $(NonExistent) ; %(NonExistent) ; $(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)"; + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + Assert.AreEqual(@"@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; ; %(NonExistent) ; \jk ; l\mno%3bpqr\stu ; @(IntermediateAssembly->'%(RelativeDir)') ; %(Language)_%(Culture)", expander.ExpandIntoStringAndUnescape(value, ExpanderOptions.ExpandProperties, MockElementLocation.Instance)); + + Assert.AreEqual(@"@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; ; ; \jk ; l\mno%3bpqr\stu ; @(IntermediateAssembly->'%(RelativeDir)') ; english_abc%3bdef;ghi", expander.ExpandIntoStringAndUnescape(value, ExpanderOptions.ExpandPropertiesAndMetadata, MockElementLocation.Instance)); + + Assert.AreEqual(@"string$(p);dialogs%3b ; splash.bmp ; ; ; ; \jk ; l\mno%3bpqr\stu ; subdir1\;subdir2\ ; english_abc%3bdef;ghi", expander.ExpandIntoStringAndUnescape(value, ExpanderOptions.ExpandAll, MockElementLocation.Instance)); + + Assert.AreEqual(@"string$(p);dialogs%3b ; splash.bmp ; ; $(NonExistent) ; %(NonExistent) ; $(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)", expander.ExpandIntoStringAndUnescape(value, ExpanderOptions.ExpandItems, MockElementLocation.Instance)); + } + + /// + /// Exercises ExpandAllIntoStringListLeaveEscaped with a complex set of data. + /// + [TestMethod] + public void ExpandAllIntoStringListLeaveEscapedComplex() + { + ReadOnlyLookup lookup; + StringMetadataTable itemMetadata; + CreateComplexPropertiesItemsMetadata(out lookup, out itemMetadata); + + Expander expander = new Expander(lookup, lookup, itemMetadata); + + string value = "@(Resource->'%(Filename)') ; @(Content) ; @(NonExistent) ; $(NonExistent) ; %(NonExistent) ; " + + "$(OutputPath) ; $(TargetPath) ; %(Language)_%(Culture)"; + + IList expanded = expander.ExpandIntoStringListLeaveEscaped(value, ExpanderOptions.ExpandAll, MockElementLocation.Instance); + + Assert.AreEqual(9, expanded.Count); + Assert.AreEqual(@"string$(p)", expanded[0]); + Assert.AreEqual(@"dialogs%253b", expanded[1]); + Assert.AreEqual(@"splash.bmp", expanded[2]); + Assert.AreEqual(@"\jk", expanded[3]); + Assert.AreEqual(@"l\mno%253bpqr\stu", expanded[4]); + Assert.AreEqual(@"subdir1\", expanded[5]); + Assert.AreEqual(@"subdir2\", expanded[6]); + Assert.AreEqual(@"english_abc%253bdef", expanded[7]); + Assert.AreEqual(@"ghi", expanded[8]); + } + + internal ITaskItem[] GetTaskArrayFromItemList(IList list) + { + ITaskItem[] items = new ITaskItem[list.Count]; + for (int i = 0; i < list.Count; ++i) + { + items[i] = list[i]; + } + + return items; + } + + /// + /// v10.0\TeamData\Microsoft.Data.Schema.Common.targets shipped with bad syntax: + /// $(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory) + /// this was evaluating to blank before, now it errors; we have to special case it to + /// evaluate to blank. + /// Note that this still works whether or not the key exists and has a value. + /// + [TestMethod] + public void RegistryPropertyInvalidPrefixSpecialCase() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(String.Empty, result); + } + + // Compat hack: WebProjects may have an import with a condition like: + // Condition=" '$(Solutions.VSVersion)' == '8.0'" + // These would have been '' in prior versions of msbuild but would be treated as a possible string function in current versions. + // Be compatible by returning an empty string here. + [TestMethod] + public void Regress692569() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Solutions.VSVersion)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(String.Empty, result); + } + + /// + /// In the general case, we should still error for properties that incorrectly miss the Registry: prefix. + /// Note that this still fails whether or not the key exists. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RegistryPropertyInvalidPrefixError() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + expander.ExpandIntoStringLeaveEscaped(@"$(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@XXXXDBDirectory)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// In the general case, we should still error for properties that incorrectly miss the Registry: prefix, like + /// the special case, but with extra char on the end. + /// Note that this still fails whether or not the key exists. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RegistryPropertyInvalidPrefixError2() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + expander.ExpandIntoStringLeaveEscaped(@"$(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectoryX)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + [TestMethod] + public void RegistryPropertyString() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue("Value", "String", RegistryValueKind.String); + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test@Value)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("String", result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void RegistryPropertyBinary() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + UTF8Encoding enc = new UTF8Encoding(); + byte[] utfText = enc.GetBytes("String".ToCharArray()); + + key.SetValue("Value", utfText, RegistryValueKind.Binary); + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test@Value)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("83;116;114;105;110;103", result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void RegistryPropertyDWord() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue("Value", 123456, RegistryValueKind.DWord); + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test@Value)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("123456", result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void RegistryPropertyExpandString() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue("Value", "%TEMP%", RegistryValueKind.ExpandString); + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test@Value)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(Environment.GetEnvironmentVariable("TEMP"), result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void RegistryPropertyQWord() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue("Value", (long)123456789123456789, RegistryValueKind.QWord); + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test@Value)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("123456789123456789", result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void RegistryPropertyMultiString() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue("Value", new string[] { "A", "B", "C", "D" }, RegistryValueKind.MultiString); + string result = expander.ExpandIntoStringLeaveEscaped(@"$(Registry:HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test@Value)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("A;B;C;D", result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void TestItemSpecModiferEscaping() + { + string content = @" + + + + + + + + + + FullPath())"" /> + '%(FullPath)'->Distinct())"" /> + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogDoesntContain("%28"); + log.AssertLogDoesntContain("%29"); + } + + [TestMethod] + public void TestGetPathToReferenceAssembliesAsFunction() + { + if (ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45) == null) + { + // if there aren't any reference assemblies installed on the machine in the first place, of course + // we're not going to find them. :) + return; + } + + string content = @" + + + + .NETFramework + v4.5 + + $(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion) + + + + + + + + + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToStandardLibraries($(TargetFrameworkIdentifier), $(TargetFrameworkVersion), $(TargetFrameworkProfile)))\ + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogDoesntContain("Reference assembly paths do not match"); + } + + /// + /// Expand property function that takes a null argument + /// + [TestMethod] + public void PropertyFunctionNullArgument() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$([System.Convert]::ChangeType('null',$(SomeStuff.GetType())))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("null", result); + } + + /// + /// Expand property function that returns a null + /// + [TestMethod] + public void PropertyFunctionNullReturn() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$([System.Convert]::ChangeType(,$(SomeStuff.GetType())))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("", result); + } + + /// + /// Expand property function that takes no arguments and returns a string + /// + [TestMethod] + public void PropertyFunctionNoArguments() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.ToUpperInvariant())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("THIS IS SOME STUFF", result); + } + + /// + /// Expand property function that takes no arguments and returns a string (trimmed) + /// + [TestMethod] + public void PropertyFunctionNoArgumentsTrim() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("FileName", " foo.ext ")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(FileName.Trim())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("foo.ext", result); + } + + /// + /// Expand property function that is a get property accessor + /// + [TestMethod] + public void PropertyFunctionPropertyGet() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.Length)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("18", result); + } + + /// + /// Expand property function which is a manual get property accessor + /// + [TestMethod] + public void PropertyFunctionPropertyManualGet() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.get_Length())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("18", result); + } + + /// + /// Expand property function which is a manual get property accessor and a concatenation of a constant + /// + [TestMethod] + public void PropertyFunctionPropertyNoArgumentsConcat() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.ToLowerInvariant())_goop", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("this is some stuff_goop", result); + } + + /// + /// Expand property function with a constant argument + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgument() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.SubString(13))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("STUff", result); + } + + /// + /// Expand property function with a constant argument that contains spaces + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgumentWithSpaces() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.SubString(8))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("SOME STUff", result); + } + + /// + /// Expand property function with a constant argument + /// + [TestMethod] + public void PropertyFunctionPropertyPathRootSubtraction() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("RootPath", @"c:\this\is\the\root")); + pg.Set(ProjectPropertyInstance.Create("MyPath", @"c:\this\is\the\root\my\project\is\here.proj")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(MyPath.SubString($(RootPath.Length)))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"\my\project\is\here.proj", result); + } + + /// + /// Expand property function with an argument that is a property + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgumentExpandedProperty() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.SubString(1$(Value)))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("STUff", result); + } + + /// + /// Expand property function that has a boolean return value + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgumentBooleanReturn() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("PathRoot", @"c:\goo")); + pg.Set(ProjectPropertyInstance.Create("PathRoot2", @"c:\goop\")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$(PathRoot2.Endswith(\))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + Assert.AreEqual("True", result); + result = expander.ExpandIntoStringLeaveEscaped(@"$(PathRoot.Endswith(\))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + Assert.AreEqual("False", result); + } + + /// + /// Expand property function with an argument that is expanded, and a chaing of other functions. + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgumentNestedAndChainedFunction() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.SubString(1$(Value)).ToLowerInvariant().SubString($(Value)))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("ff", result); + } + + + /// + /// Expand property function with chained functions on its results + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgumentChained() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.ToUpperInvariant().ToLowerInvariant())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + Assert.AreEqual("this is some stuff", result); + } + + /// + /// Expand property function with an argument that is a function + /// + [TestMethod] + public void PropertyFunctionPropertyWithArgumentNested() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "12345")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "1234567890")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.SubString($(Value.get_Length())))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("67890", result); + } + + /// + /// Expand property function that returns an generic list + /// + [TestMethod] + public void PropertyFunctionGenericListReturn() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$([MSBuild]::__GetListTest())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("A;B;C;D", result); + } + + /// + /// Expand property function that returns an array + /// + [TestMethod] + public void PropertyFunctionArrayReturn() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("List", "A-B-C-D")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(List.Split(-))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("A;B;C;D", result); + } + /// + /// Expand property function that returns a Dictionary + /// + [TestMethod] + public void PropertyFunctionDictionaryReturn() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$([System.Environment]::GetEnvironmentVariables())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance).ToUpperInvariant(); + string expected = ("OS=" + Environment.GetEnvironmentVariable("OS")).ToUpperInvariant(); + + + Assert.IsTrue(result.Contains(expected)); + } + + /// + /// Expand property function that returns an array + /// + [TestMethod] + public void PropertyFunctionArrayReturnManualSplitter() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("List", "A-B-C-D")); + pg.Set(ProjectPropertyInstance.Create("Splitter", "-")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(List.Split($(Splitter.ToCharArray())))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("A;B;C;D", result); + } + + /// + /// Expand property function that returns an array + /// + [TestMethod] + public void PropertyFunctionInCondition() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("PathRoot", @"c:\goo")); + pg.Set(ProjectPropertyInstance.Create("PathRoot2", @"c:\goop\")); + + Expander expander = new Expander(pg); + + Assert.IsTrue(ConditionEvaluator.EvaluateCondition(@"'$(PathRoot2.Endswith(`\`))' == 'true'", ParserOptions.AllowAll, expander, ExpanderOptions.ExpandProperties, Environment.CurrentDirectory, MockElementLocation.Instance, null, new BuildEventContext(1, 2, 3, 4))); + Assert.IsTrue(ConditionEvaluator.EvaluateCondition(@"'$(PathRoot.Endswith(\))' == 'false'", ParserOptions.AllowAll, expander, ExpanderOptions.ExpandProperties, Environment.CurrentDirectory, MockElementLocation.Instance, null, new BuildEventContext(1, 2, 3, 4))); + } + + /// + /// Expand property function that is invalid - properties don't take arguments + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid1() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("[$(SomeStuff($(Value)))]", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + + /// + /// Expand property function - invlaid since properties don't have properties + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid2() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("[$(SomeStuff.Lgg)]", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// Expand property function - invlaid since properties don't have properties and don't support '.' in them + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid3() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.ToUpperInvariant().Foo)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// Expand property function - properties don't take arguments + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid4() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Value", "3")); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("[$(SomeStuff($(System.DateTime.Now)))]", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + + /// + /// Expand property function - invalid expression + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid5() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(SomeStuff.ToLowerInvariant()_goop)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// Expand property function - functions with invalid arguments + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid6() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("[$(SomeStuff.Substring(HELLO!))]", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// Expand property function - functions with invalid arguments + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid7() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("[$(SomeStuff.Substring(-10))]", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// Expand property function calls a static method with quoted arguments + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void PropertyFunctionInvalid8() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(([System.DateTime]::Now).ToString(\"MM.dd.yyyy\"))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + } + + /// + /// Expand property function - we don't handle metadata functions + /// + [TestMethod] + public void PropertyFunctionInvalidNoMetadataFunctions() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("[%(LowerLetterList.Identity.ToUpper())]", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("[%(LowerLetterList.Identity.ToUpper())]", result); + } + + /// + /// Expand property function - properties won't get confused with a type or namespace + /// + [TestMethod] + public void PropertyFunctionNoCollisionsOnType() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("System", "The System Namespace")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$(System)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("The System Namespace", result); + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethodMakeRelative() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("ParentPath", @"c:\abc\def")); + pg.Set(ProjectPropertyInstance.Create("FilePath", @"c:\abc\def\foo.cpp")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::MakeRelative($(ParentPath), `$(FilePath)`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"foo.cpp", result); + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethod1() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("Drive", @"c:\")); + pg.Set(ProjectPropertyInstance.Create("File", @"foo\file.txt")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine($(Drive), `$(File)`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\foo\file.txt", result); + } + + /// + /// Expand property function that creates an instance of a type + /// + [TestMethod] + public void PropertyFunctionConstructor1() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("ver1", @"1.2.3.4")); + + Expander expander = new Expander(pg); + + object result = expander.ExpandPropertiesLeaveTypedAndEscaped(@"$([System.Version]::new($(ver1)))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Version v = result as Version; + Assert.IsNotNull(v); + + Assert.AreEqual(1, v.Major); + Assert.AreEqual(2, v.Minor); + Assert.AreEqual(3, v.Build); + Assert.AreEqual(4, v.Revision); + } + + /// + /// Expand property function that creates an instance of a type + /// + [TestMethod] + public void PropertyFunctionConstructor2() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("ver1", @"1.2.3.4")); + pg.Set(ProjectPropertyInstance.Create("ver2", @"2.2.3.4")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Version]::new($(ver1)).CompareTo($([System.Version]::new($(ver2)))))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"-1", result); + } + + /// + /// Expand property function that is only available when MSBUILDENABLEALLPROPERTYFUNCTIONS=1 + /// + [TestMethod] + public void PropertyStaticFunctionAllEnabled() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string env = Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", "1"); + + string result = expander.ExpandIntoStringLeaveEscaped("$([Microsoft.VisualBasic.FileIO.FileSystem]::CurrentDirectory)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(0, String.Compare(Environment.CurrentDirectory, result, StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", env); + AvailableStaticMethods.Reset_ForUnitTestsOnly(); + } + } + + + /// + /// Expand property function that is only available when MSBUILDENABLEALLPROPERTYFUNCTIONS=1, but cannot be found + /// + [TestMethod] + public void PropertyStaticFunctionUsingNamespaceNotFound() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string env = Environment.GetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", "1"); + + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([Microsoft.FOO.FileIO.FileSystem]::CurrentDirectory)", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([Foo.Baz]::new())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([Foo]::new())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([Foo.]::new())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([.Foo]::new())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([.]::new())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + Helpers.VerifyAssertThrows(() => expander.ExpandIntoStringLeaveEscaped("$([]::new())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance), typeof(InvalidProjectFileException)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", env); + AvailableStaticMethods.Reset_ForUnitTestsOnly(); + } + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted1() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo\file.txt")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine(`c:\`, `$(File)`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\foo\file.txt", result); + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted1Spaces() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo goo\file.txt")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine(`c:\foo goo\`, `$(File)`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\foo goo\foo goo\file.txt", result); + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted1Spaces2() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo bar\baz.txt")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine(`c:\foo baz\ `, `$(File)`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\foo baz\ \foo bar\baz.txt", result); + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted1Spaces3() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo bar\baz.txt")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine(`c:\foo baz `, `$(File)`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\foo baz \foo bar\baz.txt", result); + } + + /// + /// Expand property function calls a static method with quoted arguments + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted2() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string dateTime = "'" + _dateToParse + "'"; + string result = expander.ExpandIntoStringLeaveEscaped("$([System.DateTime]::Parse(" + dateTime + ").ToString(\"yyyy/MM/dd HH:mm:ss\"))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(System.DateTime.Parse(_dateToParse).ToString("yyyy/MM/dd HH:mm:ss"), result); + } + + /// + /// Expand property function calls a static method with quoted arguments + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted3() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + string dateTime = "'" + _dateToParse + "'"; + string result = expander.ExpandIntoStringLeaveEscaped("$([System.DateTime]::Parse(" + dateTime + ").ToString(\"MM.dd.yyyy\"))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(System.DateTime.Parse(_dateToParse).ToString("MM.dd.yyyy"), result); + } + + /// + /// Expand property function calls a static method with quoted arguments + /// + [TestMethod] + public void PropertyFunctionStaticMethodQuoted4() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped("$([System.DateTime]::Now.ToString(\"MM.dd.yyyy\"))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(DateTime.Now.ToString("MM.dd.yyyy"), result); + } + + /// + /// Expand property function calls a static method + /// + [TestMethod] + public void PropertyFunctionStaticMethodNested() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo\file.txt")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine(`c:\`, $([System.IO.Path]::Combine(`foo`,`file.txt`))))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\foo\file.txt", result); + } + + /// + /// Expand property function calls a static method regex + /// + [TestMethod] + public void PropertyFunctionStaticMethodRegex1() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo\file.txt")); + + Expander expander = new Expander(pg); + + // Support enum combines as Enum.Parse expects them + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Text.RegularExpressions.Regex]::IsMatch(`-42`, `^-?\d+(\.\d{2})?$`, `RegexOptions.IgnoreCase,RegexOptions.Singleline`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"True", result); + + // We support the C# style enum combining syntax too + result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Text.RegularExpressions.Regex]::IsMatch(`-42`, `^-?\d+(\.\d{2})?$`, System.Text.RegularExpressions.RegexOptions.IgnoreCase|RegexOptions.Singleline))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"True", result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Text.RegularExpressions.Regex]::IsMatch(`100 GBP`, `^-?\d+(\.\d{2})?$`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(@"False", result); + } + + /// + /// Expand property function calls a static method with an instance method chained + /// + [TestMethod] + public void PropertyFunctionStaticMethodChained() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + string dateTime = "'" + _dateToParse + "'"; + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.DateTime]::Parse(" + dateTime + ").ToString(`yyyy/MM/dd HH:mm:ss`))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(DateTime.Parse(_dateToParse).ToString("yyyy/MM/dd HH:mm:ss"), result); + } + + /// + /// Expand property function calls a static method an enum argument + /// + [TestMethod] + public void PropertyFunctionStaticMethodEnumArgument() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Environment]::GetFolderPath(SpecialFolder.System))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(System.Environment.GetFolderPath(Environment.SpecialFolder.System), result); + } + + /// + /// Expand intrinsic property function to locate the directory of a file above + /// + [TestMethod] + public void PropertyFunctionStaticMethodDirectoryNameOfFileAbove() + { + string tempPath = Path.GetTempPath(); + string tempFile = Path.GetFileName(FileUtilities.GetTemporaryFile()); + + try + { + string directoryStart = Path.Combine(tempPath, "one\\two\\three\\four\\five"); + + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("StartingDirectory", directoryStart)); + pg.Set(ProjectPropertyInstance.Create("FileToFind", tempFile)); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::GetDirectoryNameOfFileAbove($(StartingDirectory), $(FileToFind)))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(Microsoft.Build.Shared.FileUtilities.EnsureTrailingSlash(tempPath), Microsoft.Build.Shared.FileUtilities.EnsureTrailingSlash(result)); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::GetDirectoryNameOfFileAbove($(StartingDirectory), Hobbits))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(String.Empty, result); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Expand property function calls GetCultureInfo + /// + [TestMethod] + public void PropertyFunctionStaticMethodGetCultureInfo() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.Globalization.CultureInfo]::GetCultureInfo(`en-US`).ToString())", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(System.Globalization.CultureInfo.GetCultureInfo("en-US").ToString(), result); + } + + /// + /// Expand property function calls a static arithmetic method + /// + [TestMethod] + public void PropertyFunctionStaticMethodArithmeticAddInt32() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Add(40, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((40 + 2).ToString(), result); + } + + /// + /// Expand property function calls a static arithmetic method + /// + [TestMethod] + public void PropertyFunctionStaticMethodArithmeticAddDouble() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Add(39.9, 2.1))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((39.9 + 2.1).ToString(), result); + } + + /// + /// Expand property function chosing either the value (if not empty) or the default specfied + /// + [TestMethod] + public void PropertyFunctionValueOrDefault() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::ValueOrDefault('', '42'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("42", result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::ValueOrDefault('42', '43'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("42", result); + } + + /// + /// Expand property function chosing either the value (from the environment) or the default specfied + /// + [TestMethod] + public void PropertyFunctionValueOrDefaultFromEnvironment() + { + PropertyDictionary pg = new PropertyDictionary(); + + pg["BonkersTargetsPath"] = ProjectPropertyInstance.Create("BonkersTargetsPath", "Bonkers"); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::ValueOrDefault('$(BonkersTargetsPath)', '42'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("Bonkers", result); + + pg["BonkersTargetsPath"] = ProjectPropertyInstance.Create("BonkersTargetsPath", String.Empty); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::ValueOrDefault('$(BonkersTargetsPath)', '43'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("43", result); + } + + /// + /// Expand property function that tests for existence of the task host + /// + [TestMethod] + public void PropertyFunctionDoesTaskHostExist() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::DoesTaskHostExist('CurrentRuntime', 'CurrentArchitecture'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + // This is the current, so it had better be true! + Assert.IsTrue(String.Equals("true", result, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Expand property function that tests for existence of the task host + /// + [TestMethod] + public void PropertyFunctionDoesTaskHostExist_Whitespace() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::DoesTaskHostExist(' CurrentRuntime ', 'CurrentArchitecture'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + // This is the current, so it had better be true! + Assert.IsTrue(String.Equals("true", result, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Expand property function that tests for existence of the task host + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void PropertyFunctionDoesTaskHostExist_Error() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::DoesTaskHostExist('ASDF', 'CurrentArchitecture'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + // We should have failed before now + Assert.Fail(); + } + + /// + /// Expand property function that tests for existence of the task host + /// + [TestMethod] + public void PropertyFunctionDoesTaskHostExist_Evaluated() + { + PropertyDictionary pg = new PropertyDictionary(); + + pg["Runtime"] = ProjectPropertyInstance.Create("Runtime", "CurrentRuntime"); + pg["Architecture"] = ProjectPropertyInstance.Create("Architecture", "CurrentArchitecture"); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::DoesTaskHostExist('$(Runtime)', '$(Architecture)'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + // This is the current, so it had better be true! + Assert.IsTrue(String.Equals("true", result, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Expand property function that tests for existence of the task host + /// + [TestMethod] + public void PropertyFunctionDoesTaskHostExist_NonexistentTaskHost() + { + string taskHostName = Environment.GetEnvironmentVariable("MSBUILDTASKHOST_EXE_NAME"); + try + { + Environment.SetEnvironmentVariable("MSBUILDTASKHOST_EXE_NAME", "asdfghjkl.exe"); + NodeProviderOutOfProcTaskHost.ClearCachedTaskHostPaths(); + + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::DoesTaskHostExist('CLR2', 'CurrentArchitecture'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + // CLR has been forced to pretend not to exist, whether it actually does or not + Assert.IsTrue(String.Equals("false", result, StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDTASKHOST_EXE_NAME", taskHostName); + NodeProviderOutOfProcTaskHost.ClearCachedTaskHostPaths(); + } + } + + + /// + /// Expand property function calls a static bitwise method to retrieve file attribute + /// + [TestMethod] + public void PropertyFunctionStaticMethodFileAttributes() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string tempFile = FileUtilities.GetTemporaryFile(); + try + { + File.SetAttributes(tempFile, FileAttributes.ReadOnly | FileAttributes.Archive); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::BitwiseAnd(32,$([System.IO.File]::GetAttributes(" + tempFile + "))))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual("32", result); + } + finally + { + File.SetAttributes(tempFile, FileAttributes.Normal); + File.Delete(tempFile); + } + } + + /// + /// Expand intrinsic property function calls a static arithmetic method + /// + [TestMethod] + public void PropertyFunctionStaticMethodIntrinsicMaths() + { + PropertyDictionary pg = new PropertyDictionary(); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Add(39.9, 2.1))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((39.9 + 2.1).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Add(40, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((40 + 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Subtract(44, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((44 - 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Subtract(42.9, 0.9))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((42.9 - 0.9).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Multiply(21, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((21 * 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Multiply(84.0, 0.5))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((84.0 * 0.5).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Divide(84, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((84 / 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Divide(84.4, 2.0))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((84.4 / 2.0).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Modulo(85, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((85 % 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Modulo(2345.5, 43))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((2345.5 % 43).ToString(), result); + + // test for overflow wrapping + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::Add(9223372036854775807, 20))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + double expectedResult = 9223372036854775807D + 20D; + Assert.AreEqual(expectedResult.ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::BitwiseOr(40, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((40 | 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::BitwiseAnd(42, 2))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((42 & 2).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::BitwiseXor(213, 255))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((213 ^ 255).ToString(), result); + + result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::BitwiseNot(-43))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual((~-43).ToString(), result); + } + + /// + /// Expand a property reference that has whitespace around the property name (should result in empty) + /// + [TestMethod] + public void PropertySimpleSpaced() + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff")); + + Expander expander = new Expander(pg); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$( SomeStuff )", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(String.Empty, result); + } + + [TestMethod] + public void PropertyFunctionGetRegitryValue() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeProperty", "Value")); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue("Value", "%TEMP%", RegistryValueKind.ExpandString); + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test', '$(SomeProperty)'))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(Environment.GetEnvironmentVariable("TEMP"), result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void PropertyFunctionGetRegitryValueDefault() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeProperty", "Value")); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue(String.Empty, "%TEMP%", RegistryValueKind.ExpandString); + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test', null))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(Environment.GetEnvironmentVariable("TEMP"), result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void PropertyFunctionGetRegistryValueFromView1() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeProperty", "Value")); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue(String.Empty, "%TEMP%", RegistryValueKind.ExpandString); + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test', null, null, RegistryView.Default, RegistryView.Default))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(Environment.GetEnvironmentVariable("TEMP"), result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } + + [TestMethod] + public void PropertyFunctionGetRegistryValueFromView2() + { + try + { + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("SomeProperty", "Value")); + + Expander expander = new Expander(pg); + RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\MSBuild_test"); + + key.SetValue(String.Empty, "%TEMP%", RegistryValueKind.ExpandString); + string result = expander.ExpandIntoStringLeaveEscaped(@"$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\Software\Microsoft\MSBuild_test', null, null, Microsoft.Win32.RegistryView.Default))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + Assert.AreEqual(Environment.GetEnvironmentVariable("TEMP"), result); + } + finally + { + Registry.CurrentUser.DeleteSubKey(@"Software\Microsoft\MSBuild_test"); + } + } /// + /// Expand a property function that references item metadata + /// + [TestMethod] + public void PropertyFunctionConsumingItemMetadata() + { + ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance(); + PropertyDictionary pg = new PropertyDictionary(); + Dictionary itemMetadataTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + itemMetadataTable["Compile.Identity"] = "fOo.Cs"; + StringMetadataTable itemMetadata = new StringMetadataTable(itemMetadataTable); + + List ig = new List(); + pg.Set(ProjectPropertyInstance.Create("SomePath", @"c:\some\path")); + ig.Add(new ProjectItemInstance(project, "Compile", "fOo.Cs", project.FullPath)); + + ItemDictionary itemsByType = new ItemDictionary(); + itemsByType.ImportItems(ig); + + Expander expander = new Expander(pg, itemsByType, itemMetadata); + + string result = expander.ExpandIntoStringLeaveEscaped(@"$([System.IO.Path]::Combine($(SomePath),%(Compile.Identity)))", ExpanderOptions.ExpandAll, MockElementLocation.Instance); + + Assert.AreEqual(@"c:\some\path\fOo.Cs", result); + } + + /// + /// A whole bunch error check tests + /// + [TestMethod] + [Ignore] + // Ignore: Flaky test + public void Medley() + { + // Make absolutely sure that the static method cache hasn't been polluted by the other tests. + AvailableStaticMethods.Reset_ForUnitTestsOnly(); + + PropertyDictionary pg = new PropertyDictionary(); + pg.Set(ProjectPropertyInstance.Create("File", @"foo\file.txt")); + + pg.Set(ProjectPropertyInstance.Create("a", "no")); + pg.Set(ProjectPropertyInstance.Create("b", "true")); + pg.Set(ProjectPropertyInstance.Create("c", "1")); + pg.Set(ProjectPropertyInstance.Create("position", "4")); + pg.Set(ProjectPropertyInstance.Create("d", "xxx")); + pg.Set(ProjectPropertyInstance.Create("e", "xxx")); + pg.Set(ProjectPropertyInstance.Create("and", "and")); + pg.Set(ProjectPropertyInstance.Create("a_semi_b", "a;b")); + pg.Set(ProjectPropertyInstance.Create("a_apos_b", "a'b")); + pg.Set(ProjectPropertyInstance.Create("foo_apos_foo", "foo'foo")); + pg.Set(ProjectPropertyInstance.Create("a_escapedsemi_b", "a%3bb")); + pg.Set(ProjectPropertyInstance.Create("a_escapedapos_b", "a%27b")); + pg.Set(ProjectPropertyInstance.Create("has_trailing_slash", @"foo\")); + pg.Set(ProjectPropertyInstance.Create("emptystring", @"")); + pg.Set(ProjectPropertyInstance.Create("space", @" ")); + pg.Set(ProjectPropertyInstance.Create("listofthings", @"a;b;c;d;e;f;g;h;i;j;k;l")); + pg.Set(ProjectPropertyInstance.Create("input", @"EXPORT a")); + pg.Set(ProjectPropertyInstance.Create("propertycontainingnullasastring", @"null")); + + Expander expander = new Expander(pg); + + string[,] validTests = { + {"$(input.ToString()[1])", "X"}, + {"$(input[1])", "X"}, + {"$(listofthings.Split(';')[$(position)])","e"}, + {@"$([System.Text.RegularExpressions.Regex]::Match($(Input), `EXPORT\s+(.+)`).Groups[1].Value)","a"}, + {"$([MSBuild]::Add(1,2).CompareTo(3))", "0"}, + {"$([MSBuild]::Add(1,2).CompareTo(3))", "0"}, + {"$([MSBuild]::Add(1,2).CompareTo(3.0))", "0"}, + {"$([MSBuild]::Add(1,2).CompareTo('3'))", "0"}, + {"$([MSBuild]::Add(1,2).CompareTo(3.1))", "-1"}, + {"$([MSBuild]::Add(1,2).CompareTo(2))", "1"}, + {"$([MSBuild]::Add(1,2).Equals(3))", "True"}, + {"$([MSBuild]::Add(1,2).Equals(3.0))", "True"}, + {"$([MSBuild]::Add(1,2).Equals('3'))", "True"}, + {"$([MSBuild]::Add(1,2).Equals(3.1))", "False"}, + {"$(a.Insert(0,'%28'))", "%28no"}, + {"$(a.Insert(0,'\"'))", "\"no"}, + {"$(a.Insert(0,'(('))", "%28%28no"}, + {"$(a.Insert(0,'))'))", "%29%29no"}, + {"A$(Reg:A)A", "AA"}, + {"A$(Reg:AA)", "A"}, + {"$(Reg:AA)", ""}, + {"$(Reg:AAAA)", ""}, + {"$(Reg:AAA)", ""}, + {"$([MSBuild]::Add(2,$([System.Convert]::ToInt64('28', 16))))", "42"}, + {"$([MSBuild]::Add(2,$([System.Convert]::ToInt64('28', $([System.Convert]::ToInt32(16))))))", "42"}, + {"$(e.Length.ToString())", "3"}, + {"$(e.get_Length().ToString())", "3"}, + {"$(emptystring.Length)", "0" }, + {"$(space.Length)", "1" }, + {"$([System.TimeSpan]::Equals(null, null))", "True"}, // constant, unquoted null is a special value + {"$([MSBuild]::Add(40,null))", "40"}, + {"$([MSBuild]::Add( 40 , null ))", "40"}, + {"$([MSBuild]::Add(null,40))", "40"}, + {"$([MSBuild]::Escape(';'))", "%3b"}, + {"$([MSBuild]::UnEscape('%3b'))", ";"}, + {"$(e.Substring($(e.Length)))", ""}, + {"$([System.Int32]::MaxValue)", System.Int32.MaxValue.ToString()}, + {"x$()", "x"}, + {"A$(Reg:A)A", "AA"}, + {"A$(Reg:AA)", "A"}, + {"$(Reg:AA)", ""}, + {"$(Reg:AAAA)", ""}, + {"$(Reg:AAA)", ""} + }; + + string[] errorTests = { + "$(input[)", + "$(input.ToString()])", + "$(input.ToString()[)", + "$(input.ToString()[12])", + "$(input[])", + "$(input[-1])", + "$(listofthings.Split(';')[)", + "$(listofthings.Split(';')['goo'])", + "$(listofthings.Split(';')[])", + "$(listofthings.Split(';')[-1])", + "$([]::())", + @" + +$( + +$( + +[System.IO]::Path.GetDirectory('c:\foo\bar\baz.txt') + +).Substring( + +'$([System.IO]::Path.GetPathRoot( + +'$([System.IO]::Path.GetDirectory('c:\foo\bar\baz.txt'))' + +).Length)' + + + +) + +", + "$([Microsoft.VisualBasic.FileIO.FileSystem]::CurrentDirectory)", // not allowed + "$(e.Length..ToString())", + "$(SomeStuff.get_Length(null))", + "$(SomeStuff.Substring((1)))", + "$(b.Substring(-10, $(c)))", + "$(b.Substring(-10, $(emptystring)))", + "$(b.Substring(-10, $(space)))", + "$([MSBuild]::Add.Sub(null,40))", + "$([MSBuild]::Add( ,40))", // empty parameter is empty string + "$([MSBuild]::Add('',40))", // empty quoted parameter is empty string + "$([MSBuild]::Add(40,,,))", + "$([MSBuild]::Add(40, ,,))", + "$([MSBuild]::Add(40,)", + "$([MSBuild]::Add(40,X)", + "$([MSBuild]::Add(40,", + "$([MSBuild]::Add(40", + "$([MSBuild]::Add(,))", // gives "Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true." + "$([System.TimeSpan]::Equals(,))", // empty parameter is interpreted as empty string + "$([System.TimeSpan]::Equals($(space),$(emptystring)))", // empty parameter is interpreted as empty string + "$([System.TimeSpan]::Equals($(emptystring),$(emptystring)))", // empty parameter is interpreted as empty string + "$([MSBuild]::Add($(PropertyContainingNullAsAString),40))", // a property containing the word null is a string "null" + "$([MSBuild]::Add('null',40))", // the word null is a string "null" + "$(SomeStuff.Substring(-10))", + "$(.Length)", + "$(.Substring(1))", + "$(.get_Length())", + "$(e.)", + "$(e..)", + "$(e..Length)", + "$(e$(d).Length)", + "$($(d).Length)", + "$(e`.Length)", + "$([System.IO.Path]Combine::Combine(`a`,`b`))", + "$([System.IO.Path]::Combine((`a`,`b`))", + "$([System.IO.Path]::Combine(`|`,`b`))", + "$([System.IO.Path]Combine(::Combine(`a`,`b`))", + "$([System.IO.Path]Combine(`::Combine(`a`,`b`)`, `b`)`)", + "$([System.IO.Path]::`Combine(`a`, `b`)`)", + "$([System.IO.Path]::(`Combine(`a`, `b`)`))", + "$([System.DateTime]foofoo::Now)", + "$([System.DateTime].Now)", + "$([].Now)", + "$([ ].Now)", + "$([ .Now)", + "$([])", + "$([ )", + "$([ ])", + "$([System.Diagnostics.Process]::Start(`NOTEPAD.EXE`))", + "$([[]]::Start(`NOTEPAD.EXE`))", + "$([(::Start(`NOTEPAD.EXE`))", + "$([Goop]::Start(`NOTEPAD.EXE`))", + "$([System.Threading.Thread]::CurrentThread)", + "$", + "$(", + "$((", + "@", + "@(", + "@()", + "%", + "%(", + "%()", + "exists", + "exists(", + "exists()", + "exists( )", + "exists(,)", + "@(x->'", + "@(x->''", + "@(x-", + "@(x->'x','", + "@(x->'x',''", + "@(x->'x','')", + "-1>x", + "\n", + "\t", + "+-1", + "$(SomeStuff.)", + "$(SomeStuff.!)", + "$(SomeStuff.`)", + "$(SomeStuff.GetType)", + "$(goop.baz`)", + "$(SomeStuff.Substring(HELLO!))", + "$(SomeStuff.ToLowerInvariant()_goop)", + "$(SomeStuff($(System.DateTime.Now)))", + "$(System.Foo.Bar.Lgg)", + "$(SomeStuff.Lgg)", + "$(SomeStuff($(Value)))", + "$(e.$(e.Length))", + "$(e.Substring($(e.Substring(,)))", + "$(e.Substring($(e.Substring(a)))", + "$(e.Substring($([System.IO.Path]::Combine(`a`, `b`))))", + "$([]::())", + "$((((", + "$(Registry:X)", + "$($())", + "$", + "()" + }; + + string result; + for (int i = 0; i < validTests.GetLength(0); i++) + { + result = expander.ExpandIntoStringLeaveEscaped(validTests[i, 0], ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + + if (!String.Equals(result, validTests[i, 1])) + { + string message = "FAILURE: " + validTests[i, 0] + " expanded to '" + result + "' instead of '" + validTests[i, 1] + "'"; + Console.WriteLine(message); + Assert.Fail(message); + } + else + { + Console.WriteLine(validTests[i, 0] + " expanded to '" + result + "'"); + } + } + + for (int i = 0; i < errorTests.GetLength(0); i++) + { + // If an expression is invalid, + // - Expansion may throw InvalidProjectFileException, or + // - return the original unexpanded expression + bool success = true; + bool caughtException = false; + result = String.Empty; + try + { + result = expander.ExpandIntoStringLeaveEscaped(errorTests[i], ExpanderOptions.ExpandProperties, MockElementLocation.Instance); + if (String.Compare(result, errorTests[i]) == 0) + { + Console.WriteLine(errorTests[i] + " did not expand."); + success = false; + } + } + catch (InvalidProjectFileException ex) + { + Console.WriteLine(errorTests[i] + " caused '" + ex.Message + "'"); + caughtException = true; + } + Assert.IsTrue + ( + (success == false || caughtException == true), + "FAILURE: Expected '" + errorTests[i] + "' to not parse or not be evaluated but it evaluated to '" + result + "'" + ); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/ExpressionShredder_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/ExpressionShredder_Tests.cs new file mode 100644 index 00000000000..445137c44ce --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/ExpressionShredder_Tests.cs @@ -0,0 +1,1297 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using System.Xml; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests.Evaluation +{ + /// + /// Compares the items and metadata that ExpressionShredder finds + /// with the results from the old regexes to make sure they're identical + /// in every case. + /// + [TestClass] + public class ExpressionShredder_Tests + { + private string[] _medleyTests = new string[] + { + "a;@(foo,');');b", + "x@(z);@(zz)y", + "exists('@(u)')", + "a;b", + "a;;", + "a", + "@A->'%(x)'", + "@@(", + "@@", + "@(z1234567890_-AZaz->'z1234567890_-AZaz','a1234567890_-AZaz')", + "@(z1234567890_-AZaz,'a1234567890_-AZaz')", + "@(z1234567890_-AZaz)", + "@(z1234567890_-AXZaxz -> '%(a1234567890_-AXZaxz).%(adfas)' )", + "@(z123456.7890_-AXZaxz -> '%(a1234567890_-AXZaxz).%(adfas)' )", + "@(z->'%(x)", + "@(z->%(x)", + "@(z,'%(x)", + "@(z,%(x)", + "@(z) and true", + "@(z%(x)", + "@(z -> '%(filename).z', '$')=='xxx.z$yyy.z'", + "@(z -> '%(filename)', '!')=='xxx!yyy'", + "@(y)==$(d)", + "@(y)<=1", + "@(y -> '%(filename)')=='xxx'", + "@(x\u00DF)", + "@(x1234567890_-AZaz->'x1234567890_-AZaz')", + "@(x1234567890_-AZaz)", + "@(x123 4567890_-AZaz->'x1234567890_-AZaz')", + "@(x->)", + "@(x->)", + "@(x->'x','')", + "@(x->'x',''", + "@(x->'x','", + "@(x->')", + "@(x->''", + "@(x->''", + "@(x->'", + "@(x->", + "@(x-", + "@(x,')", + "@(x)@(x)", + "@(x)0", + "@(nonexistent)", + "@(nonexistent) and true", + "@(foo->'x')", + "@(foo->'abc;def', 'ghi;jkl')", + "@(foo->';());', ';@();')", + "@(foo->';');def;@ghi;", + "@(foo->';')", + "@(foo-->'x')", // "foo-" is a legit item type + "@(foo, ';')", + "@(a1234:567890_-AZaz->'z1234567890_-AZaz')", + "@(a1234567890_-AZaz->'z1234567890_-AZaz')", + "@(a1234567890_-AXZaxz -> 'a1234567890_-AXZaxz' , 'z1234567890_-AXZaxz' )", + "@(a1234567890_-AXZaxz , 'z123%%4567890_-AXZaxz' )", + "@(a->'a')", + "@(a->'a' , 'a')", + "@(a)@(x)!=1", + "@(a)", + "@(a) @(x)!=1", + "@(a , 'a')", + "@(_X->'_X','X')", + "@(_X->'_X')", + "@(_X,'X')", + "@(_X)", + "@(_->'@#$%$%^&*&*)','@#$%$%^&*&*)')", + "@(_->'@#$%$%^&*&*)')", + "@(_,'@#$%$%^&*&*)')", + "@(_)", + "@(\u1234%(x)", + "@(\u00DF)", + "@(Z1234567890_-AZaz)", + "@(Z1234567890_-AZaz -> 'Z1234567890_-AZaz')", + "@(Com:pile)", + "@(Com.pile)", + "@(Com%pile)", + "@(Com pile)", + "@(A1234567890_-AZaz,'!@#$%^&*)(_+'))", + "@(A1234567890_-AZaz)", + "@(A1234567890_-AZaz ->'A1234567890_-AZaz')", + "@(A1234567890_-AZaz ->'A1234567890_-AZaz' , '!@#$%^&*)(_+'))", + "@(A->'foo%(x)bar',',')", + "@(A->'%(x))", + "@(A->'%(x)')@(B->'%(x);%(y)')@(C->'%(z)')", + "@(A->'%(x)');@(B->'%(x);%(y)');;@(C->'%(z)')", + "@(A->'%(x)')", + "@(A->%(x))", + "@(A,'%(x)')", + "@(A, '%(x)->%(y)')", + "@(A, '%(x)%(y)')", + "@(A > '%(x)','+')", + "@(:Z1234567890_-AZaz -> 'Z1234567890_-AZaz')", + "@(:Compile)", + "@(1x->'@#$%$%^&*&*)')", + "@(1Compile)", + "@(1->'a')", + "@(.Compile)", + "@(.A1234567890_-AZaz ->'A1234567890_-AZaz')", + "@(-x->'_X')", + "@(-Compile)", + "@()", + "@() and true", + "@(%Compile)", + "@(%(x)", + "@(", "@()", "@", + "@(", + "@( foo -> ';);' , ';);' )", + "@( foo -> ');' )", + "@( A -> '%(Directory)%(Filename)%(Extension)', ' ** ')", + "@( )", + "@( foo )", + "@( foo ", + "@( a1234567890_-AXZaxz )", + "@", + "@ (x)", + "@(x,'@(y)%(x)@(z->')", + "@(x,'@(y)')", // verify items inside separators aren't found + "@(x,'@(y, '%(z)')')", + "@(x,'@(y)%(z)')", + "@(x,'@(y)%(x')", + "@(x,'')", + "@(x->'','')", + "@(x->'%(z)','')", + ";a;bbb;;c;;", + ";;a", + ";;;@(A->'%(x)');@(B)@(C->'%(y)');%(x)@(D->'%(y)');;", + ";;", + ";", + "; ", + "1<=@(z)", + "1<=@(w)", + "'xxx!yyy'==@(z -> '%(filename)', '!')", + "'@(z)'=='xxx;yyy'", + "'$(e)1@(y)'=='xxx1xxx'", + "'$(c)@(y)'>1", + "%x)", + "%x", + "%(z1234567890_-AZaz.z1234567890_-AZaz)", + "%(z1234567890_-AZaz)", + "%(x1234567890_-AZaz.x1234567890_-AZaz)", + "%(x1234567890_-AZaz)", + "%(x._)", + "%(x)", + "%(x", + "%(x )", + "%(foo.goo.baz)", + "%(foo.goo baz)", + "%(foo goo.rhu barb)", + "%(abc._X)", + "%(a@(z)", + "%(a1234567890_-AXZaxz)", + "%(a12.a)", + "%(a.x)", + "%(a.x )", + "%(a.a@(z)", + "%(a.@(z)", + "%(a. x)", + "%(a)", + "%(a . x)", + "%(_X)", + "%(_)", + "%(Z1234567890_-AZaz.Z1234567890_-AZaz)", + "%(Z1234567890_-AZaz)", + "%(MyType.attr)", + "%(InvalidAttrWithA Space)", + "%(Foo.Bar.)", + "%(Compile.)", + "%(Com:pile.Com:pile)", + "%(Com:pile)", + "%(Com.pile.Com.pile)", + "%(Com%pile.Com%pile)", + "%(Com%pile)", + "%(Com pile.Com pile)", + "%(Com pile)", + "%(A1234567890_-AZaz.A1234567890_-AZaz)", + "%(A1234567890_-AZaz)", + "%(A.x)%(b.x)", + "%(A.x)", + "%(A.x) %( x )", + "%(A.)", + "%(A. )", + "%(A .x)", + "%(A .)", + "%(A . )", + "%(@(z)", + "%(:Compile.:Compile)", + "%(:Compile)", + "%(1Compile.1Compile)", + "%(1Compile)", + "%(.x)", + "%(.x )", + "%(.foo.bar)", + "%(.Compile)", + "%(.)", + "%(. x)", + "%(. x )", + "%(-Compile.-Compile)", + "%(-Compile)", + "%()", + "%(%Compile.%Compile)", + "%(%Compile)", + "%( x)", + "%( MyType . attr )", + "%( A.x)", + "%( A.x )", + "%( A.)", + "%( A .)", + "%( A . x )", + "%( .x)", + "%( . x)", + "%( . x )", + "%( )", + "%( foo )", + "%( Invalid AttrWithASpace )", + "%( A . )", + "%( x )", + "%( a1234567890_-AXZaxz.a1234567890_-AXZaxz )", + "% x", + "% (x)", + "$(c)@(y)>1", + "", + "", + "!@#$%^&*", + " @(foo->'', '')", + " -> ';abc;def;' , 'ghi;jkl' )", + " %(A . x)%%%%%%%%(b . x) ", + " ; a ;b ; ;c", + " $(AssemblyOriginatorKeyFile);\n\t @(Compile);", + "@(_OutputPathItem->'%(FullPath)', ';');$(MSBuildAllProjects);" + }; + + [TestMethod] + public void Medley() + { + foreach (string test in _medleyTests) + { + VerifyExpression(test); + } + } + + [TestMethod] + public void NoOpSplit() + { + VerifySplitSemiColonSeparatedList("a", "a"); + } + + [TestMethod] + public void BasicSplit() + { + VerifySplitSemiColonSeparatedList("a;b", "a", "b"); + } + + [TestMethod] + public void Empty() + { + VerifySplitSemiColonSeparatedList("", null); + } + + [TestMethod] + public void SemicolonOnly() + { + VerifySplitSemiColonSeparatedList(";", null); + } + + [TestMethod] + public void TwoSemicolons() + { + VerifySplitSemiColonSeparatedList(";;", null); + } + + [TestMethod] + public void TwoSemicolonsAndOneEntryAtStart() + { + VerifySplitSemiColonSeparatedList("a;;", "a"); + } + + [TestMethod] + public void TwoSemicolonsAndOneEntryAtEnd() + { + VerifySplitSemiColonSeparatedList(";;a", "a"); + } + + [TestMethod] + public void AtSignAtEnd() + { + VerifySplitSemiColonSeparatedList("@", "@"); + } + + [TestMethod] + public void AtSignParenAtEnd() + { + VerifySplitSemiColonSeparatedList("foo@(", "foo@("); + } + + [TestMethod] + public void EmptyEntriesRemoved() + { + VerifySplitSemiColonSeparatedList(";a;bbb;;c;;", "a", "bbb", "c"); + } + + [TestMethod] + public void EntriesTrimmed() + { + VerifySplitSemiColonSeparatedList(" ; a ;b ; ;c\n; \r; ", "a", "b", "c"); + } + + [TestMethod] + public void NoSplittingOnMacros() + { + VerifySplitSemiColonSeparatedList("@(foo->';')", "@(foo->';')"); + } + + [TestMethod] + public void NoSplittingOnSeparators() + { + VerifySplitSemiColonSeparatedList("@(foo, ';')", "@(foo, ';')"); + } + + [TestMethod] + public void NoSplittingOnSeparatorsAndMacros() + { + VerifySplitSemiColonSeparatedList("@(foo->'abc;def', 'ghi;jkl')", "@(foo->'abc;def', 'ghi;jkl')"); + } + + [TestMethod] + public void CloseParensInMacro() + { + VerifySplitSemiColonSeparatedList("@(foo->');')", "@(foo->');')"); + } + + [TestMethod] + public void CloseParensInSeparator() + { + VerifySplitSemiColonSeparatedList("a;@(foo,');');b", "a", "@(foo,');')", "b"); + } + + [TestMethod] + public void CloseParensInMacroAndSeparator() + { + VerifySplitSemiColonSeparatedList("@(foo->';);', ';);')", "@(foo->';);', ';);')"); + } + + [TestMethod] + public void EmptyQuotesInMacroAndSeparator() + { + VerifySplitSemiColonSeparatedList(" @(foo->'', '')", "@(foo->'', '')"); + } + + [TestMethod] + public void MoreParensAndAtSigns() + { + VerifySplitSemiColonSeparatedList("@(foo->';());', ';@();')", "@(foo->';());', ';@();')"); + } + + [TestMethod] + public void SplittingExceptForMacros() + { + VerifySplitSemiColonSeparatedList("@(foo->';');def;@ghi;", "@(foo->';')", "def", "@ghi"); + } + + // Invalid item expressions shouldn't cause an error in the splitting function. + // The caller will emit an error later when it tries to parse the results. + [TestMethod] + public void InvalidItemExpressions() + { + VerifySplitSemiColonSeparatedList("@(x", "@(x"); + VerifySplitSemiColonSeparatedList("@(x->')", "@(x->')"); + VerifySplitSemiColonSeparatedList("@(x->)", "@(x->)"); + VerifySplitSemiColonSeparatedList("@(x->''", "@(x->''"); + VerifySplitSemiColonSeparatedList("@(x->)", "@(x->)"); + VerifySplitSemiColonSeparatedList("@(x->", "@(x->"); + VerifySplitSemiColonSeparatedList("@(x,')", "@(x,')"); + + // This one doesn't remove the ';' because it thinks it's in + // an item list. This isn't worth tweaking, because the invalid expression is + // going to lead to an error in the caller whether there's a ';' or not. + VerifySplitSemiColonSeparatedList("@(x''';", "@(x''';"); + } + + [TestMethod] + public void RealisticExample() + { + VerifySplitSemiColonSeparatedList("@(_OutputPathItem->'%(FullPath)', ';');$(MSBuildAllProjects);\n @(Compile);\n @(ManifestResourceWithNoCulture);\n $(ApplicationIcon);\n $(AssemblyOriginatorKeyFile);\n @(ManifestNonResxWithNoCultureOnDisk);\n @(ReferencePath);\n @(CompiledLicenseFile);\n @(EmbeddedDocumentation); \n @(CustomAdditionalCompileInputs)", + "@(_OutputPathItem->'%(FullPath)', ';')", "$(MSBuildAllProjects)", "@(Compile)", "@(ManifestResourceWithNoCulture)", "$(ApplicationIcon)", "$(AssemblyOriginatorKeyFile)", "@(ManifestNonResxWithNoCultureOnDisk)", "@(ReferencePath)", "@(CompiledLicenseFile)", "@(EmbeddedDocumentation)", "@(CustomAdditionalCompileInputs)"); + } + + // For reference, this is the authoritative definition of an item expression: + // @"@\(\s* + // (?[\w\x20-]*[\w-]+) + // (?\s*->\s*'(?[^']*)')? + // (?\s*,\s*'(?[^']*)')? + // \s*\)"; + // We need to support any item expressions that satisfy this expression. + // + // Try spaces everywhere that that regex allows spaces: + [TestMethod] + public void SpacingInItemListExpression() + { + VerifySplitSemiColonSeparatedList("@( foo \n -> \t ';abc;def;' , \t 'ghi;jkl' )", "@( foo \n -> \t ';abc;def;' , \t 'ghi;jkl' )"); + } + + /// + /// Helper method for SplitSemiColonSeparatedList tests + /// + /// + /// + private void VerifySplitSemiColonSeparatedList(string input, params string[] expected) + { + IList actual = ExpressionShredder.SplitSemiColonSeparatedList(input); + Console.WriteLine(input); + + if (null == expected) + { + // passing "null" means you expect an empty array back + expected = new string[] { }; + } + + Assert.AreEqual(actual.Count, expected.Length, "Expected " + expected.Length + " items but got " + actual.Count); + + for (int i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], actual[i]); + } + } + + private void VerifyExpression(string test) + { + List list = new List(); + list.Add(test); + ItemsAndMetadataPair pair = ExpressionShredder.GetReferencedItemNamesAndMetadata(list); + + HashSet actualItems = pair.Items; + Dictionary actualMetadata = pair.Metadata; + + HashSet expectedItems = GetConsumedItemReferences_OriginalImplementation(test); + Console.WriteLine("verifying item names..."); + VerifyAgainstCanonicalResults(test, actualItems, expectedItems); + + Dictionary expectedMetadata = GetConsumedMetadataReferences_OriginalImplementation(test); + Console.WriteLine("verifying metadata ..."); + VerifyAgainstCanonicalResults(test, actualMetadata, expectedMetadata); + + Console.WriteLine("===OK==="); + } + + private static void VerifyAgainstCanonicalResults(string test, HashSet actual, HashSet expected) + { + List messages = new List(); + + Console.WriteLine("Expecting " + expected.Count + " distinct values for <" + test + ">"); + + if (actual != null) + { + foreach (string result in actual) + { + if (expected == null || !expected.Contains(result)) + { + messages.Add("Found <" + result + "> in <" + test + "> but it wasn't expected"); + } + } + } + + if (expected != null) + { + foreach (string expect in expected) + { + if (actual == null || !actual.Contains(expect)) + { + messages.Add("Did not find <" + expect + "> in <" + test + ">"); + } + } + } + + if (messages.Count > 0) + { + if (actual != null) + { + Console.Write("FOUND: "); + foreach (string result in actual) + { + Console.Write("<" + result + "> "); + } + Console.WriteLine(); + } + } + + foreach (string message in messages) + { + Console.WriteLine(message); + } + + Assert.IsTrue(messages.Count == 0); + } + + private static void VerifyAgainstCanonicalResults(string test, IDictionary actual, IDictionary expected) + { + List messages = new List(); + + Console.WriteLine("Expecting " + expected.Count + " distinct values for <" + test + ">"); + + if (actual != null) + { + foreach (DictionaryEntry result in actual) + { + if (expected == null || !expected.Contains(result.Key)) + { + messages.Add("Found <" + result.Key + "> in <" + test + "> but it wasn't expected"); + } + } + } + + if (expected != null) + { + foreach (DictionaryEntry expect in expected) + { + if (actual == null || !actual.Contains(expect.Key)) + { + messages.Add("Did not find <" + expect.Key + "> in <" + test + ">"); + } + } + } + + if (messages.Count > 0) + { + if (actual != null) + { + Console.Write("FOUND: "); + foreach (string result in actual.Keys) + { + Console.Write("<" + result + "> "); + } + Console.WriteLine(); + } + } + + foreach (string message in messages) + { + Console.WriteLine(message); + } + + Assert.IsTrue(messages.Count == 0); + } + + [TestMethod] + public void ExtractItemVectorTransform1() + { + string expression = "@(i->'%(Meta0)'->'%(Filename)'->Substring($(Val)))"; + List expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + + ExpressionShredder.ItemExpressionCapture capture = expressions[0]; + + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("i", capture.ItemType); + Assert.AreEqual("%(Meta0)", capture.Captures[0].Value); + Assert.AreEqual("%(Filename)", capture.Captures[1].Value); + Assert.AreEqual("Substring($(Val))", capture.Captures[2].Value); + } + + /// + /// Compare the results of the expression shredder based item expression extractor with the original regex based one + /// NOTE: The medley of tests needs to be parsable by the old regex. This is a regression test against that + /// regex. New expression types should be added in other tests + /// + [TestMethod] + public void ItemExpressionMedleyRegressionTestAgainstOldRegex() + { + List expressions; + + foreach (string expression in _medleyTests) + { + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + MatchCollection matches = s_itemVectorPattern.Matches(expression); + + if (expressions != null) + { + Assert.AreEqual(matches.Count, expressions.Count); + + for (int n = 0; n < matches.Count; n++) + { + Match match = matches[n]; + ExpressionShredder.ItemExpressionCapture capture = expressions[n]; + + Assert.AreEqual(match.Value, capture.Value); + + Group transformGroup = match.Groups["TRANSFORM"]; + + if (capture.Captures != null) + { + for (int i = 0; i < transformGroup.Captures.Count; i++) + { + Assert.AreEqual(transformGroup.Captures[i].Value, capture.Captures[i].Value); + } + } + else + { + Assert.AreEqual(transformGroup.Length, 0); + } + } + } + else + { + Assert.AreEqual(matches.Count, 0); + } + } + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpressionInvalid1() + { + string expression; + List expressions; + + expression = "@(type->'%($(a)), '%'')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + Assert.IsNull(expressions); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression1() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo)"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual(null, capture.Captures); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual(null, capture.Captures); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression2() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + + expression = "@(Foo, ';')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(null, capture.Captures); + Assert.AreEqual(";", capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual(null, capture.Captures); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression3() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + + expression = "@(Foo->'%(Fullpath)')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual("%(Fullpath)", capture.Captures[0].Value); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression4() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Fullpath)',';')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual(";", capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual("%(Fullpath)", capture.Captures[0].Value); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression5() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + + expression = "@(Foo->Bar(a,b))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual("Bar(a,b)", capture.Captures[0].Value); + Assert.AreEqual("Bar", capture.Captures[0].FunctionName); + Assert.AreEqual("a,b", capture.Captures[0].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression6() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->Bar(a,b),';')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual(";", capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual("Bar(a,b)", capture.Captures[0].Value); + Assert.AreEqual("Bar", capture.Captures[0].FunctionName); + Assert.AreEqual("a,b", capture.Captures[0].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression7() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->Metadata('Meta0')->Directory())"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("Metadata('Meta0')", capture.Captures[0].Value); + Assert.AreEqual("Metadata", capture.Captures[0].FunctionName); + Assert.AreEqual("'Meta0'", capture.Captures[0].FunctionArguments); + Assert.AreEqual("Directory()", capture.Captures[1].Value); + Assert.AreEqual("Directory", capture.Captures[1].FunctionName); + Assert.AreEqual(null, capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression8() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->Metadata('Meta0')->Directory())"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("Metadata('Meta0')", capture.Captures[0].Value); + Assert.AreEqual("Metadata", capture.Captures[0].FunctionName); + Assert.AreEqual("'Meta0'", capture.Captures[0].FunctionArguments); + Assert.AreEqual("Directory()", capture.Captures[1].Value); + Assert.AreEqual("Directory", capture.Captures[1].FunctionName); + Assert.AreEqual(null, capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression9() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Fullpath)'->Directory(), '|')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual("|", capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Fullpath)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Directory()", capture.Captures[1].Value); + Assert.AreEqual("Directory", capture.Captures[1].FunctionName); + Assert.AreEqual(null, capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression10() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Fullpath)'->Directory(),';')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(";", capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Fullpath)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Directory()", capture.Captures[1].Value); + Assert.AreEqual("Directory", capture.Captures[1].FunctionName); + Assert.AreEqual(null, capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression11() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'$(SOMEPROP)%(Fullpath)')"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(1, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("$(SOMEPROP)%(Fullpath)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression12() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring($(Val), $(Boo)))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring($(Val), $(Boo))", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("$(Val), $(Boo)", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression13() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(\"AA\", 'BB', `cc`))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(\"AA\", 'BB', `cc`)", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("\"AA\", 'BB', `cc`", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression14() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring('()', $(Boo), ')('))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring('()', $(Boo), ')(')", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("'()', $(Boo), ')('", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression15() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(`()`, $(Boo), \"AA\"))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(`()`, $(Boo), \"AA\")", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("`()`, $(Boo), \"AA\"", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression16() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(`()`, $(Boo), \")(\"))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(`()`, $(Boo), \")(\")", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("`()`, $(Boo), \")(\"", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsSingleExpression17() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(\"()\", $(Boo), `)(`))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(1, expressions.Count); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(\"()\", $(Boo), `)(`)", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("\"()\", $(Boo), `)(`", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsMultipleExpression1() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Bar);@(Foo->'%(Filename)'->Substring(\"()\", $(Boo), `)(`))"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[1]; + Assert.AreEqual(2, expressions.Count); + Assert.AreEqual("Bar", expressions[0].ItemType); + Assert.AreEqual(null, expressions[0].Captures); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(\"()\", $(Boo), `)(`)", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("\"()\", $(Boo), `)(`", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsMultipleExpression2() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(\"()\", $(Boo), `)(`));@(Bar)"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(2, expressions.Count); + Assert.AreEqual("Bar", expressions[1].ItemType); + Assert.AreEqual(null, expressions[1].Captures); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(\"()\", $(Boo), `)(`)", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("\"()\", $(Boo), `)(`", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsMultipleExpression3() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(\"()\", $(Boo), `)(`));AAAAAA;@(Bar)"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(2, expressions.Count); + Assert.AreEqual("Bar", expressions[1].ItemType); + Assert.AreEqual(null, expressions[1].Captures); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(\"()\", $(Boo), `)(`)", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("\"()\", $(Boo), `)(`", capture.Captures[1].FunctionArguments); + } + + [TestMethod] + public void ExtractItemVectorExpressionsMultipleExpression4() + { + string expression; + List expressions; + ExpressionShredder.ItemExpressionCapture capture; + + expression = "@(Foo->'%(Filename)'->Substring(\"()\", $(Boo), `)(\"`));@(;);@(aaa->;b);@(bbb->'d);@(`Foo->'%(Filename)'->Distinct());@(Bar)"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + capture = expressions[0]; + Assert.AreEqual(2, expressions.Count); + Assert.AreEqual("Bar", expressions[1].ItemType); + Assert.AreEqual(null, expressions[1].Captures); + Assert.AreEqual(2, capture.Captures.Count); + Assert.AreEqual(null, capture.Separator); + Assert.AreEqual("Foo", capture.ItemType); + Assert.AreEqual("%(Filename)", capture.Captures[0].Value); + Assert.AreEqual(null, capture.Captures[0].FunctionName); + Assert.AreEqual(null, capture.Captures[0].FunctionArguments); + Assert.AreEqual("Substring(\"()\", $(Boo), `)(\"`)", capture.Captures[1].Value); + Assert.AreEqual("Substring", capture.Captures[1].FunctionName); + Assert.AreEqual("\"()\", $(Boo), `)(\"`", capture.Captures[1].FunctionArguments); + } + + + [TestMethod] + public void ExtractItemVectorExpressionsMultipleExpression5() + { + string expression; + List expressions; + + expression = "@(foo);@(foo,'-');@(foo);@(foo,',');@(foo)"; + expressions = ExpressionShredder.GetReferencedItemExpressions(expression); + Assert.AreEqual(5, expressions.Count); + Assert.AreEqual("foo", expressions[0].ItemType); + Assert.AreEqual(null, expressions[0].Separator); + + Assert.AreEqual("foo", expressions[1].ItemType); + Assert.AreEqual("-", expressions[1].Separator); + + Assert.AreEqual("foo", expressions[2].ItemType); + Assert.AreEqual(null, expressions[2].Separator); + + Assert.AreEqual("foo", expressions[3].ItemType); + Assert.AreEqual(",", expressions[3].Separator); + + Assert.AreEqual("foo", expressions[4].ItemType); + Assert.AreEqual(null, expressions[4].Separator); + } + + #region Original code to produce canonical results + + /// + /// Looks through the parameters of the batchable object, and finds all referenced item lists. + /// Returns a hashtable containing the item lists, where the key is the item name, and the + /// value is always String.Empty (not used). + /// + private static HashSet GetConsumedItemReferences_OriginalImplementation(string expression) + { + HashSet result = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (Match itemVector in s_itemVectorPattern.Matches(expression)) + { + result.Add(itemVector.Groups["TYPE"].Value); + } + + return result; + } + + /// + /// Looks through the parameters of the batchable object, and finds all references to item metadata + /// (that aren't part of an item transform). Returns a Hashtable containing a bunch of MetadataReference + /// structs. Each reference to item metadata may or may not be qualified with an item name (e.g., + /// %(Culture) vs. %(EmbeddedResource.Culture). + /// + /// Hashtable containing the metadata references. + private static Dictionary GetConsumedMetadataReferences_OriginalImplementation(string expression) + { + // The keys in the hash table are the qualified metadata names (e.g. "EmbeddedResource.Culture" + // or just "Culture"). The values are MetadataReference structs, which simply split out the item + // name (possibly null) and the actual metadata name. + Dictionary consumedMetadataReferences = new Dictionary(StringComparer.OrdinalIgnoreCase); + + FindEmbeddedMetadataReferences_OriginalImplementation(expression, consumedMetadataReferences); + + return consumedMetadataReferences; + } + + /// + /// Looks through a single parameter of the batchable object, and finds all references to item metadata + /// (that aren't part of an item transform). Populates a Hashtable containing a bunch of MetadataReference + /// structs. Each reference to item metadata may or may not be qualified with an item name (e.g., + /// %(Culture) vs. %(EmbeddedResource.Culture). + /// + /// + /// + private static void FindEmbeddedMetadataReferences_OriginalImplementation + ( + string batchableObjectParameter, + Dictionary consumedMetadataReferences + ) + { + MatchCollection embeddedMetadataReferences = FindEmbeddedMetadataReferenceMatches_OriginalImplementation(batchableObjectParameter); + + if (embeddedMetadataReferences != null) + { + foreach (Match embeddedMetadataReference in embeddedMetadataReferences) + { + string metadataName = embeddedMetadataReference.Groups["NAME"].Value; + string qualifiedMetadataName = metadataName; + + // Check if the metadata is qualified with the item name. + string itemName = null; + if (embeddedMetadataReference.Groups["ITEM_SPECIFICATION"].Length > 0) + { + itemName = embeddedMetadataReference.Groups["TYPE"].Value; + qualifiedMetadataName = itemName + "." + metadataName; + } + + consumedMetadataReferences[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); + } + } + } + + // the leading characters that indicate the start of an item vector + private const string itemVectorPrefix = "@("; + + // complete description of an item vector, including the optional transform expression and separator specification + private const string itemVectorSpecification = + @"@\(\s* + (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") + (?\s*->\s*'(?[^']*)')? + (?\s*,\s*'(?[^']*)')? + \s*\)"; + + // description of an item vector, including the optional transform expression, but not the separator specification + private const string itemVectorWithoutSeparatorSpecification = + @"@\(\s* + (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") + (?\s*->\s*'(?[^']*)')? + \s*\)"; + + // regular expression used to match item vectors, including those embedded in strings + private static readonly Regex s_itemVectorPattern = new Regex(itemVectorSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + + // regular expression used to match a list of item vectors that have no separator specification -- the item vectors + // themselves may be optionally separated by semi-colons, or they might be all jammed together + private static readonly Regex s_listOfItemVectorsWithoutSeparatorsPattern = + new Regex(@"^\s*(;\s*)*(" + + itemVectorWithoutSeparatorSpecification + + @"\s*(;\s*)*)+$", + RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + + // the leading characters that indicate the start of an item metadata reference + private const string itemMetadataPrefix = "%("; + + // complete description of an item metadata reference, including the optional qualifying item type + private const string itemMetadataSpecification = + @"%\(\s* + (?(?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @")\s*\.\s*)? + (?" + ProjectWriter.itemTypeOrMetadataNameSpecification + @") + \s*\)"; + + // regular expression used to match item metadata references embedded in strings + private static readonly Regex s_itemMetadataPattern = new Regex(itemMetadataSpecification, RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); + + // description of an item vector with a transform, split into two halves along the transform expression + private const string itemVectorWithTransformLHS = @"@\(\s*" + ProjectWriter.itemTypeOrMetadataNameSpecification + @"\s*->\s*'[^']*"; + private const string itemVectorWithTransformRHS = @"[^']*'(\s*,\s*'[^']*')?\s*\)"; + + // PERF WARNING: this Regex is complex and tends to run slowly + // regular expression used to match item metadata references outside of item vector expressions + private static readonly Regex s_nonTransformItemMetadataPattern = + new Regex(@"((?<=" + itemVectorWithTransformLHS + @")" + itemMetadataSpecification + @"(?!" + itemVectorWithTransformRHS + @")) | + ((? + /// Looks through a single parameter of the batchable object, and finds all references to item metadata + /// (that aren't part of an item transform). Populates a MatchCollection object with any regex matches + /// found in the input. Each reference to item metadata may or may not be qualified with an item name (e.g., + /// %(Culture) vs. %(EmbeddedResource.Culture). + ///
+ /// + private static MatchCollection FindEmbeddedMetadataReferenceMatches_OriginalImplementation(string batchableObjectParameter) + { + MatchCollection embeddedMetadataReferences = null; + + // PERF NOTE: Regex matching is expensive, so if the string doesn't contain any item attribute references, just bail + // out -- pre-scanning the string is actually cheaper than running the Regex, even when there are no matches! + + if (batchableObjectParameter.IndexOf(itemMetadataPrefix, StringComparison.Ordinal) != -1) + { + // if there are no item vectors in the string + if (batchableObjectParameter.IndexOf(itemVectorPrefix, StringComparison.Ordinal) == -1) + { + // run a simpler Regex to find item metadata references + embeddedMetadataReferences = s_itemMetadataPattern.Matches(batchableObjectParameter); + } + // PERF NOTE: this is a highly targeted optimization for a common pattern observed during profiling + // if the string is a list of item vectors with no separator specifications + else if (s_listOfItemVectorsWithoutSeparatorsPattern.IsMatch(batchableObjectParameter)) + { + // then even if the string contains item metadata references, those references will only be inside transform + // expressions, and can be safely skipped + embeddedMetadataReferences = null; + } + else + { + // otherwise, run the more complex Regex to find item metadata references not contained in expressions + embeddedMetadataReferences = s_nonTransformItemMetadataPattern.Matches(batchableObjectParameter); + } + } + + return embeddedMetadataReferences; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/Preprocessor_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/Preprocessor_Tests.cs new file mode 100644 index 00000000000..3bf4d252b53 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/Preprocessor_Tests.cs @@ -0,0 +1,833 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for preprocessor +//----------------------------------------------------------------------- + +using System; +using System.Xml; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using System.IO; + +namespace Microsoft.Build.UnitTests.Preprocessor +{ + /// + /// Tests mainly for project preprocessing + /// + [TestClass] + public class Preprocessor_Tests + { + /// + /// Clear out the cache + /// + [TestInitialize] + public void Setup() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + /// + /// Clear out the cache + /// + [TestCleanup] + public void Teardown() + { + Setup(); + } + + /// + /// Basic project + /// + [TestMethod] + public void Single() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + +

v1

+
+
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// InitialTargets are concatenated, outermost to innermost + /// + [TestMethod] + public void InitialTargetsOuterAndInner() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.InitialTargets = "i1"; + xml1.AddImport("p2"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.InitialTargets = "i2"; + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + +"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// InitialTargets are concatenated, outermost to innermost + /// + [TestMethod] + public void InitialTargetsInnerOnly() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddImport("p2"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.InitialTargets = "i2"; + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + +"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// InitialTargets are concatenated, outermost to innermost + /// + [TestMethod] + public void InitialTargetsOuterOnly() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.InitialTargets = "i1"; + xml1.AddImport("p2"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + +"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Basic empty project importing another + /// + [TestMethod] + public void TwoFirstEmpty() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddImport("p2"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.AddProperty("p", "v2"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + +

v2

+
+ +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// False import should not be followed + /// + [TestMethod] + public void FalseImport() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddProperty("p", "v1"); + xml1.AddImport("p2").Condition = "false"; + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.AddProperty("p", "v2"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + +

v1

+
+ +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Basic project importing another empty one + /// + [TestMethod] + public void TwoSecondEmpty() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddProperty("p", "v"); + xml1.AddImport("p2"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + +

v

+
+ + +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Basic project importing another + /// + [TestMethod] + public void TwoWithContent() + { + string one = ObjectModelHelpers.CleanupFileContents( + @" + + +

v0

+
+ + +

v2

+
+
"); + + string two = ObjectModelHelpers.CleanupFileContents( + @" + + +

v1

+
+
"); + ProjectRootElement twoXml = ProjectRootElement.Create(XmlReader.Create(new StringReader(two))); + twoXml.FullPath = "p2"; + + Project project = new Project(XmlTextReader.Create(new StringReader(one))); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + +

v0

+
+ + +

v1

+
+ + +

v2

+
+
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Basic project importing another one via an ImportGroup + /// + [TestMethod] + public void ImportGroup() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddProperty("p", "v1"); + xml1.AddImportGroup().AddImport("p2"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.AddProperty("p", "v2"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + +

v1

+
+ + + +

v2

+
+ + +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Basic project importing another one via an ImportGroup with two imports inside it, and a condition on it + /// + [TestMethod] + public void ImportGroupDoubleChildPlusCondition() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddProperty("p", "v1"); + ProjectImportGroupElement group = xml1.AddImportGroup(); + group.Condition = "true"; + group.AddImport("p2"); + group.AddImport("p3"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.AddProperty("p", "v2"); + ProjectRootElement xml3 = ProjectRootElement.Create("p3"); + xml3.AddProperty("p", "v3"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + +

v1

+
+ + + +

v2

+
+ + + +

v3

+
+ + +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// First DefaultTargets encountered is used + /// + [TestMethod] + public void DefaultTargetsOuterAndInner() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddImport("p2"); + xml1.AddImport("p3"); + xml1.DefaultTargets = "d1"; + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.DefaultTargets = "d2"; + ProjectRootElement xml3 = ProjectRootElement.Create("p3"); + xml3.DefaultTargets = "d3"; + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + +"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// First DefaultTargets encountered is used + /// + [TestMethod] + public void DefaultTargetsInnerOnly() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddImport("p2"); + xml1.AddImport("p3"); + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.DefaultTargets = "d2"; + ProjectRootElement xml3 = ProjectRootElement.Create("p3"); + xml3.DefaultTargets = "d3"; + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + +"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Basic project importing another one via an ImportGroup, but the ImportGroup condition is false + /// + [TestMethod] + public void ImportGroupFalseCondition() + { + ProjectRootElement xml1 = ProjectRootElement.Create("p1"); + xml1.AddProperty("p", "v1"); + xml1.AddImportGroup().AddImport("p2"); + xml1.LastChild.Condition = "false"; + ProjectRootElement xml2 = ProjectRootElement.Create("p2"); + xml2.AddProperty("p", "v2"); + + Project project = new Project(xml1); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + +

v1

+
+ + + +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Import has a wildcard expression + /// + [TestMethod] + public void ImportWildcard() + { + string directory = null; + ProjectRootElement xml0, xml1 = null, xml2 = null, xml3 = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(directory); + + xml0 = ProjectRootElement.Create("p1"); + xml0.AddImport(directory + "\\*.targets"); + + xml1 = ProjectRootElement.Create(directory + "\\1.targets"); + xml1.AddProperty("p", "v1"); + xml1.Save(); + + xml2 = ProjectRootElement.Create(directory + "\\2.targets"); + xml2.AddProperty("p", "v2"); + xml2.Save(); + + xml3 = ProjectRootElement.Create(directory + "\\3.xxxxxx"); + xml3.AddProperty("p", "v3"); + xml3.Save(); + + Project project = new Project(xml0); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + +

v1

+
+ + + +

v2

+
+ +
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + finally + { + File.Delete(xml1.FullPath); + File.Delete(xml2.FullPath); + File.Delete(xml3.FullPath); + Directory.Delete(directory); + } + } + + /// + /// CDATA node type cloned correctly + /// + [TestMethod] + public void CData() + { + Project project = new Project(); + project.SetProperty("p", "John Smith]]>"); + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + +

John Smith]]>

+
+
"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + + /// + /// Metadata named "Project" should not confuse it.. + /// + [TestMethod] + public void ProjectMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + {3699f81b-2d03-46c5-abd7-e88a4c946f28} + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(xml); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + + {3699f81b-2d03-46c5-abd7-e88a4c946f28} + + +"); + + Helpers.VerifyAssertLineByLine(expected, writer.ToString()); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs new file mode 100644 index 00000000000..c9abb00b56f --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for ProjectRootElementCache +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using System.Collections; +using System; +using Microsoft.Build.Construction; +using System.IO; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.OM.Evaluation +{ + /// + /// Tests for ProjectRootElementCache + /// + [TestClass] + public class ProjectRootElementCache_Tests + { + /// + /// Set up the test + /// + [TestInitialize] + public void SetUp() + { + // Empty the cache + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + /// + /// Tear down the test + /// + [TestCleanup] + public void TearDown() + { + // Empty the cache + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + /// + /// Verifies that a null entry fails + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void AddNull() + { + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => null, true); + } + + /// + /// Verifies that the delegate cannot return a project with a different path + /// + [TestMethod] + [ExpectedException(typeof(InternalErrorException))] + public void AddUnsavedProject() + { + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => ProjectRootElement.Create("c:\\bar"), true); + } + + /// + /// Tests that an entry added to the cache can be retrieved. + /// + [TestMethod] + public void AddEntry() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create("c:\\foo"); + ProjectRootElement projectRootElement2 = ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => { throw new InvalidOperationException(); }, true); + + Assert.AreSame(projectRootElement, projectRootElement2); + } + + /// + /// Tests that a strong reference is held to a single item + /// + [TestMethod] + public void AddEntryStrongReference() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create("c:\\foo"); + + projectRootElement = null; + GC.Collect(); + + projectRootElement = ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => { throw new InvalidOperationException(); }, true); + + Assert.IsNotNull(projectRootElement); + + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.DiscardStrongReferences(); + projectRootElement = null; + GC.Collect(); + + Assert.IsNull(ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.TryGet("c:\\foo")); + } + + /// + /// Tests that only a limited number of strong references are held + /// + [TestMethod] + [Ignore] // "This test seems to be flaky depending on when garbage collection happened" + public void AddManyEntriesNotAllStrongReferences() + { + List paths = new List(55); + for (int i = 0; i < 55; i++) + { + paths.Add(Path.Combine("c:\\", i.ToString())); + } + + for (int i = 0; i < paths.Count; i++) + { + ProjectRootElement.Create(paths[i]); + } + + GC.Collect(); + + // Boost one + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get(paths[2], (p, c) => null, true); + + GC.Collect(); + + // Should have only indexes 6 through 54 remaining, except #2 which got boosted + for (int i = 0; i < 6; i++) + { + if (i != 2) + { + Assert.IsNull(ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.TryGet(paths[i]), "expected " + i + " to not be in cache"); + } + } + + for (int i = 2; i < 55; i++) + { + if (i > 5 || i == 2) + { + Assert.IsNotNull(ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.TryGet(paths[i])); + } + } + + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.DiscardStrongReferences(); + + GC.Collect(); + + for (int i = 0; i < 55; i++) + { + Assert.IsNull(ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.TryGet(paths[i])); + } + } + + /// + /// Cache should not return a ProjectRootElement if the file it was loaded from has since changed - + /// if the cache was configured to auto-reload. + /// + [TestMethod] + public void GetProjectRootElementChangedOnDisk1() + { + string path = null; + + try + { + ProjectRootElementCache cache = new ProjectRootElementCache(true /* auto reload from disk */); + + path = FileUtilities.GetTemporaryFile(); + + ProjectRootElement xml0 = ProjectRootElement.Create(path); + xml0.Save(); + + cache.AddEntry(xml0); + + ProjectRootElement xml1 = cache.TryGet(path); + Assert.AreEqual(true, Object.ReferenceEquals(xml0, xml1)); + + File.SetLastWriteTime(path, DateTime.Now + new TimeSpan(1, 0, 0)); + + ProjectRootElement xml2 = cache.TryGet(path); + Assert.AreEqual(false, Object.ReferenceEquals(xml0, xml2)); + } + finally + { + File.Delete(path); + } + } + + /// + /// Cache should return a ProjectRootElement directly even if the file it was loaded from has since changed - + /// if the cache was configured to NOT auto-reload. + /// + [TestMethod] + public void GetProjectRootElementChangedOnDisk2() + { + string path = null; + + try + { + ProjectRootElementCache cache = new ProjectRootElementCache(false /* do not auto reload from disk */); + + path = FileUtilities.GetTemporaryFile(); + + ProjectRootElement xml0 = ProjectRootElement.Create(path); + xml0.Save(); + + cache.AddEntry(xml0); + + ProjectRootElement xml1 = cache.TryGet(path); + Assert.AreEqual(true, Object.ReferenceEquals(xml0, xml1)); + + File.SetLastWriteTime(path, DateTime.Now + new TimeSpan(1, 0, 0)); + + ProjectRootElement xml2 = cache.TryGet(path); + Assert.AreEqual(true, Object.ReferenceEquals(xml0, xml2)); + } + finally + { + File.Delete(path); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectStringCache_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectStringCache_Tests.cs new file mode 100644 index 00000000000..1396dbc12b6 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectStringCache_Tests.cs @@ -0,0 +1,433 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for ProjectStringCache +//----------------------------------------------------------------------- + +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.OM.Evaluation +{ + /// + /// Tests for ProjectStringCache + /// + [TestClass] + public class ProjectStringCache_Tests + { + /// + /// Test that loading two instances of the same xml file uses the same strings + /// to store read values. + /// + [TestMethod] + public void ContentIsSameAcrossInstances() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Item group content + + + "); + + string path = FileUtilities.GetTemporaryFile(); + + try + { + File.WriteAllText(path, content); + + ProjectStringCache cache = new ProjectStringCache(); + XmlDocumentWithLocation document1 = new XmlDocumentWithLocation(); + document1.StringCache = cache; + document1.Load(path); + + XmlDocumentWithLocation document2 = new XmlDocumentWithLocation(); + document2.StringCache = cache; + document2.Load(path); + + XmlNodeList nodes1 = document1.GetElementsByTagName("ItemGroup"); + XmlNodeList nodes2 = document2.GetElementsByTagName("ItemGroup"); + + Assert.AreEqual(1, nodes1.Count); + Assert.AreEqual(1, nodes2.Count); + + XmlNode node1 = nodes1[0].FirstChild; + XmlNode node2 = nodes2[0].FirstChild; + + Assert.IsNotNull(node1); + Assert.IsNotNull(node2); + Assert.AreNotSame(node1, node2); + Assert.AreSame(node1.Value, node2.Value); + } + finally + { + File.Delete(path); + } + } + + /// + /// Test that modifying one instance of a file does not affect the other file. + /// + [TestMethod] + public void ContentCanBeModified() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + Item group content + + + "); + + string path = FileUtilities.GetTemporaryFile(); + + try + { + File.WriteAllText(path, content); + ProjectStringCache cache = new ProjectStringCache(); + XmlDocumentWithLocation document1 = new XmlDocumentWithLocation(); + document1.StringCache = cache; + document1.Load(path); + + XmlDocumentWithLocation document2 = new XmlDocumentWithLocation(); + document2.StringCache = cache; + document2.Load(path); + + string outerXml1 = document1.OuterXml; + string outerXml2 = document2.OuterXml; + Assert.AreEqual(outerXml1, outerXml2); + + XmlNodeList nodes1 = document1.GetElementsByTagName("ItemGroup"); + XmlNodeList nodes2 = document2.GetElementsByTagName("ItemGroup"); + + Assert.AreEqual(1, nodes1.Count); + Assert.AreEqual(1, nodes2.Count); + + XmlNode node1 = nodes1[0]; + XmlNode node2 = nodes2[0]; + Assert.IsNotNull(node1); + Assert.IsNotNull(node2); + Assert.AreNotSame(node1, node2); + Assert.AreEqual(1, node1.Attributes.Count); + Assert.AreEqual(1, node2.Attributes.Count); + Assert.AreSame(node1.Attributes[0].Value, node2.Attributes[0].Value); + + node2.Attributes[0].Value = "attr1value"; + Assert.AreEqual(node1.Attributes[0].Value, node2.Attributes[0].Value); + Assert.AreNotSame(node1.Attributes[0].Value, node2.Attributes[0].Value); + + node1 = nodes1[0].FirstChild; + node2 = nodes2[0].FirstChild; + Assert.AreNotSame(node1, node2); + Assert.AreSame(node1.Value, node2.Value); + + XmlText newText = document2.CreateTextNode("New Value"); + XmlNode parent = node2.ParentNode; + parent.ReplaceChild(newText, node2); + + Assert.AreNotEqual(outerXml1, document2.OuterXml); + } + finally + { + File.Delete(path); + } + } + + /// + /// Test that unloading a project file makes its string entries disappear from + /// the string cache. + /// + [TestMethod] + public void RemovingFilesRemovesEntries() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + Content + + "); + + string path = FileUtilities.GetTemporaryFile(); + + try + { + File.WriteAllText(path, content); + + ProjectStringCache cache = new ProjectStringCache(); + ProjectCollection collection = new ProjectCollection(); + int entryCount; + + ProjectRootElement pre1 = ProjectRootElement.Create(collection); + pre1.XmlDocument.StringCache = cache; + pre1.FullPath = path; + pre1.XmlDocument.Load(path); + entryCount = cache.Count; + Assert.IsTrue(entryCount > 0); + + ProjectRootElement pre2 = ProjectRootElement.Create(collection); + pre2.XmlDocument.StringCache = cache; + pre2.FullPath = path; + pre2.XmlDocument.Load(path); + + // Entry count should not have changed + Assert.AreEqual(entryCount, cache.Count); + + string itemGroupContent = cache.Get("Content"); + Assert.IsNotNull(itemGroupContent); + + XmlNodeList nodes1 = pre1.XmlDocument.GetElementsByTagName("ItemGroup"); + XmlNodeList nodes2 = pre2.XmlDocument.GetElementsByTagName("ItemGroup"); + + Assert.AreEqual(1, nodes1.Count); + Assert.AreEqual(1, nodes2.Count); + + XmlNode node1 = nodes1[0]; + XmlNode node2 = nodes2[0]; + Assert.IsNotNull(node1); + Assert.IsNotNull(node2); + Assert.AreNotSame(node1, node2); + Assert.AreSame(node1.Value, node2.Value); + + // Now remove one document + collection.UnloadProject(pre1); + + // We should still be able to get Content + itemGroupContent = cache.Get("Content"); + Assert.IsNotNull(itemGroupContent); + + // Now remove the second document + collection.UnloadProject(pre2); + + // Now we should not be able to get Content + itemGroupContent = cache.Get("Content"); + Assert.IsNull(itemGroupContent); + + // And there should be no entries + Assert.AreEqual(0, cache.Count); + } + finally + { + File.Delete(path); + } + } + + /// + /// Adding a string equivalent to an existing instance and under the same document should + /// return the existing instance. + /// + [TestMethod] + public void AddReturnsSameInstanceForSameDocument() + { + ProjectStringCache cache = new ProjectStringCache(); + + XmlDocument document = new XmlDocument(); + + string stringToAdd = "Test1"; + string return1 = cache.Add(stringToAdd, document); + + // Content of string should be the same. + Assert.AreEqual(1, cache.Count); + Assert.AreEqual(stringToAdd, return1); + + // Build a new string guaranteed not to be optimized by the compiler into the same instance. + StringBuilder builder = new StringBuilder(); + builder.Append("Test"); + builder.Append("1"); + + string return2 = cache.Add(builder.ToString(), document); + + // Content of string should be the same. + Assert.AreEqual(builder.ToString(), return2); + + // Returned references should be the same + Assert.AreSame(return1, return2); + + // Should not have added any new string instances to the cache. + Assert.AreEqual(1, cache.Count); + } + + /// + /// Adding a string equivalent to an existing instance but under a different document + /// should return the existing instance. + /// + [TestMethod] + public void AddReturnsSameInstanceForDifferentDocument() + { + ProjectStringCache cache = new ProjectStringCache(); + + XmlDocument document = new XmlDocument(); + + string stringToAdd = "Test1"; + string return1 = cache.Add(stringToAdd, document); + + // Content of string should be the same. + Assert.AreEqual(stringToAdd, return1); + + // Build a new string guaranteed not to be optimized by the compiler into the same instance. + StringBuilder builder = new StringBuilder(); + builder.Append("Test"); + builder.Append("1"); + XmlDocument document2 = new XmlDocument(); + + string return2 = cache.Add(builder.ToString(), document2); + + // Content of string should be the same. + Assert.AreEqual(builder.ToString(), return2); + + // Returned references should be the same + Assert.AreSame(return1, return2); + + // Should not have added any new string instances to the cache. + Assert.AreEqual(1, cache.Count); + } + + /// + /// Removing the last document containing an instance of a string should remove the string entry. + /// A subsequent add should then return a different instance. + /// + /// + /// WHITEBOX ASSUMPTION: + /// The following method assumes knowledge of the ProjectStringCache internal implementation + /// details, and may become invalid if those details change. + /// + [TestMethod] + public void RemoveLastInstanceDeallocatesEntry() + { + ProjectStringCache cache = new ProjectStringCache(); + + XmlDocument document = new XmlDocument(); + + string stringToAdd = "Test1"; + string return1 = cache.Add(stringToAdd, document); + + cache.Clear(document); + + // Should be no instances left. + Assert.AreEqual(0, cache.Count); + + // Build a new string guaranteed not to be optimized by the compiler into the same instance. + StringBuilder builder = new StringBuilder(); + builder.Append("Test"); + builder.Append("1"); + XmlDocument document2 = new XmlDocument(); + + string return2 = cache.Add(builder.ToString(), document2); + + // Returned references should NOT be the same + Assert.AreNotSame(return1, return2); + } + + /// + /// Removing one document containing a string which already existed in the collection + /// should still leave a reference in the collection, so that a subsequent add will + /// return the existing reference. + /// + [TestMethod] + public void RemoveOneInstance() + { + ProjectStringCache cache = new ProjectStringCache(); + + XmlDocument document = new XmlDocument(); + + string stringToAdd = "Test1"; + string return1 = cache.Add(stringToAdd, document); + Assert.AreEqual(1, cache.Count); + + XmlDocument document2 = new XmlDocument(); + string return2 = cache.Add(stringToAdd, document2); + Assert.AreEqual(1, cache.Count); + + cache.Clear(document2); + + // Since there is still one document referencing the string, it should remain. + Assert.AreEqual(1, cache.Count); + + // Build a new string guaranteed not to be optimized by the compiler into the same instance. + StringBuilder builder = new StringBuilder(); + builder.Append("Test"); + builder.Append("1"); + XmlDocument document3 = new XmlDocument(); + + string return3 = cache.Add(builder.ToString(), document3); + + // Returned references should be the same + Assert.AreSame(return1, return3); + + // Still should only be one cached instance. + Assert.AreEqual(1, cache.Count); + } + + /// + /// Different strings should get their own entries. + /// + [TestMethod] + public void DifferentStringsSameDocument() + { + ProjectStringCache cache = new ProjectStringCache(); + + XmlDocument document = new XmlDocument(); + + string stringToAdd = "Test1"; + string return1 = cache.Add(stringToAdd, document); + Assert.AreEqual(1, cache.Count); + + stringToAdd = "Test2"; + string return2 = cache.Add(stringToAdd, document); + + // The second string gets its own instance. + Assert.AreEqual(2, cache.Count); + + // Build a new string guaranteed not to be optimized by the compiler into the same instance. + StringBuilder builder = new StringBuilder(); + builder.Append("Test"); + builder.Append("2"); + string return3 = cache.Add(builder.ToString(), document); + + // The new string should be the same as the other one already in the collection. + Assert.AreSame(return2, return3); + + // No new instances for string with the same content. + Assert.AreEqual(2, cache.Count); + } + + /// + /// Different strings should get their own entries. + /// + [TestMethod] + public void DifferentStringsDifferentDocuments() + { + ProjectStringCache cache = new ProjectStringCache(); + + XmlDocument document = new XmlDocument(); + + string stringToAdd = "Test1"; + string return1 = cache.Add(stringToAdd, document); + Assert.AreEqual(1, cache.Count); + + stringToAdd = "Test2"; + XmlDocument document2 = new XmlDocument(); + string return2 = cache.Add(stringToAdd, document2); + + // The second string gets its own instance. + Assert.AreEqual(2, cache.Count); + + // Build a new string guaranteed not to be optimized by the compiler into the same instance. + StringBuilder builder = new StringBuilder(); + builder.Append("Test"); + builder.Append("2"); + XmlDocument document3 = new XmlDocument(); + string return3 = cache.Add(builder.ToString(), document3); + + // The new string should be the same as the other one already in the collection. + Assert.AreSame(return2, return3); + + // No new instances for string with the same content. + Assert.AreEqual(2, cache.Count); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/EventArgsFormatting_Tests.cs b/src/XMakeBuildEngine/UnitTests/EventArgsFormatting_Tests.cs new file mode 100644 index 00000000000..357ef488ce3 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/EventArgsFormatting_Tests.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + /// + /// These tests are repeated in the Utilities unit test assembly. We know that this isn't + /// too useful, because both Engine and Utilities pull the code from the same Shared file. But it + /// gets a bunch of lines of extra coverage of Engine that we weren't otherwise getting, and + /// in theory at least the implementation in Engine should be tested too. + /// + [TestClass] + public class EventArgsFormattingTests + { + [TestMethod] + public void NoLineInfoFormatEventMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 0, 0, 0, 0, 0); + Assert.AreEqual( + "source.cs : CS error 312: Missing ;", s); + } + + // Valid forms for line/col number patterns: + // (line) or (line-line) or (line,col) or (line,col-col) or (line,col,line,col) + [TestMethod] + public void LineNumberRange() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 0, 0, 0); + Assert.AreEqual( + "source.cs(1-2): CS error 312: Missing ;", s); + } + + [TestMethod] + public void ColumnNumberRange() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 0, 0, 1, 2, 0); + Assert.AreEqual( + "source.cs : CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 3, 4, 0); + Assert.AreEqual( + "source.cs(1,3,2,4): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange2() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 0, 3, 4, 0); + Assert.AreEqual( + "source.cs(1,3-4): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange3() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 3, 0, 0); + Assert.AreEqual( + "source.cs(1-2,3): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange4() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 2, 0, 3, 0); + Assert.AreEqual( + "source.cs(1-2): CS error 312: Missing ;", s); + } + + [TestMethod] + public void LineAndColumnNumberRange5() + { + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 1, 0, 2, 0, 0); + Assert.AreEqual( + "source.cs(1,2): CS error 312: Missing ;", s); + } + + [TestMethod] + public void BasicFormatEventMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 42, 0, 0, 0, 0); + Assert.AreEqual( + "source.cs(42): CS error 312: Missing ;", s); + } + + [TestMethod] + public void EscapeCarriageReturnMessages() + { + BuildErrorEventArgs error = new BuildErrorEventArgs("CS", "312", "source.cs", 42, 0, 0, 0, "message\r Hello", "help", "sender"); + BuildWarningEventArgs warning = new BuildWarningEventArgs("CS", "312", "source.cs", 42, 0, 0, 0, "message\r Hello", "help", "sender"); + // Testing the method in Shared.EventArgsFormatting directly + string errorString = EventArgsFormatting.FormatEventMessage(error, true); + string warningString = EventArgsFormatting.FormatEventMessage(warning, true); + string errorString2 = EventArgsFormatting.FormatEventMessage(error, false); + string warningString2 = EventArgsFormatting.FormatEventMessage(warning, false); + Assert.AreEqual("source.cs(42): CS error 312: message\\r Hello", errorString); + Assert.AreEqual("source.cs(42): CS warning 312: message\\r Hello", warningString); + + Assert.AreEqual("source.cs(42): CS error 312: message\r Hello", errorString2); + Assert.AreEqual("source.cs(42): CS warning 312: message\r Hello", warningString2); + } + + [TestMethod] + public void ExactLocationFormatEventMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + "Missing ;", "312", "source.cs", 233, 236, 4, 8, 0); + Assert.AreEqual( + "source.cs(233,4,236,8): CS error 312: Missing ;", s); + } + + [TestMethod] + public void NullMessage() + { + // Testing the method in Shared.EventArgsFormatting directly + string s = EventArgsFormatting.FormatEventMessage("error", "CS", + null, "312", "source.cs", 233, 236, 4, 8, 0); + // No exception was thrown + + } + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/ExpressionTree_Tests.cs b/src/XMakeBuildEngine/UnitTests/ExpressionTree_Tests.cs new file mode 100644 index 00000000000..2736ab5033c --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/ExpressionTree_Tests.cs @@ -0,0 +1,1003 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Reflection; +using System.Collections; +using System.IO; +using System.Xml; +using System.Collections.Specialized; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Collections; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using System.Collections.Generic; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ExpressionTreeTest + { + /// + /// + [TestMethod] + public void SimpleEvaluationTests() + { + Parser p = new Parser(); + Expander expander = new Expander(new PropertyDictionary()); + + AssertParseEvaluate(p, "true", expander, true); + AssertParseEvaluate(p, "on", expander, true); + AssertParseEvaluate(p, "yes", expander, true); + AssertParseEvaluate(p, "false", expander, false); + AssertParseEvaluate(p, "off", expander, false); + AssertParseEvaluate(p, "no", expander, false); + } + + /// + /// A whole bunch of conditionals, that should be true, false, or error + /// (many coincidentally like existing QA tests) to give breadth coverage. + /// Please add more cases as they arise. + /// + [TestMethod] + public void EvaluateAVarietyOfExpressions() + { + string[] files = { "a", "a;b", "a'b", ";", "'" }; + + try + { + foreach (string file in files) + { + using (StreamWriter sw = File.CreateText(file)) {; } + } + + Parser p = new Parser(); + GenericExpressionNode tree; + + ItemDictionary itemBag = new ItemDictionary(); + + // Dummy project instance to own the items. + ProjectRootElement xml = ProjectRootElement.Create(); + xml.FullPath = @"c:\abc\foo.proj"; + + ProjectInstance parentProject = new ProjectInstance(xml); + + itemBag.Add(new ProjectItemInstance(parentProject, "u", "a'b;c", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "v", "a", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "w", "1", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "x", "true", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "y", "xxx", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "z", "xxx", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "z", "yyy", parentProject.FullPath)); + + PropertyDictionary propertyBag = new PropertyDictionary(); + + propertyBag.Set(ProjectPropertyInstance.Create("a", "no")); + propertyBag.Set(ProjectPropertyInstance.Create("b", "true")); + propertyBag.Set(ProjectPropertyInstance.Create("c", "1")); + propertyBag.Set(ProjectPropertyInstance.Create("d", "xxx")); + propertyBag.Set(ProjectPropertyInstance.Create("e", "xxx")); + propertyBag.Set(ProjectPropertyInstance.Create("f", "1.9.5")); + propertyBag.Set(ProjectPropertyInstance.Create("and", "and")); + propertyBag.Set(ProjectPropertyInstance.Create("a_semi_b", "a;b")); + propertyBag.Set(ProjectPropertyInstance.Create("a_apos_b", "a'b")); + propertyBag.Set(ProjectPropertyInstance.Create("foo_apos_foo", "foo'foo")); + propertyBag.Set(ProjectPropertyInstance.Create("a_escapedsemi_b", "a%3bb")); + propertyBag.Set(ProjectPropertyInstance.Create("a_escapedapos_b", "a%27b")); + propertyBag.Set(ProjectPropertyInstance.Create("has_trailing_slash", @"foo\")); + + Dictionary metadataDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + metadataDictionary["Culture"] = "french"; + StringMetadataTable itemMetadata = new StringMetadataTable(metadataDictionary); + + Expander expander = + new Expander(propertyBag, itemBag, itemMetadata); + + string[] trueTests = { + "true or (SHOULDNOTEVALTHIS)", // short circuit + "(true and false) or true", + "false or true or false", + "(true) and (true)", + "false or !false", + "($(a) or true)", + "('$(c)'==1 and (!false))", + "@(z -> '%(filename).z', '$')=='xxx.z$yyy.z'", + "@(w -> '%(definingprojectname).barproj') == 'foo.barproj'", + "false or (false or (false or (false or (false or (true)))))", + "!(true and false)", + "$(and)=='and'", + "0x1==1.0", + "0xa==10", + "0<0.1", + "+4>-4", + "'-$(c)'==-1", + "$(a)==faLse", + "$(a)==oFF", + "$(a)==no", + "$(a)!=true", + "$(b)== True", + "$(b)==on", + "$(b)==yes", + "$(b)!=1", + "$(c)==1", + "$(d)=='xxx'", + "$(d)==$(e)", + "$(d)=='$(e)'", + "@(y)==$(d)", + "'@(z)'=='xxx;yyy'", + "$(a)==$(a)", + "'1'=='1'", + "'1'==1", + "1\n==1", + "1\t==\t\r\n1", + "123=='0123.0'", + "123==123", + "123==0123", + "123==0123.0", + "123!=0123.01", + "1.2.3<=1.2.3.0", + "12.23.34==12.23.34", + "0.8.0.0<8.0.0", + "1.1.2>1.0.1.2", + "8.1>8.0.16.23", + "8.0.0>=8", + "6<=6.0.0.1", + "7>6.8.2", + "4<5.9.9135.4", + "3!=3.0.0", + "1.2.3.4.5.6.7==1.2.3.4.5.6.7", + "00==0", + "0==0.0", + "1\n\t==1", + "+4==4", + "44==+44.0 and -44==-44.0", + "false==no", + "true==yes", + "true==!false", + "yes!=no", + "false!=1", + "$(c)>0", + "!$(a)", + "$(b)", + "($(d)==$(e))", + "!true==false", + "a_a==a_a", + "a_a=='a_a'", + "_a== _a", + "@(y -> '%(filename)')=='xxx'", + "@(z -> '%(filename)', '!')=='xxx!yyy'", + "'xxx!yyy'==@(z -> '%(filename)', '!')", + "'$(a)'==(false)", + "('$(a)'==(false))", + "1>0", + "2<=2", + "2<=3", + "1>=1", + "1>=-1", + "-1==-1", + "-1 < 0", + "(1==1)and('a'=='a')", + "(true) and ($(a)==off)", + "(true) and ($(d)==xxx)", + "(false) or($(d)==xxx)", + "!(false)and!(false)", + "'and'=='AND'", + "$(d)=='XxX'", + "true or true or false", + "false or true or !true or'1'", + "$(a) or $(b)", + "$(a) or true", + "!!true", + "'$(e)1@(y)'=='xxx1xxx'", + "0x11==17", + "0x01a==26", + "0xa==0x0A", + "@(x)", + "'%77'=='w'", + "'%zz'=='%zz'", + "true or 1", + "true==!false", + "(!(true))=='off'", + "@(w)>0", + "1<=@(w)", + "%(culture)=='FRENCH'", + "'%(culture) fries' == 'FRENCH FRIES' ", + @"'%(HintPath)' == ''", + @"%(HintPath) != 'c:\myassemblies\foo.dll'", + "exists('a')", + "exists(a)", + "exists('a%3bb')", /* semicolon */ + "exists('a%27b')", /* apostrophe */ + "exists($(a_escapedsemi_b))", + "exists('$(a_escapedsemi_b)')", + "exists($(a_escapedapos_b))", + "exists('$(a_escapedapos_b)')", + "exists($(a_apos_b))", + "exists('$(a_apos_b)')", + "exists(@(v))", + "exists('@(v)')", + "exists('%3b')", + "exists('%27')", + "exists('@(v);@(nonexistent)')", + @"HASTRAILINGSLASH('foo\')", + @"!HasTrailingSlash('foo')", + @"HasTrailingSlash('foo/')", + @"HasTrailingSlash($(has_trailing_slash))", + "'59264.59264' == '59264.59264'", + "1" + new String('0', 500) + "==" + "1" + new String('0', 500), /* too big for double, eval as string */ + "'1" + new String('0', 500) + "'=='" + "1" + new String('0', 500) + "'" /* too big for double, eval as string */ + }; + + string[] falseTests = { + "false and SHOULDNOTEVALTHIS", // short circuit + "$(a)!=no", + "$(b)==1.1", + "$(c)==$(a)", + "$(d)!=$(e)", + "!$(b)", + "false or false or false", + "false and !((true and false))", + "on and off", + "(true) and (false)", + "false or (false or (false or (false or (false or (false)))))", + "!$(b)and true", + "1==a", + "!($(d)==$(e))", + "$(a) and true", + "true==1", + "false==0", + "(!(true))=='x'", + "oops==false", + "oops==!false", + "%(culture) == 'english'", + "'%(culture) fries' == 'english fries' ", + @"'%(HintPath)' == 'c:\myassemblies\foo.dll'", + @"%(HintPath) == 'c:\myassemblies\foo.dll'", + "exists('')", + "exists(' ')", + "exists($(nonexistent))", // DDB #141195 + "exists('$(nonexistent)')", // DDB #141195 + "exists(@(nonexistent))", // DDB #141195 + "exists('@(nonexistent)')", // DDB #141195 + "exists('\t')", + "exists('@(u)')", + "exists('$(foo_apos_foo)')", + "!exists('a')", + "!!!exists(a)", + "exists('|||||')", + @"hastrailingslash('foo')", + @"hastrailingslash('')", + @"HasTrailingSlash($(nonexistent))", + "'59264.59264' == '59264.59265'", + "1.2.0==1.2", + "$(f)!=$(f)", + "1.3.5.8>1.3.6.8", + "0.8.0.0>=1.0", + "8.0.0<=8.0", + "8.1.2<8", + "1" + new String('0', 500) + "==2", /* too big for double, eval as string */ + "'1" + new String('0', 500) + "'=='2'", /* too big for double, eval as string */ + "'1" + new String('0', 500) + "'=='01" + new String('0', 500) + "'" /* too big for double, eval as string */ + }; + + string[] errorTests = { + "$", + "$(", + "$()", + "@", + "@(", + "@()", + "%", + "%(", + "%()", + "exists", + "exists(", + "exists()", + "exists( )", + "exists(,)", + "@(x->'", + "@(x->''", + "@(x-", + "@(x->'x','", + "@(x->'x',''", + "@(x->'x','')", + "-1>x", + "%00", + "\n", + "\t", + "+-1==1", + "1==-+1", + "1==+0xa", + "!$(c)", + "'a'==('a'=='a')", + "'a'!=('a'=='a')", + "('a'=='a')!=a", + "('a'=='a')==a", + "!'x'", + "!'$(d)'", + "ab#==ab#", + "#!=#", + "$(d)$(e)=='xxxxxx'", + "1=1=1", + "'a'=='a'=='a'", + "1 > 'x'", + "x1<=1", + "1<=x", + "1>x", + "xx", + "x>=x", + "x<=x", + "x>1", + "x>=1", + "1>=x", + "@(y)<=1", + "1<=@(z)", + "1>$(d)", + "$(c)@(y)>1", + "'$(c)@(y)'>1", + "$(d)>=1", + "1>=$(b)", + "1> =0", + "or true", + "1 and", + "and", + "or", + "not", + "not true", + "()", + "(a)", + "!", + "or=or", + "1==", + "1= =1", + "=", + "'true", + "'false''", + "'a'=='a", + "('a'=='a'", + "('a'=='a'))", + "'a'=='a')", + "!and", + "@(a)@(x)!=1", + "@(a) @(x)!=1", + "$(a==off", + "=='x'", + "==", + "!0", + ">", + "true!=false==", + "true!=false==true", + "()", + "!1", + "1==(2", + "$(a)==x>1==2", + "'a'>'a'", + "0", + "$(a)>0", + "!$(e)", + "1<=1<=1", + "true $(and) true", + "--1==1", + "$(and)==and", + "!@#$%^&*", + "-($(c))==-1", + "a==b or $(d)", + "false or $()", + "$(d) or true", + "%(Culture) or true", + "@(nonexistent) and true", + "$(nonexistent) and true", + "@(nonexistent)", + "$(nonexistent)", + "@(z) and true", + "@() and true", + "@()", + "$()", + "1", + "1 or true", + "false or 1", + "1 and true", + "true and 1", + "!1", + "false or !1", + "false or 'aa'", + "true blah", + "existsX", + "!", + "nonexistentfunction('xyz')", + "exists('a;b')", /* non scalar */ + "exists(@(z))", + "exists('@(z)')", + "exists($(a_semi_b))", + "exists('$(a_semi_b)')", + "exists(@(v)x)", + "exists(@(v)$(nonexistent))", + "exists('@(v)$(a)')", + "exists(|||||)", + "HasTrailingSlash(a,'b')", + "HasTrailingSlash(,,)", + "1.2.3==1,2,3" + }; + + for (int i = 0; i < trueTests.GetLength(0); i++) + { + tree = p.Parse(trueTests[i], ParserOptions.AllowAll, ElementLocation.EmptyLocation); + ConditionEvaluator.IConditionEvaluationState state = + new ConditionEvaluator.ConditionEvaluationState + ( + trueTests[i], + expander, + ExpanderOptions.ExpandAll, + null, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + + Assert.IsTrue(tree.Evaluate(state), "expected true from '" + trueTests[i] + "'"); + } + + for (int i = 0; i < falseTests.GetLength(0); i++) + { + tree = p.Parse(falseTests[i], ParserOptions.AllowAll, ElementLocation.EmptyLocation); + ConditionEvaluator.IConditionEvaluationState state = + new ConditionEvaluator.ConditionEvaluationState + ( + falseTests[i], + expander, + ExpanderOptions.ExpandAll, + null, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + + Assert.IsFalse(tree.Evaluate(state), "expected false from '" + falseTests[i] + "' and got true"); + } + + for (int i = 0; i < errorTests.GetLength(0); i++) + { + // It seems that if an expression is invalid, + // - Parse may throw, or + // - Evaluate may throw, or + // - Evaluate may return false causing its caller EvaluateCondition to throw + bool success = true; + bool caughtException = false; + bool value; + try + { + tree = p.Parse(errorTests[i], ParserOptions.AllowAll, ElementLocation.EmptyLocation); + ConditionEvaluator.IConditionEvaluationState state = + new ConditionEvaluator.ConditionEvaluationState + ( + errorTests[i], + expander, + ExpanderOptions.ExpandAll, + null, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + + value = tree.Evaluate(state); + if (!success) Console.WriteLine(errorTests[i] + " caused Evaluate to return false"); + } + catch (InvalidProjectFileException ex) + { + Console.WriteLine(errorTests[i] + " caused '" + ex.Message + "'"); + caughtException = true; + } + Assert.IsTrue((success == false || caughtException == true), "expected '" + errorTests[i] + "' to not parse or not be evaluated"); + } + } + finally + { + foreach (string file in files) + { + if (File.Exists(file)) File.Delete(file); + } + } + } + + + /// + /// + [TestMethod] + public void EqualityTests() + { + Parser p = new Parser(); + Expander expander = new Expander(new PropertyDictionary()); + + AssertParseEvaluate(p, "true == on", expander, true); + AssertParseEvaluate(p, "TrUe == On", expander, true); + AssertParseEvaluate(p, "true != false", expander, true); + AssertParseEvaluate(p, "true==!false", expander, true); + AssertParseEvaluate(p, "4 != 5", expander, true); + AssertParseEvaluate(p, "-4 < 4", expander, true); + AssertParseEvaluate(p, "5 == +5", expander, true); + AssertParseEvaluate(p, "4 == 4.0", expander, true); + AssertParseEvaluate(p, "4 == 4.0", expander, true); + AssertParseEvaluate(p, ".45 == '.45'", expander, true); + AssertParseEvaluate(p, "4 == '4'", expander, true); + AssertParseEvaluate(p, "'0' == '4'", expander, false); + AssertParseEvaluate(p, "4 == 0x0004", expander, true); + AssertParseEvaluate(p, "0.0 == 0", expander, true); + AssertParseEvaluate(p, "simplestring == 'simplestring'", expander, true); + } + + /// + /// + [TestMethod] + public void RelationalTests() + { + Parser p = new Parser(); + Expander expander = new Expander(new PropertyDictionary()); + + AssertParseEvaluate(p, "1234 < 1235", expander, true); + AssertParseEvaluate(p, "1234 <= 1235", expander, true); + AssertParseEvaluate(p, "1235 < 1235", expander, false); + AssertParseEvaluate(p, "1234 <= 1234", expander, true); + AssertParseEvaluate(p, "1235 <= 1234", expander, false); + AssertParseEvaluate(p, "1235 > 1234", expander, true); + AssertParseEvaluate(p, "1235 >= 1235", expander, true); + AssertParseEvaluate(p, "1235 >= 1234", expander, true); + AssertParseEvaluate(p, "0.0==0", expander, true); + } + + /// + /// + [TestMethod] + public void AndandOrTests() + { + Parser p = new Parser(); + Expander expander = new Expander(new PropertyDictionary()); + + AssertParseEvaluate(p, "true == on and 1234 < 1235", expander, true); + } + + /// + /// + [TestMethod] + public void FunctionTests() + { + Parser p = new Parser(); + GenericExpressionNode tree; + Expander expander = new Expander(new PropertyDictionary(), new ItemDictionary()); + expander.Metadata = new StringMetadataTable(null); + bool value; + + string fileThatMustAlwaysExist = FileUtilities.GetTemporaryFile(); + File.WriteAllText(fileThatMustAlwaysExist, "foo"); + string command = "Exists('" + fileThatMustAlwaysExist + "')"; + tree = p.Parse(command, ParserOptions.AllowAll, ElementLocation.EmptyLocation); + + ConditionEvaluator.IConditionEvaluationState state = + new ConditionEvaluator.ConditionEvaluationState + ( + command, + expander, + ExpanderOptions.ExpandAll, + null, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + + value = tree.Evaluate(state); + Assert.IsTrue(value); + + if (File.Exists(fileThatMustAlwaysExist)) + { + File.Delete(fileThatMustAlwaysExist); + } + + AssertParseEvaluate(p, "Exists('c:\\IShouldntExist.sys')", expander, false); + } + + /// + /// + [TestMethod] + public void PropertyTests() + { + Parser p = new Parser(); + + var propertyBag = new PropertyDictionary(); + propertyBag.Set(ProjectPropertyInstance.Create("foo", "true")); + propertyBag.Set(ProjectPropertyInstance.Create("bar", "yes")); + propertyBag.Set(ProjectPropertyInstance.Create("one", "1")); + propertyBag.Set(ProjectPropertyInstance.Create("onepointzero", "1.0")); + propertyBag.Set(ProjectPropertyInstance.Create("two", "2")); + propertyBag.Set(ProjectPropertyInstance.Create("simple", "simplestring")); + propertyBag.Set(ProjectPropertyInstance.Create("complex", "This is a complex string")); + propertyBag.Set(ProjectPropertyInstance.Create("c1", "Another (complex) one.")); + propertyBag.Set(ProjectPropertyInstance.Create("c2", "Another (complex) one.")); + propertyBag.Set(ProjectPropertyInstance.Create("x86", "x86")); + propertyBag.Set(ProjectPropertyInstance.Create("no", "no")); + + Expander expander = new Expander(propertyBag, new ItemDictionary()); + AssertParseEvaluate(p, "$(foo)", expander, true); + AssertParseEvaluate(p, "!$(foo)", expander, false); + // Test properties with strings + AssertParseEvaluate(p, "$(simple) == 'simplestring'", expander, true); + AssertParseEvaluate(p, "'simplestring' == $(simple)", expander, true); + AssertParseEvaluate(p, "'foo' != $(simple)", expander, true); + AssertParseEvaluate(p, "'simplestring' == '$(simple)'", expander, true); + AssertParseEvaluate(p, "$(simple) == simplestring", expander, true); + AssertParseEvaluate(p, "$(x86) == x86", expander, true); + AssertParseEvaluate(p, "$(x86)==x86", expander, true); + AssertParseEvaluate(p, "x86==$(x86)", expander, true); + AssertParseEvaluate(p, "$(c1) == $(c2)", expander, true); + AssertParseEvaluate(p, "'$(c1)' == $(c2)", expander, true); + AssertParseEvaluate(p, "$(c1) != $(simple)", expander, true); + AssertParseEvaluate(p, "$(c1) == $(c2)", expander, true); + // Test properties with numbers + AssertParseEvaluate(p, "$(one) == $(onepointzero)", expander, true); + AssertParseEvaluate(p, "$(one) <= $(two)", expander, true); + AssertParseEvaluate(p, "$(two) > $(onepointzero)", expander, true); + AssertParseEvaluate(p, "$(one) != $(two)", expander, true); + AssertParseEvaluate(p, "'$(no)'==false", expander, true); + } + + /// + /// + [TestMethod] + public void ItemListTests() + { + Parser p = new Parser(); + + ProjectInstance parentProject = new ProjectInstance(ProjectRootElement.Create()); + ItemDictionary itemBag = new ItemDictionary(); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "foo.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "bar.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "baz.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Boolean", "true", parentProject.FullPath)); + + Expander expander = new Expander(new PropertyDictionary(), itemBag); + + AssertParseEvaluate(p, "@(Compile) == 'foo.cs;bar.cs;baz.cs'", expander, true); + AssertParseEvaluate(p, "@(Compile,' ') == 'foo.cs bar.cs baz.cs'", expander, true); + AssertParseEvaluate(p, "@(Compile,'') == 'foo.csbar.csbaz.cs'", expander, true); + AssertParseEvaluate(p, "@(Compile->'%(Filename)') == 'foo;bar;baz'", expander, true); + AssertParseEvaluate(p, "@(Compile -> 'temp\\%(Filename).xml', ' ') == 'temp\\foo.xml temp\\bar.xml temp\\baz.xml'", expander, true); + AssertParseEvaluate(p, "@(Compile->'', '') == ''", expander, true); + AssertParseEvaluate(p, "@(Compile->'') == ';;'", expander, true); + AssertParseEvaluate(p, "@(Compile->'%(Nonexistent)', '') == ''", expander, true); + AssertParseEvaluate(p, "@(Compile->'%(Nonexistent)') == ';;'", expander, true); + AssertParseEvaluate(p, "@(Boolean)", expander, true); + AssertParseEvaluate(p, "@(Boolean) == true", expander, true); + AssertParseEvaluate(p, "'@(Empty, ';')' == ''", expander, true); + } + + /// + /// + [TestMethod] + public void StringExpansionTests() + { + Parser p = new Parser(); + + ProjectInstance parentProject = new ProjectInstance(ProjectRootElement.Create()); + ItemDictionary itemBag = new ItemDictionary(); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "foo.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "bar.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "baz.cs", parentProject.FullPath)); + + PropertyDictionary propertyBag = new PropertyDictionary(); + propertyBag.Set(ProjectPropertyInstance.Create("foo", "true")); + propertyBag.Set(ProjectPropertyInstance.Create("bar", "yes")); + propertyBag.Set(ProjectPropertyInstance.Create("one", "1")); + propertyBag.Set(ProjectPropertyInstance.Create("onepointzero", "1.0")); + propertyBag.Set(ProjectPropertyInstance.Create("two", "2")); + propertyBag.Set(ProjectPropertyInstance.Create("simple", "simplestring")); + propertyBag.Set(ProjectPropertyInstance.Create("complex", "This is a complex string")); + propertyBag.Set(ProjectPropertyInstance.Create("c1", "Another (complex) one.")); + propertyBag.Set(ProjectPropertyInstance.Create("c2", "Another (complex) one.")); + propertyBag.Set(ProjectPropertyInstance.Create("TestQuote", "Contains'Quote'")); + propertyBag.Set(ProjectPropertyInstance.Create("AnotherTestQuote", "Here's Johnny!")); + propertyBag.Set(ProjectPropertyInstance.Create("Atsign", "Test the @ replacement")); + + Expander expander = new Expander(propertyBag, itemBag); + + AssertParseEvaluate(p, "'simplestring: true foo.cs;bar.cs;baz.cs' == '$(simple): $(foo) @(compile)'", expander, true); + AssertParseEvaluate(p, "'$(c1) $(c2)' == 'Another (complex) one. Another (complex) one.'", expander, true); + AssertParseEvaluate(p, "'CONTAINS%27QUOTE%27' == '$(TestQuote)'", expander, true); + AssertParseEvaluate(p, "'Here%27s Johnny!' == '$(AnotherTestQuote)'", expander, true); + AssertParseEvaluate(p, "'Test the %40 replacement' == $(Atsign)", expander, true); + } + + /// + /// + [TestMethod] + public void ComplexTests() + { + Parser p = new Parser(); + ProjectInstance parentProject = new ProjectInstance(ProjectRootElement.Create()); + ItemDictionary itemBag = new ItemDictionary(); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "foo.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "bar.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "baz.cs", parentProject.FullPath)); + + PropertyDictionary propertyBag = new PropertyDictionary(); + propertyBag.Set(ProjectPropertyInstance.Create("foo", "true")); + propertyBag.Set(ProjectPropertyInstance.Create("bar", "yes")); + propertyBag.Set(ProjectPropertyInstance.Create("one", "1")); + propertyBag.Set(ProjectPropertyInstance.Create("onepointzero", "1.0")); + propertyBag.Set(ProjectPropertyInstance.Create("two", "2")); + propertyBag.Set(ProjectPropertyInstance.Create("simple", "simplestring")); + propertyBag.Set(ProjectPropertyInstance.Create("complex", "This is a complex string")); + propertyBag.Set(ProjectPropertyInstance.Create("c1", "Another (complex) one.")); + propertyBag.Set(ProjectPropertyInstance.Create("c2", "Another (complex) one.")); + + Expander expander = new Expander(propertyBag, itemBag); + + AssertParseEvaluate(p, "(($(foo) != 'two' and $(bar)) and 5 >= 1) or $(one) == 1", expander, true); + AssertParseEvaluate(p, "(($(foo) != 'twoo' or !$(bar)) and 5 >= 1) or $(two) == 1", expander, true); + AssertParseEvaluate(p, "!((($(foo) != 'twoo' or !$(bar)) and 5 >= 1) or $(two) == 1)", expander, false); + } + + + /// + /// Make sure when a non number is used in an expression which expects a numeric value that a error is emitted. + /// + [TestMethod] + public void InvalidItemInConditionEvaluation() + { + Parser p = new Parser(); + ProjectInstance parentProject = new ProjectInstance(ProjectRootElement.Create()); + ItemDictionary itemBag = new ItemDictionary(); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "a", parentProject.FullPath)); + + PropertyDictionary propertyBag = new PropertyDictionary(); + + Expander expander = new Expander(propertyBag, itemBag); + + AssertParseEvaluateThrow(p, "@(Compile) > 0", expander, null); + } + + /// + /// + [TestMethod] + public void OldSyntaxTests() + { + Parser p = new Parser(); + ProjectInstance parentProject = new ProjectInstance(ProjectRootElement.Create()); + ItemDictionary itemBag = new ItemDictionary(); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "foo.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "bar.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "baz.cs", parentProject.FullPath)); + + PropertyDictionary propertyBag = new PropertyDictionary(); + + propertyBag.Set(ProjectPropertyInstance.Create("foo", "true")); + propertyBag.Set(ProjectPropertyInstance.Create("bar", "yes")); + propertyBag.Set(ProjectPropertyInstance.Create("one", "1")); + propertyBag.Set(ProjectPropertyInstance.Create("onepointzero", "1.0")); + propertyBag.Set(ProjectPropertyInstance.Create("two", "2")); + propertyBag.Set(ProjectPropertyInstance.Create("simple", "simplestring")); + propertyBag.Set(ProjectPropertyInstance.Create("complex", "This is a complex string")); + propertyBag.Set(ProjectPropertyInstance.Create("c1", "Another (complex) one.")); + propertyBag.Set(ProjectPropertyInstance.Create("c2", "Another (complex) one.")); + + Expander expander = new Expander(propertyBag, itemBag); + + AssertParseEvaluate(p, "(($(foo) != 'two' and $(bar)) and 5 >= 1) or $(one) == 1", expander, true); + } + + /// + /// + [TestMethod] + public void ConditionedPropertyUpdateTests() + { + Parser p = new Parser(); + ProjectInstance parentProject = new ProjectInstance(ProjectRootElement.Create()); + ItemDictionary itemBag = new ItemDictionary(); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "foo.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "bar.cs", parentProject.FullPath)); + itemBag.Add(new ProjectItemInstance(parentProject, "Compile", "baz.cs", parentProject.FullPath)); + + Expander expander = new Expander(new PropertyDictionary(), itemBag); + Dictionary> conditionedProperties = new Dictionary>(); + ConditionEvaluator.IConditionEvaluationState state = + new ConditionEvaluator.ConditionEvaluationState + ( + String.Empty, + expander, + ExpanderOptions.ExpandAll, + conditionedProperties, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + + List properties = null; + + AssertParseEvaluate(p, "'0' == '1'", expander, false, state); + Assert.IsTrue(conditionedProperties.Count == 0); + + AssertParseEvaluate(p, "$(foo) == foo", expander, false, state); + Assert.IsTrue(conditionedProperties.Count == 1); + properties = conditionedProperties["foo"]; + Assert.IsTrue(properties.Count == 1); + + AssertParseEvaluate(p, "'$(foo)' != 'bar'", expander, true, state); + Assert.IsTrue(conditionedProperties.Count == 1); + properties = conditionedProperties["foo"]; + Assert.IsTrue(properties.Count == 2); + + AssertParseEvaluate(p, "'$(branch)|$(build)|$(platform)' == 'lab22dev|debug|x86'", expander, false, state); + Assert.IsTrue(conditionedProperties.Count == 4); + properties = conditionedProperties["foo"]; + Assert.IsTrue(properties.Count == 2); + properties = conditionedProperties["branch"]; + Assert.IsTrue(properties.Count == 1); + properties = conditionedProperties["build"]; + Assert.IsTrue(properties.Count == 1); + properties = conditionedProperties["platform"]; + Assert.IsTrue(properties.Count == 1); + + AssertParseEvaluate(p, "'$(branch)|$(build)|$(platform)' == 'lab21|debug|x86'", expander, false, state); + Assert.IsTrue(conditionedProperties.Count == 4); + properties = conditionedProperties["foo"]; + Assert.IsTrue(properties.Count == 2); + properties = conditionedProperties["branch"]; + Assert.IsTrue(properties.Count == 2); + properties = conditionedProperties["build"]; + Assert.IsTrue(properties.Count == 1); + properties = conditionedProperties["platform"]; + Assert.IsTrue(properties.Count == 1); + + AssertParseEvaluate(p, "'$(branch)|$(build)|$(platform)' == 'lab23|retail|ia64'", expander, false, state); + Assert.IsTrue(conditionedProperties.Count == 4); + properties = conditionedProperties["foo"]; + Assert.IsTrue(properties.Count == 2); + properties = conditionedProperties["branch"]; + Assert.IsTrue(properties.Count == 3); + properties = conditionedProperties["build"]; + Assert.IsTrue(properties.Count == 2); + properties = conditionedProperties["platform"]; + Assert.IsTrue(properties.Count == 2); + DumpDictionary(conditionedProperties); + } + + private static void DumpDictionary(Dictionary> propertyDictionary) + { + foreach (KeyValuePair> entry in propertyDictionary) + { + Console.Write(" {0}:\t", entry.Key); + + List properties = entry.Value; + + foreach (string property in properties) + { + Console.Write("{0}, ", property); + } + Console.WriteLine(); + } + } + + /// + /// + [TestMethod] + public void NotTests() + { + Console.WriteLine("NegationParseTest()"); + Parser p = new Parser(); + + PropertyDictionary propertyBag = new PropertyDictionary(); + propertyBag.Set(ProjectPropertyInstance.Create("foo", "4")); + propertyBag.Set(ProjectPropertyInstance.Create("bar", "32")); + + Expander expander = new Expander(propertyBag, new ItemDictionary()); + + AssertParseEvaluate(p, "!true", expander, false); + AssertParseEvaluate(p, "!(true)", expander, false); + AssertParseEvaluate(p, "!($(foo) <= 5)", expander, false); + AssertParseEvaluate(p, "!($(foo) <= 5 and $(bar) >= 15)", expander, false); + } + + private void AssertParseEvaluate(Parser p, string expression, Expander expander, bool expected) + { + AssertParseEvaluate(p, expression, expander, expected, null); + } + + private void AssertParseEvaluate(Parser p, string expression, Expander expander, bool expected, ConditionEvaluator.IConditionEvaluationState state) + { + if (expander.Metadata == null) + { + expander.Metadata = new StringMetadataTable(null); + } + + GenericExpressionNode tree = p.Parse(expression, ParserOptions.AllowAll, MockElementLocation.Instance); + + if (state == null) + { + state = + new ConditionEvaluator.ConditionEvaluationState + ( + String.Empty, + expander, + ExpanderOptions.ExpandAll, + null, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + } + + bool result = tree.Evaluate(state); + Assert.AreEqual(expected, result); + } + + + private void AssertParseEvaluateThrow(Parser p, string expression, Expander expander) + { + AssertParseEvaluateThrow(p, expression, expander, null); + } + + private void AssertParseEvaluateThrow(Parser p, string expression, Expander expander, ConditionEvaluator.IConditionEvaluationState state) + { + bool fExceptionCaught; + + if (expander.Metadata == null) + { + expander.Metadata = new StringMetadataTable(null); + } + + try + { + fExceptionCaught = false; + GenericExpressionNode tree = p.Parse(expression, ParserOptions.AllowAll, MockElementLocation.Instance); + if (state == null) + { + state = + new ConditionEvaluator.ConditionEvaluationState + ( + String.Empty, + expander, + ExpanderOptions.ExpandAll, + null, + Environment.CurrentDirectory, + ElementLocation.EmptyLocation + ); + } + tree.Evaluate(state); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + + Assert.IsTrue(fExceptionCaught); + } + + /// + /// + [TestMethod] + public void NegativeTests() + { + Parser p = new Parser(); + Expander expander = new Expander(new PropertyDictionary()); + + AssertParseEvaluateThrow(p, "foo", expander); + AssertParseEvaluateThrow(p, "0", expander); + AssertParseEvaluateThrow(p, "$(platform) == xx > 1==2", expander); + AssertParseEvaluateThrow(p, "!0", expander); + AssertParseEvaluateThrow(p, ">", expander); + AssertParseEvaluateThrow(p, "true!=false==", expander); + AssertParseEvaluateThrow(p, "()", expander); + AssertParseEvaluateThrow(p, "!1", expander); + AssertParseEvaluateThrow(p, "true!=false==true", expander); + AssertParseEvaluateThrow(p, "'a'>'a'", expander); + AssertParseEvaluateThrow(p, "=='x'", expander); + AssertParseEvaluateThrow(p, "==", expander); + AssertParseEvaluateThrow(p, "1==(2", expander); + AssertParseEvaluateThrow(p, "'a'==('a'=='a')", expander); + AssertParseEvaluateThrow(p, "true == on and ''", expander); + AssertParseEvaluateThrow(p, "'' or 'true'", expander); + } + } +} + + + diff --git a/src/XMakeBuildEngine/UnitTests/FileLogger_Tests.cs b/src/XMakeBuildEngine/UnitTests/FileLogger_Tests.cs new file mode 100644 index 00000000000..423f9f26570 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/FileLogger_Tests.cs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; + +using EventSourceSink = Microsoft.Build.BackEnd.Logging.EventSourceSink; +using Project = Microsoft.Build.Evaluation.Project; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class FileLogger_Tests + { + /// + /// Basic test of the file logger. Writes to a log file in the temp directory. + /// + [TestMethod] + public void Basic() + { + FileLogger fileLogger = new FileLogger(); + string logFile = FileUtilities.GetTemporaryFile(); + fileLogger.Parameters = "verbosity=Normal;logfile=" + logFile; + + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + "); + + project.Build(fileLogger); + + project.ProjectCollection.UnregisterAllLoggers(); + + string log = File.ReadAllText(logFile); + Assert.IsTrue(log.Contains("Hello world from the FileLogger"), "Log should have contained message"); + + File.Delete(logFile); + } + + /// + /// Basic case of logging a message to a file + /// Verify it logs and encoding is ANSI + /// + [TestMethod] + public void BasicNoExistingFile() + { + string log = null; + + try + { + log = GetTempFilename(); + SetUpFileLoggerAndLogMessage("logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + VerifyFileContent(log, "message here"); + + // Verify no BOM (ANSI encoding) + byte[] content = ReadRawBytes(log); + Assert.AreEqual((byte)109, content[0]); // 'm' + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Invalid file should error nicely + /// + [TestMethod] + [ExpectedException(typeof(LoggerException))] + public void InvalidFile() + { + string log = null; + + try + { + SetUpFileLoggerAndLogMessage("logfile=||invalid||", new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Specific verbosity overrides global verbosity + /// + [TestMethod] + public void SpecificVerbosity() + { + string log = null; + + try + { + log = GetTempFilename(); + FileLogger fl = new FileLogger(); + EventSourceSink es = new EventSourceSink(); + fl.Parameters = "verbosity=diagnostic;logfile=" + log; // diagnostic specific setting + fl.Verbosity = LoggerVerbosity.Quiet; // quiet global setting + fl.Initialize(es); + fl.MessageHandler(null, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + fl.Shutdown(); + + // expect message to appear because diagnostic not quiet verbosity was used + VerifyFileContent(log, "message here"); + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Test the short hand verbosity settings for the file logger + /// + [TestMethod] + public void ValidVerbosities() + { + string[] verbositySettings = new string[] { "Q", "quiet", "m", "minimal", "N", "normal", "d", "detailed", "diag", "DIAGNOSTIC" }; + LoggerVerbosity[] verbosityEnumerations = new LoggerVerbosity[] {LoggerVerbosity.Quiet, LoggerVerbosity.Quiet, + LoggerVerbosity.Minimal, LoggerVerbosity.Minimal, + LoggerVerbosity.Normal, LoggerVerbosity.Normal, + LoggerVerbosity.Detailed, LoggerVerbosity.Detailed, + LoggerVerbosity.Diagnostic, LoggerVerbosity.Diagnostic}; + for (int i = 0; i < verbositySettings.Length; i++) + { + FileLogger fl = new FileLogger(); + fl.Parameters = "verbosity=" + verbositySettings[i] + ";"; + EventSourceSink es = new EventSourceSink(); + fl.Initialize(es); + fl.Shutdown(); + Assert.AreEqual(fl.Verbosity, verbosityEnumerations[i]); + } + + // Do the same using the v shorthand + for (int i = 0; i < verbositySettings.Length; i++) + { + FileLogger fl = new FileLogger(); + fl.Parameters = "v=" + verbositySettings[i] + ";"; + EventSourceSink es = new EventSourceSink(); + fl.Initialize(es); + fl.Shutdown(); + Assert.AreEqual(fl.Verbosity, verbosityEnumerations[i]); + } + } + + /// + /// Invalid verbosity setting + /// + [TestMethod] + [ExpectedException(typeof(LoggerException))] + public void InvalidVerbosity() + { + FileLogger fl = new FileLogger(); + fl.Parameters = "verbosity=CookiesAndCream"; + EventSourceSink es = new EventSourceSink(); + fl.Initialize(es); + } + + /// + /// Invalid encoding setting + /// + [TestMethod] + [ExpectedException(typeof(LoggerException))] + public void InvalidEncoding() + { + string log = null; + + try + { + log = GetTempFilename(); + FileLogger fl = new FileLogger(); + EventSourceSink es = new EventSourceSink(); + fl.Parameters = "encoding=foo;logfile=" + log; + fl.Initialize(es); + } + finally + { + if (null != log) File.Delete(log); + } + } + + + /// + /// Valid encoding setting + /// + [TestMethod] + public void ValidEncoding() + { + string log = null; + + try + { + log = GetTempFilename(); + SetUpFileLoggerAndLogMessage("encoding=utf-16;logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + byte[] content = ReadRawBytes(log); + + // FF FE is the BOM for UTF16 + Assert.AreEqual((byte)255, content[0]); + Assert.AreEqual((byte)254, content[1]); + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Valid encoding setting + /// + [TestMethod] + public void ValidEncoding2() + { + string log = null; + + try + { + log = GetTempFilename(); + SetUpFileLoggerAndLogMessage("encoding=utf-8;logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + byte[] content = ReadRawBytes(log); + + // EF BB BF is the BOM for UTF8 + Assert.AreEqual((byte)239, content[0]); + Assert.AreEqual((byte)187, content[1]); + Assert.AreEqual((byte)191, content[2]); + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Read the raw byte content of a file + /// + /// + /// + private byte[] ReadRawBytes(string log) + { + byte[] content; + using (FileStream stream = new FileStream(log, FileMode.Open)) + { + content = new byte[stream.Length]; + + for (int i = 0; i < stream.Length; i++) + { + content[i] = (byte)stream.ReadByte(); + } + } + + return content; + } + + /// + /// Logging a message to a file that already exists should overwrite it + /// + [TestMethod] + public void BasicExistingFileNoAppend() + { + string log = null; + + try + { + log = GetTempFilename(); + WriteContentToFile(log); + SetUpFileLoggerAndLogMessage("logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + VerifyFileContent(log, "message here"); + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Logging to a file that already exists, with "append" set, should append + /// + [TestMethod] + public void BasicExistingFileAppend() + { + string log = null; + + try + { + log = GetTempFilename(); + WriteContentToFile(log); + SetUpFileLoggerAndLogMessage("append;logfile=" + log, new BuildMessageEventArgs("message here", null, null, MessageImportance.High)); + VerifyFileContent(log, "existing content\nmessage here"); + } + finally + { + if (null != log) File.Delete(log); + } + } + + /// + /// Gets a filename for a nonexistent temporary file. + /// + /// + private string GetTempFilename() + { + string path = FileUtilities.GetTemporaryFile(); + File.Delete(path); + return path; + } + + /// + /// Writes a string to a file. + /// + /// + private void WriteContentToFile(string log) + { + using (StreamWriter sw = new StreamWriter(log)) + { + sw.WriteLine("existing content"); + } + } + + /// + /// Creates a FileLogger, sets its parameters and initializes it, + /// logs a message to it, and calls shutdown + /// + /// + /// + private void SetUpFileLoggerAndLogMessage(string parameters, BuildMessageEventArgs message) + { + FileLogger fl = new FileLogger(); + EventSourceSink es = new EventSourceSink(); + fl.Parameters = parameters; + fl.Initialize(es); + fl.MessageHandler(null, message); + fl.Shutdown(); + return; + } + + /// + /// Verifies that a file contains exactly the expected content. + /// + /// + /// + private void VerifyFileContent(string file, string expectedContent) + { + string actualContent; + using (StreamReader sr = new StreamReader(file)) + { + actualContent = sr.ReadToEnd(); + } + + string[] actualLines = actualContent.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + string[] expectedLines = expectedContent.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + + Assert.AreEqual(expectedLines.Length, actualLines.Length); + + for (int i = 0; i < expectedLines.Length; i++) + { + Assert.AreEqual(expectedLines[i].Trim(), actualLines[i].Trim()); + } + } + + #region DistributedLogger + /// + /// Check the ability of the distributed logger to correctly tell its internal file logger where to log the file + /// + [TestMethod] + public void DistributedFileLoggerParameters() + { + DistributedFileLogger fileLogger = new DistributedFileLogger(); + try + { + fileLogger.NodeId = 0; + fileLogger.Initialize(new EventSourceSink()); + Assert.IsTrue(string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=msbuild0.log;", StringComparison.OrdinalIgnoreCase) == 0); + fileLogger.Shutdown(); + + fileLogger.NodeId = 3; + fileLogger.Parameters = "logfile=" + Path.Combine(Environment.CurrentDirectory, "mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + Assert.IsTrue(string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Environment.CurrentDirectory, "mylogfile3.log") + ";", StringComparison.OrdinalIgnoreCase) == 0); + fileLogger.Shutdown(); + + fileLogger.NodeId = 4; + fileLogger.Parameters = "logfile=" + Path.Combine(Environment.CurrentDirectory, "mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + Assert.IsTrue(string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Environment.CurrentDirectory, "mylogfile4.log") + ";", StringComparison.OrdinalIgnoreCase) == 0); + fileLogger.Shutdown(); + + Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, "tempura")); + fileLogger.NodeId = 1; + fileLogger.Parameters = "logfile=" + Path.Combine(Environment.CurrentDirectory, "tempura\\mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + Assert.IsTrue(string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Environment.CurrentDirectory, "tempura\\mylogfile1.log") + ";", StringComparison.OrdinalIgnoreCase) == 0); + fileLogger.Shutdown(); + } + finally + { + if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, "tempura"))) + { + File.Delete(Path.Combine(Environment.CurrentDirectory, "tempura\\mylogfile1.log")); + Directory.Delete(Path.Combine(Environment.CurrentDirectory, "tempura")); + } + File.Delete(Path.Combine(Environment.CurrentDirectory, "mylogfile0.log")); + File.Delete(Path.Combine(Environment.CurrentDirectory, "mylogfile3.log")); + File.Delete(Path.Combine(Environment.CurrentDirectory, "mylogfile4.log")); + } + } + + [TestMethod] + [ExpectedException(typeof(LoggerException))] + public void DistributedLoggerBadPath() + { + DistributedFileLogger fileLogger = new DistributedFileLogger(); + fileLogger.NodeId = 0; + fileLogger.Initialize(new EventSourceSink()); + + fileLogger.NodeId = 1; + fileLogger.Parameters = "logfile=" + Path.Combine(Environment.CurrentDirectory, "\\DONTEXIST\\mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + Assert.IsTrue(string.Compare(fileLogger.InternalFilelogger.Parameters, ";ShowCommandLine;logfile=" + Path.Combine(Environment.CurrentDirectory, "\\DONTEXIST\\mylogfile2.log"), StringComparison.OrdinalIgnoreCase) == 0); + } + + [TestMethod] + [ExpectedException(typeof(LoggerException))] + public void DistributedLoggerNullEmpty() + { + DistributedFileLogger fileLogger = new DistributedFileLogger(); + fileLogger.NodeId = 0; + fileLogger.Initialize(new EventSourceSink()); + + fileLogger.NodeId = 1; + fileLogger.Parameters = "logfile="; + fileLogger.Initialize(new EventSourceSink()); + Assert.Fail(); + } + #endregion + + } +} diff --git a/src/XMakeBuildEngine/UnitTests/HashTableUtility_Tests.cs b/src/XMakeBuildEngine/UnitTests/HashTableUtility_Tests.cs new file mode 100644 index 00000000000..d50a9fc550a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/HashTableUtility_Tests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Collections; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Collections; +using System.Collections.Generic; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class HashTableUtilityTests + { + /// + /// Missing unittest found by mutation testing. + /// REASON TEST WASN'T ORIGINALLY PRESENT: HashTableUtility was not a separate class and + /// there was no way to hit this case through BatchingEngine directly because it never + /// calls Compare() with unequal numbers of items. + /// + /// This test ensures that hashtable with unequal numbers of items are considered not + /// equivalent. + /// + [TestMethod] + public void Regress_Mutation_HashtablesWithDifferentCountsAreNotEquivalent() + { + Dictionary h1 = new Dictionary(); + h1["a"] = "x"; // <---------- Must be the same in both hashtables. + Dictionary h2 = new Dictionary(); + h2["a"] = "x"; // <---------- Must be the same in both hashtables. + h2["b"] = "y"; + + Assert.IsTrue(HashTableUtility.Compare(h1, h2) < 0); + Assert.IsTrue(HashTableUtility.Compare(h2, h1) > 0); + } + + [TestMethod] + public void HashtableComparisons() + { + Dictionary h1 = new Dictionary(); + Dictionary h2 = new Dictionary(); + Assert.IsTrue(HashTableUtility.Compare(h1, h2) == 0); + + h1["a"] = "x"; + h2["a"] = "x"; + Assert.IsTrue(HashTableUtility.Compare(h1, h2) == 0); + + h1["b"] = "y"; + h1["c"] = "z"; + h2["b"] = "y"; + h2["c"] = "z"; + Assert.IsTrue(HashTableUtility.Compare(h1, h2) == 0); + + h1["b"] = "j"; + Assert.IsTrue(HashTableUtility.Compare(h1, h2) < 0); + + h2["b"] = "j"; + h2["c"] = "k"; + Assert.IsTrue(HashTableUtility.Compare(h1, h2) > 0); + + h1["a"] = null; + h1["c"] = "k"; + Assert.IsTrue(HashTableUtility.Compare(h1, h2) < 0); + + h2["a"] = null; + Assert.IsTrue(HashTableUtility.Compare(h1, h2) == 0); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Instance/HostServices_Tests.cs b/src/XMakeBuildEngine/UnitTests/Instance/HostServices_Tests.cs new file mode 100644 index 00000000000..cd7affe34a4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Instance/HostServices_Tests.cs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for TaskItem internal members +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for the HostServices object. + /// + [TestClass] + public class HostServices_Tests + { + /// + /// Setup + /// + [TestInitialize] + public void Setup() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Test allowed host object registrations + /// + [TestMethod] + public void TestValidHostObjectRegistration() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + TestHostObject hostObject2 = new TestHostObject(); + TestHostObject hostObject3 = new TestHostObject(); + hostServices.RegisterHostObject("foo.proj", "target", "task", hostObject); + hostServices.RegisterHostObject("foo.proj", "target2", "task", hostObject2); + hostServices.RegisterHostObject("foo.proj", "target", "task2", hostObject3); + + Assert.AreSame(hostObject, hostServices.GetHostObject("foo.proj", "target", "task")); + Assert.AreSame(hostObject2, hostServices.GetHostObject("foo.proj", "target2", "task")); + Assert.AreSame(hostObject3, hostServices.GetHostObject("foo.proj", "target", "task2")); + } + + /// + /// Test ensuring a null project for host object registration throws. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestInvalidHostObjectRegistration_NullProject() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject(null, "target", "task", hostObject); + } + + /// + /// Test ensuring a null target for host object registration throws. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestInvalidHostObjectRegistration_NullTarget() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", null, "task", hostObject); + } + + /// + /// Test ensuring a null task for host object registration throws. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestInvalidHostObjectRegistration_NullTask() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", null, hostObject); + } + + /// + /// Test which verifies host object unregistration. + /// + [TestMethod] + public void TestUnregisterHostObject() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreSame(hostObject, hostServices.GetHostObject("project", "target", "task")); + + hostServices.RegisterHostObject("project", "target", "task", null); + Assert.IsNull(hostServices.GetHostObject("project", "target", "task")); + } + + /// + /// Test which shows that affinity defaults to Any. + /// + [TestMethod] + public void TestAffinityDefaultsToAny() + { + HostServices hostServices = new HostServices(); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + } + + /// + /// Test which shows that setting a host object causes the affinity to become InProc. + /// + [TestMethod] + public void TestHostObjectCausesInProcAffinity() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + } + + /// + /// Test of the ability to set and change specific project affinities. + /// + [TestMethod] + public void TestSpecificAffinityRegistration() + { + HostServices hostServices = new HostServices(); + hostServices.SetNodeAffinity("project", NodeAffinity.InProc); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity("project", NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity("project", NodeAffinity.Any); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + } + + /// + /// Make sure we get the default affinity when the affinity map exists, but the specific + /// project we're requesting is not set. + /// + [TestMethod] + public void TestDefaultAffinityWhenProjectNotRegistered() + { + HostServices hostServices = new HostServices(); + hostServices.SetNodeAffinity("project1", NodeAffinity.InProc); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project2")); + } + + /// + /// Test of setting the default affinity. + /// + [TestMethod] + public void TestGeneralAffinityRegistration() + { + HostServices hostServices = new HostServices(); + + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.InProc); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project2")); + + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project")); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project2")); + + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.Any); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project2")); + } + + /// + /// Test which ensures specific project affinities override general affinity. + /// + [TestMethod] + public void TestOverrideGeneralAffinityRegistration() + { + HostServices hostServices = new HostServices(); + + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.InProc); + hostServices.SetNodeAffinity("project", NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project")); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project2")); + } + + /// + /// Test of clearing the affinity settings for all projects. + /// + [TestMethod] + public void TestClearingAffinities() + { + HostServices hostServices = new HostServices(); + + hostServices.SetNodeAffinity("project", NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity(null, NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity(null, NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + } + + /// + /// Test which ensures that setting an OutOfProc affinity for a project with a host object throws. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void TestContradictoryAffinityCausesException_OutOfProc() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity("project", NodeAffinity.OutOfProc); + } + + /// + /// Test which ensures that setting an Any affinity for a project with a host object throws. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void TestContradictoryAffinityCausesException_Any() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity("project", NodeAffinity.Any); + } + + /// + /// Test which ensures that setting the InProc affinity for a project with a host object is allowed. + /// + [TestMethod] + public void TestNonContradictoryAffinityAllowed() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity("project", NodeAffinity.InProc); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + } + + /// + /// Test which ensures that setting a host object for a project with an out-of-proc affinity throws. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void TestContraditcoryHostObjectCausesException_OutOfProc() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.SetNodeAffinity("project", NodeAffinity.OutOfProc); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + } + + /// + /// Test which ensures the host object can be set for a project which has the Any affinity specifically set. + /// + [TestMethod] + public void TestNonContraditcoryHostObjectAllowed_Any() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.SetNodeAffinity("project", NodeAffinity.Any); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + } + + /// + /// Test which ensures the host object can be set for a project which has an out-of-proc affinity only because that affinity + /// is implied by being set generally for all project, not for that specific project. + /// + [TestMethod] + public void TestNonContraditcoryHostObjectAllowed_ImplicitOutOfProc() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.InProc); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + } + + /// + /// Test which ensures the host object can be set for a project which has the InProc affinity specifically set. + /// + [TestMethod] + public void TestNonContraditcoryHostObjectAllowed_InProc() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.SetNodeAffinity("project", NodeAffinity.InProc); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + } + + /// + /// Test which ensures the affinity for a project can be changed once the host object is cleared. + /// + [TestMethod] + public void TestAffinityChangeAfterClearingHostObject() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + hostServices.RegisterHostObject("project", "target", "task", null); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + hostServices.SetNodeAffinity("project", NodeAffinity.OutOfProc); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project")); + } + + /// + /// Test which ensures that setting then clearing the host object restores a previously specifically set non-conflicting affinity. + /// + [TestMethod] + public void TestUnregisteringNonConflictingHostObjectRestoresOriginalAffinity() + { + HostServices hostServices = new HostServices(); + TestHostObject hostObject = new TestHostObject(); + hostServices.SetNodeAffinity(String.Empty, NodeAffinity.OutOfProc); + hostServices.SetNodeAffinity("project", NodeAffinity.Any); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project2")); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + + hostServices.RegisterHostObject("project", "target", "task", hostObject); + Assert.AreEqual(NodeAffinity.InProc, hostServices.GetNodeAffinity("project")); + hostServices.RegisterHostObject("project", "target", "task", null); + Assert.AreEqual(NodeAffinity.Any, hostServices.GetNodeAffinity("project")); + Assert.AreEqual(NodeAffinity.OutOfProc, hostServices.GetNodeAffinity("project2")); + } + + /// + /// Tests that creating a BuildRequestData with a non-conflicting HostServices and ProjectInstance works. + /// + [TestMethod] + public void TestProjectInstanceWithNonConflictingHostServices() + { + HostServices hostServices = new HostServices(); + ProjectInstance project = CreateDummyProject("foo.proj"); + + BuildRequestData data = new BuildRequestData(project, new string[] { }, hostServices); + + hostServices.SetNodeAffinity(project.FullPath, NodeAffinity.InProc); + BuildRequestData data2 = new BuildRequestData(project, new string[] { }, hostServices); + } + + /// + /// Tests that unloading all projects from the project collection + /// discards the host services + /// + [TestMethod] + public void UnloadedProjectDiscardsHostServicesAllProjects() + { + HostServices hostServices = new HostServices(); + TestHostObject th = new TestHostObject(); + ProjectCollection.GlobalProjectCollection.HostServices = hostServices; + Project project = LoadDummyProject("foo.proj"); + + hostServices.RegisterHostObject(project.FullPath, "test", "Message", th); + + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + + Assert.IsFalse(hostServices.HasHostObject(project.FullPath)); + } + + /// + /// Tests that unloading the last project from the project collection + /// discards the host services for that project + /// + [TestMethod] + public void UnloadedProjectDiscardsHostServices() + { + HostServices hostServices = new HostServices(); + TestHostObject th = new TestHostObject(); + ProjectCollection.GlobalProjectCollection.HostServices = hostServices; + Project project1 = LoadDummyProject("foo.proj"); + Project project2 = LoadDummyProject("foo.proj"); + + hostServices.RegisterHostObject(project1.FullPath, "test", "Message", th); + + ProjectCollection.GlobalProjectCollection.UnloadProject(project1); + + Assert.IsTrue(hostServices.HasHostObject(project2.FullPath)); + + ProjectCollection.GlobalProjectCollection.UnloadProject(project2); + + Assert.IsFalse(hostServices.HasHostObject(project2.FullPath)); + } + + /// + /// Creates a dummy project instance. + /// + public ProjectInstance CreateDummyProject(string fileName) + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + +"); + + Project project = new Project(new XmlTextReader(new StringReader(contents)), new Dictionary(), "4.0"); + project.FullPath = fileName; + ProjectInstance instance = project.CreateProjectInstance(); + + return instance; + } + + /// + /// Loads a dummy project instance. + /// + public Project LoadDummyProject(string fileName) + { + string contents = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + Dictionary globals = new Dictionary(StringComparer.OrdinalIgnoreCase); + globals["UniqueDummy"] = Guid.NewGuid().ToString(); + + Project project = ProjectCollection.GlobalProjectCollection.LoadProject(new XmlTextReader(new StringReader(contents)), globals, "4.0"); + project.FullPath = fileName; + + return project; + } + + /// + /// A dummy host object class. + /// + private class TestHostObject : ITaskHost + { + /// + /// Constructor. + /// + public TestHostObject() + { + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Instance/ProjectInstance_Internal_Tests.cs b/src/XMakeBuildEngine/UnitTests/Instance/ProjectInstance_Internal_Tests.cs new file mode 100644 index 00000000000..da293cc5eed --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Instance/ProjectInstance_Internal_Tests.cs @@ -0,0 +1,642 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for ProjectInstance internal members +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using System.Collections; +using System; +using Microsoft.Build.Construction; +using System.IO; +using System.Xml; +using System.Linq; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectInstance internal members + /// + [TestClass] + public class ProjectInstance_Internal_Tests + { + /// + /// Read task registrations + /// + [TestMethod] + public void GetTaskRegistrations() + { + try + { + string projectFileContent = @" + + + + + + + + "; + + string importContent = @" + + + + + +

v

+
+
"; + + string importPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("import.targets", importContent); + projectFileContent = String.Format(projectFileContent, importPath); + + ProjectInstance project = new Project(ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent)))).CreateProjectInstance(); + + Assert.AreEqual(3, project.TaskRegistry.TaskRegistrations.Count); + Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, "af0"), project.TaskRegistry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("t0", null)][0].TaskFactoryAssemblyLoadInfo.AssemblyFile); + Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, "af1a"), project.TaskRegistry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("t1", null)][0].TaskFactoryAssemblyLoadInfo.AssemblyFile); + Assert.AreEqual("an1", project.TaskRegistry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("t1", null)][1].TaskFactoryAssemblyLoadInfo.AssemblyName); + Assert.AreEqual("an2", project.TaskRegistry.TaskRegistrations[new TaskRegistry.RegisteredTaskIdentity("t2", null)][0].TaskFactoryAssemblyLoadInfo.AssemblyName); + } + finally + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + } + + /// + /// InitialTargets and DefaultTargets with imported projects. + /// DefaultTargets are not read from imported projects. + /// InitialTargets are gathered from imports depth-first. + /// + [TestMethod] + public void InitialTargetsDefaultTargets() + { + try + { + string projectFileContent = @" + + + + "; + + string import1Content = @" + + + "; + + string import2Content = @""; + + string import3Content = @""; + + string import2Path = ObjectModelHelpers.CreateFileInTempProjectDirectory("import2.targets", import2Content); + string import3Path = ObjectModelHelpers.CreateFileInTempProjectDirectory("import3.targets", import3Content); + + import1Content = String.Format(import1Content, import3Path); + string import1Path = ObjectModelHelpers.CreateFileInTempProjectDirectory("import1.targets", import1Content); + + projectFileContent = String.Format(projectFileContent, import1Path, import2Path); + + ProjectInstance project = new Project(ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent)))).CreateProjectInstance(); + + Helpers.AssertListsValueEqual(new string[] { "d0a", "d0b" }, project.DefaultTargets); + Helpers.AssertListsValueEqual(new string[] { "i0a", "i0b", "i1a", "i1b", "i3a", "i3b", "i2a", "i2b" }, project.InitialTargets); + } + finally + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + } + + /// + /// InitialTargets and DefaultTargets with imported projects. + /// DefaultTargets are not read from imported projects. + /// InitialTargets are gathered from imports depth-first. + /// + [TestMethod] + public void InitialTargetsDefaultTargetsEscaped() + { + try + { + string projectFileContent = @" + + "; + + ProjectInstance project = new Project(ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent)))).CreateProjectInstance(); + + Helpers.AssertListsValueEqual(new string[] { "d0a;d0b" }, project.DefaultTargets); + Helpers.AssertListsValueEqual(new string[] { "i0a;i0b" }, project.InitialTargets); + } + finally + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + } + + /// + /// Read property group under target + /// + [TestMethod] + public void GetPropertyGroupUnderTarget() + { + string content = @" + + + + v1 + + + + + "; + + ProjectInstance p = GetProjectInstance(content); + ProjectPropertyGroupTaskInstance propertyGroup = (ProjectPropertyGroupTaskInstance)(p.Targets["t"].Children[0]); + + Assert.AreEqual("c1", propertyGroup.Condition); + + List properties = Helpers.MakeList(propertyGroup.Properties); + Assert.AreEqual(2, properties.Count); + + Assert.AreEqual("c2", properties[0].Condition); + Assert.AreEqual("v1", properties[0].Value); + + Assert.AreEqual(String.Empty, properties[1].Condition); + Assert.AreEqual(String.Empty, properties[1].Value); + } + + /// + /// Read item group under target + /// + [TestMethod] + public void GetItemGroupUnderTarget() + { + string content = @" + + + + + m1 + n1 + + + + o1 + + + + + "; + + ProjectInstance p = GetProjectInstance(content); + ProjectItemGroupTaskInstance itemGroup = (ProjectItemGroupTaskInstance)(p.Targets["t"].Children[0]); + + Assert.AreEqual("c1", itemGroup.Condition); + + List items = Helpers.MakeList(itemGroup.Items); + Assert.AreEqual(3, items.Count); + + Assert.AreEqual("i1", items[0].Include); + Assert.AreEqual("e1", items[0].Exclude); + Assert.AreEqual(String.Empty, items[0].Remove); + Assert.AreEqual("c2", items[0].Condition); + + Assert.AreEqual(String.Empty, items[1].Include); + Assert.AreEqual(String.Empty, items[1].Exclude); + Assert.AreEqual("r1", items[1].Remove); + Assert.AreEqual(String.Empty, items[1].Condition); + + Assert.AreEqual(String.Empty, items[2].Include); + Assert.AreEqual(String.Empty, items[2].Exclude); + Assert.AreEqual(String.Empty, items[2].Remove); + Assert.AreEqual(String.Empty, items[2].Condition); + + List metadata1 = Helpers.MakeList(items[0].Metadata); + List metadata2 = Helpers.MakeList(items[1].Metadata); + List metadata3 = Helpers.MakeList(items[2].Metadata); + + Assert.AreEqual(2, metadata1.Count); + Assert.AreEqual(0, metadata2.Count); + Assert.AreEqual(1, metadata3.Count); + + Assert.AreEqual("c3", metadata1[0].Condition); + Assert.AreEqual("m1", metadata1[0].Value); + Assert.AreEqual(String.Empty, metadata1[1].Condition); + Assert.AreEqual("n1", metadata1[1].Value); + + Assert.AreEqual(String.Empty, metadata3[0].Condition); + Assert.AreEqual("o1", metadata3[0].Value); + } + + /// + /// Task registry accessor + /// + [TestMethod] + public void GetTaskRegistry() + { + ProjectInstance p = GetSampleProjectInstance(); + + Assert.AreEqual(true, p.TaskRegistry != null); + } + + /// + /// Global properties accessor + /// + [TestMethod] + public void GetGlobalProperties() + { + ProjectInstance p = GetSampleProjectInstance(); + + Assert.AreEqual("v1", p.GlobalPropertiesDictionary["g1"].EvaluatedValue); + Assert.AreEqual("v2", p.GlobalPropertiesDictionary["g2"].EvaluatedValue); + } + + /// + /// ToolsVersion accessor + /// + [TestMethod] + public void GetToolsVersion() + { + ProjectInstance p = GetSampleProjectInstance(); + + Assert.AreEqual("4.0", p.Toolset.ToolsVersion); + } + + /// + /// Toolset data is cloned properly + /// + [TestMethod] + public void CloneToolsetData() + { + var projectCollection = new ProjectCollection(); + CreateMockToolsetIfNotExists("TESTTV", projectCollection); + ProjectInstance first = GetSampleProjectInstance(null, null, projectCollection, toolsVersion: "TESTTV"); + ProjectInstance second = first.DeepCopy(); + Assert.AreEqual(first.ToolsVersion, second.ToolsVersion); + Assert.AreEqual(first.ExplicitToolsVersion, second.ExplicitToolsVersion); + Assert.AreEqual(first.ExplicitToolsVersionSpecified, second.ExplicitToolsVersionSpecified); + } + + /// + /// Test ProjectInstance's surfacing of the sub-toolset version + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void GetSubToolsetVersion() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + ProjectInstance p = GetSampleProjectInstance(null, null, new ProjectCollection()); + + Assert.AreEqual("4.0", p.Toolset.ToolsVersion); + Assert.AreEqual(p.Toolset.DefaultSubToolsetVersion, p.SubToolsetVersion); + Assert.AreEqual(p.Toolset.DefaultSubToolsetVersion, p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Test ProjectInstance's surfacing of the sub-toolset version when it is overridden by a value in the + /// environment + /// + [TestMethod] + public void GetSubToolsetVersion_FromEnvironment() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "ABCD"); + + ProjectInstance p = GetSampleProjectInstance(null, null, new ProjectCollection()); + + Assert.AreEqual("4.0", p.Toolset.ToolsVersion); + Assert.AreEqual("ABCD", p.SubToolsetVersion); + Assert.AreEqual("ABCD", p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Test ProjectInstance's surfacing of the sub-toolset version when it is overridden by a global property + /// + [TestMethod] + public void GetSubToolsetVersion_FromProjectGlobalProperties() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("VisualStudioVersion", "ABCDE"); + + ProjectInstance p = GetSampleProjectInstance(null, globalProperties, new ProjectCollection()); + + Assert.AreEqual("4.0", p.Toolset.ToolsVersion); + Assert.AreEqual("ABCDE", p.SubToolsetVersion); + Assert.AreEqual("ABCDE", p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that if a sub-toolset version is passed to the constructor, it all other heuristic methods for + /// getting the sub-toolset version. + /// + [TestMethod] + public void GetSubToolsetVersion_FromConstructor() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "ABC"); + + string projectContent = @" + + + + "; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectContent))); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("VisualStudioVersion", "ABCD"); + + IDictionary projectCollectionGlobalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + projectCollectionGlobalProperties.Add("VisualStudioVersion", "ABCDE"); + + ProjectInstance p = new ProjectInstance(xml, globalProperties, "4.0", "ABCDEF", new ProjectCollection(projectCollectionGlobalProperties)); + + Assert.AreEqual("4.0", p.Toolset.ToolsVersion); + Assert.AreEqual("ABCDEF", p.SubToolsetVersion); + Assert.AreEqual("ABCDEF", p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// DefaultTargets accessor + /// + [TestMethod] + public void GetDefaultTargets() + { + ProjectInstance p = GetSampleProjectInstance(); + + Helpers.AssertListsValueEqual(new string[] { "dt" }, p.DefaultTargets); + } + + /// + /// InitialTargets accessor + /// + [TestMethod] + public void GetInitialTargets() + { + ProjectInstance p = GetSampleProjectInstance(); + + Helpers.AssertListsValueEqual(new string[] { "it" }, p.InitialTargets); + } + + /// + /// Cloning project clones targets + /// + [TestMethod] + public void CloneTargets() + { + var hostServices = new HostServices(); + + ProjectInstance first = GetSampleProjectInstance(hostServices); + ProjectInstance second = first.DeepCopy(); + + // Targets, tasks are immutable so we can expect the same objects + Assert.IsTrue(Object.ReferenceEquals(first.Targets, second.Targets)); + Assert.IsTrue(Object.ReferenceEquals(first.Targets["t"], second.Targets["t"])); + + var firstTasks = first.Targets["t"]; + var secondTasks = second.Targets["t"]; + + Assert.IsTrue(Object.ReferenceEquals(firstTasks.Children[0], secondTasks.Children[0])); + } + + /// + /// Cloning project copies task registry + /// + [TestMethod] + public void CloneTaskRegistry() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + // Task registry object should be immutable + Assert.ReferenceEquals(first.TaskRegistry, second.TaskRegistry); + } + + /// + /// Cloning project copies global properties + /// + [TestMethod] + public void CloneGlobalProperties() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Assert.AreEqual("v1", second.GlobalPropertiesDictionary["g1"].EvaluatedValue); + Assert.AreEqual("v2", second.GlobalPropertiesDictionary["g2"].EvaluatedValue); + } + + /// + /// Cloning project copies default targets + /// + [TestMethod] + public void CloneDefaultTargets() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Helpers.AssertListsValueEqual(new string[] { "dt" }, second.DefaultTargets); + } + + /// + /// Cloning project copies initial targets + /// + [TestMethod] + public void CloneInitialTargets() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Helpers.AssertListsValueEqual(new string[] { "it" }, second.InitialTargets); + } + + /// + /// Cloning project copies toolsversion + /// + [TestMethod] + public void CloneToolsVersion() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Assert.AreEqual(first.Toolset, second.Toolset); + } + + /// + /// Tests building a simple project and verifying the log looks as expected. + /// + [TestMethod] + public void Build() + { + string projectFileContent = @" + + + + + + + + + + "; + + ProjectInstance projectInstance = GetProjectInstance(projectFileContent); + List loggers = new List(); + MockLogger mockLogger = new MockLogger(); + loggers.Add(mockLogger); + bool success = projectInstance.Build("Build", loggers); + + Assert.IsTrue(success); + mockLogger.AssertLogContains(new string[] { "Building...", "Completed!" }); + } + + /// + /// Create a ProjectInstance from provided project content + /// + private static ProjectInstance GetProjectInstance(string content) + { + return GetProjectInstance(content, null); + } + + /// + /// Create a ProjectInstance from provided project content and host services object + /// + private static ProjectInstance GetProjectInstance(string content, HostServices hostServices) + { + return GetProjectInstance(content, hostServices, null, null); + } + + /// + /// Create a ProjectInstance from provided project content and host services object + /// + private static ProjectInstance GetProjectInstance(string content, HostServices hostServices, IDictionary globalProperties, ProjectCollection projectCollection, string toolsVersion = null) + { + XmlReader reader = XmlReader.Create(new StringReader(content)); + + if (globalProperties == null) + { + // choose some interesting defaults if we weren't explicitly asked to use a set. + globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("g1", "v1"); + globalProperties.Add("g2", "v2"); + } + + Project project = new Project(reader, globalProperties, toolsVersion ?? "4.0", projectCollection ?? ProjectCollection.GlobalProjectCollection); + + ProjectInstance instance = project.CreateProjectInstance(); + + return instance; + } + + /// + /// Create a ProjectInstance with some items and properties and targets + /// + private static ProjectInstance GetSampleProjectInstance() + { + return GetSampleProjectInstance(null); + } + + /// + /// Create a ProjectInstance with some items and properties and targets + /// + private static ProjectInstance GetSampleProjectInstance(HostServices hostServices) + { + return GetSampleProjectInstance(hostServices, null, null); + } + + /// + /// Create a ProjectInstance with some items and properties and targets + /// + private static ProjectInstance GetSampleProjectInstance(HostServices hostServices, IDictionary globalProperties, ProjectCollection projectCollection, string toolsVersion = null) + { + string toolsVersionSubstring = toolsVersion != null ? "ToolsVersion=\"" + toolsVersion + "\" " : String.Empty; + string content = @" + + + v1 + v2 + $(p2)X$(p) + + + + + m1 + + + + + + + + + + "; + + ProjectInstance p = GetProjectInstance(content, hostServices, globalProperties, projectCollection, toolsVersion); + + return p; + } + + /// + /// Creates a toolset with the given tools version if one does not already exist. + /// + private static void CreateMockToolsetIfNotExists(string toolsVersion, ProjectCollection projectCollection) + { + ProjectCollection pc = projectCollection; + if (!pc.Toolsets.Any(t => String.Equals(t.ToolsVersion, toolsVersion, StringComparison.OrdinalIgnoreCase))) + { + Toolset template = pc.Toolsets.First(t => String.Equals(t.ToolsVersion, pc.DefaultToolsVersion, StringComparison.OrdinalIgnoreCase)); + var toolset = new Toolset( + toolsVersion, + template.ToolsPath, + template.Properties.ToDictionary(p => p.Key, p => p.Value.EvaluatedValue), + pc, + null); + pc.AddToolset(toolset); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Instance/ProjectMetadataInstance_Internal_Tests.cs b/src/XMakeBuildEngine/UnitTests/Instance/ProjectMetadataInstance_Internal_Tests.cs new file mode 100644 index 00000000000..35c27a0490d --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Instance/ProjectMetadataInstance_Internal_Tests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for ProjectMetadataInstance internal members +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.UnitTests.BackEnd; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectMetadataInstance internal members + /// + [TestClass] + public class ProjectMetadataInstance_Internal_Tests + { + /// + /// Cloning + /// + [TestMethod] + public void DeepClone() + { + ProjectMetadataInstance metadata = GetMetadataInstance(); + + ProjectMetadataInstance clone = metadata.DeepClone(); + + Assert.AreEqual(false, Object.ReferenceEquals(metadata, clone)); + Assert.AreEqual("m", clone.Name); + Assert.AreEqual("m1", clone.EvaluatedValue); + } + + /// + /// Tests serialization + /// + [TestMethod] + public void Serialization() + { + ProjectMetadataInstance metadata = new ProjectMetadataInstance("m1", "v1", false); + + TranslationHelpers.GetWriteTranslator().Translate(ref metadata, ProjectMetadataInstance.FactoryForDeserialization); + ProjectMetadataInstance deserializedMetadata = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedMetadata, ProjectMetadataInstance.FactoryForDeserialization); + + Assert.AreEqual(metadata.Name, deserializedMetadata.Name); + Assert.AreEqual(metadata.EvaluatedValue, deserializedMetadata.EvaluatedValue); + } + + /// + /// Get a single metadata instance + /// + private static ProjectMetadataInstance GetMetadataInstance() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectItemInstance item = projectInstance.AddItem("i", "i1"); + ProjectMetadataInstance metadata = item.SetMetadata("m", "m1"); + return metadata; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Instance/ProjectPropertyInstance_Internal_Tests.cs b/src/XMakeBuildEngine/UnitTests/Instance/ProjectPropertyInstance_Internal_Tests.cs new file mode 100644 index 00000000000..a3da7b94656 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Instance/ProjectPropertyInstance_Internal_Tests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for ProjectPropertyInstance internal members +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.UnitTests.BackEnd; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectPropertyInstance internal members + /// + [TestClass] + public class ProjectPropertyInstance_Internal_Tests + { + /// + /// Cloning + /// + [TestMethod] + public void DeepClone() + { + ProjectPropertyInstance property = GetPropertyInstance(); + + ProjectPropertyInstance clone = property.DeepClone(); + + Assert.AreEqual(false, Object.ReferenceEquals(property, clone)); + Assert.AreEqual("p", clone.Name); + Assert.AreEqual("v1", clone.EvaluatedValue); + } + + /// + /// Serialization test + /// + [TestMethod] + public void Serialization() + { + ProjectPropertyInstance property = GetPropertyInstance(); + + TranslationHelpers.GetWriteTranslator().Translate(ref property, ProjectPropertyInstance.FactoryForDeserialization); + ProjectPropertyInstance deserializedProperty = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedProperty, ProjectPropertyInstance.FactoryForDeserialization); + + Assert.AreEqual(property.Name, deserializedProperty.Name); + Assert.AreEqual(property.EvaluatedValue, deserializedProperty.EvaluatedValue); + } + + /// + /// Tests serialization. + /// + [TestMethod] + public void ProjectPropertyInstanceSerializationTest_Mutable() + { + var property = ProjectPropertyInstance.Create("p", "v", false /*mutable*/); + Assert.AreEqual(false, property.IsImmutable); + + TranslationHelpers.GetWriteTranslator().Translate(ref property, ProjectPropertyInstance.FactoryForDeserialization); + ProjectPropertyInstance deserializedProperty = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedProperty, ProjectPropertyInstance.FactoryForDeserialization); + + Assert.AreEqual(property.Name, deserializedProperty.Name); + Assert.AreEqual(property.EvaluatedValue, deserializedProperty.EvaluatedValue); + Assert.AreEqual(property.IsImmutable, deserializedProperty.IsImmutable); + Assert.AreEqual(typeof(ProjectPropertyInstance), property.GetType()); + } + + /// + /// Tests serialization. + /// + [TestMethod] + public void ProjectPropertyInstanceSerializationTest_Immutable() + { + var property = ProjectPropertyInstance.Create("p", "v", mayBeReserved: true, isImmutable: true); + Assert.AreEqual(true, property.IsImmutable); + + TranslationHelpers.GetWriteTranslator().Translate(ref property, ProjectPropertyInstance.FactoryForDeserialization); + ProjectPropertyInstance deserializedProperty = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedProperty, ProjectPropertyInstance.FactoryForDeserialization); + + Assert.AreEqual(property.Name, deserializedProperty.Name); + Assert.AreEqual(property.EvaluatedValue, deserializedProperty.EvaluatedValue); + Assert.AreEqual(property.IsImmutable, deserializedProperty.IsImmutable); + Assert.AreEqual("Microsoft.Build.Execution.ProjectPropertyInstance+ProjectPropertyInstanceImmutable", property.GetType().ToString()); + } + + /// + /// Get a ProjectPropertyInstance + /// + private static ProjectPropertyInstance GetPropertyInstance() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectPropertyInstance property = projectInstance.SetProperty("p", "v1"); + + return property; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Instance/TaskItem_Tests.cs b/src/XMakeBuildEngine/UnitTests/Instance/TaskItem_Tests.cs new file mode 100644 index 00000000000..7d82d2c6221 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Instance/TaskItem_Tests.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for TaskItem internal members +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.UnitTests.BackEnd; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using System.Xml; +using Microsoft.Build.Framework; +using System.IO; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectPropertyInstance internal members + /// + [TestClass] + public class TaskItem_Tests + { + /// + /// Test serialization + /// + [TestMethod] + public void Serialization() + { + TaskItem item = new TaskItem("foo", "bar.proj"); + item.SetMetadata("a", "b"); + + TranslationHelpers.GetWriteTranslator().Translate(ref item, TaskItem.FactoryForDeserialization); + TaskItem deserializedItem = null; + TranslationHelpers.GetReadTranslator().Translate(ref deserializedItem, TaskItem.FactoryForDeserialization); + + Assert.AreEqual(item.ItemSpec, deserializedItem.ItemSpec); + Assert.AreEqual(item.MetadataCount, deserializedItem.MetadataCount); + Assert.AreEqual(item.GetMetadata("a"), deserializedItem.GetMetadata("a")); + Assert.AreEqual(item.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath), deserializedItem.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath)); + } + + /// + /// Ensure an item is equivalent to itself. + /// + [TestMethod] + public void TestEquivalenceIdentity() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + + Assert.IsTrue(left.Equals(left)); + } + + /// + /// Ensure two items with the same item spec and no metadata are equivalent + /// + [TestMethod] + public void TestEquivalence() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + TaskItem right = new TaskItem("foo", "bar.proj"); + + Assert.IsTrue(left == right); + Assert.IsFalse(left != right); + } + + /// + /// Ensure two items with the same custom metadata are equivalent + /// + [TestMethod] + public void TestEquivalenceWithCustomMetadata() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + left.SetMetadata("a", "b"); + TaskItem right = new TaskItem("foo", "bar.proj"); + right.SetMetadata("a", "b"); + + Assert.IsTrue(left == right); + Assert.IsFalse(left != right); + } + + /// + /// Ensure two items with different custom metadata values are not equivalent + /// + [TestMethod] + public void TestInequivalenceWithDifferentCustomMetadataValues() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + left.SetMetadata("a", "b"); + TaskItem right = new TaskItem("foo", "bar.proj"); + right.SetMetadata("a", "c"); + + Assert.IsFalse(left == right); + Assert.IsTrue(left != right); + } + + /// + /// Ensure two items with different custom metadata keys are not equivalent + /// + [TestMethod] + public void TestInequivalenceWithDifferentCustomMetadataKeys() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + left.SetMetadata("a", "b"); + TaskItem right = new TaskItem("foo", "bar.proj"); + right.SetMetadata("b", "b"); + + Assert.IsFalse(left == right); + Assert.IsTrue(left != right); + } + + /// + /// Ensure two items with different numbers of custom metadata are not equivalent + /// + [TestMethod] + public void TestInequivalenceWithDifferentCustomMetadataCount() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + left.SetMetadata("a", "b"); + TaskItem right = new TaskItem("foo", "bar.proj"); + + Assert.IsFalse(left == right); + Assert.IsTrue(left != right); + } + + /// + /// Ensure two items with different numbers of custom metadata are not equivalent + /// + [TestMethod] + public void TestInequivalenceWithDifferentCustomMetadataCount2() + { + TaskItem left = new TaskItem("foo", "bar.proj"); + left.SetMetadata("a", "b"); + TaskItem right = new TaskItem("foo", "bar.proj"); + right.SetMetadata("a", "b"); + right.SetMetadata("c", "d"); + + Assert.IsFalse(left == right); + Assert.IsTrue(left != right); + } + + /// + /// Ensure when cloning an Item that the clone is equivilant to the parent item and that they are not the same object. + /// + [TestMethod] + public void TestDeepClone() + { + TaskItem parent = new TaskItem("foo", "bar.proj"); + parent.SetMetadata("a", "b"); + parent.SetMetadata("c", "d"); + + TaskItem clone = parent.DeepClone(); + Assert.IsTrue(parent.Equals(clone), "The parent and the clone should be equal"); + Assert.IsFalse(object.ReferenceEquals(parent, clone), "The parent and the child should not be the same object"); + } + + /// + /// Flushing an item through a task should not mess up special characters on the metadata. + /// + [TestMethod] + public void Escaping1() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + i1m1;i1m2 + + + j1m1;j1m2 + + + + + + + + + + + + + + + + + + + + + + + + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlTextReader.Create(new StringReader(content))); + + Project project = new Project(xml); + MockLogger logger = new MockLogger(); + project.Build("Build", new ILogger[] { logger }); + + logger.AssertLogContains("[i1m1]"); + logger.AssertLogContains("[i1m2]"); + logger.AssertLogContains("[j1m1]"); + logger.AssertLogContains("[j1m2]"); + } + + /// + /// Flushing an item through a task run in the task host also should not mess up special characters on the metadata. + /// + [TestMethod] + public void Escaping2() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + i1m1;i1m2 + + + j1m1;j1m2 + + + + + + + + + + + + + + + + + + + + + + + + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlTextReader.Create(new StringReader(content))); + + Project project = new Project(xml); + MockLogger logger = new MockLogger(); + project.Build("Build", new ILogger[] { logger }); + + logger.AssertLogContains("[i1m1]"); + logger.AssertLogContains("[i1m2]"); + logger.AssertLogContains("[j1m1]"); + logger.AssertLogContains("[j1m2]"); + } + + /// + /// Flushing an item through a task run in the task host also should not mess up the escaping of the itemspec either. + /// + [TestMethod] + public void Escaping3() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + + + + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlTextReader.Create(new StringReader(content))); + + Project project = new Project(xml); + MockLogger logger = new MockLogger(); + project.Build("Build", new ILogger[] { logger }); + + logger.AssertLogContains("i1%2ai2"); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/InvalidProjectFileException_Tests.cs b/src/XMakeBuildEngine/UnitTests/InvalidProjectFileException_Tests.cs new file mode 100644 index 00000000000..f54e59834cd --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/InvalidProjectFileException_Tests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Exceptions; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class InvalidProjectFileExceptionTests + { + /// + /// Verify I implemented ISerializable correctly + /// + [TestMethod] + public void SerializeDeserialize() + { + InvalidProjectFileException e = new InvalidProjectFileException( + "projectFile", + 1, 2, 3, 4, + "message", + "errorSubcategory", + "errorCode", + "helpKeyword"); + + using (MemoryStream memstr = new MemoryStream()) + { + BinaryFormatter frm = new BinaryFormatter(); + + frm.Serialize(memstr, e); + memstr.Position = 0; + + InvalidProjectFileException e2 = (InvalidProjectFileException)frm.Deserialize(memstr); + + Assert.AreEqual(e.ColumnNumber, e2.ColumnNumber); + Assert.AreEqual(e.EndColumnNumber, e2.EndColumnNumber); + Assert.AreEqual(e.EndLineNumber, e2.EndLineNumber); + Assert.AreEqual(e.ErrorCode, e2.ErrorCode); + Assert.AreEqual(e.ErrorSubcategory, e2.ErrorSubcategory); + Assert.AreEqual(e.HasBeenLogged, e2.HasBeenLogged); + Assert.AreEqual(e.HelpKeyword, e2.HelpKeyword); + Assert.AreEqual(e.LineNumber, e2.LineNumber); + Assert.AreEqual(e.Message, e2.Message); + Assert.AreEqual(e.ProjectFile, e2.ProjectFile); + } + } + + /// + /// Verify that nesting an IPFE copies the error code + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ErrorCodeShouldAppearForCircularDependency() + { + string file = Path.GetTempPath() + Guid.NewGuid().ToString("N"); + + try + { + File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents(@" + + + + + + ")); + + MockLogger ml = ObjectModelHelpers.BuildTempProjectFileExpectFailure(file); + + // Make sure the log contains the error code and file/line/col for the circular dependency + ml.AssertLogContains("MSB4006"); + ml.AssertLogContains("(4,29)"); + ml.AssertLogContains(file); + } + finally + { + File.Delete(file); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/LogFormatter_Tests.cs b/src/XMakeBuildEngine/UnitTests/LogFormatter_Tests.cs new file mode 100644 index 00000000000..1d673bc72de --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/LogFormatter_Tests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using System.Text.RegularExpressions; +using System.Globalization; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class LogFormatterTest + { + /* + * Method: TimeSpanMediumDuration + * + * Tests the mainline: a medium length duration + * Note the ToString overload used in FormatTimeSpan is culture insensitive. + */ + [TestMethod] + public void TimeSpanMediumDuration() + { + TimeSpan t = new TimeSpan(1254544900); + string result = LogFormatter.FormatTimeSpan(t); + Assert.AreEqual("00:02:05.45", result); + } + + + /* + * Method: TimeSpanZeroDuration + * + * Format a TimeSpan where the duration is zero. + * Note the ToString overload used in FormatTimeSpan is culture insensitive. + */ + [TestMethod] + public void TimeSpanZeroDuration() + { + TimeSpan t = new TimeSpan(0); + string result = LogFormatter.FormatTimeSpan(t); + Assert.AreEqual("00:00:00", result); + } + + [TestMethod] + public void FormatDateTime() + { + DateTime testTime = new DateTime(2007 /*Year*/, 08 /*Month*/, 20 /*Day*/, 10 /*Hour*/, 42 /*Minutes*/, 44 /*Seconds*/, 12 /*Milliseconds*/); + string result = LogFormatter.FormatLogTimeStamp(testTime); + + Assert.AreEqual(testTime.ToString("HH:mm:ss.fff", CultureInfo.CurrentCulture), result); + + testTime = new DateTime(2007, 08, 20, 05, 04, 03, 01); + result = LogFormatter.FormatLogTimeStamp(testTime); + Assert.AreEqual(testTime.ToString("HH:mm:ss.fff", CultureInfo.CurrentCulture), result); + + testTime = new DateTime(2007, 08, 20, 0, 0, 0, 0); + result = LogFormatter.FormatLogTimeStamp(testTime); + Assert.AreEqual(testTime.ToString("HH:mm:ss.fff", CultureInfo.CurrentCulture), result); + } + } +} + + + + + diff --git a/src/XMakeBuildEngine/UnitTests/LoggerDescription_Tests.cs b/src/XMakeBuildEngine/UnitTests/LoggerDescription_Tests.cs new file mode 100644 index 00000000000..d4c5e7664c2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/LoggerDescription_Tests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; + +using Microsoft.Build.Logging; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class LoggerDescription_Tests + { + [TestMethod] + public void LoggerDescriptionCustomSerialization() + { + string className = "Class"; + string loggerAssemblyName = "Class"; + string loggerFileAssembly = null; + string loggerSwitchParameters = "Class"; + LoggerVerbosity verbosity = LoggerVerbosity.Detailed; + + LoggerDescription description = new LoggerDescription(className, loggerAssemblyName, loggerFileAssembly, loggerSwitchParameters, verbosity); + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(stream); + BinaryReader reader = new BinaryReader(stream); + try + { + stream.Position = 0; + description.WriteToStream(writer); + long streamWriteEndPosition = stream.Position; + stream.Position = 0; + LoggerDescription description2 = new LoggerDescription(); + description2.CreateFromStream(reader); + long streamReadEndPosition = stream.Position; + Assert.IsTrue(streamWriteEndPosition == streamReadEndPosition, "Stream end positions should be equal"); + + Assert.IsTrue(description.Verbosity == description2.Verbosity, "Expected Verbosity to Match"); + Assert.IsTrue(description.LoggerId == description2.LoggerId, "Expected Verbosity to Match"); + Assert.IsTrue(string.Compare(description.LoggerSwitchParameters, description2.LoggerSwitchParameters, StringComparison.OrdinalIgnoreCase) == 0, "Expected LoggerSwitchParameters to Match"); + Assert.IsTrue(string.Compare(description.Name, description2.Name, StringComparison.OrdinalIgnoreCase) == 0, "Expected Name to Match"); + } + finally + { + reader.Close(); + writer = null; + stream = null; + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/LoggerException_Tests.cs b/src/XMakeBuildEngine/UnitTests/LoggerException_Tests.cs new file mode 100644 index 00000000000..7ba51581f5b --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/LoggerException_Tests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Exceptions; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class InternalLoggerExceptionTests + { + /// + /// Verify I implemented ISerializable correctly + /// + [TestMethod] + public void SerializeDeserialize() + { + InternalLoggerException e = new InternalLoggerException("message", + new Exception("innerException"), + new BuildStartedEventArgs("evMessage", "evHelpKeyword"), + "errorCode", + "helpKeyword", + false); + + using (MemoryStream memstr = new MemoryStream()) + { + BinaryFormatter frm = new BinaryFormatter(); + + frm.Serialize(memstr, e); + memstr.Position = 0; + + InternalLoggerException e2 = (InternalLoggerException)frm.Deserialize(memstr); + + Assert.AreEqual(e.BuildEventArgs.Message, e2.BuildEventArgs.Message); + Assert.AreEqual(e.BuildEventArgs.HelpKeyword, e2.BuildEventArgs.HelpKeyword); + Assert.AreEqual(e.ErrorCode, e2.ErrorCode); + Assert.AreEqual(e.HelpKeyword, e2.HelpKeyword); + Assert.AreEqual(e.Message, e2.Message); + Assert.AreEqual(e.InnerException.Message, e2.InnerException.Message); + } + } + } +} + + + + + diff --git a/src/XMakeBuildEngine/UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/XMakeBuildEngine/UnitTests/Microsoft.Build.Engine.UnitTests.csproj new file mode 100644 index 00000000000..14dbee117ea --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -0,0 +1,215 @@ + + + + + Debug + AnyCPU + {D06D5D07-9DFB-4896-B11F-0A8C44F8F971} + Library + Microsoft.Build.Engine.UnitTests + Microsoft.Build.Engine.UnitTests + + + + + + + + + true + + + + true + + + true + + + true + + + true + + + true + + + HybridDictionary_Tests.cs + + + true + + + true + + + true + + + true + + + true + + + true + XmakeAttributes_Tests.cs + + + true + + + true + AssemblyNameEx_Tests.cs + + + TaskParameter_Tests.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + App.config + Designer + + + Microsoft.Common.overridetasks + Always + + + Microsoft.Common.tasks + Always + + + + + {828566ee-6f6a-4ef4-98b0-513f7df9c628} + Microsoft.Build.Utilities + + + {59a73fe0-d3b7-4299-9063-3a587d429af4} + Microsoft.Build.Tasks + + + {16cd7635-7cf4-4c62-a77b-cf87d0f09a58} + Microsoft.Build + + + + + + + {23c9fd0e-70c5-4f1f-b08a-d2774240fb51} + MSBuild + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/MockElementLocation.cs b/src/XMakeBuildEngine/UnitTests/MockElementLocation.cs new file mode 100644 index 00000000000..0ed9458de79 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/MockElementLocation.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A dummy element location +//----------------------------------------------------------------------- + +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; +using System; + +namespace Microsoft.Build.UnitTests +{ + /// + /// A dummy element location. + /// + internal class MockElementLocation : ElementLocation + { + /// + /// Single instance + /// + private static MockElementLocation s_instance = new MockElementLocation(); + + /// + /// Private constructor + /// + private MockElementLocation() + { + } + + /// + /// File of element, eg a targets file + /// + public override string File + { + get { return "mock.targets"; } + } + + /// + /// Line number + /// + public override int Line + { + get { return 0; } + } + + /// + /// Column number + /// + public override int Column + { + get { return 1; } + } + + /// + /// Get single instance + /// + internal static MockElementLocation Instance + { + get { return s_instance; } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/MockTask.cs b/src/XMakeBuildEngine/UnitTests/MockTask.cs new file mode 100644 index 00000000000..fcdd9fcdbe7 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/MockTask.cs @@ -0,0 +1,480 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Execution; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests +{ + internal class MockTaskBase + { + private bool _myBoolParam = false; + private bool[] _myBoolArrayParam = null; + private int _myIntParam = 0; + private int[] _myIntArrayParam = null; + private string _myStringParam = null; + private string[] _myStringArrayParam = null; + private ITaskItem _myITaskItemParam = null; + private ITaskItem[] _myITaskItemArrayParam = null; + + private bool _myRequiredBoolParam = false; + private bool[] _myRequiredBoolArrayParam = null; + private int _myRequiredIntParam = 0; + private int[] _myRequiredIntArrayParam = null; + private string _myRequiredStringParam = null; + private string[] _myRequiredStringArrayParam = null; + private ITaskItem _myRequiredITaskItemParam = null; + private ITaskItem[] _myRequiredITaskItemArrayParam = null; + + internal bool myBoolParamWasSet = false; + internal bool myBoolArrayParamWasSet = false; + internal bool myIntParamWasSet = false; + internal bool myIntArrayParamWasSet = false; + internal bool myStringParamWasSet = false; + internal bool myStringArrayParamWasSet = false; + internal bool myITaskItemParamWasSet = false; + internal bool myITaskItemArrayParamWasSet = false; + + // disable csharp compiler warning #0414: field assigned unused value +#pragma warning disable 0414 + internal bool myRequiredBoolParamWasSet = false; + internal bool myRequiredBoolArrayParamWasSet = false; + internal bool myRequiredIntParamWasSet = false; + internal bool myRequiredIntArrayParamWasSet = false; + internal bool myRequiredStringParamWasSet = false; + internal bool myRequiredStringArrayParamWasSet = false; + internal bool myRequiredITaskItemParamWasSet = false; + internal bool myRequiredITaskItemArrayParamWasSet = false; +#pragma warning restore 0414 + + /// + /// Single bool parameter. + /// + public bool MyBoolParam + { + get { return _myBoolParam; } + set { _myBoolParam = value; this.myBoolParamWasSet = true; } + } + + /// + /// bool[] parameter. + /// + public bool[] MyBoolArrayParam + { + get { return _myBoolArrayParam; } + set { _myBoolArrayParam = value; this.myBoolArrayParamWasSet = true; } + } + + /// + /// Single int parameter. + /// + public int MyIntParam + { + get { return _myIntParam; } + set { _myIntParam = value; this.myIntParamWasSet = true; } + } + + /// + /// int[] parameter. + /// + public int[] MyIntArrayParam + { + get { return _myIntArrayParam; } + set { _myIntArrayParam = value; this.myIntArrayParamWasSet = true; } + } + + /// + /// Single string parameter + /// + public string MyStringParam + { + get { return _myStringParam; } + set { _myStringParam = value; this.myStringParamWasSet = true; } + } + + /// + /// A string array parameter. + /// + public string[] MyStringArrayParam + { + get { return _myStringArrayParam; } + set { _myStringArrayParam = value; this.myStringArrayParamWasSet = true; } + } + + /// + /// Single ITaskItem parameter. + /// + public ITaskItem MyITaskItemParam + { + get { return _myITaskItemParam; } + set { _myITaskItemParam = value; this.myITaskItemParamWasSet = true; } + } + + /// + /// ITaskItem[] parameter. + /// + public ITaskItem[] MyITaskItemArrayParam + { + get { return _myITaskItemArrayParam; } + set { _myITaskItemArrayParam = value; this.myITaskItemArrayParamWasSet = true; } + } + + /// + /// Single bool parameter. + /// + [Required] + public bool MyRequiredBoolParam + { + get { return _myRequiredBoolParam; } + set { _myRequiredBoolParam = value; this.myRequiredBoolParamWasSet = true; } + } + + /// + /// bool[] parameter. + /// + [Required] + public bool[] MyRequiredBoolArrayParam + { + get { return _myRequiredBoolArrayParam; } + set { _myRequiredBoolArrayParam = value; this.myRequiredBoolArrayParamWasSet = true; } + } + + /// + /// Single int parameter. + /// + [Required] + public int MyRequiredIntParam + { + get { return _myRequiredIntParam; } + set { _myRequiredIntParam = value; this.myRequiredIntParamWasSet = true; } + } + + /// + /// int[] parameter. + /// + [Required] + public int[] MyRequiredIntArrayParam + { + get { return _myRequiredIntArrayParam; } + set { _myRequiredIntArrayParam = value; this.myRequiredIntArrayParamWasSet = true; } + } + + /// + /// Single string parameter + /// + [Required] + public string MyRequiredStringParam + { + get { return _myRequiredStringParam; } + set { _myRequiredStringParam = value; this.myRequiredStringParamWasSet = true; } + } + + /// + /// A string array parameter. + /// + [Required] + public string[] MyRequiredStringArrayParam + { + get { return _myRequiredStringArrayParam; } + set { _myRequiredStringArrayParam = value; this.myRequiredStringArrayParamWasSet = true; } + } + + /// + /// Single ITaskItem parameter. + /// + [Required] + public ITaskItem MyRequiredITaskItemParam + { + get { return _myRequiredITaskItemParam; } + set { _myRequiredITaskItemParam = value; this.myRequiredITaskItemParamWasSet = true; } + } + + /// + /// ITaskItem[] parameter. + /// + [Required] + public ITaskItem[] MyRequiredITaskItemArrayParam + { + get { return _myRequiredITaskItemArrayParam; } + set { _myRequiredITaskItemArrayParam = value; this.myRequiredITaskItemArrayParamWasSet = true; } + } + + /// + /// ArrayList output parameter. (This is not supported by MSBuild.) + /// + [Output] + public ArrayList MyArrayListOutputParam + { + get { return null; } + } + + /// + /// Null ITaskItem[] output parameter. + /// + [Output] + public ITaskItem[] NullITaskItemArrayOutputParameter + { + get + { + ITaskItem[] myNullITaskItemArrayOutputParameter = null; + return myNullITaskItemArrayOutputParameter; + } + } + + /// + /// Empty string output parameter. + /// + [Output] + public string EmptyStringOutputParameter + { + get + { + return String.Empty; + } + } + + /// + /// Empty string output parameter. + /// + [Output] + public string[] EmptyStringInStringArrayOutputParameter + { + get + { + string[] myArray = new string[] { "" }; + return myArray; + } + } + + /// + /// ITaskItem output parameter. + /// + [Output] + public ITaskItem ITaskItemOutputParameter + { + get + { + ITaskItem myITaskItem = null; + return myITaskItem; + } + } + + /// + /// string output parameter. + /// + [Output] + public string StringOutputParameter + { + get + { + return "foo"; + } + } + + /// + /// string array output parameter. + /// + [Output] + public string[] StringArrayOutputParameter + { + get + { + return new string[] { "foo", "bar" }; + } + } + + /// + /// int output parameter. + /// + [Output] + public int IntOutputParameter + { + get + { + return 1; + } + } + + /// + /// int array output parameter. + /// + [Output] + public int[] IntArrayOutputParameter + { + get + { + return new int[] { 1, 2 }; + } + } + + /// + /// object array output parameter. + /// + [Output] + public object[] ObjectArrayOutputParameter + { + get + { + return new object[] { new Object() }; + } + } + + /// + /// itaskitem implementation output parameter + /// + [Output] + public MyTaskItem MyTaskItemOutputParameter + { + get + { + return new MyTaskItem(); + } + } + + /// + /// itaskitem implementation array output parameter + /// + [Output] + public MyTaskItem[] MyTaskItemArrayOutputParameter + { + get + { + return new MyTaskItem[] { new MyTaskItem() }; + } + } + + /// + /// taskitem output parameter + /// + [Output] + public TaskItem TaskItemOutputParameter + { + get + { + return new TaskItem("foo", String.Empty); + } + } + + /// + /// taskitem array output parameter + /// + [Output] + public TaskItem[] TaskItemArrayOutputParameter + { + get + { + return new TaskItem[] { new TaskItem("foo", String.Empty) }; + } + } + } + + /// + /// A simple mock task for use with Unit Testing. + /// + sealed internal class MockTask : MockTaskBase, ITask + { + private IBuildEngine _e = null; + + /// + /// Task constructor. + /// + /// + public MockTask(IBuildEngine e) + { + _e = e; + } + /// + /// Access the engine. + /// + public IBuildEngine BuildEngine + { + get { return _e; } + set { _e = value; } + } + + /// + /// Access the host object. + /// + public ITaskHost HostObject + { + get { return null; } + set { } + } + + /// + /// Main Execute method of the task does nothing. + /// + /// true if successful + public bool Execute() + { + return true; + } + } + + /// + /// Custom implementation of ITaskItem for unit testing + /// Just TaskItem would work fine, but why not test a custom type as well + /// + internal class MyTaskItem : ITaskItem + { + #region ITaskItem Members + + public string ItemSpec + { + get + { + return "foo"; + } + set + { + // do nothing + } + } + + public ICollection MetadataNames + { + get + { + return new ArrayList(); + } + } + + public int MetadataCount + { + get { return 1; } + } + + public string GetMetadata(string attributeName) + { + return "foo"; + } + + public void SetMetadata(string attributeName, string attributeValue) + { + // do nothing + } + + public void RemoveMetadata(string attributeName) + { + // do nothing + } + + public void CopyMetadataTo(ITaskItem destinationItem) + { + // do nothing + } + + public IDictionary CloneCustomMetadata() + { + return new Hashtable(); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/OpportunisticIntern_Tests.cs b/src/XMakeBuildEngine/UnitTests/OpportunisticIntern_Tests.cs new file mode 100644 index 00000000000..1aa5e288f5f --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/OpportunisticIntern_Tests.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Microsoft.Build; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class OpportunisticIntern_Tests + { + private static bool IsInternable(OpportunisticIntern.IInternable internable) + { + string i1 = OpportunisticIntern.InternableToString(internable); + string i2 = OpportunisticIntern.InternableToString(internable); + Assert.AreEqual(i1, i2); // No matter what, the same string value should return. + return Object.ReferenceEquals(i1, i2); + } + + private static void AssertInternable(OpportunisticIntern.IInternable internable) + { + Assert.IsTrue(IsInternable(internable)); + } + + private static void AssertInternable(StringBuilder sb) + { + AssertInternable(new OpportunisticIntern.StringBuilderInternTarget(sb)); + } + + private static string AssertInternable(char[] ch, int startIndex, int count) + { + var target = new OpportunisticIntern.CharArrayInternTarget(ch, startIndex, count); + AssertInternable(target); + Assert.IsTrue(target.Length == count); + + return target.ExpensiveConvertToString(); + } + + private static void AssertInternable(string value) + { + AssertInternable(new StringBuilder(value)); + AssertInternable(value.ToCharArray(), 0, value.ToCharArray().Length); + } + + private static void AssertNotInternable(OpportunisticIntern.IInternable internable) + { + Assert.IsFalse(IsInternable(internable)); + } + + private static void AssertNotInternable(StringBuilder sb) + { + AssertNotInternable(new OpportunisticIntern.StringBuilderInternTarget(sb)); + } + + private static void AssertNotInternable(char[] ch) + { + AssertNotInternable(new OpportunisticIntern.CharArrayInternTarget(ch, ch.Length)); + } + + private static void AssertNotInternable(string value) + { + AssertNotInternable(new StringBuilder(value)); + AssertNotInternable(value.ToCharArray()); + } + + /// + /// Test interning segment of char array + /// + [TestMethod] + public void SubArray() + { + var result = AssertInternable(new char[] { 'a', 't', 'r', 'u', 'e' }, 1, 4); + + Assert.AreEqual(result, "true"); + } + + /// + /// Test interning segment of char array + /// + [TestMethod] + public void SubArray2() + { + var result = AssertInternable(new char[] { 'a', 't', 'r', 'u', 'e', 'x' }, 1, 4); + + Assert.AreEqual(result, "true"); + } + + /// + /// Test a single know-to-intern tiny string to verify the mechanism. + /// + [TestMethod] + public void InternableTinyString() + { + AssertInternable("true"); + } + + /// + /// Test a single known-to-not-intern tiny string to verify the mechanism. + /// + [TestMethod] + public void NonInternableTinyString() + { + AssertNotInternable("1234"); + } + + /// + /// This is the list of hard-coded interns. They should report interned even though they are too small for normal interning. + /// + [TestMethod] + public void KnownInternableTinyStrings() + { + AssertInternable("C#"); + AssertInternable("F#"); + AssertInternable("VB"); + AssertInternable("True"); + AssertInternable("TRUE"); + AssertInternable("Copy"); + AssertInternable("v4.0"); + AssertInternable("true"); + AssertInternable("FALSE"); + AssertInternable("false"); + AssertInternable("Debug"); + AssertInternable("Build"); + AssertInternable("''!=''"); + AssertInternable("AnyCPU"); + AssertInternable("Library"); + AssertInternable("MSBuild"); + AssertInternable("Release"); + AssertInternable("ResolveAssemblyReference"); + } + + /// + /// Test a set of strings that are similar to eachother + /// + [TestMethod] + public void InternableDifferingOnlyByNthCharacter() + { + string test = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890!@#$%^&*()_+ABCDEFGHIJKLMNOPQRSTUVabcdefghijklmnopqrstuvwxyz0150"; + for (int i = 0; i < test.Length; ++i) + { + string mutated = test.Substring(0, i) + " " + test.Substring(i + 1); + AssertInternable(mutated); + } + } + + /// + /// Test The empty string + /// + [TestMethod] + public void StringDotEmpty() + { + AssertInternable(String.Empty); + } + + /// + /// Test an empty string. + /// + [TestMethod] + public void DoubleDoubleQuotes() + { + AssertInternable(""); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Parser_Tests.cs b/src/XMakeBuildEngine/UnitTests/Parser_Tests.cs new file mode 100644 index 00000000000..74f0b6550d0 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Parser_Tests.cs @@ -0,0 +1,520 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ParserTest + { + /// + /// Make a fake element location for methods who need one. + /// + private MockElementLocation _elementLocation = MockElementLocation.Instance; + + /// + /// + [TestMethod] + public void SimpleParseTest() + { + Console.WriteLine("SimpleParseTest()"); + Parser p = new Parser(); + GenericExpressionNode tree; + + tree = p.Parse("$(foo)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(foo)=='hello'", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(foo)==''", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(debug) and $(buildlab) and $(full)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(debug) or $(buildlab) or $(full)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(debug) and $(buildlab) or $(full)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(full) or $(debug) and $(buildlab)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("%(culture)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("%(culture)=='french'", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("'foo_%(culture)'=='foo_french'", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("true", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("false", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("0", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("0.0 == 0", ParserOptions.AllowAll, _elementLocation); + } + + /// + /// + [TestMethod] + public void ComplexParseTest() + { + Console.WriteLine("ComplexParseTest()"); + Parser p = new Parser(); + GenericExpressionNode tree; + + tree = p.Parse("$(foo)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("($(foo) or $(bar)) and $(baz)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("$(foo) <= 5 and $(bar) >= 15", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("(($(foo) <= 5 and $(bar) >= 15) and $(baz) == simplestring) and 'a more complex string' != $(quux)", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("(($(foo) or $(bar) == false) and !($(baz) == simplestring))", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("(($(foo) or Exists('c:\\foo.txt')) and !(($(baz) == simplestring)))", ParserOptions.AllowAll, _elementLocation); + + + tree = p.Parse("'CONTAINS%27QUOTE%27' == '$(TestQuote)'", ParserOptions.AllowAll, _elementLocation); + } + + /// + /// + [TestMethod] + public void NotParseTest() + { + Console.WriteLine("NegationParseTest()"); + Parser p = new Parser(); + GenericExpressionNode tree; + tree = p.Parse("!true", ParserOptions.AllowAll, _elementLocation); + + tree = p.Parse("!(true)", ParserOptions.AllowAll, _elementLocation); + + tree = p.Parse("!($(foo) <= 5)", ParserOptions.AllowAll, _elementLocation); + + tree = p.Parse("!(%(foo) <= 5)", ParserOptions.AllowAll, _elementLocation); + + tree = p.Parse("!($(foo) <= 5 and $(bar) >= 15)", ParserOptions.AllowAll, _elementLocation); + } + /// + /// + [TestMethod] + public void FunctionCallParseTest() + { + Console.WriteLine("FunctionCallParseTest()"); + Parser p = new Parser(); + GenericExpressionNode tree; + tree = p.Parse("SimpleFunctionCall()", ParserOptions.AllowAll, _elementLocation); + + tree = p.Parse("SimpleFunctionCall( 1234 )", ParserOptions.AllowAll, _elementLocation); + tree = p.Parse("SimpleFunctionCall( true )", ParserOptions.AllowAll, _elementLocation); + tree = p.Parse("SimpleFunctionCall( $(property) )", ParserOptions.AllowAll, _elementLocation); + + tree = p.Parse("SimpleFunctionCall( $(property), 1234, abcd, 'abcd efgh' )", ParserOptions.AllowAll, _elementLocation); + } + + [TestMethod] + public void ItemListParseTest() + { + Console.WriteLine("FunctionCallParseTest()"); + Parser p = new Parser(); + GenericExpressionNode tree; + bool fExceptionCaught; + + fExceptionCaught = false; + try + { + tree = p.Parse("@(foo) == 'a.cs;b.cs'", ParserOptions.AllowProperties, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'a.cs;b.cs' == @(foo)", ParserOptions.AllowProperties, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'@(foo)' == 'a.cs;b.cs'", ParserOptions.AllowProperties, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'otherstuff@(foo)' == 'a.cs;b.cs'", ParserOptions.AllowProperties, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'@(foo)otherstuff' == 'a.cs;b.cs'", ParserOptions.AllowProperties, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("somefunction(@(foo), 'otherstuff')", ParserOptions.AllowProperties, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + } + + [TestMethod] + public void MetadataParseTest() + { + Console.WriteLine("FunctionCallParseTest()"); + Parser p = new Parser(); + GenericExpressionNode tree; + bool fExceptionCaught; + + fExceptionCaught = false; + try + { + tree = p.Parse("%(foo) == 'a.cs;b.cs'", ParserOptions.AllowProperties | ParserOptions.AllowItemLists, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'a.cs;b.cs' == %(foo)", ParserOptions.AllowProperties | ParserOptions.AllowItemLists, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'%(foo)' == 'a.cs;b.cs'", ParserOptions.AllowProperties | ParserOptions.AllowItemLists, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'otherstuff%(foo)' == 'a.cs;b.cs'", ParserOptions.AllowProperties | ParserOptions.AllowItemLists, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("'%(foo)otherstuff' == 'a.cs;b.cs'", ParserOptions.AllowProperties | ParserOptions.AllowItemLists, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + fExceptionCaught = false; + try + { + tree = p.Parse("somefunction(%(foo), 'otherstuff')", ParserOptions.AllowProperties | ParserOptions.AllowItemLists, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + } + + /// + /// + [TestMethod] + public void NegativeTests() + { + Console.WriteLine("NegativeTests()"); + Parser p = new Parser(); + GenericExpressionNode tree; + bool fExceptionCaught; + + try + { + fExceptionCaught = false; + // Note no close quote ----------------------------------------------------V + tree = p.Parse("'a more complex' == 'asdf", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + try + { + fExceptionCaught = false; + // Note no close quote ----------------------------------------------------V + tree = p.Parse("(($(foo) <= 5 and $(bar) >= 15) and $(baz) == 'simple string) and 'a more complex string' != $(quux)", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse("($(foo) == 'simple string') $(bar)", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse("=='x'", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse("==", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse(">", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse("true!=false==", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse("true!=false==true", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + try + { + fExceptionCaught = false; + // Correct tokens, but bad parse -----------V + tree = p.Parse("1==(2", ParserOptions.AllowAll, _elementLocation); + } + catch (InvalidProjectFileException e) + { + Console.WriteLine(e.BaseMessage); + fExceptionCaught = true; + } + Assert.IsTrue(fExceptionCaught); + } + + /// + /// This test verifies that we trigger warnings for expressions that + /// could be incorrectly evaluated + /// + [TestMethod] + public void VerifyWarningForOrder() + { + // Create a project file that has an expression + MockLogger ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsTrue(ml.FullLog.Contains("MSB4130:"), "Need to warn for this expression - (a) == 1 and $(b) == 2 or $(c) == 3."); + + ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsTrue(ml.FullLog.Contains("MSB4130:"), "Need to warn for this expression - (a) == 1 or $(b) == 2 and $(c) == 3."); + + ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsTrue(ml.FullLog.Contains("MSB4130:"), "Need to warn for this expression - ($(a) == 1 or $(b) == 2 and $(c) == 3) or $(d) == 4."); + } + + /// + /// This test verifies that we don't trigger warnings for expressions that + /// couldn't be incorrectly evaluated + /// + [TestMethod] + public void VerifyNoWarningForOrder() + { + // Create a project file that has an expression + MockLogger ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsFalse(ml.FullLog.Contains("MSB4130:"), "No need to warn for this expression - (a) == 1 and $(b) == 2 and $(c) == 3."); + + ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsFalse(ml.FullLog.Contains("MSB4130:"), "No need to warn for this expression - (a) == 1 or $(b) == 2 or $(c) == 3."); + + ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsFalse(ml.FullLog.Contains("MSB4130:"), "No need to warn for this expression - ($(a) == 1 and $(b) == 2) or $(c) == 3."); + + ml = ObjectModelHelpers.BuildProjectExpectSuccess(String.Format(@" + + + + + + ", new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath)); + + // Make sure the log contains the correct strings. + Assert.IsFalse(ml.FullLog.Contains("MSB4130:"), "No need to warn for this expression - ($(a) == 1 or $(b) == 2) and $(c) == 3."); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/BuildRequestEngine_Tests.cs b/src/XMakeBuildEngine/UnitTests/QaTests/BuildRequestEngine_Tests.cs new file mode 100644 index 00000000000..595245927c7 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/BuildRequestEngine_Tests.cs @@ -0,0 +1,310 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Steps to write the tests + /// 1) Create a TestProjectDefinition object for each project file or a build request that you will submit + /// 2) Call Build() on the object to submit the build request + /// 3) Call ValidateResults() on the object to wait till the build completes and the results sent were what we expected + /// + [TestClass] + [Ignore] // "Tests are out of sync with BuildRequestEngine/Scheduler changes" + public class BuildRequestEngine_Tests + { + #region Data members + + private Common_Tests commonTests; + private QAResultsCache resultsCache; + + #endregion + + #region Constructor + + /// + /// Setup the object to be used + /// + public BuildRequestEngine_Tests() + { + this.commonTests = new Common_Tests(this.GetComponent, false); + this.resultsCache = null; + } + + #endregion + + #region Common + + /// + /// Delegate to common test setup + /// + [TestInitialize] + public void Setup() + { + this.resultsCache = new QAResultsCache(); + this.commonTests.Setup(); + } + + /// + /// Delegate to common test teardown + /// + [TestCleanup] + public void TearDown() + { + this.resultsCache = null; + this.commonTests.TearDown(); + } + + #endregion + + #region GetComponent delegate + + /// + /// Provides the components required by the tests + /// + internal IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.RequestBuilder: + QARequestBuilder requestBuilder = new QARequestBuilder(); + return (IBuildComponent)requestBuilder; + + case BuildComponentType.TaskBuilder: + TaskBuilder taskBuilder = new TaskBuilder(); + return (IBuildComponent)taskBuilder; + + case BuildComponentType.TargetBuilder: + TargetBuilder targetBuilder = new TargetBuilder(); + return (IBuildComponent)targetBuilder; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)this.resultsCache; + + default: + throw new ArgumentException("Unexpected type requested. Type = " + type.ToString()); + } + } + + #endregion + + #region Common Tests + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProject() + { + this.commonTests.BuildOneProject(); + Assert.AreEqual(this.resultsCache.CacheCount, 1, "Cache should only have 1 entry"); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build4DifferentProjects() + { + this.commonTests.Build4DifferentProjects(); + Assert.AreEqual(this.resultsCache.CacheCount, 4, "Cache should have 4 entries"); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildingTheSameProjectTwiceWithDifferentToolsVersion() + { + this.commonTests.BuildingTheSameProjectTwiceWithDifferentToolsVersion(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 2); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildingTheSameProjectTwiceWithDifferentToolsGlobalProperties() + { + this.commonTests.BuildingTheSameProjectTwiceWithDifferentGlobalProperties(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 2); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNode() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNode(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 2); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 3); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith1Reference() + { + this.commonTests.BuildOneProjectWith1Reference(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 2); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith3Reference() + { + this.commonTests.BuildOneProjectWith3Reference(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 4); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith3ReferenceWhere2AreTheSame() + { + this.commonTests.BuildOneProjectWith3ReferenceWhere2AreTheSame(); + Assert.AreEqual(((QAResultsCache)this.resultsCache).CacheCount, 3); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithEachReferencingANewProject() + { + this.commonTests.BuildMultipleProjectsWithEachReferencingANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects() + { + this.commonTests.BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects() + { + this.commonTests.BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt() + { + this.commonTests.BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt(); + } + + [TestMethod] + public void BuildMultipleProjectsWithReferencesAndDifferentGlobalProperties() + { + this.commonTests.BuildMultipleProjectsWithReferencesAndDifferentGlobalProperties(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithReferencesAndDifferentToolsVersion() + { + this.commonTests.BuildMultipleProjectsWithReferencesAndDifferentToolsVersion(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere1HasAReferenceTo3() + { + this.commonTests.Build3ProjectsWhere1HasAReferenceTo3(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere2HasAReferenceTo3() + { + this.commonTests.Build3ProjectsWhere2HasAReferenceTo3(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere3HasAReferenceTo1() + { + this.commonTests.Build3ProjectsWhere3HasAReferenceTo1(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/ITestDataProvider.cs b/src/XMakeBuildEngine/UnitTests/QaTests/ITestDataProvider.cs new file mode 100644 index 00000000000..306f8b1f5f5 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/ITestDataProvider.cs @@ -0,0 +1,60 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// An interfacing representing a build request defination cache. + /// + internal interface ITestDataProvider + { + /// + /// Indexer to get the Nth request definition from the provider + /// + RequestDefinition this[ int index ]{ get; } + + /// + /// Adds a new definition to the cache. Returns the key associated with this definition so that it can be + /// used as the configuration id also. + /// + int AddDefinition(RequestDefinition definition); + + /// + /// Adds a new configuration to the configuration cache if one is not already there. Also adds to the configuration cache. + /// + BuildRequestConfiguration CreateConfiguration(RequestDefinition definition); + + /// + /// Adds a new Request to the Enqueue(value); + /// + BuildRequest NewRequest { set; } + + /// + /// Adds a new Configuration to the Enqueue(value); + /// + BuildRequestConfiguration NewConfiguration { set; } + + /// + /// Adds a new result to the Queue + /// + ResultFromEngine NewResult { set; } + + /// + /// Exception raised by the engine. This is forwarded to all the definitions + /// + Exception EngineException { set; } + + /// + /// Dictonary of request definitions where the key is the configuration id and the value is the request defination for that configuration + /// + Dictionary RequestDefinitions { get; } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/Integration_Tests.cs b/src/XMakeBuildEngine/UnitTests/QaTests/Integration_Tests.cs new file mode 100644 index 00000000000..71579b277ba --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/Integration_Tests.cs @@ -0,0 +1,297 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; +using System.IO; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Steps to write the tests + /// 1) Create a TestProjectDefinition object for each project file or a build request that you will submit + /// 2) Call Build() on the object to submit the build request + /// 3) Call ValidateResults() on the object to wait till the build completes and the results sent were what we expected + /// + [TestClass] + [Ignore] // "QA tests are double-initializing some components such as BuildRequestEngine." + public class Integration_Tests + { + #region Data members + + private Common_Tests commonTests; + private ResultsCache resultsCache; + private string assemblyPath; + private string tempPath; + + #endregion + + #region Constructor + + /// + /// Setup the object + /// + public Integration_Tests() + { + this.commonTests = new Common_Tests(this.GetComponent, true); + this.resultsCache = null; + this.tempPath = System.IO.Path.GetTempPath(); + this.assemblyPath = Path.GetDirectoryName( + new Uri(System.Reflection.Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath); + this.assemblyPath = Path.Combine(this.assemblyPath, "Microsoft.Build.Unittest.dll"); + } + + #endregion + + #region Common + + /// + /// Delegate to common test setup + /// + [TestInitialize] + public void Setup() + { + this.resultsCache = new ResultsCache(); + this.commonTests.Setup(); + } + + /// + /// Delegate to common test teardown + /// + [TestCleanup] + public void TearDown() + { + this.commonTests.TearDown(); + this.resultsCache = null; + } + + #endregion + + #region GetComponent delegate + + /// + /// Provides the components required by the tests + /// + internal IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.RequestBuilder: + RequestBuilder requestBuilder = new RequestBuilder(); + return (IBuildComponent)requestBuilder; + + case BuildComponentType.TaskBuilder: + TaskBuilder taskBuilder = new TaskBuilder(); + return (IBuildComponent)taskBuilder; + + case BuildComponentType.TargetBuilder: + TargetBuilder targetBuilder = new TargetBuilder(); + return (IBuildComponent)targetBuilder; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)this.resultsCache; + + default: + throw new ArgumentException("Unexpected type requested. Type = " + type.ToString()); + } + } + + #endregion + + #region Data Input and Output from task + + /// + /// Send some parameters to the task and expect certain outputs + /// + [TestMethod] + [Ignore] // "Cannot use a project instance if the project is created from a file." + public void InputAndOutputFromTask() + { + string projectFileContents = String.Format(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + "), + this.assemblyPath); + + ProjectInstance projectInstance = null; + RequestDefinition r1 = GetRequestUsingProject(projectFileContents, "1.proj", "t", out projectInstance); + + r1.SubmitBuildRequest(); + + r1.WaitForResults(); + ProjectPropertyInstance property = projectInstance.GetProperty("SomeProperty"); + Assert.IsTrue(property.EvaluatedValue == "Foo", "SomeProperty=Foo"); + } + + #endregion + + #region Data Output from target + + /// + /// Target outputs + /// + [TestMethod] + public void OutputFromTarget() + { + string projectFileContents = String.Format(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + "), + this.assemblyPath); + + ProjectInstance projectInstance = null; + RequestDefinition r1 = GetRequestUsingProject(projectFileContents, "1.proj", "t", out projectInstance); + + r1.SubmitBuildRequest(); + + r1.WaitForResults(); + r1.ValidateTargetEndResult("t", TargetResultCode.Success, new string[1] { "Foo" }); + } + + #endregion + + #region Data changed + + /// + /// Send some parameters to the task and expect certain outputs. This output overwrites an existing property + /// + [TestMethod] + [Ignore] // "Cannot use a project instance if the project is created from a file." + public void OutputFromTaskUpdatesProperty() + { + string projectFileContents = String.Format(ObjectModelHelpers.CleanupFileContents( + @" + + + + oldvalue + + + + + + + + "), + this.assemblyPath); + + ProjectInstance projectInstance = null; + RequestDefinition r1 = GetRequestUsingProject(projectFileContents, "1.proj", "t", out projectInstance); + + r1.SubmitBuildRequest(); + + r1.WaitForResults(); + ProjectPropertyInstance property = projectInstance.GetProperty("SomeProperty"); + Assert.IsTrue(property.EvaluatedValue == "Foo", "SomeProperty=Foo"); + } + + #endregion + + #region OnError + + /// + /// Target1 executes task1. Task1 has an error with continue on error. Target1 then executes task2 with has an error with stop on error. + /// Target1 has OnError to execute target2 + /// + [TestMethod] + public void OnErrorTargetIsBuilt() + { + string projectFileContents = String.Format(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + + + + + "), + this.assemblyPath); + + ProjectInstance projectInstance = null; + RequestDefinition r1 = GetRequestUsingProject(projectFileContents, "1.proj", "t1", out projectInstance); + + r1.SubmitBuildRequest(); + + r1.WaitForResults(); + + r1.ValidateTargetEndResult("t1", TargetResultCode.Failure, null); + r1.ValidateNonPrimaryTargetEndResult("t2", TargetResultCode.Success, null); + } + + /// + /// Target1 executes task1. Task1 has an error with continue on error. Target1 then executes task2 with has an error with stop on error. + /// Target1 has OnError to execute target2 + /// + [TestMethod] + public void OnErrorTargetIsBuilt2() + { + string projectFileContents = String.Format(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + + + + + + "), + this.assemblyPath); + + ProjectInstance projectInstance = null; + RequestDefinition r1 = GetRequestUsingProject(projectFileContents, "1.proj", "t1", out projectInstance); + + r1.SubmitBuildRequest(); + + r1.WaitForResults(); + + r1.ValidateTargetEndResult("t1", TargetResultCode.Failure, null); + r1.ValidateNonPrimaryTargetEndResult("t2", TargetResultCode.Success, null); + } + + /// + /// Target0 depends on Target1 which executes task1. Task1 has an error with stop on error. Target1 has OnError to execute target2 + /// + [TestMethod] + public void OnErrorTargetIsBuilt3() + { + string projectFileContents = String.Format(ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/MockHost.cs b/src/XMakeBuildEngine/UnitTests/QaTests/MockHost.cs new file mode 100644 index 00000000000..f7bc86df9e9 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/MockHost.cs @@ -0,0 +1,450 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; + +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; + +using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; +using LegacyThreadingData = Microsoft.Build.Execution.LegacyThreadingData; + +namespace Microsoft.Build.UnitTests.QA +{ + #region delegate + + /// + /// Delegate the call to GetComponent if one is available + /// + internal delegate IBuildComponent GetComponentDelegate(BuildComponentType type); + + #endregion + + /// + /// The mock component host object. + /// + internal class QAMockHost : MockLoggingService, IBuildComponentHost, IBuildComponent + { + #region IBuildComponentHost Members + + /// + /// The logging service + /// + private ILoggingService loggingService = null; + + /// + /// The request engine + /// + private IBuildRequestEngine requestEngine = null; + + /// + /// The test data provider + /// + private ITestDataProvider testDataProvider = null; + /// + /// Number of miliseconds of engine idle time to cause a shutdown + /// + private int engineShutdownTimeout = 30000; + + /// + /// Number of initial node count + /// + private int initialNodeCount = 1; + + /// + /// Default node id + /// + private int nodeId = 1; + + /// + /// Only log critical events by default + /// + private bool logOnlyCriticalEvents = true; + + /// + /// The last status of the engine reported + /// + private BuildRequestEngineStatus lastEngineStatus; + + /// + /// Internal Event that is fired when the engine status changes + /// + private AutoResetEvent engineStatusChangedEvent; + + /// + /// Delegate which handles initilizing the components requested for + /// + private GetComponentDelegate getComponentCallback; + + /// + /// All the build components returned by the host + /// + private Queue buildComponents; + + /// + /// The build parameters. + /// + private BuildParameters buildParameters; + + /// + /// Global timeout is 30 seconds + /// + public static int globalTimeOut = 30000; + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + private LegacyThreadingData legacyThreadingData; + + /// + /// Constructor + /// + internal QAMockHost(GetComponentDelegate getComponentCallback) + { + this.buildParameters = new BuildParameters(); + this.getComponentCallback = getComponentCallback; + this.engineStatusChangedEvent = new AutoResetEvent(false); + this.lastEngineStatus = BuildRequestEngineStatus.Shutdown; + this.loggingService = this; + this.requestEngine = null; + this.testDataProvider = null; + this.buildComponents = new Queue(); + this.legacyThreadingData = new LegacyThreadingData(); + } + + /// + /// Returns the node logging service. We don't distinguish here. + /// + /// The build for which the service should be returned. + /// The logging service. + public ILoggingService LoggingService + { + get + { + return this.loggingService; + } + } + + /// + /// Retrieves the LegacyThreadingData associated with a particular component host + /// + LegacyThreadingData IBuildComponentHost.LegacyThreadingData + { + get + { + return legacyThreadingData; + } + } + + /// + /// Retrieves the host name. + /// + public string Name + { + get + { + return "QAMockHost"; + } + } + + /// + /// Returns the build parameters. + /// + public BuildParameters BuildParameters + { + get + { + return buildParameters; + } + } + + /// + /// Constructs and returns a component of the specified type. + /// + public IBuildComponent GetComponent(BuildComponentType type) + { + IBuildComponent returnComponent = null; + + switch(type) + { + case BuildComponentType.LoggingService: // Singleton + return (IBuildComponent)this.loggingService; + + case BuildComponentType.TestDataProvider: // Singleton + if (this.testDataProvider != null) + { + return (IBuildComponent)this.testDataProvider; + } + + returnComponent = this.getComponentCallback(type); + if (returnComponent != null) + { + returnComponent.InitializeComponent(this); + this.testDataProvider = (ITestDataProvider)returnComponent; + } + + break; + + case BuildComponentType.RequestEngine: // Singleton + if (this.requestEngine != null) + { + return (IBuildComponent)this.requestEngine; + } + + returnComponent = this.getComponentCallback(type); + if (returnComponent != null) + { + returnComponent.InitializeComponent(this); + this.requestEngine = (IBuildRequestEngine)returnComponent; + this.requestEngine.OnEngineException += new EngineExceptionDelegate(RequestEngine_OnEngineException); + this.requestEngine.OnNewConfigurationRequest += new NewConfigurationRequestDelegate(RequestEngine_OnNewConfigurationRequest); + this.requestEngine.OnRequestBlocked += new RequestBlockedDelegate(RequestEngine_OnNewRequest); + this.requestEngine.OnRequestComplete += new RequestCompleteDelegate(RequestEngine_OnRequestComplete); + this.requestEngine.OnStatusChanged += new EngineStatusChangedDelegate(RequestEngine_OnStatusChanged); + } + + break; + + default: + returnComponent = this.getComponentCallback(type); + if (returnComponent != null) + { + returnComponent.InitializeComponent(this); + } + break; + } + + if (returnComponent != null) + { + lock (this.buildComponents) + { + this.buildComponents.Enqueue(returnComponent); + } + } + + return returnComponent; + } + + /// + /// Registers a component factory. + /// + public void RegisterFactory(BuildComponentType type, BuildComponentFactoryDelegate factory) + { + } + + #endregion + + #region Public properties + + /// + /// Node ID + /// + internal int NodeId + { + get + { + return this.nodeId; + } + set + { + this.nodeId = value; + } + } + + /// + /// True to log only critical events + /// + internal bool LogOnlyCriticalEvents + { + get + { + return this.logOnlyCriticalEvents; + } + set + { + this.logOnlyCriticalEvents = value; + } + } + + /// + /// Number if idle mili seconds to wait before shutting down the build request engine + /// + internal int EngineShutdownTimeout + { + get + { + return this.engineShutdownTimeout; + } + set + { + this.engineShutdownTimeout = value; + } + } + + /// + /// Number of initial nodes + /// + internal int InitialNodeCount + { + get + { + return this.initialNodeCount; + } + set + { + this.initialNodeCount = value; + } + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the component host. Since we are a host and we do not have a parent host we do not need to do anything here. + /// + /// The component host + public void InitializeComponent(IBuildComponentHost host) + { + return; + } + + /// + /// Shuts down the component. First shutdown the request engine. Then shutdown the remaining of the component. + /// Check if the test data provider exists before shutting it down as it may not have been registered yet because this is also + /// called in TearDown and teardown is called if an exception is received. + /// + public void ShutdownComponent() + { + ShutDownRequestEngine(); + if (this.testDataProvider != null) + { + ((IBuildComponent)this.testDataProvider).ShutdownComponent(); + } + + this.buildComponents.Clear(); + this.loggingService = null; + this.requestEngine = null; + } + + /// + /// Cancels the current build + /// + public void AbortBuild() + { + this.requestEngine.CleanupForBuild(); + } + + /// + /// Wait for the Build request engine to shutdown + /// + public void ShutDownRequestEngine() + { + if (this.LastEngineStatus != BuildRequestEngineStatus.Shutdown) + { + this.requestEngine.CleanupForBuild(); + ((IBuildComponent)this.requestEngine).ShutdownComponent(); + WaitForEngineStatus(BuildRequestEngineStatus.Shutdown); + } + } + + /// + /// Waits for the engine status requested. If a status is not changed within a certain amout of time then fail. + /// + public void WaitForEngineStatus(BuildRequestEngineStatus status) + { + while (this.LastEngineStatus != status) + { + if (this.engineStatusChangedEvent.WaitOne(QAMockHost.globalTimeOut, false) == false) + { + Assert.Fail("Requested engine status was not received within - " + QAMockHost.globalTimeOut.ToString() + " seconds."); + } + } + } + + #endregion + + #region Event Methods + + /// + /// Gets called by the build request engine when the build request engine state changes. + /// Special handeling when a shutdown has been sent so that only shutdown status set the event + /// + private void RequestEngine_OnStatusChanged(BuildRequestEngineStatus newStatus) + { + this.LastEngineStatus = newStatus; + this.engineStatusChangedEvent.Set(); + } + + /// + /// Gets called by the build request engine when the build request has been completed + /// + private void RequestEngine_OnRequestComplete(BuildRequest request, BuildResult result) + { + if (this.testDataProvider != null) + { + this.testDataProvider.NewResult = new ResultFromEngine(request, result); + } + } + + /// + /// Gets called by the build request engine when there is a new build request (engine callback) + /// + /// + private void RequestEngine_OnNewRequest(BuildRequestBlocker blocker) + { + if (this.testDataProvider != null) + { + foreach (BuildRequest request in blocker.BuildRequests) + { + this.testDataProvider.NewRequest = request; + } + } + } + + /// + /// Gets called by the build request engine when the a configuration for a new build request is not present + /// + /// + private void RequestEngine_OnNewConfigurationRequest(BuildRequestConfiguration config) + { + if (this.testDataProvider != null) + { + this.testDataProvider.NewConfiguration = config; + } + } + + /// + /// Gets called by the build request engine when the build request engine when there is an exception + /// + /// + private void RequestEngine_OnEngineException(Exception e) + { + if (this.testDataProvider != null) + { + this.testDataProvider.EngineException = e; + } + } + + private BuildRequestEngineStatus LastEngineStatus + { + get + { + return this.lastEngineStatus; + } + set + { + this.lastEngineStatus = value; + } + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/MockLoggingService.cs b/src/XMakeBuildEngine/UnitTests/QaTests/MockLoggingService.cs new file mode 100644 index 00000000000..19c1f8cab34 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/MockLoggingService.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// A class providing a mock implementation of ILoggingService. + /// + internal class MockLoggingService : ILoggingService + { + #region ILoggingService Members + + /// + /// The event to raise when there is a logging exception + /// + public event LoggingExceptionDelegate OnLoggingThreadException; + + /// + /// The event to raise when ProjectStarted is processed. + /// + public event ProjectStartedEventHandler OnProjectStarted; + + /// + /// The event to raise when ProjectFinished is processed + /// + public event ProjectFinishedEventHandler OnProjectFinished; + + /// + /// Enumerator over all registered loggers. + /// + public ICollection Loggers + { + get { throw new NotImplementedException(); } + } + + /// + /// The logging service state + /// + public LoggingServiceState ServiceState + { + get + { + OnLoggingThreadException(null); + OnProjectStarted(null, null); + OnProjectFinished(null, null); + throw new NotImplementedException(); + } + } + + /// + /// The logging mode. + /// + public LoggerMode LoggingMode + { + get + { + return LoggerMode.Synchronous; + } + } + + /// + /// Whether to log critical events + /// + public bool OnlyLogCriticalEvents + { + get + { + return false; + } + + set + { + } + } + + /// + /// Returns the number of initial nodes. + /// + public int MaxCPUCount + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + /// + /// Gets the logger descriptions + /// + public ICollection LoggerDescriptions + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Gets the registered logger type names. + /// + public ICollection RegisteredLoggerTypeNames + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Retrieves the registered sink names. + /// + public ICollection RegisteredSinkNames + { + get + { + throw new NotImplementedException(); + } + } + + /// + /// Is the logging service on a remote node, this is used to determine if properties need to be serialized + /// + public bool RunningOnRemoteNode + { + get; + set; + } + + /// + /// Properties to serialize from the child node to the parent node + /// + public string[] PropertiesToSerialize + { + get; + set; + } + + /// + /// Should all properties be serialized from the child to the parent process + /// + public bool SerializeAllProperties + { + get; + set; + } + + /// + /// Registers a distributed logger. + /// + /// The central logger, which resides on the build manager. + /// The forwarding logger, which resides on the node. + /// True if successful. + public bool RegisterDistributedLogger(ILogger centralLogger, LoggerDescription forwardingLogger) + { + throw new NotImplementedException(); + } + + /// + /// Registers a logger + /// + /// The logger + /// True if successful. + public bool RegisterLogger(ILogger logger) + { + throw new NotImplementedException(); + } + + /// + /// Clear out all registered loggers so that none are registered. + /// + public void UnregisterAllLoggers() + { + throw new NotImplementedException(); + } + + /// + /// Initializes the loggers on a node + /// + /// The descriptions received from the Build Manager + /// The sink used to transmit messages to the manager. + /// The id of the node. + public void InitializeNodeLoggers(ICollection loggerDescriptions, IBuildEventSink forwardingLoggerSink, int nodeId) + { + throw new NotImplementedException(); + } + + /// + /// Logs a comment based on a message resource + /// + /// The context + /// The importance + /// The resource for the message + /// The args for the message + public void LogComment(BuildEventContext buildEventContext, MessageImportance importance, string messageResourceName, params object[] messageArgs) + { + } + + /// + /// Logs a text comment + /// + /// The context + /// The importance + /// The message + public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message) + { + } + + /// + /// Logs a pre-formed build event + /// + /// The event to log + public void LogBuildEvent(BuildEventArgs buildEvent) + { + } + + /// + /// Logs an error + /// + /// The event context + /// The file from which the error is logged + /// The message resource + /// The message args + public void LogError(BuildEventContext buildEventContext, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + } + + /// + /// Logs an error with a subcategory + /// + /// The build event context + /// The subcategory resource + /// The file + /// The message resource + /// The message args + public void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + } + + /// + /// Logs a text error + /// + /// The event context + /// The subcategory resource + /// The error code + /// A help keyword + /// The file + /// The message + public void LogErrorFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string errorCode, string helpKeyword, BuildEventFileInfo file, string message) + { + } + + /// + /// Logs an invalid project file error + /// + /// The event context + /// The exception + public void LogInvalidProjectFileError(BuildEventContext buildEventContext, InvalidProjectFileException invalidProjectFileException) + { + } + + /// + /// Logs a fatal build error + /// + /// The event context + /// The exception + /// The file + public void LogFatalBuildError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file) + { + } + + /// + /// Logs a fatal task error + /// + /// The event context + /// The exception + /// The file + /// The name of the task + public void LogFatalTaskError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) + { + } + + /// + /// Logs a generic fatal error + /// + /// The build context + /// The exception + /// The file + /// The message resource + /// The message args + public void LogFatalError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + } + + /// + /// Logs a task warning + /// + /// The build context + /// The exception + /// The file + /// The name of the task + public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file, string taskName) + { + } + + /// + /// Logs a warning + /// + /// The event context + /// The subcategory resource + /// The file + /// The message resource + /// The message args + public void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) + { + } + + /// + /// Logs a text warning + /// + /// The build context + /// The subcategory resource + /// The warning code + /// A help keyword + /// The file + /// The message + public void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message) + { + } + + /// + /// Logs a build started event + /// + public void LogBuildStarted() + { + } + + /// + /// Logs a build finished event + /// + /// Set to true if the build was successful + public void LogBuildFinished(bool success) + { + } + + /// + /// Logs a project started event + /// + /// The event context of the node + /// The project instance id + /// The parent build event context + /// The project filename + /// The names of the targets + /// The project properties + /// The project items + /// The build event context for the project. + public BuildEventContext LogProjectStarted(BuildEventContext nodeBuildEventContext, int submissionId, int projectId, BuildEventContext parentBuildEventContext, string projectFile, string targetNames, IEnumerable properties, IEnumerable items) + { + return new BuildEventContext(0, 0, 0, 0); + } + + /// + /// Logs a project finished event + /// + /// The project build event context + /// The project filename + /// Whether it was successful or not. + public void LogProjectFinished(BuildEventContext projectBuildEventContext, string projectFile, bool success) + { + } + + /// + /// Logs a target started event + /// + /// The build event context of the project + /// The name of the target + /// The project file + /// The project file containing the target element + /// The build event context for the target + public BuildEventContext LogTargetStarted(BuildEventContext projectBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, string parentTargetName) + { + return new BuildEventContext(0, 0, 0, 0); + } + + /// + /// Logs a target finished event + /// + /// The target's build event context + /// The name of the target + /// The project file + /// The project file containing the target element + /// Whether it was successful or not. + public void LogTargetFinished(BuildEventContext targetBuildEventContext, string targetName, string projectFile, string projectFileOfTargetElement, bool success, IEnumerable targetOutputs) + { + } + + /// + /// Logs a task started event + /// + /// The target's build event context + /// The name of the task + /// The project file + /// The project file containing the task node. + public void LogTaskStarted(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode) + { + } + + /// + /// Logs a task started event + /// + /// The target's build event context + /// The name of the task + /// The project file + /// The project file containing the task node. + /// The task logging context + public BuildEventContext LogTaskStarted2(BuildEventContext targetBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode) + { + return new BuildEventContext(0, 0, 0, 0); + } + + /// + /// Logs a task finished event + /// + /// The task's build event context + /// The name of the task + /// The project file + /// The project file of the task node + /// Whether the task was successful or not. + public void LogTaskFinished(BuildEventContext taskBuildEventContext, string taskName, string projectFile, string projectFileOfTaskNode, bool success) + { + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/MockRequestBuilder.cs b/src/XMakeBuildEngine/UnitTests/QaTests/MockRequestBuilder.cs new file mode 100644 index 00000000000..194859d35e9 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/MockRequestBuilder.cs @@ -0,0 +1,321 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; +using Microsoft.Build.Unittest; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Mock implementation of the RequestBuilder component. RequestBuilder exists for each BuildRequestEntry + /// + internal class QARequestBuilder : IRequestBuilder, IBuildComponent + { + #region Data members + + private IBuildComponentHost host; + private ITestDataProvider testDataProvider; + private IResultsCache resultsCache; + private IConfigCache configCache; + private Thread builderThread; + private BuildRequestEntry requestedEntry; + private AutoResetEvent continueEvent; + private AutoResetEvent cancelEvent; + private ManualResetEvent threadStarted; + private RequestDefinition currentProjectDefinition; + + #endregion + + #region Constructors + + /// + /// Constructor that takes in nothing. + /// + public QARequestBuilder() + { + this.host = null; + this.configCache = null; + this.resultsCache = null; + this.builderThread = null; + this.requestedEntry = null; + this.cancelEvent = new AutoResetEvent(false); + this.continueEvent = new AutoResetEvent(false); + this.threadStarted = new ManualResetEvent(false); + this.currentProjectDefinition = null; + } + + #endregion + + #region Events + + /// + /// Used when a new BuildRequest is to be sent + /// + public event NewBuildRequestsDelegate OnNewBuildRequests; + + /// + /// Called when a BuildRequest is completed + /// + public event BuildRequestCompletedDelegate OnBuildRequestCompleted; + + /// + /// Called when a BuildRequest is blocked. + /// + public event BuildRequestBlockedDelegate OnBuildRequestBlocked; + + #endregion + + #region IBuildComponent Members + + /// + /// The component is being initialized + /// + public void InitializeComponent(IBuildComponentHost host) + { + this.host = host; + this.resultsCache = (IResultsCache)(this.host.GetComponent(BuildComponentType.ResultsCache)); + this.configCache = (IConfigCache)(this.host.GetComponent(BuildComponentType.ConfigCache)); + this.testDataProvider = (ITestDataProvider)(this.host.GetComponent(BuildComponentType.TestDataProvider)); + } + + /// + /// The component is shutting down + /// + public void ShutdownComponent() + { + this.requestedEntry = null; + this.currentProjectDefinition = null; + } + + #endregion + + #region IRequestBuilder Members + + /// + /// Build a request entry + /// + /// + public void BuildRequest(NodeLoggingContext nodeLoggingContext, BuildRequestEntry entry) + { + + this.requestedEntry = entry; + if (null == this.requestedEntry.RequestConfiguration.Project) + { + Project mockProject = new Project(XmlReader.Create(new System.IO.StringReader( +@" + + + "))); + this.requestedEntry.RequestConfiguration.Project = mockProject.CreateProjectInstance(); + } + + this.currentProjectDefinition = this.testDataProvider[this.requestedEntry.Request.ConfigurationId]; + this.requestedEntry.Continue(); + this.builderThread = new Thread(BuilderThreadProc); + this.builderThread.Name = "Builder Thread for Request: " + entry.Request.ConfigurationId.ToString(); + this.builderThread.Start(); + } + + /// + /// Resume a request which was waiting or is new + /// + public void ContinueRequest() + { + this.threadStarted.WaitOne(); + this.continueEvent.Set(); + } + + /// + /// Cancel the request that we are processing + /// + public void CancelRequest() + { + this.BeginCancel(); + this.WaitForCancelCompletion(); + } + + /// + /// Starts to cancel an existing request. + /// + public void BeginCancel() + { + this.threadStarted.WaitOne(); + this.cancelEvent.Set(); + } + + /// + /// Waits for the cancellation until it's completed, and cleans up the internal states. + /// + public void WaitForCancelCompletion() + { + this.builderThread.Join(); + this.cancelEvent.Close(); + this.continueEvent.Close(); + this.threadStarted.Close(); + this.builderThread = null; + } + + #endregion + + #region Private methods + + /// + /// Thread to process the build request + /// + private void BuilderThreadProc() + { + bool completeSuccess = true; + WaitHandle[] handles = new WaitHandle[2] { cancelEvent, continueEvent }; + + this.threadStarted.Set(); + + // Add a request for each of the referenced projects. All we need to do is to make sure that the new project definition for the referenced + // project has been added to the host collection + + FullyQualifiedBuildRequest[] fq = new FullyQualifiedBuildRequest[this.currentProjectDefinition.ChildDefinitions.Count]; + int fqCount = 0; + foreach(RequestDefinition childDefinition in this.currentProjectDefinition.ChildDefinitions) + { + BuildRequestConfiguration unresolvedConfig = childDefinition.UnresolvedConfiguration; + fq[fqCount++] = new FullyQualifiedBuildRequest(unresolvedConfig, childDefinition.TargetsToBuild, true); + } + + try + { + // Check to see if there was a cancel before we do anything + if (cancelEvent.WaitOne(1, false)) + { + HandleCancel(); + return; + } + + // Submit the build request for the references if we have any + if (fqCount > 0) + { + OnNewBuildRequests(this.requestedEntry, fq); + + // Wait for all of them to complete till our entry is marked ready + int evt = WaitHandle.WaitAny(handles); + + // If a cancel occurs then we are done. Set the result to an exception + if (evt == 0) + { + HandleCancel(); + return; + } + + // If we get a continue then one of the reference has complete. Set the result in the cache only in case of success. + // Even though there may have been error - we cannot abandone the loop as there are already + // requests in progress which may call back to this thread + else if (evt == 1) + { + IDictionary results = requestedEntry.Continue(); + foreach (BuildResult configResult in results.Values) + { + if (configResult.OverallResult == BuildResultCode.Failure) + { + completeSuccess = false; + } + else + { + this.resultsCache.AddResult(configResult); + } + } + } + } + + // Check to see if there was a cancel we process the final result + if (cancelEvent.WaitOne(1, false)) + { + HandleCancel(); + return; + } + + // Simulate execution time for the actual entry if one was specified and if the entry built successfully + if (this.currentProjectDefinition.ExecutionTime > 0 && completeSuccess == true) + { + Thread.Sleep(this.currentProjectDefinition.ExecutionTime); + } + + // Create and send the result + BuildResult result = new BuildResult(requestedEntry.Request); + + // No specific target was asked to build. Return the default result + if (requestedEntry.Request.Targets.Count == 0) + { + result.AddResultsForTarget(RequestDefinition.defaultTargetName, new TargetResult(new TaskItem[1], completeSuccess ? TestUtilities.GetSuccessResult() : TestUtilities.GetStopWithErrorResult())); + } + else + { + foreach (string target in requestedEntry.Request.Targets) + { + result.AddResultsForTarget(target, new TargetResult(new TaskItem[1], completeSuccess ? TestUtilities.GetSuccessResult() : TestUtilities.GetStopWithErrorResult())); + } + } + + this.resultsCache.AddResult(result); + this.requestedEntry.Complete(result); + RaiseRequestComplete(this.requestedEntry); + return; + } + + catch (Exception e) + { + if (this.requestedEntry != null) + { + string message = String.Format("Test: Unhandeled exception occured: \nMessage: {0} \nStack:\n{1}", e.Message, e.StackTrace); + BuildResult errorResult = new BuildResult(this.requestedEntry.Request, new InvalidOperationException(message)); + this.requestedEntry.Complete(errorResult); + RaiseRequestComplete(this.requestedEntry); + } + } + + } + + /// + /// Process the approprate action if the cancel event was set + /// + private void HandleCancel() + { + BuildResult res = new BuildResult(this.requestedEntry.Request, new BuildAbortedException()); + this.requestedEntry.Complete(res); + RaiseRequestComplete(this.requestedEntry); + } + + /// + /// Raises the request completed event + /// + /// The entry. + private void RaiseRequestComplete(BuildRequestEntry entry) + { + if (OnBuildRequestCompleted != null) + { + OnBuildRequestCompleted(entry); + } + } + + /// + /// Raises the request blocked event. + /// + public void RaiseRequestBlocked(BuildRequestEntry entry, int blockingId, string blockingTarget) + { + if (null != OnBuildRequestBlocked) + { + OnBuildRequestBlocked(entry, blockingId, blockingTarget); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/MockResultsCache.cs b/src/XMakeBuildEngine/UnitTests/QaTests/MockResultsCache.cs new file mode 100644 index 00000000000..36c456911cf --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/MockResultsCache.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Mock Implementation of the results cache which does captures some counters and provides it + /// back to the tests for validation purposes. Most of the implementation is routed to the default component + /// by means of aggrigration + /// + internal class QAResultsCache : IResultsCache, IBuildComponent + { + #region Private Data + + private IBuildComponentHost host; + private IResultsCache resultCache; + private int clearedCount; + private int addCount; + private int getCount; + + #endregion + + /// + /// Call the actual implementation + /// + public QAResultsCache() + { + this.resultCache = new ResultsCache(); + this.addCount = 0; + this.getCount = 0; + this.clearedCount = 0; + } + + #region IResultsCache Members + + /// + /// Call the actual implementation + /// + /// The result to add. + public void AddResult(BuildResult result) + { + addCount++; + this.resultCache.AddResult(result); + } + + /// + /// Call the actual implementation + /// + public void ClearResults() + { + clearedCount++; + this.resultCache.ClearResults(); + } + + /// + /// Call the actual implementation + /// + /// The request for which results should be retrieved. + /// The build results for the specified request. + public BuildResult GetResultForRequest(BuildRequest request) + { + getCount++; + return this.resultCache.GetResultForRequest(request); + } + + /// + /// Call the actual implementation + /// + /// The configuration id for which results should be retrieved. + /// The build results for the specified configuration. + public BuildResult GetResultsForConfiguration(int configurationId) + { + getCount++; + return this.resultCache.GetResultsForConfiguration(configurationId); + } + + /// + /// Call the actual implementation. + /// + public ResultsCacheResponse SatisfyRequest(BuildRequest request, List configInitialTargets, List configDefaultTargets, List additionalTargetsToCheckForOverallResult, bool skippedResultsAreOK) + { + return this.resultCache.SatisfyRequest(request, configInitialTargets, configDefaultTargets, additionalTargetsToCheckForOverallResult, skippedResultsAreOK); + } + + /// + /// Clears the results for a specific configuration. + /// + /// The configuration id. + public void ClearResultsForConfiguration(int configurationId) + { + this.resultCache.ClearResultsForConfiguration(configurationId); + } + + /// + /// Does nothing. + /// + public void WriteResultsToDisk() + { + } + + #endregion + + #region IBuildComponent Members + + /// + /// Sets the build component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + this.host = host; + } + + /// + /// Shuts down this component + /// + public void ShutdownComponent() + { + host = null; + ((IBuildComponent)this.resultCache).ShutdownComponent(); + } + + #endregion + + #region Public methods + + /// + /// Returns the count of the size of the cache + /// + public int CacheCount + { + get + { + return ((ResultsCache)this.resultCache).ResultsDictionary.Count; + } + } + + /// + /// Number of times the cache was checked to see if a result already existed + /// + public int GetCount + { + get + { + return this.getCount; + } + } + + /// + /// Number of times results from the cache was cleared + /// + public int ClearedCount + { + get + { + return this.clearedCount; + } + } + + /// + /// Number of results added to the cache + /// + public int AddCount + { + get + { + return this.addCount; + } + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/MockTargetBuilder.cs b/src/XMakeBuildEngine/UnitTests/QaTests/MockTargetBuilder.cs new file mode 100644 index 00000000000..2bb11b2b4a5 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/MockTargetBuilder.cs @@ -0,0 +1,206 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Collections; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Unittest; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; +using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext; +using System.Threading.Tasks; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// The mock component TargetBuilder object. + /// + internal class QAMockTargetBuilder : ITargetBuilder, IBuildComponent + { + /// + /// The component host. + /// + private IBuildComponentHost host; + + /// + /// The BuildRequestEntry for which we are building targets. + /// + private BuildRequestEntry requestEntry; + + /// + /// The project logging context + /// + private ProjectLoggingContext projectLoggingContext; + + /// + /// Request Callback + /// + private IRequestBuilderCallback requestCallBack; + + /// + /// The test data provider + /// + private ITestDataProvider testDataProvider; + + /// + /// Test definition associated with the project that we are building + /// + private RequestDefinition testDefinition; + + /// + /// Event to notify that the build has been completed + /// + private AutoResetEvent buildDone; + + /// + /// The cancellation token + /// + private CancellationToken cancellationToken; + + public QAMockTargetBuilder() + { + this.host = null; + this.testDataProvider = null; + this.testDefinition = null; + this.requestCallBack = null; + this.requestEntry = null; + this.projectLoggingContext = null; + this.buildDone = new AutoResetEvent(false); + } + + /// + /// Builds the specified targets of an entry. The cancel event should only be set to true if we are planning + /// on simulating execution time when a target is built + /// + public Task BuildTargets(ProjectLoggingContext loggingContext, BuildRequestEntry entry, IRequestBuilderCallback callback, string[] targetNames, Lookup baseLookup, CancellationToken cancellationToken) + { + this.requestEntry = entry; + this.projectLoggingContext = loggingContext; + this.requestCallBack = callback; + this.testDefinition = this.testDataProvider[entry.Request.ConfigurationId]; + this.cancellationToken = cancellationToken; + BuildResult result = GenerateResults(targetNames); + + return Task.FromResult(result); + } + + #region IBuildComponent Members + + /// + /// Sets the component host. + /// + /// The component host. + public void InitializeComponent(IBuildComponentHost host) + { + this.host = host; + this.testDataProvider = (ITestDataProvider)host.GetComponent(BuildComponentType.TestDataProvider); + } + + /// + /// Shuts down the component. + /// + public void ShutdownComponent() + { + this.host = null; + this.testDataProvider = null; + this.testDefinition = null; + this.requestCallBack = null; + this.requestEntry = null; + this.projectLoggingContext = null; + } + + /// + /// Returns the tools version associated which the project configuration + /// + public string GetToolsVersion(string filename, string elementname, string attributename) + { + return this.testDefinition.ToolsVersion; + } + + #endregion + + #region Private Method + + /// + /// Generate results for the targets requested to be built. Using the TestDataProvider also simulate any + /// P2P callbacks on the first target. In order to test the cancels there is also functionality to allow the + /// target execution to wait on a cancel event before exiting + /// + private BuildResult GenerateResults(string[] targetNames) + { + bool simulatedResults = false; + BuildResult result = new BuildResult(this.requestEntry.Request); + foreach (string target in targetNames) + { + if (!simulatedResults) + { + SimulateCallBacks(); + simulatedResults = true; + } + + // Wait for this to be cancelled + if (this.testDefinition.WaitForCancel) + { + this.cancellationToken.WaitHandle.WaitOne(); + this.buildDone.Set(); + throw new BuildAbortedException(); + } + + if (this.testDefinition.ExecutionTime > 0) + { + Thread.Sleep(this.testDefinition.ExecutionTime); + } + + TaskItem[] items = new TaskItem[] { new TaskItem("itemValue", this.requestEntry.RequestConfiguration.ProjectFullPath) }; + TargetResult targetResult = new TargetResult(items, TestUtilities.GetSuccessResult()); + result.AddResultsForTarget(target, targetResult); + } + + buildDone.Set(); + return result; + } + + /// + /// Simulates callback. Access the configuration for the primary project. Retreive the test test data definition. + /// Get the child definitions if available and simulate a callback for each of the child definitions. Note that the + /// targets to build parameter is the same for all the projects - that is we instruct to build the same set of targets + /// for all of the projects. Thus the child test definitions of the entry should have the same set of targets available + /// or a common set of targets available + /// + private void SimulateCallBacks() + { + if (this.testDefinition.ChildDefinitions == null || this.testDefinition.ChildDefinitions.Count < 1) + { + return; + } + + int count = this.testDefinition.ChildDefinitions.Count; + string[] projectFiles = new string[count]; + PropertyDictionary[] properties = new PropertyDictionary[count]; + string[] toolsVersions = new string[count]; + string[] targetsToBuild = null; + + count = 0; + foreach (RequestDefinition d in this.testDefinition.ChildDefinitions) + { + projectFiles[count] = d.FileName; + properties[count] = d.GlobalProperties; + toolsVersions[count] = d.ToolsVersion; + targetsToBuild = d.TargetsToBuild; + count++; + } + + this.requestCallBack.BuildProjects(projectFiles, properties, toolsVersions, targetsToBuild, true); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/MockTaskBuilder.cs b/src/XMakeBuildEngine/UnitTests/QaTests/MockTaskBuilder.cs new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/MockTaskBuilder.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/ProjectDefinition.cs b/src/XMakeBuildEngine/UnitTests/QaTests/ProjectDefinition.cs new file mode 100644 index 00000000000..cc0c52c2380 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/ProjectDefinition.cs @@ -0,0 +1,301 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Defines the elements of a project so that we can create a in-memory project + /// + internal class ProjectDefinition : IDisposable + { + #region Private Data + + /// + /// Initial targets a project should have + /// + private string initialTargets; + + /// + /// Default targets a project should have + /// + private string defaultTargets; + + /// + /// Tools version specified in the project file + /// + private string toolsVersion; + + /// + /// Project file name + /// + private string filename; + + /// + /// If a real instance of MSBuild project should be created + /// + private bool createMSBuildProject; + + /// + /// XMLDocument representation of the project file + /// + private XmlDocumentWithLocation projectXmlDocument; + + /// + /// Project XML + /// + private XmlElement projectRootElement; + + /// + /// List of targets that have been added to this definition + /// + private Dictionary targets; + + /// + /// Project definition to use which is specified by the test + /// + private ProjectInstance msbuildProjectInstance; + + #endregion + + #region Public Method + + /// + /// Constructor which set filename only + /// + + public ProjectDefinition(string filename) + : this(filename, null, null, null, true) + { + } + + /// + /// Constructor which set filename and tools version + /// + + public ProjectDefinition(string filename, string toolsversion) + : this(filename, null, null, toolsversion, true) + { + } + + /// + /// Constructor allows you to set all the data members + /// + public ProjectDefinition(string filename, string initialTargets, string defaultTargets, string toolsVersion, bool createMSBuildProject) + { + this.initialTargets = initialTargets; + this.defaultTargets = defaultTargets; + this.toolsVersion = toolsVersion; + this.filename = filename; + this.createMSBuildProject = createMSBuildProject; + this.projectXmlDocument = new XmlDocumentWithLocation(); + this.targets = new Dictionary(); + this.projectRootElement = this.projectXmlDocument.CreateElement("Project", @"http://schemas.microsoft.com/developer/msbuild/2003"); + GenerateProjectRootElement(); + } + + /// + /// Add a new target to the project + /// + public void AddTarget(TargetDefinition target) + { + this.projectRootElement.AppendChild(target.FinalTargetXmlElement); + this.targets.Add(target.Name, target); + } + + /// + /// Generates a project object of the elements set so forth in this object. This returns the new MSBuild project instance + /// + public ProjectInstance GetMSBuildProjectInstance() + { + if (!this.createMSBuildProject) + { + return null; + } + + if (this.msbuildProjectInstance != null) + { + return this.msbuildProjectInstance; + } + + CreateDefaultTarget(); + ProjectRootElement pXml = ProjectRootElement.Open(this.projectXmlDocument); + Microsoft.Build.Evaluation.Project pDef = new Microsoft.Build.Evaluation.Project(pXml); + return pDef.CreateProjectInstance(); + } + + #endregion + + #region Public Properties + + /// + /// If the test wants to use its own project instance + /// + public ProjectInstance MSBuildProjectInstance + { + set + { + this.msbuildProjectInstance = value; + } + } + + /// + /// XMLDocument reprenstation of the project xml content + /// + public XmlDocument ProjectXmlDocument + { + get + { + return this.projectXmlDocument; + } + } + + /// + /// Project filename + /// + public string Filename + { + get + { + return this.filename; + } + } + + /// + /// List of targets in this definition + /// + public Dictionary TargetsCollection + { + get + { + return this.targets; + } + } + + /// + /// Default Targets for a project + /// + public string DefaultTargets + { + get + { + return this.defaultTargets; + } + set + { + this.defaultTargets = value; + } + } + + /// + /// Initial Targets for a project + /// + public string InitialTargets + { + get + { + return this.initialTargets; + } + set + { + this.initialTargets = value; + } + } + + /// + /// If MSBuild project object is to be created + /// + public bool CreateMSBuildProject + { + get + { + return this.createMSBuildProject; + } + set + { + this.createMSBuildProject = value; + } + } + + #endregion + + #region Private methods + + /// + /// Create a default target in the project file if one is not already there + /// + private void CreateDefaultTarget() + { + if (this.projectRootElement.GetElementsByTagName("Target") == null || this.projectRootElement.GetElementsByTagName("Target").Count == 0) + { + CreateDefaultFirstTarget(); + } + } + + /// + /// Create XML Element representing a target + /// + private void GenerateProjectRootElement() + { + + this.projectRootElement.SetAttribute("xmlns", @"http://schemas.microsoft.com/developer/msbuild/2003"); + + if (this.defaultTargets != null) + { + this.projectRootElement.SetAttribute("DefaultTargets", this.defaultTargets); + } + + if (this.initialTargets != null) + { + this.projectRootElement.SetAttribute("InitialTargets", this.initialTargets); + } + + if (this.toolsVersion != null) + { + this.projectRootElement.SetAttribute("ToolsVersion", this.toolsVersion); + } + + XmlElement propertyGroupElement = this.projectXmlDocument.CreateElement("PropertyGroup", @"http://schemas.microsoft.com/developer/msbuild/2003"); + XmlNode propertyGroup = this.projectRootElement.AppendChild(propertyGroupElement as XmlNode); + XmlElement propertyElement = this.projectXmlDocument.CreateElement("GlobalConfigurationName", @"http://schemas.microsoft.com/developer/msbuild/2003"); + propertyElement.InnerXml = this.filename + ":$(ConfigurationId)"; + propertyGroup.AppendChild(propertyElement as XmlNode); + this.projectXmlDocument.AppendChild(this.projectRootElement as XmlNode); + } + + /// + /// Create a default target in the project file if there are no existing targets in the project file + /// + private void CreateDefaultFirstTarget() + { + TargetDefinition target = new TargetDefinition(RequestDefinition.defaultTargetName, this.projectXmlDocument); + this.AddTarget(target); + this.projectRootElement.AppendChild(target.FinalTargetXmlElement); + } + + #endregion + + #region IDisposable Members + + /// + /// Clear lists created + /// + public void Dispose() + { + this.targets.Clear(); + } + + #endregion + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/RequestBuilder_Tests.cs b/src/XMakeBuildEngine/UnitTests/QaTests/RequestBuilder_Tests.cs new file mode 100644 index 00000000000..749250671cc --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/RequestBuilder_Tests.cs @@ -0,0 +1,421 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Steps to write the tests + /// 1) Create a TestProjectDefinition object for each project file or a build request that you will submit + /// 2) Call Build() on the object to submit the build request + /// 3) Call ValidateResults() on the object to wait till the build completes and the results sent were what we expected + /// NOTE: It is not valid to submit multiple build requests simultinously without waiting for the previous one to complete + /// + [TestClass] + [Ignore] // "Test infrastructure is out of sync with the BuildRequestEngine/Scheduler." + public class RequestBuilder_Tests + { + #region Data members + + private Common_Tests commonTests; + private ResultsCache resultsCache; + private string tempPath; + + #endregion + + #region Constructor + + /// + /// Setup the object + /// + public RequestBuilder_Tests() + { + this.commonTests = new Common_Tests(this.GetComponent, true); + this.resultsCache = null; + this.tempPath = System.IO.Path.GetTempPath(); + } + + #endregion + + #region Common + + /// + /// Delegate to common test setup + /// + [TestInitialize] + public void Setup() + { + this.resultsCache = new ResultsCache(); + this.commonTests.Setup(); + } + + /// + /// Delegate to common test teardown + /// + [TestCleanup] + public void TearDown() + { + this.commonTests.TearDown(); + this.resultsCache = null; + + } + + #endregion + + #region GetComponent delegate + + /// + /// Provides the components required by the tests + /// + internal IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.RequestBuilder: + RequestBuilder requestBuilder = new RequestBuilder(); + return (IBuildComponent)requestBuilder; + + case BuildComponentType.TaskBuilder: + TaskBuilder taskBuilder = new TaskBuilder(); + return (IBuildComponent)taskBuilder; + + case BuildComponentType.TargetBuilder: + QAMockTargetBuilder targetBuilder = new QAMockTargetBuilder(); + return (IBuildComponent)targetBuilder; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)this.resultsCache; + + default: + throw new ArgumentException("Unexpected type requested. Type = " + type.ToString()); + } + } + + #endregion + + #region Project build + + /// + /// Build 1 project containing a single target + /// + [TestMethod] + public void Build1ProjectWith1Target() + { + RequestDefinition p1 = CreateRequestDefinition("1.proj", null, null, 0); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + } + + /// + /// Build 1 project containing a 4 targets + /// + [TestMethod] + public void Build1ProjectWith4Targets() + { + RequestDefinition p1 = CreateRequestDefinition("1.proj", new string[4] { "target1", "target2", "target3", "target4" }, null, 0); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + } + + /// + /// Build 4 project containing a single target + /// + [TestMethod] + public void Build4ProjectWith1Target() + { + RequestDefinition p1 = CreateRequestDefinition("1.proj", null, null, 0); + RequestDefinition p2 = CreateRequestDefinition("2.proj", null, null, 0); + RequestDefinition p3 = CreateRequestDefinition("3.proj", null, null, 0); + RequestDefinition p4 = CreateRequestDefinition("4.proj", null, null, 0); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + + p4.SubmitBuildRequest(); + p4.ValidateBuildResult(); + } + + /// + /// Build 4 project containing a 4 target each + /// + [TestMethod] + public void Build4ProjectWith4Targets() + { + RequestDefinition p1 = CreateRequestDefinition("1.proj", new string[4] { "target1", "target2", "target3", "target4" }, null, 0); + RequestDefinition p2 = CreateRequestDefinition("2.proj", new string[4] { "target1", "target2", "target3", "target4" }, null, 0); + RequestDefinition p3 = CreateRequestDefinition("3.proj", new string[4] { "target1", "target2", "target3", "target4" }, null, 0); + RequestDefinition p4 = CreateRequestDefinition("4.proj", new string[4] { "target1", "target2", "target3", "target4" }, null, 0); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + + p4.SubmitBuildRequest(); + p4.ValidateBuildResult(); + } + + /// + /// Build a single project with a target that takes time to execute. Send a cancel by shutting down the engine + /// + [TestMethod] + public void TestCancellingRequest() + { + RequestDefinition p1 = CreateRequestDefinition("1.proj", null, null, 0); + p1.WaitForCancel = true; + p1.SubmitBuildRequest(); + + this.commonTests.Host.AbortBuild(); + + p1.ValidateBuildAbortedResult(); + } + + #endregion + + #region Common Tests + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProject() + { + this.commonTests.BuildOneProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build4Projects() + { + this.commonTests.Build4DifferentProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildingTheSameProjectTwiceWithDifferentToolsVersion() + { + this.commonTests.BuildingTheSameProjectTwiceWithDifferentToolsVersion(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildingTheSameProjectTwiceWithDifferentToolsGlobalProperties() + { + this.commonTests.BuildingTheSameProjectTwiceWithDifferentGlobalProperties(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNode() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNode(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith1Reference() + { + this.commonTests.BuildOneProjectWith1Reference(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith3Reference() + { + this.commonTests.BuildOneProjectWith3Reference(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith3ReferenceWhere2AreTheSame() + { + this.commonTests.BuildOneProjectWith3ReferenceWhere2AreTheSame(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithEachReferencingANewProject() + { + this.commonTests.BuildMultipleProjectsWithEachReferencingANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects() + { + this.commonTests.BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects() + { + this.commonTests.BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt() + { + this.commonTests.BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt(); + } + + [TestMethod] + public void BuildMultipleProjectsWithReferencesAndDifferentGlobalProperties() + { + this.commonTests.BuildMultipleProjectsWithReferencesAndDifferentGlobalProperties(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithReferencesAndDifferentToolsVersion() + { + this.commonTests.BuildMultipleProjectsWithReferencesAndDifferentToolsVersion(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere1HasAReferenceTo3() + { + this.commonTests.Build3ProjectsWhere1HasAReferenceTo3(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere2HasAReferenceTo3() + { + this.commonTests.Build3ProjectsWhere2HasAReferenceTo3(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere3HasAReferenceTo1() + { + this.commonTests.Build3ProjectsWhere3HasAReferenceTo1(); + } + + #endregion + + /// + /// Returns a new Request definition object created using the parameters passed in + /// + /// Name of the project file in the request. This needs to be a rooted path + /// Targets to build in that project file + /// Tools version for that project file + /// Simulated time the request should take to complete + private RequestDefinition CreateRequestDefinition(string filename, string[] targets, string toolsVersion, int executionTime) + { + if (targets == null) + { + targets = new string[1] { RequestDefinition.defaultTargetName }; + } + + if (toolsVersion == null) + { + toolsVersion = "2.0"; + } + + filename = System.IO.Path.Combine(tempPath, filename); + RequestDefinition request = new RequestDefinition(filename, toolsVersion, targets, null, executionTime, null, (IBuildComponentHost)this.commonTests.Host); + request.CreateMSBuildProject = true; + + return request; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/RequestDefinition.cs b/src/XMakeBuildEngine/UnitTests/QaTests/RequestDefinition.cs new file mode 100644 index 00000000000..1a2321325eb --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/RequestDefinition.cs @@ -0,0 +1,757 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using System.IO; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Defines the build request for a project + /// + internal class RequestDefinition : IDisposable + { + #region Private Data members + + /// + /// Build result generated of this definition + /// + private BuildResult buildResult; + + /// + /// BuildRequest generated for this definition + /// + private BuildRequest buildRequest; + + /// + /// List of child definitions which needs to be built before this definition + /// + private List childDefinitions; + + /// + /// Time in miliseconds this build needs to execute for + /// + private int executionTime; + + /// + /// Targets to build + /// + private string[] targetsToBuild; + + /// + /// Exception recorded during a build process + /// + private Exception buildException; + + /// + /// The test data provider + /// + private ITestDataProvider testDataProvider; + + /// + /// The BuildRequsetEngine + /// + private IBuildRequestEngine requestEngine; + + /// + /// The BuildRequsetEngine + /// + private IResultsCache resultsCache; + + /// + /// File name associated to this definition + /// + private string fileName; + + /// + /// Tools version associated to this definition + /// + private string toolsVersion; + + /// + /// Global properties associated to this definition + /// + private PropertyDictionary globalProperties; + + /// + /// BuildRequestConfiguration associated with this definition + /// + private BuildRequestConfiguration configuration; + + /// + /// Event that gets fired when the request has been completed + /// + private AutoResetEvent testProjectCompletedEvent; + + /// + /// Elements defining the actual project + /// + private ProjectDefinition projectDefinition; + + /// + /// This request is used for testing cancels + /// + private bool waitForCancel; + + /// + /// Global request id starts at 2. 1 is reserved for the root + /// + private static int globalRequestId = 2; + + /// + /// Default tools version name + /// + public const string defaultToolsVersion = "defaulttoolsversion"; + + /// + /// Default target to build name + /// + public const string defaultTargetName = "defaulttarget1"; + + /// + /// Default taskName + /// + public const string defaultTaskName = "defaulttask1"; + + #endregion + + #region Constructor + + /// + /// Constructor takes in the filename. + /// + public RequestDefinition(string fileName, IBuildComponentHost host) + : this(fileName, null, null, null, 0, null, host) + { + } + + /// + /// Constructor takes in the filename and the noTargetToBuild parameters. + /// + public RequestDefinition(string fileName, IBuildComponentHost host, bool noTargetsToBuild) + : this(fileName, null, null, null, 0, null, host, noTargetsToBuild) + { + } + + /// + /// Constructor takes in the filename and an array of child definitions. + /// + public RequestDefinition(string fileName, RequestDefinition[] childDefinitions, IBuildComponentHost host) + : this(fileName, null, null, null, 0, childDefinitions, host) + { + } + + /// + /// Constructor takes the filename and the build execution time + /// + public RequestDefinition(string fileName, int executionTime, IBuildComponentHost host) + : this(fileName, null, null, null, executionTime, null, host) + { + } + + /// + /// Constructor which sets most of the data members except for the noTargetsToBuild bool parameter + /// + public RequestDefinition(string fileName, string toolsVersion, string[] targets, PropertyDictionary properties, int executionTime, RequestDefinition[] childDefinitions, IBuildComponentHost host) + : this(fileName, toolsVersion, targets, properties, executionTime, childDefinitions, host, false) + { + } + /// + /// Constructor allows you to set the filname, toolsversion, targets to build, build properties and execution time. + /// Following are the defaults: + /// ToolsVersion = "ToolsVersion" + /// GlobalProperties = new BuildPropertyGroup() + /// ExecutionTime = 0; + /// Targets to build = "target1" + /// + public RequestDefinition(string fileName, string toolsVersion, string[] targets, PropertyDictionary properties, int executionTime, RequestDefinition[] childDefinitions, IBuildComponentHost host, bool noTargetsToBuild) + { + if (noTargetsToBuild || targets == null) + { + this.targetsToBuild = new string[] { }; + } + else + { + this.targetsToBuild = targets; + } + + this.globalProperties = ((properties == null) ? new PropertyDictionary() : properties); + this.toolsVersion = ((toolsVersion == null) ? RequestDefinition.defaultToolsVersion : toolsVersion); + this.fileName = fileName; + if (childDefinitions != null) + { + this.childDefinitions = new List(childDefinitions); + foreach (RequestDefinition bd in childDefinitions) + { + this.childDefinitions.Add(bd); + } + } + else + { + this.childDefinitions = new List(); + } + + this.testProjectCompletedEvent = new AutoResetEvent(false); + this.executionTime = executionTime; + this.requestEngine = (IBuildRequestEngine)host.GetComponent(BuildComponentType.RequestEngine); + this.testDataProvider = (ITestDataProvider)host.GetComponent(BuildComponentType.TestDataProvider); + this.resultsCache = (IResultsCache)host.GetComponent(BuildComponentType.ResultsCache); + this.testDataProvider.AddDefinition(this); + this.projectDefinition = new ProjectDefinition(this.fileName); + this.waitForCancel = false; + + } + + #endregion + + #region Events handlers raised by the ITestDataProvider + + /// + /// New configuration request. New BuildRequestConfiguration is created and added to the configuration cache. + /// This call is in the ProcessorThread of the TestDataProvider + /// + public void RaiseOnNewConfigurationRequest(BuildRequestConfiguration config) + { + if (this.configuration != null) + { + string message = String.Format("Configuration for request {0}:{1} has already been created", this.configuration.ConfigurationId, this.configuration.ProjectFullPath); + throw new InvalidOperationException(message); + } + + this.configuration = this.testDataProvider.CreateConfiguration(this); + this.requestEngine.ReportConfigurationResponse(new BuildRequestConfigurationResponse(config.ConfigurationId, this.configuration.ConfigurationId, this.configuration.ResultsNodeId)); + } + + /// + /// A new build request for this definition is submitted. This call is in the ProcessorThread of the TestDataProvider + /// + public void RaiseOnNewBuildRequest(BuildRequest request) + { + this.Build(request); + } + + /// + /// Request for this definition is completed. This call is in the ProcessorThread of the TestDataProvider + /// + public void RaiseOnBuildRequestCompleted(BuildRequest request, BuildResult result) + { + if (!result.ResultBelongsToRootRequest) + { + // Don't report root requests to the request engine, as it doesn't wait on them. + this.requestEngine.UnblockBuildRequest(new BuildRequestUnblocker(result)); + } + + this.buildResult = result; + this.testProjectCompletedEvent.Set(); + } + + /// + /// Build request engine threw an exception. This call is in the ProcessorThread of the TestDataProvider + /// + public void RaiseEngineException(Exception e) + { + this.buildException = e; + this.testProjectCompletedEvent.Set(); + } + + #endregion + + #region IDisposable Members + + /// + /// Clean list and events + /// + public void Dispose() + { + this.testProjectCompletedEvent.Close(); + this.childDefinitions.Clear(); + GC.SuppressFinalize(this); + } + + #endregion + + #region Public Properties + + /// + /// Returns an unresolved configuration for this object + /// + public BuildRequestConfiguration UnresolvedConfiguration + { + get + { + return new BuildRequestConfiguration(new BuildRequestData(this.fileName, this.globalProperties.ToDictionary(), this.toolsVersion, new string[0], null), "2.0"); + } + } + + /// + /// Returns the resolved BuildRequestConfiguration + /// + public BuildRequestConfiguration Configuration + { + get + { + return this.configuration; + } + } + + + /// + /// Targets to build for the project + /// + public string[] TargetsToBuild + { + get + { + return this.targetsToBuild; + } + } + + /// + /// Simulate execution of a task by sleeping for the provided # of milliseconds + /// + public int ExecutionTime + { + get + { + return this.executionTime; + } + } + + + + /// + /// Exception throw by the engine during the build process + /// + public Exception BuildException + { + get + { + return this.buildException; + } + } + + + /// + /// Referenes of this project + /// + public List ChildDefinitions + { + get + { + return this.childDefinitions; + } + } + + + /// + /// Build result + /// + public BuildResult Result + { + get + { + return this.buildResult; + } + } + + /// + /// File name of the project file + /// + public string FileName + { + get + { + return this.fileName; + } + } + + /// + /// Tools version to be used when building the project + /// + public string ToolsVersion + { + get + { + return this.toolsVersion; + } + } + + /// + /// Build properties to send when building the projects + /// + public PropertyDictionary GlobalProperties + { + get + { + return this.globalProperties; + } + } + + /// + /// The project definition + /// + public ProjectDefinition ProjectDefinition + { + get + { + return this.projectDefinition; + } + set + { + this.projectDefinition = value; + } + } + + /// + /// If MSBuild project object is to be created + /// + public bool CreateMSBuildProject + { + get + { + return this.projectDefinition.CreateMSBuildProject; + } + set + { + this.projectDefinition.CreateMSBuildProject = value; + } + } + + /// + /// This request is used for testing cancels + /// + public bool WaitForCancel + { + get + { + return this.waitForCancel; + } + set + { + this.waitForCancel = true; + } + } + + #endregion + + #region Public Methods + + /// + /// Adds a reference project to the listw + /// + public void AddChildDefinition(RequestDefinition project) + { + this.childDefinitions.Add(project); + } + + /// + /// Checks if the result received for this build definition is valid. If nothing happens in 10 seconds + /// then we will terminate. This call is in the thread where the test is running. This also validates if + /// the targets requested to be built is built correctly + /// + public void ValidateBuildResult() + { + WaitForResults(); + int targetCount = 0; + + // No specific target was specified to build. Since validateBuildResult is only called for tests where the + // request builder and there on is mocked - just testing request engine - the end result will be a default target + if (this.targetsToBuild.Length == 0) + { + Assert.IsTrue(this.buildResult.HasResultsForTarget(RequestDefinition.defaultTargetName), "Should have results for target:" + RequestDefinition.defaultTargetName); + } + else + { + foreach (string target in this.targetsToBuild) + { + targetCount++; + Assert.IsTrue(this.buildResult.HasResultsForTarget(target), "Should have results for target:" + target); + } + + Assert.AreEqual(targetCount, this.targetsToBuild.Length, "Total target count and returned target count does not match"); + } + + Assert.IsTrue(this.buildResult.OverallResult == BuildResultCode.Success, "Overall results should be a success"); + } + + /// + /// Validate if a Build Aborted exception was received by the host from the engine + /// + public void ValidateBuildAbortedResult() + { + WaitForResultsDontFail(); + + Assert.IsTrue(this.Result.Exception != null, "Expected to receive a Build Aborted exception but no exception was recorded"); + BuildAbortedException be = this.Result.Exception as BuildAbortedException; + Assert.IsTrue(be != null, "Expected to receive a BuildAbortedException but received: " + this.Result.Exception.Message); + } + + + /// + /// Waits for the build request to complete for this this.Result.Exceptionest definition. If nothing happens in 10 seconds + /// then we will terminate. This call is in the thread where the test is running. This does not verify if the targets + /// were built successfully. An exception is thrown if any of the exception object is populated in the result + /// + public void WaitForResultsThrowException() + { + WaitForResultsInternal(true, false); + } + + /// + /// Waits for the build request to complete for this test definition. If nothing happens in 10 seconds + /// then we will terminate. This call is in the thread where the test is running. This does not verify if the targets + /// were built successfully. An exception is not thrown if any of the exception object is populated in the result but we do + /// Assert.Fail + /// + public void WaitForResults() + { + WaitForResultsInternal(false, false); + } + + /// + /// /// + /// Waits for the build request to complete for this test definition. If nothing happens in 10 seconds + /// then we will terminate. This call is in the thread where the test is running. This does not verify if the targets + /// were built successfully. An exception is not thrown if any of the exception object is populated in the result and we do not do any + /// Assert.Fail either + /// + /// + public void WaitForResultsDontFail() + { + WaitForResultsInternal(false, true); + } + + /// + /// Waits for the build request to complete for this test definition. If nothing happens in 10 seconds + /// then we will terminate. This call is in the thread where the test is running. This does not verify if the targets + /// were built successfully + /// + private void WaitForResultsInternal(bool throwOnException, bool dontFail) + { + bool signaled = this.testProjectCompletedEvent.WaitOne(QAMockHost.globalTimeOut, false); + + if (!signaled) + { + Assert.Fail("Timeout after- " + QAMockHost.globalTimeOut.ToString() + " seconds waiting for project:" + this.fileName + " to complete"); + } + else + { + if (this.buildException != null) + { + Assert.Fail("Received engine exception: " + this.buildException + " when building project:" + this.fileName); + } + else + { + Assert.IsTrue(this.buildResult.ConfigurationId == this.configuration.ConfigurationId); + if (this.buildResult.Exception != null) + { + if (throwOnException) + { + throw this.buildResult.Exception; + } + + if(!dontFail) + { + Assert.Fail(this.buildResult.Exception.Message); + } + } + } + } + } + + /// + /// Validates the result for a particular target + /// + public void ValidateTargetBuilt(string targetName) + { + Assert.IsTrue(this.buildResult.HasResultsForTarget(targetName), "Should have results for target:" + targetName); + } + + /// + /// Validates the result for a particular target + /// + public void ValidateTargetDidNotBuild(string targetName) + { + Assert.IsFalse(this.buildResult.HasResultsForTarget(targetName), "Should have not have results for target:" + targetName); + + } + + /// + /// Checks if the target results are as expected. This method can only be called for targets may be built as a result of building + /// the requested / default / initial targets + /// + public void ValidateNonPrimaryTargetEndResult(string targetName, TargetResultCode expectedResultCode, string[] items) + { + BuildResult result = this.resultsCache.GetResultForRequest(this.buildRequest); + TargetResult targetResult = (TargetResult)result[targetName]; + InternalValidateTargetEndResult(targetResult, expectedResultCode, items); + } + + /// + /// Checks if the target results are as expected. This method can only be called for targets which were specifically requested + /// to be built or the default/initial targets + /// + public void ValidateTargetEndResult(string targetName, TargetResultCode expectedResultCode, string[] items) + { + TargetResult targetResult = (TargetResult)this.buildResult[targetName]; + InternalValidateTargetEndResult(targetResult, expectedResultCode, items); + } + + /// + /// Checks if the target results are as expected. + /// + private void InternalValidateTargetEndResult(TargetResult targetResult, TargetResultCode expectedResultCode, string[] items) + { + int foundCount = 0; + + Assert.AreEqual(expectedResultCode, targetResult.ResultCode, "Expected result is not the same as the received result"); + if (items != null) + { + foreach (string item in items) + { + bool foundItemValue = false; + + foreach (ITaskItem i in targetResult.Items) + { + if (item == i.ItemSpec) + { + foundItemValue = true; + foundCount++; + break; + } + } + + Assert.IsTrue(foundItemValue, "Item not found in result"); + } + + Assert.IsTrue(foundCount == items.Length, "Total items expected was not the same as what was received."); + } + } + + /// + /// Have we revceived a result for this request yet. If we have already been signaled, since this is a auto reset event, the state will + /// transit to not signaled and thus ValidateResults will fail. So we have to signal it again. + /// + public bool IsResultAvailable() + { + bool signaled = this.testProjectCompletedEvent.WaitOne(1, false); + if (signaled) + { + this.testProjectCompletedEvent.Set(); + } + + return signaled; + } + + /// + /// Cache the configuration for this build definition and then submit the build request to the request engine. If the request being submitted does not have + /// a GlobalRequestId than assign one. + /// Since we want to make all requests rooted - it the passed in request is null then we will use the dummy root request and make that the parent. This is usually + /// the case when tests submit build requets. When a request is submitted by the RequestBuilder the request is always populated and likely rooted. + /// + public void Build(BuildRequest request) + { + if (request == null) + { + this.configuration = testDataProvider.CreateConfiguration(this); + this.buildRequest = new BuildRequest(1 /* submissionId */, 1, this.configuration.ConfigurationId, this.targetsToBuild, null, BuildEventContext.Invalid, null); + this.buildRequest.GlobalRequestId = RequestDefinition.globalRequestId++; + } + else + { + this.buildRequest = request; + // Assign a new Global Request id if one is not already there + bool assignNewId = false; + if (this.buildRequest.GlobalRequestId == BuildRequest.InvalidGlobalRequestId) + { + foreach (KeyValuePair idRequestPair in this.testDataProvider.RequestDefinitions) + { + if ( + idRequestPair.Value.buildRequest != null && + idRequestPair.Value.buildRequest.ConfigurationId == this.buildRequest.ConfigurationId && + idRequestPair.Value.buildRequest.Targets.Count == this.buildRequest.Targets.Count + ) + { + List leftTargets = new List(idRequestPair.Value.buildRequest.Targets); + List rightTargets = new List(this.buildRequest.Targets); + leftTargets.Sort(StringComparer.OrdinalIgnoreCase); + rightTargets.Sort(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < leftTargets.Count; i++) + { + if (!leftTargets[i].Equals(rightTargets[i], StringComparison.OrdinalIgnoreCase)) + { + assignNewId = true; + break; + } + } + + if (!assignNewId) + { + this.buildRequest.GlobalRequestId = idRequestPair.Value.buildRequest.GlobalRequestId; + break; + } + } + } + } + + if (assignNewId) + { + this.buildRequest.GlobalRequestId = RequestDefinition.globalRequestId++; + } + } + + this.requestEngine.SubmitBuildRequest(this.buildRequest); + } + + /// + /// Cache the configuration for this build definition and then submit the build request to the request engine + /// + public void SubmitBuildRequest() + { + Build(null); + } + + + /// + /// Checks if the passed un-resolved configuration is the same as this definition's configuration elements. If the global properties are not present on either + /// then just compare the file name and the tools version + /// + public bool AreSameDefinitions(BuildRequestConfiguration config) + { + if (this.globalProperties.Count == 0 && ((PropertyDictionary)(config.Properties)).Count == 0) + { + if ( + String.Compare(this.fileName, config.ProjectFullPath, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(this.toolsVersion, config.ToolsVersion, StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return true; + } + } + else + { + if ( + String.Compare(this.fileName, config.ProjectFullPath, StringComparison.OrdinalIgnoreCase) == 0 && + String.Compare(this.toolsVersion, config.ToolsVersion, StringComparison.OrdinalIgnoreCase) == 0 && + this.globalProperties.Equals((PropertyDictionary)(config.Properties)) + ) + { + return true; + } + } + return false; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/TargetBuilder_Tests.cs b/src/XMakeBuildEngine/UnitTests/QaTests/TargetBuilder_Tests.cs new file mode 100644 index 00000000000..03cf5954488 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/TargetBuilder_Tests.cs @@ -0,0 +1,816 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; +using Microsoft.Build.Unittest; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Steps to write the tests + /// 1) Create a TestProjectDefinition object for each project file or a build request that you will submit + /// 2) Call Build() on the object to submit the build request + /// 3) Call ValidateResults() on the object to wait till the build completes and the results sent were what we expected + /// + [TestClass] + [Ignore] // "QA tests are double-initializing some components such as BuildRequestEngine." + public class TargetBuilder_Tests + { + #region Data members + + private Common_Tests commonTests; + private ResultsCache resultsCache; + private string tempPath; + + #endregion + + #region Constructor + + /// + /// Setup the object + /// + public TargetBuilder_Tests() + { + this.commonTests = new Common_Tests(this.GetComponent, true); + this.tempPath = System.IO.Path.GetTempPath(); + this.resultsCache = null; + } + + #endregion + + #region Common + + /// + /// Delegate to common test setup + /// + [TestInitialize] + public void Setup() + { + this.resultsCache = new ResultsCache(); + this.commonTests.Setup(); + } + + /// + /// Delegate to common test teardown + /// + [TestCleanup] + public void TearDown() + { + this.commonTests.TearDown(); + this.resultsCache = null; + } + + #endregion + + #region GetComponent delegate + + /// + /// Provides the components required by the tests + /// + internal IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.RequestBuilder: + RequestBuilder requestBuilder = new RequestBuilder(); + return (IBuildComponent)requestBuilder; + + case BuildComponentType.TaskBuilder: + QAMockTaskBuilder taskBuilder = new QAMockTaskBuilder(); + return (IBuildComponent)taskBuilder; + + case BuildComponentType.TargetBuilder: + TargetBuilder targetBuilder = new TargetBuilder(); + return (IBuildComponent)targetBuilder; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)this.resultsCache; + + default: + throw new ArgumentException("Unexpected type requested. Type = " + type.ToString()); + } + } + + #endregion + + #region Simple building targets + + /// + /// Build 1 project containing a single target with a single task + /// + [TestMethod] + public void Build1ProjectWith1TargetAnd1Task() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition(RequestDefinition.defaultTargetName, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + test1.WaitForResults(); + test1.ValidateTargetBuilt(RequestDefinition.defaultTargetName); + test1.ValidateTargetEndResult(RequestDefinition.defaultTargetName, TargetResultCode.Success, null); + } + + /// + /// Build 1 project containing a single target with a 2 task + /// + [TestMethod] + public void Build1ProjectWith1TargetAnd2Task() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition(RequestDefinition.defaultTargetName, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition("task1", null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition("task2", null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + target1.AddTask(task2); + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + task1.WaitForTaskToComplete(); + task2.WaitForTaskToComplete(); + test1.WaitForResults(); + test1.ValidateTargetBuilt(RequestDefinition.defaultTargetName); + test1.ValidateTargetEndResult(RequestDefinition.defaultTargetName, TargetResultCode.Success, null); + } + + /// + /// Build 1 project containing a 2 target with a 1 task + /// + [TestMethod] + public void Build1ProjectWith2TargetAnd1TaskEach() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[2] { "Target1", "Target2" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition("Target1", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition("Task1", null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition("Task2", null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + target1.AddTask(task2); + project1.AddTarget(target1); + project1.AddTarget(target2); + + test1.SubmitBuildRequest(); + + task1.WaitForTaskToComplete(); + task2.WaitForTaskToComplete(); + test1.WaitForResults(); + test1.ValidateTargetBuilt("Target1"); + test1.ValidateTargetBuilt("Target2"); + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + test1.ValidateTargetEndResult("Target2", TargetResultCode.Success, null); + } + + #endregion + + #region Dependencies + + /// + /// Build a project with 1 target which depends on another target. Validation makes sure that the targets are executed in the order + /// + [TestMethod] + public void BuildProjectWith1TargetWhichDependsOn1Target() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + target2.AddTask(task2); + project1.AddTarget(target1); + project1.AddTarget(target2); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task2.WaitForTaskToComplete(); + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Success, null); + } + + /// + /// Build a project with 1 target which depends on another target. The dependent target has a true condition Validation makes sure that the targets are executed in the order + /// + [TestMethod] + public void BuildDependentTargetWithTrueCondition() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", @"'1' == '1'", project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + target2.AddTask(task2); + project1.AddTarget(target1); + project1.AddTarget(target2); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task2.WaitForTaskToComplete(); + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Success, null); + } + + /// + /// Build a project with 1 target which depends on another target. The dependent target has a false condition Validation makes sure that the targets are executed in the order + /// + [TestMethod] + public void BuildDependentTargetWithFalseCondition() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", @"'1' == '2'", project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + target2.AddTask(task2); + project1.AddTarget(target1); + project1.AddTarget(target2); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + test1.ValidateTargetBuilt("Target1"); + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Skipped, null); + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + } + + /// + /// Dependency chain: Target1 depends on Target2 and Target3 and Target2 depends on Target4 + /// + [TestMethod] + public void TargetDependencyChain() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2;Target3", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", null, "Target4", project1.ProjectXmlDocument); + TargetDefinition target3 = new TargetDefinition("Target3", null, null, project1.ProjectXmlDocument); + TargetDefinition target4 = new TargetDefinition("Target4", null, null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task3 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task4 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + target1.AddTask(task1); + target2.AddTask(task2); + target3.AddTask(task3); + target4.AddTask(task4); + project1.AddTarget(target1); + project1.AddTarget(target2); + project1.AddTarget(target3); + project1.AddTarget(target4); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task4.WaitForTaskToComplete(); + task2.WaitForTaskToComplete(); + task3.WaitForTaskToComplete(); + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target3", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target4", TargetResultCode.Success, null); + } + + /// + /// Dependency chain: Target1 depends on Target2 and Target3 and Target3 depends on Target4 + /// + [TestMethod] + public void TargetDependencyChain2() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2;Target3", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", null, null, project1.ProjectXmlDocument); + TargetDefinition target3 = new TargetDefinition("Target3", null, "Target4", project1.ProjectXmlDocument); + TargetDefinition target4 = new TargetDefinition("Target4", null, null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task3 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task4 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + target1.AddTask(task1); + target2.AddTask(task2); + target3.AddTask(task3); + target4.AddTask(task4); + project1.AddTarget(target1); + project1.AddTarget(target2); + project1.AddTarget(target3); + project1.AddTarget(target4); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task2.WaitForTaskToComplete(); + task4.WaitForTaskToComplete(); + task3.WaitForTaskToComplete(); + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target3", TargetResultCode.Success, null); + test1.ValidateNonPrimaryTargetEndResult("Target4", TargetResultCode.Success, null); + } + + #endregion + + #region Circular Dependency + + /// + /// Target1 depends on target2 and target2 depends on target1 + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void CircularDependencyWithParentTarget() + { + ProjectDefinition project = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("target1", null, "target2", project.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", null, "target1", project.ProjectXmlDocument); + RequestDefinition test = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "target1" }, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + project.AddTarget(target1); + project.AddTarget(target2); + test.ProjectDefinition = project; + + test.SubmitBuildRequest(); + + test.WaitForResultsThrowException(); + } + + + /// + /// Project has a set of initial target - target1 and target2. target2 depends on target1 + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void CircularDependencyInvolvingTargetsWithinInitialTargets() + { + ProjectDefinition project = new ProjectDefinition(FullProjectPath("1.proj"), "target1", null, "2.0", true); + TargetDefinition target1 = new TargetDefinition("target1", null, "target2", project.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", null, "target1", project.ProjectXmlDocument); + + project.AddTarget(target1); + project.AddTarget(target2); + + RequestDefinition request = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "target1" }, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + request.ProjectDefinition = project; + + request.SubmitBuildRequest(); + + request.WaitForResultsThrowException(); + } + + /// + /// Project has a set of default target - target1 and target2. target2 depends on target1 + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void CircularDependencyInvolvingTargetsWithinDefaultTargets() + { + ProjectDefinition project = new ProjectDefinition(FullProjectPath("1.proj"), null, "target1", "2.0", true); + TargetDefinition target1 = new TargetDefinition("target1", null, "target2", project.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", null, "target1", project.ProjectXmlDocument); + + project.AddTarget(target1); + project.AddTarget(target2); + + RequestDefinition request = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "target1" }, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + request.ProjectDefinition = project; + + request.SubmitBuildRequest(); + + request.WaitForResultsThrowException(); + } + + #endregion + + #region Target Rebuild + + /// + /// Build request builds target1, target2 and target1 + /// + [TestMethod] + public void BuildRequestContainsTheSameTargetTwiceWHichPreviouslyPassed() + { + string[] targetsToBuild = new string[3] { "target1", "target2", "target1" }; + + RequestDefinition request = new RequestDefinition(FullProjectPath("1.proj"), "2.0", targetsToBuild, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + + TargetDefinition target1 = new TargetDefinition("target1", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", request.ProjectDefinition.ProjectXmlDocument); + + request.ProjectDefinition.AddTarget(target1); + request.ProjectDefinition.AddTarget(target2); + + request.SubmitBuildRequest(); + + request.WaitForResults(); + + request.ValidateTargetEndResult("target1", TargetResultCode.Success, null); + request.ValidateTargetEndResult("target2", TargetResultCode.Success, null); + } + + /// + /// Target1 depends on target2 and target3. Target2 depends on target4. Target3 depends on target4 + /// + [TestMethod] + public void DependentTargetsDependOnTheSameTarget1() + { + string[] targetsToBuild = new string[1] { "target1" }; + + RequestDefinition request = new RequestDefinition(FullProjectPath("1.proj"), "2.0", targetsToBuild, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + + TargetDefinition target1 = new TargetDefinition("target1", null, "target2;target3", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", null, "target4", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target3 = new TargetDefinition("target3", null, "target4", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target4 = new TargetDefinition("target4", request.ProjectDefinition.ProjectXmlDocument); + + request.ProjectDefinition.AddTarget(target1); + request.ProjectDefinition.AddTarget(target2); + request.ProjectDefinition.AddTarget(target3); + request.ProjectDefinition.AddTarget(target4); + + request.SubmitBuildRequest(); + + request.WaitForResults(); + + request.ValidateTargetEndResult("target1", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target2", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target3", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target4", TargetResultCode.Success, null); + } + + /// + /// Target1 depends on target2 and target3. Target2 depends on target4. Target4 depends on target5. Target3 depends on target5 + /// + [TestMethod] + public void DependentTargetsDependOnTheSameTarget2() + { + string[] targetsToBuild = new string[1] { "target1" }; + + RequestDefinition request = new RequestDefinition(FullProjectPath("1.proj"), "2.0", targetsToBuild, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + + TargetDefinition target1 = new TargetDefinition("target1", null, "target2;target3", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", null, "target4", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target3 = new TargetDefinition("target3", null, "target5", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target4 = new TargetDefinition("target4", null, "target5", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target5 = new TargetDefinition("target5", request.ProjectDefinition.ProjectXmlDocument); + + request.ProjectDefinition.AddTarget(target1); + request.ProjectDefinition.AddTarget(target2); + request.ProjectDefinition.AddTarget(target3); + request.ProjectDefinition.AddTarget(target4); + request.ProjectDefinition.AddTarget(target5); + + request.SubmitBuildRequest(); + + request.WaitForResults(); + + request.ValidateTargetEndResult("target1", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target2", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target3", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target4", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target5", TargetResultCode.Success, null); + } + + /// + /// Target1 depends on target2 and target3. Target2 depends on target4. Target4 depends on target5. Target3 depends on target2 + /// + [TestMethod] + public void DependentTargetsDependOnTheSameTarget3() + { + string[] targetsToBuild = new string[1] { "target1" }; + + RequestDefinition request = new RequestDefinition(FullProjectPath("1.proj"), "2.0", targetsToBuild, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + + TargetDefinition target1 = new TargetDefinition("target1", null, "target2;target3", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("target2", null, "target4", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target3 = new TargetDefinition("target3", null, "target2", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target4 = new TargetDefinition("target4", null, "target5", request.ProjectDefinition.ProjectXmlDocument); + TargetDefinition target5 = new TargetDefinition("target5", request.ProjectDefinition.ProjectXmlDocument); + + request.ProjectDefinition.AddTarget(target1); + request.ProjectDefinition.AddTarget(target2); + request.ProjectDefinition.AddTarget(target3); + request.ProjectDefinition.AddTarget(target4); + request.ProjectDefinition.AddTarget(target5); + + request.SubmitBuildRequest(); + + request.WaitForResults(); + + request.ValidateTargetEndResult("target1", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target2", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target3", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target4", TargetResultCode.Success, null); + request.ValidateNonPrimaryTargetEndResult("target5", TargetResultCode.Success, null); + } + + #endregion + + #region Condition on target + + /// + /// Build 1 project containing a single target with condition evaluating to true + /// + [TestMethod] + public void Build1ProjectWith1TargetWhereConditionIsTrue() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition(RequestDefinition.defaultTargetName, @"'1' == '1'", project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + test1.WaitForResults(); + test1.ValidateTargetBuilt(RequestDefinition.defaultTargetName); + test1.ValidateTargetEndResult(RequestDefinition.defaultTargetName, TargetResultCode.Success, null); + } + + /// + /// Build 1 project containing a single target with condition evaluating to false + /// + [TestMethod] + public void Build1ProjectWith1TargetWhereConditionIsFasle() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition(RequestDefinition.defaultTargetName, @"'1' == '2'", project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + test1.WaitForResults(); + test1.ValidateTargetEndResult(RequestDefinition.defaultTargetName, TargetResultCode.Skipped, null); + } + + #endregion + + #region OnError + + /// + /// Target1 executes task1. Task1 has an error with Stop on error. Target1 has OnError to execute target2 + /// + [TestMethod] + public void OnErrorTargetIsBuilt() + { + RequestDefinition r1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "tar1" }, null, 0, null, (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition p1 = r1.ProjectDefinition; + + TargetDefinition tar1 = new TargetDefinition("tar1", p1.ProjectXmlDocument); + TargetDefinition tarOnError = new TargetDefinition("error", p1.ProjectXmlDocument); + TaskDefinition tas1 = new TaskDefinition("tas1", p1.ProjectXmlDocument, TestUtilities.GetStopWithErrorResult()); + + tar1.AddTask(tas1); + tar1.AddOnError("error", null); + p1.AddTarget(tar1); + p1.AddTarget(tarOnError); + + r1.SubmitBuildRequest(); + + r1.WaitForResults(); + + r1.ValidateTargetEndResult("tar1", TargetResultCode.Failure, null); + r1.ValidateNonPrimaryTargetEndResult("error", TargetResultCode.Success, null); + } + + #endregion + + #region continue with error + + /// + /// Build 1 project containing a single target and a task where the task fails but continues on Error + /// + [TestMethod] + public void Build1ProjectWith1TargetWhereTheTaskFailButContinuesOnError() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition(RequestDefinition.defaultTargetName, null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetContinueWithErrorResult()); + target1.AddTask(task1); + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + test1.WaitForResults(); + test1.ValidateTargetBuilt(RequestDefinition.defaultTargetName); + test1.ValidateTargetEndResult(RequestDefinition.defaultTargetName, TargetResultCode.Success, null); + } + + /// + /// Build a project with 1 target which depends on another target. The task in the dependent target fails with continue on error. + /// + [TestMethod] + public void BuildProjectWith1TargetWhichDependsOn1TargetWhichErrorsWithContinue() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetContinueWithErrorResult()); + target1.AddTask(task1); + target2.AddTask(task2); + project1.AddTarget(target1); + project1.AddTarget(target2); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task2.WaitForTaskToComplete(); + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Success, null); + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + } + + /// + /// Build a project with 1 target which depends on another target. The task in the dependent target fails with continue on error. + /// + [TestMethod] + public void TasksContinueToExecuteAfterContinueOnError() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition("t1-1", null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition("t2-1", null, false, project1.ProjectXmlDocument, TestUtilities.GetContinueWithErrorResult()); + TaskDefinition task3 = new TaskDefinition("t2-2", null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + target1.AddTask(task1); + target2.AddTask(task2); + target2.AddTask(task3); + + project1.AddTarget(target1); + project1.AddTarget(target2); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task2.WaitForTaskToComplete(); + task1.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Success, null); + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, null); + } + + #endregion + + #region Stop on Error + + /// + /// Build a project with 1 target which depends on another target. The task in the dependent target fails. Task 1 should not execute + /// + [TestMethod] + public void BuildProjectWith1TargetWhichDependsOn1TargetWhichErrors() + { + ProjectDefinition project1 = new ProjectDefinition(FullProjectPath("1.proj")); + TargetDefinition target1 = new TargetDefinition("Target1", null, "Target2", project1.ProjectXmlDocument); + TargetDefinition target2 = new TargetDefinition("Target2", null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + TaskDefinition task2 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetStopWithErrorResult()); + target1.AddTask(task1); + target2.AddTask(task2); + project1.AddTarget(target1); + project1.AddTarget(target2); + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), "2.0", new string[1] { "Target1" }, null, 100, null, (IBuildComponentHost)this.commonTests.Host); + test1.ProjectDefinition = project1; + + test1.SubmitBuildRequest(); + + task2.WaitForTaskToComplete(); + test1.WaitForResults(); + + test1.ValidateNonPrimaryTargetEndResult("Target2", TargetResultCode.Failure, null); + test1.ValidateTargetEndResult("Target1", TargetResultCode.Failure, null); + + } + + /// + /// Build 1 project containing a single target and a task where the task fails + /// + [TestMethod] + public void Build1ProjectWith1TargetWhereTheTaskFail() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition(RequestDefinition.defaultTargetName, null, project1.ProjectXmlDocument); + TaskDefinition task1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, project1.ProjectXmlDocument, TestUtilities.GetStopWithErrorResult()); + target1.AddTask(task1); + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + test1.WaitForResults(); + test1.ValidateTargetBuilt(RequestDefinition.defaultTargetName); + test1.ValidateTargetEndResult(RequestDefinition.defaultTargetName, TargetResultCode.Failure, null); + } + + #endregion + + #region Input and Output + + /// + /// Build 1 project containing a single target. The target outputs a string + /// + [TestMethod] + public void ValidateTargetOutput() + { + RequestDefinition test1 = new RequestDefinition(FullProjectPath("1.proj"), (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition project1 = test1.ProjectDefinition; + TargetDefinition target1 = new TargetDefinition("Target1", null, "Foo", null, project1.ProjectXmlDocument); + + project1.AddTarget(target1); + + test1.SubmitBuildRequest(); + + test1.WaitForResults(); + test1.ValidateTargetEndResult("Target1", TargetResultCode.Success, new string[1] {"Foo"}); + } + + #endregion + + #region Task Cancellation + + /// + /// Cancel an executing task. When the task is cancelled the mocked task builder sets the exception to MockTaskBuilderException in TaskResults. + /// This is later added as the target result also + /// + + [TestMethod] + [Ignore] // "Flakey on a slow machine need to investigate" + [ExpectedException(typeof(BuildAbortedException))] + public void TaskStatusOnCancellation() + { + RequestDefinition r1 = new RequestDefinition(FullProjectPath("1.proj"), "4.0", null, null, 5000, null, (IBuildComponentHost)this.commonTests.Host); + ProjectDefinition p1 = r1.ProjectDefinition; + TargetDefinition t1 = new TargetDefinition(RequestDefinition.defaultTargetName, p1.ProjectXmlDocument); + TaskDefinition ta1 = new TaskDefinition(RequestDefinition.defaultTaskName, null, false, p1.ProjectXmlDocument, TestUtilities.GetSuccessResult()); + + t1.AddTask(ta1); + p1.AddTarget(t1); + + r1.SubmitBuildRequest(); + ta1.WaitForTaskToStart(); + this.commonTests.Host.ShutDownRequestEngine(); + + r1.WaitForResultsThrowException(); + } + + #endregion + + #region Private methods + + + /// + /// Full path of the project file + /// + /// File name + /// Full path to the file name + private string FullProjectPath(string filename) + { + filename = System.IO.Path.Combine(this.tempPath, filename); + return filename; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/TargetDefinition.cs b/src/XMakeBuildEngine/UnitTests/QaTests/TargetDefinition.cs new file mode 100644 index 00000000000..4761a961b51 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/TargetDefinition.cs @@ -0,0 +1,245 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Defines the elements of a target within a project file + /// + internal class TargetDefinition : IDisposable + { + #region Private Data + + /// + /// Name of the target + /// + private string name; + + /// + /// Condition of the target + /// + private string condition; + + /// + /// Target Inputs + /// + private string inputs; + + /// + /// Target Outputs + /// + private string outputs; + + /// + /// This target depends on the following targets + /// + private string dependsOnTargets; + + /// + /// Target XML element + /// + private XmlElement targetXmlElement; + + /// + /// Tasks which have been added to this definition + /// + private Dictionary tasks; + + /// + /// Target result + /// + private TargetResult result; + + #endregion + + #region Public Method + + /// + /// Default constructor + /// + public TargetDefinition(string name, XmlDocument projectXmlDoc) : + this(name, null, null, null, null, projectXmlDoc) + { + } + + /// + /// Constructor allows you to set the condition of a target + /// + public TargetDefinition(string name, string condition, XmlDocument projectXmlDoc) : + this(name, null, null, condition, null, projectXmlDoc) + { + } + + /// + /// Constructor allows you to set the condition and dependent targets of the target + /// + public TargetDefinition(string name, string condition, string dependsOnTargets, XmlDocument projectXmlDoc) : + this(name, null, null, condition, dependsOnTargets, projectXmlDoc) + { + } + + /// + /// Constructor allows you to set the inputs, outputs and condition of a target + /// + public TargetDefinition(string name, string inputs, string outputs, string condition, XmlDocument projectXmlDoc) : + this(name, inputs, outputs, condition, null, projectXmlDoc) + { + } + + /// + /// Constructor allows you to set all the elements of the object + /// + public TargetDefinition(string name, string inputs, string outputs, string condition, string dependsOnTargets, XmlDocument projectXmlDoc) + { + this.name = name; + this.inputs = inputs; + this.outputs = outputs; + this.condition = condition; + this.dependsOnTargets = dependsOnTargets; + this.tasks = new Dictionary(); + this.result = null; + this.targetXmlElement = projectXmlDoc.CreateElement("Target", @"http://schemas.microsoft.com/developer/msbuild/2003"); + GenerateTargetElementXml(); + } + + #endregion + + #region Public properties + + /// + /// XMLElement representing the target. This only returns what has been created so far. + /// + public XmlElement FinalTargetXmlElement + { + get + { + return this.targetXmlElement; + } + } + + /// + /// List of tasks that have been added to this definition + /// + public Dictionary TasksCollection + { + get + { + return this.tasks; + } + } + + /// + /// Target result + /// + public TargetResult Result + { + get + { + return this.result; + } + set + { + this.result = value; + } + } + + /// + /// Target Name + /// + public string Name + { + get + { + return this.name; + } + } + + #endregion + + #region Public methods + + /// + /// Adds a task to the target + /// + /// + public void AddTask(TaskDefinition task) + { + this.targetXmlElement.AppendChild(task.FinalTaskXmlElement); + this.tasks.Add(task.Name, task); + } + + /// + /// Adds the OnError Element to the target + /// + public void AddOnError(string onErrorTargets, string onErrorCondition) + { + XmlDocument xmlDoc = targetXmlElement.OwnerDocument; + XmlElement targetOnErrorElement = xmlDoc.CreateElement("OnError", @"http://schemas.microsoft.com/developer/msbuild/2003"); + targetOnErrorElement.SetAttribute("ExecuteTargets", onErrorTargets); + if (onErrorCondition != null) + { + targetOnErrorElement.SetAttribute("Condition", onErrorCondition); + } + + this.targetXmlElement.AppendChild(targetOnErrorElement); + } + + #endregion + + #region Private methods + + /// + /// Generates the base target XMLElement object + /// + /// + private void GenerateTargetElementXml() + { + this.targetXmlElement.SetAttribute("Name", this.name); + + if (this.dependsOnTargets != null) + { + this.targetXmlElement.SetAttribute("DependsOnTargets", this.dependsOnTargets); + } + + if (this.condition != null) + { + this.targetXmlElement.SetAttribute("Condition", this.condition); + } + + if (this.inputs != null) + { + this.targetXmlElement.SetAttribute("Inputs", this.inputs); + } + + if (this.outputs != null) + { + this.targetXmlElement.SetAttribute("Outputs", this.outputs); + } + } + + #endregion + + #region IDisposable Members + + /// + /// Clear lists created + /// + public void Dispose() + { + this.tasks.Clear(); + } + + #endregion + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/TaskBuilder_Tests.cs b/src/XMakeBuildEngine/UnitTests/QaTests/TaskBuilder_Tests.cs new file mode 100644 index 00000000000..16a6b406bcd --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/TaskBuilder_Tests.cs @@ -0,0 +1,260 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Steps to write the tests + /// 1) Create a TestProjectDefinition object for each project file or a build request that you will submit + /// 2) Call Build() on the object to submit the build request + /// 3) Call ValidateResults() on the object to wait till the build completes and the results sent were what we expected + /// + [TestClass] + [Ignore] // "QA tests are double-initializing some components such as BuildRequestEngine." + public class TaskBuilder_Tests + { + #region Data members + + private Common_Tests commonTests; + private ResultsCache resultsCache; + + #endregion + + #region Constructor + + /// + /// Setup the object + /// + public TaskBuilder_Tests() + { + this.commonTests = new Common_Tests(this.GetComponent, true); + this.resultsCache = null; + } + + #endregion + + #region Common + + /// + /// Delegate to common test setup + /// + [TestInitialize] + public void Setup() + { + this.resultsCache = new ResultsCache(); + this.commonTests.Setup(); + } + + /// + /// Delegate to common test teardown + /// + [TestCleanup] + public void TearDown() + { + this.commonTests.TearDown(); + this.resultsCache = null; + } + + #endregion + + #region GetComponent delegate + + /// + /// Provides the components required by the tests + /// + internal IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.RequestBuilder: + RequestBuilder requestBuilder = new RequestBuilder(); + return (IBuildComponent)requestBuilder; + + case BuildComponentType.TaskBuilder: + TaskBuilder taskBuilder = new TaskBuilder(); + return (IBuildComponent)taskBuilder; + + case BuildComponentType.TargetBuilder: + TargetBuilder targetBuilder = new TargetBuilder(); + return (IBuildComponent)targetBuilder; + + case BuildComponentType.ResultsCache: + return (IBuildComponent)this.resultsCache; + + default: + throw new ArgumentException("Unexpected type requested. Type = " + type.ToString()); + } + } + + #endregion + + + #region Common Tests for Callback testing + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNode() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNode(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties() + { + this.commonTests.ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith1Reference() + { + this.commonTests.BuildOneProjectWith1Reference(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith3Reference() + { + this.commonTests.BuildOneProjectWith3Reference(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildOneProjectWith3ReferenceWhere2AreTheSame() + { + this.commonTests.BuildOneProjectWith3ReferenceWhere2AreTheSame(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject() + { + this.commonTests.BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithEachReferencingANewProject() + { + this.commonTests.BuildMultipleProjectsWithEachReferencingANewProject(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects() + { + this.commonTests.BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects() + { + this.commonTests.BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt() + { + this.commonTests.BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void BuildMultipleProjectsWithReferencesAndDifferentToolsVersion() + { + this.commonTests.BuildMultipleProjectsWithReferencesAndDifferentToolsVersion(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere1HasAReferenceTo3() + { + this.commonTests.Build3ProjectsWhere1HasAReferenceTo3(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere2HasAReferenceTo3() + { + this.commonTests.Build3ProjectsWhere2HasAReferenceTo3(); + } + + /// + /// Delegate to common tests + /// + [TestMethod] + public void Build3ProjectsWhere3HasAReferenceTo1() + { + this.commonTests.Build3ProjectsWhere3HasAReferenceTo1(); + } + + #endregion + + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/TaskDefinition.cs b/src/XMakeBuildEngine/UnitTests/QaTests/TaskDefinition.cs new file mode 100644 index 00000000000..b7f45574f4e --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/TaskDefinition.cs @@ -0,0 +1,288 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Defines the elements of a task within a project file + /// + internal class TaskDefinition : IDisposable + { + #region Private data members + + /// + /// Task name + /// + private string name; + + /// + /// Condition on the task + /// + private string condition; + + /// + /// Target should continue if task failed + /// + private bool continueOnError; + + /// + /// Task Xml representation + /// + private XmlElement taskElement; + + /// + /// XMLDocument to use when creating elements + /// + private XmlDocument parentXmlDocument; + + /// + /// Event which notifies if the task has completed execution + /// + private AutoResetEvent taskExecuted; + + /// + /// Event which notifies if the task has started execution + /// + private AutoResetEvent taskStarted; + + /// + /// Final task parameter + /// + private Dictionary finalTaskParameters; + + /// + /// Expected result of the task + /// + private WorkUnitResult expectedResult; + + #endregion + + #region Constructor + + /// + /// Basic constructor which takes the task name + /// + public TaskDefinition(string name, XmlDocument projectXmlDocument) + : this(name, null, false, projectXmlDocument, new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null)) + { + } + + /// + /// Basic constructor which takes the task name and the task expected status + /// + public TaskDefinition(string name, XmlDocument projectXmlDocument, WorkUnitResult expectedResult) + : this(name, null, false, projectXmlDocument, expectedResult) + { + } + + /// + /// Constructor allows you to set all the data + /// + public TaskDefinition(string name, string condition, bool continueOnError, XmlDocument projectXmlDocument, WorkUnitResult expectedResult) + { + this.name = name; + this.condition = condition; + this.continueOnError = continueOnError; + this.taskElement = projectXmlDocument.CreateElement(this.name, @"http://schemas.microsoft.com/developer/msbuild/2003"); + this.parentXmlDocument = projectXmlDocument; + this.expectedResult = expectedResult; + this.finalTaskParameters = new Dictionary(); + this.taskExecuted = new AutoResetEvent(false); + this.taskStarted = new AutoResetEvent(false); + GenerateTaskElement(); + } + + #endregion + + #region Public methods + + /// + /// Add a task input parameter + /// + public void AddTaskInput(string inputName, string inputValue) + { + this.taskElement.SetAttribute(inputName, inputValue); + } + + /// + /// Adds a task output + /// + public void AddTaskOutput(string outputParameterName, string outputAssignmentName, bool assignmentAsProperty) + { + XmlElement output = this.parentXmlDocument.CreateElement("Output", @"http://schemas.microsoft.com/developer/msbuild/2003"); + + output.SetAttribute("TaskParameter", outputParameterName); + if (assignmentAsProperty) + { + output.SetAttribute("PropertyName", outputAssignmentName); + } + else + { + output.SetAttribute("ItemName", outputAssignmentName); + } + + this.taskElement.AppendChild(output as XmlNode); + } + + /// + /// Validates if the parameter name and value were populated correctly + /// + public void ValidateTaskParameter(string parameterName, string parameterValue) + { + if (!this.finalTaskParameters.ContainsKey(parameterName)) + { + Assert.Fail("Final task parameter list does not contain the parameter"); + } + + Assert.AreEqual(this.finalTaskParameters[parameterName], parameterValue, "Value is not the same as expected"); + } + + /// + /// Waits for the task to complete executing + /// + public void WaitForTaskToComplete() + { + this.taskExecuted.WaitOne(); + } + + /// + /// Waits for the task to start executing + /// + public void WaitForTaskToStart() + { + this.taskStarted.WaitOne(); + } + + /// + /// Signals the task has executed + /// + public void SignalTaskCompleted() + { + this.taskExecuted.Set(); + } + + /// + /// Signals the task has started + /// + public void SignalTaskStarted() + { + this.taskStarted.Set(); + } + + #endregion + + #region Public properties + + /// + /// Task XML element + /// + public XmlElement FinalTaskXmlElement + { + get + { + return this.taskElement; + } + } + + /// + /// Expected result code from this task + /// + public WorkUnitResult ExpectedResult + { + get + { + return this.expectedResult; + } + } + + /// + /// Task name + /// + public string Name + { + get + { + return this.name; + } + } + + /// + /// Set the final task parameters. Returns NULL on get regardless as this value can only be used by public methods and not + /// straight by user. + /// + public Dictionary FinalTaskParameters + { + get + { + return null; + } + set + { + this.finalTaskParameters = value; + } + } + + #endregion + + #region Private methods + + /// + /// Generates the task xml element and default attributes + /// + private void GenerateTaskElement() + { + if (this.condition != null) + { + this.taskElement.SetAttribute("Condition", this.condition); + } + + if (this.continueOnError) + { + this.taskElement.SetAttribute("ContinueOnError", "true"); + } + } + + #endregion + + #region IDisposable Members + + /// + /// Close the event handles + /// + public void Dispose() + { + InternalDispose(); + GC.SuppressFinalize(this); + } + + /// + /// Close the event handles + /// + private void InternalDispose() + { + this.taskStarted.Close(); + this.taskExecuted.Close(); + } + + /// + /// Destroy this object + /// + ~TaskDefinition() + { + InternalDispose(); + } + + #endregion + } +} + diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/TestDataProvider.cs b/src/XMakeBuildEngine/UnitTests/QaTests/TestDataProvider.cs new file mode 100644 index 00000000000..a98c43c2eb4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/TestDataProvider.cs @@ -0,0 +1,539 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Implements a QA test data cache to hold all the BuildRequestDefinitions. + /// + internal class TestDataProvider : ITestDataProvider, IBuildComponent, IDisposable + { + #region Data members + + /// + /// The BuildRequestDefinition + /// + private Dictionary definitions; + + /// + /// Configuration cache component + /// + private IConfigCache configurationCache; + + /// + /// The results cache component being used by the host + /// + private IResultsCache resultsCache; + + /// + /// Key assigned to a particular configuration. By default key starts at 2. 1 is reserved for the root + /// + private int key; + + /// + /// Queue that holds the new build requests from the engine + /// + private Queue newRequests; + + /// + /// Queue that holds the new configuration requests from the engine + /// + private Queue newConfigurations; + + /// + /// Queue that holds the results from the engine + /// + private Queue newResults; + + /// + /// Exception thrown by the engine + /// + private Exception engineException; + + /// + /// Thread which is responsible for processing the requests and results + /// + private Thread processorThread; + + /// + /// Event that signals the processor thread to do something + /// + private AutoResetEvent processorThreadResume; + + /// + /// Event that signals the processor thread to exit + /// + private AutoResetEvent processorThreadExit; + + /// + /// Indicates if the processor thread has already exited + /// + private bool processorThreadExited; + + #endregion + + #region Public methods + + /// + /// Creates a new BuildRequestDefinition cache. + /// + public TestDataProvider() + { + this.definitions = new Dictionary(); + this.configurationCache = null; + this.key = 2; + this.newConfigurations = new Queue(); + this.newRequests = new Queue(); + this.newResults = new Queue(); + this.engineException = null; + this.processorThreadResume = new AutoResetEvent(false); + this.processorThreadExit = new AutoResetEvent(false); + this.processorThreadExited = false; + this.processorThread = new Thread(ProcessorThreadProc); + this.processorThread.Name = "Test Data provider processor thread"; + this.processorThread.Start(); + } + + /// + /// Adds a new definition to the cache. Returns the key associated with this definition so that it can be + /// used as the configuration id also. + /// + public int AddDefinition(RequestDefinition definition) + { + int newKey; + + if (this.configurationCache.GetMatchingConfiguration(definition.UnresolvedConfiguration) != null) + { + throw new InvalidOperationException("Multiple request definition with the same configuration cannot be added"); + } + else + { + lock (this.definitions) + { + newKey = key++; + this.definitions.Add(newKey, definition); + } + } + + return newKey; + } + + /// + /// Adds a new configuration to the configuration cache if one is not already there. + /// Some times this method can be called twice specially when 2 request contains the same project + /// where one is a request from the engine (building a reference) and 1 is a request from the test. + /// If the configuration already exists in the cache then we will just use that. + /// + public BuildRequestConfiguration CreateConfiguration(RequestDefinition definition) + { + BuildRequestConfiguration unresolvedConfig = definition.UnresolvedConfiguration; + BuildRequestConfiguration newConfig = this.configurationCache.GetMatchingConfiguration(unresolvedConfig); + if (newConfig == null) + { + int newId = GetIdForUnresolvedConfiguration(unresolvedConfig); + newConfig = new BuildRequestConfiguration(newId, new BuildRequestData(definition.FileName, definition.GlobalProperties.ToDictionary(), definition.ToolsVersion, new string[0], null), "2.0"); + this.configurationCache.AddConfiguration(newConfig); + newConfig.Project = definition.ProjectDefinition.GetMSBuildProjectInstance(); + } + + return newConfig; + } + + /// + /// Dictonary of request definitions where the key is the configuration id and the value is the request defination for that configuration + /// + public Dictionary RequestDefinitions + { + get + { + return this.definitions; + } + } + + #endregion + + #region Public indexers + + /// + /// Returns the BuildRequestDefinition cached under the specified definition id. + /// + public RequestDefinition this[int definationId] + { + get + { + lock (this.definitions) + { + return this.definitions[definationId]; + } + + } + } + + #endregion + + #region ITestDataProvider Members + + /// + /// Adds a new Request to the Queue and signals the ProcessorThread to do something + /// + public BuildRequest NewRequest + { + set + { + if (this.processorThreadExited) + { + return; + } + + lock (this.newRequests) + { + this.newRequests.Enqueue(value); + } + + this.processorThreadResume.Set(); + } + } + + /// + /// Adds a new Configuration to the Queue and signals the ProcessorThread to do something + /// + public BuildRequestConfiguration NewConfiguration + { + set + { + if (this.processorThreadExited) + { + return; + } + + lock(this.newConfigurations) + { + this.newConfigurations.Enqueue(value); + } + + this.processorThreadResume.Set(); + } + } + + /// + /// Adds a new result to the Queue and signals the ProcessorThread to do something + /// + public ResultFromEngine NewResult + { + set + { + if (this.processorThreadExited) + { + return; + } + + lock (this.newResults) + { + this.newResults.Enqueue(value); + } + + this.processorThreadResume.Set(); + } + } + + /// + /// Exception raised by the engine. This is forwarded to all the definitions. + /// Signals the ProcessorThread to do something + /// + public Exception EngineException + { + set + { + if (this.processorThreadExited) + { + return; + } + + this.engineException = value; + this.processorThreadResume.Set(); + } + } + + #endregion + + #region Private methods + + /// + /// Given adefination find and return the key value associated with it which will also act as the config id. + /// Sometimes we may have multiple definitions for the same project file name. So we want to make sure that we pick the correct one. + /// + private int GetIdForUnresolvedConfiguration(BuildRequestConfiguration config) + { + int id = -1; + + lock (this.definitions) + { + foreach (KeyValuePair pair in this.definitions) + { + if (pair.Value.AreSameDefinitions(config)) + { + id = pair.Key; + break; + } + } + } + + return id; + } + + #endregion + + #region Processor Thread + + /// + /// Main thread process which is responsible for processing the configuration + /// and build requests. Currently we process all items fromeach of the Queue. + /// If there is an exception then send it to the definitions. Let the remining + /// Queue be processed then exit the thread + /// + private void ProcessorThreadProc() + { + WaitHandle[] waitHandles = { this.processorThreadExit, this.processorThreadResume }; + while (!this.processorThreadExited) + { + int handle = WaitHandle.WaitAny(waitHandles); + switch (handle) + { + case 0: + // exit + this.processorThreadExited = true; + break; + + case 1: + // something to process + if (this.engineException != null) + { + foreach (RequestDefinition definition in this.definitions.Values) + { + definition.RaiseEngineException(this.engineException); + } + + this.processorThreadExited = true; + } + + // Process new configuration requests + if (this.newConfigurations != null && this.newConfigurations.Count > 0) + { + BuildRequestConfiguration config = null; + config = this.newConfigurations.Peek(); + while (config != null) + { + int newConfigId = this.GetIdForUnresolvedConfiguration(config); + RequestDefinition definition = this[newConfigId]; + definition.RaiseOnNewConfigurationRequest(config); + + lock (this.newConfigurations) + { + this.newConfigurations.Dequeue(); + } + + if (this.newConfigurations.Count > 0) + { + config = this.newConfigurations.Peek(); + } + else + { + config = null; + } + } + } + + // Process new build requests + if (this.newRequests != null && this.newRequests.Count > 0) + { + BuildRequest request = null; + request = this.newRequests.Peek(); + while (request != null) + { + RequestDefinition definition = this[request.ConfigurationId]; + definition.RaiseOnNewBuildRequest(request); + + lock (this.newRequests) + { + this.newRequests.Dequeue(); + } + + if (this.newRequests.Count > 0) + { + request = this.newRequests.Peek(); + } + else + { + request = null; + } + } + } + + // Process results for completed requests + if (this.newResults != null && this.newResults.Count > 0) + { + ResultFromEngine result = null; + result = this.newResults.Peek(); + while (result != null) + { + RequestDefinition definition = this[result.Request.ConfigurationId]; + definition.RaiseOnBuildRequestCompleted(result.Request, result.Result); + + lock (this.newResults) + { + this.newResults.Dequeue(); + } + + if (this.newResults.Count > 0) + { + result = this.newResults.Peek(); + } + else + { + result = null; + } + } + } + + break; + + default: + // Unknown event + this.processorThreadExited = true; + throw new InvalidOperationException("Unknown wait signal received by the ProcessorThread"); + } + } + } + + #endregion + + #region IBuildComponent Members + + /// + /// Save the configuration cache information from the host + /// + public void InitializeComponent(IBuildComponentHost host) + { + this.configurationCache = (IConfigCache)host.GetComponent(BuildComponentType.ConfigCache); + this.resultsCache = (IResultsCache)host.GetComponent(BuildComponentType.ResultsCache); + } + + /// + /// Cleanup + /// + public void ShutdownComponent() + { + // If the processor thread is still there then signal it to go away + // Wait for QAMockHost.globalTimeOut seconds for the thread to go away or complete. If not then abort it. + if (!this.processorThreadExited) + { + this.processorThreadExit.Set(); + if(!this.processorThread.Join(QAMockHost.globalTimeOut)) + { + this.processorThread.Abort(); + } + + this.processorThread = null; + } + + // dispose all the definition object here. + foreach (RequestDefinition definition in this.definitions.Values) + { + definition.Dispose(); + } + + this.definitions.Clear(); + this.newResults.Clear(); + this.newRequests.Clear(); + this.newConfigurations.Clear(); + this.newRequests = null; + this.newResults = null; + this.newConfigurations = null; + this.configurationCache = null; + this.resultsCache = null; + } + + #endregion + + #region Public properties + + /// + /// Result cache build component being used by the host + /// + public IResultsCache ResultsCache + { + get + { + return this.resultsCache; + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + this.processorThreadResume.Close(); + this.processorThreadExit.Close(); + GC.SuppressFinalize(this); + } + + #endregion + } + + /// + /// Holds the result and request pair returned by the engine + /// + internal class ResultFromEngine + { + /// + /// Associated request for the result + /// + private BuildRequest request; + /// + /// Build result + /// + private BuildResult result; + + /// + /// Constructor. This is the only way of setting the data members. + /// + public ResultFromEngine(BuildRequest request, BuildResult result) + { + this.request = request; + this.result = result; + } + + /// + /// Request associated with the result + /// + public BuildRequest Request + { + get + { + return this.request; + } + } + + /// + /// Build result + /// + public BuildResult Result + { + get + { + return this.result; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTests/QaTests/common_Tests.cs b/src/XMakeBuildEngine/UnitTests/QaTests/common_Tests.cs new file mode 100644 index 00000000000..2518e1afd61 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/QaTests/common_Tests.cs @@ -0,0 +1,678 @@ +using System; +using System.Xml; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Collections; + +using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext; + +namespace Microsoft.Build.UnitTests.QA +{ + /// + /// Delegate the GetComponent call back to the test + /// + internal delegate IBuildComponent GetComponentFromTestDelegate(BuildComponentType type); + + /// + /// Steps to write the tests + /// 1) Create a TestProjectDefinition object for each project file or a build request that you will submit + /// 2) Call Build() on the object to submit the build request + /// 3) Call ValidateResults() on the object to wait till the build completes and the results sent were what we expected + /// NOTE: It is not valid to submit multiple build requests simultinously without waiting for the previous one to complete + /// + internal class Common_Tests + { + #region Data members + + private QAMockHost host; + private ConfigCache configCache; + private TestDataProvider testDataProvider; + private BuildRequestEngine requestEngine; + private GetComponentFromTestDelegate getComponent; + private bool createMSBuildProject; + private string tempPath; + + + #endregion + + #region constructor + + /// + /// Setup the delegate for GetComponent request which is delegated from the MockHost + /// + public Common_Tests(GetComponentFromTestDelegate getComponent, bool createMSBuildProject) + { + this.getComponent = getComponent; + this.configCache = null; + this.host = null; + this.requestEngine = null; + this.testDataProvider = null; + this.createMSBuildProject = createMSBuildProject; + this.tempPath = System.IO.Path.GetTempPath(); + } + + #endregion + + #region Public method + + /// + /// Delegate the types we cannot handle to the test + /// + internal IBuildComponent GetComponent(BuildComponentType type) + { + switch (type) + { + case BuildComponentType.ConfigCache: + return (IBuildComponent)this.configCache; + + case BuildComponentType.TestDataProvider: + return (IBuildComponent)this.testDataProvider; + + case BuildComponentType.RequestEngine: + return (IBuildComponent)this.requestEngine; + default: + return this.getComponent(type); + } + } + + /// + /// QA Mock host implementation + /// + public QAMockHost Host + { + get + { + return this.host; + } + } + + #endregion + + #region Common + + /// + /// Setup for each tests + /// + public void Setup() + { + this.host = new QAMockHost(this.GetComponent); + this.testDataProvider = new TestDataProvider(); + this.requestEngine = new BuildRequestEngine(); + this.requestEngine.InitializeComponent(this.host); + this.requestEngine.InitializeForBuild(new NodeLoggingContext(host.LoggingService, 0, false)); + this.configCache = new ConfigCache(); + } + + /// + /// cleanup for each tests + /// + public void TearDown() + { + this.host.ShutdownComponent(); + this.host = null; + this.configCache = null; + this.requestEngine.CleanupForBuild(); + this.requestEngine.ShutdownComponent(); + this.requestEngine = null; + this.testDataProvider = null; + + } + + #endregion + + #region Simple Build Scenarios + + /// + /// Send a build request for a project with 1 target and validate the results. The results validated are the events + /// raised by the build request engine, the results that was generated by the mock request builder and the cache contents + /// + public void BuildOneProject() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + } + + /// + /// Sends multiple build request and validate the results + /// + public void Build4DifferentProjects() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + + p4.SubmitBuildRequest(); + p4.ValidateBuildResult(); + } + + #endregion + + #region Caching Scenarios - New requests + + /// + /// Build the same project twice with different tools version + /// + public void BuildingTheSameProjectTwiceWithDifferentToolsVersion() + { + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0"); + RequestDefinition p2 = CreateNewRequest("1.proj", "3.5"); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + } + + /// + /// Build the same project twice with different global properties + /// + public void BuildingTheSameProjectTwiceWithDifferentGlobalProperties() + { + ProjectPropertyInstance prop1 = ProjectPropertyInstance.Create("prop1", "Value1"); + ProjectPropertyInstance prop2 = ProjectPropertyInstance.Create("prop2", "Value2"); + PropertyDictionary group1 = new PropertyDictionary(); + group1.Set(prop1); + PropertyDictionary group2 = new PropertyDictionary(); + group2.Set(prop2); + + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0", null, group1); + RequestDefinition p2 = CreateNewRequest("1.proj", "3.0", null, group2); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + } + + /// + /// A new build request from the node of a project which was already previously built in that node + /// + public void ReferenceAProjectAlreadyBuiltInTheNode() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + p2.AddChildDefinition(p1); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + } + + #endregion + + #region Caching Scenarios - Internal requests + + /// + /// A new build request from the node for a project which was already previously built in that node but for a different tools version + /// + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentToolsVersion() + { + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0", new string[1] { "t1" }, null); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("1.proj", "3.5", new string[1] { "t1" }, null); + p2.AddChildDefinition(p3); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + } + + /// + /// A new build request from the node for a project which was already previously built in that node but for a different global properties + /// + public void ReferenceAProjectAlreadyBuiltInTheNodeButWithDifferentGlobalProperties() + { + ProjectPropertyInstance prop1 = ProjectPropertyInstance.Create("prop1", "Value1"); + ProjectPropertyInstance prop2 = ProjectPropertyInstance.Create("prop2", "Value2"); + PropertyDictionary group1 = new PropertyDictionary(); + group1.Set(prop1); + PropertyDictionary group2 = new PropertyDictionary(); + group2.Set(prop2); + + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0", new string[1] { "t1" }, group1); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("1.proj", "2.0", new string[1] { "t1" }, group2); + p2.AddChildDefinition(p3); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + } + + #endregion + + #region Callback scenarios + + /// + /// Submit 1 build request which has a single reference + /// + public void BuildOneProjectWith1Reference() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + p1.AddChildDefinition(p2); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + } + + /// + /// Submit 1 build request which has 3 reference + /// + public void BuildOneProjectWith3Reference() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + p1.AddChildDefinition(p2); + p1.AddChildDefinition(p3); + p1.AddChildDefinition(p4); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + } + + /// + /// Submit 1 build request which has 3 reference where 2 are the same + /// UNDONE: This test will fail due to a bug where 3.proj is added 2 times in the unresolvedConfigurations list thus causing a hang + /// + public void BuildOneProjectWith3ReferenceWhere2AreTheSame() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("3.proj"); + p1.AddChildDefinition(p2); + p1.AddChildDefinition(p3); + p1.AddChildDefinition(p4); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where the second one has a project reference + /// + public void BuildMultipleProjectsWithMiddleProjectHavingReferenceToANewProject() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + p2.AddChildDefinition(p4); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + + /// + /// Submit 3 build request where the first one has a project reference + /// + public void BuildMultipleProjectsWithTheFirstProjectHavingReferenceToANewProject() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + p1.AddChildDefinition(p4); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where the last one has a project reference + /// + public void BuildMultipleProjectsWithTheLastProjectHavingReferenceToANewProject() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + p3.AddChildDefinition(p4); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where all of them have a single reference + /// + public void BuildMultipleProjectsWithEachReferencingANewProject() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + RequestDefinition p5 = CreateNewRequest("5.proj"); + RequestDefinition p6 = CreateNewRequest("6.proj"); + p1.AddChildDefinition(p4); + p2.AddChildDefinition(p5); + p3.AddChildDefinition(p6); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where first one has multiple references + /// + public void BuildMultipleProjectsWhereFirstReferencesMultipleNewProjects() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + RequestDefinition p5 = CreateNewRequest("5.proj"); + RequestDefinition p6 = CreateNewRequest("6.proj"); + p1.AddChildDefinition(p4); + p1.AddChildDefinition(p5); + p1.AddChildDefinition(p6); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where first one has multiple references and last has multiple references + /// + public void BuildMultipleProjectsWhereFirstAndLastReferencesMultipleNewProjects() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + RequestDefinition p5 = CreateNewRequest("5.proj"); + RequestDefinition p6 = CreateNewRequest("6.proj"); + RequestDefinition p7 = CreateNewRequest("7.proj"); + p1.AddChildDefinition(p4); + p1.AddChildDefinition(p5); + p3.AddChildDefinition(p6); + p3.AddChildDefinition(p7); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where first one has multiple references and last has multiple references. Some of the references are already built + /// + public void BuildMultipleProjectsWithReferencesWhereSomeReferencesAreAlreadyBuilt() + { + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + RequestDefinition p5 = CreateNewRequest("2.proj"); + RequestDefinition p6 = CreateNewRequest("6.proj"); + RequestDefinition p7 = CreateNewRequest("2.proj"); + p1.AddChildDefinition(p4); + p1.AddChildDefinition(p5); + p3.AddChildDefinition(p6); + p3.AddChildDefinition(p7); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where first one has multiple references and last has multiple references. Some of the references are built with different global properties + /// + public void BuildMultipleProjectsWithReferencesAndDifferentGlobalProperties() + { + ProjectPropertyInstance prop1 = ProjectPropertyInstance.Create("prop1", "Value1"); + ProjectPropertyInstance prop2 = ProjectPropertyInstance.Create("prop2", "Value2"); + PropertyDictionary group1 = new PropertyDictionary(); + group1.Set(prop1); + PropertyDictionary group2 = new PropertyDictionary(); + group2.Set(prop2); + + RequestDefinition p1 = CreateNewRequest("1.proj"); + RequestDefinition p2 = CreateNewRequest("2.proj"); + RequestDefinition p3 = CreateNewRequest("3.proj"); + RequestDefinition p4 = CreateNewRequest("4.proj"); + RequestDefinition p5 = CreateNewRequest("2.proj", group1); + RequestDefinition p6 = CreateNewRequest("6.proj"); + RequestDefinition p7 = CreateNewRequest("2.proj", group2); + p1.AddChildDefinition(p4); + p1.AddChildDefinition(p5); + p3.AddChildDefinition(p6); + p3.AddChildDefinition(p7); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where first one has multiple references and last has multiple references. Some of the references are built with different tools version + /// + public void BuildMultipleProjectsWithReferencesAndDifferentToolsVersion() + { + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0"); + RequestDefinition p2 = CreateNewRequest("2.proj", "2.0"); + RequestDefinition p3 = CreateNewRequest("3.proj", "2.0"); + RequestDefinition p4 = CreateNewRequest("4.proj", "2.0"); + RequestDefinition p5 = CreateNewRequest("2.proj", "3.5"); + RequestDefinition p6 = CreateNewRequest("6.proj", "2.0"); + RequestDefinition p7 = CreateNewRequest("3.proj", "3.5"); + p1.AddChildDefinition(p4); + p1.AddChildDefinition(p5); + p3.AddChildDefinition(p6); + p3.AddChildDefinition(p7); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where project 1 has a reference to Project 3 + /// + public void Build3ProjectsWhere1HasAReferenceTo3() + { + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0"); + RequestDefinition p2 = CreateNewRequest("2.proj", "2.0"); + RequestDefinition p3 = CreateNewRequest("3.proj", "2.0"); + p1.AddChildDefinition(p3); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where project 2 has a reference to Project 3 + /// + public void Build3ProjectsWhere2HasAReferenceTo3() + { + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0"); + RequestDefinition p2 = CreateNewRequest("2.proj", "2.0"); + RequestDefinition p3 = CreateNewRequest("3.proj", "2.0"); + p2.AddChildDefinition(p3); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + /// + /// Submit 3 build request where project 3 has a reference to Project 1 + /// + public void Build3ProjectsWhere3HasAReferenceTo1() + { + RequestDefinition p1 = CreateNewRequest("1.proj", "2.0"); + RequestDefinition p2 = CreateNewRequest("2.proj", "2.0"); + RequestDefinition p3 = CreateNewRequest("3.proj", "2.0"); + p3.AddChildDefinition(p1); + + p1.SubmitBuildRequest(); + p1.ValidateBuildResult(); + + p2.SubmitBuildRequest(); + p2.ValidateBuildResult(); + + p3.SubmitBuildRequest(); + p3.ValidateBuildResult(); + } + + #endregion + + #region Private methods + + /// + /// Helper method to create a new request definition given a filename. + /// + private RequestDefinition CreateNewRequest(string projectFile) + { + return CreateNewRequest(projectFile, null, null, null); + } + + /// + /// Helper method to create a new request definition given a filename and tools version + /// + private RequestDefinition CreateNewRequest(string projectFile, string toolsversion) + { + return CreateNewRequest(projectFile, toolsversion, null, null); + } + + /// + /// Helper method to create a new request definition given a filename and global properties + /// + private RequestDefinition CreateNewRequest(string projectFile, PropertyDictionary globalProperties) + { + return CreateNewRequest(projectFile, null, null, globalProperties); + } + + /// + /// Helper method to create a new request definition given a filename, tools version, targets and global properties + /// + private RequestDefinition CreateNewRequest(string projectFile, string toolsversion, string[] targetsToBuild, PropertyDictionary globalProperties) + { + if (targetsToBuild == null) + { + targetsToBuild = new string[1] { RequestDefinition.defaultTargetName }; + } + + return InternalCreateNewRequest(projectFile, toolsversion, targetsToBuild, globalProperties); + } + + /// + /// Helper method to create the request definition and setting the request definition will have an actual project instance or not + /// + private RequestDefinition InternalCreateNewRequest(string projectFile, string toolsversion, string[] targetsToBuild, PropertyDictionary globalProperties) + { + // Make sure that the path is rooted. This is particularly important when testing implementation of RequestBuilder. The RequestBuild adds default path if the project file path + // is not rooted. This will cause us to not be able to locate the approprate RequestDefinition as the file name is also used as a comparing mechinasim + projectFile = System.IO.Path.Combine(this.tempPath, projectFile); + RequestDefinition p1 = new RequestDefinition(projectFile, toolsversion, targetsToBuild, globalProperties, 0, null, (IBuildComponentHost)this.host); + + // If a project object is to be created then we will need to add all targets that we will be building to the project XML + if (this.createMSBuildProject) + { + p1.CreateMSBuildProject = true; + ProjectDefinition p = p1.ProjectDefinition; + foreach (string target in targetsToBuild) + { + TargetDefinition t = new TargetDefinition(target, p.ProjectXmlDocument); + p1.ProjectDefinition.AddTarget(t); + } + } + + return p1; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Scanner_Tests.cs b/src/XMakeBuildEngine/UnitTests/Scanner_Tests.cs new file mode 100644 index 00000000000..b2acd264358 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Scanner_Tests.cs @@ -0,0 +1,559 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ScannerTest + { + private MockElementLocation _elementLocation = MockElementLocation.Instance; + /// + /// Tests that we give a useful error position (not 0 for example) + /// + [TestMethod] + public void ErrorPosition() + { + string[,] tests = { + { "1==1.1.", "7", "AllowAll"}, // Position of second '.' + { "1==0xFG", "7", "AllowAll"}, // Position of G + { "1==-0xF", "6", "AllowAll"}, // Position of x + { "1234=5678", "6", "AllowAll"}, // Position of '5' + { " ", "2", "AllowAll"}, // Position of End of Input + { " (", "3", "AllowAll"}, // Position of End of Input + { " false or ", "12", "AllowAll"}, // Position of End of Input + { " \"foo", "2", "AllowAll"}, // Position of open quote + { " @(foo", "2", "AllowAll"}, // Position of @ + { " @(", "2", "AllowAll"}, // Position of @ + { " $", "2", "AllowAll"}, // Position of $ + { " $(foo", "2", "AllowAll"}, // Position of $ + { " $(", "2", "AllowAll"}, // Position of $ + { " $", "2", "AllowAll"}, // Position of $ + { " @(foo)", "2", "AllowProperties"}, // Position of @ + { " '@(foo)'", "3", "AllowProperties"}, // Position of @ + /* test escaped chars: message shows them escaped so count should include them */ + { "'%24%28x' == '%24(x''", "21", "AllowAll"} // Position of extra quote + }; + + // Some errors are caught by the Parser, not merely by the Lexer/Scanner. So we have to do a full Parse, + // rather than just calling AdvanceToScannerError(). (The error location is still supplied by the Scanner.) + for (int i = 0; i < tests.GetLength(0); i++) + { + Parser parser = null; + try + { + parser = new Parser(); + ParserOptions options = (ParserOptions)Enum.Parse(typeof(ParserOptions), tests[i, 2], true /* case-insensitive */); + GenericExpressionNode parsedExpression = parser.Parse(tests[i, 0], options, _elementLocation); + } + catch (InvalidProjectFileException ex) + { + Console.WriteLine(ex.Message); + Assert.IsTrue + ( + Convert.ToInt32(tests[i, 1]) == parser.errorPosition, + "Expression '" + tests[i, 0] + "' should have an error at " + tests[i, 1] + " but it was at " + parser.errorPosition + ); + } + } + } + + /// + /// Advance to the point of the lexer error. If the error is only caught by the parser, this isn't useful. + /// + /// + private void AdvanceToScannerError(Scanner lexer) + { + while (true) + { + if (!lexer.Advance()) break; + if (lexer.IsNext(Token.TokenType.EndOfInput)) break; + } + } + + /// + /// Tests the special error for "=". + /// + [TestMethod] + public void SingleEquals() + { + Scanner lexer; + + lexer = new Scanner("a=b", ParserOptions.AllowProperties); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedEqualsInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == "b"); + } + + /// + /// Tests the special errors for "$(" and "$x" and similar cases + /// + [TestMethod] + public void IllFormedProperty() + { + Scanner lexer; + + lexer = new Scanner("$(", ParserOptions.AllowProperties); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedPropertyCloseParenthesisInCondition"); + + lexer = new Scanner("$x", ParserOptions.AllowProperties); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedPropertyOpenParenthesisInCondition"); + } + + /// + /// Tests the special errors for "@(" and "@x" and similar cases. + /// + [TestMethod] + public void IllFormedItemList() + { + Scanner lexer; + + lexer = new Scanner("@(", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedItemListCloseParenthesisInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + + lexer = new Scanner("@x", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedItemListOpenParenthesisInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + + lexer = new Scanner("@(x", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedItemListCloseParenthesisInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + + lexer = new Scanner("@(x->'%(y)", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedItemListQuoteInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + + lexer = new Scanner("@(x->'%(y)', 'x", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedItemListQuoteInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + + lexer = new Scanner("@(x->'%(y)', 'x'", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedItemListCloseParenthesisInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + } + + /// + /// Tests the special error for unterminated quotes. + /// Note, scanner only understands single quotes. + /// + [TestMethod] + public void IllFormedQuotedString() + { + Scanner lexer; + + lexer = new Scanner("false or 'abc", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedQuotedStringInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + + lexer = new Scanner("\'", ParserOptions.AllowAll); + AdvanceToScannerError(lexer); + Assert.IsTrue(lexer.GetErrorResource() == "IllFormedQuotedStringInCondition"); + Assert.IsTrue(lexer.UnexpectedlyFound == null); + } + + /// + /// + [TestMethod] + public void NumericSingleTokenTests() + { + Scanner lexer; + + lexer = new Scanner("1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("1234", lexer.IsNextString()), 0); + + lexer = new Scanner("-1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("-1234", lexer.IsNextString()), 0); + + lexer = new Scanner("+1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("+1234", lexer.IsNextString()), 0); + + lexer = new Scanner("1234.1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("1234.1234", lexer.IsNextString()), 0); + + lexer = new Scanner(".1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare(".1234", lexer.IsNextString()), 0); + + lexer = new Scanner("1234.", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("1234.", lexer.IsNextString()), 0); + lexer = new Scanner("0x1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("0x1234", lexer.IsNextString()), 0); + lexer = new Scanner("0X1234abcd", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("0X1234abcd", lexer.IsNextString()), 0); + lexer = new Scanner("0x1234ABCD", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + Assert.AreEqual(String.Compare("0x1234ABCD", lexer.IsNextString()), 0); + } + + /// + /// + [TestMethod] + public void PropsStringsAndBooleanSingleTokenTests() + { + Scanner lexer = new Scanner("$(foo)", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Property), true); + lexer = new Scanner("@(foo)", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.ItemList), true); + lexer = new Scanner("abcde", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.String), true); + Assert.AreEqual(String.Compare("abcde", lexer.IsNextString()), 0); + + lexer = new Scanner("'abc-efg'", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.String), true); + Assert.AreEqual(String.Compare("abc-efg", lexer.IsNextString()), 0); + + lexer = new Scanner("and", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.And), true); + Assert.AreEqual(String.Compare("and", lexer.IsNextString()), 0); + lexer = new Scanner("or", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Or), true); + Assert.AreEqual(String.Compare("or", lexer.IsNextString()), 0); + lexer = new Scanner("AnD", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.And), true); + Assert.AreEqual(String.Compare(Token.And.String, lexer.IsNextString()), 0); + lexer = new Scanner("Or", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Or), true); + Assert.AreEqual(String.Compare(Token.Or.String, lexer.IsNextString()), 0); + } + + /// + /// + [TestMethod] + public void SimpleSingleTokenTests() + { + Scanner lexer = new Scanner("(", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.LeftParenthesis), true); + lexer = new Scanner(")", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.RightParenthesis), true); + lexer = new Scanner(",", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Comma), true); + lexer = new Scanner("==", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.EqualTo), true); + lexer = new Scanner("!=", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.NotEqualTo), true); + lexer = new Scanner("<", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.LessThan), true); + lexer = new Scanner(">", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.GreaterThan), true); + lexer = new Scanner("<=", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.LessThanOrEqualTo), true); + lexer = new Scanner(">=", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.GreaterThanOrEqualTo), true); + lexer = new Scanner("!", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Not), true); + } + + + /// + /// + [TestMethod] + public void StringEdgeTests() + { + Scanner lexer; + + lexer = new Scanner("@(Foo, ' ')", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EndOfInput)); + + lexer = new Scanner("'@(Foo, ' ')'", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EndOfInput)); + + lexer = new Scanner("'%40(( '", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EndOfInput)); + + lexer = new Scanner("'@(Complex_ItemType-123, ';')' == ''", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EndOfInput)); + } + + /// + /// + [TestMethod] + public void FunctionTests() + { + Scanner lexer; + + lexer = new Scanner("Foo()", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( 1 )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( $(Property) )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( @(ItemList) )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( simplestring )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( 'Not a Simple String' )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( 'Not a Simple String', 1234 )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( $(Property), 'Not a Simple String', 1234 )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + + lexer = new Scanner("Foo( @(ItemList), $(Property), simplestring, 'Not a Simple String', 1234 )", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Function)); + Assert.AreEqual(String.Compare("Foo", lexer.IsNextString()), 0); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LeftParenthesis)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Comma)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.RightParenthesis)); + } + + /// + /// + [TestMethod] + public void ComplexTests1() + { + Scanner lexer; + + lexer = new Scanner("'String with a $(Property) inside'", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.AreEqual(String.Compare("String with a $(Property) inside", lexer.IsNextString()), 0); + + lexer = new Scanner("'String with an embedded \\' in it'", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + // Assert.AreEqual(String.Compare("String with an embedded ' in it", lexer.IsNextString()), 0); + + lexer = new Scanner("'String with a $(Property) inside'", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.AreEqual(String.Compare("String with a $(Property) inside", lexer.IsNextString()), 0); + + lexer = new Scanner("@(list, ' ')", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.AreEqual(String.Compare("@(list, ' ')", lexer.IsNextString()), 0); + + lexer = new Scanner("@(files->'%(Filename)')", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.AreEqual(String.Compare("@(files->'%(Filename)')", lexer.IsNextString()), 0); + } + + /// + /// + [TestMethod] + public void ComplexTests2() + { + Scanner lexer = new Scanner("1234", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + + lexer = new Scanner("'abc-efg'==$(foo)", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.String), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.EqualTo), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Property), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.EndOfInput), true); + + lexer = new Scanner("$(debug)!=true", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Property), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.NotEqualTo), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.String), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.EndOfInput), true); + + lexer = new Scanner("$(VERSION)<5", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance()); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Property), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.LessThan), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.Numeric), true); + lexer.Advance(); + Assert.AreEqual(lexer.IsNext(Token.TokenType.EndOfInput), true); + } + + /// + /// Tests all tokens with no whitespace and whitespace. + /// + [TestMethod] + public void WhitespaceTests() + { + Scanner lexer; + Console.WriteLine("here"); + lexer = new Scanner("$(DEBUG) and $(FOO)", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.And)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + + lexer = new Scanner("1234$(DEBUG)0xabcd@(foo)asdf<>'foo'<=false>=true==1234!=", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LessThan)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.GreaterThan)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LessThanOrEqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.GreaterThanOrEqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.NotEqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EndOfInput)); + + lexer = new Scanner(" 1234 $(DEBUG) 0xabcd \n@(foo) \nasdf \n< \n> \n'foo' \n<= \nfalse \n>= \ntrue \n== \n 1234 \n!= ", ParserOptions.AllowAll); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Property)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.ItemList)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LessThan)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.GreaterThan)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.LessThanOrEqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.GreaterThanOrEqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.String)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.Numeric)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.NotEqualTo)); + Assert.IsTrue(lexer.Advance() && lexer.IsNext(Token.TokenType.EndOfInput)); + } + + /// + /// Tests the parsing of item lists. + /// + [TestMethod] + public void ItemListTests() + { + Scanner lexer; + + lexer = new Scanner("@(foo)", ParserOptions.AllowProperties); + Assert.IsFalse(lexer.Advance()); + Assert.IsTrue(String.Compare(lexer.GetErrorResource(), "ItemListNotAllowedInThisConditional") == 0); + + lexer = new Scanner("1234 '@(foo)'", ParserOptions.AllowProperties); + Assert.IsTrue(lexer.Advance()); + Assert.IsFalse(lexer.Advance()); + Assert.IsTrue(String.Compare(lexer.GetErrorResource(), "ItemListNotAllowedInThisConditional") == 0); + + lexer = new Scanner("'1234 @(foo)'", ParserOptions.AllowProperties); + Assert.IsFalse(lexer.Advance()); + Assert.IsTrue(String.Compare(lexer.GetErrorResource(), "ItemListNotAllowedInThisConditional") == 0); + } + + /// + /// Tests that shouldn't work. + /// + [TestMethod] + public void NegativeTests() + { + Scanner lexer; + + lexer = new Scanner("'$(DEBUG) == true", ParserOptions.AllowAll); + Assert.IsFalse(lexer.Advance()); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTests/TargetsFile_Test.cs b/src/XMakeBuildEngine/UnitTests/TargetsFile_Test.cs new file mode 100644 index 00000000000..05bbc854e2f --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/TargetsFile_Test.cs @@ -0,0 +1,2760 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Moved from Orcas Engine +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests from Orcas + /// + [TestClass] + sealed public class TargetsFile_Test + { + /// + /// Check that the ARM flag is passed to the compiler when targeting ARM. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TargetARM() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + arm + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:arm "); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + #region 32bit preferred + + /// + /// Check that with an empty platformtarget (equivalent to anycpu), library type assemblies do not + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void AnyCPULibraryProjectIsNot32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + v4.5 + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogDoesntContain(" /platform:"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an explicit platform of anycpu, library type assemblies do not + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void ExplicitAnyCPULibraryProjectIsNot32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + v4.5 + AnyCPU + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:AnyCPU "); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an empty platformtarget (equivalent to anycpu), winmdobj type assemblies do not + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void AnyCPUWinMDObjProjectIsNot32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + winmdobj + Debug + + + + v4.5 + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogDoesntContain(" /platform:"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an explicit platformtarget of anycpu, winmdobj type assemblies do not + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ExplicitAnyCPUWinMDObjProjectIsNot32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + winmdobj + Debug + AnyCPU + + + + v4.5 + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:AnyCPU "); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an empty platformtarget (equivalent to anycpu), exe type assemblies + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void AnyCPUExeProjectIs32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Exe + Debug + v4.5 + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred "); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an explicit platformtarget of anycpu, exe type assemblies + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ExplicitAnyCPUExeProjectIs32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Exe + Debug + v4.5 + AnyCPU + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred "); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an empty platformtarget (equivalent to anycpu), exe type assemblies + /// that are targeting .NET 4.0 do not get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + public void AnyCPU40ExeProjectIsNot32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Exe + Debug + v4.0 + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogDoesntContain(" /platform:"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an explicit platformtarget of anycpu, exe type assemblies that are + /// targeting .NET 4.0 do not get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ExplicitAnyCPU40ExeProjectIsNot32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Exe + Debug + v4.0 + AnyCPU + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:AnyCPU "); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an empty platformtarget (equivalent to anycpu), appcontainerexe type assemblies + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void AnyCPUAppContainerExeProjectIs32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + AppContainerExe + Debug + + + + v4.5 + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Check that with an explicit platformtarget of anycpu, appcontainerexe type assemblies + /// get forced to anycpu32bitpreferred by default. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void ExplicitAnyCPUAppContainerExeProjectIs32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + AppContainerExe + Debug + AnyCPU + + + + v4.5 + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Although AnyCPU library projects should not default to AnyCPU32BitPreferred, because that platform is + /// not supported for library projects, if Prefer32Bit is explicitly set, we should still respect that. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void AnyCPULibraryProjectIs32BitPreferredIfPrefer32BitSet() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + AnyCPU + v4.5 + true + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// A project with no explicit OutputType will end up defaulting its OutputType to exe, + /// so it should also default to Prefer32Bit = true. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void AnyCPUProjectWithNoExplicitOutputTypeIs32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Debug + AnyCPU + v4.5 + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// A project with no explicit OutputType will end up defaulting its OutputType to exe, + /// so it should also default to Prefer32Bit = true. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void AnyCPUJupiterProjectWithNoExplicitOutputTypeIs32BitPreferred() + { + string file = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + file = Helpers.CreateFiles("class1.cs")[0]; + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Debug + AnyCPU + v4.5 + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(" /platform:anycpu32bitpreferred"); + } + finally + { + if (file != null) + { + File.Delete(file); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + #endregion 32bit preferred + + /// + /// Validate that the GetFrameworkPaths target + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void TestGetFrameworkPaths() + { + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + + + + + + +", + logger + ); + + project.Build(); + + logger.AssertLogContains(false/* not case sensitive */, "Framework 4.0 = " + FrameworkLocationHelper.PathToDotNetFrameworkV40); + + // Only check .NET 3.5 and below if they're actually on the box. + if (FrameworkLocationHelper.PathToDotNetFrameworkV35 != null) + { + logger.AssertLogContains(false/* not case sensitive */, "Framework 3.5 = " + FrameworkLocationHelper.PathToDotNetFrameworkV35); + logger.AssertLogContains(false/* not case sensitive */, "Framework 3.0 = " + FrameworkLocationHelper.PathToDotNetFrameworkV30); + logger.AssertLogContains(false/* not case sensitive */, "Framework 2.0 = " + FrameworkLocationHelper.PathToDotNetFrameworkV20); + } + } + + /// + /// Validate that the GetFrameworkPaths target + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void TestTargetFrameworkPaths() + { + string[] targetFrameworkVersions = { "v2.0", "v3.0", "v3.5", "v4.0", "v4.5", "" }; + foreach (var version in targetFrameworkVersions) + { + MockLogger logger = new MockLogger(); + string projString = ObjectModelHelpers.CleanupFileContents( + @" + + + + + +" + ); + Project project = ObjectModelHelpers.CreateInMemoryProject( + projString, + logger + ); + + project.SetProperty("TargetFrameworkVersion", version); + project.Build(); + + string targetFrameworkVersion = project.GetPropertyValue("TargetFrameworkVersion"); + string msbuildFrameworkToolsRoot = project.GetPropertyValue("MSBuildFrameworkToolsRoot"); + + if (targetFrameworkVersion.Equals("v2.0")) + { + if (FrameworkLocationHelper.PathToDotNetFrameworkV20 != null) + { + logger.AssertLogContains(false/* not case sensitive */, "Target Framework Folder = " + FrameworkLocationHelper.PathToDotNetFrameworkV20); + } + else + { + // If Framework v2.0 isn't present we use the hard coded version for this validation + logger.AssertLogContains(false/* not case sensitive */, "Target Framework Folder = " + msbuildFrameworkToolsRoot + "v2.0.50727"); + } + } + else if (targetFrameworkVersion.Equals("v3.0") || targetFrameworkVersion.Equals("v3.5")) + { + logger.AssertLogContains(false/* not case sensitive */, "Target Framework Folder = " + msbuildFrameworkToolsRoot + "\\" + targetFrameworkVersion); + } + else if (targetFrameworkVersion.Equals("v4.0")) + { + logger.AssertLogContains(false/* not case sensitive */, "Target Framework Folder = " + FrameworkLocationHelper.PathToDotNetFrameworkV40); + } + else if (targetFrameworkVersion.Equals("v4.5")) + { + logger.AssertLogContains(false/* not case sensitive */, "Target Framework Folder = " + FrameworkLocationHelper.PathToDotNetFrameworkV45); + } + else if (String.IsNullOrEmpty(targetFrameworkVersion)) + { + logger.AssertLogContains(false/* not case sensitive */, "Target Framework Folder = " + FrameworkLocationHelper.PathToDotNetFrameworkV45); + } + } + } + + #region AssignLinkMetadata targets tests + + /// + /// Doesn't synthesize Link metadata if the items are defined in the project + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void NoLinkMetadataSynthesisWhenDefinedInProject() + { + string[] files = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + files = Helpers.CreateFiles("class1.cs", "File1.txt", "Content1.foo"); + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + v4.5 + true + + + + + + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(String.Format("{0}: []", files[0])); + logger.AssertLogContains(String.Format("{0}: []", files[1])); + logger.AssertLogContains(String.Format("{0}: []", files[2])); + } + finally + { + if (files != null) + { + foreach (string file in files) + { + File.Delete(file); + } + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Synthesizes Link metadata if the items are defined in an import and are on the whitelist + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SynthesizeLinkMetadataForItemsOnWhitelist() + { + string[] files = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + string directoryToDelete = null; + + try + { + files = Helpers.CreateFiles("class1.cs", "File1.txt", "Content1.foo", "a.proj"); + + directoryToDelete = Path.GetDirectoryName(files[0]); + string subProjectDirectory = Path.Combine(Path.GetDirectoryName(files[0]), "SubFolder"); + Directory.CreateDirectory(subProjectDirectory); + + string classPath = Path.Combine(subProjectDirectory, "Class1.cs"); + string textFilePath = Path.Combine(subProjectDirectory, "File1.txt"); + string contentPath = Path.Combine(subProjectDirectory, "Content1.foo"); + + File.Move(files[0], classPath); + File.Move(files[1], textFilePath); + File.Move(files[2], contentPath); + + string sharedFilesProjectContents = + @" + + + + + + "; + + File.WriteAllText(files[3], ObjectModelHelpers.CleanupFileContents(sharedFilesProjectContents)); + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + v4.5 + true + + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(String.Format(@"{0}: []", classPath)); + logger.AssertLogContains(String.Format(@"{0}: [SubFolder\File1.txt]", textFilePath)); + logger.AssertLogContains(String.Format(@"{0}: [SubFolder\Content1.foo]", contentPath)); + } + finally + { + if (directoryToDelete != null) + { + ObjectModelHelpers.DeleteDirectory(directoryToDelete); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + /// + /// Don't synthesize link metadata if the SynthesizeLinkMetadata property is false + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void DontSynthesizeLinkMetadataIfPropertyNotSet() + { + string[] files = null; + string outputPath = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + string directoryToDelete = null; + + try + { + files = Helpers.CreateFiles("class1.cs", "File1.txt", "Content1.foo", "a.proj"); + + directoryToDelete = Path.GetDirectoryName(files[0]); + string subProjectDirectory = Path.Combine(Path.GetDirectoryName(files[0]), "SubFolder"); + Directory.CreateDirectory(subProjectDirectory); + + string classPath = Path.Combine(subProjectDirectory, "Class1.cs"); + string textFilePath = Path.Combine(subProjectDirectory, "File1.txt"); + string contentPath = Path.Combine(subProjectDirectory, "Content1.foo"); + + File.Move(files[0], classPath); + File.Move(files[1], textFilePath); + File.Move(files[2], contentPath); + + string sharedFilesProjectContents = + @" + + + + + + "; + + File.WriteAllText(files[3], ObjectModelHelpers.CleanupFileContents(sharedFilesProjectContents)); + + MockLogger logger = new MockLogger(); + + Project project = ObjectModelHelpers.CreateInMemoryProject( + @" + + + " + outputPath + @" + MyAssembly + Library + Debug + v4.5 + false + + + + + + + + + + ", + logger + ); + + project.Build(); + + logger.AssertLogContains(String.Format(@"{0}: []", classPath)); + logger.AssertLogContains(String.Format(@"{0}: []", textFilePath)); + logger.AssertLogContains(String.Format(@"{0}: []", contentPath)); + } + finally + { + if (directoryToDelete != null) + { + ObjectModelHelpers.DeleteDirectory(directoryToDelete); + } + + ObjectModelHelpers.DeleteDirectory(outputPath); + } + } + + #endregion AssignLinkMetadata targets tests + +#if _NOT_YET_FULLY_CONVERTED_ + /// + /// Tests that exercise the the SplitResourcesByCulture Target in Microsoft.Common.targets. + /// This target's job is to separate the items that need to run through resgen from + /// those that need to go directly into CSC. Also, Culture and non-Culture resources + /// are split. + /// + [TestMethod] + public void SplitResourcesByCultureTarget() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + + bin\Debug\ + MyAssembly + Exe + Debug + + + + + + + + + "); + + p.Build(new string [] {"SplitResourcesByCulture"}, null); + + ProjectItem[] items = p.GetItems("EmbeddedResource").ToArray(); + + Assert.AreEqual("Resource2.resx", items[0].EvaluatedInclude); + Assert.AreEqual("false", items[0].GetMetadataValue("WithCulture")); + Assert.AreEqual("Resx", items[0].GetMetadataValue("Type")); + + Assert.AreEqual("Resource1.txt", items[1].EvaluatedInclude); + Assert.AreEqual("false", items[1].GetMetadataValue("WithCulture")); + Assert.AreEqual("Non-Resx", items[1].GetMetadataValue("Type")); + } + + /// + /// Test to make sure that referenced projects are being cleaned properly. + /// + [TestMethod] + public void Regress565788() + { + Helper.CreateTempCSharpProjectWithClassLibrary(); + + string[] buildTarget = new string[1]; + buildTarget[0] = "Build"; + MockLogger log = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", buildTarget, null); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ClassLibrary\bin\debug\ClassLibrary.dll", + "Failed to create ClassLibrary.dll, which should have been created because there was P2P reference from ConsoleApplication"); + + buildTarget[0] = "Clean"; + log = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", buildTarget, null); + + ObjectModelHelpers.AssertFileDoesNotExistInTempProjectDirectory(@"ClassLibrary\bin\debug\ClassLibrary.dll", + "Failed to delete ClassLibrary.dll, which should have been deleted because there was P2P reference from ConsoleApplication"); + } + + /// + /// Tests that we correctly handle .RESTEXT files marked as EmbeddedResource in the project. + /// + [TestMethod] + public void ResTextFiles_CSharp() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // ---------------------------------------------------------------------------- + // ConsoleApplication37.csproj + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ConsoleApplication37.csproj", @" + + + + Debug + AnyCPU + 8.0.50510 + 2.0 + {1EE23632-5998-4CF5-9EAD-11FDC67456E6} + Exe + Properties + ConsoleApplication37 + ConsoleApplication37 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + MyStrings2.resources + + + + + + + + + + "); + + // ---------------------------------------------------------------------------- + // Program.cs + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Program.cs", @" + + using System; + using System.Collections.Generic; + using System.Text; + using System.Resources; + using System.Reflection; + + namespace ConsoleApplication37 + { + class Program + { + static int Main(string[] args) + { + try + { + ResourceManager rm; + + rm = new ResourceManager(`ConsoleApplication37.Strings1`, Assembly.GetExecutingAssembly()); + Console.WriteLine(rm.GetString(`Usage`)); + + rm = new ResourceManager(`MyStrings2`, Assembly.GetExecutingAssembly()); + Console.WriteLine(String.Format(rm.GetString(`InvalidChildElement`), `Foo`)); + + rm = new ResourceManager(`ConsoleApplication37.Subfolder.Strings3`, Assembly.GetExecutingAssembly()); + Console.WriteLine(rm.GetString(`CopyrightMessage`)); + + return 0; + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return 1; + } + } + } + } + + "); + + // ---------------------------------------------------------------------------- + // Strings1.restext + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Strings1.restext", + @"Usage=Hello world! Isn't it a beautiful day?"); + + // ---------------------------------------------------------------------------- + // Strings2.restext + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Strings2.restext", + @"InvalidChildElement=The element {0} is not allowed here."); + + // ---------------------------------------------------------------------------- + // Subfolder\Strings3.restext + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Subfolder\Strings3.restext", + @"CopyrightMessage=Copyright (C) 2005, The MSBuild Team"); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"ConsoleApplication37.csproj"); + + string stdout = ObjectModelHelpers.RunTempProjectBuiltApplication(@"bin\debug\ConsoleApplication37.exe"); + + Assert.IsTrue(@"ConsoleApplication37.exe did not emit Usage string. See Standard Out tab for details.", + stdout.Contains("Hello world! Isn't it a beautiful day?")); + + Assert.IsTrue(@"ConsoleApplication37.exe did not emit InvalidChildElement string. See Standard Out tab for details.", + stdout.Contains("The element Foo is not allowed here.")); + + Assert.IsTrue(@"ConsoleApplication37.exe did not emit CopyrightMessage string. See Standard Out tab for details.", + stdout.Contains("Copyright (C) 2005, The MSBuild Team")); + } + + /// + /// Tests that we correctly handle .RESTEXT files marked as EmbeddedResource in the project. + /// + [TestMethod] + public void ResTextFiles_VB() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // ---------------------------------------------------------------------------- + // ConsoleApplication38.vbproj + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ConsoleApplication38.vbproj", @" + + + + Debug + AnyCPU + 8.0.50510 + 2.0 + {136C326F-8E8F-4164-9AFC-CA8BC754F103} + Exe + ConsoleApplication38.Module1 + ConsoleApplication38 + ConsoleApplication38 + Console + + + true + full + true + true + bin\Debug\ + ConsoleApplication38.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + pdbonly + false + true + true + bin\Release\ + ConsoleApplication38.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + + + + + + + + + + + + + + + + + + + + MyStrings2.resources + + + + + + + "); + + // ---------------------------------------------------------------------------- + // Module1.vb + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Module1.vb", @" + + Module Module1 + + Sub Main() + Try + Dim rm1 As New ResourceManager(`ConsoleApplication38.Strings1`, Assembly.GetExecutingAssembly()) + Console.WriteLine(rm1.GetString(`Usage`)) + + Dim rm2 As New ResourceManager(`MyStrings2`, Assembly.GetExecutingAssembly()) + Console.WriteLine(String.Format(rm2.GetString(`InvalidChildElement`), `Foo`)) + + Dim rm3 As New ResourceManager(`ConsoleApplication38.Strings3`, Assembly.GetExecutingAssembly()) + Console.WriteLine(rm3.GetString(`CopyrightMessage`)) + + Catch ex As Exception + + Console.WriteLine(ex.ToString()) + + End Try + End Sub + + End Module + + "); + + // ---------------------------------------------------------------------------- + // Strings1.restext + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Strings1.restext", + @"Usage=Hello world! Isn't it a beautiful day?"); + + // ---------------------------------------------------------------------------- + // Strings2.restext + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Strings2.restext", + @"InvalidChildElement=The element {0} is not allowed here."); + + // ---------------------------------------------------------------------------- + // Subfolder\Strings3.restext + // ---------------------------------------------------------------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Subfolder\Strings3.restext", + @"CopyrightMessage=Copyright (C) 2005, The MSBuild Team"); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"ConsoleApplication38.vbproj"); + + string stdout = ObjectModelHelpers.RunTempProjectBuiltApplication(@"bin\debug\ConsoleApplication38.exe"); + + Assert.IsTrue(@"ConsoleApplication38.exe did not emit Usage string. See Standard Out tab for details.", + stdout.Contains("Hello world! Isn't it a beautiful day?")); + + Assert.IsTrue(@"ConsoleApplication38.exe did not emit InvalidChildElement string. See Standard Out tab for details.", + stdout.Contains("The element Foo is not allowed here.")); + + Assert.IsTrue(@"ConsoleApplication38.exe did not emit CopyrightMessage string. See Standard Out tab for details.", + stdout.Contains("Copyright (C) 2005, The MSBuild Team")); + } + } + + /// + /// Regress specific bugs. + /// + [TestClass] + sealed public class Bugs + { + /// + /// In this bug, calling Project.EvaluatedProperties cached properties and the next call + /// to run a target didn't invalidate the cache. The result was that the TargetFrameworkDirectory + /// wasn't visible to VS. + /// + [TestMethod] + public void Regress381480() + { + string f0 = FileUtilities.GetTemporaryFile(); + string f1 = FileUtilities.GetTemporaryFile(); + try + { + Project p0 = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + "); + + Project p1 = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + "); + + p0.FullPath = f0; + p1.FullPath = f1; + + p0.Build("GetFrameworkPaths"); + Dictionary preEvaluated = p1.EvaluatedProperties; + p1.Build("GetFrameworkPaths"); + Dictionary postEvaluated = p1.EvaluatedProperties; + + Assert.IsTrue("Expected a value for TargetFrameworkDirectory.", postEvaluated["TargetFrameworkDirectory"].Value.Length > 0); + Assert.IsTrue("Expected a different property group.", preEvaluated != postEvaluated); + } + finally + { + File.Delete(f0); + File.Delete(f1); + } + + } + } + + /// + /// Tests the MainBuiltProjectOutputGroup Target which is responsible for quickly (i.e. without + /// building) returning the name of the EXE or DLL that would be built. + /// + [TestClass] + sealed public class GetTargetPath + { + /// + /// Try a basic workings. + /// + [TestMethod] + public void Basic() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + + bin\Debug\ + MyAssembly + Exe + Debug + + + + + + + "); + + Hashtable h = new Hashtable(); + p.Build(new string[] {"GetTargetPath" }, h); + ObjectModelHelpers.AssertSingleItemInDictionary(h, "<|proj|>bin\\Debug\\MyAssembly.exe"); + } + } + + /// + /// Tests that exercise the the PrepareResourceNames Target in + /// Microsoft.VisualBasic.targets. + /// + /// This target's job is to create manifest resource names for each of + /// the resource files. + /// + [TestClass] + sealed public class PrepareResourceNamesTarget + { + /// + /// Basic test. + /// + [TestMethod] + public void BasicVbResourceNames() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + + bin\Debug\ + MyAssembly + Exe + MyNamespace + true + Debug + + + + + + + + + + "); + + p.Build(new string [] {"PrepareResourceNames"}, null); + + ProjectItem[] items = p.GetItems("EmbeddedResource").ToArray(); + + Assert.AreEqual("Resource2.resx", items[0].EvaluatedInclude); + Assert.AreEqual("false", items[0].GetMetadataValue("WithCulture")); + Assert.AreEqual("Resx", items[0].GetMetadataValue("Type")); + Assert.AreEqual("MyNamespace.Resource2", items[0].GetMetadataValue("ManifestResourceName")); + + Assert.AreEqual("Resource2.fr.resx", items[1].EvaluatedInclude); + Assert.AreEqual("true", items[1].GetMetadataValue("WithCulture")); + Assert.AreEqual("Resx", items[1].GetMetadataValue("Type")); + Assert.AreEqual("MyNamespace.Resource2.fr", items[1].GetMetadataValue("ManifestResourceName")); + + Assert.AreEqual("Resource1.txt", items[2].EvaluatedInclude); + Assert.AreEqual("false", items[2].GetMetadataValue("WithCulture")); + Assert.AreEqual("Non-Resx", items[2].GetMetadataValue("Type")); + Assert.AreEqual("MyNamespace.Resource1.txt", items[2].GetMetadataValue("ManifestResourceName")); + } + } + + /// + /// Tests the CopyAppConfigFile target. + /// + [TestClass] + sealed public class CopyAppConfigFile + { + [TestMethod] + public void CopyAppConfigFileEvenForDllProjects() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // --------------------- + // Foo.csproj + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("foo.csproj", @" + + + Debug + AnyCPU + 8.0.50413 + 2.0 + Library + Properties + ClassLibrary16 + ClassLibrary16 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + "); + + // --------------------- + // Class1.cs + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + using System; + using System.Collections.Generic; + using System.Text; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + // General Information about an assembly is controlled through the following + // set of attributes. Change these attribute values to modify the information + // associated with an assembly. + [assembly: AssemblyTitle(`ClassLibrary16`)] + [assembly: AssemblyDescription(``)] + [assembly: AssemblyConfiguration(``)] + [assembly: AssemblyCompany(`Microsoft`)] + [assembly: AssemblyProduct(`ClassLibrary16`)] + [assembly: AssemblyCopyright(`Copyright © Microsoft 2005`)] + [assembly: AssemblyTrademark(``)] + [assembly: AssemblyCulture(``)] + + // Setting ComVisible to false makes the types in this assembly not visible + // to COM components. If you need to access a type in this assembly from + // COM, set the ComVisible attribute to true on that type. + [assembly: ComVisible(false)] + + // The following GUID is for the ID of the typelib if this project is exposed to COM + [assembly: Guid(`3c11545e-2e63-403b-bd07-5fb2e0f78c92`)] + + // Version information for an assembly consists of the following four values: + // + // Major Version + // Minor Version + // Build Number + // Revision + // + // You can specify all the values or you can default the Revision and Build Numbers + // by using the '*' as shown below: + [assembly: AssemblyVersion(`1.0.0.0`)] + [assembly: AssemblyFileVersion(`1.0.0.0`)] + + namespace ClassLibrary16 + { + public class Class1 + { + } + } + "); + + // --------------------- + // App.config + // --------------------- + ObjectModelHelpers.CreateFileInTempProjectDirectory("App.config", @" + + + + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("foo.csproj"); + + Assert.IsTrue(@"Did not find expected file bin\debug\ClassLibrary16.dll.config", + File.Exists(Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\debug\ClassLibrary16.dll.config"))); + } + + /// + /// Find app.config in various locations + /// + /// + /// The search order is: + /// + /// (1) Choose the value $(AppConfig) set in the main project. + /// (2) Choose @(None) App.Config in the same folder as the project. + /// (3) Choose @(Content) App.Config in the same folder as the project. + /// (4) Choose @(None) App.Config in any subfolder in the project. + /// (5) Choose @(Content) App.Config in any subfolder in the project. + /// + ///If an app.config is not found in one of these locations then there is no app.config for this project. + /// + [TestMethod] + public void AppConfigLocation() + { + // Try each of the cases in turn, by manipulating a single project + ProjectCollection e = new ProjectCollection(); + e.SetGlobalProperty("case", "0"); // Make project loadable + Project p = ObjectModelHelpers.CreateInMemoryProject(e, @" + + + + Library + foo + foo.exe.config + + + + + + + + + + ", null); + + /// (1) Choose the value $(AppConfig) set in the main project. + p.SetGlobalProperty("case", "1"); + p.Build(new string[] { "PrepareForBuild" }); + ProjectItemm item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", "foo.exe.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + + /// (2) Choose @(None) App.Config in the same folder as the project. + p.SetGlobalProperty("case", "2"); + p.Build(new string[] { "PrepareForBuild" }); + item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", "app.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + + /// (3) Choose @(Content) App.Config in the same folder as the project. + p.SetGlobalProperty("case", "3"); + p.Build(new string[] { "PrepareForBuild" }); + item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", "app.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + + /// (4) Choose @(None) App.Config in any subfolder in the project. + p.SetGlobalProperty("case", "4"); + p.Build(new string[] { "PrepareForBuild" }); + item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", "foo\\app.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + + /// (5) Choose @(Content) App.Config in any subfolder in the project. + p.SetGlobalProperty("case", "5"); + p.Build(new string[] { "PrepareForBuild" }); + item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", "bar\\app.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + + ///If an app.config is not found in one of these locations then there is no app.config for this project. + p.SetGlobalProperty("case", "6"); + p.Build(new string[] { "PrepareForBuild" }); + ObjectModelHelpers.AssertNoItem(p, "AppConfigWithTargetPath"); + } + + /// + /// Handle app.config's specified with paths like "..\..\app.config" + /// In this case both app.config's do not match exactly "app.config" so we should take the /last/ + /// match listed. This arbitrary choice matches the behavior we shipped. + /// + [TestMethod] + public void AppConfigLocationRelativeDir() + { + ProjectCollection e = new ProjectCollection(); + Project p = ObjectModelHelpers.CreateInMemoryProject(e, @" + + + + Library + foo + + + + + + + + ", null); + + /// Pick the last one + p.Build(new string[] { "PrepareForBuild" }); + ProjectItemm item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", @".\app.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + } + + /// + /// None should be chosen in preference to Content + /// + [TestMethod] + public void AppConfigLocationNoneWinsOverContent() + { + ProjectCollection e = new ProjectCollection(); + Project p = ObjectModelHelpers.CreateInMemoryProject(e, @" + + + + Library + foo + + + + + + + + ", null); + + /// Pick the last one, trying None first + p.Build(new string[] { "PrepareForBuild" }); + ProjectItemm item = ObjectModelHelpers.AssertSingleItem(p, "AppConfigWithTargetPath", @"c:\foo\app.config"); + Assert.AreEqual("foo.dll.config", item.GetMetadataValue("TargetPath")); + } + } + + /// + /// Tests some general things about our .TARGETS files, such as which properties are referenced. + /// + [TestClass] + sealed public class General + { + /// + /// Tests that our .TARGETS files do not condition on $(Configuration), thereby adding + /// configs to the VS config dropdown when they don't really exist in the project file. + /// + [TestMethod] + public void ConfigurationsReferencedInCSharpProject() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + + Debug + AnyCPU + 8.0.50502 + 2.0 + {083A5AF7-1AD5-416F-8770-BE564F54DA22} + Exe + Properties + ConsoleApplication27 + ConsoleApplication27 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + + + + + + + + + "); + + string[] configurations = p.GetConditionedProperties("Configuration"); + + Console.WriteLine("Configurations found = " + String.Join(", ", configurations)); + Assert.AreEqual("See Standard Out tab for details", 1, configurations.Length); + Assert.AreEqual("See Standard Out tab for details", "FooConfig", configurations[0]); + } + + /// + /// Tests that our .TARGETS files do not condition on $(Configuration), thereby adding + /// configs to the VS config dropdown when they don't really exist in the project file. + /// + [TestMethod] + public void ConfigurationsReferencedInVBProject() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + Debug + AnyCPU + 8.0.50502 + 2.0 + {DB0F2DFA-0164-4071-902D-348330C940E6} + Exe + ConsoleApplication28.Module1 + ConsoleApplication28 + ConsoleApplication28 + Console + + + true + full + true + true + bin\Debug\ + ConsoleApplication28.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + + + + + + + + + + + + + + + + + + True + Application.myapp + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + VbMyResourcesResXFileCodeGenerator + Resources.Designer.vb + My.Resources + + + + + MyApplicationCodeGenerator + Application.Designer.vb + + + SettingsSingleFileGenerator + Settings.Designer.vb + + + + + + "); + + string[] configurations = p.GetConditionedProperties("Configuration"); + + Console.WriteLine("Configurations found = " + String.Join(", ", configurations)); + Assert.AreEqual("See Standard Out tab for details", 1, configurations.Length); + Assert.AreEqual("See Standard Out tab for details", "FooConfig", configurations[0]); + } + + /// + /// This is the infamous path-too-long problem. All absolute paths in question are within + /// the 260 character limit that the filesystem imposes. However, when paths are accessed + /// using relative paths, sometimes the simple concatenation of the current directory with the + /// relative path can exceed 260 characters. MSBuild should solve this scenario by doing + /// smarter path manipulation. + /// + [TestMethod] + public void ProjectToProjectReferenceWithLongRelativePath() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + string tempProjectRoot = ObjectModelHelpers.TempProjectDir; + + // Set deepRelativePath = abc\abc\abc\ ... \abc\abc\abc\ + + // minus 55 to leave room for ConsoleApp\obj\debug\ResolveAssemblyReference.cache + // div 4 because that's how much each subdir costs. + int MAX_PATH = 260; + int numberOfSubDirectoriesToCreate = (MAX_PATH - tempProjectRoot.Length - 55) / 4; + StringBuilder deepRelativePath = new StringBuilder(); + for (int i = 0 ; i < numberOfSubDirectoriesToCreate ; i++) + { + deepRelativePath.Append(@"abc\"); + } + + // Set relativePathToConsoleAppDir = abc\abc\abc\ ... \abc\abc\abc\ConsoleApp\ + string relativePathToConsoleAppDir = deepRelativePath.ToString() + @"ConsoleApp\"; + + // Set relativePathToClassLibDir = abc\abc\abc\ ... \abc\abc\abc\ClassLib\ + string relativePathToClassLibDir = deepRelativePath.ToString() + @"ClassLib\"; + + // ==================================== + // ConsoleApp\ConsoleApp.csproj + // ==================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(relativePathToConsoleAppDir + "ConsoleApp.csproj", @" + + + CleanFile.txt + Debug + AnyCPU + Exe + ConsoleApp + ConsoleApp + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + "); + + // ==================================== + // ConsoleApp\Program.cs + // ==================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(relativePathToConsoleAppDir + "Program.cs", @" + namespace ConsoleApplication79 + { + class Program + { + static void Main(string[] args) + { + ClassLibrary1.LongPathBug foo = new ClassLibrary1.LongPathBug(); + } + } + } + "); + + // ==================================== + // ClassLib\ClassLib.csproj + // ==================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(relativePathToClassLibDir + "ClassLib.csproj", @" + + + CleanFile.txt + Debug + AnyCPU + Library + Properties + ClassLib + ClassLib + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + $(ManagedAssembly) + + + + + "); + + // ==================================== + // ClassLib\Class1.cs + // ==================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(relativePathToClassLibDir + "Class1.cs", @" + namespace ClassLibrary1 + { + public class LongPathBug + { + } + } + "); + + + // Build the ConsoleApp project. + ObjectModelHelpers.BuildTempProjectFileExpectSuccess(relativePathToConsoleAppDir + "ConsoleApp.csproj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(relativePathToConsoleAppDir + @"bin\Debug\ConsoleApp.exe"); + } + + /// + /// There is a C# project that has a P2P ref to a J# project. The C# project supports Debug/Release|AnyCPU. + /// The J# project supports Debug/Release|x86. There is a solution configuration defined call Debug/Release|Mixed Platforms + /// which contains the appropriate project configurations. + /// + [TestMethod] + [Ignore("Need J# to be in the v3.5 folder")] + public void SolutionConfigurationWithDifferentProjectConfigurations() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // ================================================================== + // SOLUTION1.SLN + // ================================================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory("Solution1.sln", + @"Microsoft Visual Studio Solution File, Format Version 9.00 + Visual Studio 2005 + Project(`{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}`) = `CSharpClassLib`, `CSharpClassLib\CSharpClassLib.csproj`, `{9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}` + EndProject + Project(`{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}`) = `JSharpClassLib`, `JSharpClassLib\JSharpClassLib.vjsproj`, `{DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}` + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Debug|x86.ActiveCfg = Debug|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Release|Any CPU.Build.0 = Release|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6}.Release|x86.ActiveCfg = Release|Any CPU + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Debug|Any CPU.ActiveCfg = Debug|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Debug|x86.ActiveCfg = Debug|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Debug|x86.Build.0 = Debug|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Release|Any CPU.ActiveCfg = Release|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Release|Mixed Platforms.Build.0 = Release|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Release|x86.ActiveCfg = Release|x86 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "); + + // ================================================================== + // CSHARPCLASSLIB.CSPROJ + // ================================================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"CSharpClassLib\CSharpClassLib.csproj", @" + + + Debug + AnyCPU + 8.0.50627 + 2.0 + {9FB32A10-FA44-4DD3-ABA8-5215CF599BD6} + Library + Properties + CSharpClassLib + CSharpClassLib + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078} + JSharpClassLib + + + + + + + + + "); + + // ================================================================== + // CLASS1.CS + // ================================================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"CSharpClassLib\Class1.cs", @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace CSharpClassLib + { + public class Class1 + { + JSharpClassLib.Class1 myjsharpclass = new JSharpClassLib.Class1(); + } + } + "); + + // ================================================================== + // JSHARPCLASSLIB.VJSPROJ + // ================================================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"JSharpClassLib\JSharpClassLib.vjsproj", @" + + + Debug + x86 + 8.0.50627 + 2.0 + {DFE7D1F5-B0E8-4EB8-BC1B-0274C2747078} + Library + JSharpClassLib + JSharpClassLib + 4 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + + + pdbonly + true + bin\Release\ + TRACE + + + + + + + + + + + + + + + + "); + + // ================================================================== + // CLASS1.JSL + // ================================================================== + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"JSharpClassLib\Class1.jsl", @" + package JSharpClassLib; + + + /** + * Summary description for Class1 + */ + public class Class1 + { + public Class1() + { + // + // TO DO: Add constructor logic here + // + } + } + "); + + // Build the .SLN + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("Solution1.sln"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"CSharpClassLib\bin\debug\CSharpClassLib.dll"); + } + + /// + /// Tests that the .pdb file is not copied to the output directory when the + /// SkipCopyingSymbolsToOutputDirectory property is set. + /// + [TestMethod] + public void SkipCopyingPdbFile() + { + // create a temp project + Helper.CreateTempCSharpProjectWithClassLibrary(); + + // build it and expect the .pdb to be in the output directory + ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, null); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + // set the SkipCopyingSymbolsToOutputDirectory property + Dictionary additionalProperties = new Dictionary (); + additionalProperties["SkipCopyingSymbolsToOutputDirectory"] = "true"; + + // build the project again and expect the .pdb to have been removed from the output directory + ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, additionalProperties); + ObjectModelHelpers.AssertFileDoesNotExistInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + // set the SkipCopyingSymbolsToOutputDirectory property explicitly to "false" + additionalProperties["SkipCopyingSymbolsToOutputDirectory"]= "false"; + + // build the project again and expect the .pdb to be back in the output directory + ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, null); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + } + + /// + /// Tests that the .pdb file is not produced if /p:debugtype=none is set externally + /// + [TestMethod] + public void SkipProducingPdbCsharp() + { + // create a temp project + Helper.CreateTempCSharpProjectWithClassLibrary(); + + // build it and expect the .pdb to be in the output directory: verify /debug+ /debug:full is default for debug config + MockLogger l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, null); + l.AssertLogContains("/debug+"); + l.AssertLogContains("/debug:full"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + // verify /debug:pdbonly is default for release config + Dictionary additionalProperties = new Dictionary (); + additionalProperties.SetProperty("Configuration", "release"); + l.ClearLog(); + l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, additionalProperties, true); + l.AssertLogDoesntContain("/debug+"); + l.AssertLogContains("/debug:pdbonly"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\release\ConsoleApplication.pdb"); + + // set the DebugSymbols=false property + additionalProperties = new Dictionary (); + additionalProperties.SetProperty("DebugType", "none"); + + // build the project again and expect the .pdb to have been removed from the output directory + l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, additionalProperties, true); + l.AssertLogDoesntContain("/debug+"); + l.AssertLogContains("/debug-"); + l.AssertLogDoesntContain("/debug:full"); + ObjectModelHelpers.AssertFileDoesNotExistInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + // debug config again; set the DebugType property explicitly to "full" + additionalProperties = new Dictionary(); + additionalProperties["DebugType"] ="full"; + + // build the project again and expect the .pdb to be back in the output directory + l.ClearLog(); + l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, additionalProperties, true); + l.AssertLogContains("/debug+"); + l.AssertLogContains("/debug:full"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + // try release configuration with DebugSymbols set to true, as well + additionalProperties = new Dictionary(); + additionalProperties["Configuration"] = "release"; + additionalProperties["DebugSymbols"] = "true"; + l.ClearLog(); + l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.csproj", null, additionalProperties, true); + l.AssertLogContains("/debug+"); + l.AssertLogContains("/debug:pdbonly"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\release\ConsoleApplication.pdb"); + } + + /// + /// Tests that the .pdb file is not produced if /p:debugsymbols=false is set externally + /// + [TestMethod] + public void SkipProducingPdbVB() + { + // create a temp project + Helper.CreateTempVBProject(); + + // build it and expect the .pdb to be in the output directory + MockLogger l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.vbproj", null, null, true); + l.AssertLogContains("/debug+"); + l.AssertLogContains("/debug:full"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + //// set the DebugType=none property + Dictionary additionalProperties = new Dictionary (); + additionalProperties.SetProperty("DebugType", "none"); + + // build the project again and expect the .pdb to have been removed from the output directory + l.ClearLog(); + l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.vbproj", null, additionalProperties, true); + l.AssertLogDoesntContain("/debug+"); + l.AssertLogContains("/debug-"); + l.AssertLogDoesntContain("/debug:full"); + ObjectModelHelpers.AssertFileDoesNotExistInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + + // set the DebugType property explicitly to "full" + additionalProperties.SetProperty("DebugType", "full"); + + // build the project again and expect the .pdb to be back in the output directory + l.ClearLog(); + l = ObjectModelHelpers.BuildTempProjectFileWithTargetsExpectSuccess(@"ConsoleApplication\ConsoleApplication.vbproj", null, null, true); + l.AssertLogContains("/debug+"); + l.AssertLogContains("/debug:full"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"ConsoleApplication\bin\Debug\ConsoleApplication.pdb"); + } + } + + /// + /// Helper methods for unit-tests in this file. + /// + internal static class Helper + { + /// + /// Creates a temporary project on disk for doing unit-tests on. + /// + internal static void CreateTempCSharpProjectWithClassLibrary() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ConsoleApplication\ConsoleApplication.csproj", @" + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {34D5E50A-464A-4098-9DB6-679D5310E7EB} + Exe + Properties + ConsoleApplication + ConsoleApplication + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + {50E656DA-C81B-43D5-B2ED-8B5DCB2398EB} + ClassLibrary1 + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ConsoleApplication\Program.cs", @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace ConsoleApplication1 + { + class Program + { + static void Main(string[] args) + { + } + } + } + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ClassLibrary\ClassLibrary.csproj", @" + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {50E656DA-C81B-43D5-B2ED-8B5DCB2398EB} + Library + Properties + ClassLibrary + ClassLibrary + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ClassLibrary\Class.cs", @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace ClassLibrary1 + { + public class Class1 + { + } + } + "); + } + + /// + /// Creates a temporary project on disk for doing unit-tests on. + /// + internal static void CreateTempVBProject() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ConsoleApplication\ConsoleApplication.vbproj", @" + + + Debug + AnyCPU + 8.0.60512 + 2.0 + {32D53832-D685-4FF7-B093-8ADCE0CA9F20} + Exe + VBconsoleapp.Module1 + VBconsoleapp + ConsoleApplication + 512 + Console + + + true + full + true + true + bin\Debug\ + VBconsoleapp.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + pdbonly + false + true + true + bin\Release\ + VBconsoleapp.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + + + + + + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"ConsoleApplication\Module1.vb", @" + Module Module1 + + Sub Main() + + End Sub + + End Module + "); + } +#endif + } +} diff --git a/src/XMakeBuildEngine/UnitTests/TestUtilities.cs b/src/XMakeBuildEngine/UnitTests/TestUtilities.cs new file mode 100644 index 00000000000..9d1c481fd5e --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/TestUtilities.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem; + +namespace Microsoft.Build.Unittest +{ + internal class TestUtilities + { + #region Public Static methods + + public static TargetResult GetEmptyFailingTargetResult() + { + return new TargetResult(new TaskItem[0] { }, TestUtilities.GetStopWithErrorResult()); + } + + public static TargetResult GetEmptySucceedingTargetResult() + { + return new TargetResult(new TaskItem[0] { }, TestUtilities.GetSuccessResult()); + } + + public static WorkUnitResult GetSuccessResult() + { + return new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null); + } + + public static WorkUnitResult GetSkippedResult() + { + return new WorkUnitResult(WorkUnitResultCode.Skipped, WorkUnitActionCode.Continue, null); + } + + public static WorkUnitResult GetStopWithErrorResult() + { + return new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, null); + } + + public static WorkUnitResult GetStopWithErrorResult(Exception e) + { + return new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Stop, e); + } + + public static WorkUnitResult GetContinueWithErrorResult() + { + return new WorkUnitResult(WorkUnitResultCode.Failed, WorkUnitActionCode.Continue, null); + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTests/Utilities_Tests.cs b/src/XMakeBuildEngine/UnitTests/Utilities_Tests.cs new file mode 100644 index 00000000000..4b89e845351 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTests/Utilities_Tests.cs @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Reflection; +using System.Collections; +using System.Collections.Specialized; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +using Toolset = Microsoft.Build.Evaluation.Toolset; +using Project = Microsoft.Build.Evaluation.Project; +using ProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; + +using InternalUtilities = Microsoft.Build.Internal.Utilities; +using CommunicationsUtilities = Microsoft.Build.Internal.CommunicationsUtilities; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +using XmlDocumentWithLocation = Microsoft.Build.Construction.XmlDocumentWithLocation; +using XmlElementWithLocation = Microsoft.Build.Construction.XmlElementWithLocation; + +using MSBuildApp = Microsoft.Build.CommandLine.MSBuildApp; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class UtilitiesTestStandard : UtilitiesTest + { + public UtilitiesTestStandard() + { + this.loadAsReadOnly = false; + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment5() + { + string xmlText = "<"; + string xmlContents = GetXmlContents(xmlText); + // Should get XML; note space after x added + Assert.AreEqual("<", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment6() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + // Should get XML; note space after x added + Assert.AreEqual("", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment7() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + // Should get XML; note space after x added + Assert.AreEqual("", xmlContents); + } + } + + [TestClass] + public class UtilitiesTestReadOnlyLoad : UtilitiesTest + { + public UtilitiesTestReadOnlyLoad() + { + this.loadAsReadOnly = true; + } + + /// + /// Comments should not be stripped when doing /pp. + /// This is really testing msbuild.exe but it's here because it needs to + /// call the internal reset method on the engine + /// + [TestMethod] + [Ignore] + // Ignore: Flaky test + public void CommentsInPreprocessing() + { + Microsoft.Build.Construction.XmlDocumentWithLocation.ClearReadOnlyFlags_UnitTestsOnly(); + + string input = FileUtilities.GetTemporaryFile(); + string output = FileUtilities.GetTemporaryFile(); + + try + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + +"); + File.WriteAllText(input, content); + + Assert.AreEqual(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\bin\msbuild.exe """ + input + @""" /pp:""" + output + @"""")); + + bool foundDoNotModify = false; + foreach (string line in File.ReadLines(output)) + { + if (line.Contains("")) // This is what it will look like if we're loading read/only + { + Assert.Fail(); + } + + if (line.Contains("DO NOT MODIFY")) // this is in a comment in our targets + { + foundDoNotModify = true; + } + } + + Assert.AreEqual(true, foundDoNotModify); + } + finally + { + File.Delete(input); + File.Delete(output); + } + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment5() + { + string xmlText = "<"; + string xmlContents = GetXmlContents(xmlText); + // Should get XML; note space after x added + Assert.AreEqual("<", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment6() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + // Should get XML; note space after x added + Assert.AreEqual("", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment7() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + // Should get XML; note space after x added + Assert.AreEqual("", xmlContents); + } + } + + [TestClass] + public abstract class UtilitiesTest + { + public bool loadAsReadOnly; + + /// + /// Verify Condition is illegal on ProjectExtensions tag + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void IllegalConditionOnProjectExtensions() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + "); + } + + /// + /// Verify ProjectExtensions cannot exist twice + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void RepeatedProjectExtensions() + { + Project p = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + "); + } + + /// + /// Tests that we can correctly pass a CDATA tag containing less-than signs into a property value. + /// + [TestMethod] + public void GetCDATAWithLessThanSignFromXmlNode() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual(" + /// Tests that we can correctly pass an Xml element named "CDATA" into a property value. + ///
+ [TestMethod] + public void GetLiteralCDATAWithLessThanSignFromXmlNode() + { + string xmlText = "This is not a real , just trying to fool the reader."; + string xmlContents = GetXmlContents(xmlText); + + // Notice the extra space after "CDATA" because it normalized the XML. + Assert.AreEqual("This is not a real , just trying to fool the reader.", xmlContents); + } + + /// + /// Tests that we can correctly pass a simple CDATA tag into a property value. + /// + [TestMethod] + public void GetCDATAFromXmlNode() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("whatever", xmlContents); + } + + /// + /// Tests that we can correctly pass a literal string called "CDATA" into a property value. + /// + [TestMethod] + public void GetLiteralCDATAFromXmlNode() + { + string xmlText = "This is not a real CDATA, just trying to fool the reader."; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("This is not a real CDATA, just trying to fool the reader.", xmlContents); + } + + /// + /// Tests that we can correctly parse a property that is Xml containing a CDATA tag. + /// + [TestMethod] + public void GetCDATAOccurringDeeperWithMoreXml() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("", xmlContents); + } + + /// + /// Tests that we can correctly pass CDATA where the CDATA tag itself is surrounded by whitespace + /// + [TestMethod] + public void GetCDATAWithSurroundingWhitespace() + { + string xmlText = " "; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("foo", xmlContents); + } + + /// + /// Tests that we can correctly parse a property that is some text concatenated with some XML. + /// + [TestMethod] + public void GetTextContainingLessThanSignFromXmlNode() + { + string xmlText = "This is some text contain a node , & an escaped character."; + string xmlContents = GetXmlContents(xmlText); + + // Notice the extra space in the xml node because it normalized the XML, and the + // change from single quotes to double-quotes. + Assert.AreEqual("This is some text contain a node , & an escaped character.", xmlContents); + } + + /// + /// Tests that we can correctly parse a property containing text with an escaped character. + /// + [TestMethod] + public void GetTextFromXmlNode() + { + string xmlText = "This is some text & an escaped character."; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("This is some text & an escaped character.", xmlContents); + } + + /// + /// Tests that comments are removed if there is no other XML in the value. + /// In other words, .InnerText is used even if there are comments (as long as nothing else looks like XML in the string) + /// + [TestMethod] + public void GetTextFromTextNodeWithXmlComment() + { + string xmlText = "foo; biz; & boz"; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("foo; biz; & boz", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment2() + { + string xmlText = "xyz"; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("xyz", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment3() + { + string xmlText = ""; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("", xmlContents); + } + + [TestMethod] + public void GetTextFromTextNodeWithXmlComment4() + { + string xmlText = "-->"; + string xmlContents = GetXmlContents(xmlText); + Assert.AreEqual("-->", xmlContents); + } + + /// + /// Check creating the tools version list for an error message + /// + [TestMethod] + public void CreateToolsVersionString() + { + List toolsets = new List(); + toolsets.Add(new Toolset("66", "x", new ProjectCollection(), null)); + toolsets.Add(new Toolset("44", "y", new ProjectCollection(), null)); + + string result = InternalUtilities.CreateToolsVersionListString(toolsets); + + Assert.AreEqual("\"66\", \"44\"", result); + } + + /// + /// Verify our custom way of getting env vars gives the same results as the BCL. + /// + [TestMethod] + public void GetEnvVars() + { + IDictionary envVars = CommunicationsUtilities.GetEnvironmentVariables(); + IDictionary referenceVars = Environment.GetEnvironmentVariables(); + IDictionary referenceVars2 = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (DictionaryEntry item in referenceVars) + { + referenceVars2.Add((string)item.Key, (string)item.Value); + } + + Helpers.AssertCollectionsValueEqual(envVars, referenceVars2); + } + + protected string GetXmlContents(string xmlText) + { + XmlDocumentWithLocation xmldoc = new XmlDocumentWithLocation(loadAsReadOnly); + xmldoc.LoadXml(xmlText); + + XmlElementWithLocation rootElement = (XmlElementWithLocation)xmldoc.FirstChild; + Console.WriteLine("originalxml = " + xmlText); + Console.WriteLine("innerText = " + rootElement.InnerText); + Console.WriteLine("innerXml = " + rootElement.InnerXml); + Console.WriteLine("-----------"); + + string xmlContents = InternalUtilities.GetXmlNodeInnerContents(rootElement); + return xmlContents; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/App.config b/src/XMakeBuildEngine/UnitTestsPublicOM/App.config new file mode 100644 index 00000000000..dd585970512 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/App.config @@ -0,0 +1,28 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/AssemblyResources.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/AssemblyResources.cs new file mode 100644 index 00000000000..825c882a224 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/AssemblyResources.cs @@ -0,0 +1,18 @@ +using System; + +namespace Microsoft.Build.Shared +{ + /// + /// This class provides access to the assembly's resources. + /// + internal static class AssemblyResources + { + /// + /// Dummy + /// + internal static string GetString(string name) + { + return String.Empty; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ConstructionEditing_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ConstructionEditing_Tests.cs new file mode 100644 index 00000000000..8c6d7feeeee --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ConstructionEditing_Tests.cs @@ -0,0 +1,2661 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for editing through the construction model. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for editing through the construction model + /// + [TestClass] + public class ConstructionEditing_Tests + { + /// + /// Add a target through the convenience method + /// + [TestMethod] + public void AddTargetConvenience() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(1, project.Count); + Assert.AreEqual(0, target.Count); + Assert.AreEqual(1, Helpers.Count(project.Children)); + Assert.AreEqual(0, Helpers.Count(target.Children)); + Assert.AreEqual(null, project.Parent); + Assert.AreEqual(project, target.Parent); + } + + /// + /// Simple add a target + /// + [TestMethod] + public void AppendTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + Helpers.ClearDirtyFlag(project); + ProjectTargetElement target = project.CreateTargetElement("t"); + Assert.AreEqual(false, project.HasUnsavedChanges); + + project.AppendChild(target); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(1, project.Count); + } + + /// + /// Append two targets + /// + [TestMethod] + public void AppendTargetTwice() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.CreateTargetElement("t"); + ProjectTargetElement target2 = project.CreateTargetElement("t2"); + + project.AppendChild(target1); + project.AppendChild(target2); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + + Assert.AreEqual(2, project.Count); + var targets = Helpers.MakeList(project.Targets); + Assert.AreEqual(2, targets.Count); + Assert.AreEqual(target1, targets[0]); + Assert.AreEqual(target2, targets[1]); + } + + /// + /// Add node created from different project with AppendChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAddFromDifferentProject_AppendChild() + { + ProjectRootElement project1 = ProjectRootElement.Create(); + ProjectRootElement project2 = ProjectRootElement.Create(); + ProjectTargetElement target = project1.CreateTargetElement("t"); + project2.AppendChild(target); + } + + /// + /// Add node created from different project with PrependChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAddFromDifferentProject_PrependChild() + { + ProjectRootElement project1 = ProjectRootElement.Create(); + ProjectRootElement project2 = ProjectRootElement.Create(); + ProjectTargetElement target = project1.CreateTargetElement("t"); + project2.PrependChild(target); + } + + /// + /// Add node created from different project with InsertBeforeChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAddFromDifferentProject_InsertBefore() + { + ProjectRootElement project1 = ProjectRootElement.Create(); + ProjectRootElement project2 = ProjectRootElement.Create(); + ProjectTargetElement target1 = project1.CreateTargetElement("t"); + ProjectTargetElement target2 = project2.AddTarget("t2"); + project2.InsertBeforeChild(target2, target1); + } + + /// + /// Add node created from different project with InsertAfterChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAddFromDifferentProject_InsertAfter() + { + ProjectRootElement project1 = ProjectRootElement.Create(); + ProjectRootElement project2 = ProjectRootElement.Create(); + ProjectTargetElement target1 = project1.CreateTargetElement("t"); + ProjectTargetElement target2 = project2.AddTarget("t2"); + project2.InsertAfterChild(target2, target1); + } + + /// + /// Become direct child of self with AppendChild + /// (This is prevented anyway because the parent is an invalid type.) + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidBecomeChildOfSelf_AppendChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + + choose.AppendChild(choose); + } + + /// + /// Become grandchild of self with AppendChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidBecomeGrandChildOfSelf_AppendChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + project.AppendChild(choose); + choose.AppendChild(when); + when.AppendChild(choose); + } + + /// + /// Become grandchild of self with PrependChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidBecomeGrandChildOfSelf_PrependChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + project.AppendChild(choose); + choose.AppendChild(when); + when.PrependChild(choose); + } + + /// + /// Become grandchild of self with InsertBeforeChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidBecomeGrandChildOfSelf_InsertBefore() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose1 = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + ProjectChooseElement choose2 = project.CreateChooseElement(); + project.AppendChild(choose1); + choose1.AppendChild(when); + when.PrependChild(choose2); + when.InsertBeforeChild(choose1, choose2); + } + + /// + /// Become grandchild of self with InsertAfterChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidBecomeGrandChildOfSelf_InsertAfter() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose1 = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + ProjectChooseElement choose2 = project.CreateChooseElement(); + project.AppendChild(choose1); + choose1.AppendChild(when); + when.PrependChild(choose2); + when.InsertAfterChild(choose1, choose2); + } + + /// + /// Attempt to reparent with AppendChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAlreadyParented_AppendChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + + project.AppendChild(target); + } + + /// + /// Attempt to reparent with PrependChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAlreadyParented_PrependChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + + project.PrependChild(target); + } + + /// + /// Attempt to reparent with InsertBeforeChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAlreadyParented_InsertBefore() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.InsertBeforeChild(target1, target2); + } + + /// + /// Attempt to reparent with InsertAfterChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAlreadyParented_InsertAfter() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.InsertAfterChild(target1, target2); + } + + /// + /// Attempt to add to unparented parent with AppendChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidParentNotParented_AppendChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectTaskElement task = project.CreateTaskElement("tt"); + + target.AppendChild(task); + } + + /// + /// Attempt to add to unparented parent with PrependChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidParentNotParented_PrependChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectTaskElement task = project.CreateTaskElement("tt"); + + target.PrependChild(task); + } + + /// + /// Attempt to add to unparented parent with InsertBeforeChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidParentNotParented_InsertBefore() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectTaskElement task1 = project.CreateTaskElement("tt"); + ProjectTaskElement task2 = project.CreateTaskElement("tt"); + + target.InsertBeforeChild(task2, task1); + } + + /// + /// Attempt to add to unparented parent with InsertAfterChild + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidParentNotParented_InsertAfter() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectTaskElement task1 = project.CreateTaskElement("tt"); + ProjectTaskElement task2 = project.CreateTaskElement("tt"); + + target.InsertAfterChild(task2, task1); + } + + /// + /// Setting attributes on a target should be reflected in the XML + /// + [TestMethod] + public void AppendTargetSetAllAttributes() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + project.AppendChild(target); + target.Inputs = "i"; + target.Outputs = "o"; + target.DependsOnTargets = "d"; + target.Condition = "c"; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Clearing attributes on a target should be reflected in the XML + /// + [TestMethod] + public void AppendTargetClearAttributes() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + project.AppendChild(target); + target.Inputs = "i"; + target.Outputs = "o"; + target.Inputs = String.Empty; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Prepend item group + /// + [TestMethod] + public void PrependItemGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + + project.PrependChild(itemGroup); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + + Assert.AreEqual(1, project.Count); + var children = Helpers.MakeList(project.Children); + Assert.AreEqual(1, children.Count); + Assert.AreEqual(itemGroup, children[0]); + } + + /// + /// Insert target before + /// + [TestMethod] + public void InsertTargetBefore() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + project.PrependChild(itemGroup); + project.InsertBeforeChild(target, itemGroup); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + + Assert.AreEqual(2, project.Count); + var children = Helpers.MakeList(project.Children); + Assert.AreEqual(2, children.Count); + Assert.AreEqual(target, children[0]); + Assert.AreEqual(itemGroup, children[1]); + } + + /// + /// InsertBeforeChild with a null reference node should be the same as calling AppendChild. + /// This matches XmlNode behavior. + /// + [TestMethod] + public void InsertTargetBeforeNullEquivalentToAppendChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + project.PrependChild(itemGroup); + project.InsertBeforeChild(target, null); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// InsertAfterChild with a null reference node should be the same as calling PrependChild. + /// This matches XmlNode behavior. + /// + [TestMethod] + public void InsertTargetAfterNullEquivalentToPrependChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + project.PrependChild(itemGroup); + project.InsertAfterChild(target, null); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Insert target before and after a reference + /// + [TestMethod] + public void InsertTargetBeforeAndTargetAfter() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectTargetElement target1 = project.CreateTargetElement("t"); + ProjectTargetElement target2 = project.CreateTargetElement("t2"); + + project.PrependChild(itemGroup); + project.InsertBeforeChild(target1, itemGroup); + project.InsertAfterChild(target2, itemGroup); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + + Assert.AreEqual(3, project.Count); + var children = Helpers.MakeList(project.Children); + Assert.AreEqual(3, children.Count); + Assert.AreEqual(target1, children[0]); + Assert.AreEqual(itemGroup, children[1]); + Assert.AreEqual(target2, children[2]); + } + + /// + /// Insert before when no children + /// + [TestMethod] + public void InsertTargetBeforeNothing() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.CreateTargetElement("t"); + + project.InsertBeforeChild(target1, null); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(1, project.Count); + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Insert after when no children + /// + [TestMethod] + public void InsertTargetAfterNothing() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + project.InsertAfterChild(target, null); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(1, project.Count); + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Insert task in target + /// + [TestMethod] + public void InsertTaskInTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectTaskElement task = project.CreateTaskElement("tt"); + + project.AppendChild(target); + target.AppendChild(task); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add a task through the convenience method + /// + [TestMethod] + public void AddTaskConvenience() + { + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectTargetElement target = project.AddTarget("t"); + ProjectTaskElement task = target.AddTask("tt"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Attempt to insert project in target + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAttemptToAddProjectToTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + + target.AppendChild(project); + } + + /// + /// Attempt to insert item in target + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAttemptToAddItemToTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectItemElement item = project.CreateItemElement("i"); + + project.AppendChild(target); + target.AppendChild(item); + } + + /// + /// Attempt to insert item without include in itemgroup in project + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAttemptToAddItemWithoutIncludeToItemGroupInProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectItemElement item = project.CreateItemElement("i"); + + project.AppendChild(itemGroup); + itemGroup.AppendChild(item); + } + + /// + /// Attempt to insert item with remove in itemgroup in project + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAttemptToAddItemWithRemoveToItemGroupInProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectItemElement item = project.CreateItemElement("i"); + item.Remove = "r"; + + project.AppendChild(itemGroup); + itemGroup.AppendChild(item); + } + + /// + /// Add item without include in itemgroup in target + /// + [TestMethod] + public void AddItemWithoutIncludeToItemGroupInTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectItemElement item = project.CreateItemElement("i"); + + project.AppendChild(target); + target.AppendChild(itemGroup); + itemGroup.AppendChild(item); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add item with remove in itemgroup in target + /// + [TestMethod] + public void AddItemWithRemoveToItemGroupInTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + ProjectItemGroupElement itemGroup = project.CreateItemGroupElement(); + ProjectItemElement item = project.CreateItemElement("i"); + item.Remove = "r"; + + project.AppendChild(target); + target.AppendChild(itemGroup); + itemGroup.AppendChild(item); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Remove a target + /// + [TestMethod] + public void RemoveSingleChildTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + project.RemoveChild(target); + + string expected = ObjectModelHelpers.CleanupFileContents(@""); + + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(0, Helpers.Count(project.Children)); + } + + /// + /// Attempt to remove a child that is not parented + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void InvalidRemoveUnparentedChild() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.CreateTargetElement("t"); + project.RemoveChild(target); + } + + /// + /// Attempt to remove a child that is parented by something in another project + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void InvalidRemoveChildFromOtherProject() + { + ProjectRootElement project1 = ProjectRootElement.Create(); + ProjectTargetElement target = project1.CreateTargetElement("t"); + ProjectRootElement project2 = ProjectRootElement.Create(); + + project2.RemoveChild(target); + } + + /// + /// Attempt to remove a child that is parented by something else in the same project + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidRemoveChildFromOtherParent() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup1 = project.CreateItemGroupElement(); + ProjectItemGroupElement itemGroup2 = project.CreateItemGroupElement(); + ProjectItemElement item = project.CreateItemElement("i"); + itemGroup1.AppendChild(item); + + itemGroup2.RemoveChild(item); + } + + /// + /// Attempt to add an Otherwise before a When + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidOtherwiseBeforeWhen() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + ProjectOtherwiseElement otherwise = project.CreateOtherwiseElement(); + + project.AppendChild(choose); + choose.AppendChild(when); + choose.InsertBeforeChild(otherwise, when); + } + + /// + /// Attempt to add an Otherwise after another + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidOtherwiseAfterOtherwise() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + project.AppendChild(choose); + choose.AppendChild(project.CreateWhenElement("c")); + choose.AppendChild(project.CreateOtherwiseElement()); + choose.AppendChild(project.CreateOtherwiseElement()); + } + + /// + /// Attempt to add an Otherwise before another + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidOtherwiseBeforeOtherwise() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + project.AppendChild(choose); + choose.AppendChild(project.CreateWhenElement("c")); + choose.AppendChild(project.CreateOtherwiseElement()); + choose.InsertAfterChild(project.CreateOtherwiseElement(), choose.FirstChild); + } + + /// + /// Attempt to add a When after an Otherwise + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidWhenAfterOtherwise() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + ProjectOtherwiseElement otherwise = project.CreateOtherwiseElement(); + + project.AppendChild(choose); + choose.AppendChild(otherwise); + choose.InsertAfterChild(when, otherwise); + } + + /// + /// Add When before Otherwise + /// + [TestMethod] + public void WhenBeforeOtherwise() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + ProjectOtherwiseElement otherwise = project.CreateOtherwiseElement(); + + project.AppendChild(choose); + choose.AppendChild(when); + choose.AppendChild(otherwise); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(1, Helpers.Count(project.Children)); + Assert.AreEqual(2, Helpers.Count(choose.Children)); + } + + /// + /// Remove a target that is last in a list + /// + [TestMethod] + public void RemoveLastInList() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.RemoveChild(target2); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(1, project.Count); + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(1, Helpers.Count(project.Children)); + Assert.AreEqual(target1, Helpers.GetFirst(project.Children)); + } + + /// + /// Remove a target that is first in a list + /// + [TestMethod] + public void RemoveFirstInList() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.RemoveChild(target1); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(1, project.Count); + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(1, Helpers.Count(project.Children)); + Assert.AreEqual(target2, Helpers.GetFirst(project.Children)); + } + + /// + /// Remove all children when there are some + /// + [TestMethod] + public void RemoveAllChildrenSome() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.RemoveAllChildren(); + + Assert.AreEqual(0, project.Count); + Assert.AreEqual(null, target1.Parent); + Assert.AreEqual(null, target2.Parent); + } + + /// + /// Remove all children when there aren't any. Shouldn't fail. + /// + [TestMethod] + public void RemoveAllChildrenNone() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + + target1.RemoveAllChildren(); + + Assert.AreEqual(0, target1.Count); + } + + /// + /// Remove and re-insert a node + /// + [TestMethod] + public void RemoveReinsertHasSiblingAppend() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.RemoveChild(target1); + project.AppendChild(target1); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Assert.AreEqual(2, project.Count); + Assert.AreEqual(true, project.HasUnsavedChanges); + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(2, Helpers.Count(project.Children)); + Assert.AreEqual(target2, Helpers.GetFirst(project.Children)); + } + + /// + /// Remove and re-insert a node + /// + [TestMethod] + public void RemoveReinsertHasSiblingPrepend() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.RemoveChild(target1); + project.PrependChild(target1); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Assert.AreEqual(2, project.Count); + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Remove and re-insert a node + /// + [TestMethod] + public void RemoveReinsertTwoChildrenAppend() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + + project.RemoveAllChildren(); + project.AppendChild(target1); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Assert.AreEqual(1, project.Count); + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Remove and re-insert a node with no siblings using PrependChild + /// + [TestMethod] + public void RemoveLonelyReinsertPrepend() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + + project.RemoveChild(target1); + project.PrependChild(target1); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Remove and re-insert a node with no siblings using AppendChild + /// + [TestMethod] + public void RemoveLonelyReinsertAppend() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + + project.RemoveAllChildren(); + project.AppendChild(target1); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Test the AddPropertyGroup convenience method + /// It adds after the last existing property group, if any; otherwise + /// at the start of the project. + /// + [TestMethod] + public void AddPropertyGroup_NoExistingPropertyGroups() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddTarget("t1"); + project.AddTarget("t2"); + + ProjectPropertyGroupElement propertyGroup = project.AddPropertyGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(3, Helpers.Count(project.Children)); + Assert.AreEqual(propertyGroup, Helpers.GetFirst(project.Children)); + } + + /// + /// Test the AddPropertyGroup convenience method + /// It adds after the last existing property group, if any; otherwise + /// at the start of the project. + /// + [TestMethod] + public void AddPropertyGroup_ExistingPropertyGroups() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target1 = project.AddTarget("t1"); + ProjectTargetElement target2 = project.AddTarget("t2"); + ProjectPropertyGroupElement propertyGroup1 = project.CreatePropertyGroupElement(); + ProjectPropertyGroupElement propertyGroup2 = project.CreatePropertyGroupElement(); + + project.InsertAfterChild(propertyGroup1, target1); + project.InsertAfterChild(propertyGroup2, target2); + + ProjectPropertyGroupElement propertyGroup3 = project.AddPropertyGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(5, Helpers.Count(project.Children)); + Assert.AreEqual(propertyGroup3, Helpers.GetLast(project.Children)); + } + + /// + /// Add an item group to an empty project + /// + [TestMethod] + public void AddItemGroup_NoExistingElements() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item group to a project with an existing item group; should add 2nd + /// + [TestMethod] + public void AddItemGroup_OneExistingItemGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + ProjectItemGroupElement itemGroup2 = project.AddItemGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(itemGroup2, Helpers.GetLast(project.ItemGroups)); + } + + /// + /// Add an item group to a project with an existing property group; should add 2nd + /// + [TestMethod] + public void AddItemGroup_OneExistingPropertyGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddPropertyGroup(); + project.AddItemGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item group to a project with an existing property group and item group; + /// should add after the item group + /// + [TestMethod] + public void AddItemGroup_ExistingItemGroupAndPropertyGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + project.AppendChild(project.CreatePropertyGroupElement()); + ProjectItemGroupElement itemGroup2 = project.AddItemGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(itemGroup2, Helpers.GetLast(project.ItemGroups)); + } + + /// + /// Add an item group to a project with an existing target; + /// should add at the end + /// + [TestMethod] + public void AddItemGroup_ExistingTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddTarget("t"); + project.AddItemGroup(); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to an empty project + /// should add to new item group + /// + [TestMethod] + public void AddItem_EmptyProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemElement item = project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project that only has an empty item group, + /// should reuse that group + /// + [TestMethod] + public void AddItem_ExistingEmptyItemGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + ProjectItemElement item = project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project that only has an empty item group, + /// should reuse that group, unless it has a condition + /// + [TestMethod] + public void AddItem_ExistingEmptyItemGroupWithCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.AddItemGroup(); + itemGroup.Condition = "c"; + ProjectItemElement item = project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project that only has an item group with items of a different type, + /// and an empty item group, should reuse that group + /// + [TestMethod] + public void AddItem_ExistingEmptyItemGroupPlusItemGroupOfWrongType() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.AddItemGroup(); + itemGroup.AddItem("h", "h1"); + project.AddItemGroup(); + ProjectItemElement item = project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project that only has an item group with items of a different type, + /// and an empty item group above it, should reuse the empty group + /// + [TestMethod] + public void AddItem_ExistingEmptyItemGroupPlusItemGroupOfWrongTypeBelow() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + ProjectItemGroupElement itemGroup = project.AddItemGroup(); + itemGroup.AddItem("h", "h1"); + ProjectItemElement item = project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(item, Helpers.GetFirst(Helpers.GetFirst(project.ItemGroups).Items)); + } + + /// + /// Add an item to a project with a single item group with existing items + /// of a different item type; should add in alpha order of item type + /// + [TestMethod] + public void AddItem_ExistingItemGroupWithItemsOfDifferentItemType() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "i1"); + project.AddItem("j", "j1"); + project.AddItem("h", "h1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project with a single item group with existing items of + /// same item type; should add in alpha order of itemspec + /// + [TestMethod] + public void AddItem_ExistingItemGroupWithItemsOfSameItemType() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "i1"); + project.AddItem("i", "j1"); + project.AddItem("i", "h1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project with an existing item group with items of a different + /// type; should create a new item group + /// + [TestMethod] + public void AddItem_ExistingItemGroupWithDifferentItemType() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "i1"); + project.AddItem("j", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item to a project with a single item group with existing items of + /// various item types and item specs; should add in alpha order of item type, + /// then item spec, keeping different item specs in different groups; different + /// item groups are not mutally sorted + /// + [TestMethod] + public void AddItem_ExistingItemGroupWithVariousItems() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "i1"); + project.AddItem("i", "j1"); + project.AddItem("j", "h1"); + project.AddItem("i", "h1"); + project.AddItem("h", "j1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Adding an item that's identical to an existing one should add it again and not skip + /// + [TestMethod] + public void AddItem_Duplicate() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "i1"); + project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Adding items to when and Otherwise + /// + [TestMethod] + public void AddItemToWhereOtherwise() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + ProjectItemGroupElement ig1 = project.CreateItemGroupElement(); + project.AppendChild(choose); + choose.AppendChild(when); + when.AppendChild(ig1); + ig1.AddItem("j", "j1"); + + ProjectOtherwiseElement otherwise = project.CreateOtherwiseElement(); + ProjectItemGroupElement ig2 = project.CreateItemGroupElement(); + choose.AppendChild(otherwise); + otherwise.AppendChild(ig2); + ig2.AddItem("j", "j2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Adding items to a specific item group should order them by item type and item spec + /// + [TestMethod] + public void AddItemToItemGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemGroupElement itemGroup = project.AddItemGroup(); + itemGroup.AddItem("j", "j1"); + itemGroup.AddItem("i", "i1"); + itemGroup.AddItem("h", "h1"); + itemGroup.AddItem("j", "j2"); + itemGroup.AddItem("j", "j0"); + itemGroup.AddItem("h", "h0"); + itemGroup.AddItem("g", "zzz"); + itemGroup.AddItem("k", "aaa"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item definition to an empty project + /// should add to new item definition group + /// + [TestMethod] + public void AddItemDefinition_EmptyProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemDefinitionElement itemDefinition = project.AddItemDefinition("i"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(itemDefinition, Helpers.GetFirst(Helpers.GetFirst(project.ItemDefinitionGroups).ItemDefinitions)); + } + + /// + /// Add an item definition to a project with a single empty item definition group; + /// should create another, because it doesn't have any items of the same type + /// + [TestMethod] + public void AddItemDefinition_ExistingItemDefinitionGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemDefinitionGroup(); + project.AddItemDefinition("i"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item definition to a project with a single empty item definition group with a condition; + /// should create a new one after + /// + [TestMethod] + public void AddItemDefinition_ExistingItemDefinitionGroupWithCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemDefinitionGroupElement itemGroup = project.AddItemDefinitionGroup(); + itemGroup.Condition = "c"; + project.AddItemDefinition("i"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item definition to a project with a single item definitiongroup with existing items of + /// same item type; should add in same one + /// + [TestMethod] + public void AddItemDefinition_ExistingItemDefinitionGroupWithItemsOfSameItemType() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemDefinition("i"); + project.AddItemDefinition("i"); + ProjectItemDefinitionElement last = project.AddItemDefinition("i"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(last, Helpers.GetLast(Helpers.GetFirst(project.ItemDefinitionGroups).ItemDefinitions)); + } + + /// + /// Add an item definition to a project with an existing item definition group with items of a different + /// type; should create a new item definition group + /// + [TestMethod] + public void AddItemDefinition_ExistingItemDefinitionGroupWithDifferentItemType() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemDefinition("i"); + project.AddItemDefinition("j"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add a property to an empty project + /// should add to new property group + /// + [TestMethod] + public void AddProperty_EmptyProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyElement property = project.AddProperty("p", "v1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v1

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(property, Helpers.GetFirst(Helpers.GetFirst(project.PropertyGroups).Properties)); + } + + /// + /// Add a property to a project with an existing property group + /// should add to property group + /// + [TestMethod] + public void AddProperty_ExistingPropertyGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddPropertyGroup(); + ProjectPropertyElement property = project.AddProperty("p", "v1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v1

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add a property to a project with an existing property group with condition + /// should add to new property group + /// + [TestMethod] + public void AddProperty_ExistingPropertyGroupWithCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyGroupElement propertyGroup = project.AddPropertyGroup(); + propertyGroup.Condition = "c"; + + ProjectPropertyElement property = project.AddProperty("p", "v1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + +

v1

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add a property to a project with an existing property with the same name + /// should modify and return existing property + /// + [TestMethod] + public void AddProperty_ExistingPropertySameName() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyElement property1 = project.AddProperty("p", "v1"); + + ProjectPropertyElement property2 = project.AddProperty("p", "v2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v2

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(true, Object.ReferenceEquals(property1, property2)); + } + + /// + /// Add a property to a project with an existing property with the same name but a condition; + /// should add new property + /// + [TestMethod] + public void AddProperty_ExistingPropertySameNameCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyElement property1 = project.AddProperty("p", "v1"); + property1.Condition = "c"; + + ProjectPropertyElement property2 = project.AddProperty("p", "v2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v1

+

v2

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add a property to a project with an existing property with the same name but a condition; + /// should add new property + /// + [TestMethod] + public void AddProperty_ExistingPropertySameNameConditionOnGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyElement property1 = project.AddProperty("p", "v1"); + property1.Parent.Condition = "c"; + + ProjectPropertyElement property2 = project.AddProperty("p", "v2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v1

+
+ +

v2

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Attempt to add a property with a reserved name + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAddPropertyReservedName() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddProperty("MSBuildToolsPATH", "v"); + } + + /// + /// Attempt to add a property with an illegal name + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidAddPropertyIllegalName() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddProperty("ItemGroup", "v"); + } + + /// + /// Attempt to add a property with an invalid XML name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void InvalidAddPropertyInvalidXmlName() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddProperty("@#$@#", "v"); + } + + /// + /// Too much nesting should not cause stack overflow. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidChooseOverflow() + { + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectElementContainer current = project; + while (true) + { + ProjectChooseElement choose = project.CreateChooseElement(); + ProjectWhenElement when = project.CreateWhenElement("c"); + current.AppendChild(choose); + choose.AppendChild(when); + current = when; + } + } + + /// + /// Setting item condition should dirty project + /// + [TestMethod] + public void Dirtying_ItemCondition() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + +"))); + + Project project = new Project(content); + ProjectItem item = Helpers.GetFirst(project.Items); + + item.Xml.Condition = "false"; + + Assert.AreEqual(1, Helpers.Count(project.Items)); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual(0, Helpers.Count(project.Items)); + } + + /// + /// Setting metadata condition should dirty project + /// + [TestMethod] + public void Dirtying_MetadataCondition() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"))); + + Project project = new Project(content); + ProjectMetadata metadatum = Helpers.GetFirst(project.Items).GetMetadata("m"); + + metadatum.Xml.Condition = "false"; + + Assert.AreEqual("m1", metadatum.EvaluatedValue); + + project.ReevaluateIfNecessary(); + metadatum = Helpers.GetFirst(project.Items).GetMetadata("m"); + + Assert.AreEqual(null, metadatum); + } + + /// + /// Delete all the children of a container, then add them + /// to a new one, and iterate. Should not go into infinite loop :-) + /// + [TestMethod] + public void DeleteAllChildren() + { + ProjectRootElement xml = ProjectRootElement.Create(); + ProjectItemGroupElement group1 = xml.AddItemGroup(); + ProjectItemElement item1 = group1.AddItem("i", "i1"); + ProjectItemElement item2 = group1.AddItem("i", "i2"); + group1.RemoveChild(item1); + group1.RemoveChild(item2); + + ProjectItemGroupElement group2 = xml.AddItemGroup(); + group2.AppendChild(item1); + group2.AppendChild(item2); + + List allChildren = new List(group2.AllChildren); + + Helpers.AssertListsValueEqual(allChildren, new List { item1, item2 }); + Assert.AreEqual(0, group1.Count); + } + + /// + /// Same but with Prepend for the 2nd one + /// + [TestMethod] + public void DeleteAllChildren2() + { + ProjectRootElement xml = ProjectRootElement.Create(); + ProjectItemGroupElement group1 = xml.AddItemGroup(); + ProjectItemElement item1 = group1.AddItem("i", "i1"); + ProjectItemElement item2 = group1.AddItem("i", "i2"); + group1.RemoveChild(item1); + group1.RemoveChild(item2); + + ProjectItemGroupElement group2 = xml.AddItemGroup(); + group2.AppendChild(item1); + group2.PrependChild(item2); + + List allChildren = new List(group2.AllChildren); + + Helpers.AssertListsValueEqual(allChildren, new List { item2, item1 }); + Assert.AreEqual(0, group1.Count); + } + + /// + /// Same but with InsertBefore for the 2nd one + /// + [TestMethod] + public void DeleteAllChildren3() + { + ProjectRootElement xml = ProjectRootElement.Create(); + ProjectItemGroupElement group1 = xml.AddItemGroup(); + ProjectItemElement item1 = group1.AddItem("i", "i1"); + ProjectItemElement item2 = group1.AddItem("i", "i2"); + group1.RemoveChild(item1); + group1.RemoveChild(item2); + + ProjectItemGroupElement group2 = xml.AddItemGroup(); + group2.AppendChild(item1); + group2.InsertBeforeChild(item2, item1); + + List allChildren = new List(group2.AllChildren); + + Helpers.AssertListsValueEqual(allChildren, new List { item2, item1 }); + Assert.AreEqual(0, group1.Count); + } + + /// + /// Same but with InsertAfter for the 2nd one + /// + [TestMethod] + public void DeleteAllChildren4() + { + ProjectRootElement xml = ProjectRootElement.Create(); + ProjectItemGroupElement group1 = xml.AddItemGroup(); + ProjectItemElement item1 = group1.AddItem("i", "i1"); + ProjectItemElement item2 = group1.AddItem("i", "i2"); + group1.RemoveChild(item1); + group1.RemoveChild(item2); + + ProjectItemGroupElement group2 = xml.AddItemGroup(); + group2.AppendChild(item1); + group2.InsertAfterChild(item2, item1); + + List allChildren = new List(group2.AllChildren); + + Helpers.AssertListsValueEqual(allChildren, new List { item1, item2 }); + Assert.AreEqual(0, group1.Count); + } + + /// + /// Same but with InsertAfter for the 2nd one + /// + [TestMethod] + public void DeleteAllChildren5() + { + ProjectRootElement xml = ProjectRootElement.Create(); + ProjectItemGroupElement group1 = xml.AddItemGroup(); + ProjectItemElement item1 = group1.AddItem("i", "i1"); + ProjectItemElement item2 = group1.AddItem("i", "i2"); + group1.RemoveChild(item1); + group1.RemoveChild(item2); + + ProjectItemGroupElement group2 = xml.AddItemGroup(); + group2.AppendChild(item1); + group2.InsertAfterChild(item2, item1); + + List allChildren = new List(group2.AllChildren); + + Helpers.AssertListsValueEqual(allChildren, new List { item1, item2 }); + Assert.AreEqual(0, group1.Count); + } + + /// + /// Move some children + /// + [TestMethod] + public void DeleteSomeChildren() + { + ProjectRootElement xml = ProjectRootElement.Create(); + ProjectItemGroupElement group1 = xml.AddItemGroup(); + ProjectItemElement item1 = group1.AddItem("i", "i1"); + ProjectItemElement item2 = group1.AddItem("i", "i2"); + ProjectItemElement item3 = group1.AddItem("i", "i3"); + group1.RemoveChild(item1); + group1.RemoveChild(item2); + + ProjectItemGroupElement group2 = xml.AddItemGroup(); + group2.AppendChild(item1); + group2.AppendChild(item2); + + List allChildren = new List(group2.AllChildren); + + Helpers.AssertListsValueEqual(allChildren, new List { item1, item2 }); + Assert.AreEqual(1, group1.Count); + Assert.AreEqual(true, item3.PreviousSibling == null && item3.NextSibling == null); + Assert.AreEqual(true, item2.PreviousSibling == item1 && item1.NextSibling == item2); + Assert.AreEqual(true, item1.PreviousSibling == null && item2.NextSibling == null); + } + + /// + /// Attempt to modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_1() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectImportElement import = project.AddImport("p"); + import.Parent.RemoveAllChildren(); + import.Condition = "c"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_2() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectImportElement import = project.AddImport("p"); + import.Parent.RemoveAllChildren(); + import.Project = "p"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_3() + { + ProjectRootElement.Create().CreateImportGroupElement().Condition = "c"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_4() + { + var element = ProjectRootElement.Create().AddItemDefinition("i").AddMetadata("m", "M1"); + element.Parent.RemoveAllChildren(); + element.Value = "v1"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_5() + { + var element = ProjectRootElement.Create().AddItem("i", "i1").AddMetadata("m", "M1"); + element.Parent.RemoveAllChildren(); + element.Value = "v1"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_5b() + { + var element = ProjectRootElement.Create().AddItem("i", "i1"); + element.Parent.RemoveAllChildren(); + element.ItemType = "j"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_6() + { + var element = ProjectRootElement.Create().AddItem("i", "i1"); + element.Parent.RemoveAllChildren(); + element.Include = "i2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_7() + { + var element = ProjectRootElement.Create().AddProperty("p", "v1"); + element.Parent.RemoveAllChildren(); + element.Value = "v2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_8() + { + var element = ProjectRootElement.Create().AddProperty("p", "v1"); + element.Parent.RemoveAllChildren(); + element.Condition = "c"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_9() + { + var element = ProjectRootElement.Create().AddUsingTask("n", "af", null); + element.Parent.RemoveAllChildren(); + element.TaskName = "n2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_10() + { + var element = ProjectRootElement.Create().AddUsingTask("n", "af", null); + element.Parent.RemoveAllChildren(); + element.AssemblyFile = "af2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_11() + { + var element = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + element.Parent.RemoveAllChildren(); + element.AssemblyName = "an2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_12() + { + var element = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + element.Parent.RemoveAllChildren(); + element.TaskFactory = "tf"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_15() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.RemoveAllChildren(); + element.Name = "n2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_16() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.RemoveAllChildren(); + element.Output = "o2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_17() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.RemoveAllChildren(); + element.Required = "r2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_18() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.RemoveAllChildren(); + element.ParameterType = "pt2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_19() + { + var element = ProjectRootElement.Create().AddTarget("t"); + element.Parent.RemoveAllChildren(); + element.Name = "t2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_20() + { + var element = ProjectRootElement.Create().AddTarget("t"); + element.Parent.RemoveAllChildren(); + element.Inputs = "i"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_21() + { + var element = ProjectRootElement.Create().AddTarget("t"); + element.Parent.RemoveAllChildren(); + element.Outputs = "o"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_22() + { + var element = ProjectRootElement.Create().AddTarget("t"); + element.Parent.RemoveAllChildren(); + element.DependsOnTargets = "d"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_23() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt"); + element.Parent.RemoveAllChildren(); + element.SetParameter("p", "v"); + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_24() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt"); + element.Parent.RemoveAllChildren(); + element.ContinueOnError = "coe"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_25() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputItem("tp", "i"); + element.Parent.RemoveAllChildren(); + element.TaskParameter = "tp2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_26() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputItem("tp", "i"); + element.Parent.RemoveAllChildren(); + element.ItemType = "tp2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_27() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputProperty("tp", "p"); + element.Parent.RemoveAllChildren(); + element.TaskParameter = "tp2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_28() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputProperty("tp", "p"); + element.Parent.RemoveAllChildren(); + element.PropertyName = "tp2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_29() + { + var element = ProjectRootElement.Create().AddTarget("t").AddItemGroup().AddItem("i", "i1"); + element.Parent.RemoveAllChildren(); + element.ItemType = "j"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_30() + { + var element = ProjectRootElement.Create().AddTarget("t").AddItemGroup().AddItem("i", "i1"); + element.Parent.RemoveAllChildren(); + element.Include = "i2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_31() + { + var element = ProjectRootElement.Create().AddTarget("t").AddItemGroup().AddItem("i", "i1").AddMetadata("m", "m1"); + element.Parent.RemoveAllChildren(); + element.Value = "m2"; + } + + /// + /// Legally modify a child that is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedChild_32() + { + var element = ProjectRootElement.Create().AddTarget("t").AddPropertyGroup().AddProperty("p", "v1"); + element.Parent.RemoveAllChildren(); + element.Value = "v2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_1() + { + var element = ProjectRootElement.Create().AddImportGroup().AddImport("p"); + element.Parent.Parent.RemoveAllChildren(); + element.Condition = "c"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_2() + { + var element = ProjectRootElement.Create().AddImportGroup().AddImport("p"); + element.Parent.Parent.RemoveAllChildren(); + element.Project = "p"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_3() + { + ProjectRootElement.Create().CreateImportGroupElement().Condition = "c"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_4() + { + var element = ProjectRootElement.Create().AddItemDefinition("i").AddMetadata("m", "M1"); + element.Parent.Parent.RemoveAllChildren(); + element.Value = "v1"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_5() + { + var element = ProjectRootElement.Create().AddItem("i", "i1").AddMetadata("m", "M1"); + element.Parent.Parent.RemoveAllChildren(); + element.Value = "v1"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_5b() + { + var element = ProjectRootElement.Create().AddItem("i", "i1"); + element.Parent.Parent.RemoveAllChildren(); + element.ItemType = "j"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_6() + { + var element = ProjectRootElement.Create().AddItem("i", "i1"); + element.Parent.Parent.RemoveAllChildren(); + element.Include = "i2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_7() + { + var element = ProjectRootElement.Create().AddProperty("p", "v1"); + element.Parent.Parent.RemoveAllChildren(); + element.Value = "v2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_8() + { + var element = ProjectRootElement.Create().AddProperty("p", "v1"); + element.Parent.Parent.RemoveAllChildren(); + element.Condition = "c"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_15() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.Parent.RemoveAllChildren(); + element.Name = "n2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_16() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.Parent.RemoveAllChildren(); + element.Output = "o2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_17() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.Parent.RemoveAllChildren(); + element.Required = "r2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_18() + { + var usingTask = ProjectRootElement.Create().AddUsingTask("n", null, "an"); + usingTask.TaskFactory = "f"; + var element = usingTask.AddParameterGroup().AddParameter("n", "o", "r", "pt"); + element.Parent.Parent.RemoveAllChildren(); + element.ParameterType = "pt2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_23() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt"); + element.Parent.Parent.RemoveAllChildren(); + element.SetParameter("p", "v"); + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_24() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt"); + element.Parent.Parent.RemoveAllChildren(); + element.ContinueOnError = "coe"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_25() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputItem("tp", "i"); + element.Parent.Parent.RemoveAllChildren(); + element.TaskParameter = "tp2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_26() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputItem("tp", "i"); + element.Parent.Parent.RemoveAllChildren(); + element.ItemType = "tp2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_27() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputProperty("tp", "p"); + element.Parent.Parent.RemoveAllChildren(); + element.TaskParameter = "tp2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_28() + { + var element = ProjectRootElement.Create().AddTarget("t").AddTask("tt").AddOutputProperty("tp", "p"); + element.Parent.Parent.RemoveAllChildren(); + element.PropertyName = "tp2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_29() + { + var element = ProjectRootElement.Create().AddTarget("t").AddItemGroup().AddItem("i", "i1"); + element.Parent.Parent.RemoveAllChildren(); + element.ItemType = "j"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_30() + { + var element = ProjectRootElement.Create().AddTarget("t").AddItemGroup().AddItem("i", "i1"); + element.Parent.Parent.RemoveAllChildren(); + element.Include = "i2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_31() + { + var element = ProjectRootElement.Create().AddTarget("t").AddItemGroup().AddItem("i", "i1").AddMetadata("m", "m1"); + element.Parent.Parent.RemoveAllChildren(); + element.Value = "m2"; + } + + /// + /// Legally modify a child whose parent is not parented (should not throw) + /// + [TestMethod] + public void ModifyUnparentedParentChild_32() + { + var element = ProjectRootElement.Create().AddTarget("t").AddPropertyGroup().AddProperty("p", "v1"); + element.Parent.Parent.RemoveAllChildren(); + element.Value = "v2"; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ElementLocationPublic_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ElementLocationPublic_Tests.cs new file mode 100644 index 00000000000..e85aa0c1b02 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ElementLocationPublic_Tests.cs @@ -0,0 +1,214 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ElementLocation class +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Build.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Framework; +using System.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; +using System.Xml; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Build.UnitTests.Construction +{ + /// + /// Tests for the ElementLocation class + /// + [TestClass] + public class ElementLocationPublic_Tests + { + /// + /// Check that we can get the file name off an element and attribute, even if + /// it wouldn't normally have got one because the project wasn't + /// loaded from disk, or has been edited since. + /// This is really a test of our XmlDocumentWithLocation. + /// + [TestMethod] + public void ShouldHaveFilePathLocationEvenIfNotLoadedNorSavedYet() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.FullPath = "c:\\x"; + ProjectTargetElement target = project.CreateTargetElement("t"); + target.Outputs = "o"; + project.AppendChild(target); + + Assert.AreEqual(project.FullPath, target.Location.File); + Assert.AreEqual(project.FullPath, target.OutputsLocation.File); + } + + /// + /// Element location should reflect rename. + /// This is really a test of our XmlXXXXWithLocation. + /// + [TestMethod] + public void XmlLocationReflectsRename() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.FullPath = "c:\\x"; + ProjectTargetElement target = project.CreateTargetElement("t"); + target.Outputs = "o"; + project.AppendChild(target); + + Assert.AreEqual(project.FullPath, target.Location.File); + Assert.AreEqual(project.FullPath, target.OutputsLocation.File); + + project.FullPath = "c:\\y"; + + Assert.AreEqual(project.FullPath, target.Location.File); + Assert.AreEqual(project.FullPath, target.OutputsLocation.File); + } + + /// + /// We should cache ElementLocation objects for perf. + /// + [TestMethod] + public void XmlLocationsAreCached() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.FullPath = "c:\\x"; + ProjectTargetElement target = project.CreateTargetElement("t"); + target.Outputs = "o"; + project.AppendChild(target); + + ElementLocation e1 = target.Location; + ElementLocation e2 = target.OutputsLocation; + + Assert.AreEqual(true, Object.ReferenceEquals(e1, target.Location)); + Assert.AreEqual(true, Object.ReferenceEquals(e2, target.OutputsLocation)); + } + + /// + /// Test many of the getters + /// + [TestMethod] + public void LocationStringsMedley() + { + string content = @" + + + + + + + + + + + + +

+ + + + + + + + + +

+ + + + + + "; + + var project = ObjectModelHelpers.CreateInMemoryProject(content); + + string locations = project.Xml.Location.LocationString + "\r\n"; + + foreach (var element in project.Xml.AllChildren) + { + foreach (var property in element.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.Name.Contains("Location")) + { + if (property.Name == "ParameterLocations") + { + var values = new List>(((ICollection>)property.GetValue(element, null))); + + values.ForEach((value) => locations += value.Key + ":" + value.Value.LocationString + "\r\n"); + } + else + { + var value = ((ElementLocation)property.GetValue(element, null)); + + if (value != null) // null means attribute is not present + { + locations += value.LocationString + "\r\n"; + } + } + } + } + } + + locations = locations.Replace(project.FullPath, "c:\\foo\\bar.csproj"); + + string expected = @"c:\foo\bar.csproj (2,13) +c:\foo\bar.csproj (3,32) +c:\foo\bar.csproj (3,45) +c:\foo\bar.csproj (3,62) +c:\foo\bar.csproj (3,21) +c:\foo\bar.csproj (4,32) +c:\foo\bar.csproj (4,45) +c:\foo\bar.csproj (4,62) +c:\foo\bar.csproj (4,21) +c:\foo\bar.csproj (5,42) +c:\foo\bar.csproj (5,59) +c:\foo\bar.csproj (5,21) +c:\foo\bar.csproj (6,28) +c:\foo\bar.csproj (6,25) +c:\foo\bar.csproj (8,21) +c:\foo\bar.csproj (9,28) +c:\foo\bar.csproj (9,57) +c:\foo\bar.csproj (9,40) +c:\foo\bar.csproj (9,25) +c:\foo\bar.csproj (10,32) +c:\foo\bar.csproj (10,29) +c:\foo\bar.csproj (13,21) +c:\foo\bar.csproj (14,28) +c:\foo\bar.csproj (14,25) +c:\foo\bar.csproj (16,29) +c:\foo\bar.csproj (16,59) +c:\foo\bar.csproj (16,70) +c:\foo\bar.csproj (16,29) +c:\foo\bar.csproj (16,42) +c:\foo\bar.csproj (16,21) +c:\foo\bar.csproj (17,25) +c:\foo\bar.csproj (18,32) +c:\foo\bar.csproj (18,61) +c:\foo\bar.csproj (18,44) +c:\foo\bar.csproj (18,29) +c:\foo\bar.csproj (19,36) +c:\foo\bar.csproj (19,33) +c:\foo\bar.csproj (21,32) +c:\foo\bar.csproj (21,29) +c:\foo\bar.csproj (23,25) +c:\foo\bar.csproj (24,32) +c:\foo\bar.csproj (24,29) +Text: (26,32) +Importance: (26,66) +c:\foo\bar.csproj (26,43) +c:\foo\bar.csproj (26,25) +c:\foo\bar.csproj (28,29) +c:\foo\bar.csproj (28,41) +c:\foo\bar.csproj (28,21) +"; + + Helpers.VerifyAssertLineByLine(expected, locations); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectChooseElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectChooseElement_Tests.cs new file mode 100644 index 00000000000..9b5a91ec1aa --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectChooseElement_Tests.cs @@ -0,0 +1,284 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//

Tests for the ProjectChooseElement class (and for ProjectWhenElement and ProjectOtherwiseElement). +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectChooseElement class (and for ProjectWhenElement and ProjectOtherwiseElement) + /// + [TestClass] + public class ProjectChooseElement_Tests + { + /// + /// Read choose with unexpected attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read choose with unexpected Condition attribute. + /// Condition is not currently allowed on Choose. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidConditionAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read choose with unexpected child + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidChild() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read choose with a When containing no Condition attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidWhen() + { + string content = @" + + + + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read choose with only an otherwise + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidOnlyOtherwise() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read choose with two otherwises + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidTwoOtherwise() + { + string content = @" + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read choose with otherwise before when + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidOtherwiseBeforeWhen() + { + string content = @" + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read empty choose + /// + /// + /// One might think this should work but 2.0 required at least one When. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyChoose() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectChooseElement choose = (ProjectChooseElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(null, Helpers.GetFirst(choose.Children)); + } + + /// + /// Read choose with only a when + /// + [TestMethod] + public void ReadChooseOnlyWhen() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectChooseElement choose = (ProjectChooseElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(1, Helpers.Count(choose.WhenElements)); + Assert.AreEqual(null, choose.OtherwiseElement); + } + + /// + /// Read basic choose + /// + [TestMethod] + public void ReadChooseBothWhenOtherwise() + { + string content = @" + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectChooseElement choose = (ProjectChooseElement)Helpers.GetFirst(project.Children); + + List whens = Helpers.MakeList(choose.WhenElements); + Assert.AreEqual(2, whens.Count); + Assert.AreEqual("c1", whens[0].Condition); + Assert.AreEqual("c2", whens[1].Condition); + Assert.IsNotNull(choose.OtherwiseElement); + } + + /// + /// Test stack overflow is prevented. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExcessivelyNestedChoose() + { + StringBuilder builder1 = new StringBuilder(); + StringBuilder builder2 = new StringBuilder(); + + for (int i = 0; i < 52; i++) + { + builder1.Append(""); + builder2.Append(""); + } + + string content = ""; + content += builder1.ToString(); + content += builder2.ToString(); + content += @""; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Setting a When's condition should dirty the project + /// + [TestMethod] + public void SettingWhenConditionDirties() + { + string content = @" + + + + +

v1

+
+
+
+
+ "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectChooseElement choose = Helpers.GetFirst(project.Xml.ChooseElements); + ProjectWhenElement when = Helpers.GetFirst(choose.WhenElements); + when.Condition = "false"; + + Assert.AreEqual("v1", project.GetPropertyValue("p")); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual(String.Empty, project.GetPropertyValue("p")); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectElement_Tests.cs new file mode 100644 index 00000000000..5aaea5ebd84 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectElement_Tests.cs @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectElement base class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml; + +using NUnit.Framework; + +using Microsoft.Build.Exceptions; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectElement class + /// + [TestFixture] + public class ProjectElementTests + { + /// + /// No parents + /// + [Test] + public void AllParentsNoParents() + { + string content = @""; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assertion.AssertEquals(null, xml.AllParents.Count()); + } + + /// + /// Two parents + /// + [Test] + public void AllParentsNoParents() + { + string content = @" + + + + + +"; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTaskElement task = xml.Targets.GetFirst().Tasks.GetFirst(); + Assertion.AssertEquals(2, task.AllParents.Count()); + Assertion.AssertEquals(xml.Targets.GetFirst(), task.AllParents.GetFirst()); + Assertion.AssertEquals(xml, task.AllParents.ItemAt(1)); + } + + + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectExtensionsElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectExtensionsElement_Tests.cs new file mode 100644 index 00000000000..e7a2a27f847 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectExtensionsElement_Tests.cs @@ -0,0 +1,217 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectProjectExtensions class. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + // Tests for the ProjectExtensionsElement class. + /// Tests for the class + /// + [TestClass] + public class ProjectExtensionsElement_Tests + { + /// + /// Read ProjectExtensions with some child + /// + [TestMethod] + public void Read() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectExtensionsElement extensions = (ProjectExtensionsElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(@"", extensions.Content); + } + + /// + /// Read ProjectExtensions with invalid Condition attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidCondition() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read project with more than one ProjectExtensions + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidDuplicate() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Set valid content + /// + [TestMethod] + public void SetValid() + { + ProjectExtensionsElement extensions = GetEmptyProjectExtensions(); + Helpers.ClearDirtyFlag(extensions.ContainingProject); + + extensions.Content = "ac"; + + Assert.AreEqual(@"ac", extensions.Content); + Assert.AreEqual(true, extensions.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set null content + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNull() + { + ProjectExtensionsElement extensions = GetEmptyProjectExtensions(); + + extensions.Content = null; + } + + /// + /// Delete by ID + /// + [TestMethod] + public void DeleteById() + { + string content = @" + + + x + y + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectExtensionsElement extensions = (ProjectExtensionsElement)Helpers.GetFirst(project.Children); + extensions["a"] = String.Empty; + content = extensions["a"]; + Assert.AreEqual(String.Empty, content); + extensions["a"] = String.Empty; // make sure it doesn't die or something + + Assert.AreEqual("y", extensions["b"]); + } + + /// + /// Get by ID + /// + [TestMethod] + public void GetById() + { + string content = @" + + + x + y + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectExtensionsElement extensions = (ProjectExtensionsElement)Helpers.GetFirst(project.Children); + + content = extensions["b"]; + Assert.AreEqual("y", content); + + content = extensions["nonexistent"]; + Assert.AreEqual(String.Empty, content); + } + + /// + /// Set by ID on not existing ID + /// + [TestMethod] + public void SetById() + { + string content = @" + + + x + y + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectExtensionsElement extensions = (ProjectExtensionsElement)Helpers.GetFirst(project.Children); + + extensions["c"] = "z"; + Assert.AreEqual("z", extensions["c"]); + } + + /// + /// Set by ID on existing ID + /// + [TestMethod] + public void SetByIdWhereItAlreadyExists() + { + string content = @" + + + x + y + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectExtensionsElement extensions = (ProjectExtensionsElement)Helpers.GetFirst(project.Children); + + extensions["b"] = "y2"; + Assert.AreEqual("y2", extensions["b"]); + } + + /// + /// Helper to get an empty ProjectExtensionsElement object + /// + private static ProjectExtensionsElement GetEmptyProjectExtensions() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectExtensionsElement extensions = (ProjectExtensionsElement)Helpers.GetFirst(project.Children); + return extensions; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportElement_Tests.cs new file mode 100644 index 00000000000..1fd25f1414e --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportElement_Tests.cs @@ -0,0 +1,285 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectImportElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectImportElement class + /// + [TestClass] + public class ProjectImportElement_Tests + { + /// + /// Read project with no imports + /// + [TestMethod] + public void ReadNone() + { + ProjectRootElement project = ProjectRootElement.Create(); + + Assert.AreEqual(null, project.Imports.GetEnumerator().Current); + } + + /// + /// Read import with no project attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidMissingProject() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read import with empty project attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyProject() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read import with unexpected attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read basic valid imports + /// + [TestMethod] + public void ReadBasic() + { + string content = @" + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + List imports = Helpers.MakeList(project.Imports); + + Assert.AreEqual(2, imports.Count); + Assert.AreEqual("i1.proj", imports[0].Project); + Assert.AreEqual("i2.proj", imports[1].Project); + Assert.AreEqual("c", imports[1].Condition); + } + + /// + /// Set valid project on import + /// + [TestMethod] + public void SetProjectValid() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + ProjectImportElement import = (ProjectImportElement)Helpers.GetFirst(project.Children); + + import.Project = "i1b.proj"; + Assert.AreEqual("i1b.proj", import.Project); + } + + /// + /// Set invalid empty project value on import + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetProjectInvalidEmpty() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + ProjectImportElement import = (ProjectImportElement)Helpers.GetFirst(project.Children); + + import.Project = String.Empty; + } + + /// + /// Setting the project attribute should dirty the project + /// + [TestMethod] + public void SettingProjectDirties() + { + string file1 = null; + string file2 = null; + + try + { + file1 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement importProject1 = ProjectRootElement.Create(); + importProject1.AddProperty("p", "v1"); + importProject1.Save(file1); + + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement importProject2 = ProjectRootElement.Create(); + importProject2.AddProperty("p", "v2"); + importProject2.Save(file2); + + string content = String.Format + ( + @" + +", + file1 + ); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectImportElement import = Helpers.GetFirst(project.Xml.Imports); + import.Project = file2; + + Assert.AreEqual("v1", project.GetPropertyValue("p")); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual("v2", project.GetPropertyValue("p")); + } + finally + { + File.Delete(file1); + File.Delete(file2); + } + } + + /// + /// Setting the condition should dirty the project + /// + [TestMethod] + public void SettingConditionDirties() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement importProject = ProjectRootElement.Create(); + importProject.AddProperty("p", "v1"); + importProject.Save(file); + + string content = String.Format + ( + @" + +", + file + ); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + ProjectImportElement import = Helpers.GetFirst(project.Xml.Imports); + import.Condition = "false"; + + Assert.AreEqual("v1", project.GetPropertyValue("p")); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual(String.Empty, project.GetPropertyValue("p")); + } + finally + { + File.Delete(file); + } + } + + /// + /// Importing a project which has a relative path + /// + [TestMethod] + public void ImportWithRelativePath() + { + string tempPath = Path.GetTempPath(); + string testTempPath = Path.Combine(tempPath, "UnitTestsPublicOm"); + string projectfile = Path.Combine(testTempPath, "a.proj"); + string targetsFile = Path.Combine(tempPath, "x.targets"); + string projectfileContent = String.Format + ( + @" + + + + ", + testTempPath + "\\..\\x.targets" + ); + string targetsfileContent = @" + + + "; + try + { + Directory.CreateDirectory(testTempPath); + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectfileContent))); + project.Save(projectfile); + project = ProjectRootElement.Create(XmlReader.Create(new StringReader(targetsfileContent))); + project.Save(targetsFile); + Project msbuildProject = new Project(projectfile); + } + finally + { + if (Directory.Exists(testTempPath)) + { + Directory.Delete(testTempPath, true); + } + + if (File.Exists(targetsFile)) + { + File.Delete(targetsFile); + } + } + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportGroupElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportGroupElement_Tests.cs new file mode 100644 index 00000000000..fc445245538 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectImportGroupElement_Tests.cs @@ -0,0 +1,397 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectImportGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectImportGroupElement class + /// + [TestClass] + public class ProjectImportGroupElement_Tests + { + /// + /// Tests that an import is added at the end of the file + /// when no import group exists + /// + [TestMethod] + public void AddImportWhenNoImportGroupExists() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + project.AddImport("b.proj"); + + string expectedContent = @" + + + + + "; + + Helpers.CompareProjectXml(expectedContent, project.RawXml); + } + + /// + /// Tests that an import is added to (the last) (non-conditioned) + /// import group if one exists + /// + [TestMethod] + public void AddImportToLastImportGroupWithNoCondition() + { + string content = @" + + + + + + + + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + project.AddImport("e.proj"); + + string expectedContent = @" + + + + + + + + + + + + + + + + + "; + + Helpers.CompareProjectXml(expectedContent, project.RawXml); + } + + /// + /// Tests that an import is added at the end of the file + /// when no import group exists + /// + [TestMethod] + public void AddImportOnlyConditionedImportGroupsExist() + { + string content = @" + + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + project.AddImport("d.proj"); + + string expectedContent = @" + + + + + + + + + + + "; + + Helpers.CompareProjectXml(expectedContent, project.RawXml); + } + + /// + /// Read project with no imports + /// + [TestMethod] + public void ReadNone() + { + ProjectRootElement project = ProjectRootElement.Create(); + + Assert.AreEqual(null, project.Imports.GetEnumerator().Current); + } + + /// + /// An empty import group does nothing, but also shouldn't error + /// + public void ReadNoChild() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + ProjectImportGroupElement importGroup = (ProjectImportGroupElement)Helpers.GetFirst(project.ImportGroups); + + Assert.AreEqual(null, project.Imports.GetEnumerator().Current); + Assert.AreEqual(0, Helpers.Count(importGroup.Imports)); + } + + /// + /// Read import group with a contained import that has no no project attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidChildMissingProject() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Checks that an InvalidProjectFileException is thrown when an invalid + /// child type is placed inside an ImportGroup. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidChildType() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Checks that an InvalidProjectFileException is thrown when an ImportGroup is placed + /// inside an invalid parent. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidParentType() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read import group with unexpected attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read basic valid import group + /// + [TestMethod] + public void ReadBasic() + { + string content = @" + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + List imports = Helpers.MakeList(project.Imports); + List importGroups = Helpers.MakeList(project.ImportGroups); + + Assert.AreEqual(1, importGroups.Count); + Assert.AreEqual(2, importGroups[0].Count); + Assert.AreEqual(2, imports.Count); + Assert.AreEqual("i1.proj", imports[0].Project); + Assert.AreEqual("i2.proj", imports[1].Project); + Assert.AreEqual("c", imports[1].Condition); + } + + /// + /// Multiple import groups should all show up in the project's imports + /// + [TestMethod] + public void ReadMultipleImportGroups() + { + string content = @" + + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + List imports = Helpers.MakeList(project.Imports); + List importGroups = Helpers.MakeList(project.ImportGroups); + + Assert.AreEqual(2, importGroups.Count); + Assert.AreEqual(2, importGroups[0].Count); + Assert.AreEqual(1, importGroups[1].Count); + Assert.AreEqual("second", importGroups[1].Label); + + Assert.AreEqual(3, imports.Count); + Assert.AreEqual("i1.proj", imports[0].Project); + Assert.AreEqual("i2.proj", imports[1].Project); + Assert.AreEqual("c", imports[1].Condition); + Assert.AreEqual("i3.proj", imports[2].Project); + } + + /// + /// Set valid project on import + /// + [TestMethod] + public void SetProjectValid() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + ProjectImportGroupElement importGroup = (ProjectImportGroupElement)Helpers.GetFirst(project.Children); + + ProjectImportElement import = (ProjectImportElement)Helpers.GetFirst(importGroup.Imports); + + import.Project = "i1b.proj"; + Assert.AreEqual("i1b.proj", import.Project); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set invalid empty project value on import + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetProjectInvalidEmpty() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + ProjectImportGroupElement importGroup = (ProjectImportGroupElement)Helpers.GetFirst(project.Children); + + ProjectImportElement import = (ProjectImportElement)Helpers.GetFirst(importGroup.Imports); + + import.Project = String.Empty; + } + + /// + /// Set the condition value + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddImportGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectImportGroupElement importGroup = Helpers.GetFirst(project.ImportGroups); + importGroup.Condition = "c"; + + Assert.AreEqual("c", importGroup.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set the label value + /// + [TestMethod] + public void SetLabel() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddImportGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectImportGroupElement importGroup = Helpers.GetFirst(project.ImportGroups); + importGroup.Label = "c"; + + Assert.AreEqual("c", importGroup.Label); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionElement_Tests.cs new file mode 100644 index 00000000000..0bdeb4b5a28 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionElement_Tests.cs @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectItemDefinitionElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectItemDefinitionElement class + /// + [TestClass] + public class ProjectItemDefinitionElement_Tests + { + /// + /// Read item definition with no children + /// + [TestMethod] + public void ReadNoChildren() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemDefinitionGroupElement itemDefinitionGroup = (ProjectItemDefinitionGroupElement)Helpers.GetFirst(project.Children); + ProjectItemDefinitionElement itemDefinition = Helpers.GetFirst(itemDefinitionGroup.ItemDefinitions); + + Assert.AreEqual(0, Helpers.Count(itemDefinition.Metadata)); + } + + /// + /// Read an item definition with a child + /// + [TestMethod] + public void ReadBasic() + { + string content = @" + + + + v1 + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemDefinitionGroupElement itemDefinitionGroup = (ProjectItemDefinitionGroupElement)Helpers.GetFirst(project.Children); + ProjectItemDefinitionElement definition = Helpers.GetFirst(itemDefinitionGroup.ItemDefinitions); + + Assert.AreEqual("i", definition.ItemType); + Assert.AreEqual(1, Helpers.Count(definition.Metadata)); + Assert.AreEqual("m1", Helpers.GetFirst(definition.Metadata).Name); + Assert.AreEqual("v1", Helpers.GetFirst(definition.Metadata).Value); + } + + /// + /// Read item with reserved element name + /// + /// + /// Orcas inadvertently did not check for reserved item types (like "Choose") in item definitions, + /// as we do for item types in item groups. So we do not fail here. + /// + [TestMethod] + public void ReadBuiltInElementName() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an item definition with several metadata + /// + [TestMethod] + public void ReadMetadata() + { + string content = @" + + + + v1 + v2 + v3 + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemDefinitionGroupElement itemDefinitionGroup = (ProjectItemDefinitionGroupElement)Helpers.GetFirst(project.Children); + ProjectItemDefinitionElement itemDefinition = Helpers.GetFirst(itemDefinitionGroup.ItemDefinitions); + + var metadata = Helpers.MakeList(itemDefinition.Metadata); + + Assert.AreEqual(3, metadata.Count); + Assert.AreEqual("m1", metadata[0].Name); + Assert.AreEqual("v1", metadata[0].Value); + Assert.AreEqual("m2", metadata[1].Name); + Assert.AreEqual("v2", metadata[1].Value); + Assert.AreEqual("c", metadata[1].Condition); + Assert.AreEqual("m1", metadata[2].Name); + Assert.AreEqual("v3", metadata[2].Value); + } + + /// + /// Set the condition value + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectItemDefinitionElement itemDefinition = project.AddItemDefinitionGroup().AddItemDefinition("i"); + Helpers.ClearDirtyFlag(project); + + itemDefinition.Condition = "c"; + + Assert.AreEqual("c", itemDefinition.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionGroupElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionGroupElement_Tests.cs new file mode 100644 index 00000000000..dec60e21d1b --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemDefinitionGroupElement_Tests.cs @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for of ProjectItemDefinitionGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectItemDefinitionGroupElement class + /// + [TestClass] + public class ProjectItemDefinitionGroupElement_Tests + { + /// + /// Read project with no item definition groups + /// + [TestMethod] + public void ReadNone() + { + ProjectRootElement project = ProjectRootElement.Create(); + Assert.AreEqual(0, Helpers.Count(project.Children)); + Assert.AreEqual(null, project.ItemDefinitionGroups.GetEnumerator().Current); + } + + /// + /// Read itemdefinitiongroup with unexpected attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read itemdefinitiongroup with no children + /// + [TestMethod] + public void ReadNoChildren() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemDefinitionGroupElement itemDefinitionGroup = (ProjectItemDefinitionGroupElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(0, Helpers.Count(itemDefinitionGroup.ItemDefinitions)); + } + + /// + /// Read basic valid set of itemdefinitiongroups + /// + [TestMethod] + public void ReadBasic() + { + string content = @" + + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + var itemDefinitionGroups = Helpers.MakeList(project.ItemDefinitionGroups); + Assert.AreEqual(2, itemDefinitionGroups.Count); + + Assert.AreEqual(1, Helpers.Count(itemDefinitionGroups[0].ItemDefinitions)); + Assert.AreEqual(2, Helpers.Count(itemDefinitionGroups[1].ItemDefinitions)); + Assert.AreEqual("c", itemDefinitionGroups[0].Condition); + } + + /// + /// Set the condition value + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemDefinitionGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectItemDefinitionGroupElement itemDefinitionGroup = Helpers.GetFirst(project.ItemDefinitionGroups); + itemDefinitionGroup.Condition = "c"; + + Assert.AreEqual("c", itemDefinitionGroup.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set the Label value + /// + [TestMethod] + public void SetLabel() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemDefinitionGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectItemDefinitionGroupElement itemDefinitionGroup = Helpers.GetFirst(project.ItemDefinitionGroups); + itemDefinitionGroup.Label = "c"; + + Assert.AreEqual("c", itemDefinitionGroup.Label); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemElement_Tests.cs new file mode 100644 index 00000000000..8f5fa55c697 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemElement_Tests.cs @@ -0,0 +1,662 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectItemElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Test the ProjectItemElement class + /// + [TestClass] + public class ProjectItemElement_Tests + { + /// + /// Read item with no children + /// + [TestMethod] + public void ReadNoChildren() + { + ProjectItemElement item = GetItemXml(); + + Assert.AreEqual(0, Helpers.Count(item.Metadata)); + } + + /// + /// Read item with no include + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidNoInclude() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item which contains text + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidContainsText() + { + string content = @" + + + error text + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with empty include + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyInclude() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with reserved element name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidReservedElementName() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with Exclude without Include + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidExcludeWithoutInclude() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Basic reading of items + /// + [TestMethod] + public void ReadBasic() + { + string content = @" + + + + v1 + + + v2 + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(project.Children); + + var items = Helpers.MakeList(itemGroup.Items); + + Assert.AreEqual("i1", items[0].ItemType); + Assert.AreEqual("i", items[0].Include); + + var metadata1 = Helpers.MakeList(items[0].Metadata); + Assert.AreEqual(1, metadata1.Count); + Assert.AreEqual("m1", metadata1[0].Name); + Assert.AreEqual("v1", metadata1[0].Value); + + var metadata2 = Helpers.MakeList(items[1].Metadata); + Assert.AreEqual("i2", items[1].ItemType); + Assert.AreEqual("i", items[1].Include); + Assert.AreEqual("j", items[1].Exclude); + Assert.AreEqual(1, metadata2.Count); + Assert.AreEqual("m2", metadata2[0].Name); + Assert.AreEqual("v2", metadata2[0].Value); + } + + /// + /// Read metadata on item + /// + [TestMethod] + public void ReadMetadata() + { + string content = @" + + + + v1 + v2 + v3 + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(project.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + + var metadata = Helpers.MakeList(item.Metadata); + Assert.AreEqual(3, metadata.Count); + Assert.AreEqual("m1", metadata[0].Name); + Assert.AreEqual("v1", metadata[0].Value); + Assert.AreEqual("m2", metadata[1].Name); + Assert.AreEqual("v2", metadata[1].Value); + Assert.AreEqual("c", metadata[1].Condition); + Assert.AreEqual("m1", metadata[2].Name); + Assert.AreEqual("v3", metadata[2].Value); + } + + /// + /// Read item with Remove outside of Target: not currently supported + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidRemoveOutsideTarget() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with Remove inside of Target, but with metadata + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidRemoveWithMetadataInsideTarget() + { + string content = @" + + + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with Remove inside of Target, but with Exclude: not currently supported + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidExcludeAndRemoveInsideTarget() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with Remove inside of Target, but with Include: not currently supported + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidIncludeAndRemoveInsideTarget() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with Remove inside of Target + /// + [TestMethod] + public void ReadValidRemoveInsideTarget() + { + string content = @" + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(target.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + + Assert.AreEqual("i1", item.Remove); + } + + /// + /// Read item with Exclude without Include, inside of Target + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidExcludeWithoutIncludeWithinTarget() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read item with Exclude without Include, inside of Target + /// + [TestMethod] + public void ReadValidIncludeExcludeWithinTarget() + { + string content = @" + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(target.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + + Assert.AreEqual("i1", item.Include); + Assert.AreEqual("i2", item.Exclude); + } + + /// + /// Set the include on an item + /// + [TestMethod] + public void SetInclude() + { + ProjectItemElement item = GetItemXml(); + + item.Include = "ib"; + + Assert.AreEqual("ib", item.Include); + } + + /// + /// Set empty include: this removes it + /// + [TestMethod] + public void SetEmptyInclude() + { + ProjectItemElement item = GetItemXml(); + + item.Include = String.Empty; + + Assert.AreEqual(String.Empty, item.Include); + } + + /// + /// Set null empty : this removes it + /// + [TestMethod] + public void SetNullInclude() + { + ProjectItemElement item = GetItemXml(); + + item.Include = null; + + Assert.AreEqual(String.Empty, item.Include); + } + + /// + /// Set the Exclude on an item + /// + [TestMethod] + public void SetExclude() + { + ProjectItemElement item = GetItemXml(); + + item.Exclude = "ib"; + + Assert.AreEqual("ib", item.Exclude); + } + + /// + /// Set empty Exclude: this removes it + /// + [TestMethod] + public void SetEmptyExclude() + { + ProjectItemElement item = GetItemXml(); + + item.Exclude = String.Empty; + + Assert.AreEqual(String.Empty, item.Exclude); + } + + /// + /// Set null Exclude: this removes it + /// + [TestMethod] + public void SetNullExclude() + { + ProjectItemElement item = GetItemXml(); + + item.Exclude = null; + + Assert.AreEqual(String.Empty, item.Exclude); + } + + /// + /// Set the Remove on an item + /// + [TestMethod] + public void SetRemove() + { + ProjectItemElement item = GetItemXmlWithRemove(); + + item.Remove = "ib"; + + Assert.AreEqual("ib", item.Remove); + } + + /// + /// Set empty Remove: this removes it + /// + [TestMethod] + public void SetEmptyRemove() + { + ProjectItemElement item = GetItemXmlWithRemove(); + + item.Remove = String.Empty; + + Assert.AreEqual(String.Empty, item.Remove); + } + + /// + /// Set null Remove: this removes it + /// + [TestMethod] + public void SetNullRemove() + { + ProjectItemElement item = GetItemXmlWithRemove(); + + item.Remove = null; + + Assert.AreEqual(String.Empty, item.Remove); + } + + /// + /// Set Include when Remove is present + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetInvalidIncludeWithRemove() + { + ProjectItemElement item = GetItemXmlWithRemove(); + + item.Include = "i1"; + } + + /// + /// Set Exclude when Remove is present + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetInvalidExcludeWithRemove() + { + ProjectItemElement item = GetItemXmlWithRemove(); + + item.Exclude = "i1"; + } + + /// + /// Set Remove when Include is present, inside a target + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetInvalidRemoveWithInclude() + { + ProjectItemElement item = GetItemXmlInsideTarget(); + + item.Remove = "i1"; + } + + /// + /// Set Remove outside of a target: this is currently invalid + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetInvalidRemoveOutsideTarget() + { + ProjectItemElement item = GetItemXml(); + + item.Remove = "i1"; + } + + /// + /// Set the condition on an item + /// + [TestMethod] + public void SetCondition() + { + ProjectItemElement item = GetItemXml(); + + item.Condition = "c"; + + Assert.AreEqual("c", item.Condition); + } + + /// + /// Setting condition should dirty the project + /// + [TestMethod] + public void SettingItemConditionDirties() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + item.Xml.Condition = "false"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual(0, Helpers.MakeList(project.Items).Count); + } + + /// + /// Setting include should dirty the project + /// + [TestMethod] + public void SettingItemIncludeDirties() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + item.Xml.Include = "i2"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual("i2", Helpers.GetFirst(project.Items).EvaluatedInclude); + } + + /// + /// Setting exclude should dirty the project + /// + [TestMethod] + public void SettingItemExcludeDirties() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + item.Xml.Exclude = "i1"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual(0, Helpers.MakeList(project.Items).Count); + } + + /// + /// Setting exclude should dirty the project + /// + [TestMethod] + public void SettingItemRemoveDirties() + { + ProjectRootElement project = ProjectRootElement.Create(); + + ProjectItemElement item = project.AddTarget("t").AddItemGroup().AddItem("i", "i1"); + item.Include = null; + Helpers.ClearDirtyFlag(project); + + item.Remove = "i2"; + + Assert.AreEqual("i2", item.Remove); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Get a valid ProjectItemElement with no metadata + /// + private static ProjectItemElement GetItemXml() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(project.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + return item; + } + + /// + /// Get a valid ProjectItemElement with an Include on it (inside a Target) + /// + private static ProjectItemElement GetItemXmlInsideTarget() + { + string content = @" + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(target.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + return item; + } + + /// + /// Get a valid ProjectItemElement with a Remove on it (in a target) + /// + private static ProjectItemElement GetItemXmlWithRemove() + { + string content = @" + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(target.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + return item; + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemGroupElement_tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemGroupElement_tests.cs new file mode 100644 index 00000000000..91612548e29 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectItemGroupElement_tests.cs @@ -0,0 +1,116 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test the ProjectItemGroupElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Test the ProjectItemGroupElement class + /// + [TestClass] + public class ProjectItemGroupElement_tests + { + /// + /// Read item groups in an empty project + /// + [TestMethod] + public void ReadNoItemGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + Assert.AreEqual(0, Helpers.Count(project.Children)); + Assert.AreEqual(null, project.ItemGroups.GetEnumerator().Current); + } + + /// + /// Read an empty item group + /// + [TestMethod] + public void ReadEmptyItemGroup() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemGroupElement group = (ProjectItemGroupElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(0, Helpers.Count(group.Items)); + } + + /// + /// Read an item group with two item children + /// + [TestMethod] + public void ReadItemGroupTwoItems() + { + string content = @" + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemGroupElement group = (ProjectItemGroupElement)Helpers.GetFirst(project.Children); + + var items = Helpers.MakeList(group.Items); + + Assert.AreEqual(2, items.Count); + Assert.AreEqual("i1", items[0].Include); + Assert.AreEqual("i2", items[1].Include); + } + + /// + /// Set the condition value + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectItemGroupElement itemGroup = Helpers.GetFirst(project.ItemGroups); + itemGroup.Condition = "c"; + + Assert.AreEqual("c", itemGroup.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set the Label value + /// + [TestMethod] + public void SetLabel() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItemGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectItemGroupElement itemGroup = Helpers.GetFirst(project.ItemGroups); + itemGroup.Label = "c"; + + Assert.AreEqual("c", itemGroup.Label); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectMetadataElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectMetadataElement_Tests.cs new file mode 100644 index 00000000000..d340d28ce68 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectMetadataElement_Tests.cs @@ -0,0 +1,255 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test the ProjectMetadataElement class. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectMetadataElement class + /// + [TestClass] + public class ProjectMetadataElement_Tests + { + /// + /// Read simple metadatum + /// + [TestMethod] + public void ReadMetadata() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + + Assert.AreEqual("m", metadatum.Name); + Assert.AreEqual("m1", metadatum.Value); + Assert.AreEqual("c", metadatum.Condition); + } + + /// + /// Read metadatum with invalid attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read metadatum with invalid name characters (but legal xml) + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidName() + { + string content = @" + + + + <" + "\u03A3" + @"/> + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read metadatum with invalid built-in metadata name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBuiltInName() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read metadatum with invalid built-in element name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBuiltInElementName() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Set metadatum value + /// + [TestMethod] + public void SetValue() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + + metadatum.Value = "m1b"; + Assert.AreEqual("m1b", metadatum.Value); + } + + /// + /// Rename + /// + [TestMethod] + public void SetName() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + + metadatum.Name = "m2"; + Assert.AreEqual("m2", metadatum.Name); + Assert.AreEqual(true, metadatum.ContainingProject.HasUnsavedChanges); + } + + /// + /// Rename to same value should not mark dirty + /// + [TestMethod] + public void SetNameSame() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + Helpers.ClearDirtyFlag(metadatum.ContainingProject); + + metadatum.Name = "m"; + Assert.AreEqual("m", metadatum.Name); + Assert.AreEqual(false, metadatum.ContainingProject.HasUnsavedChanges); + } + + /// + /// Rename to illegal name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetNameIllegal() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + + metadatum.Name = "ImportGroup"; + } + + /// + /// Set metadatum value to empty + /// + [TestMethod] + public void SetEmptyValue() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + + metadatum.Value = String.Empty; + Assert.AreEqual(String.Empty, metadatum.Value); + } + + /// + /// Set metadatum value to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullValue() + { + ProjectMetadataElement metadatum = GetMetadataXml(); + + metadatum.Value = null; + } + + /// + /// Read a metadatum containing an expression like @(..) but whose parent is an ItemDefinitionGroup + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidItemExpressionInMetadata() + { + string content = @" + + + + @(x) + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read a metadatum containing an expression like @(..) but whose parent is NOT an ItemDefinitionGroup + /// + [TestMethod] + public void ReadValidItemExpressionInMetadata() + { + string content = @" + + + + @(x) + + + + "; + + // Should not throw + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Helper to get a ProjectMetadataElement for a simple metadatum + /// + private static ProjectMetadataElement GetMetadataXml() + { + string content = @" + + + + m1 + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectItemGroupElement itemGroup = (ProjectItemGroupElement)Helpers.GetFirst(project.Children); + ProjectItemElement item = Helpers.GetFirst(itemGroup.Items); + ProjectMetadataElement metadata = Helpers.GetFirst(item.Metadata); + return metadata; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOnErrorElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOnErrorElement_Tests.cs new file mode 100644 index 00000000000..668dcfd28ac --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOnErrorElement_Tests.cs @@ -0,0 +1,307 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectOnErrorElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectOnErrorElement class + /// + [TestClass] + public class ProjectOnErrorElement_Tests + { + /// + /// Read a target containing only OnError + /// + [TestMethod] + public void ReadTargetOnlyContainingOnError() + { + ProjectOnErrorElement onError = GetOnError(); + + Assert.AreEqual("t", onError.ExecuteTargetsAttribute); + Assert.AreEqual("c", onError.Condition); + } + + /// + /// Read a target with two onerrors, and some tasks + /// + [TestMethod] + public void ReadTargetTwoOnErrors() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + var onErrors = Helpers.MakeList(target.OnErrors); + + ProjectOnErrorElement onError1 = onErrors[0]; + ProjectOnErrorElement onError2 = onErrors[1]; + + Assert.AreEqual("1", onError1.ExecuteTargetsAttribute); + Assert.AreEqual("2", onError2.ExecuteTargetsAttribute); + } + + /// + /// Read onerror with no executetargets attribute + /// + /// + /// This was accidentally allowed in 2.0/3.5 but it should be an error now. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadMissingExecuteTargets() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectOnErrorElement onError = (ProjectOnErrorElement)Helpers.GetFirst(target.Children); + + Assert.AreEqual(String.Empty, onError.ExecuteTargetsAttribute); + } + + /// + /// Read onerror with empty executetargets attribute + /// + /// + /// This was accidentally allowed in 2.0/3.5 but it should be an error now. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadEmptyExecuteTargets() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectOnErrorElement onError = (ProjectOnErrorElement)Helpers.GetFirst(target.Children); + + Assert.AreEqual(String.Empty, onError.ExecuteTargetsAttribute); + } + + /// + /// Read onerror with invalid attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidUnexpectedAttribute() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read onerror with invalid child element + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidUnexpectedChild() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read onerror before task + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBeforeTask() + { + string content = @" + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read onerror before task + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBeforePropertyGroup() + { + string content = @" + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read onerror before task + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBeforeItemGroup() + { + string content = @" + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Set ExecuteTargets + /// + [TestMethod] + public void SetExecuteTargetsValid() + { + ProjectOnErrorElement onError = GetOnError(); + + onError.ExecuteTargetsAttribute = "t2"; + + Assert.AreEqual("t2", onError.ExecuteTargetsAttribute); + } + + /// + /// Set ExecuteTargets to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidExecuteTargetsNull() + { + ProjectOnErrorElement onError = GetOnError(); + + onError.ExecuteTargetsAttribute = null; + } + + /// + /// Set ExecuteTargets to empty string + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidExecuteTargetsEmpty() + { + ProjectOnErrorElement onError = GetOnError(); + + onError.ExecuteTargetsAttribute = String.Empty; + } + + /// + /// Set on error condition + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + ProjectOnErrorElement onError = project.CreateOnErrorElement("et"); + target.AppendChild(onError); + Helpers.ClearDirtyFlag(project); + + onError.Condition = "c"; + + Assert.AreEqual("c", onError.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set on error executetargets value + /// + [TestMethod] + public void SetExecuteTargets() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + ProjectOnErrorElement onError = project.CreateOnErrorElement("et"); + target.AppendChild(onError); + Helpers.ClearDirtyFlag(project); + + onError.ExecuteTargetsAttribute = "et2"; + + Assert.AreEqual("et2", onError.ExecuteTargetsAttribute); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Get a basic ProjectOnErrorElement + /// + private static ProjectOnErrorElement GetOnError() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectOnErrorElement onError = (ProjectOnErrorElement)Helpers.GetFirst(target.Children); + return onError; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOutputElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOutputElement_Tests.cs new file mode 100644 index 00000000000..db1223cac9d --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectOutputElement_Tests.cs @@ -0,0 +1,314 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectOutputElement class. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Test the ProjectOutputElement class + /// + [TestClass] + public class ProjectOutputElement_Tests + { + /// + /// Read an output item + /// + [TestMethod] + public void ReadOutputItem() + { + ProjectOutputElement output = GetOutputItem(); + + Assert.AreEqual(false, output.IsOutputProperty); + Assert.AreEqual(true, output.IsOutputItem); + Assert.AreEqual("p", output.TaskParameter); + Assert.AreEqual(String.Empty, output.PropertyName); + Assert.AreEqual("i1", output.ItemType); + } + + /// + /// Read an output property + /// + [TestMethod] + public void ReadOutputProperty() + { + ProjectOutputElement output = GetOutputProperty(); + + Assert.AreEqual(true, output.IsOutputProperty); + Assert.AreEqual(false, output.IsOutputItem); + Assert.AreEqual("p", output.TaskParameter); + Assert.AreEqual("p1", output.PropertyName); + Assert.AreEqual(String.Empty, output.ItemType); + } + + /// + /// Read an output property with missing itemname and propertyname + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidOutputWithoutPropertyOrItem() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an output property with reserved property name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidReservedOutputPropertyName() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an output property with missing taskparameter + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidOutputWithoutTaskName() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an output property with missing taskparameter + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidOutputWithEmptyTaskName() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an output property with child element + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidOutputWithChildElement() + { + string content = @" + + + + + xxxxxxx + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an output property with propertyname but an empty itemname attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidPropertyValueItemBlank() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an output property with an itemname but an empty propertyname attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidItemValuePropertyBlank() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Modify the condition + /// + [TestMethod] + public void SetOutputPropertyCondition() + { + ProjectOutputElement output = GetOutputProperty(); + Helpers.ClearDirtyFlag(output.ContainingProject); + + output.Condition = "c"; + Assert.AreEqual("c", output.Condition); + Assert.AreEqual(true, output.ContainingProject.HasUnsavedChanges); + } + + /// + /// Modify the property name value + /// + [TestMethod] + public void SetOutputPropertyName() + { + ProjectOutputElement output = GetOutputProperty(); + Helpers.ClearDirtyFlag(output.ContainingProject); + + output.PropertyName = "p1b"; + Assert.AreEqual("p1b", output.PropertyName); + Assert.AreEqual(true, output.ContainingProject.HasUnsavedChanges); + } + + /// + /// Attempt to set the item name value when property name is set + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetOutputPropertyItemType() + { + ProjectOutputElement output = GetOutputProperty(); + + output.ItemType = "i1b"; + } + + /// + /// Set the item name value + /// + [TestMethod] + public void SetOutputItemItemType() + { + ProjectOutputElement output = GetOutputItem(); + Helpers.ClearDirtyFlag(output.ContainingProject); + + output.ItemType = "p1b"; + Assert.AreEqual("p1b", output.ItemType); + Assert.AreEqual(true, output.ContainingProject.HasUnsavedChanges); + } + + /// + /// Attempt to set the property name when the item name is set + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetOutputItemPropertyName() + { + ProjectOutputElement output = GetOutputItem(); + + output.PropertyName = "p1b"; + } + + /// + /// Helper to get a ProjectOutputElement for an output item + /// + private static ProjectOutputElement GetOutputItem() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectTaskElement task = (ProjectTaskElement)Helpers.GetFirst(target.Children); + return Helpers.GetFirst(task.Outputs); + } + + /// + /// Helper to get a ProjectOutputElement for an output property + /// + private static ProjectOutputElement GetOutputProperty() + { + string content = @" + + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + ProjectTaskElement task = (ProjectTaskElement)Helpers.GetFirst(target.Children); + return Helpers.GetFirst(task.Outputs); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyElement_Tests.cs new file mode 100644 index 00000000000..f8a509aea74 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyElement_Tests.cs @@ -0,0 +1,281 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test the ProjectPropertyElement class. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectPropertyElement class + /// + [TestClass] + public class ProjectPropertyElement_Tests + { + /// + /// Read simple property + /// + [TestMethod] + public void ReadProperty() + { + ProjectPropertyElement property = GetPropertyXml(); + + Assert.AreEqual("p", property.Name); + Assert.AreEqual("v", property.Value); + Assert.AreEqual("c", property.Condition); + } + + /// + /// Read property with children - they are merely part of its value + /// + [TestMethod] + public void ReadPropertyWithChildren() + { + string content = @" + + +

ACE

+
+
+ "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectPropertyGroupElement propertyGroup = (ProjectPropertyGroupElement)Helpers.GetFirst(project.Children); + ProjectPropertyElement property = Helpers.GetFirst(propertyGroup.Properties); + + Assert.AreEqual("p", property.Name); + Assert.AreEqual(@"ACE", property.Value); + } + + /// + /// Read property with invalid name (but legal xml) + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidName() + { + string content = @" + + + <" + "\u03A3" + @"/> + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read property with invalid reserved name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidReservedName() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read property with invalid built in name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBuiltInName() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read property with invalid attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + +

+ + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + ///

+ /// Read property with child element + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidChildElement() + { + string content = @" + + +

+ +

+ + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + ///

+ /// Set property value + /// + [TestMethod] + public void SetValue() + { + ProjectPropertyElement property = GetPropertyXml(); + Helpers.ClearDirtyFlag(property.ContainingProject); + + property.Value = "vb"; + Assert.AreEqual("vb", property.Value); + Assert.AreEqual(true, property.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set property value to the same value it was before. + /// This should not dirty the project. + /// + [TestMethod] + public void SetSameValue() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyElement property = project.AddProperty("p", "v1"); + Helpers.ClearDirtyFlag(property.ContainingProject); + + property.Value = "v1"; + Assert.AreEqual("v1", property.Value); + Assert.AreEqual(false, property.ContainingProject.HasUnsavedChanges); + } + + /// + /// Rename + /// + [TestMethod] + public void SetName() + { + ProjectPropertyElement property = GetPropertyXml(); + + property.Name = "p2"; + Assert.AreEqual("p2", property.Name); + Assert.AreEqual(true, property.ContainingProject.HasUnsavedChanges); + } + + /// + /// Rename to same value should not mark dirty + /// + [TestMethod] + public void SetNameSame() + { + ProjectPropertyElement property = GetPropertyXml(); + Helpers.ClearDirtyFlag(property.ContainingProject); + + property.Name = "p"; + Assert.AreEqual("p", property.Name); + Assert.AreEqual(false, property.ContainingProject.HasUnsavedChanges); + } + + /// + /// Rename to illegal name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetNameIllegal() + { + ProjectPropertyElement property = GetPropertyXml(); + + property.Name = "ImportGroup"; + } + + /// + /// Set property value to empty + /// + [TestMethod] + public void SetEmptyValue() + { + ProjectPropertyElement property = GetPropertyXml(); + Helpers.ClearDirtyFlag(property.ContainingProject); + + property.Value = String.Empty; + Assert.AreEqual(String.Empty, property.Value); + Assert.AreEqual(true, property.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set property value to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullValue() + { + ProjectPropertyElement property = GetPropertyXml(); + + property.Value = null; + } + + /// + /// Set condition + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyElement property = project.AddProperty("p", "v1"); + Helpers.ClearDirtyFlag(property.ContainingProject); + + property.Condition = "c"; + Assert.AreEqual("c", property.Condition); + Assert.AreEqual(true, property.ContainingProject.HasUnsavedChanges); + } + + /// + /// Helper to get a ProjectPropertyElement for a simple property + /// + private static ProjectPropertyElement GetPropertyXml() + { + string content = @" + + +

v

+
+
+ "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectPropertyGroupElement propertyGroup = (ProjectPropertyGroupElement)Helpers.GetFirst(project.Children); + ProjectPropertyElement property = Helpers.GetFirst(propertyGroup.Properties); + return property; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyGroupElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyGroupElement_Tests.cs new file mode 100644 index 00000000000..32739d284b1 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectPropertyGroupElement_Tests.cs @@ -0,0 +1,131 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test the ProjectPropertyGroupElement class. +//----------------------------------------------------------------------- + +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Test the ProjectPropertyGroupElement class + /// + [TestClass] + public class ProjectPropertyGroupElement_Tests + { + /// + /// Read property groups in an empty project + /// + [TestMethod] + public void ReadNoPropertyGroup() + { + ProjectRootElement project = ProjectRootElement.Create(); + Assert.AreEqual(0, Helpers.Count(project.Children)); + Assert.AreEqual(null, project.PropertyGroups.GetEnumerator().Current); + } + + /// + /// Read an empty property group + /// + [TestMethod] + public void ReadEmptyPropertyGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectPropertyGroupElement group = (ProjectPropertyGroupElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(0, Helpers.Count(group.Properties)); + } + + /// + /// Read an property group with two property children + /// + [TestMethod] + public void ReadPropertyGroupTwoProperties() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v1 + + + "); + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectPropertyGroupElement group = (ProjectPropertyGroupElement)Helpers.GetFirst(project.Children); + + var properties = Helpers.MakeList(group.Properties); + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("p1", properties[0].Name); + Assert.AreEqual("p2", properties[1].Name); + } + + /// + /// Set the condition value + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddPropertyGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectPropertyGroupElement propertyGroup = Helpers.GetFirst(project.PropertyGroups); + propertyGroup.Condition = "c"; + + Assert.AreEqual("c", propertyGroup.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set the Label value + /// + [TestMethod] + public void SetLabel() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddPropertyGroup(); + Helpers.ClearDirtyFlag(project); + + ProjectPropertyGroupElement propertyGroup = Helpers.GetFirst(project.PropertyGroups); + propertyGroup.Label = "c"; + + Assert.AreEqual("c", propertyGroup.Label); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Add a property through the convenience method on a property group + /// + [TestMethod] + public void AddProperty_ExistingPropertySameName() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectPropertyGroupElement propertyGroup = project.AddPropertyGroup(); + propertyGroup.AddProperty("p", "v1"); + propertyGroup.AddProperty("p", "v2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v1

+

v2

+
+
"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectRootElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectRootElement_Tests.cs new file mode 100644 index 00000000000..4685a984a22 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectRootElement_Tests.cs @@ -0,0 +1,1264 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test the ProjectRootElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using System.Linq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; +using System.Threading; +using Microsoft.Build.Evaluation; +using System.Diagnostics; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Test the ProjectRootElement class + /// + [TestClass] + public class ProjectRootElement_Tests + { + /// + /// Empty project content + /// + [TestMethod] + public void EmptyProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + + Assert.AreEqual(0, Helpers.Count(project.Children)); + Assert.AreEqual(String.Empty, project.DefaultTargets); + Assert.AreEqual(String.Empty, project.InitialTargets); + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.ToolsVersion); + Assert.AreEqual(true, project.HasUnsavedChanges); // it is indeed unsaved + } + + /// + /// Set defaulttargets + /// + [TestMethod] + public void SetDefaultTargets() + { + ProjectRootElement project = ProjectRootElement.Create(); + + project.DefaultTargets = "dt"; + Assert.AreEqual("dt", project.DefaultTargets); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set initialtargets + /// + [TestMethod] + public void SetInitialTargets() + { + ProjectRootElement project = ProjectRootElement.Create(); + + project.InitialTargets = "it"; + Assert.AreEqual("it", project.InitialTargets); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set toolsversion + /// + [TestMethod] + public void SetToolsVersion() + { + ProjectRootElement project = ProjectRootElement.Create(); + + project.ToolsVersion = "tv"; + Assert.AreEqual("tv", project.ToolsVersion); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Setting full path should accept and update relative path + /// + [TestMethod] + public void SetFullPath() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.FullPath = "X"; + + Assert.AreEqual(project.FullPath, Path.Combine(Environment.CurrentDirectory, "X")); + } + + /// + /// Attempting to load a second ProjectRootElement over the same file path simply + /// returns the first one. + /// A ProjectRootElement is notionally a "memory mapped" view of a file, and we assume there is only + /// one per file path, so we must reject attempts to make another. + /// + [TestMethod] + public void ConstructOverSameFileReturnsSame() + { + ProjectRootElement projectXml1 = ProjectRootElement.Create(); + projectXml1.Save(Microsoft.Build.Shared.FileUtilities.GetTemporaryFile()); + + ProjectRootElement projectXml2 = ProjectRootElement.Open(projectXml1.FullPath); + + Assert.AreEqual(true, Object.ReferenceEquals(projectXml1, projectXml2)); + } + + /// + /// Attempting to load a second ProjectRootElement over the same file path simply + /// returns the first one. This should work even if one of the paths is not a full path. + /// + [TestMethod] + public void ConstructOverSameFileReturnsSameEvenWithOneBeingRelativePath() + { + ProjectRootElement projectXml1 = ProjectRootElement.Create(); + + projectXml1.FullPath = Path.Combine(Environment.CurrentDirectory, @"xyz\abc"); + + ProjectRootElement projectXml2 = ProjectRootElement.Open(@"xyz\abc"); + + Assert.AreEqual(true, Object.ReferenceEquals(projectXml1, projectXml2)); + } + + /// + /// Attempting to load a second ProjectRootElement over the same file path simply + /// returns the first one. This should work even if one of the paths is not a full path. + /// + [TestMethod] + public void ConstructOverSameFileReturnsSameEvenWithOneBeingRelativePath2() + { + ProjectRootElement projectXml1 = ProjectRootElement.Create(); + + projectXml1.FullPath = @"xyz\abc"; + + ProjectRootElement projectXml2 = ProjectRootElement.Open(Path.Combine(Environment.CurrentDirectory, @"xyz\abc")); + + Assert.AreEqual(true, Object.ReferenceEquals(projectXml1, projectXml2)); + } + + /// + /// Using TextReader + /// + [TestMethod] + public void ConstructOverSameFileReturnsSameEvenWithOneBeingRelativePath3() + { + string content = "\r\n"; + + ProjectRootElement projectXml1 = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + projectXml1.FullPath = @"xyz\abc"; + + ProjectRootElement projectXml2 = ProjectRootElement.Open(Path.Combine(Environment.CurrentDirectory, @"xyz\abc")); + + Assert.AreEqual(true, Object.ReferenceEquals(projectXml1, projectXml2)); + } + + /// + /// Using TextReader + /// + [TestMethod] + public void ConstructOverSameFileReturnsSameEvenWithOneBeingRelativePath4() + { + string content = "\r\n"; + + ProjectRootElement projectXml1 = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + projectXml1.FullPath = Path.Combine(Environment.CurrentDirectory, @"xyz\abc"); + + ProjectRootElement projectXml2 = ProjectRootElement.Open(@"xyz\abc"); + + Assert.AreEqual(true, Object.ReferenceEquals(projectXml1, projectXml2)); + } + + /// + /// Two ProjectRootElement's over the same file path does not throw (although you shouldn't do it) + /// + [TestMethod] + public void SetFullPathProjectXmlAlreadyLoaded() + { + ProjectRootElement projectXml1 = ProjectRootElement.Create(); + projectXml1.FullPath = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + ProjectRootElement projectXml2 = ProjectRootElement.Create(); + projectXml2.FullPath = projectXml1.FullPath; + } + + /// + /// Invalid XML + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidXml() + { + ProjectRootElement.Create(XmlReader.Create(new StringReader("XXX"))); + } + + /// + /// Valid Xml, invalid namespace on the root + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidNamespace() + { + string content = @" + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Invalid root tag + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidRootTag() + { + string content = @" + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Valid Xml, invalid syntax below the root + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidChildBelowRoot() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Root indicates upgrade needed + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void NeedsUpgrade() + { + string content = @" + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Valid Xml, invalid namespace below the root + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void InvalidNamespaceBelowRoot() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Tests that the namespace error reports are correct + /// + [TestMethod] + public void InvalidNamespaceErrorReport() + { + string content = @" + + + + + + "; + + content = content.Replace("`", "\""); + MockLogger logger = new MockLogger(); + string errorMessage = String.Empty; + bool exceptionThrown = false; + try + { + Microsoft.Build.Evaluation.Project project = new Microsoft.Build.Evaluation.Project(XmlReader.Create(new StringReader(content))); + } + catch (InvalidProjectFileException ex) + { + exceptionThrown = true; + + // MSB4068: The element is unrecognized, or not supported in this context. + Assert.IsTrue(ex.ErrorCode != "MSB4068"); + + // MSB4041: The default XML namespace of the project must be the MSBuild XML namespace. + Assert.AreEqual("MSB4041", ex.ErrorCode); + } + + Assert.IsTrue(exceptionThrown, "ERROR: An invalid project file exception should have been thrown."); + } + + /// + /// Valid Xml, invalid syntax thrown by child element parsing + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ValidXmlInvalidSyntaxInChildElement() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Valid Xml, invalid syntax, should not get added to the Xml cache and + /// thus returned on the second request! + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ValidXmlInvalidSyntaxOpenFromDiskTwice() + { + string content = @" + + + + + + "; + + string path = null; + + try + { + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + File.WriteAllText(path, content); + + ProjectRootElement.Open(path); + } + catch (InvalidProjectFileException) + { + } + + // Should throw again, not get from cache + ProjectRootElement.Open(path); + } + finally + { + File.Delete(path); + } + } + + /// + /// Verify that opening project using XmlTextReader does not add it to the Xml cache + /// + [TestMethod] + public void ValidXmlXmlTextReaderNotCache() + { + string content = @" + + + "; + + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + File.WriteAllText(path, content); + + XmlTextReader reader1 = new XmlTextReader(path); + ProjectRootElement root1 = ProjectRootElement.Create(reader1); + root1.AddItem("type", "include"); + + // If it's in the cache, then the 2nd document won't see the add. + XmlTextReader reader2 = new XmlTextReader(path); + ProjectRootElement root2 = ProjectRootElement.Create(reader2); + + Assert.AreEqual(1, root1.Items.Count); + Assert.AreEqual(0, root2.Items.Count); + + reader1.Close(); + reader2.Close(); + } + finally + { + File.Delete(path); + } + } + + /// + /// Verify that opening project using the same path adds it to the Xml cache + /// + [TestMethod] + public void ValidXmlXmlReaderCache() + { + string content = @" + + + "; + + string content2 = @" + + + "; + + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + File.WriteAllText(path, content); + + ProjectRootElement root1 = ProjectRootElement.Create(path); + + File.WriteAllText(path, content2); + + // If it went in the cache, and this path also reads from the cache, + // then we'll see the first version of the file. + ProjectRootElement root2 = ProjectRootElement.Create(path); + + Assert.AreEqual(String.Empty, root1.DefaultTargets); + Assert.AreEqual(String.Empty, root2.DefaultTargets); + } + finally + { + File.Delete(path); + } + } + + /// + /// A simple "system" test: load microsoft.*.targets and verify we don't throw + /// + [TestMethod] + public void LoadCommonTargets() + { + ProjectCollection projectCollection = new ProjectCollection(); + string toolsPath = projectCollection.Toolsets.Where(toolset => (String.Compare(toolset.ToolsVersion, "4.0", StringComparison.OrdinalIgnoreCase) == 0)).First().ToolsPath; + + string[] targets = new string[] { "microsoft.common.targets", "microsoft.csharp.targets", "microsoft.visualbasic.targets" }; + + foreach (string target in targets) + { + string path = Path.Combine(toolsPath, target); + ProjectRootElement project = ProjectRootElement.Open(path); + Console.WriteLine(@"Loaded target: {0}", target); + Console.WriteLine(@"Children: {0}", Helpers.Count(project.Children)); + Console.WriteLine(@"Targets: {0}", Helpers.MakeList(project.Targets).Count); + Console.WriteLine(@"Root ItemGroups: {0}", Helpers.MakeList(project.ItemGroups).Count); + Console.WriteLine(@"Root PropertyGroups: {0}", Helpers.MakeList(project.PropertyGroups).Count); + Console.WriteLine(@"UsingTasks: {0}", Helpers.MakeList(project.UsingTasks).Count); + Console.WriteLine(@"ItemDefinitionGroups: {0}", Helpers.MakeList(project.ItemDefinitionGroups).Count); + } + } + + /// + /// Save project loaded from TextReader, without setting FullPath. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidSaveWithoutFullPath() + { + XmlReader reader = XmlReader.Create(new StringReader("")); + ProjectRootElement project = ProjectRootElement.Create(reader); + + project.Save(); + } + + /// + /// Save content with transforms. + /// The ">" should not turn into "<" + /// + [TestMethod] + public void SaveWithTransforms() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "@(h->'%(x)')"); + + StringBuilder builder = new StringBuilder(); + StringWriter writer = new StringWriter(builder); + + project.Save(writer); + + // UTF-16 because writer.Encoding is UTF-16 + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + '%(x)')"" /> + +"); + + Helpers.VerifyAssertLineByLine(expected, builder.ToString()); + } + + /// + /// Save content with transforms to a file. + /// The ">" should not turn into "<" + /// + [TestMethod] + public void SaveWithTransformsToFile() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.AddItem("i", "@(h->'%(x)')"); + + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + project.Save(file); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + + + '%(x)')"" /> + +"); + + string actual = File.ReadAllText(file); + Helpers.VerifyAssertLineByLine(expected, actual); + } + finally + { + File.Delete(file); + } + } + + /// + /// Save should create a directory if it is missing + /// + [TestMethod] + public void SaveToNonexistentDirectory() + { + ProjectRootElement project = ProjectRootElement.Create(); + string directory = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string file = "foo.proj"; + string path = Path.Combine(directory, file); + + project.Save(path); + + Assert.IsTrue(File.Exists(path)); + Assert.AreEqual(path, project.FullPath); + Assert.AreEqual(directory, project.DirectoryPath); + } + finally + { + Directory.Delete(directory, true); + } + } + + /// + /// Save should create a directory if it is missing + /// + [TestMethod] + public void SaveToNonexistentDirectoryRelativePath() + { + ProjectRootElement project = ProjectRootElement.Create(); + string directory = null; + + try + { + Environment.CurrentDirectory = Path.GetTempPath(); // should be used for project.DirectoryPath; it must exist + + string file = @"bar\foo.proj"; + string path = Path.Combine(Path.GetTempPath(), file); + directory = Path.Combine(Path.GetTempPath(), "bar"); + + project.Save(file); // relative path: file and a single directory only; should create the "bar" part + + Assert.IsTrue(File.Exists(file)); + Assert.AreEqual(path, project.FullPath); + Assert.AreEqual(directory, project.DirectoryPath); + } + finally + { + Directory.Delete(directory, true); + } + } + + /// + /// Saving an unnamed project without a path specified should give a nice exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SaveUnnamedProject() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.Save(); + } + + /// + /// Verifies that the ProjectRootElement.Encoding property getter returns values + /// that are based on the XML declaration in the file. + /// + [TestMethod] + public void EncodingGetterBasedOnXmlDeclaration() + { + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + +")))); + Assert.AreEqual(Encoding.Unicode, project.Encoding); + + project = ProjectRootElement.Create(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + +")))); + Assert.AreEqual(Encoding.UTF8, project.Encoding); + + project = ProjectRootElement.Create(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + +")))); + Assert.AreEqual(Encoding.ASCII, project.Encoding); + } + + /// + /// Verifies that ProjectRootElement.Encoding returns the correct value + /// after reading a file off disk, even if no xml declaration is present. + /// + [TestMethod] + public void EncodingGetterBasedOnActualEncodingWhenXmlDeclarationIsAbsent() + { + string projectFullPath = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + try + { + VerifyLoadedProjectHasEncoding(projectFullPath, Encoding.UTF8); + VerifyLoadedProjectHasEncoding(projectFullPath, Encoding.Unicode); + + // We don't test ASCII, since there is no byte order mark for it, + // and the XmlReader will legitimately decide to intrepret it as UTF8, + // which would fail the test although it's a reasonable assumption + // when no xml declaration is present. + ////VerifyLoadedProjectHasEncoding(projectFullPath, Encoding.ASCII); + } + finally + { + File.Delete(projectFullPath); + } + } + + /// + /// Verifies that the Save method saves an otherwise unmodified project + /// with a specified file encoding. + /// + [TestMethod] + public void SaveUnmodifiedWithNewEncoding() + { + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents(@" + +")))); + project.FullPath = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + string projectFullPath = project.FullPath; + try + { + project.Save(); + project = null; + + // We haven't made any changes to the project, but we want to save it using various encodings. + SaveProjectWithEncoding(projectFullPath, Encoding.Unicode); + SaveProjectWithEncoding(projectFullPath, Encoding.ASCII); + SaveProjectWithEncoding(projectFullPath, Encoding.UTF8); + } + finally + { + File.Delete(projectFullPath); + } + } + + /// + /// Enumerate over all properties from the project directly. + /// It should traverse into Choose's. + /// + [TestMethod] + public void PropertiesEnumerator() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + +

p1

+ q1 +
+ + + r1 + + + + + + + s1 + + + + + + + s2 + + + + + + + + t1 + + + + + +
"); + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + List properties = Helpers.MakeList(project.Properties); + + Assert.AreEqual(6, properties.Count); + + Assert.AreEqual("q", properties[1].Name); + Assert.AreEqual("r1", properties[2].Value); + Assert.AreEqual("t1", properties[5].Value); + } + + /// + /// Enumerate over all items from the project directly. + /// It should traverse into Choose's. + /// + [TestMethod] + public void ItemsEnumerator() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "); + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + List items = Helpers.MakeList(project.Items); + + Assert.AreEqual(6, items.Count); + + Assert.AreEqual("j", items[1].ItemType); + Assert.AreEqual("k1", items[2].Include); + Assert.AreEqual("k4", items[5].Include); + } + + /// + /// Build a solution file that can't be accessed + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void SolutionCanNotBeOpened() + { + string solutionFile = null; + string tempFileSentinel = null; + + IdentityReference identity = new SecurityIdentifier(WellKnownSidType.WorldSid, null); + FileSystemAccessRule rule = new FileSystemAccessRule(identity, FileSystemRights.Read, AccessControlType.Deny); + + FileSecurity security = null; + + try + { + tempFileSentinel = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + solutionFile = Path.ChangeExtension(tempFileSentinel, ".sln"); + File.Copy(tempFileSentinel, solutionFile); + + security = new FileSecurity(solutionFile, System.Security.AccessControl.AccessControlSections.All); + + security.AddAccessRule(rule); + + File.SetAccessControl(solutionFile, security); + + ProjectRootElement p = ProjectRootElement.Open(solutionFile); + } + catch (PrivilegeNotHeldException) + { + throw new InvalidProjectFileException("Running unelevated so skipping this scenario."); + } + finally + { + if (security != null) + { + security.RemoveAccessRule(rule); + } + + File.Delete(solutionFile); + File.Delete(tempFileSentinel); + Assert.AreEqual(false, File.Exists(solutionFile)); + } + } + + /// + /// Build a project file that can't be accessed + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ProjectCanNotBeOpened() + { + string projectFile = null; + + IdentityReference identity = new SecurityIdentifier(WellKnownSidType.WorldSid, null); + FileSystemAccessRule rule = new FileSystemAccessRule(identity, FileSystemRights.Read, AccessControlType.Deny); + + FileSecurity security = null; + + try + { + // Does not have .sln or .vcproj extension so loads as project + projectFile = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + security = new FileSecurity(projectFile, System.Security.AccessControl.AccessControlSections.All); + security.AddAccessRule(rule); + + File.SetAccessControl(projectFile, security); + + ProjectRootElement p = ProjectRootElement.Open(projectFile); + } + catch (PrivilegeNotHeldException) + { + throw new InvalidProjectFileException("Running unelevated so skipping the scenario."); + } + finally + { + if (security != null) + { + security.RemoveAccessRule(rule); + } + + File.Delete(projectFile); + Assert.AreEqual(false, File.Exists(projectFile)); + } + } + + /// + /// Build a corrupt solution + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void SolutionCorrupt() + { + string solutionFile = null; + + try + { + solutionFile = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + // Arbitrary corrupt content + string content = @"Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio Codename Orcas +Project(""{"; + + File.WriteAllText(solutionFile, content); + + ProjectRootElement p = ProjectRootElement.Open(solutionFile); + } + finally + { + File.Delete(solutionFile); + } + } + + /// + /// Open lots of projects concurrently to try to trigger problems + /// + [TestMethod] + public void ConcurrentProjectOpenAndCloseThroughProject() + { + int iterations = 500; + string[] paths = ObjectModelHelpers.GetTempFiles(iterations); + + try + { + Project[] projects = new Project[iterations]; + + for (int i = 0; i < iterations; i++) + { + CreatePREWithSubstantialContent().Save(paths[i]); + } + + var collection = new ProjectCollection(); + int counter = 0; + int remaining = iterations; + var done = new ManualResetEvent(false); + + for (int i = 0; i < iterations; i++) + { + ThreadPool.QueueUserWorkItem(delegate + { + var current = Interlocked.Increment(ref counter) - 1; + + projects[current] = collection.LoadProject(paths[current]); + + if (Interlocked.Decrement(ref remaining) == 0) + { + done.Set(); + } + }); + } + + done.WaitOne(); + + Assert.AreEqual(iterations, collection.LoadedProjects.Count); + + counter = 0; + remaining = iterations; + done.Reset(); + + for (int i = 0; i < iterations; i++) + { + ThreadPool.QueueUserWorkItem(delegate + { + var current = Interlocked.Increment(ref counter) - 1; + + var pre = projects[current].Xml; + collection.UnloadProject(projects[current]); + collection.UnloadProject(pre); + + if (Interlocked.Decrement(ref remaining) == 0) + { + done.Set(); + } + }); + } + + done.WaitOne(); + + Assert.AreEqual(0, collection.LoadedProjects.Count); + } + finally + { + for (int i = 0; i < iterations; i++) + { + File.Delete(paths[i]); + } + } + } + + /// + /// Open lots of projects concurrently to try to trigger problems + /// + [TestMethod] + public void ConcurrentProjectOpenAndCloseThroughProjectRootElement() + { + int iterations = 500; + string[] paths = ObjectModelHelpers.GetTempFiles(iterations); + + try + { + var projects = new ProjectRootElement[iterations]; + + var collection = new ProjectCollection(); + int counter = 0; + int remaining = iterations; + var done = new ManualResetEvent(false); + + for (int i = 0; i < iterations; i++) + { + ThreadPool.QueueUserWorkItem(delegate + { + var current = Interlocked.Increment(ref counter) - 1; + + CreatePREWithSubstantialContent().Save(paths[current]); + projects[current] = ProjectRootElement.Open(paths[current], collection); + + if (Interlocked.Decrement(ref remaining) == 0) + { + done.Set(); + } + }); + } + + done.WaitOne(); + + counter = 0; + remaining = iterations; + done.Reset(); + + for (int i = 0; i < iterations; i++) + { + ThreadPool.QueueUserWorkItem(delegate + { + var current = Interlocked.Increment(ref counter) - 1; + + collection.UnloadProject(projects[current]); + + if (Interlocked.Decrement(ref remaining) == 0) + { + done.Set(); + } + }); + } + + done.WaitOne(); + } + finally + { + for (int i = 0; i < iterations; i++) + { + File.Delete(paths[i]); + } + } + } + + /// + /// Tests DeepClone and CopyFrom for ProjectRootElements. + /// + [TestMethod] + public void DeepClone() + { + var pre = ProjectRootElement.Create(); + var pg = pre.AddPropertyGroup(); + pg.AddProperty("a", "$(b)"); + pg.AddProperty("c", ""); + + var ig = pre.AddItemGroup(); + var item = ig.AddItem("Foo", "boo$(hoo)"); + item.AddMetadata("Some", "Value"); + + var target = pre.AddTarget("SomeTarget"); + target.Condition = "Some Condition"; + var task = target.AddTask("SomeTask"); + task.AddOutputItem("p1", "it"); + task.AddOutputProperty("prop", "it2"); + target.AppendChild(pre.CreateOnErrorElement("someTarget")); + + var idg = pre.AddItemDefinitionGroup(); + var id = idg.AddItemDefinition("SomeType"); + id.AddMetadata("sm", "sv"); + + var ut = pre.AddUsingTask("name", "assembly", null); + + var inlineUt = pre.AddUsingTask("anotherName", "somefile", null); + inlineUt.TaskFactory = "SomeFactory"; + var utb = inlineUt.AddUsingTaskBody("someEvaluate", "someTaskBody"); + + var choose = pre.CreateChooseElement(); + pre.AppendChild(choose); + var when1 = pre.CreateWhenElement("some condition"); + choose.AppendChild(when1); + when1.AppendChild(pre.CreatePropertyGroupElement()); + var otherwise = pre.CreateOtherwiseElement(); + choose.AppendChild(otherwise); + otherwise.AppendChild(pre.CreateItemGroupElement()); + + var importGroup = pre.AddImportGroup(); + importGroup.AddImport("Some imported project"); + pre.AddImport("direct import"); + + ValidateDeepCloneAndCopyFrom(pre); + } + + /// + /// Tests DeepClone and CopyFrom for ProjectRootElement that contain ProjectExtensions with text inside. + /// + [TestMethod] + public void DeepCloneWithProjectExtensionsElementOfText() + { + var pre = ProjectRootElement.Create(); + + var extensions = pre.CreateProjectExtensionsElement(); + extensions.Content = "Some foo content"; + pre.AppendChild(extensions); + + ValidateDeepCloneAndCopyFrom(pre); + } + + /// + /// Tests DeepClone and CopyFrom for ProjectRootElement that contain ProjectExtensions with xml inside. + /// + [TestMethod] + public void DeepCloneWithProjectExtensionsElementOfXml() + { + var pre = ProjectRootElement.Create(); + + var extensions = pre.CreateProjectExtensionsElement(); + extensions.Content = ""; + pre.AppendChild(extensions); + + ValidateDeepCloneAndCopyFrom(pre); + } + + /// + /// Test helper for validating that DeepClone and CopyFrom work as advertised. + /// + private static void ValidateDeepCloneAndCopyFrom(ProjectRootElement pre) + { + var pre2 = pre.DeepClone(); + Assert.AreNotSame(pre2, pre); + Assert.AreEqual(pre.RawXml, pre2.RawXml); + + var pre3 = ProjectRootElement.Create(); + pre3.AddPropertyGroup(); // this should get wiped out in the DeepCopyFrom + pre3.DeepCopyFrom(pre); + Assert.AreEqual(pre.RawXml, pre3.RawXml); + } + + /// + /// Re-saves a project with a new encoding and thoroughly verifies that the right things happen. + /// + private void SaveProjectWithEncoding(string projectFullPath, Encoding encoding) + { + // Always use a new project collection to guarantee we're reading off disk. + ProjectRootElement project = ProjectRootElement.Open(projectFullPath, new ProjectCollection()); + project.Save(encoding); + Assert.AreEqual(encoding, project.Encoding, "Changing an unmodified project's encoding failed to update ProjectRootElement.Encoding."); + + // Try to verify that the xml declaration was emitted, and that the correct byte order marks + // are also present. + using (var reader = new StreamReader(projectFullPath, encoding, true)) + { + Assert.AreEqual(encoding, reader.CurrentEncoding); + string actual = reader.ReadLine(); + string expected = String.Format(@"", encoding.WebName); + Assert.AreEqual(expected, actual, "The encoding was not emitted as an XML declaration."); + } + + project = ProjectRootElement.Open(projectFullPath, new ProjectCollection()); + + // We compare body names instead of encoding instances directly because at times the instances can + // be (insignificantly) different. + Assert.AreEqual(encoding.BodyName, project.Encoding.BodyName, "A re-opened project did not retain the previously saved Encoding."); + } + + /// + /// Creates a project at a given path with a given encoding but without the Xml declaration, + /// and then verifies that when loaded by MSBuild, the encoding is correctly reported. + /// + private void VerifyLoadedProjectHasEncoding(string projectFullPath, Encoding encoding) + { + CreateProjectWithEncodingWithoutDeclaration(projectFullPath, encoding); + + // Let's just be certain the project has been read off disk... + ProjectRootElement project = ProjectRootElement.Open(projectFullPath, new ProjectCollection()); + Assert.AreEqual(encoding.WebName, project.Encoding.WebName); + } + + /// + /// Creates a project file with a specific encoding, but without an XML declaration. + /// + private void CreateProjectWithEncodingWithoutDeclaration(string projectFullPath, Encoding encoding) + { + const string EmptyProject = @" +"; + + using (StreamWriter writer = new StreamWriter(projectFullPath, false, encoding)) + { + writer.Write(ObjectModelHelpers.CleanupFileContents(EmptyProject)); + } + } + + /// + /// Create a nice big PRE + /// + private ProjectRootElement CreatePREWithSubstantialContent() + { + string content = ObjectModelHelpers.CleanupFileContents( + @" + + +

p1

+ q1 +
+ + + r1 + + + + + + + s1 + + + + + + + s2 + + + + + + + + t1 + + + + + + +
"); + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + + return project; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTargetElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTargetElement_Tests.cs new file mode 100644 index 00000000000..320f54aa3dd --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTargetElement_Tests.cs @@ -0,0 +1,351 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Test the ProjectTargetElement class. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Test the ProjectTargetElement class + /// + [TestClass] + public class ProjectTargetElement_Tests + { + /// + /// Create target with invalid name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AddTargetInvalidName() + { + ProjectRootElement project = ProjectRootElement.Create(); + project.CreateTargetElement("@#$invalid@#$"); + } + + /// + /// Read targets in an empty project + /// + [TestMethod] + public void ReadNoTarget() + { + ProjectRootElement project = ProjectRootElement.Create(); + Assert.AreEqual(null, project.Targets.GetEnumerator().Current); + } + + /// + /// Read an empty target + /// + [TestMethod] + public void ReadEmptyTarget() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + + Assert.AreEqual(0, Helpers.Count(target.Children)); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Read attributes on the target element + /// + [TestMethod] + public void ReadParameters() + { + ProjectTargetElement target = GetTargetXml(); + + Assert.AreEqual("i", target.Inputs); + Assert.AreEqual("o", target.Outputs); + Assert.AreEqual("d", target.DependsOnTargets); + Assert.AreEqual("c", target.Condition); + } + + /// + /// Set attributes on the target element + /// + [TestMethod] + public void SetParameters() + { + ProjectTargetElement target = GetTargetXml(); + + target.Inputs = "ib"; + target.Outputs = "ob"; + target.DependsOnTargets = "db"; + target.Condition = "cb"; + + Assert.AreEqual("ib", target.Inputs); + Assert.AreEqual("ob", target.Outputs); + Assert.AreEqual("db", target.DependsOnTargets); + Assert.AreEqual("cb", target.Condition); + } + + /// + /// Set null inputs on the target element + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullInputs() + { + ProjectTargetElement target = GetTargetXml(); + target.Inputs = null; + } + + /// + /// Set null outputs on the target element + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullOutputs() + { + ProjectTargetElement target = GetTargetXml(); + target.Outputs = null; + } + + /// + /// Set null dependsOnTargets on the target element + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullDependsOnTargets() + { + ProjectTargetElement target = GetTargetXml(); + target.DependsOnTargets = null; + } + + /// + /// Set null dependsOnTargets on the target element + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullKeepDuplicateOutputs() + { + ProjectTargetElement target = GetTargetXml(); + target.KeepDuplicateOutputs = null; + } + + /// + /// Set null condition on the target element + /// + [TestMethod] + public void SetNullCondition() + { + ProjectTargetElement target = GetTargetXml(); + target.Condition = null; + + Assert.AreEqual(String.Empty, target.Condition); + } + + /// + /// Read a target with a missing name + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidMissingName() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read a target with an invalid attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read an target with two task children + /// + [TestMethod] + public void ReadTargetTwoTasks() + { + ProjectTargetElement target = GetTargetXml(); + + var tasks = Helpers.MakeList(target.Children); + + Assert.AreEqual(2, tasks.Count); + + ProjectTaskElement task1 = (ProjectTaskElement)tasks[0]; + ProjectTaskElement task2 = (ProjectTaskElement)tasks[1]; + + Assert.AreEqual("t1", task1.Name); + Assert.AreEqual("t2", task2.Name); + } + + /// + /// Set name + /// + [TestMethod] + public void SetName() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.Name = "t2"; + + Assert.AreEqual("t2", target.Name); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set inputs + /// + [TestMethod] + public void SetInputs() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.Inputs = "in"; + + Assert.AreEqual("in", target.Inputs); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set outputs + /// + [TestMethod] + public void SetOutputs() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.Outputs = "out"; + + Assert.AreEqual("out", target.Outputs); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set dependsontargets + /// + [TestMethod] + public void SetDependsOnTargets() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.DependsOnTargets = "dot"; + + Assert.AreEqual("dot", target.DependsOnTargets); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set condition + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.Condition = "c"; + + Assert.AreEqual("c", target.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set KeepDuplicateOutputs attribute + /// + [TestMethod] + public void SetKeepDuplicateOutputs() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.KeepDuplicateOutputs = "true"; + + Assert.AreEqual("true", target.KeepDuplicateOutputs); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set return value. Verify that setting to the empty string and null are + /// both allowed and have distinct behaviour. + /// + [TestMethod] + public void SetReturns() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTargetElement target = project.AddTarget("t"); + Helpers.ClearDirtyFlag(project); + + target.Returns = "@(a)"; + + Assert.AreEqual("@(a)", target.Returns); + Assert.AreEqual(true, project.HasUnsavedChanges); + + Helpers.ClearDirtyFlag(project); + + target.Returns = String.Empty; + + Assert.AreEqual(String.Empty, target.Returns); + Assert.AreEqual(true, project.HasUnsavedChanges); + + Helpers.ClearDirtyFlag(project); + + target.Returns = null; + + Assert.IsNull(target.Returns); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Helper to get an empty ProjectTargetElement with various attributes and two tasks + /// + private static ProjectTargetElement GetTargetXml() + { + string content = @" + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + return target; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTaskElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTaskElement_Tests.cs new file mode 100644 index 00000000000..8210fdd2f5b --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectTaskElement_Tests.cs @@ -0,0 +1,324 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectTaskElement class. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectTaskElement class + /// + [TestClass] + public class ProjectTaskElement_Tests + { + /// + /// Read task with no parameters + /// + [TestMethod] + public void ReadNoParameters() + { + string content = @" + + + + + + "; + + ProjectTaskElement task = GetTaskFromContent(content); + var parameters = Helpers.MakeDictionary(task.Parameters); + Assert.AreEqual("t1", task.Name); + Assert.AreEqual(0, parameters.Count); + Assert.AreEqual(0, Helpers.Count(task.Outputs)); + Assert.AreEqual(String.Empty, task.ContinueOnError); + } + + /// + /// Read task with continue on error + /// + [TestMethod] + public void ReadContinueOnError() + { + string content = @" + + + + + + "; + + ProjectTaskElement task = GetTaskFromContent(content); + + Assert.AreEqual("coe", task.ContinueOnError); + } + + /// + /// Read task with condition + /// + [TestMethod] + public void ReadCondition() + { + string content = @" + + + + + + "; + + ProjectTaskElement task = GetTaskFromContent(content); + + Assert.AreEqual("c", task.Condition); + } + + /// + /// Read task with invalid child + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidChild() + { + string content = @" + + + + + + + + "; + + GetTaskFromContent(content); + } + + /// + /// Read task with empty parameter. + /// Although MSBuild does not set these on tasks, they + /// are visible in the XML objects for editing purposes. + /// + [TestMethod] + public void ReadEmptyParameter() + { + string content = @" + + + + + + "; + + ProjectTaskElement task = GetTaskFromContent(content); + + var parameters = Helpers.MakeDictionary(task.Parameters); + + Assert.AreEqual(1, parameters.Count); + } + + /// + /// Read task with parameters + /// + [TestMethod] + public void ReadParameters() + { + string content = @" + + + + + + "; + + ProjectTaskElement task = GetTaskFromContent(content); + + var parameters = Helpers.MakeDictionary(task.Parameters); + + Assert.AreEqual(2, parameters.Count); + Assert.AreEqual("v1", parameters["p1"]); + Assert.AreEqual("v2", parameters["p2"]); + + Assert.AreEqual("v1", task.GetParameter("p1")); + Assert.AreEqual(String.Empty, task.GetParameter("xxxx")); + } + + /// + /// Change a parameter value on the task + /// + [TestMethod] + public void SetParameterValue() + { + ProjectTaskElement task = GetBasicTask(); + Helpers.ClearDirtyFlag(task.ContainingProject); + + task.SetParameter("p1", "v1b"); + + var parameters = Helpers.MakeDictionary(task.Parameters); + Assert.AreEqual("v1b", parameters["p1"]); + Assert.AreEqual(true, task.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set a parameter to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullParameterValue() + { + ProjectTaskElement task = GetBasicTask(); + + task.SetParameter("p1", null); + } + + /// + /// Set a parameter with the reserved name 'continueonerror' + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidParameterNameContinueOnError() + { + ProjectTaskElement task = GetBasicTask(); + + task.SetParameter("ContinueOnError", "v"); + } + + /// + /// Set a parameter with the reserved name 'condition' + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidParameterNameCondition() + { + ProjectTaskElement task = GetBasicTask(); + + task.SetParameter("Condition", "c"); + } + + /// + /// Set a parameter using a null name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullParameterName() + { + ProjectTaskElement task = GetBasicTask(); + + task.SetParameter(null, "v1"); + } + + /// + /// Add a parameter to the task + /// + [TestMethod] + public void SetNotExistingParameter() + { + ProjectTaskElement task = GetBasicTask(); + Helpers.ClearDirtyFlag(task.ContainingProject); + + task.SetParameter("p2", "v2"); + + var parameters = Helpers.MakeDictionary(task.Parameters); + Assert.AreEqual("v2", parameters["p2"]); + Assert.AreEqual(true, task.ContainingProject.HasUnsavedChanges); + } + + /// + /// Remove a parameter from the task + /// + [TestMethod] + public void RemoveExistingParameter() + { + ProjectTaskElement task = GetBasicTask(); + Helpers.ClearDirtyFlag(task.ContainingProject); + + task.RemoveParameter("p1"); + + var parameters = Helpers.MakeDictionary(task.Parameters); + Assert.AreEqual(0, parameters.Count); + Assert.AreEqual(true, task.ContainingProject.HasUnsavedChanges); + } + + /// + /// Remove a parameter that is not on the task + /// + /// + /// This should not throw. + /// + [TestMethod] + public void RemoveNonExistingParameter() + { + ProjectTaskElement task = GetBasicTask(); + + task.RemoveParameter("XX"); + + var parameters = Helpers.MakeDictionary(task.Parameters); + Assert.AreEqual(1, parameters.Count); + } + + /// + /// Set continue on error + /// + [TestMethod] + public void SetContinueOnError() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTaskElement task = project.AddTarget("t").AddTask("tt"); + Helpers.ClearDirtyFlag(task.ContainingProject); + + task.ContinueOnError = "true"; + Assert.AreEqual("true", task.ContinueOnError); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Set condition + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectTaskElement task = project.AddTarget("t").AddTask("tt"); + Helpers.ClearDirtyFlag(task.ContainingProject); + + task.Condition = "c"; + Assert.AreEqual("c", task.Condition); + Assert.AreEqual(true, project.HasUnsavedChanges); + } + + /// + /// Helper to return the first ProjectTaskElement from the parsed project content provided + /// + private static ProjectTaskElement GetTaskFromContent(string content) + { + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectTargetElement target = (ProjectTargetElement)Helpers.GetFirst(project.Children); + return (ProjectTaskElement)Helpers.GetFirst(target.Children); + } + + /// + /// Get a basic ProjectTaskElement with one parameter p1 + /// + private static ProjectTaskElement GetBasicTask() + { + string content = @" + + + + + + "; + + ProjectTaskElement task = GetTaskFromContent(content); + return task; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectUsingTaskElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectUsingTaskElement_Tests.cs new file mode 100644 index 00000000000..6289b0523ac --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectUsingTaskElement_Tests.cs @@ -0,0 +1,523 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectUsingTaskElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; + +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectUsingTaskElement class + /// + [TestClass] + public class ProjectUsingTaskElement_Tests + { + /// + /// Read project with no usingtasks + /// + [TestMethod] + public void ReadNone() + { + ProjectRootElement project = ProjectRootElement.Create(); + + Assert.AreEqual(null, project.UsingTasks.GetEnumerator().Current); + } + + /// + /// Read usingtask with no task name attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidMissingTaskName() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with empty task name attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyTaskName() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with unexpected attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with neither AssemblyFile nor AssemblyName attributes + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidMissingAssemblyFileAssemblyName() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with only empty AssemblyFile attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyAssemblyFile() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with empty AssemblyFile attribute but AssemblyName present + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyAssemblyFileAndAssemblyNameNotEmpty() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with only empty AssemblyName attribute but AssemblyFile present + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidEmptyAssemblyNameAndAssemblyFileNotEmpty() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with both AssemblyName and AssemblyFile attributes + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBothAssemblyFileAssemblyName() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with both AssemblyName and AssemblyFile attributes but both are empty + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidBothEmptyAssemblyFileEmptyAssemblyNameBoth() + { + string content = @" + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Read usingtask with assembly file + /// + [TestMethod] + public void ReadBasicUsingTaskAssemblyFile() + { + ProjectUsingTaskElement usingTask = GetUsingTaskAssemblyFile(); + + Assert.AreEqual("t1", usingTask.TaskName); + Assert.AreEqual("af", usingTask.AssemblyFile); + Assert.AreEqual(String.Empty, usingTask.AssemblyName); + Assert.AreEqual(String.Empty, usingTask.Condition); + } + + /// + /// Read usingtask with assembly name + /// + [TestMethod] + public void ReadBasicUsingTaskAssemblyName() + { + ProjectUsingTaskElement usingTask = GetUsingTaskAssemblyName(); + + Assert.AreEqual("t2", usingTask.TaskName); + Assert.AreEqual(String.Empty, usingTask.AssemblyFile); + Assert.AreEqual("an", usingTask.AssemblyName); + Assert.AreEqual("c", usingTask.Condition); + } + + /// + /// Read usingtask with task factory, required runtime and required platform + /// + [TestMethod] + public void ReadBasicUsingTaskFactoryRuntimeAndPlatform() + { + ProjectUsingTaskElement usingTask = GetUsingTaskFactoryRuntimeAndPlatform(); + + Assert.AreEqual("t2", usingTask.TaskName); + Assert.AreEqual(String.Empty, usingTask.AssemblyFile); + Assert.AreEqual("an", usingTask.AssemblyName); + Assert.AreEqual("c", usingTask.Condition); + Assert.AreEqual("AssemblyFactory", usingTask.TaskFactory); + } + + /// + /// Verify that passing in string.empty or null for TaskFactory will remove the element from the xml. + /// + [TestMethod] + public void RemoveUsingTaskFactoryRuntimeAndPlatform() + { + ProjectUsingTaskElement usingTask = GetUsingTaskFactoryRuntimeAndPlatform(); + + string value = null; + VerifyAttributesRemoved(usingTask, value); + + usingTask = GetUsingTaskFactoryRuntimeAndPlatform(); + value = String.Empty; + VerifyAttributesRemoved(usingTask, value); + } + + /// + /// Set assembly file on a usingtask that already has assembly file + /// + [TestMethod] + public void SetUsingTaskAssemblyFileOnUsingTaskAssemblyFile() + { + ProjectUsingTaskElement usingTask = GetUsingTaskAssemblyFile(); + Helpers.ClearDirtyFlag(usingTask.ContainingProject); + + usingTask.AssemblyFile = "afb"; + Assert.AreEqual("afb", usingTask.AssemblyFile); + Assert.AreEqual(true, usingTask.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set assembly name on a usingtask that already has assembly name + /// + [TestMethod] + public void SetUsingTaskAssemblyNameOnUsingTaskAssemblyName() + { + ProjectUsingTaskElement usingTask = GetUsingTaskAssemblyName(); + Helpers.ClearDirtyFlag(usingTask.ContainingProject); + + usingTask.AssemblyName = "anb"; + Assert.AreEqual("anb", usingTask.AssemblyName); + Assert.AreEqual(true, usingTask.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set assembly file on a usingtask that already has assembly name + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetUsingTaskAssemblyFileOnUsingTaskAssemblyName() + { + ProjectUsingTaskElement usingTask = GetUsingTaskAssemblyName(); + + usingTask.AssemblyFile = "afb"; + } + + /// + /// Set assembly name on a usingtask that already has assembly file + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetUsingTaskAssemblyNameOnUsingTaskAssemblyFile() + { + ProjectUsingTaskElement usingTask = GetUsingTaskAssemblyFile(); + + usingTask.AssemblyName = "anb"; + } + + /// + /// Set task name + /// + [TestMethod] + public void SetTaskName() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectUsingTaskElement usingTask = project.AddUsingTask("t", "af", null); + Helpers.ClearDirtyFlag(usingTask.ContainingProject); + + usingTask.TaskName = "tt"; + Assert.AreEqual("tt", usingTask.TaskName); + Assert.AreEqual(true, usingTask.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set condition + /// + [TestMethod] + public void SetCondition() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectUsingTaskElement usingTask = project.AddUsingTask("t", "af", null); + Helpers.ClearDirtyFlag(usingTask.ContainingProject); + + usingTask.Condition = "c"; + Assert.AreEqual("c", usingTask.Condition); + Assert.AreEqual(true, usingTask.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set task factory + /// + [TestMethod] + public void SetTaskFactory() + { + ProjectRootElement project = ProjectRootElement.Create(); + ProjectUsingTaskElement usingTask = project.AddUsingTask("t", "af", null); + Helpers.ClearDirtyFlag(usingTask.ContainingProject); + + usingTask.TaskFactory = "AssemblyFactory"; + Assert.AreEqual("AssemblyFactory", usingTask.TaskFactory); + Assert.AreEqual(true, usingTask.ContainingProject.HasUnsavedChanges); + } + + /// + /// Make sure there is an exception when there are multiple parameter groups in the using task tag. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void DuplicateParameterGroup() + { + string content = @" + + + + + + + "; + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assert.Fail(); + } + + /// + /// Make sure there is an exception when there are multiple task groups in the using task tag. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void DuplicateTaskGroup() + { + string content = @" + + + + + + + "; + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assert.Fail(); + } + + /// + /// Make sure there is an exception when there is an unknown child + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void UnknownChild() + { + string content = @" + + + + + + "; + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assert.Fail(); + } + + /// + /// Make sure there is an no exception when there are children in the using task + /// + [TestMethod] + public void WorksWithChildren() + { + string content = @" + + + + + + + RANDOM GOO + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + Assert.IsNotNull(usingTask); + Assert.AreEqual(2, usingTask.Count); + } + + /// + /// Make sure there is an exception when a parameter group is added but no task factory attribute is on the using task + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExceptionWhenNoTaskFactoryAndHavePG() + { + string content = @" + + + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + Assert.Fail(); + } + + /// + /// Make sure there is an exception when a parameter group is added but no task factory attribute is on the using task + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExceptionWhenNoTaskFactoryAndHaveTask() + { + string content = @" + + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + Assert.Fail(); + } + + /// + /// Helper to get a ProjectUsingTaskElement with a task factory, required runtime and required platform + /// + private static ProjectUsingTaskElement GetUsingTaskFactoryRuntimeAndPlatform() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + return usingTask; + } + + /// + /// Helper to get a ProjectUsingTaskElement with an assembly file set + /// + private static ProjectUsingTaskElement GetUsingTaskAssemblyFile() + { + string content = @" + + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + return usingTask; + } + + /// + /// Helper to get a ProjectUsingTaskElement with an assembly name set + /// + private static ProjectUsingTaskElement GetUsingTaskAssemblyName() + { + string content = @" + + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + return usingTask; + } + + /// + /// Verify the attributes are removed from the xml when string.empty and null are passed in + /// + private static void VerifyAttributesRemoved(ProjectUsingTaskElement usingTask, string value) + { + Assert.IsTrue(usingTask.ContainingProject.RawXml.Contains("TaskFactory")); + usingTask.TaskFactory = value; + Assert.IsTrue(!usingTask.ContainingProject.RawXml.Contains("TaskFactory")); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/SolutionFile_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/SolutionFile_Tests.cs new file mode 100644 index 00000000000..49d774344e6 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/SolutionFile_Tests.cs @@ -0,0 +1,944 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the parts of SolutionFile that are surfaced as +// public API +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; +using Microsoft.Build.Exceptions; + +namespace Microsoft.Build.UnitTests.Construction +{ + /// + /// Tests for the parts of SolutionFile that are surfaced as public API + /// + [TestClass] + public class SolutionFile_Tests + { + /// + /// Test that a project with the C++ project guid and an extension of vcproj is seen as invalid. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ParseSolution_VC() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'Project name.vcproj', 'Relative path\to\Project name.vcproj', '{0ABED153-9451-483C-8140-9E8D7306B216}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + Assert.Fail("Should not get here"); + } + + /// + /// Test that a project with the C++ project guid and an arbitrary extension is seen as valid -- + /// we assume that all C++ projects except .vcproj are MSBuild format. + /// + [TestMethod] + public void ParseSolution_VC2() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'Project name.myvctype', 'Relative path\to\Project name.myvctype', '{0ABED153-9451-483C-8140-9E8D7306B216}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual("Project name.myvctype", solution.ProjectsInOrder[0].ProjectName); + Assert.AreEqual("Relative path\\to\\Project name.myvctype", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid); + } + + /// + /// A slightly more complicated test where there is some different whitespace. + /// + [TestMethod] + public void ParseSolutionWithDifferentSpacing() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project(' { Project GUID} ') = ' Project name ', ' Relative path to project file ' , ' {0ABED153-9451-483C-8140-9E8D7306B216} ' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual("Project name", solution.ProjectsInOrder[0].ProjectName); + Assert.AreEqual("Relative path to project file", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid); + } + + /// + /// Solution with an empty project name. This is somewhat malformed, but we should + /// still behave reasonably instead of crashing. + /// + [TestMethod] + public void ParseSolution_EmptyProjectName() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{Project GUID}') = '', 'src\.proj', '{0ABED153-9451-483C-8140-9E8D7306B216}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.IsTrue(solution.ProjectsInOrder[0].ProjectName.StartsWith("EmptyProjectName")); + Assert.AreEqual("src\\.proj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid); + } + + /// + /// Test some characters that are valid in a file name but that also could be + /// considered a delimiter by a parser. Does quoting work for special characters? + /// + [TestMethod] + public void ParseSolutionWhereProjectNameHasSpecialCharacters() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{Project GUID}') = 'MyProject,(=IsGreat)', 'Relative path to project file' , '{0ABED153-9451-483C-8140-9E8D7306B216}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual("MyProject,(=IsGreat)", solution.ProjectsInOrder[0].ProjectName); + Assert.AreEqual("Relative path to project file", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid); + } + + /// + /// Ensure that a bogus version stamp in the .SLN file results in an + /// InvalidProjectFileException. + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void BadVersionStamp() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version a.b + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Expected version numbers less than 7 to cause an invalid project file exception. + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void VersionTooLow() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 6.0 + # Visual Studio 2005 + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Test to parse a very basic .sln file to validate that description property in a solution file + /// is properly handled. + /// + [TestMethod] + public void ParseSolutionFileWithDescriptionInformation() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'AnyProject', 'AnyProject\AnyProject.csproj', '{2CAB0FBD-15D8-458B-8E63-1B5B840E9798}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + Description = Some description of this solution + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CAB0FBD-15D8-458B-8E63-1B5B840E9798}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + try + { + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + catch (Exception ex) + { + Assert.Fail("Failed to parse solution containing description information. Error: " + ex.Message); + } + } + + /// + /// Tests the parsing of a very basic .SLN file with three independent projects. + /// + [TestMethod] + public void BasicSolution() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'ConsoleApplication1', 'ConsoleApplication1\ConsoleApplication1.vbproj', '{AB3413A6-D689-486D-B7F0-A095371B3F13}' + EndProject + Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'vbClassLibrary', 'vbClassLibrary\vbClassLibrary.vbproj', '{BA333A76-4511-47B8-8DF4-CA51C303AD0B}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{DEBCE986-61B9-435E-8018-44B9EF751655}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.Build.0 = Release|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Release|AnyCPU.Build.0 = Release|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(3, solution.ProjectsInOrder.Count); + + Assert.AreEqual("ConsoleApplication1", solution.ProjectsInOrder[0].ProjectName); + Assert.AreEqual(@"ConsoleApplication1\ConsoleApplication1.vbproj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{AB3413A6-D689-486D-B7F0-A095371B3F13}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + + Assert.AreEqual("vbClassLibrary", solution.ProjectsInOrder[1].ProjectName); + Assert.AreEqual(@"vbClassLibrary\vbClassLibrary.vbproj", solution.ProjectsInOrder[1].RelativePath); + Assert.AreEqual("{BA333A76-4511-47B8-8DF4-CA51C303AD0B}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[1].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[1].ParentProjectGuid); + + Assert.AreEqual("ClassLibrary1", solution.ProjectsInOrder[2].ProjectName); + Assert.AreEqual(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[2].RelativePath); + Assert.AreEqual("{DEBCE986-61B9-435E-8018-44B9EF751655}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[2].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[2].ParentProjectGuid); + } + + /// + /// Exercises solution folders, and makes sure that samely named projects in different + /// solution folders will get correctly uniquified. + /// + [TestMethod] + public void SolutionFolders() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{34E0D07D-CF8F-459D-9449-C4188D8C5564}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{E0F97730-25D2-418A-A7BD-02CAFDC6E470}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj', '{A5EE8128-B08E-4533-86C5-E46714981680}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySubSlnFolder', 'MySubSlnFolder', '{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary2', 'ClassLibrary2\ClassLibrary2.csproj', '{6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.Build.0 = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.Build.0 = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A5EE8128-B08E-4533-86C5-E46714981680} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4} = {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(5, solution.ProjectsInOrder.Count); + + Assert.AreEqual(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{34E0D07D-CF8F-459D-9449-C4188D8C5564}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[1].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[1].ParentProjectGuid); + + Assert.AreEqual(@"MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[2].RelativePath); + Assert.AreEqual("{A5EE8128-B08E-4533-86C5-E46714981680}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[2].Dependencies.Count); + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[2].ParentProjectGuid); + + Assert.AreEqual("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}", solution.ProjectsInOrder[3].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[3].Dependencies.Count); + Assert.AreEqual("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[3].ParentProjectGuid); + + Assert.AreEqual(@"ClassLibrary2\ClassLibrary2.csproj", solution.ProjectsInOrder[4].RelativePath); + Assert.AreEqual("{6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}", solution.ProjectsInOrder[4].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[4].Dependencies.Count); + Assert.AreEqual("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}", solution.ProjectsInOrder[4].ParentProjectGuid); + } + + /// + /// Tests situation where there's a nonexistent project listed in the solution folders. We should + /// error with a useful message. + /// + [TestMethod] + public void MissingNestedProject() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{E0F97730-25D2-418A-A7BD-02CAFDC6E470}' + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj', '{A5EE8128-B08E-4533-86C5-E46714981680}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.Build.0 = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.Build.0 = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A5EE8128-B08E-4533-86C5-E46714981680} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470} + EndGlobalSection + EndGlobal + "; + + try + { + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + catch (InvalidProjectFileException e) + { + Assert.AreEqual("MSB5023", e.ErrorCode); + Assert.IsTrue(e.Message.Contains("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}")); + return; + } + + // Should not get here + Assert.Fail(); + } + + /// + /// Verifies that hand-coded project-to-project dependencies listed in the .SLN file + /// are correctly recognized by our solution parser. + /// + [TestMethod] + public void SolutionDependencies() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{05A5AD00-71B5-4612-AF2F-9EA9121C4111}' + ProjectSection(ProjectDependencies) = postProject + {FAB4EE06-6E01-495A-8926-5514599E3DD9} = {FAB4EE06-6E01-495A-8926-5514599E3DD9} + EndProjectSection + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary2', 'ClassLibrary2\ClassLibrary2.csproj', '{7F316407-AE3E-4F26-BE61-2C50D30DA158}' + ProjectSection(ProjectDependencies) = postProject + {FAB4EE06-6E01-495A-8926-5514599E3DD9} = {FAB4EE06-6E01-495A-8926-5514599E3DD9} + {05A5AD00-71B5-4612-AF2F-9EA9121C4111} = {05A5AD00-71B5-4612-AF2F-9EA9121C4111} + EndProjectSection + EndProject + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary3', 'ClassLibrary3\ClassLibrary3.csproj', '{FAB4EE06-6E01-495A-8926-5514599E3DD9}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05A5AD00-71B5-4612-AF2F-9EA9121C4111}.Release|Any CPU.Build.0 = Release|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F316407-AE3E-4F26-BE61-2C50D30DA158}.Release|Any CPU.Build.0 = Release|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAB4EE06-6E01-495A-8926-5514599E3DD9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(3, solution.ProjectsInOrder.Count); + + Assert.AreEqual(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath); + Assert.AreEqual("{05A5AD00-71B5-4612-AF2F-9EA9121C4111}", solution.ProjectsInOrder[0].ProjectGuid); + Assert.AreEqual(1, solution.ProjectsInOrder[0].Dependencies.Count); + Assert.AreEqual("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", (string) solution.ProjectsInOrder[0].Dependencies[0]); + Assert.AreEqual(null, solution.ProjectsInOrder[0].ParentProjectGuid); + + Assert.AreEqual(@"ClassLibrary2\ClassLibrary2.csproj", solution.ProjectsInOrder[1].RelativePath); + Assert.AreEqual("{7F316407-AE3E-4F26-BE61-2C50D30DA158}", solution.ProjectsInOrder[1].ProjectGuid); + Assert.AreEqual(2, solution.ProjectsInOrder[1].Dependencies.Count); + Assert.AreEqual("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", (string) solution.ProjectsInOrder[1].Dependencies[0]); + Assert.AreEqual("{05A5AD00-71B5-4612-AF2F-9EA9121C4111}", (string) solution.ProjectsInOrder[1].Dependencies[1]); + Assert.AreEqual(null, solution.ProjectsInOrder[1].ParentProjectGuid); + + Assert.AreEqual(@"ClassLibrary3\ClassLibrary3.csproj", solution.ProjectsInOrder[2].RelativePath); + Assert.AreEqual("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", solution.ProjectsInOrder[2].ProjectGuid); + Assert.AreEqual(0, solution.ProjectsInOrder[2].Dependencies.Count); + Assert.AreEqual(null, solution.ProjectsInOrder[2].ParentProjectGuid); + } + + /// + /// Make sure the solution configurations get parsed correctly for a simple mixed C#/VC solution + /// + [TestMethod] + public void ParseSolutionConfigurations() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.ActiveCfg = Debug|ARM + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.Build.0 = Debug|ARM + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Win32.ActiveCfg = Release|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(7, solution.SolutionConfigurations.Count); + + List configurationNames = new List(6); + foreach (SolutionConfigurationInSolution configuration in solution.SolutionConfigurations) + { + configurationNames.Add(configuration.FullName); + } + + Assert.IsTrue(configurationNames.Contains("Debug|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Debug|Mixed Platforms")); + Assert.IsTrue(configurationNames.Contains("Debug|Win32")); + Assert.IsTrue(configurationNames.Contains("Release|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Release|Mixed Platforms")); + Assert.IsTrue(configurationNames.Contains("Release|Win32")); + + Assert.AreEqual("Debug", solution.GetDefaultConfigurationName(), "Default solution configuration"); + Assert.AreEqual("Mixed Platforms", solution.GetDefaultPlatformName(), "Default solution platform"); + } + + /// + /// Make sure the solution configurations get parsed correctly for a simple C# application + /// + [TestMethod] + public void ParseSolutionConfigurationsNoMixedPlatform() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|ARM = Release|ARM + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|ARM.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|x86.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|ARM.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|ARM.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + Assert.AreEqual(6, solution.SolutionConfigurations.Count); + + List configurationNames = new List(6); + foreach (SolutionConfigurationInSolution configuration in solution.SolutionConfigurations) + { + configurationNames.Add(configuration.FullName); + } + + Assert.IsTrue(configurationNames.Contains("Debug|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Debug|ARM")); + Assert.IsTrue(configurationNames.Contains("Debug|x86")); + Assert.IsTrue(configurationNames.Contains("Release|Any CPU")); + Assert.IsTrue(configurationNames.Contains("Release|ARM")); + Assert.IsTrue(configurationNames.Contains("Release|x86")); + + Assert.AreEqual("Debug", solution.GetDefaultConfigurationName(), "Default solution configuration"); + Assert.AreEqual("Any CPU", solution.GetDefaultPlatformName(), "Default solution platform"); + } + + /// + /// Test some invalid cases for solution configuration parsing. + /// There can be only one '=' character in a sln cfg entry, separating two identical names + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void ParseInvalidSolutionConfigurations1() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any=CPU = Debug|Any=CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Test some invalid cases for solution configuration parsing + /// There can be only one '=' character in a sln cfg entry, separating two identical names + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void ParseInvalidSolutionConfigurations2() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Something|Else + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Test some invalid cases for solution configuration parsing + /// Solution configurations must include the platform part + /// + [ExpectedException(typeof(InvalidProjectFileException))] + [TestMethod] + public void ParseInvalidSolutionConfigurations3() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug = Debug + Release|Any CPU = Release|Any CPU + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + } + + /// + /// Make sure the project configurations in solution configurations get parsed correctly + /// for a simple mixed C#/VC solution + /// + [TestMethod] + public void ParseProjectConfigurationsInSolutionConfigurations1() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}' + EndProject + Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'MainApp', 'MainApp\MainApp.vcxproj', '{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Win32.ActiveCfg = Release|Any CPU + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Mixed Platforms.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.ActiveCfg = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Debug|Win32.Build.0 = Debug|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32 + {A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + ProjectInSolution csharpProject = (ProjectInSolution) solution.ProjectsByGuid["{6185CC21-BE89-448A-B3C0-D1C27112E595}"]; + ProjectInSolution vcProject = (ProjectInSolution) solution.ProjectsByGuid["{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}"]; + + Assert.AreEqual(6, csharpProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug|AnyCPU", csharpProject.ProjectConfigurations["Debug|Any CPU"].FullName); + Assert.AreEqual(true, csharpProject.ProjectConfigurations["Debug|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csharpProject.ProjectConfigurations["Debug|Mixed Platforms"].FullName); + Assert.AreEqual(true, csharpProject.ProjectConfigurations["Debug|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Debug|AnyCPU", csharpProject.ProjectConfigurations["Debug|Win32"].FullName); + Assert.AreEqual(false, csharpProject.ProjectConfigurations["Debug|Win32"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csharpProject.ProjectConfigurations["Release|Any CPU"].FullName); + Assert.AreEqual(true, csharpProject.ProjectConfigurations["Release|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csharpProject.ProjectConfigurations["Release|Mixed Platforms"].FullName); + Assert.AreEqual(true, csharpProject.ProjectConfigurations["Release|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Release|AnyCPU", csharpProject.ProjectConfigurations["Release|Win32"].FullName); + Assert.AreEqual(false, csharpProject.ProjectConfigurations["Release|Win32"].IncludeInBuild); + + Assert.AreEqual(6, vcProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug|Win32", vcProject.ProjectConfigurations["Debug|Any CPU"].FullName); + Assert.AreEqual(false, vcProject.ProjectConfigurations["Debug|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Debug|Win32", vcProject.ProjectConfigurations["Debug|Mixed Platforms"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Debug|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Debug|Win32", vcProject.ProjectConfigurations["Debug|Win32"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Debug|Win32"].IncludeInBuild); + + Assert.AreEqual("Release|Win32", vcProject.ProjectConfigurations["Release|Any CPU"].FullName); + Assert.AreEqual(false, vcProject.ProjectConfigurations["Release|Any CPU"].IncludeInBuild); + + Assert.AreEqual("Release|Win32", vcProject.ProjectConfigurations["Release|Mixed Platforms"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Release|Mixed Platforms"].IncludeInBuild); + + Assert.AreEqual("Release|Win32", vcProject.ProjectConfigurations["Release|Win32"].FullName); + Assert.AreEqual(true, vcProject.ProjectConfigurations["Release|Win32"].IncludeInBuild); + } + + /// + /// Make sure the project configurations in solution configurations get parsed correctly + /// for a more tricky solution + /// + [TestMethod] + public void ParseProjectConfigurationsInSolutionConfigurations2() + { + string solutionFileContents = + @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\solutions\WebSite1\', '..\WebSite1\', '{E8E75132-67E4-4D6F-9CAE-8DA4C883F418}' + EndProject + Project('{E24C65DC-7377-472B-9ABA-BC803B73C61A}') = 'C:\solutions\WebSite2\', '..\WebSite2\', '{E8E75132-67E4-4D6F-9CAE-8DA4C883F419}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'NewFolder1', 'NewFolder1', '{54D20FFE-84BE-4066-A51E-B25D040A4235}' + EndProject + Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'NewFolder2', 'NewFolder2', '{D2633E4D-46FF-4C4E-8340-4BC7CDF78615}' + EndProject + Project('{8BC9CEB9-8B4A-11D0-8D11-00A0C91BC942}') = 'MSBuild.exe', '..\..\dd\binaries.x86dbg\bin\i386\MSBuild.exe', '{25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0}' + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|.NET = Debug|.NET + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E8E75132-67E4-4D6F-9CAE-8DA4C883F418}.Debug|.NET.ActiveCfg = Debug|.NET + {E8E75132-67E4-4D6F-9CAE-8DA4C883F418}.Debug|.NET.Build.0 = Debug|.NET + {25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0}.Debug|.NET.ActiveCfg = Debug + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0} = {D2633E4D-46FF-4C4E-8340-4BC7CDF78615} + EndGlobalSection + EndGlobal + "; + + SolutionFile solution = ParseSolutionHelper(solutionFileContents); + + ProjectInSolution webProject = (ProjectInSolution)solution.ProjectsByGuid["{E8E75132-67E4-4D6F-9CAE-8DA4C883F418}"]; + ProjectInSolution exeProject = (ProjectInSolution)solution.ProjectsByGuid["{25FD9E7C-F37E-48E0-9A7C-607FE4AACCC0}"]; + ProjectInSolution missingWebProject = (ProjectInSolution)solution.ProjectsByGuid["{E8E75132-67E4-4D6F-9CAE-8DA4C883F419}"]; + + Assert.AreEqual(1, webProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug|.NET", webProject.ProjectConfigurations["Debug|.NET"].FullName); + Assert.AreEqual(true, webProject.ProjectConfigurations["Debug|.NET"].IncludeInBuild); + + Assert.AreEqual(1, exeProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug", exeProject.ProjectConfigurations["Debug|.NET"].FullName); + Assert.AreEqual(false, exeProject.ProjectConfigurations["Debug|.NET"].IncludeInBuild); + + Assert.AreEqual(0, missingWebProject.ProjectConfigurations.Count); + + Assert.AreEqual("Debug", solution.GetDefaultConfigurationName(), "Default solution configuration"); + Assert.AreEqual(".NET", solution.GetDefaultPlatformName(), "Default solution platform"); + } + + /// + /// Helper method to create a SolutionFile object, and call it to parse the SLN file + /// represented by the string contents passed in. + /// + private static SolutionFile ParseSolutionHelper(string solutionFileContents) + { + solutionFileContents = solutionFileContents.Replace('\'', '"'); + string solutionPath = FileUtilities.GetTemporaryFile(".sln"); + + try + { + File.WriteAllText(solutionPath, solutionFileContents); + SolutionFile sp = SolutionFile.Parse(solutionPath); + return sp; + } + finally + { + File.Delete(solutionPath); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskBodyElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskBodyElement_Tests.cs new file mode 100644 index 00000000000..2deeb62503a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskBodyElement_Tests.cs @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectUsingTaskBodyElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectUsingTaskElement class + /// + [TestClass] + public class UsingTaskBodyElement_Tests + { + /// + /// Read simple task body + /// + [TestMethod] + public void ReadBody() + { + ProjectUsingTaskBodyElement body = GetBodyXml(); + + Assert.IsTrue(bool.FalseString.Equals(body.Evaluate, StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual("Contents", body.TaskBody); + } + + /// + /// Read task body with an invalid attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assert.Fail(); + } + + /// + /// Create a task body outside of a using task + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void CreateBodyOutsideUsingTask() + { + string content = @" + + + Contents + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + } + + /// + /// Set body value + /// + [TestMethod] + public void SetValue() + { + ProjectUsingTaskBodyElement body = GetBodyXml(); + Helpers.ClearDirtyFlag(body.ContainingProject); + + body.TaskBody = "MoreContents"; + Assert.AreEqual("MoreContents", body.TaskBody); + Assert.AreEqual(true, body.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set body value to empty + /// + [TestMethod] + public void SetEmptyValue() + { + ProjectUsingTaskBodyElement body = GetBodyXml(); + Helpers.ClearDirtyFlag(body.ContainingProject); + + body.TaskBody = String.Empty; + Assert.AreEqual(String.Empty, body.TaskBody); + Assert.AreEqual(true, body.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set body value to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullValue() + { + ProjectUsingTaskBodyElement body = GetBodyXml(); + body.TaskBody = null; + Assert.Fail(); + } + + /// + /// Verify setting the value of evaluate to null will wipe out the element and then the property will return true by default. + /// + [TestMethod] + public void SetEvaluateAttributeToNull() + { + ProjectUsingTaskBodyElement body = GetBodyXml(); + Assert.IsTrue(body.ContainingProject.RawXml.Contains("Evaluate")); + body.Evaluate = null; + Assert.IsTrue(!body.ContainingProject.RawXml.Contains("Evaluate")); + Assert.AreEqual(bool.TrueString, body.Evaluate); + } + + /// + /// Helper to get a ProjectUsingTaskBodyElement for a simple task + /// + private static ProjectUsingTaskBodyElement GetBodyXml() + { + string content = @" + + + Contents + + + "; + + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + ProjectUsingTaskBodyElement body = usingTask.TaskBody; + return body; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterElement_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterElement_Tests.cs new file mode 100644 index 00000000000..e0471386f17 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterElement_Tests.cs @@ -0,0 +1,242 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the UsingTaskParameterElement class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectUsingParameterElement class + /// + [TestClass] + public class UsingTaskParameterElement_Tests + { + /// + /// Parameter element with all attributes set + /// + private static string contentAllAttributesSet = @" + + + + + + + + "; + + /// + /// Parameter element with no attributes set + /// + private static string contentNoAttributesSet = @" + + + + + + + + "; + + /// + /// Read simple task body + /// + [TestMethod] + public void ReadParameterWithAllAttributes() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + + Assert.AreEqual("MyParameter", parameter.Name); + Assert.AreEqual("System.String", parameter.ParameterType); + Assert.AreEqual("true", parameter.Output); + Assert.AreEqual("false", parameter.Required); + } + + /// + /// Read simple task body + /// + [TestMethod] + public void ReadParameterWithNOAttributes() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentNoAttributesSet); + + Assert.AreEqual("MyParameter", parameter.Name); + Assert.AreEqual(typeof(String).FullName, parameter.ParameterType); + Assert.AreEqual(bool.FalseString, parameter.Output); + Assert.AreEqual(bool.FalseString, parameter.Required); + } + + /// + /// Read parameter with an invalid attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assert.Fail(); + } + + /// + /// Set type value + /// + [TestMethod] + public void SetType() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.ParameterType = "newType"; + Assert.AreEqual("newType", parameter.ParameterType); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set output value + /// + [TestMethod] + public void SetOutput() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.Output = "output"; + Assert.AreEqual("output", parameter.Output); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set required value + /// + [TestMethod] + public void SetRequired() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.Required = "required"; + Assert.AreEqual("required", parameter.Required); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set type type to empty + /// + [TestMethod] + public void SetEmptyType() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.ParameterType = String.Empty; + Assert.AreEqual(typeof(String).FullName, parameter.ParameterType); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set type output to empty + /// + [TestMethod] + public void SetEmptyOutput() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.Output = String.Empty; + Assert.AreEqual(bool.FalseString, parameter.Output); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set type required to empty + /// + [TestMethod] + public void SetEmptyRequired() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.Required = String.Empty; + Assert.AreEqual(bool.FalseString, parameter.Required); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set type type to null + /// + [TestMethod] + public void SetNullType() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.ParameterType = null; + Assert.AreEqual(typeof(String).FullName, parameter.ParameterType); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set type output to null + /// + [TestMethod] + public void SetNullOutput() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.Output = null; + Assert.AreEqual(bool.FalseString, parameter.Output); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Set type required to null + /// + [TestMethod] + public void SetNullRequired() + { + ProjectUsingTaskParameterElement parameter = GetParameterXml(contentAllAttributesSet); + Helpers.ClearDirtyFlag(parameter.ContainingProject); + + parameter.Required = null; + Assert.AreEqual(bool.FalseString, parameter.Required); + Assert.AreEqual(true, parameter.ContainingProject.HasUnsavedChanges); + } + + /// + /// Helper to get a UsingTaskParameterElement from xml + /// + private static ProjectUsingTaskParameterElement GetParameterXml(string contents) + { + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(contents))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + UsingTaskParameterGroupElement parameterGroup = usingTask.ParameterGroup; + ProjectUsingTaskParameterElement body = Helpers.GetFirst(parameterGroup.Parameters); + return body; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterGroup_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterGroup_Tests.cs new file mode 100644 index 00000000000..769accf3892 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/UsingTaskParameterGroup_Tests.cs @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the UsingTaskParameterGroupElement_Tests class. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Construction +{ + /// + /// Tests for the ProjectUsingParameterElement class + /// + [TestClass] + public class UsingTaskParameterGroup_Tests + { + /// + /// ParameterGroup with no parameters inside + /// + private static string contentEmptyParameterGroup = @" + + + + + + "; + + /// + /// ParameterGroup with duplicate child parameters + /// + private static string contentDuplicateParameters = @" + + + + + + + + + "; + + /// + /// ParameterGroup with multiple parameters + /// + private static string contentMultipleParameters = @" + + + + + + + + + "; + + /// + /// Read simple parameterGroup body + /// + [TestMethod] + public void ReadEmptyParameterGroup() + { + UsingTaskParameterGroupElement parameterGroup = GetParameterGroupXml(contentEmptyParameterGroup); + Assert.IsNotNull(parameterGroup); + Assert.AreEqual(0, parameterGroup.Count); + Assert.IsNull(parameterGroup.Parameters.GetEnumerator().Current); + } + + /// + /// Read simple parameterGroup body + /// + [TestMethod] + public void ReadMutipleParameters() + { + UsingTaskParameterGroupElement parameterGroup = GetParameterGroupXml(contentMultipleParameters); + Assert.IsNotNull(parameterGroup); + Assert.AreEqual(2, parameterGroup.Count); + Assert.IsNotNull(parameterGroup.Parameters); + + bool foundFirst = false; + bool foundSecond = false; + foreach (ProjectUsingTaskParameterElement parameter in parameterGroup.Parameters) + { + if (String.Equals("MyParameter1", parameter.Name, StringComparison.OrdinalIgnoreCase)) + { + foundFirst = true; + } + + if (String.Equals("MyParameter2", parameter.Name, StringComparison.OrdinalIgnoreCase)) + { + foundSecond = true; + } + } + + Assert.IsTrue(foundFirst); + Assert.IsTrue(foundSecond); + } + + /// + /// Read simple parameterGroup body + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadDuplicateChildParameters() + { + UsingTaskParameterGroupElement parameterGroup = GetParameterGroupXml(contentDuplicateParameters); + Assert.Fail(); + } + + /// + /// Read parameterGroup with a attribute + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadInvalidAttribute() + { + string content = @" + + + + + + "; + + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Assert.Fail(); + } + + /// + /// Helper to get a UsingTaskParameterGroupElement from xml + /// + private static UsingTaskParameterGroupElement GetParameterGroupXml(string contents) + { + ProjectRootElement project = ProjectRootElement.Create(XmlReader.Create(new StringReader(contents))); + ProjectUsingTaskElement usingTask = (ProjectUsingTaskElement)Helpers.GetFirst(project.Children); + return usingTask.ParameterGroup; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/DefinitionEditing_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/DefinitionEditing_Tests.cs new file mode 100644 index 00000000000..d87747caabc --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/DefinitionEditing_Tests.cs @@ -0,0 +1,2342 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for editing through the definition model. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for editing through the definition model + /// + [TestClass] + public class DefinitionEditing_Tests + { + /// + /// Add an item to an empty project + /// + [TestMethod] + public void AddItem() + { + Project project = new Project(); + + project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual(1, items.Count); + Assert.AreEqual("i", items[0].ItemType); + Assert.AreEqual("i1", items[0].EvaluatedInclude); + Assert.AreEqual("i1", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude); + Assert.AreEqual("i1", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Add an item to an empty project, where the include is escaped + /// + [TestMethod] + public void AddItem_EscapedItemInclude() + { + Project project = new Project(); + + project.AddItem("i", "i%281%29"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual(1, items.Count); + Assert.AreEqual("i", items[0].ItemType); + Assert.AreEqual("i(1)", items[0].EvaluatedInclude); + Assert.AreEqual("i(1)", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude); + Assert.AreEqual("i(1)", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Add an item with metadata + /// + [TestMethod] + public void AddItem_WithMetadata() + { + Project project = new Project(); + + List> metadata = new List>(); + metadata.Add(new KeyValuePair("m", "m1")); + + project.AddItem("i", "i1", metadata); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add an item with empty include. + /// Should throw. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AddItem_InvalidEmptyInclude() + { + Project project = new Project(); + + project.AddItem("i", String.Empty); + } + + /// + /// Add an item with null metadata parameter. + /// Should just add no metadata. + /// + [TestMethod] + public void AddItem_NullMetadata() + { + Project project = new Project(); + + project.AddItem("i", "i1", null); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add an item whose include has a property expression. As a convenience, we attempt to expand the + /// expression to create the evaluated include. + /// + [TestMethod] + public void AddItem_IncludeContainsPropertyExpression() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + project.ReevaluateIfNecessary(); + + project.AddItem("i", "$(p)"); + + Assert.AreEqual("$(p)", Helpers.GetFirst(project.Items).UnevaluatedInclude); + Assert.AreEqual("v1", Helpers.GetFirst(project.Items).EvaluatedInclude); + } + + /// + /// Add an item whose include has a wildcard. We attempt to expand the wildcard using the + /// file system. In this case, we have one entry in the project and two evaluated items. + /// + [TestMethod] + public void AddItem_IncludeContainsWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx", "i2.xxx"); + string wildcard = Path.Combine(Path.GetDirectoryName(paths[0]), "*.xxx;"); + + Project project = new Project(); + project.AddItem("i", wildcard); + + string expected = String.Format( + ObjectModelHelpers.CleanupFileContents( +@" + + + +"), + wildcard + ); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual(2, items.Count); + Assert.AreEqual(paths[0], items[0].EvaluatedInclude); + Assert.AreEqual(paths[1], items[1].EvaluatedInclude); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// Add an item whose include has an item expression. As a convenience, we attempt to expand the + /// expression to create the evaluated include. + /// This value will not be reliable until the project is reevaluated -- + /// for example, it assumes any items referenced are defined above this one. + /// + [TestMethod] + public void AddItem_IncludeContainsItemExpression() + { + Project project = new Project(); + project.AddItem("h", "h1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = project.AddItem("i", "@(h)")[0]; + + Assert.AreEqual("@(h)", item.UnevaluatedInclude); + Assert.AreEqual("h1", item.EvaluatedInclude); + } + + /// + /// Add an item whose include contains a wildcard but doesn't match anything. + /// + [TestMethod] + public void AddItem_ContainingWildcardNoMatches() + { + Project project = new Project(); + IList items = project.AddItem("i", @"c:\" + Guid.NewGuid().ToString() + @"\**\i1"); + + Assert.AreEqual(0, items.Count); + } + + /// + /// Add an item whose include contains a wildcard. + /// In this case we don't try to reuse an existing wildcard expression. + /// + [TestMethod] + public void AddItem_ContainingWildcardExistingWildcard() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItem("i", "*.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item whose include contains a semicolon. + /// In this case we don't try to reuse an existing wildcard expression. + /// + [TestMethod] + public void AddItem_ContainingSemicolonExistingWildcard() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItem("i", "i1.xxx;i2.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// If user tries to add a new item that has the same item name as an existing + /// wildcarded item, but the wildcard won't pick up the new file, then we + /// of course have to add the new item. + /// + [TestMethod] + public void AddItem_DoesntMatchWildcard() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItem("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// When the wildcarded item already in the project file has a Condition + /// on it, we don't try to match with it when a user tries to add a new + /// item to the project. + /// + [TestMethod] + public void AddItem_MatchesWildcardWithCondition() + { + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx"); + itemElement.Condition = "true"; + project.ReevaluateIfNecessary(); + + project.AddItem("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// When the wildcarded item already in the project file has a Exclude + /// on it, we don't try to match with it when a user tries to add a new + /// item to the project. + /// + [TestMethod] + public void AddItem_MatchesWildcardWithExclude() + { + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx"); + itemElement.Exclude = "i2.xxx"; + project.ReevaluateIfNecessary(); + + project.AddItem("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// that matches that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItem_MatchesWildcard() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItem("i", "i1.xxx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(true, Object.ReferenceEquals(item1, item2)); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// that matches that wildcard, except that its item type is different. + /// In this case, we ignore the existing wildcard. + /// + [TestMethod] + public void AddItem_MatchesWildcardButNotItemType() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItem("j", "j1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a complicated recursive wildcard in the project already, and the user tries to add an item + /// that matches that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItem_MatchesComplicatedWildcard() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", @"c:\subdir1\**\subdir2\**\*.x?x"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItem("i", @"c:\subdir1\a\b\subdir2\c\i1.xyx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(true, Object.ReferenceEquals(item1, item2)); + } + + /// + /// There's a complicated recursive wildcard in the project already, and the user tries to add an item + /// that doesn't match that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItem_DoesntMatchComplicatedWildcard() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", @"c:\subdir1\**\subdir2\**\*.x?x"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItem("i", @"c:\subdir1\a\b\c\i1.xyx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(false, Object.ReferenceEquals(item1, item2)); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// that matches that wildcard. In this case, we add a new item, because the old + /// one wasn't equivalent. + /// In contrast Orcas/Whidbey assumed that the user wants + /// that metadata on the new item, too. + /// + [TestMethod] + public void AddItem_DoesNotMatchWildcardWithMetadata() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx"); + item1.AddMetadata("m", "m1"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItem("i", "i1.xxx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// with metadata. In this case, we add a new item, because the old + /// one wasn't equivalent. + /// + [TestMethod] + public void AddItemWithMetadata_DoesNotMatchWildcardWithNoMetadata() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + Dictionary metadata = new Dictionary() { { "m", "m1" } }; + ProjectItemElement item2 = project.AddItem("i", "i1.xxx", metadata)[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, but it's part of a semicolon-separated + /// list of items. Now the user tries to add an item that matches that wildcard. + /// In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItem_MatchesWildcardInSemicolonList() + { + Project project = new Project(); + project.Xml.AddItem("i", "a;*.xxx;b"); + project.ReevaluateIfNecessary(); + + project.AddItem("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Modify an item originating in a wildcard by adding a new piece of metadata. + /// We should blow up the item in the project file. + /// + [TestMethod] + public void SetMetadata_ItemOriginatingWithWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx", "i2.xxx"); + string directory = Path.GetDirectoryName(paths[0]); + string wildcard = Path.Combine(directory, "*.xxx;"); + + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", wildcard); + itemElement.AddMetadata("m", "m0"); + project.ReevaluateIfNecessary(); + + Helpers.GetFirst(project.GetItems("i")).SetMetadataValue("n", "n1"); + + string expected = String.Format( + ObjectModelHelpers.CleanupFileContents( +@" + + + m0 + n1 + + + m0 + + +"), + paths[0], + paths[1] + ); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// Set a piece of metadata on an item originating from an item list expression. + /// We should blow up the expression and set the metadata on one of the resulting items. + /// + [TestMethod] + public void SetMetadata_ItemOriginatingWithItemList() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + +"))); + + Project project = new Project(content); + + Helpers.GetFirst(project.GetItems("i")).SetMetadataValue("m", "m2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + m2 + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Change the value on a piece of metadata on an item originating from an item list expression. + /// The ProjectMetadata object is shared by all the items here, so the edit does not cause any expansion. + /// + [TestMethod] + public void SetMetadataUnevaluatedValue_ItemOriginatingWithItemList() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + +"))); + + Project project = new Project(content); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m"); + metadatum.UnevaluatedValue = "m2"; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m2 + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Modify an item originating in a wildcard by removing a piece of metadata. + /// We should blow up the item in the project file. + /// + [TestMethod] + public void RemoveMetadata_ItemOriginatingWithWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx", "i2.xxx"); + string directory = Path.GetDirectoryName(paths[0]); + string wildcard = Path.Combine(directory, "*.xxx;"); + + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", wildcard); + itemElement.AddMetadata("m", "m1"); + project.ReevaluateIfNecessary(); + + Helpers.GetFirst(project.GetItems("i")).RemoveMetadata("m"); + + string expected = String.Format( + ObjectModelHelpers.CleanupFileContents( +@" + + + + m1 + + +"), + paths[0], + paths[1] + ); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// There's a wildcard in the project already, but it's part of a semicolon-separated + /// list of items, and it uses a property reference. Now the user tries to add a new + /// item that matches that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItem_MatchesWildcardWithPropertyReference() + { + Project project = new Project(); + project.SetProperty("p", "xxx"); + project.Xml.AddItem("i", "a;*.$(p);b"); + project.ReevaluateIfNecessary(); + + project.AddItem("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( + @" + +

xxx

+
+ + + +
"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, and the user renames an item such that it + /// now matches that wildcard. We don't try to do any thing clever like reuse that wildcard. + /// + [TestMethod] + public void RenameItem_MatchesWildcard() + { + Project project = new Project(); + project.AddItem("i", "*.xxx"); + project.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetLast(project.Items); + item.Rename("i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Rename, with the new name containing a property expression. + /// Because the rename did not cause more items to appear, it is possible + /// to update the EvaluatedInclude of this one. + /// + [TestMethod] + public void RenameItem_NewNameContainsPropertyExpression() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + project.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetFirst(project.Items); + + item.Rename("$(p)"); + + Assert.AreEqual("$(p)", item.UnevaluatedInclude); + + // Rename should have been expanded in this simple case + Assert.AreEqual("v1", item.EvaluatedInclude); + + // The ProjectItemElement should be the same + ProjectItemElement newItemElement = Helpers.GetFirst((Helpers.GetFirst(project.Xml.ItemGroups)).Items); + Assert.AreEqual(true, Object.ReferenceEquals(item.Xml, newItemElement)); + } + + /// + /// Rename, with the new name containing an item expression. + /// Because the rename did not cause more items to appear, it is possible + /// to update the EvaluatedInclude of this one. + /// + [TestMethod] + public void RenameItem_NewNameContainsItemExpression() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + project.AddItem("h", "h1"); + project.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetLast(project.Items); + + item.Rename("@(h)"); + + Assert.AreEqual("@(h)", item.UnevaluatedInclude); + + // Rename should have been expanded in this simple case + Assert.AreEqual("h1", item.EvaluatedInclude); + + // The ProjectItemElement should be the same + ProjectItemElement newItemElement = Helpers.GetLast((Helpers.GetLast(project.Xml.ItemGroups)).Items); + Assert.AreEqual(true, Object.ReferenceEquals(item.Xml, newItemElement)); + } + + /// + /// Rename, with the new name containing an item expression. + /// Because the new name expands to more than one item, we don't attempt to + /// update the evaluated include. + /// + [TestMethod] + public void RenameItem_NewNameContainsItemExpressionExpandingToTwoItems() + { + Project project = new Project(); + project.AddItem("h", "h1"); + project.AddItem("h", "h2"); + project.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetLast(project.Items); + + item.Rename("@(h)"); + + Assert.AreEqual("@(h)", item.UnevaluatedInclude); + Assert.AreEqual("@(h)", item.EvaluatedInclude); + + // The ProjectItemElement should be the same + ProjectItemElement newItemElement = Helpers.GetLast((Helpers.GetLast(project.Xml.ItemGroups)).Items); + Assert.AreEqual(true, Object.ReferenceEquals(item.Xml, newItemElement)); + } + + /// + /// Rename, with the new name containing an item expression. + /// Because the new name expands to not exactly one item, we don't attempt to + /// update the evaluated include. + /// Reasoning: The case we interested in for expansion here is setting something + /// like "$(sourcesroot)\foo.cs� and expanding that to a single item. + /// If say "@(foo)� is set as the new name, and it expands to blank, that might + /// be surprising to the host and maybe even unhandled, if on full reevaluation + /// it wouldn�t expand to blank. That�s why I�m being cautious and supporting + /// the most common scenario only. Many hosts will do a ReevaluateIfNecessary before reading anyway (including CPS) + /// + [TestMethod] + public void RenameItem_NewNameContainsItemExpressionExpandingToZeroItems() + { + Project project = new Project(); + project.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetLast(project.Items); + + item.Rename("@(h)"); + + Assert.AreEqual("@(h)", item.UnevaluatedInclude); + Assert.AreEqual("@(h)", item.EvaluatedInclude); + } + + /// + /// Rename an item that originated in an expression like "@(h)" + /// We should blow up the expression and rename the correct part. + /// + [TestMethod] + public void RenameItem_OriginatingWithItemList() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + +"))); + + Project project = new Project(content); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + item.Rename("h1b"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + m1 + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Rename an item that originated in an expression like "a.cs;b.cs" + /// We should blow up the expression and rename the correct part. + /// + [TestMethod] + public void RenameItem_OriginatingWithSemicolon() + { + Project project = new Project(); + project.Xml.AddItem("i", "i1;i2;i3"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.MakeList(project.GetItems("i"))[1]; + item.Rename("i2b"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Rename an item that originated in an expression like "a.cs;b.cs" + /// to a property expression. + /// We should blow up the expression and rename the correct part, + /// and because a split had to occur, we should not expand the expression. + /// + [TestMethod] + public void RenameItem_OriginatingWithSemicolonToExpandableExpression() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + project.Xml.AddItem("i", "i1;i2;i3"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.MakeList(project.GetItems("i"))[1]; + item.Rename("$(p)"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + +

v1

+
+ + + + + +
"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + Assert.AreEqual("$(p)", (Helpers.MakeList(project.Items))[1].EvaluatedInclude); + } + + /// + /// An item originates from a wildcard, and we rename it to something + /// that no longer matches the wildcard. This should cause the wildcard to be expanded. + /// + [TestMethod] + public void RenameItem_NoLongerMatchesWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx", "i2.xxx"); + string directory = Path.GetDirectoryName(paths[0]); + string wildcard = Path.Combine(directory, "*.xxx;"); + + Project project = new Project(); + project.Xml.AddItem("i", wildcard); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetFirst(project.Items); + item.Rename("i1.yyy"); + + string expected = String.Format( + ObjectModelHelpers.CleanupFileContents( +@" + + + + +"), + Path.Combine(directory, "i2.xxx") + ); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// An item originates from a wildcard, and we rename it to something + /// that still matches the wildcard. This should not modify the project. + /// + [TestMethod] + public void RenameItem_StillMatchesWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx"); + string directory = Path.GetDirectoryName(paths[0]); + string wildcard = Path.Combine(directory, "*.xxx;"); + + Project project = new Project(); + project.AddItem("i", wildcard); + project.ReevaluateIfNecessary(); + + string before = project.Xml.RawXml; + + ProjectItem item = Helpers.GetFirst(project.Items); + item.Rename(Path.Combine(directory, "i2.xxx")); + + Helpers.VerifyAssertLineByLine(before, project.Xml.RawXml); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// Change an item type. + /// + [TestMethod] + public void ChangeItemType() + { + Project project = new Project(); + project.AddItem("i", "i1"); + + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + item.ItemType = "j"; + + Assert.AreEqual("j", item.ItemType); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + ProjectItemGroupElement itemGroupElement = Helpers.GetFirst(project.Xml.ItemGroups); + Assert.AreEqual(1, Helpers.MakeList(itemGroupElement.Items).Count); + Assert.AreEqual(true, Object.ReferenceEquals(itemGroupElement, item.Xml.Parent)); + + Assert.AreEqual(1, Helpers.MakeList(project.Items).Count); + Assert.AreEqual(1, Helpers.MakeList(project.ItemsIgnoringCondition).Count); + + Assert.AreEqual(0, Helpers.MakeList(project.GetItems("i")).Count); + Assert.AreEqual(0, Helpers.MakeList(project.GetItemsIgnoringCondition("i")).Count); + + Assert.AreEqual(true, Object.ReferenceEquals(item, Helpers.GetFirst(project.GetItems("j")))); + Assert.AreEqual(true, Object.ReferenceEquals(item, Helpers.GetFirst(project.GetItemsIgnoringCondition("j")))); + Assert.AreEqual(true, Object.ReferenceEquals(item, Helpers.GetFirst(project.GetItemsByEvaluatedInclude("i1")))); + } + + /// + /// Change an item type; metadata should stay in place + /// + [TestMethod] + public void ChangeItemTypeOnItemWithMetadata() + { + Project project = new Project(); + ProjectItem item0 = project.AddItem("i", "i1")[0]; + item0.Xml.Exclude = "e"; + ProjectMetadataElement metadatumElement1 = item0.SetMetadataValue("m", "m1").Xml; + metadatumElement1.Condition = "true"; + item0.SetMetadataValue("n", "n1"); + + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + item.ItemType = "j"; + + Assert.AreEqual("j", item.ItemType); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + n1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + // Item element identity changed unfortunately, but metadata elements should be the same objects. + ProjectItemElement itemElement = Helpers.GetFirst(Helpers.GetFirst(project.Xml.ItemGroups).Items); + Assert.AreEqual(true, Object.ReferenceEquals(itemElement, metadatumElement1.Parent)); + + Assert.AreEqual(2, Helpers.MakeList(itemElement.Metadata).Count); + + Assert.AreEqual(2 + 15 /* built-in metadata */, item.MetadataCount); + Assert.AreEqual("n1", item.GetMetadataValue("n")); + + // Remove one piece of metadata, to hopefully help verify that the DOM is in a good state + item.RemoveMetadata("m"); + + expected = ObjectModelHelpers.CleanupFileContents( +@" + + + n1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Change an item type where the item needs blowing up first. + /// + [TestMethod] + public void ChangeItemTypeOnItemNeedingSplitting() + { + Project project = new Project(); + project.Xml.AddItem("i", "i1;i2"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + item.ItemType = "j"; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + ProjectItemGroupElement itemGroupElement = Helpers.GetFirst(project.Xml.ItemGroups); + Assert.AreEqual(2, Helpers.MakeList(itemGroupElement.Items).Count); + Assert.AreEqual(true, Object.ReferenceEquals(itemGroupElement, item.Xml.Parent)); + Assert.AreEqual(true, Object.ReferenceEquals(itemGroupElement, Helpers.GetFirst(project.GetItems("i")).Xml.Parent)); + } + + /// + /// Remove an item, clearing up the empty item group as well + /// + [TestMethod] + public void RemoveItem() + { + Project project = new Project(); + project.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + project.RemoveItem(Helpers.GetFirst(project.GetItems("i"))); + + string expected = ObjectModelHelpers.CleanupFileContents(@""); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + Assert.AreEqual(0, Helpers.Count(project.Items)); + Assert.AreEqual(0, Helpers.MakeList(project.CreateProjectInstance().GetItems("i")).Count); + } + + /// + /// Remove an item that originated in an expression like "@(h)" + /// We should expand the expression to the remaining items, if any. + /// Metadata should be preserved. + /// + [TestMethod] + public void RemoveItem_OriginatingWithItemList() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + +"))); + + Project project = new Project(content); + + project.RemoveItem(Helpers.GetFirst(project.GetItems("i"))); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Remove an item that originated in an expression like "a.cs;b.cs" + /// We should keep the part of the expression that still applies. + /// + [TestMethod] + public void RemoveItem_OriginatingWithSemicolon() + { + Project project = new Project(); + project.Xml.AddItem("i", "i1;i2"); + project.ReevaluateIfNecessary(); + + project.RemoveItem(Helpers.GetFirst(project.GetItems("i"))); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Remove an item originating from a wildcard + /// This should cause the wildcard to be expanded to the remaining items, if any. + /// Expanding the wildcard should preserve the metadata on it. + /// + [TestMethod] + public void RemoveItem_OriginatingWithWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx", "i2.xxx"); + string directory = Path.GetDirectoryName(paths[0]); + string wildcard = Path.Combine(directory, "*.xxx;"); + + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", wildcard); + itemElement.AddMetadata("m", "m1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = Helpers.GetFirst(project.Items); + project.RemoveItem(item); + + string expected = String.Format( + ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +" + ), + Path.Combine(directory, "i2.xxx")); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// Items in certain locations are stored by the project despite having a false condition -- eg for populating the solution explorer. + /// Removing an item should remove it from this list too. + /// + [TestMethod] + public void RemoveItem_IncludingFromIgnoringConditionList() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + +"))); + + Project project = new Project(content); + + Assert.AreEqual(0, Helpers.MakeList(project.GetItems("i")).Count); + List itemsIgnoringCondition = Helpers.MakeList(project.GetItemsIgnoringCondition("i")); + Assert.AreEqual(1, itemsIgnoringCondition.Count); + ProjectItem item = itemsIgnoringCondition[0]; + Assert.AreEqual("i1", item.EvaluatedInclude); + + bool result = project.RemoveItem(item); + + Assert.AreEqual(false, result); // false as it was not in the regular items collection + itemsIgnoringCondition = Helpers.MakeList(project.GetItemsIgnoringCondition("i")); + Assert.AreEqual(0, itemsIgnoringCondition.Count); + } + + /// + /// Test simple property set with name and value + /// + [TestMethod] + public void SetProperty() + { + Project project = new Project(); + int environmentPropertyCount = Helpers.MakeList(project.Properties).Count; + + project.SetProperty("p1", "v1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + v1 + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + Assert.AreEqual("v1", project.GetPropertyValue("p1")); + Assert.AreEqual("v1", project.CreateProjectInstance().GetPropertyValue("p1")); + Assert.AreEqual(1, Helpers.Count(project.Properties) - environmentPropertyCount); + } + + /// + /// Test simple property set with name and value, where the value is escaped + /// + [TestMethod] + public void SetProperty_EscapedValue() + { + Project project = new Project(); + int environmentPropertyCount = Helpers.MakeList(project.Properties).Count; + + project.SetProperty("p1", "v%5E1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + v%5E1 + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + Assert.AreEqual("v^1", project.GetPropertyValue("p1")); + Assert.AreEqual("v^1", project.CreateProjectInstance().GetPropertyValue("p1")); + Assert.AreEqual(1, Helpers.Count(project.Properties) - environmentPropertyCount); + } + + /// + /// Setting a property that originates in an import should not try to edit the property there. + /// It should set it in the main project file. + /// + [TestMethod] + public void SetPropertyOriginatingInImport() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport("$(msbuildtoolspath)\\microsoft.common.targets"); + Project project = new Project(xml); + + // This property certainly exists in that imported file + project.SetProperty("OutDir", "foo"); // should not throw + + Assert.AreEqual("foo", project.GetPropertyValue("OutDir")); + Assert.AreEqual(1, Helpers.MakeList(xml.Properties).Count); + } + + /// + /// Verify properties are expanded in new property values + /// + [TestMethod] + public void SetPropertyWithPropertyExpression() + { + Project project = new Project(); + project.SetProperty("p0", "v0"); + project.SetProperty("p1", "$(p0)"); + + Assert.AreEqual("v0", project.GetPropertyValue("p1")); + } + + /// + /// Verify item expressions are not expanded in new property values. + /// NOTE: They aren't expanded to "blank". It just seems like that, because + /// when you output them, item expansion happens after property expansion, and + /// they may evaluate to blank then. (Unless items do exist at that point.) + /// + [TestMethod] + public void SetPropertyWithItemExpression() + { + Project project = new Project(); + project.AddItem("i", "i1"); + project.SetProperty("p1", "x@(i)x%(m)x"); + + Assert.AreEqual("x@(i)x%(m)x", project.GetPropertyValue("p1")); + } + + /// + /// Setting a property to the same exact unevaluated and evaluated value + /// should not dirty the project. + /// (VS seems to do this a lot.) + /// + [TestMethod] + public void SetPropertyWithNoChangesShouldNotDirty() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + + project.SetProperty("p", "v1"); + Assert.AreEqual(false, project.IsDirty); + } + + /// + /// Setting an evaluated property after its XML has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetPropertyAfterRemoved() + { + Project project = new Project(); + var property = project.SetProperty("p", "v1"); + property.Xml.Parent.RemoveAllChildren(); + property.UnevaluatedValue = "v2"; + } + + /// + /// Setting an evaluated property after its XML's parent has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetPropertyAfterRemoved2() + { + Project project = new Project(); + var property = project.SetProperty("p", "v1"); + property.Xml.Parent.Parent.RemoveAllChildren(); + property.UnevaluatedValue = "v2"; + } + + /// + /// Setting an evaluated metadatum after its XML has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetMetadatumAfterRemoved() + { + Project project = new Project(); + var metadatum = project.AddItem("i", "i1")[0].SetMetadataValue("p", "v1"); + metadatum.Xml.Parent.RemoveAllChildren(); + metadatum.UnevaluatedValue = "v2"; + } + + /// + /// Changing an item's type after its XML has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetItemTypeAfterRemoved() + { + Project project = new Project(); + var item = project.AddItem("i", "i1")[0]; + item.Xml.Parent.RemoveAllChildren(); + item.ItemType = "j"; + } + + /// + /// Changing an item's type after its XML has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void RemoveMetadataAfterItemRemoved() + { + Project project = new Project(); + var item = project.AddItem("i", "i1")[0]; + item.Xml.Parent.RemoveAllChildren(); + item.RemoveMetadata("m"); + } + + /// + /// Setting an evaluated metadatum after its XML's parent has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetMetadatumAfterRemoved2() + { + Project project = new Project(); + var metadatum = project.AddItem("i", "i1")[0].SetMetadataValue("p", "v1"); + metadatum.Xml.Parent.Parent.RemoveAllChildren(); + metadatum.UnevaluatedValue = "v2"; + } + + /// + /// Setting an evaluated metadatum after its XML's parent's parent has been removed should + /// fail. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetMetadatumAfterRemoved3() + { + Project project = new Project(); + var metadatum = project.AddItem("i", "i1")[0].SetMetadataValue("p", "v1"); + metadatum.Xml.Parent.Parent.Parent.RemoveAllChildren(); + metadatum.UnevaluatedValue = "v2"; + } + + /// + /// After removing an appropriate item group's XML without reevaluation an item is added; + /// it should go in a new one + /// + [TestMethod] + public void AddItemAfterAppropriateItemGroupRemoved() + { + Project project = new Project(); + project.AddItem("i", "i1"); + project.Xml.ItemGroups.First().Parent.RemoveAllChildren(); + project.AddItem("i", "i2"); + + Assert.AreEqual(1, project.Xml.Items.Count()); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual(1, project.Items.Count()); + } + + /// + /// Setting a property after an equivalent's XML has been removed without reevaluation, should + /// still work. + /// + [TestMethod] + public void SetNewPropertyAfterEquivalentRemoved() + { + Project project = new Project(); + var property = project.SetProperty("p", "v1"); + property.Xml.Parent.RemoveAllChildren(); + project.SetProperty("p", "v2"); + + Assert.AreEqual(1, project.Xml.Properties.Count()); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual("v2", project.GetPropertyValue("p")); + } + + /// + /// Setting a property after an equivalent's XML's parent has been removed without reevaluation, should + /// still work. + /// + [TestMethod] + public void SetNewPropertyAfterEquivalentsParentRemoved() + { + Project project = new Project(); + var property = project.SetProperty("p", "v1"); + property.Xml.Parent.Parent.RemoveAllChildren(); + project.SetProperty("p", "v2"); + + Assert.AreEqual(1, project.Xml.Properties.Count()); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual("v2", project.GetPropertyValue("p")); + } + + /// + /// Test removing a property. Parent empty group should also be removed. + /// + [TestMethod] + public void RemoveProperty() + { + Project project = new Project(); + int environmentPropertyCount = Helpers.MakeList(project.Properties).Count; + + project.SetProperty("p1", "v1"); + project.ReevaluateIfNecessary(); + + project.RemoveProperty(project.GetProperty("p1")); + + string expected = ObjectModelHelpers.CleanupFileContents(@""); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + Assert.AreEqual(null, project.GetProperty("p1")); + ProjectInstance instance = project.CreateProjectInstance(); + Assert.AreEqual(String.Empty, instance.GetPropertyValue("p1")); + Assert.AreEqual(0, Helpers.Count(project.Properties) - environmentPropertyCount); + } + + /// + /// Test removing a property. Other property should not be disturbed. + /// + [TestMethod] + public void RemovePropertyWithSibling() + { + Project project = new Project(); + project.SetProperty("p1", "v1"); + project.SetProperty("p2", "v2"); + project.ReevaluateIfNecessary(); + + project.RemoveProperty(project.GetProperty("p1")); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + v2 + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add metadata to an existing item + /// + [TestMethod] + public void AddMetadata() + { + Project project = new Project(); + + ProjectItem item = project.AddItem("i", "i1")[0]; + + item.SetMetadataValue("m", "m1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual("m1", items[0].GetMetadataValue("m")); + Assert.AreEqual("m1", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].GetMetadataValue("m")); + } + + /// + /// Add metadata to an existing item + /// + [TestMethod] + public void AddMetadata_EscapedValue() + { + Project project = new Project(); + + ProjectItem item = project.AddItem("i", "i1")[0]; + + item.SetMetadataValue("m", "m1%24%24"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1%24%24 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual("m1$$", items[0].GetMetadataValue("m")); + Assert.AreEqual("m1$$", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].GetMetadataValue("m")); + } + + /// + /// Add metadata to an existing item that has existing metadata with that name. + /// Should replace it. + /// + [TestMethod] + public void AddMetadata_Existing() + { + Project project = new Project(); + + ProjectItem item = project.AddItem("i", "i1")[0]; + + ProjectMetadata metadatum1 = item.SetMetadataValue("m", "m1"); + ProjectMetadata metadatum2 = item.SetMetadataValue("m", "m2"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m2 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual("m2", items[0].GetMetadataValue("m")); + Assert.AreEqual(true, Object.ReferenceEquals(metadatum1, metadatum2)); + } + + /// + /// Add an item whose include expands to several items. + /// Even without reevaluation, we should get two items. + /// + [TestMethod] + public void AddItem_ExpandsToSeveral() + { + Project project = new Project(); + IList items = project.AddItem("i", "a;b"); + + Assert.AreEqual(true, Object.ReferenceEquals(items[0].Xml, items[1].Xml)); + Assert.AreEqual("a;b", items[0].UnevaluatedInclude); + + items = Helpers.MakeList(project.Items); + Assert.AreEqual("a", items[0].EvaluatedInclude); + Assert.AreEqual("b", items[1].EvaluatedInclude); + } + + /// + /// Add an item expanding to several, with metadata + /// + [TestMethod] + public void AddItem_ExpandsToSeveralWithMetadata() + { + Project project = new Project(); + + List> metadata = new List>(); + metadata.Add(new KeyValuePair("m", "m1")); + + project.AddItem("i", "i1;i2", metadata); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add metadata that would be modified by evaluation. + /// Should be evaluated on a best-effort basis. + /// + [TestMethod] + public void AddMetadata_Reevaluation() + { + XmlReader content = XmlReader.Create(new StringReader(ObjectModelHelpers.CleanupFileContents( +@" + + + l1 + m1 + + +"))); + + Project project = new Project(content); + + ProjectItem item = Helpers.GetFirst(project.Items); + + ProjectMetadata metadatum = item.SetMetadataValue("m", "%(l)"); + + Assert.AreEqual("l1", item.GetMetadata("m").EvaluatedValue); + Assert.AreEqual("%(l)", item.GetMetadata("m").Xml.Value); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + l1 + %(l) + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + project.ReevaluateIfNecessary(); + + item = Helpers.GetFirst(project.Items); + + Assert.AreEqual("l1", item.GetMetadata("m").EvaluatedValue); + Assert.AreEqual("%(l)", item.GetMetadata("m").Xml.Value); + } + + /// + /// Add a new piece of item definition metadatum and update an existing one. + /// The new piece has to go in an entirely new item definition. + /// + [TestMethod] + public void AddMetadatumToItemDefinition() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0"); + Project project = new Project(xml); + + ProjectItemDefinition definition = project.ItemDefinitions["i"]; + definition.SetMetadataValue("m", "m1"); + definition.SetMetadataValue("n", "n0"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + n0 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add an item to an empty project + /// + [TestMethod] + public void AddItemFast() + { + Project project = new Project(); + + project.AddItemFast("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual(1, items.Count); + Assert.AreEqual("i", items[0].ItemType); + Assert.AreEqual("i1", items[0].EvaluatedInclude); + Assert.AreEqual("i1", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude); + Assert.AreEqual("i1", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Add an item to an empty project, where the include is escaped + /// + [TestMethod] + public void AddItemFast_EscapedItemInclude() + { + Project project = new Project(); + + project.AddItemFast("i", "i%281%29"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual(1, items.Count); + Assert.AreEqual("i", items[0].ItemType); + Assert.AreEqual("i(1)", items[0].EvaluatedInclude); + Assert.AreEqual("i(1)", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude); + Assert.AreEqual("i(1)", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Add an item with metadata + /// + [TestMethod] + public void AddItemFast_WithMetadata() + { + Project project = new Project(); + + List> metadata = new List>(); + metadata.Add(new KeyValuePair("m", "m1")); + + project.AddItemFast("i", "i1", metadata); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add an item with empty include. + /// Should throw. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AddItemFast_InvalidEmptyInclude() + { + Project project = new Project(); + + project.AddItemFast("i", String.Empty); + } + + /// + /// Add an item with null metadata parameter. + /// Should just add no metadata. + /// + [TestMethod] + public void AddItemFast_NullMetadata() + { + Project project = new Project(); + + project.AddItemFast("i", "i1", null); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Add an item whose include has a property expression. As a convenience, we attempt to expand the + /// expression to create the evaluated include. + /// + [TestMethod] + public void AddItemFast_IncludeContainsPropertyExpression() + { + Project project = new Project(); + project.SetProperty("p", "v1"); + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "$(p)"); + + Assert.AreEqual("$(p)", Helpers.GetFirst(project.Items).UnevaluatedInclude); + Assert.AreEqual("v1", Helpers.GetFirst(project.Items).EvaluatedInclude); + } + + /// + /// Add an item whose include has a wildcard. We attempt to expand the wildcard using the + /// file system. In this case, we have one entry in the project and two evaluated items. + /// + [TestMethod] + public void AddItemFast_IncludeContainsWildcard() + { + string[] paths = null; + + try + { + paths = Helpers.CreateFiles("i1.xxx", "i2.xxx"); + string wildcard = Path.Combine(Path.GetDirectoryName(paths[0]), "*.xxx;"); + + Project project = new Project(); + project.AddItemFast("i", wildcard); + + string expected = String.Format + ( + ObjectModelHelpers.CleanupFileContents( + @" + + + + " + ), + wildcard + ); + + List items = Helpers.MakeList(project.Items); + Assert.AreEqual(2, items.Count); + Assert.AreEqual(paths[0], items[0].EvaluatedInclude); + Assert.AreEqual(paths[1], items[1].EvaluatedInclude); + } + finally + { + Helpers.DeleteFiles(paths); + } + } + + /// + /// Add an item whose include has an item expression. As a convenience, we attempt to expand the + /// expression to create the evaluated include. + /// This value will not be reliable until the project is reevaluated -- + /// for example, it assumes any items referenced are defined above this one. + /// + [TestMethod] + public void AddItemFast_IncludeContainsItemExpression() + { + Project project = new Project(); + project.AddItemFast("h", "h1"); + project.ReevaluateIfNecessary(); + + ProjectItem item = project.AddItemFast("i", "@(h)")[0]; + + Assert.AreEqual("@(h)", item.UnevaluatedInclude); + Assert.AreEqual("h1", item.EvaluatedInclude); + } + + /// + /// Add an item whose include contains a wildcard but doesn't match anything. + /// + [TestMethod] + public void AddItemFast_ContainingWildcardNoMatches() + { + Project project = new Project(); + IList items = project.AddItemFast("i", @"c:\" + Guid.NewGuid().ToString() + @"\**\i1"); + + Assert.AreEqual(0, items.Count); + } + + /// + /// Add an item whose include contains a wildcard. + /// In this case we don't try to reuse an existing wildcard expression. + /// + [TestMethod] + public void AddItemFast_ContainingWildcardExistingWildcard() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "*.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// Add an item whose include contains a semicolon. + /// In this case we don't try to reuse an existing wildcard expression. + /// + [TestMethod] + public void AddItemFast_ContainingSemicolonExistingWildcard() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "i1.xxx;i2.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// If user tries to add a new item that has the same item name as an existing + /// wildcarded item, but the wildcard won't pick up the new file, then we + /// of course have to add the new item. + /// + [TestMethod] + public void AddItemFast_DoesntMatchWildcard() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "i1"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// When the wildcarded item already in the project file has a Condition + /// on it, we don't try to match with it when a user tries to add a new + /// item to the project. + /// + [TestMethod] + public void AddItemFast_MatchesWildcardWithCondition() + { + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx"); + itemElement.Condition = "true"; + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// When the wildcarded item already in the project file has a Exclude + /// on it, we don't try to match with it when a user tries to add a new + /// item to the project. + /// + [TestMethod] + public void AddItemFast_MatchesWildcardWithExclude() + { + Project project = new Project(); + ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx"); + itemElement.Exclude = "i2.xxx"; + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// that matches that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItemFast_MatchesWildcard() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItemFast("i", "i1.xxx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(true, Object.ReferenceEquals(item1, item2)); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// that matches that wildcard, except that its item type is different. + /// In this case, we ignore the existing wildcard. + /// + [TestMethod] + public void AddItemFast_MatchesWildcardButNotItemType() + { + Project project = new Project(); + project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + project.AddItemFast("j", "j1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a complicated recursive wildcard in the project already, and the user tries to add an item + /// that matches that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItemFast_MatchesComplicatedWildcard() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", @"c:\subdir1\**\subdir2\**\*.x?x"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItemFast("i", @"c:\subdir1\a\b\subdir2\c\i1.xyx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(true, Object.ReferenceEquals(item1, item2)); + } + + /// + /// There's a complicated recursive wildcard in the project already, and the user tries to add an item + /// that doesn't match that wildcard. In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItemFast_DoesntMatchComplicatedWildcard() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", @"c:\subdir1\**\subdir2\**\*.x?x"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItemFast("i", @"c:\subdir1\a\b\c\i1.xyx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + Assert.AreEqual(false, Object.ReferenceEquals(item1, item2)); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// that matches that wildcard. In this case, we add a new item, because the old + /// one wasn't equivalent. + /// In contrast Orcas/Whidbey assumed that the user wants + /// that metadata on the new item, too. + /// + [TestMethod] + public void AddItemFast_DoesNotMatchWildcardWithMetadata() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx"); + item1.AddMetadata("m", "m1"); + project.ReevaluateIfNecessary(); + + ProjectItemElement item2 = project.AddItemFast("i", "i1.xxx")[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, and the user tries to add an item + /// with metadata. In this case, we add a new item, because the old + /// one wasn't equivalent. + /// + [TestMethod] + public void AddItemFastWithMetadata_DoesNotMatchWildcardWithNoMetadata() + { + Project project = new Project(); + ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx"); + project.ReevaluateIfNecessary(); + + Dictionary metadata = new Dictionary() { { "m", "m1" } }; + ProjectItemElement item2 = project.AddItemFast("i", "i1.xxx", metadata)[0].Xml; + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + + m1 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + + /// + /// There's a wildcard in the project already, but it's part of a semicolon-separated + /// list of items. Now the user tries to add an item that matches that wildcard. + /// In this case, we don't touch the project at all. + /// + [TestMethod] + public void AddItemFast_MatchesWildcardInSemicolonList() + { + Project project = new Project(); + project.Xml.AddItem("i", "a;*.xxx;b"); + project.ReevaluateIfNecessary(); + + project.AddItemFast("i", "i1.xxx"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + +"); + + Helpers.VerifyAssertProjectContent(expected, project); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/EditingElementsReferencedByOrReferences_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/EditingElementsReferencedByOrReferences_Tests.cs new file mode 100644 index 00000000000..d5f634cd43e --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/EditingElementsReferencedByOrReferences_Tests.cs @@ -0,0 +1,352 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for editing elements that are related to other XML elements +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests around editing elements that are referenced by others or the ones that references others. + /// + [TestClass] + public class EditingElementsReferencedByOrReferences_Tests + { + /// + /// Changes the item type on an item used with the at operator. + /// + [TestMethod] + public void ChangeItemTypeInReferencedItem() + { + Project project = GetProject( +@" + + + + +"); + + ProjectItem item = project.GetItems("I").Where(i => i.UnevaluatedInclude == "X").First(); + item.ItemType = "J"; + + string expected = +@" + + + + +"; + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + project.ReevaluateIfNecessary(); + IEnumerable items = project.GetItems("I"); + + Assert.AreEqual(1, items.Count(), "Wrong number of items after changing type"); + Assert.AreEqual("Y", items.First().EvaluatedInclude, "Wrong evaluated include after changing type"); + } + + /// + /// Removes an item in a ; separated list. It blows up the list. + /// + [TestMethod] + public void RemoveItemInList() + { + Project project = GetProject( +@" + + + + +"); + + ProjectItem item = project.GetItems("I").Where(i => i.EvaluatedInclude == "Y").First(); + project.RemoveItem(item); + + string expected = +@" + + + + + +"; + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Renames an item in a ; separated list. It blows up the list. + /// + [TestMethod] + public void RenameItemInList() + { + Project project = GetProject( +@" + + + + +"); + + ProjectItem item = project.GetItems("I").Where(i => i.EvaluatedInclude == "Y").First(); + item.Rename("Z"); + + string expected = +@" + + + + + +"; + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Removes metadata duplicated in item. + /// + [TestMethod] + public void RemoveMetadata1() + { + Project project = GetProject( +@" + + + A + + + + + %(M);B + %(M);C + + + %(M);D + + +"); + + ProjectItem item1 = project.GetItems("I").Where(i => i.EvaluatedInclude == "X").First(); + Assert.AreEqual("A;B;C", item1.GetMetadataValue("M"), "Invalid metadata at start"); + + ProjectItem item2 = project.GetItems("I").Where(i => i.EvaluatedInclude == "Y").First(); + Assert.AreEqual("A;D", item2.GetMetadataValue("M"), "Invalid metadata at start"); + + item1.RemoveMetadata("M"); + + string expected = +@" + + + A + + + + + %(M);B + + + %(M);D + + +"; + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Removes duplicated metadata and checks evaluation. + /// + [TestMethod] + public void RemoveMetadata2() + { + Project project = GetProject( +@" + + + A + + + + + %(M);B + %(M);C + + + %(M);D + + +"); + + ProjectItem item1 = project.GetItems("I").Where(i => i.EvaluatedInclude == "X").First(); + item1.RemoveMetadata("M"); + + string expected = +@" + + + A + + + + + %(M);B + + + %(M);D + + +"; + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + project.ReevaluateIfNecessary(); + item1 = project.GetItems("I").Where(i => i.EvaluatedInclude == "X").First(); + Assert.AreEqual("A;B", item1.GetMetadataValue("M"), "Invalid metadata after first removal"); + ProjectItem item2 = project.GetItems("I").Where(i => i.EvaluatedInclude == "Y").First(); + Assert.AreEqual("A;D", item2.GetMetadataValue("M"), "Invalid metadata after first removal"); + } + + /// + /// Removes metadata but still keep inherited one from item definition. + /// + [TestMethod] + public void RemoveMetadata3() + { + Project project = GetProject( + @" + + + A + + + + + %(M);B + %(M);C + + + %(M);D + + +"); + + ProjectItem item1 = project.GetItems("I").Where(i => i.EvaluatedInclude == "X").First(); + item1.RemoveMetadata("M"); + + project.ReevaluateIfNecessary(); + ProjectItem item2 = project.GetItems("I").Where(i => i.EvaluatedInclude == "Y").First(); + item2.RemoveMetadata("M"); + + string expected = +@" + + + A + + + + + %(M);B + + + +"; + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + project.ReevaluateIfNecessary(); + item1 = project.GetItems("I").Where(i => i.EvaluatedInclude == "X").First(); + Assert.AreEqual("A;B", item1.GetMetadataValue("M"), "Invalid metadata after second removal"); + item2 = project.GetItems("I").Where(i => i.EvaluatedInclude == "Y").First(); + Assert.AreEqual("A", item2.GetMetadataValue("M"), "Invalid metadata after second removal"); + } + + /// + /// Removes metadata referenced with % qualification. + /// + [TestMethod] + public void RemoveReferencedMetadata() + { + Project project = GetProject( +@" + + + m + %(I.M) + + +"); + + ProjectItem item = project.GetItems("I").First(); + Assert.AreEqual("m", item.GetMetadataValue("N"), "Wrong metadata value at startup"); + + item.RemoveMetadata("M"); + + string expected = +@" + + + %(I.M) + + +"; + Helpers.VerifyAssertProjectContent(expected, project.Xml); + + project.ReevaluateIfNecessary(); + item = project.GetItems("I").First(); + ProjectMetadata metadata = item.GetMetadata("N"); + + Assert.AreEqual("%(I.M)", metadata.UnevaluatedValue, "Unevaluated value is wrong"); + Assert.AreEqual(String.Empty, metadata.EvaluatedValue, "Evaluated value is wrong"); + } + + /// + /// Removes duplicated property. + /// + [TestMethod] + public void RemoveProperty() + { + Project project = GetProject( +@" + +

A

+

$(P)B

+
+
"); + + ProjectProperty property = project.GetProperty("P"); + project.RemoveProperty(property); + + string expected = +@" + +

A

+
+
"; + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Creates a new project the given contents. + /// + /// The contents for the project. + /// The project contents. + private Project GetProject(string contents) + { + return new Project(XmlReader.Create(new StringReader(contents))); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectCollection_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectCollection_Tests.cs new file mode 100644 index 00000000000..cbc9906ee95 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectCollection_Tests.cs @@ -0,0 +1,1443 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectCollection +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for ProjectCollection + /// + [TestClass] + public class ProjectCollection_Tests + { + /// + /// Gets or sets the test context. + /// + public TestContext TestContext { get; set; } + + /// + /// Clear out the global project collection + /// + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Clear out the global project collection + /// + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.Count); + + IDictionary globalProperties = ProjectCollection.GlobalProjectCollection.GlobalProperties; + foreach (string propertyName in globalProperties.Keys) + { + ProjectCollection.GlobalProjectCollection.RemoveGlobalProperty(propertyName); + } + + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.GlobalProperties.Count); + } + + /// + /// Add a single project from disk and verify it's put in the global project collection + /// + [TestMethod] + public void AddProjectFromDisk() + { + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement xml = ProjectRootElement.Create(path); + xml.Save(); + + Project project = new Project(path); + + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject(path); + Assert.AreEqual(true, Object.ReferenceEquals(project, project2)); + } + finally + { + File.Delete(path); + } + } + + /// + /// When an unnamed project is saved, it gets a name, and should be entered into + /// the appropriate project collection. + /// + [TestMethod] + public void AddProjectOnSave() + { + string path = null; + + try + { + Project project = new Project(); + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.Count); + + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + project.Save(path); + + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject(path); + Assert.AreEqual(true, Object.ReferenceEquals(project, project2)); + } + finally + { + File.Delete(path); + } + } + + /// + /// When an unnamed project is saved, it gets a name, and should be entered into + /// the appropriate project collection. + /// + [TestMethod] + public void AddProjectOnSave_SpecifiedProjectCollection() + { + string path = null; + + try + { + ProjectCollection collection = new ProjectCollection(); + Project project = new Project(collection); + + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + project.Save(path); + + Project project2 = collection.LoadProject(path); + Assert.AreEqual(true, Object.ReferenceEquals(project, project2)); + } + finally + { + File.Delete(path); + } + } + + /// + /// When an unnamed project is given a name, it should be entered into its + /// project collection. + /// + [TestMethod] + public void AddProjectOnSetName() + { + Project project = new Project(); + project.FullPath = "c:\\x"; + + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject("c:\\x"); + Assert.AreEqual(true, Object.ReferenceEquals(project, project2)); + } + + /// + /// Loading a project from a file inherits the project collection's global properties + /// + [TestMethod] + public void GlobalPropertyInheritLoadFromFile() + { + string path = null; + + try + { + path = CreateProjectFile(); + + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("p", "v"); + Project project = collection.LoadProject(path); + + Assert.AreEqual("v", project.GlobalProperties["p"]); + } + finally + { + File.Delete(path); + } + } + + /// + /// Loading a project from a file inherits the project collection's global properties + /// + [TestMethod] + public void GlobalPropertyInheritLoadFromFile2() + { + string path = null; + + try + { + path = CreateProjectFile(); + + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("p", "v"); + Project project = collection.LoadProject(path, "4.0"); + + Assert.AreEqual("v", project.GlobalProperties["p"]); + } + finally + { + File.Delete(path); + } + } + + /// + /// Loading a project from a file inherits the project collection's global properties + /// + [TestMethod] + public void GlobalPropertyInheritLoadFromFile3() + { + string path = null; + + try + { + path = CreateProjectFile(); + + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("p", "v"); + Project project = collection.LoadProject(path, null, "4.0"); + + Assert.AreEqual("v", project.GlobalProperties["p"]); + } + finally + { + File.Delete(path); + } + } + + /// + /// Loading a project from a reader inherits the project collection's global properties + /// + [TestMethod] + public void GlobalPropertyInheritLoadFromXml1() + { + XmlReader reader = CreateProjectXmlReader(); + + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("p", "v"); + + Project project = collection.LoadProject(reader); + + Assert.AreEqual("v", project.GlobalProperties["p"]); + } + + /// + /// Loading a project from a reader inherits the project collection's global properties + /// + [TestMethod] + public void GlobalPropertyInheritLoadFromXml2() + { + XmlReader reader = CreateProjectXmlReader(); + + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("p", "v"); + + Project project = collection.LoadProject(reader, "4.0"); + + Assert.AreEqual("v", project.GlobalProperties["p"]); + } + + /// + /// Creating a project inherits the project collection's global properties + /// + [TestMethod] + public void GlobalPropertyInheritProjectConstructor() + { + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("p", "v"); + + Project project = new Project(collection); + + Assert.AreEqual("v", project.GlobalProperties["p"]); + } + + /// + /// Load project should load a project, if it wasn't already loaded. + /// + [TestMethod] + public void GetLoadedProjectNonExistent() + { + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement xml = ProjectRootElement.Create(); + xml.Save(path); + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.Count); + + Project result = ProjectCollection.GlobalProjectCollection.LoadProject(path); + + Assert.AreEqual(1, ProjectCollection.GlobalProjectCollection.Count); + } + finally + { + File.Delete(path); + } + } + + /// + /// Verify that one project collection doesn't contain the projects of another + /// + [TestMethod] + public void GetLoadedProjectWrongCollection() + { + Project project1 = new Project(); + project1.FullPath = "c:\\1"; + + ProjectCollection collection = new ProjectCollection(); + Project project2 = new Project(collection); + project2.FullPath = "c:\\1"; + + Assert.AreEqual(true, Object.ReferenceEquals(project2, collection.LoadProject("c:\\1"))); + Assert.AreEqual(false, Object.ReferenceEquals(project1, collection.LoadProject("c:\\1"))); + } + + /// + /// Verify that one project collection doesn't contain the ProjectRootElements of another + /// -- because they don't share a ProjectRootElementCache + /// + [TestMethod] + public void GetLoadedProjectRootElementWrongCollection() + { + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement.Create(path).Save(); + + ProjectCollection collection1 = new ProjectCollection(); + Project project1 = collection1.LoadProject(path); + Project project1b = collection1.LoadProject(path); + + Assert.AreEqual(true, Object.ReferenceEquals(project1.Xml, project1b.Xml)); + + ProjectCollection collection2 = new ProjectCollection(); + Project project2 = collection2.LoadProject(path); + + Assert.AreEqual(false, Object.ReferenceEquals(project1.Xml, project2.Xml)); + } + finally + { + File.Delete(path); + } + } + + /// + /// Attempt to have two equivalent projects in a project collection fails. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ErrorTwoProjectsEquivalentOneCollection() + { + Project project = new Project(); + project.FullPath = "c:\\x"; + + Project project2 = new Project(); + project2.FullPath = "c:\\x"; + } + + /// + /// Validates that when loading two projects with nominally different global properties, but that match when we take + /// into account the ProjectCollection's global properties, we get the pre-existing project if one exists. + /// + [TestMethod] + public void TwoProjectsEquivalentWhenOneInheritsFromProjectCollection() + { + Project project = new Project(); + project.FullPath = "c:\\1"; + + // Set a global property on the project collection -- this should be passed on to all + // loaded projects. + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("Configuration", "Debug"); + + Assert.AreEqual("Debug", project.GlobalProperties["Configuration"]); + + // now create a global properties dictionary to pass to a new project + Dictionary globals = new Dictionary(); + + globals.Add("Configuration", "Debug"); + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject("c:\\1", globals, null); + + Assert.AreEqual(1, ProjectCollection.GlobalProjectCollection.Count); + } + + /// + /// Two projects may have the same path but different global properties. + /// + [TestMethod] + public void TwoProjectsDistinguishedByGlobalPropertiesOnly() + { + ProjectRootElement xml = ProjectRootElement.Create(); + + Dictionary globalProperties1 = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties1.Add("p", "v1"); + Project project1 = new Project(xml, globalProperties1, "4.0"); + project1.FullPath = "c:\\1"; + + Dictionary globalProperties2 = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties2.Add("p", "v2"); + Project project2 = new Project(xml, globalProperties2, "4.0"); + project2.FullPath = "c:\\1"; + + Assert.AreEqual(true, Object.ReferenceEquals(project1, ProjectCollection.GlobalProjectCollection.LoadProject("c:\\1", globalProperties1, "4.0"))); + Assert.AreEqual(true, Object.ReferenceEquals(project2, ProjectCollection.GlobalProjectCollection.LoadProject("c:\\1", globalProperties2, "4.0"))); + + List projects = Helpers.MakeList(ProjectCollection.GlobalProjectCollection.LoadedProjects); + + Assert.AreEqual(2, projects.Count); + Assert.AreEqual(2, ProjectCollection.GlobalProjectCollection.Count); + Assert.AreEqual(true, projects.Contains(project1)); + Assert.AreEqual(true, projects.Contains(project2)); + } + + /// + /// Validates that we can correctly load two of the same project file with different global properties, even when + /// those global properties are applied to the project by the project collection (and then overrided in one case). + /// + [TestMethod] + public void TwoProjectsDistinguishedByGlobalPropertiesOnly_ProjectOverridesProjectCollection() + { + Project project = new Project(); + project.FullPath = "c:\\1"; + + // Set a global property on the project collection -- this should be passed on to all + // loaded projects. + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("Configuration", "Debug"); + + Assert.AreEqual("Debug", project.GlobalProperties["Configuration"]); + + // Differentiate this project from the one below + project.SetGlobalProperty("MyProperty", "MyValue"); + + // now create a global properties dictionary to pass to a new project + Dictionary project2Globals = new Dictionary(); + + project2Globals.Add("Configuration", "Release"); + project2Globals.Add("Platform", "Win32"); + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject("c:\\1", project2Globals, null); + + Assert.AreEqual("Release", project2.GlobalProperties["Configuration"]); + + // Setting a global property on the project collection overrides all contained projects, + // whether they were initially loaded with the global project collection's value or not. + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("Platform", "X64"); + Assert.AreEqual("X64", project.GlobalProperties["Platform"]); + Assert.AreEqual("X64", project2.GlobalProperties["Platform"]); + + // But setting a global property on the project directly should override that. + project2.SetGlobalProperty("Platform", "Itanium"); + Assert.AreEqual("Itanium", project2.GlobalProperties["Platform"]); + + // Now set global properties such that the two projects have an identical set. + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("Configuration", "Debug2"); + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("Platform", "X86"); + + bool exceptionCaught = false; + try + { + // This will make it identical, so we should get a throw here. + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("MyProperty", "MyValue2"); + } + catch (InvalidOperationException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught, "Should have caused the two projects to be identical, causing an exception to be thrown"); + } + + /// + /// Two projects may have the same path but different tools version. + /// + [TestMethod] + public void TwoProjectsDistinguishedByToolsVersionOnly() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35) == null) + { + // "Requires 3.5 to be installed" + return; + } + + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) == null) + { + // ".NET Framework 2.0 is required to be installed for this test, but it is not installed." + return; + } + + ProjectRootElement xml = ProjectRootElement.Create(); + + Project project1 = new Project(xml, null, "2.0"); + project1.FullPath = "c:\\1"; + + Project project2 = new Project(xml, null, "4.0"); + project2.FullPath = "c:\\1"; + + Assert.AreEqual(true, Object.ReferenceEquals(project1, ProjectCollection.GlobalProjectCollection.LoadProject("c:\\1", null, "2.0"))); + Assert.AreEqual(true, Object.ReferenceEquals(project2, ProjectCollection.GlobalProjectCollection.LoadProject("c:\\1", null, "4.0"))); + } + + /// + /// If the ToolsVersion in the project file is bogus, we'll default to the current ToolsVersion and successfully + /// load it. Make sure we can RE-load it, too, and successfully pick up the correct copy of the loaded project. + /// + [TestMethod] + public void ReloadProjectWithInvalidToolsVersionInFile() + { + string content = @" + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + project.FullPath = "c:\\123.proj"; + + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject("c:\\123.proj", null, null); + + Assert.IsTrue(Object.ReferenceEquals(project, project2)); + } + + /// + /// Make sure we can reload a project that has a ToolsVersion that doesn't match what it ends up getting + /// forced to by default (current). + /// + [TestMethod] + public void ReloadProjectWithProjectToolsVersionDifferentFromEffectiveToolsVersion() + { + string content = @" + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + project.FullPath = "c:\\123.proj"; + + Project project2 = ProjectCollection.GlobalProjectCollection.LoadProject("c:\\123.proj", null, null); + + Assert.IsTrue(Object.ReferenceEquals(project, project2)); + } + + /// + /// Collection stores projects distinguished by path, global properties, and tools version. + /// Changing global properties should update the collection. + /// + [TestMethod] + public void ChangingGlobalPropertiesUpdatesCollection() + { + ProjectCollection collection = new ProjectCollection(); + Project project = new Project(collection); + project.FullPath = "c:\\x"; // load into collection + project.SetGlobalProperty("p", "v1"); // should update collection + + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("p", "v1"); + Project newProject = collection.LoadProject("c:\\x", globalProperties, null); + + Assert.AreEqual(true, Object.ReferenceEquals(project, newProject)); + } + + /// + /// Changing global properties on collection should should update the collection's defaults, + /// and any projects even if they have defined the same global properties + /// + [TestMethod] + public void SettingGlobalPropertiesOnCollectionUpdatesProjects() + { + ProjectCollection collection = new ProjectCollection(); + Project project1 = new Project(collection); + project1.FullPath = "c:\\y"; // load into collection + Assert.AreEqual(0, project1.GlobalProperties.Count); + + collection.SetGlobalProperty("g1", "v1"); + collection.SetGlobalProperty("g2", "v2"); + collection.SetGlobalProperty("g2", "v2"); // try dupe + + Assert.AreEqual(2, project1.GlobalProperties.Count); + + collection.RemoveGlobalProperty("g2"); + Project project2 = new Project(collection); + project2.FullPath = "c:\\x"; // load into collection + + Assert.AreEqual(1, project1.GlobalProperties.Count); + Assert.AreEqual("v1", project2.GlobalProperties["g1"]); + + Assert.AreEqual(1, project2.GlobalProperties.Count); + Assert.AreEqual("v1", project2.GlobalProperties["g1"]); + } + + /// + /// Changing global properties on collection should should update the collection's defaults, + /// and any projects even if they have defined the same global properties + /// + [TestMethod] + public void SettingGlobalPropertiesOnCollectionUpdatesProjects2() + { + ProjectCollection collection = new ProjectCollection(); + Project project1 = new Project(collection); + project1.FullPath = "c:\\y"; // load into collection + project1.SetGlobalProperty("g1", "v0"); + Helpers.ClearDirtyFlag(project1.Xml); + + collection.SetGlobalProperty("g1", "v1"); + collection.SetGlobalProperty("g2", "v2"); + + Assert.AreEqual(2, project1.GlobalProperties.Count); + Assert.AreEqual("v1", project1.GlobalProperties["g1"]); + Assert.AreEqual("v2", project1.GlobalProperties["g2"]); // Got overwritten + Assert.AreEqual(true, project1.IsDirty); + } + + /// + /// Changing global properties on collection should should update the collection's defaults, + /// and all projects as well + /// + [TestMethod] + public void RemovingGlobalPropertiesOnCollectionUpdatesProjects() + { + ProjectCollection collection = new ProjectCollection(); + Project project1 = new Project(collection); + project1.FullPath = "c:\\y"; // load into collection + Assert.AreEqual(0, project1.GlobalProperties.Count); + + Helpers.ClearDirtyFlag(project1.Xml); + + collection.SetGlobalProperty("g1", "v1"); // should make both dirty + collection.SetGlobalProperty("g2", "v2"); // should make both dirty + + Assert.AreEqual(true, project1.IsDirty); + + Project project2 = new Project(collection); + project2.FullPath = "c:\\x"; // load into collection + + Assert.AreEqual(true, project2.IsDirty); + + Assert.AreEqual(2, project1.GlobalProperties.Count); + Assert.AreEqual("v1", project2.GlobalProperties["g1"]); + + Assert.AreEqual(2, project2.GlobalProperties.Count); + Assert.AreEqual("v1", project2.GlobalProperties["g1"]); + + Helpers.ClearDirtyFlag(project1.Xml); + Helpers.ClearDirtyFlag(project2.Xml); + + collection.RemoveGlobalProperty("g2"); // should make both dirty + + Assert.AreEqual(true, project1.IsDirty); + Assert.AreEqual(true, project2.IsDirty); + + Assert.AreEqual(1, project1.GlobalProperties.Count); + Assert.AreEqual(1, project2.GlobalProperties.Count); + + collection.RemoveGlobalProperty("g1"); + + Assert.AreEqual(0, project1.GlobalProperties.Count); + Assert.AreEqual(0, project2.GlobalProperties.Count); + } + + /// + /// Changing global properties on collection should should update the collection's defaults, + /// and all projects as well + /// + [TestMethod] + public void RemovingGlobalPropertiesOnCollectionUpdatesProjects2() + { + ProjectCollection collection = new ProjectCollection(); + collection.SetGlobalProperty("g1", "v1"); + + Project project1 = new Project(collection); + project1.FullPath = "c:\\y"; // load into collection + project1.SetGlobalProperty("g1", "v0"); // mask collection property + Helpers.ClearDirtyFlag(project1.Xml); + + collection.RemoveGlobalProperty("g1"); // should modify the project + + Assert.AreEqual(0, project1.GlobalProperties.Count); + Assert.AreEqual(true, project1.IsDirty); + } + + /// + /// Unloading a project should remove it from the project collection + /// + [TestMethod] + public void UnloadProject() + { + Project project = new Project(); + project.FullPath = "c:\\x"; // load into collection + + Assert.AreEqual(1, ProjectCollection.GlobalProjectCollection.Count); + + ProjectCollection.GlobalProjectCollection.UnloadProject(project); // should not throw + + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.Count); + Assert.AreEqual(0, Helpers.MakeList(ProjectCollection.GlobalProjectCollection.LoadedProjects).Count); + } + + /// + /// Unloading project XML should remove it from the weak cache. + /// + [TestMethod] + public void UnloadProjectXml() + { + Project project = new Project(); + project.FullPath = "c:\\x"; // load into collection + ProjectRootElement xml = project.Xml; + + // Unload the evaluation project, and then the XML. + ProjectCollection.GlobalProjectCollection.UnloadProject(project); + ProjectCollection.GlobalProjectCollection.UnloadProject(xml); + + try + { + // If the ProjectRootElement was unloaded from the cache, then + // an attempt to load it by the pretend filename should fail, + // so it makes a good test to see that the UnloadProject method worked. + ProjectCollection.GlobalProjectCollection.LoadProject(xml.FullPath); + Assert.Fail("An InvalidProjectFileException was expected."); + } + catch (InvalidProjectFileException) + { + } + } + + /// + /// Unloading project XML while it is in use should result in an exception. + /// + [TestMethod, ExpectedException(typeof(InvalidOperationException))] + public void UnloadProjectXmlWhileInDirectUse() + { + Project project = new Project(); + project.FullPath = "c:\\x"; // load into collection + + // Attempt to unload the xml before unloading the project evaluation. + ProjectCollection.GlobalProjectCollection.UnloadProject(project.Xml); + } + + /// + /// Unloading project XML while it is in use should result in an exception. + /// + [TestMethod, ExpectedException(typeof(InvalidOperationException))] + public void UnloadProjectXmlWhileInImportUse() + { + Project mainProject = new Project(); + mainProject.FullPath = "c:\\main"; // load into collection + + Project importProject = new Project(); + importProject.FullPath = "c:\\import"; // load into collection + ProjectRootElement importedXml = importProject.Xml; + + // Import into main project + mainProject.Xml.PrependChild(mainProject.Xml.CreateImportElement(importProject.FullPath)); + mainProject.ReevaluateIfNecessary(); + + // Unload the import evaluation, but not the main project that still has a reference to it. + ProjectCollection.GlobalProjectCollection.UnloadProject(importProject); + + // Attempt to unload the import xml before unloading the project that still references it. + try + { + ProjectCollection.GlobalProjectCollection.UnloadProject(importedXml); + } + catch (InvalidOperationException ex) + { + Console.WriteLine(ex.Message); + throw; + } + } + + /// + /// Renaming a project should correctly update the project collection's set of loaded projects. + /// + [TestMethod] + public void RenameProject() + { + Project project = new Project(); + project.FullPath = "c:\\x"; // load into collection + + project.FullPath = "c:\\y"; + + Assert.AreEqual(1, ProjectCollection.GlobalProjectCollection.Count); + + Assert.AreEqual(true, Object.ReferenceEquals(project, Helpers.MakeList(ProjectCollection.GlobalProjectCollection.LoadedProjects)[0])); + + ProjectCollection.GlobalProjectCollection.UnloadProject(project); // should not throw + + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.Count); + } + + /// + /// Validates that we don't somehow lose the ProjectCollection global properties when renaming the project. + /// + [TestMethod] + public void RenameProjectAndVerifyStillContainsProjectCollectionGlobalProperties() + { + Project project = new Project(); + project.FullPath = "c:\\1"; + + // Set a global property on the project collection -- this should be passed on to all + // loaded projects. + ProjectCollection.GlobalProjectCollection.SetGlobalProperty("Configuration", "Debug"); + + Assert.AreEqual("Debug", project.GlobalProperties["Configuration"]); + + project.FullPath = "c:\\2"; + + Assert.AreEqual("Debug", project.GlobalProperties["Configuration"]); + } + + /// + /// Saving a project to a new name should correctly update the project collection's set of loaded projects. + /// Reported by F#. + /// + [TestMethod] + public void SaveToNewNameAndUnload() + { + string file1 = null; + string file2 = null; + + try + { + file1 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + Project project = new Project(); + project.Save(file1); + + ProjectCollection collection = new ProjectCollection(); + + Project project2 = collection.LoadProject(file1); + project2.Save(file2); + + collection.UnloadProject(project2); + } + finally + { + File.Delete(file1); + File.Delete(file2); + } + } + + /// + /// Saving a project to a new name after loading, unloading, and reloading, should work. + /// Reported by F#. + /// + [TestMethod] + public void LoadUnloadReloadSaveToNewName() + { + string file1 = null; + string file2 = null; + + try + { + file1 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + Project project = new Project(); + project.Save(file1); + project.ProjectCollection.UnloadProject(project); + + ProjectCollection collection = new ProjectCollection(); + + Project project2 = collection.LoadProject(file1); + collection.UnloadProject(project2); + + Project project3 = collection.LoadProject(file1); + project3.Save(file2); // should not crash + + collection.UnloadProject(project3); + } + finally + { + File.Delete(file1); + File.Delete(file2); + } + } + + /// + /// Saving a project to a new name after loading, unloading, and reloading, should work. + /// Reported by F#. + /// + [TestMethod] + public void LoadUnloadAllReloadSaveToNewName() + { + string file1 = null; + string file2 = null; + + try + { + file1 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + Project project = new Project(); + project.Save(file1); + project.ProjectCollection.UnloadProject(project); + + ProjectCollection collection = new ProjectCollection(); + + Project project2 = collection.LoadProject(file1); + collection.UnloadAllProjects(); + + Project project3 = collection.LoadProject(file1); + project3.Save(file2); // should not crash + + collection.UnloadProject(project3); + } + finally + { + File.Delete(file1); + File.Delete(file2); + } + } + + /// + /// Add a toolset + /// + [TestMethod] + public void AddToolset() + { + ProjectCollection collection = new ProjectCollection(); + collection.RemoveAllToolsets(); + + Toolset toolset = new Toolset("x", "c:\\y", collection, null); + collection.AddToolset(toolset); + + Assert.AreEqual(toolset, collection.GetToolset("x")); + Assert.AreEqual(true, collection.ContainsToolset("x")); + + List toolsets = Helpers.MakeList(collection.Toolsets); + Assert.AreEqual(1, toolsets.Count); + Assert.AreEqual(toolset, toolsets[0]); + } + + /// + /// Add two toolsets + /// + [TestMethod] + public void AddTwoToolsets() + { + ProjectCollection collection = new ProjectCollection(); + collection.RemoveAllToolsets(); + + Toolset toolset1 = new Toolset("x", "c:\\y", collection, null); + Toolset toolset2 = new Toolset("y", "c:\\z", collection, null); + + collection.AddToolset(toolset1); + collection.AddToolset(toolset2); + + Assert.AreEqual(toolset1, collection.GetToolset("x")); + Assert.AreEqual(toolset2, collection.GetToolset("y")); + + List toolsets = Helpers.MakeList(collection.Toolsets); + Assert.AreEqual(2, toolsets.Count); + Assert.AreEqual(true, toolsets.Contains(toolset1)); + Assert.AreEqual(true, toolsets.Contains(toolset2)); + } + + /// + /// Add a toolset that overrides another + /// + [TestMethod] + public void ReplaceToolset() + { + ProjectCollection collection = new ProjectCollection(); + collection.RemoveAllToolsets(); + + Toolset toolset1 = new Toolset("x", "c:\\y", collection, null); + Toolset toolset2 = new Toolset("x", "c:\\z", collection, null); + + collection.AddToolset(toolset1); + collection.AddToolset(toolset2); + + Assert.AreEqual(toolset2, collection.GetToolset("x")); + + List toolsets = Helpers.MakeList(collection.Toolsets); + Assert.AreEqual(1, toolsets.Count); + Assert.AreEqual(toolset2, toolsets[0]); + } + + /// + /// Attempt to add a null toolset + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AddNullToolset() + { + ProjectCollection.GlobalProjectCollection.AddToolset(null); + } + + /// + /// Remove a toolset + /// + [TestMethod] + public void RemoveToolset() + { + ProjectCollection collection = new ProjectCollection(); + + Toolset toolset1 = new Toolset("x", "c:\\y", collection, null); + Toolset toolset2 = new Toolset("y", "c:\\z", collection, null); + + int initial = Helpers.MakeList(collection.Toolsets).Count; + + collection.AddToolset(toolset1); + collection.AddToolset(toolset2); + + Assert.AreEqual(true, collection.RemoveToolset("x")); + Assert.AreEqual(false, collection.ContainsToolset("x")); + + Assert.AreEqual(1, Helpers.MakeList(collection.Toolsets).Count - initial); + } + + /// + /// Remove a nonexistent toolset + /// + [TestMethod] + public void RemoveNonexistentToolset() + { + ProjectCollection collection = new ProjectCollection(); + Assert.AreEqual(false, collection.RemoveToolset("nonexistent")); + } + + /// + /// Attempt to remove a null tools version + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void RemoveNullToolsVersion() + { + ProjectCollection.GlobalProjectCollection.RemoveToolset(null); + } + + /// + /// Attempt to remove an empty string toolsversion + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RemoveEmptyToolsVersion() + { + ProjectCollection.GlobalProjectCollection.RemoveToolset(String.Empty); + } + + /// + /// Current default from registry is 2.0 if 2.0 is installed + /// + [TestMethod] + public void DefaultToolsVersion() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) == null) + { + // "Requires 2.0 to be installed" + return; + } + + ProjectCollection collection = new ProjectCollection(); + Assert.AreEqual("2.0", collection.DefaultToolsVersion); + } + + /// + /// Current default from registry is 4.0 if 2.0 is not installed + /// + [TestMethod] + public void DefaultToolsVersion2() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) != null) + { + // "Requires 2.0 to NOT be installed" + return; + } + + ProjectCollection collection = new ProjectCollection(); + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, collection.DefaultToolsVersion); + } + + /// + /// Error setting default tools version to empty + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetDefaultToolsVersionEmpty() + { + ProjectCollection.GlobalProjectCollection.DefaultToolsVersion = String.Empty; + } + + /// + /// Error setting default tools version to a toolset that does not exist + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetDefaultToolsVersionNonexistentToolset() + { + ProjectCollection.GlobalProjectCollection.DefaultToolsVersion = "nonexistent"; + } + + /// + /// Set default tools version; subsequent projects should use it + /// + [TestMethod] + public void SetDefaultToolsVersion() + { + ProjectCollection collection = new ProjectCollection(); + collection.AddToolset(new Toolset("x", @"c:\y", collection, null)); + + collection.DefaultToolsVersion = "x"; + + Assert.AreEqual("x", collection.DefaultToolsVersion); + + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content)), null, null, collection); + + // ... and after all that, we end up defaulting to the current ToolsVersion instead. There's a way + // to turn this behavior (new in Dev12) off, but it requires setting an environment variable and + // clearing some internal state to make sure that the update environment variable is picked up, so + // there's not a good way of doing it from these deliberately public OM only tests. + Assert.AreEqual(project.ToolsVersion, ObjectModelHelpers.MSBuildDefaultToolsVersion); + } + + /// + /// Changes to the ProjectCollection object should raise a ProjectCollectionChanged event. + /// + [TestMethod] + public void ProjectCollectionChangedEvent() + { + ProjectCollection collection = new ProjectCollection(); + bool dirtyRaised = false; + ProjectCollectionChangedState expectedChange = ProjectCollectionChangedState.Loggers; + collection.ProjectCollectionChanged += + (sender, e) => + { + Assert.AreSame(collection, sender); + Assert.AreEqual(expectedChange, e.Changed); + dirtyRaised = true; + }; + Assert.IsFalse(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.DisableMarkDirty; + dirtyRaised = false; + collection.DisableMarkDirty = true; // LEAVE THIS TRUE for rest of the test, to verify it doesn't suppress these events + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.IsBuildEnabled; + dirtyRaised = false; + collection.IsBuildEnabled = !collection.IsBuildEnabled; + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.OnlyLogCriticalEvents; + dirtyRaised = false; + collection.OnlyLogCriticalEvents = !collection.OnlyLogCriticalEvents; + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.SkipEvaluation; + dirtyRaised = false; + collection.SkipEvaluation = !collection.SkipEvaluation; + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.GlobalProperties; + dirtyRaised = false; + collection.SetGlobalProperty("a", "b"); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.GlobalProperties; + dirtyRaised = false; + collection.RemoveGlobalProperty("a"); + Assert.IsTrue(dirtyRaised); + + // Verify HostServices changes raise the event. + expectedChange = ProjectCollectionChangedState.HostServices; + dirtyRaised = false; + collection.HostServices = new Execution.HostServices(); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.Loggers; + dirtyRaised = false; + collection.RegisterLogger(new MockLogger()); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.Loggers; + dirtyRaised = false; + collection.RegisterLoggers(new Microsoft.Build.Framework.ILogger[] { new MockLogger(), new MockLogger() }); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.Loggers; + dirtyRaised = false; + collection.UnregisterAllLoggers(); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.Toolsets; + dirtyRaised = false; + collection.AddToolset(new Toolset("testTools", Path.GetTempPath(), collection, Path.GetTempPath())); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.DefaultToolsVersion; + dirtyRaised = false; + collection.DefaultToolsVersion = "testTools"; + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.Toolsets; + dirtyRaised = false; + collection.RemoveToolset("testTools"); + Assert.IsTrue(dirtyRaised); + + expectedChange = ProjectCollectionChangedState.Toolsets; + dirtyRaised = false; + collection.RemoveAllToolsets(); + Assert.IsTrue(dirtyRaised); + } + + /// + /// Changes to the ProjectCollection object should raise a ProjectCollectionChanged event. + /// + [TestMethod] + public void ProjectCollectionChangedEvent2() + { + // Verify if the project, project collection and the value we are setting in the project collection are all the same + // then the projects value for the property should not change and no event should be fired. + ProjectCollection collection = new ProjectCollection(); + XmlReader reader = CreateProjectXmlReader(); + Project project = collection.LoadProject(reader, "4.0"); + project.SetProperty("a", "1"); + collection.SetGlobalProperty("a", "1"); + VerifyProjectCollectionEvents(collection, false, "1"); + + // Verify if the project, project collection and the value we are setting in the project collection are all the same + // then the projects value for the property should not change and no event should be fired. + collection = new ProjectCollection(); + reader = CreateProjectXmlReader(); + project = collection.LoadProject(reader, "4.0"); + project.SetProperty("a", "%28x86%29"); + collection.SetGlobalProperty("a", "%28x86%29"); + VerifyProjectCollectionEvents(collection, false, "%28x86%29"); + + // Verify if the project, project collection have the same value but a new value is set in the project collection + // then the projects value for the property should be change and an event should be fired. + collection = new ProjectCollection(); + reader = CreateProjectXmlReader(); + project = collection.LoadProject(reader, "4.0"); + project.SetProperty("a", "1"); + collection.SetGlobalProperty("a", "1"); + VerifyProjectCollectionEvents(collection, true, "2"); + project.GetPropertyValue("a").Equals("2", StringComparison.OrdinalIgnoreCase); + + // Verify if the project, project collection have the same value but a new value is set in the project collection + // then the projects value for the property should be change and an event should be fired. + collection = new ProjectCollection(); + reader = CreateProjectXmlReader(); + project = collection.LoadProject(reader, "4.0"); + project.SetProperty("a", "1"); + collection.SetGlobalProperty("a", "(x86)"); + VerifyProjectCollectionEvents(collection, true, "%28x86%29"); + project.GetPropertyValue("a").Equals("%28x86%29", StringComparison.OrdinalIgnoreCase); + + // Verify if the project has one value and project collection and the property we are setting on the project collection have the same value + // then the projects value for the property should be change but no event should be fired + collection = new ProjectCollection(); + reader = CreateProjectXmlReader(); + project = collection.LoadProject(reader, "4.0"); + project.SetProperty("a", "2"); + collection.SetGlobalProperty("a", "1"); + + VerifyProjectCollectionEvents(collection, false, "1"); + project.GetPropertyValue("a").Equals("1", StringComparison.OrdinalIgnoreCase); + + // Verify if the project and the property being set have one value but the project collection has another + // then the projects value for the property should not change and event should be fired + collection = new ProjectCollection(); + reader = CreateProjectXmlReader(); + project = collection.LoadProject(reader, "4.0"); + project.SetProperty("a", "1"); + collection.SetGlobalProperty("a", "2"); + VerifyProjectCollectionEvents(collection, true, "1"); + project.GetPropertyValue("a").Equals("1", StringComparison.OrdinalIgnoreCase); + + // item is added to project collection for the first time. Make sure it is added to the project and an event is fired. + collection = new ProjectCollection(); + reader = CreateProjectXmlReader(); + project = collection.LoadProject(reader, "4.0"); + + VerifyProjectCollectionEvents(collection, true, "1"); + project.GetPropertyValue("a").Equals("1", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Changes to project XML should raise an ProjectXmlChanged event. + /// + [TestMethod] + public void ProjectXmlChangedEvent() + { + ProjectCollection collection = new ProjectCollection(); + ProjectRootElement pre = null; + bool dirtyRaised = false; + collection.ProjectXmlChanged += + (sender, e) => + { + Assert.AreSame(collection, sender); + Assert.AreSame(pre, e.ProjectXml); + this.TestContext.WriteLine(e.Reason ?? String.Empty); + dirtyRaised = true; + }; + Assert.IsFalse(dirtyRaised); + + // Ensure that the event is raised even when DisableMarkDirty is set. + collection.DisableMarkDirty = true; + + // Create a new PRE but don't change the template. + dirtyRaised = false; + pre = ProjectRootElement.Create(collection); + Assert.IsFalse(dirtyRaised); + + // Change PRE prior to setting a filename and thus associating the PRE with the ProjectCollection. + dirtyRaised = false; + pre.AppendChild(pre.CreatePropertyGroupElement()); + Assert.IsFalse(dirtyRaised); + + // Associate with the ProjectCollection + dirtyRaised = false; + pre.FullPath = FileUtilities.GetTemporaryFile(); + Assert.IsTrue(dirtyRaised); + + // Now try dirtying again and see that the event is raised this time. + dirtyRaised = false; + pre.AppendChild(pre.CreatePropertyGroupElement()); + Assert.IsTrue(dirtyRaised); + + // Make sure that project collection global properties don't raise this event. + dirtyRaised = false; + collection.SetGlobalProperty("a", "b"); + Assert.IsFalse(dirtyRaised); + + // Change GlobalProperties on a project to see that that doesn't propagate as an XML change. + dirtyRaised = false; + var project = new Project(pre); + project.SetGlobalProperty("q", "s"); + Assert.IsFalse(dirtyRaised); + + // Change XML via the Project to verify the event is raised. + dirtyRaised = false; + project.SetProperty("z", "y"); + Assert.IsTrue(dirtyRaised); + } + + /// + /// Changes to a Project evaluation object should raise a ProjectChanged event. + /// + [TestMethod] + public void ProjectChangedEvent() + { + ProjectCollection collection = new ProjectCollection(); + ProjectRootElement pre = null; + Project project = null; + bool dirtyRaised = false; + collection.ProjectChanged += + (sender, e) => + { + Assert.AreSame(collection, sender); + Assert.AreSame(project, e.Project); + dirtyRaised = true; + }; + Assert.IsFalse(dirtyRaised); + + pre = ProjectRootElement.Create(collection); + project = new Project(pre, null, null, collection); + + // all these should still pass with disableMarkDirty set + collection.DisableMarkDirty = true; + project.DisableMarkDirty = true; + + dirtyRaised = false; + pre.AppendChild(pre.CreatePropertyGroupElement()); + Assert.IsFalse(dirtyRaised, "Dirtying the XML directly should not result in a ProjectChanged event."); + + // No events should be raised before we associate a filename with the PRE + dirtyRaised = false; + project.SetGlobalProperty("someGlobal", "someValue"); + Assert.IsFalse(dirtyRaised); + + dirtyRaised = false; + project.SetProperty("someProp", "someValue"); + Assert.IsFalse(dirtyRaised); + + pre.FullPath = FileUtilities.GetTemporaryFile(); + dirtyRaised = false; + project.SetGlobalProperty("someGlobal", "someValue2"); + Assert.IsTrue(dirtyRaised); + + dirtyRaised = false; + project.RemoveGlobalProperty("someGlobal"); + Assert.IsTrue(dirtyRaised); + + dirtyRaised = false; + collection.SetGlobalProperty("somePCglobal", "someValue"); + Assert.IsTrue(dirtyRaised); + + dirtyRaised = false; + project.SetProperty("someProp", "someValue2"); + Assert.IsTrue(dirtyRaised); + } + + /// + /// Create an empty project file and return the path + /// + private static string CreateProjectFile() + { + ProjectRootElement xml = ProjectRootElement.Create(); + string path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + xml.Save(path); + return path; + } + + /// + /// Create an XmlReader around an empty project file content + /// + private XmlReader CreateProjectXmlReader() + { + ProjectRootElement xml = ProjectRootElement.Create(); + XmlReader reader = XmlReader.Create(new StringReader(xml.RawXml)); + return reader; + } + + /// + /// Verify that when a property is set on the project collection that the correct events are fired. + /// + private void VerifyProjectCollectionEvents(ProjectCollection collection, bool expectEventRaised, string propertyValue) + { + bool raisedEvent = false; + ProjectCollectionChangedState expectedChange = ProjectCollectionChangedState.Loggers; + collection.ProjectCollectionChanged += + (sender, e) => + { + Assert.AreSame(collection, sender); + Assert.AreEqual(expectedChange, e.Changed); + raisedEvent = true; + }; + + expectedChange = ProjectCollectionChangedState.GlobalProperties; + collection.SetGlobalProperty("a", propertyValue); + Assert.AreEqual(raisedEvent, expectEventRaised); + ProjectPropertyInstance property = collection.GetGlobalProperty("a"); + Assert.IsNotNull(property); + Assert.IsTrue(String.Equals(property.EvaluatedValue, ProjectCollection.Unescape(propertyValue), StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItemDefinition_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItemDefinition_Tests.cs new file mode 100644 index 00000000000..f5da9c0e769 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItemDefinition_Tests.cs @@ -0,0 +1,648 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectItemDefinition +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for ProjectItemDefinition + /// + [TestClass] + public class ProjectItemDefinition_Tests + { + /// + /// Add metadata; should add to an existing item definition group that has item definitions of the same item type + /// + [TestMethod] + public void AddMetadataExistingItemDefinitionGroup() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0"); + + Project project = new Project(xml); + project.ItemDefinitions["i"].SetMetadataValue("n", "n0"); + + string expected = ObjectModelHelpers.CleanupFileContents( +@" + + + m0 + + + n0 + + +"); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + + /// + /// Set metadata with property expression; should be expanded + /// + [TestMethod] + public void SetMetadata() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddProperty("p", "v"); + xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0"); + xml.AddItem("i", "i1"); + + Project project = new Project(xml); + + ProjectMetadata metadatum = project.ItemDefinitions["i"].GetMetadata("m"); + + metadatum.UnevaluatedValue = "$(p)"; + + Assert.AreEqual("v", Helpers.GetFirst(project.GetItems("i")).GetMetadataValue("m")); + } + + /// + /// Access metadata when there isn't any + /// + [TestMethod] + public void EmptyMetadataCollection() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItemDefinitionGroup().AddItemDefinition("i"); + Project project = new Project(xml); + + ProjectItemDefinition itemDefinition = project.ItemDefinitions["i"]; + IEnumerable metadataCollection = itemDefinition.Metadata; + + List metadataList = Helpers.MakeList(metadataCollection); + + Assert.AreEqual(0, metadataList.Count); + + Assert.AreEqual(null, itemDefinition.GetMetadata("m")); + } + + /// + /// Set metadata get collection + /// + [TestMethod] + public void GetMetadataCollection() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0"); + + Project project = new Project(xml); + + IEnumerable metadataCollection = project.ItemDefinitions["i"].Metadata; + + List metadataList = Helpers.MakeList(metadataCollection); + + Assert.AreEqual(1, metadataList.Count); + Assert.AreEqual("m", metadataList[0].Name); + Assert.AreEqual("m0", metadataList[0].EvaluatedValue); + } + + /// + /// Attempt to update metadata on imported item definition should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void UpdateMetadataImported() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement import = ProjectRootElement.Create(file); + import.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0"); + import.Save(); + + ProjectRootElement main = ProjectRootElement.Create(); + Project project = new Project(main); + main.AddImport(file); + project.ReevaluateIfNecessary(); + + ProjectItemDefinition definition = project.ItemDefinitions["i"]; + definition.SetMetadataValue("m", "m1"); + } + finally + { + File.Delete(file); + } + } + + /// + /// Attempt to add new metadata on imported item definition should succeed, + /// creating a new item definition in the main project + /// + [TestMethod] + public void SetMetadataImported() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement import = ProjectRootElement.Create(file); + import.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0"); + import.Save(); + + ProjectRootElement main = ProjectRootElement.Create(); + Project project = new Project(main); + main.AddImport(file); + project.ReevaluateIfNecessary(); + + ProjectItemDefinition definition = project.ItemDefinitions["i"]; + definition.SetMetadataValue("n", "n0"); + + string expected = String.Format + ( + ObjectModelHelpers.CleanupFileContents( +@" + + + n0 + + + +"), + file + ); + + Helpers.VerifyAssertProjectContent(expected, project.Xml); + } + finally + { + File.Delete(file); + } + } + + /// + /// Item definition metadata should be sufficient to avoid errors like + /// "error MSB4096: The item "a.foo" in item list "h" does not define a value for metadata "m". In + /// order to use this metadata, either qualify it by specifying %(h.m), or ensure that all items in this list define a value + /// for this metadata." + /// + [TestMethod] + [TestCategory("serialize")] + public void BatchingConsidersItemDefinitionMetadata() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + + + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + MockLogger logger = new MockLogger(); + List loggers = new List() { logger }; + Assert.AreEqual(true, project.Build(loggers)); + + logger.AssertLogContains("a.foo;a.bar/m1"); + logger.AssertNoErrors(); + logger.AssertNoWarnings(); + } + + /// + /// Expand built-in metadata "late" + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + %(filename) + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + Assert.AreEqual("b", item.GetMetadataValue("m")); + } + + /// + /// Expand built-in metadata "late" + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_ReferToMetadataAbove() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + %(filename) + %(m)%(extension) + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + Assert.AreEqual("b.ext", item.GetMetadataValue("m")); + } + + /// + /// Expand built-in metadata "late" + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_MixtureOfCustomAndBuiltIn() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + l1 + %(filename).%(l) + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + Assert.AreEqual("b.l1", item.GetMetadataValue("m")); + } + + /// + /// Custom metadata expressions on metadata on an ItemDefinitionGroup is still always + /// expanded right there. + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_CustomEvaluationNeverDelayed() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + n1 + %(filename).%(n) + n2 + + + + + n3 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + Assert.AreEqual("b.n1", item.GetMetadataValue("m")); + } + + /// + /// A custom metadata that bizarrely expands to a built in metadata expression should + /// not evaluate again. + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_DoNotDoubleEvaluate() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + %25(filename) + %(n) + + + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + + Assert.AreEqual("%25(filename)", Project.GetMetadataValueEscaped(item, "m")); + Assert.AreEqual("%(filename)", item.GetMetadataValue("m")); + } + + /// + /// Items created from other items should still have the built-in metadata expanded + /// on them, not the original items. + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_CopyItems() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + %(extension) + + + + + '%(identity).bar')""/> + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + Assert.AreEqual(".bar", item.GetMetadataValue("m")); + } + + /// + /// Items created from other items should still have the built-in metadata expanded + /// on them, not the original items. + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_UseInTransform() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + %(extension) + + + + + '%(m)')""/> + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = project.GetItems("i").ElementAt(0); + Assert.AreEqual(".foo", item.EvaluatedInclude); + } + + /// + /// Items created from other items should still have the built-in metadata expanded + /// on them, not the original items. + /// + [TestMethod] + [TestCategory("serialize")] + public void ExpandBuiltInMetadataAtPointOfUse_UseInBatching() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + %(extension) + + + + + + + + + n1 + + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectInstance instance = project.CreateProjectInstance(); + MockLogger l = new MockLogger(); + List loggers = new List() { l }; + instance.Build(loggers); + + ProjectItemInstance item1 = instance.GetItems("i").ElementAt(0); + Assert.AreEqual("n1", item1.GetMetadataValue("n")); + + ProjectItemInstance item2 = instance.GetItems("i").ElementAt(1); + Assert.AreEqual("", item2.GetMetadataValue("n")); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_BuiltInProhibitedOnItemDefinitionMetadataCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_UnquotedBuiltInProhibitedOnItemDefinitionMetadataCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_BuiltInProhibitedOnItemDefinitionCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_BuiltInProhibitedOnItemDefinitionGroupCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_QualifiedBuiltInProhibitedOnItemDefinitionMetadataCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_QualifiedBuiltInProhibitedOnItemDefinitionCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_QualifiedBuiltInProhibitedOnItemDefinitionGroupCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Built-in metadata is prohibited in item definition conditions. + /// Ideally it would also be late evaluated, but that's too difficult. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ExpandBuiltInMetadataAtPointOfUse_UnquotedQualifiedBuiltInProhibitedOnItemDefinitionCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + } + + /// + /// Custom metadata is allowed in item definition conditions. + /// + [TestMethod] + public void ExpandBuiltInMetadataAtPointOfUse_UnquotedQualifiedCustomAllowedOnItemDefinitionCondition() + { + string content = +ObjectModelHelpers.CleanupFileContents( +@" + + + m1 + + +"); + + Project project = new Project(XmlReader.Create(new StringReader(content))); // No exception + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItem_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItem_Tests.cs new file mode 100644 index 00000000000..f6a74ef4e71 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectItem_Tests.cs @@ -0,0 +1,1499 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectItem +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for ProjectItem + /// + [TestClass] + public class ProjectItem_Tests + { + /// + /// Gets or sets the test context, assigned by the MSTest test runner. + /// + public TestContext TestContext { get; set; } + + /// + /// Project getter + /// + [TestMethod] + public void ProjectGetter() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + + Assert.AreEqual(true, Object.ReferenceEquals(project, item.Project)); + } + + /// + /// No metadata, simple case + /// + [TestMethod] + public void NoMetadata() + { + string content = @" + + + + + + "; + + ProjectItem item = GetOneItem(content); + + Assert.IsNotNull(item.Xml); + Assert.AreEqual("i", item.ItemType); + Assert.AreEqual("i1", item.EvaluatedInclude); + Assert.AreEqual("i1", item.UnevaluatedInclude); + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + } + + /// + /// Read off metadata + /// + [TestMethod] + public void ReadMetadata() + { + string content = @" + + + + v1 + v2 + + + + "; + + ProjectItem item = GetOneItem(content); + + var itemMetadata = Helpers.MakeList(item.Metadata); + Assert.AreEqual(2, itemMetadata.Count); + Assert.AreEqual("m1", itemMetadata[0].Name); + Assert.AreEqual("m2", itemMetadata[1].Name); + Assert.AreEqual("v1", itemMetadata[0].EvaluatedValue); + Assert.AreEqual("v2", itemMetadata[1].EvaluatedValue); + + Assert.AreEqual(itemMetadata[0], item.GetMetadata("m1")); + Assert.AreEqual(itemMetadata[1], item.GetMetadata("m2")); + } + + /// + /// Get metadata inherited from item definitions + /// + [TestMethod] + public void GetMetadataObjectsFromDefinition() + { + string content = @" + + + + v0 + v1 + + + + + v1b + v2 + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + ProjectMetadata m0 = item.GetMetadata("m0"); + ProjectMetadata m1 = item.GetMetadata("m1"); + + ProjectItemDefinition definition = project.ItemDefinitions["i"]; + ProjectMetadata idm0 = definition.GetMetadata("m0"); + ProjectMetadata idm1 = definition.GetMetadata("m1"); + + Assert.AreEqual(true, Object.ReferenceEquals(m0, idm0)); + Assert.AreEqual(false, Object.ReferenceEquals(m1, idm1)); + } + + /// + /// Get metadata values inherited from item definitions + /// + [TestMethod] + public void GetMetadataValuesFromDefinition() + { + string content = @" + + + + v0 + v1 + + + + + v1b + v2 + + + + "; + + ProjectItem item = GetOneItem(content); + + Assert.AreEqual("v0", item.GetMetadataValue("m0")); + Assert.AreEqual("v1b", item.GetMetadataValue("m1")); + Assert.AreEqual("v2", item.GetMetadataValue("m2")); + } + + /// + /// Getting nonexistent metadata should return null + /// + [TestMethod] + public void GetNonexistentMetadata() + { + ProjectItem item = GetOneItemFromFragment(@""); + + Assert.AreEqual(null, item.GetMetadata("m0")); + } + + /// + /// Getting value of nonexistent metadata should return String.Empty + /// + [TestMethod] + public void GetNonexistentMetadataValue() + { + ProjectItem item = GetOneItemFromFragment(@""); + + Assert.AreEqual(String.Empty, item.GetMetadataValue("m0")); + } + + /// + /// Attempting to set metadata with an invalid XML name should fail + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidXmlNameMetadata() + { + ProjectItem item = GetOneItemFromFragment(@""); + + item.SetMetadataValue("##invalid##", "x"); + } + + /// + /// Attempting to set built-in metadata should fail + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidBuiltInMetadata() + { + ProjectItem item = GetOneItemFromFragment(@""); + + item.SetMetadataValue("FullPath", "x"); + } + + /// + /// Attempting to set reserved metadata should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetInvalidReservedMetadata() + { + ProjectItem item = GetOneItemFromFragment(@""); + + item.SetMetadataValue("Choose", "x"); + } + + /// + /// Metadata enumerator should only return custom metadata + /// + [TestMethod] + public void MetadataEnumeratorExcludesBuiltInMetadata() + { + ProjectItem item = GetOneItemFromFragment(@""); + + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + } + + /// + /// Read off built-in metadata + /// + [TestMethod] + public void BuiltInMetadata() + { + ProjectItem item = GetOneItemFromFragment(@""); + + // c:\foo\bar.baz %(FullPath) = full path of item + // c:\ %(RootDir) = root directory of item + // bar %(Filename) = item filename without extension + // .baz %(Extension) = item filename extension + // c:\foo\ %(RelativeDir) = item directory as given in item-spec + // foo\ %(Directory) = full path of item directory relative to root + // [] %(RecursiveDir) = portion of item path that matched a recursive wildcard + // c:\foo\bar.baz %(Identity) = item-spec as given + // [] %(ModifiedTime) = last write time of item + // [] %(CreatedTime) = creation time of item + // [] %(AccessedTime) = last access time of item + Assert.AreEqual(@"c:\foo\bar.baz", item.GetMetadataValue("FullPath")); + Assert.AreEqual(@"c:\", item.GetMetadataValue("RootDir")); + Assert.AreEqual(@"bar", item.GetMetadataValue("Filename")); + Assert.AreEqual(@".baz", item.GetMetadataValue("Extension")); + Assert.AreEqual(@"c:\foo\", item.GetMetadataValue("RelativeDir")); + Assert.AreEqual(@"foo\", item.GetMetadataValue("Directory")); + Assert.AreEqual(String.Empty, item.GetMetadataValue("RecursiveDir")); + Assert.AreEqual(@"c:\foo\bar.baz", item.GetMetadataValue("Identity")); + } + + /// + /// Check file-timestamp related metadata + /// + [TestMethod] + public void BuiltInMetadataTimes() + { + string path = null; + string fileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff"; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + File.WriteAllText(path, String.Empty); + FileInfo info = new FileInfo(path); + + ProjectItem item = GetOneItemFromFragment(@""); + + Assert.AreEqual(info.LastWriteTime.ToString(fileTimeFormat), item.GetMetadataValue("ModifiedTime")); + Assert.AreEqual(info.CreationTime.ToString(fileTimeFormat), item.GetMetadataValue("CreatedTime")); + Assert.AreEqual(info.LastAccessTime.ToString(fileTimeFormat), item.GetMetadataValue("AccessedTime")); + } + finally + { + File.Delete(path); + } + } + + /// + /// Test RecursiveDir metadata + /// + [TestMethod] + public void RecursiveDirMetadata() + { + string directory = null; + string subdirectory = null; + string file = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), "a"); + if (File.Exists(directory)) + { + File.Delete(directory); + } + + subdirectory = Path.Combine(directory, "b"); + if (File.Exists(subdirectory)) + { + File.Delete(subdirectory); + } + + file = Path.Combine(subdirectory, "c"); + Directory.CreateDirectory(subdirectory); + + File.WriteAllText(file, String.Empty); + + ProjectItem item = GetOneItemFromFragment(""); + + Assert.AreEqual(@"b\", item.GetMetadataValue("RecursiveDir")); + Assert.AreEqual("c", item.GetMetadataValue("Filename")); + } + finally + { + File.Delete(file); + Directory.Delete(subdirectory); + Directory.Delete(directory); + } + } + + /// + /// Correctly establish the "RecursiveDir" value when the include + /// is semicolon separated. + /// (This is what requires that the original include fragment [before wildcard + /// expansion] is stored in the item.) + /// + [TestMethod] + public void RecursiveDirWithSemicolonSeparatedInclude() + { + string directory = null; + string subdirectory = null; + string file = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), "a"); + if (File.Exists(directory)) + { + File.Delete(directory); + } + + subdirectory = Path.Combine(directory, "b"); + if (File.Exists(subdirectory)) + { + File.Delete(subdirectory); + } + + file = Path.Combine(subdirectory, "c"); + Directory.CreateDirectory(subdirectory); + + File.WriteAllText(file, String.Empty); + + IList items = GetItemsFromFragment(""); + + Assert.AreEqual(3, items.Count); + Assert.AreEqual("i0", items[0].EvaluatedInclude); + Assert.AreEqual(@"b\", items[1].GetMetadataValue("RecursiveDir")); + Assert.AreEqual("i2", items[2].EvaluatedInclude); + } + finally + { + File.Delete(file); + Directory.Delete(subdirectory); + Directory.Delete(directory); + } + } + + /// + /// Basic exclude case + /// + [TestMethod] + public void Exclude() + { + IList items = GetItemsFromFragment(""); + + Assert.AreEqual(1, items.Count); + Assert.AreEqual("a", items[0].EvaluatedInclude); + } + + /// + /// Exclude against an include with item vectors in it + /// + [TestMethod] + public void ExcludeWithIncludeVector() + { + string content = @" + + + + + + + + + + + + "; + + IList items = GetItems(content); + + // Should contain a, b, c, x, z, a, c, u, w + Assert.AreEqual(9, items.Count); + AssertEvaluatedIncludes(items, new string[] { "a", "b", "c", "x", "z", "a", "c", "u", "w" }); + } + + /// + /// Exclude with item vectors against an include with item vectors in it + /// + [TestMethod] + public void ExcludeVectorWithIncludeVector() + { + string content = @" + + + + + + + + + + + + + "; + + IList items = GetItems(content); + + // Should contain a, b, c, z, a, c, u + Assert.AreEqual(7, items.Count); + AssertEvaluatedIncludes(items, new string[] { "a", "b", "c", "z", "a", "c", "u" }); + } + + /// + /// Include and Exclude containing wildcards + /// + [TestMethod] + public void Wildcards() + { + string directory = null; + string file1 = null; + string file2 = null; + string file3 = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), "ProjectItem_Tests_Wildcards"); + Directory.CreateDirectory(directory); + + file1 = Path.Combine(directory, "a.1"); + file2 = Path.Combine(directory, "a.2"); + file3 = Path.Combine(directory, "b.1"); + + File.WriteAllText(file1, String.Empty); + File.WriteAllText(file2, String.Empty); + File.WriteAllText(file3, String.Empty); + + IList items = GetItemsFromFragment(String.Format(@"", directory)); + + Assert.AreEqual(1, items.Count); + Assert.AreEqual(String.Format(@"{0}\a.2", directory), items[0].EvaluatedInclude); + } + finally + { + File.Delete(file1); + File.Delete(file2); + File.Delete(file3); + + Directory.Delete(directory); + } + } + + /// + /// Expression like @(x) should clone metadata, but metadata should still point at the original XML objects + /// + [TestMethod] + public void CopyFromWithItemListExpressionClonesMetadata() + { + string content = @" + + + + m1 + + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + project.GetItems("i").First().SetMetadataValue("m", "m2"); + + ProjectItem item1 = project.GetItems("i").First(); + ProjectItem item2 = project.GetItems("j").First(); + + Assert.AreEqual("m2", item1.GetMetadataValue("m")); + Assert.AreEqual("m1", item2.GetMetadataValue("m")); + + // Should still point at the same XML items + Assert.AreEqual(true, Object.ReferenceEquals(item1.GetMetadata("m").Xml, item2.GetMetadata("m").Xml)); + } + + /// + /// Expression like @(x) should not clone metadata, even if the item type is different. + /// It's obvious that it shouldn't clone it if the item type is the same. + /// If it is different, it doesn't clone it for performance; even if the item definition metadata + /// changes later (this is design time), the inheritors of that item definition type + /// (even those that have subsequently been transformed to a different itemtype) should see + /// the changes, by design. + /// Just to make sure we don't change that behavior, we test it here. + /// + [TestMethod] + public void CopyFromWithItemListExpressionDoesNotCloneDefinitionMetadata() + { + string content = @" + + + + m1 + + + + + + '%(identity)')"" /> + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item1 = project.GetItems("i").First(); + ProjectItem item1b = project.GetItems("i").ElementAt(1); + ProjectItem item1c = project.GetItems("i").ElementAt(2); + ProjectItem item2 = project.GetItems("j").First(); + + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("m1", item1b.GetMetadataValue("m")); + Assert.AreEqual("m1", item1c.GetMetadataValue("m")); + Assert.AreEqual("m1", item2.GetMetadataValue("m")); + + project.ItemDefinitions["i"].SetMetadataValue("m", "m2"); + + // All the items will see this change + Assert.AreEqual("m2", item1.GetMetadataValue("m")); + Assert.AreEqual("m2", item1b.GetMetadataValue("m")); + Assert.AreEqual("m2", item1c.GetMetadataValue("m")); + Assert.AreEqual("m2", item2.GetMetadataValue("m")); + + // And verify we're not still pointing to the definition metadata objects + item1.SetMetadataValue("m", "m3"); + item1b.SetMetadataValue("m", "m4"); + item1c.SetMetadataValue("m", "m5"); + item2.SetMetadataValue("m", "m6"); + + Assert.AreEqual("m2", project.ItemDefinitions["i"].GetMetadataValue("m")); // Should not have been affected + } + + /// + /// Expression like @(x) should not clone metadata, for perf. See comment on test above. + /// + [TestMethod] + public void CopyFromWithItemListExpressionClonesDefinitionMetadata_Variation() + { + string content = @" + + + + m1 + + + + + '%(identity)')"" /> + + + + "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItem item1 = project.GetItems("i").First(); + ProjectItem item1b = project.GetItems("i").ElementAt(1); + ProjectItem item2 = project.GetItems("j").First(); + + Assert.AreEqual("m1", item1.GetMetadataValue("m")); + Assert.AreEqual("m1", item1b.GetMetadataValue("m")); + Assert.AreEqual("m1", item2.GetMetadataValue("m")); + + project.ItemDefinitions["i"].SetMetadataValue("m", "m2"); + + // The items should all see this change + Assert.AreEqual("m2", item1.GetMetadataValue("m")); + Assert.AreEqual("m2", item1b.GetMetadataValue("m")); + Assert.AreEqual("m2", item2.GetMetadataValue("m")); + + // And verify we're not still pointing to the definition metadata objects + item1.SetMetadataValue("m", "m3"); + item1b.SetMetadataValue("m", "m4"); + item2.SetMetadataValue("m", "m6"); + + Assert.AreEqual("m2", project.ItemDefinitions["i"].GetMetadataValue("m")); // Should not have been affected + } + + /// + /// Repeated copying of items with item definitions should cause the following order of precedence: + /// 1) direct metadata on the item + /// 2) item definition metadata on the very first item in the chain + /// 3) item definition on the next item, and so on until + /// 4) item definition metadata on the destination item itself + /// + [TestMethod] + public void CopyWithItemDefinition() + { + string content = @" + + + + l1 + m1 + n1 + + + m2 + o2 +

p2

+
+ + n3 + + +
+ + + l0 + + + +

p4

+
+ + + o4 + +
+
+ "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("l0", project.GetItems("i").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("i").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("i").First().GetMetadataValue("n")); + Assert.AreEqual("", project.GetItems("i").First().GetMetadataValue("o")); + Assert.AreEqual("", project.GetItems("i").First().GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("j").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("j").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("j").First().GetMetadataValue("n")); + Assert.AreEqual("o2", project.GetItems("j").First().GetMetadataValue("o")); + Assert.AreEqual("p2", project.GetItems("j").First().GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("k").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("k").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("k").First().GetMetadataValue("n")); + Assert.AreEqual("o2", project.GetItems("k").First().GetMetadataValue("o")); + Assert.AreEqual("p4", project.GetItems("k").First().GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("l").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("l").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("l").First().GetMetadataValue("n")); + Assert.AreEqual("o2", project.GetItems("l").First().GetMetadataValue("o")); + Assert.AreEqual("p4", project.GetItems("l").First().GetMetadataValue("p")); + + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("l")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("n")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("o")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("m").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("m").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("m").First().GetMetadataValue("n")); + Assert.AreEqual("o4", project.GetItems("m").First().GetMetadataValue("o")); + Assert.AreEqual("p4", project.GetItems("m").First().GetMetadataValue("p")); + + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("l")); + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("n")); + Assert.AreEqual("o4", project.GetItems("m").ElementAt(1).GetMetadataValue("o")); + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("p")); + + // Should still point at the same XML metadata + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("i").First().GetMetadata("l").Xml, project.GetItems("m").First().GetMetadata("l").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("i").First().GetMetadata("m").Xml, project.GetItems("m").First().GetMetadata("m").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("i").First().GetMetadata("n").Xml, project.GetItems("m").First().GetMetadata("n").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("j").First().GetMetadata("o").Xml, project.GetItems("k").First().GetMetadata("o").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("k").First().GetMetadata("p").Xml, project.GetItems("m").First().GetMetadata("p").Xml)); + Assert.AreEqual(true, !Object.ReferenceEquals(project.GetItems("j").First().GetMetadata("p").Xml, project.GetItems("m").First().GetMetadata("p").Xml)); + } + + /// + /// Repeated copying of items with item definitions should cause the following order of precedence: + /// 1) direct metadata on the item + /// 2) item definition metadata on the very first item in the chain + /// 3) item definition on the next item, and so on until + /// 4) item definition metadata on the destination item itself + /// + [TestMethod] + public void CopyWithItemDefinition2() + { + string content = @" + + + + l1 + m1 + n1 + + + m2 + o2 +

p2

+
+ + n3 + + +
+ + + l0 + + + +

p4

+
+ + + o4 + +
+
+ "; + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("l0", project.GetItems("i").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("i").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("i").First().GetMetadataValue("n")); + Assert.AreEqual("", project.GetItems("i").First().GetMetadataValue("o")); + Assert.AreEqual("", project.GetItems("i").First().GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("j").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("j").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("j").First().GetMetadataValue("n")); + Assert.AreEqual("o2", project.GetItems("j").First().GetMetadataValue("o")); + Assert.AreEqual("p2", project.GetItems("j").First().GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("k").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("k").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("k").First().GetMetadataValue("n")); + Assert.AreEqual("o2", project.GetItems("k").First().GetMetadataValue("o")); + Assert.AreEqual("p4", project.GetItems("k").First().GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("l").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("l").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("l").First().GetMetadataValue("n")); + Assert.AreEqual("o2", project.GetItems("l").First().GetMetadataValue("o")); + Assert.AreEqual("p4", project.GetItems("l").First().GetMetadataValue("p")); + + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("l")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("n")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("o")); + Assert.AreEqual("", project.GetItems("l").ElementAt(1).GetMetadataValue("p")); + + Assert.AreEqual("l0", project.GetItems("m").First().GetMetadataValue("l")); + Assert.AreEqual("m1", project.GetItems("m").First().GetMetadataValue("m")); + Assert.AreEqual("n1", project.GetItems("m").First().GetMetadataValue("n")); + Assert.AreEqual("o4", project.GetItems("m").First().GetMetadataValue("o")); + Assert.AreEqual("p4", project.GetItems("m").First().GetMetadataValue("p")); + + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("l")); + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("m")); + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("n")); + Assert.AreEqual("o4", project.GetItems("m").ElementAt(1).GetMetadataValue("o")); + Assert.AreEqual("", project.GetItems("m").ElementAt(1).GetMetadataValue("p")); + + // Should still point at the same XML metadata + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("i").First().GetMetadata("l").Xml, project.GetItems("m").First().GetMetadata("l").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("i").First().GetMetadata("m").Xml, project.GetItems("m").First().GetMetadata("m").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("i").First().GetMetadata("n").Xml, project.GetItems("m").First().GetMetadata("n").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("j").First().GetMetadata("o").Xml, project.GetItems("k").First().GetMetadata("o").Xml)); + Assert.AreEqual(true, Object.ReferenceEquals(project.GetItems("k").First().GetMetadata("p").Xml, project.GetItems("m").First().GetMetadata("p").Xml)); + Assert.AreEqual(true, !Object.ReferenceEquals(project.GetItems("j").First().GetMetadata("p").Xml, project.GetItems("m").First().GetMetadata("p").Xml)); + } + + /// + /// Metadata on items can refer to metadata above + /// + [TestMethod] + public void MetadataReferringToMetadataAbove() + { + string content = @" + + + + v1 + %(m1);v2;%(m0) + + + + "; + + ProjectItem item = GetOneItem(content); + + var itemMetadata = Helpers.MakeList(item.Metadata); + Assert.AreEqual(2, itemMetadata.Count); + Assert.AreEqual("v1;v2;", item.GetMetadataValue("m2")); + } + + /// + /// Built-in metadata should work, too. + /// NOTE: To work properly, this should batch. This is a temporary "patch" to make it work for now. + /// It will only give correct results if there is exactly one item in the Include. Otherwise Batching would be needed. + /// + [TestMethod] + public void BuiltInMetadataExpression() + { + string content = @" + + + + %(Identity) + + + + "; + + ProjectItem item = GetOneItem(content); + + Assert.AreEqual("i1", item.GetMetadataValue("m")); + } + + /// + /// Qualified built in metadata should work + /// + [TestMethod] + public void BuiltInQualifiedMetadataExpression() + { + string content = @" + + + + %(i.Identity) + + + + "; + + ProjectItem item = GetOneItem(content); + + Assert.AreEqual("i1", item.GetMetadataValue("m")); + } + + /// + /// Mis-qualified built in metadata should not work + /// + [TestMethod] + public void BuiltInMisqualifiedMetadataExpression() + { + string content = @" + + + + %(j.Identity) + + + + "; + + ProjectItem item = GetOneItem(content); + + Assert.AreEqual(String.Empty, item.GetMetadataValue("m")); + } + + /// + /// Metadata condition should work correctly with built-in metadata + /// + [TestMethod] + public void BuiltInMetadataInMetadataCondition() + { + string content = @" + + + + m1 + n1 + + + + "; + + ProjectItem item = GetOneItem(content); + + Assert.AreEqual("m1", item.GetMetadataValue("m")); + Assert.AreEqual(String.Empty, item.GetMetadataValue("n")); + } + + /// + /// Metadata on item condition not allowed (currently) + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void BuiltInMetadataInItemCondition() + { + string content = @" + + + + + + "; + + GetOneItem(content); + } + + /// + /// Two items should each get their own values for built-in metadata + /// + [TestMethod] + public void BuiltInMetadataTwoItems() + { + string content = @" + + + + %(Filename).obj + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"i1.obj", items[0].GetMetadataValue("m")); + Assert.AreEqual(@"i2.obj", items[1].GetMetadataValue("m")); + } + + /// + /// Items from another list, but with different metadata + /// + [TestMethod] + public void DifferentMetadataItemsFromOtherList() + { + string content = @" + + + + m1 + + + + + %(m) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"m1", items[0].GetMetadataValue("m")); + Assert.AreEqual(String.Empty, items[1].GetMetadataValue("m")); + } + + /// + /// Items from another list, but with different metadata + /// + [TestMethod] + public void DifferentBuiltInMetadataItemsFromOtherList() + { + string content = @" + + + + + + + %(extension) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@".x", items[0].GetMetadataValue("m")); + Assert.AreEqual(@".y", items[1].GetMetadataValue("m")); + } + + /// + /// Two items coming from a transform + /// + [TestMethod] + public void BuiltInMetadataTransformInInclude() + { + string content = @" + + + + + + '%(Identity).baz')""> + %(Filename)%(Extension).obj + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"h0.baz.obj", items[0].GetMetadataValue("m")); + Assert.AreEqual(@"h1.baz.obj", items[1].GetMetadataValue("m")); + } + + /// + /// Transform in the metadata value; no bare metadata involved + /// + [TestMethod] + public void BuiltInMetadataTransformInMetadataValue() + { + string content = @" + + + + + + + @(i);@(h->'%(Filename)') + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"i0;h0;h1", items[1].GetMetadataValue("m")); + Assert.AreEqual(@"i0;h0;h1", items[2].GetMetadataValue("m")); + } + + /// + /// Transform in the metadata value; bare metadata involved + /// + [TestMethod] + public void BuiltInMetadataTransformInMetadataValueBareMetadataPresent() + { + string content = @" + + + + + + + @(i);@(h->'%(Filename)');%(Extension) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"i0.x;h0;h1;.y", items[1].GetMetadataValue("m")); + Assert.AreEqual(@"i0.x;h0;h1;", items[2].GetMetadataValue("m")); + } + + /// + /// Metadata on items can refer to item lists + /// + [TestMethod] + public void MetadataValueReferringToItems() + { + string content = @" + + + + + + @(h);@(i) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual("h0;i0", items[1].GetMetadataValue("m1")); + } + + /// + /// Metadata on items' conditions can refer to item lists + /// + [TestMethod] + public void MetadataConditionReferringToItems() + { + string content = @" + + + + + + v1 + v2 + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual("v1", items[1].GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, items[1].GetMetadataValue("m2")); + } + + /// + /// Metadata on items' conditions can refer to other metadata + /// + [TestMethod] + public void MetadataConditionReferringToMetadataOnSameItem() + { + string content = @" + + + + 0 + 1 + 2 + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual("0", items[0].GetMetadataValue("m0")); + Assert.AreEqual("1", items[0].GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, items[0].GetMetadataValue("m2")); + } + + /// + /// Remove a metadatum + /// + [TestMethod] + public void RemoveMetadata() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + item.SetMetadataValue("m", "m1"); + project.ReevaluateIfNecessary(); + + bool found = item.RemoveMetadata("m"); + + Assert.AreEqual(true, found); + Assert.AreEqual(true, project.IsDirty); + Assert.AreEqual(String.Empty, item.GetMetadataValue("m")); + Assert.AreEqual(0, Helpers.Count(item.Xml.Metadata)); + } + + /// + /// Attempt to remove a metadatum originating from an item definition. + /// Should fail if it was not overridden. + /// + [TestMethod] + public void RemoveItemDefinitionMetadataMasked() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItemDefinition("i").AddMetadata("m", "m1"); + xml.AddItem("i", "i1").AddMetadata("m", "m2"); + Project project = new Project(xml); + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + + bool found = item.RemoveMetadata("m"); + Assert.AreEqual(true, found); + Assert.AreEqual(0, item.DirectMetadataCount); + Assert.AreEqual(0, Helpers.Count(item.DirectMetadata)); + Assert.AreEqual("m1", item.GetMetadataValue("m")); // Now originating from definition! + Assert.AreEqual(true, project.IsDirty); + Assert.AreEqual(0, item.Xml.Count); + } + + /// + /// Attempt to remove a metadatum originating from an item definition. + /// Should fail if it was not overridden. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void RemoveItemDefinitionMetadataNotMasked() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItemDefinition("i").AddMetadata("m", "m1"); + xml.AddItem("i", "i1"); + Project project = new Project(xml); + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + + item.RemoveMetadata("m"); // Should throw + } + + /// + /// Remove a nonexistent metadatum + /// + [TestMethod] + public void RemoveNonexistentMetadata() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + bool found = item.RemoveMetadata("m"); + + Assert.AreEqual(false, found); + Assert.AreEqual(false, project.IsDirty); + } + + /// + /// Tests removing built-in metadata. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RemoveBuiltInMetadata() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddItem("i", "i1"); + Project project = new Project(xml); + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + + // This should throw + item.RemoveMetadata("FullPath"); + } + + /// + /// Simple rename + /// + [TestMethod] + public void Rename() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + // populate built in metadata cache for this item, to verify the cache is cleared out by the rename + Assert.AreEqual("i1", item.GetMetadataValue("FileName")); + + item.Rename("i2"); + + Assert.AreEqual("i2", item.Xml.Include); + Assert.AreEqual("i2", item.EvaluatedInclude); + Assert.AreEqual(true, project.IsDirty); + Assert.AreEqual("i2", item.GetMetadataValue("FileName")); + } + + /// + /// Verifies that renaming a ProjectItem whose xml backing is a wildcard doesn't corrupt + /// the MSBuild evaluation data. + /// + [TestMethod] + public void RenameItemInProjectWithWildcards() + { + string projectDirectory = Path.Combine(this.TestContext.TestRunDirectory, Path.GetRandomFileName()); + Directory.CreateDirectory(projectDirectory); + try + { + string sourceFile = Path.Combine(projectDirectory, "a.cs"); + string renamedSourceFile = Path.Combine(projectDirectory, "b.cs"); + File.Create(sourceFile).Dispose(); + var project = new Project(); + project.AddItem("File", "*.cs"); + project.FullPath = Path.Combine(projectDirectory, "test.proj"); // assign a path so the wildcards can lock onto something. + project.ReevaluateIfNecessary(); + + var projectItem = project.Items.Single(); + Assert.AreEqual(Path.GetFileName(sourceFile), projectItem.EvaluatedInclude); + Assert.AreSame(projectItem, project.GetItemsByEvaluatedInclude(projectItem.EvaluatedInclude).Single()); + projectItem.Rename(Path.GetFileName(renamedSourceFile)); + File.Move(sourceFile, renamedSourceFile); // repro w/ or w/o this + project.ReevaluateIfNecessary(); + projectItem = project.Items.Single(); + Assert.AreEqual(Path.GetFileName(renamedSourceFile), projectItem.EvaluatedInclude); + Assert.AreSame(projectItem, project.GetItemsByEvaluatedInclude(projectItem.EvaluatedInclude).Single()); + } + finally + { + Directory.Delete(projectDirectory, recursive: true); + } + } + + /// + /// Change item type + /// + [TestMethod] + public void ChangeItemType() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + item.ItemType = "j"; + + Assert.AreEqual("j", item.ItemType); + Assert.AreEqual(true, project.IsDirty); + } + + /// + /// Change item type to invalid value + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ChangeItemTypeInvalid() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + project.ReevaluateIfNecessary(); + + item.ItemType = "|"; + } + + /// + /// Attempt to rename imported item should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void RenameImported() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + Project import = new Project(); + import.AddItem("i", "i1"); + import.Save(file); + + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport(file); + Project project = new Project(xml); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + + item.Rename("i2"); + } + finally + { + File.Delete(file); + } + } + + /// + /// Attempt to set metadata on imported item should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetMetadataImported() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + Project import = new Project(); + import.AddItem("i", "i1"); + import.Save(file); + + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport(file); + Project project = new Project(xml); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + + item.SetMetadataValue("m", "m0"); + } + finally + { + File.Delete(file); + } + } + + /// + /// Attempt to remove metadata on imported item should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void RemoveMetadataImported() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + Project import = new Project(); + ProjectItem item = import.AddItem("i", "i1")[0]; + item.SetMetadataValue("m", "m0"); + import.Save(file); + + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport(file); + Project project = new Project(xml); + + item = Helpers.GetFirst(project.GetItems("i")); + + item.RemoveMetadata("m"); + } + finally + { + File.Delete(file); + } + } + + /// + /// Get items of item type "i" with using the item xml fragment passed in + /// + private static IList GetItemsFromFragment(string fragment) + { + string content = String.Format + ( + @" + + + {0} + + + ", + fragment + ); + + IList items = GetItems(content); + return items; + } + + /// + /// Get the item of type "i" using the item Xml fragment provided. + /// If there is more than one, fail. + /// + private static ProjectItem GetOneItemFromFragment(string fragment) + { + IList items = GetItemsFromFragment(fragment); + + Assert.AreEqual(1, items.Count); + return items[0]; + } + + /// + /// Get the items of type "i" in the project provided + /// + private static IList GetItems(string content) + { + ProjectRootElement projectXml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(projectXml); + IList item = Helpers.MakeList(project.GetItems("i")); + + return item; + } + + /// + /// Get the item of type "i" in the project provided. + /// If there is more than one, fail. + /// + private static ProjectItem GetOneItem(string content) + { + IList items = GetItems(content); + + Assert.AreEqual(1, items.Count); + return items[0]; + } + + /// + /// Asserts that the list of items has the specified includes. + /// + private static void AssertEvaluatedIncludes(IList items, string[] includes) + { + for (int i = 0; i < includes.Length; i++) + { + Assert.AreEqual(includes[i], items[i].EvaluatedInclude); + } + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectMetadata_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectMetadata_Tests.cs new file mode 100644 index 00000000000..53b5ecca186 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectMetadata_Tests.cs @@ -0,0 +1,525 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectMetadata +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for ProjectMetadata + /// + [TestClass] + public class ProjectMetadata_Tests + { + /// + /// Project getter + /// + [TestMethod] + public void ProjectGetter() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + ProjectMetadata metadatum = item.SetMetadataValue("m", "m1"); + + Assert.AreEqual(true, Object.ReferenceEquals(project, metadatum.Project)); + } + + /// + /// Set a new metadata value via the evaluated ProjectMetadata object + /// + [TestMethod] + public void SetUnevaluatedValue() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v1 + v%253 + + + + "); + + ProjectRootElement projectXml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(projectXml); + + Assert.AreEqual(false, project.IsDirty); + + Helpers.GetFirst(project.GetItems("i")).SetMetadataValue("m1", "v2"); + Helpers.GetFirst(project.GetItems("i")).SetMetadataValue("m2", "v%214"); + + Assert.AreEqual(true, project.IsDirty); + + StringWriter writer = new StringWriter(); + projectXml.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + v2 + v%214 + + + + "); + + Helpers.CompareProjectXml(expected, writer.ToString()); + Assert.AreEqual("v!4", Helpers.GetFirst(project.GetItems("i")).GetMetadataValue("m2")); + } + + /// + /// If the value doesn't change then the project shouldn't dirty + /// + [TestMethod] + public void SetUnchangedValue() + { + Project project = new Project(); + ProjectItem item = project.AddItem("i", "i1")[0]; + item.SetMetadataValue("m", "m1"); + project.ReevaluateIfNecessary(); + + item.SetMetadataValue("m", "m1"); + + Assert.AreEqual(false, project.IsDirty); + + item.GetMetadata("m").UnevaluatedValue = "m1"; + + Assert.AreEqual(false, project.IsDirty); + } + + /// + /// Properties should be expanded + /// + [TestMethod] + public void SetValueWithPropertyExpression() + { + Project project = new Project(); + project.SetProperty("p", "p0"); + ProjectItem item = project.AddItem("i", "i1")[0]; + ProjectMetadata metadatum = item.SetMetadataValue("m", "m1"); + project.ReevaluateIfNecessary(); + + metadatum.UnevaluatedValue = "$(p)"; + + Assert.AreEqual("$(p)", metadatum.UnevaluatedValue); + Assert.AreEqual("p0", metadatum.EvaluatedValue); + } + + /// + /// Items should be expanded + /// + [TestMethod] + public void SetValueWithItemExpression() + { + Project project = new Project(); + project.AddItem("i", "i1"); + ProjectItem item = project.AddItem("j", "j1")[0]; + ProjectMetadata metadatum = item.SetMetadataValue("m", "@(i)"); + project.ReevaluateIfNecessary(); + + metadatum.UnevaluatedValue = "@(i)"; + + Assert.AreEqual("@(i)", metadatum.UnevaluatedValue); + Assert.AreEqual("i1", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with a qualified metadata expression. + /// Per 3.5, this expands to nothing. + /// + [TestMethod] + public void SetValueWithQualifiedMetadataExpressionOtherItemType() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v1 + + + v2 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("j")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(i.m1)"; + + Assert.AreEqual("%(i.m1)", metadatum.UnevaluatedValue); + Assert.AreEqual(String.Empty, metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with a qualified metadata expression of the same item type + /// + [TestMethod] + public void SetValueWithQualifiedMetadataExpressionSameItemType() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(i.m0)"; + + Assert.AreEqual("%(i.m0)", metadatum.UnevaluatedValue); + Assert.AreEqual("v0", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with a qualified metadata expression of the same item type + /// + [TestMethod] + public void SetValueWithQualifiedMetadataExpressionSameMetadata() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(i.m1)"; + + Assert.AreEqual("%(i.m1)", metadatum.UnevaluatedValue); + Assert.AreEqual(String.Empty, metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with an unqualified metadata expression + /// + [TestMethod] + public void SetValueWithUnqualifiedMetadataExpression() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(m0)"; + + Assert.AreEqual("%(m0)", metadatum.UnevaluatedValue); + Assert.AreEqual("v0", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with an unqualified metadata expression + /// Value from an item definition + /// + [TestMethod] + public void SetValueWithUnqualifiedMetadataExpressionFromItemDefinition() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + + + + + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(m0)"; + + Assert.AreEqual("%(m0)", metadatum.UnevaluatedValue); + Assert.AreEqual("v0", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with a qualified metadata expression + /// Value from an item definition + /// + [TestMethod] + public void SetValueWithQualifiedMetadataExpressionFromItemDefinition() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + + + + + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(i.m0)"; + + Assert.AreEqual("%(i.m0)", metadatum.UnevaluatedValue); + Assert.AreEqual("v0", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value with an qualified metadata expression + /// of the wrong item type. + /// Per 3.5, this evaluates to nothing. + /// + [TestMethod] + public void SetValueWithQualifiedMetadataExpressionWrongItemType() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + + + v0 + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(j.m0)"; + + Assert.AreEqual("%(j.m0)", metadatum.UnevaluatedValue); + Assert.AreEqual(String.Empty, metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value on an item definition with an unqualified metadata expression + /// + [TestMethod] + public void SetValueOnItemDefinitionWithUnqualifiedMetadataExpression() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + + + + + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItemDefinition itemDefinition; + project.ItemDefinitions.TryGetValue("i", out itemDefinition); + ProjectMetadata metadatum = itemDefinition.GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(m0)"; + + Assert.AreEqual("%(m0)", metadatum.UnevaluatedValue); + Assert.AreEqual("v0", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value on an item definition with an qualified metadata expression + /// + [TestMethod] + public void SetValueOnItemDefinitionWithQualifiedMetadataExpression() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItemDefinition itemDefinition; + project.ItemDefinitions.TryGetValue("i", out itemDefinition); + ProjectMetadata metadatum = itemDefinition.GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(i.m0)"; + + Assert.AreEqual("%(i.m0)", metadatum.UnevaluatedValue); + Assert.AreEqual("v0", metadatum.EvaluatedValue); + } + + /// + /// Set a new metadata value on an item definition with an qualified metadata expression + /// of the wrong item type. + /// Per 3.5, this evaluates to empty string. + /// + [TestMethod] + public void SetValueOnItemDefinitionWithQualifiedMetadataExpressionWrongItemType() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + v0 + + + v0 + v1 + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + ProjectItemDefinition itemDefinition; + project.ItemDefinitions.TryGetValue("i", out itemDefinition); + ProjectMetadata metadatum = itemDefinition.GetMetadata("m1"); + metadatum.UnevaluatedValue = "%(j.m0)"; + + Assert.AreEqual("%(j.m0)", metadatum.UnevaluatedValue); + Assert.AreEqual(String.Empty, metadatum.EvaluatedValue); + } + + /// + /// IsImported = false + /// + [TestMethod] + public void IsImportedFalse() + { + Project project = new Project(); + ProjectMetadata metadata = project.AddItem("i", "i1")[0].SetMetadataValue("m", "m1"); + + Assert.AreEqual(false, metadata.IsImported); + } + + /// + /// Attempt to set metadata on imported item should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetMetadataImported() + { + ProjectRootElement import = ProjectRootElement.Create("import"); + ProjectItemElement itemXml = import.AddItem("i", "i1"); + itemXml.AddMetadata("m", "m0"); + + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport("import"); + Project project = new Project(xml); + + ProjectItem item = Helpers.GetFirst(project.GetItems("i")); + + ProjectMetadata metadata = item.GetMetadata("m"); + + Assert.AreEqual(true, metadata.IsImported); + + metadata.UnevaluatedValue = "m1"; + } + + /// + /// Escaping in metadata values + /// + [TestMethod] + public void SpecialCharactersInMetadataValueConstruction() + { + string projectString = ObjectModelHelpers.CleanupFileContents(@" + + + %3B + %24 + + +"); + System.Xml.XmlReader reader = new System.Xml.XmlTextReader(new StringReader(projectString)); + Microsoft.Build.Evaluation.Project project = new Microsoft.Build.Evaluation.Project(reader); + Microsoft.Build.Evaluation.ProjectItem item = project.GetItems("None").Single(); + + SpecialCharactersInMetadataValueTests(item); + } + + /// + /// Escaping in metadata values + /// + [TestMethod] + public void SpecialCharactersInMetadataValueEvaluation() + { + Microsoft.Build.Evaluation.Project project = new Microsoft.Build.Evaluation.Project(); + var metadata = new Dictionary + { + { "EscapedSemicolon", "%3B" }, // Microsoft.Build.Internal.Utilities.Escape(";") + { "EscapedDollarSign", "%24" }, // Microsoft.Build.Internal.Utilities.Escape("$") + }; + Microsoft.Build.Evaluation.ProjectItem item = project.AddItem( + "None", + "MetadataTests", + metadata).Single(); + + SpecialCharactersInMetadataValueTests(item); + project.ReevaluateIfNecessary(); + SpecialCharactersInMetadataValueTests(item); + } + + /// + /// Helper for metadata escaping tests + /// + private void SpecialCharactersInMetadataValueTests(Microsoft.Build.Evaluation.ProjectItem item) + { + Assert.AreEqual("%3B", item.GetMetadata("EscapedSemicolon").UnevaluatedValue); + Assert.AreEqual(";", item.GetMetadata("EscapedSemicolon").EvaluatedValue); + Assert.AreEqual(";", item.GetMetadataValue("EscapedSemicolon")); + + Assert.AreEqual("%24", item.GetMetadata("EscapedDollarSign").UnevaluatedValue); + Assert.AreEqual("$", item.GetMetadata("EscapedDollarSign").EvaluatedValue); + Assert.AreEqual("$", item.GetMetadataValue("EscapedDollarSign")); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectProperty_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectProperty_Tests.cs new file mode 100644 index 00000000000..c533341c76c --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProjectProperty_Tests.cs @@ -0,0 +1,289 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectProperty +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for ProjectProperty + /// + [TestClass] + public class ProjectProperty_Tests + { + /// + /// Project getter + /// + [TestMethod] + public void ProjectGetter() + { + Project project = new Project(); + ProjectProperty property = project.SetProperty("p", "v"); + + Assert.AreEqual(true, Object.ReferenceEquals(project, property.Project)); + } + + /// + /// Property with nothing to expand + /// + [TestMethod] + public void NoExpansion() + { + string content = @" + + +

v1

+
+
+ "; + + ProjectProperty property = GetFirstProperty(content); + + Assert.IsNotNull(property.Xml); + Assert.AreEqual("p", property.Name); + Assert.AreEqual("v1", property.EvaluatedValue); + Assert.AreEqual("v1", property.UnevaluatedValue); + } + + /// + /// Embedded property + /// + [TestMethod] + public void ExpandProperty() + { + string content = @" + + + v1 +

$(o)

+
+
+ "; + + ProjectProperty property = GetFirstProperty(content); + + Assert.IsNotNull(property.Xml); + Assert.AreEqual("p", property.Name); + Assert.AreEqual("v1", property.EvaluatedValue); + Assert.AreEqual("$(o)", property.UnevaluatedValue); + } + + /// + /// Set the value of a property + /// + [TestMethod] + public void SetValue() + { + Project project = new Project(); + ProjectProperty property = project.SetProperty("p", "v1"); + project.ReevaluateIfNecessary(); + + property.UnevaluatedValue = "v2"; + + Assert.AreEqual("v2", property.EvaluatedValue); + Assert.AreEqual("v2", property.UnevaluatedValue); + Assert.AreEqual(true, project.IsDirty); + } + + /// + /// Set the value of a property + /// + [TestMethod] + public void SetValue_Escaped() + { + Project project = new Project(); + ProjectProperty property = project.SetProperty("p", "v1"); + project.ReevaluateIfNecessary(); + + property.UnevaluatedValue = "v%282%29"; + + Assert.AreEqual("v(2)", property.EvaluatedValue); + Assert.AreEqual("v%282%29", property.UnevaluatedValue); + Assert.AreEqual(true, project.IsDirty); + } + + /// + /// Set the value of a property to the same value. + /// This should not dirty the project. + /// + [TestMethod] + public void SetValueSameValue() + { + Project project = new Project(); + ProjectProperty property = project.SetProperty("p", "v1"); + project.ReevaluateIfNecessary(); + + property.UnevaluatedValue = "v1"; + + Assert.AreEqual(false, project.IsDirty); + } + + /// + /// Attempt to set the value of a built-in property + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void InvalidSetValueBuiltInProperty() + { + Project project = new Project(); + ProjectProperty property = project.GetProperty("MSBuildProjectDirectory"); + + property.UnevaluatedValue = "v"; + } + + /// + /// Set the value of a property originating in the environment. + /// Should work even though there is no XML behind it. + /// Also, should persist. + /// + [TestMethod] + public void SetValueEnvironmentProperty() + { + Project project = new Project(); + ProjectProperty property = project.GetProperty("Username"); + + property.UnevaluatedValue = "v"; + + Assert.AreEqual("v", property.EvaluatedValue); + Assert.AreEqual("v", property.UnevaluatedValue); + + project.ReevaluateIfNecessary(); + + property = project.GetProperty("Username"); + Assert.AreEqual("v", property.UnevaluatedValue); + } + + /// + /// Test IsEnvironmentVariable + /// + [TestMethod] + public void IsEnvironmentVariable() + { + Project project = new Project(); + + Assert.AreEqual(true, project.GetProperty("username").IsEnvironmentProperty); + Assert.AreEqual(false, project.GetProperty("username").IsGlobalProperty); + Assert.AreEqual(false, project.GetProperty("username").IsReservedProperty); + Assert.AreEqual(false, project.GetProperty("username").IsImported); + } + + /// + /// Test IsGlobalProperty + /// + [TestMethod] + public void IsGlobalProperty() + { + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties["g"] = String.Empty; + Project project = new Project(globalProperties, null, ProjectCollection.GlobalProjectCollection); + + Assert.AreEqual(false, project.GetProperty("g").IsEnvironmentProperty); + Assert.AreEqual(true, project.GetProperty("g").IsGlobalProperty); + Assert.AreEqual(false, project.GetProperty("g").IsReservedProperty); + Assert.AreEqual(false, project.GetProperty("g").IsImported); + } + + /// + /// Test IsReservedProperty + /// + [TestMethod] + public void IsReservedProperty() + { + Project project = new Project(); + project.FullPath = @"c:\x"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual(false, project.GetProperty("MSBuildProjectFile").IsEnvironmentProperty); + Assert.AreEqual(false, project.GetProperty("MSBuildProjectFile").IsGlobalProperty); + Assert.AreEqual(true, project.GetProperty("MSBuildProjectFile").IsReservedProperty); + Assert.AreEqual(false, project.GetProperty("MSBuildProjectFile").IsImported); + } + + /// + /// Verify properties are expanded in new property values + /// + [TestMethod] + public void SetPropertyWithPropertyExpression() + { + Project project = new Project(); + project.SetProperty("p0", "v0"); + ProjectProperty property = project.SetProperty("p1", "v1"); + property.UnevaluatedValue = "$(p0)"; + + Assert.AreEqual("v0", project.GetPropertyValue("p1")); + Assert.AreEqual("v0", property.EvaluatedValue); + Assert.AreEqual("$(p0)", property.UnevaluatedValue); + } + + /// + /// Verify item expressions are not expanded in new property values. + /// NOTE: They aren't expanded to "blank". It just seems like that, because + /// when you output them, item expansion happens after property expansion, and + /// they may evaluate to blank then. (Unless items do exist at that point.) + /// + [TestMethod] + public void SetPropertyWithItemAndMetadataExpression() + { + Project project = new Project(); + project.SetProperty("p0", "v0"); + ProjectProperty property = project.SetProperty("p1", "v1"); + property.UnevaluatedValue = "@(i)-%(m)"; + + Assert.AreEqual("@(i)-%(m)", project.GetPropertyValue("p1")); + Assert.AreEqual("@(i)-%(m)", property.EvaluatedValue); + Assert.AreEqual("@(i)-%(m)", property.UnevaluatedValue); + } + + /// + /// Attempt to set value on imported property should fail + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetPropertyImported() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + Project import = new Project(); + import.SetProperty("p", "v0"); + import.Save(file); + + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport(file); + Project project = new Project(xml); + + ProjectProperty property = project.GetProperty("p"); + property.UnevaluatedValue = "v1"; + } + finally + { + File.Delete(file); + } + } + + /// + /// Get the property named "p" in the project provided + /// + private static ProjectProperty GetFirstProperty(string content) + { + ProjectRootElement projectXml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(projectXml); + ProjectProperty property = project.GetProperty("p"); + + return property; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/Project_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/Project_Tests.cs new file mode 100644 index 00000000000..29e1d0ce453 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/Project_Tests.cs @@ -0,0 +1,2454 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for Project +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; +using ToolLocationHelper = Microsoft.Build.Utilities.ToolLocationHelper; +using TargetDotNetFrameworkVersion = Microsoft.Build.Utilities.TargetDotNetFrameworkVersion; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for Project public members + /// + [TestClass] + public class Project_Tests + { + /// + /// Clear out the global project collection + /// + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Clear out the global project collection + /// + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.Count); + + IDictionary globalProperties = ProjectCollection.GlobalProjectCollection.GlobalProperties; + foreach (string propertyName in globalProperties.Keys) + { + ProjectCollection.GlobalProjectCollection.RemoveGlobalProperty(propertyName); + } + + Assert.AreEqual(0, ProjectCollection.GlobalProjectCollection.GlobalProperties.Count); + } + + /// + /// Since when the project file is saved it may be intented we want to make sure the indent charachters do not affect the evaluation against empty. + /// We test here newline, tab, and carriage return. + /// + [TestMethod] + [TestCategory("serialize")] + public void VerifyNewLinesAndTabsEvaluateToEmpty() + { + MockLogger mockLogger = new MockLogger(); + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + " + Environment.NewLine + Environment.NewLine + "" + + "\t\t\t\t" + + "\r\r\r\r"+ + @"NewLineEvalAsEmpty + TabEvalAsEmpty + CarriageReturnEvalAsEmpty + + + + + + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + Project project = new Project(xml); + bool result = project.Build(new ILogger[] { mockLogger }); + Assert.AreEqual(true, result); + mockLogger.AssertLogContains("NewLineEvalAsEmpty"); + mockLogger.AssertLogContains("TabEvalAsEmpty"); + mockLogger.AssertLogContains("CarriageReturnEvalAsEmpty"); + } + + /// + /// Make sure if we build a project and specify no loggers that the loggers registered on the project collection is the one used. + /// + [TestMethod] + [TestCategory("serialize")] + public void LogWithLoggersOnProjectCollection() + { + MockLogger mockLogger = new MockLogger(); + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + ProjectCollection collection = new ProjectCollection(); + collection.RegisterLogger(mockLogger); + Project project = new Project(xml, null, null, collection); + + bool result = project.Build(); + Assert.AreEqual(true, result); + mockLogger.AssertLogContains("IHaveBeenLogged"); + } + + /// + /// Make sure if we build a project and specify we specify a custom logger that the custom logger is used instead of the one registered on the project collection. + /// + [TestMethod] + [TestCategory("serialize")] + public void LogWithLoggersOnProjectCollectionCustomOneUsed() + { + MockLogger mockLogger = new MockLogger(); + MockLogger mockLogger2 = new MockLogger(); + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + ProjectCollection collection = new ProjectCollection(); + collection.RegisterLogger(mockLogger2); + Project project = new Project(xml, null, null, collection); + + bool result = project.Build(mockLogger); + Assert.AreEqual(true, result); + mockLogger.AssertLogContains("IHaveBeenLogged"); + mockLogger2.AssertLogDoesntContain("IHaveBeenLogged"); + } + + /// + /// Load a project from a file path + /// + [TestMethod] + public void BasicFromFile() + { + string file = null; + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + string content = GetSampleProjectContent(); + File.WriteAllText(file, content); + + Project project = new Project(file); + + VerifyContentOfSampleProject(project); + } + finally + { + File.Delete(file); + } + } + + /// + /// Load a project from a file path that has valid XML that does not + /// evaluate successfully; then trying again after fixing the file should succeed. + /// + [TestMethod] + public void FailedEvaluationClearsXmlCache() + { + string file = Path.GetTempPath() + "\\" + Guid.NewGuid().ToString("N"); + + try + { + var xml = ProjectRootElement.Create(file); + xml.AddItem("i", "i1").Condition = "typo in ''condition''"; + xml.Save(); + + Project project = null; + try + { + project = new Project(file); + } + catch (InvalidProjectFileException ex) + { + Console.WriteLine(ex.Message); + } + + // Verify that we don't now have invalid project XML left in the cache + // by writing out valid project XML and trying again; + // Don't save through the OM or the cache would get updated; do it directly + File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents(@"")); + + project = new Project(file); // should not throw + } + finally + { + File.Delete(file); + } + } + + /// + /// Reading from an XMLReader that has no content should throw the correct + /// exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadFromEmptyReader1() + { + XmlReader reader = XmlReader.Create(new StringReader(String.Empty)); + ProjectRootElement xml = ProjectRootElement.Create(reader); + } + + /// + /// Reading from an XMLReader that has no content should throw the correct + /// exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadFromEmptyReader2() + { + XmlReader reader = XmlReader.Create(new StringReader(String.Empty)); + Project project = new Project(reader); + } + + /// + /// Reading from an XMLReader that has no content should throw the correct + /// exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadFromEmptyReader3() + { + // Variation, we have a reader but it's already read + XmlReader reader = XmlReader.Create(new StringReader(ProjectRootElement.Create().RawXml)); + + while (reader.Read()) + { + } + + Project project = (new ProjectCollection()).LoadProject(reader); + } + + /// + /// Reading from an XMLReader that was closed should throw the correct + /// exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReadFromClosedReader() + { + XmlReader reader = XmlReader.Create(new StringReader(String.Empty)); + reader.Close(); + Project project = new Project(reader); + } + + /// + /// Reading from an XMLReader that has TWO valid root elements should work + /// if it's already read past the first one. + /// + [TestMethod] + public void ReadFromReaderTwoDocs() + { + string emptyProject = ObjectModelHelpers.CleanupFileContents(@""); + XmlReader reader = XmlReader.Create(new StringReader(emptyProject + emptyProject), new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment }); + + reader.ReadToFollowing("Project"); + reader.Read(); + + Project project2 = new Project(reader); + + Assert.AreEqual(false, reader.Read()); + } + + /// + /// Import does not exist. Default case is an exception. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ImportDoesNotExistDefaultSettings() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport("__nonexistent__"); + + Project project = new Project(xml); + } + + /// + /// Import gives invalid uri exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ImportInvalidUriFormat() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddImport(@"//MSBuildExtensionsPath32)\4.0\Microsoft.VisualStudioVersion.v11.Common.props"); + + Project project = new Project(xml); + } + + /// + /// Necessary but not sufficient for MSBuild evaluation to be thread safe. + /// + [TestMethod] + public void ConcurrentLoadDoesNotCrash() + { + var tasks = new Task[500]; + + for (int i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Factory.StartNew(delegate() { new Project(); }); // Should not throw + } + + Task.WaitAll(tasks); + } + + /// + /// Import does not exist but ProjectLoadSettings.IgnoreMissingImports was set + /// + [TestMethod] + public void ImportDoesNotExistIgnoreMissingImports() + { + ProjectRootElement xml = ProjectRootElement.Create(); + + xml.AddProperty("p", "1"); + xml.AddImport("__nonexistent__"); + xml.AddProperty("q", "$(p)"); + + Project project = new Project(xml, null, null, new ProjectCollection(), ProjectLoadSettings.IgnoreMissingImports); + + // Make sure some evaluation did occur + Assert.AreEqual("1", project.GetPropertyValue("q")); + } + + /// + /// When we try and access the ImportsIncludingDuplicates property on the project without setting + /// the correct projectloadsetting flag, we expect an invalidoperationexception. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void TryImportsIncludingDuplicatesExpectException() + { + ProjectRootElement xml = ProjectRootElement.Create(); + Project project = new Project(xml, null, null, new ProjectCollection(), ProjectLoadSettings.IgnoreMissingImports); + IList imports = project.ImportsIncludingDuplicates; + Assert.AreEqual(0, imports.Count); + } + + /// + /// Import self ignored + /// + [TestMethod] + public void ImportSelfIgnored() + { + string file = null; + + try + { + ProjectCollection collection = new ProjectCollection(); + MockLogger logger = new MockLogger(); + collection.RegisterLogger(logger); + + Project project = new Project(collection); + project.Xml.AddImport("$(MSBuildProjectFullPath)"); + + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + project.Save(file); + project.ReevaluateIfNecessary(); + + logger.AssertLogContains("MSB4210"); // selfimport + } + finally + { + File.Delete(file); + } + } + + /// + /// Import self indirectly ignored + /// + [TestMethod] + public void ImportSelfIndirectIgnored() + { + string file = null; + string file2 = null; + + try + { + ProjectCollection collection = new ProjectCollection(); + MockLogger logger = new MockLogger(); + collection.RegisterLogger(logger); + + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + Project project = new Project(collection); + project.Xml.AddImport(file2); + project.Save(file); + + Project project2 = new Project(collection); + project2.Xml.AddImport(file); + project2.Save(file2); + + project.ReevaluateIfNecessary(); + + logger.AssertLogContains("MSB4210"); // selfimport + } + finally + { + File.Delete(file); + File.Delete(file2); + } + } + + /// + /// Double import ignored + /// + [TestMethod] + public void DoubleImportIgnored() + { + string file = null; + string file2 = null; + + try + { + ProjectCollection collection = new ProjectCollection(); + MockLogger logger = new MockLogger(); + collection.RegisterLogger(logger); + + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + Project project = new Project(collection); + project.Xml.AddImport(file2); + project.Xml.AddImport(file2); + project.Save(file); + + Project project2 = new Project(collection); + project2.Save(file2); + + project.ReevaluateIfNecessary(); + + logger.AssertLogContains("MSB4011"); // duplicate import + } + finally + { + File.Delete(file); + File.Delete(file2); + } + } + + /// + /// Double import ignored + /// + [TestMethod] + public void DoubleImportIndirectIgnored() + { + string file = null; + string file2 = null; + string file3 = null; + + try + { + ProjectCollection collection = new ProjectCollection(); + MockLogger logger = new MockLogger(); + collection.RegisterLogger(logger); + + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file2 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + file3 = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + + Project project = new Project(collection); + project.Xml.AddImport(file2); + project.Xml.AddImport(file3); + project.Save(file); + + Project project2 = new Project(collection); + project.Xml.AddImport(file3); + project2.Save(file2); + + Project project3 = new Project(collection); + project3.Save(file3); + + project.ReevaluateIfNecessary(); + + logger.AssertLogContains("MSB4011"); // duplicate import + } + finally + { + File.Delete(file); + File.Delete(file2); + File.Delete(file3); + } + } + + /// + /// Basic created from backing XML + /// + [TestMethod] + public void BasicFromXml() + { + ProjectRootElement xml = GetSampleProjectRootElement(); + Project project = new Project(xml); + + VerifyContentOfSampleProject(project); + } + + /// + /// Test Project from an XML with an import. + /// Also verify the Imports collection on the evaluated Project. + /// + [TestMethod] + public void BasicFromXmlFollowImport() + { + string importContent = ObjectModelHelpers.CleanupFileContents(@" + + + v3 + + + + + + + + "); + + string importPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("import.targets", importContent); + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + +

v1

+
+ +

v2

+
+ + X$(p) + + + + + + + + + + +
"); + + projectFileContent = String.Format(projectFileContent, importPath); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + Project project = new Project(xml); + + Assert.AreEqual("v3", project.GetPropertyValue("p2")); + + List items = Helpers.MakeList(project.GetItems("i")); + Assert.AreEqual(4, items.Count); + Assert.AreEqual("i1", items[0].EvaluatedInclude); + Assert.AreEqual("v2X", items[1].EvaluatedInclude); + Assert.AreEqual("i3", items[2].EvaluatedInclude); + Assert.AreEqual("i4", items[3].EvaluatedInclude); + + IList imports = project.Imports; + Assert.AreEqual(1, imports.Count); + Assert.AreEqual(true, Object.ReferenceEquals(imports.First().ImportingElement, xml.Imports.ElementAt(0))); + + // We can take advantage of the fact that we will get the same ProjectRootElement from the cache if we try to + // open it with a path; get that and then compare it to what project.Imports gave us. + Assert.AreEqual(true, Object.ReferenceEquals(imports.First().ImportedProject, ProjectRootElement.Open(importPath))); + + // Test the logical project iterator + List logicalElements = new List(project.GetLogicalProject()); + + Assert.AreEqual(18, logicalElements.Count); + + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + + /// + /// Get items, transforms use correct directory base, ie., the project folder + /// + [TestMethod] + public void TransformsUseCorrectDirectory_Basic() + { + string file = null; + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + + '%(FullPath)')""/> + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + + Project project = new Project(xml); + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + project.Save(file); + project.ReevaluateIfNecessary(); + + Assert.AreEqual(Path.Combine(Path.GetTempPath(), @"obj\i386\foo.dll"), project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + } + finally + { + File.Delete(file); + } + } + + /// + /// Get items, transforms use correct directory base, ie., the current + /// directory at the time of load for a project that was not yet saved + /// + [TestMethod] + public void TransformsUseCorrectDirectory_Basic_NotSaved() + { + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + + '%(FullPath)')""/> + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + + Project project = new Project(xml); + ProjectInstance projectInstance = new ProjectInstance(xml); + + Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, @"obj\i386\foo.dll"), project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, @"obj\i386\foo.dll"), projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + } + + /// + /// Directory transform uses project directory + /// + [TestMethod] + public void TransformsUseCorrectDirectory_DirectoryTransform() + { + string file = null; + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + '%(Directory)')""/> + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + xml.FullPath = file; + + Project project = new Project(xml); + ProjectInstance projectInstance = new ProjectInstance(xml); + + Assert.AreEqual(Path.Combine(Path.GetTempPath().Substring(3) /* remove c:\ */, @"obj\i386\"), project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + Assert.AreEqual(Path.Combine(Path.GetTempPath().Substring(3) /* remove c:\ */, @"obj\i386\"), projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + } + finally + { + File.Delete(file); + } + } + + /// + /// Directory item function uses project directory + /// + [TestMethod] + public void TransformsUseCorrectDirectory_DirectoryItemFunction() + { + string file = null; + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + + Directory())""/> + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + xml.FullPath = file; + + Project project = new Project(xml); + ProjectInstance projectInstance = new ProjectInstance(xml); + + Assert.AreEqual(Path.Combine(Path.GetTempPath().Substring(3) /* remove c:\ */, @"obj\i386\"), project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + Assert.AreEqual(Path.Combine(Path.GetTempPath().Substring(3) /* remove c:\ */, @"obj\i386\"), projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + } + finally + { + File.Delete(file); + } + } + + /// + /// Directory item function uses project directory + /// + [TestMethod] + public void TransformsUseCorrectDirectory_DirectoryNameItemFunction() + { + string file = null; + + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + + + DirectoryName())""/> + + "); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + xml.FullPath = file; + + Project project = new Project(xml); + ProjectInstance projectInstance = new ProjectInstance(xml); + + // Should be the full path to the directory + Assert.AreEqual(Path.Combine(Path.GetTempPath() /* remove c:\ */, @"obj\i386"), project.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + Assert.AreEqual(Path.Combine(Path.GetTempPath() /* remove c:\ */, @"obj\i386"), projectInstance.GetItems("BuiltProjectOutputGroupKeyOutput").First().EvaluatedInclude); + } + finally + { + File.Delete(file); + } + } + + /// + /// Global properties accessor + /// + [TestMethod] + public void GetGlobalProperties() + { + ProjectRootElement xml = GetSampleProjectRootElement(); + var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("g1", "v1"); + globalProperties.Add("g2", "v2"); + Project project = new Project(xml, globalProperties, null); + + Assert.AreEqual("v1", project.GlobalProperties["g1"]); + Assert.AreEqual("v2", project.GlobalProperties["g2"]); + } + + /// + /// Global properties are cloned when passed in: + /// subsequent changes have no effect + /// + [TestMethod] + public void GlobalPropertiesCloned() + { + ProjectRootElement xml = GetSampleProjectRootElement(); + var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("g1", "v1"); + Project project = new Project(xml, globalProperties, null); + + globalProperties.Add("g2", "v2"); + + Assert.AreEqual("v1", project.GlobalProperties["g1"]); + Assert.AreEqual(false, project.GlobalProperties.ContainsKey("g2")); + } + + /// + /// Global properties accessor when no global properties + /// + [TestMethod] + public void GetGlobalPropertiesNone() + { + ProjectRootElement xml = GetSampleProjectRootElement(); + Project project = new Project(xml); + + Assert.AreEqual(0, project.GlobalProperties.Count); + } + + /// + /// Changing global properties should make the project a candidate + /// for reevaluation. + /// + [TestMethod] + public void ChangeGlobalProperties() + { + Project project = new Project(); + ProjectPropertyElement propertyElement = project.Xml.AddProperty("p", "v0"); + propertyElement.Condition = "'$(g)'=='v1'"; + project.ReevaluateIfNecessary(); + Assert.AreEqual(String.Empty, project.GetPropertyValue("p")); + + Assert.AreEqual(true, project.SetGlobalProperty("g", "v1")); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + Assert.AreEqual("v0", project.GetPropertyValue("p")); + Assert.AreEqual("v1", project.GlobalProperties["g"]); + } + + /// + /// Changing global property after reevaluation should not crash + /// + [TestMethod] + public void ChangeGlobalPropertyAfterReevaluation() + { + Project project = new Project(); + project.SetGlobalProperty("p", "v1"); + project.ReevaluateIfNecessary(); + project.SetGlobalProperty("p", "v2"); + + Assert.AreEqual("v2", project.GetPropertyValue("p")); + Assert.AreEqual(true, project.GetProperty("p").IsGlobalProperty); + } + + /// + /// Test the SkipEvaluation functionality of ReevaluateIfNecessary + /// + [TestMethod] + public void SkipEvaluation() + { + Project project = new Project(); + project.SetGlobalProperty("p", "v1"); + project.ReevaluateIfNecessary(); + Assert.AreEqual("v1", project.GetPropertyValue("p")); + + project.SkipEvaluation = true; + ProjectPropertyElement propertyElement = project.Xml.AddProperty("p1", "v0"); + propertyElement.Condition = "'$(g)'=='v1'"; + project.SetGlobalProperty("g", "v1"); + project.ReevaluateIfNecessary(); + Assert.AreEqual(String.Empty, project.GetPropertyValue("p1")); + + project.SkipEvaluation = false; + project.SetGlobalProperty("g", "v1"); + project.ReevaluateIfNecessary(); + Assert.AreEqual("v0", project.GetPropertyValue("p1")); + } + + /// + /// Setting property with same name as global property but after reevaluation should error + /// because the property is global, not fail with null reference exception + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ChangeGlobalPropertyAfterReevaluation2() + { + Project project = new Project(); + project.SetGlobalProperty("p", "v1"); + project.ReevaluateIfNecessary(); + project.SetProperty("p", "v2"); + } + + /// + /// Setting environment property should create a real property + /// + [TestMethod] + public void ChangeEnvironmentProperty() + { + Project project = new Project(); + project.SetProperty("computername", "v1"); + + Assert.AreEqual("v1", project.GetPropertyValue("computername")); + Assert.AreEqual(true, project.IsDirty); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual("v1", project.GetPropertyValue("computername")); + } + + /// + /// Setting a reserved property through the project should error nicely + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void SetReservedPropertyThroughProject() + { + Project project = new Project(); + project.SetProperty("msbuildprojectdirectory", "v1"); + } + + /// + /// Changing global properties with some preexisting. + /// + [TestMethod] + public void ChangeGlobalPropertiesPreexisting() + { + Dictionary initial = new Dictionary(StringComparer.OrdinalIgnoreCase); + initial.Add("p0", "v0"); + initial.Add("p1", "v1"); + Project project = new Project(ProjectRootElement.Create(), initial, null); + ProjectPropertyElement propertyElement = project.Xml.AddProperty("pp", "vv"); + propertyElement.Condition = "'$(p0)'=='v0' and '$(p1)'=='v1b'"; + project.ReevaluateIfNecessary(); + Assert.AreEqual(String.Empty, project.GetPropertyValue("pp")); + + project.SetGlobalProperty("p1", "v1b"); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + Assert.AreEqual("vv", project.GetPropertyValue("pp")); + Assert.AreEqual("v0", project.GlobalProperties["p0"]); + Assert.AreEqual("v1b", project.GlobalProperties["p1"]); + } + + /// + /// Changing global properties with some preexisting from the project collection. + /// Should not modify those on the project collection. + /// + [TestMethod] + public void ChangeGlobalPropertiesInitiallyFromProjectCollection() + { + Dictionary initial = new Dictionary(StringComparer.OrdinalIgnoreCase); + initial.Add("p0", "v0"); + initial.Add("p1", "v1"); + ProjectCollection collection = new ProjectCollection(initial, null, ToolsetDefinitionLocations.Registry); + Project project = new Project(collection); + ProjectPropertyElement propertyElement = project.Xml.AddProperty("pp", "vv"); + propertyElement.Condition = "'$(p0)'=='v0' and '$(p1)'=='v1b'"; + project.ReevaluateIfNecessary(); + Assert.AreEqual(String.Empty, project.GetPropertyValue("pp")); + + project.SetGlobalProperty("p1", "v1b"); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + Assert.AreEqual("vv", project.GetPropertyValue("pp")); + Assert.AreEqual("v0", collection.GlobalProperties["p0"]); + Assert.AreEqual("v1", collection.GlobalProperties["p1"]); + } + + /// + /// Changing global property to the same value should not dirty the project. + /// + [TestMethod] + public void ChangeGlobalPropertiesSameValue() + { + Project project = new Project(); + project.SetGlobalProperty("g", "v1"); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + + Assert.AreEqual(false, project.SetGlobalProperty("g", "v1")); + Assert.AreEqual(false, project.IsDirty); + } + + /// + /// Removing global properties should make the project a candidate + /// for reevaluation. + /// + [TestMethod] + public void RemoveGlobalProperties() + { + Project project = new Project(); + ProjectPropertyElement propertyElement = project.Xml.AddProperty("p", "v0"); + propertyElement.Condition = "'$(g)'==''"; + project.SetGlobalProperty("g", "v1"); + project.ReevaluateIfNecessary(); + Assert.AreEqual(String.Empty, project.GetPropertyValue("p")); + + bool existed = project.RemoveGlobalProperty("g"); + Assert.AreEqual(true, existed); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + Assert.AreEqual("v0", project.GetPropertyValue("p")); + Assert.AreEqual(false, project.GlobalProperties.ContainsKey("g")); + } + + /// + /// Remove nonexistent global property should return false and not dirty the project. + /// + [TestMethod] + public void RemoveNonExistentGlobalProperties() + { + Project project = new Project(); + bool existed = project.RemoveGlobalProperty("x"); + + Assert.AreEqual(false, existed); + Assert.AreEqual(false, project.IsDirty); + } + + /// + /// ToolsVersion accessor for explicitly specified + /// + [TestMethod] + public void GetToolsVersionExplicitlySpecified() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35) == null) + { + // "Requires 3.5 to be installed" + return; + } + + ProjectRootElement xml = GetSampleProjectRootElement(); + Project project = new Project(xml, new Dictionary(StringComparer.OrdinalIgnoreCase), "4.0"); + + Assert.AreEqual("4.0", project.ToolsVersion); + } + + /// + /// ToolsVersion accessor when none was specified. + /// Should not return the value on the project element. + /// + [TestMethod] + public void GetToolsVersionNoneExplicitlySpecified() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.ToolsVersion = String.Empty; + xml.AddTarget("t"); + + Project project = new Project(xml); + + Assert.AreEqual(String.Empty, project.Xml.ToolsVersion); + + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + + /// + /// ToolsVersion defaults to 4.0 + /// + [TestMethod] + public void GetToolsVersionFromProject() + { + Project project = new Project(); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.ToolsVersion); + } + + /// + /// Project.ToolsVersion should be set to ToolsVersion evaluated with, + /// even if it is subsequently changed on the XML (without reevaluation) + /// + [TestMethod] + public void ProjectToolsVersion20Present() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) == null) + { + // "Requires 2.0 to be installed" + return; + } + + Project project = new Project(); + project.Xml.ToolsVersion = "2.0"; + project.ReevaluateIfNecessary(); + + // ... and after all that, we end up defaulting to the current ToolsVersion instead. There's a way + // to turn this behavior (new in Dev12) off, but it requires setting an environment variable and + // clearing some internal state to make sure that the update environment variable is picked up, so + // there's not a good way of doing it from these deliberately public OM only tests. + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.ToolsVersion); + + project.Xml.ToolsVersion = "4.0"; + + // Still defaulting to the current ToolsVersion + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.ToolsVersion); + } + + /// + /// Project.ToolsVersion should be set to ToolsVersion evaluated with, + /// even if it is subsequently changed on the XML (without reevaluation) + /// + [TestMethod] + public void ProjectToolsVersion20NotPresent() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) != null) + { + // "Requires 2.0 to NOT be installed" + return; + } + + Project project = new Project(); + project.Xml.ToolsVersion = "2.0"; + project.ReevaluateIfNecessary(); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.ToolsVersion); + + project.Xml.ToolsVersion = ObjectModelHelpers.MSBuildDefaultToolsVersion; + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.ToolsVersion); + } + + /// + /// $(MSBuildToolsVersion) should be set to ToolsVersion evaluated with, + /// even if it is subsequently changed on the XML (without reevaluation) + /// + [TestMethod] + public void MSBuildToolsVersionProperty() + { + if (ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20) == null) + { + // "Requires 2.0 to be installed" + return; + } + + Project project = new Project(); + project.Xml.ToolsVersion = "2.0"; + project.ReevaluateIfNecessary(); + + // ... and after all that, we end up defaulting to the current ToolsVersion instead. There's a way + // to turn this behavior (new in Dev12) off, but it requires setting an environment variable and + // clearing some internal state to make sure that the update environment variable is picked up, so + // there's not a good way of doing it from these deliberately public OM only tests. + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.GetPropertyValue("msbuildtoolsversion")); + + project.Xml.ToolsVersion = "4.0"; + + // Still current + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.GetPropertyValue("msbuildtoolsversion")); + + project.ReevaluateIfNecessary(); + + // Still current + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.GetPropertyValue("msbuildtoolsversion")); + } + + /// + /// $(MSBuildToolsVersion) should be set to ToolsVersion evaluated with, + /// even if it is subsequently changed on the XML (without reevaluation) + /// + [TestMethod] + public void MSBuildToolsVersionProperty40() + { + Project project = new Project(); + + Assert.AreEqual(ObjectModelHelpers.MSBuildDefaultToolsVersion, project.GetPropertyValue("msbuildtoolsversion")); + } + + /// + /// It's okay to change ToolsVersion to some apparently bogus value -- the project can be persisted + /// that way, and maybe later it'll correspond to some known toolset. If the effective ToolsVersion was being + /// gotten from the attribute, that'll be affected too; and thus might be bogus. + /// + [TestMethod] + public void ChangingToolsVersionAttributeToUnrecognizedValue() + { + Project project = new Project(); + + project.Xml.ToolsVersion = "bogus"; + + Assert.AreEqual("bogus", project.Xml.ToolsVersion); + } + + /// + /// Test Project's surfacing of the sub-toolset version + /// + [TestMethod] + public void GetSubToolsetVersion() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + Project p = new Project(GetSampleProjectRootElement(), null, "4.0", new ProjectCollection()); + + Assert.AreEqual("4.0", p.ToolsVersion); + + Toolset t = p.ProjectCollection.GetToolset("4.0"); + + Assert.AreEqual(t.DefaultSubToolsetVersion, p.SubToolsetVersion); + Assert.AreEqual(t.DefaultSubToolsetVersion, p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Test Project's surfacing of the sub-toolset version when it is overridden by a value in the + /// environment + /// + [TestMethod] + public void GetSubToolsetVersion_FromEnvironment() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "ABCD"); + + Project p = new Project(GetSampleProjectRootElement(), null, "4.0", new ProjectCollection()); + + Assert.AreEqual("4.0", p.ToolsVersion); + Assert.AreEqual("ABCD", p.SubToolsetVersion); + Assert.AreEqual("ABCD", p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Test ProjectInstance's surfacing of the sub-toolset version when it is overridden by a global property + /// + [TestMethod] + public void GetSubToolsetVersion_FromProjectGlobalProperties() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", null); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("VisualStudioVersion", "ABCDE"); + + Project p = new Project(GetSampleProjectRootElement(), globalProperties, "4.0", new ProjectCollection()); + + Assert.AreEqual("4.0", p.ToolsVersion); + Assert.AreEqual("ABCDE", p.SubToolsetVersion); + Assert.AreEqual("ABCDE", p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Verify that if a sub-toolset version is passed to the constructor, it all other heuristic methods for + /// getting the sub-toolset version. + /// + [TestMethod] + public void GetSubToolsetVersion_FromConstructor() + { + string originalVisualStudioVersion = Environment.GetEnvironmentVariable("VisualStudioVersion"); + + try + { + Environment.SetEnvironmentVariable("VisualStudioVersion", "ABC"); + + IDictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("VisualStudioVersion", "ABCD"); + + IDictionary projectCollectionGlobalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + projectCollectionGlobalProperties.Add("VisualStudioVersion", "ABCDE"); + + Project p = new Project(GetSampleProjectRootElement(), globalProperties, "4.0", "ABCDEF", new ProjectCollection(projectCollectionGlobalProperties), ProjectLoadSettings.Default); + + Assert.AreEqual("4.0", p.ToolsVersion); + Assert.AreEqual("ABCDEF", p.SubToolsetVersion); + Assert.AreEqual("ABCDEF", p.GetPropertyValue("VisualStudioVersion")); + } + finally + { + Environment.SetEnvironmentVariable("VisualStudioVersion", originalVisualStudioVersion); + } + } + + /// + /// Reevaluation should update the evaluation counter. + /// + [TestMethod] + public void ReevaluationCounter() + { + Project project = new Project(); + int last = project.EvaluationCounter; + + project.ReevaluateIfNecessary(); + Assert.IsTrue(project.EvaluationCounter == last); + last = project.EvaluationCounter; + + project.SetProperty("p", "v"); + project.ReevaluateIfNecessary(); + Assert.IsTrue(project.EvaluationCounter != last); + } + + /// + /// Unload should not reset the evaluation counter. + /// + [TestMethod] + public void ReevaluationCounterUnload() + { + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement.Create().Save(path); + + Project project = new Project(path); + int last = project.EvaluationCounter; + + project.ProjectCollection.UnloadAllProjects(); + + project = new Project(path); + Assert.IsTrue(project.EvaluationCounter != last); + } + finally + { + File.Delete(path); + } + } + + /// + /// Modifying the XML of an imported file should cause the project + /// to be dirtied. + /// + [TestMethod] + public void ImportedXmlModified() + { + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement import = ProjectRootElement.Create(path); + import.Save(); + + Project project = new Project(); + int last = project.EvaluationCounter; + + project.Xml.AddImport(path); + project.ReevaluateIfNecessary(); + Assert.IsTrue(project.EvaluationCounter != last); + last = project.EvaluationCounter; + + project.ReevaluateIfNecessary(); + Assert.IsTrue(project.EvaluationCounter == last); + + import.AddProperty("p", "v"); + Assert.AreEqual(true, project.IsDirty); + project.ReevaluateIfNecessary(); + Assert.IsTrue(project.EvaluationCounter != last); + last = project.EvaluationCounter; + Assert.AreEqual("v", project.GetPropertyValue("p")); + + project.ReevaluateIfNecessary(); + Assert.IsTrue(project.EvaluationCounter == last); + } + finally + { + File.Delete(path); + } + } + + /// + /// To support certain corner cases, it is possible to explicitly mark a Project + /// as dirty, so that reevaluate is productive. + /// + [TestMethod] + public void ExternallyMarkDirty() + { + Project project = new Project(); + project.SetProperty("p", "v"); + project.ReevaluateIfNecessary(); + + Assert.AreEqual(false, project.IsDirty); + + ProjectProperty property1 = project.GetProperty("p"); + + project.MarkDirty(); + + Assert.AreEqual(true, project.IsDirty); + + project.ReevaluateIfNecessary(); + + Assert.AreEqual(false, project.IsDirty); + + ProjectProperty property2 = project.GetProperty("p"); + + Assert.AreEqual(false, Object.ReferenceEquals(property1, property2)); // different object indicates reevaluation occurred + } + + /// + /// Basic test of getting items by their include + /// + [TestMethod] + public void ItemsByEvaluatedInclude() + { + Project project = new Project(); + project.Xml.AddItem("i", "i1"); + project.Xml.AddItem("i", "i1"); + project.Xml.AddItem("j", "j1"); + project.Xml.AddItem("j", "i1"); + + project.ReevaluateIfNecessary(); + + List items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1")); + + Assert.AreEqual(3, items.Count); + foreach (ProjectItem item in items) + { + Assert.AreEqual("i1", item.EvaluatedInclude); + } + } + + /// + /// Basic test of getting items by their include + /// + [TestMethod] + public void ItemsByEvaluatedInclude_EvaluatedIncludeNeedsEscaping() + { + Project project = new Project(); + project.Xml.AddItem("i", "i%261"); + project.Xml.AddItem("j", "i%25261"); + project.Xml.AddItem("k", "j1"); + project.Xml.AddItem("l", "i&1"); + + project.ReevaluateIfNecessary(); + + List items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i&1")); + + Assert.AreEqual(2, items.Count); + foreach (ProjectItem item in items) + { + Assert.AreEqual("i&1", item.EvaluatedInclude); + Assert.IsTrue + ( + String.Equals(item.ItemType, "i", StringComparison.OrdinalIgnoreCase) || + String.Equals(item.ItemType, "l", StringComparison.OrdinalIgnoreCase) + ); + } + } + + /// + /// Verify none returned when none match + /// + [TestMethod] + public void ItemsByEvaluatedIncludeNone() + { + Project project = new Project(); + project.Xml.AddItem("i", "i1"); + + project.ReevaluateIfNecessary(); + + List items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i2")); + + Assert.AreEqual(0, items.Count); + } + + /// + /// Tests the tracking of virtual items from the construction to instance model, with the removal of a virtual item. + /// + [TestMethod] + public void ItemsByEvaluatedIncludeAndExpansion() + { + List filePaths = new List(); + string testFileRoot = null; + try + { + int count = 0; + testFileRoot = Path.Combine(Path.GetTempPath(), "foodir"); + Directory.CreateDirectory(testFileRoot); + int maxFiles = 2; + for (int i = 0; i < maxFiles; i++) + { + string fileName = String.Format("foo{0}.foo", i); + string filePath = Path.Combine(testFileRoot, fileName); + File.WriteAllText(filePath, String.Empty); + filePaths.Add(filePath); + } + + ProjectRootElement projectConstruction = ProjectRootElement.Create(); + projectConstruction.AddItem("foo", Path.Combine(testFileRoot, "*.foo")); + + count = Helpers.Count(projectConstruction.Items); + Assert.AreEqual(1, count, "Construction Model"); + + Project project = new Project(projectConstruction); + + count = Helpers.Count(project.GetItems("foo")); + Assert.AreEqual(2, count, "Evaluation Model, Before Removal"); + + ProjectItem itemToRemove = null; + + // Get the first item from IEnumerable Collection. + foreach (ProjectItem item in project.Items) + { + itemToRemove = item; + break; + } + + project.RemoveItem(itemToRemove); + count = Helpers.Count(project.GetItems("foo")); + Assert.AreEqual(1, count, "Evaluation Model, After Removal"); + + ProjectInstance projectInstance = project.CreateProjectInstance(); + count = Helpers.Count(projectInstance.Items); + Assert.AreEqual(1, count, "Instance Model"); + + // Ensure XML has been updated accordingly on the Evaluation model (projectInstance doesn't back onto XML) + Assert.IsFalse(project.Xml.RawXml.Contains(itemToRemove.Xml.Include)); + Assert.IsFalse(project.Xml.RawXml.Contains("*.foo")); + } + finally + { + foreach (string filePathToRemove in filePaths) + { + File.Delete(filePathToRemove); + } + + Directory.Delete(testFileRoot); + } + } + + /// + /// Reevaluation should update items-by-evaluated-include + /// + [TestMethod] + public void ItemsByEvaluatedIncludeReevaluation() + { + Project project = new Project(); + project.Xml.AddItem("i", "i1"); + project.ReevaluateIfNecessary(); + + List items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1")); + Assert.AreEqual(1, items.Count); + + project.Xml.AddItem("j", "i1"); + project.ReevaluateIfNecessary(); + + items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1")); + Assert.AreEqual(2, items.Count); + } + + /// + /// Direct adds to the project (ie, not added by evaluation) should update + /// items-by-evaluated-include + /// + [TestMethod] + public void ItemsByEvaluatedIncludeDirectAdd() + { + Project project = new Project(); + project.AddItem("i", "i1"); + + List items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1")); + Assert.AreEqual(1, items.Count); + } + + /// + /// Direct removes from the project (ie, not removed by evaluation) should update + /// items-by-evaluated-include + /// + [TestMethod] + public void ItemsByEvaluatedIncludeDirectRemove() + { + Project project = new Project(); + ProjectItem item1 = project.AddItem("i", "i1;j1")[0]; + project.RemoveItem(item1); + + List items = Helpers.MakeList(project.GetItemsByEvaluatedInclude("i1")); + Assert.AreEqual(0, items.Count); + } + + /// + /// Choose, When has true condition + /// + [TestMethod] + public void ChooseWhenTrue() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + +

v1

+
+ + + +
+
+
+ "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("v1", project.GetPropertyValue("p")); + Assert.AreEqual("i1", Helpers.MakeList(project.GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Choose, second When has true condition + /// + [TestMethod] + public void ChooseSecondWhenTrue() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + +

v1

+
+ + + +
+ + +

v2

+
+ + + +
+
+
+ "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("v2", project.GetPropertyValue("p")); + Assert.AreEqual("i2", Helpers.MakeList(project.GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Choose, when has false condition + /// + [TestMethod] + public void ChooseOtherwise() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + +

v1

+
+ + + +
+ + +

v2

+
+ + + +
+
+
+ "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("v2", project.GetPropertyValue("p")); + Assert.AreEqual("i2", Helpers.MakeList(project.GetItems("i"))[0].EvaluatedInclude); + } + + /// + /// Choose should be entered twice, once for properties and again for items. + /// That means items should see properties defined below. + /// + [TestMethod] + public void ChooseTwoPasses() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + +

@(i);v1

+
+
+
+ + + v2 + + + + + + + + + + @(j);v1 + + + + + + v2 + +
+ "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("@(i);v1", project.GetPropertyValue("p")); + Assert.AreEqual("@(j);v1", project.GetPropertyValue("q")); + Assert.AreEqual("v1_v2", project.GetItems("i").ElementAt(0).EvaluatedInclude); + Assert.AreEqual(1, project.GetItems("i").Count()); + Assert.AreEqual("v1_v2", project.GetItems("j").ElementAt(0).EvaluatedInclude); + Assert.AreEqual(1, project.GetItems("j").Count()); + } + + /// + /// Choose conditions are only evaluated once, on the property pass + /// + [TestMethod] + public void ChooseEvaluateConditionOnlyOnce() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + + +

v

+
+ +
+ "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual(0, project.GetItems("i").Count()); + } + + /// + /// Choose items can see item definitions below + /// + [TestMethod] + public void ChooseSeesItemDefinitions() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + %(m);m1 + + + + + + + + m0 + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(content))); + + Assert.AreEqual("m0;m1", project.GetItems("i").ElementAt(0).GetMetadataValue("m")); + } + + /// + /// When build is disabled on the project, it shouldn't run, and should give MSB4112. + /// + [TestMethod] + public void BuildDisabled() + { + Project project = new Project(); + project.Xml.AddTarget("t"); + project.IsBuildEnabled = false; + MockLogger mockLogger = new MockLogger(); + ProjectCollection.GlobalProjectCollection.RegisterLogger(mockLogger); + + bool result = project.Build(); + + Assert.AreEqual(false, result); + + Assert.IsTrue + ( + mockLogger.Errors[0].Code == "MSB4112", + "Security message about disabled targets need to have code MSB4112, because code in the VS Core project system depends on this. See DesignTimeBuildFeedback.cpp." + ); + } + + /// + /// Building a nonexistent target should log an error and return false (not throw) + /// + [TestMethod] + [TestCategory("serialize")] + public void BuildNonExistentTarget() + { + Project project = new Project(); + MockLogger logger = new MockLogger(); + bool result = project.Build(new string[] { "nonexistent" }, new List() { logger }); + Assert.AreEqual(false, result); + Assert.AreEqual(1, logger.ErrorCount); + } + + /// + /// When Project.Build is invoked with custom loggers, those loggers should contain the result of any evaluation warnings and errors. + /// + [TestMethod] + [TestCategory("serialize")] + public void BuildEvaluationUsesCustomLoggers() + { + string importProjectContent = + ObjectModelHelpers.CleanupFileContents(@" + "); + + string importFileName = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile() + ".proj"; + File.WriteAllText(importFileName, importProjectContent); + + string projectContent = + ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + + Project project = new Project(XmlReader.Create(new StringReader(projectContent))); + project.MarkDirty(); + + MockLogger collectionLogger = new MockLogger(); + project.ProjectCollection.RegisterLogger(collectionLogger); + + MockLogger mockLogger = new MockLogger(); + + bool result; + + try + { + result = project.Build(new ILogger[] { mockLogger }); + } + catch + { + throw; + } + finally + { + project.ProjectCollection.UnregisterAllLoggers(); + } + + Assert.AreEqual(true, result); + + Assert.IsTrue + ( + mockLogger.WarningCount == 0, + "Log should not contain MSB4011 because the build logger will not receive evaluation messages." + ); + + Assert.IsTrue + ( + collectionLogger.Warnings[0].Code == "MSB4011", + "Log should contain MSB4011 because the project collection logger should have been used for evaluation." + ); + } + + /// + /// UsingTask expansion should throw InvalidProjectFileException + /// if it expands to nothing. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void UsingTaskExpansion1() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddUsingTask("x", "@(x->'%(x)')", null); + Project project = new Project(xml); + } + + /// + /// UsingTask expansion should throw InvalidProjectFileException + /// if it expands to nothing. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void UsingTaskExpansion2() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddUsingTask("@(x->'%(x)')", "y", null); + Project project = new Project(xml); + } + + /// + /// UsingTask expansion should throw InvalidProjectFileException + /// if it expands to nothing. + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void UsingTaskExpansion3() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddUsingTask("x", null, "@(x->'%(x)')"); + Project project = new Project(xml); + } + + /// + /// Saving project should make it "clean" for saving + /// but "dirty" for reevaluation if it was to a new location + /// + [TestMethod] + public void SavingProjectClearsDirtyBit() + { + string contents = ObjectModelHelpers.CleanupFileContents(@""); + Project project = new Project(XmlReader.Create(new StringReader(contents))); + + Assert.IsTrue(project.Xml.HasUnsavedChanges); // Not dirty for saving + Assert.IsFalse(project.IsDirty, "1"); // was evaluated on load + + string file = null; + try + { + file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + project.Save(file); + } + finally + { + if (file != null) + { + File.Delete(file); + } + } + + Assert.IsFalse(project.Xml.HasUnsavedChanges); // Not dirty for saving + Assert.IsTrue(project.IsDirty, "2"); // Dirty for reevaluation, because the project now has gotten a new file name + } + + /// + /// Remove an already removed item + /// + [TestMethod] + public void RemoveItemTwiceEvaluationProject() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + ProjectItem itemToRemove = Helpers.GetFirst(project.GetItems("Compile")); + project.RemoveItem(itemToRemove); + project.RemoveItem(itemToRemove); // should not throw + + Assert.AreEqual(0, Helpers.MakeList(project.Items).Count); + } + + /// + /// Remove an updated item + /// + [TestMethod] + public void RemoveItemOutdatedByUpdate() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + ProjectItem itemToRemove = Helpers.GetFirst(project.GetItems("Compile")); + itemToRemove.UnevaluatedInclude = "b.cs"; + project.RemoveItem(itemToRemove); // should not throw + + Assert.AreEqual(0, Helpers.MakeList(project.Items).Count); + } + + /// + /// Remove several items + /// + [TestMethod] + public void RemoveSeveralItems() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + + project.RemoveItems(project.GetItems("i")); + + Assert.AreEqual(0, project.Items.Count()); + } + + /// + /// Remove several items + /// + [TestMethod] + public void RemoveSeveralItemsOfVariousTypes() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + + List list = new List() { project.GetItems("i").FirstOrDefault(), project.GetItems("j").FirstOrDefault() }; + + project.RemoveItems(list); + + Assert.AreEqual(2, project.Items.Count()); + } + + /// + /// Remove items expanding itemlist expression + /// + [TestMethod] + public void RemoveSeveralItemsExpandExpression() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + + project.RemoveItems(project.GetItems("j").Take(2)); + Assert.AreEqual(3, project.Items.Count()); + + StringWriter writer = new StringWriter(); + project.Save(writer); + + string projectExpectedContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + + "); + + Helpers.CompareProjectXml(projectExpectedContents, writer.ToString()); + } + + /// + /// Remove several items where removing the first one + /// causes the second one to be detached + /// + [TestMethod] + public void RemoveSeveralItemsFirstZombiesSecond() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + + project.RemoveItems(project.GetItems("i")); + + Assert.AreEqual(0, project.Items.Count()); + } + + /// + /// Should not get null reference + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void RemoveItemsOneNull() + { + Project project = new Project(); + project.RemoveItems(new List() { null }); + } + + /// + /// Remove several items where removing the first one + /// causes the second one to be detached + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RemoveItemWrongProject() + { + ProjectRootElement root1 = ProjectRootElement.Create(); + root1.AddItem("i", "i1"); + ProjectRootElement root2 = ProjectRootElement.Create(); + root2.AddItem("i", "i1"); + Project project1 = new Project(root1); + Project project2 = new Project(root2); + + project1.RemoveItems(project2.Items); + } + + /// + /// Remove an item that is no longer attached. For convenience, + /// we just skip it. + /// + [TestMethod] + public void RemoveZombiedItem() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + ProjectItem item = project.GetItems("i").FirstOrDefault(); + + project.RemoveItems(new List() { item }); + project.RemoveItems(new List() { item }); + + Assert.AreEqual(0, project.Items.Count()); + } + + /// + /// Reserved property in project constructor should just throw + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ReservedPropertyProjectConstructor() + { + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("msbuildprojectdirectory", "x"); + + Project project = new Project(globalProperties, null, new ProjectCollection()); + } + + /// + /// Reserved property in project collection global properties should log an error then rethrow + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReservedPropertyProjectCollectionConstructor() + { + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("msbuildprojectdirectory", "x"); + MockLogger logger = new MockLogger(); + List loggers = new List(); + loggers.Add(logger); + + try + { + ProjectCollection collection = new ProjectCollection(globalProperties, loggers, ToolsetDefinitionLocations.None); + } + finally + { + logger.AssertLogContains("MSB4177"); + } + } + + /// + /// Invalid property (reserved name) in project collection global properties should log an error then rethrow + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReservedPropertyProjectCollectionConstructor2() + { + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Target", "x"); + MockLogger logger = new MockLogger(); + List loggers = new List(); + loggers.Add(logger); + + try + { + ProjectCollection collection = new ProjectCollection(globalProperties, loggers, ToolsetDefinitionLocations.None); + } + finally + { + logger.AssertLogContains("MSB4177"); + } + } + + /// + /// Create tree like this + /// + /// \b.targets + /// \sub\a.proj + /// + /// An item specified with "..\*" in b.targets should find b.targets + /// as it was evaluated relative to the project file itself. + /// + [TestMethod] + public void RelativePathsInItemsInTargetsFilesAreRelativeToProjectFile() + { + string directory = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + string subdirectory = Path.Combine(directory, "sub"); + Directory.CreateDirectory(subdirectory); + + string projectPath = Path.Combine(subdirectory, "a.proj"); + string targetsPath = Path.Combine(directory, "b.targets"); + + ProjectRootElement targetsXml = ProjectRootElement.Create(targetsPath); + targetsXml.AddItem("i", @"..\*"); + targetsXml.Save(); + + ProjectRootElement projectXml = ProjectRootElement.Create(projectPath); + projectXml.AddImport(@"..\b.targets"); + projectXml.Save(); + + Project project = new Project(projectPath); + + IEnumerable items = project.GetItems("i"); + Assert.AreEqual(@"..\*", Helpers.GetFirst(items).UnevaluatedInclude); + Assert.AreEqual(@"..\b.targets", Helpers.GetFirst(items).EvaluatedInclude); + } + finally + { + Directory.Delete(directory, true); + } + } + + /// + /// Invalid property (space) in project collection global properties should log an error then rethrow + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ReservedPropertyProjectCollectionConstructor3() + { + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties.Add("Target", "x"); + MockLogger logger = new MockLogger(); + List loggers = new List(); + loggers.Add(logger); + + try + { + ProjectCollection collection = new ProjectCollection(globalProperties, loggers, ToolsetDefinitionLocations.None); + } + finally + { + logger.AssertLogContains("MSB4177"); + } + } + + /// + /// Create a structure of various imports and verify that project.GetLogicalProject() + /// walks through them correctly. + /// + [TestMethod] + public void VariousImports() + { + ProjectRootElement one = ProjectRootElement.Create("c:\\1.targets"); + one.AddProperty("p", "1"); + ProjectRootElement two = ProjectRootElement.Create("c:\\2.targets"); + two.AddProperty("p", "2"); + + ProjectRootElement zero = ProjectRootElement.Create("c:\\foo\\0.targets"); + zero.AddProperty("p", "0"); + zero.AddImport(one.FullPath); + zero.AddImport(two.FullPath); + zero.AddImport(two.FullPath); // Duplicated import: only the first one should be entered + zero.AddImport(zero.FullPath); // Ignored self import + + ProjectRootElement three = ProjectRootElement.Create("c:\\3.targets"); + three.AddProperty("p", "3"); + one.AddImport(three.FullPath); + + ProjectRootElement four = ProjectRootElement.Create("c:\\4.targets"); + four.AddProperty("p", "4"); + one.AddImport(four.FullPath).Condition = "false"; // False condition; should not be entered + + Project project = new Project(zero); + + List logicalProject = new List(project.GetLogicalProject()); + + Assert.AreEqual(8, logicalProject.Count); // 4 properties + 4 property groups + Assert.AreEqual(true, Object.ReferenceEquals(zero, logicalProject[0].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(zero, logicalProject[1].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(one, logicalProject[2].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(one, logicalProject[3].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(three, logicalProject[4].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(three, logicalProject[5].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(two, logicalProject[6].ContainingProject)); + Assert.AreEqual(true, Object.ReferenceEquals(two, logicalProject[7].ContainingProject)); + + // Clear the cache + project.ProjectCollection.UnloadAllProjects(); + } + + /// + /// Create a structure containing a import statement such that the import statement results in more than one + /// file being imported. Then, verify that project.GetLogicalProject() walks through them correctly. + /// + [TestMethod] + public void LogicalProjectWithWildcardImport() + { + string myTempDir = Path.Combine(Path.GetTempPath() + "MyTempDir"); + + try + { + // Create a new directory in the system temp folder. + Directory.CreateDirectory(myTempDir); + + ProjectRootElement one = ProjectRootElement.Create(Path.Combine(myTempDir, "1.targets")); + one.Save(); + one.AddProperty("p", "1"); + + ProjectRootElement two = ProjectRootElement.Create(Path.Combine(myTempDir, "2.targets")); + two.Save(); + two.AddProperty("p", "2"); + + ProjectRootElement zero = ProjectRootElement.Create(Path.Combine(myTempDir, "0.targets")); + zero.AddProperty("p", "0"); + + // Add a single import statement that would import both one and two. + zero.AddImport(Path.Combine(myTempDir, "*.targets")); + + Project project = new Project(zero); + + List logicalProject = new List(project.GetLogicalProject()); + + Assert.AreEqual(6, logicalProject.Count); // 3 properties + 3 property groups + Assert.AreEqual(true, Object.ReferenceEquals(zero, logicalProject[0].ContainingProject)); // PropertyGroup + Assert.AreEqual(true, Object.ReferenceEquals(zero, logicalProject[1].ContainingProject)); // p = 0 + Assert.AreEqual(true, Object.ReferenceEquals(one, logicalProject[2].ContainingProject)); // PropertyGroup + Assert.AreEqual(true, Object.ReferenceEquals(one, logicalProject[3].ContainingProject)); // p = 1 + Assert.AreEqual(true, Object.ReferenceEquals(two, logicalProject[4].ContainingProject)); // PropertyGroup + Assert.AreEqual(true, Object.ReferenceEquals(two, logicalProject[5].ContainingProject)); // p = 2 + + // Clear the cache + project.ProjectCollection.UnloadAllProjects(); + } + finally + { + // Delete the temp directory that was created above. + if (Directory.Exists(myTempDir)) + { + Directory.Delete(myTempDir, true); + } + } + } + + /// + /// Import of string that evaluates to empty should give InvalidProjectFileException + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ImportPropertyEvaluatingToEmpty() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + + + "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + } + + /// + /// Import of string that evaluates to invalid path should cause InvalidProjectFileException + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void ImportPropertyEvaluatingToInvalidPath() + { + string projectOriginalContents = ObjectModelHelpers.CleanupFileContents(@" + + +

|

+
+ +
+ "); + Project project = new Project(XmlReader.Create(new StringReader(projectOriginalContents))); + } + + /// + /// Creates a simple ProjectRootElement object. + /// (When ProjectRootElement supports editing, we need not load from a string here.) + /// + private ProjectRootElement GetSampleProjectRootElement() + { + string projectFileContent = GetSampleProjectContent(); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectFileContent))); + + return xml; + } + + /// + /// Creates a simple project content. + /// + private string GetSampleProjectContent() + { + string projectFileContent = ObjectModelHelpers.CleanupFileContents(@" + + +

v1

+
+ +

v2

+
+ + X$(p) + + + + + + + + + +
+ "); + + return projectFileContent; + } + + /// + /// Check the items and properties from the sample project + /// + private void VerifyContentOfSampleProject(Project project) + { + Assert.AreEqual("v2", project.GetProperty("p").UnevaluatedValue); + Assert.AreEqual("Xv2", project.GetProperty("p2").EvaluatedValue); + Assert.AreEqual("X$(p)", project.GetProperty("p2").UnevaluatedValue); + + IList items = Helpers.MakeList(project.GetItems("i")); + Assert.AreEqual(3, items.Count); + Assert.AreEqual("i1", items[0].EvaluatedInclude); + Assert.AreEqual("v2X", items[1].EvaluatedInclude); + Assert.AreEqual("$(p)X;i3", items[1].UnevaluatedInclude); + Assert.AreEqual("i3", items[2].EvaluatedInclude); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProtectImports_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProtectImports_Tests.cs new file mode 100644 index 00000000000..4880a8e2e56 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Definition/ProtectImports_Tests.cs @@ -0,0 +1,663 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for protecting imported files while editing +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Construction; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Definition +{ + /// + /// Tests for protecting imported files while editing + /// + [TestClass] + public class ProtectImports_Tests + { + #region Constants + + /// + /// Imported metadata name + /// + private const string ImportedMetadataName = "ImportedMetadataName"; + + /// + /// Imported metadata value + /// + private const string ImportedMetadataValue = "ImportedMetadataValue"; + + /// + /// Item name to use + /// + private const string ItemName = "ItemName"; + + /// + /// Item type to use + /// + private const string ItemType = "ItemType"; + + /// + /// New value + /// + private const string NewValue = "NewValue"; + + /// + /// It's non-overridable just in the sense that the tests aren't providing new values to it; nothing else implied + /// + private const string NonOverridableMetadataName = "NonOverridableMetadataName"; + + /// + /// Overridable metadata name + /// + private const string OverridableMetadataName = "OverridableMetadataName"; + + /// + /// Project metadata name + /// + private const string ProjectMetadataName = "ProjectMetadataName"; + + /// + /// Project metadata value + /// + private const string ProjectMetadataValue = "ProjectMetadataValue"; + + /// + /// Same item type + /// + private const string SameItemType = "SameItemType"; + + /// + /// Same item value in project + /// + private const string SameItemValueInProject = "SameItemValueInProject"; + + /// + /// Same property name + /// + private const string PropertyName = "ImportedProperty"; + + #endregion + + /// + /// Import filename + /// + private string importFilename; + + #region Test lifetime + + /// + /// Configures the overall test. + /// + [TestInitialize] + public void Setup() + { + string importContents = + @" + + <$propertyName>OldPropertyValue + + + <$itemType> + <$overridableMetadataName>ImportValue + <$nonOverridableMetadataName>ImportValue + + + + <$itemType Include=""$itemName""> + <$importedMetadataName>$importedMetadataValue + + + "; + + importContents = Expand(importContents); + importFilename = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile() + ".targets"; + File.WriteAllText(importFilename, importContents); + } + + /// + /// Undoes the test configuration. + /// + [TestCleanup] + public void Teardown() + { + if (File.Exists(importFilename)) + { + File.Delete(importFilename); + } + } + + #endregion + + #region Property Tests + + /// + /// Tests against edits into imported properties thru the property itself. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void PropertySetViaProperty() + { + Project project = GetProject(); + ProjectProperty property = GetProperty(project); + + // This should throw + property.UnevaluatedValue = NewValue; + } + + /// + /// Tests against edits into imported properties thru the project. + /// Instead of editing the existing property, because it originated + /// in an imported file, it should create a new one in the main project. + /// + [TestMethod] + public void PropertySetViaProject() + { + Project project = GetProject(); + ProjectProperty property = GetProperty(project); + + project.SetProperty(PropertyName, NewValue); + + Assert.AreEqual(NewValue, project.GetPropertyValue(PropertyName)); + } + + /// + /// Tests against edits into imported properties thru the property itself. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void PropertyRemove() + { + Project project = GetProject(); + ProjectProperty property = GetProperty(project); + + // This should throw + project.RemoveProperty(property); + } + + #endregion + + #region Item Tests + + /// + /// Tests imported item type change. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ItemImportedChangeType() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + item.ItemType = "NewItemType"; + } + + /// + /// Tests imported item renaming. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ItemImportedRename() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + item.Rename("NewItemName"); + } + + /// + /// Tests imported item SetUnevaluatedValue. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ItemImportedSetUnevaluatedValue() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + item.UnevaluatedInclude = "NewItemName"; + } + + /// + /// Tests imported item removal. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ItemImportedRemove() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + project.RemoveItem(item); + } + + /// + /// Tests project item type change. + /// + [TestMethod] + public void ItemProjectChangeType() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.ItemType = NewValue; + Assert.IsTrue(project.GetItems(NewValue).Count() == 1, "Item in project didn't change name"); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests project item renaming. + /// + [TestMethod] + public void ItemProjectRename() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.Rename(NewValue); + Assert.AreEqual(NewValue, item.EvaluatedInclude, "Item in project didn't change name."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests project item SetUnevaluatedValue. + /// + [TestMethod] + public void ItemProjectSetUnevaluatedValue() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.UnevaluatedInclude = NewValue; + Assert.AreEqual(NewValue, item.EvaluatedInclude, "Item in project didn't change name."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests project item removal. + /// + [TestMethod] + public void ItemProjectRemove() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + project.RemoveItem(item); + Assert.IsTrue(project.GetItems(ItemType).Count() == 1, "Item in project wasn't removed."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + #endregion + + #region Metadata Tests + + /// + /// Tests setting existing metadata in import. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void MetadataImportSetViaProject() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + item.SetMetadataValue(ImportedMetadataName, "NewImportedMetadataValue"); + } + + /// + /// Tests setting new metadata in import. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void MetadataImportAdd() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + item.SetMetadataValue("NewMetadata", "NewImportedMetadataValue"); + } + + /// + /// Tests setting new metadata in import. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void MetadataImportSetViaMetadata() + { + Project project = GetProject(); + ProjectMetadata metadata = GetImportedMetadata(project); + + // This should throw + metadata.UnevaluatedValue = NewValue; + } + + /// + /// Tests removing metadata in import. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void MetadataImportRemove() + { + Project project = GetProject(); + ProjectItem item = GetImportedItem(project); + + // This should throw + item.RemoveMetadata(ImportedMetadataName); + } + + /// + /// Tests setting existing metadata in import. + /// + [TestMethod] + public void MetadataProjectSetViaItem() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.SetMetadataValue(ProjectMetadataName, NewValue); + Assert.AreEqual(NewValue, item.GetMetadataValue(ProjectMetadataName), "Metadata not saved correctly in project."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests setting new metadata in import. + /// + [TestMethod] + public void MetadataProjectAdd() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + string newName = "NewMetadata"; + item.SetMetadataValue(newName, NewValue); + Assert.AreEqual(NewValue, item.GetMetadataValue(newName), "Metadata not saved correctly in project."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests setting new metadata in import. + /// + [TestMethod] + public void MetadataProjectSetViaMetadata() + { + Project project = GetProject(); + ProjectMetadata metadata = GetProjectMetadata(project); + + string newValue = "NewProjectMetadataValue"; + metadata.UnevaluatedValue = newValue; + + Assert.AreEqual(newValue, metadata.EvaluatedValue); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests removing metadata in import. + /// + [TestMethod] + public void MetadataProjectRemove() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.RemoveMetadata(ProjectMetadataName); + Assert.IsFalse(item.HasMetadata(ProjectMetadataName), "Metadata was not removed from project."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + #endregion + + #region Metadata in Item Definition Tests + + /// + /// Tests setting new metadata in import. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void DefinitionMetadataImportSetViaMetadata() + { + Project project = GetProject(); + ProjectMetadata metadata = GetNonOverridableMetadata(project); + + // This should throw + metadata.UnevaluatedValue = NewValue; + } + + /// + /// Tests removing metadata in imported item definition. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void DefinitionMetadataImportRemove() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + // This should throw + item.RemoveMetadata(NonOverridableMetadataName); + } + + /// + /// Tests setting existing metadata in import. + /// + [TestMethod] + public void DefinitionMetadataProjectSetViaProject() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.SetMetadataValue(OverridableMetadataName, NewValue); + Assert.AreEqual(NewValue, item.GetMetadataValue(OverridableMetadataName), "Metadata not set correctly in project."); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests setting new metadata in import. + /// + [TestMethod] + public void DefinitionMetadataProjectSetViaMetadata() + { + Project project = GetProject(); + ProjectMetadata metadata = GetOverridableMetadata(project); + + metadata.UnevaluatedValue = NewValue; + Assert.AreEqual(NewValue, metadata.EvaluatedValue); + Assert.IsTrue(project.IsDirty, "Project was not marked dirty."); + } + + /// + /// Tests removing metadata in import. + /// + [TestMethod] + public void DefinitionMetadataProjectRemove() + { + Project project = GetProject(); + ProjectItem item = GetProjectItem(project); + + item.RemoveMetadata(OverridableMetadataName); + + ProjectMetadata metadata = item.GetMetadata(OverridableMetadataName); + Assert.IsNotNull(metadata, "Imported metadata not found after the project's one was removed."); + Assert.IsTrue(metadata.IsImported, "IsImported property is not set."); + } + + #endregion + + #region Test helpers + + /// + /// Expands variables in the string contents. + /// + /// String to be expanded. + /// Expanded string. + private string Expand(string original) + { + string expanded = original.Replace("$importFilename", importFilename); + expanded = expanded.Replace("$importedMetadataName", ImportedMetadataName); + expanded = expanded.Replace("$importedMetadataValue", ImportedMetadataValue); + expanded = expanded.Replace("$itemName", ItemName); + expanded = expanded.Replace("$itemType", ItemType); + expanded = expanded.Replace("$projectMetadataName", ProjectMetadataName); + expanded = expanded.Replace("$overridableMetadataName", OverridableMetadataName); + expanded = expanded.Replace("$nonOverridableMetadataName", NonOverridableMetadataName); + expanded = expanded.Replace("$projectMetadataValue", ProjectMetadataValue); + expanded = expanded.Replace("$propertyName", PropertyName); + + return expanded; + } + + /// + /// Gets the test item from the import. + /// + /// The project. + /// The item. + private ProjectItem GetImportedItem(Project project) + { + IEnumerable items = project.GetItems(ItemType).Where(pi => pi.IsImported); + Assert.IsTrue(items.Count() == 1, "Wrong number of items in the import."); + + ProjectItem item = items.First(); + Assert.AreEqual(importFilename, item.Xml.ContainingProject.FullPath, "Item was not found in the imported project."); + + return item; + } + + /// + /// Gets the test metadata from the import. + /// + /// The project. + /// The metadata. + private ProjectMetadata GetImportedMetadata(Project project) + { + ProjectItem item = GetImportedItem(project); + IEnumerable metadatum = item.Metadata.Where(m => m.Name == ImportedMetadataName); + Assert.IsTrue(metadatum.Count() == 1, "Incorrect number of imported metadata found."); + + ProjectMetadata metadata = metadatum.First(); + Assert.IsTrue(metadata.IsImported, "IsImport property is not set."); + + return metadata; + } + + /// + /// Gets the test templetized metadata from the import. + /// + /// The project. + /// The metadata. + private ProjectMetadata GetNonOverridableMetadata(Project project) + { + ProjectItem item = GetProjectItem(project); + IEnumerable metadatum = item.Metadata.Where(m => m.Name == NonOverridableMetadataName); + Assert.IsTrue(metadatum.Count() == 1, "Incorrect number of imported metadata found."); + + ProjectMetadata metadata = metadatum.First(); + Assert.IsTrue(metadata.IsImported, "IsImport property is not set."); + + return metadata; + } + + /// + /// Gets the test templetized metadata from the project. + /// + /// The project. + /// The metadata. + private ProjectMetadata GetOverridableMetadata(Project project) + { + ProjectItem item = GetProjectItem(project); + IEnumerable metadatum = item.Metadata.Where(m => m.Name == OverridableMetadataName); + Assert.IsTrue(metadatum.Count() == 1, "Incorrect number of imported metadata found."); + + ProjectMetadata metadata = metadatum.First(); + Assert.IsFalse(metadata.IsImported, "IsImport property is set."); + + return metadata; + } + + /// + /// Creates a new project from expanding the template contents. + /// + /// The project instance. + private Project GetProject() + { + string projectContents = + @" + + <$propertyName>OldPropertyValueInProject + + + <$itemType Include=""$itemName""> + <$projectMetadataName>projectValue + <$overridableMetadataName>ProjectValue + + + + "; + + projectContents = Expand(projectContents); + Project project = new Project(XmlReader.Create(new StringReader(projectContents))); + return project; + } + + /// + /// Gets the test property. + /// + /// The test project. + /// The test property. + private ProjectProperty GetProperty(Project project) + { + ProjectProperty property = project.GetProperty(PropertyName); + Assert.AreEqual(importFilename, property.Xml.ContainingProject.FullPath, "Property was not found in the imported project."); + Assert.IsTrue(property.IsImported, "IsImported property was not set."); + return property; + } + + /// + /// Gets the test item from the project. + /// + /// The project. + /// The item. + private ProjectItem GetProjectItem(Project project) + { + IEnumerable items = project.GetItems(ItemType).Where(pi => !pi.IsImported); + Assert.IsTrue(items.Count() == 1, "Wrong number of items in the project."); + + ProjectItem item = items.First(); + Assert.AreEqual(null, item.Xml.ContainingProject.FullPath, "Item was not found in the project."); // null because XML is in-memory + + return item; + } + + /// + /// Gets the test metadata from the project. + /// + /// The project. + /// The metadata. + private ProjectMetadata GetProjectMetadata(Project project) + { + ProjectItem item = GetProjectItem(project); + IEnumerable metadatum = item.Metadata.Where(m => m.Name == ProjectMetadataName); + Assert.IsTrue(metadatum.Count() == 1, "Incorrect number of imported metadata found."); + + ProjectMetadata metadata = metadatum.First(); + Assert.IsFalse(metadata.IsImported, "IsImport property is set."); + + return metadata; + } + + #endregion + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectInstance_Tests.cs new file mode 100644 index 00000000000..d879d11ed9e --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectInstance_Tests.cs @@ -0,0 +1,973 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectInstance public members +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.Build.Framework; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +using Microsoft.Build.BackEnd; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectInstance public members + /// + [TestClass] + public class ProjectInstance_Tests + { + /// + /// Verify that a cloned off project instance can see environment variables + /// + [TestMethod] + public void CreateProjectInstancePassesEnvironment() + { + Project p = new Project(); + ProjectInstance i = p.CreateProjectInstance(); + + Assert.AreEqual(true, i.GetPropertyValue("username") != null); + } + + /// + /// Read off properties + /// + [TestMethod] + public void PropertiesAccessors() + { + ProjectInstance p = GetSampleProjectInstance(); + + Assert.AreEqual("v1", p.GetPropertyValue("p1")); + Assert.AreEqual("v2X", p.GetPropertyValue("p2")); + } + + /// + /// Read off items + /// + [TestMethod] + public void ItemsAccessors() + { + ProjectInstance p = GetSampleProjectInstance(); + + IList items = Helpers.MakeList(p.GetItems("i")); + Assert.AreEqual(3, items.Count); + Assert.AreEqual("i", items[0].ItemType); + Assert.AreEqual("i0", items[0].EvaluatedInclude); + Assert.AreEqual(String.Empty, items[0].GetMetadataValue("m")); + Assert.AreEqual(null, items[0].GetMetadata("m")); + Assert.AreEqual("i1", items[1].EvaluatedInclude); + Assert.AreEqual("m1", items[1].GetMetadataValue("m")); + Assert.AreEqual("m1", items[1].GetMetadata("m").EvaluatedValue); + Assert.AreEqual("v1", items[2].EvaluatedInclude); + } + + /// + /// Add item + /// + [TestMethod] + public void AddItemWithoutMetadata() + { + ProjectInstance p = GetEmptyProjectInstance(); + + ProjectItemInstance returned = p.AddItem("i", "i1"); + + Assert.AreEqual("i", returned.ItemType); + Assert.AreEqual("i1", returned.EvaluatedInclude); + Assert.AreEqual(false, returned.Metadata.GetEnumerator().MoveNext()); + + foreach (ProjectItemInstance item in p.Items) + { + Assert.AreEqual("i1", item.EvaluatedInclude); + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + } + } + + /// + /// Add item + /// + [TestMethod] + public void AddItemWithoutMetadata_Escaped() + { + ProjectInstance p = GetEmptyProjectInstance(); + + ProjectItemInstance returned = p.AddItem("i", "i%3b1"); + + Assert.AreEqual("i", returned.ItemType); + Assert.AreEqual("i;1", returned.EvaluatedInclude); + Assert.AreEqual(false, returned.Metadata.GetEnumerator().MoveNext()); + + foreach (ProjectItemInstance item in p.Items) + { + Assert.AreEqual("i;1", item.EvaluatedInclude); + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + } + } + + /// + /// Add item with metadata + /// + [TestMethod] + public void AddItemWithMetadata() + { + ProjectInstance p = GetEmptyProjectInstance(); + + var metadata = new List>(); + metadata.Add(new KeyValuePair("m", "m1")); + metadata.Add(new KeyValuePair("n", "n1")); + metadata.Add(new KeyValuePair("o", "o%40")); + + ProjectItemInstance returned = p.AddItem("i", "i1", metadata); + + Assert.ReferenceEquals(returned, Helpers.MakeList(p.GetItems("i"))[0]); + + foreach (ProjectItemInstance item in p.Items) + { + Assert.ReferenceEquals(returned, item); + Assert.AreEqual("i1", item.EvaluatedInclude); + var metadataOut = Helpers.MakeList(item.Metadata); + Assert.AreEqual(3, metadataOut.Count); + Assert.AreEqual("m1", item.GetMetadataValue("m")); + Assert.AreEqual("n1", item.GetMetadataValue("n")); + Assert.AreEqual("o@", item.GetMetadataValue("o")); + } + } + + /// + /// Add item null item type + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AddItemInvalidNullItemType() + { + ProjectInstance p = GetEmptyProjectInstance(); + p.AddItem(null, "i1"); + } + + /// + /// Add item empty item type + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AddItemInvalidEmptyItemType() + { + ProjectInstance p = GetEmptyProjectInstance(); + p.AddItem(String.Empty, "i1"); + } + + /// + /// Add item null include + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AddItemInvalidNullInclude() + { + ProjectInstance p = GetEmptyProjectInstance(); + p.AddItem("i", null); + } + + /// + /// Add item null metadata + /// + [TestMethod] + public void AddItemNullMetadata() + { + ProjectInstance p = GetEmptyProjectInstance(); + ProjectItemInstance item = p.AddItem("i", "i1", null); + + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + } + + /// + /// It's okay to set properties that are also global properties, masking their value + /// + [TestMethod] + public void SetGlobalPropertyOnInstance() + { + Dictionary globals = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "p", "p1" } }; + Project p = new Project(ProjectRootElement.Create(), globals, null); + ProjectInstance instance = p.CreateProjectInstance(); + + instance.SetProperty("p", "p2"); + + Assert.AreEqual("p2", instance.GetPropertyValue("p")); + + // And clearing it should not expose the original global property value + instance.SetProperty("p", ""); + + Assert.AreEqual("", instance.GetPropertyValue("p")); + } + + /// + /// ProjectInstance itself is cloned properly + /// + [TestMethod] + public void CloneProjectItself() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Assert.IsTrue(!Object.ReferenceEquals(first, second)); + } + + /// + /// Properties are cloned properly + /// + [TestMethod] + public void CloneProperties() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Assert.IsTrue(!Object.ReferenceEquals(first.GetProperty("p1"), second.GetProperty("p1"))); + + ProjectPropertyInstance newProperty = first.SetProperty("p1", "v1b"); + Assert.AreEqual(true, Object.ReferenceEquals(newProperty, first.GetProperty("p1"))); + Assert.AreEqual("v1b", first.GetPropertyValue("p1")); + Assert.AreEqual("v1", second.GetPropertyValue("p1")); + } + + /// + /// Passing an item list into another list should copy the metadata too + /// + [TestMethod] + public void ItemEvaluationCopiesMetadata() + { + string content = @" + + + + m1 + n%3b%3b + + + + "; + + ProjectInstance project = GetProjectInstance(content); + + Assert.AreEqual(1, Helpers.MakeList(project.GetItems("j")).Count); + Assert.AreEqual("i1", Helpers.MakeList(project.GetItems("j"))[0].EvaluatedInclude); + Assert.AreEqual("m1", Helpers.MakeList(project.GetItems("j"))[0].GetMetadataValue("m")); + Assert.AreEqual("n;;", Helpers.MakeList(project.GetItems("j"))[0].GetMetadataValue("n")); + } + + /// + /// Wildcards are expanded in item groups inside targets, and the evaluatedinclude + /// is not the wildcard itself! + /// + [TestMethod] + [TestCategory("serialize")] + public void WildcardsInsideTargets() + { + string directory = null; + string file1 = null; + string file2 = null; + string file3 = null; + + try + { + directory = Path.Combine(Path.GetTempPath(), "WildcardsInsideTargets"); + Directory.CreateDirectory(directory); + file1 = Path.Combine(directory, "a.exe"); + file2 = Path.Combine(directory, "b.exe"); + file3 = Path.Combine(directory, "c.bat"); + File.WriteAllText(file1, String.Empty); + File.WriteAllText(file2, String.Empty); + File.WriteAllText(file3, String.Empty); + + string path = Path.Combine(directory, "*.exe"); + + string content = @" + + + + + + + "; + + ProjectInstance projectInstance = GetProjectInstance(content); + projectInstance.Build(); + + Assert.AreEqual(2, Helpers.MakeList(projectInstance.GetItems("i")).Count); + Assert.AreEqual(file1, Helpers.MakeList(projectInstance.GetItems("i"))[0].EvaluatedInclude); + Assert.AreEqual(file2, Helpers.MakeList(projectInstance.GetItems("i"))[1].EvaluatedInclude); + } + finally + { + File.Delete(file1); + File.Delete(file2); + File.Delete(file3); + Directory.Delete(directory); + } + } + + /// + /// Items are cloned properly + /// + [TestMethod] + public void CloneItems() + { + ProjectInstance first = GetSampleProjectInstance(); + ProjectInstance second = first.DeepCopy(); + + Assert.IsTrue(!Object.ReferenceEquals(Helpers.MakeList(first.GetItems("i"))[0], Helpers.MakeList(second.GetItems("i"))[0])); + + first.AddItem("i", "i3"); + Assert.AreEqual(4, Helpers.MakeList(first.GetItems("i")).Count); + Assert.AreEqual(3, Helpers.MakeList(second.GetItems("i")).Count); + } + + /// + /// Null target in array should give ArgumentNullException + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void BuildNullTargetInArray() + { + ProjectInstance instance = new ProjectInstance(ProjectRootElement.Create()); + instance.Build(new string[] { null }, null); + } + + /// + /// Null logger in array should give ArgumentNullException + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void BuildNullLoggerInArray() + { + ProjectInstance instance = new ProjectInstance(ProjectRootElement.Create()); + instance.Build("t", new ILogger[] { null }); + } + + /// + /// Null remote logger in array should give ArgumentNullException + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void BuildNullRemoteLoggerInArray() + { + ProjectInstance instance = new ProjectInstance(ProjectRootElement.Create()); + instance.Build("t", null, new ForwardingLoggerRecord[] { null }); + } + + /// + /// Null target name should imply the default target + /// + [TestMethod] + [TestCategory("serialize")] + public void BuildNullTargetNameIsDefaultTarget() + { + ProjectRootElement xml = ProjectRootElement.Create(); + xml.AddTarget("t").AddTask("Message").SetParameter("Text", "[OK]"); + ProjectInstance instance = new ProjectInstance(xml); + MockLogger logger = new MockLogger(); + string target = null; + instance.Build(target, new ILogger[] { logger }); + logger.AssertLogContains("[OK]"); + } + + /// + /// Build system should correctly reset itself between builds of + /// project instances. + /// + [TestMethod] + [TestCategory("serialize")] + public void BuildProjectInstancesConsecutively() + { + ProjectInstance instance1 = new Project().CreateProjectInstance(); + + BuildRequestData buildRequestData1 = new BuildRequestData(instance1, new string[] { }); + + BuildManager.DefaultBuildManager.Build(new BuildParameters(), buildRequestData1); + + ProjectInstance instance2 = new Project().CreateProjectInstance(); + + BuildRequestData buildRequestData2 = new BuildRequestData(instance1, new string[] { }); + + BuildManager.DefaultBuildManager.Build(new BuildParameters(), buildRequestData2); + } + + /// + /// Verifies that the built-in metadata for specialized ProjectInstances is present when items are the simplest (no macros or wildcards). + /// + [TestMethod] + public void CreateProjectInstanceWithItemsContainingProjects() + { + const string CapturedMetadataName = "DefiningProjectFullPath"; + var pc = new ProjectCollection(); + var projA = ProjectRootElement.Create(pc); + var projB = ProjectRootElement.Create(pc); + projA.FullPath = Path.Combine(Path.GetTempPath(), "a.proj"); + projB.FullPath = Path.Combine(Path.GetTempPath(), "b.proj"); + projB.AddImport("a.proj"); + projA.AddItem("Compile", "aItem.cs"); + projB.AddItem("Compile", "bItem.cs"); + + var projBEval = new Project(projB, null, null, pc); + var projBInstance = projBEval.CreateProjectInstance(); + var projBInstanceItem = projBInstance.GetItemsByItemTypeAndEvaluatedInclude("Compile", "bItem.cs").Single(); + var projAInstanceItem = projBInstance.GetItemsByItemTypeAndEvaluatedInclude("Compile", "aItem.cs").Single(); + Assert.AreEqual(ProjectCollection.Escape(projB.FullPath), projBInstanceItem.GetMetadataValue(CapturedMetadataName)); + Assert.AreEqual(ProjectCollection.Escape(projA.FullPath), projAInstanceItem.GetMetadataValue(CapturedMetadataName)); + + // Although GetMetadataValue returns non-null, GetMetadata returns null... + Assert.IsNull(projAInstanceItem.GetMetadata(CapturedMetadataName)); + + // .. Just like built-in metadata does: (this segment just demonstrates similar functionality -- it's not meant to test built-in metadata) + Assert.IsNotNull(projAInstanceItem.GetMetadataValue("Identity")); + Assert.IsNull(projAInstanceItem.GetMetadata("Identity")); + + Assert.IsTrue(projAInstanceItem.HasMetadata(CapturedMetadataName)); + Assert.IsFalse(projAInstanceItem.Metadata.Any()); + Assert.IsTrue(projAInstanceItem.MetadataNames.Contains(CapturedMetadataName, StringComparer.OrdinalIgnoreCase)); + Assert.AreEqual(projAInstanceItem.MetadataCount, projAInstanceItem.MetadataNames.Count); + } + + /// + /// Verifies that the built-in metadata for specialized ProjectInstances is present when items are based on wildcards in the construction model. + /// + [TestMethod] + public void DefiningProjectItemBuiltInMetadataFromWildcards() + { + const string CapturedMetadataName = "DefiningProjectFullPath"; + var pc = new ProjectCollection(); + var projA = ProjectRootElement.Create(pc); + var projB = ProjectRootElement.Create(pc); + + string tempDir = Path.GetTempFileName(); + File.Delete(tempDir); + Directory.CreateDirectory(tempDir); + File.Create(Path.Combine(tempDir, "aItem.cs")).Dispose(); + + projA.FullPath = Path.Combine(tempDir, "a.proj"); + projB.FullPath = Path.Combine(tempDir, "b.proj"); + projB.AddImport("a.proj"); + projA.AddItem("Compile", "*.cs"); + projB.AddItem("CompileB", "@(Compile)"); + + var projBEval = new Project(projB, null, null, pc); + var projBInstance = projBEval.CreateProjectInstance(); + var projAInstanceItem = projBInstance.GetItemsByItemTypeAndEvaluatedInclude("Compile", "aItem.cs").Single(); + var projBInstanceItem = projBInstance.GetItemsByItemTypeAndEvaluatedInclude("CompileB", "aItem.cs").Single(); + Assert.AreEqual(ProjectCollection.Escape(projA.FullPath), projAInstanceItem.GetMetadataValue(CapturedMetadataName)); + Assert.AreEqual(ProjectCollection.Escape(projB.FullPath), projBInstanceItem.GetMetadataValue(CapturedMetadataName)); + + Assert.IsTrue(projAInstanceItem.HasMetadata(CapturedMetadataName)); + Assert.IsFalse(projAInstanceItem.Metadata.Any()); + Assert.IsTrue(projAInstanceItem.MetadataNames.Contains(CapturedMetadataName, StringComparer.OrdinalIgnoreCase)); + Assert.AreEqual(projAInstanceItem.MetadataCount, projAInstanceItem.MetadataNames.Count); + } + + /// + /// Validate that the DefiningProject* metadata is set to the correct project based on a variety + /// of means of item creation. + /// + [TestMethod] + public void TestDefiningProjectMetadata() + { + string projectA = Path.Combine(ObjectModelHelpers.TempProjectDir, "a.proj"); + string projectB = Path.Combine(ObjectModelHelpers.TempProjectDir, "b.proj"); + + string includeFileA = Path.Combine(ObjectModelHelpers.TempProjectDir, "aaa4.cs"); + string includeFileB = Path.Combine(ObjectModelHelpers.TempProjectDir, "bbb4.cs"); + + string contentsA = + @" + + + + + + Bar + + + + + + + + + '%(Filename)')` /> + WithMetadataValue('Foo', 'Bar'))` /> + + + + + + + + + '%(Filename)')` /> + WithMetadataValue('Foo', 'Bar'))` /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + string contentsB = + @" + + + + + + Bar + + + + '%(Filename)')` /> +

WithMetadataValue('Foo', 'Bar'))` /> + + + + + + + + + + '%(Filename)')` /> + WithMetadataValue('Foo', 'Bar'))` /> + + + +"; + + try + { + File.WriteAllText(projectA, ObjectModelHelpers.CleanupFileContents(contentsA)); + File.WriteAllText(projectB, ObjectModelHelpers.CleanupFileContents(contentsB)); + + File.WriteAllText(includeFileA, "aaaaaaa"); + File.WriteAllText(includeFileB, "bbbbbbb"); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess("a.proj"); + logger.AssertNoWarnings(); + } + finally + { + if (File.Exists(projectA)) + { + File.Delete(projectA); + } + + if (File.Exists(projectB)) + { + File.Delete(projectB); + } + + if (File.Exists(includeFileA)) + { + File.Delete(includeFileA); + } + + if (File.Exists(includeFileB)) + { + File.Delete(includeFileB); + } + } + } + + ///

+ /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetProperty() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { instance.SetProperty("a", "b"); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_RemoveProperty() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { instance.RemoveProperty("p1"); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_RemoveItem() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { instance.RemoveItem(Helpers.GetFirst(instance.Items)); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_AddItem() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { instance.AddItem("a", "b"); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_AddItemWithMetadata() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { instance.AddItem("a", "b", new List>()); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_Build() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { instance.Build(); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetEvaluatedInclude() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { Helpers.GetFirst(instance.Items).EvaluatedInclude = "x"; }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetEvaluatedIncludeEscaped() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { ((ITaskItem2)Helpers.GetFirst(instance.Items)).EvaluatedIncludeEscaped = "x"; }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetItemSpec() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { ((ITaskItem2)Helpers.GetFirst(instance.Items)).ItemSpec = "x"; }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetMetadataOnItem1() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { ((ITaskItem2)Helpers.GetFirst(instance.Items)).SetMetadataValueLiteral("a", "b"); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetMetadataOnItem2() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { Helpers.GetFirst(instance.Items).SetMetadata(new List>()); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetMetadataOnItem3() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { Helpers.GetFirst(instance.Items).SetMetadata("a", "b"); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_RemoveMetadataFromItem() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate () { Helpers.GetFirst(instance.Items).RemoveMetadata("n"); }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetEvaluatedValueOnProperty() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Properties).EvaluatedValue = "v2"; }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetEvaluatedValueOnPropertyFromProject() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("p1").EvaluatedValue = "v2"; }); + } + + /// + /// Test operation fails on immutable project instance + /// + [TestMethod] + public void ImmutableProjectInstance_SetNewProperty() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.SetProperty("newproperty", "v2"); }); + } + + /// + /// Setting global properties should fail if the project is immutable, even though the property + /// was originally created as mutable + /// + [TestMethod] + public void ImmutableProjectInstance_SetGlobalProperty() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.SetProperty("g", "gv2"); }); + } + + /// + /// Setting environment originating properties should fail if the project is immutable, even though the property + /// was originally created as mutable + /// + [TestMethod] + public void ImmutableProjectInstance_SetEnvironmentProperty() + { + var instance = GetSampleProjectInstance(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.SetProperty("username", "someone_else_here"); }); + } + + /// + /// Cloning inherits unless otherwise specified + /// + [TestMethod] + public void ImmutableProjectInstance_CloneMutableFromImmutable() + { + var protoInstance = GetSampleProjectInstance(true /* immutable */); + var instance = protoInstance.DeepCopy(false /* mutable */); + + // These should not throw + instance.SetProperty("p", "pnew"); + instance.AddItem("i", "ii"); + Helpers.GetFirst(instance.Items).EvaluatedInclude = "new"; + instance.SetProperty("g", "gnew"); + instance.SetProperty("username", "someone_else_here"); + } + + /// + /// Cloning inherits unless otherwise specified + /// + [TestMethod] + public void ImmutableProjectInstance_CloneImmutableFromMutable() + { + var protoInstance = GetSampleProjectInstance(false /* mutable */); + var instance = protoInstance.DeepCopy(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("g").EvaluatedValue = "v2"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("username").EvaluatedValue = "someone_else_here"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Properties).EvaluatedValue = "v2"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Items).EvaluatedInclude = "new"; }); + } + + /// + /// Cloning inherits unless otherwise specified + /// + [TestMethod] + public void ImmutableProjectInstance_CloneImmutableFromImmutable() + { + var protoInstance = GetSampleProjectInstance(true /* immutable */); + var instance = protoInstance.DeepCopy(/* inherit */); + + // Should not have bothered cloning + Assert.IsTrue(Object.ReferenceEquals(protoInstance, instance)); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("g").EvaluatedValue = "v2"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("username").EvaluatedValue = "someone_else_here"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Properties).EvaluatedValue = "v2"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Items).EvaluatedInclude = "new"; }); + } + + /// + /// Cloning inherits unless otherwise specified + /// + [TestMethod] + public void ImmutableProjectInstance_CloneImmutableFromImmutable2() + { + var protoInstance = GetSampleProjectInstance(true /* immutable */); + var instance = protoInstance.DeepCopy(true /* immutable */); + + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("g").EvaluatedValue = "v2"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { instance.GetProperty("username").EvaluatedValue = "someone_else_here"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Properties).EvaluatedValue = "v2"; }); + Helpers.VerifyAssertThrowsInvalidOperation(delegate() { Helpers.GetFirst(instance.Items).EvaluatedInclude = "new"; }); + } + + /// + /// Cloning inherits unless otherwise specified + /// + [TestMethod] + public void ImmutableProjectInstance_CloneMutableFromMutable() + { + var protoInstance = GetSampleProjectInstance(false /* mutable */); + var instance = protoInstance.DeepCopy(/* inherit */); + + // These should not throw + instance.SetProperty("p", "pnew"); + instance.AddItem("i", "ii"); + Helpers.GetFirst(instance.Items).EvaluatedInclude = "new"; + instance.SetProperty("g", "gnew"); + instance.SetProperty("username", "someone_else_here"); + } + + /// + /// Cloning inherits unless otherwise specified + /// + [TestMethod] + public void ImmutableProjectInstance_CloneMutableFromMutable2() + { + var protoInstance = GetSampleProjectInstance(false /* mutable */); + var instance = protoInstance.DeepCopy(false /* mutable */); + + // These should not throw + instance.SetProperty("p", "pnew"); + instance.AddItem("i", "ii"); + Helpers.GetFirst(instance.Items).EvaluatedInclude = "new"; + instance.SetProperty("g", "gnew"); + instance.SetProperty("username", "someone_else_here"); + } + + /// + /// Create a ProjectInstance with some items and properties and targets + /// + private static ProjectInstance GetSampleProjectInstance(bool isImmutable = false) + { + string content = @" + + + + n1 + + + + v1 + v2 + $(p2)X$(p) + + + + + m1 + + + + + + + + + + "; + + ProjectInstance p = GetProjectInstance(content, isImmutable); + + return p; + } + + /// + /// Create a ProjectInstance from provided project content + /// + private static ProjectInstance GetProjectInstance(string content, bool immutable = false) + { + var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + globalProperties["g"] = "gv"; + + Project project = new Project(XmlReader.Create(new StringReader(content)), globalProperties, "4.0"); + ProjectInstance instance = immutable ? project.CreateProjectInstance(ProjectInstanceSettings.Immutable) : project.CreateProjectInstance(); + + return instance; + } + + /// + /// Create a ProjectInstance that's empty + /// + private static ProjectInstance GetEmptyProjectInstance() + { + ProjectRootElement xml = ProjectRootElement.Create(); + Project project = new Project(xml); + ProjectInstance instance = project.CreateProjectInstance(); + + return instance; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectItemInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectItemInstance_Tests.cs new file mode 100644 index 00000000000..3c5809c84b8 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectItemInstance_Tests.cs @@ -0,0 +1,870 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectItemInstance public members +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectItemInstance public members + /// + [TestClass] + public class ProjectItemInstance_Tests + { + /// + /// The number of built-in metadata for items. + /// + public const int BuiltInMetadataCount = 15; + + /// + /// Basic ProjectItemInstance without metadata + /// + [TestMethod] + public void AccessorsWithoutMetadata() + { + ProjectItemInstance item = GetItemInstance(); + + Assert.AreEqual("i", item.ItemType); + Assert.AreEqual("i1", item.EvaluatedInclude); + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + } + + /// + /// Basic ProjectItemInstance with metadata + /// + [TestMethod] + public void AccessorsWithMetadata() + { + ProjectItemInstance item = GetItemInstance(); + + item.SetMetadata("m1", "v0"); + item.SetMetadata("m1", "v1"); + item.SetMetadata("m2", "v2"); + + Assert.AreEqual("m1", item.GetMetadata("m1").Name); + Assert.AreEqual("m2", item.GetMetadata("m2").Name); + Assert.AreEqual("v1", item.GetMetadataValue("m1")); + Assert.AreEqual("v2", item.GetMetadataValue("m2")); + } + + /// + /// Get metadata not present + /// + [TestMethod] + public void GetMissingMetadata() + { + ProjectItemInstance item = GetItemInstance(); + Assert.AreEqual(null, item.GetMetadata("X")); + Assert.AreEqual(String.Empty, item.GetMetadataValue("X")); + } + + /// + /// Set include + /// + [TestMethod] + public void SetInclude() + { + ProjectItemInstance item = GetItemInstance(); + item.EvaluatedInclude = "i1b"; + Assert.AreEqual("i1b", item.EvaluatedInclude); + } + + /// + /// Set include to empty string + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidEmptyInclude() + { + ProjectItemInstance item = GetItemInstance(); + item.EvaluatedInclude = String.Empty; + } + + /// + /// Set include to invalid null value + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullInclude() + { + ProjectItemInstance item = GetItemInstance(); + item.EvaluatedInclude = null; + } + + /// + /// Create an item with a metadatum that has a null value + /// + [TestMethod] + public void CreateItemWithNullMetadataValue() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + + IDictionary metadata = new Dictionary(); + metadata.Add("m", null); + + ProjectItemInstance item = projectInstance.AddItem("i", "i1", metadata); + Assert.AreEqual(String.Empty, item.GetMetadataValue("m")); + } + + /// + /// Set metadata value + /// + [TestMethod] + public void SetMetadata() + { + ProjectItemInstance item = GetItemInstance(); + item.SetMetadata("m", "m1"); + Assert.AreEqual("m1", item.GetMetadataValue("m")); + } + + /// + /// Set metadata value to empty string + /// + [TestMethod] + public void SetMetadataEmptyString() + { + ProjectItemInstance item = GetItemInstance(); + item.SetMetadata("m", String.Empty); + Assert.AreEqual(String.Empty, item.GetMetadataValue("m")); + } + + /// + /// Set metadata value to null value -- this is allowed, but + /// internally converted to the empty string. + /// + [TestMethod] + public void SetNullMetadataValue() + { + ProjectItemInstance item = GetItemInstance(); + item.SetMetadata("m", null); + Assert.AreEqual(String.Empty, item.GetMetadataValue("m")); + } + + /// + /// Set metadata with invalid empty name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullMetadataName() + { + ProjectItemInstance item = GetItemInstance(); + item.SetMetadata(null, "m1"); + } + + /// + /// Set metadata with invalid empty name + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SetInvalidEmptyMetadataName() + { + ProjectItemInstance item = GetItemInstance(); + item.SetMetadata(String.Empty, "m1"); + } + + /// + /// Cast to ITaskItem + /// + [TestMethod] + public void CastToITaskItem() + { + ProjectItemInstance item = GetItemInstance(); + item.SetMetadata("m", "m1"); + + ITaskItem taskItem = (ITaskItem)item; + + Assert.AreEqual(item.EvaluatedInclude, taskItem.ItemSpec); + Assert.AreEqual(1 + BuiltInMetadataCount, taskItem.MetadataCount); + Assert.AreEqual(1 + BuiltInMetadataCount, taskItem.MetadataNames.Count); + Assert.AreEqual("m1", taskItem.GetMetadata("m")); + taskItem.SetMetadata("m", "m2"); + Assert.AreEqual("m2", item.GetMetadataValue("m")); + } + + /// + /// Creates a ProjectItemInstance and casts it to ITaskItem2; makes sure that all escaped information is + /// maintained correctly. Also creates a new Microsoft.Build.Utilities.TaskItem from the ProjectItemInstance + /// and verifies that none of the information is lost. + /// + [TestMethod] + public void ITaskItem2Operations() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + + ProjectItemInstance item = projectInstance.AddItem("EscapedItem", "esca%20ped%3bitem"); + item.SetMetadata("m", "m1"); + item.SetMetadata("m;", "m%3b1"); + ITaskItem2 taskItem = (ITaskItem2)item; + + Assert.AreEqual(taskItem.EvaluatedIncludeEscaped, "esca%20ped%3bitem"); + Assert.AreEqual(taskItem.ItemSpec, "esca ped;item"); + + Assert.AreEqual(taskItem.GetMetadata("m;"), "m;1"); + Assert.AreEqual(taskItem.GetMetadataValueEscaped("m;"), "m%3b1"); + Assert.AreEqual(taskItem.GetMetadataValueEscaped("m"), "m1"); + + Assert.AreEqual(taskItem.EvaluatedIncludeEscaped, "esca%20ped%3bitem"); + Assert.AreEqual(taskItem.ItemSpec, "esca ped;item"); + + ITaskItem2 taskItem2 = new Microsoft.Build.Utilities.TaskItem(taskItem); + + taskItem2.SetMetadataValueLiteral("m;", "m;2"); + + Assert.AreEqual(taskItem2.GetMetadataValueEscaped("m;"), "m%3b2"); + Assert.AreEqual(taskItem2.GetMetadata("m;"), "m;2"); + + IDictionary taskItem2Metadata = (IDictionary)taskItem2.CloneCustomMetadata(); + Assert.AreEqual(3, taskItem2Metadata.Count); + + foreach (KeyValuePair pair in taskItem2Metadata) + { + if (pair.Key.Equals("m")) + { + Assert.AreEqual("m1", pair.Value); + } + + if (pair.Key.Equals("m;")) + { + Assert.AreEqual("m;2", pair.Value); + } + + if (pair.Key.Equals("OriginalItemSpec")) + { + Assert.AreEqual("esca ped;item", pair.Value); + } + } + + IDictionary taskItem2MetadataEscaped = (IDictionary)taskItem2.CloneCustomMetadataEscaped(); + Assert.AreEqual(3, taskItem2MetadataEscaped.Count); + + foreach (KeyValuePair pair in taskItem2MetadataEscaped) + { + if (pair.Key.Equals("m")) + { + Assert.AreEqual("m1", pair.Value); + } + + if (pair.Key.Equals("m;")) + { + Assert.AreEqual("m%3b2", pair.Value); + } + + if (pair.Key.Equals("OriginalItemSpec")) + { + Assert.AreEqual("esca%20ped%3bitem", pair.Value); + } + } + } + + /// + /// Cast to ITaskItem + /// + [TestMethod] + public void CastToITaskItemNoMetadata() + { + ProjectItemInstance item = GetItemInstance(); + + ITaskItem taskItem = (ITaskItem)item; + + Assert.AreEqual(0 + BuiltInMetadataCount, taskItem.MetadataCount); + Assert.AreEqual(0 + BuiltInMetadataCount, taskItem.MetadataNames.Count); + Assert.AreEqual(String.Empty, taskItem.GetMetadata("m")); + } + + /* + * We must repeat all the evaluation-related tests here, + * to exercise the path that evaluates directly to instance objects. + * Although the Evaluator class is shared, its interactions with the two + * different item classes could be different, and shouldn't be. + */ + + /// + /// No metadata, simple case + /// + [TestMethod] + public void NoMetadata() + { + string content = @" + + + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + Assert.AreEqual("i", item.ItemType); + Assert.AreEqual("i1", item.EvaluatedInclude); + Assert.AreEqual(false, item.Metadata.GetEnumerator().MoveNext()); + Assert.AreEqual(0 + BuiltInMetadataCount, Helpers.MakeList(item.MetadataNames).Count); + Assert.AreEqual(0 + BuiltInMetadataCount, item.MetadataCount); + } + + /// + /// Read off metadata + /// + [TestMethod] + public void ReadMetadata() + { + string content = @" + + + + v1 + v2 + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + var itemMetadata = Helpers.MakeList(item.Metadata); + Assert.AreEqual(2, itemMetadata.Count); + Assert.AreEqual("m1", itemMetadata[0].Name); + Assert.AreEqual("m2", itemMetadata[1].Name); + Assert.AreEqual("v1", itemMetadata[0].EvaluatedValue); + Assert.AreEqual("v2", itemMetadata[1].EvaluatedValue); + + Assert.AreEqual(itemMetadata[0], item.GetMetadata("m1")); + Assert.AreEqual(itemMetadata[1], item.GetMetadata("m2")); + } + + /// + /// Create a new Microsoft.Build.Utilities.TaskItem from the ProjectItemInstance where the ProjectItemInstance + /// has item definition metadata on it. + /// + /// Verify the Utilities task item gets the expanded metadata from the ItemDefintionGroup. + /// + [TestMethod] + public void InstanceItemToUtilItemIDG() + { + string content = @" + + + + ;x86; + %(FileName).extension + ;%(FileName).extension; + v1 + %3bx86%3b + + + + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + Microsoft.Build.Utilities.TaskItem taskItem = new Microsoft.Build.Utilities.TaskItem(item); + + Assert.AreEqual(";x86;", taskItem.GetMetadata("m0")); + Assert.AreEqual("foo.extension", taskItem.GetMetadata("m1")); + Assert.AreEqual(";foo.extension;", taskItem.GetMetadata("m2")); + Assert.AreEqual("v1", taskItem.GetMetadata("m3")); + Assert.AreEqual(";x86;", taskItem.GetMetadata("m4")); + } + + /// + /// Get metadata values inherited from item definitions + /// + [TestMethod] + public void GetMetadataValuesFromDefinition() + { + string content = @" + + + + v0 + v1 + + + + + v1b + v2 + + + + "; + + ProjectItemInstance item = GetOneItem(content); + Assert.AreEqual("v0", item.GetMetadataValue("m0")); + Assert.AreEqual("v1b", item.GetMetadataValue("m1")); + Assert.AreEqual("v2", item.GetMetadataValue("m2")); + + Assert.AreEqual(3, Helpers.MakeList(item.Metadata).Count); + Assert.AreEqual(3 + BuiltInMetadataCount, Helpers.MakeList(item.MetadataNames).Count); + Assert.AreEqual(3 + BuiltInMetadataCount, item.MetadataCount); + } + + /// + /// Exclude against an include with item vectors in it + /// + [TestMethod] + public void ExcludeWithIncludeVector() + { + string content = @" + + + + + + + + + + + + "; + + IList items = GetItems(content); + + // Should contain a, b, c, x, z, a, c, u, w + Assert.AreEqual(9, items.Count); + AssertEvaluatedIncludes(items, new string[] { "a", "b", "c", "x", "z", "a", "c", "u", "w" }); + } + + /// + /// Exclude with item vectors against an include with item vectors in it + /// + [TestMethod] + public void ExcludeVectorWithIncludeVector() + { + string content = @" + + + + + + + + + + + + + "; + + IList items = GetItems(content); + + // Should contain a, b, c, z, a, c, u + Assert.AreEqual(7, items.Count); + AssertEvaluatedIncludes(items, new string[] { "a", "b", "c", "z", "a", "c", "u" }); + } + + /// + /// Metadata on items can refer to metadata above + /// + [TestMethod] + public void MetadataReferringToMetadataAbove() + { + string content = @" + + + + v1 + %(m1);v2;%(m0) + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + var itemMetadata = Helpers.MakeList(item.Metadata); + Assert.AreEqual(2, itemMetadata.Count); + Assert.AreEqual("v1;v2;", item.GetMetadataValue("m2")); + } + + /// + /// Built-in metadata should work, too. + /// NOTE: To work properly, this should batch. This is a temporary "patch" to make it work for now. + /// It will only give correct results if there is exactly one item in the Include. Otherwise Batching would be needed. + /// + [TestMethod] + public void BuiltInMetadataExpression() + { + string content = @" + + + + %(Identity) + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + Assert.AreEqual("i1", item.GetMetadataValue("m")); + } + + /// + /// Qualified built in metadata should work + /// + [TestMethod] + public void BuiltInQualifiedMetadataExpression() + { + string content = @" + + + + %(i.Identity) + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + Assert.AreEqual("i1", item.GetMetadataValue("m")); + } + + /// + /// Mis-qualified built in metadata should not work + /// + [TestMethod] + public void BuiltInMisqualifiedMetadataExpression() + { + string content = @" + + + + %(j.Identity) + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + Assert.AreEqual(String.Empty, item.GetMetadataValue("m")); + } + + /// + /// Metadata condition should work correctly with built-in metadata + /// + [TestMethod] + public void BuiltInMetadataInMetadataCondition() + { + string content = @" + + + + m1 + n1 + + + + "; + + ProjectItemInstance item = GetOneItem(content); + + Assert.AreEqual("m1", item.GetMetadataValue("m")); + Assert.AreEqual(String.Empty, item.GetMetadataValue("n")); + } + + /// + /// Metadata on item condition not allowed (currently) + /// + [TestMethod] + [ExpectedException(typeof(InvalidProjectFileException))] + public void BuiltInMetadataInItemCondition() + { + string content = @" + + + + + + "; + + GetOneItem(content); + } + + /// + /// Two items should each get their own values for built-in metadata + /// + [TestMethod] + public void BuiltInMetadataTwoItems() + { + string content = @" + + + + %(Filename).obj + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"i1.obj", items[0].GetMetadataValue("m")); + Assert.AreEqual(@"i2.obj", items[1].GetMetadataValue("m")); + } + + /// + /// Items from another list, but with different metadata + /// + [TestMethod] + public void DifferentMetadataItemsFromOtherList() + { + string content = @" + + + + m1 + + + + + %(m) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"m1", items[0].GetMetadataValue("m")); + Assert.AreEqual(String.Empty, items[1].GetMetadataValue("m")); + } + + /// + /// Items from another list, but with different metadata + /// + [TestMethod] + public void DifferentBuiltInMetadataItemsFromOtherList() + { + string content = @" + + + + + + + %(extension) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@".x", items[0].GetMetadataValue("m")); + Assert.AreEqual(@".y", items[1].GetMetadataValue("m")); + } + + /// + /// Two items coming from a transform + /// + [TestMethod] + public void BuiltInMetadataTransformInInclude() + { + string content = @" + + + + + + '%(Identity).baz')""> + %(Filename)%(Extension).obj + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"h0.baz.obj", items[0].GetMetadataValue("m")); + Assert.AreEqual(@"h1.baz.obj", items[1].GetMetadataValue("m")); + } + + /// + /// Transform in the metadata value; no bare metadata involved + /// + [TestMethod] + public void BuiltInMetadataTransformInMetadataValue() + { + string content = @" + + + + + + + @(i);@(h->'%(Filename)') + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"i0;h0;h1", items[1].GetMetadataValue("m")); + Assert.AreEqual(@"i0;h0;h1", items[2].GetMetadataValue("m")); + } + + /// + /// Transform in the metadata value; bare metadata involved + /// + [TestMethod] + public void BuiltInMetadataTransformInMetadataValueBareMetadataPresent() + { + string content = @" + + + + + + + @(i);@(h->'%(Filename)');%(Extension) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual(@"i0.x;h0;h1;.y", items[1].GetMetadataValue("m")); + Assert.AreEqual(@"i0.x;h0;h1;", items[2].GetMetadataValue("m")); + } + + /// + /// Metadata on items can refer to item lists + /// + [TestMethod] + public void MetadataValueReferringToItems() + { + string content = @" + + + + + + @(h);@(i) + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual("h0;i0", items[1].GetMetadataValue("m1")); + } + + /// + /// Metadata on items' conditions can refer to item lists + /// + [TestMethod] + public void MetadataConditionReferringToItems() + { + string content = @" + + + + + + v1 + v2 + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual("v1", items[1].GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, items[1].GetMetadataValue("m2")); + } + + /// + /// Metadata on items' conditions can refer to other metadata + /// + [TestMethod] + public void MetadataConditionReferringToMetadataOnSameItem() + { + string content = @" + + + + 0 + 1 + 2 + + + + "; + + IList items = GetItems(content); + + Assert.AreEqual("0", items[0].GetMetadataValue("m0")); + Assert.AreEqual("1", items[0].GetMetadataValue("m1")); + Assert.AreEqual(String.Empty, items[0].GetMetadataValue("m2")); + } + + /// + /// Gets the first item of type 'i' + /// + private static ProjectItemInstance GetOneItem(string content) + { + return GetItems(content)[0]; + } + + /// + /// Get all items of type 'i' + /// + private static IList GetItems(string content) + { + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + ProjectInstance project = new ProjectInstance(xml); + + return Helpers.MakeList(project.GetItems("i")); + } + + /// + /// Asserts that the list of items has the specified includes. + /// + private static void AssertEvaluatedIncludes(IList items, string[] includes) + { + for (int i = 0; i < includes.Length; i++) + { + Assert.AreEqual(includes[i], items[i].EvaluatedInclude); + } + } + + /// + /// Get a single item instance + /// + private static ProjectItemInstance GetItemInstance() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectItemInstance item = projectInstance.AddItem("i", "i1"); + return item; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectMetadataInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectMetadataInstance_Tests.cs new file mode 100644 index 00000000000..2c9a45f2ee9 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectMetadataInstance_Tests.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectMetadataInstance public members +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectMetadataInstance public members + /// + [TestClass] + public class ProjectMetadataInstance_Tests + { + /// + /// Get name and value + /// + [TestMethod] + public void Accessors() + { + ProjectMetadataInstance metadata = GetMetadataInstance(); + + Assert.AreEqual("m", metadata.Name); + Assert.AreEqual("m1", metadata.EvaluatedValue); + } + + /// + /// Get a single metadata instance + /// + private static ProjectMetadataInstance GetMetadataInstance() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectItemInstance item = projectInstance.AddItem("i", "i1"); + ProjectMetadataInstance metadata = item.SetMetadata("m", "m1"); + return metadata; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectOnErrorInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectOnErrorInstance_Tests.cs new file mode 100644 index 00000000000..617b6f553a2 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectOnErrorInstance_Tests.cs @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectOnErrorInstance class. +//----------------------------------------------------------------------- + +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for the ProjectOnErrorInstance class. + /// + [TestClass] + public class ProjectOnErrorInstance_Tests + { + /// + /// Test accessors + /// + [TestMethod] + public void Accessors() + { + var onError = GetSampleOnErrorInstance(); + + Assert.AreEqual("et", onError.ExecuteTargets); + Assert.AreEqual("c", onError.Condition); + } + + /// + /// Create a TaskInstance with some parameters + /// + private static ProjectOnErrorInstance GetSampleOnErrorInstance() + { + string content = @" + + + + + + "; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(xml); + ProjectInstance instance = project.CreateProjectInstance(); + ProjectOnErrorInstance onError = (ProjectOnErrorInstance)instance.Targets["t"].OnErrorChildren[0]; + + return onError; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectPropertyInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectPropertyInstance_Tests.cs new file mode 100644 index 00000000000..bfdb610552a --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectPropertyInstance_Tests.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for ProjectPropertyInstance public members +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectPropertyInstance public members + /// + [TestClass] + public class ProjectPropertyInstance_Tests + { + /// + /// Get name and value + /// + [TestMethod] + public void Accessors() + { + ProjectPropertyInstance property = GetPropertyInstance(); + + Assert.AreEqual("p", property.Name); + Assert.AreEqual("v1", property.EvaluatedValue); + } + + /// + /// Set value + /// + [TestMethod] + public void SetValue() + { + ProjectPropertyInstance property = GetPropertyInstance(); + property.EvaluatedValue = "v2"; + Assert.AreEqual("v2", property.EvaluatedValue); + } + + /// + /// Set value + /// + [TestMethod] + public void SetValue_Escaped() + { + ProjectPropertyInstance property = GetPropertyInstance(); + property.EvaluatedValue = "v!2"; + Assert.AreEqual("v!2", property.EvaluatedValue); + } + + /// + /// Set empty value + /// + [TestMethod] + public void SetEmptyValue() + { + ProjectPropertyInstance property = GetPropertyInstance(); + property.EvaluatedValue = String.Empty; + Assert.AreEqual(String.Empty, property.EvaluatedValue); + } + + /// + /// Set invalid null value + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SetInvalidNullValue() + { + ProjectPropertyInstance property = GetPropertyInstance(); + property.EvaluatedValue = null; + } + + /// + /// Immutable getter + /// + [TestMethod] + public void ImmutableGetterFalse() + { + ProjectPropertyInstance property = GetPropertyInstance(); + Assert.AreEqual(false, property.IsImmutable); + } + + /// + /// Immutable getter true + /// + [TestMethod] + public void ImmutableGetterTrue() + { + var project = new Project(); + project.SetProperty("p", "v1"); + var snapshot = project.CreateProjectInstance(ProjectInstanceSettings.Immutable); + var property = snapshot.GetProperty("p"); + Assert.AreEqual(true, property.IsImmutable); + } + + /// + /// Get a ProjectPropertyInstance + /// + private static ProjectPropertyInstance GetPropertyInstance() + { + Project project = new Project(); + ProjectInstance projectInstance = project.CreateProjectInstance(); + ProjectPropertyInstance property = projectInstance.SetProperty("p", "v1"); + + return property; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTargetInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTargetInstance_Tests.cs new file mode 100644 index 00000000000..023a066725b --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTargetInstance_Tests.cs @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectTargetInstanceTests class. +//----------------------------------------------------------------------- + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectTargetInstance + /// + [TestClass] + public class ProjectTargetInstance_Tests + { + /// + /// Test accessors + /// + [TestMethod] + public void Accessors() + { + ProjectTargetInstance target = GetSampleTargetInstance(); + + Assert.AreEqual("t", target.Name); + Assert.AreEqual("c", target.Condition); + Assert.AreEqual("i", target.Inputs); + Assert.AreEqual("o", target.Outputs); + Assert.AreEqual("d", target.DependsOnTargets); + Assert.AreEqual("k", target.KeepDuplicateOutputs); + Assert.AreEqual("r", target.Returns); + Assert.AreEqual("t1", ((ProjectTaskInstance)target.Children[0]).Name); + + IList tasks = Helpers.MakeList(target.Tasks); + Assert.AreEqual(1, tasks.Count); + Assert.AreEqual("t1", tasks[0].Name); + } + + /// + /// Evaluation of a project with more than one target with the same name + /// should skip all but the last one. + /// + [TestMethod] + public void TargetOverride() + { + ProjectRootElement projectXml = ProjectRootElement.Create(); + projectXml.AddTarget("t").Inputs = "i1"; + projectXml.AddTarget("t").Inputs = "i2"; + + Project project = new Project(projectXml); + ProjectInstance instance = project.CreateProjectInstance(); + + ProjectTargetInstance target = instance.Targets["t"]; + + Assert.AreEqual("i2", target.Inputs); + } + + /// + /// Evaluation of a project with more than one target with the same name + /// should skip all but the last one. This is true even if the targets + /// involved only have the same unescaped name (Orcas compat) + /// + [TestMethod] + public void TargetOverride_Escaped() + { + ProjectRootElement projectXml = ProjectRootElement.Create(); + projectXml.AddTarget("t%3b").Inputs = "i1"; + projectXml.AddTarget("t;").Inputs = "i2"; + + Project project = new Project(projectXml); + ProjectInstance instance = project.CreateProjectInstance(); + + ProjectTargetInstance target = instance.Targets["t;"]; + + Assert.AreEqual("i2", target.Inputs); + } + + /// + /// Evaluation of a project with more than one target with the same name + /// should skip all but the last one. This is true even if the targets + /// involved only have the same unescaped name (Orcas compat) + /// + [TestMethod] + public void TargetOverride_Escaped2() + { + ProjectRootElement projectXml = ProjectRootElement.Create(); + projectXml.AddTarget("t;").Inputs = "i1"; + projectXml.AddTarget("t%3b").Inputs = "i2"; + + Project project = new Project(projectXml); + ProjectInstance instance = project.CreateProjectInstance(); + + ProjectTargetInstance target = instance.Targets["t;"]; + + Assert.AreEqual("i2", target.Inputs); + } + + /// + /// Verify that targets from a saved, but subsequently edited, project + /// provide the correct full path. + /// + [TestMethod] + public void FileLocationAvailableEvenAfterEdits() + { + string path = null; + + try + { + path = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile(); + ProjectRootElement projectXml = ProjectRootElement.Create(path); + projectXml.Save(); + + projectXml.AddTarget("t"); + + Project project = new Project(projectXml); + ProjectTargetInstance target = project.Targets["t"]; + + Assert.AreEqual(project.FullPath, target.FullPath); + } + finally + { + File.Delete(path); + } + } + + /// + /// Create a ProjectTargetInstance with some parameters + /// + private static ProjectTargetInstance GetSampleTargetInstance() + { + string content = @" + + + + + + "; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(xml); + ProjectInstance instance = project.CreateProjectInstance(); + ProjectTargetInstance target = instance.Targets["t"]; + + return target; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskInstance_Tests.cs new file mode 100644 index 00000000000..94c6f0f1671 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskInstance_Tests.cs @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectTaskInstance class. +//----------------------------------------------------------------------- + +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for ProjectTaskInstance + /// + [TestClass] + public class ProjectTaskInstance_Tests + { + /// + /// Test accessors + /// + [TestMethod] + public void Accessors() + { + var task = GetSampleTaskInstance(); + + Assert.AreEqual("t1", task.Name); + Assert.AreEqual("c", task.Condition); + Assert.AreEqual("coe", task.ContinueOnError); + + var parameters = task.Parameters; + Assert.AreEqual(2, parameters.Count); + Assert.AreEqual("a1", parameters["a"]); + Assert.AreEqual("b1", parameters["b"]); + } + + /// + /// Generally, empty parameters aren't set on task classes at all, but there is + /// one exception: if the empty parameter corresponds to a task class property + /// of array type, an empty array is set on the task class. + /// Therefore empty task parameters should be returned by the parameter list. + /// + [TestMethod] + public void EmptyParameter() + { + var task = GetTaskInstance(@""); + + Assert.AreEqual(1, task.Parameters.Count); + } + + /// + /// Create a TaskInstance with some parameters + /// + private static ProjectTaskInstance GetSampleTaskInstance() + { + ProjectTaskInstance task = GetTaskInstance(@""); + + return task; + } + + /// + /// Return a task instance representing the task XML string passed in + /// + private static ProjectTaskInstance GetTaskInstance(string taskXmlString) + { + string content = @" + + + " + taskXmlString + @" + + + "; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(xml); + ProjectInstance instance = project.CreateProjectInstance(); + ProjectTaskInstance task = (ProjectTaskInstance)(instance.Targets["t"].Children[0]); + return task; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputItemInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputItemInstance_Tests.cs new file mode 100644 index 00000000000..e19f2a61cd1 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputItemInstance_Tests.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectTaskOutputItemInstance class. +//----------------------------------------------------------------------- + +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for the ProjectTaskOutputItemInstance class. + /// + [TestClass] + public class ProjectTaskOutputItemInstance_Tests + { + /// + /// Test accessors + /// + [TestMethod] + public void Accessors() + { + var output = GetSampleTaskOutputInstance(); + + Assert.AreEqual("p", output.TaskParameter); + Assert.AreEqual("c", output.Condition); + Assert.AreEqual("i", output.ItemType); + } + + /// + /// Create a TaskInstance with some parameters + /// + private static ProjectTaskOutputItemInstance GetSampleTaskOutputInstance() + { + string content = @" + + + + + + + + "; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(xml); + ProjectInstance instance = project.CreateProjectInstance(); + ProjectTaskInstance task = (ProjectTaskInstance)instance.Targets["t"].Children[0]; + ProjectTaskOutputItemInstance output = (ProjectTaskOutputItemInstance)task.Outputs[0]; + + return output; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputPropertyInstance_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputPropertyInstance_Tests.cs new file mode 100644 index 00000000000..9e62ec9dca4 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Instance/ProjectTaskOutputPropertyInstance_Tests.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for the ProjectTaskOutputPropertyInstanceTests class. +//----------------------------------------------------------------------- + +using System.IO; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.OM.Instance +{ + /// + /// Tests for the ProjectTaskOutputItemInstance class. + /// + [TestClass] + public class ProjectTaskOutputPropertyInstance_Tests + { + /// + /// Test accessors + /// + [TestMethod] + public void Accessors() + { + var output = GetSampleTaskOutputInstance(); + + Assert.AreEqual("p", output.TaskParameter); + Assert.AreEqual("c", output.Condition); + Assert.AreEqual("p1", output.PropertyName); + } + + /// + /// Create a ProjectTaskOutputPropertyInstance with some parameters + /// + private static ProjectTaskOutputPropertyInstance GetSampleTaskOutputInstance() + { + string content = @" + + + + + + + + "; + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); + Project project = new Project(xml); + ProjectInstance instance = project.CreateProjectInstance(); + ProjectTaskInstance task = (ProjectTaskInstance)instance.Targets["t"].Children[0]; + ProjectTaskOutputPropertyInstance output = (ProjectTaskOutputPropertyInstance)task.Outputs[0]; + + return output; + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/LazyFormattedEventArgs_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/LazyFormattedEventArgs_Tests.cs new file mode 100644 index 00000000000..3e4a12d3776 --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/LazyFormattedEventArgs_Tests.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Tests for LazyFormattedEventArgs +//----------------------------------------------------------------------- +using System; +using System.Linq; +using System.Xml; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text.RegularExpressions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using System.Text; +using System.IO; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.UnitTests.Framework +{ + /// + /// Tests for LazyFormattedEventArgs + /// + [TestClass] + public class LazyFormattedEventArgs_Tests + { + /// + /// Don't crash when task logs with too few format markers + /// + [TestMethod] + public void DoNotCrashOnInvalidFormatExpression() + { + string content = @" + + + + + this.Log.LogError(`Correct: {0}`, `[goodone]`); + this.Log.LogError(`This is a message logged from a task {1} blah blah [crashing].`, `[crasher]`); + + try + { + this.Log.LogError(`Correct: {0}`, 4224); + this.Log.LogError(`Malformed: {1}`, 42); // Line 13 + throw new InvalidOperationException(); + } + catch (Exception e) + { + this.Log.LogError(`Catching: {0}`, e.GetType().Name); + } + finally + { + this.Log.LogError(`Finally`); + } + + try + { + this.Log.LogError(`Correct: {0}`, 4224); + throw new InvalidOperationException(); + } + catch (Exception e) + { + this.Log.LogError(`Catching: {0}`, e.GetType().Name); + this.Log.LogError(`Malformed: {1}`, 42); // Line 19 + } + finally + { + this.Log.LogError(`Finally`); + } + + try + { + this.Log.LogError(`Correct: {0}`, 4224); + throw new InvalidOperationException(); + } + catch (Exception e) + { + this.Log.LogError(`Catching: {0}`, e.GetType().Name); + } + finally + { + this.Log.LogError(`Finally`); + this.Log.LogError(`Malformed: {1}`, 42); // Line 24 + } + + + + + + + + + + "; + + MockLogger log = Helpers.BuildProjectWithNewOMExpectSuccess(content); + + log.AssertLogContains("[goodone]"); + log.AssertLogContains("[crashing]"); + } + } +} \ No newline at end of file diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj b/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj new file mode 100644 index 00000000000..0b595f78eee --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj @@ -0,0 +1,195 @@ + + + + + Debug + AnyCPU + {9D858C8F-8D71-467E-808E-4E4F48DD0A49} + Library + Microsoft.Build.Engine.OM.UnitTests + Microsoft.Build.Engine.OM.UnitTests + v4.5 + 512 + true + true + ..\..\ + true + + + true + full + false + ..\..\build\Debug + TRACE;DEBUG;VS + prompt + 4 + + + full + true + bin\Release + prompt + 4 + false + + + true + ..\..\build\Debug-MONO\ + TRACE;DEBUG;VS;MONO + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + 4 + false + + + ..\..\build\Release-MONO\ + TRACE;VS;MONO + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + 4 + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + True + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll + + + + + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + {828566ee-6f6a-4ef4-98b0-513f7df9c628} + Microsoft.Build.Utilities + + + {16cd7635-7cf4-4c62-a77b-cf87d0f09a58} + Microsoft.Build + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/XMakeBuildEngine/Utilities/EngineFileUtilities.cs b/src/XMakeBuildEngine/Utilities/EngineFileUtilities.cs new file mode 100644 index 00000000000..790f5852dac --- /dev/null +++ b/src/XMakeBuildEngine/Utilities/EngineFileUtilities.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Internal +{ + internal class EngineFileUtilities + { + /// + /// Used for the purposes of evaluating an item specification. Given a filespec that may include wildcard characters * and + /// ?, we translate it into an actual list of files. If the input filespec doesn't contain any wildcard characters, and it + /// doesn't appear to point to an actual file on disk, then we just give back the input string as an array of length one, + /// assuming that it wasn't really intended to be a filename (as items are not required to necessarily represent files). + /// Any wildcards passed in that are unescaped will be treated as real wildcards. + /// The "include" of items passed back from the filesystem will be returned canonically escaped. + /// The ordering of the list returned is deterministic (it is sorted). + /// Will never throw IO exceptions. If path is invalid, just returns filespec verbatim. + /// + /// The directory to evaluate, escaped. + /// The filespec to evaluate, escaped. + /// Array of file paths, unescaped. + internal static string[] GetFileListUnescaped + ( + string directoryEscaped, + string filespecEscaped + ) + + { + return GetFileList(directoryEscaped, filespecEscaped, false /* returnEscaped */); + } + + /// + /// Used for the purposes of evaluating an item specification. Given a filespec that may include wildcard characters * and + /// ?, we translate it into an actual list of files. If the input filespec doesn't contain any wildcard characters, and it + /// doesn't appear to point to an actual file on disk, then we just give back the input string as an array of length one, + /// assuming that it wasn't really intended to be a filename (as items are not required to necessarily represent files). + /// Any wildcards passed in that are unescaped will be treated as real wildcards. + /// The "include" of items passed back from the filesystem will be returned canonically escaped. + /// The ordering of the list returned is deterministic (it is sorted). + /// Will never throw IO exceptions. If path is invalid, just returns filespec verbatim. + /// + /// The directory to evaluate, escaped. + /// The filespec to evaluate, escaped. + /// Array of file paths, escaped. + internal static string[] GetFileListEscaped + ( + string directoryEscaped, + string filespecEscaped + ) + { + return GetFileList(directoryEscaped, filespecEscaped, true /* returnEscaped */); + } + + /// + /// Used for the purposes of evaluating an item specification. Given a filespec that may include wildcard characters * and + /// ?, we translate it into an actual list of files. If the input filespec doesn't contain any wildcard characters, and it + /// doesn't appear to point to an actual file on disk, then we just give back the input string as an array of length one, + /// assuming that it wasn't really intended to be a filename (as items are not required to necessarily represent files). + /// Any wildcards passed in that are unescaped will be treated as real wildcards. + /// The "include" of items passed back from the filesystem will be returned canonically escaped. + /// The ordering of the list returned is deterministic (it is sorted). + /// Will never throw IO exceptions: if there is no match, returns the input verbatim. + /// + /// The directory to evaluate, escaped. + /// The filespec to evaluate, escaped. + /// Array of file paths. + private static string[] GetFileList + ( + string directoryEscaped, + string filespecEscaped, + bool returnEscaped + ) + { + ErrorUtilities.VerifyThrowInternalLength(filespecEscaped, "filespecEscaped"); + + string[] fileList; + + bool containsEscapedWildcards = EscapingUtilities.ContainsEscapedWildcards(filespecEscaped); + bool containsRealWildcards = FileMatcher.HasWildcards(filespecEscaped); + + if (containsEscapedWildcards && containsRealWildcards) + { + // Umm, this makes no sense. The item's Include has both escaped wildcards and + // real wildcards. What does he want us to do? Go to the file system and find + // files that literally have '*' in their filename? Well, that's not going to + // happen because '*' is an illegal character to have in a filename. + + // Just return the original string. + fileList = new string[] { returnEscaped ? filespecEscaped : EscapingUtilities.UnescapeAll(filespecEscaped) }; + } + else if (!containsEscapedWildcards && containsRealWildcards) + { + // Unescape before handing it to the filesystem. + string directoryUnescaped = EscapingUtilities.UnescapeAll(directoryEscaped); + string filespecUnescaped = EscapingUtilities.UnescapeAll(filespecEscaped); + + // Get the list of actual files which match the filespec. Put + // the list into a string array. If the filespec started out + // as a relative path, we will get back a bunch of relative paths. + // If the filespec started out as an absolute path, we will get + // back a bunch of absolute paths. + fileList = FileMatcher.GetFiles(directoryUnescaped, filespecUnescaped); + + ErrorUtilities.VerifyThrow(fileList != null, "We must have a list of files here, even if it's empty."); + + // Before actually returning the file list, we sort them alphabetically. This + // provides a certain amount of extra determinism and reproducability. That is, + // we're sure that the build will behave in exactly the same way every time, + // and on every machine. + Array.Sort(fileList, StringComparer.OrdinalIgnoreCase); + + if (returnEscaped) + { + // We must now go back and make sure all special characters are escaped because we always + // store data in the engine in escaped form so it doesn't interfere with our parsing. + // Note that this means that characters that were not escaped in the original filespec + // may now be escaped, but that's not easy to avoid. + for (int i = 0; i < fileList.Length; i++) + { + fileList[i] = EscapingUtilities.Escape(fileList[i]); + } + } + } + else + { + // No real wildcards means we just return the original string. Don't even bother + // escaping ... it should already be escaped appropriately since it came directly + // from the project file or the OM host. + fileList = new string[] { returnEscaped ? filespecEscaped : EscapingUtilities.UnescapeAll(filespecEscaped) }; + } + + return fileList; + } + } +} diff --git a/src/XMakeBuildEngine/Utilities/RegistryKeyWrapper.cs b/src/XMakeBuildEngine/Utilities/RegistryKeyWrapper.cs new file mode 100644 index 00000000000..a062fd683c8 --- /dev/null +++ b/src/XMakeBuildEngine/Utilities/RegistryKeyWrapper.cs @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security; + +using Microsoft.Build.Shared; +using Microsoft.Win32; +using RegistryException = Microsoft.Build.Exceptions.RegistryException; + +namespace Microsoft.Build.Internal +{ + /// + /// Thin wrapper around Microsoft.Win32.RegistryKey that can be + /// subclassed for testing purposes + /// + internal class RegistryKeyWrapper : IDisposable + { + // Path to the key this instance wraps + private string _registryKeyPath; + // The key this instance wraps + private RegistryKey _wrappedKey; + // The hive this registry key lives under + private RegistryKey _registryHive; + // This field will be set to true when we try to open the registry key + private bool _attemptedToOpenRegistryKey = false; + + /// + /// Has the object been disposed yet. + /// + private bool _disposed; + + /// + /// Initializes this RegistryKeyWrapper to wrap the specified key. + /// Does not check for a null key. + /// + /// + protected RegistryKeyWrapper(RegistryKey wrappedKey, RegistryKey registryHive) + { + _wrappedKey = wrappedKey; + _registryHive = registryHive; + } + + /// + /// Initializes this RegistryKeyWrapper to wrap the key at the specified path + /// and assumes the key is underneath HKLM + /// Note that registryKeyPath should be relative to HKLM. + /// + internal RegistryKeyWrapper(string registryKeyPath) + : this(registryKeyPath, Registry.LocalMachine) + { + } + + /// + /// Initializes this RegistryKeyWrapper to wrap the key at the specified path + /// + internal RegistryKeyWrapper(string registryKeyPath, RegistryHive registryHive, RegistryView registryView) + : this(registryKeyPath, RegistryKey.OpenBaseKey(registryHive, registryView)) + { + } + + /// + /// Initializes this RegistryKeyWrapper to wrap the key at the specified path + /// + internal RegistryKeyWrapper(string registryKeyPath, RegistryKey registryHive) + { + ErrorUtilities.VerifyThrowArgumentNull(registryKeyPath, "registryKeyPath"); + ErrorUtilities.VerifyThrowArgumentNull(registryHive, "registryHive"); + + _registryKeyPath = registryKeyPath; + _registryHive = registryHive; + } + + /// + /// Name of the registry key + /// + public virtual string Name + { + get + { + try + { + return Exists() ? WrappedKey.Name : string.Empty; + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + throw; + + throw new RegistryException(ex.Message, ex); + } + } + } + + /// + /// Convenient static helper method on RegistryKeyWrapper, for when someone is only intersted in knowing + /// whether a particular registry key exists or not. + /// + public static bool KeyExists(string registryKeyPath, RegistryHive registryHive, RegistryView registryView) + { + using (RegistryKeyWrapper wrapper = new RegistryKeyWrapper(registryKeyPath, registryHive, registryView)) + { + return wrapper.Exists(); + } + } + + /// + /// Gets the value with name "name" stored under this registry key + /// + /// + /// + public virtual object GetValue(string name) + { + try + { + return Exists() ? WrappedKey.GetValue(name) : null; + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + throw; + + throw new RegistryException(ex.Message, Name + "@" + name, ex); + } + } + + /// + /// Gets the names of all values underneath this registry key + /// + /// + public virtual string[] GetValueNames() + { + try + { + return Exists() ? WrappedKey.GetValueNames() : new string[] { }; + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + throw; + + throw new RegistryException(ex.Message, Name, ex); + } + } + + /// + /// Gets the names of all sub keys immediately below this registry key + /// + /// + public virtual string[] GetSubKeyNames() + { + try + { + return Exists() ? WrappedKey.GetSubKeyNames() : new string[] { }; + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + throw; + + throw new RegistryException(ex.Message, Name, ex); + } + } + + /// + /// Returns the RegistryKeyWrapper around the sub key with name "name". If that does + /// not exist, returns a RegistryKeyWrapper around null. + /// + /// + /// + public virtual RegistryKeyWrapper OpenSubKey(string name) + { + ErrorUtilities.VerifyThrowArgumentLength(name, "name"); + + RegistryKeyWrapper wrapper = this; + string[] keyNames = name.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < keyNames.Length && wrapper.Exists(); ++i) + { + try + { + wrapper = new RegistryKeyWrapper(wrapper.WrappedKey.OpenSubKey(keyNames[i], false /* not writeable */), _registryHive); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + throw; + + throw new RegistryException(ex.Message, wrapper.Name + "\\" + keyNames[i], ex); + } + } + + return wrapper; + } + + /// + /// Returns true if the wrapped registry key exists. + /// + /// + public virtual bool Exists() + { + return (null != WrappedKey); + } + + /// + /// Lazy getter for the root tools version registry key: means that this class + /// will never throw registry exceptions from the constructor + /// + private RegistryKey WrappedKey + { + get + { + // If we haven't wrapped a key yet, and we got a path to look at, + // and we haven't tried to look there yet + if (_wrappedKey == null && _registryKeyPath != null && !_attemptedToOpenRegistryKey) + { + try + { + _wrappedKey = _registryHive.OpenSubKey(_registryKeyPath); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedRegistryException(ex)) + throw; + + throw new RegistryException(ex.Message, Name, ex); + } + finally + { + _attemptedToOpenRegistryKey = true; + } + } + + return _wrappedKey; + } + } + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (!_disposed) + { + if (disposing) + { + if (_wrappedKey != null) + { + _wrappedKey.Dispose(); + _wrappedKey = null; + } + + if (_registryHive != null) + { + _registryHive.Dispose(); + _registryHive = null; + } + } + + // Note disposing has been done. + _disposed = true; + } + } + } +} diff --git a/src/XMakeBuildEngine/Utilities/Utilities.cs b/src/XMakeBuildEngine/Utilities/Utilities.cs new file mode 100644 index 00000000000..d17d05266fe --- /dev/null +++ b/src/XMakeBuildEngine/Utilities/Utilities.cs @@ -0,0 +1,583 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Pipes; +using System.Text; +using System.Text.RegularExpressions; +using System.Globalization; +using System.Xml.Serialization; +using System.Security; +using System.Security.Policy; +using System.Security.Permissions; + +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Evaluation; +using Toolset = Microsoft.Build.Evaluation.Toolset; +using XmlElementWithLocation = Microsoft.Build.Construction.XmlElementWithLocation; + +namespace Microsoft.Build.Internal +{ + /// + /// This class contains utility methods for the MSBuild engine. + /// + static internal class Utilities + { + /// + /// Save off the contents of the environment variable that specifies whether we should treat higher toolsversions as the current + /// toolsversion. (Some hosts require this.) + /// + private static bool s_shouldTreatHigherToolsVersionsAsCurrent = (Environment.GetEnvironmentVariable("MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT") != null); + + /// + /// Save off the contents of the environment variable that specifies whether we should treat all toolsversions, regardless of + /// whether they are higher or lower, as the current toolsversion. (Some hosts require this.) + /// + private static bool s_shouldTreatOtherToolsVersionsAsCurrent = (Environment.GetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT") != null); + + /// + /// If set, default to the ToolsVersion from the project file (or if that doesn't isn't set, default to 2.0). Otherwise, use Dev12+ + /// defaulting logic: first check the MSBUILDDEFAULTTOOLSVERSION environment variable, then check for a DefaultOverrideToolsVersion, + /// then if both fail, use the current ToolsVersion. + /// + private static bool s_uselegacyDefaultToolsVersionBehavior = (Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION") != null); + + /// + /// If set, will be used as the ToolsVersion to build with (unless MSBUILDLEGACYDEFAULTTOOLSVERSION is set). + /// + private static string s_defaultToolsVersionFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + + /// + /// Delegate for a method that, given a ToolsVersion string, returns the matching Toolset. + /// + internal delegate Toolset GetToolset(string toolsVersion); + + /// + /// INTERNAL FOR UNIT-TESTING ONLY + /// + /// We've got several environment variables that we read into statics since we don't expect them to ever + /// reasonably change, but we need some way of refreshing their values so that we can modify them for + /// unit testing purposes. + /// + internal static void RefreshInternalEnvironmentValues() + { + s_shouldTreatHigherToolsVersionsAsCurrent = (Environment.GetEnvironmentVariable("MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT") != null); + s_shouldTreatOtherToolsVersionsAsCurrent = (Environment.GetEnvironmentVariable("MSBUILDTREATALLTOOLSVERSIONSASCURRENT") != null); + s_uselegacyDefaultToolsVersionBehavior = (Environment.GetEnvironmentVariable("MSBUILDLEGACYDEFAULTTOOLSVERSION") != null); + s_defaultToolsVersionFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDDEFAULTTOOLSVERSION"); + } + + /// + /// Sets the inner XML/text of the given XML node, escaping as necessary. + /// + /// + /// Can be empty string, but not null. + internal static void SetXmlNodeInnerContents(XmlElementWithLocation node, string s) + { + ErrorUtilities.VerifyThrow(s != null, "Need value to set."); + + if (s.IndexOf('<') != -1) + { + // If the value looks like it probably contains XML markup ... + try + { + // Attempt to store it verbatim as XML. + node.InnerXml = s; + return; + } + catch (XmlException) + { + // But that may fail, in the event that "s" is not really well-formed + // XML. Eat the exception and fall through below ... + } + } + + // The value does not contain valid XML markup. Store it as text, so it gets + // escaped properly. + node.InnerText = s; + } + + /// + /// Extracts the inner XML/text of the given XML node, unescaping as necessary. + /// + /// + /// Inner XML/text of specified node. + internal static string GetXmlNodeInnerContents(XmlElementWithLocation node) + { + // XmlNode.InnerXml gives back a string that consists of the set of characters + // in between the opening and closing elements of the XML node, without doing any + // unescaping. Any "strange" character sequences (like "" will remain + // exactly so and will not be translated or interpreted. The only modification that + // .InnerXml will do is that it will normalize any Xml contained within. This means + // normalizing whitespace between XML attributes and quote characters that surround XML + // attributes. If PreserveWhitespace is false, then it will also normalize whitespace + // between elements. + // + // XmlNode.InnerText strips out any Xml contained within, and then unescapes the rest + // of the text. So if the remaining text contains certain character sequences such as + // "&" or "", these will be translated into their equivalent representations. + // + // It's hard to explain, but much easier to demonstrate with examples: + // + // Original XML XmlNode.InnerText XmlNode.InnerXml + // =========================== ============================== ====================================== + // + //
whatever + // + // 123456 123456 123456 + // + // 123456 123456 123456 + // + // 123456 123456 123456 + // + // 123&456 123&456 123&456 + + // So the trick for MSBuild when interpreting a property value is to know which one to + // use ... InnerXml or InnerText. There are two basic scenarios we care about. + // + // 1.) The first scenario is that the user is trying to create a property whose + // contents are actually XML. That is to say that the contents may be written + // to a XML file, or may be passed in as a string to XmlDocument.LoadXml. + // In this case, we would want to use XmlNode.InnerXml, because we DO NOT want + // character sequences to be unescaped. If we did unescape them, then whatever + // XML parser tried to read in the stream as XML later on would totally barf. + // + // 2.) The second scenario is the the user is trying to create a property that + // is just intended to be treated as a string. That string may be very large + // and could contain all sorts of whitespace, carriage returns, special characters, + // etc. But in the end, it's just a big string. In this case, whatever + // task is actually processing this string ... it's not going to know anything + // about character sequences such as & and <. These character sequences + // are specific to XML markup. So, here we want to use XmlNode.InnerText so that + // the character sequences get unescaped into their actual character before + // the string is passed to the task (or wherever else the property is used). + // Of course, if the string value of the property needs to contain characters + // like <, >, &, etc., then the user must XML escape these characters otherwise + // the XML parser reading the project file will croak. Or if the user doesn't + // want to escape every instance of these characters, he can surround the whole + // thing with a CDATA tag. Again, if he does this, we don't want the task to + // receive the C, D, A, T, A as part of the string ... this should be stripped off. + // Again, using XmlNode.InnerText takes care of this. + // + // 2b.) A variation of the second scenario is that the user is trying to create a property + // that is just intended to be a string, but wants to comment out part of the string. + // For example, it's a semicolon separated list that's going ultimately to end up in a list. + // eg. (DDB #56841) + // + // + // + // ndp\fx\src\VSIP\FrameWork; + // ndp\fx\src\xmlTools; + // ddsuites\src\vs\xmlTools; + // + // + // In this case, we want to treat the string as text, so that we don't retrieve the comment. + // We only want to retrieve the comment if there's some other XML in there. The + // mere presence of an XML comment shouldn't make us think the value is XML. + // + // Given these two scenarios, how do we know whether the user intended to treat + // a property value as XML or text? We use a simple heuristic which is that if + // XmlNode.InnerXml contains any "<" characters, then there pretty much has to be + // XML in there, so we'll just use XmlNode.InnerXml. If there are no "<" characters that aren't merely comments, + // then we assume it's to be treated as text and we use XmlNode.InnerText. Also, if + // it looks like the whole thing is one big CDATA block, then we also use XmlNode.InnerText. + + // XmlNode.InnerXml is much more expensive than InnerText. Don't use it for trivial cases. + // (single child node with a trivial value or no child nodes) + if (!node.HasChildNodes) + { + return string.Empty; + } + + if (node.ChildNodes.Count == 1 && (node.FirstChild.NodeType == XmlNodeType.Text || node.FirstChild.NodeType == XmlNodeType.CDATA)) + { + return node.InnerText; + } + + string innerXml = node.InnerXml; + + // If there is no markup under the XML node (detected by the presence + // of a '<' sign + int firstLessThan = innerXml.IndexOf('<'); + if (firstLessThan == -1) + { + // return the inner text so it gets properly unescaped + return node.InnerText; + } + + bool containsNoTagsOtherThanComments = ContainsNoTagsOtherThanComments(innerXml, firstLessThan); + + // ... or if the only XML is comments, + if (containsNoTagsOtherThanComments) + { + // return the inner text so the comments are stripped + // (this is how one might comment out part of a list in a property value) + return node.InnerText; + } + + // ...or it looks like the whole thing is a big CDATA tag ... + bool startsWithCData = (innerXml.IndexOf(" + /// Figure out whether there are any XML tags, other than comment tags, + /// in the string. + /// + /// + /// We know the string coming in is a valid XML fragment. (The project loaded after all.) + /// So for example we can ignore an open comment tag without a matching closing comment tag. + /// + private static bool ContainsNoTagsOtherThanComments(string innerXml, int firstLessThan) + { + bool insideComment = false; + for (int i = firstLessThan; i < innerXml.Length; i++) + { + if (!insideComment) + { + // XML comments start with exactly "" + if (i < innerXml.Length - 2 + && innerXml[i] == '-' + && innerXml[i + 1] == '-' + && innerXml[i + 2] == '>') + { + // Found the end of a comment + insideComment = false; + i = i + 2; + continue; + } + } + } + + // Didn't find any tags, except possibly comments + return true; + } + + // used to find the xmlns attribute + private static readonly Regex s_xmlnsPattern = new Regex("xmlns=\"[^\"]*\"\\s*"); + + /// + /// Removes the xmlns attribute from an XML string. + /// + /// XML string to process. + /// The modified XML string. + internal static string RemoveXmlNamespace(string xml) + { + return s_xmlnsPattern.Replace(xml, String.Empty); + } + + /// + /// Creates a comma separated list of valid tools versions suitable for an error message. + /// + internal static string CreateToolsVersionListString(IEnumerable toolsets) + { + string toolsVersionList = String.Empty; + foreach (Toolset toolset in toolsets) + { + toolsVersionList += "\"" + toolset.ToolsVersion + "\", "; + } + + // Remove trailing comma and space + if (toolsVersionList.Length > 0) + { + toolsVersionList = toolsVersionList.Substring(0, toolsVersionList.Length - 2); + } + + return toolsVersionList; + } + + /// + /// Figure out what ToolsVersion to use to actually build the project with. + /// + /// The user-specified ToolsVersion (through e.g. /tv: on the command line) + /// The ToolsVersion from the project file + /// Delegate used to test whether a toolset exists for a given ToolsVersion. May be null, in which + /// case we act as though that toolset existed. + /// The default ToolsVersion + /// The ToolsVersion we should use to build this project. Should never be null. + internal static string GenerateToolsVersionToUse(string explicitToolsVersion, string toolsVersionFromProject, GetToolset getToolset, string defaultToolsVersion) + { + string toolsVersionToUse = explicitToolsVersion; + + // hosts may need to treat toolsversions later than the current one as the current one ... or may just + // want to treat all toolsversions as though they're the current one, so give them that ability + // through an environment variable + if (s_shouldTreatOtherToolsVersionsAsCurrent) + { + toolsVersionToUse = MSBuildConstants.CurrentToolsVersion; + } + else + { + if (s_shouldTreatHigherToolsVersionsAsCurrent) + { + Version toolsVersionAsVersion; + + if (Version.TryParse(toolsVersionFromProject, out toolsVersionAsVersion)) + { + // This is higher than the current toolsversion + // Therefore we need to enter best effort mode + // and present the current one. + if (toolsVersionAsVersion > MSBuildConstants.CurrentToolsVersionAsVersion) + { + toolsVersionToUse = MSBuildConstants.CurrentToolsVersion; + } + } + } + + // If ToolsVersion has not either been explicitly set or been overridden via one of the methods + // mentioned above + if (toolsVersionToUse == null) + { + // We want to generate the ToolsVersion based on the legacy behavior if EITHER: + // - the environment variable (MSBUILDLEGACYDEFAULTTOOLSVERSION) is set + // - the current ToolsVersion doesn't actually exist. This is extremely unlikely + // to happen normally, but may happen in checked-in toolset scenarios, in which + // case we want to make sure we're at least as tolerant as Dev11 was. + Toolset currentToolset = null; + + if (getToolset != null) + { + currentToolset = getToolset(MSBuildConstants.CurrentToolsVersion); + } + + // if we want to do the legacy behavior, act as we did through Dev11: + // - If project file defines a ToolsVersion that has a valid toolset associated with it, use that + // - Otherwise, if project file defines an invalid ToolsVersion, use the current ToolsVersion + // - Otherwise, if project file does not define a ToolsVersion, use the default ToolsVersion (must + // be "2.0" since 2.0 projects did not have a ToolsVersion field). + if (s_uselegacyDefaultToolsVersionBehavior || (getToolset != null && currentToolset == null)) + { + if (!String.IsNullOrEmpty(toolsVersionFromProject)) + { + toolsVersionToUse = toolsVersionFromProject; + + // If we can tell that the toolset specified in the project is not present + // then we'll use the current version. Otherwise, we'll assume our caller + // knew what it was doing. + if (getToolset != null && getToolset(toolsVersionToUse) == null) + { + toolsVersionToUse = MSBuildConstants.CurrentToolsVersion; + } + } + else + { + toolsVersionToUse = defaultToolsVersion; + } + } + else + { + // Otherwise, first check to see if the default ToolsVersion has been set in the environment. + // Ideally we'll check to make sure it's a valid ToolsVersion, but if we don't have the ability + // to do so, we'll assume the person who set the environment variable knew what they were doing. + if (!String.IsNullOrEmpty(s_defaultToolsVersionFromEnvironment)) + { + if (getToolset == null || getToolset(s_defaultToolsVersionFromEnvironment) != null) + { + toolsVersionToUse = s_defaultToolsVersionFromEnvironment; + } + } + + // Otherwise, check to see if the override default toolsversion from the toolset works. Though + // it's attached to the Toolset, it's actually MSBuild version dependent, so any loaded Toolset + // should have the same one. + // + // And if that doesn't work, then just fall back to the current ToolsVersion. + if (toolsVersionToUse == null) + { + if (getToolset != null && currentToolset != null) + { + string defaultOverrideToolsVersion = currentToolset.DefaultOverrideToolsVersion; + + if (!String.IsNullOrEmpty(defaultOverrideToolsVersion) && getToolset(defaultOverrideToolsVersion) != null) + { + toolsVersionToUse = defaultOverrideToolsVersion; + } + else + { + toolsVersionToUse = MSBuildConstants.CurrentToolsVersion; + } + } + else + { + toolsVersionToUse = MSBuildConstants.CurrentToolsVersion; + } + } + } + } + } + + ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(toolsVersionToUse), "Should always return a ToolsVersion"); + return toolsVersionToUse; + } + + /// + /// Retrieves properties derived from the current + /// environment variables. + /// + internal static PropertyDictionary GetEnvironmentProperties() + { + IDictionary environmentVariablesBag = CommunicationsUtilities.GetEnvironmentVariables(); + + PropertyDictionary environmentProperties = new PropertyDictionary(environmentVariablesBag.Count + 2); + + // We set the MSBuildExtensionsPath variables here because we don't want to make them official + // reserved properties; we need the ability for people to override our default in their + // environment or as a global property. + + // "MSBuildExtensionsPath32". This points to whatever the value of "Program Files (x86)" environment variable is; + // but on a 32 bit box this isn't set, and we should use "Program Files" instead. + string programFiles32 = FrameworkLocationHelper.programFiles32; + string extensionsPath32 = Path.Combine(programFiles32, ReservedPropertyNames.extensionsPathSuffix); + environmentProperties.Set(ProjectPropertyInstance.Create(ReservedPropertyNames.extensionsPath32, extensionsPath32, true)); + + // "MSBuildExtensionsPath64". This points to whatever the value of "Program Files" environment variable is on a + // 64-bit machine, and is empty on a 32-bit machine. + if (FrameworkLocationHelper.programFiles64 != null) + { + // if ProgramFiles and ProgramFiles(x86) are the same, then this is a 32-bit box, + // so we only want to set MSBuildExtensionsPath64 if they're not + string extensionsPath64 = Path.Combine(FrameworkLocationHelper.programFiles64, ReservedPropertyNames.extensionsPathSuffix); + environmentProperties.Set(ProjectPropertyInstance.Create(ReservedPropertyNames.extensionsPath64, extensionsPath64, true)); + } + + // MSBuildExtensionsPath: The way this used to work is that it would point to "Program Files\MSBuild" on both + // 32-bit and 64-bit machines. We have a switch to continue using that behavior; however the default is now for + // MSBuildExtensionsPath to always point to the same location as MSBuildExtensionsPath32. + + bool useLegacyMSBuildExtensionsPathBehavior = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDLEGACYEXTENSIONSPATH")); + + string programFiles = FrameworkLocationHelper.programFiles; + string extensionsPath; + if (useLegacyMSBuildExtensionsPathBehavior) + { + extensionsPath = Path.Combine(programFiles, ReservedPropertyNames.extensionsPathSuffix); + } + else + { + extensionsPath = extensionsPath32; + } + + environmentProperties.Set(ProjectPropertyInstance.Create(ReservedPropertyNames.extensionsPath, extensionsPath, true)); + + // Windows XP and Windows Server 2003 don't define LocalAppData in their environment. + // We'll set it here if the environment doesn't have it so projects can reliably + // depend on $(LocalAppData). + string localAppData = String.Empty; + ProjectPropertyInstance localAppDataProp = environmentProperties.GetProperty(ReservedPropertyNames.localAppData); + if (localAppDataProp != null) + { + localAppData = localAppDataProp.EvaluatedValue; + } + + if (String.IsNullOrEmpty(localAppData)) + { + localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + } + + if (String.IsNullOrEmpty(localAppData)) + { + localAppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + + environmentProperties.Set(ProjectPropertyInstance.Create(ReservedPropertyNames.localAppData, localAppData)); + + // Add MSBuildUserExtensionsPath at $(LocalAppData)\Microsoft\MSBuild + string userExtensionsPath = Path.Combine(localAppData, ReservedPropertyNames.userExtensionsPathSuffix); + environmentProperties.Set(ProjectPropertyInstance.Create(ReservedPropertyNames.userExtensionsPath, userExtensionsPath)); + + if (environmentVariablesBag != null) + { + foreach (KeyValuePair environmentVariable in environmentVariablesBag) + { + // We're going to just skip environment variables that contain names + // with characters we can't handle. There's no logger registered yet + // when this method is called, so we can't really log anything. + string environmentVariableName = environmentVariable.Key; + + if (XmlUtilities.IsValidElementName(environmentVariableName) && + XMakeElements.IllegalItemPropertyNames[environmentVariableName] == null && + !ReservedPropertyNames.IsReservedProperty(environmentVariableName)) + { + ProjectPropertyInstance environmentProperty = ProjectPropertyInstance.Create(environmentVariableName, environmentVariable.Value); + + environmentProperties.Set(environmentProperty); + } + else + { + // The name was invalid, so we just didn't add the environment variable. + // That's fine, continue for the next one. + } + } + } + + return environmentProperties; + } + + /// + /// Extension to IEnumerable to get the count if it + /// can be quickly gotten, otherwise 0. + /// + public static int FastCountOrZero(this IEnumerable enumerable) + { + ICollection collection = enumerable as ICollection; + + return (collection == null) ? 0 : collection.Count; + } + + /// + /// Extension to IEnumerable of KVP of string, something to just return the somethings. + /// + public static IEnumerable Values(this IEnumerable> source) where T : class, IKeyed + { + foreach (var entry in source) + { + yield return entry.Value; + } + } + } +} diff --git a/src/XMakeBuildEngine/Xml/ProjectXmlUtilities.cs b/src/XMakeBuildEngine/Xml/ProjectXmlUtilities.cs new file mode 100644 index 00000000000..76ba2e5da65 --- /dev/null +++ b/src/XMakeBuildEngine/Xml/ProjectXmlUtilities.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using Microsoft.Build.Shared; +using Microsoft.Build.Construction; + +namespace Microsoft.Build.Internal +{ + /// + /// Project-related Xml utilities + /// + internal class ProjectXmlUtilities + { + /// + /// Gets child elements, ignoring whitespace and comments. + /// Verifies xml namespace of elements is the MSBuild namespace. + /// Throws InvalidProjectFileException for elements in the wrong namespace, and unexpected XML node types + /// + internal static List GetVerifyThrowProjectChildElements(XmlElementWithLocation element) + { + return GetChildElements(element, true /*throw for unexpected node types*/); + } + + /// + /// Gets child elements, ignoring whitespace and comments. + /// Verifies xml namespace of elements is the MSBuild namespace. + /// Throws InvalidProjectFileException for elements in the wrong namespace, and (if parameter is set) unexpected XML node types + /// + private static List GetChildElements(XmlElementWithLocation element, bool throwForInvalidNodeTypes) + { + List children = new List(); + + foreach (XmlNode child in element) + { + switch (child.NodeType) + { + case XmlNodeType.Comment: + case XmlNodeType.Whitespace: + // These are legal, and ignored + break; + + case XmlNodeType.Element: + XmlElementWithLocation childElement = (XmlElementWithLocation)child; + VerifyThrowProjectValidNamespace(childElement); + children.Add(childElement); + break; + + default: + if (throwForInvalidNodeTypes) + { + ThrowProjectInvalidChildElement(child.Name, element.Name, element.Location); + } + break; + } + } + return children; + } + + /// + /// Throw an invalid project exception if there are any child elements at all + /// + internal static void VerifyThrowProjectNoChildElements(XmlElementWithLocation element) + { + List childElements = GetVerifyThrowProjectChildElements(element); + if (childElements.Count > 0) + { + ThrowProjectInvalidChildElement(element.FirstChild.Name, element.Name, element.Location); + } + } + + + /// + /// Throw an invalid project exception indicating that the child is not valid beneath the element because it is a duplicate + /// + internal static void ThrowProjectInvalidChildElementDueToDuplicate(XmlElementWithLocation child) + { + ProjectErrorUtilities.ThrowInvalidProject(child.Location, "InvalidChildElementDueToDuplication", child.Name, child.ParentNode.Name); + } + + /// + /// Throw an invalid project exception indicating that the child is not valid beneath the element + /// + internal static void ThrowProjectInvalidChildElement(string name, string parentName, ElementLocation location) + { + ProjectErrorUtilities.ThrowInvalidProject(location, "UnrecognizedChildElement", name, parentName); + } + + /// + /// Verifies that an element is in the MSBuild namespace, otherwise throws an InvalidProjectFileException. + /// + internal static void VerifyThrowProjectValidNamespace(XmlElementWithLocation element) + { + if (element.Prefix.Length > 0 || + !String.Equals(element.NamespaceURI, XMakeAttributes.defaultXmlNamespace, StringComparison.OrdinalIgnoreCase)) + { + ProjectErrorUtilities.ThrowInvalidProject(element.Location, "CustomNamespaceNotAllowedOnThisChildElement", element.Name, element.ParentNode.Name); + } + } + + /// + /// Verifies that if the attribute is present on the element, its value is not empty + /// + internal static void VerifyThrowProjectAttributeEitherMissingOrNotEmpty(XmlElementWithLocation xmlElement, string attributeName) + { + XmlAttributeWithLocation attribute = xmlElement.GetAttributeWithLocation(attributeName); + + ProjectErrorUtilities.VerifyThrowInvalidProject + ( + attribute == null || attribute.Value.Length > 0, + (attribute == null) ? null : attribute.Location, + "InvalidAttributeValue", + String.Empty, + attributeName, + xmlElement.Name + ); + } + + /// + /// If there are any attributes on the element, throws an InvalidProjectFileException complaining that the attribute is not valid on this element. + /// + internal static void VerifyThrowProjectNoAttributes(XmlElementWithLocation element) + { + if (element.HasAttributes) + { + foreach (XmlAttributeWithLocation attribute in element.Attributes) + { + ThrowProjectInvalidAttribute(attribute); + } + } + } + + /// + /// If the condition is false, throws an InvalidProjectFileException complaining that the attribute is not valid on this element. + /// + internal static void VerifyThrowProjectInvalidAttribute(bool condition, XmlAttributeWithLocation attribute) + { + if (!condition) + { + ThrowProjectInvalidAttribute(attribute); + } + } + + /// + /// Verify that the element has the specified required attribute on it and + /// it has a value other than empty string + /// + internal static void VerifyThrowProjectRequiredAttribute(XmlElementWithLocation element, string attributeName) + { + ProjectErrorUtilities.VerifyThrowInvalidProject(element.GetAttribute(attributeName).Length > 0, element.Location, "MissingRequiredAttribute", attributeName, element.Name); + } + + /// + /// Verify that all attributes on the element are on the list of legal attributes + /// + internal static void VerifyThrowProjectAttributes(XmlElementWithLocation element, string[] validAttributes) + { + foreach (XmlAttributeWithLocation attribute in element.Attributes) + { + bool valid = false; + + for (int i = 0; i < validAttributes.Length; i++) + { + if (String.Equals(attribute.Name, validAttributes[i], StringComparison.Ordinal)) + { + valid = true; + break; + } + } + + ProjectXmlUtilities.VerifyThrowProjectInvalidAttribute(valid, attribute); + } + } + + /// + /// Throws an InvalidProjectFileException complaining that the attribute is not valid on this element. + /// + internal static void ThrowProjectInvalidAttribute(XmlAttributeWithLocation attribute) + { + ProjectErrorUtilities.ThrowInvalidProject(attribute.Location, "UnrecognizedAttribute", attribute.Name, attribute.OwnerElement.Name); + } + + /// + /// Sets the value of an attribute, but if the value to set is null or empty, just + /// removes the attribute. Returns the attribute, or null if it was removed. + /// UNDONE: Make this return a bool if the attribute did not change, so we can avoid dirtying. + /// + internal static XmlAttributeWithLocation SetOrRemoveAttribute(XmlElementWithLocation element, string name, string value) + { + return SetOrRemoveAttribute(element, name, value, false /* remove the attribute if setting to empty string */); + } + + /// + /// Sets the value of an attribute, removing the attribute if the value is null, but still setting it + /// if the value is the empty string. Returns the attribute, or null if it was removed. + /// UNDONE: Make this return a bool if the attribute did not change, so we can avoid dirtying. + /// + internal static XmlAttributeWithLocation SetOrRemoveAttribute(XmlElementWithLocation element, string name, string value, bool allowSettingEmptyAttributes) + { + if (value == null || (!allowSettingEmptyAttributes && value.Length == 0)) + { + // The caller passed in a null or an empty value. So remove the attribute. + element.RemoveAttribute(name); + return null; + } + else + { + // Set the new attribute value + element.SetAttribute(name, value); + XmlAttributeWithLocation attribute = (XmlAttributeWithLocation)element.Attributes[name]; + return attribute; + } + } + + /// + /// Returns the value of the attribute. + /// If the attribute is null, returns an empty string. + /// + internal static string GetAttributeValue(XmlAttributeWithLocation attribute, bool returnNullForNonexistentAttributes) + { + if (attribute == null) + { + return returnNullForNonexistentAttributes ? null : String.Empty; + } + else + { + return attribute.Value; + } + } + + /// + /// Returns the value of the attribute. + /// If the attribute is not present, returns an empty string. + /// + internal static string GetAttributeValue(XmlElementWithLocation element, string attributeName) + { + return GetAttributeValue(element, attributeName, false /* if the attribute is not present, return an empty string */); + } + + /// + /// Returns the value of the attribute. + /// If the attribute is not present, returns either null or an empty string, depending on the value + /// of returnNullForNonexistentAttributes. + /// + internal static string GetAttributeValue(XmlElementWithLocation element, string attributeName, bool returnNullForNonexistentAttributes) + { + XmlAttributeWithLocation attribute = (XmlAttributeWithLocation)element.GetAttributeNode(attributeName); + return GetAttributeValue(attribute, returnNullForNonexistentAttributes); + } + } +} diff --git a/src/XMakeBuildEngine/allmajorprojects/allmajorprojects.proj b/src/XMakeBuildEngine/allmajorprojects/allmajorprojects.proj new file mode 100644 index 00000000000..ff3fd6f7126 --- /dev/null +++ b/src/XMakeBuildEngine/allmajorprojects/allmajorprojects.proj @@ -0,0 +1,25 @@ + + + + + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeBuildEngine/native.rc b/src/XMakeBuildEngine/native.rc new file mode 100644 index 00000000000..402d9dacd4f --- /dev/null +++ b/src/XMakeBuildEngine/native.rc @@ -0,0 +1,5 @@ +// From Dev12 on we want the versioning strings to be the VS ones instead of .NET Framework ones. +#include +#include + +#include diff --git a/src/XMakeBuildEngine/packages.config b/src/XMakeBuildEngine/packages.config new file mode 100644 index 00000000000..efa40deef63 --- /dev/null +++ b/src/XMakeBuildEngine/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/XMakeCommandLine/AssemblyInfo.cs b/src/XMakeCommandLine/AssemblyInfo.cs new file mode 100644 index 00000000000..93dc48f1609 --- /dev/null +++ b/src/XMakeCommandLine/AssemblyInfo.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#if (STANDALONEBUILD) +[assembly: AssemblyVersion(Microsoft.Build.Shared.MSBuildConstants.CurrentAssemblyVersion)] +[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests")] +[assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests")] +#else +[assembly: InternalsVisibleTo("MSBuild.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("MSBuild.Whidbey.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +#endif + +// This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, +// so that we don't run into known security issues with loading libraries from unsafe locations +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/XMakeCommandLine/AssemblyResources.cs b/src/XMakeCommandLine/AssemblyResources.cs new file mode 100644 index 00000000000..b66368debad --- /dev/null +++ b/src/XMakeCommandLine/AssemblyResources.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Resources; +using System.Reflection; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// This class provides access to the assembly's resources. + /// + internal static class AssemblyResources + { + /// + /// Loads the specified resource string, either from the assembly's primary resources, or its shared resources. + /// + /// This method is thread-safe. + /// + /// The resource string, or null if not found. + internal static string GetString(string name) + { + // NOTE: the ResourceManager.GetString() method is thread-safe + string resource = s_resources.GetString(name, CultureInfo.CurrentUICulture); + + if (resource == null) + { + resource = s_sharedResources.GetString(name, CultureInfo.CurrentUICulture); + } + + ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); + + return resource; + } + + // assembly resources + private static readonly ResourceManager s_resources = new ResourceManager("MSBuild.Strings", Assembly.GetExecutingAssembly()); + // shared resources + private static readonly ResourceManager s_sharedResources = new ResourceManager("MSBuild.Strings.shared", Assembly.GetExecutingAssembly()); + } +} diff --git a/src/XMakeCommandLine/CommandLineSwitchException.cs b/src/XMakeCommandLine/CommandLineSwitchException.cs new file mode 100644 index 00000000000..7c602cbff41 --- /dev/null +++ b/src/XMakeCommandLine/CommandLineSwitchException.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Runtime.Serialization; +using System.Security.Permissions; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This exception is used to flag (syntax) errors in command line switches passed to the application. + /// + [Serializable] + internal sealed class CommandLineSwitchException : Exception + { + /// + /// This constructor initializes the exception message. + /// + /// + private CommandLineSwitchException + ( + string message + ) : + base(message) + { + // do nothing + } + + /// + /// This constructor initializes the exception message and saves the command line argument containing the switch error. + /// + /// + /// + private CommandLineSwitchException + ( + string message, + string commandLineArg + ) : + this(message) + { + _commandLineArg = commandLineArg; + } + + /// + /// Serialization constructor + /// + private CommandLineSwitchException + ( + SerializationInfo info, + StreamingContext context + ) : + base(info, context) + + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + _commandLineArg = info.GetString("commandLineArg"); + } + + /// + /// Gets the error message and the invalid switch, or only the error message if no invalid switch is set. + /// + public override string Message + { + get + { + if (_commandLineArg == null) + { + return base.Message; + } + else + { + return base.Message + Environment.NewLine + ResourceUtilities.FormatResourceString("InvalidSwitchIndicator", _commandLineArg); + } + } + } + + /// + /// Gets the invalid switch that caused the exception. + /// + /// Can be null. + internal string CommandLineArg + { + get + { + return _commandLineArg; + } + } + + // the invalid switch causing this exception + private string _commandLineArg; + + /// + /// Serialize the contents of the class. + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("commandLineArg", _commandLineArg, typeof(string)); + } + + /// + /// Throws the exception if the specified condition is not met. + /// + /// + /// + /// + internal static void VerifyThrow(bool condition, string messageResourceName, string commandLineArg) + { + if (!condition) + { + Throw(messageResourceName, commandLineArg); + } +#if DEBUG + else + { + ResourceUtilities.VerifyResourceStringExists(messageResourceName); + } +#endif + } + + /// + /// Throws the exception using the given message and the command line argument containing the switch error. + /// + /// + /// + /// + internal static void Throw(string messageResourceName, string commandLineArg) + { + Throw(messageResourceName, commandLineArg, String.Empty); + } + + /// + /// Throws the exception using the given message and the command line argument containing the switch error. + /// + /// + /// + /// + internal static void Throw(string messageResourceName, string commandLineArg, params string[] messageArgs) + { + string errorMessage = ResourceUtilities.FormatResourceString(messageResourceName, messageArgs); + + ErrorUtilities.VerifyThrow(errorMessage != null, "The resource string must exist."); + + throw new CommandLineSwitchException(errorMessage, commandLineArg); + } + } +} diff --git a/src/XMakeCommandLine/CommandLineSwitches.cs b/src/XMakeCommandLine/CommandLineSwitches.cs new file mode 100644 index 00000000000..338d18cdcf4 --- /dev/null +++ b/src/XMakeCommandLine/CommandLineSwitches.cs @@ -0,0 +1,921 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Win32; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This class encapsulates the switches gathered from the application command line. It helps with switch detection, parameter + /// accumulation, and error generation. + /// + internal sealed class CommandLineSwitches + { + /// + /// Enumeration of all recognized switches that do not take any parameters. + /// + /// + /// WARNING: the values of this enumeration are also used to index/size arrays, and thus the following rules apply: + /// 1) the first valid switch must have a value/index of 0 + /// 2) the value of the last member of the enumeration must indicate the number of valid switches + /// 3) the values of the first and last members of the enumeration are invalid array indices + /// + internal enum ParameterlessSwitch + { + Invalid = -1, + Help = 0, + Version, + NoLogo, + NoAutoResponse, + NoConsoleLogger, + FileLogger, + FileLogger1, + FileLogger2, + FileLogger3, + FileLogger4, + FileLogger5, + FileLogger6, + FileLogger7, + FileLogger8, + FileLogger9, +#if (!STANDALONEBUILD) + OldOM, +#endif + DistributedFileLogger, + Debugger, + DetailedSummary, + NumberOfParameterlessSwitches + } + + /// + /// Enumeration of all recognized switches that take/require parameters. + /// + /// + /// WARNING: the values of this enumeration are also used to index/size arrays, and thus the following rules apply: + /// 1) the first valid switch must have a value/index of 0 + /// 2) the value of the last member of the enumeration must indicate the number of valid switches + /// 3) the values of the first and last members of the enumeration are invalid array indices + /// + internal enum ParameterizedSwitch + { + Invalid = -1, + Project = 0, + Target, + Property, + Logger, + DistributedLogger, + Verbosity, + Validate, + ConsoleLoggerParameters, + NodeMode, + MaxCPUCount, + IgnoreProjectExtensions, + ToolsVersion, + FileLoggerParameters, + FileLoggerParameters1, + FileLoggerParameters2, + FileLoggerParameters3, + FileLoggerParameters4, + FileLoggerParameters5, + FileLoggerParameters6, + FileLoggerParameters7, + FileLoggerParameters8, + FileLoggerParameters9, + NodeReuse, + Preprocess, + NumberOfParameterizedSwitches + } + + /// + /// This struct packages the information required to identify a switch that doesn't take any parameters. It is used when + /// parsing the command line. + /// + private struct ParameterlessSwitchInfo + { + /// + /// Initializes struct data. + /// + /// + /// + /// + internal ParameterlessSwitchInfo + ( + string[] switchNames, + ParameterlessSwitch parameterlessSwitch, + string duplicateSwitchErrorMessage, + string lightUpRegistryKey + + ) + { + this.switchNames = switchNames; + this.duplicateSwitchErrorMessage = duplicateSwitchErrorMessage; + this.parameterlessSwitch = parameterlessSwitch; + this.lightUpKey = lightUpRegistryKey; + this.lightUpKeyRead = false; + this.lightUpKeyResult = false; + } + + // names of the switch (without leading switch indicator) + internal string[] switchNames; + // if null, indicates that switch is allowed to appear multiple times on the command line; otherwise, holds the error + // message to display if switch appears more than once + internal string duplicateSwitchErrorMessage; + // the switch id + internal ParameterlessSwitch parameterlessSwitch; + // The registry key that lights up this switch. + internal string lightUpKey; + // Holds the result of reading the lightUpKey. + internal bool lightUpKeyRead; + // Holds the result of reading the lightUpKey. + internal bool lightUpKeyResult; + } + + /// + /// This struct packages the information required to identify a switch that takes parameters. It is used when parsing the + /// command line. + /// + private struct ParameterizedSwitchInfo + { + /// + /// Initializes struct data. + /// + /// + /// + /// + /// + /// + /// + internal ParameterizedSwitchInfo + ( + string[] switchNames, + ParameterizedSwitch parameterizedSwitch, + string duplicateSwitchErrorMessage, + bool multipleParametersAllowed, + string missingParametersErrorMessage, + bool unquoteParameters + ) + { + this.switchNames = switchNames; + this.duplicateSwitchErrorMessage = duplicateSwitchErrorMessage; + this.multipleParametersAllowed = multipleParametersAllowed; + this.missingParametersErrorMessage = missingParametersErrorMessage; + this.unquoteParameters = unquoteParameters; + this.parameterizedSwitch = parameterizedSwitch; + } + + // names of the switch (without leading switch indicator) + internal string[] switchNames; + // if null, indicates that switch is allowed to appear multiple times on the command line; otherwise, holds the error + // message to display if switch appears more than once + internal string duplicateSwitchErrorMessage; + // indicates if switch can take multiple parameters (equivalent to switch appearing multiple times on command line) + // NOTE: for most switches, if a switch is allowed to appear multiple times on the command line, then multiple + // parameters can be provided per switch; however, some switches cannot take multiple parameters + internal bool multipleParametersAllowed; + // if null, indicates that switch is allowed to have no parameters; otherwise, holds the error message to show if + // switch is found without parameters on the command line + internal string missingParametersErrorMessage; + // indicates if quotes should be removed from the switch parameters + internal bool unquoteParameters; + // the switch id + internal ParameterizedSwitch parameterizedSwitch; + } + + + // map switches that do not take parameters to their identifiers (taken from ParameterlessSwitch enum) + // WARNING: keep this map in the same order as the ParameterlessSwitch enumeration + private static readonly ParameterlessSwitchInfo[] s_parameterlessSwitchesMap = + { + //------------------------------------------------------------------------------------------------------------------------------------------- + // Switch Names Switch Id Dup Error Light up key + //------------------------------------------------------------------------------------------------------------------------------------------- + new ParameterlessSwitchInfo( new string[] { "help", "h", "?" }, ParameterlessSwitch.Help, null, null ), + new ParameterlessSwitchInfo( new string[] { "version", "ver" }, ParameterlessSwitch.Version, null, null ), + new ParameterlessSwitchInfo( new string[] { "nologo" }, ParameterlessSwitch.NoLogo, null, null ), + new ParameterlessSwitchInfo( new string[] { "noautoresponse", "noautorsp" }, ParameterlessSwitch.NoAutoResponse, null, null ), + new ParameterlessSwitchInfo( new string[] { "noconsolelogger", "noconlog" }, ParameterlessSwitch.NoConsoleLogger, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger", "fl" }, ParameterlessSwitch.FileLogger, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger1", "fl1" }, ParameterlessSwitch.FileLogger1, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger2", "fl2" }, ParameterlessSwitch.FileLogger2, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger3", "fl3" }, ParameterlessSwitch.FileLogger3, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger4", "fl4" }, ParameterlessSwitch.FileLogger4, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger5", "fl5" }, ParameterlessSwitch.FileLogger5, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger6", "fl6" }, ParameterlessSwitch.FileLogger6, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger7", "fl7" }, ParameterlessSwitch.FileLogger7, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger8", "fl8" }, ParameterlessSwitch.FileLogger8, null, null ), + new ParameterlessSwitchInfo( new string[] { "filelogger9", "fl9" }, ParameterlessSwitch.FileLogger9, null, null ), +#if (!STANDALONEBUILD) + new ParameterlessSwitchInfo( new string[] { "oldom" }, ParameterlessSwitch.OldOM, null, null ), +#endif + new ParameterlessSwitchInfo( new string[] { "distributedfilelogger", "dfl" }, ParameterlessSwitch.DistributedFileLogger, null, null ), + + new ParameterlessSwitchInfo( new string[] { "debug", "d" }, ParameterlessSwitch.Debugger, null, "DebuggerEnabled"), + new ParameterlessSwitchInfo( new string[] { "detailedsummary", "ds" }, ParameterlessSwitch.DetailedSummary, null , null ) + }; + + // map switches that take parameters to their identifiers (taken from ParameterizedSwitch enum) + // WARNING: keep this map in the same order as the ParameterizedSwitch enumeration + private static readonly ParameterizedSwitchInfo[] s_parameterizedSwitchesMap = + { + //----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + // Switch Names Switch Id Duplicate Switch Error Multi Params? Missing Parameters Error Unquote? + //----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + new ParameterizedSwitchInfo( new string[] { null }, ParameterizedSwitch.Project, "DuplicateProjectSwitchError", false, null, true ), + new ParameterizedSwitchInfo( new string[] { "target", "t"}, ParameterizedSwitch.Target, null, true, "MissingTargetError", true ), + new ParameterizedSwitchInfo( new string[] { "property", "p" }, ParameterizedSwitch.Property, null, true, "MissingPropertyError", true ), + new ParameterizedSwitchInfo( new string[] { "logger", "l" }, ParameterizedSwitch.Logger, null, false, "MissingLoggerError", false ), + new ParameterizedSwitchInfo( new string[] { "distributedlogger", "dl" }, ParameterizedSwitch.DistributedLogger, null, false, "MissingLoggerError", false ), + new ParameterizedSwitchInfo( new string[] { "verbosity", "v" }, ParameterizedSwitch.Verbosity, null, false, "MissingVerbosityError", true ), + new ParameterizedSwitchInfo( new string[] { "validate", "val" }, ParameterizedSwitch.Validate, null, false, null, true ), + new ParameterizedSwitchInfo( new string[] { "consoleloggerparameters", "clp" }, ParameterizedSwitch.ConsoleLoggerParameters, null, false, "MissingConsoleLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "nodemode", "nmode" }, ParameterizedSwitch.NodeMode, null, false, null, false ), + new ParameterizedSwitchInfo( new string[] { "maxcpucount", "m" }, ParameterizedSwitch.MaxCPUCount, null, false, "MissingMaxCPUCountError", true ), + new ParameterizedSwitchInfo( new string[] { "ignoreprojectextensions", "ignore" }, ParameterizedSwitch.IgnoreProjectExtensions, null, true, "MissingIgnoreProjectExtensionsError", true ), + new ParameterizedSwitchInfo( new string[] { "toolsversion","tv" }, ParameterizedSwitch.ToolsVersion, null, false, "MissingToolsVersionError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters", "flp" }, ParameterizedSwitch.FileLoggerParameters, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters1", "flp1" }, ParameterizedSwitch.FileLoggerParameters1, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters2", "flp2" }, ParameterizedSwitch.FileLoggerParameters2, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters3", "flp3" }, ParameterizedSwitch.FileLoggerParameters3, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters4", "flp4" }, ParameterizedSwitch.FileLoggerParameters4, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters5", "flp5" }, ParameterizedSwitch.FileLoggerParameters5, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters6", "flp6" }, ParameterizedSwitch.FileLoggerParameters6, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters7", "flp7" }, ParameterizedSwitch.FileLoggerParameters7, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters8", "flp8" }, ParameterizedSwitch.FileLoggerParameters8, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "fileloggerparameters9", "flp9" }, ParameterizedSwitch.FileLoggerParameters9, null, false, "MissingFileLoggerParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "nodereuse", "nr" }, ParameterizedSwitch.NodeReuse, null, false, "MissingNodeReuseParameterError", true ), + new ParameterizedSwitchInfo( new string[] { "preprocess", "pp" }, ParameterizedSwitch.Preprocess, null, false, null, true ) + }; + + /// + /// Identifies/detects a switch that takes no parameters. + /// + /// + /// [out] switch identifier (from ParameterlessSwitch enumeration) + /// + /// true, if switch is a recognized switch that doesn't take parameters + internal static bool IsParameterlessSwitch + ( + string switchName, + out ParameterlessSwitch parameterlessSwitch, + out string duplicateSwitchErrorMessage + ) + { + parameterlessSwitch = ParameterlessSwitch.Invalid; + duplicateSwitchErrorMessage = null; + + foreach (ParameterlessSwitchInfo switchInfo in s_parameterlessSwitchesMap) + { + if (IsParameterlessSwitchEnabled(switchInfo)) + { + foreach (string parameterlessSwitchName in switchInfo.switchNames) + { + if (String.Compare(switchName, parameterlessSwitchName, StringComparison.OrdinalIgnoreCase) == 0) + { + parameterlessSwitch = switchInfo.parameterlessSwitch; + duplicateSwitchErrorMessage = switchInfo.duplicateSwitchErrorMessage; + break; + } + } + } + } + + return (parameterlessSwitch != ParameterlessSwitch.Invalid); + } + + /// + /// Identifies/detects a switch that takes no parameters. + /// + internal static bool IsParameterlessSwitch + ( + string switchName + ) + { + ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + return CommandLineSwitches.IsParameterlessSwitch(switchName, out parameterlessSwitch, out duplicateSwitchErrorMessage); + } + + /// + /// Identifies/detects a switch that takes parameters. + /// + /// + /// [out] switch identifier (from ParameterizedSwitch enumeration) + /// + /// + /// + /// + /// true, if switch is a recognized switch that takes parameters + internal static bool IsParameterizedSwitch + ( + string switchName, + out ParameterizedSwitch parameterizedSwitch, + out string duplicateSwitchErrorMessage, + out bool multipleParametersAllowed, + out string missingParametersErrorMessage, + out bool unquoteParameters + ) + { + parameterizedSwitch = ParameterizedSwitch.Invalid; + duplicateSwitchErrorMessage = null; + multipleParametersAllowed = false; + missingParametersErrorMessage = null; + unquoteParameters = false; + + foreach (ParameterizedSwitchInfo switchInfo in s_parameterizedSwitchesMap) + { + foreach (string parameterizedSwitchName in switchInfo.switchNames) + { + if (String.Compare(switchName, parameterizedSwitchName, StringComparison.OrdinalIgnoreCase) == 0) + { + parameterizedSwitch = switchInfo.parameterizedSwitch; + duplicateSwitchErrorMessage = switchInfo.duplicateSwitchErrorMessage; + multipleParametersAllowed = switchInfo.multipleParametersAllowed; + missingParametersErrorMessage = switchInfo.missingParametersErrorMessage; + unquoteParameters = switchInfo.unquoteParameters; + break; + } + } + } + + return (parameterizedSwitch != ParameterizedSwitch.Invalid); + } + + /// + /// This struct stores the details of a switch that doesn't take parameters that is detected on the command line. + /// + private struct DetectedParameterlessSwitch + { + // the actual text of the switch + internal string commandLineArg; + } + + /// + /// This struct stores the details of a switch that takes parameters that is detected on the command line. + /// + private struct DetectedParameterizedSwitch + { + // the actual text of the switch + internal string commandLineArg; + + // the parsed switch parameters + internal ArrayList parameters; + } + + // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the + // command line + private DetectedParameterlessSwitch[] _parameterlessSwitches; + // for each recognized switch that takes parameters, this array indicates if the switch has been detected on the command + // line, and it provides a store for the switch parameters + private DetectedParameterizedSwitch[] _parameterizedSwitches; + // NOTE: the above arrays are instance members because this class is not required to be a singleton + + /// + /// Default constructor. + /// + internal CommandLineSwitches() + { +#if DEBUG + Debug.Assert(s_parameterlessSwitchesMap.Length == (int)ParameterlessSwitch.NumberOfParameterlessSwitches, + "The map of parameterless switches must have an entry for each switch in the ParameterlessSwitch enumeration."); + Debug.Assert(s_parameterizedSwitchesMap.Length == (int)ParameterizedSwitch.NumberOfParameterizedSwitches, + "The map of parameterized switches must have an entry for each switch in the ParameterizedSwitch enumeration."); + + for (int i = 0; i < s_parameterlessSwitchesMap.Length; i++) + { + Debug.Assert(i == (int)(s_parameterlessSwitchesMap[i].parameterlessSwitch), + "The map of parameterless switches must be ordered the same way as the ParameterlessSwitch enumeration."); + } + + for (int i = 0; i < s_parameterizedSwitchesMap.Length; i++) + { + Debug.Assert(i == (int)(s_parameterizedSwitchesMap[i].parameterizedSwitch), + "The map of parameterized switches must be ordered the same way as the ParameterizedSwitch enumeration."); + } +#endif + _parameterlessSwitches = new DetectedParameterlessSwitch[(int)ParameterlessSwitch.NumberOfParameterlessSwitches]; + _parameterizedSwitches = new DetectedParameterizedSwitch[(int)ParameterizedSwitch.NumberOfParameterizedSwitches]; + } + + /// + /// Called when a recognized switch that doesn't take parameters is detected on the command line. + /// + /// + internal void SetParameterlessSwitch(ParameterlessSwitch parameterlessSwitch, string commandLineArg) + { + // save the switch text + _parameterlessSwitches[(int)parameterlessSwitch].commandLineArg = commandLineArg; + } + + // list of recognized switch parameter separators -- for switches that take multiple parameters + private static readonly char[] s_parameterSeparators = { ',', ';' }; + + /// + /// Called when a recognized switch that takes parameters is detected on the command line. + /// + /// + /// + /// + /// + /// true, if the given parameters were successfully stored + internal bool SetParameterizedSwitch + ( + ParameterizedSwitch parameterizedSwitch, + string commandLineArg, + string switchParameters, + bool multipleParametersAllowed, + bool unquoteParameters + ) + { + bool parametersStored = false; + + // if this is the first time this switch has been detected + if (_parameterizedSwitches[(int)parameterizedSwitch].commandLineArg == null) + { + // initialize its parameter storage + _parameterizedSwitches[(int)parameterizedSwitch].parameters = new ArrayList(); + } + + // save the switch text + _parameterizedSwitches[(int)parameterizedSwitch].commandLineArg = commandLineArg; + + // check if the switch has multiple parameters + if (multipleParametersAllowed) + { + // store all the switch parameters + int emptyParameters; + _parameterizedSwitches[(int)parameterizedSwitch].parameters.AddRange(QuotingUtilities.SplitUnquoted(switchParameters, int.MaxValue, false /* discard empty parameters */, unquoteParameters, out emptyParameters, s_parameterSeparators)); + + // check if they were all stored successfully i.e. they were all non-empty (after removing quoting, if requested) + parametersStored = (emptyParameters == 0); + } + else + { + if (unquoteParameters) + { + // NOTE: removing quoting from the parameters can reduce the parameters to an empty string + switchParameters = QuotingUtilities.Unquote(switchParameters); + } + + // if the switch actually has parameters, store them + if (switchParameters.Length > 0) + { + _parameterizedSwitches[(int)parameterizedSwitch].parameters.Add(switchParameters); + + parametersStored = true; + } + } + + return parametersStored; + } + + /// + /// Get the equivalent command line, with response files expanded + /// and duplicates removed. Prettified, sorted, parameterless first. + /// Don't include the project file, the caller can put it last. + /// + /// + internal string GetEquivalentCommandLineExceptProjectFile() + { + var commandLineA = new List(); + var commandLineB = new List(); + + for (int i = 0; i < _parameterlessSwitches.Length; i++) + { + if (IsParameterlessSwitchSet((ParameterlessSwitch)i)) + { + commandLineA.Add(GetParameterlessSwitchCommandLineArg((ParameterlessSwitch)i)); + } + } + + for (int i = 0; i < _parameterizedSwitches.Length; i++) + { + if (IsParameterizedSwitchSet((ParameterizedSwitch)i) && ((ParameterizedSwitch)i != ParameterizedSwitch.Project)) + { + commandLineB.Add(GetParameterizedSwitchCommandLineArg((ParameterizedSwitch)i)); + } + } + + commandLineA.Sort(StringComparer.OrdinalIgnoreCase); + commandLineB.Sort(StringComparer.OrdinalIgnoreCase); + + return (String.Join(" ", commandLineA).Trim() + " " + String.Join(" ", commandLineB).Trim()).Trim(); + } + + /// + /// Indicates if the given switch that doesn't take parameters has already been detected on the command line. + /// + /// + /// true, if switch has been seen before + internal bool IsParameterlessSwitchSet(ParameterlessSwitch parameterlessSwitch) + { + return (_parameterlessSwitches[(int)parameterlessSwitch].commandLineArg != null); + } + + /// + /// Gets the on/off state on the command line of the given parameterless switch. + /// + /// + /// This indexer is functionally equivalent to IsParameterlessSwitchSet, but semantically very different. + /// + /// + /// true if on, false if off + internal bool this[ParameterlessSwitch parameterlessSwitch] + { + get + { + return (_parameterlessSwitches[(int)parameterlessSwitch].commandLineArg != null); + } + } + + /// + /// Gets the command line argument (if any) in which the given parameterless switch was detected. + /// + /// + /// The switch text, or null if switch was not detected on the command line. + internal string GetParameterlessSwitchCommandLineArg(ParameterlessSwitch parameterlessSwitch) + { + return _parameterlessSwitches[(int)parameterlessSwitch].commandLineArg; + } + + /// + /// Indicates if the given switch that takes parameters has already been detected on the command line. + /// + /// This method is very light-weight. + /// + /// true, if switch has been seen before + internal bool IsParameterizedSwitchSet(ParameterizedSwitch parameterizedSwitch) + { + return (_parameterizedSwitches[(int)parameterizedSwitch].commandLineArg != null); + } + + // used to indicate a null parameter list for a switch + private static readonly string[] s_noParameters = { }; + + /// + /// Gets the parameters (if any) detected on the command line for the given parameterized switch. + /// + /// + /// WARNING: this indexer is not equivalent to IsParameterizedSwitchSet, and is not light-weight. + /// + /// + /// + /// An array of all the detected parameters for the given switch, or an empty array (NOT null), if the switch has not yet + /// been detected on the command line. + /// + internal string[] this[ParameterizedSwitch parameterizedSwitch] + { + get + { + // if switch has not yet been detected + if (_parameterizedSwitches[(int)parameterizedSwitch].commandLineArg == null) + { + // return an empty parameter list + return s_noParameters; + } + else + { + // return an array of all detected parameters + return (string[])_parameterizedSwitches[(int)parameterizedSwitch].parameters.ToArray(typeof(string)); + } + } + } + + /// + /// Returns an array containing an array of logger parameters for every file logger enabled on the command line. + /// If a logger is enabled but no parameters were supplied, the array entry is an empty array. + /// If a particular logger is not supplied, the array entry is null. + /// + internal string[][] GetFileLoggerParameters() + { + string[][] groupedFileLoggerParameters = new string[10][]; + + groupedFileLoggerParameters[0] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger, ParameterizedSwitch.FileLoggerParameters); + groupedFileLoggerParameters[1] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger1, ParameterizedSwitch.FileLoggerParameters1); + groupedFileLoggerParameters[2] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger2, ParameterizedSwitch.FileLoggerParameters2); + groupedFileLoggerParameters[3] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger3, ParameterizedSwitch.FileLoggerParameters3); + groupedFileLoggerParameters[4] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger4, ParameterizedSwitch.FileLoggerParameters4); + groupedFileLoggerParameters[5] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger5, ParameterizedSwitch.FileLoggerParameters5); + groupedFileLoggerParameters[6] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger6, ParameterizedSwitch.FileLoggerParameters6); + groupedFileLoggerParameters[7] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger7, ParameterizedSwitch.FileLoggerParameters7); + groupedFileLoggerParameters[8] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger8, ParameterizedSwitch.FileLoggerParameters8); + groupedFileLoggerParameters[9] = GetSpecificFileLoggerParameters(ParameterlessSwitch.FileLogger9, ParameterizedSwitch.FileLoggerParameters9); + + return groupedFileLoggerParameters; + } + + /// + /// If the specified parameterized switch is set, returns the array of parameters. + /// Otherwise, if the specified parameterless switch is set, returns an empty array. + /// Otherwise returns null. + /// This allows for example "/flp:foo=bar" to imply "/fl". + /// + private string[] GetSpecificFileLoggerParameters(ParameterlessSwitch parameterlessSwitch, ParameterizedSwitch parameterizedSwitch) + { + string[] result = null; + + if (IsParameterizedSwitchSet(parameterizedSwitch)) + { + result = this[parameterizedSwitch]; + } + else if (IsParameterlessSwitchSet(parameterlessSwitch)) + { + result = new string[] { }; + } + + return result; + } + + /// + /// Gets the command line argument (if any) in which the given parameterized switch was detected. + /// + /// + /// The switch text, or null if switch was not detected on the command line. + internal string GetParameterizedSwitchCommandLineArg(ParameterizedSwitch parameterizedSwitch) + { + return _parameterizedSwitches[(int)parameterizedSwitch].commandLineArg; + } + + /// + /// Determines whether any switches have been set in this bag. + /// + /// Returns true if any switches are set, otherwise false. + internal bool HaveAnySwitchesBeenSet() + { + for (int i = 0; i < (int)ParameterlessSwitch.NumberOfParameterlessSwitches; i++) + { + if (IsParameterlessSwitchSet((ParameterlessSwitch)i)) + { + return true; + } + } + + for (int j = 0; j < (int)ParameterizedSwitch.NumberOfParameterizedSwitches; j++) + { + if (IsParameterizedSwitchSet((ParameterizedSwitch)j)) + { + return true; + } + } + + return false; + } + + /// + /// Called to flag an error when an unrecognized switch is detected on the command line. + /// + /// + internal void SetUnknownSwitchError(string badCommandLineArgValue) + { + SetSwitchError("UnknownSwitchError", badCommandLineArgValue); + } + + /// + /// Called to flag an error when a switch that doesn't take parameters is found with parameters on the command line. + /// + /// + internal void SetUnexpectedParametersError(string badCommandLineArgValue) + { + SetSwitchError("UnexpectedParametersError", badCommandLineArgValue); + } + + // information about last flagged error + // NOTE: these instance members are not initialized unless an error is found + private string _errorMessage; + private string _badCommandLineArg; + private Exception _innerException; + private bool _isParameterError; + + /// + /// Used to flag/store switch errors. + /// + /// + /// + internal void SetSwitchError(string messageResourceNameValue, string badCommandLineArgValue) + { + SetError(messageResourceNameValue, badCommandLineArgValue, null, false); + } + + /// + /// Used to flag/store parameter errors. + /// + /// + /// + internal void SetParameterError(string messageResourceNameValue, string badCommandLineArgValue) + { + SetParameterError(messageResourceNameValue, badCommandLineArgValue, null); + } + + /// + /// Used to flag/store parameter errors. + /// + /// + /// + /// + internal void SetParameterError(string messageResourceNameValue, string badCommandLineArgValue, Exception innerExceptionValue) + { + SetError(messageResourceNameValue, badCommandLineArgValue, innerExceptionValue, true); + } + + /// + /// Used to flag/store switch and/or parameter errors. + /// + /// + /// + /// + /// + private void SetError(string messageResourceNameValue, string badCommandLineArgValue, Exception innerExceptionValue, bool isParameterErrorValue) + { + if (!HaveErrors()) + { + _errorMessage = messageResourceNameValue; + _badCommandLineArg = badCommandLineArgValue; + _innerException = innerExceptionValue; + _isParameterError = isParameterErrorValue; + } + } + + /// + /// Indicates if any errors were found while parsing the command-line. + /// + /// true, if any errors were found + internal bool HaveErrors() + { + return (_errorMessage != null); + } + + /// + /// Throws an exception if any errors were found while parsing the command-line. + /// + internal void ThrowErrors() + { + if (HaveErrors()) + { + if (_isParameterError) + { + InitializationException.Throw(_errorMessage, _badCommandLineArg, _innerException, false); + } + else + { + CommandLineSwitchException.Throw(_errorMessage, _badCommandLineArg); + } + } + } + + /// + /// Appends the given collection of command-line switches to this one. + /// + /// + /// Command-line switches have left-to-right precedence i.e. switches on the right override switches on the left. As a + /// result, this "append" operation is also performed in a left-to-right manner -- the switches being appended to are + /// considered to be on the "left", and the switches being appended are on the "right". + /// + /// + internal void Append(CommandLineSwitches switchesToAppend) + { + // if this collection doesn't already have an error registered, but the collection being appended does + if (!HaveErrors() && switchesToAppend.HaveErrors()) + { + // register the error from the given collection + // NOTE: we always store the first error found (parsing left-to-right), and since this collection is considered to + // be on the "left" of the collection being appended, the error flagged in this collection takes priority over the + // error in the collection being appended + _errorMessage = switchesToAppend._errorMessage; + _badCommandLineArg = switchesToAppend._badCommandLineArg; + _innerException = switchesToAppend._innerException; + _isParameterError = switchesToAppend._isParameterError; + } + + // NOTE: we might run into some duplicate switch errors below, but if we've already registered the error from the + // collection being appended, all the duplicate switch errors will be ignored; this is fine because we really have no + // way of telling which error would occur first in the left-to-right order without keeping track of a lot more error + // information -- so we play it safe, and register the guaranteed error + + // append the parameterless switches with left-to-right precedence, flagging duplicate switches as necessary + for (int i = 0; i < (int)ParameterlessSwitch.NumberOfParameterlessSwitches; i++) + { + if (switchesToAppend.IsParameterlessSwitchSet((ParameterlessSwitch)i)) + { + if (!IsParameterlessSwitchSet((ParameterlessSwitch)i) || + (s_parameterlessSwitchesMap[i].duplicateSwitchErrorMessage == null)) + { + _parameterlessSwitches[i].commandLineArg = switchesToAppend._parameterlessSwitches[i].commandLineArg; + } + else + { + SetSwitchError(s_parameterlessSwitchesMap[i].duplicateSwitchErrorMessage, + switchesToAppend.GetParameterlessSwitchCommandLineArg((ParameterlessSwitch)i)); + } + } + } + + // append the parameterized switches with left-to-right precedence, flagging duplicate switches as necessary + for (int j = 0; j < (int)ParameterizedSwitch.NumberOfParameterizedSwitches; j++) + { + if (switchesToAppend.IsParameterizedSwitchSet((ParameterizedSwitch)j)) + { + if (!IsParameterizedSwitchSet((ParameterizedSwitch)j) || + (s_parameterizedSwitchesMap[j].duplicateSwitchErrorMessage == null)) + { + if (_parameterizedSwitches[j].commandLineArg == null) + { + _parameterizedSwitches[j].parameters = new ArrayList(); + } + + _parameterizedSwitches[j].commandLineArg = switchesToAppend._parameterizedSwitches[j].commandLineArg; + _parameterizedSwitches[j].parameters.AddRange(switchesToAppend._parameterizedSwitches[j].parameters); + } + else + { + SetSwitchError(s_parameterizedSwitchesMap[j].duplicateSwitchErrorMessage, + switchesToAppend.GetParameterizedSwitchCommandLineArg((ParameterizedSwitch)j)); + } + } + } + } + + #region Flag Lightup Support + /// + /// Read a lightup key from either HKLM or HKCU depending on what is passed in root. + /// The key may either be a string, in which case it needs to be like "true" or "false" + /// or it may be a DWORD in which case it should be 0 or !=0. + /// + private static bool? ReadLightupBool(string root, string valueName) + { + try + { + string key = String.Format(CultureInfo.InvariantCulture, @"{0}\software\microsoft\msbuild\{1}", root, MSBuildConstants.CurrentProductVersion); + object value = Registry.GetValue(key, valueName, null); + if (value != null) + { + switch (System.Type.GetTypeCode(value.GetType())) + { + case TypeCode.Int32: + return ((int)value) != 0; + case TypeCode.String: + bool result; + if (bool.TryParse((string)value, out result)) + { + return result; + } + return null; + default: + // Recover by assuming the flag is not set. + Debug.Assert(false, "Could not read debugger enabled flag. Key was found but it had type {0}" + value.GetType().ToString()); + return null; + } + } + return null; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Recover by assuming the flag is not set. + Debug.Assert(false, "Could not read debugger enabled flag. {0}" + e.ToString()); + return null; + } + } + + /// + /// Try to read a lightup key first from HKCU and then HKLM. + /// + private static bool ReadLightupBool(string valueName) + { + bool? result = ReadLightupBool("hkey_current_user", valueName) ?? ReadLightupBool("hkey_local_machine", valueName); + if (result.HasValue) + { + return result.Value; + } + return false; + } + + /// + /// Returns true if the switch is enabled. Handles lightup logic. + /// + private static bool IsParameterlessSwitchEnabled(ParameterlessSwitchInfo parameterlessSwitch) + { + if (parameterlessSwitch.lightUpKey == null) + { + return true; + } + if (parameterlessSwitch.lightUpKeyRead) + { + return parameterlessSwitch.lightUpKeyResult; + } + // Need to read the registry + parameterlessSwitch.lightUpKeyRead = true; + parameterlessSwitch.lightUpKeyResult = ReadLightupBool(parameterlessSwitch.lightUpKey); + return parameterlessSwitch.lightUpKeyResult; + } + #endregion + } +} diff --git a/src/XMakeCommandLine/DistributedLoggerRecord.cs b/src/XMakeCommandLine/DistributedLoggerRecord.cs new file mode 100644 index 00000000000..dd2d0dada2e --- /dev/null +++ b/src/XMakeCommandLine/DistributedLoggerRecord.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Logging; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This class is a container class used to pass around information about distributed logger + /// + internal class DistributedLoggerRecord + { + #region Constructors + /// + /// Initialize the container class with the given centralLogger and forwardingLoggerDescription + /// + internal DistributedLoggerRecord(ILogger centralLogger, LoggerDescription forwardingLoggerDescription) + { + _centralLogger = centralLogger; + _forwardingLoggerDescription = forwardingLoggerDescription; + } + #endregion + + #region Properties + /// + /// Fully initialized central logger + /// + internal ILogger CentralLogger + { + get + { + return _centralLogger; + } + } + + /// + /// Description of the forwarding class + /// + internal LoggerDescription ForwardingLoggerDescription + { + get + { + return _forwardingLoggerDescription; + } + } + #endregion + + #region Data + // Central logger + private ILogger _centralLogger; + // Description of the forwarding logger + private LoggerDescription _forwardingLoggerDescription; + #endregion + } +} diff --git a/src/XMakeCommandLine/FxCopExclusions/MsBuild.Suppressions.cs b/src/XMakeCommandLine/FxCopExclusions/MsBuild.Suppressions.cs new file mode 100644 index 00000000000..c633486272d --- /dev/null +++ b/src/XMakeCommandLine/FxCopExclusions/MsBuild.Suppressions.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// FxCop Suppression file +// To Use: +// Add module level suppressions to this file to have them suppressed in the assembly +// + +using System.Diagnostics.CodeAnalysis; + +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="wrn", Scope="resource", Target="MSBuild.Strings.resources", Justification="These are correct for the help text, they are file paths extensions and switches")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="warningsonly", Scope="resource", Target="MSBuild.Strings.resources", Justification="These are correct for the help text, they are file paths extensions and switches")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="logfile", Scope="resource", Target="MSBuild.Strings.resources", Justification="These are correct for the help text, they are file paths extensions and switches")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="filelogger", Scope="resource", Target="MSBuild.Strings.resources", Justification="These are correct for the help text, they are file paths extensions and switches")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="errorsonly", Scope="resource", Target="MSBuild.Strings.resources", Justification="These are correct for the help text, they are file paths extensions and switches")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="nologo", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="nostic", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="consoleloggerparameters", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="val", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="maxcpucount", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="ormal", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="ignoreprojectextensions", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="noconsolelogger", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="noautoresponse", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="nodeid", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="fileloggerparameters", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="noautorsp", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="nodereuse", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="diag", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="noconlog", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="etailed", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="flp", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="uiet", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="clp", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="distributedlogger", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="ver", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="toolsversion", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="inimal", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="MSBuild.Strings.resources", MessageId="rsp", Justification="These are spelled correctly for the help text")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="dsp", Scope="resource", Target="MSBuild.Strings.resources", Justification=".dsp is a file extension, and is spelled correctly")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="inlining", Scope="resource", Target="MSBuild.Strings.resources", Justification="A valid form of the verb 'to inline', and perfectly understandable to the user even if it may not technically be in the dictionary.")] +[module: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification="Our assemblies are delay signed.")] +[module: SuppressMessage("Microsoft.MSInternal", "CA905:SystemAndMicrosoftNamespacesRequireApproval", Scope="namespace", Target="Microsoft.Build.CommandLine", Justification="This is an approved namespace.")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="detailedsummary", Scope="resource", Target="MSBuild.Strings.resources")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="metaproj", Scope="resource", Target="MSBuild.Strings.resources")] + +#endif + diff --git a/src/XMakeCommandLine/InitializationException.cs b/src/XMakeCommandLine/InitializationException.cs new file mode 100644 index 00000000000..0c723268bda --- /dev/null +++ b/src/XMakeCommandLine/InitializationException.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Globalization; +using System.Runtime.Serialization; +using System.Security.Permissions; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This exception is used to flag failures in application initialization, either due to invalid parameters on the command + /// line, or because the application was invoked in an invalid context. + /// + /// + /// Unlike the CommandLineSwitchException, this exception is NOT thrown for syntax errors in switches. + /// + [Serializable] + internal sealed class InitializationException : Exception + { + /// + /// Private default constructor prevents parameterless instantiation. + /// + private InitializationException() + { + // do nothing + } + + /// + /// This constructor initializes the exception message. + /// + /// + private InitializationException + ( + string message + ) : + base(message) + { + // do nothing + } + + /// + /// This constructor initializes the exception message and saves the switch that caused the initialization failure. + /// + /// + /// Can be null. + private InitializationException + ( + string message, + string invalidSwitch + ) : + this(message) + { + _invalidSwitch = invalidSwitch; + } + + /// + /// Serialization constructor + /// + private InitializationException + ( + SerializationInfo info, + StreamingContext context + ) : + base(info, context) + + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + _invalidSwitch = info.GetString("invalidSwitch"); + } + + /// + /// Gets the error message and the invalid switch, or only the error message if no invalid switch is set. + /// + public override string Message + { + get + { + if (_invalidSwitch == null) + { + return base.Message; + } + else + { + return base.Message + Environment.NewLine + ResourceUtilities.FormatResourceString("InvalidSwitchIndicator", _invalidSwitch); + } + } + } + + // the invalid switch causing this exception (can be null) + private string _invalidSwitch; + + /// + /// Serialize the contents of the class. + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue("invalidSwitch", _invalidSwitch, typeof(string)); + } + + /// + /// Throws the exception if the specified condition is not met. + /// + /// + /// + internal static void VerifyThrow(bool condition, string messageResourceName) + { + VerifyThrow(condition, messageResourceName, null); + } + + /// + /// Throws the exception if the specified condition is not met. + /// + /// + /// + /// + internal static void VerifyThrow(bool condition, string messageResourceName, string invalidSwitch) + { + if (!condition) + { + Throw(messageResourceName, invalidSwitch, null, false); + } +#if DEBUG + else + { + ResourceUtilities.VerifyResourceStringExists(messageResourceName); + } +#endif + } + + /// + /// Throws the exception using the given exception context. + /// + /// + /// + /// + /// + internal static void Throw(string messageResourceName, string invalidSwitch, Exception e, bool showStackTrace) + { + string errorMessage = AssemblyResources.GetString(messageResourceName); + + ErrorUtilities.VerifyThrow(errorMessage != null, "The resource string must exist."); + + if (showStackTrace) + { + errorMessage += Environment.NewLine + e.ToString(); + } + else + { + // the exception message can contain a format item i.e. "{0}" to hold the given exception's message + errorMessage = ResourceUtilities.FormatString(errorMessage, ((e == null) ? String.Empty : e.Message)); + } + + InitializationException.Throw(errorMessage, invalidSwitch); + } + + /// + /// Throws the exception using the given exception context. + /// + /// + /// + /// + /// + internal static void Throw(string message, string invalidSwitch) + { + ErrorUtilities.VerifyThrow(message != null, "The string must exist."); + throw new InitializationException(message, invalidSwitch); + } + } +} diff --git a/src/XMakeCommandLine/LogMessagePacket.cs b/src/XMakeCommandLine/LogMessagePacket.cs new file mode 100644 index 00000000000..f13f7c4858e --- /dev/null +++ b/src/XMakeCommandLine/LogMessagePacket.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// NodePackets which are used for node communication +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using TaskEngineAssemblyResolver = Microsoft.Build.BackEnd.Logging.TaskEngineAssemblyResolver; + +namespace Microsoft.Build.CommandLine +{ + /// + /// A packet to encapsulate a BuildEventArg logging message. + /// Contents: + /// Build Event Type + /// Build Event Args + /// + internal class LogMessagePacket : LogMessagePacketBase + { + /// + /// Encapsulates the buildEventArg in this packet. + /// + internal LogMessagePacket(KeyValuePair? nodeBuildEvent) + : base(nodeBuildEvent, null) + { + } + + /// + /// Constructor for deserialization + /// + private LogMessagePacket(INodePacketTranslator translator) + : base(translator) + { + Translate(translator); + } + + /// + /// Factory for serialization + /// + static internal INodePacket FactoryForDeserialization(INodePacketTranslator translator) + { + return new LogMessagePacket(translator); + } + } +} \ No newline at end of file diff --git a/src/XMakeCommandLine/MSBuild.csproj b/src/XMakeCommandLine/MSBuild.csproj new file mode 100644 index 00000000000..2cc15c50d28 --- /dev/null +++ b/src/XMakeCommandLine/MSBuild.csproj @@ -0,0 +1,202 @@ + + + + + Debug + AnyCPU + {23C9FD0E-70C5-4F1F-B08A-D2774240FB51} + Exe + Properties + Microsoft.Build.CommandLine + MSBuild + true + + + + + + x86 + + + x86 + + + x64 + + + x64 + + + MSBuild.exe.manifest + + + + true + + + AssemblyNameComparer.cs + + + + true + + + FileDelegates.cs + true + + + FileUtilities.cs + true + + + FileUtilitiesRegex.cs + true + + + true + + + RegisteredTaskObjectCacheBase.cs + + + true + + + true + + + true + + + true + + + + + + + + true + + + + + true + + + TempFileUtilities.cs + + + true + + + + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + true + + + + + + true + + + + + + + MSBuild.Strings.resources + Designer + + + MSBuild.Strings.shared.resources + Designer + + + + + + + + + + + {16CD7635-7CF4-4C62-A77B-CF87D0F09A58} + Microsoft.Build + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + Designer + + + + + \ No newline at end of file diff --git a/src/XMakeCommandLine/MSBuild.exe.manifest b/src/XMakeCommandLine/MSBuild.exe.manifest new file mode 100644 index 00000000000..849e8f3d394 --- /dev/null +++ b/src/XMakeCommandLine/MSBuild.exe.manifest @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/MSBuild.ico b/src/XMakeCommandLine/MSBuild.ico new file mode 100644 index 0000000000000000000000000000000000000000..69d0069cb741f01cefc78988655ac27ea9941eec GIT binary patch literal 10134 zcmeHNdsLK1mM=CtD{#(`an9zP&DnE|YsQ(3Ig_)H9Nd}gj!BG(#z%aB5fbSn=AqFw z5u(Ni@-U*JQR6Epv9D3A3T@0S%+a;5rYlTgIpphF zXjA+4?K^0`u7x!{^2j3{{CuhX@AIjzyA{YyFK; zdc4lH{HQolpz|NF)3M*~Ln=OIC^z%P{*1 z@QZ&Q@Vxl`YGLbDDJ9Oi1$33v-aaP6`EUkO@%eJ56j)v)SfX(nnZvG zZs!*IV}t#=3Gg+Oxe;=pXR^bxViPD_7gMFDPit{qA|dL*>3H*?$Odf3rsL zBTdYTPZg_cmv^%!ZwAV8>YqzQunp)8~cg8-qDND~GDLS>L93<88IB25?s2vtj(FbEI| zqzQunL4mX)Nf`tP^%!ZwAV8>YqzQunp)8~cg8-qDND~GDLS>L93<88IB25?s2vtj( zFbEJFM0-dRAQUa3b@~J5>M_!UL4Z)*ND~GDLRm-?1_44PktPfRgvuaI7z7AaM4B)N z5UQ3mVGtk`ND~GDLMb=xanlkv{o|%RqzQunp}LVK3<89*kR}WQgi0b!7z7BFL7FfK z5UPkYVGtlxEos6aKq!zV3<6T86|{_YfOfD>fKc5?69xf7Sx6HG0YW8_CJX|E${8N$1a>>7tXQ^=Si9{2oS0pY2sQd)Y?_6^d6=J{iPvN(}`7*P^Ouf81t78 zt4z`IjgCn)nG!QI%Wd~enVIa6n0Zg0ZY*Vo#0}r&M}|j)H-|U>fghVq_+&>$goi&B z{)c913$X^fgF}KtLPE&5#b|!2#~sWTet&68^;jhZyIB{03iGr)-DGuhOo+!C#!sj< zB%B<-{E$l0(iR-yNUPMHDIwx7<2{vrsqS!l2t1*l(7aHqyPX~{@9)+VPmLBu=?vF!RE{~H`twq(_(qn(zR22cxh_rz$J;m94QB+`NODOL&fpxW+YRz$2mZAb zsyqBx{#Qm9|HSt>9zH=5|M$Mn{d`aP`JTe3w}tnLB;GTKpYJKWTUdCvApT$9Q+P3v zYe_UOCehKncdR{lu>7FSR&HZf8e_(=CMh@H^jE%wZaz1$7X~rn)e8)McP_; z_QYyFX~a5Hna62b*PLk~si|>tZ*YX9rlr2EPh@=$;bSsXQ_XyC#F#BMXIN-6PkYJj zyoWd*4&wvJ_3*EmJ0|eC?R198iSK+^_vdrmu(+Hp9_O=$)4~1`EoqSvEh#Axu`wwT zoRB~HPjRVB?JgugWV5NDN{nmbXmSRZqQoNSDj97x#rYyl=LE;yP7EE4@s7Z@3XfiQr zAqGxbfPoViV8Dd=8vQ@{92(>1;ni_-(N8dT4*Dg0hQ1oJ(QERGiauk~;{&$5F>OUW z^`qy?O7zISi0(VR=&{R-mkYe;RbZkw%Ralk=)2oQzda^iEhO$P_jonkmwo!MPj8|Z z$Gp7DL=VamdC%|gqT6;aUf6UIFRVTW(bqSAcAD5QWbUSlm%loO9(mVw|DM$OipXly zF`&?kfz%mQWe*;ca|k z>4ULv^g_ar9{6zZ3m7%%&-kGKvl!m@k9egzK&Ys4Lx$(NWLo_>r}h*pj#v{qM}n;@X)QLLZBNA4Fb)-9&l*Mznt0gxiKN@I9JsOT3#)8SCI7crm+&AKf zA*o+#{UerSV+3`NE;o^2doXzmj+>}Hw3km5CezgO_R$WRzqk3iF2x@gPq?sh#&|tP znXA|kJ8h-bKXO^l9o-YSo)02#`AHL}Dh~Sf2XdbYpBeB+v-U@0?J=yMKUM3Ixyo9< zF+D@;AGLflB#uS*6sPZjm=bF>~=V~Mjko_iAi zqgUqPlfx#GoF+c4G9O5uamI&p$6aW>aTTpsFY1_UxlpU)zPZMw_uh@8WoW9}kNulg z;S53Y_U%Mv;Z{`a+Kl2x zpzxv(yDtJe&I8-(eb{`?hmBm9%vv8ZPW!Nw@fVp(>FdWIbrJc%A(CO_pOyRdLH#t2jKXm zJ}4Re0#^0!f-pmR04ph>HL&xvkf9<|1tYRE2;ru?Q9$D+;N*_kA+o$zU z+2&+BeNX=C8dLjh>(L81Fs>h3mW{;C6(6Gh(?O^l*Aqn_bi;~%PoSd8g;UKwoNNY; zf6HHi&o!c~;0gXK^3OS(mWc;n!by67$>ky~Mus-@H5%SJ8ritZXh)VC{xY)bBr6w0_P%mHI!ocimwhj1o185b88gsyz>1vlL*Ztui4R2QX+mP4nz|(h9o!e?SD^b} zDX}7JY@D8l$kTg}{+-YHr!)RV|JeLve*OE;>a#)O^UKun2*kgfJG*)A*fy`hg+n{B zb$%-2+^21j`HIiCq>tCM>_<5d^!dR4XL;NS-K;) zu9fGW8>Q7}kXuwj`8*@uei5Y?Z=<-r8O7&3C?pDqjJJDW*ZJEhava6}>T_7PyA*3r z-@;PLSz6tys6z$*F_fyizKjGi@IWPS`1bD0o7gkPSbbtTv`yGFD>H{atf12-% zrTxdxL%)yPep85_jXq;$;gyfl(d)yR=rwukvD3cPI_}jkmhU5*7S8~@ zChy98hxzsz(&OKxU0(g$iRgcQ+6un6@@%+AKY4d4$XY7y)XCS{iZ5x8{08#s__C;InT-21nf*8OpO#^=-|-qQw5n0HCWf4pSl;bh)j&y48`$2(79 z`;aHFqR(%T-u>TD%{#!Egg?RY-oIme?328s{7$#aJ&XQ3p~o-Yuvz>pw#j^tjtJL! zPjIX}%cZpQ`E_*UGuXgqhV)C}J&C`cmCE>pg;~qx+?IRf+I=q6%pa+3Kf-6vu8hz1 zb4cbQ?ZTI5t?2I?pT1p>|2%Wn>~{xcCoVN=^YeEsub4F=Tj)s(*It^qI)CPvWt*1^ zU6DQ_d+6l3m)==u)$}VL&e$m9?@l}?CpAp${&b*?bXlN&cYB~MP_mz1%fE{b;$Opq h%)79e^&7-x;v!K`oFPsV)x;5EFV5FSp`gHr{{h^UQTzY^ literal 0 HcmV?d00001 diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/AssemblyResources.cs b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/AssemblyResources.cs new file mode 100644 index 00000000000..1424df9a1fd --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/AssemblyResources.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Resource manager class for MSBuildTaskHost.exe. +//----------------------------------------------------------------------- +using System.Resources; +using System.Reflection; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// This class provides access to the assembly's resources. + /// + internal static class AssemblyResources + { + /// + /// Actual source of the resource string we'll be reading. + /// + private static readonly ResourceManager resources = new ResourceManager("MSBuildTaskHost.Strings.Shared", Assembly.GetExecutingAssembly()); + + /// + /// Loads the specified resource string, either from the assembly's primary resources, or its shared resources. + /// + /// This method is thread-safe. + /// The resource string, or null if not found. + internal static string GetString(string name) + { + // NOTE: the ResourceManager.GetString() method is thread-safe + string resource = resources.GetString(name, CultureInfo.CurrentUICulture); + + ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); + + return resource; + } + } +} diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/FxCopExclusions/MSBuildTaskHost.Suppressions.cs b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/FxCopExclusions/MSBuildTaskHost.Suppressions.cs new file mode 100644 index 00000000000..f95415ffabb --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/FxCopExclusions/MSBuildTaskHost.Suppressions.cs @@ -0,0 +1,190 @@ +// FxCop Suppression file +// To Use: +// Add module level suppressions to this file to have them suppressed in the assembly +// +using System.Diagnostics.CodeAnalysis; +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostConfiguration.#.ctor(System.Int32,System.String,System.Collections.Generic.IDictionary`2,System.Globalization.CultureInfo,System.Globalization.CultureInfo,System.AppDomainSetup,System.Int32,System.Int32,System.String,System.Boolean,System.String,System.String,System.Collections.Generic.IDictionary`2)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#.ctor(System.String,System.Boolean)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_AssemblyName()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CompareBaseNamesStringWise(System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CompareBaseNameTo(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CompareBaseNameToImpl(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CompareTo(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#EqualsIgnoreVersion(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#EscapeDisplayNameCharacters(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_FullName()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#GetHashCode()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_HasProcessorArchitectureInFusionName()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_IsSimpleName()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_IsUnnamedAssembly()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#AddRemappedAssemblyName(Microsoft.Build.Shared.AssemblyNameExtension)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#Clone()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#CloneImmutable()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_Immutable()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#MarkImmutable()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension,Microsoft.Build.Shared.PartialComparisonFlags,System.Boolean)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension,System.Boolean)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_ProcessorArchitecture()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_RemappedFromEnumerator()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#GetAssemblyNameEx(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#PartialNameCompare(Microsoft.Build.Shared.AssemblyNameExtension,Microsoft.Build.Shared.PartialComparisonFlags)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#ReplaceVersion(System.Version)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension.#get_UnnamedAssembly()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.CollectionHelpers.#RemoveNulls`1(System.Collections.Generic.List`1)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInternalLockHeld(System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#DebugTraceMessage(System.String,System.String,System.Object[])", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#ThrowArgument(System.String,System.Object[])", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#ThrowArgumentOutOfRange(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#ThrowIfTypeDoesNotImplementToString(System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrow(System.Boolean,System.String,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrow(System.Boolean,System.String,System.Object,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrow(System.Boolean,System.String,System.Object,System.Object,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String,System.Object,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.Exception,System.String,System.Object,System.Object,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String,System.Object,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgument(System.Boolean,System.String,System.Object,System.Object,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgumentArraysSameLength(System.Array,System.Array,System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgumentLengthIfNotNull(System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowArgumentOutOfRange(System.Boolean,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInternalLength(System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInternalRooted(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInvalidOperation(System.Boolean,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInvalidOperation(System.Boolean,System.String,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ErrorUtilities.#VerifyThrowInvalidOperation(System.Boolean,System.String,System.Object,System.Object)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.EscapingUtilities.#ContainsEscapedWildcards(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#IsIoRelatedException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedFunctionException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedIoOrXmlException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedReflectionException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedRegistryException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#NotExpectedSerializationException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#GetXmlLineAndColumn(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling.#IsXmlException(System.Exception)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#get_Column()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#set_Column(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#get_Line()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.ExceptionHandling+LineAndColumn.#set_Line(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#ClearCacheDirectory()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#CreateUriFromPath(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutableConfigurationFilePath()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutableDirectory()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_CurrentExecutablePath()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#DeleteDirectoryNoThrow(System.String,System.Boolean)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#DeleteNoThrow(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#DirectoryExistsNoThrow(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#EnsureNoLeadingSlash(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#EnsureNoTrailingSlash(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#EnsureTrailingSlash(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#FileExistsNoThrow(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#FileOrDirectoryExistsNoThrow(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetCacheDirectory()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetDirectoryNameOfFullPath(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#HasExtension(System.String,System.String[])", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#IsMetaprojectFilename(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#IsSolutionFilename(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#IsVCProjFilename(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#MakeRelative(System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#TrimAndStripAnyQuotes(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#get_ExecutingAssemblyPath()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetHexHash(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#GetPathsHash(System.Collections.Generic.IEnumerable`1)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities+ItemSpecModifiers.#GetItemSpecModifier(System.String,System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LoadedType.#.ctor(System.Type,Microsoft.Build.Shared.AssemblyLoadInfo)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LogMessagePacketBase.#.ctor(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LogMessagePacketBase.#get_EventType()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.LogMessagePacketBase.#get_NodeBuildEvent()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#CoWaitForMultipleHandles(Microsoft.Build.Shared.NativeMethodsShared+COWAIT_FLAGS,System.Int32,System.Int32,System.IntPtr[],System.Int32&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#CreatePipe(Microsoft.Win32.SafeHandles.SafeFileHandle&,Microsoft.Win32.SafeHandles.SafeFileHandle&,Microsoft.Build.Shared.NativeMethodsShared+SecurityAttributes,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#FindOnPath(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#FreeLibrary(System.IntPtr)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetChildProcessIds(System.Int32,System.DateTime)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetCurrentDirectory()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetCurrentDirectory(System.Int32,System.Text.StringBuilder)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetFileAttributesEx(System.String,System.Int32,Microsoft.Build.Shared.NativeMethodsShared+WIN32_FILE_ATTRIBUTE_DATA&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetFileType(System.IntPtr)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLastWriteDirectoryUtcTime(System.String,System.DateTime&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLastWriteFileUtcTime(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLongFilePath(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetLongPathName(System.String,System.Text.StringBuilder,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetMemoryStatus()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetModuleFileName(System.Runtime.InteropServices.HandleRef,System.Text.StringBuilder,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetNativeSystemInfo(Microsoft.Build.Shared.NativeMethodsShared+SYSTEM_INFO&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetOEMCP()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetParentProcessId(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetProcAddress(System.IntPtr,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetRequestedRuntimeInfo(System.String,System.String,System.String,System.UInt32,System.UInt32,System.Text.StringBuilder,System.Int32,System.UInt32&,System.Text.StringBuilder,System.Int32,System.UInt32&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetShortFilePath(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetShortPathName(System.String,System.Text.StringBuilder,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetStdHandle(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GetSystemInfo(Microsoft.Build.Shared.NativeMethodsShared+SYSTEM_INFO&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#GlobalMemoryStatusEx(Microsoft.Build.Shared.NativeMethodsShared+MemoryStatus)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#HResultFailed(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#HResultSucceeded(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#KillTree(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#LoadLibrary(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#MsgWaitOne(System.Threading.WaitHandle,System.TimeSpan)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#NtQueryInformationProcess(Microsoft.Build.Shared.NativeMethodsShared+SafeProcessHandle,Microsoft.Build.Shared.NativeMethodsShared+PROCESSINFOCLASS,Microsoft.Build.Shared.NativeMethodsShared+PROCESS_BASIC_INFORMATION&,System.Int32,System.Int32&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#OpenProcess(Microsoft.Build.Shared.NativeMethodsShared+eDesiredAccess,System.Boolean,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle,System.Byte[],System.UInt32,System.UInt32&,System.IntPtr)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SearchPath(System.String,System.String,System.String,System.Int32,System.Text.StringBuilder,System.Int32[])", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SetErrorMode(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SetErrorMode_VistaAndOlder(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#SetErrorMode_Win7AndNewer(System.Int32,System.Int32&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared.#WaitForMultipleObjects(System.UInt32,System.IntPtr[],System.Boolean,System.UInt32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.NativeMethodsShared+PROCESS_BASIC_INFORMATION.#get_Size()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.TypeLoader.#ReflectionOnlyLoad(System.String,Microsoft.Build.Shared.AssemblyLoadInfo)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#ArchitectureValuesMatch(System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#GetExplicitMSBuildArchitecture(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#GetExplicitMSBuildRuntime(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsBadlyCasedSpecialTaskAttribute(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsNonBatchingTargetAttribute(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsSpecialTaskAttribute(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsValidMSBuildArchitectureValue(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#IsValidMSBuildRuntimeValue(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#RuntimeValuesMatch(System.String,System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#TryMergeArchitectureValues(System.String,System.String,System.String&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.XMakeAttributes.#TryMergeRuntimeValues(System.String,System.String,System.String&)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.CharArrayInternTarget.#ResetCount(System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern.#EnableStatisticsGathering()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern.#InternStringIfPossible(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern.#ReportStatistics()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern+BucketedPrioritizedStringList.#ReportStatistics(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern+PrioritizedStringList.#Statistics()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern.#CharArrayToString(System.Char[],System.Int32,System.Int32)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern+BucketedPrioritizedStringList+PrioritizedStringList.#Statistics()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.OpportunisticIntern+StringInternTarget.#.ctor(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.StringInternTarget.#.ctor(System.String)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.NodeBuildComplete.#.ctor(System.Boolean)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.NodeEndpointOutOfProcBase.#get_LogDebugCommunications()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.NodeShutdown.#get_Exception()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.NodeShutdown.#FactoryForDeserialization(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.NodeShutdown.#get_Reason()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#.ctor(Microsoft.Build.BackEnd.TaskCompleteType,System.Collections.Generic.IDictionary`2,System.Collections.Generic.IDictionary`2)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#get_BuildProcessEnvironment()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#FactoryForDeserialization(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#get_TaskException()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#get_TaskExceptionMessage()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#get_TaskExceptionMessageArgs()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#get_TaskOutputParameters()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskHostTaskComplete.#get_TaskResult()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.BackEnd.TaskParameter.#get_ParameterType()", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.CommandLine.LogMessagePacket.#.ctor(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.CommandLine.LogMessagePacket.#FactoryForDeserialization(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.CommandLine.OutOfProcTaskAppDomainWrapperBase.#set_CancelPending(System.Boolean)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Internal.CommunicationsUtilities.#GetTaskHostContext(System.Collections.Generic.IDictionary`2)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Internal.CommunicationsUtilities.#ReadLongForHandshake(System.IO.Pipes.PipeStream)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.AssemblyLoadInfo.#FactoryForTranslation(Microsoft.Build.BackEnd.INodePacketTranslator)", Justification="This is shared code between MSBuild and MSBuildTaskHost")] + +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.CommandLine.OutOfProcTaskAppDomainWrapperBase.#get_WrappedTask()", Justification="This is Ok. It is called by MSBuild.exe OOP node during Cancellation. MSBuildTaskHost doesn't use this.")] + +[module: SuppressMessage("Microsoft.Reliability","CA2001:AvoidCallingProblematicMethods", MessageId="System.Reflection.Assembly.LoadFrom", Scope="member", Target="Microsoft.Build.BackEnd.Logging.TaskEngineAssemblyResolver.#ResolveAssembly(System.Object,System.ResolveEventArgs)", Justification="This has been reviewed as ok")] +[module: SuppressMessage("Microsoft.Reliability","CA2001:AvoidCallingProblematicMethods", MessageId="System.Reflection.Assembly.LoadFrom", Scope="member", Target="Microsoft.Build.Shared.TypeLoader+AssemblyInfoToLoadedTypes.#ScanAssemblyForPublicTypes()", Justification="This has been reviewed as ok")] +#endif diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/MSBuildTaskHost.csproj new file mode 100644 index 00000000000..7cf60aeb764 --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/MSBuildTaskHost.csproj @@ -0,0 +1,240 @@ + + + + v3.5 + true + + + $(DefineConstants);CLR2COMPATIBILITY + + + + Exe + MSBuildTaskHost + true + true + SAK + SAK + SAK + SAK + $(NoWarn);618 + true + + true + + true + + + + {53733ECF-0D81-43DA-B602-2AE9417F614F} + + + true + + + + $(XMakeRoot) + + + + + + $(SdkRefPath)\System.dll + + + $(SdkRefPath)\System.Core.dll + + + $(SdkRefPath)\System.Xml.dll + + + $(SdkRefPath)\System.Configuration.dll + + + $(NetFX35RefPath)\Microsoft.Build.Framework.dll + true + + + + + + AssemblyNameComparer.cs + + + true + + + BuildEngineResult.cs + + + IBuildEngine3.cs + + + RunInSTAAtribute.cs + + + ITaskItem2.cs + + + + CopyOnWriteDictionary.cs + + + + + ErrorUtilities.cs + true + + + EscapingUtilities.cs + true + + + ExceptionHandling.cs + + + FileUtilities.cs + true + + + FileUtilitiesRegex.cs + true + + + HybridDictionary.cs + + + INodeEndpoint.cs + + + INodePacket.cs + + + INodePacketFactory.cs + + + INodePacketHandler.cs + + + INodePacketTranslatable.cs + + + INodePacketTranslator.cs + + + InternalErrorException.cs + + + InterningBinaryReader.cs + + + LogMessagePacketBase.cs + + + Modifiers.cs + true + + + NativeMethodsShared.cs + true + + + NodeBuildComplete.cs + + + NodeEndpointOutOfProcBase.cs + + + NodeEngineShutdownReason.cs + + + NodePacketFactory.cs + + + NodePacketTranslator.cs + + + NodeShutdown.cs + + + OpportunisticIntern.cs + + + ReadOnlyEmptyCollection.cs + + + ResourceUtilities.cs + true + + + StringBuilderCache.cs + true + + + TaskEngineAssemblyResolver.cs + true + + + TaskParameterTypeVerifier.cs + + + XMakeAttributes.cs + true + + + + + LogMessagePacket.cs + + + + + + + OutOfProcTaskHostTaskResult.cs + + + + true + + + true + + + + + true + + + + + OutOfProcTaskAppDomainWrapperBase.cs + + + OutOfProcTaskAppDomainWrapperStub.cs + + + + + + + + + + + + + MSBuildTaskHost.Strings.shared.resources + Designer + + + + + \ No newline at end of file diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/OutOfProcTaskHost.cs b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/OutOfProcTaskHost.cs new file mode 100644 index 00000000000..5374906908a --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/OutOfProcTaskHost.cs @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Entry point class for MSBuildTaskHost.exe, which serves as the +// task host executable for CLR 2 tasks. +//----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +// CR: We could move MSBuildApp.ExitType out of MSBuildApp +using Microsoft.Build.CommandLine; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This is the Out-Of-Proc Task Host for supporting Cross-Targeting tasks. + /// + /// + /// It will be responsible for: + /// - Task execution + /// - Communicating with the MSBuildApp process, specifically the TaskHostFactory + /// (Logging messages, receiving Tasks from TaskHostFactory, sending results and other messages) + /// + public static class OutOfProcTaskHost + { + /// + /// Enumeration of the various ways in which the MSBuildTaskHost.exe application can exit. + /// + internal enum ExitType + { + /// + /// The application executed successfully. + /// + Success, + + /// + /// We received a request from MSBuild.exe to terminate + /// + TerminateRequest, + + /// + /// A logger aborted the build. + /// + LoggerAbort, + + /// + /// A logger failed unexpectedly. + /// + LoggerFailure, + + /// + /// The Task Host Node did not terminate gracefully + /// + TaskHostNodeFailed, + + /// + /// An unexpected failure + /// + Unexpected + } + + /// + /// Main Entry Point + /// + /// + /// We won't execute any tasks in the main thread, so we don't need to be in an STA + /// + [MTAThread] + public static int Main() + { + int exitCode = (Execute() == ExitType.Success ? 0 : 1); + return exitCode; + } + + /// + /// Orchestrates the execution of the application. + /// Also responsible for top-level error handling. + /// + /// + /// A value of Success if the bootstrapping succeeds + /// + internal static ExitType Execute() + { + // Provide Hook for debugger + if (Environment.GetEnvironmentVariable("MSBUILDDEBUGONSTART") == "1") + { + Debugger.Launch(); + } + + bool restart = false; + do + { + OutOfProcTaskHostNode oopTaskHostNode = new OutOfProcTaskHostNode(); + Exception taskHostShutDownException = null; + NodeEngineShutdownReason taskHostShutDownReason = oopTaskHostNode.Run(out taskHostShutDownException); + + if (taskHostShutDownException != null) + { + return ExitType.TaskHostNodeFailed; + } + + switch (taskHostShutDownReason) + { + case NodeEngineShutdownReason.BuildComplete: + return ExitType.Success; + + case NodeEngineShutdownReason.BuildCompleteReuse: + restart = true; + break; + + default: + return ExitType.TaskHostNodeFailed; + } + } + while (restart); + + // Should not happen + ErrorUtilities.ThrowInternalErrorUnreachable(); + return ExitType.Unexpected; + } + } +} diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/TypeLoader.cs b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/TypeLoader.cs new file mode 100644 index 00000000000..c797aefb5aa --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/TypeLoader.cs @@ -0,0 +1,392 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Determines if a type is in a given assembly and loads that type. +// This version is CLR 3.5-compatible. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Shared +{ + /// + /// This class is used to load types from their assemblies. + /// + internal class TypeLoader + { + /// + /// Lock for initializing the dictionary + /// + private static readonly Object cacheOfLoadedTypesByFilterLock = new Object(); + + /// + /// Lock for initializing the dictionary + /// + private static readonly Object cacheOfReflectionOnlyLoadedTypesByFilterLock = new Object(); + + /// + /// Lock for initializing the dictionary + /// + private static readonly Object loadInfoToTypeLock = new Object(); + + /// + /// Lock for initializing the dictionary + /// + private static readonly Object reflectionOnlyloadInfoToTypeLock = new Object(); + + /// + /// Cache to keep track of the assemblyLoadInfos based on a given typeFilter. + /// + private static IDictionary> cacheOfLoadedTypesByFilter = new Dictionary>(); + + /// + /// Cache to keep track of the assemblyLoadInfos based on a given typeFilter for assemblies which are to be loaded for reflectionOnlyLoads. + /// + private static IDictionary> cacheOfReflectionOnlyLoadedTypesByFilter = new Dictionary>(); + + /// + /// Typefilter for this typeloader + /// + private TypeFilter isDesiredType; + + /// + /// Constructor. + /// + internal TypeLoader(TypeFilter isDesiredType) + { + ErrorUtilities.VerifyThrow(isDesiredType != null, "need a type filter"); + + this.isDesiredType = isDesiredType; + } + + /// + /// Given two type names, looks for a partial match between them. A partial match is considered valid only if it occurs on + /// the right side (tail end) of the name strings, and at the start of a class or namespace name. + /// + /// + /// 1) Matches are case-insensitive. + /// 2) .NET conventions regarding namespaces and nested classes are respected, including escaping of reserved characters. + /// + /// + /// "Csc" and "csc" ==> exact match + /// "Microsoft.Build.Tasks.Csc" and "Microsoft.Build.Tasks.Csc" ==> exact match + /// "Microsoft.Build.Tasks.Csc" and "Csc" ==> partial match + /// "Microsoft.Build.Tasks.Csc" and "Tasks.Csc" ==> partial match + /// "MyTasks.ATask+NestedTask" and "NestedTask" ==> partial match + /// "MyTasks.ATask\\+NestedTask" and "NestedTask" ==> partial match + /// "MyTasks.CscTask" and "Csc" ==> no match + /// "MyTasks.MyCsc" and "Csc" ==> no match + /// "MyTasks.ATask\.Csc" and "Csc" ==> no match + /// "MyTasks.ATask\\\.Csc" and "Csc" ==> no match + /// + /// true, if the type names match exactly or partially; false, if there is no match at all + internal static bool IsPartialTypeNameMatch(string typeName1, string typeName2) + { + bool isPartialMatch = false; + + // if the type names are the same length, a partial match is impossible + if (typeName1.Length != typeName2.Length) + { + string longerTypeName; + string shorterTypeName; + + // figure out which type name is longer + if (typeName1.Length > typeName2.Length) + { + longerTypeName = typeName1; + shorterTypeName = typeName2; + } + else + { + longerTypeName = typeName2; + shorterTypeName = typeName1; + } + + // if the shorter type name matches the end of the longer one + if (longerTypeName.EndsWith(shorterTypeName, StringComparison.OrdinalIgnoreCase)) + { + int matchIndex = longerTypeName.Length - shorterTypeName.Length; + + // if the matched sub-string looks like the start of a namespace or class name + if ((longerTypeName[matchIndex - 1] == '.') || (longerTypeName[matchIndex - 1] == '+')) + { + int precedingBackslashes = 0; + + // confirm there are zero, or an even number of \'s preceding it... + for (int i = matchIndex - 2; i >= 0; i--) + { + if (longerTypeName[i] == '\\') + { + precedingBackslashes++; + } + else + { + break; + } + } + + if ((precedingBackslashes % 2) == 0) + { + isPartialMatch = true; + } + } + } + } + else + { + isPartialMatch = (String.Compare(typeName1, typeName2, StringComparison.OrdinalIgnoreCase) == 0); + } + + return isPartialMatch; + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + internal LoadedType Load + ( + string typeName, + AssemblyLoadInfo assembly + ) + { + return GetLoadedType(cacheOfLoadedTypesByFilterLock, loadInfoToTypeLock, cacheOfLoadedTypesByFilter, typeName, assembly); + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + /// The loaded type, or null if the type was not found. + internal LoadedType ReflectionOnlyLoad + ( + string typeName, + AssemblyLoadInfo assembly + ) + { + return GetLoadedType(cacheOfReflectionOnlyLoadedTypesByFilterLock, reflectionOnlyloadInfoToTypeLock, cacheOfReflectionOnlyLoadedTypesByFilter, typeName, assembly); + } + + /// + /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if + /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type + /// found will be returned. + /// + private LoadedType GetLoadedType(object cacheLock, object loadInfoToTypeLock, IDictionary> cache, string typeName, AssemblyLoadInfo assembly) + { + // A given typefilter have been used on a number of assemblies, Based on the typefilter we will get another dictionary which + // will map a specific AssemblyLoadInfo to a AssemblyInfoToLoadedTypes class which knows how to find a typeName in a given assembly. + IDictionary loadInfoToType = null; + lock (cacheLock) + { + if (!cache.TryGetValue(isDesiredType, out loadInfoToType)) + { + loadInfoToType = new Dictionary(); + cache.Add(isDesiredType, loadInfoToType); + } + } + + // Get an object which is able to take a typename and determine if it is in the assembly pointed to by the AssemblyInfo. + AssemblyInfoToLoadedTypes typeNameToType = null; + lock (loadInfoToTypeLock) + { + if (!loadInfoToType.TryGetValue(assembly, out typeNameToType)) + { + typeNameToType = new AssemblyInfoToLoadedTypes(isDesiredType, assembly); + loadInfoToType.Add(assembly, typeNameToType); + } + } + + return typeNameToType.GetLoadedTypeByTypeName(typeName); + } + + /// + /// Given a type filter and an asssemblyInfo object keep track of what types in a given assembly which match the typefilter. + /// Also, use this information to determine if a given TypeName is in the assembly which is pointed to by the AssemblyLoadInfo object. + /// + /// This type represents a combination of a type filter and an assemblyInfo object. + /// + private class AssemblyInfoToLoadedTypes + { + /// + /// Lock to prevent two threads from using this object at the same time. + /// Since we fill up internal structures with what is in the assembly + /// + private readonly Object lockObject = new Object(); + + /// + /// Type filter to pick the correct types out of an assembly + /// + private TypeFilter isDesiredType; + + /// + /// Assembly load information so we can load an assembly + /// + private AssemblyLoadInfo assemblyLoadInfo; + + /// + /// What is the type for the given type name, this may be null if the typeName does not map to a type. + /// + private Dictionary typeNameToType; + + /// + /// List of public types in the assembly which match the typefilter and their corresponding types + /// + private Dictionary publicTypeNameToType; + + /// + /// Have we scanned the public types for this assembly yet. + /// + private bool haveScannedPublicTypes; + + /// + /// If we loaded an assembly for this type. + /// We use this information to set the LoadedType.LoadedAssembly so that this object can be used + /// to help created AppDomains to resolve those that it could not load successfuly + /// + private Assembly loadedAssembly; + + /// + /// Given a type filter, and an assembly to load the type information from determine if a given type name is in the assembly or not. + /// + internal AssemblyInfoToLoadedTypes(TypeFilter typeFilter, AssemblyLoadInfo loadInfo) + { + ErrorUtilities.VerifyThrowArgumentNull(typeFilter, "typefilter"); + ErrorUtilities.VerifyThrowArgumentNull(loadInfo, "loadInfo"); + + this.isDesiredType = typeFilter; + this.assemblyLoadInfo = loadInfo; + this.typeNameToType = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.publicTypeNameToType = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Determine if a given type name is in the assembly or not. Return null if the type is not in the assembly + /// + internal LoadedType GetLoadedTypeByTypeName(string typeName) + { + ErrorUtilities.VerifyThrowArgumentNull(typeName, "typeName"); + + // Only one thread should be doing operations on this instance of the object at a time. + lock (lockObject) + { + Type type = null; + + // Maybe we've already cracked open this assembly before. Check to see if the typeName is in the list we dont look for partial matches here + // this is an optimization. + bool foundType = typeNameToType.TryGetValue(typeName, out type); + if (!foundType) + { + // We could still not find the type, lets try and resolve it by doing a get type. + if ((assemblyLoadInfo.AssemblyName != null) && (typeName.Length > 0)) + { + try + { + // try to load the type using its assembly qualified name + type = Type.GetType(typeName + "," + assemblyLoadInfo.AssemblyName, false /* don't throw on error */, true /* case-insensitive */); + } + catch (ArgumentException) + { + // Type.GetType() will throw this exception if the type name is invalid -- but we have no idea if it's the + // type or the assembly name that's the problem -- so just ignore the exception, because we're going to + // check the existence/validity of the assembly and type respectively, below anyway + } + + // if we found the type, it means its assembly qualified name was also its fully qualified name + if (type != null) + { + // if it's not the right type, bail out -- there's no point searching further since we already matched on the + // fully qualified name + if (!isDesiredType(type, null)) + { + typeNameToType.Add(typeName, null); + return null; + } + else + { + typeNameToType.Add(typeName, type); + } + } + } + + // We could not find the type based on the passed in type name, we now need to see if there is a type which + // will match based on partially matching the typename. To do this partial matching we need to get the public types in the assembly + if (type == null && !haveScannedPublicTypes) + { + ScanAssemblyForPublicTypes(); + haveScannedPublicTypes = true; + } + + // Could not find the type we need to look through the types in the assembly or in our cache. + if (type == null) + { + foreach (KeyValuePair desiredTypeInAssembly in publicTypeNameToType) + { + // if type matches partially on its name + if (typeName.Length == 0 || TypeLoader.IsPartialTypeNameMatch(desiredTypeInAssembly.Key, typeName)) + { + type = desiredTypeInAssembly.Value; + typeNameToType.Add(typeName, type); + break; + } + } + } + } + + if (type != null) + { + return new LoadedType(type, assemblyLoadInfo, loadedAssembly); + } + + return null; + } + } + + /// + /// Scan the assembly pointed to by the assemblyLoadInfo for public types. We will use these public types to do partial name matching on + /// to find tasks, loggers, and task factories. + /// + private void ScanAssemblyForPublicTypes() + { + // we need to search the assembly for the type... + try + { + if (assemblyLoadInfo.AssemblyName != null) + { + loadedAssembly = Assembly.Load(assemblyLoadInfo.AssemblyName); + } + else + { + loadedAssembly = Assembly.LoadFrom(assemblyLoadInfo.AssemblyFile); + } + } + catch (ArgumentException e) + { + // Assembly.Load() and Assembly.LoadFrom() will throw an ArgumentException if the assembly name is invalid + // convert to a FileNotFoundException because it's more meaningful + // NOTE: don't use ErrorUtilities.VerifyThrowFileExists() here because that will hit the disk again + throw new FileNotFoundException(null, assemblyLoadInfo.AssemblyLocation, e); + } + + // only look at public types + Type[] allPublicTypesInAssembly = loadedAssembly.GetExportedTypes(); + foreach (Type publicType in allPublicTypesInAssembly) + { + if (isDesiredType(publicType, null)) + { + publicTypeNameToType.Add(publicType.FullName, publicType); + } + } + } + } + } +} diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/app.config b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/app.config new file mode 100644 index 00000000000..8add5f0d71c --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/app.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/native.rc b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/native.rc new file mode 100644 index 00000000000..402d9dacd4f --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/MSBuildTaskHost/native.rc @@ -0,0 +1,5 @@ +// From Dev12 on we want the versioning strings to be the VS ones instead of .NET Framework ones. +#include +#include + +#include diff --git a/src/XMakeCommandLine/MSBuildTaskHost/dirs.proj b/src/XMakeCommandLine/MSBuildTaskHost/dirs.proj new file mode 100644 index 00000000000..97e61ddf113 --- /dev/null +++ b/src/XMakeCommandLine/MSBuildTaskHost/dirs.proj @@ -0,0 +1,18 @@ + + + + + true + true + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/Microsoft.Build.CommonTypes.xsd b/src/XMakeCommandLine/Microsoft.Build.CommonTypes.xsd new file mode 100644 index 00000000000..537b6b29fe3 --- /dev/null +++ b/src/XMakeCommandLine/Microsoft.Build.CommonTypes.xsd @@ -0,0 +1,5058 @@ + + + + + + + + + + + Reference to an assembly + + + + + + + + + Relative or absolute path to the assembly (optional) + + + + + Friendly display name (optional) + + + + + Fusion name of the assembly (optional) + + + + + Whether only the version in the fusion name should be referenced (optional, boolean) + + + + + Aliases for the reference (optional) + + + + + Whether the reference should be copied to the output folder (optional, boolean) + + + + + Whether the types in this reference need to embedded into the target assembly - interop asemblies only (optional, boolean) + + + + + The minimum required target framework version in order to use this assembly as a reference + + + + + + + + Assembly name or filename + + + + + + + + + Reference to an extension SDK + + + + + + + + + Friendly display name (optional) + + + + + + + + Name and version moniker representing an extension SDK + + + + + + + + + Reference to a COM component + + + + + + + + + Friendly display name (optional) + + + + + GUID in the form {00000000-0000-0000-0000-000000000000} + + + + + Major part of the version number + + + + + Minor part of the version number + + + + + Locale ID + + + + + Wrapper tool, such as tlbimp + + + + + Is it isolated (boolean) + + + + + Whether the types in this reference need to embedded into the target assembly - interop asemblies only (optional, boolean) + + + + + + + + COM component name + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Project Capability that may activate design-time components in an IDE. + + + + + + Reference to a native manifest file, or to a file that contains a native manifest + + + + + + + + + Base name of manifest file + + + + + Relative path to manifest file + + + + + + + + Reference full name + + + + + + + + + Reference to another project + + + + + + + + + Friendly display name (optional) + + + + + Project GUID, in the form {00000000-0000-0000-0000-000000000000} + + + + + Boolean specifying whether the outputs of the project referenced should be passed to the compiler. Default is true. + + + + + Whether the exact version of the assembly should be used. + + + + + Semicolon separated list of targets in the referenced projects that should be built. Default is the value of $(ProjectReferenceBuildTargets) whose default is blank, indicating the default targets. + + + + + + Item type to emit target outputs into. Default is blank. If the Reference metadata is set to "true" (default) then target outputs will become references for the compiler. + + + + + + + Whether the types in this reference need to embedded into the target assembly - interop asemblies only (optional, boolean) + + + + + + + + Path to project file + + + + + + + + + Source files for compiler + + + + + + + + + + + Whether file was generated from another file (boolean) + + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + + Display in user interface (optional, boolean) + + + + + + + + + + Semi-colon separated list of source files (wildcards are allowed) + + + + + + + + + Resources to be embedded in the generated assembly + + + + + + + + + + Name of any file generator that is run on this item + + + + + File that was created by any file generator that was run on this item + + + + + Namespace into which any file generator that is run on this item should create code + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + Display in user interface (optional, boolean) + + + + + + + + + + + Semi-colon separated list of resource files (wildcards are allowed) + + + + + + + + + Files that are not compiled, but may be embedded or published + + + + + + + + + + Name of any file generator that is run on this item + + + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + + Default, Included, Excluded, DataFile, or Prerequisite + + + + + + Display in user interface (optional, boolean) + + + + + + Copy file to output directory (optional, boolean, default false) + + + + + + + + Semi-colon separated list of content files (wildcards are allowed) + + + + + + + + + XAML files that are converted to binary and compiled into the assembly + + + + + + + + + + Name of any file generator that is run on this item + + + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + + + Copy file to output directory (optional, boolean, default false) + + + + + + + + Semi-colon separated list of XAML files (wildcards are allowed) + + + + + + + + + File that is compiled into the assembly + + + + + + + + + + Name of any file generator that is run on this item + + + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + + + Copy file to output directory (optional, boolean, default false) + + + + + + + + Semi-colon separated list of files (wildcards are allowed) + + + + + + + + + XAML file that contains the application definition, only one can be defined + + + + + + + + + + Name of any file generator that is run on this item + + + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + + + Copy file to output directory (optional, boolean, default false) + + + + + + + + + + + Files that should have no role in the build process + + + + + + + + + + Name of any file generator that is run on this item + + + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional) + + + + + Display in user interface (optional, boolean) + + + + + + + + + + + + The base application manifest for the build. Contains ClickOnce security information. + + + + + Folder on disk + + + + + Assemblies whose namespaces should be imported by the Visual Basic compiler + + + + + + Name of Web References folder to display in user interface + + + + + Represents a reference to a web service + + + + + + + + + + + + + + + + + + + URL to web service + + + + + + + + + + + + + + + Display in user interface (optional, boolean) + + + + + + + + + + + + + + + + + + + + Display in user interface (optional, boolean) + + + + + + + + + + + + + + + + + + + Display in user interface (optional, boolean) + + + + + + (boolean) + + + + + (boolean) + + + + + Default, Included, Excluded, DataFile, ManifestEntryPoint, or Prerequisite + + + + + + + + + + + + Target platform in the form of "[Identifier], Version=[Version]", for example, "Windows, Version=8.0" + + + + + + + An assembly containing diagnostic analyzers + + + + + + + + + + Relative or absolute path to the assembly (required) + + + + + + + + + + + + + + + + + + integer + + + + + Matches the expression "\d\.\d\.\d\.(\d|\*)" + + + + + Name of folder for Application Designer + + + + + + + + Name of output assembly + + + + + + + + + Indicates whether BindingRedirect elements should be automatically generated for referenced assemblies. + + + + + boolean + + + + + + HomeSite, Relative, or Absolute + + + + + + boolean + + + + + + + + + + + + + + boolean + + + + + + + Whether to emit symbols (boolean) + + + + + none, pdbonly, or full + + + + + + + + + Whether DEBUG is defined (boolean) + + + + + Whether TRACE is defined (boolean) + + + + + + + boolean + + + + + Additional options to pass to the Code Analysis command line tool. + + + + + Indicates whether to apply the XSL style sheet specified in $(CodeAnalysisLogFileXsl) to the Code Analysis report. This report is specified in $(CodeAnalysisLogFile). The default is false. + + + + + Path to the XSL style sheet that will be applied to the Code Analysis console output. The default is an empty string (''), which causes Code Analysis to use its default console output. + + + + + Culture to use for Code Analysis spelling rules, for example, 'en-US' or 'en-AU'. The default is the current user interface language for Windows. + + + + + Additional reference assembly paths to pass to the Code Analysis command line tool. + + + + + + + + A fully qualified path to a directory containing reference assemblies to be used by Code Analysis. + + + + + + + + + Code Analysis custom dictionaries. + + + + + + + + Semicolon-separated list of Code Analysis custom dictionaries. Wildcards are allowed. + + + + + + + + + Indicates whether Code Analysis should fail if a rule or rule set is missing. The default is false. + + + + + Indicates whether Code Analysis generates a report file, even when there are no active warnings or errors. The default is true. + + + + + Indicates whether Code Analysis generates a '$(CodeAnalysisInputAssembly).lastcodeanalysissucceeded' file in the output folder when no build-breaking errors occur. The default is true. + + + + + Indicates whether Code Analysis will ignore the default rule directories when searching for rules. The default is false. + + + + + Indicates whether Code Analysis will ignore the default rule set directories when searching for rule sets. The default is false. + + + + + Indicates whether Code Analysis should silently fail when analyzing invalid assemblies, such as those without managed code. The default is true. + + + + + Indicates whether Code Analysis should fail silently when it analyzes invalid assemblies, such as those without managed code. The default is true. + + + + + Code Analysis projects (*.fxcop) or reports to import. + + + + + + + + + Semicolon-separated list of Code Analysis projects (*.fxcop) or reports to import. Wildcards are allowed. + + + + + + + + + Path to the assembly to be analyzed by Code Analysis. The default is '$(OutDir)$(TargetName)$(TargetExt)'. + + + + + Path to the output file for the Code Analysis report. The default is '$(CodeAnalysisInputAssembly).CodeAnalysisLog.xml'. + + + + + Path to the XSL style sheet to reference in the Code Analysis output report. This report is specified in $(CodeAnalysisLogFile). The default is an empty string (''). + + + + + Name of the file, without the path, where Code Analysis project-level suppressions are stored. The default is 'GlobalSuppressions$(DefaultLanguageSourceExtension)'. + + + + + Indicates whether to run all overridable Code Analysis rules against all targets. This will cause specific rules, such as those within the Design and Naming categories, to run against both public and internal APIs, instead of only public APIs. The default is false. + + + + + Indicates whether to output Code Analysis warnings and errors to the console. The default is false. + + + + + Indicates whether to output verbose Code Analysis diagnostic info to the console. The default is false. + + + + + Path to the Code Analysis installation folder. The default is '$(VSINSTALLDIR)\Team Tools\Static Analysis Tools\FxCop'. + + + + + Path to the .NET Framework folder that contains platform assemblies, such as mscorlib.dll and System.dll. The default is an empty string (''). + + + + + Path to the Code Analysis project (*.fxcop) to load. The default is an empty string (''). + + + + + Indicates whether to suppress all Code Analysis console output other than errors and warnings. This applies when $(CodeAnalysisOutputToConsole) is true. The default is false. + + + + + Semicolon-separated list of paths either to Code Analysis rule assemblies or to folders that contain Code Analysis rule assemblies. The paths are in the form '[+|-][!][file|folder]', where '+' enables all rules in rule assembly, '-' disables all rules in rule assembly, and '!' causes all rules in rule assembly to be treated as errors. For example '+D:\Projects\Rules\NamingRules.dll;+!D:\Projects\Rules\SecurityRules.dll'. The default is '$(CodeAnalysisPath)\Rules'. + + + + + Semicolon-separated list of directories in which to search for rules when resolving a rule set. The default is '$(CodeAnalysisPath)\Rules' unless the CodeAnalysisIgnoreBuiltInRules property is set to true. + + + + + Semicolon-separated list of Code Analysis rules. The rules are in the form '[+|-][!]Category#CheckId', where '+' enables the rule, '-' disables the rule, and '!' causes the rule to be treated as an error. For example, '-Microsoft.Naming#CA1700;+!Microsoft.Naming#CA1701'. The default is an empty string ('') which enables all rules. + + + + + A .ruleset file which contains a list of rules to run during analysis. The string can be a full path, a path relative to the project file, or a file name. If a file name is specified, the CodeAnalysisRuleSetDirectories property will be searched to find the file. The default is an empty string (''). + + + + + Semicolon-separated list of directories in which to search for rule sets. The default is '$(VSINSTALLDIR)\Team Tools\Static Analysis Tools\Rule Sets' unless the CodeAnalysisIgnoreBuiltInRuleSets property is set to true. + + + + + Comma-separated list of the type ('Active', 'Excluded', or 'Absent') of warnings and errors to save to the output report file. The default is 'Active'. + + + + + Indicates whether Code Analysis should search the Global Assembly Cache (GAC) for missing references that are encountered during analysis. The default is true. + + + + + Indicates whether to output a Code Analysis summary to the console after analysis. The default is false. + + + + + The time, in seconds, that Code Analysis should wait for analysis of a single item to complete before it aborts analysis. Specify 0 to cause Code Analysis to wait indefinitely. The default is 120. + + + + + Indicates whether to treat all Code Analysis warnings as errors. The default is false. + + + + + Indicates whether to update the Code Analysis project (*.fxcop) specified in $(CodeAnalysisProject). This applies when there are changes during analysis. The default is false. + + + + + Indicates whether to include the name of the rule when Code Analysis emits a suppression. The default is true. + + + + + + + Whether Visual Studio should do its own faster up-to-date check before Building, rather than invoke MSBuild to do a possibly more accurate one. You would set this to false if you have a heavily customized build process and builds in Visual Studio are not occurring when they should. + + + + + + + + + + + + + + + + + + + + Sets the /sdkpath switch for a VB project to the specified value + + + + + + + + + + + + + + + Web, Unc, or Disk + + + + + + + + + + + + + + + + + boolean + + + + + Matches the expression "\d\.\d\.\d\.\d" + + + + + + + + + Whether standard libraries (such as mscorlib) should be referenced automatically (boolean) + + + + + Comma separated list of disabled warnings + + + + + + + + + boolean + + + + + Should compiler optimize output (boolean) + + + + + Option Compare setting (Text or Binary) + + + + + Should Option Explicit be set (On or Off) + + + + + Should Option Strict be set (On or Off) + + + + + Should Option Infer be set (On or Off) + + + + + + Path to output folder, with trailing slash + + + + + Type of output to generate (WinExe, Exe, or Library) + + + + + + + + + + + Command line to be run at the end of build + + + + + Command line to be run at the start of build + + + + + + + + + + + + + Semi-colon separated list of folders to search during reference resolution + + + + + + + + + + + + + + Indicates whether to run Code Analysis during the build. + + + + + + + + + + + + + + + + + + + + + + Type that contains the main entry point + + + + + + + + + + + + + + boolean + + + + + + + + boolean + + + + + + Hours, Days, or Weeks + + + + + Foreground or Background + + + + + boolean + + + + + boolean + + + + + + + + + + + + + + integer between 0 and 4 inclusive + + + + + Comma separated list of warning numbers to treat as errors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default resource language. + + + + + + App package certificate key file. + + + + + + Flag indicating whether to auto-increment package revision. + + + + + + Full path to a folder containing packaging build targets and tasks assembly. + + + + + + Full path to packaging build tasks assembly. + + + + + + Flag marking current project as capable of being packaged as an app package. + + + + + + Flag indicating whether to use hard links if possible when copying files during creation of app packages. + + + + + + Flag indicating whether to skip unchanged files when copying files during creation of app packages. + + + + + + Flag indicating whether to generate resource index files (PRI files) during packaging. + + + + + + Flag indicating whether to enable signing of app packages. + + + + + + Flag indicating whether to include private symbols in symbol packages. + + + + + + Flag indicating whether to generate a symbol package when an app package is created. + + + + + + Flag indicating whether to create test layout when an app package is created. + + + + + + Flag indicating whether to enable validation of app packages. + + + + + + Flag indicating whether to enable harvesting of WinMD registration information. + + + + + + Flag indicating whether to enable signing checks during app package generation. + + + + + + Flag indicating whether to enable strict manifest validation. + + + + + + Flag indicating whether to filter out unused language resource file maps. + + + + + + Targeted minimum OS version. + + + + + + Targeted maximum OS version tested. + + + + + + Name of the folder where app packages are produced. + + + + + + Name of the folder where platform-specific bundle artifact lists are stored. + + + + + + Full path to a folder where app packages will be saved. + + + + + + Additional qualifier to append to AppxPackageDir. + + + + + + Path to the final app manifest. + + + + + + Flag indicating whether to validate app manifest. + + + + + + Full path to makepri.exe utility. + + + + + + Full path to makeappx.exe utility. + + + + + + Full path to signtool.exe utility. + + + + + + Full path to a folder containing resgen tool. + + + + + + Full path to pdbcopy.exe utility. + + + + + + Full path to a directory where stripped PDBs will be stored. + + + + + + Flag indicating whether to enable prepending initial path when indexing RESW and RESJSON files in class libraries. + + + + + + Initial path when indexing RESW and RESJSON files in class libraries. + + + + + + File name to use for project-specific resource index file (PRI). + + + + + + Full path to project-specific resource index file (PRI). + + + + + + Full path to the app package recipe. + + + + + + Full path to the final app package recipe. + + + + + + Flag indicating whether to allow local network loopback. + + + + + + Default hash algorithm ID, used for signing an app package. + + + + + + Full path to app package file map. + + + + + + Full path to a folder with package layout. + + + + + + Name of the binary containing managed WinMD in-proc implementation. + + + + + + Flag indicating whether to enable incremental registration of the app layout. + + + + + + Full path to the packaging info file which will contain paths to produced packages. + + + + + + Flag indicating whether minimum OS version in app manifest should be replaced. + + + + + + Flag indicating whether maximum OS version tested in app manifest should be replaced. + + + + + + Full path to a text file containing packaging file writes log. + + + + + + Full path to a text file containing packaging directory writes log. + + + + + + Flag indicating whether CopyLocal files group should include XML files. + + + + + + '|'-delimited list of key=value pairs representing default resource qualifers. + + + + + + Path to an XML file containing packaging element for priconfi.xml file. + + + + + + Path to an XML file containing default element for priconfi.xml file. + + + + + + Full path to platform SDK root. + + + + + + Flag indicating whether packaging targets will produce an app bundle. + + + + + + '|'-delimited list of platforms which will be included in an app bundle. + + + + + + A platform which will be used to produce an app bundle. + + + + + + A platform which will be used to produce resource packs for an app bundle. + + + + + + Name of the folder where package layout will be prepared when producing an app bundle. + + + + + + Full path to the folder where package layout will be prepared when producing an app bundle. + + + + + + Full path to the priconfig.xml file used for splitting resource packs. + + + + + + '|'-delimited list of resource qualifers which will be used for automatic resource pack splitting. + + + + + + Prefix used for split resources .pri and .map.txt files. + + + + + + Full path to split resources .pri file. + + + + + + Full path to a log file containing a list of generated files during resource splitting. + + + + + + Full path to a log file containing a detected qualifiers during resource splitting. + + + + + + Full path to the priconfig.xml file used for generating main package file map. + + + + + + Prefix used for intermediate main package resources .pri and .map.txt files. + + + + + + Suffix used before extension of resource map files. + + + + + + Full path to an intermediate main package file map. + + + + + + Full path to an intermediate main package .pri file. + + + + + + Full path to a log file containing a list of generated files during generation of main package file map. + + + + + + Prefix used for main package resources .pri and .map.txt files. + + + + + + Full path to a main package file map. + + + + + + Suffix to append to app bundle folder. + + + + + + Full path to a folder where platform-specific bundle artifact list files are stored. + + + + + + Flag indicating whether to allow inclusion of debug framework references in an app manifest. + + + + + + Flag indicating whether to generate app package during the build. + + + + + + Flag indicating whether to insert reverse resource map during resource index generation. + + + + + + Flag indicating whether to include primary build outputs into the app package payload. + + + + + + Flag indicating whether to include debug symbols into the app package payload. + + + + + + Flag indicating whether to include documentation into the app package payload. + + + + + + Flag indicating whether to include satellite DLLs into the app package payload. + + + + + + Flag indicating whether to include source files into the app package payload. + + + + + + Flag indicating whether to include content files into the app package payload. + + + + + + Flag indicating whether to include SGen files into the app package payload. + + + + + + Flag indicating whether to include files marked as 'Copy local' into the app package payload. + + + + + + Flag indicating whether to include COM files into the app package payload. + + + + + + Flag indicating whether to include custom output group into the app package payload. + + + + + + Flag indicating whether to include WinMD artifacts into the app package payload. + + + + + + Flag indicating whether to include SDK redist into the app package payload. + + + + + + Flag indicating whether to include resource index (PRI) files into the app package payload. + + + + + + Flag indicating whether to include resolved SDK references into the app package payload. + + + + + + Name of the resource index used in the generated .pri file. + + + + + + Flag indicating whether to enable auto increment of an app package revision. + + + + + + Full path to a folder where app bundle will be produced. + + + + + + Name of the app package to generate. + + + + + + Name of the app store container to generate. + + + + + + Name of the folder where test app packages will be copied + + + + + + Full path to the app package file. + + + + + + Full path to the app symbol package file. + + + + + + Additional parameters to pass to makepri.exe when generating project PRI file. + + + + + + Additional parameters to pass to makepri.exe when generating PRI file for a portable library. + + + + + + Additional parameters to pass to makepri.exe when extracting payload file names. + + + + + + Name of the store manifest file. + + + + + + Flag indicating whether to validate store manifest. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Namespace alias used for this schema. + + + + + Namespace URI used for this schema. + + + + + + + + + + App manifest schema file. + + + + + + Store manifest schema file. + + + + + + Hash algorithm URI. + + + + + + + + Hash algorithm ID corresponding to given hash URI. + + + + + + + + + + + String resources to be indexed in app package's resource index. + + + + + + + + Notional path within project to indicate parent item of the current item (optional). + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional). + + + + + Display in user interface (optional, boolean). + + + + + + + + + + + Name of any file which is present on the machine and should not be part of the app payload. + + + + + + Reserved file name which cannot appear in the app package. + + + + + + XPath queries used to extract file names from the app manifest. + + + + + + XPath queries used to define image files in the app manifest and restrictions on them. + + + + + + + + ID of description string resource describing this type of the image. + + + + + Semicolon-delimited list of expected scale dimensions in format '{scale}:{width}x{height}'. + + + + + Semicolon-delimited list of expected target sizes. + + + + + Maximum file size in bytes. + + + + + + + + + + + app manifest template + + + + + + + + Notional path within project to indicate parent item of the current item (optional). + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional). + + + + + Visual Studio sub-type for this item (optional). + + + + + Display in user interface (optional, boolean). + + + + + + + + + + + A file containing app store association data. + + + + + + + + Notional path within project to indicate parent item of the current item (optional). + + + + + Notional path within project to display if the file is physically located outside of the project file's cone (optional). + + + + + Display in user interface (optional, boolean). + + + + + + + + + + + App manifest metadata item. Can be a literal, or it can be a path to a binary to extract version from. + + + + + + + + Literal value of app manifest metadata to insert into manifest. + + + + + Version to be inserted as a value of app manifest metadata to insert into manifest. + + + + + Name of app manifest metadata to insert into manifest. + + + + + + + + + + + Platform version description. Used to map between internal OS version and marketing OS version. + + + + + + + + Target platform identifier. + + + + + Target platform version. + + + + + Internal OS version. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/Microsoft.Build.Core.xsd b/src/XMakeCommandLine/Microsoft.Build.Core.xsd new file mode 100644 index 00000000000..8051b8da4d7 --- /dev/null +++ b/src/XMakeCommandLine/Microsoft.Build.Core.xsd @@ -0,0 +1,722 @@ + + + + + + An MSBuild Project + + + + + + + + + + + Optional semi-colon separated list of one or more targets that will be built if no targets are otherwise specified + + + + + Optional semi-colon separated list of targets that should always be built before any other targets + + + + + Optional string describing the toolset version this project should normally be built with + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Groups tasks into a section of the build process + + + + + + + + + + + + + + + Name of the target + + + + + Optional semi-colon separated list of targets that should be run before this target + + + + + Optional semi-colon separated list of files that form inputs into this target. Their timestamps will be compared with the timestamps of files in Outputs to determine whether the Target is up to date + + + + + Optional semi-colon separated list of files that form outputs into this target. Their timestamps will be compared with the timestamps of files in Inputs to determine whether the Target is up to date + + + + + Optional expression evaluated to determine whether the Target and the targets it depends on should be run + + + + + Optional expression evaluated to determine whether duplicate items in the Target's Returns should be removed before returning them. The default is not to eliminate duplicates. + + + + + Optional expression evaluated to determine which items generated by the target should be returned by the target. If there are no Returns attributes on Targets in the file, the Outputs attributes are used instead for this purpose. + + + + + Optional semi-colon separated list of targets that this target should run before. + + + + + Optional semi-colon separated list of targets that this target should run after. + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Groups property definitions + + + + + + + Optional expression evaluated to determine whether the PropertyGroup should be used + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Groups import definitions + + + + + + + Optional expression evaluated to determine whether the ImportGroup should be used + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Groups item list definitions + + + + + + + + + + + Optional expression evaluated to determine whether the ItemGroup should be used + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Groups item metadata definitions + + + + + + + + + + + Optional expression evaluated to determine whether the ItemDefinitionGroup should be used + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Groups When and Otherwise elements + + + + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Groups PropertyGroup and/or ItemGroup elements + + + + + + + + + + + Optional expression evaluated to determine whether the child PropertyGroups and/or ItemGroups should be used + + + + + + + Groups PropertyGroup and/or ItemGroup elements that are used if no Conditions on sibling When elements evaluate to true + + + + + + + + + + + + + Specifies targets to execute in the event of a recoverable error + + + + Optional expression evaluated to determine whether the targets should be executed + + + + + Semi-colon separated list of targets to execute + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Defines the assembly containing a task's implementation, or contains the implementation itself. + + + + + + + + Optional expression evaluated to determine whether the declaration should be evaluated + + + + + Optional name of assembly containing the task. Either AssemblyName or AssemblyFile must be used + + + + + Optional path to assembly containing the task. Either AssemblyName or AssemblyFile must be used + + + + + Name of task class in the assembly + + + + + Name of the task factory class in the assembly + + + + + + Defines the architecture of the task host that this task should be run in. Currently supported values: x86, x64, CurrentArchitecture, and * (any). If Architecture is not specified, either the task will be run within the MSBuild process, or the task host will be launched using the architecture of the parent MSBuild process + + + + + + + Defines the .NET runtime version of the task host that this task should be run in. Currently supported values: CLR2, CLR4, CurrentRuntime, and * (any). If Runtime is not specified, either the task will be run within the MSBuild process, or the task host will be launched using the runtime of the parent MSBuild process + + + + + + + + Groups parameters that are part of an inline task definition. + + + + + + + + + + Contains the inline task implementation. Content is opaque to MSBuild. + + + + + + + Whether the body should have properties expanded before use. Defaults to false. + + + + + + + Declares that the contents of another project file should be inserted at this location + + + + Optional expression evaluated to determine whether the import should occur + + + + + Project file to import + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + Optional section used by MSBuild hosts, that may contain arbitrary XML content that is ignored by MSBuild itself + + + + + + + + + + + + + + Optional expression evaluated to determine whether the items should be evaluated + + + + + Semi-colon separated list of files (wildcards are allowed) or other item names to include in this item list + + + + + Semi-colon separated list of files (wildcards are allowed) or other item names to exclude from the Include list + + + + + Semi-colon separated list of files (wildcards are allowed) or other item names to remove from the existing list contents + + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + + + + + + + + + + + + + + + + + + + + Optional expression evaluated to determine whether the property should be evaluated + + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + + + + + + + + + Optional expression evaluated to determine whether the property should be evaluated + + + + + + Optional expression. Used to identify or order system and user elements + + + + + + + + + + + + Optional element specifying a specific task output to be gathered + + + + + Task parameter to gather. Matches the name of a .NET Property on the task class that has an [Output] attribute + + + + + Optional name of an item list to put the gathered outputs into. Either ItemName or PropertyName must be specified + + + + + Optional name of a property to put the gathered output into. Either PropertyName or ItemName must be specified + + + + + Optional expression evaluated to determine whether the output should be gathered + + + + + + + + Optional expression evaluated to determine whether the task should be executed + + + + + Optional boolean indicating whether a recoverable task error should be ignored. Default false + + + + + + Defines the bitness of the task if it must be run specifically in a 32bit or 64bit process. If not specified, it will run with the bitness of the build process. If there are multiple tasks defined in UsingTask with the same name but with different Architecture attribute values, the value of the Architecture attribute specified here will be used to match and select the correct task + + + + + + + Defines the .NET runtime of the task. This must be specified if the task must run on a specific version of the .NET runtime. If not specified, the task will run on the runtime being used by the build process. If there are multiple tasks defined in UsingTask with the same name but with different Runtime attribute values, the value of the Runtime attribute specified here will be used to match and select the correct task + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/Microsoft.Build.xsd b/src/XMakeCommandLine/Microsoft.Build.xsd new file mode 100644 index 00000000000..8c76048c243 --- /dev/null +++ b/src/XMakeCommandLine/Microsoft.Build.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/NodeEndpointOutOfProcTaskHost.cs b/src/XMakeCommandLine/NodeEndpointOutOfProcTaskHost.cs new file mode 100644 index 00000000000..b606095a424 --- /dev/null +++ b/src/XMakeCommandLine/NodeEndpointOutOfProcTaskHost.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Implementation of a node endpoint for out-of-proc nodes. +//----------------------------------------------------------------------- + +using System; +using System.IO.Pipes; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This is an implementation of INodeEndpoint for the out-of-proc nodes. It acts only as a client. + /// + internal class NodeEndpointOutOfProcTaskHost : NodeEndpointOutOfProcBase + { + #region Constructors and Factories + + /// + /// Instantiates an endpoint to act as a client + /// + /// The name of the pipe to which we should connect. + internal NodeEndpointOutOfProcTaskHost(string pipeName) + { + InternalConstruct(pipeName); + } + + #endregion // Constructors and Factories + + /// + /// Returns the host handshake for this node endpoint + /// + protected override long GetHostHandshake() + { + long hostHandshake = CommunicationsUtilities.GetTaskHostHostHandshake(CommunicationsUtilities.GetCurrentTaskHostContext()); + return hostHandshake; + } + + /// + /// Returns the client handshake for this node endpoint + /// + protected override long GetClientHandshake() + { + long clientHandshake = CommunicationsUtilities.GetTaskHostClientHandshake(CommunicationsUtilities.GetCurrentTaskHostContext()); + return clientHandshake; + } + } +} diff --git a/src/XMakeCommandLine/OutOfProcTaskAppDomainWrapper.cs b/src/XMakeCommandLine/OutOfProcTaskAppDomainWrapper.cs new file mode 100644 index 00000000000..88bf7624007 --- /dev/null +++ b/src/XMakeCommandLine/OutOfProcTaskAppDomainWrapper.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing an out-of-proc node for executing tasks inside an AppDomain. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.CommandLine +{ + /// + /// Class for executing a task in an AppDomain + /// + [Serializable] + internal class OutOfProcTaskAppDomainWrapper : OutOfProcTaskAppDomainWrapperBase + { + /// + /// This is an extension of the OutOfProcTaskAppDomainWrapper that is responsible + /// for activating and executing the user task. + /// This extension provides support for ICancellable Out-Of-Proc tasks. + /// + /// True if the task is ICancellable + internal bool CancelTask() + { + // If the cancel was issued even before WrappedTask has been created then set a flag so that we can + // skip execution + CancelPending = true; + + // Store in a local to avoid a race + var wrappedTask = WrappedTask; + if (wrappedTask == null) + { + return true; + } + + ICancelableTask cancelableTask = wrappedTask as ICancelableTask; + if (cancelableTask != null) + { + cancelableTask.Cancel(); + return true; + } + + return false; + } + } +} diff --git a/src/XMakeCommandLine/OutOfProcTaskAppDomainWrapperBase.cs b/src/XMakeCommandLine/OutOfProcTaskAppDomainWrapperBase.cs new file mode 100644 index 00000000000..913ad6e5c28 --- /dev/null +++ b/src/XMakeCommandLine/OutOfProcTaskAppDomainWrapperBase.cs @@ -0,0 +1,414 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing an out-of-proc node for executing tasks inside an AppDomain. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Threading; +using System.Reflection; +using System.Runtime.CompilerServices; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using System.Runtime.Remoting; + +namespace Microsoft.Build.CommandLine +{ + /// + /// Class for executing a task in an AppDomain + /// + [Serializable] + internal class OutOfProcTaskAppDomainWrapperBase : MarshalByRefObject + { + /// + /// This is the actual user task whose instance we will create and invoke Execute + /// + private ITask _wrappedTask; + + /// + /// This is an appDomain instance if any is created for running this task + /// + /// + /// TaskAppDomain's non-serializability should never be an issue since even if we start running the wrapper + /// in a separate appdomain, we will not be trying to load the task on one side of the serialization + /// boundary and run it on the other. + /// + [NonSerialized] + private AppDomain _taskAppDomain; + + /// + /// Need to keep the build engine around in order to log from the task loader. + /// + private IBuildEngine _buildEngine; + + /// + /// Need to keep track of the task name also so that we can log valid information + /// from the task loader. + /// + private string _taskName; + + /// + /// This is the actual user task whose instance we will create and invoke Execute + /// + public ITask WrappedTask + { + get { return _wrappedTask; } + } + + /// + /// We have a cancel already requested + /// This can happen before we load the module and invoke execute. + /// + internal bool CancelPending + { + get; + set; + } + + /// + /// This is responsible for invoking Execute on the Task + /// Any method calling ExecuteTask must remember to call CleanupTask + /// + /// + /// We also allow the Task to have a reference to the BuildEngine by design + /// at ITask.BuildEngine + /// + /// The OutOfProcTaskHostNode as the BuildEngine + /// The name of the task to be executed + /// The path of the task binary + /// The path to the project file in which the task invocation is located. + /// The line in the project file where the task invocation is located. + /// The column in the project file where the task invocation is located. + /// The AppDomainSetup that we want to use to launch our AppDomainIsolated tasks + /// Parameters that will be passed to the task when created + /// Task completion result showing success, failure or if there was a crash + internal OutOfProcTaskHostTaskResult ExecuteTask + ( + IBuildEngine oopTaskHostNode, + string taskName, + string taskLocation, + string taskFile, + int taskLine, + int taskColumn, + AppDomainSetup appDomainSetup, + IDictionary taskParams + ) + { + _buildEngine = oopTaskHostNode; + _taskName = taskName; + + _taskAppDomain = null; + _wrappedTask = null; + + LoadedType taskType = null; + try + { + TypeLoader typeLoader = new TypeLoader(new TypeFilter(TaskLoader.IsTaskClass)); + taskType = typeLoader.Load(taskName, AssemblyLoadInfo.Create(null, taskLocation)); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Exception exceptionToReturn = e; + + // If it's a TargetInvocationException, we only care about the contents of the inner exception, + // so just save that instead. + if (e is TargetInvocationException) + { + exceptionToReturn = e.InnerException; + } + + return new OutOfProcTaskHostTaskResult + ( + TaskCompleteType.CrashedDuringInitialization, + exceptionToReturn, + "TaskInstantiationFailureError", + new string[] { taskName, taskLocation, String.Empty } + ); + } + + OutOfProcTaskHostTaskResult taskResult; + if (taskType.HasSTAThreadAttribute()) + { + taskResult = InstantiateAndExecuteTaskInSTAThread(oopTaskHostNode, taskType, taskName, taskLocation, taskFile, taskLine, taskColumn, appDomainSetup, taskParams); + } + else + { + taskResult = InstantiateAndExecuteTask(oopTaskHostNode, taskType, taskName, taskLocation, taskFile, taskLine, taskColumn, appDomainSetup, taskParams); + } + + return taskResult; + } + + /// + /// This is responsible for cleaning up the task after the OutOfProcTaskHostNode has gathered everything it needs from this execution + /// For example: We will need to hold on new AppDomains created until we finish getting all outputs from the task + /// Add any other cleanup tasks here. Any method calling ExecuteTask must remember to call CleanupTask. + /// + internal void CleanupTask() + { + if (_taskAppDomain != null) + { + AppDomain.Unload(_taskAppDomain); + } + + TaskLoader.RemoveAssemblyResolver(); + _wrappedTask = null; + } + + /// + /// Execute a task on the STA thread. + /// + /// + /// STA thread launching code lifted from XMakeBuildEngine\BackEnd\Components\RequestBuilder\TaskBuilder.cs, ExecuteTaskInSTAThread method. + /// Any bug fixes made to this code, please ensure that you also fix that code. + /// + private OutOfProcTaskHostTaskResult InstantiateAndExecuteTaskInSTAThread + ( + IBuildEngine oopTaskHostNode, + LoadedType taskType, + string taskName, + string taskLocation, + string taskFile, + int taskLine, + int taskColumn, + AppDomainSetup appDomainSetup, + IDictionary taskParams + ) + { + ManualResetEvent taskRunnerFinished = new ManualResetEvent(false); + OutOfProcTaskHostTaskResult taskResult = null; + Exception exceptionFromExecution = null; + + try + { + ThreadStart taskRunnerDelegate = delegate () + { + try + { + taskResult = InstantiateAndExecuteTask + ( + oopTaskHostNode, + taskType, + taskName, + taskLocation, + taskFile, + taskLine, + taskColumn, + appDomainSetup, + taskParams + ); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + exceptionFromExecution = e; + } + finally + { + taskRunnerFinished.Set(); + } + }; + + Thread staThread = new Thread(taskRunnerDelegate); + staThread.SetApartmentState(ApartmentState.STA); + staThread.Name = "MSBuild STA task runner thread"; + staThread.CurrentCulture = Thread.CurrentThread.CurrentCulture; + staThread.CurrentUICulture = Thread.CurrentThread.CurrentUICulture; + staThread.Start(); + + // TODO: Why not just Join on the thread??? + taskRunnerFinished.WaitOne(); + } + finally + { + taskRunnerFinished.Close(); + taskRunnerFinished = null; + } + + if (exceptionFromExecution != null) + { + // Unfortunately this will reset the callstack + throw exceptionFromExecution; + } + + return taskResult; + } + + /// + /// Do the work of actually instantiating and running the task. + /// + private OutOfProcTaskHostTaskResult InstantiateAndExecuteTask + ( + IBuildEngine oopTaskHostNode, + LoadedType taskType, + string taskName, + string taskLocation, + string taskFile, + int taskLine, + int taskColumn, + AppDomainSetup appDomainSetup, + IDictionary taskParams + ) + { + _taskAppDomain = null; + _wrappedTask = null; + + try + { + _wrappedTask = TaskLoader.CreateTask(taskType, taskName, taskFile, taskLine, taskColumn, new TaskLoader.LogError(LogErrorDelegate), appDomainSetup, true /* always out of proc */, out _taskAppDomain); + Type wrappedTaskType = _wrappedTask.GetType(); + + _wrappedTask.BuildEngine = oopTaskHostNode; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Exception exceptionToReturn = e; + + // If it's a TargetInvocationException, we only care about the contents of the inner exception, + // so just save that instead. + if (e is TargetInvocationException) + { + exceptionToReturn = e.InnerException; + } + + return new OutOfProcTaskHostTaskResult + ( + TaskCompleteType.CrashedDuringInitialization, + exceptionToReturn, + "TaskInstantiationFailureError", + new string[] { taskName, taskLocation, String.Empty } + ); + } + + foreach (KeyValuePair param in taskParams) + { + try + { + PropertyInfo paramInfo = _wrappedTask.GetType().GetProperty(param.Key, BindingFlags.Instance | BindingFlags.Public); + paramInfo.SetValue(_wrappedTask, (param.Value == null ? null : param.Value.WrappedParameter), null); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Exception exceptionToReturn = e; + + // If it's a TargetInvocationException, we only care about the contents of the inner exception, + // so just save that instead. + if (e is TargetInvocationException) + { + exceptionToReturn = e.InnerException; + } + + return new OutOfProcTaskHostTaskResult + ( + TaskCompleteType.CrashedDuringInitialization, + exceptionToReturn, + "InvalidTaskAttributeError", + new string[] { param.Key, param.Value.ToString(), taskName } + ); + } + } + + bool success = false; + try + { + if (CancelPending) + { + return new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure); + } + + // If it didn't crash and return before now, we're clear to go ahead and execute here. + success = _wrappedTask.Execute(); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + return new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, e); + } + + PropertyInfo[] finalPropertyValues = _wrappedTask.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + + IDictionary finalParameterValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (PropertyInfo value in finalPropertyValues) + { + // only record outputs + if (value.GetCustomAttributes(typeof(OutputAttribute), true).Length > 0) + { + try + { + finalParameterValues[value.Name] = value.GetValue(_wrappedTask, null); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // If it's not a critical exception, we assume there's some sort of problem in the parameter getter -- + // so save the exception, and we'll re-throw once we're back on the main node side of the + // communications pipe. + finalParameterValues[value.Name] = e; + } + } + } + + return new OutOfProcTaskHostTaskResult(success ? TaskCompleteType.Success : TaskCompleteType.Failure, finalParameterValues); + } + + /// + /// Logs errors from TaskLoader + /// + private void LogErrorDelegate(string taskLocation, int taskLine, int taskColumn, string message, params object[] messageArgs) + { + _buildEngine.LogErrorEvent(new BuildErrorEventArgs( + null, + null, + taskLocation, + taskLine, + taskColumn, + 0, + 0, + ResourceUtilities.FormatString(AssemblyResources.GetString(message), messageArgs), + null, + _taskName + ) + ); + } + } +} diff --git a/src/XMakeCommandLine/OutOfProcTaskHostNode.cs b/src/XMakeCommandLine/OutOfProcTaskHostNode.cs new file mode 100644 index 00000000000..ee892862ada --- /dev/null +++ b/src/XMakeCommandLine/OutOfProcTaskHostNode.cs @@ -0,0 +1,1117 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Class implementing an out-of-proc node for hosting tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Threading; +using System.Reflection; +using System.Runtime.CompilerServices; + +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using System.Runtime.Remoting; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This class represents an implementation of INode for out-of-proc node for hosting tasks. + /// + internal class OutOfProcTaskHostNode : MarshalByRefObject, INodePacketFactory, INodePacketHandler, +#if CLR2COMPATIBILITY + IBuildEngine3 +#else + IBuildEngine4 +#endif + { + /// + /// Keeps a record of all environment variables that, on startup of the task host, have a different + /// value from those that are passed to the task host in the configuration packet for the first task. + /// These environments are assumed to be effectively identical, so the only difference between the + /// two sets of values should be any environment variables that differ between e.g. a 32-bit and a 64-bit + /// process. Those are the variables that this dictionary should store. + /// + /// - The key into the dictionary is the name of the environment variable. + /// - The Key of the KeyValuePair is the value of the variable in the parent process -- the value that we + /// wish to ensure is replaced by whatever the correct value in our current process is. + /// - The Value of the KeyValuePair is the value of the variable in the current process -- the value that + /// we wish to replay the Key value with in the environment that we receive from the parent before + /// applying it to the current process. + /// + /// Note that either value in the KeyValuePair can be null, as it is completely possible to have an + /// environment variable that is set in 32-bit processes but not in 64-bit, or vice versa. + /// + /// This dictionary must be static because otherwise, if a node is sitting around waiting for reuse, it will + /// have inherited the environment from the previous build, and any differences between the two will be seen + /// as "legitimate". There is no way for us to know what the differences between the startup environment of + /// the previous build and the environment of the first task run in the task host in this build -- so we + /// must assume that the 4ish system environment variables that this is really meant to catch haven't + /// somehow magically changed between two builds spaced no more than 15 minutes apart. + /// + private static IDictionary> s_mismatchedEnvironmentValues; + + /// + /// The endpoint used to talk to the host. + /// + private NodeEndpointOutOfProcTaskHost _nodeEndpoint; + + /// + /// The packet factory. + /// + private NodePacketFactory _packetFactory; + + /// + /// The event which is set when we receive packets. + /// + private AutoResetEvent _packetReceivedEvent; + + /// + /// The queue of packets we have received but which have not yet been processed. + /// + private Queue _receivedPackets; + + /// + /// The current configuration for this task host. + /// + private TaskHostConfiguration _currentConfiguration; + + /// + /// The saved environment for the process. + /// + private IDictionary _savedEnvironment; + + /// + /// The event which is set when we should shut down. + /// + private ManualResetEvent _shutdownEvent; + + /// + /// The reason we are shutting down. + /// + private NodeEngineShutdownReason _shutdownReason; + + /// + /// We set this flag to track a currently executing task + /// + private bool _isTaskExecuting; + + /// + /// The event which is set when a task has completed. + /// + private AutoResetEvent _taskCompleteEvent; + + /// + /// Packet containing all the information relating to the + /// completed state of the task. + /// + private TaskHostTaskComplete _taskCompletePacket; + + /// + /// Object used to synchronize access to taskCompletePacket + /// + private Object _taskCompleteLock = new Object(); + + /// + /// The event which is set when a task is cancelled + /// + private ManualResetEvent _taskCancelledEvent; + + /// + /// The thread currently executing user task in the TaskRunner + /// + private Thread _taskRunnerThread; + + /// + /// This is the wrapper for the user task to be executed. + /// We are providing a wrapper to create a possibility of executing the task in a separate AppDomain + /// + private OutOfProcTaskAppDomainWrapper _taskWrapper; + + /// + /// Flag indicating if we should debug communications or not. + /// + private bool _debugCommunications; + + /// + /// Flag indicating whether we should modify the environment based on any differences we find between that of the + /// task host at startup and the environment passed to us in our initial task configuration packet. + /// + private bool _updateEnvironment; + + /// + /// An interim step between MSBuildTaskHostDoNotUpdateEnvironment=1 and the default update behavior: go ahead and + /// do all the updates that we would otherwise have done by default, but log any updates that are made (at low + /// importance) so that the user is aware. + /// + private bool _updateEnvironmentAndLog; + +#if !CLR2COMPATIBILITY + /// + /// The task object cache. + /// + private RegisteredTaskObjectCacheBase _registeredTaskObjectCache; +#endif + + /// + /// Constructor. + /// + public OutOfProcTaskHostNode() + { + // We don't know what the current build thinks this variable should be until RunTask(), but as a fallback in case there are + // communications before we get the configuration set up, just go with what was already in the environment from when this node + // was initially launched. + _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); + + AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(ExceptionHandling.UnhandledExceptionHandler); + + _receivedPackets = new Queue(); + + // These WaitHandles are disposed in HandleShutDown() + _packetReceivedEvent = new AutoResetEvent(false); + _shutdownEvent = new ManualResetEvent(false); + _taskCompleteEvent = new AutoResetEvent(false); + _taskCancelledEvent = new ManualResetEvent(false); + + _packetFactory = new NodePacketFactory(); + + INodePacketFactory thisINodePacketFactory = (INodePacketFactory)this; + + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostConfiguration, TaskHostConfiguration.FactoryForDeserialization, this); + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostTaskCancelled, TaskHostTaskCancelled.FactoryForDeserialization, this); + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); + } + + #region IBuildEngine Implementation (Properties) + + /// + /// Returns the value of ContinueOnError for the currently executing task. + /// + public bool ContinueOnError + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.ContinueOnError; + } + } + + /// + /// Returns the line number of the location in the project file of the currently executing task. + /// + public int LineNumberOfTaskNode + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.LineNumberOfTask; + } + } + + /// + /// Returns the column number of the location in the project file of the currently executing task. + /// + public int ColumnNumberOfTaskNode + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.ColumnNumberOfTask; + } + } + + /// + /// Returns the project file of the currently executing task. + /// + public string ProjectFileOfTaskNode + { + get + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!"); + return _currentConfiguration.ProjectFileOfTask; + } + } + + #endregion // IBuildEngine Implementation (Properties) + + #region IBuildEngine2 Implementation (Properties) + + /// + /// Stub implementation of IBuildEngine2.IsRunningMultipleNodes. The task host does not support this sort of + /// IBuildEngine callback, so error. + /// + public bool IsRunningMultipleNodes + { + get + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return false; + } + } + + #endregion // IBuildEngine2 Implementation (Properties) + + #region IBuildEngine Implementation (Methods) + + /// + /// Sends the provided error back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogErrorEvent(BuildErrorEventArgs e) + { + SendBuildEvent(e); + } + + /// + /// Sends the provided warning back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogWarningEvent(BuildWarningEventArgs e) + { + SendBuildEvent(e); + } + + /// + /// Sends the provided message back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogMessageEvent(BuildMessageEventArgs e) + { + SendBuildEvent(e); + } + + /// + /// Sends the provided custom event back to the parent node to be logged, tagging it with + /// the parent node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the parent node to begin with. + /// + public void LogCustomEvent(CustomBuildEventArgs e) + { + SendBuildEvent(e); + } + + /// + /// Stub implementation of IBuildEngine.BuildProjectFile. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return false; + } + + #endregion // IBuildEngine Implementation (Methods) + + #region IBuildEngine2 Implementation (Methods) + + /// + /// Stub implementation of IBuildEngine2.BuildProjectFile. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion) + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return false; + } + + /// + /// Stub implementation of IBuildEngine2.BuildProjectFilesInParallel. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + public bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion) + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return false; + } + + #endregion // IBuildEngine2 Implementation (Methods) + + #region IBuildEngine3 Implementation + + /// + /// Stub implementation of IBuildEngine3.BuildProjectFilesInParallel. The task host does not support IBuildEngine + /// callbacks for the purposes of building projects, so error. + /// + public BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] removeGlobalProperties, string[] toolsVersion, bool returnTargetOutputs) + { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); + return new BuildEngineResult(false, null); + } + + /// + /// Stub implementation of IBuildEngine3.Yield. The task host does not support yielding, so just go ahead and silently + /// return, letting the task continue. + /// + public void Yield() + { + return; + } + + /// + /// Stub implementation of IBuildEngine3.Reacquire. The task host does not support yielding, so just go ahead and silently + /// return, letting the task continue. + /// + public void Reacquire() + { + return; + } + + #endregion // IBuildEngine3 Implementation + +#if !CLR2COMPATIBILITY + #region IBuildEngine4 Implementation + + /// + /// Registers an object with the system that will be disposed of at some specified time + /// in the future. + /// + /// The key used to retrieve the object. + /// The object to be held for later disposal. + /// The lifetime of the object. + /// The object may be disposed earlier that the requested time if + /// MSBuild needs to reclaim memory. + public void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection) + { + _registeredTaskObjectCache.RegisterTaskObject(key, obj, lifetime, allowEarlyCollection); + } + + /// + /// Retrieves a previously registered task object stored with the specified key. + /// + /// The key used to retrieve the object. + /// The lifetime of the object. + /// + /// The registered object, or null is there is no object registered under that key or the object + /// has been discarded through early collection. + /// + public object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + return _registeredTaskObjectCache.GetRegisteredTaskObject(key, lifetime); + } + + /// + /// Unregisters a previously-registered task object. + /// + /// The key used to retrieve the object. + /// The lifetime of the object. + /// + /// The registered object, or null is there is no object registered under that key or the object + /// has been discarded through early collection. + /// + public object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime) + { + return _registeredTaskObjectCache.UnregisterTaskObject(key, lifetime); + } + + #endregion +#endif + + #region INodePacketFactory Members + + /// + /// Registers the specified handler for a particular packet type. + /// + /// The packet type. + /// The factory for packets of the specified type. + /// The handler to be called when packets of the specified type are received. + public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _packetFactory.RegisterPacketHandler(packetType, factory, handler); + } + + /// + /// Unregisters a packet handler. + /// + /// The packet type. + public void UnregisterPacketHandler(NodePacketType packetType) + { + _packetFactory.UnregisterPacketHandler(packetType); + } + + /// + /// Takes a serializer, deserializes the packet and routes it to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator containing the data from which the packet should be reconstructed. + public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, INodePacketTranslator translator) + { + _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + } + + /// + /// Routes the specified packet + /// + /// The node from which the packet was received. + /// The packet to route. + public void RoutePacket(int nodeId, INodePacket packet) + { + _packetFactory.RoutePacket(nodeId, packet); + } + + #endregion // INodePacketFactory Members + + #region INodePacketHandler Members + + /// + /// This method is invoked by the NodePacketRouter when a packet is received and is intended for + /// this recipient. + /// + /// The node from which the packet was received. + /// The packet. + public void PacketReceived(int node, INodePacket packet) + { + lock (_receivedPackets) + { + _receivedPackets.Enqueue(packet); + _packetReceivedEvent.Set(); + } + } + + #endregion // INodePacketHandler Members + + #region INode Members + + /// + /// Starts up the node and processes messages until the node is requested to shut down. + /// + /// The exception which caused shutdown, if any. + /// The reason for shutting down. + public NodeEngineShutdownReason Run(out Exception shutdownException) + { +#if !CLR2COMPATIBILITY + _registeredTaskObjectCache = new RegisteredTaskObjectCacheBase(); +#endif + shutdownException = null; + + // Snapshot the current environment + _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + + string pipeName = "MSBuild" + Process.GetCurrentProcess().Id; + + _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(pipeName); + _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged); + _nodeEndpoint.Listen(this); + + WaitHandle[] waitHandles = new WaitHandle[] { _shutdownEvent, _packetReceivedEvent, _taskCompleteEvent, _taskCancelledEvent }; + + while (true) + { + int index = WaitHandle.WaitAny(waitHandles); + switch (index) + { + case 0: // shutdownEvent + NodeEngineShutdownReason shutdownReason = HandleShutdown(); + return shutdownReason; + + case 1: // packetReceivedEvent + INodePacket packet = null; + + int packetCount = _receivedPackets.Count; + + while (packetCount > 0) + { + lock (_receivedPackets) + { + if (_receivedPackets.Count > 0) + { + packet = _receivedPackets.Dequeue(); + } + else + { + break; + } + } + + if (packet != null) + { + HandlePacket(packet); + } + } + + break; + case 2: // taskCompleteEvent + CompleteTask(); + break; + case 3: // taskCancelledEvent + CancelTask(); + break; + } + } + + // UNREACHABLE + } + #endregion + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + switch (packet.Type) + { + case NodePacketType.TaskHostConfiguration: + HandleTaskHostConfiguration(packet as TaskHostConfiguration); + break; + case NodePacketType.TaskHostTaskCancelled: + _taskCancelledEvent.Set(); + break; + case NodePacketType.NodeBuildComplete: + HandleNodeBuildComplete(packet as NodeBuildComplete); + break; + } + } + + /// + /// Configure the task host according to the information received in the + /// configuration packet + /// + private void HandleTaskHostConfiguration(TaskHostConfiguration taskHostConfiguration) + { + ErrorUtilities.VerifyThrow(_isTaskExecuting == false, "Why are we getting a TaskHostConfiguration packet while we're still executing a task?"); + _currentConfiguration = taskHostConfiguration; + + // Kick off the task running thread. + _taskRunnerThread = new Thread(new ParameterizedThreadStart(RunTask)); + _taskRunnerThread.Name = "Task runner for task " + taskHostConfiguration.TaskName; + _taskRunnerThread.Start(taskHostConfiguration); + } + + /// + /// The task has been completed + /// + private void CompleteTask() + { + ErrorUtilities.VerifyThrow(_isTaskExecuting == false, "The task should be done executing before CompleteTask."); + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + TaskHostTaskComplete taskCompletePacketToSend; + + lock (_taskCompleteLock) + { + ErrorUtilities.VerifyThrowInternalNull(_taskCompletePacket, "taskCompletePacket"); + taskCompletePacketToSend = _taskCompletePacket; + _taskCompletePacket = null; + } + + _nodeEndpoint.SendData(taskCompletePacketToSend); + } + + _currentConfiguration = null; + + // If the task has been canceled, the event will still be set. + // If so, now that we've completed the task, we want to shut down + // this node -- with no reuse, since we don't know whether the + // task we canceled left the node in a good state or not. + if (_taskCancelledEvent.WaitOne(0, false)) + { + _shutdownReason = NodeEngineShutdownReason.BuildComplete; + _shutdownEvent.Set(); + } + } + + /// + /// This task has been cancelled. Attempt to cancel the task + /// + private void CancelTask() + { + // If the task is an ICancellable task in CLR4 we will call it here and wait for it to complete + // Otherwise it's a classic ITask. + + // Store in a local to avoid a race + var wrapper = _taskWrapper; + if (wrapper != null && !wrapper.CancelTask()) + { + // Create a possibility for the task to be aborted if the user really wants it dropped dead asap + if (Environment.GetEnvironmentVariable("MSBUILDTASKHOSTABORTTASKONCANCEL") == "1") + { + // Don't bother aborting the task if it has passed the actual user task Execute() + // It means we're already in the process of shutting down - Wait for the taskCompleteEvent to be set instead. + if (_isTaskExecuting) + { + // The thread will be terminated crudely so our environment may be trashed but it's ok since we are + // shutting down ASAP. + _taskRunnerThread.Abort(); + } + } + } + } + + /// + /// Handles the NodeBuildComplete packet. + /// + private void HandleNodeBuildComplete(NodeBuildComplete buildComplete) + { + ErrorUtilities.VerifyThrow(!_isTaskExecuting, "We should never have a task in the process of executing when we receive NodeBuildComplete."); + + _shutdownReason = buildComplete.PrepareForReuse ? NodeEngineShutdownReason.BuildCompleteReuse : NodeEngineShutdownReason.BuildComplete; + _shutdownEvent.Set(); + } + + /// + /// Perform necessary actions to shut down the node. + /// + private NodeEngineShutdownReason HandleShutdown() + { + // Wait for the RunTask task runner thread before shutting down so that we can cleanly dispose all WaitHandles. + if (_taskRunnerThread != null) + { + _taskRunnerThread.Join(); + } + + if (_debugCommunications) + { + using (StreamWriter writer = File.CreateText(String.Format(CultureInfo.CurrentCulture, Path.Combine(Path.GetTempPath(), @"MSBuild_NodeShutdown_{0}.txt"), Process.GetCurrentProcess().Id))) + { + writer.WriteLine("Node shutting down with reason {0}.", _shutdownReason); + } + } + +#if !CLR2COMPATIBILITY + _registeredTaskObjectCache.DisposeCacheObjects(RegisteredTaskObjectLifetime.Build); + _registeredTaskObjectCache = null; +#endif + + // Restore the original current directory. + NativeMethodsShared.SetCurrentDirectory(Environment.SystemDirectory); + + // Restore the original environment. + CommunicationsUtilities.SetEnvironment(_savedEnvironment); + + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + // Notify the BuildManager that we are done. + _nodeEndpoint.SendData(new NodeShutdown(_shutdownReason == NodeEngineShutdownReason.Error ? NodeShutdownReason.Error : NodeShutdownReason.Requested)); + + // Flush all packets to the pipe and close it down. This blocks until the shutdown is complete. + _nodeEndpoint.OnLinkStatusChanged -= new LinkStatusChangedDelegate(OnLinkStatusChanged); + } + + _nodeEndpoint.Disconnect(); + + // Dispose these WaitHandles + _packetReceivedEvent.Close(); + _shutdownEvent.Close(); + _taskCompleteEvent.Close(); + _taskCancelledEvent.Close(); + + return _shutdownReason; + } + + /// + /// Event handler for the node endpoint's LinkStatusChanged event. + /// + private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + switch (status) + { + case LinkStatus.ConnectionFailed: + case LinkStatus.Failed: + _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; + _shutdownEvent.Set(); + break; + + case LinkStatus.Inactive: + break; + + default: + break; + } + } + + /// + /// Task runner method + /// + private void RunTask(object state) + { + _isTaskExecuting = true; + OutOfProcTaskHostTaskResult taskResult = null; + TaskHostConfiguration taskConfiguration = state as TaskHostConfiguration; + IDictionary taskParams = taskConfiguration.TaskParameters; + + // We only really know the values of these variables for sure once we see what we received from our parent + // environment -- otherwise if this was a completely new build, we could lose out on expected environment + // variables. + _debugCommunications = taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBUILDDEBUGCOMM", "1", StringComparison.OrdinalIgnoreCase); + _updateEnvironment = !taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBuildTaskHostDoNotUpdateEnvironment", "1", StringComparison.OrdinalIgnoreCase); + _updateEnvironmentAndLog = taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBuildTaskHostUpdateEnvironmentAndLog", "1", StringComparison.OrdinalIgnoreCase); + + try + { + // Change to the startup directory + NativeMethodsShared.SetCurrentDirectory(taskConfiguration.StartupDirectory); + + if (_updateEnvironment) + { + InitializeMismatchedEnvironmentTable(taskConfiguration.BuildProcessEnvironment); + } + + // Now set the new environment + SetTaskHostEnvironment(taskConfiguration.BuildProcessEnvironment); + + // Set culture + Thread.CurrentThread.CurrentCulture = taskConfiguration.Culture; + Thread.CurrentThread.CurrentUICulture = taskConfiguration.UICulture; + + string taskName = taskConfiguration.TaskName; + string taskLocation = taskConfiguration.TaskLocation; + + // We will not create an appdomain now because of a bug + // As a fix, we will create the class directly without wrapping it in a domain + _taskWrapper = new OutOfProcTaskAppDomainWrapper(); + + taskResult = _taskWrapper.ExecuteTask + ( + this as IBuildEngine, + taskName, + taskLocation, + taskConfiguration.ProjectFileOfTask, + taskConfiguration.LineNumberOfTask, + taskConfiguration.ColumnNumberOfTask, + taskConfiguration.AppDomainSetup, + taskParams + ); + } + catch (Exception e) + { + if (e is ThreadAbortException) + { + // This thread was aborted as part of Cancellation, we will return a failure task result + taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure); + } + else if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + else + { + taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, e); + } + } + finally + { + try + { + _isTaskExecuting = false; + + IDictionary currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + currentEnvironment = UpdateEnvironmentForMainNode(currentEnvironment); + + if (taskResult == null) + { + taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure); + } + + lock (_taskCompleteLock) + { + _taskCompletePacket = new TaskHostTaskComplete + ( + taskResult, + currentEnvironment + ); + } + + foreach (TaskParameter param in taskParams.Values) + { + // Tell remoting to forget connections to the parameter + RemotingServices.Disconnect(param); + } + + // Restore the original clean environment + CommunicationsUtilities.SetEnvironment(_savedEnvironment); + } + catch (Exception e) + { + lock (_taskCompleteLock) + { + // Create a minimal taskCompletePacket to carry the exception so that the TaskHostTask does not hang while waiting + _taskCompletePacket = new TaskHostTaskComplete(new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedAfterExecution, e), null); + } + } + finally + { + // Call CleanupTask to unload any domains and other necessary cleanup in the taskWrapper + _taskWrapper.CleanupTask(); + + // The task has now fully completed executing + _taskCompleteEvent.Set(); + } + } + } + + /// + /// Set the environment for the task host -- includes possibly munging the given + /// environment somewhat to account for expected environment differences between, + /// e.g. parent processes and task hosts of different bitnesses. + /// + private void SetTaskHostEnvironment(IDictionary environment) + { + ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues"); + IDictionary updatedEnvironment = null; + + if (_updateEnvironment) + { + foreach (string variable in s_mismatchedEnvironmentValues.Keys) + { + string oldValue = s_mismatchedEnvironmentValues[variable].Key; + string newValue = s_mismatchedEnvironmentValues[variable].Value; + + // We don't check the return value, because having the variable not exist == be + // null is perfectly valid, and mismatchedEnvironmentValues stores those values + // as null as well, so the String.Equals should still return that they are equal. + string environmentValue = null; + environment.TryGetValue(variable, out environmentValue); + + if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase)) + { + if (updatedEnvironment == null) + { + if (_updateEnvironmentAndLog) + { + LogMessageFromResource(MessageImportance.Low, "ModifyingTaskHostEnvironmentHeader"); + } + + updatedEnvironment = new Dictionary(environment, StringComparer.OrdinalIgnoreCase); + } + + if (newValue != null) + { + if (_updateEnvironmentAndLog) + { + LogMessageFromResource(MessageImportance.Low, "ModifyingTaskHostEnvironmentVariable", variable, newValue ?? String.Empty, environmentValue ?? String.Empty); + } + + updatedEnvironment[variable] = newValue; + } + else + { + updatedEnvironment.Remove(variable); + } + } + } + } + + // if it's still null here, there were no changes necessary -- so just + // set it to what was already passed in. + if (updatedEnvironment == null) + { + updatedEnvironment = environment; + } + + CommunicationsUtilities.SetEnvironment(updatedEnvironment); + } + + /// + /// Given the environment of the task host at the end of task execution, make sure that any + /// processor-specific variables have been re-applied in the correct form for the main node, + /// so that when we pass this dictionary back to the main node, all it should have to do + /// is just set it. + /// + private IDictionary UpdateEnvironmentForMainNode(IDictionary environment) + { + ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues"); + IDictionary updatedEnvironment = null; + + if (_updateEnvironment) + { + foreach (string variable in s_mismatchedEnvironmentValues.Keys) + { + // Since this is munging the property list for returning to the parent process, + // then the value we wish to replace is the one that is in this process, and the + // replacement value is the one that originally came from the parent process, + // instead of the other way around. + string oldValue = s_mismatchedEnvironmentValues[variable].Value; + string newValue = s_mismatchedEnvironmentValues[variable].Key; + + // We don't check the return value, because having the variable not exist == be + // null is perfectly valid, and mismatchedEnvironmentValues stores those values + // as null as well, so the String.Equals should still return that they are equal. + string environmentValue = null; + environment.TryGetValue(variable, out environmentValue); + + if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase)) + { + if (updatedEnvironment == null) + { + updatedEnvironment = new Dictionary(environment, StringComparer.OrdinalIgnoreCase); + } + + if (newValue != null) + { + updatedEnvironment[variable] = newValue; + } + else + { + updatedEnvironment.Remove(variable); + } + } + } + } + + // if it's still null here, there were no changes necessary -- so just + // set it to what was already passed in. + if (updatedEnvironment == null) + { + updatedEnvironment = environment; + } + + return updatedEnvironment; + } + + /// + /// Make sure the mismatchedEnvironmentValues table has been populated. Note that this should + /// only do actual work on the very first run of a task in the task host -- otherwise, it should + /// already have been populated. + /// + private void InitializeMismatchedEnvironmentTable(IDictionary environment) + { + if (s_mismatchedEnvironmentValues == null) + { + // This is the first time that we have received a TaskHostConfiguration packet, so we + // need to construct the mismatched environment table based on our current environment + // (assumed to be effectively identical to startup) and the environment we were given + // via the task host configuration, assumed to be effectively identical to the startup + // environment of the task host, given that the configuration packet is sent immediately + // after the node is launched. + s_mismatchedEnvironmentValues = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (string variable in _savedEnvironment.Keys) + { + string newValue = null; + string oldValue = _savedEnvironment[variable]; + if (!environment.TryGetValue(variable, out newValue)) + { + s_mismatchedEnvironmentValues[variable] = new KeyValuePair(null, oldValue); + } + else + { + if (!String.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase)) + { + s_mismatchedEnvironmentValues[variable] = new KeyValuePair(newValue, oldValue); + } + } + } + + foreach (string variable in environment.Keys) + { + string newValue = environment[variable]; + string oldValue = null; + if (!_savedEnvironment.TryGetValue(variable, out oldValue)) + { + s_mismatchedEnvironmentValues[variable] = new KeyValuePair(newValue, null); + } + else + { + if (!String.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase)) + { + s_mismatchedEnvironmentValues[variable] = new KeyValuePair(newValue, oldValue); + } + } + } + } + } + + /// + /// Sends the requested packet across to the main node. + /// + private void SendBuildEvent(BuildEventArgs e) + { + if (_nodeEndpoint != null && _nodeEndpoint.LinkStatus == LinkStatus.Active) + { + if (!e.GetType().IsSerializable) + { + // log a warning and bail. This will end up re-calling SendBuildEvent, but we know for a fact + // that the warning that we constructed is serializable, so everything should be good. + LogWarningFromResource("ExpectedEventToBeSerializable", e.GetType().Name); + return; + } + + _nodeEndpoint.SendData(new LogMessagePacket(new KeyValuePair(_currentConfiguration.NodeId, e))); + } + } + + /// + /// Generates the message event corresponding to a particular resource string and set of args + /// + private void LogMessageFromResource(MessageImportance importance, string messageResource, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log messages!"); + + // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+) + BuildMessageEventArgs message = new BuildMessageEventArgs + ( + ResourceUtilities.FormatString(AssemblyResources.GetString(messageResource), messageArgs), + null, + _currentConfiguration.TaskName, + importance + ); + + LogMessageEvent(message); + } + + /// + /// Generates the error event corresponding to a particular resource string and set of args + /// + private void LogWarningFromResource(string messageResource, params object[] messageArgs) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log warnings!"); + + // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+) + BuildWarningEventArgs warning = new BuildWarningEventArgs + ( + null, + null, + ProjectFileOfTaskNode, + LineNumberOfTaskNode, + ColumnNumberOfTaskNode, + 0, + 0, + ResourceUtilities.FormatString(AssemblyResources.GetString(messageResource), messageArgs), + null, + _currentConfiguration.TaskName + ); + + LogWarningEvent(warning); + } + + /// + /// Generates the error event corresponding to a particular resource string and set of args + /// + private void LogErrorFromResource(string messageResource) + { + ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log errors!"); + + // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+) + BuildErrorEventArgs error = new BuildErrorEventArgs + ( + null, + null, + ProjectFileOfTaskNode, + LineNumberOfTaskNode, + ColumnNumberOfTaskNode, + 0, + 0, + AssemblyResources.GetString(messageResource), + null, + _currentConfiguration.TaskName + ); + + LogErrorEvent(error); + } + } +} diff --git a/src/XMakeCommandLine/ProjectSchemaValidationHandler.cs b/src/XMakeCommandLine/ProjectSchemaValidationHandler.cs new file mode 100644 index 00000000000..63220ffcf3e --- /dev/null +++ b/src/XMakeCommandLine/ProjectSchemaValidationHandler.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Xml.Schema; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This class is used for validating projects against a designated schema. + /// + internal sealed class ProjectSchemaValidationHandler + { + // Set to true if there was a syntax error in the project file. + private bool _syntaxError; + + #region Methods + + /// + /// Validates a project file against the given schema. If no schema is given, validates + /// against the default schema + /// + /// Path of the file to validate. + /// Can be null. + /// Path to the framework directory where the default schema for + /// this ToolsVersion can be found. + /// True if the project was successfully validated against the given schema, false otherwise + internal static void VerifyProjectSchema + ( + string projectFile, + string schemaFile, + string binPath + ) + { + ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile"); + ErrorUtilities.VerifyThrowArgumentNull(binPath, "binPath"); + + if ((schemaFile == null) || (schemaFile.Length == 0)) + { + schemaFile = Path.Combine(binPath, "Microsoft.Build.xsd"); + } + + if (File.Exists(schemaFile)) + { + // Print the schema file we're using, particularly since it can vary + // according to the toolset being used + Console.WriteLine(AssemblyResources.GetString("SchemaFileLocation"), schemaFile); + } + else + { + // If we've gotten to this point, there is no schema to validate against -- just exit. + InitializationException.Throw + ( + ResourceUtilities.FormatResourceString("SchemaNotFoundErrorWithFile", schemaFile), + null /* No associated command line switch */ + ); + } + + ProjectSchemaValidationHandler validationHandler = new ProjectSchemaValidationHandler(); + + validationHandler.VerifyProjectSchema(projectFile, schemaFile); + } + + /// + /// Validates a project against the given schema. A schema file must be provided. + /// + private void VerifyProjectSchema + ( + string projectFile, + string schemaFile + ) + { + ErrorUtilities.VerifyThrowArgumentNull(schemaFile, "schemaFile"); + ErrorUtilities.VerifyThrowArgumentNull(projectFile, "projectFile"); + + // Options for XmlReader object can be set only in constructor. After the object is created, they + // become read-only. Because of that we need to create + // XmlSettings structure, fill it in with correct parameters and pass into XmlReader constructor. + + XmlReaderSettings validatorSettings = new XmlReaderSettings(); + validatorSettings.ValidationType = ValidationType.Schema; + validatorSettings.XmlResolver = null; + validatorSettings.ValidationEventHandler += new ValidationEventHandler(this.OnSchemaValidationError); + + XmlTextReader schemaReader = new XmlTextReader(schemaFile); + schemaReader.DtdProcessing = DtdProcessing.Ignore; + + using (schemaReader) + { + try + { + validatorSettings.Schemas.Add(XMakeAttributes.defaultXmlNamespace, schemaReader); + + // We need full path to the project file to be able handle it as URI in ValidationEventHandler. + // Uri class cannot instantiate with relative paths. + projectFile = Path.GetFullPath(projectFile); + + using (StreamReader contentReader = new StreamReader(projectFile)) + { + using (XmlReader validator = XmlReader.Create(contentReader, validatorSettings, projectFile)) // May also throw XmlSchemaException + { + _syntaxError = false; + bool couldRead = true; + + while (couldRead) + { + try + { + couldRead = validator.Read(); + } + catch (XmlException) + { + // We swallow exception here because XmlValidator fires the validation event to report the error + // And we handle the event. Also XmlValidator can continue parsing Xml text after throwing an exception. + // Thus we don't need any special recover here. + } + } + + VerifyThrowInitializationExceptionWithResource + ( + !_syntaxError, + projectFile, + 0 /* line */, + 0 /* end line */, + 0 /* column */, + 0 /* end column */, + "ProjectSchemaErrorHalt" + ); + } + } + } + // handle errors in the schema itself + catch (XmlException e) + { + ThrowInitializationExceptionWithResource + ( + (e.SourceUri.Length == 0) ? String.Empty : new Uri(e.SourceUri).LocalPath, + e.LineNumber, + 0 /* end line */, + e.LinePosition, + 0 /* end column */, + "InvalidSchemaFile", + schemaFile, + e.Message + ); + } + // handle errors in the schema itself + catch (XmlSchemaException e) + { + ThrowInitializationExceptionWithResource + ( + (e.SourceUri.Length == 0) ? String.Empty : new Uri(e.SourceUri).LocalPath, + e.LineNumber, + 0 /* end line */, + e.LinePosition, + 0 /* end column */, + "InvalidSchemaFile", + schemaFile, + e.Message + ); + } + } + } + + /// + /// Given the parameters passed in, if the condition is false, builds an + /// error message and throws an InitializationException with that message. + /// + private static void VerifyThrowInitializationExceptionWithResource + ( + bool condition, + string projectFile, + int fileLine, + int fileEndLine, + int fileColumn, + int fileEndColumn, + string resourceName, + params object[] args + ) + { + if (!condition) + { + ThrowInitializationExceptionWithResource + ( + projectFile, + fileLine, + fileEndLine, + fileColumn, + fileEndColumn, + resourceName, + args + ); + } + } + + /// + /// Given the parameters passed in, builds an error message and throws an + /// InitializationException with that message. + /// + private static void ThrowInitializationExceptionWithResource + ( + string projectFile, + int fileLine, + int fileEndLine, + int fileColumn, + int fileEndColumn, + string resourceName, + params object[] args + ) + { + InitializationException.Throw + ( + BuildStringFromResource + ( + projectFile, + fileLine, + fileEndLine, + fileColumn, + fileEndColumn, + resourceName, + args + ), + null /* No associated command line switch */ + ); + } + + /// + /// Given a resource string and information about a file, builds up a string + /// containing the message. + /// + private static string BuildStringFromResource + ( + string projectFile, + int fileLine, + int fileEndLine, + int fileColumn, + int fileEndColumn, + string resourceName, + params object[] args + ) + { + string errorCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out errorCode, out helpKeyword, resourceName, args); + + return EventArgsFormatting.FormatEventMessage + ( + "error", + AssemblyResources.GetString("SubCategoryForSchemaValidationErrors"), + message, + errorCode, + projectFile, + fileLine, + fileEndLine, + fileColumn, + fileEndColumn, + 0 /* thread id */ + ); + } + + #endregion // Methods + + #region Event Handlers + + /// + /// Receives any errors that occur while validating the project's schema. + /// + /// + /// + private void OnSchemaValidationError(object sender, ValidationEventArgs args) + { + _syntaxError = true; + + // We should handle empty URI specially, because Uri class does not allow to instantiate with empty string. + string filePath = String.Empty; + + if (args.Exception.SourceUri.Length != 0) + { + filePath = (new Uri(args.Exception.SourceUri)).LocalPath; + } + + Console.WriteLine + ( + BuildStringFromResource + ( + filePath, + args.Exception.LineNumber, + 0 /* end line */, + args.Exception.LinePosition, + 0 /* end column */, + "SchemaValidationError", + args.Exception.Message + ) + ); + } + + #endregion // Event Handlers + } +} diff --git a/src/XMakeCommandLine/Strings.resx b/src/XMakeCommandLine/Strings.resx new file mode 100644 index 00000000000..a816ca326b0 --- /dev/null +++ b/src/XMakeCommandLine/Strings.resx @@ -0,0 +1,949 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + MSBUILD : error MSB1011: Specify which project or solution file to use because this folder contains more than one project or solution file. + {StrBegin="MSBUILD : error MSB1011: "}UE: If no project or solution file is explicitly specified on the MSBuild.exe command-line, then the engine searches for a + project or solution file in the current directory by looking for *.*PROJ and *.SLN. If more than one file is found that matches this wildcard, we + fire this error. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : Configuration error {0}: {1} + {SubString="Configuration"}UE: This prefixes any error from reading the toolset definitions in msbuild.exe.config or the registry. + There's no error code because one was included in the error message. + LOCALIZATION: The word "Configuration" should be localized, the words "MSBuild" and "error" should NOT be localized. + + + + MSBUILD : error MSB1027: The /noautoresponse switch cannot be specified in the MSBuild.rsp auto-response file, nor in any response file that is referenced by the auto-response file. + {StrBegin="MSBUILD : error MSB1027: "}LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:", "/noautoresponse" and "MSBuild.rsp" should not be localized. + + + Microsoft (R) Build Engine version {0} +Copyright (C) Microsoft Corporation. All rights reserved. + + LOCALIZATION: {0} contains the DLL version number + + + MSBUILD : error MSB1008: Only one project can be specified. + {StrBegin="MSBUILD : error MSB1008: "}UE: This happens if the user does something like "msbuild.exe myapp.proj myapp2.proj". This is not allowed. + MSBuild.exe will only build a single project. The help topic may link to an article about how to author an MSBuild project + that itself launches MSBuild on a number of other projects. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1025: An internal failure occurred while running MSBuild. + {StrBegin="MSBUILD : error MSB1025: "}UE: This message is shown when the application has to terminate either because of a bug in the code, or because some + FX/CLR method threw an unexpected exception. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" and "MSBuild" should not be localized. + + + Syntax: MSBuild.exe [options] [project file] + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + Description: Builds the specified targets in the project file. If + a project file is not specified, MSBuild searches the + current working directory for a file that has a file + extension that ends in "proj" and uses that file. + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + Switches: + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /help Display this usage message. (Short form: /? or /h) + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /nologo Do not display the startup banner and copyright message. + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /version Display version information only. (Short form: /ver) + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + @<file> Insert command-line settings from a text file. To specify + multiple response files, specify each response file + separately. + + Any response files named "msbuild.rsp" are automatically + consumed from the following locations: + (1) the directory of msbuild.exe + (2) the directory of the first project or solution built + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /noautoresponse Do not auto-include any MSBuild.rsp files. (Short form: + /noautorsp) + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /target:<targets> Build these targets in this project. Use a semicolon or a + comma to separate multiple targets, or specify each + target separately. (Short form: /t) + Example: + /target:Resources;Compile + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /property:<n>=<v> Set or override these project-level properties. <n> is + the property name, and <v> is the property value. Use a + semicolon or a comma to separate multiple properties, or + specify each property separately. (Short form: /p) + Example: + /property:WarningLevel=2;OutDir=bin\Debug\ + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /logger:<logger> Use this logger to log events from MSBuild. To specify + multiple loggers, specify each logger separately. + The <logger> syntax is: + [<logger class>,]<logger assembly>[;<logger parameters>] + The <logger class> syntax is: + [<partial or full namespace>.]<logger class name> + The <logger assembly> syntax is: + {<assembly name>[,<strong name>] | <assembly file>} + The <logger parameters> are optional, and are passed + to the logger exactly as you typed them. (Short form: /l) + Examples: + /logger:XMLLogger,MyLogger,Version=1.0.2,Culture=neutral + /logger:XMLLogger,C:\Loggers\MyLogger.dll;OutputAsHTML + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /verbosity:<level> Display this amount of information in the event log. + The available verbosity levels are: q[uiet], m[inimal], + n[ormal], d[etailed], and diag[nostic]. (Short form: /v) + Example: + /verbosity:quiet + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /consoleloggerparameters:<parameters> + Parameters to console logger. (Short form: /clp) + The available parameters are: + PerformanceSummary--Show time spent in tasks, targets + and projects. + Summary--Show error and warning summary at the end. + NoSummary--Don't show error and warning summary at the + end. + ErrorsOnly--Show only errors. + WarningsOnly--Show only warnings. + NoItemAndPropertyList--Don't show list of items and + properties at the start of each project build. + ShowCommandLine--Show TaskCommandLineEvent messages + ShowTimestamp--Display the Timestamp as a prefix to any + message. + ShowEventId--Show eventId for started events, finished + events, and messages + ForceNoAlign--Does not align the text to the size of + the console buffer + DisableConsoleColor--Use the default console colors + for all logging messages. + DisableMPLogging-- Disable the multiprocessor + logging style of output when running in + non-multiprocessor mode. + EnableMPLogging--Enable the multiprocessor logging + style even when running in non-multiprocessor + mode. This logging style is on by default. + Verbosity--overrides the /verbosity setting for this + logger. + Example: + /consoleloggerparameters:PerformanceSummary;NoSummary; + Verbosity=minimal + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /noconsolelogger Disable the default console logger and do not log events + to the console. (Short form: /noconlog) + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /validate Validate the project against the default schema. (Short + form: /val) + + /validate:<schema> Validate the project against the specified schema. (Short + form: /val) + Example: + /validate:MyExtendedBuildSchema.xsd + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /maxcpucount[:n] Specifies the maximum number of concurrent processes to + build with. If the switch is not used, the default + value used is 1. If the switch is used without a value + MSBuild will use up to the number of processors on the + computer. (Short form: /m[:n]) + + + LOCALIZATION: "maxcpucount" should not be localized. + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + Examples: + + MSBuild MyApp.sln /t:Rebuild /p:Configuration=Release + MSBuild MyApp.csproj /t:Clean + /p:Configuration=Debug;TargetFrameworkVersion=v3.5 + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + For switch syntax, type "MSBuild /help" + UE: this message is shown when the user makes a syntax error on the command-line for a switch. + LOCALIZATION: "MSBuild /help" should not be localized. + + + + + /distributedlogger:<central logger>*<forwarding logger> + Use this logger to log events from MSBuild, attaching a + different logger instance to each node. To specify + multiple loggers, specify each logger separately. + (Short form /dl) + The <logger> syntax is: + [<logger class>,]<logger assembly>[;<logger parameters>] + The <logger class> syntax is: + [<partial or full namespace>.]<logger class name> + The <logger assembly> syntax is: + {<assembly name>[,<strong name>] | <assembly file>} + The <logger parameters> are optional, and are passed + to the logger exactly as you typed them. (Short form: /l) + Examples: + /dl:XMLLogger,MyLogger,Version=1.0.2,Culture=neutral + /dl:MyLogger,C:\My.dll*ForwardingLogger,C:\Logger.dll + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg chars. + + + + /ignoreprojectextensions:<extensions> + List of extensions to ignore when determining which + project file to build. Use a semicolon or a comma + to separate multiple extensions. + (Short form: /ignore) + Example: + /ignoreprojectextensions:.sln + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + + /toolsversion:<version> + The version of the MSBuild Toolset (tasks, targets, etc.) + to use during build. This version will override the + versions specified by individual projects. (Short form: + /tv) + Example: + /toolsversion:3.5 + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + + /fileLogger[n] Logs the build output to a file. By default + the file is in the current directory and named + "msbuild[n].log". Events from all nodes are combined into + a single log. The location of the file and other + parameters for the fileLogger can be specified through + the addition of the "/fileLoggerParameters[n]" switch. + "n" if present can be a digit from 1-9, allowing up to + 10 file loggers to be attached. (Short form: /fl[n]) + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /distributedFileLogger + Logs the build output to multiple log files, one log file + per MSBuild node. The initial location for these files is + the current directory. By default the files are called + "MSBuild<nodeid>.log". The location of the files and + other parameters for the fileLogger can be specified + with the addition of the "/fileLoggerParameters" switch. + + If a log file name is set through the fileLoggerParameters + switch the distributed logger will use the fileName as a + template and append the node id to this fileName to + create a log file for each node. + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /fileloggerparameters[n]:<parameters> + Provides any extra parameters for file loggers. + The presence of this switch implies the + corresponding /filelogger[n] switch. + "n" if present can be a digit from 1-9. + /fileloggerparameters is also used by any distributed + file logger, see description of /distributedFileLogger. + (Short form: /flp[n]) + The same parameters listed for the console logger are + available. Some additional available parameters are: + LogFile--path to the log file into which the + build log will be written. + Append--determines if the build log will be appended + to or overwrite the log file. Setting the + switch appends the build log to the log file; + Not setting the switch overwrites the + contents of an existing log file. + The default is not to append to the log file. + Encoding--specifies the encoding for the file, + for example, UTF-8, Unicode, or ASCII + Default verbosity is Detailed. + Examples: + /fileLoggerParameters:LogFile=MyLog.log;Append; + Verbosity=diagnostic;Encoding=UTF-8 + + /flp:Summary;Verbosity=minimal;LogFile=msbuild.sum + /flp1:warningsonly;logfile=msbuild.wrn + /flp2:errorsonly;logfile=msbuild.err + + + LOCALIZATION: The following should not be localized: + 1) "MSBuild", "MSBuild.exe" and "MSBuild.rsp" + 2) the string "proj" that describes the extension we look for + 3) all switch names and their short forms e.g. /property, or /p + 4) all verbosity levels and their short forms e.g. quiet, or q + LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + + + /nodeReuse:<parameters> + Enables or Disables the reuse of MSBuild nodes. + The parameters are: + True --Nodes will remain after the build completes + and will be reused by subsequent builds (default) + False--Nodes will not remain after the build completes + (Short form: /nr) + Example: + /nr:true + + + + /preprocess[:file] + Creates a single, aggregated project file by + inlining all the files that would be imported during a + build, with their boundaries marked. This can be + useful for figuring out what files are being imported + and from where, and what they will contribute to + the build. By default the output is written to + the console window. If the path to an output file + is provided that will be used instead. + (Short form: /pp) + Example: + /pp:out.txt + + + + /detailedsummary + Shows detailed information at the end of the build + about the configurations built and how they were + scheduled to nodes. + (Short form: /ds) + + + + /debug + Causes a debugger prompt to appear immediately so that + Visual Studio can be attached for you to debug the + MSBuild XML and any tasks and loggers it uses. + + + + MSBUILD : Configuration error MSB1043: The application could not start. {0} + + {StrBegin="MSBUILD : Configuration error MSB1043: "} + UE: This error is shown when the msbuild.exe.config file had invalid content. + LOCALIZATION: The prefix "MSBUILD : Configuration error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1019: Logger switch was not correctly formed. + {StrBegin="MSBUILD : error MSB1019: "}UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user does any of the following: + msbuild.exe /logger:;"logger parameters" (missing logger class and assembly) + msbuild.exe /logger:loggerclass, (missing logger assembly) + msbuild.exe /logger:loggerclass,;"logger parameters" (missing logger assembly) + The correct way to specify a logger is to give both the logger class and logger assembly, or just the logger assembly (logger + parameters are optional). + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1030: Maximum CPU count is not valid. {0} + + {StrBegin="MSBUILD : error MSB1030: "} + UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies an invalid CPU value. For example, /m:foo instead of /m:2. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1032: Maximum CPU count is not valid. Value must be an integer greater than zero and no more than 1024. + {StrBegin="MSBUILD : error MSB1032: "} + UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies a CPU value that is zero or less. For example, /m:0 instead of /m:2. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1033: Node number is not valid. {0}. + + {StrBegin="MSBUILD : error MSB1033: "} + UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies a CPU value that is zero or less. For example, /nodemode:foo instead of /nodemode:2. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1034: Node number is not valid. Value must be an integer greater than zero. + {StrBegin="MSBUILD : error MSB1034: "} + UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies a CPU value that is zero or less. For example, /nodemode:0 instead of /nodemode:2. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1006: Property is not valid. + + {StrBegin="MSBUILD : error MSB1006: "}UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown if the user does any of the following: + msbuild.exe /property:foo (missing property value) + msbuild.exe /property:=4 (missing property name) + The user must pass in an actual property name and value following the switch, as in "msbuild.exe /property:Configuration=Debug". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : MSB1046: The schema "{0}" is not valid. {1} + {StrBegin="MSBUILD : MSB1046: "}UE: This message is shown when the schema file provided for the validation of a project is itself not valid. + LOCALIZATION: "{0}" is the schema file path. "{1}" is a message from an FX exception that describes why the schema file is bad. + + + Switch: {0} + + UE: This is attached to error messages caused by an invalid switch. This message indicates what the invalid arg was. + For example, if an unknown switch is passed to MSBuild.exe, the error message will look like this: + MSBUILD : error MSB1001: Unknown switch. + Switch: /bogus + LOCALIZATION: {0} contains the invalid switch text. + + + + MSBUILD : error MSB1040: ToolsVersion is not valid. {0} + + {StrBegin="MSBUILD : error MSB1040: "} + UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies an unknown toolversion, eg /toolsversion:99 + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1018: Verbosity level is not valid. + + {StrBegin="MSBUILD : error MSB1018: "}UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies an unknown verbosity level e.g. "msbuild /verbosity:foo". The only valid verbosities + (and their short forms) are: q[uiet], m[inimal], n[ormal], d[etailed], diag[nostic]. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1028: The logger failed unexpectedly. + {StrBegin="MSBUILD : error MSB1028: "} + UE: This error is shown when a logger specified with the /logger switch throws an exception while being + initialized. This message is followed by the exception text including the stack trace. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : Logger error {0}: {1} + UE: This prefixes the error message emitted by a logger, when a logger fails in a controlled way using a LoggerException. + For example, the logger is indicating that it could not create its output file. + There's no error code because one was supplied by the logger. + LOCALIZATION: The word "Logger" should be localized, the words "MSBuild" and "error" should NOT be localized. + + + + MSBUILD : Logger error MSB1029: {0} + {SubString="Logger", "{0}"}{StrBegin="MSBUILD : "} + UE: This prefixes the error message emitted by a logger, when a logger fails in a controlled way using a LoggerException. + For example, the logger is indicating that it could not create its output file. + This is like LoggerFailurePrefixNoErrorCode, but the logger didn't supply its own error code, so we have to provide one. + LOCALIZATION: The word "Logger" should be localized, the words "MSBuild" and "error" should NOT be localized. + + + + MSBUILD : error MSB1007: Specify a logger. + + {StrBegin="MSBUILD : error MSB1007: "}UE: This happens if the user does something like "msbuild.exe /logger". The user must pass in an actual logger class + following the switch, as in "msbuild.exe /logger:XMLLogger,MyLogger,Version=1.0.2,Culture=neutral". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1031: Specify the maximum number of CPUs. + + {StrBegin="MSBUILD : error MSB1031: "}UE: This happens if the user does something like "msbuild.exe /m". The user must pass in an actual number like /m:4. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file. + + {StrBegin="MSBUILD : error MSB1003: "}UE: The user must either specify a project or solution file to build, or there must be a project file in the current directory + with a file extension ending in "proj" (e.g., foo.csproj), or a solution file ending in "sln". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1005: Specify a property and its value. + + {StrBegin="MSBUILD : error MSB1005: "}UE: This happens if the user does something like "msbuild.exe /property". The user must pass in an actual property + name and value following the switch, as in "msbuild.exe /property:Configuration=Debug". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1012: Specify a response file. + + {StrBegin="MSBUILD : error MSB1012: "}UE: This error would occur if the user did something like "msbuild.exe @ foo.proj". The at-sign must be followed by a + response file. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1004: Specify the name of the target. + + {StrBegin="MSBUILD : error MSB1004: "}UE: This happens if the user does something like "msbuild.exe /target". The user must pass in an actual target name + following the switch, as in "msbuild.exe /target:blah". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1039: Specify the version of the toolset. + + {StrBegin="MSBUILD : error MSB1039: "} + UE: This happens if the user does something like "msbuild.exe /ToolsVersion". The user must pass in an actual toolsversion + name following the switch, as in "msbuild.exe /ToolsVersion:3.5". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1016: Specify the verbosity level. + + {StrBegin="MSBUILD : error MSB1016: "}UE: This happens if the user does something like "msbuild.exe /verbosity". The user must pass in a verbosity level + after the switch e.g. "msbuild.exe /verbosity:detailed". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1024: Only one schema can be specified for validation of the project. + + {StrBegin="MSBUILD : error MSB1024: "}UE: The user did something like msbuild /validate:foo.xsd /validate:bar.xsd. We only allow one schema to be specified. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + Some command line switches were read from the auto-response file "{0}". To disable this file, use the "/noautoresponse" switch. + + UE: This message appears in high verbosity modes when we used some + switches from the auto-response file msbuild.rsp: otherwise the user may be unaware + where the switches are coming from. + + + + MSBUILD : error MSB1009: Project file does not exist. + {StrBegin="MSBUILD : error MSB1009: "}UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + Building the projects in this solution one at a time. To enable parallel build, please add the "/m" switch. + + + MSBUILD : MSB1045: Stopping because of syntax errors in project file. + {StrBegin="MSBUILD : MSB1045: "} + + + MSBUILD : error MSB1023: Cannot read the response file. {0} + {StrBegin="MSBUILD : error MSB1023: "}UE: This error is shown when the response file cannot be read off disk. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. {0} contains a localized message explaining + why the response file could not be read -- this message comes from the CLR/FX. + + + MSBUILD : error MSB1013: The response file was specified twice. A response file can be specified only once. Any files named "msbuild.rsp" in the directory of MSBuild.exe or in the directory of the first project or solution built (which if no project or solution is specified is the current working directory) were automatically used as response files. + {StrBegin="MSBUILD : error MSB1013: "}UE: Response files are just text files that contain a bunch of command-line switches to be passed to MSBuild.exe. The + purpose is so you don't have to type the same switches over and over again ... you can just pass in the response file instead. + Response files can include the @ switch in order to further include other response files. In order to prevent a circular + reference here, we disallow the same response file from being included twice. This error message would be followed by the + exact @ switch that resulted in the duplicate response file. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1022: Response file does not exist. + {StrBegin="MSBUILD : error MSB1022: "}UE: This message would show if the user did something like "msbuild @bogus.rsp" where bogus.rsp doesn't exist. This + message does not need in-line parameters because the exception takes care of displaying the invalid arg. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + Validating project using schema file "{0}". + LOCALIZATION: "{0}" is the location of the schema file. + + + MSBUILD : MSB1044: Project is not valid. {0} + {StrBegin="MSBUILD : MSB1044: "}UE: This error is shown when the user asks his project to be validated against a schema (/val switch for + MSBuild.exe), and the project has errors. "{0}" contains a message explaining the problem. + LOCALIZATION: "{0}" is a message from the System.XML schema validator and is already localized. + + + MSBUILD : error MSB1026: Schema file does not exist. + {StrBegin="MSBUILD : error MSB1026: "}UE: This error is shown when the user specifies a schema file using the /validate:<schema> switch, and the file + does not exist on disk. This message does not need in-line parameters because the exception takes care of displaying the + invalid arg. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1026: Schema file '{0}' does not exist. + {StrBegin="MSBUILD : error MSB1026: "}UE: This error is printed if the default schema does not exist or in the extremely unlikely event + that an explicit schema file was passed and existed when the command line parameters were checked but was deleted from disk before this check was made. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1002: This switch does not take any parameters. + {StrBegin="MSBUILD : error MSB1002: "}UE: For example, if somebody types "msbuild.exe /nologo:1", they would get this error because the /nologo switch + should not be followed by any parameters ... it stands alone. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1001: Unknown switch. + {StrBegin="MSBUILD : error MSB1001: "}UE: This occurs when the user passes in an unrecognized switch on the MSBuild.exe command-line. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1015: MSBuild does not run on this version of the operating system. It is only supported on Windows 2000, Windows XP, and later versions. + {StrBegin="MSBUILD : error MSB1015: "}LOCALIZATION: The error prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + Forcing load of Microsoft.Build.Engine because MSBUILDOLDOM=1... + + + MSBUILD : error MSB1035: Specify the project extensions to ignore. + {StrBegin="MSBUILD : error MSB1035: "} + UE: This happens if the user does something like "msbuild.exe /IgnoreProjectextensions". The user must pass in one or more + project extensions to ignore e.g. "msbuild.exe /IgnoreProjectExtensions:.sln". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1036: There is an invalid extension in the /ignoreprojectextensions list. Extensions must start with a period ".", have one or more characters after the period and not contain any invalid path characters or wildcards. + {StrBegin="MSBUILD : error MSB1036: "}LOCALIZATION: The error prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + MSBUILD : error MSB1037: Specify one or more parameters for the console logger if using the /consoleLoggerParameters switch + {StrBegin="MSBUILD : error MSB1037: "} + UE: This happens if the user does something like "msbuild.exe /consoleLoggerParameters:". The user must pass in one or more parameters + after the switch e.g. "msbuild.exe /consoleLoggerParameters:ErrorSummary". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1038: Specify one or more parameters for the file logger if using the /fileLoggerParameters switch + {StrBegin="MSBUILD : error MSB1038: "} + UE: This happens if the user does something like "msbuild.exe /fileLoggerParameters:". The user must pass in one or more parameters + after the switch e.g. "msbuild.exe /fileLoggerParameters:logfile=c:\temp\logfile". + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1041: Specify one or more parameters for node reuse if using the /nodereuse switch + {StrBegin="MSBUILD : error MSB1041: "} + UE: This happens if the user does something like "msbuild.exe /nodereuse:" without a true or false + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB1042: Node reuse value is not valid. {0}. + {StrBegin="MSBUILD : error MSB1042: "} + UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies a node reuse value that is not equivilant to Boolean.TrueString or Boolean.FalseString. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + Attempting to cancel the build... + + + MSBUILD : error MSB1047: File to preprocess to is not valid. {0} + {StrBegin="MSBUILD : error MSB1047: "} + + + + MSBUILD : error MSB1021: Cannot create an instance of the logger. {0} + {StrBegin="MSBUILD : error MSB1021: "} + UE: This error is shown when a logger cannot be loaded and instantiated from its assembly. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. {0} contains a message explaining why the + logger could not be created -- this message comes from the CLR/FX and is localized. + + + MSBUILD : error MSB1020: The logger was not found. Check the following: 1.) The logger name specified is the same as the name of the logger class. 2.) The logger class is "public" and implements the Microsoft.Build.Framework.ILogger interface. 3.) The path to the logger assembly is correct, or the logger can be loaded using only the assembly name provided. + + {StrBegin="MSBUILD : error MSB1020: "}UE: This message does not need in-line parameters because the exception takes care of displaying the invalid arg. + This error is shown when a user specifies an logger that does not exist e.g. "msbuild /logger:FooLoggerClass,FooAssembly". The + logger class must exist in the given assembly. + LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + + + MSBUILD : error MSB4192: The project file "{0}" is in the ".vcproj" or ".dsp" file format, which MSBuild cannot build directly. Please convert the project by opening it in the Visual Studio IDE or running the conversion tool, or, for ".vcproj", use MSBuild to build the solution file containing the project instead. + {StrBegin="MSBUILD : error MSB4192: "} LOC: ".vcproj" and ".dsp" should not be localized + + + If MSBuild debugging does not work correctly, please verify that the "Just My Code" feature is enabled in Visual Studio, and that you have selected the managed debugger. + + + MSBUILD : error MSB1048: Solution files cannot be debugged directly. Run MSBuild first with an environment variable MSBUILDEMITSOLUTION=1 to create a corresponding ".sln.metaproj" file. Then debug that. + {StrBegin="MSBUILD : error MSB1048: "} LOC: ".SLN" should not be localized + + + + + + + + + + Build started. + + + {0} ({1},{2}) + A file location to be embedded in a string. + + + + diff --git a/src/XMakeCommandLine/UnitTests/CommandLineSwitchException_Tests.cs b/src/XMakeCommandLine/UnitTests/CommandLineSwitchException_Tests.cs new file mode 100644 index 00000000000..0add3a536e1 --- /dev/null +++ b/src/XMakeCommandLine/UnitTests/CommandLineSwitchException_Tests.cs @@ -0,0 +1,61 @@ +using Microsoft.Build.CommandLine; +using NUnit.Framework; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +namespace Microsoft.Build.UnitTests +{ + [TestFixture] + public class CommandLineSwitchException_Tests + { + /// + /// Verify ISerializable is implemented correctly + /// + [Test] + public void SerializeDeserialize() + { + try + { + CommandLineSwitchException.Throw("InvalidNodeNumberValueIsNegative", "commandLineArg"); + } + catch (CommandLineSwitchException e) + { + using (MemoryStream memStream = new MemoryStream()) + { + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(memStream, e); + memStream.Position = 0; + + CommandLineSwitchException e2 = (CommandLineSwitchException)formatter.Deserialize(memStream); + + Assert.AreEqual(e.Message, e2.Message); + } + } + } + + /// + /// Verify ISerializable is implemented correctly + /// + [Test] + public void SerializeDeserialize2() + { + try + { + CommandLineSwitchException.Throw("InvalidNodeNumberValueIsNegative", null); + } + catch (CommandLineSwitchException e) + { + using (MemoryStream memStream = new MemoryStream()) + { + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(memStream, e); + memStream.Position = 0; + + CommandLineSwitchException e2 = (CommandLineSwitchException)formatter.Deserialize(memStream); + + Assert.AreEqual(e.Message, e2.Message); + } + } + } + } +} diff --git a/src/XMakeCommandLine/UnitTests/CommandLineSwitches_Tests.cs b/src/XMakeCommandLine/UnitTests/CommandLineSwitches_Tests.cs new file mode 100644 index 00000000000..986b04e511f --- /dev/null +++ b/src/XMakeCommandLine/UnitTests/CommandLineSwitches_Tests.cs @@ -0,0 +1,1163 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +using Microsoft.Build.CommandLine; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class CommandLineSwitchesTests + { + [ClassInitialize] + public static void Setup(TestContext testContext) + { + // Make sure resources are initialized + MSBuildApp.Initialize(); + } + + [TestMethod] + public void BogusSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsFalse(CommandLineSwitches.IsParameterlessSwitch("bogus", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Invalid, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsFalse(CommandLineSwitches.IsParameterizedSwitch("bogus", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Invalid, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + } + + [TestMethod] + public void HelpSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("help", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Help, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("HELP", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Help, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("Help", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Help, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("h", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Help, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("H", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Help, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("?", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Help, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void VersionSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("version", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Version, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("Version", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Version, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("VERSION", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Version, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("ver", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Version, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("VER", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Version, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("Ver", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.Version, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void NoLogoSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("nologo", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoLogo, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NOLOGO", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoLogo, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NoLogo", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoLogo, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void NoAutoResponseSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("noautoresponse", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NOAUTORESPONSE", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NoAutoResponse", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("noautorsp", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NOAUTORSP", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NoAutoRsp", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void NoConsoleLoggerSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("noconsolelogger", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NOCONSOLELOGGER", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NoConsoleLogger", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("noconlog", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NOCONLOG", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("NoConLog", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void FileLoggerSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("fileLogger", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.FileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("FILELOGGER", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.FileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("FileLogger", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.FileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("fl", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.FileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("FL", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.FileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void DistributedFileLoggerSwitchIdentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("distributedfilelogger", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DistributedFileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("DISTRIBUTEDFILELOGGER", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DistributedFileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("DistributedFileLogger", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DistributedFileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("dfl", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DistributedFileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("DFL", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DistributedFileLogger, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void FileLoggerParametersIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("flp", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.FileLoggerParameters, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("FLP", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.FileLoggerParameters, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("fileLoggerParameters", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.FileLoggerParameters, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("FILELOGGERPARAMETERS", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.FileLoggerParameters, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + + [TestMethod] + public void NodeReuseParametersIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("nr", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.NodeReuse, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("NR", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.NodeReuse, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("nodereuse", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.NodeReuse, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("NodeReuse", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.NodeReuse, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void ProjectSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch(null, out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Project, parameterizedSwitch); + Assert.IsNotNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + // for the virtual project switch, we match on null, not empty string + Assert.IsFalse(CommandLineSwitches.IsParameterizedSwitch(String.Empty, out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Invalid, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + } + + [TestMethod] + public void IgnoreProjectExtensionsSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("ignoreprojectextensions", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("IgnoreProjectExtensions", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("IGNOREPROJECTEXTENSIONS", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("ignore", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("IGNORE", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void TargetSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("target", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Target, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("TARGET", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Target, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("Target", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Target, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("t", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Target, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("T", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Target, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void PropertySwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("property", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Property, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("PROPERTY", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Property, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("Property", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Property, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("p", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Property, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("P", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Property, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsTrue(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void LoggerSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("logger", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Logger, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("LOGGER", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Logger, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("Logger", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Logger, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("l", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Logger, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("L", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Logger, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsFalse(unquoteParameters); + } + + [TestMethod] + public void VerbositySwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("verbosity", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Verbosity, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("VERBOSITY", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Verbosity, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("Verbosity", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Verbosity, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("v", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Verbosity, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("V", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Verbosity, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void DetailedSummarySwitchIndentificationTests() + { + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + string duplicateSwitchErrorMessage; + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("ds", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DetailedSummary, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("DS", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DetailedSummary, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("Ds", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DetailedSummary, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("detailedsummary", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DetailedSummary, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("DETAILEDSUMMARY", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DetailedSummary, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + + Assert.IsTrue(CommandLineSwitches.IsParameterlessSwitch("DetailedSummary", out parameterlessSwitch, out duplicateSwitchErrorMessage)); + Assert.AreEqual(CommandLineSwitches.ParameterlessSwitch.DetailedSummary, parameterlessSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + } + + [TestMethod] + public void MaxCPUCountSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("m", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.MaxCPUCount, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("M", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.MaxCPUCount, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("maxcpucount", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.MaxCPUCount, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("MAXCPUCOUNT", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.MaxCPUCount, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNotNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void ValidateSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("validate", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Validate, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("VALIDATE", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Validate, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("Validate", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Validate, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("val", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Validate, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("VAL", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Validate, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("Val", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Validate, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void PreprocessSwitchIdentificationTests() + { + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("preprocess", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Preprocess, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + + Assert.IsTrue(CommandLineSwitches.IsParameterizedSwitch("pp", out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)); + Assert.AreEqual(CommandLineSwitches.ParameterizedSwitch.Preprocess, parameterizedSwitch); + Assert.IsNull(duplicateSwitchErrorMessage); + Assert.IsFalse(multipleParametersAllowed); + Assert.IsNull(missingParametersErrorMessage); + Assert.IsTrue(unquoteParameters); + } + + [TestMethod] + public void SetParameterlessSwitchTests() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + switches.SetParameterlessSwitch(CommandLineSwitches.ParameterlessSwitch.NoLogo, "/nologo"); + + Assert.AreEqual("/nologo", switches.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoLogo)); + Assert.IsTrue(switches.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoLogo)); + Assert.IsTrue(switches[CommandLineSwitches.ParameterlessSwitch.NoLogo]); + + // set it again + switches.SetParameterlessSwitch(CommandLineSwitches.ParameterlessSwitch.NoLogo, "-NOLOGO"); + + Assert.AreEqual("-NOLOGO", switches.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoLogo)); + Assert.IsTrue(switches.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoLogo)); + Assert.IsTrue(switches[CommandLineSwitches.ParameterlessSwitch.NoLogo]); + + // we didn't set this switch + Assert.IsNull(switches.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.Version)); + Assert.IsFalse(switches.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Version)); + Assert.IsFalse(switches[CommandLineSwitches.ParameterlessSwitch.Version]); + } + + [TestMethod] + public void SetParameterizedSwitchTests1() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + Assert.IsTrue(switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Verbosity, "/v:q", "q", false, true)); + + Assert.AreEqual("/v:q", switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Verbosity)); + Assert.IsTrue(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Verbosity)); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Verbosity]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(1, parameters.Length); + Assert.AreEqual("q", parameters[0]); + + // set it again -- this is bogus, because the /verbosity switch doesn't allow multiple parameters, but for the + // purposes of testing the SetParameterizedSwitch() method, it doesn't matter + Assert.IsTrue(switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Verbosity, "/verbosity:\"diag\";minimal", "\"diag\";minimal", true, true)); + + Assert.AreEqual("/verbosity:\"diag\";minimal", switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Verbosity)); + Assert.IsTrue(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Verbosity)); + + parameters = switches[CommandLineSwitches.ParameterizedSwitch.Verbosity]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(3, parameters.Length); + Assert.AreEqual("q", parameters[0]); + Assert.AreEqual("diag", parameters[1]); + Assert.AreEqual("minimal", parameters[2]); + } + + [TestMethod] + public void SetParameterizedSwitchTests2() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + // we haven't set this switch yet + Assert.IsNull(switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Target)); + Assert.IsFalse(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Target]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(0, parameters.Length); + + // fake/missing parameters -- this is bogus because the /target switch allows multiple parameters but we're turning + // that off here just for testing purposes + Assert.IsFalse(switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Target, "/t:\"", "\"", false, true)); + + // switch has been set + Assert.AreEqual("/t:\"", switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Target)); + Assert.IsTrue(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + parameters = switches[CommandLineSwitches.ParameterizedSwitch.Target]; + + // but no parameters + Assert.IsNotNull(parameters); + Assert.AreEqual(0, parameters.Length); + + // more fake/missing parameters + Assert.IsFalse(switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Target, "/t:A,\"\";B", "A,\"\";B", true, true)); + + Assert.AreEqual("/t:A,\"\";B", switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Target)); + Assert.IsTrue(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + parameters = switches[CommandLineSwitches.ParameterizedSwitch.Target]; + + // now we have some parameters + Assert.IsNotNull(parameters); + Assert.AreEqual(2, parameters.Length); + Assert.AreEqual("A", parameters[0]); + Assert.AreEqual("B", parameters[1]); + } + + [TestMethod] + public void SetParameterizedSwitchTests3() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + // we haven't set this switch yet + Assert.IsNull(switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Logger)); + Assert.IsFalse(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Logger)); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Logger]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(0, parameters.Length); + + // don't unquote fake/missing parameters + Assert.IsTrue(switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Logger, "/l:\"", "\"", false, false)); + + Assert.AreEqual("/l:\"", switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Logger)); + Assert.IsTrue(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Logger)); + + parameters = switches[CommandLineSwitches.ParameterizedSwitch.Logger]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(1, parameters.Length); + Assert.AreEqual("\"", parameters[0]); + + // don't unquote multiple fake/missing parameters -- this is bogus because the /logger switch does not take multiple + // parameters, but for testing purposes this is fine + Assert.IsTrue(switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Logger, "/LOGGER:\"\",asm;\"p,a;r\"", "\"\",asm;\"p,a;r\"", true, false)); + + Assert.AreEqual("/LOGGER:\"\",asm;\"p,a;r\"", switches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Logger)); + Assert.IsTrue(switches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Logger)); + + parameters = switches[CommandLineSwitches.ParameterizedSwitch.Logger]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(4, parameters.Length); + Assert.AreEqual("\"", parameters[0]); + Assert.AreEqual("\"\"", parameters[1]); + Assert.AreEqual("asm", parameters[2]); + Assert.AreEqual("\"p,a;r\"", parameters[3]); + } + + [TestMethod] + public void AppendErrorTests1() + { + CommandLineSwitches switchesLeft = new CommandLineSwitches(); + CommandLineSwitches switchesRight = new CommandLineSwitches(); + + Assert.IsFalse(switchesLeft.HaveErrors()); + Assert.IsFalse(switchesRight.HaveErrors()); + + switchesLeft.Append(switchesRight); + + Assert.IsFalse(switchesLeft.HaveErrors()); + Assert.IsFalse(switchesRight.HaveErrors()); + + switchesLeft.SetUnknownSwitchError("/bogus"); + + Assert.IsTrue(switchesLeft.HaveErrors()); + Assert.IsFalse(switchesRight.HaveErrors()); + + switchesLeft.Append(switchesRight); + + Assert.IsTrue(switchesLeft.HaveErrors()); + Assert.IsFalse(switchesRight.HaveErrors()); + + VerifySwitchError(switchesLeft, "/bogus"); + + switchesRight.Append(switchesLeft); + + Assert.IsTrue(switchesLeft.HaveErrors()); + Assert.IsTrue(switchesRight.HaveErrors()); + + VerifySwitchError(switchesLeft, "/bogus"); + VerifySwitchError(switchesRight, "/bogus"); + } + + [TestMethod] + public void AppendErrorTests2() + { + CommandLineSwitches switchesLeft = new CommandLineSwitches(); + CommandLineSwitches switchesRight = new CommandLineSwitches(); + + Assert.IsFalse(switchesLeft.HaveErrors()); + Assert.IsFalse(switchesRight.HaveErrors()); + + switchesLeft.SetUnknownSwitchError("/bogus"); + switchesRight.SetUnexpectedParametersError("/nologo:foo"); + + Assert.IsTrue(switchesLeft.HaveErrors()); + Assert.IsTrue(switchesRight.HaveErrors()); + + VerifySwitchError(switchesLeft, "/bogus"); + VerifySwitchError(switchesRight, "/nologo:foo"); + + switchesLeft.Append(switchesRight); + + VerifySwitchError(switchesLeft, "/bogus"); + VerifySwitchError(switchesRight, "/nologo:foo"); + } + + [TestMethod] + public void AppendParameterlessSwitchesTests() + { + CommandLineSwitches switchesLeft = new CommandLineSwitches(); + + switchesLeft.SetParameterlessSwitch(CommandLineSwitches.ParameterlessSwitch.Help, "/?"); + + Assert.IsTrue(switchesLeft.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsFalse(switchesLeft.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + + CommandLineSwitches switchesRight1 = new CommandLineSwitches(); + + switchesRight1.SetParameterlessSwitch(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, "/noconlog"); + + Assert.IsFalse(switchesRight1.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsTrue(switchesRight1.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + + switchesLeft.Append(switchesRight1); + + Assert.AreEqual("/noconlog", switchesLeft.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + Assert.IsTrue(switchesLeft.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + Assert.IsTrue(switchesLeft[CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger]); + + // this switch is not affected + Assert.AreEqual("/?", switchesLeft.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsTrue(switchesLeft.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsTrue(switchesLeft[CommandLineSwitches.ParameterlessSwitch.Help]); + + CommandLineSwitches switchesRight2 = new CommandLineSwitches(); + + switchesRight2.SetParameterlessSwitch(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger, "/NOCONSOLELOGGER"); + + Assert.IsFalse(switchesRight2.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsTrue(switchesRight2.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + + switchesLeft.Append(switchesRight2); + + Assert.AreEqual("/NOCONSOLELOGGER", switchesLeft.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + Assert.IsTrue(switchesLeft.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger)); + Assert.IsTrue(switchesLeft[CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger]); + + Assert.AreEqual("/?", switchesLeft.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsTrue(switchesLeft.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Help)); + Assert.IsTrue(switchesLeft[CommandLineSwitches.ParameterlessSwitch.Help]); + + Assert.IsFalse(switchesLeft.HaveErrors()); + } + + [TestMethod] + public void AppendParameterizedSwitchesTests1() + { + CommandLineSwitches switchesLeft = new CommandLineSwitches(); + + switchesLeft.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Project, "tempproject.proj", "tempproject.proj", false, true); + + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Project)); + Assert.IsFalse(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + CommandLineSwitches switchesRight = new CommandLineSwitches(); + + switchesRight.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Target, "/t:build", "build", true, true); + + Assert.IsFalse(switchesRight.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Project)); + Assert.IsTrue(switchesRight.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + switchesLeft.Append(switchesRight); + + Assert.AreEqual("tempproject.proj", switchesLeft.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Project)); + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Project)); + + string[] parameters = switchesLeft[CommandLineSwitches.ParameterizedSwitch.Project]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(1, parameters.Length); + Assert.AreEqual("tempproject.proj", parameters[0]); + + Assert.AreEqual("/t:build", switchesLeft.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Target)); + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + parameters = switchesLeft[CommandLineSwitches.ParameterizedSwitch.Target]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(1, parameters.Length); + Assert.AreEqual("build", parameters[0]); + } + + [TestMethod] + public void AppendParameterizedSwitchesTests2() + { + CommandLineSwitches switchesLeft = new CommandLineSwitches(); + + switchesLeft.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Target, "/target:Clean", "Clean", true, true); + + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + CommandLineSwitches switchesRight = new CommandLineSwitches(); + + switchesRight.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Target, "/t:\"RESOURCES\";build", "\"RESOURCES\";build", true, true); + + Assert.IsTrue(switchesRight.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + switchesLeft.Append(switchesRight); + + Assert.AreEqual("/t:\"RESOURCES\";build", switchesLeft.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Target)); + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Target)); + + string[] parameters = switchesLeft[CommandLineSwitches.ParameterizedSwitch.Target]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(3, parameters.Length); + Assert.AreEqual("Clean", parameters[0]); + Assert.AreEqual("RESOURCES", parameters[1]); + Assert.AreEqual("build", parameters[2]); + } + + [TestMethod] + public void AppendParameterizedSwitchesTests3() + { + CommandLineSwitches switchesLeft = new CommandLineSwitches(); + + switchesLeft.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Project, "tempproject.proj", "tempproject.proj", false, true); + + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Project)); + + CommandLineSwitches switchesRight = new CommandLineSwitches(); + + switchesRight.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Project, "Rhubarb.proj", "Rhubarb.proj", false, true); + + Assert.IsTrue(switchesRight.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Project)); + + switchesLeft.Append(switchesRight); + + Assert.AreEqual("tempproject.proj", switchesLeft.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.Project)); + Assert.IsTrue(switchesLeft.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Project)); + + string[] parameters = switchesLeft[CommandLineSwitches.ParameterizedSwitch.Project]; + + Assert.IsNotNull(parameters); + Assert.AreEqual(1, parameters.Length); + Assert.AreEqual("tempproject.proj", parameters[0]); + + Assert.IsTrue(switchesLeft.HaveErrors()); + + VerifySwitchError(switchesLeft, "Rhubarb.proj"); + } + + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void InvalidToolsVersionErrors() + { + string filename = null; + try + { + filename = FileUtilities.GetTemporaryFile(); + ProjectRootElement project = ProjectRootElement.Create(); + project.Save(filename); + MSBuildApp.BuildProject(filename, null, "ScoobyDoo", new Dictionary(StringComparer.OrdinalIgnoreCase), new ILogger[] { }, LoggerVerbosity.Normal, new DistributedLoggerRecord[] { }, false, null, 1, true, new StringWriter(), false, false); + } + finally + { + if (File.Exists(filename)) File.Delete(filename); + } + } + + [TestMethod] + public void TestHaveAnySwitchesBeenSet() + { + // Check if method works with parameterized switch + CommandLineSwitches switches = new CommandLineSwitches(); + Assert.IsFalse(switches.HaveAnySwitchesBeenSet()); + switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.Verbosity, "/v:q", "q", false, true); + Assert.IsTrue(switches.HaveAnySwitchesBeenSet()); + + // Check if method works with parameterless switches + switches = new CommandLineSwitches(); + Assert.IsFalse(switches.HaveAnySwitchesBeenSet()); + switches.SetParameterlessSwitch(CommandLineSwitches.ParameterlessSwitch.Help, "/?"); + Assert.IsTrue(switches.HaveAnySwitchesBeenSet()); + } + + /// + /// /nodereuse:false /nodereuse:true should result in "true" + /// + [TestMethod] + public void ProcessNodeReuseSwitchTrueLast() + { + bool nodeReuse = MSBuildApp.ProcessNodeReuseSwitch(new string[] { "false", "true" }); + + Assert.IsTrue(nodeReuse); + } + + /// + /// /nodereuse:true /nodereuse:false should result in "false" + /// + [TestMethod] + public void ProcessNodeReuseSwitchFalseLast() + { + bool nodeReuse = MSBuildApp.ProcessNodeReuseSwitch(new string[] { "true", "false" }); + + Assert.AreEqual(false, nodeReuse); + } + + /// + /// Regress DDB #143341: + /// msbuild /clp:v=quiet /clp:v=diag /m:2 + /// gave console logger in quiet verbosity; expected diagnostic + /// + [TestMethod] + public void ExtractAnyLoggerParameterPickLast() + { + string result = MSBuildApp.ExtractAnyLoggerParameter("v=diag;v=q", new string[] { "v", "verbosity" }); + + Assert.AreEqual("v=q", result); + } + + /// + /// Verifies that a switch collection has an error registered for the given command line arg. + /// + /// + /// + private void VerifySwitchError(CommandLineSwitches switches, string badCommandLineArg) + { + bool caughtError = false; + + try + { + switches.ThrowErrors(); + } + catch (CommandLineSwitchException e) + { + Assert.AreEqual(badCommandLineArg, e.CommandLineArg); + + caughtError = true; + + // so I can see the message in NUnit's "Standard Out" window + Console.WriteLine(e.Message); + } + finally + { + Assert.IsTrue(caughtError); + } + } + } +} diff --git a/src/XMakeCommandLine/UnitTests/InitializationException_Tests.cs b/src/XMakeCommandLine/UnitTests/InitializationException_Tests.cs new file mode 100644 index 00000000000..b799950c6f3 --- /dev/null +++ b/src/XMakeCommandLine/UnitTests/InitializationException_Tests.cs @@ -0,0 +1,61 @@ +using Microsoft.Build.CommandLine; +using NUnit.Framework; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +namespace Microsoft.Build.UnitTests +{ + [TestFixture] + public class InitializationException_Tests + { + /// + /// Verify ISerializable is implemented correctly + /// + [Test] + public void SerializeDeserialize() + { + try + { + InitializationException.Throw("message", "invalidSwitch"); + } + catch(InitializationException e) + { + using (MemoryStream memStream = new MemoryStream()) + { + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(memStream, e); + memStream.Position = 0; + + InitializationException e2 = (InitializationException)formatter.Deserialize(memStream); + + Assert.AreEqual(e.Message, e2.Message); + } + } + } + + /// + /// Verify ISerializable is implemented correctly + /// + [Test] + public void SerializeDeserialize2() + { + try + { + InitializationException.Throw("message", null); + } + catch (InitializationException e) + { + using (MemoryStream memStream = new MemoryStream()) + { + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(memStream, e); + memStream.Position = 0; + + InitializationException e2 = (InitializationException)formatter.Deserialize(memStream); + + Assert.AreEqual(e.Message, e2.Message); + } + } + } + } +} diff --git a/src/XMakeCommandLine/UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj b/src/XMakeCommandLine/UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj new file mode 100644 index 00000000000..541d8793714 --- /dev/null +++ b/src/XMakeCommandLine/UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj @@ -0,0 +1,95 @@ + + + + + Debug + AnyCPU + {C79756DC-CC78-45D6-AE11-8BB35F201CE4} + Library + Microsoft.Build.CommandLine.UnitTests + Microsoft.Build.CommandLine.UnitTests + + + + + + + + + true + + + RegistryDelegates.cs + + + RegistryHelper.cs + + + VersionUtilities.cs + + + + + + + + + + + + + + + + + {828566ee-6f6a-4ef4-98b0-513f7df9c628} + Microsoft.Build.Utilities + + + {16cd7635-7cf4-4c62-a77b-cf87d0f09a58} + Microsoft.Build + + + {23c9fd0e-70c5-4f1f-b08a-d2774240fb51} + MSBuild + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + + + + + Microsoft.Build.Core.xsd + + + + + Microsoft.Build.CommonTypes.xsd + + + + + Microsoft.Build.xsd + + + + + + + App.config + Designer + + + + + + + \ No newline at end of file diff --git a/src/XMakeCommandLine/UnitTests/ProjectSchemaValidationHandler_Tests.cs b/src/XMakeCommandLine/UnitTests/ProjectSchemaValidationHandler_Tests.cs new file mode 100644 index 00000000000..f50f4ee5852 --- /dev/null +++ b/src/XMakeCommandLine/UnitTests/ProjectSchemaValidationHandler_Tests.cs @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Threading; +using System.Xml; +using Microsoft.Build.CommandLine; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ProjectSchemaValidationHandlerTest + { + /*********************************************************************** + * + * Test: ProjectSchemaValidationHandlerTest.VerifyProjectSchema + * + * This calls VerifyProjectSchema to validate a project file passed, where + * the project contents are invalid + * + **********************************************************************/ + [TestMethod] + public void VerifyInvalidProjectSchema + ( + ) + { + string[] msbuildTempXsdFilenames = new string[] { }; + string projectFilename = null; + string oldValueForMSBuildOldOM = null; + try + { + oldValueForMSBuildOldOM = Environment.GetEnvironmentVariable("MSBuildOldOM"); + Environment.SetEnvironmentVariable("MSBuildOldOM", ""); + + // Create schema files in the temp folder + msbuildTempXsdFilenames = PrepareSchemaFiles(); + + projectFilename = CreateTempFileOnDisk(@" + + + + + + + "); + string quotedProjectFilename = "\"" + projectFilename + "\""; + + Assert.AreEqual(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFilename + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + } + finally + { + if (projectFilename != null) File.Delete(projectFilename); + CleanupSchemaFiles(msbuildTempXsdFilenames); + Environment.SetEnvironmentVariable("MSBuildOldOM", oldValueForMSBuildOldOM); + } + } + + /// + /// Checks that an exception is thrown when the schema being validated + /// against is itself invalid + /// + [TestMethod] + public void VerifyInvalidSchemaItself1 + ( + ) + { + string invalidSchemaFile = null; + string projectFilename = null; + string oldValueForMSBuildOldOM = null; + try + { + oldValueForMSBuildOldOM = Environment.GetEnvironmentVariable("MSBuildOldOM"); + Environment.SetEnvironmentVariable("MSBuildOldOM", ""); + + // Create schema files in the temp folder + invalidSchemaFile = FileUtilities.GetTemporaryFile(); + + File.WriteAllText(invalidSchemaFile, ""); + + projectFilename = CreateTempFileOnDisk(@" + + + + "); + string quotedProjectFile = "\"" + projectFilename + "\""; + + Assert.AreEqual(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + } + finally + { + if (projectFilename != null) File.Delete(projectFilename); + if (invalidSchemaFile != null) File.Delete(invalidSchemaFile); + Environment.SetEnvironmentVariable("MSBuildOldOM", oldValueForMSBuildOldOM); + } + } + + /// + /// Checks that an exception is thrown when the schema being validated + /// against is itself invalid + /// + [TestMethod] + public void VerifyInvalidSchemaItself2 + ( + ) + { + string invalidSchemaFile = null; + string projectFilename = null; + string oldValueForMSBuildOldOM = null; + + try + { + oldValueForMSBuildOldOM = Environment.GetEnvironmentVariable("MSBuildOldOM"); + Environment.SetEnvironmentVariable("MSBuildOldOM", ""); + + // Create schema files in the temp folder + invalidSchemaFile = FileUtilities.GetTemporaryFile(); + + File.WriteAllText(invalidSchemaFile, @" + + + + + + + + + + +"); + + projectFilename = CreateTempFileOnDisk(@" + + + + "); + + string quotedProjectFile = "\"" + projectFilename + "\""; + + Assert.AreEqual(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + } + finally + { + if (invalidSchemaFile != null) File.Delete(invalidSchemaFile); + if (projectFilename != null) File.Delete(projectFilename); + Environment.SetEnvironmentVariable("MSBuildOldOM", oldValueForMSBuildOldOM); + } + } + + /*********************************************************************** + * + * Test: ProjectSchemaValidationHandlerTest.VerifyProjectSchema + * + * This calls VerifyProjectSchema to validate a project XML + * specified in a string, where the project passed is valid + * + **********************************************************************/ + [TestMethod] + public void VerifyValidProjectSchema + ( + ) + { + string[] msbuildTempXsdFilenames = new string[] { }; + string projectFilename = CreateTempFileOnDisk(@" + + + + "); + string oldValueForMSBuildOldOM = null; + + try + { + oldValueForMSBuildOldOM = Environment.GetEnvironmentVariable("MSBuildOldOM"); + Environment.SetEnvironmentVariable("MSBuildOldOM", ""); + + // Create schema files in the temp folder + msbuildTempXsdFilenames = PrepareSchemaFiles(); + string quotedProjectFile = "\"" + projectFilename + "\""; + + Assert.AreEqual(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + + //ProjectSchemaValidationHandler.VerifyProjectSchema + // ( + // projectFilename, + // msbuildTempXsdFilenames[0], + // @"c:\" + // ); + } + finally + { + File.Delete(projectFilename); + CleanupSchemaFiles(msbuildTempXsdFilenames); + Environment.SetEnvironmentVariable("MSBuildOldOM", oldValueForMSBuildOldOM); + } + } + + /// + /// The test has a valid project file, importing an invalid project file. + /// We should not validate imported files against the schema in V1, so this + /// should not be caught by the schema + /// + [TestMethod] + public void VerifyInvalidImportNotCaughtBySchema + ( + ) + { + string[] msbuildTempXsdFilenames = new string[] { }; + + string importedProjectFilename = CreateTempFileOnDisk(@" + + + + + "); + + string projectFilename = CreateTempFileOnDisk(@" + + + + + ", importedProjectFilename); + string oldValueForMSBuildOldOM = null; + + try + { + oldValueForMSBuildOldOM = Environment.GetEnvironmentVariable("MSBuildOldOM"); + Environment.SetEnvironmentVariable("MSBuildOldOM", ""); + + // Create schema files in the temp folder + msbuildTempXsdFilenames = PrepareSchemaFiles(); + string quotedProjectFile = "\"" + projectFilename + "\""; + + Assert.AreEqual(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + + //ProjectSchemaValidationHandler.VerifyProjectSchema + // ( + // projectFilename, + // msbuildTempXsdFilenames[0], + // @"c:\" + // ); + } + finally + { + CleanupSchemaFiles(msbuildTempXsdFilenames); + File.Delete(projectFilename); + File.Delete(importedProjectFilename); + Environment.SetEnvironmentVariable("MSBuildOldOM", oldValueForMSBuildOldOM); + } + } + + #region Helper Functions + + /// + /// MSBuild schemas are embedded as a resource into Microsoft.Build.Engine.UnitTests.dll. + /// Extract the stream from the resource and write the XSDs out to a temporary file, + /// so that our schema validator can access it. + /// + private string[] PrepareSchemaFiles() + { + Stream msbuildXsdStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Build.CommandLine.UnitTests.Microsoft.Build.xsd"); + StreamReader msbuildXsdStreamReader = new StreamReader(msbuildXsdStream); + string msbuildXsdContents = msbuildXsdStreamReader.ReadToEnd(); + string msbuildTempXsdFilename = FileUtilities.GetTemporaryFile(); + File.WriteAllText(msbuildTempXsdFilename, msbuildXsdContents); + + msbuildXsdStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Build.CommandLine.UnitTests.Microsoft.Build.Core.xsd"); + msbuildXsdStreamReader = new StreamReader(msbuildXsdStream); + msbuildXsdContents = msbuildXsdStreamReader.ReadToEnd(); + string msbuildXsdSubDirectory = Path.Combine(Path.GetTempPath(), "MSBuild"); + Directory.CreateDirectory(msbuildXsdSubDirectory); + string msbuildTempXsdFilename2 = Path.Combine(msbuildXsdSubDirectory, "Microsoft.Build.Core.xsd"); + File.WriteAllText(msbuildTempXsdFilename2, msbuildXsdContents); + + msbuildXsdStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Build.CommandLine.UnitTests.Microsoft.Build.CommonTypes.xsd"); + msbuildXsdStreamReader = new StreamReader(msbuildXsdStream); + msbuildXsdContents = msbuildXsdStreamReader.ReadToEnd(); + string msbuildTempXsdFilename3 = Path.Combine(msbuildXsdSubDirectory, "Microsoft.Build.CommonTypes.xsd"); + File.WriteAllText(msbuildTempXsdFilename3, msbuildXsdContents); + + return new string[] { msbuildTempXsdFilename, msbuildTempXsdFilename2, msbuildTempXsdFilename3 }; + } + + /// + /// Gets rid of the temporary files created to hold the schemas for the duration + /// of these unit tests. + /// + private void CleanupSchemaFiles(string[] msbuildTempXsdFilenames) + { + foreach (string file in msbuildTempXsdFilenames) + { + if (File.Exists(file)) + { + File.Delete(file); + } + } + string msbuildXsdSubDirectory = Path.Combine(Path.GetTempPath(), "MSBuild"); + if (Directory.Exists(msbuildXsdSubDirectory)) + { + for (int i = 0; i < 5; i++) + { + try + { + Directory.Delete(msbuildXsdSubDirectory, true /* recursive */); + break; + } + catch (Exception) + { + Thread.Sleep(1000); + // Eat exceptions from the delete + } + } + } + } + + /// + /// Create an MSBuild project file on disk and return the full path to it. + /// + /// Stolen from ObjectModelHelpers because we use relatively little + /// of the ObjectModelHelpers functionality, so as to avoid having to include in + /// this project everything that ObjectModelHelpers depends on + static internal string CreateTempFileOnDisk(string fileContents, params object[] args) + { + return CreateTempFileOnDiskNoFormat(String.Format(fileContents, args)); + } + + /// + /// Create an MSBuild project file on disk and return the full path to it. + /// + /// Stolen from ObjectModelHelpers because we use relatively little + /// of the ObjectModelHelpers functionality, so as to avoid having to include in + /// this project everything that ObjectModelHelpers depends on + static internal string CreateTempFileOnDiskNoFormat(string fileContents) + { + string projectFilePath = FileUtilities.GetTemporaryFile(); + + File.WriteAllText(projectFilePath, CleanupFileContents(fileContents)); + + return projectFilePath; + } + + /// + /// Does certain replacements in a string representing the project file contents. + /// This makes it easier to write unit tests because the author doesn't have + /// to worry about escaping double-quotes, etc. + /// + /// Stolen from ObjectModelHelpers because we use relatively little + /// of the ObjectModelHelpers functionality, so as to avoid having to include in + /// this project everything that ObjectModelHelpers depends on + static private string CleanupFileContents(string projectFileContents) + { + // Replace reverse-single-quotes with double-quotes. + projectFileContents = projectFileContents.Replace("`", "\""); + + // Place the correct MSBuild namespace into the tag. + projectFileContents = projectFileContents.Replace("msbuildnamespace", msbuildNamespace); + projectFileContents = projectFileContents.Replace("msbuilddefaulttoolsversion", msbuildDefaultToolsVersion); + + return projectFileContents; + } + + private const string msbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; + private const string msbuildDefaultToolsVersion = "4.0"; + + #endregion // Helper Functions + } +} diff --git a/src/XMakeCommandLine/UnitTests/XMake_Tests.cs b/src/XMakeCommandLine/UnitTests/XMake_Tests.cs new file mode 100644 index 00000000000..29972aad994 --- /dev/null +++ b/src/XMakeCommandLine/UnitTests/XMake_Tests.cs @@ -0,0 +1,1776 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; + +using Microsoft.Build.CommandLine; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class XMakeAppTests + { + [TestMethod] + public void GatherCommandLineSwitchesTwoProperties() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + ArrayList arguments = new ArrayList(); + arguments.AddRange(new string[] { "/p:a=b", "/p:c=d" }); + + MSBuildApp.GatherCommandLineSwitches(arguments, switches); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; + Assert.AreEqual("a=b", parameters[0]); + Assert.AreEqual("c=d", parameters[1]); + } + + [TestMethod] + public void GatherCommandLineSwitchesMaxCpuCountWithArgument() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + ArrayList arguments = new ArrayList(); + arguments.AddRange(new string[] { "/m:2" }); + + MSBuildApp.GatherCommandLineSwitches(arguments, switches); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; + Assert.AreEqual("2", parameters[0]); + Assert.AreEqual(1, parameters.Length); + + Assert.AreEqual(false, switches.HaveErrors()); + } + + [TestMethod] + public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + ArrayList arguments = new ArrayList(); + arguments.AddRange(new string[] { "/m:3", "/m" }); + + MSBuildApp.GatherCommandLineSwitches(arguments, switches); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; + Assert.AreEqual(Convert.ToString(Environment.ProcessorCount), parameters[1]); + Assert.AreEqual(2, parameters.Length); + + Assert.AreEqual(false, switches.HaveErrors()); + } + + /// + /// /m: should be an error, unlike /m:1 and /m + /// + [TestMethod] + public void GatherCommandLineSwitchesMaxCpuCountWithoutArgumentButWithColon() + { + CommandLineSwitches switches = new CommandLineSwitches(); + + ArrayList arguments = new ArrayList(); + arguments.AddRange(new string[] { "/m:" }); + + MSBuildApp.GatherCommandLineSwitches(arguments, switches); + + string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; + Assert.AreEqual(0, parameters.Length); + + Assert.IsTrue(switches.HaveErrors()); + } + + /* + * Quoting Rules: + * + * A string is considered quoted if it is enclosed in double-quotes. A double-quote can be escaped with a backslash, or it + * is automatically escaped if it is the last character in an explicitly terminated quoted string. A backslash itself can + * be escaped with another backslash IFF it precedes a double-quote, otherwise it is interpreted literally. + * + * e.g. + * abc"cde"xyz --> "cde" is quoted + * abc"xyz --> "xyz" is quoted (the terminal double-quote is assumed) + * abc"xyz" --> "xyz" is quoted (the terminal double-quote is explicit) + * + * abc\"cde"xyz --> "xyz" is quoted (the terminal double-quote is assumed) + * abc\\"cde"xyz --> "cde" is quoted + * abc\\\"cde"xyz --> "xyz" is quoted (the terminal double-quote is assumed) + * + * abc"""xyz --> """ is quoted + * abc""""xyz --> """ and "xyz" are quoted (the terminal double-quote is assumed) + * abc"""""xyz --> """ is quoted + * abc""""""xyz --> """ and """ are quoted + * abc"cde""xyz --> "cde"" is quoted + * abc"xyz"" --> "xyz"" is quoted (the terminal double-quote is explicit) + * + * abc""xyz --> nothing is quoted + * abc""cde""xyz --> nothing is quoted + */ + + [TestMethod] + public void SplitUnquotedTest() + { + ArrayList sa; + int emptySplits; + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abcdxyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abcdxyz", sa[0]); + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abcc dxyz"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abcc", sa[0]); + Assert.AreEqual("dxyz", sa[1]); + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abcc;dxyz", ';'); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abcc", sa[0]); + Assert.AreEqual("dxyz", sa[1]); + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abc,c;dxyz", ';', ','); + Assert.AreEqual(3, sa.Count); + Assert.AreEqual("abc", sa[0]); + Assert.AreEqual("c", sa[1]); + Assert.AreEqual("dxyz", sa[2]); + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abc,c;dxyz", 2, false, false, out emptySplits, ';', ','); + Assert.AreEqual(0, emptySplits); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc", sa[0]); + Assert.AreEqual("c;dxyz", sa[1]); + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abc,,;dxyz", int.MaxValue, false, false, out emptySplits, ';', ','); + Assert.AreEqual(2, emptySplits); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc", sa[0]); + Assert.AreEqual("dxyz", sa[1]); + + // nothing quoted + sa = QuotingUtilities.SplitUnquoted("abc,,;dxyz", int.MaxValue, true, false, out emptySplits, ';', ','); + Assert.AreEqual(0, emptySplits); + Assert.AreEqual(4, sa.Count); + Assert.AreEqual("abc", sa[0]); + Assert.AreEqual(String.Empty, sa[1]); + Assert.AreEqual(String.Empty, sa[2]); + Assert.AreEqual("dxyz", sa[3]); + + // "c d" is quoted + sa = QuotingUtilities.SplitUnquoted("abc\"c d\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"c d\"xyz", sa[0]); + + // "x z" is quoted (the terminal double-quote is assumed) + sa = QuotingUtilities.SplitUnquoted("abc\"x z"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"x z", sa[0]); + + // "x z" is quoted (the terminal double-quote is explicit) + sa = QuotingUtilities.SplitUnquoted("abc\"x z\""); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"x z\"", sa[0]); + + // "x z" is quoted (the terminal double-quote is assumed) + sa = QuotingUtilities.SplitUnquoted("abc\\\"cde\"x z"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\\\"cde\"x z", sa[0]); + + // "x z" is quoted (the terminal double-quote is assumed) + // "c e" is not quoted + sa = QuotingUtilities.SplitUnquoted("abc\\\"c e\"x z"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\\\"c", sa[0]); + Assert.AreEqual("e\"x z", sa[1]); + + // "c e" is quoted + sa = QuotingUtilities.SplitUnquoted("abc\\\\\"c e\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\\\\\"c e\"xyz", sa[0]); + + // "c e" is quoted + // "x z" is not quoted + sa = QuotingUtilities.SplitUnquoted("abc\\\\\"c e\"x z"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\\\\\"c e\"x", sa[0]); + Assert.AreEqual("z", sa[1]); + + // "x z" is quoted (the terminal double-quote is assumed) + sa = QuotingUtilities.SplitUnquoted("abc\\\\\\\"cde\"x z"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\\\\\\\"cde\"x z", sa[0]); + + // "xyz" is quoted (the terminal double-quote is assumed) + // "c e" is not quoted + sa = QuotingUtilities.SplitUnquoted("abc\\\\\\\"c e\"x z"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\\\\\\\"c", sa[0]); + Assert.AreEqual("e\"x z", sa[1]); + + // """ is quoted + sa = QuotingUtilities.SplitUnquoted("abc\"\"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"\"\"xyz", sa[0]); + + // " "" is quoted + sa = QuotingUtilities.SplitUnquoted("abc\" \"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\" \"\"xyz", sa[0]); + + // "x z" is quoted (the terminal double-quote is assumed) + sa = QuotingUtilities.SplitUnquoted("abc\"\" \"x z"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\"\"", sa[0]); + Assert.AreEqual("\"x z", sa[1]); + + // " "" and "xyz" are quoted (the terminal double-quote is assumed) + sa = QuotingUtilities.SplitUnquoted("abc\" \"\"\"x z"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\" \"\"\"x z", sa[0]); + + // """ is quoted + sa = QuotingUtilities.SplitUnquoted("abc\"\"\"\"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"\"\"\"\"xyz", sa[0]); + + // """ is quoted + // "x z" is not quoted + sa = QuotingUtilities.SplitUnquoted("abc\"\"\"\"\"x z"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\"\"\"\"\"x", sa[0]); + Assert.AreEqual("z", sa[1]); + + // " "" is quoted + sa = QuotingUtilities.SplitUnquoted("abc\" \"\"\"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\" \"\"\"\"xyz", sa[0]); + + // """ and """ are quoted + sa = QuotingUtilities.SplitUnquoted("abc\"\"\"\"\"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"\"\"\"\"\"xyz", sa[0]); + + // " "" and " "" are quoted + sa = QuotingUtilities.SplitUnquoted("abc\" \"\"\" \"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\" \"\"\" \"\"xyz", sa[0]); + + // """ and """ are quoted + sa = QuotingUtilities.SplitUnquoted("abc\"\"\" \"\"\"xyz"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\"\"\"", sa[0]); + Assert.AreEqual("\"\"\"xyz", sa[1]); + + // """ and """ are quoted + sa = QuotingUtilities.SplitUnquoted("abc\"\"\" \"\"\"x z"); + Assert.AreEqual(3, sa.Count); + Assert.AreEqual("abc\"\"\"", sa[0]); + Assert.AreEqual("\"\"\"x", sa[1]); + Assert.AreEqual("z", sa[2]); + + // "c e"" is quoted + sa = QuotingUtilities.SplitUnquoted("abc\"c e\"\"xyz"); + Assert.AreEqual(1, sa.Count); + Assert.AreEqual("abc\"c e\"\"xyz", sa[0]); + + // "c e"" is quoted + // "x z" is not quoted + sa = QuotingUtilities.SplitUnquoted("abc\"c e\"\"x z"); + Assert.AreEqual(2, sa.Count); + Assert.AreEqual("abc\"c e\"\"x", sa[0]); + Assert.AreEqual("z", sa[1]); + + // nothing is quoted + sa = QuotingUtilities.SplitUnquoted("a c\"\"x z"); + Assert.AreEqual(3, sa.Count); + Assert.AreEqual("a", sa[0]); + Assert.AreEqual("c\"\"x", sa[1]); + Assert.AreEqual("z", sa[2]); + + // nothing is quoted + sa = QuotingUtilities.SplitUnquoted("a c\"\"c e\"\"x z"); + Assert.AreEqual(4, sa.Count); + Assert.AreEqual("a", sa[0]); + Assert.AreEqual("c\"\"c", sa[1]); + Assert.AreEqual("e\"\"x", sa[2]); + Assert.AreEqual("z", sa[3]); + } + + [TestMethod] + public void UnquoteTest() + { + int doubleQuotesRemoved; + + // "cde" is quoted + Assert.AreEqual("abccdexyz", QuotingUtilities.Unquote("abc\"cde\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // "xyz" is quoted (the terminal double-quote is assumed) + Assert.AreEqual("abcxyz", QuotingUtilities.Unquote("abc\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(1, doubleQuotesRemoved); + + // "xyz" is quoted (the terminal double-quote is explicit) + Assert.AreEqual("abcxyz", QuotingUtilities.Unquote("abc\"xyz\"", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // "xyz" is quoted (the terminal double-quote is assumed) + Assert.AreEqual("abc\"cdexyz", QuotingUtilities.Unquote("abc\\\"cde\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(1, doubleQuotesRemoved); + + // "cde" is quoted + Assert.AreEqual("abc\\cdexyz", QuotingUtilities.Unquote("abc\\\\\"cde\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // "xyz" is quoted (the terminal double-quote is assumed) + Assert.AreEqual("abc\\\"cdexyz", QuotingUtilities.Unquote("abc\\\\\\\"cde\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(1, doubleQuotesRemoved); + + // """ is quoted + Assert.AreEqual("abc\"xyz", QuotingUtilities.Unquote("abc\"\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // """ and "xyz" are quoted (the terminal double-quote is assumed) + Assert.AreEqual("abc\"xyz", QuotingUtilities.Unquote("abc\"\"\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(3, doubleQuotesRemoved); + + // """ is quoted + Assert.AreEqual("abc\"xyz", QuotingUtilities.Unquote("abc\"\"\"\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(4, doubleQuotesRemoved); + + // """ and """ are quoted + Assert.AreEqual("abc\"\"xyz", QuotingUtilities.Unquote("abc\"\"\"\"\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(4, doubleQuotesRemoved); + + // "cde"" is quoted + Assert.AreEqual("abccde\"xyz", QuotingUtilities.Unquote("abc\"cde\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // "xyz"" is quoted (the terminal double-quote is explicit) + Assert.AreEqual("abcxyz\"", QuotingUtilities.Unquote("abc\"xyz\"\"", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // nothing is quoted + Assert.AreEqual("abcxyz", QuotingUtilities.Unquote("abc\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(2, doubleQuotesRemoved); + + // nothing is quoted + Assert.AreEqual("abccdexyz", QuotingUtilities.Unquote("abc\"\"cde\"\"xyz", out doubleQuotesRemoved)); + Assert.AreEqual(4, doubleQuotesRemoved); + } + + [TestMethod] + public void ExtractSwitchParametersTest() + { + string commandLineArg = "\"/p:foo=\"bar"; + int doubleQuotesRemovedFromArg; + string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":\"foo=\"bar", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(2, doubleQuotesRemovedFromArg); + + commandLineArg = "\"/p:foo=bar\""; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":foo=bar", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(2, doubleQuotesRemovedFromArg); + + commandLineArg = "/p:foo=bar"; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":foo=bar", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(0, doubleQuotesRemovedFromArg); + + commandLineArg = "\"\"/p:foo=bar\""; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":foo=bar\"", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(3, doubleQuotesRemovedFromArg); + + // this test is totally unreal -- we'd never attempt to extract switch parameters if the leading character is not a + // switch indicator (either '-' or '/') -- here the leading character is a double-quote + commandLineArg = "\"\"\"/p:foo=bar\""; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":foo=bar\"", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(3, doubleQuotesRemovedFromArg); + + commandLineArg = "\"/pr\"operty\":foo=bar"; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":foo=bar", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(3, doubleQuotesRemovedFromArg); + + commandLineArg = "\"/pr\"op\"\"erty\":foo=bar\""; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":foo=bar", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(6, doubleQuotesRemovedFromArg); + + commandLineArg = "/p:\"foo foo\"=\"bar bar\";\"baz=onga\""; + unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + Assert.AreEqual(":\"foo foo\"=\"bar bar\";\"baz=onga\"", MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'))); + Assert.AreEqual(6, doubleQuotesRemovedFromArg); + } + + [TestMethod] + public void Help() + { + Assert.AreEqual(MSBuildApp.ExitType.Success, + MSBuildApp.Execute(@"c:\bin\msbuild.exe -? ")); + } + + [TestMethod] + public void ErrorCommandLine() + { + Assert.AreEqual(MSBuildApp.ExitType.SwitchError, + MSBuildApp.Execute(@"c:\bin\msbuild.exe -junk")); + + Assert.AreEqual(MSBuildApp.ExitType.SwitchError, + MSBuildApp.Execute(@"msbuild.exe -t")); + + Assert.AreEqual(MSBuildApp.ExitType.InitializationError, + MSBuildApp.Execute(@"msbuild.exe @bogus.rsp")); + } + + [TestMethod] + public void ValidVerbosities() + { + Assert.AreEqual(LoggerVerbosity.Quiet, MSBuildApp.ProcessVerbositySwitch("Q")); + Assert.AreEqual(LoggerVerbosity.Quiet, MSBuildApp.ProcessVerbositySwitch("quiet")); + Assert.AreEqual(LoggerVerbosity.Minimal, MSBuildApp.ProcessVerbositySwitch("m")); + Assert.AreEqual(LoggerVerbosity.Minimal, MSBuildApp.ProcessVerbositySwitch("minimal")); + Assert.AreEqual(LoggerVerbosity.Normal, MSBuildApp.ProcessVerbositySwitch("N")); + Assert.AreEqual(LoggerVerbosity.Normal, MSBuildApp.ProcessVerbositySwitch("normal")); + Assert.AreEqual(LoggerVerbosity.Detailed, MSBuildApp.ProcessVerbositySwitch("d")); + Assert.AreEqual(LoggerVerbosity.Detailed, MSBuildApp.ProcessVerbositySwitch("detailed")); + Assert.AreEqual(LoggerVerbosity.Diagnostic, MSBuildApp.ProcessVerbositySwitch("diag")); + Assert.AreEqual(LoggerVerbosity.Diagnostic, MSBuildApp.ProcessVerbositySwitch("DIAGNOSTIC")); + } + + [TestMethod] + [ExpectedException(typeof(CommandLineSwitchException))] + public void InvalidVerbosity() + { + MSBuildApp.ProcessVerbositySwitch("loquacious"); + } + + [TestMethod] + public void ValidMaxCPUCountSwitch() + { + Assert.AreEqual(1, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "1" })); + Assert.AreEqual(2, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "2" })); + Assert.AreEqual(3, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "3" })); + Assert.AreEqual(4, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "4" })); + Assert.AreEqual(8, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "8" })); + Assert.AreEqual(63, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "63" })); + + // Should pick last value + Assert.AreEqual(4, MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "8", "4" })); + } + + [TestMethod] + [ExpectedException(typeof(CommandLineSwitchException))] + public void InvalidMaxCPUCountSwitch1() + { + MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "-1" }); + } + + [TestMethod] + [ExpectedException(typeof(CommandLineSwitchException))] + public void InvalidMaxCPUCountSwitch2() + { + MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "0" }); + } + + [TestMethod] + [ExpectedException(typeof(CommandLineSwitchException))] + public void InvalidMaxCPUCountSwitch3() + { + // Too big + MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "foo" }); + } + + [TestMethod] + [ExpectedException(typeof(CommandLineSwitchException))] + public void InvalidMaxCPUCountSwitch4() + { + MSBuildApp.ProcessMaxCPUCountSwitch(new string[] { "1025" }); + } + + /// + /// Regression test for bug where the MSBuild.exe command-line app + /// would sometimes set the UI culture to just "en" which is considered a "neutral" UI + /// culture, which didn't allow for certain kinds of string formatting/parsing. + /// + /// + /// fr-FR, de-DE, and fr-CA are guaranteed to be available on all BVTs, so we must use one of these + /// + [TestMethod] + public void SetConsoleUICulture() + { + Thread thisThread = Thread.CurrentThread; + + // Save the current UI culture, so we can restore it at the end of this unit test. + CultureInfo originalUICulture = thisThread.CurrentUICulture; + + thisThread.CurrentUICulture = new CultureInfo("fr-FR"); + MSBuildApp.SetConsoleUI(); + + // Make sure this doesn't throw an exception. + string bar = String.Format(CultureInfo.CurrentUICulture, "{0}", (int)1); + + // Restore the current UI culture back to the way it was at the beginning of this unit test. + thisThread.CurrentUICulture = originalUICulture; + } + + /// + /// Invalid configuration file should not dump stack. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void ConfigurationInvalid() + { + string startDirectory = null; + string newPathToMSBuildExe = null; + string newPathToMSBuildPdb = null; + string pathToConfigFile = null; + string pathToProjectFile = null; + string output = null; + string oldValueForMSBuildOldOM = null; + + try + { + oldValueForMSBuildOldOM = Environment.GetEnvironmentVariable("MSBuildOldOM"); + Environment.SetEnvironmentVariable("MSBuildOldOM", ""); + + Random rand = new Random(); + startDirectory = Path.Combine(Path.GetTempPath(), Convert.ToString(rand.NextDouble())); + Directory.CreateDirectory(startDirectory); + + string pathToMSBuildExe = ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ToolLocationHelper.CurrentToolsVersion); + newPathToMSBuildExe = Path.Combine(startDirectory, "msbuild.exe"); + File.Copy(pathToMSBuildExe, newPathToMSBuildExe); + + string pathToMSBuildPdb = ToolLocationHelper.GetPathToBuildToolsFile("msbuild.pdb", ToolLocationHelper.CurrentToolsVersion); + newPathToMSBuildPdb = Path.Combine(startDirectory, "msbuild.pdb"); + if (File.Exists(pathToMSBuildPdb)) + { + File.Copy(pathToMSBuildPdb, newPathToMSBuildPdb); + } + + pathToConfigFile = Path.Combine(startDirectory, "msbuild.exe.config"); + string configContent = @" + + +
+ + + + + + + + + + + + + + + + + "; + File.WriteAllText(pathToConfigFile, configContent); + + pathToProjectFile = Path.Combine(startDirectory, "foo.proj"); + string projectString = + "" + + "" + + "" + + ""; + File.WriteAllText(pathToProjectFile, projectString); + + output = RunProcessAndGetOutput(newPathToMSBuildExe, "\"" + pathToProjectFile + "\"", expectSuccess: false); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + throw; + } + finally + { + if (output != null) + { + Console.WriteLine(output); + } + + try + { + // Process does not let go its lock on the exe file until about 1 millisecond after + // p.WaitForExit() returns. Do I know why? No I don't. + RobustDelete(pathToConfigFile); + RobustDelete(newPathToMSBuildExe); + RobustDelete(newPathToMSBuildPdb); + RobustDelete(pathToProjectFile); + RobustDelete(startDirectory); + } + finally + { + Environment.SetEnvironmentVariable("MSBuildOldOM", oldValueForMSBuildOldOM); + } + } + + // If there's a space in the %TEMP% path, the config file is read in the static constructor by the URI class and we catch there; + // if there's not, we will catch when we try to read the toolsets. Either is fine; we just want to not crash. + Assert.IsTrue(output.Contains("MSB1043") || output.Contains("MSB4136")); + } + + /// + /// Try hard to delete a file or directory specified + /// + private void RobustDelete(string path) + { + if (path != null) + { + for (int i = 0; i < 10; i++) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true /*and files*/); + } + else if (File.Exists(path)) + { + File.SetAttributes(path, File.GetAttributes(path) & ~FileAttributes.ReadOnly); // make writeable + File.Delete(path); + } + } + catch (UnauthorizedAccessException) + { + Thread.Sleep(10); + break; + } + } + } + } + + /// + /// Run the process and get stdout and stderr + /// + private string RunProcessAndGetOutput(string process, string parameters, bool expectSuccess = true) + { + ProcessStartInfo psi = new ProcessStartInfo(process); + psi.CreateNoWindow = true; + psi.RedirectStandardInput = true; + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + psi.UseShellExecute = false; + psi.Arguments = parameters; + string output = String.Empty; + int exitCode = 1; + Process p = new Process(); + p.EnableRaisingEvents = true; + p.StartInfo = psi; + + p.OutputDataReceived += delegate (object sender, DataReceivedEventArgs args) + { + if (args != null) + { + output += args.Data + "\r\n"; + } + }; + + p.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs args) + { + if (args != null) + { + output += args.Data + "\r\n"; + } + }; + + Console.WriteLine("Executing [{0} {1}]", process, parameters); + + p.Start(); + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + p.StandardInput.Close(); + p.WaitForExit(); + + exitCode = p.ExitCode; + p.Close(); + + Console.WriteLine("==== OUTPUT ===="); + Console.WriteLine(output); + Console.WriteLine("=============="); + + Assert.AreEqual(expectSuccess, (exitCode == 0)); + + return output; + } + + /// + /// Tests that the environment gets passed on to the node during build. + /// + [TestMethod] + public void TestEnvironment() + { + string projectString = ObjectModelHelpers.CleanupFileContents( + @" + + + "); + string tempdir = Path.GetTempPath(); + string projectFileName = tempdir + "\\msbEnvironmenttest.proj"; + string quotedProjectFileName = "\"" + projectFileName + "\""; + + try + { + Environment.SetEnvironmentVariable("MyEnvVariable", "1"); + using (StreamWriter sw = new StreamWriter(projectFileName)) + { + sw.WriteLine(projectString); + } + //Should pass + Assert.AreEqual(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\bin\msbuild.exe " + quotedProjectFileName)); + } + finally + { + Environment.SetEnvironmentVariable("MyEnvVariable", null); + File.Delete(projectFileName); + } + } + + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void MSBuildEngineLogger() + { + string projectString = + "" + + "" + + "" + + ""; + string tempdir = Path.GetTempPath(); + string projectFileName = tempdir + "\\msbLoggertest.proj"; + string quotedProjectFileName = "\"" + projectFileName + "\""; + + try + { + using (StreamWriter sw = new StreamWriter(projectFileName)) + { + sw.WriteLine(projectString); + } + //Should pass + Assert.AreEqual(MSBuildApp.ExitType.Success, + MSBuildApp.Execute(@"c:\bin\msbuild.exe /logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"" " + quotedProjectFileName)); + + //Should fail as we are not changing existing lines + Assert.AreEqual(MSBuildApp.ExitType.InitializationError, + MSBuildApp.Execute(@"c:\bin\msbuild.exe /logger:FileLogger,Microsoft.Build,Version=11111 " + quotedProjectFileName)); + } + finally + { + File.Delete(projectFileName); + } + } + + private string _pathToMSBuildExe = Path.Combine(Environment.CurrentDirectory, "MSBuild.exe"); + private string _pathToArbitraryBogusFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "notepad.exe"); // OK on 64 bit as well + + /// + /// Basic case + /// + [TestMethod] + public void GetCommandLine() + { + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + _pathToArbitraryBogusFile + "\"" + " /v:diag", expectSuccess: false); + + Assert.IsTrue(output.Contains(_pathToMSBuildExe + " /v:diag " + _pathToArbitraryBogusFile)); + } + + /// + /// Quoted path + /// + [TestMethod] + public void GetCommandLineQuotedExe() + { + string quotedPathToMSBuildExe = "\"" + _pathToMSBuildExe + "\""; + string output = RunProcessAndGetOutput(quotedPathToMSBuildExe, "\"" + _pathToArbitraryBogusFile + "\"" + " /v:diag", expectSuccess: false); + + Assert.IsTrue(output.Contains(_pathToMSBuildExe + " /v:diag " + _pathToArbitraryBogusFile)); + } + + /// + /// On path + /// + [TestMethod] + public void GetCommandLineQuotedExeOnPath() + { + string output = null; + string current = Environment.CurrentDirectory; + + try + { + Environment.CurrentDirectory = Path.GetDirectoryName(_pathToMSBuildExe); + + output = RunProcessAndGetOutput("msbuild.exe", "\"" + _pathToArbitraryBogusFile + "\"" + " /v:diag", expectSuccess: false); + } + finally + { + Environment.CurrentDirectory = current; + } + + Assert.IsTrue(output.IndexOf(_pathToMSBuildExe + " /v:diag " + _pathToArbitraryBogusFile, StringComparison.OrdinalIgnoreCase) >= 0); + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, and should + /// take priority over any other response files. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ResponseFileInProjectDirectoryFoundImplicitly() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + + string currentDirectory = Environment.CurrentDirectory; + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string rspContent = "/p:A=1"; + File.WriteAllText(rspPath, rspContent); + + // Find the project in the current directory + Directory.SetCurrentDirectory(directory); + string output = RunProcessAndGetOutput(_pathToMSBuildExe, String.Empty); + Assert.IsTrue(output.Contains("[A=1]")); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, and should + /// take priority over any other response files. + /// + [TestMethod] + public void ResponseFileInProjectDirectoryExplicit() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string rspContent = "/p:A=1"; + File.WriteAllText(rspPath, rspContent); + + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + projectPath + "\""); + Assert.IsTrue(output.Contains("[A=1]")); + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, and not any random .rsp + /// + [TestMethod] + public void ResponseFileInProjectDirectoryRandomName() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "foo.rsp"); + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string rspContent = "/p:A=1"; + File.WriteAllText(rspPath, rspContent); + + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + projectPath + "\""); + Assert.IsTrue(output.Contains("[A=]")); + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, + /// but lower precedence than the actual command line + /// + [TestMethod] + public void ResponseFileInProjectDirectoryCommandLineWins() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string rspContent = "/p:A=1"; + File.WriteAllText(rspPath, rspContent); + + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + projectPath + "\"" + " /p:A=2"); + Assert.IsTrue(output.Contains("[A=2]")); + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, + /// but lower precedence than the actual command line and higher than the msbuild.rsp next to msbuild.exe + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void ResponseFileInProjectDirectoryWinsOverMainMSBuildRsp() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + + string exeDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string exePath = Path.Combine(exeDirectory, "msbuild.exe"); + string mainRspPath = Path.Combine(exeDirectory, "msbuild.rsp"); + + try + { + Directory.CreateDirectory(directory); + Directory.CreateDirectory(exeDirectory); + + File.Copy(_pathToMSBuildExe, exePath); + + File.WriteAllText(mainRspPath, "/p:A=0"); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + File.WriteAllText(rspPath, "/p:A=1"); + + string output = RunProcessAndGetOutput(exePath, "\"" + projectPath + "\""); + Assert.IsTrue(output.Contains("[A=1]")); + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + + File.Delete(exePath); + File.Delete(mainRspPath); + Directory.Delete(exeDirectory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, + /// but not if it's the same as the msbuild.exe directory + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void ProjectDirectoryIsMSBuildExeDirectory() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + string exePath = Path.Combine(directory, "msbuild.exe"); + + try + { + Directory.CreateDirectory(directory); + + File.Copy(_pathToMSBuildExe, exePath); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + File.WriteAllText(rspPath, "/p:A=1"); + + string output = RunProcessAndGetOutput(exePath, "\"" + projectPath + "\""); + Assert.IsTrue(output.Contains("[A=1]")); + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + File.Delete(exePath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution with /noautoresponse in, is an error + /// + [TestMethod] + public void ResponseFileInProjectDirectoryItselfWithNoAutoResponseSwitch() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string rspContent = "/p:A=1 /noautoresponse"; + File.WriteAllText(rspPath, rspContent); + + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + projectPath + "\"", expectSuccess: false); + Assert.IsTrue(output.Contains("MSB1027")); // msbuild.rsp cannot have /noautoresponse in it + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be ignored if cmd line has /noautoresponse + /// + [TestMethod] + public void ResponseFileInProjectDirectoryButCommandLineNoAutoResponseSwitch() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + string rspPath = Path.Combine(directory, "msbuild.rsp"); + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string rspContent = "/p:A=1 /noautoresponse"; + File.WriteAllText(rspPath, rspContent); + + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + projectPath + "\" /noautoresponse", expectSuccess: true); + Assert.IsTrue(output.Contains("[A=]")); + } + finally + { + File.Delete(projectPath); + File.Delete(rspPath); + Directory.Delete(directory); + } + } + + /// + /// Any msbuild.rsp in the directory of the specified project/solution should be read, and should + /// take priority over any other response files. Sanity test when there isn't one. + /// + [TestMethod] + public void ResponseFileInProjectDirectoryNullCase() + { + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + string projectPath = Path.Combine(directory, "my.proj"); + + try + { + Directory.CreateDirectory(directory); + + string content = ObjectModelHelpers.CleanupFileContents(""); + File.WriteAllText(projectPath, content); + + string output = RunProcessAndGetOutput(_pathToMSBuildExe, "\"" + projectPath + "\""); + Assert.IsTrue(output.Contains("[A=]")); + } + finally + { + File.Delete(projectPath); + Directory.Delete(directory); + } + } + + #region IgnoreProjectExtensionTests + + /// + /// Test the case where the extension is a valid extension but is not a project + /// file extension. In this case no files should be ignored + /// + [TestMethod] + public void TestProcessProjectSwitchOneProjNotFoundExtension() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { ".phantomextension" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + } + + /// + /// Test the case where two identical extensions are asked to be ignored + /// + [TestMethod] + public void TestTwoIdenticalExtensionsToIgnore() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { ".phantomextension", ".phantomextension" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + } + /// + /// Pass a null and an empty list of project extensions to ignore, this simulates the switch not being set on the commandline + /// + [TestMethod] + public void TestProcessProjectSwitchNullandEmptyProjectsToIgnore() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = (string[])null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + + extensionsToIgnore = new string[] { }; + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + } + + /// + /// Pass in one extension and a null value + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchNullInList() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { ".phantomextension", null }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + } + + /// + /// Pass in one extension and an empty string + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchEmptyInList() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { ".phantomextension", string.Empty }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + } + + /// + /// If only a dot is specified then the extension is invalid + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchExtensionWithoutDot() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { "phantomextension" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0); + } + + /// + /// Put some junk into the extension, in this case there should be an exception + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchMalformed() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { ".C:\\boocatmoo.a" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("my.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected my.proj to be only project found"); + } + + /// + /// Test what happens if there are no project or solution files in the directory + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchWildcards() + { + string[] projects = new string[] { "my.proj" }; + string[] extensionsToIgnore = new string[] { ".proj*", ".nativeproj?" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + // Should not get here + Assert.Fail(); + } + + [TestMethod] + public void TestProcessProjectSwitch() + { + string[] projects = new string[] { "test.nativeproj", "test.vcproj" }; + string[] extensionsToIgnore = new string[] { ".phantomextension", ".vcproj" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.nativeproj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.nativeproj to be only project found"); + + projects = new string[] { "test.nativeproj", "test.vcproj", "test.proj" }; + extensionsToIgnore = new string[] { ".phantomextension", ".vcproj" }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.proj to be only project found"); + + projects = new string[] { "test.nativeproj", "test.vcproj" }; + extensionsToIgnore = new string[] { ".vcproj" }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.nativeproj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.nativeproj to be only project found"); + + projects = new string[] { "test.proj", "test.sln" }; + extensionsToIgnore = new string[] { ".vcproj" }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.sln", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.sln to be only solution found"); + + projects = new string[] { "test.proj", "test.sln", "test.proj~", "test.sln~" }; + extensionsToIgnore = new string[] { }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.sln", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.sln to be only solution found"); + + projects = new string[] { "test.proj" }; + extensionsToIgnore = new string[] { }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.proj to be only project found"); + + projects = new string[] { "test.proj", "test.proj~" }; + extensionsToIgnore = new string[] { }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.proj to be only project found"); + + projects = new string[] { "test.sln" }; + extensionsToIgnore = new string[] { }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.sln", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.sln to be only solution found"); + + projects = new string[] { "test.sln", "test.sln~" }; + extensionsToIgnore = new string[] { }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.sln", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.sln to be only solution found"); + + + projects = new string[] { "test.sln~", "test.sln" }; + extensionsToIgnore = new string[] { }; + projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.sln", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.sln to be only solution found"); + } + + + /// + /// Ignore .sln and .vcproj files to replicate Building_DF_LKG functionality + /// + [TestMethod] + public void TestProcessProjectSwitchReplicateBuildingDFLKG() + { + string[] projects = new string[] { "test.proj", "test.sln", "Foo.vcproj" }; + string[] extensionsToIgnore = { ".sln", ".vcproj" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + Assert.IsTrue(String.Compare("test.proj", MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles), StringComparison.OrdinalIgnoreCase) == 0, "Expected test.proj to be only project found"); + } + + + /// + /// Test the case where we remove all of the project extensions that exist in the directory + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchRemovedAllprojects() + { + string[] projects; + string[] extensionsToIgnore = null; + projects = new string[] { "test.nativeproj", "test.vcproj" }; + extensionsToIgnore = new string[] { ".nativeproj", ".vcproj" }; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + Assert.Fail(); + } + + /// + /// Test the case where there is a solution and a project in the same directory but they have different names + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchSlnProjDifferentNames() + { + string[] projects = new string[] { "test.proj", "Different.sln" }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + // Should not get here + Assert.Fail(); + } + + /// + /// Test the case where we have two proj files in the same directory + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchTwoProj() + { + string[] projects = new string[] { "test.proj", "Different.proj" }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + + // Should not get here + Assert.Fail(); + } + + /// + /// Test the case where we have two native project files in the same directory + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchTwoNative() + { + string[] projects = new string[] { "test.nativeproj", "Different.nativeproj" }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + + // Should not get here + Assert.Fail(); + } + + /// + /// Test when there are two solutions in the smae directory + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchTwoSolutions() + { + string[] projects = new string[] { "test.sln", "Different.sln" }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + // Should not get here + Assert.Fail(); + } + + /// + /// Check the case where there are more than two projects in the directory and one is a proj file + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchMoreThenTwoProj() + { + string[] projects = new string[] { "test.nativeproj", "Different.csproj", "Another.proj" }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + // Should not get here + Assert.Fail(); + } + + /// + /// Test what happens if there are no project or solution files in the directory + /// + [TestMethod] + [ExpectedException(typeof(InitializationException))] + public void TestProcessProjectSwitchNoProjectOrSolution() + { + string[] projects = new string[] { }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(new string[0] { }, extensionsToIgnore, projectHelper.GetFiles); + // Should not get here + Assert.Fail(); + } + + /// + /// Helper class to simulate directory work for ignore project extensions + /// + internal class IgnoreProjectExtensionsHelper + { + private List _directoryFileNameList; + + /// + /// Takes in a list of file names to simulate as being in a directory + /// + /// + internal IgnoreProjectExtensionsHelper(string[] filesInDirectory) + { + _directoryFileNameList = new List(); + foreach (string file in filesInDirectory) + { + _directoryFileNameList.Add(file); + } + } + + /// + /// Mocks System.IO.GetFiles. Takes in known search patterns and returns files which + /// are provided through the constructor + /// + /// not used + /// Pattern of files to return + /// + internal string[] GetFiles(string path, string searchPattern) + { + List fileNamesToReturn = new List(); + foreach (string file in _directoryFileNameList) + { + if (String.Compare(searchPattern, "*.sln", StringComparison.OrdinalIgnoreCase) == 0) + { + if (String.Compare(Path.GetExtension(file), ".sln", StringComparison.OrdinalIgnoreCase) == 0) + { + fileNamesToReturn.Add(file); + } + } + else if (String.Compare(searchPattern, "*.*proj", StringComparison.OrdinalIgnoreCase) == 0) + { + if (Path.GetExtension(file).Contains("proj")) + { + fileNamesToReturn.Add(file); + } + } + } + return fileNamesToReturn.ToArray(); + } + } + #endregion + + #region ProcessFileLoggerSwitches + /// + /// Test the case where no file logger switches are given, should be no file loggers attached + /// + [TestMethod] + public void TestProcessFileLoggerSwitch1() + { + bool distributedFileLogger = false; + string[] fileLoggerParameters = null; + List distributedLoggerRecords = new List(); + + ArrayList loggers = new ArrayList(); + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(distributedLoggerRecords.Count == 0, "Expected no distributed loggers to be attached"); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + } + + /// + /// Test the case where a central logger and distributed logger are attached + /// + [TestMethod] + public void TestProcessFileLoggerSwitch2() + { + bool distributedFileLogger = true; + string[] fileLoggerParameters = null; + List distributedLoggerRecords = new List(); + + ArrayList loggers = new ArrayList(); + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected one distributed loggers to be attached"); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + } + + /// + /// Test the case where a central file logger is attached but no distributed logger + /// + [TestMethod] + public void TestProcessFileLoggerSwitch3() + { + bool distributedFileLogger = false; + string[] fileLoggerParameters = null; + List distributedLoggerRecords = new List(); + + ArrayList loggers = new ArrayList(); + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(distributedLoggerRecords.Count == 0, "Expected no distributed loggers to be attached"); + Assert.IsTrue(loggers.Count == 0, "Expected a central loggers to be attached"); + + // add a set of parameters and make sure the logger has those parameters + distributedLoggerRecords = new List(); + + loggers = new ArrayList(); + fileLoggerParameters = new string[1] { "Parameter" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(distributedLoggerRecords.Count == 0, "Expected no distributed loggers to be attached"); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + + distributedLoggerRecords = new List(); + + loggers = new ArrayList(); + fileLoggerParameters = new string[2] { "Parameter1", "Parameter" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(distributedLoggerRecords.Count == 0, "Expected no distributed loggers to be attached"); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + } + + /// + /// Test the case where a distributed file logger is attached but no central logger + /// + [TestMethod] + public void TestProcessFileLoggerSwitch4() + { + bool distributedFileLogger = true; + string[] fileLoggerParameters = null; + List distributedLoggerRecords = new List(); + + ArrayList loggers = new ArrayList(); + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, "logFile=" + Path.Combine(Environment.CurrentDirectory, "MSBuild.log"), StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + + // Not add a set of parameters and make sure the logger has those parameters + distributedLoggerRecords = new List(); + + loggers = new ArrayList(); + fileLoggerParameters = new string[1] { "verbosity=Normal;" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, fileLoggerParameters[0] + ";logFile=" + Path.Combine(Environment.CurrentDirectory, "MSBuild.log"), StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + + // Not add a set of parameters and make sure the logger has those parameters + distributedLoggerRecords = new List(); + + loggers = new ArrayList(); + fileLoggerParameters = new string[2] { "verbosity=Normal", "" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, fileLoggerParameters[0] + ";logFile=" + Path.Combine(Environment.CurrentDirectory, "MSBuild.log"), StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + + // Not add a set of parameters and make sure the logger has those parameters + distributedLoggerRecords = new List(); + + loggers = new ArrayList(); + fileLoggerParameters = new string[2] { "", "Parameter1" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, ";Parameter1;logFile=" + Path.Combine(Environment.CurrentDirectory, "MSBuild.log"), StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + + + // Not add a set of parameters and make sure the logger has those parameters + distributedLoggerRecords = new List(); + + loggers = new ArrayList(); + fileLoggerParameters = new string[2] { "Parameter1", "verbosity=Normal;logfile=c:\\temp\\cat.log" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, fileLoggerParameters[0] + ";" + fileLoggerParameters[1], StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + + distributedLoggerRecords = new List(); + loggers = new ArrayList(); + fileLoggerParameters = new string[2] { "Parameter1", "verbosity=Normal;logfile=..\\cat.log;Parameter1" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, "Parameter1;verbosity=Normal;logFile=" + Path.Combine(Environment.CurrentDirectory, "..\\cat.log;Parameter1"), StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + + loggers = new ArrayList(); + distributedLoggerRecords = new List(); + fileLoggerParameters = new string[6] { "Parameter1", ";Parameter;", "", ";", ";Parameter", "Parameter;" }; + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 2 + ); + Console.WriteLine(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters); + Assert.IsTrue(string.Compare(((DistributedLoggerRecord)distributedLoggerRecords[0]).ForwardingLoggerDescription.LoggerSwitchParameters, "Parameter1;Parameter;;;Parameter;Parameter;logFile=" + Path.Combine(Environment.CurrentDirectory, "msbuild.log"), StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + } + + /// + /// Verify when in single proc mode the file logger enables mp logging and does not show eventId + /// + [TestMethod] + public void TestProcessFileLoggerSwitch5() + { + bool distributedFileLogger = false; + string[] fileLoggerParameters = null; + List distributedLoggerRecords = new List(); + + ArrayList loggers = new ArrayList(); + MSBuildApp.ProcessDistributedFileLogger + ( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords, + loggers, + 1 + ); + Assert.IsTrue(distributedLoggerRecords.Count == 0, "Expected no distributed loggers to be attached"); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + } + #endregion + + #region ProcessConsoleLoggerSwitches + [TestMethod] + public void ProcessConsoleLoggerSwitches() + { + ArrayList loggers = new ArrayList(); + LoggerVerbosity verbosity = LoggerVerbosity.Normal; + List distributedLoggerRecords = new List(); ; + string[] consoleLoggerParameters = new string[6] { "Parameter1", ";Parameter;", "", ";", ";Parameter", "Parameter;" }; + + MSBuildApp.ProcessConsoleLoggerSwitch + ( + true, + consoleLoggerParameters, + distributedLoggerRecords, + verbosity, + 1, + loggers + ); + Assert.IsTrue(loggers.Count == 0, "Expected no central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 0, "Expected no distributed loggers to be attached"); + + MSBuildApp.ProcessConsoleLoggerSwitch + ( + false, + consoleLoggerParameters, + distributedLoggerRecords, + verbosity, + 1, + loggers + ); + Assert.IsTrue(loggers.Count == 1, "Expected a central loggers to be attached"); + Assert.IsTrue(string.Compare(((ILogger)loggers[0]).Parameters, "EnableMPLogging;SHOWPROJECTFILE=TRUE;Parameter1;Parameter;;;parameter;Parameter", StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameters passed in"); + + MSBuildApp.ProcessConsoleLoggerSwitch + ( + false, + consoleLoggerParameters, + distributedLoggerRecords, + verbosity, + 2, + loggers + ); + Assert.IsTrue(loggers.Count == 1, "Expected a central loggers to be attached"); + Assert.IsTrue(distributedLoggerRecords.Count == 1, "Expected a distributed logger to be attached"); + DistributedLoggerRecord distributedLogger = ((DistributedLoggerRecord)distributedLoggerRecords[0]); + Assert.IsTrue(string.Compare(distributedLogger.CentralLogger.Parameters, "SHOWPROJECTFILE=TRUE;Parameter1;Parameter;;;parameter;Parameter", StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameters passed in"); + Assert.IsTrue(string.Compare(distributedLogger.ForwardingLoggerDescription.LoggerSwitchParameters, "SHOWPROJECTFILE=TRUE;Parameter1;Parameter;;;Parameter;Parameter", StringComparison.OrdinalIgnoreCase) == 0, "Expected parameter in logger to match parameter passed in"); + } + #endregion + } +} diff --git a/src/XMakeCommandLine/XMake.cs b/src/XMakeCommandLine/XMake.cs new file mode 100644 index 00000000000..13a0e54b9ca --- /dev/null +++ b/src/XMakeCommandLine/XMake.cs @@ -0,0 +1,2998 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Configuration; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Security; +using System.Text; +using System.Threading; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Exceptions; +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif +#if MSBUILDENABLEVSPROFILING +using Microsoft.VisualStudio.Profiler; +#endif + +using FileLogger = Microsoft.Build.Logging.FileLogger; +using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger; +using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; +using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +using System.Runtime.CompilerServices; + +namespace Microsoft.Build.CommandLine +{ + /// + /// This class implements the MSBuild.exe command-line application. It processes + /// command-line arguments and invokes the build engine. + /// + static public class MSBuildApp + { + /// + /// Enumeration of the various ways in which the MSBuild.exe application can exit. + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "shipped already")] + public enum ExitType + { + /// + /// The application executed successfully. + /// + Success, + /// + /// There was a syntax error in a command line argument. + /// + SwitchError, + /// + /// A command line argument was not valid. + /// + InitializationError, + /// + /// The build failed. + /// + BuildError, + /// + /// A logger aborted the build. + /// + LoggerAbort, + /// + /// A logger failed unexpectedly. + /// + LoggerFailure, + /// + /// The build stopped unexpectedly, for example, + /// because a child died or hung. + /// + Unexpected + } + + /// + /// Whether the static constructor ran successfully. + /// + private static bool s_initialized; + + /// + /// The object used to synchronize access to shared build state + /// + private static Object s_buildLock = new Object(); + + /// + /// The currently active build, if any. + /// + private static BuildSubmission s_activeBuild; + + /// + /// Event signalled when the build is complete. + /// + private static ManualResetEvent s_buildComplete = new ManualResetEvent(false); + + /// + /// Event signalled when the cancel method is complete. + /// + private static ManualResetEvent s_cancelComplete = new ManualResetEvent(true); + + /// + /// Set to 1 when the cancel method has been invoked. + /// Never reset to false: subsequent hits of Ctrl-C should do nothing + /// + private static int s_receivedCancel; + + /// + /// Static constructor + /// + static MSBuildApp() + { + try + { + //////////////////////////////////////////////////////////////////////////////// + // Only initialize static fields here, not inline! // + // This forces the type to initialize in this static constructor and thus // + // any configuration file exceptions can be caught here. // + //////////////////////////////////////////////////////////////////////////////// + s_exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); + + s_initialized = true; + } + catch (TypeInitializationException ex) + { + if (ex.InnerException == null || ex.InnerException.GetType() != typeof(ConfigurationErrorsException)) + { + throw; + } + HandleConfigurationException(ex); + } + catch (ConfigurationException ex) + { + HandleConfigurationException(ex); + } + } + + /// + /// Static no-op method to force static constructor to run and initialize resources. + /// This is useful for unit tests. + /// + internal static void Initialize() + { + } + + /// + /// Dump any exceptions reading the configuration file, nicely + /// + private static void HandleConfigurationException(Exception ex) + { + // Error reading the configuration file - eg., unexpected element + // Since we expect users to edit it to add toolsets, this is not unreasonable to expect + StringBuilder builder = new StringBuilder(); + + Exception exception = ex; + do + { + string message = exception.Message.TrimEnd(); + + builder.Append(message); + + // One of the exceptions is missing a period! + if (message[message.Length - 1] != '.') + { + builder.Append("."); + } + builder.Append(" "); + + exception = exception.InnerException; + } + while (exception != null); + + Console.WriteLine(ResourceUtilities.FormatResourceString("InvalidConfigurationFile", builder.ToString())); + + s_initialized = false; + } + + /// + /// This is the entry point for the application. + /// + /// + /// MSBuild no longer runs any arbitrary code (tasks or loggers) on the main thread, so it never needs the + /// main thread to be in an STA. Accordingly, to avoid ambiguity, we explicitly use the [MTAThread] attribute. + /// This doesn't actually do any work unless COM interop occurs for some reason. + /// + /// 0 on success, 1 on failure + [MTAThread] + public static int Main() + { + if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") + { + DumpCounters(true /* initialize only */); + } + + // return 0 on success, non-zero on failure + int exitCode = ((s_initialized && Execute(Environment.CommandLine) == ExitType.Success) ? 0 : 1); + + if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") + { + DumpCounters(false /* log to console */); + } + + return exitCode; + } + + /// + /// Append output file with elapsedTime + /// + /// + /// This is a non-supported feature to facilitate timing multiple runs + /// + static private void AppendOutputFile(string path, Int64 elapsedTime) + { + if (!File.Exists(path)) + { + using (StreamWriter sw = File.CreateText(path)) + { + sw.WriteLine(elapsedTime); + } + } + else + { + using (StreamWriter sw = File.AppendText(path)) + { + sw.WriteLine(elapsedTime); + } + } + } + + /// + /// Dump process counters in parseable format. + /// These can't be gotten after the process ends, so log them here. + /// These are for the current process only: remote nodes are not counted. + /// + /// + /// Because some of these counters give bogus results or are poorly defined, + /// we only dump counters if an undocumented environment variable is set. + /// Also, the strings are not localized. + /// Before execution, this is called with initialize only, causing counters to get called with NextValue() to + /// initialize them. + /// + private static void DumpCounters(bool initializeOnly) + { + Process currentProcess = Process.GetCurrentProcess(); + + if (!initializeOnly) + { + Console.WriteLine("\n{0}{1}{0}", new String('=', 41 - "Process".Length / 2), "Process"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Peak Working Set", currentProcess.PeakWorkingSet64, "bytes"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Peak Paged Memory", currentProcess.PeakPagedMemorySize64, "bytes"); // Not very useful one + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Peak Virtual Memory", currentProcess.PeakVirtualMemorySize64, "bytes"); // Not very useful one + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Peak Privileged Processor Time", currentProcess.PrivilegedProcessorTime.TotalMilliseconds, "ms"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Peak User Processor Time", currentProcess.UserProcessorTime.TotalMilliseconds, "ms"); + Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Peak Total Processor Time", currentProcess.TotalProcessorTime.TotalMilliseconds, "ms"); + + Console.WriteLine("{0}{0}", new String('=', 41)); + } + + // Now some Windows performance counters + + // First get the instance name of this process, in order to look them up. + // Generally, the instance names, such as "msbuild" and "msbuild#2" are non deterministic; we want this process. + // Don't use the "ID Process" counter out of the "Process" category, as it doesn't use the same naming scheme + // as the .NET counters. However, the "Process ID" counter out of the ".NET CLR Memory" category apparently uses + // the same scheme as the other .NET categories. + string currentInstance = null; + PerformanceCounterCategory processCategory = new PerformanceCounterCategory("Process"); + foreach (string instance in processCategory.GetInstanceNames()) + { + using (PerformanceCounter counter = new PerformanceCounter(".NET CLR Memory", "Process ID", instance, true)) + { + try + { + if ((int)counter.RawValue == currentProcess.Id) + { + currentInstance = instance; + break; + } + } + catch (InvalidOperationException) // Instance 'WmiApSrv' does not exist in the specified Category. (??) + { + } + } + } + + foreach (PerformanceCounterCategory category in PerformanceCounterCategory.GetCategories()) + { + DumpAllInCategory(currentInstance, category, initializeOnly); + } + } + + /// + /// Dumps all counters in the category + /// + private static void DumpAllInCategory(string currentInstance, PerformanceCounterCategory category, bool initializeOnly) + { + if (category.CategoryName.IndexOf("remoting", StringComparison.OrdinalIgnoreCase) != -1) // not interesting + { + return; + } + + PerformanceCounter[] counters; + try + { + counters = category.GetCounters(currentInstance); + } + catch (InvalidOperationException) + { + // This is a system-wide category, ignore those + return; + } + + if (!initializeOnly) + { + Console.WriteLine("\n{0}{1}{0}", new String('=', 41 - category.CategoryName.Length / 2), category.CategoryName); + } + + foreach (PerformanceCounter counter in counters) + { + DumpCounter(counter, initializeOnly); + } + + if (!initializeOnly) + { + Console.WriteLine("{0}{0}", new String('=', 41)); + } + } + + /// + /// Dumps one counter + /// + private static void DumpCounter(PerformanceCounter counter, bool initializeOnly) + { + try + { + if (counter.CounterName.IndexOf("not displayed", StringComparison.OrdinalIgnoreCase) != -1) + { + return; + } + + float value = counter.NextValue(); + + if (!initializeOnly) + { + string friendlyCounterType = GetFriendlyCounterType(counter.CounterType, counter.CounterName); + string valueFormat; + + // At least some (such as % in GC; maybe all) "%" counters are already multiplied by 100. So we don't do that here. + + // Show decimal places if meaningful + valueFormat = value < 10 ? "{0,20:N2}" : "{0,20:N0}"; + + string valueString = String.Format(CultureInfo.CurrentCulture, valueFormat, value); + + Console.WriteLine("||{0,50}|{1}|{2,8}|", counter.CounterName, valueString, friendlyCounterType); + } + } + catch (InvalidOperationException) // Instance 'WmiApSrv' does not exist in the specified Category. (??) + { + } + } + + /// + /// Gets a friendly representation of the counter units + /// + private static string GetFriendlyCounterType(PerformanceCounterType type, string name) + { + if (name.IndexOf("bytes", StringComparison.OrdinalIgnoreCase) != -1) + { + return "bytes"; + } + + if (name.IndexOf("threads", StringComparison.OrdinalIgnoreCase) != -1) + { + return "threads"; + } + + switch (type) + { + case PerformanceCounterType.ElapsedTime: + case PerformanceCounterType.AverageTimer32: + return "s"; + + case PerformanceCounterType.Timer100Ns: + case PerformanceCounterType.Timer100NsInverse: + return "100ns"; + + case PerformanceCounterType.SampleCounter: + case PerformanceCounterType.AverageCount64: + case PerformanceCounterType.NumberOfItems32: + case PerformanceCounterType.NumberOfItems64: + case PerformanceCounterType.NumberOfItemsHEX32: + case PerformanceCounterType.NumberOfItemsHEX64: + case PerformanceCounterType.RateOfCountsPerSecond32: + case PerformanceCounterType.RateOfCountsPerSecond64: + case PerformanceCounterType.CountPerTimeInterval32: + case PerformanceCounterType.CountPerTimeInterval64: + case PerformanceCounterType.CounterTimer: + case PerformanceCounterType.CounterTimerInverse: + case PerformanceCounterType.CounterMultiTimer: + case PerformanceCounterType.CounterMultiTimerInverse: + case PerformanceCounterType.CounterDelta32: + case PerformanceCounterType.CounterDelta64: + return "#"; + + case PerformanceCounterType.CounterMultiTimer100Ns: + case PerformanceCounterType.CounterMultiTimer100NsInverse: + case PerformanceCounterType.RawFraction: + case PerformanceCounterType.SampleFraction: + return "%"; + + case PerformanceCounterType.AverageBase: + case PerformanceCounterType.RawBase: + case PerformanceCounterType.SampleBase: + case PerformanceCounterType.CounterMultiBase: + default: + return "?"; + } + } + + /// + /// Orchestrates the execution of the application, and is also responsible + /// for top-level error handling. + /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and + /// is ignored. + /// A value of type ExitType that indicates whether the build succeeded, + /// or the manner in which it failed. + public static ExitType Execute(string commandLine) + { + // Indicate to the engine that it can toss extraneous file content + // when it loads microsoft.*.targets. We can't do this in the general case, + // because tasks in the build can (and occasionally do) load MSBuild format files + // with our OM and modify and save them. They'll never do this for Microsoft.*.targets, though, + // and those form the great majority of our unnecessary memory use. + Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); + + string debugFlag = Environment.GetEnvironmentVariable("MSBUILDDEBUGONSTART"); + if (debugFlag == "1") + { + Debugger.Launch(); + } + else if (debugFlag == "2") + { + // Sometimes easier to attach rather than deal with JIT prompt + Console.ReadLine(); + } + + ErrorUtilities.VerifyThrowArgumentLength(commandLine, "commandLine"); + + ExitType exitType = ExitType.Success; + + ConsoleCancelEventHandler cancelHandler = new ConsoleCancelEventHandler(Console_CancelKeyPress); + try + { +#if (!STANDALONEBUILD) + // Enable CodeMarkers for MSBuild.exe + CodeMarkers.Instance.InitPerformanceDll(CodeMarkerApp.MSBUILDPERF, @"Software\Microsoft\MSBuild\4.0"); +#endif +#if MSBUILDENABLEVSPROFILING + string startMSBuildExe = String.Format(CultureInfo.CurrentCulture, "Running MSBuild.exe with command line {0}", commandLine); + DataCollection.CommentMarkProfile(8800, startMSBuildExe); +#endif + Console.CancelKeyPress += cancelHandler; + + // check the operating system the code is running on + VerifyThrowSupportedOS(); + + // Setup the console UI. + SetConsoleUI(); + + // reset the application state for this new build + ResetBuildState(); + + // process the detected command line switches -- gather build information, take action on non-build switches, and + // check for non-trivial errors + string projectFile = null; + string[] targets = { }; + string toolsVersion = null; + Dictionary globalProperties = null; + ILogger[] loggers = { }; + LoggerVerbosity verbosity = LoggerVerbosity.Normal; + List distributedLoggerRecords = null; + bool needToValidateProject = false; + string schemaFile = null; + int cpuCount = 1; + bool enableNodeReuse = true; + TextWriter preprocessWriter = null; + bool debugger = false; + bool detailedSummary = false; + + CommandLineSwitches switchesFromAutoResponseFile; + CommandLineSwitches switchesNotFromAutoResponseFile; + GatherAllSwitches(commandLine, out switchesFromAutoResponseFile, out switchesNotFromAutoResponseFile); + + if (ProcessCommandLineSwitches( + switchesFromAutoResponseFile, + switchesNotFromAutoResponseFile, + ref projectFile, + ref targets, + ref toolsVersion, + ref globalProperties, + ref loggers, + ref verbosity, + ref distributedLoggerRecords, + ref needToValidateProject, + ref schemaFile, + ref cpuCount, + ref enableNodeReuse, + ref preprocessWriter, + ref debugger, + ref detailedSummary, + recursing: false + )) + { + // Unfortunately /m isn't the default, and we are not yet brave enough to make it the default. + // However we want to give a hint to anyone who is building single proc without realizing it that there + // is a better way. + if (cpuCount == 1 && FileUtilities.IsSolutionFilename(projectFile) && verbosity > LoggerVerbosity.Minimal) + { + Console.WriteLine(ResourceUtilities.FormatResourceString("PossiblyOmittedMaxCPUSwitch")); + } + + if (preprocessWriter != null || debugger) + { + // Indicate to the engine that it can NOT toss extraneous file content: we want to + // see that in preprocessing/debugging + Environment.SetEnvironmentVariable("MSBUILDLOADALLFILESASWRITEABLE", "1"); + } + + DateTime t1 = DateTime.Now; + +#if !STANDALONEBUILD + if (Environment.GetEnvironmentVariable("MSBUILDOLDOM") != "1") +#endif + { + // if everything checks out, and sufficient information is available to start building + if (!BuildProject(projectFile, targets, toolsVersion, globalProperties, loggers, verbosity, distributedLoggerRecords.ToArray(), needToValidateProject, schemaFile, cpuCount, enableNodeReuse, preprocessWriter, debugger, detailedSummary)) + { + exitType = ExitType.BuildError; + } + } +#if !STANDALONEBUILD + else + { + exitType = OldOMBuildProject(exitType, projectFile, targets, toolsVersion, globalProperties, loggers, verbosity, needToValidateProject, schemaFile, cpuCount); + } +#endif + DateTime t2 = DateTime.Now; + + TimeSpan elapsedTime = t2.Subtract(t1); + + string timerOutputFilename = Environment.GetEnvironmentVariable("MSBUILDTIMEROUTPUTS"); + + if (!String.IsNullOrEmpty(timerOutputFilename)) + { + AppendOutputFile(timerOutputFilename, elapsedTime.Milliseconds); + } + } + else + { + // if there was no need to start the build e.g. because /help was triggered + // do nothing + } + } + /********************************************************************************************************************** + * WARNING: Do NOT add any more catch blocks below! Exceptions should be caught as close to their point of origin as + * possible, and converted into one of the known exceptions. The code that causes an exception best understands the + * reason for the exception, and only that code can provide the proper error message. We do NOT want to display + * messages from unknown exceptions, because those messages are most likely neither localized, nor composed in the + * canonical form with the correct prefix. + *********************************************************************************************************************/ + // handle switch errors + catch (CommandLineSwitchException e) + { + Console.WriteLine(e.Message); + Console.WriteLine(); + // prompt user to display help for proper switch usage + ShowHelpPrompt(); + + exitType = ExitType.SwitchError; + } + // handle configuration exceptions: problems reading toolset information from msbuild.exe.config or the registry + catch (InvalidToolsetDefinitionException e) + { + // Brief prefix to indicate that it's a configuration failure, and provide the "error" indication + Console.WriteLine(ResourceUtilities.FormatResourceString("ConfigurationFailurePrefixNoErrorCode", e.ErrorCode, e.Message)); + + exitType = ExitType.InitializationError; + } + // handle initialization failures + catch (InitializationException e) + { + Console.WriteLine(e.Message); + + exitType = ExitType.InitializationError; + } + // handle polite logger failures: don't dump the stack or trigger watson for these + catch (LoggerException e) + { + // display the localized message from the outer exception in canonical format + if (null != e.ErrorCode) + { + // Brief prefix to indicate that it's a logger failure, and provide the "error" indication + Console.WriteLine(ResourceUtilities.FormatResourceString("LoggerFailurePrefixNoErrorCode", e.ErrorCode, e.Message)); + } + else + { + // Brief prefix to indicate that it's a logger failure, adding a generic error code to make sure + // there's something for the user to look up in the documentation + Console.WriteLine(ResourceUtilities.FormatResourceString("LoggerFailurePrefixWithErrorCode", e.Message)); + } + + if (null != e.InnerException) + { + // write out exception details -- don't bother triggering Watson, because most of these exceptions will be coming + // from buggy loggers written by users + Console.WriteLine(e.InnerException.ToString()); + } + + exitType = ExitType.LoggerAbort; + } + // handle logger failures (logger bugs) + catch (InternalLoggerException e) + { + if (!e.InitializationException) + { + // display the localized message from the outer exception in canonical format + Console.WriteLine("MSBUILD : error " + e.ErrorCode + ": " + e.Message); +#if DEBUG + Console.WriteLine("This is an unhandled exception from a logger -- PLEASE OPEN A BUG AGAINST THE LOGGER OWNER."); +#endif + // write out exception details -- don't bother triggering Watson, because most of these exceptions will be coming + // from buggy loggers written by users + Console.WriteLine(e.InnerException.ToString()); + + exitType = ExitType.LoggerFailure; + } + else + { + Console.WriteLine("MSBUILD : error " + e.ErrorCode + ": " + e.Message + + (e.InnerException != null ? " " + e.InnerException.Message : "")); + exitType = ExitType.InitializationError; + } + } + catch (BuildAbortedException e) + { + Console.WriteLine("MSBUILD : error " + e.ErrorCode + ": " + e.Message + + (e.InnerException != null ? " " + e.InnerException.Message : String.Empty)); + + exitType = ExitType.Unexpected; + } + // handle fatal errors + catch (Exception e) + { + // display a generic localized message for the user + Console.WriteLine("{0}\r\n{1}", AssemblyResources.GetString("FatalError"), e.ToString()); +#if DEBUG + Console.WriteLine("This is an unhandled exception in MSBuild Engine -- PLEASE OPEN A BUG AGAINST THE MSBUILD TEAM.\r\n{0}", e.ToString()); +#endif + // rethrow, in case Watson is enabled on the machine -- if not, the CLR will write out exception details + // allow the build lab to set an env var to avoid jamming the build + if (Environment.GetEnvironmentVariable("MSBUILDDONOTLAUNCHDEBUGGER") != "1") + { + throw; + } + } + finally + { + s_buildComplete.Set(); + Console.CancelKeyPress -= cancelHandler; + + // Wait for any pending cancel, so that we get any remaining messages + s_cancelComplete.WaitOne(); +#if (!STANDALONEBUILD) + // Turn off codemarkers + CodeMarkers.Instance.UninitializePerformanceDLL(CodeMarkerApp.MSBUILDPERF); +#endif + } + /********************************************************************************************************************** + * WARNING: Do NOT add any more catch blocks above! + *********************************************************************************************************************/ + + return exitType; + } +#if (!STANDALONEBUILD) + /// + /// Use the Orcas Engine to build the project + /// ############################################################################################# + /// #### Segregated into another method to avoid loading the old Engine in the regular case. #### + /// #### Do not move back in to the main code path! ############################################# + /// ############################################################################################# + /// We have marked this method as NoInlining because we do not want Microsoft.Build.Engine.dll to be loaded unless we really execute this code path + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static ExitType OldOMBuildProject(ExitType exitType, string projectFile, string[] targets, string toolsVersion, Dictionary globalProperties, ILogger[] loggers, LoggerVerbosity verbosity, bool needToValidateProject, string schemaFile, int cpuCount) + { + // Log something to avoid confusion caused by errant environment variable sending us down here + Console.WriteLine(AssemblyResources.GetString("Using35Engine")); + + Microsoft.Build.BuildEngine.BuildPropertyGroup oldGlobalProps = new Microsoft.Build.BuildEngine.BuildPropertyGroup(); + // Copy over the global properties to the old OM + foreach (KeyValuePair globalProp in globalProperties) + { + oldGlobalProps.SetProperty(globalProp.Key, globalProp.Value); + } + + if (!BuildProjectWithOldOM(projectFile, targets, toolsVersion, oldGlobalProps, loggers, verbosity, null, needToValidateProject, schemaFile, cpuCount)) + { + exitType = ExitType.BuildError; + } + return exitType; + } +#endif + /// + /// Handler for when CTRL-C or CTRL-BREAK is called. + /// CTRL-BREAK means "die immediately" + /// CTRL-C means "try to stop work and exit cleanly" + /// + private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + if (e.SpecialKey == ConsoleSpecialKey.ControlBreak) + { + e.Cancel = false; // required; the process will now be terminated rudely + return; + } + + e.Cancel = true; // do not terminate rudely + + bool alreadyCalled = (Interlocked.CompareExchange(ref s_receivedCancel, 1, 0) == 1); + + if (alreadyCalled) + { + return; + } + + Console.WriteLine(ResourceUtilities.FormatResourceString("AbortingBuild")); + + // The OS takes a lock in + // kernel32.dll!_SetConsoleCtrlHandler, so if a task + // waits for that lock somehow before quitting, it would hang + // because we're in it here. One way a task can end up here is + // by calling Microsoft.Win32.SystemEvents.Initialize. + // So do our work asynchronously so we can return immediately. + // We're already on a threadpool thread anyway. + WaitCallback callback = new WaitCallback( + delegate (Object state) + { + s_cancelComplete.Reset(); + + // If the build is already complete, just exit. + // Cannot use WaitOne(int) overload because it does not exist in BCL 3.5; this is equivalent + if (s_buildComplete.WaitOne(0, false /* do not exit context */)) + { + s_cancelComplete.Set(); + return; + } + + // If the build has already started (or already finished), we will cancel it + // If the build has not yet started, it will cancel itself, because + // we set alreadyCalled=1 + BuildSubmission result = null; + lock (s_buildLock) + { + result = s_activeBuild; + } + + if (result != null) + { + BuildManager.DefaultBuildManager.CancelAllSubmissions(); + s_buildComplete.WaitOne(); + } + + s_cancelComplete.Set(); // This will release our main Execute method so we can finally exit. + }); + + ThreadPoolExtensions.QueueThreadPoolWorkItemWithCulture(callback, Thread.CurrentThread.CurrentCulture, Thread.CurrentThread.CurrentUICulture); + } + + /// + /// Clears out any state accumulated from previous builds, and resets + /// member data in preparation for a new build. + /// + private static void ResetBuildState() + { + s_includedResponseFiles = new ArrayList(); + usingSwitchesFromAutoResponseFile = false; + } + + /// + /// The location of the application executable. + /// + /// + /// Initialized in the static constructor. See comment there. + /// + private static readonly string s_exePath; // Do not initialize + + /// + /// Name of the exe (presumably msbuild.exe) + /// + private static string s_exeName; + + /// + /// Default name for the msbuild log file + /// + private const string msbuildLogFileName = "msbuild.log"; + + /// + /// Initializes the build engine, and starts the project building. + /// + /// true, if build succeeds + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Not going to refactor it right now")] + internal static bool BuildProject + ( + string projectFile, + string[] targets, + string toolsVersion, + Dictionary globalProperties, + ILogger[] loggers, + LoggerVerbosity verbosity, + DistributedLoggerRecord[] distributedLoggerRecords, + bool needToValidateProject, + string schemaFile, + int cpuCount, + bool enableNodeReuse, + TextWriter preprocessWriter, + bool debugger, + bool detailedSummary + ) + { + if (String.Equals(Path.GetExtension(projectFile), ".vcproj", StringComparison.OrdinalIgnoreCase) || + String.Equals(Path.GetExtension(projectFile), ".dsp", StringComparison.OrdinalIgnoreCase)) + { + InitializationException.Throw(ResourceUtilities.FormatResourceString("ProjectUpgradeNeededToVcxProj", projectFile), null); + } + + bool success = false; + + ProjectCollection projectCollection = null; + bool onlyLogCriticalEvents = false; + + try + { + List remoteLoggerRecords = new List(); + foreach (DistributedLoggerRecord distRecord in distributedLoggerRecords) + { + remoteLoggerRecords.Add(new ForwardingLoggerRecord(distRecord.CentralLogger, distRecord.ForwardingLoggerDescription)); + } + + // Targeted perf optimization for the case where we only have our own parallel console logger, and verbosity is quiet. In such a case + // we know we won't emit any messages except for errors and warnings, so the engine should not bother even logging them. + // If we're using the original serial console logger we can't do this, as it shows project started/finished context + // around errors and warnings. + // Telling the engine to not bother logging non-critical messages means that typically it can avoid loading any resources in the successful + // build case. + if (loggers.Length == 1 && + remoteLoggerRecords.Count == 0 && + verbosity == LoggerVerbosity.Quiet && + loggers[0].Parameters != null && + loggers[0].Parameters.IndexOf("ENABLEMPLOGGING", StringComparison.OrdinalIgnoreCase) != -1 && + loggers[0].Parameters.IndexOf("DISABLEMPLOGGING", StringComparison.OrdinalIgnoreCase) == -1 && + loggers[0].Parameters.IndexOf("V=", StringComparison.OrdinalIgnoreCase) == -1 && // Console logger could have had a verbosity + loggers[0].Parameters.IndexOf("VERBOSITY=", StringComparison.OrdinalIgnoreCase) == -1) // override with the /clp switch + { + // Must be exactly the console logger, not a derived type like the file logger. + Type t1 = loggers[0].GetType(); + Type t2 = typeof(ConsoleLogger); + if (t1 == t2) + { + onlyLogCriticalEvents = true; + } + } + + // HACK HACK: this enables task parameter logging. + // This is a hack for now to make sure the perf hit only happens + // on diagnostic. This should be changed to pipe it through properly, + // perhaps as part of a fuller tracing feature. + bool aLoggerIsDiagnostic = false; + foreach (var logger in loggers) + { + if (logger.Parameters != null && + (logger.Parameters.IndexOf("V=DIAG", StringComparison.OrdinalIgnoreCase) != -1 || + logger.Parameters.IndexOf("VERBOSITY=DIAG", StringComparison.OrdinalIgnoreCase) != -1) + ) + { + aLoggerIsDiagnostic = true; + } + } + + if (verbosity == LoggerVerbosity.Diagnostic || aLoggerIsDiagnostic) + { + Environment.SetEnvironmentVariable("MSBUILDLOGTASKINPUTS", "1"); + } + + projectCollection = new ProjectCollection + ( + globalProperties, + loggers, + null, + Microsoft.Build.Evaluation.ToolsetDefinitionLocations.ConfigurationFile | Microsoft.Build.Evaluation.ToolsetDefinitionLocations.Registry, + cpuCount, + onlyLogCriticalEvents + ); + + if (debugger) + { + // Debugging is not currently fully supported so we don't want to open + // public API for it. Also, we want to have a way to make it work when running inside VS. + // So use an environment variable. The undocumented /debug switch is just an easy way to set it. + Environment.SetEnvironmentVariable("MSBUILDDEBUGGING", "1"); + } + + if (toolsVersion != null && !projectCollection.ContainsToolset(toolsVersion)) + { + ThrowInvalidToolsVersionInitializationException(projectCollection.Toolsets, toolsVersion); + } + + // If the user has requested that the schema be validated, do that here. + if (needToValidateProject && !FileUtilities.IsSolutionFilename(projectFile)) + { + Microsoft.Build.Evaluation.Project project = projectCollection.LoadProject(projectFile, globalProperties, toolsVersion); + Microsoft.Build.Evaluation.Toolset toolset = projectCollection.GetToolset((toolsVersion == null) ? project.ToolsVersion : toolsVersion); + + if (toolset == null) + { + ThrowInvalidToolsVersionInitializationException(projectCollection.Toolsets, project.ToolsVersion); + } + + ProjectSchemaValidationHandler.VerifyProjectSchema(projectFile, schemaFile, toolset.ToolsPath); + + // If there are schema validation errors, an InitializationException is thrown, so if we get here, + // we can safely assume that the project successfully validated. + projectCollection.UnloadProject(project); + } + + if (preprocessWriter != null && !FileUtilities.IsSolutionFilename(projectFile)) + { + Project project = projectCollection.LoadProject(projectFile, globalProperties, toolsVersion); + + project.SaveLogicalProject(preprocessWriter); + + projectCollection.UnloadProject(project); + success = true; + } + else + { + BuildRequestData request = new BuildRequestData(projectFile, globalProperties, toolsVersion, targets, null); + + BuildParameters parameters = new BuildParameters(projectCollection); + + // By default we log synchronously to the console for compatibility with previous versions, + // but it is slightly slower + if (!String.Equals(Environment.GetEnvironmentVariable("MSBUILDLOGASYNC"), "1", StringComparison.Ordinal)) + { + parameters.UseSynchronousLogging = true; + } + + parameters.EnableNodeReuse = enableNodeReuse; + parameters.NodeExeLocation = Assembly.GetExecutingAssembly().Location; + parameters.MaxNodeCount = cpuCount; + parameters.Loggers = projectCollection.Loggers; + parameters.ForwardingLoggers = remoteLoggerRecords; + parameters.ToolsetDefinitionLocations = Microsoft.Build.Evaluation.ToolsetDefinitionLocations.ConfigurationFile | Microsoft.Build.Evaluation.ToolsetDefinitionLocations.Registry; + parameters.DetailedSummary = detailedSummary; + if (!String.IsNullOrEmpty(toolsVersion)) + { + parameters.DefaultToolsVersion = toolsVersion; + } + + string memoryUseLimit = Environment.GetEnvironmentVariable("MSBUILDMEMORYUSELIMIT"); + if (!String.IsNullOrEmpty(memoryUseLimit)) + { + parameters.MemoryUseLimit = Convert.ToInt32(memoryUseLimit, CultureInfo.InvariantCulture); + + // The following ensures that when we divide the use by node count to get the per-limit amount, we always end up with a + // positive value - otherwise setting it too low will result in a zero, which will enable only the default cache behavior + // which is not what is intended by using this environment variable. + if (parameters.MemoryUseLimit < parameters.MaxNodeCount) + { + parameters.MemoryUseLimit = parameters.MaxNodeCount; + } + } + + BuildManager buildManager = BuildManager.DefaultBuildManager; + +#if MSBUILDENABLEVSPROFILING + DataCollection.CommentMarkProfile(8800, "Pending Build Request from MSBuild.exe"); +#endif + BuildResult results = null; + buildManager.BeginBuild(parameters); + Exception exception = null; + try + { + try + { + lock (s_buildLock) + { + s_activeBuild = buildManager.PendBuildRequest(request); + + // Even if Ctrl-C was already hit, we still pend the build request and then cancel. + // That's so the build does not appear to have completed successfully. + if (s_receivedCancel == 1) + { + buildManager.CancelAllSubmissions(); + } + } + + results = s_activeBuild.Execute(); + } + finally + { + buildManager.EndBuild(); + } + } + catch (Exception ex) + { + exception = ex; + success = false; + } + + if (results != null && exception == null) + { + success = results.OverallResult == BuildResultCode.Success; + exception = results.Exception; + } + + if (exception != null) + { + success = false; + + // InvalidProjectFileExceptions have already been logged. + if (exception.GetType() != typeof(InvalidProjectFileException)) + { + if + ( + exception.GetType() == typeof(LoggerException) || + exception.GetType() == typeof(InternalLoggerException) + ) + { + // We will rethrow this so the outer exception handler can catch it, but we don't + // want to log the outer exception stack here. + throw exception; + } + + if (exception.GetType() == typeof(BuildAbortedException)) + { + // this is not a bug and should not dump stack. It will already have been logged + // appropriately, there is no need to take any further action with it. + } + else + { + // After throwing again below the stack will be reset. Make certain we log everything we + // can now + Console.WriteLine(AssemblyResources.GetString("FatalError")); +#if DEBUG + Console.WriteLine("This is an unhandled exception in MSBuild -- PLEASE OPEN A BUG AGAINST THE MSBUILD TEAM."); +#endif + Console.WriteLine(exception.ToString()); + Console.WriteLine(); + + throw exception; + } + } + } + } + } + // handle project file errors + catch (InvalidProjectFileException ex) + { + // just eat the exception because it has already been logged + ErrorUtilities.VerifyThrow(ex.HasBeenLogged, "Should have been logged"); + success = false; + } + finally + { + FileUtilities.ClearCacheDirectory(); + if (projectCollection != null) + { + projectCollection.Dispose(); + } + + BuildManager.DefaultBuildManager.Dispose(); + } + + return success; + } +#if (!STANDALONEBUILD) + /// + /// Initializes the build engine, and starts the project build. + /// Uses the Whidbey/Orcas object model. + /// ############################################################################################# + /// #### Segregated into another method to avoid loading the old Engine in the regular case. #### + /// #### Do not move back in to the main code path! ############################################# + /// ############################################################################################# + /// We have marked this method as NoInlining because we do not want Microsoft.Build.Engine.dll to be loaded unless we really execute this code path + /// + /// true, if build succeeds + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool BuildProjectWithOldOM(string projectFile, string[] targets, string toolsVersion, Microsoft.Build.BuildEngine.BuildPropertyGroup propertyBag, ILogger[] loggers, LoggerVerbosity verbosity, DistributedLoggerRecord[] distributedLoggerRecords, bool needToValidateProject, string schemaFile, int cpuCount) + { + string msbuildLocation = Path.GetDirectoryName(Assembly.GetAssembly(typeof(MSBuildApp)).Location); + string localNodeProviderParameters = "msbuildlocation=" + msbuildLocation; /*This assembly is the exe*/ ; + + localNodeProviderParameters += ";nodereuse=false"; + + Microsoft.Build.BuildEngine.Engine engine = new Microsoft.Build.BuildEngine.Engine(propertyBag, Microsoft.Build.BuildEngine.ToolsetDefinitionLocations.ConfigurationFile | Microsoft.Build.BuildEngine.ToolsetDefinitionLocations.Registry, cpuCount, localNodeProviderParameters); + bool success = false; + + try + { + foreach (ILogger logger in loggers) + { + engine.RegisterLogger(logger); + } + + // Targeted perf optimization for the case where we only have our own parallel console logger, and verbosity is quiet. In such a case + // we know we won't emit any messages except for errors and warnings, so the engine should not bother even logging them. + // If we're using the original serial console logger we can't do this, as it shows project started/finished context + // around errors and warnings. + // Telling the engine to not bother logging non-critical messages means that typically it can avoid loading any resources in the successful + // build case. + if (loggers.Length == 1 && + verbosity == LoggerVerbosity.Quiet && + loggers[0].Parameters.IndexOf("ENABLEMPLOGGING", StringComparison.OrdinalIgnoreCase) != -1 && + loggers[0].Parameters.IndexOf("DISABLEMPLOGGING", StringComparison.OrdinalIgnoreCase) == -1 && + loggers[0].Parameters.IndexOf("V=", StringComparison.OrdinalIgnoreCase) == -1 && // Console logger could have had a verbosity + loggers[0].Parameters.IndexOf("VERBOSITY=", StringComparison.OrdinalIgnoreCase) == -1) // override with the /clp switch + { + // Must be exactly the console logger, not a derived type like the file logger. + Type t1 = loggers[0].GetType(); + Type t2 = typeof(ConsoleLogger); + if (t1 == t2) + { + engine.OnlyLogCriticalEvents = true; + } + } + + Microsoft.Build.BuildEngine.Project project = null; + + try + { + project = new Microsoft.Build.BuildEngine.Project(engine, toolsVersion); + } + catch (InvalidOperationException e) + { + InitializationException.Throw("InvalidToolsVersionError", toolsVersion, e, false /*no stack*/); + } + + project.IsValidated = needToValidateProject; + project.SchemaFile = schemaFile; + + project.Load(projectFile); + + success = engine.BuildProject(project, targets); + } + // handle project file errors + catch (InvalidProjectFileException) + { + // just eat the exception because it has already been logged + } + finally + { + // Unregister loggers and finish with engine + engine.Shutdown(); + } + return success; + } +#endif + /// + /// Verifies that the code is running on a supported operating system. + /// + private static void VerifyThrowSupportedOS() + { + if ((Environment.OSVersion.Platform == PlatformID.Win32S) || // Win32S + (Environment.OSVersion.Platform == PlatformID.Win32Windows) || // Windows 95, Windows 98, Windows ME + (Environment.OSVersion.Platform == PlatformID.WinCE) || // Windows CE + ((Environment.OSVersion.Platform == PlatformID.Win32NT) && // Windows NT 4.0 and earlier + (Environment.OSVersion.Version.Major <= 4))) + { + // If we're running on any of the unsupported OS's, fail immediately. This way, + // we don't run into some obscure error down the line, totally confusing the user. + InitializationException.VerifyThrow(false, "UnsupportedOS"); + } + } + + /// + /// MSBuild.exe need to fallback to English if it cannot print Japanese (or other language) characters + /// + internal static void SetConsoleUI() + { + Thread thisThread = Thread.CurrentThread; + + // Eliminate the complex script cultures from the language selection. + thisThread.CurrentUICulture = CultureInfo.CurrentUICulture.GetConsoleFallbackUICulture(); + + // Determine if the language can be displayed in the current console codepage, otherwise set to US English + int codepage; + + try + { + codepage = System.Console.OutputEncoding.CodePage; + } + catch (NotSupportedException) + { + // Failed to get code page: some customers have hit this and we don't know why + thisThread.CurrentUICulture = new CultureInfo("en-US"); + return; + } + + if ( + codepage != 65001 // 65001 is Unicode + && + codepage != thisThread.CurrentUICulture.TextInfo.OEMCodePage + && + codepage != thisThread.CurrentUICulture.TextInfo.ANSICodePage + ) + { + thisThread.CurrentUICulture = new CultureInfo("en-US"); + } + } + + /// + /// Gets all specified switches, from the command line, as well as all + /// response files, including the auto-response file. + /// + /// + /// Combined bag of switches. + private static void GatherAllSwitches(string commandLine, out CommandLineSwitches switchesFromAutoResponseFile, out CommandLineSwitches switchesNotFromAutoResponseFile) + { + // split the command line on (unquoted) whitespace + ArrayList commandLineArgs = QuotingUtilities.SplitUnquoted(commandLine); + // discard the first piece, because that's the path to the executable -- the rest are args + s_exeName = QuotingUtilities.Unquote((string)commandLineArgs[0]); + + if (!s_exeName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + s_exeName += ".exe"; + } + + commandLineArgs.RemoveAt(0); + + // parse the command line, and flag syntax errors and obvious switch errors + switchesNotFromAutoResponseFile = new CommandLineSwitches(); + GatherCommandLineSwitches(commandLineArgs, switchesNotFromAutoResponseFile); + + // parse the auto-response file (if "/noautoresponse" is not specified), and combine those switches with the + // switches on the command line + switchesFromAutoResponseFile = new CommandLineSwitches(); + if (!switchesNotFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + GatherAutoResponseFileSwitches(s_exePath, switchesFromAutoResponseFile); + } + } + + /// + /// Coordinates the parsing of the command line. It detects switches on the command line, gathers their parameters, and + /// flags syntax errors, and other obvious switch errors. + /// + /// + /// Internal for unit testing only. + /// + internal static void GatherCommandLineSwitches(ArrayList commandLineArgs, CommandLineSwitches commandLineSwitches) + { + foreach (string commandLineArg in commandLineArgs) + { + int doubleQuotesRemovedFromArg; + string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); + + if (unquotedCommandLineArg.Length > 0) + { + // response file switch starts with @ + if (unquotedCommandLineArg.StartsWith("@", StringComparison.Ordinal)) + { + GatherResponseFileSwitch(unquotedCommandLineArg, commandLineSwitches); + } + else + { + string switchName; + string switchParameters; + + // all switches should start with - or / unless a project is being specified + if (!unquotedCommandLineArg.StartsWith("-", StringComparison.Ordinal) && !unquotedCommandLineArg.StartsWith("/", StringComparison.Ordinal)) + { + switchName = null; + // add a (fake) parameter indicator for later parsing + switchParameters = ":" + commandLineArg; + } + else + { + // check if switch has parameters (look for the : parameter indicator) + int switchParameterIndicator = unquotedCommandLineArg.IndexOf(':'); + + // extract the switch name and parameters -- the name is sandwiched between the switch indicator (the + // leading - or /) and the parameter indicator (if the switch has parameters); the parameters (if any) + // follow the parameter indicator + if (switchParameterIndicator == -1) + { + switchName = unquotedCommandLineArg.Substring(1); + switchParameters = String.Empty; + } + else + { + switchName = unquotedCommandLineArg.Substring(1, switchParameterIndicator - 1); + switchParameters = ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, switchName, switchParameterIndicator); + } + } + + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch; + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch; + string duplicateSwitchErrorMessage; + bool multipleParametersAllowed; + string missingParametersErrorMessage; + bool unquoteParameters; + + // Special case: for the switch "/m" or "/maxCpuCount" we wish to pretend we saw "/m:" + // This allows a subsequent /m:n on the command line to override it. + // We could create a new kind of switch with optional parameters, but it's a great deal of churn for this single case. + // Note that if no "/m" or "/maxCpuCount" switch -- either with or without parameters -- is present, then we still default to 1 cpu + // for backwards compatibility. + if (String.IsNullOrEmpty(switchParameters)) + { + if (String.Equals(switchName, "m", StringComparison.OrdinalIgnoreCase) || + String.Equals(switchName, "maxcpucount", StringComparison.OrdinalIgnoreCase)) + { + int numberOfCpus = Environment.ProcessorCount; + switchParameters = ":" + numberOfCpus; + } + } + + if (CommandLineSwitches.IsParameterlessSwitch(switchName, out parameterlessSwitch, out duplicateSwitchErrorMessage)) + { + GatherParameterlessCommandLineSwitch(commandLineSwitches, parameterlessSwitch, switchParameters, duplicateSwitchErrorMessage, unquotedCommandLineArg); + } + else if (CommandLineSwitches.IsParameterizedSwitch(switchName, out parameterizedSwitch, out duplicateSwitchErrorMessage, out multipleParametersAllowed, out missingParametersErrorMessage, out unquoteParameters)) + { + GatherParameterizedCommandLineSwitch(commandLineSwitches, parameterizedSwitch, switchParameters, duplicateSwitchErrorMessage, multipleParametersAllowed, missingParametersErrorMessage, unquoteParameters, unquotedCommandLineArg); + } + else + { + commandLineSwitches.SetUnknownSwitchError(unquotedCommandLineArg); + } + } + } + } + } + + /// + /// Extracts a switch's parameters after processing all quoting around the switch. + /// + /// + /// This method is marked "internal" for unit-testing purposes only -- ideally it should be "private". + /// + /// + /// + /// + /// + /// + /// The given switch's parameters (with interesting quoting preserved). + internal static string ExtractSwitchParameters + ( + string commandLineArg, + string unquotedCommandLineArg, + int doubleQuotesRemovedFromArg, + string switchName, + int switchParameterIndicator + ) + { + string switchParameters = null; + + // find the parameter indicator again using the quoted arg + // NOTE: since the parameter indicator cannot be part of a switch name, quoting around it is not relevant, because a + // parameter indicator cannot be escaped or made into a literal + int quotedSwitchParameterIndicator = commandLineArg.IndexOf(':'); + + // check if there is any quoting in the name portion of the switch + int doubleQuotesRemovedFromSwitchIndicatorAndName; + string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out doubleQuotesRemovedFromSwitchIndicatorAndName); + + ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(1), + "The switch name extracted from either the partially or completely unquoted arg should be the same."); + + ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, + "The name portion of the switch cannot contain more quoting than the arg itself."); + + // if quoting in the name portion of the switch was terminated + if ((doubleQuotesRemovedFromSwitchIndicatorAndName % 2) == 0) + { + // get the parameters exactly as specified on the command line i.e. including quoting + switchParameters = commandLineArg.Substring(quotedSwitchParameterIndicator); + } + else + { + // if quoting was not terminated in the name portion of the switch, and the terminal double-quote (if any) + // terminates the switch parameters + int terminalDoubleQuote = commandLineArg.IndexOf('"', quotedSwitchParameterIndicator + 1); + if (((doubleQuotesRemovedFromArg - doubleQuotesRemovedFromSwitchIndicatorAndName) <= 1) && + ((terminalDoubleQuote == -1) || (terminalDoubleQuote == (commandLineArg.Length - 1)))) + { + // then the parameters are not quoted in any interesting way, so use the unquoted parameters + switchParameters = unquotedCommandLineArg.Substring(switchParameterIndicator); + } + else + { + // otherwise, use the quoted parameters, after compensating for the quoting that was started in the name + // portion of the switch + switchParameters = ":\"" + commandLineArg.Substring(quotedSwitchParameterIndicator + 1); + } + } + + ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); + + return switchParameters; + } + + /// + /// Used to keep track of response files to prevent them from + /// being included multiple times (or even recursively). + /// + private static ArrayList s_includedResponseFiles; + + /// + /// Called when a response file switch is detected on the command line. It loads the specified response file, and parses + /// each line in it like a command line. It also prevents multiple (or recursive) inclusions of the same response file. + /// + /// + /// + private static void GatherResponseFileSwitch(string unquotedCommandLineArg, CommandLineSwitches commandLineSwitches) + { + try + { + string responseFile = unquotedCommandLineArg.Substring(1); + + if (responseFile.Length == 0) + { + commandLineSwitches.SetSwitchError("MissingResponseFileError", unquotedCommandLineArg); + } + else if (!File.Exists(responseFile)) + { + commandLineSwitches.SetParameterError("ResponseFileNotFoundError", unquotedCommandLineArg); + } + else + { + // normalize the response file path to help catch multiple (or recursive) inclusions + responseFile = Path.GetFullPath(responseFile); + // NOTE: for network paths or mapped paths, normalization is not guaranteed to work + + bool isRepeatedResponseFile = false; + + foreach (string includedResponseFile in s_includedResponseFiles) + { + if (String.Compare(responseFile, includedResponseFile, StringComparison.OrdinalIgnoreCase) == 0) + { + commandLineSwitches.SetParameterError("RepeatedResponseFileError", unquotedCommandLineArg); + isRepeatedResponseFile = true; + break; + } + } + + if (!isRepeatedResponseFile) + { + s_includedResponseFiles.Add(responseFile); + + ArrayList argsFromResponseFile; + + using (StreamReader responseFileContents = new StreamReader(responseFile, Encoding.Default)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. + { + argsFromResponseFile = new ArrayList(); + + while (responseFileContents.Peek() != -1) + { + // ignore leading whitespace on each line + string responseFileLine = responseFileContents.ReadLine().TrimStart(); + + // skip comment lines beginning with # + if (!responseFileLine.StartsWith("#", StringComparison.Ordinal)) + { + // treat each line of the response file like a command line i.e. args separated by whitespace + argsFromResponseFile.AddRange(QuotingUtilities.SplitUnquoted(Environment.ExpandEnvironmentVariables(responseFileLine))); + } + } + } + + GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches); + } + } + } + catch (NotSupportedException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e); + } + catch (SecurityException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e); + } + catch (UnauthorizedAccessException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e); + } + catch (IOException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e); + } + } + + /// + /// Called when a switch that doesn't take parameters is detected on the command line. + /// + /// + /// + /// + /// + /// + private static void GatherParameterlessCommandLineSwitch + ( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + string unquotedCommandLineArg + ) + { + // switch should not have any parameters + if (switchParameters.Length == 0) + { + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterlessSwitchSet(parameterlessSwitch) || + (duplicateSwitchErrorMessage == null)) + { + commandLineSwitches.SetParameterlessSwitch(parameterlessSwitch, unquotedCommandLineArg); + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg); + } + } + else + { + commandLineSwitches.SetUnexpectedParametersError(unquotedCommandLineArg); + } + } + + /// + /// Called when a switch that takes parameters is detected on the command line. This method flags errors and stores the + /// switch parameters. + /// + /// + /// + /// + /// + /// + /// + /// + /// + private static void GatherParameterizedCommandLineSwitch + ( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + bool multipleParametersAllowed, + string missingParametersErrorMessage, + bool unquoteParameters, + string unquotedCommandLineArg + ) + { + if (// switch must have parameters + (switchParameters.Length > 1) || + // unless the parameters are optional + (missingParametersErrorMessage == null)) + { + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterizedSwitchSet(parameterizedSwitch) || + (duplicateSwitchErrorMessage == null)) + { + // skip the parameter indicator (if any) + if (switchParameters.Length > 0) + { + switchParameters = switchParameters.Substring(1); + } + + // save the parameters after unquoting and splitting them if necessary + if (!commandLineSwitches.SetParameterizedSwitch(parameterizedSwitch, unquotedCommandLineArg, switchParameters, multipleParametersAllowed, unquoteParameters)) + { + // if parsing revealed there were no real parameters, flag an error, unless the parameters are optional + if (missingParametersErrorMessage != null) + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg); + } + } + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg); + } + } + else + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg); + } + } + + /// + /// The name of the auto-response file. + /// + private const string autoResponseFileName = "MSBuild.rsp"; + + /// + /// Whether switches from the auto-response file are being used. + /// + internal static bool usingSwitchesFromAutoResponseFile = false; + + /// + /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the + /// switches from the auto-response file with the switches passed in. + /// Returns true if the response file was found. + /// + private static bool GatherAutoResponseFileSwitches(string path, CommandLineSwitches switchesFromAutoResponseFile) + { + string autoResponseFile = Path.Combine(path, autoResponseFileName); + bool found = false; + + // if the auto-response file does not exist, only use the switches on the command line + if (File.Exists(autoResponseFile)) + { + found = true; + GatherResponseFileSwitch("@" + autoResponseFile, switchesFromAutoResponseFile); + + // if the "/noautoresponse" switch was set in the auto-response file, flag an error + if (switchesFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + switchesFromAutoResponseFile.SetSwitchError("CannotAutoDisableAutoResponseFile", + switchesFromAutoResponseFile.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse)); + } + + if (switchesFromAutoResponseFile.HaveAnySwitchesBeenSet()) + { + // we picked up some switches from the auto-response file + usingSwitchesFromAutoResponseFile = true; + } + } + + return found; + } + + /// + /// Coordinates the processing of all detected switches. It gathers information necessary to invoke the build engine, and + /// performs deeper error checking on the switches and their parameters. + /// + /// true, if build can be invoked + private static bool ProcessCommandLineSwitches + ( + CommandLineSwitches switchesFromAutoResponseFile, + CommandLineSwitches switchesNotFromAutoResponseFile, + ref string projectFile, + ref string[] targets, + ref string toolsVersion, + ref Dictionary globalProperties, + ref ILogger[] loggers, + ref LoggerVerbosity verbosity, + ref List distributedLoggerRecords, + ref bool needToValidateProject, + ref string schemaFile, + ref int cpuCount, + ref bool enableNodeReuse, + ref TextWriter preprocessWriter, + ref bool debugger, + ref bool detailedSummary, + bool recursing + ) + { + bool invokeBuild = false; + + // combine the auto-response file switches with the command line switches in a left-to-right manner, where the + // auto-response file switches are on the left (default options), and the command line switches are on the + // right (overriding options) so that we consume switches in the following sequence of increasing priority: + // (1) switches from the msbuild.rsp file/s, including recursively included response files + // (2) switches from the command line, including recursively included response file switches inserted at the point they are declared with their "@" symbol + CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + commandLineSwitches.Append(switchesFromAutoResponseFile); // lowest precedence + commandLineSwitches.Append(switchesNotFromAutoResponseFile); + + // show copyright message if nologo switch is not set + // NOTE: we heed the nologo switch even if there are switch errors + if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoLogo] && !commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Preprocess)) + { + DisplayCopyrightMessage(); + } + + // if help switch is set (regardless of switch errors), show the help message and ignore the other switches + if (commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.Help]) + { + ShowHelpMessage(); + } + else if (commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.NodeMode)) + { + StartLocalNode(commandLineSwitches); + } + else + { + // if help switch is not set, and errors were found, abort (don't process the remaining switches) + commandLineSwitches.ThrowErrors(); + + // if version switch is set, just show the version and quit (ignore the other switches) + if (commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.Version]) + { + ShowVersion(); + } + else + { + // figure out what project we are building + projectFile = ProcessProjectSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions], Directory.GetFiles); + + if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + // gather any switches from an msbuild.rsp that is next to the project or solution file itself + string projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); + + bool found = false; + + // Don't look for more response files if it's only in the same place we already looked (next to the exe) + if (!String.Equals(projectDirectory, s_exePath, StringComparison.OrdinalIgnoreCase)) + { + // this combines any found, with higher precedence, with the switches from the original auto response file switches + found = GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile); + } + + if (found) + { + // we presumably read in more switches, so start our switch processing all over again, + // so that we consume switches in the following sequence of increasing priority: + // (1) switches from the msbuild.rsp next to msbuild.exe, including recursively included response files + // (2) switches from this msbuild.rsp next to the project or solution <<--------- these we have just now merged with (1) + // (3) switches from the command line, including recursively included response file switches inserted at the point they are declared with their "@" symbol + return ProcessCommandLineSwitches( + switchesFromAutoResponseFile, + switchesNotFromAutoResponseFile, + ref projectFile, + ref targets, + ref toolsVersion, + ref globalProperties, + ref loggers, + ref verbosity, + ref distributedLoggerRecords, + ref needToValidateProject, + ref schemaFile, + ref cpuCount, + ref enableNodeReuse, + ref preprocessWriter, + ref debugger, + ref detailedSummary, + recursing: true + ); + } + } + + // figure out which targets we are building + targets = ProcessTargetSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Target]); + + // figure out which ToolsVersion has been set on the command line + toolsVersion = ProcessToolsVersionSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ToolsVersion]); + + // figure out which properties have been set on the command line + globalProperties = ProcessPropertySwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Property]); + + // figure out if there was a max cpu count provided + cpuCount = ProcessMaxCPUCountSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]); + + // figure out if we shold reuse nodes + enableNodeReuse = ProcessNodeReuseSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.NodeReuse]); + + // determine what if any writer to preprocess to + preprocessWriter = null; + if (commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Preprocess)) + { + preprocessWriter = ProcessPreprocessSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Preprocess]); + } + + debugger = commandLineSwitches.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.Debugger); + detailedSummary = commandLineSwitches.IsParameterlessSwitchSet(CommandLineSwitches.ParameterlessSwitch.DetailedSummary); + + // figure out which loggers are going to listen to build events + string[][] groupedFileLoggerParameters = commandLineSwitches.GetFileLoggerParameters(); + + loggers = ProcessLoggingSwitches( + commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Logger], + commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.DistributedLogger], + commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Verbosity], + commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoConsoleLogger], + commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.DistributedFileLogger], + commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.FileLoggerParameters], // used by DistributedFileLogger + commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ConsoleLoggerParameters], + groupedFileLoggerParameters, + out distributedLoggerRecords, + out verbosity, + ref detailedSummary, + cpuCount + ); + + // If we picked up switches from the autoreponse file, let the user know. This could be a useful + // hint to a user that does not know that we are picking up the file automatically. + // Since this is going to happen often in normal use, only log it in high verbosity mode. + // Also, only log it to the console; logging to loggers would involve increasing the public API of + // the Engine, and we don't want to do that. + if (usingSwitchesFromAutoResponseFile && LoggerVerbosity.Diagnostic == verbosity) + { + Console.WriteLine(ResourceUtilities.FormatResourceString("PickedUpSwitchesFromAutoResponse", autoResponseFileName)); + } + + if (verbosity == LoggerVerbosity.Diagnostic) + { + string equivalentCommandLine = commandLineSwitches.GetEquivalentCommandLineExceptProjectFile(); + Console.WriteLine(Path.Combine(s_exePath, s_exeName) + " " + equivalentCommandLine + " " + projectFile); + } + + // figure out if the project needs to be validated against a schema + needToValidateProject = commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.Validate); + schemaFile = ProcessValidateSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Validate]); + invokeBuild = true; + } + } + + ErrorUtilities.VerifyThrow(!invokeBuild || !String.IsNullOrEmpty(projectFile), "We should have a project file if we're going to build."); + + return invokeBuild; + } + + /// + /// Processes the node reuse switch, the user can set node reuse to true, false or not set the switch. If the switch is + /// not set the system will check to see if the process is being run as an administrator. This check in localnode provider + /// will determine the node reuse setting for that case. + /// + internal static bool ProcessNodeReuseSwitch(string[] parameters) + { + bool enableNodeReuse = true; + + if (Environment.GetEnvironmentVariable("MSBUILDDISABLENODEREUSE") == "1") // For example to disable node reuse in a gated checkin, without using the flag + { + enableNodeReuse = false; + } + + if (parameters.Length > 0) + { + try + { + // There does not seem to be a localizable function for this + enableNodeReuse = bool.Parse(parameters[parameters.Length - 1]); + } + catch (FormatException ex) + { + CommandLineSwitchException.Throw("InvalidNodeReuseValue", parameters[parameters.Length - 1], ex.Message); + } + catch (ArgumentNullException ex) + { + CommandLineSwitchException.Throw("InvalidNodeReuseValue", parameters[parameters.Length - 1], ex.Message); + } + } + + return enableNodeReuse; + } + + /// + /// Figure out what TextWriter we should preprocess the project file to. + /// If no parameter is provided to the switch, the default is to output to the console. + /// + internal static TextWriter ProcessPreprocessSwitch(string[] parameters) + { + TextWriter writer = Console.Out; + + if (parameters.Length > 0) + { + try + { + writer = new StreamWriter(parameters[parameters.Length - 1]); + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + CommandLineSwitchException.Throw("InvalidPreprocessPath", parameters[parameters.Length - 1], ex.Message); + } + } + + return writer; + } + + /// + /// Uses the input from thinNodeMode switch to start a local node server + /// + /// + private static void StartLocalNode(CommandLineSwitches commandLineSwitches) + { + string[] input = commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.NodeMode]; + int nodeNumber = 0; + + if (input.Length > 0) + { + try + { + nodeNumber = int.Parse(input[0], CultureInfo.InvariantCulture); + } + catch (FormatException ex) + { + CommandLineSwitchException.Throw("InvalidNodeNumberValue", input[0], ex.Message); + } + catch (OverflowException ex) + { + CommandLineSwitchException.Throw("InvalidNodeNumberValue", input[0], ex.Message); + } + + CommandLineSwitchException.VerifyThrow(nodeNumber >= 0, "InvalidNodeNumberValueIsNegative", input[0]); + } + +#if !STANDALONEBUILD + if (!commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.OldOM]) +#endif + { + bool restart = true; + while (restart) + { + Exception nodeException = null; + NodeEngineShutdownReason shutdownReason = NodeEngineShutdownReason.Error; + // normal OOP node case + if (nodeNumber == 1) + { + OutOfProcNode node = new OutOfProcNode(); + shutdownReason = node.Run(out nodeException); + FileUtilities.ClearCacheDirectory(); + } + else if (nodeNumber == 2) + { + OutOfProcTaskHostNode node = new OutOfProcTaskHostNode(); + shutdownReason = node.Run(out nodeException); + } + else + { + CommandLineSwitchException.Throw("InvalidNodeNumberValue", input[0]); + } + + + if (shutdownReason == NodeEngineShutdownReason.Error) + { + Debug.WriteLine("An error has happened, throwing an exception"); + throw nodeException; + } + + if (shutdownReason != NodeEngineShutdownReason.BuildCompleteReuse) + { + restart = false; + } + } + } +#if !STANDALONEBUILD + else + { + StartLocalNodeOldOM(nodeNumber); + } +#endif + } + +#if !STANDALONEBUILD + /// + /// Start an old-OM local node + /// + /// + /// ############################################################################################# + /// #### Segregated into another method to avoid loading the old Engine in the regular case. #### + /// #### Do not move back in to the main code path! ############################################# + /// ############################################################################################# + /// We have marked this method as NoInlining because we do not want Microsoft.Build.Engine.dll to be loaded unless we really execute this code path + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void StartLocalNodeOldOM(int nodeNumber) + { + Microsoft.Build.BuildEngine.LocalNode.StartLocalNodeServer(nodeNumber); + } +#endif + + /// + /// Process the /m: switch giving the CPU count + /// + /// + /// Internal for unit testing only + /// + internal static int ProcessMaxCPUCountSwitch(string[] parameters) + { + int cpuCount = 1; + + if (parameters.Length > 0) + { + try + { + cpuCount = int.Parse(parameters[parameters.Length - 1], CultureInfo.InvariantCulture); + } + catch (FormatException ex) + { + CommandLineSwitchException.Throw("InvalidMaxCPUCountValue", parameters[parameters.Length - 1], ex.Message); + } + catch (OverflowException ex) + { + CommandLineSwitchException.Throw("InvalidMaxCPUCountValue", parameters[parameters.Length - 1], ex.Message); + } + + CommandLineSwitchException.VerifyThrow(cpuCount > 0 && cpuCount <= 1024, "InvalidMaxCPUCountValueOutsideRange", parameters[parameters.Length - 1]); + } + + return cpuCount; + } + + /// + /// Figures out what project to build. + /// Throws if it cannot figure it out. + /// + /// + /// The project filename/path. + /// Internal for testing purposes + internal static string ProcessProjectSwitch + ( + string[] parameters, + string[] projectsExtensionsToIgnore, + DirectoryGetFiles getFiles + ) + { + ErrorUtilities.VerifyThrow(parameters.Length <= 1, "It should not be possible to specify more than 1 project at a time."); + string projectFile = null; + // We need to look in the current directory for a project file... + if (parameters.Length == 0) + { + // Get all files in the current directory that have a proj-like extension + string[] potentialProjectFiles = getFiles(".", "*.*proj"); + // Get all files in the current directory that have a sln extension + string[] potentialSolutionFiles = getFiles(".", "*.sln"); + + List extensionsToIgnore = new List(); + if (projectsExtensionsToIgnore != null) + { + extensionsToIgnore.AddRange(projectsExtensionsToIgnore); + } + + if (potentialProjectFiles != null) + { + foreach (string s in potentialProjectFiles) + { + if (s.EndsWith("~", true, CultureInfo.CurrentCulture)) + { + extensionsToIgnore.Add(Path.GetExtension(s)); + } + } + } + + + if (potentialSolutionFiles != null) + { + foreach (string s in potentialSolutionFiles) + { + if (s.EndsWith("~", true, CultureInfo.CurrentCulture)) + { + extensionsToIgnore.Add(Path.GetExtension(s)); + } + } + } + + Dictionary extensionsToIgnoreDictionary = ValidateExtensions(extensionsToIgnore.ToArray()); + + // Remove projects that are in the projectExtensionsToIgnore List + // If we have no extensions to ignore we can skip removing any extensions + if (extensionsToIgnoreDictionary.Count > 0) + { + // No point removing extensions if we have no project files + if (potentialProjectFiles != null && potentialProjectFiles.Length > 0) + { + potentialProjectFiles = RemoveFilesWithExtensionsToIgnore(potentialProjectFiles, extensionsToIgnoreDictionary); + } + + // No point removing extensions if we have no solutions + if (potentialSolutionFiles != null && potentialSolutionFiles.Length > 0) + { + potentialSolutionFiles = RemoveFilesWithExtensionsToIgnore(potentialSolutionFiles, extensionsToIgnoreDictionary); + } + } + // If there is exactly 1 project file and exactly 1 solution file + if ((potentialProjectFiles.Length == 1) && (potentialSolutionFiles.Length == 1)) + { + // Grab the name of both project and solution without extensions + string solutionName = Path.GetFileNameWithoutExtension(potentialSolutionFiles[0]); + string projectName = Path.GetFileNameWithoutExtension(potentialProjectFiles[0]); + // Compare the names and error if they are not identical + InitializationException.VerifyThrow(String.Compare(solutionName, projectName, StringComparison.OrdinalIgnoreCase) == 0, "AmbiguousProjectError"); + } + // If there is more than one solution file in the current directory we have no idea which one to use + else if (potentialSolutionFiles.Length > 1) + { + InitializationException.VerifyThrow(false, "AmbiguousProjectError"); + } + // If there is more than one project file in the current directory we may be able to figure it out + else if (potentialProjectFiles.Length > 1) + { + // We have more than one project, it is ambiguous at the moment + bool isAmbiguousProject = true; + + // If there are exactly two projects and one of them is a .proj use that one and ignore the other + if (potentialProjectFiles.Length == 2) + { + string firstPotentialProjectExtension = Path.GetExtension(potentialProjectFiles[0]); + string secondPotentialProjectExtension = Path.GetExtension(potentialProjectFiles[1]); + + // If the two projects have the same extension we can't decide which one to pick + if (String.Compare(firstPotentialProjectExtension, secondPotentialProjectExtension, StringComparison.OrdinalIgnoreCase) != 0) + { + // Check to see if the first project is the proj, if it is use it + if (String.Compare(firstPotentialProjectExtension, ".proj", StringComparison.OrdinalIgnoreCase) == 0) + { + potentialProjectFiles = new string[] { potentialProjectFiles[0] }; + // We have made a decision + isAmbiguousProject = false; + } + // If the first project is not the proj check to see if the second one is the proj, if so use it + else if (String.Compare(secondPotentialProjectExtension, ".proj", StringComparison.OrdinalIgnoreCase) == 0) + { + potentialProjectFiles = new string[] { potentialProjectFiles[1] }; + // We have made a decision + isAmbiguousProject = false; + } + } + } + InitializationException.VerifyThrow(!isAmbiguousProject, "AmbiguousProjectError"); + } + // if there are no project or solution files in the directory, we can't build + else if ((potentialProjectFiles.Length == 0) && + (potentialSolutionFiles.Length == 0)) + { + InitializationException.VerifyThrow(false, "MissingProjectError"); + } + + // We are down to only one project or solution. + // If only 1 solution build the solution. If only 1 project build the project + // If 1 solution and 1 project and they are of the same name build the solution + projectFile = (potentialSolutionFiles.Length == 1) ? potentialSolutionFiles[0] : potentialProjectFiles[0]; + } + else + { + InitializationException.VerifyThrow(File.Exists(parameters[0]), "ProjectNotFoundError", parameters[0]); + projectFile = parameters[0]; + } + + return projectFile; + } + + /// + /// This method takes in a list of file name extensions to ignore. It will then validate the extensions + /// to make sure they start with a period, have atleast one character after the period and do not contain + /// any invalid path chars or wild cards + /// + /// + /// + private static Dictionary ValidateExtensions(string[] projectsExtensionsToIgnore) + { + // Dictionary to contain the list of files which match the extensions to ignore. We are using a dictionary for fast lookups of extensions to ignore + Dictionary extensionsToIgnoreDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Go through each of the extensions to ignore and add them as a key in the dictionary + if (projectsExtensionsToIgnore != null && projectsExtensionsToIgnore.Length > 0) + { + string extension = null; + + foreach (string extensionToIgnore in projectsExtensionsToIgnore) + { + // Use the path get extension method to figure out if there is an extension in the passed + // in extension to ignore. Will return null or empty if there is no extension in the string + try + { + // Use GetExtension to parse the extension from the extensionToIgnore + extension = Path.GetExtension(extensionToIgnore); + } + catch (ArgumentException) + { + // There has been an invalid char in the extensionToIgnore + InitializationException.Throw("InvalidExtensionToIgnore", extensionToIgnore, null, false); + } + + // if null or empty is returned that means that there was not extension able to be parsed from the string + InitializationException.VerifyThrow(!string.IsNullOrEmpty(extension), "InvalidExtensionToIgnore", extensionToIgnore); + + // There has to be more than a . passed in as the extension + InitializationException.VerifyThrow(extension.Length >= 2, "InvalidExtensionToIgnore", extensionToIgnore); + + // The parsed extension does not match the passed in extension, this means that there were + // some other chars before the last extension + if (string.Compare(extension, extensionToIgnore, StringComparison.OrdinalIgnoreCase) != 0) + { + InitializationException.Throw("InvalidExtensionToIgnore", extensionToIgnore, null, false); + } + + // Make sure that no wild cards are in the string because for now we dont allow wild card extensions + if (extensionToIgnore.IndexOfAny(s_wildcards) > -1) + { + InitializationException.Throw("InvalidExtensionToIgnore", extensionToIgnore, null, false); + }; + if (!extensionsToIgnoreDictionary.ContainsKey(extensionToIgnore)) + { + extensionsToIgnoreDictionary.Add(extensionToIgnore, null); + } + } + } + return extensionsToIgnoreDictionary; + } + + /// + /// Removes filenames from the given list whose extensions are on the list of extensions to ignore + /// + /// A list of project or solution file names + /// A list of extensions to ignore + /// Array of project or solution files names which do not have an extension to be ignored + private static string[] RemoveFilesWithExtensionsToIgnore + ( + string[] potentialProjectOrSolutionFiles, + Dictionary extensionsToIgnoreDictionary + ) + { + // If we got to this method we should have to possible projects or solutions and some extensions to ignore + ErrorUtilities.VerifyThrow(((potentialProjectOrSolutionFiles != null) && (potentialProjectOrSolutionFiles.Length > 0)), "There should be some potential project or solution files"); + ErrorUtilities.VerifyThrow(((extensionsToIgnoreDictionary != null) && (extensionsToIgnoreDictionary.Count > 0)), "There should be some extensions to Ignore"); + + List filesToKeep = new List(); + foreach (string projectOrSolutionFile in potentialProjectOrSolutionFiles) + { + string extension = Path.GetExtension(projectOrSolutionFile); + // Check to see if the file extension of the project is in our ignore list. If not keep the file + if (!extensionsToIgnoreDictionary.ContainsKey(extension)) + { + filesToKeep.Add(projectOrSolutionFile); + } + } + return filesToKeep.ToArray(); + } + + /// + /// Figures out which targets are to be built. + /// + /// + /// List of target names. + private static string[] ProcessTargetSwitch(string[] parameters) + { + return parameters; + } + + /// + /// The = sign is used to pair properties with their values on the command line. + /// + private static readonly char[] s_propertyValueSeparator = { '=' }; + + /// + /// This is a set of wildcard chars which can cause a file extension to be invalid + /// + private static readonly char[] s_wildcards = { '*', '?' }; + + /// + /// Determines which ToolsVersion was specified on the command line. If more than + /// one ToolsVersion was specified, we honor only the final ToolsVersion. + /// + /// + /// + private static string ProcessToolsVersionSwitch(string[] parameters) + { + if (parameters.Length > 0) + { + // We don't do any validation on the value of the ToolsVersion here, since we don't + // know what a valid value looks like. The engine will take care of this later. + return parameters[parameters.Length - 1]; + } + + return null; + } + + /// + /// Figures out which properties were set on the command line. + /// Internal for unit testing. + /// + /// + /// BuildProperty bag. + internal static Dictionary ProcessPropertySwitch(string[] parameters) + { + Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (string parameter in parameters) + { + // split each = string into 2 pieces, breaking on the first = that is found + string[] parameterSections = parameter.Split(s_propertyValueSeparator, 2); + + Debug.Assert((parameterSections.Length >= 1) && (parameterSections.Length <= 2), + "String.Split() will return at least one string, and no more than two."); + + // check that the property name is not blank, and the property has a value + CommandLineSwitchException.VerifyThrow((parameterSections[0].Length > 0) && (parameterSections.Length == 2), + "InvalidPropertyError", parameter); + + // Validation of whether the property has a reserved name will occur when + // we start to build: and it will be logged then, too. + globalProperties[parameterSections[0]] = parameterSections[1]; + } + + return globalProperties; + } + + /// + /// Instantiates the loggers that are going to listen to events from this build. + /// + /// List of loggers. + private static ILogger[] ProcessLoggingSwitches + ( + string[] loggerSwitchParameters, + string[] distributedLoggerSwitchParameters, + string[] verbositySwitchParameters, + bool noConsoleLogger, + bool distributedFileLogger, + string[] fileLoggerParameters, + string[] consoleLoggerParameters, + string[][] groupedFileLoggerParameters, + out List distributedLoggerRecords, + out LoggerVerbosity verbosity, + ref bool detailedSummary, + int cpuCount + ) + { + // if verbosity level is not specified, use the default + verbosity = LoggerVerbosity.Normal; + + if (verbositySwitchParameters.Length > 0) + { + // Read the last verbosity switch found + verbosity = ProcessVerbositySwitch(verbositySwitchParameters[verbositySwitchParameters.Length - 1]); + } + + ArrayList loggers = ProcessLoggerSwitch(loggerSwitchParameters, verbosity); + + // Add any loggers which have been specified on the commandline + distributedLoggerRecords = ProcessDistributedLoggerSwitch(distributedLoggerSwitchParameters, verbosity); + + ProcessConsoleLoggerSwitch(noConsoleLogger, consoleLoggerParameters, distributedLoggerRecords, verbosity, cpuCount, loggers); + + ProcessDistributedFileLogger(distributedFileLogger, fileLoggerParameters, distributedLoggerRecords, loggers, cpuCount); + + ProcessFileLoggers(groupedFileLoggerParameters, distributedLoggerRecords, verbosity, cpuCount, loggers); + + if (verbosity == LoggerVerbosity.Diagnostic) + { + detailedSummary = true; + } + + return (ILogger[])loggers.ToArray(typeof(ILogger)); + } + + /// + /// Parameters for a particular logger may be passed in fragments that we have to aggregate: for example, + /// /flp:foo=bar;baz=biz /flp:boz=bez becomes "foo=bar;baz=biz;boz=bez" + /// We are going to aggregate the LoggerParameters into one LoggerParameters string + /// to do this we must first trim off the ; from the start and the end of the strings as + /// this would interfere with the use of string.Join by possibly having ;; at the beginning or end of a + /// logger parameter + /// + internal static string AggregateParameters(string anyPrefixingParameter, string[] parametersToAggregate) + { + for (int i = 0; i < parametersToAggregate.Length; i++) + { + parametersToAggregate[i] = parametersToAggregate[i].Trim(';'); + } + + // Join the logger parameters into one string seperated by semicolons + string result = anyPrefixingParameter ?? String.Empty; + + result += String.Join(";", parametersToAggregate); + + return result; + } + + /// + /// Add a file logger with the appropriate parameters to the loggers list for each + /// non-empty set of file logger parameters provided. + /// + private static void ProcessFileLoggers(string[][] groupedFileLoggerParameters, List distributedLoggerRecords, LoggerVerbosity verbosity, int cpuCount, ArrayList loggers) + { + for (int i = 0; i < groupedFileLoggerParameters.Length; i++) + { + // If we had no, say, "/fl5" then continue; we may have a "/fl6" and so on + if (groupedFileLoggerParameters[i] == null) continue; + + string fileParameters = "SHOWPROJECTFILE=TRUE;"; + // Use a default log file name of "msbuild.log", "msbuild1.log", "msbuild2.log", etc; put this first on the parameter + // list so that any supplied log file parameter will override it + if (i == 0) + { + fileParameters += "logfile=msbuild.log;"; + } + else + { + fileParameters += "logfile=msbuild" + i + ".log;"; + } + + if (groupedFileLoggerParameters[i].Length > 0) + { + // Join the file logger parameters into one string seperated by semicolons + fileParameters = AggregateParameters(fileParameters, groupedFileLoggerParameters[i]); + } + + FileLogger fileLogger = new FileLogger(); + // Set to detailed by default, can be overidden by fileLoggerParameters + LoggerVerbosity defaultFileLoggerVerbosity = LoggerVerbosity.Detailed; + fileLogger.Verbosity = defaultFileLoggerVerbosity; + + if (cpuCount == 1) + { + // We've decided to use the MP logger even in single proc mode. + // Switch it on here, rather than in the logger, so that other hosts that use + // the existing ConsoleLogger don't see the behavior change in single proc. + fileLogger.Parameters = "ENABLEMPLOGGING;" + fileParameters; + loggers.Add(fileLogger); + } + else + { + fileLogger.Parameters = fileParameters; + + // For performance, register this logger using the forwarding logger mechanism, rather than as an old-style + // central logger. + DistributedLoggerRecord forwardingLoggerRecord = CreateForwardingLoggerRecord(fileLogger, fileParameters, defaultFileLoggerVerbosity); + distributedLoggerRecords.Add(forwardingLoggerRecord); + } + } + } + + /// + /// Process the noconsole switch and attach or not attach the correct console loggers + /// + internal static void ProcessConsoleLoggerSwitch + ( + bool noConsoleLogger, + string[] consoleLoggerParameters, + List distributedLoggerRecords, + LoggerVerbosity verbosity, + int cpuCount, + ArrayList loggers + ) + { + // the console logger is always active, unless specifically disabled + if (!noConsoleLogger) + { + // A central logger will be created for single proc and multiproc + ConsoleLogger logger = new ConsoleLogger(verbosity); + string consoleParameters = "SHOWPROJECTFILE=TRUE;"; + + if ((consoleLoggerParameters != null) && (consoleLoggerParameters.Length > 0)) + { + consoleParameters = AggregateParameters(consoleParameters, consoleLoggerParameters); + } + + if (cpuCount == 1) + { + // We've decided to use the MP logger even in single proc mode. + // Switch it on here, rather than in the logger, so that other hosts that use + // the existing ConsoleLogger don't see the behavior change in single proc. + logger.Parameters = "ENABLEMPLOGGING;" + consoleParameters; + loggers.Add(logger); + } + else + { + logger.Parameters = consoleParameters; + + // For performance, register this logger using the forwarding logger mechanism, rather than as an old-style + // central logger. + DistributedLoggerRecord forwardingLoggerRecord = CreateForwardingLoggerRecord(logger, consoleParameters, verbosity); + distributedLoggerRecords.Add(forwardingLoggerRecord); + } + } + } + + /// + /// Returns a DistributedLoggerRecord containing this logger and a ConfigurableForwardingLogger. + /// Looks at the logger's parameters for any verbosity parameter in order to make sure it is setting up the ConfigurableForwardingLogger + /// with the verbosity level that the logger will actually use. + /// + private static DistributedLoggerRecord CreateForwardingLoggerRecord(ILogger logger, string loggerParameters, LoggerVerbosity defaultVerbosity) + { + string verbosityParameter = ExtractAnyLoggerParameter(loggerParameters, "verbosity", "v"); + + string verbosityValue = ExtractAnyParameterValue(verbosityParameter); + + LoggerVerbosity effectiveVerbosity = defaultVerbosity; + if (!String.IsNullOrEmpty(verbosityValue)) + { + effectiveVerbosity = ProcessVerbositySwitch(verbosityValue); + } + + //Gets the currently loaded assembly in which the specified class is defined + Assembly engineAssembly = Assembly.GetAssembly(typeof(ProjectCollection)); + string loggerClassName = "Microsoft.Build.Logging.ConfigurableForwardingLogger"; + string loggerAssemblyName = engineAssembly.GetName().FullName; + LoggerDescription forwardingLoggerDescription = new LoggerDescription(loggerClassName, loggerAssemblyName, null, loggerParameters, effectiveVerbosity); + DistributedLoggerRecord distributedLoggerRecord = new DistributedLoggerRecord(logger, forwardingLoggerDescription); + + return distributedLoggerRecord; + } + + /// + /// Process the file logger switches and attach the correct file loggers. Internal for testing + /// + internal static void ProcessDistributedFileLogger + ( + bool distributedFileLogger, + string[] fileLoggerParameters, + List distributedLoggerRecords, + ArrayList loggers, + int cpuCount + ) + { + if (distributedFileLogger) + { + string fileParameters = string.Empty; + if ((fileLoggerParameters != null) && (fileLoggerParameters.Length > 0)) + { + // Join the file logger parameters into one string seperated by semicolons + fileParameters = AggregateParameters(null, fileLoggerParameters); + } + + // Check to see if the logfile parameter has been set, if not set it to the current directory + string logFileParameter = ExtractAnyLoggerParameter(fileParameters, "logfile"); + + string logFileName = ExtractAnyParameterValue(logFileParameter); + + try + { + // If the path is not an absolute path set the path to the current directory of the exe combined with the relative path + // If the string is empty then send it through as the distributed file logger WILL deal with EMPTY logfile paths + if (!String.IsNullOrEmpty(logFileName) && !Path.IsPathRooted(logFileName)) + { + fileParameters = fileParameters.Replace(logFileParameter, "logFile=" + Path.Combine(Environment.CurrentDirectory, logFileName)); + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + throw new LoggerException(e.Message, e); + } + + if (String.IsNullOrEmpty(logFileName)) + { + // If the string is not empty and it does not end in a ;, we need to add a ; to seperate what is in the parameter from the logfile + // if the string is empty, no ; is needed because logfile is the only parameter which will be passed in + if (!String.IsNullOrEmpty(fileParameters) && !fileParameters.EndsWith(";", StringComparison.OrdinalIgnoreCase)) + { + fileParameters += ";"; + } + + fileParameters += "logFile=" + Path.Combine(Environment.CurrentDirectory, msbuildLogFileName); + } + + //Gets the currently loaded assembly in which the specified class is defined + Assembly engineAssembly = Assembly.GetAssembly(typeof(ProjectCollection)); + string loggerClassName = "Microsoft.Build.Logging.DistributedFileLogger"; + string loggerAssemblyName = engineAssembly.GetName().FullName; + // Node the verbosity parameter is not used by the Distributed file logger so changing it here has no effect. It must be changed in the distributed file logger + LoggerDescription forwardingLoggerDescription = new LoggerDescription(loggerClassName, loggerAssemblyName, null, fileParameters, LoggerVerbosity.Detailed); + // Use the null as the central Logger, this will cause the engine to instantiate the NullCentralLogger, this logger will throw an exception if anything except for the buildstarted and buildFinished events are sent + DistributedLoggerRecord distributedLoggerRecord = new DistributedLoggerRecord(null, forwardingLoggerDescription); + distributedLoggerRecords.Add(distributedLoggerRecord); + } + } + + /// + /// Given a string of aggregated parameters, such as "foo=bar;baz;biz=boz" and a list of parameter names, + /// such as "biz", tries to find and return the LAST matching parameter, such as "biz=boz" + /// + internal static string ExtractAnyLoggerParameter(string parameters, params string[] parameterNames) + { + string[] nameValues = parameters.Split(';'); + string result = null; + + foreach (string nameValue in nameValues) + { + foreach (string name in parameterNames) + { + bool found = nameValue.StartsWith(name + "=", StringComparison.OrdinalIgnoreCase) || // Parameters with value, such as "logfile=foo.txt" + String.Equals(name, nameValue, StringComparison.OrdinalIgnoreCase); // Parameters without value, such as "append" + + if (found) + { + result = nameValue; + } + } + } + + return result; + } + + /// + /// Given a parameter, such as "foo=bar", tries to find and return + /// the value part, ie "bar"; otherwise returns null. + /// + private static string ExtractAnyParameterValue(string parameter) + { + string value = null; + + if (!String.IsNullOrEmpty(parameter)) + { + string[] nameValuePair = parameter.Split('='); + + value = (nameValuePair.Length > 1) ? nameValuePair[1] : null; + } + + return value; + } + + /// + /// Figures out what verbosity level to assign to loggers. + /// + /// + /// Internal for unit testing only + /// + /// + /// The logger verbosity level. + internal static LoggerVerbosity ProcessVerbositySwitch(string value) + { + LoggerVerbosity verbosity = LoggerVerbosity.Normal; + + if (String.Equals(value, "q", StringComparison.OrdinalIgnoreCase) || + String.Equals(value, "quiet", StringComparison.OrdinalIgnoreCase)) + { + verbosity = LoggerVerbosity.Quiet; + } + else if (String.Equals(value, "m", StringComparison.OrdinalIgnoreCase) || + String.Equals(value, "minimal", StringComparison.OrdinalIgnoreCase)) + { + verbosity = LoggerVerbosity.Minimal; + } + else if (String.Equals(value, "n", StringComparison.OrdinalIgnoreCase) || + String.Equals(value, "normal", StringComparison.OrdinalIgnoreCase)) + { + verbosity = LoggerVerbosity.Normal; + } + else if (String.Equals(value, "d", StringComparison.OrdinalIgnoreCase) || + String.Equals(value, "detailed", StringComparison.OrdinalIgnoreCase)) + { + verbosity = LoggerVerbosity.Detailed; + } + else if (String.Equals(value, "diag", StringComparison.OrdinalIgnoreCase) || + String.Equals(value, "diagnostic", StringComparison.OrdinalIgnoreCase)) + { + verbosity = LoggerVerbosity.Diagnostic; + } + else + { + CommandLineSwitchException.Throw("InvalidVerbosityError", value); + } + + return verbosity; + } + + /// + /// Figures out which additional loggers are going to listen to build events. + /// + /// List of loggers. + private static ArrayList ProcessLoggerSwitch(string[] parameters, LoggerVerbosity verbosity) + { + ArrayList loggers = new ArrayList(); + + foreach (string parameter in parameters) + { + string unquotedParameter = QuotingUtilities.Unquote(parameter); + + LoggerDescription loggerDescription = ParseLoggingParameter(parameter, unquotedParameter, verbosity); + + loggers.Add(CreateAndConfigureLogger(loggerDescription, verbosity, unquotedParameter)); + } + + return loggers; + } + + /// + /// Parses command line arguments describing the distributed loggers + /// + /// List of distributed logger records + private static List ProcessDistributedLoggerSwitch(string[] parameters, LoggerVerbosity verbosity) + { + List distributedLoggers = new List(); + + foreach (string parameter in parameters) + { + // split each | string into two pieces, breaking on the first | that is found + int emptySplits; // ignored + ArrayList loggerSpec = QuotingUtilities.SplitUnquoted(parameter, 2, true /* keep empty splits */, false /* keep quotes */, out emptySplits, '*'); + + ErrorUtilities.VerifyThrow((loggerSpec.Count >= 1) && (loggerSpec.Count <= 2), + "SplitUnquoted() must return at least one string, and no more than two."); + + string unquotedParameter = QuotingUtilities.Unquote((string)loggerSpec[0]); + LoggerDescription centralLoggerDescription = + ParseLoggingParameter((string)loggerSpec[0], unquotedParameter, verbosity); + + ILogger centralLogger = CreateAndConfigureLogger(centralLoggerDescription, verbosity, unquotedParameter); + + // By default if no forwarding logger description is specified the same logger is used for both functions + LoggerDescription forwardingLoggerDescription = centralLoggerDescription; + + if (loggerSpec.Count > 1) + { + unquotedParameter = QuotingUtilities.Unquote((string)loggerSpec[1]); + forwardingLoggerDescription = ParseLoggingParameter((string)loggerSpec[1], unquotedParameter, verbosity); + } + + DistributedLoggerRecord distributedLoggerRecord = + new DistributedLoggerRecord(centralLogger, forwardingLoggerDescription); + + distributedLoggers.Add(distributedLoggerRecord); + } + + return distributedLoggers; + } + + /// + /// Parse a command line logger argument into a LoggerDescription structure + /// + /// the command line string + /// + private static LoggerDescription ParseLoggingParameter(string parameter, string unquotedParameter, LoggerVerbosity verbosity) + { + ArrayList loggerSpec; + string loggerClassName; + string loggerAssemblyName; + string loggerAssemblyFile; + string loggerParameters = null; + + int emptySplits; // ignored + + // split each ; string into two pieces, breaking on the first ; that is found + loggerSpec = QuotingUtilities.SplitUnquoted(parameter, 2, true /* keep empty splits */, false /* keep quotes */, out emptySplits, ';'); + + ErrorUtilities.VerifyThrow((loggerSpec.Count >= 1) && (loggerSpec.Count <= 2), + "SplitUnquoted() must return at least one string, and no more than two."); + + // check that the logger is specified + CommandLineSwitchException.VerifyThrow(((string)loggerSpec[0]).Length > 0, + "InvalidLoggerError", unquotedParameter); + + // extract logger parameters if present + if (loggerSpec.Count == 2) + { + loggerParameters = QuotingUtilities.Unquote((string)loggerSpec[1]); + } + + // split each , string into two pieces, breaking on the first , that is found + ArrayList loggerTypeSpec = QuotingUtilities.SplitUnquoted((string)loggerSpec[0], 2, true /* keep empty splits */, false /* keep quotes */, out emptySplits, ','); + + ErrorUtilities.VerifyThrow((loggerTypeSpec.Count >= 1) && (loggerTypeSpec.Count <= 2), + "SplitUnquoted() must return at least one string, and no more than two."); + + + string loggerAssemblySpec; + + // if the logger class and assembly are both specified + if (loggerTypeSpec.Count == 2) + { + loggerClassName = QuotingUtilities.Unquote((string)loggerTypeSpec[0]); + loggerAssemblySpec = QuotingUtilities.Unquote((string)loggerTypeSpec[1]); + } + else + { + loggerClassName = String.Empty; + loggerAssemblySpec = QuotingUtilities.Unquote((string)loggerTypeSpec[0]); + } + + CommandLineSwitchException.VerifyThrow(loggerAssemblySpec.Length > 0, + "InvalidLoggerError", unquotedParameter); + + loggerAssemblyName = null; + loggerAssemblyFile = null; + + // DDB Bug msbuild.exe -Logger:FileLogger,Microsoft.Build.Engine fails due to moved engine file. + // Only add strong naming if the assembly is a non-strong named 'Microsoft.Build.Engine' (i.e, no additional characteristics) + // Concat full Strong Assembly to match v4.0 + if (String.Compare(loggerAssemblySpec, "Microsoft.Build.Engine", StringComparison.OrdinalIgnoreCase) == 0) + { + loggerAssemblySpec = "Microsoft.Build.Engine,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a"; + } + + // figure out whether the assembly's identity (strong/weak name), or its filename/path is provided + if (File.Exists(loggerAssemblySpec)) + { + loggerAssemblyFile = loggerAssemblySpec; + } + else + { + loggerAssemblyName = loggerAssemblySpec; + } + + return new LoggerDescription(loggerClassName, loggerAssemblyName, loggerAssemblyFile, loggerParameters, verbosity); + } + + /// + /// Loads a logger from its assembly, instantiates it, and handles errors. + /// + /// Instantiated logger. + private static ILogger CreateAndConfigureLogger + ( + LoggerDescription loggerDescription, + LoggerVerbosity verbosity, + string unquotedParameter + ) + { + ILogger logger = null; + + try + { + logger = loggerDescription.CreateLogger(); + + if (logger == null) + { + InitializationException.VerifyThrow(logger != null, "LoggerNotFoundError", unquotedParameter); + } + } + catch (IOException e) + { + InitializationException.Throw("LoggerCreationError", unquotedParameter, e, false); + } + catch (BadImageFormatException e) + { + InitializationException.Throw("LoggerCreationError", unquotedParameter, e, false); + } + catch (SecurityException e) + { + InitializationException.Throw("LoggerCreationError", unquotedParameter, e, false); + } + catch (ReflectionTypeLoadException e) + { + InitializationException.Throw("LoggerCreationError", unquotedParameter, e, false); + } + catch (MemberAccessException e) + { + InitializationException.Throw("LoggerCreationError", unquotedParameter, e, false); + } + catch (TargetInvocationException e) + { + InitializationException.Throw("LoggerFatalError", unquotedParameter, e.InnerException, true); + } + + // Configure the logger by setting the verbosity level and parameters + try + { + // set its verbosity level + logger.Verbosity = verbosity; + + // set the logger parameters (if any) + if (loggerDescription.LoggerSwitchParameters != null) + { + logger.Parameters = loggerDescription.LoggerSwitchParameters; + } + } + + catch (LoggerException) + { + // Logger failed politely during parameter/verbosity setting + throw; + } + catch (Exception e) + { + InitializationException.Throw("LoggerFatalError", unquotedParameter, e, true); + } + + return logger; + } + + /// + /// Figures out if the project needs to be validated against a schema. + /// + /// + /// The schema to validate against, or null. + private static string ProcessValidateSwitch(string[] parameters) + { + string schemaFile = null; + + foreach (string parameter in parameters) + { + InitializationException.VerifyThrow(schemaFile == null, "MultipleSchemasError", parameter); + InitializationException.VerifyThrow(File.Exists(parameter), "SchemaNotFoundError", parameter); + + schemaFile = Path.Combine(Directory.GetCurrentDirectory(), parameter); + } + + return schemaFile; + } + + /// + /// Given an invalid ToolsVersion string and the collection of valid toolsets, + /// throws an InitializationException with the appropriate message. + /// + private static void ThrowInvalidToolsVersionInitializationException(IEnumerable toolsets, string toolsVersion) + { + string toolsVersionList = String.Empty; + foreach (Toolset toolset in toolsets) + { + toolsVersionList += "\"" + toolset.ToolsVersion + "\", "; + } + + // Remove trailing comma and space + if (toolsVersionList.Length > 0) + { + toolsVersionList = toolsVersionList.Substring(0, toolsVersionList.Length - 2); + } + + string message = ResourceUtilities.FormatResourceString + ( + "UnrecognizedToolsVersion", + toolsVersion, + toolsVersionList + ); + message = ResourceUtilities.FormatResourceString("InvalidToolsVersionError", message); + + InitializationException.Throw(message, toolsVersion); + } + + /// + /// Displays the application copyright message/logo. + /// + private static void DisplayCopyrightMessage() + { + Console.WriteLine(ResourceUtilities.FormatResourceString("CopyrightMessage", ProjectCollection.Version.ToString())); + } + + /// + /// Displays the help message that explains switch usage and syntax. + /// + private static void ShowHelpMessage() + { + // NOTE: the help message is broken into pieces because localization + // prefers it that way -- see VSW #482758 "Entire command line help + // message is stored in a single resource" + Console.WriteLine(AssemblyResources.GetString("HelpMessage_1_Syntax")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_2_Description")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_3_SwitchesHeader")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_9_TargetSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_10_PropertySwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_17_MaximumCPUSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_23_ToolsVersionSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_12_VerbositySwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_13_ConsoleLoggerParametersSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_14_NoConsoleLoggerSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_20_FileLoggerSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_22_FileLoggerParametersSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_18_DistributedLoggerSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_21_DistributedFileLoggerSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_11_LoggerSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_15_ValidateSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_19_IgnoreProjectExtensionsSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_24_NodeReuse")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_25_PreprocessSwitch")); + + Console.WriteLine(AssemblyResources.GetString("HelpMessage_26_DetailedSummarySwitch")); + + if (CommandLineSwitches.IsParameterlessSwitch("debug")) + { + Console.WriteLine(AssemblyResources.GetString("HelpMessage_27_DebuggerSwitch")); + } + + Console.WriteLine(AssemblyResources.GetString("HelpMessage_7_ResponseFile")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_8_NoAutoResponseSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_5_NoLogoSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_6_VersionSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_4_HelpSwitch")); + Console.WriteLine(AssemblyResources.GetString("HelpMessage_16_Examples")); + } + + /// + /// Displays a message prompting the user to look up help. + /// + private static void ShowHelpPrompt() + { + Console.WriteLine(AssemblyResources.GetString("HelpPrompt")); + } + + /// + /// Displays the build engine's version number. + /// + private static void ShowVersion() + { + Console.Write(ProjectCollection.Version.ToString()); + } + } +} diff --git a/src/XMakeCommandLine/app.config b/src/XMakeCommandLine/app.config new file mode 100644 index 00000000000..6dfd66bed89 --- /dev/null +++ b/src/XMakeCommandLine/app.config @@ -0,0 +1,48 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/clr2/OutOfProcTaskAppDomainWrapper.cs b/src/XMakeCommandLine/clr2/OutOfProcTaskAppDomainWrapper.cs new file mode 100644 index 00000000000..edfcf49260c --- /dev/null +++ b/src/XMakeCommandLine/clr2/OutOfProcTaskAppDomainWrapper.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Class implementing an out-of-proc node for executing tasks inside an AppDomain. +//----------------------------------------------------------------------- +using System; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.CommandLine +{ + /// + /// Class for executing a task in an AppDomain + /// + [Serializable] + internal class OutOfProcTaskAppDomainWrapper : OutOfProcTaskAppDomainWrapperBase + { + /// + /// This is a stub for CLR2 in place of the OutOfProcTaskAppDomainWrapper class + /// as used in CLR4 to support cancellation of ICancelable tasks. + /// We provide a stub for CancelTask here so that the OutOfProcTaskHostNode + /// that's shared by both the MSBuild.exe and MSBuildTaskHost.exe, + /// can safely allow MSBuild.exe CLR4 Out-Of-Proc Task Host to call ICancelableTask.Cancel() + /// + /// False - Used by the OutOfProcTaskHostNode to determine if the task is ICancelable + internal bool CancelTask() + { + // This method is a stub we will not do anything here. + return false; + } + } +} diff --git a/src/XMakeCommandLine/msbuild.suitebin.config b/src/XMakeCommandLine/msbuild.suitebin.config new file mode 100644 index 00000000000..848032d0dce --- /dev/null +++ b/src/XMakeCommandLine/msbuild.suitebin.config @@ -0,0 +1,31 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeCommandLine/msbuild_rascal.manifest b/src/XMakeCommandLine/msbuild_rascal.manifest new file mode 100644 index 00000000000..64db722f8f4 --- /dev/null +++ b/src/XMakeCommandLine/msbuild_rascal.manifest @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/XMakeCommandLine/native.rc b/src/XMakeCommandLine/native.rc new file mode 100644 index 00000000000..2a78fbf4eca --- /dev/null +++ b/src/XMakeCommandLine/native.rc @@ -0,0 +1,12 @@ +// From Dev12 on we want the versioning strings to be the VS ones instead of .NET Framework ones. + +#include +#include + +#include +#include + +APPICON ICON "MSBuild.ico" +#ifndef BUILDING_DF_LKG +1 RT_MANIFEST "msbuild.exe.manifest" +#endif diff --git a/src/XMakeCommandLine/xclpupdate.bat b/src/XMakeCommandLine/xclpupdate.bat new file mode 100644 index 00000000000..512cb096bc6 --- /dev/null +++ b/src/XMakeCommandLine/xclpupdate.bat @@ -0,0 +1,36 @@ +@if "%_echo%"=="" echo off + +::@echo ... Starting msbuild.exe (only) pupdate ... +echo Starting msbuild.exe (only) update... +setlocal + +if exist xpupdate.log del xpupdate.log + + +for /f %%i in ('dir /b /ad /on %windir%\microsoft.net\framework\v4.*') do set fxpath=%windir%\microsoft.net\framework\%%i + +call :Doit copy %_NTTREE%\bin\i386\msbuild.??? %fxpath% /y +call :Doit copy %_NTTREE%\bin\i386\msbuild.urt.config %fxpath%\msbuild.exe.config /y + +@echo Now kicking off async refresh of native images ... + +setlocal +set complus_installroot= +set complus_version= + + start "update native image for msbuildexe" /low /min %fxpath%\ngen install "%_NTTREE%\bin\i386\msbuild.exe" + +endlocal + + +goto :eof + + +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:doit +echo %* >> xpupdate.log +%* >> xpupdate.log 2>&1 2>con +if errorlevel 1 echo Error running command "%*" >> xpupdate.log > con +goto :eof + + diff --git a/src/XMakeTasks/Al.cs b/src/XMakeTasks/Al.cs new file mode 100644 index 00000000000..ca4e638927d --- /dev/null +++ b/src/XMakeTasks/Al.cs @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +using System.Collections; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines the "AL" XMake task, which enables using al.exe to link + /// modules and resource files into assemblies. + /// + public class AL : ToolTaskExtension + { + #region Properties + /* + Microsoft (R) Assembly Linker version 7.10.2175 + for Microsoft (R) .NET Framework version 1.2 + Copyright (C) Microsoft Corporation 2001-2002. All rights reserved. + + Usage: al [options] [sources] + Options: ('/out' must be specified) + + /? or /help Display this usage message + @ Read response file for more options + /algid: Algorithm used to hash files (in hexadecimal) + /base[address]: Base address for the library + /bugreport: Create a 'Bug Report' file + /comp[any]: Company name + /config[uration]: Configuration string + /copy[right]: Copyright message + /c[ulture]: Supported culture + /delay[sign][+|-] Delay sign this assembly + /descr[iption]: Description + /e[vidence]: Security evidence file to embed + /fileversion: Optional Win32 version (overrides assembly version) + /flags: Assembly flags (in hexadecimal) + /fullpaths Display files using fully-qualified filenames + /keyf[ile]: File containing key to sign the assembly + /keyn[ame]: Key container name of key to sign assembly + /main: Specifies the method name of the entry point + /nologo Suppress the startup banner and copyright message + /out: Output file name for the assembly manifest + /platform: Limit which platforms this code can run on; must be + one of x86, ia64, amd64, or portable (the default) + /prod[uct]: Product name + /productv[ersion]: Product version + /t[arget]:lib[rary] Create a library + /t[arget]:exe Create a console executable + /t[arget]:win[exe] Create a Windows executable + /template: Specifies an assembly to get default options from + /title: Title + /trade[mark]: Trademark message + /v[ersion]: Version (use * to auto-generate remaining numbers) + /win32icon: Use this icon for the output + /win32res: Specifies the Win32 resource file + + Sources: (at least one source input is required) + [,] add file to assembly + /embed[resource]:[,[,Private]] + embed the file as a resource in the assembly + /link[resource]:[,[,[,Private]]] + link the file as a resource to the assembly + +*/ + public string AlgorithmId + { + set { Bag["AlgorithmId"] = value; } + get { return (string)Bag["AlgorithmId"]; } + } + + public string BaseAddress + { + set { Bag["BaseAddress"] = value; } + get { return (string)Bag["BaseAddress"]; } + } + + public string CompanyName + { + set { Bag["CompanyName"] = value; } + get { return (string)Bag["CompanyName"]; } + } + + public string Configuration + { + set { Bag["Configuration"] = value; } + get { return (string)Bag["Configuration"]; } + } + + public string Copyright + { + set { Bag["Copyright"] = value; } + get { return (string)Bag["Copyright"]; } + } + + public string Culture + { + set { Bag["Culture"] = value; } + get { return (string)Bag["Culture"]; } + } + + public bool DelaySign + { + set { Bag["DelaySign"] = value; } + get { return GetBoolParameterWithDefault("DelaySign", false); } + } + + public string Description + { + set { Bag["Description"] = value; } + get { return (string)Bag["Description"]; } + } + + public string EvidenceFile + { + set { Bag["EvidenceFile"] = value; } + get { return (string)Bag["EvidenceFile"]; } + } + + public string FileVersion + { + set { Bag["FileVersion"] = value; } + get { return (string)Bag["FileVersion"]; } + } + + public string Flags + { + set { Bag["Flags"] = value; } + get { return (string)Bag["Flags"]; } + } + + public bool GenerateFullPaths + { + set { Bag["GenerateFullPaths"] = value; } + get { return GetBoolParameterWithDefault("GenerateFullPaths", false); } + } + + public string KeyFile + { + set { Bag["KeyFile"] = value; } + get { return (string)Bag["KeyFile"]; } + } + + public string KeyContainer + { + set { Bag["KeyContainer"] = value; } + get { return (string)Bag["KeyContainer"]; } + } + + public string MainEntryPoint + { + set { Bag["MainEntryPoint"] = value; } + get { return (string)Bag["MainEntryPoint"]; } + } + + [Output] + [Required] + public ITaskItem OutputAssembly + { + set { Bag["OutputAssembly"] = value; } + get { return (ITaskItem)Bag["OutputAssembly"]; } + } + + public string Platform + { + set { Bag["Platform"] = value; } + get { return (string)Bag["Platform"]; } + } + + // Map explicit platform of "AnyCPU" or the default platform (null or ""), since it is commonly understood in the + // managed build process to be equivalent to "AnyCPU", to platform "AnyCPU32BitPreferred" if the Prefer32Bit + // property is set. + internal string PlatformWith32BitPreference + { + get + { + string platform = this.Platform; + if ((String.IsNullOrEmpty(platform) || platform.Equals("anycpu", StringComparison.OrdinalIgnoreCase)) && this.Prefer32Bit) + { + platform = "anycpu32bitpreferred"; + } + return platform; + } + } + + public bool Prefer32Bit + { + set { Bag["Prefer32Bit"] = value; } + get { return GetBoolParameterWithDefault("Prefer32Bit", false); } + } + + public string ProductName + { + set { Bag["ProductName"] = value; } + get { return (string)Bag["ProductName"]; } + } + + public string ProductVersion + { + set { Bag["ProductVersion"] = value; } + get { return (string)Bag["ProductVersion"]; } + } + + public string[] ResponseFiles + { + set { Bag["ResponseFiles"] = value; } + get { return (string[])Bag["ResponseFiles"]; } + } + + public string TargetType + { + set { Bag["TargetType"] = value; } + get { return (string)Bag["TargetType"]; } + } + + public string TemplateFile + { + set { Bag["TemplateFile"] = value; } + get { return (string)Bag["TemplateFile"]; } + } + + public string Title + { + set { Bag["Title"] = value; } + get { return (string)Bag["Title"]; } + } + + public string Trademark + { + set { Bag["Trademark"] = value; } + get { return (string)Bag["Trademark"]; } + } + + public string Version + { + set { Bag["Version"] = value; } + get { return (string)Bag["Version"]; } + } + + public string Win32Icon + { + set { Bag["Win32Icon"] = value; } + get { return (string)Bag["Win32Icon"]; } + } + + public string Win32Resource + { + set { Bag["Win32Resource"] = value; } + get { return (string)Bag["Win32Resource"]; } + } + + + // Input files: file[,target] + // This is not required. + public ITaskItem[] SourceModules + { + set { Bag["SourceModules"] = value; } + get { return (ITaskItem[])Bag["SourceModules"]; } + } + + // Embedded resource files: file[,name[,private]] + public ITaskItem[] EmbedResources + { + set { Bag["EmbedResources"] = value; } + get { return (ITaskItem[])Bag["EmbedResources"]; } + } + + // Linked resource files: file[,name[,target][,private]]] + public ITaskItem[] LinkResources + { + set { Bag["LinkResources"] = value; } + get { return (ITaskItem[])Bag["LinkResources"]; } + } + + public string SdkToolsPath + { + set { Bag["SdkToolsPath"] = value; } + get { return (string)Bag["SdkToolsPath"]; } + } + + #endregion + + #region Tool Members + /// + /// Return the name of the tool to execute. + /// + override protected string ToolName + { + get + { + return "AL.exe"; + } + } + + /// + /// Return the path of the tool to execute + /// + override protected string GenerateFullPathToTool() + { + string pathToTool = null; + + // If COMPLUS_InstallRoot\COMPLUS_Version are set (the dogfood world), we want to find it there, instead of + // the SDK, which may or may not be installed. The following will look there. + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("COMPLUS_InstallRoot")) || !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("COMPLUS_Version"))) + { + pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest); + } + + if (String.IsNullOrEmpty(pathToTool) || !File.Exists(pathToTool)) + { + pathToTool = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, Microsoft.Build.Utilities.ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolName, Log, true); + } + + return pathToTool; + } + + /// + /// Fills the provided CommandLineBuilderExtension with those switches and other information that can go into a response file. + /// + override protected internal void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendSwitchIfNotNull("/algid:", this.AlgorithmId); + commandLine.AppendSwitchIfNotNull("/baseaddress:", this.BaseAddress); + commandLine.AppendSwitchIfNotNull("/company:", this.CompanyName); + commandLine.AppendSwitchIfNotNull("/configuration:", this.Configuration); + commandLine.AppendSwitchIfNotNull("/copyright:", this.Copyright); + commandLine.AppendSwitchIfNotNull("/culture:", this.Culture); + commandLine.AppendPlusOrMinusSwitch("/delaysign", this.Bag, "DelaySign"); + commandLine.AppendSwitchIfNotNull("/description:", this.Description); + commandLine.AppendSwitchIfNotNull("/evidence:", this.EvidenceFile); + commandLine.AppendSwitchIfNotNull("/fileversion:", this.FileVersion); + commandLine.AppendSwitchIfNotNull("/flags:", this.Flags); + commandLine.AppendWhenTrue("/fullpaths", this.Bag, "GenerateFullPaths"); + commandLine.AppendSwitchIfNotNull("/keyfile:", this.KeyFile); + commandLine.AppendSwitchIfNotNull("/keyname:", this.KeyContainer); + commandLine.AppendSwitchIfNotNull("/main:", this.MainEntryPoint); + commandLine.AppendSwitchIfNotNull("/out:", (this.OutputAssembly == null) ? null : this.OutputAssembly.ItemSpec); + commandLine.AppendSwitchIfNotNull("/platform:", this.PlatformWith32BitPreference); + commandLine.AppendSwitchIfNotNull("/product:", this.ProductName); + commandLine.AppendSwitchIfNotNull("/productversion:", this.ProductVersion); + commandLine.AppendSwitchIfNotNull("/target:", this.TargetType); + commandLine.AppendSwitchIfNotNull("/template:", this.TemplateFile); + commandLine.AppendSwitchIfNotNull("/title:", this.Title); + commandLine.AppendSwitchIfNotNull("/trademark:", this.Trademark); + commandLine.AppendSwitchIfNotNull("/version:", this.Version); + commandLine.AppendSwitchIfNotNull("/win32icon:", this.Win32Icon); + commandLine.AppendSwitchIfNotNull("/win32res:", this.Win32Resource); + + commandLine.AppendSwitchIfNotNull("", this.SourceModules, new string[] { "TargetFile" }); + + commandLine.AppendSwitchIfNotNull + ( + "/embed:", + this.EmbedResources, + new string[] { "LogicalName", "Access" } + ); + + commandLine.AppendSwitchIfNotNull + ( + "/link:", + this.LinkResources, + new string[] { "LogicalName", "TargetFile", "Access" } + ); + + // It's a good idea for the response file to be the very last switch passed, just + // from a predictability perspective. This is also consistent with the compiler + // tasks (Csc, etc.) + if (this.ResponseFiles != null) + { + foreach (string responseFile in this.ResponseFiles) + { + commandLine.AppendSwitchIfNotNull("@", responseFile); + } + } + } + + public override bool Execute() + { + if (this.Culture != null && this.OutputAssembly != null) + { + // This allows subsequent tasks in the build process to know what culture each satellite + // assembly is associated with. + this.OutputAssembly.SetMetadata("Culture", this.Culture); + } + + return base.Execute(); + } + + #endregion + } +} diff --git a/src/XMakeTasks/AppConfig/AppConfig.cs b/src/XMakeTasks/AppConfig/AppConfig.cs new file mode 100644 index 00000000000..865c3fcd377 --- /dev/null +++ b/src/XMakeTasks/AppConfig/AppConfig.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Collections; +using System.Globalization; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Read information from application .config files. + /// + internal sealed class AppConfig + { + /// + /// Corresponds to the contents of the element. + /// + private RuntimeSection _runtime = new RuntimeSection(); + + /// + /// Read the .config from a file. + /// + /// + internal void Load(string appConfigFile) + { + XmlTextReader reader = null; + try + { + reader = new XmlTextReader(appConfigFile); + reader.DtdProcessing = DtdProcessing.Ignore; + Read(reader); + } + catch (XmlException e) + { + throw new AppConfigException(e.Message, appConfigFile, (reader != null ? reader.LineNumber : 0), (reader != null ? reader.LinePosition : 0), e); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + throw new AppConfigException(e.Message, appConfigFile, (reader != null ? reader.LineNumber : 0), (reader != null ? reader.LinePosition : 0), e); + } + finally + { + if (reader != null) reader.Close(); + } + } + + /// + /// Read the .config from an XmlReader + /// + /// + internal void Read(XmlTextReader reader) + { + // Read the app.config XML + while (reader.Read()) + { + // Look for the section + if (reader.NodeType == XmlNodeType.Element && StringEquals(reader.Name, "runtime")) + { + _runtime.Read(reader); + } + } + } + + /// + /// Access the Runtime section of the application .config file. + /// + /// + internal RuntimeSection Runtime + { + get { return _runtime; } + } + + /// + /// App.config files seem to come with mixed casing for element and attribute names. + /// If the fusion loader can handle this then this code should too. + /// + /// + /// + /// + static internal bool StringEquals(string a, string b) + { + return String.Compare(a, b, StringComparison.OrdinalIgnoreCase) == 0; + } + } +} diff --git a/src/XMakeTasks/AppConfig/AppConfigException.cs b/src/XMakeTasks/AppConfig/AppConfigException.cs new file mode 100644 index 00000000000..549093f929e --- /dev/null +++ b/src/XMakeTasks/AppConfig/AppConfigException.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Tasks +{ + /// + /// An exception thrown while parsing through an app.config. + /// + [Serializable] + internal class AppConfigException : System.ApplicationException + { + /// + /// The name of the app.config file. + /// + private string _fileName = String.Empty; + internal string FileName + { + get + { + return _fileName; + } + } + + + /// + /// The line number with the error. Is initialized to zero + /// + private int _line; + internal int Line + { + get + { + return _line; + } + } + + /// + /// The column with the error. Is initialized to zero + /// + private int _column; + internal int Column + { + get + { + return _column; + } + } + + + /// + /// Construct the exception. + /// + /// + /// + /// + /// + /// + public AppConfigException(string message, string fileName, int line, int column, System.Exception inner) : base(message, inner) + { + _fileName = fileName; + _line = line; + _column = column; + } + + /// + /// Construct the exception. + /// + protected AppConfigException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/XMakeTasks/AppConfig/BindingRedirect.cs b/src/XMakeTasks/AppConfig/BindingRedirect.cs new file mode 100644 index 00000000000..a63e4b9972c --- /dev/null +++ b/src/XMakeTasks/AppConfig/BindingRedirect.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Represents a single from the app.config file. + /// + internal sealed class BindingRedirect + { + /// + /// The low end of the old version range. + /// + private Version _oldVersionLow = null; + + /// + /// The high end of the old version range. + /// + private Version _oldVersionHigh = null; + + /// + /// The new version number. + /// + private Version _newVersion = null; + + /// + /// The low end of the old version range. + /// + internal Version OldVersionLow + { + set { _oldVersionLow = value; } + get { return _oldVersionLow; } + } + + /// + /// The high end of the old version range. + /// + internal Version OldVersionHigh + { + set { _oldVersionHigh = value; } + get { return _oldVersionHigh; } + } + + /// + /// The new version number. + /// + internal Version NewVersion + { + set { _newVersion = value; } + get { return _newVersion; } + } + + /// + /// The reader is positioned on a element--read it. + /// + /// + internal void Read(XmlTextReader reader) + { + string oldVersion = reader.GetAttribute("oldVersion"); + + // A badly formed assembly name. + ErrorUtilities.VerifyThrowArgument(!String.IsNullOrEmpty(oldVersion), "AppConfig.BindingRedirectMissingOldVersion"); + + int dashPosition = oldVersion.IndexOf('-'); + + try + { + if (dashPosition != -1) + { + // This is a version range. + _oldVersionLow = new Version(oldVersion.Substring(0, dashPosition)); + _oldVersionHigh = new Version(oldVersion.Substring(dashPosition + 1)); + } + else + { + // This is a single version. + _oldVersionLow = new Version(oldVersion); + _oldVersionHigh = new Version(oldVersion); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + ErrorUtilities.VerifyThrowArgument(false, e, "AppConfig.InvalidOldVersionAttribute", e.Message); + } + + string newVersionAttribute = reader.GetAttribute("newVersion"); + + // A badly formed assembly name. + ErrorUtilities.VerifyThrowArgument(!String.IsNullOrEmpty(newVersionAttribute), "AppConfig.BindingRedirectMissingNewVersion"); + + try + { + _newVersion = new Version(newVersionAttribute); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + ErrorUtilities.VerifyThrowArgument(false, e, "AppConfig.InvalidNewVersionAttribute", e.Message); + } + } + } +} diff --git a/src/XMakeTasks/AppConfig/DependentAssembly.cs b/src/XMakeTasks/AppConfig/DependentAssembly.cs new file mode 100644 index 00000000000..3ab37f13300 --- /dev/null +++ b/src/XMakeTasks/AppConfig/DependentAssembly.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Reflection; +using System.Collections; +using System.Globalization; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Represents a single from the app.config file. + /// + internal sealed class DependentAssembly + { + /// + /// List of binding redirects. Type is BindingRedirect. + /// + private BindingRedirect[] _bindingRedirects = null; + + /// + /// The partial assemblyname, there should be no version. + /// + private AssemblyName _partialAssemblyName = null; + + /// + /// The partial assemblyname, there should be no version. + /// + internal AssemblyName PartialAssemblyName + { + set + { + _partialAssemblyName = (AssemblyName)value.Clone(); + _partialAssemblyName.Version = null; + } + get + { + if (_partialAssemblyName == null) + { + return null; + } + return (AssemblyName)_partialAssemblyName.Clone(); + } + } + + /// + /// The reader is positioned on a element--read it. + /// + /// + internal void Read(XmlTextReader reader) + { + ArrayList redirects = new ArrayList(); + + if (_bindingRedirects != null) + { + redirects.AddRange(_bindingRedirects); + } + + while (reader.Read()) + { + // Look for the end element. + if (reader.NodeType == XmlNodeType.EndElement && AppConfig.StringEquals(reader.Name, "dependentassembly")) + { + break; + } + + // Look for a element + if (reader.NodeType == XmlNodeType.Element && AppConfig.StringEquals(reader.Name, "assemblyIdentity")) + { + string name = null; + string publicKeyToken = "null"; + string culture = "neutral"; + + // App.config seems to have mixed case attributes. + while (reader.MoveToNextAttribute()) + { + if (AppConfig.StringEquals(reader.Name, "name")) + { + name = reader.Value; + } + else + if (AppConfig.StringEquals(reader.Name, "publicKeyToken")) + { + publicKeyToken = reader.Value; + } + else + if (AppConfig.StringEquals(reader.Name, "culture")) + { + culture = reader.Value; + } + } + + string assemblyName = String.Format + ( + CultureInfo.InvariantCulture, + "{0}, Version=0.0.0.0, Culture={1}, PublicKeyToken={2}", + name, + culture, + publicKeyToken + ); + + try + { + _partialAssemblyName = new AssemblyNameExtension(assemblyName).AssemblyName; + } + catch (System.IO.FileLoadException e) + { + // A badly formed assembly name. + ErrorUtilities.VerifyThrowArgument(false, e, "AppConfig.InvalidAssemblyIdentityFields"); + } + } + + // Look for a element. + if (reader.NodeType == XmlNodeType.Element && AppConfig.StringEquals(reader.Name, "bindingRedirect")) + { + BindingRedirect bindingRedirect = new BindingRedirect(); + bindingRedirect.Read(reader); + redirects.Add(bindingRedirect); + } + } + _bindingRedirects = (BindingRedirect[])redirects.ToArray(typeof(BindingRedirect)); + } + + /// + /// The binding redirects. + /// + /// + internal BindingRedirect[] BindingRedirects + { + set { _bindingRedirects = value; } + get { return _bindingRedirects; } + } + } +} diff --git a/src/XMakeTasks/AppConfig/RuntimeSection.cs b/src/XMakeTasks/AppConfig/RuntimeSection.cs new file mode 100644 index 00000000000..0a7a5506744 --- /dev/null +++ b/src/XMakeTasks/AppConfig/RuntimeSection.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Collections; + +namespace Microsoft.Build.Tasks +{ + /// + /// Wraps the section of the .config file. + /// + internal sealed class RuntimeSection + { + /// + /// List of dependent assemblies. Type is DependentAssembly. + /// + private ArrayList _dependentAssemblies = new ArrayList(); + + /// + /// The reader is positioned on a element--read it. + /// + /// + internal void Read(XmlTextReader reader) + { + while (reader.Read()) + { + // Look for the end element. + if (reader.NodeType == XmlNodeType.EndElement && AppConfig.StringEquals(reader.Name, "runtime")) + { + return; + } + + // Look for a element + if (reader.NodeType == XmlNodeType.Element && AppConfig.StringEquals(reader.Name, "dependentAssembly")) + { + DependentAssembly dependentAssembly = new DependentAssembly(); + dependentAssembly.Read(reader); + + // Only add if there was an tag. + // Otherwise, this section is no use. + if (dependentAssembly.PartialAssemblyName != null) + { + _dependentAssemblies.Add(dependentAssembly); + } + } + } + } + + /// + /// Return the collection of dependent assemblies for this runtime element. + /// + /// + internal DependentAssembly[] DependentAssemblies + { + get { return (DependentAssembly[])_dependentAssemblies.ToArray(typeof(DependentAssembly)); } + } + } +} diff --git a/src/XMakeTasks/AppDomainIsolatedTaskExtension.cs b/src/XMakeTasks/AppDomainIsolatedTaskExtension.cs new file mode 100644 index 00000000000..a3e5ef8f22a --- /dev/null +++ b/src/XMakeTasks/AppDomainIsolatedTaskExtension.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Security.Permissions; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class provides the same functionality as the Task class, but derives from MarshalByRefObject so that it can be + /// instantiated in its own app domain. + /// + [LoadInSeparateAppDomain] + public abstract class AppDomainIsolatedTaskExtension : AppDomainIsolatedTask + { + #region Constructors + + internal AppDomainIsolatedTaskExtension() : + base(AssemblyResources.PrimaryResources, "MSBuild.") + { + _logExtension = new TaskLoggingHelperExtension( + this, + AssemblyResources.PrimaryResources, + AssemblyResources.SharedResources, + "MSBuild."); + } + + #endregion + + #region Properties + + /// + /// Gets an instance of a TaskLoggingHelperExtension class containing task logging methods. + /// + /// The logging helper object. + new public TaskLoggingHelper Log + { + get + { + return _logExtension; + } + } + + // the logging helper + private TaskLoggingHelperExtension _logExtension; + + #endregion + } +} diff --git a/src/XMakeTasks/AspNetCompiler.cs b/src/XMakeTasks/AspNetCompiler.cs new file mode 100644 index 00000000000..1f541d8f94f --- /dev/null +++ b/src/XMakeTasks/AspNetCompiler.cs @@ -0,0 +1,375 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// The AspNetCompiler task, which is a wrapper around aspnet_compiler.exe + /// + public class AspNetCompiler : ToolTaskExtension + { + /* + C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg>aspnet_compiler /? + Utility to precompile an ASP.NET application + Copyright (C) Microsoft Corporation. All rights reserved. + + Usage: + aspnet_compiler [-?] [-m metabasePath | -v virtualPath [-p physicalDir]] + [[-u] [-f] [-d] targetDir] [-c] [-fixednames] + [-keyfile file | -keycontainer container [-aptca] [-delaySign]] + + -? Prints this help text. + -m The full IIS metabase path of the application. This switch + cannot be combined with the -v or -p switches. + -v The virtual path of the application to be compiled (e.g. + "/MyApp"). If -p is specified, the physical path is used to + locate the application. Otherwise, the IIS metabase is used, and + the application is assumed to be in the default site (under + "/LM/W3SVC/1/Root"). This switch cannot be combined with the -m + switch. + -p The physical path of the application to be compiled. If -p is + missing, the IIS metabase is used to locate the app. This switch + must be combined with -v. + -u If specified, the precompiled application is updatable. + -f Overwrites the target directory if it already exists. Existing + contents are lost. + -d If specified, the debug information is emitted during + compilation. + targetDir The physical path to which the application is compiled. If not + specified, the application is precompiled in-place. + -c If specified, the precompiled application is fully rebuilt. Any + previously compiled components will be re-compiled. This option + is always enabled when targetDir is specified. + -keyfile The physical path to the strong name key file. + -keycontainer Specifies a strong name key container. + -aptca If specified, the strong-name assembly will allow partially + trusted callers. + -delaysign If specified, the assemblly is not fully signed when created. + -fixednames If specified, the compiled assemblies will be given fixed names. + -nologo Suppress compiler copyright message. + + Examples: + + The following two commands are equivalent, and rely on the IIS metabase. The + compiled application is deployed to c:\MyTarget: + aspnet_compiler -m /LM/W3SVC/1/Root/MyApp c:\MyTarget + aspnet_compiler -v /MyApp c:\MyTarget + + The following command compiles the application /MyApp in-place. The effect is + that no more compilations will be needed when HTTP requests are sent to it: + aspnet_compiler -v /MyApp + + The following command does *not* rely on the IIS metabase, as it explicitly + specifies the physical source directory of the application: + aspnet_compiler -v /MyApp -p c:\myapp c:\MyTarget + */ + + private bool _updateable; + private bool _force; + private bool _debug; + private bool _clean; + private bool _aptca; + private bool _delaySign; + private bool _fixedNames; + + /// + /// If specified, the strong-name assembly will allow partially + /// trusted callers. + /// + public bool AllowPartiallyTrustedCallers + { + get { return _aptca; } + set { _aptca = value; } + } + + /// + /// If specified, the assemblly is not fully signed when created. + /// + public bool DelaySign + { + get { return _delaySign; } + set { _delaySign = value; } + } + + /// + /// If specified, the compiled assemblies will be given fixed names. + /// + public bool FixedNames + { + get { return _fixedNames; } + set { _fixedNames = value; } + } + + /// + /// Specifies a strong name key container. + /// + public string KeyContainer + { + get { return (string)Bag["KeyContainer"]; } + set { Bag["KeyContainer"] = value; } + } + + /// + /// The physical path to the strong name key file. + /// + public string KeyFile + { + get { return (string)Bag["KeyFile"]; } + set { Bag["KeyFile"] = value; } + } + + /// + /// The full IIS metabase path of the application. This switch + /// cannot be combined with the virtualPath or PhysicalDir option. + /// + public string MetabasePath + { + get { return (string)Bag["MetabasePath"]; } + set { Bag["MetabasePath"] = value; } + } + + /// + /// The physical path of the application to be compiled. If physicalDir + /// is missing, the IIS metabase is used to locate the application. + /// + public string PhysicalPath + { + get { return (string)Bag["PhysicalPath"]; } + set { Bag["PhysicalPath"] = value; } + } + + /// + /// The physical path to which the application is compiled. If not + /// specified, the application is precompiled in-place. + /// + public string TargetPath + { + get { return (string)Bag["TargetPath"]; } + set { Bag["TargetPath"] = value; } + } + + /// + /// The virtual path of the application to be compiled. If PhysicalDir is + /// used to locate the application is specified. Otherwise, the IIS metabase + /// is used, and the application is assumed to be in the default site (under + /// "/LM/W3SVC/1/Root"). + /// + public string VirtualPath + { + get { return (string)Bag["VirtualPath"]; } + set { Bag["VirtualPath"] = value; } + } + + /// + /// If Updateable is true, then the web is compile with -u flag so that it + /// can be updated after compilation + /// + public bool Updateable + { + get { return _updateable; } + set { _updateable = value; } + } + + /// + /// If Force is true, then the web is compile with -f flag overwriting + /// files in the target location + /// + public bool Force + { + get { return _force; } + set { _force = value; } + } + + /// + /// If Debug is true, then the debug information will be emitted during + /// compilation. + /// + public bool Debug + { + get { return _debug; } + set { _debug = value; } + } + + /// + /// If Clean is true, then the application will be built clean. Previously + /// compiled components will be re-compiled. + /// + public bool Clean + { + get { return _clean; } + set { _clean = value; } + } + + /// + /// The TargetFrameworkMoniker indicating which .NET Framework version of + /// aspnet_compiler.exe should be used. Only accepts .NET Framework monikers. + /// + public string TargetFrameworkMoniker + { + get { return (string)Bag["TargetFrameworkMoniker"]; } + set { Bag["TargetFrameworkMoniker"] = value; } + } + + /// + /// The name of the tool to execute + /// + protected override string ToolName + { + get { return "aspnet_compiler.exe"; } + } + + /// + /// Small helper property to get the "project name" + /// + private string ProjectName + { + get + { + if (this.PhysicalPath != null) + { + return this.PhysicalPath; + } + else if (this.VirtualPath != null) + { + return this.VirtualPath; + } + + return this.MetabasePath; + } + } + + /// + /// Small helper property for determining the "name of the target" that's currently being built + /// + private string TargetName + { + get + { + if (this.Clean) + { + return "Clean"; + } + + // building the default target + return null; + } + } + + /// + /// Override the Execute method to be able to send ExternalProjectStarted/Finished events. + /// + /// + public override bool Execute() + { + Log.LogExternalProjectStarted(string.Empty, null, ProjectName, TargetName); + bool succeeded = false; + + try + { + succeeded = base.Execute(); + } + finally + { + Log.LogExternalProjectFinished(string.Empty, null, ProjectName, succeeded); + } + + return succeeded; + } + + /// + /// Generates command line arguments for aspnet_compiler.exe + /// + /// command line builder class to add arguments to + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendSwitchIfNotNull("-m ", MetabasePath); + commandLine.AppendSwitchIfNotNull("-v ", VirtualPath); + commandLine.AppendSwitchIfNotNull("-p ", PhysicalPath); + + if (Updateable) + commandLine.AppendSwitch("-u"); + + if (Force) + commandLine.AppendSwitch("-f"); + + if (Clean) + commandLine.AppendSwitch("-c"); + + if (Debug) + commandLine.AppendSwitch("-d"); + + if (FixedNames) + commandLine.AppendSwitch("-fixednames"); + + commandLine.AppendSwitchIfNotNull("", TargetPath); + + if (AllowPartiallyTrustedCallers) + commandLine.AppendSwitch("-aptca"); + + if (DelaySign) + commandLine.AppendSwitch("-delaysign"); + + commandLine.AppendSwitchIfNotNull("-keyfile ", KeyFile); + commandLine.AppendSwitchIfNotNull("-keycontainer ", KeyContainer); + } + + /// + /// Determine the path to aspnet_compiler.exe + /// + /// path to aspnet_compiler.exe, null if not found + protected override string GenerateFullPathToTool() + { + string pathToTool = null; + + // If ToolPath wasn't passed in, we want to default to the latest + pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest); + + if (pathToTool == null) + { + Log.LogErrorWithCodeFromResources("General.FrameworksFileNotFound", ToolName, + ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.VersionLatest)); + } + + return pathToTool; + } + + /// + /// Validate the task arguments, log any warnings/errors + /// + /// true if arguments are corrent enough to continue processing, false otherwise + protected override bool ValidateParameters() + { + if (MetabasePath != null && (VirtualPath != null || PhysicalPath != null)) + { + Log.LogErrorWithCodeFromResources("AspNetCompiler.CannotCombineMetabaseAndVirtualPathOrPhysicalPath"); + return false; + } + + if (MetabasePath == null && VirtualPath == null) + { + Log.LogErrorWithCodeFromResources("AspNetCompiler.MissingMetabasePathAndVirtualPath"); + return false; + } + + if (Updateable && TargetPath == null) + { + Log.LogErrorWithCodeFromResources("AspNetCompiler.MissingTargetPathForUpdatableApplication"); + return false; + } + + if (Force && TargetPath == null) + { + Log.LogErrorWithCodeFromResources("AspNetCompiler.MissingTargetPathForOverwrittenApplication"); + return false; + } + + return true; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyFoldersExResolver.cs b/src/XMakeTasks/AssemblyDependency/AssemblyFoldersExResolver.cs new file mode 100644 index 00000000000..6bc1d3b01e6 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyFoldersExResolver.cs @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using ProcessorArchitecture = System.Reflection.ProcessorArchitecture; +using System.Diagnostics; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {Registry: *} + /// + internal class AssemblyFoldersExResolver : Resolver + { + /// + /// Regex for breaking up the searchpath pieces. + /// + private static readonly Regex s_crackAssemblyFoldersExSentinel = new Regex + ( + AssemblyResolutionConstants.assemblyFoldersExSentinel + "(?[^,]*),(?[^,]*),(?[^,]*)([,]*)(?.*)}", + RegexOptions.IgnoreCase + ); + + /// + /// Delegate. + /// + private GetRegistrySubKeyNames _getRegistrySubKeyNames; + + /// + /// Delegate + /// + private GetRegistrySubKeyDefaultValue _getRegistrySubKeyDefaultValue; + + /// + /// Open the base registry key given a hive and a view + /// + private OpenBaseKey _openBaseKey; + + /// + /// Whether or not the search path could be cracked. + /// + private bool _wasMatch; + + /// + /// From the search path. + /// + private string _registryKeyRoot; + + /// + /// From the search path. + /// + private string _targetRuntimeVersion; + + /// + /// From the search path. + /// + private string _registryKeySuffix; + + /// + /// From the search path. + /// + private string _osVersion; + + /// + /// From the search path. + /// + private string _platform; + + /// + /// Whether regex initialization has happened. + /// + private bool _isInitialized; // is initialized to false automatically + + /// + /// List of assembly folders to search for keys in. + /// + private AssemblyFoldersExCache _assemblyFoldersCache; + + /// + /// BuildEngine + /// + private IBuildEngine4 _buildEngine; + + /// + /// If it is not initialized then just return the null object, that would mean the resolver was not called. + /// + internal AssemblyFoldersEx AssemblyFoldersExLocations + { + get + { + if (_assemblyFoldersCache == null) + { + return null; + } + + return _assemblyFoldersCache.AssemblyFoldersEx; + } + } + + /// + /// Construct. + /// + /// Initialize this class if it hasn't been initialized yet. + /// + private void LazyInitialize() + { + if (_isInitialized) + return; + + _isInitialized = true; + + // Crack the search path just one time. + Match match = s_crackAssemblyFoldersExSentinel.Match(this.searchPathElement); + _wasMatch = false; + + if (match.Success) + { + _registryKeyRoot = match.Groups["REGISTRYKEYROOT"].Value.Trim(); + _targetRuntimeVersion = match.Groups["TARGETRUNTIMEVERSION"].Value.Trim(); + _registryKeySuffix = match.Groups["REGISTRYKEYSUFFIX"].Value.Trim(); + _osVersion = null; + _platform = null; + Group conditions = match.Groups["CONDITIONS"]; + + // Disregard if there are any empty values in the {Registry} tag. + if (_registryKeyRoot.Length != 0 && _targetRuntimeVersion.Length != 0 && _registryKeySuffix.Length != 0) + { + // Tolerate version keys that don't begin with "v" as these could come from user input + if (!_targetRuntimeVersion.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + _targetRuntimeVersion = _targetRuntimeVersion.Insert(0, "v"); + } + + if (conditions != null && conditions.Value != null && conditions.Length > 0 && conditions.Value.Length > 0) + { + string value = conditions.Value.Trim(); + + // Parse the condition statement for OSVersion and Platform + foreach (string c in value.Split(':')) + { + if (String.Compare(c, 0, "OSVERSION=", 0, 10, StringComparison.OrdinalIgnoreCase) == 0) + { + _osVersion = c.Substring(10); + } + else if (String.Compare(c, 0, "PLATFORM=", 0, 9, StringComparison.OrdinalIgnoreCase) == 0) + { + _platform = c.Substring(9); + } + } + } + _wasMatch = true; + + bool useCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") == null; + string key = "ca22615d-aa83-444b-80b9-b32f3d5db097" + this.searchPathElement; + if (useCache && _buildEngine != null) + { + _assemblyFoldersCache = _buildEngine.GetRegisteredTaskObject(key, RegisteredTaskObjectLifetime.Build) as AssemblyFoldersExCache; + } + + if (_assemblyFoldersCache == null) + { + AssemblyFoldersEx assemblyFolders = new AssemblyFoldersEx(_registryKeyRoot, _targetRuntimeVersion, _registryKeySuffix, _osVersion, _platform, _getRegistrySubKeyNames, _getRegistrySubKeyDefaultValue, this.targetProcessorArchitecture, _openBaseKey); + _assemblyFoldersCache = new AssemblyFoldersExCache(assemblyFolders, fileExists); + if (useCache && _buildEngine != null) + { + _buildEngine.RegisterTaskObject(key, _assemblyFoldersCache, RegisteredTaskObjectLifetime.Build, true /* dispose early ok*/); + } + } + + fileExists = _assemblyFoldersCache.FileExists; + } + } + } + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (assemblyName != null) + { + LazyInitialize(); + + if (_wasMatch) + { + string resolvedPath = null; + if (_assemblyFoldersCache != null) + { + foreach (AssemblyFoldersExInfo assemblyFolder in _assemblyFoldersCache.AssemblyFoldersEx) + { + string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder.DirectoryPath, assembliesConsideredAndRejected); + + // We have a full path returned + if (candidatePath != null) + { + if (resolvedPath == null) + { + resolvedPath = candidatePath; + } + + // We are not targeting MSIL thus we must have a match because ResolveFromDirectory only will return a match if we find an assembly matching the targeted processor architecture + if (targetProcessorArchitecture != ProcessorArchitecture.MSIL && targetProcessorArchitecture != ProcessorArchitecture.None) + { + foundPath = candidatePath; + return true; + } + else + { + // Lets see if the processor architecture matches, note this this method will cache the result when it was first called. + AssemblyNameExtension foundAssembly = getAssemblyName(candidatePath); + + // If the processor architecture does not match the we should continue to see if there is a better match. + if (foundAssembly != null && (foundAssembly.AssemblyName.ProcessorArchitecture == ProcessorArchitecture.MSIL || foundAssembly.AssemblyName.ProcessorArchitecture == ProcessorArchitecture.None)) + { + foundPath = candidatePath; + return true; + } + } + } + } + } + + // If we get to this point and have not returned then we have the best assembly we could find, lets return it. + if (resolvedPath != null) + { + foundPath = resolvedPath; + return true; + } + } + } + + + return false; + } + } + + /// + /// Contains information about entries in the AssemblyFoldersEx registry keys. + /// + internal class AssemblyFoldersExCache + { + /// + /// Directory list of folders under assemblyfoldersex + /// + private AssemblyFoldersEx _assemblyFoldersEx; + + /// + /// Set of files in ALL assemblyfoldersEx directories + /// + private HashSet _filesInDirectories = new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// File exists delegate we are replacing + /// + private FileExists _fileExists; + + /// + /// Should we use the original on or use our own + /// + private bool _useOriginalFileExists = false; + + /// + /// Constructor + /// + internal AssemblyFoldersExCache(AssemblyFoldersEx assemblyFoldersEx, FileExists fileExists) + { + _assemblyFoldersEx = assemblyFoldersEx; + _fileExists = fileExists; + + if (Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") != null) + { + _useOriginalFileExists = true; + } + else + { + Object lockobject = new Object(); + + Parallel.ForEach(assemblyFoldersEx, assemblyFolder => + { + if (FileUtilities.DirectoryExistsNoThrow(assemblyFolder.DirectoryPath)) + { + string[] files = Directory.GetFiles(assemblyFolder.DirectoryPath, "*.*", SearchOption.TopDirectoryOnly); + + lock (lockobject) + { + foreach (string file in files) + { + _filesInDirectories.Add(file); + } + } + } + }); + } + } + + /// + /// AssemblyfoldersEx object which contains the set of directories in assmblyfoldersex + /// + internal AssemblyFoldersEx AssemblyFoldersEx + { + get { return _assemblyFoldersEx; } + } + + /// + /// Fast file exists for assemblyfoldersex. + /// + internal bool FileExists(string path) + { + // Make sure that the file is in one of the directories under the assembly folders ex location + // if it is not then we can not use this fast file existence check + if (!_useOriginalFileExists) + { + bool exists = _filesInDirectories.Contains(path); + return exists; + } + else + { + return _fileExists(path); + } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyFoldersResolver.cs b/src/XMakeTasks/AssemblyDependency/AssemblyFoldersResolver.cs new file mode 100644 index 00000000000..a455c09e104 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyFoldersResolver.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {AssemblyFolders} + /// + internal class AssemblyFoldersResolver : Resolver + { + /// + /// Construct. + /// + /// + /// + /// + public AssemblyFoldersResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false) + { + } + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (assemblyName != null) + { + // {AssemblyFolders} was passed in. + ICollection assemblyFolders = AssemblyFolder.GetAssemblyFolders(assemblyFolderKey); + + if (assemblyFolders != null) + { + foreach (string assemblyFolder in assemblyFolders) + { + string resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder, assembliesConsideredAndRejected); + if (resolvedPath != null) + { + foundPath = resolvedPath; + return true; + } + } + } + } + + return false; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyInformation.cs b/src/XMakeTasks/AssemblyDependency/AssemblyInformation.cs new file mode 100644 index 00000000000..3e0480324cf --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyInformation.cs @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Text; +using System.Runtime.Versioning; + +namespace Microsoft.Build.Tasks +{ + /// + /// Collection of methods used to discover assembly metadata. + /// Primarily stolen from manifestutility.cs AssemblyMetaDataImport class. + /// + internal class AssemblyInformation : DisposableBase + { + private AssemblyNameExtension[] _assemblyDependencies = null; + private string[] _assemblyFiles = null; + private IMetaDataDispenser _metadataDispenser = null; + private IMetaDataAssemblyImport _assemblyImport = null; + private static Guid s_importerGuid = new Guid(((GuidAttribute)Attribute.GetCustomAttribute(typeof(IMetaDataImport), typeof(GuidAttribute), false)).Value); + private string _sourceFile; + private FrameworkName _frameworkName; + private static string s_targetFrameworkAttribute = "System.Runtime.Versioning.TargetFrameworkAttribute"; + + // Borrowed from genman. + private const int GENMAN_STRING_BUF_SIZE = 1024; + private const int GENMAN_LOCALE_BUF_SIZE = 64; + private const int GENMAN_ENUM_TOKEN_BUF_SIZE = 16; // 128 from genman seems too big. + + /// + /// Construct an instance for a source file. + /// + /// The assembly. + internal AssemblyInformation(string sourceFile) + { + // Extra checks for PInvoke-destined data. + ErrorUtilities.VerifyThrowArgumentNull(sourceFile, "sourceFile"); + _sourceFile = sourceFile; + + // Create the metadata dispenser and open scope on the source file. + _metadataDispenser = (IMetaDataDispenser)new CorMetaDataDispenser(); + _assemblyImport = (IMetaDataAssemblyImport)_metadataDispenser.OpenScope(sourceFile, 0, ref s_importerGuid); + } + + /// + /// Get the dependencies. + /// + /// + public AssemblyNameExtension[] Dependencies + { + get + { + if (_assemblyDependencies == null) + { + lock (this) + { + if (_assemblyDependencies == null) + { + _assemblyDependencies = ImportAssemblyDependencies(); + } + } + } + + return _assemblyDependencies; + } + } + + /// + /// Get the scatter files from the assembly metadata. + /// + /// + public string[] Files + { + get + { + if (_assemblyFiles == null) + { + lock (this) + { + if (_assemblyFiles == null) + { + _assemblyFiles = ImportFiles(); + } + } + } + + return _assemblyFiles; + } + } + + /// + /// What was the framework name that the assembly was built against. + /// + public FrameworkName FrameworkNameAttribute + { + get + { + if (_frameworkName == null) + { + lock (this) + { + if (_frameworkName == null) + { + _frameworkName = GetFrameworkName(); + } + } + } + + return _frameworkName; + } + } + + /// + /// Given an assembly name, crack it open and retrieve the list of dependent + /// assemblies and the list of scatter files. + /// + /// Path to the assembly. + /// Receives the list of dependencies. + /// Receives the list of associated scatter files. + internal static void GetAssemblyMetadata + ( + string path, + out AssemblyNameExtension[] dependencies, + out string[] scatterFiles, + out FrameworkName frameworkName + ) + { + AssemblyInformation import = null; + using (import = new AssemblyInformation(path)) + { + dependencies = import.Dependencies; + scatterFiles = import.Files; + frameworkName = import.FrameworkNameAttribute; + } + } + + /// + /// Given an assembly name, crack it open and retrieve the TargetFrameworkAttribute + /// assemblies and the list of scatter files. + /// + internal static FrameworkName GetTargetFrameworkAttribute(string path) + { + using (AssemblyInformation import = new AssemblyInformation(path)) + { + return import.FrameworkNameAttribute; + } + } + + /// + /// Determine if an file is a winmd file or not. + /// + internal static bool IsWinMDFile(string fullPath, GetAssemblyRuntimeVersion getAssemblyRuntimeVersion, FileExists fileExists, out string imageRuntimeVersion, out bool isManagedWinmd) + { + imageRuntimeVersion = String.Empty; + isManagedWinmd = false; + + // May be null or empty is the file was never resolved to a path on disk. + if (!String.IsNullOrEmpty(fullPath) && fileExists(fullPath)) + { + imageRuntimeVersion = getAssemblyRuntimeVersion(fullPath); + if (!String.IsNullOrEmpty(imageRuntimeVersion)) + { + bool containsWindowsRuntime = imageRuntimeVersion.IndexOf("WindowsRuntime", StringComparison.OrdinalIgnoreCase) >= 0; + + if (containsWindowsRuntime) + { + isManagedWinmd = imageRuntimeVersion.IndexOf("CLR", StringComparison.OrdinalIgnoreCase) >= 0; + return true; + } + } + } + + return false; + } + + /// + /// Get the framework name from the assembly. + /// + private FrameworkName GetFrameworkName() + { + FrameworkName frameworkAttribute = null; + try + { + IMetaDataImport2 import2 = (IMetaDataImport2)_assemblyImport; + IntPtr data = IntPtr.Zero; + UInt32 valueLen = 0; + string frameworkNameAttribute = null; + UInt32 assemblyScope; + + _assemblyImport.GetAssemblyFromScope(out assemblyScope); + int hr = import2.GetCustomAttributeByName(assemblyScope, s_targetFrameworkAttribute, out data, out valueLen); + + // get the AssemblyTitle + if (hr == NativeMethodsShared.S_OK) + { + // if an AssemblyTitle exists, parse the contents of the blob + if (NativeMethods.TryReadMetadataString(_sourceFile, data, valueLen, out frameworkNameAttribute)) + { + if (!String.IsNullOrEmpty(frameworkNameAttribute)) + { + frameworkAttribute = new FrameworkName(frameworkNameAttribute); + } + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + } + + return frameworkAttribute; + } + + /// + /// Release interface pointers on Dispose(). + /// + protected override void DisposeUnmanagedResources() + { + if (_assemblyImport != null) + Marshal.ReleaseComObject(_assemblyImport); + if (_metadataDispenser != null) + Marshal.ReleaseComObject(_metadataDispenser); + } + + /// + /// Given a path get the CLR runtime version of the file + /// + /// path to the file + /// The CLR runtime version or empty if the path does not exist. + internal static string GetRuntimeVersion(string path) + { + StringBuilder runtimeVersion = null; + uint hresult = 0; + uint actualBufferSize = 0; +#if _DEBUG + // Just to make sure and exercise the code that doubles the size + // every time GetRequestedRuntimeInfo fails due to insufficient buffer size. + int bufferLength = 1; +#else + int bufferLength = 11; // 11 is the length of a runtime version and null terminator v2.0.50727/0 +#endif + do + { + runtimeVersion = new StringBuilder(bufferLength); + hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out actualBufferSize); + bufferLength = bufferLength * 2; + } while (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER); + + if (hresult == NativeMethodsShared.S_OK && runtimeVersion != null) + { + return runtimeVersion.ToString(); + } + else + { + return String.Empty; + } + } + + + /// + /// Import assembly dependencies. + /// + /// The array of assembly dependencies. + private AssemblyNameExtension[] ImportAssemblyDependencies() + { + ArrayList asmRefs = new ArrayList(); + IntPtr asmRefEnum = IntPtr.Zero; + UInt32[] asmRefTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE]; + UInt32 fetched; + // Ensure the enum handle is closed. + try + { + // Enum chunks of refs in 16-ref blocks until we run out. + do + { + _assemblyImport.EnumAssemblyRefs(ref asmRefEnum, asmRefTokens, (uint)asmRefTokens.Length, out fetched); + + for (uint i = 0; i < fetched; i++) + { + // Determine the length of the string to contain the name first. + IntPtr hashDataPtr, pubKeyPtr; + UInt32 hashDataLength, pubKeyBytes, asmNameLength, flags; + _assemblyImport.GetAssemblyRefProps(asmRefTokens[i], out pubKeyPtr, out pubKeyBytes, null, 0, out asmNameLength, IntPtr.Zero, out hashDataPtr, out hashDataLength, out flags); + // Allocate assembly name buffer. + char[] asmNameBuf = new char[asmNameLength + 1]; + IntPtr asmMetaPtr = IntPtr.Zero; + // Ensure metadata structure is freed. + try + { + // Allocate metadata structure. + asmMetaPtr = AllocAsmMeta(); + // Retrieve the assembly reference properties. + _assemblyImport.GetAssemblyRefProps(asmRefTokens[i], out pubKeyPtr, out pubKeyBytes, asmNameBuf, (uint)asmNameBuf.Length, out asmNameLength, asmMetaPtr, out hashDataPtr, out hashDataLength, out flags); + // Construct the assembly name and free metadata structure. + AssemblyNameExtension asmName = ConstructAssemblyName(asmMetaPtr, asmNameBuf, asmNameLength, pubKeyPtr, pubKeyBytes, flags); + // Add the assembly name to the reference list. + asmRefs.Add(asmName); + } + finally + { + FreeAsmMeta(asmMetaPtr); + } + } + } while (fetched > 0); + } + finally + { + if (asmRefEnum != IntPtr.Zero) + _assemblyImport.CloseEnum(asmRefEnum); + } + + return (AssemblyNameExtension[])asmRefs.ToArray(typeof(AssemblyNameExtension)); + } + + /// + /// Import extra files. These are usually consituent members of a scatter assembly. + /// + /// The extra files of assembly dependencies. + private string[] ImportFiles() + { + ArrayList files = new ArrayList(); + IntPtr fileEnum = IntPtr.Zero; + UInt32[] fileTokens = new UInt32[GENMAN_ENUM_TOKEN_BUF_SIZE]; + char[] fileNameBuf = new char[GENMAN_STRING_BUF_SIZE]; + UInt32 fetched; + + // Ensure the enum handle is closed. + try + { + // Enum chunks of files until we run out. + do + { + _assemblyImport.EnumFiles(ref fileEnum, fileTokens, (uint)fileTokens.Length, out fetched); + + for (uint i = 0; i < fetched; i++) + { + IntPtr hashDataPtr; + UInt32 fileNameLength, hashDataLength, fileFlags; + + // Retrieve file properties. + _assemblyImport.GetFileProps(fileTokens[i], + fileNameBuf, (uint)fileNameBuf.Length, out fileNameLength, + out hashDataPtr, out hashDataLength, out fileFlags); + + // Add file to file list. + string file = new string(fileNameBuf, 0, (int)(fileNameLength - 1)); + files.Add(file); + } + } while (fetched > 0); + } + finally + { + if (fileEnum != IntPtr.Zero) + _assemblyImport.CloseEnum(fileEnum); + } + + return (string[])files.ToArray(typeof(string)); + } + + /// + /// Allocate assembly metadata structure buffer. + /// + /// Pointer to structure + private IntPtr AllocAsmMeta() + { + ASSEMBLYMETADATA asmMeta; + asmMeta.usMajorVersion = asmMeta.usMinorVersion = asmMeta.usBuildNumber = asmMeta.usRevisionNumber = 0; + asmMeta.cOses = asmMeta.cProcessors = 0; + asmMeta.rOses = asmMeta.rpProcessors = IntPtr.Zero; + // Allocate buffer for locale. + asmMeta.rpLocale = Marshal.AllocCoTaskMem(GENMAN_LOCALE_BUF_SIZE * 2); + asmMeta.cchLocale = (uint)GENMAN_LOCALE_BUF_SIZE; + // Convert to unmanaged structure. + int size = Marshal.SizeOf(typeof(ASSEMBLYMETADATA)); + IntPtr asmMetaPtr = Marshal.AllocCoTaskMem(size); + Marshal.StructureToPtr(asmMeta, asmMetaPtr, false); + + return asmMetaPtr; + } + + /// + /// Construct assembly name. + /// + /// Assembly metadata structure + /// Buffer containing the name + /// Length of that buffer + /// Pointer to public key + /// Count of bytes in public key. + /// Extra flags + /// The assembly name. + private AssemblyNameExtension ConstructAssemblyName(IntPtr asmMetaPtr, char[] asmNameBuf, UInt32 asmNameLength, IntPtr pubKeyPtr, UInt32 pubKeyBytes, UInt32 flags) + { + // Marshal the assembly metadata back to a managed type. + ASSEMBLYMETADATA asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); + + // Construct the assembly name. (Note asmNameLength should/must be > 0.) + AssemblyName assemblyName = new AssemblyName(); + assemblyName.Name = new string(asmNameBuf, 0, (int)asmNameLength - 1); + assemblyName.Version = new Version(asmMeta.usMajorVersion, asmMeta.usMinorVersion, asmMeta.usBuildNumber, asmMeta.usRevisionNumber); + + + // Set culture info. + string locale = Marshal.PtrToStringUni(asmMeta.rpLocale); + if (locale.Length > 0) + { + assemblyName.CultureInfo = CultureInfo.CreateSpecificCulture(locale); + } + else + { + assemblyName.CultureInfo = CultureInfo.CreateSpecificCulture(String.Empty); + } + + + // Set public key or PKT. + byte[] publicKey = new byte[pubKeyBytes]; + Marshal.Copy(pubKeyPtr, publicKey, 0, (int)pubKeyBytes); + if ((flags & (uint)CorAssemblyFlags.afPublicKey) != 0) + { + assemblyName.SetPublicKey(publicKey); + } + else + { + assemblyName.SetPublicKeyToken(publicKey); + } + + assemblyName.Flags = (AssemblyNameFlags)flags; + return new AssemblyNameExtension(assemblyName); + } + + /// + /// Free the assembly metadata structure. + /// + /// The pointer. + private void FreeAsmMeta(IntPtr asmMetaPtr) + { + if (asmMetaPtr != IntPtr.Zero) + { + // Marshal the assembly metadata back to a managed type. + ASSEMBLYMETADATA asmMeta = (ASSEMBLYMETADATA)Marshal.PtrToStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); + // Free unmanaged memory. + Marshal.FreeCoTaskMem(asmMeta.rpLocale); + Marshal.DestroyStructure(asmMetaPtr, typeof(ASSEMBLYMETADATA)); + Marshal.FreeCoTaskMem(asmMetaPtr); + } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyNameReference.cs b/src/XMakeTasks/AssemblyDependency/AssemblyNameReference.cs new file mode 100644 index 00000000000..103fc26b050 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyNameReference.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// An assembly name coupled with reference information. + /// + internal struct AssemblyNameReference : IComparable + { + internal AssemblyNameExtension assemblyName; + internal Reference reference; + + /// + /// Display as string. + /// + public override string ToString() + { + return assemblyName + ", " + reference; + } + + /// + /// Compare by assembly name. + /// + /// + /// + public int CompareTo(AssemblyNameReference other) + { + return assemblyName.CompareTo(other.assemblyName); + } + + /// + /// Construct a new AssemblyNameReference. + /// + /// + /// + /// + public static AssemblyNameReference Create(AssemblyNameExtension assemblyName, Reference reference) + { + AssemblyNameReference result; + result.assemblyName = assemblyName; + result.reference = reference; + return result; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyNameReferenceAscendingVersionComparer.cs b/src/XMakeTasks/AssemblyDependency/AssemblyNameReferenceAscendingVersionComparer.cs new file mode 100644 index 00000000000..6384a83759e --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyNameReferenceAscendingVersionComparer.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; + +namespace Microsoft.Build.Tasks +{ + /// + /// Compare the two AssemblyNameReferences by version number. + /// + sealed internal class AssemblyNameReferenceAscendingVersionComparer : IComparer + { + internal readonly static IComparer comparer = new AssemblyNameReferenceAscendingVersionComparer(); + + /// + /// Private construct so there's only one instance. + /// + private AssemblyNameReferenceAscendingVersionComparer() + { + } + + /// + /// Compare the two AssemblyNameReferences by version number. + /// + /// + /// + /// + public int Compare(object o1, object o2) + { + AssemblyNameReference i1 = (AssemblyNameReference)o1; + AssemblyNameReference i2 = (AssemblyNameReference)o2; + + Version v1 = i1.assemblyName.Version; + Version v2 = i2.assemblyName.Version; + + if (v1 == null) + { + v1 = new Version(); + } + + if (v2 == null) + { + v2 = new Version(); + } + + return v1.CompareTo(v2); + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyResolution.cs b/src/XMakeTasks/AssemblyDependency/AssemblyResolution.cs new file mode 100644 index 00000000000..3978c0183d2 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyResolution.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Reflection; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Tasks +{ + /// + /// Utility class encapsulates steps to resolve assembly references. + /// For example, this class has the code that will take: + /// + /// System.Xml + /// + /// and turn it into: + /// + /// [path-to-frameworks]\System.Xml.dll + /// + /// + /// + internal static class AssemblyResolution + { + /// + /// Implementation guts for ResolveReference. + /// + /// The array of resolvers to search with. + /// The assembly name to look up. + /// The file name to match if {RawFileName} is seen. (May be null). + /// True if this is a primary reference directly from the project file. + /// The filename extension of the assembly. Must be this or its no match. + /// This reference's hintpath + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// Receives the searchPath that the reference was resolved at. Empty if not resolved. + /// This will be true if the user requested a specific file. + /// The resolved path + internal static string ResolveReference + ( + IEnumerable jaggedResolvers, + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + out string resolvedSearchPath, + out bool userRequestedSpecificFile + ) + { + // Initialize outs. + userRequestedSpecificFile = false; + resolvedSearchPath = String.Empty; + + // Search each group of resolvers + foreach (Resolver[] resolvers in jaggedResolvers) + { + // Tolerate null resolvers. + if (resolvers == null) + { + break; + } + + // Search each searchpath. + foreach (Resolver resolver in resolvers) + { + string fileLocation; + if + ( + resolver.Resolve + ( + assemblyName, + sdkName, + rawFileNameCandidate, + isPrimaryProjectReference, + wantSpecificVersion, + executableExtensions, + hintPath, + assemblyFolderKey, + assembliesConsideredAndRejected, + out fileLocation, + out userRequestedSpecificFile + ) + ) + { + resolvedSearchPath = resolver.SearchPath; + return fileLocation; + } + } + } + + return null; + } + + /// + /// Compile search paths into an array of resolvers. + /// + /// + /// Paths to assembly files mentioned in the project. + /// Like x86 or IA64\AMD64, the processor architecture being targetted. + /// Paths to FX folders. + /// + /// + /// + /// + /// + /// + public static Resolver[] CompileSearchPaths + ( + IBuildEngine buildEngine, + string[] searchPaths, + string[] candidateAssemblyFiles, + System.Reflection.ProcessorArchitecture targetProcessorArchitecture, + string[] frameworkPaths, + FileExists fileExists, + GetAssemblyName getAssemblyName, + GetRegistrySubKeyNames getRegistrySubKeyNames, + GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, + OpenBaseKey openBaseKey, + InstalledAssemblies installedAssemblies, + GetAssemblyRuntimeVersion getRuntimeVersion, + Version targetedRuntimeVersion + ) + { + Resolver[] resolvers = new Resolver[searchPaths.Length]; + + for (int p = 0; p < searchPaths.Length; ++p) + { + string basePath = searchPaths[p]; + + // Was {HintPathFromItem} specified? If so, take the Item's + // HintPath property. + if (0 == String.Compare(basePath, AssemblyResolutionConstants.hintPathSentinel, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new HintPathResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + } + else if (0 == String.Compare(basePath, AssemblyResolutionConstants.frameworkPathSentinel, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new FrameworkPathResolver(frameworkPaths, installedAssemblies, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + } + else if (0 == String.Compare(basePath, AssemblyResolutionConstants.rawFileNameSentinel, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new RawFilenameResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + } + else if (0 == String.Compare(basePath, AssemblyResolutionConstants.candidateAssemblyFilesSentinel, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new CandidateAssemblyFilesResolver(candidateAssemblyFiles, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + } + else if (0 == String.Compare(basePath, AssemblyResolutionConstants.gacSentinel, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new GacResolver(targetProcessorArchitecture, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, buildEngine); + } + else if (0 == String.Compare(basePath, AssemblyResolutionConstants.assemblyFoldersSentinel, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new AssemblyFoldersResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + } + // Check for AssemblyFoldersEx sentinel. + else if (0 == String.Compare(basePath, 0, AssemblyResolutionConstants.assemblyFoldersExSentinel, 0, AssemblyResolutionConstants.assemblyFoldersExSentinel.Length, StringComparison.OrdinalIgnoreCase)) + { + resolvers[p] = new AssemblyFoldersExResolver(searchPaths[p], getAssemblyName, fileExists, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getRuntimeVersion, openBaseKey, targetedRuntimeVersion, targetProcessorArchitecture, true, buildEngine); + } + else + { + resolvers[p] = new DirectoryResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + } + } + return resolvers; + } + + /// + /// Build a resolver array from a set of directories to resolve directly from. + /// + /// + /// + /// + /// + internal static Resolver[] CompileDirectories + ( + IEnumerable directories, + FileExists fileExists, + GetAssemblyName getAssemblyName, + GetAssemblyRuntimeVersion getRuntimeVersion, + Version targetedRuntimeVersion + ) + { + List resolvers = new List(); + foreach (string directory in directories) + { + resolvers.Add(new DirectoryResolver(directory, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion)); + } + return resolvers.ToArray(); + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/AssemblyResolutionConstants.cs b/src/XMakeTasks/AssemblyDependency/AssemblyResolutionConstants.cs new file mode 100644 index 00000000000..3a349782ad4 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/AssemblyResolutionConstants.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Reflection; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Tasks +{ + /// + /// Constants used for assembly resolution. + /// + internal static class AssemblyResolutionConstants + { + /// + /// Special hintpath indicator. May be passed in where SearchPaths are taken. + /// + public const string hintPathSentinel = "{hintpathfromitem}"; + + /// + /// Special AssemblyFolders indicator. May be passed in where SearchPaths are taken. + /// + public const string assemblyFoldersSentinel = "{assemblyfolders}"; + + /// + /// Special CandidateAssemblyFiles indicator. May be passed in where SearchPaths are taken. + /// + public const string candidateAssemblyFilesSentinel = "{candidateassemblyfiles}"; + + /// + /// Special GAC indicator. May be passed in where SearchPaths are taken. + /// + public const string gacSentinel = "{gac}"; + + /// + /// Special Framework directory indicator. May be passed in where SearchPaths are taken. + /// + public const string frameworkPathSentinel = "{targetframeworkdirectory}"; + + /// + /// Special SearchPath indicator that means: match against the assembly item's Include as + /// if it were a file. + /// + public const string rawFileNameSentinel = "{rawfilename}"; + + /// + /// Special AssemblyFoldersEx indicator. May be passed in where SearchPaths are taken. + /// + public const string assemblyFoldersExSentinel = "{registry:"; + } +} diff --git a/src/XMakeTasks/AssemblyDependency/BadImageReferenceException.cs b/src/XMakeTasks/AssemblyDependency/BadImageReferenceException.cs new file mode 100644 index 00000000000..4e8ea9a177a --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/BadImageReferenceException.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// The reference points to a bad image. + /// + [Serializable] + internal sealed class BadImageReferenceException : Exception + { + /// + /// Don't allow default construction. + /// + private BadImageReferenceException() + { + } + + /// + /// Construct + /// + internal BadImageReferenceException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Construct + /// + private BadImageReferenceException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs b/src/XMakeTasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs new file mode 100644 index 00000000000..d014f979796 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; +using System.Diagnostics; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {CandidateAssemblyFiles} + /// + internal class CandidateAssemblyFilesResolver : Resolver + { + /// + /// The candidate assembly files. + /// + private string[] _candidateAssemblyFiles; + + /// + /// Construct. + /// + /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. + /// + /// + /// + public CandidateAssemblyFilesResolver(string[] candidateAssemblyFiles, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false) + { + _candidateAssemblyFiles = candidateAssemblyFiles; + } + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (assemblyName != null) + { + // {CandidateAssemblyFiles} was passed in. + foreach (string candidateAssemblyFile in _candidateAssemblyFiles) + { + // Filter out disallowed extensions. We don't even want to log them. + bool allowedExtension = FileUtilities.HasExtension(candidateAssemblyFile, executableExtensions); + if (allowedExtension) + { + // The file has an allowed extension, so give it a shot. + bool matched = false; + + ResolutionSearchLocation considered = null; + if (assembliesConsideredAndRejected != null) + { + considered = new ResolutionSearchLocation(); + considered.FileNameAttempted = candidateAssemblyFile; + considered.SearchPath = searchPathElement; + } + + if (FileMatchesAssemblyName(assemblyName, isPrimaryProjectReference, wantSpecificVersion, false, candidateAssemblyFile, considered)) + { + matched = true; + } + else + { + // Record this as a location that was considered. + if (assembliesConsideredAndRejected != null) + { + Debug.Assert(considered.Reason != NoMatchReason.Unknown, "Expected a no match reason here."); + assembliesConsideredAndRejected.Add(considered); + } + } + + if (matched) + { + foundPath = candidateAssemblyFile; + return true; + } + } + } + } + + + + return false; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/ConflictLossReason.cs b/src/XMakeTasks/AssemblyDependency/ConflictLossReason.cs new file mode 100644 index 00000000000..c84d211c0ca --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/ConflictLossReason.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks +{ + /// + /// If this reference lost in a conflict with another reference, this reason explains + /// why. + /// + internal enum ConflictLossReason + { + /// + /// This reference didn't lose a conflict. + /// + DidntLose, + + /// + /// This reference matched another assembly that had a higher version number. + /// + HadLowerVersion, + + /// + /// The two assemblies cannot be reconciled. + /// + InsolubleConflict, + + /// + /// In this case, this reference was a dependency and the other reference was + /// primary (specified in the project file). + /// + WasNotPrimary, + + /// + /// The two references were equivalent according to fusion and also have the same version. + /// Its hard to see how this could happen, but handle it. + /// + FusionEquivalentWithSameVersion + } +} diff --git a/src/XMakeTasks/AssemblyDependency/CopyLocalState.cs b/src/XMakeTasks/AssemblyDependency/CopyLocalState.cs new file mode 100644 index 00000000000..ece2762f8de --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/CopyLocalState.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks +{ + /// + /// The value of the copyLocal flag and why it was set that way. + /// + internal enum CopyLocalState + { + /// + /// The copy local state is undecided right now. + /// + Undecided, + + /// + /// The Reference should be CopyLocal='true' because it wasn't 'no' for any reason. + /// + YesBecauseOfHeuristic, + + /// + /// The Reference should be CopyLocal='true' because its source item has Private='true' + /// + YesBecauseReferenceItemHadMetadata, + + /// + /// The Reference should be CopyLocal='false' because it is a framework file. + /// + NoBecauseFrameworkFile, + + /// + /// The Reference should be CopyLocal='false' because it is a prerequisite file. + /// + NoBecausePrerequisite, + + /// + /// The Reference should be CopyLocal='false' because the the Private attribute is set to 'false' in the project. + /// + NoBecauseReferenceItemHadMetadata, + + /// + /// The Reference should be CopyLocal='false' because it's found in the GAC. + /// + NoBecauseReferenceFoundInGAC, + + /// + /// The Reference should be CopyLocal='false' because it lost a conflict between an same-named assembly file. + /// + NoBecauseConflictVictim, + + /// + /// The reference was unresolved. It can't be copied to the bin directory because it wasn't found. + /// + NoBecauseUnresolved, + + /// + /// The reference was embedded. It shouldn't be copied to the bin directory because it won't be loaded at runtime. + /// + NoBecauseEmbedded, + + /// + /// The property copyLocalDependenciesWhenParentReferenceInGac is set to false and all the parent source items were found in the GAC. + /// + NoBecauseParentReferencesFoundInGAC, + } + + /// + /// Helper methods for dealing with CopyLocalState enumeration. + /// + internal static class CopyLocalStateUtility + { + /// + /// Returns the true or false from a CopyLocalState. + /// + /// + /// + internal static bool IsCopyLocal(CopyLocalState state) + { + switch (state) + { + case CopyLocalState.YesBecauseOfHeuristic: + case CopyLocalState.YesBecauseReferenceItemHadMetadata: + return true; + case CopyLocalState.NoBecauseConflictVictim: + case CopyLocalState.NoBecauseUnresolved: + case CopyLocalState.NoBecauseFrameworkFile: + case CopyLocalState.NoBecausePrerequisite: + case CopyLocalState.NoBecauseReferenceItemHadMetadata: + case CopyLocalState.NoBecauseReferenceFoundInGAC: + case CopyLocalState.NoBecauseEmbedded: + case CopyLocalState.NoBecauseParentReferencesFoundInGAC: + return false; + default: + throw new InternalErrorException("Unexpected CopyLocal flag."); + // Used to be: + // + // ErrorUtilities.VerifyThrow(false, "Unexpected CopyLocal flag."); + // + // but this popped up constantly when debugging because its call + // directly by a property accessor in Reference. + } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/DependencyResolutionException.cs b/src/XMakeTasks/AssemblyDependency/DependencyResolutionException.cs new file mode 100644 index 00000000000..9968c033c0e --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/DependencyResolutionException.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Exception indicates a problem finding dependencies of a reference. + /// + [Serializable] + internal sealed class DependencyResolutionException : Exception + { + /// + /// Don't allow default construction. + /// + private DependencyResolutionException() + { + } + + /// + /// Construct + /// + internal DependencyResolutionException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Construct + /// + private DependencyResolutionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/DirectoryResolver.cs b/src/XMakeTasks/AssemblyDependency/DirectoryResolver.cs new file mode 100644 index 00000000000..27a0e7e0fca --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/DirectoryResolver.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve when the searchpath type is a simple directory name. + /// + internal class DirectoryResolver : Resolver + { + /// + /// Construct. + /// + /// + /// + /// + public DirectoryResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false) + { + } + + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + // Resolve to the given path. + string resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, searchPathElement, assembliesConsideredAndRejected); + if (resolvedPath != null) + { + foundPath = resolvedPath; + return true; + } + + + return false; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/DisposableBase.cs b/src/XMakeTasks/AssemblyDependency/DisposableBase.cs new file mode 100644 index 00000000000..b9f6b60b952 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/DisposableBase.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Tasks +{ + // + // Abstract base class to implement IDisposable interface. + // + internal abstract class DisposableBase : System.IDisposable + { + private bool _disposed; + + // Constructor + public DisposableBase() + { + _disposed = false; + } + + // Use C# destructor syntax for finalization code. + // This destructor will run only if the Dispose method + // does not get called. + // It gives your base class the opportunity to finalize. + // Do not provide destructors in types derived from this class. + ~DisposableBase() + { + // Do not re-create Dispose clean-up code here. + // Calling Dispose(false) is optimal in terms of + // readability and maintainability. + Dispose(false); + } + + // Implement IDisposable. + // Do not make this method virtual. + // A derived class should not be able to override this method. + public void Dispose() + { + // This object will be cleaned up by the Dispose method. + // Therefore, you should call GC.SupressFinalize to + // take this object off the finalization queue + // and prevent finalization code for this object + // from executing a second time. + Dispose(true); + GC.SuppressFinalize(this); + } + + // Dispose(bool disposing) executes in two distinct scenarios. + // If disposing equals true, the method has been called directly + // or indirectly by a user's code. Managed and unmanaged resources + // can be disposed. + // If disposing equals false, the method has been called by the + // runtime from inside the finalizer and you should not reference + // other objects. Only unmanaged resources can be disposed. + private void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (!_disposed) + { + // If disposing equals true, dispose all managed + // and unmanaged resources. + if (disposing) + { + // Dispose managed resources. + DisposeManagedResources(); + } + + // Call the appropriate methods to clean up + // unmanaged resources here. + // If disposing is false, + // only the following code is executed. + DisposeUnmanagedResources(); + } + + _disposed = true; + } + + // Can be overridden by derived classes to dispose managed resources. + protected virtual void DisposeManagedResources() + { + } + + // Can be overridden by derived classes to dispose unmanaged resources. + protected virtual void DisposeUnmanagedResources() + { + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/FrameworkPathResolver.cs b/src/XMakeTasks/AssemblyDependency/FrameworkPathResolver.cs new file mode 100644 index 00000000000..2cefcfe3817 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/FrameworkPathResolver.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {TargetFrameworkDirectory} + /// + internal class FrameworkPathResolver : Resolver + { + // Paths to FX folders. + private string[] _frameworkPaths; + + // Table of information about framework assemblies. + private InstalledAssemblies _installedAssemblies; + + /// + /// Construct. + /// + /// Paths to framework directories. + /// + /// + /// + /// + public FrameworkPathResolver(string[] frameworkPaths, InstalledAssemblies installedAssemblies, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false) + { + _frameworkPaths = frameworkPaths; + _installedAssemblies = installedAssemblies; + } + + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (assemblyName != null) + { + AssemblyNameExtension assemblyNameToUse = GetHighestVersionInRedist(_installedAssemblies, assemblyName); + + foreach (string frameworkPath in _frameworkPaths) + { + string resolvedPath = ResolveFromDirectory(assemblyNameToUse, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, frameworkPath, assembliesConsideredAndRejected); + + if (resolvedPath != null) + { + foundPath = resolvedPath; + return true; + } + } + } + + return false; + } + + /// + /// If the version is not set for an assembly reference, go through the redist list and find the highest version for that assembly. + /// Make sure when matching the assembly in the redist that we take into account the publicKeyToken and the Culture. + /// + internal static AssemblyNameExtension GetHighestVersionInRedist(InstalledAssemblies installedAssemblies, AssemblyNameExtension assemblyName) + { + AssemblyNameExtension assemblyNameToUse = assemblyName; + + if ((assemblyNameToUse.Version == null && installedAssemblies != null)) + { + // If there are multiple entries in the redist list for this assembly, let's + // pick the one with the highest version and resolve it. + + AssemblyEntry[] assemblyEntries = installedAssemblies.FindAssemblyNameFromSimpleName(assemblyName.Name); + + if (assemblyEntries.Length != 0) + { + for (int i = 0; i < assemblyEntries.Length; ++i) + { + AssemblyNameExtension current = new AssemblyNameExtension(assemblyEntries[i].FullName); + + // If the current version is higher than the previously looked at. + if (current.Version != null && current.Version.CompareTo(assemblyNameToUse.Version) > 0) + { + // Only compare the Culture and the public key token, the simple names will ALWAYS be the same and the version we do not care about. + if (assemblyName.PartialNameCompare(current, PartialComparisonFlags.Culture | PartialComparisonFlags.PublicKeyToken)) + { + assemblyNameToUse = current; + } + } + } + } + } + + return assemblyNameToUse; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/GacResolver.cs b/src/XMakeTasks/AssemblyDependency/GacResolver.cs new file mode 100644 index 00000000000..2c4c898c16c --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/GacResolver.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {GAC} + /// + internal class GacResolver : Resolver + { + /// + /// Build engine + /// + private IBuildEngine4 _buildEngine; + + /// + /// Construct. + /// + /// Like x86 or IA64\AMD64, the processor architecture being targetted. + public GacResolver(System.Reflection.ProcessorArchitecture targetProcessorArchitecture, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, IBuildEngine buildEngine) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, targetProcessorArchitecture, true) + { + _buildEngine = buildEngine as IBuildEngine4; + } + + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (assemblyName != null) + { + // {GAC} was passed in. + string gacResolved = GlobalAssemblyCache.GetLocation(_buildEngine, assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, false /*may not be a fusion name*/, fileExists, null /*Use default delegate in method*/, null /*Use default delegate in method*/, wantSpecificVersion); + if (gacResolved != null && gacResolved.Length > 0 && fileExists(gacResolved)) + { + foundPath = gacResolved; + return true; + } + else + { + // Record this as a location that was considered. + if (assembliesConsideredAndRejected != null) + { + ResolutionSearchLocation considered = new ResolutionSearchLocation(); + considered.FileNameAttempted = assemblyName.FullName; + considered.SearchPath = searchPathElement; + considered.AssemblyName = assemblyName; + considered.Reason = NoMatchReason.NotInGac; + assembliesConsideredAndRejected.Add(considered); + } + } + } + + + return false; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/GenerateBindingRedirects.cs b/src/XMakeTasks/AssemblyDependency/GenerateBindingRedirects.cs new file mode 100644 index 00000000000..12e292cb4b8 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/GenerateBindingRedirects.cs @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corp. All rights reserved. +// +//------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Diagnostics; +using System.Text; +using System.Reflection; + +namespace Microsoft.Build.Tasks +{ + /// + /// Take suggested redirects (from the ResolveAssemblyReference and GenerateOutOfBandAssemblyTables tasks) + /// and add them to an intermediate copy of the App.config file. + /// + public class GenerateBindingRedirects : TaskExtension + { + // RAR suggested binding redirects. + // The source App.Config file. + // The name of the target app config file: XXX.exe.config. + // The output App.Config file. + // True if there was success. + + /// + /// Sugested redirects as output from the ResolveAssemblyReference task. + /// + public ITaskItem[] SuggestedRedirects + { + get; + set; + } + + /// + /// Path to the app.config source file. + /// + public ITaskItem AppConfigFile + { + get; + set; + } + + /// + /// Name of the output application config file: $(TargetFileName).config + /// + public string TargetName + { + get; + set; + } + + /// + /// Path to an intermediate file where we can write the input app.config plus the generated binding redirects. + /// + [Output] + public ITaskItem OutputAppConfigFile + { + get; + set; + } + + /// + /// Execute the task. + /// + public override bool Execute() + { + if (SuggestedRedirects == null || SuggestedRedirects.Length == 0) + { + Log.LogMessageFromResources("GenerateBindingRedirects.NoSuggestedRedirects"); + OutputAppConfigFile = null; + return true; + } + + var redirects = ParseSuggestedRedirects(); + + var doc = LoadAppConfig(AppConfigFile); + + if (doc == null) + { + return false; + } + + var runtimeNode = doc.Root.Nodes() + .OfType() + .Where(e => e.Name.LocalName == "runtime") + .FirstOrDefault(); + + if (runtimeNode == null) + { + runtimeNode = new XElement("runtime"); + doc.Root.Add(runtimeNode); + } + else + { + UpdateExistingBindingRedirects(runtimeNode, redirects); + } + + var ns = XNamespace.Get("urn:schemas-microsoft-com:asm.v1"); + + var redirectNodes = from redirect in redirects + select new XElement( + ns + "assemblyBinding", + new XElement( + ns + "dependentAssembly", + new XElement( + ns + "assemblyIdentity", + new XAttribute("name", redirect.Key.Name), + new XAttribute("publicKeyToken", ResolveAssemblyReference.ByteArrayToString(redirect.Key.GetPublicKeyToken())), + new XAttribute("culture", String.IsNullOrEmpty(redirect.Key.CultureName) ? "neutral" : redirect.Key.CultureName)), + new XElement( + ns + "bindingRedirect", + new XAttribute("oldVersion", "0.0.0.0-" + redirect.Value), + new XAttribute("newVersion", redirect.Value)))); + + runtimeNode.Add(redirectNodes); + + if (AppConfigFile != null) + { + AppConfigFile.CopyMetadataTo(OutputAppConfigFile); + } + else + { + OutputAppConfigFile.SetMetadata(ItemMetadataNames.targetPath, TargetName); + } + + doc.Save(OutputAppConfigFile.ItemSpec); + + return !Log.HasLoggedErrors; + } + + /// + /// Determins whether the name, culture, and public key token of the given assembly name "suggestedRedirect" + /// matches the name, culture, and publicKeyToken strings. + /// + private static bool IsMatch(AssemblyName suggestedRedirect, string name, string culture, string publicKeyToken) + { + if (String.Compare(suggestedRedirect.Name, name, StringComparison.OrdinalIgnoreCase) != 0) + { + return false; + } + + if (ByteArrayMatchesString(suggestedRedirect.GetPublicKeyToken(), publicKeyToken)) + { + return false; + } + + // The binding redirect will be applied if the culture is missing from the "assemblyIdentity" node. + // So we consider it a match if the existing binding redirect doesn't have culture specified. + var cultureString = suggestedRedirect.CultureName; + if (String.IsNullOrEmpty(cultureString)) + { + // We use "neutral" for "Invariant Language (Invariant Country)" in assembly names. + cultureString = "neutral"; + } + + if (!String.IsNullOrEmpty(culture) && + String.Compare(cultureString, culture, StringComparison.OrdinalIgnoreCase) != 0) + { + return false; + } + + return true; + } + + /// + /// Determines whether string "s" is the hexdecimal representation of the byte array "a". + /// + private static bool ByteArrayMatchesString(Byte[] a, string s) + { + return String.Compare(ResolveAssemblyReference.ByteArrayToString(a), s, StringComparison.OrdinalIgnoreCase) != 0; + } + + /// + /// Going through all the binding redirects in the runtime node, if anyone overlaps with a RAR suggested redirect, + /// we update the existing redirect and output warning. + /// + private void UpdateExistingBindingRedirects(XElement runtimeNode, IDictionary redirects) + { + ErrorUtilities.VerifyThrow(runtimeNode != null, "This should not be called if the \"runtime\" node is missing."); + + var assemblyBindingNodes = runtimeNode.Nodes() + .OfType() + .Where(e => e.Name.LocalName == "assemblyBinding"); + + foreach (var assemblyBinding in assemblyBindingNodes) + { + var dependentAssembly = assemblyBinding.Nodes() + .OfType() + .FirstOrDefault(); + + if (dependentAssembly == null) + { + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MissingNode", "dependentAssembly", "assemblyBinding"); + continue; + } + + var assemblyIdentity = dependentAssembly.Nodes() + .OfType() + .Where(e => e.Name.LocalName == "assemblyIdentity") + .FirstOrDefault(); + + if (assemblyIdentity == null) + { + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MissingNode", "assemblyIdentity", "dependentAssembly"); + continue; + } + + var bindingRedirect = dependentAssembly.Nodes() + .OfType() + .Where(e => e.Name.LocalName == "bindingRedirect") + .FirstOrDefault(); + + if (bindingRedirect == null) + { + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MissingNode", "bindingRedirect", "dependentAssembly"); + continue; + } + + var name = assemblyIdentity.Attribute("name"); + var nameValue = name.Value; + var publicKeyToken = assemblyIdentity.Attribute("publicKeyToken"); + var publicKeyTokenValue = publicKeyToken.Value; + var culture = assemblyIdentity.Attribute("culture"); + var cultureValue = culture == null ? String.Empty : culture.Value; + + if (name == null || publicKeyToken == null) + { + continue; + } + + var oldVersionAttribute = bindingRedirect.Attribute("oldVersion"); + var newVersionAttribute = bindingRedirect.Attribute("newVersion"); + + if (oldVersionAttribute == null || newVersionAttribute == null) + { + continue; + } + + var oldVersionRange = oldVersionAttribute.Value.Split('-'); + if (oldVersionRange == null || oldVersionRange.Length == 0 || oldVersionRange.Length > 2) + { + continue; + } + + var oldVerStrLow = oldVersionRange[0]; + var oldVerStrHigh = oldVersionRange[oldVersionRange.Length == 1 ? 0 : 1]; + + Version oldVersionLow, oldVersionHigh; + if (!Version.TryParse(oldVerStrLow, out oldVersionLow)) + { + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MalformedVersionNumber", oldVerStrLow); + continue; + } + + if (!Version.TryParse(oldVerStrHigh, out oldVersionHigh)) + { + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MalformedVersionNumber", oldVerStrHigh); + continue; + } + + // We cannot do a simply dictionary lookup here because we want to allow relaxed "culture" matching: + // we consider it a match if the existing binding redirect doesn't specify culture in the assembly identity. + foreach (var entry in redirects) + { + if (IsMatch(entry.Key, nameValue, cultureValue, publicKeyTokenValue)) + { + string maxVerStr = entry.Value; + var maxVersion = new Version(maxVerStr); + + if (maxVersion >= oldVersionLow) + { + // Update the existing binding redirect to the RAR suggested one. + var newName = entry.Key.Name; + var newCulture = entry.Key.CultureName; + var newPublicKeyToken = entry.Key.GetPublicKeyToken(); + var newProcessorArchitecture = entry.Key.ProcessorArchitecture; + + var attributes = new List(4); + attributes.Add(new XAttribute("name", newName)); + attributes.Add(new XAttribute("culture", String.IsNullOrEmpty(newCulture) ? "neutral" : newCulture)); + attributes.Add(new XAttribute("publicKeyToken", ResolveAssemblyReference.ByteArrayToString(newPublicKeyToken))); + if (newProcessorArchitecture != 0) + { + attributes.Add(new XAttribute("processorArchitecture", newProcessorArchitecture.ToString())); + } + + assemblyIdentity.ReplaceAttributes(attributes); + + oldVersionAttribute.Value = "0.0.0.0-" + (maxVersion >= oldVersionHigh ? maxVerStr : oldVerStrHigh); + newVersionAttribute.Value = maxVerStr; + redirects.Remove(entry.Key); + + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.OverlappingBindingRedirect", entry.Key.ToString(), bindingRedirect.ToString()); + } + + break; + } + } + } + } + + /// + /// Load or create App.Config + /// + private XDocument LoadAppConfig(ITaskItem appConfigItem) + { + XDocument document; + if (appConfigItem == null) + { + document = new XDocument( + new XDeclaration("1.0", "utf-8", "true"), + new XElement("configuration")); + } + else + { + document = XDocument.Load(appConfigItem.ItemSpec); + if (document.Root == null || document.Root.Name != "configuration") + { + Log.LogErrorWithCodeFromResources("GenerateBindingRedirects.MissingConfigurationNode"); + return null; + } + } + + return document; + } + + /// + /// Parse the suggested redirects from RAR and return a dictionary containing all those suggested redirects + /// in the form of AssemblyName-MaxVersion pairs. + /// + private IDictionary ParseSuggestedRedirects() + { + ErrorUtilities.VerifyThrow(SuggestedRedirects != null && SuggestedRedirects.Length > 0, "This should not be called if there is no suggested redirect."); + + var map = new Dictionary(); + foreach (var redirect in SuggestedRedirects) + { + var redirectStr = redirect.ItemSpec; + + try + { + var maxVerStr = redirect.GetMetadata("MaxVersion"); + Log.LogMessageFromResources(MessageImportance.Low, "GenerateBindingRedirects.ProcessingSuggestedRedirect", redirectStr, maxVerStr); + + var assemblyIdentity = new AssemblyName(redirectStr); + + map.Add(assemblyIdentity, maxVerStr); + } + catch (Exception) + { + Log.LogWarningWithCodeFromResources("GenerateBindingRedirects.MalformedAssemblyName", redirectStr); + continue; + } + } + + return map; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/GlobalAssemblyCache.cs b/src/XMakeTasks/AssemblyDependency/GlobalAssemblyCache.cs new file mode 100644 index 00000000000..c2a18ced3b2 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/GlobalAssemblyCache.cs @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Runtime.InteropServices; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.Build.Framework; +using System.Collections.Concurrent; + +namespace Microsoft.Build.Tasks +{ + /// + /// Methods for dealing with the GAC. + /// + static internal class GlobalAssemblyCache + { + /// + /// Default delegate to get the path based on a fusion name. + /// + internal static readonly GetPathFromFusionName pathFromFusionName = new GetPathFromFusionName(RetreivePathFromFusionName); + + /// + /// Default delegate to get the gac enumerator. + /// + internal static readonly GetGacEnumerator gacEnumerator = new GetGacEnumerator(GetGacNativeEnumerator); + + /// + /// Given a strong name, find its path in the GAC. + /// + /// The strong name. + /// Like x86 or IA64\AMD64. + /// The path to the assembly. Empty if none exists. + private static string GetLocationImpl(AssemblyNameExtension assemblyName, string targetProcessorArchitecture, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntime, FileExists fileExists, GetPathFromFusionName getPathFromFusionName, GetGacEnumerator getGacEnumerator, bool specificVersion) + { + // Extra checks for PInvoke-destined data. + ErrorUtilities.VerifyThrowArgumentNull(assemblyName, "assemblyName"); + ErrorUtilities.VerifyThrow(assemblyName.FullName != null, "Got a null assembly name fullname."); + + string strongName = assemblyName.FullName; + + if (targetProcessorArchitecture != null && !assemblyName.HasProcessorArchitectureInFusionName) + { + strongName += ", ProcessorArchitecture=" + targetProcessorArchitecture; + } + + string assemblyPath = String.Empty; + + // Dictionary sorted by Version in reverse order, this will give the values enumeration the highest runtime version first. + SortedDictionary> assembliesByRuntime = GenerateListOfAssembliesByRuntime(strongName, getRuntimeVersion, targetedRuntime, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion); + if (assembliesByRuntime != null) + { + foreach (SortedDictionary runtimeBucket in assembliesByRuntime.Values) + { + // Grab the first element if there are one or more elements. This will give us the highest version assembly name. + if (runtimeBucket.Count > 0) + { + foreach (KeyValuePair kvp in runtimeBucket) + { + assemblyPath = kvp.Value; + break; + } + + if (!String.IsNullOrEmpty(assemblyPath)) + { + break; + } + } + } + } + + return assemblyPath; + } + + /// + /// Given a strong name generate the gac enumerator. + /// + internal static IEnumerable GetGacNativeEnumerator(string strongName) + { + IEnumerable gacEnumerator = null; + try + { + // Will fail if the publickeyToken is null but will not fail if it is missing. + gacEnumerator = new Microsoft.Build.Tasks.NativeMethods.AssemblyCacheEnum(strongName); + } + catch (FileLoadException) + { + // We could not handle the name passed in + return null; + } + + return gacEnumerator; + } + + /// + /// Enumerate the gac and generate a list of assemblies which match the strongname by runtime. + /// + private static SortedDictionary> GenerateListOfAssembliesByRuntime(string strongName, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntime, FileExists fileExists, GetPathFromFusionName getPathFromFusionName, GetGacEnumerator getGacEnumerator, bool specificVersion) + { + ErrorUtilities.VerifyThrowArgumentNull(targetedRuntime, "targetedRuntime"); + + IEnumerable gacEnum = getGacEnumerator(strongName); + + // Dictionary of Runtime version (sorted in reverse order) to a list of assemblies which are part of that runtime. This will allow us to pick the highest runtime and version first. + SortedDictionary> assembliesWithValidRuntimes = new SortedDictionary>(ReverseVersionGenericComparer.Comparer); + + // Enumerate the gac values returned based on the partial or full fusion name. + if (gacEnum != null) + { + foreach (AssemblyNameExtension gacAssembly in gacEnum) + { + // We only have a fusion name from the IAssemblyName interface we need to get the path to the assembly to resolve it and to check its runtime. + string assemblyPath = getPathFromFusionName(gacAssembly.FullName); + + // Make sure we could get the path from the Fusion name and make sure the file actually exists. + if (!String.IsNullOrEmpty(assemblyPath) && fileExists(assemblyPath)) + { + // Get the runtime version from the found assembly. + string runtimeVersionRaw = getRuntimeVersion(assemblyPath); + + // Convert the runtime string to a version so we can properly compare them as per version object comparison rules. + // We will accept version which are less than or equal to the targeted runtime. + Version runtimeVersion = VersionUtilities.ConvertToVersion(runtimeVersionRaw); + + // Make sure the targeted runtime is greater than or equal to the runtime version of the assembly we got from the gac. + if (runtimeVersion != null) + { + if (targetedRuntime.CompareTo(runtimeVersion) >= 0 || specificVersion) + { + SortedDictionary assembliesWithRuntime = null; + assembliesWithValidRuntimes.TryGetValue(runtimeVersion, out assembliesWithRuntime); + + // Create a new list if one does not exist. + if (assembliesWithRuntime == null) + { + assembliesWithRuntime = new SortedDictionary(AssemblyNameReverseVersionComparer.GenericComparer); + assembliesWithValidRuntimes.Add(runtimeVersion, assembliesWithRuntime); + } + + if (!assembliesWithRuntime.ContainsKey(gacAssembly)) + { + // Add the assembly to the list + assembliesWithRuntime.Add(gacAssembly, assemblyPath); + } + } + } + } + } + } + + return assembliesWithValidRuntimes; + } + + /// + /// Given a fusion name get the path to the assembly on disk. + /// + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "Microsoft.Build.Tasks.IAssemblyCache.QueryAssemblyInfo(System.UInt32,System.String,Microsoft.Build.Tasks.ASSEMBLY_INFO@)", Justification = "We use the out parameters to determine if we got a good assembly back or not")] + internal static string RetreivePathFromFusionName(string strongName) + { + // Extra checks for PInvoke-destined data. + ErrorUtilities.VerifyThrowArgumentNull(strongName, "assemblyName"); + + IAssemblyCache assemblyCache; + + uint hr = NativeMethods.CreateAssemblyCache(out assemblyCache, 0); + + ErrorUtilities.VerifyThrow(hr == NativeMethodsShared.S_OK, "CreateAssemblyCache failed, hr {0}", hr); + + ASSEMBLY_INFO assemblyInfo = new ASSEMBLY_INFO(); + assemblyInfo.cbAssemblyInfo = (uint)Marshal.SizeOf(typeof(ASSEMBLY_INFO)); + + assemblyCache.QueryAssemblyInfo(0, strongName, ref assemblyInfo); + + if (assemblyInfo.cbAssemblyInfo == 0) return null; + + assemblyInfo.pszCurrentAssemblyPathBuf = new string(new char[assemblyInfo.cchBuf]); + + assemblyCache.QueryAssemblyInfo(0, strongName, ref assemblyInfo); + + String value = assemblyInfo.pszCurrentAssemblyPathBuf; + + return value; + } + + /// + /// If we know we have a full fusion name we can skip enumerating the gac and just query for the path. This will + /// not check the runtime version of the assembly. + /// + private static string CheckForFullFusionNameInGac(AssemblyNameExtension assemblyName, string targetProcessorArchitecture, GetPathFromFusionName getPathFromFusionName) + { + string strongName = assemblyName.FullName; + if (targetProcessorArchitecture != null && !assemblyName.HasProcessorArchitectureInFusionName) + { + strongName += ", ProcessorArchitecture=" + targetProcessorArchitecture; + } + + return getPathFromFusionName(strongName); + } + + /// + /// Given a strong name, find its path in the GAC. + /// + /// The strong name. + /// Like x86 or IA64\AMD64. + /// Delegate to get the runtime version from a file path + /// What version of the runtime are we targeting + /// Are we guranteed to have a full fusion name. This really can only happen if we have already resolved the assembly + /// The path to the assembly. Empty if none exists. + internal static string GetLocation + ( + AssemblyNameExtension strongName, + ProcessorArchitecture targetProcessorArchitecture, + GetAssemblyRuntimeVersion getRuntimeVersion, + Version targetedRuntimeVersion, + bool fullFusionName, + FileExists fileExists, + GetPathFromFusionName getPathFromFusionName, + GetGacEnumerator getGacEnumerator, + bool specificVersion + ) + { + return GetLocation(null, strongName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion); + } + + /// + /// Given a strong name, find its path in the GAC. + /// + /// The strong name. + /// Like x86 or IA64\AMD64. + /// Delegate to get the runtime version from a file path + /// What version of the runtime are we targeting + /// Are we guranteed to have a full fusion name. This really can only happen if we have already resolved the assembly + /// The path to the assembly. Empty if none exists. + internal static string GetLocation + ( + IBuildEngine4 buildEngine, + AssemblyNameExtension strongName, + ProcessorArchitecture targetProcessorArchitecture, + GetAssemblyRuntimeVersion getRuntimeVersion, + Version targetedRuntimeVersion, + bool fullFusionName, + FileExists fileExists, + GetPathFromFusionName getPathFromFusionName, + GetGacEnumerator getGacEnumerator, + bool specificVersion + ) + { + ConcurrentDictionary fusionNameToResolvedPath = null; + bool useGacRarCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEGACRARCACHE") == null; + if (buildEngine != null && useGacRarCache) + { + string key = "44d78b60-3bbe-48fe-9493-04119ebf515f" + "|" + targetProcessorArchitecture.ToString() + "|" + targetedRuntimeVersion.ToString() + "|" + fullFusionName.ToString() + "|" + specificVersion.ToString(); + fusionNameToResolvedPath = buildEngine.GetRegisteredTaskObject(key, RegisteredTaskObjectLifetime.Build) as ConcurrentDictionary; + if (fusionNameToResolvedPath == null) + { + fusionNameToResolvedPath = new ConcurrentDictionary(AssemblyNameComparer.GenericComparer); + buildEngine.RegisterTaskObject(key, fusionNameToResolvedPath, RegisteredTaskObjectLifetime.Build, true /* dispose early ok*/); + } + else + { + if (fusionNameToResolvedPath.ContainsKey(strongName)) + { + string fusionName = null; + fusionNameToResolvedPath.TryGetValue(strongName, out fusionName); + return fusionName; + } + } + } + + // Optimize out the case where the public key token is null, if it is null it is not a strongly named assembly and CANNOT be in the gac. + // also passing it would cause the gac enumeration method to throw an exception indicating the assembly is not a strongnamed assembly. + + string location = null; + + // If the publickeyToken is null and the publickeytoken is in the fusion name then this means we are passing in a null or empty PublicKeyToken and then this cannot possibly be in the gac. + if ((strongName.GetPublicKeyToken() == null || strongName.GetPublicKeyToken().Length == 0) && strongName.FullName.IndexOf("PublicKeyToken", StringComparison.OrdinalIgnoreCase) != -1) + { + if (fusionNameToResolvedPath != null) + { + fusionNameToResolvedPath.TryAdd(strongName, location); + } + + return location; + } + + // A delegate was not passed in to use the default one + getPathFromFusionName = getPathFromFusionName ?? pathFromFusionName; + + // A delegate was not passed in to use the default one + getGacEnumerator = getGacEnumerator ?? gacEnumerator; + + // If we have no processor architecture set then we can tryout a number of processor architectures. + if (!strongName.HasProcessorArchitectureInFusionName) + { + if (targetProcessorArchitecture != ProcessorArchitecture.MSIL && targetProcessorArchitecture != ProcessorArchitecture.None) + { + string processorArchitecture = ResolveAssemblyReference.ProcessorArchitectureToString(targetProcessorArchitecture); + // Try processor specific first. + if (fullFusionName) + { + location = CheckForFullFusionNameInGac(strongName, processorArchitecture, getPathFromFusionName); + } + else + { + location = GetLocationImpl(strongName, processorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion); + } + + if (location != null && location.Length > 0) + { + if (fusionNameToResolvedPath != null) + { + fusionNameToResolvedPath.TryAdd(strongName, location); + } + return location; + } + } + + // Next, try MSIL + if (fullFusionName) + { + location = CheckForFullFusionNameInGac(strongName, "MSIL", getPathFromFusionName); + } + else + { + location = GetLocationImpl(strongName, "MSIL", getRuntimeVersion, targetedRuntimeVersion, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion); + } + if (location != null && location.Length > 0) + { + if (fusionNameToResolvedPath != null) + { + fusionNameToResolvedPath.TryAdd(strongName, location); + } + return location; + } + } + + // Next, try no processor architecure + if (fullFusionName) + { + location = CheckForFullFusionNameInGac(strongName, null, getPathFromFusionName); + } + else + { + location = GetLocationImpl(strongName, null, getRuntimeVersion, targetedRuntimeVersion, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion); + } + + if (location != null && location.Length > 0) + { + if (fusionNameToResolvedPath != null) + { + fusionNameToResolvedPath.TryAdd(strongName, location); + } + return location; + } + + if (fusionNameToResolvedPath != null) + { + fusionNameToResolvedPath.TryAdd(strongName, null); + } + + return null; + } + + /// + /// Return the root path of the GAC + /// + /// + internal static string GetGacPath() + { + int gacPathLength = 0; + NativeMethods.GetCachePath(AssemblyCacheFlags.GAC, null, ref gacPathLength); + StringBuilder gacPath = new StringBuilder(gacPathLength); + NativeMethods.GetCachePath(AssemblyCacheFlags.GAC, gacPath, ref gacPathLength); + + return gacPath.ToString(); + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/HintPathResolver.cs b/src/XMakeTasks/AssemblyDependency/HintPathResolver.cs new file mode 100644 index 00000000000..197565198e5 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/HintPathResolver.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {HintPathFromItem} + /// + internal class HintPathResolver : Resolver + { + /// + /// Construct. + /// + /// + /// + /// + public HintPathResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false) + { + } + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + if (hintPath != null && hintPath.Length > 0) + { + if (ResolveAsFile(hintPath, assemblyName, isPrimaryProjectReference, wantSpecificVersion, true, assembliesConsideredAndRejected)) + { + userRequestedSpecificFile = true; + foundPath = hintPath; + return true; + } + } + + foundPath = null; + userRequestedSpecificFile = false; + return false; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/InstalledAssemblies.cs b/src/XMakeTasks/AssemblyDependency/InstalledAssemblies.cs new file mode 100644 index 00000000000..9abd81a7fe2 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/InstalledAssemblies.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +namespace Microsoft.Build.Tasks +{ + /// + /// Utility methods that encapsulate well-known assemblies. + /// + internal class InstalledAssemblies + { + private RedistList _redistList = null; + + /// + /// Construct. + /// + /// + internal InstalledAssemblies(RedistList redistList) + { + _redistList = redistList; + } + + /// + /// Unify an assembly name according to the fx retarget rules. + /// + /// The unify-from assembly name. + /// The new version number. + /// Whether this assembly will be available on target machines. + /// May be true, false or null. Null means there was no IsRedistRoot in the redist list. + /// Name of the corresponding Resist specified in the redist list. + internal void GetInfo + ( + AssemblyNameExtension assemblyName, + out Version unifiedVersion, + out bool isPrerequisite, + out bool? isRedistRoot, + out string redistName + ) + { + unifiedVersion = assemblyName.Version; + isPrerequisite = false; + isRedistRoot = null; + redistName = null; + + + // Short-circuit in cases where there is no redist list. + if (_redistList == null) + { + return; + } + + // If there's no version, for example in a simple name, then no remapping is possible, + // and this is not a prerequisite. + if (assemblyName.Version == null) + { + return; + } + + AssemblyEntry highestVersionFromRedistList = FindHighestVersionInRedistList(assemblyName); + + // Could not find the assembly in the redist list. Return as there has been no redist list unification + if (highestVersionFromRedistList == null) + { + return; + } + + // Dont allow downgrading of reference version due to redist unification because this is automatic rather than something like an appconfig which + // has to be manually set. However if the major version is 255 then we do want to unify down the version number. + if (assemblyName.Version <= highestVersionFromRedistList.AssemblyNameExtension.Version || assemblyName.Version.Major == 255) + { + unifiedVersion = highestVersionFromRedistList.AssemblyNameExtension.Version; + isPrerequisite = _redistList.IsPrerequisiteAssembly(highestVersionFromRedistList.FullName); + isRedistRoot = _redistList.IsRedistRoot(highestVersionFromRedistList.FullName); + redistName = _redistList.RedistName(highestVersionFromRedistList.FullName); + + return; + } + } + + /// + /// We need to check to see if an assembly name is in our remapping list, if it is we should return a new assemblyNameExtension which has been remapped. + /// Remapping is usually used for portable libraries where we need to turn one assemblyName that is retargetable to another assemblyname. + /// + internal AssemblyNameExtension RemapAssemblyExtension(AssemblyNameExtension assemblyName) + { + // Short-circuit in cases where there is no redist list + if (_redistList == null) + { + return null; + } + + return _redistList.RemapAssembly(assemblyName); + } + + /// + /// Find the highest version of the assemblyName in the redist list for framework assemblies taking into account the simplename, culture and public key. + /// + /// The name of the assembly we would like to find the highest version for + /// Key value pair, K: Assembly entry of highest value in the redist list. V: AssemblyNameExtension with the version information or null if the name could not be found + internal AssemblyEntry FindHighestVersionInRedistList(AssemblyNameExtension assemblyName) + { + // The assembly we are looking for is not listed in a redist list which contains framework assemblies. We do not want to find + // find non framework assembly entries. + if (!FrameworkAssemblyEntryInRedist(assemblyName)) + { + return null; + } + + // Look up an assembly with the same base name in the installedAssemblyTables. + // This list should be sorted alphabetically by simple name and then greatest verion + AssemblyEntry[] tableCandidates = _redistList.FindAssemblyNameFromSimpleName(assemblyName.Name); + + foreach (AssemblyEntry tableCandidate in tableCandidates) + { + // Make an AssemblyNameExtension for comparing. + AssemblyNameExtension mostRecentAssemblyNameCandidate = tableCandidate.AssemblyNameExtension; + + // Optimize performance for the whidbey case by doing an exact comparison first. + if (mostRecentAssemblyNameCandidate.EqualsIgnoreVersion(assemblyName)) + { + return tableCandidate; + } + } + + return null; + } + + /// + /// Given an assemblyNameExtension, is that assembly name in the redist list and does that redist name start with Microsoft-Windows-CLRCoreComp which indicates + /// the redist entry is a framework redist list rather than a 3rd part redist list. + /// + internal bool FrameworkAssemblyEntryInRedist(AssemblyNameExtension assemblyName) + { + if (_redistList == null) + { + return false; + } + + return _redistList.FrameworkAssemblyEntryInRedist(assemblyName); + } + + /// + /// Find every assembly full name in the redist list that matches the given simple name. + /// + /// + /// The array of assembly names. + internal AssemblyEntry[] FindAssemblyNameFromSimpleName(string simpleName) + { + if (_redistList == null) + { + return new AssemblyEntry[0]; + } + + return _redistList.FindAssemblyNameFromSimpleName(simpleName); + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/InvalidReferenceAssemblyNameException.cs b/src/XMakeTasks/AssemblyDependency/InvalidReferenceAssemblyNameException.cs new file mode 100644 index 00000000000..d1be31a8527 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/InvalidReferenceAssemblyNameException.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Tasks +{ + /// + /// There reference is not a well-formed fusion name *and* its not a file + /// that exists on disk. + /// + [Serializable] + internal sealed class InvalidReferenceAssemblyNameException : Exception + { + private string _sourceItemSpec; + + /// + /// Don't allow default construction. + /// + private InvalidReferenceAssemblyNameException() + { + } + + /// + /// Construct + /// + internal InvalidReferenceAssemblyNameException(string sourceItemSpec) + { + _sourceItemSpec = sourceItemSpec; + } + + /// + /// Construct + /// + private InvalidReferenceAssemblyNameException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + /// + /// The item spec of the item that is the source fo the problem. + /// + internal string SourceItemSpec + { + get { return _sourceItemSpec; } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/NoMatchReason.cs b/src/XMakeTasks/AssemblyDependency/NoMatchReason.cs new file mode 100644 index 00000000000..5dc77b9376c --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/NoMatchReason.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Reflection; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Tasks +{ + /// + /// Reasons why a resolution might fail. + /// + internal enum NoMatchReason + { + /// + /// The default state. + /// + Unknown, + + /// + /// There was no file found. + /// + FileNotFound, + + /// + /// The file was found, but its fusion name didn't match. + /// + FusionNamesDidNotMatch, + + /// + /// The file was found, but it didn't have a fusion name. + /// + TargetHadNoFusionName, + + /// + /// The file is not in the GAC. + /// + NotInGac, + + /// + /// If treated as a filename, the file doesn't exist on disk. + /// + NotAFileNameOnDisk, + + /// + /// The processor architecure does not match the targeted processor architecture. + /// + ProcessorArchitectureDoesNotMatch + } +} diff --git a/src/XMakeTasks/AssemblyDependency/RawFilenameResolver.cs b/src/XMakeTasks/AssemblyDependency/RawFilenameResolver.cs new file mode 100644 index 00000000000..f4d3ecfc35f --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/RawFilenameResolver.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolve searchpath type {RawFileName} + /// + internal class RawFilenameResolver : Resolver + { + /// + /// Construct. + /// + /// + /// + /// + public RawFilenameResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false) + { + } + + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (rawFileNameCandidate != null) + { + // {RawFileName} was passed in. + if (fileExists(rawFileNameCandidate)) + { + userRequestedSpecificFile = true; + foundPath = rawFileNameCandidate; + return true; + } + else + { + if (assembliesConsideredAndRejected != null) + { + ResolutionSearchLocation considered = null; + considered = new ResolutionSearchLocation(); + considered.FileNameAttempted = rawFileNameCandidate; + considered.SearchPath = searchPathElement; + considered.Reason = NoMatchReason.NotAFileNameOnDisk; + assembliesConsideredAndRejected.Add(considered); + } + } + } + + + return false; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/Reference.cs b/src/XMakeTasks/AssemblyDependency/Reference.cs new file mode 100644 index 00000000000..5cab716a992 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/Reference.cs @@ -0,0 +1,1392 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.Collections.Generic; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; +using System.Runtime.Versioning; + +namespace Microsoft.Build.Tasks +{ + /// + /// A reference to an assembly along with information about resolution. + /// + sealed internal class Reference + { + /// + /// Hashtable where ITaskItem.ItemSpec (a string) is the key and ITaskItem is the value. + /// A hash table is used to remove duplicates. + /// All source items that inspired this reference (possibly indirectly through a dependency chain). + /// + private Hashtable _sourceItems = new Hashtable(StringComparer.OrdinalIgnoreCase); + + /// + /// Hashtable of Key=Reference, Value=Irrelevent. + /// A list of unique dependies. + /// + private Hashtable _dependees = new Hashtable(); + + /// + /// Hashset of Reference which depend on this reference + /// A list of unique dependencies. + /// + private HashSet _dependencies = new HashSet(); + + /// + /// Scatter files associated with this reference. + /// + private string[] _scatterFiles = new string[0]; + + /// + /// ArrayList of Exception. + /// Any errors that occurred while resolving or finding dependencies on this item. + /// + private ArrayList _errors = new ArrayList(); + + /// + /// ArrayList of string. + /// Contains any file extension that are related to this file. Pdbs and xmls are related. + /// This is an extension string starting with "." + /// + private ArrayList _relatedFileExtensions = new ArrayList(); + + /// + /// ArrayList of string. + /// Contains satellite files for this reference. + /// This file path is relative to the location of the reference. + /// + private ArrayList _satelliteFiles = new ArrayList(); + + /// + /// ArrayList of string. + /// Contains serializaion assembly files for this reference. + /// This file path is relative to the location of the reference. + /// + private ArrayList _serializationAssemblyFiles = new ArrayList(); + + /// + /// The list of assemblies that were consider for matching but that + /// didn't pan out because they didn't match exactly. + /// + private ArrayList _assembliesConsideredAndRejected = new ArrayList(); + + /// + /// The searchpath location that the reference was found at. + /// + private string _resolvedSearchPath = String.Empty; + + /// + /// This is the reference that won against this reference in the conflict contest. + /// + private AssemblyNameExtension _conflictVictorName = null; + + /// + /// The reason this reference lost + /// + private ConflictLossReason _conflictLossReason = ConflictLossReason.DidntLose; + + /// + /// AssemblyNames of references that lost collision conflicts with this reference. + /// + private ArrayList _conflictVictims = new ArrayList(); + + /// + /// These are the versions (type UnificationVersion) that were unified from. + /// + private Dictionary _preUnificationVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// If 'true' then the path that this item points to is known to be a bad image. + /// This item shouldn't be passed to compilers and so forth. + /// + private bool _isBadImage = false; + + /// + /// The original source item, as passed into the task that is directly associated + /// with this reference. This only applies to "primary" references. + /// + private ITaskItem _primarySourceItem; + + /// + /// The full path to the assembly. If this is "", then that means that this reference + /// has not been resolved. + /// + private string _fullPath = String.Empty; + + /// + /// The directory that this reference lives in. + /// + private string _directoryName = String.Empty; + + /// + /// The reference's filename without extension. + /// + private string _fileNameWithoutExtension = String.Empty; + + /// + /// The full path to the file name but without the extension. + /// + private string _fullPathWithoutExtension = String.Empty; + + /// + /// Whether this assembly came from the project. If 'false' then this reference was deduced + /// through the reference resolution process. + /// + private bool _isPrimary = false; + + /// + /// Whether or not this reference will be installed on the target machine. + /// + private bool _isPrerequisite = false; + + /// + /// Whether or not this reference is a redist root. + /// + private bool? _isRedistRoot = null; + + /// + /// The redist name for this reference (if any). + /// + private string _redistName = null; + + /// + /// Whether this reference should be copied to the local 'bin' dir or not and the reason this flag + /// was set that way. + /// + private CopyLocalState _copyLocalState = CopyLocalState.Undecided; + + /// + /// Whether or not we still need to find dependencies for this reference. + /// + private bool _dependenciesFound = false; + + /// + /// This is the HintPath from the source item. This is used to resolve the assembly. + /// + private string _hintPath = ""; + + /// + /// The list of expected extensions. + /// + private ArrayList _expectedExtensions = null; + + /// + /// Whether or not the exact specific version is required. + /// Note that simple names like "MySimpleAssemblyName" will need to match exactly. + /// That is, no version that has other information will be accepted. + /// + private bool _wantSpecificVersion = true; + + /// + /// Whether or not the types from this reference need to be embedded into the target assembly + /// + private bool _embedInteropTypes = false; + + /// + /// This is the key that was passed in to the reference through the metadata. + /// + private string _assemblyFolderKey = String.Empty; + + /// + /// This will be true if the user requested a specific file. We know this when the file was resolved + /// by hintpath or if it was resolve as a raw file name for example. + /// + private bool _userRequestedSpecificFile = false; + + /// + /// Version of the references + /// + private Version _referenceVersion = null; + + /// + /// A set of properties which are useful to log the correct information for why this reference was not resolved. + /// + private ExclusionListProperties _exclusionListProperties = new ExclusionListProperties(); + + /// + /// Is the reference a native winMD file. This means it has a image runtime of WindowsRuntime and not CLR. + /// + private bool _winMDFile; + + /// + /// Is the file a managed winmd file. That means it has both windows runtime and CLR in the imageruntime string. + /// + private bool _isManagedWinMDFile; + + /// + /// The imageruntime version for this reference. + /// + private string _imageRuntimeVersion; + + + /// + /// If the reference has an SDK name metadata this will contain that string. + /// + private string _sdkName = String.Empty; + + /// + /// Set containing the names the reference was remapped from + /// + private HashSet _remappedAssemblyNames = new HashSet(); + + /// + /// Delegate to determine if the file is a winmd file or not + /// + private IsWinMDFile _isWinMDFile; + + /// + /// Delegate to check to see if the file exists on disk + /// + private FileExists _fileExists; + + /// + /// Delegate to get the imageruntime version from a file. + /// + private GetAssemblyRuntimeVersion _getRuntimeVersion; + + /// + /// The frameworkName the reference was built against + /// + private FrameworkName _frameworkName; + + internal Reference(IsWinMDFile isWinMDFile, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion) + { + _isWinMDFile = isWinMDFile; + _fileExists = fileExists; + _getRuntimeVersion = getRuntimeVersion; + } + + /// + /// Add items that caused (possibly indirectly through a dependency chain) this Reference. + /// + internal void AddSourceItem(ITaskItem sourceItem) + { + bool sourceItemAlreadyInList = _sourceItems.Contains(sourceItem.ItemSpec); + if (!sourceItemAlreadyInList) + { + _sourceItems[sourceItem.ItemSpec] = sourceItem; + PropagateSourceItems(sourceItem); + } + } + + /// + /// Add items that caused (possibly indirectly through a dependency chain) this Reference. + /// + internal void AddSourceItems(IEnumerable sourceItemsToAdd) + { + foreach (ITaskItem sourceItem in sourceItemsToAdd) + { + AddSourceItem(sourceItem); + } + } + + + /// + /// We have had our source item list updated, we need to propagate this change to any of our dependencies so they have the new information. + /// + internal void PropagateSourceItems(ITaskItem sourceItem) + { + if (_dependencies != null) + { + foreach (Reference dependency in _dependencies) + { + dependency.AddSourceItem(sourceItem); + } + } + } + + /// + /// Get the source items for this reference. + /// This is collection of ITaskItems. + /// + internal ICollection GetSourceItems() + { + return (ICollection)_sourceItems.Values; + } + + /// + /// Add a reference which this reference depends on + /// + internal void AddDependency(Reference dependency) + { + if (!_dependencies.Contains(dependency)) + { + _dependencies.Add(dependency); + } + } + + /// + /// Add a reference that caused (possibly indirectly through a dependency chain) this Reference. + /// + internal void AddDependee(Reference dependee) + { + Debug.Assert(dependee.FullPath.Length > 0, "Cannot add dependee that doesn't have a full name. This should have already been resolved."); + + dependee.AddDependency(this); + + if (_dependees[dependee] == null) + { + _dependees[dependee] = String.Empty; + + // When a new dependee is added, this is a new place where a reference might be resolved. + // Reset this item so it will be re-resolved if possible. + if (IsUnresolvable) + { + _errors = new ArrayList(); + _assembliesConsideredAndRejected = new ArrayList(); + } + } + } + + /// + /// A dependee may be removed because it or its dependee's are in the black list + /// + /// + internal void RemoveDependee(Reference dependeeToRemove) + { + _dependees.Remove(dependeeToRemove); + } + + /// + /// A dependency may be removed because it may not be referenced any more due this reference being in the black list or being removed due to it depending on something in the black list + /// + internal void RemoveDependency(Reference dependencyToRemove) + { + _dependencies.Remove(dependencyToRemove); + } + + + /// + /// Get the dependee references for this reference. + /// This is collection of References. + /// + internal ICollection GetDependees() + { + return (ICollection)_dependees.Keys; + } + + /// + /// Scatter files associated with this assembly. + /// + /// + internal void AttachScatterFiles(string[] scatterFilesToAttach) + { + if (scatterFilesToAttach == null || scatterFilesToAttach.Length == 0) + { + _scatterFiles = new string[0]; + } + else + { + _scatterFiles = scatterFilesToAttach; + } + } + + /// + /// Scatter files associated with this assembly. + /// + /// + internal string[] GetScatterFiles() + { + return _scatterFiles; + } + + /// + /// Set one expected extension for this reference. + /// + internal void SetExecutableExtension(string extension) + { + if (_expectedExtensions == null) + { + _expectedExtensions = new ArrayList(); + } + else + { + _expectedExtensions.Clear(); + } + if (extension.Length > 0 && extension[0] != '.') + { + extension = '.' + extension; + } + _expectedExtensions.Add(extension); + } + + /// + /// Get the list of expected extensions. + /// + internal string[] GetExecutableExtensions(string[] allowedAssemblyExtensions) + { + if (_expectedExtensions == null) + { + // Use the default. + return allowedAssemblyExtensions; + } + return (string[])_expectedExtensions.ToArray(typeof(string)); + } + + /// + /// Whether the name needs to match exactly or just the simple name part needs to match. + /// + /// + internal bool WantSpecificVersion + { + get { return _wantSpecificVersion; } + } + + /// + /// Whether types need to be embedded into the target assembly + /// + /// + internal bool EmbedInteropTypes + { + get { return _embedInteropTypes; } + set { _embedInteropTypes = value; } + } + + /// + /// This will be true if the user requested a specific file. We know this when the file was resolved + /// by hintpath or if it was resolve as a raw file name for example. + /// + internal bool UserRequestedSpecificFile + { + get { return _userRequestedSpecificFile; } + set { _userRequestedSpecificFile = value; } + } + + /// + /// The version number of this reference + /// + internal Version ReferenceVersion + { + get + { + return _referenceVersion; + } + + set + { + _referenceVersion = value; + } + } + + /// + /// Is the reference in the GAC or not, if the check has not been made this will be null. + /// + internal bool? FoundInGac + { + get; + private set; + } + + /// + /// Set of properties for this reference used to log why this reference could not be resolved. + /// + internal ExclusionListProperties ExclusionListLoggingProperties + { + get + { + return _exclusionListProperties; + } + } + + /// + /// Determines if a given reference or its parent primary references have specific version metadata set to true. + /// If anyParentHasMetadata is set to true then we will return true if any parent primary reference has the specific version metadata set to true, + /// if the value is false we will return true ONLY if all parent primary references have the metadata set to true. + /// + internal bool CheckForSpecificVersionMetadataOnParentsReference(bool anyParentHasMetadata) + { + bool hasSpecificVersionMetadata = false; + + // We are our own parent, therefore the specific version metadata is what ever is passed into as wantspecificVersion for this reference. + // this saves us from having to read the metadata from our item again. + if (IsPrimary) + { + hasSpecificVersionMetadata = _wantSpecificVersion; + } + else + { + // Go through all of the primary items which lead to this dependency, if they all have specificVersion set to true then + // hasSpecificVersionMetadata will be true. If any item has the metadata set to false or not set then the value will be false. + foreach (ITaskItem item in GetSourceItems()) + { + hasSpecificVersionMetadata = MetadataConversionUtilities.TryConvertItemMetadataToBool(item, ItemMetadataNames.specificVersion); + + // Break if one of the primary references has specific version false or not set + if (anyParentHasMetadata == hasSpecificVersionMetadata) + { + break; + } + } + } + + return hasSpecificVersionMetadata; + } + + /// + /// Add a dependency or resolution error to this reference's list of errors. + /// + /// The error. + internal void AddError(Exception e) + { + if (e is BadImageReferenceException) + { + _isBadImage = true; + } + _errors.Add(e); + } + + /// + /// Return the list of dependency or resolution errors for this item. + /// + /// The collection of resolution errors. + internal ICollection GetErrors() + { + return (ICollection)_errors; + } + + /// + /// Add a new related file to this reference. + /// Related files always live in the same directory as the reference. + /// Examples include, MyAssembly.pdb and MyAssembly.xml + /// + /// This is the filename extension. + internal void AddRelatedFileExtension(string filenameExtension) + { +#if _DEBUG + Debug.Assert(filenameExtension[0]=='.', "Expected extension to start with '.'"); +#endif + _relatedFileExtensions.Add(filenameExtension); + } + + /// + /// Return the list of related files for this item. + /// + /// The collection of related file extensions. + internal ICollection GetRelatedFileExtensions() + { + return (ICollection)_relatedFileExtensions; + } + + + /// + /// Add a new satellite file + /// + /// This is the filename relative the this reference. + internal void AddSatelliteFile(string filename) + { +#if _DEBUG + Debug.Assert(!Path.IsPathRooted(filename), "Satellite path should be relative to the current reference."); +#endif + _satelliteFiles.Add(filename); + } + + /// + /// Add a new serialization assembly file. + /// + /// This is the filename relative the this reference. + internal void AddSerializationAssemblyFile(string filename) + { +#if _DEBUG + Debug.Assert(!Path.IsPathRooted(filename), "Serialization assembly path should be relative to the current reference."); +#endif + _serializationAssemblyFiles.Add(filename); + } + + /// + /// Return the list of satellite files for this item. + /// + /// The collection of satellit files. + internal ICollection GetSatelliteFiles() + { + return (ICollection)_satelliteFiles; + } + + /// + /// Return the list of serialization assembly files for this item. + /// + /// The collection of serialization assembly files. + internal ICollection GetSerializationAssemblyFiles() + { + return (ICollection)_serializationAssemblyFiles; + } + + /// + /// The full path to the assembly. If this is "", then that means that this reference + /// has not been resolved. + /// + /// The full path to this assembly. + internal string FullPath + { + get { return _fullPath; } + set + { + if (_fullPath != value) + { + _fullPath = value; + _fullPathWithoutExtension = null; + _fileNameWithoutExtension = null; + _directoryName = null; + + if (_fullPath == null || _fullPath.Length == 0) + { + _scatterFiles = new string[0]; + _satelliteFiles = new ArrayList(); + _serializationAssemblyFiles = new ArrayList(); + _assembliesConsideredAndRejected = new ArrayList(); + _resolvedSearchPath = String.Empty; + _preUnificationVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); + _isBadImage = false; + _dependenciesFound = false; + _userRequestedSpecificFile = false; + _winMDFile = false; + } + else + { + _winMDFile = _isWinMDFile(_fullPath, _getRuntimeVersion, _fileExists, out _imageRuntimeVersion, out _isManagedWinMDFile); + } + } + } + } + + /// + /// The directory that this assembly lives in. + /// + /// + internal string DirectoryName + { + get + { + if ((_directoryName == null || _directoryName.Length == 0) && (_fullPath != null && _fullPath.Length != 0)) + { + _directoryName = Path.GetDirectoryName(_fullPath); + if (_directoryName.Length == 0) + { + _directoryName = "."; + } + } + return _directoryName; + } + } + + /// + /// The file name without extension. + /// + /// + internal string FileNameWithoutExtension + { + get + { + if ((_fileNameWithoutExtension == null || _fileNameWithoutExtension.Length == 0) && (_fullPath != null && _fullPath.Length != 0)) + { + _fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_fullPath); + } + return _fileNameWithoutExtension; + } + } + + /// + /// The full path to the assembly but without an extension on the file namee + /// + /// + internal string FullPathWithoutExtension + { + get + { + if ((_fullPathWithoutExtension == null || _fullPathWithoutExtension.Length == 0) && (_fullPath != null && _fullPath.Length != 0)) + { + _fullPathWithoutExtension = Path.Combine(DirectoryName, FileNameWithoutExtension); + } + return _fullPathWithoutExtension; + } + } + + + /// + /// This is the HintPath from the source item. This is used to resolve the assembly. + /// + /// The hint path to this assembly. + internal string HintPath + { + get { return _hintPath; } + set { _hintPath = value; } + } + + /// + /// This is the key that was passed in to the reference through the metadata. + /// + /// The value. + internal string AssemblyFolderKey + { + get { return _assemblyFolderKey; } + set { _assemblyFolderKey = value; } + } + + /// + /// Whether this assembly came from the project. If 'false' then this reference was deduced + /// through the reference resolution process. + /// + /// 'true' if this reference is a primary assembly. + internal bool IsPrimary + { + get { return _isPrimary; } + } + + /// + /// Whether or not this reference will be installed on the target machine. + /// + internal bool IsPrerequisite + { + set { _isPrerequisite = value; } + get { return _isPrerequisite; } + } + + /// + /// Whether or not this reference is a redist root. + /// + internal bool? IsRedistRoot + { + set { _isRedistRoot = value; } + get { return _isRedistRoot; } + } + + /// + /// The redist name for this reference (if any) + /// + internal string RedistName + { + set { _redistName = value; } + get { return _redistName; } + } + + + /// + /// The original source item, as passed into the task that is directly associated + /// with this reference. This only applies to "primary" references. + /// + internal ITaskItem PrimarySourceItem + { + get + { + ErrorUtilities.VerifyThrow( + !(_isPrimary && _primarySourceItem == null), "A primary reference must have a primary source item."); + ErrorUtilities.VerifyThrow( + (_isPrimary || _primarySourceItem == null), "Only a primary reference can have a primary source item."); + + return _primarySourceItem; + } + } + + /// + /// If 'true' then the path that this item points to is known to be a bad image. + /// This item shouldn't be passed to compilers and so forth. + /// + /// 'true' if this reference points to a bad image. + internal bool IsBadImage + { + get { return _isBadImage; } + } + + /// + /// If true, then this item conflicted with another item and lost. + /// + internal bool IsConflictVictim + { + get + { + return ConflictVictorName != null; + } + } + + /// + /// Add a conflict victim to this reference + /// + /// + internal void AddConflictVictim(AssemblyNameExtension victim) + { + _conflictVictims.Add(victim); + } + + /// + /// Return the list of conflict victims. + /// + internal AssemblyNameExtension[] GetConflictVictims() + { + return (AssemblyNameExtension[])_conflictVictims.ToArray(typeof(AssemblyNameExtension)); + } + + /// + /// The name of the assembly that won over this reference. + /// + internal AssemblyNameExtension ConflictVictorName + { + get { return _conflictVictorName; } + set { _conflictVictorName = value; } + } + + /// + /// The reason why this reference lost to another reference. + /// + internal ConflictLossReason ConflictLossExplanation + { + get { return _conflictLossReason; } + set { _conflictLossReason = value; } + } + + /// + /// Is the file a WinMDFile. + /// + internal bool IsWinMDFile + { + get { return _winMDFile; } + set { _winMDFile = value; } + } + + /// + /// Is the file a Managed. + /// + internal bool IsManagedWinMDFile + { + get { return _isManagedWinMDFile; } + set { _isManagedWinMDFile = value; } + } + + /// + /// For winmd files there may be an implementation file sitting beside the winmd called the assemblyName.dll + /// We need to attach a piece of metadata to if this is the case. + /// + public string ImplementationAssembly + { + get; + set; + } + + /// + /// ImageRuntime Information + /// + internal string ImageRuntime + { + get { return _imageRuntimeVersion; } + set { _imageRuntimeVersion = value; } + } + + /// + /// Return the list of versions that this reference is unified from. + /// + internal List GetPreUnificationVersions() + { + return new List(_preUnificationVersions.Values); + } + + /// + /// Return the list of versions that this reference is unified from. + /// + internal HashSet RemappedAssemblyNames() + { + return _remappedAssemblyNames; + } + + /// + /// Add a new version number for a version of this reference + /// + internal void AddPreUnificationVersion(String referencePath, Version version, UnificationReason reason) + { + string key = referencePath + version.ToString() + reason.ToString(); + + // Only add a reference, version, and reason once. + UnificationVersion unificationVersion; + if (!_preUnificationVersions.TryGetValue(key, out unificationVersion)) + { + unificationVersion = new UnificationVersion(); + unificationVersion.referenceFullPath = referencePath; + unificationVersion.version = version; + unificationVersion.reason = reason; + _preUnificationVersions[key] = unificationVersion; + } + } + + /// + /// Add the AssemblyNames name we were remapped from + /// + internal void AddRemapping(AssemblyNameExtension remappedFrom, AssemblyNameExtension remappedTo) + { + ErrorUtilities.VerifyThrow(remappedFrom.Immutable, " Remapped from is NOT immutable"); + ErrorUtilities.VerifyThrow(remappedTo.Immutable, " Remapped to is NOT immutable"); + _remappedAssemblyNames.Add(new AssemblyRemapping(remappedFrom, remappedTo)); + } + + /// + /// Whether or not this reference is unified from a different version or versions. + /// + internal bool IsUnified + { + get { return _preUnificationVersions.Count != 0; } + } + + /// + /// Whether this reference should be copied to the local 'bin' dir or not and the reason this flag + /// was set that way. + /// + /// The current copy-local state. + internal CopyLocalState CopyLocal + { + get { return _copyLocalState; } + } + + /// + /// Whether the reference should be CopyLocal. For the reason, see CopyLocalState. + /// + /// 'true' if this reference should be copied. + internal bool IsCopyLocal + { + get + { + return CopyLocalStateUtility.IsCopyLocal(_copyLocalState); + } + } + + /// + /// Whether this reference has already been resolved. + /// Resolved means that the actual filename of the assembly has been found. + /// + /// 'true' if this reference has been resolved. + internal bool IsResolved + { + get { return _fullPath.Length > 0; } + } + + /// + /// Whether this reference can't be resolve. + /// References are usually unresolvable because they weren't found anywhere in the defined search paths. + /// + /// 'true' if this reference is unresolvable. + internal bool IsUnresolvable + { + // If there are any resolution errors then this reference is unresolvable. + get + { + return !IsResolved && _errors.Count > 0; + } + } + + /// + /// Whether or not we still need to find dependencies for this reference. + /// + internal bool DependenciesFound + { + get { return _dependenciesFound; } + set { _dependenciesFound = value; } + } + + /// + /// If the reference has an SDK name metadata this will contain that string. + /// + internal string SDKName + { + get + { + return _sdkName; + } + } + + /// + /// Add some records to the table of assemblies that were considered and then rejected. + /// + internal void AddAssembliesConsideredAndRejected(ArrayList assembliesConsideredAndRejectedToAdd) + { + _assembliesConsideredAndRejected.AddRange(assembliesConsideredAndRejectedToAdd); + } + + /// + /// Returns a collection of strings. Each string is the full path to an assembly that was + /// considered for resolution but then rejected because it wasn't a complete match. + /// + internal ArrayList AssembliesConsideredAndRejected + { + get { return _assembliesConsideredAndRejected; } + } + + /// + /// The searchpath location that the reference was found at. + /// + internal string ResolvedSearchPath + { + get { return _resolvedSearchPath; } + set { _resolvedSearchPath = value; } + } + + /// + /// FrameworkName attribute on this reference + /// + internal FrameworkName FrameworkNameAttribute + { + get { return _frameworkName; } + set + { + _frameworkName = value; + } + } + + /// + /// Make this reference an assembly that is a dependency of 'sourceReference' + /// + /// For example, if 'sourceReference' is MyAssembly.dll then a dependent assembly file + /// might be en\MyAssembly.resources.dll + /// + /// Assembly references do not have their own dependencies, therefore they are + /// + /// The source reference that this reference will be dependent on + internal void MakeDependentAssemblyReference(Reference sourceReference) + { + _copyLocalState = CopyLocalState.Undecided; + + // This is a true dependency, so its not primary. + _isPrimary = false; + + // This is an assembly file, so we'll need to find dependencies later. + DependenciesFound = false; + + // Dependencies must always be specific version. + _wantSpecificVersion = true; + + // Add source items from the original item. + AddSourceItems(sourceReference.GetSourceItems()); + + // Add dependees + AddDependee(sourceReference); + } + + /// + /// Make this reference a primary assembly reference. + /// This is a refrence that is an assembly and is primary. + /// + /// The source item. + /// Whether the version needs to match exactly or loosely. + /// The filename extension that the resulting assembly must have. + internal void MakePrimaryAssemblyReference + ( + ITaskItem sourceItem, + bool wantSpecificVersionValue, + string executableExtension + ) + { + _copyLocalState = CopyLocalState.Undecided; + + // This is a primary reference. + _isPrimary = true; + + // This is the source item (from the list passed into the task) that + // originally created this reference. + _primarySourceItem = sourceItem; + _sdkName = sourceItem.GetMetadata("SDKName"); + + if (executableExtension != null && executableExtension.Length > 0) + { + // Set the expected extension. + SetExecutableExtension(executableExtension); + } + + // The specific version indicator. + _wantSpecificVersion = wantSpecificVersionValue; + + // This is an assembly file, so we'll need to find dependencies later. + DependenciesFound = false; + + // Add source items from the original item. + AddSourceItem(sourceItem); + } + + /// + /// Determine whether the given assembly is an FX assembly. + /// + /// The full path to the assembly. + /// The path to the frameworks. + /// True if this is a frameworks assembly. + internal static bool IsFrameworkFile(string fullPath, string[] frameworkPaths) + { + if (frameworkPaths != null) + { + foreach (string frameworkPath in frameworkPaths) + { + if + ( + String.Compare + ( + frameworkPath, 0, + fullPath, 0, + frameworkPath.Length, + StringComparison.OrdinalIgnoreCase + ) == 0 + ) + { + return true; + } + } + } + return false; + } + + /// + /// Figure out the what the CopyLocal state of given assembly should be. + /// + /// The name of the assembly. + /// The path to the frameworks directory. + /// Like x86 or IA64\AMD64. + /// The CopyLocal state. + internal void SetFinalCopyLocalState + ( + AssemblyNameExtension assemblyName, + string[] frameworkPaths, + ProcessorArchitecture targetProcessorArchitecture, + GetAssemblyRuntimeVersion getRuntimeVersion, + Version targetedRuntimeVersion, + FileExists fileExists, + bool copyLocalDependenciesWhenParentReferenceInGac, + ReferenceTable referenceTable, + CheckIfAssemblyInGac checkIfAssemblyInGac + ) + { + // If this item was unresolvable, then copy-local is false. + if (IsUnresolvable) + { + _copyLocalState = CopyLocalState.NoBecauseUnresolved; + return; + } + + if (EmbedInteropTypes) + { + _copyLocalState = CopyLocalState.NoBecauseEmbedded; + return; + } + + // If this item was a conflict victim, then it should not be copy-local. + if (IsConflictVictim) + { + _copyLocalState = CopyLocalState.NoBecauseConflictVictim; + return; + } + + // If this is a primary reference then see if there's a Private metadata on the source item + if (IsPrimary) + { + bool found; + bool result = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + PrimarySourceItem, + ItemMetadataNames.privateMetadata, + out found + ); + + if (found) + { + if (result) + { + _copyLocalState = CopyLocalState.YesBecauseReferenceItemHadMetadata; + } + else + { + _copyLocalState = CopyLocalState.NoBecauseReferenceItemHadMetadata; + } + return; + } + } + else + { + // This is a dependency. If any primary reference that lead to this dependency + // has Private=false, then this dependency should false too. + bool privateTrueFound = false; + bool privateFalseFound = false; + foreach (DictionaryEntry entry in _sourceItems) + { + bool found; + bool result = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + (ITaskItem)entry.Value, + ItemMetadataNames.privateMetadata, + out found + ); + + if (found) + { + if (result) + { + privateTrueFound = true; + + // Once we hit this once we know there will be no modification to CopyLocal state. + // so we can immediately... + break; + } + else + { + privateFalseFound = true; + } + } + } + + if (privateFalseFound && !privateTrueFound) + { + _copyLocalState = CopyLocalState.NoBecauseReferenceItemHadMetadata; + return; + } + } + + // If the item was determined to be an prereq assembly. + if (IsPrerequisite && !UserRequestedSpecificFile) + { + _copyLocalState = CopyLocalState.NoBecausePrerequisite; + return; + } + + // Items in the frameworks directory shouldn't be copy-local + if (IsFrameworkFile(_fullPath, frameworkPaths)) + { + _copyLocalState = CopyLocalState.NoBecauseFrameworkFile; + return; + } + + if (!FoundInGac.HasValue) + { + // Check to see if the assembly has been found by the GAC resolver. If it as been the FoundInGac is true. + if (_resolvedSearchPath != null && _resolvedSearchPath.Equals(AssemblyResolutionConstants.gacSentinel, StringComparison.OrdinalIgnoreCase)) + { + FoundInGac = true; + } + else + { + bool assemblyIsInGac = checkIfAssemblyInGac(assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fileExists); + FoundInGac = assemblyIsInGac; + } + } + + if (FoundInGac.Value) + { + _copyLocalState = CopyLocalState.NoBecauseReferenceFoundInGAC; + return; + } + + // We are a dependency, check to see if all of our parent references have come from the GAC + if (!IsPrimary && !copyLocalDependenciesWhenParentReferenceInGac) + { + // Did we discover a parent reference which was not found in the GAC + bool foundSourceItemNotInGac = false; + + // Go through all of the parent source items and check to see if they were found in the GAC + foreach (DictionaryEntry entry in _sourceItems) + { + AssemblyNameExtension primaryAssemblyName = referenceTable.GetReferenceFromItemSpec((string)entry.Key); + Reference primaryReference = referenceTable.GetReference(primaryAssemblyName); + + bool assemblyIsInGac = false; + + if (!primaryReference.FoundInGac.HasValue) + { + assemblyIsInGac = checkIfAssemblyInGac(primaryAssemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fileExists); + primaryReference.FoundInGac = assemblyIsInGac; + } + else + { + assemblyIsInGac = primaryReference.FoundInGac.Value; + } + + if (!assemblyIsInGac) + { + foundSourceItemNotInGac = true; + break; + } + } + + // All parent source items were found in the GAC. + if (!foundSourceItemNotInGac) + { + _copyLocalState = CopyLocalState.NoBecauseParentReferencesFoundInGAC; + return; + } + } + + _copyLocalState = CopyLocalState.YesBecauseOfHeuristic; + return; + } + + /// + /// Produce a string representation. + /// + public override string ToString() + { + if (IsResolved) + { + return FullPath; + } + return "*Unresolved*"; + } + + /// + /// There are a number of properties which are set when we generate exclusion lists and it is useful to have this information on the references so that + /// the correct reasons can be logged for these references being in the black list. + /// + internal class ExclusionListProperties + { + #region Fields + /// + /// What is the highest version of an assembly found in the current redist list for the targeted framework + /// + private Version _highestVersionInRedist; + + /// + /// Delegate which will log the reason the assembly was not resolved + /// + private ReferenceTable.LogExclusionReason _exclusionReasonLogDelegate; + + /// + /// What is the target framework moniker of the highest redist list on the system. + /// + private string _highestRedistListMonkier; + + /// + /// Is this reference in an exclusion list + /// + private bool _isInExclusionList; + #endregion + + /// + /// Is this reference in an exclusion list + /// + internal bool IsInExclusionList + { + get { return _isInExclusionList; } + set { _isInExclusionList = value; } + } + + /// + /// What is the highest version of this assembly in the current redist list + /// + internal Version HighestVersionInRedist + { + get { return _highestVersionInRedist; } + set { _highestVersionInRedist = value; } + } + + /// + /// What is the highest versioned redist list on the machine + /// + internal string HighestRedistListMonkier + { + get { return _highestRedistListMonkier; } + set { _highestRedistListMonkier = value; } + } + + /// + /// Delegate which logs the reason for not resolving a reference + /// + internal Microsoft.Build.Tasks.ReferenceTable.LogExclusionReason ExclusionReasonLogDelegate + { + get { return _exclusionReasonLogDelegate; } + set { _exclusionReasonLogDelegate = value; } + } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/ReferenceResolutionException.cs b/src/XMakeTasks/AssemblyDependency/ReferenceResolutionException.cs new file mode 100644 index 00000000000..3121239f7c8 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/ReferenceResolutionException.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Tasks +{ + /// + /// There was a problem resolving this reference into a full file name. + /// + [Serializable] + internal sealed class ReferenceResolutionException : Exception + { + /// + /// Don't allow default construction. + /// + private ReferenceResolutionException() + { + } + + /// + /// Construct + /// + internal ReferenceResolutionException(string message, Exception innerException) + : base(message, innerException) + { + } + + + /// + /// Implement required constructors for serialization + /// + private ReferenceResolutionException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/ReferenceTable.cs b/src/XMakeTasks/AssemblyDependency/ReferenceTable.cs new file mode 100644 index 00000000000..44777011f35 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/ReferenceTable.cs @@ -0,0 +1,3073 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.Versioning; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture; + +namespace Microsoft.Build.Tasks +{ + /// + /// A table of references. + /// + sealed internal class ReferenceTable + { + /// version 4.0 + private static readonly Version s_targetFrameworkVersion_40 = new Version("4.0"); + + /// + /// A mapping of a framework identifier to the most current redist list on the system based on the target framework identifier on the moniker. + /// This is used to determine if an assembly is in a redist list for the framework targeted by the moniker. + /// + private static Dictionary> s_monikerToHighestRedistList = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// The table of references. + /// Key is assemblyName; + /// Value is Reference. + /// + private Dictionary _references = new Dictionary(AssemblyNameComparer.GenericComparer); + + /// The table of remapped assemblies. Used for Unification. + private DependentAssembly[] _remappedAssemblies = new DependentAssembly[0]; + /// If true, then search for dependencies. + private bool _findDependencies = true; + /// + /// Should version be ignored for framework primary references + /// + private bool _ignoreVersionForFrameworkReferences = false; + /// If true, then search for satellite files. + private bool _findSatellites = true; + /// If true, then search for serialization assembly files. + private bool _findSerializationAssemblies = true; + /// If true, then search for related files. + private bool _findRelatedFiles = true; + /// + /// If true, then force framework assembly version check against the target framework version + /// If false, the default behavior is to disable version checks for target framework versions 4.5 and above. + /// + private bool _checkAssemblyVersionAgainstTargetFrameworkVersion = false; + + /// Path to the FX. + private string[] _frameworkPaths; + /// The allowed assembly extensions. + private string[] _allowedAssemblyExtensions; + /// These are companion files that typically travel with assemblies + private string[] _relatedFileExtensions; + /// + /// Locations where sdks are installed. K:SDKName v: Resolved Reference item + /// + private Dictionary _resolvedSDKReferences; + /// Path to installed assembly XML tables. + private InstalledAssemblies _installedAssemblies; + /// Like x86 or IA64\AMD64, the processor architecture being targetted. + private SystemProcessorArchitecture _targetProcessorArchitecture; + /// Delegate used for checking for the existence of a file. + private FileExists _fileExists; + /// Delegate used for checking for the existence of a directory. + private DirectoryExists _directoryExists; + /// Delegate used for getting directories. + private GetDirectories _getDirectories; + /// Delegate used for getting assembly names. + private GetAssemblyName _getAssemblyName; + /// Delegate used for finding dependencies of a file. + private GetAssemblyMetadata _getAssemblyMetadata; + /// Delegate used to get the image runtime version of a file + private GetAssemblyRuntimeVersion _getRuntimeVersion; + /// Delegate to get the base registry key for AssemblyFoldersEx + private OpenBaseKey _openBaseKey; + /// Version of the runtime we are targeting + private Version _targetedRuntimeVersion = null; + + /// + /// Delegate used to get the machineType from the PE header of the dll. + /// + private ReadMachineTypeFromPEHeader _readMachineTypeFromPEHeader; + + /// + /// Is the file a winMD file + /// + private IsWinMDFile _isWinMDFile; + + /// version of the framework targeted by this project + private Version _projectTargetFramework; + + /// + /// Target framework moniker we are targeting. + /// + private FrameworkNameVersioning _targetFrameworkMoniker; + + /// + /// Searchpaths compiled into an array of resolvers. + /// + private Resolver[] _compiledSearchPaths; + + /// + /// Logging helper to allow the logging of meessages from the Reference Table + /// + private TaskLoggingHelper _log; + + /// + /// List of framework directories which are the highest on the machine + /// + private string[] _latestTargetFrameworkDirectories; + + /// + /// List of assemblies which have been excluded from being referenced. + /// + private List _listOfExcludedAssemblies = null; + + /// + /// Should dependencies be set to copy local if the parent reference is in the GAC + /// + private bool _copyLocalDependenciesWhenParentReferenceInGac; + + /// + /// Shoould the framework attribute version mismatch be ignored. + /// + private bool _ignoreFrameworkAttributeVersionMismatch; + + /// + /// Delegate to determine if an assembly name is in the GAC. + /// + private CheckIfAssemblyInGac _checkIfAssemblyIsInGac; + + /// + /// Build engine + /// + private IBuildEngine _buildEngine; + + /// + /// Should a warning or error be emitted on architecture mismatch + /// + private WarnOrErrorOnTargetArchitectureMismatchBehavior _warnOrErrorOnTargetArchitectureMismatch = WarnOrErrorOnTargetArchitectureMismatchBehavior.Warning; + + /// + /// When we exclude an assembly from resolution because it is part of out exclusion list we need to let the user know why this is. + /// There can be a number of reasons each for un-resolving a reference, these reasons are encapsulated by a different black list. We need to log a specific message + /// depending on which black list we have found the offending assembly in. This delegate allows one to tie a set of logging messages to a black list so that when we + /// discover an assembly in the black list we can log the correct message. + /// + internal delegate void LogExclusionReason(bool displayPrimaryReferenceMessage, AssemblyNameExtension assemblyName, Reference reference, ITaskItem referenceItem, string targetedFramework); + + // Offset to the PE header + private const int PEOFFSET = 0x3c; + + // PEHeader + private const int PEHEADER = 0x00004550; + + /// + /// Construct. + /// + /// If true, then search for dependencies. + /// If true, then search for satellite files. + /// If true, then search for serialization assembly files. + /// If true, then search for related files. + /// Paths to search for dependent assemblies on. + /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. + /// Resolved sdk items + /// Path to the FX. + /// Installed assembly XML tables. + /// Like x86 or IA64\AMD64, the processor architecture being targetted. + /// Delegate used for checking for the existence of a file. + /// Delegate used for files. + /// Delegate used for getting directories. + /// Delegate used for getting assembly names. + /// Delegate used for finding dependencies of a file. + /// Used to get registry subkey names. + /// Used to get registry default values. + internal ReferenceTable + ( + IBuildEngine buildEngine, + bool findDependencies, + bool findSatellites, + bool findSerializationAssemblies, + bool findRelatedFiles, + string[] searchPaths, + string[] allowedAssemblyExtensions, + string[] relatedFileExtensions, + string[] candidateAssemblyFiles, + ITaskItem[] resolvedSDKItems, + string[] frameworkPaths, + InstalledAssemblies installedAssemblies, + System.Reflection.ProcessorArchitecture targetProcessorArchitecture, + FileExists fileExists, + DirectoryExists directoryExists, + GetDirectories getDirectories, + GetAssemblyName getAssemblyName, + GetAssemblyMetadata getAssemblyMetadata, + GetRegistrySubKeyNames getRegistrySubKeyNames, + GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, + OpenBaseKey openBaseKey, + GetAssemblyRuntimeVersion getRuntimeVersion, + Version targetedRuntimeVersion, + Version projectTargetFramework, + FrameworkNameVersioning targetFrameworkMoniker, + TaskLoggingHelper log, + string[] latestTargetFrameworkDirectories, + bool copyLocalDependenciesWhenParentReferenceInGac, + CheckIfAssemblyInGac checkIfAssemblyIsInGac, + IsWinMDFile isWinMDFile, + bool ignoreVersionForFrameworkReferences, + ReadMachineTypeFromPEHeader readMachineTypeFromPEHeader, + WarnOrErrorOnTargetArchitectureMismatchBehavior warnOrErrorOnTargetArchitectureMismatch, + bool ignoreFrameworkAttributeVersionMismatch, + bool unresolveFrameworkAssembliesFromHigherFrameworks + ) + { + _buildEngine = buildEngine; + _log = log; + _findDependencies = findDependencies; + _findSatellites = findSatellites; + _findSerializationAssemblies = findSerializationAssemblies; + _findRelatedFiles = findRelatedFiles; + _frameworkPaths = frameworkPaths; + _allowedAssemblyExtensions = allowedAssemblyExtensions; + _relatedFileExtensions = relatedFileExtensions; + _installedAssemblies = installedAssemblies; + _targetProcessorArchitecture = targetProcessorArchitecture; + _fileExists = fileExists; + _directoryExists = directoryExists; + _getDirectories = getDirectories; + _getAssemblyName = getAssemblyName; + _getAssemblyMetadata = getAssemblyMetadata; + _getRuntimeVersion = getRuntimeVersion; + _projectTargetFramework = projectTargetFramework; + _targetedRuntimeVersion = targetedRuntimeVersion; + _openBaseKey = openBaseKey; + _targetFrameworkMoniker = targetFrameworkMoniker; + _latestTargetFrameworkDirectories = latestTargetFrameworkDirectories; + _copyLocalDependenciesWhenParentReferenceInGac = copyLocalDependenciesWhenParentReferenceInGac; + _checkIfAssemblyIsInGac = checkIfAssemblyIsInGac; + _isWinMDFile = isWinMDFile; + _readMachineTypeFromPEHeader = readMachineTypeFromPEHeader; + _warnOrErrorOnTargetArchitectureMismatch = warnOrErrorOnTargetArchitectureMismatch; + _ignoreFrameworkAttributeVersionMismatch = ignoreFrameworkAttributeVersionMismatch; + + // Set condition for when to check assembly version against the target framework version + _checkAssemblyVersionAgainstTargetFrameworkVersion = unresolveFrameworkAssembliesFromHigherFrameworks || ((_projectTargetFramework ?? ReferenceTable.s_targetFrameworkVersion_40) <= ReferenceTable.s_targetFrameworkVersion_40); + + // Convert the list of installed SDK's to a dictionary for faster lookup + _resolvedSDKReferences = new Dictionary(StringComparer.OrdinalIgnoreCase); + _ignoreVersionForFrameworkReferences = ignoreVersionForFrameworkReferences; + + + if (resolvedSDKItems != null) + { + foreach (ITaskItem resolvedSDK in resolvedSDKItems) + { + string sdkName = resolvedSDK.GetMetadata("SDKName"); + + if (sdkName.Length > 0) + { + if (!_resolvedSDKReferences.ContainsKey(sdkName)) + { + _resolvedSDKReferences.Add(sdkName, resolvedSDK); + } + else + { + _resolvedSDKReferences[sdkName] = resolvedSDK; + } + } + } + } + + // Compile searchpaths into fast resolver array. + _compiledSearchPaths = AssemblyResolution.CompileSearchPaths + ( + buildEngine, + searchPaths, + candidateAssemblyFiles, + targetProcessorArchitecture, + frameworkPaths, + fileExists, + getAssemblyName, + getRegistrySubKeyNames, + getRegistrySubKeyDefaultValue, + openBaseKey, + installedAssemblies, + getRuntimeVersion, + targetedRuntimeVersion + ); + } + + /// + /// Set of resolvers the reference table uses. + /// + internal Resolver[] Resolvers + { + get { return _compiledSearchPaths; } + } + + /// + /// Get a table of all vertices. + /// + /// + internal Dictionary References + { + get + { + return _references; + } + } + + /// + /// If assemblies have been marked for exclusion this contains the list of their full names + /// This may be null + /// + internal List ListOfExcludedAssemblies + { + get + { + return _listOfExcludedAssemblies; + } + } + + /// + /// Adds a reference to the table. + /// + /// The assembly name to be used as a key. + /// The reference to add. + internal void AddReference(AssemblyNameExtension assemblyName, Reference reference) + { + ErrorUtilities.VerifyThrow(assemblyName.Name != null, "Got an empty assembly name."); + if (_references.ContainsKey(assemblyName)) + { + Reference referenceGoingToBeReplaced = _references[assemblyName]; + foreach (AssemblyRemapping pair in referenceGoingToBeReplaced.RemappedAssemblyNames()) + { + reference.AddRemapping(pair.From, pair.To); + } + } + + _references[assemblyName] = reference; + } + + + /// + /// Find the reference that corresponds to the given path. + /// + /// The assembly name to find the reference for. + /// 'null' if no reference existed. + internal Reference GetReference(AssemblyNameExtension assemblyName) + { + ErrorUtilities.VerifyThrow(assemblyName.Name != null, "Got an empty assembly name."); + Reference referenceToReturn = null; + _references.TryGetValue(assemblyName, out referenceToReturn); + return referenceToReturn; + } + + /// + /// Give an assembly file name, adjust a Reference to match it. + /// + /// The reference to work on + /// The path to the assembly file. + /// The AssemblyName of assemblyFileName + private AssemblyNameExtension NameAssemblyFileReference + ( + Reference reference, + string assemblyFileName + ) + { + AssemblyNameExtension assemblyName = null; + + if (!Path.IsPathRooted(assemblyFileName)) + { + reference.FullPath = Path.GetFullPath(assemblyFileName); + } + else + { + reference.FullPath = assemblyFileName; + } + + try + { + if (_directoryExists(assemblyFileName)) + { + assemblyName = new AssemblyNameExtension("*directory*"); + + reference.AddError + ( + new ReferenceResolutionException + ( + ResourceUtilities.FormatResourceString("General.ExpectedFileGotDirectory", reference.FullPath), + null + ) + ); + reference.FullPath = String.Empty; + } + else + { + if (_fileExists(assemblyFileName)) + { + assemblyName = _getAssemblyName(assemblyFileName); + if (assemblyName != null) + { + reference.ResolvedSearchPath = assemblyFileName; + } + } + + if (assemblyName == null) + { + reference.AddError + ( + new DependencyResolutionException(ResourceUtilities.FormatResourceString("General.ExpectedFileMissing", reference.FullPath), null) + ); + } + } + } + catch (System.BadImageFormatException e) + { + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + catch (UnauthorizedAccessException e) + { + // If this isn't a valid assembly, then record the exception and continue on + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + + // If couldn't resolve the assemly name then just use the simple name extracted from + // the file name. + if (assemblyName == null) + { + string simpleName = Path.GetFileNameWithoutExtension(assemblyFileName); + assemblyName = new AssemblyNameExtension(simpleName); + } + + return assemblyName; + } + + /// + /// Given a list of task items, add them all to this table and make them the only primary items. + /// + /// The task items which contain file names to add. + /// The task items which contain fusion names to add. + /// Exceptions encountered while setting primary items. Exceptions are logged, but it doesn't stop the resolution process. + private void SetPrimaryItems + ( + ITaskItem[] referenceAssemblyFiles, + ITaskItem[] referenceAssemblyNames, + ArrayList exceptions + ) + { + // Loop over the referenceAssemblyFiles provided and add each one that doesn't exist. + // Set the primary flag to 'true'. + if (referenceAssemblyFiles != null) + { + for (int i = 0; i < referenceAssemblyFiles.Length; ++i) + { + SetPrimaryFileItem(referenceAssemblyFiles[i]); + } + } + + // Loop over the referenceAssemblyNames provided and add each one that doesn't exist. + // Set the primary flag to 'true'. + if (referenceAssemblyNames != null) + { + for (int i = 0; i < referenceAssemblyNames.Length; ++i) + { + Exception e = SetPrimaryAssemblyReferenceItem(referenceAssemblyNames[i]); + + if (e != null) + { + exceptions.Add(e); + } + } + } + } + + /// + /// Given an item that refers to a assembly name, make it a primary reference. + /// + /// The task item which contain fusion names to add. + /// Resulting exception containing resolution failure details, if any: too costly to throw it. + private Exception SetPrimaryAssemblyReferenceItem + ( + ITaskItem referenceAssemblyName + ) + { + // Get the desired executable extension. + string executableExtension = referenceAssemblyName.GetMetadata(ItemMetadataNames.executableExtension); + + // Get the assembly name, if possible. + string rawFileNameCandidate = referenceAssemblyName.ItemSpec; + AssemblyNameExtension assemblyName = null; + string itemSpec = referenceAssemblyName.ItemSpec; + string fusionName = referenceAssemblyName.GetMetadata(ItemMetadataNames.fusionName); + bool metadataFound = false; + bool result = MetadataConversionUtilities.TryConvertItemMetadataToBool(referenceAssemblyName, ItemMetadataNames.IgnoreVersionForFrameworkReference, out metadataFound); + bool ignoreVersionForFrameworkReference = false; + + if (metadataFound) + { + ignoreVersionForFrameworkReference = result; + } + else + { + ignoreVersionForFrameworkReference = _ignoreVersionForFrameworkReferences; + } + + TryConvertToAssemblyName(itemSpec, fusionName, ref assemblyName); + + // Figure out the specific version value. + bool foundSpecificVersionMetadata = false; + bool wantSpecificVersion = MetadataConversionUtilities.TryConvertItemMetadataToBool(referenceAssemblyName, ItemMetadataNames.specificVersion, out foundSpecificVersionMetadata); + + bool isSimpleName = (assemblyName != null && assemblyName.IsSimpleName); + + // Create the reference. + Reference reference = new Reference(_isWinMDFile, _fileExists, _getRuntimeVersion); + reference.MakePrimaryAssemblyReference(referenceAssemblyName, wantSpecificVersion, executableExtension); + + // Escape simple names. + // 1) If the itemSpec for the task is already a simple name + // 2) We have found the metadata and it is specifically set to false + if (assemblyName != null && (isSimpleName || (foundSpecificVersionMetadata && !wantSpecificVersion))) + { + assemblyName = new AssemblyNameExtension + ( + AssemblyNameExtension.EscapeDisplayNameCharacters(assemblyName.Name) + ); + + isSimpleName = assemblyName.IsSimpleName; + } + + // Set the HintPath if there is one. + reference.HintPath = referenceAssemblyName.GetMetadata(ItemMetadataNames.hintPath); + + if (assemblyName != null && !wantSpecificVersion && !isSimpleName && reference.HintPath.Length == 0) + { + // Check to see if the assemblyname is in the framework list just use that fusion name + if (_installedAssemblies != null && ignoreVersionForFrameworkReference) + { + AssemblyEntry entry = _installedAssemblies.FindHighestVersionInRedistList(assemblyName); + if (entry != null) + { + assemblyName = entry.AssemblyNameExtension.Clone(); + } + } + } + + if (assemblyName != null && _installedAssemblies != null && !wantSpecificVersion && reference.HintPath.Length == 0) + { + AssemblyNameExtension remappedExtension = _installedAssemblies.RemapAssemblyExtension(assemblyName); + + if (remappedExtension != null) + { + reference.AddRemapping(assemblyName.CloneImmutable(), remappedExtension.CloneImmutable()); + assemblyName = remappedExtension; + } + } + + + // Embed Interop Types aka "NOPIAs" support is not available for Fx < 4.0 + // So, we just ignore this setting on down-level platforms + if (_projectTargetFramework != null && _projectTargetFramework >= s_targetFrameworkVersion_40) + { + reference.EmbedInteropTypes = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + referenceAssemblyName, + ItemMetadataNames.embedInteropTypes + ); + } + + // Set the AssemblyFolderKey if there is one. + reference.AssemblyFolderKey = referenceAssemblyName.GetMetadata(ItemMetadataNames.assemblyFolderKey); + + // It's possible, especially in cases where the fusion name was passed in through the item + // that we'll have a better (more information) fusion name once we know the assembly path. + try + { + ResolveReference(assemblyName, rawFileNameCandidate, reference); + + if (reference.IsResolved) + { + AssemblyNameExtension possiblyBetterAssemblyName = null; + + try + { + // This may throw if, for example, the culture embedded in the assembly's manifest + // is not recognised by AssemblyName.GetAssemblyName + possiblyBetterAssemblyName = _getAssemblyName(reference.FullPath); + } + catch (ArgumentException) + { + // Give up trying to get a better name + possiblyBetterAssemblyName = null; + } + + // Use the better name if it exists. + if (possiblyBetterAssemblyName != null && possiblyBetterAssemblyName.Name != null) + { + assemblyName = possiblyBetterAssemblyName; + } + } + } + catch (BadImageFormatException e) + { + // If this isn't a valid assembly, then record the exception and continue on + reference.AddError(new BadImageReferenceException(e.Message, e)); + } + catch (FileNotFoundException e) // Why isn't this covered in NotExpectedException? + { + reference.AddError(new BadImageReferenceException(e.Message, e)); + } + catch (FileLoadException e) + { + // Managed assembly was found but could not be loaded. + reference.AddError(new BadImageReferenceException(e.Message, e)); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + reference.AddError(new BadImageReferenceException(e.Message, e)); + } + + // If there is still no assembly name then this is a case where the assembly metadata + // just doesn't contain an assembly name. We want to try to tolerate this because + // mscorlib.dll (sometimes?) doesn't contain an assembly name. + if (assemblyName == null) + { + if (!reference.IsResolved) + { + // The file doesn't exist and the reference was unresolved, there's nothing we can do at this point. + // Return, rather than throw, the exception, as in some situations it can happen thousands of times. + return new InvalidReferenceAssemblyNameException(referenceAssemblyName.ItemSpec); + } + + assemblyName = new AssemblyNameExtension + ( + AssemblyNameExtension.EscapeDisplayNameCharacters(reference.FileNameWithoutExtension) + ); + } + + // Check to see if this is a prereq assembly. + if (_installedAssemblies == null) + { + reference.IsPrerequisite = false; + } + else + { + Version unifiedVersion = null; + bool isPrerequisite = false; + bool? isRedistRoot = null; + string redistName = null; + + _installedAssemblies.GetInfo + ( + assemblyName, + out unifiedVersion, + out isPrerequisite, + out isRedistRoot, + out redistName + ); + + reference.IsPrerequisite = isPrerequisite; + reference.IsRedistRoot = isRedistRoot; + reference.RedistName = redistName; + } + + AddReference(assemblyName, reference); + + return null; + } + + /// + /// Attempts to convert an itemSpec and fusionName into an assembly name. + /// AssemblyName is left unchanged if conversion wasn't possible. + /// + /// + /// + /// + private static void TryConvertToAssemblyName(string itemSpec, string fusionName, ref AssemblyNameExtension assemblyName) + { + // FusionName is used if available. + string finalName = fusionName; + if (finalName == null || finalName.Length == 0) + { + // Otherwise, its itemSpec. + finalName = itemSpec; + } + + bool pathRooted = false; + try + { + pathRooted = Path.IsPathRooted(finalName); + } + catch (ArgumentException) + { + /* Eat this because it has invalid chars in to and cannot be a path, maybe it can be parsed as a fusion name.*/ + } + + if (!pathRooted) + { + // Now try to convert to an AssemblyName. + try + { + assemblyName = new AssemblyNameExtension(finalName, true /*throw if not valid*/); + } + catch (System.IO.FileLoadException) + { + // Not a valid AssemblyName. Maybe its a file name. + TryGatherAssemblyNameEssentials(finalName, ref assemblyName); + return; + } + } + else + { + // Maybe the string has a fusion name inside of it. + TryGatherAssemblyNameEssentials(finalName, ref assemblyName); + } + } + + /// + /// Given a string that may be a fusion name, try to gather the four essential properties: + /// Name + /// Version + /// PublicKeyToken + /// Culture + /// + /// + /// + private static void TryGatherAssemblyNameEssentials(string fusionName, ref AssemblyNameExtension assemblyName) + { + int firstComma = fusionName.IndexOf(','); + if (firstComma == -1) + { + return; + } + string name = fusionName.Substring(0, firstComma); + + string version = null; + string publicKeyToken = null; + string culture = null; + TryGetAssemblyNameComponent(fusionName, "Version", ref version); + TryGetAssemblyNameComponent(fusionName, "PublicKeyToken", ref publicKeyToken); + TryGetAssemblyNameComponent(fusionName, "Culture", ref culture); + + if (version == null || publicKeyToken == null || culture == null) + { + return; + } + + string newFusionName = String.Format(CultureInfo.InvariantCulture, + "{0}, Version={1}, Culture={2}, PublicKeyToken={3}", + name, version, culture, publicKeyToken); + + // Now try to convert to an AssemblyName. + try + { + assemblyName = new AssemblyNameExtension(newFusionName, true /* throw if not valid */); + } + catch (System.IO.FileLoadException) + { + // Not a valid AssemblyName. Maybe its a file name. + // TryGatherAssemblyNameEssentials + return; + } + } + + /// + /// Attempt to get one field out of an assembly name. + /// + /// + /// + /// + private static void TryGetAssemblyNameComponent(string fusionName, string component, ref string value) + { + int position = fusionName.IndexOf(component + "=", StringComparison.Ordinal); + if (position == -1) + { + return; + } + position += component.Length + 1; + int nextDelimiter = fusionName.IndexOfAny(new char[] { ',', ' ' }, position); + if (nextDelimiter == -1) + { + value = fusionName.Substring(position); + } + else + { + value = fusionName.Substring(position, nextDelimiter - position); + } + } + + /// + /// Given an item that refers to a file name, make it a primary reference. + /// + /// + private void SetPrimaryFileItem(ITaskItem referenceAssemblyFile) + { + try + { + // Create the reference. + Reference reference = new Reference(_isWinMDFile, _fileExists, _getRuntimeVersion); + + bool hasSpecificVersionMetadata = MetadataConversionUtilities.TryConvertItemMetadataToBool(referenceAssemblyFile, ItemMetadataNames.specificVersion); + reference.MakePrimaryAssemblyReference + ( + referenceAssemblyFile, + hasSpecificVersionMetadata, + Path.GetExtension(referenceAssemblyFile.ItemSpec) + ); + + AssemblyNameExtension assemblyName = NameAssemblyFileReference + ( + reference, + referenceAssemblyFile.ItemSpec // Contains the assembly file name. + ); + + // Embed Interop Types aka "NOPIAs" support is not available for Fx < 4.0 + // So, we just ignore this setting on down-level platforms + if (_projectTargetFramework >= s_targetFrameworkVersion_40) + { + reference.EmbedInteropTypes = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + referenceAssemblyFile, + ItemMetadataNames.embedInteropTypes + ); + } + + AddReference(assemblyName, reference); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + // Invalid file path. + throw new InvalidParameterValueException("AssemblyFiles", referenceAssemblyFile.ItemSpec, e.Message); + } + } + + /// + /// Find related files like .pdbs and .xmls + /// + /// Path to the parent assembly. + /// The reference to the parent assembly. + private void FindRelatedFiles + ( + Reference reference + ) + { + string baseName = reference.FullPathWithoutExtension; + + // Look for companion files like pdbs and xmls that ride along with + // assemblies. + foreach (string companionExtension in _relatedFileExtensions) + { + string companionFile = baseName + companionExtension; + + if (_fileExists(companionFile)) + { + reference.AddRelatedFileExtension(companionExtension); + } + } + + // Native Winmd files may have a companion dll beside it. + // If this is not a primary reference or the implementation metadata is not set on the item we need to set the implmentation metadata. + if (reference.IsWinMDFile && (!reference.IsPrimary || String.IsNullOrEmpty(reference.PrimarySourceItem.GetMetadata(ItemMetadataNames.winmdImplmentationFile))) && !reference.IsManagedWinMDFile) + { + string companionFile = baseName + ".dll"; + + if (_fileExists(companionFile)) + { + reference.ImplementationAssembly = companionFile; + } + } + } + + /// + /// Find satellite assemblies. + /// + /// Directory of the parrent assembly. + /// Path to the parent assembly. + /// The reference to the parent assembly. + private void FindSatellites + ( + Reference reference + ) + { + try + { + // If the directory doesn't exist (which is possible in the situation + // where we were passed in a pre-resolved reference from a P2P reference + // that hasn't actually been built yet), then GetDirectories will throw. + // Avoid that by just short-circuiting here. + if (!_directoryExists(reference.DirectoryName)) + { + return; + } + + string[] subDirectories = _getDirectories(reference.DirectoryName, "*."); + string sateliteFilename = reference.FileNameWithoutExtension + ".resources.dll"; + + foreach (string subDirectory in subDirectories) + { + // Is there a candidate satellite in that folder? + string cultureName = Path.GetFileName(subDirectory); + + if (CultureStringUtilities.IsValidCultureString(cultureName)) + { + string satelliteAssembly = Path.Combine(subDirectory, sateliteFilename); + if (_fileExists(satelliteAssembly)) + { + // This is valid satellite assembly. + reference.AddSatelliteFile(Path.Combine(cultureName, sateliteFilename)); + } + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + if (_log != null) + { + _log.LogErrorFromResources("ResolveAssemblyReference.ProblemFindingSatelliteAssemblies", reference.FullPath, e.Message); + } + } + } + + /// + /// Find serialization assemblies. + /// + /// Directory of the parrent assembly. + /// Path to the parent assembly. + /// The reference to the parent assembly. + private void FindSerializationAssemblies + ( + Reference reference + ) + { + // If the directory doesn't exist (which is possible in the situation + // where we were passed in a pre-resolved reference from a P2P reference + // that hasn't actually been built yet), then GetDirectories will throw. + // Avoid that by just short-circuiting here. + if (!_directoryExists(reference.DirectoryName)) + { + return; + } + + string serializationAssemblyFilename = reference.FileNameWithoutExtension + ".XmlSerializers.dll"; + string serializationAssemblyPath = Path.Combine(reference.DirectoryName, serializationAssemblyFilename); + if (_fileExists(serializationAssemblyPath)) + { + // This is valid serialization assembly. + reference.AddSerializationAssemblyFile(serializationAssemblyFilename); + } + } + + /// + /// Get unified dependencies and scatter files for a reference. + /// + /// + /// + /// + private void GetUnifiedAssemblyMetadata + ( + Reference reference, + out IEnumerable unifiedDependencies, + out string[] scatterFiles + ) + { + // Shortcut if this is a prereq file--don't find dependencies. + // We also don't want to look for dependencies if we already know + // this assembly is a bad image. + if (reference.IsPrerequisite || reference.IsBadImage) + { + unifiedDependencies = null; + scatterFiles = null; + return; + } + + AssemblyNameExtension[] dependentAssemblies = null; + FrameworkName frameworkName = null; + _getAssemblyMetadata + ( + reference.FullPath, + out dependentAssemblies, + out scatterFiles, + out frameworkName + ); + + reference.FrameworkNameAttribute = frameworkName; + + List dependencies = new List(); + + if (dependentAssemblies != null && dependentAssemblies.Length > 0) + { + // Re-map immediately so that to the sytem we actually got the remapped version when reading the manifest. + for (int i = 0; i < dependentAssemblies.Length; i++) + { + if (_installedAssemblies != null) + { + // This will return a clone of the remapped assemblyNameExtension so its ok to party on it. + AssemblyNameExtension remappedExtension = _installedAssemblies.RemapAssemblyExtension(dependentAssemblies[i]); + if (remappedExtension != null) + { + AssemblyNameExtension originalExtension = dependentAssemblies[i]; + AssemblyNameExtension existingExtension = dependencies.Find(x => x.Equals(remappedExtension)); + if (existingExtension != null) + { + existingExtension.AddRemappedAssemblyName(originalExtension.CloneImmutable()); + continue; + } + else + { + dependentAssemblies[i] = remappedExtension; + dependentAssemblies[i].AddRemappedAssemblyName(originalExtension.CloneImmutable()); + } + } + } + + // Assemblies which reference WinMD files sometimes will have references to mscorlib version 255.255.255 which is invalid. For this reason + // We will remove the dependency to mscorlib from the list of dependencies so it is not used for resolution or unification. + bool isMscorlib = IsPseudoAssembly(dependentAssemblies[i].Name); + + if (!isMscorlib || (isMscorlib && dependentAssemblies[i].Version.Major != 255)) + { + dependencies.Add(dependentAssemblies[i]); + } + } + + dependentAssemblies = dependencies.ToArray(); + } + + unifiedDependencies = GetUnifiedAssemblyNames(dependentAssemblies); + } + + /// + /// Given an enumerator of pre-unified assembly names, return an enumerator of unified + /// assembly names. + /// + /// + /// + private IEnumerable GetUnifiedAssemblyNames + ( + IEnumerable preUnificationAssemblyNames + ) + { + foreach (AssemblyNameExtension preUnificationAssemblyName in preUnificationAssemblyNames) + { + string name = preUnificationAssemblyName.Name; + // First, unify the assembly name so that we're dealing with the right version. + // Not AssemblyNameExtension because we're going to write to it. + AssemblyNameExtension dependentAssembly = new AssemblyNameExtension((AssemblyName)preUnificationAssemblyName.AssemblyName.Clone()); + + Version unifiedVersion; + bool isPrerequisite; + bool? isRedistRoot; + string redistName; + + UnificationReason unificationReason; + bool isUnified = UnifyAssemblyNameVersions(dependentAssembly, out unifiedVersion, out unificationReason, out isPrerequisite, out isRedistRoot, out redistName); + dependentAssembly.ReplaceVersion(unifiedVersion); + + yield return new UnifiedAssemblyName(preUnificationAssemblyName, dependentAssembly, isUnified, unificationReason, isPrerequisite, isRedistRoot, redistName); + } + } + + /// + /// Find references and scatter files defined for the given assembly. + /// + /// The reference to the parent assembly. + /// New references are added to this list. + /// Entries that should be removed from the list. + private void FindDependenciesAndScatterFiles + ( + Reference reference, + ArrayList newEntries + ) + { + // Before checking for dependencies check to see if the reference itself exists. + // Even though to get to this point the reference must be resolved + // the reference may not exist on disk if the reference is a project to project reference. + if (!_fileExists(reference.FullPath)) + { + reference.AddError + ( + new DependencyResolutionException(ResourceUtilities.FormatResourceString("General.ExpectedFileMissing", reference.FullPath), null) + ); + + return; + } + + try + { + IEnumerable unifiedDependencies = null; + string[] scatterFiles = null; + GetUnifiedAssemblyMetadata(reference, out unifiedDependencies, out scatterFiles); + reference.AttachScatterFiles(scatterFiles); + + // If no dependencies then fall out. + if (unifiedDependencies == null) + { + return; + } + + foreach (UnifiedAssemblyName unifiedDependency in unifiedDependencies) + { + // Now, see if it has already been found. + Reference existingReference = GetReference(unifiedDependency.PostUnified); + + if (existingReference == null) + { + // This is valid reference. + Reference newReference = new Reference(_isWinMDFile, _fileExists, _getRuntimeVersion); + + newReference.MakeDependentAssemblyReference(reference); + if (unifiedDependency.IsUnified) + { + newReference.AddPreUnificationVersion(reference.FullPath, unifiedDependency.PreUnified.Version, unifiedDependency.UnificationReason); + } + + foreach (AssemblyNameExtension remappedFromName in unifiedDependency.PreUnified.RemappedFromEnumerator) + { + newReference.AddRemapping(remappedFromName, unifiedDependency.PreUnified.CloneImmutable()); + } + + newReference.IsPrerequisite = unifiedDependency.IsPrerequisite; + + DictionaryEntry newEntry = new DictionaryEntry(unifiedDependency.PostUnified, newReference); + + newEntries.Add(newEntry); + } + else + { + // If it already existed then just append the source items. + if (existingReference == reference) + { + // This means the assembly depends on itself. This seems to be legal so we allow allow it. + // I don't think this rises to the level of a warning for the user because fusion handles + // this case gracefully. + } + else + { + // Now, add new information to the reference. + existingReference.AddSourceItems(reference.GetSourceItems()); + existingReference.AddDependee(reference); + + if (unifiedDependency.IsUnified) + { + existingReference.AddPreUnificationVersion(reference.FullPath, unifiedDependency.PreUnified.Version, unifiedDependency.UnificationReason); + } + + existingReference.IsPrerequisite = unifiedDependency.IsPrerequisite; + } + + foreach (AssemblyNameExtension remappedFromName in unifiedDependency.PreUnified.RemappedFromEnumerator) + { + existingReference.AddRemapping(remappedFromName, unifiedDependency.PreUnified.CloneImmutable()); + } + } + } + } + catch (FileNotFoundException e) // Why isn't this covered in NotExpectedException? + { + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + catch (FileLoadException e) + { + // Managed assembly was found but could not be loaded. + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + catch (BadImageFormatException e) + { + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + catch (System.Runtime.InteropServices.COMException e) + { + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + } + + /// + /// Mscorlib is not a real managed assembly. It is seen both with and without metadata. + /// We assume that the correct mscorlib is on the target platform. + /// + /// + /// + private static bool IsPseudoAssembly(string name) + { + return String.Compare(name, "mscorlib", StringComparison.OrdinalIgnoreCase) == 0; + } + + + /// + /// Based on the set of parent assemblies we want to add their directories to the list of resolvers so that + /// if the dependency is sitting beside the assembly which requires it then we will resolve the assembly from that location first. + /// + /// The only time we do not want to do this is if the parent assembly came from the GAC or AssemblyFoldersEx then we want the assembly + /// to be found using those resolvers so that our GAC and AssemblyFolders checks later on will work on those assemblies. + /// + internal static void CalcuateParentAssemblyDirectories(Hashtable parentReferenceFolderHash, List parentReferenceFolders, Reference parentReference) + { + string parentReferenceFolder = parentReference.DirectoryName; + string parentReferenceResolvedSearchPath = parentReference.ResolvedSearchPath; + + bool parentReferenceResolvedFromGAC = false; + bool parentReferenceResolvedFromAssemblyFolders = false; + if (!String.IsNullOrEmpty(parentReferenceResolvedSearchPath)) + { + parentReferenceResolvedFromGAC = parentReferenceResolvedSearchPath.Equals(AssemblyResolutionConstants.gacSentinel, StringComparison.OrdinalIgnoreCase); + parentReferenceResolvedFromAssemblyFolders = parentReferenceResolvedSearchPath.Equals(AssemblyResolutionConstants.assemblyFoldersSentinel, StringComparison.OrdinalIgnoreCase); + } + + // Only add the parent folder as a search location if we have not added it to the list yet and the parent reference has not been resolved from the GAC or AssemblyFolders + // If the reference has been resolved from one of these locations we want the dependency to be found using the GAC or AssemblyFolder resolver rather than the directory resolver + // This way the dependency is marked with the correct search path "GAC" or "AssemblyFolder" rather than "c:\xxxxxx" which prevents our GAC/AssemblyFolder check from working + if (!parentReferenceFolderHash.ContainsKey(parentReferenceFolder) && !parentReferenceResolvedFromGAC && !parentReferenceResolvedFromAssemblyFolders) + { + parentReferenceFolderHash[parentReferenceFolder] = String.Empty; + parentReferenceFolders.Add(parentReferenceFolder); + } + } + + /// + /// Given an unresolved reference (one that we don't know the full name for yet), figure out the + /// full name. Should only be called on references that haven't been resolved yet--otherwise, its + /// a perf problem. + /// + /// The fusion name for this reference. + /// The file name to match if {RawFileName} is seen. (May be null). + /// The reference object. + private void ResolveReference + ( + AssemblyNameExtension assemblyName, + string rawFileNameCandidate, + Reference reference + ) + { + // Now, resolve this reference. + string resolvedPath = null; + string resolvedSearchPath = String.Empty; + bool userRequestedSpecificFile = false; + + // A list of assemblies that might have been matches but weren't + ArrayList assembliesConsideredAndRejected = new ArrayList(); + + // First, look for the dependency in the parents' directories. Unless they are resolved from the GAC or assemblyFoldersEx then + // we should make sure we use the GAC and assemblyFolders resolvers themserves rather than a directory resolver to find the reference.\ + // this way we dont get assemblies pulled from the GAC or AssemblyFolders but dont have the marking that they were pulled form there. + Hashtable parentReferenceFolderHash = new Hashtable(StringComparer.OrdinalIgnoreCase); + List parentReferenceFolders = new List(); + foreach (Reference parentReference in reference.GetDependees()) + { + CalcuateParentAssemblyDirectories(parentReferenceFolderHash, parentReferenceFolders, parentReference); + } + + // Build the set of resolvers. + List jaggedResolvers = new List(); + + // If a reference has an SDK name on it then we must ONLY resolve it from the SDK which matches the SDKName on the refernce metadata + // this is to support the case where a single reference assembly is selected from the SDK. + // If a reference has the SDKName metadata on it then we will only search using a single resolver, that is the InstalledSDKResolver. + if (reference.SDKName.Length > 0) + { + jaggedResolvers.Add(new Resolver[] { new InstalledSDKResolver(_resolvedSDKReferences, "SDKResolver", _getAssemblyName, _fileExists, _getRuntimeVersion, _targetedRuntimeVersion) }); + } + else + { + jaggedResolvers.Add(AssemblyResolution.CompileDirectories(parentReferenceFolders, _fileExists, _getAssemblyName, _getRuntimeVersion, _targetedRuntimeVersion)); + jaggedResolvers.Add(_compiledSearchPaths); + } + + // Resolve + try + { + resolvedPath = AssemblyResolution.ResolveReference + ( + jaggedResolvers, + assemblyName, + reference.SDKName, + rawFileNameCandidate, + reference.IsPrimary, + reference.WantSpecificVersion, + reference.GetExecutableExtensions(_allowedAssemblyExtensions), + reference.HintPath, + reference.AssemblyFolderKey, + assembliesConsideredAndRejected, + out resolvedSearchPath, + out userRequestedSpecificFile + ); + } + catch (System.BadImageFormatException e) + { + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + + // Update the list of assemblies considered and rejected. + reference.AddAssembliesConsideredAndRejected(assembliesConsideredAndRejected); + + // If the path was resolved, then specify the full path on the reference. + if (resolvedPath != null) + { + if (!Path.IsPathRooted(resolvedPath)) + { + resolvedPath = Path.GetFullPath(resolvedPath); + } + + reference.FullPath = resolvedPath; + reference.ResolvedSearchPath = resolvedSearchPath; + reference.UserRequestedSpecificFile = userRequestedSpecificFile; + } + else + { + if (assemblyName != null) + { + reference.AddError + ( + new ReferenceResolutionException + ( + ResourceUtilities.FormatResourceString("General.CouldNotLocateAssembly", assemblyName.FullName), + null + ) + ); + } + } + } + + /// + /// This method will remove references from the reference table which are contained in the blacklist. + /// References which are primary references but are in the black list will be placed in the invalidResolvedFiles list. + /// References which are dependency references but are in the black list will be placed in the invalidResolvedDependencyFiles list. + /// + internal void RemoveReferencesMarkedForExclusion(bool removeOnlyNoWarning, string subsetName) + { +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildRARRemoveFromExclusionListBegin, CodeMarkerEvent.perfMSBuildRARRemoveFromExclusionListEnd)) +#endif + { + // Create a table which will contain the references which are not in the black list + Dictionary goodReferences = new Dictionary(AssemblyNameComparer.GenericComparer); + + // List of references which were removed from the reference table, we will loop through these and make sure that we get rid of the dependent references also. + List removedReferences = new List(); + + // For each reference, have a list of dependency references and their assembly names. (List>) == the dependent reference and the assembly name. + Dictionary> dependencyGraph = new Dictionary>(); + + LogExclusionReason logExclusionReason = null; + + if (subsetName == null) + { + subsetName = String.Empty; + } + + // Go through each of the references, we go through this table because in general it will be considerably smaller than the blacklist. (10's of references vs 100's of black list items) + foreach (AssemblyNameExtension assemblyName in _references.Keys) + { + Reference assemblyReference = _references[assemblyName]; + + AddToDependencyGraph(dependencyGraph, assemblyName, assemblyReference); + + // Is the assembly name not in the black list. This means the assembly could be allowed. + bool isMarkedForExclusion = assemblyReference.ExclusionListLoggingProperties.IsInExclusionList; + logExclusionReason = assemblyReference.ExclusionListLoggingProperties.ExclusionReasonLogDelegate; + + // Case one, the assembly is a primary reference + if (assemblyReference.IsPrimary) + { + // The assembly is good if it is not in the black list or it has specific version set to true. + if (!isMarkedForExclusion || assemblyReference.WantSpecificVersion) + { + // Do not add the reference to the good list if it has been added to the removed references list, possibly because of us processing another reference. + if (!removedReferences.Contains(assemblyReference)) + { + goodReferences[assemblyName] = assemblyReference; + } + } + else + { + RemovePrimaryReferenceMarkedForExclusion(logExclusionReason, removeOnlyNoWarning, subsetName, removedReferences, assemblyName, assemblyReference); + } + } + + // A Primary reference can also be dependency of other references. This means there may be other primary reference which depend on + // the current primary reference and they need to be removed. + ICollection dependees = assemblyReference.GetSourceItems(); + + // Need to deal with dependencies, this can also include primary references who are dependencies themselves and are in the black list + if (!assemblyReference.IsPrimary || (assemblyReference.IsPrimary && isMarkedForExclusion && (dependees != null && dependees.Count > 1))) + { + // Does the assembly have specific version true, or does any of its primary parent references have specific version true. + // This is checked because, if an assembly is in the black list, the only way it can possibly be allowed is if + // ANY of the primary references which caused it have specific version set to true. To see if any primary references have the metadata we pass true to the method indicating + // we want to know if any primary references have specific version set to true. + bool hasSpecificVersionTrue = assemblyReference.CheckForSpecificVersionMetadataOnParentsReference(true); + + //A dependency is "good" if it is not in the black list or any of its parents have specific version set to true + if (!isMarkedForExclusion || hasSpecificVersionTrue) + { + // Do not add the reference to the good list if it has been added to the removed references list, possibly because of us processing another reference. + if (!removedReferences.Contains(assemblyReference)) + { + goodReferences[assemblyName] = assemblyReference; + } + } + + // If the dependency is in the black list we need to remove the primary references which depend on this refernce. + // note, a reference can both be in the good references list and in the black list. This can happen if a multiple primary references + // depend on a single dependency. The dependency can be good for one reference but not allowed for the other. + if (isMarkedForExclusion) + { + RemoveDependencyMarkedForExclusion(logExclusionReason, removeOnlyNoWarning, subsetName, goodReferences, removedReferences, assemblyName, assemblyReference); + } + } + } + + // Go through each of the reference which were removed from the reference list and make sure that we get rid of all of the assemblies which were + // dependencies of them. + foreach (Reference reference in removedReferences) + { + RemoveDependencies(reference, goodReferences, dependencyGraph); + } + + // Replace the references table with the list only containing good references. + _references = goodReferences; + } + } + + /// + /// References usually only contains who they depend on, they do not know who depends on them. Given a reference + /// A we cannot inspect A to find out that B,C,D depend on it. This method will traverse the references and build up this other direction of the graph, + /// therefore we will be able to know given reference A, that B,C,D depend on it. + /// + private static void AddToDependencyGraph(Dictionary> dependencyGraph, AssemblyNameExtension assemblyName, Reference assemblyReference) + { + // Find the references who the current reference is a dependency for + foreach (Reference dependee in assemblyReference.GetDependees()) + { + // This list will contain a list of key value pairs (K: Dependent reference V: assembly Name) + List dependencies = null; + + // For a dependee see if we already have a list started + if (!dependencyGraph.TryGetValue(dependee, out dependencies)) + { + dependencies = new List(); + dependencyGraph.Add(dependee, dependencies); + } + + dependencies.Add(new ReferenceAssemblyExtensionPair(assemblyReference, assemblyName)); + } + } + + /// + /// We have determined the given assembly reference is in the black list, we now need to find the primary references which caused it and make sure those are removed from the list of references. + /// + private void RemoveDependencyMarkedForExclusion(LogExclusionReason logExclusionReason, bool removeOnlyNoWarning, string subsetName, Dictionary goodReferences, List removedReferences, AssemblyNameExtension assemblyName, Reference assemblyReference) + { + // For a dependency we would like to remove the primary references which caused this dependency to be found. + // Source Items is the list of primary itemspecs which lead to the current reference being discovered. + ICollection dependees = assemblyReference.GetSourceItems(); + foreach (ITaskItem dependee in dependees) + { + string dependeeItemSpec = dependee.ItemSpec; + + if (assemblyReference.IsPrimary) + { + // Dont process yourself + if (String.Compare(dependeeItemSpec, assemblyReference.PrimarySourceItem.ItemSpec, StringComparison.OrdinalIgnoreCase) == 0) + { + continue; + } + } + + // Get the primary reference assemblyName + AssemblyNameExtension primaryAssemblyName = GetReferenceFromItemSpec(dependeeItemSpec); + + if (primaryAssemblyName != null) + { + // Get the specific primary reference which caused this dependency + Reference primaryAssemblyReference = _references[primaryAssemblyName]; + bool hasSpecificVersionMetadata = primaryAssemblyReference.WantSpecificVersion; + + if (!hasSpecificVersionMetadata) + { + // If the reference has not been removed we need to remove it and possibly remove it from the good reference list. + if (!removedReferences.Contains(primaryAssemblyReference)) + { + removedReferences.Add(primaryAssemblyReference); + goodReferences.Remove(primaryAssemblyName); + } + + if (!removeOnlyNoWarning && logExclusionReason != null) + { + logExclusionReason(false, assemblyName, assemblyReference, dependee, subsetName); + } + } + } + } + } + + /// + /// A primary references has been determined to be in the black list, it needs to be removed from the list of references by not being added to the list of good references + /// and added to the list of removed references. + /// + private static void RemovePrimaryReferenceMarkedForExclusion(LogExclusionReason logExclusionReason, bool removeOnlyNoWarning, string subsetName, List removedReferences, AssemblyNameExtension assemblyName, Reference assemblyReference) + { + removedReferences.Add(assemblyReference); + + if (!removeOnlyNoWarning && logExclusionReason != null) + { + // Note a primary references will always have a PrimarySourceItem which is not null + logExclusionReason(true, assemblyName, assemblyReference, assemblyReference.PrimarySourceItem, subsetName); + } + } + + /// + /// Get the primary reference based on the Itemspec + /// + internal AssemblyNameExtension GetReferenceFromItemSpec(string itemSpec) + { + foreach (AssemblyNameExtension assemblyName in _references.Keys) + { + Reference assemblyReference = _references[assemblyName]; + if (assemblyReference.IsPrimary && assemblyReference.PrimarySourceItem.ItemSpec.Equals(itemSpec, StringComparison.OrdinalIgnoreCase)) + { + return assemblyName; + } + } + + return null; + } + + /// + /// Go through the dependency graph and make sure that for a reference to remove that we get rid of all dependency assemblies which are not referenced by any other + /// assembly. The remove reference list should contain ALL primary references which should be removed because they, or one of their dependencies is in the black list. + /// + /// Reference to remove dependencies for + /// Reference list which contains reference to be used in unification and returned as resolved items + /// A dictionary (Key: Reference Value: List of dependencies and their assembly name) + private void RemoveDependencies(Reference removedReference, Dictionary referenceList, Dictionary> dependencyList) + { + List dependencies = null; + + // See if the reference has a list of dependencies + if (!dependencyList.TryGetValue(removedReference, out dependencies)) + { + return; + } + + // Go through each of the dependency assemblies and remove the removedReference from the + // dependee list. + foreach (ReferenceAssemblyExtensionPair dependency in dependencies) + { + Reference reference = dependency.Key; + + // Remove the referenceToRemove from the dependee list, this will "unlink" them, in that the dependency reference will no longer know that + // referenceToRemove had a dependency on it + reference.RemoveDependee(removedReference); + + // A primary reference is special because it is declared in the project file so even if no one else deppends on it, the reference is still needed. + if (reference.IsPrimary) + { + continue; + } + + // If the referenceToRemove was the last dependee of the current dependency reference, remove the dependency reference from the reference list. + if (reference.GetDependees().Count == 0) + { + referenceList.Remove(dependency.Value); + + // Recurse using the current refererence so that we remove the next set of dependencies. + RemoveDependencies(reference, referenceList, dependencyList); + } + } + } + + /// + /// Searches the table for references that haven't been resolved to their full file names and + /// for dependencies that haven't yet been found. + /// + /// If any are found, they're resolved and then dependencies are found. Then the process is repeated + /// until nothing is left unresolved. + /// + /// The table of remapped assemblies. + /// The task items which contain file names to add. + /// The task items which contain fusion names to add. + /// Errors encountered while computing closure. + internal void ComputeClosure + ( + DependentAssembly[] remappedAssembliesValue, + ITaskItem[] referenceAssemblyFiles, + ITaskItem[] referenceAssemblyNames, + ArrayList exceptions + ) + { +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildRARComputeClosureBegin, CodeMarkerEvent.perfMSBuildRARComputeClosureEnd)) +#endif + { + _references.Clear(); + _remappedAssemblies = remappedAssembliesValue; + SetPrimaryItems(referenceAssemblyFiles, referenceAssemblyNames, exceptions); + + ComputeClosure(); + } + } + + /// + /// Implementation of ComputeClosure. + /// + private void ComputeClosure() + { + bool moreResolvable = true; + int moreResolvableIterations = 0; + const int maxIterations = 100000; // Wait for a ridiculously large number of iterations before bailing out. + + do + { + bool moreDependencies = true; + + int dependencyIterations = 0; + do + { + // Resolve all references. + ResolveAssemblyFilenames(); + + // Find prerequisites. + moreDependencies = FindAssociatedFiles(); + + ++dependencyIterations; + ErrorUtilities.VerifyThrow(dependencyIterations < maxIterations, "Maximum iterations exceeded while looking for dependencies."); + } while (moreDependencies); + + + // If everything is either resolved or unresolvable, then we can quit. + // Otherwise, loop again. + moreResolvable = false; + foreach (Reference reference in References.Values) + { + if (!reference.IsResolved) + { + if (!reference.IsUnresolvable) + { + moreResolvable = true; + break; + } + } + } + + ++moreResolvableIterations; + ErrorUtilities.VerifyThrow(moreResolvableIterations < maxIterations, "Maximum iterations exceeded while looking for resolvable references."); + } while (moreResolvable); + } + + /// + /// Find associates for references that we haven't found associates for before. + /// Returns true if new dependent assemblies were found. + /// + private bool FindAssociatedFiles() + { + bool newDependencies = false; + + ArrayList newEntries = new ArrayList(); + + foreach (Reference reference in References.Values) + { + // If the reference is resolved, but dependencies haven't been found, + // then find dependencies. + if (reference.IsResolved && !reference.DependenciesFound) + { + // Set this reference to 'resolved' so it won't be processed the next time. + reference.DependenciesFound = true; + + try + { + // We don't look for associated files for FX assemblies. + bool hasFrameworkPath = false; + string referenceDirectoryName = FileUtilities.EnsureTrailingSlash(reference.DirectoryName); + + foreach (string frameworkPath in _frameworkPaths) + { + // frameworkPath is guaranteed to have a trailing slash, because + // ResolveAssemblyReference.Execute takes care of adding it. + + if (String.Compare(referenceDirectoryName, frameworkPath, StringComparison.OrdinalIgnoreCase) == 0) + { + hasFrameworkPath = true; + } + } + + // We do not want to find dependencies of framework assembles, embedded interoptypes or assemblies in sdks. + if (!hasFrameworkPath && !reference.EmbedInteropTypes && reference.SDKName.Length == 0) + { + // Look for companion files like pdbs and xmls that ride along with + // assemblies. + if (_findRelatedFiles) + { + FindRelatedFiles(reference); + } + + // Satellite assemblies are named \.resources.dll + // where is like 'en', 'fr', etc. + if (_findSatellites) + { + FindSatellites(reference); + } + + // Look for serialization assemblies. + if (_findSerializationAssemblies) + { + FindSerializationAssemblies(reference); + } + + // Look for dependent assemblies. + if (_findDependencies) + { + FindDependenciesAndScatterFiles(reference, newEntries); + } + + + // If something was found, then break out and start fresh. + if (newEntries.Count > 0) + { + break; + } + } + } + catch (PathTooLongException e) + { + // If the directory path is too long then record the error and move on. + reference.AddError(new DependencyResolutionException(e.Message, e)); + } + } + } + + // Add each new dependency found. + foreach (DictionaryEntry newEntry in newEntries) + { + newDependencies = true; + AddReference((AssemblyNameExtension)newEntry.Key, (Reference)newEntry.Value); + } + + return newDependencies; + } + + /// + /// Resolve all references that have not been resolved yet to real files on disk. + /// + private void ResolveAssemblyFilenames() + { + foreach (AssemblyNameExtension assemblyName in References.Keys) + { + Reference reference = GetReference(assemblyName); + + // Has this reference been resolved to a file name? + if (!reference.IsResolved && !reference.IsUnresolvable) + { + ResolveReference(assemblyName, null, reference); + } + } + } + + /// + /// This methods looks for conflicts between assemblies and attempts to + /// resolve them. + /// + private int ResolveConflictsBetweenReferences() + { + int count = 0; + + // Get a table of simple name mapped to (perhaps multiple) reference. + Hashtable baseNames = BuildSimpleNameTable(); + + // Now we have references organized into groups that would conflict. + foreach (string baseName in baseNames.Keys) + { + ArrayList assemblyReferences = (ArrayList)baseNames[baseName]; + + // Sort to make it predictable. Choose to sort by ascending version number + // since this is known to reveal bugs in at least one circumstance. + assemblyReferences.Sort(AssemblyNameReferenceAscendingVersionComparer.comparer); + + // Two or more references required for there to be a conflict. + while (assemblyReferences.Count > 1) + { + // Resolve the conflict. Victim is the index of the item that lost. + int victim = ResolveAssemblyNameConflict + ( + (AssemblyNameReference)assemblyReferences[0], + (AssemblyNameReference)assemblyReferences[1] + ); + + assemblyReferences.RemoveAt(victim); + count++; + } + } + + return count; + } + + /// + /// Based on the closure, get a table of ideal remappings needed to + /// produce zero conflicts. + /// + internal void ResolveConflicts + ( + out DependentAssembly[] idealRemappings, + out AssemblyNameReference[] conflictingReferences + ) + { + idealRemappings = null; + conflictingReferences = null; + + // First, resolve all conflicts between references. + if (0 == ResolveConflictsBetweenReferences()) + { + // If there were no basename conflicts then there can be no version-to-version conflicts. + // In this case, short-circuit now rather than building up all the tables below. + return; + } + + // Build two tables, one with a count and one with the corresponding references. + // Dependencies which differ only by version number need a suggested redirect. + // The count tells us whether there are two or more. + Hashtable counts = new Hashtable(StringComparer.OrdinalIgnoreCase); + Hashtable references = new Hashtable(StringComparer.OrdinalIgnoreCase); + + foreach (AssemblyNameExtension assemblyName in References.Keys) + { + Reference reference = GetReference(assemblyName); + + // If the assembly has a parent which has specific version set to true then we need to see if it is framework assembly + if (reference.CheckForSpecificVersionMetadataOnParentsReference(true)) + { + // Try and find an entry in the redist list by comparing everything except the version. + AssemblyEntry entry = null; + + if (_installedAssemblies != null) + { + entry = _installedAssemblies.FindHighestVersionInRedistList(assemblyName); + } + + if (entry != null) + { + // We have found an entry in the redist list that this assembly is a framework assembly of some version + // also one if its parent refernces has specific version set to true, therefore we need to make sure + // that we do not consider it for conflict resolution. + continue; + } + } + + byte[] pkt = assemblyName.GetPublicKeyToken(); + if (pkt != null && pkt.Length > 0) + { + AssemblyName baseKey = (AssemblyName)assemblyName.AssemblyName.Clone(); + Version version = baseKey.Version; + baseKey.Version = null; + string key = baseKey.ToString(); + + if (counts.ContainsKey(key)) + { + counts[key] = ((int)counts[key]) + 1; + Version lastVersion = ((AssemblyNameReference)references[key]).assemblyName.Version; + + if (lastVersion == null || lastVersion < version) + { + references[key] = AssemblyNameReference.Create(assemblyName, reference); + } + } + else + { + counts[key] = 1; + references[key] = AssemblyNameReference.Create(assemblyName, reference); + } + } + } + + // Build the list of conflicted assemblies. + List assemblyNamesList = new List(); + foreach (string versionLessAssemblyName in counts.Keys) + { + if (((int)counts[versionLessAssemblyName]) > 1) + { + assemblyNamesList.Add((AssemblyNameReference)references[versionLessAssemblyName]); + } + } + + // Pass over the list of conflicting references and make a binding redirect for each. + List idealRemappingsList = new List(); + + foreach (AssemblyNameReference assemblyNameReference in assemblyNamesList) + { + DependentAssembly remapping = new DependentAssembly(); + remapping.PartialAssemblyName = assemblyNameReference.assemblyName.AssemblyName; + BindingRedirect bindingRedirect = new BindingRedirect(); + bindingRedirect.OldVersionLow = new Version("0.0.0.0"); + bindingRedirect.OldVersionHigh = assemblyNameReference.assemblyName.AssemblyName.Version; + bindingRedirect.NewVersion = assemblyNameReference.assemblyName.AssemblyName.Version; + remapping.BindingRedirects = new BindingRedirect[] { bindingRedirect }; + + idealRemappingsList.Add(remapping); + } + + idealRemappings = idealRemappingsList.ToArray(); + conflictingReferences = assemblyNamesList.ToArray(); + } + + /// + /// If a reference is a higher version than what exists in the redist list of the target framework then + /// this reference needs to be marked as excluded so that it is not not allowed to be referenced. + /// + /// If the user needs this reference then they need to set specific version to true. + /// + internal bool MarkReferencesExcludedDueToOtherFramework(AssemblyNameExtension assemblyName, Reference reference) + { + bool haveMarkedReference = false; + + // If the reference was not resolved from the GAC or AssemblyFolders then + // we do not need to check it if came from another framework + string resolvedSearchPath = reference.ResolvedSearchPath; + bool resolvedFromGAC = resolvedSearchPath.Equals(AssemblyResolutionConstants.gacSentinel, StringComparison.OrdinalIgnoreCase); + bool resolvedFromAssemblyFolders = resolvedSearchPath.Equals(AssemblyResolutionConstants.assemblyFoldersSentinel, StringComparison.OrdinalIgnoreCase); + + if (!resolvedFromGAC && !resolvedFromAssemblyFolders && reference.IsResolved) + { + return false; + } + + bool inLaterRedistListAndFromGlobalLocation = false; + + // Check against target framework version if projectTargetFramework is null or less than 4.5, also when flag to force check is set to true + if (_checkAssemblyVersionAgainstTargetFrameworkVersion) + { + // Did the assembly name get resolved from a GlobalLocation, GAC or AssemblyFolders and is it in the frameworkList.xml for the + // highest version of the currently targeted framework identifier. + inLaterRedistListAndFromGlobalLocation = InLatestRedistList(assemblyName, reference); + + if (inLaterRedistListAndFromGlobalLocation) + { + LogExclusionReason reason = new LogExclusionReason(LogAnotherFrameworkUnResolve); + reference.ExclusionListLoggingProperties.ExclusionReasonLogDelegate = reason; + reference.ExclusionListLoggingProperties.IsInExclusionList = true; + haveMarkedReference = true; + } + } + + return haveMarkedReference; + } + + /// + /// Is the assembly in the latest framework redist list as either passed into RAR on the lastestFrameworkDirectories property or determined by inspecting the file system. + /// + private bool InLatestRedistList(AssemblyNameExtension assemblyName, Reference reference) + { + bool inLaterRedistList = false; + + Tuple redistListOtherFramework = GetHighestVersionFullFrameworkForTFM(_targetFrameworkMoniker); + + if (redistListOtherFramework != null && redistListOtherFramework.Item1 != null && redistListOtherFramework.Item1.FrameworkAssemblyEntryInRedist(assemblyName)) + { + inLaterRedistList = true; + } + + return inLaterRedistList; + } + + /// + /// Get the redist list which corresponds to the highest target framework for a given target framework moniker. + /// + /// This is done in two ways: + /// First, if the latestTargetFrameworkDirectories parameter is passed into RAR those directories will be used to get the redist list + /// regardless of the target framework moniker. + /// + /// Second, if latest Target Framework Directories is not passed in then we ask the ToollocationHelper for the highest target framework which has + /// a TargetFrameworkIdentifier which matches the passed in TargetFrameworkMoniker. + /// + private Tuple GetHighestVersionFullFrameworkForTFM(FrameworkNameVersioning targetFrameworkMoniker) + { + RedistList redistList = null; + Tuple redistListAndOtherFrameworkName = null; + if (targetFrameworkMoniker != null) + { + lock (s_monikerToHighestRedistList) + { + if (!s_monikerToHighestRedistList.TryGetValue(targetFrameworkMoniker.Identifier, out redistListAndOtherFrameworkName)) + { + IList referenceAssemblyDirectories = null; + + string otherFrameworkName = null; + + // The latestTargetFrameworkDirectories can be passed into RAR, if they are then use those directories rather than + // getting a list by looking at the file system. + if (_latestTargetFrameworkDirectories != null && _latestTargetFrameworkDirectories.Length > 0) + { + referenceAssemblyDirectories = new List(_latestTargetFrameworkDirectories); + otherFrameworkName = String.Join(";", _latestTargetFrameworkDirectories); + } + else if (targetFrameworkMoniker != null) + { + FrameworkNameVersioning highestFrameworkName = null; + referenceAssemblyDirectories = GetHighestVersionReferenceAssemblyDirectories(targetFrameworkMoniker, out highestFrameworkName); + if (highestFrameworkName != null) + { + otherFrameworkName = highestFrameworkName.FullName; + } + } + + if (referenceAssemblyDirectories != null && referenceAssemblyDirectories.Count > 0) + { + HashSet seenFrameworkDirectories = new HashSet(StringComparer.OrdinalIgnoreCase); + List assemblyTableInfos = new List(); + foreach (string path in referenceAssemblyDirectories) + { + string[] listPaths = RedistList.GetRedistListPathsFromDisk(path); + foreach (string listPath in listPaths) + { + if (!seenFrameworkDirectories.Contains(listPath)) + { + assemblyTableInfos.Add(new AssemblyTableInfo(listPath, path)); + seenFrameworkDirectories.Add(listPath); + } + } + } + + // If the same set of directories was passed in before then the redist list will already be cached. + redistList = RedistList.GetRedistList(assemblyTableInfos.ToArray()); + } + + redistListAndOtherFrameworkName = new Tuple(redistList, otherFrameworkName); + s_monikerToHighestRedistList.Add(targetFrameworkMoniker.Identifier, redistListAndOtherFrameworkName); + } + } + } + + return redistListAndOtherFrameworkName; + } + + /// + /// Based on a target framework moniker, get the set of reference assembly directories which + /// correspond to the highest version of the target framework identifier property on the target framework moniker. + /// + private static IList GetHighestVersionReferenceAssemblyDirectories(FrameworkNameVersioning targetFrameworkMoniker, out FrameworkNameVersioning highestVersionMoniker) + { + IList referenceAssemblyDirectories = null; + string targetFrameworkRootDirectory = ToolLocationHelper.GetProgramFilesReferenceAssemblyRoot(); + + highestVersionMoniker = ToolLocationHelper.HighestVersionOfTargetFrameworkIdentifier(targetFrameworkRootDirectory, targetFrameworkMoniker.Identifier); + if (highestVersionMoniker == null) + { + referenceAssemblyDirectories = new List(); + } + else + { + referenceAssemblyDirectories = ToolLocationHelper.GetPathToReferenceAssemblies(targetFrameworkRootDirectory, highestVersionMoniker); + } + return referenceAssemblyDirectories; + } + + + /// + /// Is the assemblyName in the current redist list and does it have a version number which is higher than what is in the current redist list. + /// This may happen if someone passes in a p2p reference whcih is a framework assembly which is a higher version than what is in the redist list. + /// + internal void MarkReferenceWithHighestVersionInCurrentRedistList(AssemblyNameExtension assemblyName, Reference reference) + { + if (_installedAssemblies != null) + { + // Find the highest version of the assembly in the current redist list + AssemblyEntry highestInRedistList = _installedAssemblies.FindHighestVersionInRedistList(assemblyName); + + if (highestInRedistList != null) + { + reference.ExclusionListLoggingProperties.HighestVersionInRedist = highestInRedistList.AssemblyNameExtension.Version; + } + } + } + + /// + /// Is the assemblyName in the current redist list and does it have a version number which is higher than what is in the current redist list. + /// This may happen if someone passes in a p2p reference whcih is a framework assembly which is a higher version than what is in the redist list. + /// + internal bool MarkReferenceForExclusionDueToHigherThanCurrentFramework(AssemblyNameExtension assemblyName, Reference reference) + { + bool higherThanCurrentRedistList = false; + + // In this method have we marked a reference as needing to be excluded + bool haveMarkedReference = false; + + // Mark reference as excluded + + // Check against target framework version if projectTargetFramework is null or less than 4.5, also when flag to force check is set to true + if (_checkAssemblyVersionAgainstTargetFrameworkVersion) + { + // Check assemblies versions when target framework version is less than 4.5 + + // Make sure the version is higher than the version in the redist. + higherThanCurrentRedistList = (reference.ReferenceVersion != null && reference.ExclusionListLoggingProperties.HighestVersionInRedist != null) + && reference.ReferenceVersion.CompareTo(reference.ExclusionListLoggingProperties.HighestVersionInRedist) > 0; + + if (higherThanCurrentRedistList) + { + LogExclusionReason reason = new LogExclusionReason(LogHigherVersionUnresolve); + reference.ExclusionListLoggingProperties.ExclusionReasonLogDelegate = reason; + reference.ExclusionListLoggingProperties.IsInExclusionList = true; + haveMarkedReference = true; + } + } + + return haveMarkedReference; + } + + /// + /// Does the assembly have a targetFrameworkAttribute which has a higher framework version than what the project is currently targeting. + /// This may happen for example if a p2p is done between two projects with built against different target frameworks. + /// + internal bool MarkReferenceForExclusionDueToHigherThanCurrentFrameworkAttribute(AssemblyNameExtension assemblyName, Reference reference) + { + bool higherThanCurrentFramework = false; + // In this method have we marked a reference as needing to be excluded + bool haveMarkedReference = false; + + if (!(reference.IsResolved && _fileExists(reference.FullPath)) || reference.IsPrerequisite || (_frameworkPaths != null && Reference.IsFrameworkFile(reference.FullPath, _frameworkPaths))) + { + return false; + } + + // Make sure the version is higher than the version in the redist. + // If the identifier are not equal we do not check since we are not trying to catch cross framework incompatibilities. + higherThanCurrentFramework = reference.FrameworkNameAttribute != null + && _targetFrameworkMoniker != null + && String.Equals(reference.FrameworkNameAttribute.Identifier, _targetFrameworkMoniker.Identifier, StringComparison.OrdinalIgnoreCase) + && reference.FrameworkNameAttribute.Version > _targetFrameworkMoniker.Version; + + // Mark reference as excluded + if (higherThanCurrentFramework) + { + LogExclusionReason reason = new LogExclusionReason(LogHigherVersionUnresolveDueToAttribute); + reference.ExclusionListLoggingProperties.ExclusionReasonLogDelegate = reason; + reference.ExclusionListLoggingProperties.IsInExclusionList = true; + haveMarkedReference = true; + } + + return haveMarkedReference; + } + + + /// + /// Build a table of simple names mapped to assemblyname+reference. + /// + private Hashtable BuildSimpleNameTable() + { + // Build a list of base file names from references. + // These would conflict with each other if copied to the output directory. + Hashtable baseNames = new Hashtable(StringComparer.CurrentCultureIgnoreCase); + AssemblyNameReference assemblyReference; + + foreach (AssemblyNameExtension assemblyName in References.Keys) + { + assemblyReference.assemblyName = assemblyName; + assemblyReference.reference = GetReference(assemblyName); + + // Notice that unresolved assemblies are still added to the table. + // This is because an unresolved assembly may have a different version + // which would influence unification. We want to report this to the user. + string baseName = assemblyName.Name; + + if (!baseNames.ContainsKey(baseName)) + { + baseNames[baseName] = new ArrayList(); + } + + ((ArrayList)baseNames[baseName]).Add(assemblyReference); + } + + return baseNames; + } + + /// + /// Given two references along with their fusion names, resolve the filename conflict that they + /// would have if both assemblies need to be copied to the same directory. + /// + private static int ResolveAssemblyNameConflict(AssemblyNameReference assemblyReference0, AssemblyNameReference assemblyReference1) + { + int victim = 0; + + // Extra checks for PInvoke-destined data. + ErrorUtilities.VerifyThrow(assemblyReference0.assemblyName.FullName != null, "Got a null assembly name fullname. (0)"); + ErrorUtilities.VerifyThrow(assemblyReference1.assemblyName.FullName != null, "Got a null assembly name fullname. (1)"); + + string[] conflictFusionNames = new string[] { assemblyReference0.assemblyName.FullName, assemblyReference1.assemblyName.FullName }; + Reference[] conflictReferences = new Reference[] { assemblyReference0.reference, assemblyReference1.reference }; + AssemblyNameExtension[] conflictAssemblyNames = new AssemblyNameExtension[] { assemblyReference0.assemblyName, assemblyReference1.assemblyName }; + bool[] conflictLegacyUnified = new bool[] { assemblyReference0.reference.IsPrimary, assemblyReference1.reference.IsPrimary }; + + // If both assemblies being compared are primary references, the caller should pass in a zero-flag + // (non-unified) for both. (This conforms to the C# assumption that two direct references are meant to be + // SxS.) + if (conflictReferences[0].IsPrimary && conflictReferences[1].IsPrimary) + { + conflictLegacyUnified[0] = false; + conflictLegacyUnified[1] = false; + } + + // This is ok here because even if the method says two versions are equivilant the algorithm below will still pick the highest version. + bool equivalent = false; + NativeMethods.AssemblyComparisonResult result = 0; + NativeMethods.CompareAssemblyIdentity + ( + conflictFusionNames[0], + conflictLegacyUnified[0], + conflictFusionNames[1], + conflictLegacyUnified[1], + out equivalent, + out result + ); + + // Remove one and provide some information about why. + victim = 0; + ConflictLossReason reason = ConflictLossReason.InsolubleConflict; + + // Pick the one with the highest version number. + if (conflictReferences[0].IsPrimary && !conflictReferences[1].IsPrimary) + { + // Choose the primary version. + victim = 1; + reason = ConflictLossReason.WasNotPrimary; + } + else if (!conflictReferences[0].IsPrimary && conflictReferences[1].IsPrimary) + { + // Choose the primary version. + victim = 0; + reason = ConflictLossReason.WasNotPrimary; + } + else if (!conflictReferences[0].IsPrimary && !conflictReferences[1].IsPrimary) + { + if + ( + // Version comparison only if there are two versions to compare. + // Null versions can occur when simply-named assemblies are unresolved. + conflictAssemblyNames[0].Version != null && conflictAssemblyNames[1].Version != null + && conflictAssemblyNames[0].Version > conflictAssemblyNames[1].Version + ) + { + // Choose the higher version + victim = 1; + if (equivalent) + { + reason = ConflictLossReason.HadLowerVersion; + } + } + else if + ( + // Version comparison only if there are two versions to compare. + // Null versions can occur when simply-named assemblies are unresolved. + conflictAssemblyNames[0].Version != null && conflictAssemblyNames[1].Version != null + && conflictAssemblyNames[0].Version < conflictAssemblyNames[1].Version + ) + { + // Choose the higher version + victim = 0; + if (equivalent) + { + reason = ConflictLossReason.HadLowerVersion; + } + } + else + { + victim = 0; + + if (equivalent) + { + // Fusion thinks they're interchangeable. + reason = ConflictLossReason.FusionEquivalentWithSameVersion; + } + } + } + + + // Remove the one chosen. + int victor = 1 - victim; + conflictReferences[victim].ConflictVictorName = conflictAssemblyNames[victor]; + conflictReferences[victim].ConflictLossExplanation = reason; + conflictReferences[victor].AddConflictVictim(conflictAssemblyNames[victim]); + + return victim; + } + + /// + /// Returns true if an assembly has been removed from the .NET framework + /// + private bool IsAssemblyRemovedFromDotNetFramework(AssemblyNameExtension assemblyName, string fullPath, string[] frameworkPaths, InstalledAssemblies installedAssemblies) + { + if (installedAssemblies != null) + { + AssemblyEntry redistListEntry = installedAssemblies.FindHighestVersionInRedistList(assemblyName); + if (redistListEntry != null) + { + Version redistListVersion = redistListEntry.AssemblyNameExtension.Version; + + if (redistListVersion != null && assemblyName.Version >= redistListVersion && !Reference.IsFrameworkFile(fullPath, frameworkPaths)) + { + return true; + } + } + } + return false; + } + + /// + /// Get unification information for the given assembly name. + /// + /// The assembly name. + /// The new version of the assembly to use. + /// The reason this reference was unified. + /// True if this is a prereq assembly. + /// True if there was a unification. + private bool UnifyAssemblyNameVersions + ( + AssemblyNameExtension assemblyName, + out Version unifiedVersion, + out UnificationReason unificationReason, + out bool isPrerequisite, + out bool? isRedistRoot, + out string redistName + ) + { + unifiedVersion = assemblyName.Version; + isPrerequisite = false; + isRedistRoot = null; + redistName = null; + unificationReason = UnificationReason.DidntUnify; + + // If there's no version, for example in a simple name, then no remapping is possible. + if (assemblyName.Version == null) + { + return false; + } + + // Try for a remapped assemblies unification. + if (_remappedAssemblies != null) + { + foreach (DependentAssembly remappedAssembly in _remappedAssemblies) + { + // First, exclude anything without the simple name match + AssemblyNameExtension comparisonAssembly = new AssemblyNameExtension((AssemblyName)remappedAssembly.PartialAssemblyName.Clone()); + if (assemblyName.CompareBaseNameTo(comparisonAssembly) == 0) + { + // Comparison assembly is a partial name. Give it our version. + comparisonAssembly.ReplaceVersion(assemblyName.Version); + + if (assemblyName.Equals(comparisonAssembly)) + { + foreach (BindingRedirect bindingRedirect in remappedAssembly.BindingRedirects) + { + if (assemblyName.Version >= bindingRedirect.OldVersionLow && assemblyName.Version <= bindingRedirect.OldVersionHigh) + { + // If the new version is different than the old version, then there is a unification. + if (assemblyName.Version != bindingRedirect.NewVersion) + { + unifiedVersion = bindingRedirect.NewVersion; + unificationReason = UnificationReason.BecauseOfBindingRedirect; + return true; + } + } + } + } + } + } + } + + // Try for an installed assemblies unification. + if (_installedAssemblies != null) + { + _installedAssemblies.GetInfo + ( + assemblyName, + out unifiedVersion, + out isPrerequisite, + out isRedistRoot, + out redistName + ); + + // Was there a unification? + if (unifiedVersion != assemblyName.Version) + { + unificationReason = UnificationReason.FrameworkRetarget; + return assemblyName.Version != unifiedVersion; + } + } + + + return false; + } + + /// + /// Return the resulting reference items, dependencies and other files. + /// + /// Primary references fully resolved. + /// Dependent references fully resolved. + /// Related files like .xmls and .pdbs. + /// Satellite files. + /// All copy-local files out of primaryFiles+dependencyFiles+relatedFiles+satelliteFiles. + internal void GetReferenceItems + ( + out ITaskItem[] primaryFiles, + out ITaskItem[] dependencyFiles, + out ITaskItem[] relatedFiles, + out ITaskItem[] satelliteFiles, + out ITaskItem[] serializationAssemblyFiles, + out ITaskItem[] scatterFiles, + out ITaskItem[] copyLocalFiles + ) + { + primaryFiles = new ITaskItem[0]; + dependencyFiles = new ITaskItem[0]; + relatedFiles = new ITaskItem[0]; + satelliteFiles = new ITaskItem[0]; + serializationAssemblyFiles = new ITaskItem[0]; + scatterFiles = new ITaskItem[0]; + copyLocalFiles = new ITaskItem[0]; + + ArrayList primaryItems = new ArrayList(); + ArrayList dependencyItems = new ArrayList(); + ArrayList relatedItems = new ArrayList(); + ArrayList satelliteItems = new ArrayList(); + ArrayList serializationAssemblyItems = new ArrayList(); + ArrayList scatterItems = new ArrayList(); + ArrayList copyLocalItems = new ArrayList(); + + foreach (AssemblyNameExtension assemblyName in References.Keys) + { + string fusionName = assemblyName.FullName; + Reference reference = GetReference(assemblyName); + + // Conflict victims and badimages are filtered out. + if (!reference.IsBadImage) + { + reference.SetFinalCopyLocalState + ( + assemblyName, + _frameworkPaths, + _targetProcessorArchitecture, + _getRuntimeVersion, + _targetedRuntimeVersion, + _fileExists, + _copyLocalDependenciesWhenParentReferenceInGac, + this, + _checkIfAssemblyIsInGac + ); + + // If mscorlib was found as a dependency and not a primary reference we will assume that mscorlib on the target machine will be ok to use. + // If mscorlib was a primary reference then we may have resolved one which is a differnt version that is on the target + // machine and we should gather it along with the other references. + if (!reference.IsPrimary && IsPseudoAssembly(assemblyName.Name)) + { + continue; + } + + if (reference.IsResolved) + { + ITaskItem referenceItem = SetItemMetadata(relatedItems, satelliteItems, serializationAssemblyItems, scatterItems, fusionName, reference, assemblyName, _fileExists); + + if (reference.IsPrimary) + { + if (!reference.IsBadImage) + { + // Add a primary item. + primaryItems.Add(referenceItem); + } + } + else + { + // Add the reference item. + dependencyItems.Add(referenceItem); + } + } + } + } + + primaryFiles = new ITaskItem[primaryItems.Count]; + primaryItems.CopyTo(primaryFiles, 0); + + dependencyFiles = (ITaskItem[])dependencyItems.ToArray(typeof(ITaskItem)); + relatedFiles = (ITaskItem[])relatedItems.ToArray(typeof(ITaskItem)); + satelliteFiles = (ITaskItem[])satelliteItems.ToArray(typeof(ITaskItem)); + serializationAssemblyFiles = (ITaskItem[])serializationAssemblyItems.ToArray(typeof(ITaskItem)); + scatterFiles = (ITaskItem[])scatterItems.ToArray(typeof(ITaskItem)); + + // Sort for stable outputs. (These came from a hashtable, which as undefined enumeration order.) + Array.Sort(primaryFiles, TaskItemSpecFilenameComparer.comparer); + + // Find the copy-local items. + FindCopyLocalItems(primaryFiles, copyLocalItems); + FindCopyLocalItems(dependencyFiles, copyLocalItems); + FindCopyLocalItems(relatedFiles, copyLocalItems); + FindCopyLocalItems(satelliteFiles, copyLocalItems); + FindCopyLocalItems(serializationAssemblyFiles, copyLocalItems); + FindCopyLocalItems(scatterFiles, copyLocalItems); + copyLocalFiles = (ITaskItem[])copyLocalItems.ToArray(typeof(ITaskItem)); + } + + /// + /// Set metadata on the items which will be output from RAR. + /// + private ITaskItem SetItemMetadata(ArrayList relatedItems, ArrayList satelliteItems, ArrayList serializationAssemblyItems, ArrayList scatterItems, string fusionName, Reference reference, AssemblyNameExtension assemblyName, FileExists fileExists) + { + // Set up the main item. + ITaskItem referenceItem = new TaskItem(); + referenceItem.ItemSpec = reference.FullPath; + referenceItem.SetMetadata(ItemMetadataNames.resolvedFrom, reference.ResolvedSearchPath); + + // Set the CopyLocal metadata. + if (reference.IsCopyLocal) + { + referenceItem.SetMetadata(ItemMetadataNames.copyLocal, "true"); + } + else + { + referenceItem.SetMetadata(ItemMetadataNames.copyLocal, "false"); + } + + // Set the FusionName metadata. + referenceItem.SetMetadata(ItemMetadataNames.fusionName, fusionName); + + // Set the Redist name metadata. + if (!String.IsNullOrEmpty(reference.RedistName)) + { + referenceItem.SetMetadata(ItemMetadataNames.redist, reference.RedistName); + } + + if (Reference.IsFrameworkFile(reference.FullPath, _frameworkPaths) || (_installedAssemblies != null && _installedAssemblies.FrameworkAssemblyEntryInRedist(assemblyName))) + { + if (!IsAssemblyRemovedFromDotNetFramework(assemblyName, reference.FullPath, _frameworkPaths, _installedAssemblies)) + { + referenceItem.SetMetadata(ItemMetadataNames.frameworkFile, "true"); + } + } + + if (!String.IsNullOrEmpty(reference.ImageRuntime)) + { + referenceItem.SetMetadata(ItemMetadataNames.imageRuntime, reference.ImageRuntime); + } + + if (reference.IsWinMDFile) + { + referenceItem.SetMetadata(ItemMetadataNames.winMDFile, "true"); + + // The ImplementationAssembly is only set if the implementation file exits on disk + if (reference.ImplementationAssembly != null) + { + if (VerifyArchitectureOfImplementationDll(reference.ImplementationAssembly, reference.FullPath)) + { + referenceItem.SetMetadata(ItemMetadataNames.winmdImplmentationFile, Path.GetFileName(reference.ImplementationAssembly)); + + // Add the implementation item as a related file + ITaskItem item = new TaskItem(reference.ImplementationAssembly); + // Clone metadata. + referenceItem.CopyMetadataTo(item); + // Related files don't have a fusion name. + item.SetMetadata(ItemMetadataNames.fusionName, ""); + RemoveNonForwardableMetadata(item); + + // Add the related item. + relatedItems.Add(item); + } + } + + if (reference.IsManagedWinMDFile) + { + referenceItem.SetMetadata(ItemMetadataNames.winMDFileType, "Managed"); + } + else + { + referenceItem.SetMetadata(ItemMetadataNames.winMDFileType, "Native"); + } + } + + // Set the IsRedistRoot metadata + if (reference.IsRedistRoot == true) + { + referenceItem.SetMetadata(ItemMetadataNames.isRedistRoot, "true"); + } + else if (reference.IsRedistRoot == false) + { + referenceItem.SetMetadata(ItemMetadataNames.isRedistRoot, "false"); + } + else + { + // This happens when the redist root is "null". This means there + // was no IsRedistRoot flag in the Redist XML (or there was no + // redist XML at all for this item). + } + + // If there was a primary source item, then forward metadata from it. + // It's important that the metadata from the primary source item + // win over the same metadata from other source items, so that's + // why we put this first. (CopyMetadataTo will never override an + // already existing metadata.) For example, if this reference actually + // came directly from an item declared in the project file, we'd + // want to use the metadata from it, not some other random item in + // the project file that happened to have this reference as a dependency. + if (reference.PrimarySourceItem != null) + { + reference.PrimarySourceItem.CopyMetadataTo(referenceItem); + } + else + { + bool hasImplementationFile = referenceItem.GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length > 0; + bool hasImageRuntime = referenceItem.GetMetadata(ItemMetadataNames.imageRuntime).Length > 0; + bool hasWinMDFile = referenceItem.GetMetadata(ItemMetadataNames.winMDFile).Length > 0; + + // If there were non-primary source items, then forward metadata from them. + ICollection sourceItems = reference.GetSourceItems(); + foreach (ITaskItem sourceItem in sourceItems) + { + sourceItem.CopyMetadataTo(referenceItem); + } + + // If the item originally did not have the implementation file metadata then we do not want to get it from the set of primary source items + // since the implementation file is something specific to the source item and not supposed to be propigated. + if (!hasImplementationFile) + { + referenceItem.RemoveMetadata(ItemMetadataNames.winmdImplmentationFile); + } + + // If the item originally did not have the ImageRuntime metadata then we do not want to get it from the set of primary source items + // since the ImageRuntime is something specific to the source item and not supposed to be propigated. + if (!hasImageRuntime) + { + referenceItem.RemoveMetadata(ItemMetadataNames.imageRuntime); + } + + // If the item originally did not have the WinMDFile metadata then we do not want to get it from the set of primary source items + // since the WinMDFile is something specific to the source item and not supposed to be propigated + if (!hasWinMDFile) + { + referenceItem.RemoveMetadata(ItemMetadataNames.winMDFile); + } + } + + if (reference.ReferenceVersion != null) + { + referenceItem.SetMetadata(ItemMetadataNames.version, reference.ReferenceVersion.ToString()); + } + else + { + referenceItem.SetMetadata(ItemMetadataNames.version, String.Empty); + } + + // Now clone all properties onto the related files. + foreach (string relatedFileExtension in reference.GetRelatedFileExtensions()) + { + ITaskItem item = new TaskItem(reference.FullPathWithoutExtension + relatedFileExtension); + // Clone metadata. + referenceItem.CopyMetadataTo(item); + // Related files don't have a fusion name. + item.SetMetadata(ItemMetadataNames.fusionName, ""); + RemoveNonForwardableMetadata(item); + + // Add the related item. + relatedItems.Add(item); + } + + // Set up the satellites. + foreach (string satelliteFile in reference.GetSatelliteFiles()) + { + ITaskItem item = new TaskItem(Path.Combine(reference.DirectoryName, satelliteFile)); + // Clone metadata. + referenceItem.CopyMetadataTo(item); + // Set the destination directory. + item.SetMetadata(ItemMetadataNames.destinationSubDirectory, FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(satelliteFile))); + // Satellite files don't have a fusion name. + item.SetMetadata(ItemMetadataNames.fusionName, ""); + RemoveNonForwardableMetadata(item); + + // Add the satellite item. + satelliteItems.Add(item); + } + + // Set up the serialization assemblies + foreach (string serializationAssemblyFile in reference.GetSerializationAssemblyFiles()) + { + ITaskItem item = new TaskItem(Path.Combine(reference.DirectoryName, serializationAssemblyFile)); + // Clone metadata. + referenceItem.CopyMetadataTo(item); + // serialization assemblies files don't have a fusion name. + item.SetMetadata(ItemMetadataNames.fusionName, ""); + RemoveNonForwardableMetadata(item); + + // Add the serialization assembly item. + serializationAssemblyItems.Add(item); + } + + // Set up the scatter files. + foreach (string scatterFile in reference.GetScatterFiles()) + { + ITaskItem item = new TaskItem(Path.Combine(reference.DirectoryName, scatterFile)); + // Clone metadata. + referenceItem.CopyMetadataTo(item); + // We don't have a fusion name for scatter files. + item.SetMetadata(ItemMetadataNames.fusionName, ""); + RemoveNonForwardableMetadata(item); + + // Add the satellite item. + scatterItems.Add(item); + } + + // As long as the item has not come from somewhere else say it came from rar (p2p's can come from somewhere else). + if (referenceItem.GetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget).Length == 0) + { + referenceItem.SetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget, "ResolveAssemblyReference"); + } + + if (referenceItem.GetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget).Equals("ProjectReference")) + { + if (reference.PrimarySourceItem != null) + { + referenceItem.SetMetadata(ItemMetadataNames.projectReferenceOriginalItemSpec, reference.PrimarySourceItem.GetMetadata("OriginalItemSpec")); + } + } + + return referenceItem; + } + + /// + /// Verify that the implementation dll has a matching architecture to what the project is targeting. + /// + private bool VerifyArchitectureOfImplementationDll(string dllPath, string winmdFile) + { + try + { + UInt16 machineType = _readMachineTypeFromPEHeader(dllPath); + SystemProcessorArchitecture dllArchitecture = SystemProcessorArchitecture.None; + + if (machineType == NativeMethods.IMAGE_FILE_MACHINE_INVALID) + { + throw new BadImageFormatException(ResourceUtilities.FormatResourceString("ResolveAssemblyReference.ImplementationDllHasInvalidPEHeader")); + } + + switch (machineType) + { + case NativeMethods.IMAGE_FILE_MACHINE_AMD64: + dllArchitecture = SystemProcessorArchitecture.Amd64; + break; + case NativeMethods.IMAGE_FILE_MACHINE_ARM: + case NativeMethods.IMAGE_FILE_MACHINE_ARMV7: + dllArchitecture = SystemProcessorArchitecture.Arm; + break; + case NativeMethods.IMAGE_FILE_MACHINE_I386: + dllArchitecture = SystemProcessorArchitecture.X86; + break; + case NativeMethods.IMAGE_FILE_MACHINE_IA64: + dllArchitecture = SystemProcessorArchitecture.IA64; + break; + case NativeMethods.IMAGE_FILE_MACHINE_UNKNOWN: + dllArchitecture = SystemProcessorArchitecture.None; + break; + default: + if (_warnOrErrorOnTargetArchitectureMismatch == WarnOrErrorOnTargetArchitectureMismatchBehavior.Error) + { + _log.LogErrorWithCodeFromResources("ResolveAssemblyReference.UnknownProcessorArchitecture", dllPath, winmdFile, machineType.ToString("X", CultureInfo.InvariantCulture)); + return false; + } + else if (_warnOrErrorOnTargetArchitectureMismatch == WarnOrErrorOnTargetArchitectureMismatchBehavior.Warning) + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.UnknownProcessorArchitecture", dllPath, winmdFile, machineType.ToString("X", CultureInfo.InvariantCulture)); + return true; + } + break; + } + + // If the assembly is MSIL or none it can work anywhere so there does not need to be any warning ect. + if (dllArchitecture == SystemProcessorArchitecture.MSIL || dllArchitecture == SystemProcessorArchitecture.None) + { + return true; + } + + if (_targetProcessorArchitecture != dllArchitecture) + { + if (_warnOrErrorOnTargetArchitectureMismatch == WarnOrErrorOnTargetArchitectureMismatchBehavior.Error) + { + _log.LogErrorWithCodeFromResources("ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArchOfImplementation", ResolveAssemblyReference.ProcessorArchitectureToString(_targetProcessorArchitecture), ResolveAssemblyReference.ProcessorArchitectureToString(dllArchitecture), dllPath, winmdFile); + return false; + } + else if (_warnOrErrorOnTargetArchitectureMismatch == WarnOrErrorOnTargetArchitectureMismatchBehavior.Warning) + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArchOfImplementation", ResolveAssemblyReference.ProcessorArchitectureToString(_targetProcessorArchitecture), ResolveAssemblyReference.ProcessorArchitectureToString(dllArchitecture), dllPath, winmdFile); + } + } + + return true; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw e; + } + + _log.LogErrorWithCodeFromResources("ResolveAssemblyReference.ProblemReadingImplementationDll", dllPath, e.Message); + return false; + } + } + + /// + /// Read the PE header to get the machine type + /// + internal static UInt16 ReadMachineTypeFromPEHeader(string dllPath) + { + /* + At location 0x3c, the stub has the file offset to the PE signature. This information enables Windows to properly execute the image file, even though it has an MS DOS stub. This file offset is placed at location 0x3c during linking. + * After the MS DOS stub, at the file offset specified at offset 0x3c, is a 4-byte signature that identifies the file as a PE format image file. This signature is "PE\0\0" (the letters "P" and "E" followed by two null bytes). + * At the beginning of an object file, or immediately after the signature of an image file, is a standard COFF file header in the following format. Note that the Windows loader limits the number of sections to 96. + Offset + Size Field Description + 0 2 Machine The number that identifies the type of target machine. For more information, see section 3.3.1, "Machine Types." + + IMAGE_FILE_MACHINE_UNKNOWN 0x0 The contents of this field are assumed to be applicable to any machine type + IMAGE_FILE_MACHINE_AMD64 0x8664 x64 + IMAGE_FILE_MACHINE_ARM 0x1c0 ARM little endian + IMAGE_FILE_MACHINE_I386 0x14c Intel 386 or later processors and compatible processors + IMAGE_FILE_MACHINE_IA64 0x200 Intel Itanium processor family + * */ + + UInt16 machineType = NativeMethods.IMAGE_FILE_MACHINE_INVALID; + using (FileStream implementationStream = new FileStream(dllPath, FileMode.Open, FileAccess.Read)) + { + // Seek to location that contains PE offset. + implementationStream.Seek(PEOFFSET, SeekOrigin.Begin); + + using (BinaryReader reader = new BinaryReader(implementationStream)) + { + // Read the offset to the PE header + Int32 offSet = reader.ReadInt32(); + implementationStream.Seek(offSet, SeekOrigin.Begin); + + // Read the PE header should be PE\0\0 + UInt32 peHeader = reader.ReadUInt32(); + if (peHeader == PEHEADER) + { + machineType = reader.ReadUInt16(); + } + } + } + + return machineType; + } + + /// + /// Some metadata should not be forwarded between the parent and child items. + /// + private static void RemoveNonForwardableMetadata(ITaskItem item) + { + item.RemoveMetadata(ItemMetadataNames.winmdImplmentationFile); + item.RemoveMetadata(ItemMetadataNames.imageRuntime); + item.RemoveMetadata(ItemMetadataNames.winMDFile); + } + + + /// + /// Given a list of items, find all that have CopyLocal==true and add it to the list. + /// + /// + /// + private static void FindCopyLocalItems(ITaskItem[] items, ArrayList copyLocalItems) + { + foreach (ITaskItem i in items) + { + bool found; + bool copyLocal = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + i, + ItemMetadataNames.copyLocal, + out found + ); + + + if (found && copyLocal) + { + copyLocalItems.Add(i); + } + } + } + + #region ExclusionList LoggingMessage helpers + + /// + /// The reference was determined to have a version which is higher than what is in the currently targeted redist list. + /// + internal void LogHigherVersionUnresolve(bool displayPrimaryReferenceMessage, AssemblyNameExtension assemblyName, Reference reference, ITaskItem referenceItem, string targetedFramework) + { + if (displayPrimaryReferenceMessage) + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.PrimaryReferenceOutsideOfFramework", reference.PrimarySourceItem.ItemSpec /* primary item spec*/, reference.ReferenceVersion /*Version of dependent assemby*/, reference.ExclusionListLoggingProperties.HighestVersionInRedist /*Version found in redist*/); + } + else + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", referenceItem.ItemSpec /* primary item spec*/, assemblyName.FullName /*Dependent assemblyName*/, reference.ReferenceVersion /*Version of dependent assemby*/, reference.ExclusionListLoggingProperties.HighestVersionInRedist /*Version found in redist*/); + } + } + + /// + /// The reference was determined to have a version which is higher than what is in the currently targeted using the framework attribute. + /// + internal void LogHigherVersionUnresolveDueToAttribute(bool displayPrimaryReferenceMessage, AssemblyNameExtension assemblyName, Reference reference, ITaskItem referenceItem, string targetedFramework) + { + if (displayPrimaryReferenceMessage) + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.PrimaryReferenceOutsideOfFrameworkUsingAttribute", reference.PrimarySourceItem.ItemSpec /* primary item spec*/, reference.FrameworkNameAttribute /*Version of dependent assemby*/, targetedFramework); + } + else + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.DependencyReferenceOutsideOfFrameworkUsingAttribute", referenceItem.ItemSpec /* primary item spec*/, assemblyName.FullName /*Dependent assemblyName*/, reference.FrameworkNameAttribute, targetedFramework); + } + } + + /// + /// The reference was determined to not be in the current redist list but in fact are from another framework. + /// + internal void LogAnotherFrameworkUnResolve(bool displayPrimaryReferenceMessage, AssemblyNameExtension assemblyName, Reference reference, ITaskItem referenceItem, string targetedFramework) + { + if (displayPrimaryReferenceMessage) + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.PrimaryReferenceInAnotherFramework", reference.PrimarySourceItem.ItemSpec /* primary item spec*/, targetedFramework); + } + else + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.DependencyReferenceInAnotherFramework", referenceItem.ItemSpec /* primary item spec*/, assemblyName.FullName /*Dependent assemblyName*/, targetedFramework); + } + } + + /// + /// The reference was found to be resolved from a full framework while we are actually targeting a profile. + /// + internal void LogProfileExclusionUnresolve(bool displayPrimaryReferenceMessage, AssemblyNameExtension assemblyName, Reference reference, ITaskItem referenceItem, string targetedFramework) + { + if (displayPrimaryReferenceMessage) + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.FailedToResolveReferenceBecausePrimaryAssemblyInExclusionList", reference.PrimarySourceItem.ItemSpec, targetedFramework); + } + else + { + _log.LogWarningWithCodeFromResources("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", referenceItem.ItemSpec, assemblyName.FullName, targetedFramework); + } + } + #endregion + + #region Helper structures + + /// + /// Provide a class which has a key value pair for references and their assemblyNameExtensions. + /// This is used to prevent JIT'ing when using a generic list. + /// + internal struct ReferenceAssemblyExtensionPair + { + private Reference _assemblyKey; + private AssemblyNameExtension _assemblyValue; + + internal ReferenceAssemblyExtensionPair(Reference key, AssemblyNameExtension value) + { + _assemblyKey = key; + _assemblyValue = value; + } + + internal Reference Key + { + get { return _assemblyKey; } + } + + internal AssemblyNameExtension Value + { + get { return _assemblyValue; } + } + } + + #endregion + + /// + /// Rather than have exclusion lists float around, we may as well just mark the reference themselves. This allows us to attach to a reference + /// whether or not it is excluded and why. This method will do a number of checks in a specific order and mark the reference as being excluded or not. + /// + internal bool MarkReferencesForExclusion(Hashtable exclusionList) + { + bool anyMarkedReference = false; + _listOfExcludedAssemblies = new List(); + + foreach (AssemblyNameExtension assemblyName in References.Keys) + { + string assemblyFullName = assemblyName.FullName; + Reference reference = GetReference(assemblyName); + reference.ReferenceVersion = assemblyName.Version; + + MarkReferenceWithHighestVersionInCurrentRedistList(assemblyName, reference); + + // If CheckForSpecificVersionMetadataOnParentsReference is passed true then we will return true if any parent primary reference has the specific + // version metadata set to true, + // If false is passed in we will return true ONLY if all parent primary references have the metadata set to true. + if (!reference.CheckForSpecificVersionMetadataOnParentsReference(false)) + { + // Check to see if the reference is not in a profile or subset + if (exclusionList != null) + { + if (exclusionList.ContainsKey(assemblyFullName)) + { + anyMarkedReference = true; + reference.ExclusionListLoggingProperties.ExclusionReasonLogDelegate = new LogExclusionReason(LogProfileExclusionUnresolve); + reference.ExclusionListLoggingProperties.IsInExclusionList = true; + _listOfExcludedAssemblies.Add(assemblyFullName); + } + } + + // Check to see if the reference is in the current target framework but has a higher version than what exists in the target framework + if (!reference.ExclusionListLoggingProperties.IsInExclusionList) + { + if (MarkReferenceForExclusionDueToHigherThanCurrentFramework(assemblyName, reference)) + { + anyMarkedReference = true; + _listOfExcludedAssemblies.Add(assemblyFullName); + } + } + + // Check to see if the reference came from the GAC or AssemblyFolders and is in the highest redist list on the machine for the targeted framework identifier. + if (!reference.ExclusionListLoggingProperties.IsInExclusionList) + { + if (MarkReferencesExcludedDueToOtherFramework(assemblyName, reference)) + { + anyMarkedReference = true; + _listOfExcludedAssemblies.Add(assemblyFullName); + } + } + + // Check to see if the reference is built against a compatible framework + if (!reference.ExclusionListLoggingProperties.IsInExclusionList) + { + if (!_ignoreFrameworkAttributeVersionMismatch && MarkReferenceForExclusionDueToHigherThanCurrentFrameworkAttribute(assemblyName, reference)) + { + anyMarkedReference = true; + _listOfExcludedAssemblies.Add(assemblyFullName); + } + } + } + } + + return anyMarkedReference; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/ResolutionSearchLocation.cs b/src/XMakeTasks/AssemblyDependency/ResolutionSearchLocation.cs new file mode 100644 index 00000000000..6375db0d826 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/ResolutionSearchLocation.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Reflection; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// A place the resolver tried to look for an assembly along with some information + /// that can be used to provide a good error message. + /// + internal class ResolutionSearchLocation + { + private string _fileNameAttempted = null; + private string _searchPath = null; + private AssemblyNameExtension _assemblyName = null; + private NoMatchReason _reason = NoMatchReason.Unknown; + + /// + /// The name of the file that was attempted to match. + /// + internal string FileNameAttempted + { + get { return _fileNameAttempted; } + set { _fileNameAttempted = value; } + } + + /// + /// The literal searchpath element that was used to discover this location. + /// + internal string SearchPath + { + get { return _searchPath; } + set { _searchPath = value; } + } + + /// + /// The name of the assembly found at that location. Will be null if there was no assembly there. + /// + internal AssemblyNameExtension AssemblyName + { + get { return _assemblyName; } + set { _assemblyName = value; } + } + + /// + /// The reason there was no macth. + /// + internal NoMatchReason Reason + { + get { return _reason; } + set { _reason = value; } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/XMakeTasks/AssemblyDependency/ResolveAssemblyReference.cs new file mode 100644 index 00000000000..35f343199b0 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -0,0 +1,2920 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.Globalization; +using System.Text; + +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif +using LogExclusionReason = Microsoft.Build.Tasks.ReferenceTable.LogExclusionReason; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture; +using System.Xml.Linq; + +namespace Microsoft.Build.Tasks +{ + /// + /// Given a list of assemblyFiles, determine the closure of all assemblyFiles that + /// depend on those assemblyFiles including second and nth-order dependencies too. + /// + public class ResolveAssemblyReference : TaskExtension + { + /// + /// key assembly used to trigger inclusion of facade references. + /// + private const string SystemRuntimeAssemblyName = "System.Runtime"; + + /// + /// Delegate to a method that takes a targetFrameworkDirectory and returns an array of redist or subset list paths + /// + /// TargetFramework directory to search for redist or subset list + /// String array of redist or subset lists + private delegate string[] GetListPath(string targetFrameworkDirectory); + + /// + /// Cache of system state information, used to optimize performance. + /// + private SystemState _cache = null; + + /// + /// Construct + /// + public ResolveAssemblyReference() + { + } + + #region Properties + + private ITaskItem[] _assemblyFiles = new TaskItem[0]; + private ITaskItem[] _assemblyNames = new TaskItem[0]; + private ITaskItem[] _installedAssemblyTables = new TaskItem[0]; + private ITaskItem[] _installedAssemblySubsetTables = new TaskItem[0]; + private ITaskItem[] _fullFrameworkAssemblyTables = new TaskItem[0]; + private ITaskItem[] _resolvedSDKReferences = new TaskItem[0]; + private bool _ignoreDefaultInstalledAssemblyTables = false; + private bool _ignoreDefaultInstalledAssemblySubsetTables = false; + private string[] _candidateAssemblyFiles = new string[0]; + private string[] _targetFrameworkDirectories = new string[0]; + private string[] _searchPaths = new string[0]; + private string[] _allowedAssemblyExtensions = new string[] { ".winmd", ".dll", ".exe" }; + private string[] _relatedFileExtensions = new string[] { ".pdb", ".xml", ".pri" }; + private string _appConfigFile = null; + private bool _supportsBindingRedirectGeneration; + private bool _autoUnify = false; + private bool _ignoreVersionForFrameworkReferences = false; + private bool _ignoreTargetFrameworkAttributeVersionMismatch = false; + private ITaskItem[] _resolvedFiles = new TaskItem[0]; + private ITaskItem[] _resolvedDependencyFiles = new TaskItem[0]; + private ITaskItem[] _relatedFiles = new TaskItem[0]; + private ITaskItem[] _satelliteFiles = new TaskItem[0]; + private ITaskItem[] _serializationAssemblyFiles = new TaskItem[0]; + private ITaskItem[] _scatterFiles = new TaskItem[0]; + private ITaskItem[] _copyLocalFiles = new TaskItem[0]; + private ITaskItem[] _suggestedRedirects = new TaskItem[0]; + private string[] _targetFrameworkSubsets = new string[0]; + private string[] _fullTargetFrameworkSubsetNames = new string[0]; + private string _targetedFrameworkMoniker = String.Empty; + + private bool _findDependencies = true; + private bool _findSatellites = true; + private bool _findSerializationAssemblies = true; + private bool _findRelatedFiles = true; + private bool _silent = false; + private string _projectTargetFrameworkAsString = String.Empty; + private string _targetedRuntimeVersionRawValue = String.Empty; + private Version _projectTargetFramework; + + private string _stateFile = null; + private string _targetProcessorArchitecture = null; + + private string _profileName = String.Empty; + private string[] _fullFrameworkFolders = new string[0]; + private string[] _latestTargetFrameworkDirectories = new string[0]; + private bool _copyLocalDependenciesWhenParentReferenceInGac = true; + private Dictionary _showAssemblyFoldersExLocations = new Dictionary(StringComparer.OrdinalIgnoreCase); + private bool _logVerboseSearchResults = false; + private WarnOrErrorOnTargetArchitectureMismatchBehavior _warnOrErrorOnTargetArchitectureMismatch = WarnOrErrorOnTargetArchitectureMismatchBehavior.Warning; + private bool _unresolveFrameworkAssembliesFromHigherFrameworks = false; + + /// + /// If set to true, it forces to unresolve framework assemblies with versions higher or equal the version of the target framework, regardless of the target framework + /// + public bool UnresolveFrameworkAssembliesFromHigherFrameworks + { + get + { + return _unresolveFrameworkAssembliesFromHigherFrameworks; + } + set + { + _unresolveFrameworkAssembliesFromHigherFrameworks = value; + } + } + + /// + /// If there is a mismatch between the targetprocessor architecture and the architecture of a primary reference. + /// + /// When this is error, an error will be logged. + /// + /// When this is warn, if there is a mismatch between the targetprocessor architecture and the architecture of a primary reference a warning will be logged. + /// + /// When this is none, no error or warning will be logged. + /// + public string WarnOrErrorOnTargetArchitectureMismatch + { + get + { + return _warnOrErrorOnTargetArchitectureMismatch.ToString(); + } + + set + { + if (!Enum.TryParse(value, /*ignoreCase*/true, out _warnOrErrorOnTargetArchitectureMismatch)) + { + _warnOrErrorOnTargetArchitectureMismatch = WarnOrErrorOnTargetArchitectureMismatchBehavior.Warning; + } + } + } + /// + /// A list of fully qualified paths-to-assemblyFiles to find dependencies for. + /// + /// Optional attributes are: + /// bool Private [default=true] -- means 'CopyLocal' + /// string FusionName -- the simple or strong fusion name for this item. If this + /// attribute is present it can save time since the assembly file won't need + /// to be opened to get the fusion name. + /// + public ITaskItem[] AssemblyFiles + { + get { return _assemblyFiles; } + set { _assemblyFiles = value; } + } + + /// + /// The list of directories which contain the redist lists for the most current + /// framework which can be targeted on the machine. If this is not set + /// Then we will looks for the highest framework installed on the machine + /// for a given target framework identifier and use that. + /// + public string[] LatestTargetFrameworkDirectories + { + get + { + return _latestTargetFrameworkDirectories; + } + + set + { + _latestTargetFrameworkDirectories = value; + } + } + + /// + /// Should the framework attribute be ignored when checking to see if an assembly is compatible with the targeted framework. + /// + public bool IgnoreTargetFrameworkAttributeVersionMismatch + { + get + { + return _ignoreTargetFrameworkAttributeVersionMismatch; + } + + set + { + _ignoreTargetFrameworkAttributeVersionMismatch = value; + } + } + + /// + /// List of target framework subset names which will be searched for in the target framework directories + /// + public string[] TargetFrameworkSubsets + { + get { return _targetFrameworkSubsets; } + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "TargetFrameworkSubsets"); + _targetFrameworkSubsets = value; + } + } + + /// + /// These can either be simple fusion names like: + /// + /// System + /// + /// or strong names like + /// + /// System, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + /// + /// These names will be resolved into full paths and all dependencies will be found. + /// + /// Optional attributes are: + /// bool Private [default=true] -- means 'CopyLocal' + /// string HintPath [default=''] -- location of file name to consider as a reference, + /// used when {HintPathFromItem} is one of the paths in SearchPaths. + /// bool SpecificVersion [default=absent] -- + /// when true, the exact fusionname in the Include must be matched. + /// when false, any assembly with the same simple name will be a match. + /// when absent, then look at the value in Include. + /// If its a simple name then behave as if specific version=false. + /// If its a strong name name then behave as if specific version=true. + /// string ExecutableExtension [default=absent] -- + /// when present, the resolved assembly must have this extension. + /// when absent, .dll is considered and then .exe for each directory looked at. + /// string SubType -- only items with empty SubTypes will be considered. Items + /// with non-empty subtypes will be ignored. + /// string AssemblyFolderKey [default=absent] -- supported for legacy AssemblyFolder + /// resolution. This key can have a value like 'hklm\vendor folder'. When set, only + /// this particular assembly folder key will be used. + /// This is to support the scenario in VSWhidey#357946 in which there are multiple + /// side-by-side libraries installed and the user wants to pick an exact version. + /// bool EmbedInteropTyeps [default=absent] -- + /// when true, we should treat this assembly as if it has no dependencies and should + /// be completely embedded into the target assembly. + /// + public ITaskItem[] Assemblies + { + get { return _assemblyNames; } + set { _assemblyNames = value; } + } + + /// + /// A list of assembly files that can be part of the search and resolution process. + /// These must be absolute filesnames, or project-relative filenames. + /// + /// Assembly files in this list will be considered when SearchPaths contains + /// {CandidateAssemblyFiles} as one of the paths to consider. + /// + public string[] CandidateAssemblyFiles + { + get { return _candidateAssemblyFiles; } + set { _candidateAssemblyFiles = value; } + } + + /// + /// A list of resolved SDK references which contain the sdk name, sdk location and the targeted configuration. + /// These locations will only be searched if the reference has the SDKName metadata attached to it. + /// + public ITaskItem[] ResolvedSDKReferences + { + get { return _resolvedSDKReferences; } + set { _resolvedSDKReferences = value; } + } + + /// + /// Path to the target frameworks directory. Required to figure out CopyLocal status + /// for resulting items. + /// If not present, then no resulting items will be deemed CopyLocal='true' unless they explicity + /// have a Private='true' attribute on their source item. + /// + public string[] TargetFrameworkDirectories + { + get { return _targetFrameworkDirectories; } + set { _targetFrameworkDirectories = value; } + } + + /// + /// A list of XML files that contain assemblies that are expected to be installed on the target machine. + /// + /// Format of the file is like: + /// + /// + /// + /// etc. + /// + /// + /// When present, assemblies from this list will be candidates to automatically "unify" from prior versions up to + /// the version listed in the XML. Also, assemblies with InGAC='true' will be considered prerequisites and will be CopyLocal='false' + /// unless explicitly overridden. + /// Items in this list may optionally specify the "FrameworkDirectory" metadata to associate an InstalledAssemblyTable + /// with a particular framework directory. However, this setting will be ignored unless the Redist name begins with + /// "Microsoft-Windows-CLRCoreComp". + /// If there is only a single TargetFrameworkDirectories element, then any items in this list missing the + /// "FrameworkDirectory" metadata will be treated as though this metadata is set to the lone (unique) value passed + /// to TargetFrameworkDirectories. + /// + public ITaskItem[] InstalledAssemblyTables + { + get { return _installedAssemblyTables; } + set { _installedAssemblyTables = value; } + } + + /// + /// A list of XML files that contain assemblies that are expected to be in the target subset + /// + /// Format of the file is like: + /// + /// + /// + /// etc. + /// + /// + /// Items in this list may optionally specify the "FrameworkDirectory" metadata to associate an InstalledAssemblySubsetTable + /// with a particular framework directory. + /// If there is only a single TargetFrameworkDirectories element, then any items in this list missing the + /// "FrameworkDirectory" metadata will be treated as though this metadata is set to the lone (unique) value passed + /// to TargetFrameworkDirectories. + /// + public ITaskItem[] InstalledAssemblySubsetTables + { + get { return _installedAssemblySubsetTables; } + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "InstalledAssemblySubsetTables"); + _installedAssemblySubsetTables = value; + } + } + + /// + /// A list of XML files that contain the full framework for the profile. + /// + /// Normally nothing is passed in here, this is for the cases where the location of the xml file for the full framework + /// is not under a RedistList folder. + /// + /// Format of the file is like: + /// + /// + /// + /// etc. + /// + /// + /// Items in this list must specify the "FrameworkDirectory" metadata to associate an redist list + /// with a particular framework directory. If the association is not made an error will be logged. The reason is, + /// The logic in rar assumes if a FrameworkDirectory is not set it will use the target framework directory. + /// + public ITaskItem[] FullFrameworkAssemblyTables + { + get { return _fullFrameworkAssemblyTables; } + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "FullFrameworkAssemblyTables"); + _fullFrameworkAssemblyTables = value; + } + } + + /// + /// [default=false] + /// Boolean property to control whether or not the task should look for and use additional installed + /// assembly tables (a.k.a Redist Lists) found in the RedistList directory underneath the provided + /// TargetFrameworkDirectories. + /// + public bool IgnoreDefaultInstalledAssemblyTables + { + get { return _ignoreDefaultInstalledAssemblyTables; } + set { _ignoreDefaultInstalledAssemblyTables = value; } + } + + /// + /// [default=false] + /// Boolean property to control whether or not the task should look for and use additional installed + /// assembly subset tables (a.k.a Subset Lists) found in the SubsetList directory underneath the provided + /// TargetFrameworkDirectories. + /// + public bool IgnoreDefaultInstalledAssemblySubsetTables + { + get { return _ignoreDefaultInstalledAssemblySubsetTables; } + set { _ignoreDefaultInstalledAssemblySubsetTables = value; } + } + + /// + /// If the primary reference is a framework assembly ignore its version information and actually resolve the framework assembly from the currently targeted framework. + /// + public bool IgnoreVersionForFrameworkReferences + { + get { return _ignoreVersionForFrameworkReferences; } + set { _ignoreVersionForFrameworkReferences = value; } + } + + /// + /// The preferred target processor architecture. Used for resolving {GAC} references. + /// Should be like x86, IA64 or AMD64. + /// + /// This is the order of preference: + /// (1) Assemblies in the GAC that match the supplied ProcessorArchitecture. + /// (2) Assemblies in the GAC that have ProcessorArchitecture=MSIL + /// (3) Assemblies in the GAC that have no ProcessorArchitecture. + /// + /// If absent, then only consider assemblies in the GAC that have ProcessorArchitecture==MSIL or + /// no ProcessorArchitecture (these are pre-Whidbey assemblies). + /// + public string TargetProcessorArchitecture + { + get { return _targetProcessorArchitecture; } + set { _targetProcessorArchitecture = value; } + } + + /// + /// What is the runtime we are targeting, is it 2.0.57027 or anotherone, It can have a v or not prefixed onto it. + /// + public string TargetedRuntimeVersion + { + get { return _targetedRuntimeVersionRawValue; } + set { _targetedRuntimeVersionRawValue = value; } + } + + /// + /// List of locations to search for assemblyFiles when resolving dependencies. + /// The following types of things can be passed in here: + /// (1) A plain old directory path. + /// (2) {HintPathFromItem} -- Look at the HintPath attribute from the base item. + /// This attribute must be a file name *not* a directory name. + /// (3) {CandidateAssemblyFiles} -- Look at the files passed in through the CandidateAssemblyFiles + /// parameter. + /// (4) {Registry:_AssemblyFoldersBase_,_RuntimeVersion_,_AssemblyFoldersSuffix_} + /// Where: + /// + /// _AssemblyFoldersBase_ = Software\Microsoft\[.NetFramework | .NetCompactFramework] + /// _RuntimeVersion_ = the runtime version property from the project file + /// _AssemblyFoldersSuffix_ = [ PocketPC | SmartPhone | WindowsCE]\AssemblyFoldersEx + /// + /// Then look in the registry for keys with the following schema: + /// + /// [HKLM | HKCU]\SOFTWARE\MICROSOFT\.NetFramework\ + /// v1.0.3705 + /// AssemblyFoldersEx + /// ControlVendor.GridControl.1.0: + /// @Default = c:\program files\ControlVendor\grid control\1.0\bin + /// @Description = Grid Control for .NET version 1.0 + /// 9466 + /// @Default = c:\program files\ControlVendor\grid control\1.0sp1\bin + /// @Description = SP1 for Grid Control for .NET version 1.0 + /// + /// The based registry key is composed as: + /// + /// [HKLM | HKCU]\_AssemblyFoldersBase_\_RuntimeVersion_\_AssemblyFoldersSuffix_ + /// + /// (5) {AssemblyFolders} -- Use the VisualStudion 2003 .NET finding-assemblies-from-registry scheme. + /// (6) {GAC} -- Look in the GAC. + /// (7) {RawFileName} -- Consider the Include value to be an exact path and file name. + /// + /// + /// + /// + [Required] + public string[] SearchPaths + { + get { return _searchPaths; } + set { _searchPaths = value; } + } + + /// + /// [default=.exe;.dll] + /// These are the assembly extensions that will be considered during references resolution. + /// + public string[] AllowedAssemblyExtensions + { + get { return _allowedAssemblyExtensions; } + set { _allowedAssemblyExtensions = value; } + } + + + /// + /// [default=.pdb;.xml] + /// These are the extensions that will be considered when looking for related files. + /// + public string[] AllowedRelatedFileExtensions + { + get { return _relatedFileExtensions; } + set { _relatedFileExtensions = value; } + } + + + /// + /// If this file name is passed in, then we parse it as an app.config file and extract bindingRedirect mappings. These mappings are used in the dependency + /// calculation process to remap versions of assemblies. + /// + /// If this parameter is passed in, then AutoUnify must be false, otherwise error. + /// + /// + public string AppConfigFile + { + get { return _appConfigFile; } + set { _appConfigFile = value; } + } + + /// + /// This is true if the project type supports "AutoGenerateBindingRedirects" (currently only for EXE projects). + /// + /// + public bool SupportsBindingRedirectGeneration + { + get { return _supportsBindingRedirectGeneration; } + set { _supportsBindingRedirectGeneration = value; } + } + + /// + /// [default=false] + /// This parameter is used for building assemblies, such as DLLs, which cannot have a normal + /// App.Config file. + /// + /// When true, the resulting dependency graph is automatically treated as if there were an + /// App.Config file passed in to the AppConfigFile parameter. This virtual + /// App.Config file has a bindingRedirect entry for each conflicting set of assemblies such + /// that the highest version assembly is chosen. A consequence of this is that there will never + /// be a warning about conflicting assemblies because every conflict will have been resolved. + /// + /// When true, each distinct remapping will result in a high priority comment indicating the + /// old and new versions and the fact that this was done automatically because AutoUnify was true. + /// + /// When true, the AppConfigFile parameter should be empty. Otherwise, it's an + /// error. + /// + /// When false, no assembly version remapping will occur automatically. When two versions of an + /// assembly are present, there will be a warning. + /// + /// When false, each distinct conflict between different versions of the same assembly will + /// result in a high priority comment. After all of these comments are displayed, there will be + /// a single warning with a unique error code and text that reads "Found conflicts between + /// different versions of reference and dependent assemblies". + /// + /// + public bool AutoUnify + { + get { return _autoUnify; } + set { _autoUnify = value; } + } + + + /// + /// When determining if a dependency should be copied locally one of the checks done is to see if the + /// parent reference in the project file has the Private metadata set or not. If that metadata is set then + /// We will use that for the dependency as well. + /// + /// However, if the metadata is not set then the dependency will go through the same checks as the parent reference. + /// One of these checks is to see if the reference is in the GAC. If a reference is in the GAC then we will not copy it locally + /// as it is assumed it will be in the gac on the target machine as well. However this only applies to that specific reference and not its dependencies. + /// + /// This means a reference in the project file may be copy local false due to it being in the GAC but the dependencies may still be copied locally because they are not in the GAC. + /// This is the default behavior for RAR and causes the default value for this property to be true. + /// + /// When this property is false we will still check project file references to see if they are in the GAC and set their copy local state as appropriate. + /// However for dependencies we will not only check to see if they are in the GAC but we will also check to see if the parent reference from the project file is in the GAC. + /// If the parent reference from the project file is in the GAC then we will not copy the dependency locally. + /// + /// NOTE: If there are multiple parent reference and ANY of them does not come from the GAC then we will set copy local to true. + /// + public bool CopyLocalDependenciesWhenParentReferenceInGac + { + get { return _copyLocalDependenciesWhenParentReferenceInGac; } + set { _copyLocalDependenciesWhenParentReferenceInGac = value; } + } + + /// + /// An optional file name that indicates where to save intermediate build state + /// for this task. If not specified, then no inter-build caching will occur. + /// + /// + public string StateFile + { + get { return _stateFile; } + set { _stateFile = value; } + } + + /// + /// If set, then dependencies will be found. Otherwise, only Primary references will be + /// resolved. + /// + /// Default is true. + /// + /// + public bool FindDependencies + { + get { return _findDependencies; } + set { _findDependencies = value; } + } + + /// + /// If set, then satellites will be found. + /// + /// Default is true. + /// + /// + public bool FindSatellites + { + get { return _findSatellites; } + set { _findSatellites = value; } + } + + /// + /// If set, then serialization assemblies will be found. + /// + /// Default is true. + /// + /// + public bool FindSerializationAssemblies + { + get { return _findSerializationAssemblies; } + set { _findSerializationAssemblies = value; } + } + + /// + /// If set, then related files (.pdbs and .xmls) will be found. + /// + /// Default is true. + /// + /// + public bool FindRelatedFiles + { + get { return _findRelatedFiles; } + set { _findRelatedFiles = value; } + } + + /// + /// If set, then don't log any messages to the screen. + /// + /// Default is false. + /// + /// + public bool Silent + { + get { return _silent; } + set { _silent = value; } + } + + /// + /// The project target framework version. + /// + /// Default is empty. which means there will be no filtering for the reference based on their target framework. + /// + /// + public string TargetFrameworkVersion + { + get { return _projectTargetFrameworkAsString; } + set { _projectTargetFrameworkAsString = value; } + } + + /// + /// The target framework moniker we are targeting if any. This is used for logging purposes. + /// + /// Default is empty. + /// + /// + public string TargetFrameworkMoniker + { + get { return _targetedFrameworkMoniker; } + set { _targetedFrameworkMoniker = value; } + } + + /// + /// The display name of the target framework moniker, if any. This is only for logging. + /// + public string TargetFrameworkMonikerDisplayName + { + get; + set; + } + + /// + /// Provide a set of names which if seen in the TargetFrameworkSubset list will cause the ignoring + /// of TargetFrameworkSubsets. + /// + /// Full, Complete + /// + public string[] FullTargetFrameworkSubsetNames + { + get + { + return _fullTargetFrameworkSubsetNames; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "FullTargetFrameworkSubsetNames"); + _fullTargetFrameworkSubsetNames = value; + } + } + + /// + /// Name of the target framework profile we are targeting. + /// Eg. Client, Web, or Network + /// + public string ProfileName + { + get + { + return _profileName; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "profileName"); + _profileName = value; + } + } + + /// + /// Set of folders which containd a RedistList directory which represent the full framework for a given client profile. + /// An example would be + /// %programfiles%\reference assemblies\microsoft\framework\v4.0 + /// + public string[] FullFrameworkFolders + { + get + { + return _fullFrameworkFolders; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "FullFrameworkFolders"); + _fullFrameworkFolders = value; + } + } + + /// + /// Default false. Force SystemRuntimeDependency check to include direct dependencies of primary references even if FindDependencies is false + /// + public bool ForceSystemRuntimeDependencyCalculation + { + get; + set; + } + + /// + /// This is a list of all primary references resolved to full paths. + /// bool CopyLocal - whether the given reference should be copied to the output directory. + /// string FusionName - the fusion name for this dependency. + /// string ResolvedFrom - the literal search path that this file was resolved from. + /// bool IsRedistRoot - Whether or not this assembly is the representative for an entire redist. + /// 'true' means the assembly is representative of an entire redist and should be indicated as + /// an application dependency in an application manifest. + /// 'false' means the assembly is internal to a redist and should not be part of the + /// application manifest. + /// string Redist - The name (if any) of the redist that contains this assembly. + /// + [Output] + public ITaskItem[] ResolvedFiles + { + get { return _resolvedFiles; } + } + + /// + /// A list of all n-th order paths-to-dependencies with the following attributes: + /// bool CopyLocal - whether the given reference should be copied to the output directory. + /// string FusionName - the fusion name for this dependency. + /// string ResolvedFrom - the literal search path that this file was resolved from. + /// bool IsRedistRoot - Whether or not this assembly is the representative for an entire redist. + /// 'true' means the assembly is representative of an entire redist and should be indicated as + /// an application dependency in an application manifest. + /// 'false' means the assembly is internal to a redist and should not be part of the + /// application manifest. + /// string Redist - The name (if any) of the redist that contains this assembly. + /// Does not include first order primary references--this list is in ResolvedFiles. + /// + [Output] + public ITaskItem[] ResolvedDependencyFiles + { + get { return _resolvedDependencyFiles; } + } + + /// + /// Related files are files like intellidoc (.XML) and symbols (.PDB) that have the same base + /// name as a reference. + /// bool Primary [always false] - true if this assembly was passed in with Assemblies. + /// bool CopyLocal - whether the given reference should be copied to the output directory. + /// + [Output] + public ITaskItem[] RelatedFiles + { + get { return _relatedFiles; } + } + + /// + /// Any satellite files found. These will be CopyLocal=true iff the reference or dependency + /// that caused this item to exist is CopyLocal=true. + /// bool CopyLocal - whether the given reference should be copied to the output directory. + /// string DestinationSubDirectory - the relative destination directory that this file + /// should be copied to. This is mainly for satellites. + /// + [Output] + public ITaskItem[] SatelliteFiles + { + get { return _satelliteFiles; } + } + + /// + /// Any XML serialization assemblies found. These will be CopyLocal=true iff the reference or dependency + /// that caused this item to exist is CopyLocal=true. + /// bool CopyLocal - whether the given reference should be copied to the output directory. + /// + [Output] + public ITaskItem[] SerializationAssemblyFiles + { + get { return _serializationAssemblyFiles; } + } + + /// + /// Scatter files associated with one of the given assemblies. + /// bool CopyLocal - whether the given reference should be copied to the output directory. + /// + [Output] + public ITaskItem[] ScatterFiles + { + get { return _scatterFiles; } + } + + /// + /// Returns every file in ResolvedFiles+ResolvedDependencyFiles+RelatedFiles+SatelliteFiles+ScatterFiles+SatelliteAssemblyFiles + /// that have CopyLocal flags set to 'true'. + /// + /// + [Output] + public ITaskItem[] CopyLocalFiles + { + get { return _copyLocalFiles; } + } + + /// + /// Regardless of the value of AutoUnify, returns one item for every distinct conflicting assembly + /// identity--including culture and PKT--that was found that did not have a suitable bindingRedirect + /// entry in the ApplicationConfigurationFile. + /// + /// Each returned ITaskItem will have the following values: + /// ItemSpec - the full fusion name of the assembly family with empty version=0.0.0.0 + /// MaxVersion - the maximum version number. + /// + [Output] + public ITaskItem[] SuggestedRedirects + { + get { return _suggestedRedirects; } + } + + /// + /// Storage for names of all files writen to disk. + /// + private ArrayList _filesWritten = new ArrayList(); + + /// + /// The names of all files written to disk. + /// + [Output] + public ITaskItem[] FilesWritten + { + set { /*Do Nothing, Inputs not Allowed*/ } + get { return (ITaskItem[])_filesWritten.ToArray(typeof(ITaskItem)); } + } + + /// + /// Whether the assembly or any of its primary references depends on system.runtime. (Aka needs Facade references to resolve duplicate types) + /// + [Output] + public String DependsOnSystemRuntime + { + get; + private set; + } + + #endregion + #region Logging + + /// + /// Log the results. + /// + /// Reference table. + /// Array of ideal assembly remappings. + /// Array of identities of ideal assembly remappings. + /// List of exceptions that were not attributable to a particular fusion name. + /// + private bool LogResults + ( + ReferenceTable dependencyTable, + DependentAssembly[] idealAssemblyRemappings, + AssemblyNameReference[] idealAssemblyRemappingsIdentities, + ArrayList generalResolutionExceptions + ) + { + bool success = true; +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildRARLogResultsBegin, CodeMarkerEvent.perfMSBuildRARLogResultsEnd)) +#endif + { + /* + PERF NOTE: The Silent flag turns off logging completely from the task side. This means + we avoid the String.Formats that would normally occur even if the verbosity was set to + quiet at the engine level. + */ + if (!Silent) + { + // First, loop over primaries and display information. + foreach (AssemblyNameExtension assemblyName in dependencyTable.References.Keys) + { + string fusionName = assemblyName.FullName; + Reference primaryCandidate = dependencyTable.GetReference(assemblyName); + + if (primaryCandidate.IsPrimary && !(primaryCandidate.IsConflictVictim && primaryCandidate.IsCopyLocal)) + { + LogReference(primaryCandidate, fusionName); + } + } + + // Second, loop over dependencies and display information. + foreach (AssemblyNameExtension assemblyName in dependencyTable.References.Keys) + { + string fusionName = assemblyName.FullName; + Reference dependencyCandidate = dependencyTable.GetReference(assemblyName); + + if (!dependencyCandidate.IsPrimary && !(dependencyCandidate.IsConflictVictim && dependencyCandidate.IsCopyLocal)) + { + LogReference(dependencyCandidate, fusionName); + } + } + + // Third, show conflicts and their resolution. + foreach (AssemblyNameExtension assemblyName in dependencyTable.References.Keys) + { + string fusionName = assemblyName.FullName; + Reference conflictCandidate = dependencyTable.GetReference(assemblyName); + + if (conflictCandidate.IsConflictVictim) + { + LogConflict(conflictCandidate, fusionName); + + // Log the assemblies and primary source items which are related to the conflict which was just logged. + Reference victor = dependencyTable.GetReference(conflictCandidate.ConflictVictorName); + + // Log the winner of the conflict resolution, the source items and dependencies which caused it + LogReferenceDependenciesAndSourceItems(conflictCandidate.ConflictVictorName.FullName, victor); + + // Log the reference which lost the conflict and the dependencies and source items which caused it. + LogReferenceDependenciesAndSourceItems(fusionName, conflictCandidate); + } + } + + // Fourth, if there were any suggested redirects. Show one message per redirect and a single warning. + if (idealAssemblyRemappings != null) + { + bool foundAtLeastOneValidBindingRedirect = false; + bool foundAtLeastOneUnresolvableConflict = false; + + var buffer = new StringBuilder(); + var ns = XNamespace.Get("urn:schemas-microsoft-com:asm.v1"); + + // A high-priority message for each individual redirect. + for (int i = 0; i < idealAssemblyRemappings.Length; i++) + { + DependentAssembly idealRemapping = idealAssemblyRemappings[i]; + AssemblyName idealRemappingPartialAssemblyName = idealRemapping.PartialAssemblyName; + Reference reference = idealAssemblyRemappingsIdentities[i].reference; + + for (int j = 0; j < idealRemapping.BindingRedirects.Length; j++) + { + AssemblyNameExtension[] conflictVictims = reference.GetConflictVictims(); + foreach (AssemblyNameExtension conflictVictim in conflictVictims) + { + // Make note we only output a conflict suggestion if the reference has at + // least one conflict victim - that way we don't suggest redirects to + // assemblies that don't exist at runtime. For example, this avoids us suggesting + // a redirect from Foo 1.0.0.0 -> 2.0.0.0 in the following: + // + // Project -> Foo, 1.0.0.0 + // Project -> Bar -> Foo, 2.0.0.0 + // + // Above, Foo, 1.0.0.0 wins out and is copied to the output directory because + // it is a primary reference. + foundAtLeastOneValidBindingRedirect = true; + + Reference victimReference = dependencyTable.GetReference(conflictVictim); + var newVerStr = idealRemapping.BindingRedirects[j].NewVersion.ToString(); + Log.LogMessageFromResources + ( + MessageImportance.High, + "ResolveAssemblyReference.ConflictRedirectSuggestion", + idealRemappingPartialAssemblyName, + conflictVictim.Version, + victimReference.FullPath, + newVerStr, + reference.FullPath + ); + + if (!SupportsBindingRedirectGeneration && !AutoUnify) + { + // When running against projects types (such as Web Projects) where we can't auto-generate + // binding redirects during the build, populate a buffer (to be output below) with the + // binding redirect syntax that users need to add manually to the App.Config. + + var assemblyIdentityAttributes = new List(4); + + assemblyIdentityAttributes.Add(new XAttribute("name", idealRemappingPartialAssemblyName.Name)); + + // We use "neutral" for "Invariant Language (Invariant Country)" in assembly names. + var cultureString = idealRemappingPartialAssemblyName.CultureName; + assemblyIdentityAttributes.Add(new XAttribute("culture", String.IsNullOrEmpty(idealRemappingPartialAssemblyName.CultureName) ? "neutral" : idealRemappingPartialAssemblyName.CultureName)); + + var publicKeyToken = idealRemappingPartialAssemblyName.GetPublicKeyToken(); + assemblyIdentityAttributes.Add(new XAttribute("publicKeyToken", ResolveAssemblyReference.ByteArrayToString(publicKeyToken))); + + var node = new XElement( + ns + "assemblyBinding", + new XElement( + ns + "dependentAssembly", + new XElement( + ns + "assemblyIdentity", + assemblyIdentityAttributes), + new XElement( + ns + "bindingRedirect", + new XAttribute("oldVersion", "0.0.0.0-" + newVerStr), + new XAttribute("newVersion", newVerStr)))); + + buffer.Append(node.ToString(SaveOptions.DisableFormatting)); + } + } + + if (conflictVictims.Length == 0) + { + foundAtLeastOneUnresolvableConflict = true; + } + } + } + + // Log the warning + if (idealAssemblyRemappings.Length > 0 && foundAtLeastOneValidBindingRedirect) + { + if (SupportsBindingRedirectGeneration) + { + if (!AutoUnify) + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.TurnOnAutoGenerateBindingRedirects"); + } + // else we'll generate bindingRedirects to address the remappings + } + else if (!AutoUnify) + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.SuggestedRedirects", buffer.ToString()); + } + // else AutoUnify is on and bindingRedirect generation is not supported + // we don't warn in this case since the binder will automatically unify these remappings + } + + if (foundAtLeastOneUnresolvableConflict) + { + // This warning is logged regardless of AutoUnify since it means a conflict existed where the reference + // chosen was not the conflict victor in a version comparison, in other words it was older. + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.FoundConflicts"); + } + } + + // Fifth, log general resolution problems. + + // Log general resolution exceptions. + foreach (Exception error in generalResolutionExceptions) + { + if (error is InvalidReferenceAssemblyNameException) + { + InvalidReferenceAssemblyNameException e = (InvalidReferenceAssemblyNameException)error; + Log.LogWarningWithCodeFromResources("General.MalformedAssemblyName", e.SourceItemSpec); + } + else + { + // An unknown Exception type was returned. Just throw. + throw error; + } + } + } + } + + if (dependencyTable.Resolvers != null) + { + foreach (Resolver r in dependencyTable.Resolvers) + { + if (r is AssemblyFoldersExResolver) + { + AssemblyFoldersEx assemblyFoldersEx = ((AssemblyFoldersExResolver)r).AssemblyFoldersExLocations; + + MessageImportance messageImportance = MessageImportance.Low; + if (assemblyFoldersEx != null && _showAssemblyFoldersExLocations.TryGetValue(r.SearchPath, out messageImportance)) + { + Log.LogMessageFromResources(messageImportance, "ResolveAssemblyReference.AssemblyFoldersExSearchLocations", r.SearchPath); + foreach (AssemblyFoldersExInfo info in assemblyFoldersEx) + { + Log.LogMessageFromResources(messageImportance, "ResolveAssemblyReference.EightSpaceIndent", info.DirectoryPath); + } + } + } + } + } + + return success; + } + + /// + /// Used to generate the string representation of a public key token. + /// + internal static string ByteArrayToString(byte[] a) + { + if (a == null) + { + return null; + } + + var buffer = new StringBuilder(a.Length * 2); + for (int i = 0; i < a.Length; ++i) + buffer.Append(a[i].ToString("x2", CultureInfo.InvariantCulture)); + + return buffer.ToString(); + } + + /// + /// Log the source items and dependencies which lead to a given item. + /// + private void LogReferenceDependenciesAndSourceItems(string fusionName, Reference conflictCandidate) + { + ErrorUtilities.VerifyThrowInternalNull(conflictCandidate, "ConflictCandidate"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", ResourceUtilities.FormatResourceString("ResolveAssemblyReference.ReferenceDependsOn", fusionName, conflictCandidate.FullPath)); + + if (conflictCandidate.IsPrimary) + { + if (conflictCandidate.IsResolved) + { + LogDependeeReference(conflictCandidate); + } + else + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.EightSpaceIndent", ResourceUtilities.FormatResourceString("ResolveAssemblyReference.UnResolvedPrimaryItemSpec", conflictCandidate.PrimarySourceItem)); + } + } + + // Log the references for the conflict victim + foreach (Reference dependeeReference in conflictCandidate.GetDependees()) + { + LogDependeeReference(dependeeReference); + } + } + + /// + /// Log the dependee and the item specs which caused the dependee reference to be resolved. + /// + /// + private void LogDependeeReference(Reference dependeeReference) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.EightSpaceIndent", dependeeReference.FullPath); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.TenSpaceIndent", ResourceUtilities.FormatResourceString("ResolveAssemblyReference.PrimarySourceItemsForReference", dependeeReference.FullPath)); + foreach (ITaskItem sourceItem in dependeeReference.GetSourceItems()) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.TwelveSpaceIndent", sourceItem.ItemSpec); + } + } + + /// + /// Display the information about how a reference was resolved. + /// + /// The reference information + /// The fusion name of the reference. + private void LogReference(Reference reference, string fusionName) + { + // Set an importance level to be used for secondary messages. + MessageImportance importance = ChooseReferenceLoggingImportance(reference); + + // Log the fusion name and whether this is a primary or a dependency. + LogPrimaryOrDependency(reference, fusionName, importance); + + // Are there errors to report for this item? + LogReferenceErrors(reference, importance); + + // Show the full name. + LogFullName(reference, importance); + + // If there is a list of assemblyFiles that was considered but then rejected, + // show information about them. + LogAssembliesConsideredAndRejected(reference, importance); + + if (!reference.IsBadImage) + { + // Show the files that made this dependency necessary. + LogDependees(reference, importance); + + // If there were any related files (like pdbs and xmls) then show them here. + LogRelatedFiles(reference, importance); + + // If there were any satellite files then show them here. + LogSatellites(reference, importance); + + // If there were any scatter files then show them. + LogScatterFiles(reference, importance); + + // Show the CopyLocal state + LogCopyLocalState(reference, importance); + + // Show the CopyLocal state + LogImageRuntime(reference, importance); + } + } + + /// + /// Choose an importance level for reporting information about this reference. + /// + /// The reference. + private MessageImportance ChooseReferenceLoggingImportance(Reference reference) + { + MessageImportance importance = MessageImportance.Low; + + bool hadProblems = reference.GetErrors().Count > 0; + + // No problems means low importance. + if (hadProblems) + { + if (reference.IsPrimary || reference.IsCopyLocal) + { + // The user cares more about Primary files and CopyLocal files. + // Accordingly, we show messages about these files only in the higher verbosity levels + // but only if there were errors during the resolution process. + importance = MessageImportance.Normal; + } + } + + return importance; + } + + /// + /// Log all task inputs. + /// + private void LogInputs() + { + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "TargetFrameworkMoniker"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", _targetedFrameworkMoniker); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "TargetFrameworkMonikerDisplayName"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", TargetFrameworkMonikerDisplayName); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "TargetedRuntimeVersion"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", _targetedRuntimeVersionRawValue); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "Assemblies"); + foreach (ITaskItem item in Assemblies) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", item.ItemSpec); + LogAttribute(item, ItemMetadataNames.privateMetadata); + LogAttribute(item, ItemMetadataNames.hintPath); + LogAttribute(item, ItemMetadataNames.specificVersion); + LogAttribute(item, ItemMetadataNames.embedInteropTypes); + LogAttribute(item, ItemMetadataNames.executableExtension); + LogAttribute(item, ItemMetadataNames.subType); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "AssemblyFiles"); + foreach (ITaskItem item in AssemblyFiles) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", item.ItemSpec); + LogAttribute(item, ItemMetadataNames.privateMetadata); + LogAttribute(item, ItemMetadataNames.fusionName); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "CandidateAssemblyFiles"); + foreach (string file in CandidateAssemblyFiles) + { + try + { + if (FileUtilities.HasExtension(file, _allowedAssemblyExtensions)) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", file); + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + // Invalid file path. + throw new InvalidParameterValueException("CandidateAssemblyFiles", file, e.Message); + } + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "TargetFrameworkDirectories"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", String.Join(",", TargetFrameworkDirectories)); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "InstalledAssemblyTables"); + foreach (ITaskItem installedAssemblyTable in InstalledAssemblyTables) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", installedAssemblyTable); + LogAttribute(installedAssemblyTable, ItemMetadataNames.frameworkDirectory); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "IgnoreInstalledAssemblyTable"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", _ignoreDefaultInstalledAssemblyTables); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "SearchPaths"); + foreach (string path in SearchPaths) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", path); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "AllowedAssemblyExtensions"); + foreach (string allowedAssemblyExtension in _allowedAssemblyExtensions) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", allowedAssemblyExtension); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "AllowedRelatedFileExtensions"); + foreach (string allowedRelatedFileExtension in _relatedFileExtensions) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", allowedRelatedFileExtension); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "AppConfigFile"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", AppConfigFile); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "AutoUnify"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", AutoUnify.ToString()); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "CopyLocalDependenciesWhenParentReferenceInGac"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", _copyLocalDependenciesWhenParentReferenceInGac); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "FindDependencies"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", _findDependencies); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "TargetProcessorArchitecture"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", TargetProcessorArchitecture); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "StateFile"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", StateFile); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "InstalledAssemblySubsetTables"); + foreach (ITaskItem installedAssemblySubsetTable in InstalledAssemblySubsetTables) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", installedAssemblySubsetTable); + LogAttribute(installedAssemblySubsetTable, ItemMetadataNames.frameworkDirectory); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "IgnoreInstalledAssemblySubsetTable"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", _ignoreDefaultInstalledAssemblySubsetTables); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "TargetFrameworkSubsets"); + foreach (string subset in _targetFrameworkSubsets) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", subset); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "FullTargetFrameworkSubsetNames"); + foreach (string subset in FullTargetFrameworkSubsetNames) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", subset); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "ProfileName"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", ProfileName); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "FullFrameworkFolders"); + foreach (string fullFolder in FullFrameworkFolders) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", fullFolder); + } + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "LatestTargetFrameworkDirectories"); + foreach (string latestFolder in _latestTargetFrameworkDirectories) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", latestFolder); + } + + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.LogTaskPropertyFormat", "ProfileTablesLocation"); + foreach (ITaskItem profileTable in FullFrameworkAssemblyTables) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", profileTable); + LogAttribute(profileTable, ItemMetadataNames.frameworkDirectory); + } + } + } + + /// + /// Log a specific item metadata. + /// + /// + /// + private void LogAttribute(ITaskItem item, string metadataName) + { + string metadataValue = item.GetMetadata(metadataName); + if (metadataValue != null && metadataValue.Length > 0) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.LogAttributeFormat", metadataName, metadataValue)); + } + } + + /// + /// Describes whether this reference is primary or not + /// + /// The reference. + /// The fusion name for this reference. + /// The importance of the message. + private void LogPrimaryOrDependency(Reference reference, string fusionName, MessageImportance importance) + { + if (reference.IsPrimary) + { + if (reference.IsUnified) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.UnifiedPrimaryReference", fusionName); + } + else + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.PrimaryReference", fusionName); + } + } + else + { + if (reference.IsUnified) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.UnifiedDependency", fusionName); + } + else + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.Dependency", fusionName); + } + } + + foreach (UnificationVersion unificationVersion in reference.GetPreUnificationVersions()) + { + switch (unificationVersion.reason) + { + case UnificationReason.BecauseOfBindingRedirect: + if (AutoUnify) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.UnificationByAutoUnify", unificationVersion.version, unificationVersion.referenceFullPath)); + } + else + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.UnificationByAppConfig", unificationVersion.version, _appConfigFile, unificationVersion.referenceFullPath)); + } + break; + + case UnificationReason.FrameworkRetarget: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.UnificationByFrameworkRetarget", unificationVersion.version, unificationVersion.referenceFullPath)); + break; + + case UnificationReason.DidntUnify: + break; + + default: + Debug.Assert(false, "Should have handled this case."); + break; + } + } + + foreach (AssemblyRemapping remapping in reference.RemappedAssemblyNames()) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.RemappedReference", remapping.From.FullName, remapping.To.FullName)); + } + } + + /// + /// Log any errors for a reference. + /// + /// The reference. + /// The importance of the message. + private void LogReferenceErrors(Reference reference, MessageImportance importance) + { + ICollection itemErrors = reference.GetErrors(); + foreach (Exception itemError in itemErrors) + { + string message = String.Empty; + string helpKeyword = null; + bool dependencyProblem = false; + + if (itemError is ReferenceResolutionException) + { + message = Log.FormatResourceString("ResolveAssemblyReference.FailedToResolveReference", itemError.Message); + helpKeyword = "MSBuild.ResolveAssemblyReference.FailedToResolveReference"; + dependencyProblem = false; + } + else if (itemError is DependencyResolutionException) + { + message = Log.FormatResourceString("ResolveAssemblyReference.FailedToFindDependentFiles", itemError.Message); + helpKeyword = "MSBuild.ResolveAssemblyReference.FailedToFindDependentFiles"; + dependencyProblem = true; + } + else if (itemError is BadImageReferenceException) + { + message = Log.FormatResourceString("ResolveAssemblyReference.FailedWithException", itemError.Message); + helpKeyword = "MSBuild.ResolveAssemblyReference.FailedWithException"; + dependencyProblem = false; + } + else + { + Debug.Assert(false, "Unexpected exception type."); + } + + string messageOnly; + string warningCode = Log.ExtractMessageCode(message, out messageOnly); + + // Treat as warning if this is primary and the problem wasn't with a dependency, otherwise, make it a comment. + if (reference.IsPrimary && !dependencyProblem) + { + // Treat it as a warning + Log.LogWarning(null, warningCode, helpKeyword, null, 0, 0, 0, 0, messageOnly); + } + else + { + // Just show the the message as a comment. + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", messageOnly); + } + } + } + + /// + /// Show the full name of a reference. + /// + /// The reference. + /// The importance of the message. + private void LogFullName(Reference reference, MessageImportance importance) + { + ErrorUtilities.VerifyThrowArgumentNull(reference, "reference"); + + if (reference.IsResolved) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.Resolved", reference.FullPath)); + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ResolvedFrom", reference.ResolvedSearchPath)); + } + } + + /// + /// If there is a list of assemblyFiles that was considered but then rejected, + /// show information about them. + /// + /// The reference. + /// The importance of the message. + private void LogAssembliesConsideredAndRejected(Reference reference, MessageImportance importance) + { + if (reference.AssembliesConsideredAndRejected != null) + { + string lastSearchPath = null; + + foreach (ResolutionSearchLocation location in reference.AssembliesConsideredAndRejected) + { + // We need to keep track if whether or not we need to log the assemblyfoldersex folder structure at the end of RAR. + // We only need to do so if we logged a message indicating we looked in the assemblyfoldersex location + bool containsAssemblyFoldersExSentinel = String.Compare(location.SearchPath, 0, AssemblyResolutionConstants.assemblyFoldersExSentinel, 0, AssemblyResolutionConstants.assemblyFoldersExSentinel.Length, StringComparison.OrdinalIgnoreCase) == 0; + bool logAssemblyFoldersMinimal = containsAssemblyFoldersExSentinel && !_logVerboseSearchResults; + if (logAssemblyFoldersMinimal) + { + // We not only need to track if we logged a message but also what importance. We want the logging of the assemblyfoldersex folder structure to match the same importance. + MessageImportance messageImportance = MessageImportance.Low; + if (!_showAssemblyFoldersExLocations.TryGetValue(location.SearchPath, out messageImportance)) + { + _showAssemblyFoldersExLocations.Add(location.SearchPath, importance); + } + + if ((messageImportance == MessageImportance.Low && (importance == MessageImportance.Normal || importance == MessageImportance.High)) || + (messageImportance == MessageImportance.Normal && importance == MessageImportance.High) + ) + { + _showAssemblyFoldersExLocations[location.SearchPath] = importance; + } + } + + + // If this is a new search location, then show the message. + if (lastSearchPath != location.SearchPath) + { + lastSearchPath = location.SearchPath; + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.SearchPath", lastSearchPath)); + if (logAssemblyFoldersMinimal) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.SearchedAssemblyFoldersEx")); + } + } + + // Show a message based on the reason. + switch (location.Reason) + { + case NoMatchReason.FileNotFound: + { + if (!logAssemblyFoldersMinimal) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ConsideredAndRejectedBecauseNoFile", location.FileNameAttempted)); + } + break; + } + case NoMatchReason.FusionNamesDidNotMatch: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ConsideredAndRejectedBecauseFusionNamesDidntMatch", location.FileNameAttempted, location.AssemblyName.FullName)); + break; + + case NoMatchReason.TargetHadNoFusionName: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ConsideredAndRejectedBecauseTargetDidntHaveFusionName", location.FileNameAttempted)); + break; + + case NoMatchReason.NotInGac: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ConsideredAndRejectedBecauseNotInGac", location.FileNameAttempted)); + break; + + case NoMatchReason.NotAFileNameOnDisk: + { + if (!logAssemblyFoldersMinimal) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ConsideredAndRejectedBecauseNotAFileNameOnDisk", location.FileNameAttempted)); + } + + break; + } + case NoMatchReason.ProcessorArchitectureDoesNotMatch: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.EightSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.TargetedProcessorArchitectureDoesNotMatch", location.FileNameAttempted, location.AssemblyName.AssemblyName.ProcessorArchitecture.ToString(), _targetProcessorArchitecture)); + break; + default: + Debug.Assert(false, "Should have handled this case."); + break; + } + } + } + } + + /// + /// Show the files that made this dependency necessary. + /// + /// The reference. + /// The importance of the message. + private void LogDependees(Reference reference, MessageImportance importance) + { + if (!reference.IsPrimary) + { + ICollection dependees = reference.GetSourceItems(); + foreach (ITaskItem dependee in dependees) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.RequiredBy", dependee.ItemSpec)); + } + } + } + + /// + /// Log related files. + /// + /// The reference. + /// The importance of the message. + private void LogRelatedFiles(Reference reference, MessageImportance importance) + { + if (reference.IsResolved) + { + if (reference.FullPath.Length > 0) + { + foreach (string relatedFileExtension in reference.GetRelatedFileExtensions()) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.FoundRelatedFile", reference.FullPathWithoutExtension + relatedFileExtension)); + } + } + } + } + + /// + /// Log the satellite files. + /// + /// The reference. + /// The importance of the message. + private void LogSatellites(Reference reference, MessageImportance importance) + { + foreach (string satelliteFile in reference.GetSatelliteFiles()) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.FoundSatelliteFile", satelliteFile)); + } + } + + /// + /// Log the satellite files. + /// + /// The reference. + /// The importance of the message. + private void LogScatterFiles(Reference reference, MessageImportance importance) + { + foreach (string scatterFile in reference.GetScatterFiles()) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.FoundScatterFile", scatterFile)); + } + } + + /// + /// Log a message about the CopyLocal state of the reference. + /// + /// The reference. + /// The importance of the message. + private void LogCopyLocalState(Reference reference, MessageImportance importance) + { + if (!reference.IsUnresolvable && !reference.IsBadImage) + { + switch (reference.CopyLocal) + { + case CopyLocalState.YesBecauseOfHeuristic: + case CopyLocalState.YesBecauseReferenceItemHadMetadata: + break; + + case CopyLocalState.NoBecausePrerequisite: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NotCopyLocalBecausePrerequisite")); + break; + + case CopyLocalState.NoBecauseReferenceItemHadMetadata: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NotCopyLocalBecauseIncomingItemAttributeOverrode")); + break; + + case CopyLocalState.NoBecauseFrameworkFile: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NotCopyLocalBecauseFrameworksFiles")); + break; + + case CopyLocalState.NoBecauseReferenceFoundInGAC: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NotCopyLocalBecauseReferenceFoundInGAC")); + break; + + case CopyLocalState.NoBecauseConflictVictim: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NotCopyLocalBecauseConflictVictim")); + break; + + case CopyLocalState.NoBecauseEmbedded: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NotCopyLocalBecauseEmbedded")); + break; + + case CopyLocalState.NoBecauseParentReferencesFoundInGAC: + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.NoBecauseParentReferencesFoundInGac")); + break; + + default: + Debug.Assert(false, "Should have handled this case."); + break; + } + } + } + + + /// + /// Log a message about the imageruntime information. + /// + private void LogImageRuntime(Reference reference, MessageImportance importance) + { + if (!reference.IsUnresolvable && !reference.IsBadImage) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.ImageRuntimeVersion", reference.ImageRuntime)); + + if (reference.IsWinMDFile) + { + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.IsAWinMdFile")); + } + } + } + + /// + /// Log a conflict. + /// + /// The reference. + /// The fusion name of the reference. + private void LogConflict(Reference reference, string fusionName) + { + // Set an importance level to be used for secondary messages. + MessageImportance importance = ChooseReferenceLoggingImportance(reference); + + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.ConflictFound", reference.ConflictVictorName, fusionName); + switch (reference.ConflictLossExplanation) + { + case ConflictLossReason.HadLowerVersion: + { + Debug.Assert(!reference.IsPrimary, "A primary reference should never lose a conflict because of version. This is an insoluble conflict instead."); + string message = Log.FormatResourceString("ResolveAssemblyReference.ConflictHigherVersionChosen", reference.ConflictVictorName); + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", message); + break; + } + + case ConflictLossReason.WasNotPrimary: + { + string message = Log.FormatResourceString("ResolveAssemblyReference.ConflictPrimaryChosen", reference.ConflictVictorName, fusionName); + Log.LogMessageFromResources(importance, "ResolveAssemblyReference.FourSpaceIndent", message); + break; + } + + case ConflictLossReason.InsolubleConflict: + // For primary references, there's no way an app.config binding redirect could help + // so log a warning. + if (reference.IsPrimary) + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.ConflictUnsolvable", reference.ConflictVictorName, fusionName); + } + else + { + // For dependencies, adding an app.config entry could help. Log a comment, there will be + // a summary warning later on. + string message; + string code = Log.ExtractMessageCode(Log.FormatResourceString("ResolveAssemblyReference.ConflictUnsolvable", reference.ConflictVictorName, fusionName), out message); + Log.LogMessage(MessageImportance.High, message); + } + break; + // Can happen if one of the references has a dependency with the same simplename, and version but no publickeytoken and the other does. + case ConflictLossReason.FusionEquivalentWithSameVersion: + break; + default: + Debug.Assert(false, "Should have handled this case."); + break; + } + } + #endregion + #region StateFile + /// + /// Reads the state file (if present) into the cache. + /// + private void ReadStateFile() + { + _cache = (SystemState)StateFileBase.DeserializeCache(_stateFile, Log, typeof(SystemState)); + + // Construct the cache if necessary. + if (_cache == null) + { + _cache = new SystemState(); + } + } + + /// + /// Write out the state file if a state name was supplied and the cache is dirty. + /// + private void WriteStateFile() + { + if (_stateFile != null && _stateFile.Length > 0 && _cache.IsDirty) + { + _cache.SerializeCache(_stateFile, Log); + } + } + #endregion + #region App.config + /// + /// Read the app.config and get any assembly remappings from it. + /// + /// + private DependentAssembly[] GetAssemblyRemappingsFromAppConfig() + { + if (_appConfigFile != null) + { + AppConfig appConfig = new AppConfig(); + appConfig.Load(_appConfigFile); + + return appConfig.Runtime.DependentAssemblies; + } + + return null; + } + + #endregion + #region ITask Members + + /// + /// Execute the task. + /// + /// Delegate used for checking for the existence of a file. + /// Delegate used for checking for the existence of a directory. + /// Delegate used for finding directories. + /// Delegate used for finding fusion names of assemblyFiles. + /// Delegate used for finding dependencies of a file. + /// Used to get registry subkey names. + /// Used to get registry default values. + /// Delegate used to get the last write time. + /// True if there was success. + internal bool Execute + ( + FileExists fileExists, + DirectoryExists directoryExists, + GetDirectories getDirectories, + GetAssemblyName getAssemblyName, + GetAssemblyMetadata getAssemblyMetadata, + GetRegistrySubKeyNames getRegistrySubKeyNames, + GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, + GetLastWriteTime getLastWriteTime, + GetAssemblyRuntimeVersion getRuntimeVersion, + OpenBaseKey openBaseKey, + CheckIfAssemblyInGac checkIfAssemblyIsInGac, + IsWinMDFile isWinMDFile, + ReadMachineTypeFromPEHeader readMachineTypeFromPEHeader + ) + { + bool success = true; +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildResolveAssemblyReferenceBegin, CodeMarkerEvent.perfMSBuildResolveAssemblyReferenceEnd)) +#endif + { + try + { + FrameworkNameVersioning frameworkMoniker = null; + if (!String.IsNullOrEmpty(_targetedFrameworkMoniker)) + { + try + { + frameworkMoniker = new FrameworkNameVersioning(_targetedFrameworkMoniker); + } + catch (ArgumentException) + { + // The exception doesn't contain the bad value, so log it ourselves + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.InvalidParameter", "TargetFrameworkMoniker", _targetedFrameworkMoniker, String.Empty); + return false; + } + } + + Version targetedRuntimeVersion = SetTargetedRuntimeVersion(_targetedRuntimeVersionRawValue); + + // Log task inputs. + LogInputs(); + + if (!VerifyInputConditions()) + { + return false; + } + + _logVerboseSearchResults = Environment.GetEnvironmentVariable("MSBUILDLOGVERBOSERARSEARCHRESULTS") != null; + + // Loop through all the target framework directories that were passed in, + // and ensure that they all have a trailing slash. This is necessary + // for the string comparisons we will do later on. + if (_targetFrameworkDirectories != null) + { + for (int i = 0; i < _targetFrameworkDirectories.Length; i++) + { + _targetFrameworkDirectories[i] = FileUtilities.EnsureTrailingSlash(_targetFrameworkDirectories[i]); + } + } + + + // Validate the contents of the InstalledAssemblyTables parameter. + AssemblyTableInfo[] installedAssemblyTableInfo = GetInstalledAssemblyTableInfo(_ignoreDefaultInstalledAssemblyTables, _installedAssemblyTables, new GetListPath(RedistList.GetRedistListPathsFromDisk), TargetFrameworkDirectories); + AssemblyTableInfo[] whiteListSubsetTableInfo = null; + + InstalledAssemblies installedAssemblies = null; + RedistList redistList = null; + + if (installedAssemblyTableInfo != null && installedAssemblyTableInfo.Length > 0) + { + redistList = RedistList.GetRedistList(installedAssemblyTableInfo); + } + + Hashtable blackList = null; + + // The name of the subset if it is generated or the name of the profile. This will be used for error messages and logging. + string subsetOrProfileName = null; + + // Are we targeting a profile + bool targetingProfile = !String.IsNullOrEmpty(ProfileName) && ((FullFrameworkFolders.Length > 0) || (FullFrameworkAssemblyTables.Length > 0)); + bool targetingSubset = false; + List whiteListErrors = new List(); + List whiteListErrorFilesNames = new List(); + + // Check for partial success in GetRedistList and log any tolerated exceptions. + if (redistList != null && redistList.Count > 0 || targetingProfile || ShouldUseSubsetBlackList()) + { + // If we are not targeting a dev 10 profile and we have the required components to generate a orcas style subset, do so + if (!targetingProfile && ShouldUseSubsetBlackList()) + { + // Based in the target framework subset names find the paths to the files + SubsetListFinder whiteList = new SubsetListFinder(_targetFrameworkSubsets); + whiteListSubsetTableInfo = GetInstalledAssemblyTableInfo(IgnoreDefaultInstalledAssemblySubsetTables, InstalledAssemblySubsetTables, new GetListPath(whiteList.GetSubsetListPathsFromDisk), TargetFrameworkDirectories); + if (whiteListSubsetTableInfo.Length > 0 && (redistList != null && redistList.Count > 0)) + { + blackList = redistList.GenerateBlackList(whiteListSubsetTableInfo, whiteListErrors, whiteListErrorFilesNames); + } + else + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.NoSubsetsFound"); + } + + // Could get into this situation if the redist list files were full of junk and no assemblies were read in. + if (blackList == null) + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.NoRedistAssembliesToGenerateExclusionList"); + } + + subsetOrProfileName = GenerateSubSetName(_targetFrameworkSubsets, _installedAssemblySubsetTables); + targetingSubset = true; + } + else + { + // We are targeting a profile + if (targetingProfile) + { + // When targeting a profile we want the redist list to be the full framework redist list, since this is what should be used + // when unifying assemblies ect. + AssemblyTableInfo[] fullRedistAssemblyTableInfo = null; + RedistList fullFrameworkRedistList = null; + + HandleProfile(installedAssemblyTableInfo /*This is the table info related to the profile*/, out fullRedistAssemblyTableInfo, out blackList, out fullFrameworkRedistList); + + // Make sure the redist list and the installedAsemblyTableInfo structures point to the full framework, we replace the installedAssemblyTableInfo + // which contained the information about the profile redist files with the one from the full framework because when doing anything with the RAR cache + // we want to use the full frameworks redist list. Essentailly after generating the exclusion list the job of the profile redist list is done. + redistList = fullFrameworkRedistList; + + // Save the profile redist list file locations as the whiteList + whiteListSubsetTableInfo = installedAssemblyTableInfo; + + // Set the installed assembly table to the full redist list values + installedAssemblyTableInfo = fullRedistAssemblyTableInfo; + subsetOrProfileName = _profileName; + } + } + + if (redistList != null && redistList.Count > 0) + { + installedAssemblies = new InstalledAssemblies(redistList); + } + } + + // Print out any errors reading the redist list. + if (redistList != null) + { + // Some files may have been skipped. Log warnings for these. + for (int i = 0; i < redistList.Errors.Length; ++i) + { + Exception e = redistList.Errors[i]; + string filename = redistList.ErrorFileNames[i]; + + // Give the user a warning about the bad file (or files). + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.InvalidInstalledAssemblyTablesFile", filename, RedistList.RedistListFolder, e.Message); + } + + // Some files may have been skipped. Log warnings for these. + for (int i = 0; i < whiteListErrors.Count; ++i) + { + Exception e = whiteListErrors[i] as Exception; + string filename = whiteListErrorFilesNames[i] as string; + + // Give the user a warning about the bad file (or files). + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.InvalidInstalledAssemblySubsetTablesFile", filename, SubsetListFinder.SubsetListFolder, e.Message); + } + } + + // Load any prior saved state. + ReadStateFile(); + _cache.SetGetLastWriteTime(getLastWriteTime); + _cache.SetInstalledAssemblyInformation(installedAssemblyTableInfo); + + // Cache delegates. + getAssemblyName = _cache.CacheDelegate(getAssemblyName); + getAssemblyMetadata = _cache.CacheDelegate(getAssemblyMetadata); + fileExists = _cache.CacheDelegate(fileExists); + getDirectories = _cache.CacheDelegate(getDirectories); + getRuntimeVersion = _cache.CacheDelegate(getRuntimeVersion); + + _projectTargetFramework = FrameworkVersionFromString(_projectTargetFrameworkAsString); + + // Filter out all Assemblies that have SubType!='', or higher framework + FilterBySubtypeAndTargetFramework(); + + // Compute the set of bindingRedirect remappings. + DependentAssembly[] appConfigRemappedAssemblies = null; + if (FindDependencies) + { + try + { + appConfigRemappedAssemblies = GetAssemblyRemappingsFromAppConfig(); + } + catch (AppConfigException e) + { + Log.LogErrorWithCodeFromResources(null, e.FileName, e.Line, e.Column, 0, 0, "ResolveAssemblyReference.InvalidAppConfig", AppConfigFile, e.Message); + return false; + } + } + + SystemProcessorArchitecture processorArchitecture = TargetProcessorArchitectureToEnumeration(_targetProcessorArchitecture); + + if (checkIfAssemblyIsInGac == null) + { + checkIfAssemblyIsInGac = new CheckIfAssemblyInGac(CheckForAssemblyInGac); + } + + // Start the table of dependencies with all of the primary references. + ReferenceTable dependencyTable = new ReferenceTable + ( + BuildEngine, + _findDependencies, + _findSatellites, + _findSerializationAssemblies, + _findRelatedFiles, + _searchPaths, + _allowedAssemblyExtensions, + _relatedFileExtensions, + _candidateAssemblyFiles, + _resolvedSDKReferences, + _targetFrameworkDirectories, + installedAssemblies, + processorArchitecture, + fileExists, + directoryExists, + getDirectories, + getAssemblyName, + getAssemblyMetadata, + getRegistrySubKeyNames, + getRegistrySubKeyDefaultValue, + openBaseKey, + getRuntimeVersion, + targetedRuntimeVersion, + _projectTargetFramework, + frameworkMoniker, + Log, + _latestTargetFrameworkDirectories, + _copyLocalDependenciesWhenParentReferenceInGac, + checkIfAssemblyIsInGac, + isWinMDFile, + _ignoreVersionForFrameworkReferences, + readMachineTypeFromPEHeader, + _warnOrErrorOnTargetArchitectureMismatch, + _ignoreTargetFrameworkAttributeVersionMismatch, + _unresolveFrameworkAssembliesFromHigherFrameworks + ); + + // If AutoUnify, then compute the set of assembly remappings. + ArrayList generalResolutionExceptions = new ArrayList(); + + subsetOrProfileName = targetingSubset && String.IsNullOrEmpty(_targetedFrameworkMoniker) ? subsetOrProfileName : _targetedFrameworkMoniker; + bool excludedReferencesExist = false; + + DependentAssembly[] autoUnifiedRemappedAssemblies = null; + AssemblyNameReference[] autoUnifiedRemappedAssemblyReferences = null; + if (AutoUnify && FindDependencies) + { + // Compute all dependencies. + dependencyTable.ComputeClosure + ( + // Use any app.config specified binding redirects so that later when we output suggested redirects + // for the GenerateBindingRedirects target, we don't suggest ones that the user already wrote + appConfigRemappedAssemblies, + _assemblyFiles, + _assemblyNames, + generalResolutionExceptions + ); + + try + { + excludedReferencesExist = false; + if (redistList != null && redistList.Count > 0) + { + excludedReferencesExist = dependencyTable.MarkReferencesForExclusion(blackList); + } + } + catch (InvalidOperationException e) + { + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.ProblemDeterminingFrameworkMembership", e.Message); + return false; + } + + if (excludedReferencesExist) + { + dependencyTable.RemoveReferencesMarkedForExclusion(true /* Remove the reference and do not warn*/, subsetOrProfileName); + } + + + // Based on the closure, get a table of ideal remappings needed to + // produce zero conflicts. + dependencyTable.ResolveConflicts + ( + out autoUnifiedRemappedAssemblies, + out autoUnifiedRemappedAssemblyReferences + ); + } + + DependentAssembly[] allRemappedAssemblies = CombineRemappedAssemblies(appConfigRemappedAssemblies, autoUnifiedRemappedAssemblies); + + // Compute all dependencies. + dependencyTable.ComputeClosure(allRemappedAssemblies, _assemblyFiles, _assemblyNames, generalResolutionExceptions); + + try + { + excludedReferencesExist = false; + if (redistList != null && redistList.Count > 0) + { + excludedReferencesExist = dependencyTable.MarkReferencesForExclusion(blackList); + } + } + catch (InvalidOperationException e) + { + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.ProblemDeterminingFrameworkMembership", e.Message); + return false; + } + + if (excludedReferencesExist) + { + dependencyTable.RemoveReferencesMarkedForExclusion(false /* Remove the reference and warn*/, subsetOrProfileName); + } + + // Resolve any conflicts. + DependentAssembly[] idealAssemblyRemappings = null; + AssemblyNameReference[] idealAssemblyRemappingsIdentities = null; + + dependencyTable.ResolveConflicts + ( + out idealAssemblyRemappings, + out idealAssemblyRemappingsIdentities + ); + + // Build the output tables. + dependencyTable.GetReferenceItems + ( + out _resolvedFiles, + out _resolvedDependencyFiles, + out _relatedFiles, + out _satelliteFiles, + out _serializationAssemblyFiles, + out _scatterFiles, + out _copyLocalFiles + ); + + // If we're not finding dependencies, then don't suggest redirects (they're only about dependencies). + if (FindDependencies) + { + // Build the table of suggested redirects. If we're auto-unifying, we want to output all the + // assemblies that we auto-unified so that GenerateBindingRedirects can consume them, + // not just the required ones for build to succeed + DependentAssembly[] remappings = AutoUnify ? autoUnifiedRemappedAssemblies : idealAssemblyRemappings; + AssemblyNameReference[] remappedReferences = AutoUnify ? autoUnifiedRemappedAssemblyReferences : idealAssemblyRemappingsIdentities; + PopulateSuggestedRedirects(remappings, remappedReferences); + } + + bool useSystemRuntime = false; + foreach (var reference in dependencyTable.References.Keys) + { + if (string.Equals(SystemRuntimeAssemblyName, reference.Name, StringComparison.OrdinalIgnoreCase)) + { + useSystemRuntime = true; + break; + } + } + + if (!useSystemRuntime && !FindDependencies && this.ForceSystemRuntimeDependencyCalculation) + { + // when we are not producing the dependency graph look for direct dependencies of primary references. + foreach (var resolvedReference in dependencyTable.References.Values) + { + var rawDependencies = GetDependencies(resolvedReference, fileExists, getAssemblyMetadata); + if (rawDependencies != null) + { + foreach (var dependentReference in rawDependencies) + { + if (string.Equals(SystemRuntimeAssemblyName, dependentReference.Name, StringComparison.OrdinalIgnoreCase)) + { + useSystemRuntime = true; + break; + } + } + } + + if (useSystemRuntime) + { + break; + } + } + } + + this.DependsOnSystemRuntime = useSystemRuntime.ToString(); + + WriteStateFile(); + + // Save the new state out and put into the file exists if it is actually on disk. + if (_stateFile != null && fileExists(_stateFile)) + { + _filesWritten.Add(new TaskItem(_stateFile)); + } + + // Log the results. + success = LogResults(dependencyTable, idealAssemblyRemappings, idealAssemblyRemappingsIdentities, generalResolutionExceptions); + + DumpTargetProfileLists(installedAssemblyTableInfo, whiteListSubsetTableInfo, dependencyTable); + + if (processorArchitecture != SystemProcessorArchitecture.None && _warnOrErrorOnTargetArchitectureMismatch != WarnOrErrorOnTargetArchitectureMismatchBehavior.None) + { + foreach (ITaskItem item in _resolvedFiles) + { + AssemblyNameExtension assemblyName = null; + + if (fileExists(item.ItemSpec) && !Reference.IsFrameworkFile(item.ItemSpec, _targetFrameworkDirectories)) + { + try + { + assemblyName = getAssemblyName(item.ItemSpec); + } + catch (System.IO.FileLoadException) + { + // Its pretty hard to get here, you need an assembly that contains a valid reference + // to a dependent assembly that, in turn, throws a FileLoadException during GetAssemblyName. + // Still it happened once, with an older version of the CLR. + + // ...falling through and relying on the targetAssemblyName==null behavior below... + } + catch (System.IO.FileNotFoundException) + { + // Its pretty hard to get here, also since we do a file existence check right before calling this method so it can only happen if the file got deleted between that check and this call. + } + catch (UnauthorizedAccessException) + { + } + catch (BadImageFormatException) + { + } + } + + if (assemblyName != null) + { + SystemProcessorArchitecture assemblyArch = assemblyName.ProcessorArchitecture; + + // If the assembly is MSIL or none it can work anywhere so there does not need to be any warning ect. + if (assemblyArch == SystemProcessorArchitecture.MSIL || assemblyArch == SystemProcessorArchitecture.None) + { + continue; + } + + if (processorArchitecture != assemblyArch) + { + if (_warnOrErrorOnTargetArchitectureMismatch == WarnOrErrorOnTargetArchitectureMismatchBehavior.Error) + { + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", ProcessorArchitectureToString(processorArchitecture), item.GetMetadata("OriginalItemSpec"), ProcessorArchitectureToString(assemblyArch)); + } + else + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", ProcessorArchitectureToString(processorArchitecture), item.GetMetadata("OriginalItemSpec"), ProcessorArchitectureToString(assemblyArch)); + } + } + } + } + } + return success && !Log.HasLoggedErrors; + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("General.InvalidArgument", e.Message); + } + + // InvalidParameterValueException is thrown inside RAR when we find a specific parameter + // has an invalid value. It's then caught up here so that we can abort the task. + catch (InvalidParameterValueException e) + { + Log.LogErrorWithCodeFromResources(null, "", 0, 0, 0, 0, + "ResolveAssemblyReference.InvalidParameter", e.ParamName, e.ActualValue, e.Message); + } + } + + return success && !Log.HasLoggedErrors; + } + + /// + /// Returns the raw list of direct dependent assemblies from assembly's metadata. + /// + /// the delegate to check for the existence of a file. + /// reference we are interested + /// the delegate to access assembly metadata + /// list of dependencies + private AssemblyNameExtension[] GetDependencies(Reference resolvedReference, FileExists fileExists, GetAssemblyMetadata getAssemblyMetadata) + { + AssemblyNameExtension[] result = null; + if (resolvedReference != null && resolvedReference.IsPrimary && !resolvedReference.IsBadImage) + { + System.Runtime.Versioning.FrameworkName frameworkName = null; + string[] scatterFiles = null; + try + { + // in case of P2P that have not build the reference can be resolved but file does not exist on disk. + if (fileExists(resolvedReference.FullPath)) + { + getAssemblyMetadata(resolvedReference.FullPath, out result, out scatterFiles, out frameworkName); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + } + } + + return result; + } + + /// + /// Combines two DependentAssembly arrays into one. + /// + private static DependentAssembly[] CombineRemappedAssemblies(DependentAssembly[] first, DependentAssembly[] second) + { + if (first == null) + return second; + + if (second == null) + return first; + + DependentAssembly[] combined = new DependentAssembly[first.Length + second.Length]; + first.CopyTo(combined, 0); + second.CopyTo(combined, first.Length); + + return combined; + } + + + /// + /// If a targeted runtime is passed in use that, if none is passed in then we need to use v2.0.50727 + /// since the common way this would be empty is if we were using RAR as an override task. + /// + /// The targered runtime + internal static Version SetTargetedRuntimeVersion(string targetedRuntimeVersionRawValue) + { + Version versionToReturn = null; + if (targetedRuntimeVersionRawValue != null) + { + versionToReturn = VersionUtilities.ConvertToVersion(targetedRuntimeVersionRawValue); + } + + // Either the version passed in did not parse or none was passed in, lets default to 2.0 so that we can be used as an override task for tv 3.5 + if (versionToReturn == null) + { + versionToReturn = new Version(2, 0, 50727); + } + + return versionToReturn; + } + + /// + /// For a given profile generate the exclusion list and return the list of redist list files read in so they can be logged at the end of the task execution. + /// + /// Installed assembly info of the profile redist lists + /// Installed assemblyInfo for the full framework redist lists + /// Generated exclusion list + private void HandleProfile(AssemblyTableInfo[] installedAssemblyTableInfo, out AssemblyTableInfo[] fullRedistAssemblyTableInfo, out Hashtable blackList, out RedistList fullFrameworkRedistList) + { + // Redist list which will contain the full framework redist list. + fullFrameworkRedistList = null; + blackList = null; + fullRedistAssemblyTableInfo = null; + + // Make sure the framework directory is on the FullFrameworkTablesLocation if it is being used. + foreach (ITaskItem item in FullFrameworkAssemblyTables) + { + // Cannot be missing the FrameworkDirectory if we are using this property + if (String.IsNullOrEmpty(item.GetMetadata("FrameworkDirectory"))) + { + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.FrameworkDirectoryOnProfiles", item.ItemSpec); + return; + } + } + + fullRedistAssemblyTableInfo = GetInstalledAssemblyTableInfo(false, FullFrameworkAssemblyTables, new GetListPath(RedistList.GetRedistListPathsFromDisk), FullFrameworkFolders); + if (fullRedistAssemblyTableInfo.Length > 0) + { + // Get the redist list which represents the Full framework, we need this so that we can generate the exclusion list + fullFrameworkRedistList = RedistList.GetRedistList(fullRedistAssemblyTableInfo); + if (fullFrameworkRedistList != null) + { + // Generate the black list by determining what assemblies are in the full framework but not in the profile. + // The installedAssemblyTableInfo is the list of xml files for the Client Profile redist, these are the whitelist xml files. + Log.LogMessageFromResources("ResolveAssemblyReference.ProfileExclusionListWillBeGenerated"); + + // Any errors reading the profile redist list will already be logged, we do not need to re-log the errors here. + List whiteListErrors = new List(); + List whiteListErrorFilesNames = new List(); + blackList = fullFrameworkRedistList.GenerateBlackList(installedAssemblyTableInfo, whiteListErrors, whiteListErrorFilesNames); + } + + // Could get into this situation if the redist list files were full of junk and no assemblies were read in. + if (blackList == null) + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.NoRedistAssembliesToGenerateExclusionList"); + } + } + else + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.NoProfilesFound"); + } + + if (fullFrameworkRedistList != null) + { + // Any errors logged for the client profile redist list will have been logged after this method returns. + // Some files may have been skipped. Log warnings for these. + for (int i = 0; i < fullFrameworkRedistList.Errors.Length; ++i) + { + Exception e = fullFrameworkRedistList.Errors[i]; + string filename = fullFrameworkRedistList.ErrorFileNames[i]; + + // Give the user a warning about the bad file (or files). + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.InvalidProfileRedistLocation", filename, RedistList.RedistListFolder, e.Message); + } + } + } + + /// + /// Given the names of the targetFrameworkSubset lists passed in generate a single name which can be used for logging. + /// + internal static string GenerateSubSetName(string[] frameworkSubSetNames, ITaskItem[] installedSubSetNames) + { + List subsetNames = new List(); + if (frameworkSubSetNames != null) + { + foreach (string subset in frameworkSubSetNames) + { + if (!String.IsNullOrEmpty(subset)) + { + subsetNames.Add(subset); + } + } + } + + if (installedSubSetNames != null) + { + foreach (ITaskItem subsetItems in installedSubSetNames) + { + string fileName = subsetItems.ItemSpec; + if (!String.IsNullOrEmpty(fileName)) + { + string fileNameNoExtension = Path.GetFileNameWithoutExtension(fileName); + if (!String.IsNullOrEmpty(fileNameNoExtension)) + { + subsetNames.Add(fileNameNoExtension); + } + } + } + } + + return String.Join(", ", subsetNames.ToArray()); + } + + /// + /// Make sure certain combinations of properties are validated before continuing with the execution of rar. + /// + /// + private bool VerifyInputConditions() + { + bool targetFrameworkSubsetIsSet = TargetFrameworkSubsets.Length != 0 || InstalledAssemblySubsetTables.Length != 0; + + // Make sure the inputs for profiles are correct + bool profileNameIsSet = !String.IsNullOrEmpty(ProfileName); + bool fullFrameworkFoldersIsSet = FullFrameworkFolders.Length > 0; + bool fullFrameworkTableLocationsIsSet = FullFrameworkAssemblyTables.Length > 0; + bool profileIsSet = profileNameIsSet && (fullFrameworkFoldersIsSet || fullFrameworkTableLocationsIsSet); + + // Cannot target a subset and a profile at the same time + if (targetFrameworkSubsetIsSet && profileIsSet) + { + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.CannotSetProfileAndSubSet"); + return false; + } + + // A profile name and either a FullFrameworkFolders or ProfileTableLocation must be set is a profile is being used + if (profileNameIsSet && (!fullFrameworkFoldersIsSet && !fullFrameworkTableLocationsIsSet)) + { + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.MustSetProfileNameAndFolderLocations"); + return false; + } + + return true; + } + + /// + /// Log the target framework subset information. + /// + private void DumpTargetProfileLists(AssemblyTableInfo[] installedAssemblyTableInfo, AssemblyTableInfo[] whiteListSubsetTableInfo, ReferenceTable referenceTable) + { + if (installedAssemblyTableInfo != null) + { + string dumpFrameworkSubsetList = Environment.GetEnvironmentVariable("MSBUILDDUMPFRAMEWORKSUBSETLIST"); + + if (dumpFrameworkSubsetList != null) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.TargetFrameworkSubsetLogHeader"); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.TargetFrameworkRedistLogHeader"); + if (installedAssemblyTableInfo != null) + { + foreach (AssemblyTableInfo redistInfo in installedAssemblyTableInfo) + { + if (redistInfo != null) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.FormattedAssemblyInfo", redistInfo.Path)); + } + } + } + + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.TargetFrameworkWhiteListLogHeader"); + if (whiteListSubsetTableInfo != null) + { + foreach (AssemblyTableInfo whiteListInfo in whiteListSubsetTableInfo) + { + if (whiteListInfo != null) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", Log.FormatResourceString("ResolveAssemblyReference.FormattedAssemblyInfo", whiteListInfo.Path)); + } + } + } + + if (referenceTable.ListOfExcludedAssemblies != null) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.TargetFrameworkExclusionListLogHeader"); + foreach (string assemblyFullName in referenceTable.ListOfExcludedAssemblies) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", assemblyFullName); + } + } + } + } + } + + + /// + /// Determine if a black list should be used or not + /// + /// The black list should only be used if there are TargetFrameworkSubsets to use or TargetFrameworkProfiles. + /// + /// 1) If we find a Full or equivalent marker in the list of subsets passed in we do not want to generate a black list even if installedAssemblySubsets are passed in + /// 2) If we are ignoring the default installed subset tables and we have not passed in any additional subset tables, we do not want to generate a black list + /// 3) If no targetframework subsets were passed in and no additional subset tables were passed in, we do not want to generate a blacklist + /// + /// True if we should generate a black list, false if a blacklist should not be generated + private bool ShouldUseSubsetBlackList() + { + // Check for full subset names in the passed in list of subsets to search for + foreach (string fullSubsetName in _fullTargetFrameworkSubsetNames) + { + foreach (string subsetName in _targetFrameworkSubsets) + { + if (String.Equals(fullSubsetName, subsetName, StringComparison.OrdinalIgnoreCase)) + { + if (!_silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.NoExclusionListBecauseofFullClientName", subsetName); + } + return false; + } + } + } + + // We are going to ignore the default installed subsets and there are no additional installedAssemblySubsets passed in, we should not make the list + if (IgnoreDefaultInstalledAssemblySubsetTables && _installedAssemblySubsetTables.Length == 0) + { + return false; + } + + // No subset names were passed in to search for in the targetframework directories and no installed subset tables were provided, we have nothing to use to + // generate the black list with, so do not continue. + if (_targetFrameworkSubsets.Length == 0 && _installedAssemblySubsetTables.Length == 0) + { + return false; + } + + if (!_silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.UsingExclusionList"); + } + return true; + } + + /// + /// Populates the suggested redirects output parameter. + /// + /// The list of ideal remappings. + /// The list of of references to ideal assembly remappings. + private void PopulateSuggestedRedirects(DependentAssembly[] idealAssemblyRemappings, AssemblyNameReference[] idealAssemblyRemappedReferences) + { + ArrayList holdSuggestedRedirects = new ArrayList(); + if (idealAssemblyRemappings != null) + { + for (int i = 0; i < idealAssemblyRemappings.Length; i++) + { + DependentAssembly idealRemapping = idealAssemblyRemappings[i]; + string itemSpec = idealRemapping.PartialAssemblyName.ToString(); + + Reference reference = idealAssemblyRemappedReferences[i].reference; + AssemblyNameExtension[] conflictVictims = reference.GetConflictVictims(); + + // Skip any remapping that has no conflict victims since a redirect will not help. + if (null == conflictVictims || 0 == conflictVictims.Length) + { + continue; + } + + for (int j = 0; j < idealRemapping.BindingRedirects.Length; j++) + { + ITaskItem suggestedRedirect = new TaskItem(); + suggestedRedirect.ItemSpec = itemSpec; + suggestedRedirect.SetMetadata("MaxVersion", idealRemapping.BindingRedirects[j].NewVersion.ToString()); + holdSuggestedRedirects.Add(suggestedRedirect); + } + } + } + _suggestedRedirects = (ITaskItem[])holdSuggestedRedirects.ToArray(typeof(ITaskItem)); + } + + + /// + /// Process TargetFrameworkDirectories and an array of InstalledAssemblyTables. + /// The goal is this: for each installed assembly table (whether found on disk + /// or given as an input), we wish to determine the target framework directory + /// it is associated with. + /// + /// Array of AssemblyTableInfo objects (Describe the path and framework directory of a redist or subset list xml file) + private AssemblyTableInfo[] GetInstalledAssemblyTableInfo(bool ignoreInstalledAssemblyTables, ITaskItem[] assemblyTables, GetListPath GetAssemblyListPaths, string[] targetFrameworkDirectories) + { + Dictionary tableMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (!ignoreInstalledAssemblyTables) + { + // first, find redist or subset files underneath the TargetFrameworkDirectories + foreach (string targetFrameworkDirectory in targetFrameworkDirectories) + { + string[] listPaths = GetAssemblyListPaths(targetFrameworkDirectory); + foreach (string listPath in listPaths) + { + tableMap[listPath] = new AssemblyTableInfo(listPath, targetFrameworkDirectory); + } + } + } + + // now process those provided as inputs from the project file + foreach (ITaskItem installedAssemblyTable in assemblyTables) + { + string frameworkDirectory = installedAssemblyTable.GetMetadata(ItemMetadataNames.frameworkDirectory); + + // Whidbey behavior was to accept a single TargetFrameworkDirectory, and multiple + // InstalledAssemblyTables, under the assumption that all of the InstalledAssemblyTables + // were related to the single TargetFrameworkDirectory. If inputs look like the Whidbey + // case, let's make sure we behave the same way. + + if (String.IsNullOrEmpty(frameworkDirectory)) + { + if (TargetFrameworkDirectories != null && TargetFrameworkDirectories.Length == 1) + { + // Exactly one TargetFrameworkDirectory, so assume it's related to this + // InstalledAssemblyTable. + + frameworkDirectory = TargetFrameworkDirectories[0]; + } + } + else + { + // The metadata on the item was non-empty, so use it. + frameworkDirectory = FileUtilities.EnsureTrailingSlash(frameworkDirectory); + } + + tableMap[installedAssemblyTable.ItemSpec] = new AssemblyTableInfo(installedAssemblyTable.ItemSpec, frameworkDirectory); + } + + AssemblyTableInfo[] extensions = new AssemblyTableInfo[tableMap.Count]; + tableMap.Values.CopyTo(extensions, 0); + + return extensions; + } + + /// + /// Converts the string target framework value to a number. + /// Accepts both "v" prefixed and no "v" prefixed formats + /// if format is bad will log a message and return 0. + /// + /// Target framework version value + private Version FrameworkVersionFromString(string version) + { + Version parsedVersion = null; + + if (!String.IsNullOrEmpty(version)) + { + parsedVersion = VersionUtilities.ConvertToVersion(version); + + if (parsedVersion == null) + { + Log.LogMessageFromResources(MessageImportance.Normal, "ResolveAssemblyReference.BadTargetFrameworkFormat", version); + } + } + + return parsedVersion; + } + + /// + /// Check if the assembly is available for on project's target framework. + /// - Assuming the russian doll model. It will be available if the projects target framework is higher or equal than the assembly target framework + /// + /// True if the assembly is available for the project's target framework. + private bool IsAvailableForTargetFramework(string assemblyFXVersionAsString) + { + Version assemblyFXVersion = FrameworkVersionFromString(assemblyFXVersionAsString); + return (assemblyFXVersion == null) || (_projectTargetFramework == null) || (_projectTargetFramework >= assemblyFXVersion); + } + + /// + /// Validate and filter the Assemblies that were passed in. + /// - Check for assemblies that look like file names. + /// - Check for assemblies where subtype!=''. These are removed. + /// - Check for assemblies that have target framework higher than the project. These are removed. + /// + private void FilterBySubtypeAndTargetFramework() + { + ArrayList assembliesLeft = new ArrayList(); + foreach (ITaskItem assembly in Assemblies) + { + string subType = assembly.GetMetadata(ItemMetadataNames.subType); + if (subType != null && subType.Length > 0) + { + Log.LogMessageFromResources(MessageImportance.Normal, "ResolveAssemblyReference.IgnoringBecauseNonEmptySubtype", assembly.ItemSpec, subType); + } + else if (!IsAvailableForTargetFramework(assembly.GetMetadata(ItemMetadataNames.targetFramework))) + { + Log.LogWarningWithCodeFromResources("ResolveAssemblyReference.FailedToResolveReferenceBecauseHigherTargetFramework", assembly.ItemSpec, assembly.GetMetadata(ItemMetadataNames.targetFramework)); + } + else + { + assembliesLeft.Add(assembly); + } + } + + // Save the array of assemblies filtered by SubType==''. + _assemblyNames = (ITaskItem[])assembliesLeft.ToArray(typeof(ITaskItem)); + } + + /// + /// Take a processor architecure and get the string representation back. + /// + internal static string ProcessorArchitectureToString(SystemProcessorArchitecture processorArchitecture) + { + if (SystemProcessorArchitecture.Amd64 == processorArchitecture) + { + return Microsoft.Build.Utilities.ProcessorArchitecture.AMD64; + } + else if (SystemProcessorArchitecture.IA64 == processorArchitecture) + { + return Microsoft.Build.Utilities.ProcessorArchitecture.IA64; + } + else if (SystemProcessorArchitecture.MSIL == processorArchitecture) + { + return Microsoft.Build.Utilities.ProcessorArchitecture.MSIL; + } + else if (SystemProcessorArchitecture.X86 == processorArchitecture) + { + return Microsoft.Build.Utilities.ProcessorArchitecture.X86; + } + else if (SystemProcessorArchitecture.Arm == processorArchitecture) + { + return Microsoft.Build.Utilities.ProcessorArchitecture.ARM; + } + return String.Empty; + } + + // Convert the string passed into rar to a processor architecture enum so that we can properly compare it with the AssemblyName objects we find in assemblyFoldersEx + internal static SystemProcessorArchitecture TargetProcessorArchitectureToEnumeration(string targetedProcessorArchitecture) + { + if (targetedProcessorArchitecture != null) + { + if (targetedProcessorArchitecture.Equals(Microsoft.Build.Utilities.ProcessorArchitecture.AMD64, StringComparison.OrdinalIgnoreCase)) + { + return SystemProcessorArchitecture.Amd64; + } + else if (targetedProcessorArchitecture.Equals(Microsoft.Build.Utilities.ProcessorArchitecture.IA64, StringComparison.OrdinalIgnoreCase)) + { + return SystemProcessorArchitecture.IA64; + } + else if (targetedProcessorArchitecture.Equals(Microsoft.Build.Utilities.ProcessorArchitecture.MSIL, StringComparison.OrdinalIgnoreCase)) + { + return SystemProcessorArchitecture.MSIL; + } + else if (targetedProcessorArchitecture.Equals(Microsoft.Build.Utilities.ProcessorArchitecture.X86, StringComparison.OrdinalIgnoreCase)) + { + return SystemProcessorArchitecture.X86; + } + else if (targetedProcessorArchitecture.Equals(Microsoft.Build.Utilities.ProcessorArchitecture.ARM, StringComparison.OrdinalIgnoreCase)) + { + return SystemProcessorArchitecture.Arm; + } + } + + return SystemProcessorArchitecture.MSIL; + } + + /// + /// Checks to see if the assemblyName passed in is in the GAC. + /// + private bool CheckForAssemblyInGac(AssemblyNameExtension assemblyName, SystemProcessorArchitecture targetProcessorArchitecture, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, FileExists fileExists) + { + string gacLocation = null; + if (assemblyName.Version != null) + { + gacLocation = GlobalAssemblyCache.GetLocation(BuildEngine as IBuildEngine4, assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, true, fileExists, null, null, false /* this value does not matter if we are passing a full fusion name*/); + } + return gacLocation != null; + } + + /// + /// Execute the task. + /// + /// True if there was success. + override public bool Execute() + { + return Execute + ( + new FileExists(FileUtilities.FileExistsNoThrow), + new DirectoryExists(FileUtilities.DirectoryExistsNoThrow), + new GetDirectories(Directory.GetDirectories), + new GetAssemblyName(AssemblyNameExtension.GetAssemblyNameEx), + new GetAssemblyMetadata(AssemblyInformation.GetAssemblyMetadata), + new GetRegistrySubKeyNames(RegistryHelper.GetSubKeyNames), + new GetRegistrySubKeyDefaultValue(RegistryHelper.GetDefaultValue), + new GetLastWriteTime(NativeMethodsShared.GetLastWriteFileUtcTime), + new GetAssemblyRuntimeVersion(AssemblyInformation.GetRuntimeVersion), + new OpenBaseKey(RegistryHelper.OpenBaseKey), + new CheckIfAssemblyInGac(CheckForAssemblyInGac), + new IsWinMDFile(AssemblyInformation.IsWinMDFile), + new ReadMachineTypeFromPEHeader(ReferenceTable.ReadMachineTypeFromPEHeader) + ); + } + + #endregion + } +} diff --git a/src/XMakeTasks/AssemblyDependency/Resolver.cs b/src/XMakeTasks/AssemblyDependency/Resolver.cs new file mode 100644 index 00000000000..448f8e750c2 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/Resolver.cs @@ -0,0 +1,409 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for all resolver types. + /// + abstract internal class Resolver + { + /// + /// The corresponding element from the search path. + /// + protected string searchPathElement; + + /// + /// Delegate. + /// + protected GetAssemblyName getAssemblyName; + + /// + /// Delegate. + /// + protected FileExists fileExists; + + /// + /// Delegate + /// + protected GetAssemblyRuntimeVersion getRuntimeVersion; + + /// + /// Runtime we are targeting + /// + protected Version targetedRuntimeVersion = null; + + /// + /// Processor architecture we are targeting. + /// + protected ProcessorArchitecture targetProcessorArchitecture; + + /// + /// Should the processor architecture we are targeting match the assembly we resolve from disk. + /// + protected bool compareProcessorArchitecture; + + /// + /// Construct. + /// + protected Resolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, ProcessorArchitecture targetedProcessorArchitecture, bool compareProcessorArchitecture) + { + this.searchPathElement = searchPathElement; + this.getAssemblyName = getAssemblyName; + this.fileExists = fileExists; + this.getRuntimeVersion = getRuntimeVersion; + this.targetedRuntimeVersion = targetedRuntimeVesion; + this.targetProcessorArchitecture = targetedProcessorArchitecture; + this.compareProcessorArchitecture = compareProcessorArchitecture; + } + + /// + /// Resolve a reference to a specific file name. + /// + /// The assemblyname of the reference. + /// The name of the sdk to resolve. + /// The reference's 'include' treated as a raw file name. + /// Whether or not this reference was directly from the project file (and therefore not a dependency) + /// Whether an exact version match is requested. + /// Allowed executable extensions. + /// The item's hintpath value. + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// The path where the file was found. + /// Whether or not the user wanted a specific file (for example, HintPath is a request for a specific file) + /// True if the file was resolved. + abstract public bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + out string foundPath, + out bool userRequestedSpecificFile + + ); + + /// + /// The search path element that this resolver is based on. + /// + public string SearchPath + { + get { return searchPathElement; } + } + + /// + /// Resolve a single file. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// True if the file was a match, false otherwise. + protected bool ResolveAsFile + ( + string fullPath, + AssemblyNameExtension assemblyName, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + bool allowMismatchBetweenFusionNameAndFileName, + ArrayList assembliesConsideredAndRejected + ) + { + ResolutionSearchLocation considered = null; + if (assembliesConsideredAndRejected != null) + { + considered = new ResolutionSearchLocation(); + considered.FileNameAttempted = fullPath; + considered.SearchPath = searchPathElement; + } + + if (FileMatchesAssemblyName(assemblyName, isPrimaryProjectReference, wantSpecificVersion, allowMismatchBetweenFusionNameAndFileName, fullPath, considered)) + { + return true; + } + + // Record this as a location that was considered. + if (assembliesConsideredAndRejected != null) + { + assembliesConsideredAndRejected.Add(considered); + } + + return false; + } + + /// + /// Determines whether an assembly name matches the assembly pointed to by pathToCandidateAssembly + /// + /// The assembly name to look up. + /// True if this is a primary reference directly from the project file. + /// Whether the version needs to match exactly or loosely. + /// Path to a possible file. + /// Information about why the candidate file didn't match + /// Delegate for File.Exists. + /// Delegate for AssemblyName.GetAssemblyName + /// + protected bool FileMatchesAssemblyName + ( + AssemblyNameExtension assemblyName, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + bool allowMismatchBetweenFusionNameAndFileName, + string pathToCandidateAssembly, + ResolutionSearchLocation searchLocation + ) + { + searchLocation.FileNameAttempted = pathToCandidateAssembly; + + // Base name of the target file has to match the Name from the assemblyName + if (!allowMismatchBetweenFusionNameAndFileName) + { + string candidateBaseName = Path.GetFileNameWithoutExtension(pathToCandidateAssembly); + if (String.Compare(assemblyName.Name, candidateBaseName, StringComparison.CurrentCultureIgnoreCase) != 0) + { + if (searchLocation != null) + { + if (candidateBaseName.Length > 0) + { + searchLocation.AssemblyName = new AssemblyNameExtension(candidateBaseName); + searchLocation.Reason = NoMatchReason.FusionNamesDidNotMatch; + } + else + { + searchLocation.Reason = NoMatchReason.TargetHadNoFusionName; + } + } + return false; + } + } + + bool isSimpleAssemblyName = assemblyName == null ? false : assemblyName.IsSimpleName; + + if (fileExists(pathToCandidateAssembly)) + { + // If the resolver we are using is targeting a given processor architecture then we must crack open the assembly and make sure the architecture is compatible + // We cannot do these simple name matches. + if (!compareProcessorArchitecture) + { + // If the file existed and the reference is a simple primary reference which does not contain an assembly name (say a raw file name) + // then consider this a match. + if (assemblyName == null && isPrimaryProjectReference && !wantSpecificVersion) + { + return true; + } + + if (isPrimaryProjectReference && !wantSpecificVersion && isSimpleAssemblyName) + { + return true; + } + } + + // We have strong name information, so do some added verification here. + AssemblyNameExtension targetAssemblyName = null; + try + { + targetAssemblyName = getAssemblyName(pathToCandidateAssembly); + } + catch (System.IO.FileLoadException) + { + // Its pretty hard to get here, you need an assembly that contains a valid reference + // to a dependent assembly that, in turn, throws a FileLoadException during GetAssemblyName. + // Still it happened once, with an older version of the CLR. + + // ...falling through and relying on the targetAssemblyName==null behavior below... + } + + if (searchLocation != null) + { + searchLocation.AssemblyName = targetAssemblyName; + } + + // targetAssemblyName may be null if there was no metadata for this assembly. + // In this case, there's no match. + if (targetAssemblyName != null) + { + // If we are targeting a given processor architecture check to see if they match, if we are targeting MSIL then any architecture will do. + if (compareProcessorArchitecture) + { + // Only reject the assembly if the target processor architecture does not match the assemby processor architecture and the assembly processor architecture is not NONE or MSIL. + if ( + targetAssemblyName.AssemblyName.ProcessorArchitecture != targetProcessorArchitecture && /* The target and assembly architectures do not match*/ + (targetProcessorArchitecture != ProcessorArchitecture.None && targetAssemblyName.AssemblyName.ProcessorArchitecture != ProcessorArchitecture.None) /*The assembly is not none*/ + && (targetProcessorArchitecture != ProcessorArchitecture.MSIL && targetAssemblyName.AssemblyName.ProcessorArchitecture != ProcessorArchitecture.MSIL) /*The assembly is not MSIL*/ + ) + { + searchLocation.Reason = NoMatchReason.ProcessorArchitectureDoesNotMatch; + return false; + } + } + + bool matchedSpecificVersion = (wantSpecificVersion && assemblyName != null && assemblyName.Equals(targetAssemblyName)); + bool matchPartialName = !wantSpecificVersion && assemblyName != null && assemblyName.PartialNameCompare(targetAssemblyName); + + if (matchedSpecificVersion || matchPartialName) + { + return true; + } + else + { + // Reason was: FusionNames did not match. + if (searchLocation != null) + { + searchLocation.Reason = NoMatchReason.FusionNamesDidNotMatch; + } + } + } + else + { + // Reason was: Target had no fusion name. + if (searchLocation != null) + { + searchLocation.Reason = NoMatchReason.TargetHadNoFusionName; + } + } + } + else + { + // Reason was: No file found at that location. + if (searchLocation != null) + { + searchLocation.Reason = NoMatchReason.FileNotFound; + } + } + + return false; + } + + /// + /// Given a strong name, which may optionally have Name, Version and Public Key, + /// return a fully qualified directory name. + /// + /// The assembly name to look up. + /// True if this is a primary reference directly from the project file. + /// The possible filename extensions of the assembly. Must be one of these or its no match. + /// the directory to look in + /// Receives the list of locations that this function tried to find the assembly. May be "null". + /// Delegate for File.Exists. + /// Delegate for AssemblyName.GetAssemblyName + /// 'null' if the assembly wasn't found. + protected string ResolveFromDirectory + ( + AssemblyNameExtension assemblyName, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string directory, + ArrayList assembliesConsideredAndRejected + ) + { + if (assemblyName == null) + { + // This can happen if the assembly name is actually a file name. + return null; + } + + // used for the case when we are targeting MSIL and need to return that if it exists. This is different from targeting other architectures where returning an MSIL or target architecture are ok. + string candidateFullPath = null; + + if (directory != null) + { + string weakNameBase = assemblyName.Name; + for (int i = 0; i < executableExtensions.Length; ++i) + { + string baseName = weakNameBase + executableExtensions[i]; + string fullPath = null; + + try + { + fullPath = Path.Combine(directory, baseName); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + // Assuming it's the search path that's bad. But combine them both so the error is visible if it's the reference itself. + throw new InvalidParameterValueException("SearchPaths", directory + (directory.EndsWith("\\", StringComparison.OrdinalIgnoreCase) ? String.Empty : "\\") + baseName, e.Message); + } + + // We have a full path returned + if (ResolveAsFile(fullPath, assemblyName, isPrimaryProjectReference, wantSpecificVersion, false, assembliesConsideredAndRejected)) + { + if (candidateFullPath == null) + { + candidateFullPath = fullPath; + } + + + /* + * After finding a file we now will check to see if it matches the type of processor architecture we want to return. The rules are as follows + * + * If targeting AMD64 / X86 / IA64 / ARM /NONE we will return the first assembly which has a matching processor architecture OR is an assembly with a processor architecture of MSIL or NONE + * + * If targeting MSIL we will first look through all of the assemblies, if an MSIL assembly is found we will return that. If no MSIL assembly is found we will return + * the first assembly which matches reguardless of its processor architecture. + + */ + + if (targetProcessorArchitecture == ProcessorArchitecture.MSIL) + { + // Lets see if the processor architecture matches + AssemblyNameExtension foundAssembly = getAssemblyName(fullPath); + + // If the processor architecture does not match the we should continue to see if there is a better match. + if (foundAssembly != null && foundAssembly.AssemblyName.ProcessorArchitecture == ProcessorArchitecture.MSIL) + { + return fullPath; + } + } + else + { + return fullPath; + } + } + } + + // If we did not find an assembly that matched then see if the assembly name is actually a filename. + if (candidateFullPath == null) + { + // If the file ends with an extension like .dll or .exe then just try that. + string weakNameBaseExtension = Path.GetExtension(weakNameBase); + string weakNameBaseFileName = Path.GetFileNameWithoutExtension(weakNameBase); + + if (weakNameBaseExtension != null && weakNameBaseExtension.Length > 0 && weakNameBaseFileName != null && weakNameBaseFileName.Length > 0) + { + for (int i = 0; i < executableExtensions.Length; ++i) + { + if (String.Compare(executableExtensions[i], weakNameBaseExtension, StringComparison.CurrentCultureIgnoreCase) == 0) + { + string fullPath = Path.Combine(directory, weakNameBase); + AssemblyNameExtension extensionlessAssemblyName = new AssemblyNameExtension(weakNameBaseFileName); + + if (ResolveAsFile(fullPath, extensionlessAssemblyName, isPrimaryProjectReference, wantSpecificVersion, false, assembliesConsideredAndRejected)) + { + return fullPath; + } + } + } + } + } + } + + return candidateFullPath; + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/TaskItemSpecFilenameComparer.cs b/src/XMakeTasks/AssemblyDependency/TaskItemSpecFilenameComparer.cs new file mode 100644 index 00000000000..5fc5a6840fb --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/TaskItemSpecFilenameComparer.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using Microsoft.Build.Framework; +using System.Collections.Generic; + +namespace Microsoft.Build.Tasks +{ + /// + /// Compare two ITaskItems by the file name in their ItemSpec. + /// + sealed internal class TaskItemSpecFilenameComparer : IComparer, IComparer + { + internal readonly static IComparer comparer = new TaskItemSpecFilenameComparer(); + internal readonly static IComparer genericComparer = new TaskItemSpecFilenameComparer(); + + /// + /// Private construct so there's only one instance. + /// + private TaskItemSpecFilenameComparer() + { + } + + /// + /// Compare the two AssemblyNameReferences by file name, and if that is equal, by item spec. + /// + /// + /// Sorting by item spec allows these to be ordered consistently: + /// c:\Regress315619\A\MyAssembly.dll + /// c:\Regress315619\B\MyAssembly.dll + /// + public int Compare(object o1, object o2) + { + if (Object.ReferenceEquals(o1, o2)) + { + return 0; + } + + ITaskItem i1 = (ITaskItem)o1; + ITaskItem i2 = (ITaskItem)o2; + + return Compare(i1, i2); + } + + public int Compare(ITaskItem x, ITaskItem y) + { + if (Object.ReferenceEquals(x, y)) + { + return 0; + } + + string f1 = Path.GetFileName(x.ItemSpec); + string f2 = Path.GetFileName(y.ItemSpec); + + int fileComparison = String.Compare(f1, f2, StringComparison.OrdinalIgnoreCase); + if (fileComparison != 0) + { + return fileComparison; + } + + return String.Compare(x.ItemSpec, y.ItemSpec, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/UnificationReason.cs b/src/XMakeTasks/AssemblyDependency/UnificationReason.cs new file mode 100644 index 00000000000..d4db845b835 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/UnificationReason.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks +{ + /// + /// The reason that a unification happened + /// + internal enum UnificationReason + { + /// + /// This reference was not unified. + /// + DidntUnify, + + /// + /// Unified because this was a framework assembly and it the current fusion + /// loader rules would unify to a different version. + /// + FrameworkRetarget, + + /// + /// Unified because of a binding redirect coming from either an explicit + /// app.config file or implicitly because AutoUnify was true. + /// + BecauseOfBindingRedirect + } +} diff --git a/src/XMakeTasks/AssemblyDependency/UnificationVersion.cs b/src/XMakeTasks/AssemblyDependency/UnificationVersion.cs new file mode 100644 index 00000000000..00006c6570f --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/UnificationVersion.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; + +namespace Microsoft.Build.Tasks +{ + /// + /// A version number coupled with a reason why this version number + /// was chosen. + /// + internal struct UnificationVersion + { + internal string referenceFullPath; + internal Version version; + internal UnificationReason reason; + } +} diff --git a/src/XMakeTasks/AssemblyDependency/UnifiedAssemblyName.cs b/src/XMakeTasks/AssemblyDependency/UnifiedAssemblyName.cs new file mode 100644 index 00000000000..459457ca2f3 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/UnifiedAssemblyName.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// A unified assembly name. + /// + internal class UnifiedAssemblyName + { + private AssemblyNameExtension _preUnified; + private AssemblyNameExtension _postUnified; + private bool _isUnified; + private bool _isPrerequisite; + private bool? _isRedistRoot; + private string _redistName; + private UnificationReason _unificationReason; + + public UnifiedAssemblyName(AssemblyNameExtension preUnified, AssemblyNameExtension postUnified, bool isUnified, UnificationReason unificationReason, bool isPrerequisite, bool? isRedistRoot, string redistName) + { + _preUnified = preUnified; + _postUnified = postUnified; + _isUnified = isUnified; + _isPrerequisite = isPrerequisite; + _isRedistRoot = isRedistRoot; + _redistName = redistName; + _unificationReason = unificationReason; + } + + public AssemblyNameExtension PreUnified + { + get { return _preUnified; } + } + + public AssemblyNameExtension PostUnified + { + get { return _postUnified; } + } + + public bool IsUnified + { + get { return _isUnified; } + } + + public UnificationReason UnificationReason + { + get { return _unificationReason; } + } + + public bool IsPrerequisite + { + get { return _isPrerequisite; } + } + + public bool? IsRedistRoot + { + get { return _isRedistRoot; } + } + + public string RedistName + { + get { return _redistName; } + } + } +} diff --git a/src/XMakeTasks/AssemblyDependency/WarnOrErrorOnTargetArchitectureMismatchBehavior.cs b/src/XMakeTasks/AssemblyDependency/WarnOrErrorOnTargetArchitectureMismatchBehavior.cs new file mode 100644 index 00000000000..3490a10ba49 --- /dev/null +++ b/src/XMakeTasks/AssemblyDependency/WarnOrErrorOnTargetArchitectureMismatchBehavior.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Should a warning or error or nothing be emitted when there is a architecture mismatch +//----------------------------------------------------------------------- + +using System; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Reflection; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Tasks +{ + #region enum + /// + /// Enum describing the behavior when a a primary reference has an architecture different from the project + /// + internal enum WarnOrErrorOnTargetArchitectureMismatchBehavior + { + /// + /// Print an error + /// + Error, + + /// + /// Print a warning + /// + Warning, + + /// + /// Do nothing + /// + None + } + #endregion +} diff --git a/src/XMakeTasks/AssemblyFolder.cs b/src/XMakeTasks/AssemblyFolder.cs new file mode 100644 index 00000000000..4b88bcd032e --- /dev/null +++ b/src/XMakeTasks/AssemblyFolder.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Diagnostics; +using Microsoft.Build.Shared; +using Microsoft.Win32; + +namespace Microsoft.Build.Tasks +{ + /// + /// Contains utility functions for dealing with assembly folders found in the registry. + /// + internal static class AssemblyFolder + { + /// + /// Key -- Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project + /// Value -- Directory + /// + private static Hashtable s_assemblyFolders; + + /// + /// Synchronize the creation of assemblyFolders + /// + private static Object s_syncLock = new Object(); + + /// + /// Given a registry key, find all of the registered assembly folders and add them to the list. + /// + /// Like 'hklm' or 'hkcu' + private static void AddFoldersFromRegistryKey + ( + RegistryKey hive, + string key, + Hashtable directories + ) + { + using (RegistryKey baseKey = hive.OpenSubKey(key)) + { + string aliasKey = String.Empty; + + if (hive == Registry.CurrentUser) + { + aliasKey = "hkcu"; + } + else if (hive == Registry.LocalMachine) + { + aliasKey = "hklm"; + } + else + { + ErrorUtilities.VerifyThrow(false, "AssemblyFolder.AddFoldersFromRegistryKey expected a known hive."); + } + + if (baseKey != null) + { + foreach (string productName in baseKey.GetSubKeyNames()) + { + using (RegistryKey product = baseKey.OpenSubKey(productName)) + { + if (product.ValueCount > 0) + { + string folder = (string)product.GetValue(""); + if (Directory.Exists(folder)) + { + string regkeyAlias = aliasKey + "\\" + productName; + directories[regkeyAlias] = folder; + } + } + } + } + } + } + } + + /// + /// For the given key name, look for registered assembly folders in HKCU then HKLM. + /// + /// + /// + private static void AddFoldersFromRegistryKey + ( + string key, + Hashtable directories + ) + { + // First add the current user. + AddFoldersFromRegistryKey + ( + Registry.CurrentUser, + key, + directories + ); + + // Then add the local machine. + AddFoldersFromRegistryKey + ( + Registry.LocalMachine, + key, + directories + ); + } + + /// + /// Populates the internal tables. + /// + private static void CreateAssemblyFolders() + { + s_assemblyFolders = new Hashtable(StringComparer.OrdinalIgnoreCase); + + // Populate the table of assembly folders. + AddFoldersFromRegistryKey + ( + @"SOFTWARE\Microsoft\.NETFramework\AssemblyFolders", + s_assemblyFolders + ); + + AddFoldersFromRegistryKey + ( + @"SOFTWARE\Microsoft\VisualStudio\8.0\AssemblyFolders", + s_assemblyFolders + ); + } + + /// + /// Returns the list of assembly folders that we're interested in. + /// + /// Like "hklm\Vendor RegKey" as provided to a reference by the on the reference in the project. + /// Collection of assembly folders. + static internal ICollection GetAssemblyFolders(string regKeyAlias) + { + lock (s_syncLock) + { + if (s_assemblyFolders == null) + { + CreateAssemblyFolders(); + } + } + + // If no specific alias was requested then return the complete list. + if (regKeyAlias == null || regKeyAlias.Length == 0) + { + return AssemblyFolder.s_assemblyFolders.Values; + } + + // If a specific alias was requested then return only that alias. + ArrayList specificKey = new ArrayList(); + string directory = (string)s_assemblyFolders[regKeyAlias]; + if (directory != null && directory.Length > 0) + { + specificKey.Add(directory); + } + return specificKey; + } + } +} diff --git a/src/XMakeTasks/AssemblyInfo.cs b/src/XMakeTasks/AssemblyInfo.cs new file mode 100644 index 00000000000..0ef1e57fc12 --- /dev/null +++ b/src/XMakeTasks/AssemblyInfo.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Security; +using System.Security.Permissions; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +// This is the assembly-level GUID, and the GUID for the TypeLib associated with +// this assembly. We should specify this explicitly, as opposed to letting +// tlbexp just pick whatever it wants. +[assembly: GuidAttribute("E3D4D3B9-944C-407b-A82E-B19719EA7FB3")] +#if (STANDALONEBUILD) +[assembly: AssemblyVersion(Microsoft.Build.Shared.MSBuildConstants.CurrentAssemblyVersion)] +[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.UnitTests")] +#else +[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Whidbey.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +#endif + +// This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, +// so that we don't run into known security issues with loading libraries from unsafe locations +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] \ No newline at end of file diff --git a/src/XMakeTasks/AssemblyRegistrationCache.cs b/src/XMakeTasks/AssemblyRegistrationCache.cs new file mode 100644 index 00000000000..e6cc66d26f1 --- /dev/null +++ b/src/XMakeTasks/AssemblyRegistrationCache.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class is a caching mechanism for the Register/UnregisterAssembly task to keep track of registered assemblies to clean up + /// + [Serializable()] + internal sealed class AssemblyRegistrationCache : StateFileBase + { + /// + /// The list of registered assembly files. + /// + private ArrayList _assemblies = null; + + /// + /// The list of registered type library files. + /// + private ArrayList _typeLibraries = null; + + /// + /// Construct. + /// + internal AssemblyRegistrationCache() + { + _assemblies = new ArrayList(); + _typeLibraries = new ArrayList(); + } + + /// + /// The number of entries in the state file + /// + internal int Count + { + get + { + ErrorUtilities.VerifyThrow(_assemblies.Count == _typeLibraries.Count, "Internal assembly and type library lists should have the same number of entries in AssemblyRegistrationCache"); + return _assemblies.Count; + } + } + + /// + /// Sets the entry with the specified index + /// + /// + /// + /// + internal void AddEntry(string assemblyPath, string typeLibraryPath) + { + _assemblies.Add(assemblyPath); + _typeLibraries.Add(typeLibraryPath); + } + + /// + /// Gets the entry with the specified index + /// + /// + /// + /// + internal void GetEntry(int index, out string assemblyPath, out string typeLibraryPath) + { + ErrorUtilities.VerifyThrow((index >= 0) && (index < _assemblies.Count), "Invalid index in the call to AssemblyRegistrationCache.GetEntry"); + assemblyPath = (string)_assemblies[index]; + typeLibraryPath = (string)_typeLibraries[index]; + } + } +} diff --git a/src/XMakeTasks/AssemblyRemapping.cs b/src/XMakeTasks/AssemblyRemapping.cs new file mode 100644 index 00000000000..4464c354cd2 --- /dev/null +++ b/src/XMakeTasks/AssemblyRemapping.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Describes a remapping entry pair +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Describes a remapping entry pair + /// + internal class AssemblyRemapping : IEquatable + { + /// + /// The assemblyName we mapped from + /// + private readonly AssemblyNameExtension _from; + + /// + /// The assemblyName we mapped to + /// + private readonly AssemblyNameExtension _to; + + /// + /// Constructor + /// + public AssemblyRemapping(AssemblyNameExtension from, AssemblyNameExtension to) + { + _from = from; + _to = to; + } + + /// + /// The assemblyName we mapped from + /// + public AssemblyNameExtension From + { + get + { + return _from; + } + } + + /// + /// The assemblyName we mapped to + /// + public AssemblyNameExtension To + { + get + { + return _to; + } + } + + /// + /// Compare two Assembly remapping objects + /// + public override bool Equals(object obj) + { + AssemblyNameExtension name = obj as AssemblyNameExtension; + if (obj == null) + { + return false; + } + + return Equals(name); + } + + /// + /// Get the hash code + /// + public override int GetHashCode() + { + return _from.GetHashCode(); + } + + /// + /// We only compare the from because in terms of what is in the redist list unique from's are expected + /// + public bool Equals(AssemblyRemapping other) + { + return _from.Equals(other._from); + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/AssemblyResources.cs b/src/XMakeTasks/AssemblyResources.cs new file mode 100644 index 00000000000..3cb4c73ba6f --- /dev/null +++ b/src/XMakeTasks/AssemblyResources.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Resources; +using System.Reflection; +using System.Globalization; + +namespace Microsoft.Build.Shared +{ + /// + /// This class provides access to the assembly's resources. + /// + internal static class AssemblyResources + { + /// + /// Loads the specified resource string, either from the assembly's primary resources, or its shared resources. + /// + /// This method is thread-safe. + /// + /// The resource string, or null if not found. + internal static string GetString(string name) + { + // NOTE: the ResourceManager.GetString() method is thread-safe + string resource = s_resources.GetString(name, CultureInfo.CurrentUICulture); + + if (resource == null) + { + resource = s_sharedResources.GetString(name, CultureInfo.CurrentUICulture); + } + + ErrorUtilities.VerifyThrow(resource != null, "Missing resource '{0}'", name); + + return resource; + } + + /// + /// Gets the assembly's primary resources i.e. the resources exclusively owned by this assembly. + /// + /// This property is thread-safe. + /// ResourceManager for primary resources. + internal static ResourceManager PrimaryResources + { + get + { + return s_resources; + } + } + + /// + /// Gets the assembly's shared resources i.e. the resources this assembly shares with other assemblies. + /// + /// This property is thread-safe. + /// ResourceManager for shared resources. + internal static ResourceManager SharedResources + { + get + { + return s_sharedResources; + } + } + + // assembly resources + private static readonly ResourceManager s_resources = new ResourceManager("Microsoft.Build.Tasks.Strings", Assembly.GetExecutingAssembly()); + // shared resources + private static readonly ResourceManager s_sharedResources = new ResourceManager("Microsoft.Build.Tasks.Strings.shared", Assembly.GetExecutingAssembly()); + } +} diff --git a/src/XMakeTasks/AssignCulture.cs b/src/XMakeTasks/AssignCulture.cs new file mode 100644 index 00000000000..e67b3a97e3e --- /dev/null +++ b/src/XMakeTasks/AssignCulture.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using System.Globalization; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Class: AssignCulture + /// + /// This task takes a list of resource file names and sets an attribute that + /// contains the culture name embedded in the file name: + /// + /// MyResources.fr.resx ==> Culture='fr' + /// + /// The task can also return a list of "Culture-neutral" file names, like: + /// + /// MyGlyph.fr.bmp ==> MyGlyph.bmp [Culture='fr'] + /// + /// This is because embedded resources are refered to this way. + /// + /// There are plenty of corner cases with this task. See the unit test for + /// more details. + /// + public class AssignCulture : TaskExtension + { + /// + /// Constructor + /// + public AssignCulture() + { + } + + #region Properties + + private ITaskItem[] _files = new TaskItem[0]; + private ITaskItem[] _assignedFiles = null; + private ITaskItem[] _assignedFilesWithCulture = null; + private ITaskItem[] _assignedFilesWithNoCulture = null; + private ITaskItem[] _cultureNeutralAssignedFiles = null; + + /// + /// The incoming list of files to assign a culture to. + /// + [Required] + public ITaskItem[] Files + { + get { return _files; } + set { _files = value; } + } + + /// + /// This outgoing list of files is exactly the same as the incoming Files + /// list except that an attribute name "Culture" will have been added if + /// the particular file name is in the form: + /// + /// MyResource..resx + /// + /// The value of Culture will be "". + /// + /// If the incoming item from Files already has a Culture attribute then + /// that original attribute is used instead. + /// + [Output] + public ITaskItem[] AssignedFiles + { + get { return _assignedFiles; } + } + + /// + /// This is a subset of AssignedFiles that has all of the items that + /// ended up have a Culture assigned to them. This includes items that + /// already had a Culture in the incoming Files list as well as items + /// that were assigned a Culture because they had a valid culture ID + /// embedded in their file name. + /// + /// The following is always true: + /// + /// AssignedFiles = AssignedFilesWithCulture + AssignedFilesWithNoCulture + /// + [Output] + public ITaskItem[] AssignedFilesWithCulture + { + get { return _assignedFilesWithCulture; } + } + + /// + /// This is a subset of AssignedFiles that has all of the items that + /// ended up with no Culture assigned to them. + /// + /// The following is always true: + /// + /// AssignedFiles = AssignedFilesWithCulture + AssignedFilesWithNoCulture + /// + [Output] + public ITaskItem[] AssignedFilesWithNoCulture + { + get { return _assignedFilesWithNoCulture; } + } + + /// + /// This list has the same number of items as the Files list or the + /// AssignedFiles list. + /// + /// Items in this list have the file name from Files or AssignedFiles + /// but with the culture stripped if it was embedded in the file name. + /// + /// So for example, if the incoming item in Files was: + /// + /// MyBitmap.fr.bmp + /// + /// then the corresponding file in CultureNeutralAssignedFiles will be: + /// + /// MyBitmap.bmp + /// + /// The culture will only be stripped if it is a valid culture identifier. + /// So for example, + /// + /// MyCrazyFile.XX.txt + /// + /// will result in exactly the same file name: + /// + /// MyCrazyFile.XX.txt + /// + /// because 'XX' is not a valid culture identifier. + /// + [Output] + public ITaskItem[] CultureNeutralAssignedFiles + { + get { return _cultureNeutralAssignedFiles; } + } + + #endregion + + #region ITask Members + + + /// + /// Execute. + /// + /// + public override bool Execute() + { + _assignedFiles = new TaskItem[Files.Length]; + _cultureNeutralAssignedFiles = new TaskItem[Files.Length]; + ArrayList cultureList = new ArrayList(); + ArrayList noCultureList = new ArrayList(); + + bool retValue = true; + + for (int i = 0; i < Files.Length; ++i) + { + try + { + AssignedFiles[i] = new TaskItem(Files[i]); + + string dependentUpon = AssignedFiles[i].GetMetadata(ItemMetadataNames.dependentUpon); + Culture.ItemCultureInfo info = Culture.GetItemCultureInfo + ( + AssignedFiles[i].ItemSpec, + dependentUpon + ); + + if (info.culture != null && info.culture.Length > 0) + { + AssignedFiles[i].SetMetadata("Culture", info.culture); + AssignedFiles[i].SetMetadata("WithCulture", "true"); + cultureList.Add(AssignedFiles[i]); + } + else + { + noCultureList.Add(AssignedFiles[i]); + AssignedFiles[i].SetMetadata("WithCulture", "false"); + } + + CultureNeutralAssignedFiles[i] = new TaskItem(AssignedFiles[i]); + CultureNeutralAssignedFiles[i].ItemSpec = info.cultureNeutralFilename; + + Log.LogMessageFromResources + ( + MessageImportance.Low, + "AssignCulture.Comment", + AssignedFiles[i].GetMetadata("Culture"), + AssignedFiles[i].ItemSpec + ); + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("AssignCulture.CannotExtractCulture", Files[i].ItemSpec, e.Message); + retValue = false; + } +#if _DEBUG + catch (Exception e) + { + Debug.Assert(false, "Unexpected exception in AssignCulture.Execute. " + + "Please log a MSBuild bug specifying the steps to reproduce the problem. " + + e.Message); + throw; + } +#endif + } + + _assignedFilesWithCulture = (ITaskItem[])cultureList.ToArray(typeof(ITaskItem)); + _assignedFilesWithNoCulture = (ITaskItem[])noCultureList.ToArray(typeof(ITaskItem)); + + return retValue; + } + + #endregion + } +} diff --git a/src/XMakeTasks/AssignLinkMetadata.cs b/src/XMakeTasks/AssignLinkMetadata.cs new file mode 100644 index 00000000000..a5d57dc097e --- /dev/null +++ b/src/XMakeTasks/AssignLinkMetadata.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Task to assign a reasonable "Link" metadata to the provided items. +//----------------------------------------------------------------------- + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task to assign a reasonable "Link" metadata to the provided items. + /// + public class AssignLinkMetadata : TaskExtension + { + /// + /// The set of items to assign metadata to + /// + public ITaskItem[] Items + { + get; + set; + } + + /// + /// The set of items to which the Link metadata has been set + /// + [Output] + public ITaskItem[] OutputItems + { + get; + set; + } + + /// + /// Sets "Link" metadata on any item where the project file in which they + /// were defined is different from the parent project file to a sane default: + /// the relative directory compared to the defining file. + /// + /// Does NOT overwrite Link metadata if it's already defined. + /// + public override bool Execute() + { + List outputItems = new List(); + + if (this.Items != null) + { + foreach (var item in this.Items) + { + try + { + string definingProject = item.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectFullPath); + string definingProjectDirectory = item.GetMetadata(FileUtilities.ItemSpecModifiers.DefiningProjectDirectory); + string fullPath = item.GetMetadata(FileUtilities.ItemSpecModifiers.FullPath); + + if ( + String.IsNullOrEmpty(item.GetMetadata("Link")) + && !String.IsNullOrEmpty(definingProject) + && fullPath.StartsWith(definingProjectDirectory, StringComparison.OrdinalIgnoreCase) + ) + { + string link = fullPath.Substring(definingProjectDirectory.Length); + ITaskItem outputItem = new TaskItem(item); + outputItem.SetMetadata("Link", link); + + outputItems.Add(outputItem); + } + } + catch (InvalidOperationException e) + { + // can happen if the item is not a proper path + Log.LogWarningFromException(e); + } + } + } + + this.OutputItems = outputItems.ToArray(); + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/XMakeTasks/AssignProjectConfiguration.cs b/src/XMakeTasks/AssignProjectConfiguration.cs new file mode 100644 index 00000000000..e8be474fe5d --- /dev/null +++ b/src/XMakeTasks/AssignProjectConfiguration.cs @@ -0,0 +1,572 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + public class AssignProjectConfiguration : ResolveProjectBase + { + #region Constructors + + /// + /// default public constructor + /// + public AssignProjectConfiguration() + { + // do nothing + } + + #endregion + + #region Properties + + /// + /// A special XML string containing a project configuration for each project - we need to simply + /// match the projects and assign the appropriate configuration names to them + /// + public string SolutionConfigurationContents + { + get + { + return _solutionConfigurationContents; + } + set + { + _solutionConfigurationContents = value; + } + } + + private string _solutionConfigurationContents = null; + + /// + /// Whether to use the solution dependency information passed in the solution blob + /// to add synthetic project references for the purposes of build ordering + /// + public bool AddSyntheticProjectReferencesForSolutionDependencies + { + get; + set; + } + + /// + /// String containing a semicolon-delimited list of mappings from the platform names used + /// by most VS types to those used by .vcxprojs. + /// + /// + /// E.g. "AnyCPU=Win32" + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Vcx", Justification = "Public API that has already shipped; VCX is a recognizable short form for .vcxproj")] + public string DefaultToVcxPlatformMapping + { + get + { + if (_defaultToVcxPlatformMapping == null) + { + _defaultToVcxPlatformMapping = "AnyCPU=Win32;X86=Win32;X64=X64;Itanium=Itanium"; + } + + return _defaultToVcxPlatformMapping; + } + + set + { + _defaultToVcxPlatformMapping = value; + if (_defaultToVcxPlatformMapping != null && _defaultToVcxPlatformMapping.Length == 0) + { + _defaultToVcxPlatformMapping = null; + } + } + } + + private string _defaultToVcxPlatformMapping = null; + + /// + /// String containing a semicolon-delimited list of mappings from .vcxproj platform names + /// to the platform names use by most other VS project types. + /// + /// + /// E.g. "Win32=AnyCPU" + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Vcx", Justification = "Public API that has already shipped; VCX is a recognizable short form for .vcxproj")] + public string VcxToDefaultPlatformMapping + { + get + { + if (_vcxToDefaultPlatformMapping == null) + { + if (String.Equals("Library", OutputType, StringComparison.OrdinalIgnoreCase)) + { + _vcxToDefaultPlatformMapping = "Win32=AnyCPU;X64=X64;Itanium=Itanium"; + } + else + { + _vcxToDefaultPlatformMapping = "Win32=X86;X64=X64;Itanium=Itanium"; + } + } + + return _vcxToDefaultPlatformMapping; + } + + set + { + _vcxToDefaultPlatformMapping = value; + + if (_vcxToDefaultPlatformMapping != null && _vcxToDefaultPlatformMapping.Length == 0) + { + _vcxToDefaultPlatformMapping = null; + } + } + } + + private string _vcxToDefaultPlatformMapping = null; + + /// + /// The current project's full path + /// + public string CurrentProject + { + get; + set; + } + + /// + /// The current project's platform. + /// + public string CurrentProjectConfiguration + { + get + { + return _currentProjectConfiguration; + } + + set + { + _currentProjectConfiguration = value; + } + } + + private string _currentProjectConfiguration; + + /// + /// The current project's platform. + /// + public string CurrentProjectPlatform + { + get + { + return _currentProjectPlatform; + } + + set + { + _currentProjectPlatform = value; + } + } + + /// + /// Should we build references even if they were disabled in the project configuration + /// + private bool _onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration = false; + + /// + /// Should we build references even if they were disabled in the project configuration + /// + public bool OnlyReferenceAndBuildProjectsEnabledInSolutionConfiguration + { + get + { + return _onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration; + } + + set + { + _onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration = value; + } + } + + // Whether to set the project reference's GlobalPropertiesToRemove metadata to contain + // Configuration and Platform. + private bool _shouldUnsetParentConfigurationAndPlatform = false; + + /// + /// Whether to set the GlobalPropertiesToRemove metadata on the project reference such that + /// on an MSBuild call, the Configuration and Platform metadata will be unset, allowing the + /// child project to build in its default configuration / platform. + /// + public bool ShouldUnsetParentConfigurationAndPlatform + { + get + { + return _shouldUnsetParentConfigurationAndPlatform; + } + + set + { + _shouldUnsetParentConfigurationAndPlatform = value; + } + } + + private string _outputType; + + /// + /// The output type for the project + /// + public string OutputType + { + get + { + return _outputType; + } + + set + { + _outputType = value; + } + } + + private string _currentProjectPlatform; + + /// + /// True if we should use the default mappings to resolve the configuration/platform + /// of the passed in project references, false otherwise. + /// + public bool ResolveConfigurationPlatformUsingMappings + { + get + { + return _resolveConfigurationPlatformUsingMappings; + } + + set + { + _resolveConfigurationPlatformUsingMappings = value; + } + } + + private bool _resolveConfigurationPlatformUsingMappings; + + /// + /// The list of resolved reference paths (preserving the original project reference attributes) + /// + [Output] + public ITaskItem[] AssignedProjects + { + get + { + return _assignedProjects; + } + set + { + _assignedProjects = value; + } + } + + private ITaskItem[] _assignedProjects = null; + + /// + /// The list of project reference items that could not be resolved using the pre-resolved list of outputs. + /// Since VS only pre-resolves non-MSBuild projects, this means that project references in this list + /// are in the MSBuild format. + /// + [Output] + public ITaskItem[] UnassignedProjects + { + get + { + return _unassignedProjects; + } + set + { + _unassignedProjects = value; + } + } + + private ITaskItem[] _unassignedProjects = null; + + private const string attrFullConfiguration = "FullConfiguration"; + private const string buildReferenceMetadataName = "BuildReference"; + private const string referenceOutputAssemblyMetadataName = "ReferenceOutputAssembly"; + private const string buildProjectInSolutionAttribute = "BuildProjectInSolution"; + private const string attrConfiguration = "Configuration"; + private const string attrPlatform = "Platform"; + private const string attrSetConfiguration = "SetConfiguration"; + private const string attrSetPlatform = "SetPlatform"; + + private static readonly char[] s_configPlatformSeparator = new char[] { '|' }; + + private IDictionary _vcxToDefaultMap; + private IDictionary _defaultToVcxMap; + private bool _mappingsPopulated = false; + + #endregion + + #region ITask Members + + /// + /// Main task method + /// + /// + public override bool Execute() + { + try + { + if (!VerifyProjectReferenceItems(ProjectReferences, true /* treat problems as errors */)) + { + return false; + } + + ArrayList resolvedReferences = new ArrayList(ProjectReferences.GetLength(0)); + ArrayList unresolvedReferences = new ArrayList(ProjectReferences.GetLength(0)); + + if (!String.IsNullOrEmpty(SolutionConfigurationContents)) + { + CacheProjectElementsFromXml(SolutionConfigurationContents); + } + + if (AddSyntheticProjectReferencesForSolutionDependencies) + { + // The solution may have had project to project dependencies expressed in it, which were passed in with the blob. + // Add those to the list of project references as if they were regular project references. + AddSyntheticProjectReferences(CurrentProject); + } + + foreach (ITaskItem projectRef in ProjectReferences) + { + bool resolveSuccess = false; + ITaskItem resolvedReference; + + resolveSuccess = ResolveProject(projectRef, out resolvedReference); + + if (resolveSuccess) + { + resolvedReferences.Add(resolvedReference); + + Log.LogMessageFromResources(MessageImportance.Low, "AssignProjectConfiguration.ProjectConfigurationResolutionSuccess", projectRef.ItemSpec, resolvedReference.GetMetadata(attrFullConfiguration)); + } + else + { + // If the reference was unresolved, we want to undefine the Configuration and Platform + // global properties, so that the project will build using its default Configuration and + // Platform rather than that of its parent. + if (ShouldUnsetParentConfigurationAndPlatform) + { + string globalPropertiesToRemove = projectRef.GetMetadata("GlobalPropertiesToRemove"); + + if (!String.IsNullOrEmpty(globalPropertiesToRemove)) + { + globalPropertiesToRemove += ";"; + } + + if (projectRef is ITaskItem2) + { + ((ITaskItem2)projectRef).SetMetadataValueLiteral("GlobalPropertiesToRemove", globalPropertiesToRemove + "Configuration;Platform"); + } + else + { + projectRef.SetMetadata("GlobalPropertiesToRemove", EscapingUtilities.Escape(globalPropertiesToRemove + "Configuration;Platform")); + } + } + + unresolvedReferences.Add(projectRef); + + // This is not an error - we pass unresolved references to UnresolvedProjectReferences for further + // processing in the .targets file. This means this project was not checked for building in the + // active solution configuration. + Log.LogMessageFromResources(MessageImportance.Low, "AssignProjectConfiguration.ProjectConfigurationUnresolved", projectRef.ItemSpec); + } + } + + AssignedProjects = (ITaskItem[])resolvedReferences.ToArray(typeof(ITaskItem)); + UnassignedProjects = (ITaskItem[])unresolvedReferences.ToArray(typeof(ITaskItem)); + } + catch (XmlException e) + { + Log.LogErrorWithCodeFromResources("General.ErrorExecutingTask", this.GetType().Name, e.Message); + return false; + } + + return true; + } + + #endregion + + #region Methods + + + /// + /// Given a project reference task item and an XML document containing project configurations, + /// find the configuration for that task item. + /// + /// + /// resulting ITaskItem containing the resolved project item with the FullConfiguration, + /// Configuration and Platform attributes + /// + /// true if resolved successfully + internal bool ResolveProject(ITaskItem projectRef, out ITaskItem resolvedProjectWithConfiguration) + { + XmlElement projectConfigurationElement = null; + string projectConfiguration = null; + + if (!String.IsNullOrEmpty(SolutionConfigurationContents)) + { + projectConfigurationElement = GetProjectElement(projectRef); + + if (projectConfigurationElement != null) + { + projectConfiguration = projectConfigurationElement.InnerText; + } + } + + if (projectConfiguration == null && ResolveConfigurationPlatformUsingMappings) + { + if (!_mappingsPopulated) + { + SetupDefaultPlatformMappings(); + } + + string transformedPlatform = null; + + if (String.Equals(projectRef.GetMetadata("Extension"), ".vcxproj", StringComparison.OrdinalIgnoreCase)) + { + if (_defaultToVcxMap.TryGetValue(CurrentProjectPlatform, out transformedPlatform)) + { + projectConfiguration = CurrentProjectConfiguration + s_configPlatformSeparator[0] + transformedPlatform; + } + } + else + { + if (_vcxToDefaultMap.TryGetValue(CurrentProjectPlatform, out transformedPlatform)) + { + projectConfiguration = CurrentProjectConfiguration + s_configPlatformSeparator[0] + transformedPlatform; + } + } + } + + SetBuildInProjectAndReferenceOutputAssemblyMetadata(_onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration, projectRef, projectConfigurationElement); + + if (projectConfiguration != null) + { + if (!string.IsNullOrEmpty(projectConfiguration)) + { + resolvedProjectWithConfiguration = projectRef; + resolvedProjectWithConfiguration.SetMetadata(attrFullConfiguration, projectConfiguration); + + string[] configurationPlatformParts = projectConfiguration.Split(s_configPlatformSeparator); + resolvedProjectWithConfiguration.SetMetadata(attrSetConfiguration, "Configuration=" + configurationPlatformParts[0]); + resolvedProjectWithConfiguration.SetMetadata(attrConfiguration, configurationPlatformParts[0]); + + if (configurationPlatformParts.Length > 1) + { + resolvedProjectWithConfiguration.SetMetadata(attrSetPlatform, "Platform=" + configurationPlatformParts[1]); + resolvedProjectWithConfiguration.SetMetadata(attrPlatform, configurationPlatformParts[1]); + } + else + { + resolvedProjectWithConfiguration.SetMetadata(attrSetPlatform, "Platform="); + } + + return true; + } + } + + resolvedProjectWithConfiguration = null; + return false; + } + + /// + /// Given the project configuration blob and the project reference item, set the BuildInProject metadata and the ReferenceOutputAssembly metadata + /// based on the contents of the blob. + /// + internal static void SetBuildInProjectAndReferenceOutputAssemblyMetadata(bool onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration, ITaskItem resolvedProjectWithConfiguration, XmlElement projectConfigurationElement) + { + if (projectConfigurationElement != null && resolvedProjectWithConfiguration != null && onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration) + { + bool buildProject = false; + + // The value of the specified attribute. An empty string is returned if a matching attribute is not found or if the attribute does not have a specified or default value. + string buildProjectInSolution = projectConfigurationElement.GetAttribute(buildProjectInSolutionAttribute); + + // We could not parse out what was in the attribute, act as if it was not set in the first place. + if (bool.TryParse(buildProjectInSolution, out buildProject)) + { + // If we do not want to build references disabled in the solution configuration blob + // and the solution configuration indicates the build for this project is disabled + // We need to set the BuildReferenceMetadata to false and the ReferenceOutputAssembly to false (if they are not already set to anything) + if (!buildProject) + { + string buildReferenceMetadata = resolvedProjectWithConfiguration.GetMetadata(buildReferenceMetadataName); + string referenceOutputAssemblyMetadata = resolvedProjectWithConfiguration.GetMetadata(referenceOutputAssemblyMetadataName); + + if (buildReferenceMetadata.Length == 0) + { + resolvedProjectWithConfiguration.SetMetadata(buildReferenceMetadataName, "false"); + } + + if (referenceOutputAssemblyMetadata.Length == 0) + { + resolvedProjectWithConfiguration.SetMetadata(referenceOutputAssemblyMetadataName, "false"); + } + } + } + } + } + + /// + /// Given the contents of VcxToDefaultPlatformMapping and DefaultToVcxPlatformMapping properties, + /// fill out the maps that will be used to translate between the two. + /// + private void SetupDefaultPlatformMappings() + { + _vcxToDefaultMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + _defaultToVcxMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (!String.IsNullOrEmpty(VcxToDefaultPlatformMapping)) + { + PopulateMappingDictionary(_vcxToDefaultMap, VcxToDefaultPlatformMapping); + } + + if (!String.IsNullOrEmpty(DefaultToVcxPlatformMapping)) + { + PopulateMappingDictionary(_defaultToVcxMap, DefaultToVcxPlatformMapping); + } + + _mappingsPopulated = true; + } + + /// + /// Given a dictionary to populate and a string of the format "a=b;c=d", populate the + /// dictionary with the given pairs. + /// + private void PopulateMappingDictionary(IDictionary map, string mappingList) + { + string[] mappings = mappingList.Split(';'); + + foreach (string mapping in mappings) + { + string[] platforms = mapping.Split('='); + + if (platforms == null || platforms.Length != 2) + { + Log.LogErrorFromResources("AssignProjectConfiguration.IllegalMappingString", mapping.Trim(), mappingList); + } + else + { + map.Add(platforms[0], platforms[1]); + } + } + } + + #endregion + } +} diff --git a/src/XMakeTasks/AssignTargetPath.cs b/src/XMakeTasks/AssignTargetPath.cs new file mode 100644 index 00000000000..d3a6a2f9ac6 --- /dev/null +++ b/src/XMakeTasks/AssignTargetPath.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using System.Globalization; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Create a new list of items that have attributes if none was present in + /// the input. + /// + public class AssignTargetPath : TaskExtension + { + /// + /// Construct. + /// + public AssignTargetPath() + { + } + + #region Properties + + private string _rootFolder = null; + private ITaskItem[] _files = new ITaskItem[0]; + private ITaskItem[] _assignedFiles = null; + + /// + /// The folder to make the links relative to. + /// + [Required] + public string RootFolder + { + get { return _rootFolder; } + set { _rootFolder = value; } + } + + /// + /// The incoming list of files. + /// + public ITaskItem[] Files + { + get { return _files; } + set { _files = value; } + } + + /// + /// The resulting list of files. + /// + /// + [Output] + public ITaskItem[] AssignedFiles + { + get { return _assignedFiles; } + } + + #endregion + + /// + /// Execute the task. + /// + /// + public override bool Execute() + { + _assignedFiles = new TaskItem[Files.Length]; + + if (Files.Length > 0) + { + // Compose a file in the root folder. + // NOTE: at this point fullRootPath may or may not have a trailing + // slash because Path.GetFullPath() does not add or remove it + string fullRootPath = Path.GetFullPath(this.RootFolder); + + // Ensure trailing slash otherwise c:\bin appears to match part of c:\bin2\foo + fullRootPath = FileUtilities.EnsureTrailingSlash(fullRootPath); + + string currentDirectory = Directory.GetCurrentDirectory(); + + // check if the root folder is the same as the current directory + // NOTE: the path returned from Directory.GetCurrentDirectory() + // does not have a trailing slash, but fullRootPath does + bool isRootFolderSameAsCurrentDirectory = + ((fullRootPath.Length - 1 /* exclude trailing slash */) == currentDirectory.Length) + && + (String.Compare( + fullRootPath, 0, + currentDirectory, 0, + (fullRootPath.Length - 1) /* don't compare trailing slash */, + StringComparison.OrdinalIgnoreCase) == 0); + + for (int i = 0; i < Files.Length; ++i) + { + string link = Files[i].GetMetadata(ItemMetadataNames.link); + AssignedFiles[i] = new TaskItem(Files[i]); + + // If file has a link, use that. + string targetPath = link; + + if (link == null || link.Length == 0) + { + if (// if the file path is relative + !Path.IsPathRooted(Files[i].ItemSpec) && + // if the file path doesn't contain any relative specifiers + !Files[i].ItemSpec.Contains(".\\") && + // if the file path is already relative to the root folder + isRootFolderSameAsCurrentDirectory) + { + // then just use the file path as-is + // PERF NOTE: we do this to avoid calling Path.GetFullPath() below, + // because that method consumes a lot of memory, esp. when we have + // a lot of items coming through this task + targetPath = Files[i].ItemSpec; + } + else + { + // PERF WARNING: Path.GetFullPath() is expensive in terms of memory; + // we should avoid calling it whenever possible + string itemSpecFullFileNamePath = Path.GetFullPath(Files[i].ItemSpec); + + if (String.Compare(fullRootPath, 0, itemSpecFullFileNamePath, 0, fullRootPath.Length, true, CultureInfo.CurrentCulture) == 0) + { + // The item spec file is in the "cone" of the RootFolder. Return the relative path from the cone root. + targetPath = itemSpecFullFileNamePath.Substring(fullRootPath.Length); + } + else + { + // The item spec file is not in the "cone" of the RootFolder. Return the filename only. + targetPath = Path.GetFileName(Files[i].ItemSpec); + } + } + } + + AssignedFiles[i].SetMetadata(ItemMetadataNames.targetPath, EscapingUtilities.Escape(targetPath)); + } + } + + return true; + } + } +} diff --git a/src/XMakeTasks/AxImp.cs b/src/XMakeTasks/AxImp.cs new file mode 100644 index 00000000000..aa9dd751db0 --- /dev/null +++ b/src/XMakeTasks/AxImp.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// ToolTask that wraps AxImp.exe, which generates Windows forms wrappers for ActiveX controls. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Main class for the COM reference resolution task + /// + public sealed partial class ResolveComReference + { + /// + /// Defines the "AxImp" MSBuild task, which enables using AxImp.exe + /// to generate Windows Forms wrappers for ActiveX controls. + /// + internal class AxImp : AxTlbBaseTask + { + #region Properties + /* + Microsoft (R) .NET ActiveX Control to Windows Forms Assembly Generator + [Microsoft .Net Framework, Version 4.0.10719.0] + Copyright (c) Microsoft Corporation. All rights reserved. + + + Generates a Windows Forms Control that wraps ActiveX controls defined in the given OcxName. + + Usage: + AxImp OcxName [Options] + Options: + /out:FileName File name of assembly to be produced + /publickey:FileName File containing strong name public key + /keyfile:FileName File containing strong name key pair + /keycontainer:FileName Key container holding strong name key pair + /delaysign Force strong name delay signing + Used with /keyfile, /keycontainer or /publickey + /source Generate C# source code for Windows Forms wrapper + /rcw:FileName Assembly to use for Runtime Callable Wrapper rather than generating new one. + Multiple instances may be specified. Current directory is used for relative paths. + /nologo Prevents AxImp from displaying logo + /silent Prevents AxImp from displaying success message + /verbose Displays extra information + /? or /help Display this usage message + */ + + /// + /// .ocx File the ActiveX controls being wrapped are defined in. + /// + public string ActiveXControlName + { + get { return (string)Bag["ActiveXControlName"]; } + set { Bag["ActiveXControlName"] = value; } + } + + /// + /// If true, will generate C# source code for the Windows Forms wrapper. + /// + public bool GenerateSource + { + get { return GetBoolParameterWithDefault("GenerateSource", false); } + set { Bag["GenerateSource"] = value; } + } + + /// + /// If true, suppresses displaying the logo + /// + public bool NoLogo + { + get { return GetBoolParameterWithDefault("NoLogo", false); } + set { Bag["NoLogo"] = value; } + } + + /// + /// File name of assembly to be produced. + /// + public string OutputAssembly + { + get { return (string)Bag["OutputAssembly"]; } + set { Bag["OutputAssembly"] = value; } + } + + /// + /// Name of assembly to use as a RuntimeCallableWrapper instead of generating one. + /// + public string RuntimeCallableWrapperAssembly + { + get { return (string)Bag["RuntimeCallableWrapperAssembly"]; } + set { Bag["RuntimeCallableWrapperAssembly"] = value; } + } + + /// + /// If true, prevents AxImp from displaying success message. + /// + public bool Silent + { + get { return GetBoolParameterWithDefault("Silent", false); } + set { Bag["Silent"] = value; } + } + + /// + /// If true, AxImp prints more information. + /// + public bool Verbose + { + get { return GetBoolParameterWithDefault("Verbose", false); } + set { Bag["Verbose"] = value; } + } + + #endregion // Properties + + #region ToolTask Members + + /// + /// Returns the name of the tool to execute + /// + protected override string ToolName + { + get { return "AxImp.exe"; } + } + + /// + /// Fills the provided CommandLineBuilderExtension with all the command line options used when + /// executing this tool + /// + /// Gets filled with command line commands + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + // .ocx file being imported + commandLine.AppendFileNameIfNotNull(ActiveXControlName); + + // options + commandLine.AppendWhenTrue("/nologo", Bag, "NoLogo"); + commandLine.AppendSwitchIfNotNull("/out:", OutputAssembly); + commandLine.AppendSwitchIfNotNull("/rcw:", RuntimeCallableWrapperAssembly); + commandLine.AppendWhenTrue("/silent", Bag, "Silent"); + commandLine.AppendWhenTrue("/source", Bag, "GenerateSource"); + commandLine.AppendWhenTrue("/verbose", Bag, "Verbose"); + + base.AddCommandLineCommands(commandLine); + } + + /// + /// Validates the parameters passed to the task + /// + /// True if parameters are valid + protected override bool ValidateParameters() + { + // Verify that we were actually passed a .ocx to import + if (String.IsNullOrEmpty(ActiveXControlName)) + { + Log.LogErrorWithCodeFromResources("AxImp.NoInputFileSpecified"); + return false; + } + + return base.ValidateParameters(); + } + + #endregion // ToolTask Members + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/AxReference.cs b/src/XMakeTasks/AxReference.cs new file mode 100644 index 00000000000..987ee55bc7a --- /dev/null +++ b/src/XMakeTasks/AxReference.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: AxReference + * + * COM reference wrapper class for the ActiveX controls. + * + */ + internal class AxReference : AxTlbBaseReference + { + #region Constructors + /// + /// internal constructor + /// + /// task logger instance used for logging + /// callback interface for resolving dependent COM refs/NET assemblies + /// cached reference information (typelib pointer, original task item, typelib name etc.) + /// reference name (for better logging experience) + /// directory we should write the wrapper to + /// delay sign wrappers? + /// file containing public/private keys + /// container name for public/private keys + /// True if GenerateWrapper() should generate the wrapper out-of-proc using aximp.exe + /// Path to the SDK tools directory where aximp.exe can be found + /// BuildEngine of parent task; needed for logging purposes when generating wrapper out-of-proc + internal AxReference(TaskLoggingHelper taskLoggingHelper, bool silent, IComReferenceResolver resolverCallback, ComReferenceInfo referenceInfo, string itemName, string outputDirectory, + bool delaySign, string keyFile, string keyContainer, bool includeTypeLibVersionInName, string sdkToolsPath, IBuildEngine buildEngine, string[] environmentVariables) + : base(taskLoggingHelper, silent, resolverCallback, referenceInfo, itemName, outputDirectory, delaySign, keyFile, keyContainer, includeTypeLibVersionInName, true /* always execute as tool */, sdkToolsPath, buildEngine, environmentVariables) + { + // do nothing + } + + #endregion + + #region Methods + + /* + * Method: GetWrapperFileName + * + * Constructs the wrapper file name from a type library name. + */ + protected override string GetWrapperFileNameInternal(string typeLibName) + { + return GetWrapperFileName("AxInterop.", typeLibName, IncludeTypeLibVersionInName, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum); + } + + /* + * Method: GenerateWrapper + * + * Generates a wrapper for this reference. + */ + internal bool GenerateWrapper(out ComReferenceWrapperInfo wrapperInfo) + { + wrapperInfo = null; + + // The tool gets the public key for itself, but we get it here anyway to + // give nice messages in errors cases. + StrongNameKeyPair keyPair = null; + byte[] publicKey = null; + GetAndValidateStrongNameKey(out keyPair, out publicKey); + + bool generateWrapperSucceeded = true; + + string tlbName = ReferenceInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.tlbReferenceName); + + // Generate wrapper out-of-proc using aximp.exe from the target framework. MUST + // HAVE SET SDKTOOLSPATH TO THE TARGET SDK TO WORK + + var axImp = new ResolveComReference.AxImp(); + + axImp.ActiveXControlName = ReferenceInfo.strippedTypeLibPath; + + axImp.BuildEngine = BuildEngine; + axImp.ToolPath = ToolPath; + axImp.EnvironmentVariables = EnvironmentVariables; + axImp.DelaySign = DelaySign; + axImp.GenerateSource = false; + axImp.KeyContainer = KeyContainer; + axImp.KeyFile = KeyFile; + axImp.Silent = Silent; + if (ReferenceInfo != null && ReferenceInfo.primaryOfAxImpRef != null && ReferenceInfo.primaryOfAxImpRef.resolvedWrapper != null && ReferenceInfo.primaryOfAxImpRef.resolvedWrapper.path != null) + { + // This path should hit unless there was a prior resolution error or bug in the resolution code. + // The reason is that everything (tlbs and pias) gets resolved before AxImp references. + axImp.RuntimeCallableWrapperAssembly = ReferenceInfo.primaryOfAxImpRef.resolvedWrapper.path; + } + axImp.OutputAssembly = Path.Combine(OutputDirectory, GetWrapperFileName()); + + generateWrapperSucceeded = axImp.Execute(); + + string wrapperPath = GetWrapperPath(); + + // store the wrapper info... + wrapperInfo = new ComReferenceWrapperInfo(); + wrapperInfo.path = wrapperPath; + wrapperInfo.assembly = Assembly.UnsafeLoadFrom(wrapperInfo.path); + + // ...and we're done! + return generateWrapperSucceeded; + } + + #endregion + } +} diff --git a/src/XMakeTasks/AxTlbBaseReference.cs b/src/XMakeTasks/AxTlbBaseReference.cs new file mode 100644 index 00000000000..c768e4cebb5 --- /dev/null +++ b/src/XMakeTasks/AxTlbBaseReference.cs @@ -0,0 +1,396 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: AxTlbBaseReference + * + * Common abstract base for aximp and tlbimp COM reference wrapper classes. + * They share the resolution method and only differ in constructing the wrapper file name. + * + */ + internal abstract class AxTlbBaseReference : ComReference + { + #region Constructors + /// + /// internal constructor + /// + /// task logger instance used for logging + /// callback interface for resolving dependent COM refs/NET assemblies + /// cached reference information (typelib pointer, original task item, typelib name etc.) + /// reference name (for better logging experience) + /// directory we should write the wrapper to + /// delay sign wrappers? + /// file containing public/private keys + /// container name for public/private keys + /// True if GenerateWrapper() should generate the wrapper out-of-proc using aximp.exe or tlbimp.exe + /// Path to the SDK tools directory where aximp.exe or tlbimp.exe can be found + /// BuildEngine of parent task; needed for logging purposes when generating wrapper out-of-proc + internal AxTlbBaseReference(TaskLoggingHelper taskLoggingHelper, bool silent, IComReferenceResolver resolverCallback, ComReferenceInfo referenceInfo, string itemName, string outputDirectory, bool delaySign, string keyFile, string keyContainer, bool includeTypeLibVersionInName, bool executeAsTool, string toolPath, IBuildEngine buildEngine, string[] environmentVariables) + : base(taskLoggingHelper, silent, referenceInfo, itemName) + { + _resolverCallback = resolverCallback; + _outputDirectory = outputDirectory; + _includeTypeLibVersionInName = includeTypeLibVersionInName; + + BuildEngine = buildEngine; + EnvironmentVariables = environmentVariables; + DelaySign = delaySign; + ExecuteAsTool = executeAsTool; + KeyFile = keyFile; + KeyContainer = keyContainer; + ToolPath = toolPath; + } + + #endregion + + #region Properties + + /// + /// directory we should write the wrapper to + /// + protected virtual string OutputDirectory + { + get + { + return _outputDirectory; + } + } + + private string _outputDirectory; + + /// + /// callback interface for resolving dependent COM refs/NET assemblies + /// + protected IComReferenceResolver ResolverCallback + { + get + { + return _resolverCallback; + } + } + + private IComReferenceResolver _resolverCallback; + + /// + /// container name for public/private keys + /// + protected string KeyContainer { get; set; } + + /// + /// file containing public/private keys + /// + protected string KeyFile { get; set; } + + /// + /// True if generated wrappers should be delay signed + /// + protected bool DelaySign { get; set; } + + /// + /// Property to allow multitargeting of ResolveComReferences: If true, tlbimp.exe and + /// aximp.exe from the appropriate target framework will be run out-of-proc to generate + /// the necessary wrapper assemblies. + /// + protected bool ExecuteAsTool { get; set; } + + /// + /// The BuildEngine of the ResolveComReference instance that created this instance + /// of the class: necessary for passing to the AxImp or TlbImp task that is spawned + /// when ExecuteAsTool is set to true + /// + protected IBuildEngine BuildEngine { get; set; } + + /// + /// Environment variables to pass to the tool. + /// + protected string[] EnvironmentVariables { get; set; } + + /// + /// If ExecuteAsTool is true, this must be set to the SDK + /// tools path for the framework version being targeted. + /// + protected string ToolPath { get; set; } + + /// + /// When true, we include the typelib version number in the name. + /// + protected bool IncludeTypeLibVersionInName + { + get + { + return _includeTypeLibVersionInName; + } + + set + { + _includeTypeLibVersionInName = value; + } + } + + private bool _includeTypeLibVersionInName; + + #endregion + + #region Methods + + /* + * Method: FindExistingWrapper + * + * Checks if there's a preexisting wrapper for this reference. + */ + internal override bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp) + { + wrapperInfo = null; + + string wrapperPath = GetWrapperPath(); + + // now see if the wrapper assembly actually exists + if (!File.Exists(wrapperPath)) + { + return false; + } + + wrapperInfo = new ComReferenceWrapperInfo(); + wrapperInfo.path = wrapperPath; + + return IsWrapperUpToDate(wrapperInfo, componentTimestamp); + } + + /* + * Method: IsWrapperUpToDate + * + * Checks if the existing wrapper is up to date. + */ + protected virtual bool IsWrapperUpToDate(ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp) + { + Debug.Assert(ReferenceInfo.strippedTypeLibPath != null && ReferenceInfo.strippedTypeLibPath.Length > 0, "ReferenceInfo.path should be valid if we got here"); + if (ReferenceInfo.strippedTypeLibPath == null || ReferenceInfo.strippedTypeLibPath.Length == 0) + throw new ComReferenceResolutionException(); + + // if wrapper doesn't exist, wrapper is obviously not up to date + if (!File.Exists(wrapperInfo.path)) + return false; + + // if typelib file has a DIFFERENT last write time, wrapper is not up to date + // the reason we're comparing write times in an unusual way is that type libraries are unusual + // "source files" for wrappers. If you upgrade/downgrade a system component, its write + // time may be earlier than before but we should still regenerate the wrapper. + if (DateTime.Compare(File.GetLastWriteTime(ReferenceInfo.strippedTypeLibPath), componentTimestamp) != 0) + return false; + + // Compare our the existing wrapper's strong name state to the one we are requesting. + if (!SigningRequirementsMatchExistingWrapper(wrapperInfo)) + { + return false; + } + + // ok, everything's looking fine, now just verify the assembly file is valid + try + { + wrapperInfo.assembly = Assembly.UnsafeLoadFrom(wrapperInfo.path); + } + catch (BadImageFormatException) + { + // ouch, this assembly is malformed... need to regenerate the wrapper. + wrapperInfo.assembly = null; + } + + return (wrapperInfo.assembly != null); + } + + /* + * Method: GetWrapperPath + * + * Constructs the wrapper file path. + */ + internal string GetWrapperPath() + { + // combine with the specified output directory + return Path.Combine(OutputDirectory, GetWrapperFileName()); + } + + /* + * Method: GetWrapperFileName + * + * Helper method for constructing wrapper file name. + */ + internal string GetWrapperFileName() + { + return GetWrapperFileNameInternal(ReferenceInfo.typeLibName); + } + + /* + * Method: GetWrapperFileName + * + * Constructs the wrapper file name from a type library name. Specialized wrappers must override it if + * they want to use the Resolve method from this class. + */ + protected abstract string GetWrapperFileNameInternal(string typeLibName); + + /// + /// Static version of GetWrapperFileName for use when calling from the outside. + /// This version need only be used if the interop DLL needs to include the typelib version in the name + /// Default implementation + /// + /// XXX, when the interop DLL is of the form XXX.typeLibName.[Y.Z.]dll + /// The typelib to generate the wrapper name for + /// True if the interop name should include the typelib's version + /// Major version number to append to the interop DLL's name + /// Minor version number to append to the interop DLL's name + internal static string GetWrapperFileName(string interopDllHeader, string typeLibName, bool includeTypeLibVersionInName, short majorVerNum, short minorVerNum) + { + // create wrapper name of the format XXX.YYY[.Z.W].dll, where + // XXX = the header ("Interop." or the like) + // YYY = typeLibName + // Z.W = optional TLB version number + StringBuilder builder = new StringBuilder(interopDllHeader); + builder.Append(typeLibName); + + if (includeTypeLibVersionInName) + { + builder.Append('.'); + builder.Append(majorVerNum); + builder.Append('.'); + builder.Append(minorVerNum); + } + + builder.Append(".dll"); + + return builder.ToString(); + } + + /// + /// Given our KeyFile, KeyContainer, and DelaySign parameters, generate the public / private + /// key pair and validate that it exists to the extent needed. + /// + internal void GetAndValidateStrongNameKey(out StrongNameKeyPair keyPair, out byte[] publicKey) + { + keyPair = null; + publicKey = null; + + // get key pair/public key + StrongNameUtils.GetStrongNameKey(Log, KeyFile, KeyContainer, out keyPair, out publicKey); + + // make sure we give as much data to the typelib converter as necessary but not more, or we might end up + // with something we didn't want + if (DelaySign) + { + keyPair = null; + + if (publicKey == null) + { + Log.LogErrorWithCodeFromResources(null, ReferenceInfo.SourceItemSpec, 0, 0, 0, 0, "StrongNameUtils.NoPublicKeySpecified"); + throw new StrongNameException(); + } + } + else + { + publicKey = null; + + // If the user did not specify delay sign and we didn't get a public/private + // key pair then we have an error since a public key by itself is not enough + // to fully sign the assembly. (only if either KeyContainer or KeyFile was specified though) + if (keyPair == null) + { + if (KeyContainer != null && KeyContainer.Length > 0) + { + Log.LogErrorWithCodeFromResources(null, ReferenceInfo.SourceItemSpec, 0, 0, 0, 0, "ResolveComReference.StrongNameUtils.NoKeyPairInContainer", KeyContainer); + throw new StrongNameException(); + } + else if (KeyFile != null && KeyFile.Length > 0) + { + Log.LogErrorWithCodeFromResources(null, ReferenceInfo.SourceItemSpec, 0, 0, 0, 0, "ResolveComReference.StrongNameUtils.NoKeyPairInFile", KeyFile); + throw new StrongNameException(); + } + } + } + } + + /// + /// Compare the strong name signing state of the existing wrapper to the signing + /// state we are requesting in this run of the task. Return true if they match (e.g. + /// from a signing perspective, the wrapper is up-to-date) or false otherwise. + /// + private bool SigningRequirementsMatchExistingWrapper(ComReferenceWrapperInfo wrapperInfo) + { + StrongNameLevel desiredStrongNameLevel = StrongNameLevel.None; + + if ((KeyFile != null && KeyFile.Length > 0) || (KeyContainer != null && KeyContainer.Length > 0)) + { + if (DelaySign) + { + desiredStrongNameLevel = StrongNameLevel.DelaySigned; + } + else + { + desiredStrongNameLevel = StrongNameLevel.FullySigned; + } + } + + // ...and see what we have already + StrongNameLevel currentStrongNameLevel = StrongNameUtils.GetAssemblyStrongNameLevel(wrapperInfo.path); + + // if not matching, need to regenerate wrapper + if (desiredStrongNameLevel != currentStrongNameLevel) + { + return false; + } + + // if the wrapper needs a strong name, see if the public keys match + if (desiredStrongNameLevel == StrongNameLevel.DelaySigned || + desiredStrongNameLevel == StrongNameLevel.FullySigned) + { + StrongNameKeyPair snkPair; + byte[] desiredPublicKey = null; + + // get desired public key + StrongNameUtils.GetStrongNameKey(Log, KeyFile, KeyContainer, out snkPair, out desiredPublicKey); + + // get current public key + AssemblyName assemblyName = AssemblyName.GetAssemblyName(wrapperInfo.path); + + if (assemblyName == null) + { + return false; + } + + byte[] currentPublicKey = assemblyName.GetPublicKey(); + + if (currentPublicKey.Length != desiredPublicKey.Length) + { + return false; + } + + // compare public keys byte by byte + for (int i = 0; i < currentPublicKey.Length; i++) + { + if (currentPublicKey[i] != desiredPublicKey[i]) + { + return false; + } + } + } + + return true; + } + + #endregion + } +} diff --git a/src/XMakeTasks/AxTlbBaseTask.cs b/src/XMakeTasks/AxTlbBaseTask.cs new file mode 100644 index 00000000000..fb745b0b9eb --- /dev/null +++ b/src/XMakeTasks/AxTlbBaseTask.cs @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// ToolTask that contains shared functionality between the AxImp and TlbImp tasks. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// ToolTask that contains shared functionality between the AxImp and TlbImp tasks. + /// + internal abstract class AxTlbBaseTask : ToolTaskExtension + { + #region Private Data + + /// + /// True if the keyfile only contains the public key data, and thus + /// we should pass the file using the /publickey: parameter instead of + /// /keyfile. + /// + private bool _delaySigningAndKeyFileOnlyContainsPublicKey = false; + + #endregion + + #region Properties + /// + /// Force strong name delay signing. Used with KeyFile or KeyContainer. + /// + public bool DelaySign + { + get { return GetBoolParameterWithDefault("DelaySign", false); } + set { Bag["DelaySign"] = value; } + } + + /// + /// Key container containing strong name key pair. + /// + public string KeyContainer + { + get { return (string)Bag["KeyContainer"]; } + set { Bag["KeyContainer"] = value; } + } + + /// + /// File containing strong name key pair. + /// + public string KeyFile + { + get { return (string)Bag["KeyFile"]; } + set { Bag["KeyFile"] = value; } + } + + /// + /// Path to the SDK directory where AxImp.exe and TlbImp.exe can be found + /// + public string SdkToolsPath + { + get { return (string)Bag["SdkToolsPath"]; } + set { Bag["SdkToolsPath"] = value; } + } + + #endregion // Properties + + #region ToolTask Members + + /// + /// Returns the name of the tool to execute. AxTlbBaseTask is not + /// executable, so return null for the ToolName -- And make sure that + /// Execute() logs an error! + /// + protected override string ToolName + { + get + { + return null; + } + } + + /// + /// Invokes the ToolTask with the given parameters + /// + /// True if the task succeeded, false otherwise + public override bool Execute() + { + // This is not a callable task on its own -- so need to make sure that + // only descendant tasks who have defined their ToolName can be executed + if (String.IsNullOrEmpty(ToolName)) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.ToolNameMustBeSet"); + return false; + } + + return base.Execute(); + } + + /// + /// Adds commands for the tool being executed, that cannot be put in a response file. + /// + /// The CommandLineBuilderExtension to add the commands to + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + AddStrongNameOptions(commandLine); + base.AddCommandLineCommands(commandLine); + } + + /// + /// Generates the full path to the tool being executed by this ToolTask + /// + /// A string containing the full path of this tool, or null if the tool was not found + protected override string GenerateFullPathToTool() + { + string pathToTool = null; + + pathToTool = SdkToolsPathUtility.GeneratePathToTool + ( + SdkToolsPathUtility.FileInfoExists, + Microsoft.Build.Utilities.ProcessorArchitecture.CurrentProcessArchitecture, + SdkToolsPath, + ToolName, + Log, + true + ); + + return pathToTool; + } + + /// + /// Validates the parameters passed to the task + /// + /// True if parameters are valid + protected override bool ValidateParameters() + { + // Verify that a path for the tool exists -- if the tool doesn't exist in it + // we'll worry about that later + if ((String.IsNullOrEmpty(ToolPath) || !Directory.Exists(ToolPath)) && + (String.IsNullOrEmpty(SdkToolsPath) || !Directory.Exists(SdkToolsPath))) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", SdkToolsPath ?? "", ToolPath ?? ""); + return false; + } + + if (ValidateStrongNameParameters()) + { + // Allow the base class to do any validation it thinks necessary -- as far + // as we're concerned, parameters check out properly + return base.ValidateParameters(); + } + else + { + return false; + } + } + + /// + /// Adds options involving strong name signing -- syntax is the same between + /// AxImp and TlbImp + /// + /// The command line to add options to + private void AddStrongNameOptions(CommandLineBuilderExtension commandLine) + { + commandLine.AppendWhenTrue("/delaysign", Bag, "DelaySign"); + + // If we're delay-signing, we only need the public key, but if we use the /publickey + // switch, it will consume the entire key file, assume that's just the public key, and + // throw an error. + // + // So use /publickey if that's all our KeyFile contains, but KeyFile otherwise. + if (_delaySigningAndKeyFileOnlyContainsPublicKey) + { + commandLine.AppendSwitchIfNotNull("/publickey:", KeyFile); + } + else + { + commandLine.AppendSwitchIfNotNull("/keyfile:", KeyFile); + } + + commandLine.AppendSwitchIfNotNull("/keycontainer:", KeyContainer); + } + + /// + /// Validates the parameters passed to the task that involve strong name signing -- + /// DelaySign, KeyContainer, and KeyFile + /// + /// true if the parameters are valid, false otherwise. + private bool ValidateStrongNameParameters() + { + bool keyFileExists = false; + bool keyContainerSpecified = false; + + // Make sure that if KeyFile is defined, it's a real file. + if (!String.IsNullOrEmpty(KeyFile)) + { + if (File.Exists(KeyFile)) + { + keyFileExists = true; + } + else + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.InvalidKeyFileSpecified", KeyFile); + return false; + } + } + + // Check if KeyContainer name is specified + keyContainerSpecified = !String.IsNullOrEmpty(KeyContainer); + + // Cannot define both KeyFile and KeyContainer + if (keyFileExists && keyContainerSpecified) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.CannotSpecifyBothKeyFileAndKeyContainer"); + return false; + } + + // If this assembly is delay signed, either KeyFile or KeyContainer must be defined + if (DelaySign && !keyFileExists && !keyContainerSpecified) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.CannotSpecifyDelaySignWithoutEitherKeyFileOrKeyContainer"); + return false; + } + + // If KeyFile or KeyContainer is specified, verify that a key pair exists (or if delay-signed, + // even just a public key) + if (keyFileExists || keyContainerSpecified) + { + StrongNameKeyPair keyPair = null; + byte[] publicKey = null; + + try + { + StrongNameUtils.GetStrongNameKey(Log, KeyFile, KeyContainer, out keyPair, out publicKey); + } + catch (StrongNameException e) + { + Log.LogErrorFromException(e); + keyPair = null; + + // don't return here -- let the appropriate error below get logged also. + } + + if (DelaySign) + { + if (publicKey == null) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.StrongNameUtils.NoPublicKeySpecified"); + return false; + } + else if (keyPair == null) + { + // record this so we know which switch to pass to the task + _delaySigningAndKeyFileOnlyContainsPublicKey = true; + } + } + else + { + if (keyPair == null) + { + if (!String.IsNullOrEmpty(KeyContainer)) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.StrongNameUtils.NoKeyPairInContainer", KeyContainer); + return false; + } + else if (!String.IsNullOrEmpty(KeyFile)) + { + Log.LogErrorWithCodeFromResources("AxTlbBaseTask.StrongNameUtils.NoKeyPairInFile", KeyFile); + return false; + } + } + } + } + + return true; + } + + #endregion // ToolTask Members + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/BootstrapperBuilder.cs b/src/XMakeTasks/BootstrapperUtil/BootstrapperBuilder.cs new file mode 100644 index 00000000000..6bba88602c0 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/BootstrapperBuilder.cs @@ -0,0 +1,2220 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Shared; +using Microsoft.Win32; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Security.Policy; +using System.Text; +using System.Web; +using System.Xml; +using System.Xml.XPath; +using System.Xml.Xsl; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// This class is the top-level object for the bootstrapper system. + /// + [ComVisibleAttribute(true), GuidAttribute("1D9FE38A-0226-4b95-9C6B-6DFFA2236270"), ClassInterface(ClassInterfaceType.None)] + public class BootstrapperBuilder : IBootstrapperBuilder + { + private static readonly bool s_logging = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSPLOG")); + private static readonly string s_logPath = GetLogPath(); + + private string _path; + private XmlDocument _document; + + private XmlNamespaceManager _xmlNamespaceManager; + private ProductCollection _products = new ProductCollection(); + private Hashtable _cultures = new Hashtable(); + private Hashtable _validationResults = new Hashtable(); + private BuildResults _results; + private BuildResults _loopDependenciesWarnings; + private bool _fValidate = true; + private bool _fInitialized = false; + + private const string SETUP_EXE = "setup.exe"; + private const string SETUP_BIN = "setup.bin"; + private const string SETUP_RESOURCES_FILE = "setup.xml"; + + private const string ENGINE_PATH = "Engine"; // relative to bootstrapper path + private const string SCHEMA_PATH = "Schemas"; // relative to bootstrapper path + private const string PACKAGE_PATH = "Packages"; // relative to bootstrapper path + private const string RESOURCES_PATH = ""; + + private const string BOOTSTRAPPER_NAMESPACE = "http://schemas.microsoft.com/developer/2004/01/bootstrapper"; + + private const string BOOTSTRAPPER_PREFIX = "bootstrapper"; + + private const string ROOT_MANIFEST_FILE = "product.xml"; + private const string CHILD_MANIFEST_FILE = "package.xml"; + private const string MANIFEST_FILE_SCHEMA = "package.xsd"; + private const string CONFIG_TRANSFORM = "xmltoconfig.xsl"; + + private const string EULA_ATTRIBUTE = "LicenseAgreement"; + private const string HOMESITE_ATTRIBUTE = "HomeSite"; + private const string PUBLICKEY_ATTRIBUTE = "PublicKey"; + private const string URLNAME_ATTRIBUTE = "UrlName"; + private const string HASH_ATTRIBUTE = "Hash"; + + private const int MESSAGE_TABLE = 43; + private const int RESOURCE_TABLE = 45; + + /// + /// Creates a new BootstrapperBuilder. + /// + public BootstrapperBuilder() + { + _path = Util.DefaultPath; + } + + /// + /// Creates a new BootstrapperBuilder. + /// + /// The version of Visual Studio that is used to build this bootstrapper. + public BootstrapperBuilder(string visualStudioVersion) + { + _path = Util.GetDefaultPath(visualStudioVersion); + } + + #region IBootstrapperBuilder Members + + /// + /// Specifies the location of the required bootstrapper files. + /// + /// Path to bootstrapper files. + public string Path + { + get { return _path; } + set + { + if (!_fInitialized || string.Compare(_path, value, StringComparison.OrdinalIgnoreCase) != 0) + { + _path = value; + Refresh(); + } + } + } + + /// + /// Returns all products available at the current bootstrapper Path + /// + public ProductCollection Products + { + get + { + if (!_fInitialized) + Refresh(); + + return _products; + } + } + + /// + /// Generates a bootstrapper based on the specified settings. + /// + /// The properties used to build this bootstrapper. + /// The results of the bootstrapper generation + public BuildResults Build(BuildSettings settings) + { + _results = new BuildResults(); + try + { + if (settings.ApplicationFile == null && (settings.ProductBuilders == null || settings.ProductBuilders.Count == 0)) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.InvalidInput")); + return _results; + } + + if (String.IsNullOrEmpty(settings.OutputPath)) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.NoOutputPath")); + return _results; + } + + if (!_fInitialized) + Refresh(); + + if (String.IsNullOrEmpty(settings.Culture)) + settings.Culture = MapLCIDToCultureName(settings.LCID); + if (String.IsNullOrEmpty(settings.FallbackCulture)) + settings.FallbackCulture = MapLCIDToCultureName(settings.FallbackLCID); + + if (String.IsNullOrEmpty(settings.Culture) || settings.Culture == "*") + { + settings.Culture = settings.FallbackCulture; + } + + AddBuiltProducts(settings); + + ArrayList componentFilesCopied = new ArrayList(); + + // Copy setup.bin to the output directory + string strOutputExe = System.IO.Path.Combine(settings.OutputPath, SETUP_EXE); + if (!CopySetupToOutputDirectory(settings, strOutputExe)) + { + // Appropriate messages should have been stuffed into the results already + return _results; + } + + ResourceUpdater resourceUpdater = new ResourceUpdater(); + + // Build up the String table for setup.exe + if (!BuildResources(settings, resourceUpdater)) + { + // Appropriate messages should have been stuffed into the results already + return _results; + } + + AddStringResourceForUrl(resourceUpdater, "BASEURL", settings.ApplicationUrl, "ApplicationUrl"); + AddStringResourceForUrl(resourceUpdater, "COMPONENTSURL", settings.ComponentsUrl, "ComponentsUrl"); + AddStringResourceForUrl(resourceUpdater, "SUPPORTURL", settings.SupportUrl, "SupportUrl"); + if (settings.ComponentsLocation == ComponentsLocation.HomeSite) + { + resourceUpdater.AddStringResource(40, "HOMESITE", true.ToString()); + } + + XmlElement configElement = _document.CreateElement("Configuration"); + XmlElement applicationElement = CreateApplicationElement(configElement, settings); + if (applicationElement != null) + { + configElement.AppendChild(applicationElement); + } + + // Key: File hash, Value: A DictionaryEntry whose Key is "EULAx" and value is a + // fully qualified path to a eula. It can be any eula that matches the hash. + Hashtable eulas = new Hashtable(); + + // Copy package files, add each Package config info to the config file + if (!BuildPackages(settings, configElement, resourceUpdater, componentFilesCopied, eulas)) + return _results; + + // Transform the configuration xml into something the bootstrapper will understand + DumpXmlToFile(configElement, "bootstrapper.cfg.xml"); + string config = XmlToConfigurationFile(configElement); + resourceUpdater.AddStringResource(41, "SETUPCFG", config); + DumpStringToFile(config, "bootstrapper.cfg", false); + + // Put eulas in the resource stream + foreach (object obj in eulas.Values) + { + DictionaryEntry de = (DictionaryEntry)obj; + string data; + FileInfo fi = new System.IO.FileInfo(de.Value.ToString()); + using (FileStream fs = fi.OpenRead()) + { + data = new StreamReader(fs).ReadToEnd(); + } + + resourceUpdater.AddStringResource(44, de.Key.ToString(), data); + } + + resourceUpdater.AddStringResource(44, "COUNT", eulas.Count.ToString(CultureInfo.InvariantCulture)); + if (!resourceUpdater.UpdateResources(strOutputExe, _results)) + { + return _results; + } + + _results.SetKeyFile(strOutputExe); + string[] componentFiles = new string[componentFilesCopied.Count]; + componentFilesCopied.CopyTo(componentFiles); + _results.AddComponentFiles(componentFiles); + _results.BuildSucceeded(); + } + catch (Exception ex) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", ex.Message)); + } + return _results; + } + + private void Merge(Dictionary output, Dictionary input) + { + foreach (Product product in input.Values) + { + AddProduct(output, product); + } + } + + private void AddProduct(Dictionary output, Product product) + { + if (!output.ContainsKey(product.ProductCode.ToLowerInvariant())) + output.Add(product.ProductCode.ToLowerInvariant(), product); + } + + private void AddBuiltProducts(BuildSettings settings) + { + Dictionary builtProducts = new Dictionary(); + Dictionary productsAndIncludes = new Dictionary(); + + if (_loopDependenciesWarnings != null && _loopDependenciesWarnings.Messages != null) + { + foreach (BuildMessage message in _loopDependenciesWarnings.Messages) + { + _results.AddMessage(message); + } + } + + foreach (ProductBuilder builder in settings.ProductBuilders) + { + builtProducts.Add(builder.Product.ProductCode.ToLowerInvariant(), builder); + Merge(productsAndIncludes, GetIncludedProducts(builder.Product)); + AddProduct(productsAndIncludes, builder.Product); + } + + foreach (ProductBuilder builder in settings.ProductBuilders) + { + Dictionary includes = GetIncludedProducts(builder.Product); + foreach (Product p in includes.Values) + { + if (builtProducts.ContainsKey(p.ProductCode.ToLowerInvariant())) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.IncludedProductIncluded", builder.Name, p.Name)); + } + } + + foreach (List productDependency in builder.Product.Dependencies) + { + bool foundDependency = false; + foreach (Product p in productDependency) + { + if (productsAndIncludes.ContainsKey(p.ProductCode.ToLowerInvariant())) + { + foundDependency = true; + break; + } + } + + if (!foundDependency) + { + if (productDependency.Count == 1) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.MissingDependency", productDependency[0].Name, builder.Name)); + } + else + { + StringBuilder missingProductCodes = new StringBuilder(); + foreach (Product product in productDependency) + { + missingProductCodes.Append(product.Name); + missingProductCodes.Append(", "); + } + + string productCodes = missingProductCodes.ToString(); + productCodes = productCodes.Substring(0, productCodes.Length - 2); + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.MissingDependencyMultiple", productCodes, builder.Name)); + } + } + } + + foreach (ArrayList missingDependecies in builder.Product.MissingDependencies) + { + if (missingDependecies.Count == 1) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.DependencyNotFound", builder.Name, missingDependecies[0])); + } + else + { + StringBuilder missingProductCodes = new StringBuilder(); + foreach (string productCode in missingDependecies) + { + missingProductCodes.Append(productCode); + missingProductCodes.Append(", "); + } + + string productCodes = missingProductCodes.ToString(); + productCodes = productCodes.Substring(0, productCodes.Length - 2); + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.MultipleDependeciesNotFound", builder.Name, productCodes)); + } + } + } + } + + private bool CopySetupToOutputDirectory(BuildSettings settings, string strOutputExe) + { + string bootstrapperPath = BootstrapperPath; + string setupSourceFile = System.IO.Path.Combine(bootstrapperPath, SETUP_BIN); + + if (!File.Exists(setupSourceFile)) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.MissingSetupBin", SETUP_BIN, bootstrapperPath)); + return false; + } + + try + { + EnsureFolderExists(settings.OutputPath); + File.Copy(setupSourceFile, strOutputExe, true); + ClearReadOnlyAttribute(strOutputExe); + } + catch (IOException ex) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyError", setupSourceFile, strOutputExe, ex.Message)); + return false; + } + catch (UnauthorizedAccessException ex) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyError", setupSourceFile, strOutputExe, ex.Message)); + return false; + } + catch (ArgumentException ex) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyError", setupSourceFile, strOutputExe, ex.Message)); + return false; + } + catch (NotSupportedException ex) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyError", setupSourceFile, strOutputExe, ex.Message)); + return false; + } + + return true; + } + + private void AddStringResourceForUrl(ResourceUpdater resourceUpdater, string name, string url, string nameToUseInLog) + { + if (!String.IsNullOrEmpty(url)) + { + resourceUpdater.AddStringResource(40, name, url); + if (!Util.IsWebUrl(url) && !Util.IsUncPath(url)) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.InvalidUrl", nameToUseInLog, url)); + } + } + } + + #endregion + + /// + /// Returns the directories bootstrapper component files would be copied to when built given the specified settings + /// + /// The productCodes of the selected components + /// The culture used to build the bootstrapper + /// The fallback culture used to build the bootstrapper + /// How the bootstrapper would package the selected components + /// + public string[] GetOutputFolders(string[] productCodes, string culture, string fallbackCulture, ComponentsLocation componentsLocation) + { + if (!_fInitialized) + { + Refresh(); + } + + Hashtable folders = new Hashtable(); + BuildSettings settings = new BuildSettings(); + string invariantPath = PackagePath.ToLowerInvariant(); + invariantPath = Util.AddTrailingChar(invariantPath, System.IO.Path.DirectorySeparatorChar); + settings.CopyComponents = false; + settings.Culture = culture; + settings.FallbackCulture = fallbackCulture; + settings.ComponentsLocation = componentsLocation; + if (String.IsNullOrEmpty(settings.Culture) || settings.Culture == "*") + { + settings.Culture = settings.FallbackCulture; + } + + foreach (string productCode in productCodes) + { + Product product = Products.Product(productCode); + if (product == null) + continue; + settings.ProductBuilders.Add(product.ProductBuilder); + } + ArrayList files = new ArrayList(); + + BuildPackages(settings, null, null, files, null); + + foreach (string file in files) + { + string folder = System.IO.Path.GetDirectoryName(file); + if (folder.Substring(0, invariantPath.Length).ToLowerInvariant().CompareTo(invariantPath) == 0) + { + string relPath = folder.Substring(invariantPath.Length).ToLowerInvariant(); + if (!folders.Contains(relPath)) + folders.Add(relPath, relPath); + } + } + + ArrayList list = new ArrayList(folders.Values); + string[] a = new string[list.Count]; + list.CopyTo(a, 0); + return a; + } + + internal bool ContainsCulture(string culture) + { + if (!_fInitialized) + Refresh(); + return _cultures.Contains(culture); + } + + internal string[] Cultures + { + get + { + if (!_fInitialized) + Refresh(); + + ArrayList list = new ArrayList(_cultures.Values); + list.Sort(); + string[] a = new string[list.Count]; + list.CopyTo(a, 0); + return a; + } + } + + internal bool Validate + { + get { return _fValidate; } + set { _fValidate = value; } + } + + private string BootstrapperPath + { + get { return System.IO.Path.Combine(Path, ENGINE_PATH); } + } + + private string PackagePath + { + get { return System.IO.Path.Combine(Path, PACKAGE_PATH); } + } + + private string SchemaPath + { + get { return System.IO.Path.Combine(Path, SCHEMA_PATH); } + } + + private void Refresh() + { + RefreshResources(); + RefreshProducts(); + _fInitialized = true; + + if (s_logging) + { + StringBuilder productsOrder = new StringBuilder(); + foreach (Product p in Products) + { + productsOrder.Append(p.ProductCode + Environment.NewLine); + } + DumpStringToFile(productsOrder.ToString(), "BootstrapperInstallOrder.txt", false); + } + } + + private void RefreshResources() + { + string startDirectory = System.IO.Path.Combine(BootstrapperPath, RESOURCES_PATH); + _cultures.Clear(); + + if (Directory.Exists(startDirectory)) + { + foreach (string subDirectory in Directory.GetDirectories(startDirectory)) + { + string resourceDirectory = System.IO.Path.Combine(startDirectory, subDirectory); + string resourceFile = System.IO.Path.Combine(resourceDirectory, SETUP_RESOURCES_FILE); + if (File.Exists(resourceFile)) + { + XmlDocument resourceDoc = new XmlDocument(); + try + { + XmlReaderSettings xrs = new XmlReaderSettings(); + xrs.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(resourceFile, xrs)) + { + resourceDoc.Load(xr); + } + } + catch (XmlException ex) + { + // UNDONE: Log exception due to bad resource file + Debug.Fail(ex.Message); + continue; + } + + XmlNode rootNode = resourceDoc.SelectSingleNode("Resources"); + if (rootNode != null) + { + XmlAttribute cultureAttribute = (XmlAttribute)rootNode.Attributes.GetNamedItem("Culture"); + if (cultureAttribute != null) + { + XmlNode stringsNode = rootNode.SelectSingleNode("Strings"); + if (stringsNode != null) + { + XmlNode stringNode = stringsNode.SelectSingleNode(string.Format(CultureInfo.InvariantCulture, "String[@Name='{0}']", cultureAttribute.Value)); + if (stringNode != null) + { + string culture = stringNode.InnerText; + + XmlNode resourcesNode = rootNode.OwnerDocument.ImportNode(rootNode, true); + resourcesNode.Attributes.RemoveNamedItem("Culture"); + XmlAttribute newAttribute = (XmlAttribute)rootNode.OwnerDocument.ImportNode(cultureAttribute, false); + newAttribute.Value = stringNode.InnerText; + resourcesNode.Attributes.Append(newAttribute); + if (!_cultures.Contains(culture.ToLowerInvariant())) + _cultures.Add(culture.ToLowerInvariant(), resourcesNode); + else + Debug.Fail("Already found resources for culture " + stringNode.InnerText); + } + } + } + } + } + } + } + } + + private void RefreshProducts() + { + _products.Clear(); + _validationResults.Clear(); + _document = new XmlDocument(); + _xmlNamespaceManager = new XmlNamespaceManager(_document.NameTable); + + _xmlNamespaceManager.AddNamespace(BOOTSTRAPPER_PREFIX, BOOTSTRAPPER_NAMESPACE); + + XmlElement rootElement = _document.CreateElement("Products", BOOTSTRAPPER_NAMESPACE); + string packagePath = PackagePath; + + if (Directory.Exists(packagePath)) + { + foreach (string strSubDirectory in Directory.GetDirectories(packagePath)) + { + int nStartIndex = packagePath.Length; + if ((strSubDirectory.ToCharArray())[nStartIndex] == System.IO.Path.DirectorySeparatorChar) + { + nStartIndex = nStartIndex + 1; + } + ExploreDirectory(strSubDirectory.Substring(nStartIndex), rootElement); + } + } + + _document.AppendChild(rootElement); + + Hashtable availableProducts = new Hashtable(); + // A second copy of all the project which will get destroyed during the generation of the build order + Hashtable buildQueue = new Hashtable(); + + XmlNodeList productsFound = rootElement.SelectNodes(BOOTSTRAPPER_PREFIX + ":Product", _xmlNamespaceManager); + foreach (XmlNode productNode in productsFound) + { + Product p = CreateProduct(productNode); + if (p != null) + { + availableProducts.Add(p.ProductCode, p); + buildQueue.Add(p.ProductCode, CreateProduct(productNode)); + } + } + + // Set the product and included products for each product + foreach (Product p in availableProducts.Values) + { + AddDependencies(p, availableProducts); + AddIncludes(p, availableProducts); + } + + // We need only the dependencies to generate the bulid order + foreach (Product p in buildQueue.Values) + { + AddDependencies(p, buildQueue); + } + + // Scan the products and their dependencies to calculate install order + OrderProducts(availableProducts, buildQueue); + } + + private void AddDependencies(Product p, Hashtable availableProducts) + { + foreach (string relatedProductCode in SelectRelatedProducts(p, "DependsOnProduct")) + { + if (availableProducts.Contains(relatedProductCode)) + { + p.AddDependentProduct((Product)availableProducts[relatedProductCode]); + } + else + { + ArrayList missingDependencies = new ArrayList(); + missingDependencies.Add(relatedProductCode); + p.AddMissingDependency(missingDependencies); + } + } + + foreach (XmlNode eitherProductNode in SelectEitherProducts(p)) + { + List foundDependencies = new List(); + ArrayList allDependencies = new ArrayList(); + + foreach (XmlNode relatedProductNode in eitherProductNode.SelectNodes(String.Format(CultureInfo.InvariantCulture, "{0}:DependsOnProduct", BOOTSTRAPPER_PREFIX), _xmlNamespaceManager)) + { + XmlAttribute relatedProductAttribute = (XmlAttribute)(relatedProductNode.Attributes.GetNamedItem("Code")); + if (relatedProductAttribute != null) + { + string dependency = relatedProductAttribute.Value; + if (availableProducts.Contains(dependency)) + { + foundDependencies.Add((Product)availableProducts[dependency]); + } + allDependencies.Add(dependency); + } + } + + if (foundDependencies.Count > 0) + { + if (!p.ContainsDependencies(foundDependencies)) + { + p.Dependencies.Add(foundDependencies); + } + } + else if (allDependencies.Count > 0) + { + p.AddMissingDependency(allDependencies); + } + } + } + + private void AddIncludes(Product p, Hashtable availableProducts) + { + foreach (string relatedProductCode in SelectRelatedProducts(p, "IncludesProduct")) + { + if (availableProducts.Contains(relatedProductCode)) + { + p.Includes.Add((Product)availableProducts[relatedProductCode]); + } + } + } + + private string[] SelectRelatedProducts(Product p, string nodeName) + { + ArrayList list = new ArrayList(); + + XmlNodeList relatedProducts = p.Node.SelectNodes(string.Format(CultureInfo.InvariantCulture, "{0}:Package/{1}:RelatedProducts/{2}:{3}", BOOTSTRAPPER_PREFIX, BOOTSTRAPPER_PREFIX, BOOTSTRAPPER_PREFIX, nodeName), _xmlNamespaceManager); + if (relatedProducts != null) + { + foreach (XmlNode relatedProduct in relatedProducts) + { + XmlAttribute relatedProductAttribute = (XmlAttribute)(relatedProduct.Attributes.GetNamedItem("Code")); + if (relatedProductAttribute != null) + { + list.Add(relatedProductAttribute.Value); + } + } + } + + string[] a = new string[list.Count]; + list.CopyTo(a, 0); + return a; + } + + private XmlNodeList SelectEitherProducts(Product p) + { + XmlNodeList eitherProducts = p.Node.SelectNodes(string.Format(CultureInfo.InvariantCulture, "{0}:Package/{1}:RelatedProducts/{2}:EitherProducts", BOOTSTRAPPER_PREFIX, BOOTSTRAPPER_PREFIX, BOOTSTRAPPER_PREFIX), _xmlNamespaceManager); + return eitherProducts; + } + + private void OrderProducts(Hashtable availableProducts, Hashtable buildQueue) + { + bool loopDetected = false; + _loopDependenciesWarnings = new BuildResults(); + StringBuilder productsInLoop = new StringBuilder(); + while (buildQueue.Count > 0) + { + List productsToRemove = new List(); + foreach (Product p in buildQueue.Values) + { + if (p.Dependencies.Count == 0) + { + _products.Add((Product)availableProducts[p.ProductCode]); + RemoveDependency(buildQueue, p); + productsToRemove.Add(p.ProductCode); + } + } + + foreach (string productCode in productsToRemove) + { + buildQueue.Remove(productCode); + if (loopDetected) + { + productsInLoop.Append(productCode); + productsInLoop.Append(", "); + } + } + + // If we could not remove any products and there are still products in the queue + // there must be a loop in it. We'll break the loop by removing the dependencies + // of the first project in the queue; + if (buildQueue.Count > 0 && productsToRemove.Count == 0) + { + IDictionaryEnumerator enumerator = buildQueue.GetEnumerator(); + enumerator.MoveNext(); + ((Product)enumerator.Value).Dependencies.RemoveAll(m => true); + loopDetected = true; + } + + // If we've been in a loop and there are no more products left + // or no more products can be installed, we have completely walked that loop + // and now is a good time to show the warning message for the loop + if (productsInLoop.Length > 0 && (buildQueue.Count == 0 || productsToRemove.Count == 0)) + { + productsInLoop.Remove(productsInLoop.Length - 2, 2); + _loopDependenciesWarnings.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.CircularDependency", productsInLoop.ToString())); + productsInLoop.Remove(0, productsInLoop.Length); + } + } + } + + private void RemoveDependency(Hashtable availableProducts, Product product) + { + foreach (Product p in availableProducts.Values) + { + foreach (List dependency in p.Dependencies) + { + dependency.RemoveAll(m => m == product); + } + p.Dependencies.RemoveAll(m => m.Count == 0); + } + } + + private XmlDocument LoadAndValidateXmlDocument(string filePath, bool validateFilePresent, string schemaPath, string schemaNamespace, XmlValidationResults results) + { + XmlDocument xmlDocument = null; + + Debug.Assert(filePath != null, "null filePath?"); + Debug.Assert(schemaPath != null, "null schemaPath?"); + Debug.Assert(schemaNamespace != null, "null schemaNamespace?"); + + if ((filePath != null) && (schemaPath != null) && (schemaNamespace != null)) + { + // set up our validation logic by detecting the trace-switch enabled and whether or + // not our files exist. + bool validate = true; + bool fileExists = File.Exists(filePath); + bool schemaExists = File.Exists(schemaPath); + + // if we're being asked to validate but we can't find the schema file, then + // output something useful to tell user that we can't find the schema. + if (!schemaExists) + { + Debug.Fail("Could not locate schema '" + schemaPath + "', so no validation of '" + filePath + "' is possible."); + validate = false; + } + + // if we're being asked to validate but we can't find the data file, then + // output something useful to tell user that we can't find the file and that we + // can't do anything useful. + if (validate && (!fileExists) && validateFilePresent) + { + Debug.Fail("Could not locate data file '" + filePath + "'."); + validate = false; + } + + if (fileExists) + { + XmlTextReader xmlTextReader = new XmlTextReader(filePath); + xmlTextReader.DtdProcessing = DtdProcessing.Ignore; + + XmlReader xmlReader = xmlTextReader; + + if (validate) + { +#pragma warning disable 618 // Using XmlValidatingReader. TODO: We need to switch to using XmlReader.Create() with validation. + var validatingReader = new XmlValidatingReader(xmlReader); +#pragma warning restore 618 + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(schemaPath, xrSettings)) + { + try + { + // first, add our schema to the validating reader's collection of schemas + var xmlSchema = validatingReader.Schemas.Add(null, xr); + + // if our schema namespace gets out of sync, + // then all of our calls to SelectNodes and SelectSingleNode will fail + Debug.Assert((xmlSchema != null) && + string.Equals(schemaNamespace, xmlSchema.TargetNamespace, StringComparison.Ordinal), + System.IO.Path.GetFileName(schemaPath) + " and BootstrapperBuilder.vb have mismatched namespaces, so the BootstrapperBuilder will fail to work."); + + // if we're supposed to be validating, then hook up our handler + validatingReader.ValidationEventHandler += results.SchemaValidationEventHandler; + + // switch readers so the doc does the actual read over the validating + // reader so we get validation events as we load the document + xmlReader = validatingReader; + } + catch (XmlException ex) + { + if (validate) + { + Debug.Fail("Failed to load schema '" + schemaPath + "' due to the following exception:\r\n" + ex.Message); + } + validate = false; + } + catch (System.Xml.Schema.XmlSchemaException ex) + { + if (validate) + { + Debug.Fail("Failed to load schema '" + schemaPath + "' due to the following exception:\r\n" + ex.Message); + } + validate = false; + } + } + } + + try + { + Debug.Assert(_document != null, "our document should have been created by now!"); + xmlDocument = new XmlDocument(_document.NameTable); + xmlDocument.Load(xmlReader); + } + catch (XmlException ex) + { + Debug.Fail("Failed to load document '" + filePath + "' due to the following exception:\r\n" + ex.Message); + return null; + } + catch (System.Xml.Schema.XmlSchemaException ex) + { + Debug.Fail("Failed to load document '" + filePath + "' due to the following exception:\r\n" + ex.Message); + return null; + } + finally + { + xmlReader.Close(); + } + + // Note that the xml document's default namespace must match the schema namespace + // or none of our SelectNodes/SelectSingleNode calls will succeed + Debug.Assert(xmlDocument.DocumentElement != null && + string.Equals(xmlDocument.DocumentElement.NamespaceURI, schemaNamespace, StringComparison.Ordinal), + "'" + xmlDocument.DocumentElement.NamespaceURI + "' is not '" + schemaNamespace + "'..."); + + if ((xmlDocument.DocumentElement == null) || + (!string.Equals(xmlDocument.DocumentElement.NamespaceURI, schemaNamespace, StringComparison.Ordinal))) + { + } + } + } + + return xmlDocument; + } + + private void ExploreDirectory(string strSubDirectory, XmlElement rootElement) + { + try + { + string packagePath = PackagePath; + string strSubDirectoryFullPath = System.IO.Path.Combine(packagePath, strSubDirectory); + + // figure out our product file paths based on the directory full path + string strBaseManifestFilename = System.IO.Path.Combine(strSubDirectoryFullPath, ROOT_MANIFEST_FILE); + string strBaseManifestSchemaFileName = System.IO.Path.Combine(SchemaPath, MANIFEST_FILE_SCHEMA); + + ProductValidationResults productValidationResults = new ProductValidationResults(strBaseManifestFilename); + + // open the XmlDocument for this product.xml + XmlDocument productDoc = LoadAndValidateXmlDocument(strBaseManifestFilename, false, strBaseManifestSchemaFileName, BOOTSTRAPPER_NAMESPACE, productValidationResults); + if (productDoc != null) + { + bool packageAdded = false; + + XmlNode baseNode = productDoc.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Product", _xmlNamespaceManager); + if (baseNode != null) + { + // Get the ProductCode attribute for this product + XmlAttribute productCodeAttribute = (XmlAttribute)(baseNode.Attributes.GetNamedItem("ProductCode")); + if (productCodeAttribute != null) + { + // now add it to our full document if it's not already present + XmlNode productNode = rootElement.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Product[@ProductCode='" + productCodeAttribute.Value + "']", _xmlNamespaceManager); + if (productNode == null) + { + productNode = CreateProductNode(baseNode); + } + else + { + productValidationResults = (ProductValidationResults)_validationResults[productCodeAttribute]; + } + + // Fix-up the of the base node to include the SourcePath and TargetPath + XmlNode packageFilesNode = baseNode.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":PackageFiles", _xmlNamespaceManager); + XmlNode checksNode = baseNode.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":InstallChecks", _xmlNamespaceManager); + XmlNode commandsNode = baseNode.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Commands", _xmlNamespaceManager); + + // if there was a packageFiles node, then add it in to our full document with the rest + if (packageFilesNode != null) + { + UpdatePackageFileNodes(packageFilesNode, System.IO.Path.Combine(packagePath, strSubDirectory), strSubDirectory); + + ReplacePackageFileAttributes(checksNode, "PackageFile", packageFilesNode, "PackageFile", "OldName", "Name"); + ReplacePackageFileAttributes(commandsNode, "PackageFile", packageFilesNode, "PackageFile", "OldName", "Name"); + ReplacePackageFileAttributes(baseNode, EULA_ATTRIBUTE, packageFilesNode, "PackageFile", "OldName", "SourcePath"); + } + + foreach (string strLanguageDirectory in Directory.GetDirectories(strSubDirectoryFullPath)) + { + // The base node would get destroyed as we build-up this new node. + // Thus, we want to use a copy of the baseNode + XmlElement baseElement = (XmlElement)(_document.ImportNode(baseNode, true)); + + string strLangManifestFilename = System.IO.Path.Combine(strLanguageDirectory, CHILD_MANIFEST_FILE); + string strLangManifestSchemaFileName = System.IO.Path.Combine(SchemaPath, MANIFEST_FILE_SCHEMA); + + if (File.Exists(strLangManifestFilename)) + { + // Load Package.xml + XmlValidationResults packageValidationResults = new XmlValidationResults(strLangManifestFilename); + XmlDocument langDoc = LoadAndValidateXmlDocument(strLangManifestFilename, false, strLangManifestSchemaFileName, BOOTSTRAPPER_NAMESPACE, packageValidationResults); + + Debug.Assert(langDoc != null, "we couldn't load package.xml in '" + strLangManifestFilename + "'...?"); + if (langDoc == null) + continue; + + XmlNode langNode = langDoc.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Package", _xmlNamespaceManager); + Debug.Assert(langNode != null, string.Format(CultureInfo.CurrentCulture, "Unable to find a package node in {0}", strLangManifestFilename)); + if (langNode != null) + { + XmlElement langElement = (XmlElement)(_document.ImportNode(langNode, true)); + XmlElement mergeElement = _document.CreateElement("Package", BOOTSTRAPPER_NAMESPACE); + + // Update the "PackageFiles" section to reflect this language subdirectory + XmlNode packageFilesNodePackage = langElement.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":PackageFiles", _xmlNamespaceManager); + checksNode = langElement.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":InstallChecks", _xmlNamespaceManager); + commandsNode = langElement.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Commands", _xmlNamespaceManager); + + if (packageFilesNodePackage != null) + { + int nStartIndex = packagePath.Length; + + if ((strLanguageDirectory.ToCharArray())[nStartIndex] == System.IO.Path.DirectorySeparatorChar) + nStartIndex++; + UpdatePackageFileNodes(packageFilesNodePackage, strLanguageDirectory, strSubDirectory); + + ReplacePackageFileAttributes(checksNode, "PackageFile", packageFilesNodePackage, "PackageFile", "OldName", "Name"); + ReplacePackageFileAttributes(commandsNode, "PackageFile", packageFilesNodePackage, "PackageFile", "OldName", "Name"); + ReplacePackageFileAttributes(langElement, EULA_ATTRIBUTE, packageFilesNodePackage, "PackageFile", "OldName", "SourcePath"); + } + + if (packageFilesNode != null) + { + ReplacePackageFileAttributes(checksNode, "PackageFile", packageFilesNode, "PackageFile", "OldName", "Name"); + ReplacePackageFileAttributes(commandsNode, "PackageFile", packageFilesNode, "PackageFile", "OldName", "Name"); + ReplacePackageFileAttributes(langElement, EULA_ATTRIBUTE, packageFilesNode, "PackageFile", "OldName", "SourcePath"); + } + + // in general, we prefer the attributes of the language document over the + // attributes of the base document. Copy attributes from the lang to the merged, + // and then merge all unique elements into merge + foreach (XmlAttribute attribute in langElement.Attributes) + { + mergeElement.Attributes.Append((XmlAttribute)(mergeElement.OwnerDocument.ImportNode(attribute, false))); + } + + foreach (XmlAttribute attribute in baseElement.Attributes) + { + XmlAttribute convertedAttribute = (XmlAttribute)(mergeElement.OwnerDocument.ImportNode(attribute, false)); + MergeAttribute(mergeElement, convertedAttribute); + } + + // And append all of the nodes + // There is a well-known set of nodes which may have inherit children + // When merging these nodes, there may be subnodes taken from both the lang element and the base element. + // There will never be multiple nodes with the same name in the same manifest + // The function which performs this action is CombineElements(...) + CombineElements(langElement, baseElement, "Commands", "PackageFile", mergeElement); + CombineElements(langElement, baseElement, "InstallChecks", "Property", mergeElement); + CombineElements(langElement, baseElement, "PackageFiles", "Name", mergeElement); + CombineElements(langElement, baseElement, "Schedules", "Name", mergeElement); + CombineElements(langElement, baseElement, "Strings", "Name", mergeElement); + + ReplaceStrings(mergeElement); + CorrectPackageFiles(mergeElement); + + AppendNode(baseElement, "RelatedProducts", mergeElement); + + // Create a unique identifier for this package + XmlAttribute cultureAttribute = (XmlAttribute)mergeElement.Attributes.GetNamedItem("Culture"); + if (cultureAttribute != null && !String.IsNullOrEmpty(cultureAttribute.Value)) + { + string packageCode = productCodeAttribute.Value + "." + cultureAttribute.Value; + AddAttribute(mergeElement, "PackageCode", packageCode); + + if (productValidationResults != null && packageValidationResults != null) + { + productValidationResults.AddPackageResults(cultureAttribute.Value, packageValidationResults); + } + + // Only add this package if there is a culture apecified. + productNode.AppendChild(mergeElement); + packageAdded = true; + } + } + } + } + if (packageAdded) + { + rootElement.AppendChild(productNode); + if (!_validationResults.Contains(productCodeAttribute.Value)) + { + _validationResults.Add(productCodeAttribute.Value, productValidationResults); + } + else + { + Debug.WriteLine(String.Format(CultureInfo.CurrentCulture, "Validation results already added for Product Code '{0}'", productCodeAttribute)); + } + } + } + } + } + } + catch (XmlException ex) + { + Debug.Fail(ex.Message); + } + catch (System.IO.IOException ex) + { + Debug.Fail(ex.Message); + } + catch (ArgumentException ex) + { + Debug.Fail(ex.Message); + } + } + + private Product CreateProduct(XmlNode node) + { + bool fPackageAdded = false; + string productCode = ReadAttribute(node, "ProductCode"); + Product product = null; + if (!String.IsNullOrEmpty(productCode)) + { + ProductValidationResults results = (ProductValidationResults)_validationResults[productCode]; + + XmlNode packageFilesNode = node.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Package/" + BOOTSTRAPPER_PREFIX + ":PackageFiles", _xmlNamespaceManager); + string copyAllPackageFiles = String.Empty; + + if (packageFilesNode != null) copyAllPackageFiles = ReadAttribute(packageFilesNode, "CopyAllPackageFiles"); + + product = new Product(node, productCode, results, copyAllPackageFiles); + XmlNodeList packageNodeList = node.SelectNodes(BOOTSTRAPPER_PREFIX + ":Package", _xmlNamespaceManager); + + foreach (XmlNode packageNode in packageNodeList) + { + Package package = CreatePackage(packageNode, product); + if (package != null) + { + product.AddPackage(package); + fPackageAdded = true; + } + } + } + + if (fPackageAdded) + return product; + return null; + } + + private Package CreatePackage(XmlNode node, Product product) + { + string culture = ReadAttribute(node, "Culture"); + + XmlValidationResults results = null; + if (culture != null) + { + results = product.GetPackageValidationResults(culture); + } + else + { + return null; + } + + return new Package(product, node, results, ReadAttribute(node, "Name"), ReadAttribute(node, "Culture")); + } + + private void ReplaceAttributes(XmlNode targetNode, string attributeName, string oldValue, string newValue) + { + if (targetNode != null) + { + // select all nodes where the attributeName equals the oldValue + XmlNodeList nodeList = targetNode.SelectNodes(BOOTSTRAPPER_PREFIX + string.Format(CultureInfo.InvariantCulture, ":*[@{0}='{1}']", attributeName, oldValue), _xmlNamespaceManager); + + foreach (XmlNode node in nodeList) + { + ReplaceAttribute(node, attributeName, newValue); + } + + // replace attributes on the node itself + XmlAttribute attrib = targetNode.Attributes[attributeName]; + if (attrib != null && attrib.Value == oldValue) + attrib.Value = newValue; + } + } + + private void ReplaceAttribute(XmlNode targetNode, string attributeName, string attributeValue) + { + XmlAttribute attribute = targetNode.OwnerDocument.CreateAttribute(attributeName); + attribute.Value = attributeValue; + targetNode.Attributes.SetNamedItem(attribute); + } + + private void MergeAttribute(XmlNode targetNode, XmlAttribute attribute) + { + XmlAttribute targetAttribute = (XmlAttribute)(targetNode.Attributes.GetNamedItem(attribute.Name)); + if (targetAttribute == null) + { + // This node does not already contain the attribute. Add the parameter + targetNode.Attributes.Append(attribute); + } + } + + private void UpdatePackageFileNodes(XmlNode packageFilesNode, string strSourcePath, string strTargetPath) + { + XmlNodeList packageFileNodeList = packageFilesNode.SelectNodes(BOOTSTRAPPER_PREFIX + ":PackageFile", _xmlNamespaceManager); + + foreach (XmlNode packageFileNode in packageFileNodeList) + { + XmlAttribute nameAttribute = (XmlAttribute)(packageFileNode.Attributes.GetNamedItem("Name")); + + // the name attribute is required -- we can't do anything if it's not present + if (nameAttribute != null) + { + string relativePath = nameAttribute.Value; + + XmlAttribute sourcePathAttribute = packageFilesNode.OwnerDocument.CreateAttribute("SourcePath"); + string strSourceFile = System.IO.Path.Combine(strSourcePath, relativePath); + sourcePathAttribute.Value = strSourceFile; + + XmlAttribute targetPathAttribute = packageFilesNode.OwnerDocument.CreateAttribute("TargetPath"); + targetPathAttribute.Value = System.IO.Path.Combine(strTargetPath, relativePath); + + string oldNameValue = nameAttribute.Value; + string newNameValue = System.IO.Path.Combine(strTargetPath, relativePath); + + XmlAttribute oldNameAttribute = packageFilesNode.OwnerDocument.CreateAttribute("OldName"); + oldNameAttribute.Value = oldNameValue; + + ReplaceAttribute(packageFileNode, "Name", newNameValue); + MergeAttribute(packageFileNode, sourcePathAttribute); + MergeAttribute(packageFileNode, targetPathAttribute); + MergeAttribute(packageFileNode, oldNameAttribute); + } + } + } + + private void AppendNode(XmlElement element, string nodeName, XmlElement mergeElement) + { + XmlNode node = element.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":" + nodeName, _xmlNamespaceManager); + if (node != null) + { + mergeElement.AppendChild(node); + } + } + + private void CombineElements(XmlElement langElement, XmlElement baseElement, string strNodeName, string strSubNodeKey, XmlElement mergeElement) + { + XmlNode langNode = langElement.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":" + strNodeName, _xmlNamespaceManager); + XmlNode baseNode = baseElement.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":" + strNodeName, _xmlNamespaceManager); + + // There are 4 basic cases to be dealt with: + // Case # 1 2 3 4 + // base null null present present + // lang null present null present + // Result null lang base combine + // + // Cases 1 - 3 are pretty trivial. + if (baseNode == null) + { + if (langNode != null) + { + // Case 2 + mergeElement.AppendChild(langNode); + } + // Case 1 is to do nothing + } + else + { + if (langNode == null) + { + // Case 3 + mergeElement.AppendChild(baseNode); + } + else + { + XmlNode mergeSubNode = _document.CreateElement(strNodeName, BOOTSTRAPPER_NAMESPACE); + XmlNode nextNode = baseNode.FirstChild; + + // Begin case 4 + // Go through every element in the base node + while (nextNode != null) + { + if (nextNode.NodeType == XmlNodeType.Element) + { + XmlAttribute keyAttribute = (XmlAttribute)(nextNode.Attributes.GetNamedItem(strSubNodeKey)); + if (keyAttribute != null) + { + XmlNode queryResultNode = QueryForSubNode(langNode, strSubNodeKey, keyAttribute.Value); + // if there is no match in the lang node, use the current base node + // Otherwise use that node and remove it later + if (queryResultNode == null) + { + mergeSubNode.AppendChild(mergeSubNode.OwnerDocument.ImportNode(nextNode, true)); + } + else + { + mergeSubNode.AppendChild(mergeSubNode.OwnerDocument.ImportNode(queryResultNode, true)); + langNode.RemoveChild(queryResultNode); + } + } + else + { + Debug.Assert(false, "Specified key does not exist for node " + nextNode.InnerXml); + } + } + nextNode = nextNode.NextSibling; + } + + // Append all remaining lang nodes + nextNode = langNode.FirstChild; + + while (nextNode != null) + { + mergeSubNode.AppendChild(mergeSubNode.OwnerDocument.ImportNode(nextNode, true)); + nextNode = nextNode.NextSibling; + } + + // Copy all attributes. The langnode again has priority + foreach (XmlAttribute attribute in langNode.Attributes) + { + AddAttribute(mergeSubNode, attribute.Name, attribute.Value); + } + foreach (XmlAttribute attribute in baseNode.Attributes) + { + if (mergeSubNode.Attributes.GetNamedItem(attribute.Name) == null) + { + AddAttribute(mergeSubNode, attribute.Name, attribute.Value); + } + } + + mergeElement.AppendChild(mergeSubNode); + } + } + } + + private XmlNode QueryForSubNode(XmlNode subNode, string strSubNodeKey, string strTargetValue) + { + string strQuery = string.Format(CultureInfo.InvariantCulture, "{0}:*[@{1}='{2}']", BOOTSTRAPPER_PREFIX, strSubNodeKey, strTargetValue); + return subNode.SelectSingleNode(strQuery, _xmlNamespaceManager); + } + + private void CorrectPackageFiles(XmlNode node) + { + XmlNode packageFilesNode = node.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":PackageFiles", _xmlNamespaceManager); + + if (packageFilesNode != null) + { + // Map all StringKey attributes to corresponding String values + XmlNodeList packageFileNodeList = node.SelectNodes("//" + BOOTSTRAPPER_PREFIX + ":*[@PackageFile]", _xmlNamespaceManager); + foreach (XmlNode currentNode in packageFileNodeList) + { + XmlAttribute attribute = (XmlAttribute)(currentNode.Attributes.GetNamedItem("PackageFile")); + string strQuery = BOOTSTRAPPER_PREFIX + ":PackageFile[@Name='" + attribute.Value + "']"; + XmlNode packageFileNode = packageFilesNode.SelectSingleNode(strQuery, _xmlNamespaceManager); + if (packageFileNode != null) + { + XmlAttribute targetPathAttribute = (XmlAttribute)(packageFileNode.Attributes.GetNamedItem("TargetPath")); + attribute.Value = targetPathAttribute.Value; + } + } + } + } + + private void ReplaceStrings(XmlNode node) + { + XmlNode stringsNode = node.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":Strings", _xmlNamespaceManager); + XmlNode stringNode; + XmlAttribute attribute; + + if (stringsNode != null) + { + string stringNodeLookupTemplate = BOOTSTRAPPER_PREFIX + ":String[@Name='{0}']"; + + // The name attribute at the package level is an entry into the String table + ReplaceAttributeString(node, "Name", stringsNode); + ReplaceAttributeString(node, "Culture", stringsNode); + + // Homesite information is also carried in the String table + XmlNode packageFilesNode = node.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":PackageFiles", _xmlNamespaceManager); + if (packageFilesNode != null) + { + XmlNodeList packageFileNodeList = packageFilesNode.SelectNodes(BOOTSTRAPPER_PREFIX + ":PackageFile", _xmlNamespaceManager); + foreach (XmlNode packageFileNode in packageFileNodeList) + { + ReplaceAttributeString(packageFileNode, HOMESITE_ATTRIBUTE, stringsNode); + } + } + + // Map all String attributes to corresponding String values + // It is currently expected that these come from either an ExitCode or FailIf + XmlNodeList stringKeyNodeList = node.SelectNodes("//" + BOOTSTRAPPER_PREFIX + ":*[@String]", _xmlNamespaceManager); + foreach (XmlNode currentNode in stringKeyNodeList) + { + attribute = (XmlAttribute)(currentNode.Attributes.GetNamedItem("String")); + stringNode = stringsNode.SelectSingleNode(string.Format(CultureInfo.InvariantCulture, stringNodeLookupTemplate, attribute.Value), _xmlNamespaceManager); + if (stringNode != null) + { + AddAttribute(currentNode, "Text", stringNode.InnerText); + } + currentNode.Attributes.Remove(attribute); + } + + // The Strings node is no longer necessary. Remove it. + node.RemoveChild(stringsNode); + } + } + + private bool BuildPackages(BuildSettings settings, XmlElement configElement, ResourceUpdater resourceUpdater, ArrayList filesCopied, Hashtable eulas) + { + bool fSucceeded = true; + + foreach (ProductBuilder builder in settings.ProductBuilders) + { + if (Validate && !builder.Product.ValidationPassed) + { + if (_results != null) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.ProductValidation", builder.Name, builder.Product.ValidationResults.FilePath)); + foreach (string validationMessage in builder.Product.ValidationResults.ValidationErrors) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.ValidationError", builder.Product.ValidationResults.FilePath, validationMessage)); + } + foreach (string validationMessage in builder.Product.ValidationResults.ValidationWarnings) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.ValidationWarning", builder.Product.ValidationResults.FilePath, validationMessage)); + } + } + } + Package package = GetPackageForSettings(settings, builder, _results); + if (package == null) + { + // GetPackage should have already added the correct message info + continue; + } + + if (Validate && !package.ValidationPassed) + { + if (_results != null) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.PackageValidation", builder.Name, package.ValidationResults.FilePath)); + foreach (string validationMessage in package.ValidationResults.ValidationErrors) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.ValidationError", package.ValidationResults.FilePath, validationMessage)); + } + foreach (string validationMessage in package.ValidationResults.ValidationWarnings) + { + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.ValidationWarning", package.ValidationResults.FilePath, validationMessage)); + } + } + } + + XmlNode node = package.Node; + // Copy the files for this package to the output directory + XmlAttribute eulaAttribute = node.Attributes[EULA_ATTRIBUTE]; + XmlNodeList packageFileNodes = node.SelectNodes(BOOTSTRAPPER_PREFIX + ":PackageFiles/" + BOOTSTRAPPER_PREFIX + ":PackageFile", _xmlNamespaceManager); + XmlNode installChecksNode = node.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":InstallChecks", _xmlNamespaceManager); + foreach (XmlNode packageFileNode in packageFileNodes) + { + XmlAttribute packageFileSource = (XmlAttribute)(packageFileNode.Attributes.GetNamedItem("SourcePath")); + XmlAttribute packageFileDestination = (XmlAttribute)(packageFileNode.Attributes.GetNamedItem("TargetPath")); + XmlAttribute packageFileName = (XmlAttribute)(packageFileNode.Attributes.GetNamedItem("Name")); + XmlAttribute packageFileCopy = (XmlAttribute)(packageFileNode.Attributes.GetNamedItem("CopyOnBuild")); + if (packageFileSource != null && eulaAttribute != null && !String.IsNullOrEmpty(eulaAttribute.Value) && packageFileSource.Value == eulaAttribute.Value) + { + // need to remove EULA from the package file list + XmlNode packageFilesNode = node.SelectSingleNode(BOOTSTRAPPER_PREFIX + ":PackageFiles", _xmlNamespaceManager); + packageFilesNode.RemoveChild(packageFileNode); + continue; + } + + if ((packageFileSource != null) && (packageFileDestination != null)) + { + // Calculate the hash of this file and add it to the PackageFileNode + if (!AddVerificationInformation(packageFileNode, packageFileSource.Value, packageFileName.Value, builder, settings, _results)) + fSucceeded = false; + } + + if ((packageFileSource != null) && (packageFileDestination != null) && + ((packageFileCopy == null) || (String.Compare(packageFileCopy.Value, "False", StringComparison.InvariantCulture) != 0))) + { + // if this is the key for an external check, we will add it to the Resource Updater instead of copying the file + XmlNode subNode = null; + if ((installChecksNode != null) && (packageFileName != null)) + { + subNode = QueryForSubNode(installChecksNode, "PackageFile", packageFileName.Value); + } + if (subNode != null) + { + if (resourceUpdater != null) + { + if (!File.Exists(packageFileSource.Value)) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.PackageResourceFileNotFound", packageFileSource.Value, builder.Name)); + fSucceeded = false; + continue; + } + resourceUpdater.AddFileResource(packageFileSource.Value, packageFileDestination.Value); + } + } + else + { + if (settings.ComponentsLocation != ComponentsLocation.HomeSite || !VerifyHomeSiteInformation(packageFileNode, builder, settings, _results)) + { + if (settings.CopyComponents) + { + string strDestinationFileName = System.IO.Path.Combine(settings.OutputPath, packageFileDestination.Value); + try + { + if (!File.Exists(packageFileSource.Value)) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.PackageFileNotFound", packageFileDestination.Value, builder.Name)); + fSucceeded = false; + continue; + } + EnsureFolderExists(System.IO.Path.GetDirectoryName(strDestinationFileName)); + File.Copy(packageFileSource.Value, strDestinationFileName, true); + ClearReadOnlyAttribute(strDestinationFileName); + } + catch (UnauthorizedAccessException ex) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyPackageError", packageFileSource.Value, builder.Name, ex.Message)); + fSucceeded = false; + continue; + } + catch (IOException ex) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyPackageError", packageFileSource.Value, builder.Name, ex.Message)); + fSucceeded = false; + continue; + } + catch (ArgumentException ex) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyPackageError", packageFileSource.Value, builder.Name, ex.Message)); + fSucceeded = false; + continue; + } + catch (NotSupportedException ex) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.CopyPackageError", packageFileSource.Value, builder.Name, ex.Message)); + fSucceeded = false; + continue; + } + filesCopied.Add(strDestinationFileName); + } + else + { + filesCopied.Add(packageFileSource.Value); + } + + // Add the file size to the PackageFileNode + XmlAttribute sizeAttribute = packageFileNode.OwnerDocument.CreateAttribute("Size"); + FileInfo fi = new FileInfo(packageFileSource.Value); + sizeAttribute.Value = "" + (fi.Length.ToString(CultureInfo.InvariantCulture)); + MergeAttribute(packageFileNode, sizeAttribute); + } + } + } + } + // Add the Eula attribute correctly + if (eulas != null && eulaAttribute != null && !String.IsNullOrEmpty(eulaAttribute.Value)) + { + if (File.Exists(eulaAttribute.Value)) + { + // eulas[GetFileHash(eulaAttribute.Value)] = eulaAttribute.Value; + string key = GetFileHash(eulaAttribute.Value); + if (eulas.ContainsKey(key)) + eulaAttribute.Value = ((DictionaryEntry)eulas[key]).Key.ToString(); + else + { + string configFileKey = string.Format(CultureInfo.InvariantCulture, "EULA{0}", eulas.Count); + DictionaryEntry de = new DictionaryEntry(configFileKey, eulaAttribute.Value); + eulas[key] = de; + eulaAttribute.Value = configFileKey; + } + } + else + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.PackageResourceFileNotFound", eulaAttribute.Value, builder.Name)); + fSucceeded = false; + continue; + } + } + // Write the package node + if (configElement != null) + { + configElement.AppendChild(configElement.OwnerDocument.ImportNode(node, true)); + DumpXmlToFile(node, string.Format(CultureInfo.CurrentCulture, "{0}.{1}.xml", package.Product.ProductCode, package.Culture)); + } + } + + return fSucceeded; + } + + private XmlNode CreateProductNode(XmlNode node) + { + // create a new Product node for the passed-in product + XmlNode productNode = _document.CreateElement("Product", BOOTSTRAPPER_NAMESPACE); + XmlAttribute sourceAttribute; + + // find the ProductCode attribute + sourceAttribute = (XmlAttribute)(node.Attributes.GetNamedItem("ProductCode")); + Debug.Assert(sourceAttribute != null, "we should not be here if there is no ProductCode attribute"); + + AddAttribute(productNode, "ProductCode", sourceAttribute.Value); + + node.Attributes.Remove(sourceAttribute); + + return productNode; + } + + private string ReadAttribute(XmlNode node, string strAttributeName) + { + XmlAttribute attribute = (XmlAttribute)(node.Attributes.GetNamedItem(strAttributeName)); + + if (attribute != null) + return attribute.Value; + + return null; + } + + private void EnsureFolderExists(string strFolderPath) + { + if (!Directory.Exists(strFolderPath)) + { + Directory.CreateDirectory(strFolderPath); + } + } + + private void ClearReadOnlyAttribute(string strFileName) + { + FileAttributes attribs = File.GetAttributes(strFileName); + if ((attribs & FileAttributes.ReadOnly) != 0) + { + attribs = attribs & (~FileAttributes.ReadOnly); + File.SetAttributes(strFileName, attribs); + } + } + + private string ByteArrayToString(byte[] byteArray) + { + if (byteArray == null) return null; + + System.Text.StringBuilder output = new System.Text.StringBuilder(byteArray.Length); + foreach (byte byteValue in byteArray) + output.Append(byteValue.ToString("X02", CultureInfo.InvariantCulture)); + + return output.ToString(); + } + + private string GetFileHash(string filePath) + { + FileInfo fi = new System.IO.FileInfo(filePath); + String retVal = null; + + // Bootstrapper is always signed with the SHA-256 algorithm, no matter which version of + // the .NET Framework we are targeting. In ideal situations, bootstrapper files will be + // pre-signed anwyay; this is a fallback in case we ever encounter a bootstrapper that is + // not signed. + System.Security.Cryptography.SHA256CryptoServiceProvider sha = new System.Security.Cryptography.SHA256CryptoServiceProvider(); + + using (Stream s = fi.OpenRead()) + { + retVal = ByteArrayToString(sha.ComputeHash(s)); + } + return retVal; + } + + private void ReplaceAttributeString(XmlNode node, string attributeName, XmlNode stringsNode) + { + string stringNodeLookupTemplate = BOOTSTRAPPER_PREFIX + ":String[@Name='{0}']"; + XmlAttribute attribute = (XmlAttribute)(node.Attributes.GetNamedItem(attributeName)); + if (attribute != null) + { + XmlNode stringNode = stringsNode.SelectSingleNode(string.Format(CultureInfo.InvariantCulture, stringNodeLookupTemplate, attribute.Value), _xmlNamespaceManager); + if (stringNode != null) + attribute.Value = stringNode.InnerText; + } + } + + private Package GetPackageForSettings(BuildSettings settings, ProductBuilder builder, BuildResults results) + { + CultureInfo ci = Util.GetCultureInfoFromString(settings.Culture); + CultureInfo fallbackCI = Util.GetCultureInfoFromString(settings.FallbackCulture); + Package package = null; + + if (builder.Product.Packages.Count == 0) + { + if (results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.ProductCultureNotFound", builder.Name)); + return null; + } + + if (ci != null) + { + package = builder.Product.Packages.Package(ci.Name); + if (package != null) return package; + + // Target culture not found? Go through the progression of parent cultures (up until but excluding the invariant culture) -> fallback culture -> parent fallback culture -> default culture -> parent default culture -> any culture available + // Note: there is no warning if the parent culture of the requested culture is found + CultureInfo parentCulture = ci.Parent; + + // Keep going up the chain of parents, stopping at the invariant culture + while (parentCulture != null && parentCulture != CultureInfo.InvariantCulture) + { + package = GetPackageForSettings_Helper(ci, parentCulture, builder, results, false); + if (package != null) return package; + + parentCulture = parentCulture.Parent; + } + } + + + if (fallbackCI != null) + { + package = GetPackageForSettings_Helper(ci, fallbackCI, builder, results, true); + if (package != null) return package; + + if (!fallbackCI.IsNeutralCulture) + { + package = GetPackageForSettings_Helper(ci, fallbackCI.Parent, builder, results, true); + if (package != null) return package; + } + } + + package = GetPackageForSettings_Helper(ci, Util.DefaultCultureInfo, builder, results, true); + if (package != null) return package; + + if (!Util.DefaultCultureInfo.IsNeutralCulture) + { + package = GetPackageForSettings_Helper(ci, Util.DefaultCultureInfo.Parent, builder, results, true); + if (package != null) return package; + } + + if (results != null && ci != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.UsingProductCulture", ci.Name, builder.Name, builder.Product.Packages.Item(0).Culture)); + return builder.Product.Packages.Item(0); + } + + private Package GetPackageForSettings_Helper(CultureInfo culture, CultureInfo altCulture, ProductBuilder builder, BuildResults results, bool fShowWarning) + { + if (altCulture == null) + return null; + Package package = builder.Product.Packages.Package(altCulture.Name); + if (package != null) + { + if (fShowWarning && culture != null && results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.UsingProductCulture", culture.Name, builder.Name, altCulture.Name)); + return package; + } + return null; + } + + private bool BuildResources(BuildSettings settings, ResourceUpdater resourceUpdater) + { + if (_cultures.Count == 0) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.NoResources")); + return false; + } + + int codePage = -1; + XmlNode resourcesNode = GetResourcesNodeForSettings(settings, _results, ref codePage); + XmlNode stringsNode = resourcesNode.SelectSingleNode("Strings"); + XmlNode fontsNode = resourcesNode.SelectSingleNode("Fonts"); + + if (stringsNode == null) + { + if (_results != null) + _results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.NoStringsForCulture", resourcesNode.Attributes.GetNamedItem("Culture").Value)); + return false; + } + + XmlNodeList stringNodes = stringsNode.SelectNodes("String"); + + foreach (XmlNode stringNode in stringNodes) + { + XmlAttribute resourceIdAttribute = (XmlAttribute)stringNode.Attributes.GetNamedItem("Name"); + + if (resourceIdAttribute != null) + { + resourceUpdater.AddStringResource(MESSAGE_TABLE, resourceIdAttribute.Value.ToUpper(CultureInfo.InvariantCulture), stringNode.InnerText); + } + } + + if (fontsNode != null) + { + foreach (XmlNode fontNode in fontsNode.SelectNodes("Font")) + { + ConvertChildsNodeToAttributes(fontNode); + } + string fontsConfig = XmlToConfigurationFile(fontsNode); + resourceUpdater.AddStringResource(RESOURCE_TABLE, "SETUPRES", fontsConfig); + DumpXmlToFile(fontsNode, "fonts.cfg.xml"); + DumpStringToFile(fontsConfig, "fonts.cfg", false); + if (codePage != -1) + resourceUpdater.AddStringResource(RESOURCE_TABLE, "CODEPAGE", codePage.ToString(CultureInfo.InvariantCulture)); + } + return true; + } + + private XmlNode GetResourcesNodeForSettings(BuildSettings settings, BuildResults results, ref int codepage) + { + CultureInfo ci = Util.GetCultureInfoFromString(settings.Culture); + CultureInfo fallbackCI = Util.GetCultureInfoFromString(settings.FallbackCulture); + XmlNode cultureNode = null; + + + if (ci != null) + { + // Work through the progression of parent cultures (up until but excluding the invariant culture) -> fallback culture -> parent fallback culture -> default culture -> parent default culture -> any available culture + cultureNode = GetResourcesNodeForSettings_Helper(ci, ci, results, ref codepage, false); + if (cultureNode != null) return cultureNode; + CultureInfo parentCulture = ci.Parent; + + // Keep going up the chain of parents, stopping at the invariant culture + while (parentCulture != null && parentCulture != CultureInfo.InvariantCulture) + { + cultureNode = GetResourcesNodeForSettings_Helper(ci, parentCulture, results, ref codepage, false); + if (cultureNode != null) return cultureNode; + + parentCulture = parentCulture.Parent; + } + } + + if (fallbackCI != null) + { + cultureNode = GetResourcesNodeForSettings_Helper(ci, fallbackCI, results, ref codepage, true); + if (cultureNode != null) return cultureNode; + + if (!fallbackCI.IsNeutralCulture) + { + cultureNode = GetResourcesNodeForSettings_Helper(ci, fallbackCI.Parent, results, ref codepage, true); + if (cultureNode != null) return cultureNode; + } + } + + cultureNode = GetResourcesNodeForSettings_Helper(ci, Util.DefaultCultureInfo, results, ref codepage, true); + if (cultureNode != null) return cultureNode; + + if (!Util.DefaultCultureInfo.IsNeutralCulture) + { + cultureNode = GetResourcesNodeForSettings_Helper(ci, Util.DefaultCultureInfo.Parent, results, ref codepage, true); + if (cultureNode != null) return cultureNode; + } + + IEnumerator keys = _cultures.Keys.GetEnumerator(); + keys.MoveNext(); + string altCulture = (string)keys.Current; + if (ci != null && results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.UsingResourcesCulture", ci.Name, altCulture)); + GetCodePage(altCulture, ref codepage); + return (XmlNode)_cultures[altCulture.ToLowerInvariant()]; + } + + private XmlNode GetResourcesNodeForSettings_Helper(CultureInfo culture, CultureInfo altCulture, BuildResults results, ref int codepage, bool fShowWarning) + { + if (altCulture != null && _cultures.Contains(altCulture.Name.ToLowerInvariant())) + { + if (fShowWarning && culture != null && results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.UsingResourcesCulture", culture.Name, altCulture.Name)); + + codepage = altCulture.TextInfo.ANSICodePage; + return (XmlNode)_cultures[altCulture.Name.ToLowerInvariant()]; + } + + return null; + } + + private void GetCodePage(string culture, ref int codePage) + { + try + { + System.Globalization.CultureInfo info = new System.Globalization.CultureInfo(culture); + codePage = info.TextInfo.ANSICodePage; + } + catch (ArgumentException ex) + { + Debug.Fail(ex.Message); + } + } + + private void ReplacePackageFileAttributes(XmlNode targetNodes, string targetAttribute, XmlNode sourceNodes, string sourceSubNodeName, string sourceOldName, string sourceNewName) + { + XmlNodeList sourceNodeList = sourceNodes.SelectNodes(BOOTSTRAPPER_PREFIX + ":" + sourceSubNodeName, _xmlNamespaceManager); + + foreach (XmlNode sourceNode in sourceNodeList) + { + XmlAttribute oldNameAttribute = (XmlAttribute)(sourceNode.Attributes.GetNamedItem(sourceOldName)); + XmlAttribute newNameAttribute = (XmlAttribute)(sourceNode.Attributes.GetNamedItem(sourceNewName)); + + if (oldNameAttribute != null && newNameAttribute != null) + { + ReplaceAttributes(targetNodes, targetAttribute, oldNameAttribute.Value, newNameAttribute.Value); + } + } + } + + private XmlElement CreateApplicationElement(XmlElement configElement, BuildSettings settings) + { + XmlElement applicationElement = null; + + if (!String.IsNullOrEmpty(settings.ApplicationName) || !String.IsNullOrEmpty(settings.ApplicationFile)) + { + applicationElement = configElement.OwnerDocument.CreateElement("Application"); + if (!String.IsNullOrEmpty(settings.ApplicationName)) + { + AddAttribute(applicationElement, "Name", settings.ApplicationName); + } + AddAttribute(applicationElement, "RequiresElevation", settings.ApplicationRequiresElevation ? "true" : "false"); + + if (!String.IsNullOrEmpty(settings.ApplicationFile)) + { + XmlElement filesNode = applicationElement.OwnerDocument.CreateElement("Files"); + XmlElement fileNode = filesNode.OwnerDocument.CreateElement("File"); + AddAttribute(fileNode, "Name", settings.ApplicationFile); + AddAttribute(fileNode, URLNAME_ATTRIBUTE, Uri.EscapeUriString(settings.ApplicationFile)); + filesNode.AppendChild(fileNode); + applicationElement.AppendChild(filesNode); + } + } + return applicationElement; + } + + private void AddAttribute(XmlNode node, string attributeName, string attributeValue) + { + XmlAttribute attrib = node.OwnerDocument.CreateAttribute(attributeName); + attrib.Value = attributeValue; + node.Attributes.Append(attrib); + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3073: ReviewTrustedXsltUse.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + [SuppressMessage("Microsoft.Security.Xml", "CA3059: UseXmlReaderForXPathDocument.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + [SuppressMessage("Microsoft.Security.Xml", "CA3052: UseXmlResolver.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + private string XmlToConfigurationFile(XmlNode input) + { + using (XmlNodeReader reader = new XmlNodeReader(input)) + { + Stream s = GetEmbeddedResourceStream(CONFIG_TRANSFORM); + XPathDocument d = new XPathDocument(s); + XslCompiledTransform xslc = new XslCompiledTransform(); + // Using the Trusted Xslt is fine as the style sheet comes from our own assembly. + xslc.Load(d, XsltSettings.TrustedXslt, new XmlUrlResolver()); + + XPathDocument xml = new XPathDocument(reader); + + using (MemoryStream m = new MemoryStream()) + { + using (StreamWriter w = new StreamWriter(m)) + { + xslc.Transform(xml, null, w); + + w.Flush(); + m.Position = 0; + + using (StreamReader r = new StreamReader(m)) + { + // HACKHACK + string str = r.ReadToEnd(); + str = str.Replace("%NEWLINE%", Environment.NewLine); + return str; + } + } + } + } + } + + private Stream GetEmbeddedResourceStream(string name) + { + Assembly a = Assembly.GetExecutingAssembly(); + Stream s = a.GetManifestResourceStream(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", typeof(BootstrapperBuilder).Namespace, name)); + Debug.Assert(s != null, String.Format(CultureInfo.CurrentCulture, "EmbeddedResource '{0}' not found", name)); + return s; + } + + private string GetAssemblyPath() + { + return System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + + private void DumpXmlToFile(XmlNode node, string fileName) + { + if (s_logging) + { + try + { + using (XmlTextWriter xmlwriter = new XmlTextWriter(System.IO.Path.Combine(s_logPath, fileName), System.Text.Encoding.UTF8)) + { + xmlwriter.Formatting = Formatting.Indented; + xmlwriter.Indentation = 4; + xmlwriter.WriteNode(new XmlNodeReader(node), true); + } + } + catch (IOException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (UnauthorizedAccessException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (ArgumentException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (NotSupportedException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (XmlException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + } + } + + private void DumpStringToFile(string text, string fileName, bool append) + { + if (s_logging) + { + try + { + using (StreamWriter fileWriter = new StreamWriter(System.IO.Path.Combine(s_logPath, fileName), append)) + { + fileWriter.Write(text); + } + } + catch (IOException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (UnauthorizedAccessException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (ArgumentException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + catch (NotSupportedException) + { + // can't write info to a log file? This is a trouble-shooting helper only, and + // this exception can be ignored + } + } + } + + private bool VerifyHomeSiteInformation(XmlNode packageFileNode, ProductBuilder builder, BuildSettings settings, BuildResults results) + { + if (settings.ComponentsLocation != ComponentsLocation.HomeSite) + { + return true; + } + + XmlAttribute homesiteAttribute = packageFileNode.Attributes[HOMESITE_ATTRIBUTE]; + + if (homesiteAttribute == null && builder.Product.CopyAllPackageFiles != CopyAllFilesType.CopyAllFilesIfNotHomeSite) + { + if (results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.PackageHomeSiteMissing", builder.Name)); + return false; + } + + return true; + } + + private bool AddVerificationInformation(XmlNode packageFileNode, string fileSource, string fileName, ProductBuilder builder, BuildSettings settings, BuildResults results) + { + XmlAttribute hashAttribute = packageFileNode.Attributes[HASH_ATTRIBUTE]; + XmlAttribute publicKeyAttribute = packageFileNode.Attributes[PUBLICKEY_ATTRIBUTE]; + + if (File.Exists(fileSource)) + { + string publicKey = GetPublicKeyOfFile(fileSource); + if (hashAttribute == null && publicKeyAttribute == null) + { + // If neither the Hash nor PublicKey attributes were specified in the manifest, add it + if (publicKey != null) + { + AddAttribute(packageFileNode, PUBLICKEY_ATTRIBUTE, publicKey); + } + else + { + AddAttribute(packageFileNode, HASH_ATTRIBUTE, GetFileHash(fileSource)); + } + } + if (publicKeyAttribute != null) + { + // Always use the PublicKey of the file on disk + if (publicKey != null) + ReplaceAttribute(packageFileNode, PUBLICKEY_ATTRIBUTE, publicKey); + else + { + // File on disk is not signed. Remove the public key info, and make sure the hash is written instead + packageFileNode.Attributes.RemoveNamedItem(PUBLICKEY_ATTRIBUTE); + if (hashAttribute == null) + AddAttribute(packageFileNode, HASH_ATTRIBUTE, GetFileHash(fileSource)); + } + + // If the public key in the file doesn't match the public key on disk, issue a build warning + if (publicKey == null || !publicKey.ToLowerInvariant().Equals(publicKeyAttribute.Value.ToLowerInvariant())) + { + if (results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.DifferingPublicKeys", PUBLICKEY_ATTRIBUTE, builder.Name, fileSource)); + } + } + if (hashAttribute != null) + { + string fileHash = GetFileHash(fileSource); + + // Always use the Hash of the file on disk + ReplaceAttribute(packageFileNode, HASH_ATTRIBUTE, fileHash); + + // If the public key in the file doesn't match the public key on disk, issue a build warning + if (!fileHash.ToLowerInvariant().Equals(hashAttribute.Value.ToLowerInvariant())) + { + if (results != null) + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.DifferingPublicKeys", "Hash", builder.Name, fileSource)); + } + } + } + else if (settings.ComponentsLocation == ComponentsLocation.HomeSite) + { + if (hashAttribute == null && publicKeyAttribute == null) + { + if (results != null) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.MissingVerificationInformation", fileName, builder.Name)); + } + return false; + } + } + + return true; + } + + private string GetPublicKeyOfFile(string fileSource) + { + if (File.Exists(fileSource)) + { + try + { + X509Certificate cert = new X509Certificate(fileSource); + string publicKey = cert.GetPublicKeyString(); + return publicKey; + } + catch (System.Security.Cryptography.CryptographicException) + { + // This just means the file is not signed. + } + } + + return null; + } + + private void ConvertChildsNodeToAttributes(XmlNode node) + { + XmlNode childNode = node.FirstChild; + while (childNode != null) + { + // Need to get the next child node now because when the current node is removed, the NextSibling + // will be null + XmlNode currentNode = childNode; + childNode = currentNode.NextSibling; + if (currentNode.Attributes.Count == 0 && currentNode.InnerText.Length > 0) + { + AddAttribute(node, currentNode.Name, currentNode.InnerText); + node.RemoveChild(currentNode); + } + } + } + + private static string GetLogPath() + { + if (!s_logging) return null; + string logPath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + @"Microsoft\VisualStudio\" + VisualStudioConstants.CurrentVisualStudioVersion + @"\VSPLOG"); + if (!Directory.Exists(logPath)) + Directory.CreateDirectory(logPath); + return logPath; + } + + private Dictionary GetIncludedProducts(Product product) + { + Dictionary includedProducts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Add in this product in case there is a circular includes: + // we won't continue to explore this product. It will be removed later. + includedProducts.Add(product.ProductCode, product); + + // Recursively add included products + foreach (Product p in product.Includes) + { + AddIncludedProducts(p, includedProducts); + } + + includedProducts.Remove(product.ProductCode); + return includedProducts; + } + + private void AddIncludedProducts(Product product, Dictionary includedProducts) + { + if (!includedProducts.ContainsKey(product.ProductCode)) + { + includedProducts.Add(product.ProductCode, product); + foreach (Product p in product.Includes) + { + AddIncludedProducts(p, includedProducts); + } + } + } + + private string MapLCIDToCultureName(int lcid) + { + if (lcid == 0) + return Util.DefaultCultureInfo.Name; + + try + { + CultureInfo ci = new CultureInfo(lcid); + return ci.Name; + } + catch (ArgumentException) + { + // Can't convert this lcid to a CultureInfo? Just return the default CultureInfo instead... + return Util.DefaultCultureInfo.Name; + } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/BuildMessage.cs b/src/XMakeTasks/BootstrapperUtil/BuildMessage.cs new file mode 100644 index 00000000000..250ad90a8fc --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/BuildMessage.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// Represents messages that occur during the BootstrapperBuilder's Build operation. + /// + public class BuildMessage : IBuildMessage + { + private BuildMessageSeverity _severity; + private string _message; + private string _helpKeyword; + private string _helpCode; + private int _helpId; + + private static readonly Regex s_msbuildMessageCodePattern = new Regex(@"(\d+)$"); + + private BuildMessage(BuildMessageSeverity severity, string message, string helpKeyword, string helpCode) + { + _severity = severity; + _message = message; + _helpKeyword = helpKeyword; + _helpCode = helpCode; + if (!String.IsNullOrEmpty(_helpCode)) + { + Match match = s_msbuildMessageCodePattern.Match(_helpCode); + if (match.Success) + { + _helpId = int.Parse(match.Value, CultureInfo.InvariantCulture); + } + } + } + + internal static BuildMessage CreateMessage(BuildMessageSeverity severity, string resourceName, params object[] args) + { + string helpCode; + string helpKeyword; + string message = ResourceUtilities.FormatResourceString(out helpCode, out helpKeyword, resourceName, args); + + return new BuildMessage(severity, message, helpKeyword, helpCode); + } + + /// + /// This severity of this build message + /// + public BuildMessageSeverity Severity + { + get { return _severity; } + } + + /// + /// A text string describing the details of the build message + /// + public string Message + { + get { return _message; } + } + + /// + /// The MSBuild F1-help keyword for the host IDE, or null + /// + public string HelpKeyword + { + get { return _helpKeyword; } + } + + /// + /// The MSBuild help id for the host IDE + /// + public int HelpId + { + get { return _helpId; } + } + + internal string HelpCode + { + get { return _helpCode; } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/BuildResults.cs b/src/XMakeTasks/BootstrapperUtil/BuildResults.cs new file mode 100644 index 00000000000..23cfbf4f9d8 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/BuildResults.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// Represents the results of the Build operation of the BootstrapperBuilder. + /// + [ComVisible(true), GuidAttribute("FAD7BA7C-CA00-41e0-A5EF-2DA9A74E58E6"), ClassInterface(ClassInterfaceType.None)] + public class BuildResults : IBuildResults + { + private bool _succeeded; + private string _keyFile; + private ArrayList _componentFiles; + private ArrayList _messages; + + internal BuildResults() + { + _succeeded = false; + _keyFile = string.Empty; + _componentFiles = new ArrayList(); + _messages = new ArrayList(); + } + + /// + /// Returns true if the bootstrapper build was successful, false otherwise + /// + public bool Succeeded + { + get { return _succeeded; } + } + + /// + /// The file path to the generated primary bootstrapper file + /// + /// Path to setup.exe + public string KeyFile + { + get { return _keyFile; } + } + + /// + /// File paths to copied component installer files + /// + /// Path to component files + public string[] ComponentFiles + { + get + { + if (_componentFiles.Count == 0) + return null; + + string[] files = new string[_componentFiles.Count]; + _componentFiles.CopyTo(files); + return files; + } + } + + /// + /// The build messages generated from a bootstrapper build + /// + public BuildMessage[] Messages + { + get + { + if (_messages.Count == 0) + return null; + + BuildMessage[] msgs = new BuildMessage[_messages.Count]; + _messages.CopyTo(msgs); + return msgs; + } + } + + internal void AddMessage(BuildMessage message) + { + _messages.Add(message); + } + + internal void AddComponentFiles(string[] filePaths) + { + _componentFiles.AddRange(filePaths); + } + + internal void BuildSucceeded() + { + _succeeded = true; + } + + internal void SetKeyFile(string filePath) + { + _keyFile = filePath; + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/BuildSettings.cs b/src/XMakeTasks/BootstrapperUtil/BuildSettings.cs new file mode 100644 index 00000000000..7bf87c7c70d --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/BuildSettings.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// This class defines the settings for the bootstrapper build operation. + /// + [ComVisible(true), GuidAttribute("5D13802C-C830-4b41-8E7A-F69D9DD6A095"), ClassInterface(ClassInterfaceType.None)] + public class BuildSettings : IBuildSettings + { + private string _applicationName = null; + private string _applicationFile = null; + private bool _applicationRequiresElevation = false; + private string _applicationUrl = null; + private ComponentsLocation _componentsLocation = ComponentsLocation.HomeSite; + private string _componentsUrl = null; + private bool _fCopyComponents = false; + private int _lcid = Util.DefaultCultureInfo.LCID; + private int _fallbackLCID = Util.DefaultCultureInfo.LCID; + private string _outputPath = null; + private string _supportUrl = null; + private ProductBuilderCollection _productBuilders = null; + private bool _fValidate = false; + private string _culture = null; + private string _fallbackCulture = null; + + public BuildSettings() + { + _productBuilders = new ProductBuilderCollection(); + } + + /// + /// The name of the application to be installed after the bootstrapper has installed all required components. If no application is to be installed, this parameter may be null + /// + public string ApplicationName + { + get { return _applicationName; } + set { _applicationName = value; } + } + + /// + /// The file to be installed after the bootstrapper has installed the required components. It is assumed that this file path is relative to the bootstrapper source path. If no application is to be installed, this parameter may be null + /// + public string ApplicationFile + { + get { return _applicationFile; } + set { _applicationFile = value; } + } + + /// + /// A value of true indicates that the application should require elevation to install on Vista. + /// + public bool ApplicationRequiresElevation + { + get { return _applicationRequiresElevation; } + set { _applicationRequiresElevation = value; } + } + + /// + /// The expected source location if the bootstrapper is published to a website. It is expected that the ApplicationFile, if specified, will be published to the location consistent to this value. If ComponentsLocation is Relative, required component files will also be published in a manner consistent with this value. This value may be null if setup.exe is not to be published to the web + /// + public string ApplicationUrl + { + get { return _applicationUrl; } + set { _applicationUrl = value; } + } + + /// + /// Specifies the install time location for bootstrapper components + /// + public ComponentsLocation ComponentsLocation + { + get { return _componentsLocation; } + set { _componentsLocation = value; } + } + + /// + /// The location the bootstrapper install time will use for components if ComponentsLocation is "Absolute" + /// + public string ComponentsUrl + { + get { return _componentsUrl; } + set { _componentsUrl = value; } + } + + /// + /// If true, the bootstrapper components will be copied to the build output directory. If false, the files will not be copied + /// + public bool CopyComponents + { + get { return _fCopyComponents; } + set { _fCopyComponents = value; } + } + + /// + /// The culture identifier for the bootstrapper to be built + /// + public int LCID + { + get { return _lcid; } + set { _lcid = value; } + } + + /// + /// The culture identifier to use if the LCID identifier is not available + /// + public int FallbackLCID + { + get { return _fallbackLCID; } + set { _fallbackLCID = value; } + } + + /// + /// The file location to copy output files to + /// + public string OutputPath + { + get { return _outputPath; } + set { _outputPath = value; } + } + + /// + /// The product builders to use for generating the bootstrapper + /// + public ProductBuilderCollection ProductBuilders + { + get { return _productBuilders; } + } + + /// + /// Specifies a URL for the Web site containing support information for the bootstrapper + /// + public string SupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + /// + /// True if the bootstrapper will perform XML validation on the component manifests + /// + public bool Validate + { + get { return _fValidate; } + set { _fValidate = value; } + } + + internal string Culture + { + get { return _culture; } + set { _culture = value; } + } + + internal string FallbackCulture + { + get { return _fallbackCulture; } + set { _fallbackCulture = value; } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/Interfaces.cs b/src/XMakeTasks/BootstrapperUtil/Interfaces.cs new file mode 100644 index 00000000000..712396280b7 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/Interfaces.cs @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// This interface exposes functionality necessary to build a bootstrapper. + /// + [ComVisible(true)] + [Guid("1D202366-5EEA-4379-9255-6F8CDB8587C9"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IBootstrapperBuilder + { + /// + /// Specifies the location of the required bootstrapper files. + /// + /// Path to bootstrapper files. + [DispId(1)] + string Path + { + get; + set; + } + + /// + /// Returns all products available at the current bootstrapper Path + /// + [DispId(4)] + ProductCollection Products + { + get; + } + + /// + /// Generates a bootstrapper based on the specified settings. + /// + /// The properties used to build this bootstrapper. + /// The results of the bootstrapper generation + [DispId(5)] + BuildResults Build(BuildSettings settings); + } + + + /// + /// This interface defines the settings for the bootstrapper build operation. + /// + [ComVisible(true)] + [Guid("87EEBC69-0948-4ce6-A2DE-819162B87CC6"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IBuildSettings + { + /// + /// The name of the application to be installed after the bootstrapper has installed all required components. If no application is to be installed, this parameter may be null + /// + [DispId(1)] + string ApplicationName + { + get; + set; + } + + /// + /// The file to be installed after the bootstrapper has installed the required components. It is assumed that this file path is relative to the bootstrapper source path. If no application is to be installed, this parameter may be null + /// + [DispId(2)] + string ApplicationFile + { + get; + set; + } + + /// + /// The expected source location if the bootstrapper is published to a website. It is expected that the ApplicationFile, if specified, will be published to the location consistent to this value. If ComponentsLocation is Relative, required component files will also be published in a manner consistent with this value. This value may be null if setup.exe is not to be published to the web + /// + [DispId(3)] + string ApplicationUrl + { + get; + set; + } + + /// + /// The location the bootstrapper install time will use for components if ComponentsLocation is "Absolute" + /// + [DispId(4)] + string ComponentsUrl + { + get; + set; + } + + /// + /// If true, the bootstrapper components will be copied to the build output directory. If false, the files will not be copied + /// + [DispId(5)] + bool CopyComponents + { + get; + set; + } + + /// + /// The culture identifier for the bootstrapper to be built + /// + [DispId(6)] + int LCID + { + get; + set; + } + + /// + /// The culture identifier to use if the LCID identifier is not available + /// + [DispId(7)] + int FallbackLCID + { + get; + set; + } + + /// + /// The file location to copy output files to + /// + [DispId(8)] + string OutputPath + { + get; + set; + } + + /// + /// The product builders to use for generating the bootstrapper + /// + [DispId(9)] + ProductBuilderCollection ProductBuilders + { + get; + } + + /// + /// True if the bootstrapper will perform XML validation on the component manifests + /// + [DispId(10)] + bool Validate + { + get; + set; + } + + /// + /// Specifies the install time location for bootstrapper components + /// + [DispId(11)] + ComponentsLocation ComponentsLocation + { + get; + set; + } + + /// + /// Specifies a URL for the Web site containing support information for the bootstrapper + /// + [DispId(12)] + string SupportUrl + { + get; + set; + } + + /// + /// A value of true indicates that the application should require elevation to install on Windows Vista. + /// + [DispId(13)] + bool ApplicationRequiresElevation + { + get; + set; + } + } + + + + + /// + /// This interface represents a product in the found by the BootstrapperBuilder in the Path property. + /// + [ComVisible(true)] + [Guid("9E81BE3D-530F-4a10-8349-5D5947BA59AD"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IProduct + { + /// + /// The ProductBuilder representation of this Product + /// + [DispId(1)] + ProductBuilder ProductBuilder + { + get; + } + + /// + /// A human-readable name for this product + /// + [DispId(2)] + string Name + { + get; + } + + /// + /// A string specifying the unique identifier of this product + /// + [DispId(3)] + string ProductCode + { + get; + } + + /// + /// All products which this product also installs + /// + [DispId(4)] + ProductCollection Includes + { + get; + } + } + + /// + /// This interface describes a collection of Product objects. This collection is a closed set that is generated by the BootstrapperBuilder based on the Path property. The client cannot add or remove items from this collection. + /// + [ComVisible(true)] + [Guid("63F63663-8503-4875-814C-09168E595367"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IProductCollection + { + /// + /// Gets the number of elements actually contained in the ProductCollection + /// + [DispId(1)] + int Count + { + get; + } + + /// + /// Gets the Product at the specified index. + /// + /// The zero-based index of the element to get + /// The Product at the specified index + [DispId(2)] + Product Item(int index); + + /// + /// Gets the product with the specified product code + /// + /// + /// The product with the given name, null if the spercified product code is not found + [DispId(3)] + Product Product(string productCode); + } + + /// + /// This interface represents a buildable version of a Product. Used for the BootstrapperBuilder's Build method. + /// + [ComVisible(true)] + [Guid("0777432F-A60D-48b3-83DB-90326FE8C96E"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IProductBuilder + { + /// + /// The product corresponding to this builder + /// + [DispId(1)] + Product Product + { + get; + } + } + + /// + /// This class contains a collection of ProductBuilder objects. Used for the BootstrapperBuilder's Build method. + /// + [ComVisible(true)] + [Guid("0D593FC0-E3F1-4dad-A674-7EA4D327F79B"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IProductBuilderCollection + { + /// + /// Adds a builder to the collection + /// + /// The ProductBuilder to add to the collection + [DispId(2)] + void Add(ProductBuilder builder); + } + + /// + /// Represents the results of the build operation of the BootstrapperBuilder. + /// + [ComVisible(true)] + [Guid("586B842C-D9C7-43b8-84E4-9CFC3AF9F13B"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IBuildResults + { + /// + /// Returns true if the bootstrapper build was successful, false otherwise + /// + [DispId(1)] + bool Succeeded + { + get; + } + + /// + /// The file path to the generated primary bootstrapper file + /// + /// Path to setup.exe + [DispId(2)] + string KeyFile + { + get; + } + + /// + /// File paths to copied component installer files + /// + /// Path to component files + [DispId(3)] + string[] ComponentFiles + { + get; + } + + /// + /// The build messages generated from a bootstrapper build + /// + [DispId(4)] + BuildMessage[] Messages + { + get; + } + } + + /// + /// Represents messages that occur during the BootstrapperBuilder's Build operation. + /// + [ComVisible(true)] + [Guid("E3C981EA-99E6-4f48-8955-1AAFDFB5ACE4"), + InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)] + public interface IBuildMessage + { + /// + /// This severity of this build message + /// + [DispId(1)] + BuildMessageSeverity Severity + { + get; + } + + /// + /// A text string describing the details of the build message + /// + [DispId(2)] + string Message + { + get; + } + + /// + /// The MSBuild F1-help keyword for the host IDE, or null + /// + [DispId(3)] + string HelpKeyword + { + get; + } + + /// + /// The MSBuild help id for the host IDE + /// + [DispId(4)] + int HelpId + { + get; + } + } + + /// + /// This enumeration provides three levels of importance for build messages. + /// + [ComVisible(true)] + [Guid("936D32F9-1A68-4d5e-98EA-044AC9A1AADA")] + public enum BuildMessageSeverity + { + /// + /// Indicates that the message corresponds to build information + /// + Info, + /// + /// Indicates that the message corresponds to a build warning + /// + Warning, + /// + /// Indicates that the message corresponds to a build error + /// + Error + }; + + /// + /// This enumeration describes the way required components will be published + /// + [ComVisible(true)] + [Guid("12F49949-7B60-49CD-B6A0-2B5E4A638AAF")] + public enum ComponentsLocation + { + /// + /// Products will be found according to the redist vendor's designated URL + /// + HomeSite, + /// + /// Products will be located relative to generated bootstrapper + /// + Relative, + /// + /// All products will be located at s specific location + /// + Absolute + }; +} diff --git a/src/XMakeTasks/BootstrapperUtil/NativeMethods.cs b/src/XMakeTasks/BootstrapperUtil/NativeMethods.cs new file mode 100644 index 00000000000..08149eda291 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/NativeMethods.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + internal static class NativeMethods + { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr BeginUpdateResourceW(String fileName, bool deleteExistingResource); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool UpdateResourceW(IntPtr hUpdate, IntPtr lpType, String lpName, short wLanguage, byte[] data, int cbData); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool EndUpdateResource(IntPtr hUpdate, bool fDiscard); + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/Package.cs b/src/XMakeTasks/BootstrapperUtil/Package.cs new file mode 100644 index 00000000000..d46ba80d4eb --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/Package.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Xml; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + [ComVisible(false)] + internal class Package + { + private string _name; + private string _culture; + private Product _product; + private XmlNode _node; + private XmlValidationResults _validationResults; + + public Package(Product product, XmlNode node, XmlValidationResults validationResults, string name, string culture) + { + _product = product; + _node = node; + _name = name; + _culture = culture; + _validationResults = validationResults; + } + + internal XmlNode Node + { + get { return _node; } + } + + public string Name + { + get { return _name; } + } + + public string Culture + { + get { return _culture; } + } + + public Product Product + { + get { return _product; } + } + + internal bool ValidationPassed + { + get + { + if (_validationResults == null) + return true; + return _validationResults.ValidationPassed; + } + } + + internal XmlValidationResults ValidationResults + { + get { return _validationResults; } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/PackageCollection.cs b/src/XMakeTasks/BootstrapperUtil/PackageCollection.cs new file mode 100644 index 00000000000..64f2457ce39 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/PackageCollection.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + internal class PackageCollection : IEnumerable + { + private ArrayList _list; + private Hashtable _cultures; + + public PackageCollection() + { + _list = new ArrayList(); + _cultures = new Hashtable(); + } + + public Package Item(int index) + { + return (Package)_list[index]; + } + + public Package Package(string culture) + { + if (_cultures.Contains(culture.ToLowerInvariant())) + { + return (Package)_cultures[culture.ToLowerInvariant()]; + } + + return null; + } + + public int Count + { + get { return _list.Count; } + } + + internal void Add(Package package) + { + if (!_cultures.Contains(package.Culture.ToLowerInvariant())) + { + _list.Add(package); + _cultures.Add(package.Culture.ToLowerInvariant(), package); + } + else + { + Debug.Fail("Package with culture " + package.Culture + " has already been added."); + } + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/Product.cs b/src/XMakeTasks/BootstrapperUtil/Product.cs new file mode 100644 index 00000000000..5c1f28bf1ff --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/Product.cs @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + internal enum CopyAllFilesType + { + CopyAllFilesFalse, CopyAllFilesTrue, CopyAllFilesIfNotHomeSite + }; + + /// + /// This class represents a product in the found by the BootstrapperBuilder in the Path property. + /// + [ComVisible(true), GuidAttribute("532BF563-A85D-4088-8048-41F51AC5239F"), ClassInterface(ClassInterfaceType.None)] + public class Product : IProduct + { + private XmlNode _node; + private string _productCode; + private PackageCollection _packages; + private ProductCollection _includes; + private List> _dependencies; + private ArrayList _missingDependencies; + private Hashtable _cultures; + private CopyAllFilesType _copyAllPackageFiles; + private ProductValidationResults _validationResults; + + public Product() + { + Debug.Fail("Products are not to be created in this fashion. Please use IBootstrapperBuilder.Products instead."); + throw new InvalidOperationException(); + } + + internal Product(XmlNode node, string code, ProductValidationResults validationResults, string copyAll) + { + _node = node; + _packages = new PackageCollection(); + _includes = new ProductCollection(); + _dependencies = new List>(); + _missingDependencies = new ArrayList(); + _productCode = code; + _validationResults = validationResults; + _cultures = new Hashtable(); + if (copyAll == "IfNotHomeSite") + _copyAllPackageFiles = CopyAllFilesType.CopyAllFilesIfNotHomeSite; + else if (copyAll == "false") + _copyAllPackageFiles = CopyAllFilesType.CopyAllFilesFalse; + else + _copyAllPackageFiles = CopyAllFilesType.CopyAllFilesTrue; + } + + internal XmlNode Node + { + get { return _node; } + } + + internal CopyAllFilesType CopyAllPackageFiles + { + get { return _copyAllPackageFiles; } + } + + /// + /// The ProductBuilder representation of this Product + /// + public ProductBuilder ProductBuilder + { + get { return new ProductBuilder(this); } + } + + /// + /// A string specifying the unique identifier of this product + /// + public string ProductCode + { + get { return _productCode; } + } + + /// + /// A human-readable name for this product + /// + public string Name + { + get + { + CultureInfo culture = Util.DefaultCultureInfo; + Package p = _packages.Package(culture.Name); + + if (p != null) + { + return p.Name; + } + + while (culture != null && culture != CultureInfo.InvariantCulture) + { + p = _packages.Package(culture.Parent.Name); + + if (p != null) + { + return p.Name; + } + + culture = culture.Parent; + } + + if (_packages.Count > 0) + { + return _packages.Item(0).Name; + } + + return _productCode.ToString(); + } + } + + /// + /// All products which this product also installs + /// + public ProductCollection Includes + { + get { return _includes; } + } + + internal List> Dependencies + { + get { return _dependencies; } + } + + internal bool ContainsCulture(string culture) + { + return _cultures.Contains(culture.ToLowerInvariant()); + } + + internal bool ContainsDependencies(List dependenciesToCheck) + { + foreach (List d in _dependencies) + { + bool found = true; + foreach (Product p in d) + { + bool containedInDependencies = false; + foreach (Product pd in dependenciesToCheck) + { + if (p._productCode == pd._productCode) + { + containedInDependencies = true; + break; + } + } + if (!containedInDependencies) + { + found = false; + break; + } + } + + if (found) + { + return true; + } + } + + return false; + } + + internal ArrayList MissingDependencies + { + get + { + return _missingDependencies; + } + } + + internal void AddPackage(Package package) + { + if (package == null || String.IsNullOrEmpty(package.Culture)) + throw new ArgumentNullException("package"); + + if (!_cultures.Contains(package.Culture.ToLowerInvariant())) + { + _packages.Add(package); + _cultures.Add(package.Culture.ToLowerInvariant(), package); + } + else + { + Debug.WriteLine(String.Format(CultureInfo.CurrentCulture, "A package with culture '{0}' has already been added to product '{1}'", package.Culture.ToLowerInvariant(), ProductCode)); + } + } + + internal void AddIncludedProduct(Product product) + { + _includes.Add(product); + } + + internal void AddDependentProduct(Product product) + { + List newDependency = new List(); + newDependency.Add(product); + _dependencies.Add(newDependency); + } + + internal void AddMissingDependency(ArrayList productCodes) + { + bool found = false; + foreach (ArrayList md in _missingDependencies) + { + bool hasAll = true; + foreach (string dep in md) + { + if (!productCodes.Contains(dep)) + { + hasAll = false; + break; + } + } + + if (hasAll) + { + found = true; + break; + } + } + + if (!found) + { + _missingDependencies.Add(productCodes); + } + } + + internal PackageCollection Packages + { + get { return _packages; } + } + + internal XmlValidationResults GetPackageValidationResults(string culture) + { + if (_validationResults == null) + return null; + return _validationResults.PackageResults(culture); + } + + internal bool ValidationPassed + { + get + { + if (_validationResults == null) + return true; + return _validationResults.ValidationPassed; + } + } + + internal ProductValidationResults ValidationResults + { + get { return _validationResults; } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/ProductBuilder.cs b/src/XMakeTasks/BootstrapperUtil/ProductBuilder.cs new file mode 100644 index 00000000000..0fc5b866e91 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/ProductBuilder.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// A buildable version of a Product. Used for the BootstrapperBuilder's Build method. + /// + public class ProductBuilder : IProductBuilder + { + private Product _product; + private string _culture; + + internal ProductBuilder(Product product) + { + _product = product; + _culture = string.Empty; + } + + internal ProductBuilder(Product product, string culture) + { + _product = product; + _culture = culture; + } + + /// + /// The Product corresponding to this ProductBuilder + /// + public Product Product + { + get { return _product; } + } + + internal string Name + { + get { return _product.Name; } + } + + internal string ProductCode + { + get { return _product.ProductCode; } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/ProductBuilderCollection.cs b/src/XMakeTasks/BootstrapperUtil/ProductBuilderCollection.cs new file mode 100644 index 00000000000..919ea3657b9 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/ProductBuilderCollection.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// This class contains a collection of ProductBuilder objects. Used for the BootstrapperBuilder's Build method. + /// + [ComVisible(true), GuidAttribute("D25C0741-99CA-49f7-9460-95E5F25EEF43"), ClassInterface(ClassInterfaceType.None)] + public class ProductBuilderCollection : IProductBuilderCollection, IEnumerable + { + private ArrayList _list; + + internal ProductBuilderCollection() + { + _list = new ArrayList(); + } + + /// + /// Adds a ProductBuilder to the ProductBuilderCollection + /// + /// The ProductBuilder to add to this collection + public void Add(ProductBuilder builder) + { + _list.Add(builder); + } + + /// + /// Returns an enumerator that can iterate through the ProductBuilderCollection + /// + /// An enumerator that can iterate through the ProductBuilderCollection + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + internal int Count + { + get { return _list.Count; } + } + + internal ProductBuilder Item(int index) + { + return (ProductBuilder)_list[index]; + } + + internal void Insert(int index, ProductBuilder builder) + { + _list.Insert(index, builder); + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/ProductCollection.cs b/src/XMakeTasks/BootstrapperUtil/ProductCollection.cs new file mode 100644 index 00000000000..118c8d50018 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/ProductCollection.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// This class contains a collection of Product objects. This collection is a closed set that is generated by the BootstrapperBuilder based on the Path property. The client cannot add or remove items from this collection. + /// + [ComVisible(true), GuidAttribute("EFFA164B-3E87-4195-88DB-8AC004DDFE2A"), ClassInterface(ClassInterfaceType.None)] + public class ProductCollection : IProductCollection, IEnumerable + { + private ArrayList _list; + private Hashtable _table; + + internal ProductCollection() + { + _list = new ArrayList(); + _table = new Hashtable(); + } + + internal void Add(Product product) + { + if (!_table.Contains(product.ProductCode.ToUpperInvariant())) + { + _list.Add(product); + _table.Add(product.ProductCode.ToUpperInvariant(), product); + } + else + { + System.Diagnostics.Debug.WriteLine("Product '{0}' has already been added to the product list", product.ProductCode.ToUpperInvariant()); + } + } + + /// + /// Gets the Product at the specified index. + /// + /// The zero-based index of the element to get + /// The Product at the specified index + public Product Item(int index) + { + return (Product)_list[index]; + } + + /// + /// Gets the product with the specified product code + /// + /// + /// The product with the given name, null if the spercified product code is not found + public Product Product(string productCode) + { + return (Product)_table[productCode.ToUpperInvariant()]; + } + + /// + /// Gets the number of elements actually contained in the ProductCollection + /// + public int Count + { + get { return _list.Count; } + } + + internal void Clear() + { + _list.Clear(); + _table.Clear(); + } + + /// + /// Returns an enumerator that can iterate through the ProductCollection + /// + /// An enumerator that can iterate through the ProductCollection + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/ResourceUpdater.cs b/src/XMakeTasks/BootstrapperUtil/ResourceUpdater.cs new file mode 100644 index 00000000000..9c9c5d98db5 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/ResourceUpdater.cs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + internal class ResourceUpdater + { + private const int ERROR_SHARING_VIOLATION = -2147024864; + private ArrayList _stringResources; + private ArrayList _fileResources; + + public ResourceUpdater() + { + _stringResources = new ArrayList(); + _fileResources = new ArrayList(); + } + + public void AddStringResource(int type, string name, string data) + { + _stringResources.Add(new StringResource(type, name, data)); + } + + public void AddFileResource(string filename, string key) + { + _fileResources.Add(new FileResource(filename, key)); + } + + public bool UpdateResources(string filename, BuildResults results) + { + bool returnValue = true; + int beginUpdateRetries = 20; // Number of retries + const int beginUpdateRetryInterval = 100; // In milliseconds + bool endUpdate = false; // Only call EndUpdateResource() if this is true + + // Environment.CurrentDirectory has previously been set to the project location + string filePath = System.IO.Path.Combine(Environment.CurrentDirectory, filename); + + if (_stringResources.Count == 0 && _fileResources.Count == 0) + return true; + IntPtr hUpdate = IntPtr.Zero; + + try + { + hUpdate = NativeMethods.BeginUpdateResourceW(filePath, false); + while (IntPtr.Zero == hUpdate && Marshal.GetHRForLastWin32Error() == ResourceUpdater.ERROR_SHARING_VIOLATION && beginUpdateRetries > 0) // If it equals 0x80070020 (ERROR_SHARING_VIOLATION), sleep & retry + { + // This warning can be useful for debugging, but shouldn't be displayed to an actual user + // results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Warning, "GenerateBootstrapper.General", String.Format("Unable to begin updating resource for {0} with error {1:X}, trying again after short sleep", filename, Marshal.GetHRForLastWin32Error()))); + hUpdate = NativeMethods.BeginUpdateResourceW(filePath, false); + beginUpdateRetries--; + Thread.Sleep(beginUpdateRetryInterval); + } + // If after all that we still failed, throw a build error + if (IntPtr.Zero == hUpdate) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", String.Format("Unable to begin updating resource for {0} with error {1:X}", filename, Marshal.GetHRForLastWin32Error()))); + return false; + } + + endUpdate = true; + + if (hUpdate != IntPtr.Zero) + { + foreach (StringResource resource in _stringResources) + { + byte[] data = StringToByteArray(resource.Data); + + if (!NativeMethods.UpdateResourceW(hUpdate, (IntPtr)resource.Type, resource.Name, 0, data, data.Length)) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", String.Format("Unable to update resource for {0} with error {1:X}", filename, Marshal.GetHRForLastWin32Error()))); + return false; + } + } + + if (_fileResources.Count > 0) + { + int index = 0; + byte[] countArray = StringToByteArray(_fileResources.Count.ToString("G", CultureInfo.InvariantCulture)); + if (!NativeMethods.UpdateResourceW(hUpdate, (IntPtr)42, "COUNT", 0, countArray, countArray.Length)) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", String.Format("Unable to update count resource for {0} with error {1:X}", filename, Marshal.GetHRForLastWin32Error()))); + return false; + } + + foreach (FileResource resource in _fileResources) + { + // Read in the file data + int fileLength = 0; + byte[] fileContent = null; + using (FileStream fs = System.IO.File.OpenRead(resource.Filename)) + { + fileLength = (int)fs.Length; + fileContent = new byte[fileLength]; + + fs.Read(fileContent, 0, fileLength); + } + + // Update the resources to include this file's data + string dataName = string.Format(CultureInfo.InvariantCulture, "FILEDATA{0}", index); + + if (!NativeMethods.UpdateResourceW(hUpdate, (IntPtr)42, dataName, 0, fileContent, fileLength)) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", String.Format("Unable to update data resource for {0} with error {1:X}", filename, Marshal.GetHRForLastWin32Error()))); + return false; + } + + // Add this file's key to the resources + string keyName = string.Format(CultureInfo.InvariantCulture, "FILEKEY{0}", index); + byte[] data = StringToByteArray(resource.Key); + if (!NativeMethods.UpdateResourceW(hUpdate, (IntPtr)42, keyName, 0, data, data.Length)) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", String.Format("Unable to update key resource for {0} with error {1:X}", filename, Marshal.GetHRForLastWin32Error()))); + return false; + } + + index++; + } + } + } + } + finally + { + if (endUpdate && !NativeMethods.EndUpdateResource(hUpdate, false)) + { + results.AddMessage(BuildMessage.CreateMessage(BuildMessageSeverity.Error, "GenerateBootstrapper.General", String.Format("Unable to finish updating resource for {0} with error {1:X}", filename, Marshal.GetHRForLastWin32Error()))); + returnValue = false; + } + } + + return returnValue; + } + + private byte[] StringToByteArray(string str) + { + byte[] strBytes = System.Text.Encoding.Unicode.GetBytes(str); + byte[] data = new byte[strBytes.Length + 2]; + strBytes.CopyTo(data, 0); + data[data.Length - 2] = 0; + data[data.Length - 1] = 0; + return data; + } + + private class StringResource + { + public int Type; + public string Name; + public string Data; + + public StringResource(int type, string name, string data) + { + Type = type; + Name = name; + Data = data; + } + } + + private class FileResource + { + public string Filename; + public string Key; + + public FileResource(string filename, string key) + { + Filename = filename; + Key = key; + } + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/Util.cs b/src/XMakeTasks/BootstrapperUtil/Util.cs new file mode 100644 index 00000000000..fb5558c297f --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/Util.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + internal static class Util + { + private const string BOOTSTRAPPER_REGISTRY_PATH_BASE = "Software\\Microsoft\\GenericBootstrapper\\"; + private const string BOOTSTRAPPER_WOW64_REGISTRY_PATH_BASE = "Software\\Wow6432Node\\Microsoft\\GenericBootstrapper\\"; + + private const string BOOTSTRAPPER_REGISTRY_PATH_VERSION_VS2010 = "4.0"; + + private const string REGISTRY_DEFAULTPATH = "Path"; + + private static string s_defaultPath; + + public static string AddTrailingChar(string str, char ch) + { + if (str.LastIndexOf(ch) == str.Length - 1) + return str; + return str + ch; + } + + public static bool IsUncPath(string path) + { + if (String.IsNullOrEmpty(path)) + return false; + + + try + { + System.Uri uri = new System.Uri(path); + return uri.IsUnc; + } + catch (UriFormatException) + { + return false; + } + } + + + public static bool IsWebUrl(string path) + { + return path.StartsWith("http://", StringComparison.Ordinal) || path.StartsWith("https://", StringComparison.Ordinal); + } + + public static CultureInfo GetCultureInfoFromString(string cultureName) + { + try + { + CultureInfo ci = new System.Globalization.CultureInfo(cultureName); + return ci; + } + catch (ArgumentException) + { + } + + return null; + } + + public static CultureInfo DefaultCultureInfo + { + get + { + return System.Threading.Thread.CurrentThread.CurrentUICulture; + } + } + + // This is the 4.0 property and will always point to the Dev10 registry key so that we don't break backwards compatibility. + // Applications relying on 4.5 will need to use the new method that is introduced in 4.5 + public static string DefaultPath + { + get + { + if (String.IsNullOrEmpty(s_defaultPath)) + { + s_defaultPath = ReadRegistryString(Win32.Registry.LocalMachine, String.Concat(BOOTSTRAPPER_REGISTRY_PATH_BASE, BOOTSTRAPPER_REGISTRY_PATH_VERSION_VS2010), REGISTRY_DEFAULTPATH); + if (!String.IsNullOrEmpty(s_defaultPath)) + return s_defaultPath; + + s_defaultPath = ReadRegistryString(Win32.Registry.LocalMachine, String.Concat(BOOTSTRAPPER_WOW64_REGISTRY_PATH_BASE, BOOTSTRAPPER_REGISTRY_PATH_VERSION_VS2010), REGISTRY_DEFAULTPATH); + if (!String.IsNullOrEmpty(s_defaultPath)) + return s_defaultPath; + + s_defaultPath = Environment.CurrentDirectory; + } + + return s_defaultPath; + } + } + + // A new method in 4.5 to get the default path for bootstrapper packages. + // This method is not going to cache the path as it could be different depending on the Visual Studio version. + public static string GetDefaultPath(string visualStudioVersion) + { + // if the Visual Studio Version is not a valid string, we will fall back to using the v4.0 property + if (String.IsNullOrEmpty(visualStudioVersion)) + { + return DefaultPath; + } + + // With version 11.0 we start a direct mapping between the VS version and the registry key we use. + // For Dev10, we use 4.0. + + int majorVersion = 0; + int dotIndex = visualStudioVersion.IndexOf('.'); + if (dotIndex < 0) + { + dotIndex = visualStudioVersion.Length; + } + if (Int32.TryParse(visualStudioVersion.Substring(0, dotIndex), out majorVersion) && (majorVersion < 11)) + { + visualStudioVersion = BOOTSTRAPPER_REGISTRY_PATH_VERSION_VS2010; + } + + string defaultPath; + + defaultPath = ReadRegistryString(Win32.Registry.LocalMachine, String.Concat(BOOTSTRAPPER_REGISTRY_PATH_BASE, visualStudioVersion), REGISTRY_DEFAULTPATH); + if (!String.IsNullOrEmpty(defaultPath)) + return defaultPath; + + defaultPath = ReadRegistryString(Win32.Registry.LocalMachine, String.Concat(BOOTSTRAPPER_WOW64_REGISTRY_PATH_BASE, visualStudioVersion), REGISTRY_DEFAULTPATH); + if (!String.IsNullOrEmpty(defaultPath)) + return defaultPath; + + return Environment.CurrentDirectory; + } + + + + private static string ReadRegistryString(Win32.RegistryKey key, string path, string registryValue) + { + RegistryKey subKey = key.OpenSubKey(path, false); + + if (subKey != null) + { + object oValue = subKey.GetValue(registryValue); + if (oValue != null && subKey.GetValueKind(registryValue) == RegistryValueKind.String) + { + return (string)oValue; + } + } + + return null; + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/productvalidationresults.cs b/src/XMakeTasks/BootstrapperUtil/productvalidationresults.cs new file mode 100644 index 00000000000..ab6a9c76f4e --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/productvalidationresults.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Xml; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// Handles and stores xml validation events for a product, and contains the XmlValidationResults of a package. + /// + internal sealed class ProductValidationResults : XmlValidationResults + { + private Hashtable _packageValidationResults; + + public ProductValidationResults(string filePath) : base(filePath) + { + _packageValidationResults = new Hashtable(); + } + + /// + /// Adds the validation results of a package of the specified culture into the ProductValidationResults. + /// + /// The culture of the XmlValidationResults to add. + /// The vaue of the results to add. + public void AddPackageResults(string culture, XmlValidationResults results) + { + if (!_packageValidationResults.Contains(culture)) + { + _packageValidationResults.Add(culture, results); + } + else + { + System.Diagnostics.Debug.Fail("Validation results have already been added for culture '{0}'", culture); + } + } + + /// + /// Gets the XmlValidationResults for the specified culture. + /// + /// The culture of the XmlValidationResults to get. + /// The XmlValidationResults associated with the specified culture. + public XmlValidationResults PackageResults(string culture) + { + return (XmlValidationResults)_packageValidationResults[culture]; + } + } +} diff --git a/src/XMakeTasks/BootstrapperUtil/xmltoconfig.xsl b/src/XMakeTasks/BootstrapperUtil/xmltoconfig.xsl new file mode 100644 index 00000000000..3e6a51785ef --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/xmltoconfig.xsl @@ -0,0 +1,72 @@ + + + + +%NEWLINE% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Begin + + + + + + End + + + + + + + + + =" + + " + + + + \ No newline at end of file diff --git a/src/XMakeTasks/BootstrapperUtil/xmlvalidationresults.cs b/src/XMakeTasks/BootstrapperUtil/xmlvalidationresults.cs new file mode 100644 index 00000000000..e113b7d6323 --- /dev/null +++ b/src/XMakeTasks/BootstrapperUtil/xmlvalidationresults.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Xml; + +namespace Microsoft.Build.Tasks.Deployment.Bootstrapper +{ + /// + /// Handles and stores XML validation events. + /// + internal class XmlValidationResults + { + private string _filePath; + private ArrayList _validationErrors; + private ArrayList _validationWarnings; + + /// + /// Constructor which includes the path to the file being validated. + /// + /// The file which is being validated. + public XmlValidationResults(string filePath) + { + _filePath = filePath; + _validationErrors = new ArrayList(); + _validationWarnings = new ArrayList(); + } + + /// + /// Gets a string containing the name of the file being validated. + /// + /// The name of the file being validated. + public string FilePath + { + get { return _filePath; } + } + + /// + /// The delegate which will handle validation events. + /// + public void SchemaValidationEventHandler(object sender, System.Xml.Schema.ValidationEventArgs e) + { + if (e.Severity == System.Xml.Schema.XmlSeverityType.Error) + { + _validationErrors.Add(e.Message); + } + else + { + _validationWarnings.Add(e.Message); + } + } + + /// + /// Gets all of the validation errors of the file being validated. + /// + /// An array of type string, containing all of the validation errors. + /// This method uses ArrayList.Copy to copy the errors. + public string[] ValidationErrors + { + get + { + string[] a = new string[_validationErrors.Count]; + _validationErrors.CopyTo(a); + return a; + } + } + + /// + /// Gets a value indicating if there were no validation errors or warnings. + /// + /// true if there were no validation errors or warnings; otherwise false. The default value is false. + public bool ValidationPassed + { + get { return _validationErrors.Count == 0 && _validationWarnings.Count == 0; } + } + + /// + /// Gets all of the validation warnings of the file being validated. + /// + /// An array of type string, containing all of the validation warnings. + /// This method uses ArrayList.Copy to copy the warnings. + public string[] ValidationWarnings + { + get + { + string[] a = new string[_validationWarnings.Count]; + _validationWarnings.CopyTo(a); + return a; + } + } + } +} diff --git a/src/XMakeTasks/BuildCacheDisposeWrapper.cs b/src/XMakeTasks/BuildCacheDisposeWrapper.cs new file mode 100644 index 00000000000..7b77b617ab7 --- /dev/null +++ b/src/XMakeTasks/BuildCacheDisposeWrapper.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// This class is an IDisposable object which will call the delegate which is passed into it +// when its dispose method is called. +// +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Create a wrapper so that when dispose is called we execute the delegate. + /// + internal class BuildCacheDisposeWrapper : IDisposable + { + /// + /// Has this been disposed + /// + private bool _disposed; + + /// + /// Delegate to call when we are in dispose + /// + private CallDuringDispose _callDuringDispose; + + /// + /// Constructor + /// + internal BuildCacheDisposeWrapper(CallDuringDispose callDuringDispose) + { + _callDuringDispose = callDuringDispose; + } + + /// + /// Delegate to call when we are in dispose + /// + internal delegate void CallDuringDispose(); + + /// + /// IDisposable + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Clear the caches + /// + protected virtual void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (!_disposed && disposing) + { + _disposed = true; + if (_callDuringDispose != null) + { + _callDuringDispose(); + } + } + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/CSharpParserUtilities.cs b/src/XMakeTasks/CSharpParserUtilities.cs new file mode 100644 index 00000000000..4229d2f3a78 --- /dev/null +++ b/src/XMakeTasks/CSharpParserUtilities.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; + +using System.Collections; +using Microsoft.Build.Shared.LanguageParser; + +namespace Microsoft.Build.Tasks +{ + /// + /// Specific-purpose utility functions for parsing C#. + /// + internal static class CSharpParserUtilities + { + /// + /// Parse a C# file and get the first class name, fully qualified with namespace. + /// + /// + /// + static internal ExtractedClassName GetFirstClassNameFullyQualified(Stream binaryStream) + { + try + { + CSharpTokenizer tokens = new CSharpTokenizer(binaryStream, /* forceANSI */ false); + return Extract(tokens); + } + catch (DecoderFallbackException) + { + // There was no BOM and there are non UTF8 sequences. Fall back to ANSI. + CSharpTokenizer tokens = new CSharpTokenizer(binaryStream, /* forceANSI */ true); + return Extract(tokens); + } + } + + + /// + /// Extract the class name. + /// + /// + /// + private static ExtractedClassName Extract(CSharpTokenizer tokens) + { + ParseState state = new ParseState(); + ExtractedClassName result = new ExtractedClassName(); + + foreach (Token t in tokens) + { + // Search first for the namespace keyword + if (t is KeywordToken) + { + state.Reset(); + + if (t.InnerText == "namespace") + { + state.ResolvingNamespace = true; + if (state.InsideConditionalDirective) + { + result.IsInsideConditionalBlock = true; + } + } + else if (t.InnerText == "class") + { + state.ResolvingClass = true; + if (state.InsideConditionalDirective) + { + result.IsInsideConditionalBlock = true; + } + } + } + else if (t is CSharpTokenizer.OpenScopeToken) + { + state.PushNamespacePart(state.Namespace); + state.Reset(); + } + else if (t is CSharpTokenizer.CloseScopeToken) + { + state.Reset(); + state.PopNamespacePart(); + } + else if (t is OperatorOrPunctuatorToken) + { + if (state.ResolvingNamespace) + { + if (t.InnerText == ".") + { + state.Namespace += "."; + } + } + } + else if (t is IdentifierToken) + { + // If we're resolving a namespace, then this is part of the namespace. + if (state.ResolvingNamespace) + { + state.Namespace += t.InnerText; + } + // If we're resolving a class, then we're done. We found the class name. + else if (state.ResolvingClass) + { + // We're done. + result.Name = state.ComposeQualifiedClassName(t.InnerText); + return result; + } + } + else if (t is OpenConditionalDirectiveToken) + { + state.OpenConditionalDirective(); + } + else if (t is CloseConditionalDirectiveToken) + { + state.CloseConditionalDirective(); + } + } + + return result; + } + } +} diff --git a/src/XMakeTasks/CallTarget.cs b/src/XMakeTasks/CallTarget.cs new file mode 100644 index 00000000000..7d58eac480c --- /dev/null +++ b/src/XMakeTasks/CallTarget.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class implements the "CallTarget" task, which invokes other targets within the same + /// project file. Marked RunInMTA because we do not want this task to ever be invoked explicitly + /// on the STA if the RequestBuilder is running on another thread, as this will cause thread + /// id validation checks to fail. + /// + [RunInMTA] + public class CallTarget : TaskExtension + { + /// + /// Default constructor. + /// + public CallTarget() + { + } + + #region Properties + + // A list of targets to build. This is a required parameter. If you want to build the + // default targets, use the task and pass in Projects=$(MSBuildProjectFile). + private string[] _targets = null; + + // outputs of all built targets + private ArrayList _targetOutputs = new ArrayList(); + + // When this is true, instead of calling the engine once to build all the targets, + // we would call the engine once per target. The benefit of this is that + // if one target fails, you can still continue with the remaining targets. + private bool _runEachTargetSeparately = false; + + // If true the cache will be checked for the result and the result will be stored if the operation + // is run + private bool _useResultsCache = false; + + /// + /// The targets to build. + /// + /// Array of target names. + public string[] Targets + { + get + { + return _targets; + } + + set + { + _targets = value; + } + } + + /// + /// Outputs of the targets built in each project. + /// + /// Array of output items. + [Output] + public ITaskItem[] TargetOutputs + { + get + { + return (ITaskItem[])_targetOutputs.ToArray(typeof(ITaskItem)); + } + } + + /// + /// When this is true, instead of calling the engine once to build all the targets (for each project), + /// we would call the engine once per target (for each project). The benefit of this is that + /// if one target fails, you can still continue with the remaining targets. + /// + public bool RunEachTargetSeparately + { + get + { + return _runEachTargetSeparately; + } + + set + { + _runEachTargetSeparately = value; + } + } + + /// + /// If true the cached result will be returned if present and a if MSBuild + /// task is run its result will be cached in a scope (ProjectFileName, GlobalProperties)[TargetNames] + /// as a list of build items + /// + public bool UseResultsCache + { + get + { + return _useResultsCache; + } + set + { + _useResultsCache = value; + } + } + + #endregion + + #region ITask Members + + /// + /// Instructs the MSBuild engine to build one or more targets in the current project. + /// + /// true if all targets built successfully; false if any target fails + public override bool Execute() + { + // Make sure the list of targets was passed in. + if ((Targets == null) || (Targets.Length == 0)) + { + return true; + } + + // This is a list of string[]. That is, each element in the list is a string[]. Each + // string[] represents a set of target names to build. Depending on the value + // of the RunEachTargetSeparately parameter, we each just call the engine to run all + // the targets together, or we call the engine separately for each target. + ArrayList targetLists = Microsoft.Build.Tasks.MSBuild.CreateTargetLists(this.Targets, this.RunEachTargetSeparately); + + ITaskItem[] singleProject = new ITaskItem[1]; + singleProject[0] = null; + // Build the specified targets in the current project. + return Microsoft.Build.Tasks.MSBuild.ExecuteTargets + ( + singleProject, // project = null (current project) + null, // propertiesTable = null + null, // undefineProperties + targetLists, // list of targets to build + false, // stopOnFirstFailure = false + false, // rebaseOutputs = false + this.BuildEngine3, + this.Log, + _targetOutputs, + this.UseResultsCache, + false, + null // toolsVersion = null + ); + } + + #endregion + } +} diff --git a/src/XMakeTasks/CodeTaskFactory.cs b/src/XMakeTasks/CodeTaskFactory.cs new file mode 100644 index 00000000000..03948995f26 --- /dev/null +++ b/src/XMakeTasks/CodeTaskFactory.cs @@ -0,0 +1,968 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A code task factory which uses code dom to generate tasks +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.CodeDom.Compiler; +using System.Reflection; +using System.Xml; +using System.Diagnostics; +using System.IO; + +using Microsoft.Build.Framework; +using System.CodeDom; +using Microsoft.Build.Utilities; +using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Shared; +using System.Collections.Concurrent; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task factory which can take code dom supported languages and create a task out of it + /// + public class CodeTaskFactory : ITaskFactory + { + /// + /// Default assemblies names to reference during inline code compilation - from the .NET Framework + /// + private static readonly string[] s_defaultReferencedFrameworkAssemblyNames = { @"System.Core" }; + + /// + /// Default using's for the code + /// + private readonly string[] _defaultUsingNamespaces = { "System", "System.Collections", "System.Collections.Generic", "System.Text", "System.Linq", "System.IO", "Microsoft.Build.Framework", "Microsoft.Build.Utilities" }; + + /// + /// A collection of task assemblies which have been instantiated by any CodeTaskFactory. Used to prevent us from creating + /// duplicate assemblies. + /// + private static ConcurrentDictionary s_compiledTaskCache = new ConcurrentDictionary(); + + /// + /// The default assemblies to reference when compiling inline code. + /// + private static List s_defaultReferencedAssemblies; + + /// + /// Merged set of assembly reference paths (default + specified) + /// + private string[] _referencedAssemblies; + + /// + /// Merged set of namespaces (default + specified) + /// + private string[] _usingNamespaces; + + /// + /// Type of code fragment, ie Fragment, Class, Method + /// + private string _type; + + /// + /// Is the type a fragment or not + /// + private bool _typeIsFragment; + + /// + /// Is the type a method or not + /// + private bool _typeIsMethod; + + /// + /// By default the language supported is C#, but anything that supports code dom will work + /// + private string _language = "cs"; + + /// + /// The source that will be compiled + /// + private string _sourceCode; + + /// + /// The name of the task for which this is the factory + /// + private string _nameOfTask; + + /// + /// Path to source that is outside the project file + /// + private string _sourcePath; + + /// + /// The using task node from the project file + /// + private XmlNode _taskNode; + + /// + /// The inline source compiled into an in memory assembly + /// + private Assembly _compiledAssembly; + + /// + /// Helper to assist in logging messages + /// + private TaskLoggingHelper _log; + + /// + /// Task parameter type information + /// + private IDictionary _taskParameterTypeInfo; + + /// + /// MSBuild engine uses this for logging where the task comes from + /// + public string FactoryName + { + get + { + return "Code Task Factory"; + } + } + + /// + /// Gets the type of the generated task. + /// + public Type TaskType { get; private set; } + + /// + /// The assemblies that the codetaskfactory should reference by default. + /// + private static List DefaultReferencedAssemblies + { + get + { + if (s_defaultReferencedAssemblies == null) + { + s_defaultReferencedAssemblies = new List(); + + // Loading with the partial name is fine for framework assemblies -- we'll always get the correct one + // through the magic of unification + foreach (string frameworkAssembly in s_defaultReferencedFrameworkAssemblyNames) + { + s_defaultReferencedAssemblies.Add(frameworkAssembly); + } + + // We also want to add references to two MSBuild assemblies: Microsoft.Build.Framework.dll and + // Microsoft.Build.Utilities.Core.dll. If we just let the CLR unify the simple name, it will + // pick the highest version on the machine, which means that in hosts with restrictive binding + // redirects, or no binding redirects, we'd end up creating an inline task that could not be + // run. Instead, to make sure that we can actually use what we're building, just use the Framework + // and Utilities currently loaded into this process -- Since we're in Microsoft.Build.Tasks.Core.dll + // right now, by definition both of them are always already loaded. + string msbuildFrameworkPath = Assembly.GetAssembly(typeof(ITask)).Location; + string msbuildUtilitiesPath = Assembly.GetAssembly(typeof(Task)).Location; + + s_defaultReferencedAssemblies.Add(msbuildFrameworkPath); + s_defaultReferencedAssemblies.Add(msbuildUtilitiesPath); + } + + return s_defaultReferencedAssemblies; + } + } + + /// + /// Get the type information for all task parameters + /// + public TaskPropertyInfo[] GetTaskParameters() + { + TaskPropertyInfo[] properties = new TaskPropertyInfo[_taskParameterTypeInfo.Count]; + _taskParameterTypeInfo.Values.CopyTo(properties, 0); + return properties; + } + + /// + /// Initialze the task factory + /// + public bool Initialize(string taskName, IDictionary taskParameters, string taskElementContents, IBuildEngine taskFactoryLoggingHost) + { + _nameOfTask = taskName; + _log = new TaskLoggingHelper(taskFactoryLoggingHost, taskName); + _log.TaskResources = AssemblyResources.PrimaryResources; + _log.HelpKeywordPrefix = "MSBuild."; + + XmlNode taskContent = ExtractTaskContent(taskElementContents); + + if (taskContent == null) + { + // Just return false because we have already logged the error in ExtractTaskContents + return false; + } + + bool validatedTaskNode = ValidateTaskNode(); + + if (!validatedTaskNode) + { + return false; + } + + if (taskContent.Attributes["Type"] != null) + { + _type = taskContent.Attributes["Type"].Value; + if (_type.Length == 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmpty", "Type"); + return false; + } + } + + if (taskContent.Attributes["Language"] != null) + { + _language = taskContent.Attributes["Language"].Value; + if (_language.Length == 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmpty", "Language"); + return false; + } + } + + if (taskContent.Attributes["Source"] != null) + { + _sourcePath = taskContent.Attributes["Source"].Value; + + if (_sourcePath.Length == 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmpty", "Source"); + return false; + } + + if (_type == null) + { + _type = "Class"; + } + } + + _referencedAssemblies = ExtractReferencedAssemblies(); + + if (_log.HasLoggedErrors) + { + return false; + } + + _usingNamespaces = ExtractUsingNamespaces(); + + if (_log.HasLoggedErrors) + { + return false; + } + + _sourceCode = taskContent.InnerText; + + if (_log.HasLoggedErrors) + { + return false; + } + + if (_type == null) + { + _type = "Fragment"; + } + + if (_language == null) + { + _language = "cs"; + } + + if (String.Equals(_type, "Fragment", StringComparison.OrdinalIgnoreCase)) + { + _typeIsFragment = true; + _typeIsMethod = false; + } + else if (String.Equals(_type, "Method", StringComparison.OrdinalIgnoreCase)) + { + _typeIsFragment = false; + _typeIsMethod = true; + } + + _taskParameterTypeInfo = taskParameters; + + _compiledAssembly = CompileInMemoryAssembly(); + + // If it wasn't compiled, it logged why. + // If it was, continue. + if (_compiledAssembly != null) + { + // Now go find the type int he compiled assembly. + Type[] exportedTypes = _compiledAssembly.GetExportedTypes(); + + Type fullNameMatch = null; + Type partialNameMatch = null; + + foreach (Type exportedType in exportedTypes) + { + string exportedTypeName = exportedType.FullName; + if (exportedTypeName.Equals(_nameOfTask, StringComparison.OrdinalIgnoreCase)) + { + fullNameMatch = exportedType; + break; + } + else if (partialNameMatch == null && exportedTypeName.EndsWith(_nameOfTask, StringComparison.OrdinalIgnoreCase)) + { + partialNameMatch = exportedType; + } + } + + this.TaskType = fullNameMatch ?? partialNameMatch; + if (this.TaskType == null) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.CouldNotFindTaskInAssembly", _nameOfTask); + } + } + + return !_log.HasLoggedErrors; + } + + /// + /// Create a taskfactory instance which contains the data that needs to be refreshed between task invocations + /// + public ITask CreateTask(IBuildEngine loggingHost) + { + // The assembly will have been compiled during class factory initialization, create an instance of it + if (_compiledAssembly != null) + { + // In order to use the resource strings from the tasks assembly we need to register the resources with the task logging helper. + TaskLoggingHelper log = new TaskLoggingHelper(loggingHost, _nameOfTask); + log.TaskResources = AssemblyResources.PrimaryResources; + log.HelpKeywordPrefix = "MSBuild."; + + ITask taskInstance = Activator.CreateInstance(this.TaskType) as ITask; + if (taskInstance == null) + { + log.LogErrorWithCodeFromResources("CodeTaskFactory.NeedsITaskInterface", _nameOfTask); + } + + return taskInstance; + } + else + { + return null; + } + } + + /// + /// Cleans up any context or state that may have been built up for a given task. + /// + /// The task to clean up. + /// + /// For many factories, this method is a no-op. But some factories may have built up + /// an AppDomain as part of an individual task instance, and this is their opportunity + /// to shutdown the AppDomain. + /// + public void CleanupTask(ITask task) + { + ErrorUtilities.VerifyThrowArgumentNull(task, "task"); + } + + /// + /// Create a property (with the corresponding private field) from the given type information + /// + private static void CreateProperty(CodeTypeDeclaration ctd, string propertyName, Type propertyType, object defaultValue) + { + CodeMemberField field = new CodeMemberField(new CodeTypeReference(propertyType), "_" + propertyName); + field.Attributes = MemberAttributes.Private; + if (defaultValue != null) + { + field.InitExpression = new CodePrimitiveExpression(defaultValue); + } + + ctd.Members.Add(field); + + CodeMemberProperty prop = new CodeMemberProperty(); + prop.Name = propertyName; + prop.Type = new CodeTypeReference(propertyType); + prop.Attributes = MemberAttributes.Public; + prop.HasGet = true; + prop.HasSet = true; + + CodeFieldReferenceExpression fieldRef = new CodeFieldReferenceExpression(); + fieldRef.FieldName = field.Name; + prop.GetStatements.Add(new CodeMethodReturnStatement(fieldRef)); + + CodeAssignStatement fieldAssign = new CodeAssignStatement(); + fieldAssign.Left = fieldRef; + fieldAssign.Right = new CodeArgumentReferenceExpression("value"); + prop.SetStatements.Add(fieldAssign); + ctd.Members.Add(prop); + } + + /// + /// Create the Execute() method for the task from the fragment of code from the element + /// + private static void CreateExecuteMethodFromFragment(CodeTypeDeclaration codeTypeDeclaration, string executeCode) + { + CodeMemberMethod executeMethod = new CodeMemberMethod(); + executeMethod.Name = "Execute"; + executeMethod.Attributes = MemberAttributes.Override | MemberAttributes.Public; + executeMethod.Statements.Add(new CodeSnippetStatement(executeCode)); + executeMethod.ReturnType = new CodeTypeReference(typeof(Boolean)); + executeMethod.Statements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(null, "_Success"))); + codeTypeDeclaration.Members.Add(executeMethod); + } + + /// + /// Create the body of the task's code by simply using the taskCode as a snippet for the CodeDom + /// + private static void CreateTaskBody(CodeTypeDeclaration codeTypeDeclaration, string taskCode) + { + CodeSnippetTypeMember snippet = new CodeSnippetTypeMember(taskCode); + codeTypeDeclaration.Members.Add(snippet); + } + + /// + /// Create a property (with the corresponding private field) from the given type information + /// + private static void CreateProperty(CodeTypeDeclaration codeTypeDeclaration, TaskPropertyInfo propInfo, object defaultValue) + { + CreateProperty(codeTypeDeclaration, propInfo.Name, propInfo.PropertyType, defaultValue); + } + + /// + /// Extract the elements from the + /// + /// string[] of reference paths + private string[] ExtractReferencedAssemblies() + { + XmlNodeList referenceNodes = _taskNode.SelectNodes("//*[local-name()='Reference']"); + List references = new List(); + for (int i = 0; i < referenceNodes.Count; i++) + { + XmlAttribute attribute = referenceNodes[i].Attributes["Include"]; + + bool hasInvalidChildNodes = HasInvalidChildNodes(referenceNodes[i], new XmlNodeType[] { XmlNodeType.Comment, XmlNodeType.Whitespace }); + + if (hasInvalidChildNodes) + { + return null; + } + + if (attribute == null || attribute.Value.Length == 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmpty", "Include"); + return null; + } + + references.Add(attribute.Value); + } + + return references.ToArray(); + } + + /// + /// Extract the elements from the + /// + /// string[] of using's + private string[] ExtractUsingNamespaces() + { + XmlNodeList usingNodes = _taskNode.SelectNodes("//*[local-name()='Using']"); + + List usings = new List(); + for (int i = 0; i < usingNodes.Count; i++) + { + bool hasInvalidChildNodes = HasInvalidChildNodes(usingNodes[i], new XmlNodeType[] { XmlNodeType.Comment, XmlNodeType.Whitespace }); + + if (hasInvalidChildNodes) + { + return null; + } + + XmlAttribute attribute = usingNodes[i].Attributes["Namespace"]; + if (attribute == null || attribute.Value.Length == 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmpty", "Namespace"); + return null; + } + + usings.Add(attribute.Value); + } + + return usings.ToArray(); + } + + /// + /// Extract the node from the UsingTask node + /// + /// textual content of the node + /// XmlNode + private XmlNode ExtractTaskContent(string taskElementContents) + { + // We need to get the InnerXml of the node back into + // a root node so that we can execute the appropriate XPath on it + XmlDocument document = new XmlDocument(); + + _taskNode = document.CreateElement("Task"); + document.AppendChild(_taskNode); + + // record our internal representation of the node + _taskNode.InnerXml = taskElementContents; + + XmlNodeList codeNodes = _taskNode.SelectNodes("//*[local-name()='Code']"); + + if (codeNodes.Count > 1) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.MultipleCodeNodes"); + return null; + } + else if (codeNodes.Count == 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.CodeElementIsMissing", _nameOfTask); + return null; + } + + bool hasInvalidChildNodes = HasInvalidChildNodes(codeNodes[0], new XmlNodeType[] { XmlNodeType.Comment, XmlNodeType.Whitespace, XmlNodeType.Text, XmlNodeType.CDATA }); + + if (hasInvalidChildNodes) + { + return null; + } + + return codeNodes[0]; + } + + /// + /// Make sure the task node only contains Code, Reference, Usings + /// + private bool ValidateTaskNode() + { + bool foundInvalidNode = false; + if (_taskNode.HasChildNodes) + { + foreach (XmlNode childNode in _taskNode.ChildNodes) + { + switch (childNode.NodeType) + { + case XmlNodeType.Comment: + case XmlNodeType.Whitespace: + case XmlNodeType.Text: + // These are legal, and ignored + continue; + case XmlNodeType.Element: + if (childNode.Name.Equals("Code", StringComparison.OrdinalIgnoreCase) || childNode.Name.Equals("Reference", StringComparison.OrdinalIgnoreCase) || childNode.Name.Equals("Using", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + else + { + foundInvalidNode = true; + } + + break; + default: + foundInvalidNode = true; + break; + } + + if (foundInvalidNode) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidElementLocation", childNode.Name, _taskNode.Name); + return false; + } + } + } + + return true; + } + + /// + /// If a parent node has a child node and it is not supposed to, log an error indicating it has an invalid element. + /// + private bool HasInvalidChildNodes(XmlNode parentNode, XmlNodeType[] allowedNodeTypes) + { + bool hasInvalidNode = false; + if (parentNode.HasChildNodes) + { + foreach (XmlNode childNode in parentNode.ChildNodes) + { + bool elementAllowed = false; + foreach (XmlNodeType nodeType in allowedNodeTypes) + { + if (nodeType == childNode.NodeType) + { + elementAllowed = true; + break; + } + } + + if (!elementAllowed) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidElementLocation", childNode.Name, parentNode.Name); + hasInvalidNode = true; + } + else + { + continue; + } + } + } + + return hasInvalidNode; + } + + /// + /// Add a reference assembly to the list of references passed to the compiler. We will try and load the assembly to make sure it is found + /// before sending it to the compiler. The reason we load here is that we will be using it in this appdomin anyways as soon as we are going to compile, which should be right away. + /// + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] + private void AddReferenceAssemblyToReferenceList(List referenceAssemblyList, string referenceAssembly) + { + if (referenceAssemblyList != null) + { + string candidateAssemblyLocation = null; + string extension = String.Empty; + + if (!String.IsNullOrEmpty(referenceAssembly)) + { + try + { + bool fileExists = File.Exists(referenceAssembly); + if (!fileExists) + { + if (!referenceAssembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || !referenceAssembly.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { +#pragma warning disable 618 + // Unfortunately Assembly.Load is not an alternative to LoadWithPartialName, since + // Assembly.Load requires the full assembly name to be passed to it. + // Therefore we must ignore the deprecated warning. + Assembly candidateAssembly = Assembly.LoadWithPartialName(referenceAssembly); + if (candidateAssembly != null) + { + candidateAssemblyLocation = candidateAssembly.Location; + } +#pragma warning restore 618 + } + } + else + { + try + { + Assembly candidateAssembly = Assembly.UnsafeLoadFrom(referenceAssembly); + if (candidateAssembly != null) + { + candidateAssemblyLocation = candidateAssembly.Location; + } + } + catch (BadImageFormatException e) + { + Debug.Assert(e.Message.Contains("0x80131058"), "Expected Message to contain 0x80131058"); + AssemblyName.GetAssemblyName(referenceAssembly); + candidateAssemblyLocation = referenceAssembly; + _log.LogMessageFromResources(MessageImportance.Low, "CodeTaskFactory.HaveReflectionOnlyAssembly", referenceAssembly); + } + } + } + catch (Exception e) + { + if (Microsoft.Build.Shared.ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + _log.LogErrorWithCodeFromResources("CodeTaskFactory.ReferenceAssemblyIsInvalid", referenceAssembly, e.Message); + } + } + + if (candidateAssemblyLocation != null) + { + referenceAssemblyList.Add(candidateAssemblyLocation); + } + else + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.CouldNotFindReferenceAssembly", referenceAssembly); + } + } + } + + /// + /// Compile the assembly in memory and get a reference to the assembly itself. + /// If compilation fails, returns null. + /// + private Assembly CompileInMemoryAssembly() + { + // Combine our default assembly references with those specified + List finalReferencedAssemblies = new List(); + CombineReferencedAssemblies(finalReferencedAssemblies); + + // Combine our default using's with those specified + string[] finalUsingNamespaces = CombineUsingNamespaces(); + + // Language can be anything that has a codedom provider, in the standard naming method + // "c#;cs;csharp", "vb;vbs;visualbasic;vbscript", "js;jscript;javascript", "vj#;vjs;vjsharp", "c++;mc;cpp" + using (CodeDomProvider provider = CodeDomProvider.CreateProvider(_language)) + { + if (provider is Microsoft.CSharp.CSharpCodeProvider) + { + AddReferenceAssemblyToReferenceList(finalReferencedAssemblies, "System"); + } + + CompilerParameters compilerParameters = new CompilerParameters(finalReferencedAssemblies.ToArray()); + + // We don't need debug information + compilerParameters.IncludeDebugInformation = true; + + // Not a file based assembly + compilerParameters.GenerateInMemory = true; + + // Indicates that a .dll should be generated. + compilerParameters.GenerateExecutable = false; + + // Horrible code dom / compilation declarations + CodeTypeDeclaration codeTypeDeclaration; + StringBuilder codeBuilder = new StringBuilder(); + StringWriter writer = new StringWriter(codeBuilder, CultureInfo.CurrentCulture); + CodeGeneratorOptions codeGeneratorOptions = new CodeGeneratorOptions(); + codeGeneratorOptions.BlankLinesBetweenMembers = true; + codeGeneratorOptions.VerbatimOrder = true; + CodeCompileUnit compilationUnit = new CodeCompileUnit(); + + // If our code is in a separate file, then read it in here + if (_sourcePath != null) + { + _sourceCode = File.ReadAllText(_sourcePath); + } + + string fullCode = _sourceCode; + + // A fragment is essentially the contents of the execute method (except the final return true/false) + // A method is the whole execute method specified + // Anything else assumes that the whole class is being supplied + if (_typeIsFragment || _typeIsMethod) + { + codeTypeDeclaration = CreateTaskClass(); + + CreateTaskProperties(codeTypeDeclaration); + + if (_typeIsFragment) + { + CreateExecuteMethodFromFragment(codeTypeDeclaration, _sourceCode); + } + else + { + CreateTaskBody(codeTypeDeclaration, _sourceCode); + } + + CodeNamespace codeNamespace = new CodeNamespace("InlineCode"); + foreach (string importname in finalUsingNamespaces) + { + codeNamespace.Imports.Add(new CodeNamespaceImport(importname)); + } + + codeNamespace.Types.Add(codeTypeDeclaration); + compilationUnit.Namespaces.Add(codeNamespace); + + // Create the source for the CodeDom + provider.GenerateCodeFromCompileUnit(compilationUnit, writer, codeGeneratorOptions); + } + else + { + // We are a full class, so just create the CodeDom from the source + provider.GenerateCodeFromStatement(new CodeSnippetStatement(_sourceCode), writer, codeGeneratorOptions); + } + + // Our code generation is complete, grab the source from the builder ready for compilation + fullCode = codeBuilder.ToString(); + + FullTaskSpecification fullSpec = new FullTaskSpecification(finalReferencedAssemblies, fullCode); + Assembly existingAssembly; + if (!s_compiledTaskCache.TryGetValue(fullSpec, out existingAssembly)) + { + // Invokes compilation. + CompilerResults compilerResults = provider.CompileAssemblyFromSource(compilerParameters, fullCode); + + string outputPath = null; + if (compilerResults.Errors.Count > 0 || Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") != null) + { + string tempDirectory = Path.GetTempPath(); + string fileName = Guid.NewGuid().ToString() + ".txt"; + outputPath = Path.Combine(tempDirectory, fileName); + File.WriteAllText(outputPath, fullCode); + } + + if (compilerResults.NativeCompilerReturnValue != 0 && compilerResults.Errors.Count > 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.FindSourceFileAt", outputPath); + + foreach (CompilerError e in compilerResults.Errors) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.CompilerError", e.ToString()); + } + + return null; + } + + // Add to the cache. Failing to add is not a fatal error. + s_compiledTaskCache.TryAdd(fullSpec, compilerResults.CompiledAssembly); + return compilerResults.CompiledAssembly; + } + else + { + return existingAssembly; + } + } + } + + /// + /// Combine our default referenced assemblies with those explicitly specified + /// + private void CombineReferencedAssemblies(List finalReferenceList) + { + foreach (string defaultReference in DefaultReferencedAssemblies) + { + AddReferenceAssemblyToReferenceList(finalReferenceList, defaultReference); + } + + if (_referencedAssemblies != null) + { + foreach (string referenceAssembly in _referencedAssemblies) + { + AddReferenceAssemblyToReferenceList(finalReferenceList, referenceAssembly); + } + } + } + + /// + /// Combine our default imported namespaces with those explicitly specified + /// + private string[] CombineUsingNamespaces() + { + int usingNamespaceCount = _defaultUsingNamespaces.Length; + + if (_usingNamespaces != null) + { + usingNamespaceCount += _usingNamespaces.Length; + } + + string[] finalUsingNamespaces = new string[usingNamespaceCount]; + _defaultUsingNamespaces.CopyTo(finalUsingNamespaces, 0); + if (_usingNamespaces != null) + { + _usingNamespaces.CopyTo(finalUsingNamespaces, _defaultUsingNamespaces.Length); + } + + return finalUsingNamespaces; + } + + /// + /// Create the task properties + /// + private void CreateTaskProperties(CodeTypeDeclaration codeTypeDeclaration) + { + // If we are only a fragment, then create a default task parameter called + // Success - that we can use in the fragment to indicate success or failure of the task + if (_typeIsFragment) + { + CreateProperty(codeTypeDeclaration, "Success", typeof(bool), true); + } + + foreach (TaskPropertyInfo propInfo in _taskParameterTypeInfo.Values) + { + CreateProperty(codeTypeDeclaration, propInfo, null); + } + } + + /// + /// Create the task class + /// + private CodeTypeDeclaration CreateTaskClass() + { + CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration(); + codeTypeDeclaration.IsClass = true; + codeTypeDeclaration.Name = _nameOfTask; + codeTypeDeclaration.TypeAttributes = TypeAttributes.Public; + codeTypeDeclaration.Attributes = MemberAttributes.Final; + codeTypeDeclaration.BaseTypes.Add("Microsoft.Build.Utilities.Task"); + return codeTypeDeclaration; + } + + /// + /// Class used as a key for the compiled assembly cache + /// + private class FullTaskSpecification : IComparable, IEquatable + { + /// + /// The set of assemblies referenced by this task. + /// + private List _referenceAssemblies; + + /// + /// The complete source code for the task. + /// + private string _fullCode; + + /// + /// Constructor + /// + public FullTaskSpecification(List references, string fullCode) + { + _referenceAssemblies = references; + _fullCode = fullCode; + } + + /// + /// Override of GetHashCode + /// + public override int GetHashCode() + { + return _fullCode.GetHashCode(); + } + + /// + /// Override of Equals + /// + public override bool Equals(object other) + { + if (Object.ReferenceEquals(this, other)) + { + return true; + } + + FullTaskSpecification otherSpec = other as FullTaskSpecification; + if (otherSpec == null) + { + return false; + } + + return ((IEquatable)this).Equals(otherSpec); + } + + /// + /// Implementation of Equals. + /// + bool IEquatable.Equals(FullTaskSpecification other) + { + if (_referenceAssemblies.Count != other._referenceAssemblies.Count) + { + return false; + } + + for (int i = 0; i < _referenceAssemblies.Count; i++) + { + if (_referenceAssemblies[i] != other._referenceAssemblies[i]) + { + return false; + } + } + + return other._fullCode == _fullCode; + } + + /// + /// Implementation of CompareTo + /// + int IComparable.CompareTo(FullTaskSpecification other) + { + int result = Comparer.Default.Compare(_referenceAssemblies.Count, other._referenceAssemblies.Count); + if (result == 0) + { + result = Comparer.Default.Compare(_fullCode, other._fullCode); + } + + return result; + } + } + } +} diff --git a/src/XMakeTasks/CodeTaskFactoryInstance.cs b/src/XMakeTasks/CodeTaskFactoryInstance.cs new file mode 100644 index 00000000000..01995296217 --- /dev/null +++ b/src/XMakeTasks/CodeTaskFactoryInstance.cs @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// A code task factory instance which is instantiated for each batch +//----------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Text; +using System.CodeDom.Compiler; +using System.Reflection; +using System.Xml; +using System.IO; + +using Microsoft.Build.Framework; +using System.CodeDom; +using Microsoft.Build.Utilities; +using System.Globalization; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task factory instance which actually create an ITaskInstance, this factory contains the data that needs to be refreshed for each task invocation. + /// + public class CodeTaskFactoryInstance : ITaskFactory + { + /// + /// Compiled assembly to use when instantiating the Itask + /// + private Assembly compiledAssembly; + + /// + /// The instantiated Itask instance + /// + private ITask taskInstance; + + /// + /// Name of the task + /// + private string nameOfTask; + + /// + /// Create a new CodeTaskFactoryInstance using the compiled assembly and the task name + /// + public CodeTaskFactoryInstance(Assembly compiledAssembly, string taskName) + { + ErrorUtilities.VerifyThrowArgumentNull(taskName, "taskName"); + ErrorUtilities.VerifyThrowArgumentNull(compiledAssembly, "compiledAssembly"); + + nameOfTask = taskName; + this.compiledAssembly = compiledAssembly; + } + + /// + /// Create an instance of the ITask + /// + public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) + { + // The assembly will have been compiled during class factory initialization, create an instance of it + if (this.compiledAssembly != null) + { + // In order to use the resource strings from the tasks assembly we need to register the resources with the task logging helper. + TaskLoggingHelper log = new TaskLoggingHelper(taskFactoryLoggingHost, nameOfTask); + log.TaskResources = AssemblyResources.PrimaryResources; + log.HelpKeywordPrefix = "MSBuild."; + + Type[] exportedTypes = this.compiledAssembly.GetExportedTypes(); + + Type fullNameMatch = null; + Type partialNameMatch = null; + + foreach (Type exportedType in exportedTypes) + { + string exportedTypeName = exportedType.FullName; + if (exportedTypeName.Equals(nameOfTask, StringComparison.OrdinalIgnoreCase)) + { + fullNameMatch = exportedType; + break; + } + else if (partialNameMatch == null && exportedTypeName.EndsWith(nameOfTask, StringComparison.OrdinalIgnoreCase)) + { + partialNameMatch = exportedType; + } + } + + if (fullNameMatch == null && partialNameMatch == null) + { + log.LogErrorWithCodeFromResources("CodeTaskFactory.CouldNotFindTaskInAssembly", nameOfTask); + return null; + } + + this.taskInstance = this.compiledAssembly.CreateInstance(fullNameMatch != null ? fullNameMatch.FullName : partialNameMatch.FullName, true) as ITask; + + if (this.taskInstance == null) + { + log.LogErrorWithCodeFromResources("CodeTaskFactory.NeedsITaskInterface", nameOfTask); + return null; + } + + return this.taskInstance; + } + else + { + return null; + } + } + + /// + /// Clean up any state created when the task was instantiated + /// + public void CleanupTask() + { + compiledAssembly = null; + taskInstance = null; + } + + /// + /// Given a property info and a value set the parametervalue on the ITaskInstance + /// + public bool SetTaskParameterValue(PropertyInfo parameter, object parameterValue) + { + bool success = false; + + PropertyInfo propInfo = this.taskInstance.GetType().GetProperty + ( + parameter.Name, + BindingFlags.ExactBinding | BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public + ); + + propInfo.SetValue(taskInstance, parameterValue, null); + success = true; + + return success; + } + + /// + /// Given a property info get the value of the task parameter + /// + public object GetTaskParameterValue(PropertyInfo parameter) + { + // We need to work with the real propertyInfo object so that we can use reflection to collect the value + // so use our factory property info's name to get the real one, then use it. + PropertyInfo propInfo = this.taskInstance.GetType().GetProperty + ( + parameter.Name, + BindingFlags.ExactBinding | BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public + ); + return propInfo.GetValue(taskInstance, null); + } + } +} + diff --git a/src/XMakeTasks/ComDependencyWalker.cs b/src/XMakeTasks/ComDependencyWalker.cs new file mode 100644 index 00000000000..7502814680a --- /dev/null +++ b/src/XMakeTasks/ComDependencyWalker.cs @@ -0,0 +1,456 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices.ComTypes; +using System.Globalization; + + +using Microsoft.Build.Shared; + +using Marshal = System.Runtime.InteropServices.Marshal; +using COMException = System.Runtime.InteropServices.COMException; +using VarEnum = System.Runtime.InteropServices.VarEnum; + +namespace Microsoft.Build.Tasks +{ + // Abstract the method for releasing COM objects for unit testing. + // Our mocks are not actually COM objects and they would blow up if passed to the real Marshal.ReleaseComObject. + internal delegate int MarshalReleaseComObject(object o); + + /// + /// COM type library dependency walker class + /// + internal class ComDependencyWalker + { + // Dependencies of all analyzed typelibs. Can be cleared to allow for analyzing typelibs one by one while + // still skipping already seen types + private HashSet _dependencies; + + // History of already seen types. + private HashSet _analyzedTypes; + + private sealed class TYPELIBATTRComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new TYPELIBATTRComparer(); + + public bool Equals(TYPELIBATTR a, TYPELIBATTR b) + { + return a.guid == b.guid && + a.lcid == b.lcid && + a.syskind == b.syskind && + a.wLibFlags == b.wLibFlags && + a.wMajorVerNum == b.wMajorVerNum && + a.wMinorVerNum == b.wMinorVerNum; + } + + public int GetHashCode(TYPELIBATTR x) + { + return unchecked(x.guid.GetHashCode() + x.lcid + (int)x.syskind + (int)x.wLibFlags + (x.wMajorVerNum << 16) + x.wMinorVerNum); + } + } + + private struct AnalyzedTypesInfoKey + { + public readonly Guid guid; + public readonly short wMajorVerNum; + public readonly short wMinorVerNum; + public readonly int lcid; + public readonly int index; + + public AnalyzedTypesInfoKey(Guid guid, short major, short minor, int lcid, int index) + { + this.guid = guid; + this.wMajorVerNum = major; + this.wMinorVerNum = minor; + this.lcid = lcid; + this.index = index; + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}.{3}:{4}", + this.guid, this.wMajorVerNum, + this.wMinorVerNum, this.lcid, this.index); + } + } + + private sealed class AnalyzedTypesInfoKeyComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new AnalyzedTypesInfoKeyComparer(); + + public bool Equals(AnalyzedTypesInfoKey a, AnalyzedTypesInfoKey b) + { + return a.guid == b.guid && + a.wMajorVerNum == b.wMajorVerNum && + a.wMinorVerNum == b.wMinorVerNum && + a.lcid == b.lcid && + a.index == b.index; + } + + public int GetHashCode(AnalyzedTypesInfoKey x) + { + return unchecked(x.guid.GetHashCode() + (x.wMajorVerNum << 16) + x.wMinorVerNum + x.lcid + x.index); + } + } + + private MarshalReleaseComObject _marshalReleaseComObject; + + private List _encounteredProblems; + + /// + /// List of exceptions thrown by the components during scanning + /// + internal List EncounteredProblems + { + get + { + return _encounteredProblems; + } + } + + /// + /// Internal constructor + /// + internal ComDependencyWalker(MarshalReleaseComObject marshalReleaseComObject) + { + _dependencies = new HashSet(TYPELIBATTRComparer.Instance); + _analyzedTypes = new HashSet(AnalyzedTypesInfoKeyComparer.Instance); + _encounteredProblems = new List(); + + _marshalReleaseComObject = marshalReleaseComObject; + } + + /// + /// The main entry point to the dependency walker + /// + /// type library to be analyzed + internal void AnalyzeTypeLibrary(ITypeLib typeLibrary) + { + try + { + int typeInfoCount = typeLibrary.GetTypeInfoCount(); + + for (int i = 0; i < typeInfoCount; i++) + { + ITypeInfo typeInfo = null; + + try + { + typeLibrary.GetTypeInfo(i, out typeInfo); + AnalyzeTypeInfo(typeInfo); + } + finally + { + if (typeInfo != null) + { + _marshalReleaseComObject(typeInfo); + } + } + } + } + // This is the only catch block in this class, meaning that once a type library throws it's game over for it. + // I've tried using a finer grained approach but experiments with COM objects on my machine have shown that if + // a type library is broken, it's broken in several places (e.g. dependencies on a type lib that's not + // registered properly). Trying to recover from errors and continue with scanning dependencies only meant + // that we got lots of exceptions thrown which was not only not very useful for the end user, but also horribly slow. + catch (COMException ex) + { + _encounteredProblems.Add(ex); + } + } + + /// + /// Analyze the given type looking for dependencies on other type libraries + /// + /// + private void AnalyzeTypeInfo(ITypeInfo typeInfo) + { + ITypeLib containingTypeLib = null; + int indexInContainingTypeLib; + + try + { + typeInfo.GetContainingTypeLib(out containingTypeLib, out indexInContainingTypeLib); + + TYPELIBATTR containingTypeLibAttributes; + ComReference.GetTypeLibAttrForTypeLib(ref containingTypeLib, out containingTypeLibAttributes); + + // Have we analyzed this type info already? If so skip it. + AnalyzedTypesInfoKey typeInfoId = new AnalyzedTypesInfoKey( + containingTypeLibAttributes.guid, containingTypeLibAttributes.wMajorVerNum, + containingTypeLibAttributes.wMinorVerNum, containingTypeLibAttributes.lcid, indexInContainingTypeLib); + + // Get enough information about the type to figure out if we want to register it as a dependency + TYPEATTR typeAttributes; + + ComReference.GetTypeAttrForTypeInfo(typeInfo, out typeAttributes); + + // Is it one of the types we don't care about? + if (!CanSkipType(typeInfo, containingTypeLib, typeAttributes, containingTypeLibAttributes)) + { + _dependencies.Add(containingTypeLibAttributes); + + if (_analyzedTypes.Add(typeInfoId)) + { + // We haven't already analyzed this type, so rescan + ScanImplementedTypes(typeInfo, typeAttributes); + ScanDefinedVariables(typeInfo, typeAttributes); + ScanDefinedFunctions(typeInfo, typeAttributes); + } + } + // Make sure if we encounter this type again, we won't rescan it, since we already know we can skip it + else + { + _analyzedTypes.Add(typeInfoId); + } + } + finally + { + if (containingTypeLib != null) + { + _marshalReleaseComObject(containingTypeLib); + } + } + } + + /// + /// Returns true if we don't need to analyze this particular type. + /// + /// + /// + /// + /// + /// + private bool CanSkipType(ITypeInfo typeInfo, ITypeLib typeLib, TYPEATTR typeAttributes, TYPELIBATTR typeLibAttributes) + { + // Well known OLE type? + if ((typeAttributes.guid == NativeMethods.IID_IUnknown) || + (typeAttributes.guid == NativeMethods.IID_IDispatch) || + (typeAttributes.guid == NativeMethods.IID_IDispatchEx) || + (typeAttributes.guid == NativeMethods.IID_IEnumVariant) || + (typeAttributes.guid == NativeMethods.IID_ITypeInfo)) + { + return true; + } + + // Is this the Guid type? If so we should be using the corresponding .NET type. + if (typeLibAttributes.guid == NativeMethods.IID_StdOle) + { + string typeName, ignoredDocString, ignoredHelpFile; + int ignoredHelpContext; + + typeInfo.GetDocumentation(-1, out typeName, out ignoredDocString, out ignoredHelpContext, out ignoredHelpFile); + + if (string.CompareOrdinal(typeName, "GUID") == 0) + { + return true; + } + } + + // Skip types exported from .NET assemblies + ITypeLib2 typeLib2 = typeLib as ITypeLib2; + + if (typeLib2 != null) + { + object exportedFromComPlusObj; + typeLib2.GetCustData(ref NativeMethods.GUID_ExportedFromComPlus, out exportedFromComPlusObj); + + string exportedFromComPlus = exportedFromComPlusObj as string; + + if (!string.IsNullOrEmpty(exportedFromComPlus)) + { + return true; + } + } + + return false; + } + + /// + /// For a given type, analyze recursively all the types implemented by it. + /// + /// + /// + private void ScanImplementedTypes(ITypeInfo typeInfo, TYPEATTR typeAttributes) + { + for (int implTypeIndex = 0; implTypeIndex < typeAttributes.cImplTypes; implTypeIndex++) + { + IFixedTypeInfo implementedType = null; + + try + { + IntPtr hRef; + IFixedTypeInfo fixedTypeInfo = (IFixedTypeInfo)typeInfo; + fixedTypeInfo.GetRefTypeOfImplType(implTypeIndex, out hRef); + fixedTypeInfo.GetRefTypeInfo(hRef, out implementedType); + + AnalyzeTypeInfo((ITypeInfo)implementedType); + } + finally + { + if (implementedType != null) + { + _marshalReleaseComObject(implementedType); + } + } + } + } + + /// + /// For a given type, analyze all the variables defined by it + /// + /// + /// + private void ScanDefinedVariables(ITypeInfo typeInfo, TYPEATTR typeAttributes) + { + for (int definedVarIndex = 0; definedVarIndex < typeAttributes.cVars; definedVarIndex++) + { + IntPtr varDescHandleToRelease = IntPtr.Zero; + + try + { + VARDESC varDesc; + ComReference.GetVarDescForVarIndex(typeInfo, definedVarIndex, out varDesc, out varDescHandleToRelease); + AnalyzeElement(typeInfo, varDesc.elemdescVar); + } + finally + { + if (varDescHandleToRelease != IntPtr.Zero) + { + typeInfo.ReleaseVarDesc(varDescHandleToRelease); + } + } + } + } + + /// + /// For a given type, analyze all the functions implemented by it. That means all the argument and return types. + /// + /// + /// + private void ScanDefinedFunctions(ITypeInfo typeInfo, TYPEATTR typeAttributes) + { + for (int definedFuncIndex = 0; definedFuncIndex < typeAttributes.cFuncs; definedFuncIndex++) + { + IntPtr funcDescHandleToRelease = IntPtr.Zero; + + try + { + FUNCDESC funcDesc; + ComReference.GetFuncDescForDescIndex(typeInfo, definedFuncIndex, out funcDesc, out funcDescHandleToRelease); + + int offset = 0; + + // Analyze the argument types + for (int paramIndex = 0; paramIndex < funcDesc.cParams; paramIndex++) + { + ELEMDESC elemDesc = (ELEMDESC)Marshal.PtrToStructure( + new IntPtr(funcDesc.lprgelemdescParam.ToInt64() + offset), typeof(ELEMDESC)); + + AnalyzeElement(typeInfo, elemDesc); + + offset += Marshal.SizeOf(typeof(ELEMDESC)); + } + + // Analyze the return value type + AnalyzeElement(typeInfo, funcDesc.elemdescFunc); + } + finally + { + if (funcDescHandleToRelease != IntPtr.Zero) + { + typeInfo.ReleaseFuncDesc(funcDescHandleToRelease); + } + } + } + } + + /// + /// Analyze the given element (i.e. composite type of an argument) recursively + /// + /// + /// + private void AnalyzeElement(ITypeInfo typeInfo, ELEMDESC elementDesc) + { + TYPEDESC typeDesc = elementDesc.tdesc; + + // If the current type is a pointer or an array, determine the child type and analyze that. + while (((VarEnum)typeDesc.vt == VarEnum.VT_PTR) || ((VarEnum)typeDesc.vt == VarEnum.VT_SAFEARRAY)) + { + TYPEDESC childTypeDesc = (TYPEDESC)Marshal.PtrToStructure(typeDesc.lpValue, typeof(TYPEDESC)); + typeDesc = childTypeDesc; + } + + // We're only interested in user defined types for recursive analysis + if ((VarEnum)typeDesc.vt == VarEnum.VT_USERDEFINED) + { + IntPtr hrefType = typeDesc.lpValue; + IFixedTypeInfo childTypeInfo = null; + + try + { + IFixedTypeInfo fixedTypeInfo = (IFixedTypeInfo)typeInfo; + fixedTypeInfo.GetRefTypeInfo(hrefType, out childTypeInfo); + + AnalyzeTypeInfo((ITypeInfo)childTypeInfo); + } + finally + { + if (childTypeInfo != null) + { + _marshalReleaseComObject(childTypeInfo); + } + } + } + } + + /// + /// Get all the dependencies of the processed libraries + /// + /// + internal TYPELIBATTR[] GetDependencies() + { + TYPELIBATTR[] returnArray = new TYPELIBATTR[_dependencies.Count]; + _dependencies.CopyTo(returnArray); + return returnArray; + } + + /// + /// FOR UNIT-TESTING ONLY + /// Returns a list of the analyzed type names + /// + internal ICollection GetAnalyzedTypeNames() + { + string[] names = new string[_analyzedTypes.Count]; + int i = 0; + foreach (AnalyzedTypesInfoKey analyzedType in _analyzedTypes) + { + names[i++] = analyzedType.ToString(); + } + + return names; + } + + /// + /// Clear the dependency list so we can read dependencies incrementally but still have the advantage of + /// not scanning previously seen types + /// + internal void ClearDependencyList() + { + _dependencies.Clear(); + } + + /// + /// Clear the analyzed type cache. This is necessary if we have to resolve dependencies that are also + /// COM references in the project, or we may get an inaccurate view of what their dependencies are. + /// + internal void ClearAnalyzedTypeCache() + { + _analyzedTypes.Clear(); + } + } +} diff --git a/src/XMakeTasks/ComReference.cs b/src/XMakeTasks/ComReference.cs new file mode 100644 index 00000000000..eb49e966df4 --- /dev/null +++ b/src/XMakeTasks/ComReference.cs @@ -0,0 +1,607 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +using Marshal = System.Runtime.InteropServices.Marshal; +using COMException = System.Runtime.InteropServices.COMException; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: ComReference + * + * Abstract base class for COM reference wrappers providing common functionality. + * This class hierarchy is used by the ResolveComReference task. Every class deriving from ComReference + * provides functionality for wrapping Com type libraries in a given way (for example AxReference, or PiaReference). + * + */ + internal abstract class ComReference + { + #region Constructors + + /// + /// Internal constructor + /// + /// task logger instance used for logging + /// true if this task should log only errors, no warnings or messages; false otherwise + /// cached reference information (typelib pointer, original task item, typelib name etc.) + /// reference name (for better logging experience) + internal ComReference(TaskLoggingHelper taskLoggingHelper, bool silent, ComReferenceInfo referenceInfo, string itemName) + { + _referenceInfo = referenceInfo; + _itemName = itemName; + _log = taskLoggingHelper; + _silent = silent; + } + + #endregion + + #region Properties + + /// + /// various data for this reference (type lib attrs, name, path, ITypeLib pointer etc) + /// + internal virtual ComReferenceInfo ReferenceInfo + { + get + { + return _referenceInfo; + } + } + + private ComReferenceInfo _referenceInfo; + + /// + /// item name as it appears in the project file + /// (used for logging purposes, we use the actual typelib name for interesting operations) + /// + internal virtual string ItemName + { + get + { + return _itemName; + } + } + + private string _itemName; + + /// + /// task used for logging messages + /// + protected internal TaskLoggingHelper Log + { + get + { + return _log; + } + } + + private TaskLoggingHelper _log; + + /// + /// True if this class should only log errors, but no messages or warnings. + /// + protected internal bool Silent + { + get + { + return _silent; + } + } + + private bool _silent; + + /// + /// lazy-init property, returns true if ADO 2.7 is installed on the machine + /// + internal static bool Ado27Installed + { + get + { + // if we already know the answer, return it + if (ado27PropertyInitialized) + return ado27Installed; + + // not initialized? Find out if ADO 2.7 is installed + ado27Installed = true; + ado27PropertyInitialized = true; + + ITypeLib ado27 = null; + + try + { + // see if ADO 2.7 is registered. + ado27 = (ITypeLib)NativeMethods.LoadRegTypeLib(ref s_guidADO27, 2, 7, 0); + } + catch (COMException ex) + { + // it's not registered. + ado27Installed = false; + ado27ErrorMessage = ex.Message; + } + finally + { + if (ado27 != null) + Marshal.ReleaseComObject(ado27); + } + + return ado27Installed; + } + } + + internal static bool ado27PropertyInitialized = false; + internal static bool ado27Installed; + + /// + /// Error message if Ado27 is not installed on the machine (usually something like "type lib not registered") + /// Only contains valid data if ADO 2.7 is not installed and Ado27Installed was called before + /// + internal static string Ado27ErrorMessage + { + get + { + return ado27ErrorMessage; + } + } + + internal static string ado27ErrorMessage; + + #endregion + + #region Methods + + /* + * Method: UniqueKeyFromTypeLibAttr + * + * Given a TYPELIBATTR structure, generates a key that can be used in hashtables to identify it. + */ + internal static string UniqueKeyFromTypeLibAttr(TYPELIBATTR attr) + { + return String.Format(CultureInfo.InvariantCulture, @"{0}|{1}.{2}|{3}", attr.guid, attr.wMajorVerNum, attr.wMinorVerNum, attr.lcid); + } + + /* + * Method: AreTypeLibAttrEqual + * + * Compares two TYPELIBATTR structures + */ + internal static bool AreTypeLibAttrEqual(TYPELIBATTR attr1, TYPELIBATTR attr2) + { + return attr1.wMajorVerNum == attr2.wMajorVerNum && + attr1.wMinorVerNum == attr2.wMinorVerNum && + attr1.lcid == attr2.lcid && + attr1.guid == attr2.guid; + } + + /// + /// Helper method for retrieving type lib attributes for the given type lib + /// + /// + /// + /// + internal static void GetTypeLibAttrForTypeLib(ref ITypeLib typeLib, out TYPELIBATTR typeLibAttr) + { + IntPtr pAttrs = IntPtr.Zero; + typeLib.GetLibAttr(out pAttrs); + + // GetLibAttr should never return null, this is just to be safe + if (pAttrs == IntPtr.Zero) + { + throw new COMException( + ResourceUtilities.FormatResourceString("ResolveComReference.CannotGetTypeLibAttrForTypeLib")); + } + + try + { + typeLibAttr = (TYPELIBATTR)Marshal.PtrToStructure(pAttrs, typeof(TYPELIBATTR)); + } + finally + { + typeLib.ReleaseTLibAttr(pAttrs); + } + } + + /// + /// Helper method for retrieving type attributes for a given type info + /// + /// + /// + /// + internal static void GetTypeAttrForTypeInfo(ITypeInfo typeInfo, out TYPEATTR typeAttr) + { + IntPtr pAttrs = IntPtr.Zero; + typeInfo.GetTypeAttr(out pAttrs); + + // GetTypeAttr should never return null, this is just to be safe + if (pAttrs == IntPtr.Zero) + { + throw new COMException( + ResourceUtilities.FormatResourceString("ResolveComReference.CannotRetrieveTypeInformation")); + } + + try + { + typeAttr = (TYPEATTR)Marshal.PtrToStructure(pAttrs, typeof(TYPEATTR)); + } + finally + { + typeInfo.ReleaseTypeAttr(pAttrs); + } + } + + /// + /// Helper method for retrieving type attributes for a given type info + /// This method needs to also return the native pointer to be released when we're done with our VARDESC. + /// It's not really possible to copy everything to a managed struct and then release the ptr immediately + /// here, since VARDESCs contain other native pointers we may need to access. + /// + /// + /// + /// + /// + /// + /// + internal static void GetVarDescForVarIndex(ITypeInfo typeInfo, int varIndex, out VARDESC varDesc, out IntPtr varDescHandle) + { + IntPtr pVarDesc = IntPtr.Zero; + typeInfo.GetVarDesc(varIndex, out pVarDesc); + + // GetVarDesc should never return null, this is just to be safe + if (pVarDesc == IntPtr.Zero) + { + throw new COMException( + ResourceUtilities.FormatResourceString("ResolveComReference.CannotRetrieveTypeInformation")); + } + + varDesc = (VARDESC)Marshal.PtrToStructure(pVarDesc, typeof(VARDESC)); + varDescHandle = pVarDesc; + } + + /// + /// Helper method for retrieving the function description structure for the given function index. + /// This method needs to also return the native pointer to be released when we're done with our FUNCDESC. + /// It's not really possible to copy everything to a managed struct and then release the ptr immediately + /// here, since FUNCDESCs contain other native pointers we may need to access. + /// + /// + /// + /// + /// + /// + internal static void GetFuncDescForDescIndex(ITypeInfo typeInfo, int funcIndex, out FUNCDESC funcDesc, out IntPtr funcDescHandle) + { + IntPtr pFuncDesc = IntPtr.Zero; + typeInfo.GetFuncDesc(funcIndex, out pFuncDesc); + + // GetFuncDesc should never return null, this is just to be safe + if (pFuncDesc == IntPtr.Zero) + { + throw new COMException( + ResourceUtilities.FormatResourceString("ResolveComReference.CannotRetrieveTypeInformation")); + } + + funcDesc = (FUNCDESC)Marshal.PtrToStructure(pFuncDesc, typeof(FUNCDESC)); + funcDescHandle = pFuncDesc; + } + + /* + * Method: GetTypeLibNameForITypeLib + * + * Gets the name of given type library. + */ + internal static bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silent, ITypeLib typeLib, string typeLibId, out string typeLibName) + { + typeLibName = ""; + + // see if the type library supports ITypeLib2 + ITypeLib2 typeLib2 = typeLib as ITypeLib2; + + if (typeLib2 == null) + { + // Looks like the type lib doesn't support it. Let's use the Marshal method. + typeLibName = Marshal.GetTypeLibName(typeLib); + return true; + } + + // Get the custom attribute. If anything fails then just return the + // type library name. + try + { + object data = null; + + typeLib2.GetCustData(ref NativeMethods.GUID_TYPELIB_NAMESPACE, out data); + + // if returned namespace is null or its type is not System.String, fall back to the default + // way of getting the type lib name (just to be safe) + if (data == null || string.Compare(data.GetType().ToString(), "system.string", StringComparison.OrdinalIgnoreCase) != 0) + { + typeLibName = Marshal.GetTypeLibName(typeLib); + return true; + } + + // Strip off the DLL extension if it's there + typeLibName = (string)data; + + if (typeLibName.Length >= 4) + { + if (string.Compare(typeLibName.Substring(typeLibName.Length - 4), ".dll", StringComparison.OrdinalIgnoreCase) == 0) + { + typeLibName = typeLibName.Substring(0, typeLibName.Length - 4); + } + } + } + catch (COMException ex) + { + // If anything fails log a warning and just return the type library name. + if (!silent) + { + log.LogWarningWithCodeFromResources("ResolveComReference.CannotAccessTypeLibName", typeLibId, ex.Message); + } + typeLibName = Marshal.GetTypeLibName(typeLib); + return true; + } + + return true; + } + + /* + * Method: GetTypeLibNameForTypeLibAttrs + * + * Gets the name of given type library. + */ + internal static bool GetTypeLibNameForTypeLibAttrs(TaskLoggingHelper log, bool silent, TYPELIBATTR typeLibAttr, out string typeLibName) + { + typeLibName = ""; + ITypeLib typeLib = null; + + try + { + // load our type library + try + { + TYPELIBATTR attr = typeLibAttr; + typeLib = (ITypeLib)NativeMethods.LoadRegTypeLib(ref attr.guid, attr.wMajorVerNum, attr.wMinorVerNum, attr.lcid); + } + catch (COMException ex) + { + if (!silent) + { + log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, ex.Message); + } + + return false; + } + + string typeLibId = log.FormatResourceString("ResolveComReference.TypeLibAttrId", typeLibAttr.guid.ToString(), typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum); + + return GetTypeLibNameForITypeLib(log, silent, typeLib, typeLibId, out typeLibName); + } + finally + { + if (typeLib != null) + Marshal.ReleaseComObject(typeLib); + } + } + + /// + /// Strips type library number from a type library path (for example, "ref.dll\2" becomes "ref.dll") + /// + /// type library path with possible typelib number appended to it + /// proper file path to the type library + internal static string StripTypeLibNumberFromPath(string typeLibPath, FileExists fileExists) + { + bool lastChance = false; + if (typeLibPath != null && typeLibPath.Length > 0) + { + if (!fileExists(typeLibPath)) + { + // Strip the type library number + int lastSlash = typeLibPath.LastIndexOf('\\'); + + if (lastSlash != -1) + { + bool allNumbers = true; + + for (int i = lastSlash + 1; i < typeLibPath.Length; i++) + { + if (!Char.IsDigit(typeLibPath[i])) + { + allNumbers = false; + break; + } + } + + // If we had all numbers past the last slash then we're OK to strip + // the type library number + if (allNumbers) + { + typeLibPath = typeLibPath.Substring(0, lastSlash); + if (!fileExists(typeLibPath)) + { + lastChance = true; + } + } + else + { + lastChance = true; + } + } + else + { + lastChance = true; + } + } + } + + // If we couldn't find the path directly, we'll use the same mechanism Windows uses to find + // libraries. LoadLibrary() will search all of the correct paths to find this module. We can then + // use GetModuleFileName() to determine the actual path from which the module was loaded. This problem + // was exposed in Vista where certain libraries are registered but are lacking paths in the registry, + // so the old code would fail to find them on disk using the simplistic checks above. + if (lastChance) + { + IntPtr libraryHandle = NativeMethodsShared.LoadLibrary(typeLibPath); + if (IntPtr.Zero != libraryHandle) + { + try + { + StringBuilder sb = new StringBuilder(NativeMethodsShared.MAX_PATH); + System.Runtime.InteropServices.HandleRef handleRef = new System.Runtime.InteropServices.HandleRef(sb, libraryHandle); + int len = NativeMethodsShared.GetModuleFileName(handleRef, sb, sb.Capacity); + if ((len != 0) && + ((uint)Marshal.GetLastWin32Error() != NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER)) + { + typeLibPath = sb.ToString(); + } + else + { + typeLibPath = ""; + } + } + finally + { + NativeMethodsShared.FreeLibrary(libraryHandle); + } + } + else + { + typeLibPath = ""; + } + } + + return typeLibPath; + } + + /* + * Method: GetPathOfTypeLib + * + * Gets the type lib path for given type lib attributes (reused almost verbatim from vsdesigner utils code) + * NOTE: If there's a typelib number at the end of the path, does NOT strip it. + */ + internal static bool GetPathOfTypeLib(TaskLoggingHelper log, bool silent, ref TYPELIBATTR typeLibAttr, out string typeLibPath) + { + // Get which file the type library resides in. If the appropriate + // file cannot be found then a blank string is returned. + typeLibPath = ""; + + try + { + // Get the path from the registry + // This call has known issues. See http://msdn.microsoft.com/en-us/library/ms221436.aspx for the method and + // here for the fix http://support.microsoft.com/kb/982110. Most users from Win7 or Win2008R2 should have already received this post Win7SP1. + // In Summary: The issue is about calls to The QueryPathOfRegTypeLib function not returning the correct path for a 32-bit version of a + // registered type library in a 64-bit edition of Windows 7 or in Windows Server 2008 R2. It either returns the 64bit path or null. + typeLibPath = NativeMethods.QueryPathOfRegTypeLib(ref typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, typeLibAttr.lcid); + typeLibPath = Environment.ExpandEnvironmentVariables(typeLibPath); + } + catch (COMException ex) + { + if (!silent) + { + log.LogWarningWithCodeFromResources("ResolveComReference.CannotGetPathForTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, ex.Message); + } + + return false; + } + + if (typeLibPath != null && typeLibPath.Length > 0) + { + // We have to check for NULL here because QueryPathOfRegTypeLib() returns + // a BSTR with a NULL character appended to it. + if (typeLibPath[typeLibPath.Length - 1] == '\0') + { + typeLibPath = typeLibPath.Substring(0, typeLibPath.Length - 1); + } + } + + if (typeLibPath != null && typeLibPath.Length > 0) + { + return true; + } + else + { + if (!silent) + { + log.LogWarningWithCodeFromResources("ResolveComReference.CannotGetPathForTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, ""); + } + + return false; + } + } + + #region RemapAdoTypeLib guids + + // guids for RemapAdoTypeLib + private readonly static Guid s_guidADO20 = new Guid("{00000200-0000-0010-8000-00AA006D2EA4}"); + private readonly static Guid s_guidADO21 = new Guid("{00000201-0000-0010-8000-00AA006D2EA4}"); + private readonly static Guid s_guidADO25 = new Guid("{00000205-0000-0010-8000-00AA006D2EA4}"); + private readonly static Guid s_guidADO26 = new Guid("{00000206-0000-0010-8000-00AA006D2EA4}"); + // unfortunately this cannot be readonly, since it's being passed by reference to LoadRegTypeLib + private static Guid s_guidADO27 = new Guid("{EF53050B-882E-4776-B643-EDA472E8E3F2}"); + + #endregion + + /* + * Method: RemapAdoTypeLib + * + * Tries to remap an ADO type library to ADO 2.7. If the type library passed in is an older ADO tlb, + * then remap it to ADO 2.7 if it's registered on the machine (!). Otherwise don't modify the typelib. + * Returns true if the type library passed in was successfully remapped. + */ + internal static bool RemapAdoTypeLib(TaskLoggingHelper log, bool silent, ref TYPELIBATTR typeLibAttr) + { + // we only care about ADO 2.0, 2.1, 2.5 or 2.6 here. + if (typeLibAttr.wMajorVerNum == 2) + { + if ((typeLibAttr.wMinorVerNum == 0 && typeLibAttr.guid == s_guidADO20) || + (typeLibAttr.wMinorVerNum == 1 && typeLibAttr.guid == s_guidADO21) || + (typeLibAttr.wMinorVerNum == 5 && typeLibAttr.guid == s_guidADO25) || + (typeLibAttr.wMinorVerNum == 6 && typeLibAttr.guid == s_guidADO26)) + { + // see if ADO 2.7 is registered. + if (!Ado27Installed) + { + if (!silent) + { + // it's not registered. Don't change the original typelib then. + log.LogWarningWithCodeFromResources("ResolveComReference.FailedToRemapAdoTypeLib", typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, Ado27ErrorMessage); + } + + return false; + } + + typeLibAttr.guid = s_guidADO27; + typeLibAttr.wMajorVerNum = 2; + typeLibAttr.wMinorVerNum = 7; + typeLibAttr.lcid = 0; + + return true; + } + } + + return false; + } + + /// + /// Finds an existing wrapper for the specified component + /// + /// + /// + /// + internal abstract bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp); + + #endregion + } +} diff --git a/src/XMakeTasks/ComReferenceInfo.cs b/src/XMakeTasks/ComReferenceInfo.cs new file mode 100644 index 00000000000..76ae4f20e4e --- /dev/null +++ b/src/XMakeTasks/ComReferenceInfo.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Internal class representing information about a COM reference. + /// + internal class ComReferenceInfo + { + #region Properties + + /// + /// ITypeLib pointer + /// + internal ITypeLib typeLibPointer; + + /// + /// type library attributes for the reference. Taken from the task item itself or type library if + /// reference is specified as file on disk. + /// + internal TYPELIBATTR attr; + + /// + /// type library name + /// + internal string typeLibName; + + /// + /// path to the reference, with typelibrary number stripped, if any (so ref1.dll\2 becomes ref1.dll). + /// The full path is only used for loading the type library, and it's not necessary + /// to do it after the interface pointer is cached in this object. + /// + internal string strippedTypeLibPath; + + /// + /// When using TlbImp.exe, we need to make sure that we keep track of the non-stripped typelib path, + /// because that's what we need to pass to TlbImp. + /// + internal string fullTypeLibPath; + + /// + /// reference to the original ITaskItem, if any + /// + internal ITaskItem taskItem; + + /// + /// Path to the resolved reference. + /// + internal ComReferenceInfo primaryOfAxImpRef; + + /// + /// The wrapper that resulted from resolving the COM reference. + /// + internal ComReferenceWrapperInfo resolvedWrapper; + + /// + /// List of the paths to COM wrapper assemblies that this reference is dependent upon. + /// + internal List dependentWrapperPaths; + + /// + /// Reference to the ITaskItem generated from the resolved reference, if any. + /// + internal ITaskItem referencePathItem; + + #endregion + + #region Constructors + + /// + /// Default constructor + /// + internal ComReferenceInfo() + { + this.dependentWrapperPaths = new List(); + } + + /// + /// Construct a new ComReferenceInfo copying all state from the given ComReferenceInfo instance + /// + internal ComReferenceInfo(ComReferenceInfo copyFrom) + { + this.attr = copyFrom.attr; + this.typeLibName = copyFrom.typeLibName; + this.strippedTypeLibPath = copyFrom.strippedTypeLibPath; + this.fullTypeLibPath = copyFrom.fullTypeLibPath; + this.typeLibPointer = copyFrom.typeLibPointer; + this.primaryOfAxImpRef = copyFrom.primaryOfAxImpRef; + this.resolvedWrapper = copyFrom.resolvedWrapper; + this.taskItem = new TaskItem(copyFrom.taskItem); + this.dependentWrapperPaths = copyFrom.dependentWrapperPaths; + this.referencePathItem = copyFrom.referencePathItem; + } + + #endregion + + #region Methods + + /// + /// Initialize the object with type library attributes + /// + /// + /// + internal bool InitializeWithTypeLibAttrs(TaskLoggingHelper log, bool silent, TYPELIBATTR tlbAttr, ITaskItem originalTaskItem, string targetProcessorArchitecture) + { + TYPELIBATTR remappableTlbAttr = tlbAttr; + + ComReference.RemapAdoTypeLib(log, silent, ref remappableTlbAttr); + + // for attribute references, the path is not specified, so we need to get it from the registry + if (!ComReference.GetPathOfTypeLib(log, silent, ref remappableTlbAttr, out this.fullTypeLibPath)) + { + return false; + } + + // Now that we have the path, we can call InitializeWithPath to get the correct TYPELIBATTR set up + // and the correct ITypeLib pointer. + return InitializeWithPath(log, silent, this.fullTypeLibPath, originalTaskItem, targetProcessorArchitecture); + } + + /// + /// Initialize the object with a type library path + /// + /// + /// + internal bool InitializeWithPath(TaskLoggingHelper log, bool silent, string path, ITaskItem originalTaskItem, string targetProcessorArchitecture) + { + ErrorUtilities.VerifyThrowArgumentNull(path, "path"); + + this.taskItem = originalTaskItem; + + // Note that currently we DO NOT remap file ADO references. This is because when pointing to a file on disk, + // it seems unnatural to remap it to something else - a file reference means "use THIS component". + // This is still under debate though, and may be revised later. + + // save both the stripped and full path in our object -- for the most part we just need the stripped path, but if + // we're using tlbimp.exe, we need to pass the full path w/ type lib number to it, or it won't generate the interop + // assembly correctly. + this.fullTypeLibPath = path; + this.strippedTypeLibPath = ComReference.StripTypeLibNumberFromPath(path, new FileExists(File.Exists)); + + // use the unstripped path to actually load the library + switch (targetProcessorArchitecture) + { + case ProcessorArchitecture.AMD64: + case ProcessorArchitecture.IA64: + this.typeLibPointer = (ITypeLib)NativeMethods.LoadTypeLibEx(path, (int)NativeMethods.REGKIND.REGKIND_LOAD_TLB_AS_64BIT); + break; + case ProcessorArchitecture.X86: + this.typeLibPointer = (ITypeLib)NativeMethods.LoadTypeLibEx(path, (int)NativeMethods.REGKIND.REGKIND_LOAD_TLB_AS_32BIT); + break; + case ProcessorArchitecture.ARM: + case ProcessorArchitecture.MSIL: + default: + // Transmit the flag directly from the .targets files and rely on tlbimp.exe to produce a good error message. + this.typeLibPointer = (ITypeLib)NativeMethods.LoadTypeLibEx(path, (int)NativeMethods.REGKIND.REGKIND_NONE); + break; + } + + try + { + // get the type lib attributes from the retrieved interface pointer. + // do NOT remap file ADO references, since we'd end up with a totally different reference than specified. + ComReference.GetTypeLibAttrForTypeLib(ref this.typeLibPointer, out this.attr); + + // get the type lib name from the retrieved interface pointer + if (!ComReference.GetTypeLibNameForITypeLib( + log, + silent, + this.typeLibPointer, + this.GetTypeLibId(log), + out this.typeLibName)) + { + ReleaseTypeLibPtr(); + return false; + } + } + catch (COMException) + { + ReleaseTypeLibPtr(); + throw; + } + + return true; + } + + /// + /// A unique id string of this reference, it's either the item spec or (in the case of a dependency ref) + /// guid and version from typelib attributes + /// + private string GetTypeLibId(TaskLoggingHelper log) + { + if (taskItem != null) + { + return taskItem.ItemSpec; + } + else + { + return log.FormatResourceString("ResolveComReference.TypeLibAttrId", attr.guid, attr.wMajorVerNum, attr.wMinorVerNum); + } + } + + /// + /// Get the source item, if available. Null otherwise. + /// + internal string SourceItemSpec + { + get + { + if (taskItem == null) + { + return null; + } + return taskItem.ItemSpec; + } + } + + /// + /// Release the COM ITypeLib pointer for this reference + /// + internal void ReleaseTypeLibPtr() + { + if (typeLibPointer != null) + { + Marshal.ReleaseComObject(typeLibPointer); + typeLibPointer = null; + } + } + #endregion + } +} diff --git a/src/XMakeTasks/ComReferenceItemAttributes.cs b/src/XMakeTasks/ComReferenceItemAttributes.cs new file mode 100644 index 00000000000..0959fb496ce --- /dev/null +++ b/src/XMakeTasks/ComReferenceItemAttributes.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Tasks +{ + /// + /// Predefined XML attributes of a ComReference Item. + /// + internal static class ComReferenceItemMetadataNames + { + internal const string guid = "Guid"; + internal const string versionMinor = "VersionMinor"; + internal const string versionMajor = "VersionMajor"; + internal const string lcid = "Lcid"; + internal const string privatized = "Private"; + internal const string wrapperTool = "WrapperTool"; + internal const string tlbReferenceName = "TlbReferenceName"; + } +} diff --git a/src/XMakeTasks/ComReferenceResolutionException.cs b/src/XMakeTasks/ComReferenceResolutionException.cs new file mode 100644 index 00000000000..cbfe1b1efca --- /dev/null +++ b/src/XMakeTasks/ComReferenceResolutionException.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Tasks +{ + /// + /// Internal exception thrown when there's an unrecoverable failure resolving a COM reference and we should + /// move on to the next one, if it makes sense. + /// + /// + /// WARNING: marking a type [Serializable] without implementing ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both forward and backward compatibility + /// + [Serializable] + internal class ComReferenceResolutionException : Exception + { + /// + /// Default constructor + /// + internal ComReferenceResolutionException() + { + // do nothing + } + + /// + /// Constructor that allows to preserve the original exception information + /// + internal ComReferenceResolutionException(Exception innerException) : base("", innerException) + { + // do nothing + } + + /// + /// Deserializing constructor. It should not be necessary if everything goes well, but if it doesn't + /// then we don't want to crash when trying to deserialize the uncaught exception into another AppDomain. + /// + /// + /// + protected ComReferenceResolutionException(SerializationInfo info, StreamingContext context) : base(info, context) + { + // do nothing + } + } +} diff --git a/src/XMakeTasks/ComReferenceTypes.cs b/src/XMakeTasks/ComReferenceTypes.cs new file mode 100644 index 00000000000..e95da9858c1 --- /dev/null +++ b/src/XMakeTasks/ComReferenceTypes.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; + +namespace Microsoft.Build.Tasks +{ + /// + /// Predefined typelib wrapper types. + /// + internal static class ComReferenceTypes + { + internal const string tlbimp = "tlbimp"; + internal const string aximp = "aximp"; + internal const string primary = "primary"; + internal const string primaryortlbimp = "primaryortlbimp"; + + /// + /// returns true is refType equals tlbimp + /// + internal static bool IsTlbImp(string refType) + { + return (string.Compare(refType, ComReferenceTypes.tlbimp, StringComparison.OrdinalIgnoreCase) == 0); + } + + /// + /// returns true is refType equals aximp + /// + internal static bool IsAxImp(string refType) + { + return (string.Compare(refType, ComReferenceTypes.aximp, StringComparison.OrdinalIgnoreCase) == 0); + } + + /// + /// returns true is refType equals pia + /// + internal static bool IsPia(string refType) + { + return (string.Compare(refType, ComReferenceTypes.primary, StringComparison.OrdinalIgnoreCase) == 0); + } + + /// + /// returns true if refType equals primaryortlbimp, which is basically an unknown reference type + /// + /// + /// + internal static bool IsPiaOrTlbImp(string refType) + { + return (string.Compare(refType, ComReferenceTypes.primaryortlbimp, StringComparison.OrdinalIgnoreCase) == 0); + } + } +} diff --git a/src/XMakeTasks/ComReferenceWrapperInfo.cs b/src/XMakeTasks/ComReferenceWrapperInfo.cs new file mode 100644 index 00000000000..b2ed7826e3a --- /dev/null +++ b/src/XMakeTasks/ComReferenceWrapperInfo.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Class containing info about wrapper location, used for caching. + /// + internal class ComReferenceWrapperInfo + { + /// + /// Default constructor. + /// + internal ComReferenceWrapperInfo() + { + // do nothing + } + + // path to the wrapper assembly + internal string path; + + // wrapper assembly + internal Assembly assembly; + + // It's possible for PIAs to get redirected to a different assembly (a newer version), so we must + // remember the original name in case a component asks us to resolve a dependency using that old name + internal AssemblyNameExtension originalPiaName; + } +} diff --git a/src/XMakeTasks/CombinePath.cs b/src/XMakeTasks/CombinePath.cs new file mode 100644 index 00000000000..646aba43200 --- /dev/null +++ b/src/XMakeTasks/CombinePath.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task to call Path.Combine. + /// + public class CombinePath : TaskExtension + { + /// + /// Default constructor. Does nothing. + /// + public CombinePath() + { + } + + private string _basePath; + private ITaskItem[] _paths; + private ITaskItem[] _combinedPaths; + + /// + /// The base path, the first parameter into Path.Combine. Can be a relative path, + /// absolute path, or (blank). + /// + public string BasePath + { + get + { + return _basePath; + } + + set + { + _basePath = value; + } + } + + /// + /// The list of paths to combine with the base path. These can be relative paths + /// or absolute paths. + /// + [Required] + public ITaskItem[] Paths + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_paths, "paths"); + return _paths; + } + + set + { + _paths = value; + } + } + + /// + /// This is the output of the task, a list of paths produced by combining the base + /// path with each of the paths passed in. + /// + [Output] + public ITaskItem[] CombinedPaths + { + get + { + return _combinedPaths; + } + + set + { + _combinedPaths = value; + } + } + + /// + /// Calls Path.Combine for each of the inputs. Preserves metadata. + /// + /// true on success, false on failure + public override bool Execute() + { + if (this.BasePath == null) + { + this.BasePath = String.Empty; + } + + List combinedPathsList = new List(); + + foreach (ITaskItem path in this.Paths) + { + TaskItem combinedPath = new TaskItem(path); + + try + { + combinedPath.ItemSpec = Path.Combine(_basePath, path.ItemSpec); + combinedPathsList.Add(combinedPath); + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("General.InvalidArgument", e.Message); + } + } + + this.CombinedPaths = combinedPathsList.ToArray(); + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/XMakeTasks/CommandLineBuilderExtension.cs b/src/XMakeTasks/CommandLineBuilderExtension.cs new file mode 100644 index 00000000000..3f9616eb44a --- /dev/null +++ b/src/XMakeTasks/CommandLineBuilderExtension.cs @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// CommandLineBuilder derived class for specialized logic specific to MSBuild tasks + /// +#if WHIDBEY_VISIBILITY + internal +#else + public /* because its used by VJ# Vjc task. */ +#endif + class CommandLineBuilderExtension : CommandLineBuilder + { + /// + /// Set a boolean switch iff its value exists and its value is 'true'. + /// + /// + /// + /// + internal void AppendWhenTrue + ( + string switchName, + Hashtable bag, + string parameterName + ) + { + object obj = bag[parameterName]; + // If the switch isn't set, don't add it to the command line. + if (obj != null) + { + bool value = (bool)obj; + + if (value) + { + AppendSwitch(switchName); + } + } + } + + /// + /// Set a boolean switch only if its value exists. + /// + /// + /// + /// + internal void AppendPlusOrMinusSwitch + ( + string switchName, + Hashtable bag, + string parameterName + ) + { + object obj = bag[parameterName]; + // If the switch isn't set, don't add it to the command line. + if (obj != null) + { + bool value = (bool)obj; + // Do not quote - or + as they are part of the switch + AppendSwitchUnquotedIfNotNull(switchName, (value ? "+" : "-")); + } + } + + + /// + /// Set a switch if its value exists by choosing from the input choices + /// + /// + /// + /// + /// + /// + internal void AppendByChoiceSwitch + ( + string switchName, + Hashtable bag, + string parameterName, + string choice1, + string choice2 + ) + { + object obj = bag[parameterName]; + // If the switch isn't set, don't add it to the command line. + if (obj != null) + { + bool value = (bool)obj; + AppendSwitchUnquotedIfNotNull(switchName, (value ? choice1 : choice2)); + } + } + + /// + /// Set an integer switch only if its value exists. + /// + /// + /// + /// + internal void AppendSwitchWithInteger + ( + string switchName, + Hashtable bag, + string parameterName + ) + { + object obj = bag[parameterName]; + // If the switch isn't set, don't add it to the command line. + if (obj != null) + { + int value = (int)obj; + AppendSwitchIfNotNull(switchName, value.ToString(CultureInfo.InvariantCulture)); + } + } + + /// + /// Adds an aliased switch, used for ResGen: + /// /reference:Foo=System.Xml.dll + /// + /// + /// + /// + internal void AppendSwitchAliased(string switchName, string alias, string parameter) + { + AppendSwitchUnquotedIfNotNull(switchName, alias + "="); + AppendTextWithQuoting(parameter); + } + + /// + /// Adds a nested switch, used by SGen.exe. For example: + /// /compiler:"/keyfile:\"c:\some folder\myfile.snk\"" + /// + /// + /// + /// + internal void AppendNestedSwitch(string outerSwitchName, string innerSwitchName, string parameter) + { + string quotedParameter = GetQuotedText(parameter); + AppendSwitchIfNotNull(outerSwitchName, innerSwitchName + quotedParameter); + } + + /// + /// Returns a quoted string appropriate for appending to a command line. + /// + /// + /// Escapes any double quotes in the string. + /// + /// + protected string GetQuotedText(string unquotedText) + { + StringBuilder quotedText = new StringBuilder(); + + AppendQuotedTextToBuffer(quotedText, unquotedText); + + return quotedText.ToString(); + } + + /// + /// Appends a command-line switch that takes a compound string parameter. The parameter is built up from the item-spec and + /// the specified attributes. The switch is appended as many times as there are parameters given. + /// + /// + /// + /// + internal void AppendSwitchIfNotNull + ( + string switchName, + ITaskItem[] parameters, + string[] attributes + ) + { + AppendSwitchIfNotNull(switchName, parameters, attributes, null /* treatAsFlag */); + } + + /// + /// Append a switch if 'parameter' is not null. + /// Split on the characters provided. + /// + /// + /// + /// + internal void AppendSwitchWithSplitting(string switchName, string parameter, string delimiter, params char[] splitOn) + { + if (parameter != null) + { + string[] splits = parameter.Split(splitOn, /* omitEmptyEntries */ StringSplitOptions.RemoveEmptyEntries); + string[] splitAndTrimmed = new string[splits.Length]; + for (int i = 0; i < splits.Length; ++i) + { + splitAndTrimmed[i] = splits[i].Trim(); + } + AppendSwitchIfNotNull(switchName, splitAndTrimmed, delimiter); + } + } + + /// + /// Returns true if the parameter is empty in spirits, + /// even if it contains the seperators and white space only + /// Split on the characters provided. + /// + /// + /// + internal static bool IsParameterEmpty(string parameter, params char[] splitOn) + { + if (parameter != null) + { + string[] splits = parameter.Split(splitOn, /* omitEmptyEntries */ StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < splits.Length; ++i) + { + if (!String.IsNullOrEmpty(splits[i].Trim())) + { + return false; + } + } + } + return true; + } + /// + /// Designed to handle the /link and /embed swithes: + /// + /// /embed[resource]:[,[,Private]] + /// /link[resource]:[,[,Private]] + /// + /// Where the last flag--Private--is either present or not present + /// depending on whether the ITaskItem has a Private="True" attribue. + /// + /// + /// + /// + /// + internal void AppendSwitchIfNotNull + ( + string switchName, + ITaskItem[] parameters, + string[] metadataNames, + bool[] treatAsFlags // May be null. In this case no metadata are treated as flags. + ) + { + ErrorUtilities.VerifyThrow + ( + treatAsFlags == null || + (metadataNames.Length == treatAsFlags.Length), + "metadataNames and treatAsFlags should have the same length." + ); + + if (parameters != null) + { + foreach (ITaskItem parameter in parameters) + { + AppendSwitchIfNotNull(switchName, parameter.ItemSpec); + + if (metadataNames != null) + { + for (int i = 0; i < metadataNames.Length; ++i) + { + string metadataValue = parameter.GetMetadata(metadataNames[i]); + + if ((metadataValue != null) && (metadataValue.Length > 0)) + { + // Treat attribute as a boolean flag? + if (treatAsFlags == null || treatAsFlags[i] == false) + { + // Not a boolean flag. + CommandLine.Append(','); + AppendTextWithQuoting(metadataValue); + } + else + { + // A boolean flag. + bool flagSet = false; + + flagSet = MetadataConversionUtilities.TryConvertItemMetadataToBool(parameter, metadataNames[i]); + + if (flagSet) + { + CommandLine.Append(','); + AppendTextWithQuoting(metadataNames[i]); + } + } + } + else + { + if (treatAsFlags == null || treatAsFlags[i] == false) + { + // If the caller of this method asked us to add metadata + // A, B, and C, and metadata A doesn't exist on the item, + // then it doesn't make sense to check for B and C. Because + // since these metadata are just being appended on the + // command-line switch with comma-separation, you can't pass + // in the B metadata unless you've also passed in the A + // metadata. Otherwise the tool's command-line parser will + // get totally confused. + + // This only applies to non-flag attributes. + break; + } + } + } + } + } + } + } + } +} diff --git a/src/XMakeTasks/ConvertToAbsolutePath.cs b/src/XMakeTasks/ConvertToAbsolutePath.cs new file mode 100644 index 00000000000..d48d1ff1330 --- /dev/null +++ b/src/XMakeTasks/ConvertToAbsolutePath.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task to call Path.GetFullPath + /// + public class ConvertToAbsolutePath : TaskExtension + { + /// + /// Default constructor. Does nothing. + /// + public ConvertToAbsolutePath() + { + } + + private ITaskItem[] _paths; + private ITaskItem[] _absolutePaths; + + /// + /// The list of paths to convert to absolute paths. + /// + [Required] + public ITaskItem[] Paths + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_paths, "paths"); + return _paths; + } + + set + { + _paths = value; + } + } + + /// + /// This is the output of the task, a list of absolute paths for the items passed in + /// + [Output] + public ITaskItem[] AbsolutePaths + { + get + { + return _absolutePaths; + } + + set + { + _absolutePaths = value; + } + } + + /// + /// Calls Path.GetFullPath for each of the inputs. Preserves metadata. + /// + /// true on success, false on failure + public override bool Execute() + { + List absolutePathsList = new List(); + + foreach (ITaskItem path in this.Paths) + { + try + { + // Only call Path.GetFullPath if the path is not rooted to avoid + // going to disk when it is not necessary + if (!Path.IsPathRooted(path.ItemSpec)) + { + if (path is ITaskItem2) + { + ((ITaskItem2)path).EvaluatedIncludeEscaped = ((ITaskItem2)path).GetMetadataValueEscaped("FullPath"); + } + else + { + path.ItemSpec = EscapingUtilities.Escape(path.GetMetadata("FullPath")); + } + } + absolutePathsList.Add(path); + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("General.InvalidArgument", e.Message); + } + } + + this.AbsolutePaths = absolutePathsList.ToArray(); + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/XMakeTasks/Copy.cs b/src/XMakeTasks/Copy.cs new file mode 100644 index 00000000000..96355c40a91 --- /dev/null +++ b/src/XMakeTasks/Copy.cs @@ -0,0 +1,746 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Collections.Generic; +using System.Resources; +using System.Reflection; +using System.Globalization; +using System.Threading; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task that copies files. + /// + public class Copy : TaskExtension, ICancelableTask + { + /// + /// Constructor. + /// + public Copy() + { + this.RetryDelayMilliseconds = RetryDelayMillisecondsDefault; + } + + #region Properties + + private ITaskItem[] _sourceFiles = null; + private ITaskItem _destinationFolder = null; + private ITaskItem[] _destinationFiles = null; + private bool _skipUnchangedFiles = false; + private ITaskItem[] _copiedFiles = null; + private bool _canceling = false; + private bool _overwriteReadOnlyFiles = false; + private HashSet _directoriesKnownToExist = new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// Force the copy to retry even when it hits ERROR_ACCESS_DENIED -- normally we wouldn't retry in this case since + /// normally there's no point, but occasionally things get into a bad state temporarily, and retrying does actually + /// succeed. So keeping around a secret environment variable to allow forcing that behavior if necessary. + /// + private static bool s_alwaysRetryCopy = Environment.GetEnvironmentVariable("MSBUILDALWAYSRETRY") != null; + + /// + /// Default to retrying "on". This is for robustness. We know that there are some files (eg interop assys) that are + /// just prone to colliding in shared output directories -- we will still warn, so that they can improve things, and + /// spot the other case where there's an actual build process bug, but we don't want to break the build for the + /// sake of purity. + /// + private int _retries = 10; + + /// + /// Default milliseconds to wait between necessary retries + /// + private const int RetryDelayMillisecondsDefault = 1000; + + [Required] + public ITaskItem[] SourceFiles + { + get + { + return _sourceFiles; + } + + set + { + _sourceFiles = value; + } + } + + public ITaskItem DestinationFolder + { + get + { + return _destinationFolder; + } + + set + { + _destinationFolder = value; + } + } + + /// + /// How many times to attempt to copy, if all previous + /// attempts failed. Defaults to zero. + /// Warning: using retries may mask a synchronization problem in your + /// build process. + /// + public int Retries + { + get { return _retries; } + set { _retries = value; } + } + + /// + /// Delay between any necessary retries. + /// Defaults to RetryDelayMillisecondsDefault + /// + public int RetryDelayMilliseconds + { + get; + set; + } + + /// + /// Create Hard Links for the copied files rather than copy the files if possible to do so + /// + public bool UseHardlinksIfPossible + { + get; + set; + } + + public bool SkipUnchangedFiles + { + get + { + return _skipUnchangedFiles; + } + + set + { + _skipUnchangedFiles = value; + } + } + + [Output] + public ITaskItem[] DestinationFiles + { + get + { + return _destinationFiles; + } + + set + { + _destinationFiles = value; + } + } + + // Subset that were successfully copied + [Output] + public ITaskItem[] CopiedFiles + { + get + { + return _copiedFiles; + } + } + + /// + /// Whether to overwrite files in the destination + /// that have the read-only attribute set. + /// + public bool OverwriteReadOnlyFiles + { + get + { + return _overwriteReadOnlyFiles; + } + set + { + _overwriteReadOnlyFiles = value; + } + } + + #endregion + + /// + /// Stop and return (in an undefined state) as soon as possible. + /// + public void Cancel() + { + _canceling = true; + } + + #region ITask Members + + /// + /// Method compares two files and returns true if their size and timestamp are identical. + /// + /// The source file + /// The destination file + /// + private static bool IsMatchingSizeAndTimeStamp + ( + FileState sourceFile, + FileState destinationFile + ) + { + // If the destination doesn't exists, then it is not a matching file. + if (!destinationFile.FileExists) + { + return false; + } + + if (sourceFile.LastWriteTimeUtcFast != destinationFile.LastWriteTimeUtcFast) + { + return false; + } + + if (sourceFile.Length != destinationFile.Length) + { + return false; + } + + return true; + } + + /// + /// INTERNAL FOR UNIT-TESTING ONLY + /// + /// We've got several environment variables that we read into statics since we don't expect them to ever + /// reasonably change, but we need some way of refreshing their values so that we can modify them for + /// unit testing purposes. + /// + internal static void RefreshInternalEnvironmentValues() + { + s_alwaysRetryCopy = Environment.GetEnvironmentVariable("MSBUILDALWAYSRETRY") != null; + } + + /// + /// If MSBUILDALWAYSRETRY is set, also log useful diagnostic information -- as + /// a warning, so it's easily visible. + /// + private void LogDiagnostic(string message, params object[] messageArgs) + { + if (s_alwaysRetryCopy) + { + Log.LogWarning(message, messageArgs); + } + } + + /// + /// Copy one file from source to destination. Create the target directory if necessary and + /// leave the file read-write. + /// + /// + /// + /// Return true to indicate success, return false to indicate failure and NO retry, return NULL to indicate retry. + private bool? CopyFileWithLogging + ( + FileState sourceFileState, // The source file + FileState destinationFileState // The destination file + ) + { + bool destinationFileExists = false; + + if (destinationFileState.DirectoryExists) + { + Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Name, destinationFileState.Name); + return false; + } + + if (sourceFileState.DirectoryExists) + { + // If the source file passed in is actually a directory instead of a file, log a nice + // error telling the user so. Otherwise, .NET Framework's File.Copy method will throw + // an UnauthorizedAccessException saying "access is denied", which is not very useful + // to the user. + Log.LogErrorWithCodeFromResources("Copy.SourceIsDirectory", sourceFileState.Name); + return false; + } + + if (!sourceFileState.FileExists) + { + Log.LogErrorWithCodeFromResources("Copy.SourceFileNotFound", sourceFileState.Name); + return false; + } + + string destinationFolder = Path.GetDirectoryName(destinationFileState.Name); + + if (destinationFolder != null && destinationFolder.Length > 0 && !_directoriesKnownToExist.Contains(destinationFolder)) + { + if (!Directory.Exists(destinationFolder)) + { + Log.LogMessageFromResources(MessageImportance.Normal, "Copy.CreatesDirectory", destinationFolder); + Directory.CreateDirectory(destinationFolder); + } + + // It's very common for a lot of files to be copied to the same folder. + // Eg., "c:\foo\a"->"c:\bar\a", "c:\foo\b"->"c:\bar\b" and so forth. + // We don't want to check whether this folder exists for every single file we copy. So store which we've checked. + _directoriesKnownToExist.Add(destinationFolder); + } + + if (_overwriteReadOnlyFiles) + { + MakeFileWriteable(destinationFileState, true); + destinationFileExists = destinationFileState.FileExists; + } + + bool hardLinkCreated = false; + + // If we want to create hard links, then try that first + if (UseHardlinksIfPossible) + { + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "Copy.HardLinkComment", sourceFileState.Name, destinationFileState.Name); + + if (!_overwriteReadOnlyFiles) + { + destinationFileExists = destinationFileState.FileExists; + } + + // CreateHardLink cannot overwrite an existing file or hard link + // so we need to delete the existing entry before we create the hard link. + // We need to do a best-effort check to see if the files are the same + // if they are the same then we won't delete, just in case they refer to the same + // physical file on disk. + // Since we'll fall back to a copy (below) this will fail and issue a correct + // message in the case that the source and destination are in fact the same file. + if (destinationFileExists && !IsMatchingSizeAndTimeStamp(sourceFileState, destinationFileState)) + { + FileUtilities.DeleteNoThrow(destinationFileState.Name); + } + + hardLinkCreated = NativeMethods.CreateHardLink(destinationFileState.Name, sourceFileState.Name, IntPtr.Zero /* reserved, must be NULL */); + + if (!hardLinkCreated) + { + int errorCode = Marshal.GetHRForLastWin32Error(); + Exception hardLinkException = Marshal.GetExceptionForHR(errorCode); + // This is only a message since we don't want warnings when copying to network shares etc. + Log.LogMessageFromResources(MessageImportance.Low, "Copy.RetryingAsFileCopy", sourceFileState.Name, destinationFileState.Name, hardLinkException.Message); + } + } + + // If the hard link was not created (either because the user didn't want one, or because it couldn't be created) + // then let's copy the file + if (!hardLinkCreated) + { + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "Copy.FileComment", sourceFileState.Name, destinationFileState.Name); + File.Copy(sourceFileState.Name, destinationFileState.Name, true); + } + + destinationFileState.Reset(); + + // If the destinationFile file exists, then make sure it's read-write. + // The File.Copy command copies attributes, but our copy needs to + // leave the file writeable. + if (sourceFileState.IsReadOnly) + { + MakeFileWriteable(destinationFileState, false); + } + + return true; + } + + /// + /// Ensure the read-only attribute on the specified file is off, so + /// the file is writeable. + /// + private void MakeFileWriteable(FileState file, bool logActivity) + { + if (file.FileExists) + { + if (file.IsReadOnly) + { + if (logActivity) + { + Log.LogMessageFromResources(MessageImportance.Low, "Copy.RemovingReadOnlyAttribute", file.Name); + } + + File.SetAttributes(file.Name, FileAttributes.Normal); + file.Reset(); + } + } + } + + /// + /// Copy the files. + /// + /// Delegate used to copy the files. + /// + internal bool Execute + ( + CopyFileWithState copyFile + ) + { + // If there are no source files then just return success. + if (_sourceFiles == null || _sourceFiles.Length == 0) + { + _destinationFiles = new TaskItem[0]; + _copiedFiles = new TaskItem[0]; + return true; + } + + if (!(ValidateInputs() && InitializeDestinationFiles())) + { + return false; + } + + bool success = true; + + // Environment variable stomps on user-requested value if it's set. + if (Environment.GetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES") != null) + { + _overwriteReadOnlyFiles = true; + } + + // Build up the sucessfully copied subset + var destinationFilesSuccessfullyCopied = new List(); + + // Set of files we actually copied and the location from which they were originally copied. The purpose + // of this collection is to let us skip copying duplicate files. We will only copy the file if it + // either has never been copied to this destination before (key doesn't exist) or if we have copied it but + // from a different location (value is different.) + // { dest -> source } + Dictionary filesActuallyCopied = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Now that we have a list of destinationFolder files, copy from source to destinationFolder. + for (int i = 0; i < _sourceFiles.Length && !_canceling; ++i) + { + bool copyComplete = false; + string originalSource; + if (filesActuallyCopied.TryGetValue(_destinationFiles[i].ItemSpec, out originalSource)) + { + if (String.Equals(originalSource, _sourceFiles[i].ItemSpec, StringComparison.OrdinalIgnoreCase)) + { + // Already copied from this location, don't copy again. + copyComplete = true; + } + } + + if (!copyComplete) + { + if (DoCopyIfNecessary(new FileState(_sourceFiles[i].ItemSpec), new FileState(_destinationFiles[i].ItemSpec), copyFile)) + { + filesActuallyCopied[_destinationFiles[i].ItemSpec] = _sourceFiles[i].ItemSpec; + copyComplete = true; + } + else + { + success = false; + } + } + + if (copyComplete) + { + _sourceFiles[i].CopyMetadataTo(_destinationFiles[i]); + destinationFilesSuccessfullyCopied.Add(_destinationFiles[i]); + } + } + + // copiedFiles contains only the copies that were successful. + _copiedFiles = (ITaskItem[])destinationFilesSuccessfullyCopied.ToArray(); + + return success && !_canceling; + } + + /// + /// Verify that the inputs are correct. + /// + /// + private bool ValidateInputs() + { + if (Retries < 0) + { + Log.LogErrorWithCodeFromResources("Copy.InvalidRetryCount", Retries); + return false; + } + + if (RetryDelayMilliseconds < 0) + { + Log.LogErrorWithCodeFromResources("Copy.InvalidRetryDelay", RetryDelayMilliseconds); + return false; + } + + // There must be a destinationFolder (either files or directory). + if (_destinationFiles == null && _destinationFolder == null) + { + Log.LogErrorWithCodeFromResources("Copy.NeedsDestination", "DestinationFiles", "DestinationFolder"); + return false; + } + + // There can't be two kinds of destination. + if (_destinationFiles != null && _destinationFolder != null) + { + Log.LogErrorWithCodeFromResources("Copy.ExactlyOneTypeOfDestination", "DestinationFiles", "DestinationFolder"); + return false; + } + + // If the caller passed in DestinationFiles, then its length must match SourceFiles. + if (_destinationFiles != null && _destinationFiles.Length != _sourceFiles.Length) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", _destinationFiles.Length, _sourceFiles.Length, "DestinationFiles", "SourceFiles"); + return false; + } + + return true; + } + + /// + /// Set up our list of destination files. + /// + /// + private bool InitializeDestinationFiles() + { + if (_destinationFiles == null) + { + // If the caller passed in DestinationFolder, convert it to DestinationFiles + _destinationFiles = new ITaskItem[_sourceFiles.Length]; + + for (int i = 0; i < _sourceFiles.Length; ++i) + { + // Build the correct path. + string destinationFile; + try + { + destinationFile = Path.Combine(_destinationFolder.ItemSpec, Path.GetFileName(_sourceFiles[i].ItemSpec)); + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("Copy.Error", _sourceFiles[i].ItemSpec, _destinationFolder.ItemSpec, e.Message); + // Clear the outputs. + _destinationFiles = new ITaskItem[0]; + return false; + } + + // Initialize the destinationFolder item. + // ItemSpec is unescaped, and the TaskItem constructor expects an escaped input, so we need to + // make sure to re-escape it here. + _destinationFiles[i] = new TaskItem(EscapingUtilities.Escape(destinationFile)); + + // Copy meta-data from source to destinationFolder. + _sourceFiles[i].CopyMetadataTo(_destinationFiles[i]); + } + } + + return true; + } + + /// + /// Copy source to destination, unless SkipUnchangedFiles is true and they are equivalent. + /// + /// + /// + /// + /// + private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationFileState, CopyFileWithState copyFile) + { + bool success = true; + + try + { + if (_skipUnchangedFiles && IsMatchingSizeAndTimeStamp(sourceFileState, destinationFileState)) + { + // If we got here, then the file's time and size match AND + // the user set the SkipUnchangedFiles flag which means we + // should skip matching files. + Log.LogMessageFromResources + ( + MessageImportance.Low, + "Copy.DidNotCopyBecauseOfFileMatch", + sourceFileState.Name, + destinationFileState.Name, + "SkipUnchangedFiles", + "true" + ); + } + // We only do the cheap check for identicalness here, we try the more expensive check + // of comparing the fullpaths of source and destination to see if they are identical, + // in the exception handler lower down. + else if (0 != String.Compare(sourceFileState.Name, destinationFileState.Name, StringComparison.OrdinalIgnoreCase)) + { + success = DoCopyWithRetries(sourceFileState, destinationFileState, copyFile); + } + } + catch (PathTooLongException e) + { + Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Name, destinationFileState.Name, e.Message); + success = false; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Name, destinationFileState.Name, e.Message); + success = false; + } + + return success; + } + + /// + /// Copy one file with the appropriate number of retries if it fails. + /// + private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationFileState, CopyFileWithState copyFile) + { + bool? result = null; + int retries = 0; + + while (true && !_canceling) + { + try + { + result = copyFile(sourceFileState, destinationFileState); + if (result.HasValue) + { + return result.Value; + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + if (e is ArgumentException || // Invalid chars + e is NotSupportedException || // Colon in the middle of the path + e is PathTooLongException) + { + // No use retrying these cases + throw; + } + + if (e is UnauthorizedAccessException || e is IOException) // Not clear why we can get one and not the other + { + int code = Marshal.GetHRForException(e); + + LogDiagnostic("Got {0} copying {1} to {2} and HR is {3}", e.ToString(), sourceFileState.Name, destinationFileState.Name, code); + if (code == Microsoft.Build.Tasks.NativeMethods.ERROR_ACCESS_DENIED) + { + // ERROR_ACCESS_DENIED can either mean there's an ACL preventing us, or the file has the readonly bit set. + // In either case, that's likely not a race, and retrying won't help. + // Retrying is mainly for ERROR_SHARING_VIOLATION, where someone else is using the file right now. + // However, there is a limited set of circumstances where a copy failure will show up as access denied due + // to a failure to reset the readonly bit properly, in which case retrying will succeed. This seems to be + // a pretty edge scenario, but since some of our internal builds appear to be hitting it, provide a secret + // environment variable to allow overriding the default behavior and forcing retries in this circumstance as well. + if (!s_alwaysRetryCopy) + { + throw; + } + else + { + LogDiagnostic("Retrying on ERROR_ACCESS_DENIED because MSBUILDALWAYSRETRY = 1"); + } + } + } + + if (e is IOException && DestinationFolder != null && File.Exists(DestinationFolder.ItemSpec)) + { + // We failed to create the DestinationFolder because it's an existing file. No sense retrying. + // We don't check for this case upstream because it'd be another hit to the filesystem. + throw; + } + + if (e is IOException) + { + // if this was just because the source and destination files are the + // same file, that's not a failure. + // Note -- we check this exceptional case here, not before the copy, for perf. + if (PathsAreIdentical(sourceFileState.Name, destinationFileState.Name)) + { + return true; + } + } + + if (retries < Retries) + { + retries++; + Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, destinationFileState.Name, retries, RetryDelayMilliseconds, e.Message); + + // if we have to retry for some reason, wipe the state -- it may not be correct anymore. + destinationFileState.Reset(); + + Thread.Sleep(RetryDelayMilliseconds); + continue; + } + else if (Retries > 0) + { + // Exception message is logged in caller + Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, destinationFileState.Name, Retries); + throw; + } + else + { + throw; + } + } + + if (retries < Retries) + { + retries++; + Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, destinationFileState.Name, retries, RetryDelayMilliseconds, String.Empty /* no details */); + + // if we have to retry for some reason, wipe the state -- it may not be correct anymore. + destinationFileState.Reset(); + + Thread.Sleep(RetryDelayMilliseconds); + } + else if (Retries > 0) + { + Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, destinationFileState.Name, Retries); + return false; + } + else + { + return false; + } + } + + // Canceling + return false; + } + + /// + /// Standard entry point. + /// + /// + public override bool Execute() + { + return Execute + ( + new CopyFileWithState(CopyFileWithLogging) + ); + } + + /// + /// Compares two paths to see if they refer to the same file. We can't solve the general + /// canonicalization problem, so we just compare strings on the full paths. + /// + private bool PathsAreIdentical(string source, string destination) + { + string fullSourcePath = Path.GetFullPath(source); + string fullDestinationPath = Path.GetFullPath(destination); + return (0 == String.Compare(fullSourcePath, fullDestinationPath, StringComparison.OrdinalIgnoreCase)); + } + + #endregion + } +} diff --git a/src/XMakeTasks/CreateCSharpManifestResourceName.cs b/src/XMakeTasks/CreateCSharpManifestResourceName.cs new file mode 100644 index 00000000000..1c52e5e25b5 --- /dev/null +++ b/src/XMakeTasks/CreateCSharpManifestResourceName.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for task that determines the appropriate manifest resource name to + /// assign to a given resx or other resource. + /// + public class CreateCSharpManifestResourceName : CreateManifestResourceName + { + /// + /// Utility function for creating a C#-style manifest name from + /// a resource name. + /// + /// The file name of the dependent (usually a .resx) + /// The file name of the dependent (usually a .resx) + /// The root namespace (usually from the project file). May be null + /// The file name of the parent of this dependency (usually a .cs file). May be null + /// File contents binary stream, may be null + /// Returns the manifest name + override protected string CreateManifestName + ( + string fileName, + string linkFileName, + string rootNamespace, + string dependentUponFileName, + Stream binaryStream + ) + { + ITaskItem item = null; + string culture = null; + if (fileName != null && itemSpecToTaskitem.TryGetValue(fileName, out item)) + { + culture = item.GetMetadata("Culture"); + } + + /* + Actual implementation is in a static method called CreateManifestNameImpl. + The reason is that CreateManifestName can't be static because it is an + override of a method declared in the base class, but its convenient + to expose a static version anyway for unittesting purposes. + */ + return CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + fileName, + linkFileName, + PrependCultureAsDirectory, + rootNamespace, + dependentUponFileName, + culture, + binaryStream, + this.Log + ); + } + + /// + /// Utility function for creating a C#-style manifest name from + /// a resource name. Note that this function attempts to emulate the + /// Everret implementation of this code which can be found by searching for + /// ComputeNonWFCResourceName() or ComputeWFCResourceName() in + /// \vsproject\langproj\langbldmgrsite.cpp + /// + /// The file name of the dependent (usually a .resx) + /// The file name of the dependent (usually a .resx) + /// The root namespace (usually from the project file). May be null + /// should the culture name be prepended to the manifest name as a path + /// The file name of the parent of this dependency (usually a .cs file). May be null + /// The override culture of this resource, if any + /// File contents binary stream, may be null + /// Task's TaskLoggingHelper, for logging warnings or errors + /// Returns the manifest name + internal static string CreateManifestNameImpl + ( + string fileName, + string linkFileName, + bool prependCultureAsDirectory, // true by default + string rootNamespace, // May be null + string dependentUponFileName, // May be null + string culture, // may be null + Stream binaryStream, // File contents binary stream, may be null + TaskLoggingHelper log + ) + { + // Use the link file name if there is one, otherwise, fall back to file name. + string embeddedFileName = linkFileName; + if (embeddedFileName == null || embeddedFileName.Length == 0) + { + embeddedFileName = fileName; + } + + Culture.ItemCultureInfo info = Culture.GetItemCultureInfo(embeddedFileName, dependentUponFileName); + + // If the item has a culture override, respect that. + if (!String.IsNullOrEmpty(culture)) + { + info.culture = culture; + } + + StringBuilder manifestName = new StringBuilder(); + if (binaryStream != null) + { + // Resource depends on a form. Now, get the form's class name fully + // qualified with a namespace. + ExtractedClassName result = CSharpParserUtilities.GetFirstClassNameFullyQualified(binaryStream); + + if (result.IsInsideConditionalBlock && log != null) + { + log.LogWarningWithCodeFromResources("CreateManifestResourceName.DefinitionFoundWithinConditionalDirective", dependentUponFileName, embeddedFileName); + } + + if (result.Name != null && result.Name.Length > 0) + { + manifestName.Append(result.Name); + + // Append the culture if there is one. + if (info.culture != null && info.culture.Length > 0) + { + manifestName.Append(".").Append(info.culture); + } + } + } + + // If there's no manifest name at this point, then fall back to using the + // RootNamespace+Filename_with_slashes_converted_to_dots + if (manifestName.Length == 0) + { + // If Rootnamespace was null, then it wasn't set from the project resourceFile. + // Empty namespaces are allowed. + if ((rootNamespace != null) && (rootNamespace.Length > 0)) + { + manifestName.Append(rootNamespace).Append("."); + } + + // Replace spaces in the directory name with underscores. Needed for compatibility with Everett. + // Note that spaces in the file name itself are preserved. + string everettCompatibleDirectoryName = CreateManifestResourceName.MakeValidEverettIdentifier(Path.GetDirectoryName(info.cultureNeutralFilename)); + + // only strip extension for .resx and .restext files + + string sourceExtension = Path.GetExtension(info.cultureNeutralFilename); + if ( + (0 == String.Compare(sourceExtension, ".resx", StringComparison.OrdinalIgnoreCase)) + || + (0 == String.Compare(sourceExtension, ".restext", StringComparison.OrdinalIgnoreCase)) + || + (0 == String.Compare(sourceExtension, ".resources", StringComparison.OrdinalIgnoreCase)) + ) + { + manifestName.Append(Path.Combine(everettCompatibleDirectoryName, Path.GetFileNameWithoutExtension(info.cultureNeutralFilename))); + + // Replace all '\' with '.' + manifestName.Replace(Path.DirectorySeparatorChar, '.'); + manifestName.Replace(Path.AltDirectorySeparatorChar, '.'); + + // Append the culture if there is one. + if (info.culture != null && info.culture.Length > 0) + { + manifestName.Append(".").Append(info.culture); + } + + // If the original extension was .resources, add it back + if (String.Equals(sourceExtension, ".resources", StringComparison.OrdinalIgnoreCase)) + { + manifestName.Append(sourceExtension); + } + } + else + { + manifestName.Append(Path.Combine(everettCompatibleDirectoryName, Path.GetFileName(info.cultureNeutralFilename))); + + // Replace all '\' with '.' + manifestName.Replace(Path.DirectorySeparatorChar, '.'); + manifestName.Replace(Path.AltDirectorySeparatorChar, '.'); + + if (prependCultureAsDirectory) + { + // Prepend the culture as a subdirectory if there is one. + if (info.culture != null && info.culture.Length > 0) + { + manifestName.Insert(0, Path.DirectorySeparatorChar); + manifestName.Insert(0, info.culture); + } + } + } + } + + return manifestName.ToString(); + } + + /// + /// Return 'true' if this is a C# source file. + /// + /// Name of the candidate source file. + /// True, if this is a validate source file. + override protected bool IsSourceFile(string fileName) + { + string extension = Path.GetExtension(fileName); + return (String.Compare(extension, ".cs", StringComparison.OrdinalIgnoreCase) == 0); + } + } +} diff --git a/src/XMakeTasks/CreateItem.cs b/src/XMakeTasks/CreateItem.cs new file mode 100644 index 00000000000..0abb7c5b782 --- /dev/null +++ b/src/XMakeTasks/CreateItem.cs @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Microsoft.Build.Tasks +{ + /// + /// Forward a list of items from input to output. This allows dynamic item lists. + /// + public class CreateItem : TaskExtension + { + #region Properties + + private ITaskItem[] _include; + private ITaskItem[] _exclude; + private string[] _additionalMetadata; + private bool _preserveExistingMetadata = false; + + [Output] + public ITaskItem[] Include + { + get + { + return _include; + } + + set + { + _include = value; + } + } + + public ITaskItem[] Exclude + { + get + { + return _exclude; + } + + set + { + _exclude = value; + } + } + + /// + /// Only apply the additional metadata is none already exists + /// + public bool PreserveExistingMetadata + { + get + { + return _preserveExistingMetadata; + } + + set + { + _preserveExistingMetadata = value; + } + } + + /// + /// A list of metadata name/value pairs to apply to the output items. + /// A typical input: "metadataname1=metadatavalue1", "metadataname2=metadatavalue2", ... + /// + /// + /// 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[] AdditionalMetadata + { + get + { + return _additionalMetadata; + } + set + { + _additionalMetadata = value; + } + } + + #endregion + + #region ITask Members + /// + /// Execute. + /// + public override bool Execute() + { + if (Include == null) + { + _include = new TaskItem[0]; + return true; + } + + // Expand wild cards. + Include = ExpandWildcards(Include); + Exclude = ExpandWildcards(Exclude); + + // Simple case: no additional attribute to add and no Exclude. In this case the + // ouptuts are simply the inputs. + if (AdditionalMetadata == null && Exclude == null) + { + return true; + } + + // Parse the global properties into a hashtable. + Hashtable metadataTable; + if (!PropertyParser.GetTable(Log, "AdditionalMetadata", this.AdditionalMetadata, out metadataTable)) + { + return false; + } + + + // Build a table of unique items. + Hashtable excludeItems = GetUniqueItems(Exclude); + + // Produce the output items, add attribute and honor exclude. + ArrayList outputItems = CreateOutputItems(metadataTable, excludeItems); + + _include = (ITaskItem[])outputItems.ToArray(typeof(ITaskItem)); + + return !Log.HasLoggedErrors; + } + + /// + /// Create the list of output items. + /// + /// Whether attributes need to be set. + /// Items to exclude. + private ArrayList CreateOutputItems(Hashtable metadataTable, Hashtable excludeItems) + { + ArrayList outputItems = new ArrayList(); + + for (int i = 0; i < Include.Length; i++) + { + if ( + (excludeItems.Count == 0) || // minor perf optimization + (!excludeItems.ContainsKey(Include[i].ItemSpec)) + ) + { + ITaskItem newItem = _include[i]; + if (null != metadataTable) + { + foreach (DictionaryEntry nameAndValue in metadataTable) + { + // 1. If we have been asked to not preserve existing metadata then overwrite + // 2. If there is no existing metadata then apply the new + if ((!_preserveExistingMetadata) || String.IsNullOrEmpty(newItem.GetMetadata((string)nameAndValue.Key))) + { + if (FileUtilities.ItemSpecModifiers.IsItemSpecModifier((string)nameAndValue.Key)) + { + // Explicitly setting built-in metadata, is not allowed. + Log.LogErrorWithCodeFromResources("CreateItem.AdditionalMetadataError", (string)nameAndValue.Key); + break; + } + + newItem.SetMetadata((string)nameAndValue.Key, (string)nameAndValue.Value); + } + } + } + outputItems.Add(newItem); + } + } + return outputItems; + } + + /// + /// Expand wildcards in the item list. + /// + /// + /// + private static ITaskItem[] ExpandWildcards(ITaskItem[] expand) + { + if (expand == null) + { + return null; + } + else + { + ArrayList expanded = new ArrayList(); + foreach (ITaskItem i in expand) + { + if (FileMatcher.HasWildcards(i.ItemSpec)) + { + string[] files = FileMatcher.GetFiles(null /* use current directory */, i.ItemSpec); + foreach (string file in files) + { + TaskItem newItem = new TaskItem((ITaskItem)i); + newItem.ItemSpec = file; + + // Compute the RecursiveDir portion. + FileMatcher.Result match = FileMatcher.FileMatch(i.ItemSpec, file); + if (match.isLegalFileSpec && match.isMatch) + { + if (match.wildcardDirectoryPart != null && match.wildcardDirectoryPart.Length > 0) + { + newItem.SetMetadata(FileUtilities.ItemSpecModifiers.RecursiveDir, match.wildcardDirectoryPart); + } + } + + expanded.Add(newItem); + } + } + else + { + expanded.Add(i); + } + } + return (ITaskItem[])expanded.ToArray(typeof(ITaskItem)); + } + } + + /// + /// Create a table of unique items + /// + /// + private static Hashtable GetUniqueItems(ITaskItem[] items) + { + Hashtable uniqueItems = new Hashtable(StringComparer.OrdinalIgnoreCase); + + if (items != null) + { + foreach (ITaskItem item in items) + { + uniqueItems[item.ItemSpec] = String.Empty; + } + } + return uniqueItems; + } + + #endregion + } +} diff --git a/src/XMakeTasks/CreateManifestResourceName.cs b/src/XMakeTasks/CreateManifestResourceName.cs new file mode 100644 index 00000000000..a172aff8a16 --- /dev/null +++ b/src/XMakeTasks/CreateManifestResourceName.cs @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for task that determines the appropriate manifest resource name to + /// assign to a given resx or other resource. + /// + public abstract class CreateManifestResourceName : TaskExtension + { + #region Properties + + private ITaskItem[] _resourceFiles = null; + private string _rootNamespace = null; + private ITaskItem[] _manifestResourceNames = null; + private ITaskItem[] _resourceFilesWithManifestResourceNames = null; + private bool _prependCultureAsDirectory = true; + + [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields", Justification = "Shipped this way in Dev11 Beta (go-live)")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Taskitem", Justification = "Shipped this way in Dev11 Beta (go-live)")] + protected Dictionary itemSpecToTaskitem = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Should the culture name be prepended to the manifest resource name as a directory? + /// This is true by default. + /// + public bool PrependCultureAsDirectory + { + get { return _prependCultureAsDirectory; } + set { _prependCultureAsDirectory = value; } + } + + /// + /// The possibly dependent resource files. + /// + [Required] + public ITaskItem[] ResourceFiles + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_resourceFiles, "resourceFiles"); + return _resourceFiles; + } + set { _resourceFiles = value; } + } + + /// + /// Rootnamespace to use for naming. + /// + public string RootNamespace + { + get { return _rootNamespace; } + set { _rootNamespace = value; } + } + + /// + /// The resulting manifest names. + /// + /// + + [Output] + public ITaskItem[] ManifestResourceNames + { + get { return _manifestResourceNames; } + } + + /// + /// The initial list of resource names, with additional metadata for manifest resource names + /// + [Output] + public ITaskItem[] ResourceFilesWithManifestResourceNames + { + get { return _resourceFilesWithManifestResourceNames; } + set { _resourceFilesWithManifestResourceNames = value; } + } + #endregion + + + /// + /// Method in the derived class that composes the manifest name. + /// + /// The file name of the dependent (usually a .resx) + /// The name of the file specified by the Link attribute. + /// The root namespace (usually from the project file). May be null + /// The file name of the parent of this dependency. May be null + /// The override culture of this resource, if any + /// File contents binary stream, may be null + /// Returns the manifest name + abstract protected string CreateManifestName + ( + string fileName, + string linkFileName, + string rootNamespaceName, + string dependentUponFileName, + Stream binaryStream + ); + + /// + /// The derived class chooses whether this is a valid source file to work against. + /// Usually, this is just a matter of looking at the file's extension. + /// + /// Name of the candidate source file. + /// True, if this is a validate source file. + abstract protected bool IsSourceFile(string fileName); + + /// + /// Given a file path, return a stream on top of that path. + /// + /// Path to the file + /// File mode + /// Access type + /// The FileStream + private Stream CreateFileStreamOverNewFileStream(string path, FileMode mode, FileAccess access) + { + return new FileStream(path, mode, access); + } + + #region ITask Members + /// + /// Execute the task with delegate handlers. + /// + /// CreateFileStream delegate + /// True if task succeeded. + internal bool Execute + ( + CreateFileStream createFileStream + ) + { + _manifestResourceNames = new TaskItem[ResourceFiles.Length]; + _resourceFilesWithManifestResourceNames = new TaskItem[ResourceFiles.Length]; + + bool success = true; + int i = 0; + + // If Rootnamespace was null, then it wasn't set from the project resourceFile. + // Empty namespaces are allowed. + if (RootNamespace != null) + { + Log.LogMessageFromResources(MessageImportance.Low, "CreateManifestResourceName.RootNamespace", _rootNamespace); + } + else + { + Log.LogMessageFromResources(MessageImportance.Low, "CreateManifestResourceName.NoRootNamespace"); + } + + + foreach (ITaskItem resourceFile in ResourceFiles) + { + try + { + string fileName = resourceFile.ItemSpec; + string dependentUpon = (string)resourceFile.GetMetadata(ItemMetadataNames.dependentUpon); + + // Pre-log some information. + bool isDependentOnSourceFile = (dependentUpon != null) && (dependentUpon.Length > 0) && IsSourceFile(dependentUpon); + + if (isDependentOnSourceFile) + { + Log.LogMessageFromResources(MessageImportance.Low, "CreateManifestResourceName.DependsUpon", fileName, dependentUpon); + } + else + { + Log.LogMessageFromResources(MessageImportance.Low, "CreateManifestResourceName.DependsUponNothing", fileName); + } + + // Create the manifest name. + Stream binaryStream = null; + string manifestName; + + if (isDependentOnSourceFile) + { + string pathToDependent = Path.Combine(Path.GetDirectoryName(fileName), dependentUpon); + binaryStream = createFileStream(pathToDependent, FileMode.Open, FileAccess.Read); + } + + // Put the task item into a dictionary so we can access it from a derrived class quickly. + itemSpecToTaskitem[resourceFile.ItemSpec] = resourceFile; + + // This "using" statement ensures that the "binaryStream" will be disposed once + // we're done with it. + using (binaryStream) + { + manifestName = CreateManifestName + ( + fileName, + resourceFile.GetMetadata(ItemMetadataNames.targetPath), + RootNamespace, + isDependentOnSourceFile ? dependentUpon : null, + binaryStream + ); + } + + // Emit an item with our manifest name. + _manifestResourceNames[i] = new TaskItem(resourceFile); + _manifestResourceNames[i].ItemSpec = manifestName; + + // Emit a new item preserving the itemSpec of the resourceFile, but with new metadata for manifest resource name + _resourceFilesWithManifestResourceNames[i] = new TaskItem(resourceFile); + _resourceFilesWithManifestResourceNames[i].SetMetadata("ManifestResourceName", manifestName); + + // Add a LogicalName metadata to Non-Resx resources + // LogicalName isn't used for Resx resources because the ManifestResourceName metadata determines the filename of the + // .resources file which then is used as the embedded resource manifest name + if (String.IsNullOrEmpty(_resourceFilesWithManifestResourceNames[i].GetMetadata("LogicalName")) && + String.Equals(_resourceFilesWithManifestResourceNames[i].GetMetadata("Type"), "Non-Resx", StringComparison.OrdinalIgnoreCase)) + { + _resourceFilesWithManifestResourceNames[i].SetMetadata("LogicalName", manifestName); + } + + // Post-logging + Log.LogMessageFromResources(MessageImportance.Low, "CreateManifestResourceName.AssignedName", fileName, manifestName); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("CreateManifestResourceName.Error", resourceFile.ItemSpec, e.Message); + success = false; + } + + ++i; + } + + return success; + } + + /// + /// Do the task's work. + /// + /// True if succeeded. + public override bool Execute() + { + return Execute + ( + new CreateFileStream(CreateFileStreamOverNewFileStream) + ); + } + + #endregion + + #region Helper methods + + /// + /// Is the character a valid first Everett identifier character? + /// + private static bool IsValidEverettIdFirstChar(char c) + { + return + char.IsLetter(c) || + char.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation; + } + + /// + /// Is the character a valid Everett identifier character? + /// + private static bool IsValidEverettIdChar(char c) + { + UnicodeCategory cat = char.GetUnicodeCategory(c); + + return + char.IsLetterOrDigit(c) || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.EnclosingMark; + } + + /// + /// Make a folder subname into an Everett-compatible identifier + /// + private static string MakeValidEverettSubFolderIdentifier(string subName) + { + ErrorUtilities.VerifyThrowArgumentNull(subName, "subName"); + + if (subName.Length == 0) + return subName; + + // give string length to avoid reallocations; +1 since the resulting string may be one char longer than the + // original - if the first character is an invalid first identifier character but a valid subsequent one, + // we prepend an underscore to it. + StringBuilder everettId = new StringBuilder(subName.Length + 1); + + // the first character has stronger restrictions than the rest + if (!IsValidEverettIdFirstChar(subName[0])) + { + // if the first character is not even a valid subsequent character, replace it with an underscore + if (!IsValidEverettIdChar(subName[0])) + { + everettId.Append('_'); + } + // if it is a valid subsequent character, prepend an underscore to it + else + { + everettId.Append('_'); + everettId.Append(subName[0]); + } + } + else + { + everettId.Append(subName[0]); + } + + // process the rest of the subname + for (int i = 1; i < subName.Length; i++) + { + if (!IsValidEverettIdChar(subName[i])) + { + everettId.Append('_'); + } + else + { + everettId.Append(subName[i]); + } + } + + return everettId.ToString(); + } + + /// + /// Make a folder name into an Everett-compatible identifier + /// + internal static string MakeValidEverettFolderIdentifier(string name) + { + ErrorUtilities.VerifyThrowArgumentNull(name, "name"); + + // give string length to avoid reallocations; +1 since the resulting string may be one char longer than the + // original - if the name is a single underscore we add another underscore to it + StringBuilder everettId = new StringBuilder(name.Length + 1); + + // split folder name into subnames separated by '.', if any + string[] subNames = name.Split(new char[] { '.' }); + + // convert each subname separately + everettId.Append(MakeValidEverettSubFolderIdentifier(subNames[0])); + + for (int i = 1; i < subNames.Length; i++) + { + everettId.Append('.'); + everettId.Append(MakeValidEverettSubFolderIdentifier(subNames[i])); + } + + // folder name cannot be a single underscore - add another underscore to it + if (everettId.ToString() == "_") + everettId.Append('_'); + + return everettId.ToString(); + } + + /// + /// This method is provided for compatibility with Everett which used to convert parts of resource names into + /// valid identifiers + /// + /// + /// + public static string MakeValidEverettIdentifier(string name) + { + ErrorUtilities.VerifyThrowArgumentNull(name, "name"); + + StringBuilder everettId = new StringBuilder(name.Length); + + // split the name into folder names + string[] subNames = name.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + + // convert every folder name + everettId.Append(MakeValidEverettFolderIdentifier(subNames[0])); + + for (int i = 1; i < subNames.Length; i++) + { + everettId.Append('.'); + everettId.Append(MakeValidEverettFolderIdentifier(subNames[i])); + } + + return everettId.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeTasks/CreateProperty.cs b/src/XMakeTasks/CreateProperty.cs new file mode 100644 index 00000000000..88bdc10bc0a --- /dev/null +++ b/src/XMakeTasks/CreateProperty.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Microsoft.Build.Tasks +{ + /// + /// Just a straight pass-through of the inputs through to the outputs. + /// + public class CreateProperty : TaskExtension + { + private string[] _prop; + + /// + /// The in/out property value. + /// + /// + /// So ... why is this a string[] instead of a string? + /// Basically if the project author passed in: + /// + /// CreateProperty Value="Clean;Build" + /// Output TaskParameter="Value" PropertyName="MyTargetsToBuild" + /// /CreateProperty + /// + /// We need to respect the semicolon that he put in the value, and need to treat + /// this exactly as if he had done: + /// + /// PropertyGroup + /// MyTargetsToBuild="Clean;Build" + /// /PropertyGroup + /// + /// If we make this parameter a "string", then the engine will escape the + /// value on the way out from the task back to the engine, creating a property + /// that is set to "Clean%3BBuild", which is not what the user wanted. + /// + [Output] + public string[] Value + { + get + { + return _prop; + } + + set + { + _prop = value; + } + } + + /// + /// This is to fool MSBuild into not doing its little TLDA trick whereby even if + /// a target is up-to-date, it will still set the properties that were meant to + /// be set using the CreateProperty task. This is because MSBuild is smart enough + /// to figure out the value of the output property without running the task. + /// But if the input parameter is differently named than the output parameter, + /// MSBuild can't be smart enough to do that. This is an important scenario + /// for people who want to know whether a particular target was up-to-date or not. + /// + [Output] + public string[] ValueSetByTask + { + get + { + return _prop; + } + } + + /// + /// Create the property. Since the input property is the same as the + /// output property, this is rather easy. + /// + /// + public override bool Execute() + { + if (_prop == null) + { + _prop = new string[0]; + } + + return true; + } + } +} diff --git a/src/XMakeTasks/CreateVisualBasicManifestResourceName.cs b/src/XMakeTasks/CreateVisualBasicManifestResourceName.cs new file mode 100644 index 00000000000..1399fc12576 --- /dev/null +++ b/src/XMakeTasks/CreateVisualBasicManifestResourceName.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for task that determines the appropriate manifest resource name to + /// assign to a given resx or other resource. + /// + public class CreateVisualBasicManifestResourceName : CreateManifestResourceName + { + /// + /// Utility function for creating a VB-style manifest name from + /// a resource name. + /// + /// The file name of the dependent (usually a .resx) + /// The file name of the dependent (usually a .resx) + /// The root namespace (usually from the project file). May be null + /// The file name of the parent of this dependency (usually a .vb file). May be null + /// File contents binary stream, may be null + /// Returns the manifest name + override protected string CreateManifestName + ( + string fileName, + string linkFileName, + string rootNamespace, // May be null + string dependentUponFileName, // May be null + Stream binaryStream // File contents binary stream, may be null + ) + { + ITaskItem item = null; + string culture = null; + if (fileName != null && itemSpecToTaskitem.TryGetValue(fileName, out item)) + { + culture = item.GetMetadata("Culture"); + } + + /* + Actual implementation is in a static method called CreateManifestNameImpl. + The reason is that CreateManifestName can't be static because it is an + override of a method declared in the base class, but its convenient + to expose a static version anyway for unittesting purposes. + */ + return CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + fileName, + linkFileName, + PrependCultureAsDirectory, + rootNamespace, + dependentUponFileName, + culture, + binaryStream, + this.Log + ); + } + + /// + /// Utility function for creating a VB-style manifest name from + /// a resource name. Note that this function attempts to emulate the + /// Everret implementation of this code which can be found by searching for + /// ComputeNonWFCResourceName() or ComputeWFCResourceName() in + /// \vsproject\langproj\langbldmgrsite.cpp + /// + /// The file name of the dependent (usually a .resx) + /// The file name of the dependent (usually a .resx) + /// should the culture name be prepended to the manifest name as a path + /// The root namespace (usually from the project file). May be null + /// The file name of the parent of this dependency (usually a .vb file). May be null + /// The override culture of this resource, if any + /// File contents binary stream, may be null + /// Task's TaskLoggingHelper, for logging warnings or errors + /// Returns the manifest name + internal static string CreateManifestNameImpl + ( + string fileName, + string linkFileName, + bool prependCultureAsDirectory, // true by default + string rootNamespace, // May be null + string dependentUponFileName, // May be null + string culture, + Stream binaryStream, // File contents binary stream, may be null + TaskLoggingHelper log + ) + { + // Use the link file name if there is one, otherwise, fall back to file name. + string embeddedFileName = linkFileName; + if (embeddedFileName == null || embeddedFileName.Length == 0) + { + embeddedFileName = fileName; + } + + Culture.ItemCultureInfo info = Culture.GetItemCultureInfo(embeddedFileName, dependentUponFileName); + + // If the item has a culture override, respect that. + if (!String.IsNullOrEmpty(culture)) + { + info.culture = culture; + } + + StringBuilder manifestName = new StringBuilder(); + if (binaryStream != null) + { + // Resource depends on a form. Now, get the form's class name fully + // qualified with a namespace. + ExtractedClassName result = VisualBasicParserUtilities.GetFirstClassNameFullyQualified(binaryStream); + + if (result.IsInsideConditionalBlock && log != null) + { + log.LogWarningWithCodeFromResources("CreateManifestResourceName.DefinitionFoundWithinConditionalDirective", dependentUponFileName, embeddedFileName); + } + + if (result.Name != null && result.Name.Length > 0) + { + if (rootNamespace != null && rootNamespace.Length > 0) + { + manifestName.Append(rootNamespace).Append(".").Append(result.Name); + } + else + { + manifestName.Append(result.Name); + } + + // Append the culture if there is one. + if (info.culture != null && info.culture.Length > 0) + { + manifestName.Append(".").Append(info.culture); + } + } + } + + // If there's no manifest name at this point, then fall back to using the + // RootNamespace+Base file name + if (manifestName.Length == 0) + { + // If Rootnamespace was null, then it wasn't set from the project resourceFile. + // Empty namespaces are allowed. + if (rootNamespace != null && rootNamespace.Length > 0) + { + manifestName.Append(rootNamespace).Append("."); + } + + // only strip extension for .resx and .restext files + string sourceExtension = Path.GetExtension(info.cultureNeutralFilename); + if ( + (0 == String.Compare(sourceExtension, ".resx", StringComparison.OrdinalIgnoreCase)) + || + (0 == String.Compare(sourceExtension, ".restext", StringComparison.OrdinalIgnoreCase)) + || + (0 == String.Compare(sourceExtension, ".resources", StringComparison.OrdinalIgnoreCase)) + ) + { + manifestName.Append(Path.GetFileNameWithoutExtension(info.cultureNeutralFilename)); + + // Append the culture if there is one. + if (info.culture != null && info.culture.Length > 0) + { + manifestName.Append(".").Append(info.culture); + } + + // If the original extension was .resources, add it back + if (String.Equals(sourceExtension, ".resources", StringComparison.OrdinalIgnoreCase)) + { + manifestName.Append(sourceExtension); + } + } + else + { + manifestName.Append(Path.GetFileName(info.cultureNeutralFilename)); + + if (prependCultureAsDirectory) + { + // Prepend the culture as a subdirectory if there is one. + if (info.culture != null && info.culture.Length > 0) + { + manifestName.Insert(0, Path.DirectorySeparatorChar); + manifestName.Insert(0, info.culture); + } + } + } + } + + return manifestName.ToString(); + } + + /// + /// Return 'true' if this is a VB source file. + /// + /// Name of the candidate source file. + /// True, if this is a validate source file. + override protected bool IsSourceFile(string fileName) + { + string extension = Path.GetExtension(fileName); + + return (String.Compare(extension, ".vb", StringComparison.OrdinalIgnoreCase) == 0); + } + } +} diff --git a/src/XMakeTasks/Csc.cs b/src/XMakeTasks/Csc.cs new file mode 100644 index 00000000000..9b1953524ed --- /dev/null +++ b/src/XMakeTasks/Csc.cs @@ -0,0 +1,834 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.LanguageParser; +using Microsoft.Build.Tasks.Hosting; +using Microsoft.Build.Tasks.InteropUtilities; +using Microsoft.Build.Utilities; + +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines the "Csc" XMake task, which enables building assemblies from C# + /// source files by invoking the C# compiler. This is the new Roslyn XMake task, + /// meaning that the code is compiled by using the Roslyn compiler server, rather + /// than csc.exe. The two should be functionally identical, but the compiler server + /// should be significantly faster with larger projects and have a smaller memory + /// footprint. + /// + public class Csc : ManagedCompiler + { + private bool _useHostCompilerIfAvailable = false; + + #region Properties + + // Please keep these alphabetized. These are the parameters specific to Csc. The + // ones shared between Vbc and Csc are defined in ManagedCompiler.cs, which is + // the base class. + + public bool AllowUnsafeBlocks + { + set { Bag["AllowUnsafeBlocks"] = value; } + get { return GetBoolParameterWithDefault("AllowUnsafeBlocks", false); } + } + + public string ApplicationConfiguration + { + set { Bag["ApplicationConfiguration"] = value; } + get { return (string)Bag["ApplicationConfiguration"]; } + } + + public string BaseAddress + { + set { Bag["BaseAddress"] = value; } + get { return (string)Bag["BaseAddress"]; } + } + + public bool CheckForOverflowUnderflow + { + set { Bag["CheckForOverflowUnderflow"] = value; } + get { return GetBoolParameterWithDefault("CheckForOverflowUnderflow", false); } + } + + public string DocumentationFile + { + set { Bag["DocumentationFile"] = value; } + get { return (string)Bag["DocumentationFile"]; } + } + + public string DisabledWarnings + { + set { Bag["DisabledWarnings"] = value; } + get { return (string)Bag["DisabledWarnings"]; } + } + + public bool ErrorEndLocation + { + set { Bag["ErrorEndLocation"] = value; } + get { return GetBoolParameterWithDefault("ErrorEndLocation", false); } + } + + public string ErrorReport + { + set { Bag["ErrorReport"] = value; } + get { return (string)Bag["ErrorReport"]; } + } + + public bool GenerateFullPaths + { + set { Bag["GenerateFullPaths"] = value; } + get { return GetBoolParameterWithDefault("GenerateFullPaths", false); } + } + + public string LangVersion + { + set { Bag["LangVersion"] = value; } + get { return (string)Bag["LangVersion"]; } + } + + public string ModuleAssemblyName + { + set { Bag["ModuleAssemblyName"] = value; } + get { return (string)Bag["ModuleAssemblyName"]; } + } + + public bool NoStandardLib + { + set { Bag["NoStandardLib"] = value; } + get { return GetBoolParameterWithDefault("NoStandardLib", false); } + } + + public string PdbFile + { + set { Bag["PdbFile"] = value; } + get { return (string)Bag["PdbFile"]; } + } + + /// + /// Name of the language passed to "/preferreduilang" compiler option. + /// + /// + /// If set to null, "/preferreduilang" option is omitted, and csc.exe uses its default setting. + /// Otherwise, the value is passed to "/preferreduilang" as is. + /// + public string PreferredUILang + { + set { Bag["PreferredUILang"] = value; } + get { return (string)Bag["PreferredUILang"]; } + } + + public string VsSessionGuid + { + set { Bag["VsSessionGuid"] = value; } + get { return (string)Bag["VsSessionGuid"]; } + } + + public bool UseHostCompilerIfAvailable + { + set { _useHostCompilerIfAvailable = value; } + get { return _useHostCompilerIfAvailable; } + } + + public int WarningLevel + { + set { Bag["WarningLevel"] = value; } + get { return GetIntParameterWithDefault("WarningLevel", 4); } + } + + public string WarningsAsErrors + { + set { Bag["WarningsAsErrors"] = value; } + get { return (string)Bag["WarningsAsErrors"]; } + } + + public string WarningsNotAsErrors + { + set { Bag["WarningsNotAsErrors"] = value; } + get { return (string)Bag["WarningsNotAsErrors"]; } + } + + #endregion + + #region Tool Members + + /// + /// Return the name of the tool to execute. + /// + override protected string ToolName + { + get + { + return "csc2.exe"; + } + } + + /// + /// Return the path to the tool to execute. + /// + override protected string GenerateFullPathToTool() + { + string pathToTool = ToolLocationHelper.GetPathToBuildToolsFile(ToolName, ToolLocationHelper.CurrentToolsVersion); + + if (null == pathToTool) + { + pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest); + + if (null == pathToTool) + { + Log.LogErrorWithCodeFromResources("General.FrameworksFileNotFound", ToolName, ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.VersionLatest)); + } + } + + return pathToTool; + } + + /// + /// Fills the provided CommandLineBuilderExtension with those switches and other information that can go into a response file. + /// + override protected internal void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendSwitchIfNotNull("/lib:", this.AdditionalLibPaths, ","); + commandLine.AppendPlusOrMinusSwitch("/unsafe", this.Bag, "AllowUnsafeBlocks"); + commandLine.AppendPlusOrMinusSwitch("/checked", this.Bag, "CheckForOverflowUnderflow"); + commandLine.AppendSwitchWithSplitting("/nowarn:", this.DisabledWarnings, ",", ';', ','); + commandLine.AppendWhenTrue("/fullpaths", this.Bag, "GenerateFullPaths"); + commandLine.AppendSwitchIfNotNull("/langversion:", this.LangVersion); + commandLine.AppendSwitchIfNotNull("/moduleassemblyname:", this.ModuleAssemblyName); + commandLine.AppendSwitchIfNotNull("/pdb:", this.PdbFile); + commandLine.AppendPlusOrMinusSwitch("/nostdlib", this.Bag, "NoStandardLib"); + commandLine.AppendSwitchIfNotNull("/platform:", this.PlatformWith32BitPreference); + commandLine.AppendSwitchIfNotNull("/errorreport:", this.ErrorReport); + commandLine.AppendSwitchWithInteger("/warn:", this.Bag, "WarningLevel"); + commandLine.AppendSwitchIfNotNull("/doc:", this.DocumentationFile); + commandLine.AppendSwitchIfNotNull("/baseaddress:", this.BaseAddress); + commandLine.AppendSwitchUnquotedIfNotNull("/define:", this.GetDefineConstantsSwitch(this.DefineConstants)); + commandLine.AppendSwitchIfNotNull("/win32res:", this.Win32Resource); + commandLine.AppendSwitchIfNotNull("/main:", this.MainEntryPoint); + commandLine.AppendSwitchIfNotNull("/appconfig:", this.ApplicationConfiguration); + commandLine.AppendWhenTrue("/errorendlocation", this.Bag, "ErrorEndLocation"); + commandLine.AppendSwitchIfNotNull("/preferreduilang:", this.PreferredUILang); + commandLine.AppendPlusOrMinusSwitch("/highentropyva", this.Bag, "HighEntropyVA"); + + // If not design time build and the globalSessionGuid property was set then add a -globalsessionguid: + bool designTime = false; + if (this.HostObject != null) + { + var csHost = this.HostObject as ICscHostObject; + designTime = csHost.IsDesignTime(); + } + if (!designTime) + { + if (!string.IsNullOrWhiteSpace(this.VsSessionGuid)) + { + commandLine.AppendSwitchIfNotNull("/sqmsessionguid:", this.VsSessionGuid); + } + } + + this.AddReferencesToCommandLine(commandLine); + + base.AddResponseFileCommands(commandLine); + + // This should come after the "TreatWarningsAsErrors" flag is processed (in managedcompiler.cs). + // Because if TreatWarningsAsErrors=false, then we'll have a /warnaserror- on the command-line, + // and then any specific warnings that should be treated as errors should be specified with + // /warnaserror+: after the /warnaserror- switch. The order of the switches on the command-line + // does matter. + // + // Note that + // /warnaserror+ + // is just shorthand for: + // /warnaserror+: + // + // Similarly, + // /warnaserror- + // is just shorthand for: + // /warnaserror-: + commandLine.AppendSwitchWithSplitting("/warnaserror+:", this.WarningsAsErrors, ",", ';', ','); + commandLine.AppendSwitchWithSplitting("/warnaserror-:", this.WarningsNotAsErrors, ",", ';', ','); + + // It's a good idea for the response file to be the very last switch passed, just + // from a predictability perspective. + if (this.ResponseFiles != null) + { + foreach (ITaskItem response in this.ResponseFiles) + { + commandLine.AppendSwitchIfNotNull("@", response.ItemSpec); + } + } + } + + #endregion + + /// + /// The C# compiler (starting with Whidbey) supports assembly aliasing for references. + /// See spec at http://devdiv/spectool/Documents/Whidbey/VCSharp/Design%20Time/M3%20DCRs/DCR%20Assembly%20aliases.doc. + /// This method handles the necessary work of looking at the "Aliases" attribute on + /// the incoming "References" items, and making sure to generate the correct + /// command-line on csc.exe. The syntax for aliasing a reference is: + /// csc.exe /reference:Foo=System.Xml.dll + /// + /// The "Aliases" attribute on the "References" items is actually a comma-separated + /// list of aliases, and if any of the aliases specified is the string "global", + /// then we add that reference to the command-line without an alias. + /// + /// + private void AddReferencesToCommandLine + ( + CommandLineBuilderExtension commandLine + ) + { + // If there were no references passed in, don't add any /reference: switches + // on the command-line. + if ((this.References == null) || (this.References.Length == 0)) + { + return; + } + + // Loop through all the references passed in. We'll be adding separate + // /reference: switches for each reference, and in some cases even multiple + // /reference: switches per reference. + foreach (ITaskItem reference in this.References) + { + // See if there was an "Alias" attribute on the reference. + string aliasString = reference.GetMetadata(ItemMetadataNames.aliases); + + + string switchName = "/reference:"; + bool embed = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + reference, + ItemMetadataNames.embedInteropTypes + ); + + if (embed == true) + { + switchName = "/link:"; + } + + if ((aliasString == null) || (aliasString.Length == 0)) + { + // If there was no "Alias" attribute, just add this as a global reference. + commandLine.AppendSwitchIfNotNull(switchName, reference.ItemSpec); + } + else + { + // If there was an "Alias" attribute, it contains a comma-separated list + // of aliases to use for this reference. For each one of those aliases, + // we're going to add a separate /reference: switch to the csc.exe + // command-line + string[] aliases = aliasString.Split(','); + + foreach (string alias in aliases) + { + // Trim whitespace. + string trimmedAlias = alias.Trim(); + + if (alias.Length == 0) + { + continue; + } + + // The alias should be a valid C# identifier. Therefore it cannot + // contain comma, space, semicolon, or double-quote. Let's check for + // the existence of those characters right here, and bail immediately + // if any are present. There are a whole bunch of other characters + // that are not allowed in a C# identifier, but we'll just let csc.exe + // error out on those. The ones we're checking for here are the ones + // that could seriously interfere with the command-line parsing or could + // allow parameter injection. + if (trimmedAlias.IndexOfAny(new char[] { ',', ' ', ';', '"' }) != -1) + { + ErrorUtilities.VerifyThrowArgument + ( + false, + "Csc.AssemblyAliasContainsIllegalCharacters", + reference.ItemSpec, + trimmedAlias + ); + } + + // The alias called "global" is special. It means that we don't + // give it an alias on the command-line. + if (String.Compare("global", trimmedAlias, StringComparison.OrdinalIgnoreCase) == 0) + { + commandLine.AppendSwitchIfNotNull(switchName, reference.ItemSpec); + } + else + { + // We have a valid (and explicit) alias for this reference. Add + // it to the command-line using the syntax: + // /reference:Foo=System.Xml.dll + commandLine.AppendSwitchAliased(switchName, trimmedAlias, reference.ItemSpec); + } + } + } + } + } + + /// + /// Determines whether a particular string is a valid C# identifier. Legal + /// identifiers must start with a letter, and all the characters must be + /// letters or numbers. Underscore is considered a letter. + /// + private static bool IsLegalIdentifier + ( + string identifier + ) + { + // Must be non-empty. + if (identifier.Length == 0) + { + return false; + } + + // First character must be a letter. + // From 2.4.2 of the C# Language Specification + // identifier-start-letter-character: + if ( + !TokenChar.IsLetter(identifier[0]) && + (identifier[0] != '_') + ) + { + return false; + } + + // All the other characters must be letters or numbers. + // From 2.4.2 of the C# Language Specification + // identifier-part-letter-character: + for (int i = 1; i < identifier.Length; i++) + { + char currentChar = identifier[i]; + + if ( + !TokenChar.IsLetter(currentChar) && + !TokenChar.IsDecimalDigit(currentChar) && + !TokenChar.IsConnecting(currentChar) && + !TokenChar.IsCombining(currentChar) && + !TokenChar.IsFormatting(currentChar) + ) + { + return false; + } + } + + return true; + } + + /// + /// Old VS projects had some pretty messed-up looking values for the + /// "DefineConstants" property. It worked fine in the IDE, because it + /// effectively munged up the string so that it ended up being valid for + /// the compiler. We do the equivalent munging here now. + /// + /// Basically, we take the incoming string, and split it on comma/semicolon/space. + /// Then we look at the resulting list of strings, and remove any that are + /// illegal identifiers, and pass the remaining ones through to the compiler. + /// + /// Note that CSharp does support assigning a value to the constants ... in + /// other words, a constant is either defined or not defined ... it can't have + /// an actual value. + /// + internal string GetDefineConstantsSwitch + ( + string originalDefineConstants + ) + { + if (originalDefineConstants == null) + { + return null; + } + + StringBuilder finalDefineConstants = new StringBuilder(); + + // Split the incoming string on comma/semicolon/space. + string[] allIdentifiers = originalDefineConstants.Split(new char[] { ',', ';', ' ' }); + + // Loop through all the parts, and for the ones that are legal C# identifiers, + // add them to the outgoing string. + foreach (string singleIdentifier in allIdentifiers) + { + if (Csc.IsLegalIdentifier(singleIdentifier)) + { + // Separate them with a semicolon if there's something already in + // the outgoing string. + if (finalDefineConstants.Length > 0) + { + finalDefineConstants.Append(";"); + } + + finalDefineConstants.Append(singleIdentifier); + } + else if (singleIdentifier.Length > 0) + { + Log.LogWarningWithCodeFromResources("Csc.InvalidParameterWarning", "/define:", singleIdentifier); + } + } + + if (finalDefineConstants.Length > 0) + { + return finalDefineConstants.ToString(); + } + else + { + // We wouldn't want to pass in an empty /define: switch on the csc.exe command-line. + return null; + } + } + + + /// + /// This method will initialize the host compiler object with all the switches, + /// parameters, resources, references, sources, etc. + /// + /// It returns true if everything went according to plan. It returns false if the + /// host compiler had a problem with one of the parameters that was passed in. + /// + /// This method also sets the "this.HostCompilerSupportsAllParameters" property + /// accordingly. + /// + /// Example: + /// If we attempted to pass in WarningLevel="9876", then this method would + /// set HostCompilerSupportsAllParameters=true, but it would give a + /// return value of "false". This is because the host compiler fully supports + /// the WarningLevel parameter, but 9876 happens to be an illegal value. + /// + /// Example: + /// If we attempted to pass in NoConfig=false, then this method would set + /// HostCompilerSupportsAllParameters=false, because while this is a legal + /// thing for csc.exe, the IDE compiler cannot support it. In this situation + /// the return value will also be false. + /// + private bool InitializeHostCompiler + ( + // NOTE: For compat reasons this must remain ICscHostObject + // we can dynamically test for smarter interfaces later.. + ICscHostObject cscHostObject + ) + { + bool success; + this.HostCompilerSupportsAllParameters = this.UseHostCompilerIfAvailable; + string param = "Unknown"; + + try + { + // Need to set these separately, because they don't require a CommitChanges to the C# compiler in the IDE. + param = "LinkResources"; this.CheckHostObjectSupport(param, cscHostObject.SetLinkResources(this.LinkResources)); + param = "References"; this.CheckHostObjectSupport(param, cscHostObject.SetReferences(this.References)); + param = "Resources"; this.CheckHostObjectSupport(param, cscHostObject.SetResources(this.Resources)); + param = "Sources"; this.CheckHostObjectSupport(param, cscHostObject.SetSources(this.Sources)); + + // For host objects which support it, pass the list of analyzers. + IAnalyzerHostObject analyzerHostObject = cscHostObject as IAnalyzerHostObject; + if (analyzerHostObject != null) + { + param = "Analyzers"; this.CheckHostObjectSupport(param, analyzerHostObject.SetAnalyzers(this.Analyzers)); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + if (this.HostCompilerSupportsAllParameters) + { + // If the host compiler doesn't support everything we need, we're going to end up + // shelling out to the command-line compiler anyway. That means the command-line + // compiler will log the error. So here, we only log the error if we would've + // tried to use the host compiler. + Log.LogErrorWithCodeFromResources("General.CouldNotSetHostObjectParameter", param, e.Message); + } + return false; + } + + try + { + param = "BeginInitialization"; + cscHostObject.BeginInitialization(); + + param = "AdditionalLibPaths"; this.CheckHostObjectSupport(param, cscHostObject.SetAdditionalLibPaths(this.AdditionalLibPaths)); + param = "AddModules"; this.CheckHostObjectSupport(param, cscHostObject.SetAddModules(this.AddModules)); + param = "AllowUnsafeBlocks"; this.CheckHostObjectSupport(param, cscHostObject.SetAllowUnsafeBlocks(this.AllowUnsafeBlocks)); + param = "BaseAddress"; this.CheckHostObjectSupport(param, cscHostObject.SetBaseAddress(this.BaseAddress)); + param = "CheckForOverflowUnderflow"; this.CheckHostObjectSupport(param, cscHostObject.SetCheckForOverflowUnderflow(this.CheckForOverflowUnderflow)); + param = "CodePage"; this.CheckHostObjectSupport(param, cscHostObject.SetCodePage(this.CodePage)); + + // These two -- EmitDebugInformation and DebugType -- must go together, with DebugType + // getting set last, because it is more specific. + param = "EmitDebugInformation"; this.CheckHostObjectSupport(param, cscHostObject.SetEmitDebugInformation(this.EmitDebugInformation)); + param = "DebugType"; this.CheckHostObjectSupport(param, cscHostObject.SetDebugType(this.DebugType)); + + param = "DefineConstants"; this.CheckHostObjectSupport(param, cscHostObject.SetDefineConstants(this.GetDefineConstantsSwitch(this.DefineConstants))); + param = "DelaySign"; this.CheckHostObjectSupport(param, cscHostObject.SetDelaySign((this.Bag["DelaySign"] != null), this.DelaySign)); + param = "DisabledWarnings"; this.CheckHostObjectSupport(param, cscHostObject.SetDisabledWarnings(this.DisabledWarnings)); + param = "DocumentationFile"; this.CheckHostObjectSupport(param, cscHostObject.SetDocumentationFile(this.DocumentationFile)); + param = "ErrorReport"; this.CheckHostObjectSupport(param, cscHostObject.SetErrorReport(this.ErrorReport)); + param = "FileAlignment"; this.CheckHostObjectSupport(param, cscHostObject.SetFileAlignment(this.FileAlignment)); + param = "GenerateFullPaths"; this.CheckHostObjectSupport(param, cscHostObject.SetGenerateFullPaths(this.GenerateFullPaths)); + param = "KeyContainer"; this.CheckHostObjectSupport(param, cscHostObject.SetKeyContainer(this.KeyContainer)); + param = "KeyFile"; this.CheckHostObjectSupport(param, cscHostObject.SetKeyFile(this.KeyFile)); + param = "LangVersion"; this.CheckHostObjectSupport(param, cscHostObject.SetLangVersion(this.LangVersion)); + param = "MainEntryPoint"; this.CheckHostObjectSupport(param, cscHostObject.SetMainEntryPoint(this.TargetType, this.MainEntryPoint)); + param = "ModuleAssemblyName"; this.CheckHostObjectSupport(param, cscHostObject.SetModuleAssemblyName(this.ModuleAssemblyName)); + param = "NoConfig"; this.CheckHostObjectSupport(param, cscHostObject.SetNoConfig(this.NoConfig)); + param = "NoStandardLib"; this.CheckHostObjectSupport(param, cscHostObject.SetNoStandardLib(this.NoStandardLib)); + param = "Optimize"; this.CheckHostObjectSupport(param, cscHostObject.SetOptimize(this.Optimize)); + param = "OutputAssembly"; this.CheckHostObjectSupport(param, cscHostObject.SetOutputAssembly(this.OutputAssembly.ItemSpec)); + param = "PdbFile"; this.CheckHostObjectSupport(param, cscHostObject.SetPdbFile(this.PdbFile)); + + // For host objects which support it, set platform with 32BitPreference, HighEntropyVA, and SubsystemVersion + ICscHostObject4 cscHostObject4 = cscHostObject as ICscHostObject4; + if (cscHostObject4 != null) + { + param = "PlatformWith32BitPreference"; this.CheckHostObjectSupport(param, cscHostObject4.SetPlatformWith32BitPreference(this.PlatformWith32BitPreference)); + param = "HighEntropyVA"; this.CheckHostObjectSupport(param, cscHostObject4.SetHighEntropyVA(this.HighEntropyVA)); + param = "SubsystemVersion"; this.CheckHostObjectSupport(param, cscHostObject4.SetSubsystemVersion(this.SubsystemVersion)); + } + else + { + param = "Platform"; this.CheckHostObjectSupport(param, cscHostObject.SetPlatform(this.Platform)); + } + + // For host objects which support it, set the analyzer ruleset and additional files. + IAnalyzerHostObject analyzerHostObject = cscHostObject as IAnalyzerHostObject; + if (analyzerHostObject != null) + { + param = "CodeAnalysisRuleSet"; this.CheckHostObjectSupport(param, analyzerHostObject.SetRuleSet(this.CodeAnalysisRuleSet)); + param = "AdditionalFiles"; this.CheckHostObjectSupport(param, analyzerHostObject.SetAdditionalFiles(this.AdditionalFiles)); + } + + param = "ResponseFiles"; this.CheckHostObjectSupport(param, cscHostObject.SetResponseFiles(this.ResponseFiles)); + param = "TargetType"; this.CheckHostObjectSupport(param, cscHostObject.SetTargetType(this.TargetType)); + param = "TreatWarningsAsErrors"; this.CheckHostObjectSupport(param, cscHostObject.SetTreatWarningsAsErrors(this.TreatWarningsAsErrors)); + param = "WarningLevel"; this.CheckHostObjectSupport(param, cscHostObject.SetWarningLevel(this.WarningLevel)); + // This must come after TreatWarningsAsErrors. + param = "WarningsAsErrors"; this.CheckHostObjectSupport(param, cscHostObject.SetWarningsAsErrors(this.WarningsAsErrors)); + // This must come after TreatWarningsAsErrors. + param = "WarningsNotAsErrors"; this.CheckHostObjectSupport(param, cscHostObject.SetWarningsNotAsErrors(this.WarningsNotAsErrors)); + param = "Win32Icon"; this.CheckHostObjectSupport(param, cscHostObject.SetWin32Icon(this.Win32Icon)); + + // In order to maintain compatibility with previous host compilers, we must + // light-up for ICscHostObject2/ICscHostObject3 + + if (cscHostObject is ICscHostObject2) + { + ICscHostObject2 cscHostObject2 = (ICscHostObject2)cscHostObject; + param = "Win32Manifest"; this.CheckHostObjectSupport(param, cscHostObject2.SetWin32Manifest(this.GetWin32ManifestSwitch(this.NoWin32Manifest, this.Win32Manifest))); + } + else + { + // If we have been given a property that the host compiler doesn't support + // then we need to state that we are falling back to the command line compiler + if (!String.IsNullOrEmpty(Win32Manifest)) + { + this.CheckHostObjectSupport("Win32Manifest", false); + } + } + + // This must come after Win32Manifest + param = "Win32Resource"; this.CheckHostObjectSupport(param, cscHostObject.SetWin32Resource(this.Win32Resource)); + + if (cscHostObject is ICscHostObject3) + { + ICscHostObject3 cscHostObject3 = (ICscHostObject3)cscHostObject; + param = "ApplicationConfiguration"; this.CheckHostObjectSupport(param, cscHostObject3.SetApplicationConfiguration(this.ApplicationConfiguration)); + } + else + { + // If we have been given a property that the host compiler doesn't support + // then we need to state that we are falling back to the command line compiler + if (!String.IsNullOrEmpty(ApplicationConfiguration)) + { + this.CheckHostObjectSupport("ApplicationConfiguration", false); + } + } + + // If we have been given a property value that the host compiler doesn't support + // then we need to state that we are falling back to the command line compiler. + // Null is supported because it means that option should be omitted, and compiler default used - obviously always valid. + // Explicitly specified name of current locale is also supported, since it is effectively a no-op. + // Other options are not supported since in-proc compiler always uses current locale. + if (!String.IsNullOrEmpty(PreferredUILang) && !String.Equals(PreferredUILang, System.Globalization.CultureInfo.CurrentUICulture.Name, StringComparison.OrdinalIgnoreCase)) + { + this.CheckHostObjectSupport("PreferredUILang", false); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + if (this.HostCompilerSupportsAllParameters) + { + // If the host compiler doesn't support everything we need, we're going to end up + // shelling out to the command-line compiler anyway. That means the command-line + // compiler will log the error. So here, we only log the error if we would've + // tried to use the host compiler. + Log.LogErrorWithCodeFromResources("General.CouldNotSetHostObjectParameter", param, e.Message); + } + return false; + } + finally + { + int errorCode; + string errorMessage; + + success = cscHostObject.EndInitialization(out errorMessage, out errorCode); + + if (this.HostCompilerSupportsAllParameters) + { + // If the host compiler doesn't support everything we need, we're going to end up + // shelling out to the command-line compiler anyway. That means the command-line + // compiler will log the error. So here, we only log the error if we would've + // tried to use the host compiler. + + // If EndInitialization returns false, then there was an error. If EndInitialization was + // successful, but there is a valid 'errorMessage,' interpret it as a warning. + + if (!success) + { + Log.LogError(null, "CS" + errorCode.ToString("D4", CultureInfo.InvariantCulture), null, null, 0, 0, 0, 0, errorMessage); + } + else if (errorMessage != null && errorMessage.Length > 0) + { + Log.LogWarning(null, "CS" + errorCode.ToString("D4", CultureInfo.InvariantCulture), null, null, 0, 0, 0, 0, errorMessage); + } + } + } + + return (success); + } + + /// + /// This method will get called during Execute() if a host object has been passed into the Csc + /// task. Returns one of the following values to indicate what the next action should be: + /// UseHostObjectToExecute Host compiler exists and was initialized. + /// UseAlternateToolToExecute Host compiler doesn't exist or was not appropriate. + /// NoActionReturnSuccess Host compiler was already up-to-date, and we're done. + /// NoActionReturnFailure Bad parameters were passed into the task. + /// + override protected HostObjectInitializationStatus InitializeHostObject() + { + if (this.HostObject != null) + { + // When the host object was passed into the task, it was passed in as a generic + // "Object" (because ITask interface obviously can't have any Csc-specific stuff + // in it, and each task is going to want to communicate with its host in a unique + // way). Now we cast it to the specific type that the Csc task expects. If the + // host object does not match this type, the host passed in an invalid host object + // to Csc, and we error out. + + // NOTE: For compat reasons this must remain ICscHostObject + // we can dynamically test for smarter interfaces later.. + using (RCWForCurrentContext hostObject = new RCWForCurrentContext(this.HostObject as ICscHostObject)) + { + ICscHostObject cscHostObject = hostObject.RCW; + + if (cscHostObject != null) + { + bool hostObjectSuccessfullyInitialized = InitializeHostCompiler(cscHostObject); + + // If we're currently only in design-time (as opposed to build-time), + // then we're done. We've initialized the host compiler as best we + // can, and we certainly don't want to actually do the final compile. + // So return true, saying we're done and successful. + if (cscHostObject.IsDesignTime()) + { + // If we are design-time then we do not want to continue the build at + // this time. + return hostObjectSuccessfullyInitialized ? + HostObjectInitializationStatus.NoActionReturnSuccess : + HostObjectInitializationStatus.NoActionReturnFailure; + } + + if (!this.HostCompilerSupportsAllParameters || UseAlternateCommandLineToolToExecute()) + { + // Since the host compiler has refused to take on the responsibility for this compilation, + // we're about to shell out to the command-line compiler to handle it. If some of the + // references don't exist on disk, we know the command-line compiler will fail, so save + // the trouble, and just throw a consistent error ourselves. This allows us to give + // more information than the compiler would, and also make things consistent across + // Vbc / Csc / etc. + // This suite behaves differently in localized builds than on English builds because + // VBC.EXE doesn't localize the word "error" when they emit errors and so we can't scan for it. + if (!CheckAllReferencesExistOnDisk()) + { + return HostObjectInitializationStatus.NoActionReturnFailure; + } + + // The host compiler doesn't support some of the switches/parameters + // being passed to it. Therefore, we resort to using the command-line compiler + // in this case. + UsedCommandLineTool = true; + return HostObjectInitializationStatus.UseAlternateToolToExecute; + } + + // Ok, by now we validated that the host object supports the necessary switches + // and parameters. Last thing to check is whether the host object is up to date, + // and in that case, we will inform the caller that no further action is necessary. + if (hostObjectSuccessfullyInitialized) + { + return cscHostObject.IsUpToDate() ? + HostObjectInitializationStatus.NoActionReturnSuccess : + HostObjectInitializationStatus.UseHostObjectToExecute; + } + else + { + return HostObjectInitializationStatus.NoActionReturnFailure; + } + } + else + { + Log.LogErrorWithCodeFromResources("General.IncorrectHostObject", "Csc", "ICscHostObject"); + } + } + } + + // No appropriate host object was found. + UsedCommandLineTool = true; + return HostObjectInitializationStatus.UseAlternateToolToExecute; + } + + /// + /// This method will get called during Execute() if a host object has been passed into the Csc + /// task. Returns true if the compilation succeeded, otherwise false. + /// + override protected bool CallHostObjectToExecute() + { + Debug.Assert(this.HostObject != null, "We should not be here if the host object has not been set."); + + ICscHostObject cscHostObject = this.HostObject as ICscHostObject; + Debug.Assert(cscHostObject != null, "Wrong kind of host object passed in!"); + try + { +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildHostCompileBegin); +#endif + return cscHostObject.Compile(); + } + finally + { +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildHostCompileEnd); +#endif + } + } + } +} diff --git a/src/XMakeTasks/Culture.cs b/src/XMakeTasks/Culture.cs new file mode 100644 index 00000000000..a80810140eb --- /dev/null +++ b/src/XMakeTasks/Culture.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using System.Globalization; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Utility functions for dealing with Culture information. + /// + internal static class Culture + { + /// + /// Culture information about an item. + /// + internal struct ItemCultureInfo + { + internal string culture; + internal string cultureNeutralFilename; + }; + + /// + /// Given an item's filename, return information about the item including the culture and the culture-neutral filename. + /// + /// + /// We've decided to ignore explicit Culture attributes on items. + /// + /// + /// + /// + internal static ItemCultureInfo GetItemCultureInfo + ( + string name, + string dependentUponFilename + ) + { + ItemCultureInfo info; + info.culture = null; + string parentName = dependentUponFilename == null ? String.Empty : dependentUponFilename; + + if (0 == String.Compare(Path.GetFileNameWithoutExtension(parentName), + Path.GetFileNameWithoutExtension(name), + StringComparison.OrdinalIgnoreCase)) + { + // Dependent, but we treat it is as not localized because they have same base filename + info.cultureNeutralFilename = name; + } + else + { + // Either not dependent on another file, or it has a distinct base filename + + // If the item is defined as "Strings.en-US.resx", then ... + + // ... base file name will be "Strings.en-US" ... + string baseFileNameWithCulture = Path.GetFileNameWithoutExtension(name); + + // ... and cultureName will be ".en-US". + string cultureName = Path.GetExtension(baseFileNameWithCulture); + + // See if this is a valid culture name. + bool validCulture = false; + if ((cultureName != null) && (cultureName.Length > 1)) + { + // ... strip the "." to make "en-US" + cultureName = cultureName.Substring(1); + validCulture = CultureStringUtilities.IsValidCultureString(cultureName); + } + + if (validCulture) + { + // A valid culture was found. + info.culture = cultureName; + + // Copy the assigned file and make it culture-neutral + string extension = Path.GetExtension(name); + string baseFileName = Path.GetFileNameWithoutExtension(baseFileNameWithCulture); + string baseFolder = Path.GetDirectoryName(name); + string fileName = baseFileName + extension; + info.cultureNeutralFilename = Path.Combine(baseFolder, fileName); + } + else + { + // No valid culture was found. In this case, the culture-neutral + // name is the just the original file name. + info.cultureNeutralFilename = name; + } + } + + return info; + } + } +} diff --git a/src/XMakeTasks/CultureStringUtilities.cs b/src/XMakeTasks/CultureStringUtilities.cs new file mode 100644 index 00000000000..80d6a2230ee --- /dev/null +++ b/src/XMakeTasks/CultureStringUtilities.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class provides utilities for RFC1766 culture strings. + /// + internal static class CultureStringUtilities + { + static private string[] s_cultureInfoStrings; + + /// + /// Validates a RFC1766 culture string. + /// + /// + /// the culture string to be validated + /// + /// + /// returns true if the culture string is valid + /// + internal static bool IsValidCultureString(string cultureString) + { + // Get the supported cultures + PopulateCultureInfoArray(); + + // Note, it does not matter what kind of comparer we use as long as the comparer + // for Array.Sort() [see PopulateCultureInfoArray()] and Array.BinarySearch() is + // the same. + bool valid = true; + + if (Array.BinarySearch(s_cultureInfoStrings, cultureString, StringComparer.OrdinalIgnoreCase) < 0) + { + valid = false; + } + + return valid; + } + + /// + /// Populate the array of culture strings. + /// + internal static void PopulateCultureInfoArray() + { + if (s_cultureInfoStrings == null) + { + CultureInfo[] cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures); + + + s_cultureInfoStrings = new string[cultureInfos.Length]; + for (int i = 0; i < cultureInfos.Length; i++) + { + s_cultureInfoStrings[i] = cultureInfos[i].Name; + } + + // Note, it does not matter what kind of comparer we use as long as the comparer + // for Array.BinarySearch() [see ValidateCultureInfoString()] and Array.Sort() is + // the same. + Array.Sort(s_cultureInfoStrings, StringComparer.OrdinalIgnoreCase); + } + } + } +} diff --git a/src/XMakeTasks/DataDriven/DataDrivenToolTask.cs b/src/XMakeTasks/DataDriven/DataDrivenToolTask.cs new file mode 100644 index 00000000000..aff1e5542a9 --- /dev/null +++ b/src/XMakeTasks/DataDriven/DataDrivenToolTask.cs @@ -0,0 +1,859 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Resources; +using System.Globalization; +using System.IO; +using System.Security; + +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Reflection; +using System.Diagnostics; + +namespace Microsoft.Build.Tasks.DataDriven +{ + /// + /// The top class that will take care of all the tasks that wrap tools. + /// All tasks that wrap tools will derive from this class. + /// Holds a Dictionary of all switches that have been set + /// + public abstract class DataDrivenToolTask : ToolTask + { + /// + /// The dictionary that holds all set switches + /// The string is the name of the property, and the ToolSwitch holds all of the relevant information + /// i.e., switch, boolean value, type, etc. + /// + private Dictionary activeToolSwitches = new Dictionary(); + + /// + /// The dictionary holds all of the legal values that are associated with a certain switch. + /// For example, the key Optimization would hold another dictionary as the value, that had the string pairs + /// "Disabled", "/Od"; "MaxSpeed", "/O1"; "MinSpace", "/O2"; "Full", "/Ox" in it. + /// + private Dictionary> values = new Dictionary>(); + + /// + /// Any additional options (as a literal string) that may have been specified in the project file + /// We eventually want to get rid of this + /// + private string additionalOptions = String.Empty; + + /// + /// The prefix to append before all switches + /// + private char prefix = '/'; + + /// + /// True if we returned our commands directly from the command line generation and do not need to use the + /// response file (because the command-line is short enough) + /// + private bool skipResponseFileCommandGeneration; + + protected TaskLoggingHelper logPrivate; + + /// + /// Default constructor + /// +#if WHIDBEY_VISIBILITY + internal +#else + protected +#endif + DataDrivenToolTask(ResourceManager taskResources) + : base(taskResources) + { + logPrivate = new TaskLoggingHelper(this); +#if WHIDBEY_BUILD + logPrivate.TaskResources = AssemblyResources.DataDrivenToolTaskResources; +#else + logPrivate.TaskResources = AssemblyResources.PrimaryResources; +#endif + logPrivate.HelpKeywordPrefix = "MSBuild."; + } + + #region Properties + + /// + /// The list of all the switches that have been set + /// +#if WHIDBEY_VISIBILITY + internal +#else + protected +#endif + Dictionary ActiveToolSwitches + { + get + { + return activeToolSwitches; + } + } + + /// + /// The additional options that have been set. These are raw switches that + /// go last on the command line. + /// + public string AdditionalOptions + { + get + { + return additionalOptions; + } + set + { + additionalOptions = value; + } + } + + /// + /// Overridden to use UTF16, which works better than UTF8 for older versions of CL, LIB, etc. + /// + protected override Encoding ResponseFileEncoding + { + get + { + return Encoding.Unicode; + } + } + + /// + /// Ordered list of switches + /// + /// ArrayList of switches in declaration order + protected virtual ArrayList SwitchOrderList + { + get + { + return null; + } + } + #endregion + + #region ToolTask Members + + /// + /// This method is called to find the tool if ToolPath wasn't specified. + /// We just return the name of the tool so it can be found on the path. + /// Deriving classes can choose to do something else. + /// + protected override string GenerateFullPathToTool() + { +#if WHIDBEY_BUILD + // if we just have the file name, search for the file on the system path + string actualPathToTool = NativeMethodsShared.FindOnPath(ToolName); + + // if we find the file + if (actualPathToTool != null) + { + // point to it + return actualPathToTool; + } + else + { + return ToolName; + } +#else + return ToolName; +#endif + } + + /// + /// Validates all of the set properties that have either a string type or an integer type + /// + /// + override protected bool ValidateParameters() + { + return !logPrivate.HasLoggedErrors && !Log.HasLoggedErrors; + } + +#if WHIDBEY_BUILD + /// + /// Delete temporary file. If the delete fails for some reason (e.g. file locked by anti-virus) then + /// the call will not throw an exception. Instead a warning will be logged, but the build will not fail. + /// + /// File to delete + protected void DeleteTempFile(string fileName) + { + try + { + File.Delete(fileName); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + // Warn only -- occasionally temp files fail to delete because of virus checkers; we + // don't want the build to fail in such cases + Log.LogWarningWithCodeFromResources("Shared.FailedDeletingTempFile", fileName, e.Message); + } + } +#endif + #endregion + + /// + /// For testing purposes only + /// Returns the generated command line + /// + /// +#if WHIDBEY_VISIBILITY + internal virtual +#else + internal +#endif + string GetCommandLine_ForUnitTestsOnly() + { + return GenerateResponseFileCommands(); + } + + protected override string GenerateCommandLineCommands() + { + string commands = GenerateCommands(); + if (commands.Length < 32768) + { + skipResponseFileCommandGeneration = true; + return commands; + } + + skipResponseFileCommandGeneration = false; + return null; + } + + /// + /// Creates the command line and returns it as a string by: + /// 1. Adding all switches with the default set to the active switch list + /// 2. Customizing the active switch list (overridden in derived classes) + /// 3. Iterating through the list and appending switches + /// + /// + protected override string GenerateResponseFileCommands() + { + if (skipResponseFileCommandGeneration) + { + skipResponseFileCommandGeneration = false; + return null; + } + else + { + return GenerateCommands(); + } + } + + /// + /// Verifies that the required args are present. This function throws if we have missing required args + /// + /// + /// + protected virtual bool VerifyRequiredArgumentsArePresent(ToolSwitch property, bool bThrowOnError) + { + return true; + } + /// + /// Verifies that the dependencies are present, and if the dependencies are present, or if the property + /// doesn't have any dependencies, the switch gets emitted + /// + /// + /// + protected virtual bool VerifyDependenciesArePresent(ToolSwitch property) + { + // check the dependency + if (property.Parents.Count > 0) + { + // has a dependency, now check to see whether at least one parent is set + // if it is set, add to the command line + // otherwise, ignore it + bool isSet = false; + foreach (string parentName in property.Parents) + { + isSet = isSet || HasSwitch(parentName); + + } + return isSet; + } + else + { + // no dependencies to account for + return true; + } + } + + /// + /// A protected method to add the switches that are by default visible + /// e.g., /nologo is true by default + /// + protected virtual void AddDefaultsToActiveSwitchList() + { + // do nothing + } + + /// + /// A method that will add the fallbacks to the active switch list if the actual property is not set + /// + protected virtual void AddFallbacksToActiveSwitchList() + { + // do nothing + } + + /// + /// To be overriden by custom code for individual tasks + /// + protected virtual void PostProcessSwitchList() + { + // do nothing + } + + /// + /// Generates a part of the command line depending on the type + /// + /// Depending on the type of the switch, the switch is emitted with the proper values appended. + /// e.g., File switches will append file names, directory switches will append filenames with "\" on the end + /// + /// + protected void GenerateCommandsAccordingToType(CommandLineBuilder clb, ToolSwitch toolSwitch, bool bRecursive) + { + // if this property has a parent skip printing it as it was printed as part of the parent prop printing + if (toolSwitch.Parents.Count > 0 && !bRecursive) + return; + + switch (toolSwitch.Type) + { + case ToolSwitchType.Boolean: + EmitBooleanSwitch(clb, toolSwitch); + break; + case ToolSwitchType.String: + EmitStringSwitch(clb, toolSwitch); + break; + case ToolSwitchType.StringArray: + EmitStringArraySwitch(clb, toolSwitch); + break; + case ToolSwitchType.Integer: + EmitIntegerSwitch(clb, toolSwitch); + break; + case ToolSwitchType.File: + EmitFileSwitch(clb, toolSwitch); + break; + case ToolSwitchType.Directory: + EmitDirectorySwitch(clb, toolSwitch); + break; + case ToolSwitchType.ITaskItem: + EmitTaskItemSwitch(clb, toolSwitch); + break; + case ToolSwitchType.ITaskItemArray: + EmitTaskItemArraySwitch(clb, toolSwitch); + break; + case ToolSwitchType.AlwaysAppend: + EmitAlwaysAppendSwitch(clb, toolSwitch); + break; + default: + // should never reach this point - if it does, there's a bug somewhere. + ErrorUtilities.VerifyThrow(false, "InternalError"); + break; + } + } + + /// + /// Appends a literal string containing the verbatim contents of any + /// "AdditionalOptions" parameter. This goes last on the command + /// line in case it needs to cancel any earlier switch. + /// Ideally this should never be needed because the MSBuild task model + /// is to set properties, not raw switches + /// + /// + protected void BuildAdditionalArgs(CommandLineBuilder cmdLine) + { + // We want additional options to be last so that this can always override other flags. + if ((cmdLine != null) && !String.IsNullOrEmpty(additionalOptions)) + { + cmdLine.AppendSwitch(additionalOptions); + } + } + + /// + /// Emit a switch that's always appended + /// + private static void EmitAlwaysAppendSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + clb.AppendSwitch(toolSwitch.Name); + } + + /// + /// Emit a switch that's an array of task items + /// + private static void EmitTaskItemArraySwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + if (String.IsNullOrEmpty(toolSwitch.Separator)) + { + foreach (ITaskItem itemName in toolSwitch.TaskItemArray) + { + clb.AppendSwitchIfNotNull(toolSwitch.SwitchValue, itemName.ItemSpec); + } + } + else + { + clb.AppendSwitchIfNotNull(toolSwitch.SwitchValue, toolSwitch.TaskItemArray, toolSwitch.Separator); + } + } + + /// + /// Emit a switch that's a scalar task item + /// + private static void EmitTaskItemSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + if (!String.IsNullOrEmpty(toolSwitch.Name)) + { + clb.AppendSwitch(toolSwitch.Name + toolSwitch.Separator); + } + } + + /// + /// Generates the command line for the tool. + /// + private string GenerateCommands() + { + // the next three methods are overridden by the base class + // here it does nothing unless overridden + AddDefaultsToActiveSwitchList(); + + AddFallbacksToActiveSwitchList(); + + PostProcessSwitchList(); + +#if WHIDBEY_BUILD + CommandLineBuilder commandLineBuilder = new CommandLineBuilder(); +#else + CommandLineBuilder commandLineBuilder = new CommandLineBuilder(true /* quote hyphens */); +#endif + + // iterates through the list of set toolswitches + foreach (string propertyName in SwitchOrderList) + { + if (IsPropertySet(propertyName)) + { + ToolSwitch property = activeToolSwitches[propertyName]; + + // verify the dependencies + if (VerifyDependenciesArePresent(property) && VerifyRequiredArgumentsArePresent(property, false)) + { + GenerateCommandsAccordingToType(commandLineBuilder, property, false); + } + } + else if (String.Equals(propertyName, "AlwaysAppend", StringComparison.OrdinalIgnoreCase)) + { + commandLineBuilder.AppendSwitch(AlwaysAppend); + } + } + + // additional args should go on the end + BuildAdditionalArgs(commandLineBuilder); + return commandLineBuilder.ToString(); + } + + /// + /// Checks to see if the argument is required and whether an argument exists, and returns the + /// argument or else fallback argument if it exists. + /// + /// These are the conditions to look at: + /// + /// ArgumentRequired ArgumentParameter FallbackArgumentParameter Result + /// true isSet NA The value in ArgumentParameter gets returned + /// true isNotSet isSet The value in FallbackArgumentParamter gets returned + /// true isNotSet isNotSet An error occurs, as argumentrequired is true + /// false isSet NA The value in ArgumentParameter gets returned + /// false isNotSet isSet The value in FallbackArgumentParameter gets returned + /// false isNotSet isNotSet The empty string is returned, as there are no arguments, and no arguments are required + /// + /// + /// + protected virtual string GetEffectiveArgumentsValues(ToolSwitch toolSwitch) + { + //if (!toolSwitch.ArgumentRequired && !IsPropertySet(toolSwitch.ArgumentParameter) && + // !IsPropertySet(toolSwitch.FallbackArgumentParameter)) + //{ + // return String.Empty; + //} + + //// check to see if it has an argument + //if (toolSwitch.ArgumentRequired) + //{ + // if (!IsPropertySet(toolSwitch.ArgumentParameter) && !IsPropertySet(toolSwitch.FallbackArgumentParameter)) + // { + // throw new ArgumentException(logPrivate.FormatResourceString("ArgumentRequired", toolSwitch.Name)); + // } + //} + //// if it gets to here, the argument or the fallback is set + //if (IsPropertySet(toolSwitch.ArgumentParameter)) + //{ + // return ActiveToolSwitches[toolSwitch.ArgumentParameter].ArgumentValue; + //} + //else + //{ + // return ActiveToolSwitches[toolSwitch.FallbackArgumentParameter].ArgumentValue; + //} + return "GetEffectiveArgumentValue not Impl"; + } + + /// + /// Appends the directory name to the end of a switch + /// Ensure the name ends with a slash + /// + /// For directory switches (e.g., TrackerLogDirectory), the toolSwitchName (if it exists) is emitted + /// along with the FileName which is ensured to have a trailing slash + /// + /// + private static void EmitDirectorySwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + if (!String.IsNullOrEmpty(toolSwitch.SwitchValue)) + { + //clb.AppendSwitchIfNotNull(toolSwitch.Name + toolSwitch.Separator, EnsureTrailingSlash(toolSwitch.ArgumentValue)); + clb.AppendSwitch(toolSwitch.SwitchValue + toolSwitch.Separator); + } + } + + /// + /// Generates the switches that have filenames attached to the end + /// + /// For file switches (e.g., PrecompiledHeaderFile), the toolSwitchName (if it exists) is emitted + /// along with the FileName which may or may not have quotes + /// e.g., PrecompiledHeaderFile = "File" will emit /FpFile + /// + /// + private static void EmitFileSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + if (!String.IsNullOrEmpty(toolSwitch.Value)) + { + String str = toolSwitch.Value; + str.Trim(); + + if (!str.StartsWith("\"")) + { + str = "\"" + str; + if (str.EndsWith("\\") && !str.EndsWith("\\\\")) + str += "\\\""; + else + str += "\""; + } + + //we want quotes always, AppendSwitchIfNotNull will add them on as needed bases + clb.AppendSwitchUnquotedIfNotNull(toolSwitch.SwitchValue + toolSwitch.Separator, str); + } + } + + /// + /// Generates the commands for switches that have integers appended. + /// + /// For integer switches (e.g., WarningLevel), the toolSwitchName is emitted + /// with the appropriate integer appended, as well as any arguments + /// e.g., WarningLevel = "4" will emit /W4 + /// + /// + private void EmitIntegerSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + if (toolSwitch.IsValid) + { + if (!String.IsNullOrEmpty(toolSwitch.Separator)) + { + clb.AppendSwitch(toolSwitch.SwitchValue + toolSwitch.Separator + toolSwitch.Number.ToString() + GetEffectiveArgumentsValues(toolSwitch)); + } + else + { + clb.AppendSwitch(toolSwitch.SwitchValue + toolSwitch.Number.ToString() + GetEffectiveArgumentsValues(toolSwitch)); + } + } + } + + /// + /// Generates the commands for the switches that may have an array of arguments + /// The switch may be empty. + /// + /// For stringarray switches (e.g., Sources), the toolSwitchName (if it exists) is emitted + /// along with each and every one of the file names separately (if no separator is included), or with all of the + /// file names separated by the separator. + /// e.g., AdditionalIncludeDirectores = "@(Files)" where Files has File1, File2, and File3, the switch + /// /IFile1 /IFile2 /IFile3 or the switch /IFile1;File2;File3 is emitted (the latter case has a separator + /// ";" specified) + /// + /// + private static void EmitStringArraySwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + string[] ArrTrimStringList = new string [toolSwitch.StringList.Length]; + for (int i=0; i + /// Generates the switches for switches that either have literal strings appended, or have + /// different switches based on what the property is set to. + /// + /// The string switch emits a switch that depends on what the parameter is set to, with and + /// arguments + /// e.g., Optimization = "Full" will emit /Ox, whereas Optimization = "Disabled" will emit /Od + /// + /// + private void EmitStringSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + String strSwitch = String.Empty; + strSwitch += toolSwitch.SwitchValue + toolSwitch.Separator; + + StringBuilder val = new StringBuilder(GetEffectiveArgumentsValues(toolSwitch)); + String str = toolSwitch.Value; + + if (!toolSwitch.MultiValues) + { + + str.Trim(); + + if (!str.StartsWith("\"")) + { + str = "\"" + str; + if (str.EndsWith("\\") && !str.EndsWith("\\\\")) + str += "\\\""; + else + str += "\""; + } + val.Insert(0, str); + } + + if ((strSwitch.Length == 0) && (val.ToString().Length == 0)) + return; + + clb.AppendSwitchUnquotedIfNotNull(strSwitch, val.ToString()); + + } + + /// + /// Generates the switches that are nonreversible + /// + /// A boolean switch is emitted if it is set to true. If it set to false, nothing is emitted. + /// e.g. nologo = "true" will emit /Og, but nologo = "false" will emit nothing. + /// + /// + private void EmitBooleanSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + if (toolSwitch.BooleanValue) + { + if (!String.IsNullOrEmpty(toolSwitch.SwitchValue)) + { + StringBuilder val = new StringBuilder(GetEffectiveArgumentsValues(toolSwitch)); + val.Insert(0, toolSwitch.Separator); + val.Insert(0, toolSwitch.TrueSuffix); + val.Insert(0, toolSwitch.SwitchValue); + clb.AppendSwitch(val.ToString()); + } + } + else + EmitReversibleBooleanSwitch(clb, toolSwitch); + } + + /// + /// Generates the command line for switches that are reversible + /// + /// A reversible boolean switch will emit a certain switch if set to true, but emit that + /// exact same switch with a flag appended on the end if set to false. + /// e.g., GlobalOptimizations = "true" will emit /Og, and GlobalOptimizations = "false" will emit /Og- + /// + /// + private void EmitReversibleBooleanSwitch(CommandLineBuilder clb, ToolSwitch toolSwitch) + { + // if the value is set to true, append whatever the TrueSuffix is set to. + // Otherwise, append whatever the FalseSuffix is set to. + if (!String.IsNullOrEmpty(toolSwitch.ReverseSwitchValue)) + { + string suffix = (toolSwitch.BooleanValue) ? toolSwitch.TrueSuffix : toolSwitch.FalseSuffix; + StringBuilder val = new StringBuilder(GetEffectiveArgumentsValues(toolSwitch)); + val.Insert(0, suffix); + val.Insert(0, toolSwitch.Separator); + val.Insert(0, toolSwitch.TrueSuffix); + val.Insert(0, toolSwitch.ReverseSwitchValue); + clb.AppendSwitch(val.ToString()); + } + } + + /// + /// Checks to make sure that a switch has either a '/' or a '-' prefixed. + /// + /// + /// + private string Prefix(string toolSwitch) + { + if (!String.IsNullOrEmpty(toolSwitch)) + { + if (toolSwitch[0] != prefix) + { + return prefix + toolSwitch; + } + } + return toolSwitch; + } + + /// + /// A method that will validate the integer type arguments + /// If the min or max is set, and the value a property is set to is not within + /// the range, the build fails + /// + /// + /// + /// + /// + /// The valid integer passed converted to a string form + protected bool ValidateInteger(string switchName, int min, int max, int value) + { + if (value < min || value > max) + { + logPrivate.LogErrorFromResources("ArgumentOutOfRange", switchName, value); + return false; + } + + return true; + + } + + /// + /// A method for the enumerated values a property can have + /// This method checks the value a property is set to, and finds the corresponding switch + /// + /// + /// + /// + /// The switch that a certain value is mapped to + protected string ReadSwitchMap(string propertyName, string[][] switchMap, string value) + { + if (switchMap != null) + { + for (int i = 0; i < switchMap.Length; ++i) + { + if (String.Equals(switchMap[i][0], value, StringComparison.CurrentCultureIgnoreCase)) + { + return switchMap[i][1]; + } + } + logPrivate.LogErrorFromResources("ArgumentOutOfRange", propertyName, value); + } + return String.Empty; + } + + /// + /// Returns true if the property has a value in the list of active tool switches + /// + protected bool IsPropertySet(string propertyName) + { + if (!String.IsNullOrEmpty(propertyName)) + { + return activeToolSwitches.ContainsKey(propertyName); + } + else + { + return false; + } + } + + /// + /// Returns true if the property is set to true. + /// Returns false if the property is not set, or set to false. + /// + protected bool IsSetToTrue(string propertyName) + { + if (activeToolSwitches.ContainsKey(propertyName)) + { + return activeToolSwitches[propertyName].BooleanValue; + } + else + { + return false; + } + } + + /// + /// Returns true if the property is set to false. + /// Returns false if the property is not set, or set to true. + /// + protected bool IsExplicitlySetToFalse(string propertyName) + { + if (activeToolSwitches.ContainsKey(propertyName)) + { + return !activeToolSwitches[propertyName].BooleanValue; + } + else + { + return false; + } + } + + /// + /// Checks to see if the switch name is empty + /// + /// + /// + protected bool HasSwitch(string propertyName) + { + if (IsPropertySet(propertyName)) + { + return !String.IsNullOrEmpty(activeToolSwitches[propertyName].Name); + } + else + { + return false; + } + } + + /// + /// If the given path doesn't have a trailing slash then add one. + /// + /// The path to check. + /// A path with a slash. + protected static string EnsureTrailingSlash(string directoryName) + { + ErrorUtilities.VerifyThrow(directoryName != null, "InternalError"); + if (!String.IsNullOrEmpty(directoryName)) + { + char endingCharacter = directoryName[directoryName.Length - 1]; + if (!(endingCharacter == Path.DirectorySeparatorChar + || endingCharacter == Path.AltDirectorySeparatorChar) ) + { + directoryName += Path.DirectorySeparatorChar; + } + } + + return directoryName; + } + + /// + /// The string that is always appended on the command line. Overridden by deriving classes. + /// + protected virtual string AlwaysAppend + { + get + { + return String.Empty; + } + set + { + // do nothing + } + } + } +} diff --git a/src/XMakeTasks/DefaultTasks.bat b/src/XMakeTasks/DefaultTasks.bat new file mode 100644 index 00000000000..df9739c3429 --- /dev/null +++ b/src/XMakeTasks/DefaultTasks.bat @@ -0,0 +1,23 @@ +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\Microsoft.Common.tasks ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\Microsoft.Common.tasks ..\..\..\XMakeCommandLine\bin\Release + +rem Deprecated -- remove +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\CSharp.targets ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\CSharp.targets ..\..\..\XMakeCommandLine\bin\Release + +rem Deprecated -- remove +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\VisualBasic.targets ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\VisualBasic.targets ..\..\..\XMakeCommandLine\bin\Release + +rem Deprecated -- remove +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\Framework.targets ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\Framework.targets ..\..\..\XMakeCommandLine\bin\Release + +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\Microsoft.CSharp.targets ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\Microsoft.CSharp.targets ..\..\..\XMakeCommandLine\bin\Release + +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\Microsoft.VisualBasic.targets ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\Microsoft.VisualBasic.targets ..\..\..\XMakeCommandLine\bin\Release + +if exist ..\..\..\XMakeCommandLine\bin\Debug copy ..\..\Microsoft.Common.targets ..\..\..\XMakeCommandLine\bin\Debug +if exist ..\..\..\XMakeCommandLine\bin\Release copy ..\..\Microsoft.Common.targets ..\..\..\XMakeCommandLine\bin\Release diff --git a/src/XMakeTasks/Delegate.cs b/src/XMakeTasks/Delegate.cs new file mode 100644 index 00000000000..9def82973f1 --- /dev/null +++ b/src/XMakeTasks/Delegate.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.Win32; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.Runtime.Versioning; + +namespace Microsoft.Build.Tasks +{ + /// + /// File.GetAttributes delegate + /// + /// The path get attributes for. + internal delegate FileAttributes GetAttributes(string path); + + /// + /// File SetAttributes delegate + /// + /// The path to set attributes for. + internal delegate void SetAttributes(string path, FileAttributes attributes); + + /// + /// File SetLastAccessTime delegate. + /// + internal delegate void SetLastAccessTime(string path, DateTime timestamp); + + /// + /// File SetLastWriteTime delegate. + /// + internal delegate void SetLastWriteTime(string path, DateTime timestamp); + + /// + /// GetDirectories delegate + /// + /// The path to get directories for. + /// The pattern to search for. + /// An array of directories. + internal delegate string[] GetDirectories(string path, string pattern); + + /// + /// CopyFile delegate + /// + /// Source file + /// Destination file + internal delegate bool CopyFile(string source, string destination); + + /// + /// GetAssemblyName delegate + /// + /// The path to the file + /// The assembly name. + internal delegate AssemblyNameExtension GetAssemblyName(string path); + + /// + /// GetAssemblyRuntimeVersion delegate to get the clr runtime version of a file. + /// + /// The path to the file + /// The clr runtime version for the file + internal delegate string GetAssemblyRuntimeVersion(string path); + + /// + /// GetGacEnumerator delegate to get the enumerator which will enumerate over the GAC + /// + /// StrongName to get an enumerator for + /// The enumerator for the gac + internal delegate IEnumerable GetGacEnumerator(string strongName); + + /// + /// GetPathFromFusionName delegate to get path to a file based on the fusion name + /// + /// StrongName to get a path for + /// The path to the assembly + internal delegate string GetPathFromFusionName(string strongName); + + /// + /// Delegate. Given an assembly name, crack it open and retrieve the list of dependent + /// assemblies and the list of scatter files. + /// + /// Path to the assembly. + /// Receives the list of dependencies. + /// Receives the list of associated scatter files. + internal delegate void GetAssemblyMetadata + ( + string path, + out AssemblyNameExtension[] dependencies, + out string[] scatterFiles, + out FrameworkName frameworkNameAttribute + ); + + /// + /// Delegate to take in a dll path and read the machine type from the PEHeader + /// + internal delegate UInt16 ReadMachineTypeFromPEHeader(string dllPath); + + /// + /// Determines if a assembly is in the GAC + /// + internal delegate bool CheckIfAssemblyInGac(AssemblyNameExtension assemblyName, System.Reflection.ProcessorArchitecture targetProcessorArchitecture, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, FileExists fileExists); + + /// + /// Determines if a assembly is an winmd file + /// + internal delegate bool IsWinMDFile(string fullpath, GetAssemblyRuntimeVersion getAssemblyRuntimeVersion, FileExists fileExists, out string imageRuntimeVersion, out bool isManagedWinmd); + + /// + /// CreateFileString delegate. Creates a stream on top of a file. + /// + /// Path to the file + /// File mode + /// Access type + /// The Stream + internal delegate Stream CreateFileStream(string path, FileMode mode, FileAccess access); + + /// + /// Delegate for System.IO.File.GetLastWriteTime + /// + /// The file name + /// The last write time. + internal delegate DateTime GetLastWriteTime(string path); +} diff --git a/src/XMakeTasks/Delete.cs b/src/XMakeTasks/Delete.cs new file mode 100644 index 00000000000..55d63fa9598 --- /dev/null +++ b/src/XMakeTasks/Delete.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +namespace Microsoft.Build.Tasks +{ + /// + /// Delete files from disk. + /// + public sealed class Delete : TaskExtension, ICancelableTask + { + #region Properties + + private ITaskItem[] _files = null; + private bool _canceling = false; + + [Required] + public ITaskItem[] Files + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_files, "files"); + return _files; + } + + set + { + _files = value; + } + } + + private bool _treatErrorsAsWarnings = false; + + /// + /// When true, errors will be logged as warnings. + /// + public bool TreatErrorsAsWarnings + { + get { return _treatErrorsAsWarnings; } + set { _treatErrorsAsWarnings = value; } + } + + private ITaskItem[] _deletedFiles; + + [Output] + public ITaskItem[] DeletedFiles + { + get + { + return _deletedFiles; + } + set + { + _deletedFiles = value; + } + } + + + #endregion + + /// + /// Stop and return (in an undefined state) as soon as possible. + /// + public void Cancel() + { + _canceling = true; + } + + #region ITask Members + + /// + /// Delete the files. + /// + public override bool Execute() + { + ArrayList deletedFilesList = new ArrayList(); + HashSet deletedFilesSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (ITaskItem file in Files) + { + if (_canceling) + { + return false; + } + + try + { + // For speed, eliminate duplicates caused by poor targets authoring + if (!deletedFilesSet.Contains(file.ItemSpec)) + { + if (File.Exists(file.ItemSpec)) + { + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", file.ItemSpec); + + File.Delete(file.ItemSpec); + } + else + { + Log.LogMessageFromResources(MessageImportance.Low, "Delete.SkippingNonexistentFile", file.ItemSpec); + } + + // keep a running list of the files that were actually deleted + // note that we include in this list files that did not exist + ITaskItem deletedFile = new TaskItem(file); + deletedFilesList.Add(deletedFile); + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + LogError(file, e); + } + + // Add even on failure to avoid reattempting + deletedFilesSet.Add(file.ItemSpec); + } + // convert the list of deleted files into an array of ITaskItems + DeletedFiles = (ITaskItem[])deletedFilesList.ToArray(typeof(ITaskItem)); + return !Log.HasLoggedErrors; + } + + /// + /// Log an error. + /// + /// The file that wasn't deleted. + /// The exception. + /// Whether the task should return an error. + private void LogError(ITaskItem file, Exception e) + { + if (TreatErrorsAsWarnings) + { + Log.LogWarningWithCodeFromResources("Delete.Error", file.ItemSpec, e.Message); + } + else + { + Log.LogErrorWithCodeFromResources("Delete.Error", file.ItemSpec, e.Message); + } + } + + #endregion + } +} diff --git a/src/XMakeTasks/Dependencies.cs b/src/XMakeTasks/Dependencies.cs new file mode 100644 index 00000000000..4b8b963c4c8 --- /dev/null +++ b/src/XMakeTasks/Dependencies.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Represents a cache of inputs to a compilation-style task. + /// + [Serializable()] + internal class Dependencies + { + /// + /// Hashtable of other dependency files. + /// Key is filename and value is DependencyFile. + /// + private Hashtable _dependencies = new Hashtable(); + + /// + /// Look up a dependency file. Return null if its not there. + /// + /// + /// + internal DependencyFile GetDependencyFile(string filename) + { + return (DependencyFile)_dependencies[filename]; + } + + + /// + /// Add a new dependency file. + /// + /// + /// + internal void AddDependencyFile(string filename, DependencyFile file) + { + _dependencies[filename] = file; + } + + /// + /// Remove new dependency file. + /// + /// + /// + internal void RemoveDependencyFile(string filename) + { + _dependencies.Remove(filename); + } + + /// + /// Remove all entries from the dependency table. + /// + internal void Clear() + { + _dependencies.Clear(); + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/DependencyFile.cs b/src/XMakeTasks/DependencyFile.cs new file mode 100644 index 00000000000..1e25807cd68 --- /dev/null +++ b/src/XMakeTasks/DependencyFile.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Represents a single input to a compilation-style task. + /// Keeps track of timestamp for later comparison. + /// + [Serializable] + internal class DependencyFile + { + // Filename + private string _filename; + + // Date and time the file was last modified + private DateTime _lastModified; + + // Whether the file exists or not. + private bool _exists = false; + + /// + /// The name of the file. + /// + /// + internal string FileName + { + get { return _filename; } + } + + /// + /// The last-modified timestamp when the class was instantiated. + /// + /// + internal DateTime LastModified + { + get { return _lastModified; } + } + + /// + /// Returns true if the file existed when this class was instantiated. + /// + /// + internal bool Exists + { + get { return _exists; } + } + + /// + /// Construct. + /// + /// The file name. + internal DependencyFile(string filename) + { + _filename = filename; + + if (File.Exists(FileName)) + { + _lastModified = File.GetLastWriteTime(FileName); + _exists = true; + } + else + { + _exists = false; + } + } + + /// + /// Checks whether the file has changed since the last time a timestamp was recorded. + /// + /// + internal bool HasFileChanged() + { + FileInfo info = FileUtilities.GetFileInfoNoThrow(_filename); + + // Obviously if the file no longer exists then we are not up to date. + if (info == null || !info.Exists) + { + return true; + } + + // Check the saved timestamp against the current timestamp. + // If they are different then obviously we are out of date. + DateTime curLastModified = info.LastWriteTime; + if (curLastModified != _lastModified) + { + return true; + } + + // All checks passed -- the info should still be up to date. + return false; + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/Error.cs b/src/XMakeTasks/Error.cs new file mode 100644 index 00000000000..1d08e307688 --- /dev/null +++ b/src/XMakeTasks/Error.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Globalization; +using System.Resources; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task that simply emits an error. Engine will add project file path and line/column + /// information. + /// + public sealed class Error : TaskExtension + { + private string _text; + + /// + /// Error message + /// + public string Text + { + get + { + return _text; + } + + set + { + _text = value; + } + } + + private string _code; + + /// + /// Error code + /// + public string Code + { + get + { + return _code; + } + set + { + _code = value; + } + } + + private string _file; + + /// + /// Relevant file if any. + /// If none is provided, the file containing the Error + /// task will be used. + /// + public string File + { + get + { + return _file; + } + set + { + _file = value; + } + } + + private string _helpKeyword; + + /// + /// Error help keyword + /// + public string HelpKeyword + { + get + { + return _helpKeyword; + } + set + { + _helpKeyword = value; + } + } + + /// + /// Main task method + /// + /// + public override bool Execute() + { + if (Text != null || Code != null) + { + Log.LogError(null, Code, HelpKeyword, File, 0, 0, 0, 0, (Text == null) ? String.Empty : Text); + } + + // careful to return false. Otherwise the build would continue. + return false; + } + } +} diff --git a/src/XMakeTasks/ErrorFromResources.cs b/src/XMakeTasks/ErrorFromResources.cs new file mode 100644 index 00000000000..e0267a490f0 --- /dev/null +++ b/src/XMakeTasks/ErrorFromResources.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Task that logs an error given the appropriate resource string. +//----------------------------------------------------------------------- + +using System; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task that emits an error given a resource string. Engine will add project file path and line/column + /// information. + /// + public sealed class ErrorFromResources : TaskExtension + { + /// + /// Resource from which error message is extracted + /// + private string _resource; + + /// + /// Error code + /// + private string _code; + + /// + /// Relevant file if any. + /// If none is provided, the file containing the Error + /// task will be used. + /// + private string _file; + + /// + /// Error help keyword + /// + private string _helpKeyword; + + /// + /// Optional arguments to use when formatting the error message + /// + private string[] _arguments; + + /// + /// Resource from which error message is extracted + /// + [Required] + public string Resource + { + get + { + return _resource; + } + + set + { + _resource = value; + } + } + + /// + /// Optional arguments to use when formatting the error message + /// + public string[] Arguments + { + get + { + return _arguments; + } + + set + { + _arguments = value; + } + } + + /// + /// Error code + /// + public string Code + { + get + { + return _code; + } + + set + { + _code = value; + } + } + + /// + /// Relevant file if any. + /// If none is provided, the file containing the Error + /// task will be used. + /// + public string File + { + get + { + return _file; + } + + set + { + _file = value; + } + } + + /// + /// Error help keyword + /// + public string HelpKeyword + { + get + { + return _helpKeyword; + } + + set + { + _helpKeyword = value; + } + } + + /// + /// Log the requested error message. + /// + public override bool Execute() + { + try + { + string errorCode; + string message = ResourceUtilities.ExtractMessageCode(false /* all codes */, Log.FormatResourceString(Resource, Arguments), out errorCode); + + // If the user specifies a code, that should override. + Code = Code ?? errorCode; + + Log.LogError(null, Code, HelpKeyword, File, 0, 0, 0, 0, message); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("ErrorFromResources.LogErrorFailure", Resource, e.Message); + } + + // Effectively 'false', since by every codepath, some sort of error is getting logged. + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/XMakeTasks/Exec.cs b/src/XMakeTasks/Exec.cs new file mode 100644 index 00000000000..aa9dd6db5be --- /dev/null +++ b/src/XMakeTasks/Exec.cs @@ -0,0 +1,649 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Globalization; +using System.Text.RegularExpressions; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines an "Exec" MSBuild task, which simply invokes the specified process with the specified arguments, waits + /// for it to complete, and then returns True if the process completed successfully, and False if an error occurred. + /// + /// + /// UNDONE: ToolTask has a "UseCommandProcessor" flag that duplicates much of the code in this class. Remove the duplication. + /// + public class Exec : ToolTaskExtension + { + #region Constructors + + /// + /// Default constructor. + /// + public Exec() + { + // do nothing + } + + #endregion + + #region Fields + + // Are the ecodings for StdErr and StdOut streams valid + private bool _encodingParametersValid = true; + private string _command = String.Empty; + private string _userSpecifiedWorkingDirectory; + private string _workingDirectory; + private bool _ignoreExitCode = false; + private bool _consoleToMSBuild = false; + private ITaskItem[] _outputs; + internal bool workingDirectoryIsUNC = false; // internal for unit testing + private string _batchFile; + private string _customErrorRegex; + private string _customWarningRegex; + private bool _ignoreStandardErrorWarningFormat = false; // By default, detect standard-format errors + private List _nonEmptyOutput = new List(); + + #endregion + + #region Properties + + [Required] + public string Command + { + get + { + return _command; + } + + set + { + _command = value; + } + } + + public string WorkingDirectory + { + get + { + return _userSpecifiedWorkingDirectory; + } + + set + { + _userSpecifiedWorkingDirectory = value; + } + } + + public bool IgnoreExitCode + { + get + { + return _ignoreExitCode; + } + + set + { + _ignoreExitCode = value; + } + } + + /// + /// Enable the pipe of the standard out to an item (StandardOutput). + /// + /// + /// Even thought this is called a pipe, it is infact a Tee. Use StandardOutputImportance to adjust the visibility of the stdout. + /// + public bool ConsoleToMSBuild + { + get + { + return _consoleToMSBuild; + } + + set + { + _consoleToMSBuild = value; + } + } + + /// + /// Users can supply a regular expression that we should + /// use to spot error lines in the tool output. This is + /// useful for tools that produce unusually formatted output + /// + public string CustomErrorRegularExpression + { + get { return _customErrorRegex; } + set { _customErrorRegex = value; } + } + + /// + /// Users can supply a regular expression that we should + /// use to spot warning lines in the tool output. This is + /// useful for tools that produce unusually formatted output + /// + public string CustomWarningRegularExpression + { + get { return _customWarningRegex; } + set { _customWarningRegex = value; } + } + + /// + /// Whether to use pick out lines in the output that match + /// the standard error/warning format, and log them as errors/warnings. + /// Defaults to true. + /// + public bool IgnoreStandardErrorWarningFormat + { + get { return _ignoreStandardErrorWarningFormat; } + set { _ignoreStandardErrorWarningFormat = value; } + } + + /// + /// Property specifying the encoding of the captured task standard output stream + /// + protected override Encoding StandardOutputEncoding + { + get { return _standardOutputEncoding; } + } + + /// + /// Console-based output uses the current system OEM code page by default. Note that we should not use Console.OutputEncoding + /// here since processes we run don't really have much to do with our console window (and also Console.OutputEncoding + /// doesn't return the OEM code page if the running application that hosts MSBuild is not a console application). + /// + private Encoding _standardOutputEncoding = EncodingUtilities.CurrentSystemOemEncoding; + + /// + /// Property specifying the encoding of the captured task standard error stream + /// + protected override Encoding StandardErrorEncoding + { + get { return _standardErrorEncoding; } + } + + /// + /// Console-based output uses the current system OEM code page by default. Note that we should not use Console.OutputEncoding + /// here since processes we run don't really have much to do with our console window (and also Console.OutputEncoding + /// doesn't return the OEM code page if the running application that hosts MSBuild is not a console application). + /// + private Encoding _standardErrorEncoding = EncodingUtilities.CurrentSystemOemEncoding; + + /// + /// Project visible property specifying the encoding of the captured task standard output stream + /// + [Output] + public string StdOutEncoding + { + get { return StandardOutputEncoding.EncodingName; } + set + { + try + { + _standardOutputEncoding = Encoding.GetEncoding(value); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", "StdOutEncoding", "Exec"); + _encodingParametersValid = false; + } + } + } + + /// + /// Project visible property specifying the encoding of the captured task standard error stream + /// + [Output] + public string StdErrEncoding + { + get { return StandardErrorEncoding.EncodingName; } + set + { + try + { + _standardErrorEncoding = Encoding.GetEncoding(value); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", "StdErrEncoding", "Exec"); + _encodingParametersValid = false; + } + } + } + + [Output] + public ITaskItem[] Outputs + { + get + { + if (_outputs == null) + { + return new ITaskItem[0]; + } + else + { + return _outputs; + } + } + + set + { + _outputs = value; + } + } + + /// + /// Returns the output as an Item. Whitespace are trimmed. + /// ConsoleOutput is enabled when ConsoleToMSBuild is true. This avoids holding lines in memory + /// if they aren't used. ConsoleOutput is a combination of stdout and stderr. + /// + [Output] + public ITaskItem[] ConsoleOutput + { + get + { + if (!ConsoleToMSBuild) + { + return new ITaskItem[0]; + } + else + { + return _nonEmptyOutput.ToArray(); + } + } + set { } + } + + #endregion + + #region Methods + /// + /// Write out a temporary batch file with the user-specified command in it. + /// + private void CreateTemporaryBatchFile() + { + // Temporary file with the extension .Exec.bat + _batchFile = FileUtilities.GetTemporaryFile(".exec.cmd"); + + // UNICODE Batch files are not allowed as of WinXP. We can't use normal ANSI code pages either, + // since console-related apps use OEM code pages "for historical reasons". Sigh. + // We need to get the current OEM code page which will be the same language as the current ANSI code page, + // just the OEM version. + // See http://www.microsoft.com/globaldev/getWR/steps/wrg_codepage.mspx for a discussion of ANSI vs OEM + using (StreamWriter sw = new StreamWriter(_batchFile, false, EncodingUtilities.CurrentSystemOemEncoding)) // HIGHCHAR: Exec task batch files are in OEM code pages (not ANSI!) + { + // In some wierd setups, users may have set an env var actually called "errorlevel" + // this would cause our "exit %errorlevel%" to return false. + // This is because the actual errorlevel value is not an environment variable, but some commands, + // such as "exit %errorlevel%" will use the environment variable with that name if it exists, instead + // of the actual errorlevel value. So we must temporarily reset errorlevel locally first. + sw.WriteLine("setlocal"); + // One more wrinkle. + // "set foo=" has odd behavior: it sets errorlevel to 1 if there was no environment variable named + // "foo" defined. + // This has the effect of making "set errorlevel=" set an errorlevel of 1 if an environment + // variable named "errorlevel" didn't already exist! + // To avoid this problem, set errorlevel locally to a dummy value first. + sw.WriteLine("set errorlevel=dummy"); + sw.WriteLine("set errorlevel="); + + // if the working directory is a UNC path, bracket the exec command with pushd and popd, because pushd + // automatically maps the network path to a drive letter, and then popd disconnects it + if (workingDirectoryIsUNC) + { + sw.WriteLine("pushd " + _workingDirectory); + } + + sw.WriteLine(Command); + + if (workingDirectoryIsUNC) + { + sw.WriteLine("popd"); + } + + // NOTES: + // 1) there's a bug in the Process class where the exit code is not returned properly i.e. if the command + // fails with exit code 9009, Process.ExitCode returns 1 -- the statement below forces it to return the + // correct exit code + // 2) also because of another (or perhaps the same) bug in the Process class, when we use pushd/popd for a + // UNC path, even if the command fails, the exit code comes back as 0 (seemingly reflecting the success + // of popd) -- the statement below fixes that too + // 3) the above described behaviour is most likely bugs in the Process class because batch files in a + // console window do not hide or change the exit code a.k.a. errorlevel, esp. since the popd command is + // a no-fail command, and it never changes the previous errorlevel + sw.WriteLine("exit %errorlevel%"); + } + } + #endregion + + #region Overridden methods + + /// + /// Executes cmd.exe and waits for it to complete + /// + /// + /// Overridden to clean up the batch file afterwards. + /// + /// Upon completion of the process, returns True if successful, False if not. + protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands) + { + try + { + return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands); + } + finally + { + DeleteTempFile(_batchFile); + } + } + + /// + /// Allows tool to handle the return code. + /// This method will only be called with non-zero exitCode set to true. + /// + /// + /// Overridden to make sure we display the command we put in the batch file, not the cmd.exe command + /// used to run the batch file. + /// + protected override bool HandleTaskExecutionErrors() + { + if (_ignoreExitCode) + { + Log.LogMessageFromResources(MessageImportance.Normal, "Exec.CommandFailedNoErrorCode", this.Command, ExitCode); + return true; + } + else + { + if (ExitCode == NativeMethods.SE_ERR_ACCESSDENIED) + { + Log.LogErrorWithCodeFromResources("Exec.CommandFailedAccessDenied", this.Command, ExitCode); + } + else + { + Log.LogErrorWithCodeFromResources("Exec.CommandFailed", this.Command, ExitCode); + } + return false; + } + } + + /// + /// Logs the tool name and the path from where it is being run. + /// + /// + /// Overridden to avoid logging the path to "cmd.exe", which is not interesting. + /// + protected override void LogPathToTool(string toolName, string pathToTool) + { + // Do nothing + return; + } + + /// + /// Logs the command to be executed. + /// + /// + /// Overridden to log the batch file command instead of the cmd.exe command. + /// + /// + protected override void LogToolCommand + ( + string message + ) + { + //Dont print the command line if Echo is Off. + if (!EchoOff) + { + base.LogToolCommand(Command); + } + } + + /// + /// Calls a method on the TaskLoggingHelper to parse a single line of text to + /// see if there are any errors or warnings in canonical format. + /// + /// + /// Overridden to handle any custom regular expressions supplied. + /// + protected override void LogEventsFromTextOutput + ( + string singleLine, + MessageImportance messageImportance + ) + { + if (OutputMatchesRegex(singleLine, ref _customErrorRegex)) + { + Log.LogError(singleLine); + } + else if (OutputMatchesRegex(singleLine, ref _customWarningRegex)) + { + Log.LogWarning(singleLine); + } + else if (_ignoreStandardErrorWarningFormat) + { + // Not detecting regular format errors and warnings, and it didn't + // match any regexes either -- log as a regular message + Log.LogMessage(messageImportance, singleLine, null); + } + else + { + // This is the normal code path: match standard format errors and warnings + Log.LogMessageFromText(singleLine, messageImportance); + } + + if (ConsoleToMSBuild) + { + string trimmedTextLine = singleLine.Trim(); + if (trimmedTextLine.Length > 0) + { + // The lines read may be unescaped, so we need to escape them + // before passing them to the TaskItem. + _nonEmptyOutput.Add(new TaskItem(EscapingUtilities.Escape(trimmedTextLine))); + } + } + } + + /// + /// Returns true if the string is matched by the regular expression. + /// If the regular expression is invalid, logs an error, then clears it out to + /// prevent more errors. + /// + private bool OutputMatchesRegex(string singleLine, ref string regularExpression) + { + if (regularExpression == null) + { + return false; + } + + bool match = false; + + try + { + match = Regex.IsMatch(singleLine, regularExpression); + } + catch (ArgumentException ex) + { + Log.LogErrorWithCodeFromResources("Exec.InvalidRegex", regularExpression, ex.Message); + // Clear out the regex so there won't be any more errors; let the tool continue, + // then it will fail because of the error we just logged + regularExpression = null; + } + + return match; + } + + /// + /// Validate the task arguments, log any warnings/errors + /// + /// true if arguments are corrent enough to continue processing, false otherwise + protected override bool ValidateParameters() + { + // If either of the encoding parameters passed to the task were + // invalid, then we should report that fact back to tooltask + if (!_encodingParametersValid) + { + return false; + } + + // Make sure that at least the Command property was set + if (Command.Trim().Length == 0) + { + Log.LogErrorWithCodeFromResources("Exec.MissingCommandError"); + return false; + } + + // determine what the working directory for the exec command is going to be -- if the user specified a working + // directory use that, otherwise it's the current directory + _workingDirectory = ((_userSpecifiedWorkingDirectory != null) && (_userSpecifiedWorkingDirectory.Length > 0)) + ? _userSpecifiedWorkingDirectory + : Directory.GetCurrentDirectory(); + + // check if the working directory we're going to use for the exec command is a UNC path + workingDirectoryIsUNC = FileUtilitiesRegex.UNCPattern.IsMatch(_workingDirectory); + + // if the working directory is a UNC path, and all drive letters are mapped, bail out, because the pushd command + // will not be able to auto-map to the UNC path + if (workingDirectoryIsUNC && (DriveInfo.GetDrives().Length == 26)) + { + Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory); + return false; + } + + return true; + } + + /// + /// Accessor for ValidateParameters purely for unit-test use + /// + /// + internal bool ValidateParametersAccessor() + { + return ValidateParameters(); + } + + /// + /// Determining the path to cmd.exe + /// + /// path to cmd.exe + protected override string GenerateFullPathToTool() + { + // Get the fully qualified path to cmd.exe + return ToolLocationHelper.GetPathToSystemFile("cmd.exe"); + } + + /// + /// Gets the working directory to use for the process. Should return null if ToolTask should use the + /// current directory. + /// May throw an IOException if the directory to be used is somehow invalid. + /// + /// working directory + protected override string GetWorkingDirectory() + { + // If the working directory is UNC, we're going to use "pushd" in the batch file to set it. + // If it's invalid, pushd won't fail: it will just go ahead and use the system folder. + // So verify it's valid here. + if (!Directory.Exists(_workingDirectory)) + { + throw new DirectoryNotFoundException(ResourceUtilities.FormatResourceString("Exec.InvalidWorkingDirectory", _workingDirectory)); + } + + if (workingDirectoryIsUNC) + { + // if the working directory for the exec command is UNC, set the process working directory to the system path + // so that it doesn't display this silly error message: + // '\\\' + // CMD.EXE was started with the above path as the current directory. + // UNC paths are not supported. Defaulting to Windows directory. + return ToolLocationHelper.PathToSystem; + } + else + { + return _workingDirectory; + } + } + + /// + /// Accessor for GetWorkingDirectory purely for unit-test use + /// + /// + internal string GetWorkingDirectoryAccessor() + { + return GetWorkingDirectory(); + } + + /// + /// Adds the arguments for cmd.exe + /// + /// command line builder class to add arguments to + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + // Create the batch file now, + // so we have the file name for the cmd.exe command line + CreateTemporaryBatchFile(); + + string batchFileForCommandLine = _batchFile; + commandLine.AppendSwitch("/Q"); // echo off + commandLine.AppendSwitch("/C"); // run then terminate + + // If for some crazy reason the path has a & character and a space in it + // then get the short path of the temp path, which should not have spaces in it + // and then escape the & + if (batchFileForCommandLine.Contains("&") && !batchFileForCommandLine.Contains("^&")) + { + batchFileForCommandLine = NativeMethodsShared.GetShortFilePath(batchFileForCommandLine); + batchFileForCommandLine = batchFileForCommandLine.Replace("&", "^&"); + } + + commandLine.AppendFileNameIfNotNull(batchFileForCommandLine); + } + + #endregion + + #region Overridden properties + + /// + /// The name of the tool to execute + /// + protected override string ToolName + { + get + { + return "cmd.exe"; + } + } + + /// + /// Importance with which to log ordinary messages in the + /// standard error stream. + /// + protected override MessageImportance StandardErrorLoggingImportance + { + get { return MessageImportance.High; } + } + + /// + /// Importance with which to log ordinary messages in the + /// standard out stream. + /// + /// + /// Overridden to increase from the default "Low" up to "High". + /// + protected override MessageImportance StandardOutputLoggingImportance + { + get { return MessageImportance.High; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ExtractedClassName.cs b/src/XMakeTasks/ExtractedClassName.cs new file mode 100644 index 00000000000..e870a9deada --- /dev/null +++ b/src/XMakeTasks/ExtractedClassName.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.Tasks +{ + /// + /// Extracted class name from the source file. + /// + public struct ExtractedClassName + { + // whether or not we found the name inside a block of conditionally compiled code + private bool _isInsideConditionalBlock; + // the extracted class name + private string _name; + + /// + /// Whether or not we found the name inside a block of conditionally compiled code + /// + public bool IsInsideConditionalBlock + { + get { return _isInsideConditionalBlock; } + set { _isInsideConditionalBlock = value; } + } + + /// + /// Extracted class name + /// + public string Name + { + get { return _name; } + set { _name = value; } + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/FileIO/ReadLinesFromFile.cs b/src/XMakeTasks/FileIO/ReadLinesFromFile.cs new file mode 100644 index 00000000000..9d3a4f23bf2 --- /dev/null +++ b/src/XMakeTasks/FileIO/ReadLinesFromFile.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Diagnostics; +using System.Globalization; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Read a list of items from a file. + /// + public class ReadLinesFromFile : TaskExtension + { + /// + /// Construct. + /// + public ReadLinesFromFile() + { + } + + private ITaskItem _file = null; + private ITaskItem[] _lines = new TaskItem[0]; + + /// + /// File to read lines from. + /// + [Required] + public ITaskItem File + { + get { return _file; } + set { _file = value; } + } + + /// + /// Receives lines from file. + /// + [Output] + public ITaskItem[] Lines + { + get { return _lines; } + set { _lines = value; } + } + + /// + /// Execute the task. + /// + /// + public override bool Execute() + { + bool success = true; + if (File != null) + { + if (System.IO.File.Exists(File.ItemSpec)) + { + string[] textLines = null; + try + { + textLines = System.IO.File.ReadAllLines(File.ItemSpec); + + ArrayList nonEmptyLines = new ArrayList(); + char[] charsToTrim = new char[] { '\0', ' ', '\t' }; + + foreach (string textLine in textLines) + { + // A customer has given us a project with a FileList.txt file containing + // a line full of '\0' characters. We don't know how these characters + // got in there, but when we try to read the file back in, we fail + // miserably. Here, we Trim to protect us from this situation. + string trimmedTextLine = textLine.Trim(charsToTrim); + if (trimmedTextLine.Length > 0) + { + // The lines were written to the file in unescaped form, so we need to escape them + // before passing them to the TaskItem. + nonEmptyLines.Add(new TaskItem(EscapingUtilities.Escape(trimmedTextLine))); + } + } + + Lines = (ITaskItem[])nonEmptyLines.ToArray(typeof(ITaskItem)); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + LogError(_file, e, ref success); + } + } + } + + return success; + } + + /// + /// Log an error. + /// + /// The being accessed + /// The exception. + /// Whether the task should return an error. + private void LogError(ITaskItem fileName, Exception e, ref bool success) + { + Log.LogErrorWithCodeFromResources("ReadLinesFromFile.ErrorOrWarning", fileName.ItemSpec, e.Message); + success = false; + } + } +} diff --git a/src/XMakeTasks/FileIO/WriteLinesToFile.cs b/src/XMakeTasks/FileIO/WriteLinesToFile.cs new file mode 100644 index 00000000000..53f6369425a --- /dev/null +++ b/src/XMakeTasks/FileIO/WriteLinesToFile.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Collections; +using System.Globalization; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Appends a list of items to a file. One item per line with carriage returns in-between. + /// + public class WriteLinesToFile : TaskExtension + { + /// + /// Construct. + /// + public WriteLinesToFile() + { + } + + private ITaskItem _file = null; + private ITaskItem[] _lines = null; + private bool _overwrite = false; + private string _encoding = null; + + /// + /// File to write lines to. + /// + [Required] + public ITaskItem File + { + get { return _file; } + set { _file = value; } + } + + /// + /// Write each item as a line in the file. + /// + public ITaskItem[] Lines + { + get { return _lines; } + set { _lines = value; } + } + + /// + /// If true, overwrite any existing file contents. + /// + public bool Overwrite + { + get { return _overwrite; } + set { _overwrite = value; } + } + + /// + /// If true, overwrite any existing file contents. + /// + public string Encoding + { + get { return _encoding; } + set { _encoding = value; } + } + + + /// + /// Execute the task. + /// + /// + public override bool Execute() + { + bool success = true; + + if (File != null) + { + // do not return if Lines is null, because we may + // want to delete the file in that case + StringBuilder buffer = new StringBuilder(); + if (Lines != null) + { + foreach (ITaskItem line in Lines) + { + buffer.AppendLine(line.ItemSpec); + } + } + + Encoding encode = null; + if (_encoding != null) + { + try + { + encode = System.Text.Encoding.GetEncoding(_encoding); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", "Encoding", "WriteLinesToFile"); + return false; + } + } + + + try + { + if (Overwrite) + { + if (buffer.Length == 0) + { + // if overwrite==true, and there are no lines to write, + // just delete the file to leave everything tidy. + System.IO.File.Delete(File.ItemSpec); + } + else + { + // Passing a null encoding, or Encoding.Default, to WriteAllText or AppendAllText + // is not the same as calling the overload that does not take encoding! + // Encoding.Default is based on the current codepage, the overload without encoding is UTF8-without-BOM. + if (encode == null) + { + System.IO.File.WriteAllText(File.ItemSpec, buffer.ToString()); + } + else + { + System.IO.File.WriteAllText(File.ItemSpec, buffer.ToString(), encode); + } + } + } + else + { + if (encode == null) + { + System.IO.File.AppendAllText(File.ItemSpec, buffer.ToString()); + } + else + { + System.IO.File.AppendAllText(File.ItemSpec, buffer.ToString(), encode); + } + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + LogError(_file, e, ref success); + } + } + + return success; + } + + /// + /// Log an error. + /// + /// The being accessed + /// The exception. + /// Whether the task should return an error. + private void LogError(ITaskItem fileName, Exception e, ref bool success) + { + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", fileName.ItemSpec, e.Message); + success = false; + } + } +} diff --git a/src/XMakeTasks/FileState.cs b/src/XMakeTasks/FileState.cs new file mode 100644 index 00000000000..c1e7b7ee93a --- /dev/null +++ b/src/XMakeTasks/FileState.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Cache file state over file name. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// CopyFile delegate + /// + /// returns Success = true, Failure = false; Retry = null + /// + /// Source file + /// Destination file + internal delegate bool? CopyFileWithState(FileState source, FileState destination); + + /// + /// Short-term cache saves the result of IO operations on a filename. Should only be + /// used in cases where it is know there will be no exogenous changes to the filesystem + /// for this file. + /// + /// + /// Uses PInvoke rather than FileInfo because the latter does all kinds of expensive checks. + /// + /// Deficiency: some of the properties eat some or all exceptions. If they are called first, they will + /// trigger the population and eat. Subsequent calls will then not throw, but instead eg return zero. + /// This could be fixed by storing the exception from the population, and throwing no matter who does + /// the population and whether it's been done before. + /// + internal class FileState + { + /// + /// The name of the file. + /// + private string _filename; + + /// + /// The info about the file. + /// + private NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA _data; + + /// + /// Whether data is reliable. + /// False means that we tried to get it, but failed. Only Reset will get it again. + /// Null means we didn't try yet. + /// + private bool? _dataIsGood; + + /// + /// Whether the file or directory exists. + /// Used instead of an exception, for perf. + /// + private bool _fileOrDirectoryExists; + + /// + /// Constructor. + /// Only stores file name: does not grab the file state until first request. + /// + internal FileState(string filename) + { + ErrorUtilities.VerifyThrowArgumentLength(filename, "filename"); + _filename = filename; + } + + /// + /// Whether the file is readonly. + /// Returns false for directories. + /// Throws if file does not exist. + /// + internal bool IsReadOnly + { + get + { + EnsurePopulated(); + + if (DirectoryExists) + { + return false; + } + + if (!FileExists) + { + // Provoke exception + var length = (new FileInfo(_filename)).Length; + } + + return ((_data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_READONLY) != 0); + } + } + + /// + /// Whether the file exists. + /// Returns false if it is a directory, even if it exists. + /// Returns false instead of IO related exceptions. + /// + internal bool FileExists + { + get + { + try + { + EnsurePopulated(); + } + catch (Exception ex) + { + if (!ExceptionHandling.IsIoRelatedException(ex)) + { + throw; + } + + return false; + } + + return _fileOrDirectoryExists && !IsDirectory; + } + } + + /// + /// Whether the directory exists. + /// Returns false for files. + /// Returns false instead of IO related exceptions. + /// + internal bool DirectoryExists + { + get + { + try + { + EnsurePopulated(); + } + catch (Exception ex) + { + if (!ExceptionHandling.IsIoRelatedException(ex)) + { + throw; + } + + return false; + } + + return _fileOrDirectoryExists && IsDirectory; + } + } + + /// + /// Last time the file was written. + /// If file does not exist, returns 12 midnight 1/1/1601. + /// Works for directories. + /// + internal DateTime LastWriteTime + { + get + { + // Could cache this as conversion can be expensive + return LastWriteTimeUtcFast.ToLocalTime(); + } + } + + /// + /// Last time the file was written, in UTC. Avoids translation for daylight savings, time zone etc which isn't needed for just comparisons. + /// If file does not exist, returns 12 midnight 1/1/1601. + /// Works for directories. + /// + internal DateTime LastWriteTimeUtcFast + { + get + { + EnsurePopulated(); + + if (!_fileOrDirectoryExists) + { + // Same as the FileInfo class + return new DateTime(1601, 1, 1); + } + + return DateTime.FromFileTimeUtc(((long)_data.ftLastWriteTimeHigh << 0x20) | _data.ftLastWriteTimeLow); + } + } + + /// + /// Length of the file in bytes. + /// Throws if it is a directory. + /// Throws if it does not exist. + /// + internal long Length + { + get + { + EnsurePopulated(); + + if (DirectoryExists) + { + // Produce a nice file not found exception message + var info = new FileInfo(_filename).Length; + } + + if (!FileExists) + { + // Provoke exception + var length = (new FileInfo(_filename)).Length; + } + + return (((long)_data.fileSizeHigh << 0x20) | _data.fileSizeLow); + } + } + + /// + /// Name of the file as it was passed in. + /// Not normalized. + /// + internal string Name + { + get + { + return _filename; + } + } + + /// + /// Whether this is a directory. + /// Throws if it does not exist. + /// + internal bool IsDirectory + { + get + { + EnsurePopulated(); + + if (!_fileOrDirectoryExists) + { + // Provoke exception + var length = (new FileInfo(_filename)).Length; + } + + return ((_data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) != 0); + } + } + + /// + /// Use in case the state is known to have changed exogenously. + /// + internal void Reset() + { + _data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA(); + _dataIsGood = null; + } + + /// + /// Ensure we have the data. + /// Does not throw for nonexistence. + /// + private void EnsurePopulated() + { + if (_dataIsGood == null) + { + _dataIsGood = false; + _filename = FileUtilities.AttemptToShortenPath(_filename); // This is no-op unless the path actually is too long + _data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA(); + + // THIS COPIED FROM THE BCL: + // + // For floppy drives, normally the OS will pop up a dialog saying + // there is no disk in drive A:, please insert one. We don't want that. + // SetErrorMode will let us disable this, but we should set the error + // mode back, since this may have wide-ranging effects. + int oldMode = NativeMethodsShared.SetErrorMode(1 /* ErrorModes.SEM_FAILCRITICALERRORS */); + + bool success = false; + _fileOrDirectoryExists = true; + + try + { + success = NativeMethodsShared.GetFileAttributesEx(_filename, 0, ref _data); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + + // File not found is the most common case, for example we're copying + // somewhere without a file yet. Don't do something like FileInfo.Exists to + // get a nice error, or we're doing IO again! Don't even format our own string: + // that turns out to be unacceptably expensive here as well. Set a flag for this particular case. + // + // Also, when not under debugger (!) it will give error == 3 for path too long. Make that consistently throw instead. + if ((error == 2 /* ERROR_FILE_NOT_FOUND */ || error == 3 /* ERROR_PATH_NOT_FOUND */) && + _filename.Length <= NativeMethodsShared.MAX_PATH) + { + _fileOrDirectoryExists = false; + return; + } + + // Throw nice message as far as we can. At this point IO is OK. + var length = new FileInfo(_filename).Length; + + // Otherwise this will give at least something + NativeMethodsShared.ThrowExceptionForErrorCode(error); + ErrorUtilities.ThrowInternalErrorUnreachable(); + } + } + finally + { + NativeMethodsShared.SetErrorMode(oldMode); + } + + _dataIsGood = true; + } + } + } +} diff --git a/src/XMakeTasks/FindAppConfigFile.cs b/src/XMakeTasks/FindAppConfigFile.cs new file mode 100644 index 00000000000..78a595ab870 --- /dev/null +++ b/src/XMakeTasks/FindAppConfigFile.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Finds the app.config file, if any, in the provided lists. + /// For compat reasons, it has to follow a particular arbitrary algorithm. + /// It also adds the TargetPath metadata. + /// + public class FindAppConfigFile : TaskExtension + { + // The list to search through + private ITaskItem[] _primaryList; + private ITaskItem[] _secondaryList; + + // The target path metadata value to add to the found item + private string _targetPath; + + // The item found, if any + private ITaskItem _appConfigFileFound = null; + + // What we're looking for + private const string appConfigFile = "app.config"; + + /// + /// The primary list to search through + /// + [Required] + public ITaskItem[] PrimaryList + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_primaryList, "primaryList"); + return _primaryList; + } + set { _primaryList = value; } + } + + /// + /// The secondary list to search through + /// + [Required] + public ITaskItem[] SecondaryList + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_secondaryList, "secondaryList"); + return _secondaryList; + } + set { _secondaryList = value; } + } + + /// + /// The value to add as TargetPath metadata + /// + [Required] + public string TargetPath + { + get { return _targetPath; } + set { _targetPath = value; } + } + + /// + /// The first matching item found in the list, if any + /// + [Output] + public ITaskItem AppConfigFile + { + get { return _appConfigFileFound; } + set { _appConfigFileFound = value; } + } + + /// + /// Find the app config + /// + public override bool Execute() + { + // Look at the whole item spec first -- ie, + // we want to prefer app.config files that are directly in the project folder. + if (ConsultLists(true)) + { + return true; + } + + // If that fails, fall back to app.config files anywhere in the project cone. + if (ConsultLists(false)) + { + return true; + } + + // Not found + return true; + } + + private bool ConsultLists(bool matchWholeItemSpec) + { + // Look at primary list first, then secondary list + // We walk backwards on the list to find the last match (for historical reasons) + for (int i = PrimaryList.Length - 1; i >= 0; i--) + { + if (IsMatchingItem(PrimaryList[i], matchWholeItemSpec)) + { + return true; + } + } + + for (int i = SecondaryList.Length - 1; i >= 0; i--) + { + if (IsMatchingItem(SecondaryList[i], matchWholeItemSpec)) + { + return true; + } + } + + return false; + } + + /// + /// Examines the item to see if it matches what we are looking for. + /// If it does, returns true. + /// + private bool IsMatchingItem(ITaskItem item, bool matchWholeItemSpec) + { + string filename; + try + { + filename = (matchWholeItemSpec ? item.ItemSpec : Path.GetFileName(item.ItemSpec)); + + if (String.Equals(filename, appConfigFile, StringComparison.OrdinalIgnoreCase)) + { + _appConfigFileFound = item; + + // Originally the app.config was found in such a way that it's "OriginalItemSpec" + // metadata was cleared out. Although it doesn't really matter, for compatibility, + // we'll clear it out here. + _appConfigFileFound.SetMetadata("OriginalItemSpec", item.ItemSpec); + + _appConfigFileFound.SetMetadata(ItemMetadataNames.targetPath, TargetPath); + + Log.LogMessageFromResources(MessageImportance.Low, "FindInList.Found", _appConfigFileFound.ItemSpec); + return true; + } + } + catch (ArgumentException ex) + { + // Just log this: presumably this item spec is not intended to be + // a file path + Log.LogMessageFromResources(MessageImportance.Low, "FindInList.InvalidPath", item.ItemSpec, ex.Message); + } + return false; + } + } +} diff --git a/src/XMakeTasks/FindInList.cs b/src/XMakeTasks/FindInList.cs new file mode 100644 index 00000000000..bc232b0a1d8 --- /dev/null +++ b/src/XMakeTasks/FindInList.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task that finds an item with the specified itemspec, if present, + /// in the provided list. + /// + public class FindInList : TaskExtension + { + // The list to search through + private ITaskItem[] _list; + // Whether to match just the file part, or the full item spec + private bool _matchFileNameOnly = false; + // The item found, if any + private ITaskItem _itemFound = null; + // The itemspec to find + private string _itemSpecToFind; + // Whether to match case sensitively + // Default is case insensitive + private bool _caseSensitive; + // Whether to return the last match + // (default is the first match) + private bool _findLastMatch = false; + + /// + /// The list to search through + /// + [Required] + public ITaskItem[] List + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_list, "list"); + return _list; + } + set { _list = value; } + } + + /// + /// Whether to match against just the file part of the itemspec, + /// or the whole itemspec (the default) + /// + public bool MatchFileNameOnly + { + get { return _matchFileNameOnly; } + set { _matchFileNameOnly = value; } + } + + /// + /// The first matching item found in the list, if any + /// + [Output] + public ITaskItem ItemFound + { + get { return _itemFound; } + set { _itemFound = value; } + } + + /// + /// The itemspec to try to find + /// + [Required] + public string ItemSpecToFind + { + get { return _itemSpecToFind; } + set { _itemSpecToFind = value; } + } + + /// + /// Whether or not to match case sensitively + /// + public bool CaseSensitive + { + get { return _caseSensitive; } + set { _caseSensitive = value; } + } + + /// + /// Whether or not to return the last match, instead of + /// the first one + /// + public bool FindLastMatch + { + get { return _findLastMatch; } + set { _findLastMatch = value; } + } + + /// + /// Entry point + /// + public override bool Execute() + { + StringComparison comparison; + if (_caseSensitive) + { + comparison = StringComparison.Ordinal; + } + else + { + comparison = StringComparison.OrdinalIgnoreCase; + } + + if (!FindLastMatch) + { + // Walk forwards + foreach (ITaskItem item in List) + { + if (IsMatchingItem(comparison, item)) + { + return true; + } + } + } + else + { + // Walk backwards + for (int i = List.Length - 1; i >= 0; i--) + { + if (IsMatchingItem(comparison, List[i])) + { + return true; + } + } + } + + // Not found + return true; + } + + /// + /// Examines the item to see if it matches what we are looking for. + /// If it does, returns true. + /// + private bool IsMatchingItem(StringComparison comparison, ITaskItem item) + { + string filename; + try + { + filename = (MatchFileNameOnly ? Path.GetFileName(item.ItemSpec) : item.ItemSpec); + + if (String.Equals(filename, _itemSpecToFind, comparison)) + { + ItemFound = item; + Log.LogMessageFromResources(MessageImportance.Low, "FindInList.Found", item.ItemSpec); + return true; + } + } + catch (ArgumentException ex) + { + // Just log this: presumably this item spec is not intended to be + // a file path + Log.LogMessageFromResources(MessageImportance.Low, "FindInList.InvalidPath", item.ItemSpec, ex.Message); + } + return false; + } + } +} diff --git a/src/XMakeTasks/FindInvalidProjectReferences.cs b/src/XMakeTasks/FindInvalidProjectReferences.cs new file mode 100644 index 00000000000..127a77fbea7 --- /dev/null +++ b/src/XMakeTasks/FindInvalidProjectReferences.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Get the reference assembly paths for a given target framework version / moniker. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Tasks +{ + /// + /// Returns the reference assembly paths to the various frameworks + /// + public class FindInvalidProjectReferences : TaskExtension + { + #region Fields + + /// + /// Regex for breaking up the platform moniker + /// Example: XNA, Version=8.0 + /// + private static readonly Regex s_platformMonikerFormat = new Regex + ( + @"(?^[^,]*),\s*Version=(?.*)", + RegexOptions.IgnoreCase + ); + + /// + /// Reference moniker metadata + /// + private static readonly string s_referencePlatformMonikerMetadata = "TargetPlatformMoniker"; + + /// + /// SimpleName group + /// + private static readonly string s_platformSimpleNameGroup = "PLATFORMIDENTITY"; + + /// + /// Version group + /// + private static readonly string s_platformVersionGroup = "PLATFORMVERSION"; + + #endregion + + #region Properties + + /// + /// List of Platform monikers for each referenced project + /// + public ITaskItem[] ProjectReferences + { + get; + set; + } + + /// + /// Target platform version of the current project + /// + [Required] + public string TargetPlatformVersion + { + get; + set; + } + + /// + /// Target platform identifier of the current project + /// + [Required] + public string TargetPlatformIdentifier + { + get; + set; + } + + /// + /// Invalid references to be unresolved + /// + [Output] + public ITaskItem[] InvalidReferences + { + get; + private set; + } + + #endregion + + #region ITask Members + + /// + /// Execute the task. + /// + public override bool Execute() + { + Version targetPlatformVersionAsVersion = null; + List invalidReferences = new List(); + + Version.TryParse(TargetPlatformVersion, out targetPlatformVersionAsVersion); + + if (ProjectReferences != null) + { + foreach (ITaskItem item in ProjectReferences) + { + string referenceIdentity = item.ItemSpec; + string referencePlatformMoniker = item.GetMetadata(s_referencePlatformMonikerMetadata); + + string platform = null; + Version version = null; + + // For each moniker, compare version, issue localized message if the referenced project targets + // a platform with version higher than the current project and make the reference invalid by adding it to + // an invalid reference list output + if (ParseMoniker(referencePlatformMoniker, out platform, out version)) + { + if (targetPlatformVersionAsVersion < version) + { + Log.LogWarningWithCodeFromResources("FindInvalidProjectReferences.WarnWhenVersionIsIncompatible", TargetPlatformIdentifier, TargetPlatformVersion, referenceIdentity, referencePlatformMoniker); + invalidReferences.Add(item); + } + } + } + } + + InvalidReferences = invalidReferences.ToArray(); + + return true; + } + + /// + /// Take the identity and the version of a platform moniker + /// + private static bool ParseMoniker(string reference, out string platformIdentity, out Version platformVersion) + { + Match match = s_platformMonikerFormat.Match(reference); + + platformIdentity = String.Empty; + bool parsedVersion = false; + + platformVersion = null; + + if (match.Success) + { + platformIdentity = match.Groups[s_platformSimpleNameGroup].Value.Trim(); + + string rawVersion = match.Groups[s_platformVersionGroup].Value.Trim(); + parsedVersion = Version.TryParse(rawVersion, out platformVersion); + } + + return platformIdentity.Length > 0 && parsedVersion; + } + + #endregion + } +} diff --git a/src/XMakeTasks/FormatUrl.cs b/src/XMakeTasks/FormatUrl.cs new file mode 100644 index 00000000000..c9193d1a5ca --- /dev/null +++ b/src/XMakeTasks/FormatUrl.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Formats a url by canonicalizing it (i.e. " " -> "%20") and transforming "localhost" to "machinename". + /// + public sealed class FormatUrl : TaskExtension + { + private string _inputUrl; + private string _outputUrl; + + public string InputUrl + { + get { return _inputUrl; } + set { _inputUrl = value; } + } + + [Output] + public string OutputUrl + { + get { return _outputUrl; } + set { _outputUrl = value; } + } + + public override bool Execute() + { + if (_inputUrl != null) + _outputUrl = PathUtil.Format(_inputUrl); + else + _outputUrl = String.Empty; + return true; + } + } +} + diff --git a/src/XMakeTasks/FormatVersion.cs b/src/XMakeTasks/FormatVersion.cs new file mode 100644 index 00000000000..18c4a2e6e5b --- /dev/null +++ b/src/XMakeTasks/FormatVersion.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Formats a version by combining version and revision. + /// + /// + /// Case #1: Input: Version=<undefined> Revision=<don't care> Output: OutputVersion="1.0.0.0" + /// Case #2: Input: Version="1.0.0.*" Revision="5" Output: OutputVersion="1.0.0.5" + /// Case #3: Input: Version="1.0.0.0" Revision=<don't care> Output: OutputVersion="1.0.0.0" + /// + public sealed class FormatVersion : TaskExtension + { + private enum _FormatType { Version, Path } + + private _FormatType _formatType = _FormatType.Version; + private string _outputVersion; + private int _revision; + private string _version; + + private string _specifiedFormatType = null; + + [Output] + public string OutputVersion + { + get { return _outputVersion; } + set { _outputVersion = value; } + } + + public string FormatType + { + get { return _specifiedFormatType; } + set { _specifiedFormatType = value; } + } + + public int Revision + { + get { return _revision; } + set { _revision = value; } + } + + public string Version + { + get { return _version; } + set { _version = value; } + } + + public override bool Execute() + { + if (!ValidateInputs()) + return false; + + if (String.IsNullOrEmpty(Version)) + OutputVersion = "1.0.0.0"; + else if (Version.EndsWith("*", StringComparison.Ordinal)) + OutputVersion = Version.Substring(0, Version.Length - 1) + Revision.ToString("G", CultureInfo.InvariantCulture); + else + OutputVersion = Version; + + if (_formatType == _FormatType.Path) + OutputVersion = OutputVersion.Replace('.', '_'); + return true; + } + + private bool ValidateInputs() + { + if (_specifiedFormatType != null) + { + try + { + _formatType = (_FormatType)Enum.Parse(typeof(_FormatType), _specifiedFormatType, true); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", "FormatType", "FormatVersion"); + return false; + } + } + return true; + } + } +} diff --git a/src/XMakeTasks/FxCopExclusions/Microsoft.Build.Tasks.Suppressions.cs b/src/XMakeTasks/FxCopExclusions/Microsoft.Build.Tasks.Suppressions.cs new file mode 100644 index 00000000000..cfa4ec54dc4 --- /dev/null +++ b/src/XMakeTasks/FxCopExclusions/Microsoft.Build.Tasks.Suppressions.cs @@ -0,0 +1,587 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// FxCop Suppression file +// To Use: +// Add module level suppressions to this file to have them suppressed in the assembly +// + +using System.Diagnostics.CodeAnalysis; + +#if CODE_ANALYSIS +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="0", Scope="module", Target="microsoft.build.tasks.core.dll", Justification="Already shipped several versions with a name like this")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Build.Tasks.NativeMethods.GetCachePath(Microsoft.Build.Tasks.AssemblyCacheFlags,System.Text.StringBuilder,System.Int32@)", Scope="member", Target="Microsoft.Build.Tasks.GlobalAssemblyCache.#GetGacPath()", Justification="We do not use the method result we use one of the out parameters as a check instead. This is becasue the method can have a failed hresult but return a object in the out parameter.")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Build.Tasks.IAssemblyCache.QueryAssemblyInfo(System.UInt32,System.String,Microsoft.Build.Tasks.ASSEMBLY_INFO@)", Scope="member", Target="Microsoft.Build.Tasks.GlobalAssemblyCache.#GetLocationImpl(Microsoft.Build.Shared.AssemblyNameExtension,System.String)", Justification="We do not use the method result we use one of the out parameters as a check instead. This is becasue the method can have a failed hresult but return a object in the out parameter.")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Build.Tasks.NativeMethods.CreateAssemblyCache(Microsoft.Build.Tasks.IAssemblyCache@,System.UInt32)", Scope="member", Target="Microsoft.Build.Tasks.GlobalAssemblyCache.#GetLocationImpl(Microsoft.Build.Shared.AssemblyNameExtension,System.String)", Justification="We do not use the method result we use one of the out parameters as a check instead. This is becasue the method can have a failed hresult but return a object in the out parameter.")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Build.Tasks.NativeMethods.CompareAssemblyIdentity(System.String,System.Boolean,System.String,System.Boolean,System.Boolean@,Microsoft.Build.Tasks.NativeMethods+AssemblyComparisonResult@)", Scope="member", Target="Microsoft.Build.Tasks.ReferenceTable.#ResolveAssemblyNameConflict(Microsoft.Build.Tasks.AssemblyNameReference,Microsoft.Build.Tasks.AssemblyNameReference)", Justification="We do use this")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.RequiresFramework35SP1Assembly", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.XmlTrustUrlParameters", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.DeploymentUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.DeploymentUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.XmlSupportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.XmlDisallowUrlActivation", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.XmlDeploymentUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.SupportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.XmlSupportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateApplicationManifest.SupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.XmlOSSupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.SupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.OSSupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildSettings.SupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildSettings.ComponentsUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildSettings.ApplicationUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateDeploymentManifest.SupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateDeploymentManifest.DeploymentUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateBootstrapper.SupportUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateBootstrapper.ComponentsUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateBootstrapper.ApplicationUrl", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1054:UriParametersShouldNotBeStrings", MessageId="0#", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationIdentity..ctor(System.String,Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyIdentity,Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyIdentity)", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1054:UriParametersShouldNotBeStrings", MessageId="0#", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationIdentity..ctor(System.String,System.String,System.String)", Justification="These are already public properties and parameters which cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.FormatUrl.OutputUrl", Justification="This is the input and output name of the format url task which are all public and cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.FormatUrl.InputUrl", Justification="This is the input and output name of the format url task which are all public and cannot be changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.SignFile.TimestampUrl", Justification="Do not want to change string to URI as this is a public property on a public task")] +[module: SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="3#", Justification="Excluded until IFixedTypeInfo is replaced by ITypeInfo")] +[module: SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.CreateInstance(System.Object,System.Guid&,System.Object&):System.Void", MessageId="1#", Justification="Excluded until IFixedTypeInfo is replaced by ITypeInfo")] +[module: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.CreateInstance(System.Object,System.Guid&,System.Object&):System.Void", MessageId="pUnk", Justification="Excluded until IFixedTypeInfo is replaced by ITypeInfo")] +[module: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetRefTypeInfo(System.IntPtr,Microsoft.Build.Tasks.IFixedTypeInfo&):System.Void", MessageId="hRef", Justification="Excluded until IFixedTypeInfo is replaced by ITypeInfo")] +[module: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.AddResponseFileCommands(Microsoft.Build.Tasks.CommandLineBuilderExtension):System.Void", Justification="The argument parameter is Sources, this is correct as the exception is thrown if there is a problem with the Sources[0].ItemSpec. This parameter is on the class rather than the method.")] +[module: SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", Scope="member", Target="Microsoft.Build.Tasks.ToolTaskExtension.GetBoolParameterWithDefault(System.String,System.Boolean):System.Boolean", MessageId="bool", Justification="These are method that return supposed to return a language specific type. Also the method is public, has shipped and cannot be changed for compatibility reasons.")] +[module: SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", Scope="member", Target="Microsoft.Build.Tasks.ToolTaskExtension.GetIntParameterWithDefault(System.String,System.Int32):System.Int32", MessageId="int", Justification="These are method that return supposed to return a language specific type. Also the method is public, has shipped and cannot be changed for compatibility reasons.")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="regasm", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="vcbuild", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="IDE's", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="v", Justification="Casing is correct")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildSettings.LCID", MessageId="LCID", Justification="Casing is correct")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildSettings.FallbackLCID", MessageId="LCID", Justification="Casing is correct")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ProxyStub.IID", MessageId="IID", Justification="Casing is correct")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ProxyStub.XmlIID", MessageId="IID", Justification="Casing is correct")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="5", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.TypeLibs", MessageId="Libs", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.XmlTypeLibs", MessageId="Libs", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ProxyStub.NumMethods", MessageId="Num", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ProxyStub.XmlNumMethods", MessageId="Num", Justification="Are spelled correctly")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetRefTypeOfImplType(System.Int32,System.IntPtr&):System.Void", MessageId="Impl", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetImplTypeFlags(System.Int32,System.Runtime.InteropServices.ComTypes.IMPLTYPEFLAGS&):System.Void", MessageId="Impl", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetImplTypeFlags(System.Int32,System.Runtime.InteropServices.ComTypes.IMPLTYPEFLAGS&):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetDllEntry(System.Int32,System.Runtime.InteropServices.ComTypes.INVOKEKIND,System.IntPtr,System.IntPtr,System.IntPtr):System.Void", MessageId="Bstr", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetDllEntry(System.Int32,System.Runtime.InteropServices.ComTypes.INVOKEKIND,System.IntPtr,System.IntPtr,System.IntPtr):System.Void", MessageId="pw", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetDllEntry(System.Int32,System.Runtime.InteropServices.ComTypes.INVOKEKIND,System.IntPtr,System.IntPtr,System.IntPtr):System.Void", MessageId="memid", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetDllEntry(System.Int32,System.Runtime.InteropServices.ComTypes.INVOKEKIND,System.IntPtr,System.IntPtr,System.IntPtr):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="Excep", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="w", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="memid", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="Disp", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="Params", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.Invoke(System.Object,System.Int32,System.Int16,System.Runtime.InteropServices.ComTypes.DISPPARAMS&,System.IntPtr,System.IntPtr,System.Int32&):System.Void", MessageId="pu", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseFuncDesc(System.IntPtr):System.Void", MessageId="Func", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseFuncDesc(System.IntPtr):System.Void", MessageId="Desc", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseFuncDesc(System.IntPtr):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetDocumentation(System.Int32,System.String&,System.String&,System.Int32&,System.String&):System.Void", MessageId="dw", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetDocumentation(System.Int32,System.String&,System.String&,System.Int32&,System.String&):System.Void", MessageId="str", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetIDsOfNames(System.String[],System.Int32,System.Int32[]):System.Void", MessageId="Mem", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetIDsOfNames(System.String[],System.Int32,System.Int32[]):System.Void", MessageId="c", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetIDsOfNames(System.String[],System.Int32,System.Int32[]):System.Void", MessageId="rgsz", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetIDsOfNames(System.String[],System.Int32,System.Int32[]):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetIDsOfNames(System.String[],System.Int32,System.Int32[]):System.Void", MessageId="IDs", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetContainingTypeLib(System.Runtime.InteropServices.ComTypes.ITypeLib&,System.Int32&):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetContainingTypeLib(System.Runtime.InteropServices.ComTypes.ITypeLib&,System.Int32&):System.Void", MessageId="TLB", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.AddressOfMember(System.Int32,System.Runtime.InteropServices.ComTypes.INVOKEKIND,System.IntPtr&):System.Void", MessageId="memid", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.AddressOfMember(System.Int32,System.Runtime.InteropServices.ComTypes.INVOKEKIND,System.IntPtr&):System.Void", MessageId="ppv", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseTypeAttr(System.IntPtr):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseTypeAttr(System.IntPtr):System.Void", MessageId="Attr", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.CreateInstance(System.Object,System.Guid&,System.Object&):System.Void", MessageId="Unk", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.CreateInstance(System.Object,System.Guid&,System.Object&):System.Void", MessageId="riid", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.CreateInstance(System.Object,System.Guid&,System.Object&):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.CreateInstance(System.Object,System.Guid&,System.Object&):System.Void", MessageId="ppv", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetFuncDesc(System.Int32,System.IntPtr&):System.Void", MessageId="Func", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetFuncDesc(System.Int32,System.IntPtr&):System.Void", MessageId="Desc", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetNames(System.Int32,System.String[],System.Int32,System.Int32&):System.Void", MessageId="Bstr", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetNames(System.Int32,System.String[],System.Int32,System.Int32&):System.Void", MessageId="c", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetNames(System.Int32,System.String[],System.Int32,System.Int32&):System.Void", MessageId="memid", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetNames(System.Int32,System.String[],System.Int32,System.Int32&):System.Void", MessageId="rg", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetMops(System.Int32,System.String&):System.Void", MessageId="Bstr", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetMops(System.Int32,System.String&):System.Void", MessageId="memid", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetMops(System.Int32,System.String&):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetTypeAttr(System.IntPtr&):System.Void", MessageId="Attr", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetRefTypeInfo(System.IntPtr,Microsoft.Build.Tasks.IFixedTypeInfo&):System.Void", MessageId="h", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseVarDesc(System.IntPtr):System.Void", MessageId="Desc", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.ReleaseVarDesc(System.IntPtr):System.Void", MessageId="p", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.IFixedTypeInfo.GetVarDesc(System.Int32,System.IntPtr&):System.Void", MessageId="Desc", Justification="These are spelled and named in c++ syntax because it is a temporary fixed implementation of the ITypeInfo interface, when the interface is reverted to the original these suppression messages can go away.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildResults.ComponentFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IBuildResults.Messages", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.Manifest.XmlFileReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.Manifest.XmlAssemblyReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.TypeLibs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.XmlTypeLibs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.ComClasses", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.ProxyStubs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.XmlComClasses", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.FileReference.XmlProxyStubs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyManifest.ExternalProxyStubs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyManifest.XmlExternalProxyStubs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification="Our assemblies are delay signed.")] +[module: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope="namespace", Target="Microsoft.Build.Tasks.Hosting", Justification="KCwalina explicitly asked us to move these types into their own namespace since they're not tasks.")] +[module: SuppressMessage("Microsoft.MSInternal", "CA905:SystemAndMicrosoftNamespacesRequireApproval", Scope="namespace", Target="Microsoft.Build.Tasks.Hosting", Justification="Microsoft.Build.Tasks.Hosting is approved.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="type", Target="Microsoft.Build.Tasks.Hosting.ICscHostObject2", MessageId="Csc", Justification="This is the name of the C# compiler and this interface is a wrapper over it. Keeping the name.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject", MessageId="Vbc", Justification="VBC and CSC are spelled correctly.")] +[module: SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", Scope="member", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject.SetRemoveIntegerChecks(System.Boolean):System.Boolean", MessageId="0#", Justification="This parameter is not referring to a type, it's referring to a switch on the VBC compiler.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="type", Target="Microsoft.Build.Tasks.Hosting.ICscHostObject", MessageId="Csc", Justification="VBC and CSC are spelled correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Scope="member", Target="Microsoft.Build.Tasks.Hosting.ICscHostObject.EndInitialization(System.String&,System.Int32&):System.Boolean", MessageId="0#", Justification="Method has multiple return values, so it must use out parameters.")] +[module: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Scope="member", Target="Microsoft.Build.Tasks.Hosting.ICscHostObject.EndInitialization(System.String&,System.Int32&):System.Boolean", MessageId="1#", Justification="Method has multiple return values, so it must use out parameters.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject2", MessageId="Vbc", Justification="Vbc is the correct spelling for the VB compiler.")] +[module: SuppressMessage("Microsoft.MSInternal", "CA905:SystemAndMicrosoftNamespacesRequireApproval", Scope="namespace", Target="Microsoft.Build.Tasks", Justification="Microsoft.Build.Tasks is an approved namespace according to http://ddwww/apps/apiowners/")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.GetFrameworkPath..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.LC.ReferencedAssemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.LC.ReferencedAssemblies", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.LC.Sources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.LC.Sources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignProjectConfiguration.AssignedProjects", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AssignProjectConfiguration.AssignedProjects", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignProjectConfiguration.UnassignedProjects", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AssignProjectConfiguration.UnassignedProjects", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AssignProjectConfiguration..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", Scope="member", Target="Microsoft.Build.Tasks.CreateCSharpManifestResourceName.CreateManifestName(System.String,System.String,System.String,System.String,System.IO.Stream):System.String", MessageId="2#", Justification="Cannot change parameter names because that would break compat with reflection users, theoretically.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.TypeLibFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.TypeLibFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.ResolveRef(System.Reflection.Assembly):System.Object", MessageId="0#", Justification="Cannot change parameter names because that would break compat with reflection users, theoretically.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.Register(System.String,System.String):System.Boolean", MessageId="System.Reflection.Assembly.LoadFrom", Justification="We need to call Assembly.LoadFrom here, it's by design.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.Assemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.Assemblies", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.ReportEvent(System.Runtime.InteropServices.ExporterEventKind,System.Int32,System.String):System.Void", MessageId="0#", Justification="Cannot change parameter names because that would break compat with reflection users, theoretically.")] +[module: SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.ReportEvent(System.Runtime.InteropServices.ExporterEventKind,System.Int32,System.String):System.Void", MessageId="1#", Justification="Cannot change parameter names because that would break compat with reflection users, theoretically.")] +[module: SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", Scope="member", Target="Microsoft.Build.Tasks.RegisterAssembly.ReportEvent(System.Runtime.InteropServices.ExporterEventKind,System.Int32,System.String):System.Void", MessageId="2#", Justification="Cannot change parameter names because that would break compat with reflection users, theoretically.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateProperty.ValueSetByTask", Justification="this is the msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateProperty.Value", Justification="this is the msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateProperty.Value", Justification="this is the msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateItem.Include", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateItem.Include", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.CreateItem..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateItem.AdditionalMetadata", Justification="msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateItem.AdditionalMetadata", Justification="msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateItem.Exclude", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateItem.Exclude", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.GetAssemblyIdentity.AssemblyFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.GetAssemblyIdentity.AssemblyFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.GetAssemblyIdentity.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.GetAssemblyIdentity.Assemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.GetAssemblyIdentity.Assemblies", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ParseState..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.ResolvedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.ResolvedFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.TypeLibNames", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.TypeLibNames", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.Execute():System.Boolean", Justification="In some situations the underlying wrapper generation code can throw a SystemException and there's nothing else we can do to prevent MSBuild from crashing.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.TypeLibFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.TypeLibFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.ResolvedAssemblyReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.ResolvedAssemblyReferences", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.ResolvedModules", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.ResolvedModules", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReference.VerifyReferenceMetadataForNameItem(Microsoft.Build.Framework.ITaskItem,System.String&):System.Boolean", MessageId="System.Guid", Justification="The Guid is created and never used on purpose, to check if the passed-in guid is valid.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.DependentAssembly..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput.ResolvedOutputPaths", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput.ResolvedOutputPaths", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput.UnresolvedProjectReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput.UnresolvedProjectReferences", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput.PreresolvedProjectOutputs", MessageId="Preresolved", Justification="Preresolved is spelled correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.SearchPaths", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.SearchPaths", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.AssemblyFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.AssemblyFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.Assemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.Assemblies", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.ResolvedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.RelatedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.ResolvedDependencyFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.ScatterFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.SatelliteFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.SerializationAssemblyFiles")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.TargetFrameworkDirectories", Justification="This is the MSBuild task authoring model for Whidbey.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.TargetFrameworkDirectories", Justification="This is the MSBuild task authoring model for Whidbey.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.SuggestedRedirects", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.AllowedAssemblyExtensions", Justification="This is an appropriate design in this case.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.AllowedAssemblyExtensions", Justification="This is an appropriate design in this case.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.InstalledAssemblyTables", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.InstalledAssemblyTables", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.CandidateAssemblyFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.CandidateAssemblyFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.FilesWritten", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.FilesWritten", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.set_FilesWritten(Microsoft.Build.Framework.ITaskItem[]):System.Void", MessageId="value", Justification="This is an output only property so we have no use for the implicit value input parameter. We cannot however remove the property set because this would be a breaking change. Excluding instead.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.CopyLocalFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.SGen.SerializationAssembly", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.SGen.SerializationAssembly", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.SGen.References", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.SGen.References", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="member", Target="Microsoft.Build.Tasks.AspNetCompiler.MetabasePath", MessageId="Metabase", Justification="Metabase is spelled correctly - it's an IIS term")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AssemblyInformation..ctor(System.String)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignTargetPath.Files", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AssignTargetPath.Files", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignTargetPath.AssignedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AssignTargetPath..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveComReferenceCache..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateManifestResourceName.ManifestResourceNames", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.CreateManifestResourceName.Execute(Microsoft.Build.Tasks.CreateFileStream):System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.CreateManifestResourceName..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateManifestResourceName.ResourceFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateManifestResourceName.ResourceFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.MSBuild..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.Targets", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.Targets", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.TargetOutputs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.ExecuteTargets(Microsoft.Build.Framework.ITaskItem[],System.Collections.Hashtable,System.Collections.ArrayList,System.Boolean,System.Boolean,Microsoft.Build.Framework.IBuildEngine2,Microsoft.Build.Utilities.TaskLoggingHelper,System.Collections.ArrayList,System.Boolean,System.Boolean,System.String):System.Boolean", Justification="Refactoring would decrease readability")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.Projects", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.Projects", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.Properties", Justification="msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.MSBuild.Properties", Justification="msbuild task authoring model")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.BindingRedirect..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Scope="type", Target="Microsoft.Build.Tasks.ExtractedClassName", Justification="This is an internally used class and it doesn't need an implementation of Equals.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AssemblyFoldersExResolver..ctor(System.String,Microsoft.Build.Tasks.GetAssemblyName,Microsoft.Build.Shared.FileExists,Microsoft.Build.Tasks.GetRegistrySubKeyNames,Microsoft.Build.Tasks.GetRegistrySubKeyDefaultValue)", Justification="Explicit initialization improves code readability.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.AxReference.GenerateWrapper(Microsoft.Build.Tasks.ComReferenceWrapperInfo&):System.Boolean", MessageId="System.Reflection.Assembly.LoadFrom", Justification="We need to call Assembly.LoadFrom here, it's by design.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.PiaReference.FindExistingWrapper(Microsoft.Build.Tasks.ComReferenceWrapperInfo&,System.DateTime):System.Boolean", MessageId="System.Reflection.Assembly.LoadFrom", Justification="We need to call Assembly.LoadFrom here, it's by design.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.GetFrameworkSdkPath..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles+LineNumberStreamReader..ctor(System.IO.Stream)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles+LineNumberStreamReader..ctor(System.String,System.Text.Encoding,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", Scope="member", Target="Microsoft.Build.Tasks.CreateVisualBasicManifestResourceName.CreateManifestName(System.String,System.String,System.String,System.String,System.IO.Stream):System.String", MessageId="2#", Justification="Cannot change parameter names because that would break compat with reflection users, theoretically.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.ResolveKeySource.ResolveAssemblyKey():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope="member", Target="Microsoft.Build.Tasks.ResolveKeySource.ResolveAssemblyKey():System.Boolean", Justification="From comments: We use ToLower(invariant) because this is what the native equivalent of this function (Create new key, or VC++ import-er). use as well and we want to keep the hash (and key container name the same) otherwise user could be prompt for a password twice.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveKeySource..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.DisposableBase..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.ResolvedOutputPaths", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.ResolvedOutputPaths", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput..cctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.vcProjectEngineDll", Justification="It's not necessary to use a SafeHandle here - we load the dll just once and it's released automatically on process shutdown by the system.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.ProjectReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.ProjectReferences", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.ResolveUsingVCEngineObjectModel(Microsoft.Build.Framework.ITaskItem,System.String,System.String&,System.String&):System.Boolean", Justification="Refactoring would cause increased complexity rather than decrease it")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput.ResolveUsingVCEngineObjectModel(Microsoft.Build.Framework.ITaskItem,System.String,System.String&,System.String&):System.Boolean", Justification="The exception is rethrown unless it's a known type.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveVCProjectOutput..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles.ReadTextResources(System.String):System.Void", Justification="Further reduction would degrade readability.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles.ProcessFile(System.String,System.String):System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles.ResolveAssembly(System.Object,System.ResolveEventArgs):System.Reflection.Assembly", MessageId="System.Reflection.Assembly.LoadFile", Justification="This is by design.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles.ResolveAssembly(System.Object,System.ResolveEventArgs):System.Reflection.Assembly", MessageId="System.Reflection.Assembly.LoadFrom", Justification="This is intentional.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ProcessResourceFiles..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.InstalledAssemblies..ctor(Microsoft.Build.Tasks.RedistList)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.Projects", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.Projects", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.Execute():System.Boolean", Justification="This exception is rethrown if it's not a known type.")] +[module: SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.set_UserEnvironment(System.Boolean):System.Void", MessageId="value", Justification="This method only exists for backward compatibility and will be removed in a future release.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.VCBuild..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.AdditionalLibPaths", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.AdditionalLibPaths", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.VCBuild.get_EnvironmentOverride():System.Collections.Specialized.StringDictionary", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ComReference..cctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AL.SourceModules", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AL.SourceModules", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AL.EmbedResources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AL.EmbedResources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AL.LinkResources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AL.LinkResources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AL.ResponseFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AL.ResponseFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.WriteLinesToFile.Lines", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.WriteLinesToFile.Lines", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.WriteLinesToFile..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.WriteLinesToFile.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable", Scope="type", Target="Microsoft.Build.Tasks.NativeMethods+CRYPTOAPI_BLOB", Justification="Allocation/deallocation of these structs is handled elsewhere.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope="member", Target="Microsoft.Build.Tasks.NativeMethods+CRYPTOAPI_BLOB.pbData", Justification="We will do this in Visual Studio 2008. In Whidbey we must still support the Everett runtime.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.RemoveDuplicates.Inputs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.RemoveDuplicates.Inputs", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.RemoveDuplicates.Filtered", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.RemoveDuplicates.Filtered", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.RemoveDuplicates..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="type", Target="Microsoft.Build.Tasks.Vbc", MessageId="Vbc", Justification="These are named after the compiler tool, so they are spelled correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Vbc.InitializeHostCompiler(Microsoft.Build.Tasks.Hosting.IVbcHostObject):System.Boolean")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.Vbc.InitializeHostCompiler(Microsoft.Build.Tasks.Hosting.IVbcHostObject):System.Boolean")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Vbc..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Vbc.Imports", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Vbc.Imports", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Exec.Outputs", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Exec.Outputs", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Exec..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Scope="member", Target="Microsoft.Build.Tasks.Exec.WorkingDirectory", Justification="GetWorkingDirectory() is an overridden protected method and is reasonably named. The WorkingDirectory property is public and also reasonably named and has different users. In his particular scenario I think these names are the best and won't cause confusion.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.AssemblyFoldersEx.FindDirectories(Microsoft.Win32.RegistryKey,System.String,System.String,System.String,System.String,System.String,Microsoft.Build.Tasks.GetRegistrySubKeyNames,Microsoft.Build.Tasks.GetRegistrySubKeyDefaultValue):System.Void", Justification="Further reduction would degrade readability.")] +[module: SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", Scope="type", Target="Microsoft.Build.Tasks.Delete", Justification="We want to use Delete for the name of the task, as it matches the DOS command and is what users will expect.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Delete.Files", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Delete.Files", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Delete.DeletedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Delete.DeletedFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Delete..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.Delete.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.RemoveDir.RemovedDirectories", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.RemoveDir.RemovedDirectories", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.RemoveDir.RemoveReadOnlyAttributeRecursively(System.IO.DirectoryInfo):System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.RemoveDir.Directories", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.RemoveDir.Directories", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.RemoveDir.RemoveDirectory(Microsoft.Build.Framework.ITaskItem,System.Boolean,System.Boolean&):System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveProjectBase..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Scope="member", Target="Microsoft.Build.Tasks.ResolveProjectBase.VerifyReferenceAttributes(Microsoft.Build.Framework.ITaskItem,System.String&):System.Boolean", MessageId="System.Guid", Justification="We're creating a Guid object from a string to confirm that the string is a valid GUID. The Guid structure provides no other way of checking this e.g. a TryParse() method. We don't actually need the Guid object for anything.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveProjectBase.ProjectReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveProjectBase.ProjectReferences", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateTemporaryVCProject.ReferenceGuids", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateTemporaryVCProject.ReferenceGuids", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CreateTemporaryVCProject.ReferenceAssemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CreateTemporaryVCProject.ReferenceAssemblies", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.CreateTemporaryVCProject.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.LinkResources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.LinkResources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.AddModules", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.AddModules", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.Sources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.Sources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.set_TargetType(System.String):System.Void", Justification="This string is not being normalized but rather formatted to be used as a command line switch, where it is expected in lowercase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.Resources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.Resources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.ResponseFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.ResponseFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.AdditionalLibPaths", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.AdditionalLibPaths", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.References", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ManagedCompiler.References", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.SystemState..ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.SystemState..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolutionSearchLocation..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.ReadStateFile():System.Void", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.FilesWritten", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.FilesWritten", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.Sources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.Sources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.NeedSeparateAppDomain():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.References", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.References", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.OutputResources", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.GenerateResource.OutputResources", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture.AssignedFilesWithNoCulture", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture.Files", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture.Files", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture.AssignedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture.AssignedFilesWithCulture", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture.CultureNeutralAssignedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AssignCulture..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Touch.Files", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Touch.Files", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Touch.TouchedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Touch.TouchedFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Touch..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Copy.SourceFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Copy.SourceFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Copy.DestinationFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Copy.DestinationFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Copy.CopiedFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.Copy.Execute(Microsoft.Build.Tasks.CopyFile):System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Copy..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AxTlbBaseReference..ctor(Microsoft.Build.Utilities.TaskLoggingHelper,Microsoft.Build.Tasks.IComReferenceResolver,Microsoft.Build.Tasks.ComReferenceInfo,System.String,System.String,System.Boolean,System.String,System.String)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.AxTlbBaseReference.IsWrapperUpToDate(Microsoft.Build.Tasks.ComReferenceWrapperInfo,System.DateTime):System.Boolean", MessageId="System.Reflection.Assembly.LoadFrom", Justification="We need to call Assembly.LoadFrom here, it's by design.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.UnregisterAssembly.TypeLibFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.UnregisterAssembly.TypeLibFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.UnregisterAssembly.Unregister(System.String,System.String):System.Boolean", MessageId="System.Reflection.Assembly.LoadFrom", Justification="We need to call Assembly.LoadFrom here, it's by design.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.UnregisterAssembly.Assemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.UnregisterAssembly.Assemblies", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.UnregisterAssembly..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.StateFileBase.SerializeCache(System.String,Microsoft.Build.Utilities.TaskLoggingHelper):System.Void", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.StateFileBase.DeleteFile(System.String,Microsoft.Build.Utilities.TaskLoggingHelper):System.Void", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.StateFileBase.DeserializeCache(System.String,Microsoft.Build.Utilities.TaskLoggingHelper,System.Type):Microsoft.Build.Tasks.StateFileBase", Justification="We log the exception as a warning, but a problem reading the cache file should never cause building to fail, so we continue.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.SystemState+FileState..ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.SystemState+FileState..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope="type", Target="Microsoft.Build.Tasks.Csc", MessageId="Csc", Justification="These are named after the compiler tool, so they are spelled correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Csc..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Csc.InitializeHostCompiler(Microsoft.Build.Tasks.Hosting.ICscHostObject):System.Boolean")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.Csc.InitializeHostCompiler(Microsoft.Build.Tasks.Hosting.ICscHostObject):System.Boolean")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.Reference..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.Files", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.Files", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.OutOfPath", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.OutOfPath", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.InPath", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.InPath", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.FindUnderPath.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", Scope="type", Target="Microsoft.Build.Tasks.Error", Justification="This maps to an XML element name, so we need to use the name Error.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.DependencyFile..ctor(System.String)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Scope="member", Target="Microsoft.Build.Tasks.AssemblyNamesTypeResolutionService.GetAssemblyByPath(System.String,System.Boolean):System.Reflection.Assembly", MessageId="System.Reflection.Assembly.LoadFrom", Justification="We need to call Assembly.LoadFrom in AssemblyNamesTypeResolutionService.GetAssemblyByPath to locate the assembly on disk.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.ReadLinesFromFile.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ReadLinesFromFile..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ReadLinesFromFile.Lines", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ReadLinesFromFile.Lines", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable", Scope="type", Target="Microsoft.Build.Tasks.ASSEMBLYMETADATA", Justification="Allocation/deallocation of these structs is handled elsewhere.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope="member", Target="Microsoft.Build.Tasks.ASSEMBLYMETADATA.rpLocale", Justification="These pointers are incidental members of structures that aren't used.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope="member", Target="Microsoft.Build.Tasks.ASSEMBLYMETADATA.rOses", Justification="These pointers are incidental members of structures that aren't used.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope="member", Target="Microsoft.Build.Tasks.ASSEMBLYMETADATA.rpProcessors", Justification="These pointers are incidental members of structures that aren't used.")] +[module: SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources", Scope="member", Target="Microsoft.Build.Tasks.NativeMethods.InvalidIntPtr", Justification="We will do this in Visual Studio 2008. In Whidbey we must still support the Everett runtime.")] +[module: SuppressMessage("Microsoft.Interoperability", "CA1400:PInvokeEntryPointsShouldExist", Scope="member", Target="Microsoft.Build.Tasks.NativeMethods.CompareAssemblyIdentity(System.String,System.Boolean,System.String,System.Boolean,System.Boolean&,Microsoft.Build.Tasks.NativeMethods+AssemblyComparisonResult&):System.Int32", Justification="This method CompareAssemblyIdentity does exist, in fusion.dll.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CallTarget.Targets", Justification="msbuild task model necessitates this.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.CallTarget.Targets", Justification="msbuild task model necessitates this.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.CallTarget..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.CallTarget.TargetOutputs", Justification="msbuild task model necessitates this.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.MakeDir.Directories", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.MakeDir.Directories", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.MakeDir.DirectoriesCreated", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope="member", Target="Microsoft.Build.Tasks.MakeDir.Execute():System.Boolean", Justification="A helper method rethrows non-IO exceptions, but FXCop cannot detect that.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AppConfigException..ctor(System.String,System.String,System.Int32,System.Int32,System.Exception)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedLooseEtcFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedLooseEtcFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainingReferenceFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainingReferenceFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.NativeReferences", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.NativeReferences", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.AdditionalSearchPaths", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.AdditionalSearchPaths", Justification="All MSBuild tasks accept input and output item arrays this way. This is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedComComponents", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedComComponents", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedPrerequisiteAssemblies", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedPrerequisiteAssemblies", Justification="All MSBuild tasks accept input and output item arrays this way. This is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedLooseTlbFiles", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedLooseTlbFiles", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedTypeLibraries", Justification="The MSBuild task model requires that we have read/write properties that are collections/arrays. Arrays are the only way to have strongly typed collections that build on Everett, and properties are used to map to XML attributes in the file format.")] +[module: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ContainedTypeLibraries", Justification="All MSBuild tasks accept and output item arrays this way, this is by design. Tasks are only called by the MSBuild engine, which will access them correctly.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.ResolveNativeReference.ExtractFromManifest(Microsoft.Build.Framework.ITaskItem,System.String,System.Collections.Hashtable,System.Collections.Hashtable,System.Collections.Hashtable,System.Collections.Hashtable,System.Collections.Hashtable,System.Collections.Hashtable):System.Boolean", Justification="Complexity is only 26 (cutoff is 25), and further reductions would degrade readability.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.TlbReference..ctor(Microsoft.Build.Utilities.TaskLoggingHelper,Microsoft.Build.Tasks.IComReferenceResolver,Microsoft.Build.Tasks.ComReferenceInfo,System.String,System.String,System.Boolean,System.Boolean,System.String,System.String,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.AssemblyRegistrationCache..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.StreamMappedString..ctor(System.IO.Stream,System.Boolean,System.Int32)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.CSharpTokenEnumerator.FindNextToken():System.Boolean", Justification="Reduced complexity from 170+ to less than 40 - further reductions would make the code less readable.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.CSharpTokenEnumerator..ctor(System.IO.Stream,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.TokenEnumerator..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.VisualBasicTokenEnumerator.FindNextToken():System.Boolean", Justification="Reduced complexity from 170+ to less than 40 - further reductions would make the code less readable.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.VisualBasicTokenEnumerator..ctor(System.IO.Stream,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.VisualBasicTokenizer..ctor(System.IO.Stream,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.Token..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.LanguageParser.CSharpTokenizer..ctor(System.IO.Stream,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.FileMatcher+Result..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Scope="member", Target="NativeMethodsShared.GetCurrentDirectory(System.Int32,System.Text.StringBuilder):System.Int32", Justification="For performance reasons use unmanagedWin32 API. The managed one does extra security checks")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension..ctor(System.Reflection.AssemblyName)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension..ctor(System.String)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension..ctor(System.String,System.Boolean)", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Shared.AssemblyNameExtension..ctor()", Justification="We've chosen to retain these initializations because they improve the readability of the codebase.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.IsItemSpecModifier(System.String):System.Boolean", Justification="Further reduction would degrade readability.")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.GetItemSpecModifier(System.String,System.String,System.String,System.Collections.Hashtable&):System.String", Justification="was excluded before.")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="Metabase", Justification="These are correct spellings")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="bootstrapper", Justification="These are correct spellings")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="redist", Justification="These are correct spellings")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="aximp", Justification="These are correct spellings")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="tlbimp", Justification="These are correct spellings")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", MessageId="Resx", Justification="These are correct spellings")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateApplicationManifest.#ErrorReportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.GenerateDeploymentManifest.#ErrorReportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Naming","CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Scope="type", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IProductBuilderCollection", Justification="These are public properties on public interfaces, these cannot have their name changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Naming","CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Scope="type", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.IProductCollection", Justification="These are public properties on public interfaces, these cannot have their name changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.#ErrorReportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1008:EnumsShouldHaveZeroValue", Scope="type", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyIdentity+FullNameFlags", Justification="These are public properties on public tasks, these cannot have their values changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1034:NestedTypesShouldNotBeVisible", Scope="type", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyIdentity+FullNameFlags", Justification="These are public properties on public class, these cannot have their location changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.#XmlErrorReportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Naming","CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId="ClsId", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ComClass.#ClsId", Justification="These are public properties on public class, these cannot have their name changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Naming","CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId="ClsId", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ComClass.#XmlClsId", Justification="These are public properties on public class, these cannot have their name changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.#ErrorReportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.#XmlErrorReportUrl", Justification="These are public properties on public tasks, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Design","CA1059:MembersShouldNotExposeCertainConcreteTypes", MessageId="System.Xml.XmlNode", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.SecurityUtilities.#XmlToPermissionSet(System.Xml.XmlElement)", Justification="These are public functions on public classes, these cannot have their parameter changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Naming","CA1721:PropertyNamesShouldNotMatchGetMethods", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.AssemblyIdentity.#Type", Justification="hese are public properties on public class, these cannot have their type changed due to backwards compatibility")] +[module: SuppressMessage("Microsoft.Performance","CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.#XmlFileAssociations", Justification="Released API")] +[module: SuppressMessage("Microsoft.Performance","CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.#XmlCompatibleFrameworks", Justification="Released API")] +[module: SuppressMessage("Microsoft.Globalization","CA1307:SpecifyStringComparison", MessageId="System.String.CompareTo(System.String)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.Manifest+ReferenceComparer.#Compare(System.Object,System.Object)", Justification="this is an overloaded Compare method.")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Build.Tasks.Deployment.ManifestUtilities.NativeMethods.EnumResourceNames(System.IntPtr,System.IntPtr,Microsoft.Build.Tasks.Deployment.ManifestUtilities.NativeMethods+EnumResNameProc,System.IntPtr)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.EmbeddedManifestReader.#.ctor(System.String)")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ManifestReader.#Deserialize(System.String)")] +[module: SuppressMessage("Microsoft.Usage","CA2208:InstantiateArgumentExceptionsCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.TrustInfo.#set_PermissionSet(System.Security.PermissionSet)")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Globalization.CultureInfo", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.Util.#IsValidCulture(System.String)")] +[module: SuppressMessage("Microsoft.Usage","CA1816:CallGCSuppressFinalizeCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.MetadataReader.#System.IDisposable.Dispose()")] +[module: SuppressMessage("Microsoft.Usage","CA1816:CallGCSuppressFinalizeCorrectly", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.MetadataReader.#System.IDisposable.Dispose()")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.IO.StreamReader", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.SecurityUtilities.#GetXmlElement(System.String,Microsoft.Build.Utilities.FrameworkName)")] +[module: SuppressMessage("Microsoft.Naming","CA1721:PropertyNamesShouldNotMatchGetMethods", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.OutputMessage.#Type")] +[module: SuppressMessage("Microsoft.Globalization","CA1307:SpecifyStringComparison", MessageId="System.String.CompareTo(System.String)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.BootstrapperBuilder+GraphNodeNameComparer.#Compare(System.Object,System.Object)")] +[module: SuppressMessage("Microsoft.Globalization","CA1307:SpecifyStringComparison", MessageId="System.String.CompareTo(System.String)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.BootstrapperBuilder.#GetOutputFolders(System.String[],System.String,System.String,Microsoft.Build.Tasks.Deployment.Bootstrapper.ComponentsLocation)")] +[module: SuppressMessage("Microsoft.Naming", "CA1718:AvoidLanguageSpecificTypeNamesInParameters", Scope = "member", Target = "Microsoft.Build.Tasks.Hosting.IVbcHostObject.SetRemoveIntegerChecks(System.Boolean):System.Boolean", MessageId = "0#")] +[module: SuppressMessage("Microsoft.Naming", "CA1720:AvoidTypeNamesInParameters", Scope = "member", Target = "Microsoft.Build.Tasks.Hosting.IVbcHostObject.SetRemoveIntegerChecks(System.Boolean):System.Boolean", MessageId = "0#")] +[module: SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Scope = "member", Target = "Microsoft.Build.Tasks.GetFrameworkSdkPath.set_Path(System.String):System.Void", MessageId = "value")] +[module: SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Scope = "member", Target = "Microsoft.Build.Tasks.ProcessResourceFiles.ReadResources(System.String,System.Boolean):System.Void")] +[module: SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Scope = "member", Target = "Microsoft.Build.Tasks.ProcessResourceFiles.WriteResources(System.String):System.Void")] +[module: SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Scope = "member", Target = "Microsoft.Build.Tasks.ProcessResourceFiles.WriteTextResources(System.String):System.Void")] +[module: SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Scope = "member", Target = "Microsoft.Build.Tasks.Exec.CreateTemporaryBatchFile():System.Void")] +[module: SuppressMessage("Microsoft.Performance", "CA1807:AvoidUnnecessaryStringCreation", Scope = "member", Target = "Microsoft.Build.Tasks.ManagedCompiler.set_TargetType(System.String):System.Void", MessageId = "value")] +[module: SuppressMessage("Microsoft.Performance", "CA1807:AvoidUnnecessaryStringCreation", Scope = "member", Target = "Microsoft.Build.Tasks.ManagedCompiler.set_TargetType(System.String):System.Void", MessageId = "stack2")] +[module: SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", Scope = "member", Target = "Microsoft.Build.Tasks.ManagedCompiler.set_TargetType(System.String):System.Void")] +[module: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Build.Shared.FileUtilities.GetItemSpecModifier(System.String,System.String,System.String,System.Collections.Generic.Dictionary`2&):System.String")] +[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope = "resource", Target = "Microsoft.Build.Tasks.Strings.resources", MessageId = "progid")] +[module: SuppressMessage("Microsoft.Performance","CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Xaml.CommandLineToolSwitch.#TaskItemArray")] +[module: SuppressMessage("Microsoft.Naming","CA1721:PropertyNamesShouldNotMatchGetMethods", Scope="member", Target="Microsoft.Build.Tasks.Xaml.CommandLineToolSwitch.#Type")] +[module: SuppressMessage("Microsoft.Globalization","CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId="4", Scope="member", Target="Microsoft.Build.Tasks.Xaml.XamlDataDrivenToolTask+XamlTaskNativeMethods.#SearchPath(System.String,System.String,System.String,System.Int32,System.Text.StringBuilder,System.IntPtr&)")] +[module: SuppressMessage("Microsoft.Performance","CA1819:PropertiesShouldNotReturnArrays", Scope="member", Target="Microsoft.Build.Tasks.Xaml.CommandLineToolSwitch.#StringList")] +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.RequiresFramework35SP1Assembly.#ErrorReportUrl")] +[module: SuppressMessage("Microsoft.Performance","CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Utilities.CanonicalError+Parts.#.ctor()", Justification="clearer this way")] +[module: SuppressMessage("Microsoft.Performance","CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Runtime.Hosting.StrongNameHelpers.#.cctor()", Justification="we don't own this code")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Runtime.Hosting.IClrStrongNameUsingIntPtr.StrongNameFreeBuffer(System.IntPtr)", Scope="member", Target="Microsoft.Runtime.Hosting.StrongNameHelpers.#StrongNameFreeBuffer(System.IntPtr)", Justification="we don't own this code")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="Assemblyfolders", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", Justification="it is correctly spelled")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Vbc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject3", Justification="it is correctly spelled")] +[module: SuppressMessage("Microsoft.Usage","CA1801:ReviewUnusedParameters", MessageId="getRuntimeVersion", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#SetTargetedRuntimeVersion(Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="used by tests")] +[module: SuppressMessage("Microsoft.Maintainability","CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#Execute(Microsoft.Build.Shared.FileExists,Microsoft.Build.Shared.DirectoryExists,Microsoft.Build.Tasks.GetDirectories,Microsoft.Build.Tasks.GetAssemblyName,Microsoft.Build.Tasks.GetAssemblyMetadata,Microsoft.Build.Tasks.GetRegistrySubKeyNames,Microsoft.Build.Tasks.GetRegistrySubKeyDefaultValue,Microsoft.Build.Tasks.GetLastWriteTime,Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="Not going to rewrite it now")] +[module: SuppressMessage("Microsoft.Maintainability","CA1506:AvoidExcessiveClassCoupling", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#Execute(Microsoft.Build.Shared.FileExists,Microsoft.Build.Shared.DirectoryExists,Microsoft.Build.Tasks.GetDirectories,Microsoft.Build.Tasks.GetAssemblyName,Microsoft.Build.Tasks.GetAssemblyMetadata,Microsoft.Build.Tasks.GetRegistrySubKeyNames,Microsoft.Build.Tasks.GetRegistrySubKeyDefaultValue,Microsoft.Build.Tasks.GetLastWriteTime,Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="Not going to rewrite it now")] +[module: SuppressMessage("Microsoft.Maintainability","CA1500:VariableNamesShouldNotMatchFieldNames", MessageId="getAssemblyRuntimeVersion", Scope="member", Target="Microsoft.Build.Tasks.SystemState.#CacheDelegate(Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="clearer")] +[module: SuppressMessage("Microsoft.Globalization","CA1309:UseOrdinalStringComparison", MessageId="System.Collections.SortedList.#ctor(System.Collections.IComparer,System.Int32)", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#VerifyResourceNames(System.Collections.Generic.Dictionary`2,System.CodeDom.Compiler.CodeDomProvider,System.Collections.ArrayList,System.Collections.Hashtable&)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Globalization","CA1309:UseOrdinalStringComparison", MessageId="System.Collections.Hashtable.#ctor(System.Int32,System.Collections.IEqualityComparer)", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#VerifyResourceNames(System.Collections.Generic.Dictionary`2,System.CodeDom.Compiler.CodeDomProvider,System.Collections.ArrayList,System.Collections.Hashtable&)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Maintainability","CA1506:AvoidExcessiveClassCoupling", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#InternalCreate(System.Collections.Generic.Dictionary`2,System.String,System.String,System.String,System.CodeDom.Compiler.CodeDomProvider,System.Boolean,System.String[]&)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Usage","CA1801:ReviewUnusedParameters", MessageId="supportsTryCatch", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#EmitBasicClassMembers(System.CodeDom.CodeTypeDeclaration,System.String,System.String,System.String,System.Boolean,System.Boolean,System.Boolean)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Maintainability","CA1506:AvoidExcessiveClassCoupling", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#EmitBasicClassMembers(System.CodeDom.CodeTypeDeclaration,System.String,System.String,System.String,System.Boolean,System.Boolean,System.Boolean)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Globalization","CA1309:UseOrdinalStringComparison", MessageId="System.Collections.Generic.Dictionary`2.#ctor(System.Collections.Generic.IEqualityComparer`1)", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#Create(System.String,System.String,System.String,System.String,System.CodeDom.Compiler.CodeDomProvider,System.Boolean,System.String[]&)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Globalization","CA1309:UseOrdinalStringComparison", MessageId="System.Collections.Generic.Dictionary`2.#ctor(System.Collections.Generic.IEqualityComparer`1)", Scope="member", Target="Microsoft.Build.Tasks.StronglyTypedResourceBuilder.#Create(System.Collections.IDictionary,System.String,System.String,System.String,System.CodeDom.Compiler.CodeDomProvider,System.Boolean,System.String[]&)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Performance","CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.SRDescriptionAttribute.#.ctor(System.String)", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Performance","CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Tasks.SR.#.cctor()", Justification="Inherited code don't want to change it")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="resx", Scope="resource", Target="System.Design.resources", Justification="This is correct")] + +[module: SuppressMessage("Microsoft.Design","CA1056:UriPropertiesShouldNotBeStrings", Scope="member", Target="Microsoft.Build.Tasks.RequiresFramework35SP1Assembly.#ErrorReportUrl")] +[module: SuppressMessage("Microsoft.Performance","CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Build.Utilities.CanonicalError+Parts.#.ctor()", Justification="clearer this way")] +[module: SuppressMessage("Microsoft.Performance","CA1805:DoNotInitializeUnnecessarily", Scope="member", Target="Microsoft.Runtime.Hosting.StrongNameHelpers.#.cctor()", Justification="we don't own this code")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="Microsoft.Runtime.Hosting.IClrStrongNameUsingIntPtr.StrongNameFreeBuffer(System.IntPtr)", Scope="member", Target="Microsoft.Runtime.Hosting.StrongNameHelpers.#StrongNameFreeBuffer(System.IntPtr)", Justification="we don't own this code")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="Assemblyfolders", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", Justification="it is correctly spelled")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Vbc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject3", Justification="it is correctly spelled")] +[module: SuppressMessage("Microsoft.Usage","CA1801:ReviewUnusedParameters", MessageId="getRuntimeVersion", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#SetTargetedRuntimeVersion(Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="used by tests")] +[module: SuppressMessage("Microsoft.Maintainability","CA1502:AvoidExcessiveComplexity", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#Execute(Microsoft.Build.Shared.FileExists,Microsoft.Build.Shared.DirectoryExists,Microsoft.Build.Tasks.GetDirectories,Microsoft.Build.Tasks.GetAssemblyName,Microsoft.Build.Tasks.GetAssemblyMetadata,Microsoft.Build.Tasks.GetRegistrySubKeyNames,Microsoft.Build.Tasks.GetRegistrySubKeyDefaultValue,Microsoft.Build.Tasks.GetLastWriteTime,Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="Not going to rewrite it now")] +[module: SuppressMessage("Microsoft.Maintainability","CA1506:AvoidExcessiveClassCoupling", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#Execute(Microsoft.Build.Shared.FileExists,Microsoft.Build.Shared.DirectoryExists,Microsoft.Build.Tasks.GetDirectories,Microsoft.Build.Tasks.GetAssemblyName,Microsoft.Build.Tasks.GetAssemblyMetadata,Microsoft.Build.Tasks.GetRegistrySubKeyNames,Microsoft.Build.Tasks.GetRegistrySubKeyDefaultValue,Microsoft.Build.Tasks.GetLastWriteTime,Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="Not going to rewrite it now")] +[module: SuppressMessage("Microsoft.Maintainability","CA1500:VariableNamesShouldNotMatchFieldNames", MessageId="getAssemblyRuntimeVersion", Scope="member", Target="Microsoft.Build.Tasks.SystemState.#CacheDelegate(Microsoft.Build.Tasks.GetAssemblyRuntimeVersion)", Justification="clearer")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="sn", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", Justification="sn.exe is the name of a program")] + +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="Microsoft.Build.Shared.FileUtilities.#NormalizePath(System.String,System.String)", Justification="This is a shared class that this assembly doesn't use all the methods in.")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="foo", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", Justification="Used as an example")] +[module: SuppressMessage("Microsoft.Globalization","CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId="1", Scope="member", Target="System.Deployment.Internal.CodeSigning.Win32.#CertTimestampAuthenticodeLicense(System.Deployment.Internal.CodeSigning.Win32+CRYPT_DATA_BLOB&,System.String,System.Deployment.Internal.CodeSigning.Win32+CRYPT_DATA_BLOB&)", Justification="Not code owned by us")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Deployment.Internal.CodeSigning.Win32.CertFreeAuthenticodeTimestamperInfo(System.Deployment.Internal.CodeSigning.Win32+AXL_TIMESTAMPER_INFO@)", Scope="member", Target="System.Deployment.Internal.CodeSigning.SignedCmiManifest.#VerifyLicense(System.Deployment.Internal.CodeSigning.CmiManifestVerifyFlags,System.Boolean)", Justification="Not code owned by us")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Deployment.Internal.CodeSigning.Win32.CertFreeAuthenticodeSignerInfo(System.Deployment.Internal.CodeSigning.Win32+AXL_SIGNER_INFO@)", Scope="member", Target="System.Deployment.Internal.CodeSigning.SignedCmiManifest.#VerifyLicense(System.Deployment.Internal.CodeSigning.CmiManifestVerifyFlags,System.Boolean)", Justification="Not code owned by us")] +[module: SuppressMessage("Microsoft.Performance","CA1811:AvoidUncalledPrivateCode", Scope="member", Target="System.Deployment.Internal.CodeSigning.ManifestSignedXml.#FindIdElement(System.Xml.XmlElement,System.String)", Justification="Not code owned by us")] +[module: SuppressMessage("Microsoft.Usage","CA2208:InstantiateArgumentExceptionsCorrectly", Scope="member", Target="System.Deployment.Internal.CodeSigning.CmiManifestSigner.#set_IncludeOption(System.Security.Cryptography.X509Certificates.X509IncludeOption)", Justification="Not code owned by us")] +[module: SuppressMessage("Microsoft.Usage","CA2208:InstantiateArgumentExceptionsCorrectly", Scope="member", Target="System.Deployment.Internal.CodeSigning.CmiManifestSigner.#set_Flag(System.Deployment.Internal.CodeSigning.CmiManifestSignerFlag)", Justification="Not code owned by us")] +[module: SuppressMessage("Microsoft.Globalization","CA1309:UseOrdinalStringComparison", MessageId="System.String.Compare(System.String,System.String,System.StringComparison)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.BootstrapperBuilder.#BuildPackages(Microsoft.Build.Tasks.Deployment.Bootstrapper.BuildSettings,System.Xml.XmlElement,Microsoft.Build.Tasks.Deployment.Bootstrapper.ResourceUpdater,System.Collections.ArrayList,System.Collections.Hashtable)", Justification="vbcodev will look at these later")] +[module: SuppressMessage("Microsoft.Globalization","CA1305:SpecifyIFormatProvider", MessageId="System.String.Format(System.String,System.Object,System.Object)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.Bootstrapper.ResourceUpdater.#UpdateResources(System.String,Microsoft.Build.Tasks.Deployment.Bootstrapper.BuildResults)", Justification="vbcodev will look at these later")] +[module: SuppressMessage("Microsoft.Globalization","CA1305:SpecifyIFormatProvider", MessageId="System.Int32.ToString(System.IFormatProvider)", Scope="member", Target="Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest.#ValidateFileAssociations()", Justification="vbcodev will look at these later")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Boolean.TryParse(System.String,System.Boolean@)", Scope="member", Target="Microsoft.Build.Tasks.GenerateManifestBase.#IsEmbedInteropEnabledForAssembly(Microsoft.Build.Framework.ITaskItem)", Justification="vbcodev will look at these later")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Csc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.ICscHostObject3", Justification="csc is the name of the tool and spelled correctly")] + +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.GetSDKReferenceFiles.#ResolvedSDKReferences", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="type", Target="Microsoft.Build.Tasks.GetSDKReferenceFiles", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.GetInstalledSDKLocations.#SDKRegistryRoot", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.GetInstalledSDKLocations.#SDKDirectoryRoot", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDKs", Scope="member", Target="Microsoft.Build.Tasks.GetInstalledSDKLocations.#InstalledSDKs", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="type", Target="Microsoft.Build.Tasks.GetInstalledSDKLocations", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.ResolveAssemblyReference.#ResolvedSDKReferences", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="type", Target="Microsoft.Build.Tasks.ResolveSDKReference", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDKs", Scope="member", Target="Microsoft.Build.Tasks.ResolveSDKReference.#InstalledSDKs", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.ResolveSDKReference.#ResolvedSDKReferences", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.ResolveSDKReference.#SDKReferences", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.ResolveSDKReference.#TargetedSDKArchitecture", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="SDK", Scope="member", Target="Microsoft.Build.Tasks.ResolveSDKReference.#TargetedSDKConfiguration", Justification="SDK is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="PDB", Scope="member", Target="Microsoft.Build.Tasks.WinMDExp.#InputPDBFile", Justification="PDB is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="PDB", Scope="member", Target="Microsoft.Build.Tasks.WinMDExp.#OutputPDBFile", Justification="PDB is the proper casing for this")] +[module: SuppressMessage("Microsoft.Usage","CA1806:DoNotIgnoreMethodResults", MessageId="System.Boolean.TryParse(System.String,System.Boolean@)", Scope="member", Target="Microsoft.Build.Tasks.GetSDKReferenceFiles+ResolvedReferenceAssembly.#.ctor(Microsoft.Build.Framework.ITaskItem,System.String)", Justification="copyLocal is false by default")] +[module: SuppressMessage("Microsoft.Naming","CA1709:IdentifiersShouldBeCasedCorrectly", MessageId="VB", Scope="member", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject4.#SetVBRuntime(System.String)", Justification="VB is the proper casing for this")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Vbc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject4", Justification="Vbc spelling is correct for this")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Vbc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObject5", Justification="Vbc spelling is correct for this")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Vbc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.IVbcHostObjectFreeThreaded", Justification="Vbc spelling is correct for this")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="appx", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", Justification="APPX spelling is correct")] +[module: SuppressMessage("Microsoft.Usage","CA2227:CollectionPropertiesShouldBeReadOnly", Scope="member", Target="Microsoft.Build.Tasks.Xaml.CommandLineToolSwitch.#Arguments", Justification="It is ok for users to set the ICollection.")] +[module: SuppressMessage("Microsoft.Naming","CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId="fwlink", Scope="resource", Target="Microsoft.Build.Tasks.Strings.resources", Justification="fwlink is part of a valid domain")] +[module: SuppressMessage("Microsoft.Naming","CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId="Csc", Scope="type", Target="Microsoft.Build.Tasks.Hosting.ICscHostObject4", Justification="Not worh breaking customers")] +#endif + + diff --git a/src/XMakeTasks/GenerateApplicationManifest.cs b/src/XMakeTasks/GenerateApplicationManifest.cs new file mode 100644 index 00000000000..5cf84c4deb9 --- /dev/null +++ b/src/XMakeTasks/GenerateApplicationManifest.cs @@ -0,0 +1,482 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Generates an application manifest for ClickOnce projects. + /// + public sealed class GenerateApplicationManifest : GenerateManifestBase + { + private enum _ManifestType { Native, ClickOnce } + + private string _clrVersion = null; + private ITaskItem _configFile = null; + private ITaskItem[] _dependencies = null; + private string _errorReportUrl = null; + private ITaskItem[] _files = null; + private ITaskItem _iconFile = null; + private ITaskItem[] _isolatedComReferences = null; + private _ManifestType _manifestType = _ManifestType.ClickOnce; + private string _osVersion = null; + private ITaskItem _trustInfoFile = null; + private ITaskItem[] _fileAssociations = null; + private bool _hostInBrowser = false; + private bool _useApplicationTrust = false; + private string _product = null; + private string _publisher = null; + private string _suiteName = null; + private string _supportUrl = null; + private string _specifiedManifestType = null; + private string _targetFrameworkSubset = String.Empty; + private string _targetFrameworkProfile = String.Empty; + private bool _requiresMinimumFramework35SP1; + + public string ClrVersion + { + get { return _clrVersion; } + set { _clrVersion = value; } + } + + public ITaskItem ConfigFile + { + get { return _configFile; } + set { _configFile = value; } + } + + public ITaskItem[] Dependencies + { + get { return _dependencies; } + set { _dependencies = Util.SortItems(value); } + } + + public string ErrorReportUrl + { + get { return _errorReportUrl; } + set { _errorReportUrl = value; } + } + + public ITaskItem[] FileAssociations + { + get + { + // File associations are only valid when targeting 3.5 or later + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + return null; + return _fileAssociations; + } + set { _fileAssociations = value; } + } + + public ITaskItem[] Files + { + get { return _files; } + set { _files = Util.SortItems(value); } + } + + public bool HostInBrowser + { + get { return _hostInBrowser; } + set { _hostInBrowser = value; } + } + + public ITaskItem IconFile + { + get { return _iconFile; } + set { _iconFile = value; } + } + + public ITaskItem[] IsolatedComReferences + { + get { return _isolatedComReferences; } + set { _isolatedComReferences = Util.SortItems(value); } + } + + public string ManifestType + { + get { return _specifiedManifestType; } + set { _specifiedManifestType = value; } + } + + public string OSVersion + { + get { return _osVersion; } + set { _osVersion = value; } + } + + public string Product + { + get { return _product; } + set { _product = value; } + } + + public string Publisher + { + get { return _publisher; } + set { _publisher = value; } + } + + public bool RequiresMinimumFramework35SP1 + { + get { return _requiresMinimumFramework35SP1; } + set { _requiresMinimumFramework35SP1 = value; } + } + + public string SuiteName + { + get { return _suiteName; } + set { _suiteName = value; } + } + + public string SupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + public string TargetFrameworkSubset + { + get { return _targetFrameworkSubset; } + set { _targetFrameworkSubset = value; } + } + + public string TargetFrameworkProfile + { + get { return _targetFrameworkProfile; } + set { _targetFrameworkProfile = value; } + } + + public ITaskItem TrustInfoFile + { + get { return _trustInfoFile; } + set { _trustInfoFile = value; } + } + + public bool UseApplicationTrust + { + get + { + // Use application trust is only used if targeting v3.5 or later + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + return false; + return _useApplicationTrust; + } + set { _useApplicationTrust = value; } + } + + protected override Type GetObjectType() + { + return typeof(ApplicationManifest); + } + + protected override bool OnManifestLoaded(Manifest manifest) + { + return BuildApplicationManifest(manifest as ApplicationManifest); + } + + protected override bool OnManifestResolved(Manifest manifest) + { + if (UseApplicationTrust) + return BuildResolvedSettings(manifest as ApplicationManifest); + return true; + } + + private bool BuildApplicationManifest(ApplicationManifest manifest) + { + if (Dependencies != null) + foreach (ITaskItem item in Dependencies) + AddAssemblyFromItem(item); + + if (Files != null) + foreach (ITaskItem item in Files) + AddFileFromItem(item); + + // Build ClickOnce info... + manifest.IsClickOnceManifest = _manifestType == _ManifestType.ClickOnce; + if (manifest.IsClickOnceManifest) + { + if (manifest.EntryPoint == null && Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.NoEntryPoint"); + return false; + } + + if (!AddClickOnceFiles(manifest)) + return false; + + if (!AddClickOnceFileAssociations(manifest)) + return false; + } + + if (HostInBrowser && Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion30) < 0) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.HostInBrowserInvalidFrameworkVersion"); + return false; + } + + // Build isolated COM info... + if (!AddIsolatedComReferences(manifest)) + return false; + + manifest.MaxTargetPath = MaxTargetPath; + manifest.HostInBrowser = HostInBrowser; + manifest.UseApplicationTrust = UseApplicationTrust; + if (UseApplicationTrust && SupportUrl != null) + manifest.SupportUrl = SupportUrl; + if (UseApplicationTrust && SuiteName != null) + manifest.SuiteName = SuiteName; + if (UseApplicationTrust && ErrorReportUrl != null) + manifest.ErrorReportUrl = ErrorReportUrl; + + return true; + } + + private bool AddIsolatedComReferences(ApplicationManifest manifest) + { + int t1 = Environment.TickCount; + bool success = true; + if (IsolatedComReferences != null) + foreach (ITaskItem item in IsolatedComReferences) + { + string name = item.GetMetadata("Name"); + if (String.IsNullOrEmpty(name)) + name = Path.GetFileName(item.ItemSpec); + FileReference file = AddFileFromItem(item); + if (!file.ImportComComponent(item.ItemSpec, manifest.OutputMessages, name)) + success = false; + } + + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "GenerateApplicationManifest.AddIsolatedComReferences t={0}", Environment.TickCount - t1)); + return success; + } + + private bool AddClickOnceFileAssociations(ApplicationManifest manifest) + { + if (FileAssociations != null) + { + foreach (ITaskItem item in FileAssociations) + { + FileAssociation fileAssociation = new FileAssociation(); + fileAssociation.DefaultIcon = item.GetMetadata("DefaultIcon"); + fileAssociation.Description = item.GetMetadata("Description"); + fileAssociation.Extension = item.ItemSpec; + fileAssociation.ProgId = item.GetMetadata("Progid"); + manifest.FileAssociations.Add(fileAssociation); + } + } + + return true; + } + + private bool AddClickOnceFiles(ApplicationManifest manifest) + { + int t1 = Environment.TickCount; + + if (ConfigFile != null && !String.IsNullOrEmpty(ConfigFile.ItemSpec)) + manifest.ConfigFile = FindFileFromItem(ConfigFile).TargetPath; + + if (IconFile != null && !String.IsNullOrEmpty(IconFile.ItemSpec)) + manifest.IconFile = FindFileFromItem(IconFile).TargetPath; + + if (TrustInfoFile != null && !String.IsNullOrEmpty(TrustInfoFile.ItemSpec)) + { + manifest.TrustInfo = new TrustInfo(); + manifest.TrustInfo.Read(TrustInfoFile.ItemSpec); + } + + if (manifest.TrustInfo == null) + manifest.TrustInfo = new TrustInfo(); + + if (OSVersion != null) + { + manifest.OSVersion = _osVersion; + } + + if (ClrVersion != null) + { + AssemblyReference CLRPlatformAssembly = manifest.AssemblyReferences.Find(Constants.CLRPlatformAssemblyName); + if (CLRPlatformAssembly == null) + { + CLRPlatformAssembly = new AssemblyReference(); + CLRPlatformAssembly.IsPrerequisite = true; + manifest.AssemblyReferences.Add(CLRPlatformAssembly); + } + CLRPlatformAssembly.AssemblyIdentity = new AssemblyIdentity(Constants.CLRPlatformAssemblyName, ClrVersion); + } + + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion30) == 0) + { + EnsureAssemblyReferenceExists(manifest, CreateAssemblyIdentity(Constants.NET30AssemblyIdentity)); + } + else if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) == 0) + { + EnsureAssemblyReferenceExists(manifest, CreateAssemblyIdentity(Constants.NET30AssemblyIdentity)); + EnsureAssemblyReferenceExists(manifest, CreateAssemblyIdentity(Constants.NET35AssemblyIdentity)); + + if ((!String.IsNullOrEmpty(TargetFrameworkSubset) && TargetFrameworkSubset.Equals(Constants.ClientFrameworkSubset, StringComparison.OrdinalIgnoreCase)) || + (!String.IsNullOrEmpty(TargetFrameworkProfile) && TargetFrameworkProfile.Equals(Constants.ClientFrameworkSubset, StringComparison.OrdinalIgnoreCase))) + { + EnsureAssemblyReferenceExists(manifest, CreateAssemblyIdentity(Constants.NET35ClientAssemblyIdentity)); + } + else if (RequiresMinimumFramework35SP1) + { + EnsureAssemblyReferenceExists(manifest, CreateAssemblyIdentity(Constants.NET35SP1AssemblyIdentity)); + } + } + + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "GenerateApplicationManifest.AddClickOnceFiles t={0}", Environment.TickCount - t1)); + return true; + } + + protected internal override bool ValidateInputs() + { + bool valid = base.ValidateInputs(); + if (_specifiedManifestType != null) + { + try + { + _manifestType = (_ManifestType)Enum.Parse(typeof(_ManifestType), _specifiedManifestType, true); + } + catch (FormatException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "ManifestType"); + valid = false; + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "ManifestType"); + valid = false; + } + if (_manifestType == _ManifestType.Native) + EntryPoint = null; // EntryPoint is ignored if ManifestType="Native" + } + if (ClrVersion != null && !Util.IsValidVersion(ClrVersion, 4)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "ClrVersion"); + valid = false; + } + if (OSVersion != null && !Util.IsValidVersion(OSVersion, 4)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "OSVersion"); + valid = false; + } + if (!Util.IsValidFrameworkVersion(TargetFrameworkVersion) || Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion20) < 0) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "TargetFrameworkVersion"); + valid = false; + } + if (_manifestType == _ManifestType.ClickOnce) + { + // ClickOnce supports asInvoker privilege only. + string requestedExecutionLevel; + if (GetRequestedExecutionLevel(out requestedExecutionLevel) && String.CompareOrdinal(requestedExecutionLevel, Constants.UACAsInvoker) != 0) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidRequestedExecutionLevel", requestedExecutionLevel); + valid = false; + } + } + return valid; + } + + private bool BuildResolvedSettings(ApplicationManifest manifest) + { + // Note: if changing the logic in this function, please update the logic in + // GenerateDeploymentManifest.BuildResolvedSettings as well. + if (Product != null) + manifest.Product = Product; + else if (String.IsNullOrEmpty(manifest.Product)) + manifest.Product = Path.GetFileNameWithoutExtension(manifest.AssemblyIdentity.Name); + Debug.Assert(!String.IsNullOrEmpty(manifest.Product)); + + if (Publisher != null) + { + manifest.Publisher = Publisher; + } + else if (String.IsNullOrEmpty(manifest.Publisher)) + { + string org = Util.GetRegisteredOrganization(); + if (!String.IsNullOrEmpty(org)) + manifest.Publisher = org; + else + manifest.Publisher = manifest.Product; + } + Debug.Assert(!String.IsNullOrEmpty(manifest.Publisher)); + + return true; + } + + private AssemblyIdentity CreateAssemblyIdentity(string[] values) + { + if (values.Length != 5) + return null; + return new AssemblyIdentity(values[0], values[1], values[2], values[3], values[4]); + } + + private void EnsureAssemblyReferenceExists(ApplicationManifest manifest, AssemblyIdentity identity) + { + if (manifest.AssemblyReferences.Find(identity) == null) + { + AssemblyReference assembly = new AssemblyReference(); + assembly.IsPrerequisite = true; + assembly.AssemblyIdentity = identity; + manifest.AssemblyReferences.Add(assembly); + } + } + + private bool GetRequestedExecutionLevel(out string requestedExecutionLevel) + { + requestedExecutionLevel = Constants.UACAsInvoker; // For backwards compatibility we assume asInvoker to begin with. + + if (InputManifest == null || String.IsNullOrEmpty(InputManifest.ItemSpec) || String.CompareOrdinal(InputManifest.ItemSpec, "NoManifest") == 0) + { + return false; + } + + try + { + using (Stream s = File.Open(InputManifest.ItemSpec, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + XmlDocument document = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(s, xrSettings)) + { + document.Load(xr); + + //Create an XmlNamespaceManager for resolving namespaces. + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + + XmlNode node = (XmlElement)document.SelectSingleNode(XPaths.requestedExecutionLevelPath, nsmgr); + if (node != null) + { + XmlAttribute attr = (XmlAttribute)(node.Attributes.GetNamedItem("level")); + if (attr != null) + requestedExecutionLevel = attr.Value; + } + } + + return true; + } + } + catch (Exception ex) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ReadInputManifestFailed", InputManifest.ItemSpec, ex.Message); + return false; + } + } + } +} diff --git a/src/XMakeTasks/GenerateBootstrapper.cs b/src/XMakeTasks/GenerateBootstrapper.cs new file mode 100644 index 00000000000..40571fb59fa --- /dev/null +++ b/src/XMakeTasks/GenerateBootstrapper.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Collections; +using System.Globalization; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks.Deployment.Bootstrapper; + +namespace Microsoft.Build.Tasks +{ + /// + /// Generates a bootstrapper for ClickOnce deployment projects. + /// + public sealed class GenerateBootstrapper : TaskExtension + { + private string _applicationFile = null; + private string _applicationName = null; + private bool _applicationRequiresElevation = false; + private string _applicationUrl = null; + private ITaskItem[] _bootstrapperItems = null; + private string _componentsLocation = null; + private string _componentsUrl = null; + private bool _copyComponents = true; + private string _culture = Util.DefaultCultureInfo.Name; + private string _fallbackCulture = Util.DefaultCultureInfo.Name; + private string _outputPath = Environment.CurrentDirectory; + private string _path = null; + private string _supportUrl = null; + private bool _validate = true; + private string[] _bootstrapperComponentFiles = null; + private string _bootstrapperKeyFile = null; + private string _visualStudioVersion = null; + + public GenerateBootstrapper() + { + } + + public string ApplicationName + { + get { return _applicationName; } + set { _applicationName = value; } + } + + public string ApplicationFile + { + get { return _applicationFile; } + set { _applicationFile = value; } + } + + public bool ApplicationRequiresElevation + { + get { return _applicationRequiresElevation; } + set { _applicationRequiresElevation = value; } + } + + public string ApplicationUrl + { + get { return _applicationUrl; } + set { _applicationUrl = value; } + } + + public ITaskItem[] BootstrapperItems + { + get { return _bootstrapperItems; } + set { _bootstrapperItems = value; } + } + + public string ComponentsLocation + { + get { return _componentsLocation; } + set { _componentsLocation = value; } + } + + public string ComponentsUrl + { + get { return _componentsUrl; } + set { _componentsUrl = value; } + } + + public bool CopyComponents + { + get { return _copyComponents; } + set + { + _copyComponents = value; + } + } + + public string Culture + { + get { return _culture; } + set { _culture = value; } + } + + public string FallbackCulture + { + get { return _fallbackCulture; } + set { _fallbackCulture = value; } + } + + public string OutputPath + { + get { return _outputPath; } + set { _outputPath = value; } + } + + public string Path + { + get { return _path; } + set { _path = value; } + } + + public string SupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + public string VisualStudioVersion + { + get { return _visualStudioVersion; } + set { _visualStudioVersion = value; } + } + + public bool Validate + { + get { return _validate; } + set { _validate = value; } + } + + [Output] + public string BootstrapperKeyFile + { + get { return _bootstrapperKeyFile; } + set { _bootstrapperKeyFile = value; } + } + + [Output] + public string[] BootstrapperComponentFiles + { + get { return _bootstrapperComponentFiles; } + set { _bootstrapperComponentFiles = value; } + } + + /// + /// Generate the bootstrapper. + /// + /// Return true on success, false on failure. + public override bool Execute() + { + if (_path == null) + { + _path = Util.GetDefaultPath(_visualStudioVersion); + } + + BootstrapperBuilder bootstrapperBuilder = new BootstrapperBuilder(); + + bootstrapperBuilder.Validate = this.Validate; + bootstrapperBuilder.Path = this.Path; + + ProductCollection products = bootstrapperBuilder.Products; + + BuildSettings settings = new BuildSettings(); + + settings.ApplicationFile = ApplicationFile; + settings.ApplicationName = ApplicationName; + settings.ApplicationRequiresElevation = ApplicationRequiresElevation; + settings.ApplicationUrl = ApplicationUrl; + settings.ComponentsLocation = ConvertStringToComponentsLocation(this.ComponentsLocation); + settings.ComponentsUrl = ComponentsUrl; + settings.CopyComponents = CopyComponents; + settings.Culture = _culture; + settings.FallbackCulture = _fallbackCulture; + settings.OutputPath = this.OutputPath; + settings.SupportUrl = this.SupportUrl; + + if (String.IsNullOrEmpty(settings.Culture) || settings.Culture == "*") + { + settings.Culture = settings.FallbackCulture; + } + + if (this.BootstrapperItems != null) + { + // The bootstrapper items may not be in the correct order, because XMake saves + // items in alphabetical order. So we will attempt to put items into the correct + // order, according to the Products order in the search. To do this, we add all + // the items we are told to build into a hashtable, then go through our products + // in order, looking to see if the item is built. If it is, remove the item from + // the hashtable. All remaining items in the table can not be built, so errors + // will be issued. + Hashtable items = new Hashtable(StringComparer.OrdinalIgnoreCase); + + foreach (ITaskItem bootstrapperItem in this.BootstrapperItems) + { + string installAttribute = bootstrapperItem.GetMetadata("Install"); + if (String.IsNullOrEmpty(installAttribute) || Shared.ConversionUtilities.ConvertStringToBool(installAttribute)) + { + if (!items.Contains(bootstrapperItem.ItemSpec)) + { + items.Add(bootstrapperItem.ItemSpec, bootstrapperItem); + } + else + { + Log.LogWarningWithCodeFromResources("GenerateBootstrapper.DuplicateItems", bootstrapperItem.ItemSpec); + } + } + } + + foreach (Product product in products) + { + if (items.Contains(product.ProductCode)) + { + settings.ProductBuilders.Add(product.ProductBuilder); + items.Remove(product.ProductCode); + } + } + + foreach (ITaskItem bootstrapperItem in items.Values) + { + Log.LogWarningWithCodeFromResources("GenerateBootstrapper.ProductNotFound", bootstrapperItem.ItemSpec, bootstrapperBuilder.Path); + } + } + + BuildResults results = bootstrapperBuilder.Build(settings); + BuildMessage[] messages = results.Messages; + + if (messages != null) + { + foreach (BuildMessage message in messages) + { + if (message.Severity == BuildMessageSeverity.Error) + Log.LogError(null, message.HelpCode, message.HelpKeyword, null, 0, 0, 0, 0, message.Message); + else if (message.Severity == BuildMessageSeverity.Warning) + Log.LogWarning(null, message.HelpCode, message.HelpKeyword, null, 0, 0, 0, 0, message.Message); + } + } + + this.BootstrapperKeyFile = results.KeyFile; + this.BootstrapperComponentFiles = results.ComponentFiles; + + return results.Succeeded; + } + + private ComponentsLocation ConvertStringToComponentsLocation(string parameterValue) + { + if (parameterValue == null || parameterValue.Length == 0) + return Microsoft.Build.Tasks.Deployment.Bootstrapper.ComponentsLocation.HomeSite; + try + { + return (Microsoft.Build.Tasks.Deployment.Bootstrapper.ComponentsLocation)Enum.Parse(typeof(Microsoft.Build.Tasks.Deployment.Bootstrapper.ComponentsLocation), parameterValue, false); + } + catch (FormatException) + { + Log.LogWarningWithCodeFromResources("GenerateBootstrapper.InvalidComponentsLocation", parameterValue); + return Microsoft.Build.Tasks.Deployment.Bootstrapper.ComponentsLocation.HomeSite; + } + } + } +} diff --git a/src/XMakeTasks/GenerateDeploymentManifest.cs b/src/XMakeTasks/GenerateDeploymentManifest.cs new file mode 100644 index 00000000000..f65e1860a4c --- /dev/null +++ b/src/XMakeTasks/GenerateDeploymentManifest.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Generates a deploy manifest for ClickOnce projects. + /// + public sealed class GenerateDeploymentManifest : GenerateManifestBase + { + private bool? _createDesktopShortcut = null; + private string _deploymentUrl = null; + private bool? _disallowUrlActivation = null; + private string _errorReportUrl = null; + private bool? _install = null; + private bool? _mapFileExtensions = null; + private string _minimumRequiredVersion = null; + private string _product = null; + private string _publisher = null; + private string _suiteName = null; + private string _supportUrl = null; + private bool? _trustUrlParameters = null; + private bool? _updateEnabled = null; + private int? _updateInterval = null; + private UpdateMode? _updateMode = null; + private UpdateUnit? _updateUnit = null; + + private string _specifiedUpdateMode = null; + private string _specifiedUpdateUnit = null; + + public bool CreateDesktopShortcut + { + get + { + if (!_createDesktopShortcut.HasValue) + return false; + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + return false; + return (bool)_createDesktopShortcut; + } + set { _createDesktopShortcut = value; } + } + + public string DeploymentUrl + { + get { return _deploymentUrl; } + set { _deploymentUrl = value; } + } + + public bool DisallowUrlActivation + { + get { return (bool)_disallowUrlActivation; } + set { _disallowUrlActivation = value; } + } + + public string ErrorReportUrl + { + get + { + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + return null; + return _errorReportUrl; + } + set { _errorReportUrl = value; } + } + + public bool Install + { + get { return (bool)_install; } + set { _install = value; } + } + + public string MinimumRequiredVersion + { + get { return _minimumRequiredVersion; } + set { _minimumRequiredVersion = value; } + } + + public bool MapFileExtensions + { + get { return (bool)_mapFileExtensions; } + set { _mapFileExtensions = value; } + } + + public string Product + { + get { return _product; } + set { _product = value; } + } + + public string Publisher + { + get { return _publisher; } + set { _publisher = value; } + } + + public string SuiteName + { + get + { + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + return null; + return _suiteName; + } + set { _suiteName = value; } + } + + public string SupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + public bool TrustUrlParameters + { + get { return (bool)_trustUrlParameters; } + set { _trustUrlParameters = value; } + } + + public bool UpdateEnabled + { + get { return (bool)_updateEnabled; } + set { _updateEnabled = value; } + } + + public int UpdateInterval + { + get { return (int)_updateInterval; } + set { _updateInterval = value; } + } + + public string UpdateMode + { + get { return _specifiedUpdateMode; } + set { _specifiedUpdateMode = value; } + } + + public string UpdateUnit + { + get { return _specifiedUpdateUnit; } + set { _specifiedUpdateUnit = value; } + } + + private bool BuildResolvedSettings(DeployManifest manifest) + { + // Note: if changing the logic in this function, please update the logic in + // GenerateApplicationManifest.BuildResolvedSettings as well. + if (Product != null) + manifest.Product = Product; + else if (String.IsNullOrEmpty(manifest.Product)) + manifest.Product = Path.GetFileNameWithoutExtension(manifest.AssemblyIdentity.Name); + Debug.Assert(!String.IsNullOrEmpty(manifest.Product)); + + if (Publisher != null) + { + manifest.Publisher = Publisher; + } + else if (String.IsNullOrEmpty(manifest.Publisher)) + { + string org = Util.GetRegisteredOrganization(); + if (!String.IsNullOrEmpty(org)) + manifest.Publisher = org; + else + manifest.Publisher = manifest.Product; + } + Debug.Assert(!String.IsNullOrEmpty(manifest.Publisher)); + + return true; + } + + protected override Type GetObjectType() + { + return typeof(DeployManifest); + } + + protected override bool OnManifestLoaded(Manifest manifest) + { + return BuildDeployManifest(manifest as DeployManifest); + } + + protected override bool OnManifestResolved(Manifest manifest) + { + return BuildResolvedSettings(manifest as DeployManifest); + } + + private bool BuildDeployManifest(DeployManifest manifest) + { + if (manifest.EntryPoint == null) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.NoEntryPoint"); + return false; + } + + if (SupportUrl != null) + manifest.SupportUrl = SupportUrl; + + if (DeploymentUrl != null) + manifest.DeploymentUrl = DeploymentUrl; + + if (_install.HasValue) + manifest.Install = (bool)_install; + + if (_updateEnabled.HasValue) + manifest.UpdateEnabled = (bool)_updateEnabled; + + if (_updateInterval.HasValue) + manifest.UpdateInterval = (int)_updateInterval; + + if (_updateMode.HasValue) + manifest.UpdateMode = (UpdateMode)_updateMode; + + if (_updateUnit.HasValue) + manifest.UpdateUnit = (UpdateUnit)_updateUnit; + + if (MinimumRequiredVersion != null) + manifest.MinimumRequiredVersion = MinimumRequiredVersion; + + if (manifest.Install) // Ignore DisallowUrlActivation flag for online-only apps + if (_disallowUrlActivation.HasValue) + manifest.DisallowUrlActivation = (bool)_disallowUrlActivation; + + if (_mapFileExtensions.HasValue) + manifest.MapFileExtensions = (bool)_mapFileExtensions; + + if (_trustUrlParameters.HasValue) + manifest.TrustUrlParameters = (bool)_trustUrlParameters; + + if (_createDesktopShortcut.HasValue) + manifest.CreateDesktopShortcut = CreateDesktopShortcut; + + if (SuiteName != null) + manifest.SuiteName = SuiteName; + + if (ErrorReportUrl != null) + manifest.ErrorReportUrl = ErrorReportUrl; + + return true; + } + + protected internal override bool ValidateInputs() + { + bool valid = base.ValidateInputs(); + if (!String.IsNullOrEmpty(_minimumRequiredVersion) && !Util.IsValidVersion(_minimumRequiredVersion, 4)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "MinimumRequiredVersion"); + valid = false; + } + if (_specifiedUpdateMode != null) + { + try + { + _updateMode = (UpdateMode)Enum.Parse(typeof(UpdateMode), _specifiedUpdateMode, true); + } + catch (FormatException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "UpdateMode"); + valid = false; + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "UpdateMode"); + valid = false; + } + } + if (_specifiedUpdateUnit != null) + { + try + { + _updateUnit = (UpdateUnit)Enum.Parse(typeof(UpdateUnit), _specifiedUpdateUnit, true); + } + catch (FormatException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "UpdateUnit"); + valid = false; + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "UpdateUnit"); + valid = false; + } + } + return valid; + } + } +} diff --git a/src/XMakeTasks/GenerateManifestBase.cs b/src/XMakeTasks/GenerateManifestBase.cs new file mode 100644 index 00000000000..09edc0d6777 --- /dev/null +++ b/src/XMakeTasks/GenerateManifestBase.cs @@ -0,0 +1,579 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Collections; +using System.Security.Cryptography; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for all manifest generation tasks. + /// + public abstract class GenerateManifestBase : Task + { + private enum AssemblyType { Unspecified, Managed, Native, Satellite }; + private enum DependencyType { Install, Prerequisite }; + + private string _assemblyName = null; + private string _assemblyVersion = null; + private string _description = null; + private ITaskItem _entryPoint = null; + private ITaskItem _inputManifest = null; + private int _maxTargetPath = 0; + private ITaskItem _outputManifest = null; + private string _platform = null; + private string _processorArchitecture = null; + private int _startTime = 0; + private string _targetCulture = null; + private string _targetFrameworkVersion = Constants.TargetFrameworkVersion20; + private string _targetFrameworkMoniker = null; + + private Manifest _manifest; + protected abstract bool OnManifestLoaded(Manifest manifest); + protected abstract bool OnManifestResolved(Manifest manifest); + protected abstract Type GetObjectType(); + + + protected GenerateManifestBase() : base(AssemblyResources.PrimaryResources, "MSBuild.") + { + } + + public string AssemblyName + { + get { return _assemblyName; } + set { _assemblyName = value; } + } + + public string AssemblyVersion + { + get { return _assemblyVersion; } + set { _assemblyVersion = value; } + } + + public string Description + { + get { return _description; } + set { _description = value; } + } + + public ITaskItem EntryPoint + { + get { return _entryPoint; } + set { _entryPoint = value; } + } + + public ITaskItem InputManifest + { + get { return _inputManifest; } + set { _inputManifest = value; } + } + + public int MaxTargetPath + { + get { return _maxTargetPath; } + set { _maxTargetPath = value; } + } + + [Output] + public ITaskItem OutputManifest + { + get { return _outputManifest; } + set { _outputManifest = value; } + } + + public string Platform + { + get { return _platform; } + set { _platform = value; } + } + + public string TargetCulture + { + get { return _targetCulture; } + set { _targetCulture = value; } + } + + public string TargetFrameworkVersion + { + get + { + if (string.IsNullOrEmpty(_targetFrameworkVersion)) + return Constants.TargetFrameworkVersion35; + return _targetFrameworkVersion; + } + set { _targetFrameworkVersion = value; } + } + + public string TargetFrameworkMoniker + { + get + { + return _targetFrameworkMoniker; + } + set { _targetFrameworkMoniker = value; } + } + + protected internal AssemblyReference AddAssemblyNameFromItem(ITaskItem item, AssemblyReferenceType referenceType) + { + AssemblyReference assembly = new AssemblyReference(); + assembly.AssemblyIdentity = AssemblyIdentity.FromAssemblyName(item.ItemSpec); + assembly.ReferenceType = referenceType; + _manifest.AssemblyReferences.Add(assembly); + string hintPath = item.GetMetadata("HintPath"); + if (!String.IsNullOrEmpty(hintPath)) + assembly.SourcePath = hintPath; + SetItemAttributes(item, assembly); + return assembly; + } + + protected internal AssemblyReference AddAssemblyFromItem(ITaskItem item) + { + // if the assembly is a no-pia assembly and embed interop is turned on, then we don't write it to the manifest. + if (IsEmbedInteropEnabledForAssembly(item)) + return null; + + AssemblyReferenceType referenceType; + AssemblyType assemblyType = GetItemAssemblyType(item); + switch (assemblyType) + { + case AssemblyType.Managed: + referenceType = AssemblyReferenceType.ManagedAssembly; + break; + case AssemblyType.Native: + referenceType = AssemblyReferenceType.NativeAssembly; + break; + case AssemblyType.Satellite: + referenceType = AssemblyReferenceType.ManagedAssembly; + break; + default: + referenceType = AssemblyReferenceType.Unspecified; + break; + } + + DependencyType dependencyType = GetItemDependencyType(item); + AssemblyReference assembly; + if (dependencyType == DependencyType.Install) + { + assembly = _manifest.AssemblyReferences.Add(item.ItemSpec); + SetItemAttributes(item, assembly); + } + else + { + AssemblyIdentity identity = AssemblyIdentity.FromAssemblyName(item.ItemSpec); + // If we interpreted the item as a strong name, then treat it as a Fusion display name... + if (identity.IsStrongName) + { + assembly = new AssemblyReference(); + assembly.AssemblyIdentity = identity; + } + else // otherwise treat it as a file path... + { + assembly = new AssemblyReference(item.ItemSpec); + } + _manifest.AssemblyReferences.Add(assembly); + assembly.IsPrerequisite = true; + } + + assembly.ReferenceType = referenceType; + string isPrimary = item.GetMetadata(ItemMetadataNames.isPrimary); + if (string.Equals(isPrimary, "true", StringComparison.Ordinal)) + { + assembly.IsPrimary = true; + } + + return assembly; + } + + protected internal AssemblyReference AddEntryPointFromItem(ITaskItem item, AssemblyReferenceType referenceType) + { + AssemblyReference assembly = _manifest.AssemblyReferences.Add(item.ItemSpec); + assembly.ReferenceType = referenceType; + SetItemAttributes(item, assembly); + return assembly; + } + + protected internal FileReference AddFileFromItem(ITaskItem item) + { + FileReference file = _manifest.FileReferences.Add(item.ItemSpec); + SetItemAttributes(item, file); + file.IsDataFile = ConvertUtil.ToBoolean(item.GetMetadata("IsDataFile")); + return file; + } + + private AssemblyIdentity CreateAssemblyIdentity(AssemblyIdentity baseIdentity, AssemblyIdentity entryPointIdentity) + { + string name = _assemblyName; + string version = _assemblyVersion; + string publicKeyToken = "0000000000000000"; + string culture = _targetCulture; + + if (String.IsNullOrEmpty(name)) + { + if (baseIdentity != null && !String.IsNullOrEmpty(baseIdentity.Name)) + name = baseIdentity.Name; + else if (entryPointIdentity != null && !String.IsNullOrEmpty(entryPointIdentity.Name)) + { + if (_manifest is DeployManifest) + name = Path.GetFileNameWithoutExtension(entryPointIdentity.Name) + ".application"; + else if (_manifest is ApplicationManifest) + name = entryPointIdentity.Name + ".exe"; + } + } + if (String.IsNullOrEmpty(name)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.NoIdentity"); + return null; + } + + if (String.IsNullOrEmpty(version)) + { + if (baseIdentity != null && !String.IsNullOrEmpty(baseIdentity.Version)) + version = baseIdentity.Version; + else if (entryPointIdentity != null && !String.IsNullOrEmpty(entryPointIdentity.Version)) + version = entryPointIdentity.Version; + } + if (String.IsNullOrEmpty(version)) + version = "1.0.0.0"; + + if (String.IsNullOrEmpty(culture)) + { + if (baseIdentity != null && !String.IsNullOrEmpty(baseIdentity.Culture)) + culture = baseIdentity.Culture; + else if (entryPointIdentity != null && !String.IsNullOrEmpty(entryPointIdentity.Culture)) + culture = entryPointIdentity.Culture; + } + if (String.IsNullOrEmpty(culture) + || String.Equals(culture, "neutral", StringComparison.OrdinalIgnoreCase) + || String.Equals(culture, "*", StringComparison.OrdinalIgnoreCase)) + culture = "neutral"; + + if (String.IsNullOrEmpty(_processorArchitecture)) + { + if (baseIdentity != null && !String.IsNullOrEmpty(baseIdentity.ProcessorArchitecture)) + _processorArchitecture = baseIdentity.ProcessorArchitecture; + else if (entryPointIdentity != null && !String.IsNullOrEmpty(entryPointIdentity.ProcessorArchitecture)) + _processorArchitecture = entryPointIdentity.ProcessorArchitecture; + } + if (String.IsNullOrEmpty(_processorArchitecture)) + _processorArchitecture = "msil"; + + // Fixup for non-ClickOnce case... + if (_manifest is ApplicationManifest) + { + ApplicationManifest applicationManifest = _manifest as ApplicationManifest; + if (!applicationManifest.IsClickOnceManifest) + { + // Don't need publicKeyToken attribute for non-ClickOnce case + publicKeyToken = null; + // Language attribute should be omitted if neutral + if (String.Compare(culture, "neutral", StringComparison.OrdinalIgnoreCase) == 0) + culture = null; + // WinXP loader doesn't understand "msil" + if (String.Compare(_processorArchitecture, "msil", StringComparison.OrdinalIgnoreCase) == 0) + _processorArchitecture = null; + } + } + + return new AssemblyIdentity(name, version, publicKeyToken, culture, _processorArchitecture); + } + + public override bool Execute() + { + bool success = true; + + Type manifestType = GetObjectType(); + if (!InitializeManifest(manifestType)) + success = false; + + if (success && !BuildManifest()) + success = false; + + if (_manifest != null) + { + _manifest.OutputMessages.LogTaskMessages(this); + if (_manifest.OutputMessages.ErrorCount > 0) + success = false; + } + + return success; + } + + private bool BuildManifest() + { + if (!OnManifestLoaded(_manifest)) + return false; + + if (!ResolveFiles()) + return false; + + if (!ResolveIdentity()) + return false; + + _manifest.SourcePath = GetOutputPath(); + + if (!OnManifestResolved(_manifest)) + return false; + + return WriteManifest(); + } + + protected internal FileReference FindFileFromItem(ITaskItem item) + { + string targetPath = item.GetMetadata(ItemMetadataNames.targetPath); + if (String.IsNullOrEmpty(targetPath)) + targetPath = BaseReference.GetDefaultTargetPath(item.ItemSpec); + foreach (FileReference file in _manifest.FileReferences) + if (String.Compare(targetPath, file.TargetPath, StringComparison.OrdinalIgnoreCase) == 0) + return file; + return AddFileFromItem(item); + } + + private string GetDefaultFileName() + { + if (_manifest is DeployManifest) + return _manifest.AssemblyIdentity.Name; + else + return _manifest.AssemblyIdentity.Name + ".manifest"; + } + + // Returns assembly type (i.e. "Managed", "Native", or "Satellite") as specified by the item. + // Returns "Unspecified" if item does not specify the assembly type. + // Logs a warning if specified assembly type is invalid. + private AssemblyType GetItemAssemblyType(ITaskItem item) + { + string value = item.GetMetadata("AssemblyType"); + if (!String.IsNullOrEmpty(value)) + try + { + return (AssemblyType)Enum.Parse(typeof(AssemblyType), value, true); + } + catch (FormatException) + { + Log.LogWarningWithCodeFromResources("GenerateManifest.InvalidItemValue", "AssemblyType", item.ItemSpec); + } + catch (ArgumentException) + { + Log.LogWarningWithCodeFromResources("GenerateManifest.InvalidItemValue", "AssemblyType", item.ItemSpec); + } + return AssemblyType.Unspecified; + } + + private bool IsEmbedInteropEnabledForAssembly(ITaskItem item) + { + string value = item.GetMetadata("EmbedInteropTypes"); + bool result; + + bool.TryParse(value, out result); + return result; + } + + // Returns dependency type (i.e. "Install" or "Prerequisite") as specified by the item. + // Returns "Install" if item does not specify the dependency type. + // Logs a warning if specified dependency type is invalid. + private DependencyType GetItemDependencyType(ITaskItem item) + { + string value = item.GetMetadata("DependencyType"); + if (!String.IsNullOrEmpty(value)) + try + { + return (DependencyType)Enum.Parse(typeof(DependencyType), value, true); + } + catch (FormatException) + { + Log.LogWarningWithCodeFromResources("GenerateManifest.InvalidItemValue", "DependencyType", item.ItemSpec); + } + catch (ArgumentException) + { + Log.LogWarningWithCodeFromResources("GenerateManifest.InvalidItemValue", "DependencyType", item.ItemSpec); + } + return DependencyType.Install; + } + + private string GetOutputPath() + { + if (OutputManifest != null) + return OutputManifest.ItemSpec; + else + return GetDefaultFileName(); + } + + private bool InitializeManifest(Type manifestType) + { + _startTime = Environment.TickCount; + + if (!ValidateInputs()) + return false; + + if (manifestType == null) + throw new ArgumentNullException("manifestType"); + if (InputManifest == null || String.IsNullOrEmpty(InputManifest.ItemSpec)) + { + if (manifestType == typeof(ApplicationManifest)) + _manifest = new ApplicationManifest(this.TargetFrameworkVersion); + else if (manifestType == typeof(DeployManifest)) + _manifest = new DeployManifest(this.TargetFrameworkMoniker); + else + throw new ArgumentException(String.Empty /* no message */, "manifestType"); + } + else + { + try + { + _manifest = ManifestReader.ReadManifest(manifestType.Name, InputManifest.ItemSpec, true); + } + catch (Exception ex) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ReadInputManifestFailed", InputManifest.ItemSpec, ex.Message); + return false; + } + } + + if (manifestType != _manifest.GetType()) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidInputManifest"); + return false; + } + + if (_manifest is DeployManifest) + { + DeployManifest deployManifest = _manifest as DeployManifest; + if (string.IsNullOrEmpty(deployManifest.TargetFrameworkMoniker)) + deployManifest.TargetFrameworkMoniker = this.TargetFrameworkMoniker; + } + else if (_manifest is ApplicationManifest) + { + ApplicationManifest applicationManifest = _manifest as ApplicationManifest; + if (string.IsNullOrEmpty(applicationManifest.TargetFrameworkVersion)) + applicationManifest.TargetFrameworkVersion = this.TargetFrameworkVersion; + } + + if (EntryPoint != null && !String.IsNullOrEmpty(EntryPoint.ItemSpec)) + { + AssemblyReferenceType referenceType = AssemblyReferenceType.Unspecified; + if (_manifest is DeployManifest) + referenceType = AssemblyReferenceType.ClickOnceManifest; + if (_manifest is ApplicationManifest) + referenceType = AssemblyReferenceType.ManagedAssembly; + _manifest.EntryPoint = AddEntryPointFromItem(EntryPoint, referenceType); + } + + if (Description != null) + _manifest.Description = Description; + + return true; + } + + private bool ResolveFiles() + { + int t1 = Environment.TickCount; + + string[] searchPaths = { Environment.CurrentDirectory }; + _manifest.ResolveFiles(searchPaths); + _manifest.UpdateFileInfo(this.TargetFrameworkVersion); + if (_manifest.OutputMessages.ErrorCount > 0) + return false; + + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "GenerateManifestBase.ResolveFiles t={0}", Environment.TickCount - t1)); + return true; + } + + private bool ResolveIdentity() + { + AssemblyIdentity entryPointIdentity = _manifest.EntryPoint != null ? _manifest.EntryPoint.AssemblyIdentity : null; + _manifest.AssemblyIdentity = CreateAssemblyIdentity(_manifest.AssemblyIdentity, entryPointIdentity); + return _manifest.AssemblyIdentity != null; + } + + private void SetItemAttributes(ITaskItem item, BaseReference file) + { + string targetPath = item.GetMetadata(ItemMetadataNames.targetPath); + if (!String.IsNullOrEmpty(targetPath)) + file.TargetPath = targetPath; + else + file.TargetPath = Path.IsPathRooted(file.SourcePath) || file.SourcePath.StartsWith("..", StringComparison.Ordinal) ? Path.GetFileName(file.SourcePath) : file.SourcePath; + file.Group = item.GetMetadata("Group"); + file.IsOptional = !String.IsNullOrEmpty(file.Group); + if (Util.CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) >= 0) + file.IncludeHash = ConvertUtil.ToBoolean(item.GetMetadata("IncludeHash"), true); + } + + protected internal virtual bool ValidateInputs() + { + bool valid = true; + if (!String.IsNullOrEmpty(_assemblyName) && !Util.IsValidAssemblyName(_assemblyName)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "AssemblyName"); + valid = false; + } + if (!String.IsNullOrEmpty(_assemblyVersion) && !Util.IsValidVersion(_assemblyVersion, 4)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "AssemblyVersion"); + valid = false; + } + if (!String.IsNullOrEmpty(_targetCulture) && !Util.IsValidCulture(_targetCulture)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "TargetCulture"); + valid = false; + } + if (!String.IsNullOrEmpty(_platform)) + { + _processorArchitecture = Util.PlatformToProcessorArchitecture(_platform); + if (String.IsNullOrEmpty(_processorArchitecture)) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.InvalidValue", "Platform"); + valid = false; + } + } + return valid; + } + + protected internal virtual bool ValidateOutput() + { + _manifest.Validate(); + if (_manifest.OutputMessages.ErrorCount > 0) + return false; + + // Check length of manifest file name does not exceed maximum... + if (MaxTargetPath > 0) + { + string manifestFileName = Path.GetFileName(OutputManifest.ItemSpec); + if (manifestFileName.Length > MaxTargetPath) + Log.LogWarningWithCodeFromResources("GenerateManifest.TargetPathTooLong", manifestFileName, MaxTargetPath); + } + + return true; + } + + private bool WriteManifest() + { + if (OutputManifest == null) + OutputManifest = new TaskItem(GetDefaultFileName()); + + if (!ValidateOutput()) + return false; + + int t1 = Environment.TickCount; + try + { + ManifestWriter.WriteManifest(_manifest, OutputManifest.ItemSpec, this.TargetFrameworkVersion); + } + catch (Exception ex) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.WriteOutputManifestFailed", OutputManifest.ItemSpec, ex.Message); + return false; + } + + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "GenerateManifestBase.WriteManifest t={0}", Environment.TickCount - t1)); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "Total time to generate manifest '{1}': t={0}", Environment.TickCount - _startTime, Path.GetFileName(OutputManifest.ItemSpec))); + return true; + } + } +} diff --git a/src/XMakeTasks/GenerateResource.cs b/src/XMakeTasks/GenerateResource.cs new file mode 100644 index 00000000000..6a4daba76bd --- /dev/null +++ b/src/XMakeTasks/GenerateResource.cs @@ -0,0 +1,3775 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Xml; +using System.Runtime.InteropServices; +using System.Configuration; +using System.Security; +using System.ComponentModel.Design; +using System.Runtime.Remoting; + +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif +using System.Runtime.Versioning; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines the "GenerateResource" MSBuild task, which enables using resource APIs + /// to transform resource files. + /// + [RequiredRuntime("v2.0")] + public sealed partial class GenerateResource : TaskExtension + { + #region Fields + + // This cache helps us track the linked resource files listed inside of a resx resource file + private ResGenDependencies _cache; + + // This is where we store the list of input files/sources + private ITaskItem[] _sources = null; + + // Indicates whether the resource reader should use the source file's + // directory to resolve relative file paths. + private bool _useSourcePath = false; + + // This is needed for the actual items from the project + private ITaskItem[] _references = null; + + // Any additional inputs to dependency checking. + private ITaskItem[] _additionalInputs = null; + + // This is the path/name of the dependency cache file + private ITaskItem _stateFile = null; + + // This list is all of the resource file(s) generated by the task + private ITaskItem[] _outputResources = null; + + // List of those output resources that were not actually created, due to an error + private ArrayList _unsuccessfullyCreatedOutFiles = new ArrayList(); + + // Storage for names of *all files* written to disk. + private ArrayList _filesWritten = new ArrayList(); + + // StronglyTypedLanguage + private string _stronglyTypedLanguage = null; + + // StronglyTypedNamespace + private string _stronglyTypedNamespace = null; + + // StronglyTypedManifestPrefix + private string _stronglyTypedManifestPrefix = null; + + // StronglyTypedClassName + private string _stronglyTypedClassName = null; + + // StronglyTypedFileName + private string _stronglyTypedFileName = null; + + // Whether the STR class should have public members; default is false + private bool _publicClass = false; + + // Did the CodeDOM succeed when creating any Strongly Typed Resource class? + private bool _stronglyTypedResourceSuccessfullyCreated = false; + + // When true, a separate AppDomain is always created. + private bool _neverLockTypeAssemblies = false; + + private bool _foundNewestUncorrelatedInputWriteTime = false; + + private DateTime _newestUncorrelatedInputWriteTime; + + // The targets may pass in the path to the SDKToolsPath. If so this should be used to generate the commandline + // for logging purposes. Also, when ExecuteAsTool is true, it determines where the system goes looking for resgen.exe + private string _sdkToolsPath; + + // True if the resource generation should be sent out-of-proc to resgen.exe; false otherwise. Defaults to true + // because we want to execute out-of-proc when ToolsVersion is < 4.0, and the earlier targets files don't know + // about this property. + private bool _executeAsTool = true; + + // Path to resgen.exe + private string _resgenPath; + + // table of already seen types by their typename + // note the use of the ordinal comparer that matches the case sensitive Type.GetType usage + private Dictionary _typeTable = new Dictionary(StringComparer.Ordinal); + + /// + /// Table of aliases for types defined in resx / resw files + /// Ordinal comparer matches ResXResourceReader's use of a HashTable. + /// + private Dictionary _aliases = new Dictionary(StringComparer.Ordinal); + + // Our calculation is not quite correct. Using a number substantially less than 32768 in order to + // be sure we don't exceed it. + private static int s_maximumCommandLength = 28000; + + // Contains the list of paths from which inputs will not be taken into account during up-to-date check. + private ITaskItem[] _excludedInputPaths; + + /// + /// The task items that we remoted across the appdomain boundary + /// we use this list to disconnect the task items once we're done. + /// + private List _remotedTaskItems; + + /// + /// Satellite input assemblies. + /// + private List _satelliteInputs; + + #endregion // fields + + #region Properties + + /// + /// The names of the items to be converted. The extension must be one of the + /// following: .txt, .resx or .resources. + /// + [Required] + [Output] + public ITaskItem[] Sources + { + set { _sources = value; } + get { return _sources; } + } + + /// + /// Indicates whether the resource reader should use the source file's directory to + /// resolve relative file paths. + /// + public bool UseSourcePath + { + set { _useSourcePath = value; } + get { return _useSourcePath; } + } + + /// + /// Resolves types in ResX files (XML resources) for Strongly Typed Resources + /// + public ITaskItem[] References + { + set { _references = value; } + get { return _references; } + } + + /// + /// Additional inputs to the dependency checking done by this task. For example, + /// the project and targets files typically should be inputs, so that if they are updated, + /// all resources are regenerated. + /// + public ITaskItem[] AdditionalInputs + { + set { _additionalInputs = value; } + get { return _additionalInputs; } + } + + /// + /// This is the path/name of the file containing the dependency cache + /// + public ITaskItem StateFile + { + set { _stateFile = value; } + get { return _stateFile; } + } + + /// + /// The name(s) of the resource file to create. If the user does not specify this + /// attribute, the task will append a .resources extension to each input filename + /// argument and write the file to the directory that contains the input file. + /// Includes any output files that were already up to date, but not any output files + /// that failed to be written due to an error. + /// + [Output] + public ITaskItem[] OutputResources + { + set { _outputResources = value; } + get { return _outputResources; } + } + + /// + /// Storage for names of *all files* written to disk. This is part of the implementation + /// for Clean, and contains the OutputResources items and the StateFile item. + /// Includes any output files that were already up to date, but not any output files + /// that failed to be written due to an error. + /// + [Output] + public ITaskItem[] FilesWritten + { + get + { + return (ITaskItem[])_filesWritten.ToArray(typeof(ITaskItem)); + } + } + + /// + /// The language to use when generating the class source for the strongly typed resource. + /// This parameter must match exactly one of the languages used by the CodeDomProvider. + /// + public string StronglyTypedLanguage + { + set + { + // Since this string is passed directly into the framework, we don't want to + // try to validate it -- that might prevent future expansion of supported languages. + _stronglyTypedLanguage = value; + } + get { return _stronglyTypedLanguage; } + } + + /// + /// Specifies the namespace to use for the generated class source for the + /// strongly typed resource. If left blank, no namespace is used. + /// + public string StronglyTypedNamespace + { + set { _stronglyTypedNamespace = value; } + get { return _stronglyTypedNamespace; } + } + + /// + /// Specifies the resource namespace or manifest prefix to use in the generated + /// class source for the strongly typed resource. + /// + public string StronglyTypedManifestPrefix + { + set { _stronglyTypedManifestPrefix = value; } + get { return _stronglyTypedManifestPrefix; } + } + + /// + /// Specifies the class name for the strongly typed resource class. If left blank, the base + /// name of the resource file is used. + /// + [Output] + public string StronglyTypedClassName + { + set { _stronglyTypedClassName = value; } + get { return _stronglyTypedClassName; } + } + + /// + /// Specifies the filename for the source file. If left blank, the name of the class is + /// used as the base filename, with the extension dependent on the language. + /// + [Output] + public string StronglyTypedFileName + { + set { _stronglyTypedFileName = value; } + get { return _stronglyTypedFileName; } + } + + /// + /// Specifies whether the strongly typed class should be created public (with public methods) + /// instead of the default internal. Analogous to resgen.exe's /publicClass switch. + /// + public bool PublicClass + { + set { _publicClass = value; } + get { return _publicClass; } + } + + /// + /// Whether this rule is generating .resources files or extracting .ResW files from assemblies. + /// Requires some additional input filtering. + /// + public bool ExtractResWFiles + { + get; + set; + } + + /// + /// (default = false) + /// When true, a new AppDomain is always created to evaluate the .resx files. + /// When false, a new AppDomain is created only when it looks like a user's + /// assembly is referenced by the .resx. + /// + public bool NeverLockTypeAssemblies + { + set { _neverLockTypeAssemblies = value; } + get { return _neverLockTypeAssemblies; } + } + + /// + /// Even though the generate resource task will do the processing in process, a logging message is still generated. This logging message + /// will include the path to the windows SDK. Since the targets now will pass in the Windows SDK path we should use this for logging. + /// + public string SdkToolsPath + { + get { return _sdkToolsPath; } + set { _sdkToolsPath = value; } + } + + /// + /// Property to allow multitargeting of ResolveComReferences: If true, tlbimp.exe and + /// aximp.exe from the appropriate target framework will be run out-of-proc to generate + /// the necessary wrapper assemblies. + /// + public bool ExecuteAsTool + { + set { _executeAsTool = value; } + get { return _executeAsTool; } + } + + /// + /// Array of equals-separated pairs of environment + /// variables that should be passed to the spawned resgen.exe, + /// in addition to (or selectively overriding) the regular environment block. + /// These aren't currently used when resgen is run in-process. + /// + public string[] EnvironmentVariables + { + get; + set; + } + + /// + /// That set of paths from which tracked inputs will be ignored during + /// Up to date checking + /// + public ITaskItem[] ExcludedInputPaths + { + get { return _excludedInputPaths; } + set { _excludedInputPaths = value; } + } + + /// + /// Property used to set whether tracked incremental build will be used. If true, + /// incremental build is turned on; otherwise, a rebuild will be forced. + /// + public bool MinimalRebuildFromTracking + { + get + { + // not using tracking anymore + return false; + } + + set + { + // do nothing + } + } + + /// + /// True if we should be tracking file access patterns - necessary for incremental + /// build support. + /// + public bool TrackFileAccess + { + get + { + // not using tracking anymore + return false; + } + set + { + // do nothing + } + } + + /// + /// Names of the read tracking logs. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public ITaskItem[] TLogReadFiles + { + get + { + return new ITaskItem[0]; + } + } + + /// + /// Names of the write tracking logs. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "TLog", Justification = "Has now shipped as public API; plus it's unclear whether 'Tlog' or 'TLog' is the preferred casing")] + public ITaskItem[] TLogWriteFiles + { + get + { + return new ITaskItem[0]; + } + } + + /// + /// Intermediate directory into which the tracking logs from running this task will be placed. + /// + public string TrackerLogDirectory + { + get + { + return String.Empty; + } + set + { + // do nothing + } + } + + /// + /// Microsoft.Build.Utilities.ExecutableType of ResGen.exe. Used to determine whether or not + /// Tracker.exe needs to be used to spawn ResGen.exe. If empty, uses a heuristic to determine + /// a default architecture. + /// + public string ToolArchitecture + { + get + { + return String.Empty; + } + + set + { + // do nothing + } + } + + /// + /// Path to the appropriate .NET Framework location that contains FileTracker.dll. If set, the user + /// takes responsibility for making sure that the bitness of the FileTracker.dll that they pass matches + /// the bitness of the ResGen.exe that they intend to use. If not set, the task decides the appropriate + /// location based on the current .NET Framework version. + /// + /// + /// Should only need to be used in partial or full checked in toolset scenarios. + /// + public string TrackerFrameworkPath + { + get + { + return String.Empty; + } + + set + { + // do nothing + } + } + + /// + /// Path to the appropriate Windows SDK location that contains Tracker.exe. If set, the user takes + /// responsibility for making sure that the bitness of the Tracker.exe that they pass matches the + /// bitness of the ResGen.exe that they intend to use. If not set, the task decides the appropriate + /// location based on the current Windows SDK. + /// + /// + /// Should only need to be used in partial or full checked in toolset scenarios. + /// + public string TrackerSdkPath + { + get + { + return String.Empty; + } + + set + { + // do nothing + } + } + + /// + /// Where to extract ResW files. (Could be the intermediate directory.) + /// + public string OutputDirectory + { + get; + set; + } + + #endregion // properties + + /// + /// Simple public constructor. + /// + public GenerateResource() + { + // do nothing + } + + /// + /// Logs a Resgen.exe command line that indicates what parameters were + /// passed to this task. Since this task is replacing Resgen, and we used + /// to log the Resgen.exe command line, we need to continue logging an + /// equivalent command line. + /// + /// + /// + private void LogResgenCommandLine(List inputFiles, List outputFiles) + { + CommandLineBuilderExtension commandLineBuilder = new CommandLineBuilderExtension(); + + // start the command line with the path to Resgen.exe + commandLineBuilder.AppendFileNameIfNotNull(Path.Combine(_resgenPath, "resgen.exe")); + + GenerateResGenCommandLineWithoutResources(commandLineBuilder); + + if (StronglyTypedLanguage == null) + { + // append the resources to compile + for (int i = 0; i < inputFiles.Count; ++i) + { + if (!ExtractResWFiles) + { + commandLineBuilder.AppendFileNamesIfNotNull + ( + new string[] { inputFiles[i].ItemSpec, outputFiles[i].ItemSpec }, + "," + ); + } + else + { + commandLineBuilder.AppendFileNameIfNotNull(inputFiles[i].ItemSpec); + } + } + } + else + { + // append the resource to compile + commandLineBuilder.AppendFileNamesIfNotNull(inputFiles.ToArray(), " "); + commandLineBuilder.AppendFileNamesIfNotNull(outputFiles.ToArray(), " "); + + // append the strongly-typed resource details + commandLineBuilder.AppendSwitchIfNotNull + ( + "/str:", + new string[] { StronglyTypedLanguage, StronglyTypedNamespace, StronglyTypedClassName, StronglyTypedFileName }, + "," + ); + } + + Log.LogCommandLine(MessageImportance.Low, commandLineBuilder.ToString()); + } + + /// + /// Generate the parts of the resgen command line that are don't involve resgen.exe itself or the + /// resources to be generated. + /// + /// + /// Expects resGenCommand to be non-null -- otherwise, it doesn't get passed back to the caller, so it's + /// useless anyway. + /// + /// + private void GenerateResGenCommandLineWithoutResources(CommandLineBuilderExtension resGenCommand) + { + // Throw an internal error, since this method should only ever get called by other aspects of this task, not + // anything that the user touches. + ErrorUtilities.VerifyThrowInternalNull(resGenCommand, "resGenCommand"); + + // append the /useSourcePath flag if requested. + if (UseSourcePath) + { + resGenCommand.AppendSwitch("/useSourcePath"); + } + + // append the /publicClass flag if requested + if (PublicClass) + { + resGenCommand.AppendSwitch("/publicClass"); + } + + // append the references, if any + if (References != null) + { + foreach (ITaskItem reference in References) + { + resGenCommand.AppendSwitchIfNotNull("/r:", reference); + } + } + + // append /compile switch if not creating strongly typed class + if (String.IsNullOrEmpty(StronglyTypedLanguage)) + { + resGenCommand.AppendSwitch("/compile"); + } + } + + /// + /// This is the main entry point for the GenerateResource task. + /// + /// true, if task executes successfully + public override bool Execute() + { + bool outOfProcExecutionSucceeded = true; +#if (!STANDALONEBUILD) + using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildGenerateResourceBegin, CodeMarkerEvent.perfMSBuildGenerateResourceEnd)) +#endif + { + // If we're extracting ResW files from assemblies (instead of building resources), + // our Sources can contain PDB's, pictures, and other non-DLL's. Prune that list. + // .NET Framework assemblies are not included. However, other Microsoft ones + // such as MSTestFramework may be included (resolved from GetSDKReferenceFiles). + if (ExtractResWFiles && Sources != null) + { + _satelliteInputs = new List(); + + List newSources = new List(); + foreach (ITaskItem item in Sources) + { + if (item.ItemSpec.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + if (item.ItemSpec.EndsWith(".resources.dll", StringComparison.OrdinalIgnoreCase)) + { + _satelliteInputs.Add(item); + } + else + { + newSources.Add(item); + } + } + } + Sources = newSources.ToArray(); + } + + + // If there are no sources to process, just return (with success) and report the condition. + if ((Sources == null) || (Sources.Length == 0)) + { + Log.LogMessageFromResources(MessageImportance.Low, "GenerateResource.NoSources"); + // Indicate we generated nothing + OutputResources = null; + return true; + } + + if (!ValidateParameters()) + { + // Indicate we generated nothing + OutputResources = null; + return false; + } + + // In the case that OutputResources wasn't set, build up the outputs by transforming the Sources + // However if we are extracting ResW files, we cannot easily tell which files we'll produce up front, + // without loading each assembly. + if (!ExtractResWFiles && !CreateOutputResourcesNames()) + { + // Indicate we generated nothing + OutputResources = null; + return false; + } + + List inputsToProcess; + List outputsToProcess; + List cachedOutputFiles; // For incremental builds, this is the set of already-existing, up to date files. + + GetResourcesToProcess(out inputsToProcess, out outputsToProcess, out cachedOutputFiles); + + if (inputsToProcess.Count == 0 && !Log.HasLoggedErrors) + { + if (cachedOutputFiles.Count > 0) + { + OutputResources = cachedOutputFiles.ToArray(); + } + + Log.LogMessageFromResources("GenerateResource.NothingOutOfDate"); + } + else + { + if (!ComputePathToResGen()) + { + // unable to compute the path to resgen.exe and that is necessary to + // continue forward, so return now. + return false; + } + + if (ExecuteAsTool) + { + outOfProcExecutionSucceeded = GenerateResourcesUsingResGen(inputsToProcess, outputsToProcess); + } + else // Execute in-proc (or in a separate appdomain if necessary) + { + // Log equivalent command line as this is a convenient way to log all the references, etc + // even though we're not actually running resgen.exe + LogResgenCommandLine(inputsToProcess, outputsToProcess); + + // Figure out whether a separate AppDomain is required because an assembly would be locked. + bool needSeparateAppDomain = NeedSeparateAppDomain(); + + AppDomain appDomain = null; + ProcessResourceFiles process = null; + + try + { + if (needSeparateAppDomain) + { + // we're going to be remoting across the appdomain boundary, so + // create the list that we'll use to disconnect the taskitems once we're done + _remotedTaskItems = new List(); + + appDomain = AppDomain.CreateDomain + ( + "generateResourceAppDomain", + null, + AppDomain.CurrentDomain.SetupInformation + ); + + object obj = appDomain.CreateInstanceFromAndUnwrap + ( + typeof(ProcessResourceFiles).Module.FullyQualifiedName, + typeof(ProcessResourceFiles).FullName + ); + + Type processType = obj.GetType(); + ErrorUtilities.VerifyThrow(processType == typeof(ProcessResourceFiles), "Somehow got a wrong and possibly incompatible type for ProcessResourceFiles."); + + process = (ProcessResourceFiles)obj; + + RecordItemsForDisconnectIfNecessary(_references); + RecordItemsForDisconnectIfNecessary(inputsToProcess); + RecordItemsForDisconnectIfNecessary(outputsToProcess); + } + else + { + process = new ProcessResourceFiles(); + } + + process.Run(Log, _references, inputsToProcess, _satelliteInputs, outputsToProcess, UseSourcePath, + StronglyTypedLanguage, _stronglyTypedNamespace, _stronglyTypedManifestPrefix, + StronglyTypedFileName, StronglyTypedClassName, PublicClass, + ExtractResWFiles, OutputDirectory); + + this.StronglyTypedClassName = process.StronglyTypedClassName; // in case a default was chosen + this.StronglyTypedFileName = process.StronglyTypedFilename; // in case a default was chosen + _stronglyTypedResourceSuccessfullyCreated = process.StronglyTypedResourceSuccessfullyCreated; + if (null != process.UnsuccessfullyCreatedOutFiles) + { + foreach (string item in process.UnsuccessfullyCreatedOutFiles) + { + _unsuccessfullyCreatedOutFiles.Add(item); + } + } + + if (ExtractResWFiles) + { + ITaskItem[] outputResources = process.ExtractedResWFiles.ToArray(); + if (needSeparateAppDomain) + { + // Ensure we can unload the other AppDomain, yet still use the + // ITaskItems we got back. Clone them. + outputResources = CloneValuesInThisAppDomain(outputResources); + } + + if (cachedOutputFiles.Count > 0) + { + OutputResources = new ITaskItem[outputResources.Length + cachedOutputFiles.Count]; + outputResources.CopyTo(OutputResources, 0); + cachedOutputFiles.CopyTo(OutputResources, outputResources.Length); + } + else + { + OutputResources = outputResources; + } + + // Get portable library cache info (and if needed, marshal it to this AD). + List portableLibraryCacheInfo = process.PortableLibraryCacheInfo; + for (int i = 0; i < portableLibraryCacheInfo.Count; i++) + { + _cache.UpdatePortableLibrary(portableLibraryCacheInfo[i]); + } + } + + process = null; + } + finally + { + if (needSeparateAppDomain && appDomain != null) + { + Log.MarkAsInactive(); + + AppDomain.Unload(appDomain); + process = null; + appDomain = null; + + // if we've been asked to remote these items then + // we need to disconnect them from .NET Remoting now we're all done with them + if (_remotedTaskItems != null) + { + foreach (ITaskItem item in _remotedTaskItems) + { + if (item is MarshalByRefObject) + { + // Tell remoting to forget connections to the taskitem + RemotingServices.Disconnect((MarshalByRefObject)item); + } + } + } + + _remotedTaskItems = null; + } + } + } + } + + // And now we serialize the cache to save our resgen linked file resolution for later use. + WriteStateFile(); + + RemoveUnsuccessfullyCreatedResourcesFromOutputResources(); + + RecordFilesWritten(); + } + + return !Log.HasLoggedErrors && outOfProcExecutionSucceeded; + } + + /// + /// For setting OutputResources and ensuring it can be read after the second AppDomain has been unloaded. + /// + /// ITaskItems in another AppDomain + /// + private static ITaskItem[] CloneValuesInThisAppDomain(IList remoteValues) + { + ITaskItem[] clonedOutput = new ITaskItem[remoteValues.Count]; + for (int i = 0; i < remoteValues.Count; i++) + { + clonedOutput[i] = new TaskItem(remoteValues[i]); + } + + return clonedOutput; + } + + /// + /// Remember this TaskItem so that we can disconnect it when this Task has finished executing + /// Only if we're passing TaskItems to another AppDomain is this necessary. This call + /// Will make that determination for you. + /// + private void RecordItemsForDisconnectIfNecessary(IEnumerable items) + { + if (_remotedTaskItems != null && items != null) + { + // remember that we need to disconnect these items + _remotedTaskItems.AddRange(items); + } + } + + /// + /// Computes the path to ResGen.exe for use in logging and for passing to the + /// nested ResGen task. + /// + /// True if the path is found (or it doesn't matter because we're executing in memory), false otherwise + private bool ComputePathToResGen() + { + _resgenPath = null; + + if (String.IsNullOrEmpty(_sdkToolsPath)) + { + _resgenPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version35); + + if (null == _resgenPath && ExecuteAsTool) + { + Log.LogErrorWithCodeFromResources("General.PlatformSDKFileNotFound", "resgen.exe", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version35), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version35)); + } + } + else + { + _resgenPath = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, Microsoft.Build.Utilities.ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, "Resgen.exe", Log, ExecuteAsTool); + } + + if (null == _resgenPath && !ExecuteAsTool) + { + // if Resgen.exe is not installed, just use the filename + _resgenPath = String.Empty; + return true; + } + + // We may be passing this to the ResGen task (wrapper around resgen.exe), in which case + // we want to pass just the path -- ResGen will attach the "resgen.exe" onto the end + // itself. + if (_resgenPath != null) + { + _resgenPath = Path.GetDirectoryName(_resgenPath); + } + + return _resgenPath != null; + } + + /// + /// Wrapper around the call to the ResGen task that handles setting up the + /// task to run properly. + /// + /// Array of names of inputs to be processed + /// Array of output names corresponding to the inputs + private bool GenerateResourcesUsingResGen(List inputsToProcess, List outputsToProcess) + { + bool resGenSucceeded = false; + + if (StronglyTypedLanguage != null) + { + resGenSucceeded = GenerateStronglyTypedResourceUsingResGen(inputsToProcess, outputsToProcess); + } + else + { + resGenSucceeded = TransformResourceFilesUsingResGen(inputsToProcess, outputsToProcess); + } + + return resGenSucceeded; + } + + /// + /// Given an instance of the ResGen task with everything but the strongly typed + /// resource-related parameters filled out, execute the task and return the result + /// + /// The task to execute. + private bool TransformResourceFilesUsingResGen(List inputsToProcess, List outputsToProcess) + { + ErrorUtilities.VerifyThrow(inputsToProcess.Count != 0, "There should be resource files to process"); + ErrorUtilities.VerifyThrow(inputsToProcess.Count == outputsToProcess.Count, "The number of inputs and outputs should be equal"); + + bool succeeded = true; + + // We need to do a whole lot of work to make sure that we're not overrunning the command line ... UNLESS + // we're running ResGen 4.0 or later, which supports response files. + if (!_resgenPath.Equals(Path.GetDirectoryName(NativeMethodsShared.GetLongFilePath(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version35))), StringComparison.OrdinalIgnoreCase)) + { + ResGen resGen = CreateResGenTaskWithDefaultParameters(); + + resGen.InputFiles = inputsToProcess.ToArray(); + resGen.OutputFiles = outputsToProcess.ToArray(); + + ITaskItem[] outputFiles = resGen.OutputFiles; + + succeeded = resGen.Execute(); + + if (!succeeded) + { + foreach (ITaskItem outputFile in outputFiles) + { + if (!File.Exists(outputFile.ItemSpec)) + { + _unsuccessfullyCreatedOutFiles.Add(outputFile.ItemSpec); + } + } + } + } + else + { + int initialResourceIndex = 0; + int numberOfResourcesToAdd = 0; + bool doneProcessingResources = false; + CommandLineBuilderExtension resourcelessCommandBuilder = new CommandLineBuilderExtension(); + string resourcelessCommand = null; + + GenerateResGenCommandLineWithoutResources(resourcelessCommandBuilder); + + if (resourcelessCommandBuilder.Length > 0) + { + resourcelessCommand = resourcelessCommandBuilder.ToString(); + } + + while (!doneProcessingResources) + { + numberOfResourcesToAdd = CalculateResourceBatchSize(inputsToProcess, outputsToProcess, resourcelessCommand, initialResourceIndex); + ResGen resGen = CreateResGenTaskWithDefaultParameters(); + + resGen.InputFiles = inputsToProcess.GetRange(initialResourceIndex, numberOfResourcesToAdd).ToArray(); + resGen.OutputFiles = outputsToProcess.GetRange(initialResourceIndex, numberOfResourcesToAdd).ToArray(); + + ITaskItem[] outputFiles = resGen.OutputFiles; + + bool thisBatchSucceeded = resGen.Execute(); + + // This batch failed, so add the failing resources from this batch to the list of failures + if (!thisBatchSucceeded) + { + foreach (ITaskItem outputFile in outputFiles) + { + if (!File.Exists(outputFile.ItemSpec)) + { + _unsuccessfullyCreatedOutFiles.Add(outputFile.ItemSpec); + } + } + } + + initialResourceIndex += numberOfResourcesToAdd; + doneProcessingResources = initialResourceIndex == inputsToProcess.Count; + succeeded = succeeded && thisBatchSucceeded; + } + } + + return succeeded; + } + + /// + /// Given the list of inputs and outputs, returns the number of resources (starting at the provided initial index) + /// that can fit onto the commandline without exceeding MaximumCommandLength. + /// + private int CalculateResourceBatchSize(List inputsToProcess, List outputsToProcess, string resourcelessCommand, int initialResourceIndex) + { + CommandLineBuilderExtension currentCommand = new CommandLineBuilderExtension(); + + if (!String.IsNullOrEmpty(resourcelessCommand)) + { + currentCommand.AppendTextUnquoted(resourcelessCommand); + } + + int i = initialResourceIndex; + while (currentCommand.Length < s_maximumCommandLength && i < inputsToProcess.Count) + { + currentCommand.AppendFileNamesIfNotNull + ( + new ITaskItem[] { inputsToProcess[i], outputsToProcess[i] }, + "," + ); + i++; + } + + int numberOfResourcesToAdd = 0; + if (currentCommand.Length <= s_maximumCommandLength) + { + // We've successfully added all the rest. + numberOfResourcesToAdd = i - initialResourceIndex; + } + else + { + // The last one added tossed us over the edge. + numberOfResourcesToAdd = i - initialResourceIndex - 1; + } + + return numberOfResourcesToAdd; + } + + /// + /// Given an instance of the ResGen task with everything but the strongly typed + /// resource-related parameters filled out, execute the task and return the result + /// + /// The task to execute. + private bool GenerateStronglyTypedResourceUsingResGen(List inputsToProcess, List outputsToProcess) + { + ErrorUtilities.VerifyThrow(inputsToProcess.Count == 1 && outputsToProcess.Count == 1, "For STR, there should only be one input and one output."); + + ResGen resGen = CreateResGenTaskWithDefaultParameters(); + + resGen.InputFiles = inputsToProcess.ToArray(); + resGen.OutputFiles = outputsToProcess.ToArray(); + + resGen.StronglyTypedLanguage = StronglyTypedLanguage; + resGen.StronglyTypedNamespace = StronglyTypedNamespace; + resGen.StronglyTypedClassName = StronglyTypedClassName; + resGen.StronglyTypedFileName = StronglyTypedFileName; + + // Save the output file name -- ResGen will delete failing files + ITaskItem outputFile = resGen.OutputFiles[0]; + + _stronglyTypedResourceSuccessfullyCreated = resGen.Execute(); + + if (!_stronglyTypedResourceSuccessfullyCreated && (resGen.OutputFiles == null || resGen.OutputFiles.Length == 0)) + { + _unsuccessfullyCreatedOutFiles.Add(outputFile.ItemSpec); + } + + // now need to set the defaults (if defaults were chosen) so that they can be + // consumed by outside users + StronglyTypedClassName = resGen.StronglyTypedClassName; + StronglyTypedFileName = resGen.StronglyTypedFileName; + + return _stronglyTypedResourceSuccessfullyCreated; + } + + /// + /// Factoring out the setting of the default parameters to the + /// ResGen task. + /// + /// + private ResGen CreateResGenTaskWithDefaultParameters() + { + ResGen resGen = new ResGen(); + + resGen.BuildEngine = BuildEngine; + resGen.SdkToolsPath = _resgenPath; + resGen.PublicClass = PublicClass; + resGen.References = References; + resGen.UseSourcePath = UseSourcePath; + resGen.EnvironmentVariables = EnvironmentVariables; + + return resGen; + } + + /// + /// Check for parameter errors. + /// + /// true if parameters are valid + private bool ValidateParameters() + { + // make sure that if the output resources were set, they exactly match the number of input sources + if ((OutputResources != null) && (OutputResources.Length != Sources.Length)) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", Sources.Length, OutputResources.Length, "Sources", "OutputResources"); + return false; + } + + // Creating an STR is triggered merely by setting the language + if (_stronglyTypedLanguage != null) + { + // Like Resgen.exe, only a single Sources is allowed if you are generating STR. + // Otherwise, each STR class overwrites the previous one. In theory we could generate separate + // STR classes for each input, but then the class name and file name parameters would have to be vectors. + if (Sources.Length != 1) + { + Log.LogErrorWithCodeFromResources("GenerateResource.STRLanguageButNotExactlyOneSourceFile"); + return false; + } + } + else + { + if (StronglyTypedClassName != null || StronglyTypedNamespace != null || StronglyTypedFileName != null || StronglyTypedManifestPrefix != null) + { + // We have no language to generate a STR, but nevertheless the user passed us a class, + // namespace, and/or filename. Let them know that they probably wanted to pass a language too. + Log.LogErrorWithCodeFromResources("GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + return false; + } + } + + if (ExtractResWFiles && ExecuteAsTool) + { + // This combination isn't currently supported, because ResGen may produce some not-easily-predictable + // set of ResW files and we don't have any logic to get that set of files back into GenerateResource + // at the moment. This could be solved fixed with some engineering effort. + Log.LogErrorWithCodeFromResources("GenerateResource.ExecuteAsToolAndExtractResWNotSupported"); + return false; + } + + return true; + } + + /// + /// Returns true if everything is up to date and we don't need to do any work. + /// + /// + private void GetResourcesToProcess(out List inputsToProcess, out List outputsToProcess, out List cachedOutputFiles) + { + // First we look to see if we have a resgen linked files cache. If so, then we can use that + // cache to speed up processing. + ReadStateFile(); + + bool nothingOutOfDate = true; + inputsToProcess = new List(); + outputsToProcess = new List(); + cachedOutputFiles = new List(); + + // decide what sources we need to build + for (int i = 0; i < Sources.Length; ++i) + { + if (ExtractResWFiles) + { + // We can't cheaply predict the output files, since that would require + // loading each assembly. So don't even try guessing what they will be. + // However, our cache will sometimes record all the info we need (for incremental builds). + string sourceFileName = Sources[i].ItemSpec; + ResGenDependencies.PortableLibraryFile library = _cache.TryGetPortableLibraryInfo(sourceFileName); + if (library != null && library.AllOutputFilesAreUpToDate()) + { + AppendCachedOutputTaskItems(library, cachedOutputFiles); + } + else + { + inputsToProcess.Add(Sources[i]); + } + + continue; + } + + // Attributes from input items are forwarded to output items. + Sources[i].CopyMetadataTo(OutputResources[i]); + Sources[i].SetMetadata("OutputResource", OutputResources[i].ItemSpec); + + if (!File.Exists(Sources[i].ItemSpec)) + { + // Error but continue with the files that do exist + Log.LogErrorWithCodeFromResources("GenerateResource.ResourceNotFound", Sources[i].ItemSpec); + _unsuccessfullyCreatedOutFiles.Add(OutputResources[i].ItemSpec); + } + else + { + // check to see if the output resources file (and, if it is a .resx, any linked files) + // is up to date compared to the input file + if (ShouldRebuildResgenOutputFile(Sources[i].ItemSpec, OutputResources[i].ItemSpec)) + { + nothingOutOfDate = false; + inputsToProcess.Add(Sources[i]); + outputsToProcess.Add(OutputResources[i]); + } + } + } + + // If the STR class file is out of date (for example, it's missing) we don't want to skip + // resource generation. + if (_stronglyTypedLanguage != null) + { + // We're generating a STR class file, so there must be exactly one input resource file. + // If that resource file itself is out of date, the STR class file is going to get generated anyway. + if (nothingOutOfDate && File.Exists(Sources[0].ItemSpec)) + { + GetStronglyTypedResourceToProcess(ref inputsToProcess, ref outputsToProcess); + } + } + } + + /// + /// Given a cached portable library that is up to date, create ITaskItems to represent the output of the task, as if we did real work. + /// + /// The portable library cache entry to extract output files & metadata from. + /// List of output files produced from the cache. + private void AppendCachedOutputTaskItems(ResGenDependencies.PortableLibraryFile library, List cachedOutputFiles) + { + foreach (string outputFileName in library.OutputFiles) + { + ITaskItem item = new TaskItem(outputFileName); + item.SetMetadata("ResourceIndexName", library.AssemblySimpleName); + if (library.NeutralResourceLanguage != null) + { + item.SetMetadata("NeutralResourceLanguage", library.NeutralResourceLanguage); + } + + cachedOutputFiles.Add(item); + } + } + + /// + /// Checks if this list contain any duplicates. Do this so we don't have any races where we have two + /// threads trying to write to the same file simultaneously. + /// + /// A list that may have duplicates + /// Were there duplicates? + private bool ContainsDuplicates(IList originalList) + { + HashSet set = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ITaskItem item in originalList) + { + try + { + // Get the fully qualified path, to ensure two file names don't end up pointing to the same directory. + if (!set.Add(item.GetMetadata("FullPath"))) + { + Log.LogErrorWithCodeFromResources("GenerateResource.DuplicateOutputFilenames", item.ItemSpec); + return true; + } + } + catch (InvalidOperationException e) + { + Log.LogErrorWithCodeFromResources("GenerateResource.InvalidFilename", item.ItemSpec, e.Message); + // Returning true causes us to not continue executing. + return true; + } + } + + return false; + } + + /// + /// Determines if the given output file is up to date with respect to the + /// the given input file by comparing timestamps of the two files as well as + /// (if the source is a .resx) the linked files inside the .resx file itself + /// + /// + /// + private bool ShouldRebuildResgenOutputFile(string sourceFilePath, string outputFilePath) + { + // See if any uncorrelated inputs are missing before checking source and output file timestamps. + // Don't consult the uncorrelated input file times if we haven't already got them: + // typically, it's the .resx's that are out of date so we want to check those first. + if (_foundNewestUncorrelatedInputWriteTime && GetNewestUncorrelatedInputWriteTime() == DateTime.MaxValue) + { + // An uncorrelated input is missing; need to build + return true; + } + + FileInfo outputInfo = FileUtilities.GetFileInfoNoThrow(outputFilePath); + + // Quick check to see if any uncorrelated input is newer in which case we can avoid checking source file timestamp + if (_foundNewestUncorrelatedInputWriteTime && outputInfo != null && GetNewestUncorrelatedInputWriteTime() > outputInfo.LastWriteTime) + { + // An uncorrelated input is newer, need to build + return true; + } + + FileInfo sourceInfo = FileUtilities.GetFileInfoNoThrow(sourceFilePath); + + if (!String.Equals(Path.GetExtension(sourceFilePath), ".resx", StringComparison.OrdinalIgnoreCase) && + !String.Equals(Path.GetExtension(sourceFilePath), ".resw", StringComparison.OrdinalIgnoreCase)) + { + // If source file is NOT a .resx, for example a .restext file, + // timestamp checking is simple, because there's no linked files to examine, and no references. + return NeedToRebuildSourceFile(sourceInfo, outputInfo); + } + + // OK, we have a .resx file + + // PERF: Regardless of whether the outputFile exists, if the source file is a .resx + // go ahead and retrieve it from the cache. This is because we want the cache + // to be populated so that incremental builds can be fast. + // Note that this is a trade-off: clean builds will be slightly slower. However, + // for clean builds we're about to read in this very same .resx file so reading + // it now will page it in. The second read should be cheap. + ResGenDependencies.ResXFile resxFileInfo = null; + try + { + resxFileInfo = _cache.GetResXFileInfo(sourceFilePath); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedIoOrXmlException(e)) + { + throw; + } + + // Return true, so that resource processing will display the error + // No point logging a duplicate error here as well + return true; + } + + // if the .resources file is out of date even just with respect to the .resx or + // the additional inputs, we don't need to go to the point of checking the linked files. + if (NeedToRebuildSourceFile(sourceInfo, outputInfo)) + { + return true; + } + + // The .resources is up to date with respect to the .resx file - + // we need to compare timestamps for each linked file inside + // the .resx file itself + if (resxFileInfo.LinkedFiles != null) + { + foreach (string linkedFilePath in resxFileInfo.LinkedFiles) + { + FileInfo linkedFileInfo = FileUtilities.GetFileInfoNoThrow(linkedFilePath); + + if (linkedFileInfo == null) + { + // Linked file is missing - force a build, so that resource generation + // will produce a nice error message + return true; + } + + if (linkedFileInfo.LastWriteTime > outputInfo.LastWriteTime) + { + // Linked file is newer, need to build + return true; + } + } + } + + return false; + } + + /// + /// Returns true if the output does not exist, if the provided source is newer than the output, + /// or if any of the set of additional inputs is newer than the output. Otherwise, returns false. + /// + private bool NeedToRebuildSourceFile(FileInfo sourceInfo, FileInfo outputInfo) + { + if (sourceInfo == null || outputInfo == null) + { + // Source file is missing - force a build, so that resource generation + // will produce a nice error message; or output file is missing, + // need to build it + return true; + } + + if (sourceInfo.LastWriteTime > outputInfo.LastWriteTime) + { + // Source file is newer, need to build + return true; + } + + if (GetNewestUncorrelatedInputWriteTime() > outputInfo.LastWriteTime) + { + // An uncorrelated input is newer, need to build + return true; + } + + return false; + } + + /// + /// Add the strongly typed resource to the set of resources to process if it is out of date. + /// + private void GetStronglyTypedResourceToProcess(ref List inputsToProcess, ref List outputsToProcess) + { + bool needToRebuildSTR = false; + + // The resource file isn't out of date. So check whether the STR class file is. + try + { + if (StronglyTypedFileName == null) + { + CodeDomProvider provider = null; + + if (ProcessResourceFiles.TryCreateCodeDomProvider(Log, StronglyTypedLanguage, out provider)) + { + StronglyTypedFileName = ProcessResourceFiles.GenerateDefaultStronglyTypedFilename(provider, OutputResources[0].ItemSpec); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + // Now that we've logged an error, we know we're not going to do anything more, so + // don't bother to correctly populate the inputs / outputs. + Log.LogErrorWithCodeFromResources("GenerateResource.CannotWriteSTRFile", StronglyTypedFileName, e.Message); + _unsuccessfullyCreatedOutFiles.Add(OutputResources[0].ItemSpec); + _stronglyTypedResourceSuccessfullyCreated = false; + return; + } + + // Now we have the filename, check if it's up to date + FileInfo sourceInfo = FileUtilities.GetFileInfoNoThrow(Sources[0].ItemSpec); + FileInfo outputInfo = FileUtilities.GetFileInfoNoThrow(StronglyTypedFileName); + + if (sourceInfo == null || outputInfo == null) + { + // Source file is missing - force a build, so that resource generation + // will produce a nice error message; or output file is missing, + // need to build it + needToRebuildSTR = true; + } + else if (sourceInfo.LastWriteTime > outputInfo.LastWriteTime) + { + // Source file is newer, need to build + needToRebuildSTR = true; + } + + if (needToRebuildSTR) + { + // We know there's only one input, just make sure it gets processed and that will cause + // the STR class file to get updated + if (inputsToProcess.Count == 0) + { + inputsToProcess.Add(Sources[0]); + outputsToProcess.Add(OutputResources[0]); + } + } + else + { + // If the STR class file is up to date and was skipped then we + // should consider the file written and add it to FilesWritten output + _stronglyTypedResourceSuccessfullyCreated = true; + } + } + + /// + /// Returns the newest last write time among the references and additional inputs. + /// If any do not exist, returns DateTime.MaxValue so that resource generation produces a nice error. + /// + private DateTime GetNewestUncorrelatedInputWriteTime() + { + if (!_foundNewestUncorrelatedInputWriteTime) + { + _newestUncorrelatedInputWriteTime = DateTime.MinValue; + + // Check the timestamp of each of the passed-in references to find the newest + if (this.References != null) + { + foreach (ITaskItem reference in this.References) + { + // Get a FileInfo, so we can get existence and write time in a single + // disk access + FileInfo referenceInfo = FileUtilities.GetFileInfoNoThrow(reference.ItemSpec); + + if (referenceInfo == null) + { + // File does not exist: force a build to produce an error message + _foundNewestUncorrelatedInputWriteTime = true; + return DateTime.MaxValue; + } + + if (referenceInfo.LastWriteTime > _newestUncorrelatedInputWriteTime) + { + _newestUncorrelatedInputWriteTime = referenceInfo.LastWriteTime; + } + } + } + + // Check the timestamp of each of the additional inputs to see if one's even newer + if (this.AdditionalInputs != null) + { + foreach (ITaskItem additionalInput in this.AdditionalInputs) + { + // Get a FileInfo, so we can get existence and write time in a single + // disk access + FileInfo additionalInputInfo = FileUtilities.GetFileInfoNoThrow(additionalInput.ItemSpec); + + if (additionalInputInfo == null) + { + // File does not exist: force a build to produce an error message + _foundNewestUncorrelatedInputWriteTime = true; + return DateTime.MaxValue; + } + + if (additionalInputInfo.LastWriteTime > _newestUncorrelatedInputWriteTime) + { + _newestUncorrelatedInputWriteTime = additionalInputInfo.LastWriteTime; + } + } + } + + _foundNewestUncorrelatedInputWriteTime = true; + } + + return _newestUncorrelatedInputWriteTime; + } + + /// + /// Make the decision about whether a separate AppDomain is needed. + /// If this algorithm is unsure about whether a separate AppDomain is + /// needed, it should always err on the side of returning 'true'. This + /// is because a separate AppDomain, while slow to create, is always safe. + /// + /// The list of .resx files. + /// + private bool NeedSeparateAppDomain() + { + if (NeverLockTypeAssemblies) + { + Log.LogMessageFromResources(MessageImportance.Low, "GenerateResource.SeparateAppDomainBecauseNeverLockTypeAssembliesTrue"); + return true; + } + + foreach (ITaskItem source in _sources) + { + string extension = Path.GetExtension(source.ItemSpec); + + if (String.Compare(extension, ".resources.dll", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".dll", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".exe", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + + if (String.Compare(extension, ".resx", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".resw", StringComparison.OrdinalIgnoreCase) == 0) + { + XmlTextReader reader = null; + string name = null; + + try + { + reader = new XmlTextReader(source.ItemSpec); + reader.DtdProcessing = DtdProcessing.Ignore; + + while (reader.Read()) + { + // Look for the section + if (reader.NodeType == XmlNodeType.Element) + { + if (String.Equals(reader.Name, "data", StringComparison.OrdinalIgnoreCase)) + { + // Is there an attribute called type? + string typeName = reader.GetAttribute("type"); + name = reader.GetAttribute("name"); + + if (typeName != null) + { + Type type; + + // It is likely that we've seen this type before + // we'll try our table of previously seen types + // since it is *much* faster to do that than + // call Type.GetType needlessly! + if (!_typeTable.TryGetValue(typeName, out type)) + { + string resolvedTypeName = typeName; + + // This type name might be an alias, so first resolve that if any. + int indexOfSeperator = typeName.IndexOf(",", StringComparison.InvariantCulture); + + if (indexOfSeperator != -1) + { + string typeFromTypeName = typeName.Substring(0, indexOfSeperator); + string maybeAliasFromTypeName = typeName.Substring(indexOfSeperator + 1); + + if (!String.IsNullOrWhiteSpace(maybeAliasFromTypeName)) + { + maybeAliasFromTypeName = maybeAliasFromTypeName.Trim(); + + string fullName = null; + if (_aliases.TryGetValue(maybeAliasFromTypeName, out fullName)) + { + resolvedTypeName = typeFromTypeName + ", " + fullName; + } + } + } + + // Can this type be found in the GAC? + type = Type.GetType(resolvedTypeName, throwOnError: false, ignoreCase: false); + + // Remember our resolved type + _typeTable[typeName] = type; + } + + if (type == null) + { + // If the type could not be found in the GAC, then we're going to need + // to load the referenced assemblies (those passed in through the + // "References" parameter during the building of this .RESX. Therefore, + // we should create a separate app-domain, so that those assemblies + // can be unlocked when the task is finished. + // The type didn't start with "System." so return true. + Log.LogMessageFromResources + ( + MessageImportance.Low, + "GenerateResource.SeparateAppDomainBecauseOfType", + (name == null) ? String.Empty : name, + typeName, + source.ItemSpec, + ((IXmlLineInfo)reader).LineNumber + ); + + return true; + } + + // If there's a type, we don't need to look at any mimetype + continue; + } + + // DDB #9825. + // There's no type attribute on this -- if there's a MimeType, it's a serialized + // object of unknown type, and we have to assume it will need a new app domain. + // The possible mimetypes ResXResourceReader understands are: + // + // application/x-microsoft.net.object.binary.base64 + // application/x-microsoft.net.object.bytearray.base64 + // application/x-microsoft.net.object.binary.base64 + // application/x-microsoft.net.object.soap.base64 + // text/microsoft-urt/binary-serialized/base64 + // text/microsoft-urt/psuedoml-serialized/base64 + // text/microsoft-urt/soap-serialized/base64 + // + // Of these, application/x-microsoft.net.object.bytearray.base64 usually has a type attribute + // as well; ResxResourceReader will use that Type, which may not need a new app domain. So + // if there's a type attribute, we don't look at mimetype. + // + // If there is a mimetype and no type, we can't tell the type without deserializing and loading it, + // so we assume a new appdomain is needed. + // + // Actually, if application/x-microsoft.net.object.bytearray.base64 doesn't have a Type attribute, + // ResxResourceReader assumes System.String, but for safety we don't assume that here. + + string mimeType = reader.GetAttribute("mimetype"); + + if (mimeType != null) + { + if (NeedSeparateAppDomainBasedOnSerializedType(reader)) + { + Log.LogMessageFromResources + ( + MessageImportance.Low, + "GenerateResource.SeparateAppDomainBecauseOfMimeType", + (name == null) ? String.Empty : name, + mimeType, + source.ItemSpec, + ((IXmlLineInfo)reader).LineNumber + ); + + return true; + } + } + } + else if (String.Equals(reader.Name, "assembly", StringComparison.OrdinalIgnoreCase)) + { + string alias = reader.GetAttribute("alias"); + string fullName = reader.GetAttribute("name"); + + if (!String.IsNullOrWhiteSpace(alias) && !String.IsNullOrWhiteSpace(fullName)) + { + alias = alias.Trim(); + fullName = fullName.Trim(); + + _aliases[alias] = fullName; + } + } + } + } + } + catch (XmlException e) + { + Log.LogMessageFromResources + ( + MessageImportance.Low, + "GenerateResource.SeparateAppDomainBecauseOfExceptionLineNumber", + source.ItemSpec, + ((IXmlLineInfo)reader).LineNumber, + e.Message + ); + + return true; + } + catch (SerializationException e) + { + Log.LogMessageFromResources + ( + MessageImportance.Low, + "GenerateResource.SeparateAppDomainBecauseOfErrorDeserializingLineNumber", + source.ItemSpec, + (name == null) ? String.Empty : name, + ((IXmlLineInfo)reader).LineNumber, + e.Message + ); + + return true; + } + catch (Exception e) + { + // DDB#9819 + // Customers have reported the following exceptions coming out of this method's call to GetType(): + // System.Runtime.InteropServices.COMException (0x8000000A): The data necessary to complete this operation is not yet available. (Exception from HRESULT: 0x8000000A) + // System.NullReferenceException: Object reference not set to an instance of an object. + // System.InvalidOperationException: Collection was modified; enumeration operation may not execute. + // We don't have reproes, but probably the right thing to do is to assume a new app domain is needed on almost any exception. + // Any problem loading the type will get logged later when the resource reader tries it. + // + // XmlException or an IO exception is also possible from an invalid input file. + if (ExceptionHandling.IsCriticalException(e)) + throw; + + // If there was any problem parsing the .resx then log a message and + // fall back to using a separate AppDomain. + Log.LogMessageFromResources + ( + MessageImportance.Low, + "GenerateResource.SeparateAppDomainBecauseOfException", + source.ItemSpec, + e.Message + ); + + // In case we need more information from the customer (given this has been heavily reported + // and we don't understand it properly) let the usual debug switch dump the stack. + if (Environment.GetEnvironmentVariable("MSBUILDDEBUG") == "1") + { + Log.LogErrorFromException(e, /* stack */ true, /* inner exceptions */ true, null); + } + + return true; + } + finally + { + reader.Close(); + } + } + } + + return false; + } + + /// + /// Finds the "value" element expected to be the next element read from the supplied reader. + /// Deserializes the data content in order to figure out whether it implies a new app domain + /// should be used to process resources. + /// + private bool NeedSeparateAppDomainBasedOnSerializedType(XmlTextReader reader) + { + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + if (!String.Equals(reader.Name, "value", StringComparison.OrdinalIgnoreCase)) + { + // claimed it was serialized, but didn't have a ; + // return true to err on side of caution + return true; + } + + // Found "value" element + string data = reader.ReadElementContentAsString(); + + bool isSerializedObjectLoadable = DetermineWhetherSerializedObjectLoads(data); + + // If it's not loadable, it's presumably a user type, so create a new app domain + return !isSerializedObjectLoadable; + } + } + + // We didn't find any element at all -- the .resx is malformed. + // Return true to err on the side of caution. Error will appear later. + return true; + } + + /// + /// Deserializes a base64 block from a resx in order to figure out if its type is in the GAC. + /// Because we're not providing any assembly resolution callback, deserialization + /// will attempt to load the object's type using fusion rules, which essentially means + /// the GAC. So, if the object is null, it's definitely not in the GAC. + /// + private bool DetermineWhetherSerializedObjectLoads(string data) + { + byte[] serializedData = ByteArrayFromBase64WrappedString(data); + + BinaryFormatter binaryFormatter = new BinaryFormatter(); + + using (MemoryStream memoryStream = new MemoryStream(serializedData)) + { + object result = binaryFormatter.Deserialize(memoryStream); + + return (result != null); + } + } + + /// + /// Chars that should be ignored in the nicely justified block of base64 + /// + private static readonly char[] s_specialChars = new char[] { ' ', '\r', '\n' }; + + /// + /// Turns the nicely justified block of base64 found in a resx into a byte array. + /// Copied from fx\src\winforms\managed\system\winforms\control.cs + /// + private static byte[] ByteArrayFromBase64WrappedString(string text) + { + if (text.IndexOfAny(s_specialChars) != -1) + { + StringBuilder sb = new StringBuilder(text.Length); + for (int i = 0; i < text.Length; i++) + { + switch (text[i]) + { + case ' ': + case '\r': + case '\n': + break; + default: + sb.Append(text[i]); + break; + } + } + return Convert.FromBase64String(sb.ToString()); + } + else + { + return Convert.FromBase64String(text); + } + } + + /// + /// Make sure that OutputResources has 1 file name for each name in Sources. + /// + private bool CreateOutputResourcesNames() + { + if (OutputResources == null) + { + OutputResources = new ITaskItem[Sources.Length]; + int i = 0; + try + { + for (i = 0; i < Sources.Length; ++i) + { + OutputResources[i] = new TaskItem(Path.ChangeExtension(Sources[i].ItemSpec, ".resources")); + } + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("GenerateResource.InvalidFilename", Sources[i].ItemSpec, e.Message); + return false; + } + } + + // Now check for duplicates. + if (ContainsDuplicates(OutputResources)) + { + return false; + } + + return true; + } + + /// + /// Remove any output resources that we didn't successfully create (due to error) from the + /// OutputResources list. Keeps the ordering of OutputResources the same. + /// + /// + /// Q: Why didn't we keep a "successfully created" list instead, like in the Copy task does, which + /// would save us doing the removal algorithm below? + /// A: Because we want the ordering of OutputResources to be the same as the ordering passed in. + /// Some items (the up to date ones) would be added to the successful output list first, and the other items + /// are added during processing, so the ordering would change. We could fix that up, but it's better to do + /// the fix up only in the rarer error case. If there were no errors, the algorithm below skips. + private void RemoveUnsuccessfullyCreatedResourcesFromOutputResources() + { + // Normally, there aren't any unsuccessful conversions. + if (_unsuccessfullyCreatedOutFiles == null || + _unsuccessfullyCreatedOutFiles.Count == 0) + { + return; + } + + ErrorUtilities.VerifyThrow(OutputResources != null && OutputResources.Length != 0, "Should be at least one output resource"); + + // We only get here if there was at least one resource generation error. + ITaskItem[] temp = new ITaskItem[OutputResources.Length - _unsuccessfullyCreatedOutFiles.Count]; + int copied = 0; + int removed = 0; + for (int i = 0; i < Sources.Length; i++) + { + // Check whether this one is in the bad list. + if (removed < _unsuccessfullyCreatedOutFiles.Count && + _unsuccessfullyCreatedOutFiles.Contains(OutputResources[i].ItemSpec)) + { + removed++; + Sources[i].SetMetadata("OutputResource", String.Empty); + } + else + { + // Copy it to the okay list. + temp[copied] = OutputResources[i]; + copied++; + } + } + OutputResources = temp; + } + + /// + /// Record the list of file that will be written to disk. + /// + private void RecordFilesWritten() + { + // Add any output resources that were successfully created, + // or would have been if they weren't already up to date (important for Clean) + if (this.OutputResources != null) + { + foreach (ITaskItem item in this.OutputResources) + { + _filesWritten.Add(item); + } + } + + // Add any state file + if (StateFile != null && StateFile.ItemSpec.Length > 0) + { + // It's possible the file wasn't actually written (eg the path was invalid) + // We can't easily tell whether that happened here, and I think it's fine to add it anyway. + _filesWritten.Add(StateFile); + } + + // Only add the STR class file if the CodeDOM succeeded, or if it exists and is up to date. + // Otherwise, we probably didn't write it successfully. + if (_stronglyTypedResourceSuccessfullyCreated) + { + if (StronglyTypedFileName == null) + { + CodeDomProvider provider = null; + + if (ProcessResourceFiles.TryCreateCodeDomProvider(Log, StronglyTypedLanguage, out provider)) + { + StronglyTypedFileName = ProcessResourceFiles.GenerateDefaultStronglyTypedFilename( + provider, OutputResources[0].ItemSpec); + } + } + + _filesWritten.Add(new TaskItem(this.StronglyTypedFileName)); + } + } + + /// + /// Read the state file if able. + /// + private void ReadStateFile() + { + // First we look to see if we have a resgen linked files cache. If so, then we can use that + // cache to speed up processing. If there's a problem reading the cache file (or it + // just doesn't exist, then this method will return a brand new cache object. + + // This method eats IO Exceptions + _cache = ResGenDependencies.DeserializeCache((StateFile == null) ? null : StateFile.ItemSpec, UseSourcePath, Log); + ErrorUtilities.VerifyThrow(_cache != null, "We did not create a cache!"); + } + + /// + /// Write the state file if there is one to be written. + /// + private void WriteStateFile() + { + if (_cache.IsDirty) + { + // And now we serialize the cache to save our resgen linked file resolution for later use. + _cache.SerializeCache((StateFile == null) ? null : StateFile.ItemSpec, Log); + } + } + } + + /// + /// This class handles the processing of source resource files into compiled resource files. + /// Its designed to be called from a separate AppDomain so that any files locked by ResXResourceReader + /// can be released. + /// + internal sealed class ProcessResourceFiles : MarshalByRefObject + { + #region fields + /// + /// List of readers used for input. + /// + private List _readers = new List(); + + /// + /// Logger for any messages or errors + /// + private TaskLoggingHelper _logger = null; + + /// + /// Language for the strongly typed resources. + /// + private string _stronglyTypedLanguage; + + /// + /// Filename for the strongly typed resources. + /// Getter provided since the processor may choose a default. + /// + internal string StronglyTypedFilename + { + get + { + return _stronglyTypedFilename; + } + } + private string _stronglyTypedFilename; + + /// + /// Namespace for the strongly typed resources. + /// + private string _stronglyTypedNamespace; + + /// + /// ResourceNamespace for the strongly typed resources. + /// + private string _stronglyTypedResourcesNamespace; + + /// + /// Class name for the strongly typed resources. + /// Getter provided since the processor may choose a default. + /// + internal string StronglyTypedClassName + { + get + { + return _stronglyTypedClassName; + } + } + private string _stronglyTypedClassName; + + /// + /// Whether the fields in the STR class should be public, rather than internal + /// + private bool _stronglyTypedClassIsPublic; + + /// + /// Class that gets called by the ResxResourceReader to resolve references + /// to assemblies within the .RESX. + /// + private AssemblyNamesTypeResolutionService _typeResolver = null; + + /// + /// Handles assembly resolution events. + /// + private ResolveEventHandler _eventHandler; + + /// + /// The referenced assemblies + /// + private ITaskItem[] _assemblyFiles; + + /// + /// The AssemblyNameExtensions for each of the referenced assemblies in "assemblyFiles". + /// This is populated lazily. + /// + private AssemblyNameExtension[] _assemblyNames; + + /// + /// List of input files to process. + /// + private List _inFiles; + + /// + /// List of satellite input files to process. + /// + private List _satelliteInFiles; + + /// + /// List of output files to process. + /// + private List _outFiles; + + /// + /// Whether we are extracting ResW files from an assembly, instead of creating .resources files. + /// + private bool _extractResWFiles; + + /// + /// Where to write extracted ResW files. + /// + private string _resWOutputDirectory; + + internal List ExtractedResWFiles + { + get + { + if (_extractedResWFiles == null) + { + _extractedResWFiles = new List(); + } + return _extractedResWFiles; + } + } + private List _extractedResWFiles; + + /// + /// Record all the information about outputs here to avoid future incremental builds. + /// + internal List PortableLibraryCacheInfo + { + get { return _portableLibraryCacheInfo; } + } + private List _portableLibraryCacheInfo; + + /// + /// List of output files that we failed to create due to an error. + /// See note in RemoveUnsuccessfullyCreatedResourcesFromOutputResources() + /// + internal ArrayList UnsuccessfullyCreatedOutFiles + { + get + { + if (null == _unsuccessfullyCreatedOutFiles) + { + _unsuccessfullyCreatedOutFiles = new ArrayList(); + } + return _unsuccessfullyCreatedOutFiles; + } + } + private ArrayList _unsuccessfullyCreatedOutFiles; + + /// + /// Whether we successfully created the STR class + /// + internal bool StronglyTypedResourceSuccessfullyCreated + { + get + { + return _stronglyTypedResourceSuccessfullyCreated; + } + } + private bool _stronglyTypedResourceSuccessfullyCreated = false; + + /// + /// Indicates whether the resource reader should use the source file's + /// directory to resolve relative file paths. + /// + private bool _useSourcePath = false; + + #endregion + + /// + /// Process all files. + /// + internal void Run(TaskLoggingHelper log, ITaskItem[] assemblyFilesList, List inputs, List satelliteInputs, List outputs, bool sourcePath, + string language, string namespacename, string resourcesNamespace, string filename, string classname, bool publicClass, + bool extractingResWFiles, string resWOutputDirectory) + { + _logger = log; + _assemblyFiles = assemblyFilesList; + _inFiles = inputs; + _satelliteInFiles = satelliteInputs; + _outFiles = outputs; + _useSourcePath = sourcePath; + _stronglyTypedLanguage = language; + _stronglyTypedNamespace = namespacename; + _stronglyTypedResourcesNamespace = resourcesNamespace; + _stronglyTypedFilename = filename; + _stronglyTypedClassName = classname; + _stronglyTypedClassIsPublic = publicClass; + _readers = new List(); + _extractResWFiles = extractingResWFiles; + _resWOutputDirectory = resWOutputDirectory; + _portableLibraryCacheInfo = new List(); + + // If references were passed in, we will have to give the ResxResourceReader an object + // by which it can resolve types that are referenced from within the .RESX. + if ((_assemblyFiles != null) && (_assemblyFiles.Length > 0)) + { + _typeResolver = new AssemblyNamesTypeResolutionService(_assemblyFiles); + } + + try + { + // Install assembly resolution event handler. + _eventHandler = new ResolveEventHandler(ResolveAssembly); + AppDomain.CurrentDomain.AssemblyResolve += _eventHandler; + + for (int i = 0; i < _inFiles.Count; ++i) + { + string outputSpec = _extractResWFiles ? resWOutputDirectory : _outFiles[i].ItemSpec; + if (!ProcessFile(_inFiles[i].ItemSpec, outputSpec)) + { + // Since we failed, remove items from OutputResources. Note when extracting ResW + // files, we won't have added anything to OutputResources up front though. + if (!_extractResWFiles) + { + UnsuccessfullyCreatedOutFiles.Add(outputSpec); + } + } + } + } + finally + { + // Remove the event handler. + AppDomain.CurrentDomain.AssemblyResolve -= _eventHandler; + _eventHandler = null; + } + } + + /// + /// Callback to resolve assembly names to assemblies. + /// + /// + /// + /// + internal Assembly ResolveAssembly(object sender, ResolveEventArgs args) + { + AssemblyNameExtension requestedAssemblyName = new AssemblyNameExtension(args.Name); + + if (_assemblyFiles != null) + { + // Populate the list of assembly names for all passed-in references if it hasn't + // been populated already. + if (_assemblyNames == null) + { + _assemblyNames = new AssemblyNameExtension[_assemblyFiles.Length]; + for (int i = 0; i < _assemblyFiles.Length; i++) + { + ITaskItem assemblyFile = _assemblyFiles[i]; + _assemblyNames[i] = null; + + if (assemblyFile.ItemSpec != null && File.Exists(assemblyFile.ItemSpec)) + { + string fusionName = assemblyFile.GetMetadata(ItemMetadataNames.fusionName); + if (!String.IsNullOrEmpty(fusionName)) + { + _assemblyNames[i] = new AssemblyNameExtension(fusionName); + } + else + { + // whoever passed us this reference wasn't polite enough to also + // give us a metadata with the fusion name. Trying to load up every + // assembly here would take a lot of time, so just stick the assembly + // file name (which we assume generally maps to the simple name) into + // the list instead. If there's a fusion name that matches, we'll get + // that first; otherwise there's a good chance that if the simple name + // matches the file name, it's a good match. + _assemblyNames[i] = new AssemblyNameExtension(Path.GetFileNameWithoutExtension(assemblyFile.ItemSpec)); + } + } + } + } + + // Loop through all the references passed in, and see if any of them have an assembly + // name that exactly matches the requested one. + for (int i = 0; i < _assemblyNames.Length; i++) + { + AssemblyNameExtension candidateAssemblyName = _assemblyNames[i]; + + if (candidateAssemblyName != null) + { + if (candidateAssemblyName.CompareTo(requestedAssemblyName) == 0) + { + return Assembly.UnsafeLoadFrom(_assemblyFiles[i].ItemSpec); + } + } + } + + // If none of the referenced assembly names matches exactly, try to find one that + // has the same base name. This is here to fix bug where the + // serialized data inside the .RESX referred to the assembly just by the base name, + // omitting the version, culture, publickeytoken information. + for (int i = 0; i < _assemblyNames.Length; i++) + { + AssemblyNameExtension candidateAssemblyName = _assemblyNames[i]; + + if (candidateAssemblyName != null) + { + if (String.Compare(requestedAssemblyName.Name, candidateAssemblyName.Name, StringComparison.CurrentCultureIgnoreCase) == 0) + { + return Assembly.UnsafeLoadFrom(_assemblyFiles[i].ItemSpec); + } + } + } + } + + return null; + } + + #region Code from ResGen.EXE + + /// + /// Read all resources from a file and write to a new file in the chosen format + /// + /// Uses the input and output file extensions to determine their format + /// Input resources file + /// Output resources file + /// True if conversion was successful, otherwise false + private bool ProcessFile(string inFile, string outFileOrDir) + { + Format inFileFormat = GetFormat(inFile); + if (inFileFormat == Format.Error) + { + // GetFormat would have logged an error. + return false; + } + if (inFileFormat != Format.Assembly) // outFileOrDir is a directory when the input file is an assembly + { + Format outFileFormat = GetFormat(outFileOrDir); + if (outFileFormat == Format.Assembly) + { + _logger.LogErrorFromResources("GenerateResource.CannotWriteAssembly", outFileOrDir); + return false; + } + else if (outFileFormat == Format.Error) + { + return false; + } + } + + if (!_extractResWFiles) + { + _logger.LogMessageFromResources("GenerateResource.ProcessingFile", inFile, outFileOrDir); + } + + // Reset state + _readers = new List(); + + try + { + ReadResources(inFile, _useSourcePath, outFileOrDir); + } + catch (ArgumentException ae) + { + if (ae.InnerException is XmlException) + { + XmlException xe = (XmlException)ae.InnerException; + _logger.LogErrorWithCodeFromResources(null, FileUtilities.GetFullPathNoThrow(inFile), xe.LineNumber, xe.LinePosition, 0, 0, "General.InvalidResxFile", xe.Message); + } + else + { + _logger.LogErrorWithCodeFromResources(null, FileUtilities.GetFullPathNoThrow(inFile), 0, 0, 0, 0, "General.InvalidResxFile", ae.Message); + } + return false; + } + catch (TextFileException tfe) + { + // Used to pass back error context from ReadTextResources to here. + _logger.LogErrorWithCodeFromResources(null, tfe.FileName, tfe.LineNumber, tfe.LinePosition, 1, 1, "GenerateResource.MessageTunnel", tfe.Message); + return false; + } + catch (XmlException xe) + { + _logger.LogErrorWithCodeFromResources(null, FileUtilities.GetFullPathNoThrow(inFile), xe.LineNumber, xe.LinePosition, 0, 0, "General.InvalidResxFile", xe.Message); + return false; + } + catch (Exception e) + { + // DDB #9819 + // SerializationException and TargetInvocationException can occur when trying to deserialize a type from a resource format (typically with other exceptions inside) + // This is a bug in the type being serialized, so the best we can do is dump diagnostic information and move on to the next input resource file. + if (e is SerializationException || + e is TargetInvocationException) + { + _logger.LogErrorWithCodeFromResources(null, FileUtilities.GetFullPathNoThrow(inFile), 0, 0, 0, 0, "General.InvalidResxFile", e.Message); + + // Log the stack, so the problem with the type in the .resx is diagnosable by the customer + _logger.LogErrorFromException(e, /* stack */ true, /* inner exceptions */ true, FileUtilities.GetFullPathNoThrow(inFile)); + return false; + } + + if (!ExceptionHandling.NotExpectedException(e)) + { + // Regular IO error + _logger.LogErrorWithCodeFromResources(null, FileUtilities.GetFullPathNoThrow(inFile), 0, 0, 0, 0, "General.InvalidResxFile", e.Message); + return false; + } + + throw; + } + + string currentOutputFile = null; + string currentOutputDirectory = null; + string currentOutputSourceCodeFile = null; + bool currentOutputDirectoryAlreadyExisted = true; + + try + { + if (GetFormat(inFile) == Format.Assembly) + { + // Prepare cache data + ResGenDependencies.PortableLibraryFile library = new ResGenDependencies.PortableLibraryFile(inFile); + List resWFilesForThisAssembly = new List(); + + foreach (ReaderInfo reader in _readers) + { + String currentOutputFileNoPath = reader.outputFileName + ".resw"; + currentOutputFile = null; + currentOutputDirectoryAlreadyExisted = true; + string priDirectory = Path.Combine(outFileOrDir ?? String.Empty, + reader.assemblySimpleName); + currentOutputDirectory = Path.Combine(priDirectory, + reader.cultureName ?? String.Empty); + + if (!Directory.Exists(currentOutputDirectory)) + { + currentOutputDirectoryAlreadyExisted = false; + Directory.CreateDirectory(currentOutputDirectory); + } + currentOutputFile = Path.Combine(currentOutputDirectory, currentOutputFileNoPath); + + // For very long resource names, this directory structure may be too deep. + // If so, assume that the name is so long it will already uniquely distinguish itself. + // However for shorter names we'd still prefer to use the assembly simple name + // in the path to avoid conflicts. + currentOutputFile = EnsurePathIsShortEnough(currentOutputFile, currentOutputFileNoPath, outFileOrDir, reader.cultureName); + + if (currentOutputFile == null) + { + // We couldn't generate a file name short enough to handle this. Fail but continue. + continue; + } + + // Always write the output file here - other logic prevents us from processing this + // file for incremental builds if everything was up to date. + WriteResources(reader, currentOutputFile); + + string escapedOutputFile = EscapingUtilities.Escape(currentOutputFile); + ITaskItem newOutputFile = new TaskItem(escapedOutputFile); + resWFilesForThisAssembly.Add(escapedOutputFile); + newOutputFile.SetMetadata("ResourceIndexName", reader.assemblySimpleName); + library.AssemblySimpleName = reader.assemblySimpleName; + if (reader.fromNeutralResources) + { + newOutputFile.SetMetadata("NeutralResourceLanguage", reader.cultureName); + library.NeutralResourceLanguage = reader.cultureName; + } + ExtractedResWFiles.Add(newOutputFile); + } + + library.OutputFiles = resWFilesForThisAssembly.ToArray(); + _portableLibraryCacheInfo.Add(library); + } + else + { + currentOutputFile = outFileOrDir; + ErrorUtilities.VerifyThrow(_readers.Count == 1, "We have no readers, or we have multiple readers & are ignoring subsequent ones. Num readers: {0}", _readers.Count); + WriteResources(_readers[0], outFileOrDir); + } + + if (_stronglyTypedLanguage != null) + { + try + { + ErrorUtilities.VerifyThrow(_readers.Count == 1, "We have no readers, or we have multiple readers & are ignoring subsequent ones. Num readers: {0}", _readers.Count); + CreateStronglyTypedResources(_readers[0], outFileOrDir, inFile, out currentOutputSourceCodeFile); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + // IO Error + _logger.LogErrorWithCodeFromResources("GenerateResource.CannotWriteSTRFile", _stronglyTypedFilename, e.Message); + + if (File.Exists(outFileOrDir) + && GetFormat(inFile) != Format.Assembly // outFileOrDir is a directory when the input file is an assembly + && GetFormat(outFileOrDir) != Format.Assembly) // Never delete an assembly since we don't ever actually write to assemblies. + { + RemoveCorruptedFile(outFileOrDir); + } + if (currentOutputSourceCodeFile != null) + { + RemoveCorruptedFile(currentOutputSourceCodeFile); + } + return false; + } + } + } + catch (IOException io) + { + if (currentOutputFile != null) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.CannotWriteOutput", FileUtilities.GetFullPathNoThrow(currentOutputFile), io.Message); + if (File.Exists(currentOutputFile)) + { + if (GetFormat(currentOutputFile) != Format.Assembly) // Never delete an assembly since we don't ever actually write to assemblies. + { + RemoveCorruptedFile(currentOutputFile); + } + } + } + + if (currentOutputDirectory != null && + currentOutputDirectoryAlreadyExisted == false) + { // Do not annoy the user by removing an empty directory we did not create. + try + { + Directory.Delete(currentOutputDirectory); // Remove output directory if empty + } + catch (Exception e) + { // Fail silently (we are not even checking if the call to File.Delete succeeded) + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + } + } + return false; + } + catch (Exception e) + { + // DDB #9819 + // SerializationException and TargetInvocationException can occur when trying to serialize a type into a resource format (typically with other exceptions inside) + // This is a bug in the type being serialized, so the best we can do is dump diagnostic information and move on to the next input resource file. + if (e is SerializationException || + e is TargetInvocationException) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.CannotWriteOutput", FileUtilities.GetFullPathNoThrow(inFile), e.Message); // Input file is more useful to log + + // Log the stack, so the problem with the type in the .resx is diagnosable by the customer + _logger.LogErrorFromException(e, /* stack */ true, /* inner exceptions */ true, FileUtilities.GetFullPathNoThrow(inFile)); + return false; + } + + if (!ExceptionHandling.NotExpectedException(e)) + { + // Regular IO error + _logger.LogErrorWithCodeFromResources("GenerateResource.CannotWriteOutput", FileUtilities.GetFullPathNoThrow(currentOutputFile), e.Message); + return false; + } + + throw; + } + + return true; + } + + /// + /// For very long resource names, the directory structure we generate may be too deep. + /// If so, assume that the name is so long it will already uniquely distinguish itself. + /// However for shorter names we'd still prefer to use the assembly simple name + /// in the path to avoid conflicts. + /// + /// The current path name + /// The current file name without a path. + /// Output directory path + /// culture for this resource + /// The current path or a shorter one. + private string EnsurePathIsShortEnough(string currentOutputFile, string currentOutputFileNoPath, string outputDirectory, string cultureName) + { + // File names >= 260 characters won't work. File names of exactly 259 characters are odd though. + // They seem to work with Notepad and Windows Explorer, but not with MakePri. They don't work + // reliably with cmd's dir command either (depending on whether you use absolute or relative paths + // and whether there are quotes around the name). + const int EffectiveMaxPath = 258; // Everything <= EffectiveMaxPath should work well. + bool success = false; + try + { + currentOutputFile = Path.GetFullPath(currentOutputFile); + success = currentOutputFile.Length <= EffectiveMaxPath; + } + catch (PathTooLongException) + { + success = false; + } + + if (!success) + { + string shorterPath = Path.Combine(outputDirectory ?? String.Empty, cultureName ?? String.Empty); + if (!Directory.Exists(shorterPath)) + { + Directory.CreateDirectory(shorterPath); + } + currentOutputFile = Path.Combine(shorterPath, currentOutputFileNoPath); + + // Try again + try + { + currentOutputFile = Path.GetFullPath(currentOutputFile); + success = currentOutputFile.Length <= EffectiveMaxPath; + } + catch (PathTooLongException) + { + success = false; + } + + // Can't do anything more without violating correctness. + if (!success) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.PathTooLong", currentOutputFile); + currentOutputFile = null; + // We've logged an error message. This MSBuild task will fail, but can continue processing other input (to find other errors). + } + } + return currentOutputFile; + } + + /// + /// Remove a corrupted file, with error handling and a warning if we fail. + /// + /// Full path to file to delete + private void RemoveCorruptedFile(string filename) + { + _logger.LogWarningWithCodeFromResources("GenerateResource.CorruptOutput", FileUtilities.GetFullPathNoThrow(filename)); + try + { + File.Delete(filename); + } + catch (Exception deleteException) + { + _logger.LogWarningWithCodeFromResources("GenerateResource.DeleteCorruptOutputFailed", FileUtilities.GetFullPathNoThrow(filename), deleteException.Message); + + if (ExceptionHandling.NotExpectedException(deleteException)) + { + throw; + } + } + } + + /// + /// Figure out the format of an input resources file from the extension + /// + /// Input resources file + /// Resources format + private Format GetFormat(string filename) + { + string extension = String.Empty; + + try + { + extension = Path.GetExtension(filename); + } + catch (ArgumentException ex) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.InvalidFilename", filename, ex.Message); + return Format.Error; + } + + if (String.Compare(extension, ".txt", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".restext", StringComparison.OrdinalIgnoreCase) == 0) + { + return Format.Text; + } + else if (String.Compare(extension, ".resx", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".resw", StringComparison.OrdinalIgnoreCase) == 0) + { + return Format.XML; + } + else if (String.Compare(extension, ".resources.dll", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".dll", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(extension, ".exe", StringComparison.OrdinalIgnoreCase) == 0) + { + return Format.Assembly; + } + else if (String.Compare(extension, ".resources", StringComparison.OrdinalIgnoreCase) == 0) + { + return Format.Binary; + } + else + { + _logger.LogErrorWithCodeFromResources("GenerateResource.UnknownFileExtension", Path.GetExtension(filename), filename); + return Format.Error; + } + } + + /// + /// Text files are just name/value pairs. ResText is the same format + /// with a unique extension to work around some ambiguities with MSBuild + /// ResX is our existing XML format from V1. + /// + private enum Format + { + Text, // .txt or .restext + XML, // .resx + Binary, // .resources + Assembly, // .dll, .exe or .resources.dll + Error, // anything else + } + + /// + /// Reads the resources out of the specified file and populates the + /// resources hashtable. + /// + /// Filename to load + /// Whether to resolve paths in the + /// resources file relative to the resources file location + /// Output file or directory. + private void ReadResources(String filename, bool shouldUseSourcePath, String outFileOrDir) + { + Format format = GetFormat(filename); + + if (format == Format.Assembly) // Multiple input .resources files within one assembly + { + ReadAssemblyResources(filename, outFileOrDir); + } + else + { + ReaderInfo reader = new ReaderInfo(); + _readers.Add(reader); + switch (format) + { + case Format.Text: + ReadTextResources(reader, filename); + break; + + case Format.XML: + ResXResourceReader resXReader = null; + if (_typeResolver != null) + { + resXReader = new ResXResourceReader(filename, _typeResolver); + } + else + { + resXReader = new ResXResourceReader(filename); + } + + if (shouldUseSourcePath) + { + String fullPath = Path.GetFullPath(filename); + resXReader.BasePath = Path.GetDirectoryName(fullPath); + } + // ReadResources closes the reader for us + ReadResources(reader, resXReader, filename); + break; + + case Format.Binary: + ReadResources(reader, new ResourceReader(filename), filename); // closes reader for us + break; + + default: + // We should never get here, we've already checked the format + Debug.Fail("Unknown format " + format.ToString()); + return; + } + _logger.LogMessageFromResources(MessageImportance.Low, "GenerateResource.ReadResourceMessage", reader.resources.Count, filename); + } + } + + /// + /// Reads resources from an assembly. + /// + /// + /// Output file or directory. + /// This should not run for Framework assemblies. + internal void ReadAssemblyResources(String name, String outFileOrDir) + { + // If something else in the solution failed to build... + if (!File.Exists(name)) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.MissingFile", name); + return; + } + + Assembly a = null; + bool mainAssembly = false; + bool failedLoadingCultureInfo = false; + NeutralResourcesLanguageAttribute neutralResourcesLanguageAttribute = null; + AssemblyName assemblyName = null; + + try + { + a = Assembly.UnsafeLoadFrom(name); + assemblyName = a.GetName(); + + if (_extractResWFiles) + { + var targetFrameworkAttribute = a.GetCustomAttribute(); + if ( + targetFrameworkAttribute != null && + ( + targetFrameworkAttribute.FrameworkName.StartsWith("Silverlight,", StringComparison.OrdinalIgnoreCase) || + targetFrameworkAttribute.FrameworkName.StartsWith("WindowsPhone,", StringComparison.OrdinalIgnoreCase) + ) + ) + { + // Skip Silverlight assemblies. + _logger.LogMessageFromResources("GenerateResource.SkippingExtractingFromNonSupportedFramework", name, targetFrameworkAttribute.FrameworkName); + return; + } + + _logger.LogMessageFromResources("GenerateResource.ExtractingResWFiles", name, outFileOrDir); + } + + CultureInfo ci = null; + try + { + ci = assemblyName.CultureInfo; + } + catch (ArgumentException e) + { + _logger.LogWarningWithCodeFromResources(null, name, 0, 0, 0, 0, "GenerateResource.CreatingCultureInfoFailed", e.GetType().Name, e.Message, assemblyName.ToString()); + failedLoadingCultureInfo = true; + } + + if (!failedLoadingCultureInfo) + { + mainAssembly = ci.Equals(CultureInfo.InvariantCulture); + neutralResourcesLanguageAttribute = CheckAssemblyCultureInfo(name, assemblyName, ci, a, mainAssembly); + } // if (!failedLoadingCultureInfo) + } + catch (BadImageFormatException) + { + // If we're extracting ResW files, this task is being run on all DLL's referred to by the project. + // That may potentially include C++ libraries & immersive (non-portable) class libraries, which don't have resources. + // We can't easily filter those. We can simply skip them. + return; + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + throw; + _logger.LogErrorWithCodeFromResources("GenerateResource.CannotLoadAssemblyLoadFromFailed", name, e); + } + + if (a != null) + { + String[] resources = a.GetManifestResourceNames(); + CultureInfo satCulture = null; + String expectedExt = null; + if (!failedLoadingCultureInfo) + { + satCulture = assemblyName.CultureInfo; + if (!satCulture.Equals(CultureInfo.InvariantCulture)) + expectedExt = '.' + satCulture.Name + ".resources"; + } + + foreach (String resName in resources) + { + if (!resName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) // Skip non-.resources assembly blobs + continue; + + if (mainAssembly) + { + if (CultureInfo.InvariantCulture.CompareInfo.IsSuffix(resName, ".en-US.resources", CompareOptions.IgnoreCase)) + { + _logger.LogErrorFromResources("GenerateResource.ImproperlyBuiltMainAssembly", resName, name); + continue; + } + + if (neutralResourcesLanguageAttribute == null) + { + _logger.LogWarningWithCodeFromResources(null, name, 0, 0, 0, 0, "GenerateResource.MainAssemblyMissingNeutralResourcesLanguage", name); + break; + } + } + else if (!failedLoadingCultureInfo && !CultureInfo.InvariantCulture.CompareInfo.IsSuffix(resName, expectedExt, CompareOptions.IgnoreCase)) + { + _logger.LogErrorFromResources("GenerateResource.ImproperlyBuiltSatelliteAssembly", resName, expectedExt, name); + continue; + } + + try + { + Stream s = a.GetManifestResourceStream(resName); + using (IResourceReader rr = new ResourceReader(s)) + { + ReaderInfo reader = new ReaderInfo(); + if (mainAssembly) + { + reader.fromNeutralResources = true; + reader.assemblySimpleName = assemblyName.Name; + } + else + { + Debug.Assert(assemblyName.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)); + reader.assemblySimpleName = assemblyName.Name.Remove(assemblyName.Name.Length - 10); // Remove .resources from satellite assembly name + } + reader.outputFileName = resName.Remove(resName.Length - 10); // Remove the .resources extension + if (satCulture != null && !String.IsNullOrEmpty(satCulture.Name)) + { + reader.cultureName = satCulture.Name; + } + else if (neutralResourcesLanguageAttribute != null && !String.IsNullOrEmpty(neutralResourcesLanguageAttribute.CultureName)) + { + reader.cultureName = neutralResourcesLanguageAttribute.CultureName; + } + + if (reader.cultureName != null) + { + // Remove the culture from the filename + if (reader.outputFileName.EndsWith("." + reader.cultureName, StringComparison.OrdinalIgnoreCase)) + reader.outputFileName = reader.outputFileName.Remove(reader.outputFileName.Length - (reader.cultureName.Length + 1)); + } + _readers.Add(reader); + + foreach (DictionaryEntry pair in rr) + { + AddResource(reader, (string)pair.Key, pair.Value, resName); + } + } + } + catch (FileNotFoundException) + { + _logger.LogErrorWithCodeFromResources(null, name, 0, 0, 0, 0, "GenerateResource.NoResourcesFileInAssembly", resName); + } + } + } + + var satelliteAssemblies = _satelliteInFiles.Where(ti => ti.GetMetadata("OriginalItemSpec").Equals(name, StringComparison.OrdinalIgnoreCase)); + + foreach (var satelliteAssembly in satelliteAssemblies) + { + ReadAssemblyResources(satelliteAssembly.ItemSpec, outFileOrDir); + } + } + + /// + /// Checks the consistency of the CultureInfo and NeutralResourcesLanguageAttribute settings. + /// + /// Assembly's file name + /// AssemblyName of this assembly + /// Assembly's CultureInfo + private NeutralResourcesLanguageAttribute CheckAssemblyCultureInfo(String name, AssemblyName assemblyName, CultureInfo culture, Assembly a, bool mainAssembly) + { + NeutralResourcesLanguageAttribute neutralResourcesLanguageAttribute = null; + if (mainAssembly) + { + Object[] attrs = a.GetCustomAttributes(typeof(NeutralResourcesLanguageAttribute), false); + if (attrs.Length != 0) + { + neutralResourcesLanguageAttribute = (NeutralResourcesLanguageAttribute)attrs[0]; + bool fallbackToSatellite = neutralResourcesLanguageAttribute.Location == UltimateResourceFallbackLocation.Satellite; + if (!fallbackToSatellite && neutralResourcesLanguageAttribute.Location != UltimateResourceFallbackLocation.MainAssembly) + _logger.LogWarningWithCodeFromResources(null, name, 0, 0, 0, 0, "GenerateResource.UnrecognizedUltimateResourceFallbackLocation", neutralResourcesLanguageAttribute.Location, name); + // This MSBuild task needs to not report an error for main assemblies that don't have managed resources. + } + } + else + { // Satellite assembly, or a mal-formed main assembly + // Additional error checking from ResView. + if (!assemblyName.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarningWithCodeFromResources(null, name, 0, 0, 0, 0, "GenerateResource.SatelliteOrMalformedAssembly", name, culture.Name, assemblyName.Name); + return null; + } + Type[] types = a.GetTypes(); + if (types.Length > 0) + { + _logger.LogWarningWithCodeFromResources("GenerateResource.SatelliteAssemblyContainsCode", name); + } + + if (!ContainsProperlyNamedResourcesFiles(a, false)) + _logger.LogWarningWithCodeFromResources("GenerateResource.SatelliteAssemblyContainsNoResourcesFile", assemblyName.CultureInfo.Name); + } + return neutralResourcesLanguageAttribute; + } + + private static bool ContainsProperlyNamedResourcesFiles(Assembly a, bool mainAssembly) + { + String postfix = mainAssembly ? ".resources" : a.GetName().CultureInfo.Name + ".resources"; + foreach (String manifestResourceName in a.GetManifestResourceNames()) + if (manifestResourceName.EndsWith(postfix, StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + /// + /// Write resources from the resources ArrayList to the specified output file + /// + /// Output resources file + private void WriteResources(ReaderInfo reader, String filename) + { + Format format = GetFormat(filename); + switch (format) + { + case Format.Text: + WriteTextResources(reader, filename); + break; + + case Format.XML: + WriteResources(reader, new ResXResourceWriter(filename)); // closes writer for us + break; + + case Format.Assembly: + _logger.LogErrorFromResources("GenerateResource.CannotWriteAssembly", filename); + break; + + case Format.Binary: + WriteResources(reader, new ResourceWriter(filename)); // closes writer for us + break; + + default: + // We should never get here, we've already checked the format + Debug.Fail("Unknown format " + format.ToString()); + break; + } + } + + /// + /// Create a strongly typed resource class + /// + /// Output resource filename, for defaulting the class filename + /// Input resource filename, for error messages + private void CreateStronglyTypedResources(ReaderInfo reader, String outFile, String inputFileName, out String sourceFile) + { + CodeDomProvider provider = null; + + if (!TryCreateCodeDomProvider(_logger, _stronglyTypedLanguage, out provider)) + { + sourceFile = null; + return; + } + + // Default the class name if we need to + if (_stronglyTypedClassName == null) + { + _stronglyTypedClassName = Path.GetFileNameWithoutExtension(outFile); + } + + // Default the filename if we need to + if (_stronglyTypedFilename == null) + { + _stronglyTypedFilename = GenerateDefaultStronglyTypedFilename(provider, outFile); + } + sourceFile = this.StronglyTypedFilename; + + _logger.LogMessageFromResources("GenerateResource.CreatingSTR", _stronglyTypedFilename); + + // Generate the STR class + String[] errors; + bool generateInternalClass = !_stronglyTypedClassIsPublic; + //StronglyTypedResourcesNamespace can be null and this is ok. + // If it is null then the default namespace (=stronglyTypedNamespace) is used. + CodeCompileUnit ccu = StronglyTypedResourceBuilder.Create( + reader.resourcesHashTable, + _stronglyTypedClassName, + _stronglyTypedNamespace, + _stronglyTypedResourcesNamespace, + provider, + generateInternalClass, + out errors + ); + + CodeGeneratorOptions codeGenOptions = new CodeGeneratorOptions(); + using (TextWriter output = new StreamWriter(_stronglyTypedFilename)) + { + provider.GenerateCodeFromCompileUnit(ccu, output, codeGenOptions); + } + + if (errors.Length > 0) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.ErrorFromCodeDom", inputFileName); + foreach (String error in errors) + { + _logger.LogErrorWithCodeFromResources("GenerateResource.CodeDomError", error); + } + } + else + { + // No errors, and no exceptions - we presumably did create the STR class file + // and it should get added to FilesWritten. So set a flag to indicate this. + _stronglyTypedResourceSuccessfullyCreated = true; + } + } + + /// + /// If no strongly typed resource class filename was specified, we come up with a default based on the + /// input file name and the default language extension. + /// + /// + /// Broken out here so it can be called from GenerateResource class. + /// + /// A CodeDomProvider for the language + /// Name of the output resources file + /// Filename for strongly typed resource class + public static string GenerateDefaultStronglyTypedFilename(CodeDomProvider provider, string outputResourcesFile) + { + return Path.ChangeExtension(outputResourcesFile, provider.FileExtension); + } + + /// + /// Tries to create a CodeDom provider for the specified strongly typed language. If successful, returns true, + /// otherwise returns false. + /// + /// + /// Broken out here so it can be called from GenerateResource class. + /// Not a true "TryXXX" method, as it still throws if it encounters an exception it doesn't expect. + /// + /// The language to create a provider for. + /// The provider in question, if one is successfully created. + /// True if the provider was successfully created, false otherwise. + public static bool TryCreateCodeDomProvider(TaskLoggingHelper logger, string stronglyTypedLanguage, out CodeDomProvider provider) + { + provider = null; + + try + { + provider = CodeDomProvider.CreateProvider(stronglyTypedLanguage); + } + catch (ConfigurationException e) + { + logger.LogErrorWithCodeFromResources("GenerateResource.STRCodeDomProviderFailed", stronglyTypedLanguage, e.Message); + return false; + } + catch (SecurityException e) + { + logger.LogErrorWithCodeFromResources("GenerateResource.STRCodeDomProviderFailed", stronglyTypedLanguage, e.Message); + return false; + } + + return provider != null; + } + + /// + /// Read resources from an XML or binary format file + /// + /// Appropriate IResourceReader + /// Filename, for error messages + private void ReadResources(ReaderInfo readerInfo, IResourceReader reader, String fileName) + { + using (reader) + { + IDictionaryEnumerator resEnum = reader.GetEnumerator(); + while (resEnum.MoveNext()) + { + string name = (string)resEnum.Key; + object value = resEnum.Value; + AddResource(readerInfo, name, value, fileName); + } + } + } + + /// + /// Read resources from a text format file + /// + /// Input resources filename + private void ReadTextResources(ReaderInfo reader, String fileName) + { + // Check for byte order marks in the beginning of the input file, but + // default to UTF-8. + using (LineNumberStreamReader sr = new LineNumberStreamReader(fileName, new UTF8Encoding(true), true)) + { + StringBuilder name = new StringBuilder(255); + StringBuilder value = new StringBuilder(2048); + + int ch = sr.Read(); + while (ch != -1) + { + if (ch == '\n' || ch == '\r') + { + ch = sr.Read(); + continue; + } + + // Skip over commented lines or ones starting with whitespace. + // Support LocStudio INF format's comment char, ';' + if (ch == '#' || ch == '\t' || ch == ' ' || ch == ';') + { + // comment char (or blank line) - skip line. + sr.ReadLine(); + ch = sr.Read(); + continue; + } + // Note that in Beta of version 1 we recommended users should put a [strings] + // section in their file. Now it's completely unnecessary and can + // only cause bugs. We will not parse anything using '[' stuff now + // and we should give a warning about seeing [strings] stuff. + // In V1.1 or V2, we can rip this out completely, I hope. + if (ch == '[') + { + String skip = sr.ReadLine(); + if (skip.Equals("strings]")) + _logger.LogWarningWithCodeFromResources(null, fileName, sr.LineNumber - 1, 1, 0, 0, "GenerateResource.ObsoleteStringsTag"); + else + { + throw new TextFileException(_logger.FormatResourceString("GenerateResource.UnexpectedInfBracket", "[" + skip), fileName, sr.LineNumber - 1, 1); + } + ch = sr.Read(); + continue; + } + + // Read in name + name.Length = 0; + while (ch != '=') + { + if (ch == '\r' || ch == '\n') + throw new TextFileException(_logger.FormatResourceString("GenerateResource.NoEqualsInLine", name), fileName, sr.LineNumber, sr.LinePosition); + + name.Append((char)ch); + ch = sr.Read(); + if (ch == -1) + break; + } + if (name.Length == 0) + throw new TextFileException(_logger.FormatResourceString("GenerateResource.NoNameInLine"), fileName, sr.LineNumber, sr.LinePosition); + + // For the INF file, we must allow a space on both sides of the equals + // sign. Deal with it. + if (name[name.Length - 1] == ' ') + { + name.Length = name.Length - 1; + } + ch = sr.Read(); // move past = + // If it exists, move past the first space after the equals sign. + if (ch == ' ') + ch = sr.Read(); + + // Read in value + value.Length = 0; + + while (ch != -1) + { + // Did we read @"\r" or @"\n"? + bool quotedNewLine = false; + if (ch == '\\') + { + ch = sr.Read(); + switch (ch) + { + case '\\': + // nothing needed + break; + case 'n': + ch = '\n'; + quotedNewLine = true; + break; + case 'r': + ch = '\r'; + quotedNewLine = true; + break; + case 't': + ch = '\t'; + break; + case '"': + ch = '\"'; + break; + case 'u': + char[] hex = new char[4]; + int numChars = 4; + int index = 0; + while (numChars > 0) + { + int n = sr.Read(hex, index, numChars); + if (n == 0) + throw new TextFileException(_logger.FormatResourceString("GenerateResource.InvalidEscape", name.ToString(), (char)ch), fileName, sr.LineNumber, sr.LinePosition); + index += n; + numChars -= n; + } + try + { + ch = (char)UInt16.Parse(new String(hex), NumberStyles.HexNumber, CultureInfo.CurrentCulture); + } + catch (FormatException) + { + // We know about this one... + throw new TextFileException(_logger.FormatResourceString("GenerateResource.InvalidHexEscapeValue", name.ToString(), new String(hex)), fileName, sr.LineNumber, sr.LinePosition); + } + catch (OverflowException) + { + // We know about this one, too... + throw new TextFileException(_logger.FormatResourceString("GenerateResource.InvalidHexEscapeValue", name.ToString(), new String(hex)), fileName, sr.LineNumber, sr.LinePosition); + } + quotedNewLine = (ch == '\n' || ch == '\r'); + break; + + default: + throw new TextFileException(_logger.FormatResourceString("GenerateResource.InvalidEscape", name.ToString(), (char)ch), fileName, sr.LineNumber, sr.LinePosition); + } + } + + // Consume endline... + // Endline can be \r\n or \n. But do not treat a + // quoted newline (ie, @"\r" or @"\n" in text) as a + // real new line. They aren't the end of a line. + if (!quotedNewLine) + { + if (ch == '\r') + { + ch = sr.Read(); + if (ch == -1) + { + break; + } + else if (ch == '\n') + { + ch = sr.Read(); + break; + } + } + else if (ch == '\n') + { + ch = sr.Read(); + break; + } + } + + value.Append((char)ch); + ch = sr.Read(); + } + + // Note that value can be an empty string + AddResource(reader, name.ToString(), value.ToString(), fileName, sr.LineNumber, sr.LinePosition); + } + } + } + + /// + /// Write resources to an XML or binary format resources file. + /// + /// Closes writer automatically + /// Appropriate IResourceWriter + private void WriteResources(ReaderInfo reader, IResourceWriter writer) + { + Exception capturedException = null; + try + { + foreach (Entry entry in reader.resources) + { + string key = entry.name; + object value = entry.value; + writer.AddResource(key, value); + } + } + catch (Exception e) + { + capturedException = e; // Rethrow this after catching exceptions thrown by Close(). + } + finally + { + if (capturedException == null) + { + writer.Close(); // If this throws, exceptions will be caught upstream. + } + else + { + // It doesn't hurt to call Close() twice. In the event of a full disk, we *need* to call Close() twice. + // In that case, the first time we catch an exception indicating that the XML written to disk is malformed, + // specifically an InvalidOperationException: "Token EndElement in state Error would result in an invalid XML document." + try { writer.Close(); } + catch (Exception) { } // We agressively catch all exception types since we already have one we will throw. + // The second time we catch the out of disk space exception. + try { writer.Close(); } + catch (Exception) { } // We agressively catch all exception types since we already have one we will throw. + throw capturedException; // In the event of a full disk, this is an out of disk space IOException. + } + } + } + + /// + /// Write resources to a text format resources file + /// + /// Output resources file + private void WriteTextResources(ReaderInfo reader, String fileName) + { + using (StreamWriter writer = new StreamWriter(fileName, false, Encoding.UTF8)) + { + foreach (Entry entry in reader.resources) + { + String key = entry.name; + Object v = entry.value; + String value = v as String; + if (value == null) + { + _logger.LogErrorWithCodeFromResources(null, fileName, 0, 0, 0, 0, "GenerateResource.OnlyStringsSupported", key, v.GetType().FullName); + } + else + { + // Escape any special characters in the String. + value = value.Replace("\\", "\\\\"); + value = value.Replace("\n", "\\n"); + value = value.Replace("\r", "\\r"); + value = value.Replace("\t", "\\t"); + + writer.WriteLine("{0}={1}", key, value); + } + } + } + } + + /// + /// Add a resource from a text file to the internal data structures + /// + /// Resource name + /// Resource value + /// Input file for messages + /// Line number for messages + /// Column number for messages + private void AddResource(ReaderInfo reader, string name, object value, String inputFileName, int lineNumber, int linePosition) + { + Entry entry = new Entry(name, value); + + if (reader.resourcesHashTable.ContainsKey(name)) + { + _logger.LogWarningWithCodeFromResources(null, inputFileName, lineNumber, linePosition, 0, 0, "GenerateResource.DuplicateResourceName", name); + return; + } + + reader.resources.Add(entry); + reader.resourcesHashTable.Add(name, value); + } + + /// + /// Add a resource from an XML or binary format file to the internal data structures + /// + /// Resource name + /// Resource value + /// Input file for messages + private void AddResource(ReaderInfo reader, string name, object value, String inputFileName) + { + AddResource(reader, name, value, inputFileName, 0, 0); + } + + internal sealed class ReaderInfo + { + public String outputFileName; + public String cultureName; + // We use a list to preserve the resource ordering (primarily for easier testing), + // but also use a hash table to check for duplicate names. + public ArrayList resources; + public Hashtable resourcesHashTable; + public String assemblySimpleName; // The main assembly's simple name (ie, no .resources) + public bool fromNeutralResources; // Was this from the main assembly (or if the NRLA specified fallback to satellite, that satellite?) + + public ReaderInfo() + { + resources = new ArrayList(); + resourcesHashTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + } + } + + /// + /// Custom StreamReader that provides detailed position information, + /// used when reading text format resources + /// + internal sealed class LineNumberStreamReader : StreamReader + { + // Line numbers start from 1, as well as line position. + // For better error reporting, set line number to 1 and col to 0. + private int _lineNumber; + private int _col; + + internal LineNumberStreamReader(String fileName, Encoding encoding, bool detectEncoding) + : base(fileName, encoding, detectEncoding) + { + _lineNumber = 1; + _col = 0; + } + + internal LineNumberStreamReader(Stream stream) + : base(stream) + { + _lineNumber = 1; + _col = 0; + } + + public override int Read() + { + int ch = base.Read(); + if (ch != -1) + { + _col++; + if (ch == '\n') + { + _lineNumber++; + _col = 0; + } + } + return ch; + } + + public override int Read([In, Out] char[] chars, int index, int count) + { + int r = base.Read(chars, index, count); + for (int i = 0; i < r; i++) + { + if (chars[i + index] == '\n') + { + _lineNumber++; + _col = 0; + } + else + _col++; + } + return r; + } + + public override String ReadLine() + { + String s = base.ReadLine(); + if (s != null) + { + _lineNumber++; + _col = 0; + } + return s; + } + + public override String ReadToEnd() + { + throw new NotImplementedException("NYI"); + } + + internal int LineNumber + { + get { return _lineNumber; } + } + + internal int LinePosition + { + get { return _col; } + } + } + + /// + /// For flow of control & passing sufficient error context back + /// from ReadTextResources + /// + [Serializable] + internal sealed class TextFileException : Exception + { + private String _fileName; + private int _lineNumber; + private int _column; + + /// + /// Fxcop want to have the correct basic exception constructors implemented + /// + private TextFileException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + internal TextFileException(String message, String fileName, int lineNumber, int linePosition) + : base(message) + { + _fileName = fileName; + _lineNumber = lineNumber; + _column = linePosition; + } + + internal String FileName + { + get { return _fileName; } + } + + internal int LineNumber + { + get { return _lineNumber; } + } + + internal int LinePosition + { + get { return _column; } + } + } + + /// + /// Name value resource pair to go in resources list + /// + private class Entry + { + public Entry(string name, object value) + { + this.name = name; + this.value = value; + } + + public string name; + public object value; + } + #endregion // Code from ResGen.EXE + } + + /// + /// This implemention of ITypeResolutionService is passed into the ResxResourceReader + /// class, which calls back into the methods on this class in order to resolve types + /// and assemblies that are referenced inside of the .RESX. + /// + internal class AssemblyNamesTypeResolutionService : ITypeResolutionService + { + private Hashtable _cachedAssemblies; + private ITaskItem[] _referencePaths; + private Hashtable _cachedTypes = new Hashtable(); + + /// + /// Constructor, initialized with the set of resolved reference paths passed + /// into the GenerateResource task. + /// + /// + internal AssemblyNamesTypeResolutionService(ITaskItem[] referencePaths) + { + _referencePaths = referencePaths; + } + + /// + /// Not implemented. Not called by the ResxResourceReader. + /// + /// + /// + public Assembly GetAssembly(AssemblyName name) + { + throw new NotSupportedException(); + } + + /// + /// Not implemented. Not called by the ResxResourceReader. + /// + /// + /// + public Assembly GetAssembly(AssemblyName name, bool throwOnError) + { + throw new NotSupportedException(); + } + + /// + /// Given a path to an assembly, load the assembly if it's not already loaded. + /// + /// + /// + /// + private Assembly GetAssemblyByPath(string pathToAssembly, bool throwOnError) + { + if (_cachedAssemblies == null) + { + _cachedAssemblies = new Hashtable(); + } + + if (!_cachedAssemblies.Contains(pathToAssembly)) + { + try + { + _cachedAssemblies[pathToAssembly] = Assembly.UnsafeLoadFrom(pathToAssembly); + } + catch + { + if (throwOnError) + { + throw; + } + } + } + + return (Assembly)_cachedAssemblies[pathToAssembly]; + } + + /// + /// Not implemented. Not called by the ResxResourceReader. + /// + /// + /// + public string GetPathOfAssembly(AssemblyName name) + { + throw new NotSupportedException(); + } + + /// + /// Returns the type with the specified name. Searches for the type in all + /// of the assemblies passed into the References parameter of the GenerateResource + /// task. + /// + /// + /// + public Type GetType(string name) + { + return GetType(name, true); + } + + /// + /// Returns the type with the specified name. Searches for the type in all + /// of the assemblies passed into the References parameter of the GenerateResource + /// task. + /// + /// + /// + /// + public Type GetType(string name, bool throwOnError) + { + return GetType(name, throwOnError, false); + } + + /// + /// Returns the type with the specified name. Searches for the type in all + /// of the assemblies passed into the References parameter of the GenerateResource + /// task. + /// + /// + /// + /// + /// + public Type GetType(string name, bool throwOnError, bool ignoreCase) + { + Type resultFromCache = (Type)_cachedTypes[name]; + + if (!_cachedTypes.Contains(name)) + { + // first try to resolve in the GAC + Type result = Type.GetType(name, false, ignoreCase); + + // did not find it in the GAC, check each assembly + if ((result == null) && (_referencePaths != null)) + { + foreach (ITaskItem referencePath in _referencePaths) + { + Assembly a = this.GetAssemblyByPath(referencePath.ItemSpec, throwOnError); + if (a != null) + { + result = a.GetType(name, false, ignoreCase); + if (result == null) + { + int indexOfComma = name.IndexOf(",", StringComparison.Ordinal); + if (indexOfComma != -1) + { + string shortName = name.Substring(0, indexOfComma); + result = a.GetType(shortName, false, ignoreCase); + } + } + + if (result != null) + { + break; + } + } + } + } + + if (result == null && throwOnError) + { + ErrorUtilities.VerifyThrowArgument(false, "GenerateResource.CouldNotLoadType", name); + } + + _cachedTypes[name] = result; + resultFromCache = result; + } + + return resultFromCache; + } + + /// + /// Not implemented. Not called by the ResxResourceReader. + /// + /// + /// + public void ReferenceAssembly(AssemblyName name) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/XMakeTasks/GenerateTrustInfo.cs b/src/XMakeTasks/GenerateTrustInfo.cs new file mode 100644 index 00000000000..f0d675aa5ea --- /dev/null +++ b/src/XMakeTasks/GenerateTrustInfo.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; + +namespace Microsoft.Build.Tasks +{ + /// + /// This task generates the application trust from the base manifest + /// and the TargetZone and ExcludedPermissions properties. + /// + public sealed class GenerateTrustInfo : TaskExtension + { + private ITaskItem _baseManifest = null; + private string _excludedPermissions = null; + private string _targetFrameworkMoniker; + private string _targetZone; + private ITaskItem _trustInfoFile; + private ITaskItem[] _applicationDependencies; + private const string Custom = "Custom"; + + public ITaskItem BaseManifest + { + get { return _baseManifest; } + set { _baseManifest = value; } + } + + public string ExcludedPermissions + { + get { return _excludedPermissions; } + set { _excludedPermissions = value; } + } + + public string TargetFrameworkMoniker + { + get { return _targetFrameworkMoniker; } + set { _targetFrameworkMoniker = value; } + } + + public string TargetZone + { + get { return _targetZone; } + set { _targetZone = value; } + } + + public ITaskItem[] ApplicationDependencies + { + get { return _applicationDependencies; } + set { _applicationDependencies = value; } + } + + [Output] + [Required] + public ITaskItem TrustInfoFile + { + get { return _trustInfoFile; } + set { _trustInfoFile = value; } + } + + public GenerateTrustInfo() + { + } + + public override bool Execute() + { + TrustInfo trustInfo = new TrustInfo(); + trustInfo.IsFullTrust = false; + FrameworkNameVersioning fn = null; + string dotNetVersion = string.Empty; + if (!string.IsNullOrEmpty(TargetFrameworkMoniker)) + { + fn = new FrameworkNameVersioning(TargetFrameworkMoniker); + dotNetVersion = fn.Version.ToString(); + } + + // Read trust-info from app.manifest + if (BaseManifest != null && File.Exists(BaseManifest.ItemSpec)) + { + try + { + trustInfo.ReadManifest(BaseManifest.ItemSpec); + } + catch (Exception ex) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ReadInputManifestFailed", BaseManifest.ItemSpec, ex.Message); + return false; + } + } + + if (!String.IsNullOrEmpty(ExcludedPermissions)) + Log.LogWarningFromResources("GenerateManifest.ExcludedPermissionsNotSupported"); + + try + { + // If it's a known zone and the user add additional permission to it. + if (!String.IsNullOrEmpty(_targetZone) + && trustInfo.PermissionSet != null && trustInfo.PermissionSet.Count > 0 + && !String.Equals(_targetZone, Custom, StringComparison.OrdinalIgnoreCase)) + { + Log.LogErrorFromResources("GenerateManifest.KnownTargetZoneCannotHaveAdditionalPermissionType"); + return false; + } + else + { + trustInfo.PermissionSet = SecurityUtilities.ComputeZonePermissionSetHelper(TargetZone, trustInfo.PermissionSet, _applicationDependencies, TargetFrameworkMoniker); + if (trustInfo.PermissionSet == null) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.NoPermissionSetForTargetZone", dotNetVersion); + return false; + } + } + } + catch (ArgumentNullException) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.NoPermissionSetForTargetZone", dotNetVersion); + return false; + } + catch (ArgumentException ex) + { + if (String.Equals(ex.ParamName, "TargetZone", StringComparison.OrdinalIgnoreCase)) + Log.LogWarningWithCodeFromResources("GenerateManifest.InvalidItemValue", "TargetZone", TargetZone); + else + throw; + } + + // Write trust-info back to a stand-alone trust file + trustInfo.Write(TrustInfoFile.ItemSpec); + + return true; + } + + private static string[] StringToIdentityList(string s) + { + string[] a = s.Split(';'); + for (int i = 0; i < a.Length; ++i) + a[i] = a[i].Trim(); + return a; + } + } +} diff --git a/src/XMakeTasks/GetAssemblyIdentity.cs b/src/XMakeTasks/GetAssemblyIdentity.cs new file mode 100644 index 00000000000..ce5e9eb94f8 --- /dev/null +++ b/src/XMakeTasks/GetAssemblyIdentity.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Sniffs input files for their assembly identities, and outputs a set of items with the identity information. + /// + /// + /// Input: Assembly Include="foo.exe" + /// Output: Identity Include="Foo, Version=1.0.0.0", Name="Foo, Version="1.0.0.0" + /// + public class GetAssemblyIdentity : TaskExtension + { + private ITaskItem[] _assemblyFiles; + private ITaskItem[] _assemblies; + + [Required] + public ITaskItem[] AssemblyFiles + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_assemblyFiles, "assemblyFiles"); + return _assemblyFiles; + } + set { _assemblyFiles = value; } + } + + [Output] + public ITaskItem[] Assemblies + { + get { return _assemblies; } + set { _assemblies = value; } + } + + private static string ByteArrayToHex(Byte[] a) + { + if (a == null) + return null; + StringBuilder s = new StringBuilder(a.Length); + foreach (Byte b in a) + s.Append(b.ToString("X02", CultureInfo.InvariantCulture)); + return s.ToString(); + } + + public override bool Execute() + { + ArrayList list = new ArrayList(); + foreach (ITaskItem item in AssemblyFiles) + { + AssemblyName an; + try + { + an = AssemblyName.GetAssemblyName(item.ItemSpec); + } + catch (BadImageFormatException e) + { + Log.LogErrorWithCodeFromResources("GetAssemblyIdentity.CouldNotGetAssemblyName", item.ItemSpec, e.Message); + continue; + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("GetAssemblyIdentity.CouldNotGetAssemblyName", item.ItemSpec, e.Message); + continue; + } + + ITaskItem newItem = new TaskItem(an.FullName); + newItem.SetMetadata("Name", an.Name); + if (an.Version != null) + newItem.SetMetadata("Version", an.Version.ToString()); + if (an.GetPublicKeyToken() != null) + newItem.SetMetadata("PublicKeyToken", ByteArrayToHex(an.GetPublicKeyToken())); + if (an.CultureInfo != null) + newItem.SetMetadata("Culture", an.CultureInfo.ToString()); + item.CopyMetadataTo(newItem); + list.Add(newItem); + } + Assemblies = (ITaskItem[])list.ToArray(typeof(ITaskItem)); + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/XMakeTasks/GetFrameworkPath.cs b/src/XMakeTasks/GetFrameworkPath.cs new file mode 100644 index 00000000000..a27a086cf02 --- /dev/null +++ b/src/XMakeTasks/GetFrameworkPath.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Returns the paths to the various frameworks versions. + /// + public class GetFrameworkPath : TaskExtension + { + #region Properties + + // PERF NOTE: We cache these values in statics -- although the code we call does this too, + // it still seems to give an advantage perhaps because there is one less string copy. + // In a large build, this adds up. + // PERF NOTE: We also only find paths we are actually asked for (via tags) + + private static string s_path; + private static string s_version11Path; + private static string s_version20Path; + private static string s_version30Path; + private static string s_version35Path; + private static string s_version40Path; + private static string s_version45Path; + private static string s_version451Path; + private static string s_version46Path; + + /// + /// Path to the latest framework, whatever version it happens to be + /// + [Output] + public string Path + { + get + { + if (s_path == null) + { + s_path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.VersionLatest); + } + + return s_path; + } + + set + { + // Does nothing: backward compat + s_path = value; + } + } + + /// + /// Path to the v1.1 framework, if available + /// + [Output] + public string FrameworkVersion11Path + { + get + { + if (s_version11Path == null) + { + s_version11Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version11); + } + + return s_version11Path; + } + } + + /// + /// Path to the v2.0 framework, if available + /// + [Output] + public string FrameworkVersion20Path + { + get + { + if (s_version20Path == null) + { + s_version20Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20); + } + + return s_version20Path; + } + } + + /// + /// Path to the v3.0 framework, if available + /// + [Output] + public string FrameworkVersion30Path + { + get + { + if (s_version30Path == null) + { + s_version30Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version30); + } + + return s_version30Path; + } + } + + /// + /// Path to the v3.5 framework, if available + /// + [Output] + public string FrameworkVersion35Path + { + get + { + if (s_version35Path == null) + { + s_version35Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35); + } + + return s_version35Path; + } + } + + /// + /// Path to the v4.0 framework, if available + /// + [Output] + public string FrameworkVersion40Path + { + get + { + if (s_version40Path == null) + { + s_version40Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version40); + } + + return s_version40Path; + } + } + + /// + /// Path to the v4.5 framework, if available + /// + [Output] + public string FrameworkVersion45Path + { + get + { + if (s_version45Path == null) + { + s_version45Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version45); + } + + return s_version45Path; + } + } + + /// + /// Path to the v4.5.1 framework, if available + /// + [Output] + public string FrameworkVersion451Path + { + get + { + if (s_version451Path == null) + { + s_version451Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version451); + } + + return s_version451Path; + } + } + + /// + /// Path to the v4.6 framework, if available + /// + [Output] + public string FrameworkVersion46Path + { + get + { + if (s_version46Path == null) + { + s_version46Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version46); + } + + return s_version46Path; + } + } + + #endregion + + #region ITask Members + + /// + /// Does nothing: getters do all the work + /// + public override bool Execute() + { + return true; + } + + #endregion + } +} diff --git a/src/XMakeTasks/GetFrameworkSDKPath.cs b/src/XMakeTasks/GetFrameworkSDKPath.cs new file mode 100644 index 00000000000..86dad0be020 --- /dev/null +++ b/src/XMakeTasks/GetFrameworkSDKPath.cs @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Returns paths to the frameworks SDK. + /// + public class GetFrameworkSdkPath : TaskExtension + { + #region Properties + + private static string s_path; + private static string s_version20Path; + private static string s_version35Path; + private static string s_version40Path; + private static string s_version45Path; + private static string s_version451Path; + private static string s_version46Path; + + /// + /// The path to the latest .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string Path + { + get + { + if (s_path == null) + { + s_path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.VersionLatest, VisualStudioVersion.VersionLatest); + + if (String.IsNullOrEmpty(s_path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.VersionLatest, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.VersionLatest, VisualStudioVersion.VersionLatest) + ); + + s_path = String.Empty; + } + else + { + s_path = FileUtilities.EnsureTrailingSlash(s_path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_path); + } + } + + return s_path; + } + set + { + // Does nothing; for backwards compatibility only + } + } + + /// + /// The path to the v2.0 .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string FrameworkSdkVersion20Path + { + get + { + if (s_version20Path == null) + { + s_version20Path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version20); + + if (String.IsNullOrEmpty(s_version20Path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version20), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version20) + ); + + s_version20Path = String.Empty; + } + else + { + s_version20Path = FileUtilities.EnsureTrailingSlash(s_version20Path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version20Path); + } + } + + return s_version20Path; + } + } + + /// + /// The path to the v3.5 .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string FrameworkSdkVersion35Path + { + get + { + if (s_version35Path == null) + { + s_version35Path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest); + + if (String.IsNullOrEmpty(s_version35Path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest) + ); + + s_version35Path = String.Empty; + } + else + { + s_version35Path = FileUtilities.EnsureTrailingSlash(s_version35Path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version35Path); + } + } + + return s_version35Path; + } + } + + /// + /// The path to the v4.0 .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string FrameworkSdkVersion40Path + { + get + { + if (s_version40Path == null) + { + s_version40Path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.VersionLatest); + + if (String.IsNullOrEmpty(s_version40Path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.VersionLatest) + ); + + s_version40Path = String.Empty; + } + else + { + s_version40Path = FileUtilities.EnsureTrailingSlash(s_version40Path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version40Path); + } + } + + return s_version40Path; + } + } + + /// + /// The path to the v4.5 .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string FrameworkSdkVersion45Path + { + get + { + if (s_version45Path == null) + { + s_version45Path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.VersionLatest); + + if (String.IsNullOrEmpty(s_version45Path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.VersionLatest) + ); + + s_version45Path = String.Empty; + } + else + { + s_version45Path = FileUtilities.EnsureTrailingSlash(s_version45Path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version45Path); + } + } + + return s_version45Path; + } + } + + /// + /// The path to the v4.5.1 .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string FrameworkSdkVersion451Path + { + get + { + if (s_version451Path == null) + { + s_version451Path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.VersionLatest); + + if (String.IsNullOrEmpty(s_version451Path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version451, VisualStudioVersion.VersionLatest) + ); + + s_version451Path = String.Empty; + } + else + { + s_version451Path = FileUtilities.EnsureTrailingSlash(s_version451Path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version451Path); + } + } + + return s_version451Path; + } + } + + /// + /// The path to the v4.6 .NET SDK if it could be found. It will be String.Empty if the SDK was not found. + /// + [Output] + public string FrameworkSdkVersion46Path + { + get + { + if (s_version46Path == null) + { + s_version46Path = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.VersionLatest); + + if (String.IsNullOrEmpty(s_version46Path)) + { + Log.LogMessageFromResources( + MessageImportance.High, + "GetFrameworkSdkPath.CouldNotFindSDK", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version46, VisualStudioVersion.VersionLatest) + ); + + s_version46Path = String.Empty; + } + else + { + s_version46Path = FileUtilities.EnsureTrailingSlash(s_version46Path); + Log.LogMessageFromResources(MessageImportance.Low, "GetFrameworkSdkPath.FoundSDK", s_version46Path); + } + } + + return s_version46Path; + } + } + + #endregion + + #region ITask Members + + /// + /// Get the SDK. + /// + /// true + public override bool Execute() + { + //Does Nothing: getters do all the work + + return true; + } + + #endregion + } +} diff --git a/src/XMakeTasks/GetInstalledSDKLocations.cs b/src/XMakeTasks/GetInstalledSDKLocations.cs new file mode 100644 index 00000000000..91bc9614eab --- /dev/null +++ b/src/XMakeTasks/GetInstalledSDKLocations.cs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// Gathers the list of installed SDKS in the registry and on disk and outputs them into the project +// so they can be used during SDK reference resolution and RAR for single files. +// +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Tasks +{ + /// + /// Gathers the list of installed SDKS in the registry and on disk and outputs them into the project + /// so they can be used during SDK reference resolution and RAR for single files. + /// + public class GetInstalledSDKLocations : TaskExtension + { + /// + /// Metadata name for directory roots on installed SDK items + /// + internal const string DirectoryRootsMetadataName = "DirectoryRoots"; + + /// + /// Metadata name for extension directory roots on installed SDK items + /// + internal const string ExtensionDirectoryRootsMetadataName = "ExtensionDirectoryRoots"; + + /// + /// Metadata name for SDK Name + /// + internal const string SDKNameMetadataName = "SDKName"; + + /// + /// Metadata name for registry roots on installed SDK items + /// + internal const string RegistryRootMetadataName = "RegistryRoot"; + + /// + /// Key into our build cache + /// + private const string StaticSDKCacheKey = "StaticToolLocationHelperSDKCacheDisposer"; + + #region Properties + + /// + /// Target platform version + /// + private string _targetPlatformVersion = String.Empty; + + /// + /// Target platform identifier + /// + private string _targetPlatformIdentifier = String.Empty; + + /// + /// Platform version we are targeting + /// + [Required] + public string TargetPlatformVersion + { + get + { + return _targetPlatformVersion; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "TargetPlatformVersion"); + _targetPlatformVersion = value; + } + } + + /// + /// Platform identifier we are targeting + /// + [Required] + public string TargetPlatformIdentifier + { + get + { + return _targetPlatformIdentifier; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "TargetPlatformIdentifier"); + _targetPlatformIdentifier = value; + } + } + + /// + /// Root registry root to look for SDKs + /// + public string SDKRegistryRoot + { + get; + set; + } + + /// + /// Root directory on disk to look for SDKs + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public string[] SDKDirectoryRoots + { + get; + set; + } + + /// + /// Root directories on disk to look for new style extension SDKs + /// + public string[] SDKExtensionDirectoryRoots + { + get; + set; + } + + /// + /// Set of items that represent all of the installed SDKs found in the SDKDirectory and SDKRegistry roots. + /// The itemspec is the SDK install location. There is a piece of metadata called SDKName which contains the name of the SDK. + /// + [Output] + public ITaskItem[] InstalledSDKs + { + get; + set; + } + #endregion + + #region ITask Members + + /// + /// Get the SDK. + /// + /// true + public override bool Execute() + { + // TargetPlatformVersion and TargetPlatformIdentifier are requried to correctly look for SDKs. + if (String.IsNullOrEmpty(TargetPlatformVersion) || String.IsNullOrEmpty(TargetPlatformIdentifier)) + { + Log.LogErrorWithCodeFromResources("GetInstalledSDKs.TargetPlatformInformationMissing"); + return false; + } + + // Dictionary of ESDKs. Each entry is a (location, platform version) tuple + IDictionary> installedSDKs = null; + + try + { + Log.LogMessageFromResources("GetInstalledSDKs.SearchingForSDKs", _targetPlatformIdentifier, _targetPlatformVersion); + + Version platformVersion = Version.Parse(TargetPlatformVersion); + installedSDKs = ToolLocationHelper.GetPlatformExtensionSDKLocationsAndVersions(SDKDirectoryRoots, SDKExtensionDirectoryRoots, SDKRegistryRoot, TargetPlatformIdentifier, platformVersion); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("GetInstalledSDKs.CouldNotGetSDKList", e.Message); + } + + List outputItems = new List(); + + if (installedSDKs != null && installedSDKs.Count > 0) + { + Log.LogMessageFromResources(MessageImportance.Low, "GetInstalledSDKs.FoundSDKs", installedSDKs.Count); + Log.LogMessageFromResources(MessageImportance.Low, "GetInstalledSDKs.ListInstalledSDKs"); + + foreach (KeyValuePair> sdk in installedSDKs) + { + string sdkInfo = ResourceUtilities.FormatResourceString("GetInstalledSDKs.SDKNameAndLocation", sdk.Key, sdk.Value.Item1); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", sdkInfo); + + TaskItem item = new TaskItem(sdk.Value.Item1); + item.SetMetadata("SDKName", sdk.Key); + item.SetMetadata("PlatformVersion", sdk.Value.Item2); + + // Need to stash these so we can unroll the platform via GetMatchingPlatformSDK when we get the reference files for the sdks + item.SetMetadata(DirectoryRootsMetadataName, String.Join(";", SDKDirectoryRoots ?? new string[0])); + item.SetMetadata(ExtensionDirectoryRootsMetadataName, String.Join(";", SDKExtensionDirectoryRoots ?? new string[0])); + item.SetMetadata(RegistryRootMetadataName, SDKRegistryRoot); + + outputItems.Add(item); + } + } + else + { + Log.LogWarningWithCodeFromResources("GetInstalledSDKs.NoSDksFound"); + } + + InstalledSDKs = outputItems.ToArray(); + + // We need to register an object so that at the end of the build we will clear the static toolLocationhelper caches. + // this is important because if someone adds an SDK between builds we would not know about it and not be able to use it. + // This code is mainly used to deal with the case where msbuild nodes hang around between builds. + IBuildEngine4 buildEngine4 = BuildEngine as IBuildEngine4; + if (buildEngine4 != null) + { + object staticCacheDisposer = buildEngine4.GetRegisteredTaskObject(StaticSDKCacheKey, RegisteredTaskObjectLifetime.Build); + if (staticCacheDisposer == null) + { + BuildCacheDisposeWrapper staticDisposer = new BuildCacheDisposeWrapper(new BuildCacheDisposeWrapper.CallDuringDispose(ToolLocationHelper.ClearSDKStaticCache)); + buildEngine4.RegisterTaskObject(StaticSDKCacheKey, staticDisposer, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: false); + } + } + + return !Log.HasLoggedErrors; + } + + #endregion + } +} diff --git a/src/XMakeTasks/GetReferenceAssemblyPaths.cs b/src/XMakeTasks/GetReferenceAssemblyPaths.cs new file mode 100644 index 00000000000..f7f094817d2 --- /dev/null +++ b/src/XMakeTasks/GetReferenceAssemblyPaths.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Get the reference assembly paths for a given target framework version / moniker. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture; + +namespace Microsoft.Build.Tasks +{ + /// + /// Returns the reference assembly paths to the various frameworks + /// + public class GetReferenceAssemblyPaths : TaskExtension + { + #region Data + /// + /// This is the sentinel assembly for .NET FX 3.5 SP1 + /// Used to determine if SP1 of 3.5 is installed + /// + private static readonly string s_NET35SP1SentinelAssemblyName = "System.Data.Entity, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL"; + + /// + /// Cache in a static whether or not we have found the 35sp1sentinel assembly. + /// + private static bool? s_net35SP1SentinelAssemblyFound; + + /// + /// Hold the reference assembly paths based on the passed in targetframeworkmoniker. + /// + private IList _tfmPaths; + + /// + /// Hold the reference assembly paths based on the passed in targetframeworkmoniker without considering any profile passed in. + /// + private IList _tfmPathsNoProfile; + + /// + /// Target framework moniker string passd into the task + /// + private string _targetFrameworkMoniker; + + /// + /// The root path to use to generate the reference assemblyPaths + /// + private string _rootPath; + + /// + /// By default GetReferenceAssemblyPaths performs simple checks + /// to ensure that certain runtime frameworks are installed depending on the + /// target framework. + /// set bypassFrameworkInstallChecks to true in order to bypass those checks. + /// + private bool _bypassFrameworkInstallChecks; + + #endregion + + #region Properties + + /// + /// Returns the path based on the passed in TargetFrameworkMoniker. If the TargetFrameworkMoniker is null or empty + /// this path will be empty. + /// + [Output] + public string[] ReferenceAssemblyPaths + { + get + { + if (_tfmPaths != null) + { + string[] pathsToReturn = new string[_tfmPaths.Count]; + _tfmPaths.CopyTo(pathsToReturn, 0); + return pathsToReturn; + } + else + { + return new string[0]; + } + } + } + + /// + /// Returns the path based on the passed in TargetFrameworkMoniker without considering the profile part of the moniker. If the TargetFrameworkMoniker is null or empty + /// this path will be empty. + /// + [Output] + public string[] FullFrameworkReferenceAssemblyPaths + { + get + { + if (_tfmPathsNoProfile != null) + { + string[] pathsToReturn = new string[_tfmPathsNoProfile.Count]; + _tfmPathsNoProfile.CopyTo(pathsToReturn, 0); + return pathsToReturn; + } + else + { + return new string[0]; + } + } + } + + /// + /// The target framework moniker to get the reference assembly paths for + /// + public string TargetFrameworkMoniker + { + get + { + return _targetFrameworkMoniker; + } + + set + { + _targetFrameworkMoniker = value; + } + } + + /// + /// The root path to use to generate the reference assembly path + /// + public string RootPath + { + get + { + return _rootPath; + } + + set + { + _rootPath = value; + } + } + + /// + /// By default GetReferenceAssemblyPaths performs simple checks + /// to ensure that certain runtime frameworks are installed depending on the + /// target framework. + /// set BypassFrameworkInstallChecks to true in order to bypass those checks. + /// + public bool BypassFrameworkInstallChecks + { + get + { + return _bypassFrameworkInstallChecks; + } + + set + { + _bypassFrameworkInstallChecks = value; + } + } + + /// + /// Gets the display name for the targetframeworkmoniker + /// + [Output] + public string TargetFrameworkMonikerDisplayName + { + get; + set; + } + + #endregion + + #region ITask Members + + /// + /// If the target framework moniker is set, generate the correct Paths. + /// + public override bool Execute() + { + FrameworkNameVersioning moniker = null; + FrameworkNameVersioning monikerWithNoProfile = null; + + // Are we targeting a profile. + bool targetingProfile = false; + + try + { + moniker = new FrameworkNameVersioning(TargetFrameworkMoniker); + targetingProfile = !String.IsNullOrEmpty(moniker.Profile); + + // If we are targeting a profile we need to generate a set of reference assembly paths which describe where the full framework + // exists, to do so we need to get the reference assembly location without the profile as part of the moniker. + if (targetingProfile) + { + monikerWithNoProfile = new FrameworkNameVersioning(moniker.Identifier, moniker.Version); + } + + // This is a very specific "hack" to ensure that when we're targeting certain .NET Framework versions that + // WPF gets to rely on .NET FX 3.5 SP1 being installed on the build machine. + // This only needs to occur when we are targeting a .NET FX prior to v4.0 + if (!_bypassFrameworkInstallChecks && moniker.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase) && + moniker.Version.Major < 4) + { + // We have not got a value for whether or not the 35 sentinel assembly has been found + if (!s_net35SP1SentinelAssemblyFound.HasValue) + { + // get an assemblyname from the string representation of the sentinel assembly name + AssemblyNameExtension sentinelAssemblyName = new AssemblyNameExtension(s_NET35SP1SentinelAssemblyName); + + string path = GlobalAssemblyCache.GetLocation(sentinelAssemblyName, SystemProcessorArchitecture.MSIL, runtimeVersion => "v2.0.50727", new Version("2.0.57027"), false, new FileExists(FileUtilities.FileExistsNoThrow), GlobalAssemblyCache.pathFromFusionName, GlobalAssemblyCache.gacEnumerator, false); + s_net35SP1SentinelAssemblyFound = !String.IsNullOrEmpty(path); + } + + // We did not find the SP1 sentinel assembly in the GAC. Therefore we must assume that SP1 isn't installed + if (!s_net35SP1SentinelAssemblyFound.Value) + { + Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.NETFX35SP1NotIntstalled", TargetFrameworkMoniker); + } + } + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.InvalidTargetFrameworkMoniker", TargetFrameworkMoniker, e.Message); + return false; + } + + try + { + _tfmPaths = GetPaths(_rootPath, moniker); + + if (_tfmPaths != null && _tfmPaths.Count > 0) + { + TargetFrameworkMonikerDisplayName = ToolLocationHelper.GetDisplayNameForTargetFrameworkDirectory(_tfmPaths[0], moniker); + } + + // If there is a profile get the paths without the profile. + // There is no point in generating the full framework paths if profile path could not be found. + if (targetingProfile && _tfmPaths != null) + { + _tfmPathsNoProfile = GetPaths(_rootPath, monikerWithNoProfile); + } + + // The path with out the profile is just the referecne assembly paths. + if (!targetingProfile) + { + _tfmPathsNoProfile = _tfmPaths; + } + } + catch (Exception e) + { + // The reason we need to do exception E here is because we are in a task and have the ability to log the message and give the user + // feedback as to its cause, tasks if at all possible should not have exception leave them. + Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.ProblemGeneratingReferencePaths", TargetFrameworkMoniker, e.Message); + + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + _tfmPathsNoProfile = null; + TargetFrameworkMonikerDisplayName = null; + } + + return !Log.HasLoggedErrors; + } + + /// + /// Generate the set of chained reference assembly paths + /// + private IList GetPaths(string rootPath, FrameworkNameVersioning frameworkmoniker) + { + IList pathsToReturn = null; + + if (String.IsNullOrEmpty(rootPath)) + { + pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies(frameworkmoniker); + } + else + { + pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies(rootPath, frameworkmoniker); + } + + // No reference assembly paths could be found, log a warning as there could be future errors which may be confusing because of this. + if (pathsToReturn.Count == 0) + { + Log.LogWarningWithCodeFromResources("GetReferenceAssemblyPaths.NoReferenceAssemblyDirectoryFound", frameworkmoniker.ToString()); + } + + return pathsToReturn; + } + + #endregion + } +} diff --git a/src/XMakeTasks/GetSDKReferenceFiles.cs b/src/XMakeTasks/GetSDKReferenceFiles.cs new file mode 100644 index 00000000000..4251acb0501 --- /dev/null +++ b/src/XMakeTasks/GetSDKReferenceFiles.cs @@ -0,0 +1,1434 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Gathers the reference assemblies from the SDK based on what configuration and architecture a SDK references. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolves an SDKReference to a full path on disk + /// + public class GetSDKReferenceFiles : TaskExtension + { + /// + /// Set of resolvedSDK references which we will use to find the reference assemblies. + /// + private ITaskItem[] _resolvedSDKReferences = new TaskItem[0]; + + /// + /// Set of the redist files for the resolved sdks + /// + private ITaskItem[] _sdkRedistFiles = new TaskItem[0]; + + /// + /// Resolved reference assemblies from the SDK + /// + private ITaskItem[] _references = new TaskItem[0]; + + /// + /// Redist files from the SDKs + /// + private ITaskItem[] _redistFiles = new TaskItem[0]; + + /// + /// Set of resolved reference assemblies. This removes any duplicate ones between sdks. + /// + private HashSet _resolvedReferences = new HashSet(); + + /// + /// Set of resolved reference assemblies. This removes any duplicate ones between sdks. + /// + private HashSet _resolveRedistFiles = new HashSet(); + + /// + /// Files to be copied locally + /// + private ITaskItem[] _copyLocalFiles = new TaskItem[0]; + + /// + /// Set of reference assembly extensions to look for. + /// + private string[] _referenceExtensions = new string[] { ".winmd", ".dll" }; + + /// + /// Dictionary of SDK Identity to the cache file that contains the file information for it. + /// + private ConcurrentDictionary _cacheFileForSDKs = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Set of exceptions which were thrown while reading or writing to the cache file, this needs to be thread safe since TPL code will add exceptions into this structure at the same time. + /// + private ConcurrentQueue _exceptions = new ConcurrentQueue(); + + /// + /// Delegate to get the assembly name + /// + private GetAssemblyName _getAssemblyName; + + /// + /// Get the image runtime version from a file + /// + private GetAssemblyRuntimeVersion _getRuntimeVersion; + + /// + /// File exists delegate + /// + private FileExists _fileExists; + + /// + /// Folder where the cache files are written to + /// + private string _cacheFilePath = Path.GetTempPath(); + + /// + /// Constructor + /// + public GetSDKReferenceFiles() + { + CacheFileFolderPath = Path.GetTempPath(); + LogReferencesList = true; + LogRedistFilesList = true; + LogReferenceConflictBetweenSDKsAsWarning = true; + LogReferenceConflictWithinSDKAsWarning = false; + LogRedistConflictBetweenSDKsAsWarning = true; + LogRedistConflictWithinSDKAsWarning = false; + } + + #region Properties + + /// + /// Path where the cache files should be stored + /// + public string CacheFileFolderPath + { + get + { + return _cacheFilePath; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "CacheFileFolderPath"); + _cacheFilePath = value; + } + } + + /// + /// Resolved SDK references which we will get the reference assemblies from. + /// + public ITaskItem[] ResolvedSDKReferences + { + get + { + return _resolvedSDKReferences; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "ResolvedSDKReferences"); + _resolvedSDKReferences = value; + } + } + + /// + /// Extensions which should be considered reference files, we will look for + /// the files in the order they are specified in the array. + /// + public string[] ReferenceExtensions + { + get + { + return _referenceExtensions; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "ReferenceExtensions"); + _referenceExtensions = value; + } + } + + /// + /// Should the references found as part of resolving the sdk be logged. + /// The default is true + /// + public bool LogReferencesList + { + get; + set; + } + + /// + /// Should the redist files found as part of resolving the sdk be logged. + /// The default is true + /// + public bool LogRedistFilesList + { + get; + set; + } + + /// + /// The targetted SDK identifier. + /// + public string TargetSDKIdentifier + { + get; + set; + } + + /// + /// The targetted SDK version. + /// + public string TargetSDKVersion + { + get; + set; + } + + /// + /// Resolved reference items. + /// + [Output] + public ITaskItem[] References + { + get { return _references; } + } + + /// + /// Resolved redist files. + /// + [Output] + public ITaskItem[] RedistFiles + { + get { return _redistFiles; } + } + + /// + /// Files that need to be copied locally, this is the reference assemblies and the xml intellisense files. + /// + [Output] + public ITaskItem[] CopyLocalFiles + { + get { return _copyLocalFiles; } + } + + /// + /// Should conflicts between redist files within an SDK be logged as a message or a warning. + /// The default is to log them as a message. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDKAs", Justification = "Shipped this way in Dev11 Beta (go-live)")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "SDKAs", Justification = "SDK and As are two different words")] + public bool LogRedistConflictWithinSDKAsWarning + { + get; + set; + } + + /// + /// Should conflicts between redist files across different referenced SDKs be logged as a message or a warning. + /// The default is to log them as a warning. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDKs", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public bool LogRedistConflictBetweenSDKsAsWarning + { + get; + set; + } + + /// + /// Should conflicts between reference files within an SDK be logged as a message or a warning. + /// The default is to log them as a message. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDKAs", Justification = "Shipped this way in Dev11 Beta (go-live)")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "SDKAs", Justification = "SDK and As are two different words")] + public bool LogReferenceConflictWithinSDKAsWarning + { + get; + set; + } + + /// + /// Should conflicts between reference files across different referenced SDKs be logged as a message or a warning. + /// The default is to log them as a warning. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDKs", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public bool LogReferenceConflictBetweenSDKsAsWarning + { + get; + set; + } + + /// + /// Should we log exceptions which were hit when the cache file is being read and written to + /// + public bool LogCacheFileExceptions + { + get; + set; + } + #endregion + + /// + /// Execute the task + /// + public override bool Execute() + { + return Execute(new GetAssemblyName(AssemblyNameExtension.GetAssemblyNameEx), new GetAssemblyRuntimeVersion(AssemblyInformation.GetRuntimeVersion), new FileExists(FileUtilities.FileExistsNoThrow)); + } + + /// + /// Execute the task + /// + internal bool Execute(GetAssemblyName getAssemblyName, GetAssemblyRuntimeVersion getRuntimeVersion, FileExists fileExists) + { + _getAssemblyName = getAssemblyName; + _getRuntimeVersion = getRuntimeVersion; + _fileExists = fileExists; + + try + { + // Filter out all references tagged as RuntimeReferenceOnly + IEnumerable filteredResolvedSDKReferences = ResolvedSDKReferences.Where( + sdkReference => !(MetadataConversionUtilities.TryConvertItemMetadataToBool(sdkReference, "RuntimeReferenceOnly")) + ); + + PopulateReferencesForSDK(filteredResolvedSDKReferences); + + foreach (ITaskItem resolvedSDKReference in filteredResolvedSDKReferences) + { + string sdkName = resolvedSDKReference.GetMetadata("SDKName"); + string sdkIdentity = resolvedSDKReference.GetMetadata("OriginalItemSpec"); + string rootDirectory = resolvedSDKReference.ItemSpec; + string targetedConfiguration = resolvedSDKReference.GetMetadata("TargetedSDKConfiguration"); + string targetedArchitecture = resolvedSDKReference.GetMetadata("TargetedSDKArchitecture"); + + if (targetedConfiguration.Length == 0) + { + Log.LogErrorWithCodeFromResources("GetSDKReferenceFiles.CannotHaveEmptyTargetConfiguration", resolvedSDKReference.ItemSpec); + return false; + } + + if (targetedArchitecture.Length == 0) + { + Log.LogErrorWithCodeFromResources("GetSDKReferenceFiles.CannotHaveEmptyTargetArchitecture", resolvedSDKReference.ItemSpec); + return false; + } + + FindReferences(resolvedSDKReference, sdkIdentity, sdkName, rootDirectory, targetedConfiguration, targetedArchitecture); + FindRedistFiles(resolvedSDKReference, sdkIdentity, targetedConfiguration, targetedArchitecture); + } + + GenerateOutputItems(); + + if (_exceptions.Count > 0 && LogCacheFileExceptions) + { + foreach (string exceptionMessage in _exceptions) + { + Log.LogMessageFromText(exceptionMessage, MessageImportance.High); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("GetSDKReferenceFiles.CouldNotGetSDKReferenceFiles", e.Message); + } + + return !Log.HasLoggedErrors; + } + + /// + /// Find the redist files + /// + private void FindRedistFiles(ITaskItem resolvedSDKReference, string sdkIdentity, string targetedConfiguration, string targetedArchitecture) + { + // Gather the redist files, order is important because we want the most specific match of config and architecture to be the file that returns if there is a collision in destination paths + HashSet resolvedRedistFileSet = new HashSet(); + IList redistPaths = new List(); + + if (targetedConfiguration.Length > 0 && targetedArchitecture.Length > 0) + { + redistPaths = ToolLocationHelper.GetSDKRedistFolders(resolvedSDKReference.ItemSpec, targetedConfiguration, targetedArchitecture); + } + + if (LogRedistFilesList) + { + foreach (string path in redistPaths) + { + Log.LogMessageFromResources(MessageImportance.Low, "GetSDKReferenceFiles.ExpandRedistFrom", path.Replace(resolvedSDKReference.ItemSpec, String.Empty)); + } + } + + SDKInfo sdkCacheInfo = null; + if (_cacheFileForSDKs.TryGetValue(sdkIdentity, out sdkCacheInfo) && sdkCacheInfo != null) + { + foreach (string path in redistPaths) + { + GatherRedistFiles(resolvedRedistFileSet, resolvedSDKReference, path, sdkCacheInfo); + } + } + + // Add the resolved redist files to the master list of resolved redist files also log the fact we have found them. + foreach (ResolvedRedistFile redist in resolvedRedistFileSet) + { + bool success = _resolveRedistFiles.Add(redist); + + if (success) + { + if (LogRedistFilesList) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.AddingRedistFile", redist.RedistFile.Replace(redist.SDKReferenceItem.ItemSpec, String.Empty), redist.TargetPath); + } + } + else + { + ResolvedRedistFile winner = _resolveRedistFiles.First(x => x.Equals(redist)); + + if (!LogRedistConflictBetweenSDKsAsWarning) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.ConflictRedistDifferentSDK", winner.TargetPath, winner.SDKReferenceItem.GetMetadata("OriginalItemSpec"), redist.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.RedistFile, redist.RedistFile); + } + else + { + string message = ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ConflictRedistDifferentSDK", winner.TargetPath, winner.SDKReferenceItem.GetMetadata("OriginalItemSpec"), redist.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.RedistFile, redist.RedistFile); + Log.LogWarningWithCodeFromResources("GetSDKReferenceFiles.ConflictBetweenFiles", message); + } + } + } + } + + /// + /// Find references for the sdk + /// + private void FindReferences(ITaskItem resolvedSDKReference, string sdkIdentity, string sdkName, string rootDirectory, string targetedConfiguration, string targetedArchitecture) + { + bool expandSDK = false; + + if (bool.TryParse(resolvedSDKReference.GetMetadata("ExpandReferenceAssemblies"), out expandSDK) && expandSDK) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.GetSDKReferences", sdkName, rootDirectory); + + // Gather the reference assemblies, order is important because we want the most specific match of config and architecture to be searched for last + // so it can overwrite any less specific matches. + HashSet resolvedReferenceAssemblies = new HashSet(); + + // If the SDK is manifest driven we want to grab them from the ApiContracts in the manifest if possible- will only happen if TargetSdk is identified + string[] manifestReferencePaths = this.GetReferencePathsFromManifest(resolvedSDKReference); + + if (manifestReferencePaths != null && manifestReferencePaths.Length > 0) + { + // Found ApiContract references, use those + foreach (string manifestReferencePath in manifestReferencePaths) + { + resolvedReferenceAssemblies.Add(new ResolvedReferenceAssembly(resolvedSDKReference, manifestReferencePath)); + } + } + else if (targetedConfiguration.Length > 0 && targetedArchitecture.Length > 0) + { + // Couldn't find any valid ApiContracts, look up references the traditional way + IList referencePaths = new List(); + referencePaths = ToolLocationHelper.GetSDKReferenceFolders(resolvedSDKReference.ItemSpec, targetedConfiguration, targetedArchitecture); + + if (LogReferencesList) + { + foreach (string path in referencePaths) + { + Log.LogMessageFromResources(MessageImportance.Low, "GetSDKReferenceFiles.ExpandReferencesFrom", path.Replace(resolvedSDKReference.ItemSpec, String.Empty)); + } + } + + SDKInfo sdkCacheInfo = null; + if (_cacheFileForSDKs.TryGetValue(sdkIdentity, out sdkCacheInfo) && sdkCacheInfo != null) + { + foreach (string path in referencePaths) + { + GatherReferenceAssemblies(resolvedReferenceAssemblies, resolvedSDKReference, path, sdkCacheInfo); + } + } + } + + // Add the resolved references to the master list of resolved assemblies also log the fact we have found them. + foreach (ResolvedReferenceAssembly reference in resolvedReferenceAssemblies) + { + bool success = _resolvedReferences.Add(reference); + if (success) + { + if (LogReferencesList) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.AddingReference", reference.AssemblyLocation.Replace(reference.SDKReferenceItem.ItemSpec, String.Empty)); + } + } + else + { + // Multiple extension SDKs can reference the exact same WinMD now. If the assembly path is exactly the same go ahead and keep the first silently. + // (The normal matching is by filename ONLY) + if (_resolvedReferences.Any(x => String.Equals(x.AssemblyLocation, reference.AssemblyLocation, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + ResolvedReferenceAssembly winner = _resolvedReferences.First(x => x.Equals(reference)); + + if (!LogReferenceConflictBetweenSDKsAsWarning) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.ConflictReferenceDifferentSDK", winner.SDKReferenceItem.GetMetadata("OriginalItemSpec"), reference.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.AssemblyLocation, reference.AssemblyLocation); + } + else + { + string message = ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ConflictReferenceDifferentSDK", winner.SDKReferenceItem.GetMetadata("OriginalItemSpec"), reference.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.AssemblyLocation, reference.AssemblyLocation); + Log.LogWarningWithCodeFromResources("GetSDKReferenceFiles.ConflictBetweenFiles", message); + } + } + } + } + + if (!expandSDK) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.NotExpanding", sdkName); + } + } + + /// + /// Generate the output groups + /// + private void GenerateOutputItems() + { + List resolvedReferenceAssemblies = new List(); + List copyLocalReferenceAssemblies = new List(); + List redistReferenceItems = new List(); + + foreach (ResolvedReferenceAssembly reference in _resolvedReferences) + { + ITaskItem outputItem = new TaskItem(reference.AssemblyLocation); + resolvedReferenceAssemblies.Add(outputItem); + + if (outputItem.GetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget).Length == 0) + { + outputItem.SetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget, "ExpandSDKReference"); + } + + // Mark the two pieces of metadata with the SDK name + outputItem.SetMetadata(ItemMetadataNames.msbuildReferenceFromSDK, reference.SDKReferenceItem.GetMetadata("OriginalItemSpec")); + outputItem.SetMetadata(ItemMetadataNames.msbuildReferenceGrouping, reference.SDKReferenceItem.GetMetadata("OriginalItemSpec")); + outputItem.SetMetadata(ItemMetadataNames.msbuildReferenceGroupingDisplayName, reference.SDKReferenceItem.GetMetadata("DisplayName")); + + string sdkIdentity = reference.SDKReferenceItem.GetMetadata("OriginalItemSpec"); + outputItem.SetMetadata("OriginalItemSpec", sdkIdentity); + outputItem.SetMetadata("SDKRootPath", reference.SDKReferenceItem.ItemSpec); + outputItem.SetMetadata("ResolvedFrom", "GetSDKReferenceFiles"); + + SDKInfo sdkInfo = null; + if (_cacheFileForSDKs.TryGetValue(sdkIdentity, out sdkInfo) && sdkInfo != null) + { + SdkReferenceInfo referenceInfo = null; + if (sdkInfo.PathToReferenceMetadata != null && sdkInfo.PathToReferenceMetadata.TryGetValue(reference.AssemblyLocation, out referenceInfo)) + { + if (referenceInfo != null && referenceInfo.FusionName != null) + { + outputItem.SetMetadata(ItemMetadataNames.fusionName, referenceInfo.FusionName); + } + + if (referenceInfo != null && referenceInfo.ImageRuntime != null) + { + outputItem.SetMetadata(ItemMetadataNames.imageRuntime, referenceInfo.ImageRuntime); + } + + if (referenceInfo != null && referenceInfo.IsWinMD) + { + outputItem.SetMetadata(ItemMetadataNames.winMDFile, "true"); + + if (referenceInfo.IsManagedWinmd) + { + outputItem.SetMetadata(ItemMetadataNames.winMDFileType, "Managed"); + } + else + { + outputItem.SetMetadata(ItemMetadataNames.winMDFileType, "Native"); + } + } + else + { + outputItem.SetMetadata("WinMDFile", "false"); + } + } + } + + if (reference.CopyLocal) + { + outputItem.SetMetadata("CopyLocal", "true"); + copyLocalReferenceAssemblies.Add(outputItem); + + string directory = Path.GetDirectoryName(reference.AssemblyLocation); + string fileNameNoExtension = Path.GetFileNameWithoutExtension(reference.AssemblyLocation); + string xmlFile = Path.Combine(directory, fileNameNoExtension + ".xml"); + + if (FileUtilities.FileExistsNoThrow(xmlFile)) + { + ITaskItem item = new TaskItem(xmlFile); + + // Add the related item. + copyLocalReferenceAssemblies.Add(item); + } + } + else + { + outputItem.SetMetadata("CopyLocal", "false"); + } + } + + resolvedReferenceAssemblies.Sort(TaskItemSpecFilenameComparer.genericComparer); + copyLocalReferenceAssemblies.Sort(TaskItemSpecFilenameComparer.genericComparer); + + _references = resolvedReferenceAssemblies.ToArray(); + _copyLocalFiles = copyLocalReferenceAssemblies.ToArray(); + + foreach (ResolvedRedistFile file in _resolveRedistFiles) + { + ITaskItem outputItem = new TaskItem(file.RedistFile); + + if (outputItem.GetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget).Length == 0) + { + outputItem.SetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget, "ExpandSDKReference"); + } + + outputItem.SetMetadata("OriginalItemSpec", file.SDKReferenceItem.GetMetadata("OriginalItemSpec")); + outputItem.SetMetadata("SDKRootPath", file.SDKReferenceItem.ItemSpec); + outputItem.SetMetadata("ResolvedFrom", "GetSDKReferenceFiles"); + + // Target path for the file + outputItem.SetMetadata("TargetPath", file.TargetPath); + + // Pri files need to know the root directory of the target path + if (Path.GetExtension(file.RedistFile).Equals(".PRI", StringComparison.OrdinalIgnoreCase)) + { + outputItem.SetMetadata("Root", file.TargetRoot); + } + + redistReferenceItems.Add(outputItem); + } + + redistReferenceItems.Sort(TaskItemSpecFilenameComparer.genericComparer); + _redistFiles = redistReferenceItems.ToArray(); + } + + /// + /// Gather the reference assemblies from the referenceassembly directory. + /// + private void GatherReferenceAssemblies(HashSet resolvedFiles, ITaskItem sdkReference, string path, SDKInfo info) + { + List referenceFiles = null; + if (info.DirectoryToFileList != null && info.DirectoryToFileList.TryGetValue(FileUtilities.EnsureNoTrailingSlash(path), out referenceFiles) && referenceFiles != null) + { + foreach (var file in referenceFiles) + { + // We only want to find files which match the extensions the user has asked for, this will usually be dll or winmd. + bool matchesExtension = false; + foreach (var extension in _referenceExtensions) + { + string fileExtension = Path.GetExtension(file); + if (fileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)) + { + matchesExtension = true; + break; + } + } + + if (!matchesExtension) + { + continue; + } + + ResolvedReferenceAssembly resolvedReference = new ResolvedReferenceAssembly(sdkReference, file); + bool success = resolvedFiles.Add(resolvedReference); + if (!success) + { + ResolvedReferenceAssembly winner = resolvedFiles.First(x => x.Equals(resolvedReference)); + + if (!LogReferenceConflictWithinSDKAsWarning) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.ConflictReferenceSameSDK", winner.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.AssemblyLocation.Replace(winner.SDKReferenceItem.ItemSpec, String.Empty), resolvedReference.AssemblyLocation.Replace(resolvedReference.SDKReferenceItem.ItemSpec, String.Empty)); + } + else + { + string message = ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ConflictReferenceSameSDK", winner.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.AssemblyLocation.Replace(winner.SDKReferenceItem.ItemSpec, String.Empty), resolvedReference.AssemblyLocation.Replace(resolvedReference.SDKReferenceItem.ItemSpec, String.Empty)); + Log.LogWarningWithCodeFromResources("GetSDKReferenceFiles.ConflictBetweenFiles", message); + } + } + } + } + } + + /// + /// Gather the redist files from from the redist directory. + /// + private void GatherRedistFiles(HashSet resolvedRedistFiles, ITaskItem sdkReference, string redistFilePath, SDKInfo info) + { + bool copyRedist = MetadataConversionUtilities.TryConvertItemMetadataToBool(sdkReference, "CopyRedist"); + if (copyRedist) + { + foreach (KeyValuePair> directoryToFileList in info.DirectoryToFileList) + { + if (directoryToFileList.Key.StartsWith(FileUtilities.EnsureNoTrailingSlash(redistFilePath), StringComparison.OrdinalIgnoreCase)) + { + List redistFiles = directoryToFileList.Value; + string targetPathRoot = sdkReference.GetMetadata("CopyRedistToSubDirectory"); + + foreach (var file in redistFiles) + { + string relativeToBase = FileUtilities.MakeRelative(redistFilePath, file); + string targetPath = Path.Combine(targetPathRoot, relativeToBase); + + ResolvedRedistFile redistFile = new ResolvedRedistFile(sdkReference, file, targetPath, targetPathRoot); + if (!resolvedRedistFiles.Add(redistFile)) + { + ResolvedRedistFile winner = resolvedRedistFiles.First(x => x.Equals(redistFile)); + + if (!LogRedistConflictWithinSDKAsWarning) + { + Log.LogMessageFromResources("GetSDKReferenceFiles.ConflictRedistSameSDK", redistFile.TargetPath, redistFile.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.RedistFile.Replace(redistFile.SDKReferenceItem.ItemSpec, String.Empty), redistFile.RedistFile.Replace(redistFile.SDKReferenceItem.ItemSpec, String.Empty)); + } + else + { + string message = ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ConflictRedistSameSDK", redistFile.TargetPath, redistFile.SDKReferenceItem.GetMetadata("OriginalItemSpec"), winner.RedistFile.Replace(redistFile.SDKReferenceItem.ItemSpec, String.Empty), redistFile.RedistFile.Replace(redistFile.SDKReferenceItem.ItemSpec, String.Empty)); + Log.LogWarningWithCodeFromResources("GetSDKReferenceFiles.ConflictBetweenFiles", message); + } + } + } + } + } + } + } + + /// + /// Gather the contents of all of the SDK into a cache file and save it to disk. + /// + private void PopulateReferencesForSDK(IEnumerable sdks) + { + SDKFilesCache sdkFilesCache = new SDKFilesCache(_exceptions, _cacheFilePath, _getAssemblyName, _getRuntimeVersion, _fileExists); + + // Go through each sdk which has been resolved in this project + foreach (ITaskItem sdk in sdks) + { + string sdkIdentity = sdk.GetMetadata("OriginalItemSpec"); + ErrorUtilities.VerifyThrowArgument(sdkIdentity.Length != 0, "GetSDKReferenceFiles.NoOriginalItemSpec", sdk.ItemSpec); + string sdkRoot = sdk.ItemSpec; + + // Try and get the cache file for this SDK if it already exists + SDKInfo info = sdkFilesCache.LoadAssemblyListFromCacheFile(sdkIdentity, sdkRoot); + + if (info == null || !sdkFilesCache.IsAssemblyListCacheFileUpToDate(sdkIdentity, sdkRoot, _cacheFilePath)) + { + info = sdkFilesCache.GetCacheFileInfoFromSDK(sdkIdentity, sdkRoot, this.GetReferencePathsFromManifest(sdk)); + + // On a background thread save the file to disk + SaveContext saveContext = new SaveContext(sdkIdentity, sdkRoot, info); + ThreadPool.QueueUserWorkItem(new WaitCallback(sdkFilesCache.SaveAssemblyListToCacheFile), saveContext); + } + + _cacheFileForSDKs.TryAdd(sdkIdentity, info); + } + } + + /// + /// Get the referenced file names from the SDK's manifest if applicable- may return null. + /// + private string[] GetReferencePathsFromManifest(ITaskItem sdk) + { + string[] manifestReferencePaths = null; + + // It is only useful to look if we have a target SDK specified + if (!String.IsNullOrEmpty(TargetSDKIdentifier) && !String.IsNullOrEmpty(TargetSDKVersion)) + { + manifestReferencePaths = ToolLocationHelper.GetPlatformOrFrameworkExtensionSdkReferences( + sdk.GetMetadata(GetInstalledSDKLocations.SDKNameMetadataName), + TargetSDKIdentifier, + TargetSDKVersion, + sdk.GetMetadata(GetInstalledSDKLocations.DirectoryRootsMetadataName), + sdk.GetMetadata(GetInstalledSDKLocations.ExtensionDirectoryRootsMetadataName), + sdk.GetMetadata(GetInstalledSDKLocations.RegistryRootMetadataName)); + } + + return manifestReferencePaths; + } + + /// + /// Class which represents a resolved reference assembly + /// + private class ResolvedReferenceAssembly : IEquatable + { + /// + /// Is the reference copy local + /// + private bool _copyLocal = false; + + /// + /// Constructor + /// + public ResolvedReferenceAssembly(ITaskItem sdkReferenceItem, string assemblyLocation) + { + FileName = Path.GetFileNameWithoutExtension(assemblyLocation); + AssemblyLocation = assemblyLocation; + bool.TryParse(sdkReferenceItem.GetMetadata("CopyLocalExpandedReferenceAssemblies"), out _copyLocal); + SDKReferenceItem = sdkReferenceItem; + } + + /// + /// What is the file name + /// + public string FileName + { + get; + private set; + } + + /// + /// What is the location of the assembly on disk. + /// + public string AssemblyLocation + { + get; + private set; + } + + /// + /// Is the assembly copy local or not. + /// + public bool CopyLocal + { + get + { + return _copyLocal; + } + } + + /// + /// Original resolved SDK reference item passed in. + /// + public ITaskItem SDKReferenceItem + { + get; + private set; + } + + /// + /// Override object equals to use the equals redist in this object. + /// + public override bool Equals(object obj) + { + ResolvedReferenceAssembly reference = obj as ResolvedReferenceAssembly; + if (reference == null) + { + return false; + } + + return Equals(reference); + } + + /// + /// Override get hash code + /// + public override int GetHashCode() + { + return FileName.GetHashCode(); + } + + /// + /// Are two resolved references items Equal + /// + public bool Equals(ResolvedReferenceAssembly other) + { + if (other == null) + { + return false; + } + + if (Object.ReferenceEquals(other, this)) + { + return true; + } + + // We only care about the file name and not the path because if they have the same file name but different paths then they will likely contain + // the same namespaces and the compiler does not like to have two references with the same namespace passed at once without aliasing and + // we have no way to do aliasing per assembly since we are grabbing a bunch of files at once.) + return String.Equals(this.FileName, other.FileName, StringComparison.OrdinalIgnoreCase); + } + } + + /// + /// Class which represents a resolved redist file + /// + private class ResolvedRedistFile : IEquatable + { + /// + /// Constructor + /// + public ResolvedRedistFile(ITaskItem sdkReferenceItem, string redistFile, string targetPath, string targetRoot) + { + RedistFile = redistFile; + TargetPath = targetPath; + TargetRoot = targetRoot; + SDKReferenceItem = sdkReferenceItem; + } + + /// + /// What is the file name + /// + public string RedistFile + { + get; + private set; + } + + /// + /// What is the targetPath for the redist file. + /// + public string TargetPath + { + get; + private set; + } + + /// + /// What is the root directory of the target path + /// + public string TargetRoot + { + get; + private set; + } + + /// + /// Original resolved SDK reference item passed in. + /// + public ITaskItem SDKReferenceItem + { + get; + private set; + } + + /// + /// Override object equals to use the equals redist in this object. + /// + public override bool Equals(object obj) + { + ResolvedReferenceAssembly reference = obj as ResolvedReferenceAssembly; + if (reference == null) + { + return false; + } + + return Equals(reference); + } + + /// + /// Override get hash code + /// + public override int GetHashCode() + { + return TargetPath.GetHashCode(); + } + + /// + /// Are two resolved references items Equal + /// + public bool Equals(ResolvedRedistFile other) + { + if (other == null) + { + return false; + } + + if (Object.ReferenceEquals(other, this)) + { + return true; + } + + // We only care about the target path since that is the location relative to the package root where the redist file + // will be copied. + return String.Equals(this.TargetPath, other.TargetPath, StringComparison.OrdinalIgnoreCase); + } + } + + #region Cache Serialization + + /// + /// Methods which are used to save and read the cache files per sdk from and to disk. + /// + private class SDKFilesCache + { + /// + /// Threadsafe queue which contains exceptions throws during cache file reading and writing. + /// + private ConcurrentQueue _exceptionMessages; + + /// + /// Delegate to get the assembly name + /// + private GetAssemblyName _getAssemblyName; + + /// + /// Get the image runtime version from a afile + /// + private GetAssemblyRuntimeVersion _getRuntimeVersion; + + /// + /// File exists delegate + /// + private FileExists _fileExists; + + /// + /// Location for the cache files to be written to + /// + private string _cacheFileDirectory; + + /// + /// Constructor + /// + internal SDKFilesCache(ConcurrentQueue exceptionQueue, string cacheFileDirectory, GetAssemblyName getAssemblyName, GetAssemblyRuntimeVersion getRuntimeVersion, FileExists fileExists) + { + _exceptionMessages = exceptionQueue; + _cacheFileDirectory = cacheFileDirectory; + _getAssemblyName = getAssemblyName; + _getRuntimeVersion = getRuntimeVersion; + _fileExists = fileExists; + } + + /// + /// Load reference assembly information from the cache file + /// + internal SDKInfo LoadAssemblyListFromCacheFile(string sdkIdentity, string sdkRoot) + { + string[] existingCacheFiles = Directory.GetFiles(_cacheFileDirectory, GetCacheFileName(sdkIdentity, sdkRoot, "*")); + + try + { + if (existingCacheFiles.Length > 0 && File.Exists(existingCacheFiles[0])) + { + string referencesCacheFile = existingCacheFiles[0]; + using (FileStream fs = new FileStream(referencesCacheFile, FileMode.Open)) + { + BinaryFormatter formatter = new BinaryFormatter(); + return (SDKInfo)formatter.Deserialize(fs); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Queue up for later logging, does not matter if the file is deleted or not + _exceptionMessages.Enqueue(ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ProblemReadingCacheFile", existingCacheFiles.Length > 0 ? String.Empty : existingCacheFiles[0], e.Message)); + } + + return null; + } + + /// + /// Save assembly reference information to the cache file + /// + internal void SaveAssemblyListToCacheFile(object data) + { + string referencesCacheFile = String.Empty; + try + { + SaveContext saveContext = data as SaveContext; + SDKInfo cacheFileInfo = saveContext.Assemblies; + + referencesCacheFile = Path.Combine(_cacheFileDirectory, GetCacheFileName(saveContext.SdkIdentity, saveContext.SdkRoot, cacheFileInfo.Hash.ToString("X", CultureInfo.InvariantCulture))); + string[] existingCacheFiles = Directory.GetFiles(_cacheFileDirectory, GetCacheFileName(saveContext.SdkIdentity, saveContext.SdkRoot, "*")); + + // First delete any existing cache files + foreach (string existingCacheFile in existingCacheFiles) + { + try + { + File.Delete(existingCacheFile); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Queue up for later logging, does not matter if the file is deleted or not + _exceptionMessages.Enqueue(ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ProblemDeletingCacheFile", existingCacheFile, e.Message)); + } + } + + BinaryFormatter formatter = new BinaryFormatter(); + using (FileStream fs = new FileStream(referencesCacheFile, FileMode.Create)) + { + formatter.Serialize(fs, cacheFileInfo); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Queue up for later logging, does not matter if the cache got written + _exceptionMessages.Enqueue(ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ProblemWritingCacheFile", referencesCacheFile, e.Message)); + } + } + + /// + /// Get references from the paths provided, and populate the provided cache + /// + internal SDKInfo GetCacheFileInfoFromSDK(string sdkIdentity, string sdkRootDirectory, string[] sdkManifestReferences) + { + ConcurrentDictionary references = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + ConcurrentDictionary> directoryToFileList = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + + List directoriesToHash = new List(); + + var referenceDirectories = GetAllReferenceDirectories(sdkRootDirectory); + var redistDirectories = GetAllRedistDirectories(sdkRootDirectory); + + directoriesToHash.AddRange(referenceDirectories); + directoriesToHash.AddRange(redistDirectories); + + if (sdkManifestReferences != null && sdkManifestReferences.Length > 0) + { + // Manifest driven- get the info from the known list + PopulateReferencesDictionaryFromManifestPaths(directoryToFileList, references, sdkManifestReferences); + } + else + { + PopulateReferencesDictionaryFromPaths(directoryToFileList, references, referenceDirectories); + } + + PopulateRedistDictionaryFromPaths(directoryToFileList, redistDirectories); + + SDKInfo cacheInfo = new SDKInfo(references, directoryToFileList, FileUtilities.GetHexHash(sdkIdentity), FileUtilities.GetPathsHash(directoriesToHash)); + return cacheInfo; + } + + /// + /// Populate an existing assembly dictionary for the given framework moniker utilizing provided manifest reference information + /// + internal void PopulateReferencesDictionaryFromManifestPaths(ConcurrentDictionary> referencesByDirectory, ConcurrentDictionary references, string[] sdkManifestReferences) + { + // Sort by directory + var groupedByDirectory = + from reference in sdkManifestReferences + group reference by Path.GetDirectoryName(reference); + + foreach (var group in groupedByDirectory) + { + referencesByDirectory.TryAdd(group.Key, group.ToList()); + } + + Parallel.ForEach(sdkManifestReferences, reference => { references.TryAdd(reference, GetSDKReferenceInfo(reference)); }); + } + + /// + /// Populate an existing assembly dictionary for the given framework moniker + /// + internal void PopulateReferencesDictionaryFromPaths(ConcurrentDictionary> referencesByDirectory, ConcurrentDictionary references, IEnumerable referenceDirectories) + { + // Add each folder to the dictionary along with a list of all of files inside of it + Parallel.ForEach( + referenceDirectories, + path => + { + List files = Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly).ToList(); + referencesByDirectory.TryAdd(path, files); + + Parallel.ForEach(files, filePath => { references.TryAdd(filePath, GetSDKReferenceInfo(filePath)); }); + }); + } + + /// + /// Populate an existing assembly dictionary for the given framework moniker + /// + internal void PopulateRedistDictionaryFromPaths(ConcurrentDictionary> redistFilesByDirectory, IEnumerable redistDirectories) + { + // Add each folder to the dictionary along with a list of all of files inside of it + Parallel.ForEach( + redistDirectories, + path => + { + List files = Directory.GetFiles(path, "*", SearchOption.TopDirectoryOnly).ToList(); + redistFilesByDirectory.TryAdd(path, files); + }); + } + + /// + /// Is the assembly list cache file up to date. + /// This is done by comparing the last write time of the cache file to the last write time of the code. + /// If our code is newer than the last write time of the cache file then there may be some different serialization used so we should say it is out of date and just regenerate it. + /// + internal bool IsAssemblyListCacheFileUpToDate(string sdkIdentity, string sdkRoot, string cacheFileFolder) + { + // The hash is the hash of last modified times for the passed in reference paths. A directory gets modified if a file is added, deleted, or modified inside of the inside of the directory itself (modifications to child folders are not seen however). + List directoriesToHash = new List(); + directoriesToHash.AddRange(GetAllReferenceDirectories(sdkRoot)); + directoriesToHash.AddRange(GetAllRedistDirectories(sdkRoot)); + + int hash = FileUtilities.GetPathsHash(directoriesToHash); + string referencesCacheFile = Path.Combine(cacheFileFolder, GetCacheFileName(sdkIdentity, sdkRoot, hash.ToString("X", CultureInfo.InvariantCulture))); + + bool upToDate = false; + DateTime referencesCacheFileLastWriteTimeUtc = File.GetLastWriteTimeUtc(referencesCacheFile); + + string currentAssembly = String.Empty; + try + { + currentAssembly = Assembly.GetExecutingAssembly().CodeBase; + Uri codeBase = new Uri(currentAssembly); + DateTime currentCodeLastWriteTime = File.GetLastWriteTimeUtc(codeBase.LocalPath); + if (File.Exists(referencesCacheFile) && currentCodeLastWriteTime < referencesCacheFileLastWriteTimeUtc) + { + return true; + } + } + catch (Exception ex) + { + if (ExceptionHandling.IsCriticalException(ex)) + { + throw; + } + + // Queue up for later logging, does not matter if the cache got written + _exceptionMessages.Enqueue(ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ProblemGeneratingHash", currentAssembly, ex.Message)); + + // Don't care why the check failed we will just say the cache is not up to date. + } + + return upToDate; + } + + /// + /// Generate an SDKReferenceInfo object + /// + internal SdkReferenceInfo GetSDKReferenceInfo(string referencePath) + { + string imageRuntimeVersion = null; + bool isManagedWinMD = false; + bool isWinMDFile = false; + string fusionName = null; + + try + { + AssemblyNameExtension assemblyNameExtension = _getAssemblyName(referencePath); + if (assemblyNameExtension != null) + { + AssemblyName assembly = assemblyNameExtension.AssemblyName; + isWinMDFile = AssemblyInformation.IsWinMDFile(referencePath, _getRuntimeVersion, _fileExists, out imageRuntimeVersion, out isManagedWinMD); + if (assembly != null) + { + fusionName = assembly.FullName; + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // Queue up for later logging, does not matter if the cache got written + _exceptionMessages.Enqueue(ResourceUtilities.FormatResourceString("GetSDKReferenceFiles.ProblemGettingAssemblyMetadata", referencePath, e.Message)); + } + + SdkReferenceInfo referenceInfo = new SdkReferenceInfo(fusionName, imageRuntimeVersion, isWinMDFile, isManagedWinMD); + return referenceInfo; + } + + /// + /// Generate cache file name from sdkIdentity, sdkRoot and suffixHash. + /// + private static string GetCacheFileName(string sdkIdentity, string sdkRoot, string suffixHash) + { + string identityHash = FileUtilities.GetHexHash(sdkIdentity); + string rootHash = FileUtilities.GetHexHash(sdkRoot); + + return sdkIdentity + ",Set=" + identityHash + "-" + rootHash + ",Hash=" + suffixHash + ".dat"; + } + + /// + /// Get all redist subdirectories under the given path + /// + private IEnumerable GetAllRedistDirectories(string sdkRoot) + { + string redistPath = Path.Combine(sdkRoot, "Redist"); + if (FileUtilities.DirectoryExistsNoThrow(redistPath)) + { + return Directory.GetDirectories(redistPath, "*", SearchOption.AllDirectories).ToList(); + } + + return new string[0]; + } + + /// + /// Get all reference subdirectories under the given path + /// + private IEnumerable GetAllReferenceDirectories(string sdkRoot) + { + string referencesPath = Path.Combine(sdkRoot, "References"); + if (FileUtilities.DirectoryExistsNoThrow(referencesPath)) + { + return Directory.GetDirectories(referencesPath, "*", SearchOption.AllDirectories).ToList(); + } + + return new string[0]; + } + } + + /// + /// Class to contain some identity information about a file in an sdk + /// + [Serializable] + private class SdkReferenceInfo + { + /// + /// Constructor + /// + public SdkReferenceInfo(string fusionName, string imageRuntime, bool isWinMD, bool isManagedWinmd) + { + this.FusionName = fusionName; + this.ImageRuntime = imageRuntime; + this.IsWinMD = isWinMD; + this.IsManagedWinmd = isManagedWinmd; + } + + #region Properties + /// + /// The fusionName + /// + public string FusionName + { + get; + private set; + } + + /// + /// Is the file a winmd or not + /// + public bool IsWinMD + { + get; + private set; + } + + /// + /// Is the file a managed winmd or not + /// + public bool IsManagedWinmd + { + get; + private set; + } + + /// + /// What is the imageruntime information on it. + /// + public string ImageRuntime + { + get; + private set; + } + + #endregion + } + + /// + /// Structure that contains the on disk representation of the SDK in memory + /// + [Serializable] + private class SDKInfo + { + /// + /// Constructor + /// + public SDKInfo(ConcurrentDictionary pathToReferenceMetadata, ConcurrentDictionary> directoryToFileList, string cacheFileSuffix, int cacheHash) + { + PathToReferenceMetadata = pathToReferenceMetadata; + DirectoryToFileList = directoryToFileList; + Suffix = cacheFileSuffix; + Hash = cacheHash; + } + + /// + /// A dictionary which maps a file path to a structure that contain some metadata information about that file. + /// + public ConcurrentDictionary PathToReferenceMetadata { get; private set; } + + /// + /// Dictionary which maps a directory to a list of file names within that directory. This is used to shortcut hitting the disk for the list of files inside of it. + /// + public ConcurrentDictionary> DirectoryToFileList { get; private set; } + + /// + /// Suffix for the cache file + /// + public string Suffix { get; private set; } + + /// + /// Hashset + /// + public int Hash { get; private set; } + } + + /// + /// This class represents the context information used by the background cache serialization thread. + /// + private class SaveContext + { + /// + /// Constructor + /// + public SaveContext(string sdkIdentity, string sdkRoot, SDKInfo assemblies) + { + this.SdkIdentity = sdkIdentity; + this.SdkRoot = sdkRoot; + this.Assemblies = assemblies; + } + + /// + /// Identity of the sdk + /// + public string SdkIdentity { get; private set; } + + /// + /// Root path of the sdk + /// + public string SdkRoot { get; private set; } + + /// + /// Assembly metadata information + /// + public SDKInfo Assemblies { get; private set; } + } + #endregion + } +} diff --git a/src/XMakeTasks/IAnalyzerHostObject.cs b/src/XMakeTasks/IAnalyzerHostObject.cs new file mode 100644 index 00000000000..c34d279509a --- /dev/null +++ b/src/XMakeTasks/IAnalyzerHostObject.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines an interface for the Vbc/Csc tasks to communicate information about + /// analyzers and rulesets to the IDE. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("B5A95716-2053-4B70-9FBF-E4148EBA96BC")] + public interface IAnalyzerHostObject + { + bool SetAnalyzers(ITaskItem[] analyzers); + bool SetRuleSet(string ruleSetFile); + bool SetAdditionalFiles(ITaskItem[] additionalFiles); + } +} \ No newline at end of file diff --git a/src/XMakeTasks/IComReferenceResolver.cs b/src/XMakeTasks/IComReferenceResolver.cs new file mode 100644 index 00000000000..6ff2365942b --- /dev/null +++ b/src/XMakeTasks/IComReferenceResolver.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +namespace Microsoft.Build.Tasks +{ + /* + * Interface: IComReferenceResolver + * + * Callback interface for COM references to resolve their dependencies + */ + internal interface IComReferenceResolver + { + /* + * Method: ResolveComClassicReference + * + * Resolves a COM classic reference given the type library attributes and the type of wrapper to use. + * If wrapper type is not specified, this method will first look for an existing reference in the project, + * fall back to looking for a PIA and finally try to generate a regular tlbimp wrapper. + * + * This method is available for references to call back to resolve their dependencies + */ + bool ResolveComClassicReference(TYPELIBATTR typeLibAttr, string outputDirectory, string wrapperType, string refName, out ComReferenceWrapperInfo wrapperInfo); + + /* + * Method: ResolveNetAssemblyReference + * + * Resolves a .NET assembly reference using the list of resolved managed references supplied to the task. + * + * This method is available for references to call back to resolve their dependencies + */ + bool ResolveNetAssemblyReference(string assemblyName, out string assemblyPath); + + /* + * Method: ResolveComAssemblyReference + * + * Resolves a COM wrapper assembly reference based on the COM references resolved so far. This method is necessary + * for Ax wrappers only, so all necessary references will be resolved by then (since we resolve them in + * the following order: pia, tlbimp, aximp) + * + * This method is available for references to call back to resolve their dependencies + */ + bool ResolveComAssemblyReference(string assemblyName, out string assemblyPath); + } +} diff --git a/src/XMakeTasks/ICscHostObject.cs b/src/XMakeTasks/ICscHostObject.cs new file mode 100644 index 00000000000..03194abe4d1 --- /dev/null +++ b/src/XMakeTasks/ICscHostObject.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /* + * Interface: ICscHostObject + * + * Defines an interface for the Csc task to communicate with the IDE. In particular, + * the Csc task will delegate the actual compilation to the IDE, rather than shelling + * out to the command-line compilers. + * + */ + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("8520CC4D-64DC-4855-BE3F-4C28CCE048EE")] + public interface ICscHostObject : ITaskHost + { + bool IsDesignTime(); + bool Compile(); + + void BeginInitialization(); + bool EndInitialization(out string errorMessage, out int errorCode); + + bool SetAdditionalLibPaths(string[] additionalLibPaths); + bool SetAddModules(string[] addModules); + bool SetAllowUnsafeBlocks(bool allowUnsafeBlocks); + bool SetBaseAddress(string baseAddress); + bool SetCheckForOverflowUnderflow(bool checkForOverflowUnderflow); + bool SetCodePage(int codePage); + bool SetDebugType(string debugType); + bool SetDefineConstants(string defineConstants); + bool SetDelaySign(bool delaySignExplicitlySet, bool delaySign); + bool SetDisabledWarnings(string disabledWarnings); + bool SetDocumentationFile(string documentationFile); + bool SetEmitDebugInformation(bool emitDebugInformation); + bool SetErrorReport(string errorReport); + bool SetFileAlignment(int fileAlignment); + bool SetGenerateFullPaths(bool generateFullPaths); + bool SetKeyContainer(string keyContainer); + bool SetKeyFile(string keyFile); + bool SetLangVersion(string langVersion); + bool SetLinkResources(ITaskItem[] linkResources); + bool SetMainEntryPoint(string targetType, string mainEntryPoint); + bool SetModuleAssemblyName(string moduleAssemblyName); + bool SetNoConfig(bool noConfig); + bool SetNoStandardLib(bool noStandardLib); + bool SetOptimize(bool optimize); + bool SetOutputAssembly(string outputAssembly); + bool SetPlatform(string platform); + bool SetPdbFile(string pdbFile); + bool SetReferences(ITaskItem[] references); + bool SetResources(ITaskItem[] resources); + bool SetResponseFiles(ITaskItem[] responseFiles); + bool SetSources(ITaskItem[] sources); + bool SetTargetType(string targetType); + bool SetTreatWarningsAsErrors(bool treatWarningsAsErrors); + bool SetWarningLevel(int warningLevel); + bool SetWarningsAsErrors(string warningsAsErrors); + bool SetWarningsNotAsErrors(string warningsNotAsErrors); + bool SetWin32Icon(string win32Icon); + bool SetWin32Resource(string win32Resource); + + bool IsUpToDate(); + } +} diff --git a/src/XMakeTasks/ICscHostObject2.cs b/src/XMakeTasks/ICscHostObject2.cs new file mode 100644 index 00000000000..e243bda6eb2 --- /dev/null +++ b/src/XMakeTasks/ICscHostObject2.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /* + * Interface: ICscHostObject2 + * + * Defines an interface for the Csc task to communicate with the IDE. In particular, + * the Csc task will delegate the actual compilation to the IDE, rather than shelling + * out to the command-line compilers. + * + */ + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("D6D4E228-259A-4076-B5D0-0627338BCC10")] + public interface ICscHostObject2 : ICscHostObject + { + bool SetWin32Manifest(string win32Manifest); + } +} diff --git a/src/XMakeTasks/ICscHostObject3.cs b/src/XMakeTasks/ICscHostObject3.cs new file mode 100644 index 00000000000..2502953beb6 --- /dev/null +++ b/src/XMakeTasks/ICscHostObject3.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /* + * Interface: ICscHostObject3 + * + * Defines an interface for the Csc task to communicate with the IDE. In particular, + * the Csc task will delegate the actual compilation to the IDE, rather than shelling + * out to the command-line compilers. + * + */ + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("F9353662-F1ED-4a23-A323-5F5047E85F5D")] + public interface ICscHostObject3 : ICscHostObject2 + { + bool SetApplicationConfiguration(string applicationConfiguration); + } +} diff --git a/src/XMakeTasks/ICscHostObject4.cs b/src/XMakeTasks/ICscHostObject4.cs new file mode 100644 index 00000000000..1a88692c404 --- /dev/null +++ b/src/XMakeTasks/ICscHostObject4.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /* + * Interface: ICscHostObject4 + * + * Defines an interface for the Csc task to communicate with the IDE. In particular, + * the Csc task will delegate the actual compilation to the IDE, rather than shelling + * out to the command-line compilers. + * + */ + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("0DDB496F-C93C-492C-87F1-90B6FDBAA833")] + public interface ICscHostObject4 : ICscHostObject3 + { + bool SetPlatformWith32BitPreference(string platformWith32BitPreference); + bool SetHighEntropyVA(bool highEntropyVA); + bool SetSubsystemVersion(string subsystemVersion); + } +} diff --git a/src/XMakeTasks/IVbcHostObject.cs b/src/XMakeTasks/IVbcHostObject.cs new file mode 100644 index 00000000000..1802a636b1b --- /dev/null +++ b/src/XMakeTasks/IVbcHostObject.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines an interface for the Vbc task to communicate with the IDE. In particular, + /// the Vbc task will delegate the actual compilation to the IDE, rather than shelling + /// out to the command-line compilers. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("7D7AC3BE-253A-40e8-A3FF-357D0DA7C47A")] + public interface IVbcHostObject : ITaskHost + { + void BeginInitialization(); + void EndInitialization(); + + bool IsDesignTime(); + bool Compile(); + + bool SetAdditionalLibPaths(string[] additionalLibPaths); + bool SetAddModules(string[] addModules); + bool SetBaseAddress(string targetType, string baseAddress); + bool SetCodePage(int codePage); + bool SetDebugType(bool emitDebugInformation, string debugType); + bool SetDefineConstants(string defineConstants); + bool SetDelaySign(bool delaySign); + bool SetDisabledWarnings(string disabledWarnings); + bool SetDocumentationFile(string documentationFile); + bool SetErrorReport(string errorReport); + bool SetFileAlignment(int fileAlignment); + bool SetGenerateDocumentation(bool generateDocumentation); + bool SetImports(ITaskItem[] importsList); + bool SetKeyContainer(string keyContainer); + bool SetKeyFile(string keyFile); + bool SetLinkResources(ITaskItem[] linkResources); + bool SetMainEntryPoint(string mainEntryPoint); + bool SetNoConfig(bool noConfig); + bool SetNoStandardLib(bool noStandardLib); + bool SetNoWarnings(bool noWarnings); + bool SetOptimize(bool optimize); + bool SetOptionCompare(string optionCompare); + bool SetOptionExplicit(bool optionExplicit); + bool SetOptionStrict(bool optionStrict); + bool SetOptionStrictType(string optionStrictType); + bool SetOutputAssembly(string outputAssembly); + bool SetPlatform(string platform); + bool SetReferences(ITaskItem[] references); + bool SetRemoveIntegerChecks(bool removeIntegerChecks); + bool SetResources(ITaskItem[] resources); + bool SetResponseFiles(ITaskItem[] responseFiles); + bool SetRootNamespace(string rootNamespace); + bool SetSdkPath(string sdkPath); + bool SetSources(ITaskItem[] sources); + bool SetTargetCompactFramework(bool targetCompactFramework); + bool SetTargetType(string targetType); + bool SetTreatWarningsAsErrors(bool treatWarningsAsErrors); + bool SetWarningsAsErrors(string warningsAsErrors); + bool SetWarningsNotAsErrors(string warningsNotAsErrors); + bool SetWin32Icon(string win32Icon); + bool SetWin32Resource(string win32Resource); + + bool IsUpToDate(); + } +} diff --git a/src/XMakeTasks/IVbcHostObject2.cs b/src/XMakeTasks/IVbcHostObject2.cs new file mode 100644 index 00000000000..27703a1cb77 --- /dev/null +++ b/src/XMakeTasks/IVbcHostObject2.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines an interface for the Vbc task to communicate with the IDE. In particular, + /// the Vbc task will delegate the actual compilation to the IDE, rather than shelling + /// out to the command-line compilers. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("f59afc84-d102-48b1-a090-1b90c79d3e09")] + public interface IVbcHostObject2 : IVbcHostObject + { + bool SetOptionInfer(bool optionInfer); + bool SetModuleAssemblyName(string moduleAssemblyName); + bool SetWin32Manifest(string win32Manifest); + } +} diff --git a/src/XMakeTasks/IVbcHostObject3.cs b/src/XMakeTasks/IVbcHostObject3.cs new file mode 100644 index 00000000000..e2703a173e7 --- /dev/null +++ b/src/XMakeTasks/IVbcHostObject3.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines an interface for the Vbc task to communicate with the IDE. In particular, + /// the Vbc task will delegate the actual compilation to the IDE, rather than shelling + /// out to the command-line compilers. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("1186fe8f-8aba-48d6-8ce3-32ca42f53728")] + public interface IVbcHostObject3 : IVbcHostObject2 + { + bool SetLanguageVersion(string languageVersion); + } +} diff --git a/src/XMakeTasks/IVbcHostObject4.cs b/src/XMakeTasks/IVbcHostObject4.cs new file mode 100644 index 00000000000..6cfb5cdc4fd --- /dev/null +++ b/src/XMakeTasks/IVbcHostObject4.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines an interface for the Vbc task to communicate with the IDE. In particular, + /// the Vbc task will delegate the actual compilation to the IDE, rather than shelling + /// out to the command-line compilers. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("2AE3233C-8AB3-48A0-9ED9-6E3545B3C566")] + public interface IVbcHostObject4 : IVbcHostObject3 + { + bool SetVBRuntime(string VBRuntime); + } +} diff --git a/src/XMakeTasks/IVbcHostObject5.cs b/src/XMakeTasks/IVbcHostObject5.cs new file mode 100644 index 00000000000..d20292d6bb1 --- /dev/null +++ b/src/XMakeTasks/IVbcHostObject5.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines an interface that proffers a free threaded host object that + /// allows for background threads to call directly (avoids marshalling + /// to the UI thread. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("5ACF41FF-6F2B-4623-8146-740C89212B21")] + public interface IVbcHostObject5 : IVbcHostObject4 + { + IVbcHostObjectFreeThreaded GetFreeThreadedHostObject(); + [PreserveSig()] + int CompileAsync(out IntPtr buildSucceededEvent, out IntPtr buildFailedEvent); + [PreserveSig()] + int EndCompile(bool buildSuccess); + + bool SetPlatformWith32BitPreference(string platformWith32BitPreference); + bool SetHighEntropyVA(bool highEntropyVA); + bool SetSubsystemVersion(string subsystemVersion); + } +} diff --git a/src/XMakeTasks/IVbcHostObjectFreeThreaded.cs b/src/XMakeTasks/IVbcHostObjectFreeThreaded.cs new file mode 100644 index 00000000000..d78cb42b509 --- /dev/null +++ b/src/XMakeTasks/IVbcHostObjectFreeThreaded.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Build.Tasks.Hosting +{ + /// + /// Defines a free threaded interface for the Vbc task to communicate with the IDE. In particular, + /// the Vbc task will delegate the actual compilation to the IDE, rather than shelling + /// out to the command-line compilers. + /// This particular version of Compile (unlike the IVbcHostObject::Compile) is not marshalled back to the UI + /// thread. The implementor of the interface is responsible for any marshalling. + /// This was added to allow some of the implementors code to run on the BG thread from which VBC Task is being + /// called from. + /// + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [ComVisible(true)] + [Guid("ECCF972F-8C2D-4F51-9746-9288661DE2CB")] + public interface IVbcHostObjectFreeThreaded + { + bool Compile(); + } +} diff --git a/src/XMakeTasks/InstalledSDKResolver.cs b/src/XMakeTasks/InstalledSDKResolver.cs new file mode 100644 index 00000000000..673d19c925b --- /dev/null +++ b/src/XMakeTasks/InstalledSDKResolver.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Resolve reference which have SDKName metadata on them +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks +{ + /// + ///There is no search path element because the only way to get this resolver is by having the SDKName metadata on the reference. + /// + internal class InstalledSDKResolver : Resolver + { + /// + /// Resolved SDKs + /// + private Dictionary _resolvedSDKs; + + /// + /// Construct. + /// + public InstalledSDKResolver(Dictionary resolvedSDKs, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false) + { + _resolvedSDKs = resolvedSDKs; + } + + /// + /// Resolve references which are found in a specific SDK + /// + public override bool Resolve + ( + AssemblyNameExtension assemblyName, + string sdkName, + string rawFileNameCandidate, + bool isPrimaryProjectReference, + bool wantSpecificVersion, + string[] executableExtensions, + string hintPath, + string assemblyFolderKey, + ArrayList assembliesConsideredAndRejected, + out string foundPath, + out bool userRequestedSpecificFile + ) + { + foundPath = null; + userRequestedSpecificFile = false; + + if (assemblyName != null) + { + // We have found a resolved SDK item that matches the one on the reference items. + if (_resolvedSDKs.ContainsKey(sdkName)) + { + ITaskItem resolvedSDK = _resolvedSDKs[sdkName]; + + string sdkDirectory = resolvedSDK.ItemSpec; + string configuration = resolvedSDK.GetMetadata("TargetedSDKConfiguration"); + string architecture = resolvedSDK.GetMetadata("TargetedSDKArchitecture"); + + string referenceAssemblyFilePath = Path.Combine(sdkDirectory, "References", configuration, architecture); + string referenceAssemblyCommonArchFilePath = Path.Combine(sdkDirectory, "References", "CommonConfiguration", architecture); + string referenceAssemblyPathNeutral = Path.Combine(sdkDirectory, "References", configuration, "Neutral"); + string referenceAssemblyArchFilePathNeutral = Path.Combine(sdkDirectory, "References", "CommonConfiguration", "Neutral"); + + string[] searchLocations = new string[] + { + referenceAssemblyFilePath, // Config-Arch + referenceAssemblyPathNeutral, // Config-Neutral + referenceAssemblyCommonArchFilePath, // CommonArch-Config + referenceAssemblyArchFilePathNeutral // CommonArch-Neutral + }; + + // Lets try and resovle from the windowsmetadata directory first + string resolvedPath = null; + + // Go through the search locations and find the assembly + foreach (string searchLocation in searchLocations) + { + resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, searchLocation, assembliesConsideredAndRejected); + + if (resolvedPath != null) + { + foundPath = resolvedPath; + return true; + } + } + } + } + + return false; + } + } +} diff --git a/src/XMakeTasks/InvalidParameterValueException.cs b/src/XMakeTasks/InvalidParameterValueException.cs new file mode 100644 index 00000000000..64d6316fc61 --- /dev/null +++ b/src/XMakeTasks/InvalidParameterValueException.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.Serialization; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// A parameter passed to the task was invalid. + /// Currently used by the RAR task. + /// ArgumentException was not used because it does not have a property for ActualValue. + /// ArgumentOutOfRangeException does, but it appends its own message to yours. + /// + [Serializable] + internal sealed class InvalidParameterValueException : Exception + { + /// + /// Don't allow default construction. + /// + private InvalidParameterValueException() + { + } + + /// + /// Constructor + /// + internal InvalidParameterValueException(string paramName, string actualValue, string message) + : this(paramName, actualValue, message, null) + { + } + + /// + /// Constructor + /// + internal InvalidParameterValueException(string paramName, string actualValue, string message, Exception innerException) + : base(message, innerException) + { + ParamName = paramName; + ActualValue = actualValue; + } + + /// + /// Constructor + /// + private InvalidParameterValueException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private string _paramName; + + /// + /// The name of the parameter. + /// + public string ParamName + { + get { return _paramName; } + set { _paramName = value; } + } + + private string _actualValue; + + /// + /// The value supplied, that was bad. + /// + public string ActualValue + { + get { return _actualValue; } + set { _actualValue = value; } + } + } +} diff --git a/src/XMakeTasks/LC.cs b/src/XMakeTasks/LC.cs new file mode 100644 index 00000000000..8b2ce4bebd4 --- /dev/null +++ b/src/XMakeTasks/LC.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// The License Compiler task + /// + public class LC : ToolTaskExtension + { + #region Constructors + + /// + /// public constructor + /// + public LC() + { + // do nothing + } + + #endregion + + #region Input/output properties + + /// + /// Specifies the items that contain licensed components that need to be included in the .licenses file + /// + [Required] + public ITaskItem[] Sources + { + set { Bag["Sources"] = value; } + get { return (ITaskItem[])Bag["Sources"]; } + } + + /// + /// The name of the .licenses file, output only. It's inferred from LicenseTarget and OutputDirectory. + /// + [Output] + public ITaskItem OutputLicense + { + set { Bag["OutputLicense"] = value; } + get { return (ITaskItem)Bag["OutputLicense"]; } + } + + /// + /// Specifies the executable for which the .licenses files are being generated + /// + [Required] + public ITaskItem LicenseTarget + { + set { Bag["LicenseTarget"] = value; } + get { return (ITaskItem)Bag["LicenseTarget"]; } + } + + /// + /// Output directory for the generated .licenses file + /// + /// + public string OutputDirectory + { + set { Bag["OutputDirectory"] = value; } + get { return (string)Bag["OutputDirectory"]; } + } + + /// + /// Specifies the referenced components (licensed controls and possibly their dependent assemblies) + /// to load when generating the .license file. + /// + public ITaskItem[] ReferencedAssemblies + { + set { Bag["ReferencedAssemblies"] = value; } + get { return (ITaskItem[])Bag["ReferencedAssemblies"]; } + } + + /// + /// Suppresses the display of the startup banner + /// + public bool NoLogo + { + set { Bag["NoLogo"] = value; } + get { return GetBoolParameterWithDefault("NoLogo", false); } + } + + public string SdkToolsPath + { + set { Bag["SdkToolsPath"] = value; } + get { return (string)Bag["SdkToolsPath"]; } + } + #endregion + + #region Class properties + + /// + /// The name of the tool to execute + /// + protected override string ToolName + { + get + { + return "LC.exe"; + } + } + + #endregion + + #region Methods + + /// + /// Validate the task arguments, log any warnings/errors + /// + /// true if arguments are corrent enough to continue processing, false otherwise + protected override bool ValidateParameters() + { + // if all the Required attributes are set, we're good to go. + return true; + } + + /// + /// Determing the path to lc.exe + /// + /// path to lc.exe, null if not found + protected override string GenerateFullPathToTool() + { + string pathToTool = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, Microsoft.Build.Utilities.ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolName, Log, true); + return pathToTool; + } + + /// + /// Generates response file with arguments for lc.exe + /// + /// command line builder class to add arguments to the response file + protected internal override void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendSwitchIfNotNull("/target:", LicenseTarget.ItemSpec); + + foreach (ITaskItem item in Sources) + { + commandLine.AppendSwitchIfNotNull("/complist:", item.ItemSpec); + } + + commandLine.AppendSwitchIfNotNull("/outdir:", OutputDirectory); + + if (ReferencedAssemblies != null) + { + foreach (ITaskItem item in ReferencedAssemblies) + { + commandLine.AppendSwitchIfNotNull("/i:", item.ItemSpec); + } + } + + commandLine.AppendWhenTrue("/nologo", Bag, "NoLogo"); + + // generate the output file name + string outputPath = LicenseTarget.ItemSpec + ".licenses"; + + if (OutputDirectory != null) + outputPath = Path.Combine(OutputDirectory, outputPath); + + OutputLicense = new TaskItem(outputPath); + } + + #endregion + } +} diff --git a/src/XMakeTasks/ListOperators/FindUnderPath.cs b/src/XMakeTasks/ListOperators/FindUnderPath.cs new file mode 100644 index 00000000000..c9134f3a832 --- /dev/null +++ b/src/XMakeTasks/ListOperators/FindUnderPath.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Given a list of items, determine which are in the cone of the folder passed in and which aren't. + /// + public class FindUnderPath : TaskExtension + { + /// + /// Construct. + /// + public FindUnderPath() + { + } + + private bool _updateToAbsolutePaths = false; + private ITaskItem _path = null; + private ITaskItem[] _files = new TaskItem[0]; + private ITaskItem[] _inPath = null; + private ITaskItem[] _outOfPath = null; + + /// + /// Filter based on whether items fall under this path or not. + /// + [Required] + public ITaskItem Path + { + get { return _path; } + set { _path = value; } + } + + /// + /// Files to consider. + /// + public ITaskItem[] Files + { + get { return _files; } + set { _files = value; } + } + + /// + /// Set to true if the paths of the output items should be updated to be absolute + /// + public bool UpdateToAbsolutePaths + { + get { return _updateToAbsolutePaths; } + set { _updateToAbsolutePaths = value; } + } + + /// + /// Files that were inside of Path. + /// + [Output] + public ITaskItem[] InPath + { + get { return _inPath; } + set { _inPath = value; } + } + + /// + /// Files that were outside of Path. + /// + [Output] + public ITaskItem[] OutOfPath + { + get { return _outOfPath; } + set { _outOfPath = value; } + } + + /// + /// Execute the task. + /// + /// + public override bool Execute() + { + ArrayList inPathList = new ArrayList(); + ArrayList outOfPathList = new ArrayList(); + + string conePath; + + try + { + conePath = OpportunisticIntern.InternStringIfPossible(System.IO.Path.GetFullPath(_path.ItemSpec)); + conePath = FileUtilities.EnsureTrailingSlash(conePath); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + Log.LogErrorWithCodeFromResources(null, "", 0, 0, 0, 0, + "FindUnderPath.InvalidParameter", "Path", _path.ItemSpec, e.Message); + return false; + } + + int conePathLength = conePath.Length; + + Log.LogMessageFromResources(MessageImportance.Low, "FindUnderPath.ComparisonPath", Path.ItemSpec); + + foreach (ITaskItem item in Files) + { + string fullPath; + try + { + fullPath = OpportunisticIntern.InternStringIfPossible(System.IO.Path.GetFullPath(item.ItemSpec)); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + Log.LogErrorWithCodeFromResources(null, "", 0, 0, 0, 0, + "FindUnderPath.InvalidParameter", "Files", item.ItemSpec, e.Message); + return false; + } + + // Compare the left side of both strings to see if they're equal. + if (String.Compare(conePath, 0, fullPath, 0, conePathLength, StringComparison.OrdinalIgnoreCase) == 0) + { + // If we should use the absolute path, update the item contents + // Since ItemSpec, which fullPath comes from, is unescaped, re-escape when setting + // item.ItemSpec, since the setter for ItemSpec expects an escaped value. + if (_updateToAbsolutePaths) + { + item.ItemSpec = EscapingUtilities.Escape(fullPath); + } + + inPathList.Add(item); + } + else + { + outOfPathList.Add(item); + } + } + + InPath = (ITaskItem[])inPathList.ToArray(typeof(ITaskItem)); + OutOfPath = (ITaskItem[])outOfPathList.ToArray(typeof(ITaskItem)); + return true; + } + } +} diff --git a/src/XMakeTasks/ListOperators/RemoveDuplicates.cs b/src/XMakeTasks/ListOperators/RemoveDuplicates.cs new file mode 100644 index 00000000000..e66d19f050d --- /dev/null +++ b/src/XMakeTasks/ListOperators/RemoveDuplicates.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Given a list of items, remove duplicate items. Attributes are not considered. Case insensitive. + /// + public class RemoveDuplicates : TaskExtension + { + /// + /// Construct. + /// + public RemoveDuplicates() + { + } + + private ITaskItem[] _inputs = new TaskItem[0]; + private ITaskItem[] _filtered = null; + + /// + /// The left-hand set of items to be RemoveDuplicatesed from. + /// + public ITaskItem[] Inputs + { + get { return _inputs; } + set { _inputs = value; } + } + + /// + /// List of unique items. + /// + [Output] + public ITaskItem[] Filtered + { + get { return _filtered; } + set { _filtered = value; } + } + + /// + /// Execute the task. + /// + /// + public override bool Execute() + { + Hashtable alreadySeen = new Hashtable(Inputs.Length, StringComparer.OrdinalIgnoreCase); + ArrayList filteredList = new ArrayList(); + foreach (ITaskItem item in Inputs) + { + if (!alreadySeen.ContainsKey(item.ItemSpec)) + { + alreadySeen[item.ItemSpec] = String.Empty; + filteredList.Add(item); + } + } + + Filtered = (ITaskItem[])filteredList.ToArray(typeof(ITaskItem)); + + return true; + } + } +} diff --git a/src/XMakeTasks/MSBuild.cs b/src/XMakeTasks/MSBuild.cs new file mode 100644 index 00000000000..167968d5f1e --- /dev/null +++ b/src/XMakeTasks/MSBuild.cs @@ -0,0 +1,879 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Xml; +using System.Resources; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class implements the "MSBuild" task, which hands off child project files to the MSBuild engine to be built. + /// Marked RunInMTA because there is no reason MSBuild tasks should run on a thread other than that of the + /// RequestBuilder which spawned them. + /// + [RunInMTA] + public class MSBuild : TaskExtension + { + /// + /// Enum describing the behavior when a project doesn't exist on disk. + /// + private enum SkipNonexistentProjectsBehavior + { + /// + /// Skip the project if there is no file on disk. + /// + Skip, + + /// + /// Error if the project does not exist on disk. + /// + Error, + + /// + /// Build even if the project does not exist on disk. + /// + Build + } + + /// + /// Default constructor. + /// + public MSBuild() + { + } + + #region Properties + + // projects to build + private ITaskItem[] _projects = null; + // A list of targets to build. This is an optional parameter. If it's omitted, + // the default targets are built. + private string[] _targets = null; + // A list of property name/value pairs to apply as global properties to the child project. + // Each string in this array should be of the form: "propname=propvalue" + private string[] _properties = null; + + /// + /// A semicolon-delimited list of global properties to undefine + /// + private string _undefineProperties = null; + + // outputs of all built targets + private ArrayList _targetOutputs = new ArrayList(); + // indicates if the paths of target output items should be rebased relative to the calling project + private bool _rebaseOutputs = false; + // Indicates that we should stop building remaining projects as soon as one fails to build. + // The default is that we chug ahead despite failures. + private bool _stopOnFirstFailure = false; + // When this is true, instead of calling the engine once to build all the targets (for each project), + // we would call the engine once per target (for each project). The benefit of this is that + // if one target fails, you can still continue with the remaining targets. + private bool _runEachTargetSeparately = false; + // When this is true we call the engine with all the projects at once instead of + // calling the engine once per project + private bool _buildInParallel = false; + // If true the project will be unloaded once the operation is completed + private bool _unloadProjectsOnCompletion = false; + // If true the cache will be checked for the result and the result will be stored if the operation + // is run + private bool _useResultsCache = true; + // Whether to skip project files that don't exist on disk. By default we error for such projects. + private SkipNonexistentProjectsBehavior _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Error; + // Value of ToolsVersion to use when building projects passed to this task. + private string _toolsVersion = null; + // Should Targets, Properties (+ properties as project metadata) be un-escaped before processing + private string[] _targetAndPropertyListSeparators = null; + + /// + /// 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". + /// + /// + /// 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 + { + return _properties; + } + + set + { + _properties = value; + } + } + + /// + /// Gets or sets the set of global properties to remove. + /// + public string RemoveProperties + { + get + { + return _undefineProperties; + } + + set + { + _undefineProperties = value; + } + } + + /// + /// The targets to build in each project specified by the property. + /// + /// Array of target names. + public string[] Targets + { + get + { + return _targets; + } + + set + { + _targets = value; + } + } + + /// + /// The projects to build. + /// + /// Array of project items. + [Required] + public ITaskItem[] Projects + { + get + { + return _projects; + } + + set + { + _projects = value; + } + } + + /// + /// Outputs of the targets built in each project. + /// + /// Array of output items. + [Output] + public ITaskItem[] TargetOutputs + { + get + { + return (ITaskItem[])_targetOutputs.ToArray(typeof(ITaskItem)); + } + } + + /// + /// Indicates if the paths of target output items should be rebased relative to the calling project. + /// + /// true, if target output item paths should be rebased + public bool RebaseOutputs + { + get + { + return _rebaseOutputs; + } + + set + { + _rebaseOutputs = value; + } + } + + /// + /// Forces the task to stop building the remaining projects as soon as any of + /// them fail. + /// + public bool StopOnFirstFailure + { + get + { + return _stopOnFirstFailure; + } + + set + { + _stopOnFirstFailure = value; + } + } + + /// + /// When this is true, instead of calling the engine once to build all the targets (for each project), + /// we would call the engine once per target (for each project). The benefit of this is that + /// if one target fails, you can still continue with the remaining targets. + /// + public bool RunEachTargetSeparately + { + get + { + return _runEachTargetSeparately; + } + + set + { + _runEachTargetSeparately = value; + } + } + + /// + /// Value of ToolsVersion to use when building projects passed to this task. + /// + public string ToolsVersion + { + get + { + return _toolsVersion; + } + + set + { + _toolsVersion = value; + } + } + + /// + /// When this is true we call the engine with all the projects at once instead of + /// calling the engine once per project + /// + public bool BuildInParallel + { + get + { + return _buildInParallel; + } + set + { + _buildInParallel = value; + } + } + + /// + /// If true the project will be unloaded once the operation is completed + /// + public bool UnloadProjectsOnCompletion + { + get + { + return _unloadProjectsOnCompletion; + } + set + { + _unloadProjectsOnCompletion = value; + } + } + + /// + /// If true the cached result will be returned if present and a if MSBuild + /// task is run its result will be cached in a scope (ProjectFileName, GlobalProperties)[TargetNames] + /// as a list of build items + /// + public bool UseResultsCache + { + get + { + return _useResultsCache; + } + set + { + _useResultsCache = value; + } + } + + /// + /// When this is true, project files that do not exist on the disk will be skipped. By default, + /// such projects will cause an error. + /// + public string SkipNonexistentProjects + { + get + { + switch (_skipNonexistentProjects) + { + case SkipNonexistentProjectsBehavior.Build: + return "Build"; + + case SkipNonexistentProjectsBehavior.Error: + return "False"; + + case SkipNonexistentProjectsBehavior.Skip: + return "True"; + + default: + ErrorUtilities.ThrowInternalError("Unexpected case {0}", _skipNonexistentProjects); + break; + } + + ErrorUtilities.ThrowInternalErrorUnreachable(); + return null; + } + + set + { + if (String.Equals("Build", value, StringComparison.OrdinalIgnoreCase)) + { + _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Build; + } + else + { + ErrorUtilities.VerifyThrowArgument(ConversionUtilities.CanConvertStringToBool(value), "MSBuild.InvalidSkipNonexistentProjectValue"); + bool originalSkipValue = ConversionUtilities.ConvertStringToBool(value); + if (originalSkipValue) + { + _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Skip; + } + else + { + _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Error; + } + } + } + } + + /// + /// Unescape Targets, Properties (including Properties and AdditionalProperties as Project item metadata) + /// will be un-escaped before processing. e.g. %3B (an escaped ';') in the string for any of them will + /// be treated as if it were an un-escaped ';' + /// + public string[] TargetAndPropertyListSeparators + { + get + { + return _targetAndPropertyListSeparators; + } + set + { + _targetAndPropertyListSeparators = value; + } + } + + #endregion + + #region ITask Members + + /// + /// Instructs the MSBuild engine to build one or more project files whose locations are specified by the + /// property. + /// + /// true if all projects build successfully; false if any project fails + public override bool Execute() + { + // If no projects were passed in, just return success. + if ((Projects == null) || (Projects.Length == 0)) + { + return true; + } + + // We have been asked to unescape all escaped characters before processing + if (this.TargetAndPropertyListSeparators != null && this.TargetAndPropertyListSeparators.Length > 0) + { + ExpandAllTargetsAndProperties(); + } + + // Parse the global properties into a hashtable. + Hashtable propertiesTable; + if (!PropertyParser.GetTableWithEscaping(Log, ResourceUtilities.FormatResourceString("General.GlobalProperties"), "Properties", this.Properties, out propertiesTable)) + { + return false; + } + + // Parse out the properties to undefine, if any. + string[] undefinePropertiesArray = null; + if (!String.IsNullOrEmpty(_undefineProperties)) + { + Log.LogMessageFromResources(MessageImportance.Low, "General.UndefineProperties"); + undefinePropertiesArray = _undefineProperties.Split(new char[] { ';' }); + foreach (string property in undefinePropertiesArray) + { + Log.LogMessageFromText(String.Format(CultureInfo.InvariantCulture, " {0}", property), MessageImportance.Low); + } + } + + bool isRunningMultipleNodes = BuildEngine2.IsRunningMultipleNodes; + // If we are in single proc mode and stopOnFirstFailure is true, we cannot build in parallel because + // building in parallel sends all of the projects to the engine at once preventing us from not sending + // any more projects after the first failure. Therefore, to preserve compatibility with whidby if we are in this situation disable buildInParallel. + if (!isRunningMultipleNodes && _stopOnFirstFailure && _buildInParallel) + { + _buildInParallel = false; + Log.LogMessageFromResources(MessageImportance.Low, "MSBuild.NotBuildingInParallel"); + } + + // When the condition below is met, provide an information message indicating stopOnFirstFailure + // will have no effect. The reason there will be no effect is, when buildInParallel is true + // All project files will be submitted to the engine all at once, this mean there is no stopping for failures between projects. + // When RunEachTargetSpearately is false, all targets will be submitted to the engine at once, this means there is no way to stop between target failures. + // therefore the first failure seen will be the only failure seen. + if (isRunningMultipleNodes && _buildInParallel && _stopOnFirstFailure && !_runEachTargetSeparately) + { + Log.LogMessageFromResources(MessageImportance.Low, "MSBuild.NoStopOnFirstFailure"); + } + + // This is a list of string[]. That is, each element in the list is a string[]. Each + // string[] represents a set of target names to build. Depending on the value + // of the RunEachTargetSeparately parameter, we each just call the engine to run all + // the targets together, or we call the engine separately for each target. + ArrayList targetLists = CreateTargetLists(this.Targets, this.RunEachTargetSeparately); + + + bool success = true; + ITaskItem[] singleProject = null; + bool[] skipProjects = null; + + + if (_buildInParallel) + { + skipProjects = new bool[Projects.Length]; + for (int i = 0; i < skipProjects.Length; i++) + { + skipProjects[i] = true; + } + } + else + { + singleProject = new ITaskItem[1]; + } + + // Read in each project file. If there are any errors opening the file or parsing the XML, + // raise an event and return False. If any one of the projects fails to build, return False, + // otherwise return True. If parallel build is requested we first check for file existence so + // that we don't pass a non-existent file to IBuildEngine causing an exception + for (int i = 0; i < Projects.Length; i++) + { + ITaskItem project = Projects[i]; + + string projectPath = FileUtilities.AttemptToShortenPath(project.ItemSpec); + + if (_stopOnFirstFailure && !success) + { + // Inform the user that we skipped the remaining projects because StopOnFirstFailure=true. + Log.LogMessageFromResources(MessageImportance.Low, "MSBuild.SkippingRemainingProjects"); + + // We have encountered a failure. Caller has requested that we not + // continue with remaining projects. + break; + } + + if (File.Exists(projectPath) || (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Build)) + { + if (FileUtilities.IsVCProjFilename(projectPath)) + { + Log.LogErrorWithCodeFromResources("MSBuild.ProjectUpgradeNeededToVcxProj", project.ItemSpec); + success = false; + continue; + } + + // If we are building in parallel we want to only make one call to + // ExecuteTargets once we verified that all projects exist + if (!_buildInParallel) + { + singleProject[0] = project; + + if (!ExecuteTargets + ( + singleProject, + propertiesTable, + undefinePropertiesArray, + targetLists, + StopOnFirstFailure, + RebaseOutputs, + BuildEngine3, + Log, + _targetOutputs, + _useResultsCache, + _unloadProjectsOnCompletion, + ToolsVersion + ) + ) + { + success = false; + } + } + else + { + skipProjects[i] = false; + } + } + else + { + if (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Skip) + { + Log.LogMessageFromResources(MessageImportance.High, "MSBuild.ProjectFileNotFoundMessage", project.ItemSpec); + } + else + { + ErrorUtilities.VerifyThrow(_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Error, "skipNonexistentProjects has unexpected value {0}", _skipNonexistentProjects); + Log.LogErrorWithCodeFromResources("MSBuild.ProjectFileNotFound", project.ItemSpec); + success = false; + } + } + } + + // We need to build all the projects that were not skipped + if (_buildInParallel) + { + success = BuildProjectsInParallel(propertiesTable, undefinePropertiesArray, targetLists, success, skipProjects); + } + + return success; + } + + /// + /// Build projects which have not been skipped. This will be done in parallel + /// + private bool BuildProjectsInParallel(Hashtable propertiesTable, string[] undefinePropertiesArray, ArrayList targetLists, bool success, bool[] skipProjects) + { + ITaskItem[] projectToBuildInParallel = Projects; + // There were some projects that were skipped so we need to recreate the + // project array with those projects removed + List projectsToBuildArrayList = new List(); + for (int i = 0; i < Projects.Length; i++) + { + if (!skipProjects[i]) + { + projectsToBuildArrayList.Add(Projects[i]); + } + } + projectToBuildInParallel = projectsToBuildArrayList.ToArray(); + + // Make the call to build the projects + if (projectToBuildInParallel.Length > 0) + { + if (!ExecuteTargets + ( + projectToBuildInParallel, + propertiesTable, + undefinePropertiesArray, + targetLists, + StopOnFirstFailure, + RebaseOutputs, + BuildEngine3, + Log, + _targetOutputs, + _useResultsCache, + _unloadProjectsOnCompletion, + ToolsVersion + ) + ) + { + success = false; + } + } + return success; + } + + /// + /// Expand and re-construct arrays of all targets and properties + /// + private void ExpandAllTargetsAndProperties() + { + List expandedProperties = new List(); + List expandedTargets = new List(); + + if (this.Properties != null) + { + // Expand all properties + for (int n = 0; n < this.Properties.Length; n++) + { + // Split each property according to the separators + string[] expandedPropertyValues = this.Properties[n].Split(this.TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + // Add the resultant properties to the final list + foreach (string property in expandedPropertyValues) + { + expandedProperties.Add(property); + } + } + this.Properties = expandedProperties.ToArray(); + } + + if (this.Targets != null) + { + // Expand all targets + for (int n = 0; n < this.Targets.Length; n++) + { + // Split each target according to the separators + string[] expandedTargetValues = this.Targets[n].Split(this.TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + // Add the resultant targets to the final list + foreach (string target in expandedTargetValues) + { + expandedTargets.Add(target); + } + } + this.Targets = expandedTargets.ToArray(); + } + } + + /// + /// + /// + /// + /// + /// + internal static ArrayList CreateTargetLists + ( + string[] targets, + bool runEachTargetSeparately + ) + { + // This is a list of string[]. That is, each element in the list is a string[]. Each + // string[] represents a set of target names to build. Depending on the value + // of the RunEachTargetSeparately parameter, we each just call the engine to run all + // the targets together, or we call the engine separately for each target. + ArrayList targetLists = new ArrayList(); + if ((runEachTargetSeparately) && (targets != null) && (targets.Length > 0)) + { + // Separate target invocations for each individual target. + foreach (string targetName in targets) + { + targetLists.Add(new string[1] { targetName }); + } + } + else + { + // Just one target list, and that's whatever was passed in. We will call the engine + // once (per project) with the entire target list. + targetLists.Add(targets); + } + + return targetLists; + } + + /// + /// + /// + /// True if the operation was successful + internal static bool ExecuteTargets + ( + ITaskItem[] projects, + Hashtable propertiesTable, + string[] undefineProperties, + ArrayList targetLists, + bool stopOnFirstFailure, + bool rebaseOutputs, + IBuildEngine3 buildEngine, + TaskLoggingHelper log, + ArrayList targetOutputs, + bool useResultsCache, + bool unloadProjectsOnCompletion, + string toolsVersion + ) + { + bool success = true; + + // We don't log a message about the project and targets we're going to + // build, because it'll all be in the immediately subsequent ProjectStarted event. + + string[] projectDirectory = new string[projects.Length]; + string[] projectNames = new string[projects.Length]; + string[] toolsVersions = new string[projects.Length]; + IList> targetOutputsPerProject = null; + IDictionary[] projectProperties = new IDictionary[projects.Length]; + List[] undefinePropertiesPerProject = new List[projects.Length]; + + for (int i = 0; i < projectNames.Length; i++) + { + projectNames[i] = null; + projectProperties[i] = propertiesTable; + + if (projects[i] != null) + { + // Retrieve projectDirectory only the first time. It never changes anyway. + string projectPath = FileUtilities.AttemptToShortenPath(projects[i].ItemSpec); + projectDirectory[i] = Path.GetDirectoryName(projectPath); + projectNames[i] = projects[i].ItemSpec; + toolsVersions[i] = toolsVersion; + + + // 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"))) + { + Hashtable preProjectPropertiesTable; + if (!PropertyParser.GetTableWithEscaping + (log, ResourceUtilities.FormatResourceString("General.OverridingProperties", projectNames[i]), "Properties", projects[i].GetMetadata("Properties").Split(';'), + out preProjectPropertiesTable) + ) + { + return false; + } + projectProperties[i] = preProjectPropertiesTable; + } + + if (undefineProperties != null) + { + undefinePropertiesPerProject[i] = new List(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[] propertiesToUndefine = projectUndefineProperties.Split(new char[] { ';' }); + if (undefinePropertiesPerProject[i] == null) + { + undefinePropertiesPerProject[i] = new List(propertiesToUndefine.Length); + } + + if (log != null && propertiesToUndefine.Length > 0) + { + log.LogMessageFromResources(MessageImportance.Low, "General.ProjectUndefineProperties", projectNames[i]); + foreach (string property in propertiesToUndefine) + { + undefinePropertiesPerProject[i].Add(property); + log.LogMessageFromText(String.Format(CultureInfo.InvariantCulture, " {0}", property), MessageImportance.Low); + } + } + } + + // 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"))) + { + Hashtable additionalProjectPropertiesTable; + if (!PropertyParser.GetTableWithEscaping + (log, ResourceUtilities.FormatResourceString("General.AdditionalProperties", projectNames[i]), "AdditionalProperties", projects[i].GetMetadata("AdditionalProperties").Split(';'), + out additionalProjectPropertiesTable) + ) + { + return false; + } + Hashtable combinedTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + // First copy in the properties from the global table that not in the additional properties table + if (projectProperties[i] != null) + { + foreach (DictionaryEntry entry in projectProperties[i]) + { + if (!additionalProjectPropertiesTable.Contains(entry.Key)) + { + combinedTable.Add(entry.Key, entry.Value); + } + } + } + // Add all the additional properties + foreach (DictionaryEntry entry in additionalProjectPropertiesTable) + { + combinedTable.Add(entry.Key, entry.Value); + } + projectProperties[i] = combinedTable; + } + + // If the user specified a different toolsVersion for this project - then override the setting + if (!String.IsNullOrEmpty(projects[i].GetMetadata("ToolsVersion"))) + { + toolsVersions[i] = projects[i].GetMetadata("ToolsVersion"); + } + } + } + + foreach (string[] targetList in targetLists) + { + if (stopOnFirstFailure && !success) + { + // Inform the user that we skipped the remaining targets StopOnFirstFailure=true. + log.LogMessageFromResources(MessageImportance.Low, "MSBuild.SkippingRemainingTargets"); + + // We have encountered a failure. Caller has requested that we not + // continue with remaining targets. + break; + } + + // Send the project off to the build engine. By passing in null to the + // first param, we are indicating that the project to build is the same + // as the *calling* project file. + bool currentTargetResult = true; + + BuildEngineResult result = + buildEngine.BuildProjectFilesInParallel(projectNames, targetList, projectProperties, undefinePropertiesPerProject, toolsVersions, true /* ask that target outputs are returned in the buildengineresult */); + + currentTargetResult = result.Result; + targetOutputsPerProject = result.TargetOutputsPerProject; + success = success && currentTargetResult; + + // If the engine was able to satisfy the build request + if (currentTargetResult) + { + for (int i = 0; i < projects.Length; i++) + { + IEnumerable nonNullTargetList = (targetList != null) ? targetList : targetOutputsPerProject[i].Keys; + + foreach (string targetName in nonNullTargetList) + { + if (targetOutputsPerProject[i].ContainsKey(targetName)) + { + ITaskItem[] outputItemsFromTarget = (ITaskItem[])targetOutputsPerProject[i][targetName]; + + foreach (ITaskItem outputItemFromTarget in outputItemsFromTarget) + { + // No need to rebase if the calling project is the same as the callee project + // (project == null). Also no point in trying to copy item metadata either, + // because no items were passed into the Projects parameter! + if (projects[i] != null) + { + // Rebase the output item paths if necessary. No need to rebase if the calling + // project is the same as the callee project (project == null). + if (rebaseOutputs) + { + try + { + outputItemFromTarget.ItemSpec = Path.Combine(projectDirectory[i], outputItemFromTarget.ItemSpec); + } + catch (ArgumentException e) + { + log.LogWarningWithCodeFromResources(null, projects[i].ItemSpec, 0, 0, 0, 0, "MSBuild.CannotRebaseOutputItemPath", outputItemFromTarget.ItemSpec, e.Message); + } + } + + // Copy the custom item metadata from the "Projects" items to these + // output items. + projects[i].CopyMetadataTo(outputItemFromTarget); + + // Set a metadata on the output items called "MSBuildProjectFile" which tells you which project file produced this item. + if (String.IsNullOrEmpty(outputItemFromTarget.GetMetadata(ItemMetadataNames.msbuildSourceProjectFile))) + { + outputItemFromTarget.SetMetadata(ItemMetadataNames.msbuildSourceProjectFile, projects[i].GetMetadata(FileUtilities.ItemSpecModifiers.FullPath)); + } + } + + // Set a metadata on the output items called "MSBuildTargetName" which tells you which target produced this item. + if (String.IsNullOrEmpty(outputItemFromTarget.GetMetadata(ItemMetadataNames.msbuildSourceTargetName))) + { + outputItemFromTarget.SetMetadata(ItemMetadataNames.msbuildSourceTargetName, targetName); + } + } + + targetOutputs.AddRange(outputItemsFromTarget); + } + } + } + } + } + + return success; + } + + #endregion + } +} diff --git a/src/XMakeTasks/MSBuildTasks.h b/src/XMakeTasks/MSBuildTasks.h new file mode 100644 index 00000000000..da064830f43 --- /dev/null +++ b/src/XMakeTasks/MSBuildTasks.h @@ -0,0 +1,26 @@ +//-------------------------------------------------------------------------- +// +// Copyright (C) 2015 Microsoft Corporation, +// All rights reserved +// +// File: MSBuildTasks.h +// +// Unmanaged C++ code that needs to use MSBuild should #include this header +// file to get access to the MSBuildTasks interfaces. See the generated +// file MSBuildTasks.tlh for the interface definitions. +// +//--------------------------------------------------------------------------- +#if (_MSC_VER > 1000) && !defined(NO_PRAGMA_ONCE) +#pragma once +#endif + +#ifndef __MSBUILDTASKINTERFACES_H__ +#define __MSBUILDTASKINTERFACES_H__ + + +#import "Microsoft.Build.Tasks.Core.tlb" \ + no_registry \ + raw_interfaces_only named_guids \ + rename("Microsoft_Build_Tasks_Core", "MSBuildTasks") + +#endif //__MSBUILDTASKINTERFACES_H__ diff --git a/src/XMakeTasks/MakeDir.cs b/src/XMakeTasks/MakeDir.cs new file mode 100644 index 00000000000..7228919676b --- /dev/null +++ b/src/XMakeTasks/MakeDir.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task that creates a directory + /// + public class MakeDir : TaskExtension + { + /// + /// Constructor + /// + public MakeDir() + { + } + + [Required] + public ITaskItem[] Directories + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_directories, "directories"); + return _directories; + } + + set + { + _directories = value; + } + } + + [Output] + public ITaskItem[] DirectoriesCreated + { + get + { + return _directoriesCreated; + } + } + + private ITaskItem[] _directories; + private ITaskItem[] _directoriesCreated; + + #region ITask Members + + /// + /// Executes the MakeDir task. Create the directory. + /// + public override bool Execute() + { + ArrayList items = new ArrayList(); + HashSet directoriesSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (ITaskItem directory in Directories) + { + // Sometimes people pass in an item transform like @(myitem->'%(RelativeDir)') in order + // to create a bunch of directories for a set of items. But if the item + // is in the current project directory, %(RelativeDir) evaluates to empty-string. So, + // here we check for that case. + if (directory.ItemSpec.Length > 0) + { + try + { + // For speed, eliminate duplicates caused by poor targets authoring + if (!directoriesSet.Contains(directory.ItemSpec)) + { + // Only log a message if we actually need to create the folder + if (!FileUtilities.DirectoryExistsNoThrow(directory.ItemSpec)) + { + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", directory.ItemSpec); + + Directory.CreateDirectory(directory.ItemSpec); + } + + items.Add(directory); + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("MakeDir.Error", directory.ItemSpec, e.Message); + } + + // Add even on failure to avoid reattempting + directoriesSet.Add(directory.ItemSpec); + } + } + + _directoriesCreated = (ITaskItem[])items.ToArray(typeof(ITaskItem)); + + return !Log.HasLoggedErrors; + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManagedCompiler.cs b/src/XMakeTasks/ManagedCompiler.cs new file mode 100644 index 00000000000..5470e4da5a7 --- /dev/null +++ b/src/XMakeTasks/ManagedCompiler.cs @@ -0,0 +1,701 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Resources; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines all of the common stuff that is shared between Vjc, Vbc and Csc tasks. + /// This class is not instantiatable as a Task just by itself. + /// + /// The security attribute below is there to make sure that inheriting classes are MS only + /// (FxCop suggestion since we're using virtual internal methods) + /// + [System.Security.Permissions.StrongNameIdentityPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, + PublicKey = + "00240000048000009400000006020000" + + "00240000525341310004000001000100" + + "07d1fa57c4aed9f0a32e84aa0faefd0d" + + "e9e8fd6aec8f87fb03766c834c99921e" + + "b23be79ad9d5dcc1dd9ad23613210290" + + "0b723cf980957fc4e177108fc607774f" + + "29e8320e92ea05ece4e821c0a5efe8f1" + + "645c4c0c93c1ab99285d622caa652c1d" + + "fad63d745d6f2de5f17e5eaf0fc4963d" + + "261c8a12436518206dc093344d5ad293" + )] + public abstract class ManagedCompiler : ToolTaskExtension + { + #region Properties + + // Please keep these alphabetized. + public string[] AdditionalLibPaths + { + set { Bag["AdditionalLibPaths"] = value; } + get { return (string[])Bag["AdditionalLibPaths"]; } + } + + public string[] AddModules + { + set { Bag["AddModules"] = value; } + get { return (string[])Bag["AddModules"]; } + } + + public ITaskItem[] AdditionalFiles + { + set { Bag["AdditionalFiles"] = value; } + get { return (ITaskItem[])Bag["AdditionalFiles"]; } + } + + public ITaskItem[] Analyzers + { + set { Bag["Analyzers"] = value; } + get { return (ITaskItem[])Bag["Analyzers"]; } + } + + // We do not support BugReport because it always requires user interaction, + // which will cause a hang. + + public string CodeAnalysisRuleSet + { + set { Bag["CodeAnalysisRuleSet"] = value; } + get { return (string)Bag["CodeAnalysisRuleSet"]; } + } + + public int CodePage + { + set { Bag["CodePage"] = value; } + get { return GetIntParameterWithDefault("CodePage", 0); } + } + + public string DebugType + { + set { Bag["DebugType"] = value; } + get { return (string)Bag["DebugType"]; } + } + + public string DefineConstants + { + set { Bag["DefineConstants"] = value; } + get { return (string)Bag["DefineConstants"]; } + } + + public bool DelaySign + { + set { Bag["DelaySign"] = value; } + get { return GetBoolParameterWithDefault("DelaySign", false); } + } + + public bool EmitDebugInformation + { + set { Bag["EmitDebugInformation"] = value; } + get { return GetBoolParameterWithDefault("EmitDebugInformation", false); } + } + + public int FileAlignment + { + set { Bag["FileAlignment"] = value; } + get { return GetIntParameterWithDefault("FileAlignment", 0); } + } + + public bool HighEntropyVA + { + set { Bag["HighEntropyVA"] = value; } + get { return GetBoolParameterWithDefault("HighEntropyVA", false); } + } + + public string KeyContainer + { + set { Bag["KeyContainer"] = value; } + get { return (string)Bag["KeyContainer"]; } + } + + public string KeyFile + { + set { Bag["KeyFile"] = value; } + get { return (string)Bag["KeyFile"]; } + } + + public ITaskItem[] LinkResources + { + set { Bag["LinkResources"] = value; } + get { return (ITaskItem[])Bag["LinkResources"]; } + } + + public string MainEntryPoint + { + set { Bag["MainEntryPoint"] = value; } + get { return (string)Bag["MainEntryPoint"]; } + } + + public bool NoConfig + { + set { Bag["NoConfig"] = value; } + get { return GetBoolParameterWithDefault("NoConfig", false); } + } + + public bool NoLogo + { + set { Bag["NoLogo"] = value; } + get { return GetBoolParameterWithDefault("NoLogo", false); } + } + + public bool NoWin32Manifest + { + set { Bag["NoWin32Manifest"] = value; } + get { return GetBoolParameterWithDefault("NoWin32Manifest", false); } + } + + public bool Optimize + { + set { Bag["Optimize"] = value; } + get { return GetBoolParameterWithDefault("Optimize", false); } + } + + [Output] + public ITaskItem OutputAssembly + { + set { Bag["OutputAssembly"] = value; } + get { return (ITaskItem)Bag["OutputAssembly"]; } + } + + public string Platform + { + set { Bag["Platform"] = value; } + get { return (string)Bag["Platform"]; } + } + + public bool Prefer32Bit + { + set { Bag["Prefer32Bit"] = value; } + get { return GetBoolParameterWithDefault("Prefer32Bit", false); } + } + + public ITaskItem[] References + { + set { Bag["References"] = value; } + get { return (ITaskItem[])Bag["References"]; } + } + + public ITaskItem[] Resources + { + set { Bag["Resources"] = value; } + get { return (ITaskItem[])Bag["Resources"]; } + } + + public ITaskItem[] ResponseFiles + { + set { Bag["ResponseFiles"] = value; } + get { return (ITaskItem[])Bag["ResponseFiles"]; } + } + + + + public ITaskItem[] Sources + { + set + { + if (UsedCommandLineTool) + { + NormalizePaths(value); + } + + Bag["Sources"] = value; + } + get { return (ITaskItem[])Bag["Sources"]; } + } + + public string SubsystemVersion + { + set { Bag["SubsystemVersion"] = value; } + get { return (string)Bag["SubsystemVersion"]; } + } + + public string TargetType + { + set { Bag["TargetType"] = value.ToLower(CultureInfo.InvariantCulture); } + get { return (string)Bag["TargetType"]; } + } + + public bool TreatWarningsAsErrors + { + set { Bag["TreatWarningsAsErrors"] = value; } + get { return GetBoolParameterWithDefault("TreatWarningsAsErrors", false); } + } + + public bool Utf8Output + { + set { Bag["Utf8Output"] = value; } + get { return GetBoolParameterWithDefault("Utf8Output", false); } + } + + public string Win32Icon + { + set { Bag["Win32Icon"] = value; } + get { return (string)Bag["Win32Icon"]; } + } + + public string Win32Manifest + { + set { Bag["Win32Manifest"] = value; } + get { return (string)Bag["Win32Manifest"]; } + } + + public string Win32Resource + { + set { Bag["Win32Resource"] = value; } + get { return (string)Bag["Win32Resource"]; } + } + + // Map explicit platform of "AnyCPU" or the default platform (null or ""), since it is commonly understood in the + // managed build process to be equivalent to "AnyCPU", to platform "AnyCPU32BitPreferred" if the Prefer32Bit + // property is set. + internal string PlatformWith32BitPreference + { + get + { + string platform = this.Platform; + if ((String.IsNullOrEmpty(platform) || platform.Equals("anycpu", StringComparison.OrdinalIgnoreCase)) && this.Prefer32Bit) + { + platform = "anycpu32bitpreferred"; + } + return platform; + } + } + + /// + /// Overridable property specifying the encoding of the captured task standard output stream + /// + protected override Encoding StandardOutputEncoding + { + get + { + return (Utf8Output) ? Encoding.UTF8 : base.StandardOutputEncoding; + } + } + + #endregion + + /// + /// If an alternate tool name or tool path was specified in the project file, we don't want to + /// use the host compiler for IDE builds. + /// + /// false if the host compiler should be used + protected internal virtual bool UseAlternateCommandLineToolToExecute() + { + // Roslyn MSBuild task does not support using host object for compilation + return true; + } + + #region Tool Members + + /// + /// Fills the provided CommandLineBuilderExtension with those switches and other information that can go into a response file. + /// + protected internal override void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + // If outputAssembly is not specified, then an "/out: " option won't be added to + // overwrite the one resulting from the OutputAssembly member of the CompilerParameters class. + // In that case, we should set the outputAssembly member based on the first source file. + if ( + (OutputAssembly == null) && + (Sources != null) && + (Sources.Length > 0) && + (this.ResponseFiles == null) // The response file may already have a /out: switch in it, so don't try to be smart here. + ) + { + try + { + OutputAssembly = new TaskItem(Path.GetFileNameWithoutExtension(Sources[0].ItemSpec)); + } + catch (ArgumentException e) + { + throw new ArgumentException(e.Message, "Sources"); + } + if (String.Compare(TargetType, "library", StringComparison.OrdinalIgnoreCase) == 0) + { + OutputAssembly.ItemSpec += ".dll"; + } + else if (String.Compare(TargetType, "module", StringComparison.OrdinalIgnoreCase) == 0) + { + OutputAssembly.ItemSpec += ".netmodule"; + } + else + { + OutputAssembly.ItemSpec += ".exe"; + } + } + + commandLine.AppendSwitchIfNotNull("/addmodule:", this.AddModules, ","); + commandLine.AppendSwitchWithInteger("/codepage:", this.Bag, "CodePage"); + + ConfigureDebugProperties(); + + // The "DebugType" parameter should be processed after the "EmitDebugInformation" parameter + // because it's more specific. Order matters on the command-line, and the last one wins. + // /debug+ is just a shorthand for /debug:full. And /debug- is just a shorthand for /debug:none. + + commandLine.AppendPlusOrMinusSwitch("/debug", this.Bag, "EmitDebugInformation"); + commandLine.AppendSwitchIfNotNull("/debug:", this.DebugType); + + commandLine.AppendPlusOrMinusSwitch("/delaysign", this.Bag, "DelaySign"); + + commandLine.AppendSwitchWithInteger("/filealign:", this.Bag, "FileAlignment"); + commandLine.AppendSwitchIfNotNull("/keycontainer:", this.KeyContainer); + commandLine.AppendSwitchIfNotNull("/keyfile:", this.KeyFile); + // If the strings "LogicalName" or "Access" ever change, make sure to search/replace everywhere in vsproject. + commandLine.AppendSwitchIfNotNull("/linkresource:", this.LinkResources, new string[] { "LogicalName", "Access" }); + commandLine.AppendWhenTrue("/nologo", this.Bag, "NoLogo"); + commandLine.AppendWhenTrue("/nowin32manifest", this.Bag, "NoWin32Manifest"); + commandLine.AppendPlusOrMinusSwitch("/optimize", this.Bag, "Optimize"); + commandLine.AppendSwitchIfNotNull("/out:", this.OutputAssembly); + commandLine.AppendSwitchIfNotNull("/ruleset:", this.CodeAnalysisRuleSet); + commandLine.AppendSwitchIfNotNull("/subsystemversion:", this.SubsystemVersion); + // If the strings "LogicalName" or "Access" ever change, make sure to search/replace everywhere in vsproject. + commandLine.AppendSwitchIfNotNull("/resource:", this.Resources, new string[] { "LogicalName", "Access" }); + commandLine.AppendSwitchIfNotNull("/target:", this.TargetType); + commandLine.AppendPlusOrMinusSwitch("/warnaserror", this.Bag, "TreatWarningsAsErrors"); + commandLine.AppendWhenTrue("/utf8output", this.Bag, "Utf8Output"); + commandLine.AppendSwitchIfNotNull("/win32icon:", this.Win32Icon); + commandLine.AppendSwitchIfNotNull("/win32manifest:", this.Win32Manifest); + + // Append the analyzers. + this.AddAnalyzersToCommandLine(commandLine); + + // Append additional files. + this.AddAdditionalFilesToCommandLine(commandLine); + + // Append the sources. + commandLine.AppendFileNamesIfNotNull(Sources, " "); + } + + /// + /// Adds a "/analyzer:" switch to the command line for each provided analyzer. + /// + private void AddAnalyzersToCommandLine(CommandLineBuilderExtension commandLine) + { + // If there were no analyzers passed in, don't add any /analyzer: switches + // on the command-line. + if ((this.Analyzers == null) || (this.Analyzers.Length == 0)) + { + return; + } + + foreach (ITaskItem analyzer in this.Analyzers) + { + commandLine.AppendSwitchIfNotNull("/analyzer:", analyzer.ItemSpec); + } + } + + /// + /// Adds a "/analyzer:" switch to the command line for each provided analyzer. + /// + private void AddAdditionalFilesToCommandLine(CommandLineBuilderExtension commandLine) + { + // If there were no additional files passed in, don't add any /additionalfile: switches + // on the command-line. + if ((this.AdditionalFiles == null) || (this.AdditionalFiles.Length == 0)) + { + return; + } + + foreach (ITaskItem additionalFile in this.AdditionalFiles) + { + commandLine.AppendSwitchIfNotNull("/additionalfile:", additionalFile.ItemSpec); + } + } + + /// + /// Configure the debug switches which will be placed on the compiler commandline. + /// The matrix of debug type and symbol inputs and the desired results is as follows: + /// + /// Debug Symbols DebugType Desired Resilts + /// True Full /debug+ /debug:full + /// True PdbOnly /debug+ /debug:PdbOnly + /// True None /debug- + /// True Blank /debug+ + /// False Full /debug- /debug:full + /// False PdbOnly /debug- /debug:PdbOnly + /// False None /debug- + /// False Blank /debug- + /// Blank Full /debug:full + /// Blank PdbOnly /debug:PdbOnly + /// Blank None /debug- + /// Debug: Blank Blank /debug+ //Microsof.common.targets will set this + /// Release: Blank Blank "Nothing for either switch" + /// + /// The logic is as follows: + /// If debugtype is none set debugtype to empty and debugSymbols to false + /// If debugType is blank use the debugsymbols "as is" + /// If debug type is set, use its value and the debugsymbols value "as is" + /// + private void ConfigureDebugProperties() + { + // If debug type is set we need to take some action depending on the value. If debugtype is not set + // We don't need to modify the EmitDebugInformation switch as its value will be used as is. + if (Bag["DebugType"] != null) + { + // If debugtype is none then only show debug- else use the debug type and the debugsymbols as is. + if (string.Compare((string)Bag["DebugType"], "none", StringComparison.OrdinalIgnoreCase) == 0) + { + Bag["DebugType"] = null; + Bag["EmitDebugInformation"] = false; + } + } + } + + /// + /// Fills the provided CommandLineBuilderExtension with those switches and other information that can't go into a response file and + /// must go directly onto the command line. + /// + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendWhenTrue("/noconfig", this.Bag, "NoConfig"); + } + + /// + /// Validate parameters, log errors and warnings and return true if + /// Execute should proceed. + /// + protected override bool ValidateParameters() + { + return ListHasNoDuplicateItems(this.Resources, "Resources", "LogicalName") && ListHasNoDuplicateItems(this.Sources, "Sources"); + } + + /// + /// Returns true if the provided item list contains duplicate items, false otherwise. + /// + protected bool ListHasNoDuplicateItems(ITaskItem[] itemList, string parameterName) + { + return ListHasNoDuplicateItems(itemList, parameterName, null); + } + + /// + /// Returns true if the provided item list contains duplicate items, false otherwise. + /// + /// + /// Optional name of metadata that may legitimately disambiguate items. May be null. + /// + private bool ListHasNoDuplicateItems(ITaskItem[] itemList, string parameterName, string disambiguatingMetadataName) + { + if (itemList == null || itemList.Length == 0) + { + return true; + } + + Hashtable alreadySeen = new Hashtable(StringComparer.OrdinalIgnoreCase); + foreach (ITaskItem item in itemList) + { + string key; + string disambiguatingMetadataValue = null; + if (disambiguatingMetadataName != null) + { + disambiguatingMetadataValue = item.GetMetadata(disambiguatingMetadataName); + } + + if (disambiguatingMetadataName == null || String.IsNullOrEmpty(disambiguatingMetadataValue)) + { + key = item.ItemSpec; + } + else + { + key = item.ItemSpec + ":" + disambiguatingMetadataValue; + } + + if (alreadySeen.ContainsKey(key)) + { + if (disambiguatingMetadataName == null || String.IsNullOrEmpty(disambiguatingMetadataValue)) + { + Log.LogErrorWithCodeFromResources("General.DuplicateItemsNotSupported", item.ItemSpec, parameterName); + } + else + { + Log.LogErrorWithCodeFromResources("General.DuplicateItemsNotSupportedWithMetadata", item.ItemSpec, parameterName, disambiguatingMetadataValue, disambiguatingMetadataName); + } + return false; + } + else + { + alreadySeen[key] = String.Empty; + } + } + + return true; + } + + /// + /// Allows tool to handle the return code. + /// This method will only be called with non-zero exitCode. + /// + protected override bool HandleTaskExecutionErrors() + { + // For managed compilers, the compiler should emit the appropriate + // error messages before returning a non-zero exit code, so we don't + // normally need to emit any additional messages now. + // + // If somehow the compiler DID return a non-zero exit code and didn't log an error, we'd like to log that exit code. + // We can only do this for the command line compiler: if the inproc compiler was used, + // we can't tell what if anything it logged as it logs directly to Visual Studio's output window. + // + if (!Log.HasLoggedErrors && UsedCommandLineTool) + { + // This will log a message "MSB3093: The command exited with code {0}." + base.HandleTaskExecutionErrors(); + } + + return false; + } + + /// + /// Takes a list of files and returns the normalized locations of these files + /// + private void NormalizePaths(ITaskItem[] taskItems) + { + foreach (var item in taskItems) + { + item.ItemSpec = FileUtilities.GetFullPathNoThrow(item.ItemSpec); + } + } + + /// + /// Whether the command line compiler was invoked, instead + /// of the host object compiler. + /// + protected bool UsedCommandLineTool + { + get; + set; + } + + private bool _hostCompilerSupportsAllParameters; + protected bool HostCompilerSupportsAllParameters + { + get { return _hostCompilerSupportsAllParameters; } + set { _hostCompilerSupportsAllParameters = value; } + } + + /// + /// Checks the bool result from calling one of the methods on the host compiler object to + /// set one of the parameters. If it returned false, that means the host object doesn't + /// support a particular parameter or variation on a parameter. So we log a comment, + /// and set our state so we know not to call the host object to do the actual compilation. + /// + protected void CheckHostObjectSupport + ( + string parameterName, + bool resultFromHostObjectSetOperation + ) + { + if (!resultFromHostObjectSetOperation) + { + Log.LogMessageFromResources(MessageImportance.Normal, "General.ParameterUnsupportedOnHostCompiler", parameterName); + _hostCompilerSupportsAllParameters = false; + } + } + + /// + /// Checks to see whether all of the passed-in references exist on disk before we launch the compiler. + /// + protected bool CheckAllReferencesExistOnDisk() + { + if (null == this.References) + { + // No references + return true; + } + + bool success = true; + + foreach (ITaskItem reference in this.References) + { + if (!File.Exists(reference.ItemSpec)) + { + success = false; + Log.LogErrorWithCodeFromResources("General.ReferenceDoesNotExist", reference.ItemSpec); + } + } + + return success; + } + + /// + /// The IDE and command line compilers unfortunately differ in how win32 + /// manifests are specified. In particular, the command line compiler offers a + /// "/nowin32manifest" switch, while the IDE compiler does not offer analagous + /// functionality. If this switch is omitted from the command line and no win32 + /// manifest is specified, the compiler will include a default win32 manifest + /// named "default.win32manifest" found in the same directory as the compiler + /// executable. Again, the IDE compiler does not offer analagous support. + /// + /// We'd like to imitate the command line compiler's behavior in the IDE, but + /// it isn't aware of the default file, so we must compute the path to it if + /// noDefaultWin32Manifest is false and no win32Manifest was provided by the + /// project. + /// + /// This method will only be called during the initialization of the host object, + /// which is only used during IDE builds. + /// + /// + /// + /// the path to the win32 manifest to provide to the host object + internal string GetWin32ManifestSwitch + ( + bool noDefaultWin32Manifest, + string win32Manifest + ) + { + if (!noDefaultWin32Manifest) + { + if (String.IsNullOrEmpty(win32Manifest) && String.IsNullOrEmpty(this.Win32Resource)) + { + // We only want to consider the default.win32manifest if this is an executable + if (!String.Equals(TargetType, "library", StringComparison.OrdinalIgnoreCase) + && !String.Equals(TargetType, "module", StringComparison.OrdinalIgnoreCase)) + { + // We need to compute the path to the default win32 manifest + string pathToDefaultManifest = ToolLocationHelper.GetPathToDotNetFrameworkFile + ( + "default.win32manifest", + TargetDotNetFrameworkVersion.VersionLatest + ); + + if (null == pathToDefaultManifest) + { + // This is rather unlikely, and the inproc compiler seems to log an error anyway. + // So just a message is fine. + Log.LogMessageFromResources + ( + "General.ExpectedFileMissing", + "default.win32manifest" + ); + } + + return pathToDefaultManifest; + } + } + } + + return win32Manifest; + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/ApplicationIdentity.cs b/src/XMakeTasks/ManifestUtil/ApplicationIdentity.cs new file mode 100644 index 00000000000..4d5ea30979c --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ApplicationIdentity.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Provides a unique identifier for a ClickOnce application. + /// + [ComVisible(false)] + public sealed class ApplicationIdentity + { + private readonly AssemblyIdentity _applicationManifestIdentity; + private readonly AssemblyIdentity _deployManifestIdentity; + private readonly string _url; + + /// + /// Initializes a new instance of the ApplicationIdentity class. + /// + /// The deployment provider URL for the ClickOnce deployment manifest. + /// Path to ClickOnce deployment manifest. The assembly identity will be obtained from the specified file. + /// Path to ClickOnce application manifest. The assembly identity will be obtained from the specified file. + public ApplicationIdentity(string url, string deployManifestPath, string applicationManifestPath) + { + if (String.IsNullOrEmpty(url)) + throw new ArgumentNullException("url"); + if (String.IsNullOrEmpty(deployManifestPath)) + throw new ArgumentNullException("deployManifestPath"); + if (String.IsNullOrEmpty(applicationManifestPath)) + throw new ArgumentNullException("applicationManifestPath"); + _url = url; + _deployManifestIdentity = AssemblyIdentity.FromManifest(deployManifestPath); + _applicationManifestIdentity = AssemblyIdentity.FromManifest(applicationManifestPath); + } + + /// + /// Initializes a new instance of the ApplicationIdentity class. + /// + /// The deployment provider URL for the ClickOnce deployment manifest. + /// Assembly identity of the ClickOnce deployment manifest. + /// Assembly identity of the ClickOnce application manifest. + public ApplicationIdentity(string url, AssemblyIdentity deployManifestIdentity, AssemblyIdentity applicationManifestIdentity) + { + if (String.IsNullOrEmpty(url)) + throw new ArgumentNullException("url"); + if (deployManifestIdentity == null) + throw new ArgumentNullException("deployManifestIdentity"); + if (applicationManifestIdentity == null) + throw new ArgumentNullException("applicationManifestIdentity"); + _url = url; + _deployManifestIdentity = deployManifestIdentity; + _applicationManifestIdentity = applicationManifestIdentity; + } + + /// + /// Returns the full ClickOnce application identity. + /// + /// A string containing the ClickOnce application identity. + public override string ToString() + { + string dname = string.Empty; + if (_deployManifestIdentity != null) + { + dname = _deployManifestIdentity.GetFullName(AssemblyIdentity.FullNameFlags.ProcessorArchitecture); + } + + string aname = string.Empty; + if (_applicationManifestIdentity != null) + { + aname = _applicationManifestIdentity.GetFullName(AssemblyIdentity.FullNameFlags.ProcessorArchitecture | AssemblyIdentity.FullNameFlags.Type); + } + return String.Format(CultureInfo.InvariantCulture, "{0}#{1}/{2}", _url, dname, aname); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/ApplicationManifest.cs b/src/XMakeTasks/ManifestUtil/ApplicationManifest.cs new file mode 100644 index 00000000000..efa007e2395 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ApplicationManifest.cs @@ -0,0 +1,885 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml; +using System.Xml.Serialization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes a ClickOnce or native Win32 application manifest. + /// + [ComVisible(false)] + [XmlRoot("ApplicationManifest")] + public sealed class ApplicationManifest : AssemblyManifest + { + private string _configFile = null; + private AssemblyIdentity _entryPointIdentity = null; + private AssemblyReference _entryPoint = null; + private string _entryPointParameters = null; + private string _entryPointPath = null; + private string _errorReportUrl = null; + private string _iconFile = null; + private bool _isClickOnceManifest = true; + private string _oSMajor = null; + private string _oSMinor = null; + private string _oSBuild = null; + private string _oSRevision = null; + private string _oSSupportUrl = null; + private string _oSDescription = null; + private TrustInfo _trustInfo = null; + private int _maxTargetPath = 0; + private bool _hostInBrowser = false; + private bool _useApplicationTrust = false; + private string _product = null; + private string _publisher = null; + private string _suiteName = null; + private string _supportUrl = null; + private FileAssociation[] _fileAssociations; + private FileAssociationCollection _fileAssociationList = null; + private string _targetFrameworkVersion = null; + + /// + /// Initializes a new instance of the ApplicationManifest class. + /// + public ApplicationManifest() + { + } + + /// + /// Initializes a new instance of the ApplicationManifest class. + /// + /// + public ApplicationManifest(string targetFrameworkVersion) + { + _targetFrameworkVersion = targetFrameworkVersion; + } + + /// + /// Indicates the application configuration file. + /// For a Win32 native manifest, this input is ignored. + /// + [XmlIgnore] + public string ConfigFile + { + get { return _configFile; } + set { _configFile = value; } + } + + [XmlIgnore] + public override AssemblyReference EntryPoint + { + get + { + FixupEntryPoint(); + return _entryPoint; + } + set + { + _entryPoint = value; + UpdateEntryPoint(); + } + } + + /// + /// Specifies the target framework version + /// + [XmlIgnore] + public string TargetFrameworkVersion + { + get { return _targetFrameworkVersion; } + set { _targetFrameworkVersion = value; } + } + + /// + /// Specifies the link to use if there is a failure launching the application. + /// The specified value should be a fully qualified URL or UNC path. + /// + [XmlIgnore] + public string ErrorReportUrl + { + get { return _errorReportUrl; } + set { _errorReportUrl = value; } + } + + // Make sure we have a CLR dependency, add it if not... + private void FixupClrVersion() + { + AssemblyReference CLRPlatformAssembly = AssemblyReferences.Find(Constants.CLRPlatformAssemblyName); + if (CLRPlatformAssembly == null) + { + CLRPlatformAssembly = new AssemblyReference(); + CLRPlatformAssembly.IsPrerequisite = true; + AssemblyReferences.Add(CLRPlatformAssembly); + } + if (CLRPlatformAssembly.AssemblyIdentity == null || String.IsNullOrEmpty(CLRPlatformAssembly.AssemblyIdentity.Version)) + { + CLRPlatformAssembly.AssemblyIdentity = new AssemblyIdentity(Constants.CLRPlatformAssemblyName, Util.GetClrVersion(_targetFrameworkVersion)); + } + } + + private void FixupEntryPoint() + { + if (_entryPoint == null) + _entryPoint = AssemblyReferences.Find(_entryPointIdentity); + } + + // WinXP is required if app has any native assembly references or any RegFree COM definitions... + private bool WinXPRequired + { + get + { + foreach (FileReference f in FileReferences) + if (f.ComClasses != null || f.TypeLibs != null || f.ProxyStubs != null) + return true; + foreach (AssemblyReference a in AssemblyReferences) + if (a.ReferenceType == AssemblyReferenceType.NativeAssembly) + return true; + return false; + } + } + + [XmlIgnore] + public FileAssociationCollection FileAssociations + { + get + { + if (_fileAssociationList == null) + _fileAssociationList = new FileAssociationCollection(_fileAssociations); + return _fileAssociationList; + } + } + + /// + /// If true, the application will run in IE using WPF's xbap application model. + /// + [XmlIgnore] + public bool HostInBrowser + { + get { return _hostInBrowser; } + set { _hostInBrowser = value; } + } + + /// + /// Indicates the application icon file. + /// The application icon is expressed in the generated application manifest and is used for the start menu and Add/Remove Programs dialog. + /// If this input is not specified then a default icon is used. + /// For a Win32 native manifest, this input is ignored. + /// + [XmlIgnore] + public string IconFile + { + get { return _iconFile; } + set { _iconFile = value; } + } + + /// + /// Indicates whether the manifest is a ClickOnce application manifest or a native Win32 application manifest. + /// + [XmlIgnore] + public bool IsClickOnceManifest + { + get { return _isClickOnceManifest; } + set { _isClickOnceManifest = value; } + } + + + /// + /// Specifies the maximum allowable length of a file path in a ClickOnce application deployment. + /// If this value is specified, then the length of each file path in the application is checked against this limit. + /// Any items that exceed the limit will result in a warning message. + /// If this input is not specified or is zero, then no checking is performed. + /// For a Win32 native manifest, this input is ignored. + /// + [XmlIgnore] + public int MaxTargetPath + { + get { return _maxTargetPath; } + set { _maxTargetPath = value; } + } + + internal override void OnBeforeSave() + { + FixupEntryPoint(); + if (_isClickOnceManifest) + FixupClrVersion(); + base.OnBeforeSave(); + if (_isClickOnceManifest && AssemblyIdentity != null && String.IsNullOrEmpty(AssemblyIdentity.PublicKeyToken)) + AssemblyIdentity.PublicKeyToken = "0000000000000000"; + UpdateEntryPoint(); + AssemblyIdentity.Type = "win32"; // Activation on WinXP gold will fail if type="win32" attribute is not present + if (String.IsNullOrEmpty(OSVersion)) + { + if (!WinXPRequired) + OSVersion = Constants.OSVersion_Win9X; + else + OSVersion = Constants.OSVersion_WinXP; + } + if (_fileAssociationList != null) + _fileAssociations = _fileAssociationList.ToArray(); + } + + /// + /// Specifies a textual description for the OS dependency. + /// + [XmlIgnore] + public string OSDescription + { + get { return _oSDescription; } + set { _oSDescription = value; } + } + + /// + /// Specifies a support URL for the OS dependency. + /// + [XmlIgnore] + public string OSSupportUrl + { + get { return _oSSupportUrl; } + set { _oSSupportUrl = value; } + } + + /// + /// Specifies the minimum required OS version required by the application. + /// An example value is "5.1.2600.0" for Windows XP. + /// If this input is not specified a default value is used. + /// The default value is the minimum supported OS of the .NET Framework, which is "4.10.0.0" for Windows 98SE. + /// However, if the application contains any native or Reg-Free COM references, then the default will be the Windows XP version. + /// For a Win32 native manifest, this input is ignored. + /// + [XmlIgnore] + public string OSVersion + { + get + { + if (String.IsNullOrEmpty(_oSMajor)) return null; + Version v = null; + try + { + v = new Version(String.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}.{3}", _oSMajor, _oSMinor, _oSBuild, _oSRevision)); + } + catch (FormatException) + { + return null; + } + return v.ToString(); + } + set + { + if (value == null) + { + _oSMajor = null; + _oSMinor = null; + _oSBuild = null; + _oSRevision = null; + } + else + { + Version v = new Version(value); + if (v.Build < 0 || v.Revision < 0) + throw new FormatException(); + _oSMajor = v.Major.ToString("G", CultureInfo.InvariantCulture); + _oSMinor = v.Minor.ToString("G", CultureInfo.InvariantCulture); + _oSBuild = v.Build.ToString("G", CultureInfo.InvariantCulture); + _oSRevision = v.Revision.ToString("G", CultureInfo.InvariantCulture); + } + } + } + + /// + /// Specifies the name of the application. + /// If this input is not specified then the Product is not written into the Application Manifest + /// This name is used for the shortcut name on the Start menu and is part of the name that appears in the Add/Remove Programs dialog. + /// + [XmlIgnore] + public string Product + { + get { return _product; } + set { _product = value; } + } + + /// + /// Specifies the publisher of the application. + /// If this input is not specified then the Publisher is not written into the Application Manifest + /// This name is used for the folder name on the Start menu and is part of the name that appears in the Add/Remove Programs dialog. + /// + [XmlIgnore] + public string Publisher + { + get { return _publisher; } + set { _publisher = value; } + } + + /// + /// Specifies the suite name of the application. + /// This name is used for the sub-folder name on the Start menu (as a child of the publisher) + /// + [XmlIgnore] + public string SuiteName + { + get { return _suiteName; } + set { _suiteName = value; } + } + + /// + /// Specifies the link that appears in the Add/Remove Programs dialog for the application. + /// The specified value should be a fully qualified URL or UNC path. + /// + [XmlIgnore] + public string SupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + /// + /// Specifies a trust object defining the application security. + /// + [XmlIgnore] + public TrustInfo TrustInfo + { + get { return _trustInfo; } + set { _trustInfo = value; } + } + + /// + /// If true, the install will use the settings in the application manifest in the trust prompt. + /// + [XmlIgnore] + public bool UseApplicationTrust + { + get { return _useApplicationTrust; } + set { _useApplicationTrust = value; } + } + + private void UpdateEntryPoint() + { + if (_entryPoint != null) + { + _entryPointIdentity = new AssemblyIdentity(_entryPoint.AssemblyIdentity); + _entryPointPath = _entryPoint.TargetPath; + } + else + { + _entryPointIdentity = null; + _entryPointPath = null; + } + } + + public override void Validate() + { + base.Validate(); + if (_isClickOnceManifest) + { + ValidateReferencesForClickOnceApplication(); + ValidatePlatform(); + ValidateConfig(); + ValidateEntryPoint(); + ValidateFileAssociations(); + } + else + { + ValidateReferencesForNativeApplication(); + } + ValidateCom(); + } + + private void ValidateCom() + { + int t1 = Environment.TickCount; + string outputFileName = Path.GetFileName(SourcePath); + Dictionary clsidList = new Dictionary(); + Dictionary tlbidList = new Dictionary(); + + // Check for duplicate COM definitions in all dependent manifests... + foreach (AssemblyReference assembly in AssemblyReferences) + { + if (assembly.ReferenceType == AssemblyReferenceType.NativeAssembly && !assembly.IsPrerequisite && !String.IsNullOrEmpty(assembly.ResolvedPath)) + { + ComInfo[] comInfoArray = ManifestReader.GetComInfo(assembly.ResolvedPath); ; + if (comInfoArray != null) + { + foreach (ComInfo comInfo in comInfoArray) + { + if (!String.IsNullOrEmpty(comInfo.ClsId)) + { + string key = comInfo.ClsId.ToLowerInvariant(); + if (!clsidList.ContainsKey(key)) + clsidList.Add(key, comInfo); + else + OutputMessages.AddErrorMessage("GenerateManifest.DuplicateComDefinition", "clsid", comInfo.ComponentFileName, comInfo.ClsId, comInfo.ManifestFileName, clsidList[key].ManifestFileName); + } + if (!String.IsNullOrEmpty(comInfo.TlbId)) + { + string key = comInfo.TlbId.ToLowerInvariant(); + if (!tlbidList.ContainsKey(key)) + tlbidList.Add(key, comInfo); + else + OutputMessages.AddErrorMessage("GenerateManifest.DuplicateComDefinition", "tlbid", comInfo.ComponentFileName, comInfo.TlbId, comInfo.ManifestFileName, tlbidList[key].ManifestFileName); + } + } + } + } + } + + // Check for duplicate COM definitions in the manifest about to be generated... + foreach (FileReference file in FileReferences) + { + if (file.ComClasses != null) + foreach (ComClass comClass in file.ComClasses) + { + string key = comClass.ClsId.ToLowerInvariant(); + if (!clsidList.ContainsKey(key)) + clsidList.Add(key, new ComInfo(outputFileName, file.TargetPath, comClass.ClsId, null)); + else + OutputMessages.AddErrorMessage("GenerateManifest.DuplicateComDefinition", "clsid", file.ToString(), comClass.ClsId, outputFileName, clsidList[key].ManifestFileName); + } + if (file.TypeLibs != null) + foreach (TypeLib typeLib in file.TypeLibs) + { + string key = typeLib.TlbId.ToLowerInvariant(); + if (!tlbidList.ContainsKey(key)) + tlbidList.Add(key, new ComInfo(outputFileName, file.TargetPath, null, typeLib.TlbId)); + else + OutputMessages.AddErrorMessage("GenerateManifest.DuplicateComDefinition", "tlbid", file.ToString(), typeLib.TlbId, outputFileName, tlbidList[key].ManifestFileName); + } + } + + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "GenerateManifest.CheckForComDuplicates t={0}", Environment.TickCount - t1)); + } + + private void ValidateConfig() + { + if (String.IsNullOrEmpty(ConfigFile)) return; + FileReference configFile = FileReferences.FindTargetPath(ConfigFile); + if (configFile == null) return; + + if (!TrustInfo.IsFullTrust) + { + XmlDocument document = new XmlDocument(); + + XmlReaderSettings xrs = new XmlReaderSettings(); + xrs.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader xr = XmlReader.Create(configFile.ResolvedPath, xrs)) + { + document.Load(xr); + } + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + XmlNodeList nodes = document.SelectNodes(XPaths.configBindingRedirect, nsmgr); + if (nodes.Count > 0) + OutputMessages.AddWarningMessage("GenerateManifest.ConfigBindingRedirectsWithPartialTrust"); + } + } + + private void ValidateEntryPoint() + { + if (_entryPoint != null) + { + bool isCorrectFileType = !String.IsNullOrEmpty(_entryPoint.TargetPath) && _entryPoint.TargetPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase); + if (!isCorrectFileType) + OutputMessages.AddErrorMessage("GenerateManifest.InvalidEntryPoint", _entryPoint.ToString()); + } + } + + private void ValidateFileAssociations() + { + if (FileAssociations.Count > 0) + { + if (FileAssociations.Count > Constants.MaxFileAssociationsCount) + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationsCountExceedsMaximum", Constants.MaxFileAssociationsCount.ToString(CultureInfo.CurrentUICulture)); + } + + Dictionary usedExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (FileAssociation fileAssociation in FileAssociations) + { + if (string.IsNullOrEmpty(fileAssociation.Extension) || + string.IsNullOrEmpty(fileAssociation.Description) || + string.IsNullOrEmpty(fileAssociation.ProgId) || + string.IsNullOrEmpty(fileAssociation.DefaultIcon)) + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationMissingAttribute"); + } + if (!string.IsNullOrEmpty(fileAssociation.Extension)) + { + if (fileAssociation.Extension[0] != '.') + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationExtensionMissingLeadDot"); + } + if (fileAssociation.Extension.Length > Constants.MaxFileAssociationExtensionLength) + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationExtensionTooLong", fileAssociation.Extension, Constants.MaxFileAssociationExtensionLength.ToString(CultureInfo.CurrentUICulture)); + } + if (!usedExtensions.ContainsKey(fileAssociation.Extension)) + { + usedExtensions.Add(fileAssociation.Extension, fileAssociation); + } + else + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationsDuplicateExtensions", fileAssociation.Extension); + } + } + if (!string.IsNullOrEmpty(fileAssociation.DefaultIcon)) + { + FileReference defaultIconReference = null; + foreach (FileReference fileReference in FileReferences) + { + if (fileReference.TargetPath.Equals(fileAssociation.DefaultIcon, StringComparison.Ordinal)) + { + defaultIconReference = fileReference; + break; + } + } + if (defaultIconReference == null || !string.IsNullOrEmpty(defaultIconReference.Group)) + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationDefaultIconNotInstalled", fileAssociation.DefaultIcon); + } + } + } + + if (!TrustInfo.IsFullTrust) + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationsApplicationNotFullTrust"); + } + if (EntryPoint == null) + { + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationsNoEntryPoint"); + } + } + } + + private void ValidateReferencesForNativeApplication() + { + foreach (AssemblyReference assembly in AssemblyReferences) + { + // Check that the assembly identity matches the filename for all local dependencies... + if (!assembly.IsPrerequisite && !String.Equals(assembly.AssemblyIdentity.Name, Path.GetFileNameWithoutExtension(assembly.TargetPath), StringComparison.OrdinalIgnoreCase)) + OutputMessages.AddErrorMessage("GenerateManifest.IdentityFileNameMismatch", assembly.ToString(), assembly.AssemblyIdentity.Name, assembly.AssemblyIdentity.Name + Path.GetExtension(assembly.TargetPath)); + } + } + + private void ValidateReferencesForClickOnceApplication() + { + int t1 = Environment.TickCount; + bool isPartialTrust = !TrustInfo.IsFullTrust; + Dictionary> targetPathList = new Dictionary>(); + + foreach (AssemblyReference assembly in AssemblyReferences) + { + // Check all resolved dependencies for partial trust apps... + if (isPartialTrust && (assembly != EntryPoint) && !String.IsNullOrEmpty(assembly.ResolvedPath)) + ValidateReferenceForPartialTrust(assembly, TrustInfo); + + // Check TargetPath for all local dependencies, ignoring any Prerequisites + if (!assembly.IsPrerequisite && !String.IsNullOrEmpty(assembly.TargetPath)) + { + // Check target path does not exceed maximum... + if (_maxTargetPath > 0 && assembly.TargetPath.Length > _maxTargetPath) + OutputMessages.AddWarningMessage("GenerateManifest.TargetPathTooLong", assembly.ToString(), _maxTargetPath.ToString(CultureInfo.CurrentCulture)); + + // Check for two or more items with the same TargetPath... + string key = assembly.TargetPath.ToLowerInvariant(); + if (!targetPathList.ContainsKey(key)) + { + targetPathList.Add(key, false); + } + else if (targetPathList[key] == false) + { + OutputMessages.AddWarningMessage("GenerateManifest.DuplicateTargetPath", assembly.ToString()); + targetPathList[key] = true; // only warn once per path + } + } + else + { + // Check assembly name does not exceed maximum... + if (_maxTargetPath > 0 && assembly.AssemblyIdentity.Name.Length > _maxTargetPath) + OutputMessages.AddWarningMessage("GenerateManifest.TargetPathTooLong", assembly.AssemblyIdentity.Name, _maxTargetPath.ToString(CultureInfo.CurrentCulture)); + } + + // Check that all prerequisites are strong named... + if (assembly.IsPrerequisite && !assembly.AssemblyIdentity.IsStrongName && !assembly.IsVirtual) + OutputMessages.AddErrorMessage("GenerateManifest.PrerequisiteNotSigned", assembly.ToString()); + } + foreach (FileReference file in FileReferences) + { + // Check that file is not an assembly... + if (!String.IsNullOrEmpty(file.ResolvedPath) && PathUtil.IsAssembly(file.ResolvedPath)) + OutputMessages.AddWarningMessage("GenerateManifest.AssemblyAsFile", file.ToString()); + + if (!String.IsNullOrEmpty(file.TargetPath)) + { + // Check target path does not exceed maximum... + if (_maxTargetPath > 0 && file.TargetPath.Length > _maxTargetPath) + OutputMessages.AddWarningMessage("GenerateManifest.TargetPathTooLong", file.TargetPath, _maxTargetPath.ToString(CultureInfo.CurrentCulture)); + + // Check for two or more items with the same TargetPath... + string key = file.TargetPath.ToLowerInvariant(); + if (!targetPathList.ContainsKey(key)) + { + targetPathList.Add(key, false); + } + else if (targetPathList[key] == false) + { + OutputMessages.AddWarningMessage("GenerateManifest.DuplicateTargetPath", file.TargetPath); + targetPathList[key] = true; // only warn once per path + } + } + } + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "GenerateManifest.CheckManifestReferences t={0}", Environment.TickCount - t1)); + } + + private void ValidateReferenceForPartialTrust(AssemblyReference assembly, TrustInfo trustInfo) + { + if (trustInfo.IsFullTrust) + return; + string path = assembly.ResolvedPath; + AssemblyAttributeFlags flags = new AssemblyAttributeFlags(path); + + // if it's targeting v2.0 CLR then use the old logic to check for partial trust callers. + if (Util.CompareFrameworkVersions(this.TargetFrameworkVersion, Constants.TargetFrameworkVersion35) <= 0) + { + if (assembly.IsPrimary && flags.IsSigned + && !flags.HasAllowPartiallyTrustedCallersAttribute) + OutputMessages.AddWarningMessage("GenerateManifest.AllowPartiallyTrustedCallers", Path.GetFileNameWithoutExtension(path)); + } + else + { + if (assembly.AssemblyIdentity != null && assembly.AssemblyIdentity.IsInFramework(Constants.DotNetFrameworkIdentifier, TargetFrameworkVersion)) + { + // if the binary is targeting v4.0 and it has the transparent attribute then we may allow partially trusted callers. + if (assembly.IsPrimary + && !(flags.HasAllowPartiallyTrustedCallersAttribute || flags.HasSecurityTransparentAttribute)) + OutputMessages.AddWarningMessage("GenerateManifest.AllowPartiallyTrustedCallers", Path.GetFileNameWithoutExtension(path)); + } + else + { + // if the binary is targeting v4.0 and it has the transparent attribute then we may allow partially trusted callers. + if (assembly.IsPrimary && flags.IsSigned + && !(flags.HasAllowPartiallyTrustedCallersAttribute || flags.HasSecurityTransparentAttribute)) + OutputMessages.AddWarningMessage("GenerateManifest.AllowPartiallyTrustedCallers", Path.GetFileNameWithoutExtension(path)); + } + } + + if (flags.HasPrimaryInteropAssemblyAttribute || flags.HasImportedFromTypeLibAttribute) + OutputMessages.AddWarningMessage("GenerateManifest.UnmanagedCodePermission", Path.GetFileNameWithoutExtension(path)); + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("ConfigFile")] + public string XmlConfigFile + { + get { return _configFile; } + set { _configFile = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlElement("EntryPointIdentity")] + public AssemblyIdentity XmlEntryPointIdentity + { + get { return _entryPointIdentity; } + set { _entryPointIdentity = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("EntryPointParameters")] + public string XmlEntryPointParameters + { + get { return _entryPointParameters; } + set { _entryPointParameters = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("EntryPointPath")] + public string XmlEntryPointPath + { + get { return _entryPointPath; } + set { _entryPointPath = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("ErrorReportUrl")] + public string XmlErrorReportUrl + { + get { return _errorReportUrl; } + set { _errorReportUrl = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("FileAssociations")] + public FileAssociation[] XmlFileAssociations + { + get { return _fileAssociations; } + set { _fileAssociations = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("HostInBrowser")] + public string XmlHostInBrowser + { + get { return Convert.ToString(_hostInBrowser, CultureInfo.InvariantCulture).ToLowerInvariant(); } + set { _hostInBrowser = ConvertUtil.ToBoolean(value); } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("IconFile")] + public string XmlIconFile + { + get { return _iconFile; } + set { _iconFile = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("IsClickOnceManifest")] + public string XmlIsClickOnceManifest + { + get { return Convert.ToString(_isClickOnceManifest, CultureInfo.InvariantCulture).ToLowerInvariant(); } + set { _isClickOnceManifest = ConvertUtil.ToBoolean(value); } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("OSMajor")] + public string XmlOSMajor + { + get { return _oSMajor; } + set { _oSMajor = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("OSMinor")] + public string XmlOSMinor + { + get { return _oSMinor; } + set { _oSMinor = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("OSBuild")] + public string XmlOSBuild + { + get { return _oSBuild; } + set { _oSBuild = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("OSRevision")] + public string XmlOSRevision + { + get { return _oSRevision; } + set { _oSRevision = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("OSSupportUrl")] + public string XmlOSSupportUrl + { + get { return _oSSupportUrl; } + set { _oSSupportUrl = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("OSDescription")] + public string XmlOSDescription + { + get { return _oSDescription; } + set { _oSDescription = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Product")] + public string XmlProduct + { + get { return _product; } + set { _product = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Publisher")] + public string XmlPublisher + { + get { return _publisher; } + set { _publisher = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("SuiteName")] + public string XmlSuiteName + { + get { return _suiteName; } + set { _suiteName = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("SupportUrl")] + public string XmlSupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("UseApplicationTrust")] + public string XmlUseApplicationTrust + { + get { return Convert.ToString(_useApplicationTrust, CultureInfo.InvariantCulture).ToLowerInvariant(); } + set { _useApplicationTrust = ConvertUtil.ToBoolean(value); } + } + + #endregion + + #region AssemblyAttributeFlags + private class AssemblyAttributeFlags + { + public readonly bool IsSigned; + public readonly bool HasAllowPartiallyTrustedCallersAttribute; + public readonly bool HasPrimaryInteropAssemblyAttribute; + public readonly bool HasImportedFromTypeLibAttribute; + public readonly bool HasSecurityTransparentAttribute; + public readonly bool HasSecurityRulesAttribute; + + public AssemblyAttributeFlags(string path) + { + using (MetadataReader r = MetadataReader.Create(path)) + if (r != null) + { + IsSigned = !String.IsNullOrEmpty(r.PublicKeyToken); + HasAllowPartiallyTrustedCallersAttribute = r.HasAssemblyAttribute("System.Security.AllowPartiallyTrustedCallersAttribute"); + HasSecurityTransparentAttribute = r.HasAssemblyAttribute("System.Security.SecurityTransparentAttribute"); + HasPrimaryInteropAssemblyAttribute = r.HasAssemblyAttribute("System.Runtime.InteropServices.PrimaryInteropAssemblyAttribute"); + HasImportedFromTypeLibAttribute = r.HasAssemblyAttribute("System.Runtime.InteropServices.ImportedFromTypeLibAttribute"); + HasSecurityRulesAttribute = r.HasAssemblyAttribute("System.Security.SecurityRulesAttribute"); + } + } + } + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/AssemblyIdentity.cs b/src/XMakeTasks/ManifestUtil/AssemblyIdentity.cs new file mode 100644 index 00000000000..dddd9fdb459 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/AssemblyIdentity.cs @@ -0,0 +1,608 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Utilities; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Serialization; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes the identity of an assembly. + /// + [ComVisible(false)] + [XmlRoot("AssemblyIdentity")] + public sealed class AssemblyIdentity + { + /// + /// Specifies which attributes are to be returned by the GetFullName function. + /// + [Flags] + public enum FullNameFlags + { + /// + /// Include the Name, Version, Culture, and PublicKeyToken attributes. + /// + Default = 0x0000, + /// + /// Include the Name, Version, Culture, PublicKeyToken, and ProcessorArchitecture attributes. + /// + ProcessorArchitecture = 0x0001, + /// + /// Include the Name, Version, Culture, PublicKeyToken, and Type attributes. + /// + Type = 0x0002, + /// + /// Include all attributes. + /// + All = 0x0003 + } + + private string _name = null; + private string _version = null; + private string _publicKeyToken = null; + private string _culture = null; + private string _processorArchitecture = null; + private string _type = null; + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + public AssemblyIdentity() + { + } + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + /// Specifies the simple name of the assembly. + public AssemblyIdentity(string name) + { + _name = name; + } + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + /// Specifies the simple name of the assembly. + /// Specifies the version of the assembly. + public AssemblyIdentity(string name, string version) + { + _name = name; + _version = version; + } + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + /// Specifies the simple name of the assembly. + /// Specifies the version of the assembly. + /// Specifies the public key token of the assembly, which is the last 8 bytes of the SHA-1 hash of the public key under which the assembly is signed. + /// Specifies the culture of the assembly. A blank string indicates the invariant culture. + public AssemblyIdentity(string name, string version, string publicKeyToken, string culture) + { + _name = name; + _version = version; + _publicKeyToken = publicKeyToken; + _culture = culture; + } + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + /// Specifies the simple name of the assembly. + /// Specifies the version of the assembly. + /// Specifies the public key token of the assembly, which is the last 8 bytes of the SHA-1 hash of the public key under which the assembly is signed. + /// Specifies the culture of the assembly. A blank string indicates the invariant culture. + /// Specifies the processor architecture of the assembly. Valid values are "msil", "x86", "ia64", "amd64". + public AssemblyIdentity(string name, string version, string publicKeyToken, string culture, string processorArchitecture) + { + _name = name; + _version = version; + _publicKeyToken = publicKeyToken; + _culture = culture; + _processorArchitecture = processorArchitecture; + } + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + /// Specifies the simple name of the assembly. + /// Specifies the version of the assembly. + /// Specifies the public key token of the assembly, which is the last 8 bytes of the SHA-1 hash of the public key under which the assembly is signed. + /// Specifies the culture of the assembly. A blank string indicates the invariant culture. + /// Specifies the processor architecture of the assembly. Valid values are "msil", "x86", "ia64", "amd64". + /// Specifies the type attribute of the assembly. Valid values are "win32" or a blank string. + public AssemblyIdentity(string name, string version, string publicKeyToken, string culture, string processorArchitecture, string type) + { + _name = name; + _version = version; + _publicKeyToken = publicKeyToken; + _culture = culture; + _processorArchitecture = processorArchitecture; + _type = type; + } + + /// + /// Initializes a new instance of the AssemblyIdentity class. + /// + /// Specifies another instance to duplicate. + public AssemblyIdentity(AssemblyIdentity identity) + { + if (identity == null) + return; + _name = identity._name; + _version = identity._version; + _publicKeyToken = identity._publicKeyToken; + _culture = identity._culture; + _processorArchitecture = identity._processorArchitecture; + _type = identity._type; + } + + /// + /// Parses string to obtain an assembly identity. + /// Returns null if identity could not be obtained. + /// + /// The full name of the assembly, also known as the display name. + /// The resulting assembly identity. + public static AssemblyIdentity FromAssemblyName(string assemblyName) + { + // NOTE: We're not using System.Reflection.AssemblyName class here because we need ProcessorArchitecture and Type attributes. + Regex re = new Regex("^(?[^,]*)(, Version=(?[^,]*))?(, Culture=(?[^,]*))?(, PublicKeyToken=(?[^,]*))?(, ProcessorArchitecture=(?[^,]*))?(, Type=(?[^,]*))?"); + Match m = re.Match(assemblyName); + string name = m.Result("${name}"); + string version = m.Result("${version}"); + string publicKeyToken = m.Result("${pkt}"); + string culture = m.Result("${culture}"); + string processorArchitecture = m.Result("${pa}"); + string type = m.Result("${type}"); + return new AssemblyIdentity(name, version, publicKeyToken, culture, processorArchitecture, type); + } + + /// + /// Obtains identity of the specified manifest file. + /// File must be a stand-alone xml manifest file. + /// Returns null if identity could not be obtained. + /// + /// The name of the file from which the identity is to be obtained. + /// The assembly identity of the specified file. + public static AssemblyIdentity FromManifest(string path) + { + if (!File.Exists(path)) + return null; + + XmlDocument document = new XmlDocument(); + try + { + XmlReaderSettings readerSettings = new XmlReaderSettings(); + readerSettings.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader xmlReader = XmlReader.Create(path, readerSettings)) + { + document.Load(xmlReader); + } + } + catch (XmlException) + { + return null; + } + + return FromManifest(document); + } + + private static AssemblyIdentity FromManifest(Stream s) + { + XmlDocument document = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + try + { + using (XmlReader xr = XmlReader.Create(s, xrSettings)) + { + document.Load(xr); + } + } + catch (XmlException) + { + return null; + } + return FromManifest(document); + } + + private static AssemblyIdentity FromManifest(XmlDocument document) + { + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + XmlElement element = (XmlElement)document.SelectSingleNode(XPaths.assemblyIdentityPath, nsmgr); + if (element == null) + return null; + + XmlNode node = element.Attributes.GetNamedItem("name"); + string name = node != null ? node.Value : null; + node = element.Attributes.GetNamedItem("version"); + string version = node != null ? node.Value : null; + node = element.Attributes.GetNamedItem("publicKeyToken"); + string publicKeyToken = node != null ? node.Value : null; + node = element.Attributes.GetNamedItem("language"); + string culture = node != null ? node.Value : null; + node = element.Attributes.GetNamedItem("processorArchitecture"); + string processorArchitecture = node != null ? node.Value : null; + node = element.Attributes.GetNamedItem("type"); + string type = node != null ? node.Value : null; + return new AssemblyIdentity(name, version, publicKeyToken, culture, processorArchitecture, type); + } + + /// + /// Obtains identity of the specified .NET assembly. + /// File must be a .NET assembly. + /// Returns null if identity could not be obtained. + /// + /// The name of the file from which the identity is to be obtained. + /// The assembly identity of the specified file. + public static AssemblyIdentity FromManagedAssembly(string path) + { + if (!File.Exists(path)) + return null; + + // NOTE: We're not using System.Reflection.AssemblyName class here because we need ProcessorArchitecture + using (MetadataReader r = MetadataReader.Create(path)) + { + AssemblyIdentity identity = null; + if (r != null) + { + try + { + identity = new AssemblyIdentity(r.Name, r.Version, r.PublicKeyToken, r.Culture, r.ProcessorArchitecture); + } + catch (ArgumentException e) + { + if (e.HResult != unchecked((int)0x80070057)) + { + throw; + } + // 0x80070057 - "Value does not fall within the expected range." is returned from + // GetAssemblyIdentityFromFile for WinMD components + } + } + return identity; + } + } + + /// + /// Obtains identity of the specified native assembly. + /// File must be either a PE with an embedded xml manifest, or a stand-alone xml manifest file. + /// Returns null if identity could not be obtained. + /// + /// The name of the file from which the identity is to be obtained. + /// The assembly identity of the specified file. + public static AssemblyIdentity FromNativeAssembly(string path) + { + if (!File.Exists(path)) + return null; + + if (PathUtil.IsPEFile(path)) + { + Stream m = EmbeddedManifestReader.Read(path); + if (m == null) + return null; + return FromManifest(m); + } + else + return FromManifest(path); + } + + /// + /// Obtains identity of the specified assembly. + /// File can be a PE with an embedded xml manifest, a stand-alone xml manifest file, or a .NET assembly. + /// Returns null if identity could not be obtained. + /// + /// The name of the file from which the identity is to be obtained. + /// The assembly identity of the specified file. + public static AssemblyIdentity FromFile(string path) + { + if (!File.Exists(path)) + return null; + + AssemblyIdentity id = null; + id = FromNativeAssembly(path); // if there's an xml manifest use that first + if (id == null) + id = FromManagedAssembly(path); // otherwise fallback to the complib manifest if it's there + return id; + } + + internal static bool IsEqual(AssemblyIdentity a1, AssemblyIdentity a2) + { + return IsEqual(a1, a2, true); + } + + internal static bool IsEqual(AssemblyIdentity a1, AssemblyIdentity a2, bool specificVersion) + { + if (a1 == null || a2 == null) + return false; + if (specificVersion) + return String.Equals(a1._name, a2._name, StringComparison.OrdinalIgnoreCase) + && String.Equals(a1._publicKeyToken, a2._publicKeyToken, StringComparison.OrdinalIgnoreCase) + && String.Equals(a1._version, a2._version, StringComparison.OrdinalIgnoreCase) + && String.Equals(a1._culture, a2._culture, StringComparison.OrdinalIgnoreCase) + && String.Equals(a1._processorArchitecture, a2._processorArchitecture, StringComparison.OrdinalIgnoreCase); + else + return String.Equals(a1._name, a2._name, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Returns true if this assembly is part of the .NET Framework. + /// + [XmlIgnore] + public bool IsFrameworkAssembly + { + get + { + return IsInFramework(null, null); + } + } + + + /// + /// Returns true if this assembly is part of the given framework. + /// identifier is “.NETFramework” or “Silverlight”, etc. and the version string looks like this: “4.5” or “v4.5”, or “v4.0.30319" + /// If frameworkVersion is null or empty, return true if this assembly is present in any of the given framework versions + /// If both arguments are null or empty strings, return true if this assembly is present in any of the frameworks + /// + [SuppressMessage("Microsoft.Globalization", "CA1307: Specify StringComparison.")] + public bool IsInFramework(string frameworkIdentifier, string frameworkVersion) + { + Version version = null; + if (!string.IsNullOrEmpty(frameworkVersion)) + { + // CA1307:Specify StringComparison. Suppressed since a valid string representation of a version would be parsed correctly even if the the first character is not "v". + if (frameworkVersion.StartsWith("v")) + { + System.Version.TryParse(frameworkVersion.Substring(1), out version); + } + else + { + System.Version.TryParse(frameworkVersion, out version); + } + } + + if (string.IsNullOrEmpty(frameworkIdentifier) && version != null) + { + throw new ArgumentNullException("frameworkIdentifier"); + } + + Dictionary redistDictionary = new Dictionary(); + + foreach (string moniker in ToolLocationHelper.GetSupportedTargetFrameworks()) + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(moniker); + if ((string.IsNullOrEmpty(frameworkIdentifier) || frameworkName.Identifier.Equals(frameworkIdentifier, StringComparison.OrdinalIgnoreCase)) && + (version == null || frameworkName.Version == version)) + { + IList paths = ToolLocationHelper.GetPathToReferenceAssemblies(frameworkName); + + foreach (string path in paths) + { + if (!redistDictionary.ContainsKey(path)) + { + redistDictionary.Add(path, RedistList.GetRedistListFromPath(path)); + } + } + } + } + + string fullName = GetFullName(AssemblyIdentity.FullNameFlags.Default); + foreach (RedistList list in redistDictionary.Values) + { + if (list != null && list.IsFrameworkAssembly(fullName)) + return true; + } + + return false; + } + + /// + /// Specifies the culture of the assembly. A blank string indicates the invariant culture. + /// + [XmlIgnore] + public string Culture + { + get { return _culture; } + set { _culture = value; } + } + + /// + /// Returns the full name of the assembly. + /// + /// Specifies which attributes to be included in the full name. + /// A string representation of the full name. + public string GetFullName(FullNameFlags flags) + { + StringBuilder sb = new StringBuilder(); + sb.Append(_name); + if (!String.IsNullOrEmpty(_version)) + sb.Append(String.Format(CultureInfo.InvariantCulture, ", Version={0}", _version)); + if (!String.IsNullOrEmpty(_culture)) + sb.Append(String.Format(CultureInfo.InvariantCulture, ", Culture={0}", _culture)); + if (!String.IsNullOrEmpty(_publicKeyToken)) + sb.Append(String.Format(CultureInfo.InvariantCulture, ", PublicKeyToken={0}", _publicKeyToken)); + if (!String.IsNullOrEmpty(_processorArchitecture) && (flags & FullNameFlags.ProcessorArchitecture) != 0) + sb.Append(String.Format(CultureInfo.InvariantCulture, ", ProcessorArchitecture={0}", _processorArchitecture)); + if (!String.IsNullOrEmpty(_type) && (flags & FullNameFlags.Type) != 0) + sb.Append(String.Format(CultureInfo.InvariantCulture, ", Type={0}", _type)); + return sb.ToString(); + } + + /// + /// Specifies whether the assembly identity represents a neutral platform assembly. + /// + [XmlIgnore] + public bool IsNeutralPlatform + { + get { return String.IsNullOrEmpty(_processorArchitecture) || String.Equals(_processorArchitecture, "msil", StringComparison.OrdinalIgnoreCase); } + } + + /// + /// Specifies whether the assembly identity is a strong name. + /// + [XmlIgnore] + public bool IsStrongName + { + get + { + return !String.IsNullOrEmpty(_name) + && !String.IsNullOrEmpty(_version) + && !String.IsNullOrEmpty(_publicKeyToken); + } + } + + /// + /// Specifies the simple name of the assembly. + /// + [XmlIgnore] + public string Name + { + get { return _name; } + set { _name = value; } + } + + /// + /// Specifies the processor architecture of the assembly. Valid values are "msil", "x86", "ia64", "amd64". + /// + [XmlIgnore] + public string ProcessorArchitecture + { + get { return _processorArchitecture; } + set { _processorArchitecture = value; } + } + + /// + /// Specifies the public key token of the assembly, which is the last 8 bytes of the SHA-1 hash of the public key under which the assembly is signed. + /// + [XmlIgnore] + public string PublicKeyToken + { + get { return _publicKeyToken; } + set { _publicKeyToken = value; } + } + + internal string Resolve(string[] searchPaths) + { + return Resolve(searchPaths, IsStrongName); + } + + internal string Resolve(string[] searchPaths, bool specificVersion) + { + if (searchPaths == null) + searchPaths = new string[] { ".\\" }; + + foreach (string searchPath in searchPaths) + { + string file = String.Format(CultureInfo.InvariantCulture, "{0}.dll", _name); + string path = Path.Combine(searchPath, file); + if (File.Exists(path) && AssemblyIdentity.IsEqual(this, AssemblyIdentity.FromFile(path), specificVersion)) + return path; + + file = String.Format(CultureInfo.InvariantCulture, "{0}.manifest", _name); + path = Path.Combine(searchPath, file); + if (File.Exists(path) && AssemblyIdentity.IsEqual(this, AssemblyIdentity.FromManifest(path), specificVersion)) + return path; + } + + return null; + } + + public override string ToString() + { + return GetFullName(FullNameFlags.All); + } + + /// + /// Specifies the type attribute of the assembly. Valid values are "win32" or a blank string. + /// + [XmlIgnore] + public string Type + { + get { return _type; } + set { _type = value; } + } + + /// + /// Specifies the version of the assembly. + /// + [XmlIgnore] + public string Version + { + get { return _version; } + set { _version = value; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Name")] + public string XmlName + { + get { return _name; } + set { _name = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Version")] + public string XmlVersion + { + get { return _version; } + set { _version = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("PublicKeyToken")] + public string XmlPublicKeyToken + { + get { return _publicKeyToken; } + set { _publicKeyToken = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Culture")] + public string XmlCulture + { + get { return _culture; } + set { _culture = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("ProcessorArchitecture")] + public string XmlProcessorArchitecture + { + get { return _processorArchitecture; } + set { _processorArchitecture = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Type")] + public string XmlType + { + get { return _type; } + set { _type = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/AssemblyManifest.cs b/src/XMakeTasks/ManifestUtil/AssemblyManifest.cs new file mode 100644 index 00000000000..fb39281c90d --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/AssemblyManifest.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes a Win32 assembly manifest. + /// + [ComVisible(false)] + [XmlRoot("AssemblyManifest")] + public class AssemblyManifest : Manifest + { + private ProxyStub[] _externalProxyStubs = null; + + /// + /// Initializes a new instance of the AssemblyManifest class. + /// + public AssemblyManifest() + { + } + + /// + /// Specifies the set of external proxy stubs referenced by the manifest for isolated applications and Reg-Free COM. + /// + [XmlIgnore] + public ProxyStub[] ExternalProxyStubs + { + get { return _externalProxyStubs; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("ExternalProxyStubs")] + public ProxyStub[] XmlExternalProxyStubs + { + get { return _externalProxyStubs; } + set { _externalProxyStubs = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/AssemblyReference.cs b/src/XMakeTasks/ManifestUtil/AssemblyReference.cs new file mode 100644 index 00000000000..bebb3b3b03f --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/AssemblyReference.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes the type of an assembly reference. + /// + public enum AssemblyReferenceType + { + /// + /// Assembly type is unspecified and will be determined by the UpdateFileInfo method. + /// + Unspecified, + /// + /// Specifies a ClickOnce manifest. + /// + ClickOnceManifest, + /// + /// Specifies a .NET assembly. + /// + ManagedAssembly, + /// + /// Specifies a Win32 native assembly. + /// + NativeAssembly + }; + + /// + /// Describes a manifest assembly reference. + /// + [ComVisible(false)] + public sealed class AssemblyReference : BaseReference + { + private AssemblyIdentity _assemblyIdentity = null; + private bool _isPrerequisite = false; + private AssemblyReferenceType _referenceType = AssemblyReferenceType.Unspecified; + private bool _isPrimary = false; + + /// + /// Initializes a new instance of the AssemblyReference class. + /// + public AssemblyReference() + { + } + + /// + /// Initializes a new instance of the AssemblyReference class. + /// + /// The specified source path of the file. + public AssemblyReference(string path) : base(path) + { + } + + /// + /// Specifies the identity of the assembly reference. + /// + [XmlIgnore] + public AssemblyIdentity AssemblyIdentity + { + get { return _assemblyIdentity; } + set { _assemblyIdentity = value; } + } + + /// + /// Specifies whether the assembly reference is a prerequisite. + /// + [XmlIgnore] + public bool IsPrerequisite + { + get { return _isPrerequisite; } + set { _isPrerequisite = value; } + } + + [XmlIgnore] + internal bool IsVirtual + { + get + { + if (AssemblyIdentity == null) + return false; + if (String.Compare(AssemblyIdentity.Name, Constants.CLRPlatformAssemblyName, StringComparison.OrdinalIgnoreCase) == 0) + return true; + else + return false; + } + } + + /// + /// Specifies the type of the assembly reference. + /// + [XmlIgnore] + public AssemblyReferenceType ReferenceType + { + get { return _referenceType; } + set { _referenceType = value; } + } + + + /// + /// True if the reference is specified in the project file, false if it is added to the manifest as a result + /// of computing the closure of all project references. + /// + [XmlIgnore] + internal bool IsPrimary + { + get { return _isPrimary; } + set { _isPrimary = value; } + } + + protected internal override string SortName + { + get + { + if (_assemblyIdentity == null) + return null; + string name = _assemblyIdentity.ToString(); + if (IsVirtual) + name = "1: " + name; // virtual assemblies are first + else if (_isPrerequisite) + name = "2: " + name; // prerequisites are second + else + name = "3: " + name + ", " + TargetPath; // eveything else... + return name; + } + } + + public override string ToString() + { + string str = base.ToString(); + if (!String.IsNullOrEmpty(str)) + return str; + if (_assemblyIdentity != null) + return _assemblyIdentity.ToString(); + return String.Empty; + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlElement("AssemblyIdentity")] + public AssemblyIdentity XmlAssemblyIdentity + { + get { return _assemblyIdentity; } + set { _assemblyIdentity = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("IsNative")] + public string XmlIsNative + { + get { return _referenceType == AssemblyReferenceType.NativeAssembly ? "true" : "false"; } + set { _referenceType = ConvertUtil.ToBoolean(value) ? AssemblyReferenceType.NativeAssembly : AssemblyReferenceType.ManagedAssembly; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("IsPrerequisite")] + public string XmlIsPrerequisite + { + get { return Convert.ToString(_isPrerequisite, CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture); } + set { _isPrerequisite = ConvertUtil.ToBoolean(value); } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/AssemblyReferenceCollection.cs b/src/XMakeTasks/ManifestUtil/AssemblyReferenceCollection.cs new file mode 100644 index 00000000000..774e77e6e09 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/AssemblyReferenceCollection.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using System.Collections; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Provides a collection for manifest assembly references. + /// + [ComVisible(false)] + public sealed class AssemblyReferenceCollection : IEnumerable + { + private ArrayList _list = new ArrayList(); + + internal AssemblyReferenceCollection(AssemblyReference[] array) + { + if (array == null) + return; + _list.AddRange(array); + } + + /// + /// Gets the element at the specified index. + /// + /// The zero-based index of the entry to get. + /// The assembly reference instance. + public AssemblyReference this[int index] + { + get { return (AssemblyReference)_list[index]; } + } + + /// + /// Adds the specified assembly reference to the collection. + /// + /// The specified assembly reference to add. + /// The added assembly reference instance. + public AssemblyReference Add(string path) + { + return Add(new AssemblyReference(path)); + } + + /// + /// Adds the specified assembly reference to the collection. + /// + /// The specified assembly reference to add. + /// The added assembly reference instance. + public AssemblyReference Add(AssemblyReference assembly) + { + _list.Add(assembly); + return assembly; + } + + /// + /// Removes all objects from the collection. + /// + public void Clear() + { + _list.Clear(); + } + + /// + /// Gets the number of objects contained in the collection. + /// + public int Count + { + get { return _list.Count; } + } + + /// + /// Finds an assembly reference in the collection by simple name. + /// + /// The specified assembly simple name. + /// The found assembly reference. + public AssemblyReference Find(string name) + { + if (String.IsNullOrEmpty(name)) + return null; + foreach (AssemblyReference a in _list) + if (a.AssemblyIdentity != null && String.Compare(name, a.AssemblyIdentity.Name, StringComparison.OrdinalIgnoreCase) == 0) + return a; + return null; + } + + /// + /// Finds an assembly reference in the collection by the specified assembly identity. + /// + /// The specified assembly identity. + /// The found assembly reference. + public AssemblyReference Find(AssemblyIdentity identity) + { + if (identity == null) + return null; + + foreach (AssemblyReference a in _list) + { + AssemblyIdentity listItemIdentity = a.AssemblyIdentity; + + // if the item in our list doesn't have an identity but is a managed assembly, + // we calculate it by reading the file from disk to find its identity. + // + // note that this is here specifically to deal with the scenario when we are being + // asked to find a reference to one of our sentinel assemblies which are known to + // be managed assemblies. doing this ensures that our sentinel assemblies do not + // show up twice in the manifest. + // + // we are assuming the incoming identity for the sentinel assembly really is the + // sentinel assembly that MS owns and emits in manifests for ClickOnce application + // prereq verification for .Net 2.0, 3.0 and 3.5 frameworks. otherwise, we expect + // the incoming identity to fail the comparison (because something like the + // public key token won't match if there is a user-owned reference to something + // that has the same name as a sentinel assembly). + // + // note that we only read the file from disk if the incoming identity's name matches + // the file-name of the item in the list to avoid unnecessarily loading every + // reference in the list of references + // + if (listItemIdentity == null && + identity.Name != null && + a.SourcePath != null && + a.ReferenceType == AssemblyReferenceType.ManagedAssembly && + String.Equals(identity.Name, System.IO.Path.GetFileNameWithoutExtension(a.SourcePath), StringComparison.OrdinalIgnoreCase)) + { + listItemIdentity = AssemblyIdentity.FromManagedAssembly(a.SourcePath); + } + + if (AssemblyIdentity.IsEqual(listItemIdentity, identity)) + { + return a; + } + } + + return null; + } + + /// + /// Finds an assembly reference in the collection by the specified target path. + /// + /// The specified target path. + /// The found assembly reference. + public AssemblyReference FindTargetPath(string targetPath) + { + if (String.IsNullOrEmpty(targetPath)) + return null; + foreach (AssemblyReference a in _list) + if (String.Compare(targetPath, a.TargetPath, StringComparison.OrdinalIgnoreCase) == 0) + return a; + return null; + } + + /// + /// Returns an enumerator that can iterate through the collection. + /// + /// The enumerator. + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + /// + /// Removes the specified assembly reference from the collection. + /// + /// The specified assembly reference to remove. + public void Remove(AssemblyReference assemblyReference) + { + _list.Remove(assemblyReference); + } + + internal AssemblyReference[] ToArray() + { + return (AssemblyReference[])_list.ToArray(typeof(AssemblyReference)); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/BaseReference.cs b/src/XMakeTasks/ManifestUtil/BaseReference.cs new file mode 100644 index 00000000000..35c98a9724d --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/BaseReference.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes base functionality common to both file and assembly references. + /// + [ComVisible(false)] + public abstract class BaseReference + { + private bool _includeHash = true; + private string _group = null; + private string _hash = null; + private string _hashAlgorithm = null; + private string _isOptional = null; + private string _resolvedPath = null; + private string _size = null; + private string _sourcePath = null; + private string _targetPath = null; + + protected internal BaseReference() // only internal classes can extend this class + { + } + + protected internal BaseReference(string path) // only internal classes can extend this class + { + _sourcePath = path; + _targetPath = GetDefaultTargetPath(path); + } + + internal static string GetDefaultTargetPath(string path) + { + if (String.IsNullOrEmpty(path)) + return path; + + if (path.EndsWith(Constants.DeployFileExtension, StringComparison.OrdinalIgnoreCase)) + path = path.Substring(0, path.Length - Constants.DeployFileExtension.Length); + + if (!Path.IsPathRooted(path)) + return path; + else + return Path.GetFileName(path); + } + + internal bool IncludeHash + { + get { return _includeHash; } + set { _includeHash = value; } + } + + /// + /// Specifies the group for on-demand download functionality. A blank string indicates a primary file. + /// + [XmlIgnore] + public string Group + { + get { return _group; } + set { _group = value; } + } + + /// + /// Specifies the SHA1 hash of the file. + /// + [XmlIgnore] + public string Hash + { + get + { + if (!IncludeHash) + return string.Empty; + return _hash; + } + set { _hash = value; } + } + + /// + /// Specifies whether the file is optional for on-deman download functionality. + /// + [XmlIgnore] + public bool IsOptional + { + get { return ConvertUtil.ToBoolean(_isOptional); } + set { _isOptional = value ? "true" : null; } // NOTE: optional=false is implied, and Fusion prefers them to be unspecified + } + + /// + /// Specifies the resolved path to the file. This path is determined by the Resolve method, and is used to compute the file information by the UpdateFileInfo method. + /// + [XmlIgnore] + public string ResolvedPath + { + get { return _resolvedPath; } + set { _resolvedPath = value; } + } + + /// + /// Specifies the file size in bytes. + /// + [XmlIgnore] + public long Size + { + get { return Convert.ToInt64(_size, CultureInfo.InvariantCulture); } + set { _size = Convert.ToString(value, CultureInfo.InvariantCulture); } + } + + protected internal abstract string SortName { get; } + + /// + /// Specifies the source path of the file. + /// + [XmlIgnore] + public string SourcePath + { + get { return _sourcePath; } + set { _sourcePath = value; } + } + + /// + /// Specifies the target path of the file. This is the path that is used for specification in the generated manifest. + /// + [XmlIgnore] + public string TargetPath + { + get { return _targetPath; } + set { _targetPath = value; } + } + + public override string ToString() + { + if (!String.IsNullOrEmpty(_sourcePath)) + return _sourcePath; + if (!String.IsNullOrEmpty(_resolvedPath)) + return _resolvedPath; + if (!String.IsNullOrEmpty(_targetPath)) + return _targetPath; + return String.Empty; + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Group")] + public string XmlGroup + { + get { return _group; } + set { _group = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Hash")] + public string XmlHash + { + get { return Hash; } + set { _hash = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("HashAlg")] + public string XmlHashAlgorithm + { + get { return _hashAlgorithm; } + set { _hashAlgorithm = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("IsOptional")] + public string XmlIsOptional + { + get { return _isOptional != null ? _isOptional.ToLower(CultureInfo.InvariantCulture) : null; } + set { _isOptional = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Path")] + public string XmlPath + { + get { return _targetPath; } + set { _targetPath = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Size")] + public string XmlSize + { + get { return _size; } + set { _size = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/ComImporter.cs b/src/XMakeTasks/ManifestUtil/ComImporter.cs new file mode 100644 index 00000000000..e674cd64081 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ComImporter.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Win32; +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Resources; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal class ComImporter + { + private readonly TypeLib _typeLib; + private readonly ComClass[] _comClasses; + private readonly OutputMessageCollection _outputMessages; + private readonly string _outputDisplayName; + private readonly ResourceManager _resources = new ResourceManager("Microsoft.Build.Tasks.Deployment.ManifestUtilities.Strings", System.Reflection.Assembly.GetExecutingAssembly()); + private bool _success = true; + + private readonly static string[] s_emptyArray = new string[] { }; + + // These must be defined in sorted order! + private readonly static string[] s_knownImplementedCategories = new string[] + { + "{02496840-3AC4-11cf-87B9-00AA006C8166}", //CATID_VBFormat + "{02496841-3AC4-11cf-87B9-00AA006C8166}", //CATID_VBGetControl + "{40FC6ED5-2438-11CF-A3DB-080036F12502}", + }; + private readonly static string[] s_knownSubKeys = new string[] + { + "Control", + "Programmable", + "ToolboxBitmap32", + "TypeLib", + "Version", + "VersionIndependentProgID", + }; + + public ComImporter(string path, OutputMessageCollection outputMessages, string outputDisplayName) + { + _outputMessages = outputMessages; + _outputDisplayName = outputDisplayName; + + if (NativeMethods.SfcIsFileProtected(IntPtr.Zero, path) != 0) + outputMessages.AddWarningMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.ProtectedFile")); + + object obj = null; + try { NativeMethods.LoadTypeLibEx(path, NativeMethods.RegKind.RegKind_None, out obj); } + catch (COMException) { } + +#pragma warning disable 618 + UCOMITypeLib tlib = (UCOMITypeLib)obj; + if (tlib != null) + { + IntPtr typeLibAttrPtr = IntPtr.Zero; + tlib.GetLibAttr(out typeLibAttrPtr); + TYPELIBATTR typeLibAttr = (TYPELIBATTR)Marshal.PtrToStructure(typeLibAttrPtr, typeof(TYPELIBATTR)); + tlib.ReleaseTLibAttr(typeLibAttrPtr); + Guid tlbid = typeLibAttr.guid; + + string name, docString, helpFile; + int helpContext; + tlib.GetDocumentation(-1, out name, out docString, out helpContext, out helpFile); + string helpdir = Util.FilterNonprintableChars(helpFile); //Path.GetDirectoryName(helpFile); + + _typeLib = new TypeLib(tlbid, new Version(typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum), helpdir, typeLibAttr.lcid, Convert.ToInt32(typeLibAttr.wLibFlags, CultureInfo.InvariantCulture)); + + List comClassList = new List(); + int count = tlib.GetTypeInfoCount(); + for (int i = 0; i < count; ++i) + { + TYPEKIND tkind; + tlib.GetTypeInfoType(i, out tkind); + if (tkind == TYPEKIND.TKIND_COCLASS) + { + UCOMITypeInfo tinfo; + tlib.GetTypeInfo(i, out tinfo); + + IntPtr tinfoAttrPtr = IntPtr.Zero; + tinfo.GetTypeAttr(out tinfoAttrPtr); + TYPEATTR tinfoAttr = (TYPEATTR)Marshal.PtrToStructure(tinfoAttrPtr, typeof(TYPEATTR)); + tinfo.ReleaseTypeAttr(tinfoAttrPtr); + Guid clsid = tinfoAttr.guid; + string sclsid = clsid.ToString("B"); + + tlib.GetDocumentation(i, out name, out docString, out helpContext, out helpFile); + string description = Util.FilterNonprintableChars(docString); + + ClassInfo info = GetRegisteredClassInfo(clsid); + if (info == null) + continue; + + comClassList.Add(new ComClass(tlbid, clsid, info.Progid, info.ThreadingModel, description)); + } + } + if (comClassList.Count > 0) + { + _comClasses = comClassList.ToArray(); + _success = true; + } + else + { + outputMessages.AddErrorMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.NoRegisteredClasses")); + _success = false; + } + } + else + { + outputMessages.AddErrorMessage("GenerateManifest.ComImport", outputDisplayName, _resources.GetString("ComImporter.TypeLibraryLoadFailure")); + _success = false; + } +#pragma warning restore 618 + } + + private void CheckForUnknownSubKeys(RegistryKey key) + { + CheckForUnknownSubKeys(key, s_emptyArray); + } + + private void CheckForUnknownSubKeys(RegistryKey key, string[] knownNames) + { + if (key.SubKeyCount > 0) + foreach (string name in key.GetSubKeyNames()) + if (Array.BinarySearch(knownNames, name, StringComparer.OrdinalIgnoreCase) < 0) + _outputMessages.AddWarningMessage("GenerateManifest.ComImport", _outputDisplayName, String.Format(CultureInfo.CurrentCulture, _resources.GetString("ComImporter.SubKeyNotImported"), key.Name + "\\" + name)); + } + + private void CheckForUnknownValues(RegistryKey key) + { + CheckForUnknownValues(key, s_emptyArray); + } + + private void CheckForUnknownValues(RegistryKey key, string[] knownNames) + { + if (key.ValueCount > 0) + foreach (string name in key.GetValueNames()) + if (!String.IsNullOrEmpty(name) && Array.BinarySearch(knownNames, name, StringComparer.OrdinalIgnoreCase) < 0) + _outputMessages.AddWarningMessage("GenerateManifest.ComImport", _outputDisplayName, String.Format(CultureInfo.CurrentCulture, _resources.GetString("ComImporter.ValueNotImported"), key.Name + "\\@" + name)); + } + + private ClassInfo GetRegisteredClassInfo(Guid clsid) + { + ClassInfo info = null; + RegistryKey userKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\CLASSES\\CLSID"); + if (GetRegisteredClassInfo(userKey, clsid, ref info)) + return info; + RegistryKey machineKey = Registry.ClassesRoot.OpenSubKey("CLSID"); + if (GetRegisteredClassInfo(machineKey, clsid, ref info)) + return info; + return null; + } + + private bool GetRegisteredClassInfo(RegistryKey rootKey, Guid clsid, ref ClassInfo info) + { + if (rootKey == null) + return false; + + string sclsid = clsid.ToString("B"); + RegistryKey classKey = rootKey.OpenSubKey(sclsid); + if (classKey == null) + return false; + + bool succeeded = true; + string registeredPath = null; + string threadingModel = null; + string progid = null; + + string[] subKeyNames = classKey.GetSubKeyNames(); + foreach (string subKeyName in subKeyNames) + { + RegistryKey subKey = classKey.OpenSubKey(subKeyName); + if (String.Equals(subKeyName, "InProcServer32", StringComparison.OrdinalIgnoreCase)) + { + registeredPath = (string)subKey.GetValue(null); + threadingModel = (string)subKey.GetValue("ThreadingModel"); + CheckForUnknownSubKeys(subKey); + CheckForUnknownValues(subKey, new string[] { "ThreadingModel" }); + } + else if (String.Equals(subKeyName, "ProgID", StringComparison.OrdinalIgnoreCase)) + { + RegistryKey progidKey = classKey.OpenSubKey(subKeyName); + progid = (string)subKey.GetValue(null); + CheckForUnknownSubKeys(subKey); + CheckForUnknownValues(subKey); + } + else if (String.Equals(subKeyName, "LocalServer32", StringComparison.OrdinalIgnoreCase)) + { + _outputMessages.AddWarningMessage("GenerateManifest.ComImport", _outputDisplayName, String.Format(CultureInfo.CurrentCulture, _resources.GetString("ComImporter.LocalServerNotSupported"), classKey.Name + "\\LocalServer32")); + } + else if (String.Equals(subKeyName, "Implemented Categories", StringComparison.OrdinalIgnoreCase)) + { + CheckForUnknownSubKeys(subKey, s_knownImplementedCategories); + CheckForUnknownValues(subKey); + } + else + { + if (Array.BinarySearch(s_knownSubKeys, subKeyName, StringComparer.OrdinalIgnoreCase) < 0) + _outputMessages.AddWarningMessage("GenerateManifest.ComImport", _outputDisplayName, String.Format(CultureInfo.CurrentCulture, _resources.GetString("ComImporter.SubKeyNotImported"), classKey.Name + "\\" + subKeyName)); + } + } + + if (String.IsNullOrEmpty(registeredPath)) + { + _outputMessages.AddErrorMessage("GenerateManifest.ComImport", _outputDisplayName, String.Format(CultureInfo.CurrentCulture, _resources.GetString("ComImporter.MissingValue"), classKey.Name + "\\InProcServer32", "(Default)")); + succeeded = false; + } + + info = new ClassInfo(progid, registeredPath, threadingModel); + return succeeded; + } + + public bool Success { get { return _success; } } + public ComClass[] ComClasses { get { return _comClasses; } } + public TypeLib TypeLib { get { return _typeLib; } } + + + private class ClassInfo + { + internal readonly string Progid; + internal readonly string RegisteredPath; + internal readonly string ThreadingModel; + internal ClassInfo(string progid, string registeredPath, string threadingModel) + { + Progid = progid; + RegisteredPath = registeredPath; + ThreadingModel = threadingModel; + } + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/CompatibleFramework.cs b/src/XMakeTasks/ManifestUtil/CompatibleFramework.cs new file mode 100644 index 00000000000..483d7f180d8 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/CompatibleFramework.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes a CompatibleFramework for an deployment manifest + /// + [ComVisible(false)] + public sealed class CompatibleFramework + { + private string _version = null; + private string _profile = null; + private string _supportedRuntime = null; + + /// + /// Initializes a new instance of the CompatibleFramework class + /// + public CompatibleFramework() + { + } + + [XmlIgnore] + public string Version + { + get { return _version; } + set { _version = value; } + } + + [XmlIgnore] + public string Profile + { + get { return _profile; } + set { _profile = value; } + } + + [XmlIgnore] + public string SupportedRuntime + { + get { return _supportedRuntime; } + set { _supportedRuntime = value; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Version")] + public string XmlVersion + { + get { return _version; } + set { _version = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Profile")] + public string XmlProfile + { + get { return _profile; } + set { _profile = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("SupportedRuntime")] + public string XmlSupportedRuntime + { + get { return _supportedRuntime; } + set { _supportedRuntime = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/CompatibleFrameworkCollection.cs b/src/XMakeTasks/ManifestUtil/CompatibleFrameworkCollection.cs new file mode 100644 index 00000000000..a4f0f42037a --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/CompatibleFrameworkCollection.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + [ComVisible(false)] + public sealed class CompatibleFrameworkCollection : IEnumerable + { + private List _list = new List(); + + internal CompatibleFrameworkCollection(CompatibleFramework[] compatibleFrameworks) + { + if (compatibleFrameworks == null) + return; + _list.AddRange(compatibleFrameworks); + } + + public CompatibleFramework this[int index] + { + get { return _list[index]; } + } + + public void Add(CompatibleFramework compatibleFramework) + { + _list.Add(compatibleFramework); + } + + public void Clear() + { + _list.Clear(); + } + + public int Count + { + get { return _list.Count; } + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + internal CompatibleFramework[] ToArray() + { + return _list.ToArray(); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/Constants.cs b/src/XMakeTasks/ManifestUtil/Constants.cs new file mode 100644 index 00000000000..227775923c5 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/Constants.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.IO; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class Constants + { + public const string CLRPlatformAssemblyName = "Microsoft.Windows.CommonLanguageRuntime"; + public const string DeployFileExtension = ".deploy"; + public const string OSVersion_Win9X = "4.10.0.0"; + public const string OSVersion_WinXP = "5.1.2600.0"; + public static readonly Version EntryPointMinimumImageVersion = new Version("2.0.0.0"); + public const string TargetFrameworkVersion20 = "v2.0"; + public const string TargetFrameworkVersion30 = "v3.0"; + public const string TargetFrameworkVersion35 = "v3.5"; + public const string TargetFrameworkVersion40 = "v4.0"; + public static readonly string[] NET30AssemblyIdentity = { "WindowsBase", "3.0.0.0", "31bf3856ad364e35", "neutral", "msil" }; + public static readonly string[] NET35AssemblyIdentity = { "System.Core", "3.5.0.0", "b77a5c561934e089", "neutral", "msil" }; + public static readonly string[] NET35SP1AssemblyIdentity = { "System.Data.Entity", "3.5.0.0", "b77a5c561934e089", "neutral", "msil" }; + public static readonly string[] NET35ClientAssemblyIdentity = { "Sentinel.v3.5Client", "3.5.0.0", "b03f5f7f11d50a3a", "neutral", "msil" }; + public const string UACAsInvoker = "asInvoker"; + public const string UACUIAccess = "false"; + public const int MaxFileAssociationsCount = 8; + public const int MaxFileAssociationExtensionLength = 24; + public const string ClientFrameworkSubset = "Client"; + public const string DotNetFrameworkIdentifier = ".NETFramework"; + } +} diff --git a/src/XMakeTasks/ManifestUtil/ConvertUtil.cs b/src/XMakeTasks/ManifestUtil/ConvertUtil.cs new file mode 100644 index 00000000000..ffd1a913d5f --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ConvertUtil.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class ConvertUtil + { + public static bool ToBoolean(string value) + { + return ToBoolean(value, false); + } + + public static bool ToBoolean(string value, bool defaultValue) + { + if (!String.IsNullOrEmpty(value)) + { + try + { + return Convert.ToBoolean(value, CultureInfo.InvariantCulture); + } + catch (FormatException) + { + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Invalid value '{0}' for {1}, returning {2}", value, typeof(bool).Name, defaultValue.ToString())); + } + catch (ArgumentException) + { + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Invalid value '{0}' for {1}, returning {2}", value, typeof(bool).Name, defaultValue.ToString())); + } + } + return defaultValue; + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/DeployManifest.cs b/src/XMakeTasks/ManifestUtil/DeployManifest.cs new file mode 100644 index 00000000000..3a7bab24b47 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/DeployManifest.cs @@ -0,0 +1,780 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Globalization; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using System.Collections.Generic; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Specifies how the application checks for updates. + /// + [ComVisible(false)] + public enum UpdateMode + { + /// + /// Check for updates in the background, after the application starts. + /// + Background, + /// + /// Check for updates in the foreground, before the application starts. + /// + Foreground + } + + /// + /// Specifies the units for the update interval. + /// + [ComVisible(false)] + public enum UpdateUnit + { + /// + /// Update interval is in hours. + /// + Hours, + /// + /// Update interval is in days. + /// + Days, + /// + /// Update interval is in weeks. + /// + Weeks + } + + /// + /// Describes a ClickOnce deployment manifest. + /// + [ComVisible(false)] + [XmlRoot("DeployManifest")] + public sealed class DeployManifest : Manifest + { + private string _createDesktopShortcut = null; + private string _deploymentUrl = null; + private string _disallowUrlActivation = null; + private AssemblyReference _entryPoint = null; + private string _errorReportUrl = null; + private string _install = "true"; + private string _mapFileExtensions = null; + private string _minimumRequiredVersion = null; + private string _product = null; + private string _publisher = null; + private string _suiteName = null; + private string _supportUrl = null; + private string _trustUrlParameters = null; + private string _updateEnabled = null; + private string _updateInterval = "0"; + private string _updateMode = null; + private string _updateUnit = "days"; + private CompatibleFrameworkCollection _compatibleFrameworkList = null; + private List _compatibleFrameworks = null; + private string _targetFrameworkMoniker = null; + + private const string _redistListFolder = "RedistList"; + private const string _redistListFile = "FrameworkList.xml"; + + /// + /// Initializes a new instance of the DeployManifest class. + /// + public DeployManifest() + { + _compatibleFrameworks = new List(); + } + + /// + /// Initializes a new instance of the DeployManifest class. + /// + public DeployManifest(string targetFrameworkMoniker) + { + _compatibleFrameworks = new List(); + DiscoverCompatFrameworks(targetFrameworkMoniker); + } + + private void DiscoverCompatFrameworks(string moniker) + { + if (!string.IsNullOrEmpty(moniker)) + { + FrameworkNameVersioning frameworkName = new FrameworkNameVersioning(moniker); + if (frameworkName.Version.Major >= 4) + { + _compatibleFrameworks.Clear(); + DiscoverCompatibleFrameworks(frameworkName); + } + } + } + + private void DiscoverCompatibleFrameworks(FrameworkNameVersioning frameworkName) + { + FrameworkNameVersioning installableFrameworkName = GetInstallableFrameworkName(frameworkName); + + // if profile is null or empty. + if (string.IsNullOrEmpty(installableFrameworkName.Profile)) + { + _compatibleFrameworks.Add(GetFullCompatFramework(installableFrameworkName)); + } + else + { + _compatibleFrameworks.Add(GetSubsetCompatFramework(installableFrameworkName)); + _compatibleFrameworks.Add(GetFullCompatFramework(installableFrameworkName)); + } + } + + /// + /// codes from GetInstallableFrameworkForTargetFxInternal in + /// env/vscore/package/FxMultiTargeting/FrameworkMultiTargetingInternal.cs + /// + /// + /// + private FrameworkNameVersioning GetInstallableFrameworkName(FrameworkNameVersioning frameworkName) + { + string installableFramework = null; + FrameworkNameVersioning installableFrameworkObj = null; + + IList referenceAssemblyPaths = GetPathToReferenceAssemblies(frameworkName); + + if (referenceAssemblyPaths != null && referenceAssemblyPaths.Count > 0) + { + // the first one in the list is the reference assembly path for the requested TFM + string referenceAssemblyPath = referenceAssemblyPaths[0]; + + // Get the redistlist file path + string redistListFilePath = GetRedistListFilePath(referenceAssemblyPath); + + if (File.Exists(redistListFilePath)) + { + installableFramework = GetInstallableFramework(redistListFilePath); + } + } + + // If the installable framework value is not in the redist, there was no redist, or no matching FX we return the sent TFM, + // this means frameworks that are installable themselves don't need to specify this property + // and that all unknown frameworks are assumed to be installable. + if (installableFramework == null) + { + installableFrameworkObj = frameworkName; + } + else + { + try + { + installableFrameworkObj = new FrameworkNameVersioning(installableFramework); + } + catch (ArgumentException) + { + // Redist list data was invalid, behave as if it was not defined. + installableFrameworkObj = frameworkName; + } + } + + return installableFrameworkObj; + } + + private string GetRedistListFilePath(string referenceAssemblyPath) + { + string redistListPath = Path.Combine(referenceAssemblyPath, _redistListFolder); + redistListPath = Path.Combine(redistListPath, _redistListFile); + + return redistListPath; + } + + private IList GetPathToReferenceAssemblies(FrameworkNameVersioning targetFrameworkMoniker) + { + IList targetFrameworkPaths = null; + try + { + targetFrameworkPaths = ToolLocationHelper.GetPathToReferenceAssemblies(targetFrameworkMoniker); + + // this returns the chained reference assemblies folders of the framework + // ordered from highest to lowest version + } + catch (InvalidOperationException) + { + // The chained dirs does not exist + // or could not read redistlist for chain + } + + return targetFrameworkPaths; + } + + /// + /// Gets the InstallableFramework by reading the 'InstallableFramework' attribute in the redist file of the target framework + /// + /// the path to the redistlist file + /// InstallableFramework + private string GetInstallableFramework(string redistListFilePath) + { + string installableFramework = null; + + try + { + XmlDocument doc = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(redistListFilePath, xrSettings)) + { + doc.Load(xr); + XmlNode fileListNode = doc.DocumentElement; + if (fileListNode != null) + { + XmlAttribute nameattr = fileListNode.Attributes["InstallableFramework"]; + if (nameattr != null) + { + if (!String.IsNullOrEmpty(nameattr.Value)) + { + installableFramework = nameattr.Value; + } + } + } + } + } + catch (Exception) + { + } + + return installableFramework; + } + + + private CompatibleFramework GetSubsetCompatFramework(FrameworkNameVersioning frameworkName) + { + CompatibleFramework compat = GetFullCompatFramework(frameworkName); + compat.Profile = frameworkName.Profile; + + return compat; + } + + private CompatibleFramework GetFullCompatFramework(FrameworkNameVersioning frameworkName) + { + CompatibleFramework compat = new CompatibleFramework(); + compat.Version = frameworkName.Version.ToString(); + + compat.SupportedRuntime = PatchCLRVersion(Util.GetClrVersion(frameworkName.Version.ToString())); + compat.Profile = "Full"; + + return compat; + } + + /// + /// conver (MajorVersion).(MinorVersion).(Build).(Revision) to (MajorVersion).(MinorVersion).(Build) + /// + /// + /// + private string PatchCLRVersion(string version) + { + try + { + Version ver = new Version(version); + Version result = new Version(ver.Major, ver.Minor, ver.Build); + return result.ToString(); + } + catch (ArgumentException) + { + //continue + } + catch (FormatException) + { + //continue + } + catch (OverflowException) + { + //continue + } + + return version; + } + + /// + /// Specifies whether the application install will create a shortcut on the desktop + /// If True, the installation will create a shortcut to the application on the desktop. + /// The default is False + /// If Install is False, this value will be ignored + /// + [XmlIgnore] + public bool CreateDesktopShortcut + { + get { return ConvertUtil.ToBoolean(_createDesktopShortcut); } + set { _createDesktopShortcut = (value ? "true" : null); } + } + + /// + /// Specifies the target framework moniker of this project. + /// + [XmlIgnore] + public string TargetFrameworkMoniker + { + get { return _targetFrameworkMoniker; } + set + { + _targetFrameworkMoniker = value; + DiscoverCompatFrameworks(_targetFrameworkMoniker); + } + } + + /// + /// A collection of CompatibleFrameworks + /// + [XmlIgnore] + public CompatibleFrameworkCollection CompatibleFrameworks + { + get + { + if (_compatibleFrameworkList == null && _compatibleFrameworks != null) + _compatibleFrameworkList = new CompatibleFrameworkCollection(_compatibleFrameworks.ToArray()); + return _compatibleFrameworkList; + } + } + + /// + /// Specifies the update location for the application. + /// If this input is not specified then no update location will be defined for the application. + /// However, if application updates are specified then the update location must be specified. + /// The specified value should be a fully qualified URL or UNC path. + /// + [XmlIgnore] + public string DeploymentUrl + { + get { return _deploymentUrl; } + set { _deploymentUrl = value; } + } + + /// + /// Specifies whether the application should be blocked from being activated via a URL. + /// If this option is True then application can only be activated from the user's Start menu. + /// The default is False. + /// This option is ignored if the Install property is False. + /// + [XmlIgnore] + public bool DisallowUrlActivation + { + get { return ConvertUtil.ToBoolean(_disallowUrlActivation); } + set { _disallowUrlActivation = value ? "true" : null; } // NOTE: disallowUrlActivation=false is implied, and Fusion prefers the false case to be unspecified + } + + [XmlIgnore] + public override AssemblyReference EntryPoint + { + get + { + return _entryPoint; + } + set + { + _entryPoint = value; + } + } + + /// + /// Specifies the link to use if there is a failure launching the application. + /// The specified value should be a fully qualified URL or UNC path. + /// + [XmlIgnore] + public string ErrorReportUrl + { + get { return _errorReportUrl; } + set { _errorReportUrl = value; } + } + + /// + /// Specifies whether the application is an installed application or an online only application. + /// If this flag is True the application will be installed on the user's Start menu, and can be removed from the Add/Remove Programs dialog. + /// If this flag is False then the application is intended for online use from a web page. + /// The default is True. + /// + [XmlIgnore] + public bool Install + { + get { return ConvertUtil.ToBoolean(_install); } + set { _install = Convert.ToString(value, CultureInfo.InvariantCulture); } + } + + /// + /// Specifies whether or not the ".deploy" file extension mapping is used. + /// If this flag is true then every application file is published with a ".deploy" file extension. + /// This option is useful for web server security to limit the number of file extensions that need to be unblocked to enable ClickOnce application deployment. + /// The default is false. + /// + [XmlIgnore] + public bool MapFileExtensions + { + get { return ConvertUtil.ToBoolean(_mapFileExtensions); } + set { _mapFileExtensions = value ? "true" : null; } // NOTE: mapFileExtensions=false is implied, and Fusion prefers the false case to be unspecified + } + + /// + /// Specifies whether or not the user can skip the update. + /// If the user has a version less than the minimum required, he or she will not have the option to skip the update. + /// The default is to have no minimum required version. + /// This input only applies when Install is True. + /// + [XmlIgnore] + public string MinimumRequiredVersion + { + get { return _minimumRequiredVersion; } + set { _minimumRequiredVersion = value; } + } + + internal override void OnAfterLoad() + { + base.OnAfterLoad(); + if (_entryPoint == null && AssemblyReferences != null && AssemblyReferences.Count > 0) + { + _entryPoint = AssemblyReferences[0]; + _entryPoint.ReferenceType = AssemblyReferenceType.ClickOnceManifest; + } + } + + internal override void OnBeforeSave() + { + base.OnBeforeSave(); + if (AssemblyIdentity != null && String.IsNullOrEmpty(AssemblyIdentity.PublicKeyToken)) + AssemblyIdentity.PublicKeyToken = "0000000000000000"; + } + + /// + /// Specifies the name of the application. + /// If this input is not specified then the name is inferred from the identity of the generated manifest. + /// This name is used for the shortcut name on the Start menu and is part of the name that appears in the Add/Remove Programs dialog. + /// + [XmlIgnore] + public string Product + { + get { return _product; } + set { _product = value; } + } + + /// + /// Specifies the publisher of the application. + /// If this input is not specified then the name is inferred from the registered user, or the identity of the generated manifest. + /// This name is used for the folder name on the Start menu and is part of the name that appears in the Add/Remove Programs dialog. + /// + [XmlIgnore] + public string Publisher + { + get { return _publisher; } + set { _publisher = value; } + } + + /// + /// Specifies the suite name of the application. + /// This name is used for the sub-folder name on the Start menu (as a child of the publisher) + /// + [XmlIgnore] + public string SuiteName + { + get { return _suiteName; } + set { _suiteName = value; } + } + + /// + /// Specifies the link that appears in the Add/Remove Programs dialog for the application. + /// The specified value should be a fully qualified URL or UNC path. + /// + [XmlIgnore] + public string SupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + /// + /// Specifies whether or not URL query-string parameters should be made available to the application. + /// The default is False indicating that parameters will not be available to the application. + /// + [XmlIgnore] + public bool TrustUrlParameters + { + get { return ConvertUtil.ToBoolean(_trustUrlParameters); } + set { _trustUrlParameters = value ? "true" : null; } // NOTE: trustUrlParameters=false is implied, and Fusion prefers the false case to be unspecified + } + + /// + /// Indicates whether or not the application is updatable. + /// The default is False. + /// This input only applies when Install is True. + /// + [XmlIgnore] + public bool UpdateEnabled + { + get { return ConvertUtil.ToBoolean(_updateEnabled); } + set { _updateEnabled = Convert.ToString(value, CultureInfo.InvariantCulture); } + } + + /// + /// Specifies the update interval for the application. + /// The default is zero. + /// This input only applies when Install and UpdateEnabled are both True. + /// + [XmlIgnore] + public int UpdateInterval + { + get + { + try { return Convert.ToInt32(_updateInterval, CultureInfo.InvariantCulture); } + catch (ArgumentException) { return 1; } + catch (FormatException) { return 1; } + } + set { _updateInterval = Convert.ToString(value, CultureInfo.InvariantCulture); } + } + + /// + /// Specifies whether updates should be checked in the foreground before starting the application, or in the background as the application is running. + /// The default is "Background". + /// This input only applies when Install and UpdateEnabled are both True. + /// + [XmlIgnore] + public UpdateMode UpdateMode + { + get + { + try { return (UpdateMode)Enum.Parse(typeof(UpdateMode), _updateMode, true); } + catch (FormatException) { return UpdateMode.Foreground; } + catch (ArgumentException) { return UpdateMode.Foreground; } + } + set + { + _updateMode = value.ToString(); + } + } + + /// + /// Specifies the units for UpdateInterval input. + /// This input only applies when Install and UpdateEnabled are both True. + /// + [XmlIgnore] + public UpdateUnit UpdateUnit + { + get + { + try { return (UpdateUnit)Enum.Parse(typeof(UpdateUnit), _updateUnit, true); } + catch (FormatException) { return UpdateUnit.Days; } + catch (ArgumentException) { return UpdateUnit.Days; } + } + set + { + _updateUnit = value.ToString(); + } + } + + public override void Validate() + { + base.Validate(); + ValidateDeploymentProvider(); + ValidateMinimumRequiredVersion(); + ValidatePlatform(); + ValidateEntryPoint(); + } + + private void ValidateDeploymentProvider() + { + if (!String.IsNullOrEmpty(_deploymentUrl) && PathUtil.IsLocalPath(_deploymentUrl)) + OutputMessages.AddWarningMessage("GenerateManifest.InvalidDeploymentProvider"); + } + + private void ValidateEntryPoint() + { + if (_entryPoint != null) + { + if (!String.IsNullOrEmpty(_entryPoint.TargetPath) && !_entryPoint.TargetPath.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase)) + OutputMessages.AddErrorMessage("GenerateManifest.InvalidEntryPoint", _entryPoint.ToString()); + + string ManifestPath = _entryPoint.ResolvedPath; + if (ManifestPath == null) + ManifestPath = Path.Combine(Path.GetDirectoryName(SourcePath), _entryPoint.TargetPath); + if (File.Exists(ManifestPath)) + { + ApplicationManifest entryPointManifest = ManifestReader.ReadManifest(ManifestPath, false) as ApplicationManifest; + if (entryPointManifest != null) + { + if (Install) + { + if (entryPointManifest.HostInBrowser) + OutputMessages.AddErrorMessage("GenerateManifest.HostInBrowserNotOnlineOnly"); + } + else + { + if (entryPointManifest.FileAssociations != null && entryPointManifest.FileAssociations.Count > 0) + OutputMessages.AddErrorMessage("GenerateManifest.FileAssociationsNotInstalled"); + } + } + } + } + } + + private void ValidateMinimumRequiredVersion() + { + if (!String.IsNullOrEmpty(_minimumRequiredVersion)) + { + Version v1 = new Version(_minimumRequiredVersion); + Version v2 = new Version(AssemblyIdentity.Version); + if (v1 > v2) + OutputMessages.AddErrorMessage("GenerateManifest.GreaterMinimumRequiredVersion"); + } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("CreateDesktopShortcut")] + public string XmlCreateDesktopShortcut + { + get { return _createDesktopShortcut != null ? _createDesktopShortcut.ToLowerInvariant() : null; } + set { _createDesktopShortcut = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("CompatibleFrameworks")] + public CompatibleFramework[] XmlCompatibleFrameworks + { + get { return _compatibleFrameworks.Count > 0 ? _compatibleFrameworks.ToArray() : null; } + set { _compatibleFrameworks = new List(value); } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("DeploymentUrl")] + public string XmlDeploymentUrl + { + get { return _deploymentUrl; } + set { _deploymentUrl = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("DisallowUrlActivation")] + public string XmlDisallowUrlActivation + { + get { return _disallowUrlActivation != null ? _disallowUrlActivation.ToLowerInvariant() : null; } + set { _disallowUrlActivation = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("ErrorReportUrl")] + public string XmlErrorReportUrl + { + get { return _errorReportUrl; } + set { _errorReportUrl = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Install")] + public string XmlInstall + { + get { return !String.IsNullOrEmpty(_install) ? _install.ToLower(CultureInfo.InvariantCulture) : "true"; } // NOTE: Install attribute shouldn't be null in the manifest, so specify install="true" by default + set { _install = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("MapFileExtensions")] + public string XmlMapFileExtensions + { + get { return _mapFileExtensions != null ? _mapFileExtensions.ToLowerInvariant() : null; } + set { _mapFileExtensions = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("MinimumRequiredVersion")] + public string XmlMinimumRequiredVersion + { + get { return _minimumRequiredVersion; } + set { _minimumRequiredVersion = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Product")] + public string XmlProduct + { + get { return _product; } + set { _product = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Publisher")] + public string XmlPublisher + { + get { return _publisher; } + set { _publisher = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("SuiteName")] + public string XmlSuiteName + { + get { return _suiteName; } + set { _suiteName = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("SupportUrl")] + public string XmlSupportUrl + { + get { return _supportUrl; } + set { _supportUrl = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("TrustUrlParameters")] + public string XmlTrustUrlParameters + { + get { return _trustUrlParameters != null ? _trustUrlParameters.ToLowerInvariant() : null; } + set { _trustUrlParameters = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("UpdateEnabled")] + public string XmlUpdateEnabled + { + get { return _updateEnabled != null ? _updateEnabled.ToLower(CultureInfo.InvariantCulture) : null; } + set { _updateEnabled = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("UpdateInterval")] + public string XmlUpdateInterval + { + get { return _updateInterval; } + set { _updateInterval = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("UpdateMode")] + public string XmlUpdateMode + { + get { return _updateMode; } + set { _updateMode = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("UpdateUnit")] + public string XmlUpdateUnit + { + get { return _updateUnit != null ? _updateUnit.ToLower(CultureInfo.InvariantCulture) : null; } + set { _updateUnit = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/EmbeddedManifestReader.cs b/src/XMakeTasks/ManifestUtil/EmbeddedManifestReader.cs new file mode 100644 index 00000000000..6a341e61fb6 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/EmbeddedManifestReader.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal class EmbeddedManifestReader + { + private static readonly IntPtr s_id1 = new IntPtr(1); + private Stream _manifest = null; + + private EmbeddedManifestReader(string path) + { + IntPtr hModule = IntPtr.Zero; + try + { + hModule = NativeMethods.LoadLibraryExW(path, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); + if (hModule == IntPtr.Zero) + return; + NativeMethods.EnumResNameProc callback = new NativeMethods.EnumResNameProc(EnumResNameCallback); + NativeMethods.EnumResourceNames(hModule, NativeMethods.RT_MANIFEST, callback, IntPtr.Zero); + } + finally + { + if (hModule != IntPtr.Zero) + NativeMethods.FreeLibrary(hModule); + } + } + + private bool EnumResNameCallback(IntPtr hModule, IntPtr pType, IntPtr pName, IntPtr param) + { + if (pName != s_id1) + return false; // only look for resources with ID=1 + IntPtr hResInfo = NativeMethods.FindResource(hModule, pName, NativeMethods.RT_MANIFEST); + if (hResInfo == IntPtr.Zero) + return false; //continue looking + IntPtr hResource = NativeMethods.LoadResource(hModule, hResInfo); + NativeMethods.LockResource(hResource); + uint bufsize = NativeMethods.SizeofResource(hModule, hResInfo); + byte[] buffer = new byte[bufsize]; + + Marshal.Copy(hResource, buffer, 0, buffer.Length); + _manifest = new MemoryStream(buffer, false); + return false; //found what we are looking for + } + + public static Stream Read(string path) + { + if (path == null) throw new ArgumentNullException("path"); + + if (!path.EndsWith(".manifest", StringComparison.Ordinal) && !path.EndsWith(".dll", StringComparison.Ordinal)) + { + // Everything that does not end with .dll or .manifest is not a valid native assembly (this includes + // EXEs with ID1 manifest) + return null; + } + + int t1 = Environment.TickCount; + EmbeddedManifestReader r = new EmbeddedManifestReader(path); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "EmbeddedManifestReader.Read t={0}", Environment.TickCount - t1)); + return r._manifest; + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/FileAssociation.cs b/src/XMakeTasks/ManifestUtil/FileAssociation.cs new file mode 100644 index 00000000000..117a144f15e --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/FileAssociation.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes a fileAssocation for an application manifest + /// + [ComVisible(false)] + public sealed class FileAssociation + { + private string _defaultIcon = null; + private string _description = null; + private string _extension = null; + private string _progid = null; + + /// + /// Initializes a new instance of the FileAssociation class + /// + public FileAssociation() + { + } + + [XmlIgnore] + public string DefaultIcon + { + get { return _defaultIcon; } + set { _defaultIcon = value; } + } + + [XmlIgnore] + public string Description + { + get { return _description; } + set { _description = value; } + } + + [XmlIgnore] + public string Extension + { + get { return _extension; } + set { _extension = value; } + } + + [XmlIgnore] + public string ProgId + { + get { return _progid; } + set { _progid = value; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("DefaultIcon")] + public string XmlDefaultIcon + { + get { return _defaultIcon; } + set { _defaultIcon = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Description")] + public string XmlDescription + { + get { return _description; } + set { _description = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Extension")] + public string XmlExtension + { + get { return _extension; } + set { _extension = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Progid")] + public string XmlProgId + { + get { return _progid; } + set { _progid = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/FileAssociationCollection.cs b/src/XMakeTasks/ManifestUtil/FileAssociationCollection.cs new file mode 100644 index 00000000000..8008871ed7f --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/FileAssociationCollection.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + [ComVisible(false)] + public sealed class FileAssociationCollection : IEnumerable + { + private List _list = new List(); + + internal FileAssociationCollection(FileAssociation[] fileAssociations) + { + if (fileAssociations == null) + return; + _list.AddRange(fileAssociations); + } + + public FileAssociation this[int index] + { + get { return _list[index]; } + } + + public void Add(FileAssociation fileAssociation) + { + _list.Add(fileAssociation); + } + + public void Clear() + { + _list.Clear(); + } + + public int Count + { + get { return _list.Count; } + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + internal FileAssociation[] ToArray() + { + return _list.ToArray(); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/FileReference.cs b/src/XMakeTasks/ManifestUtil/FileReference.cs new file mode 100644 index 00000000000..66db68d20bd --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/FileReference.cs @@ -0,0 +1,518 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes a manifest file reference. + /// + [ComVisible(false)] + public sealed class FileReference : BaseReference + { + private ComClass[] _comClasses = null; + private string _writeableType = null; + private ProxyStub[] _proxyStubs = null; + private TypeLib[] _typeLibs = null; + + /// + /// Initializes a new instance of the FileReference class. + /// + public FileReference() : base() + { + } + + /// + /// Initializes a new instance of the FileReference class. + /// + /// The specified source path of the file. + public FileReference(string path) : base(path) + { + } + + /// + /// Specifies the set of COM classes referenced by the manifest for isolated applications and Reg-Free COM. + /// + [XmlIgnore] + public ComClass[] ComClasses + { + get { return _comClasses; } + } + + internal bool ImportComComponent(string path, OutputMessageCollection outputMessages, string outputDisplayName) + { + ComImporter importer = new ComImporter(path, outputMessages, outputDisplayName); + if (importer.Success) + { + ArrayList list = new ArrayList(); + + // Add TypeLib objects from importer... + if (_typeLibs != null) + list.AddRange(_typeLibs); + if (importer.TypeLib != null) + list.Add(importer.TypeLib); + _typeLibs = (TypeLib[])list.ToArray(typeof(TypeLib)); + + // Add ComClass objects from importer... + list.Clear(); + if (_comClasses != null) + list.AddRange(_comClasses); + if (importer.ComClasses != null) + list.AddRange(importer.ComClasses); + _comClasses = (ComClass[])list.ToArray(typeof(ComClass)); + } + return importer.Success; + } + + /// + /// Specifies whether the file is a data file. + /// + [XmlIgnore] + public bool IsDataFile + { + get { return String.Compare(_writeableType, "applicationData", StringComparison.OrdinalIgnoreCase) == 0; } + set { _writeableType = value ? "applicationData" : null; } + } + + /// + /// Specifies the set of proxy stubs referenced by the manifest for isolated applications and Reg-Free COM. + /// + [XmlIgnore] + public ProxyStub[] ProxyStubs + { + get { return _proxyStubs; } + } + + protected internal override string SortName + { + get { return TargetPath; } + } + + /// + /// Specifies the set of type libraries referenced by the manifest. + /// + [XmlIgnore] + public TypeLib[] TypeLibs + { + get { return _typeLibs; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("ComClasses")] + public ComClass[] XmlComClasses + { + get { return _comClasses; } + set { _comClasses = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("ProxyStubs")] + public ProxyStub[] XmlProxyStubs + { + get { return _proxyStubs; } + set { _proxyStubs = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("TypeLibs")] + public TypeLib[] XmlTypeLibs + { + get { return _typeLibs; } + set { _typeLibs = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("WriteableType")] + public string XmlWriteableType + { + get { return _writeableType; } + set { _writeableType = value; } + } + + #endregion + } + + [ComVisible(false)] + public class ComClass + { + private string _clsid; + private string _description; + private string _progid; + private string _threadingModel; + private string _tlbid; + + public ComClass() + { + } + + internal ComClass(Guid tlbId, Guid clsId, string progId, string threadingModel, string description) + { + _tlbid = tlbId.ToString("B"); + _clsid = clsId.ToString("B"); + _progid = progId; + _threadingModel = threadingModel; + _description = description; + } + + [XmlIgnore] + public string ClsId + { + get { return _clsid; } + } + + [XmlIgnore] + public string Description + { + get { return _description; } + } + + [XmlIgnore] + public string ProgId + { + get { return _progid; } + } + + [XmlIgnore] + public string ThreadingModel + { + get { return _threadingModel; } + } + + [XmlIgnore] + public string TlbId + { + get { return _tlbid; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Clsid")] + public string XmlClsId + { + get { return _clsid; } + set { _clsid = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Description")] + public string XmlDescription + { + get { return _description; } + set { _description = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Progid")] + public string XmlProgId + { + get { return _progid; } + set { _progid = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("ThreadingModel")] + public string XmlThreadingModel + { + get { return _threadingModel; } + set { _threadingModel = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Tlbid")] + public string XmlTlbId + { + get { return _tlbid; } + set { _tlbid = value; } + } + + #endregion + } + + [ComVisible(false)] + public class TypeLib + { + private string _flags; + private string _helpDirectory; + private string _resourceid; + private string _tlbid; + private string _version; + + public TypeLib() + { + } + + internal TypeLib(Guid tlbId, Version version, string helpDirectory, int resourceId, int flags) + { + _tlbid = tlbId.ToString("B"); + _version = version.ToString(2); + _helpDirectory = helpDirectory; + _resourceid = Convert.ToString(resourceId, 16); + _flags = FlagsFromInt(flags); + } + + [XmlIgnore] + public string Flags + { + get { return _flags; } + } + + private static string FlagsFromInt(int flags) + { + StringBuilder sb = new StringBuilder(); + if ((flags & 1) != 0) + sb.Append("RESTRICTED,"); + if ((flags & 2) != 0) + sb.Append("CONTROL,"); + if ((flags & 4) != 0) + sb.Append("HIDDEN,"); + if ((flags & 8) != 0) + sb.Append("HASDISKIMAGE,"); + return sb.ToString().TrimEnd(','); + } + + [XmlIgnore] + public string HelpDirectory + { + get { return _helpDirectory; } + } + + [XmlIgnore] + public string ResourceId + { + get { return _resourceid; } + } + + [XmlIgnore] + public string TlbId + { + get { return _tlbid; } + } + + [XmlIgnore] + public string Version + { + get { return _version; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Flags")] + public string XmlFlags + { + get { return _flags; } + set { _flags = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("HelpDir")] + public string XmlHelpDirectory + { + get { return _helpDirectory; } + set { _helpDirectory = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("ResourceId")] + public string XmlResourceId + { + get { return _resourceid; } + set { _resourceid = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Tlbid")] + public string XmlTlbId + { + get { return _tlbid; } + set { _tlbid = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Version")] + public string XmlVersion + { + get { return _version; } + set { _version = value; } + } + + #endregion + } + + [ComVisible(false)] + public class WindowClass + { + private string _name; + private string _versioned; + + public WindowClass() + { + } + + public WindowClass(string name, bool versioned) + { + _name = name; + _versioned = versioned ? "yes" : "no"; + } + + [XmlIgnore] + public string Name + { + get { return _name; } + } + + [XmlIgnore] + public bool Versioned + { + get + { + if (String.Compare(_versioned, "yes", StringComparison.OrdinalIgnoreCase) == 0) + return true; + if (String.Compare(_versioned, "no", StringComparison.OrdinalIgnoreCase) == 0) + return false; + else + return true; + } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Name")] + public string XmlName + { + get { return _name; } + set { _name = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Versioned")] + public string XmlVersioned + { + get { return _versioned; } + set { _versioned = value; } + } + + #endregion + } + + [ComVisible(false)] + public class ProxyStub + { + private string _baseInterface; + private string _iid; + private string _name; + private string _numMethods; + private string _tlbid; + + public ProxyStub() + { + } + + [XmlIgnore] + public string BaseInterface + { + get { return _baseInterface; } + } + + [XmlIgnore] + public string IID + { + get { return _iid; } + } + + [XmlIgnore] + public string Name + { + get { return _name; } + } + + [XmlIgnore] + public string NumMethods + { + get { return _numMethods; } + } + + [XmlIgnore] + public string TlbId + { + get { return _tlbid; } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("BaseInterface")] + public string XmlBaseInterface + { + get { return _baseInterface; } + set { _baseInterface = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Iid")] + public string XmlIID + { + get { return _iid; } + set { _iid = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Name")] + public string XmlName + { + get { return _name; } + set { _name = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("NumMethods")] + public string XmlNumMethods + { + get { return _numMethods; } + set { _numMethods = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Tlbid")] + public string XmlTlbId + { + get { return _tlbid; } + set { _tlbid = value; } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/FileReferenceCollection.cs b/src/XMakeTasks/ManifestUtil/FileReferenceCollection.cs new file mode 100644 index 00000000000..c21c81cdc6e --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/FileReferenceCollection.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Provides a collection for manifest file references. + /// + [ComVisible(false)] + public sealed class FileReferenceCollection : IEnumerable + { + private ArrayList _list = new ArrayList(); + + internal FileReferenceCollection(FileReference[] array) + { + if (array == null) + return; + _list.AddRange(array); + } + + /// + /// Gets the element at the specified index. + /// + /// The zero-based index of the entry to get. + /// The file reference instance. + public FileReference this[int index] + { + get { return (FileReference)_list[index]; } + } + + /// + /// Adds the specified assembly reference to the collection. + /// + /// The specified file reference to add. + /// The added file reference instance. + public FileReference Add(string path) + { + return Add(new FileReference(path)); + } + + /// + /// Adds the specified assembly reference to the collection. + /// + /// The specified file reference to add. + /// The added file reference instance. + public FileReference Add(FileReference file) + { + _list.Add(file); + return file; + } + + /// + /// Removes all objects from the collection. + /// + public void Clear() + { + _list.Clear(); + } + + /// + /// Gets the number of objects contained in the collection. + /// + public int Count + { + get { return _list.Count; } + } + + /// + /// Finds a file reference in the collection by the specified target path. + /// + /// The specified target path. + /// The found file reference. + public FileReference FindTargetPath(string targetPath) + { + if (String.IsNullOrEmpty(targetPath)) + return null; + foreach (FileReference f in _list) + if (String.Compare(targetPath, f.TargetPath, StringComparison.OrdinalIgnoreCase) == 0) + return f; + return null; + } + + /// + /// Returns an enumerator that can iterate through the collection. + /// + /// The enumerator. + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + /// + /// Removes the specified file reference from the collection. + /// + /// The specified file reference to remove. + public void Remove(FileReference file) + { + _list.Remove(file); + } + + internal FileReference[] ToArray() + { + return (FileReference[])_list.ToArray(typeof(FileReference)); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/Manifest.cs b/src/XMakeTasks/ManifestUtil/Manifest.cs new file mode 100644 index 00000000000..f09a0d417be --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/Manifest.cs @@ -0,0 +1,689 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Xml; +using System.Xml.Serialization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes base functionality common to all supported manifest types. + /// + [ComVisible(false)] + public abstract class Manifest + { + private AssemblyIdentity _assemblyIdentity = null; + private AssemblyReference[] _assemblyReferences = null; + private string _description = null; + private FileReference[] _fileReferences = null; + private string _sourcePath = null; + private Stream _inputStream = null; + private FileReferenceCollection _fileReferenceList = null; + private AssemblyReferenceCollection _assemblyReferenceList = null; + private readonly OutputMessageCollection _outputMessages = new OutputMessageCollection(); + private bool _treatUnfoundNativeAssembliesAsPrerequisites = false; + private bool _readOnly = false; + + protected internal Manifest() // only internal classes can extend this class + { + } + + /// + /// Specifies the identity of the manifest. + /// + [XmlIgnore] + public AssemblyIdentity AssemblyIdentity + { + get + { + if (_assemblyIdentity == null) + _assemblyIdentity = new AssemblyIdentity(); + return _assemblyIdentity; + } + set { _assemblyIdentity = value; } + } + + /// + /// Specifies the set of assemblies referenced by the manifest. + /// + [XmlIgnore] + public AssemblyReferenceCollection AssemblyReferences + { + get + { + if (_assemblyReferenceList == null) + _assemblyReferenceList = new AssemblyReferenceCollection(_assemblyReferences); + return _assemblyReferenceList; + } + } + + private void CollectionToArray() + { + if (_assemblyReferenceList != null) + { + _assemblyReferences = _assemblyReferenceList.ToArray(); + _assemblyReferenceList = null; + } + if (_fileReferenceList != null) + { + _fileReferences = _fileReferenceList.ToArray(); + _fileReferenceList = null; + } + } + + /// + /// Specifies a textual description for the manifest. + /// + [XmlIgnore] + public string Description + { + get { return _description; } + set { _description = value; } + } + + /// + /// Identifies an assembly reference which is the entry point of the application. + /// + [XmlIgnore] + public virtual AssemblyReference EntryPoint + { + get { return null; } + set { } + } + + /// + /// Specifies the set of files referenced by the manifest. + /// + [XmlIgnore] + public FileReferenceCollection FileReferences + { + get + { + if (_fileReferenceList == null) + _fileReferenceList = new FileReferenceCollection(_fileReferences); + return _fileReferenceList; + } + } + + /// + /// The input stream from which the manifest was read. + /// Used by ManifestWriter to reconstitute input which is not represented in the object representation. + /// + [XmlIgnore] + public Stream InputStream + { + get { return _inputStream; } + set { _inputStream = value; } + } + + internal virtual void OnAfterLoad() + { + } + + internal virtual void OnBeforeSave() + { + CollectionToArray(); + SortFiles(); + } + + /// + /// Contains a collection of current error and warning messages. + /// + [XmlIgnore] + public OutputMessageCollection OutputMessages + { + get { return _outputMessages; } + } + + /// + /// Specifies whether the manifest is operating in read-only or read-write mode. + /// If only using to read a manifest then set this flag to true. + /// If using to write a new manifest then set this flag to false. + /// The default is false. + /// This flag provides additional context for the manifest generator, and affects how some error messages are reported. + /// + [XmlIgnore] + public bool ReadOnly + { + get { return _readOnly; } + set { _readOnly = value; } + } + + private bool ResolveAssembly(AssemblyReference a, string[] searchPaths) + { + if (a == null) + return false; + + a.ResolvedPath = ResolvePath(a.SourcePath, searchPaths); + if (!String.IsNullOrEmpty(a.ResolvedPath)) + return true; + + if (a.AssemblyIdentity != null) + { + a.ResolvedPath = a.AssemblyIdentity.Resolve(searchPaths); + if (!String.IsNullOrEmpty(a.ResolvedPath)) + return true; + } + + a.ResolvedPath = ResolvePath(a.TargetPath, searchPaths); + if (!String.IsNullOrEmpty(a.ResolvedPath)) + return true; + + return false; + } + + private bool ResolveFile(BaseReference f, string[] searchPaths) + { + if (f == null) + return false; + + f.ResolvedPath = ResolvePath(f.SourcePath, searchPaths); + if (!String.IsNullOrEmpty(f.ResolvedPath)) + return true; + + f.ResolvedPath = ResolvePath(f.TargetPath, searchPaths); + if (!String.IsNullOrEmpty(f.ResolvedPath)) + return true; + + return false; + } + + /// + /// Locates all specified assembly and file references by searching in the same directory as the loaded manifest, or in the current directory. + /// The location of each referenced assembly and file is required for hash computation and assembly identity resolution. + /// Any resulting errors or warnings are reported in the OutputMessages collection. + /// + public void ResolveFiles() + { + string defaultDir = String.Empty; + if (!String.IsNullOrEmpty(_sourcePath)) + defaultDir = Path.GetDirectoryName(_sourcePath); + if (!Path.IsPathRooted(defaultDir)) + defaultDir = Path.Combine(Environment.CurrentDirectory, defaultDir); + string[] searchPaths = { defaultDir }; + ResolveFiles(searchPaths); + } + + /// + /// Locates all specified assembly and file references by searching in the specified directories. + /// The location of each referenced assembly and file is required for hash computation and assembly identity resolution. + /// Any resulting errors or warnings are reported in the OutputMessages collection. + /// + /// An array of strings specify directories to search. + public void ResolveFiles(string[] searchPaths) + { + if (searchPaths == null) + throw new ArgumentNullException("searchPaths"); + CollectionToArray(); + ResolveFiles_1(searchPaths); + ResolveFiles_2(searchPaths); + } + + private void ResolveFiles_1(string[] searchPaths) + { + if (_assemblyReferences != null) + foreach (AssemblyReference a in _assemblyReferences) + if (!a.IsPrerequisite || a.AssemblyIdentity == null) + if (!ResolveAssembly(a, searchPaths)) + { + if (_treatUnfoundNativeAssembliesAsPrerequisites && a.ReferenceType == AssemblyReferenceType.NativeAssembly) + { + a.IsPrerequisite = true; + } + else + { + // When we're only reading a manifest (i.e. from ResolveNativeReference task), it's + // very useful to report what manifest has the unresolvable reference. However, when + // we're generating a new manifest (i.e. from GenerateApplicationManifest task) + // reporting the manifest is awkward and sometimes looks like a bug. + // So we use the ReadOnly flag to tell the difference between the two cases... + if (_readOnly) + OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadOnlyMode", a.ToString(), this.ToString()); + else + OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadWriteMode", a.ToString()); + } + } + } + + private void ResolveFiles_2(string[] searchPaths) + { + if (_fileReferences != null) + foreach (FileReference f in _fileReferences) + if (!ResolveFile(f, searchPaths)) + { + // When we're only reading a manifest (i.e. from ResolveNativeReference task), it's + // very useful to report what manifest has the unresolvable reference. However, when + // we're generating a new manifest (i.e. from GenerateApplicationManifest task) + // reporting the manifest is awkward and sometimes looks like a bug. + // So we use the ReadOnly flag to tell the difference between the two cases... + if (_readOnly) + OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadOnlyMode", f.ToString(), this.ToString()); + else + OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadWriteMode", f.ToString()); + } + } + + private string ResolvePath(string path, string[] searchPaths) + { + if (String.IsNullOrEmpty(path)) + return null; + if (Path.IsPathRooted(path)) + { + if (File.Exists(path)) + return path; + else + return null; + } + if (searchPaths == null) + return null; + foreach (string searchPath in searchPaths) + if (!String.IsNullOrEmpty(searchPath)) + { + string resolvedPath = Path.Combine(searchPath, path); + resolvedPath = Path.GetFullPath(resolvedPath); + if (File.Exists(resolvedPath)) + return resolvedPath; + } + return null; + } + + private void SortFiles() + { + CollectionToArray(); + ReferenceComparer comparer = new ReferenceComparer(); + if (_assemblyReferences != null) + Array.Sort(_assemblyReferences, comparer); + if (_fileReferences != null) + Array.Sort(_fileReferences, comparer); + } + + /// + /// Specifies the location where the manifest was loaded or saved. + /// + [XmlIgnore] + public string SourcePath + { + get { return _sourcePath; } + set { _sourcePath = value; } + } + + public override string ToString() + { + if (!String.IsNullOrEmpty(_sourcePath)) + return _sourcePath; + else + return AssemblyIdentity.ToString(); + } + + internal bool TreatUnfoundNativeAssembliesAsPrerequisites + { + get { return _treatUnfoundNativeAssembliesAsPrerequisites; } + set { _treatUnfoundNativeAssembliesAsPrerequisites = value; } + } + + internal static void UpdateEntryPoint(string inputPath, string outputPath, string updatedApplicationPath, string applicationManifestPath, string targetFrameworkVersion) + { + XmlDocument document = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(inputPath, xrSettings)) + { + document.Load(xr); + } + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + AssemblyIdentity appManifest = AssemblyIdentity.FromManifest(applicationManifestPath); + + // update path to application manifest + XmlNode codeBaseNode = null; + foreach (string xpath in XPaths.codebasePaths) + { + codeBaseNode = document.SelectSingleNode(xpath, nsmgr); + if (codeBaseNode != null) + break; + } + if (codeBaseNode == null) + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.InvariantCulture, "XPath not found: {0}", XPaths.codebasePaths[0])); + + codeBaseNode.Value = updatedApplicationPath; + + // update Public key token of application manifest + XmlNode publicKeyTokenNode = ((XmlAttribute)codeBaseNode).OwnerElement.SelectSingleNode(XPaths.dependencyPublicKeyTokenAttribute, nsmgr); + if (publicKeyTokenNode == null) + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.InvariantCulture, "XPath not found: {0}", XPaths.dependencyPublicKeyTokenAttribute)); + + publicKeyTokenNode.Value = appManifest.PublicKeyToken; + + // update hash of application manifest + string hash; + long size; + Util.GetFileInfo(applicationManifestPath, targetFrameworkVersion, out hash, out size); + + // Hash node may not be present with optional signing + XmlNode hashNode = ((XmlAttribute)codeBaseNode).OwnerElement.SelectSingleNode(XPaths.hashElement, nsmgr); + if (hashNode != null) + { + ((XmlElement)hashNode).InnerText = hash; + } + + // update file size of application manifest + XmlAttribute sizeAttribute = ((XmlAttribute)codeBaseNode).OwnerElement.Attributes[XmlUtil.TrimPrefix(XPaths.fileSizeAttribute)]; + if (sizeAttribute == null) + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.InvariantCulture, "XPath not found: {0}", XPaths.fileSizeAttribute)); + + sizeAttribute.Value = size.ToString(System.Globalization.CultureInfo.InvariantCulture); + + document.Save(outputPath); + } + + private void UpdateAssemblyReference(AssemblyReference a, string targetFrameworkVersion) + { + if (a.IsVirtual) + return; + + if (a.AssemblyIdentity == null) + switch (a.ReferenceType) + { + case AssemblyReferenceType.ClickOnceManifest: + a.AssemblyIdentity = AssemblyIdentity.FromManifest(a.ResolvedPath); + break; + case AssemblyReferenceType.ManagedAssembly: + a.AssemblyIdentity = AssemblyIdentity.FromManagedAssembly(a.ResolvedPath); + break; + case AssemblyReferenceType.NativeAssembly: + a.AssemblyIdentity = AssemblyIdentity.FromNativeAssembly(a.ResolvedPath); + break; + default: + a.AssemblyIdentity = AssemblyIdentity.FromFile(a.ResolvedPath); + break; + } + if (!a.IsPrerequisite) + UpdateFileReference(a, targetFrameworkVersion); + + // If unspecified assembly type then let's figure out what it actually is... + if (a.ReferenceType == AssemblyReferenceType.Unspecified) + { + // a ClickOnce deployment manifest can only refer to a ClickOnce application manifest... + if (this is DeployManifest) + { + a.ReferenceType = AssemblyReferenceType.ClickOnceManifest; + } + // otherwise it can only be either a managed or a native assembly, but we can only tell if we have the path... + else if (!String.IsNullOrEmpty(a.ResolvedPath)) + { + if (PathUtil.IsNativeAssembly(a.ResolvedPath)) + a.ReferenceType = AssemblyReferenceType.NativeAssembly; + else + a.ReferenceType = AssemblyReferenceType.ManagedAssembly; + } + // there's one other way we can tell, Type="win32" references are always native... + else if (a.AssemblyIdentity != null && String.Equals(a.AssemblyIdentity.Type, "win32", StringComparison.OrdinalIgnoreCase)) + { + a.ReferenceType = AssemblyReferenceType.NativeAssembly; + } + } + } + + private static void UpdateFileReference(BaseReference f, string targetFrameworkVersion) + { + if (String.IsNullOrEmpty(f.ResolvedPath)) + throw new FileNotFoundException(null, f.SourcePath); + string hash; + long size; + + if (string.IsNullOrEmpty(targetFrameworkVersion)) + { + Util.GetFileInfo(f.ResolvedPath, out hash, out size); + } + else + { + Util.GetFileInfo(f.ResolvedPath, targetFrameworkVersion, out hash, out size); + } + f.Hash = hash; + f.Size = size; + if (String.IsNullOrEmpty(f.TargetPath)) + { + if (!String.IsNullOrEmpty(f.SourcePath)) + f.TargetPath = BaseReference.GetDefaultTargetPath(f.SourcePath); + else + f.TargetPath = BaseReference.GetDefaultTargetPath(Path.GetFileName(f.ResolvedPath)); + } + } + + /// + /// Updates file information for each referenced assembly and file. + /// The file information includes a hash computation and a file size for each referenced file and assembly. + /// Also, the assembly identity is obtained for any referenced assemblies with an unspecified assembly identity. + /// Any resulting errors or warnings are reported in the OutputMessages collection. + /// + public void UpdateFileInfo() + { + UpdateFileInfoImpl(null); + } + + public void UpdateFileInfo(string targetFrameworkVersion) + { + UpdateFileInfoImpl(targetFrameworkVersion); + } + + /// + /// Implementation of UpdateFileInfo + /// + /// null, if not TFV. If no TFV, it will use sha256 signature algorithm. + private void UpdateFileInfoImpl(string targetFrameworkVersion) + { + if (_assemblyReferences != null) + foreach (AssemblyReference a in _assemblyReferences) + if (!String.IsNullOrEmpty(a.ResolvedPath)) // only check resolved items... + { + try + { + UpdateAssemblyReference(a, targetFrameworkVersion); + if (a.AssemblyIdentity == null) + { + BadImageFormatException exception = new BadImageFormatException(null, a.ResolvedPath); + OutputMessages.AddErrorMessage("GenerateManifest.General", exception.Message); + } + } + catch (System.Exception e) + { + OutputMessages.AddErrorMessage("GenerateManifest.General", e.Message); + } + } + if (_fileReferences != null) + foreach (FileReference f in _fileReferences) + if (!String.IsNullOrEmpty(f.ResolvedPath)) // only check resolved items... + { + try + { + UpdateFileReference(f, targetFrameworkVersion); + } + catch (System.Exception e) + { + OutputMessages.AddErrorMessage("GenerateManifest.General", e.Message); + } + } + } + + /// + /// Performs various checks to verify the validity of the manifest. + /// Any resulting errors or warnings are reported in the OutputMessages collection. + /// + public virtual void Validate() + { + ValidateReferences(); + } + + private void ValidateReferences() + { + if (AssemblyReferences.Count <= 1) + return; + + Dictionary> identityList = new Dictionary>(); + foreach (AssemblyReference assembly in AssemblyReferences) + { + if (assembly.AssemblyIdentity != null) + { + // Check for two or more assemblies with the same identity... + string identity = assembly.AssemblyIdentity.GetFullName(AssemblyIdentity.FullNameFlags.All); + string key = identity.ToLowerInvariant(); + if (!identityList.ContainsKey(key)) + { + identityList.Add(key, false); + } + else if (identityList[key] == false) + { + OutputMessages.AddWarningMessage("GenerateManifest.DuplicateAssemblyIdentity", identity); + identityList[key] = true; // only warn once per identity + } + } + + // Check that resolved assembly identity matches filename... + if (!assembly.IsPrerequisite) + if (assembly.AssemblyIdentity != null) + if (!String.Equals(assembly.AssemblyIdentity.Name, Path.GetFileNameWithoutExtension(assembly.TargetPath), StringComparison.OrdinalIgnoreCase)) + OutputMessages.AddWarningMessage("GenerateManifest.IdentityFileNameMismatch", assembly.ToString(), assembly.AssemblyIdentity.Name, assembly.AssemblyIdentity.Name + Path.GetExtension(assembly.TargetPath)); + } + } + + + + protected void ValidatePlatform() + { + foreach (AssemblyReference assembly in AssemblyReferences) + if (IsMismatchedPlatform(assembly)) + OutputMessages.AddWarningMessage("GenerateManifest.PlatformMismatch", assembly.ToString()); + } + + + + // Determines whether the platform of the specified assembly reference is mismatched with the applicaion's platform. + private bool IsMismatchedPlatform(AssemblyReference assembly) + { + // Never flag the "Microsoft.CommonLanguageRuntime" dependency as a mismatch... + if (assembly.IsVirtual) + return false; + // Can't tell anything if either of these are not resolved... + if (AssemblyIdentity == null || assembly.AssemblyIdentity == null) + return false; + + if (AssemblyIdentity.IsNeutralPlatform) + { + // If component is a native assembly then it is non-platform neutral by definition, so always flag as a mismatch... + if (assembly.ReferenceType == AssemblyReferenceType.NativeAssembly) + return true; + // Otherwise flag component as a mismatch only if it's not also platform neutral... + return !assembly.AssemblyIdentity.IsNeutralPlatform; + } + else + { + // We want the application platform for the entry point to always match the setting for the whole application, + // but the dependencies do not necessarily have to match... + if (assembly != EntryPoint) + { + // If application IS NOT platform neutral but the component is, then component shouldn't be flagged as a mismatch... + if (assembly.AssemblyIdentity.IsNeutralPlatform) + return false; + } + + // Either we are looking at the entry point assembly or the assembly is not platform neutral. + // We need to compare the application's platform to the component's platform, + // if they don't match then flag component as a mismatch... + return !String.Equals(AssemblyIdentity.ProcessorArchitecture, assembly.AssemblyIdentity.ProcessorArchitecture, StringComparison.OrdinalIgnoreCase); + } + } + + #region " XmlSerializer " + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlElement("AssemblyIdentity")] + public AssemblyIdentity XmlAssemblyIdentity + { + get { return _assemblyIdentity; } + set { _assemblyIdentity = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("AssemblyReferences")] + public AssemblyReference[] XmlAssemblyReferences + { + get { return _assemblyReferences; } + set { _assemblyReferences = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Description")] + public string XmlDescription + { + get { return _description; } + set { _description = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlArray("FileReferences")] + public FileReference[] XmlFileReferences + { + get { return _fileReferences; } + set { _fileReferences = value; } + } + + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlAttribute("Schema")] + public string XmlSchema + { + get { return Util.Schema; } + set { } + } + + #endregion + + + #region " ReferenceComparer " + + private class ReferenceComparer : IComparer + { + public int Compare(object x, object y) + { + if (x == null || y == null) + { + Debug.Fail("Comparing null objects"); + return 0; + } + if (!(x is BaseReference) || !(y is BaseReference)) + { + Debug.Fail("Comparing objects that are not BaseReferences"); + return 0; + } + + BaseReference xRef = x as BaseReference; + BaseReference yRef = y as BaseReference; + + if (xRef.SortName == null || yRef.SortName == null) + { + Debug.Fail("Objects do not have a SortName"); + return 0; + } + + return xRef.SortName.CompareTo(yRef.SortName); + } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ManifestUtil/ManifestFormatter.cs b/src/XMakeTasks/ManifestUtil/ManifestFormatter.cs new file mode 100644 index 00000000000..12cf21a64a7 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ManifestFormatter.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class ManifestFormatter + { + public static Stream Format(Stream input) + { + int t1 = Environment.TickCount; + + XmlTextReader r = new XmlTextReader(input); + r.DtdProcessing = DtdProcessing.Ignore; + r.WhitespaceHandling = WhitespaceHandling.None; + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(r.NameTable); + + MemoryStream m = new MemoryStream(); + XmlTextWriter w = new XmlTextWriter(m, Encoding.UTF8); + w.Formatting = Formatting.Indented; + w.Indentation = 2; + w.WriteStartDocument(); + + while (r.Read()) + { + switch (r.NodeType) + { + case XmlNodeType.Element: + w.WriteStartElement(r.Prefix, r.LocalName, r.NamespaceURI); + if (r.HasAttributes) + { + string elementQName = XmlUtil.GetQName(r, nsmgr); + for (int i = 0; i < r.AttributeCount; ++i) + { + r.MoveToAttribute(i); + string attributeQName = XmlUtil.GetQName(r, nsmgr); + string xpath = elementQName + "/@" + attributeQName; + // Filter out language="*" + if ((xpath.Equals(XPaths.languageAttribute1, StringComparison.Ordinal) || xpath.Equals(XPaths.languageAttribute2, StringComparison.Ordinal)) && String.Equals(r.Value, "*", StringComparison.Ordinal)) + continue; + // Filter out attributes with empty values if attribute is on the list... + if (String.IsNullOrEmpty(r.Value) && Array.BinarySearch(XPaths.emptyAttributeList, xpath) >= 0) + continue; + w.WriteAttributeString(r.Prefix, r.LocalName, r.NamespaceURI, r.Value); + } + + r.MoveToElement(); //Moves the reader back to the element node. + } + + if (r.IsEmptyElement) + w.WriteEndElement(); + + break; + + case XmlNodeType.EndElement: + w.WriteEndElement(); + break; + + case XmlNodeType.Comment: + w.WriteComment(r.Value); + break; + + case XmlNodeType.CDATA: + w.WriteCData(r.Value); + break; + + case XmlNodeType.Text: + w.WriteString(r.Value); + break; + } + } + + w.WriteEndDocument(); + w.Flush(); + m.Position = 0; + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "ManifestWriter.Format t={0}", Environment.TickCount - t1)); + return m; + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/ManifestReader.cs b/src/XMakeTasks/ManifestUtil/ManifestReader.cs new file mode 100644 index 00000000000..a19b3448635 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ManifestReader.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Reads an XML manifest file into an object representation. + /// + [ComVisible(false)] + public static class ManifestReader + { + internal static ComInfo[] GetComInfo(string path) + { + XmlDocument document = GetXmlDocument(path); + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + string manifestFileName = Path.GetFileName(path); + + List comInfoList = new List(); + XmlNodeList comNodes = document.SelectNodes(XPaths.comFilesPath, nsmgr); + foreach (XmlNode comNode in comNodes) + { + XmlNode nameNode = comNode.SelectSingleNode(XPaths.fileNameAttribute, nsmgr); + string componentFileName = nameNode != null ? nameNode.Value : null; + + XmlNodeList clsidNodes = comNode.SelectNodes(XPaths.clsidAttribute, nsmgr); + foreach (XmlNode clsidNode in clsidNodes) + comInfoList.Add(new ComInfo(manifestFileName, componentFileName, clsidNode.Value, null)); + + XmlNodeList tlbidNodes = comNode.SelectNodes(XPaths.tlbidAttribute, nsmgr); + foreach (XmlNode tlbidNode in tlbidNodes) + comInfoList.Add(new ComInfo(manifestFileName, componentFileName, null, tlbidNode.Value)); + } + + return comInfoList.ToArray(); + } + + private static XmlDocument GetXmlDocument(string path) + { + using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + byte[] buffer = new byte[2]; + s.Read(buffer, 0, 2); + s.Position = 0; + XmlDocument document = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + // if first two bytes are "MZ" then we're looking at an .exe or a .dll not a .manifest + if ((buffer[0] == 0x4D) && (buffer[1] == 0x5A)) + { + Stream m = EmbeddedManifestReader.Read(path); + if (m == null) + throw new BadImageFormatException(null, path); + + using (XmlReader xr = XmlReader.Create(m, xrSettings)) + { + document.Load(xr); + } + } + else + { + using (XmlReader xr = XmlReader.Create(s, xrSettings)) + { + document.Load(xr); + } + } + + return document; + } + } + + private static Manifest ReadEmbeddedManifest(string path) + { + Stream m = EmbeddedManifestReader.Read(path); + if (m == null) + return null; + + Util.WriteLogFile(Path.GetFileNameWithoutExtension(path) + ".embedded.xml", m); + Manifest manifest = ReadManifest(m, false); + manifest.SourcePath = path; + return manifest; + } + + /// + /// Reads the specified manifest XML and returns an object representation. + /// + /// The name of the input file. + /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. This option is not honored if the specified input file is an embedded manfiest in a PE. + /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + public static Manifest ReadManifest(string path, bool preserveStream) + { + if (path == null) throw new ArgumentNullException("path"); + string manifestType = null; + if (path.EndsWith(".application", StringComparison.Ordinal)) + manifestType = "DeployManifest"; + else if (path.EndsWith(".exe.manifest", StringComparison.Ordinal)) + manifestType = "ApplicationManifest"; + return ReadManifest(manifestType, path, preserveStream); + } + + /// + /// Reads the specified manifest XML and returns an object representation. + /// + /// Specifies the expected type of the manifest. Valid values are "AssemblyManifest", "ApplicationManifest", or "DepoyManifest". + /// The name of the input file. + /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. This option is not honored if the specified input file is an embedded manfiest in a PE. + /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + public static Manifest ReadManifest(string manifestType, string path, bool preserveStream) + { + Manifest m = null; + using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + byte[] buffer = new byte[2]; + s.Read(buffer, 0, 2); + s.Position = 0; + // if first two bytes are "MZ" then we're looking at an .exe or a .dll not a .manifest + if ((buffer[0] == 0x4D) && (buffer[1] == 0x5A)) + { + m = ReadEmbeddedManifest(path); + } + else + { + m = ReadManifest(manifestType, s, preserveStream); + m.SourcePath = path; + } + } + return m; + } + + /// + /// Reads the specified manifest XML and returns an object representation. + /// + /// Specifies an input stream. + /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. + /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + public static Manifest ReadManifest(Stream input, bool preserveStream) + { + return ReadManifest(null, input, preserveStream); + } + + /// + /// Reads the specified manifest XML and returns an object representation. + /// + /// Specifies the expected type of the manifest. Valid values are "AssemblyManifest", "ApplicationManifest", or "DepoyManifest". + /// Specifies an input stream. + /// Specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation. + /// A base object representation of the manifest. Can be cast to AssemblyManifest, ApplicationManifest, or DeployManifest to access more specific functionality. + public static Manifest ReadManifest(string manifestType, Stream input, bool preserveStream) + { + int t1 = Environment.TickCount; + string resource = "read2.xsl"; + Manifest m = null; + Stream s; + if (manifestType != null) + { + DictionaryEntry arg = new DictionaryEntry("manifest-type", manifestType); + s = XmlUtil.XslTransform(resource, input, arg); + } + else + { + s = XmlUtil.XslTransform(resource, input); + } + + try + { + s.Position = 0; + m = Deserialize(s); + if (m.GetType() == typeof(ApplicationManifest)) + { + ApplicationManifest am = (ApplicationManifest)m; + am.TrustInfo = new TrustInfo(); + am.TrustInfo.ReadManifest(input); + } + if (preserveStream) + { + input.Position = 0; + m.InputStream = new MemoryStream(); + Util.CopyStream(input, m.InputStream); + } + s.Position = 0; + string n = m.AssemblyIdentity.GetFullName(AssemblyIdentity.FullNameFlags.All); + if (String.IsNullOrEmpty(n)) + n = m.GetType().Name; + Util.WriteLogFile(n + ".read.xml", s); + } + finally + { + s.Close(); + } + Util.WriteLog(String.Format(CultureInfo.InvariantCulture, "ManifestReader.ReadManifest t={0}", Environment.TickCount - t1)); + m.OnAfterLoad(); + return m; + } + + private static Manifest Deserialize(string path) + { + Manifest m = null; + + using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + m = Deserialize(s); + } + + return m; + } + + private static Manifest Deserialize(Stream s) + { + s.Position = 0; + XmlTextReader r = new XmlTextReader(s); + r.DtdProcessing = DtdProcessing.Ignore; + + do + r.Read(); + while (r.NodeType != XmlNodeType.Element); + string ns = typeof(Util).Namespace; + string tn = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", ns, r.Name); + Type t = Type.GetType(tn); + s.Position = 0; + + XmlSerializer xs = new XmlSerializer(t); + + int t1 = Environment.TickCount; + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(s, xrSettings)) + { + Manifest m = (Manifest)xs.Deserialize(xr); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "ManifestReader.Deserialize t={0}", Environment.TickCount - t1)); + return m; + } + } + } + + internal class ComInfo + { + private readonly string _componentFileName; + private readonly string _clsid; + private readonly string _manifestFileName; + private readonly string _tlbid; + public ComInfo(string manifestFileName, string componentFileName, string clsid, string tlbid) + { + _componentFileName = componentFileName; + _clsid = clsid; + _manifestFileName = manifestFileName; + _tlbid = tlbid; + } + public string ComponentFileName { get { return _componentFileName; } } + public string ClsId { get { return _clsid; } } + public string ManifestFileName { get { return _manifestFileName; } } + public string TlbId { get { return _tlbid; } } + } +} diff --git a/src/XMakeTasks/ManifestUtil/ManifestWriter.cs b/src/XMakeTasks/ManifestUtil/ManifestWriter.cs new file mode 100644 index 00000000000..07302c947e1 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/ManifestWriter.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml; +using System.Xml.Serialization; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Writes object representation of a manifest to XML. + /// + [ComVisible(false)] + public static class ManifestWriter + { + private static Stream Serialize(Manifest manifest) + { + manifest.OnBeforeSave(); + MemoryStream m = new MemoryStream(); + XmlSerializer s = new XmlSerializer(manifest.GetType()); + StreamWriter w = new StreamWriter(m); + + int t1 = Environment.TickCount; + s.Serialize(w, manifest); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "ManifestWriter.Serialize t={0}", Environment.TickCount - t1)); + + w.Flush(); + m.Position = 0; + return m; + } + + /// + /// Writes the specified object representation of a manifest to XML. + /// The name of the output file is inferred from the SourcePath property of the manifest. + /// + /// The object representation of the manifest. + public static void WriteManifest(Manifest manifest) + { + string path = manifest.SourcePath; + if (path == null) + path = "manifest.xml"; + WriteManifest(manifest, path); + } + + /// + /// Writes the specified object representation of a manifest to XML. + /// + /// The object representation of the manifest. + /// The name of the output file. + public static void WriteManifest(Manifest manifest, string path) + { + using (Stream s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Write)) + { + WriteManifest(manifest, s); + } + } + + + /// + /// Writes the specified object representation of a manifest to XML. + /// + /// The object representation of the manifest. + /// The name of the output file. + /// The target framework version. + public static void WriteManifest(Manifest manifest, string path, string targetframeWorkVersion) + { + using (Stream s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Write)) + { + WriteManifest(manifest, s, targetframeWorkVersion); + } + } + + /// + /// Writes the specified object representation of a manifest to XML. + /// + /// The object representation of the manifest. + /// Specifies an output stream. + public static void WriteManifest(Manifest manifest, Stream output) + { + WriteManifest(manifest, output, null); + } + + /// + /// + /// + /// + /// + /// it will always use sha256 as signature algorithm if TFV is null + private static void WriteManifest(Manifest manifest, Stream output, string targetframeWorkVersion) + { + int t1 = Environment.TickCount; + Stream s1 = Serialize(manifest); + string n = manifest.AssemblyIdentity.GetFullName(AssemblyIdentity.FullNameFlags.All); + if (String.IsNullOrEmpty(n)) + n = manifest.GetType().Name; + Util.WriteLogFile(n + ".write.0-serialized.xml", s1); + + string resource = null; + + if (targetframeWorkVersion == null || targetframeWorkVersion.Length == 0 || Util.CompareFrameworkVersions(targetframeWorkVersion, Constants.TargetFrameworkVersion40) <= 0) + { + resource = "write2.xsl"; + } + else + { + resource = "write3.xsl"; + } + + Stream s2 = null; + + if (manifest.GetType() == typeof(ApplicationManifest)) + { + ApplicationManifest am = (ApplicationManifest)manifest; + if (am.TrustInfo == null) + { + s2 = XmlUtil.XslTransform(resource, s1); + } + else + { + // May throw IO-related exceptions + string temp = FileUtilities.GetTemporaryFile(); + + am.TrustInfo.Write(temp); + if (Util.logging) + { + try + { + File.Copy(temp, Path.Combine(Util.logPath, n + ".trust-file.xml"), true); + } + catch (IOException) + { + } + catch (ArgumentException) + { + } + catch (UnauthorizedAccessException) + { + } + catch (NotSupportedException) + { + } + } + + DictionaryEntry arg = new DictionaryEntry("trust-file", temp); + try + { + s2 = XmlUtil.XslTransform(resource, s1, arg); + } + finally + { + File.Delete(temp); + } + } + } + else + s2 = XmlUtil.XslTransform(resource, s1); + Util.WriteLogFile(n + ".write.1-transformed.xml", s2); + + Stream s3 = null; + if (manifest.InputStream == null) + { + s3 = s2; + } + else + { + string temp = Util.WriteTempFile(manifest.InputStream); + DictionaryEntry arg = new DictionaryEntry("base-file", temp); + try + { + s3 = XmlUtil.XslTransform("merge.xsl", s2, arg); + } + finally + { + File.Delete(temp); + } + Util.WriteLogFile(n + ".write.2-merged.xml", s3); + } + + Stream s4 = ManifestFormatter.Format(s3); + Util.WriteLogFile(n + ".write.3-formatted.xml", s4); + + Util.CopyStream(s4, output); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "ManifestWriter.WriteManifest t={0}", Environment.TickCount - t1)); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/MetadataReader.cs b/src/XMakeTasks/ManifestUtil/MetadataReader.cs new file mode 100644 index 00000000000..98478c4d3ad --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/MetadataReader.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Collections.Specialized; +using Microsoft.Build.Tasks; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal class MetadataReader : IDisposable + { + private readonly string _path = null; + private StringDictionary _attributes = null; + + private IMetaDataDispenser _metaDispenser = null; + private IMetaDataAssemblyImport _assemblyImport = null; + + private static Guid s_importerGuid = GetGuidOfType(typeof(IMetaDataImport)); + private static Guid s_refidGuid = GetGuidOfType(typeof(IReferenceIdentity)); + + private MetadataReader(string path) + { + _path = path; + // Create the metadata dispenser and open scope on the source file. + _metaDispenser = (IMetaDataDispenser)new CorMetaDataDispenser(); + object obj; + int hr = _metaDispenser.OpenScope(path, 0, ref s_importerGuid, out obj); + if (hr == 0) + _assemblyImport = (IMetaDataAssemblyImport)obj; + } + + public static MetadataReader Create(string path) + { + MetadataReader r = new MetadataReader(path); + if (r._assemblyImport != null) + return r; + else + return null; + } + + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "Microsoft.Build.Tasks.IMetaDataImport2.GetCustomAttributeByName(System.UInt32,System.String,System.IntPtr@,System.UInt32@)", Justification = "We verify the valueLen, we don't care what the return value is in this case")] + public bool HasAssemblyAttribute(string name) + { + UInt32 assemblyScope; + _assemblyImport.GetAssemblyFromScope(out assemblyScope); + IMetaDataImport2 import2 = (IMetaDataImport2)_assemblyImport; + IntPtr valuePtr = IntPtr.Zero; + UInt32 valueLen = 0; + import2.GetCustomAttributeByName(assemblyScope, name, out valuePtr, out valueLen); + return valueLen != 0; + } + + public string Name { get { return Attributes["Name"]; } } + public string Version { get { return Attributes["Version"]; } } + public string PublicKeyToken { get { return Attributes["PublicKeyToken"]; } } + public string Culture { get { return Attributes["Culture"]; } } + public string ProcessorArchitecture { get { return Attributes["ProcessorArchitecture"]; } } + + private StringDictionary Attributes + { + get + { + if (_attributes == null) + { + lock (this) + { + if (_attributes == null) + ImportAttributes(); + } + } + + return _attributes; + } + } + + public void Close() + { + if (_assemblyImport != null) + Marshal.ReleaseComObject(_assemblyImport); + if (_metaDispenser != null) + Marshal.ReleaseComObject(_metaDispenser); + _attributes = null; + _metaDispenser = null; + _assemblyImport = null; + } + + private void ImportAttributes() + { + IReferenceIdentity refid = (IReferenceIdentity)NativeMethods.GetAssemblyIdentityFromFile(_path, ref s_refidGuid); + + string name = refid.GetAttribute(null, "name"); + string version = refid.GetAttribute(null, "version"); + string publicKeyToken = refid.GetAttribute(null, "publicKeyToken"); + if (String.Equals(publicKeyToken, "neutral", StringComparison.OrdinalIgnoreCase)) + publicKeyToken = String.Empty; + else if (!String.IsNullOrEmpty(publicKeyToken)) + publicKeyToken = publicKeyToken.ToUpperInvariant(); + string culture = refid.GetAttribute(null, "culture"); + string processorArchitecture = refid.GetAttribute(null, "processorArchitecture"); + if (!String.IsNullOrEmpty(processorArchitecture)) + processorArchitecture = processorArchitecture.ToLowerInvariant(); + + _attributes = new StringDictionary(); + _attributes.Add("Name", name); + _attributes.Add("Version", version); + _attributes.Add("PublicKeyToken", publicKeyToken); + _attributes.Add("Culture", culture); + _attributes.Add("ProcessorArchitecture", processorArchitecture); + + refid = null; + } + + void IDisposable.Dispose() + { + Close(); + } + + private static Guid GetGuidOfType(Type type) + { + GuidAttribute guidAttr = (GuidAttribute)Attribute.GetCustomAttribute(type, typeof(GuidAttribute), false); + return new Guid(guidAttr.Value); + } + + [ComImport] + [Guid("6eaf5ace-7917-4f3c-b129-e046a9704766")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IReferenceIdentity + { + [return: MarshalAs(UnmanagedType.LPWStr)] + string GetAttribute([In, MarshalAs(UnmanagedType.LPWStr)] string Namespace, [In, MarshalAs(UnmanagedType.LPWStr)] string Name); + void SetAttribute(); + void EnumAttributes(); + void Clone(); + } + + [ComImport] + [Guid("809c652e-7396-11d2-9771-00a0c9b4d50c")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [TypeLibType(TypeLibTypeFlags.FRestricted)] + private interface IMetaDataDispenser + { + int DefineScope(); + [PreserveSig] + int OpenScope([In][MarshalAs(UnmanagedType.LPWStr)] string szScope, [In] UInt32 dwOpenFlags, [In] ref Guid riid, [Out][MarshalAs(UnmanagedType.Interface)] out object obj); + int OpenScopeOnMemory(); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/NativeMethods.cs b/src/XMakeTasks/ManifestUtil/NativeMethods.cs new file mode 100644 index 00000000000..759cf8b93fd --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/NativeMethods.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class NativeMethods + { + public const UInt32 LOAD_LIBRARY_AS_DATAFILE = 0x00000002; + public static readonly IntPtr RT_MANIFEST = new IntPtr(24); + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr LoadLibraryExW(string strFileName, IntPtr hFile, UInt32 ulFlags); + [DllImport("Kernel32.dll", SetLastError = true)] + public static extern bool FreeLibrary(IntPtr hModule); + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr FindResource(IntPtr hModule, IntPtr pName, IntPtr pType); + [DllImport("Kernel32.dll", SetLastError = true)] + public static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResource); + [DllImport("Kernel32.dll", SetLastError = true)] + public static extern uint SizeofResource(IntPtr hModule, IntPtr hResource); + [DllImport("Kernel32.dll")] + public static extern IntPtr LockResource(IntPtr hGlobal); + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern int EnumResourceNames(IntPtr hModule, IntPtr pType, EnumResNameProc enumFunc, IntPtr param); + public delegate bool EnumResNameProc(IntPtr hModule, IntPtr pType, IntPtr pName, IntPtr param); + + public enum RegKind + { + RegKind_Default = 0, + RegKind_Register = 1, + RegKind_None = 2 + } + [DllImport("oleaut32.dll", CharSet = CharSet.Unicode, PreserveSig = false)] + public static extern void LoadTypeLibEx(string strTypeLibName, RegKind regKind, [MarshalAs(UnmanagedType.Interface)] out object typeLib); + + [DllImport("sfc.dll", CharSet = CharSet.Unicode, PreserveSig = true)] + public static extern int SfcIsFileProtected(IntPtr RpcHandle, string ProtFileName); + + [DllImport("mscorwks.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] + [return: MarshalAs(UnmanagedType.IUnknown)] + public static extern object GetAssemblyIdentityFromFile([In, MarshalAs(UnmanagedType.LPWStr)] string filePath, [In] ref Guid riid); + } +} diff --git a/src/XMakeTasks/ManifestUtil/OutputMessage.cs b/src/XMakeTasks/ManifestUtil/OutputMessage.cs new file mode 100644 index 00000000000..ce0f6389e9d --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/OutputMessage.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Specifies the type of output message as either an error, warning, or informational. + /// + [ComVisible(false)] + public enum OutputMessageType + { + /// + /// Indicates an informational message. + /// + Info, + /// + /// Indicates a warning. + /// + Warning, + /// + /// Indicates an error. + /// + Error + } + + /// + /// Describes an error, warning, or informational output message for the manifest generator. + /// + [ComVisible(false)] + public sealed class OutputMessage + { + private readonly string[] _arguments; + private readonly string _name; + private readonly string _text; + private readonly OutputMessageType _type; + + internal OutputMessage(OutputMessageType type, string name, string text, params string[] arguments) + { + if (name == null) throw new ArgumentNullException("name"); + if (arguments == null) throw new ArgumentNullException("arguments"); + _type = type; + _name = name; + _arguments = arguments; + _text = text; + } + + /// + /// Returns a string array of arguments for the message. + /// + /// + public string[] GetArguments() { return _arguments; } + + /// + /// Specifies an identifier for the message. + /// + public string Name { get { return _name; } } + + /// + /// Contains the text of the message. + /// + public string Text { get { return _text; } } + + /// + /// Indicates whether the message is an error, warning, or informational message. + /// + public OutputMessageType Type { get { return _type; } } + } + + /// + /// Provides a collection for output messages. + /// + [ComVisible(false)] + public sealed class OutputMessageCollection : IEnumerable + { + private readonly System.Resources.ResourceManager _taskResources = Microsoft.Build.Shared.AssemblyResources.PrimaryResources; + private readonly List _list = new List(); + private int _errorCount = 0; + private int _warningCount = 0; + + internal OutputMessageCollection() + { + } + + /// + /// Gets the element at the specified index. + /// + /// The zero-based index of the entry to get. + /// The file reference instance. + public OutputMessage this[int index] + { + get { return (OutputMessage)_list[index]; } + } + + internal void AddErrorMessage(string taskResourceName, params string[] arguments) + { + ++_errorCount; + string taskText = _taskResources.GetString(taskResourceName); + if (!String.IsNullOrEmpty(taskText)) + taskText = String.Format(CultureInfo.CurrentCulture, taskText, arguments); + _list.Add(new OutputMessage(OutputMessageType.Error, taskResourceName, taskText, arguments)); + } + + internal void AddWarningMessage(string taskResourceName, params string[] arguments) + { + ++_warningCount; + string taskText = _taskResources.GetString(taskResourceName); + if (!String.IsNullOrEmpty(taskText)) + taskText = String.Format(CultureInfo.CurrentCulture, taskText, arguments); + _list.Add(new OutputMessage(OutputMessageType.Warning, taskResourceName, taskText, arguments)); + } + + /// + /// Removes all objects from the collection. + /// + public void Clear() + { + _list.Clear(); + _errorCount = 0; + _warningCount = 0; + } + + /// + /// Gets the number of error messages in the collecction. + /// + public int ErrorCount + { + get + { + return _errorCount; + } + } + + /// + /// Returns an enumerator that can iterate through the collection. + /// + /// The enumerator. + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + internal bool LogTaskMessages(Task task) + { + foreach (OutputMessage message in _list) + { + switch (message.Type) + { + case OutputMessageType.Warning: + task.Log.LogWarningWithCodeFromResources(message.Name, message.GetArguments()); + break; + case OutputMessageType.Error: + task.Log.LogErrorWithCodeFromResources(message.Name, message.GetArguments()); + break; + } + } + return _errorCount <= 0; + } + + /// + /// Gets the number of warning messages in the collecction. + /// + public int WarningCount + { + get + { + return _warningCount; + } + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/PathUtil.cs b/src/XMakeTasks/ManifestUtil/PathUtil.cs new file mode 100644 index 00000000000..21e8fbdde52 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/PathUtil.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class PathUtil + { + public static string CanonicalizePath(string path) + { + if (!String.IsNullOrEmpty(path)) + { + path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + path = path.TrimEnd(Path.DirectorySeparatorChar); + } + return path; + } + + public static string CanonicalizeUrl(string url) + { + Uri uri = new Uri(url); + return uri.AbsoluteUri; + } + + // REVIEW: Can we use System.Uri segments instead + public static string[] GetPathSegments(string path) + { + path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + path = path.TrimEnd(Path.DirectorySeparatorChar); + return path.Split(Path.DirectorySeparatorChar); + } + + //Resolves the path, and if path is a url also canonicalizes it. + public static string Format(string path) + { + if (String.IsNullOrEmpty(path)) return path; + string resolvedPath = Resolve(path); + Uri u = new Uri(resolvedPath); + return u.AbsoluteUri; + } + + public static bool IsAssembly(string path) + { + if (String.IsNullOrEmpty(path)) throw new ArgumentNullException("path"); + if (String.Equals(Path.GetExtension(path), ".application", StringComparison.Ordinal)) return true; + if (String.Equals(Path.GetExtension(path), ".manifest", StringComparison.Ordinal)) return true; + if (!IsProgramFile(path)) return false; // optimization, don't want to sniff every every kind of file -- just dll's or exe's + if (IsManagedAssembly(path)) return true; + if (IsNativeAssembly(path)) return true; + return false; + } + + // This function must be kept in sync with \dd\vb\publish\design\baseprovider\pathutil.vb + public static bool IsDataFile(string path) + { + return path.EndsWith(".mdf", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".mdb", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".ldf", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".sdf", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) + ; + } + + public static bool IsEqualPath(string path1, string path2) + { + return String.Compare(CanonicalizePath(path1), CanonicalizePath(path2), true, System.Globalization.CultureInfo.CurrentCulture) == 0; + } + + public static bool IsLocalPath(string path) + { + Uri u = new Uri(path, UriKind.RelativeOrAbsolute); + if (!u.IsAbsoluteUri) + return true; + return String.IsNullOrEmpty(u.Host); + } + + public static bool IsManagedAssembly(string path) + { + if (String.IsNullOrEmpty(path)) throw new ArgumentNullException("path"); + using (MetadataReader r = MetadataReader.Create(path)) + return r != null; + } + + public static bool IsNativeAssembly(string path) + { + if (String.IsNullOrEmpty(path)) throw new ArgumentNullException("path"); + if (String.Equals(Path.GetExtension(path), ".manifest", StringComparison.Ordinal)) return true; + return EmbeddedManifestReader.Read(path) != null; + } + + public static bool IsPEFile(string path) + { + byte[] buffer = new byte[2]; + using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + s.Read(buffer, 0, 2); + } + + // if first two bytes are "MZ" then we're looking at an .exe or a .dll not a .manifest + return ((buffer[0] == 0x4D) && (buffer[1] == 0x5A)); + } + + public static bool IsProgramFile(string path) + { + return path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + ; + } + + public static bool IsUncPath(string path) + { + Uri u = null; + if (!Uri.TryCreate(path, UriKind.Absolute, out u) || u == null) + return false; + return u.IsUnc; + } + + public static bool IsUrl(string path) + { + Uri u = null; + if (!Uri.TryCreate(path, UriKind.Absolute, out u) || u == null) + return false; + return !u.IsUnc && !String.IsNullOrEmpty(u.Host); + } + + //If path is a url and starts with "localhost", resolves to machine name. + //If path is a relative path, resolves to a full path. + public static string Resolve(string path) + { + if (String.IsNullOrEmpty(path)) return path; + if (IsUncPath(path)) return path; // if it's UNC then do nothing + if (IsUrl(path)) // if it's a URL then need to check for "localhost"... + { + // Replace "localhost" with the actual machine name... + const string LocalHost = "localhost"; + Uri u = new Uri(path); + if (String.Equals(u.Host, LocalHost, StringComparison.OrdinalIgnoreCase)) + { + // Unfortunatly Uri.Host is read-only, so we need to reconstruct it manually... + int i = path.IndexOf(LocalHost, StringComparison.OrdinalIgnoreCase); + return i >= 0 ? path.Substring(0, i) + Environment.MachineName.ToLowerInvariant() + path.Substring(i + LocalHost.Length) : path; + } + else + return path; + } + else // if not unc or url then it must be a local disk path... + return System.IO.Path.GetFullPath(path); // make sure it's a full path + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs b/src/XMakeTasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs new file mode 100644 index 00000000000..3ecbc91f06d --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/RSAPKCS1SHA256SignatureDescription.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +namespace System.Deployment.Internal.CodeSigning +{ + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "RSAPKCS", Justification = "This casing is to match the existing RSAPKCS1SHA1SignatureDescription type")] + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SHA", Justification = "This casing is to match the use of SHA throughout the framework")] + public sealed class RSAPKCS1SHA256SignatureDescription : SignatureDescription + { + public RSAPKCS1SHA256SignatureDescription() + { + KeyAlgorithm = typeof(RSACryptoServiceProvider).FullName; + DigestAlgorithm = typeof(SHA256Cng).FullName; + FormatterAlgorithm = typeof(RSAPKCS1SignatureFormatter).FullName; + DeformatterAlgorithm = typeof(RSAPKCS1SignatureDeformatter).FullName; + } + + public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key) + { + if (key == null) + throw new ArgumentNullException("key"); + + RSAPKCS1SignatureDeformatter deformatter = new RSAPKCS1SignatureDeformatter(key); + deformatter.SetHashAlgorithm("SHA256"); + return deformatter; + } + + public override AsymmetricSignatureFormatter CreateFormatter(AsymmetricAlgorithm key) + { + if (key == null) + throw new ArgumentNullException("key"); + + RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(key); + formatter.SetHashAlgorithm("SHA256"); + return formatter; + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/SecurityUtil.cs b/src/XMakeTasks/ManifestUtil/SecurityUtil.cs new file mode 100644 index 00000000000..abad267e865 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/SecurityUtil.cs @@ -0,0 +1,812 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Win32; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Deployment.Internal.CodeSigning; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Permissions; +using System.Security.Policy; +using System.Text; +using System.Xml; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Provides a set of utility functions for manipulating security permision sets and signing. + /// + [ComVisible(false)] + public static class SecurityUtilities + { + private const string PermissionSetsFolder = "PermissionSets"; + private const string Everything = "Everything"; + private const string LocalIntranet = "LocalIntranet"; + private const string Internet = "Internet"; + private const string Custom = "Custom"; + private const string ToolName = "signtool.exe"; + private const int Fx2MajorVersion = 2; + private const int Fx3MajorVersion = 3; + private static readonly Version s_dotNet40Version = new Version("4.0"); + private static readonly Version s_dotNet45Version = new Version("4.5"); + + private const string InternetPermissionSetXml = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + private const string LocalIntranetPermissionSetXml = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + private const string InternetPermissionSetWithWPFXml = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + private const string LocalIntranetPermissionSetWithWPFXml = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + /// + /// Generates a permission set by computed the zone default permission set and adding any included permissions. + /// + /// Specifies a zone default permission set, which is obtained from machine policy. Valid values are "Internet", "LocalIntranet", or "Custom". If "Custom" is specified, the generated permission set is based only on the includedPermissionSet parameter. + /// A PermissionSet object containing the set of permissions to be explicitly included in the generated permission set. Permissions specified in this parameter will be included verbatim in the generated permission set, regardless of targetZone parameter. + /// This property is no longer used. + /// The generated permission set. + public static PermissionSet ComputeZonePermissionSet(string targetZone, PermissionSet includedPermissionSet, string[] excludedPermissions) + { + return ComputeZonePermissionSetHelper(targetZone, includedPermissionSet, null, string.Empty); + } + + internal static PermissionSet ComputeZonePermissionSetHelper(string targetZone, PermissionSet includedPermissionSet, ITaskItem[] dependencies, string targetFrameworkMoniker) + { + // Custom Set. + if (String.IsNullOrEmpty(targetZone) || String.Equals(targetZone, Custom, StringComparison.OrdinalIgnoreCase)) + { + // just return the included set, no magic + return includedPermissionSet.Copy(); + } + + PermissionSet retSet = GetNamedPermissionSetFromZone(targetZone, dependencies, targetFrameworkMoniker); + + return retSet; + } + + private static PermissionSet GetNamedPermissionSetFromZone(string targetZone, ITaskItem[] dependencies, string targetFrameworkMoniker) + { + switch (targetZone) + { + case LocalIntranet: + return GetNamedPermissionSet(LocalIntranet, dependencies, targetFrameworkMoniker); + case Internet: + return GetNamedPermissionSet(Internet, dependencies, targetFrameworkMoniker); + default: + throw new ArgumentException(String.Empty /* no message */, "targetZone"); + } + } + + private static PermissionSet GetNamedPermissionSet(string targetZone, ITaskItem[] dependencies, string targetFrameworkMoniker) + { + FrameworkNameVersioning fn = null; + + if (!string.IsNullOrEmpty(targetFrameworkMoniker)) + { + fn = new FrameworkNameVersioning(targetFrameworkMoniker); + } + else + { + fn = new FrameworkNameVersioning(".NETFramework", s_dotNet40Version); + } + + int majorVersion = fn.Version.Major; + + if (majorVersion == Fx2MajorVersion) + { + return SecurityUtilities.XmlToPermissionSet((GetXmlElement(targetZone, majorVersion))); + } + else if (majorVersion == Fx3MajorVersion) + { + return SecurityUtilities.XmlToPermissionSet((GetXmlElement(targetZone, majorVersion))); + } + else + { + return SecurityUtilities.XmlToPermissionSet((GetXmlElement(targetZone, fn))); + } + } + + private static XmlElement GetXmlElement(string targetZone, FrameworkNameVersioning fn) + { + IList paths = ToolLocationHelper.GetPathToReferenceAssemblies(fn); + + // Is the targeted CLR even installed? + if (paths.Count > 0) + { + // first one is always framework requested. + string path = Path.Combine(paths[0], PermissionSetsFolder); + + // PermissionSets folder doesn't exit + if (Directory.Exists(path)) + { + string[] files = Directory.GetFiles(path, "*.xml"); + FileInfo[] filesInfo = new FileInfo[files.Length]; + + int indexFound = -1; + + // trim the extension. + for (int i = 0; i < files.Length; i++) + { + filesInfo[i] = new FileInfo(files[i]); + + string fileInfoNoExt = Path.GetFileNameWithoutExtension(files[i]); + + if (string.Equals(fileInfoNoExt, targetZone, StringComparison.OrdinalIgnoreCase)) + { + indexFound = i; + break; + } + } + + if (indexFound != -1) + { + string data = string.Empty; + FileInfo resultFile = filesInfo[indexFound]; + using (FileStream fs = resultFile.OpenRead()) + { + try + { + StreamReader sr = new StreamReader(fs); + data = sr.ReadToEnd(); // fs.Position value will be the length of the stream. + if (!string.IsNullOrEmpty(data)) + { + XmlDocument doc = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + + // http://msdn.microsoft.com/en-us/library/h2344bs2(v=vs.110).aspx + // PermissionSets do not conform to document level, which is the default setting. + xrSettings.ConformanceLevel = ConformanceLevel.Auto; + try + { + fs.Position = 0; // Reset to 0 before using this stream in any other reader. + using (XmlReader xr = XmlReader.Create(fs, xrSettings)) + { + doc.Load(xr); + return (XmlElement)doc.DocumentElement; + } + } + catch (Exception) + { + //continue. + } + } + } + catch (ArgumentException) + { + //continue. + } + } + } + } + } + + return GetCurrentCLRPermissions(targetZone); + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3057: DoNotUseLoadXml.")] + private static XmlElement GetCurrentCLRPermissions(string targetZone) + { + string resultInString = string.Empty; + SecurityZone zone = SecurityZone.NoZone; + switch (targetZone) + { + case LocalIntranet: + zone = SecurityZone.Intranet; + break; + case Internet: + zone = SecurityZone.Internet; + break; + default: + throw new ArgumentException(String.Empty /* no message */, "targetZone"); + } + + Evidence evidence = new Evidence(new EvidenceBase[] { new Zone(zone), new System.Runtime.Hosting.ActivationArguments(new System.ApplicationIdentity("")) }, null); + + PermissionSet sandbox = SecurityManager.GetStandardSandbox(evidence); + resultInString = sandbox.ToString(); + + if (!string.IsNullOrEmpty(resultInString)) + { + XmlDocument doc = new XmlDocument(); + // CA3057: DoNotUseLoadXml. Suppressed since the xml being loaded is a string representation of the PermissionSet. + doc.LoadXml(resultInString); + + return (XmlElement)doc.DocumentElement; + } + + return null; + } + + private static XmlElement GetXmlElement(string targetZone, int majorVersion) + { + XmlDocument doc = null; + + switch (majorVersion) + { + case Fx2MajorVersion: + doc = CreateXmlDocV2(targetZone); + break; + case Fx3MajorVersion: + doc = CreateXmlDocV3(targetZone); + break; + default: + throw new ArgumentException(String.Empty /* no message */, "majorVersion"); + } + + XmlElement rootElement = (XmlElement)doc.DocumentElement; + + if (rootElement == null) + return null; + + return rootElement; + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3057: DoNotUseLoadXml.")] + private static XmlDocument CreateXmlDocV2(string targetZone) + { + XmlDocument doc = new XmlDocument(); + + switch (targetZone) + { + case LocalIntranet: + // CA3057: DoNotUseLoadXml. Suppressed since is LocalIntranetPermissionSetXml a constant string. + doc.LoadXml(LocalIntranetPermissionSetXml); + return doc; + case Internet: + // CA3057: DoNotUseLoadXml. Suppressed since is InternetPermissionSetXml a constant string. + doc.LoadXml(InternetPermissionSetXml); + return doc; + default: + throw new ArgumentException(String.Empty /* no message */, "targetZone"); + } + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3057: DoNotUseLoadXml.")] + private static XmlDocument CreateXmlDocV3(string targetZone) + { + XmlDocument doc = new XmlDocument(); + + switch (targetZone) + { + case LocalIntranet: + // CA3057: DoNotUseLoadXml. Suppressed since is LocalIntranetPermissionSetXml a constant string. + doc.LoadXml(LocalIntranetPermissionSetWithWPFXml); + return doc; + case Internet: + // CA3057: DoNotUseLoadXml. Suppressed since is InternetPermissionSetXml a constant string. + doc.LoadXml(InternetPermissionSetWithWPFXml); + return doc; + default: + throw new ArgumentException(String.Empty /* no message */, "targetZone"); + } + } + + private static string[] GetRegistryPermissionSetByName(string name) + { + string[] extensibleNamedPermissionSetRegistryInfo = null; + RegistryKey localMachineKey = Registry.LocalMachine; + + using (RegistryKey versionIndependentFXKey = localMachineKey.OpenSubKey(@"Software\Microsoft\.NETFramework", false)) + { + if (versionIndependentFXKey != null) + { + using (RegistryKey namedPermissionSetsKey = versionIndependentFXKey.OpenSubKey(@"Security\Policy\Extensions\NamedPermissionSets", false)) + { + if (namedPermissionSetsKey != null) + { + using (RegistryKey permissionSetKey = namedPermissionSetsKey.OpenSubKey(name, false)) + { + if (permissionSetKey != null) + { + string[] permissionKeys = permissionSetKey.GetSubKeyNames(); + extensibleNamedPermissionSetRegistryInfo = new string[permissionKeys.Length]; + for (int i = 0; i < permissionKeys.Length; i++) + { + using (RegistryKey permissionKey = permissionSetKey.OpenSubKey(permissionKeys[i], false)) + { + string permissionXml = permissionKey.GetValue("Xml") as string; + extensibleNamedPermissionSetRegistryInfo[i] = permissionXml; + } + } + } + } + } + } + } + } + return extensibleNamedPermissionSetRegistryInfo; + } + + private static PermissionSet RemoveNonReferencedPermissions(string[] setToFilter, ITaskItem[] dependencies) + { + PermissionSet retSet = new PermissionSet(PermissionState.None); + if (dependencies == null || setToFilter == null || setToFilter.Length == 0) + return retSet; + + List assemblyNameList = new List(); + foreach (ITaskItem dependency in dependencies) + { + AssemblyName dependentAssemblyName = AssemblyName.GetAssemblyName(dependency.ItemSpec); + assemblyNameList.Add(dependentAssemblyName.Name + ", " + dependentAssemblyName.Version.ToString()); + } + SecurityElement retSetElement = retSet.ToXml(); + foreach (string permissionXml in setToFilter) + { + if (!String.IsNullOrEmpty(permissionXml)) + { + string permissionAssemblyName; + string className; + string assemblyVersion; + + SecurityElement permission = SecurityElement.FromString(permissionXml); + + if (!ParseElementForAssemblyIdentification(permission, out className, out permissionAssemblyName, out assemblyVersion)) + continue; + if (assemblyNameList.Contains(permissionAssemblyName + ", " + assemblyVersion)) + { + retSetElement.AddChild(SecurityElement.FromString(permissionXml)); + } + } + } + retSet = new ReadOnlyPermissionSet(retSetElement); + return retSet; + } + + internal static bool ParseElementForAssemblyIdentification(SecurityElement el, + out String className, + out String assemblyName, // for example "WindowsBase" + out String assemblyVersion) + { + className = null; + assemblyName = null; + assemblyVersion = null; + + String fullClassName = el.Attribute("class"); + + if (fullClassName == null) + { + return false; + } + if (fullClassName.IndexOf('\'') >= 0) + { + fullClassName = fullClassName.Replace('\'', '\"'); + } + + int commaIndex = fullClassName.IndexOf(','); + int namespaceClassNameLength; + + // If the classname is tagged with assembly information, find where + // the assembly information begins. + + if (commaIndex == -1) + { + return false; + } + + namespaceClassNameLength = commaIndex; + className = fullClassName.Substring(0, namespaceClassNameLength); + String assemblyFullName = fullClassName.Substring(commaIndex + 1); + AssemblyName an = new AssemblyName(assemblyFullName); + assemblyName = an.Name; + assemblyVersion = an.Version.ToString(); + return true; + } + + + /// + /// Converts an array of permission identity strings to a permission set object. + /// + /// An array of permission identity strings. + /// The converted permission set. + public static PermissionSet IdentityListToPermissionSet(string[] ids) + { + XmlDocument document = new XmlDocument(); + XmlElement permissionSetElement = document.CreateElement("PermissionSet"); + document.AppendChild(permissionSetElement); + foreach (string id in ids) + { + XmlElement permissionElement = document.CreateElement("IPermission"); + XmlAttribute a = document.CreateAttribute("class"); + a.Value = id; + permissionElement.Attributes.Append(a); + permissionSetElement.AppendChild(permissionElement); + } + return XmlToPermissionSet(permissionSetElement); + } + + /// + /// Converts a permission set object to an array of permission identity strings. + /// + /// The input permission set to be converted. + /// An array of permission identity strings. + [SuppressMessage("Microsoft.Security.Xml", "CA3057: DoNotUseLoadXml.")] + public static string[] PermissionSetToIdentityList(PermissionSet permissionSet) + { + string psXml = permissionSet != null ? permissionSet.ToString() : ""; + XmlDocument psDocument = new XmlDocument(); + // CA3057: DoNotUseLoadXml. Suppressed since 'psXml' is a trusted or a constant string. + psDocument.LoadXml(psXml); + return XmlToIdentityList(psDocument.DocumentElement); + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3057: DoNotUseLoadXml.")] + internal static XmlDocument PermissionSetToXml(PermissionSet ps) + { + XmlDocument inputDocument = new XmlDocument(); + string xml = (ps != null) ? ps.ToString() : ""; + + // CA3057: DoNotUseLoadXml. Suppressed since 'xml' is a trusted or a constant string. + inputDocument.LoadXml(xml); + XmlDocument outputDocument = new XmlDocument(); + XmlElement psElement = XmlUtil.CloneElementToDocument(inputDocument.DocumentElement, outputDocument, XmlNamespaces.asmv2); + outputDocument.AppendChild(psElement); + return outputDocument; + } + + private static SecurityElement XmlElementToSecurityElement(XmlElement xe) + { + SecurityElement se = new SecurityElement(xe.Name); + foreach (XmlAttribute xa in xe.Attributes) + se.AddAttribute(xa.Name, xa.Value); + foreach (XmlNode xn in xe.ChildNodes) + if (xn.NodeType == XmlNodeType.Element) + se.AddChild(XmlElementToSecurityElement((XmlElement)xn)); + return se; + } + + private static string[] XmlToIdentityList(XmlElement psElement) + { + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(psElement.OwnerDocument.NameTable); + XmlNodeList nodes = psElement.SelectNodes(XPaths.permissionClassAttributeQuery, nsmgr); + if (nodes == null || nodes.Count == 0) + nodes = psElement.SelectNodes(XmlUtil.TrimPrefix(XPaths.permissionClassAttributeQuery)); + string[] a; + if (nodes != null) + { + a = new string[nodes.Count]; + int i = 0; + foreach (XmlNode node in nodes) + a[i++] = node.Value; + } + else + a = new string[0]; + return a; + } + + /// + /// Converts an XmlElement into a PermissionSet object. + /// + /// An XML representation of the permission set. + /// The converted permission set. + public static PermissionSet XmlToPermissionSet(XmlElement element) + { + if (element == null) + return null; + + SecurityElement se = XmlElementToSecurityElement(element); + if (se == null) + return null; + + PermissionSet ps = new PermissionSet(PermissionState.None); + try + { + ps = new ReadOnlyPermissionSet(se); + } + catch (ArgumentException ex) + { + //UNDONE: Need to log exception thrown from PermissionSet.FromXml + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "PermissionSet.FromXml failed: {0}\r\n\r\n{1}", ex.Message, element.OuterXml)); + return null; + } + return ps; + } + + /// + /// Signs a ClickOnce manifest or PE file. + /// + /// Hexadecimal string that contains the SHA-1 hash of the certificate. + /// URL that specifies an address of a time stamping server. + /// Path of the file to sign with the certificate. + public static void SignFile(string certThumbprint, Uri timestampUrl, string path) + { + SignFile(certThumbprint, timestampUrl, path, null); + } + + /// + /// Signs a ClickOnce manifest or PE file. + /// + /// Hexadecimal string that contains the SHA-1 hash of the certificate. + /// URL that specifies an address of a time stamping server. + /// Path of the file to sign with the certificate. + /// Version of the .NET Framework for the target. + public static void SignFile(string certThumbprint, Uri timestampUrl, string path, string targetFrameworkVersion) + { + System.Resources.ResourceManager resources = new System.Resources.ResourceManager("Microsoft.Build.Tasks.Deployment.ManifestUtilities.Strings", typeof(SecurityUtilities).Module.Assembly); + + if (String.IsNullOrEmpty(certThumbprint)) + throw new ArgumentNullException("certThumbprint"); + + X509Certificate2 cert = GetCert(certThumbprint); + + if (cert == null) + throw new ArgumentException(resources.GetString("CertNotInStore"), "certThumbprint"); + + if (!String.IsNullOrEmpty(targetFrameworkVersion)) + { + Version targetVersion = Util.GetTargetFrameworkVersion(targetFrameworkVersion); + + if (targetVersion == null) + throw new ArgumentException("TargetFrameworkVersion"); + + // SHA-256 digest can be parsed only with .NET 4.5 or higher. + bool isTargetFrameworkSha256Supported = targetVersion.CompareTo(s_dotNet45Version) >= 0; + SignFileInternal(cert, timestampUrl, path, isTargetFrameworkSha256Supported, resources); + } + else + { + SignFile(cert, timestampUrl, path); + } + } + + // We need to refactor these functions to handle real sign tool + /// + /// Signs a ClickOnce manifest. + /// + /// The certificate to be used to sign the file. + /// The certificate password. + /// URL that specifies an address of a time stamping server. + /// Path of the file to sign with the certificate. + /// This function is only for signing a manifest, not a PE file. + public static void SignFile(string certPath, SecureString certPassword, Uri timestampUrl, string path) + { + X509Certificate2 cert = new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.PersistKeySet); + SignFile(cert, timestampUrl, path); + } + + private static bool UseSha256Algorithm(X509Certificate2 cert) + { + Oid oid = cert.SignatureAlgorithm; + return string.Equals(oid.FriendlyName, "sha256RSA", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Signs a ClickOnce manifest or PE file. + /// + /// The certificate to be used to sign the file. + /// URL that specifies an address of a time stamping server. + /// Path of the file to sign with the certificate. + /// This function can only sign a PE file if the X509Certificate2 parameter represents a certificate in the + /// current user's personal certificate store. + public static void SignFile(X509Certificate2 cert, Uri timestampUrl, string path) + { + // setup resources + System.Resources.ResourceManager resources = new System.Resources.ResourceManager("Microsoft.Build.Tasks.Deployment.ManifestUtilities.Strings", typeof(SecurityUtilities).Module.Assembly); + SignFileInternal(cert, timestampUrl, path, true, resources); + } + + private static void SignFileInternal(X509Certificate2 cert, Uri timestampUrl, string path, bool targetFrameworkSupportsSha256, System.Resources.ResourceManager resources) + { + if (cert == null) + throw new ArgumentNullException("cert"); + + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException("path"); + + if (!File.Exists(path)) + throw new FileNotFoundException(String.Format(CultureInfo.InvariantCulture, resources.GetString("SecurityUtil.SignTargetNotFound"), path), path); + + bool useSha256 = UseSha256Algorithm(cert) && targetFrameworkSupportsSha256; + + if (PathUtil.IsPEFile(path)) + { + if (IsCertInStore(cert)) + SignPEFile(cert, timestampUrl, path, resources, useSha256); + else + throw new InvalidOperationException(resources.GetString("SignFile.CertNotInStore")); + } + else + { + if (cert.PrivateKey.GetType() != typeof(RSACryptoServiceProvider)) + throw new ApplicationException(resources.GetString("SecurityUtil.OnlyRSACertsAreAllowed")); + try + { + XmlDocument doc = new XmlDocument(); + doc.PreserveWhitespace = true; + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(path, xrSettings)) + { + doc.Load(xr); + } + SignedCmiManifest2 manifest = new SignedCmiManifest2(doc, useSha256); + RSACryptoServiceProvider csp; + + if (useSha256) + { + csp = SignedCmiManifest2.GetFixedRSACryptoServiceProvider(cert.PrivateKey as RSACryptoServiceProvider, useSha256); + } + else + { + csp = cert.PrivateKey as RSACryptoServiceProvider; + } + + CmiManifestSigner2 signer = new CmiManifestSigner2(csp, cert, useSha256); + if (timestampUrl == null) + manifest.Sign(signer); + else + manifest.Sign(signer, timestampUrl.ToString()); + doc.Save(path); + } + catch (Exception ex) + { + int exceptionHR = System.Runtime.InteropServices.Marshal.GetHRForException(ex); + if (exceptionHR == -2147012889 || exceptionHR == -2147012867) + { + throw new ApplicationException(resources.GetString("SecurityUtil.TimestampUrlNotFound"), ex); + } + throw new ApplicationException(ex.Message, ex); + } + } + } + + + private static void SignPEFile(X509Certificate2 cert, System.Uri timestampUrl, string path, System.Resources.ResourceManager resources, bool useSha256) + { + if (GetPathToTool() == null) throw new ApplicationException(resources.GetString("SecurityUtil.SigntoolNotFound")); + + ProcessStartInfo startInfo = new ProcessStartInfo(GetPathToTool(), GetCommandLineParameters(cert.Thumbprint, timestampUrl, path, useSha256)); + startInfo.CreateNoWindow = true; + startInfo.UseShellExecute = false; + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + + Process signTool = null; + + try + { + signTool = Process.Start(startInfo); + signTool.WaitForExit(); + + while (!signTool.HasExited) + { + System.Threading.Thread.Sleep(50); + } + switch (signTool.ExitCode) + { + case 0: + // everything was fine + break; + case 1: + // error, report it + throw new ApplicationException(String.Format(CultureInfo.InvariantCulture, resources.GetString("SecurityUtil.SigntoolFail"), path, signTool.StandardError.ReadToEnd())); + case 2: + // warning, report it + throw new WarningException(String.Format(CultureInfo.InvariantCulture, resources.GetString("SecurityUtil.SigntoolWarning"), path, signTool.StandardError.ReadToEnd())); + default: + // treat as error + throw new ApplicationException(String.Format(CultureInfo.InvariantCulture, resources.GetString("SecurityUtil.SigntoolFail"), path, signTool.StandardError.ReadToEnd())); + } + } + finally + { + if (signTool != null) + signTool.Close(); + } + } + + internal static string GetCommandLineParameters(string certThumbprint, Uri timestampUrl, string path, bool useSha256) + { + StringBuilder commandLine = new StringBuilder(); + if (useSha256) + commandLine.Append(String.Format(CultureInfo.InvariantCulture, "sign /fd sha256 /sha1 {0} ", certThumbprint)); + else + { + commandLine.Append(String.Format(CultureInfo.InvariantCulture, "sign /sha1 {0} ", certThumbprint)); + } + if (timestampUrl != null) + commandLine.Append(String.Format(CultureInfo.InvariantCulture, "/t {0} ", timestampUrl.ToString())); + commandLine.Append(string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path)); + return commandLine.ToString(); + } + + internal static string GetPathToTool() + { +#pragma warning disable 618 // Disabling warning on using internal ToolLocationHelper API. At some point we should migrate this. + string toolPath = ToolLocationHelper.GetPathToWindowsSdkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest, VisualStudioVersion.VersionLatest); + if (toolPath == null) + toolPath = ToolLocationHelper.GetPathToWindowsSdkFile(ToolName, TargetDotNetFrameworkVersion.Version45, VisualStudioVersion.Version110); + if (toolPath == null) + toolPath = Path.Combine(ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version40, VisualStudioVersion.Version100), "bin", ToolName); + if (toolPath == null) + toolPath = Path.Combine(Environment.CurrentDirectory, ToolName); + if (!File.Exists(toolPath)) + toolPath = null; + return toolPath; +#pragma warning restore 618 + } + + internal static X509Certificate2 GetCert(string thumbprint) + { + X509Store personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + personalStore.Open(OpenFlags.ReadOnly); + X509Certificate2Collection foundCerts = personalStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (foundCerts.Count == 1) + { + return foundCerts[0]; + } + } + finally + { + personalStore.Close(); + } + return null; + } + + private static bool IsCertInStore(X509Certificate2 cert) + { + X509Store personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + personalStore.Open(OpenFlags.ReadOnly); + X509Certificate2Collection foundCerts = personalStore.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false); + if (foundCerts.Count == 1) + return true; + } + finally + { + personalStore.Close(); + } + return false; + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/Strings.resx b/src/XMakeTasks/ManifestUtil/Strings.resx new file mode 100644 index 00000000000..ff8466f9ce0 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/Strings.resx @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Out of process servers are not supported + + + Registry key '{0}' is missing value '{1}'. + + + No registered classes were detected for this component. + + + Components under system file protection should not be isolated. + + + Could not load type library. + + + Registry key '{0}' was not imported. + + + Registry value '{0}' was not imported. + + + The file to be signed {0} could not be found. + + + Failed to sign {0}. {1} + + + Warning while signing {0}. {1} + + + SignTool.exe not found. + + + Timestamp URL server name or address could not be resolved. + + + Only certificates using RSA encryption are valid for signing ClickOnce manifests. + + + + UAC Manifest Options + If you want to change the Windows User Account Control level replace the + requestedExecutionLevel node with one of the following. + + <requestedExecutionLevel level="asInvoker" uiAccess="false" /> + <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> + <requestedExecutionLevel level="highestAvailable" uiAccess="false" /> + + If you want to utilize File and Registry Virtualization for backward + compatibility then delete the requestedExecutionLevel node. + + + \ No newline at end of file diff --git a/src/XMakeTasks/ManifestUtil/TrustInfo.cs b/src/XMakeTasks/ManifestUtil/TrustInfo.cs new file mode 100644 index 00000000000..ad3c24bf402 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/TrustInfo.cs @@ -0,0 +1,733 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Xml; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + /// + /// Describes the application security trust information. + /// + [ComVisible(false)] + public sealed class TrustInfo + { + private PermissionSet _inputPermissionSet = null; + private XmlDocument _inputTrustInfoDocument = null; + private bool _isFullTrust = true; + private PermissionSet _outputPermissionSet = null; + private bool _preserveFullTrustPermissionSet = false; + private string _sameSiteSetting = "site"; + private bool _sameSiteChanged = false; + + /// + /// Initializes a new instance of the TrustInfo class. + /// + public TrustInfo() + { + } + + private void AddSameSiteAttribute(XmlElement permissionSetElement) + { + XmlAttribute sameSiteAttribute = (XmlAttribute)permissionSetElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.sameSiteAttribute)); + if (sameSiteAttribute == null) + { + sameSiteAttribute = permissionSetElement.OwnerDocument.CreateAttribute(XmlUtil.TrimPrefix(XPaths.sameSiteAttribute)); + permissionSetElement.Attributes.Append(sameSiteAttribute); + } + + sameSiteAttribute.Value = _sameSiteSetting; + } + + /// + /// Resets the object to its default state. + /// + public void Clear() + { + _inputPermissionSet = null; + _inputTrustInfoDocument = null; + _isFullTrust = true; + _outputPermissionSet = null; + } + + private void FixupPermissionSetElement(XmlElement permissionSetElement) + { + XmlDocument document = permissionSetElement.OwnerDocument; + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + + if (_preserveFullTrustPermissionSet) + { + XmlAttribute unrestrictedAttribute = (XmlAttribute)permissionSetElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + if (_isFullTrust) + { + if (unrestrictedAttribute == null) + { + unrestrictedAttribute = document.CreateAttribute(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + permissionSetElement.Attributes.Append(unrestrictedAttribute); + } + unrestrictedAttribute.Value = "true"; + } + else + { + if (unrestrictedAttribute != null) + permissionSetElement.Attributes.RemoveNamedItem(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + } + } + else + { + if (_isFullTrust) + { + XmlAttribute unrestrictedAttribute = (XmlAttribute)permissionSetElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + if (unrestrictedAttribute == null) + { + unrestrictedAttribute = document.CreateAttribute(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + permissionSetElement.Attributes.Append(unrestrictedAttribute); + } + unrestrictedAttribute.Value = "true"; + while (permissionSetElement.FirstChild != null) + permissionSetElement.RemoveChild(permissionSetElement.FirstChild); + } + } + + // Add ID="Custom" attribute if there's not one already + XmlAttribute idAttribute = (XmlAttribute)permissionSetElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.idAttribute)); + if (idAttribute == null) + { + idAttribute = document.CreateAttribute(XmlUtil.TrimPrefix(XPaths.idAttribute)); + permissionSetElement.Attributes.Append(idAttribute); + } + + if (String.IsNullOrEmpty(idAttribute.Value)) + idAttribute.Value = "Custom"; + + AddSameSiteAttribute(permissionSetElement); + + if (permissionSetElement.ParentNode == null || permissionSetElement.ParentNode.NodeType == XmlNodeType.Document) + return; + + XmlAttribute idrefAttribute = null; + XmlElement defaultAssemblyRequestElement = (XmlElement)permissionSetElement.ParentNode.SelectSingleNode(XPaths.defaultAssemblyRequestElement, nsmgr); + if (defaultAssemblyRequestElement == null) + { + defaultAssemblyRequestElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.defaultAssemblyRequestElement), XmlNamespaces.asmv2); + permissionSetElement.ParentNode.AppendChild(defaultAssemblyRequestElement); + } + idrefAttribute = (XmlAttribute)permissionSetElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.permissionSetReferenceAttribute)); + if (idrefAttribute == null) + { + idrefAttribute = document.CreateAttribute(XmlUtil.TrimPrefix(XPaths.permissionSetReferenceAttribute)); + defaultAssemblyRequestElement.Attributes.Append(idrefAttribute); + } + + if (String.Compare(idAttribute.Value, idrefAttribute.Value, StringComparison.Ordinal) != 0) + idrefAttribute.Value = idAttribute.Value; + } + + private PermissionSet GetInputPermissionSet() + { + if (_inputPermissionSet == null) + { + XmlElement psElement = GetInputPermissionSetElement(); + if (_preserveFullTrustPermissionSet) + { + XmlAttribute unrestrictedAttribute = (XmlAttribute)psElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + _isFullTrust = unrestrictedAttribute != null && Boolean.Parse(unrestrictedAttribute.Value); + if (_isFullTrust) + { + XmlDocument document = new XmlDocument(); + document.AppendChild(document.ImportNode(psElement, true)); + document.DocumentElement.Attributes.RemoveNamedItem(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + psElement = document.DocumentElement; + } + _inputPermissionSet = SecurityUtilities.XmlToPermissionSet(psElement); + } + else + { + _inputPermissionSet = SecurityUtilities.XmlToPermissionSet(psElement); + _isFullTrust = _inputPermissionSet.IsUnrestricted(); + } + } + return _inputPermissionSet; + } + + private XmlElement GetInputPermissionSetElement() + { + if (_inputTrustInfoDocument == null) + { + _inputTrustInfoDocument = new XmlDocument(); + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(_inputTrustInfoDocument.NameTable); + XmlElement trustInfoElement = _inputTrustInfoDocument.CreateElement(XmlUtil.TrimPrefix(XPaths.trustInfoElement), XmlNamespaces.asmv2); + _inputTrustInfoDocument.AppendChild(trustInfoElement); + } + return GetPermissionSetElement(_inputTrustInfoDocument); + } + + private XmlElement GetInputRequestedPrivilegeElement() + { + if (_inputTrustInfoDocument == null) + return null; + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(_inputTrustInfoDocument.NameTable); + XmlElement trustInfoElement = _inputTrustInfoDocument.DocumentElement; + if (trustInfoElement == null) + return null; + XmlElement securityElement = (XmlElement)trustInfoElement.SelectSingleNode(XPaths.securityElement, nsmgr); + if (securityElement == null) + return null; + XmlElement requestedPrivilegeElement = (XmlElement)securityElement.SelectSingleNode(XPaths.requestedPrivilegeElement, nsmgr); + if (requestedPrivilegeElement == null) + return null; + return requestedPrivilegeElement; + } + + private XmlElement GetRequestedPrivilegeElement(XmlElement inputRequestedPrivilegeElement, XmlDocument document) + { + // + // + // + // + + + // we always create a requestedPrivilege node to put into the generated TrustInfo document + // + XmlElement requestedPrivilegeElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.requestedPrivilegeElement), XmlNamespaces.asmv3); + document.AppendChild(requestedPrivilegeElement); + + // our three cases we need to handle are: + // (a) no requestedPrivilege node (and therefore no requestedExecutionLevel node as well) - use default values + // (b) requestedPrivilege node and no requestedExecutionLevel node - omit the requestedExecutionLevel node + // (c) requestedPrivilege node and requestedExecutionLevel node - use the incoming requestedExecutionLevel node values + // + // using null for both values is case (b) above -- do not output values + // + string executionLevelString = null; + string executionUIAccessString = null; + string commentString = null; + + // case (a) above -- load default values + // + if (inputRequestedPrivilegeElement == null) + { + // If UAC requestedPrivilege node is missing (possibly due to upgraded project) then automatically + // add a default UAC requestedPrivilege node with a default requestedExecutionLevel node set to + // the expected ClickOnce level (asInvoker) with uiAccess as false + // + executionLevelString = Constants.UACAsInvoker; + executionUIAccessString = Constants.UACUIAccess; + + // load up a default comment string that we put in front of the requestedExecutionLevel node + // here so we can allow the passed-in node to override it if there is a comment present + // + System.Resources.ResourceManager resources = new System.Resources.ResourceManager("Microsoft.Build.Tasks.Deployment.ManifestUtilities.Strings", typeof(SecurityUtilities).Module.Assembly); + commentString = resources.GetString("TrustInfo.RequestedExecutionLevelComment"); ; + } + else + { + // we need to see if the requestedExecutionLevel node is present to decide whether or not to create one. + // + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + XmlElement inputRequestedExecutionLevel = (XmlElement)inputRequestedPrivilegeElement.SelectSingleNode(XPaths.requestedExecutionLevelElement, nsmgr); + + // case (c) above -- use incoming values [note that we should do nothing for case (b) above + // because the default values will make us not emit the requestedExecutionLevel and comment] + // + if (inputRequestedExecutionLevel != null) + { + XmlNode previousNode = inputRequestedExecutionLevel.PreviousSibling; + + // fetch the current comment node if there is one (if there is not one, simply + // keep the default null value which means we will not create one in the + // output document) + // + if (previousNode != null && previousNode.NodeType == XmlNodeType.Comment) + { + commentString = ((XmlComment)previousNode).Data; + } + + // fetch the current requestedExecutionLevel node's level attribute if there is one + // + if (inputRequestedExecutionLevel.HasAttribute("level")) + { + executionLevelString = inputRequestedExecutionLevel.GetAttribute("level"); + } + + // fetch the current requestedExecutionLevel node's uiAccess attribute if there is one + // + if (inputRequestedExecutionLevel.HasAttribute("uiAccess")) + { + executionUIAccessString = inputRequestedExecutionLevel.GetAttribute("uiAccess"); + } + } + } + + if (commentString != null) + { + XmlComment requestedPrivilegeComment = document.CreateComment(commentString); + requestedPrivilegeElement.AppendChild(requestedPrivilegeComment); + } + + if (executionLevelString != null) + { + XmlElement requestedExecutionLevelElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.requestedExecutionLevelElement), XmlNamespaces.asmv3); + requestedPrivilegeElement.AppendChild(requestedExecutionLevelElement); + + XmlAttribute levelAttribute = document.CreateAttribute("level"); + levelAttribute.Value = executionLevelString; + requestedExecutionLevelElement.Attributes.Append(levelAttribute); + + if (executionUIAccessString != null) + { + XmlAttribute uiAccessAttribute = document.CreateAttribute("uiAccess"); + uiAccessAttribute.Value = executionUIAccessString; + requestedExecutionLevelElement.Attributes.Append(uiAccessAttribute); + } + } + + return requestedPrivilegeElement; + } + + // Returns permission set sub-element, creating a full-trust permission-set if one doesn't exist + private XmlElement GetPermissionSetElement(XmlDocument document) + { + Debug.Assert(document != null, "GetPermissionSetElement was passed a null document"); + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + XmlElement trustInfoElement = document.DocumentElement; + XmlElement securityElement = (XmlElement)trustInfoElement.SelectSingleNode(XPaths.securityElement, nsmgr); + if (securityElement == null) + { + securityElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.securityElement), XmlNamespaces.asmv2); + trustInfoElement.AppendChild(securityElement); + } + XmlElement applicationRequestMinimumElement = (XmlElement)securityElement.SelectSingleNode(XPaths.applicationRequestMinimumElement, nsmgr); + if (applicationRequestMinimumElement == null) + { + applicationRequestMinimumElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.applicationRequestMinimumElement), XmlNamespaces.asmv2); + securityElement.AppendChild(applicationRequestMinimumElement); + } + XmlElement permissionSetElement = (XmlElement)applicationRequestMinimumElement.SelectSingleNode(XPaths.permissionSetElement, nsmgr); + if (permissionSetElement == null) + { + permissionSetElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.permissionSetElement), XmlNamespaces.asmv2); + applicationRequestMinimumElement.AppendChild(permissionSetElement); + XmlAttribute unrestrictedAttribute = document.CreateAttribute(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute), XmlNamespaces.asmv2); + unrestrictedAttribute.Value = _isFullTrust.ToString().ToLowerInvariant(); + permissionSetElement.Attributes.Append(unrestrictedAttribute); + } + return permissionSetElement; + } + + private PermissionSet GetOutputPermissionSet() + { + if (_outputPermissionSet == null) + { + // NOTE: outputPermissionSet is now the same as the inputPermissionSet + // we used to maintain a list of loadable vs. non-loadable permissions + // this is now cut. If we get more time we should simplify this further + // so there is only one permission set. + _outputPermissionSet = GetInputPermissionSet(); + } + + return _outputPermissionSet; + } + + // Computes permission set from _outputPermissionSet and _unknownPermissions and returns new document + private XmlDocument GetOutputPermissionSetDocument() + { + PermissionSet outputPermissionSet = GetOutputPermissionSet(); + XmlDocument outputDocument = SecurityUtilities.PermissionSetToXml(outputPermissionSet); + + return outputDocument; + } + + /// + /// Determines whether the application has permission to call unmanaged code. + /// + public bool HasUnmanagedCodePermission + { + get + { + PermissionSet ps = GetOutputPermissionSet(); + if (ps == null) + return false; + PermissionSet ups = new PermissionSet(PermissionState.None); + ups.AddPermission(new SecurityPermission(SecurityPermissionFlag.UnmanagedCode)); + return ps.Intersect(ups) != null; + } + } + + /// + /// Determines whether the application is full trust or partial trust. + /// + public bool IsFullTrust + { + get + { + GetInputPermissionSet(); + return _isFullTrust; + } + set + { + _isFullTrust = value; + } + } + + /// + /// Gets or sets the permission set object for the application trust. + /// + public PermissionSet PermissionSet + { + get + { + return GetOutputPermissionSet(); + } + set + { + // Can't allow null because we use that to track whether we should save or not, pass an empty permission set instead + if (value == null) + throw new ArgumentNullException("PermissionSet cannot be set to null."); + _outputPermissionSet = value; + } + } + + /// + /// Determines whether to preserve partial trust permission when the full trust flag is set. + /// If this option is false with full trust specified, then any permissions defined in the permission set object will be dropped on save. + /// + public bool PreserveFullTrustPermissionSet + { + get { return _preserveFullTrustPermissionSet; } + set { _preserveFullTrustPermissionSet = value; } + } + + /// + /// Reads the application trust from an XML file. + /// + /// The name of the input file. + public void Read(string path) + { + using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + Read(s); + } + } + + /// + /// Reads the application trust from an XML file. + /// + /// Specifies an input stream. + public void Read(Stream input) + { + Read(input, XPaths.trustInfoPath); + } + + private void Read(Stream s, string xpath) + { + Clear(); + XmlDocument document = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(s, xrSettings)) + { + document.Load(xr); + } + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + XmlElement trustInfoElement = (XmlElement)document.SelectSingleNode(xpath, nsmgr); + if (trustInfoElement == null) + return; // no trustInfo element is okay + ReadTrustInfo(trustInfoElement.OuterXml); + } + + /// + /// Reads the application trust from a ClickOnce application manifest. + /// + /// The name of the input file. + public void ReadManifest(string path) + { + using (Stream s = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + ReadManifest(s); + } + } + + /// + /// Reads the application trust from a ClickOnce application manifest. + /// + /// Specifies an input stream. + public void ReadManifest(Stream input) + { + Read(input, XPaths.manifestTrustInfoPath); + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3057: DoNotUseLoadXml.")] + private void ReadTrustInfo(string xml) + { + _inputTrustInfoDocument = new XmlDocument(); + // CA3057: DoNotUseLoadXml. Suppressed since the suggested fix is to use XmlReader. + // XmlReader.Create(string) requires an URI. Whereas the input parameter 'xml' is file content and not a path. + _inputTrustInfoDocument.LoadXml(xml); + XmlElement psElement = GetInputPermissionSetElement(); + XmlAttribute unrestrictedAttribute = (XmlAttribute)psElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.unrestrictedAttribute)); + _isFullTrust = unrestrictedAttribute != null && Boolean.Parse(unrestrictedAttribute.Value); + XmlAttribute sameSiteAttribute = (XmlAttribute)psElement.Attributes.GetNamedItem(XmlUtil.TrimPrefix(XPaths.sameSiteAttribute)); + if (sameSiteAttribute != null) + _sameSiteSetting = sameSiteAttribute.Value; + } + + /// + /// Describes the level of "same site" access permitted, specifying whether the application has permission to communicate with the server from which it was deployed. + /// + public string SameSiteAccess + { + get { return _sameSiteSetting; } + set + { + _sameSiteSetting = value; + _sameSiteChanged = true; + } + } + + public override string ToString() + { + MemoryStream m = new MemoryStream(); + Write(m); + m.Position = 0; + StreamReader r = new StreamReader(m); + return r.ReadToEnd(); + } + + /// + /// Writes the application trust to an XML file. + /// + /// The name of the output file. + public void Write(string path) + { + using (Stream s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None)) + { + Write(s); + s.Flush(); + } + } + + /// + /// Writes the application trust to an XML file. + /// + /// + public void Write(Stream output) + { + XmlDocument outputDocument = new XmlDocument(); + XmlElement inputPermissionSetElement = GetInputPermissionSetElement(); + + //NOTE: XmlDocument.ImportNode munges "xmlns:asmv2" to "xmlns:d1p1" for some reason, use XmlUtil.CloneElementToDocument instead + XmlElement outputPermissionSetElement = XmlUtil.CloneElementToDocument(inputPermissionSetElement, outputDocument, XmlNamespaces.asmv2); + outputDocument.AppendChild(outputPermissionSetElement); + + string tempPrivilegeDocument = null; + + XmlDocument privilegeDocument = new XmlDocument(); + XmlElement inputRequestedPrivilegeElement = GetInputRequestedPrivilegeElement(); + XmlElement requestedPrivilegeElement = null; + + requestedPrivilegeElement = GetRequestedPrivilegeElement(inputRequestedPrivilegeElement, privilegeDocument); + + if (requestedPrivilegeElement != null) + { + privilegeDocument.AppendChild(requestedPrivilegeElement); + + MemoryStream p = new MemoryStream(); + privilegeDocument.Save(p); + p.Position = 0; + tempPrivilegeDocument = Util.WriteTempFile(p); + } + + try + { + string trustInfoResource2 = "trustinfo2.xsl"; + + // If permission set was not altered, just write out what was read in... + MemoryStream m = new MemoryStream(); + if (_outputPermissionSet == null && !_sameSiteChanged) + { + XmlElement permissionSetElement = outputDocument.DocumentElement; + FixupPermissionSetElement(permissionSetElement); + + outputDocument.Save(m); + m.Position = 0; + } + else + { + XmlDocument permissionSetDocument = GetOutputPermissionSetDocument(); + XmlElement permissionSetElement = permissionSetDocument.DocumentElement; + FixupPermissionSetElement(permissionSetElement); + + if (outputDocument.DocumentElement == null) + { + permissionSetDocument.Save(m); + m.Position = 0; + } + else + { + XmlElement oldPermissionSetElement = outputDocument.DocumentElement; + XmlElement newPermissionSetElement = (XmlElement)outputDocument.ImportNode(permissionSetElement, true); + oldPermissionSetElement.ParentNode.ReplaceChild(newPermissionSetElement, oldPermissionSetElement); + + outputDocument.Save(m); + m.Position = 0; + } + } + + // Wrap in a section + Stream s = null; + if (tempPrivilegeDocument != null) + s = XmlUtil.XslTransform(trustInfoResource2, m, new DictionaryEntry("defaultRequestedPrivileges", tempPrivilegeDocument)); + else + s = XmlUtil.XslTransform(trustInfoResource2, m); + Util.CopyStream(s, output); + } + finally + { + if (tempPrivilegeDocument != null) + File.Delete(tempPrivilegeDocument); + } + } + + /// + /// Writes the application trust to a ClickOnce application manifest. + /// If the file exists, the trust section will be updated. + /// If the file does not exist, a new template manifest with the specified trust will be created. + /// + /// The name of the output file. + public void WriteManifest(string path) + { + Stream s = null; + try + { + if (File.Exists(path)) + { + s = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + } + else + { + s = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None); + } + + if (s.Length > 0) + { + // If the file is not empty then we assume that it is already a + // valid manifest and that we are just modifying the trust info + WriteManifest(s, s); + } + else + { + // If the file is empty we need to start with a manifest template. + WriteManifest(s); + } + } + finally + { + if (s != null) + { + s.Flush(); + s.Close(); + } + } + } + + /// + /// Writes the application trust to a new template ClickOnce application manifest. + /// + /// Specifies an output stream. + public void WriteManifest(Stream output) + { + string r = "manifest.xml"; + Stream input = Util.GetEmbeddedResourceStream(r); + WriteManifest(input, output); + } + + /// + /// Updates an existing ClickOnce application manifest with the specified trust. + /// + /// Specifies an input stream. + /// Specifies an output stream. + public void WriteManifest(Stream input, Stream output) + { + int t1 = Environment.TickCount; + XmlDocument document = new XmlDocument(); + XmlReaderSettings xrSettings = new XmlReaderSettings(); + xrSettings.DtdProcessing = DtdProcessing.Ignore; + using (XmlReader xr = XmlReader.Create(input, xrSettings)) + { + document.Load(xr); + } + XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable); + XmlElement assemblyElement = (XmlElement)document.SelectSingleNode(XPaths.assemblyElement, nsmgr); + if (assemblyElement == null) + throw new BadImageFormatException(); + + XmlElement trustInfoElement = (XmlElement)assemblyElement.SelectSingleNode(XPaths.trustInfoElement, nsmgr); + if (trustInfoElement == null) + { + trustInfoElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.trustInfoElement), XmlNamespaces.asmv2); + assemblyElement.AppendChild(trustInfoElement); + } + + // If we have an input trustinfo document and no output specified then just copy the input to the output + if (_inputTrustInfoDocument != null && _outputPermissionSet == null && !_sameSiteChanged) + { + XmlElement newTrustInfoElement = (XmlElement)document.ImportNode(_inputTrustInfoDocument.DocumentElement, true); + trustInfoElement.ParentNode.ReplaceChild(newTrustInfoElement, trustInfoElement); + } + else + { + XmlElement securityElement = (XmlElement)trustInfoElement.SelectSingleNode(XPaths.securityElement, nsmgr); + if (securityElement == null) + { + securityElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.securityElement), XmlNamespaces.asmv2); + trustInfoElement.AppendChild(securityElement); + } + XmlElement applicationRequestMinimumElement = (XmlElement)securityElement.SelectSingleNode(XPaths.applicationRequestMinimumElement, nsmgr); + if (applicationRequestMinimumElement == null) + { + applicationRequestMinimumElement = document.CreateElement(XmlUtil.TrimPrefix(XPaths.applicationRequestMinimumElement), XmlNamespaces.asmv2); + securityElement.AppendChild(applicationRequestMinimumElement); + } + + XmlNodeList permissionSetNodes = applicationRequestMinimumElement.SelectNodes(XPaths.permissionSetElement, nsmgr); + foreach (XmlNode permissionSetNode in permissionSetNodes) + applicationRequestMinimumElement.RemoveChild(permissionSetNode); + + XmlDocument permissionSetDocument = GetOutputPermissionSetDocument(); + XmlElement permissionSetElement = (XmlElement)document.ImportNode(permissionSetDocument.DocumentElement, true); + applicationRequestMinimumElement.AppendChild(permissionSetElement); + FixupPermissionSetElement(permissionSetElement); + } + + // Truncate any contents that may be in the file + if (output.Length > 0) + { + output.SetLength(0); + output.Flush(); + } + document.Save(output); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "ManifestWriter.WriteTrustInfo t={0}", Environment.TickCount - t1)); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/Util.cs b/src/XMakeTasks/ManifestUtil/Util.cs new file mode 100644 index 00000000000..d7ce5f4f45e --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/Util.cs @@ -0,0 +1,526 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Win32; +using System; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Xml; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class Util + { + internal static readonly string Schema = Environment.GetEnvironmentVariable("VSPSCHEMA"); + internal static readonly bool logging = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSPLOG")); + internal static readonly string logPath = GetLogPath(); + private static readonly char[] s_fileNameInvalidChars = new char[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' }; + private static StreamWriter s_logFileWriter = null; + // Major, Minor, Build and Revision of CLR v2.0 + private static int[] s_clrVersion2 = new int[] { 2, 0, 50727, 0 }; + + #region " Platform <-> ProcessorArchitecture mapping " + // Note: These two arrays are parallel and must correspond to one another. + private readonly static string[] s_platforms = new string[] + { + "AnyCPU", + "x86", + "x64", + "Itanium", + "arm" + }; + private readonly static string[] s_processorArchitectures = new string[] + { + "msil", + "x86", + "amd64", + "ia64", + "arm" + }; + #endregion + + public static string ByteArrayToHex(Byte[] a) + { + if (a == null) + return null; + StringBuilder s = new StringBuilder(a.Length); + foreach (Byte b in a) + s.Append(b.ToString("X02", CultureInfo.InvariantCulture)); + return s.ToString(); + } + + public static string ByteArrayToString(Byte[] a) + { + if (a == null) + return null; + StringBuilder s = new StringBuilder(a.Length); + foreach (Byte b in a) + s.Append(Convert.ToChar(b)); + return s.ToString(); + } + + public static int CopyStream(Stream input, Stream output) + { + const int bufferSize = 0x4000; + byte[] buffer = new byte[bufferSize]; + int bytesCopied = 0; + int bytesRead = 0; + do + { + bytesRead = input.Read(buffer, 0, bufferSize); + output.Write(buffer, 0, bytesRead); + bytesCopied += bytesRead; + } while (bytesRead > 0); + output.Flush(); + input.Position = 0; + output.Position = 0; + return bytesCopied; + } + + public static string FilterNonprintableChars(string value) + { + StringBuilder sb = new StringBuilder(value); + int i = 0; + while (i < sb.Length) + if (sb[i] < ' ') + sb.Remove(i, 1); + else + ++i; + return sb.ToString(); + } + + public static string GetAssemblyPath() + { + return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + + public static string GetClrVersion() + { + Version v = System.Environment.Version; + v = new Version(v.Major, v.Minor, v.Build, 0); + return v.ToString(); + } + + /// + /// Return a CLRVersion from a given target framework version. + /// + /// + /// + public static string GetClrVersion(string targetFrameworkVersion) + { + if (string.IsNullOrEmpty(targetFrameworkVersion)) + return GetClrVersion(); + + Version clrVersion = null; + Version currentVersion = System.Environment.Version; + Version frameworkVersion = GetTargetFrameworkVersion(targetFrameworkVersion); + + // for FX 4.0 or above use the current version. + if (frameworkVersion != null && (frameworkVersion.Major >= currentVersion.Major)) + { + clrVersion = new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build, 0); + } + else + { + clrVersion = new Version(s_clrVersion2[0], s_clrVersion2[1], s_clrVersion2[2], s_clrVersion2[3]); + } + return clrVersion.ToString(); + } + + /// + /// Gets a Version object corresponding to the given target framework version string. + /// + public static Version GetTargetFrameworkVersion(string targetFramework) + { + Version frameworkVersion = null; + if (!String.IsNullOrEmpty(targetFramework)) + { + if (targetFramework.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + Version.TryParse(targetFramework.Substring(1), out frameworkVersion); + } + else + { + Version.TryParse(targetFramework, out frameworkVersion); + } + } + return frameworkVersion; + } + + public static string GetEmbeddedResourceString(string name) + { + Stream s = GetEmbeddedResourceStream(name); + StreamReader r = new StreamReader(s); + return r.ReadToEnd(); + } + + public static Stream GetEmbeddedResourceStream(string name) + { + Assembly a = Assembly.GetExecutingAssembly(); + Stream s = a.GetManifestResourceStream(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", typeof(Util).Namespace, name)); + Debug.Assert(s != null, String.Format(CultureInfo.CurrentCulture, "EmbeddedResource '{0}' not found", name)); + return s; + } + + public static void GetFileInfo(string path, out string hash, out long length) + { + GetFileInfoImpl(path, null, out hash, out length); + } + + public static void GetFileInfo(string path, string targetFrameworkVersion, out string hash, out long length) + { + GetFileInfoImpl(path, targetFrameworkVersion, out hash, out length); + } + + [SuppressMessage("Microsoft.Security.Cryptography", "CA5354: SHA1CannotBeUsed.", Justification = ".NET 4.0 and earlier versions cannot parse SHA-2.")] + private static void GetFileInfoImpl(string path, string targetFrameWorkVersion, out string hash, out long length) + { + FileInfo fi = new FileInfo(path); + length = fi.Length; + + Stream s = null; + try + { + s = fi.OpenRead(); + HashAlgorithm hashAlg = null; + + if (targetFrameWorkVersion == null || targetFrameWorkVersion.Length == 0 || Util.CompareFrameworkVersions(targetFrameWorkVersion, Constants.TargetFrameworkVersion40) <= 0) + { + hashAlg = new SHA1CryptoServiceProvider(); + } + else + { + hashAlg = new SHA256CryptoServiceProvider(); + } + byte[] hashBytes = hashAlg.ComputeHash(s); + hash = Convert.ToBase64String(hashBytes); + } + finally + { + if (s != null) + s.Close(); + } + } + + private static string GetLogPath() + { + if (!logging) return null; + string logPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Microsoft\VisualStudio\8.0\VSPLOG"); + if (!Directory.Exists(logPath)) + Directory.CreateDirectory(logPath); + return logPath; + } + + public static string GetRegisteredOrganization() + { + RegistryKey key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", false); + if (key != null) + { + string org = (string)key.GetValue("RegisteredOrganization"); + if (org != null) + org = org.Trim(); + if (!String.IsNullOrEmpty(org)) + return org; + } + return null; + } + + public static bool IsValidAssemblyName(string value) + { + return IsValidFileName(value); + } + + public static bool IsValidCulture(string value) + { + if (String.Equals(value, "neutral", StringComparison.OrdinalIgnoreCase)) + return true; // "neutral" is valid in a manifest but not in CultureInfo class + if (String.Equals(value, "*", StringComparison.OrdinalIgnoreCase)) + return true; // "*" is same as "neutral" + CultureInfo culture; + try + { + culture = new CultureInfo(value); + } + catch (ArgumentException) + { + return false; + } + return true; + } + + public static bool IsValidFileName(string value) + { + return value.IndexOfAny(s_fileNameInvalidChars) < 0; + } + + public static bool IsValidVersion(string value, int octets) + { + Version version; + try + { + version = new Version(value); + } + catch (FormatException) + { + return false; + } + catch (ArgumentOutOfRangeException) + { + return false; + } + catch (ArgumentNullException) + { + return false; + } + catch (ArgumentException) + { + return false; + } + catch (OverflowException) + { + return false; + } + + if (octets >= 1 && version.Major < 0) + return false; + if (octets >= 2 && version.Minor < 0) + return false; + if (octets >= 3 && version.Build < 0) + return false; + if (octets >= 4 && version.Revision < 0) + return false; + + return true; + } + + internal static bool IsValidFrameworkVersion(string value) + { + if (value.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + return IsValidVersion(value.Substring(1), 2); + return IsValidVersion(value, 2); + } + + public static string PlatformToProcessorArchitecture(string platform) + { + for (int i = 0; i < s_platforms.Length; ++i) + if (String.Compare(platform, s_platforms[i], StringComparison.OrdinalIgnoreCase) == 0) + return s_processorArchitectures[i]; + return null; + } + + private static ITaskItem[] RemoveDuplicateItems(ITaskItem[] items) + { + if (items == null) + return null; + if (items.Length <= 1) + return items; + Hashtable list = new Hashtable(); + foreach (ITaskItem item in items) + { + if (String.IsNullOrEmpty(item.ItemSpec)) + continue; + + string key = null; + AssemblyIdentity id = new AssemblyIdentity(item.ItemSpec); + if (id.IsStrongName) + { + key = id.GetFullName(AssemblyIdentity.FullNameFlags.All); + } + else + { + key = Path.GetFullPath(item.ItemSpec).ToUpperInvariant(); + } + + if (!list.Contains(key)) + list.Add(key, item); + } + + ITaskItem[] outputItems = new ITaskItem[list.Count]; + list.Values.CopyTo(outputItems, 0); + return outputItems; + } + + public static ITaskItem[] SortItems(ITaskItem[] items) + { + ITaskItem[] outputItems = RemoveDuplicateItems(items); + if (outputItems != null) + Array.Sort(outputItems, s_itemComparer); + return outputItems; + } + + public static void WriteFile(string path, string s) + { + using (StreamWriter w = new StreamWriter(path)) + { + w.Write(s); + } + } + + public static void WriteFile(string path, Stream s) + { + StreamReader r = new StreamReader(s); + WriteFile(path, r.ReadToEnd()); + } + + public static void WriteLog(string text) + { + if (!logging) + return; + if (s_logFileWriter == null) + try + { + s_logFileWriter = new StreamWriter(Path.Combine(logPath, "Microsoft.Build.Tasks.log"), false); + } + catch (UnauthorizedAccessException) + { + return; + } + catch (ArgumentException) + { + return; + } + catch (IOException) + { + return; + } + catch (SecurityException) + { + return; + } + s_logFileWriter.WriteLine(text); + s_logFileWriter.Flush(); + } + + public static void WriteLogFile(string filename, Stream s) + { + if (!logging) + return; + string path = Path.Combine(logPath, filename); + StreamReader r = new StreamReader(s); + string text = r.ReadToEnd(); + try + { + WriteFile(path, text); + } + catch (UnauthorizedAccessException) + { + } + catch (ArgumentException) + { + } + catch (IOException) + { + } + catch (SecurityException) + { + } + s.Position = 0; + } + + public static void WriteLogFile(string filename, string s) + { + if (!logging) + return; + string path = Path.Combine(logPath, filename); + try + { + WriteFile(path, s); + } + catch (UnauthorizedAccessException) + { + return; + } + catch (ArgumentException) + { + return; + } + catch (IOException) + { + return; + } + catch (SecurityException) + { + return; + } + } + + public static void WriteLogFile(string filename, System.Xml.XmlElement element) + { + if (!logging) + return; + WriteLogFile(filename, element.OuterXml); + } + + public static string WriteTempFile(Stream s) + { + // May throw IO-related exceptions + string path = FileUtilities.GetTemporaryFile(); + + WriteFile(path, s); + return path; + } + + public static string WriteTempFile(string s) + { + // May throw IO-related exceptions + string path = FileUtilities.GetTemporaryFile(); + + WriteFile(path, s); + return path; + } + + #region ItemComparer + private static readonly ItemComparer s_itemComparer = new ItemComparer(); + private class ItemComparer : IComparer + { + int IComparer.Compare(object obj1, object obj2) + { + if (obj1 == null || obj2 == null) + { + Debug.Fail("Comparing null objects"); + return 0; + } + if (!(obj1 is ITaskItem) || !(obj2 is ITaskItem)) + { + Debug.Fail("Comparing objects that are not ITaskItem"); + return 0; + } + ITaskItem item1 = obj1 as ITaskItem; + ITaskItem item2 = obj2 as ITaskItem; + if (item1.ItemSpec == null || item2.ItemSpec == null) + { + Debug.Fail("Objects do not have a ItemSpec"); + return 0; + } + return String.Compare(item1.ItemSpec, item2.ItemSpec, StringComparison.Ordinal); + } + } + #endregion + + + public static Version ConvertFrameworkVersionToString(string version) + { + if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + return new Version(version.Substring(1)); + } + return new Version(version); + } + + public static int CompareFrameworkVersions(string versionA, string versionB) + { + Version version1 = ConvertFrameworkVersionToString(versionA); + Version version2 = ConvertFrameworkVersionToString(versionB); + return version1.CompareTo(version2); + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/XPaths.cs b/src/XMakeTasks/ManifestUtil/XPaths.cs new file mode 100644 index 00000000000..5d6bb20b786 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/XPaths.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class XPaths + { + public const string applicationRequestMinimumElement = "asmv2:applicationRequestMinimum"; + public const string assemblyElement = "asmv1:assembly"; + public const string assemblyIdentityPath = "/asmv1:assembly/asmv1:assemblyIdentity|/asmv1:assembly/asmv2:assemblyIdentity"; + public const string clsidAttribute = "asmv1:comClass/@clsid"; + public const string comFilesPath = "/asmv1:assembly/asmv1:file[asmv1:typelib or asmv1:comClass]"; + public const string configBindingRedirect = "configuration/runtime/asmv1:assemblyBinding/asmv1:dependentAssembly/asmv1:bindingRedirect"; + public const string defaultAssemblyRequestElement = "asmv2:defaultAssemblyRequest"; + public const string dependencyPublicKeyTokenAttribute = "asmv2:assemblyIdentity/@publicKeyToken"; + public const string fileNameAttribute = "@name"; + public const string fileSizeAttribute = "asmv2:size"; + public const string hashElement = "asmv2:hash/dsig:DigestValue"; + public const string idAttribute = "asmv2:ID"; + public const string languageAttribute1 = "asmv1:assemblyIdentity/@language"; + public const string languageAttribute2 = "asmv2:assemblyIdentity/@language"; + public const string manifestTrustInfoPath = "/asmv1:assembly/asmv2:trustInfo"; + public const string permissionIdentityQuery = "asmv2:IPermission[@class='{0}']"; + public const string permissionClassAttributeQuery = "asmv2:IPermission/@class"; + public const string permissionSetElement = "asmv2:PermissionSet"; + public const string permissionSetReferenceAttribute = "asmv2:permissionSetReference"; + public const string publicKeyTokenAttribute = "asmv2:publicKeyToken"; + public const string requestedExecutionLevelPath = "/asmv1:assembly/asmv2:trustInfo/asmv2:security/asmv3:requestedPrivileges/asmv3:requestedExecutionLevel"; + public const string requestedPrivilegeElement = "asmv3:requestedPrivileges"; + public const string requestedExecutionLevelElement = "asmv3:requestedExecutionLevel"; + public const string sameSiteAttribute = "asmv2:SameSite"; + public const string securityElement = "asmv2:security"; + public const string signaturePath = "/asmv1:assembly/dsig:Signature"; + public const string tlbidAttribute = "asmv1:typelib/@tlbid"; + public const string trustInfoElement = "asmv2:trustInfo"; + public const string trustInfoPath = "/asmv2:trustInfo"; + public const string unrestrictedAttribute = "asmv2:Unrestricted"; + + // List of paths where codebase may be found in a manifest. + // Used by Manifest class. + // In order of most likely occurance.... + public static readonly string[] codebasePaths = + { + "/asmv1:assembly/asmv2:dependency/asmv2:dependentAssembly/@codebase", + "/asmv1:assembly/asmv1:dependency/asmv1:dependentAssembly/@asmv2:codebase", + "/asmv1:assembly/asmv1:dependency/asmv2:dependentAssembly/@codebase", + "/asmv1:assembly/asmv2:dependency/asmv1:dependentAssembly/@asmv2:codebase" + }; + + // List of attributes that are to be filtered out if empty. + // Used by ManifestFormatter class. + // These must be defined in sorted order! + public static readonly string[] emptyAttributeList = + { + "asmv1:assemblyIdentity/@language", + "asmv1:assemblyIdentity/@processorArchitecture", + "asmv1:assemblyIdentity/@publicKeyToken", + "asmv1:assemblyIdentity/@type", + "asmv1:comClass/@description", + "asmv1:comClass/@progid", + "asmv1:comClass/@threadingModel", + "asmv1:dependency/@optional", + "asmv1:dependentAssembly/@asmv2:codebase", + "asmv1:dependentAssembly/@asmv2:group", + "asmv1:dependentAssembly/@asmv2:hash", + "asmv1:dependentAssembly/@asmv2:hashalg", + "asmv1:dependentAssembly/@asmv2:optional", + "asmv1:dependentAssembly/@asmv2:resourceFallbackCulture", + "asmv1:dependentAssembly/@asmv2:resourceFallbackCultureInternal", + "asmv1:dependentAssembly/@asmv2:resourceType", + "asmv1:dependentAssembly/@asmv2:size", + "asmv1:description/@asmv2:iconFile", + "asmv1:description/@asmv2:product", + "asmv1:description/@asmv2:publisher", + "asmv1:description/@asmv2:supportUrl", + "asmv1:description/@co.v1:errorReportUrl", + "asmv1:description/@co.v1:suiteName", + "asmv1:file/@asmv2:group", + "asmv1:file/@asmv2:optional", + "asmv1:file/@asmv2:writeableType", + "asmv1:typelib/@flags", + "asmv2:assemblyIdentity/@language", + "asmv2:assemblyIdentity/@processorArchitecture", + "asmv2:assemblyIdentity/@publicKeyToken", + "asmv2:assemblyIdentity/@type", + "asmv2:dependency/@optional", + "asmv2:dependentAssembly/@codebase", + "asmv2:dependentAssembly/@group", + "asmv2:dependentAssembly/@hash", + "asmv2:dependentAssembly/@hashalg", + "asmv2:dependentAssembly/@optional", + "asmv2:dependentAssembly/@resourceFallbackCulture", + "asmv2:dependentAssembly/@resourceFallbackCultureInternal", + "asmv2:dependentAssembly/@resourceType", + "asmv2:dependentAssembly/@size", + "asmv2:dependentOS/@description", + "asmv2:dependentOS/@supportUrl", + "asmv2:deployment/@co.v1:createDesktopShortcut", + "asmv2:deployment/@disallowUrlActivation", + "asmv2:deployment/@install", + "asmv2:deployment/@mapFileExtensions", + "asmv2:deployment/@minimumRequiredVersion", + "asmv2:deployment/@trustURLParameters", + "asmv2:description/@iconFile", + "asmv2:description/@product", + "asmv2:description/@publisher", + "asmv2:description/@supportUrl", + "asmv2:file/@group", + "asmv2:file/@optional", + "asmv2:file/@writeableType", + }; + } +} diff --git a/src/XMakeTasks/ManifestUtil/XmlNamespaces.cs b/src/XMakeTasks/ManifestUtil/XmlNamespaces.cs new file mode 100644 index 00000000000..eeafcb757aa --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/XmlNamespaces.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Xml; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class XmlNamespaces + { + public const string asmv1 = "urn:schemas-microsoft-com:asm.v1"; + public const string asmv2 = "urn:schemas-microsoft-com:asm.v2"; + public const string asmv3 = "urn:schemas-microsoft-com:asm.v3"; + public const string dsig = "http://www.w3.org/2000/09/xmldsig#"; + public const string xrml = "urn:mpeg:mpeg21:2003:01-REL-R-NS"; + public const string xsi = "http://www.w3.org/2001/XMLSchema-instance"; + + public static XmlNamespaceManager GetNamespaceManager(XmlNameTable nameTable) + { + XmlNamespaceManager nsmgr = new XmlNamespaceManager(nameTable); + nsmgr.AddNamespace("asmv1", asmv1); + nsmgr.AddNamespace("asmv2", asmv2); + nsmgr.AddNamespace("asmv3", asmv3); + nsmgr.AddNamespace("dsig", dsig); + nsmgr.AddNamespace("xrml", xrml); + nsmgr.AddNamespace("xsi", xsi); + return nsmgr; + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/XmlUtil.cs b/src/XMakeTasks/ManifestUtil/XmlUtil.cs new file mode 100644 index 00000000000..85b8988f1ab --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/XmlUtil.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Security.Policy; +using System.Text; +using System.Xml; +using System.Xml.XPath; +using System.Xml.Xsl; + +namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities +{ + internal static class XmlUtil + { + private static readonly ResourceResolver s_resolver = new ResourceResolver(); + + public static string GetQName(XmlTextReader r, XmlNamespaceManager nsmgr) + { + string prefix = !String.IsNullOrEmpty(r.Prefix) ? r.Prefix : nsmgr.LookupPrefix(r.NamespaceURI); + if (!String.IsNullOrEmpty(prefix)) + return prefix + ":" + r.LocalName; + else + return r.LocalName; + } + + //NOTE: XmlDocument.ImportNode munges "xmlns:asmv2" to "xmlns:d1p1" for some reason, use XmlUtil.CloneElementToDocument instead + public static XmlElement CloneElementToDocument(XmlElement element, XmlDocument document, string namespaceURI) + { + XmlElement newElement = document.CreateElement(element.Name, namespaceURI); + foreach (XmlAttribute attribute in element.Attributes) + { + XmlAttribute newAttribute = document.CreateAttribute(attribute.Name); + newAttribute.Value = attribute.Value; + newElement.Attributes.Append(newAttribute); + } + foreach (XmlNode node in element.ChildNodes) + if (node.NodeType == XmlNodeType.Element) + { + XmlElement childElement = CloneElementToDocument((XmlElement)node, document, namespaceURI); + newElement.AppendChild(childElement); + } + else if (node.NodeType == XmlNodeType.Comment) + { + XmlComment childComment = document.CreateComment(((XmlComment)node).Data); + newElement.AppendChild(childComment); + } + return newElement; + } + + public static string TrimPrefix(string s) + { + int i = s.IndexOf(':'); + if (i < 0) + return s; + return s.Substring(i + 1); + } + + [SuppressMessage("Microsoft.Security.Xml", "CA3073: ReviewTrustedXsltUse.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + [SuppressMessage("Microsoft.Security.Xml", "CA3053: UseSecureXmlResolver.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + [SuppressMessage("Microsoft.Security.Xml", "CA3059: UseXmlReaderForXPathDocument.", Justification = "Input style sheet comes from our own assemblies. Hence it is a trusted source.")] + public static Stream XslTransform(string resource, Stream input, params DictionaryEntry[] entries) + { + int t1 = Environment.TickCount; + + Stream s = Util.GetEmbeddedResourceStream(resource); + + int t2 = Environment.TickCount; + XPathDocument d = new XPathDocument(s); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "new XPathDocument(1) t={1}", resource, Environment.TickCount - t2)); + + int t3 = Environment.TickCount; + XslCompiledTransform xslc = new XslCompiledTransform(); + // Using the Trusted Xslt is fine as the style sheet comes from our own assemblies. + // This is similar to the prior this.GetType().Assembly/Evidence method that was used in the now depricated XslTransform. + xslc.Load(d, XsltSettings.TrustedXslt, s_resolver); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "XslCompiledTransform.Load t={1}", resource, Environment.TickCount - t3)); + + // Need to copy input stream because XmlReader will close it, + // causing errors for later callers that access the same stream + MemoryStream clonedInput = new MemoryStream(); + Util.CopyStream(input, clonedInput); + + int t4 = Environment.TickCount; + XmlReader xml = XmlReader.Create(clonedInput); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "new XmlReader(2) t={1}", resource, Environment.TickCount - t4)); + + XsltArgumentList args = null; + if (entries.Length > 0) + { + args = new XsltArgumentList(); + foreach (DictionaryEntry entry in entries) + { + string key = entry.Key.ToString(); + object val = entry.Value.ToString(); + args.AddParam(key, "", val); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "arg: key='{0}' value='{1}'", key, val.ToString())); + } + } + + MemoryStream m = new MemoryStream(); + XmlTextWriter w = new XmlTextWriter(m, Encoding.UTF8); + w.WriteStartDocument(); + + int t5 = Environment.TickCount; + xslc.Transform(xml, args, w, s_resolver); + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "XslCompiledTransform.Transform t={1}", resource, Environment.TickCount - t4)); + + w.WriteEndDocument(); + w.Flush(); + m.Position = 0; + + Util.WriteLog(String.Format(CultureInfo.CurrentCulture, "XslCompiledTransform(\"{0}\") t={1}", resource, Environment.TickCount - t1)); + return m; + } + + private class ResourceResolver : XmlUrlResolver + { + public override Object GetEntity(Uri uri, string role, Type t) + { + if (!uri.IsAbsoluteUri) + { + // As this is not an absolute URI, the file operations below won't work anyways, so we return null. + // This method used to throw an exception on an absolute URI, but it was silently consumed by XslTransform. XslCompiledTransform is no longer silent about these inner exceptions. + return null; + } + + string filename = uri.Segments[uri.Segments.Length - 1]; + Stream s = null; + + // If path is in temp then we immediately know we can skip the first two checks... + if (!uri.LocalPath.StartsWith(Path.GetTempPath(), StringComparison.Ordinal)) + { + // First look in assembly resources... + Assembly a = Assembly.GetExecutingAssembly(); + s = a.GetManifestResourceStream(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", typeof(Util).Namespace, filename)); + + if (s != null) + return s; + + // Next look in current directory... + try + { + s = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch (FileNotFoundException) + { + } + if (s != null) + return s; + } + + // Lastly, look at full specified uri path... + try + { + s = new FileStream(uri.LocalPath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch (DirectoryNotFoundException) + { + } + catch (FileNotFoundException) + { + } + if (s != null) + return s; + + // Didn't find the resource... + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "ResourceResolver could not find file '{0}'", filename)); + return null; + } + } + } +} diff --git a/src/XMakeTasks/ManifestUtil/manifest.xml b/src/XMakeTasks/ManifestUtil/manifest.xml new file mode 100644 index 00000000000..9e29e10a1d5 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/manifest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/XMakeTasks/ManifestUtil/mansign.cs b/src/XMakeTasks/ManifestUtil/mansign.cs new file mode 100644 index 00000000000..5826a16250f --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/mansign.cs @@ -0,0 +1,1554 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +// +// + + +using System; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Xml; +using System.Runtime.InteropServices; + +using _FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; + +namespace System.Deployment.Internal.CodeSigning +{ + internal static class Win32 + { + // + // PInvoke dll's. + // + internal const String KERNEL32 = "kernel32.dll"; +#if (true) + +#if FEATURE_MAIN_CLR_MODULE_USES_CORE_NAME + internal const String MSCORWKS = "coreclr.dll"; +#elif USE_OLD_MSCORWKS_NAME // for updating devdiv toolset until it has clr.dll + internal const String MSCORWKS = "mscorwks.dll"; +#else //FEATURE_MAIN_CLR_MODULE_USES_CORE_NAME + internal const String MSCORWKS = "clr.dll"; +#endif //FEATURE_MAIN_CLR_MODULE_USES_CORE_NAME + +#else + internal const String MSCORWKS = "isowhidbey.dll"; +#endif + // + // Constants. + // + internal const int S_OK = unchecked((int)0x00000000); + internal const int NTE_BAD_KEY = unchecked((int)0x80090003); + + // Trust errors. + internal const int TRUST_E_SYSTEM_ERROR = unchecked((int)0x80096001); + internal const int TRUST_E_NO_SIGNER_CERT = unchecked((int)0x80096002); + internal const int TRUST_E_COUNTER_SIGNER = unchecked((int)0x80096003); + internal const int TRUST_E_CERT_SIGNATURE = unchecked((int)0x80096004); + internal const int TRUST_E_TIME_STAMP = unchecked((int)0x80096005); + internal const int TRUST_E_BAD_DIGEST = unchecked((int)0x80096010); + internal const int TRUST_E_BASIC_CONSTRAINTS = unchecked((int)0x80096019); + internal const int TRUST_E_FINANCIAL_CRITERIA = unchecked((int)0x8009601E); + internal const int TRUST_E_PROVIDER_UNKNOWN = unchecked((int)0x800B0001); + internal const int TRUST_E_ACTION_UNKNOWN = unchecked((int)0x800B0002); + internal const int TRUST_E_SUBJECT_FORM_UNKNOWN = unchecked((int)0x800B0003); + internal const int TRUST_E_SUBJECT_NOT_TRUSTED = unchecked((int)0x800B0004); + internal const int TRUST_E_NOSIGNATURE = unchecked((int)0x800B0100); + internal const int CERT_E_UNTRUSTEDROOT = unchecked((int)0x800B0109); + internal const int TRUST_E_FAIL = unchecked((int)0x800B010B); + internal const int TRUST_E_EXPLICIT_DISTRUST = unchecked((int)0x800B0111); + internal const int CERT_E_CHAINING = unchecked((int)0x800B010A); + + + // Values for dwFlags of CertVerifyAuthenticodeLicense. + internal const int AXL_REVOCATION_NO_CHECK = unchecked((int)0x00000001); + internal const int AXL_REVOCATION_CHECK_END_CERT_ONLY = unchecked((int)0x00000002); + internal const int AXL_REVOCATION_CHECK_ENTIRE_CHAIN = unchecked((int)0x00000004); + internal const int AXL_URL_CACHE_ONLY_RETRIEVAL = unchecked((int)0x00000008); + internal const int AXL_LIFETIME_SIGNING = unchecked((int)0x00000010); + internal const int AXL_TRUST_MICROSOFT_ROOT_ONLY = unchecked((int)0x00000020); + + // Wintrust Policy Flag + // These are set during install and can be modified by the user + // through various means. The SETREG.EXE utility (found in the Authenticode + // Tools Pack) will select/deselect each of them. + internal const int WTPF_IGNOREREVOKATION = (int)0x00000200; // Do revocation check + + // The default WinVerifyTrust Authenticode policy is to treat all time stamped + // signatures as being valid forever. This OID limits the valid lifetime of the + // signature to the lifetime of the certificate. This allows timestamped + // signatures to expire. Normally this OID will be used in conjunction with + // szOID_PKIX_KP_CODE_SIGNING to indicate new time stamp semantics should be + // used. Support for this OID was added in WXP. + internal const string szOID_KP_LIFETIME_SIGNING = "1.3.6.1.4.1.311.10.3.13"; + internal const string szOID_RSA_signingTime = "1.2.840.113549.1.9.5"; + + // + // Structures. + // + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct CRYPT_DATA_BLOB + { + internal uint cbData; + internal IntPtr pbData; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct AXL_SIGNER_INFO + { + internal uint cbSize; // sizeof(AXL_SIGNER_INFO). + internal uint dwError; // Error code. + internal uint algHash; // Hash algorithm (ALG_ID). + internal IntPtr pwszHash; // Hash. + internal IntPtr pwszDescription; // Description. + internal IntPtr pwszDescriptionUrl; // Description URL. + internal IntPtr pChainContext; // Signer's chain context. + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct AXL_TIMESTAMPER_INFO + { + internal uint cbSize; // sizeof(AXL_TIMESTAMPER_INFO). + internal uint dwError; // Error code. + internal uint algHash; // Hash algorithm (ALG_ID). + internal _FILETIME ftTimestamp; // Timestamp time. + internal IntPtr pChainContext; // Timestamper's chain context. + } + + // + // DllImport declarations. + // + [DllImport(KERNEL32, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + IntPtr GetProcessHeap(); + + [DllImport(KERNEL32, CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal extern static + bool HeapFree( + [In] IntPtr hHeap, + [In] uint dwFlags, + [In] IntPtr lpMem); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int CertTimestampAuthenticodeLicense( + [In] ref CRYPT_DATA_BLOB pSignedLicenseBlob, + [In] string pwszTimestampURI, + [In, Out] ref CRYPT_DATA_BLOB pTimestampSignatureBlob); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int CertVerifyAuthenticodeLicense( + [In] ref CRYPT_DATA_BLOB pLicenseBlob, + [In] uint dwFlags, + [In, Out] ref AXL_SIGNER_INFO pSignerInfo, + [In, Out] ref AXL_TIMESTAMPER_INFO pTimestamperInfo); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int CertFreeAuthenticodeSignerInfo( + [In] ref AXL_SIGNER_INFO pSignerInfo); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int CertFreeAuthenticodeTimestamperInfo( + [In] ref AXL_TIMESTAMPER_INFO pTimestamperInfo); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int _AxlGetIssuerPublicKeyHash( + [In] IntPtr pCertContext, + [In, Out] ref IntPtr ppwszPublicKeyHash); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int _AxlRSAKeyValueToPublicKeyToken( + [In] ref CRYPT_DATA_BLOB pModulusBlob, + [In] ref CRYPT_DATA_BLOB pExponentBlob, + [In, Out] ref IntPtr ppwszPublicKeyToken); + + [DllImport(MSCORWKS, CharSet = CharSet.Auto, SetLastError = true)] + internal extern static + int _AxlPublicKeyBlobToPublicKeyToken( + [In] ref CRYPT_DATA_BLOB pCspPublicKeyBlob, + [In, Out] ref IntPtr ppwszPublicKeyToken); + } + + internal class ManifestSignedXml : SignedXml + { + private bool _verify = false; + + internal ManifestSignedXml() : base() { } + internal ManifestSignedXml(XmlElement elem) : base(elem) { } + internal ManifestSignedXml(XmlDocument document) : base(document) { } + + internal ManifestSignedXml(XmlDocument document, bool verify) : base(document) + { + _verify = verify; + } + + private static XmlElement FindIdElement(XmlElement context, string idValue) + { + if (context == null) + return null; + + XmlElement idReference = context.SelectSingleNode("//*[@Id=\"" + idValue + "\"]") as XmlElement; + if (idReference != null) + return idReference; + idReference = context.SelectSingleNode("//*[@id=\"" + idValue + "\"]") as XmlElement; + if (idReference != null) + return idReference; + return context.SelectSingleNode("//*[@ID=\"" + idValue + "\"]") as XmlElement; + } + + public override XmlElement GetIdElement(XmlDocument document, string idValue) + { + // We only care about Id references inside of the KeyInfo section + if (_verify) + return base.GetIdElement(document, idValue); + + KeyInfo keyInfo = this.KeyInfo; + if (keyInfo.Id != idValue) + return null; + return keyInfo.GetXml(); + } + } + + internal class SignedCmiManifest + { + private XmlDocument _manifestDom = null; + private CmiStrongNameSignerInfo _strongNameSignerInfo = null; + private CmiAuthenticodeSignerInfo _authenticodeSignerInfo = null; + + private SignedCmiManifest() { } + + internal SignedCmiManifest(XmlDocument manifestDom) + { + if (manifestDom == null) + throw new ArgumentNullException("manifestDom"); + _manifestDom = manifestDom; + } + + internal void Sign(CmiManifestSigner signer) + { + Sign(signer, null); + } + + internal void Sign(CmiManifestSigner signer, string timeStampUrl) + { + // Reset signer infos. + _strongNameSignerInfo = null; + _authenticodeSignerInfo = null; + + // Signer cannot be null. + if (signer == null || signer.StrongNameKey == null) + { + throw new ArgumentNullException("signer"); + } + + // Remove existing SN signature. + RemoveExistingSignature(_manifestDom); + + // Replace public key token in assemblyIdentity if requested. + if ((signer.Flag & CmiManifestSignerFlag.DontReplacePublicKeyToken) == 0) + { + ReplacePublicKeyToken(_manifestDom, signer.StrongNameKey); + } + + // No cert means don't Authenticode sign and timestamp. + XmlDocument licenseDom = null; + if (signer.Certificate != null) + { + // Yes. We will Authenticode sign, so first insert + // element, if necessary. + InsertPublisherIdentity(_manifestDom, signer.Certificate); + + // Now create the license DOM, and then sign it. + licenseDom = CreateLicenseDom(signer, ExtractPrincipalFromManifest(), ComputeHashFromManifest(_manifestDom)); + AuthenticodeSignLicenseDom(licenseDom, signer, timeStampUrl); + } + StrongNameSignManifestDom(_manifestDom, licenseDom, signer); + } + + // throw cryptographic exception for any verification errors. + internal void Verify(CmiManifestVerifyFlags verifyFlags) + { + // Reset signer infos. + _strongNameSignerInfo = null; + _authenticodeSignerInfo = null; + + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + XmlElement signatureNode = _manifestDom.SelectSingleNode("//ds:Signature", nsm) as XmlElement; + if (signatureNode == null) + { + throw new CryptographicException(Win32.TRUST_E_NOSIGNATURE); + } + + // Make sure it is indeed SN signature, and it is an enveloped signature. + string snIdName = "Id"; + if (!signatureNode.HasAttribute(snIdName)) + { + snIdName = "id"; + if (!signatureNode.HasAttribute(snIdName)) + { + snIdName = "ID"; + if (!signatureNode.HasAttribute(snIdName)) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + } + } + + string snIdValue = signatureNode.GetAttribute(snIdName); + if (snIdValue == null || + String.Compare(snIdValue, "StrongNameSignature", StringComparison.Ordinal) != 0) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Make sure it is indeed an enveloped signature. + bool oldFormat = false; + bool validFormat = false; + XmlNodeList referenceNodes = signatureNode.SelectNodes("ds:SignedInfo/ds:Reference", nsm); + foreach (XmlNode referenceNode in referenceNodes) + { + XmlElement reference = referenceNode as XmlElement; + if (reference != null && reference.HasAttribute("URI")) + { + string uriValue = reference.GetAttribute("URI"); + if (uriValue != null) + { + // We expect URI="" (empty URI value which means to hash the entire document). + if (uriValue.Length == 0) + { + XmlNode transformsNode = reference.SelectSingleNode("ds:Transforms", nsm); + if (transformsNode == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Make sure the transforms are what we expected. + XmlNodeList transforms = transformsNode.SelectNodes("ds:Transform", nsm); + if (transforms.Count < 2) + { + // We expect at least: + // + // + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + bool c14 = false; + bool enveloped = false; + for (int i = 0; i < transforms.Count; i++) + { + XmlElement transform = transforms[i] as XmlElement; + string algorithm = transform.GetAttribute("Algorithm"); + if (algorithm == null) + { + break; + } + else if (String.Compare(algorithm, SignedXml.XmlDsigExcC14NTransformUrl, StringComparison.Ordinal) != 0) + { + c14 = true; + if (enveloped) + { + validFormat = true; + break; + } + } + else if (String.Compare(algorithm, SignedXml.XmlDsigEnvelopedSignatureTransformUrl, StringComparison.Ordinal) != 0) + { + enveloped = true; + if (c14) + { + validFormat = true; + break; + } + } + } + } + else if (String.Compare(uriValue, "#StrongNameKeyInfo", StringComparison.Ordinal) == 0) + { + oldFormat = true; + + XmlNode transformsNode = referenceNode.SelectSingleNode("ds:Transforms", nsm); + if (transformsNode == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Make sure the transforms are what we expected. + XmlNodeList transforms = transformsNode.SelectNodes("ds:Transform", nsm); + if (transforms.Count < 1) + { + // We expect at least: + // + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + for (int i = 0; i < transforms.Count; i++) + { + XmlElement transform = transforms[i] as XmlElement; + string algorithm = transform.GetAttribute("Algorithm"); + if (algorithm == null) + { + break; + } + else if (String.Compare(algorithm, SignedXml.XmlDsigExcC14NTransformUrl, StringComparison.Ordinal) != 0) + { + validFormat = true; + break; + } + } + } + } + } + } + + if (!validFormat) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // It is the DSig we want, now make sure the public key matches the token. + string publicKeyToken = VerifyPublicKeyToken(); + + // OK. We found the SN signature with matching public key token, so + // instantiate the SN signer info property. + _strongNameSignerInfo = new CmiStrongNameSignerInfo(Win32.TRUST_E_FAIL, publicKeyToken); + + // Now verify the SN signature, and Authenticode license if available. + ManifestSignedXml signedXml = new ManifestSignedXml(_manifestDom, true); + signedXml.LoadXml(signatureNode); + + AsymmetricAlgorithm key = null; + bool dsigValid = signedXml.CheckSignatureReturningKey(out key); + _strongNameSignerInfo.PublicKey = key; + if (!dsigValid) + { + _strongNameSignerInfo.ErrorCode = Win32.TRUST_E_BAD_DIGEST; + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + // Verify license as well if requested. + if ((verifyFlags & CmiManifestVerifyFlags.StrongNameOnly) != CmiManifestVerifyFlags.StrongNameOnly) + { + VerifyLicense(verifyFlags, oldFormat); + } + } + + internal CmiStrongNameSignerInfo StrongNameSignerInfo + { + get + { + return _strongNameSignerInfo; + } + } + + internal CmiAuthenticodeSignerInfo AuthenticodeSignerInfo + { + get + { + return _authenticodeSignerInfo; + } + } + + // + // Privates. + // + private void VerifyLicense(CmiManifestVerifyFlags verifyFlags, bool oldFormat) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("asm2", AssemblyV2NamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + nsm.AddNamespace("msrel", MSRelNamespaceUri); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + + // We are done if no license. + XmlElement licenseNode = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license", nsm) as XmlElement; + if (licenseNode == null) + { + return; + } + + // Make sure this license is for this manifest. + VerifyAssemblyIdentity(nsm); + + // Found a license, so instantiate signer info property. + _authenticodeSignerInfo = new CmiAuthenticodeSignerInfo(Win32.TRUST_E_FAIL); + + unsafe + { + byte[] licenseXml = Encoding.UTF8.GetBytes(licenseNode.OuterXml); + fixed (byte* pbLicense = licenseXml) + { + Win32.AXL_SIGNER_INFO signerInfo = new Win32.AXL_SIGNER_INFO(); + signerInfo.cbSize = (uint)Marshal.SizeOf(typeof(Win32.AXL_SIGNER_INFO)); + Win32.AXL_TIMESTAMPER_INFO timestamperInfo = new Win32.AXL_TIMESTAMPER_INFO(); + timestamperInfo.cbSize = (uint)Marshal.SizeOf(typeof(Win32.AXL_TIMESTAMPER_INFO)); + Win32.CRYPT_DATA_BLOB licenseBlob = new Win32.CRYPT_DATA_BLOB(); + IntPtr pvLicense = new IntPtr(pbLicense); + licenseBlob.cbData = (uint)licenseXml.Length; + licenseBlob.pbData = pvLicense; + + int hr = Win32.CertVerifyAuthenticodeLicense(ref licenseBlob, (uint)verifyFlags, ref signerInfo, ref timestamperInfo); + if (Win32.TRUST_E_NOSIGNATURE != (int)signerInfo.dwError) + { + _authenticodeSignerInfo = new CmiAuthenticodeSignerInfo(signerInfo, timestamperInfo); + } + + Win32.CertFreeAuthenticodeSignerInfo(ref signerInfo); + Win32.CertFreeAuthenticodeTimestamperInfo(ref timestamperInfo); + + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + } + } + + if (!oldFormat) + // Make sure we have the intended Authenticode signer. + VerifyPublisherIdentity(nsm); + } + + private XmlElement ExtractPrincipalFromManifest() + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + XmlNode assemblyIdentityNode = _manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm); + if (assemblyIdentityNode == null) + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + return assemblyIdentityNode as XmlElement; + } + + private void VerifyAssemblyIdentity(XmlNamespaceManager nsm) + { + XmlElement assemblyIdentity = _manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + XmlElement principal = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license/r:grant/as:ManifestInformation/as:assemblyIdentity", nsm) as XmlElement; + + if (assemblyIdentity == null || principal == null || + !assemblyIdentity.HasAttributes || !principal.HasAttributes) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + XmlAttributeCollection asmIdAttrs = assemblyIdentity.Attributes; + + if (asmIdAttrs.Count == 0 || asmIdAttrs.Count != principal.Attributes.Count) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + foreach (XmlAttribute asmIdAttr in asmIdAttrs) + { + if (!principal.HasAttribute(asmIdAttr.LocalName) || + asmIdAttr.Value != principal.GetAttribute(asmIdAttr.LocalName)) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + } + + VerifyHash(nsm); + } + + private void VerifyPublisherIdentity(XmlNamespaceManager nsm) + { + // Nothing to do if no signature. + if (_authenticodeSignerInfo.ErrorCode == Win32.TRUST_E_NOSIGNATURE) + { + return; + } + + X509Certificate2 signerCert = _authenticodeSignerInfo.SignerChain.ChainElements[0].Certificate; + + // Find the publisherIdentity element. + XmlElement publisherIdentity = _manifestDom.SelectSingleNode("asm:assembly/asm2:publisherIdentity", nsm) as XmlElement; + if (publisherIdentity == null || !publisherIdentity.HasAttributes) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Get name and issuerKeyHash attribute values. + if (!publisherIdentity.HasAttribute("name") || !publisherIdentity.HasAttribute("issuerKeyHash")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + string publisherName = publisherIdentity.GetAttribute("name"); + string publisherIssuerKeyHash = publisherIdentity.GetAttribute("issuerKeyHash"); + + // Calculate the issuer key hash. + IntPtr pIssuerKeyHash = new IntPtr(); + int hr = Win32._AxlGetIssuerPublicKeyHash(signerCert.Handle, ref pIssuerKeyHash); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + string issuerKeyHash = Marshal.PtrToStringUni(pIssuerKeyHash); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pIssuerKeyHash); + + // Make sure name and issuerKeyHash match. + if (String.Compare(publisherName, signerCert.SubjectName.Name, StringComparison.Ordinal) != 0 || + String.Compare(publisherIssuerKeyHash, issuerKeyHash, StringComparison.Ordinal) != 0) + { + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + } + + private void VerifyHash(XmlNamespaceManager nsm) + { + XmlDocument manifestDom = new XmlDocument(); + // We always preserve white space as Fusion XML engine always preserve white space. + manifestDom.PreserveWhitespace = true; + manifestDom = (XmlDocument)_manifestDom.Clone(); + + XmlElement manifestInformation = manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license/r:grant/as:ManifestInformation", nsm) as XmlElement; + if (manifestInformation == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + if (!manifestInformation.HasAttribute("Hash")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + string hash = manifestInformation.GetAttribute("Hash"); + if (hash == null || hash.Length == 0) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Now compute the hash for the manifest without the entire SN + // signature element. + + // First remove the Signture element from the DOM. + XmlElement dsElement = manifestDom.SelectSingleNode("asm:assembly/ds:Signature", nsm) as XmlElement; + if (dsElement == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + dsElement.ParentNode.RemoveChild(dsElement); + + // Now compute the hash from the manifest, without the Signature element. + byte[] hashBytes = HexStringToBytes(manifestInformation.GetAttribute("Hash")); + byte[] computedHashBytes = ComputeHashFromManifest(manifestDom); + + // Do they match? + if (hashBytes.Length == 0 || hashBytes.Length != computedHashBytes.Length) + { + byte[] computedOldHashBytes = ComputeHashFromManifest(manifestDom, true); + + // Do they match? + if (hashBytes.Length == 0 || hashBytes.Length != computedOldHashBytes.Length) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + for (int i = 0; i < hashBytes.Length; i++) + { + if (hashBytes[i] != computedOldHashBytes[i]) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + } + } + + for (int i = 0; i < hashBytes.Length; i++) + { + if (hashBytes[i] != computedHashBytes[i]) + { +#if (true) // BUGBUG: Remove before RTM once old format support is no longer needed. + byte[] computedOldHashBytes = ComputeHashFromManifest(manifestDom, true); + + // Do they match? + if (hashBytes.Length == 0 || hashBytes.Length != computedOldHashBytes.Length) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + for (i = 0; i < hashBytes.Length; i++) + { + if (hashBytes[i] != computedOldHashBytes[i]) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + } +#else + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); +#endif + } + } + } + + private string VerifyPublicKeyToken() + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlElement snModulus = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Modulus", nsm) as XmlElement; + XmlElement snExponent = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Exponent", nsm) as XmlElement; + + if (snModulus == null || snExponent == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + byte[] modulus = Encoding.UTF8.GetBytes(snModulus.InnerXml); + byte[] exponent = Encoding.UTF8.GetBytes(snExponent.InnerXml); + + string tokenString = GetPublicKeyToken(_manifestDom); + byte[] publicKeyToken = HexStringToBytes(tokenString); + byte[] computedPublicKeyToken; + + unsafe + { + fixed (byte* pbModulus = modulus) + { + fixed (byte* pbExponent = exponent) + { + Win32.CRYPT_DATA_BLOB modulusBlob = new Win32.CRYPT_DATA_BLOB(); + Win32.CRYPT_DATA_BLOB exponentBlob = new Win32.CRYPT_DATA_BLOB(); + IntPtr pComputedToken = new IntPtr(); + + modulusBlob.cbData = (uint)modulus.Length; + modulusBlob.pbData = new IntPtr(pbModulus); + exponentBlob.cbData = (uint)exponent.Length; + exponentBlob.pbData = new IntPtr(pbExponent); + + // Now compute the public key token. + int hr = Win32._AxlRSAKeyValueToPublicKeyToken(ref modulusBlob, ref exponentBlob, ref pComputedToken); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + computedPublicKeyToken = HexStringToBytes(Marshal.PtrToStringUni(pComputedToken)); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pComputedToken); + } + } + } + + // Do they match? + if (publicKeyToken.Length == 0 || publicKeyToken.Length != computedPublicKeyToken.Length) + { + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + + for (int i = 0; i < publicKeyToken.Length; i++) + { + if (publicKeyToken[i] != computedPublicKeyToken[i]) + { + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + } + + return tokenString; + } + + // + // Statics. + // + private static void InsertPublisherIdentity(XmlDocument manifestDom, X509Certificate2 signerCert) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("asm2", AssemblyV2NamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlElement assembly = manifestDom.SelectSingleNode("asm:assembly", nsm) as XmlElement; + XmlElement assemblyIdentity = manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + if (assemblyIdentity == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Reuse existing node if exists + XmlElement publisherIdentity = manifestDom.SelectSingleNode("asm:assembly/asm2:publisherIdentity", nsm) as XmlElement; + if (publisherIdentity == null) + { + // create new if not exist + publisherIdentity = manifestDom.CreateElement("publisherIdentity", AssemblyV2NamespaceUri); + } + // Get the issuer's public key blob hash. + IntPtr pIssuerKeyHash = new IntPtr(); + int hr = Win32._AxlGetIssuerPublicKeyHash(signerCert.Handle, ref pIssuerKeyHash); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + string issuerKeyHash = Marshal.PtrToStringUni(pIssuerKeyHash); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pIssuerKeyHash); + + publisherIdentity.SetAttribute("name", signerCert.SubjectName.Name); + publisherIdentity.SetAttribute("issuerKeyHash", issuerKeyHash); + + XmlElement signature = manifestDom.SelectSingleNode("asm:assembly/ds:Signature", nsm) as XmlElement; + if (signature != null) + { + assembly.InsertBefore(publisherIdentity, signature); + } + else + { + assembly.AppendChild(publisherIdentity); + } + } + + private static void RemoveExistingSignature(XmlDocument manifestDom) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + XmlNode signatureNode = manifestDom.SelectSingleNode("asm:assembly/ds:Signature", nsm); + if (signatureNode != null) + signatureNode.ParentNode.RemoveChild(signatureNode); + } + + private static void ReplacePublicKeyToken(XmlDocument manifestDom, AsymmetricAlgorithm snKey) + { + // Make sure we can find the publicKeyToken attribute. + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + XmlElement assemblyIdentity = manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + if (assemblyIdentity == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + if (!assemblyIdentity.HasAttribute("publicKeyToken")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + byte[] cspPublicKeyBlob = ((RSACryptoServiceProvider)snKey).ExportCspBlob(false); + if (cspPublicKeyBlob == null || cspPublicKeyBlob.Length == 0) + { + throw new CryptographicException(Win32.NTE_BAD_KEY); + } + + // Now compute the public key token. + unsafe + { + fixed (byte* pbPublicKeyBlob = cspPublicKeyBlob) + { + Win32.CRYPT_DATA_BLOB publicKeyBlob = new Win32.CRYPT_DATA_BLOB(); + publicKeyBlob.cbData = (uint)cspPublicKeyBlob.Length; + publicKeyBlob.pbData = new IntPtr(pbPublicKeyBlob); + IntPtr pPublicKeyToken = new IntPtr(); + + int hr = Win32._AxlPublicKeyBlobToPublicKeyToken(ref publicKeyBlob, ref pPublicKeyToken); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + string publicKeyToken = Marshal.PtrToStringUni(pPublicKeyToken); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pPublicKeyToken); + + assemblyIdentity.SetAttribute("publicKeyToken", publicKeyToken); + } + } + } + + private static string GetPublicKeyToken(XmlDocument manifestDom) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlElement assemblyIdentity = manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + + if (assemblyIdentity == null || !assemblyIdentity.HasAttribute("publicKeyToken")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + return assemblyIdentity.GetAttribute("publicKeyToken"); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Cryptographic.Standard", "CA5354:SHA1CannotBeUsed", Justification = "SHA1 is retained for compatibility reasons as an option in VisualStudio signing page and consequently in the trust manager, default is SHA2.")] + private static byte[] ComputeHashFromManifest(XmlDocument manifestDom) + { + return ComputeHashFromManifest(manifestDom, false); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Cryptographic.Standard", "CA5354:SHA1CannotBeUsed", Justification = "SHA1 is retained for compatibility reasons as an option in VisualStudio signing page and consequently in the trust manager, default is SHA2.")] + private static byte[] ComputeHashFromManifest(XmlDocument manifestDom, bool oldFormat) + { + if (oldFormat) + { + XmlDsigExcC14NTransform exc = new XmlDsigExcC14NTransform(); + exc.LoadInput(manifestDom); + using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider()) + { + byte[] hash = sha1.ComputeHash(exc.GetOutput() as MemoryStream); + if (hash == null) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + return hash; + } + } + else + { + // Since the DOM given to us is not guaranteed to be normalized, + // we need to normalize it ourselves. Also, we always preserve + // white space as Fusion XML engine always preserve white space. + XmlDocument normalizedDom = new XmlDocument(); + normalizedDom.PreserveWhitespace = true; + + // Normalize the document + using (TextReader stringReader = new StringReader(manifestDom.OuterXml)) + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Parse; + XmlReader reader = XmlReader.Create(stringReader, settings, manifestDom.BaseURI); + normalizedDom.Load(reader); + } + + XmlDsigExcC14NTransform exc = new XmlDsigExcC14NTransform(); + exc.LoadInput(normalizedDom); + using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider()) + { + byte[] hash = sha1.ComputeHash(exc.GetOutput() as MemoryStream); + if (hash == null) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + return hash; + } + } + } + + private const string AssemblyNamespaceUri = "urn:schemas-microsoft-com:asm.v1"; + private const string AssemblyV2NamespaceUri = "urn:schemas-microsoft-com:asm.v2"; + private const string MSRelNamespaceUri = "http://schemas.microsoft.com/windows/rel/2005/reldata"; + private const string LicenseNamespaceUri = "urn:mpeg:mpeg21:2003:01-REL-R-NS"; + private const string AuthenticodeNamespaceUri = "http://schemas.microsoft.com/windows/pki/2005/Authenticode"; + private const string licenseTemplate = "" + + @"" + + @"" + + @"" + + @"" + + @"" + + @"" + + @"CN=dummy" + + @"" + + @""; + + private static XmlDocument CreateLicenseDom(CmiManifestSigner signer, XmlElement principal, byte[] hash) + { + XmlDocument licenseDom = new XmlDocument(); + licenseDom.PreserveWhitespace = true; + licenseDom.LoadXml(licenseTemplate); + XmlNamespaceManager nsm = new XmlNamespaceManager(licenseDom.NameTable); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + XmlElement assemblyIdentityNode = licenseDom.SelectSingleNode("r:license/r:grant/as:ManifestInformation/as:assemblyIdentity", nsm) as XmlElement; + assemblyIdentityNode.RemoveAllAttributes(); + foreach (XmlAttribute attribute in principal.Attributes) + { + assemblyIdentityNode.SetAttribute(attribute.Name, attribute.Value); + } + + XmlElement manifestInformationNode = licenseDom.SelectSingleNode("r:license/r:grant/as:ManifestInformation", nsm) as XmlElement; + + manifestInformationNode.SetAttribute("Hash", hash.Length == 0 ? "" : BytesToHexString(hash, 0, hash.Length)); + manifestInformationNode.SetAttribute("Description", signer.Description == null ? "" : signer.Description); + manifestInformationNode.SetAttribute("Url", signer.DescriptionUrl == null ? "" : signer.DescriptionUrl); + + XmlElement authenticodePublisherNode = licenseDom.SelectSingleNode("r:license/r:grant/as:AuthenticodePublisher/as:X509SubjectName", nsm) as XmlElement; + authenticodePublisherNode.InnerText = signer.Certificate.SubjectName.Name; + + return licenseDom; + } + + private static void AuthenticodeSignLicenseDom(XmlDocument licenseDom, CmiManifestSigner signer, string timeStampUrl) + { + // Make sure it is RSA, as this is the only one Fusion will support. + if (signer.Certificate.PublicKey.Key.GetType() != typeof(RSACryptoServiceProvider)) + { + throw new NotSupportedException(); + } + + // Setup up XMLDSIG engine. + ManifestSignedXml signedXml = new ManifestSignedXml(licenseDom); + signedXml.SigningKey = signer.Certificate.PrivateKey; + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + + // Add the key information. + signedXml.KeyInfo.AddClause(new RSAKeyValue(signer.Certificate.PublicKey.Key as RSA)); + signedXml.KeyInfo.AddClause(new KeyInfoX509Data(signer.Certificate, signer.IncludeOption)); + + // Add the enveloped reference. + Reference reference = new Reference(); + reference.Uri = ""; + + // Add an enveloped and an Exc-C14N transform. + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + + // Add the reference. + signedXml.AddReference(reference); + + // Compute the signature. + signedXml.ComputeSignature(); + + // Get the XML representation + XmlElement xmlDigitalSignature = signedXml.GetXml(); + xmlDigitalSignature.SetAttribute("Id", "AuthenticodeSignature"); + + // Insert the signature node under the issuer element. + XmlNamespaceManager nsm = new XmlNamespaceManager(licenseDom.NameTable); + nsm.AddNamespace("r", LicenseNamespaceUri); + XmlElement issuerNode = licenseDom.SelectSingleNode("r:license/r:issuer", nsm) as XmlElement; + issuerNode.AppendChild(licenseDom.ImportNode(xmlDigitalSignature, true)); + + // Time stamp it if requested. + if (timeStampUrl != null && timeStampUrl.Length != 0) + { + TimestampSignedLicenseDom(licenseDom, timeStampUrl); + } + + // Wrap it inside a RelData element. + licenseDom.DocumentElement.ParentNode.InnerXml = "" + + licenseDom.OuterXml + ""; + } + + private static void TimestampSignedLicenseDom(XmlDocument licenseDom, string timeStampUrl) + { + Win32.CRYPT_DATA_BLOB timestampBlob = new Win32.CRYPT_DATA_BLOB(); + + XmlNamespaceManager nsm = new XmlNamespaceManager(licenseDom.NameTable); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + + byte[] licenseXml = Encoding.UTF8.GetBytes(licenseDom.OuterXml); + + unsafe + { + fixed (byte* pbLicense = licenseXml) + { + Win32.CRYPT_DATA_BLOB licenseBlob = new Win32.CRYPT_DATA_BLOB(); + IntPtr pvLicense = new IntPtr(pbLicense); + licenseBlob.cbData = (uint)licenseXml.Length; + licenseBlob.pbData = pvLicense; + + int hr = Win32.CertTimestampAuthenticodeLicense(ref licenseBlob, timeStampUrl, ref timestampBlob); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + } + } + + byte[] timestampSignature = new byte[timestampBlob.cbData]; + Marshal.Copy(timestampBlob.pbData, timestampSignature, 0, timestampSignature.Length); + Win32.HeapFree(Win32.GetProcessHeap(), 0, timestampBlob.pbData); + + XmlElement asTimestamp = licenseDom.CreateElement("as", "Timestamp", AuthenticodeNamespaceUri); + asTimestamp.InnerText = Encoding.UTF8.GetString(timestampSignature); + + XmlElement dsObject = licenseDom.CreateElement("Object", SignedXml.XmlDsigNamespaceUrl); + dsObject.AppendChild(asTimestamp); + + XmlElement signatureNode = licenseDom.SelectSingleNode("r:license/r:issuer/ds:Signature", nsm) as XmlElement; + signatureNode.AppendChild(dsObject); + } + + private static void StrongNameSignManifestDom(XmlDocument manifestDom, XmlDocument licenseDom, CmiManifestSigner signer) + { + RSA snKey = signer.StrongNameKey as RSA; + + // Make sure it is RSA, as this is the only one Fusion will support. + if (snKey == null) + { + throw new NotSupportedException(); + } + + // Setup namespace manager. + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + + // Get to root element. + XmlElement signatureParent = manifestDom.SelectSingleNode("asm:assembly", nsm) as XmlElement; + if (signatureParent == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Setup up XMLDSIG engine. + ManifestSignedXml signedXml = new ManifestSignedXml(signatureParent); + signedXml.SigningKey = signer.StrongNameKey; + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + + // Add the key information. + signedXml.KeyInfo.AddClause(new RSAKeyValue(snKey)); + if (licenseDom != null) + { + signedXml.KeyInfo.AddClause(new KeyInfoNode(licenseDom.DocumentElement)); + } + signedXml.KeyInfo.Id = "StrongNameKeyInfo"; + + // Add the enveloped reference. + Reference enveloped = new Reference(); + enveloped.Uri = ""; + + // Add an enveloped then Exc-C14N transform. + enveloped.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + enveloped.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(enveloped); + +#if (false) // DSIE: New format does not sign KeyInfo. + // Add the key info reference. + Reference strongNameKeyInfo = new Reference(); + strongNameKeyInfo.Uri = "#StrongNameKeyInfo"; + strongNameKeyInfo.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(strongNameKeyInfo); +#endif + // Compute the signature. + signedXml.ComputeSignature(); + + // Get the XML representation + XmlElement xmlDigitalSignature = signedXml.GetXml(); + xmlDigitalSignature.SetAttribute("Id", "StrongNameSignature"); + + // Insert the signature now. + signatureParent.AppendChild(xmlDigitalSignature); + } + private static readonly char[] s_hexValues = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + private static string BytesToHexString(byte[] array, int start, int end) + { + string result = null; + if (array != null) + { + char[] hexOrder = new char[(end - start) * 2]; + int i = end; + int digit, j = 0; + while (i-- > start) + { + digit = (array[i] & 0xf0) >> 4; + hexOrder[j++] = s_hexValues[digit]; + digit = (array[i] & 0x0f); + hexOrder[j++] = s_hexValues[digit]; + } + result = new String(hexOrder); + } + return result; + } + + private static byte[] HexStringToBytes(string hexString) + { + uint cbHex = (uint)hexString.Length / 2; + byte[] hex = new byte[cbHex]; + int i = hexString.Length - 2; + for (int index = 0; index < cbHex; index++) + { + hex[index] = (byte)((HexToByte(hexString[i]) << 4) | HexToByte(hexString[i + 1])); + i -= 2; + } + return hex; + } + + private static byte HexToByte(char val) + { + if (val <= '9' && val >= '0') + return (byte)(val - '0'); + else if (val >= 'a' && val <= 'f') + return (byte)((val - 'a') + 10); + else if (val >= 'A' && val <= 'F') + return (byte)((val - 'A') + 10); + else + return 0xFF; + } + } + + [Flags] + internal enum CmiManifestSignerFlag + { + None = 0x00000000, + DontReplacePublicKeyToken = 0x00000001 + } + + [Flags] + internal enum CmiManifestVerifyFlags + { + None = 0x00000000, + RevocationNoCheck = 0x00000001, + RevocationCheckEndCertOnly = 0x00000002, + RevocationCheckEntireChain = 0x00000004, + UrlCacheOnlyRetrieval = 0x00000008, + LifetimeSigning = 0x00000010, + TrustMicrosoftRootOnly = 0x00000020, + StrongNameOnly = 0x00010000 + } + + internal class CmiManifestSigner + { + private AsymmetricAlgorithm _strongNameKey; + private X509Certificate2 _certificate; + private string _description; + private string _url; + private X509Certificate2Collection _certificates; + private X509IncludeOption _includeOption; + private CmiManifestSignerFlag _signerFlag; + + private CmiManifestSigner() { } + + internal CmiManifestSigner(AsymmetricAlgorithm strongNameKey) : + this(strongNameKey, null) + { } + + internal CmiManifestSigner(AsymmetricAlgorithm strongNameKey, X509Certificate2 certificate) + { + if (strongNameKey == null) + throw new ArgumentNullException("strongNameKey"); + + RSA rsa = strongNameKey as RSA; + if (rsa == null) + throw new ArgumentNullException("strongNameKey"); + _strongNameKey = strongNameKey; + _certificate = certificate; + _certificates = new X509Certificate2Collection(); + _includeOption = X509IncludeOption.ExcludeRoot; + _signerFlag = CmiManifestSignerFlag.None; + } + + internal AsymmetricAlgorithm StrongNameKey + { + get + { + return _strongNameKey; + } + } + + internal X509Certificate2 Certificate + { + get + { + return _certificate; + } + } + + internal string Description + { + get + { + return _description; + } + set + { + _description = value; + } + } + + internal string DescriptionUrl + { + get + { + return _url; + } + set + { + _url = value; + } + } + + internal X509Certificate2Collection ExtraStore + { + get + { + return _certificates; + } + } + + internal X509IncludeOption IncludeOption + { + get + { + return _includeOption; + } + set + { + if (value < X509IncludeOption.None || value > X509IncludeOption.WholeChain) + throw new ArgumentException("value"); + if (_includeOption == X509IncludeOption.None) + throw new NotSupportedException(); + _includeOption = value; + } + } + + internal CmiManifestSignerFlag Flag + { + get + { + return _signerFlag; + } + set + { + unchecked + { + if ((value & ((CmiManifestSignerFlag)~CimManifestSignerFlagMask)) != 0) + throw new ArgumentException("value"); + } + _signerFlag = value; + } + } + + internal const uint CimManifestSignerFlagMask = (uint)0x00000001; + } + + internal class CmiStrongNameSignerInfo + { + private int _error = 0; + private string _publicKeyToken = null; + private AsymmetricAlgorithm _snKey = null; + + internal CmiStrongNameSignerInfo() { } + + internal CmiStrongNameSignerInfo(int errorCode, string publicKeyToken) + { + _error = errorCode; + _publicKeyToken = publicKeyToken; + } + + internal int ErrorCode + { + get + { + return _error; + } + + set + { + _error = value; + } + } + + internal string PublicKeyToken + { + get + { + return _publicKeyToken; + } + + set + { + _publicKeyToken = value; + } + } + + internal AsymmetricAlgorithm PublicKey + { + get + { + return _snKey; + } + + set + { + _snKey = value; + } + } + } + + internal class CmiAuthenticodeSignerInfo + { + private int _error = 0; + private X509Chain _signerChain = null; + private uint _algHash = 0; + private string _hash = null; + private string _description = null; + private string _descriptionUrl = null; + private CmiAuthenticodeTimestamperInfo _timestamperInfo = null; + + internal CmiAuthenticodeSignerInfo() { } + + internal CmiAuthenticodeSignerInfo(int errorCode) + { + _error = errorCode; + } + + internal CmiAuthenticodeSignerInfo(Win32.AXL_SIGNER_INFO signerInfo, + Win32.AXL_TIMESTAMPER_INFO timestamperInfo) + { + _error = (int)signerInfo.dwError; + if (signerInfo.pChainContext != IntPtr.Zero) + { + _signerChain = new X509Chain(signerInfo.pChainContext); + } + + _algHash = signerInfo.algHash; + if (signerInfo.pwszHash != IntPtr.Zero) + { + _hash = Marshal.PtrToStringUni(signerInfo.pwszHash); + } + if (signerInfo.pwszDescription != IntPtr.Zero) + { + _description = Marshal.PtrToStringUni(signerInfo.pwszDescription); + } + if (signerInfo.pwszDescriptionUrl != IntPtr.Zero) + { + _descriptionUrl = Marshal.PtrToStringUni(signerInfo.pwszDescriptionUrl); + } + if ((int)timestamperInfo.dwError != Win32.TRUST_E_NOSIGNATURE) + { + _timestamperInfo = new CmiAuthenticodeTimestamperInfo(timestamperInfo); + } + } + + internal int ErrorCode + { + get + { + return _error; + } + set + { + _error = value; + } + } + + internal uint HashAlgId + { + get + { + return _algHash; + } + set + { + _algHash = value; + } + } + + internal string Hash + { + get + { + return _hash; + } + set + { + _hash = value; + } + } + + internal string Description + { + get + { + return _description; + } + set + { + _description = value; + } + } + + internal string DescriptionUrl + { + get + { + return _descriptionUrl; + } + set + { + _descriptionUrl = value; + } + } + + internal CmiAuthenticodeTimestamperInfo TimestamperInfo + { + get + { + return _timestamperInfo; + } + } + + internal X509Chain SignerChain + { + get + { + return _signerChain; + } + set + { + _signerChain = value; + } + } + } + + internal class CmiAuthenticodeTimestamperInfo + { + private int _error = 0; + private X509Chain _timestamperChain = null; + private DateTime _timestampTime; + private uint _algHash = 0; + + private CmiAuthenticodeTimestamperInfo() { } + + internal CmiAuthenticodeTimestamperInfo(Win32.AXL_TIMESTAMPER_INFO timestamperInfo) + { + _error = (int)timestamperInfo.dwError; + _algHash = timestamperInfo.algHash; + long dt = (((long)(uint)timestamperInfo.ftTimestamp.dwHighDateTime) << 32) | ((long)(uint)timestamperInfo.ftTimestamp.dwLowDateTime); + _timestampTime = DateTime.FromFileTime(dt); + if (timestamperInfo.pChainContext != IntPtr.Zero) + { + _timestamperChain = new X509Chain(timestamperInfo.pChainContext); + } + } + + internal int ErrorCode + { + get + { + return _error; + } + } + + internal uint HashAlgId + { + get + { + return _algHash; + } + } + + internal DateTime TimestampTime + { + get + { + return _timestampTime; + } + } + + internal X509Chain TimestamperChain + { + get + { + return _timestamperChain; + } + } + } +} + diff --git a/src/XMakeTasks/ManifestUtil/mansign2.cs b/src/XMakeTasks/ManifestUtil/mansign2.cs new file mode 100644 index 00000000000..8dee6dbebcf --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/mansign2.cs @@ -0,0 +1,1734 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +// +// mansign.cs +// + +using System; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.Xml; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Xml; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Pkcs; +using Microsoft.Win32; + +using _FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; + +namespace System.Deployment.Internal.CodeSigning +{ + internal class ManifestSignedXml2 : SignedXml + { + private bool _verify = false; + private const string Sha256SignatureMethodUri = @"http://www.w3.org/2000/09/xmldsig#rsa-sha256"; + private const string Sha256DigestMethod = @"http://www.w3.org/2000/09/xmldsig#sha256"; + + internal ManifestSignedXml2() + : base() + { + init(); + } + internal ManifestSignedXml2(XmlElement elem) + : base(elem) + { + init(); + } + internal ManifestSignedXml2(XmlDocument document) + : base(document) + { + init(); + } + + internal ManifestSignedXml2(XmlDocument document, bool verify) + : base(document) + { + _verify = verify; + init(); + } + + private void init() + { + CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), + Sha256SignatureMethodUri); + + CryptoConfig.AddAlgorithm(typeof(System.Security.Cryptography.SHA256Cng), + Sha256DigestMethod); + } + + private static XmlElement FindIdElement(XmlElement context, string idValue) + { + if (context == null) + return null; + + XmlElement idReference = context.SelectSingleNode("//*[@Id=\"" + idValue + "\"]") as XmlElement; + if (idReference != null) + return idReference; + idReference = context.SelectSingleNode("//*[@id=\"" + idValue + "\"]") as XmlElement; + if (idReference != null) + return idReference; + return context.SelectSingleNode("//*[@ID=\"" + idValue + "\"]") as XmlElement; + } + + public override XmlElement GetIdElement(XmlDocument document, string idValue) + { + // We only care about Id references inside of the KeyInfo section + if (_verify) + return base.GetIdElement(document, idValue); + + KeyInfo keyInfo = this.KeyInfo; + if (keyInfo.Id != idValue) + return null; + return keyInfo.GetXml(); + } + } + + internal class SignedCmiManifest2 + { + private XmlDocument _manifestDom = null; + private CmiStrongNameSignerInfo _strongNameSignerInfo = null; + private CmiAuthenticodeSignerInfo _authenticodeSignerInfo = null; + private bool _useSha256; + + private const string Sha256SignatureMethodUri = @"http://www.w3.org/2000/09/xmldsig#rsa-sha256"; + private const string Sha256DigestMethod = @"http://www.w3.org/2000/09/xmldsig#sha256"; + + private const string wintrustPolicyFlagsRegPath = "Software\\Microsoft\\Windows\\CurrentVersion\\WinTrust\\Trust Providers\\Software Publishing"; + private const string wintrustPolicyFlagsRegName = "State"; + + private SignedCmiManifest2() { } + + internal SignedCmiManifest2(XmlDocument manifestDom, bool useSha256) + { + if (manifestDom == null) + throw new ArgumentNullException("manifestDom"); + _manifestDom = manifestDom; + _useSha256 = useSha256; + } + + internal void Sign(CmiManifestSigner2 signer) + { + Sign(signer, null); + } + + internal void Sign(CmiManifestSigner2 signer, string timeStampUrl) + { + // Reset signer infos. + _strongNameSignerInfo = null; + _authenticodeSignerInfo = null; + + // Signer cannot be null. + if (signer == null || signer.StrongNameKey == null) + { + throw new ArgumentNullException("signer"); + } + + // Remove existing SN signature. + RemoveExistingSignature(_manifestDom); + + // Replace public key token in assemblyIdentity if requested. + if ((signer.Flag & CmiManifestSignerFlag.DontReplacePublicKeyToken) == 0) + { + ReplacePublicKeyToken(_manifestDom, signer.StrongNameKey, _useSha256); + } + + // No cert means don't Authenticode sign and timestamp. + XmlDocument licenseDom = null; + if (signer.Certificate != null) + { + // Yes. We will Authenticode sign, so first insert + // element, if necessary. + InsertPublisherIdentity(_manifestDom, signer.Certificate); + + // Now create the license DOM, and then sign it. + licenseDom = CreateLicenseDom(signer, ExtractPrincipalFromManifest(), ComputeHashFromManifest(_manifestDom, _useSha256)); + AuthenticodeSignLicenseDom(licenseDom, signer, timeStampUrl, _useSha256); + } + StrongNameSignManifestDom(_manifestDom, licenseDom, signer, _useSha256); + } + + // throw cryptographic exception for any verification errors. + internal void Verify(CmiManifestVerifyFlags verifyFlags) + { + // Reset signer infos. + _strongNameSignerInfo = null; + _authenticodeSignerInfo = null; + + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + XmlElement signatureNode = _manifestDom.SelectSingleNode("//ds:Signature", nsm) as XmlElement; + if (signatureNode == null) + { + throw new CryptographicException(Win32.TRUST_E_NOSIGNATURE); + } + + // Make sure it is indeed SN signature, and it is an enveloped signature. + bool oldFormat = VerifySignatureForm(signatureNode, "StrongNameSignature", nsm); + + // It is the DSig we want, now make sure the public key matches the token. + string publicKeyToken = VerifyPublicKeyToken(); + + // OK. We found the SN signature with matching public key token, so + // instantiate the SN signer info property. + _strongNameSignerInfo = new CmiStrongNameSignerInfo(Win32.TRUST_E_FAIL, publicKeyToken); + + // Now verify the SN signature, and Authenticode license if available. + ManifestSignedXml2 signedXml = new ManifestSignedXml2(_manifestDom, true); + signedXml.LoadXml(signatureNode); + if (_useSha256) + { + signedXml.SignedInfo.SignatureMethod = Sha256SignatureMethodUri; + } + + AsymmetricAlgorithm key = null; + bool dsigValid = signedXml.CheckSignatureReturningKey(out key); + _strongNameSignerInfo.PublicKey = key; + if (!dsigValid) + { + _strongNameSignerInfo.ErrorCode = Win32.TRUST_E_BAD_DIGEST; + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + // Verify license as well if requested. + if ((verifyFlags & CmiManifestVerifyFlags.StrongNameOnly) != CmiManifestVerifyFlags.StrongNameOnly) + { + if (_useSha256) + { + VerifyLicenseNew(verifyFlags, oldFormat); + } + else + { + VerifyLicense(verifyFlags, oldFormat); + } + } + } + + internal CmiStrongNameSignerInfo StrongNameSignerInfo + { + get + { + return _strongNameSignerInfo; + } + } + + internal CmiAuthenticodeSignerInfo AuthenticodeSignerInfo + { + get + { + return _authenticodeSignerInfo; + } + } + + // + // Privates. + // + private void VerifyLicense(CmiManifestVerifyFlags verifyFlags, bool oldFormat) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("asm2", AssemblyV2NamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + nsm.AddNamespace("msrel", MSRelNamespaceUri); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + + // We are done if no license. + XmlElement licenseNode = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license", nsm) as XmlElement; + if (licenseNode == null) + { + return; + } + + // Make sure this license is for this manifest. + VerifyAssemblyIdentity(nsm); + + // Found a license, so instantiate signer info property. + _authenticodeSignerInfo = new CmiAuthenticodeSignerInfo(Win32.TRUST_E_FAIL); + + unsafe + { + byte[] licenseXml = Encoding.UTF8.GetBytes(licenseNode.OuterXml); + fixed (byte* pbLicense = licenseXml) + { + Win32.AXL_SIGNER_INFO signerInfo = new Win32.AXL_SIGNER_INFO(); + signerInfo.cbSize = (uint)Marshal.SizeOf(typeof(Win32.AXL_SIGNER_INFO)); + Win32.AXL_TIMESTAMPER_INFO timestamperInfo = new Win32.AXL_TIMESTAMPER_INFO(); + timestamperInfo.cbSize = (uint)Marshal.SizeOf(typeof(Win32.AXL_TIMESTAMPER_INFO)); + Win32.CRYPT_DATA_BLOB licenseBlob = new Win32.CRYPT_DATA_BLOB(); + IntPtr pvLicense = new IntPtr(pbLicense); + licenseBlob.cbData = (uint)licenseXml.Length; + licenseBlob.pbData = pvLicense; + + int hr = Win32.CertVerifyAuthenticodeLicense(ref licenseBlob, (uint)verifyFlags, ref signerInfo, ref timestamperInfo); + if (Win32.TRUST_E_NOSIGNATURE != (int)signerInfo.dwError) + { + _authenticodeSignerInfo = new CmiAuthenticodeSignerInfo(signerInfo, timestamperInfo); + } + + Win32.CertFreeAuthenticodeSignerInfo(ref signerInfo); + Win32.CertFreeAuthenticodeTimestamperInfo(ref timestamperInfo); + + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + } + } + + if (!oldFormat) + // Make sure we have the intended Authenticode signer. + VerifyPublisherIdentity(nsm); + } + + // can be used with sha1 or sha2 + // logic is copied from the "isolation library" in NDP\iso_whid\ds\security\cryptoapi\pkisign\msaxlapi\mansign.cpp + private void VerifyLicenseNew(CmiManifestVerifyFlags verifyFlags, bool oldFormat) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("asm2", AssemblyV2NamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + nsm.AddNamespace("msrel", MSRelNamespaceUri); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + + // We are done if no license. + XmlElement licenseNode = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license", nsm) as XmlElement; + if (licenseNode == null) + { + return; + } + + // Make sure this license is for this manifest. + VerifyAssemblyIdentity(nsm); + + // Found a license, so instantiate signer info property. + _authenticodeSignerInfo = new CmiAuthenticodeSignerInfo(Win32.TRUST_E_FAIL); + + // Find the license's signature + XmlElement signatureNode = licenseNode.SelectSingleNode("//r:issuer/ds:Signature", nsm) as XmlElement; + if (signatureNode == null) + { + throw new CryptographicException(Win32.TRUST_E_NOSIGNATURE); + } + + // Make sure it is indeed an Authenticode signature, and it is an enveloped signature. + // Then make sure the transforms are valid. + VerifySignatureForm(signatureNode, "AuthenticodeSignature", nsm); + + // Now read the enveloped license signature. + XmlDocument licenseDom = new XmlDocument(); + licenseDom.LoadXml(licenseNode.OuterXml); + signatureNode = licenseDom.SelectSingleNode("//r:issuer/ds:Signature", nsm) as XmlElement; + + ManifestSignedXml2 signedXml = new ManifestSignedXml2(licenseDom); + signedXml.LoadXml(signatureNode); + if (_useSha256) + { + signedXml.SignedInfo.SignatureMethod = Sha256SignatureMethodUri; + } + + // Check the signature + if (!signedXml.CheckSignature()) + { + _authenticodeSignerInfo = null; + throw new CryptographicException(Win32.TRUST_E_CERT_SIGNATURE); + } + + X509Certificate2 signingCertificate = GetSigningCertificate(signedXml, nsm); + + // First make sure certificate is not explicitly disallowed. + X509Store store = new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + X509Certificate2Collection storedCertificates = null; + try + { + storedCertificates = (X509Certificate2Collection)store.Certificates; + if (storedCertificates == null) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_FAIL; + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + if (storedCertificates.Contains(signingCertificate)) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_EXPLICIT_DISTRUST; + throw new CryptographicException(Win32.TRUST_E_EXPLICIT_DISTRUST); + } + } + finally + { + store.Close(); + } + + // prepare information for the TrustManager to display + string hash; + string description; + string url; + if (!GetManifestInformation(licenseNode, nsm, out hash, out description, out url)) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_SUBJECT_FORM_UNKNOWN; + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + _authenticodeSignerInfo.Hash = hash; + _authenticodeSignerInfo.Description = description; + _authenticodeSignerInfo.DescriptionUrl = url; + + // read the timestamp from the manifest + DateTime verificationTime; + bool isTimestamped = VerifySignatureTimestamp(signatureNode, nsm, out verificationTime); + bool isLifetimeSigning = false; + if (isTimestamped) + { + isLifetimeSigning = ((verifyFlags & CmiManifestVerifyFlags.LifetimeSigning) == CmiManifestVerifyFlags.LifetimeSigning); + if (!isLifetimeSigning) + { + isLifetimeSigning = GetLifetimeSigning(signingCertificate); + } + } + + // Retrieve the Authenticode policy settings from registry. + uint policies = GetAuthenticodePolicies(); + + X509Chain chain = new X509Chain(); // use the current user profile + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + if ((CmiManifestVerifyFlags.RevocationCheckEndCertOnly & verifyFlags) == CmiManifestVerifyFlags.RevocationCheckEndCertOnly) + { + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly; + } + else if ((CmiManifestVerifyFlags.RevocationCheckEntireChain & verifyFlags) == CmiManifestVerifyFlags.RevocationCheckEntireChain) + { + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; + } + else if (((CmiManifestVerifyFlags.RevocationNoCheck & verifyFlags) == CmiManifestVerifyFlags.RevocationNoCheck) || + ((Win32.WTPF_IGNOREREVOKATION & policies) == Win32.WTPF_IGNOREREVOKATION)) + { + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + } + + chain.ChainPolicy.VerificationTime = verificationTime; // local time + if (isTimestamped && isLifetimeSigning) + { + chain.ChainPolicy.ApplicationPolicy.Add(new Oid(Win32.szOID_KP_LIFETIME_SIGNING)); + } + + chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0); + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; // don't ignore anything + + bool chainIsValid = chain.Build(signingCertificate); + + if (!chainIsValid) + { +#if DEBUG + X509ChainStatus[] statuses = chain.ChainStatus; + foreach (X509ChainStatus status in statuses) + { + System.Diagnostics.Debug.WriteLine("flag = " + status.Status + " " + status.StatusInformation); + } +#endif + AuthenticodeSignerInfo.ErrorCode = Win32.TRUST_E_SUBJECT_NOT_TRUSTED; + throw new CryptographicException(Win32.TRUST_E_SUBJECT_NOT_TRUSTED); + } + + // package information for the trust manager + _authenticodeSignerInfo.SignerChain = chain; + + store = new X509Store(StoreName.TrustedPublisher, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + try + { + storedCertificates = (X509Certificate2Collection)store.Certificates; + if (storedCertificates == null) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_FAIL; + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + if (!storedCertificates.Contains(signingCertificate)) + { + AuthenticodeSignerInfo.ErrorCode = Win32.TRUST_E_SUBJECT_NOT_TRUSTED; + throw new CryptographicException(Win32.TRUST_E_SUBJECT_NOT_TRUSTED); + } + } + finally + { + store.Close(); + } + + // Verify Certificate publisher name + XmlElement subjectNode = licenseNode.SelectSingleNode("r:grant/as:AuthenticodePublisher/as:X509SubjectName", nsm) as XmlElement; + if (subjectNode == null || String.Compare(signingCertificate.Subject, subjectNode.InnerText, StringComparison.Ordinal) != 0) + { + AuthenticodeSignerInfo.ErrorCode = Win32.TRUST_E_CERT_SIGNATURE; + throw new CryptographicException(Win32.TRUST_E_CERT_SIGNATURE); + } + + if (!oldFormat) + // Make sure we have the intended Authenticode signer. + VerifyPublisherIdentity(nsm); + } + + private X509Certificate2 GetSigningCertificate(ManifestSignedXml2 signedXml, XmlNamespaceManager nsm) + { + X509Certificate2 signingCertificate = null; + + KeyInfo keyInfo = signedXml.KeyInfo; + KeyInfoX509Data kiX509 = null; + RSAKeyValue keyValue = null; + foreach (KeyInfoClause kic in keyInfo) + { + if (keyValue == null) + { + keyValue = kic as RSAKeyValue; + if (keyValue == null) + { + break; + } + } + + if (kiX509 == null) + { + kiX509 = kic as KeyInfoX509Data; + } + + if (keyValue != null && kiX509 != null) + { + break; + } + } + + if (keyValue == null || kiX509 == null) + { + // no X509Certificate KeyInfoClause + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_SUBJECT_FORM_UNKNOWN; + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // get public key from signing keyInfo + byte[] signingPublicKey = null; + RSACryptoServiceProvider rsaProvider = keyValue.Key as RSACryptoServiceProvider; + if (rsaProvider != null) + { + signingPublicKey = rsaProvider.ExportCspBlob(false); + } + if (signingPublicKey == null) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_CERT_SIGNATURE; + throw new CryptographicException(Win32.TRUST_E_CERT_SIGNATURE); + } + + // enumerate all certificates in x509Data searching for the one whose public key is used in + foreach (X509Certificate2 certificate in kiX509.Certificates) + { + if (certificate == null) + { + continue; + } + + bool certificateAuthority = false; + foreach (X509Extension extention in certificate.Extensions) + { + X509BasicConstraintsExtension basicExtention = extention as X509BasicConstraintsExtension; + if (basicExtention != null) + { + certificateAuthority = basicExtention.CertificateAuthority; + if (certificateAuthority) + { + break; + } + } + } + + if (certificateAuthority) + { + // Ignore certs that have "Subject Type=CA" in basic contraints + continue; + } + + RSACryptoServiceProvider p = (RSACryptoServiceProvider)certificate.PublicKey.Key; + byte[] certificatePublicKey = p.ExportCspBlob(false); + bool noMatch = false; + if (signingPublicKey.Length == certificatePublicKey.Length) + { + for (int i = 0; i < signingPublicKey.Length; i++) + { + if (signingPublicKey[i] != certificatePublicKey[i]) + { + noMatch = true; + break; + } + } + if (!noMatch) + { + signingCertificate = certificate; + break; + } + } + } + + if (signingCertificate == null) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_CERT_SIGNATURE; + throw new CryptographicException(Win32.TRUST_E_CERT_SIGNATURE); + } + return signingCertificate; + } + + private bool VerifySignatureForm(XmlElement signatureNode, string signatureKind, XmlNamespaceManager nsm) + { + bool oldFormat = false; + string snIdName = "Id"; + if (!signatureNode.HasAttribute(snIdName)) + { + snIdName = "id"; + if (!signatureNode.HasAttribute(snIdName)) + { + snIdName = "ID"; + if (!signatureNode.HasAttribute(snIdName)) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + } + } + + string snIdValue = signatureNode.GetAttribute(snIdName); + if (snIdValue == null || + String.Compare(snIdValue, signatureKind, StringComparison.Ordinal) != 0) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Make sure it is indeed an enveloped signature. + bool validFormat = false; + XmlNodeList referenceNodes = signatureNode.SelectNodes("ds:SignedInfo/ds:Reference", nsm); + foreach (XmlNode referenceNode in referenceNodes) + { + XmlElement reference = referenceNode as XmlElement; + if (reference != null && reference.HasAttribute("URI")) + { + string uriValue = reference.GetAttribute("URI"); + if (uriValue != null) + { + // We expect URI="" (empty URI value which means to hash the entire document). + if (uriValue.Length == 0) + { + XmlNode transformsNode = reference.SelectSingleNode("ds:Transforms", nsm); + if (transformsNode == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Make sure the transforms are what we expected. + XmlNodeList transforms = transformsNode.SelectNodes("ds:Transform", nsm); + if (transforms.Count < 2) + { + // We expect at least: + // + // + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + bool c14 = false; + bool enveloped = false; + for (int i = 0; i < transforms.Count; i++) + { + XmlElement transform = transforms[i] as XmlElement; + string algorithm = transform.GetAttribute("Algorithm"); + if (algorithm == null) + { + break; + } + else if (String.Compare(algorithm, SignedXml.XmlDsigExcC14NTransformUrl, StringComparison.Ordinal) != 0) + { + c14 = true; + if (enveloped) + { + validFormat = true; + break; + } + } + else if (String.Compare(algorithm, SignedXml.XmlDsigEnvelopedSignatureTransformUrl, StringComparison.Ordinal) != 0) + { + enveloped = true; + if (c14) + { + validFormat = true; + break; + } + } + } + } + else if (String.Compare(uriValue, "#StrongNameKeyInfo", StringComparison.Ordinal) == 0) + { + oldFormat = true; + + XmlNode transformsNode = referenceNode.SelectSingleNode("ds:Transforms", nsm); + if (transformsNode == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Make sure the transforms are what we expected. + XmlNodeList transforms = transformsNode.SelectNodes("ds:Transform", nsm); + if (transforms.Count < 1) + { + // We expect at least: + // + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + for (int i = 0; i < transforms.Count; i++) + { + XmlElement transform = transforms[i] as XmlElement; + string algorithm = transform.GetAttribute("Algorithm"); + if (algorithm == null) + { + break; + } + else if (String.Compare(algorithm, SignedXml.XmlDsigExcC14NTransformUrl, StringComparison.Ordinal) != 0) + { + validFormat = true; + break; + } + } + } + } + } + } + + if (!validFormat) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + return oldFormat; + } + + private bool GetManifestInformation(XmlElement licenseNode, XmlNamespaceManager nsm, out string hash, out string description, out string url) + { + hash = ""; + description = ""; + url = ""; + + XmlElement manifestInformation = licenseNode.SelectSingleNode("r:grant/as:ManifestInformation", nsm) as XmlElement; + if (manifestInformation == null) + { + return false; + } + if (!manifestInformation.HasAttribute("Hash")) + { + return false; + } + + hash = manifestInformation.GetAttribute("Hash"); + if (string.IsNullOrEmpty(hash)) + { + return false; + } + + foreach (char c in hash) + { + if (0xFF == HexToByte(c)) + { + return false; + } + } + + if (manifestInformation.HasAttribute("Description")) + { + description = manifestInformation.GetAttribute("Description"); + } + + if (manifestInformation.HasAttribute("Url")) + { + url = manifestInformation.GetAttribute("Url"); + } + + return true; + } + + private bool VerifySignatureTimestamp(XmlElement signatureNode, XmlNamespaceManager nsm, out DateTime verificationTime) + { + verificationTime = DateTime.Now; + + XmlElement node = signatureNode.SelectSingleNode("ds:Object/as:Timestamp", nsm) as XmlElement; + if (node != null) + { + string encodedMessage = node.InnerText; + + if (!string.IsNullOrEmpty(encodedMessage)) + { + byte[] base64DecodedMessage = null; + try + { + base64DecodedMessage = Convert.FromBase64String(encodedMessage); + } + catch (FormatException) + { + _authenticodeSignerInfo.ErrorCode = Win32.TRUST_E_TIME_STAMP; + throw new CryptographicException(Win32.TRUST_E_TIME_STAMP); + } + if (base64DecodedMessage != null) + { + // Create a new, nondetached SignedCms message. + SignedCms signedCms = new SignedCms(); + signedCms.Decode(base64DecodedMessage); + + // Verify the signature without validating the + // certificate. + signedCms.CheckSignature(true); + + byte[] signingTime = null; + CryptographicAttributeObjectCollection caos = signedCms.SignerInfos[0].SignedAttributes; + foreach (CryptographicAttributeObject cao in caos) + { + if (0 == string.Compare(cao.Oid.Value, Win32.szOID_RSA_signingTime, StringComparison.Ordinal)) + { + foreach (AsnEncodedData d in cao.Values) + { + if (0 == string.Compare(d.Oid.Value, Win32.szOID_RSA_signingTime, StringComparison.Ordinal)) + { + signingTime = d.RawData; + Pkcs9SigningTime time = new Pkcs9SigningTime(signingTime); + verificationTime = time.SigningTime; + return true; + } + } + } + } + } + } + } + + return false; + } + + private bool GetLifetimeSigning(X509Certificate2 signingCertificate) + { + foreach (X509Extension extension in signingCertificate.Extensions) + { + X509EnhancedKeyUsageExtension ekuExtention = extension as X509EnhancedKeyUsageExtension; + if (ekuExtention != null) + { + OidCollection oids = ekuExtention.EnhancedKeyUsages; + foreach (Oid oid in oids) + { + if (0 == string.Compare(Win32.szOID_KP_LIFETIME_SIGNING, oid.Value, StringComparison.Ordinal)) + { + return true; + } + } + } + } + return false; + } + + // Retrieve the Authenticode policy settings from registry. + // Isolation library was ignoring missing or inaccessible key/value errors + private uint GetAuthenticodePolicies() + { + uint policies = 0; + + try + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(wintrustPolicyFlagsRegPath); + if (key != null) + { + RegistryValueKind kind = key.GetValueKind(wintrustPolicyFlagsRegName); + if (kind == RegistryValueKind.DWord || kind == RegistryValueKind.Binary) + { + object value = key.GetValue(wintrustPolicyFlagsRegName); + if (value != null) + { + policies = Convert.ToUInt32(value); + } + } + key.Close(); + } + } + catch (System.Security.SecurityException) + { + } + catch (ObjectDisposedException) + { + } + catch (UnauthorizedAccessException) + { + } + catch (IOException) + { + } + return policies; + } + + private XmlElement ExtractPrincipalFromManifest() + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + XmlNode assemblyIdentityNode = _manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm); + if (assemblyIdentityNode == null) + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + return assemblyIdentityNode as XmlElement; + } + + private void VerifyAssemblyIdentity(XmlNamespaceManager nsm) + { + XmlElement assemblyIdentity = _manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + XmlElement principal = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license/r:grant/as:ManifestInformation/as:assemblyIdentity", nsm) as XmlElement; + + if (assemblyIdentity == null || principal == null || + !assemblyIdentity.HasAttributes || !principal.HasAttributes) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + XmlAttributeCollection asmIdAttrs = assemblyIdentity.Attributes; + + if (asmIdAttrs.Count == 0 || asmIdAttrs.Count != principal.Attributes.Count) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + foreach (XmlAttribute asmIdAttr in asmIdAttrs) + { + if (!principal.HasAttribute(asmIdAttr.LocalName) || + asmIdAttr.Value != principal.GetAttribute(asmIdAttr.LocalName)) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + } + + VerifyHash(nsm); + } + + private void VerifyPublisherIdentity(XmlNamespaceManager nsm) + { + // Nothing to do if no signature. + if (_authenticodeSignerInfo.ErrorCode == Win32.TRUST_E_NOSIGNATURE) + { + return; + } + + X509Certificate2 signerCert = _authenticodeSignerInfo.SignerChain.ChainElements[0].Certificate; + + // Find the publisherIdentity element. + XmlElement publisherIdentity = _manifestDom.SelectSingleNode("asm:assembly/asm2:publisherIdentity", nsm) as XmlElement; + if (publisherIdentity == null || !publisherIdentity.HasAttributes) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Get name and issuerKeyHash attribute values. + if (!publisherIdentity.HasAttribute("name") || !publisherIdentity.HasAttribute("issuerKeyHash")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + string publisherName = publisherIdentity.GetAttribute("name"); + string publisherIssuerKeyHash = publisherIdentity.GetAttribute("issuerKeyHash"); + + // Calculate the issuer key hash. + IntPtr pIssuerKeyHash = new IntPtr(); + int hr = Win32._AxlGetIssuerPublicKeyHash(signerCert.Handle, ref pIssuerKeyHash); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + string issuerKeyHash = Marshal.PtrToStringUni(pIssuerKeyHash); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pIssuerKeyHash); + + // Make sure name and issuerKeyHash match. + if (String.Compare(publisherName, signerCert.SubjectName.Name, StringComparison.Ordinal) != 0 || + String.Compare(publisherIssuerKeyHash, issuerKeyHash, StringComparison.Ordinal) != 0) + { + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + } + + private void VerifyHash(XmlNamespaceManager nsm) + { + XmlDocument manifestDom = new XmlDocument(); + // We always preserve white space as Fusion XML engine always preserve white space. + manifestDom.PreserveWhitespace = true; + manifestDom = (XmlDocument)_manifestDom.Clone(); + + XmlElement manifestInformation = manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/msrel:RelData/r:license/r:grant/as:ManifestInformation", nsm) as XmlElement; + if (manifestInformation == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + if (!manifestInformation.HasAttribute("Hash")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + string hash = manifestInformation.GetAttribute("Hash"); + if (hash == null || hash.Length == 0) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Now compute the hash for the manifest without the entire SN + // signature element. + + // First remove the Signture element from the DOM. + XmlElement dsElement = manifestDom.SelectSingleNode("asm:assembly/ds:Signature", nsm) as XmlElement; + if (dsElement == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + dsElement.ParentNode.RemoveChild(dsElement); + + // Now compute the hash from the manifest, without the Signature element. + byte[] hashBytes = HexStringToBytes(manifestInformation.GetAttribute("Hash")); + byte[] computedHashBytes = ComputeHashFromManifest(manifestDom, _useSha256); + + // Do they match? + if (hashBytes.Length == 0 || hashBytes.Length != computedHashBytes.Length) + { + byte[] computedOldHashBytes = ComputeHashFromManifest(manifestDom, true, _useSha256); + + // Do they match? + if (hashBytes.Length == 0 || hashBytes.Length != computedOldHashBytes.Length) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + for (int i = 0; i < hashBytes.Length; i++) + { + if (hashBytes[i] != computedOldHashBytes[i]) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + } + } + + for (int i = 0; i < hashBytes.Length; i++) + { + if (hashBytes[i] != computedHashBytes[i]) + { +#if (true) // BUGBUG: Remove before RTM once old format support is no longer needed. + byte[] computedOldHashBytes = ComputeHashFromManifest(manifestDom, true, _useSha256); + + // Do they match? + if (hashBytes.Length == 0 || hashBytes.Length != computedOldHashBytes.Length) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + for (i = 0; i < hashBytes.Length; i++) + { + if (hashBytes[i] != computedOldHashBytes[i]) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + } +#else + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); +#endif + } + } + } + + private string VerifyPublicKeyToken() + { + XmlNamespaceManager nsm = new XmlNamespaceManager(_manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlElement snModulus = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Modulus", nsm) as XmlElement; + XmlElement snExponent = _manifestDom.SelectSingleNode("asm:assembly/ds:Signature/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Exponent", nsm) as XmlElement; + + if (snModulus == null || snExponent == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + byte[] modulus = Encoding.UTF8.GetBytes(snModulus.InnerXml); + byte[] exponent = Encoding.UTF8.GetBytes(snExponent.InnerXml); + + string tokenString = GetPublicKeyToken(_manifestDom); + byte[] publicKeyToken = HexStringToBytes(tokenString); + byte[] computedPublicKeyToken; + + unsafe + { + fixed (byte* pbModulus = modulus) + { + fixed (byte* pbExponent = exponent) + { + Win32.CRYPT_DATA_BLOB modulusBlob = new Win32.CRYPT_DATA_BLOB(); + Win32.CRYPT_DATA_BLOB exponentBlob = new Win32.CRYPT_DATA_BLOB(); + IntPtr pComputedToken = new IntPtr(); + + modulusBlob.cbData = (uint)modulus.Length; + modulusBlob.pbData = new IntPtr(pbModulus); + exponentBlob.cbData = (uint)exponent.Length; + exponentBlob.pbData = new IntPtr(pbExponent); + + // Now compute the public key token. + int hr = Win32._AxlRSAKeyValueToPublicKeyToken(ref modulusBlob, ref exponentBlob, ref pComputedToken); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + computedPublicKeyToken = HexStringToBytes(Marshal.PtrToStringUni(pComputedToken)); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pComputedToken); + } + } + } + + // Do they match? + if (publicKeyToken.Length == 0 || publicKeyToken.Length != computedPublicKeyToken.Length) + { + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + + for (int i = 0; i < publicKeyToken.Length; i++) + { + if (publicKeyToken[i] != computedPublicKeyToken[i]) + { + throw new CryptographicException(Win32.TRUST_E_FAIL); + } + } + + return tokenString; + } + + // + // Statics. + // + private static void InsertPublisherIdentity(XmlDocument manifestDom, X509Certificate2 signerCert) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("asm2", AssemblyV2NamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlElement assembly = manifestDom.SelectSingleNode("asm:assembly", nsm) as XmlElement; + XmlElement assemblyIdentity = manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + if (assemblyIdentity == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + // Reuse existing node if exists + XmlElement publisherIdentity = manifestDom.SelectSingleNode("asm:assembly/asm2:publisherIdentity", nsm) as XmlElement; + if (publisherIdentity == null) + { + // create new if not exist + publisherIdentity = manifestDom.CreateElement("publisherIdentity", AssemblyV2NamespaceUri); + } + // Get the issuer's public key blob hash. + IntPtr pIssuerKeyHash = new IntPtr(); + int hr = Win32._AxlGetIssuerPublicKeyHash(signerCert.Handle, ref pIssuerKeyHash); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + string issuerKeyHash = Marshal.PtrToStringUni(pIssuerKeyHash); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pIssuerKeyHash); + + publisherIdentity.SetAttribute("name", signerCert.SubjectName.Name); + publisherIdentity.SetAttribute("issuerKeyHash", issuerKeyHash); + + XmlElement signature = manifestDom.SelectSingleNode("asm:assembly/ds:Signature", nsm) as XmlElement; + if (signature != null) + { + assembly.InsertBefore(publisherIdentity, signature); + } + else + { + assembly.AppendChild(publisherIdentity); + } + } + + private static void RemoveExistingSignature(XmlDocument manifestDom) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + XmlNode signatureNode = manifestDom.SelectSingleNode("asm:assembly/ds:Signature", nsm); + if (signatureNode != null) + signatureNode.ParentNode.RemoveChild(signatureNode); + } + + /// + /// The reason you need provider type 24, is because that’s the only RSA provider type that supports SHA-2 operations. (For instance, PROV_RSA_FULL does not support SHA-2). + /// As for official guidance – I’m not sure of any. For workarounds though, if you’re using the Microsoft software CSPs, they share the underlying key store. You can get the key container name from your RSA object, then open up a new RSA object with the same key container name but with PROV_RSA_AES. At that point, you should be able to use SHA-2 algorithms. + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Cryptographic.Standard", "CA5358:RSAProviderNeeds2048bitKey", Justification = "SHA1 is retained for compatibility reasons as an option in VisualStudio signing page and consequently in the trust manager, default is SHA2.")] + internal static RSACryptoServiceProvider GetFixedRSACryptoServiceProvider(RSACryptoServiceProvider oldCsp, bool useSha256) + { + if (!useSha256) + { + return oldCsp; + } + + const int PROV_RSA_AES = 24; // CryptoApi provider type for an RSA provider supporting sha-256 digital signatures + CspParameters csp = new CspParameters(); + csp.ProviderType = PROV_RSA_AES; + csp.KeyContainerName = oldCsp.CspKeyContainerInfo.KeyContainerName; + csp.KeyNumber = (int)oldCsp.CspKeyContainerInfo.KeyNumber; + if (oldCsp.CspKeyContainerInfo.MachineKeyStore) + { + csp.Flags = CspProviderFlags.UseMachineKeyStore; + } + RSACryptoServiceProvider fixedRsa = new RSACryptoServiceProvider(csp); + + return fixedRsa; + } + + private static void ReplacePublicKeyToken(XmlDocument manifestDom, AsymmetricAlgorithm snKey, bool useSha256) + { + // Make sure we can find the publicKeyToken attribute. + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + XmlElement assemblyIdentity = manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + if (assemblyIdentity == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + if (!assemblyIdentity.HasAttribute("publicKeyToken")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + byte[] cspPublicKeyBlob = (GetFixedRSACryptoServiceProvider((RSACryptoServiceProvider)snKey, useSha256)).ExportCspBlob(false); + if (cspPublicKeyBlob == null || cspPublicKeyBlob.Length == 0) + { + throw new CryptographicException(Win32.NTE_BAD_KEY); + } + + // Now compute the public key token. + unsafe + { + fixed (byte* pbPublicKeyBlob = cspPublicKeyBlob) + { + Win32.CRYPT_DATA_BLOB publicKeyBlob = new Win32.CRYPT_DATA_BLOB(); + publicKeyBlob.cbData = (uint)cspPublicKeyBlob.Length; + publicKeyBlob.pbData = new IntPtr(pbPublicKeyBlob); + IntPtr pPublicKeyToken = new IntPtr(); + + int hr = Win32._AxlPublicKeyBlobToPublicKeyToken(ref publicKeyBlob, ref pPublicKeyToken); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + + string publicKeyToken = Marshal.PtrToStringUni(pPublicKeyToken); + Win32.HeapFree(Win32.GetProcessHeap(), 0, pPublicKeyToken); + + assemblyIdentity.SetAttribute("publicKeyToken", publicKeyToken); + } + } + } + + private static string GetPublicKeyToken(XmlDocument manifestDom) + { + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + + XmlElement assemblyIdentity = manifestDom.SelectSingleNode("asm:assembly/asm:assemblyIdentity", nsm) as XmlElement; + + if (assemblyIdentity == null || !assemblyIdentity.HasAttribute("publicKeyToken")) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + return assemblyIdentity.GetAttribute("publicKeyToken"); + } + + private static byte[] ComputeHashFromManifest(XmlDocument manifestDom, bool useSha256) + { + return ComputeHashFromManifest(manifestDom, false, useSha256); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Cryptographic.Standard", "CA5354:SHA1CannotBeUsed", Justification = "SHA1 is retained for compatibility reasons as an option in VisualStudio signing page and consequently in the trust manager, default is SHA2.")] + private static byte[] ComputeHashFromManifest(XmlDocument manifestDom, bool oldFormat, bool useSha256) + { + if (oldFormat) + { + XmlDsigExcC14NTransform exc = new XmlDsigExcC14NTransform(); + exc.LoadInput(manifestDom); + + if (useSha256) + { + using (SHA256CryptoServiceProvider sha2 = new SHA256CryptoServiceProvider()) + { + byte[] hash = sha2.ComputeHash(exc.GetOutput() as MemoryStream); + if (hash == null) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + return hash; + } + } + else + { + using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider()) + { + byte[] hash = sha1.ComputeHash(exc.GetOutput() as MemoryStream); + if (hash == null) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + return hash; + } + } + } + else + { + // Since the DOM given to us is not guaranteed to be normalized, + // we need to normalize it ourselves. Also, we always preserve + // white space as Fusion XML engine always preserve white space. + XmlDocument normalizedDom = new XmlDocument(); + normalizedDom.PreserveWhitespace = true; + + // Normalize the document + using (TextReader stringReader = new StringReader(manifestDom.OuterXml)) + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Parse; + XmlReader reader = XmlReader.Create(stringReader, settings, manifestDom.BaseURI); + normalizedDom.Load(reader); + } + + XmlDsigExcC14NTransform exc = new XmlDsigExcC14NTransform(); + exc.LoadInput(normalizedDom); + + if (useSha256) + { + using (SHA256CryptoServiceProvider sha2 = new SHA256CryptoServiceProvider()) + { + byte[] hash = sha2.ComputeHash(exc.GetOutput() as MemoryStream); + if (hash == null) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + return hash; + } + } + else + { + using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider()) + { + byte[] hash = sha1.ComputeHash(exc.GetOutput() as MemoryStream); + if (hash == null) + { + throw new CryptographicException(Win32.TRUST_E_BAD_DIGEST); + } + + return hash; + } + } + } + } + + private const string AssemblyNamespaceUri = "urn:schemas-microsoft-com:asm.v1"; + private const string AssemblyV2NamespaceUri = "urn:schemas-microsoft-com:asm.v2"; + private const string MSRelNamespaceUri = "http://schemas.microsoft.com/windows/rel/2005/reldata"; + private const string LicenseNamespaceUri = "urn:mpeg:mpeg21:2003:01-REL-R-NS"; + private const string AuthenticodeNamespaceUri = "http://schemas.microsoft.com/windows/pki/2005/Authenticode"; + private const string licenseTemplate = "" + + @"" + + @"" + + @"" + + @"" + + @"" + + @"" + + @"CN=dummy" + + @"" + + @""; + + private static XmlDocument CreateLicenseDom(CmiManifestSigner2 signer, XmlElement principal, byte[] hash) + { + XmlDocument licenseDom = new XmlDocument(); + licenseDom.PreserveWhitespace = true; + licenseDom.LoadXml(licenseTemplate); + XmlNamespaceManager nsm = new XmlNamespaceManager(licenseDom.NameTable); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + XmlElement assemblyIdentityNode = licenseDom.SelectSingleNode("r:license/r:grant/as:ManifestInformation/as:assemblyIdentity", nsm) as XmlElement; + assemblyIdentityNode.RemoveAllAttributes(); + foreach (XmlAttribute attribute in principal.Attributes) + { + assemblyIdentityNode.SetAttribute(attribute.Name, attribute.Value); + } + + XmlElement manifestInformationNode = licenseDom.SelectSingleNode("r:license/r:grant/as:ManifestInformation", nsm) as XmlElement; + + manifestInformationNode.SetAttribute("Hash", hash.Length == 0 ? "" : BytesToHexString(hash, 0, hash.Length)); + manifestInformationNode.SetAttribute("Description", signer.Description == null ? "" : signer.Description); + manifestInformationNode.SetAttribute("Url", signer.DescriptionUrl == null ? "" : signer.DescriptionUrl); + + XmlElement authenticodePublisherNode = licenseDom.SelectSingleNode("r:license/r:grant/as:AuthenticodePublisher/as:X509SubjectName", nsm) as XmlElement; + authenticodePublisherNode.InnerText = signer.Certificate.SubjectName.Name; + + return licenseDom; + } + + private static void AuthenticodeSignLicenseDom(XmlDocument licenseDom, CmiManifestSigner2 signer, string timeStampUrl, bool useSha256) + { + // Make sure it is RSA, as this is the only one Fusion will support. + if (signer.Certificate.PublicKey.Key.GetType() != typeof(RSACryptoServiceProvider)) + { + throw new NotSupportedException(); + } + + if (signer.Certificate.PrivateKey.GetType() != typeof(RSACryptoServiceProvider)) + { + throw new NotSupportedException(); + } + + // Setup up XMLDSIG engine. + ManifestSignedXml2 signedXml = new ManifestSignedXml2(licenseDom); + signedXml.SigningKey = GetFixedRSACryptoServiceProvider(signer.Certificate.PrivateKey as RSACryptoServiceProvider, useSha256); + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + if (signer.UseSha256) + signedXml.SignedInfo.SignatureMethod = Sha256SignatureMethodUri; + + // Add the key information. + signedXml.KeyInfo.AddClause(new RSAKeyValue(GetFixedRSACryptoServiceProvider(signer.Certificate.PrivateKey as RSACryptoServiceProvider, useSha256) as RSA)); + signedXml.KeyInfo.AddClause(new KeyInfoX509Data(signer.Certificate, signer.IncludeOption)); + + // Add the enveloped reference. + Reference reference = new Reference(); + reference.Uri = ""; + if (signer.UseSha256) + reference.DigestMethod = Sha256DigestMethod; + + // Add an enveloped and an Exc-C14N transform. + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlLicenseTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + + // Add the reference. + signedXml.AddReference(reference); + + // Compute the signature. + signedXml.ComputeSignature(); + + // Get the XML representation + XmlElement xmlDigitalSignature = signedXml.GetXml(); + xmlDigitalSignature.SetAttribute("Id", "AuthenticodeSignature"); + + // Insert the signature node under the issuer element. + XmlNamespaceManager nsm = new XmlNamespaceManager(licenseDom.NameTable); + nsm.AddNamespace("r", LicenseNamespaceUri); + XmlElement issuerNode = licenseDom.SelectSingleNode("r:license/r:issuer", nsm) as XmlElement; + issuerNode.AppendChild(licenseDom.ImportNode(xmlDigitalSignature, true)); + + // Time stamp it if requested. + if (timeStampUrl != null && timeStampUrl.Length != 0) + { + TimestampSignedLicenseDom(licenseDom, timeStampUrl); + } + + // Wrap it inside a RelData element. + licenseDom.DocumentElement.ParentNode.InnerXml = "" + + licenseDom.OuterXml + ""; + } + + private static void TimestampSignedLicenseDom(XmlDocument licenseDom, string timeStampUrl) + { + Win32.CRYPT_DATA_BLOB timestampBlob = new Win32.CRYPT_DATA_BLOB(); + + XmlNamespaceManager nsm = new XmlNamespaceManager(licenseDom.NameTable); + nsm.AddNamespace("r", LicenseNamespaceUri); + nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl); + nsm.AddNamespace("as", AuthenticodeNamespaceUri); + + byte[] licenseXml = Encoding.UTF8.GetBytes(licenseDom.OuterXml); + + unsafe + { + fixed (byte* pbLicense = licenseXml) + { + Win32.CRYPT_DATA_BLOB licenseBlob = new Win32.CRYPT_DATA_BLOB(); + IntPtr pvLicense = new IntPtr(pbLicense); + licenseBlob.cbData = (uint)licenseXml.Length; + licenseBlob.pbData = pvLicense; + + int hr = Win32.CertTimestampAuthenticodeLicense(ref licenseBlob, timeStampUrl, ref timestampBlob); + if (hr != Win32.S_OK) + { + throw new CryptographicException(hr); + } + } + } + + byte[] timestampSignature = new byte[timestampBlob.cbData]; + Marshal.Copy(timestampBlob.pbData, timestampSignature, 0, timestampSignature.Length); + Win32.HeapFree(Win32.GetProcessHeap(), 0, timestampBlob.pbData); + + XmlElement asTimestamp = licenseDom.CreateElement("as", "Timestamp", AuthenticodeNamespaceUri); + asTimestamp.InnerText = Encoding.UTF8.GetString(timestampSignature); + + XmlElement dsObject = licenseDom.CreateElement("Object", SignedXml.XmlDsigNamespaceUrl); + dsObject.AppendChild(asTimestamp); + + XmlElement signatureNode = licenseDom.SelectSingleNode("r:license/r:issuer/ds:Signature", nsm) as XmlElement; + signatureNode.AppendChild(dsObject); + } + + private static void StrongNameSignManifestDom(XmlDocument manifestDom, XmlDocument licenseDom, CmiManifestSigner2 signer, bool useSha256) + { + RSA snKey = signer.StrongNameKey as RSA; + + // Make sure it is RSA, as this is the only one Fusion will support. + if (snKey == null) + { + throw new NotSupportedException(); + } + + // Setup namespace manager. + XmlNamespaceManager nsm = new XmlNamespaceManager(manifestDom.NameTable); + nsm.AddNamespace("asm", AssemblyNamespaceUri); + + // Get to root element. + XmlElement signatureParent = manifestDom.SelectSingleNode("asm:assembly", nsm) as XmlElement; + if (signatureParent == null) + { + throw new CryptographicException(Win32.TRUST_E_SUBJECT_FORM_UNKNOWN); + } + + if (signer.StrongNameKey.GetType() != typeof(RSACryptoServiceProvider)) + { + throw new NotSupportedException(); + } + + // Setup up XMLDSIG engine. + ManifestSignedXml2 signedXml = new ManifestSignedXml2(signatureParent); + signedXml.SigningKey = GetFixedRSACryptoServiceProvider(signer.StrongNameKey as RSACryptoServiceProvider, useSha256); + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + if (signer.UseSha256) + signedXml.SignedInfo.SignatureMethod = Sha256SignatureMethodUri; + + // Add the key information. + signedXml.KeyInfo.AddClause(new RSAKeyValue(snKey)); + if (licenseDom != null) + { + signedXml.KeyInfo.AddClause(new KeyInfoNode(licenseDom.DocumentElement)); + } + signedXml.KeyInfo.Id = "StrongNameKeyInfo"; + + // Add the enveloped reference. + Reference enveloped = new Reference(); + enveloped.Uri = ""; + if (signer.UseSha256) + enveloped.DigestMethod = Sha256DigestMethod; + + // Add an enveloped then Exc-C14N transform. + enveloped.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + enveloped.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(enveloped); + +#if (false) // DSIE: New format does not sign KeyInfo. + // Add the key info reference. + Reference strongNameKeyInfo = new Reference(); + strongNameKeyInfo.Uri = "#StrongNameKeyInfo"; + strongNameKeyInfo.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(strongNameKeyInfo); +#endif + // Compute the signature. + signedXml.ComputeSignature(); + + // Get the XML representation + XmlElement xmlDigitalSignature = signedXml.GetXml(); + xmlDigitalSignature.SetAttribute("Id", "StrongNameSignature"); + + // Insert the signature now. + signatureParent.AppendChild(xmlDigitalSignature); + } + private static readonly char[] s_hexValues = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + private static string BytesToHexString(byte[] array, int start, int end) + { + string result = null; + if (array != null) + { + char[] hexOrder = new char[(end - start) * 2]; + int i = end; + int digit, j = 0; + while (i-- > start) + { + digit = (array[i] & 0xf0) >> 4; + hexOrder[j++] = s_hexValues[digit]; + digit = (array[i] & 0x0f); + hexOrder[j++] = s_hexValues[digit]; + } + result = new String(hexOrder); + } + return result; + } + + private static byte[] HexStringToBytes(string hexString) + { + uint cbHex = (uint)hexString.Length / 2; + byte[] hex = new byte[cbHex]; + int i = hexString.Length - 2; + for (int index = 0; index < cbHex; index++) + { + hex[index] = (byte)((HexToByte(hexString[i]) << 4) | HexToByte(hexString[i + 1])); + i -= 2; + } + return hex; + } + + private static byte HexToByte(char val) + { + if (val <= '9' && val >= '0') + return (byte)(val - '0'); + else if (val >= 'a' && val <= 'f') + return (byte)((val - 'a') + 10); + else if (val >= 'A' && val <= 'F') + return (byte)((val - 'A') + 10); + else + return 0xFF; + } + } + + internal class CmiManifestSigner2 + { + private AsymmetricAlgorithm _strongNameKey; + private X509Certificate2 _certificate; + private string _description; + private string _url; + private X509Certificate2Collection _certificates; + private X509IncludeOption _includeOption; + private CmiManifestSignerFlag _signerFlag; + private bool _useSha256; + + private CmiManifestSigner2() { } + + internal CmiManifestSigner2(AsymmetricAlgorithm strongNameKey) : + this(strongNameKey, null, false) + { } + + internal CmiManifestSigner2(AsymmetricAlgorithm strongNameKey, X509Certificate2 certificate, bool useSha256) + { + if (strongNameKey == null) + throw new ArgumentNullException("strongNameKey"); + + RSA rsa = strongNameKey as RSA; + if (rsa == null) + throw new ArgumentNullException("strongNameKey"); + + _strongNameKey = strongNameKey; + _certificate = certificate; + _certificates = new X509Certificate2Collection(); + _includeOption = X509IncludeOption.ExcludeRoot; + _signerFlag = CmiManifestSignerFlag.None; + _useSha256 = useSha256; + } + + internal bool UseSha256 + { + get + { + return _useSha256; + } + } + + internal AsymmetricAlgorithm StrongNameKey + { + get + { + return _strongNameKey; + } + } + + internal X509Certificate2 Certificate + { + get + { + return _certificate; + } + } + + internal string Description + { + get + { + return _description; + } + set + { + _description = value; + } + } + + internal string DescriptionUrl + { + get + { + return _url; + } + set + { + _url = value; + } + } + + internal X509Certificate2Collection ExtraStore + { + get + { + return _certificates; + } + } + + internal X509IncludeOption IncludeOption + { + get + { + return _includeOption; + } + set + { + if (value < X509IncludeOption.None || value > X509IncludeOption.WholeChain) + throw new ArgumentException("value"); + if (_includeOption == X509IncludeOption.None) + throw new NotSupportedException(); + _includeOption = value; + } + } + + internal CmiManifestSignerFlag Flag + { + get + { + return _signerFlag; + } + set + { + unchecked + { + if ((value & ((CmiManifestSignerFlag)~CimManifestSignerFlagMask)) != 0) + throw new ArgumentException("value"); + } + _signerFlag = value; + } + } + + internal const uint CimManifestSignerFlagMask = (uint)0x00000001; + } +} + diff --git a/src/XMakeTasks/ManifestUtil/merge.xsl b/src/XMakeTasks/ManifestUtil/merge.xsl new file mode 100644 index 00000000000..b4ee472ffa6 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/merge.xsl @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/ManifestUtil/read2.xsl b/src/XMakeTasks/ManifestUtil/read2.xsl new file mode 100644 index 00000000000..23ae5f0c804 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/read2.xsl @@ -0,0 +1,568 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Foreground + + + Background + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EntryPointIdentity + + + + + + + + + + + + + + + + + + + + + + + + + + + + AssemblyIdentity + + + + + + true + + + + + + + + false + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AssemblyIdentity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/ManifestUtil/trustinfo2.xsl b/src/XMakeTasks/ManifestUtil/trustinfo2.xsl new file mode 100644 index 00000000000..d89a96a268a --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/trustinfo2.xsl @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/ManifestUtil/write2.xsl b/src/XMakeTasks/ManifestUtil/write2.xsl new file mode 100644 index 00000000000..537a8d08177 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/write2.xsl @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/ManifestUtil/write3.xsl b/src/XMakeTasks/ManifestUtil/write3.xsl new file mode 100644 index 00000000000..f0c0390dc83 --- /dev/null +++ b/src/XMakeTasks/ManifestUtil/write3.xsl @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/Message.cs b/src/XMakeTasks/Message.cs new file mode 100644 index 00000000000..8e6d3758966 --- /dev/null +++ b/src/XMakeTasks/Message.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Globalization; +using System.Resources; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task that simply emits a message. Importance defaults to high if not specified. + /// + public sealed class Message : TaskExtension + { + private string _text; + + /// + /// Text to log. + /// + public string Text + { + get + { + return _text; + } + + set + { + _text = value; + } + } + + private string _importance; + + /// + /// Importance: high, normal, low (default normal) + /// + public string Importance + { + get + { + return _importance; + } + + set + { + _importance = value; + } + } + + private string _code; + + /// + /// Message code + /// + public string Code + { + get + { + return _code; + } + set + { + _code = value; + } + } + + private string _file; + + /// + /// Relevant file if any. + /// If none is provided and this is a critical message, the file containing the Message + /// task will be used. + /// + public string File + { + get + { + return _file; + } + set + { + _file = value; + } + } + + private string _helpKeyword; + + /// + /// Message help keyword + /// + public string HelpKeyword + { + get + { + return _helpKeyword; + } + set + { + _helpKeyword = value; + } + } + + private bool _isCritical; + + /// + /// Indicates if this is a critical message + /// + public bool IsCritical + { + get + { + return _isCritical; + } + set + { + _isCritical = value; + } + } + + public override bool Execute() + { + MessageImportance messageImportance; + + if ((Importance == null) || (Importance.Length == 0)) + { + messageImportance = MessageImportance.Normal; + } + else + { + try + { + // Parse the raw importance string into a strongly typed enumeration. + messageImportance = (MessageImportance)Enum.Parse(typeof(MessageImportance), Importance, true /* case-insensitive */); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("Message.InvalidImportance", Importance); + return false; + } + } + + if (Text != null) + { + if (IsCritical) + { + Log.LogCriticalMessage(null, Code, HelpKeyword, File, 0, 0, 0, 0, "{0}", Text); + } + else + { + if (File != null) + { + Log.LogMessage(null, Code, HelpKeyword, File, 0, 0, 0, 0, messageImportance, "{0}", Text); + } + else + { + Log.LogMessage(messageImportance, "{0}", Text); + } + } + } + + return true; + } + } +} diff --git a/src/XMakeTasks/Microsoft.Build.Tasks.csproj b/src/XMakeTasks/Microsoft.Build.Tasks.csproj new file mode 100644 index 00000000000..e5a57e56c0a --- /dev/null +++ b/src/XMakeTasks/Microsoft.Build.Tasks.csproj @@ -0,0 +1,916 @@ + + + + + Debug + AnyCPU + {59A73FE0-D3B7-4299-9063-3A587D429AF4} + Library + Properties + Microsoft.Build.Tasks + Microsoft.Build.Tasks.Core + true + + + + + + + + + Resx + false + %(Filename) + Microsoft.Build.Tasks.SR + System.Design.resources + + + + + + + + + + true + + + AssemblyDependency\AssemblyFoldersEx.cs + true + + + AssemblyNameComparer.cs + + + AssemblyDependency\AssemblyNameReverseVersionComparer.cs + + + CanonicalError.cs + True + + + Constants.cs + True + + + + ExtensionFoldersRegistryKey.cs + + + FileDelegates.cs + true + + + HybridDictionary.cs + + + NGen.cs + + + OpportunisticIntern.cs + + + PropertyParser.cs + True + + + ReadOnlyEmptyCollection.cs + + + RegistryDelegates.cs + true + + + RegistryHelper.cs + true + + + StringBuilderCache.cs + true + + + StrongNameHelpers.cs + + + TaskLoggingHelperExtension.cs + True + + + + MetadataConversionUtilities.cs + true + + + StreamMappedString.cs + true + + + ExceptionHandling.cs + + + FileUtilities.cs + true + + + EscapingUtilities.cs + true + + + true + + + Modifiers.cs + true + + + + + + + VersionUtilities.cs + true + + + VisualStudioConstants.cs + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + Code + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + + true + + + true + + + + true + + + true + + + + true + + + true + + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + true + + + true + + + true + + + true + + + true + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + true + + + true + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + + true + + + + true + + + + true + + + + + + + true + + + true + + + true + + + true + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + Designer + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Designer + + + Designer + Microsoft.Build.Tasks.Strings.shared.resources + + + + + + + + + + + + + + + + + + {828566EE-6F6A-4EF4-98B0-513F7DF9C628} + Microsoft.Build.Utilities + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + Microsoft.Build.Tasks.Deployment.ManifestUtilities.manifest.xml + + + Microsoft.Build.Tasks.Deployment.ManifestUtilities.merge.xsl + + + Microsoft.Build.Tasks.Deployment.ManifestUtilities.read2.xsl + + + Microsoft.Build.Tasks.Deployment.ManifestUtilities.trustinfo2.xsl + + + Microsoft.Build.Tasks.Deployment.ManifestUtilities.write2.xsl + Designer + + + Microsoft.Build.Tasks.Deployment.ManifestUtilities.write3.xsl + Designer + + + Microsoft.Build.Tasks.Deployment.Bootstrapper.xmltoconfig.xsl + + + System.Design + true + internal + true + Microsoft.Build.Tasks.SR + System.Design.resources + true + + + + Designer + + + Designer + + + + + Designer + + + Designer + + + + + + + $(SuiteBinPath) + + + $(AssetCompatPropsDir) + + + $(AssetCompatPropsDir) + + + + + + + + + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + $(XamlRulesOutputDir) + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Microsoft.CSharp.CurrentVersion.targets b/src/XMakeTasks/Microsoft.CSharp.CurrentVersion.targets new file mode 100644 index 00000000000..56f05004ad5 --- /dev/null +++ b/src/XMakeTasks/Microsoft.CSharp.CurrentVersion.targets @@ -0,0 +1,496 @@ + + + + + + true + true + true + true + + + + + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.CSharp.targets + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.Microsoft.CSharp.targets + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + .cs + C# + Managed + true + true + true + true + + + + + + File + + + BrowseObject + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + <_DebugSymbolsIntermediatePathTemporary Include="$(PdbFile)"/> + + <_DebugSymbolsIntermediatePath Include="@(_DebugSymbolsIntermediatePathTemporary->'%(RootDir)%(Directory)%(Filename).pdb')"/> + + + + _ComputeNonExistentFileProperty;ResolveCodeAnalysisRuleSet + true + + + + + + + $(NoWarn);1701;1702 + + + + + $(NoWarn);2008 + + + + + $(MsBuildToolsPath) + + + + + + + + + + + $(AppConfig) + + + $(IntermediateOutputPath)$(TargetName).compile.pdb + + + + + false + + + + + + + + + + + + + + + + + + + $(NoWarn);1701;1702 + + + + + $(NoWarn);2008 + + + + + $(MsBuildToolsPath) + + + + + + + + + + + $(AppConfig) + + + $(IntermediateOutputPath)$(TargetName).compile.pdb + + + + + false + + + + + + + + + + + + <_CoreCompileResourceInputs Remove="@(_CoreCompileResourceInputs)" /> + + + + + + + + + + + + +// <autogenerated /> +using System%3b +using System.Reflection%3b +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute("$(TargetFrameworkMoniker)", FrameworkDisplayName = "$(TargetFrameworkMonikerDisplayName)")] + + + + + + true + + + true + + true + + + $([System.Globalization.CultureInfo]::CurrentUICulture.Name) + + + + + + <_ExplicitReference Include="$(FrameworkPathOverride)\mscorlib.dll" /> + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.CSharp.targets b/src/XMakeTasks/Microsoft.CSharp.targets new file mode 100644 index 00000000000..74fe11deedf --- /dev/null +++ b/src/XMakeTasks/Microsoft.CSharp.targets @@ -0,0 +1,185 @@ + + + + + + + + + $(MSBuildFrameworkToolsPath)\Microsoft.CSharp.targets + + + $(MsBuildFrameworkToolsPath) + + + + + $(MSBuildToolsPath)\Microsoft.CSharp.CurrentVersion.targets + + + + + + + + $(ImportByWildcardBeforeMicrosoftCommonTargets) + true + + $(ImportByWildcardAfterMicrosoftCommonTargets) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftCommonTargets) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftCommonTargets) + true + + false + false + false + false + + $(MSBuildExtensionsPath)\v4.0\Custom.Before.Microsoft.Common.targets + $(MSBuildExtensionsPath)\v4.0\Custom.After.Microsoft.Common.targets + + + $(ImportByWildcardBeforeMicrosoftCSharpTargets) + true + + $(ImportByWildcardAfterMicrosoftCSharpTargets) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftCSharpTargets) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftCSharpTargets) + true + + false + false + false + false + + $(MSBuildExtensionsPath)\v4.0\Custom.Before.Microsoft.CSharp.targets + $(MSBuildExtensionsPath)\v4.0\Custom.After.Microsoft.CSharp.targets + + + + + $(ImportByWildcardBeforeMicrosoftNetFrameworkProps) + true + + $(ImportByWildcardAfterMicrosoftNetFrameworkProps) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftNetFrameworkProps) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftNetFrameworkProps) + true + + false + false + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildFrameworkToolsPath) + + diff --git a/src/XMakeTasks/Microsoft.Common.CurrentVersion.targets b/src/XMakeTasks/Microsoft.Common.CurrentVersion.targets new file mode 100644 index 00000000000..9e7aefb79fa --- /dev/null +++ b/src/XMakeTasks/Microsoft.Common.CurrentVersion.targets @@ -0,0 +1,5462 @@ + + + + + + + + true + true + true + true + + + + + + + + + + + 10.0 + + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.Microsoft.Common.targets + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\ReportingServices\Microsoft.ReportingServices.targets + + + + + + + Managed + + + + + .NETFramework + v4.0 + + + + + Any CPU,x86,x64,Itanium + Any CPU,x86,x64 + + + + + + + + true + + + + $(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion),Profile=$(TargetFrameworkProfile) + $(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion) + + + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToStandardLibraries($(TargetFrameworkIdentifier), $(TargetFrameworkVersion), $(TargetFrameworkProfile), $(PlatformTarget))) + $(MSBuildFrameworkToolsPath) + + + + Windows + 7.0 + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\Software\Microsoft\Microsoft SDKs\Windows\v$(TargetPlatformVersion)', InstallationFolder, null, RegistryView.Registry32, RegistryView.Default)) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation($(TargetPlatformIdentifier), $(TargetPlatformVersion))) + $(TargetPlatformSdkPath)Windows Metadata + $(TargetPlatformSdkPath)References\CommonConfiguration\Neutral + $(TargetPlatformSdkMetadataLocation) + true + $(WinDir)\System32\WinMetadata + $(TargetPlatformIdentifier),Version=$(TargetPlatformVersion) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKDisplayName($(TargetPlatformIdentifier), $(TargetPlatformVersion))) + + + + + + + $(OutputPath)\ + $(MSBuildProjectName) + + bin\Debug\ + <_OriginalConfiguration>$(Configuration) + <_OriginalPlatform>$(Platform) + Debug + $(Configuration) + + AnyCPU + + $(TargetType) + library + exe + + true + + + <_DebugSymbolsProduced>false + <_DebugSymbolsProduced Condition="'$(DebugSymbols)'=='true'">true + <_DebugSymbolsProduced Condition="'$(DebugType)'=='none'">false + <_DebugSymbolsProduced Condition="'$(DebugType)'=='pdbonly'">true + <_DebugSymbolsProduced Condition="'$(DebugType)'=='full'">true + + + <_DocumentationFileProduced>true + <_DocumentationFileProduced Condition="'$(DocumentationFile)'==''">false + + + + + + <_InvalidConfigurationError Condition=" '$(SkipInvalidConfigurations)' != 'true' ">true + <_InvalidConfigurationWarning Condition=" '$(SkipInvalidConfigurations)' == 'true' ">true + + + + + .exe + .exe + .exe + .dll + .netmodule + .winmdobj + + + + + + true + + $(OutputPath) + + + $(OutDir)\ + $(MSBuildProjectName) + + + + $(OutDir)$(ProjectName)\ + + $(RootNamespace) + $(AssemblyName) + + $(MSBuildProjectFile) + + $(MSBuildProjectExtension) + + $(TargetName).winmd + $(WinMDExpOutputWindowsMetadataFilename) + $(TargetName)$(TargetExt) + + + + + + + <_DeploymentPublishableProjectDefault Condition="'$(OutputType)'=='winexe' or '$(OutputType)'=='exe' or '$(OutputType)'=='appcontainerexe'">true + $(_DeploymentPublishableProjectDefault) + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='library'">Native.$(AssemblyName).manifest + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='winexe'">$(TargetFileName).manifest + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='exe'">$(TargetFileName).manifest + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='appcontainerexe'">$(TargetFileName).manifest + + $(AssemblyName).application + + $(AssemblyName).xbap + + $(GenerateManifests) + + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='library'">Native.$(AssemblyName) + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='winexe'">$(AssemblyName).exe + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='exe'">$(AssemblyName).exe + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='appcontainerexe'">$(AssemblyName).exe + <_DeploymentDeployManifestIdentity Condition="'$(HostInBrowser)' != 'true'">$(AssemblyName).application + <_DeploymentDeployManifestIdentity Condition="'$(HostInBrowser)' == 'true'">$(AssemblyName).xbap + + <_DeploymentFileMappingExtension Condition="'$(MapFileExtensions)'=='true'">.deploy + <_DeploymentFileMappingExtension Condition="'$(MapFileExtensions)'!='true'"> + + <_DeploymentBuiltUpdateInterval Condition="'$(UpdatePeriodically)'=='true'">$(UpdateInterval) + <_DeploymentBuiltUpdateIntervalUnits Condition="'$(UpdatePeriodically)'=='true'">$(UpdateIntervalUnits) + <_DeploymentBuiltUpdateInterval Condition="'$(UpdatePeriodically)'!='true'">0 + <_DeploymentBuiltUpdateIntervalUnits Condition="'$(UpdatePeriodically)'!='true'">Days + <_DeploymentBuiltMinimumRequiredVersion Condition="'$(UpdateRequired)'=='true' and '$(Install)'=='true'">$(MinimumRequiredVersion) + + 100 + + + + + * + $(UICulture) + + + + + <_OutputPathItem Include="$(OutDir)"/> + + <_UnmanagedRegistrationCache Include="$(BaseIntermediateOutputPath)$(MSBuildProjectFile).UnmanagedRegistration.cache"/> + <_ResolveComReferenceCache Include="$(IntermediateOutputPath)$(MSBuildProjectFile).ResolveComReference.cache"/> + + + + + + $([MSBuild]::Escape($([System.IO.Path]::GetFullPath(`$([System.IO.Path]::Combine(`$(MSBuildProjectDirectory)`, `$(OutDir)`))`)))) + + + $(TargetDir)$(TargetFileName) + + + $(MSBuildProjectDirectory)\ + + + $(ProjectDir)$(ProjectFileName) + + + $(Platform) + + + + + + + + + + *Undefined* + *Undefined* + + *Undefined* + + *Undefined* + + *Undefined* + + *Undefined* + + + + + true + + + true + false + obj\ + $(BaseIntermediateOutputPath)\ + $(MSBuildProjectFile).FileListAbsolute.txt + + false + + true + true + <_ResolveReferenceDependencies Condition="'$(_ResolveReferenceDependencies)' == ''">false + <_GetChildProjectCopyToOutputDirectoryItems Condition="'$(_GetChildProjectCopyToOutputDirectoryItems)' == ''">true + false + false + + + + $(BaseIntermediateOutputPath)$(Configuration)\ + $(BaseIntermediateOutputPath)$(PlatformName)\$(Configuration)\ + + + $(IntermediateOutputPath)\ + <_GenerateBindingRedirectsIntermediateAppConfig>$(IntermediateOutputPath)$(MSBuildProjectFile).$(TargetFileName).config + + + + + + + + <_DebugSymbolsIntermediatePath Include="$(IntermediateOutputPath)$(TargetName).compile.pdb" Condition="'$(OutputType)' == 'winmdobj' and '@(_DebugSymbolsIntermediatePath)' == ''"/> + <_DebugSymbolsIntermediatePath Include="$(IntermediateOutputPath)$(TargetName).pdb" Condition="'$(OutputType)' != 'winmdobj' and '@(_DebugSymbolsIntermediatePath)' == ''"/> + <_DebugSymbolsOutputPath Include="@(_DebugSymbolsIntermediatePath->'$(OutDir)%(Filename)%(Extension)')" /> + + + + $(IntermediateOutputPath)$(TargetName).pdb + <_WinMDDebugSymbolsOutputPath>$([System.IO.Path]::Combine('$(OutDir)', $([System.IO.Path]::GetFileName('$(WinMDExpOutputPdb)')))) + + + + $(IntermediateOutputPath)$(TargetName).xml + <_WinMDDocFileOutputPath>$([System.IO.Path]::Combine('$(OutDir)', $([System.IO.Path]::GetFileName('$(WinMDOutputDocumentationFile)')))) + + + + <_IntermediateWindowsMetadataPath>$(IntermediateOutputPath)$(WinMDExpOutputWindowsMetadataFilename) + <_WindowsMetadataOutputPath>$(OutDir)$(WinMDExpOutputWindowsMetadataFilename) + + + + + <_DeploymentManifestEntryPoint Include="@(IntermediateAssembly)"> + $(TargetFileName) + + + + + <_DeploymentManifestIconFile Include="$(ApplicationIcon)" Condition="Exists('$(ApplicationIcon)')"> + $(ApplicationIcon) + + + + + $(_DeploymentTargetApplicationManifestFileName) + + + + <_ApplicationManifestFinal Include="$(OutDir)$(_DeploymentTargetApplicationManifestFileName)"> + $(_DeploymentTargetApplicationManifestFileName) + + + + + $(TargetDeployManifestFileName) + + + + <_DeploymentIntermediateTrustInfoFile Include="$(IntermediateOutputPath)$(TargetName).TrustInfo.xml" Condition="'$(TargetZone)'!=''"/> + + + + + <_DeploymentUrl Condition="'$(_DeploymentUrl)'==''">$(UpdateUrl) + <_DeploymentUrl Condition="'$(_DeploymentUrl)'==''">$(InstallUrl) + <_DeploymentUrl Condition="'$(_DeploymentUrl)'==''">$(PublishUrl) + <_DeploymentUrl Condition="!('$(UpdateUrl)'=='') and '$(Install)'=='false'"> + <_DeploymentUrl Condition="'$(_DeploymentUrl)'!=''">$(_DeploymentUrl)$(TargetDeployManifestFileName) + + <_DeploymentUrl Condition="'$(UpdateUrl)'=='' and !('$(Install)'=='true' and '$(UpdateEnabled)'=='true')"> + <_DeploymentUrl Condition="'$(ExcludeDeploymentUrl)'=='true'"> + + + + + <_DeploymentApplicationUrl Condition="'$(IsWebBootstrapper)'=='true'">$(InstallUrl) + <_DeploymentApplicationUrl Condition="'$(IsWebBootstrapper)'=='true' and '$(InstallUrl)'==''">$(PublishUrl) + <_DeploymentComponentsUrl Condition="'$(BootstrapperComponentsLocation)'=='Absolute'">$(BootstrapperComponentsUrl) + + + + + $(PublishDir)\ + $(OutputPath)app.publish\ + + + + + + $(PlatformTarget) + + + msil + amd64 + ia64 + x86 + arm + + + + true + + + + + $(Platform) + msil + amd64 + ia64 + x86 + arm + + + None + $(PROCESSOR_ARCHITECTURE) + + + + + + CLR2 + CLR4 + CurrentRuntime + + true + false + + $(PlatformTarget) + x86 + x64 + CurrentArchitecture + + + + + Client + + + + + false + + + + + + true + true + false + + + + $(MSBuildAllProjects);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath) + $(MSBuildAllProjects);$(MSBuildProjectFullPath).user + + + + + AssemblyFoldersEx + Software\Microsoft\$(TargetFrameworkIdentifier) + Software\Microsoft\Microsoft SDKs\$(TargetPlatformIdentifier) + + + {CandidateAssemblyFiles}; + $(ReferencePath); + {HintPathFromItem}; + {TargetFrameworkDirectory}; + {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)}; + {AssemblyFolders}; + {GAC}; + {RawFileName}; + $(OutDir) + + + + + .winmd; + .dll; + .exe + + + + + .pdb; + .xml; + .pri + + + + Full + + + + + false + + + + + $(MSBuildThisFileDirectory)$(LangName)\ + + + + + $(MSBuildThisFileDirectory)en-US\ + + + + + + + Project + + + + BrowseObject + + + + File + + + + Invisible + + + + File;BrowseObject + + + + File;ProjectSubscriptionService + + + + + ;BrowseObject + + + ProjectSubscriptionService;BrowseObject + + + + + ;BrowseObject + + + ProjectSubscriptionService;BrowseObject + + + + + ;BrowseObject + + + ProjectSubscriptionService;BrowseObject + + + + + + + + + + Never + + + Never + + + Never + + + Never + + + + + + + + + + <_InvalidConfigurationMessageText>The OutputPath property is not set for project '$(MSBuildProjectFile)'. Please check to make sure that you have specified a valid combination of Configuration and Platform for this project. Configuration='$(_OriginalConfiguration)' Platform='$(_OriginalPlatform)'. + <_InvalidConfigurationMessageText Condition="'$(BuildingInsideVisualStudio)' == 'true'">$(_InvalidConfigurationMessageText) This error may also appear if some other project is trying to follow a project-to-project reference to this project, this project has been unloaded or is not included in the solution, and the referencing project does not build using the same or an equivalent Configuration or Platform. + <_InvalidConfigurationMessageText Condition="'$(BuildingInsideVisualStudio)' != 'true'">$(_InvalidConfigurationMessageText) You may be seeing this message because you are trying to build a project without a solution file, and have specified a non-default Configuration or Platform that doesn't exist for this project. + + + + + + + + + + + + + + + + x86 + + + + + + + BeforeBuild; + CoreBuild; + AfterBuild + + + + + + + + + + + + + + BuildOnlySettings; + PrepareForBuild; + PreBuildEvent; + ResolveReferences; + PrepareResources; + ResolveKeySource; + Compile; + ExportWindowsMDFile; + UnmanagedUnregistration; + GenerateSerializationAssemblies; + CreateSatelliteAssemblies; + GenerateManifests; + GetTargetPath; + PrepareForRun; + UnmanagedRegistration; + IncrementalClean; + PostBuildEvent + + + + + + + + + + + + + <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' != ''">$(MSBuildProjectDefaultTargets) + <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' == ''">Build + + + BeforeRebuild; + Clean; + $(_ProjectDefaultTargets); + AfterRebuild; + + + + BeforeRebuild; + Clean; + Build; + AfterRebuild; + + + + + + + + + + + + + + Build + + + + + + + + + + + + Build + + + + + + + + + + + + Build + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + true + + + + + + + + + + + GetFrameworkPaths;GetReferenceAssemblyPaths;AssignLinkMetadata + + + + + + $(TargetFileName).config + + + + + + + + + + + + + + + + + @(_TargetFramework40DirectoryItem) + @(_TargetFramework35DirectoryItem) + @(_TargetFramework30DirectoryItem) + @(_TargetFramework20DirectoryItem) + + @(_TargetFramework20DirectoryItem) + @(_TargetFramework40DirectoryItem) + @(_TargetedFrameworkDirectoryItem) + @(_TargetFrameworkSDKDirectoryItem) + + + + + + + + + + + + + + + + + + + + + + + $(_TargetFrameworkDirectories);$(TargetFrameworkDirectory);$(WinFXAssemblyDirectory) + $(TargetFrameworkDirectory);$(TargetPlatformWinMDLocation) + + + + + + true + + + + $(AssemblySearchPaths.Replace('{AssemblyFolders}', '').Split(';')) + + + + + + + + + $(TargetFrameworkDirectory);@(DesignTimeFacadeDirectories) + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + $(TargetFrameworkAsMSBuildRuntime) + + + CurrentRuntime + + + + + + + + + + + + + + + + + + + + BeforeResolveReferences; + AssignProjectConfiguration; + ResolveProjectReferences; + FindInvalidProjectReferences; + ResolveNativeReferences; + ResolveAssemblyReferences; + GenerateBindingRedirects; + ResolveComReferences; + AfterResolveReferences + + + + + + + + + + + + + + + true + true + false + + + false + + + true + + + + + + + + + + + + + + + <_ProjectReferenceWithConfiguration> + true + true + + + true + true + + + + + + + + + + + + + + + + + + + <_MSBuildProjectReference Include="@(ProjectReferenceWithConfiguration)" Condition="'$(BuildingInsideVisualStudio)'!='true' and '@(ProjectReferenceWithConfiguration)'!=''"/> + + + + + <_MSBuildProjectReferenceExistent Include="@(_MSBuildProjectReference)" Condition="Exists('%(Identity)')"/> + <_MSBuildProjectReferenceNonexistent Include="@(_MSBuildProjectReference)" Condition="!Exists('%(Identity)')"/> + + + + + + + + + + $(ProjectReferenceBuildTargets) + + + ProjectReference + + + + + + + + <_GetTargetPathName>GetTargetPath + <_GetTargetPathName Condition="'$(IntelliSenseBuild)' == 'true'">GetTargetPathWithTargetPlatformMoniker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ResolvedProjectReferencePaths Remove="@(_ResolvedProjectReferencePaths)" Condition="'%(_ResolvedProjectReferencePaths.ResolveableAssembly)' == 'false'" /> + + <_ResolvedProjectReferencePaths> + %(_ResolvedProjectReferencePaths.OriginalItemSpec) + + + + + + + + + + + + <_ProjectReferencesFromRAR Include="@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))"> + %(ReferencePath.ProjectReferenceOriginalItemSpec) + + + + + + + + + $(GetTargetPathDependsOn) + + + + + + + $(GetTargetPathDependsOn); GetTargetPath + + + + + + $(TargetPlatformMoniker) + $(TargetPlatformIdentifier) + + + + + + + + + + %(_ApplicationManifestFinal.FullPath) + + + + + + + + + + + + + + + + + + + + + + + GetFrameworkPaths; + GetReferenceAssemblyPaths; + PrepareForBuild; + ResolveSDKReferences; + ExpandSDKReferences; + + + + + + <_ReferenceInstalledAssemblyDirectory Include="$(TargetFrameworkDirectory)" /> + <_ReferenceInstalledAssemblySubsets Include="$(TargetFrameworkSubset)" /> + + + + + $(IntermediateOutputPath)$(MSBuildProjectFile)ResolveAssemblyReference.cache + + + + + <_ResolveAssemblyReferencesApplicationConfigFileForExes Include="@(AppConfigWithTargetPath)" Condition="'$(AutoGenerateBindingRedirects)'=='true' or '$(AutoUnifyAssemblyReferences)'=='false'"/> + + + + + <_FindDependencies Condition="'$(BuildingProject)' != 'true' and '$(_ResolveReferenceDependencies)' != 'true'">false + <_ForceSystemRuntimeDependencyCalculation Condition="'$(IntelliSenseBuild)' == 'true' or '$(CheckForSystemRuntimeDependency)' == 'true'">true + true + false + Warning + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(_GenerateBindingRedirectsIntermediateAppConfig) + + + + + $(TargetFileName).config + + + + + + + Software\Microsoft\Microsoft SDKs + $(LocalAppData)\Microsoft SDKs;$(MSBuildProgramFiles32)\Microsoft SDKs + $(LocalAppData)\$(SDKIdentifier)\$(SDKVersion);$(MSBuildProgramFiles32)\$(SDKIdentifier)\$(SDKVersion) + + + + + + + + + + + + + + GetInstalledSDKLocations + + + + + Debug + Retail + Retail + $(ProcessorArchitecture) + Neutral + + + + true + + + + + + + + + + + + + + + + + + + + + + GetReferenceTargetPlatformMonikers + + + + + + + + + + + <_ResolvedProjectReferencePaths Remove="@(InvalidProjectReferences)" /> + + + + + + + + + + + + + + ResolveSDKReferences + + + + .winmd; + .dll + + + + + + + + + + + + + + + + + + + + + false + false + + $(TargetFrameworkSDKToolsDirectory) + true + + + + + + + + + + + + + + + + + + + <_ReferencesFromRAR Include="@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ResolveAssemblyReference'))"/> + + + + + + {CandidateAssemblyFiles}; + $(ReferencePath); + {HintPathFromItem}; + {TargetFrameworkDirectory}; + {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)}; + {RawFileName}; + $(TargetDir) + + + + + + + GetFrameworkPaths; + GetReferenceAssemblyPaths; + ResolveReferences + + + + + + + <_DesignTimeReferenceInstalledAssemblyDirectory Include="$(TargetFrameworkDirectory)" /> + + + + $(IntermediateOutputPath)$(MSBuildProjectFile)DesignTimeResolveAssemblyReferences.cache + + + + + {CandidateAssemblyFiles}; + $(ReferencePath); + {HintPathFromItem}; + {TargetFrameworkDirectory}; + {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)}; + {RawFileName}; + $(OutDir) + + + + + false + false + false + false + false + true + false + + + + <_DesignTimeReferenceAssemblies Include ="$(DesignTimeReference)" /> + + + + <_RARResolvedReferencePath Include="@(ReferencePath)" /> + + + + + + + + + + + + + false + + + + + + $(IntermediateOutputPath) + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + $(TargetFrameworkSDKToolsDirectory) + false + + + + + + + + + + + + + + + + + + + + + + + + + + + PrepareResourceNames; + ResGen; + CompileLicxFiles + + + + + + + + AssignTargetPaths; + SplitResourcesByCulture; + CreateManifestResourceNames; + CreateCustomManifestResourceNames + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_LicxFile Include="@(EmbeddedResource)" Condition="'%(Extension)'=='.licx'"/> + + + + Resx + + + Non-Resx + + + + + + + + + + + + + + + + + + + + Resx + + + Non-Resx + + + + + + + + + + + + + + <_MixedResourceWithNoCulture Remove="@(_MixedResourceWithNoCulture)" /> + <_MixedResourceWithCulture Remove="@(_MixedResourceWithCulture)" /> + + + + + + + + + + + + + + ResolveAssemblyReferences;SplitResourcesByCulture;BeforeResGen;CoreResGen;AfterResGen + + true + false + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + $(PlatformTargetAsMSBuildArchitecture) + + $(TargetFrameworkSDKToolsDirectory) + + + + + $(TargetFrameworkAsMSBuildRuntime) + + + CurrentRuntime + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + true + + + true + + + + + true + + + true + + + + + + + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResolveReferences; + ResolveKeySource; + SetWin32ManifestProperties; + _GenerateCompileInputs; + BeforeCompile; + _TimeStampBeforeCompile; + CoreCompile; + _TimeStampAfterCompile; + AfterCompile; + + + + + + + + + + + + + + <_CoreCompileResourceInputs Include="@(EmbeddedResource->'%(OutputResource)')" Condition="'%(EmbeddedResource.WithCulture)' == 'false' and '%(EmbeddedResource.Type)' == 'Resx'" /> + <_CoreCompileResourceInputs Include="@(EmbeddedResource)" Condition="'%(EmbeddedResource.WithCulture)' == 'false' and '%(EmbeddedResource.Type)' == 'Non-Resx' " /> + + + <_CoreCompileResourceInputs Include="@(ManifestResourceWithNoCulture)" Condition="'%(ManifestResourceWithNoCulture.EmittedForCompatibilityOnly)'==''"> + Resx + false + + <_CoreCompileResourceInputs Include="@(ManifestNonResxWithNoCultureOnDisk)" Condition="'%(ManifestNonResxWithNoCultureOnDisk.EmittedForCompatibilityOnly)'==''"> + Non-Resx + false + + + + + + + + + + true + $([System.IO.Path]::Combine('$([System.IO.Path]::GetTempPath())','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) + + + + true + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + <_AssemblyTimestampBeforeCompile>%(IntermediateAssembly.ModifiedTime) + + + + + + + + + <_AssemblyTimestampAfterCompile>%(IntermediateAssembly.ModifiedTime) + + + + + + + + + __NonExistentSubDir__\__NonExistentFile__ + + + + + + + + + + + <_SGenDllName>$(TargetName).XmlSerializers.dll + <_SGenDllCreated>false + <_SGenGenerateSerializationAssembliesConfig>$(GenerateSerializationAssemblies) + <_SGenGenerateSerializationAssembliesConfig Condition="'$(GenerateSerializationAssemblies)' == ''">Auto + <_SGenGenerateSerializationAssembliesConfig Condition="'$(ConfigurationName)'=='Debug' and '$(_SGenGenerateSerializationAssembliesConfig)' == 'Auto'">Off + true + false + true + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + + + + + + + + + + + + _GenerateSatelliteAssemblyInputs; + ComputeIntermediateSatelliteAssemblies; + GenerateSatelliteAssemblies + + + + + + + + + + + + + <_SatelliteAssemblyResourceInputs Include="@(EmbeddedResource->'%(OutputResource)')" Condition="'%(EmbeddedResource.WithCulture)' == 'true' and '%(EmbeddedResource.Type)' == 'Resx'" /> + <_SatelliteAssemblyResourceInputs Include="@(EmbeddedResource)" Condition="'%(EmbeddedResource.WithCulture)' == 'true' and '%(EmbeddedResource.Type)' == 'Non-Resx'" /> + + + <_SatelliteAssemblyResourceInputs Include="@(ManifestResourceWithCulture)" Condition="'%(ManifestResourceWithCulture.EmittedForCompatibilityOnly)'==''"> + Resx + true + + <_SatelliteAssemblyResourceInputs Include="@(ManifestNonResxWithCultureOnDisk)" Condition="'%(ManifestNonResxWithCultureOnDisk.EmittedForCompatibilityOnly)'==''"> + Non-Resx + true + + + + + + + + + + + + + + + + + + + + + CreateManifestResourceNames + + + + + + + %(EmbeddedResource.Culture) + %(EmbeddedResource.Culture)\$(TargetName).resources.dll + + + + + + + + $(Win32Manifest) + + + + + + + + + + <_DeploymentBaseManifest>$(ApplicationManifest) + <_DeploymentBaseManifest Condition="'$(_DeploymentBaseManifest)'==''">@(_DeploymentBaseManifestWithTargetPath) + + + true + + + + + + + + $(ApplicationManifest) + $(ApplicationManifest) + + + + + + + + $(_FrameworkVersion40Path)\default.win32manifest + + + + + + + + + + + + + SetWin32ManifestProperties; + GenerateApplicationManifest; + GenerateDeploymentManifest + + + + + + + <_DeploymentPublishFileOfTypeManifestEntryPoint Include="@(PublishFile)" Condition="'%(FileType)'=='ManifestEntryPoint'"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + <_DeploymentCopyApplicationManifest>true + + + + + + + + + + + + + + <_DeploymentManifestType>Native + + + + + + + + + <_DeploymentManifestVersion>@(_IntermediateAssemblyIdentity->'%(Version)') + + + + + + + + + + <_SGenDllsRelatedToCurrentDll Include="@(_ReferenceSerializationAssemblyPaths->'%(FullPath)')" Condition="'%(Extension)' == '.dll'"/> + <_SGenDllsRelatedToCurrentDll Include="@(SerializationAssembly->'%(FullPath)')" Condition="'%(Extension)' == '.dll'"/> + + + + + <_DeploymentReferencePaths Include="@(ReferencePath)"> + true + + + + + + + + + + + + + + + + + + + + + + + <_DeploymentManifestType>ClickOnce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + false + + + + + + CopyFilesToOutputDirectory + + + + + + + + false + + + + + + + false + + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + AssignTargetPaths; + _SplitProjectReferencesByFileExistence + + + + + + + + <_GCTODIKeepDuplicates>false + <_GCTODIKeepMetadata>CopyToOutputDirectory;TargetPath + + + + + + + + + + + + <_SourceItemsToCopyToOutputDirectoryAlways KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_AllChildProjectItemsWithTargetPath->'%(FullPath)')" Condition="'%(_AllChildProjectItemsWithTargetPath.CopyToOutputDirectory)'=='Always'"/> + <_SourceItemsToCopyToOutputDirectory KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_AllChildProjectItemsWithTargetPath->'%(FullPath)')" Condition="'%(_AllChildProjectItemsWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/> + + + + + <_AllChildProjectItemsWithTargetPath Remove="@(_AllChildProjectItemsWithTargetPath)"/> + + + + + <_SourceItemsToCopyToOutputDirectoryAlways KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always'"/> + <_SourceItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/> + + + + <_SourceItemsToCopyToOutputDirectoryAlways KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToOutputDirectory)'=='Always'"/> + <_SourceItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToOutputDirectory)'=='PreserveNewest'"/> + + + + <_CompileItemsToCopy Include="@(Compile->'%(FullPath)')" Condition="'%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest'"/> + + + + + + + + <_SourceItemsToCopyToOutputDirectoryAlways KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_CompileItemsToCopyWithTargetPath)" Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'"/> + <_SourceItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_CompileItemsToCopyWithTargetPath)" Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/> + + + + <_SourceItemsToCopyToOutputDirectoryAlways KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always'"/> + <_SourceItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_DocumentationFileProduced Condition="!Exists('@(DocFileItem)')">false + + + + + + + + + <_DebugSymbolsProduced Condition="!Exists('@(_DebugSymbolsIntermediatePath)')">false + + + + + + + + + + + + + <_SGenDllCreated Condition="Exists('$(IntermediateOutputPath)$(_SGenDllName)')">true + + + + + + + + + + + + + + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + $(TargetFrameworkAsMSBuildRuntime) + + + CurrentRuntime + + + + + + + + + + + + + + + + + + + + + <_CleanOrphanFileWrites Include="@(_CleanPriorFileWrites)" Exclude="@(_CleanCurrentFileWrites)"/> + + + + + + + + + + + + + + + + + + + + + + <_CleanRemainingFileWritesAfterIncrementalClean Include="@(_CleanPriorFileWrites);@(_CleanCurrentFileWrites)" Exclude="@(_CleanOrphanFilesDeleted)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_CleanPriorFileWrites Include="@(_CleanUnfilteredPriorFileWrites)" Exclude="@(_ResolveAssemblyReferenceResolvedFilesAbsolute)"/> + + + + + + + + + + + + + + + + + + + + <_CleanCurrentFileWritesWithNoReferences Include="@(_CleanCurrentFileWritesInOutput);@(_CleanCurrentFileWritesInIntermediate)" Exclude="@(_ResolveAssemblyReferenceResolvedFilesAbsolute)"/> + + + + + + + + + + + + + + + BeforeClean; + UnmanagedUnregistration; + CoreClean; + CleanReferencedProjects; + CleanPublishFolder; + AfterClean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_CleanRemainingFileWritesAfterClean Include="@(_CleanPriorFileWrites)" Exclude="@(_CleanPriorFileWritesDeleted)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SetGenerateManifests; + Build; + PublishOnly + + + _DeploymentUnpublishable + + + + + + + + + + + + + + + + + + + true + + + + + + + + SetGenerateManifests; + PublishBuild; + BeforePublish; + GenerateManifests; + CopyFilesToOutputDirectory; + _CopyFilesToPublishFolder; + _DeploymentGenerateBootstrapper; + ResolveKeySource; + _DeploymentSignClickOnceDeployment; + AfterPublish + + + + + + + + + + + + + + BuildOnlySettings; + PrepareForBuild; + ResolveReferences; + PrepareResources; + ResolveKeySource; + GenerateSerializationAssemblies; + CreateSatelliteAssemblies; + + + + + + + + + + + + + + <_DeploymentApplicationFolderName>Application Files\$(AssemblyName)_$(_DeploymentApplicationVersionFragment) + <_DeploymentApplicationDir>$(PublishDir)$(_DeploymentApplicationFolderName)\ + + + + + false + + + + '$(_DeploymentApplicationDir)%(TargetPath)'); + @(_DeploymentManifestEntryPoint->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)'); + @(_DeploymentManifestFiles->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)'); + @(ReferenceComWrappersToCopyLocal->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)'); + @(ResolvedIsolatedComModules->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)'); + @(_DeploymentLooseManifestFile->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)')" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + UseHardlinksIfPossible="$(CreateHardLinksForPublishFilesIfPossible)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + $(TargetPath) + $(TargetFileName) + true + + + + + + + true + $(TargetPath) + $(TargetFileName) + + + + + + PrepareForBuild + true + + + + + <_BuiltProjectOutputGroupOutputIntermediate Include="@(BuiltProjectOutputGroupKeyOutput)"/> + + + + + <_BuiltProjectOutputGroupOutputIntermediate Include="$(AppConfig)" Condition="'$(AddAppConfigToBuildOutputs)'=='true'"> + $(TargetDir)$(TargetFileName).config + $(TargetFileName).config + + $(AppConfig) + + + + + <_IsolatedComReference Include="@(COMReference)" Condition=" '%(COMReference.Isolated)' == 'true' "/> + <_IsolatedComReference Include="@(COMFileReference)" Condition=" '%(COMFileReference.Isolated)' == 'true' "/> + + + + + <_BuiltProjectOutputGroupOutputIntermediate Include="$(OutDir)$(_DeploymentTargetApplicationManifestFileName)" Condition="'@(NativeReference)'!='' or '@(_IsolatedComReference)'!=''"> + $(_DeploymentTargetApplicationManifestFileName) + + $(OutDir)$(_DeploymentTargetApplicationManifestFileName) + + + + + + + + %(_BuiltProjectOutputGroupOutputIntermediate.FullPath) + + + + + + + + + + + + @(_DebugSymbolsOutputPath->'%(FullPath)') + @(_DebugSymbolsIntermediatePath->'%(Filename)%(Extension)') + + + + + + + @(WinMDExpFinalOutputPdbItem->'%(FullPath)') + @(WinMDExpOutputPdbItem->'%(Filename)%(Extension)') + + + + + + + + + + + @(FinalDocFile) + true + @(DocFileItem->'%(Filename)%(Extension)') + + + + + + + @(WinMDExpFinalOutputDocItem->'%(FullPath)') + @(WinMDOutputDocumentationFileItem->'%(Filename)%(Extension)') + + + + + + + PrepareForBuild;PrepareResourceNames + + + + + + %(EmbeddedResource.Culture)\$(TargetName).resources.dll + + + + + + + + %(SatelliteDllsProjectOutputGroupOutputIntermediate.Identity) + + + + + + + + PrepareForBuild;AssignTargetPaths + + + + + + + + + + + + + + + $(MSBuildProjectFullPath) + $(ProjectFileName) + + + + + + + + + + + PrepareForBuild;AssignTargetPaths + + + + + + + + + + + + + + + + + @(_OutputPathItem->'%(FullPath)$(_SGenDllName)') + $(_SGenDllName) + + + + + + + + + + + + + + + + + + + + ResolveSDKReferences;ExpandSDKReferences + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeAnalysis\Microsoft.CodeAnalysis.targets + + + + + + + + + + + + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TeamTest\Microsoft.TeamTest.targets + + + + + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\AppxPackage\Microsoft.AppXPackage.Targets + + + + + + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.Common.overridetasks b/src/XMakeTasks/Microsoft.Common.overridetasks new file mode 100644 index 00000000000..631cf09a70d --- /dev/null +++ b/src/XMakeTasks/Microsoft.Common.overridetasks @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.Common.props b/src/XMakeTasks/Microsoft.Common.props new file mode 100644 index 00000000000..a167fd6d857 --- /dev/null +++ b/src/XMakeTasks/Microsoft.Common.props @@ -0,0 +1,92 @@ + + + + + + + 11.0 + 10.0 + + + + + $(MSBuildExtensionsPath)\v4.0\Custom.Before.$(MSBuildThisFile) + $(MSBuildExtensionsPath)\v4.0\Custom.After.$(MSBuildThisFile) + + + + + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.$(MSBuildThisFile) + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.$(MSBuildThisFile) + + + + + + + + true + + + + $(DefaultProjectConfiguration) + $(DefaultProjectPlatform) + + + + WJProject + JavaScript + + + + + + + diff --git a/src/XMakeTasks/Microsoft.Common.targets b/src/XMakeTasks/Microsoft.Common.targets new file mode 100644 index 00000000000..0274cc2ad52 --- /dev/null +++ b/src/XMakeTasks/Microsoft.Common.targets @@ -0,0 +1,132 @@ + + + + + + + + + $(MSBuildFrameworkToolsPath)\Microsoft.Common.targets + + + + + $(MSBuildToolsPath)\Microsoft.Common.CurrentVersion.targets + + + + + + + $(ImportByWildcardBeforeMicrosoftCommonTargets) + true + + $(ImportByWildcardAfterMicrosoftCommonTargets) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftCommonTargets) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftCommonTargets) + true + + false + false + false + false + + $(MSBuildExtensionsPath)\v4.0\Custom.Before.Microsoft.Common.targets + $(MSBuildExtensionsPath)\v4.0\Custom.After.Microsoft.Common.targets + + + + + $(ImportByWildcardBeforeMicrosoftNetFrameworkProps) + true + + $(ImportByWildcardAfterMicrosoftNetFrameworkProps) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftNetFrameworkProps) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftNetFrameworkProps) + true + + false + false + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildFrameworkToolsPath) + + diff --git a/src/XMakeTasks/Microsoft.Common.tasks b/src/XMakeTasks/Microsoft.Common.tasks new file mode 100644 index 00000000000..dc7dee9f759 --- /dev/null +++ b/src/XMakeTasks/Microsoft.Common.tasks @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.Data.Entity.Shim.targets b/src/XMakeTasks/Microsoft.Data.Entity.Shim.targets new file mode 100644 index 00000000000..98eac686116 --- /dev/null +++ b/src/XMakeTasks/Microsoft.Data.Entity.Shim.targets @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.props b/src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.props new file mode 100644 index 00000000000..5240d05a012 --- /dev/null +++ b/src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.props @@ -0,0 +1,141 @@ + + + + + + true + true + true + true + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + true + + $(TargetFrameworkSubset) + + + + + + + <_FullFrameworkReferenceAssemblyPaths>$(FrameworkPathOverride) + <_TargetFrameworkDirectories>$(FrameworkPathOverride) + + + + + + + <_FullFrameworkReferenceAssemblyPaths Condition="Exists('$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\RedistList\FrameworkList.xml')">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0 + <_TargetFrameworkDirectories Condition="'$(TargetFrameworkProfile)' == ''">$(_FullFrameworkReferenceAssemblyPaths) + $(_TargetFrameworkDirectories) + + + <_TargetFrameworkDirectories Condition="'$(TargetFrameworkProfile)' == 'Client' and Exists('$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\$(TargetFrameworkProfile)\RedistList\FrameworkList.xml')">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\$(TargetFrameworkProfile) + $(_TargetFrameworkDirectories) + .NET Framework 4 + .NET Framework 4 Client Profile + + + + $(Registry:HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework@InstallRoot) + <_DeploymentSignClickOnceManifests Condition="'$(TargetFrameworkVersion)' == 'v2.0' or '$(TargetFrameworkVersion)' == 'v3.0' or '$(SignManifests)' == 'true'">true + + + true + System.Core;$(AdditionalExplicitAssemblyReferences) + + + + + + true + + $(MSBuildFrameworkToolsRoot)\v3.5 + + $(SDK35ToolsPath) + + + $(SDK40ToolsPath) + + v2.0.50727 + v$(MSBuildRuntimeVersion) + + + + + true + + + + false + + + + true + + + + false + + + + + 6.02 + + 6.00 + + + + $(ExecuteAsTool) + true + + + + $(ExecuteAsTool) + true + + + + + true + + + + + + + diff --git a/src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.targets b/src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.targets new file mode 100644 index 00000000000..7013ffa8e89 --- /dev/null +++ b/src/XMakeTasks/Microsoft.NETFramework.CurrentVersion.targets @@ -0,0 +1,121 @@ + + + + + + true + true + true + true + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + + + + <_TargetFramework40DirectoryItem Include="$(MSBuildFrameworkToolsRoot)v4.0.30319"/> + <_TargetFramework35DirectoryItem Include="$(MSBuildFrameworkToolsRoot)v3.5"/> + <_TargetFramework30DirectoryItem Include="$(MSBuildFrameworkToolsRoot)v3.0"/> + <_TargetFramework20DirectoryItem Include="$(MSBuildFrameworkToolsRoot)v2.0.50727"/> + + <_TargetedFrameworkDirectoryItem Condition="'$(TargetFrameworkVersion)' == 'v2.0'" Include="@(_TargetFramework20DirectoryItem)"/> + <_TargetedFrameworkDirectoryItem Condition="'$(TargetFrameworkVersion)' == 'v3.0' OR '$(TargetFrameworkVersion)' == 'v3.5'" Include="$(MSBuildFrameworkToolsRoot)\$(TargetFrameworkVersion)"/> + <_TargetedFrameworkDirectoryItem Condition="'@(_TargetedFrameworkDirectoryItem)' == ''" Include="@(_TargetFramework40DirectoryItem)"/> + + + + <_CombinedTargetFrameworkDirectoriesItem Condition=" '$(TargetFrameworkVersion)' == 'v4.0' " + Include="@(_TargetFramework40DirectoryItem)" /> + + <_CombinedTargetFrameworkDirectoriesItem Condition=" '$(TargetFrameworkVersion)' == 'v3.5'" + Include="@(_TargetFramework35DirectoryItem)"/> + + <_CombinedTargetFrameworkDirectoriesItem Condition=" '$(TargetFrameworkVersion)' == 'v3.0' or '$(TargetFrameworkVersion)' == 'v3.5'" + Include="@(_TargetFramework30DirectoryItem)" /> + + <_CombinedTargetFrameworkDirectoriesItem Condition=" '$(TargetFrameworkVersion)' == 'v2.0' or '$(TargetFrameworkVersion)' == 'v3.0' or '$(TargetFrameworkVersion)' == 'v3.5'" + Include="@(_TargetFramework20DirectoryItem)" /> + + <_CombinedTargetFrameworkDirectoriesItem Condition=" '@(_CombinedTargetFrameworkDirectoriesItem)' == ''" + Include="@(_TargetedFrameworkDirectoryItem)" /> + + + + + @(_CombinedTargetFrameworkDirectoriesItem) + $(FrameworkSDKRoot) + + + + <_TargetFrameworkSDKDirectoryItem Include="$(TargetFrameworkSDKDirectory)"/> + + + + + + $(ResolveReferencesDependsOn); + ImplicitlyExpandDesignTimeFacades + + + + $(ImplicitlyExpandDesignTimeFacadesDependsOn); + GetReferenceAssemblyPaths + + + + + + + + + <_HasReferenceToSystemRuntime Condition="'$(DependsOnSystemRuntime)' == 'true' or '%(_ResolvedProjectReferencePaths.TargetPlatformIdentifier)' == 'Portable'">true + + + + <_DesignTimeFacadeAssemblies Include="%(DesignTimeFacadeDirectories.Identity)*.dll"/> + + + + false + false + ImplicitlyExpandDesignTimeFacades + + <_ResolveAssemblyReferenceResolvedFiles Include="@(ReferencePath)" Condition="'%(ReferencePath.ResolvedFrom)' == 'ImplicitlyExpandDesignTimeFacades'" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Microsoft.NETFramework.props b/src/XMakeTasks/Microsoft.NETFramework.props new file mode 100644 index 00000000000..fd676d4d89c --- /dev/null +++ b/src/XMakeTasks/Microsoft.NETFramework.props @@ -0,0 +1,46 @@ + + + + + + + + + $(MSBuildFrameworkToolsPath)\Microsoft.NetFramework.props + + + + + $(MSBuildToolsPath)\Microsoft.NetFramework.CurrentVersion.props + + + + + + + diff --git a/src/XMakeTasks/Microsoft.NETFramework.targets b/src/XMakeTasks/Microsoft.NETFramework.targets new file mode 100644 index 00000000000..08104b7ed95 --- /dev/null +++ b/src/XMakeTasks/Microsoft.NETFramework.targets @@ -0,0 +1,79 @@ + + + + + + + + + $(MSBuildFrameworkToolsPath)\Microsoft.NetFramework.targets + + + + + $(MSBuildToolsPath)\Microsoft.NetFramework.CurrentVersion.targets + + + + + + + $(ImportByWildcardBeforeMicrosoftNetFrameworkTargets) + true + + $(ImportByWildcardAfterMicrosoftNetFrameworkTargets) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftNetFrameworkTargets) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftNetFrameworkTargets) + true + + false + false + false + false + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.ServiceModel.Shim.targets b/src/XMakeTasks/Microsoft.ServiceModel.Shim.targets new file mode 100644 index 00000000000..b6273042508 --- /dev/null +++ b/src/XMakeTasks/Microsoft.ServiceModel.Shim.targets @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Microsoft.VisualBasic.CurrentVersion.targets b/src/XMakeTasks/Microsoft.VisualBasic.CurrentVersion.targets new file mode 100644 index 00000000000..339f96259f2 --- /dev/null +++ b/src/XMakeTasks/Microsoft.VisualBasic.CurrentVersion.targets @@ -0,0 +1,494 @@ + + + + + + true + true + true + true + + + + + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.VisualBasic.targets + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.Microsoft.VisualBasic.targets + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + .vb + VB + Managed + true + true + true + true + + + + + + File + + + BrowseObject + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + CONFIG="$(Configuration)" + $(FinalDefineConstants),DEBUG=-1 + $(FinalDefineConstants),TRACE=-1 + $(FinalDefineConstants),_MyType="$(MyType)" + $(FinalDefineConstants),PLATFORM="$(Platform)" + $(FinalDefineConstants),PLATFORM="AnyCPU" + $(FinalDefineConstants),$(DefineConstants) + + + true + + + + + + + + <_DebugSymbolsIntermediatePathTemporary Include="$(PdbFile)"/> + + <_DebugSymbolsIntermediatePath Include="@(_DebugSymbolsIntermediatePathTemporary->'%(RootDir)%(Directory)%(Filename).pdb')"/> + + + + _ComputeNonExistentFileProperty;ResolveCodeAnalysisRuleSet + true + + + + + + <_NoWarnings Condition=" '$(WarningLevel)' == '0' ">true + <_NoWarnings Condition=" '$(WarningLevel)' == '1' ">false + + + + + $(MsBuildToolsPath) + + + $(IntermediateOutputPath)$(TargetName).compile.pdb + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + <_NoWarnings Condition=" '$(WarningLevel)' == '0' ">true + <_NoWarnings Condition=" '$(WarningLevel)' == '1' ">false + + + + + $(MsBuildToolsPath) + + + $(IntermediateOutputPath)$(TargetName).compile.pdb + + + + + + + + + + + false + + + + + + + + + + + + <_CoreCompileResourceInputs Remove="@(_CoreCompileResourceInputs)" /> + + + + + + + + + + + + + + + Option Strict Off + Option Explicit On + + Imports System + Imports System.Reflection + <Assembly: Global.System.Runtime.Versioning.TargetFrameworkAttribute("$(TargetFrameworkMoniker)", FrameworkDisplayName:="$(TargetFrameworkMonikerDisplayName)")> + + + + + + true + + + true + + + $([System.Globalization.CultureInfo]::CurrentUICulture.Name) + + + + + + <_ExplicitReference Include="$(FrameworkPathOverride)\System.dll" /> + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.VisualBasic.targets b/src/XMakeTasks/Microsoft.VisualBasic.targets new file mode 100644 index 00000000000..cc457afa8d7 --- /dev/null +++ b/src/XMakeTasks/Microsoft.VisualBasic.targets @@ -0,0 +1,185 @@ + + + + + + + + + + $(MSBuildFrameworkToolsPath)\Microsoft.VisualBasic.targets + + + $(MsBuildFrameworkToolsPath) + + + + + $(MSBuildToolsPath)\Microsoft.VisualBasic.CurrentVersion.targets + + + + + + + $(ImportByWildcardBeforeMicrosoftCommonTargets) + true + + $(ImportByWildcardAfterMicrosoftCommonTargets) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftCommonTargets) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftCommonTargets) + true + + false + false + false + false + + $(MSBuildExtensionsPath)\v4.0\Custom.Before.Microsoft.Common.targets + $(MSBuildExtensionsPath)\v4.0\Custom.After.Microsoft.Common.targets + + + $(ImportByWildcardBeforeMicrosoftVisualBasicTargets) + true + + $(ImportByWildcardAfterMicrosoftVisualBasicTargets) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftVisualBasicTargets) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftVisualBasicTargets) + true + + false + false + false + false + + $(MSBuildExtensionsPath)\v4.0\Custom.Before.Microsoft.VisualBasic.targets + $(MSBuildExtensionsPath)\v4.0\Custom.After.Microsoft.VisualBasic.targets + + + + + $(ImportByWildcardBeforeMicrosoftNetFrameworkProps) + true + + $(ImportByWildcardAfterMicrosoftNetFrameworkProps) + true + + $(ImportUserLocationsByWildcardBeforeMicrosoftNetFrameworkProps) + true + + $(ImportUserLocationsByWildcardAfterMicrosoftNetFrameworkProps) + true + + false + false + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildFrameworkToolsPath) + + diff --git a/src/XMakeTasks/Microsoft.VisualStudioVersion.v11.Common.props b/src/XMakeTasks/Microsoft.VisualStudioVersion.v11.Common.props new file mode 100644 index 00000000000..858e86ecee9 --- /dev/null +++ b/src/XMakeTasks/Microsoft.VisualStudioVersion.v11.Common.props @@ -0,0 +1,20 @@ + + + + + + 11.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + diff --git a/src/XMakeTasks/Microsoft.VisualStudioVersion.v12.Common.props b/src/XMakeTasks/Microsoft.VisualStudioVersion.v12.Common.props new file mode 100644 index 00000000000..6fb9cfdbfa4 --- /dev/null +++ b/src/XMakeTasks/Microsoft.VisualStudioVersion.v12.Common.props @@ -0,0 +1,20 @@ + + + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + diff --git a/src/XMakeTasks/Microsoft.VisualStudioVersion.v14.Common.props b/src/XMakeTasks/Microsoft.VisualStudioVersion.v14.Common.props new file mode 100644 index 00000000000..1f0a2830339 --- /dev/null +++ b/src/XMakeTasks/Microsoft.VisualStudioVersion.v14.Common.props @@ -0,0 +1,20 @@ + + + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + diff --git a/src/XMakeTasks/Microsoft.WinFx.Shim.targets b/src/XMakeTasks/Microsoft.WinFx.Shim.targets new file mode 100644 index 00000000000..7f7af1e0a18 --- /dev/null +++ b/src/XMakeTasks/Microsoft.WinFx.Shim.targets @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Microsoft.WorkflowBuildExtensions.Shim.targets b/src/XMakeTasks/Microsoft.WorkflowBuildExtensions.Shim.targets new file mode 100644 index 00000000000..ee25d359337 --- /dev/null +++ b/src/XMakeTasks/Microsoft.WorkflowBuildExtensions.Shim.targets @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/src/XMakeTasks/Microsoft.Xaml.Shim.targets b/src/XMakeTasks/Microsoft.Xaml.Shim.targets new file mode 100644 index 00000000000..899d3f02437 --- /dev/null +++ b/src/XMakeTasks/Microsoft.Xaml.Shim.targets @@ -0,0 +1,21 @@ + + + + + + $(MSBuildFrameworkToolsPath) + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Move.cs b/src/XMakeTasks/Move.cs new file mode 100644 index 00000000000..2ec35816e31 --- /dev/null +++ b/src/XMakeTasks/Move.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Moves files from one place to another. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using System.Runtime.InteropServices; +using System.ComponentModel; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task to move one or more files. + /// + /// + /// This does not support moving directories (ie, xcopy) + /// but this could restriction could be lifted as MoveFileEx, + /// which is used here, supports it. + /// + public class Move : TaskExtension, ICancelableTask + { + /// + /// Flags for MoveFileEx. + /// + /// + private const NativeMethods.MoveFileFlags Flags = NativeMethods.MoveFileFlags.MOVEFILE_WRITE_THROUGH | // Do not return until the Move is complete + NativeMethods.MoveFileFlags.MOVEFILE_REPLACE_EXISTING | // Replace any existing target + NativeMethods.MoveFileFlags.MOVEFILE_COPY_ALLOWED; // Moving across volumes is allowed + + /// + /// Subset of specified files that were actually moved + /// + private ITaskItem[] _movedFiles; + + /// + /// Whether we should cancel. + /// + private bool _canceling; + + /// + /// List of files to move. + /// + [Required] + public ITaskItem[] SourceFiles + { + get; + set; + } + + /// + /// Destination folder for all the source files. + /// + public ITaskItem DestinationFolder + { + get; + set; + } + + /// + /// Whether to overwrite files in the destination + /// that have the read-only attribute set. + /// Default is to not overwrite. + /// + public bool OverwriteReadOnlyFiles + { + get; + set; + } + + /// + /// Destination files matching each of the source files. + /// + [Output] + public ITaskItem[] DestinationFiles + { + get; + set; + } + + /// + /// Subset that were successfully moved + /// + [Output] + public ITaskItem[] MovedFiles + { + get { return _movedFiles; } + } + + /// + /// Stop and return (in an undefined state) as soon as possible. + /// + public void Cancel() + { + _canceling = true; + } + + /// + /// Main entry point. + /// + public override bool Execute() + { + bool success = true; + + // If there are no source files then just return success. + if (SourceFiles == null || SourceFiles.Length == 0) + { + DestinationFiles = new TaskItem[0]; + _movedFiles = new TaskItem[0]; + return true; + } + + // There must be a DestinationFolder (either files or directory). + if (DestinationFiles == null && DestinationFolder == null) + { + Log.LogErrorWithCodeFromResources("Move.NeedsDestination", "DestinationFiles", "DestinationDirectory"); + return false; + } + + // There can't be two kinds of destination. + if (DestinationFiles != null && DestinationFolder != null) + { + Log.LogErrorWithCodeFromResources("Move.ExactlyOneTypeOfDestination", "DestinationFiles", "DestinationDirectory"); + return false; + } + + // If the caller passed in DestinationFiles, then its length must match SourceFiles. + if (DestinationFiles != null && DestinationFiles.Length != SourceFiles.Length) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", DestinationFiles.Length, SourceFiles.Length, "DestinationFiles", "SourceFiles"); + return false; + } + + // If the caller passed in DestinationFolder, convert it to DestinationFiles + if (DestinationFiles == null) + { + DestinationFiles = new ITaskItem[SourceFiles.Length]; + + for (int i = 0; i < SourceFiles.Length; ++i) + { + // Build the correct path. + string destinationFile; + try + { + destinationFile = Path.Combine(DestinationFolder.ItemSpec, Path.GetFileName(SourceFiles[i].ItemSpec)); + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("Move.Error", SourceFiles[i].ItemSpec, DestinationFolder.ItemSpec, e.Message); + + // Clear the outputs. + DestinationFiles = new ITaskItem[0]; + return false; + } + + // Initialize the DestinationFolder item. + DestinationFiles[i] = new TaskItem(destinationFile); + } + } + + // Build up the sucessfully moved subset + ArrayList destinationFilesSuccessfullyMoved = new ArrayList(); + + // Now that we have a list of DestinationFolder files, move from source to DestinationFolder. + for (int i = 0; i < SourceFiles.Length && !_canceling; ++i) + { + string sourceFile = SourceFiles[i].ItemSpec; + string destinationFile = DestinationFiles[i].ItemSpec; + + try + { + if (MoveFileWithLogging(sourceFile, destinationFile)) + { + SourceFiles[i].CopyMetadataTo(DestinationFiles[i]); + destinationFilesSuccessfullyMoved.Add(DestinationFiles[i]); + } + else + { + success = false; + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("Move.Error", sourceFile, destinationFile, e.Message); + success = false; + + // Continue with the rest of the list + } + } + + // MovedFiles contains only the copies that were successful. + _movedFiles = (ITaskItem[])destinationFilesSuccessfullyMoved.ToArray(typeof(ITaskItem)); + + return success && !_canceling; + } + + /// + /// Makes the provided file writeable if necessary + /// + private static void MakeWriteableIfReadOnly(string file) + { + FileInfo info = new FileInfo(file); + if ((info.Attributes & FileAttributes.ReadOnly) != 0) + { + info.Attributes = info.Attributes & ~FileAttributes.ReadOnly; + } + } + + /// + /// Move one file from source to destination. Create the target directory if necessary. + /// + /// IO related exceptions + private bool MoveFileWithLogging + ( + string sourceFile, + string destinationFile + ) + { + if (Directory.Exists(destinationFile)) + { + Log.LogErrorWithCodeFromResources("Move.DestinationIsDirectory", sourceFile, destinationFile); + return false; + } + + if (Directory.Exists(sourceFile)) + { + // If the source file passed in is actually a directory instead of a file, log a nice + // error telling the user so. Otherwise, .NET Framework's File.Move method will throw + // an FileNotFoundException, which is not very useful to the user. + Log.LogErrorWithCodeFromResources("Move.SourceIsDirectory", sourceFile); + return false; + } + + // Check the source exists. + if (!File.Exists(sourceFile)) + { + Log.LogErrorWithCodeFromResources("Move.SourceDoesNotExist", sourceFile); + return false; + } + + // We can't ovewrite a file unless it's writeable + if (OverwriteReadOnlyFiles && File.Exists(destinationFile)) + { + MakeWriteableIfReadOnly(destinationFile); + } + + string destinationFolder = Path.GetDirectoryName(destinationFile); + + if (destinationFolder != null && destinationFolder.Length > 0 && !Directory.Exists(destinationFolder)) + { + Log.LogMessageFromResources(MessageImportance.Normal, "Move.CreatesDirectory", destinationFolder); + Directory.CreateDirectory(destinationFolder); + } + + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "Move.FileComment", sourceFile, destinationFile); + + // We want to always overwrite any existing destination file. + // Unlike File.Copy, File.Move does not have an overload to overwrite the destination. + // We cannot simply delete the destination file first because possibly it is also the source! + // Nor do we want to just do a Copy followed by a Delete, because for large files that will be slow. + // We are forced to use Win32's MoveFileEx. + bool result = NativeMethods.MoveFileEx(sourceFile, destinationFile, Flags); + + if (!result) + { + // It failed so we need a nice error message. Unfortunately + // Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); and + // throw new IOException((new Win32Exception(error)).Message) + // do not produce great error messages (eg., "The operation succeeded" (!)). + // For this reason the BCL has is own mapping in System.IO.__Error.WinIOError + // which is unfortunately internal. + // So try to get a nice message by using the BCL Move(), which will likely fail + // and throw. Otherwise use the "correct" method. + System.IO.File.Move(sourceFile, destinationFile); + + // Apparently that didn't throw, so.. + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + // If the destination file exists, then make sure it's read-write. + // The File.Move command copies attributes, but our move needs to + // leave the file writeable. + if (File.Exists(destinationFile)) + { + // Make it writable + MakeWriteableIfReadOnly(destinationFile); + } + + return true; + } + } +} diff --git a/src/XMakeTasks/NativeMethods.cs b/src/XMakeTasks/NativeMethods.cs new file mode 100644 index 00000000000..48717f9e8e8 --- /dev/null +++ b/src/XMakeTasks/NativeMethods.cs @@ -0,0 +1,1318 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Runtime.InteropServices; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using System.Collections; +using System.Runtime.ExceptionServices; + +namespace Microsoft.Build.Tasks +{ + /// + /// The original ITypeInfo interface in the CLR has incorrect definitions for GetRefTypeOfImplType and GetRefTypeInfo. + /// It uses ints for marshalling handles which will result in a crash on 64 bit systems. This is a temporary interface + /// for use until the one in the CLR is fixed. When it is we can go back to using ITypeInfo. + /// + [Guid("00020401-0000-0000-C000-000000000046")] + [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + public interface IFixedTypeInfo + { + void GetTypeAttr(out IntPtr ppTypeAttr); + void GetTypeComp(out System.Runtime.InteropServices.ComTypes.ITypeComp ppTComp); + void GetFuncDesc(int index, out IntPtr ppFuncDesc); + void GetVarDesc(int index, out IntPtr ppVarDesc); + void GetNames(int memid, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] String[] rgBstrNames, int cMaxNames, out int pcNames); + void GetRefTypeOfImplType(int index, out IntPtr href); + void GetImplTypeFlags(int index, out System.Runtime.InteropServices.ComTypes.IMPLTYPEFLAGS pImplTypeFlags); + void GetIDsOfNames([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 1), In] String[] rgszNames, int cNames, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] int[] pMemId); + void Invoke([MarshalAs(UnmanagedType.IUnknown)] Object pvInstance, int memid, Int16 wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, IntPtr pVarResult, IntPtr pExcepInfo, out int puArgErr); + void GetDocumentation(int index, out String strName, out String strDocString, out int dwHelpContext, out String strHelpFile); + void GetDllEntry(int memid, System.Runtime.InteropServices.ComTypes.INVOKEKIND invKind, IntPtr pBstrDllName, IntPtr pBstrName, IntPtr pwOrdinal); + void GetRefTypeInfo(IntPtr hRef, out IFixedTypeInfo ppTI); + void AddressOfMember(int memid, System.Runtime.InteropServices.ComTypes.INVOKEKIND invKind, out IntPtr ppv); + void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] Object pUnkOuter, [In] ref Guid riid, [MarshalAs(UnmanagedType.IUnknown), Out] out Object ppvObj); + void GetMops(int memid, out String pBstrMops); + void GetContainingTypeLib(out System.Runtime.InteropServices.ComTypes.ITypeLib ppTLB, out int pIndex); + [PreserveSig] + void ReleaseTypeAttr(IntPtr pTypeAttr); + [PreserveSig] + void ReleaseFuncDesc(IntPtr pFuncDesc); + [PreserveSig] + void ReleaseVarDesc(IntPtr pVarDesc); + } + + [GuidAttribute("00020406-0000-0000-C000-000000000046")] + [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] + [ComImport] + internal interface UCOMICreateITypeLib + { + void CreateTypeInfo(); + void SetName(); + void SetVersion(); + void SetGuid(); + void SetDocString(); + void SetHelpFileName(); + void SetHelpContext(); + void SetLcid(); + void SetLibFlags(); + void SaveAllChanges(); + } + + [ComImport] + [Guid("E5CB7A31-7512-11d2-89CE-0080C792E5D8")] + [TypeLibType(TypeLibTypeFlags.FCanCreate)] + [ClassInterface(ClassInterfaceType.None)] + internal class CorMetaDataDispenser + { + } + + [ComImport] + [Guid("809c652e-7396-11d2-9771-00a0c9b4d50c")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown /*0x0001*/)] + [TypeLibType(TypeLibTypeFlags.FRestricted /*0x0200*/)] + internal interface IMetaDataDispenser + { + [return: MarshalAs(UnmanagedType.Interface)] + object DefineScope([In] ref Guid rclsid, [In] UInt32 dwCreateFlags, [In] ref Guid riid); + + [return: MarshalAs(UnmanagedType.Interface)] + object OpenScope([In][MarshalAs(UnmanagedType.LPWStr)] string szScope, [In] UInt32 dwOpenFlags, [In] ref Guid riid); + + [return: MarshalAs(UnmanagedType.Interface)] + object OpenScopeOnMemory([In] IntPtr pData, [In] UInt32 cbData, [In] UInt32 dwOpenFlags, [In] ref Guid riid); + } + + [ComImport] + [Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IMetaDataImport + { + // PreserveSig because this method is an exception that + // actually returns void, not HRESULT. + [PreserveSig] + void CloseEnum(); + void CountEnum(IntPtr iRef, ref System.UInt32 ulCount); + void ResetEnum(); + void EnumTypeDefs(); + void EnumInterfaceImpls(); + void EnumTypeRefs(); + void FindTypeDefByName(); + void GetScopeProps(); + void GetModuleFromScope(); + void GetTypeDefProps(); + void GetInterfaceImplProps(); + void GetTypeRefProps(); + void ResolveTypeRef(); + void EnumMembers(); + void EnumMembersWithName(); + void EnumMethods(); + void EnumMethodsWithName(); + void EnumFields(); + void EnumFieldsWithName(); + void EnumParams(); + void EnumMemberRefs(); + void EnumMethodImpls(); + void EnumPermissionSets(); + void FindMember(); + void FindMethod(); + void FindField(); + void FindMemberRef(); + void GetMethodProps(); + void GetMemberRefProps(); + void EnumProperties(); + void EnumEvents(); + void GetEventProps(); + void EnumMethodSemantics(); + void GetMethodSemantics(); + void GetClassLayout(); + void GetFieldMarshal(); + void GetRVA(); + void GetPermissionSetProps(); + void GetSigFromToken(); + void GetModuleRefProps(); + void EnumModuleRefs(); + void GetTypeSpecFromToken(); + void GetNameFromToken(); + void EnumUnresolvedMethods(); + void GetUserString(); + void GetPinvokeMap(); + void EnumSignatures(); + void EnumTypeSpecs(); + void EnumUserStrings(); + void GetParamForMethodIndex(); + void EnumCustomAttributes(); + void GetCustomAttributeProps(); + void FindTypeRef(); + void GetMemberProps(); + void GetFieldProps(); + void GetPropertyProps(); + void GetParamProps(); + void GetCustomAttributeByName(); + void IsValidToken(); // Note: Need preservesig for this if ever going to be used. + void GetNestedClassProps(); + void GetNativeCallConvFromSig(); + void IsGlobal(); + } + + [ComImport] + [Guid("FCE5EFA0-8BBA-4f8e-A036-8F2022B08466")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IMetaDataImport2 + { + void CloseEnum(); + void CountEnum(); + void ResetEnum(); + void EnumTypeDefs(); + void EnumInterfaceImpls(); + void EnumTypeRefs(); + void FindTypeDefByName(); + void GetScopeProps(); + void GetModuleFromScope(); + void GetTypeDefProps(); + void GetInterfaceImplProps(); + void GetTypeRefProps(); + void ResolveTypeRef(); + void EnumMembers(); + void EnumMembersWithName(); + void EnumMethods(); + void EnumMethodsWithName(); + void EnumFields(); + void EnumFieldsWithName(); + void EnumParams(); + void EnumMemberRefs(); + void EnumMethodImpls(); + void EnumPermissionSets(); + void FindMember(); + void FindMethod(); + void FindField(); + void FindMemberRef(); + void GetMethodProps(); + void GetMemberRefProps(); + void EnumProperties(); + void EnumEvents(); + void GetEventProps(); + void EnumMethodSemantics(); + void GetMethodSemantics(); + void GetClassLayout(); + void GetFieldMarshal(); + void GetRVA(); + void GetPermissionSetProps(); + void GetSigFromToken(); + void GetModuleRefProps(); + void EnumModuleRefs(); + void GetTypeSpecFromToken(); + void GetNameFromToken(); + void EnumUnresolvedMethods(); + void GetUserString(); + void GetPinvokeMap(); + void EnumSignatures(); + void EnumTypeSpecs(); + void EnumUserStrings(); + void GetParamForMethodIndex(); + void EnumCustomAttributes(); + void GetCustomAttributeProps(); + void FindTypeRef(); + void GetMemberProps(); + void GetFieldProps(); + void GetPropertyProps(); + void GetParamProps(); + [PreserveSig] + int GetCustomAttributeByName(UInt32 mdTokenObj, [MarshalAs(UnmanagedType.LPWStr)] string szName, out IntPtr ppData, out uint pDataSize); + void IsValidToken(); + void GetNestedClassProps(); + void GetNativeCallConvFromSig(); + void IsGlobal(); + void EnumGenericParams(); + void GetGenericParamProps(); + void GetMethodSpecProps(); + void EnumGenericParamConstraints(); + void GetGenericParamConstraintProps(); + void GetPEKind(out UInt32 pdwPEKind, out UInt32 pdwMachine); + void GetVersionString([MarshalAs(UnmanagedType.LPArray)] char[] pwzBuf, UInt32 ccBufSize, out UInt32 pccBufSize); + } + + // Flags for OpenScope + internal enum CorOpenFlags + { + ofRead = 0x00000000, // Open scope for read + ofWrite = 0x00000001, // Open scope for write. + ofCopyMemory = 0x00000002, // Open scope with memory. Ask metadata to maintain its own copy of memory. + ofCacheImage = 0x00000004, // EE maps but does not do relocations or verify image + ofNoTypeLib = 0x00000080, // Don't OpenScope on a typelib. + }; + + [ComImport] + [Guid("EE62470B-E94B-424e-9B7C-2F00C9249F93")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IMetaDataAssemblyImport + { + void GetAssemblyProps(UInt32 mdAsm, out IntPtr pPublicKeyPtr, out UInt32 ucbPublicKeyPtr, out UInt32 uHashAlg, [MarshalAs(UnmanagedType.LPArray)] char[] strName, UInt32 cchNameIn, out UInt32 cchNameRequired, IntPtr amdInfo, out UInt32 dwFlags); + void GetAssemblyRefProps(UInt32 mdAsmRef, out IntPtr ppbPublicKeyOrToken, out UInt32 pcbPublicKeyOrToken, [MarshalAs(UnmanagedType.LPArray)] char[] strName, UInt32 cchNameIn, out UInt32 pchNameOut, IntPtr amdInfo, out IntPtr ppbHashValue, out UInt32 pcbHashValue, out UInt32 pdwAssemblyRefFlags); + void GetFileProps([In] UInt32 mdFile, [MarshalAs(UnmanagedType.LPArray)] char[] strName, UInt32 cchName, out UInt32 cchNameRequired, out IntPtr bHashData, out UInt32 cchHashBytes, out UInt32 dwFileFlags); + void GetExportedTypeProps(); + void GetManifestResourceProps(); + void EnumAssemblyRefs([In, Out] ref IntPtr phEnum, [MarshalAs(UnmanagedType.LPArray), Out] UInt32[] asmRefs, System.UInt32 asmRefCount, out System.UInt32 iFetched); + void EnumFiles([In, Out] ref IntPtr phEnum, [MarshalAs(UnmanagedType.LPArray), Out] UInt32[] fileRefs, System.UInt32 fileRefCount, out System.UInt32 iFetched); + void EnumExportedTypes(); + void EnumManifestResources(); + void GetAssemblyFromScope(out UInt32 mdAsm); + void FindExportedTypeByName(); + void FindManifestResourceByName(); + // PreserveSig because this method is an exception that + // actually returns void, not HRESULT. + [PreserveSig] + void CloseEnum([In] IntPtr phEnum); + void FindAssembliesByName(); + } + + [ComImport] + [Guid("00000001-0000-0000-c000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IClassFactory + { + void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown), Out] out object ppvObject); + void LockServer(bool fLock); + } + + // Subset of CorAssemblyFlags from corhdr.h + internal enum CorAssemblyFlags : uint + { + afPublicKey = 0x0001, // The assembly ref holds the full (unhashed) public key. + afRetargetable = 0x0100 // The assembly can be retargeted (at runtime) to an + // assembly from a different publisher. + }; + [StructLayout(LayoutKind.Sequential)] + + /* + From cor.h: + typedef struct + { + USHORT usMajorVersion; // Major Version. + USHORT usMinorVersion; // Minor Version. + USHORT usBuildNumber; // Build Number. + USHORT usRevisionNumber; // Revision Number. + LPWSTR szLocale; // Locale. + ULONG cbLocale; // [IN/OUT] Size of the buffer in wide chars/Actual size. + DWORD *rProcessor; // Processor ID array. + ULONG ulProcessor; // [IN/OUT] Size of the Processor ID array/Actual # of entries filled in. + OSINFO *rOS; // OSINFO array. + ULONG ulOS; // [IN/OUT]Size of the OSINFO array/Actual # of entries filled in. + } ASSEMBLYMETADATA; + */ + internal struct ASSEMBLYMETADATA + { + public UInt16 usMajorVersion; + public UInt16 usMinorVersion; + public UInt16 usBuildNumber; + public UInt16 usRevisionNumber; + public IntPtr rpLocale; + public UInt32 cchLocale; + public IntPtr rpProcessors; + public UInt32 cProcessors; + public IntPtr rOses; + public UInt32 cOses; + } + + internal enum ASSEMBLYINFO_FLAG + { + VALIDATE = 1, + GETSIZE = 2 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ASSEMBLY_INFO + { + public uint cbAssemblyInfo; + public uint dwAssemblyFlags; + public ulong uliAssemblySizeInKB; + [MarshalAs(UnmanagedType.LPWStr)] + public string pszCurrentAssemblyPathBuf; + public uint cchBuf; + } + + [ComImport(), Guid("E707DCDE-D1CD-11D2-BAB9-00C04F8ECEAE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAssemblyCache + { + /* Unused. + [PreserveSig()] + int UninstallAssembly(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName, IntPtr pvReserved, int pulDisposition); + */ + int UninstallAssembly(); + + [PreserveSig()] + uint QueryAssemblyInfo(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName, ref ASSEMBLY_INFO pAsmInfo); + + /* Unused. + [PreserveSig()] + int CreateAssemblyCacheItem(uint dwFlags, IntPtr pvReserved, out object ppAsmItem, [MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName); + */ + int CreateAssemblyCacheItem(); + + /* Unused. + [PreserveSig()] + int CreateAssemblyScavenger(out object ppAsmScavenger); + */ + int CreateAssemblyScavenger(); + + /* Unused. + [PreserveSig()] + int InstallAssembly(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszManifestFilePath, IntPtr pvReserved); + */ + int InstallAssembly(); + } + + [Flags] + internal enum AssemblyCacheFlags + { + ZAP = 1, + GAC = 2, + DOWNLOAD = 4 + } + + [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("CD193BC0-B4BC-11d2-9833-00C04FC31D2E")] + internal interface IAssemblyName + { + [PreserveSig()] + int SetProperty( + int PropertyId, + IntPtr pvProperty, + int cbProperty); + + [PreserveSig()] + int GetProperty( + int PropertyId, + IntPtr pvProperty, + ref int pcbProperty); + + [PreserveSig()] + int Finalize(); + + [PreserveSig()] + int GetDisplayName( + StringBuilder pDisplayName, + ref int pccDisplayName, + int displayFlags); + + [PreserveSig()] + int Reserved(ref Guid guid, + Object obj1, + Object obj2, + String string1, + Int64 llFlags, + IntPtr pvReserved, + int cbReserved, + out IntPtr ppv); + + [PreserveSig()] + int GetName( + ref int pccBuffer, + StringBuilder pwzName); + + [PreserveSig()] + int GetVersion( + out int versionHi, + out int versionLow); + [PreserveSig()] + int IsEqual( + IAssemblyName pAsmName, + int cmpFlags); + + [PreserveSig()] + int Clone(out IAssemblyName pAsmName); + }// IAssemblyName + + [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("21b8916c-f28e-11d2-a473-00c04f8ef448")] + internal interface IAssemblyEnum + { + [PreserveSig()] + int GetNextAssembly( + IntPtr pvReserved, + out IAssemblyName ppName, + int flags); + [PreserveSig()] + int Reset(); + [PreserveSig()] + int Clone(out IAssemblyEnum ppEnum); + }// IAssemblyEnum + + internal enum CreateAssemblyNameObjectFlags + { + CANOF_DEFAULT = 0, + CANOF_PARSE_DISPLAY_NAME = 1, + } + + [Flags] + internal enum AssemblyNameDisplayFlags + { + VERSION = 0x01, + CULTURE = 0x02, + PUBLIC_KEY_TOKEN = 0x04, + PROCESSORARCHITECTURE = 0x20, + RETARGETABLE = 0x80, + // This enum will change in the future to include + // more attributes. + ALL = VERSION + | CULTURE + | PUBLIC_KEY_TOKEN + | PROCESSORARCHITECTURE + | RETARGETABLE + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct STARTUPINFO + { + internal Int32 cb; + internal string lpReserved; + internal string lpDesktop; + internal string lpTitle; + internal Int32 dwX; + internal Int32 dwY; + internal Int32 dwXSize; + internal Int32 dwYSize; + internal Int32 dwXCountChars; + internal Int32 dwYCountChars; + internal Int32 dwFillAttribute; + internal Int32 dwFlags; + internal Int16 wShowWindow; + internal Int16 cbReserved2; + internal IntPtr lpReserved2; + internal IntPtr hStdInput; + internal IntPtr hStdOutput; + internal IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + /// + /// Interop methods. + /// + internal static class NativeMethods + { + #region Constants + + internal static readonly IntPtr NullPtr = IntPtr.Zero; + internal static readonly IntPtr InvalidIntPtr = new IntPtr((int)-1); + + internal const uint NORMAL_PRIORITY_CLASS = 0x0020; + internal const uint CREATE_NO_WINDOW = 0x08000000; + internal const Int32 STARTF_USESTDHANDLES = 0x00000100; + internal const int ERROR_SUCCESS = 0; + + internal const int TYPE_E_REGISTRYACCESS = -2147319780; + internal const int TYPE_E_CANTLOADLIBRARY = -2147312566; + + internal const int HRESULT_E_CLASSNOTREGISTERED = -2147221164; + + internal const int ERROR_ACCESS_DENIED = -2147024891; // ACL'd or r/o + internal const int ERROR_SHARING_VIOLATION = -2147024864; // File locked by another use + + internal static Guid GUID_TYPELIB_NAMESPACE = new Guid("{0F21F359-AB84-41E8-9A78-36D110E6D2F9}"); + internal static Guid GUID_ExportedFromComPlus = new Guid("{90883f05-3d28-11d2-8f17-00a0c9a6186d}"); + + internal static Guid IID_IUnknown = new Guid("{00000000-0000-0000-C000-000000000046}"); + internal static Guid IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}"); + internal static Guid IID_ITypeInfo = new Guid("{00020401-0000-0000-C000-000000000046}"); + internal static Guid IID_IEnumVariant = new Guid("{00020404-0000-0000-C000-000000000046}"); + internal static Guid IID_IDispatchEx = new Guid("{A6EF9860-C720-11D0-9337-00A0C90DCAA9}"); + + internal static Guid IID_StdOle = new Guid("{00020430-0000-0000-C000-000000000046}"); + + // used in LoadTypeLibEx + internal enum REGKIND + { + REGKIND_DEFAULT = 0, + REGKIND_REGISTER = 1, + REGKIND_NONE = 2, + REGKIND_LOAD_TLB_AS_32BIT = 0x20, + REGKIND_LOAD_TLB_AS_64BIT = 0x40, + } + + // Set of IMAGE_FILE constants which represent the processor architectures for native assemblies. + internal const UInt16 IMAGE_FILE_MACHINE_UNKNOWN = 0x0; // The contents of this field are assumed to be applicable to any machine type + internal const UInt16 IMAGE_FILE_MACHINE_INVALID = UInt16.MaxValue; // Invalid value for the machine type. + internal const UInt16 IMAGE_FILE_MACHINE_AMD64 = 0x8664; // x64 + internal const UInt16 IMAGE_FILE_MACHINE_ARM = 0x1c0; // ARM little endian + internal const UInt16 IMAGE_FILE_MACHINE_ARMV7 = 0x1c4; // ARMv7 (or higher) Thumb mode only + internal const UInt16 IMAGE_FILE_MACHINE_I386 = 0x14c; // Intel 386 or later processors and compatible processors + internal const UInt16 IMAGE_FILE_MACHINE_IA64 = 0x200; // Intel Itanium processor family + internal const UInt16 IMAGE_FILE_MACHINE_R4000 = 0x166; // Used to test a architecture we do not expect to reference + + internal const uint GENERIC_READ = 0x80000000; + + internal const uint PAGE_READONLY = 0x02; + + internal const uint FILE_MAP_READ = 0x04; + + internal const uint FILE_TYPE_DISK = 0x01; + + internal const int SE_ERR_ACCESSDENIED = 5; + + // CryptoApi flags and constants + [Flags] + internal enum CryptFlags + { + Exportable = 0x1, + UserProtected = 0x2, + MachineKeySet = 0x20, + UserKeySet = 0x1000 + } + + internal enum KeySpec + { + AT_KEYEXCHANGE = 1, + AT_SIGNATURE = 2 + } + + internal enum BlobType + { + SIMPLEBLOB = 0x1, + PUBLICKEYBLOB = 0x6, + PRIVATEKEYBLOB = 0x7, + PLAINTEXTKEYBLOB = 0x8, + OPAQUEKEYBLOB = 0x9, + PUBLICKEYBLOBEX = 0xA, + SYMMETRICWRAPKEYBLOB = 0xB, + } + + [Flags] + internal enum CertStoreClose + { + CERT_CLOSE_STORE_FORCE_FLAG = 0x00000001, + CERT_CLOSE_STORE_CHECK_FLAG = 0x00000002, + } + + [Flags] + internal enum MoveFileFlags + { + MOVEFILE_REPLACE_EXISTING = 0x00000001, + MOVEFILE_COPY_ALLOWED = 0x00000002, + MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004, + MOVEFILE_WRITE_THROUGH = 0x00000008, + MOVEFILE_CREATE_HARDLINK = 0x00000010, + MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020 + } + + #endregion + + #region NT header stuff + + internal const uint IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b; + internal const uint IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b; + + internal const uint IMAGE_DIRECTORY_ENTRY_COMHEADER = 14; + + internal const uint COMIMAGE_FLAGS_STRONGNAMESIGNED = 0x08; + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_FILE_HEADER + { + internal ushort Machine; + internal ushort NumberOfSections; + internal uint TimeDateStamp; + internal uint PointerToSymbolTable; + internal uint NumberOfSymbols; + internal ushort SizeOfOptionalHeader; + internal ushort Characteristics; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_DATA_DIRECTORY + { + internal uint VirtualAddress; + internal uint Size; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_OPTIONAL_HEADER32 + { + internal ushort Magic; + internal byte MajorLinkerVersion; + internal byte MinorLinkerVersion; + internal uint SizeOfCode; + internal uint SizeOfInitializedData; + internal uint SizeOfUninitializedData; + internal uint AddressOfEntryPoint; + internal uint BaseOfCode; + internal uint BaseOfData; + internal uint ImageBase; + internal uint SectionAlignment; + internal uint FileAlignment; + internal ushort MajorOperatingSystemVersion; + internal ushort MinorOperatingSystemVersion; + internal ushort MajorImageVersion; + internal ushort MinorImageVersion; + internal ushort MajorSubsystemVersion; + internal ushort MinorSubsystemVersion; + internal uint Win32VersionValue; + internal uint SizeOfImage; + internal uint SizeOfHeaders; + internal uint CheckSum; + internal ushort Subsystem; + internal ushort DllCharacteristics; + internal uint SizeOfStackReserve; + internal uint SizeOfStackCommit; + internal uint SizeOfHeapReserve; + internal uint SizeOfHeapCommit; + internal uint LoaderFlags; + internal uint NumberOfRvaAndSizes; + + // should be: + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] internal IMAGE_DATA_DIRECTORY[] DataDirectory; + // but fixed size arrays only work with simple types, so I have to use ulongs and convert them to IMAGE_DATA_DIRECTORY structs + // Fortunately, IMAGE_DATA_DIRECTORY is only 8 bytes long... (whew) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + internal ulong[] DataDirectory; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_OPTIONAL_HEADER64 + { + internal ushort Magic; + internal byte MajorLinkerVersion; + internal byte MinorLinkerVersion; + internal uint SizeOfCode; + internal uint SizeOfInitializedData; + internal uint SizeOfUninitializedData; + internal uint AddressOfEntryPoint; + internal uint BaseOfCode; + internal ulong ImageBase; + internal uint SectionAlignment; + internal uint FileAlignment; + internal ushort MajorOperatingSystemVersion; + internal ushort MinorOperatingSystemVersion; + internal ushort MajorImageVersion; + internal ushort MinorImageVersion; + internal ushort MajorSubsystemVersion; + internal ushort MinorSubsystemVersion; + internal uint Win32VersionValue; + internal uint SizeOfImage; + internal uint SizeOfHeaders; + internal uint CheckSum; + internal ushort Subsystem; + internal ushort DllCharacteristics; + internal ulong SizeOfStackReserve; + internal ulong SizeOfStackCommit; + internal ulong SizeOfHeapReserve; + internal ulong SizeOfHeapCommit; + internal uint LoaderFlags; + internal uint NumberOfRvaAndSizes; + + // should be: + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] internal IMAGE_DATA_DIRECTORY[] DataDirectory; + // but fixed size arrays only work with simple types, so I have to use ulongs and convert them to IMAGE_DATA_DIRECTORY structs + // Fortunately, IMAGE_DATA_DIRECTORY is only 8 bytes long... (whew) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + internal ulong[] DataDirectory; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_NT_HEADERS32 + { + internal uint signature; + internal IMAGE_FILE_HEADER fileHeader; + internal IMAGE_OPTIONAL_HEADER32 optionalHeader; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_NT_HEADERS64 + { + internal uint signature; + internal IMAGE_FILE_HEADER fileHeader; + internal IMAGE_OPTIONAL_HEADER64 optionalHeader; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IMAGE_COR20_HEADER + { + internal uint cb; + internal ushort MajorRuntimeVersion; + internal ushort MinorRuntimeVersion; + internal IMAGE_DATA_DIRECTORY MetaData; + internal uint Flags; + internal uint EntryPointTokenOrEntryPointRVA; + internal IMAGE_DATA_DIRECTORY Resources; + internal IMAGE_DATA_DIRECTORY StrongNameSignature; + internal IMAGE_DATA_DIRECTORY CodeManagerTable; + internal IMAGE_DATA_DIRECTORY VTableFixups; + internal IMAGE_DATA_DIRECTORY ExportAddressTableJumps; + internal IMAGE_DATA_DIRECTORY ManagedNativeHeader; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct CRYPTOAPI_BLOB + { + internal uint cbData; + internal IntPtr pbData; + } + + #endregion + + #region PInvoke + private const string Crypt32DLL = "crypt32.dll"; + private const string Advapi32DLL = "advapi32.dll"; + private const string MscoreeDLL = "mscoree.dll"; + + //------------------------------------------------------------------------------ + // CreateHardLink + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool CreateHardLink(string newFileName, string exitingFileName, IntPtr securityAttributes); + + //------------------------------------------------------------------------------ + // MoveFileEx + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool MoveFileEx + ( + [In] string existingFileName, + [In] string newFileName, + [In] MoveFileFlags flags + ); + + //------------------------------------------------------------------------------ + // RegisterTypeLib + //------------------------------------------------------------------------------ + [DllImport("oleaut32", PreserveSig = false, EntryPoint = "RegisterTypeLib")] + internal static extern void RegisterTypeLib([In, MarshalAs(UnmanagedType.Interface)] object pTypeLib, [In, MarshalAs(UnmanagedType.LPWStr)] string szFullPath, [In, MarshalAs(UnmanagedType.LPWStr)] string szHelpDir); + + //------------------------------------------------------------------------------ + // UnRegisterTypeLib + //------------------------------------------------------------------------------ + [DllImport("oleaut32", PreserveSig = false, EntryPoint = "UnRegisterTypeLib")] + internal static extern void UnregisterTypeLib + ( + [In] ref Guid guid, + [In] short wMajorVerNum, + [In] short wMinorVerNum, + [In] int lcid, + [In] System.Runtime.InteropServices.ComTypes.SYSKIND syskind + ); + + //------------------------------------------------------------------------------ + // LoadTypeLib + //------------------------------------------------------------------------------ + [DllImport("oleaut32", PreserveSig = false, EntryPoint = "LoadTypeLibEx")] + [return: MarshalAs(UnmanagedType.Interface)] + internal static extern object LoadTypeLibEx([In, MarshalAs(UnmanagedType.LPWStr)] string szFullPath, [In] int regKind); + + //------------------------------------------------------------------------------ + // LoadRegTypeLib + //------------------------------------------------------------------------------ + [DllImport("oleaut32", PreserveSig = false)] + [return: MarshalAs(UnmanagedType.Interface)] + internal static extern object LoadRegTypeLib([In] ref Guid clsid, [In] short majorVersion, [In] short minorVersion, [In] int lcid); + + //------------------------------------------------------------------------------ + // QueryPathOfRegTypeLib + //------------------------------------------------------------------------------ + [DllImport("oleaut32", PreserveSig = false)] + [return: MarshalAs(UnmanagedType.BStr)] + internal static extern string QueryPathOfRegTypeLib([In] ref Guid clsid, [In] short majorVersion, [In] short minorVersion, [In] int lcid); + + //------------------------------------------------------------------------------ + // CreateFile + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, FileShare dwShareMode, + IntPtr lpSecurityAttributes, FileMode dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); + + //------------------------------------------------------------------------------ + // GetFileType + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern uint GetFileType(IntPtr hFile); + + //------------------------------------------------------------------------------ + // CloseHandle + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CloseHandle(IntPtr hObject); + + //------------------------------------------------------------------------------ + // CreateFileMapping + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CreateFileMapping(IntPtr hFile, IntPtr lpFileMappingAttributes, uint flProtect, + uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName); + + //------------------------------------------------------------------------------ + // MapViewOfFile + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern IntPtr MapViewOfFile(IntPtr hFileMapping, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, IntPtr dwNumberOfBytesToMap); + + //------------------------------------------------------------------------------ + // UnmapViewOfFile + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UnmapViewOfFile(IntPtr lpBaseAddress); + + //------------------------------------------------------------------------------ + // CreateProcess + //------------------------------------------------------------------------------ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CreateProcess + ( + string lpApplicationName, + string lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + [In, MarshalAs(UnmanagedType.Bool)] + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + [In] ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation + ); + + //------------------------------------------------------------------------------ + // ImageNtHeader + //------------------------------------------------------------------------------ + [DllImport("dbghelp.dll", SetLastError = true)] + internal static extern IntPtr ImageNtHeader(IntPtr imageBase); + + //------------------------------------------------------------------------------ + // ImageRvaToVa + //------------------------------------------------------------------------------ + [DllImport("dbghelp.dll", SetLastError = true)] + internal static extern IntPtr ImageRvaToVa(IntPtr ntHeaders, IntPtr imageBase, uint Rva, out IntPtr LastRvaSection); + + //------------------------------------------------------------------------------ + // CreateAssemblyCache + //------------------------------------------------------------------------------ + [DllImport("fusion.dll")] + internal static extern uint CreateAssemblyCache(out IAssemblyCache ppAsmCache, uint dwReserved); + + [DllImport("fusion.dll")] + internal static extern int CreateAssemblyEnum( + out IAssemblyEnum ppEnum, + IntPtr pUnkReserved, + IAssemblyName pName, + AssemblyCacheFlags flags, + IntPtr pvReserved); + + [DllImport("fusion.dll")] + internal static extern int CreateAssemblyNameObject( + out IAssemblyName ppAssemblyNameObj, + [MarshalAs(UnmanagedType.LPWStr)] + String szAssemblyName, + CreateAssemblyNameObjectFlags flags, + IntPtr pvReserved); + + /// + /// GetCachePath from fusion.dll. + /// Using StringBuilder here is a way to pass a preallocated buffer of characters to (native) functions that require it. + /// A common design pattern in unmanaged C++ is calling a function twice, once to determine the length of the string + /// and then again to pass the client-allocated character buffer. StringBuilder is the most straightforward way + /// to allocate a mutable buffer of characters and pass it around. + /// + [DllImport("fusion.dll", CharSet = CharSet.Unicode)] + internal static extern int GetCachePath(AssemblyCacheFlags cacheFlags, StringBuilder cachePath, ref int pcchPath); + + /*------------------------------------------------------------------------------ + CompareAssemblyIdentity + The Fusion API to compare two assembly identities to determine whether or not they are equivalent is now available. This new API is exported from mscorwks.dll, which you can access via mscoree's GetRealProcAddress. The function prototype is defined in fusion.h as follows: + + STDAPI CompareAssemblyIdentity(LPCWSTR pwzAssemblyIdentity1, + BOOL fUnified1, + LPCWSTR pwzAssemblyIdentity2, + BOOL fUnified2, + BOOL *pfEquivalent, + AssemblyComparisonResult *pResult); + +typedef enum _tagAssemblyComparisonResult +{ + ACR_Unknown, // Unknown + ACR_EquivalentFullMatch, // all fields match + ACR_EquivalentWeakNamed, // match based on weak-name, version numbers ignored + ACR_EquivalentFXUnified, // match based on FX-unification of version numbers + ACR_EquivalentUnified, // match based on legacy-unification of version numbers + ACR_NonEquivalentVersion, // all fields match except version field + ACR_NonEquivalent, // no match + + ACR_EquivalentPartialMatch, + ACR_EquivalentPartialWeakNamed, + ACR_EquivalentPartialUnified, + ACR_EquivalentPartialFXUnified, + ACR_NonEquivalentPartialVersion +} AssemblyComparisonResult; + + Parameters: + [in] LPCWSTR pwzAssemblyIdentity1 : Textual identity of the first assembly to be compared + [in] BOOL fUnified1 : Flag to indicate user-specified unification for pwzAssemblyIdentity1 (see below) + [in] LPCWSTR pwzAssemblyIdentity2 : Textual identity of the second assembly to be compared + [in] BOOL fUnified2 : Flag to inidcate user-specified unification for pwzAssemblyIdentity2 (see below) + [out] BOOL *pfEquivalent : Boolean indicating whether the identities are equivalent + [out] AssemblyComparisonResult *pResult : Contains detailed information about the comparison result + + This API will check whether or not pwzAssemblyIdentity1 and pwzAssemblyIdentity2 are equivalent. Both of these identities must be full-specified (name, version, pkt, culture). The pfEquivalent parameter will be set to TRUE if one (or more) of the following conditions is true: + + a) The assembly identities are equivalent. For strongly-named assemblies this means full match on (name, version, pkt, culture); for simply-named assemblies this means a match on (name, culture) + + b) The assemblies being compared are FX assemblies (even if the version numbers are not the same, these will compare as equivalent by way of unification) + + c) The assemblies are not FX assemblies but are equivalent because fUnified1 and/or fUnified2 were set. + + The fUnified flag is used to indicate that all versions up to the version number of the strongly-named assembly are considered equivalent to itself. For example, if pwzAssemblyIdentity1 is "foo, version=5.0.0.0, culture=neutral, publicKeyToken=...." and fUnified1==TRUE, then this means to treat all versions of the assembly in the range 0.0.0.0-5.0.0.0 to be equivalent to "foo, version=5.0.0.0, culture=neutral, publicKeyToken=...". If pwzAssemblyIdentity2 is the same as pwzAssemblyIdentity1, except has a lower version number (e.g. version range 0.0.0.0-5.0.0.0), then the API will return that the identities are equivalent. If pwzAssemblyIdentity2 is the same as pwzAssemblyIdentity1, but has a greater version number than 5.0.0.0 then the two identities will only be equivalent if fUnified2 is set. + + The AssemblyComparisonResult gives you information about why the identities compared as equal or not equal. The description of the meaning of each ACR_* return value is described in the declaration above. + ------------------------------------------------------------------------------*/ + [DllImport("fusion.dll", CharSet = CharSet.Unicode)] + internal static extern int CompareAssemblyIdentity + ( + string pwzAssemblyIdentity1, + [MarshalAs(UnmanagedType.Bool)] bool fUnified1, + string pwzAssemblyIdentity2, + [MarshalAs(UnmanagedType.Bool)] bool fUnified2, + [MarshalAs(UnmanagedType.Bool)] out bool pfEquivalent, + out AssemblyComparisonResult pResult + ); + + internal enum AssemblyComparisonResult + { + ACR_Unknown, // Unknown + ACR_EquivalentFullMatch, // all fields match + ACR_EquivalentWeakNamed, // match based on weak-name, version numbers ignored + ACR_EquivalentFXUnified, // match based on FX-unification of version numbers + ACR_EquivalentUnified, // match based on legacy-unification of version numbers + ACR_NonEquivalentVersion, // all fields match except version field + ACR_NonEquivalent, // no match + + ACR_EquivalentPartialMatch, + ACR_EquivalentPartialWeakNamed, + ACR_EquivalentPartialUnified, + ACR_EquivalentPartialFXUnified, + ACR_NonEquivalentPartialVersion + } + + //------------------------------------------------------------------------------ + // PFXImportCertStore + //------------------------------------------------------------------------------ + [DllImport(Crypt32DLL, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr PFXImportCertStore([In] IntPtr blob, [In] string password, [In] CryptFlags flags); + + //------------------------------------------------------------------------------ + // CertCloseStore + //------------------------------------------------------------------------------ + [DllImport(Crypt32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CertCloseStore([In] IntPtr CertStore, CertStoreClose Flags); + + //------------------------------------------------------------------------------ + // CertEnumCertificatesInStore + //------------------------------------------------------------------------------ + [DllImport(Crypt32DLL, SetLastError = true)] + internal static extern IntPtr CertEnumCertificatesInStore([In] IntPtr CertStore, [In] IntPtr PrevCertContext); + + //------------------------------------------------------------------------------ + // CryptAcquireCertificatePrivateKey + //------------------------------------------------------------------------------ + [DllImport(Crypt32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptAcquireCertificatePrivateKey([In] IntPtr CertContext, [In] uint flags, [In] IntPtr reserved, [In, Out] ref IntPtr CryptProv, [In, Out] ref KeySpec KeySpec, [In, Out, MarshalAs(UnmanagedType.Bool)] ref bool CallerFreeProv); + + //------------------------------------------------------------------------------ + // CryptGetUserKey + //------------------------------------------------------------------------------ + [DllImport(Advapi32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptGetUserKey([In] IntPtr CryptProv, [In] KeySpec KeySpec, [In, Out] ref IntPtr Key); + + //------------------------------------------------------------------------------ + // CryptExportKey + //------------------------------------------------------------------------------ + [DllImport(Advapi32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptExportKey([In] IntPtr Key, [In] IntPtr ExpKey, [In] BlobType type, [In] uint Flags, [In] IntPtr Data, [In, Out] ref uint DataLen); + + //------------------------------------------------------------------------------ + // CryptDestroyKey + //------------------------------------------------------------------------------ + [DllImport(Advapi32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CryptDestroyKey(IntPtr hKey); + + //------------------------------------------------------------------------------ + // CryptReleaseContext + //------------------------------------------------------------------------------ + [DllImport(Advapi32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal extern static bool CryptReleaseContext([In] IntPtr Prov, [In] uint Flags); + + //------------------------------------------------------------------------------ + // CertFreeCertificateContext + //------------------------------------------------------------------------------ + [DllImport(Crypt32DLL, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CertFreeCertificateContext(IntPtr CertContext); + + /// + /// Get the runtime version for a given file + /// + /// The path of the file to be examined + /// The buffer allocated for the version information that is returned. + /// The size, in wide characters, of szBuffer + /// The size, in bytes, of the returned szBuffer. + /// HResult + [DllImport(MscoreeDLL, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern uint GetFileVersion(String szFullPath, StringBuilder szBuffer, int cchBuffer, out uint dwLength); + #endregion + + #region Methods + + /// + /// Given a pointer to a metadata blob, read the string parameter from it. Returns true if + /// a valid string was constructed and false otherwise. + /// + /// Adapted from bizapps\server\designers\models\packagemodel\nativemethods.cs (TryReadStringArgument) and + /// the original ARD implementation in vsproject\compsvcspkg\enumcomplus.cpp (GetStringCustomAttribute) + /// This code was taken from the vsproject\ReferenceManager\Providers\NativeMethods.cs + /// + [HandleProcessCorruptedStateExceptions] + internal static unsafe bool TryReadMetadataString(string fullPath, IntPtr attrData, uint attrDataSize, out string strValue) + { + IntPtr attrDataPostProlog = IntPtr.Zero; + int attrDataOffset = 0; + int strLen = 0; + int i = 0; + strValue = null; + + try + { + // Blob structure for an attribute with a constructor receiving one string + // and no named parameters: + // + // [2 bytes] Prolog: unsigned int16 with value 0x0001 + // [1, 2 or 4 bytes] PackedLen: Number of bytes of string parameter + // [PackedLen bytes] String parameter encoded as UTF8 + // [1 byte] Name Parameter Count: Named parameter count equal to 0 + + // Minimum size is 4-bytes (Prolog + PackedLen). Prolog must be 0x0001. + if ((attrDataSize >= 4) && (Marshal.ReadInt16(attrData, attrDataOffset) == 1)) + { + int preReadOffset = 2; // pass the prolog + attrDataPostProlog = attrData + preReadOffset; + + // Get the offset at which the uncompressed data starts, and the + // length of the uncompressed data. + attrDataOffset = CorSigUncompressData(attrDataPostProlog, out strLen); + + if (strLen != -1) + { + // the full size of the blob we were passed in should be sufficient to + // cover the prolog, compressed string length, and actual string. + if (attrDataSize >= preReadOffset + attrDataOffset + strLen) + { + // Read in the uncompressed data + byte[] bytes = new byte[(int)strLen]; + for (i = 0; i < strLen; i++) + { + bytes[i] = Marshal.ReadByte(attrDataPostProlog, attrDataOffset + i); + } + + // And convert it to the output string. + strValue = new String(Encoding.UTF8.GetChars(bytes)); + } + else + { + return false; + } + } + } + else + { + return false; + } + } + catch (AccessViolationException) + { + // The Marshal.ReadXXXX functions throw AVs when they're fed an invalid pointer, and very occasionally, + // for some reason, on what seem to be otherwise perfectly valid assemblies (it must be + // intermittent given that otherwise the user would be completely unable to use the reference + // manager), the pointer that we generate to look up the AssemblyTitle is apparently invalid, + // or for some reason Marshal.ReadByte thinks it is. + // + return false; + } + + return (strValue != null); + } + + /// + /// Returns the number of bytes that compressed data -- the length of the uncompressed + /// data -- takes up, and has an out value of the length of the string. + /// + /// Decompression algorithm stolen from ndp\clr\src\toolbox\mdbg\corapi\metadata\cormetadata.cs, which + /// was translated from the base implementation in ndp\clr\src\inc\cor.h + /// This code was taken from the vsproject\ReferenceManager\Providers\NativeMethods.cs + /// + /// Pointer to the beginning of the data block + /// Length of the uncompressed data block + internal static unsafe int CorSigUncompressData(IntPtr data, out int uncompressedDataLength) + { + // As described in bizapps\server\designers\models\packagemodel\nativemethods.cs: + // The maximum encodable integer is 29 bits long, 0x1FFFFFFF. The compression algorithm used is as follows (bit 0 is the least significant bit): + // - If the value lies between 0 (0x00) and 127 (0x7F), inclusive, encode as a one-byte integer (bit 7 is clear, value held in bits 6 through 0) + // - If the value lies between 2^8 (0x80) and 2^14 - 1 (0x3FFF), inclusive, encode as a 2-byte integer with bit 15 set, bit 14 clear (value held in bits 13 through 0) + // - Otherwise, encode as a 4-byte integer, with bit 31 set, bit 30 set, bit 29 clear (value held in bits 28 through 0) + // - A null string should be represented with the reserved single byte 0xFF, and no following data + int count = -1; + byte* bytes = (byte*)(data); + uncompressedDataLength = 0; + + // Smallest. + if ((*bytes & 0x80) == 0x00) // 0??? ???? + { + uncompressedDataLength = *bytes; + count = 1; + } + // Medium. + else if ((*bytes & 0xC0) == 0x80) // 10?? ???? + { + uncompressedDataLength = (int)(((*bytes & 0x3f) << 8 | *(bytes + 1))); + count = 2; + } + else if ((*bytes & 0xE0) == 0xC0) // 110? ???? + { + uncompressedDataLength = (int)(((*bytes & 0x1f) << 24 | *(bytes + 1) << 16 | *(bytes + 2) << 8 | *(bytes + 3))); + count = 4; + } + + return count; + } + #endregion + #region InternalClass + + /// + /// This class is a wrapper over the native GAC enumeration API. + /// + [ComVisible(false)] + internal class AssemblyCacheEnum : IEnumerable + { + /// + /// The IAssemblyEnum interface which allows us to ask for the next assembly from the GAC enumeration. + /// + private IAssemblyEnum _assemblyEnum; + + /// + /// Are we done going through the enumeration. + /// + private bool _done; + + // null means enumerate all the assemblies + internal AssemblyCacheEnum(String assemblyName) + { + InitializeEnum(assemblyName); + } + + /// + /// Initialize the GAC Enum + /// + /// + private void InitializeEnum(String assemblyName) + { + IAssemblyName fusionName = null; + + int hr = 0; + + if (assemblyName != null) + { + hr = CreateAssemblyNameObject( + out fusionName, + assemblyName, + CreateAssemblyNameObjectFlags.CANOF_PARSE_DISPLAY_NAME /* parse components assuming the assemblyName is a fusion name, this does not have to be a full fusion name*/, + IntPtr.Zero); + } + + if (hr >= 0) + { + hr = CreateAssemblyEnum( + out _assemblyEnum, + IntPtr.Zero, + fusionName, + AssemblyCacheFlags.GAC, + IntPtr.Zero); + } + + if (hr < 0) + { + _assemblyEnum = null; + } + } + + public IEnumerator GetEnumerator() + { + int hr = 0; + IAssemblyName fusionName = null; + + if (_assemblyEnum == null) + { + yield break; + } + + if (_done) + { + yield break; + } + + while (!_done) + { + // Now get next IAssemblyName from m_AssemblyEnum + hr = _assemblyEnum.GetNextAssembly((IntPtr)0, out fusionName, 0); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + if (fusionName != null) + { + string assemblyFusionName = GetFullName(fusionName); + yield return new AssemblyNameExtension(assemblyFusionName); + } + else + { + _done = true; + yield break; + } + } + } + + private static string GetFullName(IAssemblyName fusionAsmName) + { + int ilen = 1024; + StringBuilder sDisplayName = new StringBuilder(ilen); + int hr = fusionAsmName.GetDisplayName(sDisplayName, ref ilen, (int)AssemblyNameDisplayFlags.ALL); + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + return sDisplayName.ToString(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ParserState.cs b/src/XMakeTasks/ParserState.cs new file mode 100644 index 00000000000..80148a3affe --- /dev/null +++ b/src/XMakeTasks/ParserState.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Collections; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: ParseState + * + * State used by the c# and vb parsers. Maintains information about + * what's being parsed and what has been seen so far. + * + */ + sealed internal class ParseState + { + // Currently resolving a namespace name? + private bool _resolvingNamespace; + // Currently resolving a class name? + private bool _resolvingClass; + // Currently inside an open conditional preprocessor directive? + private int _openConditionalDirectives = 0; + // The current namespace name as its being resolved. + private string _namespaceName; + // A stack of namespaces so that nested namespaces can be supported. + private Stack _namespaceStack = new Stack(); + + /* + * Method: ParseState + * + * Construct. + */ + internal ParseState() + { + Reset(); + } + + /* + * Method: ResolvingNamespace + * + * Get or set the ResolvingNamespace property. + */ + internal bool ResolvingNamespace + { + get { return _resolvingNamespace; } + set { _resolvingNamespace = value; } + } + + /* + * Method: ResolvingClass + * + * Get or set the ResolvingClass property. + */ + internal bool ResolvingClass + { + get { return _resolvingClass; } + set { _resolvingClass = value; } + } + + /* + * Method: InsideConditionalDirective + * + * Get the InsideConditionalDirective property. + */ + internal bool InsideConditionalDirective + { + get { return _openConditionalDirectives > 0; } + } + + /* + * Method: Namespace + * + * Get or set the Namespace property. + */ + internal string Namespace + { + get { return _namespaceName; } + set { _namespaceName = value; } + } + + /* + * Method: Reset + * + * Reset the state, but don't throw away namespace stack information. + */ + internal void Reset() + { + _resolvingNamespace = false; + _resolvingClass = false; + _namespaceName = String.Empty; + } + + /* + * Method: OpenConditionalDirective + * + * Note that we've entered a conditional directive + */ + internal void OpenConditionalDirective() + { + _openConditionalDirectives++; + } + + /* + * Method: CloseConditionalDirective + * + * Note that we've exited a conditional directive + */ + internal void CloseConditionalDirective() + { + _openConditionalDirectives--; + } + + /* + * Method: PushNamespacePart + * + * Push a namespace element onto the stack. May be null. + */ + internal void PushNamespacePart(string namespacePart) + { + _namespaceStack.Push(namespacePart); + } + + /* + * Method: PopNamespacePart + * + * Pop a namespace element from the stack. May be null. + */ + internal string PopNamespacePart() + { + if (_namespaceStack.Count == 0) + { + return null; + } + + return (string)_namespaceStack.Pop(); + } + + /* + * Method: ComposeQualifiedClassName + * + * Build a fully qualified (i.e. with the namespace) class name + * base on the contents of the stack. + */ + internal string ComposeQualifiedClassName(string className) + { + StringBuilder fullClass = new StringBuilder(1024); + foreach (string namespacePiece in _namespaceStack) + { + if (null != namespacePiece && namespacePiece.Length > 0) + { + fullClass.Insert(0, '.'); + fullClass.Insert(0, namespacePiece); + } + } + + // Append the class. + fullClass.Append(className); + return fullClass.ToString(); + } + } +} diff --git a/src/XMakeTasks/PiaReference.cs b/src/XMakeTasks/PiaReference.cs new file mode 100644 index 00000000000..34acd98ca9e --- /dev/null +++ b/src/XMakeTasks/PiaReference.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: PiaReference + * + * COM reference wrapper class for the tlbimp tool using a PIA. + * + */ + internal sealed class PiaReference : ComReference + { + #region Constructors + + /* + * Method: PiaReference constructor + * + */ + internal PiaReference(TaskLoggingHelper taskLoggingHelper, bool silent, ComReferenceInfo referenceInfo, string itemName) + : base(taskLoggingHelper, silent, referenceInfo, itemName) + { + // do nothing + } + + #endregion + + #region Methods + + /* + * Method: Resolve + * + * Gets the resolved assembly path for the typelib wrapper. + */ + internal override bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp) + { + wrapperInfo = null; + + // Let NDP do the dirty work... + TypeLibConverter converter = new TypeLibConverter(); + string asmName, asmCodeBase; + + if (!converter.GetPrimaryInteropAssembly(ReferenceInfo.attr.guid, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum, ReferenceInfo.attr.lcid, + out asmName, out asmCodeBase)) + { + return false; + } + + // let's try to load the assembly to determine its path and if it's there + try + { + if (asmCodeBase != null && asmCodeBase.Length > 0) + { + Uri uri = new Uri(asmCodeBase); + + // make sure the PIA can be loaded + Assembly assembly = Assembly.UnsafeLoadFrom(uri.LocalPath); + + // got here? then assembly must have been loaded successfully. + wrapperInfo = new ComReferenceWrapperInfo(); + wrapperInfo.path = uri.LocalPath; + wrapperInfo.assembly = assembly; + + // We need to remember the original assembly name of this PIA in case it gets redirected to a newer + // version and other COM components use that name to reference the PIA. assembly.FullName wouldn't + // work here since we'd get the redirected assembly name. + wrapperInfo.originalPiaName = new AssemblyNameExtension(AssemblyName.GetAssemblyName(uri.LocalPath)); + } + else + { + Assembly assembly = Assembly.Load(asmName); + + // got here? then assembly must have been loaded successfully. + wrapperInfo = new ComReferenceWrapperInfo(); + wrapperInfo.path = assembly.Location; + wrapperInfo.assembly = assembly; + + // We need to remember the original assembly name of this PIA in case it gets redirected to a newer + // version and other COM components use that name to reference the PIA. + wrapperInfo.originalPiaName = new AssemblyNameExtension(asmName, true); + } + } + catch (FileNotFoundException) + { + // This means that assembly file cannot be found. + // We don't need to do anything here; wrapperInfo is not set + // and we'll assume that the assembly doesn't exist. + } + catch (BadImageFormatException) + { + // Similar case as above, except we should additionally warn the user that the assembly file + // is not really a valid assembly file. + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.BadAssemblyImage", asmName); + } + } + + // have we found the wrapper? + if (wrapperInfo != null) + { + return true; + } + + return false; + } + + #endregion + } +} diff --git a/src/XMakeTasks/RCWForCurrentContext.cs b/src/XMakeTasks/RCWForCurrentContext.cs new file mode 100644 index 00000000000..c1b4ddcb082 --- /dev/null +++ b/src/XMakeTasks/RCWForCurrentContext.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Helpers for COM Interop. +//----------------------------------------------------------------------- + +using System; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace Microsoft.Build.Tasks.InteropUtilities +{ + /// + /// Create an RCW for the current context/apartment. + /// This improves performance of cross apartment calls as the CLR will only + /// cache marshalled pointers for an RCW created in the current context. + /// + /// Type of the RCW object + internal class RCWForCurrentContext : IDisposable where T : class + { + /// + /// The last RCW that was created for the current context. + /// + private T _rcwForCurrentCtx; + + /// + /// Indicates if we created the RCW and therefore need to release it's com reference. + /// + private bool _shouldReleaseRCW; + + /// + /// Constructor creates the new RCW in the current context. + /// + /// The RCW created in the original context. + public RCWForCurrentContext(T rcw) + { + // To improve performance we create a new RCW for the current context so we get + // the caching behaviour of the marshaled pointer. + // See RCW::GetComIPForMethodTableFromCache in ndp\clr\src\VM\RuntimeCallableWrapper.cpp + IntPtr iunknownPtr = Marshal.GetIUnknownForObject(rcw); + Object objInCurrentCtx = null; + + try + { + objInCurrentCtx = Marshal.GetObjectForIUnknown(iunknownPtr); + } + finally + { + Marshal.Release(iunknownPtr); + } + + Debug.Assert(objInCurrentCtx != null, "Unable to marshal COM Object to the current context (apartment). This will hurt performance."); + + // If we failed to create the new RCW we default to returning the original RCW. + if (objInCurrentCtx == null) + { + _shouldReleaseRCW = false; + _rcwForCurrentCtx = rcw; + } + else + { + _shouldReleaseRCW = true; + _rcwForCurrentCtx = objInCurrentCtx as T; + } + } + + /// + /// Finalizer + /// + ~RCWForCurrentContext() + { + Debug.Fail("This object requires explicit call to Dispose"); + CleanupComObject(); + } + + /// + /// Call this helper if your managed object is really an RCW to a COM object + /// and that COM object was created in a different apartment from where it is being accessed + /// + /// An RCW object created in the original apartment context. + /// A new RCW created in the current apartment context + public T RCW + { + get + { + if (null == _rcwForCurrentCtx) + { + throw new ObjectDisposedException("RCWForCurrentCtx"); + } + + return _rcwForCurrentCtx; + } + } + + /// + /// Override for IDisposable::Dispose + /// + /// + /// We created an RCW for the current apartment. When this object goes out of scope + /// we need to release the COM object before the apartment is released (via COUninitialize) + /// + public void Dispose() + { + CleanupComObject(); + GC.SuppressFinalize(this); + } + + /// + /// Cleanup our RCW com object references if required. + /// + private void CleanupComObject() + { + try + { + if (null != _rcwForCurrentCtx && + _shouldReleaseRCW && + Marshal.IsComObject(_rcwForCurrentCtx)) + { + Marshal.ReleaseComObject(_rcwForCurrentCtx); + } + } + finally + { + _rcwForCurrentCtx = null; + } + } + } +} diff --git a/src/XMakeTasks/RedistList.cs b/src/XMakeTasks/RedistList.cs new file mode 100644 index 00000000000..876a1369361 --- /dev/null +++ b/src/XMakeTasks/RedistList.cs @@ -0,0 +1,1224 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Win32; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: RedistList + * + */ + /// + /// Defines list of redistributable assemblies for use in dependency analysis. + /// The input is a set of XML files in a well known format consisting of + /// File elements. Each File element defines the assembly name of an assembly + /// that is part of a redistributable unit, such as the .NET Framework + /// (i.e. dotnetfx.exe) or the J# Framework. For the .NET Framework, these + /// data files are specified in a sub-folder of the .NET Framework named + /// "RedistList". This list is used by the build system to unify previous + /// Framework version dependencies to the current Framework version. + /// This list is also used by the deployment system to exclude Framework + /// dependencies from customer deployment packages. + /// + internal sealed class RedistList + { + // List of cached RedistList objects, the key is a semi-colon delimited list of data file paths + private readonly static Hashtable s_cachedRedistList = new Hashtable(StringComparer.OrdinalIgnoreCase); + // Process wide cache of redist lists found on disk under fx directories. + // K: target framework directory, V: redist lists found on disk underneath K + private static Dictionary s_redistListPathCache; + + // Lock object + private static readonly Object s_locker = new Object(); + + /// + /// When we check to see if an assembly is in this redist list we want to cache it so that if we ask again we do not + /// have to re-scan bits of the redist list and do the assemblynameExtension comparisons. + /// + private ConcurrentDictionary> _assemblyNameInRedist = new ConcurrentDictionary>(AssemblyNameComparer.GenericComparer); + + /// + /// AssemblyName to unified assemblyName. We make this kind of call a lot and also will ask for the same name multiple times. + /// + private ConcurrentDictionary _assemblyNameToUnifiedAssemblyName = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// When we check to see if an assembly is remapped we should cache the result because we may get asked the same question a number of times. + /// Since the remapping list does not change between builds neither would the results of the remapping for a given fusion name. + /// + private ConcurrentDictionary _remappingCache = new ConcurrentDictionary(AssemblyNameComparer.GenericComparerConsiderRetargetable); + + // List of cached BlackList RedistList objects, the key is a semi-colon delimited list of data file paths + private ConcurrentDictionary _cachedBlackList = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + + /***************Fields which are only set in the constructor and should not be modified by the class. **********************/ + // Array of errors encountered while reading files. + private ReadOnlyCollection _errors; + // Array of files corresponding to the errors above. + private ReadOnlyCollection _errorFilenames; + + // List of assembly entries loaded from the XML data files, one entry for each valid File element + private readonly ReadOnlyCollection _assemblyList; + + // Maps simple names to assembly entries, the key is a simple name and the value is an index into assemblyList + private readonly ReadOnlyDictionary _simpleNameMap; + + // Remapping entries read from xml files in the RedistList directory. + private readonly ReadOnlyCollection _remapEntries; + + // Constants for locating redist lists under an fx directory. + private const string MatchPattern = "*.xml"; + internal const string RedistListFolder = "RedistList"; + + private RedistList(AssemblyTableInfo[] assemblyTableInfos) + { + List errors = new List(); + List errorFilenames = new List(); + List assemblyList = new List(); + List remappingEntries = new List(); + + if (assemblyTableInfos == null) throw new ArgumentNullException("assemblyTableInfos"); + foreach (AssemblyTableInfo assemblyTableInfo in assemblyTableInfos) + { + ReadFile(assemblyTableInfo, assemblyList, errors, errorFilenames, remappingEntries); + } + + _errors = new ReadOnlyCollection(errors); + _errorFilenames = new ReadOnlyCollection(errorFilenames); + _remapEntries = new ReadOnlyCollection(remappingEntries); + + // With the same simple name and then the version so that for each simple name we want the assemblies to also be sorted by version. + assemblyList.Sort(s_sortByVersionDescending); + _assemblyList = new ReadOnlyCollection(assemblyList); + + Dictionary simpleNameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < assemblyList.Count; ++i) + { + AssemblyEntry entry = assemblyList[i]; + if (!simpleNameMap.ContainsKey(entry.SimpleName)) + simpleNameMap.Add(entry.SimpleName, i); + } + + _simpleNameMap = new ReadOnlyDictionary(simpleNameMap); + } + + /// + /// Returns any exceptions encountered while reading\parsing the XML. + /// + internal Exception[] Errors + { + get + { + return _errors.ToArray(); + } + } + + /// + /// Returns any exceptions encountered while reading\parsing the XML. + /// + internal string[] ErrorFileNames + { + get + { + return _errorFilenames.ToArray(); + } + } + + + /// + /// Returns the number of entries in the redist list + /// + internal int Count + { + get + { + return _assemblyList.Count; + } + } + + /// + /// Determines whether or not the specified assembly is part of the Framework. + /// Assemblies from a previous version of the Framework will be + /// correctly identified. + /// + public bool IsFrameworkAssembly(string assemblyName) + { + AssemblyEntry entry = GetUnifiedAssemblyEntry(assemblyName); + if (entry != null && !String.IsNullOrEmpty(entry.RedistName)) + return entry.RedistName.StartsWith("Microsoft-Windows-CLRCoreComp", StringComparison.OrdinalIgnoreCase); + else + return false; + } + + /// + /// Determines whether or not the specified assembly is a Prerequisite assembly. + /// A prerequisite assembly is any assembly that is flagged as being installed in the GAC + /// in a redist data file. + /// + public bool IsPrerequisiteAssembly(string assemblyName) + { + AssemblyEntry entry = GetUnifiedAssemblyEntry(assemblyName); + if (entry != null) + return entry.InGAC; + else + return false; + } + + /// + /// If there was a remapping entry in the redist list list then remap the passed in assemblynameextension + /// if not just return the original one. + /// + public AssemblyNameExtension RemapAssembly(AssemblyNameExtension extensionToRemap) + { + AssemblyNameExtension remappedExtension = null; + + if (!_remappingCache.TryGetValue(extensionToRemap, out remappedExtension)) + { + // We do not expect there to be more than a handfull of entries + foreach (AssemblyRemapping remapEntry in _remapEntries) + { + if (remapEntry.From.PartialNameCompare(extensionToRemap, true/* consider retargetable flag*/)) + { + remappedExtension = remapEntry.To; + break; + } + } + _remappingCache.TryAdd(extensionToRemap, remappedExtension); + } + + // Important to clone since we tend to mutate assemblyNameExtensions in RAR + return remappedExtension != null ? remappedExtension.Clone() : null; + } + + + /// + /// Determines whether or not the specified assembly is a redist root. + /// + internal bool? IsRedistRoot(string assemblyName) + { + AssemblyEntry entry = GetUnifiedAssemblyEntry(assemblyName); + if (entry != null) + { + return entry.IsRedistRoot; + } + else + { + return null; + } + } + + /// + /// Returns an instance of RedistList initialized from the framework folder for v2.0 + /// This function returns a statically cached object, so all calls will return the + /// same instance. + /// + public static RedistList GetFrameworkList20() + { + string frameworkVersion20Path = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version20); + string[] redistListPaths = new string[0]; + if (frameworkVersion20Path != null) + { + redistListPaths = RedistList.GetRedistListPathsFromDisk(frameworkVersion20Path); + } + + AssemblyTableInfo[] assemblyTableInfos = new AssemblyTableInfo[redistListPaths.Length]; + + for (int i = 0; i < redistListPaths.Length; ++i) + { + assemblyTableInfos[i] = new AssemblyTableInfo(redistListPaths[i], frameworkVersion20Path); + } + + return GetRedistList(assemblyTableInfos); + } + + + /// + /// Returns an instance of RedistList initialized from the framework folder for v3.0 + /// This function returns a statically cached object, so all calls will return the + /// same instance. + /// + public static RedistList GetFrameworkList30() + { + return GetFrameworkListFromReferenceAssembliesPath(TargetDotNetFrameworkVersion.Version30); + } + + /// + /// Returns an instance of RedistList initialized from the framework folder for v3.5 + /// This function returns a statically cached object, so all calls will return the + /// same instance. + /// + public static RedistList GetFrameworkList35() + { + return GetFrameworkListFromReferenceAssembliesPath(TargetDotNetFrameworkVersion.Version35); + } + + /// + /// This is owned by chris mann + /// + /// + /// + public static RedistList GetRedistListFromPath(string path) + { + string[] redistListPaths = (path == null) ? new string[0] : RedistList.GetRedistListPathsFromDisk(path); + + AssemblyTableInfo[] assemblyTableInfos = new AssemblyTableInfo[redistListPaths.Length]; + + for (int i = 0; i < redistListPaths.Length; ++i) + { + assemblyTableInfos[i] = new AssemblyTableInfo(redistListPaths[i], path); + } + + return GetRedistList(assemblyTableInfos); + } + + private static RedistList GetFrameworkListFromReferenceAssembliesPath(TargetDotNetFrameworkVersion version) + { + string referenceAssembliesPath = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(version); + + // On dogfood build machines, v3.5 is not formally installed, so this returns null. + // We don't use redist lists in this case. + string[] redistListPaths = (referenceAssembliesPath == null) ? new string[0] : RedistList.GetRedistListPathsFromDisk(referenceAssembliesPath); + + AssemblyTableInfo[] assemblyTableInfos = new AssemblyTableInfo[redistListPaths.Length]; + + for (int i = 0; i < redistListPaths.Length; ++i) + { + assemblyTableInfos[i] = new AssemblyTableInfo(redistListPaths[i], referenceAssembliesPath); + } + + return GetRedistList(assemblyTableInfos); + } + + /// + /// Given a framework directory path, this static method will find matching + /// redist list files underneath that path. A process-wide cache is used to + /// avoid hitting the disk multiple times for the same framework directory. + /// + /// + /// Array of paths to redist lists under given framework directory. + public static string[] GetRedistListPathsFromDisk(string frameworkDirectory) + { + ErrorUtilities.VerifyThrowArgumentNull(frameworkDirectory, "frameworkDirectory"); + + lock (s_locker) + { + if (s_redistListPathCache == null) + { + s_redistListPathCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (!s_redistListPathCache.ContainsKey(frameworkDirectory)) + { + string redistDirectory = Path.Combine(frameworkDirectory, RedistListFolder); + + if (Directory.Exists(redistDirectory)) + { + string[] results = Directory.GetFiles(redistDirectory, MatchPattern); + s_redistListPathCache.Add(frameworkDirectory, results); + + return s_redistListPathCache[frameworkDirectory]; + } + } + else + { + return s_redistListPathCache[frameworkDirectory]; + } + } + + return new string[0]; + } + + /// + /// The name of this redist. + /// + /// + /// + internal string RedistName(string assemblyName) + { + AssemblyEntry entry = GetUnifiedAssemblyEntry(assemblyName); + if (entry != null) + { + return entry.RedistName; + } + else + { + return null; + } + } + + /// + /// Returns an instance of RedistList initialized from the specified set of files. + /// This function returns a statically cached object, so subsequent calls with the same set + /// of files will return the same instance. + /// + public static RedistList GetRedistList(AssemblyTableInfo[] assemblyTables) + { + if (assemblyTables == null) throw new ArgumentNullException("assemblyTables"); + Array.Sort(assemblyTables); + + StringBuilder keyBuilder = assemblyTables.Length > 0 ? new StringBuilder(assemblyTables[0].Descriptor) : new StringBuilder(); + for (int i = 1; i < assemblyTables.Length; ++i) + { + keyBuilder.Append(';'); + keyBuilder.Append(assemblyTables[i].Descriptor); + } + + string key = keyBuilder.ToString(); + lock (s_locker) + { + if (s_cachedRedistList.ContainsKey(key)) + return (RedistList)s_cachedRedistList[key]; + + RedistList redistList = new RedistList(assemblyTables); + s_cachedRedistList.Add(key, redistList); + + return redistList; + } + } + + private static string GetSimpleName(string assemblyName) + { + if (assemblyName == null) throw new ArgumentNullException("assemblyName"); + int i = assemblyName.IndexOf(",", StringComparison.Ordinal); + return i > 0 ? assemblyName.Substring(0, i) : assemblyName; + } + + private AssemblyEntry GetUnifiedAssemblyEntry(string assemblyName) + { + if (assemblyName == null) throw new ArgumentNullException("assemblyName"); + AssemblyEntry unifiedEntry = null; + if (!_assemblyNameToUnifiedAssemblyName.TryGetValue(assemblyName, out unifiedEntry)) + { + string simpleName = GetSimpleName(assemblyName); + if (_simpleNameMap.ContainsKey(simpleName)) + { + // Provides the starting index into assemblyList of the simpleName + int index = (int)_simpleNameMap[simpleName]; + AssemblyNameExtension highestVersionInRedist = new AssemblyNameExtension(_assemblyList[index].FullName); + for (int i = index; i < _assemblyList.Count; ++i) + { + AssemblyEntry entry = _assemblyList[i]; + if (!string.Equals(simpleName, entry.SimpleName, StringComparison.OrdinalIgnoreCase)) + break; + + AssemblyNameExtension firstAssembly = new AssemblyNameExtension(assemblyName); + AssemblyNameExtension secondAssembly = entry.AssemblyNameExtension; + + bool matchNotConsideringVersion = firstAssembly.EqualsIgnoreVersion(secondAssembly); + + // Do not want to downgrade a version which would be the case where two assemblies match even if one has a version greater than the highest in the redist list. + if (matchNotConsideringVersion && highestVersionInRedist.Version <= secondAssembly.Version) + { + unifiedEntry = entry; + break; + } + } + } + + // unified entry can be null but this is used to keep us from trying to generate the unified name when one does not exist in the redist list. + _assemblyNameToUnifiedAssemblyName.TryAdd(assemblyName, unifiedEntry); + } + + return unifiedEntry; + } + + /// + /// Given an assemblyNameExtension, is that assembly name in the redist list or not. This will use partial matching and match as much of the fusion name as exists in the assemblyName passed in. + /// + public bool FrameworkAssemblyEntryInRedist(AssemblyNameExtension assemblyName) + { + ErrorUtilities.VerifyThrowArgumentNull(assemblyName, "assemblyName"); + + NGen isAssemblyNameInRedist = false; + if (!_assemblyNameInRedist.TryGetValue(assemblyName, out isAssemblyNameInRedist)) + { + string simpleName = GetSimpleName(assemblyName.Name); + if (_simpleNameMap.ContainsKey(simpleName)) + { + // Provides the starting index into assemblyList of the simpleName + int index = (int)_simpleNameMap[simpleName]; + for (int i = index; i < _assemblyList.Count; ++i) + { + AssemblyEntry entry = _assemblyList[i]; + if (!string.Equals(simpleName, entry.SimpleName, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + // Make sure the redist name starts with Microsoft-Windows-CLRCoreComp or else it could be a third party redist list. + if (!entry.RedistName.StartsWith("Microsoft-Windows-CLRCoreComp", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + AssemblyNameExtension firstAssembly = assemblyName; + AssemblyNameExtension secondAssembly = entry.AssemblyNameExtension; + if (firstAssembly.PartialNameCompare(secondAssembly, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken | PartialComparisonFlags.Culture)) + { + isAssemblyNameInRedist = true; + break; + } + } + } + + // We need to make the assemblyname immutable before we add it to the dictionary because the original object may be mutated afterward + _assemblyNameInRedist.TryAdd(assemblyName.CloneImmutable(), isAssemblyNameInRedist); + } + + return isAssemblyNameInRedist; + } + + /// + /// Returns the unified version of the specified assembly. + /// Assemblies from a previous version of the Framework will be + /// returned with the current runtime version. + /// + public string GetUnifiedAssemblyName(string assemblyName) + { + AssemblyEntry entry = GetUnifiedAssemblyEntry(assemblyName); + if (entry != null) + return entry.FullName; + else + return assemblyName; + } + + /// + /// Find every assembly full name that matches the given simple name. + /// + /// + /// The array of assembly names. + internal AssemblyEntry[] FindAssemblyNameFromSimpleName + ( + string simpleName + ) + { + List candidateNames = new List(); + + if (_simpleNameMap.ContainsKey(simpleName)) + { + int index = (int)_simpleNameMap[simpleName]; + for (int i = index; i < _assemblyList.Count; ++i) + { + AssemblyEntry entry = _assemblyList[i]; + if (!String.Equals(simpleName, entry.SimpleName, StringComparison.OrdinalIgnoreCase)) + { + break; + } + candidateNames.Add(entry); + } + } + + return candidateNames.ToArray(); + } + + /// + /// This method will take a list of AssemblyTableInfo and generate a black list by subtracting the + /// assemblies listed in the WhiteList from the RedistList. + /// + /// 1) If there are assemblies in the redist list and one or more client subset files are read in with matching names then + /// the subtraction will take place. If there were no matching redist lists read in the black list will be empty. + /// + /// 2) If the subset has a matching name but there are no files inside of it then the black list will contain ALL files in the redist list. + /// + /// 3) If the redist list assembly has a null or empty redist name or the subset list has a null or empty subset name they will not be used for black list generation. + /// + /// When generating the blacklist, we will first see if the black list is in the appdomain wide cache + /// so that we do not regenerate one for multiple calls using the same whiteListAssemblyTableInfo. + /// + /// + /// List of paths to white list xml files + /// A hashtable containing the full assembly names of black listed assemblies as the key, and null as the value. + /// If there is no assemblies in the redist list null is returned. + /// + internal Hashtable GenerateBlackList(AssemblyTableInfo[] whiteListAssemblyTableInfo, List whiteListErrors, List whiteListErrorFileNames) + { + // Return null if there are no assemblies in the redist list. + if (_assemblyList.Count == 0) + { + return null; + } + + // Sort so that the same set of whiteListAssemblyTableInfo will generate the same key for the cache + Array.Sort(whiteListAssemblyTableInfo); + + StringBuilder keyBuilder = whiteListAssemblyTableInfo.Length > 0 ? new StringBuilder(whiteListAssemblyTableInfo[0].Descriptor) : new StringBuilder(); + + // Concatenate the paths to the whitelist xml files together to get the key into the blacklist cache. + for (int i = 1; i < whiteListAssemblyTableInfo.Length; ++i) + { + keyBuilder.Append(';'); + keyBuilder.Append(whiteListAssemblyTableInfo[i].Descriptor); + } + + string key = keyBuilder.ToString(); + + Hashtable returnTable = null; + + if (!_cachedBlackList.TryGetValue(key, out returnTable)) + { + List whiteListAssemblies = new List(); + + // Unique list of redist names in the subset files read in. We use this to make sure we are subtracting from the correct framework list. + Hashtable uniqueClientListNames = new Hashtable(StringComparer.OrdinalIgnoreCase); + + // Get the assembly entries for the white list + foreach (AssemblyTableInfo info in whiteListAssemblyTableInfo) + { + List whiteListAssembliesReadIn = new List(); + + // Need to know how many errors are in the list before the read file call so that if the redist name is null due to an error + // we do not get a "redist name is null or empty" error when in actual fact it was a file not found error. + int errorsBeforeReadCall = whiteListErrors.Count; + + // Read in the subset list file. + string redistName = ReadFile(info, whiteListAssembliesReadIn, whiteListErrors, whiteListErrorFileNames, null); + + + // Get the client subset name which has been read in. + if (!String.IsNullOrEmpty(redistName)) + { + // Populate the list of assemblies which are to be used as white list assemblies. + whiteListAssemblies.AddRange(whiteListAssembliesReadIn); + + // We may have the same redist name for multiple files, we only want to get the set of unique names. + if (!uniqueClientListNames.ContainsKey(redistName)) + { + uniqueClientListNames[redistName] = null; + } + } + else + { + // There are no extra errors reading in the subset list file which would have caused the redist list name to be null or empty. + // This means the redist name read in must be null or empty + if (whiteListErrors.Count == errorsBeforeReadCall) + { + // The whiteList errors passes back problems reading the redist file through the use of an array containing exceptions + whiteListErrors.Add(new Exception(ResourceUtilities.FormatResourceString("ResolveAssemblyReference.NoSubSetRedistListName", info.Path))); + whiteListErrorFileNames.Add(info.Path); + } + } + } + + // Dont care about the case of the assembly name + Hashtable blackList = new Hashtable(StringComparer.OrdinalIgnoreCase); + + // Do we have any subset names? + bool uniqueClientNamesExist = uniqueClientListNames.Count > 0; + + // Fill the hashtable with the entries, if there are no white list assemblies the black list will contain all assemblies in the redist list + foreach (AssemblyEntry entry in _assemblyList) + { + string entryFullName = entry.FullName; + string redistName = entry.RedistName; + if (String.IsNullOrEmpty(redistName)) + { + // Ignore null or empty redist entries as we cannot match these up with any client subset lists. + continue; + } + + string hashKey = entryFullName + "," + redistName; + + // If there were no subset list names read in we cannot generate a black list. (warnings will be logged as part of the reading of the subset list). + if (uniqueClientNamesExist) + { + if (!blackList.ContainsKey(hashKey) && uniqueClientListNames.ContainsKey(redistName)) + { + blackList[hashKey] = entryFullName; + } + } + } + + + // Go through each of the white list assemblies and remove it from the black list. Do this based on the assembly name and the redist name + foreach (AssemblyEntry whiteListEntry in whiteListAssemblies) + { + blackList.Remove(whiteListEntry.FullName + "," + whiteListEntry.RedistName); + } + + //The output hashtable needs to be just the full names and not the names + redist name + Hashtable blackListOfAssemblyNames = new Hashtable(StringComparer.OrdinalIgnoreCase); + foreach (string name in blackList.Values) + { + blackListOfAssemblyNames[name] = null; + } + + _cachedBlackList.TryAdd(key, blackListOfAssemblyNames); + + return blackListOfAssemblyNames; + } + + return returnTable; + } + + /// + /// Read the redist list from disk. + /// XML formatting issues will recorded in the 'errors' collection. + /// + /// Information about the redistlist file. + /// Redist name of the redist list just read in + internal static string ReadFile(AssemblyTableInfo assemblyTableInfo, List assembliesList, List errorsList, List errorFilenamesList, List remapEntries) + { + string path = assemblyTableInfo.Path; + string redistName = null; + XmlTextReader reader = null; + + // Keep track of what assembly entries we have read in from the redist list, we want to track this because we need to know if there are duplicate entries + // if there are duplicate entries one with ingac = true and one with InGac=false we want to choose the one with ingac true. + // The reason we want to take the ingac True over ingac false is that this indicates the assembly IS in the gac. + Dictionary assemblyEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); + + try + { + reader = new XmlTextReader(path); + reader.DtdProcessing = DtdProcessing.Ignore; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + if (string.Equals(reader.Name, "FileList", StringComparison.OrdinalIgnoreCase)) + { + reader.MoveToFirstAttribute(); + do + { + if (string.Equals(reader.Name, "Redist", StringComparison.OrdinalIgnoreCase)) + { + redistName = reader.Value; + break; + } + } + while (reader.MoveToNextAttribute()); + reader.MoveToElement(); + + ParseFileListSection(assemblyTableInfo, path, redistName, reader, assemblyEntries, remapEntries); + } + + if (string.Equals(reader.Name, "Remap", StringComparison.OrdinalIgnoreCase)) + { + if (remapEntries != null) + { + ParseRemapSection(assemblyTableInfo, path, redistName, reader, remapEntries); + } + } + } + } + } + catch (XmlException ex) + { + // Log the error and continue on. + errorsList.Add(ex); + errorFilenamesList.Add(path); + } + catch (Exception ex) + { + // If there was a problem writing the file (like it's read-only or locked on disk, for + // example), then eat the exception and log a warning. Otherwise, rethrow. + if (ExceptionHandling.NotExpectedException(ex)) + throw; + + // Log the error and continue on. + errorsList.Add(ex); + errorFilenamesList.Add(path); + } + finally + { + if (reader != null) + { + reader.Close(); + } + } + + foreach (AssemblyEntry entry in assemblyEntries.Values) + { + assembliesList.Add(entry); + } + + return redistName; + } + + /// + /// Parse the remapping xml element in the redist list + /// + private static void ParseRemapSection(AssemblyTableInfo assemblyTableInfo, string path, string redistName, XmlTextReader reader, List mapping) + { + AssemblyNameExtension fromEntry = null; + AssemblyNameExtension toEntry = null; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + if (string.Equals(reader.Name, "From", StringComparison.OrdinalIgnoreCase) && !reader.IsEmptyElement && fromEntry == null) + { + AssemblyEntry newEntry = ReadFileListEntry(assemblyTableInfo, path, redistName, reader, false); + if (newEntry != null) + { + fromEntry = newEntry.AssemblyNameExtension; + } + } + + if (string.Equals(reader.Name, "To", StringComparison.OrdinalIgnoreCase) && fromEntry != null && toEntry == null) + { + AssemblyEntry newEntry = ReadFileListEntry(assemblyTableInfo, path, redistName, reader, false); + if (newEntry != null) + { + toEntry = newEntry.AssemblyNameExtension; + } + } + + if (fromEntry != null && toEntry != null) + { + AssemblyRemapping pair = new AssemblyRemapping(fromEntry, toEntry); + + if (!mapping.Any(x => x.From.Equals(pair.From))) + { + mapping.Add(pair); + } + + fromEntry = null; + toEntry = null; + } + } + + if (reader.NodeType == XmlNodeType.EndElement && string.Equals(reader.Name, "From", StringComparison.OrdinalIgnoreCase)) + { + fromEntry = null; + toEntry = null; + } + + if (reader.NodeType == XmlNodeType.EndElement && string.Equals(reader.Name, "Remap", StringComparison.OrdinalIgnoreCase)) + { + break; + } + } + } + + /// + /// Parse the FileList section in the redist list. + /// + private static void ParseFileListSection(AssemblyTableInfo assemblyTableInfo, string path, string redistName, XmlTextReader reader, Dictionary assemblyEntries, List remapEntries) + { + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + if (string.Equals(reader.Name, "File", StringComparison.OrdinalIgnoreCase)) + { + AssemblyEntry newEntry = ReadFileListEntry(assemblyTableInfo, path, redistName, reader, true); + if (newEntry != null) + { + // When comparing the assembly entries we want to compare the FullName which is a formatted as name, version, publicKeyToken and culture and whether the entry is a redistroot flag + // We do not need to add the redistName and the framework directory because this will be the same for all entries in the current redist list being read. + string hashIndex = String.Format(CultureInfo.InvariantCulture, "{0},{1}", newEntry.FullName, newEntry.IsRedistRoot == null ? "null" : newEntry.IsRedistRoot.ToString()); + + AssemblyEntry dictionaryEntry = null; + assemblyEntries.TryGetValue(hashIndex, out dictionaryEntry); + // If the entry is not in the hashtable or the entry is in the hashtable but the new entry has the ingac flag true, make sure the hashtable contains the entry with the ingac true. + if (dictionaryEntry == null || (dictionaryEntry != null && newEntry.InGAC)) + { + assemblyEntries[hashIndex] = newEntry; + } + } + } + + if (string.Equals(reader.Name, "Remap", StringComparison.OrdinalIgnoreCase)) + { + if (remapEntries != null) + { + ParseRemapSection(assemblyTableInfo, path, redistName, reader, remapEntries); + } + } + } + + // We are at the end of the fileList lets bail out and see if we can find other sections + if (reader.NodeType == XmlNodeType.EndElement && string.Equals(reader.Name, "FileList", StringComparison.OrdinalIgnoreCase)) + { + break; + } + } + } + + /// + /// Parse an individual FileListEntry in the redist list + /// + private static AssemblyEntry ReadFileListEntry(AssemblyTableInfo assemblyTableInfo, string path, string redistName, XmlTextReader reader, bool fullFusionNameRequired) + { + Dictionary attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + reader.MoveToFirstAttribute(); + do + { + attributes.Add(reader.Name, reader.Value); + } while (reader.MoveToNextAttribute()); + + reader.MoveToElement(); + + string name; + attributes.TryGetValue("AssemblyName", out name); + + string version; + attributes.TryGetValue("Version", out version); + + string publicKeyToken; + attributes.TryGetValue("PublicKeyToken", out publicKeyToken); + + string culture; + attributes.TryGetValue("Culture", out culture); + + string inGAC; + attributes.TryGetValue("InGAC", out inGAC); + + string retargetable; + attributes.TryGetValue("Retargetable", out retargetable); + + string isRedistRoot; + attributes.TryGetValue("IsRedistRoot", out isRedistRoot); + + bool inGACFlag; + if (!bool.TryParse(inGAC, out inGACFlag)) + { + inGACFlag = true; // true by default + } + bool retargetableFlag; + // The retargetable flag is Yes or No for some reason + retargetableFlag = "Yes".Equals(retargetable, StringComparison.OrdinalIgnoreCase); + + bool isRedistRootAsBoolean; + bool? isRedistRootFlag = null; // null by default. + if (bool.TryParse(isRedistRoot, out isRedistRootAsBoolean)) + { + isRedistRootFlag = isRedistRootAsBoolean; + } + + bool isValidEntry = !string.IsNullOrEmpty(name) && (!fullFusionNameRequired || (fullFusionNameRequired && !string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(publicKeyToken) && !string.IsNullOrEmpty(culture))); + Debug.Assert(isValidEntry, string.Format(CultureInfo.InvariantCulture, "Missing attribute in redist file: {0}, line #{1}", path, reader.LineNumber)); + AssemblyEntry newEntry = null; + if (isValidEntry) + { + // Get the new entry from the redist list + newEntry = new AssemblyEntry(name, version, publicKeyToken, culture, inGACFlag, isRedistRootFlag, redistName, assemblyTableInfo.FrameworkDirectory, retargetableFlag); + } + + return newEntry; + } + + #region Comparers + private readonly static IComparer s_sortByVersionDescending = new SortByVersionDescending(); + + /// + /// The redist list is a collection of AssemblyEntry. We would like to have the redist list sorted on two keys. + /// The first key is simple name, the simple names should be sorted alphabetically in ascending order (a,b,c,d,e). + /// When the simple names are the same the sorting shouldbe done by version number rather than the alphabetical representation of the version. + /// A numerical comparison is required because the alphabetical sort does not place the versions in numerical order. For example 1, 10, 2, 3, 4 + /// This sort should be done descending ( 10,9,8,7,6,5) so that if the resdist list is read from top to bottom the newest version is seen first. + /// + internal class SortByVersionDescending : IComparer, IComparer + { + public int Compare(object a, object b) + { + AssemblyEntry firstEntry = a as AssemblyEntry; + AssemblyEntry secondEntry = b as AssemblyEntry; + return Compare(firstEntry, secondEntry); + } + + public int Compare(AssemblyEntry firstEntry, AssemblyEntry secondEntry) + { + Debug.Assert(firstEntry != null && secondEntry != null); + if (firstEntry == null || secondEntry == null) return 0; + + AssemblyNameExtension firstAssemblyName = firstEntry.AssemblyNameExtension; + AssemblyNameExtension secondAssemblyName = secondEntry.AssemblyNameExtension; + + // We want to sort first on the assembly name. + int stringResult = string.Compare(firstAssemblyName.Name, secondAssemblyName.Name, StringComparison.OrdinalIgnoreCase); + + // If the simple names do not match then we do not need to sort based on version. + if (stringResult != 0) + { + return stringResult; + } + + // We now want to sort based on the version number + // The compare method is expected to return the following values: + // Less than zero = right instance is less than left. + // Zero = right instance is equal to left. + // Greater than zero = right instance is greater than left. + + // Want the greater version number to be on top in a list so we need to reverse the comparison + int returnValue = firstAssemblyName.Version.CompareTo(secondAssemblyName.Version); + if (returnValue == 0) + { + return 0; + } + + // The firstAssemblyName has a lower version than secondAssemblyName, we want to reverse them. + return -returnValue; + } + } + #endregion + } + + /// + /// Internal class representing a redist list or whitelist and its corresponding framework directory. + /// + internal class AssemblyTableInfo : IComparable + { + private readonly string _path; + private readonly string _frameworkDirectory; + private string _descriptor; + + internal AssemblyTableInfo(string path, string frameworkDirectory) + { + _path = path; + _frameworkDirectory = frameworkDirectory; + } + + internal string Path + { + get { return _path; } + } + + internal string FrameworkDirectory + { + get { return _frameworkDirectory; } + } + + internal string Descriptor + { + get + { + if (null == _descriptor) + { + _descriptor = _path + _frameworkDirectory; + } + return _descriptor; + } + } + + public int CompareTo(object obj) + { + AssemblyTableInfo that = (AssemblyTableInfo)obj; + return String.Compare(Descriptor, that.Descriptor, StringComparison.OrdinalIgnoreCase); + } + } + + /// + /// Provide a mechanism to determine where the subset white lists are located by searching the target framework folders + /// for a list of provided subset list names. + /// + internal class SubsetListFinder + { + #region Data + + // Process wide cache of subset lists found on disk under fx directories. + // K: target framework directory + subsetNames, V: subset list paths found on disk underneath the subsetList folder + private static Dictionary s_subsetListPathCache; + + // Locl for subsetListPathCache + private static Object s_subsetListPathCacheLock = new Object(); + + // Folder to look for the subset lists in under the target framework directories + private const string subsetListFolder = "SubsetList"; + + /// + /// The subset names to search for. + /// + private string[] _subsetToSearchFor; + + #endregion + + #region Constructor + + /// + /// This class takes in a list of subset names to look for and provides a method to search the target framework directories to see if those + /// files exist. + /// + /// String array of subset names, ie Client, Net, MySubset. This may be null or empty if no subsets were requested to be + /// found in the target framework directories. This can happen if the the subsets are instead passed in as InstalledDefaultSubsetTables + internal SubsetListFinder(string[] subsetToSearchFor) + { + ErrorUtilities.VerifyThrowArgumentNull(subsetToSearchFor, "subsetToSearchFor"); + _subsetToSearchFor = subsetToSearchFor; + } + + #endregion + + #region Properties + /// + /// Folder to look for the subset lists under the target framework directories + /// + public static string SubsetListFolder + { + get + { + return subsetListFolder; + } + } + + #endregion + + #region Methods + /// + /// Given a framework directory path, this method will find matching + /// subset list files underneath that path. An appdomain-wide cache is used to + /// avoid hitting the disk multiple times for the same framework directory and set of requested subset names. + /// + /// Framework directory to look for set of subset files under + /// Array of paths locations to subset lists under the given framework directory. + public string[] GetSubsetListPathsFromDisk(string frameworkDirectory) + { + ErrorUtilities.VerifyThrowArgumentNull(frameworkDirectory, "frameworkDirectory"); + + // Make sure we have some subset names to search for it is possible that no subsets are asked for + // so we should return as quickly as possible in that case. + if (_subsetToSearchFor.Length > 0) + { + lock (s_subsetListPathCacheLock) + { + // We want to cache the paths to the subset files so that we do not have to hit the disk and check for the files + // each time RAR is called within the appdomain. + if (s_subsetListPathCache == null) + { + s_subsetListPathCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + + // TargetFrameworkDirectory is not unique enough because a different invocation could ask for a different + // set of subset files from the same TargetFrameworkDirectory + string concatenatedSubsetListNames = String.Join(";", _subsetToSearchFor); + + string key = frameworkDirectory + ":" + concatenatedSubsetListNames; + + + string[] subsetLists = null; + s_subsetListPathCache.TryGetValue(key, out subsetLists); + if (subsetLists == null) + { + // Get the path to the subset folder under the target framework directory + string subsetDirectory = Path.Combine(frameworkDirectory, subsetListFolder); + + List subsetFilesForFrameworkDirectory = new List(); + + // Go through each of the subsets and see if it is in the target framework subset directory + foreach (string subsetName in _subsetToSearchFor) + { + string subsetFilePath = Path.Combine(subsetDirectory, subsetName + ".xml"); + if (File.Exists(subsetFilePath)) + { + subsetFilesForFrameworkDirectory.Add(subsetFilePath); + } + } + + // Note, even if the array is empty we still want to add it to the cache, because some + // target framework directories may never contain a subset file (for example 2.05727 and 3.0) + // for this reason we should not check them everytime if the files are not found. + s_subsetListPathCache[key] = subsetFilesForFrameworkDirectory.ToArray(); + return s_subsetListPathCache[key]; + } + else + { + return subsetLists; + } + } + } + + return new string[0]; + } + #endregion + } + + /// + /// Describes an assembly entry found in an installed assembly table. + /// + internal class AssemblyEntry + { + private readonly string _fullName; + private readonly bool _inGAC; + private readonly bool? _isRedistRoot; + private readonly string _redistName; + private readonly string _simpleName; + private readonly string _frameworkDirectory; + private readonly bool _retargetable; + private AssemblyNameExtension _assemblyName; + public AssemblyEntry(string name, string version, string publicKeyToken, string culture, bool inGAC, bool? isRedistRoot, string redistName, string frameworkDirectory, bool retargetable) + { + Debug.Assert(name != null && frameworkDirectory != null); + _simpleName = name; + if (name != null && version != null && publicKeyToken != null && culture != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}, Version={1}, Culture={2}, PublicKeyToken={3}", name, version, culture, publicKeyToken); + } + else if (name != null && version != null && publicKeyToken != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}, Version={1}, PublicKeyToken={2}", name, version, publicKeyToken); + } + else if (name != null && version != null && culture != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}, Version={1}, Culture={2}", name, version, culture); + } + else if (name != null && version != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}, Version={1}", name, version); + } + else if (name != null && publicKeyToken != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}, PublicKeyToken={1}", name, version); + } + else if (name != null && culture != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}, Culture={1}", name, culture); + } + else if (name != null) + { + _fullName = string.Format(CultureInfo.InvariantCulture, "{0}", name); + } + + if (retargetable) + { + _fullName += ", Retargetable=Yes"; + } + + _inGAC = inGAC; + _isRedistRoot = isRedistRoot; + _redistName = redistName; + _frameworkDirectory = frameworkDirectory; + _retargetable = retargetable; + } + public string FullName { get { return _fullName; } } + public bool InGAC { get { return _inGAC; } } + public bool? IsRedistRoot { get { return _isRedistRoot; } } + public string RedistName { get { return _redistName; } } + public string SimpleName { get { return _simpleName; } } + public string FrameworkDirectory { get { return _frameworkDirectory; } } + public bool Retargetable { get { return _retargetable; } } + public AssemblyNameExtension AssemblyNameExtension + { + get + { + if (_assemblyName == null) + { + _assemblyName = new AssemblyNameExtension(_fullName, true); + _assemblyName.MarkImmutable(); + } + + return _assemblyName; + } + } + } +} diff --git a/src/XMakeTasks/RegisterAssembly.cs b/src/XMakeTasks/RegisterAssembly.cs new file mode 100644 index 00000000000..eac1af26e86 --- /dev/null +++ b/src/XMakeTasks/RegisterAssembly.cs @@ -0,0 +1,399 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Runtime.InteropServices.ComTypes; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Registers a managed assembly for COM interop (equivalent of regasm.exe functionality, but this code doesn't actually call the exe). + /// + /// ITypeLibExporterNotifySink is necessary for the ITypeLibConverter.ConvertAssemblyToTypeLib call. + public class RegisterAssembly : AppDomainIsolatedTaskExtension, ITypeLibExporterNotifySink + { + #region Constructors + + /// + /// Constructor + /// + public RegisterAssembly() + { + // do nothing + } + + #endregion + + #region Properties + + [Required] + public ITaskItem[] Assemblies + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_assemblies, "assemblies"); + return _assemblies; + } + set { _assemblies = value; } + } + + private ITaskItem[] _assemblies = null; + + [Output] + public ITaskItem[] TypeLibFiles + { + get { return _typeLibFiles; } + set { _typeLibFiles = value; } + } + + private ITaskItem[] _typeLibFiles = null; + + public bool CreateCodeBase + { + get { return _createCodeBase; } + set { _createCodeBase = value; } + } + + private bool _createCodeBase = false; + + /// + /// The cache file for Register/UnregisterAssembly. Necessary for UnregisterAssembly to do the proper clean up. + /// + public ITaskItem AssemblyListFile + { + get { return _assemblyListFile; } + set { _assemblyListFile = value; } + } + + private ITaskItem _assemblyListFile = null; + + #endregion + + #region ITask members + + /// + /// Task entry point + /// + /// + override public bool Execute() + { + // TypeLibFiles isn't [Required], but if it is specified, it must have the same length as Assemblies + if ((TypeLibFiles != null) && (TypeLibFiles.Length != Assemblies.Length)) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", Assemblies.Length, TypeLibFiles.Length, "Assemblies", "TypeLibFiles"); + return false; + } + + if (TypeLibFiles == null) + { + TypeLibFiles = new TaskItem[Assemblies.Length]; + } + + AssemblyRegistrationCache cacheFile = null; + + if ((AssemblyListFile != null) && (AssemblyListFile.ItemSpec.Length > 0)) + { + cacheFile = (AssemblyRegistrationCache)StateFileBase.DeserializeCache(AssemblyListFile.ItemSpec, Log, typeof(AssemblyRegistrationCache)); + + if (cacheFile == null) + { + cacheFile = new AssemblyRegistrationCache(); + } + } + + bool taskReturnValue = true; + + try + { + for (int i = 0; i < Assemblies.Length; i++) + { + try + { + string tlbPath; + + // if the type lib path is not supplied, generate default one + if ((TypeLibFiles[i] != null) && (TypeLibFiles[i].ItemSpec.Length > 0)) + { + tlbPath = TypeLibFiles[i].ItemSpec; + } + else + { + tlbPath = Path.ChangeExtension(Assemblies[i].ItemSpec, ".tlb"); + TypeLibFiles[i] = new TaskItem(tlbPath); + } + + // If one of assemblies failed to register, the whole task failed. + // We still process the rest of assemblies though. + if (!Register(Assemblies[i].ItemSpec, tlbPath)) + { + taskReturnValue = false; + } + else + { + if (cacheFile != null) + { + cacheFile.AddEntry(Assemblies[i].ItemSpec, tlbPath); + } + } + } + catch (ArgumentException ex) // assembly path has invalid chars in it + { + Log.LogErrorWithCodeFromResources("General.InvalidAssemblyName", Assemblies[i], ex.Message); + taskReturnValue = false; + } +#if _DEBUG + catch (Exception e) + { + Debug.Assert(false, "Unexpected exception in AssemblyRegistration.Execute. " + + "Please log a MSBuild bug specifying the steps to reproduce the problem. " + + e.Message); + throw; + } +#endif + } + } + finally + { + if (cacheFile != null) + { + cacheFile.SerializeCache(AssemblyListFile.ItemSpec, Log); + } + } + + return taskReturnValue; + } + + #endregion + + #region ITypeLibExporterNotifySink methods + + private bool _typeLibExportFailed = false; + + /// + /// Callback method for reporting type library export events + /// + /// + /// + /// + public void ReportEvent(ExporterEventKind kind, int code, string msg) + { + // if we get an error, log it and remember we should fail ExportTypeLib + if (kind == ExporterEventKind.ERROR_REFTOINVALIDASSEMBLY) + { + Log.LogError(msg); + _typeLibExportFailed = true; + } + // if it's just a warning, log it and proceed + else if (kind == ExporterEventKind.NOTIF_CONVERTWARNING) + { + Log.LogWarning(msg); + } + // it's just a status message (type xxx converted etc.), log it at lowest possible priority + else if (kind == ExporterEventKind.NOTIF_TYPECONVERTED) + { + Log.LogMessage(MessageImportance.Low, msg); + } + else + { + Debug.Assert(false, "Unknown ImporterEventKind value"); + Log.LogMessage(MessageImportance.Low, msg); + } + } + + /// + /// Callback method for finding type libraries for given assemblies. If we are here, it means + /// the type library we're looking for is not in the current directory and it's not registered. + /// Currently we assume that all dependent type libs are already registered. + /// + /// + /// In theory, we could automatically register dependent assemblies for COM interop and return + /// a newly created typelib here. However, one danger of such approach is the following scenario: + /// The user creates several projects registered for COM interop, all of them referencing assembly A. + /// The first project that happens to be built will register assembly A for COM interop, creating + /// a type library in its output directory and registering it. The other projects will then refer to that + /// type library, since it's already registered. If then for some reason the first project is deleted + /// from disk, the typelib for assembly A goes away too, and all the other projects, built five years ago, + /// suddenly stop working. + /// + public object ResolveRef(Assembly assemblyToResolve) + { + ErrorUtilities.VerifyThrowArgumentNull(assemblyToResolve, "assemblyToResolve"); + + Log.LogErrorWithCodeFromResources("RegisterAssembly.AssemblyNotRegisteredForComInterop", assemblyToResolve.GetName().FullName); + _typeLibExportFailed = true; + return null; + } + + #endregion + + #region Methods + + /// + /// Helper registration method + /// + /// + /// + /// + private bool Register(string assemblyPath, string typeLibPath) + { + ErrorUtilities.VerifyThrowArgumentNull(typeLibPath, "typeLibPath"); + + Log.LogMessageFromResources(MessageImportance.Low, "RegisterAssembly.RegisteringAssembly", assemblyPath); + + if (!File.Exists(assemblyPath)) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.RegisterAsmFileDoesNotExist", assemblyPath); + return false; + } + + ITypeLib typeLib = null; + + try + { + // Load the specified assembly. + Assembly asm = Assembly.UnsafeLoadFrom(assemblyPath); + + RegistrationServices comRegistrar = new RegistrationServices(); + + // Register the assembly + if (!comRegistrar.RegisterAssembly(asm, (CreateCodeBase) ? AssemblyRegistrationFlags.SetCodeBase : AssemblyRegistrationFlags.None)) + { + // If the assembly doesn't contain any types that could be registered for COM interop, + // warn the user about it. + Log.LogWarningWithCodeFromResources("RegisterAssembly.NoValidTypes", assemblyPath); + } + + // Even if there aren't any types that could be registered for COM interop, + // regasm still creates and tries to register the type library, so we should too. + Log.LogMessageFromResources(MessageImportance.Low, "RegisterAssembly.RegisteringTypeLib", typeLibPath); + + // only regenerate the type lib if necessary + if ((!File.Exists(typeLibPath)) || + (File.GetLastWriteTime(typeLibPath) < File.GetLastWriteTime(assemblyPath))) + { + // Regenerate the type library + try + { + // if export failed the error message is already logged, so just exit + if (!ExportTypeLib(asm, typeLibPath)) + return false; + } + catch (COMException ex) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantExportTypeLib", assemblyPath, ex.Message); + return false; + } + } + else + Log.LogMessageFromResources(MessageImportance.Low, "RegisterAssembly.TypeLibUpToDate", typeLibPath); + + // Also register the type library + try + { + typeLib = (ITypeLib)NativeMethods.LoadTypeLibEx(typeLibPath, (int)NativeMethods.REGKIND.REGKIND_NONE); + + // if we got here, load must have succeeded + NativeMethods.RegisterTypeLib(typeLib, typeLibPath, null); + } + catch (COMException ex) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterTypeLib", typeLibPath, ex.Message); + return false; + } + } + catch (ArgumentNullException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); + return false; + } + catch (InvalidOperationException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); + return false; + } + catch (TargetInvocationException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); + return false; + } + catch (IOException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); + return false; + } + catch (TypeLoadException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); + return false; + } + catch (UnauthorizedAccessException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.UnauthorizedAccess", assemblyPath, e.Message); + return false; + } + catch (BadImageFormatException) + { + Log.LogErrorWithCodeFromResources("General.InvalidAssembly", assemblyPath); + return false; + } + catch (SecurityException e) + { + Log.LogErrorWithCodeFromResources("RegisterAssembly.CantRegisterAssembly", assemblyPath, e.Message); + return false; + } + finally + { + if (typeLib != null) + { + Marshal.ReleaseComObject(typeLib); + } + } + + return true; + } + + /// + /// Helper method - exports a type library for an assembly. Returns true if succeeded. + /// + private bool ExportTypeLib(Assembly asm, string typeLibFileName) + { + _typeLibExportFailed = false; + ITypeLib convertedTypeLib = null; + + try + { + // Create a converter and run the conversion + ITypeLibConverter tlbConverter = new TypeLibConverter(); + convertedTypeLib = (ITypeLib)tlbConverter.ConvertAssemblyToTypeLib(asm, typeLibFileName, 0, this); + + if (convertedTypeLib == null || _typeLibExportFailed) + return false; + + // Persist the type library + UCOMICreateITypeLib createTypeLib = (UCOMICreateITypeLib)convertedTypeLib; + + createTypeLib.SaveAllChanges(); + } + finally + { + if (convertedTypeLib != null) + Marshal.ReleaseComObject((ITypeLib)convertedTypeLib); + } + + return !_typeLibExportFailed; + } + + #endregion + } +} diff --git a/src/XMakeTasks/RemoveDir.cs b/src/XMakeTasks/RemoveDir.cs new file mode 100644 index 00000000000..5dd2f6a3703 --- /dev/null +++ b/src/XMakeTasks/RemoveDir.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Globalization; +using System.Reflection; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Remove the specified directories. + /// + public class RemoveDir : TaskExtension + { + //----------------------------------------------------------------------------------- + // Property: directory to remove + //----------------------------------------------------------------------------------- + private ITaskItem[] _directories; + + [Required] + public ITaskItem[] Directories + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_directories, "directories"); + return _directories; + } + set { _directories = value; } + } + + //----------------------------------------------------------------------------------- + // Property: list of directories that were removed from disk + //----------------------------------------------------------------------------------- + private ITaskItem[] _removedDirectories; + + [Output] + public ITaskItem[] RemovedDirectories + { + get { return _removedDirectories; } + set { _removedDirectories = value; } + } + + //----------------------------------------------------------------------------------- + // Execute -- this runs the task + //----------------------------------------------------------------------------------- + public override bool Execute() + { + // Delete each directory + bool overallSuccess = true; + // Our record of the directories that were removed + ArrayList removedDirectoriesList = new ArrayList(); + + foreach (ITaskItem directory in Directories) + { + if (Directory.Exists(directory.ItemSpec)) + { + bool unauthorizedAccess = false; + bool currentSuccess; + + // Do not log a fake command line as well, as it's superfluous, and also potentially expensive + Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.Removing", directory.ItemSpec); + + // Try to remove the directory, this will not log unauthorized access errors since + // we will attempt to remove read only attributes and try again. + currentSuccess = RemoveDirectory(directory, false, out unauthorizedAccess); + + // The first attempt failed, to we will remove readonly attributes and try again.. + if (!currentSuccess && unauthorizedAccess) + { + // If the directory delete operation returns an unauthorized access exception + // we need to attempt to remove the readonly attributes and try again. + currentSuccess = RemoveReadOnlyAttributeRecursively(new DirectoryInfo(directory.ItemSpec)); + if (currentSuccess) + { + // Retry the remove directory operation, this time we want to log any errors + currentSuccess = RemoveDirectory(directory, true, out unauthorizedAccess); + } + } + + // The current directory was not removed successfully + if (!currentSuccess) + { + overallSuccess = false; + } + + // We successfully removed the directory, so add the removed directory to our record + if (currentSuccess) + { + // keep a running list of the directories that were actually removed + // note that we include in this list directories that did not exist + removedDirectoriesList.Add(new TaskItem(directory)); + } + } + else + { + Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.SkippingNonexistentDirectory", directory.ItemSpec); + // keep a running list of the directories that were actually removed + // note that we include in this list directories that did not exist + removedDirectoriesList.Add(new TaskItem(directory)); + } + } + // convert the list of deleted files into an array of ITaskItems + RemovedDirectories = (ITaskItem[])removedDirectoriesList.ToArray(typeof(ITaskItem)); + return overallSuccess; + } + + // Core implementation of directory removal + private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out bool unauthorizedAccess) + { + bool success = true; + + unauthorizedAccess = false; + + try + { + // Try to delete the directory + System.IO.Directory.Delete(directory.ItemSpec, true); + } + catch (System.UnauthorizedAccessException e) + { + success = false; + // Log the fact that there was a problem only if we have been asked to. + if (logUnauthorizedError) + { + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory, e.Message); + } + unauthorizedAccess = true; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory.ItemSpec, e.Message); + success = false; + } + + return success; + } + + // recursively remove RO attribs from all files + private bool RemoveReadOnlyAttributeRecursively(DirectoryInfo directory) + { + bool success = true; + try + { + // Remove the ReadOnly attribute from the directory if it is present + if ((directory.Attributes & FileAttributes.ReadOnly) != 0) + { + FileAttributes faNew = (directory.Attributes & ~FileAttributes.ReadOnly); + directory.Attributes = faNew; + } + + // For each file in the directory remove the readonly attribute if it is present + foreach (FileSystemInfo file in directory.GetFileSystemInfos()) + { + if ((file.Attributes & FileAttributes.ReadOnly) != 0) + { + FileAttributes faNew = (file.Attributes & ~FileAttributes.ReadOnly); + file.Attributes = faNew; + } + } + + // Recursively call ourselves for sub-directories + foreach (DirectoryInfo folder in directory.GetDirectories()) + { + success = RemoveReadOnlyAttributeRecursively(folder); + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory, e.Message); + success = false; + } + + return success; + } + } +} diff --git a/src/XMakeTasks/RequiresFramework35SP1Assembly.cs b/src/XMakeTasks/RequiresFramework35SP1Assembly.cs new file mode 100644 index 00000000000..7cbf654be34 --- /dev/null +++ b/src/XMakeTasks/RequiresFramework35SP1Assembly.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This task determines if this project requires VS2008 SP1 assembly. + /// + public sealed class RequiresFramework35SP1Assembly : TaskExtension + { + #region Fields + private string _errorReportUrl; + private string _targetFrameworkVersion = Constants.TargetFrameworkVersion20; + private bool? _createDesktopShortcut; + private bool _signingManifests; + private bool _outputRequiresMinimumFramework35SP1; + + private ITaskItem[] _referencedAssemblies; + private ITaskItem[] _assemblies; + private ITaskItem _deploymentManifestEntryPoint; + private ITaskItem _entryPoint; + private ITaskItem[] _files; + private string _suiteName; + #endregion + + #region Properties + + public string ErrorReportUrl + { + get { return _errorReportUrl; } + set { _errorReportUrl = value; } + } + + public string TargetFrameworkVersion + { + get + { + if (string.IsNullOrEmpty(_targetFrameworkVersion)) + { + return Constants.TargetFrameworkVersion35; + } + return _targetFrameworkVersion; + } + set { _targetFrameworkVersion = value; } + } + + public bool CreateDesktopShortcut + { + get + { + if (!_createDesktopShortcut.HasValue) + { + return false; + } + if (CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) < 0) + { + return false; + } + return (bool)_createDesktopShortcut; + } + set { _createDesktopShortcut = value; } + } + + public bool SigningManifests + { + get { return _signingManifests; } + set { _signingManifests = value; } + } + + public ITaskItem[] ReferencedAssemblies + { + get { return _referencedAssemblies; } + set { _referencedAssemblies = value; } + } + + public ITaskItem[] Assemblies + { + get { return _assemblies; } + set { _assemblies = value; } + } + + public ITaskItem DeploymentManifestEntryPoint + { + get { return _deploymentManifestEntryPoint; } + set { _deploymentManifestEntryPoint = value; } + } + + public ITaskItem EntryPoint + { + get { return _entryPoint; } + set { _entryPoint = value; } + } + + public ITaskItem[] Files + { + get { return _files; } + set { _files = value; } + } + + public string SuiteName + { + get { return _suiteName; } + set { _suiteName = value; } + } + + [Output] + public bool RequiresMinimumFramework35SP1 + { + get { return _outputRequiresMinimumFramework35SP1; } + set { _outputRequiresMinimumFramework35SP1 = value; } + } + + #endregion + + #region helper + private static Version ConvertFrameworkVersionToString(string version) + { + if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + return new Version(version.Substring(1)); + } + return new Version(version); + } + + private static int CompareFrameworkVersions(string versionA, string versionB) + { + Version version1 = ConvertFrameworkVersionToString(versionA); + Version version2 = ConvertFrameworkVersionToString(versionB); + return version1.CompareTo(version2); + } + + private bool HasErrorUrl() + { + if (string.IsNullOrEmpty(ErrorReportUrl)) + { + return false; + } + else + { + return true; + } + } + + private bool HasCreatedShortcut() + { + return CreateDesktopShortcut; + } + + private bool UncheckedSigning() + { + return !SigningManifests; + } + + private bool ExcludeReferenceFromHashing() + { + if (HasExcludedFileOrSP1File(_referencedAssemblies) || + HasExcludedFileOrSP1File(_assemblies) || + HasExcludedFileOrSP1File(_files) || + IsExcludedFileOrSP1File(_deploymentManifestEntryPoint) || + IsExcludedFileOrSP1File(_entryPoint)) + { + return true; + } + + return false; + } + + private static bool HasExcludedFileOrSP1File(ITaskItem[] candidateFiles) + { + if (candidateFiles != null) + { + foreach (ITaskItem file in candidateFiles) + { + if (IsExcludedFileOrSP1File(file)) + { + return true; + } + } + } + return false; + } + + /// + /// Is this file's IncludeHash set to false? + /// Is this file System.Data.Entity.dll? + /// Is this file Client Sentinel Assembly? + /// + /// + /// + private static bool IsExcludedFileOrSP1File(ITaskItem candidateFile) + { + if (candidateFile != null && + (string.Equals(candidateFile.GetMetadata("IncludeHash"), "false", StringComparison.OrdinalIgnoreCase) + || string.Equals(candidateFile.ItemSpec, Constants.NET35SP1AssemblyIdentity[0], StringComparison.OrdinalIgnoreCase) + || string.Equals(candidateFile.ItemSpec, Constants.NET35ClientAssemblyIdentity[0], StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + return false; + } + + private bool HasSuiteName() + { + return !string.IsNullOrEmpty(SuiteName); + } + + #endregion + + public RequiresFramework35SP1Assembly() + { + } + + public override bool Execute() + { + _outputRequiresMinimumFramework35SP1 = false; + + if (HasErrorUrl() || HasCreatedShortcut() || UncheckedSigning() || ExcludeReferenceFromHashing() || HasSuiteName()) + { + _outputRequiresMinimumFramework35SP1 = true; + } + + return true; + } + } +} diff --git a/src/XMakeTasks/ResGen.cs b/src/XMakeTasks/ResGen.cs new file mode 100644 index 00000000000..fc9b482d327 --- /dev/null +++ b/src/XMakeTasks/ResGen.cs @@ -0,0 +1,648 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// ToolTask that wraps ResGen.exe, which transforms resource files. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +using CodeDomProvider = System.CodeDom.Compiler.CodeDomProvider; +using MSBuildProcessorArchitecture = Microsoft.Build.Utilities.ProcessorArchitecture; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines the "GenerateResource" MSBuild task, which enables using resource APIs + /// to transform resource files. + /// + /// See GenerateResource.cs for the source code to the GenerateResource task; this file + /// just contains the nested internal ResGen task + public sealed partial class GenerateResource : TaskExtension + { + /// + /// Defines the "ResGen" MSBuild task, which enables using ResGen.exe + /// to generate strongly-typed resource classes and convert resource + /// files from one format to another. + /// + internal class ResGen : ToolTaskExtension + { + #region Properties + /* + Microsoft (R) .NET Resource Generator + [Microsoft .Net Framework, Version 4.0.10719.0] + Copyright (c) Microsoft Corporation. All rights reserved. + + Usage: + ResGen inputFile.ext [outputFile.ext] [/str:lang[,namespace[,class[,file]]]] + ResGen [options] /compile inputFile1.ext[,outputFile1.resources] [...] + + Where .ext is .resX, .restext, .txt, or .resources + + Converts files from one resource format to another. If the output + filename is not specified, inputFile.resources will be used. + Options: + /compile Converts a list of resource files from one format to another + in one bulk operation. By default, it converts into .resources + files, using inputFile[i].resources for the output file name. + /str:[,[,[,]]]] + Creates a strongly-typed resource class in the specified + programming language using CodeDOM. In order for the strongly + typed resource class to work properly, the name of your output + file without the .resources must match the + [namespace.]classname of your strongly typed resource class. + You may need to rename your output file before using it or + embedding it into an assembly. + /useSourcePath Use each source file's directory as the current directory + for resolving relative file paths. + /publicClass Create the strongly typed resource class as a public class. + This option is ignored if the /str: option is not used. + /r: Load types from these assemblies. A ResX file with a previous + version of a type will use the one in this assembly, when set. + /define:A[,B] For #ifdef support in .ResText files, pass a comma-separated + list of symbols. ResText files can use "#ifdef A" or "#if !B". + + Miscellaneous: + @ Read response file for more options. At most one response file + may be specified, and its entries must be line-separated. + + .restext & .txt files have this format: + + # Use # at the beginning of a line for a comment character. + name=value + more elaborate name=value + + Example response file contents: + + # Use # at the beginning of a line for a comment character. + /useSourcePath + /compile + file1.resx,file1.resources + file2.resx,file2.resources + + + Language names valid for the /str: option are: + c#, cs, csharp, vb, vbs, visualbasic, vbscript, js, jscript, javascript, vj#, vjs, vjsharp, c++, mc, cpp + */ + + /// + /// Files being passed to ResGen.exe to be converted to a different resource format. + /// If a strongly typed resource class is being created, only one file may be + /// passed to InputFiles at a time. + /// + public ITaskItem[] InputFiles + { + get { return (ITaskItem[])Bag["InputFiles"]; } + set { Bag["InputFiles"] = value; } + } + + /// + /// Should be the same length as InputFiles or null. If null, the files output + /// by ResGen.exe will be named "inputFiles[i].resources". Otherwise, the + /// extensions on the output filesnames indicate which format the corresponding + /// input file will be translated to. + /// + public ITaskItem[] OutputFiles + { + get { return (ITaskItem[])Bag["OutputFiles"]; } + set { Bag["OutputFiles"] = value; } + } + + /// + /// Specifies whether the strongly typed class should be created public (with public methods) + /// instead of the default internal. Analogous to resgen.exe's /publicClass switch. + /// + public bool PublicClass + { + get { return GetBoolParameterWithDefault("PublicClass", false); } + set { Bag["PublicClass"] = value; } + } + + /// + /// Resolves types in ResX files (XML resources) for Strongly Typed Resources + /// + public ITaskItem[] References + { + get { return (ITaskItem[])Bag["References"]; } + set { Bag["References"] = value; } + } + + /// + /// Path to the SDK directory where ResGen.exe can be found + /// + public string SdkToolsPath + { + get { return (string)Bag["SdkToolsPath"]; } + set { Bag["SdkToolsPath"] = value; } + } + + /// + /// The language to use when generating the class source for the strongly typed resource. + /// This parameter must match exactly one of the languages used by the CodeDomProvider. + /// + public string StronglyTypedLanguage + { + get { return (string)Bag["StronglyTypedLanguage"]; } + set { Bag["StronglyTypedLanguage"] = value; } + } + + /// + /// Specifies the namespace to use for the generated class source for the + /// strongly typed resource. If left blank, no namespace is used. + /// + public string StronglyTypedNamespace + { + get { return (string)Bag["StronglyTypedNamespace"]; } + set { Bag["StronglyTypedNamespace"] = value; } + } + + /// + /// Specifies the class name for the strongly typed resource class. If left blank, the base + /// name of the resource file is used. + /// + public string StronglyTypedClassName + { + get { return (string)Bag["StronglyTypedClassName"]; } + set { Bag["StronglyTypedClassName"] = value; } + } + + /// + /// Specifies the filename for the source file. If left blank, the name of the class is + /// used as the base filename, with the extension dependent on the language. + /// + public string StronglyTypedFileName + { + get { return (string)Bag["StronglyTypedFileName"]; } + set { Bag["StronglyTypedFileName"] = value; } + } + + /// + /// Indicates whether the resource reader should use the source file's directory to + /// resolve relative file paths. + /// + public bool UseSourcePath + { + get { return GetBoolParameterWithDefault("UseSourcePath", false); } + set { Bag["UseSourcePath"] = value; } + } + + #endregion // Properties + + #region ToolTask Members + + /// + /// Returns the name of the tool to execute + /// + protected override string ToolName + { + get + { + return "ResGen.exe"; + } + } + + /// + /// Tracker.exe wants Unicode response files, and ResGen.exe doesn't care, + /// so make them Unicode across the board. + /// + /// + /// We no longer use Tracker.exe in ResGen, but given that as ResGen doesn't care, + /// there doesn't really seem to be a particular reason to change it back, either... + /// + protected override Encoding ResponseFileEncoding + { + get { return Encoding.Unicode; } + } + + /// + /// Invokes the ToolTask with the given parameters + /// + /// True if the task succeeded, false otherwise + public override bool Execute() + { + // If there aren't any input resources, well, we've already succeeded! + if (ResGen.IsNullOrEmpty(InputFiles)) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResGen.NoInputFiles"); + return !Log.HasLoggedErrors; + } + + if (ResGen.IsNullOrEmpty(OutputFiles)) + { + GenerateOutputFileNames(); + } + + bool success = false; + + // if command line is too long, fail + string commandLineCommands = GenerateCommandLineCommands(); + + // when comparing command line length, need to add one for leading space added between command arguments and tool name + if (!string.IsNullOrEmpty(commandLineCommands) && (commandLineCommands.Length + 1) > s_maximumCommandLength) + { + Log.LogErrorWithCodeFromResources("ResGen.CommandTooLong", commandLineCommands.Length); + success = false; + } + else + { + // Use ToolTaskExtension's Execute() + success = base.Execute(); + } + + if (String.IsNullOrEmpty(StronglyTypedLanguage)) + { + if (!success) + { + // One or more of the generated resources was not, in fact generated -- + // only keep in OutputFiles the ones that actually exist. + ITaskItem[] outputFiles = this.OutputFiles; + List successfullyGenerated = new List(); + + for (int i = 0; i < outputFiles.Length; i++) + { + if (File.Exists(outputFiles[i].ItemSpec)) + { + successfullyGenerated.Add(outputFiles[i]); + } + } + + this.OutputFiles = successfullyGenerated.ToArray(); + } + } + else + { + ITaskItem outputFile = OutputFiles[0]; + + // if the resource generation was unsuccessful, check to see that the resource file + // was in fact generated + if (!success) + { + if (!File.Exists(outputFile.ItemSpec)) + { + this.OutputFiles = new ITaskItem[0]; + } + } + + // Default the class name if we need to - regardless of whether the STR was successfully generated + if (StronglyTypedClassName == null) + { + StronglyTypedClassName = Path.GetFileNameWithoutExtension(outputFile.ItemSpec); + } + + // Default the filename if we need to - regardless of whether the STR was successfully generated + if (StronglyTypedFileName == null) + { + CodeDomProvider provider = null; + try + { + provider = CodeDomProvider.CreateProvider(StronglyTypedLanguage); + } + catch (System.Configuration.ConfigurationException) + { + // If the language can't be found, then ResGen.exe will already have + // logged an appropriate error. + return false; + } + catch (System.Security.SecurityException) + { + // If the language can't be found, then ResGen.exe will already have + // logged an appropriate error. + return false; + } + + StronglyTypedFileName = ProcessResourceFiles.GenerateDefaultStronglyTypedFilename(provider, outputFile.ItemSpec); + } + } + + return success && !Log.HasLoggedErrors; + } + + /// + /// Fills the provided CommandLineBuilderExtension with all the command line options used when + /// executing this tool that can go into a response file. + /// + /// + /// ResGen 3.5 and earlier doesn't support response files, but ResGen 4.0 and later does. + /// + /// Gets filled with command line options + protected internal override void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + string pathToResGen = GenerateResGenFullPath(); + + // Only do anything if we can actually use response files + if ( + pathToResGen != null && + !pathToResGen.Equals(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version35), StringComparison.OrdinalIgnoreCase) && + String.IsNullOrEmpty(StronglyTypedLanguage) + ) + { + // 4.0 resgen.exe does support response files, so we can return the resgen arguments here! + CommandLineBuilderExtension resGenArguments = new CommandLineBuilderExtension(); + GenerateResGenCommands(resGenArguments, true /* arguments must be line-delimited */); + + commandLine.AppendTextUnquoted(resGenArguments.ToString()); + } + else + { + // return nothing -- if it's not 4.0, or if we're building strongly typed resources, we assume that, + // as far as ToolTask is concerned at least, response files are not supported. + } + } + + /// + /// Fills the provided CommandLineBuilderExtension with all the command line options used when + /// executing this tool that must go on the command line + /// + /// + /// Has to be command line commands because ResGen 3.5 and earlier don't know about + /// response files. + /// + /// Gets filled with command line options + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + ErrorUtilities.VerifyThrow(!ResGen.IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); + + CommandLineBuilderExtension resGenArguments = new CommandLineBuilderExtension(); + GenerateResGenCommands(resGenArguments, false /* don't line-delimit arguments; spaces are just fine */); + + string pathToResGen = GenerateResGenFullPath(); + + if ( + pathToResGen != null && + !pathToResGen.Equals(NativeMethodsShared.GetLongFilePath(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version35)), StringComparison.OrdinalIgnoreCase) && + String.IsNullOrEmpty(StronglyTypedLanguage) + ) + { + // 4.0 resgen.exe does support response files (at least as long as you're not building an STR), so we can + // make use of them here by returning nothing! + } + else + { + // otherwise, the toolname is ResGen.exe and we just need the resgen arguments in CommandLineCommands. + commandLine.AppendTextUnquoted(resGenArguments.ToString()); + } + } + + /// + /// Generates the full path to the tool being executed by this ToolTask + /// + /// A string containing the full path of this tool, or null if the tool was not found + protected override string GenerateFullPathToTool() + { + string pathToTool = null; + + // Use ToolPath if it exists. + pathToTool = GenerateResGenFullPath(); + return pathToTool; + } + + /// + /// Validates the parameters passed to the task + /// + /// True if parameters are valid + protected override bool ValidateParameters() + { + ErrorUtilities.VerifyThrow(!ResGen.IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); + + // make sure that if the output resources were set, they exactly match the number of input sources + if (!ResGen.IsNullOrEmpty(OutputFiles) && (OutputFiles.Length != InputFiles.Length)) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", InputFiles.Length, OutputFiles.Length, "InputFiles", "OutputFiles"); + return false; + } + + // Creating an STR is triggered merely by setting the language + if (!String.IsNullOrEmpty(StronglyTypedLanguage)) + { + // Only a single Sources is allowed if you are generating STR. + // Otherwise, each STR class overwrites the previous one. In theory we could generate separate + // STR classes for each input, but then the class name and file name parameters would have to be vectors. + if (InputFiles.Length != 1) + { + Log.LogErrorWithCodeFromResources("ResGen.STRLanguageButNotExactlyOneSourceFile"); + return false; + } + } + else + { + if ( + !String.IsNullOrEmpty(StronglyTypedClassName) || + !String.IsNullOrEmpty(StronglyTypedNamespace) || + !String.IsNullOrEmpty(StronglyTypedFileName) + ) + { + // We have no language to generate a STR, but nevertheless the user passed us a class, + // namespace, and/or filename. Let them know that they probably wanted to pass a language too. + Log.LogErrorWithCodeFromResources("ResGen.STRClassNamespaceOrFilenameWithoutLanguage"); + return false; + } + } + + // Verify that the ToolPath exists -- if the tool doesn't exist in it + // we'll worry about that later + if ((String.IsNullOrEmpty(ToolPath) || !Directory.Exists(ToolPath)) && + (String.IsNullOrEmpty(SdkToolsPath) || !Directory.Exists(SdkToolsPath))) + { + Log.LogErrorWithCodeFromResources("ResGen.SdkOrToolPathNotSpecifiedOrInvalid", SdkToolsPath ?? "", ToolPath ?? ""); + return false; + } + + return base.ValidateParameters(); + } + + #endregion // ToolTask Members + + #region Helper methods + + /// + /// Checks a string array for null or length zero. Does not check if + /// individual members are null + /// + /// The string array to check + /// True if the array is null or has length zero + private static bool IsNullOrEmpty(ITaskItem[] value) + { + return (value == null || value.Length == 0); + } + + /// + /// If OutputFiles is null, we need to generate default output names + /// to pass to resgen.exe (which would generate the names on its own, but + /// then we wouldn't have access to them) + /// + private void GenerateOutputFileNames() + { + ErrorUtilities.VerifyThrow(!ResGen.IsNullOrEmpty(InputFiles), "If InputFiles is empty, the task should have returned before reaching this point"); + + ITaskItem[] inputFiles = InputFiles; + ITaskItem[] outputFiles = new ITaskItem[inputFiles.Length]; + + // Set the default OutputFiles values + for (int i = 0; i < inputFiles.Length; i++) + { + ITaskItem2 inputFileAsITaskItem2 = inputFiles[i] as ITaskItem2; + + if (inputFileAsITaskItem2 != null) + { + outputFiles[i] = new TaskItem(Path.ChangeExtension(inputFileAsITaskItem2.EvaluatedIncludeEscaped, ".resources")); + } + else + { + outputFiles[i] = new TaskItem(Path.ChangeExtension(EscapingUtilities.Escape(inputFiles[i].ItemSpec), ".resources")); + } + } + + Bag["OutputFiles"] = outputFiles; + } + + /// + /// Generates the full path to ResGen.exe. + /// + /// The path to ResGen.exe, or null. + private string GenerateResGenFullPath() + { + string pathToTool = null; + + // Use ToolPath if it exists. + pathToTool = (string)Bag["ToolPathWithFile"]; + + if (pathToTool == null) + { + // First see if the user has set ToolPath + if (ToolPath != null) + { + pathToTool = Path.Combine(ToolPath, ToolExe); + + if (!File.Exists(pathToTool)) + { + pathToTool = null; + } + } + + // If it still hasn't been found, try to generate the appropriate path. + if (pathToTool == null) + { + pathToTool = SdkToolsPathUtility.GeneratePathToTool + ( + SdkToolsPathUtility.FileInfoExists, + MSBuildProcessorArchitecture.CurrentProcessArchitecture, + SdkToolsPath, + ToolName, + Log, + true /* log errors and warnings */ + ); + + pathToTool = NativeMethodsShared.GetLongFilePath(pathToTool); + } + + // And then set it for future reference. If it's still null, there's nothing else + // we can do, and we've already logged an appropriate error. + Bag["ToolPathWithFile"] = pathToTool; + } + + return pathToTool; + } + + /// + /// Generate the command line to be passed to resgen.exe, sans the path to the tool. + /// + private void GenerateResGenCommands(CommandLineBuilderExtension resGenArguments, bool useForResponseFile) + { + resGenArguments = resGenArguments ?? new CommandLineBuilderExtension(); + + if (ResGen.IsNullOrEmpty(OutputFiles)) + { + GenerateOutputFileNames(); + } + + // Append boolean flags if requested + string useSourcePathSwitch = "/useSourcePath" + (useForResponseFile ? "\n" : String.Empty); + string publicClassSwitch = "/publicClass" + (useForResponseFile ? "\n" : String.Empty); + resGenArguments.AppendWhenTrue(useSourcePathSwitch, Bag, "UseSourcePath"); + resGenArguments.AppendWhenTrue(publicClassSwitch, Bag, "PublicClass"); + + // append the references, if any + if (References != null) + { + foreach (ITaskItem reference in References) + { + // ResGen.exe response files frown on quotes in filenames, even if there are + // spaces in the names of the files. + if (useForResponseFile && reference != null) + { + resGenArguments.AppendTextUnquoted("/r:"); + resGenArguments.AppendTextUnquoted(reference.ItemSpec); + resGenArguments.AppendTextUnquoted("\n"); + } + else + { + resGenArguments.AppendSwitchIfNotNull("/r:", reference); + } + } + } + + if (String.IsNullOrEmpty(StronglyTypedLanguage)) + { + // append the compile switch + resGenArguments.AppendSwitch("/compile" + (useForResponseFile ? "\n" : String.Empty)); + + // append the resources to compile + if (InputFiles != null && InputFiles.Length > 0) + { + ITaskItem[] inputFiles = InputFiles; + ITaskItem[] outputFiles = OutputFiles; + + for (int i = 0; i < inputFiles.Length; ++i) + { + if (useForResponseFile) + { + // ResGen.exe response files frown on quotes in filenames, even if there are + // spaces in the names of the files. + if (inputFiles[i] != null && outputFiles[i] != null) + { + resGenArguments.AppendTextUnquoted(inputFiles[i].ItemSpec); + resGenArguments.AppendTextUnquoted(","); + resGenArguments.AppendTextUnquoted(outputFiles[i].ItemSpec); + resGenArguments.AppendTextUnquoted("\n"); + } + } + else + { + resGenArguments.AppendFileNamesIfNotNull + ( + new ITaskItem[] { inputFiles[i], outputFiles[i] }, + "," + ); + } + } + } + } + else + { + // append the resource to compile + resGenArguments.AppendFileNamesIfNotNull(InputFiles, " "); + resGenArguments.AppendFileNamesIfNotNull(OutputFiles, " "); + + // append the strongly-typed resource details + resGenArguments.AppendSwitchIfNotNull + ( + "/str:", + new string[] { StronglyTypedLanguage, StronglyTypedNamespace, StronglyTypedClassName, StronglyTypedFileName }, + "," + ); + } + } + + #endregion // Helper methods + } + } +} diff --git a/src/XMakeTasks/ResGenDependencies.cs b/src/XMakeTasks/ResGenDependencies.cs new file mode 100644 index 00000000000..dcbf2039c52 --- /dev/null +++ b/src/XMakeTasks/ResGenDependencies.cs @@ -0,0 +1,375 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Xml; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class is a caching mechanism for the resgen task to keep track of linked + /// files within processed .resx files. + /// + [Serializable()] + internal sealed class ResGenDependencies : StateFileBase + { + /// + /// The list of resx files. + /// + private Dependencies _resXFiles = new Dependencies(); + + /// + /// A list of portable libraries and the ResW files they can produce. + /// + private Dependencies _portableLibraries = new Dependencies(); + + /// + /// A newly-created ResGenDependencies is not dirty. + /// What would be the point in saving the default? + /// + [NonSerialized] + private bool _isDirty = false; + + /// + /// This is the directory that will be used for resolution of files linked within a .resx. + /// If this is NULL then we use the directory in which the .resx is in (that should always + /// be the default!) + /// + private string _baseLinkedFileDirectory; + + /// + /// Construct. + /// + internal ResGenDependencies() + { + } + + internal string BaseLinkedFileDirectory + { + get + { + return _baseLinkedFileDirectory; + } + set + { + if (value == null && _baseLinkedFileDirectory == null) + { + // No change + return; + } + else if ((value == null && _baseLinkedFileDirectory != null) || + (value != null && _baseLinkedFileDirectory == null) || + (String.Compare(_baseLinkedFileDirectory, value, StringComparison.OrdinalIgnoreCase) != 0)) + { + // Ok, this is slightly complicated. Changing the base directory in any manner may + // result in changes to how we find .resx files. Therefore, we must clear our out + // cache whenever the base directory changes. + _resXFiles.Clear(); + _isDirty = true; + _baseLinkedFileDirectory = value; + } + } + } + + internal bool UseSourcePath + { + set + { + // Ensure that the cache is properly initialized with respect to how resgen will + // resolve linked files within .resx files. ResGen has two different + // ways for resolving relative file-paths in linked files. The way + // that ResGen resolved relative paths before Whidbey was always to + // resolve from the current working directory. In Whidbey a new command-line + // switch "/useSourcePath" instructs ResGen to use the folder that + // contains the .resx file as the path from which it should resolve + // relative paths. So we should base our timestamp/existence checking + // on the same switch & resolve in the same manner as ResGen. + BaseLinkedFileDirectory = value ? null : Environment.CurrentDirectory; + } + } + + internal ResXFile GetResXFileInfo(string resxFile) + { + // First, try to retrieve the resx information from our hashtable. + ResXFile retVal = (ResXFile)_resXFiles.GetDependencyFile(resxFile); + + if (retVal == null) + { + // Ok, the file wasn't there. Add it to our cache and return it to the caller. + retVal = AddResxFile(resxFile); + } + else + { + // The file was there. Is it up to date? If not, then we'll have to refresh the file + // by removing it from the hashtable and readding it. + if (retVal.HasFileChanged()) + { + _resXFiles.RemoveDependencyFile(resxFile); + _isDirty = true; + retVal = AddResxFile(resxFile); + } + } + + return retVal; + } + + private ResXFile AddResxFile(string file) + { + // This method adds a .resx file "file" to our .resx cache. The method causes the file + // to be cracked for contained files. + + ResXFile resxFile = new ResXFile(file, BaseLinkedFileDirectory); + _resXFiles.AddDependencyFile(file, resxFile); + _isDirty = true; + return resxFile; + } + + internal PortableLibraryFile TryGetPortableLibraryInfo(string libraryPath) + { + // First, try to retrieve the portable library information from our hashtable. + PortableLibraryFile retVal = (PortableLibraryFile)_portableLibraries.GetDependencyFile(libraryPath); + + // The file is in our cache. Make sure it's up to date. If not, discard + // this entry from the cache and rebuild all the state at a later point. + if (retVal != null && retVal.HasFileChanged()) + { + _portableLibraries.RemoveDependencyFile(libraryPath); + _isDirty = true; + retVal = null; + } + + return retVal; + } + + internal void UpdatePortableLibrary(PortableLibraryFile library) + { + PortableLibraryFile cached = (PortableLibraryFile)_portableLibraries.GetDependencyFile(library.FileName); + if (cached == null || !library.Equals(cached)) + { + // Add a new entry or replace the existing one. + _portableLibraries.AddDependencyFile(library.FileName, library); + _isDirty = true; + } + } + + /// + /// Writes the contents of this object out to the specified file. + /// + /// + override internal void SerializeCache(string stateFile, TaskLoggingHelper log) + { + base.SerializeCache(stateFile, log); + _isDirty = false; + } + + /// + /// Reads the .cache file from disk into a ResGenDependencies object. + /// + /// + /// + /// + internal static ResGenDependencies DeserializeCache(string stateFile, bool useSourcePath, TaskLoggingHelper log) + { + ResGenDependencies retVal = (ResGenDependencies)StateFileBase.DeserializeCache(stateFile, log, typeof(ResGenDependencies)); + + if (retVal == null) + { + retVal = new ResGenDependencies(); + } + + // Ensure that the cache is properly initialized with respect to how resgen will + // resolve linked files within .resx files. ResGen has two different + // ways for resolving relative file-paths in linked files. The way + // that ResGen resolved relative paths before Whidbey was always to + // resolve from the current working directory. In Whidbey a new command-line + // switch "/useSourcePath" instructs ResGen to use the folder that + // contains the .resx file as the path from which it should resolve + // relative paths. So we should base our timestamp/existence checking + // on the same switch & resolve in the same manner as ResGen. + retVal.UseSourcePath = useSourcePath; + + return retVal; + } + + /// + /// Represents a single .resx file in the dependency cache. + /// + [Serializable()] + internal sealed class ResXFile : DependencyFile + { + // Files contained within this resx file. + private string[] _linkedFiles; + + internal string[] LinkedFiles + { + get { return _linkedFiles; } + } + + internal ResXFile(string filename, string baseLinkedFileDirectory) : base(filename) + { + // Creates a new ResXFile object and populates the class member variables + // by computing a list of linked files within the .resx that was passed in. + // + // filename is the filename of the .resx file that is to be examined. + + if (File.Exists(FileName)) + { + _linkedFiles = ResXFile.GetLinkedFiles(filename, baseLinkedFileDirectory); + } + } + + /// + /// Given a .RESX file, returns all the linked files that are referenced within that .RESX. + /// + /// + /// + /// + /// May be thrown if Resx is invalid. May contain XmlException. + /// May be thrown if Resx is invalid + internal static string[] GetLinkedFiles(string filename, string baseLinkedFileDirectory) + { + // This method finds all linked .resx files for the .resx file that is passed in. + // filename is the filename of the .resx file that is to be examined. + + // Construct the return array + ArrayList retVal = new ArrayList(); + + using (ResXResourceReader resxReader = new ResXResourceReader(filename)) + { + // Tell the reader to return ResXDataNode's instead of the object type + // the resource becomes at runtime so we can figure out which files + // the .resx references + resxReader.UseResXDataNodes = true; + + // First we need to figure out where the linked file resides in order + // to see if it exists & compare its timestamp, and we need to do that + // comparison in the same way ResGen does it. ResGen has two different + // ways for resolving relative file-paths in linked files. The way + // that ResGen resolved relative paths before Whidbey was always to + // resolve from the current working directory. In Whidbey a new command-line + // switch "/useSourcePath" instructs ResGen to use the folder that + // contains the .resx file as the path from which it should resolve + // relative paths. So we should base our timestamp/existence checking + // on the same switch & resolve in the same manner as ResGen. + resxReader.BasePath = (baseLinkedFileDirectory == null) ? Path.GetDirectoryName(filename) : baseLinkedFileDirectory; + + foreach (DictionaryEntry dictEntry in resxReader) + { + if ((dictEntry.Value != null) && (dictEntry.Value is ResXDataNode)) + { + ResXFileRef resxFileRef = ((ResXDataNode)dictEntry.Value).FileRef; + if (resxFileRef != null) + retVal.Add(resxFileRef.FileName); + } + } + } + + return (string[])retVal.ToArray(typeof(string)); + } + } + + /// + /// Represents a single assembly in the dependency cache, which may produce + /// 0 to many ResW files. + /// + [Serializable()] + internal sealed class PortableLibraryFile : DependencyFile + { + private string[] _outputFiles; + private string _neutralResourceLanguage; + private string _assemblySimpleName; + + internal PortableLibraryFile(string filename) + : base(filename) + { + } + + internal string[] OutputFiles + { + get { return _outputFiles; } + set { _outputFiles = value; } + } + + internal string NeutralResourceLanguage + { + get { return _neutralResourceLanguage; } + set { _neutralResourceLanguage = value; } + } + + internal string AssemblySimpleName + { + get { return _assemblySimpleName; } + set { _assemblySimpleName = value; } + } + + internal bool AllOutputFilesAreUpToDate() + { + Debug.Assert(_outputFiles != null, "OutputFiles hasn't been set"); + foreach (string outputFileName in _outputFiles) + { + FileInfo outputFile = new FileInfo(outputFileName); + if (!outputFile.Exists || outputFile.LastWriteTime < this.LastModified) + { + return false; + } + } + + return true; + } + + internal bool Equals(PortableLibraryFile otherLibrary) + { + if (!String.Equals(_assemblySimpleName, otherLibrary._assemblySimpleName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!String.Equals(_neutralResourceLanguage, otherLibrary._neutralResourceLanguage, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + Debug.Assert(OutputFiles != null, "This has not been initialized"); + Debug.Assert(otherLibrary.OutputFiles != null, "The other library has not been initialized"); + if (OutputFiles.Length != otherLibrary.OutputFiles.Length) + { + return false; + } + + for (int i = 0; i < OutputFiles.Length; i++) + { + if (!String.Equals(OutputFiles[i], otherLibrary.OutputFiles[i], StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } + } + + + /// + /// Whether this cache is dirty or not. + /// + internal bool IsDirty + { + get + { + return _isDirty; + } + } + } +} diff --git a/src/XMakeTasks/ResolveCodeAnalysisRuleSet.cs b/src/XMakeTasks/ResolveCodeAnalysisRuleSet.cs new file mode 100644 index 00000000000..54a6d0983d0 --- /dev/null +++ b/src/XMakeTasks/ResolveCodeAnalysisRuleSet.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// A task to determine the code analysis rule set file. +//----------------------------------------------------------------------- + +using System.IO; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks +{ + /// + /// Determines which file, if any, to be used as the code analysis rule set based + /// on the supplied code analysis properties. + /// + public sealed class ResolveCodeAnalysisRuleSet : TaskExtension + { + /// + /// The desired code analysis rule set file. + /// + private string _codeAnalysisRuleSet; + + /// + /// The location of the project currently being built. + /// + private string _projectDirectory; + + /// + /// The set of additional directories to search for code analysis rule set files. + /// + private string[] _codeAnalysisRuleSetDirectories; + + /// + /// The location of the resolved rule set file. May be null if the file + /// does not exist on disk. + /// + private string _resolvedCodeAnalysisRuleSet; + + #region Properties + + /// + /// The desired code analysis rule set file. May be a simple name, relative + /// path, or full path. + /// + public string CodeAnalysisRuleSet + { + get { return _codeAnalysisRuleSet; } + set { _codeAnalysisRuleSet = value; } + } + + /// + /// The set of additional directories to search for code analysis rule set files. + /// + public string[] CodeAnalysisRuleSetDirectories + { + get { return _codeAnalysisRuleSetDirectories; } + set { _codeAnalysisRuleSetDirectories = value; } + } + + /// + /// The location of the project currently being built. + /// + public string MSBuildProjectDirectory + { + get { return _projectDirectory; } + set { _projectDirectory = value; } + } + + /// + /// The location of the resolved rule set file. May be null if the file + /// does not exist on disk. + /// + [Output] + public string ResolvedCodeAnalysisRuleSet + { + get { return _resolvedCodeAnalysisRuleSet; } + } + + /// + /// Runs the task. + /// + /// True if the task succeeds without errors; false otherwise. + public override bool Execute() + { + _resolvedCodeAnalysisRuleSet = GetResolvedRuleSetPath(); + + return !Log.HasLoggedErrors; + } + + /// + /// Computes the resolved rule set path. + /// + /// There are four cases: null, file name, relative path, and full path. + /// + /// If we were given no value for the ruleset, simply return null. + /// + /// For full path we return the string as it is. + /// + /// A simple file name can refer to either a file in the MSBuild project directory + /// or a file in the rule set search paths. In the former case we return the string as-is. + /// In the latter case, we create a full path by prepending the first rule set search path + /// where the file is found. + /// + /// For relative paths we return the string as-is. + /// + /// In all cases, we return null if the file does not actual exist. + /// + /// The full or relative path to the rule set, or null if the file does not exist. + private string GetResolvedRuleSetPath() + { + if (string.IsNullOrEmpty(_codeAnalysisRuleSet)) + { + return null; + } + + if (_codeAnalysisRuleSet == Path.GetFileName(_codeAnalysisRuleSet)) + { + // This is a simple file name. + // Check if the file exists in the MSBuild project directory. + if (!string.IsNullOrEmpty(_projectDirectory)) + { + string fullName = Path.Combine(_projectDirectory, _codeAnalysisRuleSet); + if (File.Exists(fullName)) + { + return _codeAnalysisRuleSet; + } + } + + // Try the rule set directories if we have some. + if (_codeAnalysisRuleSetDirectories != null) + { + foreach (string directory in _codeAnalysisRuleSetDirectories) + { + string fullName = Path.Combine(directory, _codeAnalysisRuleSet); + if (File.Exists(fullName)) + { + return fullName; + } + } + } + } + else if (!Path.IsPathRooted(_codeAnalysisRuleSet)) + { + // This is a path relative to the project. + if (!string.IsNullOrEmpty(_projectDirectory)) + { + string fullName = Path.Combine(_projectDirectory, _codeAnalysisRuleSet); + if (File.Exists(fullName)) + { + return _codeAnalysisRuleSet; + } + } + } + else if (File.Exists(_codeAnalysisRuleSet)) + { + // This is a full path. + return _codeAnalysisRuleSet; + } + + // We can't resolve the rule set to any existing file. + Log.LogWarningWithCodeFromResources("Compiler.UnableToFindRuleSet", _codeAnalysisRuleSet); + return null; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeTasks/ResolveComReference.cs b/src/XMakeTasks/ResolveComReference.cs new file mode 100644 index 00000000000..13f229b9fe4 --- /dev/null +++ b/src/XMakeTasks/ResolveComReference.cs @@ -0,0 +1,1871 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Collections.Generic; +using System.Linq; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; +using UtilitiesProcessorArchitecture = Microsoft.Build.Utilities.ProcessorArchitecture; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Main class for the COM reference resolution task + /// + public sealed partial class ResolveComReference : AppDomainIsolatedTaskExtension, IComReferenceResolver + { + #region Constructors + + /// + /// public constructor + /// + public ResolveComReference() + { + // do nothing. + } + + #endregion + + #region Properties + + /// + /// COM references specified by guid/version/lcid + /// + public ITaskItem[] TypeLibNames + { + get + { + return _typeLibNames; + } + set + { + _typeLibNames = value; + } + } + + private ITaskItem[] _typeLibNames = null; + + /// + /// COM references specified by type library file path + /// + public ITaskItem[] TypeLibFiles + { + get + { + return _typeLibFiles; + } + set + { + _typeLibFiles = value; + } + } + + /// + /// Array of equals-separated pairs of environment + /// variables that should be passed to the spawned tlbimp.exe and aximp.exe, + /// in addition to (or selectively overriding) the regular environment block. + /// + public string[] EnvironmentVariables + { + get; + set; + } + + private ITaskItem[] _typeLibFiles = null; + + /// + /// merged array containing typeLibNames and typeLibFiles (internal for unit testing) + /// + internal List allProjectRefs = null; + + /// + /// array containing all dependency references + /// + internal List allDependencyRefs = null; + + /// + /// the directory wrapper files get generated into + /// + public string WrapperOutputDirectory + { + get + { + return _wrapperOutputDirectory; + } + set + { + _wrapperOutputDirectory = value; + } + } + + private string _wrapperOutputDirectory = null; + + /// + /// When set to true, the typelib version will be included in the wrapper name. Default is false. + /// + public bool IncludeVersionInInteropName + { + get + { + return _includeVersionInInteropName; + } + + set + { + _includeVersionInInteropName = value; + } + } + + private bool _includeVersionInInteropName; + + /// + /// source of resolved .NET assemblies - we need this for ActiveX wrappers, since we can't resolve .NET assembly + /// references ourselves + /// + public ITaskItem[] ResolvedAssemblyReferences + { + get + { + return _resolvedAssemblyReferences; + } + set + { + _resolvedAssemblyReferences = value; + } + } + + private ITaskItem[] _resolvedAssemblyReferences = null; + + /// + /// container name for public/private keys + /// + public string KeyContainer + { + get + { + return _keyContainer; + } + set + { + _keyContainer = value; + } + } + + private string _keyContainer = null; + + /// + /// file containing public/private keys + /// + public string KeyFile + { + get + { + return _keyFile; + } + set + { + _keyFile = value; + } + } + + private string _keyFile = null; + + /// + /// delay sign wrappers? + /// + public bool DelaySign + { + get + { + return _delaySign; + } + set + { + _delaySign = value; + } + } + + private bool _delaySign = false; + + /// + /// Passes the TypeLibImporterFlags.PreventClassMembers flag to tlb wrapper generation + /// + public bool NoClassMembers + { + get + { + return _noClassMembers; + } + set + { + _noClassMembers = value; + } + } + + private bool _noClassMembers = false; + + /// + /// If true, do not log messages or warnings. Default is false. + /// + public bool Silent + { + get + { + return _silent; + } + + set + { + _silent = value; + } + } + + private bool _silent = false; + + /// + /// The preferred target processor architecture. Passed to tlbimp.exe /machine flag after translation. + /// Should be a member of Microsoft.Build.Utilities.ProcessorArchitecture. + /// + public string TargetProcessorArchitecture + { + get + { + return _targetProcessorArchitecture; + } + + set + { + if (UtilitiesProcessorArchitecture.X86.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + _targetProcessorArchitecture = UtilitiesProcessorArchitecture.X86; + } + else if (UtilitiesProcessorArchitecture.MSIL.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + _targetProcessorArchitecture = UtilitiesProcessorArchitecture.MSIL; + } + else if (UtilitiesProcessorArchitecture.AMD64.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + _targetProcessorArchitecture = UtilitiesProcessorArchitecture.AMD64; + } + else if (UtilitiesProcessorArchitecture.IA64.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + _targetProcessorArchitecture = UtilitiesProcessorArchitecture.IA64; + } + else if (UtilitiesProcessorArchitecture.ARM.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + _targetProcessorArchitecture = UtilitiesProcessorArchitecture.ARM; + } + else + { + _targetProcessorArchitecture = value; + } + } + } + + private string _targetProcessorArchitecture = null; + + /// + /// Property to allow multitargeting of ResolveComReferences: If true, tlbimp.exe + /// from the appropriate target framework will be run out-of-proc to generate + /// the necessary wrapper assemblies. Aximp is always run out of proc. + /// + public bool ExecuteAsTool + { + get + { + return _executeAsTool; + } + set + { + _executeAsTool = value; + } + } + + private bool _executeAsTool = true; + + /// + /// paths to found/generated reference wrappers + /// + [Output] + public ITaskItem[] ResolvedFiles + { + get + { + return _resolvedFiles; + } + set + { + _resolvedFiles = value; + } + } + + private ITaskItem[] _resolvedFiles = null; + + /// + /// paths to found modules (needed for isolation) + /// + [Output] + public ITaskItem[] ResolvedModules + { + get + { + return _resolvedModules; + } + set + { + _resolvedModules = value; + } + } + + private ITaskItem[] _resolvedModules = null; + + /// + /// If ExecuteAsTool is true, this must be set to the SDK + /// tools path for the framework version being targeted. + /// + public string SdkToolsPath + { + get { return _sdkToolsPath; } + set { _sdkToolsPath = value; } + } + + private string _sdkToolsPath; + + /// + /// Cache file for COM component timestamps. If not present, every run will regenerate all the wrappers. + /// + public string StateFile + { + get { return _stateFile; } + set { _stateFile = value; } + } + + private string _stateFile = null; + + /// + /// The project target framework version. + /// + /// Default is empty. which means there will be no filtering for the reference based on their target framework. + /// + /// + public string TargetFrameworkVersion + { + get { return _projectTargetFrameworkAsString; } + set { _projectTargetFrameworkAsString = value; } + } + + private string _projectTargetFrameworkAsString = String.Empty; + private Version _projectTargetFramework; + + + /// version 4.0 + private static readonly Version s_targetFrameworkVersion_40 = new Version("4.0"); + + private ResolveComReferenceCache _timestampCache = null; + + // Cache hashtables for different wrapper types + private Hashtable _cachePia = new Hashtable(); + private Hashtable _cacheTlb = new Hashtable(); + private Hashtable _cacheAx = new Hashtable(); + + // Paths for the out-of-proc tools being used + private string _aximpPath; + private string _tlbimpPath; + + #endregion + + #region ITask members + + /// + /// Task entry point. + /// + /// + public override bool Execute() + { + if (!VerifyAndInitializeInputs()) + { + return false; + } + + if (!ComputePathToAxImp() || !ComputePathToTlbImp()) + { + // unable to compute the path to tlbimp.exe, aximp.exe, or both and that is necessary to + // continue forward, so return now. + return false; + } + + allProjectRefs = new List(); + allDependencyRefs = new List(); + + _timestampCache = (ResolveComReferenceCache)StateFileBase.DeserializeCache(StateFile, Log, typeof(ResolveComReferenceCache)); + + if (_timestampCache == null || (_timestampCache != null && !_timestampCache.ToolPathsMatchCachePaths(_tlbimpPath, _aximpPath))) + { + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.NotUsingCacheFile", StateFile == null ? String.Empty : StateFile); + } + + _timestampCache = new ResolveComReferenceCache(_tlbimpPath, _aximpPath); + } + else if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.UsingCacheFile", StateFile == null ? String.Empty : StateFile); + } + + try + { + ConvertAttrReferencesToComReferenceInfo(allProjectRefs, TypeLibNames); + ConvertFileReferencesToComReferenceInfo(allProjectRefs, TypeLibFiles); + + // add missing tlbimp references for aximp ones + AddMissingTlbReferences(); + + // see if we have any typelib name clashes. Ignore the return value - we now remove the conflicting refs + // and continue (first one wins) + CheckForConflictingReferences(); + + SetFrameworkVersionFromString(_projectTargetFrameworkAsString); + + // Process each task item. If one of them fails we still process the rest of them, but + // remember that the task should return failure. + // DESIGN CHANGE: we no longer fail the task when one or more references fail to resolve. + // Unless we experience a catastrophic failure, we'll log warnings for those refs and proceed + // (and return success) + ArrayList moduleList = new ArrayList(); + ArrayList resolvedReferenceList = new ArrayList(); + + ComDependencyWalker dependencyWalker = new ComDependencyWalker(Marshal.ReleaseComObject); + bool allReferencesResolvedSuccessfully = true; + for (int pass = 0; pass < 4; pass++) + { + foreach (ComReferenceInfo projectRefInfo in allProjectRefs) + { + string wrapperType = projectRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool); + + // first resolve all PIA refs, then regular tlb refs and finally ActiveX refs + if ((pass == 0 && ComReferenceTypes.IsPia(wrapperType)) || + (pass == 1 && ComReferenceTypes.IsTlbImp(wrapperType)) || + (pass == 2 && ComReferenceTypes.IsPiaOrTlbImp(wrapperType)) || + (pass == 3 && ComReferenceTypes.IsAxImp(wrapperType))) + { + try + { + if (!this.ResolveReferenceAndAddToList(dependencyWalker, projectRefInfo, resolvedReferenceList, moduleList)) + { + allReferencesResolvedSuccessfully = false; + } + } + catch (ComReferenceResolutionException) + { + // problem resolving this reference? continue so that we can display all error messages + } + catch (StrongNameException) + { + // key extraction problem? No point in continuing, since all wrappers will hit the same problem. + // error message has already been logged + return false; + } + catch (FileLoadException ex) + { + // This exception is thrown when we try to load a delay signed assembly without disabling + // strong name verification first. So print a nice information if we're generating + // delay signed wrappers, otherwise rethrow, since it's an unexpected exception. + if (DelaySign) + { + Log.LogErrorWithCodeFromResources(null, projectRefInfo.SourceItemSpec, 0, 0, 0, 0, "ResolveComReference.LoadingDelaySignedAssemblyWithStrongNameVerificationEnabled", ex.Message); + + // no point in printing the same thing multiple times... + return false; + } + else + { + Debug.Assert(false, "Unexpected exception in ResolveComReference.Execute. " + + "Please log a MSBuild bug specifying the steps to reproduce the problem."); + throw; + } + } + catch (ArgumentException ex) + { + // This exception is thrown when we try to convert some of the Metadata from the project + // file and the conversion fails. Most likely, the user needs to correct a type in the + // project file. + Log.LogErrorWithCodeFromResources("General.InvalidArgument", ex.Message); + return false; + } + catch (SystemException ex) + { + Log.LogErrorWithCodeFromResources("ResolveComReference.FailedToResolveComReference", + projectRefInfo.attr.guid, projectRefInfo.attr.wMajorVerNum, projectRefInfo.attr.wMinorVerNum, + ex.Message); + } + } + } + } + + SetCopyLocalToFalseOnGacOrNoPIAAssemblies(resolvedReferenceList, GlobalAssemblyCache.GetGacPath()); + + ResolvedModules = (ITaskItem[])moduleList.ToArray(typeof(ITaskItem)); + ResolvedFiles = (ITaskItem[])resolvedReferenceList.ToArray(typeof(ITaskItem)); + + // The Logs from AxImp and TlbImp aren't part of our log, but if the task failed, it will return false from + // GenerateWrapper, which should get passed all the way back up here. + return allReferencesResolvedSuccessfully && !Log.HasLoggedErrors; + } + finally + { + if ((_timestampCache != null) && _timestampCache.Dirty) + { + _timestampCache.SerializeCache(StateFile, Log); + } + + Cleanup(); + } + } + + #endregion + + #region Methods + + /// + /// Converts the string target framework value to a number. + /// Accepts both "v" prefixed and no "v" prefixed formats + /// if format is bad will log a message and return 0. + /// + /// Target framework version value + internal void SetFrameworkVersionFromString(string version) + { + Version parsedVersion = null; + if (!String.IsNullOrEmpty(version)) + { + parsedVersion = VersionUtilities.ConvertToVersion(version); + + if (parsedVersion == null && !Silent) + { + Log.LogMessageFromResources(MessageImportance.Normal, "ResolveComReference.BadTargetFrameworkFormat", version); + } + } + + _projectTargetFramework = parsedVersion; + return; + } + + /// + /// Computes the path to TlbImp.exe for use in logging and for passing to the + /// nested TlbImp task. + /// + /// True if the path is found (or it doesn't matter because we're executing in memory), false otherwise + private bool ComputePathToTlbImp() + { + _tlbimpPath = null; + + if (String.IsNullOrEmpty(_sdkToolsPath)) + { + _tlbimpPath = GetPathToSDKFileWithCurrentlyTargetedArchitecture("TlbImp.exe", TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest); + + if (null == _tlbimpPath && ExecuteAsTool) + { + Log.LogErrorWithCodeFromResources("General.PlatformSDKFileNotFound", "TlbImp.exe", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest)); + } + } + else + { + _tlbimpPath = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, this.TargetProcessorArchitecture, SdkToolsPath, "TlbImp.exe", Log, ExecuteAsTool); + } + + if (null == _tlbimpPath && !ExecuteAsTool) + { + // if TlbImp.exe is not installed, just use the filename + _tlbimpPath = "TlbImp.exe"; + return true; + } + + if (_tlbimpPath != null) + { + _tlbimpPath = Path.GetDirectoryName(_tlbimpPath); + } + + return _tlbimpPath != null; + } + + /// + /// Computes the path to AxImp.exe for use in logging and for passing to the + /// nested AxImp task. + /// + /// True if the path is found, false otherwise + private bool ComputePathToAxImp() + { + // We always execute AxImp.exe out of proc + _aximpPath = null; + + if (String.IsNullOrEmpty(_sdkToolsPath)) + { + // In certain cases -- such as trying to build a Dev10 project on a machine that only has Dev11 installed -- + // it's possible to have ExecuteAsTool set to false (e.g. "use the current CLR") but still have SDKToolsPath + // be empty (because it's referencing the 7.0A SDK in the registry, which doesn't exist). In that case, we + // want to look for VersionLatest. However, if ExecuteAsTool is true (default value) and SDKToolsPath is + // empty, then we can safely assume that we want to get the 3.5 version of the tool. + TargetDotNetFrameworkVersion targetAxImpVersion = ExecuteAsTool ? TargetDotNetFrameworkVersion.Version35 : TargetDotNetFrameworkVersion.VersionLatest; + + // We want to use the copy of AxImp corresponding to our targeted architecture if possible. + _aximpPath = GetPathToSDKFileWithCurrentlyTargetedArchitecture("AxImp.exe", targetAxImpVersion, VisualStudioVersion.VersionLatest); + + if (null == _aximpPath) + { + Log.LogErrorWithCodeFromResources("General.PlatformSDKFileNotFound", "AxImp.exe", + ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(targetAxImpVersion, VisualStudioVersion.VersionLatest), + ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(targetAxImpVersion, VisualStudioVersion.VersionLatest)); + } + } + else + { + _aximpPath = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, this.TargetProcessorArchitecture, SdkToolsPath, "AxImp.exe", Log, true /* log errors */); + } + + if (_aximpPath != null) + { + _aximpPath = Path.GetDirectoryName(_aximpPath); + } + + return _aximpPath != null; + } + + /// + /// Try to get the path to the tool in the Windows SDK with the given .NET Framework version and + /// of the same architecture as we were currently given for TargetProcessorArchitecture. + /// + private string GetPathToSDKFileWithCurrentlyTargetedArchitecture(string file, TargetDotNetFrameworkVersion targetFrameworkVersion, VisualStudioVersion visualStudioVersion) + { + string path = null; + + switch (this.TargetProcessorArchitecture) + { + case UtilitiesProcessorArchitecture.ARM: + case UtilitiesProcessorArchitecture.X86: + path = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(file, targetFrameworkVersion, visualStudioVersion, DotNetFrameworkArchitecture.Bitness32); + break; + case UtilitiesProcessorArchitecture.AMD64: + case UtilitiesProcessorArchitecture.IA64: + path = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(file, targetFrameworkVersion, visualStudioVersion, DotNetFrameworkArchitecture.Bitness64); + break; + case UtilitiesProcessorArchitecture.MSIL: + default: + // just go with the default lookup + break; + } + + if (path == null) + { + // fall back to the default lookup (current architecture / x86) just in case it's found there ... + path = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(file, targetFrameworkVersion, visualStudioVersion); + } + + return path; + } + + /// + /// Clean various caches and other state that should not be preserved between subsequent runs + /// + private void Cleanup() + { + // clear the wrapper caches - since references can change between runs, wrapper objects should not be reused + _cacheAx.Clear(); + _cachePia.Clear(); + _cacheTlb.Clear(); + + // release COM interface pointers for dependency references + foreach (ComReferenceInfo dependencyRefInfo in allDependencyRefs) + { + dependencyRefInfo.ReleaseTypeLibPtr(); + } + + // release COM interface pointers for project references + foreach (ComReferenceInfo projectRefInfo in allProjectRefs) + { + projectRefInfo.ReleaseTypeLibPtr(); + } + } + + /* + * Method: VerifyAndInitializeInputs + * + * Helper method. Verifies the input task items have correct metadata and initializes optional ones with + * default values if they're not present. + */ + private bool VerifyAndInitializeInputs() + { + if ((KeyContainer != null && KeyContainer.Length != 0) + && (KeyFile != null && KeyFile.Length != 0)) + { + Log.LogErrorWithCodeFromResources("ResolveComReference.CannotSpecifyBothKeyFileAndKeyContainer"); + return false; + } + + if (DelaySign) + { + if ((KeyContainer == null || KeyContainer.Length == 0) && + (KeyFile == null || KeyFile.Length == 0)) + { + Log.LogErrorWithCodeFromResources("ResolveComReference.CannotSpecifyDelaySignWithoutEitherKeyFileOrKeyContainer"); + return false; + } + } + + // if no output directory specified, default to the project directory + if (WrapperOutputDirectory == null) + { + WrapperOutputDirectory = String.Empty; + } + + int typeLibNamesLength = (TypeLibNames == null) ? 0 : TypeLibNames.GetLength(0); + int typeLibFilesLength = (TypeLibFiles == null) ? 0 : TypeLibFiles.GetLength(0); + + // nothing to do? we cannot tell the difference between not passing in anything and passing in empty list, + // so let's just exit. + if (typeLibFilesLength + typeLibNamesLength == 0) + { + Log.LogErrorWithCodeFromResources("ResolveComReference.NoComReferencesSpecified"); + return false; + } + + bool metadataValid = true; + + for (int i = 0; i < typeLibNamesLength; i++) + { + // verify the COM reference item contains all the required attributes + string missingMetadata; + + if (!VerifyReferenceMetadataForNameItem(TypeLibNames[i], out missingMetadata)) + { + Log.LogErrorWithCodeFromResources(null, TypeLibNames[i].ItemSpec, 0, 0, 0, 0, "ResolveComReference.MissingOrUnknownComReferenceAttribute", missingMetadata, TypeLibNames[i].ItemSpec); + + // don't exit immediately... check all the refs and display all errors + metadataValid = false; + } + else + { + // Initialize optional attributes with default values if they're missing + InitializeDefaultMetadataForNameItem(TypeLibNames[i]); + } + } + + for (int i = 0; i < typeLibFilesLength; i++) + { + // File COM references don't have any required metadata, so no verification necessary here + // Initialize optional metadata with default values if they're missing + InitializeDefaultMetadataForFileItem(TypeLibFiles[i]); + } + + return metadataValid; + } + + /* + * Method: ConvertAttrReferencesToComReferenceInfo + * + * Helper method. Converts TypeLibAttr references to ComReferenceInfo objects. + * This method cannot fail, since we want to proceed with the task even if some references won't load. + */ + private void ConvertAttrReferencesToComReferenceInfo(List projectRefs, ITaskItem[] typeLibAttrs) + { + int typeLibAttrsLength = (typeLibAttrs == null) ? 0 : typeLibAttrs.GetLength(0); + + for (int i = 0; i < typeLibAttrsLength; i++) + { + ComReferenceInfo projectRefInfo = new ComReferenceInfo(); + + try + { + if (projectRefInfo.InitializeWithTypeLibAttrs(Log, Silent, TaskItemToTypeLibAttr(typeLibAttrs[i]), typeLibAttrs[i], this.TargetProcessorArchitecture)) + { + projectRefs.Add(projectRefInfo); + } + else + { + projectRefInfo.ReleaseTypeLibPtr(); + } + } + catch (COMException ex) + { + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLibItemSpec", typeLibAttrs[i].ItemSpec, ex.Message); + } + + projectRefInfo.ReleaseTypeLibPtr(); + // we don't want to fail the task if one of the references is not registered, so just continue + } + } + } + + /* + * Method: ConvertFileReferencesToComReferenceInfo + * + * Helper method. Converts TypeLibFiles references to ComReferenceInfo objects + * This method cannot fail, since we want to proceed with the task even if some references won't load. + */ + private void ConvertFileReferencesToComReferenceInfo(List projectRefs, ITaskItem[] tlbFiles) + { + int tlbFilesLength = (tlbFiles == null) ? 0 : tlbFiles.GetLength(0); + + for (int i = 0; i < tlbFilesLength; i++) + { + string refPath = tlbFiles[i].ItemSpec; + + if (!Path.IsPathRooted(refPath)) + refPath = Path.Combine(Directory.GetCurrentDirectory(), refPath); + + ComReferenceInfo projectRefInfo = new ComReferenceInfo(); + + try + { + if (projectRefInfo.InitializeWithPath(Log, Silent, refPath, tlbFiles[i], this.TargetProcessorArchitecture)) + { + projectRefs.Add(projectRefInfo); + } + else + { + projectRefInfo.ReleaseTypeLibPtr(); + } + } + catch (COMException ex) + { + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLibItemSpec", tlbFiles[i].ItemSpec, ex.Message); + } + + projectRefInfo.ReleaseTypeLibPtr(); + // we don't want to fail the task if one of the references is not registered, so just continue + } + } + } + + /// + /// Every ActiveX reference (aximp) requires a corresponding tlbimp reference. If the tlbimp reference is + /// missing from the project file we pretend it's there to save the user some useless typing. + /// + internal void AddMissingTlbReferences() + { + var newProjectRefs = new List(); + + foreach (ComReferenceInfo axRefInfo in allProjectRefs) + { + // Try to find the matching tlbimp/pia reference for each aximp reference + // There is an obscured case in this algorithm: there may be more than one match. Arbitrarily chooses the first. + if (ComReferenceTypes.IsAxImp(axRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool))) + { + bool matchingTlbRefPresent = false; + + foreach (ComReferenceInfo tlbRefInfo in allProjectRefs) + { + string tlbWrapperType = tlbRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool); + + if (ComReferenceTypes.IsTlbImp(tlbWrapperType) || ComReferenceTypes.IsPia(tlbWrapperType) || ComReferenceTypes.IsPiaOrTlbImp(tlbWrapperType)) + { + if (ComReference.AreTypeLibAttrEqual(axRefInfo.attr, tlbRefInfo.attr)) + { + axRefInfo.taskItem.SetMetadata(ComReferenceItemMetadataNames.tlbReferenceName, tlbRefInfo.typeLibName); + + // Check and demote EmbedInteropTypes to "false" for wrappers of ActiveX controls. The compilers won't embed + // the ActiveX control and so will transitively turn this wrapper into a reference as well. We need to know to + // make the wrapper CopyLocal=true later so switch to EmbedInteropTypes=false now. + string embedInteropTypes = tlbRefInfo.taskItem.GetMetadata(ItemMetadataNames.embedInteropTypes); + if (ConversionUtilities.CanConvertStringToBool(embedInteropTypes)) + { + if (ConversionUtilities.ConvertStringToBool(embedInteropTypes)) + { + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.High, "ResolveComReference.TreatingTlbOfActiveXAsNonEmbedded", tlbRefInfo.taskItem.ItemSpec, axRefInfo.taskItem.ItemSpec); + } + + tlbRefInfo.taskItem.SetMetadata(ItemMetadataNames.embedInteropTypes, "false"); + } + } + axRefInfo.primaryOfAxImpRef = tlbRefInfo; + matchingTlbRefPresent = true; + break; + } + } + } + + // add the matching tlbimp ref if not already there + if (!matchingTlbRefPresent) + { + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.AddingMissingTlbReference", axRefInfo.taskItem.ItemSpec); + } + + ComReferenceInfo newTlbRef = new ComReferenceInfo(axRefInfo); + newTlbRef.taskItem.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.primaryortlbimp); + newTlbRef.taskItem.SetMetadata(ItemMetadataNames.embedInteropTypes, "false"); + axRefInfo.primaryOfAxImpRef = newTlbRef; + + newProjectRefs.Add(newTlbRef); + axRefInfo.taskItem.SetMetadata(ComReferenceItemMetadataNames.tlbReferenceName, newTlbRef.typeLibName); + } + } + } + + foreach (ComReferenceInfo refInfo in newProjectRefs) + { + allProjectRefs.Add(refInfo); + } + } + + /// + /// Resolves the COM reference, and adds it to the appropriate item list. + /// + /// + /// + /// + /// + private bool ResolveReferenceAndAddToList + ( + ComDependencyWalker dependencyWalker, + ComReferenceInfo projectRefInfo, + ArrayList resolvedReferenceList, + ArrayList moduleList + ) + { + ITaskItem referencePath; + + if (ResolveReference(dependencyWalker, projectRefInfo, WrapperOutputDirectory, out referencePath)) + { + resolvedReferenceList.Add(referencePath); + + bool metadataFound = false; + bool isolated = MetadataConversionUtilities.TryConvertItemMetadataToBool(projectRefInfo.taskItem, "Isolated", out metadataFound); + + if (metadataFound && isolated) + { + string modulePath = projectRefInfo.strippedTypeLibPath; + if (modulePath != null) + { + ITaskItem moduleItem = new TaskItem(modulePath); + moduleItem.SetMetadata("Name", projectRefInfo.taskItem.ItemSpec); + moduleList.Add(moduleItem); + } + else + { + return false; + } + } + } + else + { + return false; + } + + return true; + } + + /* + * Method: ResolveReference + * + * Helper COM resolution method. Creates an appropriate helper class for the given tool and calls + * the Resolve method on it. + */ + internal bool ResolveReference(ComDependencyWalker dependencyWalker, ComReferenceInfo referenceInfo, string outputDirectory, out ITaskItem referencePathItem) + { + if (referenceInfo.referencePathItem == null) + { + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.Resolving", referenceInfo.taskItem.ItemSpec, referenceInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool)); + } + + List dependencyPaths = ScanAndResolveAllDependencies(dependencyWalker, referenceInfo); + + referenceInfo.dependentWrapperPaths = dependencyPaths; + referencePathItem = new TaskItem(); + referenceInfo.referencePathItem = referencePathItem; + + ComReferenceWrapperInfo wrapperInfo; + + if (ResolveComClassicReference(referenceInfo, outputDirectory, + referenceInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool), + referenceInfo.taskItem.ItemSpec, true, referenceInfo.dependentWrapperPaths, out wrapperInfo)) + { + referencePathItem.ItemSpec = wrapperInfo.path; + referenceInfo.taskItem.CopyMetadataTo(referencePathItem); + + string fusionName = AssemblyName.GetAssemblyName(wrapperInfo.path).FullName; + referencePathItem.SetMetadata(ItemMetadataNames.fusionName, fusionName); + + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ResolvedReference", referenceInfo.taskItem.ItemSpec, wrapperInfo.path); + } + + return true; + } + + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.CannotFindWrapperForTypeLib", referenceInfo.taskItem.ItemSpec); + } + + return false; + } + else + { + bool successfullyResolved = !String.IsNullOrEmpty(referenceInfo.referencePathItem.ItemSpec); + referencePathItem = referenceInfo.referencePathItem; + + return successfullyResolved; + } + } + + /* + * Method: IsExistingProjectReference + * + * If given typelib attributes are already a project reference, return that reference. + */ + internal bool IsExistingProjectReference(TYPELIBATTR typeLibAttr, string neededRefType, out ComReferenceInfo referenceInfo) + { + for (int pass = 0; pass < 3; pass++) + { + // First PIAs, then tlbimps, then aximp + // Only execute each pass if the needed ref type matches or is null + // Important: the condition for Ax wrapper is different, since we don't want to find Ax references + // for unknown wrapper types - "unknown" wrapper means we're only looking for a tlbimp or a primary reference + if ((pass == 0 && (ComReferenceTypes.IsPia(neededRefType) || neededRefType == null)) || + (pass == 1 && (ComReferenceTypes.IsTlbImp(neededRefType) || neededRefType == null)) || + (pass == 2 && (ComReferenceTypes.IsAxImp(neededRefType)))) + { + foreach (ComReferenceInfo projectRefInfo in allProjectRefs) + { + string wrapperType = projectRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool); + + // First PIAs, then tlbimps, then aximp + if ((pass == 0 && ComReferenceTypes.IsPia(wrapperType)) || + (pass == 1 && ComReferenceTypes.IsTlbImp(wrapperType)) || + (pass == 2 && ComReferenceTypes.IsAxImp(wrapperType))) + { + // found it? return the existing reference + if (ComReference.AreTypeLibAttrEqual(projectRefInfo.attr, typeLibAttr)) + { + referenceInfo = projectRefInfo; + return true; + } + } + } + } + } + + referenceInfo = null; + return false; + } + + /* + * Method: IsExistingDependencyReference + * + * If given typelib attributes are already a dependency reference (that is, was already + * processed) return that reference. + */ + internal bool IsExistingDependencyReference(TYPELIBATTR typeLibAttr, out ComReferenceInfo referenceInfo) + { + foreach (ComReferenceInfo dependencyRefInfo in allDependencyRefs) + { + // found it? return the existing reference + if (ComReference.AreTypeLibAttrEqual(dependencyRefInfo.attr, typeLibAttr)) + { + referenceInfo = dependencyRefInfo; + return true; + } + } + + referenceInfo = null; + return false; + } + + /* + * Method: ResolveComClassicReference + * + * Resolves a COM classic reference given the type library attributes and the type of wrapper to use. + * If wrapper type is not specified, this method will first look for an existing reference in the project, + * fall back to looking for a PIA and finally try to generate a regular tlbimp wrapper. + */ + internal bool ResolveComClassicReference(ComReferenceInfo referenceInfo, string outputDirectory, string wrapperType, string refName, bool topLevelRef, List dependencyPaths, out ComReferenceWrapperInfo wrapperInfo) + { + wrapperInfo = null; + + bool retVal = false; + + // only look for an existing PIA + if (ComReferenceTypes.IsPia(wrapperType)) + { + retVal = ResolveComReferencePia(referenceInfo, refName, out wrapperInfo); + } + // find/generate a tlb wrapper + else if (ComReferenceTypes.IsTlbImp(wrapperType)) + { + retVal = ResolveComReferenceTlb(referenceInfo, outputDirectory, refName, topLevelRef, dependencyPaths, out wrapperInfo); + } + // find/generate an Ax wrapper + else if (ComReferenceTypes.IsAxImp(wrapperType)) + { + retVal = ResolveComReferenceAx(referenceInfo, outputDirectory, refName, out wrapperInfo); + } + // find/generate a pia/tlb wrapper (it's only possible to get here via a callback) + else if (wrapperType == null || ComReferenceTypes.IsPiaOrTlbImp(wrapperType)) + { + // if this reference does not exist in the project, try looking for a PIA first + retVal = ResolveComReferencePia(referenceInfo, refName, out wrapperInfo); + if (!retVal) + { + // failing that, try a regular tlb wrapper + retVal = ResolveComReferenceTlb(referenceInfo, outputDirectory, refName, false /* dependency */, dependencyPaths, out wrapperInfo); + } + } + else + { + ErrorUtilities.VerifyThrow(false, "Unknown wrapper type!"); + } + referenceInfo.resolvedWrapper = wrapperInfo; + + // update the timestamp cache with the timestamp of the component we just processed + _timestampCache[referenceInfo.strippedTypeLibPath] = File.GetLastWriteTime(referenceInfo.strippedTypeLibPath); + + return retVal; + } + + /* + * Method: ResolveComClassicReference + * + * Resolves a COM classic reference given the type library attributes and the type of wrapper to use. + * If wrapper type is not specified, this method will first look for an existing reference in the project, + * fall back to looking for a PIA and finally try to generate a regular tlbimp wrapper. + * + * This is the method available for references to call back to resolve their dependencies + */ + bool IComReferenceResolver.ResolveComClassicReference(TYPELIBATTR typeLibAttr, string outputDirectory, string wrapperType, string refName, out ComReferenceWrapperInfo wrapperInfo) + { + // does this reference exist in the project or is it a dependency? + bool topLevelRef = false; + + wrapperInfo = null; + + // remap the type lib to ADO 2.7 if necessary + TYPELIBATTR oldAttr = typeLibAttr; + + if (ComReference.RemapAdoTypeLib(Log, Silent, ref typeLibAttr) && !Silent) + { + // if successfully remapped the reference to ADO 2.7, notify the user + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.RemappingAdoTypeLib", oldAttr.wMajorVerNum, oldAttr.wMinorVerNum); + } + + ComReferenceInfo referenceInfo; + + // find an existing ref in the project (taking the desired wrapperType into account, if any) + if (IsExistingProjectReference(typeLibAttr, wrapperType, out referenceInfo)) + { + // IsExistingProjectReference should not return null... + Debug.Assert(referenceInfo != null, "IsExistingProjectReference should not return null"); + topLevelRef = true; + wrapperType = referenceInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool); + } + // was this dependency already processed? + else if (IsExistingDependencyReference(typeLibAttr, out referenceInfo)) + { + Debug.Assert(referenceInfo != null, "IsExistingDependencyReference should not return null"); + + // we've seen this dependency before, so we should know what its wrapper type is. + if (wrapperType == null || ComReferenceTypes.IsPiaOrTlbImp(wrapperType)) + { + string typeLibKey = ComReference.UniqueKeyFromTypeLibAttr(typeLibAttr); + if (_cachePia.ContainsKey(typeLibKey)) + { + wrapperType = ComReferenceTypes.primary; + } + else if (_cacheTlb.ContainsKey(typeLibKey)) + { + wrapperType = ComReferenceTypes.tlbimp; + } + } + } + // if not found anywhere, create a new ComReferenceInfo object and resolve it. + else + { + try + { + referenceInfo = new ComReferenceInfo(); + + if (referenceInfo.InitializeWithTypeLibAttrs(Log, Silent, typeLibAttr, null, this.TargetProcessorArchitecture)) + { + allDependencyRefs.Add(referenceInfo); + } + else + { + referenceInfo.ReleaseTypeLibPtr(); + return false; + } + } + catch (COMException ex) + { + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLib", typeLibAttr.guid, + typeLibAttr.wMajorVerNum.ToString(CultureInfo.InvariantCulture), + typeLibAttr.wMinorVerNum.ToString(CultureInfo.InvariantCulture), + ex.Message); + } + + referenceInfo.ReleaseTypeLibPtr(); + + // can't resolve an unregistered and unknown dependency, so return false + return false; + } + } + + // if we don't have the reference name, use the typelib name + if (refName == null) + { + refName = referenceInfo.typeLibName; + } + + return ResolveComClassicReference(referenceInfo, outputDirectory, wrapperType, refName, topLevelRef, referenceInfo.dependentWrapperPaths, out wrapperInfo); + } + + /* + * Method: ResolveNetAssemblyReference + * + * Resolves a .NET assembly reference using the list of resolved managed references supplied to the task. + * + * This is the method available for references to call back to resolve their dependencies + */ + bool IComReferenceResolver.ResolveNetAssemblyReference(string assemblyName, out string assemblyPath) + { + int commaIndex = assemblyName.IndexOf(','); + + // if we have a strong name, strip off everything but the assembly name + if (commaIndex != -1) + assemblyName = assemblyName.Substring(0, commaIndex); + + assemblyName += ".dll"; + + for (int i = 0; i < ResolvedAssemblyReferences.GetLength(0); i++) + { + if (String.Compare(Path.GetFileName(ResolvedAssemblyReferences[i].ItemSpec), assemblyName, StringComparison.OrdinalIgnoreCase) == 0) + { + assemblyPath = ResolvedAssemblyReferences[i].ItemSpec; + return true; + } + } + + assemblyPath = null; + return false; + } + + /* + * Method: ResolveComAssemblyReference + * + * Resolves a COM wrapper assembly reference based on the COM references resolved so far. This method is necessary + * for Ax wrappers only, so all necessary references will be resolved by then (since we resolve them in + * the following order: pia, tlbimp, aximp) + * + * This is the method available for references to call back to resolve their dependencies + */ + bool IComReferenceResolver.ResolveComAssemblyReference(string fullAssemblyName, out string assemblyPath) + { + AssemblyNameExtension fullAssemblyNameEx = new AssemblyNameExtension(fullAssemblyName); + + foreach (ComReferenceWrapperInfo wrapperInfo in _cachePia.Values) + { + // this should not happen, but it would be a non fatal error + Debug.Assert(wrapperInfo.path != null); + if (wrapperInfo.path == null) + continue; + + // we have already verified all cached wrappers, so we don't expect this methods to throw anything + AssemblyNameExtension wrapperAssemblyNameEx = new AssemblyNameExtension(AssemblyName.GetAssemblyName(wrapperInfo.path)); + + if (fullAssemblyNameEx.Equals(wrapperAssemblyNameEx)) + { + assemblyPath = wrapperInfo.path; + return true; + } + // The PIA might have been redirected, so check its original assembly name too + else if (fullAssemblyNameEx.Equals(wrapperInfo.originalPiaName)) + { + assemblyPath = wrapperInfo.path; + return true; + } + } + + foreach (ComReferenceWrapperInfo wrapperInfo in _cacheTlb.Values) + { + // temporary wrapper? skip it. + if (wrapperInfo.path == null) + continue; + + // we have already verified all cached wrappers, so we don't expect this methods to throw anything + AssemblyNameExtension wrapperAssemblyNameEx = new AssemblyNameExtension(AssemblyName.GetAssemblyName(wrapperInfo.path)); + + if (fullAssemblyNameEx.Equals(wrapperAssemblyNameEx)) + { + assemblyPath = wrapperInfo.path; + return true; + } + } + + foreach (ComReferenceWrapperInfo wrapperInfo in _cacheAx.Values) + { + // this should not happen, but it would be a non fatal error + Debug.Assert(wrapperInfo.path != null); + if (wrapperInfo.path == null) + continue; + + // we have already verified all cached wrappers, so we don't expect this methods to throw anything + AssemblyNameExtension wrapperAssemblyNameEx = new AssemblyNameExtension(AssemblyName.GetAssemblyName(wrapperInfo.path)); + + if (fullAssemblyNameEx.Equals(wrapperAssemblyNameEx)) + { + assemblyPath = wrapperInfo.path; + return true; + } + } + + assemblyPath = null; + return false; + } + + /// + /// Helper function - resolves a PIA COM classic reference given the type library attributes. + /// + /// Information about the reference to be resolved + /// Name of reference + /// Information about wrapper locations + /// True if the reference was already found or successfully generated, false otherwise. + internal bool ResolveComReferencePia(ComReferenceInfo referenceInfo, string refName, out ComReferenceWrapperInfo wrapperInfo) + { + wrapperInfo = null; + string typeLibKey = ComReference.UniqueKeyFromTypeLibAttr(referenceInfo.attr); + + // look in the PIA cache first + if (_cachePia.ContainsKey(typeLibKey)) + { + wrapperInfo = (ComReferenceWrapperInfo)_cachePia[typeLibKey]; + return true; + } + + try + { + // if not in the cache, we have no choice but to go looking for the PIA + PiaReference reference = new PiaReference(Log, Silent, referenceInfo, refName); + + // if not found, fail (we do not fall back to tlbimp wrappers if we're looking specifically for a PIA) + if (!reference.FindExistingWrapper(out wrapperInfo, _timestampCache[referenceInfo.strippedTypeLibPath])) + { + return false; + } + + // if found, add it to the PIA cache + _cachePia.Add(typeLibKey, wrapperInfo); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + else + { + return false; + } + } + + return true; + } + + /// + /// Return the set of item specs for the resolved assembly references. + /// + /// + internal IEnumerable GetResolvedAssemblyReferenceItemSpecs() + { + return (ResolvedAssemblyReferences == null) ? new string[0] : ResolvedAssemblyReferences.Select(rar => rar.ItemSpec); + } + + /// + /// Helper function - resolves a regular tlb COM classic reference given the type library attributes. + /// + /// Information about the reference to be resolved + /// Directory the interop DLL should be written to + /// Name of reference + /// True if this is a top-level reference + /// Information about wrapper locations + /// True if the reference was already found or successfully generated, false otherwise. + internal bool ResolveComReferenceTlb(ComReferenceInfo referenceInfo, string outputDirectory, string refName, bool topLevelRef, List dependencyPaths, out ComReferenceWrapperInfo wrapperInfo) + { + wrapperInfo = null; + string typeLibKey = ComReference.UniqueKeyFromTypeLibAttr(referenceInfo.attr); + + // look in the TLB cache first + if (_cacheTlb.ContainsKey(typeLibKey)) + { + wrapperInfo = (ComReferenceWrapperInfo)_cacheTlb[typeLibKey]; + return true; + } + + // is it a temporary wrapper? + bool isTemporary = false; + + // no top level (included in the project) refs can have temporary wrappers + if (!topLevelRef) + { + // wrapper is temporary if there's a top level tlb reference with the same typelib name, but different attributes + foreach (ComReferenceInfo projectRefInfo in allProjectRefs) + { + if (ComReferenceTypes.IsTlbImp(projectRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool))) + { + // conflicting typelib names for different typelibs? generate a temporary wrapper + if (!ComReference.AreTypeLibAttrEqual(referenceInfo.attr, projectRefInfo.attr) && + String.Compare(referenceInfo.typeLibName, projectRefInfo.typeLibName, StringComparison.OrdinalIgnoreCase) == 0) + { + isTemporary = true; + } + } + } + } + + try + { + List referencePaths = new List(GetResolvedAssemblyReferenceItemSpecs()); + + if (dependencyPaths != null) + { + referencePaths.AddRange(dependencyPaths); + } + + // not in the cache? see if anyone was kind enough to generate it for us + TlbReference reference = new TlbReference(Log, Silent, this, referencePaths, referenceInfo, refName, outputDirectory, isTemporary, DelaySign, KeyFile, KeyContainer, this.NoClassMembers, this.TargetProcessorArchitecture, IncludeVersionInInteropName, ExecuteAsTool, _tlbimpPath, BuildEngine, EnvironmentVariables); + + // wrapper doesn't exist or needs regeneration? generate it then + if (!reference.FindExistingWrapper(out wrapperInfo, _timestampCache[referenceInfo.strippedTypeLibPath])) + { + if (!reference.GenerateWrapper(out wrapperInfo)) + return false; + } + + // if found or successfully generated, cache it. + _cacheTlb.Add(typeLibKey, wrapperInfo); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + else + { + return false; + } + } + return true; + } + + /// + /// Helper function - resolves an ActiveX reference given the type library attributes. + /// + /// Information about the reference to be resolved + /// Directory the interop DLL should be written to + /// Name of reference + /// Information about wrapper locations + /// True if the reference was already found or successfully generated, false otherwise. + internal bool ResolveComReferenceAx(ComReferenceInfo referenceInfo, string outputDirectory, string refName, out ComReferenceWrapperInfo wrapperInfo) + { + wrapperInfo = null; + string typeLibKey = ComReference.UniqueKeyFromTypeLibAttr(referenceInfo.attr); + + // look in the Ax cache first + if (_cacheAx.ContainsKey(typeLibKey)) + { + wrapperInfo = (ComReferenceWrapperInfo)_cacheAx[typeLibKey]; + return true; + } + + try + { + // not in the cache? see if anyone was kind enough to generate it for us + + AxReference reference = new AxReference(Log, Silent, this, referenceInfo, refName, outputDirectory, DelaySign, KeyFile, KeyContainer, IncludeVersionInInteropName, _aximpPath, BuildEngine, EnvironmentVariables); + + // wrapper doesn't exist or needs regeneration? generate it then + if (!reference.FindExistingWrapper(out wrapperInfo, _timestampCache[referenceInfo.strippedTypeLibPath])) + { + if (!reference.GenerateWrapper(out wrapperInfo)) + return false; + } + + // if found or successfully generated, cache it. + _cacheAx.Add(typeLibKey, wrapperInfo); + } + catch (Exception e) + { + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + else + { + return false; + } + } + + return true; + } + + #region VerifyReferenceMetadataForNameItem required metadata + + // Metadata required on a valid Com reference item + private static readonly string[] s_requiredMetadataForNameItem = { + ComReferenceItemMetadataNames.guid, + ComReferenceItemMetadataNames.versionMajor, + ComReferenceItemMetadataNames.versionMinor + }; + + #endregion + + /* + * Method: VerifyReferenceMetadataForNameItem + * + * Verifies that all required metadata on the COM reference item are there. + */ + internal static bool VerifyReferenceMetadataForNameItem(ITaskItem reference, out string missingOrInvalidMetadata) + { + missingOrInvalidMetadata = ""; + + // go through the list of required metadata and fail if one of them is not found + foreach (string metadataName in s_requiredMetadataForNameItem) + { + if (reference.GetMetadata(metadataName).Length == 0) + { + missingOrInvalidMetadata = metadataName; + return false; + } + } + + // now verify they contain valid data + Guid guid; + if (!Guid.TryParse(reference.GetMetadata(ComReferenceItemMetadataNames.guid), out guid)) + { + // invalid guid format + missingOrInvalidMetadata = ComReferenceItemMetadataNames.guid; + return false; + } + + try + { + // invalid versionMajor format + missingOrInvalidMetadata = ComReferenceItemMetadataNames.versionMajor; + short.Parse(reference.GetMetadata(ComReferenceItemMetadataNames.versionMajor), NumberStyles.Integer, CultureInfo.InvariantCulture); + + // invalid versionMinor format + missingOrInvalidMetadata = ComReferenceItemMetadataNames.versionMinor; + short.Parse(reference.GetMetadata(ComReferenceItemMetadataNames.versionMinor), NumberStyles.Integer, CultureInfo.InvariantCulture); + + // only check lcid if specified + if (reference.GetMetadata(ComReferenceItemMetadataNames.lcid).Length > 0) + { + // invalid lcid format + missingOrInvalidMetadata = ComReferenceItemMetadataNames.lcid; + int.Parse(reference.GetMetadata(ComReferenceItemMetadataNames.lcid), NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + // only check wrapperTool if specified + if (reference.GetMetadata(ComReferenceItemMetadataNames.wrapperTool).Length > 0) + { + // invalid wrapperTool type + missingOrInvalidMetadata = ComReferenceItemMetadataNames.wrapperTool; + string wrapperTool = reference.GetMetadata(ComReferenceItemMetadataNames.wrapperTool); + + if ((!ComReferenceTypes.IsAxImp(wrapperTool)) && + (!ComReferenceTypes.IsTlbImp(wrapperTool)) && + (!ComReferenceTypes.IsPia(wrapperTool))) + { + return false; + } + } + } + catch (OverflowException) + { + return false; + } + catch (FormatException) + { + return false; + } + + // all metadata were found + missingOrInvalidMetadata = String.Empty; + return true; + } + + /* + * Method: InitializeDefaultMetadataForNameItem + * + * Initializes optional metadata on given name item to their default values if they're not present + */ + internal static void InitializeDefaultMetadataForNameItem(ITaskItem reference) + { + // default value for lcid is 0 + if (reference.GetMetadata(ComReferenceItemMetadataNames.lcid).Length == 0) + reference.SetMetadata(ComReferenceItemMetadataNames.lcid, "0"); + + // default value for wrapperTool is tlbimp + if (reference.GetMetadata(ComReferenceItemMetadataNames.wrapperTool).Length == 0) + reference.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.tlbimp); + } + + /* + * Method: InitializeDefaultMetadataForFileItem + * + * Initializes optional metadata on given file item to their default values if they're not present + */ + internal static void InitializeDefaultMetadataForFileItem(ITaskItem reference) + { + // default value for wrapperTool is tlbimp + if (reference.GetMetadata(ComReferenceItemMetadataNames.wrapperTool).Length == 0) + reference.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.tlbimp); + } + + /* + * Method: CheckForConflictingReferences + * + * Checks if we have any conflicting references. + */ + internal bool CheckForConflictingReferences() + { + Hashtable namesForReferences = new Hashtable(); + ArrayList refsToBeRemoved = new ArrayList(); + bool noConflictsFound = true; + + for (int pass = 0; pass < 2; pass++) + { + foreach (ComReferenceInfo projectRefInfo in allProjectRefs) + { + string wrapperType = projectRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool); + + // only check aximp and tlbimp references + if ((pass == 0 && ComReferenceTypes.IsAxImp(wrapperType)) || + (pass == 1 && ComReferenceTypes.IsTlbImp(wrapperType))) + { + // if we already have a reference with this name, compare attributes + if (namesForReferences.ContainsKey(projectRefInfo.typeLibName)) + { + // if different type lib attributes, we have a conflict, remove the conflicting reference + // and continue processing + ComReferenceInfo conflictingRef = (ComReferenceInfo)namesForReferences[projectRefInfo.typeLibName]; + + if (!ComReference.AreTypeLibAttrEqual(projectRefInfo.attr, conflictingRef.attr)) + { + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.ConflictingReferences", projectRefInfo.taskItem.ItemSpec, conflictingRef.taskItem.ItemSpec); + } + + // mark the reference for removal, can't do it here because we're iterating through the ref's container + refsToBeRemoved.Add(projectRefInfo); + noConflictsFound = false; + } + } + else + { + // store the current reference + namesForReferences.Add(projectRefInfo.typeLibName, projectRefInfo); + } + } + } + + // use a new hashtable for different passes - refs to the same typelib with different wrapper types are OK + namesForReferences.Clear(); + } + + // now that we're outside the loop, we can safely remove the marked references + foreach (ComReferenceInfo projectRefInfo in refsToBeRemoved) + { + // remove and cleanup + allProjectRefs.Remove(projectRefInfo); + projectRefInfo.ReleaseTypeLibPtr(); + } + + return noConflictsFound; + } + + /// + /// Set the CopyLocal metadata to false on all assemblies that are located in the GAC. + /// + /// ArrayList of ITaskItems that will be outputted from the task + /// The GAC root path + internal void SetCopyLocalToFalseOnGacOrNoPIAAssemblies(ArrayList outputTaskItems, string gacPath) + { + foreach (ITaskItem taskItem in outputTaskItems) + { + if (taskItem.GetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget).Length == 0) + { + taskItem.SetMetadata(ItemMetadataNames.msbuildReferenceSourceTarget, "ResolveComReference"); + } + + string embedInteropTypesMetadata = taskItem.GetMetadata(ItemMetadataNames.embedInteropTypes); + + if (_projectTargetFramework != null && (_projectTargetFramework >= s_targetFrameworkVersion_40)) + { + if ((embedInteropTypesMetadata != null) && + (String.Compare(embedInteropTypesMetadata, "true", StringComparison.OrdinalIgnoreCase) == 0)) + { + // Embed Interop Types forces CopyLocal to false + taskItem.SetMetadata(ItemMetadataNames.copyLocal, "false"); + continue; + } + } + + string privateMetadata = taskItem.GetMetadata(ItemMetadataNames.privateMetadata); + + // if Private is not set on the original item, we set CopyLocal to false for GAC items + // and true for non-GAC items + if ((privateMetadata == null) || (privateMetadata.Length == 0)) + { + if (String.Compare(taskItem.ItemSpec, 0, gacPath, 0, gacPath.Length, StringComparison.OrdinalIgnoreCase) == 0) + { + taskItem.SetMetadata(ItemMetadataNames.copyLocal, "false"); + } + else + { + taskItem.SetMetadata(ItemMetadataNames.copyLocal, "true"); + } + } + // if Private is set, it always takes precedence + else + { + taskItem.SetMetadata(ItemMetadataNames.copyLocal, privateMetadata); + } + } + } + + /// + /// Scan all the dependencies of the main project references and preresolve them + /// so that when we get asked about a previously unknown dependency in the form of a .NET assembly + /// we know what to do with it. + /// + private List ScanAndResolveAllDependencies(ComDependencyWalker dependencyWalker, ComReferenceInfo reference) + { + dependencyWalker.ClearDependencyList(); + + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ScanningDependencies", reference.SourceItemSpec); + } + + dependencyWalker.AnalyzeTypeLibrary(reference.typeLibPointer); + + if (!Silent) + { + foreach (Exception ex in dependencyWalker.EncounteredProblems) + { + // A failure to resolve a reference due to something possibly being missing from disk is not + // an error; the user may not be actually consuming types from it + Log.LogWarningWithCodeFromResources("ResolveComReference.FailedToScanDependencies", + reference.SourceItemSpec, ex.Message); + } + } + + dependencyWalker.EncounteredProblems.Clear(); + + HashSet dependentPaths = new HashSet(); + TYPELIBATTR[] dependentAttrs = dependencyWalker.GetDependencies(); + + foreach (TYPELIBATTR dependencyTypeLibAttr in dependentAttrs) + { + // We don't need to even try to resolve if the dependency reference is ourselves. + if (!ComReference.AreTypeLibAttrEqual(dependencyTypeLibAttr, reference.attr)) + { + ComReferenceInfo existingReference; + + if (IsExistingProjectReference(dependencyTypeLibAttr, null, out existingReference)) + { + ITaskItem resolvedItem; + + // If we're resolving another project reference, empty out the type cache -- if the dependencies are buried, + // caching the analyzed types can make it so that we don't recognize our dependencies' dependencies. + dependencyWalker.ClearAnalyzedTypeCache(); + + if (ResolveReference(dependencyWalker, existingReference, WrapperOutputDirectory, out resolvedItem)) + { + // Add the resolved dependency + dependentPaths.Add(resolvedItem.ItemSpec); + + // and anything it depends on + foreach (string dependentPath in existingReference.dependentWrapperPaths) + { + dependentPaths.Add(dependentPath); + } + } + } + else + { + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ResolvingDependency", + dependencyTypeLibAttr.guid, dependencyTypeLibAttr.wMajorVerNum, dependencyTypeLibAttr.wMinorVerNum); + } + + ComReferenceWrapperInfo wrapperInfo; + + ((IComReferenceResolver)this).ResolveComClassicReference(dependencyTypeLibAttr, WrapperOutputDirectory, + null /* unknown wrapper type */, null /* unknown name */, out wrapperInfo); + + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ResolvedDependentComReference", + dependencyTypeLibAttr.guid, dependencyTypeLibAttr.wMajorVerNum, dependencyTypeLibAttr.wMinorVerNum, + wrapperInfo.path); + } + + dependentPaths.Add(wrapperInfo.path); + } + } + } + + return dependentPaths.ToList(); + } + + /* + * Method: TaskItemToTypeLibAttr + * + * Gets the TLIBATTR structure based on the reference we have. + * Sets guid, versions major & minor, lcid. + */ + internal static TYPELIBATTR TaskItemToTypeLibAttr(ITaskItem taskItem) + { + TYPELIBATTR attr = new TYPELIBATTR(); + + // copy metadata from Reference to our TYPELIBATTR + attr.guid = new Guid(taskItem.GetMetadata(ComReferenceItemMetadataNames.guid)); + attr.wMajorVerNum = short.Parse(taskItem.GetMetadata(ComReferenceItemMetadataNames.versionMajor), NumberStyles.Integer, CultureInfo.InvariantCulture); + attr.wMinorVerNum = short.Parse(taskItem.GetMetadata(ComReferenceItemMetadataNames.versionMinor), NumberStyles.Integer, CultureInfo.InvariantCulture); + attr.lcid = int.Parse(taskItem.GetMetadata(ComReferenceItemMetadataNames.lcid), NumberStyles.Integer, CultureInfo.InvariantCulture); + return attr; + } + + #endregion + } +} diff --git a/src/XMakeTasks/ResolveComReferenceCache.cs b/src/XMakeTasks/ResolveComReferenceCache.cs new file mode 100644 index 00000000000..38e2cfa7cca --- /dev/null +++ b/src/XMakeTasks/ResolveComReferenceCache.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Stores timestamps of COM components processed in the last run. The problem here is that installing/uninstalling + /// COM components does not update their timestamps with the current time (for a good reason). So if you revert to + /// an earlier revision of a COM component, its timestamp can go back in time and we still need to regenerate its + /// wrapper. So in ResolveComReference we compare the stored timestamp with the current component timestamp, and if + /// they are different, we regenerate the wrapper. + /// + [Serializable()] + internal sealed class ResolveComReferenceCache : StateFileBase + { + /// + /// Component timestamps. + /// Key: Component path on disk + /// Value: DateTime struct + /// + private Hashtable _componentTimestamps = null; + private string _tlbImpLocation = null; + private string _axImpLocation = null; + + /// + /// indicates whether the cache contents have changed since it's been created + /// + internal bool Dirty + { + get { return _dirty; } + } + + + [NonSerialized] + private bool _dirty; + + /// + /// Construct. + /// + internal ResolveComReferenceCache(string tlbImpPath, string axImpPath) + { + ErrorUtilities.VerifyThrowArgumentNull(tlbImpPath, "tlbImpPath"); + ErrorUtilities.VerifyThrowArgumentNull(axImpPath, "axImpPath"); + + _tlbImpLocation = tlbImpPath; + _axImpLocation = axImpPath; + _componentTimestamps = new Hashtable(); + } + + /// + /// Compares the tlbimp and aximp paths to what the paths were when the cache was created + /// If these are different return false. + /// + /// True if both paths match what is in the cache, false otherwise + internal bool ToolPathsMatchCachePaths(string tlbImpPath, string axImpPath) + { + return (String.Equals(_tlbImpLocation, tlbImpPath, StringComparison.OrdinalIgnoreCase)) && (String.Equals(_axImpLocation, axImpPath, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Gets or sets the timestamp associated with the specified component file + /// + /// + /// + internal DateTime this[string componentPath] + { + get + { + if (_componentTimestamps.ContainsKey(componentPath)) + { + return (DateTime)_componentTimestamps[componentPath]; + } + else + { + // If the entry is not present in the cache, return the current time. Since no component should be timestamped + // with the current time, this will effectively always regenerate the wrapper. + return DateTime.Now; + } + } + set + { + // only set the value and dirty the cache if the timestamp doesn't exist yet or is different than the current one + if (DateTime.Compare(this[componentPath], value) != 0) + { + _componentTimestamps[componentPath] = value; + _dirty = true; + } + } + } + } +} diff --git a/src/XMakeTasks/ResolveKeySource.cs b/src/XMakeTasks/ResolveKeySource.cs new file mode 100644 index 00000000000..6b97f01baf3 --- /dev/null +++ b/src/XMakeTasks/ResolveKeySource.cs @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Resources; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.Runtime.Hosting; + +namespace Microsoft.Build.Tasks +{ + /// + /// Determine the strong name key source + /// + public class ResolveKeySource : TaskExtension + { + private string _keyFile; + private string _certificateThumbprint; + private string _certificateFile; + private string _resolvedKeyContainer = String.Empty; + private string _resolvedKeyFile = String.Empty; + private string _resolvedThumbprint = String.Empty; + private const string pfxFileExtension = ".pfx"; + private const string pfxFileContainerPrefix = "VS_KEY_"; + private bool _suppressAutoClosePasswordPrompt = false; + private bool _showImportDialogDespitePreviousFailures = false; + private int _autoClosePasswordPromptTimeout = 20; + private int _autoClosePasswordPromptShow = 15; + static private Hashtable s_pfxKeysToIgnore = new Hashtable(StringComparer.OrdinalIgnoreCase); + + + #region Properties + + public string KeyFile + { + get { return _keyFile; } + set { _keyFile = value; } + } + + public string CertificateThumbprint + { + get { return _certificateThumbprint; } + set { _certificateThumbprint = value; } + } + + public string CertificateFile + { + get { return _certificateFile; } + set { _certificateFile = value; } + } + + public bool SuppressAutoClosePasswordPrompt + { + get { return _suppressAutoClosePasswordPrompt; } + set { _suppressAutoClosePasswordPrompt = value; } + } + + public bool ShowImportDialogDespitePreviousFailures + { + get { return _showImportDialogDespitePreviousFailures; } + set { _showImportDialogDespitePreviousFailures = value; } + } + + public int AutoClosePasswordPromptTimeout + { + get { return _autoClosePasswordPromptTimeout; } + set { _autoClosePasswordPromptTimeout = value; } + } + + public int AutoClosePasswordPromptShow + { + get { return _autoClosePasswordPromptShow; } + set { _autoClosePasswordPromptShow = value; } + } + + [Output] + public string ResolvedThumbprint + { + get { return _resolvedThumbprint; } + set { _resolvedThumbprint = value; } + } + + [Output] + public string ResolvedKeyContainer + { + get { return _resolvedKeyContainer; } + set { _resolvedKeyContainer = value; } + } + + [Output] + public string ResolvedKeyFile + { + get { return _resolvedKeyFile; } + set { _resolvedKeyFile = value; } + } + + #endregion + + + #region ITask Members + + public override bool Execute() + { + return ResolveAssemblyKey() && ResolveManifestKey(); + } + + // We we use hash the contens of .pfx file so we can establish relationship file <-> container name, whithout + // need to prompt for password. Note this is not used for any security reasons. With the departure from standard MD5 algoritm + // we need as simple hash function for replacement. The data blobs we use (.pfx files) are + // encrypted meaning they have high entropy, so in all practical pupose even a simpliest + // hash would give good enough results. This code needs to be kept in sync with the code in compsvcpkgs + // to prevent double prompt for newly created keys. The magic numbers here are just random primes + // in the range 10m/20m. + static private UInt64 HashFromBlob(byte[] data) + { + UInt32 dw1 = 17339221; + UInt32 dw2 = 19619429; + UInt32 pos = 10803503; + + foreach (byte b in data) + { + UInt32 value = b ^ pos; + pos *= 10803503; + dw1 += ((value ^ dw2) * 15816943) + 17368321; + dw2 ^= ((value + dw1) * 14984549) ^ 11746499; + } + UInt64 result = dw1; + result <<= 32; + result |= dw2; + return result; + } + + + private bool ResolveAssemblyKey() + { + bool pfxSuccess = true; + if (KeyFile != null && KeyFile.Length > 0) + { + string keyFileExtension = String.Empty; + try + { + keyFileExtension = Path.GetExtension(KeyFile); + } + catch (ArgumentException ex) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.InvalidKeyName", KeyFile, ex.Message); + pfxSuccess = false; + } + if (pfxSuccess) + { + if (0 != String.Compare(keyFileExtension, pfxFileExtension, StringComparison.OrdinalIgnoreCase)) + { + ResolvedKeyFile = KeyFile; + } + else + { + pfxSuccess = false; + // it is .pfx file. It is being imported into key container with name = "VS_KEY_" + System.IO.FileStream fs = null; + try + { + string hashedContainerName = String.Empty; + string currentUserName = Environment.UserDomainName + "\\" + Environment.UserName; + // we use the curent user name to randomize the associated container name, i.e different user on the same machine will export to different keys + // this is because SNAPI by default will create keys in "per-machine" crypto store (visible for all the user) but will set the permission such only + // creator will be able to use it. This will make imposible for other user both to sign or export the key again (since they also can not delete that key). + // Now different users will use different container name. We use ToLower(invariant) because this is what the native equivalent of this function (Create new key, or VC++ import-er). + // use as well and we want to keep the hash (and key container name the same) otherwise user could be prompt for a password twice. + byte[] userNameBytes = System.Text.Encoding.Unicode.GetBytes(currentUserName.ToLower(CultureInfo.InvariantCulture)); + fs = System.IO.File.OpenRead(KeyFile); + int fileLength = (int)fs.Length; + byte[] keyBytes = new byte[fileLength]; + fs.Read(keyBytes, 0, fileLength); + + UInt64 hash = HashFromBlob(keyBytes); + hash ^= HashFromBlob(userNameBytes); // modify it with the username hash, so each user would get different hash for the same key + + hashedContainerName = pfxFileContainerPrefix + hash.ToString("X016", CultureInfo.InvariantCulture); + + IntPtr publicKeyBlob = IntPtr.Zero; + int publicKeyBlobSize = 0; + if (StrongNameHelpers.StrongNameGetPublicKey(hashedContainerName, IntPtr.Zero, 0, out publicKeyBlob, out publicKeyBlobSize) && publicKeyBlob != IntPtr.Zero) + { + StrongNameHelpers.StrongNameFreeBuffer(publicKeyBlob); + pfxSuccess = true; + } + else + { + if (ShowImportDialogDespitePreviousFailures || !s_pfxKeysToIgnore.Contains(hashedContainerName)) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyFileForSignAssemblyNotImported", KeyFile, hashedContainerName); + } + + if (!pfxSuccess) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyImportError", KeyFile); + } + } + if (pfxSuccess) + { + ResolvedKeyContainer = hashedContainerName; + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyMD5SumError", KeyFile, e.Message); + } + finally + { + if (fs != null) + { + fs.Close(); + } + } + } + } + } + + return pfxSuccess; + } + + private bool ResolveManifestKey() + { + bool certSuccess = false; + bool certInStore = false; + if (!string.IsNullOrEmpty(CertificateThumbprint)) + { + // look for cert in the cert store + X509Store personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + personalStore.Open(OpenFlags.ReadWrite); + X509Certificate2Collection foundCerts = personalStore.Certificates.Find(X509FindType.FindByThumbprint, CertificateThumbprint, false); + if (foundCerts.Count == 1) + { + certInStore = true; + ResolvedThumbprint = CertificateThumbprint; + certSuccess = true; + } + } + finally + { + personalStore.Close(); + } + if (!certSuccess) + { + Log.LogWarningWithCodeFromResources("ResolveKeySource.ResolvedThumbprintEmpty"); + } + } + + if (!string.IsNullOrEmpty(CertificateFile) && !certInStore) + { + // if the cert isn't on disk, we can't import it + if (!File.Exists(CertificateFile)) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.CertificateNotInStore"); + } + else + { + // add the cert to the store optionally prompting for the password + if (X509Certificate2.GetCertContentType(CertificateFile) == X509ContentType.Pfx) + { + bool imported = false; + // first try it with no password + X509Certificate2 cert = new X509Certificate2(); + X509Store personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + personalStore.Open(OpenFlags.ReadWrite); + cert.Import(CertificateFile, (string)null, X509KeyStorageFlags.PersistKeySet); + personalStore.Add(cert); + ResolvedThumbprint = cert.Thumbprint; + imported = true; + certSuccess = true; + } + catch (CryptographicException) + { + // cert has a password, move on and prompt for it + } + finally + { + personalStore.Close(); + } + if (!imported && ShowImportDialogDespitePreviousFailures) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyFileForManifestNotImported", KeyFile); + } + if (!certSuccess) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyImportError", CertificateFile); + } + } + else + { + X509Store personalStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); + try + { + X509Certificate2 cert = new X509Certificate2(CertificateFile); + personalStore.Open(OpenFlags.ReadWrite); + personalStore.Add(cert); + ResolvedThumbprint = cert.Thumbprint; + certSuccess = true; + } + catch (CryptographicException) + { + Log.LogErrorWithCodeFromResources("ResolveKeySource.KeyImportError", CertificateFile); + } + finally + { + personalStore.Close(); + } + } + } + } + else if (!certInStore && !string.IsNullOrEmpty(CertificateFile) && !string.IsNullOrEmpty(CertificateThumbprint)) + { + // no file and not in store, error out + Log.LogErrorWithCodeFromResources("ResolveKeySource.CertificateNotInStore"); + certSuccess = false; + } + else + certSuccess = true; + + return certSuccess; + } + + #endregion + } +} diff --git a/src/XMakeTasks/ResolveManifestFiles.cs b/src/XMakeTasks/ResolveManifestFiles.cs new file mode 100644 index 00000000000..8e6cdb1c35f --- /dev/null +++ b/src/XMakeTasks/ResolveManifestFiles.cs @@ -0,0 +1,924 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This task resolves items in the build process (built, dependencies, satellites, + /// content, debug symbols, documentation, etc.) to files for manifest generation. + /// + /// + /// This task executes following steps: + /// (1) Filter out Framework assemblies + /// (2) Filter out non-existant files + /// (3) Build list of Dependencies from built items with CopyLocal=True + /// (4) Build list of Prerequisites from built items with CopyLocal=False + /// (5) Build list of Satellites from built items based on TargetCulture + /// (6) Build list of Files from Files and ExtraFiles inputs, using next step + /// (7) For each PublishFile item... + /// If item is on Dependencies list then move it to Prerequisites list + /// If item is on Content list then add it to File list unless it is excluded + /// If item is on Extra list then add it to File list only if it is included + /// Apply Group and Optional attributes from PublishFile items to built items + /// (8) Insure all output items have a TargetPath, and if in a Group that IsOptional is set + /// + public sealed class ResolveManifestFiles : TaskExtension + { + #region Fields + private ITaskItem _deploymentManifestEntryPoint = null; + private ITaskItem _entryPoint; + private ITaskItem[] _extraFiles; + private ITaskItem[] _files; + private ITaskItem[] _managedAssemblies; + private ITaskItem[] _nativeAssemblies; + private ITaskItem[] _outputAssemblies; + private ITaskItem _outputDeploymentManifestEntryPoint = null; + private ITaskItem _outputEntryPoint = null; + private ITaskItem[] _outputFiles; + private ITaskItem[] _publishFiles; + private ITaskItem[] _satelliteAssemblies; + private string _specifiedTargetCulture; + private CultureInfo _targetCulture = null; + private bool _includeAllSatellites = false; + private bool _signingManifests = false; + private string _targetFrameworkVersion; + // if signing manifests is on and not all app files are included, then the project can't be published. + private bool _canPublish; + #endregion + + #region Properties + + public ITaskItem DeploymentManifestEntryPoint + { + get { return _deploymentManifestEntryPoint; } + set { _deploymentManifestEntryPoint = value; } + } + + public ITaskItem EntryPoint + { + get { return _entryPoint; } + set { _entryPoint = value; } + } + + public ITaskItem[] ExtraFiles + { + get { return _extraFiles; } + set { _extraFiles = Util.SortItems(value); } + } + + public ITaskItem[] Files + { + get { return _files; } + set { _files = Util.SortItems(value); } + } + + public ITaskItem[] ManagedAssemblies + { + get { return _managedAssemblies; } + set { _managedAssemblies = Util.SortItems(value); } + } + + public ITaskItem[] NativeAssemblies + { + get { return _nativeAssemblies; } + set { _nativeAssemblies = Util.SortItems(value); } + } + + [Output] + public ITaskItem[] OutputAssemblies + { + get { return _outputAssemblies; } + set { _outputAssemblies = value; } + } + + [Output] + public ITaskItem OutputDeploymentManifestEntryPoint + { + get { return _outputDeploymentManifestEntryPoint; } + set { _outputDeploymentManifestEntryPoint = value; } + } + + [Output] + public ITaskItem OutputEntryPoint + { + get { return _outputEntryPoint; } + set { OutputEntryPoint = value; } + } + + [Output] + public ITaskItem[] OutputFiles + { + get { return _outputFiles; } + set { _outputFiles = value; } + } + + public ITaskItem[] PublishFiles + { + get { return _publishFiles; } + set { _publishFiles = Util.SortItems(value); } + } + + public ITaskItem[] SatelliteAssemblies + { + get { return _satelliteAssemblies; } + set { _satelliteAssemblies = Util.SortItems(value); } + } + + public string TargetCulture + { + get { return _specifiedTargetCulture; } + set { _specifiedTargetCulture = value; } + } + + public bool SigningManifests + { + get { return _signingManifests; } + set { _signingManifests = value; } + } + + public string TargetFrameworkVersion + { + get + { + if (string.IsNullOrEmpty(_targetFrameworkVersion)) + return Constants.TargetFrameworkVersion35; + return _targetFrameworkVersion; + } + set { _targetFrameworkVersion = value; } + } + + #endregion + + public ResolveManifestFiles() + { + } + + public override bool Execute() + { + if (!ValidateInputs()) + return false; + + // if signing manifests is on and not all app files are included, then the project can't be published. + _canPublish = true; + bool is35Project = (CompareFrameworkVersions(TargetFrameworkVersion, Constants.TargetFrameworkVersion35) >= 0); + + PublishInfo[] assemblyPublishInfoList; + PublishInfo[] filePublishInfoList; + PublishInfo[] satellitePublishInfoList; + PublishInfo[] manifestEntryPointList; + GetPublishInfo(out assemblyPublishInfoList, out filePublishInfoList, out satellitePublishInfoList, out manifestEntryPointList); + + _outputAssemblies = GetOutputAssembliesAndSatellites(assemblyPublishInfoList, satellitePublishInfoList); + + if (!_canPublish && is35Project) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ManifestsSignedHashExcluded"); + return false; + } + + _outputFiles = GetOutputFiles(filePublishInfoList); + + if (!_canPublish && is35Project) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ManifestsSignedHashExcluded"); + return false; + } + + _outputEntryPoint = GetOutputEntryPoint(_entryPoint, manifestEntryPointList); + + if (!_canPublish && is35Project) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ManifestsSignedHashExcluded"); + return false; + } + + _outputDeploymentManifestEntryPoint = GetOutputEntryPoint(_deploymentManifestEntryPoint, manifestEntryPointList); + + if (!_canPublish && is35Project) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ManifestsSignedHashExcluded"); + return false; + } + + return true; + } + + private Version ConvertFrameworkVersionToString(string version) + { + if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase)) + { + return new Version(version.Substring(1)); + } + return new Version(version); + } + + private int CompareFrameworkVersions(string versionA, string versionB) + { + Version version1 = ConvertFrameworkVersionToString(versionA); + Version version2 = ConvertFrameworkVersionToString(versionB); + return version1.CompareTo(version2); + } + + private bool ValidateInputs() + { + if (!String.IsNullOrEmpty(_specifiedTargetCulture)) + { + if (String.Equals(_specifiedTargetCulture, "*", StringComparison.Ordinal)) + { + _includeAllSatellites = true; + } + else if (!String.Equals(_specifiedTargetCulture, "neutral", StringComparison.Ordinal)) + { + try + { + _targetCulture = new CultureInfo(_specifiedTargetCulture); + } + catch (ArgumentException) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", "TargetCulture", "ResolveManifestFiles"); + return false; + } + } + } + return true; + } + + + #region Helpers + + // Creates an output item for a an assembly, with optional Group attribute. + private static ITaskItem CreateAssemblyItem(ITaskItem item, string group, string targetPath, string includeHash) + { + ITaskItem outputItem = new TaskItem(item.ItemSpec); + item.CopyMetadataTo(outputItem); + outputItem.SetMetadata("DependencyType", "Install"); + if (String.IsNullOrEmpty(targetPath)) + targetPath = GetItemTargetPath(outputItem); + outputItem.SetMetadata(ItemMetadataNames.targetPath, targetPath); + if (!String.IsNullOrEmpty(group)) + outputItem.SetMetadata("Group", group); + if (!String.IsNullOrEmpty(includeHash)) + outputItem.SetMetadata("IncludeHash", includeHash); + return outputItem; + } + + // Creates an output item for a file, with optional Group and IsData attributes. + private static ITaskItem CreateFileItem(ITaskItem item, string group, string targetPath, string includeHash, bool isDataFile) + { + ITaskItem outputItem = new TaskItem(item.ItemSpec); + item.CopyMetadataTo(outputItem); + if (String.IsNullOrEmpty(targetPath)) + targetPath = GetItemTargetPath(outputItem); + outputItem.SetMetadata(ItemMetadataNames.targetPath, targetPath); + if (!String.IsNullOrEmpty(group) && !isDataFile) + outputItem.SetMetadata("Group", group); + if (!String.IsNullOrEmpty(includeHash)) + outputItem.SetMetadata("IncludeHash", includeHash); + + outputItem.SetMetadata("IsDataFile", isDataFile.ToString().ToLowerInvariant()); + return outputItem; + } + + // Creates an output item for a prerequisite. + private static ITaskItem CreatePrerequisiteItem(ITaskItem item) + { + ITaskItem outputItem = new TaskItem(item.ItemSpec); + item.CopyMetadataTo(outputItem); + outputItem.SetMetadata("DependencyType", "Prerequisite"); + return outputItem; + } + + private static bool GetItemCopyLocal(ITaskItem item) + { + string copyLocal = item.GetMetadata(ItemMetadataNames.copyLocal); + if (!String.IsNullOrEmpty(copyLocal)) + return ConvertUtil.ToBoolean(copyLocal); + else + return true; // always return true if item does not have a CopyLocal attribute + } + + // Returns the culture for the specified item, first by looking for an attribute and if not found + // attempts to infer from the disk path. + private static CultureInfo GetItemCulture(ITaskItem item) + { + string itemCulture = item.GetMetadata("Culture"); + if (String.IsNullOrEmpty(itemCulture)) + { + // Infer culture from path (i.e. "obj\debug\fr\WindowsApplication1.resources.dll" -> "fr") + string[] pathSegments = PathUtil.GetPathSegments(item.ItemSpec); + itemCulture = pathSegments.Length > 1 ? pathSegments[pathSegments.Length - 2] : null; + Debug.Assert(!String.IsNullOrEmpty(itemCulture), String.Format(CultureInfo.CurrentCulture, "Satellite item '{0}' is missing expected attribute '{1}'", item.ItemSpec, "Culture")); + item.SetMetadata("Culture", itemCulture); + } + return new CultureInfo(itemCulture); + } + + private static string GetItemTargetPath(ITaskItem item) + { + string targetPath = item.GetMetadata(ItemMetadataNames.targetPath); + if (String.IsNullOrEmpty(targetPath)) + { + targetPath = Path.GetFileName(item.ItemSpec); + // If item is a satellite then make sure the culture is part of the path... + string assemblyType = item.GetMetadata("AssemblyType"); + if (String.Equals(assemblyType, "Satellite", StringComparison.Ordinal)) + { + CultureInfo itemCulture = GetItemCulture(item); + if (itemCulture != null) + targetPath = Path.Combine(itemCulture.ToString(), targetPath); + } + } + return targetPath; + } + + private void GetOutputAssemblies(PublishInfo[] publishInfos, ref List assemblyList) + { + AssemblyMap assemblyMap = new AssemblyMap(); + + // Add all managed assemblies to the AssemblyMap, except assemblies that are part of the .NET Framework... + if (_managedAssemblies != null) + foreach (ITaskItem item in _managedAssemblies) + if (!IsFiltered(item)) + { + item.SetMetadata("AssemblyType", "Managed"); + assemblyMap.Add(item); + } + + if (_nativeAssemblies != null) + foreach (ITaskItem item in _nativeAssemblies) + if (!IsFiltered(item)) + { + item.SetMetadata("AssemblyType", "Native"); + assemblyMap.Add(item); + } + + // Apply PublishInfo state from PublishFile items... + foreach (PublishInfo publishInfo in publishInfos) + { + MapEntry entry = assemblyMap[publishInfo.key]; + if (entry != null) + entry.publishInfo = publishInfo; + else + Log.LogWarningWithCodeFromResources("ResolveManifestFiles.PublishFileNotFound", publishInfo.key); + } + + // Go through the AssemblyMap and determine which items get added to ouput AssemblyList based + // on computed PublishFlags for each item... + foreach (MapEntry entry in assemblyMap) + { + // If PublishInfo didn't come from a PublishFile item, then construct PublishInfo from the item + if (entry.publishInfo == null) + entry.publishInfo = new PublishInfo(); + + // If state is auto then also need to look on the item to see whether the dependency type + // has alread been specified upstream (i.e. from ResolveNativeReference task)... + if (entry.publishInfo.state == PublishState.Auto) + { + string dependencyType = entry.item.GetMetadata("DependencyType"); + if (String.Equals(dependencyType, "Prerequisite", StringComparison.Ordinal)) + entry.publishInfo.state = PublishState.Prerequisite; + else if (String.Equals(dependencyType, "Install", StringComparison.Ordinal)) + entry.publishInfo.state = PublishState.Include; + } + + bool copyLocal = GetItemCopyLocal(entry.item); + PublishFlags flags = PublishFlags.GetAssemblyFlags(entry.publishInfo.state, copyLocal); + + if (flags.IsPublished && + string.Equals(entry.publishInfo.includeHash, "false", StringComparison.OrdinalIgnoreCase) && + SigningManifests == true) + _canPublish = false; + + if (flags.IsPublished) + assemblyList.Add(CreateAssemblyItem(entry.item, entry.publishInfo.group, entry.publishInfo.targetPath, entry.publishInfo.includeHash)); + else if (flags.IsPrerequisite) + assemblyList.Add(CreatePrerequisiteItem(entry.item)); + } + } + + private ITaskItem[] GetOutputAssembliesAndSatellites(PublishInfo[] assemblyPublishInfos, PublishInfo[] satellitePublishInfos) + { + List assemblyList = new List(); + GetOutputAssemblies(assemblyPublishInfos, ref assemblyList); + GetOutputSatellites(satellitePublishInfos, ref assemblyList); + return assemblyList.ToArray(); + } + + private ITaskItem[] GetOutputFiles(PublishInfo[] publishInfos) + { + List fileList = new List(); + FileMap fileMap = new FileMap(); + + // Add all input Files to the FileMap, flagging them to be published by default... + if (Files != null) + foreach (ITaskItem item in Files) + fileMap.Add(item, true); + + // Add all input ExtraFiles to the FileMap, flagging them to NOT be published by default... + if (ExtraFiles != null) + foreach (ITaskItem item in ExtraFiles) + fileMap.Add(item, false); + + // Apply PublishInfo state from PublishFile items... + foreach (PublishInfo publishInfo in publishInfos) + { + MapEntry entry = fileMap[publishInfo.key]; + if (entry != null) + entry.publishInfo = publishInfo; + else + Log.LogWarningWithCodeFromResources("ResolveManifestFiles.PublishFileNotFound", publishInfo.key); + } + + // Go through the FileMap and determine which items get added to ouput FileList based + // on computed PublishFlags for each item... + foreach (MapEntry entry in fileMap) + { + // If PublishInfo didn't come from a PublishFile item, then construct PublishInfo from the item + if (entry.publishInfo == null) + entry.publishInfo = new PublishInfo(); + + string fileExtension = Path.GetExtension(entry.item.ItemSpec); + PublishFlags flags = PublishFlags.GetFileFlags(entry.publishInfo.state, fileExtension, entry.includedByDefault); + + if (flags.IsPublished && + string.Equals(entry.publishInfo.includeHash, "false", StringComparison.OrdinalIgnoreCase) && + SigningManifests == true) + _canPublish = false; + + if (flags.IsPublished) + fileList.Add(CreateFileItem(entry.item, entry.publishInfo.group, entry.publishInfo.targetPath, entry.publishInfo.includeHash, flags.IsDataFile)); + } + + return fileList.ToArray(); + } + + private void GetOutputSatellites(PublishInfo[] publishInfos, ref List assemblyList) + { + FileMap satelliteMap = new FileMap(); + + if (_satelliteAssemblies != null) + foreach (ITaskItem item in _satelliteAssemblies) + { + item.SetMetadata("AssemblyType", "Satellite"); + satelliteMap.Add(item, true); + } + + // Apply PublishInfo state from PublishFile items... + foreach (PublishInfo publishInfo in publishInfos) + { + string key = publishInfo.key + ".dll"; + MapEntry entry = satelliteMap[key]; + if (entry != null) + entry.publishInfo = publishInfo; + else + Log.LogWarningWithCodeFromResources("ResolveManifestFiles.PublishFileNotFound", publishInfo.key); + } + + // Go through the AssemblyMap and determine which items get added to ouput SatelliteList based + // on computed PublishFlags for each item... + foreach (MapEntry entry in satelliteMap) + { + // If PublishInfo didn't come from a PublishFile item, then construct PublishInfo from the item + if (entry.publishInfo == null) + { + entry.publishInfo = new PublishInfo(); + } + + CultureInfo satelliteCulture = GetItemCulture(entry.item); + PublishFlags flags = PublishFlags.GetSatelliteFlags(entry.publishInfo.state, satelliteCulture, _targetCulture, _includeAllSatellites); + + if (flags.IsPublished && + string.Equals(entry.publishInfo.includeHash, "false", StringComparison.OrdinalIgnoreCase) && + SigningManifests == true) + _canPublish = false; + + if (flags.IsPublished) + { + assemblyList.Add(CreateAssemblyItem(entry.item, entry.publishInfo.group, entry.publishInfo.targetPath, entry.publishInfo.includeHash)); + } + else if (flags.IsPrerequisite) + { + assemblyList.Add(CreatePrerequisiteItem(entry.item)); + } + } + } + + private ITaskItem GetOutputEntryPoint(ITaskItem entryPoint, PublishInfo[] manifestEntryPointList) + { + if (entryPoint == null) + { + return null; + } + TaskItem outputEntryPoint = new TaskItem(entryPoint.ItemSpec); + entryPoint.CopyMetadataTo(outputEntryPoint); + string targetPath = entryPoint.GetMetadata("TargetPath"); + if (!string.IsNullOrEmpty(targetPath)) + { + for (int i = 0; i < manifestEntryPointList.Length; i++) + { + if (String.Equals(targetPath, manifestEntryPointList[i].key, StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(manifestEntryPointList[i].includeHash)) + { + if (manifestEntryPointList[i].state != PublishState.Exclude && + string.Equals(manifestEntryPointList[i].includeHash, "false", StringComparison.OrdinalIgnoreCase) && + SigningManifests == true) + _canPublish = false; + outputEntryPoint.SetMetadata("IncludeHash", manifestEntryPointList[i].includeHash); + } + return outputEntryPoint; + } + } + } + + return outputEntryPoint; + } + + // Returns PublishFile items seperated into seperate arrays by FileType attribute. + private void GetPublishInfo( + out PublishInfo[] assemblyPublishInfos, + out PublishInfo[] filePublishInfos, + out PublishInfo[] satellitePublishInfos, + out PublishInfo[] manifestEntryPointPublishInfos) + { + List assemblyList = new List(); + List fileList = new List(); + List satelliteList = new List(); + List manifestEntryPointList = new List(); + + if (PublishFiles != null) + foreach (ITaskItem item in PublishFiles) + { + PublishInfo publishInfo = new PublishInfo(item); + string fileType = item.GetMetadata("FileType"); + switch (fileType) + { + case "Assembly": + assemblyList.Add(publishInfo); + break; + case "File": + fileList.Add(publishInfo); + break; + case "Satellite": + satelliteList.Add(publishInfo); + break; + case "ManifestEntryPoint": + manifestEntryPointList.Add(publishInfo); + break; + default: + Log.LogWarningWithCodeFromResources("GenerateManifest.InvalidItemValue", "FileType", item.ItemSpec); + continue; + } + } + + assemblyPublishInfos = assemblyList.ToArray(); + filePublishInfos = fileList.ToArray(); + satellitePublishInfos = satelliteList.ToArray(); + manifestEntryPointPublishInfos = manifestEntryPointList.ToArray(); + } + + private bool IsFiltered(ITaskItem item) + { + // If assembly is part of the FX then it should be filtered out... + // System.Reflection.AssemblyName.GetAssemblyName throws if file is not an assembly. + // We're using AssemblyIdentity.FromManagedAssembly here because it just does an + // OpenScope and returns null if not an assembly, which is much faster. + + AssemblyIdentity identity = AssemblyIdentity.FromManagedAssembly(item.ItemSpec); + if (identity != null && identity.IsInFramework(Constants.DotNetFrameworkIdentifier, TargetFrameworkVersion)) + { + return true; + } + + // If assembly is not a "Redist Root" then it should be filtered out... + string str = item.GetMetadata("IsRedistRoot"); + if (!String.IsNullOrEmpty(str)) + { + bool isRedistRoot; + if (Boolean.TryParse(str, out isRedistRoot)) + { + return !isRedistRoot; + } + } + return false; + } + + #endregion + + #region PublishInfo + private class PublishInfo + { + public readonly string key = null; + public readonly string group = null; + public readonly string targetPath = null; + public readonly string includeHash = null; + public PublishState state = PublishState.Auto; + public PublishInfo() + { + } + public PublishInfo(ITaskItem item) + { + this.key = item.ItemSpec != null ? item.ItemSpec.ToLowerInvariant() : null; + this.group = item.GetMetadata("Group"); + this.state = StringToPublishState(item.GetMetadata("PublishState")); + this.includeHash = item.GetMetadata("IncludeHash"); + this.targetPath = item.GetMetadata(ItemMetadataNames.targetPath); + } + } + #endregion + + #region MapEntry + private class MapEntry + { + public readonly ITaskItem item; + public readonly bool includedByDefault; + public PublishInfo publishInfo = null; + public MapEntry(ITaskItem item, bool includedByDefault) + { + this.item = item; + this.includedByDefault = includedByDefault; + } + } + #endregion + + #region AssemblyMap + private class AssemblyMap : IEnumerable + { + private readonly Dictionary _dictionary = new Dictionary(); + private readonly Dictionary _simpleNameDictionary = new Dictionary(); + + public MapEntry this[string fusionName] + { + get + { + MapEntry entry = null; + string key = fusionName.ToLowerInvariant(); + if (!_dictionary.TryGetValue(key, out entry)) + _simpleNameDictionary.TryGetValue(key, out entry); + return entry; + } + } + + public void Add(ITaskItem item) + { + MapEntry entry = new MapEntry(item, true); + string key; + string fusionName = item.GetMetadata(ItemMetadataNames.fusionName); + if (String.IsNullOrEmpty(fusionName)) + fusionName = Path.GetFileNameWithoutExtension(item.ItemSpec); + + // Add to map with full name, for SpecificVersion=true case + key = fusionName.ToLowerInvariant(); + Debug.Assert(!_dictionary.ContainsKey(key), String.Format(CultureInfo.CurrentCulture, "Two or more items with same key '{0}' detected", key)); + if (!_dictionary.ContainsKey(key)) + _dictionary.Add(key, entry); + + // Also add to map with simple name, for SpecificVersion=false case + int i = fusionName.IndexOf(','); + if (i > 0) + { + string simpleName = fusionName.Substring(0, i); //example: "ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" -> "ClassLibrary1" + key = simpleName.ToLowerInvariant(); + // If there are multiple with same simple name then we'll take the first one and ignore the rest, which is not an unreasonable thing to do + if (!_simpleNameDictionary.ContainsKey(key)) + _simpleNameDictionary.Add(key, entry); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + } + #endregion + + #region FileMap + private class FileMap : IEnumerable + { + private readonly Dictionary _dictionary = new Dictionary(); + + public MapEntry this[string targetPath] + { + get + { + MapEntry entry = null; + string key = targetPath.ToLowerInvariant(); + _dictionary.TryGetValue(key, out entry); + return entry; + } + } + + public void Add(ITaskItem item, bool includedByDefault) + { + string targetPath = GetItemTargetPath(item); + Debug.Assert(!String.IsNullOrEmpty(targetPath)); + if (String.IsNullOrEmpty(targetPath)) return; + string key = targetPath.ToLowerInvariant(); + Debug.Assert(!_dictionary.ContainsKey(key), String.Format(CultureInfo.CurrentCulture, "Two or more items with same '{0}' attribute detected", ItemMetadataNames.targetPath)); + MapEntry entry = new MapEntry(item, includedByDefault); + if (!_dictionary.ContainsKey(key)) + _dictionary.Add(key, entry); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + } + #endregion + + #region PublishFlags + private enum PublishState + { + Auto, + Include, + Exclude, + DataFile, + Prerequisite + } + + private static PublishState StringToPublishState(string value) + { + if (!String.IsNullOrEmpty(value)) + { + try + { + return (PublishState)Enum.Parse(typeof(PublishState), value, false); + } + catch (FormatException) + { + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Invalid value '{0}' for {1}", value, "PublishState")); + } + catch (ArgumentException) + { + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Invalid value '{0}' for {1}", value, "PublishState")); + } + } + return PublishState.Auto; + } + + private class PublishFlags + { + private bool _isDataFile = false; + private bool _isPrerequisite = false; + private bool _isPublished = false; + + private PublishFlags(bool isDataFile, bool isPrerequisite, bool isPublished) + { + _isDataFile = isDataFile; + _isPrerequisite = isPrerequisite; + _isPublished = isPublished; + } + + public static PublishFlags GetAssemblyFlags(PublishState state, bool copyLocal) + { + bool isDataFile = false; + bool isPrerequisite = false; + bool isPublished = false; + switch (state) + { + case PublishState.Auto: + isPrerequisite = !copyLocal; + isPublished = copyLocal; + break; + case PublishState.Include: + isPrerequisite = false; + isPublished = true; + break; + case PublishState.Exclude: + isPrerequisite = false; + isPublished = false; + break; + case PublishState.DataFile: + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "PublishState.DataFile is invalid for an assembly")); + break; + case PublishState.Prerequisite: + isPrerequisite = true; + isPublished = false; + break; + default: + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Unhandled value PublishFlags.{0}", state.ToString())); + break; + } + return new PublishFlags(isDataFile, isPrerequisite, isPublished); + } + + public static PublishFlags GetFileFlags(PublishState state, string fileExtension, bool includedByDefault) + { + bool isDataFile = false; + bool isPrerequisite = false; + bool isPublished = false; + switch (state) + { + case PublishState.Auto: + isDataFile = includedByDefault && PathUtil.IsDataFile(fileExtension); + isPublished = includedByDefault; + break; + case PublishState.Include: + isDataFile = false; + isPublished = true; + break; + case PublishState.Exclude: + isDataFile = false; + isPublished = false; + break; + case PublishState.DataFile: + isDataFile = true; + isPublished = true; + break; + case PublishState.Prerequisite: + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "PublishState.Prerequisite is invalid for a file")); + break; + default: + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Unhandled value PublishFlags.{0}", state.ToString())); + break; + } + return new PublishFlags(isDataFile, isPrerequisite, isPublished); + } + + public static PublishFlags GetSatelliteFlags(PublishState state, CultureInfo satelliteCulture, CultureInfo targetCulture, bool includeAllSatellites) + { + bool includedByDefault = IsSatelliteIncludedByDefault(satelliteCulture, targetCulture, includeAllSatellites); + bool isDataFile = false; + bool isPrerequisite = false; + bool isPublished = false; + switch (state) + { + case PublishState.Auto: + isPrerequisite = false; + isPublished = includedByDefault; + break; + case PublishState.Include: + isPrerequisite = false; + isPublished = true; + break; + case PublishState.Exclude: + isPrerequisite = false; + isPublished = false; + break; + case PublishState.DataFile: + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "PublishState.DataFile is invalid for an assembly")); + break; + case PublishState.Prerequisite: + isPrerequisite = true; + isPublished = false; + break; + default: + Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Unhandled value PublishFlags.{0}", state.ToString())); + break; + } + return new PublishFlags(isDataFile, isPrerequisite, isPublished); + } + + public bool IsDataFile + { + get { return _isDataFile; } + } + + public bool IsPrerequisite + { + get { return _isPrerequisite; } + } + + public bool IsPublished + { + get { return _isPublished; } + } + + private static bool IsSatelliteIncludedByDefault(CultureInfo satelliteCulture, CultureInfo targetCulture, bool includeAllSatellites) + { + // If target culture not specified then satellite is not included by default... + if (targetCulture == null) + return includeAllSatellites; + + // If satellite culture matches target culture then satellite is included by default... + if (targetCulture.Equals(satelliteCulture)) + return true; + + // If satellite culture matches target culture's neutral culture then satellite is included by default... + // For example, if target culture is "fr-FR" then target culture's neutral culture is "fr", + // so if satellite culture is also "fr" then it will be included as well. + if (!targetCulture.IsNeutralCulture && targetCulture.Parent.Equals(satelliteCulture)) + return true; + + // Otherwise satellite is not included by default... + return includeAllSatellites; + } + } + #endregion + } +} diff --git a/src/XMakeTasks/ResolveNativeReference.cs b/src/XMakeTasks/ResolveNativeReference.cs new file mode 100644 index 00000000000..14e0a6a2282 --- /dev/null +++ b/src/XMakeTasks/ResolveNativeReference.cs @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using System.Resources; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Main class for the native reference resolution task. + /// + public class ResolveNativeReference : TaskExtension + { + #region Constructors + + /// + /// ResolveNativeReference constructor + /// + public ResolveNativeReference() + { + // do nothing. + } + #endregion + + #region Properties + [Required] + public ITaskItem[] NativeReferences + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_nativeReferences, "nativeReferences"); + return _nativeReferences; + } + set + { + _nativeReferences = value; + } + } + + [Required] + public string[] AdditionalSearchPaths + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_additionalSearchPaths, "additionalSearchPaths"); + return _additionalSearchPaths; + } + set + { + _additionalSearchPaths = value; + } + } + + [Output] + public ITaskItem[] ContainingReferenceFiles + { + get + { + return _containingReferenceFiles; + } + set + { + _containingReferenceFiles = value; + } + } + + [Output] + public ITaskItem[] ContainedPrerequisiteAssemblies + { + get + { + return _containedPrerequisiteAssemblies; + } + set + { + _containedPrerequisiteAssemblies = value; + } + } + + [Output] + public ITaskItem[] ContainedComComponents + { + get + { + return _containedComComponents; + } + set + { + _containedComComponents = value; + } + } + + [Output] + public ITaskItem[] ContainedTypeLibraries + { + get + { + return _containedTypeLibraries; + } + set + { + _containedTypeLibraries = value; + } + } + + [Output] + public ITaskItem[] ContainedLooseTlbFiles + { + get + { + return _containedLooseTlbFiles; + } + set + { + _containedLooseTlbFiles = value; + } + } + + [Output] + public ITaskItem[] ContainedLooseEtcFiles + { + get + { + return _containedLooseEtcFiles; + } + set + { + _containedLooseEtcFiles = value; + } + } + + private ITaskItem[] _nativeReferences = null; + private ITaskItem[] _containingReferenceFiles = null; + private ITaskItem[] _containedPrerequisiteAssemblies = null; + private ITaskItem[] _containedComComponents = null; + private ITaskItem[] _containedTypeLibraries = null; + private ITaskItem[] _containedLooseTlbFiles = null; + private ITaskItem[] _containedLooseEtcFiles = null; + private string[] _additionalSearchPaths = new string[0]; + #endregion + + #region Nested classes + private class ItemSpecComparerClass : IComparer + { + int IComparer.Compare(Object taskItem1, Object taskItem2) + { + // simply calls string.Compare on the item specs of the items + return (string.Compare(((ITaskItem)taskItem1).ItemSpec, ((ITaskItem)taskItem2).ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + } + #endregion + + #region ITask members + + /// + /// Task entry point. + /// + /// + public override bool Execute() + { + // Process each task item. If one of them fails we still process the + // rest of them, but remember that the task should return failure. + bool retValue = true; + int reference = 0; + + Hashtable containingReferenceFilesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + Hashtable containedPrerequisiteAssembliesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + Hashtable containedComComponentsTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + Hashtable containedTypeLibrariesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + Hashtable containedLooseTlbFilesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + Hashtable containedLooseEtcFilesTable = new Hashtable(StringComparer.OrdinalIgnoreCase); + + for (reference = 0; reference < NativeReferences.GetLength(0); reference++) + { + ITaskItem item = NativeReferences[reference]; + string path = item.GetMetadata("HintPath"); + // If no HintPath then fallback to trying to resolve from the assembly identity... + if (String.IsNullOrEmpty(path) || !File.Exists(path)) + { + AssemblyIdentity ai = AssemblyIdentity.FromAssemblyName(item.ItemSpec); + if (ai != null) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveNativeReference.ResolveReference", item.ItemSpec); + foreach (string searchPath in AdditionalSearchPaths) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", searchPath); + } + path = ai.Resolve(AdditionalSearchPaths); + } + } + else + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveNativeReference.ResolveReference", item.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", path); + } + + if (!String.IsNullOrEmpty(path)) + { +#if _DEBUG + try + { +#endif + if (!ExtractFromManifest(NativeReferences[reference], path, containingReferenceFilesTable, containedPrerequisiteAssembliesTable, containedComComponentsTable, containedTypeLibrariesTable, containedLooseTlbFilesTable, containedLooseEtcFilesTable)) + { + retValue = false; + } +#if _DEBUG + } + catch (Exception) + { + Debug.Assert(false, "Unexpected exception in ResolveNativeReference.Execute. " + + "Please log a MSBuild bug specifying the steps to reproduce the problem."); + throw; + } +#endif + } + else + { + Log.LogWarningWithCodeFromResources("ResolveNativeReference.FailedToResolveReference", item.ItemSpec); + } + } + + IComparer itemSpecComparer = new ItemSpecComparerClass(); + + _containingReferenceFiles = new ITaskItem[containingReferenceFilesTable.Count]; + containingReferenceFilesTable.Values.CopyTo(_containingReferenceFiles, 0); + Array.Sort(_containingReferenceFiles, itemSpecComparer); + + _containedPrerequisiteAssemblies = new ITaskItem[containedPrerequisiteAssembliesTable.Count]; + containedPrerequisiteAssembliesTable.Values.CopyTo(_containedPrerequisiteAssemblies, 0); + Array.Sort(_containedPrerequisiteAssemblies, itemSpecComparer); + + _containedComComponents = new ITaskItem[containedComComponentsTable.Count]; + containedComComponentsTable.Values.CopyTo(_containedComComponents, 0); + Array.Sort(_containedComComponents, itemSpecComparer); + + _containedTypeLibraries = new ITaskItem[containedTypeLibrariesTable.Count]; + containedTypeLibrariesTable.Values.CopyTo(_containedTypeLibraries, 0); + Array.Sort(_containedTypeLibraries, itemSpecComparer); + + _containedLooseTlbFiles = new ITaskItem[containedLooseTlbFilesTable.Count]; + containedLooseTlbFilesTable.Values.CopyTo(_containedLooseTlbFiles, 0); + Array.Sort(_containedLooseTlbFiles, itemSpecComparer); + + _containedLooseEtcFiles = new ITaskItem[containedLooseEtcFilesTable.Count]; + containedLooseEtcFilesTable.Values.CopyTo(_containedLooseEtcFiles, 0); + Array.Sort(_containedLooseEtcFiles, itemSpecComparer); + + return retValue; + } + #endregion + + #region Methods + + /// + /// Helper manifest resolution method. Cracks the manifest and extracts the different elements from it. + /// + internal bool ExtractFromManifest(ITaskItem taskItem, string path, Hashtable containingReferenceFilesTable, Hashtable containedPrerequisiteAssembliesTable, Hashtable containedComComponentsTable, Hashtable containedTypeLibrariesTable, Hashtable containedLooseTlbFilesTable, Hashtable containedLooseEtcFilesTable) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveNativeReference.Comment", path); + + Manifest manifest = null; + + try + { + manifest = ManifestReader.ReadManifest(path, false); + } + catch (System.Xml.XmlException ex) + { + Log.LogErrorWithCodeFromResources("GenerateManifest.ReadInputManifestFailed", path, ex.Message); + return false; + } + + if (manifest != null) + { + manifest.TreatUnfoundNativeAssembliesAsPrerequisites = true; + manifest.ReadOnly = true; // only reading a manifest, set flag so we get GenerateManifest.ResolveFailedInReadOnlyMode instead of GenerateManifest.ResolveFailedInReadWriteMode messages + manifest.ResolveFiles(); + if (!manifest.OutputMessages.LogTaskMessages(this)) + return false; + + ApplicationManifest applicationManifest = manifest as ApplicationManifest; + bool isClickOnceApp = applicationManifest != null && applicationManifest.IsClickOnceManifest; + // ClickOnce application manifest should not be added as native reference, but we should open and process it. + if (containingReferenceFilesTable.ContainsKey(path) == false && !isClickOnceApp) + { + ITaskItem itemNativeReferenceFile = new TaskItem(); + itemNativeReferenceFile.ItemSpec = path; + if (manifest.AssemblyIdentity.Name != null) + itemNativeReferenceFile.SetMetadata(ItemMetadataNames.fusionName, manifest.AssemblyIdentity.Name); + if (taskItem != null) + taskItem.CopyMetadataTo(itemNativeReferenceFile); + containingReferenceFilesTable.Add(path, itemNativeReferenceFile); + } + + if (manifest.AssemblyReferences != null) + { + foreach (AssemblyReference assemblyref in manifest.AssemblyReferences) + { + if (assemblyref.IsVirtual) + { + //It is a CLR virtual reference, not a real reference. + continue; + } + + if (!assemblyref.IsPrerequisite) + { + // recurse and call ExtractFromManifest for this assembly--if it has a manifest it will be cracked. + ExtractFromManifest(null, assemblyref.ResolvedPath, containingReferenceFilesTable, containedPrerequisiteAssembliesTable, containedComComponentsTable, containedTypeLibrariesTable, containedLooseTlbFilesTable, containedLooseEtcFilesTable); + } + else + { + string id = assemblyref.AssemblyIdentity.GetFullName(AssemblyIdentity.FullNameFlags.All); + // add the assembly to the prerequisites list, if it's not already there + if (containedPrerequisiteAssembliesTable.ContainsKey(id) == false) + { + ITaskItem item = new TaskItem(); + item.ItemSpec = id; + item.SetMetadata("DependencyType", "Prerequisite"); + containedPrerequisiteAssembliesTable.Add(id, item); + } + } + } + } + + if (manifest.FileReferences != null) + { + foreach (FileReference fileref in manifest.FileReferences) + { + if (fileref.ResolvedPath == null) + continue; + + // add the loose file to the outputs list, if it's not already there + if (containedLooseEtcFilesTable.ContainsKey(fileref.ResolvedPath) == false) + { + ITaskItem itemLooseEtcFile = new TaskItem(); + itemLooseEtcFile.ItemSpec = fileref.ResolvedPath; + // The ParentFile attribute (visible thru Project Outputs) relates the loose + // file to the parent assembly of which it is a part. This is important so we can + // group those files together with their parent assembly in the deployment tool + // (i.e. ClickOnce application files dialog). + itemLooseEtcFile.SetMetadata(ItemMetadataNames.parentFile, Path.GetFileName(path)); + containedLooseEtcFilesTable.Add(fileref.ResolvedPath, itemLooseEtcFile); + } + + if (fileref.ComClasses != null) + { + foreach (ComClass comclass in fileref.ComClasses) + { + // add the comclass to the outputs list, if it's not already there + if (containedComComponentsTable.ContainsKey(comclass.ClsId) == false) + { + ITaskItem itemComClass = new TaskItem(); + itemComClass.ItemSpec = comclass.ClsId; + containedComComponentsTable.Add(comclass.ClsId, itemComClass); + } + } + } + + if (fileref.TypeLibs != null) + { + foreach (TypeLib typelib in fileref.TypeLibs) + { + // add the typelib to the outputs list, if it's not already there + if (containedTypeLibrariesTable.ContainsKey(typelib.TlbId) == false) + { + ITaskItem itemTypeLib = new TaskItem(); + itemTypeLib.ItemSpec = typelib.TlbId; + itemTypeLib.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.tlbimp); + itemTypeLib.SetMetadata(ComReferenceItemMetadataNames.guid, typelib.TlbId); + itemTypeLib.SetMetadata(ComReferenceItemMetadataNames.lcid, "0"); + string delimStr = "."; + char[] delimiter = delimStr.ToCharArray(); + string[] verMajorAndMinor = null; + verMajorAndMinor = typelib.Version.Split(delimiter); + // UNDONE: are major and minor version numbers in base 10 or 16? + itemTypeLib.SetMetadata(ComReferenceItemMetadataNames.versionMajor, verMajorAndMinor[0]); + itemTypeLib.SetMetadata(ComReferenceItemMetadataNames.versionMinor, verMajorAndMinor[1]); + containedTypeLibrariesTable.Add(typelib.TlbId, itemTypeLib); + } + } + + // add the loose TLB file to the outputs list, if it's not already there + if (containedLooseTlbFilesTable.Contains(fileref.ResolvedPath) == false) + { + ITaskItem itemLooseTlbFile = new TaskItem(); + itemLooseTlbFile.ItemSpec = fileref.ResolvedPath; + containedLooseTlbFilesTable.Add(fileref.ResolvedPath, itemLooseTlbFile); + } + } + } + } + } + return true; + } + #endregion + } +} diff --git a/src/XMakeTasks/ResolveNonMSBuildProjectOutput.cs b/src/XMakeTasks/ResolveNonMSBuildProjectOutput.cs new file mode 100644 index 00000000000..ad2dac39b76 --- /dev/null +++ b/src/XMakeTasks/ResolveNonMSBuildProjectOutput.cs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This task determines the output files for non-MSBuild project references. We look inside + /// a special property set by the VS IDE for the list of project guids and their associated outputs. + /// While there's nothing that would prevent resolution of MSBuild projects in this task, the IDE + /// only pre-resolves non-MSBuild projects so that we can separate MSBuild project references from + /// non-MSBuild ones and return the list of MSBuild projects as UnresolvedProjectReferences. + /// Then we can use more powerful MSBuild mechanisms to manipulate just the MSBuild project + /// references (i.e. calling into specific targets of references to get the manifest file name) + /// which would not be possible with a mixed list of MSBuild & non-MSBuild references. + /// + public class ResolveNonMSBuildProjectOutput : ResolveProjectBase + { + #region Constructors + + /// + /// default public constructor + /// + public ResolveNonMSBuildProjectOutput() + { + // do nothing + } + + #endregion + + #region Properties + + /// + /// A special XML string containing resolved project outputs - we need to simply match the projects and + /// return the appropriate paths + /// + public string PreresolvedProjectOutputs + { + get + { + return _preresolvedProjectOutputs; + } + set + { + _preresolvedProjectOutputs = value; + } + } + + private string _preresolvedProjectOutputs = null; + + /// + /// The list of resolved reference paths (preserving the original project reference attributes) + /// + [Output] + public ITaskItem[] ResolvedOutputPaths + { + get + { + return _resolvedOutputPaths; + } + set + { + _resolvedOutputPaths = value; + } + } + + private ITaskItem[] _resolvedOutputPaths = null; + + /// + /// The list of project reference items that could not be resolved using the pre-resolved list of outputs. + /// Since VS only pre-resolves non-MSBuild projects, this means that project references in this list + /// are in the MSBuild format. + /// + [Output] + public ITaskItem[] UnresolvedProjectReferences + { + get + { + return _unresolvedProjectReferences; + } + set + { + _unresolvedProjectReferences = value; + } + } + + private ITaskItem[] _unresolvedProjectReferences = null; + + /// + /// A delegate with a signature that matches AssemblyName.GetAssemblyName. + /// + internal delegate AssemblyName GetAssemblyNameDelegate(string path); + + /// + /// A dependency-injection way of getting an assembly name. + /// + internal GetAssemblyNameDelegate GetAssemblyName { get; set; } + + #endregion + + #region ITask Members + + /// + /// Main task method + /// + /// + public override bool Execute() + { + // Allow unit tests to inject a non-file system dependent version of this. + if (GetAssemblyName == null) + { + GetAssemblyName = AssemblyName.GetAssemblyName; + } + + try + { + if (!VerifyProjectReferenceItems(ProjectReferences, false /* treat problems as warnings */)) + return false; + + ArrayList resolvedPaths = new ArrayList(ProjectReferences.GetLength(0)); + ArrayList unresolvedReferences = new ArrayList(ProjectReferences.GetLength(0)); + + CacheProjectElementsFromXml(PreresolvedProjectOutputs); + + foreach (ITaskItem projectRef in ProjectReferences) + { + bool resolveSuccess = false; + ITaskItem resolvedPath; + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveNonMSBuildProjectOutput.ProjectReferenceResolutionStarting", projectRef.ItemSpec); + + resolveSuccess = ResolveProject(projectRef, out resolvedPath); + + if (resolveSuccess) + { + if (resolvedPath.ItemSpec.Length > 0) + { + // VC project system does not look like an MSBuild project type yet because VC do not + // yet implement IVSProjectBuildSystem. So project references to VC managed libraries + // need to be recognized as such. + // Even after VC implements this IVSProjectBuildSystem, other project types (NMake, NAnt, etc.) + // still can generate managed assemblies, in which case we still need to perform this managed + // assembly check. + try + { + GetAssemblyName(resolvedPath.ItemSpec); + resolvedPath.SetMetadata("ManagedAssembly", "true"); + } + catch (BadImageFormatException) + { + } + + resolvedPaths.Add(resolvedPath); + + Log.LogMessageFromResources(MessageImportance.Low, "ResolveNonMSBuildProjectOutput.ProjectReferenceResolutionSuccess", projectRef.ItemSpec, resolvedPath.ItemSpec); + } + // If resolved path is empty, log a warning. This means that this reference is not an MSBuild reference, + // but could not be resolved in the IDE. + else + { + Log.LogWarningWithCodeFromResources("ResolveNonMSBuildProjectOutput.ProjectReferenceResolutionFailure", projectRef.ItemSpec); + } + } + else + { + unresolvedReferences.Add(projectRef); + + // This is not an error - we pass unresolved references to UnresolvedProjectReferences for further + // processing in the .targets file. + Log.LogMessageFromResources(MessageImportance.Low, "ResolveNonMSBuildProjectOutput.ProjectReferenceUnresolved", projectRef.ItemSpec); + } + } + + ResolvedOutputPaths = (ITaskItem[])resolvedPaths.ToArray(typeof(ITaskItem)); + UnresolvedProjectReferences = (ITaskItem[])unresolvedReferences.ToArray(typeof(ITaskItem)); + } + catch (XmlException e) + { + Log.LogErrorWithCodeFromResources("General.ErrorExecutingTask", this.GetType().Name, e.Message); + return false; + } + + return true; + } + + #endregion + + #region Methods + + /// + /// Given a project reference task item and an XML document containing pre-resolved output paths, + /// find the output path for that task item. + /// + /// resulting ITaskItem containing the resolved path + /// true if resolved successfully + internal bool ResolveProject(ITaskItem projectRef, out ITaskItem resolvedPath) + { + string projectOutputPath = GetProjectItem(projectRef); + if (projectOutputPath != null) + { + resolvedPath = new TaskItem(projectOutputPath); + projectRef.CopyMetadataTo(resolvedPath); + return true; + } + + resolvedPath = null; + return false; + } + + #endregion + } +} + diff --git a/src/XMakeTasks/ResolveProjectBase.cs b/src/XMakeTasks/ResolveProjectBase.cs new file mode 100644 index 00000000000..40679b4d996 --- /dev/null +++ b/src/XMakeTasks/ResolveProjectBase.cs @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for ResolveNonMSBuildProjectOutput and AssignProjectConfiguration, since they have + /// similar architecture + /// + abstract public class ResolveProjectBase : TaskExtension + { + #region Properties + + /// + /// The list of project references + /// + [Required] + public ITaskItem[] ProjectReferences + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_projectReferences, "projectReferences"); + return _projectReferences; + } + set + { + _projectReferences = value; + } + } + + private ITaskItem[] _projectReferences = null; + + // This field stores all the distinct project references by project absolute path + private HashSet _cachedProjectReferencesByAbsolutePath = new HashSet(StringComparer.OrdinalIgnoreCase); + + // This field stores pre-cached project elements for project guids for quicker access by project guid + private Dictionary _cachedProjectElements = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // This field stores pre-cached project elements for project guids for quicker access by project absolute path + private Dictionary _cachedProjectElementsByAbsolutePath = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // This field stores the project absolute path for quicker access by project guid + private Dictionary _cachedProjectAbsolutePathsByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // This field stores the project guid for quicker access by project absolute path + private Dictionary _cachedProjectGuidsByAbsolutePath = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // This field stores the list of dependency project guids by depending project guid + private Dictionary> _cachedDependencyProjectGuidsByDependingProjectGuid = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + private const string attributeProject = "Project"; + + private const string attributeAbsolutePath = "AbsolutePath"; + + #endregion + + #region Methods + + /// + /// Checks if a project reference task item contains all the required attributes. + /// Currently, the only required attribute is project GUID for inside the IDE mode. + /// + internal bool VerifyReferenceAttributes(ITaskItem reference, out string missingAttribute) + { + missingAttribute = attributeProject; + string attrValue = reference.GetMetadata(missingAttribute); + + // missing project GUID? (no longer required, but if it's there then validate it) + if (attrValue.Length > 0) + { + // invalid project GUID format? + Guid guid; + if (!Guid.TryParse(attrValue, out guid)) + { + return false; + } + } + + missingAttribute = null; + + return true; + } + + /// + /// Checks all project reference task items for required attributes + /// Internal for unit testing + /// + internal bool VerifyProjectReferenceItems(ITaskItem[] references, bool treatAsError) + { + bool referencesValid = true; + string missingAttribute; + + foreach (ITaskItem reference in references) + { + _cachedProjectReferencesByAbsolutePath.Add(reference.GetMetadata("FullPath")); // metadata is cached and used again later + + if (!VerifyReferenceAttributes(reference, out missingAttribute)) + { + if (treatAsError) + { + Log.LogErrorWithCodeFromResources("General.MissingOrUnknownProjectReferenceAttribute", reference.ItemSpec, missingAttribute); + referencesValid = false; + } + else + { + Log.LogWarningWithCodeFromResources("General.MissingOrUnknownProjectReferenceAttribute", reference.ItemSpec, missingAttribute); + } + } + } + + return referencesValid; + } + + /// + /// Pre-cache individual project elements from the XML string in a hashtable for quicker access. + /// + /// + internal void CacheProjectElementsFromXml(string xmlString) + { + XmlDocument doc = null; + + if (!string.IsNullOrEmpty(xmlString)) + { + doc = new XmlDocument(); + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader reader = XmlReader.Create(new StringReader(xmlString), settings)) + { + doc.Load(reader); + } + } + + // Example: + // + // + // Debug|AnyCPU + // Debug|AnyCPU + // Debug|AnyCPU + // + // + if (doc != null && doc.DocumentElement != null) + { + foreach (XmlElement xmlElement in doc.DocumentElement.ChildNodes) + { + string projectGuid = xmlElement.GetAttribute(attributeProject); + string projectAbsolutePath = xmlElement.GetAttribute(attributeAbsolutePath); + + // What we really want here is the normalized path, like we'd get with an item's "FullPath" metadata. However, + // if there's some bogus full path in the solution configuration (e.g. a website with a "full path" of c:\solutiondirectory\http://localhost) + // we do NOT want to throw -- chances are extremely high that that's information that will never actually be used. So resolve the full path + // but just swallow any IO-related exceptions that result. If the path is bogus, the method will return null, so we'll just quietly fail + // to cache it below. + projectAbsolutePath = FileUtilities.GetFullPathNoThrow(projectAbsolutePath); + + if (!string.IsNullOrEmpty(projectGuid)) + { + _cachedProjectElements[projectGuid] = xmlElement; + if (!string.IsNullOrEmpty(projectAbsolutePath)) + { + _cachedProjectElementsByAbsolutePath[projectAbsolutePath] = xmlElement; + _cachedProjectAbsolutePathsByGuid[projectGuid] = projectAbsolutePath; + _cachedProjectGuidsByAbsolutePath[projectAbsolutePath] = projectGuid; + } + + foreach (XmlNode dependencyNode in xmlElement.ChildNodes) + { + if (dependencyNode.NodeType != XmlNodeType.Element) + { + continue; + } + + XmlElement dependencyElement = ((XmlElement)dependencyNode); + + if (!String.Equals(dependencyElement.Name, "ProjectDependency", StringComparison.Ordinal)) + { + continue; + } + + string dependencyGuid = dependencyElement.GetAttribute("Project"); + + if (dependencyGuid.Length == 0) + { + continue; + } + + List list; + if (!_cachedDependencyProjectGuidsByDependingProjectGuid.TryGetValue(projectGuid, out list)) + { + list = new List(); + _cachedDependencyProjectGuidsByDependingProjectGuid.Add(projectGuid, list); + } + + list.Add(dependencyGuid); + } + } + } + } + } + + /// + /// Helper method for retrieving whatever was stored in the XML string for the given project + /// + /// + /// + /// + protected string GetProjectItem(ITaskItem projectRef) + { + XmlElement projectElement = GetProjectElement(projectRef); + return projectElement != null ? projectElement.InnerText : null; + } + + /// + /// Helper method for retrieving the XML element for the given project + /// + [SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes", MessageId = "System.Xml.XmlNode", Justification = "protected method on a public base class that has previously shipped, so changing this now would be a breaking change.")] + protected XmlElement GetProjectElement(ITaskItem projectRef) + { + string projectGuid = projectRef.GetMetadata(attributeProject); + XmlElement projectElement = null; + + if ((_cachedProjectElements.TryGetValue(projectGuid, out projectElement)) && (projectElement != null)) + { + return projectElement; + } + + // We didn't find the project element by locating a project guid on the P2P reference + // next we'll try a lookup by the absolute path of the project + string projectFullPath = projectRef.GetMetadata("FullPath"); // reserved metadata "FullPath" is used at it will cache the value + + if ((_cachedProjectElementsByAbsolutePath.TryGetValue(projectFullPath, out projectElement)) && (projectElement != null)) + { + return projectElement; + } + + return null; + } + + /// + /// Helper method for retrieving the extra "project references" passed in the solution blob. + /// These came from dependencies expressed in the solution file itself. + /// + protected void AddSyntheticProjectReferences(string currentProjectAbsolutePath) + { + // Get the guid for this project + string projectGuid; + if (!_cachedProjectGuidsByAbsolutePath.TryGetValue(currentProjectAbsolutePath, out projectGuid)) + { + // We were passed a blob, but we weren't listed in it. Odd. Return. + return; + } + + // Use the guid to look up the dependencies for it + List guids; + if (!_cachedDependencyProjectGuidsByDependingProjectGuid.TryGetValue(projectGuid, out guids)) + { + // We didn't have dependencies listed in the blob + return; + } + + // ProjectReferences is a fixed size array, so start aggregating in a list + List updatedProjectReferenceList = new List(_projectReferences); + + foreach (string guid in guids) + { + // Get the absolute path of the dependency, using the blob + string path; + if (!_cachedProjectAbsolutePathsByGuid.TryGetValue(guid, out path)) + { + // We had a dependency listed in the blob that wasn't itself in the blob. Odd. Return. + continue; + } + + // If the dependency's already specified as a project reference, ignore it; no sense referencing it twice + if (!_cachedProjectReferencesByAbsolutePath.Contains(path)) + { + _cachedProjectReferencesByAbsolutePath.Add(path); + var item = new TaskItem(path); + + // Unfortunately we've used several different metadata names to trigger + // project references to do stuff other than trigger a build + item.SetMetadata("ReferenceOutputAssembly", "false"); + item.SetMetadata("LinkLibraryDependencies", "false"); + item.SetMetadata("CopyLocal", "false"); + + updatedProjectReferenceList.Add(item); + } + } + + // Finally, set our new augmented project references list as the official list; + // note that this means that the output parameter may include project references that weren't passed in + _projectReferences = new ITaskItem[updatedProjectReferenceList.Count]; + updatedProjectReferenceList.CopyTo(_projectReferences); + } + + #endregion + } +} diff --git a/src/XMakeTasks/ResolveSDKReference.cs b/src/XMakeTasks/ResolveSDKReference.cs new file mode 100644 index 00000000000..53e806e76c7 --- /dev/null +++ b/src/XMakeTasks/ResolveSDKReference.cs @@ -0,0 +1,1799 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Resolves an SDKReference to a full path on disk +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.Win32; +using System.Linq; +using System.Globalization; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build.Tasks +{ + /// + /// Resolves an SDKReference to a full path on disk + /// + public class ResolveSDKReference : TaskExtension + { + #region fields + + /// + /// Regex for breaking up the sdk reference include into pieces. + /// Example: XNA, Version=8.0 + /// + private static readonly Regex s_sdkReferenceFormat = new Regex + ( + @"(?^[^,]*),\s*Version=(?.*)", + RegexOptions.IgnoreCase + ); + + /// + /// SimpleName group + /// + private static readonly string s_SDKsimpleNameGroup = "SDKSIMPLENAME"; + + /// + /// Version group + /// + private static readonly string s_SDKVersionGroup = "SDKVERSION"; + + /// + /// Delimiter used to delimit the dependent sdk's in the warning message + /// + private const string CommaSpaceDelimiter = ", "; + + /// + /// Split char for the appx attribute + /// + private static readonly char[] s_appxSplitChar = new char[] { '-' }; + + /// + /// SDKName + /// + private const string SDKName = "SDKName"; + + /// + /// PlatformVersion + /// + private const string SDKPlatformVersion = "PlatformVersion"; + + /// + /// Split char for strings + /// + private static readonly char[] s_semicolonSplitChar = { ';' }; + + /// + /// Default target platform version + /// + private static Version s_defaultTargetPlatformVersion = new Version("7.0"); + + /// + /// Set of sdk references to resolve to paths on disk. + /// + private ITaskItem[] _sdkReferences = new ITaskItem[0]; + + /// + /// The list of installed SDKs the location of the SDK, the SDKName metadata is the SDKName. + /// + private ITaskItem[] _installedSDKs = new ITaskItem[0]; + + /// + /// Should resolution errors be logged as warnings or errors. + /// + private bool _logResolutionErrorsAsWarnings = false; + + /// + /// The value of the prefer32bit flag used in the build + /// + private bool _prefer32Bit = false; + + /// + /// stores value of TargetPlatformVersion property + /// + private Version _targetPlatformVersion = null; + + /// + /// Stores TargetPlatform property + /// + private string _targetPlatformIdentifier = null; + + /// + /// Stores ProjectName property + /// + private string _projectName = null; + + /// + /// Stores dictionary with runtime only reference dependencies + /// + private Dictionary _runtimeReferenceOnlyDependenciesByName; + + /// + /// Stores flag to enable warning if SDK's max platform version is not specified in the manifest + /// + private bool _enableMaxPlatformVersionEmptyWarning = false; + + #endregion + + #region Properties + + /// + /// Set of SDK References to resolve to paths on disk + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + [Required] + public ITaskItem[] SDKReferences + { + get + { + return _sdkReferences; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "SDKReferences"); + _sdkReferences = value; + } + } + + /// + /// The list of installed SDKs the location of the SDK, the SDKName metadata is the SDKName. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + [Required] + public ITaskItem[] InstalledSDKs + { + get + { + return _installedSDKs; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "InstalledSDKs"); + _installedSDKs = value; + } + } + + /// + /// TargetPlatform used in warning/error messages + /// + [Required] + public string TargetPlatformIdentifier + { + get + { + _targetPlatformIdentifier = _targetPlatformIdentifier ?? String.Empty; + return _targetPlatformIdentifier; + } + + set + { + _targetPlatformIdentifier = value; + } + } + + /// + /// ProjectName used in warning/error messages + /// + [Required] + public string ProjectName + { + get + { + _projectName = _projectName ?? String.Empty; + return _projectName; + } + + set + { + _projectName = value; + } + } + + /// + /// TargetPlatformVersion property used to filter SDKs + /// + [Required] + public string TargetPlatformVersion + { + get + { + return TargetPlatformAsVersion.ToString(); + } + + set + { + Version versionValue; + if (Version.TryParse(value, out versionValue)) + { + TargetPlatformAsVersion = versionValue; + } + } + } + + /// + /// Reference may be passed in so their SDKNames can be resolved and then sdkroot paths can be tacked onto the reference + /// so RAR can find the assembly correctly in the sdk location. + /// + public ITaskItem[] References + { + get; + set; + } + + /// + /// List of disallowed dependencies passed from the targets file (deprecated) + /// For instance "VCLibs 11" should be disallowed in projects targeting Win 8.1 or higher. + /// + public ITaskItem[] DisallowedSDKDependencies + { + get; + set; + } + + /// + /// List of dependencies passed from the targets file that will have the metadata RuntimeReferenceOnly set as true. + /// For instance "VCLibs 11" should have such a metadata set to true in projects targeting Win 8.1 or higher. + /// + public ITaskItem[] RuntimeReferenceOnlySDKDependencies + { + get; + set; + } + + /// + /// Configuration for SDK's which are resolved + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public string TargetedSDKConfiguration + { + get; + set; + } + + /// + /// Architecture of the SDK's we are targeting + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public string TargetedSDKArchitecture + { + get; + set; + } + + /// + /// Enables warning when MaxPlatformVersion is not present in the manifest and the ESDK platform version (from its path) + /// is different than the target platform version (from the project) + /// + public bool WarnOnMissingPlatformVersion + { + get + { + return _enableMaxPlatformVersionEmptyWarning; + } + + set + { + _enableMaxPlatformVersionEmptyWarning = value; + } + } + + /// + /// Should problems resolving SDKs be logged as a warning or an error. + /// If the resolution problem is logged as an error the build will fail. + /// If the resolution problem is logged as a warning we will warn and continue. + /// + public bool LogResolutionErrorsAsWarnings + { + get + { + return _logResolutionErrorsAsWarnings; + } + + set + { + _logResolutionErrorsAsWarnings = value; + } + } + + /// + /// The prefer32bit flag used during the build + /// + public bool Prefer32Bit + { + get + { + return _prefer32Bit; + } + + set + { + _prefer32Bit = value; + } + } + + /// + /// Resolved SDK References + /// + [Output] + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")] + public ITaskItem[] ResolvedSDKReferences + { + get; + private set; + } + + /// + /// Version object containing target platform version + /// + private Version TargetPlatformAsVersion + { + get + { + _targetPlatformVersion = _targetPlatformVersion ?? s_defaultTargetPlatformVersion; + return _targetPlatformVersion; + } + + set + { + _targetPlatformVersion = value; + } + } + + #endregion + + /// + /// Execute the task. + /// + public override bool Execute() + { + ResolvedSDKReferences = new TaskItem[0]; + + if (InstalledSDKs.Length == 0) + { + Log.LogMessageFromResources("ResolveSDKReference.NoSDKLocationsSpecified"); + return true; + } + + _runtimeReferenceOnlyDependenciesByName = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + if (RuntimeReferenceOnlySDKDependencies != null) + { + foreach (ITaskItem runtimeDependencyOnlyItem in RuntimeReferenceOnlySDKDependencies) + { + string dependencyName; + string dependencyVersion; + + if (ParseSDKReference(runtimeDependencyOnlyItem.ItemSpec, out dependencyName, out dependencyVersion)) + { + _runtimeReferenceOnlyDependenciesByName[dependencyName] = dependencyVersion; + } + } + } + + // Convert the list of installed SDK's to a dictionary for faster lookup + Dictionary sdkItems = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (ITaskItem installedsdk in InstalledSDKs) + { + string installLocation = installedsdk.ItemSpec; + string sdkName = installedsdk.GetMetadata(SDKName); + string sdkPlatformVersion = installedsdk.GetMetadata(SDKPlatformVersion); + + if (installLocation.Length > 0 && sdkName.Length > 0) + { + if (!sdkItems.ContainsKey(sdkName)) + { + sdkItems.Add(sdkName, installedsdk); + } + else + { + sdkItems[sdkName] = installedsdk; + } + } + } + + // We need to check to see if there are any SDKNames on any of the reference items in the project. If there are + // then we do not want those SDKs to expand their reference assemblies by default because we are going to use RAR to look inside of them for certain reference assemblies only. + HashSet sdkNamesOnReferenceItems = new HashSet(StringComparer.OrdinalIgnoreCase); + if (References != null) + { + foreach (ITaskItem referenceItem in References) + { + string sdkName = referenceItem.GetMetadata(SDKName); + if (sdkName.Length > 0) + { + sdkNamesOnReferenceItems.Add(sdkName); + } + } + } + + // The set of reference items declared in the project file, without duplicate entries. + HashSet sdkReferenceItems = new HashSet(); + + // Maps a product family name to a set of SDKs with that product family name + Dictionary> productFamilyNameToSDK = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // Maps a sdk name (no version) to a set of SDKReferences with the same name + Dictionary> sdkNameToSDK = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // Set of sdks which are not compatible with other sdks of the same product famuily or with the same sdk name + HashSet sdksNotCompatibleWithOtherSDKs = new HashSet(); + + // Go through each reference passed in and determine if it is in the set of installed SDKs. + // Also create new output items if the item is in an installed SDK and set the metadata correctly. + foreach (ITaskItem referenceItem in SDKReferences) + { + // Parse the SDK reference item include. The name could have been added by a user and may have extra spaces or be not well formatted. + SDKReference reference = ParseSDKReference(referenceItem); + + // Could not parse the reference, lets skip over this reference item. An error would have been logged in the ParseSDKReference method to tell the + // user why the parsing did not happen. + if (reference == null) + { + continue; + } + + // Make sure we do not include a duplicate reference item if one has already been seen in the project file. + if (!sdkReferenceItems.Contains(reference) /* filter out duplicate sdk reference entries*/) + { + sdkReferenceItems.Add(reference); + reference.Resolve(sdkItems, TargetedSDKConfiguration, TargetedSDKArchitecture, sdkNamesOnReferenceItems, _logResolutionErrorsAsWarnings, _prefer32Bit, TargetPlatformIdentifier, TargetPlatformAsVersion, ProjectName, _enableMaxPlatformVersionEmptyWarning); + if (reference.Resolved) + { + if (!String.IsNullOrEmpty(reference.ProductFamilyName)) + { + HashSet sdksWithProductFamilyName = null; + if (!productFamilyNameToSDK.TryGetValue(reference.ProductFamilyName, out sdksWithProductFamilyName)) + { + productFamilyNameToSDK.Add(reference.ProductFamilyName, new HashSet { reference }); + } + else + { + sdksWithProductFamilyName.Add(reference); + } + } + + if (reference.SupportsMultipleVersions != MultipleVersionSupport.Allow && !reference.SimpleName.Equals("Microsoft.VCLibs", StringComparison.InvariantCultureIgnoreCase)) + { + sdksNotCompatibleWithOtherSDKs.Add(reference); + } + + HashSet sdksWithSimpleName = null; + if (!sdkNameToSDK.TryGetValue(reference.SimpleName, out sdksWithSimpleName)) + { + sdkNameToSDK.Add(reference.SimpleName, new HashSet { reference }); + } + else + { + sdksWithSimpleName.Add(reference); + } + } + } + } + + // Go through each of the items which have been processed and log the results. + foreach (SDKReference reference in sdkReferenceItems) + { + LogResolution(reference); + } + + // Go through each of the incompatible references and log the ones that are not compatible with it + // starting with being incompatible with the product family then with the sdk name. + foreach (SDKReference notCompatibleReference in sdksNotCompatibleWithOtherSDKs) + { + // If we have already error or warned about an sdk not being compatible with one of the notCompatibleReferences then do not log it again + // an sdk could be incompatible because the productfamily is the same but also be incompatible at the same time due to the sdk name + // we only want to log one of those cases so we do not get 2 warings or errors for the same sdks. + HashSet sdksAlreadyErrorOrWarnedFor = new HashSet(); + + // Check to see if a productfamily was set, we want to emit this warning or error first. + if (!String.IsNullOrEmpty(notCompatibleReference.ProductFamilyName)) + { + HashSet referenceInProductFamily = null; + if (productFamilyNameToSDK.TryGetValue(notCompatibleReference.ProductFamilyName, out referenceInProductFamily)) + { + // We want to build a list of incompatible reference names so we can emit them in the error or warnings. + List listOfIncompatibleReferences = new List(); + foreach (SDKReference incompatibleReference in referenceInProductFamily) + { + if (!sdksAlreadyErrorOrWarnedFor.Contains(incompatibleReference) && incompatibleReference != notCompatibleReference /*cannot be incompatible with self*/) + { + listOfIncompatibleReferences.Add(String.Format(CultureInfo.CurrentCulture, "\"{0}\"", incompatibleReference.SDKName)); + sdksAlreadyErrorOrWarnedFor.Add(incompatibleReference); + } + } + + // Only log a warning or error if there were incompatible references + if (listOfIncompatibleReferences.Count > 0) + { + string incompatibleReferencesDelimited = String.Join(", ", listOfIncompatibleReferences.ToArray()); + if (notCompatibleReference.SupportsMultipleVersions == MultipleVersionSupport.Error) + { + Log.LogErrorWithCodeFromResources("ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", notCompatibleReference.SDKName, incompatibleReferencesDelimited, notCompatibleReference.ProductFamilyName); + } + else + { + Log.LogWarningWithCodeFromResources("ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", notCompatibleReference.SDKName, incompatibleReferencesDelimited, notCompatibleReference.ProductFamilyName); + } + } + } + } + + HashSet referenceWithSameName = null; + if (sdkNameToSDK.TryGetValue(notCompatibleReference.SimpleName, out referenceWithSameName)) + { + // We want to build a list of incompatible reference names so we can emit them in the error or warnings. + List listOfIncompatibleReferences = new List(); + foreach (SDKReference incompatibleReference in referenceWithSameName) + { + if (!sdksAlreadyErrorOrWarnedFor.Contains(incompatibleReference) && incompatibleReference != notCompatibleReference /*cannot be incompatible with self*/) + { + listOfIncompatibleReferences.Add(String.Format(CultureInfo.CurrentCulture, "\"{0}\"", incompatibleReference.SDKName)); + sdksAlreadyErrorOrWarnedFor.Add(incompatibleReference); + } + } + + // Only log a warning or error if there were incompatible references + if (listOfIncompatibleReferences.Count > 0) + { + string incompatibleReferencesDelimited = String.Join(", ", listOfIncompatibleReferences.ToArray()); + if (notCompatibleReference.SupportsMultipleVersions == MultipleVersionSupport.Error) + { + Log.LogErrorWithCodeFromResources("ResolveSDKReference.CannotReferenceTwoSDKsSameName", notCompatibleReference.SDKName, incompatibleReferencesDelimited); + } + else + { + Log.LogWarningWithCodeFromResources("ResolveSDKReference.CannotReferenceTwoSDKsSameName", notCompatibleReference.SDKName, incompatibleReferencesDelimited); + } + } + } + } + + AddMetadataToReferences(Log, sdkReferenceItems, _runtimeReferenceOnlyDependenciesByName, "RuntimeReferenceOnly", "true"); + + // Gather the ResolvedItems from the SDKReference where the reference was resolved. + ResolvedSDKReferences = sdkReferenceItems.Where(x => x.Resolved).Select(x => x.ResolvedItem).ToArray(); + + VerifySDKDependsOn(Log, sdkReferenceItems); + + return !Log.HasLoggedErrors; + } + + /// + /// Add metadata to a specified subset of reference items + /// + internal static void AddMetadataToReferences(TaskLoggingHelper log, HashSet sdkReferenceItems, Dictionary referencesToAddMetadata, string metadataName, string metadataValue) + { + if (referencesToAddMetadata != null) + { + foreach (var referenceItem in sdkReferenceItems) + { + string sdkSimpleName = referenceItem.SimpleName; + string rawSdkVersion = referenceItem.Version; + + if (referencesToAddMetadata.ContainsKey(sdkSimpleName) && referencesToAddMetadata[sdkSimpleName].Equals(rawSdkVersion, StringComparison.InvariantCultureIgnoreCase)) + { + referenceItem.ResolvedItem.SetMetadata(metadataName, metadataValue); + } + } + } + } + + /// + /// Verify the dependencies SDKs have for each other + /// + internal static void VerifySDKDependsOn(TaskLoggingHelper log, HashSet sdkReferenceItems) + { + foreach (var reference in sdkReferenceItems) + { + List dependentSDKs = ParseDependsOnSDK(reference.DependsOnSDK); + if (dependentSDKs.Count > 0) + { + // Get the list of dependencies that are not resolved and are depended on by the current sdk + string[] unresolvedDependencyIdentities = GetUnresolvedDependentSDKs(sdkReferenceItems, dependentSDKs); + + // Generate the string of sdks which were not resolved and are depended upon by this sdk + string missingDependencies = String.Join(CommaSpaceDelimiter, unresolvedDependencyIdentities); + + if (missingDependencies.Length > 0) + { + log.LogWarningWithCodeFromResources("ResolveSDKReference.SDKMissingDependency", reference.SDKName, missingDependencies); + } + } + } + } + + /// + /// Get a set of unresolved SDK identities + /// + internal static string[] GetUnresolvedDependentSDKs(HashSet sdkReferenceItems, List dependentSDKs) + { + string[] unresolvedDependencyIdentities = dependentSDKs.Where(x => + { + string simpleName; + string sdkVersion; + bool parseSuccessful = ParseSDKReference(x, out simpleName, out sdkVersion); + if (!parseSuccessful) + { + // If a dependency could not be parsed as an SDK identity then ignore it from the list of unresolved dependencies + return false; + } + + // See if there are any resolved references that have the correct simple name and version + var resolvedReference = sdkReferenceItems.Where(y => String.Equals(y.SimpleName, simpleName, StringComparison.OrdinalIgnoreCase) && String.Equals(y.Version, sdkVersion, StringComparison.OrdinalIgnoreCase)).DefaultIfEmpty(null).FirstOrDefault(); + + // Return true if no reference could be found + return resolvedReference == null; + }) + .Select(y => String.Format(CultureInfo.CurrentCulture, "\"{0}\"", y)) + .ToArray(); + + return unresolvedDependencyIdentities; + } + + /// + /// Parse out the sdk identities + /// + internal static List ParseDependsOnSDK(string dependsOnSDK) + { + if (!String.IsNullOrEmpty(dependsOnSDK)) + { + return dependsOnSDK.Split(s_semicolonSplitChar, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Where(y => y.Length > 0).ToList(); + } + + return new List(); + } + + /// + /// Parse the item include of the SDKReference item into its simple name and version parts. + /// + internal SDKReference ParseSDKReference(ITaskItem referenceItem) + { + string sdkSimpleName; + string rawSdkVersion; + bool splitSuccessful = ParseSDKReference(referenceItem.ItemSpec, out sdkSimpleName, out rawSdkVersion); + + if (!splitSuccessful) + { + LogErrorOrWarning(new Tuple("ResolveSDKReference.SDKReferenceIncorrectFormat", new object[] { referenceItem.ItemSpec })); + return null; + } + + SDKReference reference = new SDKReference(referenceItem, sdkSimpleName, rawSdkVersion); + return reference; + } + + /// + /// Take the identity of an sdk and use a regex to parse out the version and simple name + /// + private static bool ParseSDKReference(string reference, out string sdkSimpleName, out string rawSdkVersion) + { + Match match = s_sdkReferenceFormat.Match(reference); + + sdkSimpleName = String.Empty; + bool parsedVersion = false; + rawSdkVersion = String.Empty; + + if (match.Success) + { + sdkSimpleName = match.Groups[s_SDKsimpleNameGroup].Value.Trim(); + + rawSdkVersion = match.Groups[s_SDKVersionGroup].Value.Trim(); + Version sdkVersion = null; + parsedVersion = Version.TryParse(rawSdkVersion, out sdkVersion); + } + + return sdkSimpleName.Length > 0 && parsedVersion; + } + + /// + /// Log where we searched ect, for sdk references and if we found them or not. + /// + private void LogResolution(SDKReference reference) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveSDKReference.SearchingForSDK", reference.ReferenceItem.ItemSpec); + + if (reference.Resolved) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveSDKReference.FoundSDK", reference.ResolvedPath); + if (reference.StatusMessages != null) + { + foreach (Tuple message in reference.StatusMessages) + { + string formattedMessage = ResourceUtilities.FormatResourceString(message.Item1, message.Item2); + Log.LogMessage(" " + formattedMessage); + } + } + } + else if (reference.ResolutionErrors == null || reference.ResolutionErrors.Count == 0) + { + // We only want to say we could not find it if there were no other errors which would cause it not to be found + LogErrorOrWarning(new Tuple("ResolveSDKReference.CouldNotResolveSDK", new object[] { reference.ReferenceItem.ItemSpec })); + } + + // Log warnings + if (reference.ResolutionWarnings != null) + { + foreach (Tuple warning in reference.ResolutionWarnings) + { + Log.LogWarningWithCodeFromResources(warning.Item1, warning.Item2); + } + } + + // Log errors + if (reference.ResolutionErrors != null) + { + foreach (Tuple error in reference.ResolutionErrors) + { + LogErrorOrWarning(error); + } + } + } + + /// + /// Log an error or warning depending on the LogErrorsAsWarnigns propertry. + /// + private void LogErrorOrWarning(Tuple errorOrWarning) + { + if (_logResolutionErrorsAsWarnings) + { + Log.LogWarningWithCodeFromResources(errorOrWarning.Item1, errorOrWarning.Item2); + } + else + { + Log.LogErrorWithCodeFromResources(errorOrWarning.Item1, errorOrWarning.Item2); + } + } + + /// + /// This class holds the sdk reference task item and the split versions of the simple name and version. + /// + internal class SDKReference : IEquatable + { + /// + /// Delimiter for supported architectures + /// + private static readonly char[] s_supportedArchitecturesSplitChars = new Char[] { ';' }; + + /// + /// Delimiter used to delimit the supported architectures in the error message + /// + private const string SupportedArchitectureJoinDelimiter = ", "; + + /// + /// Neutral architecture name + /// + private const string NeutralArch = "Neutral"; + + /// + /// Neutral architecture name + /// + private const string X64Arch = "X64"; + + /// + /// X86 architecture name + /// + private const string X86Arch = "X86"; + + /// + /// ARM architecture name + /// + private const string ARMArch = "ARM"; + + /// + /// ANY CPU architecture name + /// + private const string AnyCPUArch = "Any CPU"; + + /// + /// TargetedSDKArchitecture metadata name + /// + private const string TargetedSDKArchitecture = "TargetedSDKArchitecture"; + + /// + /// TargetedSDKConfiguration metadata name + /// + private const string TargetedSDKConfiguration = "TargetedSDKConfiguration"; + + /// + /// Retail config name + /// + private const string Retail = "Retail"; + + /// + /// Debug config name + /// + private const string Debug = "Debug"; + + /// + /// Path to the sdk manifest file + /// + private string _sdkManifestPath = String.Empty; + + /// + /// SDKManifest object encapsulating all the information contained in the manifest xml file + /// + private SDKManifest _sdkManifest = null; + + /// + /// What should happen if this sdk is resolved with other sdks of the same productfamily or same sdk name. + /// + private MultipleVersionSupport _supportsMultipleVersions; + + /// + /// Value of the prefer32Bit property from the project. + /// + private bool _prefer32BitFromProject = false; + + #region Constructor + /// + /// Constructor + /// + public SDKReference(ITaskItem taskItem, string sdkName, string sdkVersion) + { + ErrorUtilities.VerifyThrowArgumentNull(taskItem, "taskItem"); + ErrorUtilities.VerifyThrowArgumentLength(sdkName, "sdkName"); + ErrorUtilities.VerifyThrowArgumentLength(sdkVersion, "sdkVersion"); + + ReferenceItem = taskItem; + SimpleName = sdkName; + Version = sdkVersion; + SDKName = String.Format(CultureInfo.InvariantCulture, "{0}, Version={1}", SimpleName, Version); + FrameworkIdentitiesFromManifest = new Dictionary(StringComparer.OrdinalIgnoreCase); + AppxLocationsFromManifest = new Dictionary(StringComparer.OrdinalIgnoreCase); + ResolutionErrors = new List>(); + ResolutionWarnings = new List>(); + StatusMessages = new List>(); + _supportsMultipleVersions = MultipleVersionSupport.Allow; + } + #endregion + + #region Properties + /// + /// Sdk reference item passed in from the build + /// + public ITaskItem ReferenceItem + { + get; + private set; + } + + /// + /// Parsed simple name + /// + public string SimpleName + { + get; + private set; + } + + /// + /// Parsed version. + /// + public string Version + { + get; + private set; + } + + /// + /// Resolved full path to the root of the sdk. + /// + public string ResolvedPath + { + get; + private set; + } + + /// + /// Has the reference been resolved + /// + public bool Resolved + { + get + { + return !String.IsNullOrEmpty(ResolvedPath); + } + } + + /// + /// Messages which may be warnings or errors depending on the logging setting. + /// + public List> ResolutionErrors + { + get; + private set; + } + + /// + /// Warning messages only + /// + public List> ResolutionWarnings + { + get; + private set; + } + + /// + /// Messages generated during resolution + /// + public List> StatusMessages + { + get; + private set; + } + + /// + /// SDKName, this is a formatted name based on the SimpleName and the Version + /// + public string SDKName + { + get; + private set; + } + + /// + /// Resolved item which will be output by the task. + /// + public ITaskItem ResolvedItem + { + get; + set; + } + + /// + /// SDKType found in the sdk manifest + /// + public SDKType SDKType + { + get; + set; + } + + /// + /// The target platform in the sdk manifest + /// + public string TargetPlatform + { + get; + set; + } + + /// + /// The target platform min version in the sdk manifest + /// + public string TargetPlatformMinVersion + { + get; + set; + } + + /// + /// The target platform max version in the sdk manifest + /// + public string TargetPlatformVersion + { + get; + set; + } + + /// + /// DisplayName found in the sdk manifest + /// + public string DisplayName + { + get; + set; + } + + /// + /// Support Prefer32bit found in the sdk manifest + /// + public string SupportPrefer32Bit + { + get; + set; + } + + /// + /// CopyRedistToSubDirectory specifies where the redist files should be copied to relative to the root of the package. + /// + public string CopyRedistToSubDirectory + { + get; + set; + } + + /// + /// ProductFamilyName specifies the product family for the SDK. This is offered up as metadata on the resolved sdkreference and is used to detect sdk conflicts. + /// + public string ProductFamilyName + { + get; + set; + } + + /// + /// SupportsMultipleVersions specifies what should happen if multiple versions of the product family or sdk name are detected + /// + public MultipleVersionSupport SupportsMultipleVersions + { + get + { + return _supportsMultipleVersions; + } + + set + { + _supportsMultipleVersions = value; + } + } + + /// + /// Supported Architectures is a semicolon delimited list of architectures that the SDK supports. + /// + public string SupportedArchitectures + { + get; + set; + } + + /// + /// DependsOnSDK is a semicolon delimited list of SDK identities that the SDK requires be resolved in order to function. + /// + public string DependsOnSDK + { + get; + set; + } + + /// + /// MaxPlatformVersion as in the manifest + /// + public string MaxPlatformVersion + { + get; + set; + } + + /// + /// MinOSVersion as in the manifest + /// + public string MinOSVersion + { + get; + set; + } + + /// + /// MaxOSVersionTested as in the manifest + /// + public string MaxOSVersionTested + { + get; + set; + } + + /// + /// MoreInfo as in the manifest + /// + public string MoreInfo + { + get; + set; + } + + /// + /// What ever framework identities we found in the manifest. + /// + private Dictionary FrameworkIdentitiesFromManifest + { + get; + set; + } + + /// + /// The frameworkIdentity for the sdk, this may be a single name or a | delimited name + /// + private string FrameworkIdentity + { + get; + set; + } + + /// + /// PlatformIdentity if it exists in the appx manifest for this sdk. + /// + private string PlatformIdentity + { + get; + set; + } + + /// + /// Whatever appx locations we found in the manifest + /// + private Dictionary AppxLocationsFromManifest + { + get; + set; + } + + /// + /// The appxlocation for the sdk can be a single name or a | delimited list + /// + private string AppxLocation + { + get; + set; + } + + #endregion + + #region Methods + + /// + /// Set the location where the reference was resolved. + /// + public void Resolve(Dictionary sdks, string targetConfiguration, string targetArchitecture, HashSet sdkNamesOnReferenceItems, bool treatErrorsAsWarnings, bool prefer32Bit, string identifierTargetPlatform, Version versionTargetPlatform, string projectName, bool enableMaxPlatformVersionEmptyWarning) + { + if (sdks.ContainsKey(SDKName)) + { + _prefer32BitFromProject = prefer32Bit; + + // There must be a trailing slash or else the ExpandSDKReferenceAssemblies will not work. + ResolvedPath = FileUtilities.EnsureTrailingSlash(sdks[SDKName].ItemSpec); + + Version targetPlatformVersionFromItem; + System.Version.TryParse(sdks[SDKName].GetMetadata(SDKPlatformVersion), out targetPlatformVersionFromItem); + + GetSDKManifestAttributes(); + + CreateResolvedReferenceItem(targetConfiguration, targetArchitecture, sdkNamesOnReferenceItems, identifierTargetPlatform, versionTargetPlatform, targetPlatformVersionFromItem, projectName, enableMaxPlatformVersionEmptyWarning); + + // Need to pass these along so we can unroll the platform via GetMatchingPlatformSDK when we get reference files + ResolvedItem.SetMetadata(GetInstalledSDKLocations.DirectoryRootsMetadataName, sdks[SDKName].GetMetadata(GetInstalledSDKLocations.DirectoryRootsMetadataName)); + ResolvedItem.SetMetadata(GetInstalledSDKLocations.ExtensionDirectoryRootsMetadataName, sdks[SDKName].GetMetadata(GetInstalledSDKLocations.ExtensionDirectoryRootsMetadataName)); + ResolvedItem.SetMetadata(GetInstalledSDKLocations.RegistryRootMetadataName, sdks[SDKName].GetMetadata(GetInstalledSDKLocations.RegistryRootMetadataName)); + + if (!treatErrorsAsWarnings && ResolutionErrors.Count > 0) + { + ResolvedPath = String.Empty; + } + } + } + + /// + /// Override object equals to use the equals implementation in this object. + /// + public override bool Equals(object obj) + { + SDKReference reference = obj as SDKReference; + if (reference == null) + { + return false; + } + + return Equals(reference); + } + + /// + /// Override get hash code + /// + public override int GetHashCode() + { + return SimpleName.GetHashCode() ^ Version.GetHashCode(); + } + + /// + /// Are two SDKReference items Equal + /// + public bool Equals(SDKReference other) + { + if (other == null) + { + return false; + } + + if (Object.ReferenceEquals(other, this)) + { + return true; + } + + bool simpleNameMatches = String.Equals(this.SimpleName, other.SimpleName, StringComparison.OrdinalIgnoreCase); + bool versionMatches = Version.Equals(other.Version, StringComparison.OrdinalIgnoreCase); + + return simpleNameMatches && versionMatches; + } + + /// + /// Add a resolution error or warning to the reference + /// + internal void AddResolutionErrorOrWarning(string resourceId, params object[] parameters) + { + ResolutionErrors.Add(new Tuple(resourceId, parameters)); + } + + /// + /// Add a resolution warning to the reference + /// + internal void AddResolutionWarning(string resourceId, params object[] parameters) + { + ResolutionWarnings.Add(new Tuple(resourceId, parameters)); + } + + /// + /// Get a piece of metadata off an item and make sureit is trimmed + /// + private string GetItemMetadataTrimmed(ITaskItem item, string metadataName) + { + string metadataValue = item.GetMetadata(metadataName); + return metadataValue = metadataValue != null ? metadataValue.Trim() : metadataValue; + } + + /// + /// After resolving a reference we need to check to see if there is a SDKManifest file in the root directory and if there is we need to extract the frameworkidentity. + /// We ignore other attributes to leave room for expansion of the file format. + /// + /// + private void GetSDKManifestAttributes() + { + if (_sdkManifest == null) + { + _sdkManifestPath = Path.Combine(ResolvedPath, "SDKManifest.xml"); + + AddStatusMessage("ResolveSDKReference.ReadingSDKManifestFile", _sdkManifestPath); + + _sdkManifest = new SDKManifest(ResolvedPath); + + if (_sdkManifest.ReadError) + { + AddResolutionErrorOrWarning("ResolveSDKReference.ErrorResolvingSDK", ReferenceItem.ItemSpec, ResourceUtilities.FormatResourceString("ResolveSDKReference.ErrorReadingManifest", _sdkManifestPath, _sdkManifest.ReadErrorMessage)); + } + } + + SupportedArchitectures = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.SupportedArchitectures); + if (String.IsNullOrEmpty(SupportedArchitectures)) + { + SupportedArchitectures = _sdkManifest.SupportedArchitectures ?? String.Empty; + } + + DependsOnSDK = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.DependsOnSDK); + if (String.IsNullOrEmpty(DependsOnSDK)) + { + DependsOnSDK = _sdkManifest.DependsOnSDK ?? String.Empty; + } + + FrameworkIdentity = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.FrameworkIdentity); + if (String.IsNullOrEmpty(FrameworkIdentity)) + { + if (_sdkManifest.FrameworkIdentities != null) + { + foreach (string key in _sdkManifest.FrameworkIdentities.Keys) + { + if (!FrameworkIdentitiesFromManifest.ContainsKey(key)) + { + FrameworkIdentitiesFromManifest.Add(key, _sdkManifest.FrameworkIdentities[key]); + } + } + } + + FrameworkIdentity = _sdkManifest.FrameworkIdentity ?? String.Empty; + } + + AppxLocation = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.AppxLocation); + if (String.IsNullOrEmpty(AppxLocation)) + { + if (_sdkManifest.AppxLocations != null) + { + foreach (string key in _sdkManifest.AppxLocations.Keys) + { + if (!AppxLocationsFromManifest.ContainsKey(key)) + { + AppxLocationsFromManifest.Add(key, _sdkManifest.AppxLocations[key]); + } + } + } + } + + PlatformIdentity = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.PlatformIdentity); + if (String.IsNullOrEmpty(PlatformIdentity)) + { + PlatformIdentity = _sdkManifest.PlatformIdentity ?? String.Empty; + } + + MinOSVersion = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.MinOSVersion); + if (String.IsNullOrEmpty(MinOSVersion)) + { + MinOSVersion = _sdkManifest.MinOSVersion ?? String.Empty; + } + + MaxOSVersionTested = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.MaxOSVersionTested); + if (String.IsNullOrEmpty(MaxOSVersionTested)) + { + MaxOSVersionTested = _sdkManifest.MaxOSVersionTested ?? String.Empty; + } + + MoreInfo = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.MoreInfo); + if (String.IsNullOrEmpty(MoreInfo)) + { + MoreInfo = _sdkManifest.MoreInfo ?? String.Empty; + } + + MaxPlatformVersion = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.MaxPlatformVersion); + if (String.IsNullOrEmpty(MaxPlatformVersion)) + { + MaxPlatformVersion = _sdkManifest.MaxPlatformVersion ?? String.Empty; + } + + TargetPlatform = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.TargetPlatform); + if (String.IsNullOrEmpty(TargetPlatform)) + { + TargetPlatform = _sdkManifest.TargetPlatform ?? String.Empty; + } + + TargetPlatformMinVersion = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.TargetPlatformMinVersion); + if (String.IsNullOrEmpty(TargetPlatformMinVersion)) + { + TargetPlatformMinVersion = _sdkManifest.TargetPlatformMinVersion ?? String.Empty; + } + + TargetPlatformVersion = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.TargetPlatformVersion); + if (String.IsNullOrEmpty(TargetPlatformVersion)) + { + TargetPlatformVersion = _sdkManifest.TargetPlatformVersion ?? String.Empty; + } + + string sdkTypeFromMetadata = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.SDKType); + if (String.IsNullOrEmpty(sdkTypeFromMetadata)) + { + SDKType = _sdkManifest.SDKType; + } + else + { + SDKType sdkType = SDKType.Unspecified; + Enum.TryParse(sdkTypeFromMetadata, out sdkType); + SDKType = sdkType; + } + + DisplayName = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.DisplayName); + if (String.IsNullOrEmpty(DisplayName)) + { + DisplayName = _sdkManifest.DisplayName ?? String.Empty; + } + + SupportPrefer32Bit = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.SupportPrefer32Bit); + if (String.IsNullOrEmpty(SupportPrefer32Bit)) + { + SupportPrefer32Bit = _sdkManifest.SupportPrefer32Bit ?? String.Empty; + } + + CopyRedistToSubDirectory = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.CopyRedistToSubDirectory); + if (String.IsNullOrEmpty(CopyRedistToSubDirectory)) + { + CopyRedistToSubDirectory = _sdkManifest.CopyRedistToSubDirectory ?? String.Empty; + } + + ProductFamilyName = GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.ProductFamilyName); + if (String.IsNullOrEmpty(ProductFamilyName)) + { + ProductFamilyName = _sdkManifest.ProductFamilyName ?? String.Empty; + } + + if (!ParseSupportMultipleVersions(GetItemMetadataTrimmed(ReferenceItem, SDKManifest.Attributes.SupportsMultipleVersions))) + { + _supportsMultipleVersions = _sdkManifest.SupportsMultipleVersions; + } + } + + /// + /// Parse the multipleversions string and set supportsMultipleVersions if it can be parsed correctly. + /// + private bool ParseSupportMultipleVersions(string multipleVersionsValue) + { + return !String.IsNullOrEmpty(multipleVersionsValue) && Enum.TryParse(multipleVersionsValue, /*ignoreCase*/true, out _supportsMultipleVersions); + } + + /// + /// Create a resolved output item which contains the path to the SDK and the associated metadata about it. + /// + private void CreateResolvedReferenceItem(string targetConfiguration, string targetArchitecture, HashSet sdkNamesOnReferenceItems, string targetPlatformIdentifier, Version targetPlatformVersion, Version targetPlatformVersionFromItem, string projectName, bool enableMaxPlatformVersionEmptyWarning) + { + // Make output item to send to the project file which represents a resolve SDKReference + ResolvedItem = new TaskItem(ResolvedPath); + ResolvedItem.SetMetadata("SDKName", SDKName); + + if (!String.IsNullOrEmpty(ProductFamilyName)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.ProductFamilyName, ProductFamilyName); + } + + // Copy existing metadata onto the output item + ReferenceItem.CopyMetadataTo(ResolvedItem); + + ResolvedItem.SetMetadata("SupportsMultipleVersions", _supportsMultipleVersions.ToString()); + + // If no architecture and configuration is passed in then default to retail neutral + targetArchitecture = String.IsNullOrEmpty(targetArchitecture) ? NeutralArch : targetArchitecture; + targetConfiguration = String.IsNullOrEmpty(targetConfiguration) ? Retail : targetConfiguration; + + // Check to see if there was metadata on the original reference item, if there is then that wins. + string sdkConfiguration = ReferenceItem.GetMetadata(TargetedSDKConfiguration); + sdkConfiguration = sdkConfiguration.Length > 0 ? sdkConfiguration : targetConfiguration; + + string sdkArchitecture = ReferenceItem.GetMetadata(TargetedSDKArchitecture).Length > 0 ? ReferenceItem.GetMetadata(TargetedSDKArchitecture) : targetArchitecture; + sdkArchitecture = sdkArchitecture.Length > 0 ? sdkArchitecture : targetArchitecture; + + // Configuration is somewhat special, if Release is passed in me want to convert it to Retail and set that on the resulting output item. + sdkConfiguration = sdkConfiguration.Equals("Release", StringComparison.OrdinalIgnoreCase) ? Retail : sdkConfiguration; + + sdkArchitecture = sdkArchitecture.Equals("msil", StringComparison.OrdinalIgnoreCase) ? NeutralArch : sdkArchitecture; + sdkArchitecture = sdkArchitecture.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) ? NeutralArch : sdkArchitecture; + sdkArchitecture = sdkArchitecture.Equals("Any CPU", StringComparison.OrdinalIgnoreCase) ? NeutralArch : sdkArchitecture; + sdkArchitecture = sdkArchitecture.Equals("amd64", StringComparison.OrdinalIgnoreCase) ? X64Arch : sdkArchitecture; + + ResolvedItem.SetMetadata(TargetedSDKConfiguration, sdkConfiguration); + ResolvedItem.SetMetadata(TargetedSDKArchitecture, sdkArchitecture); + + // Print out a message indicating what our targeted sdk configuration and architecture is so users know what the reference is targeting. + AddStatusMessage("ResolveSDKReference.TargetedConfigAndArchitecture", sdkConfiguration, sdkArchitecture); + + string[] supportedArchitectures = null; + if (SupportedArchitectures != null && SupportedArchitectures.Length > 0) + { + supportedArchitectures = SupportedArchitectures.Split(s_supportedArchitecturesSplitChars, StringSplitOptions.RemoveEmptyEntries); + } + + if (supportedArchitectures != null) + { + bool foundTargetArchitecture = false; + + // SupportedArchitectures will usually only contain a handful of elements therefore putting this into a hashtable or dictionary would not likely give us much performance improvement. + foreach (string architecture in supportedArchitectures) + { + if (architecture.Equals(sdkArchitecture, StringComparison.OrdinalIgnoreCase)) + { + foundTargetArchitecture = true; + break; + } + } + + if (!foundTargetArchitecture) + { + string remappedArchitecture = sdkArchitecture.Equals(NeutralArch, StringComparison.OrdinalIgnoreCase) ? AnyCPUArch : sdkArchitecture; + string supportedArchList = String.Empty; + + for (int i = 0; i < supportedArchitectures.Length; i++) + { + supportedArchList += supportedArchitectures[i].Equals(NeutralArch, StringComparison.OrdinalIgnoreCase) ? AnyCPUArch : supportedArchitectures[i]; + + // only put a comma after the first if there is more that one and do not put one after the end + if (supportedArchitectures.Length > 1 && i != supportedArchitectures.Length - 1) + { + supportedArchList += SupportedArchitectureJoinDelimiter; + } + } + + AddResolutionErrorOrWarning("ResolveSDKReference.TargetArchitectureNotSupported", remappedArchitecture, SDKName, supportedArchList); + } + } + + if (!String.IsNullOrEmpty(MaxPlatformVersion)) + { + Version maxPlatformVersionAsVersion; + + if (System.Version.TryParse(MaxPlatformVersion, out maxPlatformVersionAsVersion) && (maxPlatformVersionAsVersion < targetPlatformVersion)) + { + AddResolutionWarning("ResolveSDKReference.MaxPlatformVersionLessThanTargetPlatformVersion", projectName, DisplayName, Version, targetPlatformIdentifier, MaxPlatformVersion, targetPlatformIdentifier, targetPlatformVersion.ToString()); + } + } + else if (enableMaxPlatformVersionEmptyWarning && targetPlatformVersionFromItem != null && targetPlatformVersionFromItem < targetPlatformVersion) + { + AddResolutionWarning("ResolveSDKReference.MaxPlatformVersionNotSpecified", projectName, DisplayName, Version, targetPlatformIdentifier, targetPlatformVersionFromItem.ToString(), targetPlatformIdentifier, targetPlatformVersion.ToString()); + } + + if (!String.IsNullOrEmpty(TargetPlatform) && !String.Equals(targetPlatformIdentifier, TargetPlatform)) + { + AddResolutionErrorOrWarning("ResolveSDKReference.TargetPlatformIdentifierDoesNotMatch", projectName, DisplayName, Version, targetPlatformIdentifier, TargetPlatform); + } + + if (!String.IsNullOrEmpty(TargetPlatformMinVersion)) + { + Version targetPlatformMinVersionAsVersion; + + if (System.Version.TryParse(TargetPlatformMinVersion, out targetPlatformMinVersionAsVersion) && (targetPlatformVersion < targetPlatformMinVersionAsVersion)) + { + AddResolutionErrorOrWarning("ResolveSDKReference.PlatformVersionIsLessThanMinVersion", projectName, DisplayName, Version, targetPlatformVersion.ToString(), targetPlatformMinVersionAsVersion.ToString()); + } + } + + if (String.Equals(NeutralArch, sdkArchitecture, StringComparison.OrdinalIgnoreCase) && !String.IsNullOrEmpty(SupportPrefer32Bit) && _prefer32BitFromProject) + { + bool supportPrefer32Bit = true; + bool.TryParse(SupportPrefer32Bit, out supportPrefer32Bit); + + if (!supportPrefer32Bit) + { + AddResolutionErrorOrWarning("ResolveSDKReference.Prefer32BitNotSupportedWithNeutralProject", SDKName); + } + } + + // The SDKManifest may have had a number of frameworkidentity entries inside of it. We want to match the one + // which has the correct configuration and architecture. If a perfect match cannot be found + // then we will look for ones that declare only the configuration. If that cannot be found we just try and find an element that only is "FrameworkIdentity". + if (String.IsNullOrEmpty(FrameworkIdentity)) + { + if (FrameworkIdentitiesFromManifest.Count > 0) + { + // Try and find a framework identity that matches on both the configuration and architecture "FrameworkIdentity--" + FrameworkIdentity = null; + string frameworkIdentityKey = String.Format(CultureInfo.InvariantCulture, "{0}-{1}-{2}", SDKManifest.Attributes.FrameworkIdentity, sdkConfiguration, sdkArchitecture); + FrameworkIdentity = FindFrameworkIdentity(frameworkIdentityKey); + + // Try and find a framework identity that matches on the configuration , Element must be named "FrameworkIdentity-" only. + if (FrameworkIdentity == null) + { + frameworkIdentityKey = String.Format(CultureInfo.InvariantCulture, "{0}-{1}", SDKManifest.Attributes.FrameworkIdentity, sdkConfiguration); + FrameworkIdentity = FindFrameworkIdentity(frameworkIdentityKey); + } + + // See if there is an element just called "FrameworkIdentity" + if (FrameworkIdentity == null) + { + frameworkIdentityKey = SDKManifest.Attributes.FrameworkIdentity; + FrameworkIdentity = FindFrameworkIdentity(frameworkIdentityKey); + } + + if (FrameworkIdentity == null) + { + AddResolutionErrorOrWarning("ResolveSDKReference.NoMatchingFrameworkIdentity", _sdkManifestPath, sdkConfiguration, sdkArchitecture); + } + else + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.FrameworkIdentity, FrameworkIdentity); + } + } + else + { + AddStatusMessage("ResolveSDKReference.NoFrameworkIdentitiesFound"); + } + } + else + { + AddStatusMessage("ResolveSDKReference.FoundFrameworkIdentity", FrameworkIdentity); + } + + // Print out if we are a platform SDK + if (!String.IsNullOrEmpty(PlatformIdentity)) + { + AddStatusMessage("ResolveSDKReference.PlatformSDK", PlatformIdentity); + ResolvedItem.SetMetadata(SDKManifest.Attributes.PlatformIdentity, PlatformIdentity); + } + + // The SDKManifest may have had a number of AppxLocation entries inside of it. We want to return the set of unique architectures for a selected configuration. + if (String.IsNullOrEmpty(AppxLocation)) + { + if (AppxLocationsFromManifest.Count > 0) + { + AppxLocation = null; + + // For testing especially it's nice to have a set order of what the generated appxlocation string will be at the end + SortedDictionary architectureLocations = new SortedDictionary(StringComparer.InvariantCultureIgnoreCase); + List appxLocationComponents = new List(); + + foreach (var appxLocation in AppxLocationsFromManifest) + { + if (!String.IsNullOrEmpty(appxLocation.Key)) + { + string[] appxComponents = appxLocation.Key.Split(s_appxSplitChar, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); + + // The first component needs to be appx + if (!String.Equals("Appx", appxComponents[0], StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string configurationComponent = null; + string architectureComponent = null; + switch (appxComponents.Length) + { + case 1: + architectureComponent = NeutralArch; + break; + case 2: + configurationComponent = appxComponents[1]; + architectureComponent = NeutralArch; + + // If the configuration is not debug or retail then we will assume it is an architecture + if (!(configurationComponent.Equals(Debug, StringComparison.OrdinalIgnoreCase) || configurationComponent.Equals(Retail, StringComparison.OrdinalIgnoreCase))) + { + configurationComponent = null; + architectureComponent = appxComponents[1]; + } + + break; + case 3: + configurationComponent = appxComponents[1]; + architectureComponent = appxComponents[2]; + break; + default: + // Not one of the cases we expect, just skip it + continue; + } + + bool containsKey = architectureLocations.ContainsKey(architectureComponent); + + // If we have not seen this architecture before (and it has a compatible configuration with what we are targeting) then add it. + // Also, replace the entry if we have already added an entry for a non configuration specific entry and we now have a configuration specific entry that matches what we are targeting. + if ((configurationComponent == null && !containsKey) || (configurationComponent != null && configurationComponent.Equals(sdkConfiguration, StringComparison.OrdinalIgnoreCase))) + { + AddStatusMessage("ResolveSDKReference.FoundAppxLocation", appxLocation.Key + "=" + appxLocation.Value); + + if (containsKey) + { + AddStatusMessage("ResolveSDKReference.ReplaceAppxLocation", architectureComponent, architectureLocations[architectureComponent], appxLocation.Value); + } + + architectureLocations[architectureComponent] = appxLocation.Value; + } + } + } + + foreach (var location in architectureLocations) + { + appxLocationComponents.Add(location.Key); + appxLocationComponents.Add(location.Value); + } + + if (appxLocationComponents.Count > 0) + { + AppxLocation = String.Join("|", appxLocationComponents.ToArray()); + } + + if (AppxLocation == null) + { + AddResolutionErrorOrWarning("ResolveSDKReference.NoMatchingAppxLocation", _sdkManifestPath, sdkConfiguration, sdkArchitecture); + } + else + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.AppxLocation, AppxLocation); + } + } + else + { + AddStatusMessage("ResolveSDKReference.NoAppxLocationsFound"); + } + } + else + { + AddStatusMessage("ResolveSDKReference.FoundAppxLocation", AppxLocation); + } + + ResolvedItem.SetMetadata("SimpleName", SimpleName); + ResolvedItem.SetMetadata("Version", Version); + + // Check to see if the copy local metadata has been set in the project file. + bool result; + bool hasExpandReferenceAssemblies = bool.TryParse(ReferenceItem.GetMetadata(SDKManifest.Attributes.ExpandReferenceAssemblies), out result); + bool hasCopyRedist = bool.TryParse(ReferenceItem.GetMetadata(SDKManifest.Attributes.CopyRedist), out result); + bool hasCopyLocalExpandedReferenceAssemblies = bool.TryParse(ReferenceItem.GetMetadata(SDKManifest.Attributes.CopyLocalExpandedReferenceAssemblies), out result); + + bool referenceItemHasSDKName = sdkNamesOnReferenceItems.Contains(SDKName); + + if (SDKType != SDKType.Unspecified) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.SDKType, SDKType.ToString()); + } + + if (!String.IsNullOrEmpty(DisplayName)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.DisplayName, DisplayName); + } + + // Could be null or empty depending if blank metadata was set or not. + bool frameworkSDK = SDKType == SDKType.Framework || !String.IsNullOrEmpty(FrameworkIdentity); + bool hasPlatformIdentity = SDKType == SDKType.Platform || !String.IsNullOrEmpty(PlatformIdentity); + + if (!hasExpandReferenceAssemblies) + { + if (referenceItemHasSDKName) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.ExpandReferenceAssemblies, "false"); + } + else + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.ExpandReferenceAssemblies, "true"); + } + } + + if (!hasCopyRedist) + { + if (frameworkSDK || hasPlatformIdentity) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.CopyRedist, "false"); + } + else + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.CopyRedist, "true"); + } + } + + if (!hasCopyLocalExpandedReferenceAssemblies) + { + if (frameworkSDK || referenceItemHasSDKName || hasPlatformIdentity) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.CopyLocalExpandedReferenceAssemblies, "false"); + } + else + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.CopyLocalExpandedReferenceAssemblies, "true"); + } + } + + if (!String.IsNullOrEmpty(CopyRedistToSubDirectory)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.CopyRedistToSubDirectory, CopyRedistToSubDirectory); + } + + if (!String.IsNullOrEmpty(MaxPlatformVersion)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.MaxPlatformVersion, MaxPlatformVersion); + } + + if (!String.IsNullOrEmpty(MinOSVersion)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.MinOSVersion, MinOSVersion); + } + + if (!String.IsNullOrEmpty(MaxOSVersionTested)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.MaxOSVersionTested, MaxOSVersionTested); + } + + if (!String.IsNullOrEmpty(MoreInfo)) + { + ResolvedItem.SetMetadata(SDKManifest.Attributes.MoreInfo, MoreInfo); + } + } + + /// + /// Check to see if an FrameworkIdentity is in the list of framework identities found in the SDKManifest. + /// + private string FindFrameworkIdentity(string frameworkIdentityKey) + { + string frameworkIdentityValue = null; + if (FrameworkIdentitiesFromManifest.ContainsKey(frameworkIdentityKey)) + { + frameworkIdentityValue = FrameworkIdentitiesFromManifest[frameworkIdentityKey]; + AddStatusMessage("ResolveSDKReference.FoundFrameworkIdentity", frameworkIdentityValue); + } + else + { + AddStatusMessage("ResolveSDKReference.CouldNotFindFrameworkIdentity", frameworkIdentityKey); + } + + return frameworkIdentityValue; + } + + /// + /// Keep track of messages which are status information about resolving this reference. We want to print it out in a nicer format at the end of resolution. + /// + private void AddStatusMessage(string resource, params object[] parameters) + { + StatusMessages.Add(new Tuple(resource, parameters)); + } + #endregion + } + } +} diff --git a/src/XMakeTasks/SGen.cs b/src/XMakeTasks/SGen.cs new file mode 100644 index 00000000000..64ac9facaef --- /dev/null +++ b/src/XMakeTasks/SGen.cs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Genererates a serialization assembly containing XML serializers for the input assembly. + /// + public class SGen : ToolTaskExtension + { + private string _buildAssemblyPath; + #region Properties + + // Input files + [Required] + public string BuildAssemblyName + { + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "BuildAssemblyName"); + Bag["BuildAssemblyName"] = value; + } + get + { + return (string)Bag["BuildAssemblyName"]; + } + } + + [Required] + public string BuildAssemblyPath + { + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "BuildAssemblyPath"); + _buildAssemblyPath = value; + } + + get + { + string thisPath = null; + try + { + thisPath = Path.GetFullPath(_buildAssemblyPath); + } + catch (Exception e) + { + if (!ExceptionHandling.NotExpectedException(e)) + { + // If it is an Expected Exception log the error + Log.LogErrorWithCodeFromResources("SGen.InvalidPath", "BuildAssemblyPath", e.Message); + } + + throw; + } + + return thisPath; + } + } + + [Required] + public bool ShouldGenerateSerializer + { + set + { + Bag["ShouldGenerateSerializer"] = value; + } + get + { + return GetBoolParameterWithDefault("ShouldGenerateSerializer", false); + } + } + + [Required] + public bool UseProxyTypes + { + set + { + Bag["UseProxyTypes"] = value; + } + get + { + return GetBoolParameterWithDefault("UseProxyTypes", false); + } + } + + + public bool UseKeep + { + set + { + Bag["UseKeep"] = value; + } + get + { + return GetBoolParameterWithDefault("UseKeep", false); + } + } + + public string[] References + { + set + { + Bag["References"] = value; + } + get + { + return (string[])Bag["References"]; + } + } + + public string KeyContainer + { + set + { + Bag["KeyContainer"] = value; + } + get + { + return (string)Bag["KeyContainer"]; + } + } + + public string KeyFile + { + set + { + Bag["KeyFile"] = value; + } + get + { + return (string)Bag["KeyFile"]; + } + } + + public bool DelaySign + { + set + { + Bag["DelaySign"] = value; + } + get + { + return GetBoolParameterWithDefault("DelaySign", false); + } + } + + [Output] + public ITaskItem[] SerializationAssembly + { + set { Bag["SerializationAssembly"] = value; } + get { return (ITaskItem[])Bag["SerializationAssembly"]; } + } + + public string SerializationAssemblyName + { + get + { + Debug.Assert(BuildAssemblyName.Length > 0, "Build assembly name is blank"); + string prunedAssemblyName = null; + try + { + prunedAssemblyName = Path.GetFileNameWithoutExtension(BuildAssemblyName); + } + catch (ArgumentException e) + { + Log.LogErrorWithCodeFromResources("SGen.InvalidPath", "BuildAssemblyName", e.Message); + throw; + } + prunedAssemblyName += ".XmlSerializers.dll"; + return prunedAssemblyName; + } + } + + private string SerializationAssemblyPath + { + get + { + Debug.Assert(BuildAssemblyPath.Length > 0, "Build assembly path is blank"); + return Path.Combine(BuildAssemblyPath, SerializationAssemblyName); + } + } + + private string AssemblyFullPath + { + get + { + return Path.Combine(BuildAssemblyPath, BuildAssemblyName); + } + } + + public string SdkToolsPath + { + set { Bag["SdkToolsPath"] = value; } + get { return (string)Bag["SdkToolsPath"]; } + } + + /// + /// Gets or Sets the Compiler Platform used by SGen to generate the output assembly. + /// + public string Platform + { + set { Bag["Platform"] = value; } + get { return (string)Bag["Platform"]; } + } + + /// + /// Gets or Sets a list of specific Types to generate serialization code for, SGen will generate serialization code only for those types. + /// + public string[] Types + { + set { Bag["Types"] = value; } + get { return (string[])Bag["Types"]; } + } + + #endregion + + #region Tool Members + /// + /// The name of the tool to execute. + /// + override protected string ToolName + { + get + { + return "sgen.exe"; + } + } + + /// + /// The full path of the tool to execute. + /// + override protected string GenerateFullPathToTool() + { + string pathToTool = null; + + // If COMPLUS_InstallRoot\COMPLUS_Version are set (the dogfood world), we want to find it there, instead of + // the SDK, which may or may not be installed. The following will look there. + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("COMPLUS_InstallRoot")) || !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("COMPLUS_Version"))) + { + pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest); + } + + if (String.IsNullOrEmpty(pathToTool) || !File.Exists(pathToTool)) + { + pathToTool = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, Microsoft.Build.Utilities.ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolName, Log, true); + } + + return pathToTool; + } + + /// + /// Validate parameters, log errors and warnings and return true if Execute should proceed. + /// + override protected bool ValidateParameters() + { + // Ensure the references exist before passing them to SGen.exe + if (References != null) + { + foreach (string reference in References) + { + if (!File.Exists(reference)) + { + Log.LogErrorWithCodeFromResources("SGen.ResourceNotFound", reference); + return false; + } + } + } + + return true; + } + + /// + /// Returns true if task execution is not necessary. Executed after ValidateParameters + /// + /// + override protected bool SkipTaskExecution() + { + return SerializationAssembly == null && !ShouldGenerateSerializer; + } + + /// + /// Returns a string with those switches and other information that can't go into a response file and + /// must go directly onto the command line. + /// Called after ValidateParameters and SkipTaskExecution + /// + override protected string GenerateCommandLineCommands() + { + CommandLineBuilderExtension commandLineBuilder = new CommandLineBuilderExtension(); + bool serializationAssemblyPathExists = false; + try + { + if (SerializationAssembly == null) + { + Debug.Assert(ShouldGenerateSerializer, "GenerateCommandLineCommands() should not be called if ShouldGenerateSerializer is true and SerializationAssembly is null."); + + SerializationAssembly = new TaskItem[] { new TaskItem(SerializationAssemblyPath) }; + } + + // Add the assembly switch + commandLineBuilder.AppendSwitchIfNotNull("/assembly:", AssemblyFullPath); + + commandLineBuilder.AppendWhenTrue("/proxytypes", Bag, "UseProxyTypes"); + + //add the keep switch + commandLineBuilder.AppendWhenTrue("/keep", Bag, "UseKeep"); + + // Append the references, if any. + if (References != null) + { + foreach (string reference in References) + { + commandLineBuilder.AppendSwitchIfNotNull("/reference:", reference); + } + } + + //Append the Types to the command line, if any. + if (Types != null) + { + foreach (string type in Types) + { + commandLineBuilder.AppendSwitchIfNotNull("/type:", type); + } + } + + // The arguments to the "/compiler" switch are themselves switches to be passed to + // the compiler when generating the serialization assembly. + + // Add the compiler command switches for strong naming on the serialization assembly + if (KeyFile != null) + { + commandLineBuilder.AppendNestedSwitch("/compiler:", "/keyfile:", KeyFile); + } + else if (KeyContainer != null) + { + commandLineBuilder.AppendNestedSwitch("/compiler:", "/keycontainer:", KeyContainer); + } + + commandLineBuilder.AppendPlusOrMinusSwitch("/compiler:/delaysign", Bag, "DelaySign"); + + // Add the Platform switch to the compiler. + if (Platform != null) + { + commandLineBuilder.AppendNestedSwitch("/compiler:", "/platform:", Platform); + } + + serializationAssemblyPathExists = File.Exists(SerializationAssemblyPath); + } + catch (Exception e) + { + // If it is not an expected exception rethrow the exception. + // Ignore the expected exceptions because they have already been logged + if (ExceptionHandling.NotExpectedException(e)) + { + throw; + } + } + + // Delete the assembly if it already exists. + if (serializationAssemblyPathExists) + { + try + { + File.Delete(SerializationAssemblyPath); + } + // Of all of the exceptions that can be thrown on a File.Delete, the only ones we need to + // be immediately concerned with are the UnauthorizedAccessException and the IOException + // (file is in use exception). We need to make sure that the assembly is gone before we + // try to produce a new one because it is possible that after some changes were made to the + // base assembly, there will, in fact, not be a serialization assembly produced. We cannot + // leave the earlier produced assembly around to be propagated by later processes. + catch (UnauthorizedAccessException e) + { + Log.LogErrorWithCodeFromResources("SGen.CouldNotDeleteSerializer", SerializationAssemblyPath, e.Message); + } + catch (IOException e) + { + Log.LogErrorWithCodeFromResources("SGen.CouldNotDeleteSerializer", SerializationAssemblyPath, e.Message); + } + // The DirectoryNotFoundException is safely ignorable since that means that there is no + // existing serialization assembly. This would be extremely unlikely anyway because we + // found the serializer just a couple of milliseconds ago. + } + + return commandLineBuilder.ToString(); + } + + #endregion + } +} + diff --git a/src/XMakeTasks/SdkToolsPathUtility.cs b/src/XMakeTasks/SdkToolsPathUtility.cs new file mode 100644 index 00000000000..95bc800eb62 --- /dev/null +++ b/src/XMakeTasks/SdkToolsPathUtility.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Provide a helper class for tasks to find their tools if they are in the SDK +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class will provide the ability for classes given an SdkToolsPath and their tool name to find that tool. + /// The tool will be looked for either under the SDKToolPath passed into the task or as fallback to look for the toolname using the toolslocation helper. + /// + internal static class SdkToolsPathUtility + { + /// + /// Cache the file exists delegate which will determine if a file exists or not but will not eat the CAS exceptions. + /// + private static FileExists s_fileInfoExists; + + /// + /// Provide a delegate which will do the correct file exists. + /// + internal static FileExists FileInfoExists + { + get + { + if (s_fileInfoExists == null) + { + s_fileInfoExists = new FileExists(FileExists); + } + + return s_fileInfoExists; + } + } + + /// + /// This method will take a sdkToolsPath and a toolName and return the path to the tool if it is found and exists. + /// + /// First the method will try and find the tool under the sdkToolsPath taking into account the current processor architecture + /// If the tool could not be found the method will try and find the tool under the sdkToolsPath (which should point to the x86 sdk directory). + /// + /// Finally if the method has not found the tool yet it will fallback and use the toolslocation helper method to try and find the tool. + /// + /// Path including the toolName of the tool if found, null if it is not found + internal static string GeneratePathToTool(FileExists fileExists, string currentArchitecture, string sdkToolsPath, string toolName, TaskLoggingHelper log, bool logErrorsAndWarnings) + { + // Null until we combine the toolname with the path. + string pathToTool = null; + if (!String.IsNullOrEmpty(sdkToolsPath)) + { + string processorSpecificToolDirectory = String.Empty; + try + { + switch (currentArchitecture) + { + // There may not be an arm directory so we will fall back to the x86 tool location + // but if there is then we should try and use it. + case ProcessorArchitecture.ARM: + processorSpecificToolDirectory = Path.Combine(sdkToolsPath, "arm"); + break; + case ProcessorArchitecture.AMD64: + processorSpecificToolDirectory = Path.Combine(sdkToolsPath, "x64"); + break; + case ProcessorArchitecture.IA64: + processorSpecificToolDirectory = Path.Combine(sdkToolsPath, "ia64"); + break; + case ProcessorArchitecture.X86: + default: + processorSpecificToolDirectory = sdkToolsPath; + break; + } + + pathToTool = Path.Combine(processorSpecificToolDirectory, toolName); + + if (!fileExists(pathToTool)) + { + // Try falling back to the x86 location + if (currentArchitecture != ProcessorArchitecture.X86) + { + pathToTool = Path.Combine(sdkToolsPath, toolName); + } + } + else + { + return pathToTool; + } + } + catch (ArgumentException e) + { + // Catch exceptions from path.combine + log.LogErrorWithCodeFromResources("General.SdkToolsPathError", toolName, e.Message); + return null; + } + + if (fileExists(pathToTool)) + { + return pathToTool; + } + else + { + if (logErrorsAndWarnings) + { + // Log an error indicating we could not find it in the processor specific architecture or x86 locations. + // We could not find the tool at all, lot a error. + log.LogWarningWithCodeFromResources("General.PlatformSDKFileNotFoundSdkToolsPath", toolName, processorSpecificToolDirectory, sdkToolsPath); + } + } + } + else + { + if (logErrorsAndWarnings) + { + log.LogMessageFromResources(MessageImportance.Low, "General.SdkToolsPathNotSpecifiedOrToolDoesNotExist", toolName, sdkToolsPath); + } + } + + // Fall back and see if we can find it with the toolsLocation helper methods. This is not optimal because + // the location they are looking at is based on when the Microsoft.Build.Utilities.dll was compiled + // but it is better than nothing. + if (null == pathToTool || !fileExists(pathToTool)) + { + pathToTool = FindSDKToolUsingToolsLocationHelper(toolName); + + if (pathToTool == null && logErrorsAndWarnings) + { + log.LogErrorWithCodeFromResources("General.SdkToolsPathToolDoesNotExist", toolName, sdkToolsPath, ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.VersionLatest, VisualStudioVersion.VersionLatest)); + } + } + + return pathToTool; + } + + /// + /// This method will take the toolName and use the Legacy ToolLocation helper methods to try and find the tool. + /// This is a last ditch effort to find the tool when we cannot find it using the passed in SDKToolsPath (in either the x86 or processor specific directories). + /// + /// Name of the tool to find the sdk path for + /// A path to the tool or null if the path does not exist. + internal static string FindSDKToolUsingToolsLocationHelper(string toolName) + { + // If it isn't there, we should find it in the SDK based on the version compiled into the utilities + string pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(toolName, TargetDotNetFrameworkVersion.VersionLatest, VisualStudioVersion.VersionLatest); + return pathToTool; + } + + /// + /// Provide a method which can be used with a delegate to provide a specific FileExists behavior. + /// + /// Use FileInfo instead of File.Exists(...) because the latter fails silently (by design) if CAS + /// doesn't grant access. We want the security exception if there is going to be one. + /// + /// True if the file exists. False if it does not + private static bool FileExists(string filePath) + { + return new FileInfo(filePath).Exists; + } + } +} diff --git a/src/XMakeTasks/SignFile.cs b/src/XMakeTasks/SignFile.cs new file mode 100644 index 00000000000..1d23e9142db --- /dev/null +++ b/src/XMakeTasks/SignFile.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#region Using directives +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Security; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.ComponentModel; + + +#endregion +namespace Microsoft.Build.Tasks +{ + /// + /// This task signs the passed in file using the Authenticode cert + /// provided and optionally uses a timestamp if a URL is provided. + /// It can sign ClickOnce manifests as well as exe's. + /// + public sealed class SignFile : Task + { + public SignFile() + : base(AssemblyResources.PrimaryResources, "MSBuild.") + { + } + + private string _certificateThumbprint; + private ITaskItem _sigingTarget; + private string _targetFrameVersion; + private string _timestampUrl; + + [Required()] + public string CertificateThumbprint + { + get { return _certificateThumbprint; } + set { _certificateThumbprint = value; } + } + + [Required()] + public ITaskItem SigningTarget + { + get { return _sigingTarget; } + set { _sigingTarget = value; } + } + + public String TargetFrameworkVersion + { + get { return _targetFrameVersion; } + set { _targetFrameVersion = value; } + } + + public string TimestampUrl + { + get { return _timestampUrl; } + set { _timestampUrl = value; } + } + + public override bool Execute() + { + try + { + SecurityUtilities.SignFile(CertificateThumbprint, + TimestampUrl == null ? null : new Uri(TimestampUrl), + SigningTarget.ItemSpec, TargetFrameworkVersion); + return true; + } + catch (ArgumentException ex) + { + if (ex.ParamName.Equals("certThumbprint")) + { + Log.LogErrorWithCodeFromResources("SignFile.CertNotInStore"); + return false; + } + else + throw; + } + catch (FileNotFoundException ex) + { + Log.LogErrorWithCodeFromResources("SignFile.TargetFileNotFound", ex.FileName); + return false; + } + catch (ApplicationException ex) + { + Log.LogErrorWithCodeFromResources("SignFile.SignToolError", ex.Message.Trim()); + return false; + } + catch (WarningException ex) + { + Log.LogWarningWithCodeFromResources("SignFile.SignToolWarning", ex.Message.Trim()); + return true; + } + catch (CryptographicException ex) + { + Log.LogErrorWithCodeFromResources("SignFile.SignToolError", ex.Message.Trim()); + return false; + } + catch (Win32Exception ex) + { + Log.LogErrorWithCodeFromResources("SignFile.SignToolError", ex.Message.Trim()); + return false; + } + catch (UriFormatException ex) + { + Log.LogErrorWithCodeFromResources("SignFile.SignToolError", ex.Message.Trim()); + return false; + } + } + } +} diff --git a/src/XMakeTasks/StateFileBase.cs b/src/XMakeTasks/StateFileBase.cs new file mode 100644 index 00000000000..f5b431e4599 --- /dev/null +++ b/src/XMakeTasks/StateFileBase.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Base class for task state files. + /// + [Serializable()] + internal class StateFileBase + { + /// + /// Default constructor + /// + internal StateFileBase() + { + // do nothing + } + + /// + /// Writes the contents of this object out to the specified file. + /// + /// + virtual internal void SerializeCache(string stateFile, TaskLoggingHelper log) + { + try + { + if (stateFile != null && stateFile.Length > 0) + { + if (File.Exists(stateFile)) + { + File.Delete(stateFile); + } + + using (FileStream s = new FileStream(stateFile, FileMode.CreateNew)) + { + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(s, this); + } + } + } + catch (Exception e) + { + // If there was a problem writing the file (like it's read-only or locked on disk, for + // example), then eat the exception and log a warning. Otherwise, rethrow. + if (ExceptionHandling.NotExpectedSerializationException(e)) + throw; + + // Not being able to serialize the cache is not an error, but we let the user know anyway. + // Don't want to hold up processing just because we couldn't read the file. + log.LogWarningWithCodeFromResources("General.CouldNotWriteStateFile", stateFile, e.Message); + } + } + + /// + /// Reads the specified file from disk into a StateFileBase derived object. + /// + /// + /// + static internal StateFileBase DeserializeCache(string stateFile, TaskLoggingHelper log, Type requiredReturnType) + { + StateFileBase retVal = null; + + // First, we read the cache from disk if one exists, or if one does not exist + // then we create one. + try + { + if (stateFile != null && stateFile.Length > 0 && File.Exists(stateFile)) + { + using (FileStream s = new FileStream(stateFile, FileMode.Open)) + { + BinaryFormatter formatter = new BinaryFormatter(); + object deserializedObject = formatter.Deserialize(s); + retVal = deserializedObject as StateFileBase; + + // If the deserialized object is null then there would be no cast error but retVal would still be null + // only log the message if there would have been a cast error + if (retVal == null && deserializedObject != null) + { + // When upgrading to Visual Studio 2008 and running the build for the first time the resource cache files are replaced which causes a cast error due + // to a new version number on the tasks class. "Unable to cast object of type 'Microsoft.Build.Tasks.SystemState' to type 'Microsoft.Build.Tasks.StateFileBase”. + // If there is an invalid cast, a message rather than a warning should be emitted. + log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, log.FormatResourceString("General.IncompatibleStateFileType")); + } + + if ((retVal != null) && (!requiredReturnType.IsInstanceOfType(retVal))) + { + log.LogWarningWithCodeFromResources("General.CouldNotReadStateFile", stateFile, + log.FormatResourceString("General.IncompatibleStateFileType")); + retVal = null; + } + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // The deserialization process seems like it can throw just about + // any exception imaginable. Catch them all here. + // Not being able to deserialize the cache is not an error, but we let the user know anyway. + // Don't want to hold up processing just because we couldn't read the file. + log.LogWarningWithCodeFromResources("General.CouldNotReadStateFile", stateFile, e.Message); + } + + return retVal; + } + + /// + /// Deletes the state file from disk + /// + /// + /// + static internal void DeleteFile(string stateFile, TaskLoggingHelper log) + { + try + { + if (stateFile != null && stateFile.Length > 0) + { + if (File.Exists(stateFile)) + { + File.Delete(stateFile); + } + } + } + catch (Exception e) + { + // If there was a problem deleting the file (like it's read-only or locked on disk, for + // example), then eat the exception and log a warning. Otherwise, rethrow. + if (ExceptionHandling.NotExpectedException(e)) + throw; + + log.LogWarningWithCodeFromResources("General.CouldNotDeleteStateFile", stateFile, e.Message); + } + } + } +} diff --git a/src/XMakeTasks/Strings.resx b/src/XMakeTasks/Strings.resx new file mode 100644 index 00000000000..2d1200f96a6 --- /dev/null +++ b/src/XMakeTasks/Strings.resx @@ -0,0 +1,2683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + BindingRedirect is missing required field 'oldVersion'. + + + BindingRedirect is missing required field 'newVersion'. + + + Some attributes of the assemblyIdentity element are incorrect. + + + There was a problem parsing the oldVersion attribute. {0} + + + There was a problem parsing the newVersion attribute. {0} + + + + The platform mapping "{0}" in the platform mapping list "{1}" is malformed. Please only pass in a semicolon-delimited list of constant string values separated by "=", e.g., "foo=bar;foo2=bar2". + + + Project reference "{0}" has not been resolved. + + UE and LOCALIZATION: + This is not an error - we pass unresolved references to UnresolvedProjectReferences for further + processing in the .targets file. + + + + Project reference "{0}" has been assigned the "{1}" configuration. + + + + MSB3461: The MetabasePath parameter cannot be combined with VirtualPath or PhysicalPath. + {StrBegin="MSB3461: "} + + + MSB3462: Either MetabasePath or VirtualPath must be specified. + {StrBegin="MSB3462: "} + + + MSB3463: The TargetPath parameter must be specified if the application is updatable. + {StrBegin="MSB3463: "} + + + MSB3464: The TargetPath parameter must be specified if the target directory needs to be overwritten. + {StrBegin="MSB3464: "} + + + + MSB3001: Cannot extract culture information from file name "{0}". {1} + {StrBegin="MSB3001: "} + + + Culture of "{0}" was assigned to file "{1}". + + + + MSB3656: No input file has been passed to the task, exiting. + {StrBegin="MSB3656: "} + + + + MSB3646: Cannot specify values for both KeyFile and KeyContainer. + {StrBegin="MSB3646: "} + + + MSB3647: DelaySign parameter is true, but no KeyFile or KeyContainer was specified. + {StrBegin="MSB3647: "} + + + MSB3649: The KeyFile path '{0}' is invalid. KeyFile must point to an existing file. + {StrBegin="MSB3649: "} + + + MSB3650: Neither SDKToolsPath '{0}' nor ToolPath '{1}' is a valid directory. One of these must be set. + {StrBegin="MSB3650: "} + + + MSB3651: The key container '{0}' does not contain a public/private key pair. + {StrBegin="MSB3651: "} + + + MSB3652: The key file '{0}' does not contain a public/private key pair. + {StrBegin="MSB3652: "} + + + MSB3653: AxTlbBaseTask is not an executable task. If deriving from it, please ensure the ToolName property was set. + {StrBegin="MSB3653: "} + + + MSB3654: Delay signing requires that at least a public key be specified. Please either supply a public key using the KeyFile or KeyContainer properties, or disable delay signing. + {StrBegin="MSB3654: "} + + + + MSB3881: Fatal Error: more than {0} command line arguments. + {StrBegin="MSB3881: "} + + + MSB3882: Fatal Error: No response from server. + {StrBegin="MSB3882: "} + + + MSB3883: Unexpected exception: + {StrBegin="MSB3883: "} + + + MSB3884: Could not find rule set file "{0}". + {StrBegin="MSB3884: "} + + + + Creating directory "{0}". + + + MSB3024: Could not copy the file "{0}" to the destination file "{1}", because the destination is a folder instead of a file. To copy the source file into a folder, consider using the DestinationFolder parameter instead of DestinationFiles. + {StrBegin="MSB3024: "} + + + Did not copy from file "{0}" to file "{1}" because the "{2}" parameter was set to "{3}" in the project and the files' sizes and timestamps match. + + + MSB3021: Unable to copy file "{0}" to "{1}". {2} + {StrBegin="MSB3021: "} + + + MSB3022: Both "{0}" and "{1}" were specified as input parameters in the project file. Please choose one or the other. + {StrBegin="MSB3022: "} + + + Copying file from "{0}" to "{1}". + LOCALIZATION: {0} and {1} are paths. + + + Creating hard link to copy "{0}" to "{1}". + LOCALIZATION: {0} and {1} are paths. + + + Could not use a hard link to copy "{0}" to "{1}". Copying the file instead. {2} + LOCALIZATION: {0} and {1} are paths. {2} is an optional localized message. + + + MSB3023: No destination specified for Copy. Please supply either "{0}" or "{1}". + {StrBegin="MSB3023: "} + + + Removing read-only attribute from "{0}". + + + MSB3025: The source file "{0}" is actually a directory. The "Copy" task does not support copying directories. + {StrBegin="MSB3025: "} + + + MSB3026: Could not copy "{0}" to "{1}". Beginning retry {2} in {3}ms. {4} + {StrBegin="MSB3026: "} LOCALIZATION: {0} and {1} are paths. {2} and {3} are numbers. {4} is an optional localized message. + + + MSB3027: Could not copy "{0}" to "{1}". Exceeded retry count of {2}. Failed. + {StrBegin="MSB3027: "} LOCALIZATION: {0} and {1} are paths. {2} is a number. + + + MSB3028: {0} is an invalid retry count. Value must not be negative. + {StrBegin="MSB3028: "} LOCALIZATION: {0} is a number. + + + MSB3029: {0} is an invalid retry delay. Value must not be negative. + {StrBegin="MSB3029: "} LOCALIZATION: {0} is a number. + + + MSB3030: Could not copy the file "{0}" because it was not found. + {StrBegin="MSB3030: "} LOCALIZATION: {0} is a number. + + + + + MSB3031: Could not set additional metadata. "{0}" is a reserved metadata name and cannot be modified. + {StrBegin="MSB3031: "} UE: Tasks and OM users are not allowed to remove or change the value of the built-in meta-data on items e.g. the meta-data "FullPath", "RelativeDir", etc. are reserved. + + + + Resource file '{0}' gets manifest resource name '{1}'. + + + MSB3042: A namespace or class definition was found within a conditional compilation directive in the file "{0}". This may lead to an incorrect choice for the manifest resource name for resource "{1}". + {StrBegin="MSB3042: "} + + + Resource file '{0}' depends on '{1}'. + + + Resource file '{0}' doesn't depend on any other file. + + + MSB3041: Unable to create a manifest resource name for "{0}". {1} + {StrBegin="MSB3041: "} + + + Root namespace is empty. + + + Root namespace is '{0}'. + + + + + MSB3053: The assembly alias "{1}" on reference "{0}" contains illegal characters. + {StrBegin="MSB3053: "} + + + MSB3051: The parameter to the compiler is invalid. {0} + {StrBegin="MSB3051: "} + + + MSB3052: The parameter to the compiler is invalid, '{0}{1}' will be ignored. + {StrBegin="MSB3052: "} + + + + Deleting file "{0}". + + + MSB3061: Unable to delete file "{0}". {1} + {StrBegin="MSB3061: "} + + + File "{0}" doesn't exist. Skipping. + + + + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. + + + MSB3073: The command "{0}" exited with code {1}. + {StrBegin="MSB3073: "} + + + MSB3075: The command "{0}" exited with code {1}. Please verify that you have sufficient rights to run this command. + {StrBegin="MSB3075: "} + + + The command "{0}" exited with code {1}. + + + MSB3076: The regular expression "{0}" that was supplied is invalid. {1} + {StrBegin="MSB3076: "} + + + MSB3072: The "Exec" task needs a command to execute. + {StrBegin="MSB3072: "}LOCALIZATION: "Exec" should not be localized. + + + The working directory "{0}" does not exist. + No error code because an error will be prefixed. + + + + Found "{0}". + + + "{0}" is not a valid file name. {1} + + + + + + Comparison path is "{0}". + + + MSB3541: {0} has invalid value "{1}". {2} + {StrBegin="MSB3541: "} + + + + MSB3102: Could not delete state file "{0}". {1} + {StrBegin="MSB3102: "} + + + Could not locate the assembly "{0}". Check to make sure the assembly exists on disk. + + + MSB3088: Could not read state file "{0}". {1} + {StrBegin="MSB3088: "} + + + Could not read state file "{0}". {1} + + + MSB3081: A problem occurred while trying to set the "{0}" parameter for the IDE's in-process compiler. {1} + {StrBegin="MSB3081: "} + + + MSB3101: Could not write state file "{0}". {1} + {StrBegin="MSB3101: "} + + + MSB3105: The item "{0}" was specified more than once in the "{1}" parameter. Duplicate items are not supported by the "{1}" parameter. + {StrBegin="MSB3105: "} + + + MSB3083: The item "{0}" was specified more than once in the "{1}" parameter and both items had the same value "{2}" for the "{3}" metadata. Duplicate items are not supported by the "{1}" parameter unless they have different values for the "{3}" metadata. + {StrBegin="MSB3083: "} + + + MSB3108: Error executing the {0} task. {1} + {StrBegin="MSB3108: "} + + + Expected a file but got directory "{0}". + + + Expected file "{0}" does not exist. + + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. + {StrBegin="MSB3082: "} + + + MSB3087: An incompatible host object was passed into the "{0}" task. The host object for this task must implement the "{1}" interface. + {StrBegin="MSB3087: "} + + + The format of this state file is not valid. + + + Item "{0}" has attribute "{1}" with value "{2}" that could not be converted to "{3}". + + + MSB3095: Invalid argument. {0} + {StrBegin="MSB3095: "} + + + MSB3097: File "{0}" is not a valid assembly. + {StrBegin="MSB3097: "} + + + MSB3098: "{1}" task received an invalid value for the "{0}" parameter. + {StrBegin="MSB3098: "} + + + MSB3099: Invalid assembly name "{0}". {1} + {StrBegin="MSB3099: "}UE: This message is shown when RegisterAssembly or UnregisterAssembly is passed an assembly with an invalid filename. "{0}" is the name of the file, and "{1}" is a message explaining the problem. LOCALIZATION: "{1}" is a localized message. + + + MSB3100: Syntax for "{0}" parameter is not valid ({1}). Correct syntax is {0}="<name>=<value>". + {StrBegin="MSB3100: "}This error is shown if the user does any of the following: + Properties="foo" (missing property value) + Properties="=4" (missing property name) + The user must pass in an actual property name and value, as in Properties="Configuration=Debug". + + + Global Properties: + + + Removing Properties: + + + Overriding Global Properties for project "{0}" with: + + + Additional Properties for project "{0}": + + + Removing Properties for project "{0}": + + + MSB3103: Invalid Resx file. {0} + {StrBegin="MSB3103: "} + + + MSB3106: Assembly strong name "{0}" is either a path which could not be found or it is a full assembly name which is badly formed. If it is a full assembly name it may contain characters that need to be escaped with backslash(\). Those characters are Equals(=), Comma(,), Quote("), Apostrophe('), Backslash(\). + {StrBegin="MSB3106: "} + + + MSB3107: The specified project reference metadata for the reference "{0}" is missing or has an invalid value: {1} + {StrBegin="MSB3107: "} + + + The IDE's in-process compiler does not support the specified values for the "{0}" parameter. Therefore, this task will fallback to using the command-line compiler. + + + MSB3091: Task failed because "{0}" was not found, or the correct Microsoft Windows SDK is not installed. The task is looking for "{0}" in the "bin" subdirectory beneath the location specified in the {1} value of the registry key {2}. You may be able to solve the problem by doing one of the following: 1) Install the Microsoft Windows SDK. 2) Install Visual Studio 2010. 3) Manually set the above registry key to the correct location. 4) Pass the correct location into the "ToolPath" parameter of the task. + {StrBegin="MSB3091: "} + + + MSB3084: Task attempted to find "{0}" in two locations. 1) Under the "{1}" processor specific directory which is generated based on SdkToolsPath 2) The x86 specific directory under "{2}" which is specified by the SDKToolsPath property. You may be able to solve the problem by doing one of the following: 1) Set the "SDKToolsPath" property to the location of the Microsoft Windows SDK. + {StrBegin="MSB3084: "} + + + Task attempted to find "{0}" using the SdkToolsPath value "{1}". Make sure the SdkToolsPath is set to the correct value and the tool exists in the correct processor specific location below it. + + + MSB3086: Task could not find "{0}" using the SdkToolsPath "{1}" or the registry key "{2}". Make sure the SdkToolsPath is set and the tool exists in the correct processor specific location under the SdkToolsPath and that the Microsoft Windows SDK is installed + {StrBegin="MSB3086: "} + + + MSB3666: The SDK tool "{0}" could not be found. {1} + {StrBegin="MSB3666: "} The {1} will be the exception message + + + MSB3104: The referenced assembly "{0}" was not found. If this assembly is produced by another one of your projects, please make sure to build that project before building this one. + {StrBegin="MSB3104: "} + + + MSB3093: The command exited with code {0}. + {StrBegin="MSB3093: "} + + + MSB3094: "{2}" refers to {0} item(s), and "{3}" refers to {1} item(s). They must have the same number of items. + {StrBegin="MSB3094: "} + + + + MSB3831: The application configuration file must have root configuration element. + {StrBegin="MSB3831: "} + + + MSB3832: The version number "{0}" is invalid. + {StrBegin="MSB3832: "} + + + MSB3833: The assembly name "{0}" contained in the suggested binding redirect is invalid. + {StrBegin="MSB3833: "} + + + No suggested binding redirects from ResolveAssemblyReferences. + + + MSB3835: The "{0}" node is missing from the "{1}" node. Skipping. + {StrBegin="MSB3835: "} + + + MSB3836: The existing binding redirect on "{0}" is incorrect. Consider removing it from the application configuration file. The build will replace it with: "{1}". + {StrBegin="MSB3836: "} + + + Processing suggested binding redirect on "{0}" with MaxVersion "{1}". + + + + MSB3161: A circular dependency was detected between the following built packages: {0}. + {StrBegin="MSB3161: "} + + + MSB3142: An error occurred trying to copy '{0}' to '{1}': {2} + {StrBegin="MSB3142: "} + + + MSB3143: An error occurred trying to copy '{0}' for item '{1}': {2} + {StrBegin="MSB3143: "} + + + MSB3162: The '{0}' item selected requires '{1}'. Select the missing prerequisite in the Prerequisites Dialog Box or create a bootstrapper package for the missing prerequisite. + {StrBegin="MSB3162: "} + + + MSB3165: The value of the '{0}' attribute in '{1}' does not match that of file '{2}'. + {StrBegin="MSB3165: "} + + + MSB3168: Duplicate item '{0}' will be ignored. + {StrBegin="MSB3168: "} + + + MSB3169: An error occurred generating a bootstrapper: {0} + {StrBegin="MSB3169: "} + + + MSB3151: Item '{0}' already includes '{1}'. + {StrBegin="MSB3151: "} + + + MSB3163: Build input parameter 'ComponentsLocation={0}' is not valid. The value must be one of 'HomeSite', 'Relative', or 'Absolute'. Defaulting to 'HomeSite'. + {StrBegin="MSB3163: "} + + + MSB3144: Not enough data was provided to generate a bootstrapper. Please provide a value for at least one of the parameters: 'ApplicationFile' or 'BootstrapperItems'. + {StrBegin="MSB3144: "} + + + MSB3145: Build input parameter '{0}={1}' is not a web url or UNC share. + {StrBegin="MSB3145: "} + + + MSB3146: Item '{0}' is required by '{1}', but was not included. + {StrBegin="MSB3146: "} + + + MSB3696: One of the following items '{0}' is required by '{1}', but none were included. + {StrBegin="MSB3696: "} + + + MSB3147: Could not find required file '{0}' in '{1}'. + {StrBegin="MSB3147: "} + + + MSB3141: No 'PublicKey' or 'Hash' attribute specified for file '{0}' in item '{1}'. + {StrBegin="MSB3141: "} + + + MSB3170: Item '{0}' could not find any of dependent items '{1}'. + {StrBegin="MSB3170: "} + + + MSB3148: No output path specified in build settings. + {StrBegin="MSB3148: "} + + + MSB3149: No resources available for building a bootstrapper. + {StrBegin="MSB3149: "} + + + MSB3150: No string resources available for building a bootstrapper with culture '{0}'. + {StrBegin="MSB3150: "} + + + MSB3152: To enable 'Download prerequisites from the same location as my application' in the Prerequisites dialog box, you must download file '{0}' for item '{1}' to your local machine. For more information, see http://go.microsoft.com/fwlink/?LinkId=239883. + {StrBegin="MSB3152: "} + + + MSB3166: Could not find required file '{0}' for item '{1}'. + {StrBegin="MSB3166: "} + + + MSB3164: No 'HomeSite' attribute has been provided for '{0}', so the package will be published to the same location as the bootstrapper. + {StrBegin="MSB3164: "} + + + MSB3153: Xml validation did not pass for item '{0}' located at '{1}'. + {StrBegin="MSB3153: "} + + + MSB3154: Could not find string resources for item '{0}'. + {StrBegin="MSB3154: "} + + + MSB3155: Item '{0}' could not be located in '{1}'. + {StrBegin="MSB3155: "} + + + MSB3156: Xml validation did not pass for item '{0}' located at '{1}'. + {StrBegin="MSB3156: "} + + + MSB3157: Could not match culture '{0}' for item '{1}'. Using culture '{2}' instead. + {StrBegin="MSB3157: "} + + + MSB3158: Could not find resources for culture '{0}'. Using culture '{1}' instead. + {StrBegin="MSB3158: "} + + + MSB3159: Xml Validation error in file '{0}': {1} + {StrBegin="MSB3159: "} + + + MSB3160: Xml Validation warning in file '{0}': {1} + {StrBegin="MSB3160: "} + + + + MSB3177: Reference '{0}' does not allow partially trusted callers. + {StrBegin="MSB3177: "} + + + MSB3178: Assembly '{0}' is incorrectly specified as a file. + {StrBegin="MSB3178: "} + + + MSB3179: Problem isolating COM reference '{0}': {1} + {StrBegin="MSB3179: "} + + + MSB3111: Use of app.config binding redirects requires full trust. + {StrBegin="MSB3111: "} + + + MSB3112: Two or more assemblies have the same identity '{0}'. + {StrBegin="MSB3112: "} + + + MSB3180: COM component '{1}' is defined in both '{3}' and '{4}', {0}="{2}". + {StrBegin="MSB3180: "} + + + MSB3181: Two or more files have the same target path '{0}'. + {StrBegin="MSB3181: "} + + + MSB3127: The default icon {0} could not be found in the current file references or is not part of the required download group. The default icon file name is case sensitive so the file name referenced in the application manifest must exactly match the icon's file name. + {StrBegin="MSB3127: "} + + + MSB3119: File association extensions must start with a period character (.). + {StrBegin="MSB3119: "} + + + MSB3120: File association extension '{0}' exceeds the maximum allowed length of {1}. + {StrBegin="MSB3120: "} + + + MSB3121: The file association element in the application manifest is missing one or more of the following required attributes: extension, description, progid, or default icon. + {StrBegin="MSB3121: "} + + + MSB3122: Use of file associations requires full trust. + {StrBegin="MSB3122: "} + + + MSB3123: The number of file associations exceeds the limit of {0}. + {StrBegin="MSB3123: "} + + + MSB3124: A file association has already been created for extension '{0}'. + {StrBegin="MSB3124: "} + + + MSB3125: The application is using file associations but has no EntryPoint build parameter. + {StrBegin="MSB3125: "} + + + MSB3126: The application is using file associations but is not marked for installation. File associations cannot be used for applications that are not installed such as applications hosted in a web browser. + {StrBegin="MSB3126: "} + + + MSB3171: Problem generating manifest. {0} + {StrBegin="MSB3171: "} + + + MSB3176: Specified minimum required version is greater than the current publish version. Please specify a version less than or equal to the current publish version. + {StrBegin="MSB3176: "} + + + MSB3117: Application is set to host in browser but the TargetFrameworkVersion is set to v2.0. + + + MSB3116: Application is marked to host in browser but is also marked for online and offline use. Please change your application to online only. + + + MSB3110: Assembly '{0}' has mismatched identity '{1}', expected file name: '{2}'. + {StrBegin="MSB3110: "} + + + MSB3115: File '{0}' is not a valid entry point. + {StrBegin="MSB3115: "} + + + MSB3184: Input manifest is invalid. + {StrBegin="MSB3184: "} + + + MSB3133: The ExcludePermissions property is deprecated. The permission set requested by the application has been set to the permissions defined in Internet or Local Intranet zone. To continue using a custom Permission Set, define your custom permission set in the Security Page of the Project Designer. + {StrBegin="MSB3133: "} + + + MSB3134: The permission set requested by the application exceeded the permissions allowed by the Internet or Intranet zones. Select Full Trust or to continue using partial trust, define your custom permission set in the Security Page of the Project Designer. + {StrBegin="MSB3134: "} + + + MSB3135: The PermissionSet for the target zone has not been defined for the following version of the .NET Framework: {0}. + {StrBegin="MSB3135: "} + + + MSB3175: Invalid value for '{0}' of item '{1}'. + {StrBegin="MSB3175: "} + + + MSB3174: Invalid value for '{0}'. + {StrBegin="MSB3174: "} + + + MSB3189: The update location for this application is a local path. + {StrBegin="MSB3189: "} + + + MSB3185: EntryPoint not specified for manifest. + {StrBegin="MSB3185: "} + + + MSB3186: Unable to infer an assembly identity for generated manifest from task input parameters. + {StrBegin="MSB3186: "} + + + MSB3187: Referenced assembly '{0}' targets a different processor than the application. + {StrBegin="MSB3187: "} + + + MSB3188: Assembly '{0}' must be strong signed in order to be marked as a prerequisite. + {StrBegin="MSB3188: "} + + + MSB3172: Unable to read manifest '{0}'. {1} + {StrBegin="MSB3172: "} + + + MSB3114: Could not find file '{0}' referenced by assembly '{1}'. + {StrBegin="MSB3114: "} + + + MSB3113: Could not find file '{0}'. + {StrBegin="MSB3113: "} + + + MSB3128: The ClickOnce manifests cannot be signed because they contain one or more references that are not hashed. + {StrBegin="MSB3128: "} + + + MSB3182: File name '{0}' exceeds {1} characters. + {StrBegin="MSB3182: "} + + + MSB3183: Reference '{0}' is an interop assembly requiring full trust. + {StrBegin="MSB3183: "} + + + MSB3173: Unable to write manifest '{0}'. {1} + {StrBegin="MSB3173: "} + + + MSB3190: ClickOnce does not support the request execution level '{0}'. + {StrBegin="MSB3190: "} + + + + MSB3552: Resource file "{0}" cannot be found. + {StrBegin="MSB3552: "} + + + MSB3553: Resource file "{0}" has an invalid name. {1} + {StrBegin="MSB3553: "}Appears if the input file name is so invalid we can't change the file extension on it. + + + MSB3554: Cannot write to the output file "{0}". {1} + {StrBegin="MSB3554: "} + + + MSB3555: Output file "{0}" is possibly corrupt. + {StrBegin="MSB3555: "} + + + MSB3556: Only strings can be written to a .txt file, resource "{0}" is type {1}. + {StrBegin="MSB3556: "} + + + MSB3557: Error(s) generating strongly typed resources for file "{0}". + {StrBegin="MSB3557: "} + + + MSB3558: Unsupported file extension "{0}" on file "{1}". + {StrBegin="MSB3558: "} + + + MSB3559: The code DOM provider for the "{0}" language failed. {1} + {StrBegin="MSB3559: "} + + + MSB3560: Could not delete the possibly corrupt output file "{0}". {1} + {StrBegin="MSB3560: "} + + + MSB3562: The "[strings]" tag is no longer necessary in text resources; please remove it. + {StrBegin="MSB3562: "} + + + MSB3563: Unsupported square bracket keyword, "{0}". + {StrBegin="MSB3563: "} + + + MSB3564: Resource line without an equals sign, "{0}". + {StrBegin="MSB3564: "} + + + MSB3565: Resource line without a name. + {StrBegin="MSB3565: "} + + + MSB3566: Unsupported or invalid escape character in resource "{0}", char '{1}'. + {StrBegin="MSB3566: "} + + + MSB3567: Could not generate property on class "{0}". + {StrBegin="MSB3567: "} + + + Could not load type {0} which is used in the .RESX file. Ensure that the necessary references have been added to your project. + + + MSB3568: Duplicate resource name "{0}" is not allowed, ignored. + {StrBegin="MSB3568: "} + + + MSB3569: Invalid hex value after '\u' in resource "{0}", value '{1}'. + {StrBegin="MSB3569: "} + + + MSB3570: Cannot write to the Strongly Typed Resource class file "{0}". {1} + {StrBegin="MSB3570: "} + + + MSB3572: StronglyTypedClassName, StronglyTypedNamespace, and/or StronglyTypedFileName parameters were passed in, but no StronglyTypedLanguage. If you want to create a strongly typed resource class, please specify a language. Otherwise remove all class, file name, and namespace parameters. + {StrBegin="MSB3572: "} + + + MSB3573: The language for a strongly typed resource class was specified, but more than one source file was passed in. Please pass in only one source file at a time if you want to generate strongly typed resource classes. + {StrBegin="MSB3573: "} + + + {0} + + + Processing resource file "{0}" into "{1}". + + + Extracting .ResW files from assembly "{0}" into "{1}". + + + Skipping extracting .ResW files from assembly "{0}" because it declares non-supported framework "{1}". + + + Processing {0} resources from file "{1}". + + + Creating strongly typed resources class "{0}". + + + No resources specified in "Sources". Skipping resource generation. + + LOCALIZATION: Please don't localize "Sources" this is an item meta-data name. + + + + No resources are out of date with respect to their source files. Skipping resource generation. + + + Additional input "{0}" has been updated since the last build. Forcing regeneration of all resources. + + + Creating a separate AppDomain because "NeverLockTypeAssemblies" evaluated to 'true'. + + + Creating a separate AppDomain because while parsing "{0}" the serialized type "{1}" on line {2} could not be loaded. {3} + + + Creating a separate AppDomain because of error parsing "{0}". {1} + + + Creating a separate AppDomain because of error parsing "{0}" on line {1}. {2} + + + Creating a separate AppDomain because of resource "{0}" representing a serialized type "{1}" in "{2}" on line {3}. + + + Creating a separate AppDomain because of resource "{0}" of type "{1}" in "{2}" on line {3}. + + + MSB3574: Did not recognize "{0}" as a managed assembly. + {StrBegin="MSB3574: "} + + + MSB3575: GenerateResource cannot write assemblies, only read from them. Cannot create assembly "{0}". + {StrBegin="MSB3575: "} + + + MSB3576: Creating the CultureInfo failed for assembly "{2}". Note the set of cultures supported is Operating System-dependent, and the Operating System has removed some cultures from time to time (ie, some Serbian cultures are split up in Windows 7). The culture may be a user-defined custom culture that we can't currently load on this machine. Exception info: {0}: {1} + {StrBegin="MSB3576: "} + + + MSB3577: Two output file names resolved to the same output path: "{0}" + {StrBegin="MSB3577: "} + + + MSB3578: This assembly contains neutral resources corresponding to the culture "{0}". These resources will not be considered neutral in the output format as we are unable to preserve this information. The resources will continue to correspond to "{0}" in the output format. + {StrBegin="MSB3578: "} + + + MSB3579: Couldn't find the linked resources file "{0}" listed in the assembly manifest. + {StrBegin="MSB3579: "} + + + MSB3580: The assembly in file "{0}" has an assembly culture, indicating it is a satellite assembly for culture "{1}". But satellite assembly simple names must end in ".resources", while this one's simple name is "{2}". This is either a main assembly with the culture incorrectly set, or a satellite assembly with an incorrect simple name. + {StrBegin="MSB3580: "} + + + MSB3811: The assembly "{0}" says it is a satellite assembly, but it contains code. Main assemblies shouldn't specify the assembly culture in their manifest, and satellites should not contain code. This is almost certainly an error in your build process. + {StrBegin="MSB3811: "} + + + MSB3812: This assembly claims to be a satellite assembly, but doesn't contain any properly named .resources files as manifest resources. The name of the files should end in {0}.resources. There is probably a build-related problem with this assembly. + {StrBegin="MSB3812: "} + + + MSB3813: Invalid or unrecognized UltimateResourceFallbackLocation value in the NeutralResourcesLanguageAttribute for assembly "{1}". Location: "{0}" + {StrBegin="MSB3813: "} + + + MSB3814: Main assembly "{1}" was built improperly. The manifest resource "{0}" ends in .en-US.resources, when it should end in .resources. Either rename it to something like foo.resources (and consider using the NeutralResourcesLanguageAttribute on the main assembly), or move it to a US English satellite assembly. + {StrBegin="MSB3814: "} + + + MSB3815: Satellite assembly "{2}" was built improperly. The manifest resource "{0}" will not be found by the ResourceManager. It must end in "{1}". + {StrBegin="MSB3815: "} + + + MSB3816: Loading assembly "{0}" failed. {1} + {StrBegin="MSB3816: "} + + + MSB3817: The assembly "{0}" does not have a NeutralResourcesLanguageAttribute on it. To be used in an app package, portable libraries must define a NeutralResourcesLanguageAttribute on their main assembly (ie, the one containing code, not a satellite assembly). + {StrBegin="MSB3817: "} + + + MSB3818: The GenerateResource task doesn't currently support simultaneously running as an external tool and extracting ResW files from assemblies. + {StrBegin="MSB3818: "} + + + MSB3819: Cannot find assembly "{0}", which may contain managed resources that need to be included in this app package. Please ensure that this assembly exists. + {StrBegin="MSB3819: "} + + + MSB3820: The path needed to store build-related temporary files is too long. Try your project in a shorter directory, or rename some of your resources. The full path was "{0}". + {StrBegin="MSB3820: "} + + + + + + + MSB3441: Cannot get assembly name for "{0}". {1} + {StrBegin="MSB3441: "} + + + + Could not locate the expected version of the Microsoft Windows SDK. Looked for a location specified in the "{0}" value of the registry key "{1}". If your build process does not need the SDK then this can be ignored. Otherwise you can solve the problem by doing one of the following: 1) Install the Microsoft Windows SDK. 2) Install Visual Studio 2010. 3) Manually set the above registry key to the correct location. + + + Found the Microsoft Windows SDK installed at "{0}". + + + + + Creating directory "{0}". + + + MSB3191: Unable to create directory "{0}". {1} + {StrBegin="MSB3191: "} + + + + MSB3511: "{0}" is an invalid value for the "Importance" parameter. Valid values are: High, Normal and Low. + {StrBegin="MSB3511: "}UE: This message is shown when a user specifies a value for the importance attribute of Message which is not valid. + The importance enumeration is: High, Normal and Low. Specifying any other importance will result in this message being shown + LOCALIZATION: "Importance" should not be localized. + High should not be localized. + Normal should not be localized. + Low should not be localized. + + + + Creating directory "{0}". + + + MSB3676: Could not move the file "{0}" to the destination file "{1}", because the destination is a folder instead of a file. To move the source file into a folder, consider using the DestinationFolder parameter instead of DestinationFiles. + {StrBegin="MSB3676: "} + + + MSB3677: Unable to move file "{0}" to "{1}". {2} + {StrBegin="MSB3677: "} + + + MSB3678: Both "{0}" and "{1}" were specified as input parameters in the project file. Only one must be provided. + {StrBegin="MSB3678: "} + + + Moving file from "{0}" to "{1}". + + + MSB3679: No destination specified for Move. Please supply either "{0}" or "{1}". + {StrBegin="MSB3679: "} + + + MSB3680: The source file "{0}" does not exist. + {StrBegin="MSB3680: "} + + + MSB3681: The source file "{0}" is a directory. The "Move" task does not support moving directories. + {StrBegin="MSB3681: "} + + + + MSB3203: The output path "{0}" cannot be rebased. {1} + {StrBegin="MSB3203: "}UE: This message is shown when the user asks the "MSBuild" task to rebase the paths of its output items relative to the project from where the "MSBuild" task is called (as opposed to the project(s) on which the "MSBuild" task is called), and one of the output item paths is invalid. LOCALIZATION: "{1}" is a localized message from a CLR/FX exception explaining the problem. + + + MSB3202: The project file "{0}" was not found. + {StrBegin="MSB3202: "}UE: This message is shown when the user passes a non-existent project file to the MSBuild task, in the "Projects" parameter. + and they have not specified the SkipNonexistentProjects parameter, or it is set to false. + + + Skipping project "{0}" because it was not found. + UE: This message is shown when the user passes a non-existent project file to the MSBuild task, in the "Projects" parameter, and they have specified the SkipNonexistentProjects parameter. + + + MSB3204: The project file "{0}" is in the ".vcproj" file format, which MSBuild no longer supports. Please convert the project by opening it in the Visual Studio IDE or running the conversion tool, or use MSBuild 3.5 or earlier to build it. + {StrBegin="MSB3204: "} LOC: ".vcproj" should not be localized + + + MSB3205: SkipNonexistentProject can only accept values of "True", "False" and "Build". + {StrBegin="MSB3205: "} LOC: "SkipNonexistentProject", "True", "False" and "Build" should not be localized + + + The MSBuild task is skipping the remaining projects because the StopOnFirstFailure parameter was set to true. + LOCALIZATION: Do not localize the words "MSBuild" or "StopOnFirstFailure". + + + The MSBuild task is skipping the remaining targets because the StopOnFirstFailure parameter was set to true. + LOCALIZATION: Do not localize the words "MSBuild" or "StopOnFirstFailure". + + + Overriding the BuildingInParallel property by setting it to false. This is due to the system being run in single process mode with StopOnFirstFailure set to true. + LOCALIZATION: Do not localize the words "MSBuild", "BuildingInParallel", or "StopOnFirstFailure". + + + StopOnFirstFailure will have no effect when the following conditions are all present: 1) The system is running in multiple process mode 2) The BuildInParallel property is true. 3) The RunEachTargetSeparately property is false. + LOCALIZATION: Do not localize the words "RunEachTargetSeparately", "BuildingInParallel", or "StopOnFirstFailure". + + + + MSB3501: Could not read lines from file "{0}". {1} + {StrBegin="MSB3501: "} + + + + MSB3211: The assembly '{0}' is not registered for COM Interop. Please register it with regasm.exe /tlb. + {StrBegin="MSB3211: "} + + + MSB3212: The assembly "{0}" could not be converted to a type library. {1} + {StrBegin="MSB3212: "} + + + MSB3217: Cannot register assembly "{0}". {1} + {StrBegin="MSB3217: "} + + + MSB3213: Cannot register type library "{0}". {1} + {StrBegin="MSB3213: "} + + + MSB3214: "{0}" does not contain any types that can be registered for COM Interop. + {StrBegin="MSB3214: "} + + + MSB3215: Cannot register assembly "{0}" - file doesn't exist. + {StrBegin="MSB3215: "} + + + Registering assembly "{0}" for COM Interop. + + + Exporting and registering type library "{0}". + + + Type library "{0}" is up to date, skipping regeneration. + + + MSB3216: Cannot register assembly "{0}" - access denied. Please make sure you're running the application as administrator. {1} + {StrBegin="MSB3216: "} + + + + MSB3231: Unable to remove directory "{0}". {1} + {StrBegin="MSB3231: "} + + + Removing directory "{0}". + + + Directory "{0}" doesn't exist. Skipping. + + + + No resources specified in "InputFiles". Skipping resource generation. + + + MSB3451: Neither SDKToolsPath '{0}' nor ToolPath '{1}' is a valid directory. One of these must be set. + {StrBegin="MSB3451: "} + + + MSB3452: StronglyTypedClassName, StronglyTypedNamespace, and/or StronglyTypedFileName parameters were passed in, but no StronglyTypedLanguage. If you want to create a strongly typed resource class, please specify a language. Otherwise remove all class, file name, and namespace parameters. + {StrBegin="MSB3452: "} + + + MSB3453: The language for a strongly typed resource class was specified, but more than one source file was passed in. Please pass in only one source file at a time if you want to generate strongly typed resource classes. + {StrBegin="MSB3453: "} + + + MSB3454: Tracker.exe is required to correctly incrementally generate resources in some circumstances, such as when building on a 64-bit OS using 32-bit MSBuild. This build requires Tracker.exe, but it could not be found. The task is looking for Tracker.exe beneath the {0} value of the registry key {1}. To solve the problem, either: 1) Install the Microsoft Windows SDK v7.0A or later. 2) Install Microsoft Visual Studio 2010. 3) Manually set the above registry key to the correct location. Alternatively, you can turn off incremental resource generation by setting the "TrackFileAccess" property to "false". + {StrBegin="MSB3454: "} + + + MSB3455: ResGen.exe may not run because the command line is {0} characters long, which exceeds the maximum length of the command. To fix this problem, please either (1) remove unnecessary assembly references, or (2) make the paths to those references shorter. + {StrBegin="MSB3455: "} + + + + AssemblyFoldersEx location: "{0}" + + + Considered AssemblyFoldersEx locations. + + + There was a conflict between "{0}" and "{1}". + + + "{0}" was chosen because it had a higher version. + + + "{0}" was chosen because it was primary and "{1}" was not. + + + Consider app.config remapping of assembly "{0}" from Version "{1}" [{2}] to Version "{3}" [{4}] to solve conflict and get rid of warning. + + UE and LOCALIZATION: + {1} and {3} are version numbers like 1.0.0.0 + {2} and {4} are file names correspending to {1} and {3} respectively + {0} is an assembly name with no version number like 'D, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa' + + + + Considered "{0}", but its name "{1}" didn't match. + + + Considered "{0}", but it didn't exist. + + + Considered treating "{0}" as a file name, but it didn't exist. + + + Considered "{0}", which was not found in the GAC. + + + Considered "{0}", which existed but didn't have a valid identity. This may not be an assembly. + + + Considered "{0}", which existed but had a processor architecture "{1}" which does not match the targeted processor architecture "{2}". + + + Dependency "{0}". + + + {0} + + + {0} + + + {0} + + + Found related file "{0}". + + + Found satellite file "{0}". + + + Found embedded scatter file "{0}". + + + {0} + + + Ignoring "{0}" because it has a non-empty subtype "{1}". + + + Ignoring invalid Target Framework value "{0}". + + + MSB3242: Conflict between mutually exclusive parameters. AutoUnify was 'true' and AppConfigFile was set. + {StrBegin="MSB3242: "} + + + MSB3243: No way to resolve conflict between "{0}" and "{1}". Choosing "{0}" arbitrarily. + {StrBegin="MSB3243: "} + + + MSB3244: Could not find dependent files. {0} + {StrBegin="MSB3244: "} + + + MSB3245: Could not resolve this reference. {0} If this reference is required by your code, you may get compilation errors. + {StrBegin="MSB3245: "} + + + MSB3246: Resolved file has a bad image, no metadata, or is otherwise inaccessible. {0} + {StrBegin="MSB3246: "} + + + MSB3247: Found conflicts between different versions of the same dependent assembly. In Visual Studio, double-click this warning (or select it and press Enter) to fix the conflicts; otherwise, add the following binding redirects to the "runtime" node in the application configuration file: {0} + {StrBegin="MSB3247: "} + + + MSB3248: Parameter "{0}" has invalid value "{1}". {2} + {StrBegin="MSB3248: "} + + + MSB3249: Application Configuration file "{0}" is invalid. {1} + {StrBegin="MSB3249: "} + + + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} + {StrBegin="MSB3250: "} + + + MSB3251: Could not resolve assembly {0}. The target framework required by this assembly ({1}) is higher than the project target framework. If this reference is required by your code, you may get compilation errors. + + + MSB3252: The currently targeted framework "{1}" does not include the referenced assembly "{0}". To fix this, either (1) change the targeted framework for this project or (2) remove the referenced assembly from the project. + {StrBegin="MSB3252: "} + + + MSB3253: The currently targeted framework "{2}" does not include "{1}" which the referenced assembly "{0}" depends on. This caused the referenced assembly to not resolve. To fix this, either (1) change the targeted framework for this project, or (2) remove the referenced assembly from the project. + {StrBegin="MSB3253: "} + + + MSB3254: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblySubsetTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} + {StrBegin="MSB3254: "} + + + MSB3255: Could not find any Target Framework Subset files in the Target Framework Directories or at the locations specified in the InstalledAssemblySubsetTables. + {StrBegin="MSB3255: "} + + + MSB3256: No assemblies were read in from the redist lists. A TargetFramework profile exclusion list could not be generated. + {StrBegin="MSB3256: "} + + + MSB3257: The primary reference "{0}" could not be resolved because it has a higher version "{1}" than exists in the current target framework. The version found in the current target framework is "{2}". + {StrBegin="MSB3257: "} + + + MSB3258: The primary reference "{0}" could not be resolved because it has an indirect dependency on the .NET Framework assembly "{1}" which has a higher version "{2}" than the version "{3}" in the current target framework. + {StrBegin="MSB3258: "} + + + MSB3259: Invalid parameter combination. Can only set either subset or profile parameters. Cannot set one or more subset parameters ("TargetFrameworkSubsets", "InstalledAssemblySubsetTables") and one or more profile parameters ("ProfileName", "FullFrameworkFolders", "FullFrameworkAssemblyTables") at the same time. + {StrBegin="MSB3259: "} + + + MSB3260: Could not find any target framework profile redist files in the FullFrameworkFolders locations. + {StrBegin="MSB3260: "} + + + MSB3261: The FrameworkDirectory metadata must be set on all items passed to the FullFrameworkAssemblyTables parameter. The item "{0}" did not have the metadata set. + {StrBegin="MSB3261: "} + + + MSB3262: When targeting a profile the ProfileName parameter and one of FullFrameworkFolders or FullFrameworkAssemblyTables must be set. + {StrBegin="MSB3262: "} + + + MSB3263: The file "{0}" will be ignored because it cannot be read. This file was either passed in to FullFrameworkAssemblyTables or was found by searching the "{1}" folder in the FullFrameworkFolders. {2} + {StrBegin="MSB3263: "} + + + MSB3267: The primary reference "{0}", which is a framework assembly, could not be resolved in the currently targeted framework. "{1}". To resolve this problem, either remove the reference "{0}" or retarget your application to a framework version which contains "{0}". + {StrBegin="MSB3267: "} + + + MSB3268: The primary reference "{0}" could not be resolved because it has an indirect dependency on the framework assembly "{1}" which could not be resolved in the currently targeted framework. "{2}". To resolve this problem, either remove the reference "{0}" or retarget your application to a framework version which contains "{1}". + {StrBegin="MSB3268: "} + + + MSB3269: Could not determine if resolved references are part of the targeted framework because of an error. "{0}" + {StrBegin="MSB3269: "} + + + MSB3270: There was a mismatch between the processor architecture of the project being built "{0}" and the processor architecture of the reference "{1}", "{2}". This mismatch may cause runtime failures. Please consider changing the targeted processor architecture of your project through the Configuration Manager so as to align the processor architectures between your project and references, or take a dependency on references with a processor architecture that matches the targeted processor architecture of your project. + {StrBegin="MSB3270: "} + + + MSB3271: There was a mismatch between the processor architecture of the project being built "{0}" and the processor architecture, "{1}", of the implementation file "{2}" for "{3}". This mismatch may cause runtime failures. Please consider changing the targeted processor architecture of your project through the Configuration Manager so as to align the processor architectures between your project and implementation file, or choose a winmd file with an implementation file that has a processor architecture which matches the targeted processor architecture of your project. + {StrBegin="MSB3271: "} + + + MSB3272: There was a problem reading the implementation file "{0}". "{1}" + {StrBegin="MSB3272: "} + + + Invalid PE header found. The implementation file will not used. + This message can be used as the {1} in MSB3272 + + + MSB3273: Unknown processor architecture. The implementation file "{0}" for "{1}" had an ImageFileMachine value of "0x{2}". If you wish to use this implementation file make sure the "ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch" property in your project is set to "Warning" or "None". + {StrBegin="MSB3273: "} + + + MSB3274: The primary reference "{0}" could not be resolved because it was built against the "{1}" framework. This is a higher version than the currently targeted framework "{2}". + {StrBegin="MSB3274: "} + + + MSB3275: The primary reference "{0}" could not be resolved because it has an indirect dependency on the assembly "{1}" which was built against the "{2}" framework. This is a higher version than the currently targeted framework "{3}". + {StrBegin="MSB3275: "} + + + MSB3276: Found conflicts between different versions of the same dependent assembly. Please set the "AutoGenerateBindingRedirects" property to true in the project file. For more information, see http://go.microsoft.com/fwlink/?LinkId=294190. + {StrBegin="MSB3276: "} + + + MSB3277: Found conflicts between different versions of the same dependent assembly that could not be resolved. These reference conflicts are listed in the build log when log verbosity is set to detailed. + {StrBegin="MSB3277: "} + + + {0} = '{1}' + + + {0}: + + + This reference is not "CopyLocal" because it conflicted with another reference with the same name and lost the conflict. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. + + + + The ImageRuntimeVersion for this reference is "{0}". + + LOCALIZATION: Please don't localize "ImageRuntimeVersion" this is an item meta-data name. + + + + This reference is a WinMDFile. + + LOCALIZATION: Please don't localize "WinMDFile" this is an item meta-data name. + + + + This reference is not "CopyLocal" because the CopyLocalDependenciesWhenParentReferenceInGac property is set to false and all the parent references for this reference are found in the GAC. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. "CopyLocalDependenciesWhenParentReferenceInGac" is a property name. + + + + This reference is not "CopyLocal" because its types will be embedded into the target assembly. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. + + + + This reference is not "CopyLocal" because the CopyLocalDependenciesWhenParentReferenceInGac property is set to false and all the parent references for this reference are found in the GAC. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. "CopyLocalDependenciesWhenParentReferenceInGac" is a property name. + + + + This reference is not "CopyLocal" because it's in a Frameworks directory. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. + + + + This reference is not "CopyLocal" because at least one source item had "Private" set to "false" and no source items had "Private" set to "true". + + LOCALIZATION: Please don't localize "CopyLocal", "Private", "false", "true". + + + + This reference is not "CopyLocal" because it's a prerequisite file. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. + + + + This reference is not "CopyLocal" because it's registered in the GAC. + + LOCALIZATION: Please don't localize "CopyLocal" this is an item meta-data name. + + + + Primary reference "{0}". + + + Required by "{0}". + + + References which depend on "{0}" [{1}]. + This will look like references which depend on "A, Version=2.0.0.0 PublicKey=4a4fded9gisujf" [a.dll]. + + + Unresolved primary reference with an item include of "{0}". + This messages is for a reference which could not be resolved, however we have its item spec and will display that. {0} will be somethign like System or A, Version=xxx + + + Project file item includes which caused reference "{0}". + This will look like, Project file item includes which caused reference "a.dll". + + + Resolved file path is "{0}". + + + Reference found at search path location "{0}". + + + For SearchPath "{0}". + + + Using this version instead of original version "{0}" in "{2}" because of a binding redirect entry in the file "{1}". + + + Using this version instead of original version "{0}" in "{1}" because AutoUnify is 'true'. + + + Due to a remapping entry in the currently targeted framework redist list, reference "{0}" was remapped to "{1}". + + + Using this version instead of original version "{0}" in "{1}" because there is a more recent version of this framework file. + + + Unified Dependency "{0}". + + + Unified primary reference "{0}". + + + Could not find satellite assemblies for reference "{0}". {1} + + + A TargetFramework profile exclusion list will be generated. The exclusion list is a list of assemblies not in the profile. + + + A TargetFramework profile exclusion list will not be generated. A full client name "{0}" was found in the TargetFrameworkSubsetNames list. + + + No TargetFramework subset exclusion list will be generated. IgnoreDefaultInstalledAssemblySubsetTables is true and no additional profile files were passed in to InstalledAssemblySubsetTables. + + + A TargetFramework profile exclusion list will be generated. + + + No TargetFramework profile exclusion list will be generated. No TargetFrameworkSubsets were provided and no additional profile files were passed in to InstalledAssemblySubsetTables. + + + TargetFramework Profile List Information: + + + TargetFramework Profile List Paths: + + + Redist List File Paths: + + + Computed TargetFramework profile exclusion list assembly full names: + + + Path: "{0}" + + + The redist list file "{0}" has a null or empty Redist name in the FileList element. Make sure the Redist Name is not null or empty. + + + + + COM Reference '{0}' is the interop assembly for ActiveX control '{1}' but was marked to be linked by the compiler with the /link flag. This COM reference will be treated as a reference and will not be linked. + + + Adding a matching tlbimp reference for the aximp reference "{0}". + + + Using cache file at "{0}". + + + Creating new cache file at "{0}". + + + MSB3281: The assembly "{0}" is not a valid assembly file. + {StrBegin="MSB3281: "} + + + MSB3282: Cannot access type library name for library "{0}". {1} + {StrBegin="MSB3282: "} + + + MSB3283: Cannot find wrapper assembly for type library "{0}". Verify that (1) the COM component is registered correctly and (2) your target platform is the same as the bitness of the COM component. For example, if the COM component is 32-bit, your target platform must not be 64-bit. + {StrBegin="MSB3283: "} + + + MSB3284: Cannot get the file path for type library "{0}" version {1}.{2}. {3} + {StrBegin="MSB3284: "} + + + MSB3285: Cannot get type library attributes for a dependent type library! + {StrBegin="MSB3285: "} + + + MSB3286: Cannot load type library "{0}" version {1}.{2}. {3} + {StrBegin="MSB3286: "} + + + MSB3287: Cannot load type library for reference "{0}". {1} + {StrBegin="MSB3287: "} + + + MSB3302: Cannot retrieve information about a dependent type. + {StrBegin="MSB3302: "} + + + MSB3300: Cannot specify values for both KeyFile and KeyContainer. + {StrBegin="MSB3300: "} + + + MSB3301: DelaySign parameter is true, but no KeyFile or KeyContainer was specified. + {StrBegin="MSB3301: "} + + + MSB3288: COM reference "{0}" conflicts with reference "{1}" - the project references different type libraries with the same type library names. Ignoring reference "{0}". + {StrBegin="MSB3288: "} + + + MSB3290: Failed to create the wrapper assembly for type library "{0}". {1} + {StrBegin="MSB3290: "} + + + MSB3291: Could not resolve dependent .NET assembly "{0}". Please make sure this assembly is included in the references section of the project file. + {StrBegin="MSB3291: "} + + + MSB3292: Failed to remap ADO reference version {0}.{1} to version 2.7 - "{2}". + {StrBegin="MSB3292: "} + + + MSB3303: Could not resolve COM reference "{0}" version {1}.{2}. {3} + {StrBegin="MSB3303: "} + + + MSB3293: Could not resolve dependent COM reference "{0}" version {1}.{2}. + {StrBegin="MSB3293: "} + + + MSB3294: Could not resolve dependent COM reference "{0}". + {StrBegin="MSB3294: "} + + + MSB3304: Could not determine the dependencies of the COM reference "{0}". {1} + {StrBegin="MSB3304: "} + + + MSB3305: Processing COM reference "{0}" from path "{1}". {2} + {StrBegin="MSB3305: "} + + + Processing COM reference "{0}" from path "{1}". {2} + + + MSB3295: Failed to load an assembly. Please make sure you have disabled strong name verification for your public key if you want to generate delay signed wrappers. {0} + {StrBegin="MSB3295: "} + + + MSB3296: The specified COM reference meta-data for the reference "{1}" is missing or has an invalid value: "{0}". + {StrBegin="MSB3296: "} + + + MSB3297: No COM references have been passed into the task, exiting. + {StrBegin="MSB3297: "} + + + Remapping ADO reference version {0}.{1} to version 2.7. + + + Resolved COM reference dependency "{0}" version {1}.{2}: "{3}" + + + Resolved COM reference dependency "{0}": "{1}" + + + Resolved COM reference for item "{0}": "{1}". + + + Resolving COM reference for item "{0}" with a wrapper "{1}". + + + Resolving COM reference dependency "{0}" version {1}.{2}. + + + Determining dependencies of the COM reference "{0}". + + + MSB3298: The key container '{0}' does not contain a public/private key pair. + {StrBegin="MSB3298: "} + + + MSB3299: The key file '{0}' does not contain a public/private key pair. + {StrBegin="MSB3299: "} + + + {0} {1}.{2} + + + + MSB3321: Importing key file "{0}" was canceled. + {StrBegin="MSB3321: "} + + + MSB3322: Unable to get MD5 checksum for the key file "{0}". {1} + {StrBegin="MSB3322: "} + + + Error importing key + + + MSB3323: Unable to find manifest signing certificate in the certificate store. + {StrBegin="MSB3323: "} + + + MSB3324: Invalid key file name "{0}". {1} + {StrBegin="MSB3324: "} + + + MSB3325: Cannot import the following key file: {0}. The key file may be password protected. To correct this, try to import the certificate again or manually install the certificate to the Strong Name CSP with the following key container name: {1} + {StrBegin="MSB3325: "} + + + MSB3326: Cannot import the following key file: {0}. The key file may be password protected. To correct this, try to import the certificate again or import the certificate manually into the current user’s personal certificate store. + {StrBegin="MSB3326: "} + + + MSB3327: Unable to find code signing certificate in the current user’s Windows certificate store. To correct this, either disable signing of the ClickOnce manifest or install the certificate into the certificate store. + {StrBegin="MSB3327: "} + + + + MSB3331: Unable to apply publish properties for item "{0}". + {StrBegin="MSB3331: "} + + + + Processing manifest file "{0}". + + + Attempting to resolve reference "{0}" on path(s): + + + MSB3341: Could not resolve reference "{0}". If this reference is required by your code, you may get compilation errors. + {StrBegin="MSB3341: "} + + + + MSB3582: Could not resolve project reference "{0}". + {StrBegin="MSB3582: "} + + + Project reference "{0}" has not been resolved. + + UE and LOCALIZATION: + This is not an error - we pass unresolved references to UnresolvedProjectReferences for further + processing in the .targets file. + + + + Resolving project reference "{0}". + + + Project reference "{0}" resolved as "{1}". + + + + + MSB3471: Non-existent reference "{0}" passed to the SGen task. + {StrBegin="MSB3471: "} + + + MSB3472: We were unable to delete the existing serializer "{0}" before creating a new one: {1} + {StrBegin="MSB3472: "} + + + MSB3473: Path for "{0}" is invalid. {1} + {StrBegin="MSB3473: "} + + + + MSB3481: The signing certificate could not be located. Ensure that it is in the current user's personal store. + {StrBegin="MSB3481: "} + + + MSB3482: An error occurred while signing: {0} + {StrBegin="MSB3482: "} + + + MSB3483: A warning occurred while signing: {0} + {StrBegin="MSB3483: "} + + + MSB3484: Signing target '{0}' could not be found. + {StrBegin="MSB3484: "} + + + + MSB3351: Unable to create a strong name key pair from key container '{0}'. + {StrBegin="MSB3351: "} + + + MSB3352: The specified key file '{0}' could not be read. + {StrBegin="MSB3352: "} + + + MSB3353: Public key necessary for delay signing was not specified. + {StrBegin="MSB3353: "} + + + + MSB3661: Invalid value '{0}' passed to the Transform property. + {StrBegin="MSB3661: "} + + + MSB3662: No input file has been passed to the task, exiting. + {StrBegin="MSB3662: "} + + + + MSB3371: The file "{0}" cannot be created. {1} + {StrBegin="MSB3371: "} + + + MSB3372: The file "{0}" cannot be made writable. {1} + {StrBegin="MSB3372: "} + + + MSB3373: The attributes on file "{0}" cannot be restored to their original value. {1} + {StrBegin="MSB3373: "} + + + MSB3374: The last access/last write time on file "{0}" cannot be set. {1} + {StrBegin="MSB3374: "} + + + Creating "{0}" because "{1}" was specified. + + + MSB3375: The file "{0}" does not exist. + {StrBegin="MSB3375: "} + + + MSB3376: The syntax of the Time parameter is incorrect. {0} + {StrBegin="MSB3376: "} + + + Touching "{0}". + + + + MSB3396: The "{0}" task needs either an assembly path or state file path. + {StrBegin="MSB3396: "} + + + MSB3395: Cannot unregister assembly "{0}". {1} + {StrBegin="MSB3395: "} + + + MSB3391: "{0}" does not contain any types that can be unregistered for COM Interop. + {StrBegin="MSB3391: "} + + + MSB3392: Cannot unregister assembly "{0}" - access denied. Please make sure you're running the application as administrator. {1} + {StrBegin="MSB3392: "} + + + MSB3393: Cannot unregister assembly "{0}" - file doesn't exist. + {StrBegin="MSB3393: "} + + + Unregistering assembly "{0}" for COM Interop. + + + Unregistering type library "{0}". + + + MSB3397: Cannot unregister type library "{0}" - cannot load file, check to make sure it's a valid type library. + {StrBegin="MSB3397: "} + + + Cannot unregister type library "{0}" - file doesn't exist. + + + MSB3394: Type library "{0}" is not registered, cannot unregister. + {StrBegin="MSB3394: "} + + + + MSB3401: "{1}" is an invalid value for the "{0}" parameter. The valid values are: {2} + {StrBegin="MSB3401: "} + + + MSB3402: There was an error creating the pdb file "{0}". {1} + {StrBegin="MSB3402: "} + + + "{1}" is an invalid value for the "{0}" parameter. + + + + + MSB3491: Could not write lines to file "{0}". {1} + {StrBegin="MSB3491: "} + + + + MSB3642: The Target Framework Moniker "{0}" is invalid. "{1}" + {StrBegin="MSB3642: "} + + + MSB3643: There was an error generating reference assembly paths based on the TargetFrameworkMoniker "{0}". {1} + {StrBegin="MSB3643: "} + + + MSB3644: The reference assemblies for framework "{0}" were not found. To resolve this, install the SDK or Targeting Pack for this framework version or retarget your application to a version of the framework for which you have the SDK or Targeting Pack installed. Note that assemblies will be resolved from the Global Assembly Cache (GAC) and will be used in place of reference assemblies. Therefore your assembly may not be correctly targeted for the framework you intend. + {StrBegin="MSB3644: "} + + + MSB3645: .NET Framework v3.5 Service Pack 1 was not found. In order to target "{0}", .NET Framework v3.5 Service Pack 1 or later must be installed. + {StrBegin="MSB3645: "} + + + + + + MSB3762: No references were passed to the task. References to at least mscorlib and Windows.Foundation are required. + {StrBegin="MSB3762: "} + + + + + MSB3686: Unable to create Xaml task. Compilation failed. {0} + {StrBegin="MSB3686: "} + + + MSB3687: Unable to parse Xaml rule. {0} + {StrBegin="MSB3687: "} + + + MSB3688: Unable to create Xaml task. File not found: {0}. + {StrBegin="MSB3688: "} + + + MSB3689: Unable to execute Xaml task. If the CommandLineTemplate task parameter is not specified, then the ToolName attribute must be specified in the Rule or the ToolExe task parameter must be set. + {StrBegin="MSB3689: "} + + + MSB3690: Unable to create Xaml task. The Rule "{0}" was not found. + {StrBegin="MSB3690: "} + + + MSB3691: Unable to create Xaml task. The root object was not of type ProjectSchemaDefinitions. + {StrBegin="MSB3691: "} + + + MSB3692: Unable to create Xaml task. The <UsingTask> does not contain a <Task> definition. + {StrBegin="MSB3692: "} + + + MSB3693: Unable to execute Xaml task. The value "{1}" specified for task parameter "{0}" is not valid. + {StrBegin="MSB3693: "} + + + MSB3694: Unable to create Xaml task. The EnumValue "{1}" on EnumProperty "{0}" is missing the SwitchValue attribute. + {StrBegin="MSB3694: "} + + + + MSB3721: The command "{0}" exited with code {1}. + {StrBegin="MSB3721: "} + + + MSB3722: The command "{0}" exited with code {1}. Please verify that you have sufficient rights to run this command. + {StrBegin="MSB3722: "} + + + MSB3723: The parameter "{0}" requires missing parameter "{1}" to be set. + {StrBegin="MSB3723: "} + + + + + MSB3701: Unable to load arguments for the XslTransformation task. {0} + {StrBegin="MSB3701: "} + + + MSB3702: Unable to process the XsltParameters argument for the XslTransformation task. {0} + {StrBegin="MSB3702: "} + + + MSB3703: Unable to execute transformation. {0} + {StrBegin="MSB3703: "} + + + Only one of XmlContent or XmlInputPaths arguments can be set. + + + One of XmlContent or XmlInputPaths arguments must be set. + + + Only one of XslContent, XslInputPath or XslCompiledDllPath arguments can be set. + + + One of XslContent, XslInputPath and XslCompiledDllPath arguments must be set. + + + MSB3704: Unable to load the specified Xslt. {0} + {StrBegin="MSB3704: "} + + + When specifying assembly "{0}", you must specify the type as well. + + + The specified Xslt Parameter doesn't have attribute "{0}". + + + The specified Xslt Parameter attribute is not a well-formed XML fragment. + + + The usage of the document() method and embedded scripts is prohibited by default, due to risks of foreign code execution. If "{0}" is a trusted source that requires those constructs, please set the "UseTrustedSettings" parameter to "true" to allow their execution. + + + + + + MSB3731: Unable to process the Namespaces argument for the XmlPoke task. {0} + {StrBegin="MSB3731: "} + + + The specified Namespaces attribute doesn't have attribute "{0}". + + + The specified Namespaces attribute is not a well-formed XML fragment. + + + Replaced "{0}" with "{1}". + + + Made {0} replacement(s). + + + MSB3732: Unable to set XPath expression's Context. {0} + {StrBegin="MSB3732: "} + + + MSB3733: Input file "{0}" cannot be opened. {1} + {StrBegin="MSB3733: "} + + + MSB3734: XPath Query "{0}" cannot be loaded. {1} + {StrBegin="MSB3734: "} + + + MSB3735: Error while executing poke operation with the Value parameter "{0}". {1} + {StrBegin="MSB3735: "} + + + + MSB3741: Unable to load arguments for the XmlPeek task. {0} + {StrBegin="MSB3741: "} + + + MSB3742: Unable to process the Namespaces argument for the XmlPeek task. {0} + {StrBegin="MSB3742: "} + + + MSB3743: Unable to set XPath expression's Context. {0} + {StrBegin="MSB3743: "} + + + Only one of XmlContent or XmlInputPath arguments can be set. + + + One of XmlContent or XmlInputPath arguments must be set. + + + The specified Namespaces attribute does not have attribute "{0}". + + + The specified Namespaces attribute is not a well-formed XML fragment. + + + Found "{0}". + + + The specified XPath query did not capture any nodes. + + + + MSB3711: At least one of OutputFile or OutputDirectory must be provided. + {StrBegin="MSB3711: "} + + + MSB3712: Code for the language "{0}" could not be generated. {1} + {StrBegin="MSB3712: "} + + + MSB3713: The file "{0}" could not be created. {1} + {StrBegin="MSB3713: "} + + + MSB3714: The parameter "{0}" was supplied, but not all previously numbered parameters. + {StrBegin="MSB3714: "} + + + No output file was written because no code was specified to create. + + + Emitted specified code into "{0}". + + + Generated by the MSBuild WriteCodeFragment class on {0}. + + + + + + MSB3751: The <Code> element is missing for the "{0}" task. This element is required. + {StrBegin="MSB3751: "} <Code> should not be localized it is the name of an xml element + + + MSB3752: The "{0}" attribute has been set but is empty. If the "{0}" attribute is set it must not be empty. + {StrBegin="MSB3752: "} + + + MSB3753: The task could not be instantiated because it does not implement the ITask interface. Make sure the task implements the Microsoft.Build.Framework.ITask interface. + {StrBegin="MSB3753: "} + + + MSB3754: The reference assembly "{0}" is invalid. "{1}" + {StrBegin="MSB3754: "} + + + MSB3755: Could not find reference "{0}". If this reference is required by your code, you may get compilation errors." + {StrBegin="MSB3755: "} + + + MSB3756: The xml element "{0}" is not valid under the xml element "{1}". + {StrBegin="MSB3756: "} + + + MSB3757: Multiple Code xml elements have been found, this is not allowed. To fix this issue remove any additional Code xml elements from the task. + {StrBegin="MSB3757: "} + + + MSB3758: An error has occurred during compilation. {0} + {StrBegin="MSB3758: "} + + + The task name "{0}" could not be found. + + + The source file for this compilation can be found at: "{0}" + + + The reference assembly "{0}" is a metadata only assembly. + + + + + Searching for SDK "{0}": + + + Found at search location "{0}". + + + There was a problem reading the SDK manifest file "{0}". {1} + + + Reading SDK manifest file "{0}". + + + Targeted configuration and architecture "{0}|{1}" + + + Has a platform identity of "{0}". + + + Could not find "FrameworkIdentity" attribute "{0}" in the SDK manifest. + + + No FrameworkIdentity attributes were found in the SDK manifest, treating this SDK as a non-framework SDK. + + + No "APPX" attributes indicating app package locations were found in the SDK manifest. If an app package is required at runtime the project may not run. + + + Found "FrameworkIdentity" attribute "{0}" in the SDK manifest. + + + Found "APPX" location attribute "{0}" in the SDK manifest. + + + Updating the "{0}" architecture "APPX" location from "{1}" to "{2}". + + + Cannot resolve "SDKReference" items because no installed SDK locations were passed into the property "InstalledSdks". + "SDKReference" and "InstalledSDKs" are property names on the task and should not be localized + + + MSB3773: The SDK "{0}" cannot be referenced alongside SDK(s) {1}, as they all belong to the same SDK product Family "{2}". Please consider removing references to other SDKs of the same product family. + {StrBegin="MSB3773: "} + + + MSB3774: Could not find SDK "{0}". + {StrBegin="MSB3774: "} + + + MSB3775: There was an error resolving the SDK "{0}". {1} + {StrBegin="MSB3775: "} "{0}" will be the root location which could not be searched. Ie (c:\program files\sdks\..) + + + MSB3776: The SDK Reference "{0}" is incorrectly formatted. It must be in the following format "<SDKName>, Version=<SDKVersion>. For example: "MySDK, Version=2.0" + {StrBegin="MSB3776: "} + + + MSB3777: "FrameworkIdentity" attributes were found in the SDK manifest file "{0}", however none of the attributes matched the targeted configuration and architecture "{1} | {2}" and no "FrameworkIdentity" attribute without configuration and architecture could be found. If this project is to be packaged, packaging will fail. + {StrBegin="MSB3777: "} + + + MSB3778: "APPX" attributes were found in the SDK manifest file "{0}" however none of the attributes matched the targeted configuration and architecture "{1} | {2}" and no "APPX" attribute without configuration and architecture could be found. If an app package is required then the project will fail at runtime. + {StrBegin="MSB3778: "} + + + MSB3779: The processor architecture of the project being built "{0}" is not supported by the referenced SDK "{1}". Please consider changing the targeted processor architecture of your project (in Visual Studio this can be done through the Configuration Manager) to one of the architectures supported by the SDK: "{2}". + {StrBegin="MSB3779: "} + + + MSB3780: The SDK "{0}" cannot be referenced alongside SDK(s) {1}, because only one version of the SDK can be referenced from a project. Please consider removing references to the other SDKs. + {StrBegin="MSB3780: "} + + + MSB3781: The SDK "{0}" depends on the following SDK(s) {1}, which have not been added to the project or were not found. Please ensure that you add these dependencies to your project or you may experience runtime issues. You can add dependencies to your project through the Reference Manager. + {StrBegin="MSB3781: "} + + + MSB3782: The "{0}" SDK does not support targeting a neutral architecture with "Prefer 32-Bit" enabled for the project. Please go to the project properties (Build tab for C# and Compile tab for VB) and disable the "Prefer 32-bit" option, or change your project to target a non-neutral architecture. + {StrBegin="MSB3782: "} Also, please localize "Prefer 32-Bit" in the same way that it is localized in wizard\vbdesigner\designer\proppages\buildproppage.resx + + + MSB3783: Project "{0}" depends upon SDK "{1} v{2}" which was released originally for apps targeting "{3} {4}". To verify whether "{1} v{2}" is compatible with "{5} {6}", contact the SDK author or see http://go.microsoft.com/fwlink/?LinkID=309181. + {StrBegin="MSB3783: "} + + + MSB3841: The SDK "{0}" depends on the SDK "{1}", which is not compatible with "{2} {3}". Please reference a version of SDK "{0}" which supports "{2} {3}". + {StrBegin="MSB3841: "} + + + MSB3842: Project "{0}" depends upon SDK "{1} v{2}" which supports apps targeting "{3} {4}". To verify whether "{1} v{2}" is compatible with "{5} {6}", contact the SDK author or see http://go.microsoft.com/fwlink/?LinkID=309181. + {StrBegin="MSB3842: "} + + + MSB3843: Project "{0}" targets platform "{3}", but references SDK "{1} v{2}" which targets platform "{4}". + {StrBegin="MSB3843: "} + + + MSB3844: Project "{0}" targets platform version "{3}", but references SDK "{1} v{2}" which requires platform version "{4}" or higher. + {StrBegin="MSB3844: "} + + + + Installed SDKs: + + + SDK "{0}" is installed at "{1}" + + + Found "{0}" SDKs. + + + Searching for SDKs targeting "{0}, {1}". + {0} will be the platform identifier, "Windows" and {1} will be a version number + + + MSB3784: "TargetPlatformVersion" and "TargetPlatformIdentifier" cannot be empty. + {StrBegin="MSB3784: "} TargetPlatformVersion and TargetPlatformIdentifier root are property names and should not be localized + + + MSB3785: No SDKs were found. SDKReference items will not be resolved. If your application requires these references there may be compilation errors. + {StrBegin="MSB3785: "} "SDKReference" referrs to SDKReference items in the project file and should not be localized. + + + MSB3786: There was a problem retrieving the installed SDKs on the machine. {0} + {StrBegin="MSB3786: "} + + + + + Enumerating SDK Reference "{0}" from "{1}". + + + Looking for references under "{0}". + + + Looking for redist files under "{0}". + + + Not enumerating SDK Reference "{0}" because the "ExpandReferences" metadata is not true on the reference. + + + Adding reference "{0}". + + + Adding file "{0}" from redist folder with target path "{1}". + + + There was a conflict between two redist files going to the same target path "{0}" within the "{1}" SDK. Choosing "{2}" over "{3}" because it was resolved first. + + + There was a conflict between two files from the redist folder files going to the same target path "{0}" between the "{1}" and "{2}" SDKs. Choosing "{3}" over "{4}" because it was resolved first. + + + There was a conflict between two references with the same file name resolved within the "{0}" SDK. Choosing "{1}" over "{2}" because it was resolved first. + + + There was a conflict between two references with the same file name between the "{0}" and "{1}" SDKs. Choosing "{2}" over "{3}" because it was resolved first. + + + There was a problem reading the cache file "{0}". "{1}" + + + There was a problem deleting the cache file "{0}". "{1}" + + + There was a problem getting the time stamp of the current assembly "{0}". "{1}" + + + There was a problem getting the assembly metadata for "{0}". "{1}" + + + There was a problem writing the cache file "{0}". "{1}" + + + The "OriginalItemSpec" metadata for the resolved SDK with path "{0}" was empty. The "OriginalItemSpec" metadata must be set." + + + MSB3795: There was a problem in the GetSDKReferenceFiles task. {0} + {StrBegin="MSB3795: "} + + + MSB3796: There was a conflict between two files. {0} + {StrBegin="MSB3796: "} + + + MSB3797: The targeted configuration for the resolved sdk reference "{0}" was empty. Cannot find reference or redist files without a targeted configuration. + {StrBegin="MSB3797: "} + + + MSB3798: The targeted architecture for the resolved sdk reference "{0}" was empty. Cannot find reference or redist files without a targeted architecture. + {StrBegin="MSB3798: "} + + + + + MSB3851: This project targets "{0}, Version={1}", but it is attempting to reference "{2}" targeting "{3}", which is invalid. + {StrBegin="MSB3851: "} + + + + + MSB3861: Failed to log an error using resource string "{0}". {1} + {StrBegin="MSB3861: "} + + + + + MSB3871: Shared projects cannot be built on their own. Please either build a project that references this project, or build the entire solution. + {StrBegin="MSB3871: "} + + + + diff --git a/src/XMakeTasks/StrongNameException.cs b/src/XMakeTasks/StrongNameException.cs new file mode 100644 index 00000000000..040d841c0a4 --- /dev/null +++ b/src/XMakeTasks/StrongNameException.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Build.Tasks +{ + /// + /// Internal exception thrown when there's an unrecoverable failure extracting public/private keys. + /// + /// + /// WARNING: marking a type [Serializable] without implementing ISerializable imposes a serialization contract -- it is a + /// promise to never change the type's fields i.e. the type is immutable; adding new fields in the next version of the type + /// without following certain special FX guidelines, can break both forward and backward compatibility + /// + [Serializable] + internal class StrongNameException : Exception + { + /// + /// Default constructor + /// + internal StrongNameException() + { + // do nothing + } + + /// + /// Constructor that allows to preserve the original exception information + /// + internal StrongNameException(Exception innerException) : base("", innerException) + { + // do nothing + } + + + /// + /// Constructor to implement required constructors for serialization + /// + protected StrongNameException(SerializationInfo info, StreamingContext context) : base(info, context) + { + // do nothing + } + } +} diff --git a/src/XMakeTasks/StrongNameUtils.cs b/src/XMakeTasks/StrongNameUtils.cs new file mode 100644 index 00000000000..b23641719d0 --- /dev/null +++ b/src/XMakeTasks/StrongNameUtils.cs @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Security; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Possible strong name states of an assembly + /// + internal enum StrongNameLevel + { + None, DelaySigned, FullySigned, Unknown + }; + + /// + /// Strong naming utilities. + /// + internal static class StrongNameUtils + { + /// + /// Reads contents of a key file. Reused from vsdesigner code. + /// + /// + /// + /// + /// + internal static void ReadKeyFile(TaskLoggingHelper log, string keyFile, out StrongNameKeyPair keyPair, out byte[] publicKey) + { + // Initialize parameters + keyPair = null; + publicKey = null; + + byte[] keyFileContents; + + try + { + // Read the stuff from the file stream + using (FileStream fs = new FileStream(keyFile, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + keyFileContents = new byte[(int)fs.Length]; + fs.Read(keyFileContents, 0, (int)fs.Length); + } + } + catch (ArgumentException e) + { + log.LogErrorWithCodeFromResources("StrongNameUtils.KeyFileReadFailure", keyFile); + log.LogErrorFromException(e); + throw new StrongNameException(e); + } + catch (IOException e) + { + log.LogErrorWithCodeFromResources("StrongNameUtils.KeyFileReadFailure", keyFile); + log.LogErrorFromException(e); + throw new StrongNameException(e); + } + catch (SecurityException e) + { + log.LogErrorWithCodeFromResources("StrongNameUtils.KeyFileReadFailure", keyFile); + log.LogErrorFromException(e); + throw new StrongNameException(e); + } + + // Make a new key pair from what we read + StrongNameKeyPair snp = new StrongNameKeyPair(keyFileContents); + + // If anything fails reading the public key portion of the strong name key pair, then + // assume that keyFile contained only the public key portion of the public/private pair. + try + { + publicKey = snp.PublicKey; + + // If we didn't throw up to this point then we have a valid public/private key pair, + // so assign the object just created above to the out parameter. + keyPair = snp; + } + catch (ArgumentException) + { + publicKey = keyFileContents; + } + } + + /// + /// Given a key file or container, extract private/public key data. Reused from vsdesigner code. + /// + /// + /// + /// + /// + /// + internal static void GetStrongNameKey(TaskLoggingHelper log, string keyFile, string keyContainer, out StrongNameKeyPair keyPair, out byte[] publicKey) + { + // Gets either a strong name key pair from the key file or a key container. + // If keyFile and keyContainer are both null/zero length then returns null. + // Initialize parameters + keyPair = null; + publicKey = null; + if (keyContainer != null && keyContainer.Length != 0) + { + try + { + keyPair = new StrongNameKeyPair(keyContainer); + publicKey = keyPair.PublicKey; + } + catch (SecurityException e) + { + log.LogErrorWithCodeFromResources("StrongNameUtils.BadKeyContainer", keyContainer); + log.LogErrorFromException(e); + throw new StrongNameException(e); + } + catch (ArgumentException e) + { + log.LogErrorWithCodeFromResources("StrongNameUtils.BadKeyContainer", keyContainer); + log.LogErrorFromException(e); + throw new StrongNameException(e); + } + } + else if (keyFile != null && keyFile.Length != 0) + { + ReadKeyFile(log, keyFile, out keyPair, out publicKey); + } + } + + /// + /// Given an assembly path, determine if the assembly is [delay] signed or not. This code is based on similar unmanaged + /// routines in vsproject and sn.exe (ndp tools) codebases. + /// + /// + /// + internal static StrongNameLevel GetAssemblyStrongNameLevel(string assemblyPath) + { + ErrorUtilities.VerifyThrowArgumentNull(assemblyPath, "assemblyPath"); + + StrongNameLevel snLevel = StrongNameLevel.Unknown; + IntPtr fileHandle = NativeMethods.InvalidIntPtr; + + try + { + // open the assembly + fileHandle = NativeMethods.CreateFile(assemblyPath, NativeMethods.GENERIC_READ, FileShare.Read, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero); + if (fileHandle == NativeMethods.InvalidIntPtr) + return snLevel; + + // if it's not a disk file, exit + if (NativeMethods.GetFileType(fileHandle) != NativeMethods.FILE_TYPE_DISK) + return snLevel; + + IntPtr fileMappingHandle = IntPtr.Zero; + + try + { + fileMappingHandle = NativeMethods.CreateFileMapping(fileHandle, IntPtr.Zero, NativeMethods.PAGE_READONLY, 0, 0, null); + if (fileMappingHandle == IntPtr.Zero) + return snLevel; + + IntPtr fileMappingBase = IntPtr.Zero; + + try + { + fileMappingBase = NativeMethods.MapViewOfFile(fileMappingHandle, NativeMethods.FILE_MAP_READ, 0, 0, IntPtr.Zero); + if (fileMappingBase == IntPtr.Zero) + return snLevel; + + // retrieve NT headers pointer from the file + IntPtr ntHeader = NativeMethods.ImageNtHeader(fileMappingBase); + + if (ntHeader == IntPtr.Zero) + return snLevel; + + // get relative virtual address of the COR20 header + uint cor20HeaderRva = GetCor20HeaderRva(ntHeader); + + if (cor20HeaderRva == 0) + return snLevel; + + IntPtr lastRvaSection = IntPtr.Zero; + // get the pointer to the COR20 header structure + IntPtr cor20HeaderPtr = NativeMethods.ImageRvaToVa(ntHeader, fileMappingBase, cor20HeaderRva, out lastRvaSection); + + if (cor20HeaderPtr == IntPtr.Zero) + return snLevel; + + // get the COR20 structure itself + NativeMethods.IMAGE_COR20_HEADER cor20Header = (NativeMethods.IMAGE_COR20_HEADER)Marshal.PtrToStructure(cor20HeaderPtr, typeof(NativeMethods.IMAGE_COR20_HEADER)); + + // and finally, examine it. If no space is allocated for strong name signature, assembly is not signed. + if ((cor20Header.StrongNameSignature.VirtualAddress == 0) || (cor20Header.StrongNameSignature.Size == 0)) + { + snLevel = StrongNameLevel.None; + } + else + { + // if there's allocated space and strong name flag is set, assembly is fully signed, or delay signed otherwise + if ((cor20Header.Flags & NativeMethods.COMIMAGE_FLAGS_STRONGNAMESIGNED) != 0) + { + snLevel = StrongNameLevel.FullySigned; + } + else + { + snLevel = StrongNameLevel.DelaySigned; + } + } + } + finally + { + if (fileMappingBase != IntPtr.Zero) + { + NativeMethods.UnmapViewOfFile(fileMappingBase); + fileMappingBase = IntPtr.Zero; + } + } + } + finally + { + if (fileMappingHandle != IntPtr.Zero) + { + NativeMethods.CloseHandle(fileMappingHandle); + fileMappingHandle = IntPtr.Zero; + } + } + } + finally + { + if (fileHandle != NativeMethods.InvalidIntPtr) + { + NativeMethods.CloseHandle(fileHandle); + fileHandle = NativeMethods.InvalidIntPtr; + } + } + + return snLevel; + } + + /// + /// Retrieves the relative virtual address of the COR20 header, given the address of the NT headers structure. The catch + /// here is that the NT headers struct can be either 32 or 64 bit version, and some fields have different sizes there. We + /// need to see if we're dealing with a 32bit header or a 64bit one first. + /// + /// + /// + private static uint GetCor20HeaderRva(IntPtr ntHeadersPtr) + { + // read the first ushort in the optional header - we have an uint and IMAGE_FILE_HEADER preceding it + ushort optionalHeaderMagic = (ushort)Marshal.ReadInt16(ntHeadersPtr, Marshal.SizeOf(typeof(uint)) + Marshal.SizeOf(typeof(NativeMethods.IMAGE_FILE_HEADER))); + + // this should really be a structure, but NDP can't marshal fixed size struct arrays in a struct... ugh. + // this ulong corresponds to a IMAGE_DATA_DIRECTORY structure + ulong cor20DataDirectoryLong = 0; + + // see if we have a 32bit header or a 64bit header + if (optionalHeaderMagic == NativeMethods.IMAGE_NT_OPTIONAL_HDR32_MAGIC) + { + // marshal data into the appropriate structure + NativeMethods.IMAGE_NT_HEADERS32 ntHeader32 = (NativeMethods.IMAGE_NT_HEADERS32)Marshal.PtrToStructure(ntHeadersPtr, typeof(NativeMethods.IMAGE_NT_HEADERS32)); + cor20DataDirectoryLong = ntHeader32.optionalHeader.DataDirectory[NativeMethods.IMAGE_DIRECTORY_ENTRY_COMHEADER]; + } + else if (optionalHeaderMagic == NativeMethods.IMAGE_NT_OPTIONAL_HDR64_MAGIC) + { + // marshal data into the appropriate structure + NativeMethods.IMAGE_NT_HEADERS64 ntHeader64 = (NativeMethods.IMAGE_NT_HEADERS64)Marshal.PtrToStructure(ntHeadersPtr, typeof(NativeMethods.IMAGE_NT_HEADERS64)); + cor20DataDirectoryLong = ntHeader64.optionalHeader.DataDirectory[NativeMethods.IMAGE_DIRECTORY_ENTRY_COMHEADER]; + } + else + { + Debug.Assert(false, "invalid file type!"); + return 0; + } + + // cor20DataDirectoryLong is really a IMAGE_DATA_DIRECTORY structure which I had to pack into an ulong + // (see comments for IMAGE_OPTIONAL_HEADER32/64 in NativeMethods.cs) + // this code extracts the virtualAddress (uint) and size (uint) fields from the ulong by doing simple + // bit masking/shifting ops + uint virtualAddress = (uint)(cor20DataDirectoryLong & 0x00000000ffffffff); + uint size = (uint)(cor20DataDirectoryLong >> 32); + + return virtualAddress; + } + } +} diff --git a/src/XMakeTasks/StronglyTypedResourceBuilder.cs b/src/XMakeTasks/StronglyTypedResourceBuilder.cs new file mode 100644 index 00000000000..0b8aa5d9a44 --- /dev/null +++ b/src/XMakeTasks/StronglyTypedResourceBuilder.cs @@ -0,0 +1,680 @@ +// ************************************************************************************************ +// ************************************************************************************************ +// Originated in ndp\fx\src\designer\system\resources\tools\stronglytypedresourcebuilder.cs +// Extracted in order to remove System.Designer dependency from Microsoft.Build.Tasks. +// When they add typeforwarders for StronglyTypedResourceBuilder this can be removed. +// Almost completely unchanged, except for visibility and namespace. +// +// When making changes to this file, consider whether the changes also need to be made in +// ndp\fx\src\designer\system\resources\tools\stronglytypedresourcebuilder.cs +// venus\project\webapp\package\GlobalResourceProxyGenerator.cs +// ************************************************************************************************ +// ************************************************************************************************ + + +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +/*============================================================ +** +** Class: StronglyTypedResourceBuilder +** +** Purpose: Uses CodeDOM to produce a resource class, with a +** strongly-typed property for every resource. +** For usability & eventually some perf work. +** +** Date: January 16, 2003 +** +===========================================================*/ + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Resources; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Reflection; +using System.Globalization; +using System.Xml; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Shared; + +/* + Plan for the future: + Ideally we will be able to change the property getters here to use a + resource index calculated at build time, being the x'th resource in the + .resources file. We would then call something like + ResourceManager.LookupResourceByIndex(). This would avoid some string + comparisons during resource lookup. + + This would require work from ResourceReader and/or ResourceWriter (or + a standalone, separate utility with duplicated code) to calculate the + id's. It would also require that all satellite assemblies use the same + resource ID's as the main assembly. This would require dummy entries + for resources in all satellites. + + I'm not sure how much time this will save, but it does sound like an + interesting idea. + -- Brian Grunkemeyer, 1/16/2003 +*/ + + +namespace Microsoft.Build.Tasks +{ + internal static class StronglyTypedResourceBuilder + { + // Note - if you add a new property to the class, add logic to reject + // keys of that name in VerifyResourceNames. + private const String ResMgrFieldName = "resourceMan"; + private const String ResMgrPropertyName = "ResourceManager"; + private const String CultureInfoFieldName = "resourceCulture"; + private const String CultureInfoPropertyName = "Culture"; + + // When fixing up identifiers, we will replace all these chars with + // a single char that is valid in identifiers, such as '_'. + private static readonly char[] CharsToReplace = new char[] { ' ', + '\u00A0' /* non-breaking space */, '.', ',', ';', '|', '~', '@', + '#', '%', '^', '&', '*', '+', '-', '/', '\\', '<', '>', '?', '[', + ']', '(', ')', '{', '}', '\"', '\'', ':', '!' }; + private const char ReplacementChar = '_'; + + private const String DocCommentSummaryStart = ""; + private const String DocCommentSummaryEnd = ""; + + // Maximum size of a String resource to show in the doc comment for its property + private const int DocCommentLengthThreshold = 512; + + // Save the strings for better doc comments. + internal sealed class ResourceData + { + private Type _type; + private String _valueAsString; + + internal ResourceData(Type type, String valueAsString) + { + _type = type; + _valueAsString = valueAsString; + } + + internal Type Type { + get { return _type; } + } + + internal String ValueAsString { + get { return _valueAsString; } + } + } + + + internal static CodeCompileUnit Create(IDictionary resourceList, String baseName, String generatedCodeNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + return Create(resourceList, baseName, generatedCodeNamespace, null, codeProvider, internalClass, out unmatchable); + } + + internal static CodeCompileUnit Create(IDictionary resourceList, String baseName, String generatedCodeNamespace, String resourcesNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + if (resourceList == null) + throw new ArgumentNullException("resourceList"); + + Dictionary resourceTypes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach(DictionaryEntry de in resourceList) { + + ResXDataNode node = de.Value as ResXDataNode; + ResourceData data; + if (node != null) { + string keyname = (string) de.Key ; + if (keyname != node.Name) + throw new ArgumentException(SR.GetString(SR.MismatchedResourceName, keyname, node.Name)); + + String typeName = node.GetValueTypeName((AssemblyName[])null); + Type type = Type.GetType(typeName); + String valueAsString = node.GetValue((AssemblyName[])null).ToString(); + data = new ResourceData(type, valueAsString); + } + else { + // If the object is null, we don't have a good way of guessing the + // type. Use Object. This will be rare after WinForms gets away + // from their resource pull model in Whidbey M3. + Type type = (de.Value == null) ? typeof(Object) : de.Value.GetType(); + data = new ResourceData(type, de.Value == null ? null : de.Value.ToString()); + } + resourceTypes.Add((String) de.Key, data); + } + + // Note we still need to verify the resource names are valid language + // keywords, etc. So there's no point to duplicating the code above. + + return InternalCreate(resourceTypes, baseName, generatedCodeNamespace, resourcesNamespace, codeProvider, internalClass, out unmatchable); + } + + private static CodeCompileUnit InternalCreate(Dictionary resourceList, String baseName, String generatedCodeNamespace, String resourcesNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + if (baseName == null) + throw new ArgumentNullException("baseName"); + if (codeProvider == null) + throw new ArgumentNullException("codeProvider"); + + // Keep a list of errors describing known strings that couldn't be + // fixed up (like "4"), as well as listing all duplicate resources that + // were fixed up to the same name (like "A B" and "A-B" both going to + // "A_B"). + ArrayList errors = new ArrayList(0); + + // Verify the resource names are valid property names, and they don't + // conflict. This includes checking for language-specific keywords, + // translating spaces to underscores, etc. + SortedList cleanedResourceList; + Hashtable reverseFixupTable; + cleanedResourceList = VerifyResourceNames(resourceList, codeProvider, errors, out reverseFixupTable); + + // Verify the class name is legal. + String className = baseName; + // Attempt to fix up class name, and throw an exception if it fails. + if (!codeProvider.IsValidIdentifier(className)) { + String fixedClassName = VerifyResourceName(className, codeProvider); + if (fixedClassName != null) + className = fixedClassName; + } + if (!codeProvider.IsValidIdentifier(className)) + throw new ArgumentException(SR.GetString(SR.InvalidIdentifier, className)); + + // If we have a namespace, verify the namespace is legal, + // attempting to fix it up if needed. + if (!String.IsNullOrEmpty(generatedCodeNamespace)) { + if (!codeProvider.IsValidIdentifier(generatedCodeNamespace)) { + String fixedNamespace = VerifyResourceName(generatedCodeNamespace, codeProvider, true); + if (fixedNamespace != null) + generatedCodeNamespace = fixedNamespace; + } + // Note we cannot really ensure that the generated code namespace + // is a valid identifier, as namespaces can have '.' and '::', but + // identifiers cannot. + } + + CodeCompileUnit ccu = new CodeCompileUnit(); + ccu.ReferencedAssemblies.Add("System.dll"); + + ccu.UserData.Add("AllowLateBound", false); + ccu.UserData.Add("RequireVariableDeclaration", true); + + CodeNamespace ns = new CodeNamespace(generatedCodeNamespace); + ns.Imports.Add(new CodeNamespaceImport("System")); + ccu.Namespaces.Add(ns); + + // Generate class + CodeTypeDeclaration srClass = new CodeTypeDeclaration(className); + ns.Types.Add(srClass); + AddGeneratedCodeAttributeforMember(srClass); + + TypeAttributes ta = internalClass ? TypeAttributes.NotPublic : TypeAttributes.Public; + //ta |= TypeAttributes.Sealed; + srClass.TypeAttributes = ta; + srClass.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + srClass.Comments.Add(new CodeCommentStatement(SR.GetString(SR.ClassDocComment), true)); + + CodeCommentStatement comment = new CodeCommentStatement(SR.GetString(SR.ClassComments1), true); + srClass.Comments.Add(comment); + comment = new CodeCommentStatement(SR.GetString(SR.ClassComments3), true); + srClass.Comments.Add(comment); + + srClass.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + CodeTypeReference debuggerAttrib = new CodeTypeReference(typeof(System.Diagnostics.DebuggerNonUserCodeAttribute)); + debuggerAttrib.Options = CodeTypeReferenceOptions.GlobalReference; + srClass.CustomAttributes.Add(new CodeAttributeDeclaration(debuggerAttrib)); + + CodeTypeReference compilerGenedAttrib = new CodeTypeReference(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute)); + compilerGenedAttrib.Options = CodeTypeReferenceOptions.GlobalReference; + srClass.CustomAttributes.Add(new CodeAttributeDeclaration(compilerGenedAttrib)); + + // Figure out some basic restrictions to the code generation + bool useStatic = internalClass || codeProvider.Supports(GeneratorSupport.PublicStaticMembers); + bool supportsTryCatch = codeProvider.Supports(GeneratorSupport.TryCatchStatements); + EmitBasicClassMembers(srClass, generatedCodeNamespace, baseName, resourcesNamespace, internalClass, useStatic, supportsTryCatch); + + // Now for each resource, add a property + foreach(DictionaryEntry entry in cleanedResourceList) { + String propertyName = (String) entry.Key; + // The resourceName will be the original value, before fixups, + // if any. + String resourceName = (String) reverseFixupTable[propertyName]; + if (resourceName == null) + resourceName = propertyName; + bool r = DefineResourceFetchingProperty(propertyName, resourceName, (ResourceData) entry.Value, srClass, internalClass, useStatic); + if (!r) { + errors.Add(entry.Key); + } + } + + unmatchable = (String[]) errors.ToArray(typeof(String)); + + // Validate the generated class now + CodeGenerator.ValidateIdentifiers(ccu); + + return ccu; + } + + internal static CodeCompileUnit Create(String resxFile, String baseName, String generatedCodeNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + return Create(resxFile, baseName, generatedCodeNamespace, null, codeProvider, internalClass, out unmatchable); + } + + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] + internal static CodeCompileUnit Create(String resxFile, String baseName, String generatedCodeNamespace, String resourcesNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + if (resxFile == null) + throw new ArgumentNullException("resxFile"); + + // Read the resources from a ResX file into a dictionary - name & type name + Dictionary resourceList = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + using(ResXResourceReader rr = new ResXResourceReader(resxFile)) { + rr.UseResXDataNodes = true; + foreach(DictionaryEntry de in rr) { + ResXDataNode node = (ResXDataNode) de.Value; + String typeName = node.GetValueTypeName((AssemblyName[])null); + Type type = Type.GetType(typeName); + String valueAsString = node.GetValue((AssemblyName[])null).ToString(); + ResourceData data = new ResourceData(type, valueAsString); + resourceList.Add((String) de.Key, data); + } + } + + // Note we still need to verify the resource names are valid language + // keywords, etc. So there's no point to duplicating the code above. + + return InternalCreate(resourceList, baseName, generatedCodeNamespace, resourcesNamespace, codeProvider, internalClass, out unmatchable); + } + + private static void AddGeneratedCodeAttributeforMember(CodeTypeMember typeMember) { + CodeAttributeDeclaration generatedCodeAttrib = new CodeAttributeDeclaration(new CodeTypeReference(typeof(System.CodeDom.Compiler.GeneratedCodeAttribute))); + generatedCodeAttrib.AttributeType.Options = CodeTypeReferenceOptions.GlobalReference; + CodeAttributeArgument toolArg = new CodeAttributeArgument(new CodePrimitiveExpression(typeof(StronglyTypedResourceBuilder).FullName)); + CodeAttributeArgument versionArg = new CodeAttributeArgument(new CodePrimitiveExpression(MSBuildConstants.CurrentAssemblyVersion)); + + generatedCodeAttrib.Arguments.Add(toolArg); + generatedCodeAttrib.Arguments.Add(versionArg); + + typeMember.CustomAttributes.Add(generatedCodeAttrib); + } + + [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] + private static void EmitBasicClassMembers(CodeTypeDeclaration srClass, String nameSpace, String baseName, String resourcesNamespace, bool internalClass, bool useStatic, bool supportsTryCatch) + { + const String tmpVarName = "temp"; + String resMgrCtorParam; + + if (resourcesNamespace != null) { + if (resourcesNamespace.Length > 0) + resMgrCtorParam = resourcesNamespace + '.' + baseName; + else + resMgrCtorParam = baseName; + } + else if ((nameSpace != null) && (nameSpace.Length > 0)) { + resMgrCtorParam = nameSpace + '.' + baseName; + } + else { + resMgrCtorParam = baseName; + } + + CodeAttributeDeclaration suppressMessageAttrib = new CodeAttributeDeclaration(new CodeTypeReference(typeof(System.Diagnostics.CodeAnalysis.SuppressMessageAttribute))); + suppressMessageAttrib.AttributeType.Options = CodeTypeReferenceOptions.GlobalReference; + suppressMessageAttrib.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression("Microsoft.Performance"))); + suppressMessageAttrib.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression("CA1811:AvoidUncalledPrivateCode"))); + + // Emit a constructor - make it protected even if it is a "static" class to allow subclassing + CodeConstructor ctor = new CodeConstructor(); + ctor.CustomAttributes.Add(suppressMessageAttrib); + if (useStatic || internalClass) + ctor.Attributes = MemberAttributes.FamilyAndAssembly; + else + ctor.Attributes = MemberAttributes.Public; + srClass.Members.Add(ctor); + + // Emit _resMgr field. + CodeTypeReference ResMgrCodeTypeReference = new CodeTypeReference(typeof(ResourceManager), CodeTypeReferenceOptions.GlobalReference); + CodeMemberField field = new CodeMemberField(ResMgrCodeTypeReference, ResMgrFieldName); + field.Attributes = MemberAttributes.Private; + if (useStatic) + field.Attributes |= MemberAttributes.Static; + srClass.Members.Add(field); + + // Emit _resCulture field, and leave it set to null. + CodeTypeReference CultureTypeReference = new CodeTypeReference(typeof(CultureInfo), CodeTypeReferenceOptions.GlobalReference); + field = new CodeMemberField(CultureTypeReference, CultureInfoFieldName); + field.Attributes = MemberAttributes.Private; + if (useStatic) + field.Attributes |= MemberAttributes.Static; + srClass.Members.Add(field); + + // Emit ResMgr property + CodeMemberProperty resMgr = new CodeMemberProperty(); + srClass.Members.Add(resMgr); + resMgr.Name = ResMgrPropertyName; + resMgr.HasGet = true; + resMgr.HasSet = false; + resMgr.Type = ResMgrCodeTypeReference; + if (internalClass) + resMgr.Attributes = MemberAttributes.Assembly; + else + resMgr.Attributes = MemberAttributes.Public; + if (useStatic) + resMgr.Attributes |= MemberAttributes.Static; + + // Mark the ResMgr property as advanced + CodeTypeReference editorBrowsableStateTypeRef = new CodeTypeReference(typeof(System.ComponentModel.EditorBrowsableState)); + editorBrowsableStateTypeRef.Options = CodeTypeReferenceOptions.GlobalReference; + + CodeAttributeArgument editorBrowsableStateAdvanced = new CodeAttributeArgument(new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(editorBrowsableStateTypeRef), "Advanced")); + CodeAttributeDeclaration editorBrowsableAdvancedAttribute = new CodeAttributeDeclaration("System.ComponentModel.EditorBrowsableAttribute", + new CodeAttributeArgument[] {editorBrowsableStateAdvanced}); + editorBrowsableAdvancedAttribute.AttributeType.Options = CodeTypeReferenceOptions.GlobalReference; + resMgr.CustomAttributes.Add(editorBrowsableAdvancedAttribute); + + // Emit the Culture property (read/write) + CodeMemberProperty culture = new CodeMemberProperty(); + srClass.Members.Add(culture); + culture.Name = CultureInfoPropertyName; + culture.HasGet = true; + culture.HasSet = true; + culture.Type = CultureTypeReference; + if (internalClass) + culture.Attributes = MemberAttributes.Assembly; + else + culture.Attributes = MemberAttributes.Public; + + if (useStatic) + culture.Attributes |= MemberAttributes.Static; + + // Mark the Culture property as advanced + culture.CustomAttributes.Add(editorBrowsableAdvancedAttribute); + + + /* + // Here's what I'm trying to emit. Since not all languages support + // try/finally, we'll avoid our double lock pattern here. + // This will only hurt perf when we get two threads racing through + // this method the first time. Unfortunate, but not a big deal. + // Also, the .NET Compact Framework doesn't support + // Thread.MemoryBarrier (they only run on processors w/ a strong + // memory model, and who knows about IA64...) + // Once we have Interlocked.CompareExchange, we should use it here. + if (_resMgr == null) { + ResourceManager tmp = new ResourceManager("", typeof("").Assembly); + _resMgr = tmp; + } + return _resMgr; + */ + CodeFieldReferenceExpression field_resMgr = new CodeFieldReferenceExpression(null, ResMgrFieldName); + CodeMethodReferenceExpression object_equalsMethod = new CodeMethodReferenceExpression(new CodeTypeReferenceExpression(typeof(Object)), "ReferenceEquals"); + + CodeMethodInvokeExpression isResMgrNull = new CodeMethodInvokeExpression(object_equalsMethod, new CodeExpression[] { field_resMgr, new CodePrimitiveExpression(null)}); + + // typeof().Assembly + CodePropertyReferenceExpression getAssembly = new CodePropertyReferenceExpression(new CodeTypeOfExpression(new CodeTypeReference(srClass.Name)), "Assembly"); + + // new ResourceManager(resMgrCtorParam, typeof().Assembly); + CodeObjectCreateExpression newResMgr = new CodeObjectCreateExpression(ResMgrCodeTypeReference, new CodePrimitiveExpression(resMgrCtorParam), getAssembly); + + CodeStatement[] init = new CodeStatement[2]; + init[0] = new CodeVariableDeclarationStatement(ResMgrCodeTypeReference, tmpVarName, newResMgr); + init[1] = new CodeAssignStatement(field_resMgr, new CodeVariableReferenceExpression(tmpVarName)); + + resMgr.GetStatements.Add(new CodeConditionStatement(isResMgrNull, init)); + resMgr.GetStatements.Add(new CodeMethodReturnStatement(field_resMgr)); + + // Add a doc comment to the ResourceManager property + resMgr.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + resMgr.Comments.Add(new CodeCommentStatement(SR.GetString(SR.ResMgrPropertyComment), true)); + resMgr.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + + + // Emit code for Culture property + CodeFieldReferenceExpression field_resCulture = new CodeFieldReferenceExpression(null, CultureInfoFieldName); + culture.GetStatements.Add(new CodeMethodReturnStatement(field_resCulture)); + + CodePropertySetValueReferenceExpression newCulture = new CodePropertySetValueReferenceExpression(); + culture.SetStatements.Add(new CodeAssignStatement(field_resCulture, newCulture)); + + // Add a doc comment to Culture property + culture.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + culture.Comments.Add(new CodeCommentStatement(SR.GetString(SR.CulturePropertyComment1), true)); + culture.Comments.Add(new CodeCommentStatement(SR.GetString(SR.CulturePropertyComment2), true)); + culture.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + } + + // Helper method for DefineResourceFetchingProperty + // Truncates a comment string if it is too long and ensures it is safely encoded for XML. + private static string TruncateAndFormatCommentStringForOutput(string commentString) + { + if (commentString != null) + { + // Stop at some length + if (commentString.Length > DocCommentLengthThreshold) + commentString = SR.GetString(SR.StringPropertyTruncatedComment, commentString.Substring(0, DocCommentLengthThreshold)); + + // Encode the comment so it is safe for xml. SecurityElement.Escape is the only method I've found to do this. + commentString = System.Security.SecurityElement.Escape(commentString); + } + + return commentString; + } + + // Defines a property like this: + // {internal|internal} {static} Point MyPoint { + // get { + // Object obj = ResourceManager.GetObject("MyPoint", _resCulture); + // return (Point) obj; } + // } + // Special cases static vs. non-static, as well as internal vs. internal. + // Also note the resource name could contain spaces, etc, while the + // property name has to be a valid language identifier. + [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] + private static bool DefineResourceFetchingProperty(String propertyName, String resourceName, ResourceData data, CodeTypeDeclaration srClass, bool internalClass, bool useStatic) + { + CodeMemberProperty prop = new CodeMemberProperty(); + prop.Name = propertyName; + prop.HasGet = true; + prop.HasSet = false; + + Type type = data.Type; + + if (type == null) { + return false; + } + + if (type == typeof(MemoryStream)) + type = typeof(UnmanagedMemoryStream); + + // Ensure type is internalally visible. This is necessary to ensure + // users can access classes via a base type. Imagine a class like + // Image or Stream as a internalally available base class, then an + // internal type like MyBitmap or __UnmanagedMemoryStream as an + // internal implementation for that base class. For internalally + // available strongly typed resource classes, we must return the + // internal type. For simplicity, we'll do that for internal strongly + // typed resource classes as well. Ideally we'd also like to check + // for interfaces like IList, but I don't know how to do that without + // special casing collection interfaces & ignoring serialization + // interfaces or IDisposable. + while(!type.IsPublic) + type = type.BaseType; + + CodeTypeReference valueType = new CodeTypeReference(type); + prop.Type = valueType; + if (internalClass) + prop.Attributes = MemberAttributes.Assembly; + else + prop.Attributes = MemberAttributes.Public; + + if (useStatic) + prop.Attributes |= MemberAttributes.Static; + + // For Strings, emit this: + // return ResourceManager.GetString("name", _resCulture); + // For Streams, emit this: + // return ResourceManager.GetStream("name", _resCulture); + // For Objects, emit this: + // Object obj = ResourceManager.GetObject("name", _resCulture); + // return (MyValueType) obj; + CodePropertyReferenceExpression resMgr = new CodePropertyReferenceExpression(null, "ResourceManager"); + CodeFieldReferenceExpression resCultureField = new CodeFieldReferenceExpression((useStatic) ? null : new CodeThisReferenceExpression(), CultureInfoFieldName); + + bool isString = type == typeof(String); + bool isStream = type == typeof(UnmanagedMemoryStream) || type == typeof(MemoryStream); + String getMethodName = String.Empty; + String text = String.Empty; + String valueAsString = TruncateAndFormatCommentStringForOutput(data.ValueAsString); + String typeName = String.Empty; + + if (!isString) // Stream or Object + typeName = TruncateAndFormatCommentStringForOutput(type.ToString()); + + if (isString) + getMethodName = "GetString"; + else if (isStream) + getMethodName = "GetStream"; + else + getMethodName = "GetObject"; + + if (isString) + text = SR.GetString(SR.StringPropertyComment, valueAsString); + else { // Stream or Object + if (valueAsString == null || + String.Equals(typeName, valueAsString)) // If the type did not override ToString, ToString just returns the type name. + text = SR.GetString(SR.NonStringPropertyComment, typeName); + else + text = SR.GetString(SR.NonStringPropertyDetailedComment, typeName, valueAsString); + } + + prop.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + prop.Comments.Add(new CodeCommentStatement(text, true)); + prop.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + + CodeExpression getValue = new CodeMethodInvokeExpression(resMgr, getMethodName, new CodePrimitiveExpression(resourceName), resCultureField); + CodeMethodReturnStatement ret; + if (isString || isStream) { + ret = new CodeMethodReturnStatement(getValue); + } + else { + CodeVariableDeclarationStatement returnObj = new CodeVariableDeclarationStatement(typeof(Object), "obj", getValue); + prop.GetStatements.Add(returnObj); + + ret = new CodeMethodReturnStatement(new CodeCastExpression(valueType, new CodeVariableReferenceExpression("obj"))); + } + prop.GetStatements.Add(ret); + + srClass.Members.Add(prop); + return true; + } + + // Returns a valid identifier made from key, or null if it can't. + internal static String VerifyResourceName(String key, CodeDomProvider provider) + { + return VerifyResourceName(key, provider, false); + } + + // Once CodeDom provides a way to verify a namespace name, revisit this method. + private static String VerifyResourceName(String key, CodeDomProvider provider, bool isNameSpace) { + if (key == null) + throw new ArgumentNullException("key"); + if (provider == null) + throw new ArgumentNullException("provider"); + + foreach(char c in CharsToReplace) { + // For namespaces, allow . and :: + if (!(isNameSpace && (c == '.' || c == ':'))) + key = key.Replace(c, ReplacementChar); + } + + if (provider.IsValidIdentifier(key)) + return key; + + // Now try fixing up keywords like "for". + key = provider.CreateValidIdentifier(key); + if (provider.IsValidIdentifier(key)) + return key; + + // make one last ditch effort by prepending _. This fixes keys that start with a number + key = "_" + key; + if (provider.IsValidIdentifier(key)) + return key; + + return null; + } + + private static SortedList VerifyResourceNames(Dictionary resourceList, CodeDomProvider codeProvider, ArrayList errors, out Hashtable reverseFixupTable) + { + reverseFixupTable = new Hashtable(0, StringComparer.InvariantCultureIgnoreCase); + SortedList cleanedResourceList = new SortedList(StringComparer.InvariantCultureIgnoreCase, resourceList.Count); + + foreach(KeyValuePair entry in resourceList) { + String key = entry.Key; + + // Disallow a property named ResourceManager or Culture - we add + // those. (Any other properties we add also must be listed here) + // Also disallow resource values of type Void. + if (String.Equals(key, ResMgrPropertyName) || + String.Equals(key, CultureInfoPropertyName) || + typeof(void) == entry.Value.Type) + { + errors.Add(key); + continue; + } + + // Ignore WinForms design time and hierarchy information. + // Skip resources starting with $ or >>, like "$this.Text", + // ">>$this.Name" or ">>treeView1.Parent". + if ((key.Length > 0 && key[0] == '$') || + (key.Length > 1 && key[0] == '>' && key[1] == '>')) { + continue; + } + + + if (!codeProvider.IsValidIdentifier(key)) { + String newKey = VerifyResourceName(key, codeProvider, false); + if (newKey == null) { + errors.Add(key); + continue; + } + + // Now see if we've already mapped another key to the + // same name. + String oldDuplicateKey = (String) reverseFixupTable[newKey]; + if (oldDuplicateKey != null) { + // We can't handle this key nor the previous one. + // Remove the old one. + if (!errors.Contains(oldDuplicateKey)) + errors.Add(oldDuplicateKey); + if (cleanedResourceList.Contains(newKey)) + cleanedResourceList.Remove(newKey); + errors.Add(key); + continue; + } + reverseFixupTable[newKey] = key; + key = newKey; + } + ResourceData value = entry.Value; + if (!cleanedResourceList.Contains(key)) + cleanedResourceList.Add(key, value); + else { + // There was a case-insensitive conflict between two keys. + // Or possibly one key was fixed up in a way that conflicts + // with another key (ie, "A B" and "A_B"). + String fixedUp = (String) reverseFixupTable[key]; + if (fixedUp != null) { + if (!errors.Contains(fixedUp)) + errors.Add(fixedUp); + reverseFixupTable.Remove(key); + } + errors.Add(entry.Key); + cleanedResourceList.Remove(key); + } + } + return cleanedResourceList; + } + +} +} + diff --git a/src/XMakeTasks/System.Design.cs b/src/XMakeTasks/System.Design.cs new file mode 100644 index 00000000000..d0974599fde --- /dev/null +++ b/src/XMakeTasks/System.Design.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//------------------------------------------------------------------------------ +// +// Information Contained Herein is Proprietary and Confidential. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; +using System.Globalization; +using System.Resources; +using System.Text; +using System.Threading; +using System.Security.Permissions; + + +using System.ComponentModel; + + +namespace Microsoft.Build.Tasks +{ + [AttributeUsage(AttributeTargets.All)] + internal sealed class SRDescriptionAttribute : DescriptionAttribute + { + private bool _replaced = false; + + /// + /// Constructs a new sys description. + /// + /// + /// description text. + /// + public SRDescriptionAttribute(string description) : base(description) + { + } + + /// + /// Retrieves the description text. + /// + /// + /// description + /// + public override string Description + { + get + { + if (!_replaced) + { + _replaced = true; + DescriptionValue = SR.GetString(base.Description); + } + return base.Description; + } + } + } + + [AttributeUsage(AttributeTargets.All)] + internal sealed class SRCategoryAttribute : CategoryAttribute + { + public SRCategoryAttribute(string category) : base(category) + { + } + + protected override string GetLocalizedString(string value) + { + return SR.GetString(value); + } + } + + + /// + /// AutoGenerated resource class. Usage: + /// + /// string s = SR.GetString(SR.MyIdenfitier); + /// + + internal sealed class SR + { + internal const string ClassDocComment = "ClassDocComment"; + internal const string ClassComments1 = "ClassComments1"; + internal const string ClassComments3 = "ClassComments3"; + internal const string StringPropertyComment = "StringPropertyComment"; + internal const string StringPropertyTruncatedComment = "StringPropertyTruncatedComment"; + internal const string NonStringPropertyComment = "NonStringPropertyComment"; + internal const string NonStringPropertyDetailedComment = "NonStringPropertyDetailedComment"; + internal const string CulturePropertyComment1 = "CulturePropertyComment1"; + internal const string CulturePropertyComment2 = "CulturePropertyComment2"; + internal const string ResMgrPropertyComment = "ResMgrPropertyComment"; + internal const string MismatchedResourceName = "MismatchedResourceName"; + internal const string InvalidIdentifier = "InvalidIdentifier"; + + private static SR s_loader = null; + private ResourceManager _resources; + + internal SR() + { + _resources = new System.Resources.ResourceManager("System.Design", this.GetType().Assembly); + } + + private static SR GetLoader() + { + if (s_loader == null) + { + SR sr = new SR(); + Interlocked.CompareExchange(ref s_loader, sr, null); + } + return s_loader; + } + + private static CultureInfo Culture + { + get { return null/*use ResourceManager default, CultureInfo.CurrentUICulture*/; } + } + + public static ResourceManager Resources + { + get + { + return GetLoader()._resources; + } + } + + public static string GetString(string name, params object[] args) + { + SR sys = GetLoader(); + if (sys == null) + return null; + string res = sys._resources.GetString(name, SR.Culture); + + if (args != null && args.Length > 0) + { + for (int i = 0; i < args.Length; i++) + { + String value = args[i] as String; + if (value != null && value.Length > 1024) + { + args[i] = value.Substring(0, 1024 - 3) + "..."; + } + } + return String.Format(CultureInfo.CurrentCulture, res, args); + } + else + { + return res; + } + } + + public static string GetString(string name) + { + SR sys = GetLoader(); + if (sys == null) + return null; + return sys._resources.GetString(name, SR.Culture); + } + + public static string GetString(string name, out bool usedFallback) + { + // always false for this version of gensr + usedFallback = false; + return GetString(name); + } + + public static object GetObject(string name) + { + SR sys = GetLoader(); + if (sys == null) + return null; + return sys._resources.GetObject(name, SR.Culture); + } + } +} diff --git a/src/XMakeTasks/SystemState.cs b/src/XMakeTasks/SystemState.cs new file mode 100644 index 00000000000..67e0bf9d913 --- /dev/null +++ b/src/XMakeTasks/SystemState.cs @@ -0,0 +1,550 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Reflection; +using System.Collections; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Globalization; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; + +namespace Microsoft.Build.Tasks +{ + /// + /// Class is used to cache system state. + /// + [Serializable] + internal sealed class SystemState : StateFileBase, ISerializable + { + /// + /// State information for cached files kept at the SystemState instance level. + /// + private Hashtable _instanceLocalFileStateCache = new Hashtable(); + + /// + /// FileExists information is purely instance-local. It doesn't make sense to + /// cache this for long periods of time since there's no way (without actually + /// calling File.Exists) to tell whether the cache is out-of-date. + /// + private Hashtable _instanceLocalFileExists = new Hashtable(); + + /// + /// GetDirectories information is also purely instance-local. This information + /// is only considered good for the lifetime of the task (or whatever) that owns + /// this instance. + /// + private Hashtable _instanceLocalDirectories = new Hashtable(); + + /// + /// Additional level of caching kept at the process level. + /// + private static ConcurrentDictionary s_processWideFileStateCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// XML tables of installed assemblies. + /// + private RedistList _redistList = null; + + /// + /// True if the contents have changed. + /// + private bool _isDirty = false; + + /// + /// Delegate used internally. + /// + private GetLastWriteTime _getLastWriteTime = null; + + /// + /// Cached delegate. + /// + private GetAssemblyName _getAssemblyName = null; + + /// + /// Cached delegate. + /// + private GetAssemblyMetadata _getAssemblyMetadata = null; + + /// + /// Cached delegate. + /// + private FileExists _fileExists = null; + + /// + /// Cached delegate. + /// + private GetDirectories _getDirectories = null; + + /// + /// Cached delegate + /// + private GetAssemblyRuntimeVersion _getAssemblyRuntimeVersion = null; + + /// + /// Class that holds the current file state. + /// + [Serializable] + private sealed class FileState : ISerializable + { + /// + /// The last modified time for this file. + /// + private DateTime _lastModified; + + /// + /// The fusion name of this file. + /// + private AssemblyNameExtension _assemblyName = null; + + /// + /// The assemblies that this file depends on. + /// + internal AssemblyNameExtension[] dependencies = null; + + /// + /// The scatter files associated with this assembly. + /// + internal string[] scatterFiles = null; + + /// + /// FrameworkName the file was built against + /// + internal FrameworkName frameworkName = null; + + /// + /// The CLR runtime version for the assembly. + /// + internal string runtimeVersion = null; + + /// + /// Default construct. + /// + internal FileState() + { + } + + /// + /// Deserializing constuctor. + /// + internal FileState(SerializationInfo info, StreamingContext context) + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + _lastModified = info.GetDateTime("lastModified"); + _assemblyName = (AssemblyNameExtension)info.GetValue("assemblyName", typeof(AssemblyNameExtension)); + dependencies = (AssemblyNameExtension[])info.GetValue("dependencies", typeof(AssemblyNameExtension[])); + scatterFiles = (string[])info.GetValue("scatterFiles", typeof(string[])); + runtimeVersion = (string)info.GetValue("runtimeVersion", typeof(string)); + frameworkName = (FrameworkName)info.GetValue("frameworkName", typeof(FrameworkName)); + } + + /// + /// Serialize the contents of the class. + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + info.AddValue("lastModified", _lastModified); + info.AddValue("assemblyName", _assemblyName); + info.AddValue("dependencies", dependencies); + info.AddValue("scatterFiles", scatterFiles); + info.AddValue("runtimeVersion", runtimeVersion); + info.AddValue("frameworkName", frameworkName); + } + + /// + /// Get or set the assemblyName. + /// + /// + internal DateTime LastModified + { + get { return _lastModified; } + set { _lastModified = value; } + } + + /// + /// Get or set the assemblyName. + /// + /// + internal AssemblyNameExtension Assembly + { + get { return _assemblyName; } + set { _assemblyName = value; } + } + + /// + /// Get or set the runtimeVersion + /// + /// + internal string RuntimeVersion + { + get { return this.runtimeVersion; } + set { this.runtimeVersion = value; } + } + + /// + /// Get or set the framework name the file was built against + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Could be used in other assemblies")] + internal FrameworkName FrameworkNameAttribute + { + get { return this.frameworkName; } + set { this.frameworkName = value; } + } + } + + /// + /// Construct. + /// + internal SystemState() + { + } + + /// + /// Deserialize the contents of the class. + /// + internal SystemState(SerializationInfo info, StreamingContext context) + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + _instanceLocalFileStateCache = (Hashtable)info.GetValue("fileState", typeof(Hashtable)); + _isDirty = false; + } + + /// + /// Set the target framework paths. + /// This is used to optimize IO in the case of files requested from one + /// of the FX folders. + /// + /// + /// + internal void SetInstalledAssemblyInformation + ( + AssemblyTableInfo[] installedAssemblyTableInfos + ) + { + _redistList = RedistList.GetRedistList(installedAssemblyTableInfos); + } + + /// + /// Serialize the contents of the class. + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + ErrorUtilities.VerifyThrowArgumentNull(info, "info"); + + info.AddValue("fileState", _instanceLocalFileStateCache); + } + + /// + /// Flag that indicates + /// + /// + internal bool IsDirty + { + get { return _isDirty; } + } + + /// + /// Set the GetLastWriteTime delegate. + /// + /// Delegate used to get the last write time. + internal void SetGetLastWriteTime(GetLastWriteTime getLastWriteTimeValue) + { + _getLastWriteTime = getLastWriteTimeValue; + } + + /// + /// Cache the results of a GetAssemblyName delegate. + /// + /// The delegate. + /// Cached version of the delegate. + internal GetAssemblyName CacheDelegate(GetAssemblyName getAssemblyNameValue) + { + _getAssemblyName = getAssemblyNameValue; + return new GetAssemblyName(this.GetAssemblyName); + } + + /// + /// Cache the results of a GetAssemblyMetadata delegate. + /// + /// The delegate. + /// Cached version of the delegate. + internal GetAssemblyMetadata CacheDelegate(GetAssemblyMetadata getAssemblyMetadataValue) + { + _getAssemblyMetadata = getAssemblyMetadataValue; + return new GetAssemblyMetadata(this.GetAssemblyMetadata); + } + + /// + /// Cache the results of a FileExists delegate. + /// + /// The delegate. + /// Cached version of the delegate. + internal FileExists CacheDelegate(FileExists fileExistsValue) + { + _fileExists = fileExistsValue; + return new FileExists(this.FileExists); + } + + /// + /// Cache the results of a GetDirectories delegate. + /// + /// The delegate. + /// Cached version of the delegate. + internal GetDirectories CacheDelegate(GetDirectories getDirectoriesValue) + { + _getDirectories = getDirectoriesValue; + return new GetDirectories(this.GetDirectories); + } + + /// + /// Cache the results of a GetAssemblyRuntimeVersion delegate. + /// + /// The delegate. + /// Cached version of the delegate. + internal GetAssemblyRuntimeVersion CacheDelegate(GetAssemblyRuntimeVersion getAssemblyRuntimeVersion) + { + _getAssemblyRuntimeVersion = getAssemblyRuntimeVersion; + return new GetAssemblyRuntimeVersion(this.GetRuntimeVersion); + } + + /// + /// Retrieve the file state object for this path. Create if necessary. + /// + /// The name of the file. + /// The file state object. + private FileState GetFileState(string path) + { + // Is it in the process-wide cache? + FileState cacheFileState = null; + FileState processFileState = null; + SystemState.s_processWideFileStateCache.TryGetValue(path, out processFileState); + FileState instanceLocalFileState = instanceLocalFileState = (FileState)_instanceLocalFileStateCache[path]; + + // Sync the caches. + if (processFileState == null && instanceLocalFileState != null) + { + cacheFileState = instanceLocalFileState; + SystemState.s_processWideFileStateCache.TryAdd(path, instanceLocalFileState); + } + else if (processFileState != null && instanceLocalFileState == null) + { + cacheFileState = processFileState; + _instanceLocalFileStateCache[path] = processFileState; + } + else if (processFileState != null && instanceLocalFileState != null) + { + if (processFileState.LastModified > instanceLocalFileState.LastModified) + { + cacheFileState = processFileState; + _instanceLocalFileStateCache[path] = processFileState; + } + else + { + cacheFileState = instanceLocalFileState; + SystemState.s_processWideFileStateCache.TryAdd(path, instanceLocalFileState); + } + } + + // Still no--need to create. + if (cacheFileState == null) // Or check time stamp + { + cacheFileState = new FileState(); + cacheFileState.LastModified = _getLastWriteTime(path); + _instanceLocalFileStateCache[path] = cacheFileState; + SystemState.s_processWideFileStateCache.TryAdd(path, cacheFileState); + _isDirty = true; + } + else + { + // If time stamps have changed, then purge. + DateTime lastModified = _getLastWriteTime(path); + if (lastModified != cacheFileState.LastModified) + { + cacheFileState = new FileState(); + cacheFileState.LastModified = _getLastWriteTime(path); + _instanceLocalFileStateCache[path] = cacheFileState; + SystemState.s_processWideFileStateCache.TryAdd(path, cacheFileState); + _isDirty = true; + } + } + + return cacheFileState; + } + + /// + /// Cached implementation of GetAssemblyName. + /// + /// The path to the file + /// The assembly name. + private AssemblyNameExtension GetAssemblyName(string path) + { + // If the assembly is in an FX folder and its a well-known assembly + // then we can short-circuit the File IO involved with GetAssemblyName() + if (_redistList != null) + { + string extension = Path.GetExtension(path); + if (String.Compare(extension, ".dll", StringComparison.OrdinalIgnoreCase) == 0) + { + AssemblyEntry[] assemblyNames = _redistList.FindAssemblyNameFromSimpleName + ( + Path.GetFileNameWithoutExtension(path) + ); + + for (int i = 0; i < assemblyNames.Length; ++i) + { + string filename = Path.GetFileName(path); + string pathFromRedistList = Path.Combine(assemblyNames[i].FrameworkDirectory, filename); + + if (String.Equals(path, pathFromRedistList, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension(assemblyNames[i].FullName); + } + } + } + } + + + // Not a well-known FX assembly so now check the cache. + FileState fileState = GetFileState(path); + if (fileState.Assembly == null) + { + fileState.Assembly = _getAssemblyName(path); + + // Certain assemblies, like mscorlib may not have metadata. + // Avoid continuously calling getAssemblyName on these files by + // recording these as having an empty name. + if (fileState.Assembly == null) + { + fileState.Assembly = AssemblyNameExtension.UnnamedAssembly; + } + _isDirty = true; + } + + if (fileState.Assembly.IsUnnamedAssembly) + { + return null; + } + + return fileState.Assembly; + } + + /// + /// Cached implementation. Given a path, crack it open and retrieve runtimeversion for the assembly. + /// + /// Path to the assembly. + private string GetRuntimeVersion(string path) + { + FileState fileState = GetFileState(path); + if (String.IsNullOrEmpty(fileState.RuntimeVersion)) + { + fileState.RuntimeVersion = _getAssemblyRuntimeVersion(path); + _isDirty = true; + } + + return fileState.RuntimeVersion; + } + + /// + /// Cached implementation. Given an assembly name, crack it open and retrieve the list of dependent + /// assemblies and the list of scatter files. + /// + /// Path to the assembly. + /// Receives the list of dependencies. + /// Receives the list of associated scatter files. + private void GetAssemblyMetadata + ( + string path, + out AssemblyNameExtension[] dependencies, + out string[] scatterFiles, + out FrameworkName frameworkName + ) + { + FileState fileState = GetFileState(path); + if (fileState.dependencies == null) + { + _getAssemblyMetadata + ( + path, + out fileState.dependencies, + out fileState.scatterFiles, + out fileState.frameworkName + ); + + _isDirty = true; + } + + dependencies = fileState.dependencies; + scatterFiles = fileState.scatterFiles; + frameworkName = fileState.frameworkName; + } + + /// + /// Cached implementation of GetDirectories. + /// + /// + /// + /// + private string[] GetDirectories(string path, string pattern) + { + // Only cache the *. pattern. This is by far the most common pattern + // and generalized caching would require a call to Path.Combine which + // is a string-copy. + if (pattern == "*.") + { + object cached = _instanceLocalDirectories[path]; + if (cached == null) + { + string[] directories = _getDirectories(path, pattern); + _instanceLocalDirectories[path] = directories; + return directories; + } + return (string[])cached; + } + + // This path is currently uncalled. Use assert to tell the dev that adds a new code-path + // that this is an unoptimized path. + Debug.Assert(false, "Using slow-path in SystemState.GetDirectories, was this intentional?"); + + return _getDirectories(path, pattern); + } + + /// + /// Cached implementation of FileExists. + /// + /// Path to file. + /// True if the file exists. + private bool FileExists(string path) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // FIRST -- Look in the primary cache for this path. + ///////////////////////////////////////////////////////////////////////////////////////////// + object flag = _instanceLocalFileExists[path]; + if (flag != null) + { + return (bool)flag; + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // SECOND -- fall back to plain old File.Exists and cache the result. + ///////////////////////////////////////////////////////////////////////////////////////////// + bool exists = _fileExists(path); + _instanceLocalFileExists[path] = exists; + return exists; + } + } +} diff --git a/src/XMakeTasks/TaskExtension.cs b/src/XMakeTasks/TaskExtension.cs new file mode 100644 index 00000000000..334a90f8fda --- /dev/null +++ b/src/XMakeTasks/TaskExtension.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// A small intermediate class for MSBuild tasks, see also TaskLoadInSeparateAppDomainExtension + /// + abstract public class TaskExtension : Task + { + #region Constructors + + internal TaskExtension() : + base(AssemblyResources.PrimaryResources, "MSBuild.") + { + _logExtension = new TaskLoggingHelperExtension( + this, + AssemblyResources.PrimaryResources, + AssemblyResources.SharedResources, + "MSBuild."); + } + + #endregion + + #region Properties + + /// + /// Gets an instance of a TaskLoggingHelperExtension class containing task logging methods. + /// + /// The logging helper object. + new public TaskLoggingHelper Log + { + get + { + return _logExtension; + } + } + + // the logging helper + private TaskLoggingHelperExtension _logExtension; + + #endregion + } +} diff --git a/src/XMakeTasks/TlbImp.cs b/src/XMakeTasks/TlbImp.cs new file mode 100644 index 00000000000..ac16bcfb43d --- /dev/null +++ b/src/XMakeTasks/TlbImp.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// ToolTask that wraps TlbImp.exe, which generates assemblies from type libraries. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Resources; +using System.Reflection; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Linq; + +namespace Microsoft.Build.Tasks +{ + /// + /// Main class for the COM reference resolution task + /// + public sealed partial class ResolveComReference + { + /// + /// Passed to the "Transform" property on the TlbImp task to indicate + /// what transforms, if any, to apply to the type library during + /// assembly generation + /// + internal enum TlbImpTransformFlags + { + /// + /// No transforms should be applied. + /// + None, + + /// + /// Transforms [out, retval] parameters of methods on dispatch-only + /// interfaces into return values. + /// + TransformDispRetVals, + + /// + /// Mark all value classes as serializable. + /// + SerializableValueClasses + } + + /// + /// Defines the "TlbImp" MSBuild task, which enables using TlbImp.exe + /// to generate assemblies from type libraries. + /// + internal class TlbImp : AxTlbBaseTask + { + #region Properties + /* + Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.10719.0 + Copyright (C) Microsoft Corporation. All rights reserved. + + Syntax: TlbImp TypeLibName [Options] + Options: + /out:FileName File name of assembly to be produced + /namespace:Namespace Namespace of the assembly to be produced + /asmversion:Version Version number of the assembly to be produced + /reference:FileName File name of assembly to use to resolve references + /tlbreference:FileName File name of typelib to use to resolve references + /publickey:FileName File containing strong name public key + /keyfile:FileName File containing strong name key pair + /keycontainer:FileName Key container holding strong name key pair + /delaysign Force strong name delay signing + /product:Product The name of the product with which this assembly + is distributed + /productversion:Version The version of the product with which this + assembly is distributed + /company:Company The name of the company that produced this + assembly + /copyright:Copyright Describes all copyright notices, trademarks, and + registered trademarks that apply to this assembly + /trademark:Trademark Describes all trademarks and registered trademarks + that apply to this assembly + /unsafe Produce interfaces without runtime security checks + /noclassmembers Prevents TlbImp from adding members to classes + /nologo Prevents TlbImp from displaying logo + /silent Suppresses all output except for errors + /silence:WarningNumber Suppresses output for the given warning (Can not + be used with /silent) + /verbose Displays extra information + /primary Produce a primary interop assembly + /sysarray Import SAFEARRAY as System.Array + /machine:MachineType Create an assembly for the specified machine type + /transform:TransformName Perform the specified transformation + /strictref Only use assemblies specified using /reference and + registered PIAs + /strictref:nopia Only use assemblies specified using /reference and + ignore PIAs + /? or /help Display this usage message + + The assembly version must be specified as: Major.Minor.Build.Revision. + + Multiple reference assemblies can be specified by using the /reference option + multiple times. + + Supported machine types: + X86 + X64 + Itanium + Agnostic + + Supported transforms: + SerializableValueClasses Mark all value classes as serializable + DispRet Apply the [out, retval] parameter transformation + to methods of disp only interfaces + + A resource ID can optionally be appended to the TypeLibName when importing a + type library from a module containing multiple type libraries. + example: TlbImp MyModule.dll\1 + */ + + /// + /// Type library being imported to an assembly. + /// + public string TypeLibName + { + get { return (string)Bag["TypeLibName"]; } + set { Bag["TypeLibName"] = value; } + } + + /// + /// Namespace of the generated assembly + /// + public string AssemblyNamespace + { + get { return (string)Bag["AssemblyNamespace"]; } + set { Bag["AssemblyNamespace"] = value; } + } + + /// + /// Version of the generated assembly + /// + public Version AssemblyVersion + { + get { return (Version)Bag["AssemblyVersion"]; } + set { Bag["AssemblyVersion"] = value; } + } + + /// + /// Create an assembly for the specified machine type + /// Supported machine types: + /// X86 + /// X64 + /// Itanium + /// Agnostic + /// + public string Machine + { + get { return (string)Bag["Machine"]; } + set { Bag["Machine"] = value; } + } + + /// + /// If true, suppresses displaying the logo + /// + public bool NoLogo + { + get { return GetBoolParameterWithDefault("NoLogo", false); } + set { Bag["NoLogo"] = value; } + } + + /// + /// File name of assembly to be produced. + /// + public string OutputAssembly + { + get { return (string)Bag["OutputAssembly"]; } + set { Bag["OutputAssembly"] = value; } + } + + /// + /// If true, prevents TlbImp from adding members to classes + /// + public bool PreventClassMembers + { + get { return GetBoolParameterWithDefault("PreventClassMembers", false); } + set { Bag["PreventClassMembers"] = value; } + } + + /// + /// If true, import the SAFEARRAY type as System.Arrays + /// + public bool SafeArrayAsSystemArray + { + get { return GetBoolParameterWithDefault("SafeArrayAsSystemArray", false); } + set { Bag["SafeArrayAsSystemArray"] = value; } + } + + /// + /// If true, prevents AxImp from displaying success message. + /// + public bool Silent + { + get { return GetBoolParameterWithDefault("Silent", false); } + set { Bag["Silent"] = value; } + } + + /// + /// Transformation to be applied to the resulting assembly. + /// + public TlbImpTransformFlags Transform + { + get { return GetTlbImpTransformFlagsParameterWithDefault("Transform", TlbImpTransformFlags.None); } + set { Bag["Transform"] = value; } + } + + /// + /// If true, AxImp prints more information. + /// + public bool Verbose + { + get { return GetBoolParameterWithDefault("Verbose", false); } + set { Bag["Verbose"] = value; } + } + + /// + /// References to dependency assemblies. + /// + public string[] ReferenceFiles + { + get { return (string[])Bag["ReferenceFiles"]; } + set { Bag["ReferenceFiles"] = value; } + } + + #endregion // Properties + + #region ToolTask Members + + /// + /// Returns the name of the tool to execute + /// + protected override string ToolName + { + get { return "TlbImp.exe"; } + } + + /// + /// Fills the provided CommandLineBuilderExtension with all the command line options used when + /// executing this tool + /// + /// Gets filled with command line commands + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + // .ocx file being imported + commandLine.AppendFileNameIfNotNull(TypeLibName); + + // options + commandLine.AppendSwitchIfNotNull("/asmversion:", (AssemblyVersion != null) ? AssemblyVersion.ToString() : null); + commandLine.AppendSwitchIfNotNull("/namespace:", AssemblyNamespace); + commandLine.AppendSwitchIfNotNull("/machine:", Machine); + commandLine.AppendWhenTrue("/noclassmembers", Bag, "PreventClassMembers"); + commandLine.AppendWhenTrue("/nologo", Bag, "NoLogo"); + commandLine.AppendSwitchIfNotNull("/out:", OutputAssembly); + commandLine.AppendWhenTrue("/silent", Bag, "Silent"); + commandLine.AppendWhenTrue("/sysarray", Bag, "SafeArrayAsSystemArray"); + commandLine.AppendSwitchIfNotNull("/transform:", ConvertTransformFlagsToCommandLineCommand(Transform)); + commandLine.AppendWhenTrue("/verbose", Bag, "Verbose"); + if (ReferenceFiles != null) + { + foreach (var referenceFile in ReferenceFiles) + { + commandLine.AppendSwitchIfNotNull("/reference:", referenceFile); + } + } + + base.AddCommandLineCommands(commandLine); + } + + /// + /// Validates the parameters passed to the task + /// + /// True if parameters are valid + protected override bool ValidateParameters() + { + // Verify that we were actually passed a .tlb to import + if (String.IsNullOrEmpty(TypeLibName)) + { + Log.LogErrorWithCodeFromResources("TlbImp.NoInputFileSpecified"); + return false; + } + + // Verify that an allowed combination of TlbImpTransformFlags has been + // passed to the Transform property. + if (!ValidateTransformFlags()) + { + Log.LogErrorWithCodeFromResources("TlbImp.InvalidTransformParameter", Transform.ToString()); + return false; + } + + return base.ValidateParameters(); + } + + /// + /// Returns the TlbImpTransformFlags value stored in the hashtable under the provided + /// parameter, or the default value passed if the value in the hashtable is null + /// + /// The parameter used to retrieve the value from the hashtable + /// The default value to return if the hashtable value is null + /// The value contained in the hashtable, or if that's null, the default value passed to the method + private TlbImpTransformFlags GetTlbImpTransformFlagsParameterWithDefault(string parameterName, TlbImpTransformFlags defaultValue) + { + object obj = Bag[parameterName]; + return (obj == null) ? defaultValue : (TlbImpTransformFlags)obj; + } + + /// + /// Verifies that an allowed combination of TlbImpTransformFlags has been + /// passed to the Transform property. + /// + /// True if Transform is valid and false otherwise + private bool ValidateTransformFlags() + { + // Any flag on its own is fine ... + switch (Transform) + { + case TlbImpTransformFlags.None: + return true; + case TlbImpTransformFlags.SerializableValueClasses: + return true; + case TlbImpTransformFlags.TransformDispRetVals: + return true; + } + + // ... But any and all other combinations of flags are disallowed. + return false; + } + + /// + /// Converts a given flag to the equivalent parameter passed to the /transform: + /// option of tlbimp.exe + /// + /// The TlbImpTransformFlags being converted + /// A string that can be passed to /transform: on the command line + private string ConvertTransformFlagsToCommandLineCommand(TlbImpTransformFlags flags) + { + switch (flags) + { + case TlbImpTransformFlags.None: + return null; + case TlbImpTransformFlags.SerializableValueClasses: + return "SerializableValueClasses"; + case TlbImpTransformFlags.TransformDispRetVals: + return "DispRet"; + } + + return null; + } + + #endregion // ToolTask Members + } + } +} diff --git a/src/XMakeTasks/TlbReference.cs b/src/XMakeTasks/TlbReference.cs new file mode 100644 index 00000000000..fb4970c9b38 --- /dev/null +++ b/src/XMakeTasks/TlbReference.cs @@ -0,0 +1,467 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using System.Security; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Linq; + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; +using UtilitiesProcessorArchitecture = Microsoft.Build.Utilities.ProcessorArchitecture; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /* + * Class: TlbReference + * + * COM reference wrapper class for the tlbimp tool. + * + */ + internal class TlbReference : AxTlbBaseReference, ITypeLibImporterNotifySink + { + #region Constructors + /// + /// internal constructor + /// + /// task logger instance used for logging + /// callback interface for resolving dependent COM refs/NET assemblies + /// cached reference information (typelib pointer, original task item, typelib name etc.) + /// reference name (for better logging experience) + /// directory we should write the wrapper to + /// delay sign wrappers? + /// file containing public/private keys + /// container name for public/private keys + /// True if GenerateWrapper() should generate the wrapper out-of-proc using tlbimp.exe + /// Path to the SDK tools directory where tlbimp.exe can be found + /// BuildEngine of parent task; needed for logging purposes when generating wrapper out-of-proc + internal TlbReference(TaskLoggingHelper taskLoggingHelper, bool silent, IComReferenceResolver resolverCallback, IEnumerable referenceFiles, ComReferenceInfo referenceInfo, string itemName, string outputDirectory, bool hasTemporaryWrapper, + bool delaySign, string keyFile, string keyContainer, bool noClassMembers, string targetProcessorArchitecture, bool includeTypeLibVersionInName, bool executeAsTool, string sdkToolsPath, IBuildEngine buildEngine, string[] environmentVariables) + : base(taskLoggingHelper, silent, resolverCallback, referenceInfo, itemName, outputDirectory, delaySign, keyFile, keyContainer, includeTypeLibVersionInName, executeAsTool, sdkToolsPath, buildEngine, environmentVariables) + { + _hasTemporaryWrapper = hasTemporaryWrapper; + _noClassMembers = noClassMembers; + _targetProcessorArchitecture = targetProcessorArchitecture; + _referenceFiles = referenceFiles; + } + + #endregion + + #region Properties + + /// + /// does this reference have a temporary (i.e. written to tmp directory) wrapper? + /// + private bool HasTemporaryWrapper + { + get + { + return _hasTemporaryWrapper; + } + } + + private bool _hasTemporaryWrapper; + + /// + /// directory we should write the wrapper to + /// + protected override string OutputDirectory + { + get + { + return (HasTemporaryWrapper) ? Path.GetTempPath() : base.OutputDirectory; + } + } + + private bool _noClassMembers = false; + private string _targetProcessorArchitecture = null; + private IEnumerable _referenceFiles = null; + + #endregion + + #region Methods + + /* + * Method: GetWrapperFileName + * + * Constructs the wrapper file name from a type library name. + */ + protected override string GetWrapperFileNameInternal(string typeLibName) + { + return GetWrapperFileName("Interop.", typeLibName, IncludeTypeLibVersionInName, ReferenceInfo.attr.wMajorVerNum, ReferenceInfo.attr.wMinorVerNum); + } + + /// + /// Static version of GetWrapperFileName, as it really doesn't depend on + /// anything specific to the class, and this way it can be called using + /// TlbReference.GetWrapperFileName from outside + /// + /// The typelib to generate the wrapper name for + /// The appropriate wrapper filename + internal static string GetWrapperFileName(string typeLibName) + { + return GetWrapperFileName(typeLibName, false /* don't include version in name */, 1, 0 /* v1.0 = some random version that won't be used */); + } + + /// + /// Static version of GetWrapperFileName, as it really doesn't depend on + /// anything specific to the class, and this way it can be called using + /// TlbReference.GetWrapperFileName from outside + /// + /// The typelib to generate the wrapper name for + /// True if the interop name should include the typelib's version + /// Major version number to append to the interop DLL's name + /// Minor version number to append to the interop DLL's name + /// The appropriate wrapper filename + internal static string GetWrapperFileName(string typeLibName, bool includeTypeLibVersionInName, short majorVerNum, short minorVerNum) + { + return GetWrapperFileName("Interop.", typeLibName, includeTypeLibVersionInName, majorVerNum, minorVerNum); + } + + /* + * Method: FindExistingWrapper + * + * Checks if there's a preexisting wrapper for this reference. + */ + internal override bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp) + { + if (!HasTemporaryWrapper) + { + return base.FindExistingWrapper(out wrapperInfo, componentTimestamp); + } + + // if this reference has a temporary wrapper, it can't possibly have a preexisting one + wrapperInfo = null; + return false; + } + + /* + * Method: GenerateWrapper + * + * Generates a wrapper for this reference. + */ + internal bool GenerateWrapper(out ComReferenceWrapperInfo wrapperInfo) + { + wrapperInfo = null; + + string rootNamespace = ReferenceInfo.typeLibName; + string wrapperPath = GetWrapperPath(); + bool generateWrapperSucceeded = true; + + if (ExecuteAsTool) + { + // delegate generation of the assembly to an instance of the TlbImp ToolTask. MUST + // HAVE SET SDKTOOLSPATH TO THE TARGET SDK TO WORK + var tlbImp = new ResolveComReference.TlbImp(); + + tlbImp.BuildEngine = BuildEngine; + tlbImp.EnvironmentVariables = EnvironmentVariables; + tlbImp.DelaySign = DelaySign; + tlbImp.KeyContainer = KeyContainer; + tlbImp.KeyFile = KeyFile; + tlbImp.OutputAssembly = wrapperPath; + tlbImp.ToolPath = ToolPath; + tlbImp.TypeLibName = ReferenceInfo.fullTypeLibPath; + tlbImp.AssemblyNamespace = rootNamespace; + tlbImp.AssemblyVersion = null; + tlbImp.PreventClassMembers = _noClassMembers; + tlbImp.SafeArrayAsSystemArray = true; + tlbImp.Silent = Silent; + tlbImp.Transform = ResolveComReference.TlbImpTransformFlags.TransformDispRetVals; + if (_referenceFiles != null) + { + // Issue is that there may be reference dependencies that need to be passed in. It is possible + // that the set of references will also contain the file that is meant to be written here (when reference resolution + // found the file in the output folder). We need to filter out this case. + var fullPathToOutput = Path.GetFullPath(wrapperPath); // Current directory is the directory of the project file. + tlbImp.ReferenceFiles = _referenceFiles.Where(rf => String.Compare(fullPathToOutput, rf, StringComparison.OrdinalIgnoreCase) != 0).ToArray(); + } + + switch (_targetProcessorArchitecture) + { + case UtilitiesProcessorArchitecture.MSIL: + tlbImp.Machine = "Agnostic"; + break; + case UtilitiesProcessorArchitecture.AMD64: + tlbImp.Machine = "X64"; + break; + case UtilitiesProcessorArchitecture.IA64: + tlbImp.Machine = "Itanium"; + break; + case UtilitiesProcessorArchitecture.X86: + tlbImp.Machine = "X86"; + break; + case UtilitiesProcessorArchitecture.ARM: + tlbImp.Machine = "ARM"; + break; + case null: + break; + default: + // Transmit the flag directly from the .targets files and rely on tlbimp.exe to produce a good error message. + tlbImp.Machine = _targetProcessorArchitecture; + break; + } + + generateWrapperSucceeded = tlbImp.Execute(); + + // store the wrapper info... + wrapperInfo = new ComReferenceWrapperInfo(); + wrapperInfo.path = (HasTemporaryWrapper) ? null : wrapperPath; + // Changed to ReflectionOnlyLoadFrom, related to bug: + // RCR: Bad COM-interop assemblies being generated when using 64-bit MSBuild to build a project that is targeting 32-bit platform + // The original call to UnsafeLoadFrom loads the assembly in preparation for execution. If the assembly is x86 and this is x64 msbuild.exe then we + // have problems (UnsafeLoadFrom will fail). We only use this assembly for reference resolution so we don't need to be ready to execute the code. + // + // Its actually not clear to me that we even need to load the assembly at all. Reference resoluton is only used in the !ExecuteAsTool which is not + // where we are right now. + // + // If we really do need to load it then: + // + // wrapperInfo.assembly = Assembly.ReflectionOnlyLoadFrom(wrapperPath); + } + else + { + // use framework classes in-proc to generate the assembly + TypeLibConverter converter = new TypeLibConverter(); + + AssemblyBuilder assemblyBuilder = null; + StrongNameKeyPair keyPair; + byte[] publicKey; + + GetAndValidateStrongNameKey(out keyPair, out publicKey); + + try + { + TypeLibImporterFlags flags = TypeLibImporterFlags.SafeArrayAsSystemArray | TypeLibImporterFlags.TransformDispRetVals; + + if (_noClassMembers) + { + flags |= TypeLibImporterFlags.PreventClassMembers; + } + + switch (_targetProcessorArchitecture) + { + case UtilitiesProcessorArchitecture.MSIL: + flags |= TypeLibImporterFlags.ImportAsAgnostic; + break; + case UtilitiesProcessorArchitecture.AMD64: + flags |= TypeLibImporterFlags.ImportAsX64; + break; + case UtilitiesProcessorArchitecture.IA64: + flags |= TypeLibImporterFlags.ImportAsItanium; + break; + case UtilitiesProcessorArchitecture.X86: + flags |= TypeLibImporterFlags.ImportAsX86; + break; + case UtilitiesProcessorArchitecture.ARM: + flags |= TypeLibImporterFlags.ImportAsArm; + break; + + default: + // Let the type importer decide. + break; + } + + // Start the conversion process. We'll get callbacks on ITypeLibImporterNotifySink to resolve dependent refs. + assemblyBuilder = converter.ConvertTypeLibToAssembly(ReferenceInfo.typeLibPointer, wrapperPath, + flags, this, publicKey, keyPair, rootNamespace, null); + } + catch (COMException ex) + { + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.ErrorCreatingWrapperAssembly", ItemName, ex.Message); + } + + throw new ComReferenceResolutionException(ex); + } + + // if we're done, and this is not a temporary wrapper, write it out to disk + if (!HasTemporaryWrapper) + { + WriteWrapperToDisk(assemblyBuilder, wrapperPath); + } + + // store the wrapper info... + wrapperInfo = new ComReferenceWrapperInfo(); + wrapperInfo.path = (HasTemporaryWrapper) ? null : wrapperPath; + wrapperInfo.assembly = assemblyBuilder; + } + + // ...and we're done! + return generateWrapperSucceeded; + } + + /* + * Method: WriteWrapperToDisk + * + * Writes the generated wrapper out to disk. Should only be called for permanent wrappers. + */ + private void WriteWrapperToDisk(AssemblyBuilder assemblyBuilder, string wrapperPath) + { + try + { + FileInfo wrapperFile = new FileInfo(wrapperPath); + + if (wrapperFile.Exists) + { + wrapperFile.Delete(); + } + + switch (_targetProcessorArchitecture) + { + case UtilitiesProcessorArchitecture.X86: + assemblyBuilder.Save + ( + wrapperFile.Name, + PortableExecutableKinds.ILOnly | PortableExecutableKinds.Required32Bit, + ImageFileMachine.I386 + ); + break; + case UtilitiesProcessorArchitecture.AMD64: + assemblyBuilder.Save + ( + wrapperFile.Name, + PortableExecutableKinds.ILOnly | PortableExecutableKinds.PE32Plus, + ImageFileMachine.AMD64 + ); + break; + case UtilitiesProcessorArchitecture.IA64: + assemblyBuilder.Save + ( + wrapperFile.Name, + PortableExecutableKinds.ILOnly | PortableExecutableKinds.PE32Plus, + ImageFileMachine.IA64 + ); + break; + case UtilitiesProcessorArchitecture.ARM: + assemblyBuilder.Save + ( + wrapperFile.Name, + PortableExecutableKinds.ILOnly | PortableExecutableKinds.Required32Bit, + ImageFileMachine.ARM + ); + break; + case UtilitiesProcessorArchitecture.MSIL: + default: + // If no target processor architecture was passed, we assume MSIL; calling Save + // with no parameters should be equivalent to saving as ILOnly. + assemblyBuilder.Save(wrapperFile.Name); + break; + } + + // AssemblyBuilder doesn't always throw when it's supposed to write stuff to a non-writable + // network path. Make sure that the assembly actually got written to where we wanted it to. + File.GetLastWriteTime(wrapperPath); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.ErrorCreatingWrapperAssembly", ItemName, e.Message); + } + + throw new ComReferenceResolutionException(e); + } + } + + #endregion + + #region ITypeLibImporterNotifySink Members + + /* + * Method: ITypeLibImporterNotifySink.ResolveRef + * + * Implementation of ITypeLibImporterNotifySink.ResolveRef - this method is called by the NDP type lib converter + * to resolve dependencies. + * We should never return null here - it's not documented as the proper way of failing dependency resolution. + * Instead, we use an exception to abort the conversion process. + */ + Assembly ITypeLibImporterNotifySink.ResolveRef(object objTypeLib) + { + TYPELIBATTR attr; + + // get attributes for our dependent typelib + ITypeLib typeLib = (ITypeLib)objTypeLib; + ComReference.GetTypeLibAttrForTypeLib(ref typeLib, out attr); + + ComReferenceWrapperInfo wrapperInfo; + + // call our callback to do the dirty work for us + if (!ResolverCallback.ResolveComClassicReference(attr, base.OutputDirectory, null, null, out wrapperInfo)) + { + if (!Silent) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.FailedToResolveDependentComReference", attr.guid, attr.wMajorVerNum, attr.wMinorVerNum); + } + + throw new ComReferenceResolutionException(); + } + + Debug.Assert(wrapperInfo.assembly != null, "Successfully resolved assembly cannot be null!"); + if (wrapperInfo.assembly == null) + throw new ComReferenceResolutionException(); + + if (!Silent) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ResolvedDependentComReference", + attr.guid, attr.wMajorVerNum, attr.wMinorVerNum, wrapperInfo.path); + } + + Debug.Assert(wrapperInfo.assembly != null, "Expected a non-null wrapperInfo.assembly. It should have been loaded in GenerateWrapper if it was going to be necessary."); + return wrapperInfo.assembly; + } + + /* + * Method: ITypeLibImporterNotifySink.ReportEvent + * + * Implementation of ITypeLibImporterNotifySink.ReportEvent - this method gets called by NDP type lib converter + * to report various messages (like "type blahblah converted" or "failed to convert type blahblah"). + */ + void ITypeLibImporterNotifySink.ReportEvent(ImporterEventKind eventKind, int eventCode, string eventMsg) + { + if (!Silent) + { + if (eventKind == ImporterEventKind.ERROR_REFTOINVALIDTYPELIB) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.ResolutionWarning", ReferenceInfo.SourceItemSpec, ReferenceInfo.strippedTypeLibPath, eventMsg); + } + else if (eventKind == ImporterEventKind.NOTIF_CONVERTWARNING) + { + Log.LogWarningWithCodeFromResources("ResolveComReference.ResolutionWarning", ReferenceInfo.SourceItemSpec, ReferenceInfo.strippedTypeLibPath, eventMsg); + } + else if (eventKind == ImporterEventKind.NOTIF_TYPECONVERTED) + { + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ResolutionMessage", ReferenceInfo.SourceItemSpec, ReferenceInfo.strippedTypeLibPath, eventMsg); + } + else + { + Debug.Assert(false, "Unknown ImporterEventKind value"); + Log.LogMessageFromResources(MessageImportance.Low, "ResolveComReference.ResolutionMessage", ReferenceInfo.SourceItemSpec, ReferenceInfo.strippedTypeLibPath, eventMsg); + } + } + else + { + if (eventKind != ImporterEventKind.ERROR_REFTOINVALIDTYPELIB && eventKind != ImporterEventKind.NOTIF_CONVERTWARNING && eventKind != ImporterEventKind.NOTIF_TYPECONVERTED) + { + Debug.Assert(false, "Unknown ImporterEventKind value"); + } + } + } + + #endregion + } +} diff --git a/src/XMakeTasks/ToolTaskExtension.cs b/src/XMakeTasks/ToolTaskExtension.cs new file mode 100644 index 00000000000..0cc27aedcf0 --- /dev/null +++ b/src/XMakeTasks/ToolTaskExtension.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// A small intermediate class between ToolTask and classes using it in XMakeTasks, implementing functionality + /// that we didn't want to expose in Utilities + /// + /// + /// This class has to be public because the tasks that derive from it are public. + /// Ideally we would like this class to be internal, but C# does not allow a base class + /// to be less accessible than its derived classes. + /// + public abstract class ToolTaskExtension : ToolTask + { + #region Constructors + + /// + /// Default constructor. + /// + internal ToolTaskExtension() : + base(AssemblyResources.PrimaryResources, "MSBuild.") + { + _logExtension = new TaskLoggingHelperExtension( + this, + AssemblyResources.PrimaryResources, + AssemblyResources.SharedResources, + "MSBuild."); + } + + #endregion + + #region Properties + + /// + /// Gets an instance of a TaskLoggingHelperExtension class containing task logging methods. + /// + /// The logging helper object. + new public TaskLoggingHelper Log + { + get + { + return _logExtension; + } + } + + // the logging helper + private TaskLoggingHelperExtension _logExtension; + + /// + /// Whether this ToolTaskExtension has logged any errors + /// + protected override bool HasLoggedErrors + { + get + { + return (Log.HasLoggedErrors || base.HasLoggedErrors); + } + } + + /// + /// Gets the collection of parameters used by the derived task class. + /// + /// Parameter bag. + protected internal Hashtable Bag + { + get + { + return _bag; + } + } + + private Hashtable _bag = new Hashtable(); + + #endregion + + #region Methods + + /// + /// Get a bool parameter and return a default if its not present + /// in the hash table. + /// + /// + /// + /// + /// + protected internal bool GetBoolParameterWithDefault(string parameterName, bool defaultValue) + { + object obj = _bag[parameterName]; + return (obj == null) ? defaultValue : (bool)obj; + } + + /// + /// Get an int parameter and return a default if its not present + /// in the hash table. + /// + /// + /// + /// + /// + protected internal int GetIntParameterWithDefault(string parameterName, int defaultValue) + { + object obj = _bag[parameterName]; + return (obj == null) ? defaultValue : (int)obj; + } + + /// + /// Returns the command line switch used by the tool executable to specify the response file + /// Will only be called if the task returned a non empty string from GetResponseFileCommands + /// Called after ValidateParameters, SkipTaskExecution and GetResponseFileCommands + /// + /// full path to the temporarily created response file + /// + override protected string GenerateResponseFileCommands() + { + CommandLineBuilderExtension commandLineBuilder = new CommandLineBuilderExtension(); + AddResponseFileCommands(commandLineBuilder); + return commandLineBuilder.ToString(); + } + + /// + /// Returns a string with those switches and other information that can't go into a response file and + /// must go directly onto the command line. + /// Called after ValidateParameters and SkipTaskExecution + /// + /// + override protected string GenerateCommandLineCommands() + { + CommandLineBuilderExtension commandLineBuilder = new CommandLineBuilderExtension(); + AddCommandLineCommands(commandLineBuilder); + return commandLineBuilder.ToString(); + } + + /// + /// Fills the provided CommandLineBuilderExtension with those switches and other information that can go into a response file. + /// + /// + protected internal virtual void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + } + + /// + /// Fills the provided CommandLineBuilderExtension with those switches and other information that can't go into a response file and + /// must go directly onto the command line. + /// + /// + /// true, if successful + protected internal virtual void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + } + + #endregion + } +} diff --git a/src/XMakeTasks/Touch.cs b/src/XMakeTasks/Touch.cs new file mode 100644 index 00000000000..a39b1a81545 --- /dev/null +++ b/src/XMakeTasks/Touch.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines the touch task. + /// + public class Touch : TaskExtension + { + private bool _forceTouch; + private bool _alwaysCreate; + private string _specificTime; + private ITaskItem[] _files; + private ITaskItem[] _touchedFiles; + + //----------------------------------------------------------------------------------- + // Constructor + //----------------------------------------------------------------------------------- + public Touch() + { + _alwaysCreate = false; + _forceTouch = false; + } + + //----------------------------------------------------------------------------------- + // Property: force touch even if the file to be touched is read-only + //----------------------------------------------------------------------------------- + public bool ForceTouch + { + get { return _forceTouch; } + set { _forceTouch = value; } + } + + //----------------------------------------------------------------------------------- + // Property: create the file if it doesn't exist + //----------------------------------------------------------------------------------- + public bool AlwaysCreate + { + get { return _alwaysCreate; } + set { _alwaysCreate = value; } + } + + //----------------------------------------------------------------------------------- + // Property: specifies a specific time other than current + //----------------------------------------------------------------------------------- + public string Time + { + get { return _specificTime; } + set { _specificTime = value; } + } + + //----------------------------------------------------------------------------------- + // Property: file(s) to touch + //----------------------------------------------------------------------------------- + [Required] + public ITaskItem[] Files + { + get { return _files; } + set { _files = value; } + } + + //----------------------------------------------------------------------------------- + // Output of this task -- which files were touched + //----------------------------------------------------------------------------------- + [Output] + public ITaskItem[] TouchedFiles + { + get { return _touchedFiles; } + set { _touchedFiles = value; } + } + + /// + /// Implementation of the execute method. + /// + /// + internal bool ExecuteImpl + ( + FileExists fileExists, + FileCreate fileCreate, + GetAttributes fileGetAttributes, + SetAttributes fileSetAttributes, + SetLastAccessTime fileSetLastAccessTime, + SetLastWriteTime fileSetLastWriteTime + + ) + { + // See what time we are touching all files to + DateTime touchDateTime; + try + { + touchDateTime = GetTouchDateTime(); + } + catch (FormatException e) + { + Log.LogErrorWithCodeFromResources("Touch.TimeSyntaxIncorrect", e.Message); + return false; + } + + // Go through all files and touch 'em + bool retVal = true; + ArrayList touchedItems = new ArrayList(); + HashSet touchedFilesSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (ITaskItem file in Files) + { + // For speed, eliminate duplicates caused by poor targets authoring + if (touchedFilesSet.Contains(file.ItemSpec)) + { + continue; + } + + // Touch the file. If the file was touched successfully then add it to our array of + // touched items. + if + ( + TouchFile + ( + file.ItemSpec, + touchDateTime, + fileExists, + fileCreate, + fileGetAttributes, + fileSetAttributes, + fileSetLastAccessTime, + fileSetLastWriteTime + + ) + ) + { + touchedItems.Add(file); + } + else + { + retVal = false; + } + + // Add even on failure to avoid reattempting + touchedFilesSet.Add(file.ItemSpec); + } + + // Now, set the property that indicates which items we touched. Note that we + // touch all the items + TouchedFiles = (ITaskItem[])touchedItems.ToArray(typeof(ITaskItem)); + return retVal; + } + + /// + /// Run the task + /// + /// + public override bool Execute() + { + return ExecuteImpl + ( + new FileExists(System.IO.File.Exists), + new FileCreate(System.IO.File.Create), + new GetAttributes(System.IO.File.GetAttributes), + new SetAttributes(System.IO.File.SetAttributes), + new SetLastAccessTime(System.IO.File.SetLastAccessTime), + new SetLastWriteTime(System.IO.File.SetLastWriteTime) + ); + } + + /// + /// Helper method creates a file. + /// + /// + /// + /// "true" if the file was created. + private bool CreateFile + ( + string file, + FileCreate fileCreate + ) + { + try + { + using (System.IO.FileStream fs = fileCreate(file)) + { + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("Touch.CannotCreateFile", file, e.Message); + return false; + } + + return true; + } + + /// + /// Helper method touches a file. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// "True" if the file was touched. + private bool TouchFile + ( + string file, + DateTime dt, + FileExists fileExists, + FileCreate fileCreate, + GetAttributes fileGetAttributes, + SetAttributes fileSetAttributes, + SetLastAccessTime fileSetLastAccessTime, + SetLastWriteTime fileSetLastWriteTime + ) + { + if (!fileExists(file)) + { + // If the file does not exist then we check if we need to create it. + if (AlwaysCreate) + { + Log.LogMessageFromResources(MessageImportance.Normal, "Touch.CreatingFile", file, "AlwaysCreate"); + if (!CreateFile(file, fileCreate)) + { + return false; + } + } + else + { + Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file); + return false; + } + } + else + { + Log.LogMessageFromResources(MessageImportance.Normal, "Touch.Touching", file); + } + + // If the file is read only then we must either issue an error, or, if the user so + // specified, make the file temporarily not read only. + bool needToRestoreAttributes = false; + System.IO.FileAttributes faOriginal = fileGetAttributes(file); + if ((faOriginal & System.IO.FileAttributes.ReadOnly) == System.IO.FileAttributes.ReadOnly) + { + if (ForceTouch) + { + try + { + System.IO.FileAttributes faNew = (faOriginal & ~System.IO.FileAttributes.ReadOnly); + fileSetAttributes(file, faNew); + needToRestoreAttributes = true; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("Touch.CannotMakeFileWritable", file, e.Message); + return false; + } + } + } + + // Do the actual touch operation + bool retVal = true; + try + { + fileSetLastAccessTime(file, dt); + fileSetLastWriteTime(file, dt); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("Touch.CannotTouch", file, e.Message); + return false; + } + finally + { + if (needToRestoreAttributes) + { + // Attempt to restore the attributes. If we fail here, then there is + // not much we can do. + try + { + fileSetAttributes(file, faOriginal); + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + if (ExceptionHandling.NotExpectedException(e)) + throw; + Log.LogErrorWithCodeFromResources("Touch.CannotRestoreAttributes", file, e.Message); + retVal = false; + } + } + } + + return retVal; + } + + //----------------------------------------------------------------------------------- + // Helper methods + //----------------------------------------------------------------------------------- + private DateTime GetTouchDateTime() + { + // If we have a specified time to which files need to be built then attempt + // to parse it from the Time property. Otherwise, we get the current time. + if (Time == null || Time.Length == 0) + return DateTime.Now; + else + return DateTime.Parse(Time, DateTimeFormatInfo.InvariantInfo); + } + } +} diff --git a/src/XMakeTasks/UnitTests/Al_Tests.cs b/src/XMakeTasks/UnitTests/Al_Tests.cs new file mode 100644 index 00000000000..5c4e0d6d37d --- /dev/null +++ b/src/XMakeTasks/UnitTests/Al_Tests.cs @@ -0,0 +1,608 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: AlTests + * + * Test the AL task in various ways. + * + */ + [TestClass] + sealed public class AlTests + { + /// + /// Tests the AlgorithmId parameter + /// + [TestMethod] + public void AlgorithmId() + { + AL t = new AL(); + + Assert.IsNull(t.AlgorithmId, "Default value"); + t.AlgorithmId = "whatisthis"; + Assert.AreEqual("whatisthis", t.AlgorithmId, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/algid:whatisthis"); + } + + /// + /// Tests the BaseAddress parameter + /// + [TestMethod] + public void BaseAddress() + { + AL t = new AL(); + + Assert.IsNull(t.BaseAddress, "Default value"); + t.BaseAddress = "12345678"; + Assert.AreEqual("12345678", t.BaseAddress, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/baseaddress:12345678"); + } + + /// + /// Tests the CompanyName parameter + /// + [TestMethod] + public void CompanyName() + { + AL t = new AL(); + + Assert.IsNull(t.CompanyName, "Default value"); + t.CompanyName = "Google"; + Assert.AreEqual("Google", t.CompanyName, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/company:Google"); + } + + /// + /// Tests the Configuration parameter + /// + [TestMethod] + public void Configuration() + { + AL t = new AL(); + + Assert.IsNull(t.Configuration, "Default value"); + t.Configuration = "debug"; + Assert.AreEqual("debug", t.Configuration, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/configuration:debug"); + } + + /// + /// Tests the Copyright parameter + /// + [TestMethod] + public void Copyright() + { + AL t = new AL(); + + Assert.IsNull(t.Copyright, "Default value"); + t.Copyright = "(C) 2005"; + Assert.AreEqual("(C) 2005", t.Copyright, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/copyright:(C) 2005"); + } + + /// + /// Tests the Culture parameter + /// + [TestMethod] + public void Culture() + { + AL t = new AL(); + + Assert.IsNull(t.Culture, "Default value"); + t.Culture = "aussie"; + Assert.AreEqual("aussie", t.Culture, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/culture:aussie"); + } + + /// + /// Tests the DelaySign parameter. + /// + [TestMethod] + public void DelaySign() + { + AL t = new AL(); + + Assert.IsFalse(t.DelaySign, "Default value"); + t.DelaySign = true; + Assert.IsTrue(t.DelaySign, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, "/delaysign+"); + } + + /// + /// Tests the Description parameter + /// + [TestMethod] + public void Description() + { + AL t = new AL(); + + Assert.IsNull(t.Description, "Default value"); + t.Description = "whatever"; + Assert.AreEqual("whatever", t.Description, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/description:whatever"); + } + + /// + /// Tests the EmbedResources parameter with an item that has metadata LogicalName and Access=private + /// + [TestMethod] + public void EmbedResourcesWithPrivateAccess() + { + AL t = new AL(); + + Assert.IsNull(t.EmbedResources, "Default value"); + + // Construct the task item. + TaskItem i = new TaskItem(); + i.ItemSpec = "MyResource.bmp"; + i.SetMetadata("LogicalName", "Kenny"); + i.SetMetadata("Access", "Private"); + t.EmbedResources = new ITaskItem[] { i }; + + Assert.AreEqual(1, t.EmbedResources.Length, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, "/embed:MyResource.bmp,Kenny,Private"); + } + + /// + /// Tests the EvidenceFile parameter + /// + [TestMethod] + public void EvidenceFile() + { + AL t = new AL(); + + Assert.IsNull(t.EvidenceFile, "Default value"); + t.EvidenceFile = "MyEvidenceFile"; + Assert.AreEqual("MyEvidenceFile", t.EvidenceFile, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/evidence:MyEvidenceFile"); + } + + /// + /// Tests the FileVersion parameter + /// + [TestMethod] + public void FileVersion() + { + AL t = new AL(); + + Assert.IsNull(t.FileVersion, "Default value"); + t.FileVersion = "1.2.3.4"; + Assert.AreEqual("1.2.3.4", t.FileVersion, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/fileversion:1.2.3.4"); + } + + /// + /// Tests the Flags parameter + /// + [TestMethod] + public void Flags() + { + AL t = new AL(); + + Assert.IsNull(t.Flags, "Default value"); + t.Flags = "0x8421"; + Assert.AreEqual("0x8421", t.Flags, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/flags:0x8421"); + } + + /// + /// Tests the GenerateFullPaths parameter. + /// + [TestMethod] + public void GenerateFullPaths() + { + AL t = new AL(); + + Assert.IsFalse(t.GenerateFullPaths, "Default value"); + t.GenerateFullPaths = true; + Assert.IsTrue(t.GenerateFullPaths, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, "/fullpaths"); + } + + /// + /// Tests the KeyFile parameter + /// + [TestMethod] + public void KeyFile() + { + AL t = new AL(); + + Assert.IsNull(t.KeyFile, "Default value"); + t.KeyFile = "mykey.snk"; + Assert.AreEqual("mykey.snk", t.KeyFile, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/keyfile:mykey.snk"); + } + + /// + /// Tests the KeyContainer parameter + /// + [TestMethod] + public void KeyContainer() + { + AL t = new AL(); + + Assert.IsNull(t.KeyContainer, "Default value"); + t.KeyContainer = "MyKeyContainer"; + Assert.AreEqual("MyKeyContainer", t.KeyContainer, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/keyname:MyKeyContainer"); + } + + /// + /// Tests the LinkResources parameter with an item that has metadata LogicalName, Target, and Access=private + /// + [TestMethod] + public void LinkResourcesWithPrivateAccessAndTargetFile() + { + AL t = new AL(); + + Assert.IsNull(t.LinkResources, "Default value"); + + // Construct the task item. + TaskItem i = new TaskItem(); + i.ItemSpec = "MyResource.bmp"; + i.SetMetadata("LogicalName", "Kenny"); + i.SetMetadata("TargetFile", @"working\MyResource.bmp"); + i.SetMetadata("Access", "Private"); + t.LinkResources = new ITaskItem[] { i }; + + Assert.AreEqual(1, t.LinkResources.Length, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/link:MyResource.bmp,Kenny,working\MyResource.bmp,Private"); + } + + /// + /// Tests the LinkResources parameter with two items with differing metdata. + /// + [TestMethod] + public void LinkResourcesWithTwoItems() + { + AL t = new AL(); + + Assert.IsNull(t.LinkResources, "Default value"); + + // Construct the task item. + TaskItem i1 = new TaskItem(); + i1.ItemSpec = "MyResource.bmp"; + i1.SetMetadata("LogicalName", "Kenny"); + i1.SetMetadata("TargetFile", @"working\MyResource.bmp"); + i1.SetMetadata("Access", "Private"); + TaskItem i2 = new TaskItem(); + i2.ItemSpec = "MyResource2.bmp"; + i2.SetMetadata("LogicalName", "Chef"); + i2.SetMetadata("TargetFile", @"working\MyResource2.bmp"); + t.LinkResources = new ITaskItem[] { i1, i2 }; + + Assert.AreEqual(2, t.LinkResources.Length, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/link:MyResource.bmp,Kenny,working\MyResource.bmp,Private"); + CommandLine.ValidateHasParameter(t, @"/link:MyResource2.bmp,Chef,working\MyResource2.bmp"); + } + + /// + /// Tests the MainEntryPoint parameter + /// + [TestMethod] + public void MainEntryPoint() + { + AL t = new AL(); + + Assert.IsNull(t.MainEntryPoint, "Default value"); + t.MainEntryPoint = "Class1.Main"; + Assert.AreEqual("Class1.Main", t.MainEntryPoint, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/main:Class1.Main"); + } + + /// + /// Tests the OutputAssembly parameter + /// + [TestMethod] + public void OutputAssembly() + { + AL t = new AL(); + + Assert.IsNull(t.OutputAssembly, "Default value"); + t.OutputAssembly = new TaskItem("foo.dll"); + Assert.AreEqual("foo.dll", t.OutputAssembly.ItemSpec, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/out:foo.dll"); + } + + /// + /// Tests the Platform parameter + /// + [TestMethod] + public void Platform() + { + AL t = new AL(); + + Assert.IsNull(t.Platform, "Default value"); + t.Platform = "x86"; + Assert.AreEqual("x86", t.Platform, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + } + + // Tests the "Platform" and "Prefer32Bit" parameter combinations on the AL task, + // and confirms that it sets the /platform switch on the command-line correctly. + [TestMethod] + public void PlatformAndPrefer32Bit() + { + // Implicit "anycpu" + AL t = new AL(); + CommandLine.ValidateNoParameterStartsWith(t, @"/platform:"); + t = new AL(); + t.Prefer32Bit = false; + CommandLine.ValidateNoParameterStartsWith(t, @"/platform:"); + t = new AL(); + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu32bitpreferred"); + + // Explicit "anycpu" + t = new AL(); + t.Platform = "anycpu"; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu"); + t = new AL(); + t.Platform = "anycpu"; + t.Prefer32Bit = false; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu"); + t = new AL(); + t.Platform = "anycpu"; + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu32bitpreferred"); + + // Explicit "x86" + t = new AL(); + t.Platform = "x86"; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + t = new AL(); + t.Platform = "x86"; + t.Prefer32Bit = false; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + t = new AL(); + t.Platform = "x86"; + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + } + + /// + /// Tests the ProductName parameter + /// + [TestMethod] + public void ProductName() + { + AL t = new AL(); + + Assert.IsNull(t.ProductName, "Default value"); + t.ProductName = "VisualStudio"; + Assert.AreEqual("VisualStudio", t.ProductName, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/product:VisualStudio"); + } + + /// + /// Tests the ProductVersion parameter + /// + [TestMethod] + public void ProductVersion() + { + AL t = new AL(); + + Assert.IsNull(t.ProductVersion, "Default value"); + t.ProductVersion = "8.0"; + Assert.AreEqual("8.0", t.ProductVersion, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/productversion:8.0"); + } + + /// + /// Tests the ResponseFiles parameter + /// + [TestMethod] + public void ResponseFiles() + { + AL t = new AL(); + + Assert.IsNull(t.ResponseFiles, "Default value"); + t.ResponseFiles = new string[2] { "one.rsp", "two.rsp" }; + Assert.AreEqual(2, t.ResponseFiles.Length, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"@one.rsp"); + CommandLine.ValidateHasParameter(t, @"@two.rsp"); + } + + /// + /// Tests the SourceModules parameter + /// + [TestMethod] + public void SourceModules() + { + AL t = new AL(); + + Assert.IsNull(t.SourceModules, "Default value"); + + // Construct the task items. + TaskItem i1 = new TaskItem(); + i1.ItemSpec = "Strings.resources"; + i1.SetMetadata("TargetFile", @"working\MyResource.bmp"); + TaskItem i2 = new TaskItem(); + i2.ItemSpec = "Dialogs.resources"; + t.SourceModules = new ITaskItem[] { i1, i2 }; + + Assert.AreEqual(2, t.SourceModules.Length, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"Strings.resources,working\MyResource.bmp"); + CommandLine.ValidateHasParameter(t, @"Dialogs.resources"); + } + + /// + /// Tests the TargetType parameter + /// + [TestMethod] + public void TargetType() + { + AL t = new AL(); + + Assert.IsNull(t.TargetType, "Default value"); + t.TargetType = "winexe"; + Assert.AreEqual("winexe", t.TargetType, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/target:winexe"); + } + + /// + /// Tests the TemplateFile parameter + /// + [TestMethod] + public void TemplateFile() + { + AL t = new AL(); + + Assert.IsNull(t.TemplateFile, "Default value"); + t.TemplateFile = "mymainassembly.dll"; + Assert.AreEqual("mymainassembly.dll", t.TemplateFile, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/template:mymainassembly.dll"); + } + + /// + /// Tests the Title parameter + /// + [TestMethod] + public void Title() + { + AL t = new AL(); + + Assert.IsNull(t.Title, "Default value"); + t.Title = "WarAndPeace"; + Assert.AreEqual("WarAndPeace", t.Title, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/title:WarAndPeace"); + } + + /// + /// Tests the Trademark parameter + /// + [TestMethod] + public void Trademark() + { + AL t = new AL(); + + Assert.IsNull(t.Trademark, "Default value"); + t.Trademark = "MyTrademark"; + Assert.AreEqual("MyTrademark", t.Trademark, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/trademark:MyTrademark"); + } + + /// + /// Tests the Version parameter + /// + [TestMethod] + public void Version() + { + AL t = new AL(); + + Assert.IsNull(t.Version, "Default value"); + t.Version = "WowHowManyKindsOfVersionsAreThere"; + Assert.AreEqual("WowHowManyKindsOfVersionsAreThere", t.Version, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/version:WowHowManyKindsOfVersionsAreThere"); + } + + /// + /// Tests the Win32Icon parameter + /// + [TestMethod] + public void Win32Icon() + { + AL t = new AL(); + + Assert.IsNull(t.Win32Icon, "Default value"); + t.Win32Icon = "foo.ico"; + Assert.AreEqual("foo.ico", t.Win32Icon, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/win32icon:foo.ico"); + } + + /// + /// Tests the Win32Resource parameter + /// + [TestMethod] + public void Win32Resource() + { + AL t = new AL(); + + Assert.IsNull(t.Win32Resource, "Default value"); + t.Win32Resource = "foo.res"; + Assert.AreEqual("foo.res", t.Win32Resource, "New value"); + + // Check the parameters. + CommandLine.ValidateHasParameter(t, @"/win32res:foo.res"); + } + } +} + + + + + diff --git a/src/XMakeTasks/UnitTests/AppConfig_Tests.cs b/src/XMakeTasks/UnitTests/AppConfig_Tests.cs new file mode 100644 index 00000000000..aac2a0d83e5 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AppConfig_Tests.cs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Unit tests for the AppConfig class + /// + [TestClass] + public class AppConfig_Tests + { + /// + /// A simple app.config. + /// + [TestMethod] + public void Simple() + { + AppConfig app = new AppConfig(); + + string xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + app.Read(new XmlTextReader(xml, XmlNodeType.Document, null)); + + string s = Summarize(app); + + Assert.IsTrue(s.Contains("Dependent Assembly: Simple, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a oldVersionLow=1.0.0.0 oldVersionHigh=1.0.0.0 newVersion=2.0.0.0")); + } + + /// + /// A simple app.config. + /// + [TestMethod] + public void SimpleRange() + { + AppConfig app = new AppConfig(); + + string xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + app.Read(new XmlTextReader(xml, XmlNodeType.Document, null)); + + string s = Summarize(app); + + Assert.IsTrue(s.Contains("Dependent Assembly: Simple, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a oldVersionLow=1.0.0.0 oldVersionHigh=2.0.0.0 newVersion=2.0.0.0")); + } + + /// + /// An app.config taken from rascal, that has some bindingRedirects. + /// + [TestMethod] + public void RascalTest() + { + AppConfig app = new AppConfig(); + + string xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + app.Read(new XmlTextReader(xml, XmlNodeType.Document, null)); + + string s = Summarize(app); + + Assert.IsTrue(s.Contains("Dependent Assembly: Microsoft.VSDesigner, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a oldVersionLow=7.0.3300.0 oldVersionHigh=7.0.3300.0 newVersion=8.0.1000.0")); + Assert.IsTrue(s.Contains("Dependent Assembly: Microsoft.VisualStudio.Designer.Interfaces, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a oldVersionLow=1.0.3300.0 oldVersionHigh=1.0.3300.0 newVersion=1.2.3400.0")); + Assert.IsTrue(s.Contains("Dependent Assembly: Microsoft.VisualStudio, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a oldVersionLow=1.0.3300.0 oldVersionHigh=1.0.3300.0 newVersion=1.2.3400.0")); + } + + /// + /// A machine.config file. + /// + [TestMethod] + public void MachineConfig() + { + AppConfig app = new AppConfig(); + + string xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; + + app.Read(new XmlTextReader(xml, XmlNodeType.Document, null)); + + string s = Summarize(app); + + Assert.IsTrue(s.Contains("Dependent Assembly: Microsoft.VSDesigner, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a oldVersionLow=7.1.3300.0 oldVersionHigh=7.1.3300.0 newVersion=7.2.3300.0")); + } + + /// + /// Make sure that only dependent assemblies under the configuration-->runtime tag work. + /// + [TestMethod] + public void Regress339840_DependentAssemblyUnderAlienTag() + { + AppConfig app = new AppConfig(); + + string xml = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + app.Read(new XmlTextReader(xml, XmlNodeType.Document, null)); + + string s = Summarize(app); + + Assert.IsTrue(s.Contains("Dependent Assembly")); + } + + + + /// + /// Summarize the parsed contents of the app.config files. + /// + /// + private static string Summarize(AppConfig app) + { + StringBuilder b = new StringBuilder(); + + foreach (DependentAssembly dependentAssembly in app.Runtime.DependentAssemblies) + { + foreach (BindingRedirect bindingRedirect in dependentAssembly.BindingRedirects) + { + string message = String.Format("Dependent Assembly: {0} oldVersionLow={1} oldVersionHigh={2} newVersion={3}", dependentAssembly.PartialAssemblyName, bindingRedirect.OldVersionLow, bindingRedirect.OldVersionHigh, bindingRedirect.NewVersion); + b.AppendLine(message); + } + } + Console.WriteLine(b.ToString()); + return b.ToString(); + } + } +} diff --git a/src/XMakeTasks/UnitTests/AspNetCompiler_Tests.cs b/src/XMakeTasks/UnitTests/AspNetCompiler_Tests.cs new file mode 100644 index 00000000000..e4120ebe6cf --- /dev/null +++ b/src/XMakeTasks/UnitTests/AspNetCompiler_Tests.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Evaluation; +using System.Text.RegularExpressions; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: AspNetCompilerTests + * + * Test the AspNetCompiler task in various ways. + * + */ + [TestClass] + sealed public class AspNetCompilerTests + { + [TestMethod] + public void NoParameters() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + // It's invalid to have zero parameters, so we expect a "false" return value from ValidateParameters. + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void OnlyMetabasePath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + + // This should be valid. + Assert.IsTrue(CommandLine.CallValidateParameters(t)); + + CommandLine.ValidateEquals(t, @"-m /LM/W3SVC/1/Root/MyApp", false); + } + + [TestMethod] + public void OnlyVirtualPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.VirtualPath = @"/MyApp"; + + // This should be valid. + Assert.IsTrue(CommandLine.CallValidateParameters(t)); + + CommandLine.ValidateEquals(t, @"-v /MyApp", false); + } + + + [TestMethod] + public void OnlyPhysicalPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.PhysicalPath = @"c:\MyApp"; + + // This is not valid. Either MetabasePath or VirtualPath must be specified. + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void OnlyTargetPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.TargetPath = @"c:\MyTarget"; + + // This is not valid. Either MetabasePath or VirtualPath must be specified. + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void MetabasePathAndVirtualPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.VirtualPath = @"/MyApp"; + + // This is not valid. Can't specify both MetabasePath and (VirtualPath or PhysicalPath). + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void MetabasePathAndPhysicalPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.PhysicalPath = @"c:\MyApp"; + + // This is not valid. Can't specify both MetabasePath and (VirtualPath or PhysicalPath). + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void MetabasePathAndTargetPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is valid. + Assert.IsTrue(CommandLine.CallValidateParameters(t)); + + CommandLine.ValidateEquals(t, @"-m /LM/W3SVC/1/Root/MyApp c:\MyTarget", false); + } + + [TestMethod] + public void VirtualPathAndPhysicalPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.VirtualPath = @"/MyApp"; + t.PhysicalPath = @"c:\MyApp"; + + // This is valid. + Assert.IsTrue(CommandLine.CallValidateParameters(t)); + + CommandLine.ValidateEquals(t, @"-v /MyApp -p c:\MyApp", false); + } + + [TestMethod] + public void VirtualPathAndTargetPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.VirtualPath = @"/MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is valid. + Assert.IsTrue(CommandLine.CallValidateParameters(t)); + + CommandLine.ValidateEquals(t, @"-v /MyApp c:\MyTarget", false); + } + + [TestMethod] + public void PhysicalPathAndTargetPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.PhysicalPath = @"c:\MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is not valid. Either MetabasePath or VirtualPath must be specified. + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void AllExceptMetabasePath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.VirtualPath = @"/MyApp"; + t.PhysicalPath = @"c:\MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is valid. + Assert.IsTrue(CommandLine.CallValidateParameters(t)); + + CommandLine.ValidateEquals(t, @"-v /MyApp -p c:\MyApp c:\MyTarget", false); + } + + [TestMethod] + public void AllExceptVirtualPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.PhysicalPath = @"c:\MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is not valid. Can't specify both MetabasePath and (VirtualPath or PhysicalPath). + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void AllExceptPhysicalPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.VirtualPath = @"/MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is not valid. Can't specify both MetabasePath and (VirtualPath or PhysicalPath). + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void AllExceptTargetPath() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.VirtualPath = @"/MyApp"; + t.PhysicalPath = @"c:\MyApp"; + + // This is not valid. Can't specify both MetabasePath and (VirtualPath or PhysicalPath). + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + [TestMethod] + public void AllParameters() + { + AspNetCompiler t = new AspNetCompiler(); + t.BuildEngine = new MockEngine(); + + t.MetabasePath = @"/LM/W3SVC/1/Root/MyApp"; + t.VirtualPath = @"/MyApp"; + t.PhysicalPath = @"c:\MyApp"; + t.TargetPath = @"c:\MyTarget"; + + // This is not valid. Can't specify both MetabasePath and (VirtualPath or PhysicalPath). + Assert.IsFalse(CommandLine.CallValidateParameters(t)); + } + + /// + /// Make sure AspNetCompiler sends ExternalProjectStarted/Finished events properly. The tasks will fail since + /// the project files don't exist, but we only care about the events anyway. + /// + [TestMethod] + public void TestExternalProjectEvents() + { + string projectFileContents = @" + + + + + + + + + + + + + + + "; + + string fullProjectFile = string.Format(CultureInfo.InvariantCulture, projectFileContents, typeof(AspNetCompiler).Assembly.FullName); + + MockLogger logger = new MockLogger(); + Project proj = ObjectModelHelpers.CreateInMemoryProject(fullProjectFile, logger); + Assert.AreEqual(false, proj.Build(logger)); + + Assert.AreEqual(3, logger.ExternalProjectStartedEvents.Count); + Assert.AreEqual(3, logger.ExternalProjectFinishedEvents.Count); + + Assert.AreEqual(@"..\..\solutions\WebSite1\", logger.ExternalProjectStartedEvents[0].ProjectFile); + Assert.AreEqual("/WebSite2", logger.ExternalProjectStartedEvents[1].ProjectFile); + Assert.AreEqual("/LM/W3SVC/1/Root/MyApp", logger.ExternalProjectStartedEvents[2].ProjectFile); + + Assert.AreEqual(@"..\..\solutions\WebSite1\", logger.ExternalProjectFinishedEvents[0].ProjectFile); + Assert.AreEqual("/WebSite2", logger.ExternalProjectFinishedEvents[1].ProjectFile); + Assert.AreEqual("/LM/W3SVC/1/Root/MyApp", logger.ExternalProjectFinishedEvents[2].ProjectFile); + + Assert.AreEqual(null, logger.ExternalProjectStartedEvents[0].TargetNames); + Assert.AreEqual("Clean", logger.ExternalProjectStartedEvents[1].TargetNames); + Assert.AreEqual(null, logger.ExternalProjectStartedEvents[2].TargetNames); + + Assert.AreEqual(false, logger.ExternalProjectFinishedEvents[0].Succeeded); + Assert.AreEqual(false, logger.ExternalProjectFinishedEvents[1].Succeeded); + Assert.AreEqual(false, logger.ExternalProjectFinishedEvents[2].Succeeded); + } + } +} diff --git a/src/XMakeTasks/UnitTests/AssemblyIdentity_Tests.cs b/src/XMakeTasks/UnitTests/AssemblyIdentity_Tests.cs new file mode 100644 index 00000000000..db953b2be8f --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssemblyIdentity_Tests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// These functions are now guarded with a linkDemand and requires the caller to be signed with a +// ms pkt. The test harness does not appear to be signed. +#if never +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class AssemblyIdentity_Tests + { + + /// + /// Convert an AssemblyIdentity to a string. + /// + [TestMethod] + public void ConvertToString() + { + AssemblyIdentity a = new AssemblyIdentity("MyAssembly", "1.0.0.0"); + + Assert.AreEqual("MyAssembly, Version=1.0.0.0", a.ToString("D")); + Assert.AreEqual("MyAssembly_1.0.0.0", a.ToString("N")); + Assert.AreEqual("MyAssembly, Version=1.0.0.0, Culture=, PublicKeyToken=, ProcessorArchitecture=", a.ToString("P")); + } + + /// + /// Attempt to resolve an assembly. + /// + [TestMethod] + public void AttemptResolveButFailed() + { + AssemblyIdentity a = new AssemblyIdentity("MyAssembly", "1.0.0.0"); + + string path = a.Resolve(new string [] {Path.GetTempPath()}); + } + } +} +#endif + + diff --git a/src/XMakeTasks/UnitTests/AssemblyNameEx_Tests.cs b/src/XMakeTasks/UnitTests/AssemblyNameEx_Tests.cs new file mode 100644 index 00000000000..680eb00867a --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssemblyNameEx_Tests.cs @@ -0,0 +1,594 @@ +using System; +using System.IO; +using System.Reflection; +using System.Globalization; +using System.Resources; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using NUnit.Framework; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Collections.Generic; + +namespace Microsoft.Build.UnitTests +{ + [TestFixture] + sealed public class AssemblyNameEx_Tests + { + /// + /// Delegate defines a function that produces an AssemblyNameExtension from a string. + /// + /// + /// + internal delegate AssemblyNameExtension ProduceAssemblyNameEx(string name); + + static string[] assemblyStrings = + { + "System.Xml, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.XML, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, Version=2.0.0.0, Culture=neutral", + "System.XM, Version=2.0.0.0", + "System.XM, PublicKeyToken=b03f5f7f11d50a3a", + "System.XM, Culture=neutral", + "System.Xml", + "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", + "System.Drawing" + }; + + static string[] assembliesForPartialMatch = + { + "System.Xml, Version=10.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Culture=en, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=10.0.0.0, PublicKeyToken=b03f5f7f11d50a3a", + "System.Xml, Version=10.0.0.0, Culture=en" + }; + + /// + /// All the different ways the same assembly name can be represented. + /// + static ProduceAssemblyNameEx[] producers = + { + new ProduceAssemblyNameEx(ProduceAsString), + new ProduceAssemblyNameEx(ProduceAsAssemblyName), + new ProduceAssemblyNameEx(ProduceAsBoth), + new ProduceAssemblyNameEx(ProduceAsLowerString), + new ProduceAssemblyNameEx(ProduceAsLowerAssemblyName), + new ProduceAssemblyNameEx(ProduceAsLowerBoth) + }; + + + + private static AssemblyNameExtension ProduceAsString(string name) + { + return new AssemblyNameExtension(name); + } + + private static AssemblyNameExtension ProduceAsLowerString(string name) + { + return new AssemblyNameExtension(name.ToLower()); + } + + private static AssemblyNameExtension ProduceAsAssemblyName(string name) + { + return new AssemblyNameExtension(new AssemblyName(name)); + } + + private static AssemblyNameExtension ProduceAsLowerAssemblyName(string name) + { + return new AssemblyNameExtension(new AssemblyName(name.ToLower())); + } + + private static AssemblyNameExtension ProduceAsBoth(string name) + { + AssemblyNameExtension result = new AssemblyNameExtension(new AssemblyName(name)); + + // Force the string version to be produced too. + string backToString = result.FullName; + + return result; + } + + private static AssemblyNameExtension ProduceAsLowerBoth(string name) + { + return ProduceAsBoth(name.ToLower()); + } + + /// + /// General base name comparison validator. + /// + [Test] + public void CompareBaseName() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in producers) + { + foreach (ProduceAssemblyNameEx produce2 in producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + int result = a1.CompareBaseNameTo(a2); + int resultBaseline = String.Compare(baseName1.Name, baseName2.Name, StringComparison.OrdinalIgnoreCase); + if (resultBaseline != result) + { + Assertion.AssertEquals(resultBaseline, result); + } + } + } + } + } + } + + /// + /// General compareTo validator + /// + [Test] + public void CompareTo() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in producers) + { + foreach (ProduceAssemblyNameEx produce2 in producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + int result = a1.CompareTo(a2); + + if (a1.Equals(a2)) + { + Assertion.AssertEquals(0, result); + } + + if (a1.CompareBaseNameTo(a2) != 0) + { + Assertion.AssertEquals(a1.CompareBaseNameTo(a2), result); + } + + if + ( + a1.CompareBaseNameTo(a2) == 0 // Only check version if basenames match + && a1.Version != a2.Version + ) + { + if (a1.Version == null) + { + // Expect -1 if a1.Version is null and the baseNames match + Assertion.AssertEquals(-1, result); + } + else + { + Assertion.AssertEquals(a1.Version.CompareTo(a2.Version), result); + } + } + + int resultBaseline = String.Compare(a1.FullName, a2.FullName, StringComparison.OrdinalIgnoreCase); + // Only check to see if the result and the resultBaseline match when the result baseline is 0 and the result is not 0. + if (resultBaseline != result && resultBaseline == 0) + { + Assertion.AssertEquals(resultBaseline, result); + } + } + } + } + } + } + + [Test] + public void ExerciseMiscMethods() + { + AssemblyNameExtension a1 = producers[0](assemblyStrings[0]); + Assertion.AssertNotNull(a1.GetHashCode()); + + Version newVersion = new Version(1, 2); + a1.ReplaceVersion(newVersion); + Assertion.Assert(a1.Version.Equals(newVersion)); + + Assertion.AssertNotNull(a1.ToString()); + } + + [Test] + public void EscapeDisplayNameCharacters() + { + // /// Those characters are Equals(=), Comma(,), Quote("), Apostrophe('), Backslash(\). + string displayName = @"Hello,""Don't"" eat the \CAT"; + Assertion.Assert(String.Compare(AssemblyNameExtension.EscapeDisplayNameCharacters(displayName), @"Hello\,\""Don\'t\"" eat the \\CAT",StringComparison.OrdinalIgnoreCase) == 0); + } + + + /// + /// General equals comparison validator. + /// + [Test] + public void Equals() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in producers) + { + foreach (ProduceAssemblyNameEx produce2 in producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + // Baseline is a mismatch which is known to exercise + // the full code path. + AssemblyNameExtension a3 = ProduceAsAssemblyName(assemblyString1); + AssemblyNameExtension a4 = ProduceAsString(assemblyString2); + + bool result = a1.Equals(a2); + bool resultBaseline = a3.Equals(a4); + if (result != resultBaseline) + { + Assertion.AssertEquals(resultBaseline, result); + } + } + } + } + } + } + + + /// + /// General equals comparison validator when we are ignoring the version numbers in the name. + /// + [Test] + public void EqualsIgnoreVersion() + { + // For each pair of assembly strings... + foreach (string assemblyString1 in assemblyStrings) + { + AssemblyName baseName1 = new AssemblyName(assemblyString1); + + foreach (string assemblyString2 in assemblyStrings) + { + AssemblyName baseName2 = new AssemblyName(assemblyString2); + + // ...and for each pair of production methods... + foreach (ProduceAssemblyNameEx produce1 in producers) + { + foreach (ProduceAssemblyNameEx produce2 in producers) + { + AssemblyNameExtension a1 = produce1(assemblyString1); + AssemblyNameExtension a2 = produce2(assemblyString2); + + // Baseline is a mismatch which is known to exercise + // the full code path. + AssemblyNameExtension a3 = ProduceAsAssemblyName(assemblyString1); + AssemblyNameExtension a4 = ProduceAsString(assemblyString2); + + bool result = a1.EqualsIgnoreVersion(a2); + bool resultBaseline = a3.EqualsIgnoreVersion(a4); + if (result != resultBaseline) + { + Assertion.AssertEquals(resultBaseline, result); + } + } + } + } + } + } + + /// + /// This repros a bug that was found while coding AssemblyNameExtension. + /// + [Test] + public void CompareBaseNameRealCase1() + { + AssemblyNameExtension a1 = ProduceAsBoth("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension a2 = ProduceAsString("System.Drawing"); + + int result = a1.CompareBaseNameTo(a2); + + // Base names should be equal. + Assertion.AssertEquals(0, result); + } + + /// + /// Verify an exception is thrown when the simple name is not in the itemspec. + /// + /// + [Test] + [ExpectedException(typeof(FileLoadException))] + public void CreateAssemblyNameExtensionWithNoSimpleName() + { + AssemblyNameExtension extension = new AssemblyNameExtension("Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a", true); + } + + /// + /// Verify an exception is thrown when the simple name is not in the itemspec. + /// + /// + [Test] + [ExpectedException(typeof(FileLoadException))] + public void CreateAssemblyNameExtensionWithNoSimpleName2() + { + AssemblyNameExtension extension = new AssemblyNameExtension("Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension extension2 = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + extension2.PartialNameCompare(extension); + } + + /// + /// Create an assembly name extension providing the name, version, culture, and public key. Also test cases + /// where the public key is the only item specified + /// + [Test] + public void CreateAssemblyNameWithNameAndVersionCulturePublicKey() + { + AssemblyNameExtension extension = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A, Version=2.0.0.0, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(Object.ReferenceEquals(extension.CultureInfo, null)); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Object.ReferenceEquals(extension.Version, null)); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Object.ReferenceEquals(extension.Version, null)); + Assert.IsTrue(Object.ReferenceEquals(extension.CultureInfo, null)); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + + extension = new AssemblyNameExtension("A"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Object.ReferenceEquals(extension.Version, null)); + Assert.IsTrue(Object.ReferenceEquals(extension.CultureInfo, null)); + + } + + /// + /// Make sure processor architecture is seen when it is in the string. + /// + [Test] + public void CreateAssemblyNameWithNameAndProcessorArchitecture() + { + AssemblyNameExtension extension = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a, ProcessorArchitecture=MSIL"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + Assert.IsTrue(extension.FullName.Contains("MSIL")); + Assert.IsTrue(extension.HasProcessorArchitectureInFusionName); + + extension = new AssemblyNameExtension("A, Version=2.0.0.0, Culture=en, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.IsTrue(extension.Name.Equals("A", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extension.Version.Equals(new Version("2.0.0.0"))); + Assert.IsTrue(extension.CultureInfo.Equals(CultureInfo.GetCultureInfo("en"))); + Assert.IsTrue(extension.FullName.Contains("b03f5f7f11d50a3a")); + Assert.IsFalse(extension.HasProcessorArchitectureInFusionName); + } + + + /// + /// Verify partial matching on the simple name works + /// + [Test] + public void TestAssemblyPatialMatchSimpleName() + { + AssemblyNameExtension assemblyNameToMatch = new AssemblyNameExtension("System.Xml"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xmla"); + + foreach (string assembly in assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + Assert.IsTrue(assemblyNameToMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName)); + } + } + + /// + /// Verify partial matching on the simple name and version + /// + [Test] + public void TestAssemblyPatialMatchSimpleNameVersion() + { + AssemblyNameExtension assemblyNameToMatchVersion = new AssemblyNameExtension("System.Xml, Version=10.0.0.0"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, Version=5.0.0.0"); + AssemblyNameExtension assemblyMatchNoVersion = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + // If there is a version make sure the assembly name with the correct version matches + // Make sure the assembly with the wrong version does not match + if (assemblyToCompare.Version != null) + { + Assert.IsTrue(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + // Matches because version is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + } + else + { + + // If there is no version make names with a version specified do not match + Assert.IsFalse(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToMatchVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + + // Matches because version is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Version)); + } + } + } + + /// + /// Verify partial matching on the simple name and culture + /// + [Test] + public void TestAssemblyPatialMatchSimpleNameCulture() + { + AssemblyNameExtension assemblyNameToMatchCulture = new AssemblyNameExtension("System.Xml, Culture=en"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, Culture=de-DE"); + AssemblyNameExtension assemblyMatchNoVersion = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + // If there is a version make sure the assembly name with the correct culture matches + // Make sure the assembly with the wrong culture does not match + if (assemblyToCompare.CultureInfo != null) + { + Assert.IsTrue(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + // Matches because culture is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + } + else + { + // If there is no version make names with a culture specified do not match + Assert.IsFalse(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToMatchCulture.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + + // Matches because culture is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.Culture)); + } + } + } + + /// + /// Verify partial matching on the simple name and PublicKeyToken + /// + [Test] + public void TestAssemblyPatialMatchSimpleNamePublicKeyToken() + { + AssemblyNameExtension assemblyNameToMatchPublicToken = new AssemblyNameExtension("System.Xml, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension assemblyNameToNotMatch = new AssemblyNameExtension("System.Xml, PublicKeyToken=b03f5f7f11d50a3b"); + AssemblyNameExtension assemblyMatchNoVersion = new AssemblyNameExtension("System.Xml"); + + foreach (string assembly in assembliesForPartialMatch) + { + AssemblyNameExtension assemblyToCompare = new AssemblyNameExtension(assembly); + + // If there is a version make sure the assembly name with the correct publicKeyToken matches + // Make sure the assembly with the wrong publicKeyToken does not match + if (assemblyToCompare.GetPublicKeyToken() != null) + { + Assert.IsTrue(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + // Matches because publicKeyToken is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + } + else + { + // If there is no version make names with a publicKeyToken specified do not match + Assert.IsFalse(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToMatchPublicToken.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare)); + Assert.IsFalse(assemblyNameToNotMatch.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + + // Matches because publicKeyToken is not specified + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare)); + Assert.IsTrue(assemblyMatchNoVersion.PartialNameCompare(assemblyToCompare, PartialComparisonFlags.SimpleName | PartialComparisonFlags.PublicKeyToken)); + } + } + } + + /// + /// Make sure the reverse version comparer will compare the version in a way that would sort them in reverse order. + /// + [Test] + public void VerifyReverseVersionComparer() + { + AssemblyNameExtension x = new AssemblyNameExtension("System, Version=2.0.0.0"); + AssemblyNameExtension y = new AssemblyNameExtension("System, Version=1.0.0.0"); + AssemblyNameExtension z = new AssemblyNameExtension("System, Version=2.0.0.0"); + AssemblyNameExtension a = new AssemblyNameExtension("Zar, Version=3.0.0.0"); + + AssemblyNameReverseVersionComparer reverseComparer = new AssemblyNameReverseVersionComparer(); + Assert.AreEqual(-1, reverseComparer.Compare(x ,y)); + Assert.AreEqual(1, reverseComparer.Compare(y, x)); + Assert.AreEqual(0, reverseComparer.Compare(x, z)); + Assert.AreEqual(0, reverseComparer.Compare(null, null)); + Assert.AreEqual(-1, reverseComparer.Compare(x ,null)); + Assert.AreEqual(1, reverseComparer.Compare(null ,y)); + Assert.AreEqual(-1, reverseComparer.Compare(a, x)); + + List assemblies = new List(); + assemblies.Add(y); + assemblies.Add(x); + assemblies.Add(z); + + assemblies.Sort(AssemblyNameReverseVersionComparer.GenericComparer); + + Assert.IsTrue(assemblies[0].Equals(x)); + Assert.IsTrue(assemblies[1].Equals(z)); + Assert.IsTrue(assemblies[2].Equals(y)); + } + + } +} + + + + diff --git a/src/XMakeTasks/UnitTests/AssemblyRefs.cs b/src/XMakeTasks/UnitTests/AssemblyRefs.cs new file mode 100644 index 00000000000..dcb93918ba1 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssemblyRefs.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +internal static class FXAssembly +{ + internal const string Version = "4.0.0.0"; +} + +internal static class ThisAssembly +{ + internal const string Title = "Microsoft.Build.Tasks.Unittest.dll"; + internal const string Description = "Microsoft.Build.Tasks.Unittest.dll"; + internal const string DefaultAlias = "Microsoft.Build.Tasks.Unittest.dll"; + internal const string Copyright = "\u00A9 Microsoft Corporation. All rights reserved."; + internal const string Version = "4.0.0.0"; + internal const string InformationalVersion = "4.0.30319.0"; + internal const string DailyBuildNumberStr = "30319"; + internal const string BuildRevisionStr = "0"; + internal const int DailyBuildNumber = 30319; +} + +#pragma warning disable 436 +internal static class AssemblyRef +{ + internal const string EcmaPublicKey = "b77a5c561934e089"; + internal const string EcmaPublicKeyToken = "b77a5c561934e089"; + internal const string EcmaPublicKeyFull = "00000000000000000400000000000000"; + internal const string SilverlightPublicKey = "31bf3856ad364e35"; + internal const string SilverlightPublicKeyToken = "31bf3856ad364e35"; + internal const string SilverlightPublicKeyFull = "0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9"; + internal const string SilverlightPlatformPublicKey = "7cec85d7bea7798e"; + internal const string SilverlightPlatformPublicKeyToken = "7cec85d7bea7798e"; + internal const string SilverlightPlatformPublicKeyFull = "00240000048000009400000006020000002400005253413100040000010001008D56C76F9E8649383049F383C44BE0EC204181822A6C31CF5EB7EF486944D032188EA1D3920763712CCB12D75FB77E9811149E6148E5D32FBAAB37611C1878DDC19E20EF135D0CB2CFF2BFEC3D115810C3D9069638FE4BE215DBF795861920E5AB6F7DB2E2CEEF136AC23D5DD2BF031700AEC232F6C6B1C785B4305C123B37AB"; + internal const string PlatformPublicKey = EcmaPublicKey; + internal const string PlatformPublicKeyToken = EcmaPublicKeyToken; + internal const string PlatformPublicKeyFull = EcmaPublicKeyFull; + internal const string Mscorlib = "mscorlib, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + PlatformPublicKey; + internal const string SystemData = "System.Data, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string SystemDataOracleClient = "System.Data.OracleClient, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string System = "System, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + PlatformPublicKey; + internal const string SystemCore = "System.Core, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + PlatformPublicKey; + internal const string SystemNumerics = "System.Numerics, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + PlatformPublicKey; + internal const string SystemRuntimeRemoting = "System.Runtime.Remoting, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string SystemThreadingTasksDataflow = "System.Threading.Tasks.Dataflow, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + PlatformPublicKey; + internal const string SystemWindowsForms = "System.Windows.Forms, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string SystemXml = "System.Xml, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string MicrosoftPublicKey = "b03f5f7f11d50a3a"; + internal const string MicrosoftPublicKeyToken = "b03f5f7f11d50a3a"; + internal const string MicrosoftPublicKeyFull = "002400000480000094000000060200000024000052534131000400000100010007D1FA57C4AED9F0A32E84AA0FAEFD0DE9E8FD6AEC8F87FB03766C834C99921EB23BE79AD9D5DCC1DD9AD236132102900B723CF980957FC4E177108FC607774F29E8320E92EA05ECE4E821C0A5EFE8F1645C4C0C93C1AB99285D622CAA652C1DFAD63D745D6F2DE5F17E5EAF0FC4963D261C8A12436518206DC093344D5AD293"; + internal const string SharedLibPublicKey = "31bf3856ad364e35"; + internal const string SharedLibPublicKeyToken = "31bf3856ad364e35"; + internal const string SharedLibPublicKeyFull = "0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9"; + internal const string SystemComponentModelDataAnnotations = "System.ComponentModel.DataAnnotations, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemConfiguration = "System.Configuration, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemConfigurationInstall = "System.Configuration.Install, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemDeployment = "System.Deployment, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemDesign = "System.Design, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemDirectoryServices = "System.DirectoryServices, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemDrawingDesign = "System.Drawing.Design, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemDrawing = "System.Drawing, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemEnterpriseServices = "System.EnterpriseServices, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemManagement = "System.Management, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemMessaging = "System.Messaging, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemNetHttp = "System.Net.Http, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemNetHttpWebRequest = "System.Net.Http.WebRequest, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemRuntimeSerializationFormattersSoap = "System.Runtime.Serialization.Formatters.Soap, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemRuntimeWindowsRuntime = "System.Runtime.WindowsRuntime, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string SystemRuntimeWindowsRuntimeUIXaml = "System.Runtime.WindowsRuntimeUIXaml, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string SystemSecurity = "System.Security, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemServiceModelWeb = "System.ServiceModel.Web, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemServiceProcess = "System.ServiceProcess, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemWeb = "System.Web, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemWebAbstractions = "System.Web.Abstractions, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemWebDynamicData = "System.Web.DynamicData, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemWebDynamicDataDesign = "System.Web.DynamicData.Design, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemWebEntityDesign = "System.Web.Entity.Design, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + EcmaPublicKey; + internal const string SystemWebExtensions = "System.Web.Extensions, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemWebExtensionsDesign = "System.Web.Extensions.Design, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemWebMobile = "System.Web.Mobile, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemWebRegularExpressions = "System.Web.RegularExpressions, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string SystemWebRouting = "System.Web.Routing, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string SystemWebServices = "System.Web.Services, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string WindowsBase = "WindowsBase, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + SharedLibPublicKey; + internal const string MicrosoftVisualStudio = "Microsoft.VisualStudio, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string MicrosoftVisualStudioWindowsForms = "Microsoft.VisualStudio.Windows.Forms, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string VJSharpCodeProvider = "VJSharpCodeProvider, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string ASPBrowserCapsPublicKey = "b7bd7678b977bd8f"; + internal const string ASPBrowserCapsFactory = "ASP.BrowserCapsFactory, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + ASPBrowserCapsPublicKey; + internal const string MicrosoftVSDesigner = "Microsoft.VSDesigner, Version=10.0.0.0, Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string MicrosoftVisualStudioWeb = "Microsoft.VisualStudio.Web, Version=10.0.0.0, Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string MicrosoftWebDesign = "Microsoft.Web.Design.Client, Version=10.0.0.0, Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string MicrosoftVSDesignerMobile = "Microsoft.VSDesigner.Mobile, Version=8.0.0.0, Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; + internal const string MicrosoftJScript = "Microsoft.JScript, Version=8.0.0.0, Culture=neutral, PublicKeyToken=" + MicrosoftPublicKey; +} +#pragma warning restore 436 diff --git a/src/XMakeTasks/UnitTests/AssemblyRegistrationCache_Tests.cs b/src/XMakeTasks/UnitTests/AssemblyRegistrationCache_Tests.cs new file mode 100644 index 00000000000..c3a9bb60689 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssemblyRegistrationCache_Tests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class AssemblyRegistrationCache_Tests + { + [TestMethod] + public void ExerciseCache() + { + AssemblyRegistrationCache arc = new AssemblyRegistrationCache(); + + Assert.AreEqual(0, arc.Count); + + arc.AddEntry("foo", "bar"); + + Assert.AreEqual(1, arc.Count); + + string assembly; + string tlb; + arc.GetEntry(0, out assembly, out tlb); + + Assert.AreEqual("foo", assembly); + Assert.AreEqual("bar", tlb); + } + } +} diff --git a/src/XMakeTasks/UnitTests/AssignCulture_Tests.cs b/src/XMakeTasks/UnitTests/AssignCulture_Tests.cs new file mode 100644 index 00000000000..0ab3e035a05 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssignCulture_Tests.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class AssignCulture_Tests + { + /* + * Method: Basic + * + * Test the basic functionality. + */ + [TestMethod] + public void Basic() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.fr.resx"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.AreEqual("fr", t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource.fr.resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource.resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + /* + * Method: LooksLikeCultureButIsnt + * + * Not everything that looks like a culture, really is. + * Only a specific set of culture ids should be matched. + */ + [TestMethod] + public void LooksLikeCultureButIsnt() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.yy.resx"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.IsTrue(String.Empty == t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource.yy.resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource.yy.resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + /* + * Method: CultureAttributePrecedence + * + * Any pre-existing Culture attribute on the item is to be ignored + */ + [TestMethod] + public void CultureAttributePrecedence() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.fr.resx"); + i.SetMetadata("Culture", "en-GB"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.AreEqual("fr", t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource.fr.resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource.resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + /* + * Method: CultureAttributePrecedenceWithBogusCulture + * + * This is really a corner case. + * If the incoming item has a 'Culture' attribute already, but that culture is invalid, + * we still overwrite that culture. + */ + [TestMethod] + public void CultureAttributePrecedenceWithBogusCulture() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.fr.resx"); + i.SetMetadata("Culture", "invalid"); // Bogus culture. + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.AreEqual("fr", t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource.fr.resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource.resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + + + /* + * Method: AttributeForwarding + * + * Make sure that attributes set on input items are forwarded to ouput items. + * This applies to every attribute except for the one pointed to by CultureAttribute. + */ + [TestMethod] + public void AttributeForwarding() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.fr.resx"); + i.SetMetadata("MyAttribute", "My Random String"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.AreEqual("fr", t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("My Random String", t.AssignedFiles[0].GetMetadata("MyAttribute")); + Assert.AreEqual("MyResource.fr.resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource.resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + + /* + * Method: NoCulture + * + * Test the case where an item has no embedded culture. For example, + * "MyResource.resx" + */ + [TestMethod] + public void NoCulture() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.resx"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.IsTrue(String.Empty == t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource.resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource.resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + /* + * Method: NoExtension + * + * Test the case where an item has no extension. For example "MyResource". + */ + [TestMethod] + public void NoExtension() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.IsTrue(String.Empty == t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + /* + * Method: DoubleDot + * + * Test the case where an item has two dots embedded, but otherwise looks + * like a well-formed item. For example "MyResource..resx". + */ + [TestMethod] + public void DoubleDot() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource..resx"); + t.Files = new ITaskItem[] { i }; + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(1, t.CultureNeutralAssignedFiles.Length); + Assert.IsTrue(String.Empty == t.AssignedFiles[0].GetMetadata("Culture")); + Assert.AreEqual("MyResource..resx", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual("MyResource..resx", t.CultureNeutralAssignedFiles[0].ItemSpec); + } + + /// + /// If an item has a "DependentUpon" who's base name matches exactly, then just assume this + /// is a resource and form that happen to have an embedded culture. That is, don't assign a + /// culture to these. + /// + [TestMethod] + public void Regress283991() + { + AssignCulture t = new AssignCulture(); + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("MyResource.fr.resx"); + i.SetMetadata("DependentUpon", "MyResourcE.fr.vb"); + t.Files = new ITaskItem[] { i }; + + t.Execute(); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(0, t.AssignedFilesWithCulture.Length); + Assert.AreEqual(1, t.AssignedFilesWithNoCulture.Length); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/AssignLinkMetadata_Tests.cs b/src/XMakeTasks/UnitTests/AssignLinkMetadata_Tests.cs new file mode 100644 index 00000000000..3039d843f81 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssignLinkMetadata_Tests.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Evaluation; +using System.Collections.Generic; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class AssignLinkMetadata_Tests + { + /// + /// AssignLinkMetadata should behave nicely when no items are set to it + /// + [TestMethod] + public void NoItems() + { + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.OutputItems.Length); + } + + /// + /// AssignLinkMetadata should behave nicely when there is an item with an + /// itemspec that contains invalid path characters. + /// + [TestMethod] + public void InvalidItemPath() + { + ITaskItem item = GetParentedTaskItem(); + item.ItemSpec = "|||"; + + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + t.Items = new ITaskItem[] { new TaskItem(item) }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.OutputItems.Length); + } + + /// + /// Test basic function of the AssignLinkMetadata task + /// + [TestMethod] + public void Basic() + { + ITaskItem item = GetParentedTaskItem(); + + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + t.Items = new ITaskItem[] { new TaskItem(item) }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.OutputItems.Length); + Assert.AreEqual(item.ItemSpec, t.OutputItems[0].ItemSpec); + + // Link metadata should have been added by the task, and OriginalItemSpec was added by the copy + Assert.AreEqual(item.MetadataCount + 2, t.OutputItems[0].MetadataCount); + Assert.AreEqual(@"SubFolder\a.cs", t.OutputItems[0].GetMetadata("Link")); + } + + /// + /// AssignLinkMetadata should behave nicely when there is an item with an + /// itemspec that contains invalid path characters, and still successfully + /// output any items that aren't problematic. + /// + [TestMethod] + public void InvalidItemPathWithOtherValidItem() + { + ITaskItem item1 = GetParentedTaskItem(itemSpec: "|||"); + ITaskItem item2 = GetParentedTaskItem(); + + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + t.Items = new ITaskItem[] { new TaskItem(item1), new TaskItem(item2) }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.OutputItems.Length); + Assert.AreEqual(item2.ItemSpec, t.OutputItems[0].ItemSpec); + + // Link metadata should have been added by the task, and OriginalItemSpec was added by the copy + Assert.AreEqual(item2.MetadataCount + 2, t.OutputItems[0].MetadataCount); + Assert.AreEqual(@"SubFolder\a.cs", t.OutputItems[0].GetMetadata("Link")); + } + + /// + /// AssignLinkMetadata should not override if Link is already set + /// + [TestMethod] + public void DontOverrideLink() + { + ITaskItem item = GetParentedTaskItem(linkMetadata: @"SubFolder2\SubSubFolder\a.cs"); + + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + t.Items = new ITaskItem[] { new TaskItem(item) }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.OutputItems.Length); + } + + /// + /// AssignLinkMetadata should not set Link if the item is outside the + /// defining project's cone + /// + [TestMethod] + public void OutsideDefiningProjectCone() + { + ITaskItem item = GetParentedTaskItem(itemSpec: @"c:\subfolder\a.cs"); + + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + t.Items = new ITaskItem[] { new TaskItem(item) }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.OutputItems.Length); + } + + /// + /// AssignLinkMetadata should not set Link if the item does not know its + /// defining project + /// + [TestMethod] + public void NoDefiningProjectMetadata() + { + ITaskItem item = new TaskItem(@"SubFolder\a.cs"); + + AssignLinkMetadata t = new AssignLinkMetadata(); + t.BuildEngine = new MockEngine(); + t.Items = new ITaskItem[] { item }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.OutputItems.Length); + } + + /// + /// Helper function creating a task item that is associated with a parent project + /// + private ITaskItem GetParentedTaskItem(string linkMetadata = null) + { + return GetParentedTaskItem(Path.Combine(Path.GetTempPath(), "SubFolder", "a.cs"), linkMetadata); + } + + /// + /// Helper function creating a task item that is associated with a parent project + /// + private ITaskItem GetParentedTaskItem(string itemSpec, string linkMetadata = null) + { + Project p = new Project(new ProjectCollection()); + p.FullPath = Path.Combine(Path.GetTempPath(), "a.proj"); + ProjectInstance pi = p.CreateProjectInstance(); + + IDictionary metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (linkMetadata != null) + { + metadata.Add("Link", linkMetadata); + } + + ITaskItem item = pi.AddItem("Foo", itemSpec, metadata); + return item; + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/AssignProjectConfiguration_Tests.cs b/src/XMakeTasks/UnitTests/AssignProjectConfiguration_Tests.cs new file mode 100644 index 00000000000..49ad31036c3 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssignProjectConfiguration_Tests.cs @@ -0,0 +1,412 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Text; +using System.Xml; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Most of the common ResolveNonMSBuildOutput/AssignProjectConfiguration functionality is tested + /// in ResolveNonMSBuildProjectOutput_Tests. + /// Here, only test the AssignProjectConfiguration specific code + /// + [TestClass] + sealed public class AssignProjectConfiguration_Tests + { + private void TestResolveHelper(string itemSpec, string projectGuid, string package, string name, + Hashtable pregenConfigurations, bool expectedResult, + string expectedFullConfiguration, string expectedConfiguration, string expectedPlatform) + { + ITaskItem reference = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem(itemSpec, projectGuid, package, name); + // Use the XML string generation method from our sister class - XML element names will be different, + // but they are ignored anyway, and the rest is identical + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(pregenConfigurations); + ITaskItem resolvedProjectWithConfiguration; + + AssignProjectConfiguration rpc = new AssignProjectConfiguration(); + rpc.SolutionConfigurationContents = xmlString; + rpc.CacheProjectElementsFromXml(xmlString); + bool result = rpc.ResolveProject(reference, out resolvedProjectWithConfiguration); + + string message = string.Format("Reference \"{0}\" [project \"{1}\", package \"{2}\", name \"{3}\"] Pregen Xml string : \"{4}\"" + + "expected result \"{5}\", actual result \"{6}\", expected configuration \"{7}\", actual configuration \"{8}\".", + itemSpec, projectGuid, package, name, xmlString, expectedResult, result, expectedFullConfiguration, + (resolvedProjectWithConfiguration == null) ? string.Empty : resolvedProjectWithConfiguration.GetMetadata("FullConfiguration")); + + Assert.AreEqual(expectedResult, result, message); + if (result == true) + { + Assert.AreEqual(expectedFullConfiguration, resolvedProjectWithConfiguration.GetMetadata("FullConfiguration")); + Assert.AreEqual(expectedConfiguration, resolvedProjectWithConfiguration.GetMetadata("Configuration")); + Assert.AreEqual(expectedPlatform, resolvedProjectWithConfiguration.GetMetadata("Platform")); + Assert.AreEqual("Configuration=" + expectedConfiguration, resolvedProjectWithConfiguration.GetMetadata("SetConfiguration")); + Assert.AreEqual("Platform=" + expectedPlatform, resolvedProjectWithConfiguration.GetMetadata("SetPlatform")); + Assert.AreEqual(reference.ItemSpec, resolvedProjectWithConfiguration.ItemSpec); + } + else + { + Assert.AreEqual(null, resolvedProjectWithConfiguration); + } + } + + [TestMethod] + public void TestResolve() + { + // empty pre-generated string + Hashtable projectOutputs = new Hashtable(); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, false, null, null, null); + + // non matching project in string + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"Debug|Win32"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, false, null, null, null); + + // matching projects in string + projectOutputs = new Hashtable(); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"Debug|Win32"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-666666666666}", @"Debug"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, true, @"Debug|Win32", "Debug", "Win32"); + TestResolveHelper("MCDep2.vcproj", "{2F6BBCC3-7111-4116-A68B-666666666666}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, true, @"Debug", "Debug", string.Empty); + + // multiple non matching projects in string + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"Config1|Win32"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"Config2|AnyCPU"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"Config3|AnyCPU"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, false, null, null, null); + + // multiple non matching projects in string, two matching + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"Config1|Win32"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"Config2|AnyCPU"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"Config3|AnyCPU"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"CorrectProjectConfig|Platform"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-666666666666}", @"JustConfig"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, true, @"CorrectProjectConfig|Platform", "CorrectProjectConfig", "Platform"); + TestResolveHelper("MCDep2.vcproj", "{2F6BBCC3-7111-4116-A68B-666666666666}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, true, @"JustConfig", "JustConfig", string.Empty); + } + + + /// + /// Test the case where the project reference does not have either of the metadata set on it. + /// + /// We would expect the following case: + /// + /// 1) The xml element does not have the BuildProjectInSolution attribute set + /// Expect none of the metadata to be set + /// + [TestMethod] + public void TestReferenceWithNoMetadataBadBuildInProjectAttribute() + { + // Test the case where the metadata is missing and we are not supposed to build the reference + ITaskItem referenceItem = new TaskItem("TestItem"); + XmlDocument doc = new XmlDocument(); + XmlElement element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "IAmReallyABadOne"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Length == 0); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Length == 0); + } + + /// + /// Test the case where the project reference does not have either of the metadata set on it. + /// + /// We would expect the following case: + /// + /// 1) The xml element does not have the BuildProjectInSolution attribute set + /// Expect none of the metadata to be set + /// + [TestMethod] + public void TestReferenceWithNoMetadataNoBuildInProjectAttribute() + { + // Test the case where the metadata is missing and we are not supposed to build the reference + ITaskItem referenceItem = new TaskItem("TestItem"); + XmlDocument doc = new XmlDocument(); + XmlElement element = doc.CreateElement("TestElement"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Length == 0); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Length == 0); + } + + /// + /// Test the case where the project reference does not have either of the metadata set on it. + /// + /// We would expect the following case: + /// 1) The xml element has BuildProjectInSolution set to true + /// Expect none of the metadata to be set + /// + [TestMethod] + public void TestReferenceWithNoMetadataBuildInProjectAttributeTrue() + { + // Test the case where the metadata is missing and we are not supposed to build the reference + ITaskItem referenceItem = new TaskItem("TestItem"); + XmlDocument doc = new XmlDocument(); + XmlElement element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "true"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Length == 0); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Length == 0); + } + + + /// + /// Test the case where the project reference does not have either of the metadata set on it. + /// + /// We would expect the following case: + /// ReferenceAndBuildProjectsDisabledInProjectConfiguration is set to true meaning we want to build disabled projects. + /// + /// 1) The xml element has BuildProjectInSolution set to false + /// Expect no pieces of metadata to be set on the reference item + /// + [TestMethod] + public void TestReferenceWithNoMetadataBuildInProjectAttributeFalseReferenceAndBuildProjectsDisabledInProjectConfiguration() + { + // Test the case where the metadata is missing and we are not supposed to build the reference + ITaskItem referenceItem = new TaskItem("TestItem"); + XmlDocument doc = new XmlDocument(); + XmlElement element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "false"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(false, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Length == 0); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Length == 0); + } + + /// + /// Test the case where the project reference does not have either of the metadata set on it. + /// + /// We would expect the following case: + /// 1) The xml element has BuildProjectInSolution set to false + /// Expect two pieces of metadata to be put on the item and be set to false (BuildReference, and ReferenceOutputAssembly) + /// + [TestMethod] + public void TestReferenceWithNoMetadataBuildInProjectAttributeFalse() + { + // Test the case where the metadata is missing and we are not supposed to build the reference + ITaskItem referenceItem = new TaskItem("TestItem"); + XmlDocument doc = new XmlDocument(); + XmlElement element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "false"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Equals("false", StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// Test the case where the project reference does has one or more of the metadata set on it. + /// + /// We would expect the following case: + /// 1) The xml element has BuildProjectInSolution set to false + /// Expect two pieces of metadata to be put on the item and be set to true since they were already set (BuildReference, and ReferenceOutputAssembly) + /// + [TestMethod] + public void TestReferenceWithMetadataAlreadySetBuildInProjectAttributeFalse() + { + // Test the case where the metadata is missing and we are not supposed to build the reference + ITaskItem referenceItem = new TaskItem("TestItem"); + referenceItem.SetMetadata("BuildReference", "true"); + referenceItem.SetMetadata("ReferenceOutputAssembly", "true"); + + XmlDocument doc = new XmlDocument(); + XmlElement element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "false"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Equals("true", StringComparison.OrdinalIgnoreCase)); + + // Test the case where only ReferenceOutputAssembly is not set + referenceItem = new TaskItem("TestItem"); + referenceItem.SetMetadata("BuildReference", "true"); + doc = new XmlDocument(); + element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "false"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Equals("false", StringComparison.OrdinalIgnoreCase)); + + // Test the case where only BuildReference is not set + referenceItem = new TaskItem("TestItem"); + referenceItem.SetMetadata("ReferenceOutputAssembly", "true"); + doc = new XmlDocument(); + element = doc.CreateElement("TestElement"); + element.SetAttribute("BuildProjectInSolution", "false"); + AssignProjectConfiguration.SetBuildInProjectAndReferenceOutputAssemblyMetadata(true, referenceItem, element); + Assert.IsTrue(referenceItem.GetMetadata("BuildReference").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(referenceItem.GetMetadata("ReferenceOutputAssembly").Equals("true", StringComparison.OrdinalIgnoreCase)); + } + + + private void TestUnresolvedReferencesHelper(ArrayList projectRefs, Hashtable pregenConfigurations, + out Hashtable unresolvedProjects, out Hashtable resolvedProjects) + { + // Use the XML string generation method from our sister class - XML element names will be different, + // but they are ignored anyway, and the rest is identical + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(pregenConfigurations); + + MockEngine engine = new MockEngine(); + AssignProjectConfiguration rpc = new AssignProjectConfiguration(); + rpc.BuildEngine = engine; + rpc.SolutionConfigurationContents = xmlString; + rpc.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + + bool result = rpc.Execute(); + unresolvedProjects = new Hashtable(); + + for (int i = 0; i < rpc.UnassignedProjects.Length; i++) + { + unresolvedProjects[rpc.UnassignedProjects[i].ItemSpec] = rpc.UnassignedProjects[i]; + } + + resolvedProjects = new Hashtable(); + for (int i = 0; i < rpc.AssignedProjects.Length; i++) + { + resolvedProjects[rpc.AssignedProjects[i].GetMetadata("FullConfiguration")] = rpc.AssignedProjects[i]; + } + } + + /// + /// Verifies that the UnresolvedProjectReferences output parameter is populated correctly. + /// + [TestMethod] + public void TestUnresolvedReferences() + { + Hashtable unresolvedProjects = null; + Hashtable resolvedProjects = null; + Hashtable projectConfigurations = null; + ArrayList projectRefs = null; + + projectRefs = new ArrayList(); + projectRefs.Add(ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-000000000000}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1")); + projectRefs.Add(ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem("MCDep2.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep2")); + + // 1. multiple projects, none resolvable + projectConfigurations = new Hashtable(); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111111}", @"Config1|Win32"); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111112}", @"Config2|AnyCPU"); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111113}", @"Config3|AnyCPU"); + + TestUnresolvedReferencesHelper(projectRefs, projectConfigurations, out unresolvedProjects, out resolvedProjects); + + Assert.IsTrue(resolvedProjects.Count == 0, "No resolved refs expected for case 1"); + Assert.IsTrue(unresolvedProjects.Count == 2, "Two unresolved refs expected for case 1"); + Assert.IsTrue(unresolvedProjects["MCDep1.vcproj"] == projectRefs[0]); + Assert.IsTrue(unresolvedProjects["MCDep2.vcproj"] == projectRefs[1]); + + // 2. multiple projects, one resolvable + projectConfigurations = new Hashtable(); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111111}", @"Config1|Win32"); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111112}", @"Config2|AnyCPU"); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111113}", @"Config3|AnyCPU"); + projectConfigurations.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"CorrectProjectConfig|Platform"); + + TestUnresolvedReferencesHelper(projectRefs, projectConfigurations, out unresolvedProjects, out resolvedProjects); + + Assert.IsTrue(resolvedProjects.Count == 1, "One resolved ref expected for case 2"); + Assert.IsTrue(resolvedProjects.ContainsKey(@"CorrectProjectConfig|Platform")); + Assert.IsTrue(unresolvedProjects.Count == 1, "One unresolved ref expected for case 2"); + Assert.IsTrue(unresolvedProjects["MCDep1.vcproj"] == projectRefs[0]); + + // 3. multiple projects, all resolvable + projectConfigurations = new Hashtable(); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111111}", @"Config1|Win32"); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111112}", @"Config2|AnyCPU"); + projectConfigurations.Add("{11111111-1111-1111-1111-111111111113}", @"Config3|AnyCPU"); + projectConfigurations.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"CorrectProjectConfig|Platform"); + projectConfigurations.Add("{2F6BBCC3-7111-4116-A68B-000000000000}", @"CorrectProjectConfig2|Platform"); + + TestUnresolvedReferencesHelper(projectRefs, projectConfigurations, out unresolvedProjects, out resolvedProjects); + + Assert.IsTrue(resolvedProjects.Count == 2, "Two resolved refs expected for case 3"); + Assert.IsTrue(resolvedProjects.ContainsKey(@"CorrectProjectConfig|Platform")); + Assert.IsTrue(resolvedProjects.ContainsKey(@"CorrectProjectConfig2|Platform")); + Assert.IsTrue(unresolvedProjects.Count == 0, "No unresolved refs expected for case 3"); + } + + #region Test Defaults + /// + /// Verify if no values are passed in for certain properties that their default values are used. + /// + [TestMethod] + public void VerifyDefaultValueDefaultToVcxPlatformMappings() + { + string expectedDefaultToVcxPlatformMapping = "AnyCPU=Win32;X86=Win32;X64=X64;Itanium=Itanium"; + + AssignProjectConfiguration assignProjectConfiguration = new AssignProjectConfiguration(); + + /// Test defaults with nothign set + string actualDefaultToVcxPlatformMapping = assignProjectConfiguration.DefaultToVcxPlatformMapping; + Assert.IsTrue(expectedDefaultToVcxPlatformMapping.Equals(actualDefaultToVcxPlatformMapping, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedDefaultToVcxPlatformMapping, actualDefaultToVcxPlatformMapping)); + + assignProjectConfiguration.DefaultToVcxPlatformMapping = String.Empty; + actualDefaultToVcxPlatformMapping = assignProjectConfiguration.DefaultToVcxPlatformMapping; + Assert.IsTrue(expectedDefaultToVcxPlatformMapping.Equals(actualDefaultToVcxPlatformMapping, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedDefaultToVcxPlatformMapping, actualDefaultToVcxPlatformMapping)); + + assignProjectConfiguration.DefaultToVcxPlatformMapping = null; + actualDefaultToVcxPlatformMapping = assignProjectConfiguration.DefaultToVcxPlatformMapping; + Assert.IsTrue(expectedDefaultToVcxPlatformMapping.Equals(actualDefaultToVcxPlatformMapping, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedDefaultToVcxPlatformMapping, actualDefaultToVcxPlatformMapping)); + } + + /// + /// Verify if no values are passed in for certain properties that their default values are used. + /// + [TestMethod] + public void VerifyDefaultValuesVcxToDefaultPlatformMappingNoOutput() + { + string expectedVcxToDefaultPlatformMappingNoOutput = "Win32=X86;X64=X64;Itanium=Itanium"; + AssignProjectConfiguration assignProjectConfiguration = new AssignProjectConfiguration(); + + // Test the case for VcxToDefaultPlatformMapping when the outputType is not library + string actualVcxToDefaultPlatformMappingNoOutput = assignProjectConfiguration.VcxToDefaultPlatformMapping; + Assert.IsTrue(expectedVcxToDefaultPlatformMappingNoOutput.Equals(actualVcxToDefaultPlatformMappingNoOutput, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedVcxToDefaultPlatformMappingNoOutput, actualVcxToDefaultPlatformMappingNoOutput)); + + assignProjectConfiguration.VcxToDefaultPlatformMapping = String.Empty; + actualVcxToDefaultPlatformMappingNoOutput = assignProjectConfiguration.VcxToDefaultPlatformMapping; + Assert.IsTrue(expectedVcxToDefaultPlatformMappingNoOutput.Equals(actualVcxToDefaultPlatformMappingNoOutput, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedVcxToDefaultPlatformMappingNoOutput, actualVcxToDefaultPlatformMappingNoOutput)); + + assignProjectConfiguration.VcxToDefaultPlatformMapping = null; + actualVcxToDefaultPlatformMappingNoOutput = assignProjectConfiguration.VcxToDefaultPlatformMapping; + Assert.IsTrue(expectedVcxToDefaultPlatformMappingNoOutput.Equals(actualVcxToDefaultPlatformMappingNoOutput, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedVcxToDefaultPlatformMappingNoOutput, actualVcxToDefaultPlatformMappingNoOutput)); + } + + /// + /// Verify if no values are passed in for certain properties that their default values are used. + /// + [TestMethod] + public void VerifyDefaultValuesVcxToDefaultPlatformMappingLibraryOutput() + { + string expectedVcxToDefaultPlatformMappingLibraryOutput = "Win32=AnyCPU;X64=X64;Itanium=Itanium"; + AssignProjectConfiguration assignProjectConfiguration = new AssignProjectConfiguration(); + + // Test the case for VcxToDefaultPlatformMapping when the outputType is library + assignProjectConfiguration.OutputType = "Library"; + string actualVcxToDefaultPlatformMappingNoOutput = assignProjectConfiguration.VcxToDefaultPlatformMapping; + Assert.IsTrue(expectedVcxToDefaultPlatformMappingLibraryOutput.Equals(actualVcxToDefaultPlatformMappingNoOutput, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedVcxToDefaultPlatformMappingLibraryOutput, actualVcxToDefaultPlatformMappingNoOutput)); + + assignProjectConfiguration.VcxToDefaultPlatformMapping = String.Empty; + actualVcxToDefaultPlatformMappingNoOutput = assignProjectConfiguration.VcxToDefaultPlatformMapping; + Assert.IsTrue(expectedVcxToDefaultPlatformMappingLibraryOutput.Equals(actualVcxToDefaultPlatformMappingNoOutput, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedVcxToDefaultPlatformMappingLibraryOutput, actualVcxToDefaultPlatformMappingNoOutput)); + + assignProjectConfiguration.VcxToDefaultPlatformMapping = null; + actualVcxToDefaultPlatformMappingNoOutput = assignProjectConfiguration.VcxToDefaultPlatformMapping; + Assert.IsTrue(expectedVcxToDefaultPlatformMappingLibraryOutput.Equals(actualVcxToDefaultPlatformMappingNoOutput, StringComparison.OrdinalIgnoreCase), String.Format("Expected '{0}' but found '{1}'", expectedVcxToDefaultPlatformMappingLibraryOutput, actualVcxToDefaultPlatformMappingNoOutput)); + } + #endregion + } +} diff --git a/src/XMakeTasks/UnitTests/AssignTargetPath_Tests.cs b/src/XMakeTasks/UnitTests/AssignTargetPath_Tests.cs new file mode 100644 index 00000000000..301114d5225 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AssignTargetPath_Tests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class AssignTargetPath_Tests + { + [TestMethod] + public void Regress314791() + { + AssignTargetPath t = new AssignTargetPath(); + t.BuildEngine = new MockEngine(); + t.Files = new ITaskItem[] { new TaskItem(@"c:\bin2\abc.efg") }; + t.RootFolder = @"c:\bin"; + + bool success = t.Execute(); + + Assert.IsTrue(success); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(@"c:\bin2\abc.efg", t.AssignedFiles[0].ItemSpec); + Assert.AreEqual(@"abc.efg", t.AssignedFiles[0].GetMetadata("TargetPath")); + } + + [TestMethod] + public void AtConeRoot() + { + AssignTargetPath t = new AssignTargetPath(); + t.BuildEngine = new MockEngine(); + t.Files = new ITaskItem[] { new TaskItem(@"c:\f1\f2\file.txt") }; + t.RootFolder = @"c:\f1\f2"; + + bool success = t.Execute(); + + Assert.IsTrue(success); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(@"file.txt", t.AssignedFiles[0].GetMetadata("TargetPath")); + } + + [TestMethod] + public void OutOfCone() + { + AssignTargetPath t = new AssignTargetPath(); + t.BuildEngine = new MockEngine(); + t.Files = new ITaskItem[] { new TaskItem(@"d:\f1\f2\f3\f4\file.txt") }; + t.RootFolder = @"c:\f1"; + + bool success = t.Execute(); + + Assert.IsTrue(success); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual("file.txt", t.AssignedFiles[0].GetMetadata("TargetPath")); + } + + [TestMethod] + public void InConeButAbsolute() + { + AssignTargetPath t = new AssignTargetPath(); + t.BuildEngine = new MockEngine(); + t.Files = new ITaskItem[] { new TaskItem(@"c:\f1\f2\f3\f4\file.txt") }; + t.RootFolder = @"c:\f1\f2"; + + bool success = t.Execute(); + + Assert.IsTrue(success); + + Assert.AreEqual(1, t.AssignedFiles.Length); + Assert.AreEqual(@"f3\f4\file.txt", t.AssignedFiles[0].GetMetadata("TargetPath")); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/AxImp_Tests.cs b/src/XMakeTasks/UnitTests/AxImp_Tests.cs new file mode 100644 index 00000000000..d09bd549ed4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/AxImp_Tests.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +namespace Microsoft.Build.UnitTests.AxTlbImp_Tests +{ + [TestClass] + sealed public class AxImp_Tests + { + /// + /// Tests that the assembly being imported is passed to the command line + /// + [TestMethod] + public void ActiveXControlName() + { + var t = new ResolveComReference.AxImp(); + string testParameterValue = "AxInterop.Foo.dll"; + + Assert.IsNull(t.ActiveXControlName, "ActiveXControlName should be null by default"); + + t.ActiveXControlName = testParameterValue; + Assert.AreEqual(testParameterValue, t.ActiveXControlName, "New ActiveXControlName value should be set"); + CommandLine.ValidateHasParameter(t, testParameterValue, false /* no response file */); + } + + /// + /// Tests that the assembly being imported is passed to the command line + /// + [TestMethod] + public void ActiveXControlNameWithSpaces() + { + var t = new ResolveComReference.AxImp(); + string testParameterValue = @"c:\Program Files\AxInterop.Foo.dll"; + + Assert.IsNull(t.ActiveXControlName, "ActiveXControlName should be null by default"); + + t.ActiveXControlName = testParameterValue; + Assert.AreEqual(testParameterValue, t.ActiveXControlName, "New ActiveXControlName value should be set"); + CommandLine.ValidateHasParameter(t, testParameterValue, false /* no response file */); + } + + /// + /// Tests the /source switch + /// + [TestMethod] + public void GenerateSource() + { + var t = new ResolveComReference.AxImp(); + + Assert.IsFalse(t.GenerateSource, "GenerateSource should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/source", false /* no response file */); + + t.GenerateSource = true; + Assert.IsTrue(t.GenerateSource, "GenerateSource should be true"); + CommandLine.ValidateHasParameter(t, @"/source", false /* no response file */); + } + + /// + /// Tests the /nologo switch + /// + [TestMethod] + public void NoLogo() + { + var t = new ResolveComReference.AxImp(); + + Assert.IsFalse(t.NoLogo, "NoLogo should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/nologo", false /* no response file */); + + t.NoLogo = true; + Assert.IsTrue(t.NoLogo, "NoLogo should be true"); + CommandLine.ValidateHasParameter(t, @"/nologo", false /* no response file */); + } + + /// + /// Tests the /out: switch + /// + [TestMethod] + public void OutputAssembly() + { + var t = new ResolveComReference.AxImp(); + string testParameterValue = "AxInterop.Foo.dll"; + + Assert.IsNull(t.OutputAssembly, "OutputAssembly should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/out:", false /* no response file */); + + t.OutputAssembly = testParameterValue; + Assert.AreEqual(testParameterValue, t.OutputAssembly, "New OutputAssembly value should be set"); + CommandLine.ValidateHasParameter(t, @"/out:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /out: switch, with a space in the output file + /// + [TestMethod] + public void OutputAssemblyWithSpaces() + { + var t = new ResolveComReference.AxImp(); + string testParameterValue = @"c:\Program Files\AxInterop.Foo.dll"; + + Assert.IsNull(t.OutputAssembly, "OutputAssembly should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/out:", false /* no response file */); + + t.OutputAssembly = testParameterValue; + Assert.AreEqual(testParameterValue, t.OutputAssembly, "New OutputAssembly value should be set"); + CommandLine.ValidateHasParameter(t, @"/out:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /rcw: switch + /// + [TestMethod] + public void RuntimeCallableWrapper() + { + var t = new ResolveComReference.AxImp(); + string testParameterValue = "Interop.Foo.dll"; + + Assert.IsNull(t.RuntimeCallableWrapperAssembly, "RuntimeCallableWrapper should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/rcw:", false /* no response file */); + + t.RuntimeCallableWrapperAssembly = testParameterValue; + Assert.AreEqual(testParameterValue, t.RuntimeCallableWrapperAssembly, "New RuntimeCallableWrapper value should be set"); + CommandLine.ValidateHasParameter(t, @"/rcw:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /rcw: switch with a space in the filename + /// + [TestMethod] + public void RuntimeCallableWrapperWithSpaces() + { + var t = new ResolveComReference.AxImp(); + string testParameterValue = @"C:\Program Files\Microsoft Visual Studio 10.0\Interop.Foo.dll"; + + Assert.IsNull(t.RuntimeCallableWrapperAssembly, "RuntimeCallableWrapper should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/rcw:", false /* no response file */); + + t.RuntimeCallableWrapperAssembly = testParameterValue; + Assert.AreEqual(testParameterValue, t.RuntimeCallableWrapperAssembly, "New RuntimeCallableWrapper value should be set"); + CommandLine.ValidateHasParameter(t, @"/rcw:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /silent switch + /// + [TestMethod] + public void Silent() + { + var t = new ResolveComReference.AxImp(); + + Assert.IsFalse(t.Silent, "Silent should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/silent", false /* no response file */); + + t.Silent = true; + Assert.IsTrue(t.Silent, "Silent should be true"); + CommandLine.ValidateHasParameter(t, @"/silent", false /* no response file */); + } + + /// + /// Tests the /verbose switch + /// + [TestMethod] + public void Verbose() + { + var t = new ResolveComReference.AxImp(); + + Assert.IsFalse(t.Verbose, "Verbose should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/verbose", false /* no response file */); + + t.Verbose = true; + Assert.IsTrue(t.Verbose, "Verbose should be true"); + CommandLine.ValidateHasParameter(t, @"/verbose", false /* no response file */); + } + + /// + /// Tests that task does the right thing (fails) when no .ocx file is passed to it + /// + [TestMethod] + public void TaskFailsWithNoInputs() + { + var t = new ResolveComReference.AxImp(); + + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxImp.NoInputFileSpecified"); + } + } +} diff --git a/src/XMakeTasks/UnitTests/AxTlbBaseTask_Tests.cs b/src/XMakeTasks/UnitTests/AxTlbBaseTask_Tests.cs new file mode 100644 index 00000000000..fe04576f65c --- /dev/null +++ b/src/XMakeTasks/UnitTests/AxTlbBaseTask_Tests.cs @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; +using Microsoft.Runtime.Hosting; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests.AxTlbImp_Tests +{ + [TestClass] + sealed public class AxTlbBaseTask_Tests + { + /// + /// Tests the /delaysign switch + /// + [TestMethod] + public void DelaySign() + { + AxTlbBaseTask t = new ResolveComReference.AxImp(); + + Assert.IsFalse(t.DelaySign, "DelaySign should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/delaysign", false /* no response file */); + + t.DelaySign = true; + Assert.IsTrue(t.DelaySign, "DelaySign should be true"); + CommandLine.ValidateHasParameter(t, @"/delaysign", false /* no response file */); + } + + /// + /// Tests the /keycontainer: switch + /// + [TestMethod] + public void KeyContainer() + { + var t = new ResolveComReference.TlbImp(); + t.TypeLibName = "FakeTlb.tlb"; + string badParameterValue = "badKeyContainer"; + string goodParameterValue = "myKeyContainer"; + + try + { + t.ToolPath = Path.GetTempPath(); + + Assert.IsNull(t.KeyContainer, "KeyContainer should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/keycontainer:", false /* no response file */); + + t.KeyContainer = badParameterValue; + Assert.AreEqual(badParameterValue, t.KeyContainer, "New KeyContainer value should be set"); + CommandLine.ValidateHasParameter(t, @"/keycontainer:" + badParameterValue, false /* no response file */); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.StrongNameUtils.NoKeyPairInContainer", t.KeyContainer); + //ensure the key does not exist in the CSP + StrongNameHelpers.StrongNameKeyDelete(goodParameterValue); + + IntPtr publicKeyBlob = IntPtr.Zero; + int publicKeyBlobSize = 0; + + //add key to CSP + if (StrongNameHelpers.StrongNameKeyGen(goodParameterValue, 1 /* leave key registered */, out publicKeyBlob, out publicKeyBlobSize) && publicKeyBlob != IntPtr.Zero) + { + StrongNameHelpers.StrongNameFreeBuffer(publicKeyBlob); + + t.KeyContainer = goodParameterValue; + Assert.AreEqual(goodParameterValue, t.KeyContainer, "New KeyContainer value should be set"); + CommandLine.ValidateHasParameter(t, @"/keycontainer:" + goodParameterValue, false /* no response file */); + Utilities.ExecuteTaskAndVerifyLogDoesNotContainErrorFromResource(t, "AxTlbBaseTask.StrongNameUtils.NoKeyPairInContainer", t.KeyContainer); + } + else + { + Assert.Fail("Key container could not be created."); + } + } + finally + { + //remove key from CSP + StrongNameHelpers.StrongNameKeyDelete(goodParameterValue); + + // get rid of the generated temp file + if (goodParameterValue != null) + { + File.Delete(goodParameterValue); + } + } + } + + /// + /// Tests the /keycontainer: switch with a space in the name + /// + [TestMethod] + public void KeyContainerWithSpaces() + { + AxTlbBaseTask t = new ResolveComReference.AxImp(); + string testParameterValue = @"my Key Container"; + + Assert.IsNull(t.KeyContainer, "KeyContainer should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/keycontainer:", false /* no response file */); + + t.KeyContainer = testParameterValue; + Assert.AreEqual(testParameterValue, t.KeyContainer, "New KeyContainer value should be set"); + CommandLine.ValidateHasParameter(t, @"/keycontainer:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /keyfile: switch + /// + [TestMethod] + public void KeyFile() + { + var t = new ResolveComReference.AxImp(); + t.ActiveXControlName = "FakeControl.ocx"; + string badParameterValue = "myKeyFile.key"; + string goodParameterValue = null; + + try + { + goodParameterValue = FileUtilities.GetTemporaryFile(); + t.ToolPath = Path.GetTempPath(); + + Assert.IsNull(t.KeyFile, "KeyFile should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/keyfile:", false /* no response file */); + + t.KeyFile = badParameterValue; + Assert.AreEqual(badParameterValue, t.KeyFile, "New KeyFile value should be set"); + CommandLine.ValidateHasParameter(t, @"/keyfile:" + badParameterValue, false /* no response file */); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.InvalidKeyFileSpecified", t.KeyFile); + + t.KeyFile = goodParameterValue; + Assert.AreEqual(goodParameterValue, t.KeyFile, "New KeyFile value should be set"); + CommandLine.ValidateHasParameter(t, @"/keyfile:" + goodParameterValue, false /* no response file */); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.StrongNameUtils.NoKeyPairInFile", t.KeyFile); + } + finally + { + if (goodParameterValue != null) + { + // get rid of the generated temp file + File.Delete(goodParameterValue); + } + } + } + + /// + /// Tests the /keyfile: switch with a space in the filename + /// + [TestMethod] + public void KeyFileWithSpaces() + { + AxTlbBaseTask t = new ResolveComReference.TlbImp(); + string testParameterValue = @"C:\Program Files\myKeyFile.key"; + + Assert.IsNull(t.KeyFile, "KeyFile should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/keyfile:", false /* no response file */); + + t.KeyFile = testParameterValue; + Assert.AreEqual(testParameterValue, t.KeyFile, "New KeyFile value should be set"); + CommandLine.ValidateHasParameter(t, @"/keyfile:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the SdkToolsPath property: Should log an error if it's null or a bad path. + /// + [TestMethod] + public void SdkToolsPath() + { + var t = new ResolveComReference.TlbImp(); + t.TypeLibName = "FakeLibrary.tlb"; + string badParameterValue = @"C:\Program Files\Microsoft Visual Studio 10.0\My Fake SDK Path"; + string goodParameterValue = Path.GetTempPath(); + bool taskPassed; + + Assert.IsNull(t.SdkToolsPath, "SdkToolsPath should be null by default"); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + t.SdkToolsPath = badParameterValue; + Assert.AreEqual(badParameterValue, t.SdkToolsPath, "New SdkToolsPath value should be set"); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.SdkToolsPath = goodParameterValue; + + Assert.AreEqual(goodParameterValue, t.SdkToolsPath, "New SdkToolsPath value should be set"); + taskPassed = t.Execute(); + Assert.IsFalse(taskPassed, "Task should still fail -- there are other things wrong with it."); + + // but that particular error shouldn't be there anymore. + string sdkToolsPathMessage = t.Log.FormatResourceString("AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + string messageWithNoCode; + string sdkToolsPathCode = t.Log.ExtractMessageCode(sdkToolsPathMessage, out messageWithNoCode); + e.AssertLogDoesntContain(sdkToolsPathCode); + } + + /// + /// Tests the ToolPath property: Should log an error if it's null or a bad path. + /// + [TestMethod] + public void ToolPath() + { + var t = new ResolveComReference.AxImp(); + t.ActiveXControlName = "FakeControl.ocx"; + string badParameterValue = @"C:\Program Files\Microsoft Visual Studio 10.0\My Fake SDK Path"; + string goodParameterValue = Path.GetTempPath(); + bool taskPassed; + + Assert.IsNull(t.ToolPath, "ToolPath should be null by default"); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + t.ToolPath = badParameterValue; + Assert.AreEqual(badParameterValue, t.ToolPath, "New ToolPath value should be set"); + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.ToolPath = goodParameterValue; + + Assert.AreEqual(goodParameterValue, t.ToolPath, "New ToolPath value should be set"); + taskPassed = t.Execute(); + Assert.IsFalse(taskPassed, "Task should still fail -- there are other things wrong with it."); + + // but that particular error shouldn't be there anymore. + string toolPathMessage = t.Log.FormatResourceString("AxTlbBaseTask.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + string messageWithNoCode; + string toolPathCode = t.Log.ExtractMessageCode(toolPathMessage, out messageWithNoCode); + e.AssertLogDoesntContain(toolPathCode); + } + + /// + /// Tests that strong name sign-related parameters are validated properly, causing the task + /// to fail if they are incorrectly set up. + /// + [TestMethod] + public void TaskFailsWhenImproperlySigned() + { + var t = new ResolveComReference.TlbImp(); + t.TypeLibName = "Blah.tlb"; + string tempKeyContainer = null; + string tempKeyFile = null; + + try + { + tempKeyContainer = FileUtilities.GetTemporaryFile(); + tempKeyFile = FileUtilities.GetTemporaryFile(); + t.ToolPath = Path.GetTempPath(); + + // DelaySign is passed without a KeyFile or a KeyContainer + t.DelaySign = true; + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.CannotSpecifyDelaySignWithoutEitherKeyFileOrKeyContainer"); + + // KeyContainer and KeyFile are both passed in + t.KeyContainer = tempKeyContainer; + t.KeyFile = tempKeyFile; + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.CannotSpecifyBothKeyFileAndKeyContainer"); + + // All the inputs are correct, but the KeyContainer passed in is bad + t.DelaySign = false; + t.KeyContainer = tempKeyContainer; + t.KeyFile = null; + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.StrongNameUtils.NoKeyPairInContainer", t.KeyContainer); + + // All the inputs are correct, but the KeyFile passed in is bad + t.KeyContainer = null; + t.KeyFile = tempKeyFile; + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "AxTlbBaseTask.StrongNameUtils.NoKeyPairInFile", t.KeyFile); + } + finally + { + if (tempKeyContainer != null) + { + File.Delete(tempKeyContainer); + } + if (tempKeyFile != null) + { + File.Delete(tempKeyContainer); + } + } + } + } + + sealed internal class Utilities + { + /// + /// Given an instance of an AxImp task, executes that task (assuming all necessary parameters + /// have been set ahead of time) and verifies that the execution log contains the error + /// corresponding to the resource name passed in. + /// + /// The task to execute and check + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + internal static void ExecuteTaskAndVerifyLogContainsErrorFromResource(AxTlbBaseTask t, string errorResource, params object[] args) + { + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + bool taskPassed = t.Execute(); + Assert.IsFalse(taskPassed, "Task should have failed"); + + VerifyLogContainsErrorFromResource(e, t.Log, errorResource, args); + } + + /// + /// Given a log and a resource string, acquires the text of that resource string and + /// compares it to the log. Asserts if the log does not contain the desired string. + /// + /// The MockEngine that contains the log we're checking + /// The TaskLoggingHelper that we use to load the string resource + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + internal static void VerifyLogContainsErrorFromResource(MockEngine e, TaskLoggingHelper log, string errorResource, params object[] args) + { + string errorMessage = log.FormatResourceString(errorResource, args); + e.AssertLogContains(errorMessage); + } + + /// + /// Given an instance of an AxImp task, executes that task (assuming all necessary parameters + /// have been set ahead of time) and verifies that the execution log does not contain the error + /// corresponding to the resource name passed in. + /// + /// The task to execute and check + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + internal static void ExecuteTaskAndVerifyLogDoesNotContainErrorFromResource(AxTlbBaseTask t, string errorResource, params object[] args) + { + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + bool taskPassed = t.Execute(); + + VerifyLogDoesNotContainErrorFromResource(e, t.Log, errorResource, args); + } + + /// + /// Given a log and a resource string, acquires the text of that resource string and + /// compares it to the log. Assert fails if the log contains the desired string. + /// + /// The MockEngine that contains the log we're checking + /// The TaskLoggingHelper that we use to load the string resource + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + internal static void VerifyLogDoesNotContainErrorFromResource(MockEngine e, TaskLoggingHelper log, string errorResource, params object[] args) + { + string errorMessage = log.FormatResourceString(errorResource, args); + e.AssertLogDoesntContain(errorMessage); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CSharpParserUtilitites_Tests.cs b/src/XMakeTasks/UnitTests/CSharpParserUtilitites_Tests.cs new file mode 100644 index 00000000000..b7025789ae7 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CSharpParserUtilitites_Tests.cs @@ -0,0 +1,385 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CSharpParserUtilititesTests + { + // Try just and empty file + [TestMethod] + public void EmptyFile() + { + AssertParse("", null); + } + + // Simplest case of getting a fully-qualified class name from + // a c# file. + [TestMethod] + public void Simple() + { + AssertParse("namespace MyNamespace { class MyClass {} }", "MyNamespace.MyClass"); + } + + [TestMethod] + public void EmbeddedComment() + { + AssertParse("namespace /**/ MyNamespace /**/ { /**/ class /**/ MyClass/**/{}} //", "MyNamespace.MyClass"); + } + + [TestMethod] + public void MinSpace() + { + AssertParse("namespace MyNamespace{class MyClass{}}", "MyNamespace.MyClass"); + } + + [TestMethod] + public void NoNamespace() + { + AssertParse("class MyClass{}", "MyClass"); + } + + [TestMethod] + public void SneakyComment() + { + AssertParse("/*namespace MyNamespace { */ class MyClass {} /* } */", "MyClass"); + } + + [TestMethod] + public void CompoundNamespace() + { + AssertParse("namespace MyNamespace.Feline { class MyClass {} }", "MyNamespace.Feline.MyClass"); + } + + [TestMethod] + public void NestedNamespace() + { + AssertParse("namespace MyNamespace{ namespace Feline {class MyClass {} }}", "MyNamespace.Feline.MyClass"); + } + + [TestMethod] + public void NestedNamespace2() + { + AssertParse("namespace MyNamespace{ namespace Feline {namespace Bovine{public sealed class MyClass {} }} }", "MyNamespace.Feline.Bovine.MyClass"); + } + + [TestMethod] + public void NestedCompoundNamespace() + { + AssertParse("namespace MyNamespace/**/.A{ namespace Feline . B {namespace Bovine.C {sealed class MyClass {} }} }", "MyNamespace.A.Feline.B.Bovine.C.MyClass"); + } + + [TestMethod] + public void DoubleClass() + { + AssertParse("namespace MyNamespace{class Feline{}class Bovine}", "MyNamespace.Feline"); + } + + [TestMethod] + public void EscapedKeywordClass() + { + AssertParse("namespace MyNamespace{class @class{}}", "MyNamespace.class"); + } + + [TestMethod] + public void LeadingUnderscore() + { + AssertParse("namespace _MyNamespace{class _MyClass{}}", "_MyNamespace._MyClass"); + } + + [TestMethod] + public void SkipInterveningNamespaces() + { + AssertParse("namespace MyNamespace { namespace XXX {} class MyClass {} }", "MyNamespace.MyClass"); + } + + + [TestMethod] + public void SkipPeerNamespaces() + { + AssertParse("namespace XXX {} namespace MyNamespace { class MyClass {} }", "MyNamespace.MyClass"); + } + + [TestMethod] + public void SolitaryNamespaceSyntaxError() + { + AssertParse("namespace", null); + } + + [TestMethod] + public void NamespaceNamespaceSyntaxError() + { + AssertParse("namespace namespace", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error. But we can't tell because the preprocessor doesn't work yet." + public void NamelessNamespaceSyntaxError() + { + AssertParse("namespace { class MyClass {} }", null); + } + + [TestMethod] + public void ScopelessNamespaceClassSyntaxError() + { + AssertParse("namespace class {}", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error, but since the preprocessor isn't working, we can't be sure." + public void NamespaceDotDotSyntaxError() + { + AssertParse("namespace poo..i { class MyClass {} }", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error, but since the preprocessor isn't working, we can't be sure." + public void DotNamespaceSyntaxError() + { + AssertParse("namespace .i { class MyClass {} }", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error, but since the preprocessor isn't working, we can't be sure." + public void NamespaceDotNamespaceSyntaxError() + { + AssertParse("namespace i { namespace .j {class MyClass {}} }", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error, but we'd have to look-ahead past the class name." + public void NamespaceClassDotClassSyntaxError() + { + AssertParse("namespace i { namespace j {class a.b {}} }", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error, but since the preprocessor isn't working, we can't be sure." + public void NamespaceCloseScopeSyntaxError() + { + AssertParse("namespace i } class a {} }", null); + } + + [TestMethod] + [Ignore] // "If we went to the trouble of tracking open and closing scopes, we really should do something like build up a parse tree. Too much hassle, just for this simple function." + public void NamespaceEmbeddedScopeSyntaxError() + { + AssertParse("namespace i { {} class a {} }", null); + } + + [TestMethod] + [Ignore] // "This should be a syntax error, but since the preprocessor isn't working, we can't be sure." + public void ScopelessNamespaceSyntaxError() + { + AssertParse("namespace i; namespace j { class a {} }", null); + } + + [TestMethod] + public void AssemblyAttributeBool() + { + AssertParse("[assembly :AssemblyDelaySign(false)] namespace i { class a { } }", "i.a"); + } + + [TestMethod] + public void AssemblyAttributeString() + { + AssertParse("[assembly :MyString(\"namespace\")] namespace i { class a { } }", "i.a"); + } + + [TestMethod] + public void AssemblyAttributeInt() + { + AssertParse("[assembly :MyInt(55)] namespace i { class a { } }", "i.a"); + } + + [TestMethod] + public void AssemblyAttributeReal() + { + AssertParse("[assembly :MyReal(5.5)] namespace i { class a { } }", "i.a"); + } + + [TestMethod] + public void AssemblyAttributeNull() + { + AssertParse("[assembly :MyNull(null)] namespace i { class a { } }", "i.a"); + } + + [TestMethod] + public void AssemblyAttributeChar() + { + AssertParse("[assembly :MyChar('a')] namespace i { class a { } }", "i.a"); + } + + + [TestMethod] + public void ClassAttributeBool() + { + AssertParse("namespace i { [ClassDelaySign(false)] class a { } }", "i.a"); + } + + [TestMethod] + public void ClassAttributeString() + { + AssertParse("namespace i { [MyString(\"class b\")] class a { } }", "i.a"); + } + + [TestMethod] + public void ClassAttributeInt() + { + AssertParse("namespace i { [MyInt(55)] class a { } }", "i.a"); + } + + [TestMethod] + public void ClassAttributeReal() + { + AssertParse("namespace i { [MyReal(5.5)] class a { } }", "i.a"); + } + + [TestMethod] + public void ClassAttributeNull() + { + AssertParse("[namespace i { MyNull(null)] class a { } }", "i.a"); + } + + [TestMethod] + public void ClassAttributeChar() + { + AssertParse("namespace i { [MyChar('a')] class a { } }", "i.a"); + } + + [TestMethod] + [Ignore] // "For this to pass, we need to support every kind of Char token in the tokenizer" + public void ClassAttributeCharIsCloseScope() + { + AssertParse("namespace i { [MyChar('\x0000')] class a { } }", "i.a"); + } + + [TestMethod] + public void ClassAttributeStringIsCloseScope() + { + AssertParse("namespace i { [MyString(\"}\")] class a { } }", "i.a"); + } + + [TestMethod] + public void NameSpaceStructEnum() + { + AssertParse("namespace n { public struct s { enum e {} } class c {} }", "n.c"); + } + + [TestMethod] + public void PreprocessorControllingTwoNamespaces() + { + // This works by coincidence since preprocessor directives are currently ignored. + AssertParse + ( + @" +#if (false) +namespace n1 +#else +namespace n2 +#endif +{ class c {} } + ", "n2.c"); + } + + [TestMethod] + public void PreprocessorControllingTwoNamespacesWithInterveningKeyword() + { + // This works by coincidence since preprocessor directives are currently ignored. + AssertParse + ( + @" +#if (false) +namespace n1 +#else +using a=b; +namespace n2 +#endif +{ class c {} } + ", "n2.c"); + } + + [TestMethod] + public void Preprocessor() + { + AssertParse + ( + @" +#if MY_CONSTANT +namespace i +{ + #region Put the class in a region + class a + { + } + #endregion +} +#endif // MY_CONSTANT + ", "i.a"); + } + + [TestMethod] + [Ignore] // "Preprocessor is not yet implemented." + public void PreprocessorNamespaceInFalsePreprocessorBlock() + { + AssertParse + ( + @" +#if (false) +namespace i +{ +#endif + class a + { + } +#if (false) +namespace i +} +#endif + ", "a"); + } + + + + [TestMethod] + public void Regress_Mutation_SingleLineCommentsShouldBeIgnored() + { + AssertParse + ( + @" +namespace n2 +// namespace n1 +{ class c {} } + ", "n2.c"); + } + + /* + * Method: AssertParse + * + * Parse 'source' as C# source code and get the first class name fully-qualified + * with namespace information. That classname must match the expected class name. + */ + private static void AssertParse(string source, string expectedClassName) + { + ExtractedClassName className = CSharpParserUtilities.GetFirstClassNameFullyQualified + ( + StreamHelpers.StringToStream(source) + ); + + Assert.AreEqual(expectedClassName, className.Name); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/CSharpTokenizer_Tests.cs b/src/XMakeTasks/UnitTests/CSharpTokenizer_Tests.cs new file mode 100644 index 00000000000..bcdf932d435 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CSharpTokenizer_Tests.cs @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +using Microsoft.Build.Shared.LanguageParser; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CSharpTokenizerTests + { + // Simple whitespace handling. + [TestMethod] + public void Empty() { AssertTokenize("", "", 0); } + [TestMethod] + public void OneSpace() { AssertTokenize(" ", " \x0d", ".Whitespace"); } + [TestMethod] + public void TwoSpace() { AssertTokenize(" ", " \x0d", ".Whitespace"); } + [TestMethod] + public void Tab() { AssertTokenize("\t", "\t\x0d", ".Whitespace"); } + [TestMethod] + public void TwoTab() { AssertTokenize("\t\t", "\t\t\x0d", ".Whitespace"); } + [TestMethod] + public void SpaceTab() { AssertTokenize(" \t", " \t\x0d", ".Whitespace"); } + [TestMethod] + public void CrLf() { AssertTokenize("\x0d\x0a", ".Whitespace"); } + [TestMethod] + public void SpaceCrLfSpace() { AssertTokenize(" \x0d\x0a ", " \x0d\x0a \x0d", ".Whitespace"); } + // From section 2.3.3 of the C# spec, these are also whitespace. + [TestMethod] + public void LineSeparator() { AssertTokenizeUnicode("\x2028", ".Whitespace"); } + [TestMethod] + public void ParagraphSeparator() { AssertTokenizeUnicode("\x2029", ".Whitespace"); } + + /* + Special whitespace handling. + Horizontal tab character (U+0009) + Vertical tab character (U+000B) + Form feed character (U+000C) + */ + [TestMethod] + public void SpecialWhitespace() { AssertTokenize("\x09\x0b\x0c\x0d", ".Whitespace"); } + + // One-line comments (i.e. those starting with //) + [TestMethod] + public void OneLineComment() { AssertTokenize("// My one line comment.\x0d", ".Comment.Whitespace"); } + [TestMethod] + public void SpaceOneLineComment() { AssertTokenize(" // My one line comment.\x0d", ".Whitespace.Comment.Whitespace"); } + [TestMethod] + public void OneLineCommentTab() { AssertTokenize(" //\tMy one line comment.\x0d", ".Whitespace.Comment.Whitespace"); } + [TestMethod] + public void OneLineCommentCr() { AssertTokenize("// My one line comment.\x0d", ".Comment.Whitespace"); } + [TestMethod] + public void OneLineCommentLf() { AssertTokenize("// My one line comment.\x0a", ".Comment.Whitespace"); } + [TestMethod] + public void OneLineCommentLineSeparator() { AssertTokenizeUnicode("// My one line comment.\x2028", ".Comment.Whitespace"); } + [TestMethod] + public void OneLineCommentParagraphSeparator() { AssertTokenizeUnicode("// My one line comment.\x2029", ".Comment.Whitespace"); } + [TestMethod] + public void OneLineCommentWithEmbeddedMultiLine() { AssertTokenize("// /* */\x0d", ".Comment.Whitespace"); } + + // Multi-line comments (i.e those like /* */) + [TestMethod] + public void OneLineMultilineComment() { AssertTokenize("/* My comment. */\x0d", ".Comment.Whitespace"); } + [TestMethod] + public void MultilineComment() { AssertTokenize("/* My comment. \x0d\x0a Second Line*/\x0d", ".Comment.Whitespace", 3); } + [TestMethod] + public void MultilineCommentWithEmbeddedSingleLine() { AssertTokenize("/* // */\x0d", ".Comment.Whitespace"); } + [TestMethod] + public void LeftHalfOfUnbalanceMultilineComment() { AssertTokenize("/*\x0d", ".EndOfFileInsideComment"); } + [TestMethod] + public void LeftHalfOfUnbalanceMultilineCommentWithStuff() { AssertTokenize("/* unbalanced\x0d", ".EndOfFileInsideComment"); } + + // If the last character of the source file is a Control-Z character (U+001A), this character is deleted. + [TestMethod] + public void NothingPlustControlZatEOF() { AssertTokenize("\x1A", "", "", 0); } + [TestMethod] + public void SomethingPlusControlZatEOF() { AssertTokenize("// My comment\x1A", "// My comment\x0d", ".Comment.Whitespace"); } + + // A carriage-return character (U+000D) is added to the end of the source file if that source file is non-empty and if the last character + // of the source file is not a carriage return (U+000D), a line feed (U+000A), a line separator (U+2028), or a paragraph separator + // (U+2029). + [TestMethod] + public void NoEOLatEOF() { AssertTokenize("// My comment", "// My comment\x0d", ".Comment.Whitespace"); } + [TestMethod] + public void NoEOLatEOFButFileIsEmpty() { AssertTokenize("", "", "", 0); } + + // An identifier that has a "_" embedded somewhere + [TestMethod] + public void IdentifierWithEmbeddedUnderscore() { AssertTokenize("_x_\xd", ".Identifier.Whitespace"); } + + // An identifier with a number + [TestMethod] + public void IdentifierWithNumber() { AssertTokenize("x3\xd", ".Identifier.Whitespace"); } + + // An non-identifier with a @ and a number + [TestMethod] + public void EscapedIdentifierWithNumber() { AssertTokenize("@3Identifier\xd", ".ExpectedIdentifier"); } + + // A very simple namespace and class. + [TestMethod] + public void NamespacePlusClass() + { + AssertTokenize + ("namespace MyNamespace { class MyClass {} }\x0d", + ".Keyword.Whitespace.Identifier.Whitespace.OpenScope.Whitespace.Keyword.Whitespace.Identifier.Whitespace.OpenScope.CloseScope.Whitespace.CloseScope.Whitespace"); + } + + // If a keyword has '@' in front, then its treated as an identifier. + [TestMethod] + public void EscapedKeywordMakesIdentifier() + { + AssertTokenize + ( + "namespace @namespace { class @class {} }\x0d", + "namespace namespace { class class {} }\x0d", // Resulting tokens have '@' stripped. + ".Keyword.Whitespace.Identifier.Whitespace.OpenScope.Whitespace.Keyword.Whitespace.Identifier.Whitespace.OpenScope.CloseScope.Whitespace.CloseScope.Whitespace" + ); + } + + // Check boolean literals + [TestMethod] + public void LiteralTrue() { AssertTokenize("true\x0d", ".BooleanLiteral.Whitespace"); } + [TestMethod] + public void LiteralFalse() { AssertTokenize("false\x0d", ".BooleanLiteral.Whitespace"); } + [TestMethod] + public void LiteralNull() { AssertTokenize("null\x0d", ".NullLiteral.Whitespace"); } + + // Check integer literals + [TestMethod] + public void HexIntegerLiteral() { AssertTokenize("0x123F\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexUppercaseXIntegerLiteral() { AssertTokenize("0X1f23\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void IntegerLiteral() { AssertTokenize("123\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void InvalidHexIntegerWithNoneValid() { AssertTokenize("0xG\x0d", ".ExpectedValidHexDigit"); } + + // Hex literal long suffix: U u L l UL Ul uL ul LU Lu lU lu + [TestMethod] + public void HexIntegerLiteralUpperU() { AssertTokenize("0x123FU\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralLowerU() { AssertTokenize("0x123Fu\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralUpperL() { AssertTokenize("0x123FL\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralLowerL() { AssertTokenize("0x123Fl\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralUpperUUpperL() { AssertTokenize("0x123FUL\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralUpperULowerL() { AssertTokenize("0x123FUl\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralLowerUUpperL() { AssertTokenize("0x123FuL\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralUpperLUpperU() { AssertTokenize("0x123FLU\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralUpperLLowerU() { AssertTokenize("0x123FLu\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralLowerLUpperU() { AssertTokenize("0x123FlU\x0d", ".HexIntegerLiteral.Whitespace"); } + [TestMethod] + public void HexIntegerLiteralLowerLLowerU() { AssertTokenize("0x123Flu\x0d", ".HexIntegerLiteral.Whitespace"); } + + // Decimal literal long suffix: U u L l UL Ul uL ul LU Lu lU lu + [TestMethod] + public void DecimalIntegerLiteralUpperU() { AssertTokenize("1234U\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralLowerU() { AssertTokenize("1234u\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralUpperL() { AssertTokenize("1234L\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralLowerL() { AssertTokenize("1234l\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralUpperUUpperL() { AssertTokenize("1234UL\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralUpperULowerL() { AssertTokenize("1234Ul\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralLowerUUpperL() { AssertTokenize("1234uL\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralUpperLUpperU() { AssertTokenize("1234LU\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralUpperLLowerU() { AssertTokenize("1234Lu\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralLowerLUpperU() { AssertTokenize("1234lU\x0d", ".DecimalIntegerLiteral.Whitespace"); } + [TestMethod] + public void DecimalIntegerLiteralLowerLLowerU() { AssertTokenize("1234lu\x0d", ".DecimalIntegerLiteral.Whitespace"); } + + // Reals aren't supported yet. + // Reals can take many different forms: 1.1, .1, 1.1e6, etc. + // If you turn this on, please create test for the other forms too. + [TestMethod] + [Ignore] // "Reals aren't supported yet." + public void RealLiteral1() { AssertTokenize("1.1\x0d", ".RealLiteral.Whitespace"); } + + // Char literals aren't supported yet. + [TestMethod] + public void CharLiteral1() { AssertTokenize("'c'\x0d", ".CharLiteral.Whitespace"); } + + [TestMethod] + [Ignore] // "Escape sequences aren't supported" + public void CharLiteralIllegalEscapeSequence() { AssertTokenize("'\\z'\x0d", ".SyntaxErrorIllegalEscapeSequence"); } + + [TestMethod] + [Ignore] // "Escape sequences aren't supported" + public void CharLiteralHexEscapeSequence() { AssertTokenize("'\\x0022a'\x0d", "'\"a'\x0d", ".CharLiteral.Whitespace"); } + + // Check string literals + [TestMethod] + public void LiteralStringBasic() { AssertTokenize("\"string\"\x0d", ".StringLiteral.Whitespace"); } + [TestMethod] + public void LiteralStringAllEscapes() { AssertTokenize("\"\\'\\\"\\\\\\0\\a\\b\\f\\n\\r\\t\\x0\\v\"\x0d", ".StringLiteral.Whitespace"); } + [TestMethod] + public void LiteralStringUnclosed() { AssertTokenize("\"string\x0d", ".NewlineInsideString"); } + [TestMethod] + public void LiteralVerbatimStringBasic() { AssertTokenize("@\"string\"\x0d", "\"string\"\x0d", ".StringLiteral.Whitespace"); } + [TestMethod] + public void LiteralVerbatimStringAllEscapes() { AssertTokenize("@\"\\a\\b\\c\"\x0d", "\"\\a\\b\\c\"\x0d", ".StringLiteral.Whitespace"); } + [TestMethod] + public void LiteralVerbatimStringUnclosed() { AssertTokenize("@\"string\x0d", ".EndOfFileInsideString"); } + [TestMethod] + public void LiteralVerbatimStringQuoteEscapeSequence() { AssertTokenize("@\"\"\"\"\x0d", "\"\"\"\"\x0d", ".StringLiteral.Whitespace"); } + + // Single-digit operators and punctuators. + [TestMethod] + public void PunctuatorOpenBracket() { AssertTokenize("[\x0d", ".OperatorOrPunctuator.Whitespace"); } + [TestMethod] + public void PunctuatorCloseBracket() { AssertTokenize("]\x0d", ".OperatorOrPunctuator.Whitespace"); } + [TestMethod] + public void PunctuatorOpenParen() { AssertTokenize("(\x0d", ".OperatorOrPunctuator.Whitespace"); } + [TestMethod] + public void PunctuatorCloseParen() { AssertTokenize(")\x0d", ".OperatorOrPunctuator.Whitespace"); } + [TestMethod] + public void PunctuatorDot() { AssertTokenize(".\x0d", ".OperatorOrPunctuator.Whitespace"); } + [TestMethod] + public void PunctuatorColon() { AssertTokenize(":\x0d", ".OperatorOrPunctuator.Whitespace"); } + [TestMethod] + public void PunctuatorSemicolon() { AssertTokenize(";\x0d", ".OperatorOrPunctuator.Whitespace"); } + + // Preprocessor. + [TestMethod] + public void Preprocessor() { AssertTokenize("#if\x0d", ".OpenConditionalDirective.Whitespace"); } + + + /* + * Method: AssertTokenize + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also, the source must be regenerated exactly when the tokens are concatenated + * back together, + */ + static private void AssertTokenize(string source, string expectedTokenKey) + { + // Most of the time, we expect the rebuilt source to be the same as the input source. + AssertTokenize(source, source, expectedTokenKey); + } + + /* + * Method: AssertTokenizeUnicode + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also, the source must be regenerated exactly when the tokens are concatenated + * back together, + */ + static private void AssertTokenizeUnicode(string source, string expectedTokenKey) + { + // Most of the time, we expect the rebuilt source to be the same as the input source. + AssertTokenizeUnicode(source, source, expectedTokenKey); + } + + /* + * Method: AssertTokenize + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also, the source must be regenerated exactly when the tokens are concatenated + * back together, + */ + static private void AssertTokenize + ( + string source, + string expectedTokenKey, + int expectedLastLineNumber + ) + { + // Most of the time, we expect the rebuilt source to be the same as the input source. + AssertTokenize(source, source, expectedTokenKey, expectedLastLineNumber); + } + + /* + * Method: AssertTokenizeUnicode + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also, the source must be regenerated exactly when the tokens are concatenated + * back together, + */ + static private void AssertTokenizeUnicode + ( + string source, + string expectedTokenKey, + int expectedLastLineNumber + ) + { + // Most of the time, we expect the rebuilt source to be the same as the input source. + AssertTokenizeUnicode(source, source, expectedTokenKey, expectedLastLineNumber); + } + + /* + * Method: AssertTokenize + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also compare the source that is regenerated by concatenating all of the tokens + * to 'expectedSource'. + */ + static private void AssertTokenize + ( + string source, + string expectedSource, + string expectedTokenKey + ) + { + // Two lines is the most common test case. + AssertTokenize(source, expectedSource, expectedTokenKey, 1); + } + + /* + * Method: AssertTokenizeUnicode + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also compare the source that is regenerated by concatenating all of the tokens + * to 'expectedSource'. + */ + static private void AssertTokenizeUnicode + ( + string source, + string expectedSource, + string expectedTokenKey + ) + { + // Two lines is the most common test case. + AssertTokenizeUnicode(source, expectedSource, expectedTokenKey, 1); + } + + /* + * Method: AssertTokenizeUnicode + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also compare the source that is regenerated by concatenating all of the tokens + * to 'expectedSource'. + */ + static private void AssertTokenizeUnicode + ( + string source, + string expectedSource, + string expectedTokenKey, + int expectedLastLineNumber + ) + { + AssertTokenizeStream + ( + StreamHelpers.StringToStream(source, System.Text.Encoding.Unicode), + expectedSource, + expectedTokenKey, + expectedLastLineNumber + ); + } + + /* + * Method: AssertTokenize + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also compare the source that is regenerated by concatenating all of the tokens + * to 'expectedSource'. + */ + static private void AssertTokenize + ( + string source, + string expectedSource, + string expectedTokenKey, + int expectedLastLineNumber + ) + { + // This version of AssertTokenize tests several different encodings. + // The reason is that we want to be sure each of these works in the + // various encoding formats supported by C# + AssertTokenizeStream(StreamHelpers.StringToStream(source), expectedSource, expectedTokenKey, expectedLastLineNumber); + AssertTokenizeStream(StreamHelpers.StringToStream(source, System.Text.Encoding.Unicode), expectedSource, expectedTokenKey, expectedLastLineNumber); + AssertTokenizeStream(StreamHelpers.StringToStream(source, System.Text.Encoding.UTF8), expectedSource, expectedTokenKey, expectedLastLineNumber); + AssertTokenizeStream(StreamHelpers.StringToStream(source, System.Text.Encoding.BigEndianUnicode), expectedSource, expectedTokenKey, expectedLastLineNumber); + AssertTokenizeStream(StreamHelpers.StringToStream(source, System.Text.Encoding.UTF32), expectedSource, expectedTokenKey, expectedLastLineNumber); + AssertTokenizeStream(StreamHelpers.StringToStream(source, System.Text.Encoding.ASCII), expectedSource, expectedTokenKey, expectedLastLineNumber); + } + + /* + * Method: AssertTokenizeStream + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also compare the source that is regenerated by concatenating all of the tokens + * to 'expectedSource'. + */ + static private void AssertTokenizeStream + ( + Stream source, + string expectedSource, + string expectedTokenKey, + int expectedLastLineNumber + ) + { + CSharpTokenizer tokens = new CSharpTokenizer + ( + source, + false + ); + string results = ""; + string tokenKey = ""; + int lastLine = 0; + bool syntaxError = false; + foreach (Token t in tokens) + { + results += t.InnerText; + lastLine = t.Line; + + if (!syntaxError) + { + // Its not really a file name, but GetExtension serves the purpose of getting the class name without + // the namespace prepended. + string tokenClass = t.ToString(); + int pos = tokenClass.LastIndexOfAny(new char[] { '+', '.' }); + + tokenKey += "."; + tokenKey += tokenClass.Substring(pos + 1); + } + + if (t is SyntaxErrorToken) + { + // Stop processing after the first syntax error because + // the order of tokens after this is an implementation detail and + // shouldn't be encoded into the unit tests. + syntaxError = true; + } + } + tokenKey = tokenKey.Replace("Token", ""); + Console.WriteLine(tokenKey); + + Assert.AreEqual(expectedSource, results); + Assert.AreEqual(expectedTokenKey, tokenKey); + Assert.AreEqual(expectedLastLineNumber, lastLine); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/CallTarget_Tests.cs b/src/XMakeTasks/UnitTests/CallTarget_Tests.cs new file mode 100644 index 00000000000..115504a92d0 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CallTarget_Tests.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CallTarget_Tests + { + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Simple test of the CallTarget task. + /// + [TestMethod] + public void Simple() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(@" + + + + + + + + + + "); + + logger.AssertLogContains("Inside B"); + } + + /// + /// Simple test of the CallTarget task, where one of the middle targets invoked fails. + /// + [TestMethod] + public void FailedTargets() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectFailure(@" + + + + + + + + + + + + + + + + "); + + logger.AssertLogContains("Inside A"); + logger.AssertLogContains("Inside B"); + + // Target C should not have been run. + logger.AssertLogDoesntContain("Inside C"); + } + + /// + /// Test the CallTarget task, where one of the middle targets invoked fails, but we + /// specified RunEachTargetSeparately, so all the targets should have been run anyway. + /// + [TestMethod] + public void FailedTargetsRunSeparately() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectFailure(@" + + + + + + + + + + + + + + + + "); + + // All three targets should have been run. + logger.AssertLogContains("Inside A"); + logger.AssertLogContains("Inside B"); + logger.AssertLogContains("Inside C"); + } + + /// + /// Test the CallTarget task, where we don't pass in any targets. This is expected + /// to succeed, so that callers of the task don't have to add a Condition to ensure + /// that the list of targets is non-empty. + /// + [TestMethod] + public void NoTargets() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(@" + + + + + + + "); + } + + /// + /// Test the CallTarget task and capture the outputs of the invoked targets. + /// + [TestMethod] + public void CaptureTargetOutputs() + { + Project project = ObjectModelHelpers.CreateInMemoryProject(@" + + + + + + + + + + + + + + + + + + + + + + "); + + ProjectInstance instance = project.CreateProjectInstance(); + bool success = instance.Build(); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + + IEnumerable targetOutputs = instance.GetItems("myfancytargetoutputs"); + + // Convert to a list of TaskItems for easier verification. + List targetOutputsTaskItems = new List(); + foreach (ProjectItemInstance item in targetOutputs) + { + targetOutputsTaskItems.Add(new TaskItem(item.EvaluatedInclude)); + } + + // Order independent verification of the right set of items. + ObjectModelHelpers.AssertItemsMatch(@" + c.txt + b.txt + a.t!@#$%^xt + ", + targetOutputsTaskItems.ToArray(), false /* ignore the order */); + } + + [TestMethod] + public void CaptureTargetOutputsRunningEachTargetSeparately() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(@" + + + + + + + + + + + + + "); + + // All three targets should have been run. + logger.AssertLogContains("CallTarget Outputs: a.txt;b.txt;c.txt"); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CodeTaskFactoryTests.cs b/src/XMakeTasks/UnitTests/CodeTaskFactoryTests.cs new file mode 100644 index 00000000000..0c41e543201 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CodeTaskFactoryTests.cs @@ -0,0 +1,1045 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using System.IO; +using System; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.CodeDom.Compiler; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CodeTaskFactoryTests + { + /// + /// Test the simple case where we have a string parameter and we want to log that. + /// + [TestMethod] + public void BuildTaskSimpleCodeFactory() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + } + + /// + /// Test the simple case where we have a string parameter and we want to log that. + /// Specifically testing that even when the ToolsVersion is post-4.0, and thus + /// Microsoft.Build.Tasks.v4.0.dll is expected to NOT be in MSBuildToolsPath, that + /// we will redirect under the covers to use the current tasks instead. + /// + [TestMethod] + public void BuildTaskSimpleCodeFactory_RedirectFrom4() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + mockLogger.AssertLogDoesntContain("Microsoft.Build.Tasks.v4.0.dll"); + } + + /// + /// Test the simple case where we have a string parameter and we want to log that. + /// Specifically testing that even when the ToolsVersion is post-12.0, and thus + /// Microsoft.Build.Tasks.v12.0.dll is expected to NOT be in MSBuildToolsPath, that + /// we will redirect under the covers to use the current tasks instead. + /// + [TestMethod] + [Ignore] // Doesn't make sense while we continue to (temporarily) install Tasks.v12.0 + public void BuildTaskSimpleCodeFactory_RedirectFrom12() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + mockLogger.AssertLogDoesntContain("Microsoft.Build.Tasks.v12.0.dll"); + } + + /// + /// Test the simple case where we have a string parameter and we want to log that. + /// Specifically testing that even when the ToolsVersion is post-4.0, and we have redirection + /// logic in place for the AssemblyFile case to deal with Microsoft.Build.Tasks.v4.0.dll not + /// being in MSBuildToolsPath anymore, that this does NOT affect full fusion AssemblyNames -- + /// it's picked up from the GAC, where it is anyway, so there's no need to redirect. + /// + [TestMethod] + public void BuildTaskSimpleCodeFactory_NoAssemblyNameRedirect() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + mockLogger.AssertLogContains("Microsoft.Build.Tasks.Core, Version=14.1.0.0"); + } + + /// + /// Test the simple case where we have a string parameter and we want to log that. + /// + [TestMethod] + public void VerifyRequiredAttribute() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + mockLogger.AssertLogContains("MSB4044"); + } + + /// + /// Verify we get an error if a runtime exception is logged + /// + [TestMethod] + public void RuntimeException() + { + string projectFileContents = @" + + + + + + + + throw new InvalidOperationException(""MyCustomException""); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, true); + mockLogger.AssertLogContains("MSB4018"); + mockLogger.AssertLogContains("MyCustomException"); + } + + /// + /// Verify we get an error if a the languages attribute is set but it is empty + /// + [TestMethod] + public void EmptyLanguage() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.AttributeEmpty"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Language")); + } + + + /// + /// Verify we get an error if a the Type attribute is set but it is empty + /// + [TestMethod] + public void EmptyType() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.AttributeEmpty"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Type")); + } + + /// + /// Verify we get an error if a the source attribute is set but it is empty + /// + [TestMethod] + public void EmptySource() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.AttributeEmpty"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Source")); + } + + /// + /// Verify we get an error if a reference is missing an include attribute is set but it is empty + /// + [TestMethod] + public void EmptyReferenceInclude() + { + string projectFileContents = @" + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.AttributeEmpty"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Include")); + } + + /// + /// Verify we get an error if a Using statement is missing an namespace attribute is set but it is empty + /// + [TestMethod] + public void EmptyUsingNamespace() + { + string projectFileContents = @" + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.AttributeEmpty"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Namespace")); + } + + /// + /// Verify we get pass even if the reference is not a full path + /// + [TestMethod] + public void ReferenceNotPath() + { + string projectFileContents = @" + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello"); + } + + /// + /// Verify we get an error a reference has strange chars + /// + [TestMethod] + public void ReferenceInvalidChars() + { + string projectFileContents = @" + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + mockLogger.AssertLogContains("MSB3755"); + mockLogger.AssertLogContains("@@#$@#"); + } + + /// + /// Verify we get an error if a using has invalid chars + /// + [TestMethod] + public void UsingInvalidChars() + { + string projectFileContents = @" + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + mockLogger.AssertLogContains("CS1646"); + } + + /// + /// Verify we get an error if the sources points to an invalid file + /// + [TestMethod] + public void SourcesInvalidFile() + { + string tempFileName = "Moose_" + Guid.NewGuid().ToString() + ".cs"; + + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + mockLogger.AssertLogContains(Environment.GetEnvironmentVariable("SystemDrive") + '\\' + tempFileName); + } + + /// + /// Verify we get an error if a the the code element is missing + /// + [TestMethod] + public void MissingCodeElement() + { + string projectFileContents = @" + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + mockLogger.AssertLogContains(String.Format(ResourceUtilities.GetResourceString("CodeTaskFactory.CodeElementIsMissing"), "CustomTaskFromCodeFactory_MissingCodeElement")); + } + + /// + /// Test the case where we have adding a using statement + /// + [TestMethod] + public void BuildTaskSimpleCodeFactoryTestExtraUsing() + { + string projectFileContents = @" + + + + + + + + + string linqString = ExpressionType.Add.ToString(); + Log.LogMessage(MessageImportance.High, linqString + Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + string linqString = System.Linq.Expressions.ExpressionType.Add.ToString(); + mockLogger.AssertLogContains(linqString + ":Hello, World!"); + } + + /// + /// Verify setting the output tag on the parameter causes it to be an output from the perspective of the targets + /// + [TestMethod] + public void BuildTaskDateCodeFactory() + { + string projectFileContents = @" + + + + + + + + CurrentDate = DateTime.Now.ToString(); + + + + + + + + + + "; + + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Current Date and Time:"); + mockLogger.AssertLogDoesntContain("[[]]"); + } + + /// + /// Verify that the vb language works and that creating the execute method also works + /// + [TestMethod] + public void MethodImplmentationVB() + { + string projectFileContents = @" + + + + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("IAMVBTEXT"); + } + + /// + /// Verify that System does not need to be passed in as a extra reference when targeting vb + /// + [TestMethod] + public void BuildTaskSimpleCodeFactoryTestSystemVB() + { + string projectFileContents = @" + + + + + + + + Dim headerRequest As String + headerRequest = System.Net.HttpRequestHeader.Accept.ToString() + Log.LogMessage(MessageImportance.High, headerRequest + Text) + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Accept" + ":Hello, World!"); + } + + /// + /// Verify that System does not need to be passed in as a extra reference when targeting c# + /// + [TestMethod] + public void BuildTaskSimpleCodeFactoryTestSystemCS() + { + string projectFileContents = @" + + + + + + + + string headerRequest = System.Net.HttpRequestHeader.Accept.ToString(); + Log.LogMessage(MessageImportance.High, headerRequest + Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Accept" + ":Hello, World!"); + } + + /// + /// Make sure we can pass in extra references than the automatic ones. For example the c# compiler does not pass in + /// system.dll. So lets test that case + /// + [TestMethod] + public void BuildTaskSimpleCodeFactoryTestExtraReferenceCS() + { + string netFrameworkDirectory = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45); + if (netFrameworkDirectory == null) + { + // "CouldNotFindRequiredTestDirectory" + return; + } + + string systemNETLocation = Path.Combine(netFrameworkDirectory, "System.Net.dll"); + + if (!File.Exists(systemNETLocation)) + { + // "CouldNotFindRequiredTestFile" + return; + } + + string projectFileContents = @" + + + + + + + + + + string netString = System.Net.HttpStatusCode.OK.ToString(); + Log.LogMessage(MessageImportance.High, netString + Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("OK" + ":Hello, World!"); + } + + /// + /// jscript .net works + /// + [TestMethod] + public void MethodImplementationJScriptNet() + { + if (!CodeDomProvider.IsDefinedLanguage("js")) + { + // "JScript .net Is not installed on the test machine this test cannot run" + return; + } + + string projectFileContents = @" + + + + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("IAMJSTEXT"); + } + + /// + /// Verify we can set a code type of Method which expects us to override the execute method entirely. + /// + [TestMethod] + public void MethodImplementation() + { + string projectFileContents = @" + + + + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("IAMTEXT"); + } + + /// + /// Verify we can set the type to Class and this expects an entire class to be entered into the code tag + /// + [TestMethod] + public void ClassImplementationTest() + { + string projectFileContents = @" + + + + + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("MyName=MyValue"); + } + + /// + /// Verify we can set the type to Class and this expects an entire class to be entered into the code tag + /// + [TestMethod] + public void ClassImplementationTestDoesNotInheritFromITask() + { + string projectFileContents = @" + + + + + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.NeedsITaskInterface"); + mockLogger.AssertLogContains(unformattedMessage); + } + + + /// + /// Verify we get an error if a the Type attribute is set but it is empty + /// + [TestMethod] + public void MultipleCodeElements() + { + string projectFileContents = @" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.MultipleCodeNodes"); + mockLogger.AssertLogContains(unformattedMessage); + } + + /// + /// Verify we get an error if a the Type attribute is set but it is empty + /// + [TestMethod] + public void ReferenceNestedInCode() + { + string projectFileContents = @" + + + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.InvalidElementLocation"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Reference", "Code")); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Using", "Code")); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Task", "Code")); + } + + /// + /// Verify we get an error if there is an unknown element in the task tag + /// + [TestMethod] + public void UnknownElementInTask() + { + string projectFileContents = @" + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectFailure(projectFileContents, false); + string unformattedMessage = ResourceUtilities.GetResourceString("CodeTaskFactory.InvalidElementLocation"); + mockLogger.AssertLogContains(String.Format(unformattedMessage, "Unknown", "Task")); + } + + /// + /// Verify we can set a source file location and this will be read in and used. + /// + [TestMethod] + public void ClassSourcesTest() + { + string sourceFileContent = @" + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.Build.Utilities; + using Microsoft.Build.Framework; + + namespace Microsoft.Build.NonShippingTasks + { + public class LogNameValue_ClassSourcesTest : Task + { + private string variableName; + private string variableValue; + + + [Required] + public string Name + { + get { return variableName; } + set { variableName = value; } + } + + + public string Value + { + get { return variableValue; } + set { variableValue = value; } + } + + + public override bool Execute() + { + // Set the process environment + Log.LogMessage(""Setting {0}={1}"", this.variableName, this.variableValue); + return true; + } + } + } +"; + + string tempFileDirectory = Path.GetTempPath(); + string tempFileName = Guid.NewGuid().ToString() + ".cs"; + string tempSourceFile = Path.Combine(tempFileDirectory, tempFileName); + File.WriteAllText(tempSourceFile, sourceFileContent); + + try + { + string projectFileContents = @" + + + + + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("MyName=MyValue"); + } + finally + { + if (File.Exists(tempSourceFile)) + { + File.Delete(tempSourceFile); + } + } + } + } +} + diff --git a/src/XMakeTasks/UnitTests/ComReferenceWalker_Tests.cs b/src/XMakeTasks/UnitTests/ComReferenceWalker_Tests.cs new file mode 100644 index 00000000000..e02536951e0 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ComReferenceWalker_Tests.cs @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Tasks; +using System.Runtime.InteropServices.ComTypes; + +using Marshal = System.Runtime.InteropServices.Marshal; +using COMException = System.Runtime.InteropServices.COMException; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ComReferenceWalker_Tests + { + static private int MockReleaseComObject(object o) + { + return 0; + } + + private void AssertDependenciesContainTypeLib(TYPELIBATTR[] dependencies, MockTypeLib typeLib, bool contains) + { + AssertDependenciesContainTypeLib("", dependencies, typeLib, contains); + } + + private void AssertDependenciesContainTypeLib(string message, TYPELIBATTR[] dependencies, MockTypeLib typeLib, bool contains) + { + bool dependencyExists = false; + + foreach (TYPELIBATTR attr in dependencies) + { + if (attr.guid == typeLib.Attributes.guid) + { + dependencyExists = true; + break; + } + } + + Assert.AreEqual(contains, dependencyExists, message); + } + + [TestMethod] + public void WalkTypeInfosInEmptyLibrary() + { + MockTypeLib typeLib = new MockTypeLib(); + + ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); + walker.AnalyzeTypeLibrary(typeLib); + Assert.AreEqual(0, walker.GetDependencies().GetLength(0)); + + typeLib.AssertAllHandlesReleased(); + } + + private void CreateTwoTypeLibs(out MockTypeLib mainTypeLib, out MockTypeLib dependencyTypeLib) + { + mainTypeLib = new MockTypeLib(); + mainTypeLib.AddTypeInfo(new MockTypeInfo()); + + dependencyTypeLib = new MockTypeLib(); + dependencyTypeLib.AddTypeInfo(new MockTypeInfo()); + } + + private TYPELIBATTR[] RunDependencyWalker(MockTypeLib mainTypeLib, MockTypeLib dependencyTypeLib, bool dependencyShouldBePresent) + { + ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); + walker.AnalyzeTypeLibrary(mainTypeLib); + + TYPELIBATTR[] dependencies = walker.GetDependencies(); + + // types from the main type library should be in the dependency list + AssertDependenciesContainTypeLib(dependencies, mainTypeLib, true); + + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib, dependencyShouldBePresent); + + mainTypeLib.AssertAllHandlesReleased(); + dependencyTypeLib.AssertAllHandlesReleased(); + + return dependencies; + } + + /// + /// A type in the main type library implements an interface from a dependent type library + /// + [TestMethod] + public void ImplementedInterfaces() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].ImplementsInterface(dependencyTypeLib.ContainedTypeInfos[0]); + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, true); + } + + [TestMethod] + public void DefinedVariableUDT() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib.ContainedTypeInfos[0]); + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, true); + } + + [TestMethod] + public void DefinedVariableUDTArray() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(new ArrayCompositeTypeInfo(dependencyTypeLib.ContainedTypeInfos[0])); + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, true); + } + + [TestMethod] + public void DefinedVariableUDTPtr() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(new PtrCompositeTypeInfo(dependencyTypeLib.ContainedTypeInfos[0])); + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, true); + } + + [TestMethod] + public void ThereAndBackAgain() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(new PtrCompositeTypeInfo(dependencyTypeLib.ContainedTypeInfos[0])); + dependencyTypeLib.ContainedTypeInfos[0].ImplementsInterface(mainTypeLib.ContainedTypeInfos[0]); + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, true); + } + + [TestMethod] + public void ComplexComposition() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].DefinesVariable( + new ArrayCompositeTypeInfo(new ArrayCompositeTypeInfo(new PtrCompositeTypeInfo( + new PtrCompositeTypeInfo(new ArrayCompositeTypeInfo(new PtrCompositeTypeInfo(dependencyTypeLib.ContainedTypeInfos[0]))))))); + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, true); + } + + [TestMethod] + public void DefinedFunction() + { + MockTypeLib mainTypeLib, dependencyTypeLib1, dependencyTypeLib2, dependencyTypeLib3; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib1); + CreateTwoTypeLibs(out dependencyTypeLib2, out dependencyTypeLib3); + + mainTypeLib.ContainedTypeInfos[0].DefinesFunction( + new MockTypeInfo[] { dependencyTypeLib1.ContainedTypeInfos[0], dependencyTypeLib2.ContainedTypeInfos[0] }, + dependencyTypeLib3.ContainedTypeInfos[0]); + + TYPELIBATTR[] dependencies = RunDependencyWalker(mainTypeLib, dependencyTypeLib1, true); + + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib2, true); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib3, true); + + dependencyTypeLib2.AssertAllHandlesReleased(); + dependencyTypeLib3.AssertAllHandlesReleased(); + } + + [TestMethod] + public void IgnoreKnownOleTypes() + { + MockTypeLib mainTypeLib = new MockTypeLib(); + mainTypeLib.AddTypeInfo(new MockTypeInfo()); + + MockTypeLib oleTypeLib = new MockTypeLib(); + oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IUnknown)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IDispatch)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IDispatchEx)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_IEnumVariant)); + oleTypeLib.AddTypeInfo(new MockTypeInfo(NativeMethods.IID_ITypeInfo)); + + // We don't check for this type in the ComDependencyWalker, so it doesn't get counted as a known OLE type. + // It's too late in the Dev10 cycle to add it to shipping code without phenomenally good reason, but we should + // re-examine this in Dev11. + // oleTypeLib.AddTypeInfo(new MockTypeInfo(TYPEKIND.TKIND_ENUM)); + + foreach (MockTypeInfo typeInfo in oleTypeLib.ContainedTypeInfos) + { + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(typeInfo); + } + + RunDependencyWalker(mainTypeLib, oleTypeLib, false); + } + + [TestMethod] + public void IgnoreGuidType() + { + MockTypeLib mainTypeLib = new MockTypeLib(); + mainTypeLib.AddTypeInfo(new MockTypeInfo()); + + MockTypeLib oleTypeLib = new MockTypeLib(NativeMethods.IID_StdOle); + oleTypeLib.AddTypeInfo(new MockTypeInfo()); + oleTypeLib.ContainedTypeInfos[0].TypeName = "GUID"; + + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(oleTypeLib.ContainedTypeInfos[0]); + + RunDependencyWalker(mainTypeLib, oleTypeLib, false); + } + + [TestMethod] + public void IgnoreNetExportedTypeLibs() + { + MockTypeLib mainTypeLib, dependencyTypeLib; + CreateTwoTypeLibs(out mainTypeLib, out dependencyTypeLib); + + mainTypeLib.ContainedTypeInfos[0].DefinesFunction( + new MockTypeInfo[] { dependencyTypeLib.ContainedTypeInfos[0] }, dependencyTypeLib.ContainedTypeInfos[0]); + dependencyTypeLib.ExportedFromComPlus = "1"; + + RunDependencyWalker(mainTypeLib, dependencyTypeLib, false); + } + + /// + /// The main type lib is broken... don't expect any results, but make sure we don't throw. + /// + [TestMethod] + public void FaultInjectionMainLib() + { + // The primary test here is that we don't throw, which can't be explicitly expressed in NUnit... + // other asserts are secondary + foreach (MockTypeLibrariesFailurePoints failurePoint in Enum.GetValues(typeof(MockTypeLibrariesFailurePoints))) + { + MockTypeLib mainTypeLib = new MockTypeLib(); + mainTypeLib.AddTypeInfo(new MockTypeInfo()); + + // Make it the StdOle lib to exercise the ITypeInfo.GetDocumentation failure point + MockTypeLib dependencyTypeLib = new MockTypeLib(NativeMethods.IID_StdOle); + dependencyTypeLib.AddTypeInfo(new MockTypeInfo()); + + COMException failureException = new COMException("unhandled exception in " + failurePoint.ToString()); + mainTypeLib.InjectFailure(failurePoint, failureException); + dependencyTypeLib.InjectFailure(failurePoint, failureException); + + mainTypeLib.ContainedTypeInfos[0].ImplementsInterface(dependencyTypeLib.ContainedTypeInfos[0]); + mainTypeLib.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib.ContainedTypeInfos[0]); + mainTypeLib.ContainedTypeInfos[0].DefinesFunction( + new MockTypeInfo[] { dependencyTypeLib.ContainedTypeInfos[0] }, dependencyTypeLib.ContainedTypeInfos[0]); + + ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); + walker.AnalyzeTypeLibrary(mainTypeLib); + + Assert.AreEqual(1, walker.EncounteredProblems.Count, "Test failed for failure point " + failurePoint.ToString()); + Assert.AreEqual(failureException, walker.EncounteredProblems[0], "Test failed for failure point " + failurePoint.ToString()); + + mainTypeLib.AssertAllHandlesReleased(); + dependencyTypeLib.AssertAllHandlesReleased(); + } + } + + private static void CreateFaultInjectionTypeLibs(MockTypeLibrariesFailurePoints failurePoint, out MockTypeLib mainTypeLib, + out MockTypeLib dependencyTypeLibGood1, out MockTypeLib dependencyTypeLibBad1, + out MockTypeLib dependencyTypeLibGood2, out MockTypeLib dependencyTypeLibBad2) + { + mainTypeLib = new MockTypeLib(); + mainTypeLib.AddTypeInfo(new MockTypeInfo()); + mainTypeLib.AddTypeInfo(new MockTypeInfo()); + + dependencyTypeLibGood1 = new MockTypeLib(); + dependencyTypeLibGood1.AddTypeInfo(new MockTypeInfo()); + + // Make it the StdOle lib to exercise the ITypeInfo.GetDocumentation failure point + dependencyTypeLibBad1 = new MockTypeLib(NativeMethods.IID_StdOle); + dependencyTypeLibBad1.AddTypeInfo(new MockTypeInfo()); + + dependencyTypeLibGood2 = new MockTypeLib(); + dependencyTypeLibGood2.AddTypeInfo(new MockTypeInfo()); + + // Make it the StdOle lib to exercise the ITypeInfo.GetDocumentation failure point + dependencyTypeLibBad2 = new MockTypeLib(NativeMethods.IID_StdOle); + dependencyTypeLibBad2.AddTypeInfo(new MockTypeInfo()); + + COMException failureException = new COMException("unhandled exception in " + failurePoint.ToString()); + + dependencyTypeLibBad1.InjectFailure(failurePoint, failureException); + dependencyTypeLibBad2.InjectFailure(failurePoint, failureException); + } + + private void RunDependencyWalkerFaultInjection(MockTypeLibrariesFailurePoints failurePoint, MockTypeLib mainTypeLib, MockTypeLib dependencyTypeLibGood1, MockTypeLib dependencyTypeLibBad1, MockTypeLib dependencyTypeLibGood2, MockTypeLib dependencyTypeLibBad2) + { + ComDependencyWalker walker = new ComDependencyWalker(new MarshalReleaseComObject(MockReleaseComObject)); + walker.AnalyzeTypeLibrary(mainTypeLib); + + // Did the current failure point get hit for this test? If not then no point in checking anything + // The previous test (FaultInjectionMainLib) ensures that all defined failure points actually + // cause some sort of trouble + if (walker.EncounteredProblems.Count > 0) + { + TYPELIBATTR[] dependencies = walker.GetDependencies(); + AssertDependenciesContainTypeLib("Test failed for failure point " + failurePoint.ToString(), + dependencies, mainTypeLib, true); + AssertDependenciesContainTypeLib("Test failed for failure point " + failurePoint.ToString(), + dependencies, dependencyTypeLibGood1, true); + AssertDependenciesContainTypeLib("Test failed for failure point " + failurePoint.ToString(), + dependencies, dependencyTypeLibGood2, true); + AssertDependenciesContainTypeLib("Test failed for failure point " + failurePoint.ToString(), + dependencies, dependencyTypeLibBad1, false); + AssertDependenciesContainTypeLib("Test failed for failure point " + failurePoint.ToString(), + dependencies, dependencyTypeLibBad2, false); + } + + mainTypeLib.AssertAllHandlesReleased(); + dependencyTypeLibGood1.AssertAllHandlesReleased(); + dependencyTypeLibGood2.AssertAllHandlesReleased(); + dependencyTypeLibBad1.AssertAllHandlesReleased(); + dependencyTypeLibBad2.AssertAllHandlesReleased(); + } + + [TestMethod] + public void FullDependenciesWithIncrementalAnalysis() + { + MockTypeLib mainTypeLib1, mainTypeLib2, mainTypeLib3, dependencyTypeLib1, dependencyTypeLib2, dependencyTypeLib3; + CreateTwoTypeLibs(out mainTypeLib1, out dependencyTypeLib1); + CreateTwoTypeLibs(out mainTypeLib2, out dependencyTypeLib2); + CreateTwoTypeLibs(out mainTypeLib3, out dependencyTypeLib3); + + mainTypeLib1.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib1.ContainedTypeInfos[0]); + + mainTypeLib2.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib1.ContainedTypeInfos[0]); + mainTypeLib2.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib2.ContainedTypeInfos[0]); + + mainTypeLib3.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib1.ContainedTypeInfos[0]); + mainTypeLib3.ContainedTypeInfos[0].DefinesVariable(dependencyTypeLib3.ContainedTypeInfos[0]); + + ComDependencyWalker walker = new ComDependencyWalker(MockReleaseComObject); + + walker.AnalyzeTypeLibrary(mainTypeLib1); + TYPELIBATTR[] dependencies = walker.GetDependencies(); + ICollection analyzedTypes = walker.GetAnalyzedTypeNames(); + + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib1, true); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib2, false); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib3, false); + Assert.AreEqual(2, analyzedTypes.Count); + + walker.ClearDependencyList(); + walker.AnalyzeTypeLibrary(mainTypeLib2); + dependencies = walker.GetDependencies(); + analyzedTypes = walker.GetAnalyzedTypeNames(); + + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib1, true); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib2, true); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib3, false); + Assert.AreEqual(4, analyzedTypes.Count); + + walker.ClearDependencyList(); + walker.AnalyzeTypeLibrary(mainTypeLib3); + dependencies = walker.GetDependencies(); + analyzedTypes = walker.GetAnalyzedTypeNames(); + + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib1, true); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib2, false); + AssertDependenciesContainTypeLib(dependencies, dependencyTypeLib3, true); + Assert.AreEqual(6, analyzedTypes.Count); + } + } +} diff --git a/src/XMakeTasks/UnitTests/ComReference_Tests.cs b/src/XMakeTasks/UnitTests/ComReference_Tests.cs new file mode 100644 index 00000000000..70a685604ed --- /dev/null +++ b/src/XMakeTasks/UnitTests/ComReference_Tests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ComReference_Tests + { + private static Dictionary s_existingFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private static Dictionary ExistingFilesDictionary + { + get + { + if (s_existingFiles.Count != 0) + return s_existingFiles; + + s_existingFiles.Add(@"C:\test\typelib1.dll", string.Empty); + s_existingFiles.Add(@"C:\test\typelib2\2.dll", string.Empty); + s_existingFiles.Add(@"C:\test\typelib3.\3dll", string.Empty); + s_existingFiles.Add(@"C:\test\typelib4.dll", string.Empty); + s_existingFiles.Add(@"C:\test\typelib5.dll", string.Empty); + + return s_existingFiles; + } + } + + private static bool FileExistsMock(string filepath) + { + return ExistingFilesDictionary.ContainsKey(filepath); + } + + [TestMethod] + public void TestStripTypeLibNumber() + { + Assert.AreEqual(@"C:\test\typelib1.dll", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib1.dll", new FileExists(FileExistsMock))); + Assert.AreEqual(@"C:\test\typelib2\2.dll", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib2\2.dll", new FileExists(FileExistsMock))); + Assert.AreEqual(@"C:\test\typelib3.\3dll", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib3.\3dll", new FileExists(FileExistsMock))); + Assert.AreEqual(@"C:\test\typelib4.dll", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib4.dll\4", new FileExists(FileExistsMock))); + Assert.AreEqual(@"C:\test\typelib5.dll", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib5.dll\555", new FileExists(FileExistsMock))); + Assert.AreEqual(@"", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib6.dll", new FileExists(FileExistsMock))); + Assert.AreEqual(@"", ComReference.StripTypeLibNumberFromPath(@"C:\test\typelib7.dll\7", new FileExists(FileExistsMock))); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CombinePath_Tests.cs b/src/XMakeTasks/UnitTests/CombinePath_Tests.cs new file mode 100644 index 00000000000..5fa2f5c9b9d --- /dev/null +++ b/src/XMakeTasks/UnitTests/CombinePath_Tests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CombinePath_Tests + { + /// + /// Base path is relative. Paths are relative. + /// + [TestMethod] + public void RelativeRelative1() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"abc\def"; + t.Paths = new ITaskItem[] { new TaskItem(@"ghi.txt"), new TaskItem(@"jkl\mno.txt") }; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + abc\def\ghi.txt + abc\def\jkl\mno.txt + ", t.CombinedPaths, true); + } + + /// + /// Base path is relative. Paths are absolute. + /// + [TestMethod] + public void RelativeAbsolute1() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"abc\def"; + t.Paths = new ITaskItem[] { new TaskItem(@"c:\ghi.txt"), new TaskItem(@"d:\jkl\mno.txt"), new TaskItem(@"\\myserver\myshare") }; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + c:\ghi.txt + d:\jkl\mno.txt + \\myserver\myshare + ", t.CombinedPaths, true); + } + + /// + /// Base path is absolute. Paths are relative. + /// + [TestMethod] + public void AbsoluteRelative1() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"c:\abc\def"; + t.Paths = new ITaskItem[] { new TaskItem(@"\ghi\jkl.txt"), new TaskItem(@"mno\qrs.txt") }; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch( + @"\ghi\jkl.txt" + "\r\n" + // I think this is a bug in Path.Combine. It should have been "c:\ghi\jkl.txt". + @"c:\abc\def\mno\qrs.txt", + t.CombinedPaths, true); + } + + /// + /// Base path is absolute. Paths are absolute. + /// + [TestMethod] + public void AbsoluteAbsolute1() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"\\fileserver\public"; + t.Paths = new ITaskItem[] { new TaskItem(@"c:\ghi.txt"), new TaskItem(@"d:\jkl\mno.txt"), new TaskItem(@"\\myserver\myshare") }; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + c:\ghi.txt + d:\jkl\mno.txt + \\myserver\myshare + ", t.CombinedPaths, true); + } + + /// + /// All item metadata from the paths should be preserved when producing the output items. + /// + [TestMethod] + public void MetadataPreserved() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"c:\abc\def\"; + t.Paths = new ITaskItem[] { new TaskItem(@"jkl\mno.txt") }; + t.Paths[0].SetMetadata("Culture", "english"); + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + c:\abc\def\jkl\mno.txt : Culture=english + ", t.CombinedPaths, true); + } + + /// + /// No base path passed in should be treated as a blank base path, which means that + /// the original paths are returned untouched. + /// + [TestMethod] + public void NoBasePath() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.Paths = new ITaskItem[] { new TaskItem(@"jkl\mno.txt"), new TaskItem(@"c:\abc\def\ghi.txt") }; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + jkl\mno.txt + c:\abc\def\ghi.txt + ", t.CombinedPaths, true); + } + + /// + /// Passing in an array of zero paths. Task should succeed and return zero paths. + /// + [TestMethod] + public void NoPaths() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"c:\abc\def"; + t.Paths = new ITaskItem[0]; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + ", t.CombinedPaths, true); + } + + /// + /// Passing in a (blank) path. Task should simply return the base path. + /// + [TestMethod] + public void BlankPath() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(); + + t.BasePath = @"c:\abc\def"; + t.Paths = new ITaskItem[] { new TaskItem("") }; + Assert.IsTrue(t.Execute(), "success"); + + ObjectModelHelpers.AssertItemsMatch(@" + c:\abc\def + ", t.CombinedPaths, true); + } + + /// + /// Specified paths contain invalid characters. Task should continue processing remaining items. + /// + [TestMethod] + public void InvalidPath() + { + CombinePath t = new CombinePath(); + t.BuildEngine = new MockEngine(true); + + t.BasePath = @"c:\abc\def"; + t.Paths = new ITaskItem[] { new TaskItem("ghi.txt"), new TaskItem("|.txt"), new TaskItem("jkl.txt") }; + Assert.IsFalse(t.Execute(), "should have failed"); + ((MockEngine)t.BuildEngine).AssertLogContains("MSB3095"); + + ObjectModelHelpers.AssertItemsMatch(@" + c:\abc\def\ghi.txt + c:\abc\def\jkl.txt + ", t.CombinedPaths, true); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CommandLineBuilderExtension_Tests.cs b/src/XMakeTasks/UnitTests/CommandLineBuilderExtension_Tests.cs new file mode 100644 index 00000000000..56ae12ab0aa --- /dev/null +++ b/src/XMakeTasks/UnitTests/CommandLineBuilderExtension_Tests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CommandLineBuilderExtensionTest + { + /* + * Method: AppendItemWithInvalidBooleanAttribute + * + * When appending an ITaskItem[] where some of the flags are 'bool', it's possible that + * the boolean flag has a string value that cannot be converted to a boolean. In this + * case we expect an exception. + */ + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AppendItemWithInvalidBooleanAttribute() + { + // Construct the task item. + TaskItem i = new TaskItem(); + i.ItemSpec = "MyResource.bmp"; + i.SetMetadata("Name", "Kenny"); + i.SetMetadata("Private", "Yes"); // This is our flag. + + CommandLineBuilderExtension c = new CommandLineBuilderExtension(); + + // Validate that a legitimate bool works first. + try + { + c.AppendSwitchIfNotNull + ( + "/myswitch:", + new ITaskItem[] { i }, + new string[] { "Name", "Private" }, + new bool[] { false, true } + ); + Assert.AreEqual(@"/myswitch:MyResource.bmp,Kenny,Private", c.ToString()); + } + catch (ArgumentException e) + { + Assert.Fail("Got an unexpected exception:" + e.Message); + } + + // Now try a bogus boolean. + i.SetMetadata("Private", "Maybe"); // This is our flag. + c.AppendSwitchIfNotNull + ( + "/myswitch:", + new ITaskItem[] { i }, + new string[] { "Name", "Private" }, + new bool[] { false, true } + ); // <-- Expect an ArgumentException here. + } + + /// + /// When appending an ITaskItem[] where some of the optional attributes are + /// present, but others aren't. We can't be emitted attributes in the wrong + /// order on the command-line, so we skip all subsequent attributes as soon + /// as we find one missing. + /// + [TestMethod] + public void AppendItemWithMissingAttribute() + { + // Construct the task items. + TaskItem i = new TaskItem(); + i.ItemSpec = "MySoundEffect.wav"; + i.SetMetadata("Name", "Kenny"); + i.SetMetadata("Access", "Private"); + + TaskItem j = new TaskItem(); + j.ItemSpec = "MySplashScreen.bmp"; + j.SetMetadata("Name", "Cartman"); + j.SetMetadata("HintPath", @"c:\foo"); + j.SetMetadata("Access", "Public"); + + CommandLineBuilderExtension c = new CommandLineBuilderExtension(); + + c.AppendSwitchIfNotNull + ( + "/myswitch:", + new ITaskItem[] { i, j }, + new string[] { "Name", "HintPath", "Access" }, + null + ); + Assert.AreEqual(@"/myswitch:MySoundEffect.wav,Kenny /myswitch:MySplashScreen.bmp,Cartman,c:\foo,Public", c.ToString()); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CommandLineGenerator_Tests.cs b/src/XMakeTasks/UnitTests/CommandLineGenerator_Tests.cs new file mode 100644 index 00000000000..bb47a31f06a --- /dev/null +++ b/src/XMakeTasks/UnitTests/CommandLineGenerator_Tests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Windows.Markup; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.XamlTypes; +using Microsoft.Build.Tasks.Xaml; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CommandLineGenerator_Tests + { + private const string testXamlFile = @" + + + Memory Reporting Tool + + + + + General + + + + + + + + + + + + + + + + + + + + + + Debug Information Format + + + Specifies the type of debugging information generated by the compiler. You must also change linker settings appropriately to match. (/Z7, Zd, /Zi, /ZI) + + + + C7 compatible + + + Select the type of debugging information created for your program and whether this information is kept in object (.obj) files or in a program database (PDB). + + + + + Program Database + + + Produces a program database (PDB) that contains type information and symbolic debugging information for use with the debugger. The symbolic debugging information includes the names and types of variables, as well as functions and line numbers. + + + + + Program Database for Edit And Continue + + + Produces a program database, as described above, in a format that supports the Edit and Continue feature. + + + + + + Empty Enum + + + Specifies the type of debugging information generated by the compiler. You must also change linker settings appropriately to match. (/Z7, Zd, /Zi, /ZI) + + + + Empty + + + An empty enum switch + + + + + "; + + /// + /// Tests a command line generated from all of the specified switch values. + /// + [TestMethod] + public void BasicCommandLine() + { + CommandLineGenerator generator = CreateGenerator(); + string commandLine = generator.GenerateCommandLine(); + Assert.AreEqual("/P /SSubstituteThis!_postfix SubstituteThis!AsWell /AtEndSubstitute\\ /I42_postfix /Xone /Xtwo /Xthree a.cs b.cs /Z7", commandLine); + } + + /// + /// Tests a command line generated from a specific template + /// + [TestMethod] + public void TemplatedCommandLine() + { + CommandLineGenerator generator = CreateGenerator(); + generator.CommandLineTemplate = "[Sources] [Program]"; + string commandLine = generator.GenerateCommandLine(); + Assert.AreEqual("a.cs b.cs /P", commandLine); + } + + /// + /// Tests a command line generated from a specific template is not case sensitive on the parameter names. + /// + [TestMethod] + public void TemplateParametersAreCaseInsensitive() + { + CommandLineGenerator generator = CreateGenerator(); + generator.CommandLineTemplate = "[sources]"; + string commandLine = generator.GenerateCommandLine(); + Assert.AreEqual("a.cs b.cs", commandLine); + } + + private CommandLineGenerator CreateGenerator() + { + Rule rule = XamlReader.Parse(testXamlFile) as Rule; + + Dictionary switchValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + switchValues["Program"] = true; + switchValues["Debug"] = false; + switchValues["Subst"] = "SubstituteThis!"; + switchValues["Subst2"] = "SubstituteThis!AsWell"; + switchValues["Subst3"] = "Substitute\\"; + switchValues["SubstInt"] = (int)42; + switchValues["Strings"] = new string[] { "one", "two", "three" }; + ITaskItem[] sources = new ITaskItem[] + { + new TaskItem("a.cs"), + new TaskItem("b.cs") + }; + + switchValues["Sources"] = sources; + switchValues["DebugInformationFormat"] = "OldStyle"; + switchValues["EmptyTest"] = "Empty"; + + CommandLineGenerator generator = new CommandLineGenerator(rule, switchValues); + return generator; + } + + private class TaskItem : ITaskItem + { + public TaskItem(string itemSpec) + { + ItemSpec = itemSpec; + } + + #region ITaskItem Members + + public string ItemSpec + { + get; + set; + } + + public System.Collections.ICollection MetadataNames + { + get { throw new NotImplementedException(); } + } + + public int MetadataCount + { + get { throw new NotImplementedException(); } + } + + public string GetMetadata(string metadataName) + { + throw new NotImplementedException(); + } + + public void SetMetadata(string metadataName, string metadataValue) + { + throw new NotImplementedException(); + } + + public void RemoveMetadata(string metadataName) + { + throw new NotImplementedException(); + } + + public void CopyMetadataTo(ITaskItem destinationItem) + { + throw new NotImplementedException(); + } + + public System.Collections.IDictionary CloneCustomMetadata() + { + throw new NotImplementedException(); + } + + #endregion + } + } +} diff --git a/src/XMakeTasks/UnitTests/CommandLine_Support.cs b/src/XMakeTasks/UnitTests/CommandLine_Support.cs new file mode 100644 index 00000000000..96ca175df04 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CommandLine_Support.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Text.RegularExpressions; +using System.Globalization; +using System.Text; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: CscTests + * + * Test the Csc task in various ways. + * + */ + sealed internal class CommandLine + { + /// + /// Invokes the ValidateParameters on the given ToolTask instance. We need to use reflection since + /// ValidateParameters is inaccessible to Tasks unit tests. + /// + /// + static internal bool CallValidateParameters(ToolTask task) + { + MethodInfo validateMethod = typeof(ToolTask).GetMethod("ValidateParameters", BindingFlags.Instance | BindingFlags.NonPublic); + return (bool)validateMethod.Invoke(task, null); + } + + /* + * Method: ParseCommandLine + * + * Simulates the parsing of a command line, taking into account quoting. + * + */ + private static string[] Parse(string cl) + { + int emptySplits = 0; + string[] pieces = (string[])QuotingUtilities.SplitUnquoted(cl, int.MaxValue, false, true, out emptySplits, ' ', '\n').ToArray(typeof(string)); +#if NEVER + foreach(string s in pieces) + { + Console.WriteLine("Found = [{0}]", s); + } +#endif + return pieces; + } + + /* + * Method: ValidateHasParameter + * + * Validates that the the given ToolTaskExtension's command line contains the indicated + * parameter. Returns the index of the parameter that matched. + * + */ + internal static int ValidateHasParameter(ToolTaskExtension t, string parameter) + { + return ValidateHasParameter(t, parameter, true /* use response file */); + } + + /* + * Method: ValidateHasParameter + * + * Validates that the the given ToolTaskExtension's command line contains the indicated + * parameter. Returns the index of the parameter that matched. + * + */ + internal static int ValidateHasParameter(ToolTaskExtension t, string parameter, bool useResponseFile) + { + CommandLineBuilderExtension b = new CommandLineBuilderExtension(); + + if (useResponseFile) + t.AddResponseFileCommands(b); + else + t.AddCommandLineCommands(b); + + string cl = b.ToString(); + string msg = String.Format("Command-line = [{0}]\r\n", cl); + msg += String.Format(" Searching for [{0}]\r\n", parameter); + + string[] pieces = Parse(cl); + + int i = 0; + foreach (string s in pieces) + { + msg += String.Format(" Parm = [{0}]\r\n", s); + if (s == parameter) + { + return i; + } + + i++; + } + + msg += "Not found!\r\n"; + Console.WriteLine(msg); + Assert.Fail(msg); // Could not find the parameter. + + return 0; + } + + /// + /// Validates that the the given ToolTaskExtension's command line does not contain + /// any parameter starting with the given string. + /// + /// task to get the command line from + /// string to look for in the command line + /// if true, use the response file cmd line, else use regular cmd line + internal static void ValidateNoParameterStartsWith(ToolTaskExtension t, string startsWith, bool useResponseFile) + { + ValidateNoParameterStartsWith(t, startsWith, "", useResponseFile); + } + + /// + /// Validates that the the given ToolTaskExtension's command line does not contain + /// any parameter starting with the given string. + /// + /// task to get the command line from + /// string to look for in the command line + internal static void ValidateNoParameterStartsWith(ToolTaskExtension t, string startsWith) + { + ValidateNoParameterStartsWith(t, startsWith, true); + } + + /// + /// Validates that the the given ToolTaskExtension's command line does not contain + /// any parameter starting with the given string. + /// + /// task to get the command line from + /// string to look for in the command line + /// only find strings that don't contain this argument + internal static void ValidateNoParameterStartsWith(ToolTaskExtension t, string startsWith, string except) + { + ValidateNoParameterStartsWith(t, startsWith, except, true); + } + + /// + /// Validates that the the given ToolTaskExtension's command line does not contain + /// any parameter starting with the given string. + /// + /// task to get the command line from + /// string to look for in the command line + /// only find strings that don't contain this argument + /// if true, use the response file cmd line, else use regular cmd line + internal static void ValidateNoParameterStartsWith( + ToolTaskExtension t, + string startsWith, + string except, + bool useResponseFile + ) + { + CommandLineBuilderExtension b = new CommandLineBuilderExtension(); + + if (useResponseFile) + t.AddResponseFileCommands(b); + else + t.AddCommandLineCommands(b); + + string cl = b.ToString(); + + string msg = String.Format("Command-line = [{0}]\r\n", cl); + msg += String.Format(" Searching for something that starts with [{0}]\r\n", startsWith); + msg += String.Format(" that doesn't contain [{0}]\r\n", except); + + string[] pieces = Parse(cl); + + foreach (string s in pieces) + { + msg += String.Format(" Parm = [{0}]\r\n", s); + + if (s.Length < startsWith.Length) + { + // Skip anything shorter than the compare string. + continue; + } + if (String.Compare(s.Substring(0, startsWith.Length), startsWith, StringComparison.OrdinalIgnoreCase) == 0) + { + // If this doesn't match the 'except' then this is an error. + if (String.Compare(s, except, StringComparison.Ordinal) != 0) + { + msg += String.Format(" Found something!\r\n"); + Console.WriteLine(msg); + Assert.Fail(msg); // Found the startsWith but shouldn't have. + return; + } + } + } + } + + /// + /// Checks if command line generated by task contains given string. + /// This is used to verify that the stuff that should get quoted actually does. + /// + /// task to get the command line from + /// string to look for in the command line + /// if true, use the response file cmd line, else use regular cmd line + internal static void ValidateContains(ToolTaskExtension t, string lookFor, bool useResponseFile) + { + CommandLineBuilderExtension b = new CommandLineBuilderExtension(); + + if (useResponseFile) + t.AddResponseFileCommands(b); + else + t.AddCommandLineCommands(b); + + string cl = b.ToString(); + string msg = String.Format("Command-line = [{0}]\r\n", cl); + + msg += String.Format(" Searching for [{0}]\r\n", lookFor); + + if (cl.IndexOf(lookFor) == -1) + { + msg += "Not found!\r\n"; + Console.WriteLine(msg); + Assert.Fail(msg); + } + } + + /// + /// Checks if command line generated by task contains given string. + /// This is used to verify that the stuff that should get quoted actually does. + /// + /// task to get the command line from + /// string to look for in the command line + /// if true, use the response file cmd line, else use regular cmd line + internal static void ValidateDoesNotContain(ToolTaskExtension t, string lookFor, bool useResponseFile) + { + CommandLineBuilderExtension b = new CommandLineBuilderExtension(); + + if (useResponseFile) + t.AddResponseFileCommands(b); + else + t.AddCommandLineCommands(b); + + string cl = b.ToString(); + string msg = String.Format("Command-line = [{0}]\r\n", cl); + + msg += String.Format(" Searching for [{0}]\r\n", lookFor); + if (cl.IndexOf(lookFor) != -1) + { + msg += "Found!\r\n"; + Console.WriteLine(msg); + Assert.Fail(msg); + } + } + + /// + /// Checks if command line generated by task matches the given string. + /// + /// task to get the command line from + /// string to look for in the command line + /// if true, use the response file cmd line, else use regular cmd line + internal static void ValidateEquals(ToolTaskExtension t, string lookFor, bool useResponseFile) + { + CommandLineBuilderExtension b = new CommandLineBuilderExtension(); + + if (useResponseFile) + t.AddResponseFileCommands(b); + else + t.AddCommandLineCommands(b); + + string cl = b.ToString(); + string msg; + msg = String.Format("Command-line = [{0}]\r\n", cl); + msg += String.Format("Expected = [{0}]\r\n", lookFor); + + if (cl != lookFor) + { + msg += "Does not match!\r\n"; + Console.WriteLine(msg); + Assert.Fail(msg); + } + } + + internal static string GetCommandLine(ToolTaskExtension t, bool useResponseFile) + { + CommandLineBuilderExtension b = new CommandLineBuilderExtension(); + + if (useResponseFile) + t.AddResponseFileCommands(b); + else + t.AddCommandLineCommands(b); + + return b.ToString(); + } + } +} diff --git a/src/XMakeTasks/UnitTests/ConvertToAbsolutePath_Tests.cs b/src/XMakeTasks/UnitTests/ConvertToAbsolutePath_Tests.cs new file mode 100644 index 00000000000..5619c4eaaa1 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ConvertToAbsolutePath_Tests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ConvertToAbsolutePath_Tests + { + /// + /// Passing in a relative path (expecting an absolute back) + /// + [TestMethod] + public void RelativePath() + { + string fileName = ObjectModelHelpers.CreateFileInTempProjectDirectory("file.temp", "foo"); + FileInfo testFile = new FileInfo(fileName); + + ConvertToAbsolutePath t = new ConvertToAbsolutePath(); + t.BuildEngine = new MockEngine(); + + string currentDirectory = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(ObjectModelHelpers.TempProjectDir); + t.Paths = new ITaskItem[] { new TaskItem(@"file.temp") }; + Assert.IsTrue(t.Execute(), "success"); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + + Assert.AreEqual(1, t.AbsolutePaths.Length); + Assert.AreEqual(testFile.FullName, t.AbsolutePaths[0].ItemSpec); + + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + + /// + /// Passing in a relative path (expecting an absolute back) + /// + [TestMethod] + public void RelativePathWithEscaping() + { + string fileName = ObjectModelHelpers.CreateFileInTempProjectDirectory("file%3A.temp", "foo"); + FileInfo testFile = new FileInfo(fileName); + + ConvertToAbsolutePath t = new ConvertToAbsolutePath(); + t.BuildEngine = new MockEngine(); + + string currentDirectory = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(ObjectModelHelpers.TempProjectDir); + t.Paths = new ITaskItem[] { new TaskItem(@"file%253A.temp") }; + Assert.IsTrue(t.Execute(), "success"); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + + Assert.AreEqual(1, t.AbsolutePaths.Length); + Assert.AreEqual(testFile.FullName, t.AbsolutePaths[0].ItemSpec); + + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + + /// + /// Passing in a absolute path (expecting an absolute back) + /// + [TestMethod] + public void AbsolutePath() + { + string fileName = ObjectModelHelpers.CreateFileInTempProjectDirectory("file.temp", "foo"); + FileInfo testFile = new FileInfo(fileName); + + ConvertToAbsolutePath t = new ConvertToAbsolutePath(); + t.BuildEngine = new MockEngine(); + + string currentDirectory = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(ObjectModelHelpers.TempProjectDir); + t.Paths = new ITaskItem[] { new TaskItem(fileName) }; + Assert.IsTrue(t.Execute(), "success"); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + + Assert.AreEqual(1, t.AbsolutePaths.Length); + Assert.AreEqual(testFile.FullName, t.AbsolutePaths[0].ItemSpec); + + ObjectModelHelpers.DeleteTempProjectDirectory(); + } + + /// + /// Passing in a relative path that doesn't exist (expecting sucess) + /// + [TestMethod] + public void FakeFile() + { + ConvertToAbsolutePath t = new ConvertToAbsolutePath(); + t.BuildEngine = new MockEngine(); + + t.Paths = new ITaskItem[] { new TaskItem("RandomFileThatDoesntExist.txt") }; + + Assert.IsTrue(t.Execute(), "success"); + + Assert.AreEqual(1, t.AbsolutePaths.Length); + } + } +} diff --git a/src/XMakeTasks/UnitTests/Copy_Tests.cs b/src/XMakeTasks/UnitTests/Copy_Tests.cs new file mode 100644 index 00000000000..80b1b7d53ad --- /dev/null +++ b/src/XMakeTasks/UnitTests/Copy_Tests.cs @@ -0,0 +1,2150 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Security.AccessControl; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public abstract class Copy_Tests + { + public bool useHardLinks = false; + + /// + /// Temporarily save off the value of MSBUILDALWAYSOVERWRITEREADONLYFILES, so that we can run + /// the tests isolated from the current state of the environment, but put it back how it belongs + /// once we're done. + /// + private string _alwaysOverwriteReadOnlyFiles = null; + + /// + /// Temporarily save off the value of MSBUILDALWAYSRETRY, so that we can run + /// the tests isolated from the current state of the environment, but put it back how it belongs + /// once we're done. + /// + private string _alwaysRetry = null; + + /// + /// There are a couple of environment variables that can affect the operation of the Copy + /// task. Make sure none of them are set. + /// + [TestInitialize] + public void Setup() + { + _alwaysOverwriteReadOnlyFiles = Environment.GetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES"); + _alwaysRetry = Environment.GetEnvironmentVariable("MSBUILDALWAYSRETRY"); + + Environment.SetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES", String.Empty); + Environment.SetEnvironmentVariable("MSBUILDALWAYSRETRY", String.Empty); + + Copy.RefreshInternalEnvironmentValues(); + } + + /// + /// Restore the environment variables we cleared out at the beginning of the test. + /// + [TestCleanup] + public void TearDown() + { + Environment.SetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES", _alwaysOverwriteReadOnlyFiles); + Environment.SetEnvironmentVariable("MSBUILDALWAYSRETRY", _alwaysRetry); + + Copy.RefreshInternalEnvironmentValues(); + } + + /* + * Method: DontCopyOverSameFile + * + * If OnlyCopyIfDifferent is set to "true" then we shouldn't copy over files that + * have the same date and time. + */ + [TestMethod] + public void DontCopyOverSameFile() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(file, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a temp file."); + + ITaskItem f = new TaskItem(file); + ITaskItem[] sourceFiles = new ITaskItem[] { f }; + ITaskItem[] destinationFiles = new ITaskItem[] { f }; + + CopyMonitor m = new CopyMonitor(); + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + t.Execute + ( + new Microsoft.Build.Tasks.CopyFileWithState(m.CopyFile) + ); + + // Expect for there to have been no copies. + Assert.AreEqual(0, m.copyCount); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(file); + } + } + + /// + /// Unless ignore readonly attributes is set, we should not copy over readonly files. + /// + [TestMethod] + public void DoNotNormallyCopyOverReadOnlyFile() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(source, true)) + sw.Write("This is a source file."); + using (StreamWriter sw = new StreamWriter(destination, true)) + sw.Write("This is a destination file."); + + File.SetAttributes(destination, FileAttributes.ReadOnly); + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = new ITaskItem[] { sourceItem }; + ITaskItem[] destinationFiles = new ITaskItem[] { destinationItem }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + // OverwriteReadOnlyFiles defaults to false + + // Should fail: target is readonly + Assert.IsTrue(!t.Execute()); + + // Expect for there to have been no copies. + Assert.AreEqual(0, t.CopiedFiles.Length); + + string destinationContent = File.ReadAllText(destination); + Assert.AreEqual("This is a destination file.", destinationContent); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // did not do retries as it was r/o + } + finally + { + File.SetAttributes(source, FileAttributes.Normal); + File.SetAttributes(destination, FileAttributes.Normal); + File.Delete(source); + File.Delete(destination); + } + } + + /// + /// If MSBUILDALWAYSOVERWRITEREADONLYFILES is set, then overwrite read-only even when + /// OverwriteReadOnlyFiles is false + /// + [TestMethod] + public void CopyOverReadOnlyFileEnvironmentOverride() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(); + string oldAlwaysOverwriteValue = Environment.GetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES", "1 "); + + using (StreamWriter sw = new StreamWriter(source, true)) + sw.Write("This is a source file."); + using (StreamWriter sw = new StreamWriter(destination, true)) + sw.Write("This is a destination file."); + + File.SetAttributes(destination, FileAttributes.ReadOnly); + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = new ITaskItem[] { sourceItem }; + ITaskItem[] destinationFiles = new ITaskItem[] { destinationItem }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + t.OverwriteReadOnlyFiles = false; + + // Should not fail although target is readonly + Assert.IsTrue(t.Execute()); + + // Should have copied file anyway + Assert.AreEqual(1, t.CopiedFiles.Length); + + string destinationContent = File.ReadAllText(destination); + Assert.AreEqual("This is a source file.", destinationContent); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDALWAYSOVERWRITEREADONLYFILES", oldAlwaysOverwriteValue); + + File.Delete(source); + File.Delete(destination); + } + } + + /// + /// If MSBUILDALWAYSRETRY is set, keep retrying the copy. + /// + [TestMethod] + public void AlwaysRetryCopyEnvironmentOverride() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(); + string oldAlwaysOverwriteValue = Environment.GetEnvironmentVariable("MSBUILDALWAYSRETRY"); + + try + { + Environment.SetEnvironmentVariable("MSBUILDALWAYSRETRY", "1 "); + Copy.RefreshInternalEnvironmentValues(); + + using (StreamWriter sw = new StreamWriter(source, true)) + sw.Write("This is a source file."); + using (StreamWriter sw = new StreamWriter(destination, true)) + sw.Write("This is a destination file."); + + File.SetAttributes(destination, FileAttributes.ReadOnly); + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = new ITaskItem[] { sourceItem }; + ITaskItem[] destinationFiles = new ITaskItem[] { destinationItem }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + t.OverwriteReadOnlyFiles = false; + t.Retries = 5; + + // The file is read-only, so the retries will all fail. + Assert.IsFalse(t.Execute()); + + // 3 warnings per retry, except the last one which has only two. + ((MockEngine)t.BuildEngine).AssertLogContains("MSB3026"); + Assert.AreEqual(((t.Retries + 1) * 3) - 1, ((MockEngine)t.BuildEngine).Warnings); + + // One error for "retrying failed", one error for "copy failed" + ((MockEngine)t.BuildEngine).AssertLogContains("MSB3027"); + ((MockEngine)t.BuildEngine).AssertLogContains("MSB3021"); + Assert.AreEqual(2, ((MockEngine)t.BuildEngine).Errors); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDALWAYSRETRY", oldAlwaysOverwriteValue); + Copy.RefreshInternalEnvironmentValues(); + + File.SetAttributes(destination, FileAttributes.Normal); + + File.Delete(source); + File.Delete(destination); + } + } + + /// + /// Unless ignore readonly attributes is set, we should not copy over readonly files. + /// + [TestMethod] + public void CopyOverReadOnlyFileParameterIsSet() + { + string source = FileUtilities.GetTemporaryFile(); + string destination = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(source, true)) + sw.Write("This is a source file."); + using (StreamWriter sw = new StreamWriter(destination, true)) + sw.Write("This is a destination file."); + + File.SetAttributes(destination, FileAttributes.ReadOnly); + + ITaskItem sourceItem = new TaskItem(source); + ITaskItem destinationItem = new TaskItem(destination); + ITaskItem[] sourceFiles = new ITaskItem[] { sourceItem }; + ITaskItem[] destinationFiles = new ITaskItem[] { destinationItem }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + t.OverwriteReadOnlyFiles = true; + + // Should not fail although target is readonly + Assert.IsTrue(t.Execute()); + + // Should have copied file anyway + Assert.AreEqual(1, t.CopiedFiles.Length); + + string destinationContent = File.ReadAllText(destination); + Assert.AreEqual("This is a source file.", destinationContent); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(source); + File.Delete(destination); + } + } + + /// + /// Unless ignore readonly attributes is set, we should not copy over readonly files. + /// + [TestMethod] + public void CopyOverReadOnlyFileParameterIsSetWithDestinationFolder() + { + string source1 = FileUtilities.GetTemporaryFile(); + string source2 = FileUtilities.GetTemporaryFile(); + string destinationFolder = Path.Combine(Path.GetTempPath(), "2A333ED756AF4dc392E728D0F874A398"); + string destination1 = Path.Combine(destinationFolder, Path.GetFileName(source1)); + string destination2 = Path.Combine(destinationFolder, Path.GetFileName(source2)); + try + { + Directory.CreateDirectory(destinationFolder); + + using (StreamWriter sw = new StreamWriter(source1, true)) + sw.Write("This is a source file1."); + using (StreamWriter sw = new StreamWriter(source2, true)) + sw.Write("This is a source file2."); + using (StreamWriter sw = new StreamWriter(destination1, true)) + sw.Write("This is a destination file1."); + using (StreamWriter sw = new StreamWriter(destination2, true)) + sw.Write("This is a destination file2."); + + // Set one destination readonly. + File.SetAttributes(destination1, FileAttributes.ReadOnly); + + ITaskItem sourceItem1 = new TaskItem(source1); + ITaskItem sourceItem2 = new TaskItem(source2); + ITaskItem[] sourceFiles = new ITaskItem[] { sourceItem1, sourceItem2 }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destinationFolder); + t.OverwriteReadOnlyFiles = true; + + // Should not fail although one target is readonly + Assert.IsTrue(t.Execute()); + + // Should have copied files anyway + Assert.AreEqual(2, t.CopiedFiles.Length); + + string destinationContent1 = File.ReadAllText(destination1); + Assert.AreEqual("This is a source file1.", destinationContent1); + string destinationContent2 = File.ReadAllText(destination2); + Assert.AreEqual("This is a source file2.", destinationContent2); + + Assert.IsTrue((File.GetAttributes(destination1) & FileAttributes.ReadOnly) != FileAttributes.ReadOnly); + Assert.IsTrue((File.GetAttributes(destination2) & FileAttributes.ReadOnly) != FileAttributes.ReadOnly); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.SetAttributes(destination1, FileAttributes.Normal); // just in case + File.SetAttributes(destination2, FileAttributes.Normal); // just in case + File.Delete(source1); + File.Delete(source2); + File.Delete(destination1); + File.Delete(destination2); + Directory.Delete(destinationFolder, true); + } + } + + /* + * Method: DoCopyOverDifferentFile + * + * If OnlyCopyIfDifferent is set to "true" then we should still copy over files that + * have different dates or sizes. + */ + [TestMethod] + public void DoCopyOverDifferentFile() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + using (StreamWriter sw = new StreamWriter(destinationFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + t.Execute(); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destinationFile)) // HIGHCHAR: Test reads ASCII (not ANSI). + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /* + * Method: DoCopyOverNonExistentFile + * + * If OnlyCopyIfDifferent is set to "true" then we should still copy over files that + * don't exist. + */ + [TestMethod] + public void DoCopyOverNonExistentFile() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + using (StreamWriter sw = new StreamWriter(destinationFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + File.Delete(destinationFile); + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + t.Execute(); + + Assert.IsTrue(File.Exists(destinationFile), "Expected the destination file to exist."); + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /// + /// Make sure we do not retry when the source file has a misplaced colon + /// + [TestMethod] + public void DoNotRetryCopyNotSupportedException() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = "foo:bar"; + + try + { + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + bool result = t.Execute(); + Assert.IsFalse(result); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + engine.AssertLogContains("MSB3021"); + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(sourceFile); + } + } + + /// + /// Make sure we do not retry when the source file does not exist + /// + [TestMethod] + public void DoNotRetryCopyNonExistentSourceFile() + { + string sourceFile = "Nannanacat"; + string destinationFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(destinationFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + File.Delete(destinationFile); + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + bool result = t.Execute(); + Assert.IsFalse(result); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + engine.AssertLogContains("MSB3030"); + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(destinationFile); + } + } + + /// + /// Make sure we do not retry when the source file is a folder + /// + [TestMethod] + public void DoNotRetryCopyWhenSourceIsFolder() + { + string sourceFile = Path.GetTempPath(); + string destinationFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(destinationFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + File.Delete(destinationFile); + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + bool result = t.Execute(); + Assert.IsFalse(result); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + engine.AssertLogContains("MSB3025"); + engine.AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(destinationFile); + } + } + + /// + /// Most important case is when destination is locked + /// + [TestMethod] + public void DoRetryWhenDestinationLocked() + { + string destinationFile = Path.GetTempFileName(); + string sourceFile = Path.GetTempFileName(); + + try + { + using (StreamWriter sw = new StreamWriter(destinationFile, true)) // Keep it locked + { + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = sourceFiles; + t.DestinationFiles = new TaskItem[] { new TaskItem(destinationFile) }; + + bool result = t.Execute(); + Assert.IsFalse(result); + + engine.AssertLogContains("MSB3021"); // copy failed + engine.AssertLogContains("MSB3026"); // DID retry + + Assert.IsTrue(engine.Errors == 2); // retries failed, and actual failure + Assert.IsTrue(engine.Warnings == 10); + } + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /// + /// When destination is inaccessible due to ACL, do NOT retry + /// + [TestMethod] + public void DoNotRetryWhenDestinationLockedDueToAcl() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "DoNotRetryWhenDestinationLockedDueToAcl"); + string destinationFile = Path.Combine(tempDirectory, "DestinationFile.txt"); + string sourceFile = Path.Combine(tempDirectory, "SourceFile.txt"); + + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + + Directory.CreateDirectory(tempDirectory); + + File.WriteAllText(destinationFile, "Destination"); + File.WriteAllText(sourceFile, "SourceFile"); + + string userAccount = string.Format(@"{0}\{1}", System.Environment.UserDomainName, System.Environment.UserName); + + FileSystemAccessRule denyFile = new FileSystemAccessRule(userAccount, FileSystemRights.Write | FileSystemRights.Delete | FileSystemRights.DeleteSubdirectoriesAndFiles | FileSystemRights.WriteData, AccessControlType.Deny); + FileSystemAccessRule denyDirectory = new FileSystemAccessRule(userAccount, FileSystemRights.DeleteSubdirectoriesAndFiles, AccessControlType.Deny); + + FileSecurity fSecurity = File.GetAccessControl(destinationFile); + DirectorySecurity dSecurity = Directory.GetAccessControl(tempDirectory); + + try + { + fSecurity.AddAccessRule(denyFile); + File.SetAccessControl(destinationFile, fSecurity); + + dSecurity.AddAccessRule(denyDirectory); + Directory.SetAccessControl(tempDirectory, dSecurity); + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = new TaskItem[] { new TaskItem(sourceFile) }; + t.DestinationFiles = new TaskItem[] { new TaskItem(destinationFile) }; + + bool result = t.Execute(); + Assert.IsFalse(result); + + engine.AssertLogContains("MSB3021"); // copy failed + engine.AssertLogDoesntContain("MSB3026"); // Didn't retry + + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + } + finally + { + fSecurity.RemoveAccessRule(denyFile); + File.SetAccessControl(destinationFile, fSecurity); + + dSecurity.RemoveAccessRule(denyDirectory); + Directory.SetAccessControl(tempDirectory, dSecurity); + + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + } + } + + /// + /// Make sure we do not retry when the destination file is a folder + /// + [TestMethod] + public void DoNotRetryCopyWhenDestinationFolderIsFile() + { + string destinationFile = FileUtilities.GetTemporaryFile(); + string sourceFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destinationFile); + t.SkipUnchangedFiles = true; + + bool result = t.Execute(); + Assert.IsFalse(result); + + engine.AssertLogContains("MSB3021"); // copy failed + engine.AssertLogDoesntContain("MSB3026"); // Didn't retry + + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + } + finally + { + File.Delete(sourceFile); + } + } + + /// + /// Make sure we do not retry when the destination file is a folder + /// + [TestMethod] + public void DoNotRetryCopyWhenDestinationFileIsFolder() + { + string destinationFile = Path.GetTempPath(); + string sourceFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + t.SkipUnchangedFiles = true; + + bool result = t.Execute(); + Assert.IsFalse(result); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + engine.AssertLogContains("MSB3024"); + engine.AssertLogDoesntContain("MSB3026"); + } + finally + { + File.Delete(sourceFile); + } + } + + internal class CopyMonitor + { + internal int copyCount = 0; + + /* + * Method: CopyFile + * + * Don't really copy the file, just count how many times this was called. + */ + internal bool? CopyFile(FileState source, FileState destination) + { + ++copyCount; + return true; + } + } + + /// + /// CopiedFiles should only include files that were successfully copied + /// (or skipped), not files for which there was an error. + /// + [TestMethod] + public void OutputsOnlyIncludeSuccessfulCopies() + { + string temp = Path.GetTempPath(); + string inFile1 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A392"); + string inFile2 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A393"); + string invalidFile = "!@#$%^&*()|"; + string validOutFile = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A394"); + + try + { + FileStream fs = null; + FileStream fs2 = null; + + try + { + fs = File.Create(inFile1); + fs2 = File.Create(inFile2); + } + finally + { + fs.Close(); + fs2.Close(); + } + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + ITaskItem i1 = new TaskItem(inFile1); + i1.SetMetadata("Locale", "en-GB"); + i1.SetMetadata("Color", "taupe"); + t.SourceFiles = new ITaskItem[] { new TaskItem(inFile2), i1 }; + + ITaskItem o1 = new TaskItem(validOutFile); + o1.SetMetadata("Locale", "fr"); + o1.SetMetadata("Flavor", "Pumpkin"); + t.DestinationFiles = new ITaskItem[] { new TaskItem(invalidFile), o1 }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(validOutFile, t.CopiedFiles[0].ItemSpec); + Assert.AreEqual(2, t.DestinationFiles.Length); + Assert.AreEqual("fr", t.DestinationFiles[1].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual(invalidFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(validOutFile, t.DestinationFiles[1].ItemSpec); + Assert.AreEqual(validOutFile, t.CopiedFiles[0].ItemSpec); + + // Sources attributes should be left untouched. + Assert.AreEqual("en-GB", t.SourceFiles[1].GetMetadata("Locale")); + Assert.AreEqual("taupe", t.SourceFiles[1].GetMetadata("Color")); + + // Attributes not on Sources should be left untouched. + Assert.AreEqual("Pumpkin", t.DestinationFiles[1].GetMetadata("Flavor")); + Assert.AreEqual("Pumpkin", t.CopiedFiles[0].GetMetadata("Flavor")); + + // Attribute should have been forwarded + Assert.AreEqual("taupe", t.DestinationFiles[1].GetMetadata("Color")); + Assert.AreEqual("taupe", t.CopiedFiles[0].GetMetadata("Color")); + + // Attribute should not have been updated if it already existed on destination + Assert.AreEqual("fr", t.DestinationFiles[1].GetMetadata("Locale")); + Assert.AreEqual("fr", t.CopiedFiles[0].GetMetadata("Locale")); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(inFile1); + File.Delete(inFile2); + File.Delete(validOutFile); + } + } + + /// + /// Copying a file on top of itself should be a success (no-op) whether + /// or not skipUnchangedFiles is true or false. + /// + [TestMethod] + public void CopyFileOnItself() + { + string temp = Path.GetTempPath(); + string file = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A395"); + + try + { + FileStream fs = null; + + try + { + fs = File.Create(file); + } + finally + { + fs.Close(); + } + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem(file) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(file) }; + t.SkipUnchangedFiles = true; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(file, t.DestinationFiles[0].ItemSpec); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries, nothing to do + + t = new Copy(); + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem(file) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(file) }; + t.SkipUnchangedFiles = false; + + success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(file, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(1, t.CopiedFiles.Length); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries, nothing to do + } + finally + { + File.Delete(file); + } + } + + /// + /// Copying a file on top of itself should be a success (no-op) whether + /// or not skipUnchangedFiles is true or false. Variation with different casing/relativeness. + /// + [TestMethod] + public void CopyFileOnItself2() + { + string currdir = Environment.CurrentDirectory; + string filename = "2A333ED756AF4dc392E728D0F864A396"; + string file = Path.Combine(currdir, filename); + + try + { + FileStream fs = null; + + try + { + fs = File.Create(file); + } + finally + { + fs.Close(); + } + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem(file) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(filename.ToLowerInvariant()) }; + t.SkipUnchangedFiles = false; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(filename.ToLowerInvariant(), t.DestinationFiles[0].ItemSpec); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries, nothing to do + } + finally + { + File.Delete(file); + } + } + + /// + /// Copying a file on top of itself should be a success (no-op) whether + /// or not skipUnchangedFiles is true or false. Variation with a second copy failure. + /// + [TestMethod] + public void CopyFileOnItselfAndFailACopy() + { + string temp = Path.GetTempPath(); + string file = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A395"); + string invalidFile = "!@#$%^&*()|"; + string dest2 = "whatever"; + + try + { + FileStream fs = null; + + try + { + fs = File.Create(file); + } + finally + { + fs.Close(); + } + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem(file), new TaskItem(invalidFile) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(file), new TaskItem(dest2) }; + t.SkipUnchangedFiles = false; + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(2, t.DestinationFiles.Length); + Assert.AreEqual(file, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(dest2, t.DestinationFiles[1].ItemSpec); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(file, t.CopiedFiles[0].ItemSpec); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries, no op then invalid + } + finally + { + File.Delete(file); + } + } + + /// + /// DestinationFolder should work. + /// + [TestMethod] + public void CopyToDestinationFolder() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string temp = Path.GetTempPath(); + string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string destFile = Path.Combine(destFolder, Path.GetFileName(sourceFile)); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + // Don't create the dest folder, let task do that + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine me = new MockEngine(); + + t.BuildEngine = me; + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destFolder); + t.SkipUnchangedFiles = true; + + bool success = t.Execute(); + + Assert.IsTrue(success, "success"); + Assert.IsTrue(File.Exists(destFile), "destination exists"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + if (!useHardLinks) + { + Microsoft.Build.UnitTests.MockEngine.GetStringDelegate resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + me.AssertLogDoesntContainMessageFromResource(resourceDelegate, "Copy.HardLinkComment", sourceFile, destFile); + } + else + { + Microsoft.Build.UnitTests.MockEngine.GetStringDelegate resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + me.AssertLogContainsMessageFromResource(resourceDelegate, "Copy.HardLinkComment", sourceFile, destFile); + } + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(destFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(destFile, t.CopiedFiles[0].ItemSpec); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + Helpers.DeleteFiles(sourceFile, destFile); + } + } + + /// + /// DestinationFolder should work. + /// + [TestMethod] + public void CopyDoubleEscapableFileToDestinationFolder() + { + string sourceFileEscaped = Path.GetTempPath() + "a%253A_" + Guid.NewGuid().ToString("N") + ".txt"; + string sourceFile = EscapingUtilities.UnescapeAll(sourceFileEscaped); + string temp = Path.GetTempPath(); + string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string destFile = Path.Combine(destFolder, Path.GetFileName(sourceFile)); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + { + sw.Write("This is a source temp file."); + } + + // Don't create the dest folder, let task do that + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFileEscaped) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destFolder); + t.SkipUnchangedFiles = true; + + bool success = t.Execute(); + + Assert.IsTrue(success, "success"); + Assert.IsTrue(File.Exists(destFile), "destination exists"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destFile)) + { + destinationFileContents = sr.ReadToEnd(); + } + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(destFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(destFile, t.CopiedFiles[0].ItemSpec); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + Helpers.DeleteFiles(sourceFile, destFile); + } + } + + /// + /// Copying duplicates should only perform the actual copy once for each unique source/destination pair + /// but should still produce outputs for all specified source/destination pairs. + /// + [TestMethod] + public void CopyWithDuplicatesUsingFolder() + { + string tempPath = Path.GetTempPath(); + + ITaskItem[] sourceFiles = new ITaskItem[] + { + new TaskItem(Path.Combine(tempPath, "a.cs")), + new TaskItem(Path.Combine(tempPath, "b.cs")), + new TaskItem(Path.Combine(tempPath, "a.cs")), + new TaskItem(Path.Combine(tempPath, "a.cs")), + }; + + foreach (ITaskItem item in sourceFiles) + { + using (StreamWriter sw = new StreamWriter(item.ItemSpec)) // HIGHCHAR: Test writes in UTF8 without preamble. + { + sw.Write("This is a source temp file."); + } + } + + var filesActuallyCopied = new List>(); + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(Path.Combine(tempPath, "foo")); + + bool success = t.Execute(delegate (FileState source, FileState dest) + { + filesActuallyCopied.Add(new KeyValuePair(source, dest)); + return true; + }); + + Assert.IsTrue(success); + Assert.AreEqual(2, filesActuallyCopied.Count); + Assert.AreEqual(4, t.CopiedFiles.Length); + Assert.AreEqual(Path.Combine(tempPath, "a.cs"), filesActuallyCopied[0].Key.Name); + Assert.AreEqual(Path.Combine(tempPath, "b.cs"), filesActuallyCopied[1].Key.Name); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + + /// + /// Copying duplicates should only perform the actual copy once for each unique source/destination pair + /// but should still produce outputs for all specified source/destination pairs. + /// + [TestMethod] + public void CopyWithDuplicatesUsingFiles() + { + string tempPath = Path.GetTempPath(); + + ITaskItem[] sourceFiles = new ITaskItem[] + { + new TaskItem(Path.Combine(tempPath, "a.cs")), + new TaskItem(Path.Combine(tempPath, "b.cs")), + new TaskItem(Path.Combine(tempPath, "a.cs")), + new TaskItem(Path.Combine(tempPath, "a.cs")), + new TaskItem(Path.Combine(tempPath, "a.cs")), + }; + + foreach (ITaskItem item in sourceFiles) + { + using (StreamWriter sw = new StreamWriter(item.ItemSpec)) // HIGHCHAR: Test writes in UTF8 without preamble. + { + sw.Write("This is a source temp file."); + } + } + + ITaskItem[] destFiles = new ITaskItem[] + { + new TaskItem(Path.Combine(tempPath, @"xa.cs")), // a.cs -> xa.cs + new TaskItem(Path.Combine(tempPath, @"xa.cs")), // b.cs -> xa.cs should copy because it's a different source + new TaskItem(Path.Combine(tempPath, @"xb.cs")), // a.cs -> xb.cs should copy because it's a different destination + new TaskItem(Path.Combine(tempPath, @"xa.cs")), // a.cs -> xa.cs should copy because it's a different source + new TaskItem(Path.Combine(tempPath, @"xa.cs")), // a.cs -> xa.cs should not copy because it's the same source + }; + + var filesActuallyCopied = new List>(); + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destFiles; + + bool success = t.Execute(delegate (FileState source, FileState dest) + { + filesActuallyCopied.Add(new KeyValuePair(source, dest)); + return true; + }); + + Assert.IsTrue(success); + Assert.AreEqual(4, filesActuallyCopied.Count); + Assert.AreEqual(5, t.CopiedFiles.Length); + Assert.AreEqual(Path.Combine(tempPath, "a.cs"), filesActuallyCopied[0].Key.Name); + Assert.AreEqual(Path.Combine(tempPath, "b.cs"), filesActuallyCopied[1].Key.Name); + Assert.AreEqual(Path.Combine(tempPath, "a.cs"), filesActuallyCopied[2].Key.Name); + Assert.AreEqual(Path.Combine(tempPath, "a.cs"), filesActuallyCopied[3].Key.Name); + Assert.AreEqual(Path.Combine(tempPath, "xa.cs"), filesActuallyCopied[0].Value.Name); + Assert.AreEqual(Path.Combine(tempPath, "xa.cs"), filesActuallyCopied[1].Value.Name); + Assert.AreEqual(Path.Combine(tempPath, "xb.cs"), filesActuallyCopied[2].Value.Name); + Assert.AreEqual(Path.Combine(tempPath, "xa.cs"), filesActuallyCopied[3].Value.Name); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + + /// + /// DestinationFiles should only include files that were successfully copied + /// (or skipped), not files for which there was an error. + /// + [TestMethod] + public void DestinationFilesLengthNotEqualSourceFilesLength() + { + string temp = Path.GetTempPath(); + string inFile1 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string inFile2 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A399"); + string outFile1 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A400"); + + try + { + FileStream fs = null; + FileStream fs2 = null; + + try + { + fs = File.Create(inFile1); + fs2 = File.Create(inFile2); + } + finally + { + fs.Close(); + fs2.Close(); + } + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.SourceFiles = new ITaskItem[] { new TaskItem(inFile1), new TaskItem(inFile2) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(outFile1) }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.IsNull(t.CopiedFiles); + Assert.IsTrue(!File.Exists(outFile1)); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(inFile1); + File.Delete(inFile2); + File.Delete(outFile1); + } + } + + /// + /// If the destination path is too long, the task should not bubble up + /// the System.IO.PathTooLongException + /// + [TestMethod] + public void Regress451057_ExitGracefullyIfPathNameIsTooLong() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(sourceFile); + } + } + + /// + /// If the source path is too long, the task should not bubble up + /// the System.IO.PathTooLongException + /// + [TestMethod] + public void Regress451057_ExitGracefullyIfPathNameIsTooLong2() + { + string sourceFile = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"; + string destinationFile = FileUtilities.GetTemporaryFile(); + File.Delete(destinationFile); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + Assert.IsTrue(!File.Exists(destinationFile)); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + + /// + /// If the SourceFiles parameter is given invalid path characters, make sure the task exits gracefully. + /// + [TestMethod] + public void ExitGracefullyOnInvalidPathCharacters() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + t.BuildEngine = new MockEngine(); + t.SourceFiles = new ITaskItem[] { new TaskItem("foo | bar") }; ; + t.DestinationFolder = new TaskItem("dest"); + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + + /// + /// Verifies that we error for retries less than 0 + /// + [TestMethod] + public void InvalidRetryCount() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }; + t.Retries = -1; + + bool result = t.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3028"); + } + + /// + /// Verifies that we error for retry delay less than 0 + /// + [TestMethod] + public void InvalidRetryDelayCount() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }; + t.Retries = 1; + t.RetryDelayMilliseconds = -1; + + bool result = t.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3029"); + } + + /// + /// Verifies that we do not log the retrying warning if we didn't request + /// retries. + /// + [TestMethod] + public void FailureWithNoRetries() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }; + t.Retries = 0; + + CopyFunctor copyFunctor = new CopyFunctor(2, false /* do not throw on failure */); + bool result = t.Execute(copyFunctor.Copy); + + Assert.AreEqual(false, result); + engine.AssertLogDoesntContain("MSB3026"); + engine.AssertLogDoesntContain("MSB3027"); + } + + /// + /// Retrying default + /// + [TestMethod] + public void DefaultRetriesIs10() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + + Assert.AreEqual(10, t.Retries); + } + + /// + /// Delay default + /// + [TestMethod] + public void DefaultRetryDelayIs1000() + { + Copy t = new Copy(); + + Assert.AreEqual(1000, t.RetryDelayMilliseconds); + } + + /// + /// Hardlink default + /// + [TestMethod] + public void DefaultNoHardlink() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + + Assert.AreEqual(false, t.UseHardlinksIfPossible); + } + + /// + /// Verifies that we get the one retry we ask for after the first attempt fails, + /// and we get appropriate messages. + /// + [TestMethod] + public void SuccessAfterOneRetry() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }; + t.Retries = 1; + t.RetryDelayMilliseconds = 0; // Can't really test the delay, but at least try passing in a value + + CopyFunctor copyFunctor = new CopyFunctor(2, false /* do not throw on failure */); + bool result = t.Execute(copyFunctor.Copy); + + Assert.AreEqual(true, result); + engine.AssertLogContains("MSB3026"); + engine.AssertLogDoesntContain("MSB3027"); + } + + /// + /// Verifies that after a successful retry we continue to the next file + /// + [TestMethod] + public void SuccessAfterOneRetryContinueToNextFile() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source"), new TaskItem("c:\\source2") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination"), new TaskItem("c:\\destination2") }; + t.Retries = 1; + t.RetryDelayMilliseconds = 1; // Can't really test the delay, but at least try passing in a value + + CopyFunctor copyFunctor = new CopyFunctor(2, false /* do not throw on failure */); + bool result = t.Execute(copyFunctor.Copy); + + Assert.AreEqual(true, result); + engine.AssertLogContains("MSB3026"); + engine.AssertLogDoesntContain("MSB3027"); + Assert.AreEqual(copyFunctor.FilesCopiedSuccessfully[0].Name, "c:\\source"); + Assert.AreEqual(copyFunctor.FilesCopiedSuccessfully[1].Name, "c:\\source2"); + } + + /// + /// The copy delegate can return false, or throw on failure. + /// This test tests returning false. + /// + [TestMethod] + public void TooFewRetriesReturnsFalse() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }; + t.Retries = 2; + + CopyFunctor copyFunctor = new CopyFunctor(4, false /* do not throw */); + bool result = t.Execute(copyFunctor.Copy); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3026"); + engine.AssertLogContains("MSB3027"); + } + + /// + /// The copy delegate can return false, or throw on failure. + /// This test tests the throw case. + /// + [TestMethod] + public void TooFewRetriesThrows() + { + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + // Allow the task's default (false) to have a chance + if (useHardLinks) + { + t.UseHardlinksIfPossible = useHardLinks; + } + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }; + t.Retries = 1; + + CopyFunctor copyFunctor = new CopyFunctor(3, true /* throw */); + bool result = t.Execute(copyFunctor.Copy); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3026"); + engine.AssertLogContains("MSB3027"); + } + + /// + /// Helper functor for retry tests. + /// Simulates the File.Copy method without touching the disk. + /// First copy fails as requested, subsequent copies succeed. + /// + private class CopyFunctor + { + /// + /// On what attempt count should we stop failing? + /// + private int _countOfSuccess; + + /// + /// Should we throw when we fail, instead of just returning false? + /// + private bool _throwOnFailure; + + /// + /// How many tries have we done so far + /// + private int _tries; + + /// + /// Which files we actually copied. + /// + private List _filesCopiedSuccessfully; + + /// + /// Which files we actually copied + /// + internal List FilesCopiedSuccessfully + { + get { return _filesCopiedSuccessfully; } + } + + /// + /// Constructor + /// + internal CopyFunctor(int countOfSuccess, bool throwOnFailure) + { + _countOfSuccess = countOfSuccess; + _throwOnFailure = throwOnFailure; + _tries = 0; + _filesCopiedSuccessfully = new List(); + } + + /// + /// Pretend to be File.Copy. + /// + internal bool? Copy(FileState source, FileState destination) + { + _tries++; + + // 2nd and subsequent copies always succeed + if (_filesCopiedSuccessfully.Count > 0 || _countOfSuccess == _tries) + { + Console.WriteLine("Copied {0} to {1} OK", source, destination); + _filesCopiedSuccessfully.Add(source); + return true; + } + + if (_throwOnFailure) + { + throw new IOException("oops"); + } + else + { + return null; + } + } + } + } + + [TestClass] + public class CopyNotHardLink_Tests : Copy_Tests + { + public CopyNotHardLink_Tests() + { + this.useHardLinks = false; + } + } + + [TestClass] + public class CopyHardLink_Tests : Copy_Tests + { + public CopyHardLink_Tests() + { + this.useHardLinks = true; + } + + /// + /// DestinationFolder should work. + /// + [TestMethod] + public void CopyToDestinationFolderWithHardLinkCheck() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string temp = Path.GetTempPath(); + string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string destFile = Path.Combine(destFolder, Path.GetFileName(sourceFile)); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + // Don't create the dest folder, let task do that + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + + // Allow the task's default (false) to have a chance + t.UseHardlinksIfPossible = true; + + MockEngine me = new MockEngine(true); + t.BuildEngine = me; + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destFolder); + t.SkipUnchangedFiles = true; + + bool success = t.Execute(); + + Assert.IsTrue(success, "success"); + Assert.IsTrue(File.Exists(destFile), "destination exists"); + Microsoft.Build.UnitTests.MockEngine.GetStringDelegate resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + + me.AssertLogContainsMessageFromResource(resourceDelegate, "Copy.HardLinkComment", sourceFile, destFile); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination hard linked file to contain the contents of source file." + ); + + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(destFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(destFile, t.CopiedFiles[0].ItemSpec); + + // Now we will write new content to the source file + // we'll then check that the destination file automatically + // has the same content (i.e. it's been hard linked) + using (StreamWriter sw = new StreamWriter(sourceFile, false)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is another source temp file."); + + // Read the destination file (it should have the same modified content as the source) + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is another source temp file.", + "Expected the destination hard linked file to contain the contents of source file. Even after modification of the source" + ); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + Helpers.DeleteFiles(sourceFile, destFile); + } + } + + /// + /// DestinationFolder should work. + /// + [TestMethod] + [Ignore] + // Ignore: Flaky test + public void CopyToDestinationFolderWithHardLinkFallbackNetwork() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string temp = @"\\localhost\c$\temp"; + string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string destFile = Path.Combine(destFolder, Path.GetFileName(sourceFile)); + + try + { + if (!Directory.Exists(destFolder)) + { + Directory.CreateDirectory(destFolder); + } + + string nothingFile = Path.Combine(destFolder, "nothing.txt"); + File.WriteAllText(nothingFile, "nothing"); + File.Delete(nothingFile); + } + catch (Exception) + { + Console.WriteLine("CopyToDestinationFolderWithHardLinkFallbackNetwork test could not access the network."); + // Something caused us to not be able to access our "network" share, don't fail. + return; + } + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + t.UseHardlinksIfPossible = true; + MockEngine me = new MockEngine(true); + t.BuildEngine = me; + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destFolder); + t.SkipUnchangedFiles = true; + + bool success = t.Execute(); + + Assert.IsTrue(success, "success"); + Assert.IsTrue(File.Exists(destFile), "destination exists"); + Microsoft.Build.UnitTests.MockEngine.GetStringDelegate resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + + me.AssertLogContainsMessageFromResource(resourceDelegate, "Copy.HardLinkComment", sourceFile, destFile); + + // Can't do this below, because the real message doesn't end with String.Empty, it ends with a CLR exception string, and so matching breaks in PLOC. + // Instead look for the HRESULT that CLR unfortunately puts inside its exception string. Something like this + // The system cannot move the file to a different disk drive. (Exception from HRESULT: 0x80070011) + // me.AssertLogContainsMessageFromResource(resourceDelegate, "Copy.RetryingAsFileCopy", sourceFile, destFile, String.Empty); + me.AssertLogContains("0x80070011"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(destFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(destFile, t.CopiedFiles[0].ItemSpec); + + // Now we will write new content to the source file + // we'll then check that the destination file automatically + // has the same content (i.e. it's been hard linked) + using (StreamWriter sw = new StreamWriter(sourceFile, false)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is another source temp file."); + + // Read the destination file (it should have the same modified content as the source) + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination copied file to contain the contents of original source file only." + ); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(sourceFile); + File.Delete(destFile); + Directory.Delete(destFolder, true); + } + } + + /// + /// DestinationFolder should work. + /// + [TestMethod] + [Ignore] + // Ignore: Flaky test + public void CopyToDestinationFolderWithHardLinkFallbackTooManyLinks() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string temp = Path.GetTempPath(); + string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string destFile = Path.Combine(destFolder, Path.GetFileName(sourceFile)); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + if (!Directory.Exists(destFolder)) + { + Directory.CreateDirectory(destFolder); + } + + // Exhaust the number (1024) of directory entries that can be created for a file + // This is 1 + (1 x hard links) + // We need to test the fallback code path when we're out of directory entries for a file.. + for (int n = 0; n < 1025 /* make sure */; n++) + { + string destLink = Path.Combine(destFolder, Path.GetFileNameWithoutExtension(sourceFile) + "." + n.ToString()); + NativeMethods.CreateHardLink(destLink, sourceFile, IntPtr.Zero); + } + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Copy t = new Copy(); + t.RetryDelayMilliseconds = 1; // speed up tests! + t.UseHardlinksIfPossible = true; + MockEngine me = new MockEngine(true); + t.BuildEngine = me; + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destFolder); + t.SkipUnchangedFiles = true; + + bool success = t.Execute(); + + Assert.IsTrue(success, "success"); + Assert.IsTrue(File.Exists(destFile), "destination exists"); + Microsoft.Build.UnitTests.MockEngine.GetStringDelegate resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + + me.AssertLogContainsMessageFromResource(resourceDelegate, "Copy.HardLinkComment", sourceFile, destFile); + + // Can't do this below, because the real message doesn't end with String.Empty, it ends with a CLR exception string, and so matching breaks in PLOC. + // Instead look for the HRESULT that CLR unfortunately puts inside its exception string. Something like this + // Tried to create more than a few links to a file that is supported by the file system. (! yhMcE! Exception from HRESULT: Table c?! 0x80070476) + // me.AssertLogContainsMessageFromResource(resourceDelegate, "Copy.RetryingAsFileCopy", sourceFile, destFile, String.Empty); + me.AssertLogContains("0x80070476"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(1, t.CopiedFiles.Length); + Assert.AreEqual(destFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(destFile, t.CopiedFiles[0].ItemSpec); + + // Now we will write new content to the source file + // we'll then check that the destination file automatically + // has the same content (i.e. it's been hard linked) + using (StreamWriter sw = new StreamWriter(sourceFile, false)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is another source temp file."); + + // Read the destination file (it should have the same modified content as the source) + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination copied file to contain the contents of original source file only." + ); + + ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries + } + finally + { + File.Delete(sourceFile); + File.Delete(destFile); + Directory.Delete(destFolder, true); + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/CreateCSharpManifestResourceName_Tests.cs b/src/XMakeTasks/UnitTests/CreateCSharpManifestResourceName_Tests.cs new file mode 100644 index 00000000000..a1d24820b73 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CreateCSharpManifestResourceName_Tests.cs @@ -0,0 +1,721 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CreateCSharpManifestResourceName_Tests + { + /// + /// Test the basic functionality. + /// + [TestMethod] + public void Basic() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, + true, + null, // Root namespace + null, + null, + StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"), + null + ); + + Assert.AreEqual("MyStuff.Namespace.Class", result); + } + + /// + /// Test for a namespace that has ANSI but non-ascii characters. + /// + /// NOTE: namespace dÃa {} get's compiled into different IL depending on the language of the OS + /// that its running on. This is because 'Ã' is a high ANSI character which is interpretted differently + /// for different codepages. + /// + [TestMethod] + public void Regress172107() + { + // Can't embed the 'Ã' directly because the string is Unicode already and the Unicode<-->ANSI transform + // isn't bidirectional. + MemoryStream sourcesStream = (MemoryStream)StreamHelpers.StringToStream("namespace d?a { class Class {} }"); + + // Instead, directly write the ANSI character into the memory buffer. + sourcesStream.Seek(11, SeekOrigin.Begin); + sourcesStream.WriteByte(0xc3); // Plug the 'Ã' in + sourcesStream.Seek(0, SeekOrigin.Begin); + + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"irrelevent", + null, + true, + null, // Root namespace + null, + null, + sourcesStream, + null + ); + + + MemoryStream m = new MemoryStream(); + m.Write(new byte[] { 0x64, 0xc3, 0x61, 0x2e, 0x43, 0x6c, 0x61, 0x73, 0x73 }, 0, 9); // dÃa.Class in ANSI + m.Flush(); + m.Seek(0, SeekOrigin.Begin); + + StreamReader r = new StreamReader(m, System.Text.Encoding.Default, true); // HIGHCHAR: Test reads ANSI because that's the scenario. + string className = r.ReadToEnd(); + + Assert.AreEqual(className, result); + } + + + + /// + /// Test for a namespace that has UTF8 characters but there's no BOM at the start. + /// + /// + [TestMethod] + public void Regress249540() + { + // Special character is 'Ä' in UTF8: 0xC3 84 + MemoryStream sourcesStream = (MemoryStream)StreamHelpers.StringToStream("namespace d??a { class Class {} }"); + + // Instead, directly write the ANSI character into the memory buffer. + sourcesStream.Seek(11, SeekOrigin.Begin); + sourcesStream.WriteByte(0xc3); // Plug the first byte of 'Ä' in. + sourcesStream.WriteByte(0x84); // Plug the second byte of 'Ä' in. + sourcesStream.Seek(0, SeekOrigin.Begin); + + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"irrelevent", + null, + true, + null, // Root namespace + null, + null, + sourcesStream, + null + ); + + Assert.AreEqual("d\u00C4a.Class", result); + } + + /// + /// Test a dependent with a relative path + /// + [TestMethod] + public void RelativeDependentUpon() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, + true, + null, // Root namespace + null, + null, + StreamHelpers.StringToStream("namespace Namespace { class Class {} }"), + null + ); + + Assert.AreEqual("Namespace.Class", result); + } + + /// + /// Test a dependent with a relative path + /// + [TestMethod] + public void AbsoluteDependentUpon() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + null, + null, + StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"), + null + ); + + Assert.AreEqual("MyStuff.Namespace.Class", result); + } + + /// + /// A dependent class plus there is a culture. + /// + [TestMethod] + public void DependentWithCulture() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.en-GB.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + null, + null, + StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"), + null + ); + + Assert.AreEqual("MyStuff.Namespace.Class.en-GB", result); + } + + /// + /// A dependent class plus there is a culture that was expressed in the metadata of the + /// item rather than the filename. + /// + [TestMethod] + public void DependentWithCultureMetadata() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + null, + "en-GB", + StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"), + null + ); + + Assert.AreEqual("MyStuff.Namespace.Class.en-GB", result); + } + + /// + /// A dependent class plus there is a culture embedded in the .RESX filename. + /// + [TestMethod] + public void DependentWithEmbeddedCulture() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.fr-fr.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + null, + null, + StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"), + null + ); + + Assert.AreEqual("MyStuff.Namespace.Class.fr-fr", result); + } + + /// + /// No dependent class, but there is a root namespace place. Also, the .resx + /// extension contains some upper-case characters. + /// + [TestMethod] + public void RootnamespaceWithCulture() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\MyForm.en-GB.ResX", + null, + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual("RootNamespace.SubFolder.MyForm.en-GB", result); + } + + /// + /// If there is a link file name then it is preferred over the main file name. + /// + [TestMethod] + public void Regress222308() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"..\..\XmlEditor\Setup\XmlEditor.rgs", + @"XmlEditor.rgs", + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual("RootNamespace.XmlEditor.rgs", result); + } + + /// + /// A non-resx file in a subfolder, with a root namespace. + /// + [TestMethod] + public void BitmapWithRootNamespace() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\SplashScreen.bmp", + null, + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual("RootNamespace.SubFolder.SplashScreen.bmp", result); + } + + /// + /// A culture-specific non-resx file in a subfolder, with a root namespace. + /// + [TestMethod] + public void CulturedBitmapWithRootNamespace() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\SplashScreen.fr.bmp", + null, + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"fr\RootNamespace.SubFolder.SplashScreen.bmp", result); + } + + /// + /// A culture-specific non-resx file in a subfolder, with a root namespace, but no culture directory prefix + /// + [TestMethod] + public void CulturedBitmapWithRootNamespaceNoDirectoryPrefix() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\SplashScreen.fr.bmp", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.SubFolder.SplashScreen.bmp", result); + } + + /// + /// If the filename passed in as the "DependentUpon" file doesn't end in .cs then + /// we want to fall back to the RootNamespace+FileName logic. + /// + [TestMethod] + public void Regress188319() + { + CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("SR1.resx"); + i.SetMetadata("BuildAction", "EmbeddedResource"); + i.SetMetadata("DependentUpon", "SR1.strings"); // Normally, this would be a C# file. + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "CustomToolTest"; + bool success = t.Execute + ( + new Microsoft.Build.Tasks.CreateFileStream(CreateFileStream) + ); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceNames = t.ManifestResourceNames; + + Assert.AreEqual(1, resourceNames.Length); + Assert.AreEqual(@"CustomToolTest.SR1", resourceNames[0].ItemSpec); + } + + /// + /// helper method for verifying manifest resource names + /// + private void VerifyExpectedManifestResourceName(string resourcePath, string expectedName) + { + string result = CreateCSharpManifestResourceName.CreateManifestNameImpl(resourcePath, null, true, "Root", null, null, null, null); + string expected = "Root." + expectedName; + + Assert.AreEqual(result, expected); + } + + /// + /// Need to convert any spaces in the directory name of embedded resource files to underscores. + /// Leave spaces in the file name itself alone. That's how Everett did it. + /// + [TestMethod] + public void Regress309027() + { + VerifyExpectedManifestResourceName( + @"SubFolder With Spaces\Splash Screen.bmp", "SubFolder_With_Spaces.Splash Screen.bmp"); + } + + /// + /// The folder part of embedded resource names (not the file name though) needs to be a proper identifier, + /// since that's how Everett used to do this + /// + [TestMethod] + public void Regress311473() + { + // First char must be a letter or a connector (underscore), others must be a letter/digit/connector or a combining mark + // If the first character is not a valid first character but valid subsequent character, the name is prepended + // with an underscore. Invalid subsequent characters are replaced with an underscore. + VerifyExpectedManifestResourceName(@"1abc()\pic.bmp", "_1abc__.pic.bmp"); + + // if the first character is not a valid id character at all, it's replaced with an underscore instead of + // prepending an underscore to it + VerifyExpectedManifestResourceName(@"@abc\pic.bmp", "_abc.pic.bmp"); + + // Each folder name is processed independently + VerifyExpectedManifestResourceName(@"1234\1abc\pic.bmp", "_1234._1abc.pic.bmp"); + + // Each part of folder name separated by dots is processed independently as well + VerifyExpectedManifestResourceName(@"1abc.@abc@._1234()\pic.bmp", "_1abc._abc_._1234__.pic.bmp"); + VerifyExpectedManifestResourceName(@"1abc\@abc@\_1234()\pic.bmp", "_1abc._abc_._1234__.pic.bmp"); + + // Combination of dots and folders + VerifyExpectedManifestResourceName(@"@Ab2.=gh\1hl.l=a1\pic.bmp", "_Ab2._gh._1hl.l_a1.pic.bmp"); + + // A single underscore folder name is expanded to two underscores + VerifyExpectedManifestResourceName(@"_\pic.bmp", "__.pic.bmp"); + + // A more complex example of the last rule + VerifyExpectedManifestResourceName(@"_\__\_.__\_\pic.bmp", "__.__._.__.__.pic.bmp"); + } + + /// + /// If the dependent upon filename and the resource filename both contain what looks like + /// a culture, do not treat it as a culture identifier. E.g.: + /// + /// Form1.ro.resx == DependentUpon ==> Form1.ro.vb + /// + /// In this case, we don't include "ro" as the culture because it's in both filenames. In + /// the case of: + /// + /// Form1.ro.resx == DependentUpon ==> Form1.vb + /// + /// we continue to treat "ro" as the culture. + /// + [TestMethod] + public void Regress419591() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + "MyForm.en-GB.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + "MyForm.en-GB.cs", + null, + StreamHelpers.StringToStream("namespace ClassLibrary1 { class MyForm {} }"), + null + ); + + Assert.AreEqual("ClassLibrary1.MyForm", result); + } + + /// + /// If the dependent upon filename and the resource filename both contain what looks like + /// a culture, do not treat it as a culture identifier. E.g.: + /// + /// Form1.ro.resx == DependentUpon ==> Form1.ro.vb + /// + /// In this case, we don't include "ro" as the culture because it's in both filenames. If + /// The parent source file doesn't have a class name in it, we just use the culture neutral + /// filename of the resource file. + /// + [TestMethod] + public void Regress419591_EmptySource() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + "MyForm.en-GB.resx", + null, + true, + "RootNamespace", + "MyForm.en-GB.cs", + null, + StreamHelpers.StringToStream(""), + null + ); + + Assert.AreEqual("RootNamespace.MyForm.en-GB", result); + } + + /// + /// If we encounter a class or namespace name within a conditional compilation directive, + /// we need to warn because we do not try to resolve the correct manifest name depending + /// on conditional compilation of code. + /// + [TestMethod] + public void Regress459265() + { + MockEngine m = new MockEngine(); + CreateCSharpManifestResourceName c = new CreateCSharpManifestResourceName(); + c.BuildEngine = m; + + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + "MyForm.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + "MyForm.cs", + null, + StreamHelpers.StringToStream( +@"using System; +#if false +namespace ClassLibrary1 +#endif +#if Debug +namespace ClassLibrary2 +#else +namespace ClassLibrary3 +#endif +{ + class MyForm + { + } +}" + ), + c.Log + ); + + Assert.IsTrue + ( + m.Log.Contains + ( + String.Format(AssemblyResources.GetString("CreateManifestResourceName.DefinitionFoundWithinConditionalDirective"), "MyForm.cs", "MyForm.resx") + ) + ); + } + + /// + /// Given a file path, return a stream on top of that path. + /// + /// Path to the file + /// File mode + /// Access type + /// The Stream + private Stream CreateFileStream(string path, FileMode mode, FileAccess access) + { + if (String.Compare(path, "SR1.strings", StringComparison.OrdinalIgnoreCase) == 0) + { + return StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"); + } + Assert.Fail(String.Format("Encountered a new path {0}, needs unittesting support", path)); + return null; + } + + /// + /// Tests to ensure that the ResourceFilesWithManifestResourceNames contains everything that + /// the ResourceFiles property on the task contains, but with additional metadata called ManifestResourceName + /// + [TestMethod] + public void ResourceFilesWithManifestResourceNamesContainsAdditionalMetadata() + { + CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("strings.resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"strings.resx", resourceFiles[0].ItemSpec); + Assert.AreEqual(@"ResourceRoot.strings", resourceFiles[0].GetMetadata("ManifestResourceName")); + } + + /// + /// Ensure that if no LogicalName is specified, that the same ManifestResourceName metadata + /// gets applied as LogicalName + /// + [TestMethod] + public void AddLogicalNameForNonResx() + { + CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("pic.bmp"); + i.SetMetadata("Type", "Non-Resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"pic.bmp", resourceFiles[0].ItemSpec); + Assert.AreEqual(@"ResourceRoot.pic.bmp", resourceFiles[0].GetMetadata("LogicalName")); + } + + /// + /// Ensure that a LogicalName that is already present is preserved during manifest name generation + /// + [TestMethod] + public void PreserveLogicalNameForNonResx() + { + CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("pic.bmp"); + i.SetMetadata("LogicalName", "foo"); + i.SetMetadata("Type", "Non-Resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"pic.bmp", resourceFiles[0].ItemSpec); + Assert.AreEqual(@"foo", resourceFiles[0].GetMetadata("LogicalName")); + } + + /// + /// Resx resources should not get ManifestResourceName metadata copied to the LogicalName value + /// + [TestMethod] + public void NoLogicalNameAddedForResx() + { + CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("strings.resx"); + i.SetMetadata("Type", "Resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"strings.resx", resourceFiles[0].ItemSpec); + Assert.AreEqual(String.Empty, resourceFiles[0].GetMetadata("LogicalName")); + } + + /// + /// A culture-specific resources file in a subfolder, with a root namespace + /// + [TestMethod] + public void CulturedResourcesFileWithRootNamespaceWithinSubfolder() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\MyResource.fr.resources", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.SubFolder.MyResource.fr.resources", result); + } + + /// + /// A culture-specific resources file with a root namespace + /// + [TestMethod] + public void CulturedResourcesFileWithRootNamespace() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"MyResource.fr.resources", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.MyResource.fr.resources", result); + } + + /// + /// A non-culture-specific resources file with a root namespace + /// + [TestMethod] + public void ResourcesFileWithRootNamespace() + { + string result = + CreateCSharpManifestResourceName.CreateManifestNameImpl + ( + @"MyResource.resources", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.MyResource.resources", result); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CreateItem_Tests.cs b/src/XMakeTasks/UnitTests/CreateItem_Tests.cs new file mode 100644 index 00000000000..80f9edfc84d --- /dev/null +++ b/src/XMakeTasks/UnitTests/CreateItem_Tests.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CreateItem_Tests + { + /// + /// CreateIteming identical lists results in empty list. + /// + [TestMethod] + public void OneFromOneIsZero() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + t.Include = new ITaskItem[] { new TaskItem("MyFile.txt") }; + t.Exclude = new ITaskItem[] { new TaskItem("MyFile.txt") }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.Include.Length); + } + + /// + /// CreateIteming completely different lists results in left list. + /// + [TestMethod] + public void OneFromOneMismatchIsOne() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + t.Include = new ITaskItem[] { new TaskItem("MyFile.txt") }; + t.Exclude = new ITaskItem[] { new TaskItem("MyFileOther.txt") }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.Include.Length); + Assert.AreEqual("MyFile.txt", t.Include[0].ItemSpec); + } + + /// + /// If 'Exclude' is unspecified, then 'Include' is the result. + /// + [TestMethod] + public void UnspecifiedFromOneIsOne() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + t.Include = new ITaskItem[] { new TaskItem("MyFile.txt") }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.Include.Length); + Assert.AreEqual(t.Include[0].ItemSpec, t.Include[0].ItemSpec); + } + + + /// + /// If 'Include' is unspecified, then empty is the result. + /// + [TestMethod] + public void OneFromUnspecifiedIsEmpty() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + t.Exclude = new ITaskItem[] { new TaskItem("MyFile.txt") }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.Include.Length); + } + + /// + /// If 'Include' and 'Exclude' are unspecified, then empty is the result. + /// + [TestMethod] + public void UnspecifiedFromUnspecifiedIsEmpty() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.Include.Length); + } + + + /// + /// CreateItem is case insensitive. + /// + [TestMethod] + public void CaseDoesntMatter() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + t.Include = new ITaskItem[] { new TaskItem("MyFile.txt") }; + t.Exclude = new ITaskItem[] { new TaskItem("myfile.tXt") }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.Include.Length); + } + + /// + /// Using the CreateItem task to expand wildcards, and then try accessing the RecursiveDir + /// metadata to force batching. + /// + [TestMethod] + public void WildcardsWithRecursiveDir() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("Myapp.proj", @" + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("Foo.txt", "foo"); + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"Subdir\Bar.txt", "bar"); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("Myapp.proj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"Destination\Foo.txt"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"Destination\Subdir\Bar.txt"); + } + + /// + /// CreateItem should add additional metadata when instructed + /// + [TestMethod] + public void AdditionalMetaData() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + t.Include = new ITaskItem[] { new TaskItem("MyFile.txt") }; + t.AdditionalMetadata = new string[] { "MyMetaData=SomeValue" }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual("SomeValue", t.Include[0].GetMetadata("MyMetaData")); + } + + /// + /// We should be able to preserve the existing metadata on items + /// + [TestMethod] + public void AdditionalMetaDataPreserveExisting() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + TaskItem item = new TaskItem("MyFile.txt"); + item.SetMetadata("MyMetaData", "SomePreserveMeValue"); + + t.Include = new ITaskItem[] { item }; + t.PreserveExistingMetadata = true; + + t.AdditionalMetadata = new string[] { "MyMetaData=SomeValue" }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual("SomePreserveMeValue", t.Include[0].GetMetadata("MyMetaData")); + } + + /// + /// The default is to overwrite existing metadata on items + /// + [TestMethod] + public void AdditionalMetaDataOverwriteExisting() + { + CreateItem t = new CreateItem(); + t.BuildEngine = new MockEngine(); + + TaskItem item = new TaskItem("MyFile.txt"); + item.SetMetadata("MyMetaData", "SomePreserveMeValue"); + + t.Include = new ITaskItem[] { item }; + + // The default for CreateItem is to overwrite any existing metadata + // t.PreserveExistingMetadata = false; + + t.AdditionalMetadata = new string[] { "MyMetaData=SomeOverwriteValue" }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual("SomeOverwriteValue", t.Include[0].GetMetadata("MyMetaData")); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/CreateProperty_Tests.cs b/src/XMakeTasks/UnitTests/CreateProperty_Tests.cs new file mode 100644 index 00000000000..e956ede6d24 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CreateProperty_Tests.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CreateProperty_Tests + { + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + /// + /// Make sure that I can use the CreateProperty task to blank out a property value. + /// + [TestMethod] + public void CreateBlankProperty() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(@" + + + + Twenty + + + + + + + + + + + "); + + logger.AssertLogContains("NumberOfProcessors=''"); + } + + /// + /// Make sure that I can use the CreateProperty task to create a property + /// that has a parseable semicolon in it. + /// + [TestMethod] + public void CreatePropertyWithSemicolon() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(@" + + + + + + + + + + + + + + + "); + + logger.AssertLogContains("TargetsToRunLaterProperty = Clean;Build"); + logger.AssertLogContains("TargetsToRunLaterItem = Clean----Build"); + } + + /// + /// Make sure that I can use the CreateProperty task to create a property + /// that has a literal semicolon in it. + /// + [TestMethod] + public void CreatePropertyWithLiteralSemicolon() + { + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(@" + + + + + + + + + + + + + + + "); + + logger.AssertLogContains("TargetsToRunLaterProperty = Clean;Build"); + logger.AssertLogContains("TargetsToRunLaterItem = Clean;Build"); + } + } +} diff --git a/src/XMakeTasks/UnitTests/CreateVisualBasicManifestResourceName_Tests.cs b/src/XMakeTasks/UnitTests/CreateVisualBasicManifestResourceName_Tests.cs new file mode 100644 index 00000000000..c75fda8dff9 --- /dev/null +++ b/src/XMakeTasks/UnitTests/CreateVisualBasicManifestResourceName_Tests.cs @@ -0,0 +1,667 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class CreateVisualBasicManifestResourceName_Tests + { + /// + /// Test the basic functionality. + /// + [TestMethod] + public void Basic() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, // Link file name + true, + null, // Root namespace + null, + null, + StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"), + null + ); + + Assert.AreEqual("Nested.TestNamespace.TestClass", result); + } + + /// + /// Test a dependent with a relative path + /// + [TestMethod] + public void RelativeDependentUpon() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, // Link file name + true, + null, // Root namespace + null, + null, + StreamHelpers.StringToStream( +@" +Namespace TestNamespace + Class TestClass + End Class +End Namespace +"), + null + + ); + + Assert.AreEqual("TestNamespace.TestClass", result); + } + + /// + /// Test a dependent with a relative path + /// + [TestMethod] + public void AbsoluteDependentUpon() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, // Link file name + true, + null, // Root namespace + null, + null, + StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"), + null + + ); + + Assert.AreEqual("Nested.TestNamespace.TestClass", result); + } + + /// + /// A dependent class plus there is a culture. + /// + [TestMethod] + public void DependentWithCulture() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.en-GB.resx", + null, // Link file name + true, + null, // Root namespace + null, + null, + StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"), + null + + ); + + Assert.AreEqual("Nested.TestNamespace.TestClass.en-GB", result); + } + + /// + /// A dependent class plus there is a culture that was expressed in the metadata of the + /// item rather than the filename. + /// + [TestMethod] + public void DependentWithCultureMetadata() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.resx", + null, // Link file name + true, + null, // Root namespace + null, + "en-GB", + StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"), + null + + ); + + Assert.AreEqual("Nested.TestNamespace.TestClass.en-GB", result); + } + + /// + /// A dependent class plus there is a culture and a root namespace. + /// + [TestMethod] + public void DependentWithCultureAndRootNamespace() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.en-GB.resx", + null, // Link file name + true, + "RootNamespace", + null, + null, + StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"), + null + + ); + + Assert.AreEqual("RootNamespace.Nested.TestNamespace.TestClass.en-GB", result); + } + + /// + /// A dependent class plus there is a culture embedded in the .RESX filename. + /// + [TestMethod] + public void DependentWithEmbeddedCulture() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"f:\myproject\SubFolder\MyForm.fr-fr.resx", + null, // Link file name + true, + "RootNamespace", // Root namespace + null, + null, + StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"), + null + + ); + + Assert.AreEqual("RootNamespace.Nested.TestNamespace.TestClass.fr-fr", result); + } + + /// + /// No dependent class, but there is a root namespace place. Also, the .resx + /// extension contains some upper-case characters. + /// + [TestMethod] + public void RootnamespaceWithCulture() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\MyForm.en-GB.ResX", + null, // Link file name + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + + ); + + Assert.AreEqual("RootNamespace.MyForm.en-GB", result); + } + + /// + /// If there is a link file name then it is preferred over the main file name. + /// + [TestMethod] + public void Regress222308() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"..\..\XmlEditor\Setup\XmlEditor.rgs", + @"MyXmlEditor.rgs", + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + + ); + + Assert.AreEqual("RootNamespace.MyXmlEditor.rgs", result); + } + + /// + /// A non-resx file in a subfolder, with a root namespace. + /// + [TestMethod] + public void BitmapWithRootNamespace() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\SplashScreen.bmp", + null, // Link file name + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + + ); + + Assert.AreEqual("RootNamespace.SplashScreen.bmp", result); + } + + /// + /// A culture-specific non-resx file in a subfolder, with a root namespace. + /// + [TestMethod] + public void CulturedBitmapWithRootNamespace() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\SplashScreen.fr.bmp", + null, // Link file name + true, + "RootNamespace", // Root namespace + null, + null, + null, + null + + ); + + Assert.AreEqual(@"fr\RootNamespace.SplashScreen.bmp", result); + } + + /// + /// A culture-specific non-resx file in a subfolder, with a root namespace, but no culture directory prefix + /// + [TestMethod] + public void CulturedBitmapWithRootNamespaceNoDirectoryPrefix() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\SplashScreen.fr.bmp", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + + ); + + Assert.AreEqual(@"RootNamespace.SplashScreen.bmp", result); + } + + /// + /// If the filename passed in as the "DependentUpon" file doesn't end in .cs then + /// we want to fall back to the RootNamespace+FileName logic. + /// + [TestMethod] + public void Regress188319() + { + CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName(); + + t.BuildEngine = new MockEngine(); + + ITaskItem i = new TaskItem("SR1.resx"); + + i.SetMetadata("BuildAction", "EmbeddedResource"); + i.SetMetadata("DependentUpon", "SR1.strings"); // Normally, this would be a C# file. + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "CustomToolTest"; + + bool success = t.Execute(new Microsoft.Build.Tasks.CreateFileStream(CreateFileStream)); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceNames = t.ManifestResourceNames; + + Assert.AreEqual(1, resourceNames.Length); + Assert.AreEqual(@"CustomToolTest.SR1", resourceNames[0].ItemSpec); + } + + /// + /// Given a file path, return a stream on top of that path. + /// + /// Path to the file + /// File mode + /// Access type + /// The Stream + private Stream CreateFileStream(string path, FileMode mode, FileAccess access) + { + if (String.Compare(path, "SR1.strings", StringComparison.OrdinalIgnoreCase) == 0) + { + return StreamHelpers.StringToStream( +@" +Namespace Nested.TestNamespace + Class TestClass + End Class +End Namespace +"); + } + + Assert.Fail(String.Format("Encountered a new path {0}, needs unittesting support", path)); + return null; + } + + /// + /// If the dependent upon filename and the resource filename both contain what looks like + /// a culture, do not treat it as a culture identifier. E.g.: + /// + /// Form1.ro.resx == DependentUpon ==> Form1.ro.vb + /// + /// In this case, we don't include "ro" as the culture because it's in both filenames. In + /// the case of: + /// + /// Form1.ro.resx == DependentUpon ==> Form1.vb + /// + /// we continue to treat "ro" as the culture. + /// + [TestMethod] + public void Regress419591() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + "MyForm.ro.resx", + null, // Link file name + true, + "RootNamespace", // Root namespace + "MyForm.ro.vb", + null, + StreamHelpers.StringToStream( +@" + Class MyForm + End Class +"), + null + + ); + + Assert.AreEqual("RootNamespace.MyForm", result); + } + + /// + /// If we encounter a class or namespace name within a conditional compilation directive, + /// we need to warn because we do not try to resolve the correct manifest name depending + /// on conditional compilation of code. + /// + [TestMethod] + public void Regress459265() + { + MockEngine m = new MockEngine(); + CreateVisualBasicManifestResourceName c = new CreateVisualBasicManifestResourceName(); + c.BuildEngine = m; + + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + "MyForm.resx", + null, + true, + "RootNamespace", // Root namespace (will be ignored because it's dependent) + "MyForm.vb", + null, + StreamHelpers.StringToStream( +@"Imports System + +#if false +Namespace ClassLibrary1 +#end if +#if Debug +Namespace ClassLibrary2 +#else +Namespace ClassLibrary3 +#end if + Class MyForm + End Class +End Namespace +" + ), + c.Log + ); + + Assert.IsTrue + ( + m.Log.Contains + ( + String.Format(AssemblyResources.GetString("CreateManifestResourceName.DefinitionFoundWithinConditionalDirective"), "MyForm.vb", "MyForm.resx") + ) + ); + } + + /// + /// Tests to ensure that the ResourceFilesWithManifestResourceNames contains everything that + /// the ResourceFiles property on the task contains, but with additional metadata called ManifestResourceName + /// + [TestMethod] + public void ResourceFilesWithManifestResourceNamesContainsAdditionalMetadata() + { + CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("strings.resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"strings.resx", resourceFiles[0].ItemSpec); + Assert.AreEqual(@"ResourceRoot.strings", resourceFiles[0].GetMetadata("ManifestResourceName")); + } + + /// + /// Ensure that if no LogicalName is specified, that the same ManifestResourceName metadata + /// gets applied as LogicalName + /// + [TestMethod] + public void AddLogicalNameForNonResx() + { + CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("pic.bmp"); + i.SetMetadata("Type", "Non-Resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"pic.bmp", resourceFiles[0].ItemSpec); + Assert.AreEqual(@"ResourceRoot.pic.bmp", resourceFiles[0].GetMetadata("LogicalName")); + } + + /// + /// Ensure that a LogicalName that is already present is preserved during manifest name generation + /// + [TestMethod] + public void PreserveLogicalNameForNonResx() + { + CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("pic.bmp"); + i.SetMetadata("LogicalName", "foo"); + i.SetMetadata("Type", "Non-Resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"pic.bmp", resourceFiles[0].ItemSpec); + Assert.AreEqual(@"foo", resourceFiles[0].GetMetadata("LogicalName")); + } + + /// + /// Resx resources should not get ManifestResourceName metadata copied to the LogicalName value + /// + [TestMethod] + public void NoLogicalNameAddedForResx() + { + CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName(); + + t.BuildEngine = new MockEngine(); + ITaskItem i = new TaskItem("strings.resx"); + i.SetMetadata("Type", "Resx"); + + t.ResourceFiles = new ITaskItem[] { i }; + t.RootNamespace = "ResourceRoot"; + bool success = t.Execute(); + + Assert.IsTrue(success, "Expected the task to succceed."); + + ITaskItem[] resourceFiles = t.ResourceFilesWithManifestResourceNames; + + Assert.AreEqual(1, resourceFiles.Length); + Assert.AreEqual(@"strings.resx", resourceFiles[0].ItemSpec); + Assert.AreEqual(String.Empty, resourceFiles[0].GetMetadata("LogicalName")); + } + + /// + /// A culture-specific resources file in a subfolder, with a root namespace + /// + [TestMethod] + public void CulturedResourcesFileWithRootNamespaceWithinSubfolder() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"SubFolder\MyResource.fr.resources", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.MyResource.fr.resources", result); + } + + /// + /// A culture-specific resources file with a root namespace + /// + [TestMethod] + public void CulturedResourcesFileWithRootNamespace() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"MyResource.fr.resources", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.MyResource.fr.resources", result); + } + + /// + /// A non-culture-specific resources file with a root namespace + /// + [TestMethod] + public void ResourcesFileWithRootNamespace() + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + @"MyResource.resources", + null, // Link file name + false, + "RootNamespace", // Root namespace + null, + null, + null, + null + ); + + Assert.AreEqual(@"RootNamespace.MyResource.resources", result); + } + + private void AssertSimpleCase(string code, string expected) + { + string result = + CreateVisualBasicManifestResourceName.CreateManifestNameImpl + ( + "MyForm.resx", + null, // Link file name + true, + "RootNamespace", // Root namespace + "MyForm.vb", + null, + StreamHelpers.StringToStream(code), + null + ); + + Assert.AreEqual(expected, result); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/Csc_Tests.cs b/src/XMakeTasks/UnitTests/Csc_Tests.cs new file mode 100644 index 00000000000..532c04a5353 --- /dev/null +++ b/src/XMakeTasks/UnitTests/Csc_Tests.cs @@ -0,0 +1,1097 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: CscTests + * + * Test the Csc task in various ways. + * + */ + [TestClass] + sealed public class CscTests + { + /// + /// Tests the "References" parameter on the Csc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Csc task + /// supports assembly aliases, so we want to make sure that we pass an assembly + /// alias into csc.exe. + /// + [TestMethod] + public void SingleAliasOnAReference() + { + Csc t = new Csc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + reference.SetMetadata("Aliases", "Foo"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:Foo=System.Xml.dll"); + } + + /// + /// Tests the "References" parameter on the Csc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Csc task + /// supports assembly aliases, so we want to make sure that we pass an assembly + /// alias into csc.exe. + /// + [TestMethod] + public void SingleAliasUnicodeOnAReference() + { + Csc t = new Csc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + reference.SetMetadata("Aliases", "?"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:?=System.Xml.dll"); + } + + + /// + /// Tests the "References" parameter on the Csc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Csc task + /// supports assembly aliases, so we want to make sure that we pass an assembly + /// alias into csc.exe. + /// + [TestMethod] + public void MultipleAliasesOnAReference() + { + Csc t = new Csc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + reference.SetMetadata("Aliases", "Foo, Bar"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:Foo=System.Xml.dll"); + CommandLine.ValidateHasParameter(t, "/reference:Bar=System.Xml.dll"); + } + + /// + /// Tests the "References" parameter on the Csc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Csc task + /// supports assembly aliases, so we want to make sure that we pass an assembly + /// alias into csc.exe. + /// + [TestMethod] + public void NonAliasedReference1() + { + Csc t = new Csc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + reference.SetMetadata("Aliases", "global"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:System.Xml.dll"); + } + + /// + /// Tests the "References" parameter on the Csc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Csc task + /// supports assembly aliases, so we want to make sure that we pass an assembly + /// alias into csc.exe. + /// + [TestMethod] + public void NonAliasedReference2() + { + Csc t = new Csc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:System.Xml.dll"); + } + + /// + /// Tests the "References" parameter on the Csc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Csc task + /// supports assembly aliases, so we want to make sure that we pass an assembly + /// alias into csc.exe. + /// + [TestMethod] + public void GlobalAndExplicitAliasOnAReference() + { + Csc t = new Csc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + reference.SetMetadata("Aliases", "global , Foo"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:System.Xml.dll"); + CommandLine.ValidateHasParameter(t, "/reference:Foo=System.Xml.dll"); + } + + // Tests the "DefineConstants" parameter on the Csc task. The task actually + // needs to slightly munge the string that was passed in from the project file, + // in order to maintain compatibility with VS. + [TestMethod] + public void DefineConstants() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc t = new Csc(); + t.BuildEngine = mockEngine; + + // Perfectly valid, so no change expected. + Assert.AreEqual("DEBUG;TRACE", + t.GetDefineConstantsSwitch("DEBUG;TRACE")); + + // Spaces should be removed. + Assert.AreEqual("DEBUG;TRACE", + t.GetDefineConstantsSwitch("DEBUG; TRACE")); + + // Commas become semicolons. + Assert.AreEqual("DEBUG;TRACE", + t.GetDefineConstantsSwitch("DEBUG , TRACE")); + + // We ignore anything that has quotes. + Assert.AreEqual("DEBUG", + t.GetDefineConstantsSwitch("DEBUG , \"TRACE\"")); + + // We ignore anything that has an equals sign. + Assert.AreEqual("DEBUG;TRACE", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE=MYVALUE")); + + // Since we split on space and comma, what seems like a value actually + // becomes a new constant. Yes, this is really what happens in + // Everett VS. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE;MYVALUE", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE = MYVALUE")); + + // Since we split on space and comma/semicolon, what seems like a value actually + // becomes a new constant. Yes, this is really what happens in + // Everett VS. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE;MY;VALUE", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE = MY VALUE")); + + // Even if the comma is inside quotes, we still split on it. Yup, this + // is what VS did in Everett. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE;WEIRD", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE = \"MY,WEIRD,VALUE\"")); + + // Once again, quotes aren't allowed. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE = \"MY VALUE\"")); + + // (,),@,%,$ aren't allowed, and spaces are a valid delimiter. + Assert.AreEqual("DEBUG;TRACE;a;b", + t.GetDefineConstantsSwitch("DEBUG;TRACE;a b;(;);@;%;$")); + + // Dash is not allowed either. It's not a valid character in an + // identifier. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE = -1")); + + // Identifiers cannot begin with numbers. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE;123ABC")); + + // But identifiers can contain numbers. + Assert.AreEqual("DEBUG;TRACE;MYDEFINE;ABC123", + t.GetDefineConstantsSwitch("DEBUG,TRACE,MYDEFINE;ABC123")); + + // Identifiers can contain initial underscores and embedded underscores. + Assert.AreEqual("_DEBUG;MY_DEFINE", + t.GetDefineConstantsSwitch("_DEBUG, MY_DEFINE")); + + // We should get back "null" if there's nothing valid in there. + Assert.AreEqual(null, + t.GetDefineConstantsSwitch("DEBUG=\"myvalue\"")); + } + + // Tests the "DebugType" and "EmitDebuggingInformation" parameters on the Csc task, + // and confirms that it sets the /debug switch on the command-line correctly. + [TestMethod] + public void DebugType() + { + Csc t = new Csc(); + + t.DebugType = "pdbonly"; + t.EmitDebugInformation = true; + + int firstParamLocation = CommandLine.ValidateHasParameter(t, "/debug+"); + int secondParamLocation = CommandLine.ValidateHasParameter(t, "/debug:pdbonly"); + + Assert.IsTrue(secondParamLocation > firstParamLocation, "The order of the /debug switches is incorrect."); + } + + // Tests the "LangVersion" parameter on the Csc task, and confirms that it sets + // the /langversion switch on the command-line correctly. + [TestMethod] + public void LangVersion() + { + Csc t = new Csc(); + + t.LangVersion = "v7.1"; + CommandLine.ValidateHasParameter(t, @"/langversion:v7.1"); + } + + // Tests the "AdditionalLibPaths" parameter on the Csc task, and confirms that it sets + // the /lib switch on the command-line correctly. + [TestMethod] + public void AdditionaLibPaths() + { + Csc t = new Csc(); + + t.AdditionalLibPaths = new string[] { @"c:\xmake\", @"c:\msbuild" }; + CommandLine.ValidateHasParameter(t, @"/lib:c:\xmake\,c:\msbuild"); + } + + // Tests the "PreferredUILang" parameter on the Csc task, and confirms that it sets + // the /preferreduilang switch on the command-line correctly. + [TestMethod] + public void PreferredUILang() + { + Csc t = new Csc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/preferreduilang:"); + + t.PreferredUILang = "en-US"; + CommandLine.ValidateHasParameter(t, @"/preferreduilang:en-US"); + } + + // Tests the "Platform" parameter on the Csc task, and confirms that it sets + // the /platform switch on the command-line correctly. + [TestMethod] + public void Platform() + { + Csc t = new Csc(); + + t.Platform = "x86"; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + } + + // Tests the "Platform" and "Prefer32Bit" parameter combinations on the Csc task, + // and confirms that it sets the /platform switch on the command-line correctly. + [TestMethod] + public void PlatformAndPrefer32Bit() + { + // Implicit "anycpu" + Csc t = new Csc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/platform:"); + t = new Csc(); + t.Prefer32Bit = false; + CommandLine.ValidateNoParameterStartsWith(t, @"/platform:"); + t = new Csc(); + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu32bitpreferred"); + + // Explicit "anycpu" + t = new Csc(); + t.Platform = "anycpu"; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu"); + t = new Csc(); + t.Platform = "anycpu"; + t.Prefer32Bit = false; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu"); + t = new Csc(); + t.Platform = "anycpu"; + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu32bitpreferred"); + + // Explicit "x86" + t = new Csc(); + t.Platform = "x86"; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + t = new Csc(); + t.Platform = "x86"; + t.Prefer32Bit = false; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + t = new Csc(); + t.Platform = "x86"; + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + } + + // Tests the "HighEntropyVA" parameter on the Csc task, and confirms that it + // sets the /highentropyva switch on the command-line correctly. + [TestMethod] + public void HighEntropyVA() + { + // Implicit /highentropyva- + Csc t = new Csc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/highentropyva"); + + // Explicit /highentropyva- + t = new Csc(); + t.HighEntropyVA = false; + CommandLine.ValidateHasParameter(t, @"/highentropyva-"); + + // Explicit /highentropyva+ + t = new Csc(); + t.HighEntropyVA = true; + CommandLine.ValidateHasParameter(t, @"/highentropyva+"); + } + + // Tests the "PdbFile" parameter on the Csc task, and confirms that it sets + // the /pdb switch on the command-line correctly. + [TestMethod] + public void Pdb() + { + Csc t = new Csc(); + + t.PdbFile = "foo.pdb"; + CommandLine.ValidateHasParameter(t, @"/pdb:foo.pdb"); + } + + // Tests the "SubsystemVersion" parameter on the Csc task, and confirms that it sets + // the /subsystemversion switch on the command-line correctly. + [TestMethod] + public void SubsystemVersion() + { + Csc t = new Csc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/subsystemversion"); + + t = new Csc(); + t.SubsystemVersion = "4.0"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:4.0"); + + t = new Csc(); + t.SubsystemVersion = "5"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:5"); + + t = new Csc(); + t.SubsystemVersion = "6.02"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:6.02"); + + t = new Csc(); + t.SubsystemVersion = "garbage"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:garbage"); + } + + // Tests the "ApplicationConfiguration" parameter on the Csc task, and confirms that it sets + // the /appconfig switch on the command-line correctly. + [TestMethod] + public void ApplicationConfiguration() + { + Csc t = new Csc(); + + t.ApplicationConfiguration = "ConsoleApplication1.exe.config"; + CommandLine.ValidateHasParameter(t, @"/appconfig:ConsoleApplication1.exe.config"); + } + + // Tests the "UnsafeBlocks" parameter on the Csc task, and confirms that it sets + // the /unsafe switch on the command-line correctly. + [TestMethod] + public void UnsafeBlocks() + { + Csc t = new Csc(); + t.AllowUnsafeBlocks = true; + CommandLine.ValidateHasParameter(t, "/unsafe+"); + t.AllowUnsafeBlocks = false; + CommandLine.ValidateHasParameter(t, "/unsafe-"); + } + + // Tests the "WarningsAsErrors" parameter on the Csc task, and confirms that it sets + // the /warnaserror switch on the command-line correctly. + [TestMethod] + public void WarningsAsErrors() + { + Csc t = new Csc(); + + t.WarningsAsErrors = "1234 ;5678"; + t.TreatWarningsAsErrors = false; + + int firstParamLocation = CommandLine.ValidateHasParameter(t, "/warnaserror-"); + int secondParamLocation = CommandLine.ValidateHasParameter(t, "/warnaserror+:1234,5678"); + + Assert.IsTrue(secondParamLocation > firstParamLocation, "The order of the /warnaserror switches is incorrect."); + } + + // Check all parameters that are based on ints, bools and other value types. + // This is because parameters with these types go through a not-so-typesafe check + // for existence in the property bag. + [TestMethod] + public void FlagsAndInts() + { + Csc t = new Csc(); + + // From managed compiler + t.CodePage = 5; + t.EmitDebugInformation = true; + t.DelaySign = true; + t.FileAlignment = 9; + t.NoLogo = true; + t.Optimize = true; + t.TreatWarningsAsErrors = true; + t.Utf8Output = true; + + // From csc. + t.AllowUnsafeBlocks = true; + t.CheckForOverflowUnderflow = true; + t.GenerateFullPaths = true; + t.NoStandardLib = true; + t.WarningLevel = 5; + + // Check the parameters. + CommandLine.ValidateHasParameter(t, "/codepage:5"); + CommandLine.ValidateHasParameter(t, "/debug+"); + CommandLine.ValidateHasParameter(t, "/delaysign+"); + CommandLine.ValidateHasParameter(t, "/filealign:9"); + CommandLine.ValidateHasParameter(t, "/nologo"); + CommandLine.ValidateHasParameter(t, "/optimize+"); + CommandLine.ValidateHasParameter(t, "/warnaserror+"); + CommandLine.ValidateHasParameter(t, "/utf8output"); + CommandLine.ValidateHasParameter(t, "/unsafe+"); + CommandLine.ValidateHasParameter(t, "/checked+"); + CommandLine.ValidateHasParameter(t, "/fullpaths"); + CommandLine.ValidateHasParameter(t, "/warn:5"); + } + + + [TestMethod] + [Ignore] + public void CscHostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + MockCscHostObject cscHostObject = new MockCscHostObject(); + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsTrue(!cscHostObject.CompileMethodWasCalled); + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.IsTrue(cscHostObject.CompileMethodWasCalled); + } + + [TestMethod] + [Ignore] + public void CscHostObject2() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + MockCscHostObject2 cscHostObject = new MockCscHostObject2(); + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsTrue(!cscHostObject.CompileMethodWasCalled); + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.IsTrue(cscHostObject.CompileMethodWasCalled); + } + + [TestMethod] + [Ignore] + public void CscHostObject3() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + MockCscHostObject3 cscHostObject = new MockCscHostObject3(); + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsTrue(!cscHostObject.CompileMethodWasCalled); + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.IsTrue(cscHostObject.CompileMethodWasCalled); + } + + [TestMethod] + public void CscHostObjectNotUsedIfToolNameSpecified() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + MockCscHostObject cscHostObject = new MockCscHostObject(); + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + csc.ToolExe = "csc_custom.exe"; + + Assert.IsTrue(csc.UseAlternateCommandLineToolToExecute()); + } + + [TestMethod] + public void CscHostObjectNotUsedIfToolPathSpecified() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + MockCscHostObject cscHostObject = new MockCscHostObject(); + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + csc.ToolPath = "c:\\some\\custom\\path"; + + Assert.IsTrue(csc.UseAlternateCommandLineToolToExecute()); + } + + + /* + * Class: CscCrossParameterInjection + * + * Test the Csc task for cases where the parameters are passed in + * with with whitespace and other special characters to try to fool + * us into spawning Csc.exe while bypassing security + */ + [TestClass] + sealed public class CscCrossParameterInjection + { + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void Win32IconEmbeddedQuote() + { + Csc t = new Csc(); + t.Win32Icon = "MyFile.ico\\\" /out:c:\\windows\\system32\\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void Sources() + { + Csc t = new Csc(); + + t.Sources = new TaskItem[] + { + new TaskItem("parm0.cs"), + new TaskItem("parm1 /out:c:\\windows\\system32\\notepad.exe") + }; + + // If sources are specified, but not OutputAssembly is specified then the Csc task + // will create an OutputAssembly that is the name of the first source file with the + // extension replaced with .exe. + CommandLine.ValidateHasParameter(t, "/out:parm0.exe"); + + // Still, we don't want and additional /out added. + CommandLine.ValidateNoParameterStartsWith(t, "/out", "/out:parm0.exe"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void SourcesEmbeddedQuote() + { + Csc t = new Csc(); + + // Embedded quotes could be used to escape from our quoting. We throw an exception + // when this happens. + t.Sources = new TaskItem[] + { + new TaskItem("parm0\\\" /out:c:\\windows\\system32\\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void Win32Icon() + { + Csc t = new Csc(); + t.Win32Icon = @"MyFile.ico /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + CommandLine.ValidateHasParameter(t, @"/win32icon:MyFile.ico /out:c:\windows\system32\notepad.exe"); + } + + [TestMethod] + public void AdditionalLibPaths() + { + Csc t = new Csc(); + t.AdditionalLibPaths = new string[] { @"parm /out:c:\windows\system32\notepad.exe" }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void AddModules() + { + Csc t = new Csc(); + t.AddModules = new string[] { @"parm /out:c:\windows\system32\notepad.exe" }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void BaseAddress() + { + Csc t = new Csc(); + t.BaseAddress = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void DebugType() + { + Csc t = new Csc(); + t.DebugType = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + [Ignore] // "Because constants may legitimately contains quotes _and_ we've cut security, we decided to let DefineConstants be passed through literally." + public void DefineConstants() + { + Csc t = new Csc(); + t.DefineConstants = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void DisabledWarnings() + { + Csc t = new Csc(); + t.DisabledWarnings = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void DocumentationFile() + { + Csc t = new Csc(); + t.DocumentationFile = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void KeyContainer() + { + Csc t = new Csc(); + t.KeyContainer = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void KeyFile() + { + Csc t = new Csc(); + t.KeyFile = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void LinkResources() + { + Csc t = new Csc(); + t.KeyFile = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void MainEntryPoint() + { + Csc t = new Csc(); + t.MainEntryPoint = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void OutputAssembly() + { + Csc t = new Csc(); + t.OutputAssembly = new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe"); + CommandLine.ValidateHasParameter(t, @"/out:parm1 /out:c:\windows\system32\notepad.exe"); + } + + [TestMethod] + public void References() + { + Csc t = new Csc(); + t.References = new TaskItem[] + { + new TaskItem("parm0"), + new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void Resources() + { + Csc t = new Csc(); + t.References = new TaskItem[] + { + new TaskItem("parm0"), + new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void ResponseFiles() + { + Csc t = new Csc(); + t.ResponseFiles = new TaskItem[] + { + new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void TargetType() + { + Csc t = new Csc(); + t.TargetType = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void ToolPath() + { + Csc t = new Csc(); + t.ToolPath = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void WarningsAsErrors() + { + Csc t = new Csc(); + t.WarningsAsErrors = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + + [TestMethod] + public void Win32Resource() + { + Csc t = new Csc(); + t.Win32Resource = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + } + + [TestMethod] + public void MultipleResponseFiles() + { + Csc t = new Csc(); + t.ResponseFiles = new TaskItem[] + { + new TaskItem(@"1.rsp"), + new TaskItem(@"2.rsp"), + new TaskItem(@"3.rsp"), + new TaskItem(@"4.rsp") + }; + CommandLine.ValidateContains(t, "@1.rsp @2.rsp @3.rsp @4.rsp", true); + } + + [TestMethod] + public void SingleResponseFile() + { + Csc t = new Csc(); + t.ResponseFiles = new TaskItem[] + { + new TaskItem(@"1.rsp") + }; + CommandLine.ValidateHasParameter(t, "@1.rsp"); + } + + [TestMethod] + public void NoAnalyzers_CommandLine() + { + Csc csc = new Csc(); + + CommandLine.ValidateNoParameterStartsWith(csc, "/analyzer"); + } + + [TestMethod] + public void Analyzer_CommandLine() + { + Csc csc = new Csc(); + csc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll") + }; + + CommandLine.ValidateHasParameter(csc, "/analyzer:Foo.dll"); + } + + [TestMethod] + public void MultipleAnalyzers_CommandLine() + { + Csc csc = new Csc(); + csc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll"), + new TaskItem("Bar.dll") + }; + + CommandLine.ValidateHasParameter(csc, "/analyzer:Foo.dll"); + CommandLine.ValidateHasParameter(csc, "/analyzer:Bar.dll"); + } + + [TestMethod] + public void NoAnalyzer_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.Analyzers); + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.IsNull(cscHostObject.Analyzers); + } + + [TestMethod] + public void Analyzer_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.Analyzers); + + csc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll") + }; + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.AreEqual(1, cscHostObject.Analyzers.Length); + Assert.AreEqual("Foo.dll", cscHostObject.Analyzers[0].ItemSpec); + } + + [TestMethod] + public void MultipleAnalyzers_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.Analyzers); + + csc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll"), + new TaskItem("Bar.dll") + }; + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + + Assert.AreEqual(2, cscHostObject.Analyzers.Length); + Assert.AreEqual("Foo.dll", cscHostObject.Analyzers[0].ItemSpec); + Assert.AreEqual("Bar.dll", cscHostObject.Analyzers[1].ItemSpec); + } + + [TestMethod] + public void NoRuleSet_CommandLine() + { + Csc csc = new Csc(); + + CommandLine.ValidateNoParameterStartsWith(csc, "/ruleset"); + } + + [TestMethod] + public void RuleSet_CommandLine() + { + Csc csc = new Csc(); + csc.CodeAnalysisRuleSet = "Bar.ruleset"; + + CommandLine.ValidateHasParameter(csc, "/ruleset:Bar.ruleset"); + } + + [TestMethod] + public void NoRuleSet_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.RuleSet); + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.IsNull(cscHostObject.RuleSet); + } + + [TestMethod] + public void RuleSet_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.RuleSet); + + csc.CodeAnalysisRuleSet = "Bar.ruleset"; + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.AreEqual("Bar.ruleset", cscHostObject.RuleSet); + } + + [TestMethod] + public void NoAdditionalFiles_CommandLine() + { + Csc csc = new Csc(); + + CommandLine.ValidateNoParameterStartsWith(csc, "/additionalfile"); + } + + [TestMethod] + public void AdditionalFiles_CommandLine() + { + Csc csc = new Csc(); + csc.AdditionalFiles = new TaskItem[] + { + new TaskItem("web.config") + }; + + CommandLine.ValidateHasParameter(csc, "/additionalfile:web.config"); + } + + [TestMethod] + public void MultipleAdditionalFiles_CommandLine() + { + Csc csc = new Csc(); + csc.AdditionalFiles = new TaskItem[] + { + new TaskItem("app.config"), + new TaskItem("web.config") + }; + + CommandLine.ValidateHasParameter(csc, "/additionalfile:app.config"); + CommandLine.ValidateHasParameter(csc, "/additionalfile:web.config"); + } + + [TestMethod] + public void NoAdditionalFile_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.AdditionalFiles); + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.IsNull(cscHostObject.AdditionalFiles); + } + + [TestMethod] + public void AdditionalFile_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.AdditionalFiles); + + csc.AdditionalFiles = new TaskItem[] + { + new TaskItem("web.config") + }; + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + Assert.AreEqual(1, cscHostObject.AdditionalFiles.Length); + Assert.AreEqual("web.config", cscHostObject.AdditionalFiles[0].ItemSpec); + } + + [TestMethod] + public void MultipleAdditionalFiles_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Csc csc = new Csc(); + csc.BuildEngine = mockEngine; + + MockCscAnalyzerHostObject cscHostObject = new MockCscAnalyzerHostObject(); + cscHostObject.SetDesignTime(true); + + csc.HostObject = cscHostObject; + csc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(cscHostObject.AdditionalFiles); + + csc.AdditionalFiles = new TaskItem[] + { + new TaskItem("web.config"), + new TaskItem("app.config") + }; + + csc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool cscSuccess = csc.Execute(); + + Assert.IsTrue(cscSuccess, "Csc task failed."); + + Assert.AreEqual(2, cscHostObject.AdditionalFiles.Length); + Assert.AreEqual("web.config", cscHostObject.AdditionalFiles[0].ItemSpec); + Assert.AreEqual("app.config", cscHostObject.AdditionalFiles[1].ItemSpec); + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/Culture_Tests.cs b/src/XMakeTasks/UnitTests/Culture_Tests.cs new file mode 100644 index 00000000000..f8cb7a22902 --- /dev/null +++ b/src/XMakeTasks/UnitTests/Culture_Tests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class Culture_Tests + { + /* + * Method: Basic + * + * Test the basic functionality. + */ + [TestMethod] + public void Basic() + { + Culture.ItemCultureInfo info = Culture.GetItemCultureInfo("MyResource.fr.resx", null); + Assert.AreEqual("fr", info.culture); + Assert.AreEqual("MyResource.resx", info.cultureNeutralFilename); + } + + /* + * Method: NonCultureFile + * + * The item doesn't have a culture, and there isn't one embedded in the file name. + */ + [TestMethod] + public void NonCultureFile() + { + Culture.ItemCultureInfo info = Culture.GetItemCultureInfo("MyResource.resx", null); + Assert.AreEqual(null, info.culture); + Assert.AreEqual("MyResource.resx", info.cultureNeutralFilename); + } + + + /* + * Method: BogusEmbeddedCulture + * + * The item has something that looks like an embedded culture, but isn't. + */ + [TestMethod] + public void BogusEmbeddedCulture() + { + Culture.ItemCultureInfo info = Culture.GetItemCultureInfo("MyResource.notalocale.resx", null); + Assert.AreEqual(null, info.culture); + Assert.AreEqual("MyResource.notalocale.resx", info.cultureNeutralFilename); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/Delete_Tests.cs b/src/XMakeTasks/UnitTests/Delete_Tests.cs new file mode 100644 index 00000000000..e4582206cad --- /dev/null +++ b/src/XMakeTasks/UnitTests/Delete_Tests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class Delete_Tests + { + /* + * Method: AttributeForwarding + * + * Make sure that attributes set on input items are forwarded to ouput items. + */ + [TestMethod] + public void AttributeForwarding() + { + Delete t = new Delete(); + + ITaskItem i = new TaskItem("MyFiles.nonexistent"); + i.SetMetadata("Locale", "en-GB"); + t.Files = new ITaskItem[] { i }; + t.BuildEngine = new MockEngine(); + + t.Execute(); + + Assert.AreEqual("en-GB", t.DeletedFiles[0].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual("MyFiles.nonexistent", t.DeletedFiles[0].ItemSpec); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/DependentAssembly_Tests.cs b/src/XMakeTasks/UnitTests/DependentAssembly_Tests.cs new file mode 100644 index 00000000000..26a5fa853e1 --- /dev/null +++ b/src/XMakeTasks/UnitTests/DependentAssembly_Tests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Xml; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class DependentAssembly_Tests + { + /// + /// Verify that a reference without a public key works correctly + /// + [TestMethod] + public void SerializeDeserialize() + { + DependentAssembly dependentAssembly = new DependentAssembly(); + + string xml = ""; + + dependentAssembly.Read(new XmlTextReader(xml, XmlNodeType.Document, null)); + + Assert.IsTrue(dependentAssembly.PartialAssemblyName != null); + } + } +} diff --git a/src/XMakeTasks/UnitTests/ErrorWarningMessage_Tests.cs b/src/XMakeTasks/UnitTests/ErrorWarningMessage_Tests.cs new file mode 100644 index 00000000000..75927926681 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ErrorWarningMessage_Tests.cs @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Exceptions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ErrorWarningMessage_Tests + { + /// + /// Simple case + /// + [TestMethod] + public void Message() + { + MockEngine e = new MockEngine(); + Message m = new Message(); + m.BuildEngine = e; + + m.Text = "messagetext"; + + bool retval = m.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(retval); + Assert.IsTrue(e.Log.IndexOf("messagetext") != -1); + } + + /// + /// Multiple lines + /// + [TestMethod] + public void MultilineMessage() + { + MockEngine e = new MockEngine(); + Message m = new Message(); + m.BuildEngine = e; + + m.Text = "messagetext\n messagetext2 \n\nmessagetext3"; + + bool retval = m.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(retval); + Assert.IsTrue(e.Log.IndexOf("messagetext\n messagetext2 \n\nmessagetext3") != -1); + } + + /// + /// Empty message should not log an event + /// + [TestMethod] + public void EmptyMessage() + { + MockEngine e = new MockEngine(); + Message m = new Message(); + m.BuildEngine = e; + + // don't set text + + bool retval = m.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(retval); + Assert.IsTrue(e.Messages == 0); + } + + /// + /// Simple case + /// + [TestMethod] + public void Warning() + { + MockEngine e = new MockEngine(true); + Warning w = new Warning(); + w.BuildEngine = e; + + w.Text = "warningtext"; + w.File = "c:\\file"; + + bool retval = w.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(retval); + e.AssertLogContains("c:\\file(0,0): WARNING : warningtext"); + Assert.IsTrue(e.Warnings == 1); + } + + /// + /// Empty warning should not log an event + /// + [TestMethod] + public void EmptyWarning() + { + MockEngine e = new MockEngine(); + Warning w = new Warning(); + w.BuildEngine = e; + + // don't set text + + bool retval = w.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(retval); + Assert.IsTrue(e.Warnings == 0); + } + + /// + /// Empty warning message but a code specified should still be logged + /// + [TestMethod] + public void EmptyWarningMessageButCodeSpecified() + { + MockEngine e = new MockEngine(); + Warning w = new Warning(); + w.BuildEngine = e; + + // don't set text + w.Code = "123"; + + bool retval = w.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(retval); + Assert.IsTrue(e.Warnings == 1); + } + + /// + /// Empty error should not log an event + /// + [TestMethod] + public void EmptyError() + { + MockEngine e = new MockEngine(); + Error err = new Error(); + err.BuildEngine = e; + + // don't set text + + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(false == retval); + Assert.IsTrue(e.Errors == 0); + } + + /// + /// Empty error message but a code specified should still be logged + /// + [TestMethod] + public void EmptyErrorMessageButCodeSpecified() + { + MockEngine e = new MockEngine(); + Error err = new Error(); + err.BuildEngine = e; + + // don't set text + err.Code = "999"; + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(false == retval); + Assert.IsTrue(e.Errors == 1); + } + + /// + /// Simple case + /// + [TestMethod] + public void Error() + { + MockEngine e = new MockEngine(true); + Error err = new Error(); + err.BuildEngine = e; + + err.Text = "errortext"; + err.File = "c:\\file"; + + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsTrue(false == retval); + e.AssertLogContains("c:\\file(0,0): ERROR : errortext"); + Assert.IsTrue(e.Errors == 1); + } + + /// + /// Simple case for error message coming from a resource string + /// + [TestMethod] + public void ErrorFromResources() + { + MockEngine e = new MockEngine(true); + ErrorFromResources err = new ErrorFromResources(); + err.BuildEngine = e; + + err.Resource = "Exec.MissingCommandError"; + + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsFalse(retval); + + string message = AssemblyResources.GetString(err.Resource); + e.AssertLogContains(message); + Assert.IsTrue(e.Errors == 1); + } + + /// + /// If a "Code" is passed to the task, use it to override the code + /// (if any) defined in the error message. + /// + [TestMethod] + public void ErrorFromResourcesWithOverriddenCode() + { + MockEngine e = new MockEngine(true); + ErrorFromResources err = new ErrorFromResources(); + err.BuildEngine = e; + + err.Resource = "Exec.MissingCommandError"; + err.Code = "ABC1234"; + + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsFalse(retval); + + string message = AssemblyResources.GetString(err.Resource); + string updatedMessage = message.Replace("MSB3072", "ABC1234"); + e.AssertLogContains(updatedMessage); + Assert.IsTrue(e.Errors == 1); + } + + /// + /// Simple case of logging a resource-based error that takes + /// arguments + /// + [TestMethod] + public void ErrorFromResourcesWithArguments() + { + MockEngine e = new MockEngine(true); + ErrorFromResources err = new ErrorFromResources(); + err.BuildEngine = e; + + err.Resource = "Copy.Error"; + err.Arguments = new string[] { "a.txt", "b.txt", "xyz" }; + + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsFalse(retval); + + string message = String.Format(AssemblyResources.GetString(err.Resource), err.Arguments); + e.AssertLogContains(message); + Assert.IsTrue(e.Errors == 1); + } + + /// + /// If invalid arguments are passed to the task, it should still + /// log an error informing the user of that. + /// + [TestMethod] + public void ErrorFromResourcesWithInvalidArguments() + { + MockEngine e = new MockEngine(true); + ErrorFromResources err = new ErrorFromResources(); + err.BuildEngine = e; + + err.Resource = "Copy.Error"; + err.Arguments = new string[] { "a.txt", "b.txt" }; + + bool retval = err.Execute(); + + Console.WriteLine("==="); + Console.WriteLine(e.Log); + Console.WriteLine("==="); + + Assert.IsFalse(retval); + + e.AssertLogDoesntContain("a.txt"); + e.AssertLogContains("MSB3861"); + Assert.IsTrue(e.Errors == 1); + } + + /// + /// If no resource string is passed to ErrorFromResources, we should error + /// because a required parameter is missing. + /// + [TestMethod] + public void ErrorFromResourcesNoResources() + { + string projectContents = @" + + + + + +"; + + MockLogger logger = ObjectModelHelpers.BuildProjectExpectFailure(projectContents); + + // missing required parameter + logger.AssertLogContains("MSB4044"); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/Exec_Tests.cs b/src/XMakeTasks/UnitTests/Exec_Tests.cs new file mode 100644 index 00000000000..c0eef116beb --- /dev/null +++ b/src/XMakeTasks/UnitTests/Exec_Tests.cs @@ -0,0 +1,587 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for the Exec task + /// + [TestClass] + sealed public class Exec_Tests + { + private Exec PrepareExec(string command) + { + IBuildEngine2 mockEngine = new MockEngine(true); + Exec exec = new Exec(); + exec.BuildEngine = mockEngine; + exec.Command = command; + return exec; + } + + private ExecWrapper PrepareExecWrapper(string command) + { + IBuildEngine2 mockEngine = new MockEngine(true); + ExecWrapper exec = new ExecWrapper(); + exec.BuildEngine = mockEngine; + exec.Command = command; + return exec; + } + + /// + /// Ensures that calling the Exec task does not leave any extra TEMP files + /// lying around. + /// + [TestMethod] + public void NoTempFileLeaks() + { + // Get a count of how many temp files there are right now. + string tempPath = Path.GetTempPath(); + string[] tempFiles = Directory.GetFiles(tempPath); + int originalTempFileCount = tempFiles.Length; + + // Now run the Exec task on a simple command. + Exec exec = PrepareExec("echo Four days 'till ZBB!"); + bool result = exec.Execute(); + + // Get the new count of temp files. + tempFiles = Directory.GetFiles(tempPath); + int newTempFileCount = tempFiles.Length; + + // Ensure that Exec succeeded. + Assert.IsTrue(result); + // Ensure the new temp file count equals the old temp file count. + Assert.AreEqual(originalTempFileCount, newTempFileCount); + } + + [TestMethod] + public void ExitCodeCausesFailure() + { + Exec exec = PrepareExec("xcopy thisisanonexistentfile"); + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(4, exec.ExitCode); + ((MockEngine)exec.BuildEngine).AssertLogContains("MSB3073"); + } + + [TestMethod] + [Ignore] // "Timing issue found on RI candidate from ToolPlat to Main, disabling for RI only." + public void Timeout() + { + Exec exec = PrepareExec(":foo \n goto foo"); + exec.Timeout = 5; + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(-1, exec.ExitCode); + ((MockEngine)exec.BuildEngine).AssertLogContains("MSB5002"); + Assert.AreEqual(1, ((MockEngine)exec.BuildEngine).Warnings); + Assert.AreEqual(1, ((MockEngine)exec.BuildEngine).Errors); + } + + [TestMethod] + public void ExitCodeGetter() + { + Exec exec = PrepareExec("exit 666"); + bool result = exec.Execute(); + + Assert.AreEqual(666, exec.ExitCode); + } + + [TestMethod] + public void LoggedErrorsCauseFailureDespiteExitCode0() + { + // This will return 0 exit code, but emitted a canonical error + Exec exec = PrepareExec("echo myfile(88,37): error AB1234: thisisacanonicalerror"); + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + // Exitcode is set to -1 + Assert.AreEqual(-1, exec.ExitCode); + ((MockEngine)exec.BuildEngine).AssertLogContains("MSB3073"); + } + + [TestMethod] + public void IgnoreExitCodeTrueMakesTaskSucceedDespiteLoggingErrors() + { + Exec exec = PrepareExec("echo myfile(88,37): error AB1234: thisisacanonicalerror"); + exec.IgnoreExitCode = true; + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + } + + [TestMethod] + public void IgnoreExitCodeTrueMakesTaskSucceedDespiteExitCode1() + { + Exec exec = PrepareExec("dir ||invalid||"); + exec.IgnoreExitCode = true; + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + } + + [TestMethod] + public void NonUNCWorkingDirectoryUsed() + { + Exec exec = PrepareExec("echo [%cd%]"); + string working = Environment.GetFolderPath(Environment.SpecialFolder.Windows); // not desktop etc - IT redirection messes it up + exec.WorkingDirectory = working; + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("[" + working + "]"); + } + + [TestMethod] + public void UNCWorkingDirectoryUsed() + { + Exec exec = PrepareExec("echo [%cd%]"); + string working = @"\\" + Environment.MachineName + @"\c$"; + exec.WorkingDirectory = working; + bool result = exec.ValidateParametersAccessor(); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, exec.workingDirectoryIsUNC); + Assert.AreEqual(working, exec.WorkingDirectory); + // Should give ToolTask the system folder as the working directory, when it's a UNC + string system = Environment.GetFolderPath(Environment.SpecialFolder.System); + Assert.AreEqual(system, exec.GetWorkingDirectoryAccessor()); + } + + [TestMethod] + public void NoWorkingDirectorySet() + { + var cd = Directory.GetCurrentDirectory(); + + try + { + Directory.SetCurrentDirectory(Environment.GetFolderPath(Environment.SpecialFolder.Windows)); + + Exec exec = PrepareExec("echo [%cd%]"); + bool result = exec.Execute(); + + string expected = Directory.GetCurrentDirectory(); + Assert.AreEqual(true, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("[" + expected + "]"); + } + finally + { + Directory.SetCurrentDirectory(cd); + } + } + + /// + /// Tests that Exec still executes properly when there's an '&' in the temp directory path + /// + [TestMethod] + public void TempPathContainsAmpersand1() + { + string directoryWithAmpersand = "foo&bar"; + string newTmp = Path.Combine(Path.GetTempPath(), directoryWithAmpersand); + string oldTmp = Environment.GetEnvironmentVariable("TMP"); + + try + { + Directory.CreateDirectory(newTmp); + Environment.SetEnvironmentVariable("TMP", newTmp); + Exec exec = PrepareExec("echo [hello]"); + + Assert.IsTrue(exec.Execute(), "Task should have succeeded"); + ((MockEngine)exec.BuildEngine).AssertLogContains("[hello]"); + } + finally + { + Environment.SetEnvironmentVariable("TMP", oldTmp); + if (Directory.Exists(newTmp)) Directory.Delete(newTmp); + } + } + + /// + /// Tests that Exec still executes properly when there's an ' &' in the temp directory path + /// + [TestMethod] + public void TempPathContainsAmpersand2() + { + string directoryWithAmpersand = "foo &bar"; + string newTmp = Path.Combine(Path.GetTempPath(), directoryWithAmpersand); + string oldTmp = Environment.GetEnvironmentVariable("TMP"); + + try + { + Directory.CreateDirectory(newTmp); + Environment.SetEnvironmentVariable("TMP", newTmp); + Exec exec = PrepareExec("echo [hello]"); + + bool taskSucceeded = exec.Execute(); + Assert.IsTrue(taskSucceeded, "Task should have succeeded"); + ((MockEngine)exec.BuildEngine).AssertLogContains("[hello]"); + } + finally + { + Environment.SetEnvironmentVariable("TMP", oldTmp); + if (Directory.Exists(newTmp)) Directory.Delete(newTmp); + } + } + + /// + /// Tests that Exec still executes properly when there's an '& ' in the temp directory path + /// + [TestMethod] + public void TempPathContainsAmpersand3() + { + string directoryWithAmpersand = "foo& bar"; + string newTmp = Path.Combine(Path.GetTempPath(), directoryWithAmpersand); + string oldTmp = Environment.GetEnvironmentVariable("TMP"); + + try + { + Directory.CreateDirectory(newTmp); + Environment.SetEnvironmentVariable("TMP", newTmp); + Exec exec = PrepareExec("echo [hello]"); + + Assert.IsTrue(exec.Execute(), "Task should have succeeded"); + ((MockEngine)exec.BuildEngine).AssertLogContains("[hello]"); + } + finally + { + Environment.SetEnvironmentVariable("TMP", oldTmp); + if (Directory.Exists(newTmp)) Directory.Delete(newTmp); + } + } + + /// + /// Tests that Exec still executes properly when there's an ' & ' in the temp directory path + /// + [TestMethod] + public void TempPathContainsAmpersand4() + { + string directoryWithAmpersand = "foo & bar"; + string newTmp = Path.Combine(Path.GetTempPath(), directoryWithAmpersand); + string oldTmp = Environment.GetEnvironmentVariable("TMP"); + + try + { + Directory.CreateDirectory(newTmp); + Environment.SetEnvironmentVariable("TMP", newTmp); + Exec exec = PrepareExec("echo [hello]"); + + Assert.IsTrue(exec.Execute(), "Task should have succeeded"); + ((MockEngine)exec.BuildEngine).AssertLogContains("[hello]"); + } + finally + { + Environment.SetEnvironmentVariable("TMP", oldTmp); + if (Directory.Exists(newTmp)) Directory.Delete(newTmp); + } + } + + [TestMethod] + public void InvalidUncDirectorySet() + { + Exec exec = PrepareExec("echo [%cd%]"); + exec.WorkingDirectory = @"\\thiscomputerdoesnotexistxyz\thiscomputerdoesnotexistxyz"; + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("MSB6003"); + } + + [TestMethod] + public void InvalidWorkingDirectorySet() + { + Exec exec = PrepareExec("echo [%cd%]"); + exec.WorkingDirectory = @"||invalid||"; + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("MSB6003"); + } + + [TestMethod] + public void BogusCustomRegexesCauseOneErrorEach() + { + Exec exec = PrepareExec("echo Some output & echo Some output & echo Some output & echo Some output "); + exec.CustomErrorRegularExpression = "~!@#$%^_)(*&^%$#@@#XF &%^%T$REd((((([[[["; + exec.CustomWarningRegularExpression = "*"; + bool result = exec.Execute(); + + MockEngine e = (MockEngine)exec.BuildEngine; + Console.WriteLine(e.Log); + Assert.AreEqual(3, e.Errors); + e.AssertLogContains("MSB3076"); + } + + [TestMethod] + public void CustomErrorRegexSupplied() + { + Exec exec = PrepareExec("echo Some output & echo ALERT:This is an error & echo Some more output"); + bool result = exec.Execute(); + + MockEngine e = (MockEngine)exec.BuildEngine; + Console.WriteLine(e.Log); + Assert.AreEqual(0, e.Errors); + e.AssertLogContains("ALERT:This is an error"); + + exec = PrepareExec("echo Some output & echo ALERT:This is an error & echo Some more output"); + exec.CustomErrorRegularExpression = ".*ALERT.*"; + result = exec.Execute(); + + e = (MockEngine)exec.BuildEngine; + Console.WriteLine(e.Log); + Assert.AreEqual(2, e.Errors); + e.AssertLogContains("ALERT:This is an error"); + } + + [TestMethod] + public void CustomWarningRegexSupplied() + { + Exec exec = PrepareExec("echo Some output & echo YOOHOO:This is a warning & echo Some more output"); + bool result = exec.Execute(); + + MockEngine e = (MockEngine)exec.BuildEngine; + Console.WriteLine(e.Log); + Assert.AreEqual(0, e.Errors); + Assert.AreEqual(0, e.Warnings); + e.AssertLogContains("YOOHOO:This is a warning"); + + exec = PrepareExec("echo Some output & echo YOOHOO:This is a warning & echo Some more output"); + exec.CustomWarningRegularExpression = ".*YOOHOO.*"; + result = exec.Execute(); + + e = (MockEngine)exec.BuildEngine; + Console.WriteLine(e.Log); + Assert.AreEqual(0, e.Errors); + Assert.AreEqual(1, e.Warnings); + e.AssertLogContains("YOOHOO:This is a warning"); + } + + [TestMethod] + public void ErrorsAndWarningsWithIgnoreStandardErrorWarningFormatTrue() + { + Exec exec = PrepareExec("echo myfile(88,37): error AB1234: thisisacanonicalerror & echo foo: warning CDE1234: thisisacanonicalwarning"); + exec.IgnoreStandardErrorWarningFormat = true; + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Errors); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Warnings); + } + + [TestMethod] + public void CustomAndStandardErrorsAndWarnings() + { + Exec exec = PrepareExec("echo myfile(88,37): error AB1234: thisisacanonicalerror & echo foo: warning CDE1234: thisisacanonicalwarning & echo YOGI & echo BEAR & echo some content"); + exec.CustomWarningRegularExpression = ".*BEAR.*"; + exec.CustomErrorRegularExpression = ".*YOGI.*"; + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(3, ((MockEngine)exec.BuildEngine).Errors); + Assert.AreEqual(2, ((MockEngine)exec.BuildEngine).Warnings); + } + + /// + /// Nobody should try to run a string emitted from the task through String.Format. + /// Firstly that's unnecessary and secondly if there's eg an unmatched curly it will throw. + /// + [TestMethod] + public void DoNotAttemptToFormatTaskOutput() + { + Exec exec = PrepareExec("echo unmatched curly {"); + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("unmatched curly {"); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Errors); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Warnings); + } + + /// + /// Nobody should try to run a string emitted from the task through String.Format. + /// Firstly that's unnecessary and secondly if there's eg an unmatched curly it will throw. + /// + [TestMethod] + public void DoNotAttemptToFormatTaskOutput2() + { + Exec exec = PrepareExec("echo unmatched curly {"); + exec.IgnoreStandardErrorWarningFormat = true; + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("unmatched curly {"); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Errors); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Warnings); + } + + [TestMethod] + public void NoDuplicateMessagesWhenCustomRegexAndRegularRegexBothMatch() + { + Exec exec = PrepareExec("echo myfile(88,37): error AB1234: thisisacanonicalerror & echo foo: warning CDE1234: thisisacanonicalwarning "); + exec.CustomErrorRegularExpression = ".*canonicale.*"; + exec.CustomWarningRegularExpression = ".*canonicalw.*"; + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(2, ((MockEngine)exec.BuildEngine).Errors); + Assert.AreEqual(1, ((MockEngine)exec.BuildEngine).Warnings); + } + + [TestMethod] + public void OnlySingleErrorWhenCustomWarningAndCustomErrorRegexesBothMatch() + { + Exec exec = PrepareExec("echo YOGI BEAR "); + exec.CustomErrorRegularExpression = ".*YOGI.*"; + exec.CustomWarningRegularExpression = ".*BEAR.*"; + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + Assert.AreEqual(2, ((MockEngine)exec.BuildEngine).Errors); + Assert.AreEqual(0, ((MockEngine)exec.BuildEngine).Warnings); + } + + [TestMethod] + public void GettersSetters() + { + Exec exec = PrepareExec("echo [%cd%]"); + exec.WorkingDirectory = "foo"; + Assert.AreEqual("foo", exec.WorkingDirectory); + exec.IgnoreExitCode = true; + Assert.AreEqual(true, exec.IgnoreExitCode); + exec.Outputs = null; + Assert.AreEqual(0, exec.Outputs.Length); + + ITaskItem[] items = new TaskItem[] { new TaskItem("hi"), new TaskItem("ho") }; + exec.Outputs = items; + Assert.AreEqual(items, exec.Outputs); + } + + [TestMethod] + public void StdEncodings() + { + ExecWrapper exec = PrepareExecWrapper("echo [%cd%]"); + + exec.StdErrEncoding = "US-ASCII"; + Assert.AreEqual(true, exec.StdErrEncoding.Contains("US-ASCII")); + Assert.AreEqual(true, exec.StdErrorEncoding.EncodingName.Contains("US-ASCII")); + + exec.StdOutEncoding = "US-ASCII"; + Assert.AreEqual(true, exec.StdOutEncoding.Contains("US-ASCII")); + Assert.AreEqual(true, exec.StdOutputEncoding.EncodingName.Contains("US-ASCII")); + } + + [TestMethod] + public void AnyExistingEnvVarCalledErrorLevelIsIgnored() + { + string oldValue = Environment.GetEnvironmentVariable("errorlevel"); + + try + { + Exec exec = PrepareExec("echo this is an innocuous successful command"); + Environment.SetEnvironmentVariable("errorlevel", "1"); + bool result = exec.Execute(); + + Assert.AreEqual(true, result); + } + finally + { + Environment.SetEnvironmentVariable("errorlevel", oldValue); + } + } + + [TestMethod] + public void ValidateParametersNoCommand() + { + Exec exec = PrepareExec(" "); + + bool result = exec.Execute(); + + Assert.AreEqual(false, result); + ((MockEngine)exec.BuildEngine).AssertLogContains("MSB3072"); + } + + /// + /// Verify that the EnvironmentVariables parameter exposed publicly + /// by ToolTask can be used to modify the environment of the cmd.exe spawned. + /// + [TestMethod] + public void SetEnvironmentVariableParameter() + { + Exec exec = new Exec(); + exec.BuildEngine = new MockEngine(); + exec.Command = "echo [%MYENVVAR%]"; + exec.EnvironmentVariables = new string[] { "myenvvar=myvalue" }; + exec.Execute(); + + ((MockEngine)exec.BuildEngine).AssertLogContains("[myvalue]"); + } + + /// + /// Execute return output as an Item + /// Test include ConsoleToMSBuild, StandardOutput + /// + [TestMethod] + public void ConsoleToMSBuild() + { + //Exec with no output + Exec exec = PrepareExec("set foo=blah"); + //Test Set and Get of ConsoleToMSBuild + exec.ConsoleToMSBuild = true; + Assert.AreEqual(true, exec.ConsoleToMSBuild); + + bool result = exec.Execute(); + Assert.AreEqual(true, result); + + //Nothing to run, so the list should be empty + Assert.AreEqual(0, exec.ConsoleOutput.Length); + + + //first echo prints "Hello stderr" to stderr, second echo prints to stdout + string testString = "echo Hello stderr 1>&2\necho Hello stdout"; + exec = PrepareExec(testString); + + //Test Set and Get of ConsoleToMSBuild + exec.ConsoleToMSBuild = true; + Assert.AreEqual(true, exec.ConsoleToMSBuild); + + result = exec.Execute(); + Assert.AreEqual(true, result); + + //Both two lines should had gone to stdout + Assert.AreEqual(2, exec.ConsoleOutput.Length); + } + } + + internal class ExecWrapper : Exec + { + public Encoding StdOutputEncoding + { + get + { + return StandardOutputEncoding; + } + } + + public Encoding StdErrorEncoding + { + get + { + return StandardErrorEncoding; + } + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/FileStateTests.cs b/src/XMakeTasks/UnitTests/FileStateTests.cs new file mode 100644 index 00000000000..5b48e296b5f --- /dev/null +++ b/src/XMakeTasks/UnitTests/FileStateTests.cs @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Test FileState utility class + /// + [TestClass] + public class FileStateTests + { + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void BadNoName() + { + new FileState(""); + } + + [TestMethod] + public void BadCharsCtorOK() + { + new FileState("|"); + } + + [TestMethod] + public void BadTooLongCtorOK() + { + new FileState(new String('x', 5000)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void BadChars() + { + var state = new FileState("|"); + var time = state.LastWriteTime; + } + + [TestMethod] + public void BadTooLongLastWriteTime() + { + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(new String('x', 5000)).LastWriteTime; }, delegate () { var x = new FileState(new String('x', 5000)).LastWriteTime; }); + } + + [TestMethod] + public void Exists() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.Exists, state.FileExists); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void Name() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.FullName, state.Name); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void IsDirectoryTrue() + { + var state = new FileState(Path.GetTempPath()); + + Assert.AreEqual(true, state.IsDirectory); + } + + [TestMethod] + public void LastWriteTime() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.LastWriteTime, state.LastWriteTime); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void LastWriteTimeUtc() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void Length() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.Length, state.Length); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void AccessDenied() + { + string locked = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "system32\\notepad.exe"); + + FileInfo info = new FileInfo(locked); + FileState state = new FileState(locked); + + Assert.AreEqual(info.Exists, state.FileExists); + + if (!info.Exists) + { + // meh, somewhere else + return; + } + + Assert.AreEqual(info.Length, state.Length); + Assert.AreEqual(info.LastWriteTime, state.LastWriteTime); + Assert.AreEqual(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); + } + +#if CHECKING4GBFILESWORK + [TestMethod] + public void LengthHuge() + { + var bigFile = @"d:\proj\hugefile"; + //var dummy = new string('x', 10000000); + //using (StreamWriter w = new StreamWriter(bigFile)) + //{ + // for (int i = 0; i < 450; i++) + // { + // w.Write(dummy); + // } + //} + + Console.WriteLine((new FileState(bigFile)).Length); + + FileInfo info = new FileInfo(bigFile); + FileState state = new FileState(bigFile); + + Assert.AreEqual(info.Exists, state.Exists); + + if (!info.Exists) + { + // meh, somewhere else + return; + } + + Assert.AreEqual(info.Length, state.Length); + } +#endif + + [TestMethod] + public void ReadOnly() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.IsReadOnly, state.IsReadOnly); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void ExistsReset() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.Exists, state.FileExists); + File.Delete(file); + Assert.AreEqual(true, state.FileExists); + state.Reset(); + Assert.AreEqual(false, state.FileExists); + } + finally + { + if (File.Exists(file)) + { + File.Delete(file); + } + } + } + + [TestMethod] + public void NameReset() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.FullName, state.Name); + string originalName = info.FullName; + string oldFile = file; + file = oldFile + "2"; + File.Move(oldFile, file); + Assert.AreEqual(originalName, state.Name); + state.Reset(); + Assert.AreEqual(originalName, state.Name); // Name is from the constructor, didn't change + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void LastWriteTimeReset() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.LastWriteTime, state.LastWriteTime); + + var time = new DateTime(2111, 1, 1); + info.LastWriteTime = time; + + Assert.AreNotEqual(time, state.LastWriteTime); + state.Reset(); + Assert.AreEqual(time, state.LastWriteTime); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void LastWriteTimeUtcReset() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); + + var time = new DateTime(2111, 1, 1); + info.LastWriteTime = time; + + Assert.AreNotEqual(time.ToUniversalTime(), state.LastWriteTimeUtcFast); + state.Reset(); + Assert.AreEqual(time.ToUniversalTime(), state.LastWriteTimeUtcFast); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void LengthReset() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.Length, state.Length); + File.WriteAllText(file, "x"); + + Assert.AreEqual(info.Length, state.Length); + state.Reset(); + info.Refresh(); + Assert.AreEqual(info.Length, state.Length); + } + finally + { + File.Delete(file); + } + } + + [TestMethod] + public void ReadOnlyReset() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + FileInfo info = new FileInfo(file); + FileState state = new FileState(file); + + Assert.AreEqual(info.IsReadOnly, state.IsReadOnly); + info.IsReadOnly = !info.IsReadOnly; + state.Reset(); + Assert.AreEqual(true, state.IsReadOnly); + } + finally + { + (new FileInfo(file)).IsReadOnly = false; + File.Delete(file); + } + } + + [TestMethod] + public void ExistsButDirectory() + { + Assert.AreEqual(new FileInfo(Path.GetTempPath()).Exists, new FileState(Path.GetTempPath()).FileExists); + Assert.AreEqual(true, (new FileState(Path.GetTempPath()).IsDirectory)); + } + + [TestMethod] + public void ReadOnlyOnDirectory() + { + Assert.AreEqual(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(Path.GetTempPath()).IsReadOnly); + } + + [TestMethod] + public void LastWriteTimeOnDirectory() + { + Assert.AreEqual(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(Path.GetTempPath()).LastWriteTime); + } + + [TestMethod] + public void LastWriteTimeUtcOnDirectory() + { + Assert.AreEqual(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(Path.GetTempPath()).LastWriteTimeUtcFast); + } + + [TestMethod] + public void LengthOnDirectory() + { + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(Path.GetTempPath()).Length; }); + } + + [TestMethod] + public void DoesNotExistLastWriteTime() + { + string file = Guid.NewGuid().ToString("N"); + + Assert.AreEqual(new FileInfo(file).LastWriteTime, new FileState(file).LastWriteTime); + } + + [TestMethod] + public void DoesNotExistLastWriteTimeUtc() + { + string file = Guid.NewGuid().ToString("N"); + + Assert.AreEqual(new FileInfo(file).LastWriteTimeUtc, new FileState(file).LastWriteTimeUtcFast); + } + + [TestMethod] + public void DoesNotExistLength() + { + string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist + + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(file).Length; }); + } + + [TestMethod] + [ExpectedException(typeof(FileNotFoundException))] + public void DoesNotExistIsDirectory() + { + string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist + + var x = new FileState(file).IsDirectory; + } + + [TestMethod] + public void DoesNotExistDirectoryOrFileExists() + { + string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist + + Assert.AreEqual(Directory.Exists(file), new FileState(file).DirectoryExists); + } + + [TestMethod] + public void DoesNotExistParentFolderNotFound() + { + string file = Guid.NewGuid().ToString("N") + "\\x"; // presumably doesn't exist + + Assert.AreEqual(false, new FileState(file).FileExists); + Assert.AreEqual(false, new FileState(file).DirectoryExists); + } + } +} + + + + + diff --git a/src/XMakeTasks/UnitTests/FindAppConfigFile_Tests.cs b/src/XMakeTasks/UnitTests/FindAppConfigFile_Tests.cs new file mode 100644 index 00000000000..3bbfc806d56 --- /dev/null +++ b/src/XMakeTasks/UnitTests/FindAppConfigFile_Tests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class FindAppConfigFile_Tests + { + [TestMethod] + public void FoundInFirstInProjectDirectory() + { + FindAppConfigFile f = new FindAppConfigFile(); + f.BuildEngine = new MockEngine(); + f.PrimaryList = new ITaskItem[] { new TaskItem("app.config"), new TaskItem("xxx") }; + f.SecondaryList = new ITaskItem[] { }; + f.TargetPath = "targetpath"; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("app.config", f.AppConfigFile.ItemSpec); + Assert.AreEqual("targetpath", f.AppConfigFile.GetMetadata("TargetPath")); + } + + [TestMethod] + public void FoundInSecondInProjectDirectory() + { + FindAppConfigFile f = new FindAppConfigFile(); + f.BuildEngine = new MockEngine(); + f.PrimaryList = new ITaskItem[] { new TaskItem("yyy"), new TaskItem("xxx") }; + f.SecondaryList = new ITaskItem[] { new TaskItem("app.config"), new TaskItem("xxx") }; + f.TargetPath = "targetpath"; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("app.config", f.AppConfigFile.ItemSpec); + Assert.AreEqual("targetpath", f.AppConfigFile.GetMetadata("TargetPath")); + } + + [TestMethod] + public void FoundInSecondBelowProjectDirectory() + { + FindAppConfigFile f = new FindAppConfigFile(); + f.BuildEngine = new MockEngine(); + f.PrimaryList = new ITaskItem[] { new TaskItem("yyy"), new TaskItem("xxx") }; + f.SecondaryList = new ITaskItem[] { new TaskItem("foo\\app.config"), new TaskItem("xxx") }; + f.TargetPath = "targetpath"; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("foo\\app.config", f.AppConfigFile.ItemSpec); + Assert.AreEqual("targetpath", f.AppConfigFile.GetMetadata("TargetPath")); + } + + [TestMethod] + public void NotFound() + { + FindAppConfigFile f = new FindAppConfigFile(); + f.BuildEngine = new MockEngine(); + f.PrimaryList = new ITaskItem[] { new TaskItem("yyy"), new TaskItem("xxx") }; + f.SecondaryList = new ITaskItem[] { new TaskItem("iii"), new TaskItem("xxx") }; + f.TargetPath = "targetpath"; + Assert.IsTrue(f.Execute()); + Assert.AreEqual(null, f.AppConfigFile); + } + + [TestMethod] + public void MatchFileNameOnlyWithAnInvalidPath() + { + FindAppConfigFile f = new FindAppConfigFile(); + f.BuildEngine = new MockEngine(); + f.PrimaryList = new ITaskItem[] { new TaskItem("yyy"), new TaskItem("xxx") }; + f.SecondaryList = new ITaskItem[] { new TaskItem("|||"), new TaskItem(@"foo\\app.config"), new TaskItem(@"!@#$@$%|"), new TaskItem("uuu") }; + f.TargetPath = "targetpath"; + Assert.IsTrue(f.Execute()); + // Should ignore the invalid paths + Assert.AreEqual(@"foo\\app.config", f.AppConfigFile.ItemSpec); + } + + // For historical reasons, we should return the last one in the list + [TestMethod] + public void ReturnsLastOne() + { + FindAppConfigFile f = new FindAppConfigFile(); + f.BuildEngine = new MockEngine(); + ITaskItem item1 = new TaskItem("app.config"); + item1.SetMetadata("id", "1"); + ITaskItem item2 = new TaskItem("app.config"); + item2.SetMetadata("id", "2"); + f.PrimaryList = new ITaskItem[] { item1, item2 }; + f.SecondaryList = new ITaskItem[] { }; + f.TargetPath = "targetpath"; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("app.config", f.AppConfigFile.ItemSpec); + Assert.AreEqual(item2.GetMetadata("id"), f.AppConfigFile.GetMetadata("id")); + } + } +} + diff --git a/src/XMakeTasks/UnitTests/FindInList_Tests.cs b/src/XMakeTasks/UnitTests/FindInList_Tests.cs new file mode 100644 index 00000000000..3e1a13c64f0 --- /dev/null +++ b/src/XMakeTasks/UnitTests/FindInList_Tests.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class FindInList_Tests + { + [TestMethod] + public void FoundCaseInsensitive() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.List = new ITaskItem[] { new TaskItem("A.CS"), new TaskItem("b.cs") }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("A.CS", f.ItemFound.ItemSpec); + } + + [TestMethod] + public void FoundCaseSensitive() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.CaseSensitive = true; + f.List = new ITaskItem[] { new TaskItem("A.CS"), new TaskItem("a.cs") }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("a.cs", f.ItemFound.ItemSpec); + } + + [TestMethod] + public void NotFoundCaseSensitive() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.CaseSensitive = true; + f.List = new ITaskItem[] { new TaskItem("A.CS"), new TaskItem("b.cs") }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual(null, f.ItemFound); + } + + [TestMethod] + public void ReturnsFirstOne() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + ITaskItem item1 = new TaskItem("a.cs"); + item1.SetMetadata("id", "1"); + ITaskItem item2 = new TaskItem("a.cs"); + item2.SetMetadata("id", "2"); + f.List = new ITaskItem[] { item1, item2 }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual("a.cs", f.ItemFound.ItemSpec); + Assert.AreEqual(item1.GetMetadata("id"), f.ItemFound.GetMetadata("id")); + } + + /// + /// Given two items (distinguished with metadata) verify that the last one is picked. + /// + [TestMethod] + public void ReturnsLastOne() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.FindLastMatch = true; + ITaskItem item1 = new TaskItem("a.cs"); + item1.SetMetadata("id", "1"); + ITaskItem item2 = new TaskItem("a.cs"); + item2.SetMetadata("id", "2"); + f.List = new ITaskItem[] { item1, item2 }; + Assert.IsTrue(f.Execute(), "Expect success"); + Assert.AreEqual("a.cs", f.ItemFound.ItemSpec); + Assert.AreEqual(item2.GetMetadata("id"), f.ItemFound.GetMetadata("id")); + } + + [TestMethod] + public void ReturnsLastOneEmptyList() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.FindLastMatch = true; + f.List = new ITaskItem[] { }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual(null, f.ItemFound); + } + + [TestMethod] + public void NotFound() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.List = new ITaskItem[] { new TaskItem("foo\a.cs"), new TaskItem("b.cs") }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual(null, f.ItemFound); + } + + [TestMethod] + public void MatchFileNameOnly() + { + FindInList f = new FindInList(); + f.BuildEngine = new MockEngine(); + f.ItemSpecToFind = "a.cs"; + f.MatchFileNameOnly = true; + f.List = new ITaskItem[] { new TaskItem(@"c:\foo\a.cs"), new TaskItem("b.cs") }; + Assert.IsTrue(f.Execute()); + Assert.AreEqual(@"c:\foo\a.cs", f.ItemFound.ItemSpec); + } + + [TestMethod] + public void MatchFileNameOnlyWithAnInvalidPath() + { + FindInList f = new FindInList(); + MockEngine e = new MockEngine(); + f.BuildEngine = e; + f.ItemSpecToFind = "a.cs"; + f.MatchFileNameOnly = true; + f.List = new ITaskItem[] { new TaskItem(@"!@#$@$%|"), new TaskItem(@"foo\a.cs"), new TaskItem("b.cs") }; + Assert.IsTrue(f.Execute()); + Console.WriteLine(e.Log); + // Should ignore the invalid paths + Assert.AreEqual(@"foo\a.cs", f.ItemFound.ItemSpec); + } + } +} diff --git a/src/XMakeTasks/UnitTests/FindInvalidProjectReferences_Tests.cs b/src/XMakeTasks/UnitTests/FindInvalidProjectReferences_Tests.cs new file mode 100644 index 00000000000..5d693630182 --- /dev/null +++ b/src/XMakeTasks/UnitTests/FindInvalidProjectReferences_Tests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for the task that resolves an FindInvalidProjectReferences to a full path on disk +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class FindInvalidProjectReferences_Tests + { + /// + /// Verify FindInvalidProjectReferences for several target platform monikers + /// + [TestMethod] + public void VerifyFindInvalidProjectReferences() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + FindInvalidProjectReferences t = new FindInvalidProjectReferences(); + t.TargetPlatformVersion = "8.0"; + t.TargetPlatformIdentifier = "Windows"; + Dictionary proj1 = new Dictionary(); + proj1["TargetPlatformMoniker"] = "Windows, Version=7.0"; + + Dictionary proj2 = new Dictionary(); + proj2["TargetPlatformMoniker"] = "Windows, Version=8.0"; + + Dictionary proj3 = new Dictionary(); + proj3["TargetPlatformMoniker"] = "Windows, Version=8.1"; + + Dictionary proj4 = new Dictionary(); + proj4["TargetPlatformMoniker"] = "Windows, Version=8.2"; + + t.ProjectReferences = new TaskItem[] { new TaskItem("proj1.proj", proj1), new TaskItem("proj2.proj", proj2), new TaskItem("proj3.proj", proj3), new TaskItem("proj4.proj", proj4) }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + string warning1 = ResourceUtilities.FormatResourceString("FindInvalidProjectReferences.WarnWhenVersionIsIncompatible", "Windows", "8.0", "proj1.proj", "Windows, Version=7.0"); + engine.AssertLogDoesntContain(warning1); + + string warning2 = ResourceUtilities.FormatResourceString("FindInvalidProjectReferences.WarnWhenVersionIsIncompatible", "Windows", "8.0", "proj2.proj", "Windows, Version=8.0"); + engine.AssertLogDoesntContain(warning2); + + string warning3 = ResourceUtilities.FormatResourceString("FindInvalidProjectReferences.WarnWhenVersionIsIncompatible", "Windows", "8.0", "proj3.proj", "Windows, Version=8.1"); + engine.AssertLogContains(warning3); + + string warning4 = ResourceUtilities.FormatResourceString("FindInvalidProjectReferences.WarnWhenVersionIsIncompatible", "Windows", "8.0", "proj4.proj", "Windows, Version=8.2"); + engine.AssertLogContains(warning4); + + Assert.AreEqual(t.InvalidReferences.Length, 2); + Assert.AreEqual(t.InvalidReferences[0].ItemSpec, "proj3.proj"); + Assert.AreEqual(t.InvalidReferences[1].ItemSpec, "proj4.proj"); + } + } +} diff --git a/src/XMakeTasks/UnitTests/FindUnderPath_Tests.cs b/src/XMakeTasks/UnitTests/FindUnderPath_Tests.cs new file mode 100644 index 00000000000..72931f5c892 --- /dev/null +++ b/src/XMakeTasks/UnitTests/FindUnderPath_Tests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class FindUnderPath_Tests + { + [TestMethod] + public void BasicFilter() + { + FindUnderPath t = new FindUnderPath(); + t.BuildEngine = new MockEngine(); + + t.Path = new TaskItem(@"C:\MyProject"); + t.Files = new ITaskItem[] { new TaskItem(@"C:\MyProject\File1.txt"), new TaskItem(@"C:\SomeoneElsesProject\File2.txt") }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.InPath.Length); + Assert.AreEqual(1, t.OutOfPath.Length); + Assert.AreEqual(@"C:\MyProject\File1.txt", t.InPath[0].ItemSpec); + Assert.AreEqual(@"C:\SomeoneElsesProject\File2.txt", t.OutOfPath[0].ItemSpec); + } + + [TestMethod] + public void InvalidFile() + { + FindUnderPath t = new FindUnderPath(); + t.BuildEngine = new MockEngine(); + + t.Path = new TaskItem(@"C:\MyProject"); + t.Files = new ITaskItem[] { new TaskItem(@":::") }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + + // Don't crash + } + + [TestMethod] + public void InvalidPath() + { + FindUnderPath t = new FindUnderPath(); + t.BuildEngine = new MockEngine(); + + t.Path = new TaskItem(@"||::||"); + t.Files = new ITaskItem[] { new TaskItem(@"foo") }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + + // Don't crash + } + + // Create a temporary file and run the task on it + private static void RunTask(FindUnderPath t, out FileInfo testFile, out bool success) + { + string fileName = ObjectModelHelpers.CreateFileInTempProjectDirectory("file%3b.temp", "foo"); + testFile = new FileInfo(fileName); + + t.Path = new TaskItem(ObjectModelHelpers.TempProjectDir); + t.Files = new ITaskItem[] { new TaskItem(EscapingUtilities.Escape(testFile.Name)), new TaskItem(@"C:\SomeoneElsesProject\File2.txt") }; + + success = false; + string currentDirectory = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(ObjectModelHelpers.TempProjectDir); + success = t.Execute(); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + } + + [TestMethod] + public void VerifyFullPath() + { + FindUnderPath t = new FindUnderPath(); + t.BuildEngine = new MockEngine(); + + t.UpdateToAbsolutePaths = true; + + FileInfo testFile; + bool success; + RunTask(t, out testFile, out success); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.InPath.Length); + Assert.AreEqual(1, t.OutOfPath.Length); + Assert.AreEqual(testFile.FullName, t.InPath[0].ItemSpec); + Assert.AreEqual(@"C:\SomeoneElsesProject\File2.txt", t.OutOfPath[0].ItemSpec); + } + + [TestMethod] + public void VerifyFullPathNegative() + { + FindUnderPath t = new FindUnderPath(); + t.BuildEngine = new MockEngine(); + + t.UpdateToAbsolutePaths = false; + + FileInfo testFile; + bool success; + RunTask(t, out testFile, out success); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.InPath.Length); + Assert.AreEqual(1, t.OutOfPath.Length); + Assert.AreEqual(testFile.Name, t.InPath[0].ItemSpec); + Assert.AreEqual(@"C:\SomeoneElsesProject\File2.txt", t.OutOfPath[0].ItemSpec); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/GenerateResourceOutOfProc_Tests.cs b/src/XMakeTasks/UnitTests/GenerateResourceOutOfProc_Tests.cs new file mode 100644 index 00000000000..c27f20359bb --- /dev/null +++ b/src/XMakeTasks/UnitTests/GenerateResourceOutOfProc_Tests.cs @@ -0,0 +1,2949 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Text.RegularExpressions; +using System.Text; + +namespace Microsoft.Build.UnitTests.GenerateResource_Tests.OutOfProc +{ + [TestClass] + sealed public class RequiredTransformations + { + /// + /// ResX to Resources, no references + /// + [TestMethod] + public void BasicResX2Resources() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing BasicResX2Resources() test"); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + string resxFile = Utilities.WriteTestResX(false, null, null); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.Sources[0].SetMetadata("Attribute", "InputValue"); + + Utilities.ExecuteTask(t); + + Assert.AreEqual("InputValue", t.OutputResources[0].GetMetadata("Attribute")); + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Ensure that OutputResource Metadata is populated on the Sources item + /// + [TestMethod] + public void OutputResourceMetadataPopulatedOnInputItems() + { + string resxFile0 = Utilities.WriteTestResX(false, null, null); + string resxFile1 = Utilities.WriteTestResX(false, null, null); + string resxFile2 = Utilities.WriteTestResX(false, null, null); + string resxFile3 = Utilities.WriteTestResX(false, null, null); + + string expectedOutFile0 = Path.Combine(Path.GetTempPath(), Path.ChangeExtension(resxFile0, ".resources")); + string expectedOutFile1 = Path.Combine(Path.GetTempPath(), "resx1.foo.resources"); + string expectedOutFile2 = Path.Combine(Path.GetTempPath(), Utilities.GetTempFileName(".resources")); + string expectedOutFile3 = Path.Combine(Path.GetTempPath(), Utilities.GetTempFileName(".resources")); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { + new TaskItem(resxFile0), new TaskItem(resxFile1), new TaskItem(resxFile2), new TaskItem(resxFile3) }; + + t.OutputResources = new ITaskItem[] { + new TaskItem(expectedOutFile0), new TaskItem(expectedOutFile1), new TaskItem(expectedOutFile2), new TaskItem(expectedOutFile3) }; + + Utilities.ExecuteTask(t); + + Assert.AreEqual(expectedOutFile0, t.Sources[0].GetMetadata("OutputResource")); + Assert.AreEqual(expectedOutFile1, t.Sources[1].GetMetadata("OutputResource")); + Assert.AreEqual(expectedOutFile2, t.Sources[2].GetMetadata("OutputResource")); + Assert.AreEqual(expectedOutFile3, t.Sources[3].GetMetadata("OutputResource")); + + // Done, so clean up. + File.Delete(resxFile0); + File.Delete(resxFile1); + File.Delete(resxFile2); + File.Delete(resxFile3); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Text to Resources + /// + [TestMethod] + public void BasicText2Resources() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + string textFile = Utilities.WriteTestText(null, null); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.Sources[0].SetMetadata("Attribute", "InputValue"); + + Utilities.ExecuteTask(t); + + Assert.AreEqual("InputValue", t.OutputResources[0].GetMetadata("Attribute")); + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// ResX to Resources with references that are used in the resx + /// + /// System dll is not locked because it forces a new app domain + [TestMethod] + public void ResX2ResourcesWithReferences() + { + string systemDll = Utilities.GetPathToCopiedSystemDLL(); + string resxFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + resxFile = Utilities.WriteTestResX(true /*system type*/, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem(systemDll) }; + + Utilities.ExecuteTask(t); + + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile); + } + finally + { + File.Delete(systemDll); + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + } + } + + /// + /// Resources to ResX + /// + [TestMethod] + public void BasicResources2ResX() + { + string resourcesFile = Utilities.CreateBasicResourcesFile(false); + + // Fork 1: create a resx file directly from the resources + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resourcesFile, ".resx")) }; + Utilities.ExecuteTask(t); + Assert.IsTrue(Path.GetExtension(t.FilesWritten[0].ItemSpec) == ".resx"); + + // Fork 2a: create a text file from the resources + GenerateResource t2a = Utilities.CreateTaskOutOfProc(); + t2a.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t2a.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resourcesFile, ".txt")) }; + Utilities.ExecuteTask(t2a); + Assert.IsTrue(Path.GetExtension(t2a.FilesWritten[0].ItemSpec) == ".txt"); + + // Fork 2b: create a resx file from the text file + GenerateResource t2b = Utilities.CreateTaskOutOfProc(); + t2b.Sources = new ITaskItem[] { new TaskItem(t2a.FilesWritten[0].ItemSpec) }; + t2b.OutputResources = new ITaskItem[] { new TaskItem(Utilities.GetTempFileName(".resx")) }; + Utilities.ExecuteTask(t2b); + Assert.IsTrue(Path.GetExtension(t2b.FilesWritten[0].ItemSpec) == ".resx"); + + // make sure the output resx files from each fork are the same + Assert.AreEqual(Utilities.ReadFileContent(t.OutputResources[0].ItemSpec), + Utilities.ReadFileContent(t2b.OutputResources[0].ItemSpec)); + + // Done, so clean up. + File.Delete(resourcesFile); + File.Delete(t.OutputResources[0].ItemSpec); + File.Delete(t2a.OutputResources[0].ItemSpec); + foreach (ITaskItem item in t2b.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Resources to Text + /// + [TestMethod] + public void BasicResources2Text() + { + string resourcesFile = Utilities.CreateBasicResourcesFile(false); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + + string outputFile = Path.ChangeExtension(resourcesFile, ".txt"); + t.OutputResources = new ITaskItem[] { new TaskItem(outputFile) }; + Utilities.ExecuteTask(t); + + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".txt"); + Assert.AreEqual(Utilities.GetTestTextContent(null, null, true /*cleaned up */), Utilities.ReadFileContent(resourcesFile)); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Force out-of-date with ShouldRebuildResgenOutputFile on the source only + /// + [TestMethod] + public void ForceOutOfDate() + { + string resxFile = Utilities.WriteTestResX(false, null, null); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + try + { + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + System.Threading.Thread.Sleep(200); + File.SetLastWriteTime(resxFile, DateTime.Now); + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec), time) > 0); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Force out-of-date with ShouldRebuildResgenOutputFile on the linked file + /// + [TestMethod] + public void ForceOutOfDateLinked() + { + string bitmap = Utilities.CreateWorldsSmallestBitmap(); + string resxFile = Utilities.WriteTestResX(false, bitmap, null, false); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + try + { + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + System.Threading.Thread.Sleep(200); + File.SetLastWriteTime(bitmap, DateTime.Now); + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec), time) > 0); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(bitmap); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Force partially out-of-date: should build only the out of date inputs + /// + [TestMethod] + public void ForceSomeOutOfDate() + { + string resxFile = null; + string resxFile2 = null; + string cache = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + resxFile2 = Utilities.WriteTestResX(false, null, null); + cache = Utilities.GetTempFileName(".cache"); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(cache); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile2) }; + + // Transform both + Utilities.ExecuteTask(t); + + // Create a new task to transform them again + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.StateFile = new TaskItem(t.StateFile.ItemSpec); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile2) }; + + // Get current write times of outputs + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + DateTime time2 = File.GetLastWriteTime(t.OutputResources[1].ItemSpec); + System.Threading.Thread.Sleep(200); + // Touch one input + File.SetLastWriteTime(resxFile, DateTime.Now); + + Utilities.ExecuteTask(t2); + + // Check only one output was updated + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec), time) > 0); + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[1].ItemSpec), time2) == 0); + + // Although only one file was updated, both should be in OutputResources and FilesWritten + Assert.IsTrue(t2.OutputResources[0].ItemSpec == t.OutputResources[0].ItemSpec); + Assert.IsTrue(t2.OutputResources[1].ItemSpec == t.OutputResources[1].ItemSpec); + Assert.IsTrue(t2.FilesWritten[0].ItemSpec == t.FilesWritten[0].ItemSpec); + Assert.IsTrue(t2.FilesWritten[1].ItemSpec == t.FilesWritten[1].ItemSpec); + } + finally + { + if (null != resxFile) File.Delete(resxFile); + if (null != resxFile2) File.Delete(resxFile2); + if (null != cache) File.Delete(cache); + if (null != resxFile) File.Delete(Path.ChangeExtension(resxFile, ".resources")); + if (null != resxFile2) File.Delete(Path.ChangeExtension(resxFile2, ".resources")); + } + } + + /// + /// Allow ShouldRebuildResgenOutputFile to return "false" since nothing's out of date, including linked file + /// + [TestMethod] + public void AllowLinkedNoGenerate() + { + string bitmap = Utilities.CreateWorldsSmallestBitmap(); + string resxFile = Utilities.WriteTestResX(false, bitmap, null, false); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + try + { + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + System.Threading.Thread.Sleep(200); + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(time.Equals(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec))); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(bitmap); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Allow the task to skip processing based on having nothing out of date + /// + [TestMethod] + public void NothingOutOfDate() + { + string resxFile = null; + string txtFile = null; + string resourcesFile1 = null; + string resourcesFile2 = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + txtFile = Utilities.WriteTestText(null, null); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(txtFile) }; + resourcesFile1 = Path.ChangeExtension(resxFile, ".resources"); + resourcesFile2 = Path.ChangeExtension(txtFile, ".resources"); + + Utilities.ExecuteTask(t); + + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t.OutputResources[1].ItemSpec == resourcesFile2); + Assert.IsTrue(t.FilesWritten[1].ItemSpec == resourcesFile2); + + Utilities.AssertStateFileWasWritten(t); + + // Repeat, and it should do nothing as they are up to date + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(txtFile) }; + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + DateTime time2 = File.GetLastWriteTime(t.OutputResources[1].ItemSpec); + System.Threading.Thread.Sleep(200); + + Utilities.ExecuteTask(t2); + // Although everything was up to date, OutputResources and FilesWritten + // must contain the files that would have been created if they weren't up to date. + Assert.IsTrue(t2.OutputResources[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t2.FilesWritten[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t2.OutputResources[1].ItemSpec == resourcesFile2); + Assert.IsTrue(t2.FilesWritten[1].ItemSpec == resourcesFile2); + + Utilities.AssertStateFileWasWritten(t2); + + Assert.IsTrue(time.Equals(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec))); + Assert.IsTrue(time2.Equals(File.GetLastWriteTime(t2.OutputResources[1].ItemSpec))); + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile1 != null) File.Delete(resourcesFile1); + if (resourcesFile2 != null) File.Delete(resourcesFile2); + } + } + + /// + /// If the reference has been touched, it should rebuild even if the inputs are + /// otherwise up to date + /// + /// System dll is not locked because it forces a new app domain + [TestMethod] + public void NothingOutOfDateExceptReference() + { + string resxFile = null; + string resourcesFile = null; + string systemDll = Utilities.GetPathToCopiedSystemDLL(); + + try + { + resxFile = Utilities.WriteTestResX(true /* uses system type */, null, null); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem(systemDll) }; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + Utilities.ExecuteTask(t); + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + + // Repeat, and it should do nothing as they are up to date + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t2.References = new ITaskItem[] { new TaskItem(systemDll) }; + t2.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t2); + Assert.IsTrue(time.Equals(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec))); + + // Touch the reference, and repeat, it should now rebuild + DateTime newTime = DateTime.Now + new TimeSpan(0, 1, 0); + File.SetLastWriteTime(systemDll, newTime); + GenerateResource t3 = Utilities.CreateTaskOutOfProc(); + t3.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t3.References = new ITaskItem[] { new TaskItem(systemDll) }; + t3.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t3); + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t3.OutputResources[0].ItemSpec), time) > 0); + resourcesFile = t3.OutputResources[0].ItemSpec; + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (systemDll != null) File.Delete(systemDll); + } + } + + /// + /// If an additional input is out of date, resources should be regenerated. + /// + [TestMethod] + public void NothingOutOfDateExceptAdditionalInput() + { + string resxFile = null; + string resourcesFile = null; + ITaskItem[] additionalInputs = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + additionalInputs = new ITaskItem[] { new TaskItem(FileUtilities.GetTemporaryFile()), new TaskItem(FileUtilities.GetTemporaryFile()) }; + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.AdditionalInputs = additionalInputs; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + Utilities.ExecuteTask(t); + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + + // Repeat, and it should do nothing as they are up to date + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t2.AdditionalInputs = additionalInputs; + t2.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t2); + Utilities.AssertLogContainsResource(t2, "GenerateResource.NothingOutOfDate", ""); + + // Touch one of the additional inputs and repeat, it should now rebuild + DateTime newTime = DateTime.Now + new TimeSpan(0, 1, 0); + File.SetLastWriteTime(additionalInputs[1].ItemSpec, newTime); + GenerateResource t3 = Utilities.CreateTaskOutOfProc(); + t3.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t3.AdditionalInputs = additionalInputs; + t3.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t3); + Utilities.AssertLogNotContainsResource(t3, "GenerateResource.NothingOutOfDate", ""); + resourcesFile = t3.OutputResources[0].ItemSpec; + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (additionalInputs != null && additionalInputs[0] != null && File.Exists(additionalInputs[0].ItemSpec)) File.Delete(additionalInputs[0].ItemSpec); + if (additionalInputs != null && additionalInputs[1] != null && File.Exists(additionalInputs[1].ItemSpec)) File.Delete(additionalInputs[1].ItemSpec); + } + } + + /// + /// Text to ResX + /// + [TestMethod] + public void BasicText2ResX() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(textFile, ".resx")) }; + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resx"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Round trip from resx to resources to resx with the same blobs + /// + [TestMethod] + public void ResX2ResX() + { + try + { + string resourcesFile = Utilities.CreateBasicResourcesFile(true); + + // Step 1: create a resx file directly from the resources, to get a framework generated resx + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resourcesFile, ".resx")) }; + Utilities.ExecuteTask(t); + Assert.IsTrue(Path.GetExtension(t.FilesWritten[0].ItemSpec) == ".resx"); + + // Step 2a: create a resources file from the resx + GenerateResource t2a = Utilities.CreateTaskOutOfProc(); + t2a.Sources = new ITaskItem[] { new TaskItem(t.FilesWritten[0].ItemSpec) }; + t2a.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(t.FilesWritten[0].ItemSpec, ".resources")) }; + Utilities.ExecuteTask(t2a); + Assert.IsTrue(Path.GetExtension(t2a.FilesWritten[0].ItemSpec) == ".resources"); + + // Step 2b: create a resx from the resources + GenerateResource t2b = Utilities.CreateTaskOutOfProc(); + t2b.Sources = new ITaskItem[] { new TaskItem(t2a.FilesWritten[0].ItemSpec) }; + t2b.OutputResources = new ITaskItem[] { new TaskItem(Utilities.GetTempFileName(".resx")) }; + File.Delete(t2b.OutputResources[0].ItemSpec); + Utilities.ExecuteTask(t2b); + Assert.IsTrue(Path.GetExtension(t2b.FilesWritten[0].ItemSpec) == ".resx"); + + // make sure the output resx files from each fork are the same + Assert.AreEqual(Utilities.ReadFileContent(t.OutputResources[0].ItemSpec), + Utilities.ReadFileContent(t2b.OutputResources[0].ItemSpec)); + + // Done, so clean up. + File.Delete(resourcesFile); + File.Delete(t.OutputResources[0].ItemSpec); + File.Delete(t2a.OutputResources[0].ItemSpec); + foreach (ITaskItem item in t2b.FilesWritten) + File.Delete(item.ItemSpec); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + + /// + /// Round trip from text to resources to text with the same blobs + /// + [TestMethod] + public void Text2Text() + { + string textFile = Utilities.WriteTestText(null, null); + + // Round 1, do the Text2Resource + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + + Utilities.ExecuteTask(t); + + // make sure round 1 is successful + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + // round 2, do the resources2Text from the same file + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + + t2.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + string outputFile = Utilities.GetTempFileName(".txt"); + t2.OutputResources = new ITaskItem[] { new TaskItem(outputFile) }; + Utilities.ExecuteTask(t2); + + resourcesFile = t2.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".txt"); + + Assert.AreEqual(Utilities.GetTestTextContent(null, null, true /*cleaned up */), Utilities.ReadFileContent(resourcesFile)); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + File.Delete(t2.Sources[0].ItemSpec); + foreach (ITaskItem item in t2.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// STR without references yields proper output, message + /// + [TestMethod] + public void StronglyTypedResources() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + // STR class name should have been generated from the output + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + // Files written should contain STR class file + string stronglyTypedFileName = Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs"); + Assert.IsTrue(t.FilesWritten[t.FilesWritten.Length - 1].ItemSpec == stronglyTypedFileName); + Assert.IsTrue(File.Exists(stronglyTypedFileName)); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContains(t, typeName); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR without references yields proper output, message + /// + [TestMethod] + public void StronglyTypedResourcesUpToDate() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + GenerateResource t2 = Utilities.CreateTaskOutOfProc(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + // STR class name should have been generated from the output + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + // Files written should contain STR class file + string stronglyTypedFileName = Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs"); + Assert.IsTrue(t.FilesWritten[t.FilesWritten.Length - 1].ItemSpec == stronglyTypedFileName); + Assert.IsTrue(File.Exists(stronglyTypedFileName)); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContains(t, typeName); + + // Now that we have done it, do it again to make sure that we don't do + t2.StateFile = new TaskItem(t.StateFile); + + t2.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t2.StronglyTypedLanguage = "CSharp"; + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(t2.OutputResources[0].ItemSpec == resourcesFile); + Assert.IsTrue(t2.FilesWritten[0].ItemSpec == resourcesFile); + Utilities.AssertStateFileWasWritten(t2); + Assert.IsTrue(t2.FilesWritten[t2.FilesWritten.Length - 1].ItemSpec == Path.ChangeExtension(t2.Sources[0].ItemSpec, ".cs")); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + foreach (ITaskItem item in t2.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR class file is out of date, but resources are up to date. Should still generate it. + /// + [TestMethod] + public void StronglyTypedResourcesOutOfDate() + { + string resxFile = null; + string resourcesFile = null; + string strFile = null; + string cacheFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Utilities.GetTempFileName(".resources"); + strFile = Path.ChangeExtension(resourcesFile, ".cs"); // STR filename should be generated from output not input filename + cacheFile = Utilities.GetTempFileName(".cache"); + + // Make sure the .cs file isn't already there. + File.Delete(strFile); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StateFile = new TaskItem(cacheFile); + Utilities.ExecuteTask(t); + + // STR class name generated from output resource file name + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(resourcesFile); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten[t.FilesWritten.Length - 1].ItemSpec == strFile); + Assert.IsTrue(File.Exists(strFile)); + + // Repeat. It should not update either file. + // First move both the timestamps back so they're still up to date, + // but we'd know if they were updated (this is quicker than sleeping and okay as there's no cache being used) + Utilities.MoveBackTimestamp(resxFile, 1); + DateTime strTime = Utilities.MoveBackTimestamp(strFile, 1); + t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StateFile = new TaskItem(cacheFile); + Utilities.ExecuteTask(t); + Assert.IsTrue(!Utilities.FileUpdated(strFile, strTime)); // Was not updated + + // OK, now delete the STR class file + File.Delete(strFile); + + // Repeat. It should recreate the STR class file + t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StateFile = new TaskItem(cacheFile); + Utilities.ExecuteTask(t); + Assert.IsTrue(Utilities.FileUpdated(strFile, strTime)); // Was updated + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile); + Assert.IsTrue(File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten[t.FilesWritten.Length - 1].ItemSpec == strFile); + Assert.IsTrue(File.Exists(strFile)); + + // OK, now delete the STR class file again + File.Delete(strFile); + + // Repeat, but specify the filename this time, instead of having it generated from the output resources + // It should recreate the STR class file again + t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StronglyTypedFileName = strFile; + Utilities.ExecuteTask(t); + Assert.IsTrue(File.Exists(strFile)); + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (strFile != null) File.Delete(strFile); + if (cacheFile != null) File.Delete(cacheFile); + } + } + + /// + /// Verify STR generation with a specified specific filename + /// + [TestMethod] + public void StronglyTypedResourcesWithFilename() + { + string txtFile = null; + string strFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + txtFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedLanguage = "CSharp"; + strFile = FileUtilities.GetTemporaryFile(); + t.StronglyTypedFileName = strFile; + + Utilities.ExecuteTask(t); + + // Check resources is output + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(Path.GetExtension(t.FilesWritten[0].ItemSpec) == ".resources"); + Assert.IsTrue(File.Exists(resourcesFile)); + + // Check STR file is output + Assert.IsTrue(t.FilesWritten[1].ItemSpec == strFile); + Assert.IsTrue(t.StronglyTypedFileName == strFile); + Assert.IsTrue(File.Exists(strFile)); + + string typeName = ""; + if (t.StronglyTypedNamespace != null) + { + typeName = t.StronglyTypedNamespace + "."; + } + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContains(t, typeName); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (strFile != null) File.Delete(strFile); + } + } + + /// + /// STR with VB + /// + [TestMethod] + public void StronglyTypedResourcesVB() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "VB"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + // FilesWritten should contain STR class file + string stronglyTypedFileName = Path.ChangeExtension(t.Sources[0].ItemSpec, ".vb"); + Assert.IsTrue(t.FilesWritten[t.FilesWritten.Length - 1].ItemSpec == stronglyTypedFileName); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + Assert.IsTrue(File.Exists(stronglyTypedFileName)); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContains(t, typeName); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR namespace can be empty + /// + [TestMethod] + public void StronglyTypedResourcesWithoutNamespaceOrClassOrFilename() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + // Should have defaulted the STR filename to the bare output resource name + ".cs" + string STRfile = Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs"); + Assert.IsTrue(t.StronglyTypedFileName == STRfile); + Assert.IsTrue(File.Exists(STRfile)); + + // Should have defaulted the class name to the bare output resource name + Assert.IsTrue(t.StronglyTypedClassName == Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec)); + + // Should not have used a namespace + Assert.IsTrue(!File.ReadAllText(t.StronglyTypedFileName).Contains("namespace")); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR with resource namespace yields proper output, message (CS) + /// + [TestMethod] + public void STRWithResourcesNamespaceCS() + { + Utilities.STRNamespaceTestHelper("CSharp", "MyResourcesNamespace", null); + } + + /// + /// STR with resource namespace yields proper output, message (VB) + /// + [TestMethod] + public void STRWithResourcesNamespaceVB() + { + Utilities.STRNamespaceTestHelper("VB", "MyResourcesNamespace", null); + } + + /// + /// STR with resource namespace and STR namespace yields proper output, message (CS) + /// + [TestMethod] + public void STRWithResourcesNamespaceAndSTRNamespaceCS() + { + Utilities.STRNamespaceTestHelper("CSharp", "MyResourcesNamespace", "MySTClassNamespace"); + } + + /// + /// STR with resource namespace and STR namespace yields proper output, message (CS) + /// + [TestMethod] + public void STRWithResourcesNamespaceAndSTRNamespaceVB() + { + Utilities.STRNamespaceTestHelper("VB", "MyResourcesNamespace", "MySTClassNamespace"); + } + } + + [TestClass] + sealed public class TransformationErrors + { + /// + /// Text input failures, no name, no '=', 'strings' token, invalid token, invalid escape + /// + [TestMethod] + public void TextToResourcesBadFormat() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing TextToResourcesBadFormat() test"); + + // The first string in each row is passed into the text block that's created in the file + // The second string is a fragment of the expected error message + string[][] tests = new string[][] { + // invalid token in file, "unsupported square bracket keyword" + new string[] { "[goober]", "MSB3563" }, + // no '=', "resource line without an equals sign" + new string[] { "abcdefaghha", "MSB3564" }, + // no name, "resource line without a name" + new string[] { "=abced", "MSB3565" }, + // invalid escape, "unsupported or invalid escape character" + new string[] { "abc=de\\efght", "MSB3566" }, + // another invalid escape, this one more serious, "unsupported or invalid escape character" + new string[] { @"foo=\ujjjjbar", "MSB3569"}, + }; + + GenerateResource t = null; + string textFile = null; + + foreach (string[] test in tests) + { + t = Utilities.CreateTaskOutOfProc(); + + textFile = Utilities.WriteTestText(null, test[0]); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.Execute(); + + // errors listed above -- boils down to resgen.exe error + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + // text file uses the strings token; since it's only a warning we have to have special asserts + t = Utilities.CreateTaskOutOfProc(); + + textFile = Utilities.WriteTestText(null, "[strings]"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + bool success = t.Execute(); + // Task should have succeeded (it was just a warning) + Assert.IsTrue(success); + // warning that 'strings' is an obsolete tag + Utilities.AssertLogContains(t, "WARNING RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Cause failures in ResXResourceReader + /// + [TestMethod] + public void FailedResXReader() + { + string resxFile1 = null; + string resxFile2 = null; + string resourcesFile1 = null; + string resourcesFile2 = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + // Invalid one + resxFile1 = Utilities.WriteTestResX(false, null, " >>>>>\xd\xa Assembly\xd\xa \xd\xa", false); + // Also include a valid one. It should still get processed + resxFile2 = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile1), new TaskItem(resxFile2) }; + resourcesFile1 = Path.ChangeExtension(resxFile1, ".resources"); + resourcesFile2 = Path.ChangeExtension(resxFile2, ".resources"); + File.Delete(resourcesFile1); + File.Delete(resourcesFile2); + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertStateFileWasWritten(t); + // Should not have created an output for the invalid resx + // Should have created the other file + Assert.IsTrue(!File.Exists(resourcesFile1)); + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile2); + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile2); + Assert.IsTrue(File.Exists(resourcesFile2)); + + // "error in resource file" with exception from the framework -- + // resgen.exe error + Utilities.AssertLogContains(t, "ERROR RG0000"); + } + finally + { + if (null != resxFile1) File.Delete(resxFile1); + if (null != resxFile2) File.Delete(resxFile2); + if (null != resourcesFile1) File.Delete(resourcesFile1); + if (null != resourcesFile2) File.Delete(resourcesFile2); + } + } + + /// + /// Cause failures in ResXResourceReader, different codepath + /// + [TestMethod] + public void FailedResXReaderWithAllOutputResourcesSpecified() + { + string resxFile1 = null; + string resxFile2 = null; + string resourcesFile1 = null; + string resourcesFile2 = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + // Invalid one + resxFile1 = Utilities.WriteTestResX(false, null, " >>>>>\xd\xa Assembly\xd\xa \xd\xa", false); + // Also include a valid one. It should still get processed + resxFile2 = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile1), new TaskItem(resxFile2) }; + resourcesFile1 = Path.ChangeExtension(resxFile1, ".resources"); + resourcesFile2 = Path.ChangeExtension(resxFile2, ".resources"); + File.Delete(resourcesFile1); + File.Delete(resourcesFile2); + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile1), new TaskItem(resourcesFile2) }; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertStateFileWasWritten(t); + + // Should not have created an output for the invalid resx + // Should have created the other file + Assert.IsTrue(!File.Exists(resourcesFile1)); + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile2); + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile2); + Assert.IsTrue(File.Exists(resourcesFile2)); + + // "error in resource file" with exception from the framework -- + // resgen.exe error + Utilities.AssertLogContains(t, "ERROR RG0000"); + +#if false // we can't do this because FX strings ARE localized -- VSW#455956 + // This is a literal because it comes from the ResX parser in the framework + Utilities.AssertLogContains(t, "'valueAB' start tag on line 18 does not match the end tag of 'value'"); +#endif + // so just look for the unlocalizable portions + Utilities.AssertLogContains(t, "valueAB"); + Utilities.AssertLogContains(t, "value"); + } + finally + { + if (null != resxFile1) File.Delete(resxFile1); + if (null != resxFile2) File.Delete(resxFile2); + if (null != resourcesFile1) File.Delete(resourcesFile1); + if (null != resourcesFile2) File.Delete(resourcesFile2); + } + } + + /// + /// Duplicate resource names + /// + [TestMethod] + public void DuplicateResourceNames() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + string textFile = Utilities.WriteTestText(null, "Marley=some guy from Jamaica"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + bool success = t.Execute(); + // Task should have succeeded (it was just a warning) + Assert.IsTrue(success); + + // "duplicate resource name" -- from resgen.exe + Utilities.AssertLogContains(t, "WARNING RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Non-string resource with text output + /// + [TestMethod] + public void UnsupportedTextType() + { + string bitmap = Utilities.CreateWorldsSmallestBitmap(); + string resxFile = Utilities.WriteTestResX(false, bitmap, null, false); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resxFile, ".txt")) }; + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // "only strings can be written to a .txt file" + // resgen.exe error + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(bitmap); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Can't write the statefile + /// + [TestMethod] + public void InvalidStateFile() + { + string resxFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + resxFile = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.StateFile = new TaskItem("||invalid filename||"); + + // Should still succeed + Assert.IsTrue(t.Execute()); + + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == t.OutputResources[0].ItemSpec); + } + finally + { + if (null != resxFile) File.Delete(resxFile); + if (null != resourcesFile) File.Delete(resourcesFile); + } + } + + /// + /// Cause failures in ResourceReader + /// + [TestMethod] + public void FailedResourceReader() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + // to cause a failure, we're going to transform a bad .resources file to a .resx + // the simplest thing is to create a .resx, but call it .resources + string resxFile = Utilities.WriteTestResX(false, null, null); + string resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Move(resxFile, resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resxFile) }; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // "error in resource file" with exception from the framework -- + // resgen.exe error + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Invalid STR Class name + /// + [TestMethod] + public void FailedSTRProperty() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, "object=some string"); + + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + // Invalid class name + t.StronglyTypedClassName = "~!@#$%^&*("; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // cannot write to STR class file -- resgen.exe error + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Reference passed in that can't be loaded should error + /// + [TestMethod] + public void InvalidReference() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + // Create resx with invalid ref "INVALID" + txtFile = Utilities.WriteTestResX(false, null, null, true /*data with invalid type*/); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.References = new TaskItem[] { new TaskItem("INVALID") }; + + bool result = t.Execute(); + // Task should have failed + Assert.IsTrue(!result); + + // Should have not written any files + Assert.IsTrue(t.FilesWritten != null && t.FilesWritten.Length == 0); + Assert.IsTrue(!File.Exists(resourcesFile)); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + } + + [TestClass] + sealed public class PropertyHandling + { + /// + /// Sources attributes are copied to given OutputResources + /// + [TestMethod] + public void AttributeForwarding() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing AttributeForwarding() test"); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + string resxFile = Utilities.WriteTestResX(false, null, null); + ITaskItem i = new TaskItem(resxFile); + i.SetMetadata("Locale", "en-GB"); + t.Sources = new ITaskItem[] { i }; + + ITaskItem o = new TaskItem("MyAlternateResource.resources"); + o.SetMetadata("Locale", "fr"); + o.SetMetadata("Flavor", "Pumpkin"); + t.OutputResources = new ITaskItem[] { o }; + + Utilities.ExecuteTask(t); + + // Locale was forward from source item and should overwrite the 'fr' + // locale that the output item originally had. + Assert.AreEqual("fr", t.OutputResources[0].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual("MyAlternateResource.resources", t.OutputResources[0].ItemSpec); + + // Attributes not on Sources should be left untouched. + Assert.AreEqual("Pumpkin", t.OutputResources[0].GetMetadata("Flavor")); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Sources attributes copied to computed OutputResources + /// + [TestMethod] + public void AttributeForwardingOnEmptyOutputs() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + string resxFile = Utilities.WriteTestResX(false, null, null); + ITaskItem i = new TaskItem(resxFile); + i.SetMetadata("Locale", "en-GB"); + t.Sources = new ITaskItem[] { i }; + + Utilities.ExecuteTask(t); + + // Output ItemSpec should be computed from input + string resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + Assert.AreEqual(resourcesFile, t.OutputResources[0].ItemSpec); + + // Attribute from source should be copied to output + Assert.AreEqual("en-GB", t.OutputResources[0].GetMetadata("Locale")); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// OutputFiles used for output, and also are synthesized if not set on input + /// + [TestMethod] + public void OutputFilesNotSpecified() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + t.Sources = new ITaskItem[] { + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null)), + }; + + Utilities.ExecuteTask(t); + + // Output ItemSpec should be computed from input + for (int i = 0; i < t.Sources.Length; i++) + { + string outputFile = Path.ChangeExtension(t.Sources[i].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[i].ItemSpec); + } + + // Done, so clean up. + foreach (ITaskItem item in t.Sources) + File.Delete(item.ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// FilesWritten contains OutputResources + StateFile + /// + [TestMethod] + public void FilesWrittenSet() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + t.Sources = new ITaskItem[] { + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null)), + }; + + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + int i = 0; + + for (i = 0; i < 4; i++) + { + Assert.AreEqual(t.FilesWritten[i].ItemSpec, t.OutputResources[i].ItemSpec); + Assert.IsTrue(File.Exists(t.FilesWritten[i].ItemSpec)); + } + + Utilities.AssertStateFileWasWritten(t); + + // Done, so clean up. + File.Delete(t.StateFile.ItemSpec); + foreach (ITaskItem item in t.Sources) + { + File.Delete(item.ItemSpec); + } + foreach (ITaskItem item in t.FilesWritten) + { + File.Delete(item.ItemSpec); + } + } + + /// + /// Resource transformation fails on 3rd of 4 inputs, inputs 1 & 2 & 4 are in outputs and fileswritten. + /// + [TestMethod] + public void OutputFilesPartialInputs() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + t.Sources = new ITaskItem[] { + new TaskItem( Utilities.WriteTestText(null, null) ), + new TaskItem( Utilities.WriteTestText(null, null) ), + new TaskItem( Utilities.WriteTestText("goober", null) ), + new TaskItem( Utilities.WriteTestText(null, null)), + }; + foreach (ITaskItem taskItem in t.Sources) + { + File.Delete(Path.ChangeExtension(taskItem.ItemSpec, ".resources")); + } + + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + string outputFile = Path.ChangeExtension(t.Sources[0].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[0].ItemSpec); + Assert.IsTrue(File.Exists(t.OutputResources[0].ItemSpec)); + outputFile = Path.ChangeExtension(t.Sources[1].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[1].ItemSpec); + Assert.IsTrue(File.Exists(t.OutputResources[1].ItemSpec)); + // Sources[2] should NOT have been converted and should not be in OutputResources + outputFile = Path.ChangeExtension(t.Sources[2].ItemSpec, ".resources"); + Assert.IsTrue(!File.Exists(outputFile)); + // Sources[3] should have been converted + outputFile = Path.ChangeExtension(t.Sources[3].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[2].ItemSpec); + Assert.IsTrue(File.Exists(t.OutputResources[2].ItemSpec)); + + // FilesWritten should contain only the 3 successfully output .resources and the cache + Assert.IsTrue(t.FilesWritten[0].ItemSpec == Path.ChangeExtension(t.Sources[0].ItemSpec, ".resources")); + Assert.IsTrue(t.FilesWritten[1].ItemSpec == Path.ChangeExtension(t.Sources[1].ItemSpec, ".resources")); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == Path.ChangeExtension(t.Sources[3].ItemSpec, ".resources")); + Utilities.AssertStateFileWasWritten(t); + + // Make sure there was an error on the second resource + // "unsupported square bracket keyword" + Utilities.AssertLogContains(t, "ERROR RG0000"); + Utilities.AssertLogContains(t, "[goober]"); + } + finally + { + // Done, so clean up. + foreach (ITaskItem item in t.Sources) + { + File.Delete(item.ItemSpec); + } + foreach (ITaskItem item in t.FilesWritten) + { + File.Delete(item.ItemSpec); + } + } + } + + /// + /// STR class name derived from output file transformation + /// + [TestMethod] + public void StronglyTypedClassName() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StronglyTypedFileName = "somefile.cs"; + t.PublicClass = true; + t.OutputResources = new ITaskItem[] { new TaskItem("somefile.resources") }; + + Utilities.ExecuteTask(t); + + Assert.AreEqual(t.StronglyTypedClassName, Path.GetFileNameWithoutExtension(t.StronglyTypedFileName)); + // Verify class was public, as we specified + Assert.IsTrue(File.ReadAllText(t.StronglyTypedFileName).Contains("public class " + t.StronglyTypedClassName)); + + Utilities.AssertStateFileWasWritten(t); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR class file name derived from class name transformation + /// + [TestMethod] + public void StronglyTypedFileName() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + File.Delete(Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs")); + + Utilities.ExecuteTask(t); + + Utilities.AssertStateFileWasWritten(t); + Assert.AreEqual(t.StronglyTypedFileName, Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs")); + Assert.IsTrue(File.Exists(t.StronglyTypedFileName)); + + // Verify class was internal, since we didn't specify a preference + Assert.IsTrue(File.ReadAllText(t.StronglyTypedFileName).Contains("internal class " + t.StronglyTypedClassName)); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + } + + [TestClass] + sealed public class PropertyErrors + { + /// + /// Empty Sources yields message, success + /// + [TestMethod] + public void EmptySources() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing EmptySources() test"); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + Utilities.ExecuteTask(t); + Utilities.AssertLogContainsResource(t, "GenerateResource.NoSources", ""); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// References with invalid assemblies yields warning + /// + [TestMethod] + public void ReferencesToBadAssemblies() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + string textFile = null; + + try + { + textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.References = new ITaskItem[] { new TaskItem("some non-existent DLL name goes here.dll") }; + bool success = t.Execute(); + + // Resgen.exe attempts to consume the bad reference even if it's not + // necessary, so task should fail + Assert.IsTrue(!success); + } + finally + { + // Done, so clean up. + if (textFile != null) + { + File.Delete(textFile); + File.Delete(Path.ChangeExtension(textFile, ".resources")); + } + } + } + + /// + /// Source item not found + /// + [TestMethod] + public void SourceItemMissing() + { + string txtFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + txtFile = Utilities.WriteTestText(null, null); + resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem("non-existent.resx"), new TaskItem(txtFile) }; + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // Invalid resx file: "Resource file cannot be found". + Utilities.AssertLogContains(t, "ERROR MSB3552"); + + // Should have processed remaining file + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile); + Assert.IsTrue(File.Exists(resourcesFile)); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile != null) File.Delete(resourcesFile); + } + } + + /// + /// Non-existent StateFile yields message + /// + [TestMethod] + public void StateFileUnwritable() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StateFile = new TaskItem(FileUtilities.GetTemporaryFile()); + File.SetAttributes(t.StateFile.ItemSpec, FileAttributes.ReadOnly); + t.Execute(); + + // "cannot read state file (opening for read/write)" + Utilities.AssertLogContains(t, "MSB3088"); + // "cannot write state file (opening for read/write)" + Utilities.AssertLogContains(t, "MSB3101"); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.SetAttributes(t.StateFile.ItemSpec, FileAttributes.Normal); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Bad file extension on input + /// + [TestMethod] + public void InputFileExtension() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + string textFile = Utilities.WriteTestText(null, null); + string newTextFile = Path.ChangeExtension(textFile, ".foo"); + File.Move(textFile, newTextFile); + t.Sources = new ITaskItem[] { new TaskItem(newTextFile) }; + + t.Execute(); + + // "unsupported file extension" -- An error from resgen.exe + // should be in the log + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// Bad file extension on output + /// + [TestMethod] + public void OutputFileExtension() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + string textFile = Utilities.WriteTestText(null, null); + string resxFile = Path.ChangeExtension(textFile, ".foo"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resxFile) }; + + t.Execute(); + + // "unsupported file extension" -- an error from resgen.exe should + // be in the log + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// Sources and OutputResources different # of elements + /// + [TestMethod] + public void SourcesMatchesOutputResources() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + + string textFile = Utilities.WriteTestText(null, null); + string resxFile = Path.ChangeExtension(textFile, ".resources"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem("someother.resources") }; + + t.Execute(); + + // "two vectors must have the same length" + Utilities.AssertLogContains(t, "MSB3094"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// Invalid StronglyTypedLanguage yields CodeDOM exception + /// + [TestMethod] + public void UnknownStronglyTypedLanguage() + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "AkbarAndJeff"; + + t.Execute(); + + // "no codedom provider defined" -- An error from resgen.exe + // should be in the log + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// StronglyTypedLanguage, but more than one resources file + /// + [TestMethod] + public void StronglyTypedResourceWithMoreThanOneInputResourceFile() + { + string resxFile = null; + string resxFile2 = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + resxFile2 = Utilities.WriteTestResX(false, null, null); + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile2) }; + t.StronglyTypedLanguage = "VisualBasic"; + + Assert.IsTrue(!t.Execute()); + + // "str language but more than one source file" + Utilities.AssertLogContains(t, "MSB3573"); + + Assert.IsTrue(t.FilesWritten.Length == 0); + Assert.IsTrue(t.OutputResources == null || t.OutputResources.Length == 0); + } + finally + { + if (null != resxFile) File.Delete(resxFile); + if (null != resxFile2) File.Delete(resxFile2); + if (null != resxFile) File.Delete(Path.ChangeExtension(resxFile, ".resources")); + if (null != resxFile2) File.Delete(Path.ChangeExtension(resxFile2, ".resources")); + } + } + + /// + /// STR class name derived from output file transormation + /// + [TestMethod] + public void BadStronglyTypedFilename() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + txtFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StronglyTypedClassName = "cc"; + t.StronglyTypedFileName = "||"; + t.OutputResources = new ITaskItem[] { new TaskItem("somefile.resources") }; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // Other messages in InProc.PropertyErrors.BadStronglyTypedFilename() will not + // show up because their equivalents (in sentiment but not exact syntax) will + // be logged through resgen.exe instead. + + // We should get at least one error from resgen.exe because of the bad STR filename + Utilities.AssertLogContains(t, "ERROR RG0000"); + + // it didn't write the STR class successfully, so it shouldn't be in FilesWritten -- all we should see is + // the statefile, because resgen.exe doesn't write the .resources file when STR creation fails + Utilities.AssertStateFileWasWritten(t); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR class without a language, errors + /// + [TestMethod] + public void StronglyTypedResourceClassWithoutLanguage() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + txtFile = Utilities.WriteTestText(null, null); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedClassName = "myclassname"; + // no language + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + + // Even the .resources wasn't created + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR namespace without a language, errors + /// + [TestMethod] + public void StronglyTypedResourceNamespaceWithoutLanguage() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + txtFile = Utilities.WriteTestText(null, null); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedNamespace = "mynamespace"; + // no language + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + + // Even the .resources wasn't created + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR filename without a language, errors + /// + [TestMethod] + public void StronglyTypedResourceFilenameWithoutLanguage() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + txtFile = Utilities.WriteTestText(null, null); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedFileName = "myfile"; + // no language + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + + // Even the .resources wasn't created + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (null != txtFile) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR language with more than 1 sources errors + /// + [TestMethod] + public void StronglyTypedResourceFileIsExistingDirectory() + { + string dir = null; + string txtFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + txtFile = Utilities.WriteTestText(null, null); + resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + string csFile = Path.ChangeExtension(txtFile, ".cs"); + File.Delete(csFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedLanguage = "C#"; + dir = Path.Combine(Path.GetTempPath(), "directory"); + Directory.CreateDirectory(dir); + t.StronglyTypedFileName = dir; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // "AccessDeniedException" -- StronglyTypedFileName can't be + // a directory + Utilities.AssertLogContains(t, "ERROR RG0000"); + Utilities.AssertLogContains(t, t.StronglyTypedClassName); + + // Resgen.exe does not create either the resources or the STR file + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(!File.Exists(csFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (dir != null) Directory.Delete(dir); + } + } + + [TestMethod] + public void Regress25163_OutputResourcesContainsInvalidPathCharacters() + { + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + resourcesFile = Utilities.WriteTestResX(false, null, null); + + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem("||") }; + + bool success = t.Execute(); + + Assert.IsFalse(success, "Task should have failed."); + + // We will now hit the error earlier in task execution when checking for duplicates so we will not get resgen to even execute. + Utilities.AssertLogContains(t, "MSB3553"); + } + finally + { + if (resourcesFile != null) File.Delete(resourcesFile); + } + } + } + + [TestClass] + public class References + { + [TestMethod] + [Ignore] // "ResGen.exe is claiming there is a null reference -- have contacted CDF about the issue" + public void DontLockP2PReferenceWhenResolvingSystemTypes() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing DontLockP2PReferenceWhenResolvingSystemTypes() test"); + + // ------------------------------------------------------------------------------- + // Need to produce a .DLL assembly on disk, so we can pass it in as a reference to + // GenerateResource. + // ------------------------------------------------------------------------------- + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("lib1.csproj", @" + + + + Local + Debug + AnyCPU + lib1 + Library + lib1 + + + bin\Debug\ + true + false + + + bin\Release\ + false + true + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + public class Class1 + { + } + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("lib1.csproj"); + + string p2pReference = Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\debug\lib1.dll"); + Assert.IsTrue(File.Exists(p2pReference), "lib1.dll doesn't exist."); + + // ------------------------------------------------------------------------------- + // Done producing an assembly on disk. + // ------------------------------------------------------------------------------- + + // Create a .RESX that references unqualified (without an assembly name) System types. + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"MyStrings.resx", @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.0.0.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Graph Legend + Used in reports to label the graph legend that pops up + + + {0}'s Responses + Used in challenge checklist tables + + + Strength Area + Used in challenge checklist tables + + + Neutral Area + Used in challenge checklist tables + + + Challenge Area + Used in challenge checklist tables + + + Click here for scale calculation + Used in Profile Scale area of main report to point to resource section scale tables. + + + Page + In footer of PDF report, and used in PDF links + + + Table of Contents + On second page of PDF report + + + and + On title page of PDF, joining two participants in a list + + + + "); + + // Run the GenerateResource task on the above .RESX file, passing in an unused reference + // to lib1.dll. + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(Path.Combine(ObjectModelHelpers.TempProjectDir, "MyStrings.resx")) }; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + t.References = new ITaskItem[] + { + new TaskItem(p2pReference), + + // Path to System.dll + new TaskItem(new Uri((typeof(string)).Assembly.EscapedCodeBase).LocalPath) + }; + + bool success = t.Execute(); + + // Make sure the resource was built. + Assert.IsTrue(success, "GenerateResource failed"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory("MyStrings.resources"); + + // Make sure the P2P reference is not locked after calling GenerateResource. + File.Delete(p2pReference); + } + + /// + /// A reference is being passed into the + /// GenerateResource task, but it's specified using a relative path. GenerateResource + /// was failing on this, because in the ResolveAssembly handler, it was calling + /// Assembly.LoadFile on that relative path, which fails (LoadFile requires an + /// absolute path). The fix was to use Assembly.LoadFrom instead. + /// + [TestMethod] + [Ignore] // "ResGen.exe is claiming there is a null reference -- have contacted CDF about the issue" + public void ReferencedAssemblySpecifiedUsingRelativePath() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing ReferencedAssemblySpecifiedUsingRelativePath() test"); + + // ------------------------------------------------------------------------------- + // Need to produce a .DLL assembly on disk, so we can pass it in as a reference to + // GenerateResource. + // ------------------------------------------------------------------------------- + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("ClassLibrary20.csproj", @" + + + + Local + Debug + AnyCPU + ClassLibrary20 + Library + lib1 + + + bin\Debug\ + true + false + + + bin\Release\ + false + true + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace ClassLibrary20 + { + [Serializable] + public class Class1 + { + public string foo; + } + } + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("ClassLibrary20.csproj"); + + // ------------------------------------------------------------------------------- + // Done producing an assembly on disk. + // ------------------------------------------------------------------------------- + + // Create a .RESX that references a type from ClassLibrary20.dll + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"MyStrings.resx", @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.0.0.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + blah + + + + "); + + // Run the GenerateResource task on the above .RESX file, passing in an unused reference + // to lib1.dll. + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(Path.Combine(ObjectModelHelpers.TempProjectDir, "MyStrings.resx")) }; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + + TaskItem reference = new TaskItem(@"bin\debug\ClassLibrary20.dll"); + reference.SetMetadata("FusionName", "ClassLibrary20, version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + + t.References = new ITaskItem[] { reference }; + + // Set the current working directory to the location of ClassLibrary20.csproj. + // This is what allows us to pass in a relative path to the referenced assembly. + string originalCurrentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = ObjectModelHelpers.TempProjectDir; + + bool success = t.Execute(); + + // Restore the current working directory to what it was before the test. + Environment.CurrentDirectory = originalCurrentDirectory; + + // Make sure the resource was built. + Assert.IsTrue(success, "GenerateResource failed"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory("MyStrings.resources"); + } + } + + [TestClass] + public class MiscTests + { + [TestMethod] + public void ResgenCommandLineLogging() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing ResgenCommandLineLogging() test"); + + // we use this to check if paths need quoting + CommandLineBuilderHelper commandLineBuilderHelper = new CommandLineBuilderHelper(); + + string resxFile = Utilities.WriteTestResX(false, null, null); + string resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Delete(resourcesFile); + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + // Since this is resgen 4.0, will be in a response-file, which is line-delineated + // and doesn't like spaces in filenames. + Utilities.AssertLogContains(t, "/compile"); + Utilities.AssertLogContains(t, resxFile + "," + resourcesFile); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + } + + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Delete(resourcesFile); + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem("baz"), new TaskItem("jazz") }; + t.UseSourcePath = true; + t.PublicClass = true; + t.StronglyTypedLanguage = "C#"; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + string possiblyQuotedResxFile = resxFile; + string possiblyQuotedResourcesFile = resourcesFile; + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile)) + { + possiblyQuotedResxFile = "\"" + resxFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile)) + { + possiblyQuotedResourcesFile = "\"" + resourcesFile + "\""; + } + + Utilities.AssertLogContains(t, + " /useSourcePath /publicClass /r:baz /r:jazz " + + possiblyQuotedResxFile + + " " + + possiblyQuotedResourcesFile + + " /str:\"C#\",,,"); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + File.Delete(Path.ChangeExtension(resxFile, ".cs")); + } + + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Delete(resourcesFile); + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem("baz"), new TaskItem("jazz") }; + t.UseSourcePath = true; + t.StronglyTypedLanguage = "C#"; + t.StronglyTypedClassName = "wagwag"; + t.StronglyTypedFileName = "boo"; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + string possiblyQuotedResxFile = resxFile; + string possiblyQuotedResourcesFile = resourcesFile; + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile)) + { + possiblyQuotedResxFile = "\"" + resxFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile)) + { + possiblyQuotedResourcesFile = "\"" + resourcesFile + "\""; + } + + Utilities.AssertLogContains(t, + " /useSourcePath /r:baz /r:jazz " + + possiblyQuotedResxFile + + " " + + possiblyQuotedResourcesFile + + " /str:\"C#\",,wagwag,boo"); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + } + + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Path.ChangeExtension(resxFile, ".myresources"); + File.Delete(resourcesFile); + string resxFile1 = Utilities.WriteTestResX(false, null, null); + string resourcesFile1 = Path.ChangeExtension(resxFile1, ".myresources"); + File.Delete(resourcesFile1); + + try + { + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile1) }; + t.OutputResources = new ITaskItem[] + { + new TaskItem(resourcesFile), + new TaskItem(resourcesFile1) + }; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + // Since this is resgen 4.0, will be in a response-file, which is line-delineated + // and doesn't like spaces in filenames. + Utilities.AssertLogContains(t, "/compile"); + Utilities.AssertLogContains(t, resxFile + "," + resourcesFile); + Utilities.AssertLogContains(t, resxFile1 + "," + resourcesFile1); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + File.Delete(resxFile1); + File.Delete(resourcesFile1); + } + } + + /// + /// Validate that when using ResGen 3.5, a command line command where the last parameter takes us past the 28,000 character limit is handled appropriately + /// + [TestMethod] + public void ResgenCommandLineExceedsAllowedLength() + { + string sdkToolsPath; + string net35 = ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version35); + string net35sdk = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.Version35, VisualStudioVersion.VersionLatest); + // If .NET 3.5 isn't installed, then the ToolLocationHelper will either return null or there won't be an MSBuild subfolder under the Framework directory for .NET 3.5 + if (net35 != null && Directory.Exists(Path.Combine(net35, "MSBuild")) && net35sdk != null && Directory.Exists(Path.Combine(net35sdk, "bin"))) + { + sdkToolsPath = Path.Combine(net35sdk, "bin"); + } + else + { + Assert.IsTrue(true, "We only need to test .NET 3.5 ResGen, if it isn't on disk then pass the test and return"); + return; + } + + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing ResgenCommandLineExceedsAllowedLength() test"); + + // we use this to check if paths need quoting + CommandLineBuilderHelper commandLineBuilderHelper = new CommandLineBuilderHelper(); + + List sources = new List(); + List outputResources = new List(); + + try + { + int filesToBeCreated = 83; + + // The filesToBeCreated number is determined from the Username length and the given explicitly set temp folder. + // These numbers were shown through trial and error to be the correct numbers. + switch (Environment.UserName.Length) + { + case 1: + case 2: + filesToBeCreated = 89; + break; + case 3: + case 4: + case 5: + filesToBeCreated = 88; + break; + case 6: + case 7: + case 8: + case 9: + filesToBeCreated = 87; + break; + case 10: + case 11: + case 12: + case 13: + filesToBeCreated = 86; + break; + case 14: + case 15: + case 16: + case 17: + filesToBeCreated = 85; + break; + case 18: + case 19: + case 20: + filesToBeCreated = 84; + break; + } + + // Get the generic "Temp" folder from the users' LocalAppData path in case they specify a different "Temp" folder or it's + // located on a drive other than C. + string tempFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "temp"); + + // This loop creates "filesToBeCreated" source files (+ output files) of 140 characters each such that the last one exceeds + // the 28,000 command line max length limit in order to validate that resgen is behaving properly in that scenario + for (int x = 0; x < filesToBeCreated; x++) + { + string fileName = new String('c', 133) + String.Format("{0:00}", x); + string resxFile = MyResxFileCreator(tempFolder, fileName); + string resourcesFile = Path.ChangeExtension(fileName, ".resources"); + sources.Add(new TaskItem(resxFile)); + outputResources.Add(new TaskItem(resourcesFile)); + File.Delete(resourcesFile); + } + + GenerateResource t = Utilities.CreateTaskOutOfProc(); + t.Sources = sources.ToArray(); + t.OutputResources = outputResources.ToArray(); + t.StronglyTypedLanguage = null; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + t.SdkToolsPath = sdkToolsPath; + Assert.IsTrue(t.Execute(), "Task should have completed succesfully"); + + Utilities.AssertLogContains(t, "/compile"); + foreach (ITaskItem i in sources) + { + Utilities.AssertLogContains(t, i.ItemSpec); + } + foreach (ITaskItem i in outputResources) + { + Utilities.AssertLogContains(t, i.ItemSpec); + } + } + finally + { + foreach (ITaskItem i in sources) + { + File.Delete(i.ItemSpec); + } + + foreach (ITaskItem i in outputResources) + { + File.Delete(i.ItemSpec); + } + } + } + + /// + /// Personalized resx creator. + /// + /// Path in which to create the resx file + /// File name of the created resx + /// Path to the resx file + private string MyResxFileCreator(string pathName, string fileName) + { + Directory.CreateDirectory(pathName); + string resgenFile = Path.Combine(pathName, fileName + ".resx"); + if (File.Exists(resgenFile)) + { + File.Delete(resgenFile); + } + File.WriteAllText(resgenFile, Utilities.GetTestResXContent(false, null, null, false)); + return resgenFile; + } + + /// + /// In order to make GenerateResource multitargetable, a property, ExecuteAsTool, was added. + /// In order to have correct behavior when using pre-4.0 + /// toolsversions, ExecuteAsTool must default to true, and the paths to the tools will be the + /// v3.5 path. It is difficult to verify the tool paths in a unit test, however, so + /// this was done by ad hoc testing and will be maintained by the dev suites. + /// + [TestMethod] + public void MultiTargetingDefaultsSetCorrectly() + { + GenerateResource t = new GenerateResource(); + + Assert.IsTrue(t.ExecuteAsTool, "ExecuteAsTool should default to true"); + } + } +} + +namespace Microsoft.Build.UnitTests.GenerateResource_Tests +{ + /// + /// This Utilities class provides some static helper methods for resource tests + /// + internal sealed partial class Utilities + { + /// + /// This method creates a GenerateResource task and performs basic setup on it, e.g. BuildEngine + /// + public static GenerateResource CreateTaskOutOfProc() + { + GenerateResource t = CreateTask(); + t.ExecuteAsTool = true; + t.SdkToolsPath = ToolLocationHelper.GetPathToDotNetFrameworkSdk(TargetDotNetFrameworkVersion.VersionLatest); + + return t; + } + + /// + /// Asserts if the passed in ITaskItem array contains any items that are not tlogs + /// + /// + public static void AssertContainsOnlyTLogs(ITaskItem[] filesWritten) + { + foreach (ITaskItem file in filesWritten) + { + Assert.IsTrue(Path.GetExtension(file.ItemSpec).Equals(".tlog", StringComparison.OrdinalIgnoreCase), "The only files written should be tlogs"); + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/GenerateResource_Tests.cs b/src/XMakeTasks/UnitTests/GenerateResource_Tests.cs new file mode 100644 index 00000000000..ef8719a7479 --- /dev/null +++ b/src/XMakeTasks/UnitTests/GenerateResource_Tests.cs @@ -0,0 +1,3371 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.Text.RegularExpressions; +using System.Text; + +namespace Microsoft.Build.UnitTests.GenerateResource_Tests.InProc +{ + [TestClass] + sealed public class RequiredTransformations + { + /// + /// ResX to Resources, no references + /// + [TestMethod] + public void BasicResX2Resources() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing BasicResX2Resources() test"); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + try + { + string resxFile = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.Sources[0].SetMetadata("Attribute", "InputValue"); + + Utilities.ExecuteTask(t); + + Assert.AreEqual("InputValue", t.OutputResources[0].GetMetadata("Attribute")); + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", resxFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 1, resxFile); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Ensure that OutputResource Metadata is populated on the Sources item + /// + [TestMethod] + public void OutputResourceMetadataPopulatedOnInputItems() + { + string resxFile0 = Utilities.WriteTestResX(false, null, null); + string resxFile1 = Utilities.WriteTestResX(false, null, null); + string resxFile2 = Utilities.WriteTestResX(false, null, null); + string resxFile3 = Utilities.WriteTestResX(false, null, null); + + string expectedOutFile0 = Path.Combine(Path.GetTempPath(), Path.ChangeExtension(resxFile0, ".resources")); + string expectedOutFile1 = Path.Combine(Path.GetTempPath(), "resx1.foo.resources"); + string expectedOutFile2 = Path.Combine(Path.GetTempPath(), Utilities.GetTempFileName(".resources")); + string expectedOutFile3 = Path.Combine(Path.GetTempPath(), Utilities.GetTempFileName(".resources")); + + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { + new TaskItem(resxFile0), new TaskItem(resxFile1), new TaskItem(resxFile2), new TaskItem(resxFile3) }; + + t.OutputResources = new ITaskItem[] { + new TaskItem(expectedOutFile0), new TaskItem(expectedOutFile1), new TaskItem(expectedOutFile2), new TaskItem(expectedOutFile3) }; + + Utilities.ExecuteTask(t); + + Assert.AreEqual(expectedOutFile0, t.Sources[0].GetMetadata("OutputResource")); + Assert.AreEqual(expectedOutFile1, t.Sources[1].GetMetadata("OutputResource")); + Assert.AreEqual(expectedOutFile2, t.Sources[2].GetMetadata("OutputResource")); + Assert.AreEqual(expectedOutFile3, t.Sources[3].GetMetadata("OutputResource")); + + // Done, so clean up. + File.Delete(resxFile0); + File.Delete(resxFile1); + File.Delete(resxFile2); + File.Delete(resxFile3); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Text to Resources + /// + [TestMethod] + public void BasicText2Resources() + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.Sources[0].SetMetadata("Attribute", "InputValue"); + + Utilities.ExecuteTask(t); + + Assert.AreEqual("InputValue", t.OutputResources[0].GetMetadata("Attribute")); + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// ResX to Resources with references that are used in the resx + /// + /// System dll is not locked because it forces a new app domain + [TestMethod] + public void ResX2ResourcesWithReferences() + { + string systemDll = Utilities.GetPathToCopiedSystemDLL(); + string resxFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + + resxFile = Utilities.WriteTestResX(true /*system type*/, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem(systemDll) }; + + Utilities.ExecuteTask(t); + + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", resxFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 2, resxFile); + } + finally + { + File.Delete(systemDll); + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + } + } + + /// + /// Resources to ResX + /// + [TestMethod] + public void BasicResources2ResX() + { + string resourcesFile = Utilities.CreateBasicResourcesFile(false); + + // Fork 1: create a resx file directly from the resources + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resourcesFile, ".resx")) }; + Utilities.ExecuteTask(t); + Assert.IsTrue(Path.GetExtension(t.FilesWritten[0].ItemSpec) == ".resx"); + + // Fork 2a: create a text file from the resources + GenerateResource t2a = Utilities.CreateTask(); + t2a.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t2a.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resourcesFile, ".txt")) }; + Utilities.ExecuteTask(t2a); + Assert.IsTrue(Path.GetExtension(t2a.FilesWritten[0].ItemSpec) == ".txt"); + + // Fork 2b: create a resx file from the text file + GenerateResource t2b = Utilities.CreateTask(); + t2b.Sources = new ITaskItem[] { new TaskItem(t2a.FilesWritten[0].ItemSpec) }; + t2b.OutputResources = new ITaskItem[] { new TaskItem(Utilities.GetTempFileName(".resx")) }; + Utilities.ExecuteTask(t2b); + Assert.IsTrue(Path.GetExtension(t2b.FilesWritten[0].ItemSpec) == ".resx"); + + // make sure the output resx files from each fork are the same + Assert.AreEqual(Utilities.ReadFileContent(t.OutputResources[0].ItemSpec), + Utilities.ReadFileContent(t2b.OutputResources[0].ItemSpec)); + Utilities.AssertLogContainsResource(t2b, "GenerateResource.ProcessingFile", t2b.Sources[0].ItemSpec, t2b.OutputResources[0].ItemSpec); + Utilities.AssertLogContainsResource(t2b, "GenerateResource.ReadResourceMessage", 4, t2b.Sources[0].ItemSpec); + + // Done, so clean up. + File.Delete(resourcesFile); + File.Delete(t.OutputResources[0].ItemSpec); + File.Delete(t2a.OutputResources[0].ItemSpec); + foreach (ITaskItem item in t2b.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Resources to Text + /// + [TestMethod] + public void BasicResources2Text() + { + string resourcesFile = Utilities.CreateBasicResourcesFile(false); + + GenerateResource t = Utilities.CreateTask(); + + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + + string outputFile = Path.ChangeExtension(resourcesFile, ".txt"); + t.OutputResources = new ITaskItem[] { new TaskItem(outputFile) }; + Utilities.ExecuteTask(t); + + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".txt"); + Assert.AreEqual(Utilities.GetTestTextContent(null, null, true /*cleaned up */), Utilities.ReadFileContent(resourcesFile)); + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", t.Sources[0].ItemSpec, outputFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, t.Sources[0].ItemSpec); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Force out-of-date with ShouldRebuildResgenOutputFile on the source only + /// + [TestMethod] + public void ForceOutOfDate() + { + string resxFile = Utilities.WriteTestResX(false, null, null); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + try + { + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + GenerateResource t2 = Utilities.CreateTask(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + System.Threading.Thread.Sleep(200); + File.SetLastWriteTime(resxFile, DateTime.Now); + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec), time) > 0); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Force out-of-date with ShouldRebuildResgenOutputFile on the linked file + /// + [TestMethod] + public void ForceOutOfDateLinked() + { + string bitmap = Utilities.CreateWorldsSmallestBitmap(); + string resxFile = Utilities.WriteTestResX(false, bitmap, null, false); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + try + { + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + GenerateResource t2 = Utilities.CreateTask(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + System.Threading.Thread.Sleep(200); + File.SetLastWriteTime(bitmap, DateTime.Now); + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec), time) > 0); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(bitmap); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Force partially out-of-date: should build only the out of date inputs + /// + [TestMethod] + public void ForceSomeOutOfDate() + { + string resxFile = null; + string resxFile2 = null; + string cache = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + resxFile2 = Utilities.WriteTestResX(false, null, null); + cache = Utilities.GetTempFileName(".cache"); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(cache); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile2) }; + + // Transform both + Utilities.ExecuteTask(t); + + // Create a new task to transform them again + GenerateResource t2 = Utilities.CreateTask(); + t2.StateFile = new TaskItem(t.StateFile.ItemSpec); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile2) }; + + // Get current write times of outputs + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + DateTime time2 = File.GetLastWriteTime(t.OutputResources[1].ItemSpec); + System.Threading.Thread.Sleep(200); + // Touch one input + File.SetLastWriteTime(resxFile, DateTime.Now); + + Utilities.ExecuteTask(t2); + + // Check only one output was updated + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec), time) > 0); + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t2.OutputResources[1].ItemSpec), time2) == 0); + + // Although only one file was updated, both should be in OutputResources and FilesWritten + Assert.IsTrue(t2.OutputResources[0].ItemSpec == t.OutputResources[0].ItemSpec); + Assert.IsTrue(t2.OutputResources[1].ItemSpec == t.OutputResources[1].ItemSpec); + Assert.IsTrue(t2.FilesWritten[0].ItemSpec == t.FilesWritten[0].ItemSpec); + Assert.IsTrue(t2.FilesWritten[1].ItemSpec == t.FilesWritten[1].ItemSpec); + } + finally + { + if (null != resxFile) File.Delete(resxFile); + if (null != resxFile2) File.Delete(resxFile2); + if (null != cache) File.Delete(cache); + if (null != resxFile) File.Delete(Path.ChangeExtension(resxFile, ".resources")); + if (null != resxFile2) File.Delete(Path.ChangeExtension(resxFile2, ".resources")); + } + } + + /// + /// Allow ShouldRebuildResgenOutputFile to return "false" since nothing's out of date, including linked file + /// + [TestMethod] + public void AllowLinkedNoGenerate() + { + string bitmap = Utilities.CreateWorldsSmallestBitmap(); + string resxFile = Utilities.WriteTestResX(false, bitmap, null, false); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + try + { + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + + GenerateResource t2 = Utilities.CreateTask(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + + System.Threading.Thread.Sleep(500); + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(time.Equals(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec))); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(bitmap); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Allow the task to skip processing based on having nothing out of date + /// + [TestMethod] + public void NothingOutOfDate() + { + string resxFile = null; + string txtFile = null; + string resourcesFile1 = null; + string resourcesFile2 = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + txtFile = Utilities.WriteTestText(null, null); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(txtFile) }; + resourcesFile1 = Path.ChangeExtension(resxFile, ".resources"); + resourcesFile2 = Path.ChangeExtension(txtFile, ".resources"); + + Utilities.ExecuteTask(t); + + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t.OutputResources[1].ItemSpec == resourcesFile2); + Assert.IsTrue(t.FilesWritten[1].ItemSpec == resourcesFile2); + + Utilities.AssertStateFileWasWritten(t); + + // Repeat, and it should do nothing as they are up to date + GenerateResource t2 = Utilities.CreateTask(); + t2.StateFile = new TaskItem(t.StateFile); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(txtFile) }; + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + DateTime time2 = File.GetLastWriteTime(t.OutputResources[1].ItemSpec); + System.Threading.Thread.Sleep(200); + + Utilities.ExecuteTask(t2); + // Although everything was up to date, OutputResources and FilesWritten + // must contain the files that would have been created if they weren't up to date. + Assert.IsTrue(t2.OutputResources[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t2.FilesWritten[0].ItemSpec == resourcesFile1); + Assert.IsTrue(t2.OutputResources[1].ItemSpec == resourcesFile2); + Assert.IsTrue(t2.FilesWritten[1].ItemSpec == resourcesFile2); + + Utilities.AssertStateFileWasWritten(t2); + + Assert.IsTrue(time.Equals(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec))); + Assert.IsTrue(time2.Equals(File.GetLastWriteTime(t2.OutputResources[1].ItemSpec))); + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile1 != null) File.Delete(resourcesFile1); + if (resourcesFile2 != null) File.Delete(resourcesFile2); + } + } + + /// + /// If the reference has been touched, it should rebuild even if the inputs are + /// otherwise up to date + /// + /// System dll is not locked because it forces a new app domain + [TestMethod] + public void NothingOutOfDateExceptReference() + { + string resxFile = null; + string resourcesFile = null; + string systemDll = Utilities.GetPathToCopiedSystemDLL(); + + try + { + resxFile = Utilities.WriteTestResX(true /* uses system type */, null, null); + + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem(systemDll) }; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + Utilities.ExecuteTask(t); + + DateTime time = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + + // Repeat, and it should do nothing as they are up to date + GenerateResource t2 = Utilities.CreateTask(); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t2.References = new ITaskItem[] { new TaskItem(systemDll) }; + t2.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t2); + Assert.IsTrue(time.Equals(File.GetLastWriteTime(t2.OutputResources[0].ItemSpec))); + + // Touch the reference, and repeat, it should now rebuild + DateTime newTime = DateTime.Now + new TimeSpan(0, 1, 0); + File.SetLastWriteTime(systemDll, newTime); + GenerateResource t3 = Utilities.CreateTask(); + t3.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t3.References = new ITaskItem[] { new TaskItem(systemDll) }; + t3.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t3); + Assert.IsTrue(DateTime.Compare(File.GetLastWriteTime(t3.OutputResources[0].ItemSpec), time) > 0); + resourcesFile = t3.OutputResources[0].ItemSpec; + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (systemDll != null) File.Delete(systemDll); + } + } + + /// + /// If an additional input is out of date, resources should be regenerated. + /// + [TestMethod] + public void NothingOutOfDateExceptAdditionalInput() + { + string resxFile = null; + string resourcesFile = null; + ITaskItem[] additionalInputs = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + additionalInputs = new ITaskItem[] { new TaskItem(FileUtilities.GetTemporaryFile()), new TaskItem(FileUtilities.GetTemporaryFile()) }; + + foreach (ITaskItem file in additionalInputs) + { + if (!File.Exists(file.ItemSpec)) + { + File.WriteAllText(file.ItemSpec, ""); + } + } + + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.AdditionalInputs = additionalInputs; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + Utilities.ExecuteTask(t); + + // Repeat, and it should do nothing as they are up to date + GenerateResource t2 = Utilities.CreateTask(); + t2.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t2.AdditionalInputs = additionalInputs; + t2.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t2); + Utilities.AssertLogContainsResource(t2, "GenerateResource.NothingOutOfDate", ""); + + // Touch one of the additional inputs and repeat, it should now rebuild + DateTime newTime = DateTime.Now + new TimeSpan(0, 1, 0); + File.SetLastWriteTime(additionalInputs[1].ItemSpec, newTime); + GenerateResource t3 = Utilities.CreateTask(); + t3.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t3.AdditionalInputs = additionalInputs; + t3.StateFile = new TaskItem(t.StateFile); + Utilities.ExecuteTask(t3); + Utilities.AssertLogNotContainsResource(t3, "GenerateResource.NothingOutOfDate", ""); + resourcesFile = t3.OutputResources[0].ItemSpec; + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (additionalInputs != null && additionalInputs[0] != null && File.Exists(additionalInputs[0].ItemSpec)) File.Delete(additionalInputs[0].ItemSpec); + if (additionalInputs != null && additionalInputs[1] != null && File.Exists(additionalInputs[1].ItemSpec)) File.Delete(additionalInputs[1].ItemSpec); + } + } + + /// + /// Text to ResX + /// + [TestMethod] + public void BasicText2ResX() + { + GenerateResource t = Utilities.CreateTask(); + + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(textFile, ".resx")) }; + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resx"); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Round trip from resx to resources to resx with the same blobs + /// + [TestMethod] + public void ResX2ResX() + { + try + { + string resourcesFile = Utilities.CreateBasicResourcesFile(true); + + // Step 1: create a resx file directly from the resources, to get a framework generated resx + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resourcesFile, ".resx")) }; + Utilities.ExecuteTask(t); + Assert.IsTrue(Path.GetExtension(t.FilesWritten[0].ItemSpec) == ".resx"); + + // Step 2a: create a resources file from the resx + GenerateResource t2a = Utilities.CreateTask(); + t2a.Sources = new ITaskItem[] { new TaskItem(t.FilesWritten[0].ItemSpec) }; + t2a.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(t.FilesWritten[0].ItemSpec, ".resources")) }; + Utilities.ExecuteTask(t2a); + Assert.IsTrue(Path.GetExtension(t2a.FilesWritten[0].ItemSpec) == ".resources"); + + // Step 2b: create a resx from the resources + GenerateResource t2b = Utilities.CreateTask(); + t2b.Sources = new ITaskItem[] { new TaskItem(t2a.FilesWritten[0].ItemSpec) }; + t2b.OutputResources = new ITaskItem[] { new TaskItem(Utilities.GetTempFileName(".resx")) }; + File.Delete(t2b.OutputResources[0].ItemSpec); + Utilities.ExecuteTask(t2b); + Assert.IsTrue(Path.GetExtension(t2b.FilesWritten[0].ItemSpec) == ".resx"); + + // make sure the output resx files from each fork are the same + Assert.AreEqual(Utilities.ReadFileContent(t.OutputResources[0].ItemSpec), + Utilities.ReadFileContent(t2b.OutputResources[0].ItemSpec)); + + // Done, so clean up. + File.Delete(resourcesFile); + File.Delete(t.OutputResources[0].ItemSpec); + File.Delete(t2a.OutputResources[0].ItemSpec); + foreach (ITaskItem item in t2b.FilesWritten) + File.Delete(item.ItemSpec); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + + /// + /// Round trip from text to resources to text with the same blobs + /// + [TestMethod] + public void Text2Text() + { + string textFile = Utilities.WriteTestText(null, null); + + // Round 1, do the Text2Resource + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + + Utilities.ExecuteTask(t); + + // make sure round 1 is successful + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + // round 2, do the resources2Text from the same file + GenerateResource t2 = Utilities.CreateTask(); + + t2.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + string outputFile = Utilities.GetTempFileName(".txt"); + t2.OutputResources = new ITaskItem[] { new TaskItem(outputFile) }; + Utilities.ExecuteTask(t2); + + resourcesFile = t2.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".txt"); + + Assert.AreEqual(Utilities.GetTestTextContent(null, null, true /*cleaned up */), Utilities.ReadFileContent(resourcesFile)); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + File.Delete(t2.Sources[0].ItemSpec); + foreach (ITaskItem item in t2.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// STR without references yields proper output, message + /// + [TestMethod] + public void StronglyTypedResources() + { + GenerateResource t = Utilities.CreateTask(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + // STR class name should have been generated from the output + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Utilities.AssertStateFileWasWritten(t); + // Files written should contain STR class file + string stronglyTypedFileName = Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs"); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == stronglyTypedFileName); + Assert.IsTrue(File.Exists(stronglyTypedFileName)); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", stronglyTypedFileName); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR without references yields proper output, message + /// + [TestMethod] + public void StronglyTypedResourcesUpToDate() + { + GenerateResource t = Utilities.CreateTask(); + GenerateResource t2 = Utilities.CreateTask(); + try + { + string textFile = Utilities.WriteTestText(null, null); + + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + // STR class name should have been generated from the output + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + // Files written should contain STR class file + string stronglyTypedFileName = Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs"); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == stronglyTypedFileName); + Assert.IsTrue(File.Exists(stronglyTypedFileName)); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", stronglyTypedFileName); + + // Now that we have done it, do it again to make sure that we don't do + t2.StateFile = new TaskItem(t.StateFile); + + t2.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t2.StronglyTypedLanguage = "CSharp"; + + Utilities.ExecuteTask(t2); + + Assert.IsTrue(t2.OutputResources[0].ItemSpec == resourcesFile); + Assert.IsTrue(t2.FilesWritten[0].ItemSpec == resourcesFile); + + Utilities.AssertStateFileWasWritten(t2); + Assert.IsTrue(t2.FilesWritten[2].ItemSpec == Path.ChangeExtension(t2.Sources[0].ItemSpec, ".cs")); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + foreach (ITaskItem item in t2.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR class file is out of date, but resources are up to date. Should still generate it. + /// + [TestMethod] + public void StronglyTypedResourcesOutOfDate() + { + string resxFile = null; + string resourcesFile = null; + string strFile = null; + string stateFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Utilities.GetTempFileName(".resources"); + strFile = Path.ChangeExtension(resourcesFile, ".cs"); // STR filename should be generated from output not input filename + stateFile = Utilities.GetTempFileName(".cache"); + + // Make sure the .cs file isn't already there. + File.Delete(strFile); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StateFile = new TaskItem(stateFile); + Utilities.ExecuteTask(t); + + // STR class name generated from output resource file name + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(resourcesFile); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == strFile); + Assert.IsTrue(File.Exists(strFile)); + + // Repeat. It should not update either file. + // First move both the timestamps back so they're still up to date, + // but we'd know if they were updated (this is quicker than sleeping and okay as there's no cache being used) + Utilities.MoveBackTimestamp(resxFile, 1); + DateTime strTime = Utilities.MoveBackTimestamp(strFile, 1); + t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StateFile = new TaskItem(stateFile); + Utilities.ExecuteTask(t); + Utilities.AssertLogContainsResource(t, "GenerateResource.NothingOutOfDate", ""); + Assert.IsTrue(!Utilities.FileUpdated(strFile, strTime)); // Was not updated + + // OK, now delete the STR class file + File.Delete(strFile); + + // Repeat. It should recreate the STR class file + t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StateFile = new TaskItem(stateFile); + Utilities.ExecuteTask(t); + Assert.IsTrue(Utilities.FileUpdated(strFile, strTime)); // Was updated + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile); + Assert.IsTrue(File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == strFile); + Assert.IsTrue(File.Exists(strFile)); + + // OK, now delete the STR class file again + File.Delete(strFile); + + // Repeat, but specify the filename this time, instead of having it generated from the output resources + // It should recreate the STR class file again + t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.StronglyTypedLanguage = "C#"; + t.StronglyTypedFileName = strFile; + Utilities.ExecuteTask(t); + Assert.IsTrue(File.Exists(strFile)); + } + finally + { + if (resxFile != null) File.Delete(resxFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (strFile != null) File.Delete(strFile); + } + } + + /// + /// Verify STR generation with a specified specific filename + /// + [TestMethod] + public void StronglyTypedResourcesWithFilename() + { + string txtFile = null; + string strFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + + txtFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedLanguage = "CSharp"; + strFile = FileUtilities.GetTemporaryFile(); + t.StronglyTypedFileName = strFile; + + Utilities.ExecuteTask(t); + + // Check resources is output + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(Path.GetExtension(t.FilesWritten[0].ItemSpec) == ".resources"); + Assert.IsTrue(File.Exists(resourcesFile)); + + // Check STR file is output + Assert.IsTrue(t.FilesWritten[1].ItemSpec == strFile); + Assert.IsTrue(t.StronglyTypedFileName == strFile); + Assert.IsTrue(File.Exists(strFile)); + + // Check log + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", txtFile, t.OutputResources[0].ItemSpec); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, txtFile); + + string typeName = ""; + if (t.StronglyTypedNamespace != null) + { + typeName = t.StronglyTypedNamespace + "."; + } + + typeName += t.StronglyTypedClassName; + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", strFile); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (strFile != null) File.Delete(strFile); + } + } + + /// + /// STR with VB + /// + [TestMethod] + public void StronglyTypedResourcesVB() + { + GenerateResource t = Utilities.CreateTask(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "VB"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + // FilesWritten should contain STR class file + string stronglyTypedFileName = Path.ChangeExtension(t.Sources[0].ItemSpec, ".vb"); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == stronglyTypedFileName); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + Assert.IsTrue(File.Exists(stronglyTypedFileName)); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", stronglyTypedFileName); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR namespace can be empty + /// + [TestMethod] + public void StronglyTypedResourcesWithoutNamespaceOrClassOrFilename() + { + GenerateResource t = Utilities.CreateTask(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + string resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + // Should have defaulted the STR filename to the bare output resource name + ".cs" + string STRfile = Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs"); + Assert.IsTrue(t.StronglyTypedFileName == STRfile); + Assert.IsTrue(File.Exists(STRfile)); + + // Should have defaulted the class name to the bare output resource name + Assert.IsTrue(t.StronglyTypedClassName == Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec)); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", t.StronglyTypedFileName); + + // Should not have used a namespace + Assert.IsTrue(!File.ReadAllText(t.StronglyTypedFileName).Contains("namespace")); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR with resource namespace yields proper output, message (CS) + /// + [TestMethod] + public void STRWithResourcesNamespaceCS() + { + Utilities.STRNamespaceTestHelper("CSharp", "MyResourcesNamespace", null); + } + + /// + /// STR with resource namespace yields proper output, message (VB) + /// + [TestMethod] + public void STRWithResourcesNamespaceVB() + { + Utilities.STRNamespaceTestHelper("VB", "MyResourcesNamespace", null); + } + + /// + /// STR with resource namespace and STR namespace yields proper output, message (CS) + /// + [TestMethod] + public void STRWithResourcesNamespaceAndSTRNamespaceCS() + { + Utilities.STRNamespaceTestHelper("CSharp", "MyResourcesNamespace", "MySTClassNamespace"); + } + + /// + /// STR with resource namespace and STR namespace yields proper output, message (CS) + /// + [TestMethod] + public void STRWithResourcesNamespaceAndSTRNamespaceVB() + { + Utilities.STRNamespaceTestHelper("VB", "MyResourcesNamespace", "MySTClassNamespace"); + } + } + + [TestClass] + sealed public class TransformationErrors + { + /// + /// Text input failures, no name, no '=', 'strings' token, invalid token, invalid escape + /// + [TestMethod] + public void TextToResourcesBadFormat() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing TextToResourcesBadFormat() test"); + + // The first string in each row is passed into the text block that's created in the file + // The second string is a fragment of the expected error message + string[][] tests = new string[][] { + // invalid token in file, "unsupported square bracket keyword" + new string[] { "[goober]", "MSB3563" }, + // no '=', "resource line without an equals sign" + new string[] { "abcdefaghha", "MSB3564" }, + // no name, "resource line without a name" + new string[] { "=abced", "MSB3565" }, + // invalid escape, "unsupported or invalid escape character" + new string[] { "abc=de\\efght", "MSB3566" }, + // another invalid escape, this one more serious, "unsupported or invalid escape character" + new string[] { @"foo=\ujjjjbar", "MSB3569"}, + }; + + GenerateResource t = null; + string textFile = null; + + foreach (string[] test in tests) + { + t = Utilities.CreateTask(); + + textFile = Utilities.WriteTestText(null, test[0]); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.Execute(); + Utilities.AssertLogContains(t, test[1]); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + // text file uses the strings token; since it's only a warning we have to have special asserts + t = Utilities.CreateTask(); + + textFile = Utilities.WriteTestText(null, "[strings]"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + bool success = t.Execute(); + // Task should have succeeded (it was just a warning) + Assert.IsTrue(success); + // warning that 'strings' is an obsolete tag + Utilities.AssertLogContains(t, "MSB3562"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Cause failures in ResXResourceReader + /// + [TestMethod] + public void FailedResXReader() + { + string resxFile1 = null; + string resxFile2 = null; + string resourcesFile1 = null; + string resourcesFile2 = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + // Invalid one + resxFile1 = Utilities.WriteTestResX(false, null, " >>>>>\xd\xa Assembly\xd\xa \xd\xa", false); + // Also include a valid one. It should still get processed + resxFile2 = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile1), new TaskItem(resxFile2) }; + resourcesFile1 = Path.ChangeExtension(resxFile1, ".resources"); + resourcesFile2 = Path.ChangeExtension(resxFile2, ".resources"); + File.Delete(resourcesFile1); + File.Delete(resourcesFile2); + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertStateFileWasWritten(t); + // Should not have created an output for the invalid resx + // Should have created the other file + Assert.IsTrue(!File.Exists(resourcesFile1)); + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile2); + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile2); + Assert.IsTrue(File.Exists(resourcesFile2)); + + // "error in resource file" with exception from the framework + Utilities.AssertLogContains(t, "MSB3103"); + } + finally + { + if (null != resxFile1) File.Delete(resxFile1); + if (null != resxFile2) File.Delete(resxFile2); + if (null != resourcesFile1) File.Delete(resourcesFile1); + if (null != resourcesFile2) File.Delete(resourcesFile2); + } + } + + /// + /// Cause failures in ResXResourceReader, different codepath + /// + [TestMethod] + public void FailedResXReaderWithAllOutputResourcesSpecified() + { + string resxFile1 = null; + string resxFile2 = null; + string resourcesFile1 = null; + string resourcesFile2 = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + // Invalid one + resxFile1 = Utilities.WriteTestResX(false, null, " >>>>>\xd\xa Assembly\xd\xa \xd\xa", false); + // Also include a valid one. It should still get processed + resxFile2 = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile1), new TaskItem(resxFile2) }; + resourcesFile1 = Path.ChangeExtension(resxFile1, ".resources"); + resourcesFile2 = Path.ChangeExtension(resxFile2, ".resources"); + File.Delete(resourcesFile1); + File.Delete(resourcesFile2); + t.OutputResources = new ITaskItem[] { new TaskItem(resourcesFile1), new TaskItem(resourcesFile2) }; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertStateFileWasWritten(t); + + // Should not have created an output for the invalid resx + // Should have created the other file + Assert.IsTrue(!File.Exists(resourcesFile1)); + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile2); + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == resourcesFile2); + Assert.IsTrue(File.Exists(resourcesFile2)); + + // "error in resource file" with exception from the framework + Utilities.AssertLogContains(t, "MSB3103"); + +#if false // we can't do this because FX strings ARE localized -- VSW#455956 + // This is a literal because it comes from the ResX parser in the framework + Utilities.AssertLogContains(t, "'valueAB' start tag on line 18 does not match the end tag of 'value'"); +#endif + // so just look for the unlocalizable portions + Utilities.AssertLogContains(t, "valueAB"); + Utilities.AssertLogContains(t, "value"); + } + finally + { + if (null != resxFile1) File.Delete(resxFile1); + if (null != resxFile2) File.Delete(resxFile2); + if (null != resourcesFile1) File.Delete(resourcesFile1); + if (null != resourcesFile2) File.Delete(resourcesFile2); + } + } + + /// + /// Duplicate resource names + /// + [TestMethod] + public void DuplicateResourceNames() + { + GenerateResource t = Utilities.CreateTask(); + + string textFile = Utilities.WriteTestText(null, "Marley=some guy from Jamaica"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + bool success = t.Execute(); + // Task should have succeeded (it was just a warning) + Assert.IsTrue(success); + + // "duplicate resource name" + Utilities.AssertLogContains(t, "MSB3568"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Non-string resource with text output + /// + [TestMethod] + public void UnsupportedTextType() + { + string bitmap = Utilities.CreateWorldsSmallestBitmap(); + string resxFile = Utilities.WriteTestResX(false, bitmap, null, false); + + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(Path.ChangeExtension(resxFile, ".txt")) }; + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + // "only strings can be written to a .txt file" + Utilities.AssertLogContains(t, "MSB3556"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(bitmap); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Can't write the statefile + /// + [TestMethod] + public void InvalidStateFile() + { + string resxFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + resxFile = Utilities.WriteTestResX(false, null, null); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.StateFile = new TaskItem("||invalid filename||"); + + // Should still succeed + Assert.IsTrue(t.Execute()); + + resourcesFile = t.OutputResources[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + Assert.IsTrue(t.FilesWritten[0].ItemSpec == t.OutputResources[0].ItemSpec); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", resxFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 1, resxFile); + } + finally + { + if (null != resxFile) File.Delete(resxFile); + if (null != resourcesFile) File.Delete(resourcesFile); + } + } + + /// + /// Cause failures in ResourceReader + /// + [TestMethod] + public void FailedResourceReader() + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + // to cause a failure, we're going to transform a bad .resources file to a .resx + // the simplest thing is to create a .resx, but call it .resources + string resxFile = Utilities.WriteTestResX(false, null, null); + string resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Move(resxFile, resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resxFile) }; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // "error in resource file" with exception from the framework + Utilities.AssertLogContains(t, "MSB3103"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Invalid STR Class name + /// + [TestMethod] + public void FailedSTRProperty() + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, "object=some string"); + string resourcesFile = Path.ChangeExtension(textFile, ".resources"); + + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + // Invalid class name + t.StronglyTypedClassName = "~!@#$%^&*("; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 5, textFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", t.StronglyTypedFileName); + Utilities.AssertLogContains(t, "MSB3570"); + Utilities.AssertLogContains(t, t.StronglyTypedFileName); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Reference passed in that can't be loaded should error + /// + [TestMethod] + public void InvalidReference() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + + // Create resx with invalid ref "INVALID" + txtFile = Utilities.WriteTestResX(false, null, null, true /*data with invalid type*/); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.References = new TaskItem[] { new TaskItem("INVALID") }; + + bool result = t.Execute(); + // Task should have failed + Assert.IsTrue(!result); + + // Should have not written any files + Assert.IsTrue(t.FilesWritten != null && t.FilesWritten.Length == 0); + Assert.IsTrue(!File.Exists(resourcesFile)); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + } + + [TestClass] + sealed public class PropertyHandling + { + /// + /// Sources attributes are copied to given OutputResources + /// + [TestMethod] + public void AttributeForwarding() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing AttributeForwarding() test"); + + GenerateResource t = Utilities.CreateTask(); + + string resxFile = Utilities.WriteTestResX(false, null, null); + ITaskItem i = new TaskItem(resxFile); + i.SetMetadata("Locale", "en-GB"); + t.Sources = new ITaskItem[] { i }; + + ITaskItem o = new TaskItem("MyAlternateResource.resources"); + o.SetMetadata("Locale", "fr"); + o.SetMetadata("Flavor", "Pumpkin"); + t.OutputResources = new ITaskItem[] { o }; + + Utilities.ExecuteTask(t); + + // Locale was forward from source item and should overwrite the 'fr' + // locale that the output item originally had. + Assert.AreEqual("fr", t.OutputResources[0].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual("MyAlternateResource.resources", t.OutputResources[0].ItemSpec); + + // Attributes not on Sources should be left untouched. + Assert.AreEqual("Pumpkin", t.OutputResources[0].GetMetadata("Flavor")); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// Sources attributes copied to computed OutputResources + /// + [TestMethod] + public void AttributeForwardingOnEmptyOutputs() + { + GenerateResource t = Utilities.CreateTask(); + + string resxFile = Utilities.WriteTestResX(false, null, null); + ITaskItem i = new TaskItem(resxFile); + i.SetMetadata("Locale", "en-GB"); + t.Sources = new ITaskItem[] { i }; + + Utilities.ExecuteTask(t); + + // Output ItemSpec should be computed from input + string resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + Assert.AreEqual(resourcesFile, t.OutputResources[0].ItemSpec); + + // Attribute from source should be copied to output + Assert.AreEqual("en-GB", t.OutputResources[0].GetMetadata("Locale")); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// OutputFiles used for output, and also are synthesized if not set on input + /// + [TestMethod] + public void OutputFilesNotSpecified() + { + GenerateResource t = Utilities.CreateTask(); + + t.Sources = new ITaskItem[] { + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null)), + }; + + Utilities.ExecuteTask(t); + + // Output ItemSpec should be computed from input + for (int i = 0; i < t.Sources.Length; i++) + { + string outputFile = Path.ChangeExtension(t.Sources[i].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[i].ItemSpec); + } + + // Done, so clean up. + foreach (ITaskItem item in t.Sources) + File.Delete(item.ItemSpec); + foreach (ITaskItem item in t.FilesWritten) + File.Delete(item.ItemSpec); + } + + /// + /// FilesWritten contains OutputResources + StateFile + /// + [TestMethod] + public void FilesWrittenSet() + { + GenerateResource t = Utilities.CreateTask(); + + t.Sources = new ITaskItem[] { + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null) ), + new TaskItem( Utilities.WriteTestResX(false, null, null)), + }; + + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + int i = 0; + + // should be four files written, not including the tlogs + for (i = 0; i < 4; i++) + { + Assert.AreEqual(t.FilesWritten[i].ItemSpec, t.OutputResources[i].ItemSpec); + Assert.IsTrue(File.Exists(t.FilesWritten[i].ItemSpec)); + } + + Utilities.AssertStateFileWasWritten(t); + + // Done, so clean up. + File.Delete(t.StateFile.ItemSpec); + foreach (ITaskItem item in t.Sources) + { + File.Delete(item.ItemSpec); + } + foreach (ITaskItem item in t.FilesWritten) + { + File.Delete(item.ItemSpec); + } + } + + /// + /// Resource transformation fails on 3rd of 4 inputs, inputs 1 & 2 & 4 are in outputs and fileswritten. + /// + [TestMethod] + public void OutputFilesPartialInputs() + { + GenerateResource t = Utilities.CreateTask(); + + try + { + t.Sources = new ITaskItem[] { + new TaskItem( Utilities.WriteTestText(null, null) ), + new TaskItem( Utilities.WriteTestText(null, null) ), + new TaskItem( Utilities.WriteTestText("goober", null) ), + new TaskItem( Utilities.WriteTestText(null, null)), + }; + + foreach (ITaskItem taskItem in t.Sources) + { + File.Delete(Path.ChangeExtension(taskItem.ItemSpec, ".resources")); + } + + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + string outputFile = Path.ChangeExtension(t.Sources[0].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[0].ItemSpec); + Assert.IsTrue(File.Exists(t.OutputResources[0].ItemSpec)); + outputFile = Path.ChangeExtension(t.Sources[1].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[1].ItemSpec); + Assert.IsTrue(File.Exists(t.OutputResources[1].ItemSpec)); + // Sources[2] should NOT have been converted and should not be in OutputResources + outputFile = Path.ChangeExtension(t.Sources[2].ItemSpec, ".resources"); + Assert.IsTrue(!File.Exists(outputFile)); + // Sources[3] should have been converted + outputFile = Path.ChangeExtension(t.Sources[3].ItemSpec, ".resources"); + Assert.AreEqual(outputFile, t.OutputResources[2].ItemSpec); + Assert.IsTrue(File.Exists(t.OutputResources[2].ItemSpec)); + + // FilesWritten should contain only the 3 successfully output .resources and the cache + Assert.IsTrue(t.FilesWritten[0].ItemSpec == Path.ChangeExtension(t.Sources[0].ItemSpec, ".resources")); + Assert.IsTrue(t.FilesWritten[1].ItemSpec == Path.ChangeExtension(t.Sources[1].ItemSpec, ".resources")); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == Path.ChangeExtension(t.Sources[3].ItemSpec, ".resources")); + Utilities.AssertStateFileWasWritten(t); + + // Make sure there was an error on the second resource + // "unsupported square bracket keyword" + Utilities.AssertLogContains(t, "MSB3563"); + Utilities.AssertLogContains(t, "[goober]"); + } + finally + { + // Done, so clean up. + foreach (ITaskItem item in t.Sources) + { + File.Delete(item.ItemSpec); + } + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR class name derived from output file transformation + /// + [TestMethod] + public void StronglyTypedClassName() + { + GenerateResource t = Utilities.CreateTask(); + + try + { + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StronglyTypedFileName = "somefile.cs"; + t.PublicClass = true; + t.OutputResources = new ITaskItem[] { new TaskItem("somefile.resources") }; + + Utilities.ExecuteTask(t); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, t.OutputResources[0].ItemSpec); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", t.StronglyTypedFileName); + Assert.AreEqual(t.StronglyTypedClassName, Path.GetFileNameWithoutExtension(t.StronglyTypedFileName)); + // Verify class was public, as we specified + Assert.IsTrue(File.ReadAllText(t.StronglyTypedFileName).Contains("public class " + t.StronglyTypedClassName)); + + Utilities.AssertStateFileWasWritten(t); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// STR class file name derived from class name transformation + /// + [TestMethod] + public void StronglyTypedFileName() + { + GenerateResource t = Utilities.CreateTask(); + + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "CSharp"; + File.Delete(Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs")); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + Utilities.ExecuteTask(t); + + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, t.OutputResources[0].ItemSpec); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", t.StronglyTypedFileName); + + Assert.AreEqual(t.StronglyTypedFileName, Path.ChangeExtension(t.Sources[0].ItemSpec, ".cs")); + Assert.IsTrue(File.Exists(t.StronglyTypedFileName)); + + Utilities.AssertStateFileWasWritten(t); + + // Verify class was internal, since we didn't specify a preference + Assert.IsTrue(File.ReadAllText(t.StronglyTypedFileName).Contains("internal class " + t.StronglyTypedClassName)); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + } + + [TestClass] + sealed public class PropertyErrors + { + /// + /// Empty Sources yields message, success + /// + [TestMethod] + public void EmptySources() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing EmptySources() test"); + + GenerateResource t = Utilities.CreateTask(); + Utilities.ExecuteTask(t); + Utilities.AssertLogContainsResource(t, "GenerateResource.NoSources", ""); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// References with invalid assemblies yields warning + /// + [TestMethod] + public void ReferencesToBadAssemblies() + { + GenerateResource t = Utilities.CreateTask(); + string textFile = null; + + try + { + textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.References = new ITaskItem[] { new TaskItem("some non-existent DLL name goes here.dll") }; + bool success = t.Execute(); + // Task should have succeeded, because the bad reference was never consumed. + Assert.IsTrue(success); + } + finally + { + // Done, so clean up. + if (textFile != null) + { + File.Delete(textFile); + File.Delete(Path.ChangeExtension(textFile, ".resources")); + } + } + } + + /// + /// Source item not found + /// + [TestMethod] + public void SourceItemMissing() + { + string txtFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + txtFile = Utilities.WriteTestText(null, null); + resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem("non-existent.resx"), new TaskItem(txtFile) }; + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // "Resource file cannot be found" + Utilities.AssertLogContains(t, "MSB3552"); + + // Should have processed remaining file + Assert.IsTrue(t.OutputResources.Length == 1); + Assert.IsTrue(t.OutputResources[0].ItemSpec == resourcesFile); + Assert.IsTrue(File.Exists(resourcesFile)); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile != null) File.Delete(resourcesFile); + } + } + + /// + /// Read-only StateFile yields message + /// + [TestMethod] + public void StateFileUnwritable() + { + GenerateResource t = Utilities.CreateTask(); + try + { + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StateFile = new TaskItem(FileUtilities.GetTemporaryFile()); + File.SetAttributes(t.StateFile.ItemSpec, FileAttributes.ReadOnly); + t.Execute(); + + // "cannot read state file (opening for read/write)" + Utilities.AssertLogContains(t, "MSB3088"); + // "cannot write state file (opening for read/write)" + Utilities.AssertLogContains(t, "MSB3101"); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.SetAttributes(t.StateFile.ItemSpec, FileAttributes.Normal); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + } + + /// + /// Bad file extension on input + /// + [TestMethod] + public void InputFileExtension() + { + GenerateResource t = Utilities.CreateTask(); + + string textFile = Utilities.WriteTestText(null, null); + string newTextFile = Path.ChangeExtension(textFile, ".foo"); + File.Move(textFile, newTextFile); + t.Sources = new ITaskItem[] { new TaskItem(newTextFile) }; + + t.Execute(); + + // "unsupported file extension" + Utilities.AssertLogContains(t, "MSB3558"); + Utilities.AssertLogNotContainsResource(t, "GenerateResource.ReadResourceMessage", 4, newTextFile); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// Bad file extension on output + /// + [TestMethod] + public void OutputFileExtension() + { + GenerateResource t = Utilities.CreateTask(); + + string textFile = Utilities.WriteTestText(null, null); + string resxFile = Path.ChangeExtension(textFile, ".foo"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resxFile) }; + + t.Execute(); + + // "unsupported file extension" + Utilities.AssertLogContains(t, "MSB3558"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// Sources and OutputResources different # of elements + /// + [TestMethod] + public void SourcesMatchesOutputResources() + { + GenerateResource t = Utilities.CreateTask(); + + string textFile = Utilities.WriteTestText(null, null); + string resxFile = Path.ChangeExtension(textFile, ".resources"); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem("someother.resources") }; + + t.Execute(); + + // "two vectors must have the same length" + Utilities.AssertLogContains(t, "MSB3094"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// Invalid StronglyTypedLanguage yields CodeDOM exception + /// + [TestMethod] + public void UnknownStronglyTypedLanguage() + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + string textFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + t.StronglyTypedLanguage = "AkbarAndJeff"; + + t.Execute(); + + // "the codedom provider failed" + Utilities.AssertLogContains(t, "MSB3559"); + + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + if (t.FilesWritten != null) + { + foreach (ITaskItem item in t.FilesWritten) + { + if (item.ItemSpec != null) + File.Delete(item.ItemSpec); + } + } + } + + /// + /// StronglyTypedLanguage, but more than one resources file + /// + [TestMethod] + public void StronglyTypedResourceWithMoreThanOneInputResourceFile() + { + string resxFile = null; + string resxFile2 = null; + + try + { + resxFile = Utilities.WriteTestResX(false, null, null); + resxFile2 = Utilities.WriteTestResX(false, null, null); + + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile2) }; + t.StronglyTypedLanguage = "VisualBasic"; + + Assert.IsTrue(!t.Execute()); + + // "str language but more than one source file" + Utilities.AssertLogContains(t, "MSB3573"); + + Assert.IsTrue(t.FilesWritten.Length == 0); + Assert.IsTrue(t.OutputResources == null || t.OutputResources.Length == 0); + } + finally + { + if (null != resxFile) File.Delete(resxFile); + if (null != resxFile2) File.Delete(resxFile2); + if (null != resxFile) File.Delete(Path.ChangeExtension(resxFile, ".resources")); + if (null != resxFile2) File.Delete(Path.ChangeExtension(resxFile2, ".resources")); + } + } + + /// + /// STR class name derived from output file transformation + /// + [TestMethod] + public void BadStronglyTypedFilename() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + + txtFile = Utilities.WriteTestText(null, null); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedLanguage = "CSharp"; + t.StronglyTypedClassName = "cc"; + t.StronglyTypedFileName = "||"; + t.OutputResources = new ITaskItem[] { new TaskItem("somefile.resources") }; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + // Cannot create strongly typed resource file + Utilities.AssertLogContains(t, "MSB3570"); + + // it didn't write the STR class successfully, but it did still do some processing, so the + // state file is still around. + Assert.IsTrue(t.FilesWritten.Length == 1); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR class without a language, errors + /// + [TestMethod] + public void StronglyTypedResourceClassWithoutLanguage() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + txtFile = Utilities.WriteTestText(null, null); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedClassName = "myclassname"; + // no language + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + + // Even the .resources wasn't created + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR namespace without a language, errors + /// + [TestMethod] + public void StronglyTypedResourceNamespaceWithoutLanguage() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + txtFile = Utilities.WriteTestText(null, null); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedNamespace = "mynamespace"; + // no language + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + + // Even the .resources wasn't created + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR filename without a language, errors + /// + [TestMethod] + public void StronglyTypedResourceFilenameWithoutLanguage() + { + string txtFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + txtFile = Utilities.WriteTestText(null, null); + string resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedFileName = "myfile"; + // no language + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContainsResource(t, "GenerateResource.STRClassNamespaceOrFilenameWithoutLanguage"); + + // Even the .resources wasn't created + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (null != txtFile) File.Delete(txtFile); + } + } + + /// + /// Verify that passing a STR language with more than 1 sources errors + /// + [TestMethod] + public void StronglyTypedResourceFileIsExistingDirectory() + { + string dir = null; + string txtFile = null; + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + txtFile = Utilities.WriteTestText(null, null); + resourcesFile = Path.ChangeExtension(txtFile, ".resources"); + File.Delete(resourcesFile); + string csFile = Path.ChangeExtension(txtFile, ".cs"); + File.Delete(csFile); + t.Sources = new ITaskItem[] { new TaskItem(txtFile) }; + t.StronglyTypedLanguage = "C#"; + dir = Path.Combine(Path.GetTempPath(), "directory"); + Directory.CreateDirectory(dir); + t.StronglyTypedFileName = dir; + + bool success = t.Execute(); + // Task should have failed + Assert.IsTrue(!success); + + Utilities.AssertLogContains(t, "MSB3570"); + Utilities.AssertLogContains(t, t.StronglyTypedClassName); + + // Since STR creation fails, doesn't create the .resources file either + Assert.IsTrue(!File.Exists(resourcesFile)); + Assert.IsTrue(!File.Exists(csFile)); + Assert.IsTrue(t.FilesWritten.Length == 0); + } + finally + { + if (txtFile != null) File.Delete(txtFile); + if (resourcesFile != null) File.Delete(resourcesFile); + if (dir != null) Directory.Delete(dir); + } + } + + [TestMethod] + public void Regress25163_OutputResourcesContainsInvalidPathCharacters() + { + string resourcesFile = null; + + try + { + GenerateResource t = Utilities.CreateTask(); + resourcesFile = Utilities.WriteTestResX(false, null, null); + + t.Sources = new ITaskItem[] { new TaskItem(resourcesFile) }; + t.OutputResources = new ITaskItem[] { new TaskItem("||") }; + + bool success = t.Execute(); + + Assert.IsFalse(success, "Task should have failed."); + + Utilities.AssertLogContains(t, "MSB3553"); + } + finally + { + if (resourcesFile != null) File.Delete(resourcesFile); + } + } + } + + [TestClass] + public class References + { + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void DontLockP2PReferenceWhenResolvingSystemTypes() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing DontLockP2PReferenceWhenResolvingSystemTypes() test"); + + // ------------------------------------------------------------------------------- + // Need to produce a .DLL assembly on disk, so we can pass it in as a reference to + // GenerateResource. + // ------------------------------------------------------------------------------- + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("lib1.csproj", @" + + + + Local + Debug + AnyCPU + lib1 + Library + lib1 + + + bin\Debug\ + true + false + + + bin\Release\ + false + true + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + public class Class1 + { + } + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("lib1.csproj"); + + string p2pReference = Path.Combine(ObjectModelHelpers.TempProjectDir, @"bin\debug\lib1.dll"); + Assert.IsTrue(File.Exists(p2pReference), "lib1.dll doesn't exist."); + + // ------------------------------------------------------------------------------- + // Done producing an assembly on disk. + // ------------------------------------------------------------------------------- + + // Create a .RESX that references unqualified (without an assembly name) System types. + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"MyStrings.resx", @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.0.0.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Graph Legend + Used in reports to label the graph legend that pops up + + + {0}'s Responses + Used in challenge checklist tables + + + Strength Area + Used in challenge checklist tables + + + Neutral Area + Used in challenge checklist tables + + + Challenge Area + Used in challenge checklist tables + + + Click here for scale calculation + Used in Profile Scale area of main report to point to resource section scale tables. + + + Page + In footer of PDF report, and used in PDF links + + + Table of Contents + On second page of PDF report + + + and + On title page of PDF, joining two participants in a list + + + + "); + + // Run the GenerateResource task on the above .RESX file, passing in an unused reference + // to lib1.dll. + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(Path.Combine(ObjectModelHelpers.TempProjectDir, "MyStrings.resx")) }; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + t.References = new ITaskItem[] + { + new TaskItem(p2pReference), + + // Path to System.dll + new TaskItem(new Uri((typeof(string)).Assembly.EscapedCodeBase).LocalPath) + }; + + bool success = t.Execute(); + + // Make sure the resource was built. + Assert.IsTrue(success, "GenerateResource failed"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory("MyStrings.resources"); + + // Make sure the P2P reference is not locked after calling GenerateResource. + File.Delete(p2pReference); + } + + /// + /// A reference is being passed into the GenerateResource task, but it's specified + /// using a relative path. GenerateResource was failing on this, because in the + /// ResolveAssembly handler, it was calling Assembly.LoadFile on that relative path, + /// which fails (LoadFile requires an absolute path). The fix was to use + /// Assembly.LoadFrom instead. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires dependent components (e.g. csc2.exe). + public void ReferencedAssemblySpecifiedUsingRelativePath() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing ReferencedAssemblySpecifiedUsingRelativePath() test"); + + // ------------------------------------------------------------------------------- + // Need to produce a .DLL assembly on disk, so we can pass it in as a reference to + // GenerateResource. + // ------------------------------------------------------------------------------- + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("ClassLibrary20.csproj", @" + + + + Local + Debug + AnyCPU + ClassLibrary20 + Library + lib1 + + + bin\Debug\ + true + false + + + bin\Release\ + false + true + + + + + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("Class1.cs", @" + using System; + using System.Collections.Generic; + using System.Text; + + namespace ClassLibrary20 + { + [Serializable] + public class Class1 + { + public string foo; + } + } + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess("ClassLibrary20.csproj"); + + // ------------------------------------------------------------------------------- + // Done producing an assembly on disk. + // ------------------------------------------------------------------------------- + + // Create a .RESX that references a type from ClassLibrary20.dll + ObjectModelHelpers.CreateFileInTempProjectDirectory(@"MyStrings.resx", @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.0.0.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + blah + + + + "); + + // Run the GenerateResource task on the above .RESX file, passing in an unused reference + // to lib1.dll. + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(Path.Combine(ObjectModelHelpers.TempProjectDir, "MyStrings.resx")) }; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + + TaskItem reference = new TaskItem(@"bin\debug\ClassLibrary20.dll"); + reference.SetMetadata("FusionName", "ClassLibrary20, version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + + t.References = new ITaskItem[] { reference }; + + // Set the current working directory to the location of ClassLibrary20.csproj. + // This is what allows us to pass in a relative path to the referenced assembly. + string originalCurrentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = ObjectModelHelpers.TempProjectDir; + + bool success = t.Execute(); + + // Restore the current working directory to what it was before the test. + Environment.CurrentDirectory = originalCurrentDirectory; + + // Make sure the resource was built. + Assert.IsTrue(success, "GenerateResource failed"); + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory("MyStrings.resources"); + } + } + + [TestClass] + public class MiscTests + { + [TestMethod] + public void ResgenCommandLineLogging() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing ResgenCommandLineLogging() test"); + + // we use this to check if paths need quoting + CommandLineBuilderHelper commandLineBuilderHelper = new CommandLineBuilderHelper(); + + string resxFile = Utilities.WriteTestResX(false, null, null); + string resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Delete(resourcesFile); + + try + { + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.UseSourcePath = false; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + string possiblyQuotedResxFile = resxFile; + string possiblyQuotedResourcesFile = resourcesFile; + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile)) + { + possiblyQuotedResxFile = "\"" + resxFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile)) + { + possiblyQuotedResourcesFile = "\"" + resourcesFile + "\""; + } + + Utilities.AssertLogContains(t, + " /compile " + possiblyQuotedResxFile + "," + possiblyQuotedResourcesFile); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + } + + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Delete(resourcesFile); + + try + { + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem("baz"), new TaskItem("jazz") }; + t.UseSourcePath = true; + t.PublicClass = true; + t.StronglyTypedLanguage = "C#"; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + string possiblyQuotedResxFile = resxFile; + string possiblyQuotedResourcesFile = resourcesFile; + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile)) + { + possiblyQuotedResxFile = "\"" + resxFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile)) + { + possiblyQuotedResourcesFile = "\"" + resourcesFile + "\""; + } + + Utilities.AssertLogContains(t, + " /useSourcePath /publicClass /r:baz /r:jazz " + + possiblyQuotedResxFile + + " " + + possiblyQuotedResourcesFile + + " /str:\"C#\",,,"); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + File.Delete(Path.ChangeExtension(resxFile, ".cs")); + } + + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Path.ChangeExtension(resxFile, ".resources"); + File.Delete(resourcesFile); + + try + { + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile) }; + t.References = new ITaskItem[] { new TaskItem("baz"), new TaskItem("jazz") }; + t.UseSourcePath = true; + t.StronglyTypedLanguage = "C#"; + t.StronglyTypedClassName = "wagwag"; + t.StronglyTypedFileName = "boo"; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + string possiblyQuotedResxFile = resxFile; + string possiblyQuotedResourcesFile = resourcesFile; + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile)) + { + possiblyQuotedResxFile = "\"" + resxFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile)) + { + possiblyQuotedResourcesFile = "\"" + resourcesFile + "\""; + } + + Utilities.AssertLogContains(t, + " /useSourcePath /r:baz /r:jazz " + + possiblyQuotedResxFile + + " " + + possiblyQuotedResourcesFile + + " /str:\"C#\",,wagwag,boo"); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + } + + resxFile = Utilities.WriteTestResX(false, null, null); + resourcesFile = Path.ChangeExtension(resxFile, ".myresources"); + File.Delete(resourcesFile); + string resxFile1 = Utilities.WriteTestResX(false, null, null); + string resourcesFile1 = Path.ChangeExtension(resxFile1, ".myresources"); + File.Delete(resourcesFile1); + + try + { + GenerateResource t = Utilities.CreateTask(); + t.Sources = new ITaskItem[] { new TaskItem(resxFile), new TaskItem(resxFile1) }; + t.OutputResources = new ITaskItem[] + { + new TaskItem(resourcesFile), + new TaskItem(resourcesFile1) + }; + t.NeverLockTypeAssemblies = false; + t.Execute(); + + string possiblyQuotedResxFile = resxFile; + string possiblyQuotedResourcesFile = resourcesFile; + string possiblyQuotedResxFile1 = resxFile1; + string possiblyQuotedResourcesFile1 = resourcesFile1; + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile)) + { + possiblyQuotedResxFile = "\"" + resxFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile)) + { + possiblyQuotedResourcesFile = "\"" + resourcesFile + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resxFile1)) + { + possiblyQuotedResxFile1 = "\"" + resxFile1 + "\""; + } + + if (commandLineBuilderHelper.DoesPathNeedQuotes(resourcesFile1)) + { + possiblyQuotedResourcesFile1 = "\"" + resourcesFile1 + "\""; + } + + Utilities.AssertLogContains(t, + " /compile " + + possiblyQuotedResxFile + + "," + + possiblyQuotedResourcesFile + + " " + + possiblyQuotedResxFile1 + + "," + + possiblyQuotedResourcesFile1); + } + finally + { + File.Delete(resxFile); + File.Delete(resourcesFile); + File.Delete(resxFile1); + File.Delete(resourcesFile1); + } + } + + /// + /// In order to make GenerateResource multitargetable, a property, ExecuteAsTool, was added. + /// In order to have correct behavior when using pre-4.0 + /// toolsversions, ExecuteAsTool must default to true, and the paths to the tools will be the + /// v3.5 path. It is difficult to verify the tool paths in a unit test, however, so + /// this was done by ad hoc testing and will be maintained by the dev suites. + /// + [TestMethod] + public void MultiTargetingDefaultsSetCorrectly() + { + GenerateResource t = new GenerateResource(); + + Assert.IsTrue(t.ExecuteAsTool, "ExecuteAsTool should default to true"); + } + } +} + +namespace Microsoft.Build.UnitTests.GenerateResource_Tests +{ + /// + /// This Utilities class provides some static helper methods for resource tests + /// + internal sealed partial class Utilities + { + /// + /// Set the last write time to be n minutes back in time. + /// + public static DateTime MoveBackTimestamp(string fileName, int minutes) + { + DateTime newTime = File.GetLastWriteTime(fileName) - new TimeSpan(0, minutes, 0); + File.SetLastWriteTime(fileName, newTime); + return newTime; + } + + /// + /// Return whether the file was written to since the specified time. + /// + public static bool FileUpdated(string fileName, DateTime previousWriteTime) + { + return (File.GetLastWriteTime(fileName) > previousWriteTime); + } + + /// + /// Looks for a message in the output log for the task execution, including formatted parameters. + /// + public static void AssertLogContainsResource(GenerateResource t, string messageID, params object[] replacements) + { + Assert.IsTrue( + ((MockEngine)t.BuildEngine).Log.Contains(String.Format(AssemblyResources.GetString(messageID), replacements)) + ); + } + + /// + /// Looks for a message in the output log for the task execution., including formatted parameters. + /// + public static void AssertLogContains(GenerateResource t, string message) + { + Assert.IsTrue(((MockEngine)t.BuildEngine).Log.Contains(message)); + } + + /// + /// Looks for a message in the output log for the task execution, including formatted parameters. + /// + public static void AssertLogNotContainsResource(GenerateResource t, string messageID, params object[] replacements) + { + Assert.IsTrue( + !((MockEngine)t.BuildEngine).Log.Contains(String.Format(AssemblyResources.GetString(messageID), replacements)) + ); + } + + /// + /// Looks for a message in the output log for the task execution., including formatted parameters. + /// + public static void AssertLogNotContains(GenerateResource t, string message) + { + Assert.IsTrue(!((MockEngine)t.BuildEngine).Log.Contains(message)); + } + + /// + /// Given an array of ITaskItems, checks to make sure that at least one read tlog and at least one + /// write tlog exist, and that they were written to disk. If that is not true, asserts. + /// + public static void AssertStateFileWasWritten(GenerateResource t) + { + Assert.IsNotNull(t.FilesWritten, "The state file should have been written, but there aren't any."); + Assert.IsNotNull(t.StateFile, "State file should be defined"); + Assert.IsTrue(File.Exists(t.StateFile.ItemSpec), "State file should exist"); + + bool foundStateFile = false; + + // start from the end because the statefile is usually marked as a written file fairly late in the process + for (int i = t.FilesWritten.Length - 1; i >= 0; i--) + { + if (t.StateFile.ItemSpec.Equals(t.FilesWritten[i].ItemSpec)) + { + foundStateFile = true; + break; + } + } + + Assert.IsTrue(foundStateFile, "Expected there to be a state file, but there wasn't"); + } + + /// + /// + public static string CreateBasicResourcesFile(bool useResX) + { + GenerateResource t = CreateTask(); + + string sourceFile = null; + if (useResX) + sourceFile = WriteTestResX(false, null, null); + else + sourceFile = WriteTestText(null, null); + + t.Sources = new ITaskItem[] { new TaskItem(sourceFile) }; + + // phase 1, generate the .resources file (we don't care about outcomes) + Utilities.ExecuteTask(t); + + File.Delete(sourceFile); + return t.OutputResources[0].ItemSpec; + } + + /// + /// + public static string ReadFileContent(string fileName) + { + return File.ReadAllText(fileName); + } + + /// + /// ExecuteTask performs the task Execute method and asserts basic success criteria + /// + public static void ExecuteTask(GenerateResource t) + { + bool success = t.Execute(); + Assert.IsTrue(success); + + if (t.OutputResources != null && t.OutputResources[0] != null) + { + DateTime resourcesLastModified = File.GetLastWriteTime(t.OutputResources[0].ItemSpec); + if (t.Sources[0] != null) + Assert.IsTrue(resourcesLastModified >= File.GetLastWriteTime(t.Sources[0].ItemSpec)); + } + } + + /// + /// This method creates a GenerateResource task and performs basic setup on it, e.g. BuildEngine + /// + public static GenerateResource CreateTask() + { + // always use the internal ctor that says don't perform separate app domain check + GenerateResource t = new GenerateResource(); + t.BuildEngine = new MockEngine(); + + // Make the task execute in-proc + t.ExecuteAsTool = false; + + return t; + } + + /// + /// This method creates and returns a string that is the contents of a canonical .txt resource file. + /// Gives the opportunity to create a warning/error in the text by specifying a [tag] value, null for nothiing. + /// Gives the opportunity to add one name-value pair to the text. Null for nothing. + /// + /// The content of the text blob as a string + public static string GetTestTextContent(string tagName, string oneLine) + { + return GetTestTextContent(tagName, oneLine, false); + } + + /// + /// Allows test to get the cleaned up resources, as they would be expected after being transformed + /// back and forth. + /// + /// + /// + /// + /// + public static string GetTestTextContent(string tagName, string oneLine, bool cleanedUp) + { + // Make sure these are in alpha order by name, as the round trip will sort them + string textFileContents; + + if (!cleanedUp) + { + textFileContents = + "\nMalade=There is trouble in the hen\\n house\xd\xa" + + "# this is a comment\xd\xa" + + "Marley=The man, the myth, \\rthe legend\xd\xa" + + "Name2 = Put the li\u1111me in the \\tcoconut and drink 'em both up\xd\xa" + + "Name1=Some S\\\\tring Comes \\\"Here\xd\xa"; + } + else + { + // Content as it would be expected after being transformed and transformed back + textFileContents = + "Malade=There is trouble in the hen\\n house\xd\xa" + + "Marley=The man, the myth, \\rthe legend\xd\xa" + + "Name2=Put the li\u1111me in the \\tcoconut and drink 'em both up\xd\xa" + + "Name1=Some S\\\\tring Comes \"Here\xd\xa"; + } + + StringBuilder txt = new StringBuilder(); + + if (tagName != null) + { + txt.Append("["); + txt.Append(tagName); + txt.Append("]\xd\xa"); + } + + txt.Append(textFileContents); + + if (oneLine != null) + { + txt.Append(oneLine); + txt.Append("\xd\xa"); + } + + return txt.ToString(); + } + + /// + /// This method creates a temporary file based on the canonical .txt resource file. + /// Gives the opportunity to create a warning/error in the text by specifying a [tag] value, null for nothiing. + /// Gives the opportunity to add one name-value pair to the text. Null for nothing. + /// + public static string WriteTestText(string tagName, string oneLine) + { + string textFile = Utilities.GetTempFileName(".txt"); + File.Delete(textFile); + File.WriteAllText(textFile, GetTestTextContent(tagName, oneLine)); + return textFile; + } + + /// + /// Write a test .resx file to a temporary location. + /// + /// Indicates whether to include an enum to test type-specific resource encoding with assembly references + /// The name of a linked-in bitmap. use 'null' for no bitmap. + /// The content of the resx blob as a string + /// The name of the text file + public static string GetTestResXContent(bool useType, string linkedBitmap, string extraToken, bool useInvalidType) + { + StringBuilder resgenFileContents = new StringBuilder(); + + resgenFileContents.Append( + "\xd\xa" + + " \xd\xa" + + " text/microsoft-resx\xd\xa" + + " \xd\xa" + + " \xd\xa" + + " 2.0\xd\xa" + + " \xd\xa" + + " \xd\xa" + + " System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\xd\xa" + + " \xd\xa" + + " \xd\xa" + + " System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\xd\xa" + + " \xd\xa" + ); + + resgenFileContents.Append( + // A plain old string value. + " \xd\xa" + + " MyValue\xd\xa" + + " \xd\xa" + ); + + if (extraToken != null) + resgenFileContents.Append(extraToken); + + if (useType) + { + // A non-standard type. In this case, an enum. + resgenFileContents.Append( + " \xd\xa" + + " Assembly\xd\xa" + + " \xd\xa" + ); + } + + if (useInvalidType) + { + // A type that won't be resolved.. oops! + resgenFileContents.Append( + " \xd\xa" + + " 1\xd\xa" + + " \xd\xa" + ); + } + + if (linkedBitmap != null) + { + // A linked-in bitmap. + resgenFileContents.Append( + " \xd\xa" + + " " + ); + + // The linked file may have a different case than reported by the filesystem + // simulate this by lower-casing our file before writing it into the resx. + resgenFileContents.Append(linkedBitmap.ToUpper(System.Globalization.CultureInfo.InvariantCulture)); + + resgenFileContents.Append( + ";System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a\xd\xa" + + " \xd\xa" + ); + } + + resgenFileContents.Append("\xd\xa"); + + return resgenFileContents.ToString(); + } + + /// + /// Write a test .resx file to a temporary location. + /// + /// Indicates whether to include an enum to test type-specific resource encoding with assembly references + /// The name of a linked-in bitmap. use 'null' for no bitmap. + /// The name of the resx file + public static string WriteTestResX(bool useType, string linkedBitmap, string extraToken) + { + string resgenFile = Utilities.GetTempFileName(".resx"); + File.Delete(resgenFile); + File.WriteAllText(resgenFile, GetTestResXContent(useType, linkedBitmap, extraToken, false)); + return resgenFile; + } + + /// + /// Write a test .resx file to a temporary location. + /// + /// Indicates whether to include an enum to test type-specific resource encoding with assembly references + /// The name of a linked-in bitmap. use 'null' for no bitmap. + /// The name of the resx file + public static string WriteTestResX(bool useType, string linkedBitmap, string extraToken, bool useInvalidType) + { + string resgenFile = Utilities.GetTempFileName(".resx"); + File.Delete(resgenFile); + File.WriteAllText(resgenFile, GetTestResXContent(useType, linkedBitmap, extraToken, useInvalidType)); + return resgenFile; + } + + /// + /// Copy system.dll (so we can later touch it) to a temporary location. + /// + /// The name of the copied file. + public static string GetPathToCopiedSystemDLL() + { + string tempSystemDLL = Utilities.GetTempFileName(".dll"); + File.Copy(ToolLocationHelper.GetPathToDotNetFrameworkFile("system.dll", TargetDotNetFrameworkVersion.Version45), tempSystemDLL); + return tempSystemDLL; + } + + /// + /// Create a tiny bitmap at a temporary location. + /// + /// The name of the bitmap. + public static string CreateWorldsSmallestBitmap() + { + string smallestBitmapFile = Utilities.GetTempFileName(".bmp"); + + byte[] bmp = new byte[66]; + bmp[0x00] = 0x42; bmp[0x01] = 0x4D; bmp[0x02] = 0x42; + bmp[0x0a] = 0x3E; bmp[0x0e] = 0x28; bmp[0x12] = 0x01; bmp[0x16] = 0x01; + bmp[0x1a] = 0x01; bmp[0x1c] = 0x01; bmp[0x22] = 0x04; + bmp[0x3a] = 0xFF; bmp[0x3b] = 0xFF; bmp[0x3c] = 0xFF; + bmp[0x3e] = 0x80; + + File.Delete(smallestBitmapFile); + File.WriteAllBytes(smallestBitmapFile.ToUpperInvariant(), bmp); + return smallestBitmapFile; + } + + /// + /// + public static MethodInfo GetPrivateMethod(object o, string methodName) + { + return o.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + } + + /// + /// Since GetTempFileName creates an empty file, it's bad mojo to just append a new extension + /// because when you clean up your modified filename, you'll leave behind the original .tmp + /// file. This method gives you a unique filename with your desired extension, but also + /// deletes the original root file. It's not perfect, but... + /// + public static string GetTempFileName(string extension) + { + string f = FileUtilities.GetTemporaryFile(); + string filename = Path.ChangeExtension(f, extension); + File.Delete(f); + // Make sure that the new file doesn't already exist, since the test is probably + // expecting it not to + File.Delete(filename); + return filename; + } + + /// + /// Helper method to test STRNamespace parameter of Generate Resource task + /// + /// + /// + /// + public static void STRNamespaceTestHelper(string strLanguage, string resourcesNamespace, string classNamespace) + { + // these two parameters shouldnot be null + Assert.IsNotNull(strLanguage); + Assert.IsNotNull(resourcesNamespace); + // Generate Task + GenerateResource t = Utilities.CreateTask(); + try + { + t.StateFile = new TaskItem(Utilities.GetTempFileName(".cache")); + // Create an input text file + string textFile = Utilities.WriteTestText(null, null); + // set the Sources parameter + t.Sources = new ITaskItem[] { new TaskItem(textFile) }; + // Set the StronglyTypedLanguage parameter + t.StronglyTypedLanguage = strLanguage; + // Set the StronglyTypedManifestPrefix parameter + t.StronglyTypedManifestPrefix = resourcesNamespace; + + // Set the StronglyTypedNamespace parameter + t.StronglyTypedNamespace = classNamespace; + + string codeFileExtension = null; + if (strLanguage == "CSharp") + codeFileExtension = ".cs"; + else if (strLanguage == "VB") + codeFileExtension = ".vb"; + + // Execute task + Utilities.ExecuteTask(t); + + // Get the OutputResources + string resourcesFile = t.OutputResources[0].ItemSpec; + + // Verify that the OutputResources has the same name as Sources (=textFile) + Assert.IsTrue(Path.GetFileNameWithoutExtension(textFile).Equals(Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec))); + + // Verify that STR class name should have been generated from the output + string stronglyTypedClassName = Path.GetFileNameWithoutExtension(t.OutputResources[0].ItemSpec); + Assert.IsTrue(t.StronglyTypedClassName == stronglyTypedClassName); + + // Verify that the extension of the resource file is .resources + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + // Verify that the 1st item in FilesWritten property is the .resource file generated + resourcesFile = t.FilesWritten[0].ItemSpec; + Assert.IsTrue(Path.GetExtension(resourcesFile) == ".resources"); + + Utilities.AssertStateFileWasWritten(t); + + // Files written should contain STR class file + Assert.AreEqual(Path.ChangeExtension(t.Sources[0].ItemSpec, codeFileExtension), t.StronglyTypedFileName); + Assert.IsTrue(t.FilesWritten[2].ItemSpec == t.StronglyTypedFileName); + + // Verify that the STR File is generated + Assert.IsTrue(File.Exists(t.StronglyTypedFileName)); + + // Verify that the STR File was generated correctly + string STRFile = Path.ChangeExtension(textFile, codeFileExtension); + // Verify that the ResourceManager in the STR class is instantiated correctly + Assert.IsTrue(Utilities.ReadFileContent(STRFile).Contains("ResourceManager(\"" + resourcesNamespace + "." + t.StronglyTypedClassName)); + // Verify that the class name of the STR class is as expected + Assert.IsTrue(Utilities.ReadFileContent(STRFile).ToLower().Contains("class " + Path.GetFileNameWithoutExtension(textFile).ToLower())); + // Verify that the namespace of the STR class is as expected + + Assert.IsTrue(!Utilities.ReadFileContent(STRFile).ToLower().Contains("namespace " + resourcesNamespace.ToLower())); + if (classNamespace != null) + Assert.IsTrue(Utilities.ReadFileContent(STRFile).ToLower().Contains("namespace " + classNamespace.ToLower())); + + + // Verify log is as expected + Utilities.AssertLogContainsResource(t, "GenerateResource.ProcessingFile", textFile, resourcesFile); + Utilities.AssertLogContainsResource(t, "GenerateResource.ReadResourceMessage", 4, textFile); + + string typeName = null; + if (t.StronglyTypedNamespace != null) + typeName = t.StronglyTypedNamespace + "."; + else + typeName = ""; + + typeName += t.StronglyTypedClassName; + // Verify that the type is generated correctly + Utilities.AssertLogContainsResource(t, "GenerateResource.CreatingSTR", t.StronglyTypedFileName); + } + finally + { + // Done, so clean up. + File.Delete(t.Sources[0].ItemSpec); + File.Delete(t.StronglyTypedFileName); + foreach (ITaskItem item in t.FilesWritten) + { + if (File.Exists(item.ItemSpec)) + { + File.Delete(item.ItemSpec); + } + } + } + } + } + + /// + /// Extends the CommandLineBuilderClass to get at its protected methods. + /// + internal sealed class CommandLineBuilderHelper : CommandLineBuilder + { + /// + /// Redirects to the protected method IsQuotingRequired(). + /// + /// true, if given path needs to be quoted. + internal bool DoesPathNeedQuotes(string path) + { + return base.IsQuotingRequired(path); + } + } +} diff --git a/src/XMakeTasks/UnitTests/GetInstalledSDKLocations_Tests.cs b/src/XMakeTasks/UnitTests/GetInstalledSDKLocations_Tests.cs new file mode 100644 index 00000000000..7a8a718a13c --- /dev/null +++ b/src/XMakeTasks/UnitTests/GetInstalledSDKLocations_Tests.cs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Test that we can retrieve the list of SDKs and output them to the project file. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Reflection; +using System.Globalization; +using System.Resources; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.UnitTests.ResolveAssemblyReference_Tests; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using Microsoft.Build.Evaluation; +using System.Linq; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.UnitTests.GetInstalledSDKLocations_Tests +{ + /// + /// Test the GetInstalledSDKLocations task + /// + [TestClass] + public class GetInstalledSDKLocationsTestFixture + { + private static string s_fakeSDKStructureRoot = null; + private static string s_fakeSDKStructureRoot2 = null; + + [ClassInitialize] + public static void ClassSetup(TestContext context) + { + s_fakeSDKStructureRoot = MakeFakeSDKStructure(); + s_fakeSDKStructureRoot2 = MakeFakeSDKStructure2(); + } + + [ClassCleanup] + public static void ClassCleanup() + { + if (FileUtilities.DirectoryExistsNoThrow(s_fakeSDKStructureRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(s_fakeSDKStructureRoot, true); + } + + if (FileUtilities.DirectoryExistsNoThrow(s_fakeSDKStructureRoot2)) + { + FileUtilities.DeleteDirectoryNoThrow(s_fakeSDKStructureRoot2, true); + } + } + + #region TestMethods + /// + /// Make sure we get a ArgumentException if null is passed into the target platform version. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullTargetPlatformVersion() + { + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Hello"; + t.TargetPlatformVersion = null; + t.Execute(); + } + + /// + /// Make sure we get a ArgumentException if null is passed into the target platform version. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullTargetPlatformIdentifier() + { + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = null; + t.TargetPlatformVersion = "1.0"; + t.Execute(); + } + + /// + /// Make sure we get an error message if an empty platform identifier is passed in. + /// + [TestMethod] + public void EmptyTargetPlatformIdentifier() + { + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = String.Empty; + t.TargetPlatformVersion = "1.0"; + t.BuildEngine = engine; + bool success = t.Execute(); + Assert.IsFalse(success); + + Assert.IsTrue(engine.Errors == 1); + engine.AssertLogContains("MSB3784"); + } + + /// + /// Make sure we get an error message if an empty platform Version is passed in. + /// + [TestMethod] + public void EmptyTargetPlatformVersion() + { + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Hello"; + t.TargetPlatformVersion = String.Empty; + t.BuildEngine = engine; + bool success = t.Execute(); + Assert.IsFalse(success); + + + Assert.IsTrue(engine.Errors == 1); + engine.AssertLogContains("MSB3784"); + } + + /// + /// Make sure we get an error message if an empty platform Version is passed in. + /// + [TestMethod] + public void BadTargetPlatformVersion() + { + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Hello"; + t.TargetPlatformVersion = "CAT"; + t.BuildEngine = engine; + bool success = t.Execute(); + Assert.IsFalse(success); + + + Assert.IsTrue(engine.Errors == 1); + engine.AssertLogContains("MSB3786"); + } + + /// + /// Make sure we get an Warning if no SDKs were found. + /// + [TestMethod] + public void NoSDKsFound() + { + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Hello"; + t.TargetPlatformVersion = "1.0"; + t.BuildEngine = engine; + bool success = t.Execute(); + Assert.IsTrue(success); + + Assert.IsTrue(engine.Warnings == 1); + engine.AssertLogContains("MSB3785"); + } + + /// + /// Get a good set of SDKS installed on the machine from the fake SDK location. + /// + [TestMethod] + public void GetSDKVersions() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", s_fakeSDKStructureRoot + ";" + s_fakeSDKStructureRoot2); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Windows"; + t.TargetPlatformVersion = new Version(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue).ToString(); + t.SDKRegistryRoot = "Somewhere"; + t.SDKRegistryRoot = "Hello;Jello"; + t.BuildEngine = engine; + bool success = t.Execute(); + Assert.IsTrue(success); + + ITaskItem[] installedSDKs = t.InstalledSDKs; + Assert.IsTrue(installedSDKs.Length == 6); + + Dictionary sdksAndVersions = new Dictionary(); + + foreach (ITaskItem item in installedSDKs) + { + sdksAndVersions.Add(item.GetMetadata("SDKName"), item.GetMetadata("PlatformVersion")); + } + + Assert.IsTrue(sdksAndVersions["MyAssembly, Version=1.0"] == "1.0"); + Assert.IsTrue(sdksAndVersions["MyAssembly, Version=2.0"] == "1.0"); + Assert.IsTrue(sdksAndVersions["MyAssembly, Version=3.0"] == "2.0"); + Assert.IsTrue(sdksAndVersions["MyAssembly, Version=4.0"] == "1.0"); + Assert.IsTrue(sdksAndVersions["MyAssembly, Version=5.0"] == "1.0"); + Assert.IsTrue(sdksAndVersions["MyAssembly, Version=6.0"] == "2.0"); + + Assert.IsFalse(sdksAndVersions.ContainsValue("3.0")); + Assert.IsFalse(sdksAndVersions.ContainsValue("4.0")); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Get a good set of SDKS installed on the machine from the fake SDK location. + /// + [TestMethod] + public void GetGoodSDKs() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", s_fakeSDKStructureRoot + ";" + s_fakeSDKStructureRoot2); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Windows"; + t.TargetPlatformVersion = new Version(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue).ToString(); + t.SDKRegistryRoot = "Somewhere"; + t.SDKRegistryRoot = "Hello;Jello"; + t.BuildEngine = engine; + bool success = t.Execute(); + Assert.IsTrue(success); + + ITaskItem[] installedSDKs = t.InstalledSDKs; + Assert.IsTrue(installedSDKs.Length == 6); + + Dictionary extensionSDKs = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (ITaskItem item in installedSDKs) + { + extensionSDKs.Add(item.GetMetadata("SDKName"), item.ItemSpec); + } + + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeSDKStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=2.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=2.0"].Equals(Path.Combine(s_fakeSDKStructureRoot, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=3.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=3.0"].Equals(Path.Combine(s_fakeSDKStructureRoot, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=4.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=4.0"].Equals(Path.Combine(s_fakeSDKStructureRoot2, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\4.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=5.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=5.0"].Equals(Path.Combine(s_fakeSDKStructureRoot2, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\5.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=6.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=6.0"].Equals(Path.Combine(s_fakeSDKStructureRoot2, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\6.0\\"), StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Get a good set of SDKS installed on the machine from the fake SDK location. + /// + [TestMethod] + public void GetGoodSDKs2() + { + try + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + MockEngine engine = new MockEngine(); + GetInstalledSDKLocations t = new GetInstalledSDKLocations(); + t.TargetPlatformIdentifier = "Windows"; + t.TargetPlatformVersion = new Version(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue).ToString(); + t.BuildEngine = engine; + t.SDKRegistryRoot = String.Empty; + t.SDKDirectoryRoots = new string[] { s_fakeSDKStructureRoot, s_fakeSDKStructureRoot2 }; + bool success = t.Execute(); + Assert.IsTrue(success); + + ITaskItem[] installedSDKs = t.InstalledSDKs; + Assert.IsTrue(installedSDKs.Length == 6); + + Dictionary extensionSDKs = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (ITaskItem item in installedSDKs) + { + extensionSDKs.Add(item.GetMetadata("SDKName"), item.ItemSpec); + } + + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=1.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=1.0"].Equals(Path.Combine(s_fakeSDKStructureRoot, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=2.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=2.0"].Equals(Path.Combine(s_fakeSDKStructureRoot, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=3.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=3.0"].Equals(Path.Combine(s_fakeSDKStructureRoot, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0\\"), StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=4.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=4.0"].Equals(Path.Combine(s_fakeSDKStructureRoot2, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\4.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=5.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=5.0"].Equals(Path.Combine(s_fakeSDKStructureRoot2, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\5.0\\"), StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(extensionSDKs.ContainsKey("MyAssembly, Version=6.0")); + Assert.IsTrue(extensionSDKs["MyAssembly, Version=6.0"].Equals(Path.Combine(s_fakeSDKStructureRoot2, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\6.0\\"), StringComparison.OrdinalIgnoreCase)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + } + } + + /// + /// Make a fake SDK structure on disk for testing. + /// + private static string MakeFakeSDKStructure() + { + string tempPath = Path.Combine(Path.GetTempPath(), "FakeSDKDirectory"); + try + { + // Good + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0")); + File.WriteAllText(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\1.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\2.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\3.0", "sdkmanifest.xml"), "Hello"); + + //Bad because of v in the sdk version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\FluterShy\\v1.1")); + + //Bad because no extensionSDKs directory under the platform version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v3.0\\")); + + // Bad because the directory under the identifier is not a version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\NotAVersion\\")); + + // Bad because the directory under the identifier is not a version + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\NotAVersion\\ExtensionSDKs\\Assembly\\1.0")); + + // Good but are in a different target platform + // Doors does not have an sdk manifest but does have extensionsdks under it so they should be found + // when we are targeting doors + Directory.CreateDirectory(Path.Combine(tempPath, "Doors\\2.0\\ExtensionSDKs\\MyAssembly\\3.0")); + File.WriteAllText(Path.Combine(tempPath, "Doors\\2.0\\ExtensionSDKs\\MyAssembly\\3.0\\", "sdkmanifest.xml"), "Hello"); + + // Walls has an SDK manifest so it should be found when looking for targetplatform sdks. + // But it has no extensionSDKs so none should be found + Directory.CreateDirectory(Path.Combine(tempPath, "Walls\\1.0\\")); + File.WriteAllText(Path.Combine(tempPath, "Walls\\1.0\\", "sdkmanifest.xml"), "Hello"); + } + catch (Exception) + { + FileUtilities.DeleteDirectoryNoThrow(tempPath, true); + return null; + } + + return tempPath; + } + + /// + /// Make a fake SDK structure on disk for testing. + /// + private static string MakeFakeSDKStructure2() + { + string tempPath = Path.Combine(Path.GetTempPath(), "FakeSDKDirectory2"); + try + { + // Good + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\4.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\5.0")); + Directory.CreateDirectory(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\6.0")); + File.WriteAllText(Path.Combine(tempPath, "Windows\\v1.0\\ExtensionSDKs\\MyAssembly\\4.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\1.0\\ExtensionSDKs\\MyAssembly\\5.0", "sdkmanifest.xml"), "Hello"); + File.WriteAllText(Path.Combine(tempPath, "Windows\\2.0\\ExtensionSDKs\\MyAssembly\\6.0", "sdkmanifest.xml"), "Hello"); + } + catch (Exception) + { + FileUtilities.DeleteDirectoryNoThrow(tempPath, true); + return null; + } + + return tempPath; + } + #endregion + } +} diff --git a/src/XMakeTasks/UnitTests/GetReferencePaths_Tests.cs b/src/XMakeTasks/UnitTests/GetReferencePaths_Tests.cs new file mode 100644 index 00000000000..f3ebe4dc7bf --- /dev/null +++ b/src/XMakeTasks/UnitTests/GetReferencePaths_Tests.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// +// Tests for the task which gets the reference assembly paths for a given target framework version / moniker. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using System.IO; +using Microsoft.Build.Utilities; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for the task which gets the reference assembly paths for a given target framework version / moniker + /// + [TestClass] + sealed public class GetReferenceAssmeblyPath_Tests + { + /// + /// Test the case where there is a good target framework moniker passed in. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerGood() + { + string targetFrameworkMoniker = ".NetFramework, Version=v4.5"; + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + getReferencePaths.TargetFrameworkMoniker = targetFrameworkMoniker; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + Assert.AreEqual(ToolLocationHelper.GetPathToReferenceAssemblies(new FrameworkNameVersioning(targetFrameworkMoniker)).Count, returnedPaths.Length); + Assert.IsTrue(engine.Errors == 0, "Expected the log to contain no errors"); + } + + /// + /// Test the case where there is a good target framework moniker passed in. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerGoodWithRoot() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "TestGeneralFrameworkMonikerGoodWithRoot"); + string framework41Directory = Path.Combine(tempDirectory, "MyFramework\\v4.1\\"); + string redistListDirectory = Path.Combine(framework41Directory, "RedistList"); + string redistListFile = Path.Combine(redistListDirectory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(framework41Directory); + Directory.CreateDirectory(redistListDirectory); + + string redistListContents = + "" + + "" + + "" + + ""; + + File.WriteAllText(redistListFile, redistListContents); + + string targetFrameworkMoniker = "MyFramework, Version=v4.1"; + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + getReferencePaths.TargetFrameworkMoniker = targetFrameworkMoniker; + getReferencePaths.RootPath = tempDirectory; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + string displayName = getReferencePaths.TargetFrameworkMonikerDisplayName; + Assert.AreEqual(1, returnedPaths.Length); + Assert.IsTrue(returnedPaths[0].Equals(framework41Directory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(engine.Log.Length == 0, "Expected the log to contain nothing"); + Assert.IsTrue(displayName.Equals(".NET Framework 4.1", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(framework41Directory)) + { + Directory.Delete(framework41Directory, true); + } + } + } + + /// + /// Test the case where there is a good target framework moniker passed in. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerGoodWithRootWithProfile() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "TestGeneralFrameworkMonikerGoodWithRootWithProfile"); + string framework41Directory = Path.Combine(tempDirectory, "MyFramework\\v4.1\\Profile\\Client"); + string redistListDirectory = Path.Combine(framework41Directory, "RedistList"); + string redistListFile = Path.Combine(redistListDirectory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(framework41Directory); + Directory.CreateDirectory(redistListDirectory); + + string redistListContents = + "" + + "" + + "" + + ""; + + File.WriteAllText(redistListFile, redistListContents); + FrameworkNameVersioning name = new FrameworkNameVersioning("MyFramework", new Version("4.1"), "Client"); + string targetFrameworkMoniker = name.FullName; + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + getReferencePaths.TargetFrameworkMoniker = targetFrameworkMoniker; + getReferencePaths.RootPath = tempDirectory; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + string displayName = getReferencePaths.TargetFrameworkMonikerDisplayName; + Assert.AreEqual(1, returnedPaths.Length); + Assert.IsTrue(returnedPaths[0].Equals(framework41Directory + "\\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(displayName.Equals(".NET Framework 4.1 Client", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(framework41Directory)) + { + Directory.Delete(framework41Directory, true); + } + } + } + + /// + /// Test the case where the target framework moniker is null. Expect there to be an error logged. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerNull() + { + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + getReferencePaths.TargetFrameworkMoniker = null; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + Assert.IsNull(getReferencePaths.TargetFrameworkMonikerDisplayName); + Assert.AreEqual(0, returnedPaths.Length); + Assert.AreEqual(1, engine.Errors); + } + + /// + /// Test the case where the target framework moniker is empty. Expect there to be an error logged. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerNonExistent() + { + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + // Make a framework which does not exist, intentional mispelling of framework + getReferencePaths.TargetFrameworkMoniker = ".NetFramewok, Version=v99.0"; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + Assert.AreEqual(0, returnedPaths.Length); + string displayName = getReferencePaths.TargetFrameworkMonikerDisplayName; + Assert.IsNull(displayName); + FrameworkNameVersioning frameworkMoniker = new FrameworkNameVersioning(getReferencePaths.TargetFrameworkMoniker); + string message = ResourceUtilities.FormatResourceString("GetReferenceAssemblyPaths.NoReferenceAssemblyDirectoryFound", frameworkMoniker.ToString()); + engine.AssertLogContains(message); + } + + /// + /// Test the case where there is a good target framework moniker passed in. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerGoodWithInvalidIncludePath() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "TestGeneralFrameworkMonikerGoodWithInvalidIncludePath"); + string framework41Directory = Path.Combine(tempDirectory, "MyFramework\\v4.1\\"); + string redistListDirectory = Path.Combine(framework41Directory, "RedistList"); + string redistListFile = Path.Combine(redistListDirectory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(framework41Directory); + Directory.CreateDirectory(redistListDirectory); + + string redistListContents = + "" + + "" + + "" + + ""; + + File.WriteAllText(redistListFile, redistListContents); + + string targetFrameworkMoniker = "MyFramework, Version=v4.1"; + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + getReferencePaths.TargetFrameworkMoniker = targetFrameworkMoniker; + getReferencePaths.RootPath = tempDirectory; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + Assert.AreEqual(0, returnedPaths.Length); + string displayName = getReferencePaths.TargetFrameworkMonikerDisplayName; + Assert.IsNull(displayName); + FrameworkNameVersioning frameworkMoniker = new FrameworkNameVersioning(getReferencePaths.TargetFrameworkMoniker); + string message = ResourceUtilities.FormatResourceString("GetReferenceAssemblyPaths.NoReferenceAssemblyDirectoryFound", frameworkMoniker.ToString()); + engine.AssertLogContains(message); + } + finally + { + if (Directory.Exists(framework41Directory)) + { + Directory.Delete(framework41Directory, true); + } + } + } + + /// + /// Test the case where there is a good target framework moniker passed in but there is a problem with the RedistList. + /// + [TestMethod] + public void TestGeneralFrameworkMonikerGoodWithInvalidCharInIncludePath() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "TestGeneralFrameworkMonikerGoodWithInvalidCharInIncludePath"); + string framework41Directory = Path.Combine(tempDirectory, "MyFramework\\v4.1\\"); + string redistListDirectory = Path.Combine(framework41Directory, "RedistList"); + string redistListFile = Path.Combine(redistListDirectory, "FrameworkList.xml"); + try + { + Directory.CreateDirectory(framework41Directory); + Directory.CreateDirectory(redistListDirectory); + + string redistListContents = + "" + + "" + + "" + + ""; + + File.WriteAllText(redistListFile, redistListContents); + + string targetFrameworkMoniker = "MyFramework, Version=v4.1"; + MockEngine engine = new MockEngine(); + GetReferenceAssemblyPaths getReferencePaths = new GetReferenceAssemblyPaths(); + getReferencePaths.BuildEngine = engine; + getReferencePaths.TargetFrameworkMoniker = targetFrameworkMoniker; + getReferencePaths.RootPath = tempDirectory; + getReferencePaths.Execute(); + string[] returnedPaths = getReferencePaths.ReferenceAssemblyPaths; + Assert.AreEqual(0, returnedPaths.Length); + string displayName = getReferencePaths.TargetFrameworkMonikerDisplayName; + Assert.IsNull(displayName); + FrameworkNameVersioning frameworkMoniker = new FrameworkNameVersioning(getReferencePaths.TargetFrameworkMoniker); + engine.AssertLogContains("MSB3643"); + } + finally + { + if (Directory.Exists(framework41Directory)) + { + Directory.Delete(framework41Directory, true); + } + } + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/GetSDKReference_Tests.cs b/src/XMakeTasks/UnitTests/GetSDKReference_Tests.cs new file mode 100644 index 00000000000..d1f238bb7c5 --- /dev/null +++ b/src/XMakeTasks/UnitTests/GetSDKReference_Tests.cs @@ -0,0 +1,1404 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// Tests for the task that extracts the list of reference assemblies from the SDK +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.GetSDKReferenceFiles_Tests +{ + /// + /// Test the expansion of sdk reference assemblies. + /// + [TestClass] + public class GetSDKReferenceFilesTestFixture + { + private static string s_fakeSDKStructureRoot = null; + private static string s_fakeSDKStructureRoot2 = null; + private static string s_sdkDirectory = null; + private static string s_sdkDirectory2 = null; + private static Microsoft.Build.UnitTests.MockEngine.GetStringDelegate s_resourceDelegate; + private static FileExists s_fileExists = new FileExists(FileUtilities.FileExistsNoThrow); + private static GetAssemblyName s_getAssemblyName = new GetAssemblyName(GetAssemblyName); + private static GetAssemblyRuntimeVersion s_getAssemblyRuntimeVersion = new GetAssemblyRuntimeVersion(GetImageRuntimeVersion); + private static string s_cacheDirectory = Path.Combine(Path.GetTempPath(), "GetSDKReferenceFiles"); + + [ClassInitialize] + public static void ClassSetup(TestContext context) + { + s_fakeSDKStructureRoot = CreateFakeSDKReferenceAssemblyDirectory1(out s_sdkDirectory); + s_fakeSDKStructureRoot2 = CreateFakeSDKReferenceAssemblyDirectory2(out s_sdkDirectory2); + s_resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + } + + [ClassCleanup] + public static void ClassCleanup() + { + if (FileUtilities.DirectoryExistsNoThrow(s_fakeSDKStructureRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(s_fakeSDKStructureRoot, true); + } + } + + [TestInitialize] + public void Setup() + { + if (FileUtilities.DirectoryExistsNoThrow(s_cacheDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(s_cacheDirectory, true); + } + + Directory.CreateDirectory(s_cacheDirectory); + } + + [TestCleanup] + public void Cleanup() + { + if (FileUtilities.DirectoryExistsNoThrow(s_cacheDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(s_cacheDirectory, true); + } + } + + /// + /// Make sure there are no outputs if no resolved sdk files are passed in. + /// + [TestMethod] + public void PassReferenceWithNoReferenceDirectory() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + ITaskItem item = new TaskItem("C:\\SDKDoesNotExist"); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + t.CacheFileFolderPath = s_cacheDirectory; + + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 0); + } + + + private delegate IList GetSDKFolders(string sdkRoot); + private delegate IList GetSDKFolders2(string sdkRoot, string configuration, string architecture); + + /// + /// Make sure we get the correct folder list when asking for it. + /// + [TestMethod] + public void GetSDKReferenceFolders() + { + GetSDKFolders getReferenceFolders = new GetSDKFolders(ToolLocationHelper.GetSDKReferenceFolders); + GetSDKFolders2 getReferenceFolders2 = new GetSDKFolders2(ToolLocationHelper.GetSDKReferenceFolders); + + VerifySDKFolders(getReferenceFolders, getReferenceFolders2, "References"); + } + + private static void VerifySDKFolders(GetSDKFolders singleParamDelegate, GetSDKFolders2 multiParamDelegate, string folderName) + { + IList sdkFolders = singleParamDelegate(s_sdkDirectory); + Assert.AreEqual(2, sdkFolders.Count); + + Assert.IsTrue(sdkFolders[0].Equals(Path.Combine(s_sdkDirectory, folderName + "\\Retail\\Neutral\\"))); + Assert.IsTrue(sdkFolders[1].Equals(Path.Combine(s_sdkDirectory, folderName + "\\CommonConfiguration\\Neutral\\"))); + + sdkFolders = multiParamDelegate(s_sdkDirectory, "Retail", "Neutral"); + Assert.AreEqual(2, sdkFolders.Count); + + Assert.IsTrue(sdkFolders[0].Equals(Path.Combine(s_sdkDirectory, folderName + "\\Retail\\Neutral\\"))); + Assert.IsTrue(sdkFolders[1].Equals(Path.Combine(s_sdkDirectory, folderName + "\\CommonConfiguration\\Neutral\\"))); + + sdkFolders = multiParamDelegate(s_sdkDirectory, "Retail", "X86"); + Assert.AreEqual(4, sdkFolders.Count); + + Assert.IsTrue(sdkFolders[0].Equals(Path.Combine(s_sdkDirectory, folderName + "\\Retail\\X86\\"))); + Assert.IsTrue(sdkFolders[1].Equals(Path.Combine(s_sdkDirectory, folderName + "\\Retail\\Neutral\\"))); + Assert.IsTrue(sdkFolders[2].Equals(Path.Combine(s_sdkDirectory, folderName + "\\CommonConfiguration\\X86\\"))); + Assert.IsTrue(sdkFolders[3].Equals(Path.Combine(s_sdkDirectory, folderName + "\\CommonConfiguration\\Neutral\\"))); + } + + /// + /// Make sure we get the correct folder list when asking for it. + /// + [TestMethod] + public void GetSDKRedistFolders() + { + GetSDKFolders getRedistFolders = new GetSDKFolders(ToolLocationHelper.GetSDKRedistFolders); + GetSDKFolders2 getRedistFolders2 = new GetSDKFolders2(ToolLocationHelper.GetSDKRedistFolders); + + VerifySDKFolders(getRedistFolders, getRedistFolders2, "Redist"); + } + + /// + /// Make sure we get the correct folder list when asking for it. + /// + [TestMethod] + public void GetSDKDesignTimeFolders() + { + GetSDKFolders getDesignTimeFolders = new GetSDKFolders(ToolLocationHelper.GetSDKDesignTimeFolders); + GetSDKFolders2 getDesignTimeFolders2 = new GetSDKFolders2(ToolLocationHelper.GetSDKDesignTimeFolders); + + VerifySDKFolders(getDesignTimeFolders, getDesignTimeFolders2, "DesignTime"); + } + + /// + /// Make sure there are no outputs if an sdk which does not exist is passed in. + /// + [TestMethod] + public void PassNoSDKReferences() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 0); + } + + /// + /// Make sure there are no outputs if expand sdks is not true. + /// + [TestMethod] + public void PassReferenceWithExpandFalse() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "false"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 0); + } + + /// + /// Make sure there are no redist outputs if CopyRedist is false + /// + [TestMethod] + public void PassReferenceWithCopyRedistFalse() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("ExpandReferenceAssemblies", "false"); + item.SetMetadata("CopyRedist", "false"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 0); + } + + /// + /// Verify we get the correct set of reference assemblies and copy local files when the CopyLocal flag is true + /// + [TestMethod] + public void GetReferenceAssembliesWhenExpandTrueCopyLocalTrue() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyLocalExpandedReferenceAssemblies", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 9); + Assert.IsTrue(t.References.Length == 8); + + string winmd = Path.Combine(s_sdkDirectory, "References\\Retail\\X86\\A.winmd"); + + Assert.IsTrue(t.References[0].ItemSpec.Equals(winmd, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(Path.GetFileName(t.References[0].ItemSpec).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ImageRuntime").Equals("WindowsRuntime 1.0;CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("FusionName").Equals("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFileType").Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("CopyLocal").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[4].ItemSpec).Equals("E.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ImageRuntime").Equals("CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("FusionName").Equals("E, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[4].GetMetadata("CopyLocal").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.CopyLocalFiles[0].ItemSpec).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("ImageRuntime").Equals("WindowsRuntime 1.0;CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("FusionName").Equals("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("WinMDFileType").Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("CopyLocal").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.CopyLocalFiles[5].ItemSpec).Equals("E.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("ImageRuntime").Equals("CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("FusionName").Equals("E, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("CopyLocal").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.CopyLocalFiles[5].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.CopyLocalFiles[2].ItemSpec).Equals("B.xml", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify reference is not processed by GetSDKReferenceFiles when "ReferenceOnly" metadata is set. + /// + [TestMethod] + public void VerifyNoCopyWhenReferenceOnlyIsTrue() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item1 = new TaskItem(s_sdkDirectory); + item1.SetMetadata("ExpandReferenceAssemblies", "true"); + item1.SetMetadata("TargetedSDKConfiguration", "Retail"); + item1.SetMetadata("TargetedSDKArchitecture", "x86"); + item1.SetMetadata("CopyLocalExpandedReferenceAssemblies", "false"); + item1.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + ITaskItem item2 = new TaskItem(s_sdkDirectory); + item2.SetMetadata("ExpandReferenceAssemblies", "true"); + item2.SetMetadata("TargetedSDKConfiguration", "Retail"); + item2.SetMetadata("TargetedSDKArchitecture", "x86"); + item2.SetMetadata("CopyLocalExpandedReferenceAssemblies", "false"); + item2.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + item2.SetMetadata("RuntimeReferenceOnly", "true"); + + // Process both regular and runtime-only references + t.ResolvedSDKReferences = new ITaskItem[] { item1, item2 }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + + Assert.IsTrue(t.References.Length == 8); + + // Process regular references + t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + t.ResolvedSDKReferences = new ITaskItem[] { item1 }; + success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + + Assert.IsTrue(t.References.Length == 8); + + // Process runtime-only references + t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + t.ResolvedSDKReferences = new ITaskItem[] { item2 }; + success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + + Assert.IsTrue(t.References.Length == 0); + } + + /// + /// Verify we get the correct set of reference assemblies and copy local files when the CopyLocal flag is false + /// + [TestMethod] + public void GetReferenceAssembliesWhenExpandTrueCopyLocalFalse() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyLocalExpandedReferenceAssemblies", "false"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + + Assert.IsTrue(Path.GetFileName(t.References[0].ItemSpec).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ImageRuntime").Equals("WindowsRuntime 1.0;CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("FusionName").Equals("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFileType").Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[1].ItemSpec).Equals("B.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("ImageRuntime").Equals("WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("FusionName").Equals("B, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("WinMDFileType").Equals("Native", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[1].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[4].ItemSpec).Equals("E.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ImageRuntime").Equals("CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("FusionName").Equals("E, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[4].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify that different cache files are created and used correctly for assemblies with the same identity but with files in different directories + /// Also verifies that when + /// + [TestMethod] + public void VerifyCacheFileNames() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + ITaskItem[] references1 = t.References; + + // Verify the task created a cache file + string sdkIdentity = item.GetMetadata("OriginalItemSpec"); + string sdkRoot = item.ItemSpec; + string cacheFile = sdkIdentity + ",Set=" + FileUtilities.GetHexHash(sdkIdentity) + "-" + FileUtilities.GetHexHash(sdkRoot) + ",Hash=*.dat"; + Thread.Sleep(100); + string[] existingCacheFiles = Directory.GetFiles(s_cacheDirectory, cacheFile); + Assert.IsTrue(existingCacheFiles.Length == 1); + + GetSDKReferenceFiles t2 = new GetSDKReferenceFiles(); + t2.BuildEngine = engine; + t2.CacheFileFolderPath = s_cacheDirectory; + + // Same SDK with different path + ITaskItem item2 = new TaskItem(s_sdkDirectory2); + item2.SetMetadata("ExpandReferenceAssemblies", "true"); + item2.SetMetadata("TargetedSDKConfiguration", "Retail"); + item2.SetMetadata("TargetedSDKArchitecture", "x86"); + item2.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t2.ResolvedSDKReferences = new ITaskItem[] { item2 }; + bool success2 = t2.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + ITaskItem[] references2 = t2.References; + Assert.IsTrue(success2); + + // References from the two builds should not overlap, otherwise the cache files are being misused + foreach (var ref2 in references2) + { + Assert.IsTrue(references1.Count(i => i.ItemSpec.Equals(ref2.ItemSpec, StringComparison.InvariantCultureIgnoreCase)) == 0); + } + + Thread.Sleep(100); + string sdkIdentity2 = item.GetMetadata("OriginalItemSpec"); + string sdkRoot2 = item.ItemSpec; + string cacheFile2 = sdkIdentity2 + ",Set=" + FileUtilities.GetHexHash(sdkIdentity2) + "-" + FileUtilities.GetHexHash(sdkRoot2) + ",Hash=*.dat"; + string[] existingCacheFiles2 = Directory.GetFiles(s_cacheDirectory, cacheFile); + Assert.IsTrue(existingCacheFiles2.Length == 1); + + // There should have two cache files with the same prefix and first hash + Thread.Sleep(100); + string[] allCacheFiles = Directory.GetFiles(s_cacheDirectory, sdkIdentity2 + ",Set=" + FileUtilities.GetHexHash(sdkIdentity2) + "*"); + Assert.IsTrue(allCacheFiles.Length == 2); + } + + /// + /// Verify the correct reference files are found and that by default we do log the reference files + /// added. + /// + [TestMethod] + public void VerifyReferencesLogged() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[0].ItemSpec.Replace(t.References[0].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[1].ItemSpec.Replace(t.References[1].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[2].ItemSpec.Replace(t.References[2].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[3].ItemSpec.Replace(t.References[3].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[4].ItemSpec.Replace(t.References[4].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[5].ItemSpec.Replace(t.References[5].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[6].ItemSpec.Replace(t.References[6].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[7].ItemSpec.Replace(t.References[7].GetMetadata("SDKRootPath"), String.Empty)); + + Assert.IsTrue(Path.GetFileName(t.References[0].ItemSpec).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFileType").Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[4].ItemSpec).Equals("E.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[4].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify the correct reference files are found and that by default we do log the reference files + /// added. + /// + [TestMethod] + public void VerifyReferencesLoggedFilterOutWinmd() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + t.ReferenceExtensions = new string[] { ".dll" }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 5); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[0].ItemSpec.Replace(t.References[0].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[1].ItemSpec.Replace(t.References[1].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[2].ItemSpec.Replace(t.References[2].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[3].ItemSpec.Replace(t.References[3].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[4].ItemSpec.Replace(t.References[4].GetMetadata("SDKRootPath"), String.Empty)); + + Assert.IsTrue(Path.GetFileName(t.References[0].ItemSpec).Equals("A.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[0].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[4].ItemSpec).Equals("h.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[4].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify we log an error if no configuration is on the sdk reference + /// + [TestMethod] + public void LogErrorWhenNoConfiguration() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", ""); + item.SetMetadata("TargetedSDKArchitecture", "amd64"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsFalse(success); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.CannotHaveEmptyTargetConfiguration", s_sdkDirectory); + } + + /// + /// Verify we log an error if no configuration is on the sdk reference + /// + [TestMethod] + public void LogErrorWhenNoArchitecture() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Debug"); + item.SetMetadata("TargetedSDKArchitecture", ""); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsFalse(success); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.CannotHaveEmptyTargetArchitecture", s_sdkDirectory); + } + + + /// + /// Verify the correct reference files are found and that by default we do log the reference files + /// added. + /// + [TestMethod] + public void VerifyReferencesLoggedAmd64() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "amd64"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[0].ItemSpec.Replace(t.References[0].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[1].ItemSpec.Replace(t.References[1].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[2].ItemSpec.Replace(t.References[2].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[3].ItemSpec.Replace(t.References[3].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[4].ItemSpec.Replace(t.References[4].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[5].ItemSpec.Replace(t.References[5].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[6].ItemSpec.Replace(t.References[6].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[7].ItemSpec.Replace(t.References[7].GetMetadata("SDKRootPath"), String.Empty)); + + Assert.IsTrue(t.References[0].ItemSpec.IndexOf("x64", StringComparison.OrdinalIgnoreCase) > -1); + Assert.IsTrue(Path.GetFileName(t.References[0].ItemSpec).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ReferenceGrouping").Equals("SDKWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ReferenceGroupingDisplayName").Length == 0); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFileType").Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[4].ItemSpec).Equals("E.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[4].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify the correct reference files are found and that by default we do log the reference files + /// added. + /// + [TestMethod] + public void VerifyReferencesLoggedX64() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x64"); + item.SetMetadata("DisplayName", "SDKWithManifestDisplayName"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[0].ItemSpec.Replace(t.References[0].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[1].ItemSpec.Replace(t.References[1].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[2].ItemSpec.Replace(t.References[2].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[3].ItemSpec.Replace(t.References[3].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[4].ItemSpec.Replace(t.References[4].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[5].ItemSpec.Replace(t.References[5].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[6].ItemSpec.Replace(t.References[6].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[7].ItemSpec.Replace(t.References[7].GetMetadata("SDKRootPath"), String.Empty)); + + Assert.IsTrue(t.References[0].ItemSpec.IndexOf("x64", StringComparison.OrdinalIgnoreCase) > -1); + Assert.IsTrue(Path.GetFileName(t.References[0].ItemSpec).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFile").Equals("true", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ReferenceGrouping").Equals("SDKWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("WinMDFileType").Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ReferenceGroupingDisplayName").Equals("SDKWithManifestDisplayName", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.References[4].ItemSpec).Equals("E.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFile").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("WinMDFileType").Length == 0); + Assert.IsTrue(t.References[4].GetMetadata("CopyLocal").Equals("false", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.References[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify the correct reference files are found and that if we do not want to log them we can set a property to do so. + /// + [TestMethod] + public void VerifyLogReferencesFalse() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + t.LogReferencesList = false; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[0].ItemSpec.Replace(t.References[0].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[1].ItemSpec.Replace(t.References[1].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[2].ItemSpec.Replace(t.References[2].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[3].ItemSpec.Replace(t.References[3].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[4].ItemSpec.Replace(t.References[4].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[5].ItemSpec.Replace(t.References[5].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[6].ItemSpec.Replace(t.References[6].GetMetadata("SDKRootPath"), String.Empty)); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingReference", t.References[7].ItemSpec.Replace(t.References[7].GetMetadata("SDKRootPath"), String.Empty)); + } + + /// + /// Verify the correct redist files are found and that by default we do not log the redist files + /// added. + /// + [TestMethod] + public void VerifyRedistFilesLogRedistFalse() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("CopyRedistToSubDirectory", "Super"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + t.LogRedistFilesList = false; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + Assert.IsTrue(t.RedistFiles.Length == 5); + + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[0].ItemSpec.Replace(t.RedistFiles[0].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[0].GetMetadata("TargetPath")); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[1].ItemSpec.Replace(t.RedistFiles[1].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[1].GetMetadata("TargetPath")); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[2].ItemSpec.Replace(t.RedistFiles[2].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[2].GetMetadata("TargetPath")); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[3].ItemSpec.Replace(t.RedistFiles[3].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[3].GetMetadata("TargetPath")); + engine.AssertLogDoesntContainMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[4].ItemSpec.Replace(t.RedistFiles[4].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[4].GetMetadata("TargetPath")); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[0].ItemSpec).Equals("A.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("TargetPath").Equals("Super\\A.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[1].ItemSpec).Equals("B.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("TargetPath").Equals("Super\\ASubDirectory\\TwoDeep\\B.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[2].ItemSpec).Equals("B.PRI", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("TargetPath").Equals("Super\\B.PRI", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("Root").Equals("Super", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[3].ItemSpec).Equals("C.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("TargetPath").Equals("Super\\C.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[4].ItemSpec).Equals("D.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("TargetPath").Equals("Super\\D.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("Root").Length == 0); + } + + /// + /// Verify the correct redist files are found and that by default we do not log the redist files + /// added. + /// + [TestMethod] + public void VerifyRedistFilesLogRedistTrue() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 5); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[0].ItemSpec.Replace(t.RedistFiles[0].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[0].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[1].ItemSpec.Replace(t.RedistFiles[1].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[1].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[2].ItemSpec.Replace(t.RedistFiles[2].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[2].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[3].ItemSpec.Replace(t.RedistFiles[3].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[3].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[4].ItemSpec.Replace(t.RedistFiles[4].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[4].GetMetadata("TargetPath")); + } + + /// + /// Verify the correct redist files are found and that by default we do not log the redist files + /// added. + /// + [TestMethod] + public void VerifyRedistFilesLogRedistTrueX64() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x64"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 5); + + Assert.IsTrue(t.RedistFiles[0].ItemSpec.IndexOf("x64", StringComparison.OrdinalIgnoreCase) > -1); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[0].ItemSpec.Replace(t.RedistFiles[0].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[0].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[1].ItemSpec.Replace(t.RedistFiles[1].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[1].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[2].ItemSpec.Replace(t.RedistFiles[2].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[2].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[3].ItemSpec.Replace(t.RedistFiles[3].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[3].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[4].ItemSpec.Replace(t.RedistFiles[4].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[4].GetMetadata("TargetPath")); + } + + /// + /// Verify the correct redist files are found and that by default we do not log the redist files + /// added. + /// + [TestMethod] + public void VerifyRedistFilesLogRedistTrueAmd64() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "amd64"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.RedistFiles.Length == 5); + + Assert.IsTrue(t.RedistFiles[0].ItemSpec.IndexOf("x64", StringComparison.OrdinalIgnoreCase) > -1); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[0].ItemSpec.Replace(t.RedistFiles[0].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[0].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[1].ItemSpec.Replace(t.RedistFiles[1].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[1].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[2].ItemSpec.Replace(t.RedistFiles[2].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[2].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[3].ItemSpec.Replace(t.RedistFiles[3].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[3].GetMetadata("TargetPath")); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.AddingRedistFile", t.RedistFiles[4].ItemSpec.Replace(t.RedistFiles[4].GetMetadata("SDKRootPath"), String.Empty), t.RedistFiles[4].GetMetadata("TargetPath")); + } + + /// + /// Make sure by default conflicts between references are logged as a comment if they are within the sdk itself + /// + [TestMethod] + public void LogNoWarningForReferenceConflictWithinSDK() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "false"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.References.Length == 8); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictReferenceSameSDK", "SDKWithManifest, Version=2.0", "References\\Retail\\X86\\A.winmd", "References\\CommonConfiguration\\Neutral\\A.dll"); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictReferenceSameSDK", "SDKWithManifest, Version=2.0", "References\\Retail\\X86\\A.winmd", "References\\CommonConfiguration\\Neutral\\A.winmd"); + Assert.AreEqual(0, engine.Warnings); + } + + /// + /// Make sure that if the LogReferenceConflictsWithinSDKAsWarning is set log a warning for conflicts within an SDK for references. + /// + [TestMethod] + public void LogWarningForReferenceConflictWithinSDK() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "false"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + t.LogReferenceConflictWithinSDKAsWarning = true; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.References.Length == 8); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictReferenceSameSDK", "SDKWithManifest, Version=2.0", "References\\Retail\\X86\\A.winmd", "References\\CommonConfiguration\\Neutral\\A.dll"); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictReferenceSameSDK", "SDKWithManifest, Version=2.0", "References\\Retail\\X86\\A.winmd", "References\\CommonConfiguration\\Neutral\\A.winmd"); + Assert.AreEqual(2, engine.Warnings); + } + + /// + /// Make sure by default conflicts between references are logged as a comment if they are within the sdk itself + /// + [TestMethod] + public void LogNoWarningForRedistConflictWithinSDK() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "false"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.RedistFiles.Length == 5); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictRedistSameSDK", "A.dll", "SDKWithManifest, Version=2.0", "Redist\\Retail\\X86\\A.dll", "Redist\\CommonConfiguration\\Neutral\\A.dll"); + Assert.AreEqual(0, engine.Warnings); + } + + /// + /// Make sure that if the LogRedistConflictsWithinSDKAsWarning is set log a warning for conflicts within an SDK for redist files. + /// + [TestMethod] + public void LogWarningForRedistConflictWithinSDK() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "false"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item }; + t.LogRedistConflictWithinSDKAsWarning = true; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + Assert.IsTrue(success); + Assert.IsTrue(t.RedistFiles.Length == 5); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictRedistSameSDK", "A.dll", "SDKWithManifest, Version=2.0", "Redist\\Retail\\X86\\A.dll", "Redist\\CommonConfiguration\\Neutral\\A.dll"); + Assert.AreEqual(1, engine.Warnings); + } + + /// + /// Verify if there are conflicts between references or redist files between sdks that we log a warning by default. + /// + [TestMethod] + public void LogReferenceAndRedistConflictBetweenSdks() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + ITaskItem item2 = new TaskItem(s_sdkDirectory2); + item2.SetMetadata("ExpandReferenceAssemblies", "true"); + item2.SetMetadata("TargetedSDKConfiguration", "Retail"); + item2.SetMetadata("TargetedSDKArchitecture", "x86"); + item2.SetMetadata("CopyRedist", "true"); + item2.SetMetadata("OriginalItemSpec", "AnotherSDK, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item, item2 }; + t.LogReferencesList = false; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + Assert.IsTrue(t.RedistFiles.Length == 6); + Assert.IsTrue(engine.Warnings == 2); + + string redistWinner = Path.Combine(s_sdkDirectory, "Redist\\Retail\\Neutral\\B.pri"); + string redistVictim = Path.Combine(s_sdkDirectory2, "Redist\\Retail\\X86\\B.pri"); + string referenceWinner = Path.Combine(s_sdkDirectory, "References\\Retail\\Neutral\\B.WinMD"); + string referenceVictim = Path.Combine(s_sdkDirectory2, "References\\Retail\\X86\\B.WinMD"); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictRedistDifferentSDK", "B.PRI", "SDKWithManifest, Version=2.0", "AnotherSDK, Version=2.0", redistWinner, redistVictim); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictReferenceDifferentSDK", "SDKWithManifest, Version=2.0", "AnotherSDK, Version=2.0", referenceWinner, referenceVictim); + } + + + /// + /// If a user create a target path that causes a conflict between two sdks then we want to warn + /// + [TestMethod] + public void LogReferenceAndRedistConflictBetweenSdksDueToCustomTargetPath() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + ITaskItem item2 = new TaskItem(s_sdkDirectory2); + item2.SetMetadata("ExpandReferenceAssemblies", "true"); + item2.SetMetadata("TargetedSDKConfiguration", "Retail"); + item2.SetMetadata("TargetedSDKArchitecture", "x86"); + item2.SetMetadata("CopyRedist", "true"); + item2.SetMetadata("OriginalItemSpec", "AnotherSDK, Version=2.0"); + item2.SetMetadata("CopyRedistToSubDirectory", "ASubDirectory\\TwoDeep"); + + t.ResolvedSDKReferences = new ITaskItem[] { item, item2 }; + t.LogReferencesList = false; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + Assert.IsTrue(t.RedistFiles.Length == 6); + Assert.IsTrue(engine.Warnings == 2); + + string redistWinner = Path.Combine(s_sdkDirectory, "Redist\\Retail\\Neutral\\ASubDirectory\\TwoDeep\\B.dll"); + string redistVictim = Path.Combine(s_sdkDirectory2, "Redist\\Retail\\X86\\B.dll"); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictRedistDifferentSDK", "ASUBDIRECTORY\\TWODEEP\\B.DLL", "SDKWithManifest, Version=2.0", "AnotherSDK, Version=2.0", redistWinner, redistVictim); + } + + /// + /// Verify if there are conflicts between references or redist files between sdks that we do not log a warning if a certain property (LogxxxConflictBetweenSDKsAsWarning is set to false. + /// + [TestMethod] + public void LogReferenceAndRedistConflictBetweenSdksNowarning() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "true"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + ITaskItem item2 = new TaskItem(s_sdkDirectory2); + item2.SetMetadata("ExpandReferenceAssemblies", "true"); + item2.SetMetadata("TargetedSDKConfiguration", "Retail"); + item2.SetMetadata("TargetedSDKArchitecture", "x86"); + item2.SetMetadata("CopyRedist", "true"); + item2.SetMetadata("OriginalItemSpec", "AnotherSDK, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item, item2 }; + t.LogReferencesList = false; + t.LogReferenceConflictBetweenSDKsAsWarning = false; + t.LogRedistConflictBetweenSDKsAsWarning = false; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + + Assert.IsTrue(success); + Assert.IsTrue(t.CopyLocalFiles.Length == 0); + Assert.IsTrue(t.References.Length == 8); + Assert.IsTrue(t.RedistFiles.Length == 6); + Assert.IsTrue(engine.Warnings == 0); + + + string redistWinner = Path.Combine(s_sdkDirectory, "Redist\\Retail\\Neutral\\B.pri"); + string redistVictim = Path.Combine(s_sdkDirectory2, "Redist\\Retail\\X86\\B.pri"); + string referenceWinner = Path.Combine(s_sdkDirectory, "References\\Retail\\Neutral\\B.WinMD"); + string referenceVictim = Path.Combine(s_sdkDirectory2, "References\\Retail\\X86\\B.WinMD"); + + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictRedistDifferentSDK", "B.PRI", "SDKWithManifest, Version=2.0", "AnotherSDK, Version=2.0", redistWinner, redistVictim); + engine.AssertLogContainsMessageFromResource(s_resourceDelegate, "GetSDKReferenceFiles.ConflictReferenceDifferentSDK", "SDKWithManifest, Version=2.0", "AnotherSDK, Version=2.0", referenceWinner, referenceVictim); + } + + /// + /// If there are conflicting redist files between two sdks but their target paths are different then we should copy both to the appx + /// + [TestMethod] + public void TwoSDKSConflictRedistButDifferentTargetPaths() + { + MockEngine engine = new MockEngine(); + GetSDKReferenceFiles t = new GetSDKReferenceFiles(); + t.BuildEngine = engine; + t.CacheFileFolderPath = s_cacheDirectory; + + ITaskItem item = new TaskItem(s_sdkDirectory); + item.SetMetadata("ExpandReferenceAssemblies", "false"); + item.SetMetadata("TargetedSDKConfiguration", "Retail"); + item.SetMetadata("TargetedSDKArchitecture", "x86"); + item.SetMetadata("CopyRedistToSubDirectory", "SDK1"); + item.SetMetadata("CopyRedist", "true"); + item.SetMetadata("OriginalItemSpec", "SDKWithManifest, Version=2.0"); + + ITaskItem item2 = new TaskItem(s_sdkDirectory2); + item2.SetMetadata("ExpandReferenceAssemblies", "false"); + item2.SetMetadata("TargetedSDKConfiguration", "Retail"); + item2.SetMetadata("TargetedSDKArchitecture", "x86"); + item2.SetMetadata("CopyRedistToSubDirectory", "SDK2"); + item2.SetMetadata("CopyRedist", "true"); + item2.SetMetadata("OriginalItemSpec", "AnotherSDK, Version=2.0"); + + t.ResolvedSDKReferences = new ITaskItem[] { item, item2 }; + t.LogReferencesList = false; + bool success = t.Execute(s_getAssemblyName, s_getAssemblyRuntimeVersion, FileUtilities.FileExistsNoThrow); + + Assert.IsTrue(success); + Assert.IsTrue(t.RedistFiles.Length == 7); + Assert.IsTrue(engine.Warnings == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[0].ItemSpec).Equals("A.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("TargetPath").Equals("SDK1\\A.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[0].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[1].ItemSpec).Equals("B.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("TargetPath").Equals("SDK2\\B.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("OriginalItemSpec").Equals("AnotherSDK, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[1].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[2].ItemSpec).Equals("B.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("TargetPath").Equals("SDK1\\ASubDirectory\\TwoDeep\\B.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[2].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[3].ItemSpec).Equals("B.pri", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("TargetPath").Equals("SDK2\\B.Pri", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("OriginalItemSpec").Equals("AnotherSDK, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[3].GetMetadata("Root").Equals("SDK2", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[4].ItemSpec).Equals("B.PRI", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("TargetPath").Equals("SDK1\\B.PRI", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[4].GetMetadata("Root").Equals("SDK1", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[5].ItemSpec).Equals("C.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[5].GetMetadata("TargetPath").Equals("SDK1\\C.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[5].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[5].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[5].GetMetadata("Root").Length == 0); + + Assert.IsTrue(Path.GetFileName(t.RedistFiles[6].ItemSpec).Equals("D.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[6].GetMetadata("TargetPath").Equals("SDK1\\D.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[6].GetMetadata("OriginalItemSpec").Equals("SDkWithManifest, Version=2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[6].GetMetadata("ResolvedFrom").Equals("GetSDKReferenceFiles", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.RedistFiles[6].GetMetadata("Root").Length == 0); + } + + private static AssemblyNameExtension GetAssemblyName(string path) + { + if (Path.GetFileName(path).Equals("C.winmd", StringComparison.OrdinalIgnoreCase)) + { + throw new BadImageFormatException(); + } + + if (Path.GetExtension(path).Equals(".winmd", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(path).Equals(".dll", StringComparison.OrdinalIgnoreCase)) + { + string fileName = Path.GetFileNameWithoutExtension(path); + return new AssemblyNameExtension(fileName + ", Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + return null; + } + + private static string GetImageRuntimeVersion(string path) + { + if (Path.GetFileName(path).Equals("A.winmd", StringComparison.OrdinalIgnoreCase)) + { + return "WindowsRuntime 1.0;CLR V2.0.50727"; + } + if (Path.GetExtension(path).Equals(".winmd", StringComparison.OrdinalIgnoreCase)) + { + return "WindowsRuntime 1.0"; + } + + if (Path.GetExtension(path).Equals(".dll", StringComparison.OrdinalIgnoreCase)) + { + return "CLR V2.0.50727"; + } + + return null; + } + + /// + /// Create a fake sdk structure on disk + /// + private static string CreateFakeSDKReferenceAssemblyDirectory1(out string sdkDirectory) + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "FakeSDKForReferenceAssemblies"); + sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string referenceAssemblyDirectoryConfigx86 = Path.Combine(sdkDirectory, "References\\Retail\\X86"); + string referenceAssemblyDirectoryConfigx64 = Path.Combine(sdkDirectory, "References\\Retail\\X64"); + string referenceAssemblyDirectoryConfigNeutral = Path.Combine(sdkDirectory, "References\\Retail\\Neutral"); + string referenceAssemblyDirectoryCommonConfigNeutral = Path.Combine(sdkDirectory, "References\\CommonConfiguration\\Neutral"); + string referenceAssemblyDirectoryCommonConfigX86 = Path.Combine(sdkDirectory, "References\\CommonConfiguration\\X86"); + string referenceAssemblyDirectoryCommonConfigX64 = Path.Combine(sdkDirectory, "References\\CommonConfiguration\\X64"); + + string redistDirectoryConfigx86 = Path.Combine(sdkDirectory, "Redist\\Retail\\X86"); + string redistDirectoryConfigx64 = Path.Combine(sdkDirectory, "Redist\\Retail\\X64"); + string redistDirectoryConfigNeutral = Path.Combine(sdkDirectory, "Redist\\Retail\\Neutral"); + string redistDirectoryCommonConfigNeutral = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\Neutral"); + string redistDirectoryCommonConfigX86 = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\X86"); + string redistDirectoryCommonConfigX64 = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\X64"); + + string designTimeDirectoryConfigx86 = Path.Combine(sdkDirectory, "DesignTime\\Retail\\X86"); + string designTimeDirectoryConfigNeutral = Path.Combine(sdkDirectory, "DesignTime\\Retail\\Neutral"); + string designTimeDirectoryCommonConfigNeutral = Path.Combine(sdkDirectory, "DesignTime\\CommonConfiguration\\Neutral"); + string designTimeDirectoryCommonConfigX86 = Path.Combine(sdkDirectory, "DesignTime\\CommonConfiguration\\X86"); + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + + Directory.CreateDirectory(referenceAssemblyDirectoryConfigx86); + Directory.CreateDirectory(referenceAssemblyDirectoryConfigx64); + Directory.CreateDirectory(referenceAssemblyDirectoryConfigNeutral); + Directory.CreateDirectory(referenceAssemblyDirectoryCommonConfigNeutral); + Directory.CreateDirectory(referenceAssemblyDirectoryCommonConfigX86); + Directory.CreateDirectory(referenceAssemblyDirectoryCommonConfigX64); + + Directory.CreateDirectory(redistDirectoryConfigx86); + Directory.CreateDirectory(redistDirectoryConfigx64); + Directory.CreateDirectory(redistDirectoryConfigNeutral); + Directory.CreateDirectory(Path.Combine(redistDirectoryConfigNeutral, "ASubDirectory\\TwoDeep")); + Directory.CreateDirectory(redistDirectoryCommonConfigNeutral); + Directory.CreateDirectory(redistDirectoryCommonConfigX86); + Directory.CreateDirectory(redistDirectoryCommonConfigX64); + + Directory.CreateDirectory(designTimeDirectoryConfigx86); + Directory.CreateDirectory(designTimeDirectoryConfigNeutral); + Directory.CreateDirectory(designTimeDirectoryCommonConfigNeutral); + Directory.CreateDirectory(designTimeDirectoryCommonConfigX86); + + string testWinMD = Path.Combine(referenceAssemblyDirectoryConfigx86, "A.winmd"); + string testWinMD64 = Path.Combine(referenceAssemblyDirectoryConfigx64, "A.winmd"); + string testWinMDNeutral = Path.Combine(referenceAssemblyDirectoryConfigNeutral, "B.winmd"); + string testWinMDNeutralWinXML = Path.Combine(referenceAssemblyDirectoryConfigNeutral, "B.xml"); + string testWinMDCommonConfigurationx86 = Path.Combine(referenceAssemblyDirectoryCommonConfigX86, "C.winmd"); + string testWinMDCommonConfigurationx64 = Path.Combine(referenceAssemblyDirectoryCommonConfigX64, "C.winmd"); + string testWinMDCommonConfigurationNeutral = Path.Combine(referenceAssemblyDirectoryCommonConfigNeutral, "D.winmd"); + string testWinMDCommonConfigurationNeutralDupe = Path.Combine(referenceAssemblyDirectoryCommonConfigNeutral, "A.winmd"); + + string testRA = Path.Combine(referenceAssemblyDirectoryConfigx86, "E.dll"); + string testRA64 = Path.Combine(referenceAssemblyDirectoryConfigx64, "E.dll"); + string testRANeutral = Path.Combine(referenceAssemblyDirectoryConfigNeutral, "F.dll"); + string testRACommonConfigurationx86 = Path.Combine(referenceAssemblyDirectoryCommonConfigX86, "G.dll"); + string testRACommonConfigurationx64 = Path.Combine(referenceAssemblyDirectoryCommonConfigX64, "G.dll"); + string testRACommonConfigurationNeutral = Path.Combine(referenceAssemblyDirectoryCommonConfigNeutral, "H.dll"); + // Make duplicate of winmd but change to dll extenson so that we can make sure that we eliminate duplicate file names. + string testRACommonConfigurationNeutralDupe = Path.Combine(referenceAssemblyDirectoryCommonConfigNeutral, "A.dll"); + + string redist = Path.Combine(redistDirectoryConfigx86, "A.dll"); + string redist64 = Path.Combine(redistDirectoryConfigx64, "A.dll"); + string redistNeutral = Path.Combine(redistDirectoryConfigNeutral, "ASubDirectory\\TwoDeep\\B.dll"); + string redistNeutralPri = Path.Combine(redistDirectoryConfigNeutral, "B.pri"); + string redistCommonConfigurationx86 = Path.Combine(redistDirectoryCommonConfigX86, "C.dll"); + string redistCommonConfigurationx64 = Path.Combine(redistDirectoryCommonConfigX64, "C.dll"); + string redistCommonConfigurationNeutral = Path.Combine(redistDirectoryCommonConfigNeutral, "D.dll"); + string redistCommonConfigurationNeutralDupe = Path.Combine(redistDirectoryCommonConfigNeutral, "A.dll"); + + + File.WriteAllText(testWinMDNeutralWinXML, "TestXml"); + File.WriteAllText(testWinMD, "TestWinmd"); + File.WriteAllText(testWinMD64, "TestWinmd"); + File.WriteAllText(testWinMDNeutral, "TestWinmd"); + File.WriteAllText(testWinMDCommonConfigurationNeutral, "TestWinmd"); + File.WriteAllText(testWinMDCommonConfigurationx86, "TestWinmd"); + File.WriteAllText(testWinMDCommonConfigurationx64, "TestWinmd"); + File.WriteAllText(testWinMDCommonConfigurationNeutralDupe, "TestWinmd"); + File.WriteAllText(testRA, "TestWinmd"); + File.WriteAllText(testRA64, "TestWinmd"); + File.WriteAllText(testRANeutral, "TestWinmd"); + File.WriteAllText(testRACommonConfigurationNeutral, "TestWinmd"); + File.WriteAllText(testRACommonConfigurationx86, "TestWinmd"); + File.WriteAllText(testRACommonConfigurationx64, "TestWinmd"); + File.WriteAllText(testRACommonConfigurationNeutralDupe, "TestWinmd"); + + File.WriteAllText(redist, "TestWinmd"); + File.WriteAllText(redist64, "TestWinmd"); + File.WriteAllText(redistNeutral, "TestWinmd"); + File.WriteAllText(redistNeutralPri, "TestWinmd"); + File.WriteAllText(redistCommonConfigurationNeutral, "TestWinmd"); + File.WriteAllText(redistCommonConfigurationx86, "TestWinmd"); + File.WriteAllText(redistCommonConfigurationx64, "TestWinmd"); + File.WriteAllText(redistCommonConfigurationNeutralDupe, "TestWinmd"); + + return testDirectoryRoot; + } + + /// + /// Create a fake sdk structure on disk + /// + private static string CreateFakeSDKReferenceAssemblyDirectory2(out string sdkDirectory) + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "FakeSDKForReferenceAssemblies"); + sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\AnotherSDK\\2.0\\"); + string referenceAssemblyDirectoryConfigx86 = Path.Combine(sdkDirectory, "References\\Retail\\X86"); + string redistDirectoryConfigx86 = Path.Combine(sdkDirectory, "Redist\\Retail\\X86"); + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + + Directory.CreateDirectory(referenceAssemblyDirectoryConfigx86); + Directory.CreateDirectory(redistDirectoryConfigx86); + + string testWinMD = Path.Combine(referenceAssemblyDirectoryConfigx86, "B.winmd"); + string redist = Path.Combine(redistDirectoryConfigx86, "B.pri"); + string redist2 = Path.Combine(redistDirectoryConfigx86, "B.dll"); + + File.WriteAllText(testWinMD, "TestWinmd"); + File.WriteAllText(redist, "TestWinmd"); + File.WriteAllText(redist2, "TestWinmd"); + + return testDirectoryRoot; + } + } +} diff --git a/src/XMakeTasks/UnitTests/LC_Tests.cs b/src/XMakeTasks/UnitTests/LC_Tests.cs new file mode 100644 index 00000000000..43e86b88e4a --- /dev/null +++ b/src/XMakeTasks/UnitTests/LC_Tests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Unit tests for the LC task + /// + [TestClass] + public class LC_Tests + { + /// + /// Tests a simple case of valid arguments + /// + [TestMethod] + [Ignore] + // Ignore: Doesn't work. + public void SimpleValidArguments() + { + LC task = new LC(); + + task.BuildEngine = new MockEngine(); + task.Sources = new TaskItem[] { new TaskItem("complist.licx"), new TaskItem("othersrc.txt") }; + task.LicenseTarget = new TaskItem("target.exe"); + task.OutputDirectory = "bin\\debug"; + task.ReferencedAssemblies = new TaskItem[] { new TaskItem("LicensedControl.dll"), new TaskItem("OtherControl.dll") }; + task.NoLogo = true; + + CommandLine.ValidateHasParameter(task, "/complist:complist.licx", false /* don't use response file */); + CommandLine.ValidateHasParameter(task, "/complist:othersrc.txt", false /* don't use response file */); + CommandLine.ValidateHasParameter(task, "/target:target.exe", false /* don't use response file */); + CommandLine.ValidateHasParameter(task, "/outdir:bin\\debug", false /* don't use response file */); + CommandLine.ValidateHasParameter(task, "/i:LicensedControl.dll", false /* don't use response file */); + CommandLine.ValidateHasParameter(task, "/i:OtherControl.dll", false /* don't use response file */); + CommandLine.ValidateHasParameter(task, "/nologo", false /* don't use response file */); + } + } +} diff --git a/src/XMakeTasks/UnitTests/MSBuild_Tests.cs b/src/XMakeTasks/UnitTests/MSBuild_Tests.cs new file mode 100644 index 00000000000..b7d4c521458 --- /dev/null +++ b/src/XMakeTasks/UnitTests/MSBuild_Tests.cs @@ -0,0 +1,1353 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class MSBuildTask_Tests + { + [TestInitialize] + public void SetUp() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + [TestCleanup] + public void TearDown() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + } + + + + /// + /// If we pass in an item spec that is over the max path but it can be normalized down to something under the max path, we should still work and not + /// throw a path too long exception + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void ProjectItemSpecTooLong() + { + string currentDirectory = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = Path.GetTempPath(); + + string tempPath = Path.GetTempPath(); + + string tempProject = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + + + "); + + string fileName = Path.GetFileName(tempProject); + + string projectFile1 = null; + for (int i = 0; i < 250; i++) + { + projectFile1 += "..\\"; + } + + int rootLength = Path.GetPathRoot(tempPath).Length; + string tempPathNoRoot = tempPath.Substring(rootLength); + + projectFile1 += Path.Combine(tempPathNoRoot, fileName); + try + { + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + + msbuildTask.Projects = new ITaskItem[] { new TaskItem(projectFile1) }; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(tempProject); + } + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + + /// + /// Ensure that the MSBuild task tags any output items with two pieces of metadata -- MSBuildSourceProjectFile and + /// MSBuildSourceTargetName -- that give an indication of where the items came from. + /// + [TestMethod] + public void OutputItemsAreTaggedWithProjectFileAndTargetName() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + + msbuildTask.Projects = new ITaskItem[] { new TaskItem(projectFile1), new TaskItem(projectFile2) }; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + a1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetA + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + b2.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + c1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetC + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + g2.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Ensures that it is possible to call the MSBuild task + /// with an empty Projects parameter, and it shouldn't error, and it shouldn't try to + /// build itself. + /// + [TestMethod] + public void EmptyProjectsParameterResultsInNoop() + { + string projectContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + "); + + MockLogger logger = new MockLogger(); + Project project = ObjectModelHelpers.CreateInMemoryProject(projectContents, logger); + + bool success = project.Build(); + Assert.IsTrue(success, "Build failed. See Standard Out tab for details"); + } + + /// + /// Verifies that nonexistent projects aren't normally skipped + /// + [TestMethod] + public void NormallyDoNotSkipNonexistentProjects() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"SkipNonexistentProjectsMain.csproj"); + Assert.IsTrue(logger.FullLog.Contains("MSB3202")); // project file not found + } + + /// + /// Verifies that nonexistent projects aren't normally skipped + /// + [TestMethod] + public void NormallyDoNotSkipNonexistentProjectsBuildInParallel() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"SkipNonexistentProjectsMain.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 1); + Assert.IsTrue(logger.FullLog.Contains("MSB3202")); // project file not found + } + + /// + /// Verifies that nonexistent projects are skipped when requested + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SkipNonexistentProjects() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "foo.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"SkipNonexistentProjectsMain.csproj"); + + logger.AssertLogContains("Hello from foo.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 0); + Assert.IsTrue(logger.FullLog.Contains("this_project_does_not_exist.csproj")); // for the missing project + Assert.IsFalse(logger.FullLog.Contains("MSB3202")); // project file not found error + } + + /// + /// Verifies that nonexistent projects are skipped when requested when building in parallel. + /// DDB # 125831 + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SkipNonexistentProjectsBuildingInParallel() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "SkipNonexistentProjectsMain.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "foo.csproj", + @" + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"SkipNonexistentProjectsMain.csproj"); + + logger.AssertLogContains("Hello from foo.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 0); + Assert.IsTrue(logger.FullLog.Contains("this_project_does_not_exist.csproj")); // for the missing project + Assert.IsFalse(logger.FullLog.Contains("MSB3202")); // project file not found error + } + + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void LogErrorWhenBuildingVCProj() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "BuildingVCProjMain.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "foo.csproj", + @" + + + + + "); + + ObjectModelHelpers.CreateFileInTempProjectDirectory( + "blah.vcproj", + @" + + + + + + "); + + MockLogger logger = ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"BuildingVCProjMain.csproj"); + + logger.AssertLogContains("Hello from foo.csproj"); + Assert.IsTrue(logger.WarningCount == 0); + Assert.IsTrue(logger.ErrorCount == 1); + Assert.IsTrue(logger.FullLog.Contains("MSB3204")); // upgrade to vcxproj needed + } + + /// + /// Calling the MSBuild task, passing in a property + /// in the Properties parameter that has a special character in its value, such as semicolon. + /// However, it's a situation where the project author doesn't have control over the + /// property value and so he can't escape it himself. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void PropertyOverridesContainSemicolon() + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + // ------------------------------------------------------- + // ConsoleApplication1.csproj + // ------------------------------------------------------- + + // Just a normal console application project. + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"bug'533'369\Sub;Dir\ConsoleApplication1\ConsoleApplication1.csproj", @" + + + + Debug + AnyCPU + Exe + ConsoleApplication1 + + + true + full + false + bin\Debug\ + + + pdbonly + true + bin\Release\ + + + + + + + + + + + + "); + + // ------------------------------------------------------- + // Program.cs + // ------------------------------------------------------- + + // Just a normal console application project. + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"bug'533'369\Sub;Dir\ConsoleApplication1\Program.cs", @" + using System; + + namespace ConsoleApplication32 + { + class Program + { + static void Main(string[] args) + { + Console.WriteLine(`Hello world`); + } + } + } + "); + + + // ------------------------------------------------------- + // TeamBuild.proj + // ------------------------------------------------------- + // Attempts to build the above ConsoleApplication1.csproj by calling the MSBuild task, + // and overriding the OutDir property. However, the value being passed into OutDir + // is coming from another property which is produced by CreateProperty and has + // some special characters in it. + ObjectModelHelpers.CreateFileInTempProjectDirectory( + @"bug'533'369\Sub;Dir\TeamBuild.proj", @" + + + + + + + + + + + + + "); + + ObjectModelHelpers.BuildTempProjectFileExpectSuccess(@"bug'533'369\Sub;Dir\TeamBuild.proj"); + + ObjectModelHelpers.AssertFileExistsInTempProjectDirectory(@"bug'533'369\Sub;Dir\binaries\ConsoleApplication1.exe"); + } + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + public void DifferentGlobalPropertiesWithDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile1), + new TaskItem(projectFile2), + new TaskItem(projectFile2) + }; + projects[1].SetMetadata("Properties", "MyProp=1"); + projects[3].SetMetadata("Properties", "MyProp=1"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + msbuildTask.Projects = projects; + msbuildTask.Properties = new string[] { "MyProp=0" }; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + a1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetA + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + public void DifferentGlobalPropertiesWithoutDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile1), + new TaskItem(projectFile2), + new TaskItem(projectFile2) + }; + projects[1].SetMetadata("Properties", "MyProp=1"); + projects[3].SetMetadata("Properties", "MyProp=1"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + public void DifferentGlobalPropertiesWithBlanks() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile1), + new TaskItem(projectFile2), + new TaskItem(projectFile2) + }; + projects[1].SetMetadata("Properties", ""); + projects[3].SetMetadata("Properties", "MyProp=1"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing different global properites via metadata works + /// + [TestMethod] + public void DifferentGlobalPropertiesInvalid() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile1), + new TaskItem(projectFile2), + new TaskItem(projectFile2) + }; + projects[1].SetMetadata("Properties", "=1"); + projects[3].SetMetadata("Properties", "=;1"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsFalse(success, "Build succeeded. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Check if passing additional global properites via metadata works + /// + [TestMethod] + public void DifferentAdditionalPropertiesWithDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile2) + }; + projects[0].SetMetadata("AdditionalProperties", "MyPropA=1"); + projects[1].SetMetadata("AdditionalProperties", "MyPropA=0"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + msbuildTask.Properties = new string[] { "MyPropG=1" }; + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + a1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetA + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing additional global properites via metadata works + /// + [TestMethod] + public void DifferentAdditionalPropertiesWithGlobalProperties() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile2) + }; + + projects[0].SetMetadata("AdditionalProperties", "MyPropA=1"); + projects[1].SetMetadata("AdditionalProperties", "MyPropA=1"); + + projects[0].SetMetadata("Properties", "MyPropG=1"); + projects[1].SetMetadata("Properties", "MyPropG=0"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + g1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetG + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + + /// + /// Check if passing additional global properites via metadata works + /// + [TestMethod] + public void DifferentAdditionalPropertiesWithoutDefault() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile1), + new TaskItem(projectFile2) + }; + + projects[0].SetMetadata("AdditionalProperties", "MyPropA=1"); + projects[1].SetMetadata("AdditionalProperties", "MyPropA=1"); + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + string expectedItemOutputs = string.Format(@" + b1.dll : MSBuildSourceProjectFile={0} ; MSBuildSourceTargetName=TargetB + h1.dll : MSBuildSourceProjectFile={1} ; MSBuildSourceTargetName=TargetH + ", projectFile1, projectFile2); + + ObjectModelHelpers.AssertItemsMatch(expectedItemOutputs, msbuildTask.TargetOutputs, false /* order of items not enforced */); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Properties and Targets that use non-standard separation chars + /// + [TestMethod] + public void TargetsWithSeparationChars() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + Clean%3BBuild%3CBuildAgain + + + + + + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile2) + }; + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Verify stopOnFirstFailure with BuildInParallel override message are correctly logged + /// Also verify stop on first failure will not build the second project if the first one failed + /// The Aardvark tests which also test StopOnFirstFailure are at: + /// qa\md\wd\DTP\MSBuild\ShippingExtensions\ShippingTasks\MSBuild\_Tst\MSBuild.StopOnFirstFailure + /// + [TestMethod] + public void StopOnFirstFailureandBuildInParallelSingleNode() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string project2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(project1), new TaskItem(project2) + }; + + // Test the various combinations of BuildInParallel and StopOnFirstFailure when the msbuild task is told there are not multiple nodes + // running in the system + for (int i = 0; i < 4; i++) + { + MSBuild msbuildTask = new MSBuild(); + // By default IsMultipleNodesIs false + MockEngine mockEngine = new MockEngine(); + mockEngine.IsRunningMultipleNodes = false; + msbuildTask.BuildEngine = mockEngine; + msbuildTask.Projects = projects; + msbuildTask.Targets = new string[] { "msbuild" }; + // Make success true as the expected result is false + bool success = true; + switch (i) + { + case 0: + // Verify setting BuildInParallel and StopOnFirstFailure to + // true will cause the msbuild task to set BuildInParallel to false during the execute + msbuildTask.BuildInParallel = true; + msbuildTask.StopOnFirstFailure = true; + success = msbuildTask.Execute(); + // Verify build did not build second project which has the message SecondProject + mockEngine.AssertLogDoesntContain("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsFalse(msbuildTask.BuildInParallel, "Iteration of 0 Epected BuildInParallel to be false"); + break; + case 1: + // Verify setting BuildInParallel to true and StopOnFirstFailure to + // false will cause no change in BuildInParallel + msbuildTask.BuildInParallel = true; + msbuildTask.StopOnFirstFailure = false; + success = msbuildTask.Execute(); + // Verify build did build second project which has the message SecondProject + mockEngine.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsTrue(msbuildTask.BuildInParallel, "Iteration of 1 Epected BuildInParallel to be true"); + break; + case 2: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // true will cause no change in BuildInParallel + msbuildTask.BuildInParallel = false; + msbuildTask.StopOnFirstFailure = true; + success = msbuildTask.Execute(); + // Verify build did not build second project which has the message SecondProject + mockEngine.AssertLogDoesntContain("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsFalse(msbuildTask.BuildInParallel, "Iteration of 2 Epected BuildInParallel to be false"); + break; + + case 3: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // false will cause no change in BuildInParallel + msbuildTask.BuildInParallel = false; + msbuildTask.StopOnFirstFailure = false; + success = msbuildTask.Execute(); + // Verify build did build second project which has the message SecondProject + mockEngine.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsFalse(msbuildTask.BuildInParallel, "Iteration of 3 Epected BuildInParallel to be false"); + break; + } + // The build should fail as the first project has an error + Assert.IsFalse(success, "Iteration of i " + i + " Build Succeded. See 'Standard Out' tab for details."); + } + } + finally + { + File.Delete(project1); + File.Delete(project2); + } + } + + /// + /// Verify stopOnFirstFailure with BuildInParallel override message are correctly logged when there are multiple nodes + /// + [TestMethod] + public void StopOnFirstFailureandBuildInParallelMultipleNode() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string project2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(project1), new TaskItem(project2) + }; + + // Test the various combinations of BuildInParallel and StopOnFirstFailure when the msbuild task is told there are multiple nodes + // running in the system + for (int i = 0; i < 4; i++) + { + MSBuild msbuildTask = new MSBuild(); + MockEngine mockEngine = new MockEngine(); + mockEngine.IsRunningMultipleNodes = true; + msbuildTask.BuildEngine = mockEngine; + msbuildTask.Projects = projects; + msbuildTask.Targets = new string[] { "msbuild" }; + // Make success true as the expected resultis false + bool success = true; + switch (i) + { + case 0: + // Verify setting BuildInParallel and StopOnFirstFailure to + // true will not cause the msbuild task to set BuildInParallel to false during the execute + msbuildTask.BuildInParallel = true; + msbuildTask.StopOnFirstFailure = true; + success = msbuildTask.Execute(); + // Verify build did build second project which has the message SecondProject + mockEngine.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsTrue(msbuildTask.BuildInParallel, "Iteration of 0 Epected BuildInParallel to be true"); + break; + case 1: + // Verify setting BuildInParallel to true and StopOnFirstFailure to + // false will cause no change in BuildInParallel + msbuildTask.BuildInParallel = true; + msbuildTask.StopOnFirstFailure = false; + success = msbuildTask.Execute(); + // Verify build did build second project which has the message SecondProject + mockEngine.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsTrue(msbuildTask.BuildInParallel, "Iteration of 1 Epected BuildInParallel to be true"); + break; + case 2: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // true will cause no change in BuildInParallel + msbuildTask.BuildInParallel = false; + msbuildTask.StopOnFirstFailure = true; + success = msbuildTask.Execute(); + // Verify build did not build second project which has the message SecondProject + mockEngine.AssertLogDoesntContain("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsFalse(msbuildTask.BuildInParallel, "Iteration of 2 Epected BuildInParallel to be false"); + break; + + case 3: + // Verify setting BuildInParallel to false and StopOnFirstFailure to + // false will cause no change in BuildInParallel + msbuildTask.BuildInParallel = false; + msbuildTask.StopOnFirstFailure = false; + success = msbuildTask.Execute(); + // Verify build did build second project which has the message SecondProject + mockEngine.AssertLogContains("SecondProject"); + // Verify the correct msbuild task messages are in the log + Assert.IsFalse(msbuildTask.BuildInParallel, "Iteration of 3 Epected BuildInParallel to be false"); + break; + } + // The build should fail as the first project has an error + Assert.IsFalse(success, "Iteration of i " + i + " Build Succeded. See 'Standard Out' tab for details."); + } + } + finally + { + File.Delete(project1); + File.Delete(project2); + } + } + + /// + /// Test the skipping of the remaining projects. Verify the skip message is only displayed when there are projects to skip. + /// + [TestMethod] + public void SkipRemainingProjects() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + string project2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + "); + + try + { + // Test the case where there is only one project and it has an error + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(project1) + }; + + MSBuild msbuildTask = new MSBuild(); + MockEngine mockEngine = new MockEngine(); + mockEngine.IsRunningMultipleNodes = true; + msbuildTask.BuildEngine = mockEngine; + msbuildTask.Projects = projects; + msbuildTask.Targets = new string[] { "msbuild" }; + msbuildTask.BuildInParallel = false; + msbuildTask.StopOnFirstFailure = true; + bool success = msbuildTask.Execute(); + Assert.IsFalse(success, "Build Succeeded. See 'Standard Out' tab for details."); + + // Test the case where there are two projects and the last one has an error + projects = new ITaskItem[] + { + new TaskItem(project2), new TaskItem(project1) + }; + + msbuildTask = new MSBuild(); + mockEngine = new MockEngine(); + mockEngine.IsRunningMultipleNodes = true; + msbuildTask.BuildEngine = mockEngine; + msbuildTask.Projects = projects; + msbuildTask.Targets = new string[] { "msbuild" }; + msbuildTask.BuildInParallel = false; + msbuildTask.StopOnFirstFailure = true; + success = msbuildTask.Execute(); + Assert.IsFalse(success, "Build Succeeded. See 'Standard Out' tab for details."); + } + finally + { + File.Delete(project1); + File.Delete(project2); + } + } + + /// + /// Verify the behavior of Target execution with StopOnFirstFailure + /// + [TestMethod] + public void TargetStopOnFirstFailureBuildInParallel() + { + string project1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(project1) + }; + for (int i = 0; i < 6; i++) + { + // Test the case where the error is in the last target + MSBuild msbuildTask = new MSBuild(); + MockEngine mockEngine = new MockEngine(); + msbuildTask.BuildEngine = mockEngine; + msbuildTask.Projects = projects; + // Set to true as the expected result is false + bool success = true; + switch (i) + { + case 0: + // Test the case where the error is in the last project and RunEachTargetSeparately = true + msbuildTask.StopOnFirstFailure = true; + msbuildTask.RunEachTargetSeparately = true; + msbuildTask.Targets = new string[] { "T1", "T2", "T3" }; + success = msbuildTask.Execute(); + mockEngine.AssertLogContains("Proj2 T1 message"); + mockEngine.AssertLogContains("Proj2 T2 message"); + break; + case 1: + // Test the case where the error is in the second target out of 3. + msbuildTask.StopOnFirstFailure = true; + msbuildTask.RunEachTargetSeparately = true; + msbuildTask.Targets = new string[] { "T1", "T3", "T2" }; + success = msbuildTask.Execute(); + mockEngine.AssertLogContains("Proj2 T1 message"); + mockEngine.AssertLogDoesntContain("Proj2 T2 message"); + // The build should fail as the first project has an error + break; + case 2: + // Test case where error is in second last target but stopOnFirstFailure is false + msbuildTask.RunEachTargetSeparately = true; + msbuildTask.StopOnFirstFailure = false; + msbuildTask.Targets = new string[] { "T1", "T3", "T2" }; + success = msbuildTask.Execute(); + mockEngine.AssertLogContains("Proj2 T1 message"); + mockEngine.AssertLogContains("Proj2 T2 message"); + break; + // Test the cases where RunEachTargetSeparately is false. In these cases all of the targets should be submitted at once + case 3: + // Test the case where the error is in the last project and RunEachTargetSeparately = true + msbuildTask.StopOnFirstFailure = true; + msbuildTask.Targets = new string[] { "T1", "T2", "T3" }; + success = msbuildTask.Execute(); + mockEngine.AssertLogContains("Proj2 T1 message"); + mockEngine.AssertLogContains("Proj2 T2 message"); + // The build should fail as the first project has an error + break; + case 4: + // Test the case where the error is in the second target out of 3. + msbuildTask.StopOnFirstFailure = true; + msbuildTask.Targets = new string[] { "T1", "T3", "T2" }; + success = msbuildTask.Execute(); + mockEngine.AssertLogContains("Proj2 T1 message"); + mockEngine.AssertLogDoesntContain("Proj2 T2 message"); + // The build should fail as the first project has an error + break; + case 5: + // Test case where error is in second last target but stopOnFirstFailure is false + msbuildTask.StopOnFirstFailure = false; + msbuildTask.Targets = new string[] { "T1", "T3", "T2" }; + success = msbuildTask.Execute(); + mockEngine.AssertLogContains("Proj2 T1 message"); + mockEngine.AssertLogDoesntContain("Proj2 T2 message"); + break; + } + + // The build should fail as the first project has an error + Assert.IsFalse(success, "Iteration of i:" + i + "Build Succeded. See 'Standard Out' tab for details."); + } + } + finally + { + File.Delete(project1); + } + } + + /// + /// Properties and Targets that use non-standard separation chars + /// + [TestMethod] + public void PropertiesWithSeparationChars() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + a%3BA + b;B + c;C + d%3BD + + + + + C=$(CValues)%3BD=$(DValues) + + + + + + + + + + "); + + try + { + ITaskItem[] projects = new ITaskItem[] + { + new TaskItem(projectFile2) + }; + + MSBuild msbuildTask = new MSBuild(); + msbuildTask.BuildEngine = new MockEngine(); + msbuildTask.Projects = projects; + + bool success = msbuildTask.Execute(); + Assert.IsTrue(success, "Build failed. See 'Standard Out' tab for details."); + + Assert.AreEqual(5, msbuildTask.TargetOutputs.Length); + Assert.AreEqual("|a", msbuildTask.TargetOutputs[0].ItemSpec); + Assert.AreEqual("A|b", msbuildTask.TargetOutputs[1].ItemSpec); + Assert.AreEqual("B|c", msbuildTask.TargetOutputs[2].ItemSpec); + Assert.AreEqual("C|d", msbuildTask.TargetOutputs[3].ItemSpec); + Assert.AreEqual("D|", msbuildTask.TargetOutputs[4].ItemSpec); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + + /// + /// Orcas had a bug that if the target casing specified was not correct, we would still build it, + /// but not return any target outputs! + /// + [TestMethod] + public void TargetNameIsCaseInsensitive() + { + string projectFile1 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + "); + + string projectFile2 = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + + + + + + + "); + + try + { + Project project = new Project(projectFile2); + MockLogger logger = new MockLogger(); + + project.Build(logger); + + logger.AssertLogContains("[foo.out]"); + } + finally + { + File.Delete(projectFile1); + File.Delete(projectFile2); + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/MakeDir_Tests.cs b/src/XMakeTasks/UnitTests/MakeDir_Tests.cs new file mode 100644 index 00000000000..bb9b449f0de --- /dev/null +++ b/src/XMakeTasks/UnitTests/MakeDir_Tests.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class MakeDir_Tests + { + /// + /// Make sure that attributes set on input items are forwarded to output items. + /// + [TestMethod] + public void AttributeForwarding() + { + string temp = Path.GetTempPath(); + string dir = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A391"); + + try + { + MakeDir t = new MakeDir(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Directories = new ITaskItem[] + { + new TaskItem(dir) + }; + t.Directories[0].SetMetadata("Locale", "en-GB"); + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.DirectoriesCreated.Length); + Assert.AreEqual(dir, t.DirectoriesCreated[0].ItemSpec); + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("MakeDir.Comment"), dir) + ) + ); + Assert.AreEqual("en-GB", t.DirectoriesCreated[0].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual(dir, t.DirectoriesCreated[0].ItemSpec); + } + finally + { + Directory.Delete(dir); + } + } + + /// + /// Check that if we fail to create a folder, we don't pass + /// through the input. + /// + [TestMethod] + public void SomeInputsFailToCreate() + { + string temp = Path.GetTempPath(); + string file = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A38e"); + string dir = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A38f"); + string invalid = "!@#$%^&*()|"; + string dir2 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A390"); + + try + { + FileStream fs = File.Create(file); + fs.Close(); //we're gonna try to delete it + + MakeDir t = new MakeDir(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Directories = new ITaskItem[] + { + new TaskItem(dir), + new TaskItem(file), + new TaskItem(invalid), + new TaskItem(dir2) + }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(2, t.DirectoriesCreated.Length); + Assert.AreEqual(dir, t.DirectoriesCreated[0].ItemSpec); + Assert.AreEqual(dir2, t.DirectoriesCreated[1].ItemSpec); + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("MakeDir.Comment"), dir) + ) + ); + } + finally + { + Directory.Delete(dir); + File.Delete(file); + Directory.Delete(dir2); + } + } + + /// + /// Creating a directory that already exists should not log anything. + /// + [TestMethod] + public void CreateNewDirectory() + { + string temp = Path.GetTempPath(); + string dir = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A38C"); + + try + { + MakeDir t = new MakeDir(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Directories = new ITaskItem[] + { + new TaskItem(dir) + }; + + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.DirectoriesCreated.Length); + Assert.AreEqual(dir, t.DirectoriesCreated[0].ItemSpec); + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("MakeDir.Comment"), dir) + ) + ); + + engine.Log = ""; + success = t.Execute(); + + Assert.IsTrue(success); + // should still return directory even though it didn't need to be created + Assert.AreEqual(1, t.DirectoriesCreated.Length); + Assert.AreEqual(dir, t.DirectoriesCreated[0].ItemSpec); + Assert.IsTrue + ( + !engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("MakeDir.Comment"), dir) + ) + ); + } + finally + { + Directory.Delete(dir); + } + } + + /* + * Method: FileAlreadyExists + * + * Make sure that nice message is logged if a file already exists with that name. + */ + [TestMethod] + public void FileAlreadyExists() + { + string temp = Path.GetTempPath(); + string file = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A38d"); + + try + { + FileStream fs = File.Create(file); + fs.Close(); //we're gonna try to delete it + + MakeDir t = new MakeDir(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Directories = new ITaskItem[] + { + new TaskItem(file) + }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(0, t.DirectoriesCreated.Length); + Assert.IsTrue(engine.Log.Contains("MSB3191")); + Assert.IsTrue(engine.Log.Contains(file)); + } + finally + { + File.Delete(file); + } + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/ManagedCompiler_Tests.cs b/src/XMakeTasks/UnitTests/ManagedCompiler_Tests.cs new file mode 100644 index 00000000000..a87225270a4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ManagedCompiler_Tests.cs @@ -0,0 +1,360 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ManagedCompiler_Tests + { + #region Test DebugType and EmitDebugInformation settings + // We are testing to verify the following functionality + // Debug Symbols DebugType Desired Resilts + // True Full /debug+ /debug:full + // True PdbOnly /debug+ /debug:PdbOnly + // True None /debug- + // True Blank /debug+ + // False Full /debug- /debug:full + // False PdbOnly /debug- /debug:PdbOnly + // False None /debug- + // False Blank /debug- + // Blank Full /debug:full + // Blank PdbOnly /debug:PdbOnly + // Blank None /debug- + // Debug: Blank Blank /debug+ //Microsof.common.targets will set DebugSymbols to true + // Release: Blank Blank "Nothing for either switch" + + /// + /// Verify the test matrix for DebugSymbols = true + /// + [TestMethod] + public void TestDebugSymbolsTrue() + { + // Verify each of the DebugType settings when EmitDebugInformation is true + MyManagedCompiler m = new MyManagedCompiler(); + m.DebugType = "Full"; + m.EmitDebugInformation = true; + m.AddResponseFileCommands(); + // We expect to see only /debug+ on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == true, "Expected to find /debug+ on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == false, "Not expected to find /debug- on the commandline"); + // Expect to only find Full on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:Full") == true, "Expected to find /debug:Full on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:PdbOnly") == false, "Not expected to find /debug:PdbOnly on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = "PdbOnly"; + m.EmitDebugInformation = true; + m.AddResponseFileCommands(); + // We expect to see only /debug+ on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == true, "Expected to find /debug+ on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == false, "Not expected to find /debug- on the commandline"); + // Expect to find only PdbOnly on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:PdbOnly") == true, "Expected to find /debug:PdbOnly on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:Full") == false, "Not expected to find /debug:Full on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = "none"; + m.EmitDebugInformation = true; + m.AddResponseFileCommands(); + // We expect to see /debug- on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == true, "Expected to find /debug- on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + // We do not expect to see any /debug: on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:") == false, "Not expected to find /debug: on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = null; + m.EmitDebugInformation = true; + m.AddResponseFileCommands(); + // We expect to see only /debug+ on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == true, "Expected to find /debug+ on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == false, "Not expected to find /debug- on the commandline"); + // We expect to not find any /debug: on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:") == false, "Not expected to find /debug: on the commandline"); + } + + /// + /// Verify the test matrix for DebugSymbols = false + /// + [TestMethod] + public void DebugSymbolsFalse() + { + // Verify each of the DebugType settings when EmitDebugInformation is false + MyManagedCompiler m = new MyManagedCompiler(); + m.DebugType = "Full"; + m.EmitDebugInformation = false; + m.AddResponseFileCommands(); + // We expect to see /debug- + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == true, "Expected to find /debug- on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + // We expect to find /debug:Full + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:Full") == true, "Expected to find /debug:Full on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:PdbOnly") == false, "Not expected to find /debug:PdbOnly on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = "PdbOnly"; + m.EmitDebugInformation = false; + m.AddResponseFileCommands(); + // We expect to see /debug- + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == true, "Expected to find /debug- on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + // We expect to find /debug:PdbOnly + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:PdbOnly") == true, "Expected to find /debug:PdbOnly on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:Full") == false, "Not expected to find /debug:Full on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = "none"; + m.EmitDebugInformation = false; + m.AddResponseFileCommands(); + // We expect to see /debug- on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == true, "Expected to find /debug- on the commandline"); + // We do not expect to see andy /debug: on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:") == false, "Not expected to find /debug: on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = null; + m.EmitDebugInformation = false; + m.AddResponseFileCommands(); + // We expect to see /debug- + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == true, "Expected to find /debug- on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + // We do not expect to find ANY /debug: on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:") == false, "Not expected to find /debug: on the commandline"); + } + + /// + /// Verify the test matrix for DebugSymbols when it is not set + /// + [TestMethod] + public void TestDebugSymbolsNull() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.DebugType = "Full"; + m.AddResponseFileCommands(); + // We expect to not see /debug + or - + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == false, "Not expected to find /debug- on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + // We expect to find /debug:Full + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:Full") == true, "Expected to find /debug:Full on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:PdbOnly") == false, "Not expected to find /debug:PdbOnly on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = "PdbOnly"; + m.AddResponseFileCommands(); + // We do not expect to see /debug + or - + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == false, "Not expected to find /debug- on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + // We expect to find /debug:PdbOnly + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:PdbOnly") == true, "Expected to find /debug:PdbOnly on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:Full") == false, "Not expected to find /debug:Full on the commandline"); + + m = new MyManagedCompiler(); + m.DebugType = "none"; + m.AddResponseFileCommands(); + // We expect to see /debug- on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == true, "Expected to find /debug- on the commandline"); + // We do not expect to see any /debug: on the commandline + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:") == false, "Not expected to find /debug: on the commandline"); + + // The cases where DebugType and DebugSymbols are Blank(not set) is a special case because in microsoft.common.targets + // when the configuration is "debug" and both DebugType and DebugSymbols are blank DebugSymbols will be set to True. + // In relase the DebugSymbols will remail blank + // Debug: Blank Blank /debug+ //Microsof.common.targets will set DebugSymbols to true. + // This makes the case equal to the testing of EmitDebugSymbols=true and DebugType=null which is done in TestDebugSymbolsTrue above. + // Release: Blank Blank "Nothing for either switch" + m = new MyManagedCompiler(); + m.DebugType = null; + m.AddResponseFileCommands(); + // We do not expect to find /debug+ or /debug- + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug+") == false, "Not expected to find /debug+ on the commandline"); + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug-") == false, "Not expected to find /debug- on the commandline"); + // We do not expect to find /debug: + Assert.IsTrue(m.VerifySwitchOnCommandLine("/debug:") == false, "Not expected to find /debug: on the commandline"); + } + + #endregion + + [TestMethod] + public void DuplicateSources() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("foo"), new TaskItem("foo") }; + Assert.IsTrue(!m.AccessValidateParameters()); + ((MockEngine)m.BuildEngine).AssertLogContains("MSB3105"); + } + + [TestMethod] + public void DuplicateResourcesWithNoLogicalNames() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + m.Resources = new ITaskItem[] { new TaskItem("foo.resources"), new TaskItem("foo.resources") }; + // This is an error + Assert.IsTrue(!m.AccessValidateParameters()); + ((MockEngine)m.BuildEngine).AssertLogContains("MSB3105"); + } + + [TestMethod] + public void DuplicateResourcesButWithDifferentLogicalNames() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + TaskItem resource1 = new TaskItem("foo.resources"); + resource1.SetMetadata("LogicalName", "value1"); + TaskItem resource2 = new TaskItem("foo.resources"); + resource2.SetMetadata("LogicalName", "value2"); + m.Resources = new ITaskItem[] { resource1, resource2 }; + // This is okay + Assert.IsTrue(m.AccessValidateParameters()); + ((MockEngine)m.BuildEngine).AssertLogDoesntContain("MSB3105"); + ((MockEngine)m.BuildEngine).AssertLogDoesntContain("MSB3083"); + } + + [TestMethod] + public void DefaultWin32ManifestEmbeddedInConsoleApp() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + m.TargetType = "EXE"; + + Assert.IsTrue + ( + m.AccessGetWin32ManifestSwitch(false, null).EndsWith("default.win32manifest", StringComparison.OrdinalIgnoreCase), + "default.win32manifest should be embedded in a console exe!" + ); + } + + [TestMethod] + public void DefaultWin32ManifestEmbeddedInConsoleAppWhenTargetTypeInferred() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + + Assert.IsTrue + ( + m.AccessGetWin32ManifestSwitch(false, null).EndsWith("default.win32manifest", StringComparison.OrdinalIgnoreCase), + "default.win32manifest should be embedded in a console exe!" + ); + } + + [TestMethod] + public void DefaultWin32ManifestNotEmbeddedInClassLibrary() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + m.TargetType = "LIBRary"; + + Assert.IsTrue + ( + String.IsNullOrEmpty(m.AccessGetWin32ManifestSwitch(false, null)), + "default.win32manifest should NOT be embedded in a class library!" + ); + } + + [TestMethod] + public void DefaultWin32ManifestNotEmbeddedInNetModule() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + m.TargetType = "modULE"; + + Assert.IsTrue + ( + String.IsNullOrEmpty(m.AccessGetWin32ManifestSwitch(false, null)), + "default.win32manifest should NOT be embedded in a net module!" + ); + } + + [TestMethod] + public void DuplicateResourcesWithSameLogicalNames() + { + MyManagedCompiler m = new MyManagedCompiler(); + m.BuildEngine = new MockEngine(true); + m.Sources = new ITaskItem[] { new TaskItem("bar") }; + TaskItem resource1 = new TaskItem("foo.resources"); + resource1.SetMetadata("LogicalName", "value1"); + TaskItem resource2 = new TaskItem("foo.resources"); + resource2.SetMetadata("LogicalName", "value1"); + m.Resources = new ITaskItem[] { resource1, resource2 }; + // This is an error + Assert.IsTrue(!m.AccessValidateParameters()); + ((MockEngine)m.BuildEngine).AssertLogContains("MSB3083"); + } + } + + /// + /// Class implementing ManagedCompiler so that its protected methods can + /// be accessed + /// + internal class MyManagedCompiler : ManagedCompiler + { + private MyCommandLineBuilderExtension _commandLineBuilder = new MyCommandLineBuilderExtension(); + protected override string ToolName { get { return String.Empty; } } + protected override string GenerateFullPathToTool() { return String.Empty; } + + public void AddResponseFileCommands() + { + base.AddResponseFileCommands(_commandLineBuilder); + } + + public bool VerifySwitchOnCommandLine(string switchToVerify) + { + return _commandLineBuilder.LogContains(switchToVerify); + } + + public bool AccessValidateParameters() + { + return base.ValidateParameters(); + } + + public string AccessGetWin32ManifestSwitch(bool noDefaultWin32Manifest, string win32Manifest) + { + return base.GetWin32ManifestSwitch(noDefaultWin32Manifest, win32Manifest); + } + } + + + /// + /// Class implementing CommandLineBuilderExtension so that its protected methods can + /// be accessed + /// + internal class MyCommandLineBuilderExtension : CommandLineBuilderExtension + { + internal bool LogContains(string contains) + { + if (this.CommandLine != null) + { + string commandLineUpperInvariant = this.CommandLine.ToString().ToUpperInvariant(); + return commandLineUpperInvariant.Contains(contains.ToUpperInvariant()); + } + else + { + if (contains == null) + { + return true; + } + else + { + return false; + } + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/ManifestWriter_Tests.cs b/src/XMakeTasks/UnitTests/ManifestWriter_Tests.cs new file mode 100644 index 00000000000..4cdaf72cda0 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ManifestWriter_Tests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// These functions are now guarded with a linkDemand and requires the caller to be signed with a +// ms pkt. The test harness does not appear to be signed. +#if never +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; + + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ManifestWriter_Tests + { + /// + /// Attempts to write an AssemblyManifest to a temporary file. + /// + [TestMethod] + public void BasicWriteAssemblyManifestToPath() + { + Manifest m = new AssemblyManifest(); + string file = FileUtilities.GetTemporaryFile(); + ManifestWriter.WriteManifest(m, file); + File.Delete(file); + } + } +} +#endif + + diff --git a/src/XMakeTasks/UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/XMakeTasks/UnitTests/Microsoft.Build.Tasks.UnitTests.csproj new file mode 100644 index 00000000000..01430ad64c2 --- /dev/null +++ b/src/XMakeTasks/UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -0,0 +1,176 @@ + + + + + Debug + AnyCPU + {32126DCE-7484-4E4B-85DA-12378C0F2FC7} + Library + Microsoft.Build.Tasks.UnitTests + Microsoft.Build.Tasks.UnitTests + + + + + + + + + true + + + + + + + + NativeMethodsShared_Tests.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + App.config + Designer + + + + + + + + + + + + + + + + {828566ee-6f6a-4ef4-98b0-513f7df9c628} + Microsoft.Build.Utilities + + + {16cd7635-7cf4-4c62-a77b-cf87d0f09a58} + Microsoft.Build + + + {59A73FE0-D3B7-4299-9063-3A587D429AF4} + Microsoft.Build.Tasks + + + + + + {571F09DB-A81A-4444-945C-6F7B530054CD} + Microsoft.Build.Framework + + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/MockCscHostObject.cs b/src/XMakeTasks/UnitTests/MockCscHostObject.cs new file mode 100644 index 00000000000..ebfd0fe19ea --- /dev/null +++ b/src/XMakeTasks/UnitTests/MockCscHostObject.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Build.Tasks; +using Microsoft.Build.Tasks.Hosting; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.UnitTests +{ + internal class MockCscHostObject : ICscHostObject + { + private bool _compileMethodWasCalled = false; + private bool _designTime = false; + + /// + /// Construct + /// + public MockCscHostObject() + { + } + + /// + /// Allows unit tests to control whether or not the host object is running + /// in "design time". + /// + /// + public void SetDesignTime(bool designTime) + { + _designTime = designTime; + } + + /// + /// Returns true if the "Compile()" method has ever been called on this + /// instance of this object, false otherwise. + /// + /// + public bool CompileMethodWasCalled + { + get { return _compileMethodWasCalled; } + } + + /// + /// + /// + /// + bool ICscHostObject.IsDesignTime() + { + return _designTime; + } + + /// + /// This is a method from the ICscHostObject interface. This will get + /// called during the build as the Vbc task is executing. + /// + /// + bool ICscHostObject.Compile() + { + _compileMethodWasCalled = true; + return true; + } + + void ICscHostObject.BeginInitialization() { return; } + + bool ICscHostObject.SetAdditionalLibPaths(string[] additionalLibPaths) { return true; } + bool ICscHostObject.SetAddModules(string[] addModules) { return true; } + bool ICscHostObject.SetCodePage(int codePage) { return true; } + bool ICscHostObject.SetDefineConstants(string defineConstants) { return true; } + bool ICscHostObject.SetDisabledWarnings(string disabledWarnings) { return true; } + bool ICscHostObject.SetDocumentationFile(string documentationFile) { return true; } + bool ICscHostObject.SetErrorReport(string errorReport) { return true; } + bool ICscHostObject.SetFileAlignment(int fileAlignment) { return true; } + bool ICscHostObject.SetKeyContainer(string keyContainer) { return true; } + bool ICscHostObject.SetKeyFile(string keyFile) { return true; } + bool ICscHostObject.SetLinkResources(ITaskItem[] linkResources) { return true; } + bool ICscHostObject.SetModuleAssemblyName(string moduleAssemblyName) { return true; } + bool ICscHostObject.SetNoConfig(bool noConfig) { return true; } + bool ICscHostObject.SetNoStandardLib(bool noStandardLib) { return true; } + bool ICscHostObject.SetOptimize(bool optimize) { return true; } + bool ICscHostObject.SetOutputAssembly(string outputAssembly) { return true; } + bool ICscHostObject.SetPdbFile(string pdbFile) { return true; } + bool ICscHostObject.SetPlatform(string platform) { return true; } + bool ICscHostObject.SetReferences(ITaskItem[] references) { return true; } + bool ICscHostObject.SetResources(ITaskItem[] resources) { return true; } + bool ICscHostObject.SetResponseFiles(ITaskItem[] responseFiles) { return true; } + bool ICscHostObject.SetSources(ITaskItem[] sources) { return true; } + bool ICscHostObject.SetTargetType(string targetType) { return true; } + bool ICscHostObject.SetTreatWarningsAsErrors(bool treatWarningsAsErrors) { return true; } + bool ICscHostObject.SetWarningsAsErrors(string warningsAsErrors) { return true; } + bool ICscHostObject.SetWarningsNotAsErrors(string warningsNotAsErrors) { return true; } + bool ICscHostObject.SetWin32Icon(string win32Icon) { return true; } + bool ICscHostObject.SetWin32Resource(string win32Resource) { return true; } + + + bool ICscHostObject.EndInitialization(out string a, out int b) { a = null; b = 0; return true; } + bool ICscHostObject.SetAllowUnsafeBlocks(bool a) { return true; } + bool ICscHostObject.SetBaseAddress(string a) { return true; } + bool ICscHostObject.SetCheckForOverflowUnderflow(bool a) { return true; } + bool ICscHostObject.SetDebugType(string a) { return true; } + bool ICscHostObject.SetDelaySign(bool a, bool b) { return true; } + bool ICscHostObject.SetEmitDebugInformation(bool a) { return true; } + bool ICscHostObject.SetGenerateFullPaths(bool a) { return true; } + bool ICscHostObject.SetLangVersion(string a) { return true; } + bool ICscHostObject.SetMainEntryPoint(string a, string b) { return true; } + bool ICscHostObject.SetWarningLevel(int a) { return true; } + + bool ICscHostObject.IsUpToDate() { return false; } + } + + internal class MockCscHostObject2 : MockCscHostObject, ICscHostObject2 + { + bool ICscHostObject2.SetWin32Manifest(string win32Manifest) { return true; } + } + + internal class MockCscHostObject3 : MockCscHostObject2, ICscHostObject3 + { + bool ICscHostObject3.SetApplicationConfiguration(string applicationConfiguration) { return true; } + } + + internal class MockCscHostObject4 : MockCscHostObject3, ICscHostObject4 + { + bool ICscHostObject4.SetPlatformWith32BitPreference(string platformWith32BitPreference) { return true; } + bool ICscHostObject4.SetHighEntropyVA(bool highEntropyVA) { return true; } + bool ICscHostObject4.SetSubsystemVersion(string subsystemVersion) { return true; } + } + + internal class MockCscAnalyzerHostObject : MockCscHostObject4, IAnalyzerHostObject + { + public ITaskItem[] Analyzers { get; private set; } + public string RuleSet { get; private set; } + public ITaskItem[] AdditionalFiles { get; private set; } + + bool IAnalyzerHostObject.SetAnalyzers(ITaskItem[] analyzers) + { + this.Analyzers = analyzers; + return true; + } + + bool IAnalyzerHostObject.SetRuleSet(string ruleSetFile) + { + this.RuleSet = ruleSetFile; + return true; + } + + bool IAnalyzerHostObject.SetAdditionalFiles(ITaskItem[] additionalFiles) + { + this.AdditionalFiles = additionalFiles; + return true; + } + } +} diff --git a/src/XMakeTasks/UnitTests/MockFaultInjectionHelper.cs b/src/XMakeTasks/UnitTests/MockFaultInjectionHelper.cs new file mode 100644 index 00000000000..c4b532cb80b --- /dev/null +++ b/src/XMakeTasks/UnitTests/MockFaultInjectionHelper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Build.UnitTests +{ + public class MockFaultInjectionHelper + where FailurePointEnum : IComparable + { + private FailurePointEnum _failureToInject; + private Exception _exceptionToThrow; + + public MockFaultInjectionHelper() + { + } + + public void InjectFailure(FailurePointEnum failureToInject, Exception exceptionToThrow) + { + _failureToInject = failureToInject; + _exceptionToThrow = exceptionToThrow; + } + + public void FailurePointThrow(FailurePointEnum failurePointId) + { + if (_failureToInject.CompareTo(failurePointId) == 0) + { + throw _exceptionToThrow; + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/MockTypeInfo.cs b/src/XMakeTasks/UnitTests/MockTypeInfo.cs new file mode 100644 index 00000000000..7cfefb958f3 --- /dev/null +++ b/src/XMakeTasks/UnitTests/MockTypeInfo.cs @@ -0,0 +1,499 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices.ComTypes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Marshal = System.Runtime.InteropServices.Marshal; +using VarEnum = System.Runtime.InteropServices.VarEnum; +using IFixedTypeInfo = Microsoft.Build.Tasks.IFixedTypeInfo; + +namespace Microsoft.Build.UnitTests +{ + /// + /// A generic interface for creating composite type infos - e.g. a safe array of pointers to something + /// + public interface ICompositeTypeInfo + { + TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper); + + MockTypeInfo GetFinalTypeInfo(); + } + + /// + /// Safe array composite type info + /// + public class ArrayCompositeTypeInfo : ICompositeTypeInfo + { + private ICompositeTypeInfo _baseElementType; + + public ArrayCompositeTypeInfo(ICompositeTypeInfo baseElement) + { + _baseElementType = baseElement; + } + + #region ICreateTypeDesc Members + + public TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper) + { + TYPEDESC typeDesc; + typeDesc.vt = (short)VarEnum.VT_SAFEARRAY; + typeDesc.lpValue = memoryHelper.AllocateHandle(Marshal.SizeOf(typeof(TYPEDESC))); + Marshal.StructureToPtr(_baseElementType.CreateTypeDesc(finalTypeHRef, memoryHelper), typeDesc.lpValue, false); + return typeDesc; + } + + /// + /// Defer to the base element to get the final type info - this will eventually terminate at a MockTypeInfo node + /// which returns itself + /// + /// + public MockTypeInfo GetFinalTypeInfo() + { + return _baseElementType.GetFinalTypeInfo(); + } + + #endregion + } + + /// + /// Pointer composite type info + /// + public class PtrCompositeTypeInfo : ICompositeTypeInfo + { + private ICompositeTypeInfo _baseElementType; + + public PtrCompositeTypeInfo(ICompositeTypeInfo baseElement) + { + _baseElementType = baseElement; + } + + #region ICompositeTypeInfo Members + + public TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper) + { + TYPEDESC typeDesc; + typeDesc.vt = (short)VarEnum.VT_PTR; + typeDesc.lpValue = memoryHelper.AllocateHandle(Marshal.SizeOf(typeof(TYPEDESC))); + Marshal.StructureToPtr(_baseElementType.CreateTypeDesc(finalTypeHRef, memoryHelper), typeDesc.lpValue, false); + return typeDesc; + } + + public MockTypeInfo GetFinalTypeInfo() + { + return _baseElementType.GetFinalTypeInfo(); + } + + #endregion + } + + /// + /// All the information necessary to describe a single function signature + /// + public struct FuncInfo + { + public FuncInfo(ICompositeTypeInfo[] parameters, ICompositeTypeInfo returnType) + { + this.parameters = parameters; + this.returnType = returnType; + } + + public ICompositeTypeInfo[] parameters; + public ICompositeTypeInfo returnType; + } + + /// + /// Mock class for the ITypeInfo interface + /// + public class MockTypeInfo : ITypeInfo, ICompositeTypeInfo, IFixedTypeInfo + { + static private int s_HREF_IMPLTYPES_OFFSET = 1000; + static private int s_HREF_VARS_OFFSET = 2000; + static private int s_HREF_FUNCSRET_OFFSET = 3000; + static private int s_HREF_FUNCSPARAM_OFFSET = 4000; + static private int s_HREF_FUNCSPARAM_OFFSET_PERFUNC = 100; + static private int s_HREF_RANGE = 999; + + private MockTypeLib _containingTypeLib; + + public MockTypeLib ContainingTypeLib + { + set + { + _containingTypeLib = value; + } + } + + private int _indexInContainingTypeLib; + + public int IndexInContainingTypeLib + { + set + { + _indexInContainingTypeLib = value; + } + } + + private string _typeName; + + public string TypeName + { + set + { + _typeName = value; + } + } + + private TYPEATTR _typeAttributes; + + private List _implementedTypes; + private List _definedVariables; + private List _definedFunctions; + + private MockUnmanagedMemoryHelper _memoryHelper; + + private MockFaultInjectionHelper _faultInjector; + + /// + /// Default constructor + /// + public MockTypeInfo() + { + _implementedTypes = new List(); + _definedVariables = new List(); + _definedFunctions = new List(); + + _memoryHelper = new MockUnmanagedMemoryHelper(); + + // each type has a unique guid + _typeAttributes.guid = Guid.NewGuid(); + + // default typekind value is TKIND_ENUM so just pick something else that doesn't have a special meaning in the code + // (we skip enum type infos) + _typeAttributes.typekind = TYPEKIND.TKIND_INTERFACE; + } + + /// + /// Use a known guid for creating this type info + /// + /// + public MockTypeInfo(Guid guid) + : this() + { + _typeAttributes.guid = guid; + } + + /// + /// Use a custom type kind + /// + /// + public MockTypeInfo(TYPEKIND typeKind) + : this() + { + _typeAttributes.typekind = typeKind; + } + + /// + /// Adds implementedType to the list of this type's implemented interfaces + /// + /// + public void ImplementsInterface(MockTypeInfo implementedType) + { + _typeAttributes.cImplTypes++; + _implementedTypes.Add(implementedType); + } + + /// + /// Adds implementedType to the list of this type's defined variables + /// + /// + public void DefinesVariable(ICompositeTypeInfo variableType) + { + _typeAttributes.cVars++; + _definedVariables.Add(variableType); + } + + /// + /// Adds a new function signature to the list of this type's implemented functions + /// + /// + public void DefinesFunction(MockTypeInfo[] parameters, MockTypeInfo returnType) + { + _typeAttributes.cFuncs++; + _definedFunctions.Add(new FuncInfo(parameters, returnType)); + } + + /// + /// Sets the fault injection object for this type + /// + /// + public void SetFaultInjector(MockFaultInjectionHelper faultInjector) + { + _faultInjector = faultInjector; + } + + /// + /// Helper method for verifying there are no memory leaks + /// + public void AssertAllHandlesReleased() + { + _memoryHelper.AssertAllHandlesReleased(); + } + + #region IFixedTypeInfo members + + void IFixedTypeInfo.GetRefTypeOfImplType(int index, out System.IntPtr href) + { + Assert.IsTrue(index >= 0 && index < _typeAttributes.cImplTypes); + + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeOfImplType); + + href = ((System.IntPtr)index + s_HREF_IMPLTYPES_OFFSET); + } + + void IFixedTypeInfo.GetRefTypeInfo(System.IntPtr hRef, out IFixedTypeInfo ppTI) + { + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeInfo); + int hRefInt = (int)hRef; + + if (hRefInt >= s_HREF_IMPLTYPES_OFFSET && hRefInt <= s_HREF_IMPLTYPES_OFFSET + s_HREF_RANGE) + { + ppTI = _implementedTypes[hRefInt - s_HREF_IMPLTYPES_OFFSET]; + } + else if (hRefInt >= s_HREF_VARS_OFFSET && hRefInt <= s_HREF_VARS_OFFSET + s_HREF_RANGE) + { + ppTI = _definedVariables[hRefInt - s_HREF_VARS_OFFSET].GetFinalTypeInfo(); + } + else if (hRefInt >= s_HREF_FUNCSRET_OFFSET && hRefInt <= s_HREF_FUNCSRET_OFFSET + s_HREF_RANGE) + { + ppTI = _definedFunctions[hRefInt - s_HREF_FUNCSRET_OFFSET].returnType.GetFinalTypeInfo(); + } + else if (hRefInt >= s_HREF_FUNCSPARAM_OFFSET && hRefInt <= s_HREF_FUNCSPARAM_OFFSET + s_HREF_RANGE) + { + ppTI = _definedFunctions[(hRefInt - s_HREF_FUNCSPARAM_OFFSET) / s_HREF_FUNCSPARAM_OFFSET_PERFUNC].parameters[(hRefInt - s_HREF_FUNCSPARAM_OFFSET) % s_HREF_FUNCSPARAM_OFFSET_PERFUNC].GetFinalTypeInfo(); + } + else + { + ppTI = null; + Assert.Fail("unexpected hRef value"); + } + } + + #endregion + + #region Implemented ITypeInfo members + + public void GetContainingTypeLib(out ITypeLib ppTLB, out int pIndex) + { + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetContainingTypeLib); + + ppTLB = _containingTypeLib; + pIndex = _indexInContainingTypeLib; + } + + public void GetTypeAttr(out IntPtr ppTypeAttr) + { + // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method + // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak. + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetTypeAttr); + + ppTypeAttr = _memoryHelper.AllocateHandle(Marshal.SizeOf(typeof(TYPEATTR))); + Marshal.StructureToPtr(_typeAttributes, ppTypeAttr, false); + } + + public void ReleaseTypeAttr(IntPtr pTypeAttr) + { + _memoryHelper.FreeHandle(pTypeAttr); + + // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method + // there's really nothing we can do to avoid leaking stuff + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_ReleaseTypeAttr); + } + + public void GetRefTypeOfImplType(int index, out int href) + { + Assert.IsTrue(index >= 0 && index < _typeAttributes.cImplTypes); + + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeOfImplType); + + href = index + s_HREF_IMPLTYPES_OFFSET; + } + + public void GetRefTypeInfo(int hRef, out ITypeInfo ppTI) + { + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeInfo); + + if (hRef >= s_HREF_IMPLTYPES_OFFSET && hRef <= s_HREF_IMPLTYPES_OFFSET + s_HREF_RANGE) + { + ppTI = _implementedTypes[hRef - s_HREF_IMPLTYPES_OFFSET]; + } + else if (hRef >= s_HREF_VARS_OFFSET && hRef <= s_HREF_VARS_OFFSET + s_HREF_RANGE) + { + ppTI = _definedVariables[hRef - s_HREF_VARS_OFFSET].GetFinalTypeInfo(); + } + else if (hRef >= s_HREF_FUNCSRET_OFFSET && hRef <= s_HREF_FUNCSRET_OFFSET + s_HREF_RANGE) + { + ppTI = _definedFunctions[hRef - s_HREF_FUNCSRET_OFFSET].returnType.GetFinalTypeInfo(); + } + else if (hRef >= s_HREF_FUNCSPARAM_OFFSET && hRef <= s_HREF_FUNCSPARAM_OFFSET + s_HREF_RANGE) + { + ppTI = _definedFunctions[(hRef - s_HREF_FUNCSPARAM_OFFSET) / s_HREF_FUNCSPARAM_OFFSET_PERFUNC].parameters[(hRef - s_HREF_FUNCSPARAM_OFFSET) % s_HREF_FUNCSPARAM_OFFSET_PERFUNC].GetFinalTypeInfo(); + } + else + { + ppTI = null; + Assert.Fail("unexpected hRef value"); + } + } + + public void GetVarDesc(int index, out IntPtr ppVarDesc) + { + // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method + // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak. + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetVarDesc); + + ppVarDesc = _memoryHelper.AllocateHandle(Marshal.SizeOf(typeof(VARDESC))); + + _memoryHelper.EnterSubAllocationScope(ppVarDesc); + VARDESC varDesc = new VARDESC(); + varDesc.elemdescVar.tdesc = _definedVariables[index].CreateTypeDesc(new IntPtr(index + s_HREF_VARS_OFFSET), _memoryHelper); + _memoryHelper.ExitSubAllocationScope(); + + Marshal.StructureToPtr(varDesc, ppVarDesc, false); + } + + public void ReleaseVarDesc(IntPtr pVarDesc) + { + _memoryHelper.FreeHandle(pVarDesc); + + // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method + // there's really nothing we can do to avoid leaking stuff + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_ReleaseVarDesc); + } + + public void GetFuncDesc(int index, out IntPtr ppFuncDesc) + { + // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method + // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak. + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetFuncDesc); + + ppFuncDesc = _memoryHelper.AllocateHandle(Marshal.SizeOf(typeof(FUNCDESC))); + + _memoryHelper.EnterSubAllocationScope(ppFuncDesc); + FUNCDESC funcDesc = new FUNCDESC(); + + funcDesc.lprgelemdescParam = _memoryHelper.AllocateHandle(_definedFunctions[index].parameters.Length * Marshal.SizeOf(typeof(ELEMDESC))); + funcDesc.cParams = (short)_definedFunctions[index].parameters.Length; + + for (int i = 0; i < _definedFunctions[index].parameters.Length; i++) + { + ELEMDESC elemDesc = new ELEMDESC(); + elemDesc.tdesc = _definedFunctions[index].parameters[i].CreateTypeDesc( + new IntPtr(index * s_HREF_FUNCSPARAM_OFFSET_PERFUNC + i + s_HREF_FUNCSPARAM_OFFSET), _memoryHelper); + + Marshal.StructureToPtr( + elemDesc, + new IntPtr(funcDesc.lprgelemdescParam.ToInt64() + i * Marshal.SizeOf(typeof(ELEMDESC))), + false); + } + + funcDesc.elemdescFunc.tdesc = _definedFunctions[index].returnType.CreateTypeDesc( + new IntPtr(index + s_HREF_FUNCSRET_OFFSET), _memoryHelper); + _memoryHelper.ExitSubAllocationScope(); + + Marshal.StructureToPtr(funcDesc, ppFuncDesc, false); + } + + public void ReleaseFuncDesc(IntPtr pFuncDesc) + { + _memoryHelper.FreeHandle(pFuncDesc); + + // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method + // there's really nothing we can do to avoid leaking stuff + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_ReleaseFuncDesc); + } + + public void GetDocumentation(int index, out string strName, out string strDocString, out int dwHelpContext, out string strHelpFile) + { + Assert.AreEqual(-1, index); + + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetDocumentation); + + strName = _typeName; + strDocString = "garbage"; + dwHelpContext = -1; + strHelpFile = "garbage^2"; + } + + #endregion + + #region ICreateTypeDesc Members + + public TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper) + { + TYPEDESC typeDesc; + typeDesc.vt = (short)VarEnum.VT_USERDEFINED; + typeDesc.lpValue = finalTypeHRef; + return typeDesc; + } + + public MockTypeInfo GetFinalTypeInfo() + { + return this; + } + + #endregion + + #region Stubbed ITypeInfo members + + public void AddressOfMember(int memid, INVOKEKIND invKind, out IntPtr ppv) + { + throw new Exception("The method or operation is not implemented."); + } + + public void CreateInstance(object pUnkOuter, ref Guid riid, out object ppvObj) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetDllEntry(int memid, INVOKEKIND invKind, IntPtr pBstrDllName, IntPtr pBstrName, IntPtr pwOrdinal) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetIDsOfNames(string[] rgszNames, int cNames, int[] pMemId) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetImplTypeFlags(int index, out IMPLTYPEFLAGS pImplTypeFlags) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetMops(int memid, out string pBstrMops) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetNames(int memid, string[] rgBstrNames, int cMaxNames, out int pcNames) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetTypeComp(out ITypeComp ppTComp) + { + throw new Exception("The method or operation is not implemented."); + } + + public void Invoke(object pvInstance, int memid, short wFlags, ref DISPPARAMS pDispParams, IntPtr pVarResult, IntPtr pExcepInfo, out int puArgErr) + { + throw new Exception("The method or operation is not implemented."); + } + + #endregion + } +} diff --git a/src/XMakeTasks/UnitTests/MockTypeLib.cs b/src/XMakeTasks/UnitTests/MockTypeLib.cs new file mode 100644 index 00000000000..05c79d37ca0 --- /dev/null +++ b/src/XMakeTasks/UnitTests/MockTypeLib.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices.ComTypes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Tasks; + +using Marshal = System.Runtime.InteropServices.Marshal; + +namespace Microsoft.Build.UnitTests +{ + /// + /// All possible failure points in MockTypeLib/MockTypeInfo classes. When adding new functionality there, + /// add a new failure point here and add a corresponding call to faultInjector.FailurePointThrow in the method. + /// + public enum MockTypeLibrariesFailurePoints + { + // MockTypeLibrary.ITypeLib + ITypeLib_GetLibAttr = 1, + ITypeLib_ReleaseTLibAttr, + ITypeLib_GetTypeInfo, + ITypeLib_GetTypeInfoCount, + + // MockTypeLibrary.ITypeLib2 + ITypeLib2_GetCustData, + + // MockTypeInfo.ITypeInfo + ITypeInfo_GetContainingTypeLib, + ITypeInfo_GetTypeAttr, + ITypeInfo_ReleaseTypeAttr, + ITypeInfo_GetRefTypeOfImplType, + ITypeInfo_GetRefTypeInfo, + ITypeInfo_GetVarDesc, + ITypeInfo_ReleaseVarDesc, + ITypeInfo_GetFuncDesc, + ITypeInfo_ReleaseFuncDesc, + ITypeInfo_GetDocumentation + } + + /// + /// Mock class for the ITypeLib interface + /// + public class MockTypeLib : ITypeLib, ITypeLib2 + { + private List _containedTypeInfos; + + public List ContainedTypeInfos + { + get + { + return _containedTypeInfos; + } + } + + private TYPELIBATTR _typeLibAttributes; + + public TYPELIBATTR Attributes + { + get + { + return _typeLibAttributes; + } + } + + private string _exportedFromComPlus; + + public string ExportedFromComPlus + { + set + { + _exportedFromComPlus = value; + } + } + + // helper class for unmanaged allocations and leak tracking + private MockUnmanagedMemoryHelper _memoryHelper; + + // helper class for injecting failures into chosen method calls + private MockFaultInjectionHelper _faultInjector; + + /// + /// Public constructor + /// + public MockTypeLib() + { + _containedTypeInfos = new List(); + _typeLibAttributes.guid = Guid.NewGuid(); + _exportedFromComPlus = null; + + _memoryHelper = new MockUnmanagedMemoryHelper(); + _faultInjector = new MockFaultInjectionHelper(); + } + + /// + /// Create a mock type library with a specific guid + /// + /// + public MockTypeLib(Guid guid) + : this() + { + _typeLibAttributes.guid = guid; + } + + /// + /// Tells the type lib to inject a specific failure (exception) at the chosen failure point. + /// + /// + /// + public void InjectFailure(MockTypeLibrariesFailurePoints failurePoint, Exception exceptionToThrow) + { + _faultInjector.InjectFailure(failurePoint, exceptionToThrow); + } + + /// + /// Add a new type info to the type library + /// + /// + public void AddTypeInfo(MockTypeInfo typeInfo) + { + _containedTypeInfos.Add(typeInfo); + typeInfo.ContainingTypeLib = this; + typeInfo.IndexInContainingTypeLib = _containedTypeInfos.Count - 1; + typeInfo.SetFaultInjector(_faultInjector); + } + + /// + /// Helper method for verifying there are no memory leaks from unmanaged allocations + /// + public void AssertAllHandlesReleased() + { + _memoryHelper.AssertAllHandlesReleased(); + + foreach (MockTypeInfo typeInfo in _containedTypeInfos) + { + typeInfo.AssertAllHandlesReleased(); + } + } + + #region Implemented ITypeLib members + + public void GetLibAttr(out IntPtr ppTLibAttr) + { + // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method + // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak. + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeLib_GetLibAttr); + + ppTLibAttr = _memoryHelper.AllocateHandle(Marshal.SizeOf(typeof(TYPELIBATTR))); + Marshal.StructureToPtr(this.Attributes, ppTLibAttr, false); + } + + public void ReleaseTLibAttr(IntPtr pTLibAttr) + { + _memoryHelper.FreeHandle(pTLibAttr); + + // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method + // there's really nothing we can do to avoid leaking stuff + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeLib_ReleaseTLibAttr); + } + + public void GetTypeInfo(int index, out ITypeInfo ppTI) + { + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeLib_GetTypeInfo); + + Assert.IsTrue(index >= 0 && index < _containedTypeInfos.Count); + ppTI = _containedTypeInfos[index]; + } + + public int GetTypeInfoCount() + { + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeLib_GetTypeInfoCount); + + return _containedTypeInfos.Count; + } + + #endregion + + #region Stubbed ITypeLib members + + public void FindName(string szNameBuf, int lHashVal, ITypeInfo[] ppTInfo, int[] rgMemId, ref short pcFound) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetDocumentation(int index, out string strName, out string strDocString, out int dwHelpContext, out string strHelpFile) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetTypeComp(out ITypeComp ppTComp) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetTypeInfoOfGuid(ref Guid guid, out ITypeInfo ppTInfo) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetTypeInfoType(int index, out TYPEKIND pTKind) + { + throw new Exception("The method or operation is not implemented."); + } + + public bool IsName(string szNameBuf, int lHashVal) + { + throw new Exception("The method or operation is not implemented."); + } + + #endregion + + #region Implemented ITypeLib2 members + + public void GetCustData(ref Guid guid, out object pVarVal) + { + _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeLib2_GetCustData); + + if (guid == NativeMethods.GUID_ExportedFromComPlus) + { + pVarVal = _exportedFromComPlus; + } + else + { + Assert.Fail("unexpected guid in ITypeLib2.GetCustData"); + pVarVal = null; + } + } + + #endregion + + #region Stubbed ITypeLib2 Members + + public void GetAllCustData(IntPtr pCustData) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetDocumentation2(int index, out string pbstrHelpString, out int pdwHelpStringContext, out string pbstrHelpStringDll) + { + throw new Exception("The method or operation is not implemented."); + } + + public void GetLibStatistics(IntPtr pcUniqueNames, out int pcchUniqueNames) + { + throw new Exception("The method or operation is not implemented."); + } + + #endregion + } +} diff --git a/src/XMakeTasks/UnitTests/MockUnmanagedMemoryHelper.cs b/src/XMakeTasks/UnitTests/MockUnmanagedMemoryHelper.cs new file mode 100644 index 00000000000..e4e73e475d7 --- /dev/null +++ b/src/XMakeTasks/UnitTests/MockUnmanagedMemoryHelper.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices.ComTypes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Marshal = System.Runtime.InteropServices.Marshal; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Helper class for managing native allocations and tracking possible memory leaks. + /// + public class MockUnmanagedMemoryHelper + { + private List _allocatedHandles; + + // Zero if we're allocating independent chunks of memory; + // Something else if we're allocating connected chunks of memory that we'll want to release with one ReleaseHandle + private IntPtr _mainAllocationHandle = IntPtr.Zero; + + // List of linked allocations that we want to release when releasing the IntPtr + private Dictionary> _dependentAllocations = new Dictionary>(); + + /// + /// Public constructor + /// + public MockUnmanagedMemoryHelper() + { + _allocatedHandles = new List(); + } + + /// + /// Allocate a native handle for a buffer of cb bytes + /// + /// + /// + public IntPtr AllocateHandle(int cb) + { + IntPtr handle = Marshal.AllocHGlobal(cb); + + // If this is a dependent allocation, add it to the list of dependencies + if (_mainAllocationHandle != IntPtr.Zero) + { + if (!_dependentAllocations.ContainsKey(_mainAllocationHandle)) + { + _dependentAllocations.Add(_mainAllocationHandle, new List()); + } + + _dependentAllocations[_mainAllocationHandle].Add(handle); + } + else + { + _allocatedHandles.Add(handle); + } + + return handle; + } + + /// + /// Release a handle and its dependent handles if any + /// + /// + public void FreeHandle(IntPtr handle) + { + Assert.IsTrue(_allocatedHandles.Exists(new Predicate( + delegate (IntPtr ptr) { return ptr == handle; } + ))); + Marshal.FreeHGlobal(handle); + _allocatedHandles.Remove(handle); + + // Any dependencies? Free them as well + if (_dependentAllocations.ContainsKey(handle)) + { + while (_dependentAllocations[handle].Count > 0) + { + Marshal.FreeHGlobal(_dependentAllocations[handle][0]); + _dependentAllocations[handle].RemoveAt(0); + } + } + } + + /// + /// Tells us that we're going to allocate dependent handles for the given handle + /// + /// + public void EnterSubAllocationScope(IntPtr mainAllocation) + { + Assert.AreEqual(IntPtr.Zero, _mainAllocationHandle); + + _mainAllocationHandle = mainAllocation; + } + + /// + /// Tells us we're no longer allocating dependent handles. + /// + public void ExitSubAllocationScope() + { + Assert.IsTrue(IntPtr.Zero != _mainAllocationHandle); + + _mainAllocationHandle = IntPtr.Zero; + } + + /// + /// Helper method for making sure this object has no unreleased memory handles + /// + public void AssertAllHandlesReleased() + { + Assert.AreEqual(0, _allocatedHandles.Count); + } + } +} diff --git a/src/XMakeTasks/UnitTests/MockVbcHostObject.cs b/src/XMakeTasks/UnitTests/MockVbcHostObject.cs new file mode 100644 index 00000000000..5cdaeff5855 --- /dev/null +++ b/src/XMakeTasks/UnitTests/MockVbcHostObject.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Build.Tasks; +using Microsoft.Build.Tasks.Hosting; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.UnitTests +{ + internal class MockVbcHostObject : IVbcHostObject + { + private bool _compileMethodWasCalled = false; + private bool _designTime = false; + + /*********************************************************************** + * Constructor: MockVbcHostObject + * + **********************************************************************/ + public MockVbcHostObject() + { + } + + /*********************************************************************** + * Method: MockVbcHostObject.SetDesignTime + * + * Allows unit tests to control whether or not the host object is running + * in "design time". + **********************************************************************/ + public void SetDesignTime(bool designTime) + { + _designTime = designTime; + } + + /*********************************************************************** + * Method: MockVbcHostObject.CompileMethodWasCalled + * + * Returns true if the "Compile()" method has ever been called on this + * instance of this object, false otherwise. + * + **********************************************************************/ + public bool CompileMethodWasCalled + { + get { return _compileMethodWasCalled; } + set { _compileMethodWasCalled = value; } + } + + /*********************************************************************** + * Method: MockVbcHostObject.IsDesignTime + * + **********************************************************************/ + bool IVbcHostObject.IsDesignTime() + { + return _designTime; + } + + /*********************************************************************** + * Method: MockVbcHostObject.Compile + * + * This is a method from the IVbcHostObject interface. This will get + * called during the build as the Vbc task is executing. + * + **********************************************************************/ + bool IVbcHostObject.Compile() + { + _compileMethodWasCalled = true; + return true; + } + + void IVbcHostObject.BeginInitialization() { return; } + void IVbcHostObject.EndInitialization() { return; } + + bool IVbcHostObject.SetAdditionalLibPaths(string[] additionalLibPaths) { return true; } + bool IVbcHostObject.SetAddModules(string[] addModules) { return true; } + bool IVbcHostObject.SetBaseAddress(string targetType, string baseAddress) { return true; } + bool IVbcHostObject.SetCodePage(int codePage) { return true; } + bool IVbcHostObject.SetDebugType(bool emitDebugInformation, string debugType) { return true; } + bool IVbcHostObject.SetDefineConstants(string defineConstants) { return true; } + bool IVbcHostObject.SetDelaySign(bool delaySign) { return true; } + bool IVbcHostObject.SetDisabledWarnings(string disabledWarnings) { return true; } + bool IVbcHostObject.SetDocumentationFile(string documentationFile) { return true; } + bool IVbcHostObject.SetErrorReport(string errorReport) { return true; } + bool IVbcHostObject.SetFileAlignment(int fileAlignment) { return true; } + bool IVbcHostObject.SetGenerateDocumentation(bool generateDocumentation) { return true; } + bool IVbcHostObject.SetImports(ITaskItem[] imports) { return true; } + bool IVbcHostObject.SetKeyContainer(string keyContainer) { return true; } + bool IVbcHostObject.SetKeyFile(string keyFile) { return true; } + bool IVbcHostObject.SetLinkResources(ITaskItem[] linkResources) { return true; } + bool IVbcHostObject.SetMainEntryPoint(string mainEntryPoint) { return true; } + bool IVbcHostObject.SetNoConfig(bool noConfig) { return true; } + bool IVbcHostObject.SetNoStandardLib(bool noStandardLib) { return true; } + bool IVbcHostObject.SetNoWarnings(bool noWarnings) { return true; } + bool IVbcHostObject.SetOptimize(bool optimize) { return true; } + bool IVbcHostObject.SetOptionCompare(string optionCompare) { return true; } + bool IVbcHostObject.SetOptionExplicit(bool optionExplicit) { return true; } + bool IVbcHostObject.SetOptionStrict(bool optionStrict) { return true; } + bool IVbcHostObject.SetOptionStrictType(string optionStrictType) { return true; } + bool IVbcHostObject.SetOutputAssembly(string outputAssembly) { return true; } + bool IVbcHostObject.SetPlatform(string platform) { return true; } + bool IVbcHostObject.SetReferences(ITaskItem[] references) { return true; } + bool IVbcHostObject.SetRemoveIntegerChecks(bool removeIntegerChecks) { return true; } + bool IVbcHostObject.SetResources(ITaskItem[] resources) { return true; } + bool IVbcHostObject.SetResponseFiles(ITaskItem[] responseFiles) { return true; } + bool IVbcHostObject.SetRootNamespace(string rootNamespace) { return true; } + bool IVbcHostObject.SetSdkPath(string sdkPath) { return true; } + bool IVbcHostObject.SetSources(ITaskItem[] sources) { return true; } + bool IVbcHostObject.SetTargetCompactFramework(bool targetCompactFramework) { return true; } + bool IVbcHostObject.SetTargetType(string targetType) { return true; } + bool IVbcHostObject.SetTreatWarningsAsErrors(bool treatWarningsAsErrors) { return true; } + bool IVbcHostObject.SetWarningsAsErrors(string warningsAsErrors) { return true; } + bool IVbcHostObject.SetWarningsNotAsErrors(string warningsNotAsErrors) { return true; } + bool IVbcHostObject.SetWin32Icon(string win32Icon) { return true; } + bool IVbcHostObject.SetWin32Resource(string win32Resource) { return true; } + + bool IVbcHostObject.IsUpToDate() { return false; } + } + + internal class MockVbcHostObject2 : MockVbcHostObject, IVbcHostObject2 + { + bool IVbcHostObject2.SetOptionInfer(bool optionInfer) { return true; } + bool IVbcHostObject2.SetModuleAssemblyName(string moduleAssemblyName) { return true; } + bool IVbcHostObject2.SetWin32Manifest(string win32Manifest) { return true; } + } + + internal class MockVbcHostObject3 : MockVbcHostObject2, IVbcHostObject3 + { + bool IVbcHostObject3.SetLanguageVersion(string languageVersion) { return true; } + } + + internal class MockVbcHostObject4 : MockVbcHostObject3, IVbcHostObject4 + { + bool IVbcHostObject4.SetVBRuntime(string VBRuntime) { return true; } + } + + internal class MockVbcHostObject5 : MockVbcHostObject4, IVbcHostObject5 + { + IVbcHostObjectFreeThreaded IVbcHostObject5.GetFreeThreadedHostObject() { return new MockVbcHostObjectFreeThreaded(this); } + int IVbcHostObject5.CompileAsync(out IntPtr buildSucceededEvent, out IntPtr buildFailedEvent) { buildSucceededEvent = IntPtr.Zero; buildFailedEvent = IntPtr.Zero; return 0; } + int IVbcHostObject5.EndCompile(bool buildSuccess) { return 0; } + bool IVbcHostObject5.SetPlatformWith32BitPreference(string platformWith32BitPreference) { return true; } + bool IVbcHostObject5.SetHighEntropyVA(bool highEntropyVA) { return true; } + bool IVbcHostObject5.SetSubsystemVersion(string subsystemVersion) { return true; } + } + + internal class MockVbcHostObjectFreeThreaded : IVbcHostObjectFreeThreaded + { + private MockVbcHostObject5 _mock; + internal MockVbcHostObjectFreeThreaded(MockVbcHostObject5 mock) { _mock = mock; } + bool IVbcHostObjectFreeThreaded.Compile() { _mock.CompileMethodWasCalled = true; return true; } + } + + internal class MockVbcAnalyzerHostObject : MockVbcHostObject5, IAnalyzerHostObject + { + public ITaskItem[] Analyzers { get; private set; } + public string RuleSet { get; private set; } + public ITaskItem[] AdditionalFiles { get; private set; } + + bool IAnalyzerHostObject.SetAnalyzers(ITaskItem[] analyzers) + { + this.Analyzers = analyzers; + return true; + } + + bool IAnalyzerHostObject.SetRuleSet(string ruleSetFile) + { + this.RuleSet = ruleSetFile; + return true; + } + + bool IAnalyzerHostObject.SetAdditionalFiles(ITaskItem[] additionalFiles) + { + this.AdditionalFiles = additionalFiles; + return true; + } + } +} diff --git a/src/XMakeTasks/UnitTests/Move_Tests.cs b/src/XMakeTasks/UnitTests/Move_Tests.cs new file mode 100644 index 00000000000..8d59295e125 --- /dev/null +++ b/src/XMakeTasks/UnitTests/Move_Tests.cs @@ -0,0 +1,877 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class Move_Tests + { + /// + /// Basic case of moving a file + /// + [TestMethod] + public void BasicMove() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + FileInfo file = new FileInfo(sourceFile); + file.Attributes = file.Attributes | FileAttributes.ReadOnly; // mark read only + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + File.Delete(destinationFile); + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + Assert.IsTrue(t.Execute()); + + Assert.IsTrue(!File.Exists(sourceFile), "Expected the source file to be gone."); + Assert.IsTrue(File.Exists(destinationFile), "Expected the destination file to exist."); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(destinationFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(1, t.MovedFiles.Length); + Assert.AreEqual(true, ((new FileInfo(destinationFile)).Attributes & FileAttributes.ReadOnly) == 0); // should have cleared r/o bit + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /// + /// Basic case of moving a file but with OverwriteReadOnlyFiles = true. + /// + [TestMethod] + public void BasicMoveOverwriteReadOnlyFilesTrue() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + FileInfo file = new FileInfo(sourceFile); + file.Attributes = file.Attributes | FileAttributes.ReadOnly; // mark read only + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + File.Delete(destinationFile); + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.OverwriteReadOnlyFiles = true; + t.DestinationFiles = destinationFiles; + + Assert.IsTrue(t.Execute()); + + Assert.IsFalse(File.Exists(sourceFile), "Expected the source file to be gone."); + Assert.IsTrue(File.Exists(destinationFile), "Expected the destination file to exist."); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(destinationFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(1, t.MovedFiles.Length); + Assert.AreEqual(true, ((new FileInfo(destinationFile)).Attributes & FileAttributes.ReadOnly) == 0); // should have cleared r/o bit + } + finally + { + if (File.Exists(sourceFile)) + { + FileInfo file = new FileInfo(sourceFile); + file.Attributes = file.Attributes & ~FileAttributes.ReadOnly; // mark read only + File.Delete(sourceFile); + } + + File.Delete(destinationFile); + } + } + + /// + /// File to move does not exist + /// Should not overwrite destination! + /// + [TestMethod] + public void NonexistentSource() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + + try + { + File.Delete(sourceFile); + using (StreamWriter sw = new StreamWriter(destinationFile, true)) + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + Assert.IsTrue(!t.Execute()); + + Assert.IsFalse(File.Exists(sourceFile), "Expected the source file to still not exist."); + Assert.IsTrue(File.Exists(destinationFile), "Expected the destination file to still exist."); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(destinationFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(0, t.MovedFiles.Length); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destinationFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a destination temp file.", + "Expected the destination file to still contain the contents of destination file." + ); + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /// + /// A file can be moved onto itself successfully (it's a no-op). + /// + [TestMethod] + public void MoveOverSelfIsSuccessful() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + { + sw.Write("This is a temp file."); + } + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Move t = new Move(); + + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + // Success + Assert.IsTrue(t.Execute()); + + // File is still there. + Assert.IsTrue(File.Exists(sourceFile), "Source file should be there"); + } + finally + { + File.Delete(sourceFile); + } + } + + /// + /// Move should overwrite any destination file except if it's r/o + /// + [TestMethod] + public void MoveOverExistingFileReadOnlyNoOverwrite() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + + using (StreamWriter sw = new StreamWriter(destinationFile, true)) + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + FileInfo file = new FileInfo(destinationFile); + file.Attributes = file.Attributes | FileAttributes.ReadOnly; // mark destination read only + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + Assert.IsTrue(!t.Execute()); + + Assert.IsTrue(File.Exists(sourceFile), "Source file should be present"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destinationFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a destination temp file.", + "Expected the destination file to be unchanged." + ); + + Assert.AreEqual(true, ((new FileInfo(destinationFile)).Attributes & FileAttributes.ReadOnly) != 0); // should still be r/o + } + finally + { + File.Delete(sourceFile); + + FileInfo file = new FileInfo(destinationFile); + file.Attributes = file.Attributes ^ FileAttributes.ReadOnly; // mark destination writeable only + File.Delete(destinationFile); + } + } + + /// + /// Move should overwrite any writeable destination file + /// + [TestMethod] + public void MoveOverExistingFileDestinationWriteable() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + + using (StreamWriter sw = new StreamWriter(destinationFile, true)) + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + t.Execute(); + + Assert.IsFalse(File.Exists(sourceFile), "Source file should be gone"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destinationFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + + /// + /// Move should overwrite any destination file even if it's not r/o + /// if OverwriteReadOnlyFiles is set. + /// + /// This is a regression test for bug 814744 where a move operation with OverwriteReadonlyFiles = true on a destination file with the readonly + /// flag not set caused the readonly flag to be set before the move which caused the move to fail. + /// + [TestMethod] + public void MoveOverExistingFileOverwriteReadOnly() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + + using (StreamWriter sw = new StreamWriter(destinationFile, true)) + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + FileInfo file = new FileInfo(destinationFile); + file.Attributes = file.Attributes & ~FileAttributes.ReadOnly; // mark not read only + + Move t = new Move(); + t.OverwriteReadOnlyFiles = true; + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + t.Execute(); + + Assert.IsFalse(File.Exists(sourceFile), "Source file should be gone"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destinationFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(true, ((new FileInfo(destinationFile)).Attributes & FileAttributes.ReadOnly) == 0); // readonly bit should not be set + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /// + /// Move should overwrite any destination file even if it's r/o + /// if OverwriteReadOnlyFiles is set. + /// + [TestMethod] + public void MoveOverExistingFileOverwriteReadOnlyOverWriteReadOnlyFilesTrue() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = FileUtilities.GetTemporaryFile(); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + + using (StreamWriter sw = new StreamWriter(destinationFile, true)) + sw.Write("This is a destination temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + FileInfo file = new FileInfo(destinationFile); + file.Attributes = file.Attributes | FileAttributes.ReadOnly; // mark read only + + Move t = new Move(); + t.OverwriteReadOnlyFiles = true; + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + t.Execute(); + + Assert.IsFalse(File.Exists(sourceFile), "Source file should be gone"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destinationFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(true, ((new FileInfo(destinationFile)).Attributes & FileAttributes.ReadOnly) == 0); // should have cleared r/o bit + } + finally + { + File.Delete(sourceFile); + File.Delete(destinationFile); + } + } + + /// + /// MovedFiles should only include files that were successfully moved + /// (or skipped), not files for which there was an error. + /// + [TestMethod] + public void OutputsOnlyIncludeSuccessfulMoves() + { + string temp = Path.GetTempPath(); + string inFile1 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A392"); + string inFile2 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A393"); + string invalidFile = "!@#$%^&*()|"; + string validOutFile = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A394"); + + try + { + FileStream fs = null; + FileStream fs2 = null; + + try + { + fs = File.Create(inFile1); + fs2 = File.Create(inFile2); + } + finally + { + fs.Close(); + fs2.Close(); + } + + Move t = new Move(); + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + + ITaskItem i1 = new TaskItem(inFile1); + i1.SetMetadata("Locale", "en-GB"); + i1.SetMetadata("Color", "taupe"); + t.SourceFiles = new ITaskItem[] { new TaskItem(inFile2), i1 }; + + ITaskItem o1 = new TaskItem(validOutFile); + o1.SetMetadata("Locale", "fr"); + o1.SetMetadata("Flavor", "Pumpkin"); + t.DestinationFiles = new ITaskItem[] { new TaskItem(invalidFile), o1 }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(1, t.MovedFiles.Length); + Assert.AreEqual(validOutFile, t.MovedFiles[0].ItemSpec); + Assert.AreEqual(2, t.DestinationFiles.Length); + Assert.AreEqual("fr", t.DestinationFiles[1].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual(invalidFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(validOutFile, t.DestinationFiles[1].ItemSpec); + Assert.AreEqual(validOutFile, t.MovedFiles[0].ItemSpec); + + // Sources attributes should be left untouched. + Assert.AreEqual("en-GB", t.SourceFiles[1].GetMetadata("Locale")); + Assert.AreEqual("taupe", t.SourceFiles[1].GetMetadata("Color")); + + // Attributes not on Sources should be left untouched. + Assert.AreEqual("Pumpkin", t.DestinationFiles[1].GetMetadata("Flavor")); + Assert.AreEqual("Pumpkin", t.MovedFiles[0].GetMetadata("Flavor")); + + // Attribute should have been forwarded + Assert.AreEqual("taupe", t.DestinationFiles[1].GetMetadata("Color")); + Assert.AreEqual("taupe", t.MovedFiles[0].GetMetadata("Color")); + + // Attribute should not have been updated if it already existed on destination + Assert.AreEqual("fr", t.DestinationFiles[1].GetMetadata("Locale")); + Assert.AreEqual("fr", t.MovedFiles[0].GetMetadata("Locale")); + } + finally + { + File.Delete(inFile1); + File.Delete(inFile2); + File.Delete(validOutFile); + } + } + + /// + /// Moving a locked file will fail + /// + [TestMethod] + public void MoveLockedFile() + { + string file = null; + + try + { + file = FileUtilities.GetTemporaryFile(); + bool result; + Move move = null; + + using (StreamWriter writer = new StreamWriter(file)) // lock it for write + { + move = new Move(); + move.BuildEngine = new MockEngine(true /* log to console */); + move.SourceFiles = new ITaskItem[] { new TaskItem(file) }; + move.DestinationFiles = new ITaskItem[] { new TaskItem(file + "2") }; + result = move.Execute(); + } + + Assert.IsTrue(!result); + ((MockEngine)move.BuildEngine).AssertLogContains("MSB3677"); + Assert.AreEqual(false, File.Exists(file + "2")); + } + finally + { + File.Delete(file); + } + } + + /// + /// Must have destination + /// + [TestMethod] + public void NoDestination() + { + Move move = new Move(); + move.BuildEngine = new MockEngine(); + move.SourceFiles = new ITaskItem[] { new TaskItem(Assembly.GetExecutingAssembly().Location) }; + + Assert.AreEqual(false, move.Execute()); + ((MockEngine)move.BuildEngine).AssertLogContains("MSB3679"); + } + + /// + /// Can't have both destination file and directory + /// + [TestMethod] + public void DestinationFileAndDirectory() + { + Move move = new Move(); + move.BuildEngine = new MockEngine(); + move.SourceFiles = new ITaskItem[] { new TaskItem(Assembly.GetExecutingAssembly().Location) }; + move.DestinationFiles = new ITaskItem[] { new TaskItem("x") }; + move.DestinationFolder = new TaskItem(Environment.CurrentDirectory); + + Assert.AreEqual(false, move.Execute()); + ((MockEngine)move.BuildEngine).AssertLogContains("MSB3678"); + } + + /// + /// Can't specify a directory for the destination file + /// + [TestMethod] + public void DestinationFileIsDirectory() + { + Move move = new Move(); + move.BuildEngine = new MockEngine(); + move.SourceFiles = new ITaskItem[] { new TaskItem(Assembly.GetExecutingAssembly().Location) }; + move.DestinationFiles = new ITaskItem[] { new TaskItem(Environment.CurrentDirectory) }; + + Assert.AreEqual(false, move.Execute()); + ((MockEngine)move.BuildEngine).AssertLogContains("MSB3676"); + } + + /// + /// Can't move a directory to a file + /// + [TestMethod] + public void SourceFileIsDirectory() + { + Move move = new Move(); + move.BuildEngine = new MockEngine(); + move.DestinationFiles = new ITaskItem[] { new TaskItem(Assembly.GetExecutingAssembly().Location) }; + move.SourceFiles = new ITaskItem[] { new TaskItem(Environment.CurrentDirectory) }; + + Assert.AreEqual(false, move.Execute()); + ((MockEngine)move.BuildEngine).AssertLogContains("MSB3681"); + } + + /// + /// Moving a file on top of itself should be a success (no-op). + /// Variation with different casing/relativeness. + /// + [TestMethod] + public void MoveFileOnItself2() + { + string currdir = Environment.CurrentDirectory; + string filename = "2A333ED756AF4dc392E728D0F864A396"; + string file = Path.Combine(currdir, filename); + + try + { + FileStream fs = null; + + try + { + fs = File.Create(file); + } + finally + { + fs.Close(); + } + + Move t = new Move(); + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem(file) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(filename.ToLowerInvariant()) }; + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(filename.ToLowerInvariant(), t.DestinationFiles[0].ItemSpec); + + Assert.IsTrue(File.Exists(file), "Source file should be there"); + } + finally + { + File.Delete(file); + } + } + + /// + /// Moving a file on top of itself should be a success (no-op). + /// Variation with a second move failure. + /// + [TestMethod] + public void MoveFileOnItselfAndFailAMove() + { + string temp = Path.GetTempPath(); + string file = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A395"); + string invalidFile = "!@#$%^&*()|"; + string dest2 = "whatever"; + + try + { + FileStream fs = null; + + try + { + fs = File.Create(file); + } + finally + { + fs.Close(); + } + + Move t = new Move(); + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + t.SourceFiles = new ITaskItem[] { new TaskItem(file), new TaskItem(invalidFile) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(file), new TaskItem(dest2) }; + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(2, t.DestinationFiles.Length); + Assert.AreEqual(file, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(dest2, t.DestinationFiles[1].ItemSpec); + Assert.AreEqual(1, t.MovedFiles.Length); + Assert.AreEqual(file, t.MovedFiles[0].ItemSpec); + } + finally + { + File.Delete(file); + } + } + + /// + /// DestinationFolder should work. + /// + [TestMethod] + public void MoveToNonexistentDestinationFolder() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string temp = Path.GetTempPath(); + string destFolder = Path.Combine(temp, "2A333ED756AF4d1392E728D0F864A398"); + string destFile = Path.Combine(destFolder, Path.GetFileName(sourceFile)); + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) + sw.Write("This is a source temp file."); + + // Don't create the dest folder, let task do that + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFolder = new TaskItem(destFolder); + + bool success = t.Execute(); + + Assert.IsTrue(success, "success"); + Assert.IsFalse(File.Exists(sourceFile), "source gone"); + Assert.IsTrue(File.Exists(destFile), "destination exists"); + + string destinationFileContents; + using (StreamReader sr = new StreamReader(destFile)) + destinationFileContents = sr.ReadToEnd(); + + Assert.IsTrue + ( + destinationFileContents == "This is a source temp file.", + "Expected the destination file to contain the contents of source file." + ); + + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.AreEqual(1, t.MovedFiles.Length); + Assert.AreEqual(destFile, t.DestinationFiles[0].ItemSpec); + Assert.AreEqual(destFile, t.MovedFiles[0].ItemSpec); + } + finally + { + File.Delete(sourceFile); + File.Delete(destFile); + Directory.Delete(destFolder); + } + } + + + /// + /// DestinationFiles should only include files that were successfully moved, + /// not files for which there was an error. + /// + [TestMethod] + public void DestinationFilesLengthNotEqualSourceFilesLength() + { + string temp = Path.GetTempPath(); + string inFile1 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398"); + string inFile2 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A399"); + string outFile1 = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A400"); + + try + { + FileStream fs = null; + FileStream fs2 = null; + + try + { + fs = File.Create(inFile1); + fs2 = File.Create(inFile2); + } + finally + { + fs.Close(); + fs2.Close(); + } + + Move t = new Move(); + MockEngine engine = new MockEngine(true /* log to console */); + t.BuildEngine = engine; + + t.SourceFiles = new ITaskItem[] { new TaskItem(inFile1), new TaskItem(inFile2) }; + t.DestinationFiles = new ITaskItem[] { new TaskItem(outFile1) }; + + bool success = t.Execute(); + + Assert.IsTrue(!success); + Assert.AreEqual(1, t.DestinationFiles.Length); + Assert.IsNull(t.MovedFiles); + Assert.IsTrue(!File.Exists(outFile1)); + } + finally + { + File.Delete(inFile1); + File.Delete(inFile2); + File.Delete(outFile1); + } + } + + /// + /// If the destination path is too long, the task should not bubble up + /// the System.IO.PathTooLongException + /// + [TestMethod] + public void Regress451057_ExitGracefullyIfPathNameIsTooLong() + { + string sourceFile = FileUtilities.GetTemporaryFile(); + string destinationFile = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + try + { + using (StreamWriter sw = new StreamWriter(sourceFile, true)) // HIGHCHAR: Test writes in UTF8 without preamble. + sw.Write("This is a source temp file."); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + } + finally + { + File.Delete(sourceFile); + } + } + + /// + /// If the source path is too long, the task should not bubble up + /// the System.IO.PathTooLongException + /// + [TestMethod] + public void Regress451057_ExitGracefullyIfPathNameIsTooLong2() + { + string sourceFile = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"; + string destinationFile = FileUtilities.GetTemporaryFile(); + + ITaskItem[] sourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }; + ITaskItem[] destinationFiles = new ITaskItem[] { new TaskItem(destinationFile) }; + + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = sourceFiles; + t.DestinationFiles = destinationFiles; + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + } + + /// + /// If the SourceFiles parameter is given invalid path + /// characters, make sure the task exits gracefully. + /// + [TestMethod] + public void ExitGracefullyOnInvalidPathCharacters() + { + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = new ITaskItem[] { new TaskItem("foo | bar") }; + t.DestinationFolder = new TaskItem("dest"); + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + } + + /// + /// If the DestinationFile parameter is given invalid path + /// characters, make sure the task exits gracefully. + /// + [TestMethod] + public void ExitGracefullyOnInvalidPathCharactersInDestinationFile() + { + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = new ITaskItem[] { new TaskItem("source") }; + t.DestinationFiles = new ITaskItem[] { new TaskItem("foo | bar") }; + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + } + + /// + /// If the DestinationFile parameter is given invalid path + /// characters, make sure the task exits gracefully. + /// + [TestMethod] + public void ExitGracefullyOnInvalidPathCharactersInDestinationFolder() + { + Move t = new Move(); + t.BuildEngine = new MockEngine(true /* log to console */); + t.SourceFiles = new ITaskItem[] { new TaskItem("source") }; + t.DestinationFolder = new TaskItem("foo | bar"); + + bool result = t.Execute(); + + // Expect for there to have been no copies. + Assert.AreEqual(false, result); + } + } +} diff --git a/src/XMakeTasks/UnitTests/PropertyParser_Tests.cs b/src/XMakeTasks/UnitTests/PropertyParser_Tests.cs new file mode 100644 index 00000000000..8f43d8c3a43 --- /dev/null +++ b/src/XMakeTasks/UnitTests/PropertyParser_Tests.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class PropertyParser_Tests + { + /// + /// + [TestMethod] + public void GetTable1() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", null, out propertiesTable)); + + // We should have null table. + Assert.IsNull(propertiesTable); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable3() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", + new string[] { "Configuration=Debug" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Configuration Debug + + Assert.AreEqual(1, propertiesTable.Count); + Assert.AreEqual("Debug", (string)propertiesTable["Configuration"]); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable4() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", + new string[] { "Configuration=Debug", "Platform=AnyCPU", "VBL=Lab22Dev" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Configuration Debug + // Platform AnyCPU + // VBL Lab22Dev + + Assert.AreEqual(3, propertiesTable.Count); + Assert.AreEqual("Debug", (string)propertiesTable["Configuration"]); + Assert.AreEqual("AnyCPU", (string)propertiesTable["Platform"]); + Assert.AreEqual("Lab22Dev", (string)propertiesTable["VBL"]); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable5() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", + new string[] { "Configuration = Debug", "Platform \t= AnyCPU" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Configuration Debug + // Platform AnyCPU + + Assert.AreEqual(2, propertiesTable.Count); + Assert.AreEqual("Debug", (string)propertiesTable["Configuration"]); + Assert.AreEqual("AnyCPU", (string)propertiesTable["Platform"]); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable6() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", + new string[] { "Configuration=", "Platform = " }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Configuration + // Platform + + Assert.AreEqual(2, propertiesTable.Count); + Assert.AreEqual("", (string)propertiesTable["Configuration"]); + Assert.AreEqual("", (string)propertiesTable["Platform"]); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable7() + { + Hashtable propertiesTable; + + // This is a failure case. + Assert.IsTrue(!PropertyParser.GetTable(null, "Properties", new string[] { "=Debug" }, out propertiesTable)); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable8() + { + Hashtable propertiesTable; + + // This is a failure case. (Second property "x86" doesn't have a value.) + Assert.IsTrue(!PropertyParser.GetTable(null, "Properties", + new string[] { "Configuration=Debug", "x86" }, out propertiesTable)); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable9() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", + new string[] { "DependsOn = Clean; Build" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Depends On Clean; Build + + Assert.AreEqual(1, propertiesTable.Count); + Assert.AreEqual("Clean; Build", (string)propertiesTable["DependsOn"]); + } + + /// + /// + [TestMethod] + public void GetPropertiesTable10() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTable(null, "Properties", + new string[] { "Depends On = CleanBuild" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Depends On CleanBuild + + Assert.AreEqual(1, propertiesTable.Count); + Assert.AreEqual("CleanBuild", (string)propertiesTable["Depends On"]); + } + + [TestMethod] + public void GetPropertiesTableWithEscaping1() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTableWithEscaping(null, "Properties", "Properties", + new string[] { "Configuration = Debug", "Platform = Any CPU" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // Configuration Debug + // Platform Any CPU + + Assert.AreEqual(2, propertiesTable.Count); + Assert.AreEqual("Debug", (string)propertiesTable["Configuration"]); + Assert.AreEqual("Any CPU", (string)propertiesTable["Platform"]); + } + + [TestMethod] + public void GetPropertiesTableWithEscaping2() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTableWithEscaping(null, "Properties", "Properties", + new string[] { "WarningsAsErrors = 1234", "5678", "9999", "Configuration=Debug" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // WarningsAsErrors 1234;5678;9999 + // Configuration Debug + + Assert.AreEqual(2, propertiesTable.Count); + Assert.AreEqual("1234;5678;9999", (string)propertiesTable["WarningsAsErrors"]); + Assert.AreEqual("Debug", (string)propertiesTable["Configuration"]); + } + + [TestMethod] + public void GetPropertiesTableWithEscaping3() + { + Hashtable propertiesTable; + Assert.IsTrue(PropertyParser.GetTableWithEscaping(null, "Properties", "Properties", + new string[] { @"OutDir=c:\Rajeev;s Stuff\binaries", "Configuration=Debug" }, out propertiesTable)); + + // We should have a table that looks like this: + // KEY VALUE + // ================= ========================= + // OutDir c:\Rajeev%3bs Stuff\binaries + // Configuration Debug + + Assert.AreEqual(2, propertiesTable.Count); + Assert.AreEqual(@"c:\Rajeev%3bs Stuff\binaries", (string)propertiesTable["OutDir"]); + Assert.AreEqual("Debug", (string)propertiesTable["Configuration"]); + } + } +} diff --git a/src/XMakeTasks/UnitTests/ReadLinesFromFile_Tests.cs b/src/XMakeTasks/UnitTests/ReadLinesFromFile_Tests.cs new file mode 100644 index 00000000000..f12dcc938e4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ReadLinesFromFile_Tests.cs @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Security.AccessControl; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ReadLinesFromFile_Tests + { + /// + /// Write one line, read one line. + /// + [TestMethod] + public void Basic() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + // Start with a missing file. + File.Delete(file); + + // Append one line to the file. + WriteLinesToFile a = new WriteLinesToFile(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] { new TaskItem("Line1") }; + Assert.IsTrue(a.Execute()); + + // Read the line from the file. + ReadLinesFromFile r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.AreEqual(1, r.Lines.Length); + Assert.AreEqual("Line1", r.Lines[0].ItemSpec); + + // Write two more lines to the file. + a.Lines = new ITaskItem[] { new TaskItem("Line2"), new TaskItem("Line3") }; + Assert.IsTrue(a.Execute()); + + // Read all of the lines and verify them. + Assert.IsTrue(r.Execute()); + Assert.AreEqual(3, r.Lines.Length); + Assert.AreEqual("Line1", r.Lines[0].ItemSpec); + Assert.AreEqual("Line2", r.Lines[1].ItemSpec); + Assert.AreEqual("Line3", r.Lines[2].ItemSpec); + } + finally + { + File.Delete(file); + } + } + + /// + /// Write one line, read one line, where the line contains MSBuild-escapable characters. + /// The file should contain the *unescaped* lines, but no escaping information should be + /// lost when read. + /// + [TestMethod] + public void Escaping() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + // Start with a missing file. + File.Delete(file); + + // Append one line to the file. + WriteLinesToFile a = new WriteLinesToFile(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] { new TaskItem("Line1_%253b_") }; + Assert.IsTrue(a.Execute()); + + // Read the line from the file. + ReadLinesFromFile r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.AreEqual(1, r.Lines.Length); + Assert.AreEqual("Line1_%3b_", r.Lines[0].ItemSpec); + + // Write two more lines to the file. + a.Lines = new ITaskItem[] { new TaskItem("Line2"), new TaskItem("Line3") }; + Assert.IsTrue(a.Execute()); + + // Read all of the lines and verify them. + Assert.IsTrue(r.Execute()); + Assert.AreEqual(3, r.Lines.Length); + Assert.AreEqual("Line1_%3b_", r.Lines[0].ItemSpec); + Assert.AreEqual("Line2", r.Lines[1].ItemSpec); + Assert.AreEqual("Line3", r.Lines[2].ItemSpec); + } + finally + { + File.Delete(file); + } + } + + /// + /// Write a line that contains an ANSI character that is not ASCII. + /// + [TestMethod] + public void ANSINonASCII() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + // Start with a missing file. + File.Delete(file); + + // Append one line to the file. + WriteLinesToFile a = new WriteLinesToFile(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] { new TaskItem("My special character is \u00C3") }; + Assert.IsTrue(a.Execute()); + + // Read the line from the file. + ReadLinesFromFile r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.AreEqual(1, r.Lines.Length); + Assert.AreEqual("My special character is \u00C3", r.Lines[0].ItemSpec); + } + finally + { + File.Delete(file); + } + } + + /// + /// Reading lines from an missing file should result in the empty list. + /// + [TestMethod] + public void ReadMissing() + { + string file = FileUtilities.GetTemporaryFile(); + File.Delete(file); + + // Read the line from the file. + ReadLinesFromFile r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.AreEqual(0, r.Lines.Length); + } + + /// + /// Reading blank lines from a file should be ignored. + /// + [TestMethod] + public void IgnoreBlankLines() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + // Start with a missing file. + File.Delete(file); + + // Append one line to the file. + WriteLinesToFile a = new WriteLinesToFile(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] + { + new TaskItem("Line1"), + new TaskItem(" "), + new TaskItem("Line2"), + new TaskItem(""), + new TaskItem("Line3"), + new TaskItem("\0\0\0\0\0\0\0\0\0") + }; + Assert.IsTrue(a.Execute()); + + // Read the line from the file. + ReadLinesFromFile r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.AreEqual(3, r.Lines.Length); + Assert.AreEqual("Line1", r.Lines[0].ItemSpec); + Assert.AreEqual("Line2", r.Lines[1].ItemSpec); + Assert.AreEqual("Line3", r.Lines[2].ItemSpec); + } + finally + { + File.Delete(file); + } + } + + /// + /// Reading lines from a file that you have no access to. + /// + [TestMethod] + public void ReadNoAccess() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + // Start with a missing file. + File.Delete(file); + + // Append one line to the file. + WriteLinesToFile a = new WriteLinesToFile(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] { new TaskItem("This is a new line") }; + Assert.IsTrue(a.Execute()); + + // Remove all File access to the file to current user + FileSecurity fSecurity = File.GetAccessControl(file); + string userAccount = string.Format(@"{0}\{1}", System.Environment.UserDomainName, System.Environment.UserName); + fSecurity.AddAccessRule(new FileSystemAccessRule(userAccount, FileSystemRights.ReadData, AccessControlType.Deny)); + File.SetAccessControl(file, fSecurity); + + // Attempt to Read lines from the file. + ReadLinesFromFile r = new ReadLinesFromFile(); + MockEngine mEngine = new MockEngine(); + r.BuildEngine = mEngine; + r.File = new TaskItem(file); + Assert.IsFalse(r.Execute()); + } + finally + { + FileSecurity fSecurity = File.GetAccessControl(file); + string userAccount = string.Format(@"{0}\{1}", System.Environment.UserDomainName, System.Environment.UserName); + fSecurity.AddAccessRule(new FileSystemAccessRule(userAccount, FileSystemRights.ReadData, AccessControlType.Allow)); + File.SetAccessControl(file, fSecurity); + + // Delete file + File.Delete(file); + } + } + + /// + /// Invalid encoding + /// + [TestMethod] + public void InvalidEncoding() + { + WriteLinesToFile a = new WriteLinesToFile(); + a.BuildEngine = new MockEngine(); + a.Encoding = "||invalid||"; + a.File = new TaskItem("c:\\" + Guid.NewGuid().ToString()); + a.Lines = new TaskItem[] { new TaskItem("x") }; + + Assert.AreEqual(false, a.Execute()); + ((MockEngine)a.BuildEngine).AssertLogContains("MSB3098"); + Assert.AreEqual(false, File.Exists(a.File.ItemSpec)); + } + + /// + /// Reading blank lines from a file should be ignored. + /// + [TestMethod] + public void Encoding() + { + string file = FileUtilities.GetTemporaryFile(); + try + { + // Write default encoding: UTF8 + WriteLinesToFile a = new WriteLinesToFile(); + a.BuildEngine = new MockEngine(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] { new TaskItem("\uBDEA") }; + Assert.IsTrue(a.Execute()); + + ReadLinesFromFile r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.IsTrue("\uBDEA" == r.Lines[0].ItemSpec); + + File.Delete(file); + + // Write ANSI .. that won't work! + a = new WriteLinesToFile(); + a.BuildEngine = new MockEngine(); + a.File = new TaskItem(file); + a.Lines = new ITaskItem[] { new TaskItem("\uBDEA") }; + a.Encoding = "ASCII"; + Assert.IsTrue(a.Execute()); + + // Read the line from the file. + r = new ReadLinesFromFile(); + r.File = new TaskItem(file); + Assert.IsTrue(r.Execute()); + + Assert.IsTrue("\uBDEA" != r.Lines[0].ItemSpec); + } + finally + { + File.Delete(file); + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/RemoveDir_Tests.cs b/src/XMakeTasks/UnitTests/RemoveDir_Tests.cs new file mode 100644 index 00000000000..5f6053b1528 --- /dev/null +++ b/src/XMakeTasks/UnitTests/RemoveDir_Tests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class RemoveDir_Tests + { + /* + * Method: AttributeForwarding + * + * Make sure that attributes set on input items are forwarded to ouput items. + */ + [TestMethod] + public void AttributeForwarding() + { + RemoveDir t = new RemoveDir(); + + ITaskItem i = new TaskItem("MyNonExistentDirectory"); + i.SetMetadata("Locale", "en-GB"); + t.Directories = new ITaskItem[] { i }; + t.BuildEngine = new MockEngine(); + + t.Execute(); + + Assert.AreEqual("en-GB", t.RemovedDirectories[0].GetMetadata("Locale")); + + // Output ItemSpec should not be overwritten. + Assert.AreEqual("MyNonExistentDirectory", t.RemovedDirectories[0].ItemSpec); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/RemoveDuplicates_Tests.cs b/src/XMakeTasks/UnitTests/RemoveDuplicates_Tests.cs new file mode 100644 index 00000000000..78c70c8b17f --- /dev/null +++ b/src/XMakeTasks/UnitTests/RemoveDuplicates_Tests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class RemoveDuplicates_Tests + { + /// + /// Pass one item in, get the same item back. + /// + [TestMethod] + public void OneItemNop() + { + RemoveDuplicates t = new RemoveDuplicates(); + t.BuildEngine = new MockEngine(); + + t.Inputs = new ITaskItem[] { new TaskItem("MyFile.txt") }; + + bool success = t.Execute(); + Assert.IsTrue(success); + Assert.AreEqual("MyFile.txt", t.Filtered[0].ItemSpec); + } + + /// + /// Pass in two of the same items. + /// + [TestMethod] + public void TwoItemsTheSame() + { + RemoveDuplicates t = new RemoveDuplicates(); + t.BuildEngine = new MockEngine(); + + t.Inputs = new ITaskItem[] { new TaskItem("MyFile.txt"), new TaskItem("MyFile.txt") }; + + bool success = t.Execute(); + Assert.IsTrue(success); + Assert.AreEqual("MyFile.txt", t.Filtered[0].ItemSpec); + } + + /// + /// Pass in two items that are different. + /// + [TestMethod] + public void TwoItemsDifferent() + { + RemoveDuplicates t = new RemoveDuplicates(); + t.BuildEngine = new MockEngine(); + + t.Inputs = new ITaskItem[] { new TaskItem("MyFile1.txt"), new TaskItem("MyFile2.txt") }; + + bool success = t.Execute(); + Assert.IsTrue(success); + Assert.AreEqual("MyFile1.txt", t.Filtered[0].ItemSpec); + Assert.AreEqual("MyFile2.txt", t.Filtered[1].ItemSpec); + } + + /// + /// Case should not matter. + /// + [TestMethod] + public void CaseInsensitive() + { + RemoveDuplicates t = new RemoveDuplicates(); + t.BuildEngine = new MockEngine(); + + t.Inputs = new ITaskItem[] { new TaskItem("MyFile.txt"), new TaskItem("MyFIle.tXt") }; + + bool success = t.Execute(); + Assert.IsTrue(success); + Assert.AreEqual("MyFile.txt", t.Filtered[0].ItemSpec); + } + + /// + /// No inputs should result in zero-length outputs. + /// + [TestMethod] + public void MissingInputs() + { + RemoveDuplicates t = new RemoveDuplicates(); + t.BuildEngine = new MockEngine(); + bool success = t.Execute(); + + Assert.IsTrue(success); + Assert.AreEqual(0, t.Filtered.Length); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/ResGenDependencies_Tests.cs b/src/XMakeTasks/UnitTests/ResGenDependencies_Tests.cs new file mode 100644 index 00000000000..8c77c105959 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResGenDependencies_Tests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Resources; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ResGenDependencies_Tests + { + [TestMethod] + public void DirtyCleanScenario() + { + ResGenDependencies cache = new ResGenDependencies(); + + string resx = CreateSampleResx(); + string stateFile = FileUtilities.GetTemporaryFile(); + + try + { + // A newly created cache is not dirty. + Assert.IsTrue(!cache.IsDirty); + + // Getting a file that wasn't in the cache is a write operation. + cache.GetResXFileInfo(resx); + Assert.IsTrue(cache.IsDirty); + + // Writing the file to disk should make the cache clean. + cache.SerializeCache(stateFile, /* Log */ null); + Assert.IsTrue(!cache.IsDirty); + + // Deserialize from disk. Result should not be dirty. + cache = ResGenDependencies.DeserializeCache(stateFile, true, /* Log */ null); + Assert.IsTrue(!cache.IsDirty); + + // Asking for a file that's in the cache should not dirty the cache. + cache.GetResXFileInfo(resx); + Assert.IsTrue(!cache.IsDirty); + + // Changing UseSourcePath to false should dirty the cache. + cache.UseSourcePath = false; + Assert.IsTrue(cache.IsDirty); + } + finally + { + File.Delete(resx); + File.Delete(stateFile); + } + } + + /// + /// Create a sample resx file on disk. Caller is responsible for deleting. + /// + /// + private string CreateSampleResx() + { + string resx = FileUtilities.GetTemporaryFile(); + File.Delete(resx); + Stream fileToSend = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Build.Tasks.UnitTests.SampleResx"); + using (FileStream f = new FileStream(resx, FileMode.CreateNew)) + { + byte[] buffer = new byte[2048]; + int bytes; + while ((bytes = fileToSend.Read(buffer, 0, 2048)) > 0) + { + f.Write(buffer, 0, bytes); + } + fileToSend.Close(); + } + return resx; + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/ResGen_Tests.cs b/src/XMakeTasks/UnitTests/ResGen_Tests.cs new file mode 100644 index 00000000000..dcacad9d0ff --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResGen_Tests.cs @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +using ResGen = Microsoft.Build.Tasks.GenerateResource.ResGen; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ResGen_Tests + { + /// + /// Verify InputFiles: + /// - Defaults to null, in which case the task just returns true and continues + /// - If there are InputFiles, verify that they all show up on the command line + /// - Verify that OutputFiles defaults appropriately + /// + [TestMethod] + public void InputFiles() + { + ResGen t = new ResGen(); + ITaskItem[] singleTestFile = { new TaskItem("foo.resx") }; + ITaskItem[] singleOutput = { new TaskItem("foo.resources") }; + ITaskItem[] multipleTestFiles = { new TaskItem("hello.resx"), new TaskItem("world.resx"), new TaskItem("!.resx") }; + ITaskItem[] multipleOutputs = { new TaskItem("hello.resources"), new TaskItem("world.resources"), new TaskItem("!.resources") }; + + // Default: InputFiles is null + Assert.IsNull(t.InputFiles, "InputFiles is null by default"); + Assert.IsNull(t.OutputFiles, "OutputFiles is null by default"); + ExecuteTaskAndVerifyLogContainsResource(t, true /* task passes */, "ResGen.NoInputFiles"); + + // One input file -- compile + t.InputFiles = singleTestFile; + t.StronglyTypedLanguage = null; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + Assert.AreEqual(singleTestFile, t.InputFiles, "New InputFiles value should be set"); + Assert.IsNull(t.OutputFiles, "OutputFiles is null until default name generation is triggered"); + + string commandLineParameter = String.Join(",", new string[] { singleTestFile[0].ItemSpec, singleOutput[0].ItemSpec }); + CommandLine.ValidateHasParameter(t, commandLineParameter, true /* resgen 4.0 supports response files */); + CommandLine.ValidateHasParameter(t, @"/compile", true /* resgen 4.0 supports response files */); + + // One input file -- STR + t.InputFiles = singleTestFile; + t.StronglyTypedLanguage = "c#"; + + CommandLine.ValidateHasParameter(t, singleTestFile[0].ItemSpec, false /* resgen 4.0 does not appear to support response files for STR */); + CommandLine.ValidateHasParameter(t, singleOutput[0].ItemSpec, false /* resgen 4.0 does not appear to support response files for STR */); + CommandLine.ValidateHasParameter(t, "/str:c#,,,", false /* resgen 4.0 does not appear to support response files for STR */); + + // Multiple input files -- compile + t.InputFiles = multipleTestFiles; + t.OutputFiles = null; // want it to reset to default + t.StronglyTypedLanguage = null; + + Assert.AreEqual(multipleTestFiles, t.InputFiles, "New InputFiles value should be set"); + + CommandLine.ValidateHasParameter(t, @"/compile", true /* resgen 4.0 supports response files */); + for (int i = 0; i < multipleTestFiles.Length; i++) + { + commandLineParameter = String.Join(",", new string[] { multipleTestFiles[i].ItemSpec, multipleOutputs[i].ItemSpec }); + CommandLine.ValidateHasParameter(t, commandLineParameter, true /* resgen 4.0 supports response files */); + } + + // Multiple input files -- STR (should error) + t.InputFiles = multipleTestFiles; + t.StronglyTypedLanguage = "vb"; + + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.STRLanguageButNotExactlyOneSourceFile"); + } + + /// + /// Verify OutputFiles: + /// - Default values were tested by InputFiles() + /// - Verify that if InputFiles and OutputFiles are different lengths (and both exist), an error is logged + /// - Verify that if OutputFiles are set explicitly, they map and show up on the command line as expected + /// + [TestMethod] + public void OutputFiles() + { + ResGen t = new ResGen(); + + ITaskItem[] differentLengthInput = { new TaskItem("hello.resx") }; + ITaskItem[] differentLengthOutput = { new TaskItem("world.resources"), new TaskItem("!.resources") }; + ITaskItem[] differentLengthDefaultOutput = { new TaskItem("hello.resources") }; + + // Different length inputs -- should error + t.InputFiles = differentLengthInput; + t.OutputFiles = differentLengthOutput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + Assert.AreEqual(differentLengthInput, t.InputFiles, "New InputFiles value should be set"); + Assert.AreEqual(differentLengthOutput, t.OutputFiles, "New OutputFiles value should be set"); + + ExecuteTaskAndVerifyLogContainsErrorFromResource + ( + t, + "General.TwoVectorsMustHaveSameLength", + differentLengthInput.Length, + differentLengthOutput.Length, + "InputFiles", + "OutputFiles" + ); + + // If only OutputFiles is set, then the task should return -- as far as + // it's concerned, no work needs to be done. + t = new ResGen(); // zero out the log + t.InputFiles = null; + t.OutputFiles = differentLengthOutput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + Assert.IsNull(t.InputFiles, "New InputFiles value should be set"); + Assert.AreEqual(differentLengthOutput, t.OutputFiles, "New OutputFiles value should be set"); + + ExecuteTaskAndVerifyLogContainsResource(t, true /* task passes */, "ResGen.NoInputFiles"); + + // However, iff OutputFiles is set to null, it should revert back to default + t.InputFiles = differentLengthInput; + t.OutputFiles = null; + + Assert.AreEqual(differentLengthInput, t.InputFiles, "New InputFiles value should be set"); + Assert.IsNull(t.OutputFiles, "OutputFiles is null until default name generation is triggered"); + + string commandLineParameter = String.Join(",", new string[] { differentLengthInput[0].ItemSpec, differentLengthDefaultOutput[0].ItemSpec }); + CommandLine.ValidateHasParameter(t, commandLineParameter, true /* resgen 4.0 supports response files */); + CommandLine.ValidateHasParameter(t, @"/compile", true /* resgen 4.0 supports response files */); + + // Explicitly setting output + ITaskItem[] inputFiles = { new TaskItem("foo.resx") }; + ITaskItem[] defaultOutput = { new TaskItem("foo.resources") }; + ITaskItem[] explicitOutput = { new TaskItem("bar.txt") }; + + t.InputFiles = inputFiles; + t.OutputFiles = null; + + Assert.AreEqual(inputFiles, t.InputFiles, "New InputFiles value should be set"); + Assert.IsNull(t.OutputFiles, "OutputFiles is null until default name generation is triggered"); + + commandLineParameter = String.Join(",", new string[] { inputFiles[0].ItemSpec, defaultOutput[0].ItemSpec }); + CommandLine.ValidateHasParameter(t, commandLineParameter, true /* resgen 4.0 supports response files */); + CommandLine.ValidateHasParameter(t, @"/compile", true /* resgen 4.0 supports response files */); + + t.OutputFiles = explicitOutput; + + Assert.AreEqual(inputFiles, t.InputFiles, "New InputFiles value should be set"); + Assert.AreEqual(explicitOutput, t.OutputFiles, "New OutputFiles value should be set"); + + commandLineParameter = String.Join(",", new string[] { inputFiles[0].ItemSpec, explicitOutput[0].ItemSpec }); + CommandLine.ValidateHasParameter(t, commandLineParameter, true /* resgen 4.0 supports response files */); + CommandLine.ValidateHasParameter(t, @"/compile", true /* resgen 4.0 supports response files */); + } + + /// + /// Tests ResGen's /publicClass switch + /// + [TestMethod] + public void PublicClass() + { + ResGen t = new ResGen(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + + t.InputFiles = throwawayInput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + Assert.IsFalse(t.PublicClass, "PublicClass should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/publicClass", true /* resgen 4.0 supports response files */); + + t.PublicClass = true; + Assert.IsTrue(t.PublicClass, "PublicClass should be true"); + CommandLine.ValidateHasParameter(t, @"/publicClass", true /* resgen 4.0 supports response files */); + } + + /// + /// Tests the /r: parameter (passing in reference assemblies) + /// + [TestMethod] + public void References() + { + ResGen t = new ResGen(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + ITaskItem a = new TaskItem(); + ITaskItem b = new TaskItem(); + + t.InputFiles = throwawayInput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + a.ItemSpec = "foo.dll"; + b.ItemSpec = "bar.dll"; + + ITaskItem[] singleReference = { a }; + ITaskItem[] multipleReferences = { a, b }; + + Assert.IsNull(t.References, "References should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, "/r:", true /* resgen 4.0 supports response files */); + + // Single reference + t.References = singleReference; + Assert.AreEqual(singleReference, t.References, "New References value should be set"); + CommandLine.ValidateHasParameter(t, "/r:" + singleReference[0].ItemSpec, true /* resgen 4.0 supports response files */); + + // MultipleReferences + t.References = multipleReferences; + Assert.AreEqual(multipleReferences, t.References, "New References value should be set"); + + foreach (ITaskItem reference in multipleReferences) + { + CommandLine.ValidateHasParameter(t, "/r:" + reference.ItemSpec, true /* resgen 4.0 supports response files */); + } + // test cases where command line length is equal to the maximum allowed length and just above the maximum allowed length + // we do some calculation here to do ensure that the resulting command lines match these cases (see Case 1 and 2 below) + + // reference switch adds space + "/r:", (4 characters) + int referenceSwitchDelta = 4; + + //subtract one because leading space is added to command line arguments + int maxCommandLineLength = 28000 - 1; + int referencePathLength = 200; + + //min reference argument is " /r:a.dll" + int minReferenceArgumentLength = referenceSwitchDelta + "a.dll".Length; + + // reference name is of the form aaa...aaa###.dll (repeated a characters followed by 3 + // digit identifier for uniqueness followed by the .dll file extension + StringBuilder referencePathBuilder = new StringBuilder(); + referencePathBuilder.Append('a', referencePathLength - (3 /* 3 digit identifier */ + 4 /* file extension */)); + string longReferenceNameBase = referencePathBuilder.ToString(); + + + // reference switch length plus the length of the reference path + int referenceArgumentLength = referencePathLength + referenceSwitchDelta; + + t = CreateCommandLineResGen(); + + // compute command line with only one reference switch so remaining added reference + // arguments will have the same length, since the first reference argument added may not have a + // leading space + List references = new List(); + references.Add(new TaskItem() { ItemSpec = "a.dll" }); + + t.References = references.ToArray(); + int baseCommandLineLength = CommandLine.GetCommandLine(t, false).Length; + + Assert.IsTrue(baseCommandLineLength < maxCommandLineLength, "Cannot create command line less than the maximum allowed command line"); + + // calculate how many reference arguments will need to be added and what the length of the last argument + // should be so that the command line length is equal to the maximum allowed length + int remainder; + int quotient = Math.DivRem(maxCommandLineLength - baseCommandLineLength, referenceArgumentLength, out remainder); + if (remainder < minReferenceArgumentLength) + { + remainder += referenceArgumentLength; + quotient--; + } + + // compute the length of the last reference argument + int lastReferencePathLength = remainder - (4 /* switch length */ + 4 /* file extension */); + + for (int i = 0; i < quotient; i++) + { + string refIndex = i.ToString().PadLeft(3, '0'); + references.Add(new TaskItem() { ItemSpec = (longReferenceNameBase + refIndex + ".dll") }); + } + + + // + // Case 1: Command line length is equal to the maximum allowed value + // + + // create last reference argument + referencePathBuilder.Clear(); + referencePathBuilder.Append('b', lastReferencePathLength).Append(".dll"); + ITaskItem lastReference = new TaskItem() { ItemSpec = referencePathBuilder.ToString() }; + references.Add(lastReference); + + // set references + t.References = references.ToArray(); + + int commandLineLength = CommandLine.GetCommandLine(t, false).Length; + Assert.IsTrue(commandLineLength == maxCommandLineLength, "Command line length {0} is not equal to the maximum possible command line length {1}", commandLineLength, maxCommandLineLength); + + ExecuteTaskAndVerifyLogDoesNotContainResource + ( + t, + false, + "ResGen.CommandTooLong", + CommandLine.GetCommandLine(t, false).Length + ); + + VerifyLogDoesNotContainResource((MockEngine)t.BuildEngine, GetPrivateLog(t), "ToolTask.CommandTooLong", typeof(ResGen).Name); + + // + // Case 2: Command line length is one more than the maximum allowed value + // + + + // make last reference name longer by one character so that command line should become too long + referencePathBuilder.Insert(0, 'b'); + lastReference.ItemSpec = referencePathBuilder.ToString(); + + // reset ResGen task, since execution can change the command line + t = CreateCommandLineResGen(); + t.References = references.ToArray(); + + commandLineLength = CommandLine.GetCommandLine(t, false).Length; + Assert.IsTrue(commandLineLength == (maxCommandLineLength + 1), "Command line length {0} is not one more than the maximum possible command line length {1}", commandLineLength, maxCommandLineLength + 1); + + ExecuteTaskAndVerifyLogContainsErrorFromResource + ( + t, + "ResGen.CommandTooLong", + CommandLine.GetCommandLine(t, false).Length + ); + + VerifyLogDoesNotContainResource((MockEngine)t.BuildEngine, GetPrivateLog(t), "ToolTask.CommandTooLong", typeof(ResGen).Name); + } + + private ResGen CreateCommandLineResGen() + { + ResGen t = new ResGen(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + + // setting these values should ensure that no response file is used + t.UseCommandProcessor = false; + t.StronglyTypedLanguage = "CSharp"; + + t.InputFiles = throwawayInput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + return t; + } + + /// + /// Tests the SdkToolsPath property: Should log an error if it's null or a bad path. + /// + [TestMethod] + public void SdkToolsPath() + { + ResGen t = new ResGen(); + string badParameterValue = @"C:\Program Files\Microsoft Visual Studio 10.0\My Fake SDK Path"; + string goodParameterValue = Path.GetTempPath(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + + // Without any inputs, the task just passes + t.InputFiles = throwawayInput; + + Assert.IsNull(t.SdkToolsPath, "SdkToolsPath should be null by default"); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + t.SdkToolsPath = badParameterValue; + Assert.AreEqual(badParameterValue, t.SdkToolsPath, "New SdkToolsPath value should be set"); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.SdkToolsPath = goodParameterValue; + + Assert.AreEqual(goodParameterValue, t.SdkToolsPath, "New SdkToolsPath value should be set"); + + bool taskPassed = t.Execute(); + Assert.IsFalse(taskPassed, "Task should still fail -- there are other things wrong with it."); + + // but that particular error shouldn't be there anymore. + string sdkToolsPathMessage = t.Log.FormatResourceString("ResGen.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + string messageWithNoCode; + string sdkToolsPathCode = t.Log.ExtractMessageCode(sdkToolsPathMessage, out messageWithNoCode); + e.AssertLogDoesntContain(sdkToolsPathCode); + } + + /// + /// Verifies the parameters that for resgen.exe's /str: switch + /// + [TestMethod] + public void StronglyTypedParameters() + { + ResGen t = new ResGen(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + + string strLanguage = "c#"; + string strNamespace = "Microsoft.Build.Foo"; + string strClass = "MyFoo"; + string strFile = "MyFoo.cs"; + + // Without any inputs, the task just passes + t.InputFiles = throwawayInput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + // Language is null by default + Assert.IsNull(t.StronglyTypedLanguage, "StronglyTypedLanguage should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, "/str:", false /* resgen 4.0 does not appear to support response files for STR */); + + // If other STR parameters are passed, we error, suggesting they might want a language as well. + t.StronglyTypedNamespace = strNamespace; + CommandLine.ValidateNoParameterStartsWith(t, "/str:", false /* resgen 4.0 does not appear to support response files for STR */); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.STRClassNamespaceOrFilenameWithoutLanguage"); + + t.StronglyTypedNamespace = ""; + t.StronglyTypedClassName = strClass; + CommandLine.ValidateNoParameterStartsWith(t, "/str:", false /* resgen 4.0 does not appear to support response files for STR */); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.STRClassNamespaceOrFilenameWithoutLanguage"); + + t.StronglyTypedClassName = ""; + t.StronglyTypedFileName = strFile; + CommandLine.ValidateNoParameterStartsWith(t, "/str:", false /* resgen 4.0 does not appear to support response files for STR */); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.STRClassNamespaceOrFilenameWithoutLanguage"); + + // However, if it is passed, the /str: switch gets added + t.StronglyTypedLanguage = strLanguage; + t.StronglyTypedNamespace = ""; + t.StronglyTypedClassName = ""; + t.StronglyTypedFileName = ""; + + CommandLine.ValidateHasParameter(t, "/str:" + strLanguage + ",,,", false /* resgen 4.0 does not appear to support response files for STR */); + + t.StronglyTypedNamespace = strNamespace; + CommandLine.ValidateHasParameter(t, "/str:" + strLanguage + "," + strNamespace + ",,", false /* resgen 4.0 does not appear to support response files for STR */); + + t.StronglyTypedClassName = strClass; + CommandLine.ValidateHasParameter(t, "/str:" + strLanguage + "," + strNamespace + "," + strClass + ",", false /* resgen 4.0 does not appear to support response files for STR */); + + t.StronglyTypedFileName = strFile; + CommandLine.ValidateHasParameter(t, "/str:" + strLanguage + "," + strNamespace + "," + strClass + "," + strFile, false /* resgen 4.0 does not appear to support response files for STR */); + } + + /// + /// Tests the ToolPath property: Should log an error if it's null or a bad path. + /// + [TestMethod] + public void ToolPath() + { + ResGen t = new ResGen(); + string badParameterValue = @"C:\Program Files\Microsoft Visual Studio 10.0\My Fake SDK Path"; + string goodParameterValue = Path.GetTempPath(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + + // Without any inputs, the task just passes + t.InputFiles = throwawayInput; + + Assert.IsNull(t.ToolPath, "ToolPath should be null by default"); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + t.ToolPath = badParameterValue; + Assert.AreEqual(badParameterValue, t.ToolPath, "New ToolPath value should be set"); + ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "ResGen.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.ToolPath = goodParameterValue; + + Assert.AreEqual(goodParameterValue, t.ToolPath, "New ToolPath value should be set"); + + bool taskPassed = t.Execute(); + Assert.IsFalse(taskPassed, "Task should still fail -- there are other things wrong with it."); + + // but that particular error shouldn't be there anymore. + string toolPathMessage = t.Log.FormatResourceString("ResGen.SdkOrToolPathNotSpecifiedOrInvalid", t.SdkToolsPath, t.ToolPath); + string messageWithNoCode; + string toolPathCode = t.Log.ExtractMessageCode(toolPathMessage, out messageWithNoCode); + e.AssertLogDoesntContain(toolPathCode); + } + + /// + /// Tests ResGen's /useSourcePath switch + /// + [TestMethod] + public void UseSourcePath() + { + ResGen t = new ResGen(); + ITaskItem[] throwawayInput = { new TaskItem("hello.resx") }; + + t.InputFiles = throwawayInput; + t.ToolPath = Path.GetDirectoryName(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("resgen.exe", TargetDotNetFrameworkVersion.Version45)); + + Assert.IsFalse(t.UseSourcePath, "UseSourcePath should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/useSourcePath", true /* resgen 4.0 supports response files */); + + t.UseSourcePath = true; + Assert.IsTrue(t.UseSourcePath, "UseSourcePath should be true"); + CommandLine.ValidateHasParameter(t, @"/useSourcePath", true /* resgen 4.0 supports response files */); + } + + #region Helper Functions + + /// + /// Given an instance of a ResGen task, executes that task (assuming all necessary parameters + /// have been set ahead of time) and verifies that the execution log contains the error + /// corresponding to the resource name passed in. + /// + /// The task to execute and check + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + private void ExecuteTaskAndVerifyLogContainsErrorFromResource(ResGen t, string errorResource, params object[] args) + { + ExecuteTaskAndVerifyLogContainsResource(t, false, errorResource, args); + } + + /// + /// Given an instance of a ResGen task, executes that task (assuming all necessary parameters + /// have been set ahead of time), verifies that the task had the expected result, and checks + /// the log for the string corresponding to the resource name passed in + /// + /// The task to execute and check + /// true if the task is expected to pass, false otherwise + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + private void ExecuteTaskAndVerifyLogContainsResource(ResGen t, bool expectedResult, string resourceString, params object[] args) + { + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + bool taskPassed = t.Execute(); + Assert.IsTrue(taskPassed == expectedResult, "Unexpected task result"); + + VerifyLogContainsResource(e, t.Log, resourceString, args); + } + + /// + /// Given a log and a resource string, acquires the text of that resource string and + /// compares it to the log. Asserts if the log does not contain the desired string. + /// + /// The MockEngine that contains the log we're checking + /// The TaskLoggingHelper that we use to load the string resource + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + private void VerifyLogContainsResource(MockEngine e, TaskLoggingHelper log, string messageResource, params object[] args) + { + string message = log.FormatResourceString(messageResource, args); + e.AssertLogContains(message); + } + + /// + /// Given an instance of a ResGen task, executes that task (assuming all necessary parameters + /// have been set ahead of time), verifies that the task had the expected result, and ensures + /// the log does not contain the string corresponding to the resource name passed in + /// + /// The task to execute and check + /// true if the task is expected to pass, false otherwise + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + private void ExecuteTaskAndVerifyLogDoesNotContainResource(ResGen t, bool expectedResult, string resourceString, params object[] args) + { + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + bool taskPassed = t.Execute(); + Assert.IsTrue(taskPassed == expectedResult, "Unexpected task result"); + + VerifyLogDoesNotContainResource(e, t.Log, resourceString, args); + } + + /// + /// Given a log and a resource string, acquires the text of that resource string and + /// compares it to the log. Assert fails if the log contain the desired string. + /// + /// The MockEngine that contains the log we're checking + /// The TaskLoggingHelper that we use to load the string resource + /// The name of the resource string to check the log for + /// Arguments needed to format the resource string properly + private void VerifyLogDoesNotContainResource(MockEngine e, TaskLoggingHelper log, string messageResource, params object[] args) + { + string message = log.FormatResourceString(messageResource, args); + e.AssertLogDoesntContain(message); + } + + /// + /// Gets the LogPrivate on the given ToolTask instance. We need to use reflection since + /// LogPrivate is a private property. + /// + /// + static private TaskLoggingHelper GetPrivateLog(ToolTask task) + { + PropertyInfo logPrivateProperty = typeof(ToolTask).GetProperty("LogPrivate", BindingFlags.Instance | BindingFlags.NonPublic); + return (TaskLoggingHelper)logPrivateProperty.GetValue(task, null); + } + + #endregion // Helper Functions + } +} diff --git a/src/XMakeTasks/UnitTests/ResolveAssemblyReference_Tests.cs b/src/XMakeTasks/UnitTests/ResolveAssemblyReference_Tests.cs new file mode 100644 index 00000000000..d7a986b3464 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResolveAssemblyReference_Tests.cs @@ -0,0 +1,16523 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Globalization; +using System.Resources; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.UnitTests.ResolveAssemblyReference_Tests; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using LogExclusionReason = Microsoft.Build.Tasks.ReferenceTable.LogExclusionReason; +using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; +using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture; +using System.Linq; + +namespace Microsoft.Build.UnitTests.ResolveAssemblyReference_Tests +{ + [TestClass] + public class ResolveAssemblyReferenceTestFixture + { + // Create the mocks. + internal static Microsoft.Build.Shared.FileExists fileExists = new Microsoft.Build.Shared.FileExists(FileExists); + internal static Microsoft.Build.Shared.DirectoryExists directoryExists = new Microsoft.Build.Shared.DirectoryExists(DirectoryExists); + internal static Microsoft.Build.Tasks.GetDirectories getDirectories = new Microsoft.Build.Tasks.GetDirectories(GetDirectories); + internal static Microsoft.Build.Tasks.GetAssemblyName getAssemblyName = new Microsoft.Build.Tasks.GetAssemblyName(GetAssemblyName); + internal static Microsoft.Build.Tasks.GetAssemblyMetadata getAssemblyMetadata = new Microsoft.Build.Tasks.GetAssemblyMetadata(GetAssemblyMetadata); + internal static Microsoft.Build.Shared.GetRegistrySubKeyNames getRegistrySubKeyNames = new Microsoft.Build.Shared.GetRegistrySubKeyNames(GetRegistrySubKeyNames); + internal static Microsoft.Build.Shared.GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue = new Microsoft.Build.Shared.GetRegistrySubKeyDefaultValue(GetRegistrySubKeyDefaultValue); + internal static Microsoft.Build.Tasks.GetLastWriteTime getLastWriteTime = new Microsoft.Build.Tasks.GetLastWriteTime(GetLastWriteTime); + internal static Microsoft.Build.Tasks.GetAssemblyRuntimeVersion getRuntimeVersion = new Microsoft.Build.Tasks.GetAssemblyRuntimeVersion(GetRuntimeVersion); + internal static Microsoft.Build.Tasks.CheckIfAssemblyInGac checkIfAssemblyIsInGac = new Microsoft.Build.Tasks.CheckIfAssemblyInGac(CheckForAssemblyInGac); + internal static Microsoft.Build.Shared.OpenBaseKey openBaseKey = new Microsoft.Build.Shared.OpenBaseKey(GetBaseKey); + internal Microsoft.Build.UnitTests.MockEngine.GetStringDelegate resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + internal static Microsoft.Build.Tasks.IsWinMDFile isWinMDFile = new Microsoft.Build.Tasks.IsWinMDFile(IsWinMDFile); + internal static Microsoft.Build.Tasks.ReadMachineTypeFromPEHeader readMachineTypeFromPEHeader = new Microsoft.Build.Tasks.ReadMachineTypeFromPEHeader(ReadMachineTypeFromPEHeader); + + // Performance checks. + internal static Hashtable uniqueFileExists = null; + internal static Hashtable uniqueGetAssemblyName = null; + internal static bool useFrameworkFileExists = false; + internal const string REDISTLIST = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +; + + [TestInitialize] + public void Setup() + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE", "1"); + } + + [TestCleanup] + public void Cleanup() + { + Environment.SetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE", null); + } + + /// + /// Search paths to use. + /// + private static readonly string[] s_defaultPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"c:\MyProject", + @"c:\MyComponents\misc\", + @"c:\MyComponents\1.0", + @"c:\MyComponents\2.0", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", + "{AssemblyFolders}", + "{HintPathFromItem}" + }; + + /// + /// Return the default search paths. + /// + /// + internal string[] DefaultPaths + { + get { return s_defaultPaths; } + } + + /// + /// Start monitoring IO calls. + /// + internal void StartIOMonitoring() + { + // If tables are present then the corresponding IO function will do some monitoring. + uniqueFileExists = new Hashtable(); + uniqueGetAssemblyName = new Hashtable(); + } + + /// + /// Stop monitoring IO calls and assert if any unnecessary IO was used. + /// + internal void StopIOMonitoringAndAssert_Minimal_IOUse() + { + // Check for minimal IO in File.Exists. + foreach (DictionaryEntry entry in uniqueFileExists) + { + string path = (string)entry.Key; + int count = (int)entry.Value; + if (count > 1) + { + string message = String.Format("File.Exists() was called {0} times with path {1}.", count, path); + Assert.Fail(message); + } + } + + + uniqueFileExists = null; + uniqueGetAssemblyName = null; + } + + /// + /// Stop monitoring IO calls and assert if any IO was used. + /// + internal void StopIOMonitoringAndAssert_Zero_IOUse() + { + // Check for minimal IO in File.Exists. + foreach (DictionaryEntry entry in uniqueFileExists) + { + string path = (string)entry.Key; + int count = (int)entry.Value; + if (count > 0) + { + string message = String.Format("File.Exists() was called {0} times with path {1}.", count, path); + Assert.Fail(message); + } + } + + + // Check for zero IO in GetAssemblyName. + foreach (DictionaryEntry entry in uniqueGetAssemblyName) + { + string path = (string)entry.Key; + int count = (int)entry.Value; + if (count > 0) + { + string message = String.Format("GetAssemblyName() was called {0} times with path {1}.", count, path); + Assert.Fail(message); + } + } + + uniqueFileExists = null; + uniqueGetAssemblyName = null; + } + + private static List s_existentFiles = new List + { + @"c:\Frameworks\DependsOnFoo4Framework.dll", + @"c:\Frameworks\DependsOnFoo45Framework.dll", + @"c:\Frameworks\DependsOnFoo35Framework.dll", + @"c:\Frameworks\IndirectDependsOnFoo45Framework.dll", + @"c:\Frameworks\IndirectDependsOnFoo4Framework.dll", + @"c:\Frameworks\IndirectDependsOnFoo35Framework.dll", + Path.Combine(Path.GetTempPath(), @"RawFileNameRelative\System.Xml.dll"), + Path.Combine(Path.GetTempPath(), @"RelativeAssemblyFiles\System.Xml.dll"), + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Data.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.pdb", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.xml", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en\System.Xml.resources.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en\System.Xml.resources.pdb", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en\System.Xml.resources.config", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\xx\System.Xml.resources.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en-GB\System.Xml.resources.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en-GB\System.Xml.resources.pdb", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en-GB\System.Xml.resources.config", + @"c:\MyProject\MyPrivateAssembly.exe", + @"c:\MyProject\MyCopyLocalAssembly.dll", + @"c:\MyProject\MyDontCopyLocalAssembly.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\BadImage.dll", // An assembly that will give a BadImageFormatException from GetAssemblyName + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\BadImage.pdb", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\MyGacAssembly.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\MyGacAssembly.pdb", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\xx\MyGacAssembly.resources.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.dll", + @"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion\System.dll", + @"c:\WINNT\Microsoft.NET\Framework\v9.0.MyVersion\System.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\mscorlib.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC\mscorlib.dll", + @"C:\myassemblies\My.Assembly.dll", + @"c:\MyProject\mscorlib.dll", // This is an mscorlib.dll that has no metadata (i.e. GetAssemblyName returns null) + @"c:\MyProject\System.Data.dll", // This is a System.Data.dll that has the wrong pkt, it shouldn't be matched. + @"C:\MyComponents\MyGrid.dll", // A vendor component that we should find in the registry. + @"C:\MyComponentsA\CustomComponent.dll", // A vendor component that we should find in the registry. + @"C:\MyComponentsB\CustomComponent.dll", // A vendor component that we should find in the registry. + @"C:\MyWinMDComponents7\MyGridWinMD.winmd", + @"C:\MyWinMDComponents9\MyGridWinMD.winmd", + @"C:\MyWinMDComponents\MyGridWinMD.winmd", + @"C:\MyWinMDComponents2\MyGridWinMD.winmd", + @"C:\MyWinMDComponentsA\CustomComponentWinMD.winmd", + @"C:\MyWinMDComponentsB\CustomComponentWinMD.winmd", + @"C:\MyWinMDComponentsVv1\MyGridWinMD2.winmd", + @"C:\MyWinMDComponentsV1\MyGridWinMD3.winmd", + @"C:\MyRawDropControls\MyRawDropControl.dll", // A control installed by VSREG under v2.0.x86chk + @"C:\MyComponents\HKLM Components\MyHKLMControl.dll", // A vendor component that is installed under HKLM but not HKCU. + @"C:\MyComponents\HKCU Components\MyHKLMandHKCUControl.dll", // A vendor component that is installed under HKLM and HKCU. + @"C:\MyComponents\HKLM Components\MyHKLMandHKCUControl.dll", // A vendor component that is installed under HKLM and HKCU. + @"C:\MyWinMDComponents\HKLM Components\MyHKLMControlWinMD.winmd", // A vendor component that is installed under HKLM but not HKCU. + @"C:\MyWinMDComponents\HKCU Components\MyHKLMandHKCUControlWinMD.winmd", // A vendor component that is installed under HKLM and HKCU. + @"C:\MyWinMDComponents\HKLM Components\MyHKLMandHKCUControlWinMD.winmd", // A vendor component that is installed under HKLM and HKCU. + @"C:\MyComponents\v3.0\MyControlWithFutureTargetNDPVersion.dll", // The future version of a component. + @"C:\MyComponents\v2.0\MyControlWithFutureTargetNDPVersion.dll", // The current version of a component. + @"C:\MyComponents\v1.0\MyNDP1Control.dll", // A control that only has an NDP 1.0 version + @"C:\MyComponents\v2.0\MyControlWithPastTargetNDPVersion.dll", // The current version of a component. + @"C:\MyComponents\v1.0\MyControlWithPastTargetNDPVersion.dll", // The past version of a component. + @"C:\MyComponentServicePack\MyControlWithServicePack.dll", // The service pack 1 version of the control + @"C:\MyComponentBase\MyControlWithServicePack.dll", // The non-service pack version of the control. + @"C:\MyComponentServicePack2\MyControlWithServicePack.dll", // The service pack 1 version of the control + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC\mscorlib.dll", // A devices mscorlib. + @"c:\MyLibraries\A.dll", + @"c:\MyExecutableLibraries\A.exe", + @"c:\MyLibraries\B.dll", + @"c:\MyLibraries\C.dll", + @"c:\MyLibraries\v1\D.dll", + @"c:\MyLibraries\v1\E\E.dll", + @"c:\RogueLibraries\v1\D.dll", + @"c:\MyLibraries\v2\D.dll", + @"c:\MyStronglyNamed\A.dll", + @"c:\MyWeaklyNamed\A.dll", + @"c:\MyInaccessible\A.dll", + @"c:\MyNameMismatch\Foo.dll", + @"c:\MyEscapedName\=A=.dll", + @"c:\MyEscapedName\__'ASP'dw0024ry.dll", + @"c:\MyApp\DependsOnSimpleA.dll", + @"C:\Regress312873\a.dll", + @"C:\Regress312873\b.dll", + @"C:\Regress312873-2\a.dll", + @"C:\Regress275161\a.dll", + @"C:\Regress317975\a.dll", + @"C:\Regress317975\b.dll", + @"C:\Regress317975\v2\b.dll", + @"c:\Regress313086\mscorlib.dll", + @"c:\V1Control\MyDeviceControlAssembly.dll", + @"c:\V1ControlSP1\MyDeviceControlAssembly.dll", + @"C:\Regress339786\FolderA\a.dll", + @"C:\Regress339786\FolderA\c.dll", // v1 of c + @"C:\Regress339786\FolderB\b.dll", + @"C:\Regress339786\FolderB\c.dll", // v2 of c + @"c:\OldClrBug\MyFileLoadExceptionAssembly.dll", + @"c:\OldClrBug\DependsMyFileLoadExceptionAssembly.dll", + @"c:\Regress563286\DependsOnBadImage.dll", + @"C:\Regress407623\CrystalReportsAssembly.dll", + @"C:\Regress435487\microsoft.build.engine.dll", + @"C:\Regress313747\Microsoft.Office.Interop.Excel.dll", + @"C:\Regress313747\MS.Internal.Test.Automation.Office.Excel.dll", + @"C:\Regress442570\A.dll", + @"C:\Regress442570\B.dll", + @"C:\Regress454863\A.dll", + @"C:\Regress454863\B.dll", + @"C:\Regress393931\A.metadata_dll", + @"c:\Regress387218\A.dll", + @"c:\Regress387218\B.dll", + @"c:\Regress387218\v1\D.dll", + @"c:\Regress387218\v2\D.dll", + @"c:\Regress390219\A.dll", + @"c:\Regress390219\B.dll", + @"c:\Regress390219\v1\D.dll", + @"c:\Regress390219\v2\D.dll", + @"c:\Regress315619\A\MyAssembly.dll", + @"c:\Regress315619\B\MyAssembly.dll", + @"c:\SGenDependeicies\mycomponent.dll", + @"c:\SGenDependeicies\mycomponent.XmlSerializers.dll", + @"c:\SGenDependeicies\mycomponent2.dll", + @"c:\SGenDependeicies\mycomponent2.XmlSerializers.dll", + @"c:\Regress315619\A\MyAssembly.dll", + @"c:\Regress315619\B\MyAssembly.dll", + @"c:\MyRedist\MyRedistRootAssembly.dll", + @"c:\MyRedist\MyOtherAssembly.dll", + @"c:\MyRedist\MyThirdAssembly.dll", + // ==[Related File Extensions Testing]================================================================================================ + @"C:\AssemblyFolder\SomeAssembly.dll", + @"C:\AssemblyFolder\SomeAssembly.pdb", + @"C:\AssemblyFolder\SomeAssembly.xml", + @"C:\AssemblyFolder\SomeAssembly.pri", + @"C:\AssemblyFolder\SomeAssembly.licenses", + @"C:\AssemblyFolder\SomeAssembly.config", + // ==[Related File Extensions Testing]================================================================================================ + + // ==[Unification Testing]============================================================================================================ + //@"C:\MyComponents\v0.5\UnifyMe.dll", // For unification testing, a version that doesn't exist. + @"C:\MyComponents\v1.0\UnifyMe.dll", + @"C:\MyComponents\v2.0\UnifyMe.dll", + @"C:\MyComponents\v3.0\UnifyMe.dll", + //@"C:\MyComponents\v4.0\UnifyMe.dll", + @"C:\MyApp\v0.5\DependsOnUnified.dll", + @"C:\MyApp\v1.0\DependsOnUnified.dll", + @"C:\MyApp\v2.0\DependsOnUnified.dll", + @"C:\MyApp\v3.0\DependsOnUnified.dll", + @"C:\MyApp\DependsOnWeaklyNamedUnified.dll", + @"C:\MyApp\v1.0\DependsOnEverettSystem.dll", + @"C:\Framework\Everett\System.dll", + @"C:\Framework\Whidbey\System.dll", + // ==[Unification Testing]============================================================================================================ + + // ==[Test assemblies reference higher versions than the current target framework===================================================== + @"c:\MyComponents\misc\DependsOnOnlyv4Assemblies.dll", // Only depends on 4.0.0 assemblies + @"c:\MyComponents\misc\ReferenceVersion9.dll", //Is in redist list and is a 9.0 assembly + @"c:\MyComponents\misc\DependsOn9.dll", //Depends on 9.0 assemblies + @"c:\MyComponents\misc\DependsOn9Also.dll", // Depends on 9.0 assemblies + @"c:\MyComponents\1.0\DependsOn9.dll", // Depends on 9.0 assemblies + @"c:\MyComponents\2.0\DependsOn9.dll", // Depends on 9.0 assemblies + @"c:\Regress444809\A.dll", + @"c:\Regress444809\v2\A.dll", + @"c:\Regress444809\B.dll", + @"c:\Regress444809\C.dll", + @"c:\Regress444809\D.dll", + @"c:\MyComponents\4.0Component\DependsOnOnlyv4Assemblies.dll", + @"C:\Regress714052\MSIL\a.dll", + @"C:\Regress714052\X86\a.dll", + @"C:\Regress714052\NONE\a.dll", + @"C:\Regress714052\Mix\a.dll", + @"C:\Regress714052\Mix\a.winmd", + @"C:\Regress714052\MSIL\b.dll", + @"C:\Regress714052\X86\b.dll", + @"C:\Regress714052\NONE\b.dll", + @"C:\Regress714052\Mix\b.dll", + @"C:\Regress714052\Mix\b.winmd", + + @"C:\MyComponents\X.dll", + @"C:\MyComponents\Y.dll", + @"C:\MyComponents\Z.dll", + + @"C:\MyComponents\Microsoft.Build.dll", + @"C:\MyComponents\DependsOnMSBuild12.dll", + + // WinMD sample files + @"C:\WinMD\v4\mscorlib.dll", // Fake 4.0 mscorlib so we can actually resolve it for one of the tests. With a version of 4 + @"C:\WinMD\v255\mscorlib.dll", // Fake 4.0 mscorlib so we can actually resolve it for one of the tests. With a version of 255 + @"C:\WinMD\DotNetAssemblyDependsOnWinMD.dll", + @"C:\WinMD\DotNetAssemblyDependsOn255WinMD.dll", + @"C:\WinMD\SampleWindowsRuntimeAndCLR.Winmd", + @"C:\WinMD\SampleWindowsRuntimeAndCLR.dll", + @"C:\WinMD\SampleWindowsRuntimeAndOther.Winmd", + @"C:\WinMD\SampleWindowsRuntimeOnly.Winmd", + @"C:\WinMD\SampleWindowsRuntimeOnly.dll", + @"C:\WinMD\SampleWindowsRuntimeOnly.pri", + @"C:\WinMD\SampleWindowsRuntimeOnly2.Winmd", + @"C:\WinMD\SampleWindowsRuntimeOnly3.Winmd", + @"C:\WinMD\SampleWindowsRuntimeOnly4.Winmd", + @"C:\WinMD\SampleWindowsRuntimeReferencingSystem.Winmd", + @"C:\WinMD\SampleWindowsRuntimeReferencingSystemDNE.Winmd", + @"C:\WinMD\SampleClrOnly.Winmd", + @"C:\WinMD\SampleBadWindowsRuntime.Winmd", + @"C:\WinMD\WinMDWithVersion255.Winmd", + @"C:\WinMDArchVerification\DependsOnInvalidPeHeader.Winmd", + @"C:\WinMDArchVerification\DependsOnInvalidPeHeader.dll", + @"C:\WinMDArchVerification\DependsOnAmd64.Winmd", + @"C:\WinMDArchVerification\DependsOnAmd64.dll", + @"C:\WinMDArchVerification\DependsOnArm.Winmd", + @"C:\WinMDArchVerification\DependsOnArm.dll", + @"C:\WinMDArchVerification\DependsOnArmv7.Winmd", + @"C:\WinMDArchVerification\DependsOnArmv7.dll", + @"C:\WinMDArchVerification\DependsOnX86.Winmd", + @"C:\WinMDArchVerification\DependsOnX86.dll", + @"C:\WinMDArchVerification\DependsOnAnyCPUUnknown.Winmd", + @"C:\WinMDArchVerification\DependsOnAnyCPUUnknown.dll", + @"C:\WinMDArchVerification\DependsOnIA64.Winmd", + @"C:\WinMDArchVerification\DependsOnIA64.dll", + @"C:\WinMDArchVerification\DependsOnUnknown.Winmd", + @"C:\WinMDArchVerification\DependsOnUnknown.dll", + @"C:\FakeSDK\References\Debug\X86\DebugX86SDKWinMD.Winmd", + @"C:\FakeSDK\References\Debug\Neutral\DebugNeutralSDKWinMD.Winmd", + @"C:\FakeSDK\References\CommonConfiguration\x86\x86SDKWinMD.Winmd", + @"C:\FakeSDK\References\CommonConfiguration\Neutral\NeutralSDKWinMD.Winmd", + @"C:\FakeSDK\References\Debug\X86\DebugX86SDKRA.dll", + @"C:\FakeSDK\References\Debug\Neutral\DebugNeutralSDKRA.dll", + @"C:\FakeSDK\References\CommonConfiguration\x86\x86SDKRA.dll", + @"C:\FakeSDK\References\CommonConfiguration\Neutral\NeutralSDKRA.dll", + @"C:\FakeSDK\References\Debug\X86\SDKReference.dll", + @"C:\DirectoryContainsOnlyDll\a.dll", + @"C:\DirectoryContainsdllAndWinmd\b.dll", + @"C:\DirectoryContainsdllAndWinmd\c.winmd", + @"C:\DirectoryContainstwoWinmd\a.winmd", + @"C:\DirectoryContainstwoWinmd\c.winmd", + @"C:\SystemRuntime\System.Runtime.dll", + @"C:\SystemRuntime\Portable.dll", + @"C:\SystemRuntime\Regular.dll", + }; + + /// + /// Mocked up GetFiles. + /// + /// + /// + /// + internal static string[] GetFiles(string path, string pattern) + { + if (path.Length > 240) + { + throw new PathTooLongException(); + } + + string extension = null; + if (pattern == "*.xml") + { + extension = ".xml"; + } + else if (pattern == "*.pdb") + { + extension = ".pdb"; + } + else + { + Assert.Fail("Unsupported GetFiles pattern " + pattern); + } + + ArrayList matches = new ArrayList(); + foreach (string file in s_existentFiles) + { + string baseDir = Path.GetDirectoryName(file); + + if (0 == String.Compare(baseDir, path, StringComparison.OrdinalIgnoreCase)) + { + string fileExtension = Path.GetExtension(file); + + if (0 == String.Compare(fileExtension, extension, StringComparison.OrdinalIgnoreCase)) + { + matches.Add(file); + } + } + } + + return (string[])matches.ToArray(typeof(string)); + } + + /// + /// Reads the machine type out of the PEHeader of the native dll + /// + private static UInt16 ReadMachineTypeFromPEHeader(string dllPath) + { + if (@"C:\WinMDArchVerification\DependsOnInvalidPeHeader.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_INVALID; + } + else if (@"C:\WinMDArchVerification\DependsOnAmd64.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_AMD64; + } + else if (@"C:\WinMDArchVerification\DependsOnX86.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_I386; + } + else if (@"C:\WinMDArchVerification\DependsOnArm.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_ARM; + } + else if (@"C:\WinMDArchVerification\DependsOnArmV7.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_ARMV7; + } + else if (@"C:\WinMDArchVerification\DependsOnIA64.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_IA64; + } + else if (@"C:\WinMDArchVerification\DependsOnUnknown.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_R4000; + } + else if (@"C:\WinMDArchVerification\DependsOnAnyCPUUnknown.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_UNKNOWN; + } + else if (@"C:\WinMD\SampleWindowsRuntimeOnly.dll".Equals(dllPath, StringComparison.OrdinalIgnoreCase)) + { + return NativeMethods.IMAGE_FILE_MACHINE_I386; + } + + return NativeMethods.IMAGE_FILE_MACHINE_INVALID; + } + + /// + /// Checks to see if the file is a winmd file. + /// + private static bool IsWinMDFile(string fullPath, GetAssemblyRuntimeVersion getAssemblyRuntimeVersion, FileExists fileExists, out string imageRuntimeVersion, out bool isManagedWinMD) + { + imageRuntimeVersion = getAssemblyRuntimeVersion(fullPath); + isManagedWinMD = false; + + if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeOnly.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeAndCLR.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + isManagedWinMD = true; + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\WinMDWithVersion255.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeOnly2.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeOnly3.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeOnly4.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeReferencingSystem.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (String.Compare(fullPath, @"C:\WinMD\SampleWindowsRuntimeReferencingSystemDNE.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (fullPath.StartsWith(@"C:\MyWinMDComponents", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (String.Compare(fullPath, @"C:\FakeSDK\WindowsMetadata\SDKWinMD2.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else if (fullPath.StartsWith(@"C:\DirectoryContains", StringComparison.OrdinalIgnoreCase) && Path.GetExtension(fullPath).Equals(".winmd", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (fullPath.StartsWith(@"C:\WinMDArchVerification", StringComparison.OrdinalIgnoreCase) && Path.GetExtension(fullPath).Equals(".winmd", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (String.Compare(fullPath, @"C:\FakeSDK\WindowsMetadata\SDKWinMD.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + + return false; + } + + /// + /// Checks to see if the assemblyName passed in is in the GAC. + /// + private static bool CheckForAssemblyInGac(AssemblyNameExtension assemblyName, SystemProcessorArchitecture targetProcessorArchitecture, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, FileExists fileExists) + { + if (assemblyName.Equals(new AssemblyNameExtension("Z, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"))) + { + return false; + } + else if (assemblyName.Equals(new AssemblyNameExtension("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"))) + { + return true; + } + else if (assemblyName.Equals(new AssemblyNameExtension("Y, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"))) + { + return false; + } + else + { + string gacLocation = null; + if (assemblyName.Version != null) + { + gacLocation = GlobalAssemblyCache.GetLocation(assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, true, fileExists, null, null, false /* this value does not matter if we are passing a full fusion name*/); + } + return gacLocation != null; + } + } + + /// + /// Mock the File.Exists method. + /// + /// The path to check. + /// 'true' if the file is supposed to exist + internal static bool FileExists(string path) + { + // For very long paths, File.Exists just returns false + if (path.Length > 240) + { + return false; + } + + // Do a real File.Exists to make it throw exceptions for illegal paths. + if (File.Exists(path) && useFrameworkFileExists) + { + return true; + } + + // Do IO monitoring if needed. + if (uniqueFileExists != null) + { + string lowerPath = path.ToLower(); + if (uniqueFileExists[lowerPath] == null) + { + uniqueFileExists[lowerPath] = 0; + } + else + { + uniqueFileExists[lowerPath] = (int)uniqueFileExists[lowerPath] + 1; + } + } + + + // First, MyMissingAssembly doesn't exist anywhere. + if (path.IndexOf("MyMissingAssembly") != -1) + { + return false; + } + + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(path); + } + + + foreach (string file in s_existentFiles) + { + if (0 == String.Compare(path, file, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + + // Everything else doesn't exist. + return false; + } + + /// + /// Mock the Directory.Exists method. + /// + /// The path to check. + /// 'true' if the directory is supposed to exist + internal static bool DirectoryExists(string path) + { + // Now specify the remaining files. + string[] existentDirs = new string[] + { + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"c:\SGenDependeicies", + Path.GetTempPath() + }; + + foreach (string dir in existentDirs) + { + if (0 == String.Compare(path, dir, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + // Everything else doesn't exist. + return false; + } + + /// + /// A mock delagate for Directory.GetDirectories. + /// + /// The file path. + /// The file pattern. + /// A set of subdirectories + internal static string[] GetDirectories(string path, string pattern) + { + if (path.EndsWith(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion")) + { + string[] paths = new string[] { + Path.Combine(path, "en"), Path.Combine(path, "en-GB"), Path.Combine(path, "xx") + }; + + return paths; + } + else if (String.Compare(path, @".", StringComparison.OrdinalIgnoreCase) == 0) + { + // Pretend the current directory has a few subfolders. + return new string[] { + Path.Combine(path, "en"), Path.Combine(path, "en-GB"), Path.Combine(path, "xx") + }; + } + + return new string[0]; + } + + /// + /// Given a path return the corosponding CLR runtime version + /// + /// Path to the file + /// Image runtime version + internal static string GetRuntimeVersion(string path) + { + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeAndCLR.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "WindowsRuntime 1.0, CLR V2.0.50727"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\WinMDWithVersion255.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly2.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly3.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly4.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeReferencingSystem.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeReferencingSystemDNE.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\WinMD\SampleClrOnly.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "CLR V2.0.50727"; + } + else if (String.Compare(path, @"C:\WinMD\SampleBadWindowsRuntime.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "Windows Runtime"; + } + else if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeAndOther.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "WindowsRuntime 1.0, Other V2.0.50727"; + } + + else if (String.Compare(path, @"C:\DirectoryContainsOnlyDll\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return "V2.0.50727"; + } + else if (String.Compare(path, @"C:\DirectoryContainsdllAndWinmd\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return "V2.0.50727"; + } + else if (String.Compare(path, @"C:\DirectoryContainsdllAndWinmd\c.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\DirectoryContainstwoWinmd\a.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "WindowsRuntime 1.0"; + } + else if (String.Compare(path, @"C:\DirectoryContainstwoWinmd\c.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return "WindowsRuntime 1.0"; + } + else if (path.StartsWith(@"C:\MyWinMDComponents", StringComparison.OrdinalIgnoreCase)) + { + return "Windows Runtime"; + } + else if (path.StartsWith(@"C:\WinMDArchVerification", StringComparison.OrdinalIgnoreCase) && path.EndsWith(".winmd")) + { + return "WindowsRuntime 1.0"; + } + else if (path.EndsWith(".dll") || path.EndsWith(".exe") || path.EndsWith(".winmd")) + { + return "v2.0.50727"; + } + else + { + return ""; + } + } + + /// + /// Given a path, return the corresponding AssemblyName + /// + /// Path to the assembly. + /// The assemblyname. + internal static AssemblyNameExtension GetAssemblyName(string path) + { + // Do IO monitoring if needed. + if (uniqueGetAssemblyName != null) + { + string lowerPath = path.ToLower(); + if (uniqueGetAssemblyName[lowerPath] == null) + { + uniqueGetAssemblyName[lowerPath] = 0; + } + else + { + uniqueGetAssemblyName[lowerPath] = (int)uniqueGetAssemblyName[lowerPath] + 1; + } + } + + // For very long paths, GetAssemblyName throws an exception. + if (path.Length > 240) + { + throw new FileNotFoundException(path); + } + + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(path); + } + + if + ( + String.Compare(path, @"c:\OldClrBug\MyFileLoadExceptionAssembly.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + // An older LKG of the CLR could throw a FileLoadException if it doesn't recognize + // the assembly. We need to support this for dogfooding purposes. + throw new FileLoadException("Could not load " + path); + } + + if + ( + String.Compare(path, @"c:\Regress313086\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + // This is an mscorlib that returns null for its assembly name. + return null; + } + + if + ( + String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\BadImage.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + throw new System.BadImageFormatException(@"The format of the file 'c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\BadImage.dll' is invalid"); + } + + if + ( + String.Compare(path, @"c:\MyProject\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + // This is an mscorlib.dll with no metadata. + return null; + } + + if + ( + String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + // This is an mscorlib.dll with no metadata. + return null; + } + + if (path.Contains("MyMissingAssembly")) + { + throw new FileNotFoundException(path); + } + + if (String.Compare(path, @"c:\Frameworks\DependsOnFoo45Framework.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnFoo45Framework, Version=4.5.0.0, PublicKeyToken=null, Culture=Neutral"); + } + + if (String.Compare(path, @"c:\Frameworks\DependsOnFoo4Framework.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnFoo4Framework, Version=4.0.0.0, PublicKeyToken=null, Culture=Neutral"); + } + + if (String.Compare(path, @"c:\Frameworks\DependsOnFoo35Framework.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnFoo35Framework, Version=3.5.0.0, PublicKeyToken=null, Culture=Neutral"); + } + + if (String.Compare(path, @"c:\Regress315619\A\MyAssembly.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyAssembly, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\Regress315619\B\MyAssembly.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyAssembly, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\Regress442570\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089"); + } + if (String.Compare(path, @"c:\Regress387218\v1\D.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("D, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\Regress442570\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\Regress387218\v2\D.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("D, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\Regress390219\v1\D.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("D, Version=1.0.0.0, Culture=fr, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\Regress390219\v2\D.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("D, Version=2.0.0.0, Culture=en, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\Regress442570\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\Regress442570\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\MyStronglyNamed\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neUtral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\MyNameMismatch\Foo.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Notice the metadata assembly name does not match the base file name. + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neUtral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\MyEscapedName\=A=.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Notice the metadata assembly name does not match the base file name. + return new AssemblyNameExtension("\\=A\\=, Version=2.0.0.0, Culture=neUtral, PublicKeyToken=b77a5c561934e089", true); + } + + if (String.Compare(path, @"c:\MyEscapedName\__'ASP'dw0024ry.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Notice the metadata assembly name does not match the base file name. + return new AssemblyNameExtension("__\\'ASP\\'dw0024ry", true); + } + + if (String.Compare(path, @"c:\MyInaccessible\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate an assembly that throws an UnauthorizedAccessException upon access. + throw new UnauthorizedAccessException(); + } + + if (String.Compare(path, Path.Combine(Path.GetTempPath(), @"RawFileNameRelative\System.Xml.dll"), StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension(AssemblyRef.SystemXml); + } + + if (String.Compare(path, Path.Combine(Path.GetTempPath(), @"RelativeAssemblyFiles\System.Xml.dll"), StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension(AssemblyRef.SystemXml); + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.XML.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension(AssemblyRef.SystemXml); + } + + // This is an assembly with an earlier version. + if (String.Compare(path, @"c:\MyProject\System.Xml.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension(AssemblyRef.SystemXml); + } + + // This is an assembly with an incorrect PKT. + if (String.Compare(path, @"c:\MyProject\System.Data.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=A77a5c561934e089"); + } + + if (path.EndsWith(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\MyGacAssembly.dll")) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGacAssembly, Version=9.2.3401.1, Culture=neutral, PublicKeyToken=a6694b450823df78"); + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("System, VeRSion=2.0.0.0, Culture=neutRAl, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("System, VeRSion=4.0.0.0, Culture=neutRAl, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v9.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("System, VeRSion=9.0.0.0, Culture=neutRAl, PublicKeyToken=b77a5c561934e089"); + } + + if + ( + String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Data.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension(AssemblyRef.SystemData); + } + + if (path.EndsWith(@"c:\MyLibraries\v1\D.dll")) + { + // Version 1 of D + return new AssemblyNameExtension("D, Version=1.0.0.0, CulTUre=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa"); + } + + if (path.EndsWith(@"c:\RogueLibraries\v1\D.dll")) + { + // Version 1 of D, but with a different PKT + return new AssemblyNameExtension("D, VERsion=1.0.0.0, Culture=neutral, PublicKeyToken=bbbbbbbbbbbbbbbb"); + } + + if (path.EndsWith(@"c:\MyLibraries\v1\E\E.dll")) + { + return new AssemblyNameExtension("E, Version=0.0.0.0, Culture=neutral, PUBlicKeyToken=null"); + } + + + if (String.Compare(path, @"C:\MyComponents\v0.5\UnifyMe.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + throw new FileNotFoundException(); + } + + if (String.Compare(path, @"C:\MyComponents\v1.0\UnifyMe.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("UnifyMe, Version=1.0.0.0, Culture=nEUtral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL"); + } + + if (String.Compare(path, @"C:\Framework\Everett\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("System, Version=1.0.5000.0, Culture=neutral, PublICKeyToken=" + AssemblyRef.EcmaPublicKey); + } + + if (String.Compare(path, @"C:\Framework\Whidbey\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=" + AssemblyRef.EcmaPublicKey); + } + + + if (String.Compare(path, @"C:\MyApp\v1.0\DependsOnEverettSystem.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnEverettSystem, VersION=1.0.5000.0, Culture=neutral, PublicKeyToken=feedbeadbadcadbe"); + } + + if (String.Compare(path, @"C:\MyApp\v0.5\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnUnified, Version=0.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\Regress339786\FolderA\C.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("C, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\Regress339786\FolderB\C.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("C, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\MyApp\v1.0\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnUnified, VERSion=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\MyApp\v2.0\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnUnified, VeRSIon=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\MyApp\v3.0\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnUnified, Version=3.0.0.0, Culture=neutral, PublicKEYToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\MyComponents\v2.0\UnifyMe.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("UnifyMe, Version=2.0.0.0, Culture=neutral, PublicKeyTOKEn=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\MyComponents\v3.0\UnifyMe.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("UnifyMe, Version=3.0.0.0, Culture=neutral, PublICkeyToken=b77a5c561934e089"); + } + + if (path.EndsWith(@"c:\MyLibraries\v2\D.dll")) + { + return new AssemblyNameExtension("D, VErsion=2.0.0.0, CulturE=neutral, PublicKEyToken=aaaaaaaaaaaaaaaa"); + } + + if (String.Compare(path, @"C:\Regress317975\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("A, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + if (String.Compare(path, @"C:\Regress317975\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + if (String.Compare(path, @"C:\Regress317975\v2\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + // Set up assembly names for testing target framework version checks + // Is version 4 and will only depends on 4.0 assemblies + if (String.Compare(path, @"c:\MyComponents\4.0Component\DependsOnOnlyv4Assemblies.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnOnlyv4Assemblies, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + } + + // Is version 9 and will not have any dependencies, will be in the redist list + if (String.Compare(path, @"c:\MyComponents\misc\ReferenceVersion9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("ReferenceVersion9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + } + + // Is a third party assembly which depends on a version 9 assembly + if (String.Compare(path, @"c:\MyComponents\misc\DependsOn9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + } + + //A second assembly which depends on version 9 framework assemblies. + if (String.Compare(path, @"c:\MyComponents\misc\DependsOn9Also.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOn9Also, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + } + + if (String.Compare(path, @"c:\MyComponents\1.0\DependsOn9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOn9, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + } + + if (String.Compare(path, @"c:\MyComponents\2.0\DependsOn9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOn9, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + } + + if (String.Compare(path, @"c:\Regress444809\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\Regress444809\v2\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\Regress444809\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("B, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\Regress444809\C.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("C, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\Regress444809\D.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("D, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\Regress714052\X86\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=X86"); + } + if (String.Compare(path, @"C:\Regress714052\Mix\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=X86"); + } + if (String.Compare(path, @"C:\Regress714052\Mix\a.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=MSIL"); + } + + if (String.Compare(path, @"C:\Regress714052\MSIL\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=MSIL"); + } + + if (String.Compare(path, @"C:\Regress714052\None\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + if (String.Compare(path, @"C:\Regress714052\X86\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=X86"); + } + if (String.Compare(path, @"C:\Regress714052\Mix\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=X86"); + } + if (String.Compare(path, @"C:\Regress714052\Mix\b.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=MSIL"); + } + if (String.Compare(path, @"C:\Regress714052\MSIL\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null, ProcessorArchitecture=MSIL"); + } + if (String.Compare(path, @"C:\Regress714052\None\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + if (String.Compare(path, @"c:\MyComponents\X.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\MyComponents\Z.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("Z, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\MyComponents\Y.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("Y, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"c:\MyComponents\Microsoft.Build.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("Microsoft.Build, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + if (String.Compare(path, @"c:\MyComponents\DependsOnMSBuild12.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension("DependsOnMSBuild12, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\WinMD\v4\MsCorlib.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("mscorlib, Version=4.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\WinMD\v255\MsCorlib.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=Neutral, PublicKeyToken=b77a5c561934e089"); + } + + if (String.Compare(path, @"C:\WinMD\DotNetAssemblyDependsOnWinMD.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DotNetAssemblyDependsOnWinMD, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\WinMD\DotNetAssemblyDependsOn255WinMD.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DotNetAssemblyDependsOn255WinMD, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeOnly, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnInvalidPeHeader.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnInvalidPeHeader, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnAmd64.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnAmd64, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnArm.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnArm, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnIA64.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnIA64, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnArmv7.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnArmv7, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnX86.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnX86, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnUnknown.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnUnknown, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMDArchVerification\DependsOnAnyCPUUnknown.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DependsOnAnyCPUUnknown, Version=1.0.0.0"); + } + if (String.Compare(path, @"C:\WinMD\WinMDWithVersion255.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("WinMDWithVersion255, Version=255.255.255.255"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly2.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeOnly2, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly3.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeOnly3, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly4.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeOnly4, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeReferencingSystem.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeReferencingSystem, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeReferencingSystemDNE.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeReferencingSystemDNE, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeAndCLR.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SampleWindowsRuntimeAndCLR, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\MyWinMDComponents\MyGridWinMD.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGridWinMD, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\MyWinMDComponents2\MyGridWinMD.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGridWinMD, Version=2.0.0.0"); + } + + if (String.Compare(path, @"C:\MyWinMDComponent7s\MyGridWinMD.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGridWinMD, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\MyWinMDComponents9\MyGridWinMD.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGridWinMD, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\MyWinMDComponentsVv1\MyGridWinMD2.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGridWinMD2, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\MyWinMDComponentsV1\MyGridWinMD3.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("MyGridWinMD3, Version=1.0.0.0"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\Debug\X86\DebugX86SDKWinMD.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DebugX86SDKWinMD, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\Debug\Neutral\DebugNeutralSDKWinMD.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DebugNeutralSDKWinMD, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\CommonConfiguration\x86\x86SDKWinMD.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("X86SDKWinMD, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\CommonConfiguration\Neutral\NeutralSDKWinMD.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("NeutralSDKWINMD, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\Debug\X86\DebugX86SDKRA.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("Debugx86SDKRA, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\Debug\Neutral\DebugNeutralSDKRA.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("DebugNeutralSDKRA, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\CommonConfiguration\x86\x86SDKRA.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("X86SDKRA, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\CommonConfiguration\Neutral\NeutralSDKRA.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("NeutralSDKRA, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\FakeSDK\References\Debug\X86\SDKReference.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("SDKReference, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\DirectoryContainsOnlyDll\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\DirectoryContainsdllAndWinmd\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("b, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\DirectoryContainsdllAndWinmd\c.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("C, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\DirectoryContainstwoWinmd\a.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("A, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + if (String.Compare(path, @"C:\DirectoryContainstwoWinmd\c.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension("C, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"); + } + + string defaultName = String.Format("{0}, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral", Path.GetFileNameWithoutExtension(path)); + return new AssemblyNameExtension(defaultName); + } + + + /// + /// Cached implementation. Given an assembly name, crack it open and retrieve the list of dependent + /// assemblies and the list of scatter files. + /// + /// Path to the assembly. + /// Receives the list of dependencies. + /// Receives the list of associated scatter files. + internal static void GetAssemblyMetadata + ( + string path, + out AssemblyNameExtension[] dependencies, + out string[] scatterFiles, + out FrameworkNameVersioning frameworkName + ) + { + dependencies = GetDependencies(path); + scatterFiles = null; + frameworkName = GetTargetFrameworkAttribute(path); + + if (@"C:\Regress275161\a.dll" == path) + { + scatterFiles = new string[] + { + @"m1.netmodule", + @"m2.netmodule" + }; + } + } + + /// + /// Cached implementation. Given an assembly name, crack it open and retrieve the TargetFrameworkAttribute + /// + internal static FrameworkNameVersioning GetTargetFrameworkAttribute + ( + string path + ) + { + FrameworkNameVersioning frameworkName = null; + + if (String.Equals(path, @"c:\Frameworks\DependsOnFoo4Framework.dll", StringComparison.OrdinalIgnoreCase)) + { + frameworkName = new FrameworkNameVersioning("FoO, Version=v4.0"); + } + else if (String.Equals(path, @"c:\Frameworks\DependsOnFoo45Framework.dll", StringComparison.OrdinalIgnoreCase)) + { + frameworkName = new FrameworkNameVersioning("FoO, Version=v4.5"); + } + else if (String.Equals(path, @"c:\Frameworks\DependsOnFoo35Framework.dll", StringComparison.OrdinalIgnoreCase)) + { + frameworkName = new FrameworkNameVersioning("FoO, Version=v3.5"); + } + else if (String.Equals(path, @"c:\Frameworks\IndirectDependsOnFoo4Framework.dll", StringComparison.OrdinalIgnoreCase)) + { + frameworkName = new FrameworkNameVersioning("FoO, Version=v4.0"); + } + else if (String.Equals(path, @"c:\Frameworks\IndirectDependsOnFoo45Framework.dll", StringComparison.OrdinalIgnoreCase)) + { + frameworkName = new FrameworkNameVersioning("FoO, Version=v4.0"); + } + else if (String.Equals(path, @"c:\Frameworks\IndirectDependsOnFoo35Framework.dll", StringComparison.OrdinalIgnoreCase)) + { + frameworkName = new FrameworkNameVersioning("FoO, Version=v4.0"); + } + + return frameworkName; + } + + /// + /// Given an assembly, with optional assemblyName return all of the dependent assemblies. + /// + /// The full path to the parent assembly + /// The array of dependent assembly names. + internal static AssemblyNameExtension[] GetDependencies(string path) + { + if (String.Compare(path, @"c:\Frameworks\IndirectDependsOnFoo4Framework.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("DependsOnFoo4Framework, Version=4.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"c:\Frameworks\IndirectDependsOnFoo45Framework.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("DependsOnFoo45Framework, Version=4.5.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"c:\Frameworks\IndirectDependsOnFoo35Framework.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("DependsOnFoo35Framework, Version=3.5.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress454863\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("B, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress442570\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension(" A, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\Regress313747\Microsoft.Office.Interop.Excel.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension(" Office, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") + }; + } + + if (String.Compare(path, @"C:\Regress313747\MS.Internal.Test.Automation.Office.Excel.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension(" Office, Version=12.0.0.0, Culture=neutral, PublicKeyToken=94de0004b6e3fcc5") + }; + } + + if (String.Compare(path, @"c:\Regress387218\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, Version=1.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"c:\Regress387218\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, Version=2.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"c:\Regress390219\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, Version=1.0.0.0, PublicKeyToken=b77a5c561934e089, Culture=fr") + }; + } + + if (String.Compare(path, @"c:\Regress390219\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, Version=2.0.0.0, PublicKeyToken=b77a5c561934e089, Culture=en") + }; + } + + if (String.Compare(path, @"C:\Regress454863\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("B, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress442570\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension(" A, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\Regress313747\Microsoft.Office.Interop.Excel.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension(" Office, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") + }; + } + + if (String.Compare(path, @"C:\Regress313747\MS.Internal.Test.Automation.Office.Excel.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension(" Office, Version=12.0.0.0, Culture=neutral, PublicKeyToken=94de0004b6e3fcc5") + }; + } + + if (String.Compare(path, @"c:\OldClrBug\DependsMyFileLoadExceptionAssembly.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("MyFileLoadExceptionAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\Regress563286\DependsOnBadImage.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("BadImage, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"c:\MyInaccessible\A.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + throw new UnauthorizedAccessException(); + } + + if (String.Compare(path, @"c:\Regress313086\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] { }; + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\WinMD\DotNetAssemblyDependsOnWinMD.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("SampleWindowsRuntimeOnly, Version=1.0.0.0") + }; + } + + if (String.Compare(path, @"C:\WinMD\DotNetAssemblyDependsOn255WinMD.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("WinMDWithVersion255, Version=255.255.255.255") + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeAndClr.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=4.0.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\WinMD\WinMDWithVersion255.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly2.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("SampleWindowsRuntimeOnly, Version=1.0.0.0") + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly3.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyNameExtension("SampleWindowsRuntimeOnly, Version=1.0.0.0"), + new AssemblyNameExtension("SampleWindowsRuntimeReferencingSystem, Version=1.0.0.0"), + new AssemblyNameExtension("WinMDWithVersion255, Version=255.255.255.255") + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeOnly4.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyNameExtension("SampleWindowsRuntimeReferencingSystemDNE, Version=1.0.0.0"), + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeReferencingSystem.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyNameExtension("System, Version=255.255.255.255, Culture=Neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\WinMD\SampleWindowsRuntimeReferencingSystemDNE.Winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=255.255.255.255, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyNameExtension("System.DoesNotExist, Version=255.255.255.255") + }; + } + + if + ( + String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC\mscorlib.dll", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return new AssemblyNameExtension[] + { + }; + } + + if (String.Compare(path, @"MyRelativeAssembly.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + }; + } + + if (String.Compare(path, @"c:\MyApp\DependsOnSimpleA.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("A, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress312873\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("A, Version=0.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress339786\FolderA\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("C, Version=1.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress339786\FolderB\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("C, Version=2.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\Regress317975\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("B, Version=1.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\myassemblies\My.Assembly.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=2.0.0.0, Culture=NEUtraL, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\MyComponents\MyGrid.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, VeRsIon=2.0.0.0, Culture=neuTRal, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\MyRawDropControls\MyRawDropControl.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, VeRsIon=2.0.0.0, Culture=neuTRal, PublicKeyToken=b77a5c561934e089") + }; + } + + + if (String.Compare(path, @"c:\MyLibraries\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, Version=1.0.0.0, CuLtUrE=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + } + + if (String.Compare(path, @"c:\MyLibraries\t.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, VeRsIon=1.0.0.0, Culture=neutral, PublicKeyToken=bbbbbbbbbbbbbbbb") + }; + } + + if (String.Compare(path, @"c:\MyLibraries\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("D, Version=2.0.0.0, Culture=neutral, PuBlIcKeYToken=aaaaaaaaaaaaaaaa") + }; + } + + if (String.Compare(path, @"c:\MyLibraries\v1\d.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("E, VERSIOn=0.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\MyLibraries\v2\d.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("E, Version=0.0.0.0, Culture=neutRAL, PUblicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\MyLibraries\v1\E\E.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + }; + } + + if (String.Compare(path, @"C:\MyApp\v0.5\DependsOnWeaklyNamedUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("UnifyMe, Version=0.0.0.0, PUBLICKeyToken=null, CuLTURE=Neutral") + }; + } + + if (String.Compare(path, @"C:\MyApp\v1.0\DependsOnEverettSystem.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("System, VeRsiON=1.0.5000.0, Culture=neutral, PublicKeyToken="+AssemblyRef.EcmaPublicKey) + }; + } + + if (String.Compare(path, @"C:\MyApp\v0.5\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("UnifyMe, Version=0.5.0.0, CuLTUre=neUTral, PubLICKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\MyApp\v1.0\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("UNIFyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\MyApp\v2.0\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("UniFYme, Version=2.0.0.0, Culture=NeutraL, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\MyApp\v3.0\DependsOnUnified.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("UnIfyMe, Version=3.0.0.0, Culture=nEutral, PublicKEyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\MyProject\MyMissingAssembly.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + throw new FileNotFoundException(path); + } + + // Set up assembly names for testing target framework version checks + // Is version 4 and will only depends on 4.0 assemblies + if (String.Compare(path, @"c:\MyComponents\4.0Component\DependsOnOnlyv4Assemblies.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + // Is version 9 and will not have any dependencies, will be in the redist list + if (String.Compare(path, @"c:\MyComponents\misc\ReferenceVersion9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("mscorlib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyNameExtension("RandomAssembly, Version=9.0.0.0, Culture=neutral, PublicKeyToken=c77a5c561934e089") + }; + } + + // Is a third party assembly which depends on a version 9 assembly + if (String.Compare(path, @"c:\MyComponents\misc\DependsOn9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new AssemblyNameExtension("System.Data, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + //A second assembly which depends on version 9 framework assemblies. + if (String.Compare(path, @"c:\MyComponents\misc\DependsOn9Also.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\MyComponents\1.0\DependsOn9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\MyComponents\2.0\DependsOn9.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\Regress444809\C.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"), + new AssemblyNameExtension("A, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\Regress444809\B.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("A, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\Regress444809\D.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("A, Version=20.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\MyComponents\X.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("Z, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\MyComponents\Z.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] { }; + } + + if (String.Compare(path, @"c:\MyComponents\Y.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("Z, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + } + + if (String.Compare(path, @"c:\MyComponents\Microsoft.Build.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] { }; + } + + if (String.Compare(path, @"c:\MyComponents\DependsOnMSBuild12.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("Microsoft.Build, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") + }; + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("msCORlib, Version=2.0.0.0, Culture=NEutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("msCORlib, Version=2.0.0.0, Culture=NEutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"c:\WINNT\Microsoft.NET\Framework\v9.0.MyVersion\System.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("msCORlib, Version=2.0.0.0, Culture=NEutral, PublicKeyToken=b77a5c561934e089") + }; + } + + if (String.Compare(path, @"C:\DirectoryContainsOnlyDll\a.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("C, Version=1.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\DirectoryContainsdllAndWinmd\b.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("C, Version=1.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\DirectoryContainsdllAndWinmd\c.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[0]; + } + + if (String.Compare(path, @"C:\DirectoryContainstwoWinmd\a.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("C, Version=1.0.0.0, PublickEyToken=null, Culture=Neutral") + }; + } + + if (String.Compare(path, @"C:\DirectoryContainstwoWinmd\c.winmd", StringComparison.OrdinalIgnoreCase) == 0) + { + return new AssemblyNameExtension[0]; + } + + if (path.StartsWith(@"C:\FakeSDK\", StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension[0]; + } + + if (String.Compare(path, @"C:\SystemRuntime\Portable.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // Simulate a strongly named assembly. + return new AssemblyNameExtension[] + { + GetAssemblyName(@"C:\SystemRuntime\System.Runtime.dll") + }; + } + + + // Use a default list. + return new AssemblyNameExtension[] + { + new AssemblyNameExtension("SysTem, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77A5c561934e089"), + new AssemblyNameExtension("msCORlib, Version=2.0.0.0, Culture=NEutral, PublicKeyToken=b77a5c561934e089") + }; + } + + /// + /// Registry access delegate. Given a hive and a view, return the registry base key. + /// + private static RegistryKey GetBaseKey(RegistryHive hive, RegistryView view) + { + if (hive == RegistryHive.CurrentUser) + { + return Registry.CurrentUser; + } + else if (hive == RegistryHive.LocalMachine) + { + return Registry.LocalMachine; + } + + return null; + } + + /// + /// Simplified registry access delegate. Given a baseKey and a subKey, get all of the subkey + /// names. + /// + /// The base registry key. + /// The subkey + /// An enumeration of strings. + private static IEnumerable GetRegistrySubKeyNames(RegistryKey baseKey, string subKey) + { + if (baseKey == Registry.CurrentUser) + { + if (String.Compare(subKey, @"Software\Regress714052", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\X86", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\MSIL", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\Mix", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\Mix\Mix", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\None", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\X86\X86", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\MSIL\MSIL", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\None\None", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "", "vBogusVersion", "v1.a.2.3", "v1.0", "v3.0", "v2.0.50727", "v2.0.x86chk", "RandomJunk" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "ZControlA", "ZControlB", "Infragistics.GridControl.1.0", "Infragistics.MyHKLMControl.1.0", "Infragistics.MyControlWithFutureTargetNDPVersion.1.0", "Infragistics.MyControlWithPastTargetNDPVersion.1.0", "Infragistics.MyControlWithServicePack.1.0" }; + } + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.x86chk\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "RawDropControls" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v3.0\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Infragistics.MyControlWithFutureTargetNDPVersion.1.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v1.0\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Infragistics.MyNDP1Control.1.0", "Infragistics.MyControlWithPastTargetNDPVersion.1.0" }; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.GridControl.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithFutureTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyHKLMControl.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v3.0\AssemblyFoldersEx\Infragistics.MyControlWithFutureTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v1.0\AssemblyFoldersEx\Infragistics.MyNDP1Control.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithPastTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v1.0\AssemblyFoldersEx\Infragistics.MyControlWithPastTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.x86chk\AssemblyFoldersEx\RawDropControls", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\ZControlA", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\ZControlB", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return new string[] { }; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithServicePack.1.0", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + // This control has a service pack + return new string[] { "sp1", "sp2" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "v2.0.3600" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework\v2.0.3600", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "PocketPC" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "AFETestDeviceControl" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "1234" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\Microsoft SDKs", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Windows" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\Microsoft SDKs\Windows", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "7.0", "8.0", "v8.0", "9.0" }; + } + } + + if (baseKey == Registry.LocalMachine) + { + if (String.Compare(subKey, @"Software\Regress714052", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "v2.0.0" }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "A", "B" }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\AssemblyFoldersEx\A", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\AssemblyFoldersEx\B", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\X86", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "X86" }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\MSIL", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "MSIL" }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\None", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "None" }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\Mix", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Mix" }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\Mix\Mix", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\X86\X86", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\MSIL\MSIL", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\None\None", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "vBogusVersion", "v2.0.50727" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "Infragistics.FancyControl.1.0", "Infragistics.MyHKLMControl.1.0" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.FancyControl.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyHKLMControl.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "v2.0.3600" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework\v2.0.3600", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "PocketPC" }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { }; + } + + if (String.Compare(subKey, @"Software\Microsoft\Microsoft SDKs\Windows", StringComparison.OrdinalIgnoreCase) == 0) + { + return new string[] { "8.0" }; + } + } + + Console.WriteLine("subKey={0}", subKey); + Assert.Fail("New GetRegistrySubKeyNames parameters encountered, need to add unittesting support"); + return null; + } + + /// + /// Simplified registry access delegate. Given a baseKey and subKey, get the default value + /// of the subKey. + /// + /// The base registry key. + /// The subkey + /// A string containing the default value. + private static string GetRegistrySubKeyDefaultValue(RegistryKey baseKey, string subKey) + { + if (baseKey == Registry.CurrentUser) + { + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\ZControlA", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponentsA"; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\ZControlB", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponentsB"; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.GridControl.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponents"; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.x86chk\AssemblyFoldersEx\RawDropControls", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyRawDropControls"; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyHKLMControl.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponents\HKCU Components"; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v3.0\AssemblyFoldersEx\Infragistics.MyControlWithFutureTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponents\v3.0"; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithFutureTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithPastTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithServicePack.1.0", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return @"C:\MyComponents\v2.0"; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithServicePack.1.0", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return @"C:\MyComponentBase"; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithServicePack.1.0\sp1", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return @"C:\MyComponentServicePack1"; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyControlWithServicePack.1.0\sp2", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return @"C:\MyComponentServicePack2"; + } + + if + ( + String.Compare(subKey, @"Software\Microsoft\.NetFramework\v1.0\AssemblyFoldersEx\Infragistics.MyNDP1Control.1.0", StringComparison.OrdinalIgnoreCase) == 0 + || String.Compare(subKey, @"Software\Microsoft\.NetFramework\v1.0\AssemblyFoldersEx\Infragistics.MyControlWithPastTargetNDPVersion.1.0", StringComparison.OrdinalIgnoreCase) == 0 + ) + { + return @"C:\MyComponents\v1.0"; + } + + if (String.Compare(subKey, @"SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\V1Control"; + } + if (String.Compare(subKey, @"SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl\1234", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\V1ControlSP1"; + } + } + + if (baseKey == Registry.LocalMachine) + { + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.FancyControl.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponents\HKLM Components"; + } + + if (String.Compare(subKey, @"Software\Microsoft\.NetFramework\v2.0.50727\AssemblyFoldersEx\Infragistics.MyHKLMControl.1.0", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\MyComponents\HKLM Components"; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\AssemblyFoldersEx\B", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\X86"; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\AssemblyFoldersEx\A", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\MSIL"; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\X86\X86", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\X86"; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\Mix\Mix", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\Mix"; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\MSIL\MSIL", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\MSIL"; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\X86\X86", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\X86"; + } + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\MSIL\MSIL", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\MSIL"; + } + + if (String.Compare(subKey, @"Software\Regress714052\v2.0.0\None\None", StringComparison.OrdinalIgnoreCase) == 0) + { + return @"C:\Regress714052\None"; + } + } + + Console.WriteLine("subKey={0}", subKey); + Assert.Fail("New GetRegistrySubKeyDefaultValue parameters encountered, need to add unittesting support"); + return null; + } + + /// + /// Delegate for System.IO.File.GetLastWriteTime + /// + /// The file name + /// The last write time. + private static DateTime GetLastWriteTime(string path) + { + return DateTime.FromOADate(0.0); + } + + /// + /// Assert that two strings are equal without regard to case. + /// + /// The expected string. + /// The actual string. + internal protected static void AssertNoCase(string expected, string actual) + { + if (0 != String.Compare(expected, actual, StringComparison.OrdinalIgnoreCase)) + { + string message = String.Format("Expected value '{0}' but received '{1}'", expected, actual); + Console.WriteLine(message); + Assert.Fail(message); + } + } + + /// + /// Assert that two strings are equal without regard to case. + /// + /// The expected string. + /// The actual string. + internal protected static void AssertNoCase(string message, string expected, string actual) + { + if (0 != String.Compare(expected, actual, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(message); + Assert.Fail(message); + } + } + + /// + /// Write out an appConfig file. + /// Return the filename that was written. + /// + /// + /// + protected static string WriteAppConfig(string redirects) + { + string appConfigContents = + "\n" + + " \n" + + redirects + + " \n" + + ""; + + string appConfigFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(appConfigFile, appConfigContents); + return appConfigFile; + } + + /// + /// Determines whether the given item array has an item with the given spec. + /// + /// The item array. + /// The spec to search for. + /// True if the spec was found. + protected static bool ContainsItem(ITaskItem[] items, string spec) + { + foreach (ITaskItem item in items) + { + if (0 == String.Compare(item.ItemSpec, spec, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + /// + /// Execute the task. + /// + /// + /// NOTE! This test is not in fact completely isolated from its environment: it is reading the real redist lists. + /// + protected static bool Execute(ResolveAssemblyReference t) + { + return Execute(t, true); + } + + /// + /// Execute the task. Without confirming that the number of files resolved with and without find dependencies is identical. + /// This is becasue profiles could cause the number of primary references to be different. + /// + protected static bool Execute(ResolveAssemblyReference t, bool buildConsistencyCheck) + { + string tempPath = Path.GetTempPath(); + string redistListPath = Path.Combine(tempPath, Guid.NewGuid() + ".xml"); + string rarCacheFile = Path.Combine(tempPath, Guid.NewGuid() + ".RarCache"); + s_existentFiles.Add(rarCacheFile); + + bool succeeded = false; + + try + { + // Set the InstalledAssemblyTables parameter. + if (t.InstalledAssemblyTables.Length == 0) + { + File.WriteAllText(redistListPath, REDISTLIST); + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + } + + // First, run it in loading-a-project mode. + t.Silent = true; + t.FindDependencies = false; + t.FindSatellites = false; + t.FindSerializationAssemblies = false; + t.FindRelatedFiles = false; + t.StateFile = null; + t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader); + + // A few checks. These should always be true or it may be a perf issue for project load. + ITaskItem[] loadModeResolvedFiles = new TaskItem[0]; + if (t.ResolvedFiles != null) + { + loadModeResolvedFiles = (ITaskItem[])t.ResolvedFiles.Clone(); + } + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, t.SatelliteFiles.Length); + Assert.AreEqual(0, t.RelatedFiles.Length); + Assert.AreEqual(0, t.SuggestedRedirects.Length); + Assert.IsTrue(t.FilesWritten.Length == 0); + + // Now, run it in building-a-project mode. + MockEngine e = (MockEngine)t.BuildEngine; + e.Warnings = 0; + e.Errors = 0; + e.Log = ""; + t.Silent = false; + t.FindDependencies = true; + t.FindSatellites = true; + t.FindSerializationAssemblies = true; + t.FindRelatedFiles = true; + string cache = rarCacheFile; + t.StateFile = cache; + File.Delete(t.StateFile); + succeeded = t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader); + if (FileUtilities.FileExistsNoThrow(t.StateFile)) + { + Assert.IsTrue(t.FilesWritten.Length == 1); + Assert.IsTrue(t.FilesWritten[0].ItemSpec.Equals(cache, StringComparison.OrdinalIgnoreCase)); + } + + File.Delete(t.StateFile); + + if (buildConsistencyCheck) + { + // Some consistency checks between load mode and build mode. + Assert.AreEqual(loadModeResolvedFiles.Length, t.ResolvedFiles.Length); + for (int i = 0; i < loadModeResolvedFiles.Length; i++) + { + Assert.AreEqual(loadModeResolvedFiles[i].ItemSpec, t.ResolvedFiles[i].ItemSpec); + Assert.AreEqual(loadModeResolvedFiles[i].GetMetadata("CopyLocal"), t.ResolvedFiles[i].GetMetadata("CopyLocal")); + Assert.AreEqual(loadModeResolvedFiles[i].GetMetadata("ResolvedFrom"), t.ResolvedFiles[i].GetMetadata("ResolvedFrom")); + } + } + + // Check attributes on resolve files. + for (int i = 0; i < t.ResolvedFiles.Length; i++) + { + // OriginalItemSpec attribute on resolved items is to support VS in figuring out which + // project file reference caused a particular resolved file. + string originalItemSpec = t.ResolvedFiles[i].GetMetadata("OriginalItemSpec"); + Assert.IsTrue + ( + ContainsItem(t.Assemblies, originalItemSpec) || ContainsItem(t.AssemblyFiles, originalItemSpec), + "Expected to find OriginalItemSpec in Assemblies or AssemblyFiles task parameters" + ); + } + } + finally + { + s_existentFiles.Remove(rarCacheFile); + if (File.Exists(redistListPath)) + { + FileUtilities.DeleteNoThrow(redistListPath); + } + + if (File.Exists(rarCacheFile)) + { + FileUtilities.DeleteNoThrow(rarCacheFile); + } + } + return succeeded; + } + + /// + /// Helper method which allows tests to specify additional assembly search paths. + /// + /// + internal void ExecuteRAROnItemsAndRedist(ResolveAssemblyReference t, MockEngine e, ITaskItem[] items, string redistString, bool consistencyCheck) + { + ExecuteRAROnItemsAndRedist(t, e, items, redistString, consistencyCheck, null); + } + + /// + /// Helper method to get rid of some of the code duplication + /// + internal void ExecuteRAROnItemsAndRedist(ResolveAssemblyReference t, MockEngine e, ITaskItem[] items, string redistString, bool consistencyCheck, List additionalSearchPaths) + { + t.BuildEngine = e; + List searchPaths = new List(DefaultPaths); + + if (additionalSearchPaths != null) + { + searchPaths.AddRange(additionalSearchPaths); + } + + t.Assemblies = items; + t.SearchPaths = searchPaths.ToArray(); + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + redistString + ); + + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistFile) }; + + Execute(t, consistencyCheck); + } + finally + { + File.Delete(redistFile); + } + } + } + + namespace VersioningAndUnification.Prerequisite + { + [TestClass] + sealed public class StronglyNamedDependency : ResolveAssemblyReferenceTestFixture + { + /// + /// Return the default search paths. + /// + /// + new internal string[] DefaultPaths + { + get { return new string[] { @"C:\MyApp\v1.0", @"C:\Framework\Whidbey", @"C:\Framework\Everett" }; } + } + + /// + /// In this case, + /// - A single reference to DependsOnEverettSystem was passed in. + /// - This assembly depends on version 1.0.5000.0 of System.DLL. + /// - No app.config is passed in. + /// - Version 1.0.5000.0 of System.dll exists. + /// - Whidbey Version of System.dll exists. + /// Expected: + /// - The resulting System.dll returned should be Whidbey version. + /// Rationale: + /// We automatically unify FX dependencies. + /// + [TestMethod] + public void Exists() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing VersioningAndUnification.Prerequisite.StronglyNamedDependency.Exists() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnEverettSystem, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=feedbeadbadcadbe") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + AssertNoCase + ( + "System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=" + AssemblyRef.EcmaPublicKey, t.ResolvedDependencyFiles[0].GetMetadata("FusionName") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByFrameworkRetarget"), "1.0.5000.0", @"C:\MyApp\v1.0\DependsOnEverettSystem.dll") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.NotCopyLocalBecausePrerequisite")) + ); + + AssertNoCase("false", t.ResolvedDependencyFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// In this case, + /// - A single reference to DependsOnEverettSystem was passed in. + /// - This assembly depends on version 1.0.5000.0 of System.DLL. + /// - No app.config is passed in. + /// - Version 1.0.5000.0 of System.dll exists. + /// - Whidbey Version of System.dll *does not* exist. + /// Expected: + /// - This should be an unresolved reference, we shouldn't fallback to the old version. + /// Rationale: + /// The fusion loader is going to want to respect the unified-to assembly. There's no point in + /// feeding it the wrong version, and the drawback is that builds would be different from + /// machine-to-machine. + /// + [TestMethod] + public void HighVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnEverettSystem, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=feedbeadbadcadbe") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = new string[] { @"C:\MyApp\v1.0", @"C:\Framework\Everett" }; ; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByFrameworkRetarget"), "1.0.5000.0", @"C:\MyApp\v1.0\DependsOnEverettSystem.dll") + ); + } + + [TestMethod] + public void VerifyAssemblyPulledOutOfFrameworkDoesntGetFrameworkFileAttribute() + { + MockEngine e = new MockEngine(); + + string actualFrameworkDirectory = @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion"; + string alternativeFrameworkDirectory = @"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion"; + + ITaskItem[] items = new TaskItem[] { new TaskItem(Path.Combine(actualFrameworkDirectory, "System.dll")) }; + + // Version and directory match framework - it is a framework assembly + string redistString1 = "" + + "" + + ""; + + ResolveAssemblyReference t1 = new ResolveAssemblyReference(); + t1.TargetFrameworkVersion = "v4.5"; + t1.TargetFrameworkDirectories = new string[] { actualFrameworkDirectory }; + ExecuteRAROnItemsAndRedist(t1, e, items, redistString1, true, new List() { "{RawFileName}" }); + + Assert.IsTrue(!String.IsNullOrEmpty(t1.ResolvedFiles[0].GetMetadata("FrameworkFile"))); + + // Higher version than framework, but directory matches - it is a framework assembly + string redistString2 = "" + + "" + + ""; + + ResolveAssemblyReference t2 = new ResolveAssemblyReference(); + t2.TargetFrameworkVersion = "v4.5"; + t2.TargetFrameworkDirectories = new string[] { actualFrameworkDirectory }; + ExecuteRAROnItemsAndRedist(t2, e, items, redistString2, true, new List() { "{RawFileName}" }); + + Assert.IsTrue(!String.IsNullOrEmpty(t2.ResolvedFiles[0].GetMetadata("FrameworkFile"))); + + // Version is lower but directory does not match - it is a framework assembly + string redistString3 = "" + + "" + + ""; + + ResolveAssemblyReference t3 = new ResolveAssemblyReference(); + t3.TargetFrameworkVersion = "v4.5"; + t3.TargetFrameworkDirectories = new string[] { alternativeFrameworkDirectory }; + ExecuteRAROnItemsAndRedist(t3, e, items, redistString3, true, new List() { "{RawFileName}" }); + + Assert.IsTrue(!String.IsNullOrEmpty(t3.ResolvedFiles[0].GetMetadata("FrameworkFile"))); + + // Version is higher and directory does not match - this assembly has been pulled out of .NET + string redistString4 = "" + + "" + + ""; + + ResolveAssemblyReference t4 = new ResolveAssemblyReference(); + t4.TargetFrameworkVersion = "v4.5"; + t4.TargetFrameworkDirectories = new string[] { alternativeFrameworkDirectory }; + ExecuteRAROnItemsAndRedist(t4, e, items, redistString4, true, new List() { "{RawFileName}" }); + + Assert.IsTrue(String.IsNullOrEmpty(t4.ResolvedFiles[0].GetMetadata("FrameworkFile"))); + } + } + } + + namespace VersioningAndUnification.AppConfig + { + [TestClass] + sealed public class FilePrimary : ResolveAssemblyReferenceTestFixture + { + /// + /// In this case, + /// - A single primary file reference to assembly version 1.0.0.0 was passed in. + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void Exists() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing VersioningAndUnification.AppConfig.FilePrimary.Exists() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + + /// + /// Test the case where the appconfig has a malformed binding redirect version. + /// + [TestMethod] + public void BadAppconfigOldVersion() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + ); + + try + { + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsFalse(succeeded); + engine.AssertLogContains("MSB3249"); + } + finally + { + if (File.Exists(appConfigFile)) + { + // Cleanup. + File.Delete(appConfigFile); + } + } + } + + /// + /// Test the case where the appconfig has a malformed binding redirect version. + /// + [TestMethod] + public void BadAppconfigNewVersion() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + ); + + try + { + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsFalse(succeeded); + engine.AssertLogContains("MSB3249"); + } + finally + { + if (File.Exists(appConfigFile)) + { + // Cleanup. + File.Delete(appConfigFile); + } + } + } + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 1.0.0.0 of UnifyMe. + /// - An app.config was passed in that promotes UnifyMe version from 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// -Version 2.0.0.0 of UnifyMe is in the Black List + /// Expected: + /// - There should be a warning indicating that DependsOnUnified has a dependency UnifyMe 2.0.0.0 which is not in a TargetFrameworkSubset. + /// - There will be no unified message. + /// Rationale: + /// Strongly named dependencies should unify according to the bindingRedirects in the app.config, if the unified version is in the black list it should be removed and warned. + /// + [TestMethod] + public void ExistsPromotedDependencyInTheBlackList() + { + string implicitRedistListContents = + "" + + "" + + ""; + + string engineOnlySubset = + "" + + "" + + ""; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + string appConfigFile = null; + try + { + File.WriteAllText(redistListPath, implicitRedistListContents); + File.WriteAllText(subsetListPath, engineOnlySubset); + + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new TaskItem[] { new TaskItem(subsetListPath) }; + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + engine.AssertLogDoesntContain + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "1.0.0.0", appConfigFile, @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + } + finally + { + File.Delete(redistListPath); + File.Delete(subsetListPath); + + // Cleanup. + File.Delete(appConfigFile); + } + } + + /// + /// In this case, + /// - A single primary file reference to assembly version 1.0.0.0 was passed in. + /// - An app.config was passed in that promotes a *different* assembly version name from + // 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// - The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// One entry in the app.config file should not be able to impact the mapping of an assembly + /// with a different name. + /// + [TestMethod] + public void ExistsDifferentName() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary file reference to assembly version 1.0.0.0 was passed in. + /// - An app.config was passed in that promotes assembly version from range 0.0.0.0-1.5.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// - The resulting assembly returned should be 2.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void ExistsOldVersionRange() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary file reference to assembly version 1.0.0.0 was passed in. + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 4.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 4.0.0.0 of the file *does not* exist. + /// Expected: + /// -- The resulting assembly returned should be 2.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void HighVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary file reference to assembly version 0.5.0.0 was passed in. + /// - An app.config was passed in that promotes assembly version from 0.0.0.0-2.0.0.0 to 2.0.0.0 + /// - Version 0.5.0.0 of the file *does not* exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 2.0.0.0. + /// Rationale: + /// There's no way for the resolve algorithm to determine that the file reference corresponds + /// to a particular AssemblyName. Because of this, there's no way to determine that we want to + /// promote from 0.5.0.0 to 2.0.0.0. In this case, just use the assembly name that was passed in. + /// + [TestMethod] + public void LowVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v0.5\UnifyMe.dll") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(t.ResolvedFiles[0].ItemSpec, assemblyFiles[0].ItemSpec); + + + // Cleanup. + File.Delete(appConfigFile); + } + } + + [TestClass] + sealed public class SpecificVersionPrimary : ResolveAssemblyReferenceTestFixture + { + /// + /// In this case, + /// - A single primary version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// - The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void Exists() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing VersioningAndUnification.Prerequisite.SpecificVersionPrimary.Exists() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "true"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + AssertNoCase(@"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes a *different* assembly version name from + // 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void ExistsDifferentName() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "true"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes assembly version from range 0.0.0.0-1.5.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + /// + [TestMethod] + public void ExistsOldVersionRange() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "true"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 4.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 4.0.0.0 of the file *does not* exist. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void HighVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "true"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary version-strict reference was passed in to assembly version 0.5.0.0 + /// - An app.config was passed in that promotes assembly version from 0.0.0.0-2.0.0.0 to 2.0.0.0 + /// - Version 0.5.0.0 of the file *does not* exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// - The reference is not resolved. + /// Rationale: + /// Primary references are never unified--even those that don't exist on disk. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void LowVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=0.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "true"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedFiles.Length); + + // Cleanup. + File.Delete(appConfigFile); + } + } + + [TestClass] + sealed public class NonSpecificVersionStrictPrimary : ResolveAssemblyReferenceTestFixture + { + /// + /// Return the default search paths. + /// + /// + new internal string[] DefaultPaths + { + get { return new string[] { @"C:\MyComponents\v0.5", @"C:\MyComponents\v1.0", @"C:\MyComponents\v2.0", @"C:\MyComponents\v3.0" }; } + } + + + /// + /// In this case, + /// - A single primary non-version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// - The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void Exists() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "false"); + + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + + + /// + /// In this case, + /// - A single primary non-version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes a *different* assembly version name from + // 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// One entry in the app.config file should not be able to impact the mapping of an assembly + /// with a different name. + /// + [TestMethod] + public void ExistsDifferentName() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "false"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + + /// + /// In this case, + /// - A single primary non-version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes assembly version from range 0.0.0.0-1.5.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void ExistsOldVersionRange() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "false"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary non-version-strict reference was passed in to assembly version 1.0.0.0 + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 4.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 4.0.0.0 of the file *does not* exist. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// Primary references are never unified. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void HighVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "false"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single primary non-version-strict reference was passed in to assembly version 0.5.0.0 + /// - An app.config was passed in that promotes assembly version from 0.0.0.0-2.0.0.0 to 2.0.0.0 + /// - Version 0.5.0.0 of the file *does not* exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0 (remember this is non-version-strict) + /// Rationale: + /// Primary references are never unified--even those that don't exist on disk. This is because: + /// (a) The user expects that a primary reference will be respected. + /// (b) When FindDependencies is false and AutoUnify is true, we'd have to find all + /// dependencies anyway to make things work consistently. This would be a significant + /// perf hit when loading large solutions. + /// + [TestMethod] + public void LowVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("UnifyMe, Version=0.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + assemblyNames[0].SetMetadata("SpecificVersion", "false"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + } + + [TestClass] + sealed public class StronglyNamedDependency : ResolveAssemblyReferenceTestFixture + { + /// + /// Return the default search paths. + /// + /// + new internal string[] DefaultPaths + { + get { return new string[] { @"C:\MyApp\v0.5", @"C:\MyApp\v1.0", @"C:\MyComponents\v0.5", @"C:\MyComponents\v1.0", @"C:\MyComponents\v2.0", @"C:\MyComponents\v3.0" }; } + } + + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 1.0.0.0 of UnifyMe. + /// - An app.config was passed in that promotes UnifyMe version from 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// Expected: + /// - The resulting UnifyMe returned should be 2.0.0.0. + /// Rationale: + /// Strongly named dependencies should unify according to the bindingRedirects in the app.config. + /// + [TestMethod] + public void Exists() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + AssertNoCase("UnifyMe, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "1.0.0.0", appConfigFile, @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 1.0.0.0 of UnifyMe. + /// - An app.config was passed in that promotes UnifyMe version from 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// -Version 2.0.0.0 of UnifyMe is in the Black List + /// Expected: + /// - There should be a warning indicating that DependsOnUnified has a dependency UnifyMe 2.0.0.0 which is not in a TargetFrameworkSubset. + /// - There will be no unified message. + /// Rationale: + /// Strongly named dependencies should unify according to the bindingRedirects in the app.config, if the unified version is in the black list it should be removed and warned. + /// + [TestMethod] + public void ExistsPromotedDependencyInTheBlackList() + { + string engineOnlySubset = + "" + + "" + + ""; + + string implicitRedistListContents = + "" + + "" + + ""; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + string appConfigFile = null; + try + { + File.WriteAllText(redistListPath, implicitRedistListContents); + File.WriteAllText(subsetListPath, engineOnlySubset); + + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new TaskItem[] { new TaskItem(subsetListPath) }; + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t, false); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + engine.AssertLogDoesntContain + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "1.0.0.0", appConfigFile, @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + } + finally + { + File.Delete(redistListPath); + File.Delete(subsetListPath); + + // Cleanup. + File.Delete(appConfigFile); + } + } + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 1.0.0.0 of UnifyMe. + /// - An app.config was passed in that promotes a *different* assembly version name from + // 1.0.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 1.0.0.0. + /// Rationale: + /// An unrelated bindingRedirect in the app.config should have no bearing on unification + /// of another file. + /// + [TestMethod] + public void ExistsDifferentName() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + + // Cleanup. + File.Delete(appConfigFile); + } + + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 1.0.0.0 of UnifyMe. + /// - An app.config was passed in that promotes assembly version from range 0.0.0.0-1.5.0.0 to 2.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// -- The resulting assembly returned should be 2.0.0.0. + /// Rationale: + /// Strongly named dependencies should unify according to the bindingRedirects in the app.config, even + /// if a range is involved. + /// + [TestMethod] + public void ExistsOldVersionRange() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + AssertNoCase("UnifyMe, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "1.0.0.0", appConfigFile, @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + + // Cleanup. + File.Delete(appConfigFile); + } + + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 1.0.0.0 of UnifyMe. + /// - An app.config was passed in that promotes assembly version from 1.0.0.0 to 4.0.0.0 + /// - Version 1.0.0.0 of the file exists. + /// - Version 4.0.0.0 of the file *does not* exist. + /// Expected: + /// - The dependent assembly should be unresolved. + /// Rationale: + /// The fusion loader is going to want to respect the app.config file. There's no point in + /// feeding it the wrong version. + /// + [TestMethod] + public void HighVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + string shouldContain; + + string code = t.Log.ExtractMessageCode + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.FailedToResolveReference"), + String.Format(AssemblyResources.GetString("General.CouldNotLocateAssembly"), "UNIFyMe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")), + out shouldContain + ); + + + engine.AssertLogContains + ( + shouldContain + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "1.0.0.0", appConfigFile, @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnifiedDependency"), "UNIFyMe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + ); + + + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - A single reference to DependsOnUnified was passed in. + /// - This assembly depends on version 0.5.0.0 of UnifyMe. + /// - An app.config was passed in that promotes assembly version from 0.0.0.0-2.0.0.0 to 2.0.0.0 + /// - Version 0.5.0.0 of the file *does not* exists. + /// - Version 2.0.0.0 of the file exists. + /// Expected: + /// - The resulting assembly returned should be 2.0.0.0. + /// Rationale: + /// The lower (unified-from) version need not exist on disk (in fact we shouldn't even try to + /// resolve it) in order to arrive at the correct answer. + /// + [TestMethod] + public void LowVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=0.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + AssertNoCase("UnifyMe, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "0.5.0.0", appConfigFile, @"C:\MyApp\v0.5\DependsOnUnified.dll") + ); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - An app.config is passed in that has some garbage in the version number. + /// Expected: + /// - An error and task failure. + /// Rationale: + /// Can't proceed with a bad app.config. + /// + [TestMethod] + public void GarbageVersionInAppConfigFile() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + Assert.IsFalse(succeeded); + Assert.AreEqual(1, engine.Errors); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - An app.config is passed in that has a missing oldVersion in a bindingRedirect. + /// Expected: + /// - An error and task failure. + /// Rationale: + /// Can't proceed with a bad app.config. + /// + [TestMethod] + public void GarbageAppConfigMissingOldVersion() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + Assert.IsFalse(succeeded); + Assert.AreEqual(1, engine.Errors); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("AppConfig.BindingRedirectMissingOldVersion")) + ); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - An app.config is passed in that has a missing newVersion in a bindingRedirect. + /// Expected: + /// - An error and task failure. + /// Rationale: + /// Can't proceed with a bad app.config. + /// + [TestMethod] + public void GarbageAppConfigMissingNewVersion() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + Assert.IsFalse(succeeded); + Assert.AreEqual(1, engine.Errors); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("AppConfig.BindingRedirectMissingNewVersion")) + ); + + // Cleanup. + File.Delete(appConfigFile); + } + + + /// + /// In this case, + /// - An app.config is passed in that has some missing information in element. + /// Expected: + /// - An error and task failure. + /// Rationale: + /// Can't proceed with a bad app.config. + /// + [TestMethod] + public void GarbageAppConfigAssemblyNameMissingPKTAndCulture() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t); + Assert.IsFalse(succeeded); + Assert.AreEqual(1, engine.Errors); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - An app.config is specified + /// *and* + /// - AutoUnify=true. + /// Expected: + /// - Success. + /// Rationale: + /// With the introduction of the GenerateBindingRedirects task, RAR now accepts AutoUnify and App.Config at the same time. + /// + [TestMethod] + public void AppConfigSpecifiedWhenAutoUnifyEqualsTrue() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + t.AutoUnify = true; + + bool succeeded = Execute(t); + + // With the introduction of GenerateBindingRedirects task, RAR now accepts AutoUnify and App.Config at the same time. + Assert.IsTrue(succeeded); + Assert.AreEqual(0, engine.Errors); + + // Cleanup. + File.Delete(appConfigFile); + } + + /// + /// In this case, + /// - An app.config is specified, but the file doesn't exist. + /// Expected: + /// - An error and task failure. + /// Rationale: + /// App.config must exist if specifed. + /// + [TestMethod] + public void AppConfigDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = @"C:\MyNonexistentFolder\MyNonExistentApp.config"; + + bool succeeded = Execute(t); + Assert.IsFalse(succeeded); + Assert.AreEqual(1, engine.Errors); + } + } + } + + namespace VersioningAndUnification.AutoUnify + { + [TestClass] + sealed public class StronglyNamedDependency : ResolveAssemblyReferenceTestFixture + { + /// + /// Return the default search paths. + /// + /// + new internal string[] DefaultPaths + { + get { return new string[] { @"C:\MyApp\v0.5", @"C:\MyApp\v1.0", @"C:\MyApp\v2.0", @"C:\MyApp\v3.0", @"C:\MyComponents\v0.5", @"C:\MyComponents\v1.0", @"C:\MyComponents\v2.0", @"C:\MyComponents\v3.0" }; } + } + + + /// + /// In this case, + /// - Two references are passed in: + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 1.0.0.0. + /// - DependsOnUnified 2.0.0.0 depends on UnifyMe 2.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// Expected: + /// - There should be exactly one UnifyMe dependency returned and it should be version 2.0.0.0. + /// Rationale: + /// When AutoUnify is true, we need to resolve to the highest version of each particular assembly + /// dependency seen. + /// + [TestMethod] + public void Exists() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing VersioningAndUnification.AutoUnify.StronglyNamedDependency.Exists() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AutoUnify = true; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + AssertNoCase("UnifyMe, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + AssertNoCase(@"C:\MyComponents\v2.0\UnifyMe.dll", t.ResolvedDependencyFiles[0].ItemSpec); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnifiedDependency"), "UniFYme, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "1.0.0.0", @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + } + + /// + /// In this case, + /// - Two references are passed in: + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 1.0.0.0. + /// - DependsOnUnified 2.0.0.0 depends on UnifyMe 2.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// - DependsOnUnified 2.0.0.0 is on the black list. + /// Expected: + /// - There should be exactly one UnifyMe dependency returned and it should be version 1.0.0.0. + /// Rationale: + /// When AutoUnify is true, we need to resolve to the highest version of each particular assembly + /// dependency seen. However if the higher assembly is a dependency of an assembly in the black list it should not be considered during unification. + /// + [TestMethod] + public void ExistsWithPrimaryReferenceOnBlackList() + { + string implicitRedistListContents = + "" + + "" + + ""; + + string engineOnlySubset = + "" + + "" + + ""; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(redistListPath, implicitRedistListContents); + File.WriteAllText(subsetListPath, engineOnlySubset); + + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new TaskItem[] { new TaskItem(subsetListPath) }; + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AutoUnify = true; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles.Length == 1, "Expected there to only be one resolved file"); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains(@"C:\MyApp\v1.0\DependsOnUnified.dll"), "Expected the ItemSpec of the resolved file to be the item spec of the 1.0.0.0 assembly"); + Assert.IsTrue(t.ResolvedDependencyFiles.Length == 1, "Expected there to be two resolved dependencies"); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + AssertNoCase(@"C:\MyComponents\v1.0\UnifyMe.dll", t.ResolvedDependencyFiles[0].ItemSpec); + + engine.AssertLogDoesntContain + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnifiedDependency"), "UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL") + ); + + engine.AssertLogDoesntContain + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "1.0.0.0", @"C:\MyApp\v2.0\DependsOnUnified.dll") + ); + } + finally + { + File.Delete(redistListPath); + File.Delete(subsetListPath); + } + } + + + /// + /// In this case, + /// - Two references are passed in: + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 1.0.0.0. + /// - DependsOnUnified 2.0.0.0 depends on UnifyMe 2.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// - UnifyMe 2.0.0.0 is on the black list + /// Expected: + /// - There should be exactly one UnifyMe dependency returned and it should be version 1.0.0.0. + /// Also there should be a warning about the primary reference DependsOnUnified 2.0.0.0 having a dependency which was in the black list. + /// Rationale: + /// When AutoUnify is true, we need to resolve to the highest version of each particular assembly + /// dependency seen. However if the higher assembly is a dependency of an assembly in the black list it should not be considered during unification. + /// + [TestMethod] + public void ExistsPromotedDependencyInTheBlackList() + { + string implicitRedistListContents = + "" + + "" + + ""; + + string engineOnlySubset = + "" + + "" + + ""; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + string appConfigFile = null; + try + { + File.WriteAllText(redistListPath, implicitRedistListContents); + File.WriteAllText(subsetListPath, engineOnlySubset); + + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Construct the app.config. + appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new TaskItem[] { new TaskItem(subsetListPath) }; + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + bool succeeded = Execute(t, false); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + engine.AssertLogDoesntContain + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAppConfig"), "1.0.0.0", appConfigFile, @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + } + finally + { + File.Delete(redistListPath); + File.Delete(subsetListPath); + + // Cleanup. + File.Delete(appConfigFile); + } + } + + /// + /// In this case, + /// - Two references are passed in: + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 1.0.0.0. + /// - DependsOnUnified 2.0.0.0 depends on UnifyMe 2.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// - UnifyMe 2.0.0.0 is on the black list because it is higher than what is in the redist list, 1.0.0.0 is also in a black list because it is not in the subset but is in the redist list. + /// Expected: + /// - There should be no UnifyMe dependency returned + /// There should be a warning indicating the primary reference DependsOnUnified 1.0.0.0 has a dependency that in the black list + /// There should be a warning indicating the primary reference DependsOnUnified 2.0.0.0 has a dependency that in the black list + /// + [TestMethod] + public void ExistsWithBothDependentReferenceOnBlackList() + { + string implicitRedistListContents = + "" + + "" + + ""; + + string engineOnlySubset = + "" + + "" + + ""; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(redistListPath, implicitRedistListContents); + File.WriteAllText(subsetListPath, engineOnlySubset); + + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new TaskItem[] { new TaskItem(subsetListPath) }; + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AutoUnify = true; + + bool succeeded = Execute(t, false); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles.Length == 0, "Expected there to be no resolved files"); + + Assert.IsFalse(ContainsItem(t.ResolvedFiles, @"C:\MyApp\v1.0\DependsOnUnified.dll"), "Expected the ItemSpec of the resolved file to not be the item spec of the 1.0.0.0 assembly"); + Assert.IsFalse(ContainsItem(t.ResolvedFiles, @"C:\MyApp\v2.0\DependsOnUnified.dll"), "Expected the ItemSpec of the resolved file to not be the item spec of the 2.0.0.0 assembly"); + string stringList = ResolveAssemblyReference.GenerateSubSetName(null, new ITaskItem[] { new TaskItem(subsetListPath) }); + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", assemblyNames[0].ItemSpec, "UniFYme, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", stringList)); + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", assemblyNames[1].ItemSpec, "UniFYme, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "2.0.0.0", "1.0.0.0")); + } + finally + { + File.Delete(redistListPath); + File.Delete(subsetListPath); + } + } + + /// + /// In this case, + /// - Three references are passed in: + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 1.0.0.0. + /// - DependsOnUnified 2.0.0.0 depends on UnifyMe 2.0.0.0. + /// - DependsOnUnified 3.0.0.0 depends on UnifyMe 3.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// - Version 3.0.0.0 of UnifyMe exists. + /// - Vesion 3.0.0.0 of DependsOn is on black list + /// Expected: + /// - There should be exactly one UnifyMe dependency returned and it should be version 2.0.0.0. + /// - There should be messages saying that 2.0.0.0 was unified from 1.0.0.0. + /// Rationale: + /// AutoUnify works even when unifying multiple prior versions. + /// + [TestMethod] + public void MultipleUnifiedFromNamesMiddlePrimaryOnBlackList() + { + string implicitRedistListContents = + "" + + "" + + ""; + + string engineOnlySubset = + "" + + "" + + ""; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + File.WriteAllText(redistListPath, implicitRedistListContents); + File.WriteAllText(subsetListPath, engineOnlySubset); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new TaskItem[] { new TaskItem(subsetListPath) }; + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + t.AutoUnify = true; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles.Length == 2, "Expected to find two resolved assemblies"); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"C:\MyApp\v1.0\DependsOnUnified.dll"), "Expected the ItemSpec of the resolved file to be the item spec of the 1.0.0.0 assembly"); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"C:\MyApp\v2.0\DependsOnUnified.dll"), "Expected the ItemSpec of the resolved file to be the item spec of the 2.0.0.0 assembly"); + AssertNoCase("UnifyMe, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + AssertNoCase(@"C:\MyComponents\v2.0\UnifyMe.dll", t.ResolvedDependencyFiles[0].ItemSpec); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnifiedDependency"), "UniFYme, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "1.0.0.0", @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + + engine.AssertLogDoesntContain + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "2.0.0.0", @"C:\MyApp\v2.0\DependsOnUnified.dll") + ); + } + + /// + /// In this case, + /// - Two references are passed in: + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 1.0.0.0. + /// - DependsOnUnified 2.0.0.0 depends on UnifyMe 2.0.0.0. + /// - DependsOnUnified 3.0.0.0 depends on UnifyMe 2.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 1.0.0.0 of UnifyMe exists. + /// - Version 2.0.0.0 of UnifyMe exists. + /// - Version 3.0.0.0 of UnifyMe exists. + /// Expected: + /// - There should be exactly one UnifyMe dependency returned and it should be version 3.0.0.0. + /// - There should be messages saying that 3.0.0.0 was unified from 1.0.0.0 *and* 2.0.0.0. + /// Rationale: + /// AutoUnify works even when unifying multiple prior versions. + /// + [TestMethod] + public void MultipleUnifiedFromNames() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + t.AutoUnify = true; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + AssertNoCase("UnifyMe, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + AssertNoCase(@"C:\MyComponents\v3.0\UnifyMe.dll", t.ResolvedDependencyFiles[0].ItemSpec); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnifiedDependency"), "UniFYme, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "1.0.0.0", @"C:\MyApp\v1.0\DependsOnUnified.dll") + ); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "2.0.0.0", @"C:\MyApp\v2.0\DependsOnUnified.dll") + ); + } + + /// + /// In this case, + /// - Two references are passed in: + /// - DependsOnUnified 0.5.0.0 depends on UnifyMe 0.5.0.0. + /// - DependsOnUnified 1.0.0.0 depends on UnifyMe 2.0.0.0. + /// - The AutoUnify flag is set to 'true'. + /// - Version 0.5.0.0 of UnifyMe *does not* exist. + /// - Version 1.0.0.0 of UnifyMe exists. + /// Expected: + /// - There should be exactly one UnifyMe dependency returned and it should be version 1.0.0.0. + /// - There should be message saying that 1.0.0.0 was unified from 0.5.0.0 + /// Rationale: + /// AutoUnify works even when unifying prior versions that don't exist on disk. + /// + [TestMethod] + public void LowVersionDoesntExist() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("DependsOnUnified, Version=0.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("DependsOnUnified, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = DefaultPaths; + t.AutoUnify = true; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.ResolvedDependencyFiles[0].GetMetadata("FusionName")); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.UnificationByAutoUnify"), "0.5.0.0", @"C:\MyApp\v0.5\DependsOnUnified.dll") + ); + } + } + } + + /// + /// Unit tests for the ResolveAssemblyReference task. + /// + [TestClass] + sealed public class ReferenceTests : ResolveAssemblyReferenceTestFixture + { + /// + /// Check to make sure if, the specific version metadata is set on a primary reference, that true is returned from CheckForSpecificMetadataOnParent + /// + [TestMethod] + public void CheckForSpecificMetadataOnParent() + { + Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem = new TaskItem("TestReference"); + taskItem.SetMetadata("SpecificVersion", "true"); + reference.MakePrimaryAssemblyReference(taskItem, true, ".dll"); + Assert.IsTrue(reference.CheckForSpecificVersionMetadataOnParentsReference(false)); + } + + /// + /// Check to make sure if, the specific version metadata is set on all primary references which a dependency depends on, that true is returned from CheckForSpecificMetadataOnParent + /// + [TestMethod] + public void CheckForSpecificMetadataOnParentAllParentsHaveMetadata() + { + Reference primaryReference1 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem = new TaskItem("TestPrimary1"); + taskItem.SetMetadata("SpecificVersion", "true"); + primaryReference1.MakePrimaryAssemblyReference(taskItem, true, ".dll"); + primaryReference1.FullPath = "FullPath"; + + Reference primaryReference2 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem2 = new TaskItem("TestPrimary2"); + taskItem2.SetMetadata("SpecificVersion", "true"); + primaryReference2.MakePrimaryAssemblyReference(taskItem2, true, ".dll"); + primaryReference2.FullPath = "FullPath"; + + Reference dependentReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + dependentReference.FullPath = "FullPath"; + + dependentReference.MakeDependentAssemblyReference(primaryReference1); + dependentReference.MakeDependentAssemblyReference(primaryReference2); + + Assert.IsTrue(dependentReference.CheckForSpecificVersionMetadataOnParentsReference(false)); + } + + /// + /// Check to make sure if, the specific version metadata is set on some primary references which a dependency depends on, that false is returned from CheckForSpecificMetadataOnParent + /// + [TestMethod] + public void CheckForSpecificMetadataOnParentNotAllParentsHaveMetadata() + { + Reference primaryReference1 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem = new TaskItem("TestPrimary1"); + taskItem.SetMetadata("SpecificVersion", "false"); + primaryReference1.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + primaryReference1.FullPath = "FullPath"; + + Reference primaryReference2 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem2 = new TaskItem("TestPrimary2"); + taskItem2.SetMetadata("SpecificVersion", "true"); + primaryReference2.MakePrimaryAssemblyReference(taskItem2, true, ".dll"); + primaryReference2.FullPath = "FullPath"; + + Reference dependentReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + dependentReference.FullPath = "FullPath"; + + dependentReference.MakeDependentAssemblyReference(primaryReference1); + dependentReference.MakeDependentAssemblyReference(primaryReference2); + + Assert.IsFalse(dependentReference.CheckForSpecificVersionMetadataOnParentsReference(false), "Expected check to return false but it returned true."); + } + + /// + /// Check to make sure if, the specific version metadata is set on some primary references which a dependency depends on, that false is returned from CheckForSpecificMetadataOnParent + /// + [TestMethod] + public void CheckForSpecificMetadataOnParentNotAllParentsHaveMetadata2() + { + Reference primaryReference1 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem = new TaskItem("TestPrimary1"); + primaryReference1.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + primaryReference1.FullPath = "FullPath"; + + Reference primaryReference2 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem2 = new TaskItem("TestPrimary2"); + taskItem2.SetMetadata("SpecificVersion", "true"); + primaryReference2.MakePrimaryAssemblyReference(taskItem2, true, ".dll"); + primaryReference2.FullPath = "FullPath"; + + Reference dependentReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + dependentReference.FullPath = "FullPath"; + + dependentReference.MakeDependentAssemblyReference(primaryReference1); + dependentReference.MakeDependentAssemblyReference(primaryReference2); + + Assert.IsFalse(dependentReference.CheckForSpecificVersionMetadataOnParentsReference(false), "Expected check to return false but it returned true."); + } + + /// + /// Check to make sure if, the specific version metadata is set on some primary references which a dependency depends on, that true is returned from CheckForSpecificMetadataOnParent if the anyParentHasmetadata parameter is set to true. + /// + [TestMethod] + public void CheckForSpecificMetadataOnParentNotAllParentsHaveMetadata3() + { + Reference primaryReference1 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem = new TaskItem("TestPrimary1"); + taskItem.SetMetadata("SpecificVersion", "false"); + primaryReference1.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + primaryReference1.FullPath = "FullPath"; + + Reference primaryReference2 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + ITaskItem taskItem2 = new TaskItem("TestPrimary2"); + taskItem2.SetMetadata("SpecificVersion", "true"); + primaryReference2.MakePrimaryAssemblyReference(taskItem2, true, ".dll"); + primaryReference2.FullPath = "FullPath"; + + Reference dependentReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + dependentReference.FullPath = "FullPath"; + + dependentReference.MakeDependentAssemblyReference(primaryReference1); + dependentReference.MakeDependentAssemblyReference(primaryReference2); + + Assert.IsTrue(dependentReference.CheckForSpecificVersionMetadataOnParentsReference(true), "Expected check to return false but it returned true."); + } + } + + + /// + /// Test a few perf scenarios. + /// + [TestClass] + sealed public class Perf : ResolveAssemblyReferenceTestFixture + { + [TestMethod] + public void AutoUnifyUsesMinimumIO() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing Perf.AutoUnifyUsesMinimumIO() test"); + + // Manually instantiate a test fixture and run it. + VersioningAndUnification.AutoUnify.StronglyNamedDependency t = new VersioningAndUnification.AutoUnify.StronglyNamedDependency(); + t.StartIOMonitoring(); + t.Exists(); + t.StopIOMonitoringAndAssert_Minimal_IOUse(); + } + } + + + + /// + /// Unit test the cases where we need to determine if the target framework is greater than the current target framework through the use of the target framework attribute + /// + [TestClass] + sealed public class VerifyTargetFrameworkAttribute : ResolveAssemblyReferenceTestFixture + { + /// + /// Verify there are no warnings if the target framework identifier passed to rar and the target framework identifier in the dll do not match. + /// + [TestMethod] + public void FrameworksDoNotMatch() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnFoo4Framework"), + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "BAR, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "BAR"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\DependsOnFoo4Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if it is the same framework but we are a lower version. With a primary reference in the project. + /// + [TestMethod] + public void LowerVersionSameFrameworkDirect() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnFoo35Framework"), + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=v4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\DependsOnFoo35Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if it is the same framework and the same version and a direct reference + /// + [TestMethod] + public void SameVersionSameFrameworkDirect() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnFoo4Framework"), + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\DependsOnFoo4Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if the reference was built for a higher framework but specific version is true + /// + [TestMethod] + public void HigherVersionButSpecificVersionDirect() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOnFoo45Framework, Version=4.5.0.0, PublicKeyToken=null, Culture=Neutral"); + item.SetMetadata("SpecificVersion", "true"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\DependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if it is the same framework but we are a lower version. + /// + [TestMethod] + public void LowerVersionSameFrameworkInDirect() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("IndirectDependsOnFoo35Framework"), + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=v4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\IndirectDependsOnFoo35Framework.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Frameworks\DependsOnFoo35Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if it is the same framework and the same version. + /// + [TestMethod] + public void SameVersionSameFrameworkInDirect() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("IndirectDependsOnFoo4Framework"), + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\IndirectDependsOnFoo4Framework.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Frameworks\DependsOnFoo4Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if it is the same framework and a higher version but specific version is true. + /// + [TestMethod] + public void HigherVersionButSpecificVersionInDirect() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("IndirectDependsOnFoo45Framework, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral"); + item.SetMetadata("SpecificVersion", "true"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\IndirectDependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Frameworks\DependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are warnings if there is an indirect reference to a dll that is higher that what the current target framework is. + /// + [TestMethod] + public void HigherVersionInDirect() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("IndirectDependsOnFoo45Framework, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t, false); + + Assert.IsTrue(e.Warnings == 1, "One warning expected in this scenario."); + e.AssertLogContains("MSB3275"); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + } + + /// + /// Verify there are no warnings if there is an indirect reference to a dll that is higher that what the current target framework is but IgnoreFrameworkAttributeVersionMismatch is true. + /// + [TestMethod] + public void HigherVersionInDirectIgnoreMismatch() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("IndirectDependsOnFoo45Framework, Version=0.0.0.0, PublicKeyToken=null, Culture=Neutral"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + t.IgnoreTargetFrameworkAttributeVersionMismatch = true; + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\IndirectDependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Frameworks\DependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify there are no warnings if there is a direct reference to a dll that is higher that what the current target framework is but the property IgnoreFrameworkAttributeVersionMismatch is true. + /// + [TestMethod] + public void HigherVersionDirectIgnoreMismatch() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOnFoo45Framework"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + t.IgnoreTargetFrameworkAttributeVersionMismatch = true; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\DependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + } + + + /// + /// Verify there are warnings if there is a direct reference to a dll that is higher that what the current target framework is. + /// + [TestMethod] + public void HigherVersionDirect() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOnFoo45Framework"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Execute(t, false); + + Assert.IsTrue(e.Warnings == 1, "One warning expected in this scenario."); + e.AssertLogContains("MSB3274"); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + } + + /// + /// Verify there are no warnings if there is a direct reference to a dll that is higher that what the current target framework is but + /// find dependencies is false. This is because we do not want to add an extra read for this attribute during the project load phase. + /// which has dependencies set to false. A regular build or design time build has this set to true so we do the correct check. + /// + [TestMethod] + public void HigherVersionDirectDependenciesFalse() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOnFoo45Framework"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = e; + t.Assemblies = items; + t.FindDependencies = false; + t.TargetFrameworkMoniker = "Foo, Version=4.0"; + t.TargetFrameworkMonikerDisplayName = "Foo"; + t.SearchPaths = new string[] { @"c:\Frameworks\" }; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + + Assert.IsTrue(e.Warnings == 0, "No warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Frameworks\DependsOnFoo45Framework.dll"), "Expected to find assembly, but didn't."); + } + } + + /// + /// Unit test the cases where we need to determine if the target framework is greater than the current target framework + /// + [TestClass] + sealed public class VerifyTargetFrameworkHigherThanRedist : ResolveAssemblyReferenceTestFixture + { + /// + /// Verify there are no warnings when the assembly being resolved is not in the redist list and only has dependencies to references in the redist list with the same + /// version as is described in the redist list. + /// + [TestMethod] + public void TargetCurrentTargetFramework() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnOnlyv4Assemblies") + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOnOnlyv4Assemblies.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// ReferenceVersion9 depends on mscorlib 9. However the redist list only allows 4.0 since framework unification for dependencies only + /// allows upward unification this would result in a warning. Therefore we need to remap mscorlib 9 to 4.0 + /// + /// + [TestMethod] + public void RemapAssemblyBasic() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("ReferenceVersion9"), + new TaskItem("DependsOnOnlyv4Assemblies"), + new TaskItem("AnotherOne") + }; + + string redistString = "" + + "" + + "" + + "" + + " " + + " " + + "" + + " " + + " " + + "" + + " " + + " " + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 0, "Expected NO warning in this scenario."); + e.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.RemappedReference", "DependsOnOnlyv4Assemblies", "ReferenceVersion9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + e.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.RemappedReference", "AnotherOne", "ReferenceVersion9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata("OriginalItemSpec").Equals("AnotherOne", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"c:\MyComponents\misc\ReferenceVersion9.dll", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify an error is emitted when the reference itself is in the redist list but is a higher version that is described in the redist list. + /// In this case ReferenceVersion9 is version=9.0.0.0 but in the redist we show its highest version as 4.0.0.0. + /// + [TestMethod] + public void HigherThanHighestInRedistList() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("ReferenceVersion9") + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains("MSB3257"); + e.AssertLogContains("ReferenceVersion9"); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Verify that if the reference that is higher than the highest version in the redist list is an MSBuild assembly, we do + /// not warn -- this is a hack until we figure out how to properly deal with .NET assemblies being removed from the framework. + /// + [TestMethod] + public void HigherThanHighestInRedistListForMSBuildAssembly() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("Microsoft.Build") + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t1 = new ResolveAssemblyReference(); + t1.TargetFrameworkVersion = "v4.5"; + + ExecuteRAROnItemsAndRedist(t1, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 0, "Expected successful resolution with no warnings."); + e.AssertLogContains("Microsoft.Build.dll"); + Assert.AreEqual(1, t1.ResolvedFiles.Length); + + ResolveAssemblyReference t2 = new ResolveAssemblyReference(); + t2.TargetFrameworkVersion = "v4.0"; + + ExecuteRAROnItemsAndRedist(t2, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains("Microsoft.Build.dll"); + Assert.AreEqual(0, t2.ResolvedFiles.Length); + + ResolveAssemblyReference t3 = new ResolveAssemblyReference(); + t3.TargetFrameworkVersion = "v4.5"; + t3.UnresolveFrameworkAssembliesFromHigherFrameworks = true; + + ExecuteRAROnItemsAndRedist(t3, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains("Microsoft.Build.dll"); + Assert.AreEqual(1, t1.ResolvedFiles.Length); + } + + /// + /// Expect no warning from a 3rd party redist list since they are not considered for multi targeting warnings. + /// + [TestMethod] + public void HigherThanHighestInRedistList3rdPartyRedist() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("ReferenceVersion9") + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 0, "Expected one warning in this scenario."); + e.AssertLogDoesntContain("MSB3257"); + e.AssertLogContains("ReferenceVersion9"); + Assert.AreEqual(1, t.ResolvedFiles.Length); + } + + /// + /// Test the same case as above except for add the specific version metadata to ignore the warning. + /// + [TestMethod] + public void HigherThanHighestInRedistListWithSpecificVersionMetadata() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("ReferenceVersion9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089") + }; + + items[0].SetMetadata("SpecificVersion", "true"); + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + e.AssertLogDoesntContain("MSB3258"); + e.AssertLogDoesntContain("MSB3257"); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\ReferenceVersion9.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify the case where the assembly itself is not in the redist list but it depends on an assembly which is in the redist list and is a higher version that what is listed in the redist list. + /// In this case the assembly DependsOn9 depends on System 9.0.0.0 while the redist list only goes up to 4.0.0.0. + /// + [TestMethod] + public void DependenciesHigherThanHighestInRedistList() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOn9") + }; + + string redistString = "" + + "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 2, "Expected one warning in this scenario."); + e.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", "DependsOn9", "System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "9.0.0.0", "4.0.0.0")); + e.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", "DependsOn9", "System.Data, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "9.0.0.0", "4.0.0.0")); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Verify that if the reference that is higher than the highest version in the redist list is an MSBuild assembly, we do + /// not warn -- this is a hack until we figure out how to properly deal with .NET assemblies being removed from the framework. + /// + [TestMethod] + public void DependenciesHigherThanHighestInRedistListForMSBuildAssembly() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnMSBuild12") + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t1 = new ResolveAssemblyReference(); + t1.TargetFrameworkVersion = "v5.0"; + + ExecuteRAROnItemsAndRedist(t1, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 0, "Expected successful resolution with no warnings."); + e.AssertLogContains("DependsOnMSBuild12"); + e.AssertLogContains("Microsoft.Build.dll"); + Assert.AreEqual(1, t1.ResolvedFiles.Length); + + ResolveAssemblyReference t2 = new ResolveAssemblyReference(); + t2.TargetFrameworkVersion = "v4.0"; + + ExecuteRAROnItemsAndRedist(t2, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario"); + e.AssertLogContains("DependsOnMSBuild12"); + e.AssertLogContains("Microsoft.Build.dll"); + Assert.AreEqual(0, t2.ResolvedFiles.Length); + + ResolveAssemblyReference t3 = new ResolveAssemblyReference(); + //t2.TargetFrameworkVersion is null + + ExecuteRAROnItemsAndRedist(t3, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario"); + e.AssertLogContains("DependsOnMSBuild12"); + e.AssertLogContains("Microsoft.Build.dll"); + Assert.AreEqual(0, t3.ResolvedFiles.Length); + } + + /// + /// Make sure when specific version is set to true and the dependencies of the reference are a higher version than what is in the redist list do not warn, do not unresolve + /// + [TestMethod] + public void DependenciesHigherThanHighestInRedistListSpecificVersionMetadata() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089") + }; + + items[0].SetMetadata("SpecificVersion", "true"); + + string redistString = "" + + "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + e.AssertLogDoesntContain("MSB3258"); + e.AssertLogDoesntContain("MSB3257"); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify the case where two assemblies depend on an assembly which is in the redist list but has a higher version than what is described in the redist list. + /// DependsOn9 and DependsOn9Also both depend on System, Version=9.0.0.0 one of the items has the SpecificVersion metadata set. In this case + /// we expect to only see a warning from one of the assemblies. + /// + [TestMethod] + public void TwoDependenciesHigherThanHighestInRedistListIgnoreOnOne() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"), + new TaskItem("DependsOn9Also") + }; + + items[0].SetMetadata("SpecificVersion", "true"); + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", "DependsOn9Also", "System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "9.0.0.0", "4.0.0.0")); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to not find assembly, but did."); + Assert.IsFalse(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9Also.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify the case where two assemblies depend on an assembly which is in the redist list but has a higher version than what is described in the redist list. + /// DependsOn9 and DependsOn9Also both depend on System, Version=9.0.0.0. Both of the items has the specificVersion metadata set. In this case + /// we expect to only see no warnings from the assemblies. + /// + [TestMethod] + public void TwoDependenciesHigherThanHighestInRedistListIgnoreOnBoth() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"), + new TaskItem("DependsOn9Also, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089") + }; + + items[0].SetMetadata("SpecificVersion", "true"); + items[1].SetMetadata("SpecificVersion", "true"); + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + e.AssertLogDoesntContain("MSB3258"); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9Also.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Test the case where two assemblies with different versions but the same name depend on an assembly which is in the redist list but has a higher version than + /// what is described in the redist list. We expect two warnings because both assemblies are goign to be resolved even though one of them will not be copy local. + /// + [TestMethod] + public void TwoDependenciesSameNameDependOnHigherVersion() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOn9, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"), + new TaskItem("DependsOn9, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089") + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false); + + Assert.IsTrue(e.Warnings == 2, "Expected two warnings."); + e.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", "DependsOn9, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089", "System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "9.0.0.0", "4.0.0.0")); + e.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.DependencyReferenceOutsideOfFramework", "DependsOn9, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089", "System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "9.0.0.0", "4.0.0.0")); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Test the case where the project has two references, one of them has dependencies which are contained within the projects target framework + /// and there is another reference which has dependencies on a future framework (this is the light up scenario assembly). + /// + /// Make sure that if specific version is set on the lightup assembly that we do not unresolve it, and we also should not unify its dependencies. + /// + [TestMethod] + public void MixedDependenciesSpecificVersionOnHigherVersionMetadataSet() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnOnlyv4Assemblies, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"), + new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089") + }; + + items[1].SetMetadata("SpecificVersion", "true"); + + string redistString = "" + + "" + + ""; + + List additionalPaths = new List(); + additionalPaths.Add(@"c:\MyComponents\4.0Component\"); + additionalPaths.Add(@"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion"); + additionalPaths.Add(@"c:\WINNT\Microsoft.NET\Framework\v9.0.MyVersion\"); + + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false, additionalPaths); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(2, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\4.0Component\DependsOnOnlyv4Assemblies.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Test the case where the project has two references, one of them has dependencies which are contained within the projects target framework + /// and there is another reference which has dependencies on a future framework (this is the light up scenario assembly). + /// + /// Verify that if specific version is set on the other reference that we get the expected behavior: + /// Un resolve the light up assembly. + /// + [TestMethod] + public void MixedDependenciesSpecificVersionOnLowerVersionMetadataSet() + { + MockEngine e = new MockEngine(); + + ITaskItem[] items = new ITaskItem[] + { + new TaskItem("DependsOnOnlyv4Assemblies, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"), + new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089") + }; + + items[0].SetMetadata("SpecificVersion", "true"); + + string redistString = "" + + "" + + ""; + + List additionalPaths = new List(); + additionalPaths.Add(@"c:\MyComponents\4.0Component\"); + additionalPaths.Add(@"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion"); + additionalPaths.Add(@"c:\WINNT\Microsoft.NET\Framework\v9.0.MyVersion\"); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, false, additionalPaths); + + Assert.IsTrue(e.Warnings == 1, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\4.0Component\DependsOnOnlyv4Assemblies.dll"), "Expected to find assembly, but didn't."); + Assert.IsFalse(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + } + } + + + /// + /// Unit test the cases where we need to determine if the target framework is greater than the current target framework + /// + [TestClass] + sealed public class VerifyIgnoreVersionForFrameworkReference : ResolveAssemblyReferenceTestFixture + { + /// + /// Verify that we ignore the version information on the assembly + /// + [TestMethod] + public void IgnoreVersionBasic() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOn9, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.IgnoreVersionForFrameworkReferences = true; + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + + // Do the resolution without the metadata, expect it to not work since we should not be able to find Dependson9 version 10.0.0.0 + e = new MockEngine(); + + item = new TaskItem("DependsOn9, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + + items = new ITaskItem[] + { + item + }; + + redistString = "" + + "" + + ""; + + t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains("MSB3257"); + e.AssertLogContains("DependsOn9"); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Verify that we ignore the version information on the assembly + /// + [TestMethod] + public void IgnoreVersionBasicTestMetadata() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOn9, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + item.SetMetadata("IgnoreVersionForFrameworkReference", "True"); + + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + + // Do the resolution without the metadata, expect it to not work since we should not be able to find Dependson9 version 10.0.0.0 + e = new MockEngine(); + + item = new TaskItem("DependsOn9, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + + items = new ITaskItem[] + { + item + }; + + redistString = "" + + "" + + ""; + + t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains("MSB3257"); + e.AssertLogContains("DependsOn9"); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Verify that we ignore the version information on the assembly + /// + [TestMethod] + public void IgnoreVersionDisableIfSpecificVersionTrue() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + item.SetMetadata("IgnoreVersionForFrameworkReference", "True"); + item.SetMetadata("SpecificVersion", "True"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyComponents\misc\DependsOn9.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Verify that we ignore the version information on the assembly + /// + [TestMethod] + public void IgnoreVersionDisableIfHintPath() + { + MockEngine e = new MockEngine(); + + TaskItem item = new TaskItem("DependsOn9, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + item.SetMetadata("IgnoreVersionForFrameworkReference", "True"); + item.SetMetadata("HintPath", @"c:\MyComponents\misc\DependsOn9.dll"); + + ITaskItem[] items = new ITaskItem[] + { + item + }; + + string redistString = "" + + "" + + ""; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + ExecuteRAROnItemsAndRedist(t, e, items, redistString, true); + + + Assert.IsTrue(e.Warnings == 1, "Expected one warning in this scenario."); + e.AssertLogContains("MSB3257"); + e.AssertLogContains("DependsOn9"); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + } + + /// + /// Unit tests for the InstalledSDKResolver task. + /// + [TestClass] + sealed public class InstalledSDKResolverFixture : ResolveAssemblyReferenceTestFixture + { + /// + /// Verify that we do not find the winmd file even if it on the search path if the sdkname does not match something passed into the ResolvedSDKs property. + /// + [TestMethod] + public void SDkNameNotInResolvedSDKListButOnSearchPath() + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem taskItem = new TaskItem(@"SDKWinMD"); + taskItem.SetMetadata("SDKName", "NotInstalled, Version=1.0"); + + TaskItem[] assemblies = new TaskItem[] { taskItem }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblies; + t.SearchPaths = new String[] { @"C:\FakeSDK\References" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(0, t.ResolvedFiles.Length); + + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(1, engine.Warnings); + } + + /// + /// Verify when we are trying to match a name which is is the reference assembly directory + /// + [TestMethod] + public void SDkNameMatchInRADirectory() + { + ResolveSDKFromRefereneAssemblyLocation("DebugX86SDKWinMD", @"C:\FakeSDK\References\Debug\X86\DebugX86SDKWinMD.Winmd"); + ResolveSDKFromRefereneAssemblyLocation("DebugNeutralSDKWinMD", @"C:\FakeSDK\References\Debug\Neutral\DebugNeutralSDKWinMD.Winmd"); + ResolveSDKFromRefereneAssemblyLocation("x86SDKWinMD", @"C:\FakeSDK\References\CommonConfiguration\x86\x86SDKWinMD.Winmd"); + ResolveSDKFromRefereneAssemblyLocation("NeutralSDKWinMD", @"C:\FakeSDK\References\CommonConfiguration\Neutral\NeutralSDKWinMD.Winmd"); + ResolveSDKFromRefereneAssemblyLocation("SDKReference", @"C:\FakeSDK\References\Debug\X86\SDKReference.dll"); + ResolveSDKFromRefereneAssemblyLocation("DebugX86SDKRA", @"C:\FakeSDK\References\Debug\X86\DebugX86SDKRA.dll"); + ResolveSDKFromRefereneAssemblyLocation("DebugNeutralSDKRA", @"C:\FakeSDK\References\Debug\Neutral\DebugNeutralSDKRA.dll"); + ResolveSDKFromRefereneAssemblyLocation("x86SDKRA", @"C:\FakeSDK\References\CommonConfiguration\x86\x86SDKRA.dll"); + ResolveSDKFromRefereneAssemblyLocation("NeutralSDKRA", @"C:\FakeSDK\References\CommonConfiguration\Neutral\NeutralSDKRA.dll"); + } + + private static void ResolveSDKFromRefereneAssemblyLocation(string referenceName, string expectedPath) + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem taskItem = new TaskItem(referenceName); + taskItem.SetMetadata("SDKName", "FakeSDK, Version=1.0"); + + TaskItem resolvedSDK = new TaskItem(@"C:\FakeSDK"); + resolvedSDK.SetMetadata("SDKName", "FakeSDK, Version=1.0"); + resolvedSDK.SetMetadata("TargetedSDKConfiguration", "Debug"); + resolvedSDK.SetMetadata("TargetedSDKArchitecture", "X86"); + + TaskItem[] assemblies = new TaskItem[] { taskItem }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblies; + t.ResolvedSDKReferences = new ITaskItem[] { resolvedSDK }; + t.SearchPaths = new String[] { @"C:\SomeOtherPlace" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(expectedPath, StringComparison.OrdinalIgnoreCase)); + } + } + + /// + /// Unit tests for the ResolveAssemblyReference task. + /// + [TestClass] + sealed public class WinMDTests : ResolveAssemblyReferenceTestFixture + { + #region AssemblyInformationIsWinMDFile Tests + + /// + /// Verify a null file path passed in return the fact the file is not a winmd file. + /// + [TestMethod] + public void IsWinMDFileNullFilePath() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsFalse(AssemblyInformation.IsWinMDFile(null, getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// Verify if a empty file path is passed in that the file is not a winmd file. + /// + [TestMethod] + public void IsWinMDFileEmptyFilePath() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsFalse(AssemblyInformation.IsWinMDFile(String.Empty, getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// If the file does nto exist then we should report this is not a winmd file. + /// + [TestMethod] + public void IsWinMDFileFileDoesNotExistFilePath() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsFalse(AssemblyInformation.IsWinMDFile(@"C:\WinMD\SampleDoesNotExist.Winmd", getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// The file exists and has the correct windowsruntime metadata, we should report this is a winmd file. + /// + [TestMethod] + public void IsWinMDFileGoodFile() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsTrue(AssemblyInformation.IsWinMDFile(@"C:\WinMD\SampleWindowsRuntimeOnly.Winmd", getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// This file is a mixed file with CLR and windowsruntime metadata we should report this is a winmd file. + /// + [TestMethod] + public void IsWinMDFileMixedFile() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsTrue(AssemblyInformation.IsWinMDFile(@"C:\WinMD\SampleWindowsRuntimeAndCLR.Winmd", getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsTrue(isManagedWinMD); + } + + /// + /// The file has only CLR metadata we should report this is not a winmd file + /// + [TestMethod] + public void IsWinMDFileCLROnlyFile() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsFalse(AssemblyInformation.IsWinMDFile(@"C:\WinMD\SampleClrOnly.Winmd", getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// The windows runtime string is not correctly formatted, report this is not a winmd file. + /// + [TestMethod] + public void IsWinMDFileBadWindowsRuntimeFile() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsFalse(AssemblyInformation.IsWinMDFile(@"C:\WinMD\SampleBadWindowsRuntime.Winmd", getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// We should report that a regluar net assembly is not a winmd file. + /// + [TestMethod] + public void IsWinMDFileRegularNetAssemblyFile() + { + string imageRuntime; + bool isManagedWinMD; + Assert.IsFalse(AssemblyInformation.IsWinMDFile(@"C:\Framework\Whidbey\System.dll", getRuntimeVersion, fileExists, out imageRuntime, out isManagedWinMD)); + Assert.IsFalse(isManagedWinMD); + } + + /// + /// When a project to project reference is passed in we want to verify that + /// the winmd references get the correct metadata applied to them + /// + [TestMethod] + public void VerifyP2PHaveCorrectMetadataWinMD() + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem taskItem = new TaskItem(@"C:\WinMD\SampleWindowsRuntimeOnly.Winmd"); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + taskItem + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.TargetProcessorArchitecture = "X86"; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(2, t.RelatedFiles.Length); + + bool dllFound = false; + bool priFound = false; + + foreach (ITaskItem item in t.RelatedFiles) + { + if (item.ItemSpec.EndsWith(@"C:\WinMD\SampleWindowsRuntimeOnly.dll")) + { + dllFound = true; + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.imageRuntime).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + } + if (item.ItemSpec.EndsWith(@"C:\WinMD\SampleWindowsRuntimeOnly.pri")) + { + priFound = true; + + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.imageRuntime).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + } + } + + Assert.IsTrue(dllFound && priFound, "Expected to find .dll and .pri related files."); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFileType).Equals("Native", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals("SampleWindowsRuntimeOnly.dll")); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals("WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// When a project to project reference is passed in we want to verify that + /// the winmd references get the correct metadata applied to them + /// + [TestMethod] + public void VerifyP2PHaveCorrectMetadataWinMDManaged() + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem taskItem = new TaskItem(@"C:\WinMD\SampleWindowsRuntimeAndCLR.Winmd"); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + taskItem + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(0, t.RelatedFiles.Length); + + + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFileType).Equals("Managed", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals("WindowsRuntime 1.0, CLR V2.0.50727", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// When a project to project reference is passed in we want to verify that + /// the winmd references get the correct metadata applied to them + /// + [TestMethod] + public void VerifyP2PHaveCorrectMetadataNonWinMD() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\AssemblyFolder\SomeAssembly.dll") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when we reference a winmd file as a reference item make sure we ignore the mscorlib. + /// + [TestMethod] + public void IgnoreReferenceToMscorlib() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"SampleWindowsRuntimeOnly"), new TaskItem(@"SampleWindowsRuntimeAndClr") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.TargetProcessorArchitecture = "X86"; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + engine.AssertLogDoesntContain("conflict"); + } + + /// + /// Verify when we reference a mixed winmd file that we do resolve the reference to the mscorlib + /// + [TestMethod] + public void MixedWinMDGoodReferenceToMscorlib() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"SampleWindowsRuntimeAndClr") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(0, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.Resolved", @"C:\WinMD\v4\mscorlib.dll"); + } + + + /// + /// Verify when a winmd file depends on another winmd file that we do resolve the dependency + /// + [TestMethod] + public void WinMdFileDependsOnAnotherWinMDFile() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"SampleWindowsRuntimeOnly2") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.TargetProcessorArchitecture = "X86"; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMD\SampleWindowsRuntimeOnly2.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + + Assert.IsTrue(t.ResolvedDependencyFiles[0].ItemSpec.Equals(@"C:\WinMD\SampleWindowsRuntimeOnly.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + } + + + + /// + /// We have two dlls which depend on a winmd, the first dll does not have the winmd beside it, the second one does + /// we want to make sure that the winmd file is resolved beside the second dll. + /// + [TestMethod] + public void ResolveWinmdBesideDll() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\DirectoryContainsOnlyDll\A.dll"), + new TaskItem(@"C:\DirectoryContainsdllAndWinmd\B.dll"), + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { "{RAWFILENAME}" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(t.ResolvedDependencyFiles[0].ItemSpec.Equals(@"C:\DirectoryContainsdllAndWinmd\C.winmd", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// We have a winmd file and a dll depend on a winmd, there are copies of the winmd beside each of the files. + /// we want to make sure that the winmd file is resolved beside the winmd since that is the first file resolved. + /// + [TestMethod] + public void ResolveWinmdBesideDll2() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\DirectoryContainstwoWinmd\A.winmd"), + new TaskItem(@"C:\DirectoryContainsdllAndWinmd\B.dll"), + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"{RAWFILENAME}" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + Assert.IsTrue(t.ResolvedDependencyFiles[0].ItemSpec.Equals(@"C:\DirectoryContainstwoWinmd\C.winmd", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when a winmd file depends on another winmd file that itself has framework dependencies that we do not resolve any of the + /// dependencies due to the winmd to winmd reference + /// + [TestMethod] + public void WinMdFileDependsOnAnotherWinMDFileWithFrameworkDependencies() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"SampleWindowsRuntimeOnly3") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"{TargetFrameworkDirectory}", @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v4.0.MyVersion" }; + t.TargetProcessorArchitecture = "x86"; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(4, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMD\SampleWindowsRuntimeOnly3.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + } + + /// + /// Make sure when a dot net assembly depends on a WinMDFile that + /// we get the winmd file resolved. Also make sure that if there is Implementation, ImageRuntime, or IsWinMD set on the dll that + /// it does not get propigated to the winmd file dependency. + /// + [TestMethod] + public void DotNetAssemblyDependsOnAWinMDFile() + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem item = new TaskItem(@"DotNetAssemblyDependsOnWinMD"); + // This should not be used for anything, it is recalculated in rar, this is to make sure it is not forwarded to child items. + item.SetMetadata(ItemMetadataNames.imageRuntime, "FOO"); + // This should not be used for anything, it is recalculated in rar, this is to make sure it is not forwarded to child items. + item.SetMetadata(ItemMetadataNames.winMDFile, "NOPE"); + item.SetMetadata(ItemMetadataNames.winmdImplmentationFile, "IMPL"); + ITaskItem[] assemblyFiles = new TaskItem[] + { + item + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.TargetProcessorArchitecture = "X86"; + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMD\DotNetAssemblyDependsOnWinMD.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile).Equals("NOPE", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals("IMPL", StringComparison.OrdinalIgnoreCase)); + + Assert.IsTrue(t.ResolvedDependencyFiles[0].ItemSpec.Equals(@"C:\WinMD\SampleWindowsRuntimeOnly.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + Assert.IsTrue(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals("SampleWindowsRuntimeOnly.dll")); + } + + /// + /// Resolve a winmd file which depends on a native implementation dll that has an invalid pe header. + /// This will always result in an error since the dll is malformed + /// + [TestMethod] + public void ResolveWinmdWithInvalidPENativeDependency() + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem item = new TaskItem(@"DependsOnInvalidPeHeader"); + ITaskItem[] assemblyFiles = new TaskItem[] { item }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMDArchVerification" }; + bool succeeded = Execute(t); + + // Should fail since PE Header is not valid and this is always an error. + Assert.IsFalse(succeeded); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + + // The original winmd will resolve but its impelmentation dll must not be there + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + + string invalidPEMessage = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.ImplementationDllHasInvalidPEHeader"); + string fullMessage = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.ProblemReadingImplementationDll", @"C:\WinMDArchVerification\DependsOnInvalidPeHeader.dll", invalidPEMessage); + engine.AssertLogContains(fullMessage); + } + + /// + /// Resolve a winmd file which depends a native dll that matches the targeted architecture + /// + [TestMethod] + public void ResolveWinmdWithArchitectureDependencyMatchingArchitecturesX86() + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem item = new TaskItem("DependsOnX86"); + ITaskItem[] assemblyFiles = new TaskItem[] { item }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMDArchVerification" }; + t.TargetProcessorArchitecture = "X86"; + t.WarnOrErrorOnTargetArchitectureMismatch = "Error"; + + bool succeeded = Execute(t); + Assert.AreEqual(1, t.ResolvedFiles.Length); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMDArchVerification\DependsOnX86.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals("DependsOnX86.dll")); + Assert.IsTrue(engine.Errors == 0); + Assert.IsTrue(engine.Warnings == 0); + } + + /// + /// Resolve a winmd file which depends a native dll that matches the targeted architecture + /// + [TestMethod] + public void ResolveWinmdWithArchitectureDependencyAnyCPUNative() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + // IMAGE_FILE_MACHINE unknown is supposed to work on all machine types + TaskItem item = new TaskItem("DependsOnAnyCPUUnknown"); + ITaskItem[] assemblyFiles = new TaskItem[] { item }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMDArchVerification" }; + t.TargetProcessorArchitecture = "X86"; + t.WarnOrErrorOnTargetArchitectureMismatch = "Error"; + + bool succeeded = Execute(t); + Assert.AreEqual(1, t.ResolvedFiles.Length); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMDArchVerification\DependsOnAnyCPUUnknown.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals("DependsOnAnyCPUUnknown.dll")); + Assert.IsTrue(engine.Errors == 0); + Assert.IsTrue(engine.Warnings == 0); + } + + /// + /// Resolve a winmd file which depends on a native implementation dll that has an invalid pe header. + /// A warning or error is expectected in the log depending on the WarnOrErrorOnTargetArchitecture property value. + /// + [TestMethod] + public void ResolveWinmdWithArchitectureDependency() + { + VerifyImplementationArchitecture("DependsOnX86", "MSIL", "X86", "Error"); + VerifyImplementationArchitecture("DependsOnX86", "MSIL", "X86", "Warning"); + VerifyImplementationArchitecture("DependsOnX86", "MSIL", "X86", "None"); + VerifyImplementationArchitecture("DependsOnX86", "AMD64", "X86", "Error"); + VerifyImplementationArchitecture("DependsOnX86", "AMD64", "X86", "Warning"); + VerifyImplementationArchitecture("DependsOnX86", "AMD64", "X86", "None"); + VerifyImplementationArchitecture("DependsOnAmd64", "MSIL", "AMD64", "Error"); + VerifyImplementationArchitecture("DependsOnAmd64", "MSIL", "AMD64", "Warning"); + VerifyImplementationArchitecture("DependsOnAmd64", "MSIL", "AMD64", "None"); + VerifyImplementationArchitecture("DependsOnAmd64", "X86", "AMD64", "Error"); + VerifyImplementationArchitecture("DependsOnAmd64", "X86", "AMD64", "Warning"); + VerifyImplementationArchitecture("DependsOnAmd64", "X86", "AMD64", "None"); + VerifyImplementationArchitecture("DependsOnARM", "MSIL", "ARM", "Error"); + VerifyImplementationArchitecture("DependsOnARM", "MSIL", "ARM", "Warning"); + VerifyImplementationArchitecture("DependsOnARM", "MSIL", "ARM", "None"); + VerifyImplementationArchitecture("DependsOnARMV7", "MSIL", "ARM", "Error"); + VerifyImplementationArchitecture("DependsOnARMV7", "MSIL", "ARM", "Warning"); + VerifyImplementationArchitecture("DependsOnARMv7", "MSIL", "ARM", "None"); + VerifyImplementationArchitecture("DependsOnIA64", "MSIL", "IA64", "Error"); + VerifyImplementationArchitecture("DependsOnIA64", "MSIL", "IA64", "Warning"); + VerifyImplementationArchitecture("DependsOnIA64", "MSIL", "IA64", "None"); + VerifyImplementationArchitecture("DependsOnUnknown", "MSIL", "Unknown", "Error"); + VerifyImplementationArchitecture("DependsOnUnknown", "MSIL", "Unknown", "Warning"); + VerifyImplementationArchitecture("DependsOnUnknown", "MSIL", "Unknown", "None"); + } + + private void VerifyImplementationArchitecture(string winmdName, string targetProcessorArchitecture, string implementationFileArch, string warnOrErrorOnTargetArchitectureMismatch) + { + // Create the engine. + MockEngine engine = new MockEngine(); + TaskItem item = new TaskItem(winmdName); + ITaskItem[] assemblyFiles = new TaskItem[] { item }; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMDArchVerification" }; + t.TargetProcessorArchitecture = targetProcessorArchitecture; + t.WarnOrErrorOnTargetArchitectureMismatch = warnOrErrorOnTargetArchitectureMismatch; + + bool succeeded = Execute(t); + Assert.AreEqual(1, t.ResolvedFiles.Length); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMDArchVerification\" + winmdName + ".winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + + string fullMessage = null; + if (implementationFileArch.Equals("Unknown")) + { + fullMessage = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.UnknownProcessorArchitecture", @"C:\WinMDArchVerification\" + winmdName + ".dll", @"C:\WinMDArchVerification\" + winmdName + ".winmd", NativeMethods.IMAGE_FILE_MACHINE_R4000.ToString("X", CultureInfo.InvariantCulture)); + } + else + { + fullMessage = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArchOfImplementation", targetProcessorArchitecture, implementationFileArch, @"C:\WinMDArchVerification\" + winmdName + ".dll", @"C:\WinMDArchVerification\" + winmdName + ".winmd"); + } + + if (warnOrErrorOnTargetArchitectureMismatch.Equals("None", StringComparison.OrdinalIgnoreCase)) + { + engine.AssertLogDoesntContain(fullMessage); + } + else + { + engine.AssertLogContains(fullMessage); + } + + if (warnOrErrorOnTargetArchitectureMismatch.Equals("Warning", StringComparison.OrdinalIgnoreCase)) + { + // Should fail since PE Header is not valid and this is always an error. + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals(winmdName + ".dll")); + Assert.IsTrue(engine.Errors == 0); + Assert.IsTrue(engine.Warnings == 1); + } + else if (warnOrErrorOnTargetArchitectureMismatch.Equals("Error", StringComparison.OrdinalIgnoreCase)) + { + // Should fail since PE Header is not valid and this is always an error. + Assert.IsFalse(succeeded); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0); + } + else if (warnOrErrorOnTargetArchitectureMismatch.Equals("None", StringComparison.OrdinalIgnoreCase)) + { + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winmdImplmentationFile).Equals(winmdName + ".dll")); + Assert.IsTrue(engine.Errors == 0); + Assert.IsTrue(engine.Warnings == 0); + } + } + + /// + /// Verify when a winmd file depends on another winmd file that we resolve both and that the metadata is correct. + /// + [TestMethod] + public void DotNetAssemblyDependsOnAWinMDFileWithVersion255() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"DotNetAssemblyDependsOn255WinMD") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyFiles; + t.SearchPaths = new String[] { @"C:\WinMD", @"C:\WinMD\v4\", @"C:\WinMD\v255\" }; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\WinMD\DotNetAssemblyDependsOn255WinMD.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + + Assert.IsTrue(t.ResolvedDependencyFiles[0].ItemSpec.Equals(@"C:\WinMD\WinMDWithVersion255.winmd", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.imageRuntime).Equals(@"WindowsRuntime 1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(bool.Parse(t.ResolvedDependencyFiles[0].GetMetadata(ItemMetadataNames.winMDFile))); + } + #endregion + } + + + /// + /// Unit tests for the ResolveAssemblyReference task. + /// + [TestClass] + sealed public class Miscellaneous : ResolveAssemblyReferenceTestFixture + { + private static List s_assemblyFolderExTestVersions = new List + { + "v1.0", + "v2.0.50727", + "v3.0", + "v3.5", + "v4.0", + "v4.0.2116", + "v4.1", + "v4.0.255", + "v4.0.255.87", + "v4.0.9999", + "v4.0.0000", + "v4.0001.0", + "v4.0.2116.87", + "v3.0SP1", + "v3.0 BAZ", + "v5.0", + "v1", + "v5", + "v3.5.0.x86chk", + "v3.5.1.x86chk", + "v3.5.256.x86chk", + "v", + "1", + "1.0", + "1.0.0", + "V3.5.0.0.0", + "V3..", + "V-1", + "V9999999999999999", + "Dan_rocks_bigtime", + "v00001.0" + }; + + private string _fullRedistListContents = + "" + + "" + + "" + + ""; + + /// + /// The contents of a subsetFile which only contain the Microsoft.Build.Engine assembly in the white list + /// + private string _engineOnlySubset = + "" + + "" + + ""; + + /// + /// The contents of a subsetFile which only contain the System.Xml assembly in the white list + /// + private string _xmlOnlySubset = + "" + + "" + + ""; + + /// + /// The contents of a subsetFile which contain both the Microsoft.Build.Engine and System.Xml assemblies in the white list + /// + private string _engineAndXmlSubset = + "" + + "" + + "" + + ""; + + + + /// + /// Let us have the following dependency structure + /// + /// X which is in the gac, depends on Z which is not in the GAC + /// + /// Let copyLocalDependenciesWhenParentReferenceInGac be set to false + /// + /// Since copyLocalDependenciesWhenParentReferenceInGac is set to false and the parent of Z is in the GAC + /// + [TestMethod] + public void CopyLocalDependenciesWhenParentReferenceInGacFalseAllParentsInGac() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = new string[] { @"c:\MyComponents" }; + t.CopyLocalDependenciesWhenParentReferenceInGac = false; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + AssertNoCase("false", t.ResolvedDependencyFiles[0].GetMetadata("CopyLocal")); + AssertNoCase("false", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + } + + + + [TestMethod] + public void ValidateFrameworkNameError() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = new string[] { @"c:\MyComponents" }; + t.TargetFrameworkMoniker = "I am a random frameworkName"; + bool succeeded = Execute(t); + + Assert.IsFalse(succeeded); + Assert.AreEqual(1, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + string message = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.InvalidParameter", "TargetFrameworkMoniker", t.TargetFrameworkMoniker, String.Empty); + engine.AssertLogContains(message); + } + + /// + /// Let us have the following dependency structure + /// + /// X which is in the gac, depends on Z which is not in the GAC + /// Y which is not in the gac, depends on Z which is not in the GAC + /// + /// Let copyLocalDependenciesWhenParentReferenceInGac be set to false + /// + /// Since copyLocalDependenciesWhenParentReferenceInGac is set to false but one of the parents of Z is not in the GAC and Z is not in the gac we should be copy local + /// + [TestMethod] + public void CopyLocalDependenciesWhenParentReferenceInGacFalseSomeParentsInGac() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"), + new TaskItem("Y, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = new string[] { @"c:\MyComponents" }; + t.CopyLocalDependenciesWhenParentReferenceInGac = false; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + AssertNoCase("false", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + AssertNoCase("true", t.ResolvedFiles[1].GetMetadata("CopyLocal")); + AssertNoCase("true", t.ResolvedDependencyFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// Make sure that when we parse the runtime version that if there is a bad one we default to 2.0. + /// + [TestMethod] + public void TestSetRuntimeVersion() + { + Version parsedVersion = ResolveAssemblyReference.SetTargetedRuntimeVersion("4.0.21006"); + Assert.IsTrue(parsedVersion.Equals(new Version("4.0.21006"))); + + parsedVersion = ResolveAssemblyReference.SetTargetedRuntimeVersion("BadVersion"); + Assert.IsTrue(parsedVersion.Equals(new Version("2.0.50727"))); + } + + /// + /// Let us have the following dependency structure + /// + /// X which is in the gac, depends on Z which is not in the GAC + /// + /// Let copyLocalDependenciesWhenParentReferenceInGac be set to true + /// + /// Since copyLocalDependenciesWhenParentReferenceInGac is set to true and Z is not in the GAC it will be copy local true + /// + [TestMethod] + public void CopyLocalDependenciesWhenParentReferenceInGacTrueAllParentsInGac() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = new string[] { @"c:\MyComponents" }; + t.CopyLocalDependenciesWhenParentReferenceInGac = true; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + AssertNoCase("true", t.ResolvedDependencyFiles[0].GetMetadata("CopyLocal")); + AssertNoCase("false", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// Let us have the following dependency structure + /// + /// X which is in the gac, depends on Z which is not in the GAC + /// Y which is not in the gac, depends on Z which is not in the GAC + /// + /// Let copyLocalDependenciesWhenParentReferenceInGac be set to true + /// + /// Since copyLocalDependenciesWhenParentReferenceInGac is set to true and Z is not in the GAC it will be copy local true + /// + [TestMethod] + public void CopyLocalDependenciesWhenParentReferenceInGacTrueSomeParentsInGac() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("X, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"), + new TaskItem("Y, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.SearchPaths = new string[] { @"c:\MyComponents" }; + t.CopyLocalDependenciesWhenParentReferenceInGac = true; + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(0, engine.Errors); + Assert.AreEqual(0, engine.Warnings); + AssertNoCase("false", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + AssertNoCase("true", t.ResolvedFiles[1].GetMetadata("CopyLocal")); + AssertNoCase("true", t.ResolvedDependencyFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// Very basic test. + /// + [TestMethod] + public void Basic() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing Miscellaneous.Basic() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + // Construct a list of assembly files. + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"c:\MyProject\MyMissingAssembly.dll") + }; + + // Also construct a set of assembly names to pass in. + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + new TaskItem("MyPrivateAssembly"), + new TaskItem("MyGacAssembly"), + new TaskItem("MyCopyLocalAssembly"), + new TaskItem("MyDontCopyLocalAssembly"), + new TaskItem("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") + }; + + assemblyNames[0].SetMetadata("RandomAttributeThatShouldBeForwarded", "1776"); + // Metadata which should NOT be forwarded + assemblyNames[0].SetMetadata(ItemMetadataNames.imageRuntime, "FOO"); + assemblyNames[0].SetMetadata(ItemMetadataNames.winMDFile, "NOPE"); + assemblyNames[0].SetMetadata(ItemMetadataNames.winmdImplmentationFile, "IMPL"); + + + assemblyNames[1].SetMetadata("Private", "true"); + assemblyNames[2].SetMetadata("Private", "false"); + assemblyNames[4].SetMetadata("Private", "false"); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.Assemblies = assemblyNames; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.SearchPaths = DefaultPaths; + Execute(t); + + // Now, loop over the closure of dependencies and make sure we have what we need. + bool enSatellitePdbFound = false; + bool systemXmlFound = false; + bool systemDataFound = false; + bool systemFound = false; + bool mscorlibFound = false; + bool myGacAssemblyFound = false; + bool myPrivateAssemblyFound = false; + bool myCopyLocalAssemblyFound = false; + bool myDontCopyLocalAssemblyFound = false; + bool engbSatellitePdbFound = false; + bool missingAssemblyFound = false; + + // Process the primary items. + foreach (ITaskItem item in t.ResolvedFiles) + { + if (String.Compare(item.ItemSpec, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.XML.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + systemXmlFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("1776", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("false", item.GetMetadata("CopyLocal")); + AssertNoCase("System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", item.GetMetadata("FusionName")); + AssertNoCase("v2.0.50727", item.GetMetadata(ItemMetadataNames.imageRuntime)); + AssertNoCase("NOPE", item.GetMetadata(ItemMetadataNames.winMDFile)); + AssertNoCase("IMPL", item.GetMetadata(ItemMetadataNames.winmdImplmentationFile)); + } + else if (item.ItemSpec.EndsWith(@"v2.0.MyVersion\System.Data.dll")) + { + systemDataFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("false", item.GetMetadata("CopyLocal")); + AssertNoCase("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", item.GetMetadata("FusionName")); + } + else if (item.ItemSpec.EndsWith(@"v2.0.MyVersion\MyGacAssembly.dll")) + { + myGacAssemblyFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("false", item.GetMetadata("CopyLocal")); + } + else if (item.ItemSpec.EndsWith(@"MyProject\MyPrivateAssembly.exe")) + { + myPrivateAssemblyFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("true", item.GetMetadata("CopyLocal")); + } + else if (item.ItemSpec.EndsWith(@"MyProject\MyCopyLocalAssembly.dll")) + { + myCopyLocalAssemblyFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("true", item.GetMetadata("CopyLocal")); + } + else if (item.ItemSpec.EndsWith(@"MyProject\MyDontCopyLocalAssembly.dll")) + { + myDontCopyLocalAssemblyFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("false", item.GetMetadata("CopyLocal")); + } + else if (item.ItemSpec.EndsWith(@"MyProject\MyMissingAssembly.dll")) + { + missingAssemblyFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + + // Its debatable whether this file should be CopyLocal or not. + // It doesn't exist on disk, but is it ResolveAssemblyReference's job to make sure that it does? + // For now, let the default CopyLocal rules apply. + AssertNoCase("true", item.GetMetadata("CopyLocal")); + AssertNoCase("MyMissingAssembly", item.GetMetadata("FusionName")); + } + else if (String.Compare(item.ItemSpec, @"c:\MyProject\System.Xml.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // The version of System.Xml.dll in C:\MyProject is an older version. + // This version is not a match. When want the current version which should have been in a different directory. + Assert.Fail("Wrong version of System.Xml.dll matched--version was wrong"); + } + else if (String.Compare(item.ItemSpec, @"c:\MyProject\System.Data.dll", StringComparison.OrdinalIgnoreCase) == 0) + { + // The version of System.Data.dll in C:\MyProject has an incorrect PKT + // This version is not a match. + Assert.Fail("Wrong version of System.Data.dll matched--public key token was wrong"); + } + else + { + Console.WriteLine(item.ItemSpec); + Assert.Fail(String.Format("A new resolved file called '{0}' was found. If this is intentional, then add unittests above.", item.ItemSpec)); + } + } + + // Process the dependencies. + foreach (ITaskItem item in t.ResolvedDependencyFiles) + { + if (item.ItemSpec.EndsWith(@"v2.0.MyVersion\SysTem.dll")) + { + systemFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("false", item.GetMetadata("CopyLocal")); + AssertNoCase("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", item.GetMetadata("FusionName")); + } + else if (item.ItemSpec.EndsWith(@"v2.0.MyVersion\mscorlib.dll")) + { + mscorlibFound = true; + AssertNoCase("", item.GetMetadata("DestinationSubDirectory")); + AssertNoCase("1776", item.GetMetadata("RandomAttributeThatShouldBeForwarded")); + AssertNoCase("false", item.GetMetadata("CopyLocal")); + AssertNoCase("v2.0.50727", item.GetMetadata(ItemMetadataNames.imageRuntime)); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + + // Notice how the following doesn't have 'version'. This is because all versions of mscorlib 'unify' + Assert.AreEqual(AssemblyRef.Mscorlib, item.GetMetadata("FusionName")); + } + else + { + Console.WriteLine(item.ItemSpec); + Assert.Fail(String.Format("A new dependency called '{0}' was found. If this is intentional, then add unittests above.", item.ItemSpec)); + } + } + + // Process the related files. + foreach (ITaskItem item in t.RelatedFiles) + { + Console.WriteLine(item.ItemSpec); + Assert.Fail(String.Format("A new dependency called '{0}' was found. If this is intentional, then add unittests above.", item.ItemSpec)); + } + + // Process the satellites. + foreach (ITaskItem item in t.SatelliteFiles) + { + if (String.Compare(item.ItemSpec, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en\System.XML.resources.pdb", StringComparison.OrdinalIgnoreCase) == 0) + { + enSatellitePdbFound = true; + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.imageRuntime).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + } + else if (String.Compare(item.ItemSpec, @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\en-GB\System.XML.resources.pdb", StringComparison.OrdinalIgnoreCase) == 0) + { + engbSatellitePdbFound = true; + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.imageRuntime).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winMDFile).Length == 0); + Assert.IsTrue(item.GetMetadata(ItemMetadataNames.winmdImplmentationFile).Length == 0); + } + else + { + Console.WriteLine(item.ItemSpec); + Assert.Fail(String.Format("A new dependency called '{0}' was found. If this is intentional, then add unittests above.", item.ItemSpec)); + } + } + + Assert.IsFalse(enSatellitePdbFound, "Expected to not find satellite pdb."); + Assert.IsTrue(systemXmlFound, "Expected to find returned item."); + Assert.IsTrue(systemDataFound, "Expected to find returned item."); + Assert.IsTrue(systemFound, "Expected to find returned item."); + Assert.IsFalse(mscorlibFound, "Expected to not find returned item."); + Assert.IsTrue(myGacAssemblyFound, "Expected to find returned item."); + Assert.IsTrue(myPrivateAssemblyFound, "Expected to find returned item."); + Assert.IsTrue(myCopyLocalAssemblyFound, "Expected to find returned item."); + Assert.IsTrue(myDontCopyLocalAssemblyFound, "Expected to find returned item."); + Assert.IsFalse(engbSatellitePdbFound, "Expected to not find satellite pdb."); + Assert.IsTrue(missingAssemblyFound, "Expected to find returned item."); + } + + /// + /// Auxiliary enumeration for EmbedInteropTypes test. + /// Defines indices for accessing test's data structures. + /// + private enum EmbedInteropTypes_Indices + { + MyMissingAssembly = 0, + MyCopyLocalAssembly = 1, + MyDontCopyLocalAssembly = 2, + + EndMarker + }; + + /// + /// Make sure the imageruntime is correctly returned. + /// + [TestMethod] + public void TestGetImageRuntimeVersion() + { + string imageRuntimeReportedByAsssembly = this.GetType().Assembly.ImageRuntimeVersion; + string pathForAssembly = this.GetType().Assembly.Location; + + string inspectedRuntimeVersion = AssemblyInformation.GetRuntimeVersion(pathForAssembly); + Assert.IsTrue(imageRuntimeReportedByAsssembly.Equals(inspectedRuntimeVersion, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Make sure the imageruntime is correctly returned. + /// + [TestMethod] + public void TestGetImageRuntimeVersionBadPath() + { + string realFile = FileUtilities.GetTemporaryFile(); + try + { + string inspectedRuntimeVersion = AssemblyInformation.GetRuntimeVersion(realFile); + Assert.IsTrue(inspectedRuntimeVersion == String.Empty); + } + finally + { + File.Delete(realFile); + } + } + + /// + /// When specifying "EmbedInteropTypes" on a project targeting Fx higher thatn v4.0 - + /// CopyLocal should be overriden to false + /// + [TestMethod] + public void EmbedInteropTypes() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing Miscellaneous.Basic() test"); + + // Construct a list of assembly files. + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"c:\MyProject\MyMissingAssembly.dll") + }; + + assemblyFiles[0].SetMetadata("Private", "true"); + assemblyFiles[0].SetMetadata("EmbedInteropTypes", "true"); + + // Construct a list of assembly names. + ITaskItem[] assemblies = new TaskItem[] + { + new TaskItem("MyCopyLocalAssembly"), + new TaskItem("MyDontCopyLocalAssembly") + }; + + assemblies[0].SetMetadata("Private", "true"); + assemblies[0].SetMetadata("EmbedInteropTypes", "true"); + assemblies[1].SetMetadata("Private", "false"); + assemblies[1].SetMetadata("EmbedInteropTypes", "true"); + + // the matrix of TargetFrameworkVersion values we are testing + string[] fxVersions = + { + "v2.0", + "v3.0", + "v3.5", + "v4.0" + }; + + // expected ItemSpecs for corresponding assemblies + string[] expectedItemSpec = + { + @"MyProject\MyMissingAssembly.dll", // MyMissingAssembly + @"MyProject\MyCopyLocalAssembly.dll", // MyCopyLocalAssembly + @"MyProject\MyDontCopyLocalAssembly.dll", // MyDontCopyLocalAssembly + }; + + // matrix of expected CopyLocal value per assembly per framwork + string[,] expectedCopyLocal = + { + // v2.0 v3.0 v3.5 v4.0 + { "true", "true", "true", "false" }, // MyMissingAssembly + { "true", "true", "true", "false" }, // MyCopyLocalAssembly + { "false", "false", "false", "false" } // MyDontCopyLocalAssembly + }; + + + int assembliesCount = (int)EmbedInteropTypes_Indices.EndMarker; + + // now let's verify our data structures are all set up correctly + Assert.IsTrue(fxVersions.GetLength(0) == expectedCopyLocal.GetLength(1), "fxVersions: test setup is incorrect"); + Assert.IsTrue(expectedItemSpec.Length == assembliesCount, "expectedItemSpec: test setup is incorrect"); + Assert.IsTrue(expectedCopyLocal.GetLength(0) == assembliesCount, "expectedCopyLocal: test setup is incorrect"); + + for (int i = 0; i < fxVersions.Length; i++) + { + // Create the engine. + MockEngine engine = new MockEngine(); + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = engine; + t.Assemblies = assemblies; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + + string fxVersion = fxVersions[i]; + t.TargetFrameworkDirectories = new string[] { String.Format(@"c:\WINNT\Microsoft.NET\Framework\{0}.MyVersion", fxVersion) }; + t.TargetFrameworkVersion = fxVersion; + Execute(t); + + bool[] assembliesFound = new bool[assembliesCount]; + + // Now, process primary items and make sure we have what we need. + foreach (ITaskItem item in t.ResolvedFiles) + { + string copyLocal = item.GetMetadata("CopyLocal"); + + int j; + for (j = 0; j < assembliesCount; j++) + { + if (item.ItemSpec.EndsWith(expectedItemSpec[j])) + { + assembliesFound[j] = true; + string assemblyName = Enum.GetName(typeof(EmbedInteropTypes_Indices), j); + AssertNoCase(fxVersion + ": unexpected CopyValue for " + assemblyName, expectedCopyLocal[j, i], copyLocal); + break; + } + } + + if (j == assembliesCount) + { + Console.WriteLine(item.ItemSpec); + Assert.Fail(String.Format("{0}: A new resolved file called '{1}' was found. If this is intentional, then add unittests above.", fxVersion, item.ItemSpec)); + } + } + + for (int j = 0; j < assembliesCount; j++) + { + string assemblyName = Enum.GetName(typeof(EmbedInteropTypes_Indices), j); + Assert.IsTrue(assembliesFound[j], fxVersion + ": Expected to find returned item " + assemblyName); + } + } + } + + /// + /// If items lists are empty, then this is a NOP not a failure. + /// + [TestMethod] + public void NOPForEmptyItemLists() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.SearchPaths = DefaultPaths; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded, "Expected success."); + } + + + /// + /// If no related file extensions are input to RAR, .pdb and .xml should be used + /// by default. + /// + [TestMethod] + public void DefaultAllowedRelatedFileExtensionsAreUsed() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing Miscellaneous.DefaultRelatedFileExtensionsAreUsed() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + // Construct a list of assembly files. + ITaskItem[] assemblies = new TaskItem[] + { + new TaskItem(@"c:\AssemblyFolder\SomeAssembly.dll") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblies; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.SearchPaths = DefaultPaths; + Execute(t); + + Assert.IsTrue(t.ResolvedFiles.Length == 1, String.Format("Expected one resolved file, but found {0}.", t.ResolvedFiles.Length)); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.dll")); + + // Process the related files. + Assert.IsTrue(t.RelatedFiles.Length == 3, String.Format("Expected three related files, but found {0}.", t.RelatedFiles.Length)); + + bool pdbFound = false; + bool xmlFound = false; + bool priFound = false; + + foreach (ITaskItem item in t.RelatedFiles) + { + if (item.ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.pdb")) + { + pdbFound = true; + } + if (item.ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.xml")) + { + xmlFound = true; + } + if (item.ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.pri")) + { + priFound = true; + } + } + + Assert.IsTrue(pdbFound && xmlFound && priFound, "Expected to find .pdb, .xml, and .pri related files."); + } + + /// + /// RAR should use any given related file extensions. + /// + [TestMethod] + public void InputAllowedRelatedFileExtensionsAreUsed() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing Miscellaneous.InputRelatedFileExtensionsAreUsed() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + // Construct a list of assembly files. + ITaskItem[] assemblies = new TaskItem[] + { + new TaskItem(@"c:\AssemblyFolder\SomeAssembly.dll") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblies; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.SearchPaths = DefaultPaths; + t.AllowedRelatedFileExtensions = new string[] { @".licenses", ".xml" }; //no .pdb or .config + Execute(t); + + Assert.IsTrue(t.ResolvedFiles.Length == 1, String.Format("Expected one resolved file, but found {0}.", t.ResolvedFiles.Length)); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.dll")); + + // Process the related files. + Assert.IsTrue(t.RelatedFiles.Length == 2, String.Format("Expected two related files, but found {0}.", t.RelatedFiles.Length)); + + bool licensesFound = false; + bool xmlFound = false; + foreach (ITaskItem item in t.RelatedFiles) + { + if (item.ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.licenses")) + { + licensesFound = true; + } + if (item.ItemSpec.EndsWith(@"AssemblyFolder\SomeAssembly.xml")) + { + xmlFound = true; + } + } + + Assert.IsTrue(licensesFound && xmlFound, "Expected to find .licenses and .xml related files."); + } + + /// + /// Simulate a CreateProject resolution. This is primarily for IO monitoring. + /// + public void SimulateCreateProjectAgainstWhidbey(string fxfolder) + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing SimulateCreateProjectAgainstWhidbey() test"); + + // Create the engine. + MockEngine engine = new MockEngine(); + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = new ITaskItem[] { + new TaskItem("System"), + new TaskItem("System.Deployment"), + new TaskItem("System.Drawing"), + new TaskItem("System.Windows.Forms"), + }; + t.TargetFrameworkDirectories = new string[] { fxfolder }; + + t.SearchPaths = new string[] + { + "{CandidateAssemblyFiles}", + // Reference path + "{HintPathFromItem}", + @"{TargetFrameworkDirectory}", + @"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", + "{AssemblyFolders}", + "{GAC}", + "{RawFileName}" + }; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded, "Expected success."); + } + + /// + /// Test with a standard path. + /// + [TestMethod] + public void SimulateCreateProjectAgainstWhidbey() + { + SimulateCreateProjectAgainstWhidbey(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version45)); + } + + /// + /// Test with a standard trailing-slash path. + /// + [TestMethod] + public void SimulateCreateProjectAgainstWhidbeyWithTrailingSlash() + { + SimulateCreateProjectAgainstWhidbey(ToolLocationHelper.GetPathToDotNetFramework(TargetDotNetFrameworkVersion.Version45) + @"\"); + } + + + /// + /// Invalid candidate assembly files should not crash + /// + [TestMethod] + public void Regress286699_InvalidCandidateAssemblyFiles() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; + t.CandidateAssemblyFiles = new string[] { "|" }; + + bool retval = Execute(t); + + Assert.IsFalse(retval); + + // Should not crash. + } + + /// + /// Invalid assembly files should not crash + /// + [TestMethod] + public void Regress286699_InvalidAssemblyFiles() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; + t.AssemblyFiles = new ITaskItem[] { new TaskItem("|") }; + + bool retval = Execute(t); + + Assert.IsFalse(retval); + + // Should not crash. + } + + /// + /// Invalid assemblies param should not crash + /// + [TestMethod] + public void Regress286699_InvalidAssembliesParameter() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("|!@#$%::") }; + + bool retval = Execute(t); + + // I think this should return true + Assert.IsTrue(retval); + + // Should not crash. + } + + /// + /// Target framework path with a newline should not crash. + /// + [TestMethod] + public void Regress286699_InvalidTargetFrameworkDirectory() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing Regress286699_InvalidTargetFrameworkDirectory() test"); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.TargetFrameworkDirectories = new string[] { "\nc:\\blah\\v2.0.1234" }; + + bool retval = Execute(t); + + Assert.IsFalse(retval); + + // Should not crash. + } + + /// + /// Invalid search path should not crash. + /// + [TestMethod] + public void Regress286699_InvalidSearchPath() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; + t.SearchPaths = new string[] { "|" }; + + bool retval = Execute(t); + + Assert.IsFalse(retval); + + // Should not crash. + } + + /// + /// Invalid app.config path should not crash. + /// + [TestMethod] + public void Regress286699_InvalidAppConfig() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; + t.AppConfigFile = "|"; + + bool retval = Execute(t); + + Assert.IsFalse(retval); + + // Should not crash. + } + + /// + /// Make sure that nonexistent references are just eliminated. + /// + [TestMethod] + public void NonExistentReference() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + t.Assemblies = new ITaskItem[] { + new TaskItem("System.Xml"), new TaskItem("System.Nonexistent") + }; + t.SearchPaths = new string[] { Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName), "{AssemblyFolders}", "{HintPathFromItem}", "{RawFileName}" }; + t.Execute(); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(0 == String.Compare(ToolLocationHelper.GetPathToDotNetFrameworkFile("System.Xml.dll", TargetDotNetFrameworkVersion.Version45), t.ResolvedFiles[0].ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// Consider this situation. + /// + /// Assembly A + /// References: B (a simple name) + /// + /// Assembly B + /// Assembly Name: B, PKT=aaa, Version=bbb, Culture=ccc + /// + /// A does _not_ want to load B because it simple name B does not match the + /// B's assembly name. + /// + /// Because of this, we want to be sure that if A asks for B (as a simple name) + /// that we don't find a strongly named assembly. + /// + [TestMethod] + public void StrongWeakMismatchInDependency() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + t.Assemblies = new ITaskItem[] + { + new TaskItem("DependsOnSimpleA") + }; + + t.SearchPaths = new string[] { @"c:\MyApp", @"c:\MyStronglyNamed", @"c:\MyWeaklyNamed" }; + Execute(t); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + Assert.AreEqual(@"c:\MyWeaklyNamed\A.dll", t.ResolvedDependencyFiles[0].ItemSpec); + } + + /// + /// If an Item has a HintPath and there is a {HintPathFromItem} in the SearchPaths + /// property, then the task should be able to resolve an assembly there. + /// + [TestMethod] + public void UseSuppliedHintPath() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing UseSuppliedHintPath() test"); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + ITaskItem i = new TaskItem("My.Assembly"); + + i.SetMetadata("HintPath", @"C:\myassemblies\My.Assembly.dll"); + i.SetMetadata("Baggage", @"Carry-On"); + t.Assemblies = new ITaskItem[] { i }; + t.SearchPaths = DefaultPaths; + Execute(t); + Assert.AreEqual(@"C:\myassemblies\My.Assembly.dll", t.ResolvedFiles[0].ItemSpec); + Assert.AreEqual(1, t.ResolvedFiles.Length); + + // All attributes, including HintPath, should be forwarded from input to output + Assert.AreEqual(@"C:\myassemblies\My.Assembly.dll", t.ResolvedFiles[0].GetMetadata("HintPath")); + Assert.AreEqual(@"Carry-On", t.ResolvedFiles[0].GetMetadata("Baggage")); + } + + /// + /// Regress this devices bug. + /// If a simple name is provided then we need to accept the first simple file name match. + /// Devices frameworks files are signed with a different PK so there should be no unification + /// with normal fx files. + /// + [TestMethod] + public void Regress200872() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; + t.SearchPaths = new string[] + { + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion.PocketPC\mscorlib.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Do the most basic AssemblyFoldersEx resolve. + /// + [TestMethod] + public void AssemblyFoldersExBasic() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyGrid") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\MyGrid.dll", t.ResolvedFiles[0].ItemSpec); + AssertNoCase(@"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// Verify that higher alphabetical values for a component are chosen over lower alphabetic values of a component. + /// + [TestMethod] + public void AssemblyFoldersExVerifyComponentFolderSorting() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("CustomComponent") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponentsB\CustomComponent.dll", t.ResolvedFiles[0].ItemSpec); + AssertNoCase(@"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// If the target framework version provided by the targets file doesn't begin + /// with the letter "v", we should tolerate it and treat it as if it does. + /// + [TestMethod] + public void AssemblyFoldersExTargetFrameworkVersionDoesNotBeginWithV() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyGrid") }; + t.SearchPaths = new string[] { @"{Registry:Software\Microsoft\.NetFramework,2.0,AssemblyFoldersEx}" }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\MyGrid.dll", t.ResolvedFiles[0].ItemSpec); + AssertNoCase(@"{Registry:Software\Microsoft\.NetFramework,2.0,AssemblyFoldersEx}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target AMD64 and try to get an assembly out of the X86 directory. + /// Expect it not to resolve and get a message on the console + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchDoesNotMatch() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "AMD64"; + Execute(t); + + Assert.AreEqual(0, t.ResolvedFiles.Length); + string message = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.TargetedProcessorArchitectureDoesNotMatch", @"C:\Regress714052\X86\A.dll", "X86", "AMD64"); + mockEngine.AssertLogContains(message); + } + + /// + /// Regress DevDiv Bugs 714052. + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target MSIL and get an assembly out of the X86 directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchMSILX86() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "MSIL"; + t.WarnOrErrorOnTargetArchitectureMismatch = "None"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(mockEngine.Warnings == 0); + Assert.IsTrue(mockEngine.Errors == 0); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,X86}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// Verify if there is a mismatch between what the project targets and the architecture of the resolved primary reference log a warning. + /// + [TestMethod] + public void VerifyProcessArchitectureMismatchWarning() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A"), new TaskItem("B") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "MSIL"; + t.WarnOrErrorOnTargetArchitectureMismatch = "Warning"; + Execute(t); + + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.IsTrue(mockEngine.Warnings == 2); + Assert.IsTrue(mockEngine.Errors == 0); + mockEngine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", "MSIL", @"A", "X86"); + mockEngine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", "MSIL", @"B", "X86"); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,X86}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// Verify if there is a mismatch between what the project targets and the architecture of the resolved primary reference log a warning. + /// + [TestMethod] + public void VerifyProcessArchitectureMismatchWarningDefault() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A"), new TaskItem("B") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "MSIL"; + Execute(t); + + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.IsTrue(mockEngine.Warnings == 2); + Assert.IsTrue(mockEngine.Errors == 0); + mockEngine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", "MSIL", @"A", "X86"); + mockEngine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", "MSIL", @"B", "X86"); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,X86}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// Verify if there is a mismatch between what the project targets and the architecture of the resolved primary reference log a error. + /// + [TestMethod] + public void VerifyProcessArchitectureMismatchError() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A"), new TaskItem("B") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "MSIL"; + t.WarnOrErrorOnTargetArchitectureMismatch = "Error"; + Execute(t); + + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.IsTrue(mockEngine.Warnings == 0); + Assert.IsTrue(mockEngine.Errors == 2); + mockEngine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", "MSIL", @"A", "X86"); + mockEngine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.MismatchBetweenTargetedAndReferencedArch", "MSIL", @"B", "X86"); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,X86}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target None and get an assembly out of the X86 directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchNoneX86() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "NONE"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,X86}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// If we are targeting NONE and there are two assemblies with the same name then we want to pick the first one rather than look for an assembly which + /// has a MSIL architecture or a NONE architecture. NONE means you do not care what architecure is picked. + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchNoneMix() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,MIX}" }; + t.TargetProcessorArchitecture = "NONE"; + t.WarnOrErrorOnTargetArchitectureMismatch = "Error"; // should not do anything + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(mockEngine.Warnings == 0); + Assert.IsTrue(mockEngine.Errors == 0); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Equals(@"C:\Regress714052\Mix\a.winmd", StringComparison.OrdinalIgnoreCase)); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,Mix}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Assume the folders are searched in the order A and B. A contains an x86 assembly and B contains an MSIL assembly. + /// When targeting MSIL we want to return the MSIL assembly even if we find one in a previous folder first. + /// Target MSIL and get an assembly out of the MSIL directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchMSILLastFolder() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,AssemblyFoldersEx}" }; + t.TargetProcessorArchitecture = "MSIL"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(t.ResolvedFiles[0].ItemSpec, @"C:\Regress714052\MSIL\A.dll"); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,AssemblyFoldersEX}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Assume the folders are searched in the order A and B. A contains an x86 assembly and B contains an MSIL assembly. + /// When targeting None we want to return the MSIL assembly even if we find one in a previous folder first. + /// Target None and get an assembly out of the MSIL directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchNoneLastFolder() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,AssemblyFoldersEx}" }; + t.TargetProcessorArchitecture = "None"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(t.ResolvedFiles[0].ItemSpec, @"C:\Regress714052\MSIL\A.dll"); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,AssemblyFoldersEX}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Assume the folders are searched in the order A and B. A contains an x86 assembly and B contains an MSIL assembly. + /// When targeting X86 we want to return the MSIL assembly even if we find one in a previous folder first. + /// Target MSIL and get an assembly out of the MSIL directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchX86FirstFolder() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,AssemblyFoldersEx}" }; + t.TargetProcessorArchitecture = "X86"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(t.ResolvedFiles[0].ItemSpec, @"C:\Regress714052\X86\A.dll"); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,AssemblyFoldersEX}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target X86 and get an assembly out of the MSIL directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchX86MSIL() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,MSIL}" }; + t.TargetProcessorArchitecture = "X86"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,MSIL}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target X86 and get an assembly out of the None directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchX86None() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,None}" }; + t.TargetProcessorArchitecture = "X86"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,None}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target None and get an assembly out of the None directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchNoneNone() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,None}" }; + t.TargetProcessorArchitecture = "None"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,None}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target MSIL and get an assembly out of the None directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArcMSILNone() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,None}" }; + t.TargetProcessorArchitecture = "MSIL"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,None}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target None and get an assembly out of the MSIL directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchNoneMSIL() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,MSIL}" }; + t.TargetProcessorArchitecture = "None"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,MSIL}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target MSIL and get an assembly out of the MSIL directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchMSILMSIL() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine mockEngine = new MockEngine(); + t.BuildEngine = mockEngine; + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,MSIL}" }; + t.TargetProcessorArchitecture = "MSIL"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,MSIL}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// The above but now requires us to make sure the processor architecture of what we are targeting matches what we are resolving. + /// + /// Target X86 and get an assembly out of the X86 directory. + /// + /// + [TestMethod] + public void AssemblyFoldersExProcessorArchMatches() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("A") }; + t.SearchPaths = new string[] { @"{Registry:Software\Regress714052,v2.0.0,X86}" }; + t.TargetProcessorArchitecture = "X86"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\Regress714052\X86\A.dll", t.ResolvedFiles[0].ItemSpec); + AssertNoCase(@"{Registry:Software\Regress714052,v2.0.0,X86}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// If the target framework version specified in the registry search path + /// provided by the targets file has some bogus value, we should just ignore it. + /// + /// This means if there are remaining search paths to inspect, we should + /// carry on and inspect those. + /// + [TestMethod] + public void AssemblyFoldersExTargetFrameworkVersionBogusValue() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + ITaskItem assemblyToResolve = new TaskItem("MyGrid"); + assemblyToResolve.SetMetadata("HintPath", @"C:\MyComponents\MyGrid.dll"); + t.Assemblies = new ITaskItem[] { assemblyToResolve }; + t.SearchPaths = new string[] { @"{Registry:Software\Microsoft\.NetFramework,x.y.z,AssemblyFoldersEx}", "{HintPathFromItem}" }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue + ( + t.ResolvedFiles[0].GetMetadata("ResolvedFrom").Equals("{HintPathFromItem}", StringComparison.OrdinalIgnoreCase), + "Assembly should have been resolved from HintPathFromItem!" + ); + } + + /// + /// Tolerate keys like v2.0.x86chk. + /// + [TestMethod] + public void Regress357227_AssemblyFoldersExAgainstRawDrop() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyRawDropControl") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyRawDropControls\MyRawDropControl.dll", t.ResolvedFiles[0].ItemSpec); + AssertNoCase(@"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", t.ResolvedFiles[0].GetMetadata("ResolvedFrom")); + } + + /// + /// Matches that exist only in the HKLM hive. + /// + [TestMethod] + public void AssemblyFoldersExHKLM() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyHKLMControl") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\HKLM Components\MyHKLMControl.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Matches that exist in both HKLM and HKCU should favor HKCU + /// + [TestMethod] + public void AssemblyFoldersExHKCUTrumpsHKLM() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing AssemblyFoldersExHKCUTrumpsHKLM() test"); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyHKLMandHKCUControl") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\HKCU Components\MyHKLMandHKCUControl.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// When matches that have v3.0 (future) and v2.0 (current) versions, the 2.0 version wins. + /// + [TestMethod] + public void AssemblyFoldersExFutureTargetNDPVersionsDontMatch() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyControlWithFutureTargetNDPVersion") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\v2.0\MyControlWithFutureTargetNDPVersion.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// If there is no v2.0 (current target NDP) match, then v1.0 should match. + /// + [TestMethod] + public void AssemblyFoldersExMatchBackVersion() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyNDP1Control") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\v1.0\MyNDP1Control.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// If there is a 2.0 and a 1.0 then match 2.0. + /// + [TestMethod] + public void AssemblyFoldersExCurrentTargetVersionTrumpsPastTargetVersion() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyControlWithPastTargetNDPVersion") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponents\v2.0\MyControlWithPastTargetNDPVersion.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// If a control has a service pack then that wins over the control itself + /// + [TestMethod] + public void AssemblyFoldersExServicePackTrumpsBaseVersion() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyControlWithServicePack") }; + t.SearchPaths = DefaultPaths; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\MyComponentServicePack2\MyControlWithServicePack.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Conditions (OSVersion/Platform) can be passed in SearchPaths to filter the result. + /// Test MaxOSVersion condition + /// + [TestMethod] + public void AssemblyFoldersExConditionFilterMaxOS() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing AssemblyFoldersExConditionFilterMaxOS() test"); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyDeviceControlAssembly") }; + t.SearchPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"c:\MyProject", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"{Registry:Software\Microsoft\.NETCompactFramework,v2.0,PocketPC\AssemblyFoldersEx,OSVersion=4.0.0:Platform=3C41C503-53EF-4c2a-8DD4-A8217CAD115E}", + "{AssemblyFolders}", + "{HintPathFromItem}" + }; + + SetupAssemblyFoldersExTestConditionRegistryKey(); + + try + { + Execute(t); + } + finally + { + RemoveAssemblyFoldersExTestConditionRegistryKey(); + } + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\V1ControlSP1\MyDeviceControlAssembly.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Conditions (OSVersion/Platform) can be passed in SearchPaths to filter the result. + /// Test MinOSVersion condition + /// + [TestMethod] + public void AssemblyFoldersExConditionFilterMinOS() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyDeviceControlAssembly") }; + t.SearchPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"c:\MyProject", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"{Registry:Software\Microsoft\.NETCompactFramework,v2.0,PocketPC\AssemblyFoldersEx,OSVersion=5.1.0:Platform=3C41C503-53EF-4c2a-8DD4-A8217CAD115E}", + "{AssemblyFolders}", + "{HintPathFromItem}" + }; + + SetupAssemblyFoldersExTestConditionRegistryKey(); + + try + { + Execute(t); + } + finally + { + RemoveAssemblyFoldersExTestConditionRegistryKey(); + } + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\V1Control\MyDeviceControlAssembly.dll", t.ResolvedFiles[0].ItemSpec); + } + + [TestMethod] + public void GatherVersions10DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v1.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 2); + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions20DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v2.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 3); + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions30DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v3.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 6); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v3.0SP1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v3.0 BAZ", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersionsVDotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 27); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v5.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.0001.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0.255.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v4.0.255", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v4.0.0000", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[9].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[10].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[11].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[12].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[13].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[14].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[15].RegistryKey).Equals("v3.0SP1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[16].RegistryKey).Equals("v3.0 BAZ", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[17].RegistryKey).Equals("v1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[18].RegistryKey).Equals("v5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[19].RegistryKey).Equals("v3.5.0.x86chk", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[20].RegistryKey).Equals("v3.5.1.x86chk", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[21].RegistryKey).Equals("v3.5.256.x86chk", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[22].RegistryKey).Equals("v", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[23].RegistryKey).Equals("V3.5.0.0.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[24].RegistryKey).Equals("V3..", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[25].RegistryKey).Equals("V-1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[26].RegistryKey).Equals("v9999999999999999", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions35DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v3.5", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 9); + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v3.5.0.x86chk", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v3.5.1.x86chk", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v3.5.256.x86chk", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("V3.5.0.0.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions40DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v4.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 9); + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions400DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v4.0.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 10); + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v4.0.0000", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[9].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions41DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v4.1", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 13); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v4.1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.0.255.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.0.255", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0.0000", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[9].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[10].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[11].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[12].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions410DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v4.1.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 14); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v4.0001.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.0.255.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0.255", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v4.0.0000", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[9].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[10].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[11].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[12].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[13].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + + [TestMethod] + public void GatherVersions40255DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v4.0.255", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 12); + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v4.0.255.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.0.255", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.0.0000", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[9].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[10].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[11].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions5DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v5.0", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 15); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v5.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v4.0001.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[2].RegistryKey).Equals("v4.1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[3].RegistryKey).Equals("v4.0.255.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[4].RegistryKey).Equals("v4.0.255", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[5].RegistryKey).Equals("v4.0.0000", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[6].RegistryKey).Equals("v4.0.9999", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[7].RegistryKey).Equals("v4.0.2116.87", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[8].RegistryKey).Equals("v4.0.2116", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[9].RegistryKey).Equals("v4.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[10].RegistryKey).Equals("v3.5", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[11].RegistryKey).Equals("v3.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[12].RegistryKey).Equals("v2.0.50727", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[13].RegistryKey).Equals("v1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[14].RegistryKey).Equals("v00001.0", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersionsv5DotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v5", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 2); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v5.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(((string)returnedVersions[1].RegistryKey).Equals("v5", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void GatherVersions35x86chkDotNet() + { + List returnedVersions = AssemblyFoldersEx.GatherVersionStrings("v3.5.0.x86chk", s_assemblyFolderExTestVersions); + + Assert.IsNotNull(returnedVersions); + Assert.IsTrue(returnedVersions.Count == 1); + + Assert.IsTrue(((string)returnedVersions[0].RegistryKey).Equals("v3.5.0.x86chk", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Conditions (OSVersion/Platform) can be passed in SearchPaths to filter the result. + /// Test Platform condition + /// + [TestMethod] + public void AssemblyFoldersExConditionFilterPlatform() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("MyDeviceControlAssembly") }; + t.SearchPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"c:\MyProject", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"{Registry:Software\Microsoft\.NETCompactFramework,v2.0,PocketPC\AssemblyFoldersEx,Platform=3C41C503-X-4c2a-8DD4-A8217CAD115E}", + "{AssemblyFolders}", + "{HintPathFromItem}" + }; + + SetupAssemblyFoldersExTestConditionRegistryKey(); + + try + { + Execute(t); + } + finally + { + RemoveAssemblyFoldersExTestConditionRegistryKey(); + } + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"C:\V1Control\MyDeviceControlAssembly.dll", t.ResolvedFiles[0].ItemSpec); + } + + private void SetupAssemblyFoldersExTestConditionRegistryKey() + { + // Setup the following registry keys: + // HKCU\SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl + // @c:\V1Control + // @MinOSVersion=5.0.0 + // HKCU\SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl\1234 + // @c:\V1ControlSP1 + // @MinOSVersion=4.0.0 + // @MaxOSVersion=4.1.0 + // @Platform=4118C335-430C-497f-BE48-11C3316B135E;3C41C503-53EF-4c2a-8DD4-A8217CAD115E + + RegistryKey baseKey = Registry.CurrentUser; + RegistryKey folderKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl"); + folderKey.SetValue("", @"C:\V1Control"); + folderKey.SetValue("MinOSVersion", "5.0.0"); + + RegistryKey servicePackKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl\1234"); + servicePackKey.SetValue("", @"C:\V1ControlSP1"); + servicePackKey.SetValue("MinOSVersion", "4.0.0"); + + servicePackKey.SetValue("MaxOSVersion", "4.1.0"); + servicePackKey.SetValue("Platform", "4118C335-430C-497f-BE48-11C3316B135E;3C41C503-53EF-4c2a-8DD4-A8217CAD115E"); + } + + private void RemoveAssemblyFoldersExTestConditionRegistryKey() + { + RegistryKey baseKey = Registry.CurrentUser; + try + { + baseKey.DeleteSubKeyTree(@"SOFTWARE\Microsoft\.NETCompactFramework\v2.0.3600\PocketPC\AssemblyFoldersEx\AFETestDeviceControl"); + } + catch (Exception) + { + } + } + + + /// + /// CandidateAssemblyFiles are extra files passed in through the CandidateAssemblyFiles + /// that should be considered for matching whem search paths contains {CandidateAssemblyFiles} + /// + [TestMethod] + public void CandidateAssemblyFiles() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("System.XML") }; + t.SearchPaths = new string[] { "{CandidateAssemblyFiles}" }; + t.CandidateAssemblyFiles = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll" }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", t.ResolvedFiles[0].ItemSpec); + } + + + /// + /// Make sure three part version numbers put on the required target framework do not cause a problem. + /// + [TestMethod] + public void ThreePartVersionNumberRequiredFrameworkHigherThanTargetFramework() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + TaskItem item = new TaskItem("System.XML"); + item.SetMetadata("RequiredTargetFramework", "v4.0.255"); + t.Assemblies = new ITaskItem[] { item }; + t.SearchPaths = new string[] { "{CandidateAssemblyFiles}" }; + t.CandidateAssemblyFiles = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll" }; + t.TargetFrameworkVersion = "v4.0"; + Execute(t); + + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Make sure three part version numbers put on the required target framework do not cause a problem. + /// + [TestMethod] + public void ThreePartVersionNumberRequiredFrameworkLowerThanTargetFramework() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + TaskItem item = new TaskItem("System.XML"); + item.SetMetadata("RequiredTargetFramework", "v4.0.255"); + t.Assemblies = new ITaskItem[] { item }; + t.SearchPaths = new string[] { "{CandidateAssemblyFiles}" }; + t.CandidateAssemblyFiles = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll" }; + t.TargetFrameworkVersion = "v4.0.256"; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Try a candidate assembly file that has an extension but no base name. + /// + [TestMethod] + public void Regress242970() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] { new TaskItem("System.XML") }; + t.SearchPaths = new string[] { "{CandidateAssemblyFiles}" }; + t.CandidateAssemblyFiles = new string[] + { + @"NonUI\testDirectoryRoot\.hiddenfile", + @"NonUI\testDirectoryRoot\.dll", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll" + }; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", t.ResolvedFiles[0].ItemSpec); + + // For {CandidateAssemblyFiles} we don't even want to see a comment logged for files with non-standard extensions. + // This is because {CandidateAssemblyFiles} is very likely to contain non-assemblies and its best not to clutter + // up the log. + engine.AssertLogDoesntContain + ( + String.Format(".hiddenfile") + ); + + // ...but we do want to see a log entry for standard extensions, even if the base file name is empty. + engine.AssertLogContains + ( + String.Format(@"NonUI\testDirectoryRoot\.dll") + ); + } + + /// + /// If a file name is passed in through the Assemblies parameter and the search paths contains {RawFileName} + /// then try to resolve directly to that file name. + /// + [TestMethod] + public void RawFileName() + { + // This WriteLine is a hack. On a slow machine, the Tasks unittest fails because remoting + // times out the object used for remoting console writes. Adding a write in the middle of + // keeps remoting from timing out the object. + Console.WriteLine("Performing RawFileName() test"); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll") }; + t.SearchPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"C:\MyProject", + "{TargetFrameworkDirectory}", + @"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", + "{AssemblyFolders}", + "{HintPathFromItem}", + "{GAC}" + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Make sure when there are duplicate entries in the redist list, with different versions of ingac (true and false) that we will not read in two entries, + /// we will instead pick the one with ingac true and ignore the ingac false entry. If there is one of more entries in the redist list with ingac false + /// and no entries with ingac true for a given assembly then we should only have one entry with ingac false. + /// + [TestMethod] + public void TestDuplicateHandlingForRedistLists() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(redistFile, fullRedistListContentsDuplicates); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, null); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(assembliesReadIn.Count == 4); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Make sure that if there are different SimpleName then they will not be considered duplicates. + /// + [TestMethod] + public void TestDuplicateHandling() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + ""; + + ExpectRedistEntries(fullRedistListContentsDuplicates, 1, 0); + } + + /// + /// Make sure that if there are different IsRedistRoot then they will not be considered duplicates. + /// + [TestMethod] + public void TestDuplicateHandlingDifferentIsRedistRoot() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + ""; + + ExpectRedistEntries(fullRedistListContentsDuplicates, 3, 0); + } + + /// + /// Make sure that if there are different IsRedistRoot then they will not be considered duplicates. + /// + [TestMethod] + public void TestDuplicateHandlingDifferentName() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + ""; + + ExpectRedistEntries(fullRedistListContentsDuplicates, 3, 0); + } + + /// + /// Make sure that if there are different culture then they will not be considered duplicates. + /// + [TestMethod] + public void TestDuplicateHandlingDifferentCulture() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + ""; + + ExpectRedistEntries(fullRedistListContentsDuplicates, 3, 0); + } + + /// + /// Make sure that if there are different public key tokens then they will not be considered duplicates. + /// + [TestMethod] + public void TestDuplicateHandlingDifferentPublicKeyToken() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + ""; + + ExpectRedistEntries(fullRedistListContentsDuplicates, 3, 0); + } + + /// + /// Make sure that if there are different retargetable flags then they will not be considered duplicates. + /// + [TestMethod] + public void TestDuplicateHandlingDifferentRetargetable() + { + string fullRedistListContentsDuplicates = + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + ExpectRedistEntries(fullRedistListContentsDuplicates, 2, 1); + } + /// + /// Make sure that if there are different versons that they are all picked + /// + [TestMethod] + public void TestDuplicateHandlingDifferentVersion() + { + string fullRedistListContentsDuplicates = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + List entries = ExpectRedistEntries(fullRedistListContentsDuplicates, 3, 2); + } + + /// + /// Expect to read in a certain number of redist list entries, this is factored out becase we went to test a number of input combinations which will all result in entries returned. + /// + private static List ExpectRedistEntries(string fullRedistListContentsDuplicates, int numberOfExpectedEntries, int numberofExpectedRemapEntries) + { + string redistFile = FileUtilities.GetTemporaryFile(); + List assembliesReadIn = new List(); + List remapEntries = new List(); + try + { + File.WriteAllText(redistFile, fullRedistListContentsDuplicates); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remapEntries); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(assembliesReadIn.Count == numberOfExpectedEntries); + Assert.IsTrue(remapEntries.Count == numberofExpectedRemapEntries); + } + finally + { + File.Delete(redistFile); + } + + return assembliesReadIn; + } + + /// + /// Test the basics of reading in the remapping section + /// + [TestMethod] + public void TestRemappingSectionBasic() + { + string fullRedistListContents = + "" + + "" + + "" + + "" + + ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 1); + + AssemblyRemapping pair = remap[0]; + Assert.IsTrue(pair.From.Name.Equals("System.Xml", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(pair.To.Name.Equals("Remapped", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(pair.From.Retargetable); + Assert.IsFalse(pair.To.Retargetable); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// If there are multiple "To" elements under the "From" element then pick the first one. + /// + [TestMethod] + public void MultipleToElementsUnderFrom() + { + string fullRedistListContents = + "" + + "" + + "" + + "" + + "" + + ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 1); + + AssemblyRemapping pair = remap.First(); + Assert.IsTrue(pair.From.Name.Equals("System.Xml", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(pair.To.Name.Equals("Remapped", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(pair.From.Retargetable); + Assert.IsFalse(pair.To.Retargetable); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// If there are two from tags which map to the same "To" element then we still need two entries. + /// + [TestMethod] + public void DifferentFromsToSameTo() + { + string fullRedistListContents = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 2); + + foreach (AssemblyRemapping pair in remap) + { + Assert.IsTrue(pair.To.Name.Equals("Remapped", StringComparison.OrdinalIgnoreCase)); + Assert.IsFalse(pair.To.Retargetable); + } + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// If there are two identical entries then pick the first one + /// + [TestMethod] + public void DuplicateEntries() + { + string fullRedistListContents = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 1); + + + AssemblyRemapping pair = remap.First(); + Assert.IsTrue(pair.To.Name.Equals("Remapped", StringComparison.OrdinalIgnoreCase)); + Assert.IsFalse(pair.To.Retargetable); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Test if the remapping section is empty + /// + [TestMethod] + public void EmptyRemapping() + { + string fullRedistListContents = ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 0); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Test if the we have a "from" element but no "to" element. We expect that to be ignored + /// + [TestMethod] + public void FromElementButNoToElement() + { + string fullRedistListContents = + "" + + "" + + "" + + "" + + "" + + ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 1); + + AssemblyRemapping pair = remap.First(); + Assert.IsTrue(pair.From.Name.Equals("System.Xml", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(pair.To.Name.Equals("Remapped", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(pair.From.Retargetable); + Assert.IsFalse(pair.To.Retargetable); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Test if the we have a "To" element but no "from" element. We expect that to be ignored + /// + [TestMethod] + public void ToElementButNoFrom() + { + string fullRedistListContents = + "" + + "" + + ""; + + string redistFile = Path.GetTempFileName(); + try + { + File.WriteAllText(redistFile, fullRedistListContents); + + AssemblyTableInfo info = new AssemblyTableInfo(redistFile, String.Empty); + List assembliesReadIn = new List(); + List remap = new List(); + List errors = new List(); + List errorFileNames = new List(); + RedistList.ReadFile(info, assembliesReadIn, errors, errorFileNames, remap); + Assert.IsTrue(errors.Count == 0, "Expected no Errors"); + Assert.IsTrue(errorFileNames.Count == 0, "Expected no Error file names"); + Assert.IsTrue(remap.Count == 0); + } + finally + { + File.Delete(redistFile); + } + } + + + /// + /// If a relative file name is passed in through the Assemblies parameter and the search paths contains {RawFileName} + /// then try to resolve directly to that file name and make it a full path. + /// + [TestMethod] + public void RawFileNameRelative() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + string testPath = Path.Combine(Path.GetTempPath(), @"RawFileNameRelative"); + string previousCurrentDirectory = Environment.CurrentDirectory; + + Directory.CreateDirectory(testPath); + Environment.CurrentDirectory = testPath; + try + { + t.Assemblies = new ITaskItem[] { new TaskItem(@"..\RawFileNameRelative\System.Xml.dll") }; + t.SearchPaths = new string[] { "{RawFileName}" }; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(Path.Combine(testPath, "System.Xml.dll"), t.ResolvedFiles[0].ItemSpec); + } + finally + { + Environment.CurrentDirectory = previousCurrentDirectory; + + if (Directory.Exists(testPath)) + { + Directory.Delete(testPath); + } + } + } + + + /// + /// If a relative searchPath is passed in through the search path parameter + /// then try to resolve the file but make sure it is a full name + /// + [TestMethod] + public void RelativeDirectoryResolver() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + string testPath = Path.Combine(Path.GetTempPath(), @"RawFileNameRelative"); + string previousCurrentDirectory = Environment.CurrentDirectory; + + Directory.CreateDirectory(testPath); + Environment.CurrentDirectory = testPath; + try + { + t.Assemblies = new ITaskItem[] { new TaskItem(@"System.Xml.dll") }; + t.SearchPaths = new string[] { "..\\RawFileNameRelative" }; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(Path.Combine(testPath, "System.Xml.dll"), t.ResolvedFiles[0].ItemSpec); + } + finally + { + Environment.CurrentDirectory = previousCurrentDirectory; + + if (Directory.Exists(testPath)) + { + Directory.Delete(testPath); + } + } + } + + /// + /// If a relative file name is passed in through the HintPath then try to resolve directly to that file name and make it a full path. + /// + [TestMethod] + public void HintPathRelative() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + string testPath = Path.Combine(Path.GetTempPath(), @"RawFileNameRelative"); + string previousCurrentDirectory = Environment.CurrentDirectory; + + Directory.CreateDirectory(testPath); + Environment.CurrentDirectory = testPath; + try + { + TaskItem taskItem = new TaskItem(AssemblyRef.SystemXml); + taskItem.SetMetadata("HintPath", @"..\RawFileNameRelative\System.Xml.dll"); + + t.Assemblies = new ITaskItem[] { taskItem }; + t.SearchPaths = new string[] { "{HintPathFromItem}" }; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(Path.Combine(testPath, "System.Xml.dll"), t.ResolvedFiles[0].ItemSpec); + } + finally + { + Environment.CurrentDirectory = previousCurrentDirectory; + + if (Directory.Exists(testPath)) + { + Directory.Delete(testPath); + } + } + } + /// + /// Make sure we do not crash if a raw file name is passed in and the specific version metadata is set + /// + [TestMethod] + public void RawFileNameWithSpecificVersionFalse() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + ITaskItem taskItem = new TaskItem(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll"); + taskItem.SetMetadata("SpecificVersion", "false"); + + t.Assemblies = new ITaskItem[] { taskItem }; + t.SearchPaths = new string[] + { + "{RawFileName}", + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// Make sure we do not crash if a raw file name is passed in and the specific version metadata is set + /// + [TestMethod] + public void RawFileNameWithSpecificVersionTrue() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + ITaskItem taskItem = new TaskItem(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll"); + taskItem.SetMetadata("SpecificVersion", "true"); + + t.Assemblies = new ITaskItem[] { taskItem }; + t.SearchPaths = new string[] + { + "{RawFileName}", + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// If the user passed in a file name but no {RawFileName} was specified. + /// + [TestMethod] + public void Regress363340_RawFileNameMissing() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Xml.dll"), + new TaskItem(@"System.Data") + }; + + t.SearchPaths = new string[] + { + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Data.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// If the reference include looks like a file name rather than a properly formatted reference and a good hint path is provided, + /// good means the hintpath points to a file which exists on disk. Then we were getting an exception + /// because assemblyName was null and we were comparing the assemblyName from the hintPath to the null assemblyName. + /// + [TestMethod] + public void Regress444793() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + TaskItem item = new TaskItem(@"c:\DoesntExist\System.Xml.dll"); + item.SetMetadata("HintPath", @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Data.dll"); + item.SetMetadata("SpecificVersion", "true"); + t.Assemblies = new ITaskItem[] { item }; + t.SearchPaths = new string[] + { + @"{HintPathFromItem}" + }; + + bool succeeded = Execute(t); + Assert.IsTrue(succeeded); + engine.AssertLogDoesntContain("MSB4018"); + + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("General.MalformedAssemblyName"), "c:\\DoesntExist\\System.Xml.dll") + ); + } + + + /// + /// If a file name is passed in through the Assemblies parameter and the search paths contains {RawFileName} + /// then try to resolve directly to that file name. + /// + [TestMethod] + public void RawFileNameDoesntExist() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] { new TaskItem(@"c:\DoesntExist\System.Xml.dll") }; + t.SearchPaths = new string[] { "{RawFileName}" }; + + bool succeeded = Execute(t); + Assert.IsTrue(succeeded); + engine.AssertLogContains + ( + String.Format(AssemblyResources.GetString("General.MalformedAssemblyName"), "c:\\DoesntExist\\System.Xml.dll") + ); + } + + /// + /// If a candidate file has a different base name, then this should not be a match. + /// + [TestMethod] + public void CandidateAssemblyFilesDifferentBaseName() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem("VendorAssembly") }; + t.SearchPaths = new string[] { "{CandidateAssemblyFiles}" }; + t.CandidateAssemblyFiles = new string[] { @"Dlls\ProjectItemAssembly.dll" }; + + Execute(t); + + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Given a strong name, resolve it to a location in the GAC if possible. + /// + [TestMethod] + public void ResolveToGAC() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] { new TaskItem("System") }; + t.TargetedRuntimeVersion = typeof(Object).Assembly.ImageRuntimeVersion; + t.SearchPaths = new string[] { "{GAC}" }; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles.Length == 1); + } + + /// + /// Given a strong name, resolve it to a location in the GAC if possible. + /// + [TestMethod] + public void ResolveToGACSpecificVersion() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + TaskItem item = new TaskItem("System"); + item.SetMetadata("SpecificVersion", "true"); + t.Assemblies = new ITaskItem[] { item }; + t.SearchPaths = new string[] { "{GAC}" }; + t.TargetedRuntimeVersion = new Version("0.5.0.0").ToString(); + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedFiles.Length == 1); + } + + /// + /// Verify that when we are calculating the search paths for a dependency that we take into account where the parent assembly was resolved from + /// for example if the parent assembly was resolved from the GAC or AssemblyFolders then we do not want to look in the parent assembly directory + /// instead we want to let the assembly be resolved normally so that the GAC and AF checks will work. + /// + [TestMethod] + public void ParentAssemblyResolvedFromAForGac() + { + Hashtable parentReferenceFolderHash = new Hashtable(); + List parentReferenceFolders = new List(); + List referenceList = new List(); + + TaskItem taskItem = new TaskItem("Microsoft.VisualStudio.Interopt, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + reference.FullPath = "c:\\AssemblyFolders\\Microsoft.VisualStudio.Interopt.dll"; + reference.ResolvedSearchPath = "{AssemblyFolders}"; + + Reference reference2 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + reference2.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + reference2.FullPath = "c:\\SomeOtherFolder\\Microsoft.VisualStudio.Interopt2.dll"; + reference2.ResolvedSearchPath = "c:\\SomeOtherFolder"; + + Reference reference3 = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + reference3.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + reference3.FullPath = "c:\\SomeOtherFolder\\Microsoft.VisualStudio.Interopt3.dll"; + reference3.ResolvedSearchPath = "{GAC}"; + + referenceList.Add(reference); + referenceList.Add(reference2); + referenceList.Add(reference3); + + foreach (Reference parentReference in referenceList) + { + ReferenceTable.CalcuateParentAssemblyDirectories(parentReferenceFolderHash, parentReferenceFolders, parentReference); + } + + Assert.IsTrue(parentReferenceFolders.Count == 1); + Assert.IsTrue(parentReferenceFolders[0].Equals(reference2.ResolvedSearchPath, StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// Generate a fake reference which has been resolved from the gac. We will use it to verify the creation of the exclusion list. + /// + /// + private ReferenceTable GenerateTableWithAssemblyFromTheGlobalLocation(string location) + { + ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, new string[0], null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, null, null, null, null, null, null, new Version("4.0"), null, null, null, true, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false); + + AssemblyNameExtension assemblyNameExtension = new AssemblyNameExtension(new AssemblyName("Microsoft.VisualStudio.Interopt, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")); + TaskItem taskItem = new TaskItem("Microsoft.VisualStudio.Interopt, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + + Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + // "Resolve the assembly from the gac" + reference.FullPath = "c:\\Microsoft.VisualStudio.Interopt.dll"; + reference.ResolvedSearchPath = location; + referenceTable.AddReference(assemblyNameExtension, reference); + + assemblyNameExtension = new AssemblyNameExtension(new AssemblyName("Team.System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")); + taskItem = new TaskItem("Team, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); + + reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + + // "Resolve the assembly from the gac" + reference.FullPath = "c:\\Team.System.dll"; + reference.ResolvedSearchPath = location; + referenceTable.AddReference(assemblyNameExtension, reference); + return referenceTable; + } + + /// + /// Given a reference that resolves to a bad image, we should get a warning and + /// no reference. We don't want an exception. + /// + [TestMethod] + public void ResolveBadImageInPrimary() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.Assemblies = new ITaskItem[] + { + new TaskItem("BadImage") + }; + t.Assemblies[0].SetMetadata("Private", "true"); + t.SearchPaths = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + Execute(t); + + // There should be no resolved file, because the image was bad. + Assert.AreEqual(0, t.ResolvedFiles.Length); + + // There should be no related files either. + Assert.AreEqual(0, t.RelatedFiles.Length); + engine.AssertLogDoesntContain("BadImage.pdb"); + engine.AssertLogDoesntContain("HRESULT"); + + // There should have been one warning about the exception. + Assert.AreEqual(1, engine.Warnings); + } + + /// + /// Given a reference that resolves to a bad image, we should get a message, no warning and + /// no reference. We don't want an exception. + /// + [TestMethod] + public void ResolveBadImageInSecondary() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(true); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("DependsOnBadImage") + }; + + t.SearchPaths = new string[] + { + @"c:\Regress563286", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" + }; + Execute(t); + + // There should be one resolved file, because the dependency was bad. + Assert.AreEqual(1, t.ResolvedFiles.Length); + + // There should be no related files. + Assert.AreEqual(0, t.RelatedFiles.Length); + engine.AssertLogDoesntContain("BadImage.pdb"); + engine.AssertLogDoesntContain("HRESULT"); + + // There should have been no warning about the exception because it's only a dependency + Assert.AreEqual(0, engine.Warnings); + } + + /// + /// Test the case where the search path, earlier on, contains an assembly that almost matches + /// but the PKT is wrong. + /// + [TestMethod] + public void ResolveReferenceThatHasWrongPKTInEarlierAssembly() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem(AssemblyRef.SystemData) }; + t.SearchPaths = new string[] + { + @"c:\MyProject", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Data.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// FX assemblies should not be CopyLocal. + /// + [TestMethod] + public void PrimaryFXAssemblyRefIsNotCopyLocal() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { new TaskItem(AssemblyRef.SystemData) }; + t.SearchPaths = new string[] + { + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" + }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.Data.dll", t.ResolvedFiles[0].ItemSpec); + Assert.AreEqual("false", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// If an item is explictly Private=='true' (as opposed to implicitly when the attribute isn't set at all) + /// then it should be CopyLocal true even if its in the FX directory + /// + [TestMethod] + public void PrivateItemInFrameworksGetsCopyLocalTrue() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + // Create the mocks. + Microsoft.Build.Shared.FileExists fileExists = new Microsoft.Build.Shared.FileExists(FileExists); + Microsoft.Build.Shared.DirectoryExists directoryExists = new Microsoft.Build.Shared.DirectoryExists(DirectoryExists); + Microsoft.Build.Tasks.GetDirectories getDirectories = new Microsoft.Build.Tasks.GetDirectories(GetDirectories); + Microsoft.Build.Tasks.GetAssemblyName getAssemblyName = new Microsoft.Build.Tasks.GetAssemblyName(GetAssemblyName); + Microsoft.Build.Tasks.GetAssemblyMetadata getAssemblyMetadata = new Microsoft.Build.Tasks.GetAssemblyMetadata(GetAssemblyMetadata); + + // Also construct a set of assembly names to pass in. + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + }; + + assemblyNames[0].SetMetadata("Private", "true"); // Fx file, but user chose private=true. + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.SearchPaths = DefaultPaths; + Execute(t); + Assert.AreEqual(@"true", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// If we have no framework directories passed in and an assembly is found outside of the GAC then it should be able to be copy local. + /// + [TestMethod] + public void NoFrameworkDirectoriesStillCopyLocal() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + // Also construct a set of assembly names to pass in. + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem(@"C:\AssemblyFolder\SomeAssembly.dll"), + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.TargetFrameworkDirectories = new string[] { }; + t.SearchPaths = new string[] { "{RawFileName}" }; + Execute(t); + Assert.AreEqual(@"true", t.ResolvedFiles[0].GetMetadata("CopyLocal")); + } + + /// + /// If an item has a bad value for a boolean attribute, report a nice error that indicates which attribute it was. + /// + [TestMethod] + public void Regress284485_PrivateItemWithBogusValue() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + // Also construct a set of assembly names to pass in. + ITaskItem[] assemblyNames = new TaskItem[] + { + new TaskItem("System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + }; + + assemblyNames[0].SetMetadata("Private", "bogus"); // Fx file, but user chose private=true. + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.Assemblies = assemblyNames; + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + t.SearchPaths = DefaultPaths; + Execute(t); + + string message = String.Format(AssemblyResources.GetString("General.InvalidAttributeMetadata"), assemblyNames[0].ItemSpec, "Private", "bogus", "bool"); + Assert.IsTrue + ( + engine.Log.Contains + ( + message + ) + ); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 + /// References - B + /// Depends on D version 2 + /// + /// And neither D1 nor D2 are CopyLocal = true. In this case, both dependencies + /// are kept because this will work in a SxS manner. + /// + [TestMethod] + public void ConflictBetweenNonCopyLocalDependencies() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", + @"c:\MyLibraries\v1", + @"c:\MyLibraries\v2" + }; + + Execute(t); + + Assert.AreEqual(2, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\MyLibraries\v2\D.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + Assert.AreEqual(1, t.SuggestedRedirects.Length); + Assert.IsTrue(ContainsItem(t.SuggestedRedirects, @"D, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa"), "Expected to find suggested redirect, but didn't"); + Assert.IsTrue(e.Warnings == 1, "Should only be one warning for suggested redirects."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 + /// References - B + /// Depends on D version 2 + /// + /// And both D1 and D2 are CopyLocal = true. This case is a warning because both + /// assemblies can't be copied to the output directory. + /// + [TestMethod] + public void ConflictBetweenCopyLocalDependencies() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] { + new TaskItem("A"), new TaskItem("B") + }; + + t.SearchPaths = new string[] { + @"c:\MyLibraries", @"c:\MyLibraries\v1", @"c:\MyLibraries\v2" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + Assert.IsTrue(engine.Warnings == 1, @"Expected a warning because this is an unresolvable conflict."); + Assert.AreEqual(1, t.SuggestedRedirects.Length); + Assert.IsTrue(ContainsItem(t.SuggestedRedirects, @"D, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa"), "Expected to find suggested redirect, but didn't"); + Assert.IsTrue(engine.Warnings == 1, "Should only be one warning for suggested redirects."); + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format + ( + AssemblyResources.GetString + ( + "ResolveAssemblyReference.ConflictRedirectSuggestion" + ), + "D, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa", + "1.0.0.0", + "c:\\MyLibraries\\v1\\D.dll", + "2.0.0.0", + "c:\\MyLibraries\\v2\\D.dll" + ) + ) + ); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// Primary References + /// C + /// A version 2 + /// And both A version 2 and C are CopyLocal=true + /// References - C + /// Depends on A version 1 + /// Depends on B + /// References - B + /// Depends on A version 2 + /// + /// + /// Expect to have some information indicating that C and B depend on two different versions of A and that the primary refrence which caused the problems + /// are A and C. + /// + [TestMethod] + public void ConflictBetweenCopyLocalDependenciesRegress444809() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] { + new TaskItem("A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null"), new TaskItem("C, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null") + }; + + t.SearchPaths = new string[] { + @"c:\Regress444809", @"c:\Regress444809\v2" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + ResourceManager resources = new ResourceManager("Microsoft.Build.Tasks.Strings", Assembly.GetExecutingAssembly()); + + //Unresolved primary reference with itemspec "A, Version=20.0.0.0, Culture=Neutral, PublicKeyToken=null". + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.ReferenceDependsOn", "A, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null", @"c:\Regress444809\A.dll"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.ReferenceDependsOn", "A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", @"c:\Regress444809\v2\A.dll"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.PrimarySourceItemsForReference", @"c:\Regress444809\C.dll"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.PrimarySourceItemsForReference", @"c:\Regress444809\B.dll"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.PrimarySourceItemsForReference", @"c:\Regress444809\v2\a.dll"); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// Primary References + /// A version 20 (Un Resolved) + /// B + /// D + /// References - B + /// Depends on A version 2 + /// References - D + /// Depends on A version 20 + /// + /// + /// Expect to have some information indicating that Primary reference A, Reference B and Reference D conflict. + /// + [TestMethod] + public void ConflictBetweenCopyLocalDependenciesRegress444809UnResolvedPrimaryReference() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] { + new TaskItem("A, Version=20.0.0.0, Culture=Neutral, PublicKeyToken=null"), + new TaskItem("B, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null"), + new TaskItem("D, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null") + }; + + t.SearchPaths = new string[] { + @"c:\Regress444809", @"c:\Regress444809\v2" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.ReferenceDependsOn", "A, Version=20.0.0.0, Culture=Neutral, PublicKeyToken=null", String.Empty); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.ReferenceDependsOn", "A, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", @"c:\Regress444809\v2\A.dll"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.UnResolvedPrimaryItemSpec", "A, Version=20.0.0.0, Culture=Neutral, PublicKeyToken=null"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.PrimarySourceItemsForReference", @"c:\Regress444809\D.dll"); + engine.AssertLogContainsMessageFromResource(resourceDelegate, "ResolveAssemblyReference.PrimarySourceItemsForReference", @"c:\Regress444809\B.dll"); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 + /// References - B + /// Depends on D version 2 + /// + /// And both D1 and D2 are CopyLocal = true. In this case, there is no warning because + /// AutoUnify is set to true. + /// + [TestMethod] + public void ConflictBetweenCopyLocalDependenciesWithAutoUnify() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.AutoUnify = true; + + t.Assemblies = new ITaskItem[] { + new TaskItem("A"), new TaskItem("B") + }; + + t.SearchPaths = new string[] { + @"c:\MyLibraries", @"c:\MyLibraries\v1", @"c:\MyLibraries\v2" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + // RAR will now produce suggested redirects even if AutoUnify is on. + Assert.AreEqual(1, t.SuggestedRedirects.Length); + Assert.IsTrue(engine.Warnings == 0, "Should be no warning for suggested redirects."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 + /// References - B + /// Depends on D version 2 + /// References - D, version 1 + /// + /// Both D1 and D2 are CopyLocal. This is a warning because D1 is a lower version + /// than D2 so that can't unify. These means that eventually when they're copied + /// to the output directory they'll conflict. + /// + [TestMethod] + public void ConflictWithBackVersionPrimary() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("B"), + new TaskItem("A"), + new TaskItem("D, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + Assert.IsTrue(e.Warnings == 1, @"Expected one warning."); + + Assert.AreEqual(0, t.SuggestedRedirects.Length); + Assert.AreEqual(3, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Same as ConflictWithBackVersionPrimary, except AutoUnify is true. + /// Even when AutoUnify is set we should see a warning since the binder will not allow + /// an older version to satisfy a reference to a newer version. + /// + [TestMethod] + public void ConflictWithBackVersionPrimaryWithAutoUnify() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("B"), + new TaskItem("A"), + new TaskItem("D, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + + t.AutoUnify = true; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + Assert.IsTrue(e.Warnings == 1, @"Expected one warning."); + + Assert.AreEqual(0, t.SuggestedRedirects.Length); + Assert.AreEqual(3, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 + /// References - B + /// Depends on D version 2 + /// References - D, version 2 + /// + /// Both D1 and D2 are CopyLocal. This is not an error because D2 is a higher version + /// than D1 so that can unify. D2 should be output as a Primary and D1 should be output + /// as a dependency. + /// + [TestMethod] + public void ConflictWithForeVersionPrimary() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("B"), + new TaskItem("A"), + new TaskItem("D, Version=2.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + + t.SearchPaths = new string[] { + @"c:\MyLibraries", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + Assert.IsTrue(result, @"Expected a success because this conflict is solvable."); + Assert.AreEqual(3, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v2\D.dll"), "Expected to find assembly, but didn't."); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + } + + + /// + /// Consider this dependency chain: + /// + /// App + /// References - D, version 1 + /// References - D, version 2 + /// + /// Both D1 and D2 are CopyLocal. This is an error because both D1 and D2 can't be copied to + /// the output directory. + /// + [TestMethod] + public void ConflictBetweenBackAndForeVersionsCopyLocal() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("D, Version=2.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa"), + new TaskItem("D, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + Assert.IsTrue(e.Warnings == 2, @"Expected a warning because this is an unresolvable conflict."); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v2\D.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - D, version 1 + /// References - D, version 2 + /// + /// Neither D1 nor D2 are CopyLocal. This is a solveable conflict because D2 has a higher version + /// than D1 and there won't be an output directory conflict. + /// + [TestMethod] + public void ConflictBetweenBackAndForeVersionsNotCopyLocal() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("D, Version=2.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa"), + new TaskItem("D, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + + t.SearchPaths = new string[] { + @"c:\MyLibraries", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1" + }; + + bool result = Execute(t); + + Assert.IsTrue(result, @"Expected success because this conflict is solvable."); + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v2\D.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1, PKT=XXXX + /// References - C + /// Depends on D version 1, PKT=YYYY + /// + /// We can't tell which should win because the PKTs are different. This should be an error. + /// + [TestMethod] + public void ConflictingDependenciesWithNonMatchingNames() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("C") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v1", @"c:\RogueLibraries\v1" + }; + + bool result = Execute(t); + Assert.IsTrue(result, "Execute should have failed because of insoluble conflict."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1, PKT=XXXX + /// References - C + /// Depends on D version 1, PKT=YYYY + /// References - D version 1, PKT=XXXX + /// + /// D, PKT=XXXX should win because its referenced in the project. + /// + /// + [TestMethod] + public void ConflictingDependenciesWithNonMatchingNamesAndHardReferenceInProject() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("C"), + new TaskItem("D, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v1", @"c:\RogueLibraries\v1" + }; + + Execute(t); + + Assert.AreEqual(3, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// A reference with a bogus version is provided. However, the user has chosen + /// SpecificVersion='false' so we match the first one we come across. + /// + [TestMethod] + public void SpecificVersionFalse() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem(@"System.XML, Version=9.9.9999.9, Culture=neutral, PublicKeyToken=abababababababab") + }; + + t.Assemblies[0].SetMetadata("SpecificVersion", "false"); + + t.SearchPaths = DefaultPaths; + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(@"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion\System.XML.dll", t.ResolvedFiles[0].ItemSpec); + } + + /// + /// A reference with a bogus version is provided and the user has chosen SpecificVersion=true. + /// In this case, since there is no specific version that can be matched, no reference is returned. + /// + [TestMethod] + public void SpecificVersionTrue() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { + new TaskItem(@"System.XML, Version=9.9.9999.9, Culture=neutral, PublicKeyToken=abababababababab") + }; + + t.Assemblies[0].SetMetadata("SpecificVersion", "true"); + + t.SearchPaths = DefaultPaths; + Execute(t); + + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// A reference with a bogus version is provided and the user has left off SpecificVersion. + /// In this case assume SpecificVersion=true implicitly. + /// + [TestMethod] + public void SpecificVersionAbsent() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] { + new TaskItem(@"System.XML, Version=9.9.9999.9, Culture=neutral, PublicKeyToken=abababababababab") + }; + + t.SearchPaths = DefaultPaths; + Execute(t); + + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + + /// + /// Unresolved primary references should result in warnings. + /// + [TestMethod] + public void Regress199998() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine m = new MockEngine(); + t.BuildEngine = m; + + t.Assemblies = new ITaskItem[] + { + // An assembly that is unresolvable because it doesn't exist. + new TaskItem(@"System.XML, Version=9.9.9999.9, Culture=neutral, PublicKeyToken=abababababababab") + }; + + t.SearchPaths = DefaultPaths; + Execute(t); + + Assert.AreEqual(0, t.ResolvedFiles.Length); + // One warning for the un-resolved reference and one warning saying you are trying to target an assembly higher than the current target + // framework. + Assert.AreEqual(1, m.Warnings); + } + + + /// + /// In this case, + /// - A single primary file reference to simple name "A". + /// - The reference has an .exe tag. + /// - Both a.exe and a.dll exist on disk. + /// Expected: + /// - The resulting assembly returned should be a.exe + /// Rationale: + /// The user browsed to an .exe, so that's what we should give them. + /// + [TestMethod] + public void ExecutableExtensionEXE() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.Assemblies[0].SetMetadata("ExecutableExtension", ".eXe"); + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", + @"c:\MyExecutableLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyExecutableLibraries\a.exe"), "Expected to find assembly, but didn't."); + } + + /// + /// In this case, + /// - A single primary file reference to simple name "A". + /// - The reference has an .dll tag. + /// - Both a.exe and a.dll exist on disk. + /// Expected: + /// - The resulting assembly returned should be a.dll + /// Rationale: + /// The user browsed to a .dll, so that's what we should give them. + /// + [TestMethod] + public void ExecutableExtensionDLL() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.Assemblies[0].SetMetadata("ExecutableExtension", ".DlL"); + + t.SearchPaths = new string[] + { + @"c:\MyExecutableLibraries", + @"c:\MyLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\a.DlL"), "Expected to find assembly, but didn't."); + } + + /// + /// In this case, + /// - A single primary file reference to simple name "A". + /// - The reference has no tag. + /// - Both a.exe and a.dll exist on disk. + /// - A.dll is first in the search order. + /// Expected: + /// - The resulting assembly returned should be a.dll + /// Rationale: + /// Without an ExecutableExtension the first assembly out of .dll,.exe wins. + /// + [TestMethod] + public void ExecutableExtensionDefaultDLLFirst() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", + @"c:\MyExecutableLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\a.DlL"), "Expected to find assembly, but didn't."); + } + + /// + /// In this case, + /// - A single primary file reference to simple name "A". + /// - The reference has no tag. + /// - Both a.exe and a.dll exist on disk. + /// - A.exe is first in the search order. + /// Expected: + /// - The resulting assembly returned should be a.exe + /// Rationale: + /// Without an ExecutableExtension the first assembly out of .dll,.exe wins. + /// + [TestMethod] + public void ExecutableExtensionDefaultEXEFirst() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.SearchPaths = new string[] + { + @"c:\MyExecutableLibraries", + @"c:\MyLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyExecutableLibraries\A.exe"), "Expected to find assembly, but didn't."); + } + + /// + /// In this case, + /// - A single primary file reference to simple name "A". + /// - The reference has true tag. + /// - An assembly with a strong fusion name "A, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" exists first in the search order. + /// - An assembly with a weak fusion name "A, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" is second in the search order. + /// Expected: + /// - This is an unresolved reference. + /// Rationale: + /// If specific version is true, but the reference is a simple name like "A", then there is no way to do a specific version match. + /// This is a corner case. Other solutions that might have been just as good: + /// - Fall back to SpecificVersion=false behavior. + /// - Only match "A, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null". Note that all of our default VS projects have at least a version number. + /// + [TestMethod] + public void SimpleNameWithSpecificVersionTrue() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + t.Assemblies[0].SetMetadata("SpecificVersion", "true"); + + t.SearchPaths = new string[] + { + @"c:\MyStronglyNamed", + @"c:\MyLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 1, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// In this case, + /// - A single primary file reference to simple name "A". + /// - The reference has true tag. + /// - An assembly with a strong fusion name "A, PKT=..., Version=..., Culture=..." exists first in the search order. + /// - An assembly with a weak fusion name "A, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" is second in the search order. + /// Expected: + /// - The resulting assembly returned should be the strongly named a.dll. + /// Rationale: + /// If specific version is false, then we should match the first "A" that we find. + /// + [TestMethod] + public void SimpleNameWithSpecificVersionFalse() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + t.Assemblies[0].SetMetadata("SpecificVersion", "false"); + + t.SearchPaths = new string[] + { + @"c:\MyStronglyNamed", + @"c:\MyLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyStronglyNamed\A.dll"), "Expected to find assembly, but didn't."); + } + + + /// + /// Consider this situation: + /// + /// App + /// References - D, version 1, IrreleventKeyValue=poo. + /// + /// There's plenty of junk that might end up in a fusion name that have nothing to do with + /// assembly resolution. Make sure we can tolerate this for primary references. + /// + [TestMethod] + public void IrrelevantAssemblyNameElement() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("D, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aaaaaaaaaaaaaaaa, IrreleventKeyValue=poo"), + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion" }; + + bool result = Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + } + + + /// + /// Regress EVERETT QFE 626 + /// Consider this dependency chain: + /// + /// App + /// References - A (Private=undefined) + /// Depends on D + /// Depends on E + /// References - D (Private=false) + /// + /// - Reference A does not have a Private attribute, but resolves to CopyLocal=true. + /// - Reference D has explicit Private=false. + /// - D would normally be CopyLocal=true. + /// - E would normally be CopyLocal=true. + /// + /// Expected: + /// - D should be CopyLocal=false because the of the matching Reference D which has explicit private=false. + /// - E should be CopyLocal=false because it's a dependency of D which has explicit private=false. + /// + /// Rationale: + /// This is QFE 626. If the user has set "Copy Local" to "false" in VS (means Private=false) + /// then even if this turns out to be a dependency too, we still shouldn't copy. + /// + /// + [TestMethod] + public void RegressQFE626() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("D") + }; + t.Assemblies[1].SetMetadata("Private", "false"); + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v1", @"c:\MyLibraries\v1\E" + }; + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + + Execute(t); + + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); // Not 2 because D is treated as a primary reference. + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\MyLibraries\v1\E\E.dll"), "Expected to find assembly, but didn't."); + Assert.AreEqual(0, engine.Warnings); + Assert.AreEqual(0, engine.Errors); + + foreach (ITaskItem item in t.ResolvedDependencyFiles) + { + if (0 == String.Compare(item.ItemSpec, @"c:\MyLibraries\v1\E\E.dll", StringComparison.OrdinalIgnoreCase)) + { + Assert.AreEqual("false", item.GetMetadata("CopyLocal")); + } + } + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A (private=false) + /// Depends on D v1 + /// Depends on E + /// References - B (private=true) + /// Depends on D v2 + /// Depends on E + /// + /// Reference A is explicitly Private=false. + /// Reference B is explicitly Private=true. + /// Dependencies D and E would normally be CopyLocal=true. + /// + /// Expected: + /// - D will be CopyLocal=false because it's dependency of A, which is private=false. + /// - E will be CopyLocal=true because all source primary references aren't private=false. + /// + /// Rationale: + /// Dependencies will be CopyLocal=false if all source primary references are Private=false. + /// + /// + [TestMethod] + public void Regress265054() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") + }; + t.Assemblies[0].SetMetadata("Private", "false"); + t.Assemblies[1].SetMetadata("Private", "true"); + + t.SearchPaths = new string[] + { + @"c:\MyLibraries", @"c:\MyLibraries\v1", @"c:\MyLibraries\v2", @"c:\MyLibraries\v1\E" + }; + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + + Execute(t); + + Assert.AreEqual(2, t.ResolvedFiles.Length); + Assert.AreEqual(3, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\MyLibraries\v1\D.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\MyLibraries\v1\E\E.dll"), "Expected to find assembly, but didn't."); + + foreach (ITaskItem item in t.ResolvedDependencyFiles) + { + if (0 == String.Compare(item.ItemSpec, @"c:\MyLibraries\v1\D.dll", StringComparison.OrdinalIgnoreCase)) + { + Assert.AreEqual("false", item.GetMetadata("CopyLocal")); + } + + if (0 == String.Compare(item.ItemSpec, @"c:\MyLibraries\v1\E\E.dll", StringComparison.OrdinalIgnoreCase)) + { + Assert.AreEqual("true", item.GetMetadata("CopyLocal")); + } + } + } + + /// + /// Here's how you get into this situation: + /// + /// App + /// References - A + /// References - B + /// Depends on A + /// + /// And, the following conditions. + /// Primary "A" has no explicit Version (i.e. it's a simple name) + /// Primary "A" *is not* resolved. + /// Dependency "A" *is* resolved. + /// + /// Expected result: + /// * No exceptions. + /// * Build error about unresolved primary reference. + /// + /// + [TestMethod] + public void Regress312873_UnresolvedPrimaryWithResolveDependency() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B"), + + // We need a one more "A" because the bug was in a Compare function + // called by .Sort. We need enough items to guarantee that A with null version + // will be on the left side of a compare. + new TaskItem("A") +}; + + t.Assemblies[1].SetMetadata("HintPath", @"C:\Regress312873\b.dll"); + t.Assemblies[2].SetMetadata("HintPath", @"C:\Regress312873-2\a.dll"); + + t.SearchPaths = new string[] + { + @"{HintPathFromItem}" + }; + + Execute(t); + } + + /// + /// We weren't handling scatter assemblies. + /// + /// App + /// References - A + /// + /// And, the following conditions. + /// Primary "A" has has two scatter files "M1" and "M2" + /// + /// Expected result: + /// * M1 and M2 should be output in ScatterFiles and CopyLocal. + /// + /// + [TestMethod] + public void Regress275161_ScatterAssemblies() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\Regress275161\a.dll"); + + t.SearchPaths = new string[] + { + @"{HintPathFromItem}" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + + Execute(t); + + Assert.IsTrue + ( + ContainsItem(t.ScatterFiles, @"C:\Regress275161\m1.netmodule"), + "Expected to find scatter file m1." + ); + + Assert.IsTrue + ( + ContainsItem(t.ScatterFiles, @"C:\Regress275161\m2.netmodule"), + "Expected to find scatter file m2." + ); + + Assert.IsTrue + ( + ContainsItem(t.CopyLocalFiles, @"C:\Regress275161\m1.netmodule"), + "Expected to find scatter file m1 in CopyLocalFiles." + ); + + Assert.IsTrue + ( + ContainsItem(t.CopyLocalFiles, @"C:\Regress275161\m2.netmodule"), + "Expected to find scatter file m2 in CopyLocalFiles." + ); + } + + /// + /// We weren't handling scatter assemblies. + /// + /// App + /// References - A + /// Depends on B v1.0.0.0 + /// References - B v2.0.0.0 + /// + /// + /// And, the following conditions. + /// * All assemblies are resolved. + /// * All assemblies are CopyLocal=true. + /// * Notice the conflict between versions of B. + /// + /// Expected result: + /// * During conflict resolution, B v2.0.0.0 should win. + /// * B v1.0.0.0 should still be listed in dependencies (there's not a strong case for this either way) + /// * B v1.0.0.0 should be CopyLocal='false' + /// + /// + [TestMethod] + public void Regress317975_LeftoverLowerVersion() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") +}; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\Regress317975\a.dll"); + t.Assemblies[1].SetMetadata("HintPath", @"C:\Regress317975\v2\b.dll"); + + t.SearchPaths = new string[] + { + @"{HintPathFromItem}" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + + Execute(t); + + foreach (ITaskItem i in t.ResolvedDependencyFiles) + { + Assert.IsTrue(0 == String.Compare(i.GetMetadata("CopyLocal"), "false", StringComparison.OrdinalIgnoreCase)); + } + + Assert.IsTrue + ( + ContainsItem(t.ResolvedDependencyFiles, @"C:\Regress317975\B.dll"), + "Expected to find lower version listed in dependencies." + ); + } + + /// + /// Mscorlib is special in that it doesn't always have complete metadata. For example, + /// GetAssemblyName can return null. This was confusing the {RawFileName} resolution path, + /// which is fairly different from the other code paths. + /// + /// App + /// References - "c:\path-to-mscorlib\mscorlib.dll" (Current FX) + /// + /// Expected result: + /// * Even though mscorlib.dll doesn't have an assembly name, we should be able to return + /// a result. + /// + /// NOTES: + /// * This test works because path-to-mscorlib is the same as the path to the FX folder. + /// Because of this, the hard-cache is used rather than actually calling GetAssemblyName + /// on mscorlib.dll. This isn't going to work in cases where mscorlib is from an FX other + /// than the current target. See the Part2 for a test that covers this other case. + /// + /// + [TestMethod] + public void Regress313086_Part1_MscorlibAsRawFilename() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem(typeof(object).Module.FullyQualifiedName.ToLower()) +}; + + t.SearchPaths = new string[] + { + @"{RawFileName}" + }; + + t.TargetFrameworkDirectories = new string[] { Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName) }; + + t.Execute(); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + } + + /// + /// Mscorlib is special in that it doesn't always have complete metadata. For example, + /// GetAssemblyName can return null. This was confusing the {RawFileName} resolution path, + /// which is fairly different from the other code paths. + /// + /// App + /// References - "c:\path-to-mscorlib\mscorlib.dll" (non-Current FX) + /// + /// Expected result: + /// * Even though mscorlib.dll doesn't have an assembly name, we should be able to return + /// a result. + /// + /// NOTES: + /// * This test is covering the case where mscorlib.dll is coming from somewhere besides + /// the main (ie Whidbey) FX. + /// + /// + [TestMethod] + public void Regress313086_Part2_MscorlibAsRawFilename() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem(@"c:\Regress313086\mscorlib.dll") +}; + + t.SearchPaths = new string[] + { + @"{RawFileName}" + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\myfx" }; + + Execute(t); + + Assert.AreEqual(1, t.ResolvedFiles.Length); + } + + + /// + /// If a directory path is passed into AssemblyFiles, then we should warn and continue on. + /// + [TestMethod] + public void Regress284466_DirectoryIntoAssemblyFiles() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll"), + new TaskItem(Path.GetTempPath()) + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + AssertNoCase("UnifyMe, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL", t.ResolvedFiles[0].GetMetadata("FusionName")); + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("General.ExpectedFileGotDirectory"), Path.GetTempPath()) + ) + ); + } + + /// + /// If a relative assemblyFile is passed in resolve it as a full path. + /// + [TestMethod] + public void RelativeAssemblyFiles() + { + string testPath = Path.Combine(Path.GetTempPath(), @"RelativeAssemblyFiles"); + string previousCurrentDirectory = Environment.CurrentDirectory; + + Directory.CreateDirectory(testPath); + Environment.CurrentDirectory = testPath; + try + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"..\RelativeAssemblyFiles\System.Xml.dll") + }; + + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + + bool succeeded = Execute(t); + + Assert.IsTrue(succeeded); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.AreEqual(Path.Combine(testPath, "System.Xml.dll"), t.ResolvedFiles[0].ItemSpec); + } + finally + { + Environment.CurrentDirectory = previousCurrentDirectory; + + if (Directory.Exists(testPath)) + { + Directory.Delete(testPath); + } + } + } + + + /// + /// Behave gracefully if a referenced assembly is inaccessible to the user. + /// + [TestMethod] + public void Regress316906_UnauthorizedAccessViolation_PrimaryReferenceIsInaccessible() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + t.Assemblies[0].SetMetadata("SpecificVersion", "false"); + + t.SearchPaths = new string[] + { + @"c:\MyInaccessible" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 1, "One warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// Behave gracefully if a referenced assembly is inaccessible to the user. + /// In this case, the file is still resolved because it was passed in directly. + /// There's no way to determine dependencies however. + /// + [TestMethod] + public void Regress316906_UnauthorizedAccessViolation_PrimaryFileIsInaccessible() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.AssemblyFiles = new ITaskItem[] + { + new TaskItem(@"c:\MyInaccessible\A.dll") + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + } + + + /// + /// Behave gracefully if a referenced assembly is inaccessible to the user. + /// + [TestMethod] + public void Regress316906_UnauthorizedAccessViolation_PrimaryAsRawFileIsInaccessible() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem(@"c:\MyInaccessible\A.dll") + }; + t.SearchPaths = new string[] { "{RawFileName}" }; + + + Execute(t); + + Assert.IsTrue(e.Warnings == 1, "One warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + + + /// + /// If there's a SearhPath like {Registry:,,} then still behave nicely. + /// + [TestMethod] + public void Regress269704_MissingRegistryElements() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + t.Assemblies[0].SetMetadata("SpecificVersion", "false"); + + t.SearchPaths = new string[] + { + @"{Registry:,,}", + @"c:\MyAssemblyDoesntExistHere" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 1, "No warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); + } + + /// + /// 1. Create a C# classlibrary, and build it. + /// 2. Go to disk, and rename ClassLibrary1.dll (or whatever it was) to Foo.dll + /// 3. Create a C# console application. + /// 4. In the console app, add a File reference to Foo.dll. + /// 5. Build the console app. + /// + /// RESULTS (before bugfix): + /// ======================== + /// MSBUILD : warning : Couldn't resolve this reference. Could not locate assembly "ClassLibrary1" + /// + /// EXPECTED (after bugfix): + /// ======================== + /// We think it might be reasonable for the ResolveAssemblyReference task to correctly resolve + /// this reference, especially given the fact that the HintPath was provided in the project file. + /// + [TestMethod] + public void Regress276548_AssemblyNameDifferentThanFusionName() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + t.Assemblies[0].SetMetadata + ( + "HintPath", + @"c:\MyNameMismatch\Foo.dll" + ); + + t.SearchPaths = new string[] + { + @"{HintPathFromItem}" + }; + + Execute(t); + + + Assert.IsTrue(e.Warnings == 0, "One warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + } + + /// + /// When very long paths are passed in we should be robust. + /// + [TestMethod] + public void Regress314573_VeryLongPaths() + { + string veryLongPath = @"C:\" + new String('a', 260); + string veryLongFile = veryLongPath + "\\A.dll"; + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") // Resolved by HintPath + }; + t.Assemblies[0].SetMetadata + ( + "HintPath", + veryLongFile + ); + + t.SearchPaths = new string[] + { + "{HintPathFromItem}" + }; + + t.AssemblyFiles = new ITaskItem[] + { + new TaskItem(veryLongFile) // Resolved as File Reference + }; + + Execute(t); + + + Assert.IsTrue(e.Warnings == 1, "One warning expected in this scenario."); // Couldn't find dependencies for {HintPathFromItem}-resolved item. + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(0, t.ResolvedFiles.Length); // This test used to have 1 here. But that was because the mock GetAssemblyName was not accurately throwing an exception for non-existent files. + } + + + /// + /// Need to be robust in the face of assembly names with special characters. + /// + [TestMethod] + public void Regress265003_EscapedCharactersInFusionName() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("\\=A\\=, Version=2.0.0.0, Culture=neUtral, PublicKeyToken=b77a5c561934e089"), // Characters that should be escaped in fusion names: \ , " ' = + new TaskItem("__\\'ASP\\'dw0024ry") + }; + + t.Assemblies[0].SetMetadata("SpecificVersion", "false"); // Important to this bug. + t.Assemblies[1].SetMetadata("HintPath", @"c:\MyEscapedName\__'ASP'dw0024ry.dll"); + t.TargetFrameworkDirectories = new string[] { Path.GetDirectoryName(typeof(object).Module.FullyQualifiedName) }; + + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}", + @"{HintPathFromItem}", + @"c:\MyEscapedName" + }; + + Execute(t); + + + Assert.IsTrue(e.Warnings == 0, "One warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(2, t.ResolvedFiles.Length); + } + + /// + /// If we're given bogus Include (one with characters that would normally need escaping) but we also + /// have a hintpath, then go ahead and resolve anyway because we know what the path should be. + /// + [TestMethod] + public void Regress284081_UnescapedCharactersInFusionNameWithHintPath() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("__'ASP'dw0024ry") // Would normally require quoting for the tick marks. + }; + + t.Assemblies[0].SetMetadata("HintPath", @"c:\MyEscapedName\__'ASP'dw0024ry.dll"); + + t.SearchPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"c:\MyProject", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", + "{AssemblyFolders}", + "{HintPathFromItem}" + }; + + Execute(t); + + + Assert.IsTrue(e.Warnings == 0, "No warning expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + } + + /// + /// Everett supported assembly names that had .dll at the end. + /// + [TestMethod] + public void Regress366322_ReferencesWithFileExtensions() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A.dll") // User really meant a fusion name here. + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries" + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\a.DlL"), "Expected to find assembly, but didn't."); + } + + /// + /// Support for multiple framework directories. + /// + [TestMethod] + public void Regress366814_MultipleFrameworksFolders() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.TargetFrameworkDirectories = new string[] { @"c:\boguslocation", @"c:\MyLibraries" }; + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}", + }; + + Execute(t); + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\MyLibraries\a.DlL"), "Expected to find assembly, but didn't."); + } + + /// + /// If the App.Config file has a bad .XML then handle it gracefully. + /// (i.e. no exception is thrown from the task. + /// + [TestMethod] + public void Regress271273_BogusAppConfig() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ITaskItem[] assemblyFiles = new TaskItem[] + { + new TaskItem(@"C:\MyComponents\v1.0\UnifyMe.dll") + }; + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + ); + + try + { + // Now, pass feed resolved primary references into ResolveAssemblyReference. + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = engine; + t.AssemblyFiles = assemblyFiles; + t.SearchPaths = DefaultPaths; + t.AppConfigFile = appConfigFile; + + Execute(t); + } + finally + { + // Cleanup. + File.Delete(appConfigFile); + } + } + + /// + /// The user might pass in a HintPath that has a trailing slash. Need to not crash. + /// + /// + [TestMethod] + public void Regress354669_HintPathWithTrailingSlash() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\Regress354669\"); + + + t.SearchPaths = new string[] + { + "{RawFileName}", + "{CandidateAssemblyFiles}", + @"c:\MyProject", + @"c:\WINNT\Microsoft.NET\Framework\v2.0.MyVersion", + @"{Registry:Software\Microsoft\.NetFramework,v2.0,AssemblyFoldersEx}", + "{AssemblyFolders}", + "{HintPathFromItem}" + }; + Execute(t); + } + + /// + /// The user might pass in a HintPath that has a trailing slash. Need to not crash. + /// + /// Assembly A + /// References: C, version 2 + /// + /// Assembly B + /// References: C, version 1 + /// + /// There is an App.Config file that redirects all versions of C to V2. + /// Assemblies A and B are both located via their HintPath. + /// + /// + [TestMethod] + public void Regress339786_CrossVersionsWithAppConfig() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("B"), + new TaskItem("A"), + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\Regress339786\FolderB\B.dll"); + t.Assemblies[1].SetMetadata("HintPath", @"C:\Regress339786\FolderA\A.dll"); + + // Construct the app.config. + string appConfigFile = WriteAppConfig + ( + " \n" + + " \n" + + " \n" + + " \n" + ); + t.AppConfigFile = appConfigFile; + + try + { + t.SearchPaths = new string[] + { + "{HintPathFromItem}" + }; + Execute(t); + } + finally + { + File.Delete(appConfigFile); + } + + Assert.AreEqual(1, t.ResolvedDependencyFiles.Length); + } + + /// + /// An older LKG of the CLR could throw a FileLoadException if it doesn't recognize + /// the assembly. We need to support this for dogfooding purposes. + /// + [TestMethod] + public void Regress_DogfoodCLRThrowsFileLoadException() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("DependsMyFileLoadExceptionAssembly") + }; + + t.SearchPaths = new string[] + { + @"c:\OldClrBug" + }; + Execute(t); + } + + + /// + /// There was a bug in which any file mentioned in the InstalledAssemblyTables was automatically + /// considered to be a file present in the framework directory. This assumption was originally true, + /// but became false when Crystal Reports started putting their assemblies in this table. + /// + [TestMethod] + public void Regress407623_RedistListDoesNotImplyPresenceInFrameworks() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("CrystalReportsAssembly") + }; + + t.Assemblies[0].SetMetadata("SpecificVersion", "false"); // Important to this bug. + t.TargetFrameworkDirectories = new string[] { @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx" }; + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}", // Assembly is not here. + @"c:\Regress407623" // Assembly is here. + }; + + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistFile) }; + + Execute(t); + } + finally + { + File.Delete(redistFile); + } + + Assert.IsTrue(e.Warnings == 0, "No warnings expected in this scenario."); + Assert.IsTrue(e.Errors == 0, "No errors expected in this scenario."); + Assert.AreEqual(1, t.ResolvedFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"c:\Regress407623\CrystalReportsAssembly.dll"), "Expected to find assembly, but didn't."); + } + + /// + /// If an invalid file name is passed to InstalledAssemblyTables we expect a warning even if no other redist lists are passed. + /// + [TestMethod] + public void InvalidCharsInInstalledAssemblyTable() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("SomeAssembly") + }; + + + t.TargetFrameworkDirectories = new string[] { @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx" }; + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem("asdfasdfasjr390rjfiogatg~~!@@##$%$%%^&**()") }; + + Execute(t); + e.AssertLogContains("MSB3250"); + } + + /// + /// Here's how you get into this situation: + /// + /// App + /// References - Microsoft.Build.Engine + /// Hintpath = C:\Regress435487\microsoft.build.engine.dll + /// + /// And, the following conditions. + /// microsoft.build.engine.dll has the redistlist InGac=true flag set. + /// + /// Expected result: + /// * For the assembly to be CopyLocal=true + /// + /// + [TestMethod] + public void Regress435487_FxFileResolvedByHintPathShouldByCopyLocal() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Microsoft.Build.Engine") + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\Regress435487\microsoft.build.engine.dll"); + + + t.SearchPaths = new string[] + { + @"{HintPathFromItem}", + @"{TargetFrameworkDirectory}" + }; + t.TargetFrameworkDirectories = new string[] { @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx" }; + + string redistFile = FileUtilities.GetTemporaryFile(); + + try + { + File.Delete(redistFile); + + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistFile) }; + + Execute(t); + } + finally + { + File.Delete(redistFile); + } + + Assert.IsTrue(t.ResolvedFiles[0].GetMetadata("CopyLocal") == "true", "Expected CopyLocal==true."); + } + + /// + /// Verify when doing partial name matching with the assembly name that we also correctly do the partial name matching when trying to find + /// assemblies from the redist list. + /// + [TestMethod] + public void PartialNameMatchingFromRedist() + { + string redistFile = FileUtilities.GetTemporaryFile(); + + try + { + File.Delete(redistFile); + + File.WriteAllText + ( + redistFile, + "" + + // Simple name match where everything is the same except for version + "" + + "" + + "" + + "" + ); + + AssemblyName v1 = new AssemblyName("A, Culture=de-DE, PublicKeyToken=a5d015c7d5a0b012, Version=1.0.0.0"); + AssemblyName v2 = new AssemblyName("A, Culture=Neutral, PublicKeyToken=a5d015c7d5a0b012, Version=2.0.0.0"); + AssemblyName v3 = new AssemblyName("A, Culture=de-DE, PublicKeyToken=null, Version=3.0.0.0"); + + AssemblyNameExtension Av1 = new AssemblyNameExtension(v1); + AssemblyNameExtension Av2 = new AssemblyNameExtension(v2); + AssemblyNameExtension Av3 = new AssemblyNameExtension(v3); + + + AssemblyTableInfo assemblyTableInfo = new AssemblyTableInfo(redistFile, "MyFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { assemblyTableInfo }); + InstalledAssemblies installedAssemblies = new InstalledAssemblies(redistList); + + AssemblyNameExtension assemblyName = new AssemblyNameExtension("A"); + AssemblyNameExtension foundAssemblyName = FrameworkPathResolver.GetHighestVersionInRedist(installedAssemblies, assemblyName); + Assert.IsTrue(foundAssemblyName.Equals(Av3)); + + assemblyName = new AssemblyNameExtension("A, PublicKeyToken=a5d015c7d5a0b012"); + foundAssemblyName = FrameworkPathResolver.GetHighestVersionInRedist(installedAssemblies, assemblyName); + Assert.IsTrue(foundAssemblyName.Equals(Av2)); + + assemblyName = new AssemblyNameExtension("A, Culture=de-DE"); + foundAssemblyName = FrameworkPathResolver.GetHighestVersionInRedist(installedAssemblies, assemblyName); + Assert.IsTrue(foundAssemblyName.Equals(Av3)); + + assemblyName = new AssemblyNameExtension("A, PublicKeyToken=a5d015c7d5a0b012, Culture=de-DE"); + foundAssemblyName = FrameworkPathResolver.GetHighestVersionInRedist(installedAssemblies, assemblyName); + Assert.IsTrue(foundAssemblyName.Equals(Av1)); + + assemblyName = new AssemblyNameExtension("A, Version=17.0.0.0, PublicKeyToken=a5d015c7d5a0b012, Culture=de-DE"); + foundAssemblyName = FrameworkPathResolver.GetHighestVersionInRedist(installedAssemblies, assemblyName); + Assert.IsTrue(foundAssemblyName.Equals(assemblyName)); + } + finally + { + File.Delete(redistFile); + } + } + + [TestMethod] + public void Regress46599_BogusInGACValueForAssemblyInRedistList() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Microsoft.Build.Engine"), + new TaskItem("System.Xml") + }; + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}" + }; + t.TargetFrameworkDirectories = new string[] { @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx" }; + + FileExists cachedFileExists = fileExists; + GetAssemblyName cachedGetAssemblyName = getAssemblyName; + string redistFile = CreateGenericRedistList(); + + bool success = false; + try + { + fileExists = new FileExists(delegate (string path) + { + if (String.Equals(path, @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx\Microsoft.Build.Engine.dll", StringComparison.OrdinalIgnoreCase) || + String.Equals(path, @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx\System.Xml.dll", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("RarCache", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + }); + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistFile) }; + + success = Execute(t); + } + finally + { + fileExists = cachedFileExists; + getAssemblyName = cachedGetAssemblyName; + File.Delete(redistFile); + } + + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(2, t.ResolvedFiles.Length, "Expected two resolved assemblies."); + } + + [TestMethod] + public void VerifyFrameworkFileMetadataFiles() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + // In framework directory and redist, should have metadata + new TaskItem("Microsoft.Build.Engine"), + new TaskItem("System.Xml"), + // In framework directory, should have metadata + new TaskItem("B"), + // Not in framework directory but in redist, should have metadata + new TaskItem("C"), + // Not in framework directory and not in redist, should not have metadata + new TaskItem("D") + }; + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}", + @"c:\Somewhere\" + }; + t.TargetFrameworkDirectories = new string[] { @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx" }; + + FileExists cachedFileExists = fileExists; + GetAssemblyName cachedGetAssemblyName = getAssemblyName; + + // Create a redist list which will contains both of the assemblies to search for + string redistListContents = + "" + + "" + + "" + + "" + + ""; + + string redistFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(redistFile, redistListContents); + + bool success = false; + try + { + fileExists = new FileExists(delegate (string path) + { + if (String.Equals(path, @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx\Microsoft.Build.Engine.dll", StringComparison.OrdinalIgnoreCase) || + String.Equals(path, @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx\System.Xml.dll", StringComparison.OrdinalIgnoreCase) || + String.Equals(path, @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx\B.dll", StringComparison.OrdinalIgnoreCase) || + String.Equals(path, @"c:\somewhere\c.dll", StringComparison.OrdinalIgnoreCase) || + String.Equals(path, @"c:\somewhere\d.dll", StringComparison.OrdinalIgnoreCase) || + path.EndsWith("RarCache", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + }); + + getAssemblyName = new GetAssemblyName(delegate (string path) + { + if (String.Equals(path, @"r:\WINDOWS\Microsoft.NET\Framework\v2.0.myfx\B.dll", StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("B, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + if (String.Equals(path, @"c:\somewhere\d.dll", StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("D, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + return null; + }); + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistFile) }; + + success = Execute(t); + } + finally + { + fileExists = cachedFileExists; + getAssemblyName = cachedGetAssemblyName; + File.Delete(redistFile); + } + + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(5, t.ResolvedFiles.Length, "Expected two resolved assemblies."); + Assert.IsTrue(t.ResolvedFiles.Where(Item => Item.GetMetadata("OriginalItemSpec").Equals("Microsoft.Build.Engine", StringComparison.OrdinalIgnoreCase)).First().GetMetadata("FrameworkFile").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles.Where(Item => Item.GetMetadata("OriginalItemSpec").Equals("System.Xml", StringComparison.OrdinalIgnoreCase)).First().GetMetadata("FrameworkFile").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles.Where(Item => Item.GetMetadata("OriginalItemSpec").Equals("B", StringComparison.OrdinalIgnoreCase)).First().GetMetadata("FrameworkFile").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles.Where(Item => Item.GetMetadata("OriginalItemSpec").Equals("C", StringComparison.OrdinalIgnoreCase)).First().GetMetadata("FrameworkFile").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedFiles.Where(Item => Item.GetMetadata("OriginalItemSpec").Equals("D", StringComparison.OrdinalIgnoreCase)).First().GetMetadata("FrameworkFile").Length == 0); + } + + /// + /// Create a redist file which is used by many different tests + /// + /// Path to the redist list + private static string CreateGenericRedistList() + { + // Create a redist list which will contains both of the assemblies to search for + string redistListContents = + "" + + "" + + "" + + ""; + + string tempFile = FileUtilities.GetTemporaryFile(); + File.WriteAllText(tempFile, redistListContents); + return tempFile; + } + + + [TestMethod] + public void GetRedistListPathsFromDisk_ThrowsArgumentNullException() + { + bool caughtArgumentNullException = false; + + try + { + RedistList.GetRedistListPathsFromDisk(null); + } + catch (ArgumentNullException) + { + caughtArgumentNullException = true; + } + + Assert.IsTrue(caughtArgumentNullException, "Public method RedistList.GetRedistListPathsFromDisk should throw ArgumentNullException when its argument is null!"); + } + + /// + /// Test the case where the redist list is empty and we pass in an empty set of white lists + /// We should return null as there is no point generating a white list if there is nothing to subtract from. + /// ResolveAssemblyReference will see this as null and log a warning indicating no redist assemblies were found therefore no black list could be + /// generated + /// + [TestMethod] + public void RedistListGenerateBlackListEmptyAssemblyInfoNoRedistAssemblies() + { + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[0]); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[0], whiteListErrors, whiteListErrorFileNames); + Assert.IsNull(blackList, "Should return null if the AssemblyTableInfo is empty and the redist list is empty"); + } + + /// + /// Verify that when we go to generate a black list but there were no subset list files passed in that we get NO black list genreated as there is nothing to subtract. + /// Nothing meaning, we dont have any matching subset list files to say there are no good files. + /// + [TestMethod] + public void RedistListGenerateBlackListEmptyAssemblyInfoWithRedistAssemblies() + { + string redistFile = CreateGenericRedistList(); + try + { + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[0], whiteListErrors, whiteListErrorFileNames); + + + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Test the case where the subset lists cannot be read. The expectation is that the black list will be empty as we have no proper white lists to compare it to. + /// + [TestMethod] + public void RedistListGenerateBlackListNotFoundSubsetFiles() + { + string redistFile = CreateGenericRedistList(); + try + { + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + + Hashtable blackList = redistList.GenerateBlackList( + new AssemblyTableInfo[] + { + new AssemblyTableInfo("c:\\RandomDirectory.xml", "TargetFrameworkDirectory"), + new AssemblyTableInfo("c:\\AnotherRandomDirectory.xml", "TargetFrameworkDirectory") + }, + whiteListErrors, + whiteListErrorFileNames + ); + + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + Assert.IsTrue(whiteListErrors.Count == 2, "Expected there to be two errors in the whiteListErrors, one for each missing file"); + Assert.IsTrue(whiteListErrorFileNames.Count == 2, "Expected there to be two errors in the whiteListErrorFileNames, one for each missing file"); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Test the case where there is random goo in the subsetList file. Expect the file to not be read in and a warning indicating the file was skipped due to a read error. + /// This should also cause the white list to be empty as the badly formatted file was the only whitelist subset file. + /// + [TestMethod] + public void RedistListGenerateBlackListGarbageSubsetListFiles() + { + string redistFile = CreateGenericRedistList(); + string garbageSubsetFile = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText + ( + garbageSubsetFile, + "RandomGarbage, i am a bad file with random goo rather than anything important" + ); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(garbageSubsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + Assert.IsTrue(whiteListErrors.Count == 1, "Expected there to be an error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 1, "Expected there to be an error in the whiteListErrorFileNames"); + Assert.IsTrue(!((Exception)whiteListErrors[0]).Message.Contains("MSB3257"), "Expect to not have the null redist warning"); + } + finally + { + File.Delete(redistFile); + File.Delete(garbageSubsetFile); + } + } + + /// + /// Inputs: + /// Redist list which has entries and has a redist name + /// Subset list which has no redist name but has entries + /// + /// Expected: + /// Expect a warning that a redist list or subset list has no redist name. + /// There should be no black list generated as no sub set lists were read in. + /// + /// Rational: + /// If we have no redist name to compare to the redist list redist name we cannot subtract the lists correctly. + /// + [TestMethod] + public void RedistListNoSubsetListName() + { + string redistFile = CreateGenericRedistList(); + string subsetFile = FileUtilities.GetTemporaryFile(); + try + { + string subsetListContents = + "" + + "" + + "" + + ""; + File.WriteAllText(subsetFile, subsetListContents); + + + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(subsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // If the names do not match then i expect there to be no black list items + Assert.IsTrue(blackList.Count == 0, "Expected to have no assembly in the black list"); + Assert.IsTrue(whiteListErrors.Count == 1, "Expected there to be one error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 1, "Expected there to be one error in the whiteListErrorFileNames"); + string message = ResourceUtilities.FormatResourceString("ResolveAssemblyReference.NoSubSetRedistListName", subsetFile); + Assert.IsTrue(((Exception)whiteListErrors[0]).Message.Contains(message), "Expected assertion to contain correct error code"); + } + finally + { + File.Delete(redistFile); + File.Delete(subsetFile); + } + } + + /// + /// Inputs: + /// Redist list which has entries but no redist name + /// Subset list which has a redist name and entries + /// + /// Expected: + /// Expect no black list to be generated and no warnigns to be emitted + /// + /// Rational: + /// Since the redist list name is null or empty we have no way of matching any subset list up to it. + /// + [TestMethod] + public void RedistListNullkRedistListName() + { + string redistFile = FileUtilities.GetTemporaryFile(); + string subsetFile = FileUtilities.GetTemporaryFile(); + try + { + string subsetListContents = + "" + + "" + + "" + + ""; + File.WriteAllText(subsetFile, subsetListContents); + + string redistListContents = + "" + + "" + + "" + + ""; + File.WriteAllText(redistFile, redistListContents); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(subsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // If the names do not match then i expect there to be no black list items + Assert.IsTrue(blackList.Count == 0, "Expected to have no assembly in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no errors in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no errors in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(subsetFile); + } + } + + /// + /// Inputs: + /// Redist list which has entries and has a redist name + /// Subset list which has entries but has a different redist name than the redist list + /// + /// Expected: + /// There should be no black list generated as no sub set lists with matching names were found. + /// + /// Rational: + /// If the redist name does not match then that subset list should not be subtracted from the redist list. + /// We only add assemblies to the black list if there is a corosponding white list even if it is empty to inform us what assemblies are good and which are not. + /// + [TestMethod] + public void RedistListDifferentNameToSubSet() + { + string redistFile = CreateGenericRedistList(); + string subsetFile = FileUtilities.GetTemporaryFile(); + try + { + string subsetListContents = + "" + + "" + + "" + + ""; + File.WriteAllText(subsetFile, subsetListContents); + + + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(subsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // If the names do not match then i expect there to be no black list items + Assert.IsTrue(blackList.Count == 0, "Expected to have no assembly in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(subsetFile); + } + } + + /// + /// Test the case where the subset list has the same name as the redist list but it has no entries In this case + /// the black list should contain ALL redist list entries becasue there are no white list files to remove from the black list. + /// + [TestMethod] + public void RedistListEmptySubsetMatchingName() + { + string redistFile = CreateGenericRedistList(); + string subsetFile = FileUtilities.GetTemporaryFile(); + try + { + string subsetListContents = + "" + + ""; + File.WriteAllText(subsetFile, subsetListContents); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(subsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // If the names do not match then i expect there to be no black list items + Assert.IsTrue(blackList.Count == 2, "Expected to have two assembly in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + + ArrayList whiteListErrors2 = new ArrayList(); + ArrayList whiteListErrorFileNames2 = new ArrayList(); + Hashtable blackList2 = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + Assert.ReferenceEquals(blackList, blackList2); + } + finally + { + File.Delete(redistFile); + File.Delete(subsetFile); + } + } + + /// + /// Test the case where, no redist assemblies are read in. + /// In this case no blacklist can be generated. + /// We should get a warning informing us that we could not create a black list. + /// + [TestMethod] + public void RedistListNoAssembliesinRedistList() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Microsoft.Build.Engine"), + new TaskItem("System.Xml") + }; + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}" + }; + + string redistListPath = FileUtilities.GetTemporaryFile(); + string subsetListPath = FileUtilities.GetTemporaryFile(); + File.WriteAllText(subsetListPath, _xmlOnlySubset); + try + { + File.WriteAllText + ( + redistListPath, + "RANDOMBOOOOOGOOGOOG" + ); + + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + t.InstalledAssemblySubsetTables = new ITaskItem[] { new TaskItem(subsetListPath) }; + + Execute(t); + MockEngine engine = (MockEngine)t.BuildEngine; + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.NoRedistAssembliesToGenerateExclusionList")); + } + finally + { + File.Delete(redistListPath); + File.Delete(subsetListPath); + } + } + + /// + /// Test the case where the subset list is a subset of the redist list. Make sure that + /// even though there are two files in the redist list that only one shows up in the black list. + /// + [TestMethod] + public void RedistListGenerateBlackListGoodListsSubsetIsSubsetOfRedist() + { + string redistFile = CreateGenericRedistList(); ; + string goodSubsetFile = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(goodSubsetFile, _engineOnlySubset); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(goodSubsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + Assert.IsTrue(blackList.Count == 1, "Expected to have one assembly in the black list"); + Assert.IsTrue(blackList.ContainsKey("System.Xml, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a"), "Expected System.xml to be in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(goodSubsetFile); + } + } + + /// + /// Test the case where we generate a black list based on a set of subset file paths, and then ask for + /// another black list using the same file paths. We expect to get the exact same Hashtable out + /// as it should be pulled from the cache. + /// + [TestMethod] + public void RedistListGenerateBlackListVerifyBlackListCache() + { + string redistFile = CreateGenericRedistList(); + string goodSubsetFile = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(goodSubsetFile, _engineOnlySubset); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(goodSubsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 1, "Expected to have one assembly in the black list"); + Assert.IsTrue(blackList.ContainsKey("System.Xml, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a"), "Expected System.xml to be in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + + List whiteListErrors2 = new List(); + List whiteListErrorFileNames2 = new List(); + Hashtable blackList2 = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors2, whiteListErrorFileNames2); + Assert.ReferenceEquals(blackList, blackList2); + } + finally + { + File.Delete(redistFile); + File.Delete(goodSubsetFile); + } + } + + /// + /// Test the case where the white list and the redist list are identical + /// In this case the black list should be empty. + /// + /// We are also in a way testing the combining of subset files as we read in one assembly from two + /// different subset lists while the redist list already contains both assemblies. + /// + [TestMethod] + public void RedistListGenerateBlackListGoodListsSubsetIsSameAsRedistList() + { + string redistFile = CreateGenericRedistList(); + string goodSubsetFile = FileUtilities.GetTemporaryFile(); + string goodSubsetFile2 = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(goodSubsetFile, _engineOnlySubset); + File.WriteAllText(goodSubsetFile2, _xmlOnlySubset); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(goodSubsetFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo2 = new AssemblyTableInfo(goodSubsetFile2, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo, subsetListInfo2 }, whiteListErrors, whiteListErrorFileNames); + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(goodSubsetFile); + } + } + + /// + /// Test the case where the white list is a superset of the redist list. + /// This means there are more assemblies in the white list than in the black list. + /// + /// The black list should be empty. + /// + [TestMethod] + public void RedistListGenerateBlackListGoodListsSubsetIsSuperSet() + { + string redistFile = CreateGenericRedistList(); + string goodSubsetFile = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText + ( + goodSubsetFile, + "" + + "" + + "" + + "" + + "" + ); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(goodSubsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(goodSubsetFile); + } + } + + /// + /// Check to see if comparing the assemblies in the redist list to the ones in the subset + /// list are case sensitive or not, they should not be case sensitive. + /// + [TestMethod] + public void RedistListGenerateBlackListGoodListsCheckCaseInsensitive() + { + string redistFile = CreateGenericRedistList(); + string goodSubsetFile = FileUtilities.GetTemporaryFile(); + try + { + File.WriteAllText(goodSubsetFile, _engineAndXmlSubset.ToUpperInvariant()); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(goodSubsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFileNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFileNames); + + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFileNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(goodSubsetFile); + } + } + + /// + /// Verify that when we go to generate a black list but there were no subset list files passed in that we get NO black list genreated as there is nothing to subtract. + /// Nothing meaning, we dont have any matching subset list files to say there are no good files. + /// + [TestMethod] + public void RedistListGenerateBlackListGoodListsMultipleIdenticalAssembliesInRedistList() + { + string redistFile = FileUtilities.GetTemporaryFile(); + string goodSubsetFile = FileUtilities.GetTemporaryFile(); + try + { + // Create a redist list which will contains both of the assemblies to search for + string redistListContents = + "" + + "" + + "" + + "" + + ""; + + File.WriteAllText(redistFile, redistListContents); + File.WriteAllText(goodSubsetFile, _engineAndXmlSubset); + + AssemblyTableInfo redistListInfo = new AssemblyTableInfo(redistFile, "TargetFrameworkDirectory"); + AssemblyTableInfo subsetListInfo = new AssemblyTableInfo(goodSubsetFile, "TargetFrameworkDirectory"); + RedistList redistList = RedistList.GetRedistList(new AssemblyTableInfo[] { redistListInfo }); + List whiteListErrors = new List(); + List whiteListErrorFilesNames = new List(); + Hashtable blackList = redistList.GenerateBlackList(new AssemblyTableInfo[] { subsetListInfo }, whiteListErrors, whiteListErrorFilesNames); + + // Since there were no white list expect the black list to return null + Assert.IsTrue(blackList.Count == 0, "Expected to have no assemblies in the black list"); + Assert.IsTrue(whiteListErrors.Count == 0, "Expected there to be no error in the whiteListErrors"); + Assert.IsTrue(whiteListErrorFilesNames.Count == 0, "Expected there to be no error in the whiteListErrorFileNames"); + } + finally + { + File.Delete(redistFile); + File.Delete(goodSubsetFile); + } + } + + /// + /// Test the case where the framework directory is passed in as null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SubsetListFinderNullFrameworkDirectory() + { + SubsetListFinder finder = new SubsetListFinder(new string[0]); + finder.GetSubsetListPathsFromDisk(null); + } + + /// + /// Test the case where the subsetsToSearchFor are passed in as null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void SubsetListFinderNullSubsetToSearchFor() + { + SubsetListFinder finder = new SubsetListFinder(null); + } + + /// + /// Test the case where the subsetsToSearchFor are an empty array + /// + [TestMethod] + public void SubsetListFinderEmptySubsetToSearchFor() + { + SubsetListFinder finder = new SubsetListFinder(new string[0]); + string[] returnArray = finder.GetSubsetListPathsFromDisk("FrameworkDirectory"); + Assert.IsTrue(returnArray.Length == 0, "Expected the array returned to be 0 lengh"); + } + + + /// + /// Verify that the method will not crash if there are empty string array elements, and that when we call the + /// method twice with the same set of SubsetToSearchFor and TargetFrameworkDirectory that we get the exact same array back. + /// + [TestMethod] + public void SubsetListFinderVerifyEmptyInSubsetsToSearchForAndCaching() + { + // Verify the program will not crach when an empty string is passed in and that when we call the method twice that we get the + // exact same array of strings back. + SubsetListFinder finder = new SubsetListFinder(new string[] { "Clent", string.Empty, "Bar" }); + string[] returnArray = finder.GetSubsetListPathsFromDisk("FrameworkDirectory"); + string[] returnArray2 = finder.GetSubsetListPathsFromDisk("FrameworkDirectory"); + + Assert.IsTrue(Object.ReferenceEquals(returnArray, returnArray2), "Expected the string arrays to be the exact same reference"); + // Verify that if i call the method again with a different target framework directory that I get a different array back + string[] returnArray3 = finder.GetSubsetListPathsFromDisk("FrameworkDirectory2"); + Assert.IsTrue(!Object.ReferenceEquals(returnArray2, returnArray3), "Expected the string arrays to not be the exact same reference"); + } + + /// + /// Verify when we have valid subset files and their names are in the subsets to search for that we correctly find the files + /// + [TestMethod] + public void SubsetListFinderSubsetExists() + { + string frameworkDirectory = Path.Combine(ObjectModelHelpers.TempProjectDir, "SubsetListsTestExists"); + string subsetDirectory = Path.Combine(frameworkDirectory, SubsetListFinder.SubsetListFolder); + string clientXml = Path.Combine(subsetDirectory, "Client.xml"); + string fooXml = Path.Combine(subsetDirectory, "Foo.xml"); + + try + { + Directory.CreateDirectory(subsetDirectory); + File.WriteAllText(clientXml, "Random File Contents"); + File.WriteAllText(fooXml, "Random File Contents"); + SubsetListFinder finder = new SubsetListFinder(new string[] { "Client", "Foo" }); + string[] returnArray = finder.GetSubsetListPathsFromDisk(frameworkDirectory); + Assert.IsTrue(returnArray[0].Contains("Client.xml"), "Expected first element to contain Client.xml"); + Assert.IsTrue(returnArray[1].Contains("Foo.xml"), "Expected first element to contain Foo.xml"); + Assert.IsTrue(returnArray.Length == 2, "Expected there to be two elements in the array"); + } + finally + { + Directory.Delete(frameworkDirectory, true); + } + } + + /// + /// Verify that if there are files of the correct name but of the wrong extension that they are not found. + /// + [TestMethod] + public void SubsetListFinderNullSubsetExistsButNotXml() + { + string frameworkDirectory = Path.Combine(ObjectModelHelpers.TempProjectDir, "SubsetListsTestExistsNotXml"); + string subsetDirectory = Path.Combine(frameworkDirectory, SubsetListFinder.SubsetListFolder); + string clientXml = Path.Combine(subsetDirectory, "Clent.Notxml"); + string fooXml = Path.Combine(subsetDirectory, "Foo.Notxml"); + + try + { + Directory.CreateDirectory(subsetDirectory); + File.WriteAllText(clientXml, "Random File Contents"); + File.WriteAllText(fooXml, "Random File Contents"); + SubsetListFinder finder = new SubsetListFinder(new string[] { "Client", "Foo" }); + string[] returnArray = finder.GetSubsetListPathsFromDisk(frameworkDirectory); + Assert.IsTrue(returnArray.Length == 0, "Expected there to be two elements in the array"); + } + finally + { + Directory.Delete(frameworkDirectory, true); + } + } + + [TestMethod] + public void IgnoreDefaultInstalledAssemblyTables() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Microsoft.Build.Engine"), + new TaskItem("System.Xml") + }; + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}" + }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + + string implicitRedistListContents = + "" + + "" + + ""; + string implicitRedistListPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("v3.5\\RedistList\\ImplicitList.xml", implicitRedistListContents); + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine"); + + string explicitRedistListContents = + "" + + "" + + ""; + string explicitRedistListPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("v3.5\\RedistList\\ExplicitList.xml", explicitRedistListContents); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(explicitRedistListPath) }; + + // Only the explicitly specified redist list should be used + t.IgnoreDefaultInstalledAssemblyTables = true; + + FileExists cachedFileExists = fileExists; + GetAssemblyName cachedGetAssemblyName = getAssemblyName; + + fileExists = new FileExists(delegate (string path) + { + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase) || + String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase) || + path.EndsWith("RarCache", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + }); + + getAssemblyName = new GetAssemblyName(delegate (string path) + { + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + else if (String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + return null; + }); + + bool success; + try + { + success = Execute(t); + } + finally + { + fileExists = cachedFileExists; + getAssemblyName = cachedGetAssemblyName; + } + + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("System.Xml"), "Expected System.Xml to resolve."); + } + + /// + /// A null black list should be the same as an empty one. + /// + [TestMethod] + public void ReferenceTableNullBlackList() + { + TaskLoggingHelper log = new TaskLoggingHelper(new ResolveAssemblyReference()); + ReferenceTable referenceTable = MakeEmptyReferenceTable(log); + Dictionary table = referenceTable.References; + + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + + table.Add(engineAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + table.Add(xmlAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + + referenceTable.MarkReferencesForExclusion(null); + referenceTable.RemoveReferencesMarkedForExclusion(false, String.Empty); + Dictionary table2 = referenceTable.References; + Assert.IsTrue(!Object.ReferenceEquals(table, table2), "Expected hashtable to be a different instance"); + Assert.IsTrue(table2.Count == 2, "Expected there to be two elements in the hashtable"); + Assert.IsTrue(table2.ContainsKey(engineAssemblyName), "Expected to find the engineAssemblyName in the referenceList"); + Assert.IsTrue(table2.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + } + + /// + /// Test the case where the blacklist is empty. + /// + [TestMethod] + public void ReferenceTableEmptyBlackList() + { + TaskLoggingHelper log = new TaskLoggingHelper(new ResolveAssemblyReference()); + ReferenceTable referenceTable = MakeEmptyReferenceTable(log); + Dictionary table = referenceTable.References; + + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + + table.Add(engineAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + table.Add(xmlAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + + referenceTable.MarkReferencesForExclusion(new Hashtable()); + referenceTable.RemoveReferencesMarkedForExclusion(false, String.Empty); + Dictionary table2 = referenceTable.References; + Assert.IsTrue(!Object.ReferenceEquals(table, table2), "Expected hashtable to be a different instance"); + Assert.IsTrue(table2.Count == 2, "Expected there to be two elements in the hashtable"); + Assert.IsTrue(table2.ContainsKey(engineAssemblyName), "Expected to find the engineAssemblyName in the referenceList"); + Assert.IsTrue(table2.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + } + + /// + /// Verify the case where there are primary references in the reference table which are also in the black list + /// + [TestMethod] + public void ReferenceTablePrimaryItemInBlackList() + { + MockEngine mockEngine = new MockEngine(); + ResolveAssemblyReference rar = new ResolveAssemblyReference(); + rar.BuildEngine = mockEngine; + + ReferenceTable referenceTable = MakeEmptyReferenceTable(rar.Log); + Dictionary table = referenceTable.References; + + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + + Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + TaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + table.Add(engineAssemblyName, reference); + table.Add(xmlAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + + Hashtable blackList = new Hashtable(StringComparer.OrdinalIgnoreCase); + blackList[engineAssemblyName.FullName] = null; + string[] targetFrameworks = new string[] { "Client", "Web" }; + string subSetName = ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, null); + + referenceTable.MarkReferencesForExclusion(blackList); + referenceTable.RemoveReferencesMarkedForExclusion(false, subSetName); + + Dictionary table2 = referenceTable.References; + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailedToResolveReferenceBecausePrimaryAssemblyInExclusionList", taskItem.ItemSpec, subSetName); + Assert.IsTrue(!Object.ReferenceEquals(table, table2), "Expected hashtable to be a different instance"); + Assert.IsTrue(table2.Count == 1, "Expected there to be one elements in the hashtable"); + Assert.IsTrue(!table2.ContainsKey(engineAssemblyName), "Expected to not find the engineAssemblyName in the referenceList"); + Assert.IsTrue(table2.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + mockEngine.AssertLogContains(warningMessage); + } + + /// + /// Verify the case where there are primary references in the reference table which are also in the black list + /// + [TestMethod] + public void ReferenceTablePrimaryItemInBlackListSpecificVersionTrue() + { + MockEngine mockEngine = new MockEngine(); + ResolveAssemblyReference rar = new ResolveAssemblyReference(); + rar.BuildEngine = mockEngine; + + ReferenceTable referenceTable = MakeEmptyReferenceTable(rar.Log); + Dictionary table = referenceTable.References; + + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + + Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + TaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + taskItem.SetMetadata("SpecificVersion", "true"); + reference.MakePrimaryAssemblyReference(taskItem, true, ".dll"); + table.Add(engineAssemblyName, reference); + table.Add(xmlAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + + Hashtable blackList = new Hashtable(StringComparer.OrdinalIgnoreCase); + blackList[engineAssemblyName.FullName] = null; + string[] targetFrameworks = new string[] { "Client", "Web" }; + string subSetName = ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, null); + referenceTable.MarkReferencesForExclusion(blackList); + referenceTable.RemoveReferencesMarkedForExclusion(false, subSetName); + + Dictionary table2 = referenceTable.References; + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailedToResolveReferenceBecausePrimaryAssemblyInExclusionList", taskItem.ItemSpec, subSetName); + Assert.IsTrue(!Object.ReferenceEquals(table, table2), "Expected hashtable to be a different instance"); + Assert.IsTrue(table2.Count == 2, "Expected there to be two elements in the hashtable"); + Assert.IsTrue(table2.ContainsKey(engineAssemblyName), "Expected to find the engineAssemblyName in the referenceList"); + Assert.IsTrue(table2.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + mockEngine.AssertLogDoesntContain(warningMessage); + } + + /// + /// Verify the generation of the targetFrameworkSubSetName + /// + [TestMethod] + public void TestGenerateFrameworkName() + { + string[] targetFrameworks = new string[] { "Client" }; + Assert.IsTrue(string.Equals("Client", ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, null), StringComparison.OrdinalIgnoreCase)); + + targetFrameworks = new string[] { "Client", "Framework" }; + Assert.IsTrue(string.Equals("Client, Framework", ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, null), StringComparison.OrdinalIgnoreCase)); + + targetFrameworks = new string[0]; + Assert.IsTrue(String.IsNullOrEmpty(ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, null))); + + targetFrameworks = null; + Assert.IsTrue(String.IsNullOrEmpty(ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, null))); + + ITaskItem[] installedSubSetTable = new ITaskItem[] { new TaskItem("c:\\foo\\Client.xml") }; + Assert.IsTrue(string.Equals("Client", ResolveAssemblyReference.GenerateSubSetName(null, installedSubSetTable), StringComparison.OrdinalIgnoreCase)); + + installedSubSetTable = new ITaskItem[] { new TaskItem("c:\\foo\\Client.xml"), new TaskItem("D:\\foo\\bar\\Framework.xml") }; + Assert.IsTrue(string.Equals("Client, Framework", ResolveAssemblyReference.GenerateSubSetName(null, installedSubSetTable), StringComparison.OrdinalIgnoreCase)); + + installedSubSetTable = new ITaskItem[] { new TaskItem("c:\\foo\\Client.xml"), new TaskItem("D:\\foo\\bar\\Framework2\\"), new TaskItem("D:\\foo\\bar\\Framework"), new TaskItem("Nothing") }; + Assert.IsTrue(string.Equals("Client, Framework, Nothing", ResolveAssemblyReference.GenerateSubSetName(null, installedSubSetTable), StringComparison.OrdinalIgnoreCase)); + + installedSubSetTable = new ITaskItem[0]; + Assert.IsTrue(String.IsNullOrEmpty(ResolveAssemblyReference.GenerateSubSetName(null, installedSubSetTable))); + + installedSubSetTable = null; + Assert.IsTrue(String.IsNullOrEmpty(ResolveAssemblyReference.GenerateSubSetName(null, installedSubSetTable))); + + + targetFrameworks = new string[] { "Client", "Framework" }; + installedSubSetTable = new ITaskItem[] { new TaskItem("c:\\foo\\Mouse.xml"), new TaskItem("D:\\foo\\bar\\Man.xml") }; + Assert.IsTrue(string.Equals("Client, Framework, Mouse, Man", ResolveAssemblyReference.GenerateSubSetName(targetFrameworks, installedSubSetTable), StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify the case where we just want to remove the references before conflict resolution and not print out the warning. + /// + [TestMethod] + public void ReferenceTablePrimaryItemInBlackListRemoveOnlyNoWarn() + { + MockEngine mockEngine = new MockEngine(); + ResolveAssemblyReference rar = new ResolveAssemblyReference(); + rar.BuildEngine = mockEngine; + + ReferenceTable referenceTable = MakeEmptyReferenceTable(rar.Log); + Dictionary table = referenceTable.References; + + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + + Reference reference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + TaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + reference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + table.Add(engineAssemblyName, reference); + table.Add(xmlAssemblyName, new Reference(isWinMDFile, fileExists, getRuntimeVersion)); + + Hashtable blackList = new Hashtable(StringComparer.OrdinalIgnoreCase); + blackList[engineAssemblyName.FullName] = null; + referenceTable.MarkReferencesForExclusion(blackList); + referenceTable.RemoveReferencesMarkedForExclusion(true, String.Empty); + + Dictionary table2 = referenceTable.References; + string subSetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailedToResolveReferenceBecausePrimaryAssemblyInExclusionList", taskItem.ItemSpec, subSetName); + Assert.IsTrue(!Object.ReferenceEquals(table, table2), "Expected hashtable to be a different instance"); + Assert.IsTrue(table2.Count == 1, "Expected there to be one elements in the hashtable"); + Assert.IsTrue(!table2.ContainsKey(engineAssemblyName), "Expected to not find the engineAssemblyName in the referenceList"); + Assert.IsTrue(table2.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + Assert.IsTrue(String.IsNullOrEmpty(mockEngine.Log)); + } + + /// + /// Testing case enginePrimary -> dataDependencyReference->sqlDependencyReference : sqlDependencyReference is in black list + /// expect to see one dependency warning message + /// + [TestMethod] + public void ReferenceTableDependentItemsInBlackList() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension sqlclientAssemblyName = new AssemblyNameExtension("System.SqlClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference; + Reference dataDependencyReference; + Reference sqlDependencyReference; + Reference xmlPrimaryReference; + + GenerateNewReferences(out enginePrimaryReference, out dataDependencyReference, out sqlDependencyReference, out xmlPrimaryReference); + + TaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + dataDependencyReference.FullPath = "FullPath"; + sqlDependencyReference.MakeDependentAssemblyReference(dataDependencyReference); + sqlDependencyReference.AddError(new Exception("CouldNotResolveSQLDependency")); + xmlPrimaryReference.FullPath = "FullPath"; + xmlPrimaryReference.MakeDependentAssemblyReference(enginePrimaryReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, sqlDependencyReference, xmlPrimaryReference); + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { sqlclientAssemblyName }, out blackList); + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + VerifyReferenceTable(referenceTable, mockEngine, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, new string[] { warningMessage }); + } + + + /// + /// Testing case enginePrimary -> dataDependencyReference->sqlDependencyReference + /// and enginePrimary->sqlDependencyReference: sqlDependencyReference is in black list + /// and systemxml->enginePrimary + /// expect to see one dependency warning message + /// + [TestMethod] + public void ReferenceTableDependentItemsInBlackList2() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension sqlclientAssemblyName = new AssemblyNameExtension("System.SqlClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference; + Reference dataDependencyReference; + Reference sqlDependencyReference; + Reference xmlPrimaryReference; + + GenerateNewReferences(out enginePrimaryReference, out dataDependencyReference, out sqlDependencyReference, out xmlPrimaryReference); + + ITaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + dataDependencyReference.FullPath = "FullPath"; + sqlDependencyReference.FullPath = "FullPath"; + xmlPrimaryReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(dataDependencyReference); + xmlPrimaryReference.MakeDependentAssemblyReference(enginePrimaryReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, sqlDependencyReference, xmlPrimaryReference); + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { sqlclientAssemblyName }, out blackList); + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + VerifyReferenceTable(referenceTable, mockEngine, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, new string[] { warningMessage }); + } + + /// + /// Testing case enginePrimary->XmlPrimary with XMLPrimary in the BL + /// + [TestMethod] + public void ReferenceTablePrimaryToPrimaryDependencyWithOneInBlackList() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + Reference xmlPrimaryReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + + TaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + + ITaskItem taskItem2 = new TaskItem("System.Xml"); + xmlPrimaryReference.FullPath = "FullPath"; + xmlPrimaryReference.MakePrimaryAssemblyReference(taskItem2, false, ".dll"); + // Make engine depend on xml primary when xml primary is a primary reference as well + xmlPrimaryReference.AddSourceItems(enginePrimaryReference.GetSourceItems()); + xmlPrimaryReference.AddDependee(enginePrimaryReference); + + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, null, null, xmlAssemblyName, enginePrimaryReference, null, null, xmlPrimaryReference); + + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { xmlAssemblyName }, out blackList); + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, xmlAssemblyName.FullName, subsetName); + string warningMessage2 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailedToResolveReferenceBecausePrimaryAssemblyInExclusionList", taskItem2.ItemSpec, subsetName); + mockEngine.AssertLogContains(warningMessage); + mockEngine.AssertLogContains(warningMessage2); + + Dictionary table = referenceTable.References; + Assert.IsTrue(!table.ContainsKey(xmlAssemblyName), "Expected to not find the xmlAssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(engineAssemblyName), "Expected to not find the engineAssemblyName in the referenceList"); + } + + /// + /// Testing case enginePrimary->XmlPrimary->dataDependency with dataDependency in the BL + /// + [TestMethod] + public void ReferenceTablePrimaryToPrimaryToDependencyWithOneInBlackList() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + Reference xmlPrimaryReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + Reference dataDependencyReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + + TaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + + ITaskItem taskItem2 = new TaskItem("System.Xml"); + xmlPrimaryReference.FullPath = "FullPath"; + xmlPrimaryReference.MakePrimaryAssemblyReference(taskItem2, false, ".dll"); + // Make engine depend on xml primary when xml primary is a primary reference as well + xmlPrimaryReference.AddSourceItems(enginePrimaryReference.GetSourceItems()); + xmlPrimaryReference.AddDependee(enginePrimaryReference); + + + dataDependencyReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(xmlPrimaryReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, null, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, null, xmlPrimaryReference); + + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { dataAssemblyName }, out blackList); + + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, dataAssemblyName.FullName, subsetName); + string warningMessage2 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem2.ItemSpec, dataAssemblyName.FullName, subsetName); + mockEngine.AssertLogContains(warningMessage); + mockEngine.AssertLogContains(warningMessage2); + + Dictionary table = referenceTable.References; + Assert.IsTrue(!table.ContainsKey(xmlAssemblyName), "Expected to not find the xmlAssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(engineAssemblyName), "Expected to not find the engineAssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(dataAssemblyName), "Expected to not find the dataAssemblyName in the referenceList"); + } + + /// + /// Testing case enginePrimary -> dataDependencyReference->sqlDependencyReference + /// and xmlPrimary->sqlDependencyReference: sqlDependencyReference is in black list + /// expect to see one dependency warning message + /// + [TestMethod] + public void ReferenceTableDependentItemsInBlackList3() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension sqlclientAssemblyName = new AssemblyNameExtension("System.SqlClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference; + Reference dataDependencyReference; + Reference sqlDependencyReference; + Reference xmlPrimaryReference; + + GenerateNewReferences(out enginePrimaryReference, out dataDependencyReference, out sqlDependencyReference, out xmlPrimaryReference); + + ITaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + ITaskItem taskItem2 = new TaskItem("System.Xml"); + xmlPrimaryReference.MakePrimaryAssemblyReference(taskItem2, false, ".dll"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + dataDependencyReference.FullPath = "FullPath"; + xmlPrimaryReference.FullPath = "FullPath"; + sqlDependencyReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(xmlPrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(dataDependencyReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, sqlDependencyReference, xmlPrimaryReference); + + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { sqlclientAssemblyName }, out blackList); + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + string warningMessage2 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem2.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + VerifyReferenceTable(referenceTable, mockEngine, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, new string[] { warningMessage, warningMessage2 }); + } + + + /// + /// Testing case enginePrimary -> dataDependencyReference->sqlDependencyReference + /// and xmlPrimary->dataDependencyReference: sqlDependencyReference is in black list + /// expect to see one dependency warning message + /// + [TestMethod] + public void ReferenceTableDependentItemsInBlackList4() + { + ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, new string[0], null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, null, null, null, null, null, null, new Version("4.0"), null, null, null, true, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false); + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension sqlclientAssemblyName = new AssemblyNameExtension("System.SqlClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference; + Reference dataDependencyReference; + Reference sqlDependencyReference; + Reference xmlPrimaryReference; + + GenerateNewReferences(out enginePrimaryReference, out dataDependencyReference, out sqlDependencyReference, out xmlPrimaryReference); + + ITaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + ITaskItem taskItem2 = new TaskItem("System.Xml"); + xmlPrimaryReference.MakePrimaryAssemblyReference(taskItem2, false, ".dll"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + xmlPrimaryReference.FullPath = "FullPath"; + dataDependencyReference.FullPath = "FullPath"; + sqlDependencyReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + dataDependencyReference.MakeDependentAssemblyReference(xmlPrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(dataDependencyReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, sqlDependencyReference, xmlPrimaryReference); + + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { sqlclientAssemblyName }, out blackList); + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + string warningMessage2 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem2.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + VerifyReferenceTable(referenceTable, mockEngine, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, new string[] { warningMessage, warningMessage2 }); + } + + /// + /// Testing case enginePrimary -> dataDependencyReference->sqlDependencyReference + /// enginePrimary -> dataDependencyReference + /// xmlPrimaryReference ->DataDependency + /// dataDependencyReference and sqlDependencyReference are in black list + /// expect to see two dependency warning messages in the enginePrimaryCase and one in the xmlPrimarycase + /// + [TestMethod] + public void ReferenceTableDependentItemsInBlackList5() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension sqlclientAssemblyName = new AssemblyNameExtension("System.SqlClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference; + Reference dataDependencyReference; + Reference sqlDependencyReference; + Reference xmlPrimaryReference; + + GenerateNewReferences(out enginePrimaryReference, out dataDependencyReference, out sqlDependencyReference, out xmlPrimaryReference); + + ITaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + ITaskItem taskItem2 = new TaskItem("System.Xml"); + xmlPrimaryReference.MakePrimaryAssemblyReference(taskItem2, false, ".dll"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, false, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + xmlPrimaryReference.FullPath = "FullPath"; + dataDependencyReference.FullPath = "FullPath"; + sqlDependencyReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + dataDependencyReference.MakeDependentAssemblyReference(xmlPrimaryReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, sqlDependencyReference, xmlPrimaryReference); + + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { sqlclientAssemblyName, dataAssemblyName }, out blackList); + + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + string warningMessage2 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, dataAssemblyName.FullName, subsetName); + string warningMessage3 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem2.ItemSpec, dataAssemblyName.FullName, subsetName); + + Dictionary table = referenceTable.References; + Assert.IsTrue(table.Count == 0, "Expected there to be two elements in the hashtable"); + Assert.IsTrue(!table.ContainsKey(sqlclientAssemblyName), "Expected to not find the sqlclientAssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(dataAssemblyName), "Expected to not to find the dataAssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(engineAssemblyName), "Expected to find the engineAssemblyName in the referenceList"); + + string[] warningMessages = new string[] { warningMessage, warningMessage2, warningMessage3 }; + foreach (string message in warningMessages) + { + Console.Out.WriteLine("WarningMessageToAssert:" + message); + mockEngine.AssertLogContains(message); + } + table.Clear(); + } + + + /// + /// Testing case + /// enginePrimary -> dataDependencyReference also enginePrimary->sqlDependencyReference specific version = true on the primary + /// xmlPrimaryReference ->dataDependencyReference specific version = false on the primary + /// dataDependencyReference and sqlDependencyReference is in the black list. + /// Expect to see one dependency warning messages xmlPrimarycase and no message for enginePrimary + /// Also expect to resolve all files except for xmlPrimaryReference + /// + [TestMethod] + public void ReferenceTableDependentItemsInBlackListPrimaryWithSpecificVersion() + { + ReferenceTable referenceTable; + MockEngine mockEngine; + ResolveAssemblyReference rar; + Hashtable blackList; + AssemblyNameExtension engineAssemblyName = new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension dataAssemblyName = new AssemblyNameExtension("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension sqlclientAssemblyName = new AssemblyNameExtension("System.SqlClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + AssemblyNameExtension xmlAssemblyName = new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Reference enginePrimaryReference; + Reference dataDependencyReference; + Reference sqlDependencyReference; + Reference xmlPrimaryReference; + + GenerateNewReferences(out enginePrimaryReference, out dataDependencyReference, out sqlDependencyReference, out xmlPrimaryReference); + + ITaskItem taskItem = new TaskItem("Microsoft.Build.Engine"); + taskItem.SetMetadata("SpecificVersion", "true"); + + ITaskItem taskItem2 = new TaskItem("System.Xml"); + taskItem2.SetMetadata("SpecificVersion", "false"); + + xmlPrimaryReference.MakePrimaryAssemblyReference(taskItem2, false, ".dll"); + enginePrimaryReference.MakePrimaryAssemblyReference(taskItem, true, ".dll"); + enginePrimaryReference.FullPath = "FullPath"; + xmlPrimaryReference.FullPath = "FullPath"; + dataDependencyReference.FullPath = "FullPath"; + sqlDependencyReference.FullPath = "FullPath"; + dataDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + sqlDependencyReference.MakeDependentAssemblyReference(enginePrimaryReference); + dataDependencyReference.MakeDependentAssemblyReference(xmlPrimaryReference); + + InitializeMockEngine(out referenceTable, out mockEngine, out rar); + AddReferencesToReferenceTable(referenceTable, engineAssemblyName, dataAssemblyName, sqlclientAssemblyName, xmlAssemblyName, enginePrimaryReference, dataDependencyReference, sqlDependencyReference, xmlPrimaryReference); + + InitializeExclusionList(referenceTable, new AssemblyNameExtension[] { sqlclientAssemblyName, dataAssemblyName }, out blackList); + + string subsetName = ResolveAssemblyReference.GenerateSubSetName(new string[] { "Client" }, null); + string warningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem2.ItemSpec, dataAssemblyName.FullName, subsetName); + string notExpectedwarningMessage = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, dataAssemblyName.FullName, subsetName); + string notExpectedwarningMessage2 = rar.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", taskItem.ItemSpec, sqlclientAssemblyName.FullName, subsetName); + + Dictionary table = referenceTable.References; + Assert.IsTrue(table.Count == 3, "Expected there to be three elements in the hashtable"); + Assert.IsTrue(table.ContainsKey(sqlclientAssemblyName), "Expected to find the sqlclientAssemblyName in the referenceList"); + Assert.IsTrue(table.ContainsKey(dataAssemblyName), "Expected to find the dataAssemblyName in the referenceList"); + Assert.IsTrue(!table.ContainsKey(xmlAssemblyName), "Expected not to find the xmlssemblyName in the referenceList"); + Assert.IsTrue(table.ContainsKey(engineAssemblyName), "Expected to find the engineAssemblyName in the referenceList"); + + string[] warningMessages = new string[] { warningMessage }; + foreach (string message in warningMessages) + { + Console.Out.WriteLine("WarningMessageToAssert:" + message); + mockEngine.AssertLogContains(message); + } + + mockEngine.AssertLogDoesntContain(notExpectedwarningMessage); + mockEngine.AssertLogDoesntContain(notExpectedwarningMessage2); + table.Clear(); + } + + private static ReferenceTable MakeEmptyReferenceTable(TaskLoggingHelper log) + { + ReferenceTable referenceTable = new ReferenceTable(null, false, false, false, false, new string[0], null, null, null, null, null, null, SystemProcessorArchitecture.None, fileExists, null, null, null, null, null, null, null, null, null, new Version("4.0"), null, log, null, true, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false); + return referenceTable; + } + + /// + /// Verify the correct references are still in the references table and that references which are in the black list are not in the references table + /// Also verify any expected warning messages are seen in the log. + /// + private static void VerifyReferenceTable(ReferenceTable referenceTable, MockEngine mockEngine, AssemblyNameExtension engineAssemblyName, AssemblyNameExtension dataAssemblyName, AssemblyNameExtension sqlclientAssemblyName, AssemblyNameExtension xmlAssemblyName, string[] warningMessages) + { + Dictionary table = referenceTable.References; + Assert.IsTrue(table.Count == 0, "Expected there to be zero elements in the hashtable"); + + if (warningMessages != null) + { + foreach (string warningMessage in warningMessages) + { + Console.Out.WriteLine("WarningMessageToAssert:" + warningMessages); + mockEngine.AssertLogContains(warningMessage); + } + } + + table.Clear(); + } + + /// + /// Make sure we get an argument null exception when the profileName is set to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestProfileNameNull() + { + ResolveAssemblyReference rar = new ResolveAssemblyReference(); + rar.ProfileName = null; + } + + /// + /// Make sure we get an argument null exception when the ProfileFullFrameworkFolders is set to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestProfileFullFrameworkFoldersFoldersNull() + { + ResolveAssemblyReference rar = new ResolveAssemblyReference(); + rar.FullFrameworkFolders = null; + } + + /// + /// Make sure we get an argument null exception when the ProfileFullFrameworkAssemblyTables is set to null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestProfileFullFrameworkAssemblyTablesNull() + { + ResolveAssemblyReference rar = new ResolveAssemblyReference(); + rar.FullFrameworkAssemblyTables = null; + } + + /// + /// Verify that setting a subset and a profile at the same time will cause an error to be logged and rar to return false + /// + [TestMethod] + public void TestProfileAndSubset1() + { + MockEngine mockEngine; + ResolveAssemblyReference rar; + InitializeRARwithMockEngine(out mockEngine, out rar); + + rar.TargetFrameworkSubsets = new string[] { "Client" }; + rar.ProfileName = "Client"; + rar.FullFrameworkFolders = new string[] { "Client" }; + Assert.IsFalse(rar.Execute()); + mockEngine.AssertLogContains(rar.Log.FormatResourceString("ResolveAssemblyReference.CannotSetProfileAndSubSet")); + } + + /// + /// Verify that setting a subset and a profile at the same time will cause an error to be logged and rar to return false + /// + [TestMethod] + public void TestProfileAndSubset2() + { + MockEngine mockEngine; + ResolveAssemblyReference rar; + InitializeRARwithMockEngine(out mockEngine, out rar); + + rar.InstalledAssemblySubsetTables = new ITaskItem[] { new TaskItem("Client.xml") }; + rar.ProfileName = "Client"; + rar.FullFrameworkFolders = new string[] { "Client" }; + Assert.IsFalse(rar.Execute()); + mockEngine.AssertLogContains(rar.Log.FormatResourceString("ResolveAssemblyReference.CannotSetProfileAndSubSet")); + } + + /// + /// Verify setting certain combinations of Profile parameters will case an error to be logged and rar to fail execution. + /// + /// Test the case where the profile name is not set and ProfileFullFrameworkFolders is set. + /// + [TestMethod] + public void TestProfileParameterCombinations() + { + MockEngine mockEngine; + ResolveAssemblyReference rar; + InitializeRARwithMockEngine(out mockEngine, out rar); + rar.ProfileName = "Client"; + Assert.IsFalse(rar.Execute()); + mockEngine.AssertLogContains(rar.Log.FormatResourceString("ResolveAssemblyReference.MustSetProfileNameAndFolderLocations")); + } + + /// + /// Verify when the frameworkdirectory metadata is not set on the ProfileFullFrameworkAssemblyTables that an + /// error is logged and rar fails. + /// + [TestMethod] + public void TestFrameworkDirectoryMetadata() + { + MockEngine mockEngine; + ResolveAssemblyReference rar; + InitializeRARwithMockEngine(out mockEngine, out rar); + TaskItem item = new TaskItem("Client.xml"); + rar.ProfileName = "Client"; + rar.FullFrameworkAssemblyTables = new ITaskItem[] { item }; + Assert.IsFalse(rar.Execute()); + mockEngine.AssertLogContains(rar.Log.FormatResourceString("ResolveAssemblyReference.FrameworkDirectoryOnProfiles", item.ItemSpec)); + } + + private static void InitializeRARwithMockEngine(out MockEngine mockEngine, out ResolveAssemblyReference rar) + { + mockEngine = new MockEngine(); + rar = new ResolveAssemblyReference(); + rar.BuildEngine = mockEngine; + } + + /// + /// Add a set of references and their names to the reference table. + /// + private static void AddReferencesToReferenceTable(ReferenceTable referenceTable, AssemblyNameExtension engineAssemblyName, AssemblyNameExtension dataAssemblyName, AssemblyNameExtension sqlclientAssemblyName, AssemblyNameExtension xmlAssemblyName, Reference enginePrimaryReference, Reference dataDependencyReference, Reference sqlDependencyReference, Reference xmlPrimaryReference) + { + Dictionary table = referenceTable.References; + if (enginePrimaryReference != null) + { + table.Add(engineAssemblyName, enginePrimaryReference); + } + + if (dataDependencyReference != null) + { + table.Add(dataAssemblyName, dataDependencyReference); + } + if (sqlDependencyReference != null) + { + table.Add(sqlclientAssemblyName, sqlDependencyReference); + } + + if (xmlPrimaryReference != null) + { + table.Add(xmlAssemblyName, xmlPrimaryReference); + } + } + + /// + /// Initialize the mock engine so we can look at the warning messages, also put the assembly name which is to be in the black list into the black list. + /// Call remove references so that we can then validate the results. + /// + private void InitializeMockEngine(out ReferenceTable referenceTable, out MockEngine mockEngine, out ResolveAssemblyReference rar) + { + mockEngine = new MockEngine(); + rar = new ResolveAssemblyReference(); + rar.BuildEngine = mockEngine; + + referenceTable = MakeEmptyReferenceTable(rar.Log); + } + + + /// + ///Initialize the black list and use it to remove references from the reference table + /// + private void InitializeExclusionList(ReferenceTable referenceTable, AssemblyNameExtension[] assembliesForBlackList, out Hashtable blackList) + { + blackList = new Hashtable(StringComparer.OrdinalIgnoreCase); + foreach (AssemblyNameExtension assemblyName in assembliesForBlackList) + { + blackList[assemblyName.FullName] = null; + } + + referenceTable.MarkReferencesForExclusion(blackList); + referenceTable.RemoveReferencesMarkedForExclusion(false, "Client"); + } + + /// + /// Before each test to validate the references are correctly removed from the reference table we need to make new instances of them + /// + /// + /// + /// + /// + private static void GenerateNewReferences(out Reference enginePrimaryReference, out Reference dataDependencyReference, out Reference sqlDependencyReference, out Reference xmlPrimaryReference) + { + enginePrimaryReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + dataDependencyReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + sqlDependencyReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + xmlPrimaryReference = new Reference(isWinMDFile, fileExists, getRuntimeVersion); + } + + /// + /// This test will verify the IgnoreDefaultInstalledSubsetTables property on the RAR task. + /// The property determines whether or not RAR will search the target framework directories under the subsetList folder for + /// xml files matching the client subset names passed into the TargetFrameworkSubset property. + /// + /// The default for the property is false, when the value is false RAR will search the SubsetList folder under the TargetFramework directories + /// for the xml files with names in the TargetFrameworkSubset property. When the value is true, RAR will not search the SubsetList directory. The only + /// way to specify a TargetFrameworkSubset is to pass one to the InstalledAssemblySubsetTables property. + /// + [TestMethod] + public void IgnoreDefaultInstalledSubsetTables() + { + string redistListPath = CreateGenericRedistList(); + string subsetListClientPath = string.Empty; + string explicitSubsetListPath = string.Empty; + + try + { + subsetListClientPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("v3.5\\SubsetList\\Client.xml", _engineOnlySubset); + explicitSubsetListPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("v3.5\\SubsetList\\ExplicitList.xml", _xmlOnlySubset); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + t.Assemblies = new ITaskItem[] { new TaskItem("Microsoft.Build.Engine"), new TaskItem("System.Xml") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}" }; + + // This is a TargetFrameworkSubset that would be searched by RAR if IgnoreDefaultINstalledAssemblySubsetTables does not work. + t.TargetFrameworkSubsets = new string[] { "Client" }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + t.InstalledAssemblySubsetTables = new ITaskItem[] { new TaskItem(explicitSubsetListPath) }; + t.IgnoreDefaultInstalledAssemblySubsetTables = true; + + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + + bool success = GenerateHelperDelegatesAndExecuteTask(t, microsoftBuildEnginePath, systemXmlPath); + + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("System.Xml"), "Expected System.Xml to resolve."); + } + finally + { + File.Delete(redistListPath); + } + } + + /// + /// Generate helper delegates for returning the file existence and the assembly name. + /// Also run the rest and return the result. + /// + private bool GenerateHelperDelegatesAndExecuteTask(ResolveAssemblyReference t, string microsoftBuildEnginePath, string systemXmlPath) + { + FileExists cachedFileExists = fileExists; + GetAssemblyName cachedGetAssemblyName = getAssemblyName; + fileExists = new FileExists(delegate (string path) + { + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase) || + String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase) || + path.EndsWith("RarCache", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + }); + + getAssemblyName = new GetAssemblyName(delegate (string path) + { + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + else if (String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + return null; + }); + + bool success; + try + { + success = Execute(t); + } + finally + { + fileExists = cachedFileExists; + getAssemblyName = cachedGetAssemblyName; + } + return success; + } + + /// + /// Test the case where there are no client subset names passed in but an InstalledDefaultSubsetTable + /// is passed in. We expect to use that. + /// + [TestMethod] + public void NoClientSubsetButInstalledSubTables() + { + string redistListPath = CreateGenericRedistList(); + try + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + // These are the assemblies we are going to try and resolve + t.Assemblies = new ITaskItem[] { new TaskItem("Microsoft.Build.Engine"), new TaskItem("System.Xml") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}" }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + // Only the explicitly specified redist list should be used + t.TargetFrameworkSubsets = new string[0]; + + // Create a subset list which should be read in + string explicitSubsetListContents = + "" + + "" + + ""; + + string explicitSubsetListPath = ObjectModelHelpers.CreateFileInTempProjectDirectory("v3.5\\SubsetList\\ExplicitList.xml", explicitSubsetListContents); + t.InstalledAssemblySubsetTables = new ITaskItem[] { new TaskItem(explicitSubsetListPath) }; + t.IgnoreDefaultInstalledAssemblySubsetTables = true; + + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + bool success = GenerateHelperDelegatesAndExecuteTask(t, microsoftBuildEnginePath, systemXmlPath); + + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("System.Xml"), "Expected System.Xml to resolve."); + MockEngine engine = ((MockEngine)t.BuildEngine); + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.UsingExclusionList")); + } + finally + { + File.Delete(redistListPath); + } + } + + /// + /// Verify the case where the installedSubsetTables are null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullInstalledSubsetTables() + { + ResolveAssemblyReference reference = new ResolveAssemblyReference(); + reference.InstalledAssemblySubsetTables = null; + } + + /// + /// Verify the case where the targetFrameworkSubsets are null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullTargetFrameworkSubsets() + { + ResolveAssemblyReference reference = new ResolveAssemblyReference(); + reference.TargetFrameworkSubsets = null; + } + + /// + /// Verify the case where the FulltargetFrameworkSubsetNames are null + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullFullTargetFrameworkSubsetNames() + { + ResolveAssemblyReference reference = new ResolveAssemblyReference(); + reference.FullTargetFrameworkSubsetNames = null; + } + + /// + /// Test the case where a non existent subset list path is used and no additional subsets are passed in. + /// + [TestMethod] + public void FakeSubsetListPathsNoAdditionalSubsets() + { + string redistListPath = CreateGenericRedistList(); + try + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + // These are the assemblies we are going to try and resolve + t.Assemblies = new ITaskItem[] { new TaskItem("Microsoft.Build.Engine"), new TaskItem("System.Xml") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}" }; + + t.TargetFrameworkSubsets = new string[] { "NOTTOEXIST" }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + + // Only the explicitly specified redist list should be used + t.IgnoreDefaultInstalledAssemblyTables = true; + + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + bool success = GenerateHelperDelegatesAndExecuteTask(t, microsoftBuildEnginePath, systemXmlPath); + Assert.IsTrue(success, "Expected no errors."); + MockEngine engine = ((MockEngine)t.BuildEngine); + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.UsingExclusionList")); + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.NoSubsetsFound")); + Assert.AreEqual(2, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[1].ItemSpec.Contains("System.Xml"), "Expected System.Xml to resolve."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("Microsoft.Build.Engine"), "Expected Microsoft.Build.Engine to resolve."); + } + finally + { + File.Delete(redistListPath); + } + } + + /// + /// This test will verify when the full client name is passed in and it appears in the TargetFrameworkSubsetList, that the + /// black list is not used. + /// + [TestMethod] + public void ResolveAssemblyReferenceVerifyFullClientName() + { + string redistListPath = CreateGenericRedistList(); + try + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + + // These are the assemblies we are going to try and resolve + t.Assemblies = new ITaskItem[] { new TaskItem("System.Xml") }; + + // This is a TargetFrameworkSubset that would be searched by RAR if IgnoreDefaultINstalledAssemblySubsetTables does not work. + t.TargetFrameworkSubsets = new string[] { "Client", "Full" }; + t.FullTargetFrameworkSubsetNames = new string[] { "Full" }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + + Execute(t); + MockEngine engine = (MockEngine)t.BuildEngine; + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.NoExclusionListBecauseofFullClientName", "Full")); + } + finally + { + File.Delete(redistListPath); + } + } + + + /// + /// This test will verify when the full client name is passed in and it appears in the TargetFrameworkSubsetList, that the + /// black list is not used. + /// + [TestMethod] + public void ResolveAssemblyReferenceVerifyFullClientNameWithSubsetTables() + { + string redistListPath = CreateGenericRedistList(); + try + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + // These are the assemblies we are going to try and resolve + t.Assemblies = new ITaskItem[] { new TaskItem("System.Xml") }; + + // This is a TargetFrameworkSubset that would be searched by RAR if IgnoreDefaultINstalledAssemblySubsetTables does not work. + t.TargetFrameworkSubsets = new string[] { "Client", "Full" }; + t.FullTargetFrameworkSubsetNames = new string[] { "Full" }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + t.IgnoreDefaultInstalledAssemblySubsetTables = true; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + t.InstalledAssemblySubsetTables = new ITaskItem[] { new TaskItem(@"C:\LocationOfSubset.xml") }; + t.IgnoreDefaultInstalledAssemblyTables = true; + + Execute(t); + + MockEngine engine = (MockEngine)t.BuildEngine; + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.NoExclusionListBecauseofFullClientName", "Full")); + } + finally + { + File.Delete(redistListPath); + } + } + + + /// + /// This test will verify when the full client name is passed in and it appears in the TargetFrameworkSubsetList, that the + /// black list is not used. + /// + [TestMethod] + public void ResolveAssemblyReferenceVerifyFullClientNameNoTablesPassedIn() + { + string redistListPath = CreateGenericRedistList(); + try + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + // These are the assemblies we are going to try and resolve + t.Assemblies = new ITaskItem[] { new TaskItem("System.Xml") }; + + // This is a TargetFrameworkSubset that would be searched by RAR if IgnoreDefaultINstalledAssemblySubsetTables does not work. + t.TargetFrameworkSubsets = new string[] { "Client", "Full" }; + t.FullTargetFrameworkSubsetNames = new string[] { "Full" }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + t.IgnoreDefaultInstalledAssemblySubsetTables = true; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + + Execute(t); + + MockEngine engine = (MockEngine)t.BuildEngine; + engine.AssertLogContains(t.Log.FormatResourceString("ResolveAssemblyReference.NoExclusionListBecauseofFullClientName", "Full")); + } + finally + { + File.Delete(redistListPath); + } + } + + /// + /// Verify the correct references are still in the references table and that references which are in the black list are not in the references table + /// Also verify any expected warning messages are seen in the log. + /// + private static void VerifyReferenceTable(ReferenceTable referenceTable, MockEngine mockEngine, AssemblyNameExtension engineAssemblyName, AssemblyNameExtension dataAssemblyName, AssemblyNameExtension sqlclientAssemblyName, AssemblyNameExtension xmlAssemblyName, string warningMessage, string warningMessage2) + { + IDictionary table = referenceTable.References; + Assert.IsTrue(table.Count == 3, "Expected there to be three elements in the hashtable"); + Assert.IsTrue(!table.ContainsKey(sqlclientAssemblyName), "Expected to not find the sqlclientAssemblyName in the referenceList"); + Assert.IsTrue(table.ContainsKey(xmlAssemblyName), "Expected to find the xmlssemblyName in the referenceList"); + Assert.IsTrue(table.ContainsKey(dataAssemblyName), "Expected to find the dataAssemblyName in the referenceList"); + Assert.IsTrue(table.ContainsKey(engineAssemblyName), "Expected to find the engineAssemblyName in the referenceList"); + if (warningMessage != null) + { + mockEngine.AssertLogContains(warningMessage); + } + if (warningMessage2 != null) + { + mockEngine.AssertLogContains(warningMessage2); + } + table.Clear(); + } + + /// + /// Generate helper delegates for returning the file existence and the assembly name. + /// Also run the rest and return the result. + /// + private bool GenerateHelperDelegatesAndExecuteTask(ResolveAssemblyReference t) + { + FileExists cachedFileExists = fileExists; + GetAssemblyName cachedGetAssemblyName = getAssemblyName; + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + fileExists = new FileExists(delegate (string path) +{ + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase) || + String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase) || + path.EndsWith("RarCache", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; +}); + + getAssemblyName = new GetAssemblyName(delegate (string path) + { + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + else if (String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + return null; + }); + + bool success; + try + { + success = Execute(t); + } + finally + { + fileExists = cachedFileExists; + getAssemblyName = cachedGetAssemblyName; + } + return success; + } + + [TestMethod] + public void DoNotAssumeFilesDescribedByRedistListExistOnDisk() + { + string redistListPath = CreateGenericRedistList(); + try + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Microsoft.Build.Engine"), + new TaskItem("System.Xml") + }; + + t.SearchPaths = new string[] + { + @"{TargetFrameworkDirectory}" + }; + t.TargetFrameworkDirectories = new string[] { Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5") }; + string microsoftBuildEnginePath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\Microsoft.Build.Engine"); + string systemXmlPath = Path.Combine(ObjectModelHelpers.TempProjectDir, "v3.5\\System.Xml.dll"); + + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(redistListPath) }; + + FileExists cachedFileExists = fileExists; + GetAssemblyName cachedGetAssemblyName = getAssemblyName; + + // Note that Microsoft.Build.Engine.dll does not exist + fileExists = new FileExists(delegate (string path) + { + if (String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase) || path.EndsWith("RarCache", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + }); + + getAssemblyName = new GetAssemblyName(delegate (string path) + { + if (String.Equals(path, microsoftBuildEnginePath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("Microsoft.Build.Engine, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + else if (String.Equals(path, systemXmlPath, StringComparison.OrdinalIgnoreCase)) + { + return new AssemblyNameExtension("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + } + + return null; + }); + + bool success; + try + { + success = Execute(t); + } + finally + { + fileExists = cachedFileExists; + getAssemblyName = cachedGetAssemblyName; + } + + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("System.Xml"), "Expected System.Xml to resolve."); + } + finally + { + File.Delete(redistListPath); + } + } + + /// + /// Here's how you get into this situation: + /// + /// App + /// References - A + /// + /// And, the following conditions. + /// $(ReferencePath) = c:\apath;: + /// + /// Expected result: + /// * Invalid paths should be ignored. + /// + /// + [TestMethod] + public void Regress397129_HandleInvalidDirectoriesAndFiles_Case1() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.SearchPaths = new string[] + { + @"c:\apath", + @":" + }; + + Execute(t); // Expect no exception. + } + + /// + /// Here's how you get into this situation: + /// + /// App + /// References - A + /// Hintpath=||invalidpath|| + /// + /// Expected result: + /// * No exceptions. + /// + /// + [TestMethod] + public void Regress397129_HandleInvalidDirectoriesAndFiles_Case2() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + t.Assemblies[0].SetMetadata("HintPath", @"||invalidpath||"); + + + t.SearchPaths = new string[] + { + @"{HintPathFromItem}" + }; + + Execute(t); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - Microsoft.Office.Interop.Excel + /// Depends on Office, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c + /// + /// References - MS.Internal.Test.Automation.Office.Excel + /// Depends on Office, Version=12.0.0.0, Culture=neutral, PublicKeyToken=94de0004b6e3fcc5 + /// + /// Notice that the two primaries have dependencies that only differ by PKT. Suggested redirects should + /// only happen if the two assemblies differ by nothing but version. + /// + [TestMethod] + public void Regress313747_FalseSuggestedRedirectsWhenAssembliesDifferOnlyByPkt() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Microsoft.Office.Interop.Excel"), + new TaskItem("MS.Internal.Test.Automation.Office.Excel") + }; + + t.SearchPaths = new string[] + { + @"c:\Regress313747", + }; + + Execute(t); + + Assert.AreEqual(0, t.SuggestedRedirects.Length); + } + + /// + /// Consider this dependency chain: + /// + /// (1) Primary reference A v 2.0.0.0 is found. + /// (2) Primary reference B is found. + /// (3) Primary reference B depends on A v 1.0.0.0 + /// (4) Dependency A v 1.0.0.0 is not found. + /// (5) App.Config does not contain a binding redirect from A v 1.0.0.0 -> 2.0.0.0 + /// + /// We need to warn and suggest an app.config entry because the runtime environment will require a binding + /// redirect to function. Without a binding redirect, loading B will cause A.V1 to try to load. It won't be + /// there and there won't be a binding redirect to point it at 2.0.0.0. + /// + [TestMethod] + public void Regress442570_MissingBackVersionShouldWarn() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") + }; + + t.SearchPaths = new string[] + { + @"c:\Regress442570", + }; + + Execute(t); + + // Expect a suggested redirect plus a warning + Assert.AreEqual(1, t.SuggestedRedirects.Length); + Assert.AreEqual(1, e.Warnings); + } + + + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on B + /// Will be found by hintpath. + /// References -B + /// No hintpath + /// Exists in A.dll's folder. + /// + /// B.dll should be unresolved even though its in A's folder because primary resolution needs to work + /// without looking at dependencies because of the load-time perf scenarios don't look at dependencies. + /// We must be consistent between primaries resolved with FindDependencies=true and FindDependencies=false. + [TestMethod] + public void ByDesignRelatedTo454863_PrimaryReferencesDontResolveToParentFolders() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") + }; + t.Assemblies[0].SetMetadata("HintPath", @"C:\Regress454863\A.dll"); + + t.SearchPaths = new string[] + { + "{HintPathFromItem}" + }; + + Execute(t); + + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"C:\Regress454863\A.dll"), "Expected A.dll to be resolved."); + Assert.IsTrue(!ContainsItem(t.ResolvedFiles, @"C:\Regress454863\B.dll"), "Expected B.dll to be *not* be resolved."); + } + + [TestMethod] + public void Regress393931_AllowAlternateAssemblyExtensions_Case1() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + + t.SearchPaths = new string[] + { + @"C:\Regress393931" + }; + t.AllowedAssemblyExtensions = new string[] + { + ".metaData_dll" + }; + + Execute(t); + + // Expect a suggested redirect plus a warning + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"C:\Regress393931\A.metadata_dll"), "Expected A.dll to be resolved."); + } + + /// + /// Allow alternate extension values to be passed in. + /// + [TestMethod] + public void Regress393931_AllowAlternateAssemblyExtensions() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A") + }; + + + t.SearchPaths = new string[] + { + @"C:\Regress393931" + }; + t.AllowedAssemblyExtensions = new string[] + { + ".metaData_dll" + }; + + Execute(t); + + // Expect a suggested redirect plus a warning + Assert.IsTrue(ContainsItem(t.ResolvedFiles, @"C:\Regress393931\A.metadata_dll"), "Expected A.dll to be resolved."); + } + + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 (but PKT=null) + /// References - B + /// Depends on D version 2 (but PKT=null) + /// + /// There should be no suggested redirect because only strongly named assemblies can have + /// binding redirects. + /// + [TestMethod] + public void Regress387218_UnificationRequiresStrongName() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") + }; + + t.SearchPaths = new string[] + { + @"c:\Regress387218", + @"c:\Regress387218\v1", + @"c:\Regress387218\v2" + }; + + Execute(t); + + Assert.AreEqual(2, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Regress387218\v2\D.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Regress387218\v1\D.dll"), "Expected to find assembly, but didn't."); + Assert.AreEqual(0, t.SuggestedRedirects.Length); + Assert.IsTrue(e.Warnings == 0, "Should only be no warning about suggested redirects."); + } + + /// + /// Consider this dependency chain: + /// + /// App + /// References - A + /// Depends on D version 1 (but Culture=fr) + /// References - B + /// Depends on D version 2 (but Culture=en) + /// + /// There should be no suggested redirect because assemblies with different cultures cannot unify. + /// + [TestMethod] + public void Regress390219_UnificationRequiresSameCulture() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B") + }; + + t.SearchPaths = new string[] + { + @"c:\Regress390219", + @"c:\Regress390219\v1", + @"c:\Regress390219\v2" + }; + + Execute(t); + + Assert.AreEqual(2, t.ResolvedDependencyFiles.Length); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Regress390219\v2\D.dll"), "Expected to find assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.ResolvedDependencyFiles, @"c:\Regress390219\v1\D.dll"), "Expected to find assembly, but didn't."); + Assert.AreEqual(0, t.SuggestedRedirects.Length); + Assert.IsTrue(e.Warnings == 0, "Should only be no warning about suggested redirects."); + } + + + [TestMethod] + public void SGenDependeicies() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.Assemblies = new TaskItem[] + { + new TaskItem("mycomponent"), + new TaskItem("mycomponent2") + }; + + t.AssemblyFiles = new TaskItem[] + { + new TaskItem(@"c:\SGenDependeicies\mycomponent.dll"), + new TaskItem(@"c:\SGenDependeicies\mycomponent2.dll") + }; + + t.SearchPaths = new string[] + { + @"c:\SGenDependeicies" + }; + + t.FindSerializationAssemblies = true; + + Execute(t); + + Assert.IsTrue(t.FindSerializationAssemblies, "Expected to find serialization assembly."); + Assert.IsTrue(ContainsItem(t.SerializationAssemblyFiles, @"c:\SGenDependeicies\mycomponent.XmlSerializers.dll"), "Expected to find serialization assembly, but didn't."); + Assert.IsTrue(ContainsItem(t.SerializationAssemblyFiles, @"c:\SGenDependeicies\mycomponent2.XmlSerializers.dll"), "Expected to find serialization assembly, but didn't."); + } + + + /// + /// Consider this dependency chain: + /// + /// App + /// Has project reference to c:\Regress315619\A\MyAssembly.dll + /// Has project reference to c:\Regress315619\B\MyAssembly.dll + /// + /// These two project references have different versions. Important: PKT is null. + /// + [TestMethod] + public void Regress315619_TwoWeaklyNamedPrimariesIsInsoluble() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine e = new MockEngine(); + t.BuildEngine = e; + + t.AssemblyFiles = new ITaskItem[] + { + new TaskItem(@"c:\Regress315619\A\MyAssembly.dll"), + new TaskItem(@"c:\Regress315619\B\MyAssembly.dll") + }; + + t.SearchPaths = new string[] + { + @"c:\Regress315619\A", + @"c:\Regress315619\B" + }; + + Execute(t); + + e.AssertLogContains + ( + String.Format(AssemblyResources.GetString("ResolveAssemblyReference.ConflictUnsolvable"), @"MyAssembly, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=null", "MyAssembly, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null") + ); + } + + /// + /// This is a fix to help ClickOnce folks correctly display information about which + /// redist components can be deployed. + /// + /// Two new attributes are added to resolved references: + /// (1) IsRedistRoot (bool) -- The flag from the redist *.xml file. If there is no + /// flag in the file then there will be no flag on the resulting item. This flag means + /// "I am the UI representative for this entire redist". ClickOnce will use this to hide + /// all other redist items and to show only this item. + /// + /// (2) Redist (string) -- This the the value of FileList Redist from the *.xml file. + /// This string means "I am the unique name of this entire redist". + /// + /// + [TestMethod] + public void ForwardRedistRoot() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("MyRedistRootAssembly"), + new TaskItem("MyOtherAssembly"), + new TaskItem("MyThirdAssembly") + }; + + t.SearchPaths = new string[] + { + @"c:\MyRedist" + }; + + string redistFile = FileUtilities.GetTemporaryFile(); + + try + { + File.Delete(redistFile); + File.WriteAllText +( + redistFile, + "" + + "" + + "" + + "" + + "" + ); + + t.InstalledAssemblyTables = new TaskItem[] { new TaskItem(redistFile) }; + + Execute(t); + } + finally + { + File.Delete(redistFile); + } + + Assert.IsTrue(t.ResolvedFiles.Length == 3, "Expected three assemblies to be found."); + Assert.AreEqual("true", t.ResolvedFiles[1].GetMetadata("IsRedistRoot")); + Assert.AreEqual("false", t.ResolvedFiles[0].GetMetadata("IsRedistRoot")); + Assert.AreEqual("", t.ResolvedFiles[2].GetMetadata("IsRedistRoot")); + + Assert.AreEqual("Microsoft-Windows-CLRCoreComp", t.ResolvedFiles[0].GetMetadata("Redist")); + Assert.AreEqual("Microsoft-Windows-CLRCoreComp", t.ResolvedFiles[1].GetMetadata("Redist")); + Assert.AreEqual("Microsoft-Windows-CLRCoreComp", t.ResolvedFiles[2].GetMetadata("Redist")); + } + + /// + /// helper for TargetFrameworkFiltering + /// + private int RunTargetFrameworkFilteringTest(string projectTargetFramework) + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + t.BuildEngine = new MockEngine(); + t.Assemblies = new ITaskItem[] + { + new TaskItem("A"), + new TaskItem("B"), + new TaskItem("C") + }; + + t.SearchPaths = new string[] + { + @"c:\MyLibraries" + }; + + t.Assemblies[1].SetMetadata("RequiredTargetFramework", "3.0"); + t.Assemblies[2].SetMetadata("RequiredTargetFramework", "3.5"); + t.TargetFrameworkVersion = projectTargetFramework; + + Execute(t); + + int set = 0; + foreach (ITaskItem item in t.ResolvedFiles) + { + int mask = 0; + if (item.ItemSpec.EndsWith(@"\A.dll")) + { + mask = 1; + } + else if (item.ItemSpec.EndsWith(@"\B.dll")) + { + mask = 2; + } + else if (item.ItemSpec.EndsWith(@"\C.dll")) + { + mask = 4; + } + Assert.IsTrue(mask != 0, "Unexpected assembly in resolved list."); + Assert.IsTrue((mask & set) == 0, "Assembly found twice in resolved list."); + set = set | mask; + } + return set; + } + + /// + /// Make sure the reverse assembly name comparer correctly sorts the assembly names in reverse order + /// + [TestMethod] + public void ReverseAssemblyNameExtensionComparer() + { + IComparer sortByVersionDescending = new RedistList.SortByVersionDescending(); + AssemblyEntry a1 = new AssemblyEntry("Microsoft.Build.Engine", "1.0.0.0", "b03f5f7f11d50a3a", "neutral", true, true, "Foo", "none", true); + AssemblyEntry a2 = new AssemblyEntry("Microsoft.Build.Engine", "2.0.0.0", "b03f5f7f11d50a3a", "neutral", true, true, "Foo", "none", false); + AssemblyEntry a3 = new AssemblyEntry("Microsoft.Build.Engine", "3.0.0.0", "b03f5f7f11d50a3a", "neutral", true, true, "Foo", "none", true); + AssemblyEntry a4 = new AssemblyEntry("A", "3.0.0.0", "b03f5f7f11d50a3a", "neutral", true, true, "Foo", "none", true); + AssemblyEntry a5 = new AssemblyEntry("B", "3.0.0.0", "b03f5f7f11d50a3a", "neutral", true, true, "Foo", "none", true); + + // Verify versions sort correctly when simple name is same + Assert.AreEqual(0, sortByVersionDescending.Compare(a1, a1)); + Assert.AreEqual(1, sortByVersionDescending.Compare(a1, a2)); + Assert.AreEqual(1, sortByVersionDescending.Compare(a1, a3)); + Assert.AreEqual(-1, sortByVersionDescending.Compare(a2, a1)); + Assert.AreEqual(1, sortByVersionDescending.Compare(a2, a3)); + + // Verify the names sort alphabetically + Assert.AreEqual(-1, sortByVersionDescending.Compare(a4, a5)); + } + + /// + /// Check the Filtering based on Target Framework. + /// + [TestMethod] + public void TargetFrameworkFiltering() + { + int resultSet = 0; + resultSet = RunTargetFrameworkFilteringTest("3.0"); + Assert.IsTrue(resultSet == 0x3, "Expected assemblies A & B to be found."); + + resultSet = RunTargetFrameworkFilteringTest("3.5"); + Assert.IsTrue(resultSet == 0x7, "Expected assemblies A, B & C to be found."); + + resultSet = RunTargetFrameworkFilteringTest(null); + Assert.IsTrue(resultSet == 0x7, "Expected assemblies A, B & C to be found."); + + resultSet = RunTargetFrameworkFilteringTest("2.0"); + Assert.IsTrue(resultSet == 0x1, "Expected only assembly A to be found."); + } + + /// + /// Verify the when a simple name is asked for that the assemblies are returned in sorted order by version. + /// + [TestMethod] + public void VerifyGetSimpleNamesIsSorted() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyEntry[] entryArray = redist.FindAssemblyNameFromSimpleName("System"); + Assert.IsTrue(entryArray.Length == 6); + AssemblyNameExtension a1 = new AssemblyNameExtension(entryArray[0].FullName); + AssemblyNameExtension a2 = new AssemblyNameExtension(entryArray[1].FullName); + AssemblyNameExtension a3 = new AssemblyNameExtension(entryArray[2].FullName); + AssemblyNameExtension a4 = new AssemblyNameExtension(entryArray[3].FullName); + AssemblyNameExtension a5 = new AssemblyNameExtension(entryArray[4].FullName); + AssemblyNameExtension a6 = new AssemblyNameExtension(entryArray[5].FullName); + + Assert.IsTrue(a1.Version.Equals(new Version("100.0.0.0")), "Expect to find version 100.0.0.0 but instead found:" + a1.Version); + Assert.IsTrue(a2.Version.Equals(new Version("10.0.0.0")), "Expect to find version 10.0.0.0 but instead found:" + a2.Version); + Assert.IsTrue(a3.Version.Equals(new Version("4.0.0.0")), "Expect to find version 4.0.0.0 but instead found:" + a3.Version); + Assert.IsTrue(a4.Version.Equals(new Version("3.0.0.0")), "Expect to find version 3.0.0.0 but instead found:" + a4.Version); + Assert.IsTrue(a5.Version.Equals(new Version("2.0.0.0")), "Expect to find version 2.0.0.0 but instead found:" + a5.Version); + Assert.IsTrue(a6.Version.Equals(new Version("1.0.0.0")), "Expect to find version 1.0.0.0 but instead found:" + a6.Version); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// If the assembly was found in a redis list which does not have the correct redist name , Microsoft-Windows-CLRCoreComp then we should not consider it a framework assembly. + /// + [TestMethod] + public void VerifyAssemblyInRedistListNonWindowsRedistName() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsFalse(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// If the assembly was found in a redis list which does have the correct redist name , Microsoft-Windows-CLRCoreComp then we should consider it a framework assembly. + /// + [TestMethod] + public void VerifyAssemblyInRedistListWindowsRedistName() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsTrue(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// If the assembly was found in a redis list which does have the correct redist name , Microsoft-Windows-CLRCoreComp then we should consider it a framework assembly taking into account including partial matching + /// + [TestMethod] + public void VerifyAssemblyInRedistListPartialMatches() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsTrue(inRedistList); + + a1 = new AssemblyNameExtension("System, Culture=Neutral, PublicKeyToken='b77a5c561934e089'"); + inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsTrue(inRedistList); + + a1 = new AssemblyNameExtension("System, PublicKeyToken='b77a5c561934e089'"); + inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsTrue(inRedistList); + + a1 = new AssemblyNameExtension("System"); + inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsTrue(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + /// + /// Verify when we ask if an assembly is in the redist list we get the right answer. + /// The version should not be compared + /// + [TestMethod] + public void VerifyAssemblyInRedistListDiffVersion() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("System, Version=5.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsTrue(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + + + /// + /// Verify when we ask if an assembly is in the redist list we get the right answer. + /// The public key is significant and should make the match not work + /// + [TestMethod] + public void VerifyAssemblyInRedistListDiffPublicKey() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("System, Version=5.0.0.0, Culture=Neutral, PublicKeyToken='b67a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsFalse(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Verify when we ask if an assembly is in the redist list we get the right answer. + /// The Culture is significant and should make the match not work + /// + [TestMethod] + public void VerifyAssemblyInRedistListDiffCulture() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b67a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsFalse(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Verify when we ask if an assembly is in the redist list we get the right answer. + /// The SimpleName is significant and should make the match not work + /// + [TestMethod] + public void VerifyAssemblyInRedistListDiffSimpleName() + { + string redistFile = FileUtilities.GetTemporaryFile(); + try + { + File.Delete(redistFile); + File.WriteAllText + ( + redistFile, + "" + + "" + + "" + ); + + AssemblyTableInfo tableInfo = new AssemblyTableInfo(redistFile, "DoesNotExist"); + RedistList redist = RedistList.GetRedistList(new AssemblyTableInfo[] { tableInfo }); + + AssemblyNameExtension a1 = new AssemblyNameExtension("Something, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'"); + bool inRedistList = redist.FrameworkAssemblyEntryInRedist(a1); + Assert.IsFalse(inRedistList); + } + finally + { + File.Delete(redistFile); + } + } + + /// + /// Verify when a p2p (assemblies in the AssemblyFiles property) are passed to rar that we properly un-resolve them if they depend on references which are in the black list for the profile. + /// + [TestMethod] + public void Verifyp2pAndProfile() + { + // Create a generic redist list with system.xml and microsoft.build.engine. + string profileRedistList = String.Empty; + string fullRedistList = String.Empty; + string fullFrameworkDirectory = Path.Combine(Path.GetTempPath(), "Verifyp2pAndProfile"); + string targetFrameworkDirectory = Path.Combine(fullFrameworkDirectory, "Profiles\\Client"); + + string fullRedistListContents = + "" + + "" + + ""; + + try + { + GenerateRedistAndProfileXmlLocations(fullRedistListContents, _engineOnlySubset, out profileRedistList, out fullRedistList, fullFrameworkDirectory, targetFrameworkDirectory); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.AssemblyFiles = new ITaskItem[] { new TaskItem(@"c:\MyComponents\misc\DependsOn9Also.dll") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}", fullFrameworkDirectory }; + t.TargetFrameworkDirectories = new string[] { targetFrameworkDirectory }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(profileRedistList) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + t.FullFrameworkFolders = new string[] { fullFrameworkDirectory }; + t.ProfileName = "Client"; + t.TargetFrameworkMoniker = ".Net Framework, Version=v4.0"; + + bool success = Execute(t, false); + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(0, t.ResolvedFiles.Length, "Expected no resolved assemblies."); + string warningMessage = t.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", @"c:\MyComponents\misc\DependsOn9Also.dll", "System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.TargetFrameworkMoniker); + e.AssertLogContains(warningMessage); + } + finally + { + if (Directory.Exists(fullFrameworkDirectory)) + { + Directory.Delete(fullFrameworkDirectory, true); + } + } + } + + /// + /// Verify when a p2p (assemblies in the AssemblyFiles property) are passed to rar that we properly resolve them if they depend on references which are in the black list for the profile but have specific version set to true. + /// + [TestMethod] + public void Verifyp2pAndProfile2() + { + // Create a generic redist list with system.xml and microsoft.build.engine. + string profileRedistList = String.Empty; + string fullRedistList = String.Empty; + string fullFrameworkDirectory = Path.Combine(Path.GetTempPath(), "Verifyp2pAndProfile"); + string targetFrameworkDirectory = Path.Combine(fullFrameworkDirectory, "Profiles\\Client"); + + string fullRedistListContents = + "" + + "" + + ""; + + try + { + GenerateRedistAndProfileXmlLocations(fullRedistListContents, _engineOnlySubset, out profileRedistList, out fullRedistList, fullFrameworkDirectory, targetFrameworkDirectory); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine e = new MockEngine(); + t.BuildEngine = e; + TaskItem item = new TaskItem(@"c:\MyComponents\misc\DependsOn9Also.dll"); + item.SetMetadata("SpecificVersion", "true"); + t.AssemblyFiles = new ITaskItem[] { item }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}", fullFrameworkDirectory }; + t.TargetFrameworkDirectories = new string[] { targetFrameworkDirectory }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(profileRedistList) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + t.FullFrameworkFolders = new string[] { fullFrameworkDirectory }; + t.ProfileName = "Client"; + + bool success = Execute(t); + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected no resolved assemblies."); + string warningMessage = t.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", @"c:\MyComponents\misc\DependsOn9Also.dll", "System, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "Client"); + e.AssertLogDoesntContain(warningMessage); + } + finally + { + if (Directory.Exists(fullFrameworkDirectory)) + { + Directory.Delete(fullFrameworkDirectory, true); + } + } + } + + /// + /// Verify when a profile is used that assemblies not in the profile are excluded or have metadata attached to indicate there are dependencies + /// which are not in the profile. + /// + [TestMethod] + public void VerifyClientProfileRedistListAndProfileList() + { + // Create a generic redist list with system.xml and microsoft.build.engine. + string profileRedistList = String.Empty; + string fullRedistList = String.Empty; + string fullFrameworkDirectory = Path.Combine(Path.GetTempPath(), "VerifyClientProfileRedistListAndProfileList"); + string targetFrameworkDirectory = Path.Combine(fullFrameworkDirectory, "Profiles\\Client"); + try + { + GenerateRedistAndProfileXmlLocations(_fullRedistListContents, _engineOnlySubset, out profileRedistList, out fullRedistList, fullFrameworkDirectory, targetFrameworkDirectory); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.Assemblies = new ITaskItem[] { new TaskItem("Microsoft.Build.Engine"), new TaskItem("System.Xml") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}", fullFrameworkDirectory }; + t.TargetFrameworkDirectories = new string[] { targetFrameworkDirectory }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(profileRedistList) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + t.FullFrameworkFolders = new string[] { fullFrameworkDirectory }; + t.ProfileName = "Client"; + + string microsoftBuildEnginePath = Path.Combine(fullFrameworkDirectory, "Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(targetFrameworkDirectory, "System.Xml.dll"); + + bool success = GenerateHelperDelegatesAndExecuteTask(t, microsoftBuildEnginePath, systemXmlPath); + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("Microsoft.Build.Engine"), "Expected Engine to resolve."); + e.AssertLogContains("MSB3252"); + } + finally + { + if (Directory.Exists(fullFrameworkDirectory)) + { + Directory.Delete(fullFrameworkDirectory, true); + } + } + } + + /// + /// Verify when a profile is used that assemblies not in the profile are excluded or have metadata attached to indicate there are dependencies + /// which are not in the profile. + /// + /// Make sure the ProfileFullFrameworkAssemblyTable parameter works. + /// + [TestMethod] + public void VerifyClientProfileRedistListAndProfileList2() + { + // Create a generic redist list with system.xml and microsoft.build.engine. + string profileRedistList = String.Empty; + string fullRedistList = String.Empty; + string fullFrameworkDirectory = Path.Combine(Path.GetTempPath(), "VerifyClientProfileRedistListAndProfileList2"); + string targetFrameworkDirectory = Path.Combine(fullFrameworkDirectory, "Profiles\\Client"); + try + { + GenerateRedistAndProfileXmlLocations(_fullRedistListContents, _engineOnlySubset, out profileRedistList, out fullRedistList, fullFrameworkDirectory, targetFrameworkDirectory); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.Assemblies = new ITaskItem[] { new TaskItem("Microsoft.Build.Engine"), new TaskItem("System.Xml") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}", fullFrameworkDirectory }; + t.TargetFrameworkDirectories = new string[] { targetFrameworkDirectory }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(profileRedistList) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + + ITaskItem item = new TaskItem(fullRedistList); + item.SetMetadata("FrameworkDirectory", Path.GetDirectoryName(fullRedistList)); + t.FullFrameworkAssemblyTables = new ITaskItem[] { item }; + t.ProfileName = "Client"; + + string microsoftBuildEnginePath = Path.Combine(fullFrameworkDirectory, "Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(targetFrameworkDirectory, "System.Xml.dll"); + + bool success = GenerateHelperDelegatesAndExecuteTask(t, microsoftBuildEnginePath, systemXmlPath); + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(1, t.ResolvedFiles.Length, "Expected one resolved assembly."); + Assert.IsTrue(t.ResolvedFiles[0].ItemSpec.Contains("Microsoft.Build.Engine"), "Expected Engine to resolve."); + e.AssertLogContains("MSB3252"); + } + finally + { + if (Directory.Exists(fullFrameworkDirectory)) + { + Directory.Delete(fullFrameworkDirectory, true); + } + } + } + + /// + /// When targeting a profile make sure that we do not resolve the assembly if we reference something from the full framework which is in the GAC. + /// This will cover the same where we are referencing a full framework assembly. + /// + [TestMethod] + public void VerifyAssemblyInGacButNotInProfileIsNotResolved() + { + // Create a generic redist list with system.xml and microsoft.build.engine. + string profileRedistList = String.Empty; + string fullRedistList = String.Empty; + string fullFrameworkDirectory = Path.Combine(Path.GetTempPath(), "VerifyAssemblyInGacButNotInProfileIsNotResolved"); + string targetFrameworkDirectory = Path.Combine(fullFrameworkDirectory, "Profiles\\Client"); + useFrameworkFileExists = true; + string fullRedistListContents = + "" + + "" + + ""; + + try + { + GenerateRedistAndProfileXmlLocations(fullRedistListContents, _engineOnlySubset, out profileRedistList, out fullRedistList, fullFrameworkDirectory, targetFrameworkDirectory); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine e = new MockEngine(); + t.BuildEngine = e; + TaskItem item = new TaskItem(@"DependsOnOnlyv4Assemblies, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089"); + t.Assemblies = new ITaskItem[] { item }; + t.SearchPaths = new string[] { @"c:\MyComponents\4.0Component\", "{GAC}" }; + t.TargetFrameworkDirectories = new string[] { targetFrameworkDirectory }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(profileRedistList) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + t.FullFrameworkFolders = new string[] { fullFrameworkDirectory }; + t.LatestTargetFrameworkDirectories = new string[] { fullFrameworkDirectory }; + t.ProfileName = "Client"; + t.TargetFrameworkMoniker = ".NETFramework, Version=4.0"; + + bool success = Execute(t, false); + Console.Out.WriteLine(e.Log); + Assert.IsTrue(success, "Expected no errors."); + Assert.AreEqual(0, t.ResolvedFiles.Length, "Expected no files to resolved."); + string warningMessage = t.Log.FormatResourceString("ResolveAssemblyReference.FailBecauseDependentAssemblyInExclusionList", "DependsOnOnlyv4Assemblies, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089", "SysTem, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", t.TargetFrameworkMoniker); + e.AssertLogContains(warningMessage); + } + finally + { + useFrameworkFileExists = false; + if (Directory.Exists(fullFrameworkDirectory)) + { + Directory.Delete(fullFrameworkDirectory, true); + } + } + } + + /// + /// Make sure when reading in the full framework redist list or when reading in the white list xml files. + /// Errors in reading the file should be logged as warnings and no assemblies should be excluded. + /// + /// + [TestMethod] + public void VerifyProfileErrorsAreLogged() + { + // Create a generic redist list with system.xml and microsoft.build.engine. + string profileRedistList = String.Empty; + string fullRedistList = String.Empty; + string fullFrameworkDirectory = Path.Combine(Path.GetTempPath(), "VerifyProfileErrorsAreLogged"); + string targetFrameworkDirectory = Path.Combine(fullFrameworkDirectory, "Profiles\\Client"); + try + { + string fullRedistListContentsErrors = + "" + + "File AssemblyName='System.Xml' Version='2.0.0.0' PublicKeyToken='b03f5f7f11d50a3a' Culture='Neutral' FileVersion='2.0.50727.208' InGAC='true' >" + + "File AssemblyName='Microsoft.Build.Engine' Version='2.0.0.0' PublicKeyToken='b03f5f7f11d50a3a' Culture='Neutral' FileVersion='2.0.50727.208' InGAC='true' >" + + ""; + + GenerateRedistAndProfileXmlLocations(fullRedistListContentsErrors, _engineOnlySubset, out profileRedistList, out fullRedistList, fullFrameworkDirectory, targetFrameworkDirectory); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + MockEngine e = new MockEngine(); + t.BuildEngine = e; + t.Assemblies = new ITaskItem[] { new TaskItem("Microsoft.Build.Engine"), new TaskItem("System.Xml") }; + t.SearchPaths = new string[] { @"{TargetFrameworkDirectory}", fullFrameworkDirectory }; + t.TargetFrameworkDirectories = new string[] { targetFrameworkDirectory }; + t.InstalledAssemblyTables = new ITaskItem[] { new TaskItem(profileRedistList) }; + t.IgnoreDefaultInstalledAssemblyTables = true; + + ITaskItem item = new TaskItem(fullRedistList); + item.SetMetadata("FrameworkDirectory", Path.GetDirectoryName(fullRedistList)); + t.FullFrameworkAssemblyTables = new ITaskItem[] { item }; + t.ProfileName = "Client"; + + string microsoftBuildEnginePath = Path.Combine(fullFrameworkDirectory, "Microsoft.Build.Engine.dll"); + string systemXmlPath = Path.Combine(targetFrameworkDirectory, "System.Xml.dll"); + + bool success = GenerateHelperDelegatesAndExecuteTask(t, microsoftBuildEnginePath, systemXmlPath); + Assert.IsTrue(success, "Expected errors."); + Assert.AreEqual(2, t.ResolvedFiles.Length, "Expected two resolved assembly."); + e.AssertLogContains("MSB3263"); + } + finally + { + if (Directory.Exists(fullFrameworkDirectory)) + { + Directory.Delete(fullFrameworkDirectory, true); + } + } + } + + /// + /// Generate the full framework and profile redist list directories and files + /// + private static void GenerateRedistAndProfileXmlLocations(string fullRedistContents, string profileListContents, out string profileRedistList, out string fullRedistList, string fullFrameworkDirectory, string targetFrameworkDirectory) + { + fullRedistList = Path.Combine(fullFrameworkDirectory, "RedistList\\FrameworkList.xml"); + string redistDirectory = Path.GetDirectoryName(fullRedistList); + if (Directory.Exists(redistDirectory)) + { + Directory.Delete(redistDirectory); + } + + Directory.CreateDirectory(redistDirectory); + + File.WriteAllText(fullRedistList, fullRedistContents); + + profileRedistList = Path.Combine(targetFrameworkDirectory, "RedistList\\FrameworkList.xml"); + + redistDirectory = Path.GetDirectoryName(profileRedistList); + if (Directory.Exists(redistDirectory)) + { + Directory.Delete(redistDirectory); + } + + Directory.CreateDirectory(redistDirectory); + + File.WriteAllText(profileRedistList, profileListContents); + } + } + + /// + /// Unit tests for the ResolveAssemblyReference GlobalAssemblyCache. + /// + [TestClass] + sealed public class GlobalAssemblyCacheTests : ResolveAssemblyReferenceTestFixture + { + private const string system4 = "System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + private const string system2 = "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + private const string system1 = "System, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + private const string systemNotStrong = "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"; + + private const string system4Path = "c:\\clr4\\System.dll"; + private const string system2Path = "c:\\clr2\\System.dll"; + private const string system1Path = "c:\\clr2\\System1.dll"; + + private GetAssemblyRuntimeVersion _runtimeVersion = new GetAssemblyRuntimeVersion(MockGetRuntimeVersion); + private GetPathFromFusionName _getPathFromFusionName = new GetPathFromFusionName(MockGetPathFromFusionName); + private GetGacEnumerator _gacEnumerator = new GetGacEnumerator(MockAssemblyCacheEnumerator); + + /// + /// Verify when the GAC enumerator returns + /// + /// System, Version=4.0.0.0 Runtime=4.0xxxx + /// System, Version=2.0.0.0 Runtime=2.0xxxx + /// System, Version=1.0.0.0 Runtime=2.0xxxx + /// + /// And we target 2.0 runtime that we get the Version 2.0.0.0 system. + /// + /// This test two aspects. First that we get the correct runtime, second that we get the highest version for that assembly in the runtime. + /// + [TestMethod] + public void VerifySimpleNamev2057020() + { + // We want to pass a very generic name to get the correct gac entries. + AssemblyNameExtension fusionName = new AssemblyNameExtension("System"); + + + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("2.0.57027"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, false); + Assert.IsNotNull(path); + Assert.IsTrue(path.Equals(system2Path, StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// Verify when the GAC enumerator returns + /// + /// System, Version=4.0.0.0 Runtime=4.0xxxx + /// System, Version=2.0.0.0 Runtime=2.0xxxx + /// System, Version=1.0.0.0 Runtime=2.0xxxx + /// + /// And we target 2.0 runtime that we get the Version 2.0.0.0 system. + /// + /// Verify that by setting the wants sspecific version to true that we will return the highest version when only the simple name is used. + /// Essentially specific version for the gac resolver means do not filter by runtime. + /// + [TestMethod] + public void VerifySimpleNamev2057020SpecificVersion() + { + // We want to pass a very generic name to get the correct gac entries. + AssemblyNameExtension fusionName = new AssemblyNameExtension("System"); + + + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("2.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true); + Assert.IsNotNull(path); + Assert.IsTrue(path.Equals(system4Path, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when the GAC enumerator returns + /// + /// System, Version=2.0.0.0 Runtime=2.0xxxx + /// System, Version=1.0.0.0 Runtime=2.0xxxx + /// + /// And we target 2.0 runtime that we get the Version 2.0.0.0 system. + /// + /// Verify that by setting the wants sspecific version to true that we will return the highest version when only the simple name is used. + /// Essentially specific version for the gac resolver means do not filter by runtime. + /// + [TestMethod] + public void VerifyFusionNamev2057020SpecificVersion() + { + // We want to pass a very generic name to get the correct gac entries. + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, Version=2.0.0.0"); + + + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("2.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true); + Assert.IsNotNull(path); + Assert.IsTrue(path.Equals(system2Path, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when the GAC enumerator returns + /// + /// System, Version=4.0.0.0 Runtime=4.0xxxx + /// System, Version=2.0.0.0 Runtime=2.0xxxx + /// System, Version=1.0.0.0 Runtime=2.0xxxx + /// + /// And we target 4.0 runtime that we get the Version 4.0.0.0 system. + /// + /// This test two aspects. First that we get the correct runtime, second that we get the highest version for that assembly in the runtime. + /// + [TestMethod] + public void VerifySimpleNamev40() + { + // We want to pass a very generic name to get the correct gac entries. + AssemblyNameExtension fusionName = new AssemblyNameExtension("System"); + + + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("4.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, false); + Assert.IsNotNull(path); + Assert.IsTrue(path.Equals(system4Path, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when the GAC enumerator returns + /// + /// System, Version=4.0.0.0 Runtime=4.0xxxx + /// System, Version=2.0.0.0 Runtime=2.0xxxx + /// System, Version=1.0.0.0 Runtime=2.0xxxx + /// + /// And we target 4.0 runtime that we get the Version 4.0.0.0 system. + /// + /// Verify that by setting the wants sspecific version to true that we will return the highest version when only the simple name is used. + /// Essentially specific version for the gac resolver means do not filter by runtime. + /// + [TestMethod] + public void VerifySimpleNamev40SpecificVersion() + { + // We want to pass a very generic name to get the correct gac entries. + AssemblyNameExtension fusionName = new AssemblyNameExtension("System"); + + + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("4.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true); + Assert.IsNotNull(path); + Assert.IsTrue(path.Equals(system4Path, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when the GAC enumerator returns + /// + /// System, Version=4.0.0.0 Runtime=4.0xxxx + /// + /// + /// Verify that by setting the wants sspecific version to true that we will return the highest version when only the simple name is used. + /// Essentially specific version for the gac resolver means do not filter by runtime. + /// + [TestMethod] + public void VerifyFusionNamev40SpecificVersion() + { + // We want to pass a very generic name to get the correct gac entries. + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, Version=4.0.0.0"); + + + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("4.0.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true); + Assert.IsNotNull(path); + Assert.IsTrue(path.Equals(system4Path, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify when a assembly name is passed in which has the public key explicitly set to null that we return null as the assembly cannot be in the gac. + /// + [TestMethod] + [ExpectedException(typeof(FileLoadException))] + public void VerifyEmptyPublicKeyspecificVersion() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken="); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true); + } + + + /// + /// Verify when a assembly name is passed in which has the public key explicitly set to null that we return null as the assembly cannot be in the gac. + /// + [TestMethod] + public void VerifyNullPublicKey() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken=null"); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, false); + Assert.IsNull(path); + } + + /// + /// Verify when a assembly name is passed in which has the public key explicitly set to null that we return null as the assembly cannot be in the gac. + /// + [TestMethod] + public void VerifyNullPublicKeyspecificVersion() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken=null"); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true); + Assert.IsNull(path); + } + + + /// + /// When a processor architecture is on the end of a fusion name we were appending another processor architecture onto the end causing an invalid fusion name + /// this was causing the GAC (api's) to crash. + /// + [TestMethod] + public void VerifyProcessorArchitectureDoesNotCrash() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL"); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.MSIL, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, null /* use the real gac enumerator*/, false); + Assert.IsNull(path); + } + + /// + /// When a processor architecture is on the end of a fusion name we were appending another processor architecture onto the end causing an invalid fusion name + /// this was causing the GAC (api's) to crash. + /// + [TestMethod] + public void VerifyProcessorArchitectureDoesNotCrashSpecificVersion() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL"); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.MSIL, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, null /* use the real gac enumerator*/, true); + Assert.IsNull(path); + } + + /// + /// See bug 648678, when a processor architecture is on the end of a fusion name we were appending another processor architecture onto the end causing an invalid fusion name + /// this was causing the GAC (api's) to crash. + /// + [TestMethod] + public void VerifyProcessorArchitectureDoesNotCrashFullFusionName() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL"); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.MSIL, getRuntimeVersion, new Version("2.0.50727"), true, new FileExists(MockFileExists), _getPathFromFusionName, null /* use the real gac enumerator*/, false); + Assert.IsNull(path); + } + + /// + /// When a processor architecture is on the end of a fusion name we were appending another processor architecture onto the end causing an invalid fusion name + /// this was causing the GAC (api's) to crash. + /// + [TestMethod] + public void VerifyProcessorArchitectureDoesNotCrashFullFusionNameSpecificVersion() + { + AssemblyNameExtension fusionName = new AssemblyNameExtension("System, PublicKeyToken=b77a5c561934e089, ProcessorArchitecture=MSIL"); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.MSIL, getRuntimeVersion, new Version("2.0.50727"), true, new FileExists(MockFileExists), _getPathFromFusionName, null /* use the real gac enumerator*/, true); + Assert.IsNull(path); + } + + + // System.Runtime dependency calculation tests + + // No dependency + [TestMethod] + public void SystemRuntimeDepends_No_Build() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Regular"), + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\SystemRuntime\Regular.dll"); + + t.SearchPaths = DefaultPaths; + + // build mode + t.FindDependencies = true; + t.ForceSystemRuntimeDependencyCalculation = false; + + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "false", StringComparison.OrdinalIgnoreCase), + "Expected no System.Runtime dependency found during build."); + + // intelli build mode + t.FindDependencies = false; + t.ForceSystemRuntimeDependencyCalculation = true; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "false", StringComparison.OrdinalIgnoreCase), + "Expected no System.Runtime dependency found during intellibuild."); + + // rar mode + // intelli build mode + t.FindDependencies = false; + t.ForceSystemRuntimeDependencyCalculation = false; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "false", StringComparison.OrdinalIgnoreCase), + "Expected no System.Runtime dependency found for standalone RAR."); + } + + + // Direct dependency + [TestMethod] + public void SystemRuntimeDepends_Yes() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("System.Runtime"), + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\SystemRuntime\System.Runtime.dll"); + + t.SearchPaths = DefaultPaths; + + // build mode + t.FindDependencies = true; + t.ForceSystemRuntimeDependencyCalculation = false; + + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "true", StringComparison.OrdinalIgnoreCase), + "Expected System.Runtime dependency found during build."); + + // intelli build mode + t.FindDependencies = false; + t.ForceSystemRuntimeDependencyCalculation = true; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "true", StringComparison.OrdinalIgnoreCase), + "Expected System.Runtime dependency found during intellibuild."); + + // rar mode + // intelli build mode + t.FindDependencies = false; + t.ForceSystemRuntimeDependencyCalculation = false; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "true", StringComparison.OrdinalIgnoreCase), + "Expected System.Runtime dependency found for standalone RAR."); + } + + // Indirect dependency + [TestMethod] + public void SystemRuntimeDepends_Yes_Indirect() + { + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + t.BuildEngine = new MockEngine(); + + t.Assemblies = new ITaskItem[] + { + new TaskItem("Portable"), + }; + + t.Assemblies[0].SetMetadata("HintPath", @"C:\SystemRuntime\Portable.dll"); + + t.SearchPaths = DefaultPaths; + + // build mode + t.FindDependencies = true; + t.ForceSystemRuntimeDependencyCalculation = false; + + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "true", StringComparison.OrdinalIgnoreCase), + "Expected System.Runtime dependency found during build."); + + // intelli build mode + t.FindDependencies = false; + t.ForceSystemRuntimeDependencyCalculation = true; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "true", StringComparison.OrdinalIgnoreCase), + "Expected System.Runtime dependency found during intellibuild."); + + // rar mode + // intelli build mode + t.FindDependencies = false; + t.ForceSystemRuntimeDependencyCalculation = false; + Assert.IsTrue(t.Execute(fileExists, directoryExists, getDirectories, getAssemblyName, getAssemblyMetadata, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getLastWriteTime, getRuntimeVersion, openBaseKey, checkIfAssemblyIsInGac, isWinMDFile, readMachineTypeFromPEHeader)); + + Assert.IsTrue(string.Equals(t.DependsOnSystemRuntime, "false", StringComparison.OrdinalIgnoreCase), + "Expected no System.Runtime dependency found for standalone RAR."); + } + + #region HelperDelegates + + private static string MockGetRuntimeVersion(string path) + { + if (path.Equals(system1Path, StringComparison.OrdinalIgnoreCase)) + { + return "v2.0.50727"; + } + + if (path.Equals(system4Path, StringComparison.OrdinalIgnoreCase)) + { + return "v4.0.0"; + } + + if (path.Equals(system2Path, StringComparison.OrdinalIgnoreCase)) + { + return "v2.0.50727"; + } + + return String.Empty; + } + + private bool MockFileExists(string path) + { + return true; + } + + private static string MockGetPathFromFusionName(string strongName) + { + if (strongName.Equals(system1, StringComparison.OrdinalIgnoreCase)) + { + return system1Path; + } + + if (strongName.Equals(system2, StringComparison.OrdinalIgnoreCase)) + { + return system2Path; + } + + if (strongName.Equals(systemNotStrong, StringComparison.OrdinalIgnoreCase)) + { + return system2Path; + } + + if (strongName.Equals(system4, StringComparison.OrdinalIgnoreCase)) + { + return system4Path; + } + + return String.Empty; + } + + private static IEnumerable MockAssemblyCacheEnumerator(string strongName) + { + List listOfAssemblies = new List(); + + if (strongName.StartsWith("System, Version=2.0.0.0", StringComparison.OrdinalIgnoreCase)) + { + listOfAssemblies.Add(system2); + } + else if (strongName.StartsWith("System, Version=4.0.0.0", StringComparison.OrdinalIgnoreCase)) + { + listOfAssemblies.Add(system4); + } + else + { + listOfAssemblies.Add(system1); + listOfAssemblies.Add(system2); + listOfAssemblies.Add(system4); + } + return new MockEnumerator(listOfAssemblies); + } + + internal class MockEnumerator : IEnumerable + { + private List _assembliesToEnumerate = null; + private List.Enumerator _enumerator; + + public MockEnumerator(List assembliesToEnumerate) + { + _assembliesToEnumerate = assembliesToEnumerate; + + _enumerator = assembliesToEnumerate.GetEnumerator(); + } + + + public IEnumerator GetEnumerator() + { + foreach (string assembly in _assembliesToEnumerate) + { + yield return new AssemblyNameExtension(assembly); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator)GetEnumerator(); + } + } + + #endregion + } +} diff --git a/src/XMakeTasks/UnitTests/ResolveCodeAnalysisRuleSet_Tests.cs b/src/XMakeTasks/UnitTests/ResolveCodeAnalysisRuleSet_Tests.cs new file mode 100644 index 00000000000..45dd151e7bf --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResolveCodeAnalysisRuleSet_Tests.cs @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ResolveAnalyzerRuleSet_Tests + { + private class TemporaryFile : IDisposable + { + private readonly string _fullPath; + + public TemporaryFile(string fullPath, string contents) + { + _fullPath = fullPath; + + File.WriteAllText(fullPath, contents); + } + + public void Dispose() + { + File.Delete(_fullPath); + } + } + + private class TemporaryDirectory : IDisposable + { + private readonly string _path; + + public TemporaryDirectory(string path) + { + _path = path; + + Directory.CreateDirectory(path); + } + + public void Dispose() + { + Directory.Delete(_path, recursive: true); + } + } + + [TestMethod] + public void GetResolvedRuleSetPath_FullPath_NonExistent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string codeAnalysisRuleSet = @"C:\foo\bar\CodeAnalysis.ruleset"; + + task.CodeAnalysisRuleSet = codeAnalysisRuleSet; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogContains("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_FullPath_Existent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string codeAnalysisRuleSet = Path.Combine(Path.GetTempPath(), @"CodeAnalysis.ruleset"); + + task.CodeAnalysisRuleSet = codeAnalysisRuleSet; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = null; + + using (new TemporaryFile(codeAnalysisRuleSet, "foo")) + { + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: codeAnalysisRuleSet, actual: resolvedRuleSet); + mockEngine.AssertLogDoesntContain("MSB3884"); + } + } + + [TestMethod] + public void GetResolvedRuleSetPath_SimpleNameAlone_NonExistent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + task.CodeAnalysisRuleSet = @"CodeAnalysis.ruleset"; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogContains("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_SimpleNameAndProjectDirectory_Existent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string codeAnalysisRuleSet = @"CodeAnalysis.ruleset"; + string projectDirectory = Path.GetTempPath(); + + task.CodeAnalysisRuleSet = codeAnalysisRuleSet; + task.MSBuildProjectDirectory = projectDirectory; + task.CodeAnalysisRuleSetDirectories = null; + + string ruleSetFullPath = Path.Combine(projectDirectory, codeAnalysisRuleSet); + + using (new TemporaryFile(ruleSetFullPath, "foo")) + { + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: codeAnalysisRuleSet, actual: resolvedRuleSet); + mockEngine.AssertLogDoesntContain("MSB3884"); + } + } + + [TestMethod] + public void GetResolvedRuleSetPath_SimpleNameAndProjectDirectory_NonExistent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string projectDirectory = Path.GetTempPath(); + string codeAnalysisRuleSet = Path.GetRandomFileName() + ".ruleset"; + + task.CodeAnalysisRuleSet = codeAnalysisRuleSet; + task.MSBuildProjectDirectory = projectDirectory; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogContains("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_SimpleNameAndDirectories_Existent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string codeAnalysisRuleSet = @"CodeAnalysis.ruleset"; + var directory = Path.GetTempPath(); + + task.CodeAnalysisRuleSet = codeAnalysisRuleSet; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = new[] { directory }; + + string ruleSetFullPath = Path.Combine(directory, codeAnalysisRuleSet); + + using (new TemporaryFile(ruleSetFullPath, "foo")) + { + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: ruleSetFullPath, actual: resolvedRuleSet); + mockEngine.AssertLogDoesntContain("MSB3884"); + } + } + + [TestMethod] + public void GetResolvedRuleSetPath_SimpleNameAndDirectories_NonExistent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string directory = Path.GetTempPath(); + + task.CodeAnalysisRuleSet = Path.GetRandomFileName() + ".ruleset"; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = new[] { directory }; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogContains("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_RelativePath_WithProject_NonExistent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string subdirectoryName = Path.GetRandomFileName(); + string projectDirectory = Path.GetTempPath(); + + task.CodeAnalysisRuleSet = Path.Combine(subdirectoryName, "CodeAnalysis.ruleset"); + task.MSBuildProjectDirectory = projectDirectory; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogContains("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_RelativePath_WithProject_Existent() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string subdirectoryName = Path.GetRandomFileName(); + string codeAnalysisRuleSet = Path.Combine(subdirectoryName, "CodeAnalysis.ruleset"); + string projectDirectory = Path.GetTempPath(); + + task.CodeAnalysisRuleSet = codeAnalysisRuleSet; + task.MSBuildProjectDirectory = projectDirectory; + task.CodeAnalysisRuleSetDirectories = null; + + string ruleSetFullPath = Path.Combine(projectDirectory, codeAnalysisRuleSet); + + using (new TemporaryDirectory(Path.GetDirectoryName(ruleSetFullPath))) + using (new TemporaryFile(ruleSetFullPath, "foo")) + { + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: codeAnalysisRuleSet, actual: resolvedRuleSet); + mockEngine.AssertLogDoesntContain("MSB3884"); + } + } + + [TestMethod] + public void GetResolvedRuleSetPath_RelativePath_NoProject() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + string subdirectoryName = Path.GetRandomFileName(); + task.CodeAnalysisRuleSet = Path.Combine(subdirectoryName, "CodeAnalysis.ruleset"); + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogContains("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_EmptyString() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + task.CodeAnalysisRuleSet = string.Empty; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogDoesntContain("MSB3884"); + } + + [TestMethod] + public void GetResolvedRuleSetPath_Null() + { + MockEngine mockEngine = new MockEngine(); + ResolveCodeAnalysisRuleSet task = new ResolveCodeAnalysisRuleSet(); + task.BuildEngine = mockEngine; + + task.CodeAnalysisRuleSet = null; + task.MSBuildProjectDirectory = null; + task.CodeAnalysisRuleSetDirectories = null; + + bool result = task.Execute(); + string resolvedRuleSet = task.ResolvedCodeAnalysisRuleSet; + + Assert.AreEqual(expected: true, actual: result); + Assert.AreEqual(expected: null, actual: resolvedRuleSet); + mockEngine.AssertLogDoesntContain("MSB3884"); + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/ResolveComReference_Tests.cs b/src/XMakeTasks/UnitTests/ResolveComReference_Tests.cs new file mode 100644 index 00000000000..a18b979c798 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResolveComReference_Tests.cs @@ -0,0 +1,839 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Collections.Generic; + + +// TYPELIBATTR clashes with the one in InteropServices. +using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: ResolveComReference_Tests + * + * Test the ResolveComReference task in various ways. + * + */ + [TestClass] + sealed public class ResolveComReference_Tests + { + /* + * Method: SetupTaskItem + * + * Creates a valid task item that's modified later + */ + private TaskItem SetupTaskItem() + { + TaskItem item = new TaskItem(); + + item.SetMetadata(ComReferenceItemMetadataNames.guid, "{5C6D0C4D-D530-4B08-B22F-307CA6BFCB65}"); + item.SetMetadata(ComReferenceItemMetadataNames.versionMajor, "1"); + item.SetMetadata(ComReferenceItemMetadataNames.versionMinor, "0"); + item.SetMetadata(ComReferenceItemMetadataNames.lcid, "0"); + item.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, "tlbimp"); + + return item; + } + + private void AssertReference(ITaskItem item, bool valid, string attribute) + { + string missingOrInvalidAttribute = null; + Assert.IsTrue(ResolveComReference.VerifyReferenceMetadataForNameItem(item, out missingOrInvalidAttribute) == valid); + Assert.IsTrue(missingOrInvalidAttribute == attribute); + } + + private void AssertMetadataInitialized(ITaskItem item, string metadataName, string metadataValue) + { + Assert.IsTrue(item.GetMetadata(metadataName) == metadataValue); + } + + /// + /// Issue in this bug was an ArgumentNullException when ResolvedAssemblyReferences was null + /// + [TestMethod] + public void GetResolvedASsemblyReferenceSpecNotNull() + { + var task = new ResolveComReference(); + Assert.IsTrue(null != task.GetResolvedAssemblyReferenceItemSpecs()); + } + + /* + * Method: CheckComReferenceAttributeVerificationForNameItems + * + * Checks if verification of Com reference item metadata works properly + */ + [TestMethod] + public void CheckComReferenceMetadataVerificationForNameItems() + { + // valid item + TaskItem item = SetupTaskItem(); + AssertReference(item, true, ""); + + // invalid guid + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.guid, "{I'm pretty sure this is not a valid guid}"); + AssertReference(item, false, ComReferenceItemMetadataNames.guid); + + // missing guid + item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.guid); + AssertReference(item, false, ComReferenceItemMetadataNames.guid); + + // invalid verMajor + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.versionMajor, "eleventy one"); + AssertReference(item, false, ComReferenceItemMetadataNames.versionMajor); + + // missing verMajor + item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.versionMajor); + AssertReference(item, false, ComReferenceItemMetadataNames.versionMajor); + + // invalid verMinor + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.versionMinor, "eleventy one"); + AssertReference(item, false, ComReferenceItemMetadataNames.versionMinor); + + // missing verMinor + item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.versionMinor); + AssertReference(item, false, ComReferenceItemMetadataNames.versionMinor); + + // invalid lcid + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.lcid, "Mars-us"); + AssertReference(item, false, ComReferenceItemMetadataNames.lcid); + + // missing lcid - it's optional, so this should work ok + item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.lcid); + AssertReference(item, true, String.Empty); + + // invalid tool + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, "crowbar"); + AssertReference(item, false, ComReferenceItemMetadataNames.wrapperTool); + + // missing tool - it's optional, so this should work ok + item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.wrapperTool); + AssertReference(item, true, String.Empty); + } + + /* + * Method: CheckComReferenceAttributeInitializationForNameItems + * + * Checks if missing optional attributes for COM name references get initialized correctly + */ + [TestMethod] + public void CheckComReferenceMetadataInitializationForNameItems() + { + // missing lcid - should get initialized to 0 + TaskItem item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.lcid); + ResolveComReference.InitializeDefaultMetadataForNameItem(item); + AssertMetadataInitialized(item, ComReferenceItemMetadataNames.lcid, "0"); + + // existing lcid - should not get modified + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.lcid, "1033"); + ResolveComReference.InitializeDefaultMetadataForNameItem(item); + AssertMetadataInitialized(item, ComReferenceItemMetadataNames.lcid, "1033"); + + // missing wrapperTool - should get initialized to tlbimp + item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.wrapperTool); + ResolveComReference.InitializeDefaultMetadataForNameItem(item); + AssertMetadataInitialized(item, ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.tlbimp); + + // existing wrapperTool - should not get modified + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.aximp); + ResolveComReference.InitializeDefaultMetadataForNameItem(item); + AssertMetadataInitialized(item, ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.aximp); + } + + /* + * Method: CheckComReferenceAttributeInitializationForFileItems + * + * Checks if missing optional attributes for COM file references get initialized correctly + */ + [TestMethod] + public void CheckComReferenceMetadataInitializationForFileItems() + { + // missing wrapperTool - should get initialized to tlbimp + TaskItem item = SetupTaskItem(); + item.RemoveMetadata(ComReferenceItemMetadataNames.wrapperTool); + ResolveComReference.InitializeDefaultMetadataForFileItem(item); + AssertMetadataInitialized(item, ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.tlbimp); + + // existing wrapperTool - should not get modified + item = SetupTaskItem(); + item.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.aximp); + ResolveComReference.InitializeDefaultMetadataForFileItem(item); + AssertMetadataInitialized(item, ComReferenceItemMetadataNames.wrapperTool, ComReferenceTypes.aximp); + } + + /// + /// Helper function for creating a COM reference task item instance + /// + private TaskItem CreateComReferenceTaskItem(string itemSpec, string guid, string vMajor, string vMinor, string lcid, string wrapperType, string embedInteropTypes) + { + TaskItem item = new TaskItem(itemSpec); + + item.SetMetadata(ComReferenceItemMetadataNames.guid, guid); + item.SetMetadata(ComReferenceItemMetadataNames.versionMajor, vMajor); + item.SetMetadata(ComReferenceItemMetadataNames.versionMinor, vMinor); + item.SetMetadata(ComReferenceItemMetadataNames.lcid, lcid); + item.SetMetadata(ComReferenceItemMetadataNames.wrapperTool, wrapperType); + item.SetMetadata(ItemMetadataNames.embedInteropTypes, embedInteropTypes); + + return item; + } + + /// + /// Helper function for creating a COM reference task item instance + /// + private TaskItem CreateComReferenceTaskItem(string itemSpec, string guid, string vMajor, string vMinor, string lcid, string wrapperType) + { + return CreateComReferenceTaskItem(itemSpec, guid, vMajor, vMinor, lcid, wrapperType, String.Empty); + } + + /// + /// Test the ResolveComReference.TaskItemToTypeLibAttr method + /// + [TestMethod] + public void CheckTaskItemToTypeLibAttr() + { + Guid refGuid = Guid.NewGuid(); + + TaskItem reference = CreateComReferenceTaskItem("ref", refGuid.ToString(), "11", "0", "1033", ComReferenceTypes.tlbimp); + TYPELIBATTR refAttr = ResolveComReference.TaskItemToTypeLibAttr(reference); + + Assert.IsTrue(refGuid == refAttr.guid, "incorrect guid"); + Assert.IsTrue(refAttr.wMajorVerNum == 11, "incorrect version major"); + Assert.IsTrue(refAttr.wMinorVerNum == 0, "incorrect version minor"); + Assert.IsTrue(refAttr.lcid == 1033, "incorrect lcid"); + } + + /// + /// Helper function for creating a ComReferenceInfo object using an existing TaskInfo object and + /// typelib name/path. The type lib pointer will obviously not be initialized, so this object cannot + /// be used in any code that uses it. + /// + private ComReferenceInfo CreateComReferenceInfo(ITaskItem taskItem, string typeLibName, string typeLibPath) + { + ComReferenceInfo referenceInfo = new ComReferenceInfo(); + + referenceInfo.taskItem = taskItem; + referenceInfo.attr = ResolveComReference.TaskItemToTypeLibAttr(taskItem); + referenceInfo.typeLibName = typeLibName; + referenceInfo.fullTypeLibPath = typeLibPath; + referenceInfo.strippedTypeLibPath = typeLibPath; + referenceInfo.typeLibPointer = null; + + return referenceInfo; + } + + /// + /// Create a few test references for unit tests + /// + private void CreateTestReferences( + out ComReferenceInfo axRefInfo, out ComReferenceInfo tlbRefInfo, out ComReferenceInfo piaRefInfo, + out TYPELIBATTR axAttr, out TYPELIBATTR tlbAttr, out TYPELIBATTR piaAttr, out TYPELIBATTR notInProjectAttr) + { + // doing my part to deplete the worldwide guid reserves... + Guid axGuid = Guid.NewGuid(); + Guid tlbGuid = Guid.NewGuid(); + Guid piaGuid = Guid.NewGuid(); + + // create reference task items + TaskItem axTaskItem = CreateComReferenceTaskItem("axref", axGuid.ToString(), "1", "0", "1033", ComReferenceTypes.aximp); + TaskItem tlbTaskItem = CreateComReferenceTaskItem("tlbref", tlbGuid.ToString(), "5", "1", "0", ComReferenceTypes.tlbimp); + TaskItem piaTaskItem = CreateComReferenceTaskItem("piaref", piaGuid.ToString(), "999", "444", "123", ComReferenceTypes.primary); + + // create reference infos + axRefInfo = CreateComReferenceInfo(axTaskItem, "AxRefLibName", "AxRefLibPath"); + tlbRefInfo = CreateComReferenceInfo(tlbTaskItem, "TlbRefLibName", "TlbRefLibPath"); + piaRefInfo = CreateComReferenceInfo(piaTaskItem, "PiaRefLibName", "PiaRefLibPath"); + + // get the references' typelib attributes + axAttr = ResolveComReference.TaskItemToTypeLibAttr(axTaskItem); + tlbAttr = ResolveComReference.TaskItemToTypeLibAttr(tlbTaskItem); + piaAttr = ResolveComReference.TaskItemToTypeLibAttr(piaTaskItem); + + // create typelib attributes not matching any of the project refs + notInProjectAttr = new TYPELIBATTR(); + notInProjectAttr.guid = tlbGuid; + notInProjectAttr.wMajorVerNum = 5; + notInProjectAttr.wMinorVerNum = 1; + notInProjectAttr.lcid = 1033; + } + + /// + /// Unit test for the ResolveComReference.IsExistingProjectReference() method + /// + [TestMethod] + public void CheckIsExistingProjectReference() + { + TYPELIBATTR axAttr, tlbAttr, piaAttr, notInProjectAttr; + ComReferenceInfo axRefInfo, tlbRefInfo, piaRefInfo; + + CreateTestReferences(out axRefInfo, out tlbRefInfo, out piaRefInfo, + out axAttr, out tlbAttr, out piaAttr, out notInProjectAttr); + + ResolveComReference rcr = new ResolveComReference(); + + // populate the ResolveComReference's list of project references + rcr.allProjectRefs = new List(); + rcr.allProjectRefs.Add(axRefInfo); + rcr.allProjectRefs.Add(tlbRefInfo); + rcr.allProjectRefs.Add(piaRefInfo); + + ComReferenceInfo referenceInfo; + + // find the Ax ref, matching with any type of reference - should NOT find it + bool retValue = rcr.IsExistingProjectReference(axAttr, null, out referenceInfo); + Assert.IsTrue(retValue == false && referenceInfo == null, "ActiveX ref should NOT be found for any type of ref"); + + // find the Ax ref, matching with aximp types - should find it + retValue = rcr.IsExistingProjectReference(axAttr, ComReferenceTypes.aximp, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == axRefInfo, "ActiveX ref should be found for aximp ref types"); + + // find the Ax ref, matching with tlbimp types - should NOT find it + retValue = rcr.IsExistingProjectReference(axAttr, ComReferenceTypes.tlbimp, out referenceInfo); + Assert.IsTrue(retValue == false && referenceInfo == null, "ActiveX ref should NOT be found for tlbimp ref types"); + + + // find the Tlb ref, matching with any type of reference - should find it + retValue = rcr.IsExistingProjectReference(tlbAttr, null, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == tlbRefInfo, "Tlb ref should be found for any type of ref"); + + // find the Tlb ref, matching with tlbimp types - should find it + retValue = rcr.IsExistingProjectReference(tlbAttr, ComReferenceTypes.tlbimp, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == tlbRefInfo, "Tlb ref should be found for tlbimp ref types"); + + // find the Tlb ref, matching with pia types - should NOT find it + retValue = rcr.IsExistingProjectReference(tlbAttr, ComReferenceTypes.primary, out referenceInfo); + Assert.IsTrue(retValue == false && referenceInfo == null, "Tlb ref should NOT be found for primary ref types"); + + + // find the Pia ref, matching with any type of reference - should find it + retValue = rcr.IsExistingProjectReference(piaAttr, null, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == piaRefInfo, "Pia ref should be found for any type of ref"); + + // find the Pia ref, matching with pia types - should find it + retValue = rcr.IsExistingProjectReference(piaAttr, ComReferenceTypes.primary, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == piaRefInfo, "Pia ref should be found for pia ref types"); + + // find the Pia ref, matching with pia types - should NOT find it + retValue = rcr.IsExistingProjectReference(piaAttr, ComReferenceTypes.aximp, out referenceInfo); + Assert.IsTrue(retValue == false && referenceInfo == null, "Pia ref should NOT be found for aximp ref types"); + + + // try to find a non existing reference + retValue = rcr.IsExistingProjectReference(notInProjectAttr, null, out referenceInfo); + Assert.IsTrue(retValue == false && referenceInfo == null, "not in project ref should not be found"); + } + + /// + /// Unit test for the ResolveComReference.IsExistingDependencyReference() method + /// + [TestMethod] + public void CheckIsExistingDependencyReference() + { + TYPELIBATTR axAttr, tlbAttr, piaAttr, notInProjectAttr; + ComReferenceInfo axRefInfo, tlbRefInfo, piaRefInfo; + + CreateTestReferences(out axRefInfo, out tlbRefInfo, out piaRefInfo, + out axAttr, out tlbAttr, out piaAttr, out notInProjectAttr); + + ResolveComReference rcr = new ResolveComReference(); + + // populate the ResolveComReference's list of project references + rcr.allDependencyRefs = new List(); + rcr.allDependencyRefs.Add(axRefInfo); + rcr.allDependencyRefs.Add(tlbRefInfo); + rcr.allDependencyRefs.Add(piaRefInfo); + + ComReferenceInfo referenceInfo; + + // find the Ax ref - should find it + bool retValue = rcr.IsExistingDependencyReference(axAttr, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == axRefInfo, "ActiveX ref should be found"); + + // find the Tlb ref - should find it + retValue = rcr.IsExistingDependencyReference(tlbAttr, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == tlbRefInfo, "Tlb ref should be found"); + + // find the Pia ref - should find it + retValue = rcr.IsExistingDependencyReference(piaAttr, out referenceInfo); + Assert.IsTrue(retValue == true && referenceInfo == piaRefInfo, "Pia ref should be found"); + + // try to find a non existing reference - should not find it + retValue = rcr.IsExistingDependencyReference(notInProjectAttr, out referenceInfo); + Assert.IsTrue(retValue == false && referenceInfo == null, "not in project ref should not be found"); + + // Now, try to resolve a non-existent ComAssemblyReference. + string path; + IComReferenceResolver resolver = (IComReferenceResolver)rcr; + Assert.IsFalse(resolver.ResolveComAssemblyReference("MyAssembly", out path)); + Assert.AreEqual(null, path); + } + + /// + /// ResolveComReference automatically adds missing tlbimp references for aximp references. + /// This test verifies we actually create the missing references. + /// + [TestMethod] + public void CheckAddMissingTlbReference() + { + TYPELIBATTR axAttr, tlbAttr, piaAttr, notInProjectAttr; + ComReferenceInfo axRefInfo, tlbRefInfo, piaRefInfo; + + CreateTestReferences(out axRefInfo, out tlbRefInfo, out piaRefInfo, + out axAttr, out tlbAttr, out piaAttr, out notInProjectAttr); + + ResolveComReference rcr = new ResolveComReference(); + rcr.BuildEngine = new MockEngine(); + + // populate the ResolveComReference's list of project references + rcr.allProjectRefs = new List(); + rcr.allProjectRefs.Add(axRefInfo); + rcr.allProjectRefs.Add(tlbRefInfo); + rcr.allProjectRefs.Add(piaRefInfo); + + rcr.AddMissingTlbReferences(); + + Assert.IsTrue(rcr.allProjectRefs.Count == 4, "There should be four references now"); + + ComReferenceInfo newTlbInfo = (ComReferenceInfo)rcr.allProjectRefs[3]; + Assert.IsTrue(axRefInfo.primaryOfAxImpRef == newTlbInfo, "axRefInfo should hold back reference to tlbRefInfo"); + Assert.IsTrue(ComReference.AreTypeLibAttrEqual(newTlbInfo.attr, axRefInfo.attr), "The added reference should have the same attributes as the Ax reference"); + Assert.IsTrue(newTlbInfo.typeLibName == axRefInfo.typeLibName, "The added reference should have the same type lib name as the Ax reference"); + Assert.IsTrue(newTlbInfo.strippedTypeLibPath == axRefInfo.strippedTypeLibPath, "The added reference should have the same type lib path as the Ax reference"); + + Assert.IsTrue(newTlbInfo.taskItem.ItemSpec == axRefInfo.taskItem.ItemSpec, "The added reference should have the same task item spec as the Ax reference"); + Assert.IsTrue(newTlbInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.wrapperTool) == ComReferenceTypes.primaryortlbimp, "The added reference should have the tlbimp/primary wrapper tool"); + + rcr.AddMissingTlbReferences(); + Assert.IsTrue(rcr.allProjectRefs.Count == 4, "There should still be four references"); + } + + [TestMethod] + public void BothKeyFileAndKeyContainer() + { + ResolveComReference rcr = new ResolveComReference(); + MockEngine e = new MockEngine(); + rcr.BuildEngine = e; + + rcr.KeyFile = "foo"; + rcr.KeyContainer = "bar"; + + Assert.IsTrue(!rcr.Execute()); + + e.AssertLogContains("MSB3300"); + } + + [TestMethod] + public void DelaySignWithoutEitherKeyFileOrKeyContainer() + { + ResolveComReference rcr = new ResolveComReference(); + MockEngine e = new MockEngine(); + rcr.BuildEngine = e; + + rcr.DelaySign = true; + Assert.IsTrue(!rcr.Execute()); + + e.AssertLogContains("MSB3301"); + } + + /// + /// Test if assemblies located in the gac get their CopyLocal attribute set to False + /// + [TestMethod] + public void CheckSetCopyLocalToFalseOnEmbedInteropTypesAssemblies() + { + string gacPath = @"C:\windows\gac"; + + ResolveComReference rcr = new ResolveComReference(); + rcr.BuildEngine = new MockEngine(); + + // the matrix of TargetFrameworkVersion values we are testing + string[] fxVersions = + { + "v2.0", + "v3.0", + "v3.5", + "v4.0" + }; + + for (int i = 0; i < fxVersions.Length; i++) + { + string fxVersion = fxVersions[i]; + + ArrayList taskItems = new ArrayList(); + + TaskItem nonGacNoPrivate = new TaskItem(@"C:\windows\gar\test1.dll"); + nonGacNoPrivate.SetMetadata(ItemMetadataNames.embedInteropTypes, "true"); + + TaskItem gacNoPrivate = new TaskItem(@"C:\windows\gac\assembly1.dll"); + gacNoPrivate.SetMetadata(ItemMetadataNames.embedInteropTypes, "true"); + + TaskItem nonGacPrivateFalse = new TaskItem(@"C:\windows\gar\test1.dll"); + nonGacPrivateFalse.SetMetadata(ItemMetadataNames.privateMetadata, "false"); + nonGacPrivateFalse.SetMetadata(ItemMetadataNames.embedInteropTypes, "true"); + + TaskItem gacPrivateFalse = new TaskItem(@"C:\windows\gac\assembly1.dll"); + gacPrivateFalse.SetMetadata(ItemMetadataNames.privateMetadata, "false"); + gacPrivateFalse.SetMetadata(ItemMetadataNames.embedInteropTypes, "true"); + + TaskItem nonGacPrivateTrue = new TaskItem(@"C:\windows\gar\test1.dll"); + nonGacPrivateTrue.SetMetadata(ItemMetadataNames.privateMetadata, "true"); + nonGacPrivateTrue.SetMetadata(ItemMetadataNames.embedInteropTypes, "true"); + + TaskItem gacPrivateTrue = new TaskItem(@"C:\windows\gac\assembly1.dll"); + gacPrivateTrue.SetMetadata(ItemMetadataNames.privateMetadata, "true"); + gacPrivateTrue.SetMetadata(ItemMetadataNames.embedInteropTypes, "true"); + + taskItems.Add(nonGacNoPrivate); + taskItems.Add(gacNoPrivate); + + taskItems.Add(nonGacPrivateFalse); + taskItems.Add(gacPrivateFalse); + + taskItems.Add(nonGacPrivateTrue); + taskItems.Add(gacPrivateTrue); + + rcr.TargetFrameworkVersion = fxVersion; + rcr.SetFrameworkVersionFromString(rcr.TargetFrameworkVersion); + + rcr.SetCopyLocalToFalseOnGacOrNoPIAAssemblies(taskItems, gacPath); + + bool enabledNoPIA = false; + switch (fxVersion) + { + case "v4.0": + enabledNoPIA = true; + break; + default: + break; + } + + // if Private is missing, by default GAC items are CopyLocal=false, non GAC CopyLocal=true + Assert.IsTrue + ( + nonGacNoPrivate.GetMetadata(ItemMetadataNames.copyLocal) == (enabledNoPIA ? "false" : "true"), + fxVersion + ": Non Gac assembly, missing Private" + ); + + Assert.IsTrue + ( + gacNoPrivate.GetMetadata(ItemMetadataNames.copyLocal) == (enabledNoPIA ? "false" : "false"), + fxVersion + ": Gac assembly, missing Private" + ); + + // if Private is set, it takes precedence + Assert.IsTrue + ( + nonGacPrivateFalse.GetMetadata(ItemMetadataNames.copyLocal) == (enabledNoPIA ? "false" : "false"), + fxVersion + ": Non Gac assembly, Private false" + ); + + Assert.IsTrue + ( + gacPrivateFalse.GetMetadata(ItemMetadataNames.copyLocal) == (enabledNoPIA ? "false" : "false"), + fxVersion + ": Gac assembly, Private false" + ); + + Assert.IsTrue + ( + nonGacPrivateTrue.GetMetadata(ItemMetadataNames.copyLocal) == (enabledNoPIA ? "false" : "true"), + fxVersion + ": Non Gac assembly, Private true, should be TRUE" + ); + + Assert.IsTrue + ( + gacPrivateTrue.GetMetadata(ItemMetadataNames.copyLocal) == (enabledNoPIA ? "false" : "true"), + fxVersion + ": Gac assembly, Private true, should be TRUE" + ); + } + } + + /// + /// Test if assemblies located in the gac get their CopyLocal attribute set to False + /// + [TestMethod] + public void CheckSetCopyLocalToFalseOnGacAssemblies() + { + string gacPath = @"C:\windows\gac"; + + ResolveComReference rcr = new ResolveComReference(); + rcr.BuildEngine = new MockEngine(); + + ArrayList taskItems = new ArrayList(); + TaskItem nonGacNoPrivate = new TaskItem(@"C:\windows\gar\test1.dll"); + TaskItem gacNoPrivate = new TaskItem(@"C:\windows\gac\assembly1.dll"); + + TaskItem nonGacPrivateFalse = new TaskItem(@"C:\windows\gar\test1.dll"); + nonGacPrivateFalse.SetMetadata(ItemMetadataNames.privateMetadata, "false"); + TaskItem gacPrivateFalse = new TaskItem(@"C:\windows\gac\assembly1.dll"); + gacPrivateFalse.SetMetadata(ItemMetadataNames.privateMetadata, "false"); + + TaskItem nonGacPrivateTrue = new TaskItem(@"C:\windows\gar\test1.dll"); + nonGacPrivateTrue.SetMetadata(ItemMetadataNames.privateMetadata, "true"); + TaskItem gacPrivateTrue = new TaskItem(@"C:\windows\gac\assembly1.dll"); + gacPrivateTrue.SetMetadata(ItemMetadataNames.privateMetadata, "true"); + + taskItems.Add(nonGacNoPrivate); + taskItems.Add(gacNoPrivate); + + taskItems.Add(nonGacPrivateFalse); + taskItems.Add(gacPrivateFalse); + + taskItems.Add(nonGacPrivateTrue); + taskItems.Add(gacPrivateTrue); + + rcr.SetCopyLocalToFalseOnGacOrNoPIAAssemblies(taskItems, gacPath); + + // if Private is missing, by default GAC items are CopyLocal=false, non GAC CopyLocal=true + Assert.IsTrue(nonGacNoPrivate.GetMetadata(ItemMetadataNames.copyLocal) == "true", "Non Gac assembly, missing Private, should be TRUE"); + + Assert.IsTrue(gacNoPrivate.GetMetadata(ItemMetadataNames.copyLocal) == "false", "Gac assembly, missing Private, should be FALSE"); + + // if Private is set, it takes precedence + Assert.IsTrue(nonGacPrivateFalse.GetMetadata(ItemMetadataNames.copyLocal) == "false", "Non Gac assembly, Private false, should be FALSE"); + + Assert.IsTrue(gacPrivateFalse.GetMetadata(ItemMetadataNames.copyLocal) == "false", "Gac assembly, Private false, should be FALSE"); + + Assert.IsTrue(nonGacPrivateTrue.GetMetadata(ItemMetadataNames.copyLocal) == "true", "Non Gac assembly, Private true, should be TRUE"); + + Assert.IsTrue(gacPrivateTrue.GetMetadata(ItemMetadataNames.copyLocal) == "true", "Gac assembly, Private true, should be TRUE"); + } + + /// + /// Make sure the conflicting references are detected correctly + /// + [TestMethod] + public void TestCheckForConflictingReferences() + { + TYPELIBATTR axAttr, tlbAttr, piaAttr, notInProjectAttr; + ComReferenceInfo axRefInfo, tlbRefInfo, piaRefInfo; + + CreateTestReferences(out axRefInfo, out tlbRefInfo, out piaRefInfo, + out axAttr, out tlbAttr, out piaAttr, out notInProjectAttr); + + ResolveComReference rcr = new ResolveComReference(); + rcr.BuildEngine = new MockEngine(); + + // populate the ResolveComReference's list of project references + rcr.allProjectRefs = new List(); + rcr.allProjectRefs.Add(axRefInfo); + rcr.allProjectRefs.Add(tlbRefInfo); + rcr.allProjectRefs.Add(piaRefInfo); + + // no conflicts should be found with just the three initial refs + Assert.IsTrue(rcr.CheckForConflictingReferences()); + Assert.AreEqual(3, rcr.allProjectRefs.Count); + + ComReferenceInfo referenceInfo; + + // duplicate refs should not be treated as conflicts + referenceInfo = new ComReferenceInfo(tlbRefInfo); + rcr.allProjectRefs.Add(referenceInfo); + referenceInfo = new ComReferenceInfo(axRefInfo); + rcr.allProjectRefs.Add(referenceInfo); + referenceInfo = new ComReferenceInfo(piaRefInfo); + rcr.allProjectRefs.Add(referenceInfo); + + Assert.IsTrue(rcr.CheckForConflictingReferences()); + Assert.AreEqual(6, rcr.allProjectRefs.Count); + + // tlb and ax refs with same lib name but different attributes should be considered conflicting + // We don't care about typelib name conflicts for PIA refs, because we don't have to create wrappers for them + ComReferenceInfo conflictTlb = new ComReferenceInfo(tlbRefInfo); + conflictTlb.attr = notInProjectAttr; + rcr.allProjectRefs.Add(conflictTlb); + ComReferenceInfo conflictAx = new ComReferenceInfo(axRefInfo); + conflictAx.attr = notInProjectAttr; + rcr.allProjectRefs.Add(conflictAx); + ComReferenceInfo piaRef = new ComReferenceInfo(piaRefInfo); + piaRef.attr = notInProjectAttr; + rcr.allProjectRefs.Add(piaRef); + + Assert.IsTrue(!rcr.CheckForConflictingReferences()); + + // ... and conflicting references should have been removed + Assert.AreEqual(7, rcr.allProjectRefs.Count); + Assert.IsTrue(!rcr.allProjectRefs.Contains(conflictTlb)); + Assert.IsTrue(!rcr.allProjectRefs.Contains(conflictAx)); + Assert.IsTrue(rcr.allProjectRefs.Contains(piaRef)); + } + + /// + /// In order to make ResolveComReferences multitargetable, two properties, ExecuteAsTool + /// and SdkToolsPath were added. In order to have correct behavior when using pre-4.0 + /// toolsversions, ExecuteAsTool must default to true, and the paths to the tools will be the + /// v3.5 path. It is difficult to verify the tool paths in a unit test, however, so + /// this was done by ad hoc testing and will be maintained by the dev suites. + /// + [TestMethod] + public void MultiTargetingDefaultSetCorrectly() + { + ResolveComReference t = new ResolveComReference(); + + Assert.IsTrue(t.ExecuteAsTool, "ExecuteAsTool should default to true"); + } + + /// + /// When calling AxImp.exe directly, the runtime-callable wrapper needs to be + /// passed via the /rcw switch, so RCR needs to make sure that the ax reference knows about + /// its corresponding TLB wrapper. + /// + [TestMethod] + public void AxReferenceKnowsItsRCWCreateTlb() + { + CheckAxReferenceRCWTlbExists(RcwStyle.GenerateTlb /* have RCR create the TLB reference */, false /* don't include TLB version in the interop name */); + } + + /// + /// When calling AxImp.exe directly, the runtime-callable wrapper needs to be + /// passed via the /rcw switch, so RCR needs to make sure that the ax reference knows about + /// its corresponding TLB wrapper. + /// + [TestMethod] + public void AxReferenceKnowsItsRCWCreateTlb_IncludeVersion() + { + CheckAxReferenceRCWTlbExists(RcwStyle.GenerateTlb /* have RCR create the TLB reference */, true /* include TLB version in the interop name */); + } + + /// + /// When calling AxImp.exe directly, the runtime-callable wrapper needs to be + /// passed via the /rcw switch, so RCR needs to make sure that the ax reference knows about + /// its corresponding TLB wrapper. + /// + [TestMethod] + public void AxReferenceKnowsItsRCWTlbExists() + { + CheckAxReferenceRCWTlbExists(RcwStyle.PreexistingTlb /* pass in the TLB reference */, false /* don't include TLB version in the interop name */); + } + + /// + /// When calling AxImp.exe directly, the runtime-callable wrapper needs to be + /// passed via the /rcw switch, so RCR needs to make sure that the ax reference knows about + /// its corresponding TLB wrapper. + /// + /// Tests that still works when IncludeVersionInInteropName = true + /// + [TestMethod] + public void AxReferenceKnowsItsRCWTlbExists_IncludeVersion() + { + CheckAxReferenceRCWTlbExists(RcwStyle.PreexistingTlb /* pass in the TLB reference */, true /* include TLB version in the interop name */); + } + + /// + /// When calling AxImp.exe directly, the runtime-callable wrapper needs to be + /// passed via the /rcw switch, so RCR needs to make sure that the ax reference knows about + /// its corresponding TLB wrapper. + /// + [TestMethod] + public void AxReferenceKnowsItsRCWPiaExists() + { + CheckAxReferenceRCWTlbExists(RcwStyle.PreexistingPia /* pass in the TLB reference */, false /* don't include version in the interop name */); + } + + /// + /// When calling AxImp.exe directly, the runtime-callable wrapper needs to be + /// passed via the /rcw switch, so RCR needs to make sure that the ax reference knows about + /// its corresponding TLB wrapper. + /// + /// Tests that still works when IncludeVersionInInteropName = true + /// + [TestMethod] + public void AxReferenceKnowsItsRCWPiaExists_IncludeVersion() + { + CheckAxReferenceRCWTlbExists(RcwStyle.PreexistingPia /* pass in the PIA reference */, true /* include version in the interop name */); + } + + private enum RcwStyle { GenerateTlb, PreexistingTlb, PreexistingPia }; + + /// + /// Helper method that will new up an AX and matching TLB reference, and verify that the AX reference + /// sets its RCW appropriately. + /// + private void CheckAxReferenceRCWTlbExists(RcwStyle rcwStyle, bool includeVersionInInteropName) + { + Guid axGuid = Guid.NewGuid(); + ComReferenceInfo tlbRefInfo; + + ResolveComReference rcr = new ResolveComReference(); + rcr.BuildEngine = new MockEngine(); + rcr.IncludeVersionInInteropName = includeVersionInInteropName; + + rcr.allProjectRefs = new List(); + + TaskItem axTaskItem = CreateComReferenceTaskItem("ref", axGuid.ToString(), "1", "2", "1033", ComReferenceTypes.aximp); + ComReferenceInfo axRefInfo = CreateComReferenceInfo(axTaskItem, "RefLibName", "RefLibPath"); + rcr.allProjectRefs.Add(axRefInfo); + + switch (rcwStyle) + { + case RcwStyle.GenerateTlb: break; + case RcwStyle.PreexistingTlb: + { + TaskItem tlbTaskItem = CreateComReferenceTaskItem("ref", axGuid.ToString(), "1", "2", "1033", ComReferenceTypes.tlbimp, "true"); + tlbRefInfo = CreateComReferenceInfo(tlbTaskItem, "RefLibName", "RefLibPath"); + rcr.allProjectRefs.Add(tlbRefInfo); + break; + } + case RcwStyle.PreexistingPia: + { + TaskItem tlbTaskItem = CreateComReferenceTaskItem("ref", axGuid.ToString(), "1", "2", "1033", ComReferenceTypes.primary, "true"); + tlbRefInfo = CreateComReferenceInfo(tlbTaskItem, "RefLibName", "RefLibPath"); + rcr.allProjectRefs.Add(tlbRefInfo); + break; + } + } + + rcr.AddMissingTlbReferences(); + + Assert.AreEqual(2, rcr.allProjectRefs.Count, "Should be two references"); + + tlbRefInfo = (ComReferenceInfo)rcr.allProjectRefs[1]; + var embedInteropTypes = tlbRefInfo.taskItem.GetMetadata(ItemMetadataNames.embedInteropTypes); + Assert.AreEqual("false", embedInteropTypes, "The tlb wrapper for the activex control should have EmbedInteropTypes=false not " + embedInteropTypes); + Assert.IsTrue(ComReference.AreTypeLibAttrEqual(tlbRefInfo.attr, axRefInfo.attr), "reference information should be the same"); + Assert.AreEqual + ( + TlbReference.GetWrapperFileName + ( + axRefInfo.taskItem.GetMetadata(ComReferenceItemMetadataNames.tlbReferenceName), + includeVersionInInteropName, + axRefInfo.attr.wMajorVerNum, + axRefInfo.attr.wMinorVerNum + ), + TlbReference.GetWrapperFileName + ( + tlbRefInfo.typeLibName, + includeVersionInInteropName, + tlbRefInfo.attr.wMajorVerNum, + tlbRefInfo.attr.wMinorVerNum + ), + "Expected Ax reference's RCW name to match the new TLB" + ); + } + } +} + diff --git a/src/XMakeTasks/UnitTests/ResolveNonMSBuildProjectOutput_Tests.cs b/src/XMakeTasks/UnitTests/ResolveNonMSBuildProjectOutput_Tests.cs new file mode 100644 index 00000000000..feb0b3fb679 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResolveNonMSBuildProjectOutput_Tests.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Text; +using System.Linq; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class ResolveNonMSBuildProjectOutput_Tests + { + private const string attributeProject = "Project"; + + static internal ITaskItem CreateReferenceItem(string itemSpec, string projectGuid, string package, string name) + { + TaskItem reference = new TaskItem(itemSpec); + + if (projectGuid != null) + reference.SetMetadata(attributeProject, projectGuid); + if (package != null) + reference.SetMetadata("Package", package); + if (name != null) + reference.SetMetadata("Name", name); + + return reference; + } + + private void TestVerifyReferenceAttributesHelper(string itemSpec, string projectGuid, string package, string name, + bool expectedResult, string expectedMissingAttribute) + { + ITaskItem reference = CreateReferenceItem(itemSpec, projectGuid, package, name); + + ResolveNonMSBuildProjectOutput rvpo = new ResolveNonMSBuildProjectOutput(); + string missingAttr = null; + bool result = rvpo.VerifyReferenceAttributes(reference, out missingAttr); + + string message = string.Format("Reference \"{0}\" [project \"{1}\", package \"{2}\", name \"{3}\"], " + + "expected result \"{4}\", actual result \"{5}\", expected missing attr \"{6}\", actual missing attr \"{7}\".", + itemSpec, projectGuid, package, name, expectedResult, result, + expectedMissingAttribute, missingAttr); + + Assert.IsTrue(result == expectedResult, message); + if (result == false) + { + Assert.IsTrue(missingAttr == expectedMissingAttribute, message); + } + else + { + Assert.IsNull(missingAttr, message); + } + } + + [TestMethod] + public void TestVerifyReferenceAttributes() + { + // a correct reference + TestVerifyReferenceAttributesHelper("proj1.csproj", "{CFF438C3-51D1-4E61-BECD-D7D3A6193DF7}", "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", "CSDep", + true, null); + + // incorrect project guid - should not work + TestVerifyReferenceAttributesHelper("proj1.csproj", "{invalid guid}", "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", "CSDep", + false, "Project"); + } + + static internal string CreatePregeneratedPathDoc(IDictionary projectOutputs) + { + string xmlString = ""; + + foreach (DictionaryEntry entry in projectOutputs) + { + xmlString += string.Format("{1}", entry.Key, entry.Value); + } + + xmlString += ""; + + return xmlString; + } + + private void TestResolveHelper(string itemSpec, string projectGuid, string package, string name, + Hashtable pregenOutputs, bool expectedResult, string expectedPath) + { + ITaskItem reference = CreateReferenceItem(itemSpec, projectGuid, package, name); + string xmlString = CreatePregeneratedPathDoc(pregenOutputs); + ITaskItem resolvedPath; + + ResolveNonMSBuildProjectOutput rvpo = new ResolveNonMSBuildProjectOutput(); + rvpo.CacheProjectElementsFromXml(xmlString); + bool result = rvpo.ResolveProject(reference, out resolvedPath); + + string message = string.Format("Reference \"{0}\" [project \"{1}\", package \"{2}\", name \"{3}\"] Pregen Xml string : \"{4}\"" + + "expected result \"{5}\", actual result \"{6}\", expected path \"{7}\", actual path \"{8}\".", + itemSpec, projectGuid, package, name, xmlString, expectedResult, result, expectedPath, resolvedPath); + + Assert.IsTrue(result == expectedResult, message); + if (result == true) + { + Assert.IsTrue(resolvedPath.ItemSpec == expectedPath, message); + } + else + { + Assert.IsNull(resolvedPath, message); + } + } + + [TestMethod] + public void TestResolve() + { + // empty pre-generated string + Hashtable projectOutputs = new Hashtable(); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, false, null); + + // non matching project in string + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, false, null); + + // matching project in string + projectOutputs = new Hashtable(); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"obj\correct.dll"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, true, @"obj\correct.dll"); + + // multiple non matching projects in string + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, false, null); + + // multiple non matching projects in string, one matching + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"obj\correct.dll"); + TestResolveHelper("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1", + projectOutputs, true, @"obj\correct.dll"); + } + + private void TestUnresolvedReferencesHelper(ArrayList projectRefs, Hashtable pregenOutputs, + out Hashtable unresolvedOutputs, out Hashtable resolvedOutputs) + { + TestUnresolvedReferencesHelper(projectRefs, pregenOutputs, null, out unresolvedOutputs, out resolvedOutputs); + } + + private void TestUnresolvedReferencesHelper(ArrayList projectRefs, Hashtable pregenOutputs, Func isManaged, + out Hashtable unresolvedOutputs, out Hashtable resolvedOutputs) + { + ResolveNonMSBuildProjectOutput.GetAssemblyNameDelegate pretendGetAssemblyName = path => + { + if (isManaged != null && isManaged(path)) + { + return null; // just don't throw an exception + } + else + { + throw new BadImageFormatException(); // the hint that the caller takes for an unmanaged binary. + } + }; + + string xmlString = CreatePregeneratedPathDoc(pregenOutputs); + + MockEngine engine = new MockEngine(); + ResolveNonMSBuildProjectOutput rvpo = new ResolveNonMSBuildProjectOutput(); + rvpo.GetAssemblyName = pretendGetAssemblyName; + rvpo.BuildEngine = engine; + rvpo.PreresolvedProjectOutputs = xmlString; + rvpo.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + + bool result = rvpo.Execute(); + unresolvedOutputs = new Hashtable(); + + for (int i = 0; i < rvpo.UnresolvedProjectReferences.Length; i++) + { + unresolvedOutputs[rvpo.UnresolvedProjectReferences[i].ItemSpec] = rvpo.UnresolvedProjectReferences[i]; + } + + resolvedOutputs = new Hashtable(); + for (int i = 0; i < rvpo.ResolvedOutputPaths.Length; i++) + { + resolvedOutputs[rvpo.ResolvedOutputPaths[i].ItemSpec] = rvpo.ResolvedOutputPaths[i]; + } + } + + [TestMethod] + public void TestManagedCheck() + { + Hashtable unresolvedOutputs = null; + Hashtable resolvedOutputs = null; + Hashtable projectOutputs = null; + ArrayList projectRefs = null; + + projectRefs = new ArrayList(); + projectRefs.Add(CreateReferenceItem("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-000000000000}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1")); + projectRefs.Add(CreateReferenceItem("MCDep2.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep2")); + + // 1. multiple project refs, none resolvable + projectOutputs = new Hashtable(); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-000000000000}", @"obj\managed.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"obj\unmanaged.dll"); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, path => (path == @"obj\managed.dll"), out unresolvedOutputs, out resolvedOutputs); + + Assert.IsNotNull(resolvedOutputs); + Assert.IsTrue(resolvedOutputs.Contains(@"obj\managed.dll")); + Assert.IsTrue(resolvedOutputs.Contains(@"obj\unmanaged.dll")); + Assert.AreEqual(((ITaskItem)resolvedOutputs[@"obj\managed.dll"]).GetMetadata("ManagedAssembly"), "true"); + Assert.AreNotEqual(((ITaskItem)resolvedOutputs[@"obj\unmanaged.dll"]).GetMetadata("ManagedAssembly"), "true"); + } + + /// + /// Verifies that the UnresolvedProjectReferences output parameter is populated correctly. + /// + [TestMethod] + public void TestUnresolvedReferences() + { + Hashtable unresolvedOutputs = null; + Hashtable resolvedOutputs = null; + Hashtable projectOutputs = null; + ArrayList projectRefs = null; + + projectRefs = new ArrayList(); + projectRefs.Add(CreateReferenceItem("MCDep1.vcproj", "{2F6BBCC3-7111-4116-A68B-000000000000}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep1")); + projectRefs.Add(CreateReferenceItem("MCDep2.vcproj", "{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", "MCDep2")); + + // 1. multiple project refs, none resolvable + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, out unresolvedOutputs, out resolvedOutputs); + + Assert.IsTrue(resolvedOutputs.Count == 0, "No resolved refs expected for case 1"); + Assert.IsTrue(unresolvedOutputs.Count == 2, "Two unresolved refs expected for case 1"); + Assert.IsTrue(unresolvedOutputs["MCDep1.vcproj"] == projectRefs[0]); + Assert.IsTrue(unresolvedOutputs["MCDep2.vcproj"] == projectRefs[1]); + + // 2. multiple project refs, one resolvable + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"obj\correct.dll"); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, out unresolvedOutputs, out resolvedOutputs); + + Assert.IsTrue(resolvedOutputs.Count == 1, "One resolved ref expected for case 2"); + Assert.IsTrue(resolvedOutputs.ContainsKey(@"obj\correct.dll")); + Assert.IsTrue(unresolvedOutputs.Count == 1, "One unresolved ref expected for case 2"); + Assert.IsTrue(unresolvedOutputs["MCDep1.vcproj"] == projectRefs[0]); + + // 3. multiple project refs, all resolvable + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"obj\correct.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-000000000000}", @"obj\correct2.dll"); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, out unresolvedOutputs, out resolvedOutputs); + + Assert.IsTrue(resolvedOutputs.Count == 2, "Two resolved refs expected for case 3"); + Assert.IsTrue(resolvedOutputs.ContainsKey(@"obj\correct.dll")); + Assert.IsTrue(resolvedOutputs.ContainsKey(@"obj\correct2.dll")); + Assert.IsTrue(unresolvedOutputs.Count == 0, "No unresolved refs expected for case 3"); + + // 4. multiple project refs, all failed to resolve + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @""); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-000000000000}", @""); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, out unresolvedOutputs, out resolvedOutputs); + + Assert.IsTrue(resolvedOutputs.Count == 0, "No resolved refs expected for case 4"); + Assert.IsTrue(unresolvedOutputs.Count == 0, "No unresolved refs expected for case 4"); + + // 5. multiple project refs, one resolvable, one failed to resolve + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-34CFC76F37C5}", @"obj\correct.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-000000000000}", @""); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, out unresolvedOutputs, out resolvedOutputs); + + Assert.IsTrue(resolvedOutputs.Count == 1, "One resolved ref expected for case 5"); + Assert.IsTrue(resolvedOutputs.ContainsKey(@"obj\correct.dll")); + Assert.IsTrue(unresolvedOutputs.Count == 0, "No unresolved refs expected for case 5"); + + // 6. multiple project refs, one unresolvable, one failed to resolve + projectOutputs = new Hashtable(); + projectOutputs.Add("{11111111-1111-1111-1111-111111111111}", @"obj\wrong.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111112}", @"obj\wrong2.dll"); + projectOutputs.Add("{11111111-1111-1111-1111-111111111113}", @"obj\wrong3.dll"); + projectOutputs.Add("{2F6BBCC3-7111-4116-A68B-000000000000}", @""); + + TestUnresolvedReferencesHelper(projectRefs, projectOutputs, out unresolvedOutputs, out resolvedOutputs); + + Assert.IsTrue(resolvedOutputs.Count == 0, "No resolved refs expected for case 6"); + Assert.IsTrue(unresolvedOutputs.Count == 1, "One unresolved ref expected for case 6"); + Assert.IsTrue(unresolvedOutputs["MCDep2.vcproj"] == projectRefs[1]); + } + + [TestMethod] + public void TestVerifyProjectReferenceItem() + { + ResolveNonMSBuildProjectOutput rvpo = new ResolveNonMSBuildProjectOutput(); + + ITaskItem[] taskItems = new ITaskItem[1]; + // bad GUID - this reference is invalid + taskItems[0] = new TaskItem("projectReference"); + taskItems[0].SetMetadata(attributeProject, "{invalid guid}"); + + MockEngine engine = new MockEngine(); + rvpo.BuildEngine = engine; + Assert.AreEqual(true, rvpo.VerifyProjectReferenceItems(taskItems, false /* treat problems as warnings */)); + Assert.AreEqual(1, engine.Warnings); + Assert.AreEqual(0, engine.Errors); + engine.AssertLogContains("MSB3107"); + + engine = new MockEngine(); + rvpo.BuildEngine = engine; + Assert.AreEqual(false, rvpo.VerifyProjectReferenceItems(taskItems, true /* treat problems as errors */)); + Assert.AreEqual(0, engine.Warnings); + Assert.AreEqual(1, engine.Errors); + engine.AssertLogContains("MSB3107"); + } + } +} diff --git a/src/XMakeTasks/UnitTests/ResolveSDKReference_Tests.cs b/src/XMakeTasks/UnitTests/ResolveSDKReference_Tests.cs new file mode 100644 index 00000000000..318ff2d42c3 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResolveSDKReference_Tests.cs @@ -0,0 +1,4324 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// Tests for the task that resolves an SDKReference to a full path on disk +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Reflection; +using System.Globalization; +using System.Resources; +using System.Text.RegularExpressions; +using Microsoft.Win32; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.UnitTests.ResolveAssemblyReference_Tests; +using Microsoft.Build.Shared; +using System.Collections.Generic; +using SDKReference = Microsoft.Build.Tasks.ResolveSDKReference.SDKReference; +using ProcessorArchitecture = Microsoft.Build.Utilities.ProcessorArchitecture; +using Microsoft.Build.Evaluation; +using System.Linq; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.UnitTests.ResolveSDKReference_Tests +{ + [TestClass] + public class ResolveSDKReferenceTestFixture + { + private Microsoft.Build.UnitTests.MockEngine.GetStringDelegate _resourceDelegate = new Microsoft.Build.UnitTests.MockEngine.GetStringDelegate(AssemblyResources.GetString); + #region TestMethods + + /// + /// Make sure that SDK reference which should be good are parsed correctly. + /// + [TestMethod] + public void ParseItemSpecGood() + { + TestGoodSDKReferenceIncludes(new TaskItem("Cat, Version=8.0"), "Cat", "8.0"); + TestGoodSDKReferenceIncludes(new TaskItem("Cat, Version= 8.0"), "Cat", "8.0"); + TestGoodSDKReferenceIncludes(new TaskItem("Cat, Version=8.0 "), "Cat", "8.0"); + TestGoodSDKReferenceIncludes(new TaskItem("Cat, Version=8.0.255"), "Cat", "8.0.255"); + TestGoodSDKReferenceIncludes(new TaskItem(" Cat, Version=8.0.255"), "Cat", "8.0.255"); + TestGoodSDKReferenceIncludes(new TaskItem("Cat , Version=8.0.255"), "Cat", "8.0.255"); + TestGoodSDKReferenceIncludes(new TaskItem("Cat,Version=8.0.255"), "Cat", "8.0.255"); + TestGoodSDKReferenceIncludes(new TaskItem("Cat, Version=8.0.255"), "Cat", "8.0.255"); + } + + /// + /// Make sure ones which are incorrect and log the correct error. + /// + [TestMethod] + public void ParseItemSpecBadNames() + { + //These should all be bad the format must be , Version=. + TestBadSDKReferenceIncludes(new TaskItem("")); + TestBadSDKReferenceIncludes(new TaskItem("Cat, Version=8")); + TestBadSDKReferenceIncludes(new TaskItem("Cat 8.0")); + TestBadSDKReferenceIncludes(new TaskItem("Cat Version=8.0")); + TestBadSDKReferenceIncludes(new TaskItem("Dog, Cat, Version=8.0")); + TestBadSDKReferenceIncludes(new TaskItem("Cat, Version=8.0, Moose")); + TestBadSDKReferenceIncludes(new TaskItem("Cat Version=v8.0")); + TestBadSDKReferenceIncludes(new TaskItem(" , Version=8.0")); + TestBadSDKReferenceIncludes(new TaskItem("Cat, Version=v8.0")); + TestBadSDKReferenceIncludes(new TaskItem("Cat, Version=8.0.344.555.666.777.666.555.444")); + TestBadSDKReferenceIncludes(new TaskItem("Cat,")); + TestBadSDKReferenceIncludes(new TaskItem("Cat, Version=")); + } + + /// + /// Make sure ones which are incorrect and log the correct error. + /// + [TestMethod] + public void ParseDependsOnString() + { + Assert.IsTrue(ResolveSDKReference.ParseDependsOnSDK(null).Count == 0); + Assert.IsTrue(ResolveSDKReference.ParseDependsOnSDK(String.Empty).Count == 0); + Assert.IsTrue(ResolveSDKReference.ParseDependsOnSDK(";;").Count == 0); + Assert.IsTrue(ResolveSDKReference.ParseDependsOnSDK("; ;").Count == 0); + + List parsedDependencies = ResolveSDKReference.ParseDependsOnSDK("; foo ;"); + Assert.IsTrue(parsedDependencies.Count == 1); + Assert.IsTrue(parsedDependencies[0].Equals("foo", StringComparison.OrdinalIgnoreCase)); + + parsedDependencies = ResolveSDKReference.ParseDependsOnSDK(";;;bar, Version=1.0 ; ; ; foo, Version=2.0 ;;;;;;"); + Assert.IsTrue(parsedDependencies.Count == 2); + Assert.IsTrue(parsedDependencies[0].Equals("bar, Version=1.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(parsedDependencies[1].Equals("foo, Version=2.0", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Make sure ones which are incorrect and log the correct error. + /// + [TestMethod] + public void GetUnResolvedDependentSDKs() + { + string[] result = null; + HashSet resolvedSDKsEmpty = new HashSet(); + List dependentSDKsEmpty = new List(); + + HashSet resolvedSDKs = new HashSet() { new SDKReference(new TaskItem(), "bar", "1.0"), new SDKReference(new TaskItem(), "foo", "1.0"), new SDKReference(new TaskItem(), "Newt", "1.0") }; + List dependentSDKs = new List() { "bar, Version=1.0", "bar, Version=2.0", "baz, Version=2.0", "CannotParseMeAsSDK", "newt, version=1.0" }; + + result = ResolveSDKReference.GetUnresolvedDependentSDKs(resolvedSDKsEmpty, dependentSDKsEmpty); + Assert.IsTrue(result.Length == 0); + + result = ResolveSDKReference.GetUnresolvedDependentSDKs(new HashSet(), dependentSDKs); + Assert.IsTrue(result.Length == 4); + Assert.IsTrue(result[0].Equals("\"bar, Version=1.0\"", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(result[1].Equals("\"bar, Version=2.0\"", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(result[2].Equals("\"baz, Version=2.0\"", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(result[3].Equals("\"newt, Version=1.0\"", StringComparison.OrdinalIgnoreCase)); + + result = ResolveSDKReference.GetUnresolvedDependentSDKs(resolvedSDKs, dependentSDKsEmpty); + Assert.IsTrue(result.Length == 0); + + result = ResolveSDKReference.GetUnresolvedDependentSDKs(resolvedSDKs, dependentSDKs); + Assert.IsTrue(result.Length == 2); + Assert.IsTrue(result[0].Equals("\"bar, Version=2.0\"", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(result[1].Equals("\"baz, Version=2.0\"", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void VerifyBuildWarningForESDKWithoutMaxPlatformVersionOnBlueOrAbove() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "TestMaxPlatformVersionWithTargetFrameworkVersion"); + string testDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + + // manifest does not contain MaxPlatformVersion + string sdkManifestContents1 = + @" + + + + "; + + // manifest contains MaxPlatformVersion + string sdkManifestContents2 = + @" + + + + "; + + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + installLocation.SetMetadata("PlatformVersion", "8.0"); + + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + + // Resolve with PlatformVersion 7.0 + MockEngine engine1 = new MockEngine(); + TaskLoggingHelper log1 = new TaskLoggingHelper(engine1, "ResolveSDKReference"); + log1.TaskResources = AssemblyResources.PrimaryResources; + + ResolveSDKReference t1 = new ResolveSDKReference(); + t1.SDKReferences = new ITaskItem[] { item }; + t1.InstalledSDKs = new ITaskItem[] { installLocation }; + t1.WarnOnMissingPlatformVersion = true; + t1.BuildEngine = engine1; + t1.TargetPlatformVersion = "7.0"; + t1.ProjectName = "project.proj"; + t1.TargetPlatformIdentifier = "windows"; + bool succeeded1 = t1.Execute(); + + Assert.IsTrue(succeeded1); + engine1.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionNotSpecified", "project.proj", "GoodTestSDK", "2.0", "windows", "8.0", "windows", t1.TargetPlatformVersion); + + // Resolve with PlatformVersion 8.0 + MockEngine engine2 = new MockEngine(); + TaskLoggingHelper log2 = new TaskLoggingHelper(engine2, "ResolveSDKReference"); + log2.TaskResources = AssemblyResources.PrimaryResources; + + ResolveSDKReference t2 = new ResolveSDKReference(); + t2.SDKReferences = new ITaskItem[] { item }; + t2.InstalledSDKs = new ITaskItem[] { installLocation }; + t2.WarnOnMissingPlatformVersion = true; + t2.BuildEngine = engine2; + t2.TargetPlatformVersion = "8.0"; + t2.ProjectName = "project.proj"; + t2.TargetPlatformIdentifier = "windows"; + bool succeeded2 = t2.Execute(); + + Assert.IsTrue(succeeded2); + engine2.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionNotSpecified", "project.proj", "GoodTestSDK", "2.0", "windows", "8.0", "windows", t2.TargetPlatformVersion); + + // Resolve with PlatformVersion 8.1 + MockEngine engine3 = new MockEngine(); + TaskLoggingHelper log3 = new TaskLoggingHelper(engine3, "ResolveSDKReference"); + log3.TaskResources = AssemblyResources.PrimaryResources; + + ResolveSDKReference t3 = new ResolveSDKReference(); + t3.SDKReferences = new ITaskItem[] { item }; + t3.InstalledSDKs = new ITaskItem[] { installLocation }; + t3.WarnOnMissingPlatformVersion = true; + t3.BuildEngine = engine3; + t3.TargetPlatformVersion = "8.1"; + t3.ProjectName = "project.proj"; + t3.TargetPlatformIdentifier = "windows"; + bool succeeded3 = t3.Execute(); + + Assert.IsTrue(succeeded3); + engine3.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionNotSpecified", "project.proj", "GoodTestSDK", "2.0", "windows", "8.0", "windows", t3.TargetPlatformVersion); + + // Resolve with PlatformVersion 8.1 with WarnOnMissingPlatformVersion = false + MockEngine engine3a = new MockEngine(); + TaskLoggingHelper log3a = new TaskLoggingHelper(engine3a, "ResolveSDKReference"); + log3a.TaskResources = AssemblyResources.PrimaryResources; + + ResolveSDKReference t3a = new ResolveSDKReference(); + t3a.SDKReferences = new ITaskItem[] { item }; + t3a.InstalledSDKs = new ITaskItem[] { installLocation }; + t3a.WarnOnMissingPlatformVersion = false; + t3a.BuildEngine = engine3a; + t3a.TargetPlatformVersion = "8.1"; + t3a.ProjectName = "project.proj"; + t3a.TargetPlatformIdentifier = "windows"; + bool succeeded3a = t3a.Execute(); + + Assert.IsTrue(succeeded3a); + engine3a.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionNotSpecified", "project.proj", "GoodTestSDK", "2.0", "windows", "8.0", "windows", t3a.TargetPlatformVersion); + + FileUtilities.DeleteNoThrow(sdkManifestFile); + // Manifest with MaxPlatformVersion + File.WriteAllText(sdkManifestFile, sdkManifestContents2); + + // Resolve with PlatformVersion 8.0 + MockEngine engine4 = new MockEngine(); + TaskLoggingHelper log4 = new TaskLoggingHelper(engine4, "ResolveSDKReference"); + log4.TaskResources = AssemblyResources.PrimaryResources; + ResolveSDKReference t4 = new ResolveSDKReference(); + t4.SDKReferences = new ITaskItem[] { item }; + t4.InstalledSDKs = new ITaskItem[] { installLocation }; + t4.WarnOnMissingPlatformVersion = true; + t4.BuildEngine = engine4; + t4.TargetPlatformVersion = "8.0"; + t4.ProjectName = "project.proj"; + t4.TargetPlatformIdentifier = "windows"; + bool succeeded4 = t4.Execute(); + + Assert.IsTrue(succeeded4); + engine4.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionNotSpecified", "project.proj", "BadTestSDK", "1.0", "windows", "8.0", "windows", t4.TargetPlatformVersion); + + // Resolve with PlatformVersion 8.1 + MockEngine engine5 = new MockEngine(); + TaskLoggingHelper log5 = new TaskLoggingHelper(engine5, "ResolveSDKReference"); + log5.TaskResources = AssemblyResources.PrimaryResources; + ResolveSDKReference t5 = new ResolveSDKReference(); + t5.SDKReferences = new ITaskItem[] { item }; + t5.InstalledSDKs = new ITaskItem[] { installLocation }; + t5.WarnOnMissingPlatformVersion = true; + t5.BuildEngine = engine5; + t5.ProjectName = "project.proj"; + t5.TargetPlatformVersion = "8.1"; + t5.TargetPlatformIdentifier = "windows"; + bool succeeded5 = t5.Execute(); + + Assert.IsTrue(succeeded5); + engine5.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionNotSpecified", "project.proj", "BadTestSDK", "1.0", "windows", "8.0", "windows", t5.TargetPlatformVersion); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + /// + /// Verify "RuntimeReferenceOnly" equals to "true" is set for specified references + /// + [TestMethod] + public void VerifyAddMetadataToReferences() + { + MockEngine engine = new MockEngine(); + TaskLoggingHelper log = new TaskLoggingHelper(engine, "ResolveSDKReference"); + log.TaskResources = AssemblyResources.PrimaryResources; + + HashSet references = new HashSet(); + SDKReference reference1 = new SDKReference(new TaskItem(), "Microsoft.VCLibs", "12.0"); + reference1.ResolvedItem = new TaskItem(); + references.Add(reference1); + + SDKReference reference2 = new SDKReference(new TaskItem(), "Microsoft.VCLibs", "11.0"); + reference2.ResolvedItem = new TaskItem(); + references.Add(reference2); + + SDKReference reference3 = new SDKReference(new TaskItem(), "Foo", "11.0"); + reference3.ResolvedItem = new TaskItem(); + references.Add(reference3); + + // Dictionary with runtime-only dependencies + Dictionary dict = new Dictionary(); + dict.Add("Microsoft.VCLibs", "11.0"); + + ResolveSDKReference.AddMetadataToReferences(log, references, dict, "RuntimeReferenceOnly", "true"); + + foreach (SDKReference reference in references) + { + if (reference.SimpleName.Equals("Microsoft.VCLibs") && reference.Version.Equals("11.0")) + { + Assert.IsTrue(reference.ResolvedItem.GetMetadata("RuntimeReferenceOnly").Equals("true")); + } + else + { + Assert.IsFalse(reference.ResolvedItem.MetadataNames.ToString().Contains("RuntimeReferenceOnly")); + } + } + } + + /// + /// Make sure ones which are incorrect and log the correct warning. + /// + [TestMethod] + public void VerifyUnResolvedSDKMessage() + { + MockEngine engine = new MockEngine(); + TaskLoggingHelper log = new TaskLoggingHelper(engine, "ResolveSDKReference"); + + HashSet references = new HashSet(); + + // All of the dependencies resolve correctly no warnings are expected + SDKReference reference1 = new SDKReference(new TaskItem(), "reference1", "1.0"); + references.Add(reference1); + + SDKReference reference2 = new SDKReference(new TaskItem(), "reference2", "1.0"); + reference2.DependsOnSDK = "reference1, Version=1.0"; + references.Add(reference2); + + SDKReference reference3 = new SDKReference(new TaskItem(), "reference3", "1.0"); + reference3.DependsOnSDK = "reference1, Version=1.0;reference2, Version=1.0"; + references.Add(reference3); + + SDKReference reference4 = new SDKReference(new TaskItem(), "reference4", "1.0"); + reference4.DependsOnSDK = "reference1, Version=1.0"; + references.Add(reference4); + + SDKReference reference5 = new SDKReference(new TaskItem(), "reference5", "1.0"); + reference5.DependsOnSDK = "reference1, Version=1.0"; + references.Add(reference5); + + ResolveSDKReference.VerifySDKDependsOn(log, references); //, new Version(8, 1), "Windows", null); + Assert.IsTrue(engine.Warnings == 0); + Assert.IsTrue(engine.Errors == 0); + Assert.IsTrue(engine.Log.Length == 0); + + engine = new MockEngine(); + log = new TaskLoggingHelper(engine, "ResolveSDKReference"); + log.TaskResources = AssemblyResources.PrimaryResources; + + references = new HashSet(); + + reference1 = new SDKReference(new TaskItem(), "reference1", "1.0"); + reference1.DependsOnSDK = "NotThere, Version=1.0"; + references.Add(reference1); + + reference2 = new SDKReference(new TaskItem(), "reference2", "1.0"); + reference2.DependsOnSDK = "reference11, Version=1.0;reference2, Version=1.0;reference77, Version=1.0"; + references.Add(reference2); + + reference3 = new SDKReference(new TaskItem(), "reference3", "1.0"); + reference3.DependsOnSDK = "reference1, Version=1.0;NotThere, Version=1.0;WhereAmI, Version=1.0"; + references.Add(reference3); + + reference4 = new SDKReference(new TaskItem(), "reference4", "1.0"); + reference4.DependsOnSDK = "NotThere, Version=1.0"; + references.Add(reference4); + + ResolveSDKReference.VerifySDKDependsOn(log, references);//, new Version(8, 1), "Windows", null); + Assert.IsTrue(engine.Warnings == 4); + Assert.IsTrue(engine.Errors == 0); + + string warning = ResourceUtilities.FormatResourceString("ResolveSDKReference.SDKMissingDependency", reference1.SDKName, "\"NotThere, Version=1.0\""); + engine.AssertLogContains(warning); + + warning = ResourceUtilities.FormatResourceString("ResolveSDKReference.SDKMissingDependency", reference2.SDKName, "\"reference11, Version=1.0\", \"reference77, Version=1.0\""); + engine.AssertLogContains(warning); + + warning = ResourceUtilities.FormatResourceString("ResolveSDKReference.SDKMissingDependency", reference3.SDKName, "\"NotThere, Version=1.0\", \"WhereAmI, Version=1.0\""); + engine.AssertLogContains(warning); + + warning = ResourceUtilities.FormatResourceString("ResolveSDKReference.SDKMissingDependency", reference4.SDKName, "\"NotThere, Version=1.0\""); + engine.AssertLogContains(warning); + } + + /// + /// Verify if the DependsOn metadata is set on the reference item and that dependency is not resolved then cause the warning to happen. + /// + [TestMethod] + public void VerifyDependencyWarningFromMetadata() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("DependsOn", "NotHere, Version=1.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.References = null; + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + + string warning = ResourceUtilities.FormatResourceString("ResolveSDKReference.SDKMissingDependency", "GoodTestSDK, Version=2.0", "\"NotHere, Version=1.0\""); + engine.AssertLogContains(warning); + } + + /// + /// Verify we get the correct dependson warning + /// + [TestMethod] + public void VerifyDependsOnWarningFromManifest() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "VerifyDependsOnWarningFromManifest"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + string warning = ResourceUtilities.FormatResourceString("ResolveSDKReference.SDKMissingDependency", "GoodTestSDK, Version=2.0", "\"Foo, Version=1.0\", \"bar, Version=2.0\""); + engine.AssertLogContains(warning); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Make sure the equals works on the SDKReference. + /// + [TestMethod] + public void TestSDkReferenceEquals() + { + ITaskItem dummyItem = new TaskItem(); + + SDKReference sdkReference1 = new SDKReference(dummyItem, "Reference1", "8.0"); + SDKReference shouldBeEqualToOne = new SDKReference(dummyItem, "Reference1", "8.0"); + SDKReference sdkReference2 = new SDKReference(dummyItem, "Reference2", "8.0"); + SDKReference sdkReference2DiffVer = new SDKReference(dummyItem, "Reference2", "7.0"); + + Assert.IsTrue(sdkReference1.Equals(sdkReference1)); + Assert.IsTrue(sdkReference1.Equals(shouldBeEqualToOne)); + Assert.IsTrue(!sdkReference1.Equals(sdkReference2)); + Assert.IsTrue(!sdkReference1.Equals(sdkReference2DiffVer)); + Assert.IsTrue(!sdkReference2.Equals(sdkReference2DiffVer)); + } + + private static void TestGoodSDKReferenceIncludes(ITaskItem referenceInclude, string simpleName, string version) + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + t.BuildEngine = engine; + + SDKReference reference = t.ParseSDKReference(referenceInclude); + Assert.IsNotNull(reference); + Assert.IsTrue(reference.SimpleName.Equals(simpleName, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(reference.Version.Equals(version, StringComparison.OrdinalIgnoreCase)); + } + + private static void TestBadSDKReferenceIncludes(ITaskItem referenceInclude) + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + t.BuildEngine = engine; + + Assert.IsNull(t.ParseSDKReference(referenceInclude)); + string errorMessage = t.Log.FormatResourceString("ResolveSDKReference.SDKReferenceIncorrectFormat", referenceInclude.ItemSpec); + engine.AssertLogContains(errorMessage); + } + + + /// + /// Project: Prefer32bit true Manifest:SupportPrefer32Bit:true Target:msil Expect: No error + /// + [TestMethod] + public void Prefer32bit1() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit1"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit true Manifest:SupportPrefer32Bit:false Target:AnyCPU Expect: error + /// + [TestMethod] + public void Prefer32bit2() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit2"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "Any CPU"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + string errorMessage = t.Log.FormatResourceString("ResolveSDKReference.Prefer32BitNotSupportedWithNeutralProject", item.ItemSpec); + engine.AssertLogContains(errorMessage); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + + /// + /// Project: Prefer32bit true Manifest:SupportPrefer32Bit:false Target:x86 Expect: No error + /// + [TestMethod] + public void Prefer32bit3() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit3"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "x86"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit false Manifest:SupportPrefer32Bit:false Target:msil Expect: No error + /// + [TestMethod] + public void Prefer32bit4() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit4"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.Prefer32Bit = false; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit false Manifest:SupportPrefer32Bit:false Target:x86 Expect: No error + /// + [TestMethod] + public void Prefer32bit5() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit5"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "x86"; + t.Prefer32Bit = false; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit true Manifest:SupportPrefer32Bit:FOO Target:msil Expect: error + /// + [TestMethod] + public void Prefer32bit6() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit6"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + Assert.IsTrue(engine.Errors == 1); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); ; + string errorMessage = t.Log.FormatResourceString("ResolveSDKReference.Prefer32BitNotSupportedWithNeutralProject", item.ItemSpec); + engine.AssertLogContains(errorMessage); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit true Manifest:SupportPrefer32Bit:empty Target:msil Expect: No error + /// + [TestMethod] + public void Prefer32bit7() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit7"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit true Manifest:SupportPrefer32Bit:missing Target:msil Expect: No Error + /// + [TestMethod] + public void Prefer32bit8() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit8"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Project: Prefer32bit false Manifest:SupportPrefer32Bit:true Target:msil Expect: No Error + /// + [TestMethod] + public void Prefer32bit9() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "Prefer32bit9"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.Prefer32Bit = true; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Warnings == 0, "Expected no warnings"); + Assert.IsTrue(engine.Errors == 0, "Expected no errors"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Resolve from an SDK which exists and is not a framework SDK. This means there is no frameworkIdentity or APPXLocation. + /// Also since no configuration or architecture were passed in we expect the defaults. + /// + [TestMethod] + public void ResolveFromNonFrameworkNoManifest() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.References = null; + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("PlatformIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Resolve from an SDK which exists and is not a framework SDK. This means there is no frameworkIdentity or APPXLocation. + /// Also since no configuration or architecture were passed in we expect the defaults. + /// + [TestMethod] + public void ResolveFromNonFrameworkPassInConfigAndArch() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.References = null; + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + t.TargetedSDKConfiguration = "Release"; + t.TargetedSDKArchitecture = "msil"; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + + // Expect retail if release is passed in + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Resolve from an SDK which exists and is not a framework SDK. This means there is no frameworkIdentity or APPXLocation. + /// Also since no configuration or architecture were passed in we expect the defaults. + /// + [TestMethod] + public void ResolveFromNonFrameworkPassInConfigAndArchOverrideByMetadata() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("TargetedSDKConfiguration", "Release"); + item.SetMetadata("TargetedSDKArchitecture", "AnyCPU"); + + t.SDKReferences = new ITaskItem[] { item }; + t.References = null; + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + t.TargetedSDKConfiguration = "Debug"; + t.TargetedSDKConfiguration = "x86"; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + + // Expect retail if release is passed in + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + + + /// + /// When duplicate references are passed in we only want the first one. + /// + [TestMethod] + public void DuplicateSDKReferences() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item, item2 }; + t.References = new TaskItem[0]; + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Verify that if refernces have SDKName metadata on them that matches a resolved SDK then that SDK should + /// not have its reference assemblies expanded. + /// + [TestMethod] + public void DoNotExpandSDKsWhichAreAlsoTargetedByReferences() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + TaskItem referenceItem = new TaskItem("RandomWinMD"); + referenceItem.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.References = new TaskItem[] { referenceItem }; + + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SimpleName").Equals("GoodTestSDK", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("Version").Equals("2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + + // Make sure that if the SDKName does not match the sdk being resolved then it should have no effect. + // Create the engine. + engine = new MockEngine(); + + t = new ResolveSDKReference(); + item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + referenceItem = new TaskItem("RandomWinMD"); + referenceItem.SetMetadata("SDKName", "DifferentSDK, Version=2.0"); + t.References = new TaskItem[] { referenceItem }; + + installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// When InstalledSDK is empty we should log a message and succeed. + /// + [TestMethod] + public void InstalledSDKEmpty() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.References = null; + t.InstalledSDKs = new ITaskItem[0]; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoSDKLocationsSpecified"); + } + + /// + /// Lets have a mix of install sdk items, some are good, some are bad (missing item spec) others are bad (missing SDKName) + /// + [TestMethod] + public void MixOfInstalledSDKItemsGoodDuplicateAndBad() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.References = new TaskItem[0]; + + ITaskItem installedSDK1 = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK1.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + + ITaskItem installedSDK2 = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK2.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + + ITaskItem installedSDK3 = new TaskItem(String.Empty); + installedSDK3.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + + ITaskItem installedSDK4 = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK4.SetMetadata("SDKName", String.Empty); + + ITaskItem installedSDK5 = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + + t.InstalledSDKs = new ITaskItem[] { installedSDK1, installedSDK2, installedSDK3, installedSDK4, installedSDK5 }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("DisplayName").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKName").Equals("GoodTestSDK, Version=2.0", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Make sure when no sdks are resolved there are no problems and that the names of the sdks which were not resolved are logged. + /// + [TestMethod] + public void NOSDKResolved() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK2, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item, item2 }; + + ITaskItem installedSDK = new TaskItem("DoesNotExist"); + installedSDK.SetMetadata("SDKName", "RandomSDK, Version=1.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsFalse(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CouldNotResolveSDK", "GoodTestSDK, Version=2.0"); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CouldNotResolveSDK", "GoodTestSDK2, Version=2.0"); + } + + /// + /// When there is a mix of resolved and unresolved SDKs make sure that the resolved ones are correctly found + /// and the unresolved ones are logged. + /// + [TestMethod] + public void MixOfResolvedAndUnResolved() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem item2 = new TaskItem("RandomSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item, item2 }; + + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + t.LogResolutionErrorsAsWarnings = true; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(@"c:\SDKDirectory\GoodTestSDK\2.0\", StringComparison.OrdinalIgnoreCase)); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.FoundSDK", @"c:\SDKDirectory\GoodTestSDK\2.0\"); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CouldNotResolveSDK", "RandomSDK, Version=2.0"); + } + + /// + /// When a null is passed into the SDKReferences property make sure we get the correct exception out. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullSDKReferences() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + t.SDKReferences = null; + bool succeeded = t.Execute(); + } + + /// + /// When a null is passed into the set of InstalledSDKS property make sure we get the correct exception out. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void NullInstalledSDKs() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + t.InstalledSDKs = null; + bool succeeded = t.Execute(); + } + + + /// + /// If no SDKReferences are passed in then we should get nothing out. + /// + [TestMethod] + public void EmptySDKReferencesList() + { + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[0]; + ITaskItem installedSDK = new TaskItem(@"c:\SDKDirectory\GoodTestSDK\2.0\"); + installedSDK.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installedSDK }; + + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + } + + /// + /// When we find the SDKManifest it may be poorly formatted. If that happens we need to log the error + /// and not resolve the SDK. We also add a good one as well to make sure resolution continues. + /// + [TestMethod] + public void SDKFoundButBadlyFormattedSDKManifestWarnings() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SDKFoundButBadlyFormattedSDKManifestWarnings"); + string testDirectory = Path.Combine(testDirectoryRoot, "BadTestSDK\\2.0\\"); + string sdkManifestContents = + @"IAMNOTANXMLFILE"; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("BadTestSDK, Version=2.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item, item2 }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "BadTestSDK, Version=2.0"); + + ITaskItem installLocation2 = new TaskItem("C:\\GoodSDKLocation"); + installLocation2.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation, installLocation2 }; + t.BuildEngine = engine; + t.LogResolutionErrorsAsWarnings = true; + bool succeeded = t.Execute(); + + Assert.IsTrue(succeeded); + engine.AssertLogContains("MSB3775"); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 2); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[1].ItemSpec.Equals("C:\\GoodSDKLocation\\", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// When we find the SDKManifest it may be poorly formatted. If that happens we need to log the error + /// and not resolve the SDK. We also add a good one as well to make sure resolution continues. + /// + [TestMethod] + public void SDKFoundButBadlyFormattedSDKManifestErrors() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SDKFoundButBadlyFormattedSDKManifestErrors"); + string testDirectory = Path.Combine(testDirectoryRoot, "BadTestSDK\\2.0\\"); + string sdkManifestContents = + @"IAMNOTANXMLFILE"; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("BadTestSDK, Version=2.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item, item2 }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "BadTestSDK, Version=2.0"); + + ITaskItem installLocation2 = new TaskItem("C:\\GoodSDKLocation"); + installLocation2.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation, installLocation2 }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + + Assert.IsFalse(succeeded); + engine.AssertLogContains("MSB3775"); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + [TestMethod] + public void TestMaxPlatformVersionWithTargetFrameworkVersion() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "TestMaxPlatformVersionWithTargetFrameworkVersion"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents1 = + @" + + + + "; + + string sdkManifestContents2 = + @" + + + + "; + + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + + // In the test below the SDK MaxPlatformVersion is smaller than the TargetPlatformVersion - the build fails + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + MockEngine engine1 = new MockEngine(); + ResolveSDKReference t1 = new ResolveSDKReference(); + t1.SDKReferences = new ITaskItem[] { item }; + t1.InstalledSDKs = new ITaskItem[] { installLocation }; + t1.TargetPlatformIdentifier = "Windows"; + t1.ProjectName = "myproject.csproj"; + t1.BuildEngine = engine1; + bool succeeded1 = t1.Execute(); + + Assert.IsTrue(succeeded1); + engine1.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionLessThanTargetPlatformVersion", "myproject.csproj", "GoodTestSDK", "2.0", "Windows", "6.0", "Windows", "7.0"); + // In the test below the SDK MaxPlatformVersion is greater than the TargetPlatformVersion - the build succeeds + File.WriteAllText(sdkManifestFile, sdkManifestContents2); + MockEngine engine2 = new MockEngine(); + ResolveSDKReference t2 = new ResolveSDKReference(); + t2.SDKReferences = new ITaskItem[] { item }; + t2.InstalledSDKs = new ITaskItem[] { installLocation }; + t1.TargetPlatformIdentifier = "Windows"; + t1.ProjectName = "myproject.csproj"; + t2.BuildEngine = engine1; + bool succeeded2 = t2.Execute(); + + Assert.IsTrue(succeeded2); + engine2.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.MaxPlatformVersionLessThanTargetPlatformVersion", "myproject.csproj", "GoodTestSDK", "2.0", "Windows", "6.0", "Windows", "7.0"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the manifest attributes are empty. + /// + [TestMethod] + public void EmptySDKManifestAttributes() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "EmptySDKManifestAttributes"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportedArchitectures").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ProductFamilyName").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("DisplayName").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ArchitectureForRuntime").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("MaxPlatformVersion").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("MaxOSVersionTested").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("MinOSVersion").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportsMultipleVersions").Equals("Allow", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where we override ALL of the manifest properties with ones on the metadata + /// + [TestMethod] + public void OverrideManifestAttributes() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "OverrideManifestAttributes"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("FrameworkIdentity", "MetadataIdentity"); + item.SetMetadata("PlatformIdentity", "PlatformIdentity"); + item.SetMetadata("AppXLocation", "Metadata AppxLocation"); + item.SetMetadata("SDKType", "External"); + item.SetMetadata("SupportsMultipleVersions", "Error"); + item.SetMetadata("DisplayName", "ManifestDisplayName"); + item.SetMetadata("CopyRedist", "True"); + item.SetMetadata("ExpandReferenceAssemblies", "True"); + item.SetMetadata("CopyLocalExpandedReferenceAssemblies", "True"); + item.SetMetadata("TargetedSDKConfiguration", "Custom"); + item.SetMetadata("TargetedSDKArchitecture", "Any CPU"); + item.SetMetadata("CopyRedistToSubDirectory", "MyRedistSubDirectory"); + item.SetMetadata("MaxPlatformVersion", "9.0"); + item.SetMetadata("MaxOSVersionTested", "3.3.3"); + item.SetMetadata("MinOSVersion", "3.3.3"); + + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("MetadataIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("PlatformIdentity").Equals("PlatformIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Metadata AppxLocation", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportsMultipleVersions").Equals("Error", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("DisplayName").Equals("ManifestDisplayName", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Custom", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Equals("MyRedistSubDirectory", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("MaxPlatformVersion").Equals("9.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("MaxOSVersionTested").Equals("3.3.3", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("MinOSVersion").Equals("3.3.3", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where we Have a good manfest that had framework and appx locations that exactly match the targeted sdk configuration and architecture. + /// + [TestMethod] + public void GoodManifestMatchingConfigAndArch() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "GoodManifestMatchingConfigAndArch"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Neutral|RetailX86Location", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Equals("GoodTestSDK\\Redist", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where we Have a good manfest that had framework and appx locations that only match the targeted sdk configuration. + /// + [TestMethod] + public void GoodManifestMatchingConfigOnly() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "GoodManifestMatchingConfigOnly"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Neutral|RetailNeutralLocation", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// TVerify that when a platform identity is found that we do not copy the references or redist + /// + [TestMethod] + public void NoCopyOnPlatformIdentityFound() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "NoCopyOnPlatformIdentityFound"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Length == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("PlatformIdentity").Equals("PlatformID", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where we Have a good manfest that had framework and appx locations that does not match any of the config arch combinations but does match + /// and entry name simply FrameworkIdentity or APPX + /// + [TestMethod] + public void GoodManifestMatchingBaseNameOnly() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "GoodManifestMatchingConfigOnly"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("PlatformIdentity").Equals("Good Platform", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Neutral|Location", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("X86", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where we only have the arm APPX and it can be found + /// + [TestMethod] + public void ManifestOnlyHasArmLocation() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ManifestOnlyHasArmLocation"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.TargetedSDKArchitecture = "arm"; + t.TargetedSDKConfiguration = "Debug"; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("PlatformIdentity").Equals("Good Platform", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("arm|ARMAppx", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Debug", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("arm", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where we have a number of locations and arm APPX and can be found + /// + [TestMethod] + public void ManifestArmLocationWithOthers() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ManifestArmLocationWithOthers"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.TargetedSDKArchitecture = "arm"; + t.TargetedSDKConfiguration = "Debug"; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("PlatformIdentity").Equals("Good Platform", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("arm|ARMAppx|x64|x64Appx|x86|x86Appx", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Debug", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("arm", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there are framework identity attributes but none of the match and there is no base FrameworkIdentity, the + /// same is true for APPX. + /// + [TestMethod] + public void MatchNoNamesButNamesExistWarning() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "MatchNoNamesButNamesExistWarning"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + t.BuildEngine = engine; + t.LogResolutionErrorsAsWarnings = true; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 1); + + string message = ResourceUtilities.FormatResourceString("ResolveSDKReference.ReadingSDKManifestFile", sdkManifestFile); + engine.AssertLogContains(message); + + string errorMessage = ResourceUtilities.FormatResourceString("ResolveSDKReference.NoMatchingFrameworkIdentity", sdkManifestFile, "Retail", "x86"); + engine.AssertLogContains(errorMessage); + + errorMessage = ResourceUtilities.FormatResourceString("ResolveSDKReference.NoMatchingAppxLocation", sdkManifestFile, "Retail", "x86"); + engine.AssertLogContains(errorMessage); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there are framework identity attributes but none of the match and there is no base FrameworkIdentity, the + /// same is true for APPX. + /// + [TestMethod] + public void MatchNoNamesButNamesExistError() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "MatchNoNamesButNamesExistError"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + t.LogResolutionErrorsAsWarnings = false; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + + string errorMessage = ResourceUtilities.FormatResourceString("ResolveSDKReference.NoMatchingFrameworkIdentity", sdkManifestFile, "Retail", "x86"); + engine.AssertLogContains(errorMessage); + + errorMessage = ResourceUtilities.FormatResourceString("ResolveSDKReference.NoMatchingAppxLocation", sdkManifestFile, "Retail", "x86"); + engine.AssertLogContains(errorMessage); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + + /// + /// Test the case where there is a single supported architecture and the project targets that architecture + /// + [TestMethod] + public void SingleSupportedArchitectureMatchesProject() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SingleSupportedArchitectureMatchesProject"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("x64|RetailX64Location|x86|RetailX86Location", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("X86", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Equals("GoodTestSDK\\Redist", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the productfamily is set in the manifest and not as metadata on the reference item. + /// + [TestMethod] + public void ProductFamilySetInManifest() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ProductFamilySetInManifest"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ProductFamilyName").Equals("MyFamily", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the productfamily is set in the manifest and as metadata on the reference item. Expect the metadata to win. + /// + [TestMethod] + public void ProductFamilySetInManifestAndMetadata() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ProductFamilySetInManifestAndMetadata"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("ProductFamilyName", "MetadataFamily"); + + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ProductFamilyName").Equals("MetadataFamily", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the SupportsMultipleVersions is NOT in the manifest or on metadata + /// + [TestMethod] + public void SupportsMultipleVersionsNotInManifest() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SupportsMultipleVersionsNotInManifest"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + Assert.IsTrue(engine.Warnings == 0); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportsMultipleVersions").Equals("Allow", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where metadata on the item is bad, we should then read from the manifest. + /// + [TestMethod] + public void SupportsMultipleVersionsBadMetadata() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SupportsMultipleVersionsBadMetadata"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("SupportsMultipleVersions", "WoofWoof"); + + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportsMultipleVersions").Equals("Warning", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there are conflicts between sdks of the same product family + /// + [TestMethod] + public void ConflictsBetweenSameProductFamilySameName() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ConflictsBetweenSameProductFamilySameName"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\1.0\\"); + string testDirectory2 = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string testDirectory3 = Path.Combine(testDirectoryRoot, "GoodTestSDK\\3.0\\"); + + string sdkManifestContents1 = + @" + "; + + string sdkManifestContents2 = + @" + "; + + string sdkManifestContents3 = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + string sdkManifestFile2 = Path.Combine(testDirectory2, "SdkManifest.xml"); + string sdkManifestFile3 = Path.Combine(testDirectory3, "SdkManifest.xml"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + Directory.CreateDirectory(testDirectory2); + Directory.CreateDirectory(testDirectory3); + + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + File.WriteAllText(sdkManifestFile2, sdkManifestContents2); + File.WriteAllText(sdkManifestFile3, sdkManifestContents3); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=1.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem item3 = new TaskItem("GoodTestSDK, Version=3.0"); + + t.SDKReferences = new ITaskItem[] { item, item2, item3 }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=1.0"); + + ITaskItem installLocation2 = new TaskItem(testDirectory2); + installLocation2.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + + ITaskItem installLocation3 = new TaskItem(testDirectory3); + installLocation3.SetMetadata("SDKName", "GoodTestSDK, Version=3.0"); + + t.InstalledSDKs = new ITaskItem[] { installLocation, installLocation2, installLocation3 }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", "GoodTestSDK, Version=1.0", "\"GoodTestSDK, Version=2.0\", \"GoodTestSDK, Version=3.0\"", "MyFamily"); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", "GoodTestSDK, Version=2.0", "\"GoodTestSDK, Version=1.0\", \"GoodTestSDK, Version=3.0\"", "MyFamily"); + Assert.IsTrue(engine.Warnings == 1); + Assert.IsTrue(engine.Errors == 1); + + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[1].ItemSpec.Equals(testDirectory2, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[2].ItemSpec.Equals(testDirectory3, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there are conflicts between sdks of the same product family + /// + [TestMethod] + public void ConflictsBetweenSameProductFamilyDiffName() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ConflictsBetweenSameProductFamilyDiffName"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\1.0\\"); + string testDirectory2 = Path.Combine(testDirectoryRoot, "GoodTestSDK1\\2.0\\"); + string testDirectory3 = Path.Combine(testDirectoryRoot, "GoodTestSDK3\\3.0\\"); + + string sdkManifestContents1 = + @" + "; + + string sdkManifestContents2 = + @" + "; + + string sdkManifestContents3 = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + string sdkManifestFile2 = Path.Combine(testDirectory2, "SdkManifest.xml"); + string sdkManifestFile3 = Path.Combine(testDirectory3, "SdkManifest.xml"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + Directory.CreateDirectory(testDirectory2); + Directory.CreateDirectory(testDirectory3); + + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + File.WriteAllText(sdkManifestFile2, sdkManifestContents2); + File.WriteAllText(sdkManifestFile3, sdkManifestContents3); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=1.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK2, Version=2.0"); + ITaskItem item3 = new TaskItem("GoodTestSDK3, Version=3.0"); + + t.SDKReferences = new ITaskItem[] { item, item2, item3 }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=1.0"); + + ITaskItem installLocation2 = new TaskItem(testDirectory2); + installLocation2.SetMetadata("SDKName", "GoodTestSDK2, Version=2.0"); + + ITaskItem installLocation3 = new TaskItem(testDirectory3); + installLocation3.SetMetadata("SDKName", "GoodTestSDK3, Version=3.0"); + + t.InstalledSDKs = new ITaskItem[] { installLocation, installLocation2, installLocation3 }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", "GoodTestSDK, Version=1.0", "\"GoodTestSDK2, Version=2.0\", \"GoodTestSDK3, Version=3.0\"", "MyFamily"); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", "GoodTestSDK2, Version=2.0", "\"GoodTestSDK, Version=1.0\", \"GoodTestSDK3, Version=3.0\"", "MyFamily"); + Assert.IsTrue(engine.Warnings == 1); + Assert.IsTrue(engine.Errors == 1); + + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[1].ItemSpec.Equals(testDirectory2, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[2].ItemSpec.Equals(testDirectory3, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there are conflicts between sdks of the same product family + /// + [TestMethod] + public void ConflictsBetweenMIXPFAndName() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ConflictsBetweenSameProductFamilyDiffName"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\1.0\\"); + string testDirectory2 = Path.Combine(testDirectoryRoot, "GoodTestSDK2\\2.0\\"); + string testDirectory3 = Path.Combine(testDirectoryRoot, "GoodTestSDK3\\3.0\\"); + string testDirectory4 = Path.Combine(testDirectoryRoot, "GoodTestSDK3\\4.0\\"); + + string sdkManifestContents1 = + @" + "; + + string sdkManifestContents2 = + @" + "; + + string sdkManifestContents3 = + @" + "; + + + string sdkManifestContents4 = + @" + "; + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + string sdkManifestFile2 = Path.Combine(testDirectory2, "SdkManifest.xml"); + string sdkManifestFile3 = Path.Combine(testDirectory3, "SdkManifest.xml"); + string sdkManifestFile4 = Path.Combine(testDirectory4, "SdkManifest.xml"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + Directory.CreateDirectory(testDirectory2); + Directory.CreateDirectory(testDirectory3); + Directory.CreateDirectory(testDirectory4); + + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + File.WriteAllText(sdkManifestFile2, sdkManifestContents2); + File.WriteAllText(sdkManifestFile3, sdkManifestContents3); + File.WriteAllText(sdkManifestFile4, sdkManifestContents4); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=1.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK2, Version=2.0"); + ITaskItem item3 = new TaskItem("GoodTestSDK3, Version=3.0"); + ITaskItem item4 = new TaskItem("GoodTestSDK3, Version=4.0"); + + t.SDKReferences = new ITaskItem[] { item, item2, item3, item4 }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=1.0"); + + ITaskItem installLocation2 = new TaskItem(testDirectory2); + installLocation2.SetMetadata("SDKName", "GoodTestSDK2, Version=2.0"); + + ITaskItem installLocation3 = new TaskItem(testDirectory3); + installLocation3.SetMetadata("SDKName", "GoodTestSDK3, Version=3.0"); + + ITaskItem installLocation4 = new TaskItem(testDirectory4); + installLocation4.SetMetadata("SDKName", "GoodTestSDK3, Version=4.0"); + + t.InstalledSDKs = new ITaskItem[] { installLocation, installLocation2, installLocation3, installLocation4 }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameFamily", "GoodTestSDK, Version=1.0", "\"GoodTestSDK2, Version=2.0\"", "MyFamily"); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameName", "GoodTestSDK3, Version=3.0", "\"GoodTestSDK3, Version=4.0\""); + Assert.IsTrue(engine.Warnings == 2); + Assert.IsTrue(engine.Errors == 0); + + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[1].ItemSpec.Equals(testDirectory2, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[2].ItemSpec.Equals(testDirectory3, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[3].ItemSpec.Equals(testDirectory4, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there are conflicts between sdks of the same SDK Name + /// + [TestMethod] + public void ConflictsBetweenSameSDKName() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "ConflictsBetweenSameSDKName"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\1.0\\"); + string testDirectory2 = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string testDirectory3 = Path.Combine(testDirectoryRoot, "GoodTestSDK\\3.0\\"); + + string sdkManifestContents1 = + @" + "; + + string sdkManifestContents2 = + @" + "; + + string sdkManifestContents3 = + @" + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + string sdkManifestFile2 = Path.Combine(testDirectory2, "SdkManifest.xml"); + string sdkManifestFile3 = Path.Combine(testDirectory3, "SdkManifest.xml"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + Directory.CreateDirectory(testDirectory2); + Directory.CreateDirectory(testDirectory3); + + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + File.WriteAllText(sdkManifestFile2, sdkManifestContents2); + File.WriteAllText(sdkManifestFile3, sdkManifestContents3); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=1.0"); + ITaskItem item2 = new TaskItem("GoodTestSDK, Version=2.0"); + ITaskItem item3 = new TaskItem("GoodTestSDK, Version=3.0"); + + t.SDKReferences = new ITaskItem[] { item, item2, item3 }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=1.0"); + + ITaskItem installLocation2 = new TaskItem(testDirectory2); + installLocation2.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + + ITaskItem installLocation3 = new TaskItem(testDirectory3); + installLocation3.SetMetadata("SDKName", "GoodTestSDK, Version=3.0"); + + t.InstalledSDKs = new ITaskItem[] { installLocation, installLocation2, installLocation3 }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameName", "GoodTestSDK, Version=1.0", "\"GoodTestSDK, Version=2.0\", \"GoodTestSDK, Version=3.0\""); + engine.AssertLogContainsMessageFromResource(_resourceDelegate, "ResolveSDKReference.CannotReferenceTwoSDKsSameName", "GoodTestSDK, Version=2.0", "\"GoodTestSDK, Version=1.0\", \"GoodTestSDK, Version=3.0\""); + Assert.IsTrue(engine.Warnings == 1); + Assert.IsTrue(engine.Errors == 1); + + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[1].ItemSpec.Equals(testDirectory2, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[2].ItemSpec.Equals(testDirectory3, StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where metadata on the item is bad, we should then read from the manifest. + /// + [TestMethod] + public void SupportsMultipleVersionsReadManifest() + { + SupportsMultipleVersionsVerifyManifestReading("Error"); + SupportsMultipleVersionsVerifyManifestReading("Warning"); + SupportsMultipleVersionsVerifyManifestReading("Allow"); + SupportsMultipleVersionsVerifyManifestReading("WoofWoof"); + } + + private void SupportsMultipleVersionsVerifyManifestReading(string manifestEntry) + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SupportsMultipleVersionsVerifyManifestReading"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "x86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + if (String.Equals(manifestEntry, "WoofWoof", StringComparison.OrdinalIgnoreCase)) + { + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportsMultipleVersions").Equals("Allow", StringComparison.OrdinalIgnoreCase)); + } + else + { + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SupportsMultipleVersions").Equals(manifestEntry, StringComparison.OrdinalIgnoreCase)); + } + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the supportedArchitectures are empty + /// + [TestMethod] + public void EmptyArchitectures() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "OverrideManifestWithMetadata"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("SupportedArchitectures", "X86"); + + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "X86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Arm|RetailArmLocation|Neutral|NeutralLocation|X64|RetailX64Location|X86|RetailX86Location", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("X86", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Equals("GoodTestSDK\\Redist", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the metadata on the reference overrides what is in the manifest but it does not match what is being targeted + /// + [TestMethod] + public void OverrideManifestWithMetadataButMetadataDoesNotMatch() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "OverrideManifestWithMetadataButMetadataDoesNotMatch"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("SupportedArchitectures", "ARM"); + + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "X86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + engine.AssertLogContains("MSB3779"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where the metadata on the reference overrides what is in the manifest + /// + [TestMethod] + public void OverrideManifestWithMetadata() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "OverrideManifestWithMetadata"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + item.SetMetadata("SupportedArchitectures", "X64;X86;Neutral"); + + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "X86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Neutral|NeutralLocation|X64|RetailX64Location|X86|RetailX86Location", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("X86", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Equals("GoodTestSDK\\Redist", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there is a single supported architecture and the project does not target that architecture + /// + [TestMethod] + public void SingleSupportedArchitectureDoesNotMatchProject() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "SingleSupportedArchitectureDoesNotMatchProject"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "X86"; + t.TargetedSDKConfiguration = "Release"; + + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + engine.AssertLogContains("MSB3779"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there is are multiple supported architecture and the project targets one of those architectures + /// + [TestMethod] + public void MultipleSupportedArchitectureMatchesProject() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "MultipleSupportedArchitectureMatchesProject"); + string testDirectory = Path.Combine(testDirectoryRoot, "GoodTestSDK\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "AnyCPU"; + t.TargetedSDKConfiguration = "Release"; + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsTrue(succeeded); + + engine.AssertLogDoesntContainMessageFromResource(_resourceDelegate, "ResolveSDKReference.NoFrameworkIdentitiesFound"); + Assert.IsTrue(t.ResolvedSDKReferences[0].ItemSpec.Equals(testDirectory, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("FrameworkIdentity").Equals("GoodTestSDKIdentity", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("AppXLocation").Equals("Neutral|RetailNeutralLocation", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("SDKType").Equals("External", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedist").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("ExpandReferenceAssemblies").Equals("True", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyLocalExpandedReferenceAssemblies").Equals("False", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("OriginalItemSpec").Equals(item.ItemSpec, StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(t.ResolvedSDKReferences[0].GetMetadata("CopyRedistToSubDirectory").Equals("GoodTestSDK\\Redist", StringComparison.OrdinalIgnoreCase)); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + /// + /// Test the case where there is are multiple supported architecture and the project does not match one of those architectures + /// + [TestMethod] + public void MultipleSupportedArchitectureDoesNotMatchProject() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "MultipleSupportedArchitectureMatchesProject"); + string testDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string sdkManifestContents = + @" + + + + "; + + try + { + string sdkManifestFile = Path.Combine(testDirectory, "SdkManifest.xml"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectory); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + + // Create the engine. + MockEngine engine = new MockEngine(); + + ResolveSDKReference t = new ResolveSDKReference(); + ITaskItem item = new TaskItem("GoodTestSDK, Version=2.0"); + t.SDKReferences = new ITaskItem[] { item }; + t.TargetedSDKArchitecture = "ARM"; + t.TargetedSDKConfiguration = "Release"; + ITaskItem installLocation = new TaskItem(testDirectory); + installLocation.SetMetadata("SDKName", "GoodTestSDK, Version=2.0"); + t.InstalledSDKs = new ITaskItem[] { installLocation }; + t.BuildEngine = engine; + bool succeeded = t.Execute(); + Assert.IsFalse(succeeded); + + Assert.IsTrue(t.ResolvedSDKReferences.Length == 0); + engine.AssertLogContains("MSB3779"); + } + finally + { + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + #endregion + } + + /// + /// Test the output groups which will be used to generate the recipe fileGatherSDKOutputGroups + /// + [TestClass] + public class GatherSDKOutputGroupsTestFixture + { + [TestMethod] + public void GatherSDKOutputGroupsTargetArchitectureExists() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "GatherSDKOutputGroupsWithFramework"); + string sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string archRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\x86"); + string neutralRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\Neutral"); + string archCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\x86"); + string neutralCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\Neutral"); + + string sdkDirectory3 = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\FrameworkSDkWithManifest\\2.0\\"); + string archRedist3 = Path.Combine(sdkDirectory3, "Redist\\Retail\\x64"); + string archRedist33 = Path.Combine(sdkDirectory3, "Redist\\Retail\\Neutral"); + string archCommonRedist3 = Path.Combine(sdkDirectory3, "Redist\\CommonConfiguration\\x64"); + + string sdkManifestContents = + @" + + + + "; + + string sdkManifestContents2 = + @" + + + + "; + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + + Release + AnotherRedistLocation + + + amd64 + AnotherRedistLocation + + + + CAT" + + @"" + testDirectoryRoot + "" + + @"MyPlatform + 8.0 + + + "); + + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", testDirectoryRoot); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + Directory.CreateDirectory(archRedist); + Directory.CreateDirectory(neutralRedist); + Directory.CreateDirectory(archCommonRedist); + Directory.CreateDirectory(neutralCommonRedist); + + Directory.CreateDirectory(sdkDirectory3); + Directory.CreateDirectory(archRedist3); + Directory.CreateDirectory(archRedist33); + Directory.CreateDirectory(archCommonRedist3); + + string sdkManifestFile = Path.Combine(sdkDirectory, "SdkManifest.xml"); + string sdkManifestFile2 = Path.Combine(sdkDirectory3, "SdkManifest.xml"); + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + File.WriteAllText(sdkManifestFile2, sdkManifestContents2); + File.WriteAllText(testProjectFile, tempProjectContents); + + + string redist1 = Path.Combine(archRedist, "A.dll"); + string redist2 = Path.Combine(neutralRedist, "B.dll"); + string redist3 = Path.Combine(archCommonRedist, "C.dll"); + string redist4 = Path.Combine(neutralCommonRedist, "D.dll"); + string redist5 = Path.Combine(archRedist33, "A.dll"); + string redist6 = Path.Combine(archCommonRedist3, "B.dll"); + + File.WriteAllText(redist1, "Test"); + File.WriteAllText(redist2, "Test"); + File.WriteAllText(redist3, "Test"); + File.WriteAllText(redist4, "Test"); + File.WriteAllText(redist5, "Test"); + File.WriteAllText(redist6, "Test"); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + ProjectInstance project = pc.LoadProject(testProjectFile).CreateProjectInstance(); + project.SetProperty("SDKReferenceDirectoryRoot", testDirectoryRoot); + project.SetProperty("SDKReferenceRegistryRoot", ""); + + IDictionary targetResults = new Dictionary(); + bool success = project.Build(new string[] { "GetResolvedSDKReferences" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("GetResolvedSDKReferences")); + TargetResult result = targetResults["GetResolvedSDKReferences"]; + ITaskItem[] resolvedSDKReferences = result.Items; + Assert.IsTrue(resolvedSDKReferences.Length == 2); + + logger = new MockLogger(); + targetResults = new Dictionary(); + success = project.Build(new string[] { "SDKRedistOutputGroup" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("SDKRedistOutputGroup")); + result = targetResults["SDKRedistOutputGroup"]; + ITaskItem[] SDkRedistFolders = result.Items; + Assert.IsTrue(SDkRedistFolders.Length == 2); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + [TestMethod] + public void GatherSDKOutputGroupsTargetArchitectureExists2() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "GatherSDKOutputGroupsWithFramework"); + string sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string archRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\x86"); + string neutralRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\Neutral"); + string archCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\x86"); + string neutralCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\Neutral"); + + string sdkDirectory3 = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\FrameworkSDkWithManifest\\2.0\\"); + string archRedist3 = Path.Combine(sdkDirectory3, "Redist\\Retail\\x64"); + string archRedist33 = Path.Combine(sdkDirectory3, "Redist\\Retail\\Neutral"); + string archCommonRedist3 = Path.Combine(sdkDirectory3, "Redist\\CommonConfiguration\\x64"); + + string sdkManifestContents = + @" + + + + "; + + string sdkManifestContents2 = + @" + + + + "; + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + + Release + + + amd64 + + + + CAT" + + @"" + testDirectoryRoot + "" + + @"MyPlatform + 8.0 + + + "); + + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", testDirectoryRoot); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + Directory.CreateDirectory(archRedist); + Directory.CreateDirectory(neutralRedist); + Directory.CreateDirectory(archCommonRedist); + Directory.CreateDirectory(neutralCommonRedist); + + Directory.CreateDirectory(sdkDirectory3); + Directory.CreateDirectory(archRedist3); + Directory.CreateDirectory(archRedist33); + Directory.CreateDirectory(archCommonRedist3); + + string sdkManifestFile = Path.Combine(sdkDirectory, "SdkManifest.xml"); + string sdkManifestFile2 = Path.Combine(sdkDirectory3, "SdkManifest.xml"); + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + File.WriteAllText(sdkManifestFile2, sdkManifestContents2); + File.WriteAllText(testProjectFile, tempProjectContents); + + + string redist1 = Path.Combine(archRedist, "A.dll"); + string redist2 = Path.Combine(neutralRedist, "B.dll"); + string redist3 = Path.Combine(archCommonRedist, "C.dll"); + string redist4 = Path.Combine(neutralCommonRedist, "D.dll"); + string redist5 = Path.Combine(archRedist3, "D.dll"); + string redist6 = Path.Combine(archRedist33, "A.dll"); + string redist7 = Path.Combine(archCommonRedist3, "B.dll"); + + File.WriteAllText(redist1, "Test"); + File.WriteAllText(redist2, "Test"); + File.WriteAllText(redist3, "Test"); + File.WriteAllText(redist4, "Test"); + File.WriteAllText(redist5, "Test"); + File.WriteAllText(redist6, "Test"); + File.WriteAllText(redist7, "Test"); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + ProjectInstance project = pc.LoadProject(testProjectFile).CreateProjectInstance(); + project.SetProperty("SDKReferenceDirectoryRoot", testDirectoryRoot); + project.SetProperty("SDKReferenceRegistryRoot", ""); + + IDictionary targetResults = new Dictionary(); + bool success = project.Build(new string[] { "GetResolvedSDKReferences" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("GetResolvedSDKReferences")); + TargetResult result = targetResults["GetResolvedSDKReferences"]; + ITaskItem[] resolvedSDKReferences = result.Items; + Assert.IsTrue(resolvedSDKReferences.Length == 2); + + logger = new MockLogger(); + targetResults = new Dictionary(); + success = project.Build(new string[] { "SDKRedistOutputGroup" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("SDKRedistOutputGroup")); + result = targetResults["SDKRedistOutputGroup"]; + ITaskItem[] SDkRedistFolders = result.Items; + Assert.IsTrue(SDkRedistFolders.Length == 2); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + + [TestMethod] + public void GatherSDKOutputGroupsTargetArchitectureDoesNotExists() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "GatherSDKOutputGroupsTargetArchitectureDoesNotExists"); + string sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string x86Redist = Path.Combine(sdkDirectory, "Redist\\Retail\\x86"); + string neutralRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\Neutral"); + string x86CommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\x86"); + string neutralCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\Neutral"); + + string sdkManifestContents = + @" + + + + "; + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + + Release + + + + CAT" + + @"" + testDirectoryRoot + "" + + @"MyPlatform + 8.0 + + + "); + + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", testDirectoryRoot); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + Directory.CreateDirectory(x86Redist); + Directory.CreateDirectory(x86CommonRedist); + Directory.CreateDirectory(neutralRedist); + Directory.CreateDirectory(neutralCommonRedist); + + string sdkManifestFile = Path.Combine(sdkDirectory, "SdkManifest.xml"); + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + File.WriteAllText(testProjectFile, tempProjectContents); + + + string redist1 = Path.Combine(x86CommonRedist, "A.dll"); + string redist2 = Path.Combine(x86Redist, "B.dll"); + string redist3 = Path.Combine(neutralRedist, "C.dll"); + string redist4 = Path.Combine(neutralCommonRedist, "D.dll"); + + File.WriteAllText(redist1, "Test"); + File.WriteAllText(redist2, "Test"); + File.WriteAllText(redist3, "Test"); + File.WriteAllText(redist4, "Test"); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + ProjectInstance project = pc.LoadProject(testProjectFile).CreateProjectInstance(); + project.SetProperty("SDKReferenceDirectoryRoot", testDirectoryRoot); + project.SetProperty("SDKReferenceRegistryRoot", ""); + + IDictionary targetResults = new Dictionary(); + bool success = project.Build(new string[] { "GetResolvedSDKReferences" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("GetResolvedSDKReferences")); + TargetResult result = targetResults["GetResolvedSDKReferences"]; + ITaskItem[] resolvedSDKReferences = result.Items; + Assert.IsTrue(resolvedSDKReferences.Length == 1); + + logger = new MockLogger(); + targetResults = new Dictionary(); + success = project.Build(new string[] { "SDKRedistOutputGroup" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("SDKRedistOutputGroup")); + result = targetResults["SDKRedistOutputGroup"]; + ITaskItem[] SDkRedistFolders = result.Items; + Assert.IsTrue(SDkRedistFolders.Length == 2); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + + + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + [TestMethod] + public void CheckDefaultingOfTargetConfigAndArchitecture() + { + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "CheckDefaultingOfTargetConfigAndArchitecture"); + string sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string neutralRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\Neutral"); + string neutralCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\Neutral"); + + string sdkManifestContents = + @" + + + + "; + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + + + + CAT" + + @"" + testDirectoryRoot + "" + + @"MyPlatform + 8.0 + + + "); + + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", testDirectoryRoot); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + Directory.CreateDirectory(neutralRedist); + Directory.CreateDirectory(neutralCommonRedist); + + string sdkManifestFile = Path.Combine(sdkDirectory, "SdkManifest.xml"); + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + + File.WriteAllText(sdkManifestFile, sdkManifestContents); + File.WriteAllText(testProjectFile, tempProjectContents); + + string redist1 = Path.Combine(neutralRedist, "B.dll"); + string redist2 = Path.Combine(neutralCommonRedist, "C.dll"); + + File.WriteAllText(redist1, "Test"); + File.WriteAllText(redist2, "Test"); + + MockLogger logger = new MockLogger(); + + ProjectCollection pc = new ProjectCollection(); + ProjectInstance project = pc.LoadProject(testProjectFile).CreateProjectInstance(); + project.SetProperty("SDKReferenceDirectoryRoot", testDirectoryRoot); + project.SetProperty("SDKReferenceRegistryRoot", ""); + + IDictionary targetResults = new Dictionary(); + bool success = project.Build(new string[] { "GetResolvedSDKReferences" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("GetResolvedSDKReferences")); + TargetResult result = targetResults["GetResolvedSDKReferences"]; + ITaskItem[] resolvedSDKReferences = result.Items; + Assert.IsTrue(resolvedSDKReferences.Length == 1); + Assert.IsTrue(resolvedSDKReferences[0].GetMetadata("TargetedSDKConfiguration").Equals("Retail", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(resolvedSDKReferences[0].GetMetadata("TargetedSDKArchitecture").Equals("Neutral", StringComparison.OrdinalIgnoreCase)); + + logger = new MockLogger(); + targetResults = new Dictionary(); + success = project.Build(new string[] { "SDKRedistOutputGroup" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + Assert.IsTrue(targetResults.ContainsKey("SDKRedistOutputGroup")); + result = targetResults["SDKRedistOutputGroup"]; + ITaskItem[] SDkRedistFolders = result.Items; + Assert.IsTrue(SDkRedistFolders.Length == 2); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + [TestMethod] + public void CheckAttributesFromManifestArePassedToResolvedAssemblies() + { + /* \Microsoft SDKs\Windows\v8.0\ExtensionSDKs */ + string testDirectoryRoot = Path.Combine(Path.GetTempPath(), "CheckDefaultingOfTargetConfigAndArchitecture"); + string sdkDirectory = Path.Combine(testDirectoryRoot, "MyPlatform\\v8.0\\ExtensionSDKs\\SDkWithManifest\\2.0\\"); + string neutralRedist = Path.Combine(sdkDirectory, "Redist\\Retail\\Neutral"); + string neutralCommonRedist = Path.Combine(sdkDirectory, "Redist\\CommonConfiguration\\Neutral"); + + string sdkManifestContents1 = + @" + + + + "; + + // This is not a framework SDK because it does not have FrameworkIdentity set + string sdkManifestContents2 = @" + + + + + + + + "; + + + + string tempProjectContents = ObjectModelHelpers.CleanupFileContents(@" + + + + + + CAT" + + @"" + testDirectoryRoot + "" + + @"MyPlatform + 8.0 + + + "); + + try + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", testDirectoryRoot); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", "true"); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + + Directory.CreateDirectory(testDirectoryRoot); + Directory.CreateDirectory(sdkDirectory); + Directory.CreateDirectory(neutralRedist); + Directory.CreateDirectory(neutralCommonRedist); + + string redist1 = Path.Combine(neutralRedist, "B.dll"); + string redist2 = Path.Combine(neutralCommonRedist, "C.dll"); + + File.WriteAllText(redist1, "Test"); + File.WriteAllText(redist2, "Test"); + + MockLogger logger = new MockLogger(); + + string testProjectFile = Path.Combine(testDirectoryRoot, "testproject.csproj"); + File.WriteAllText(testProjectFile, tempProjectContents); + + string sdkManifestFile = Path.Combine(sdkDirectory, "SdkManifest.xml"); + + File.WriteAllText(sdkManifestFile, sdkManifestContents1); + ITaskItem[] resolvedSDKReferences1 = RunBuildAndReturnResolvedSDKReferences(logger, testProjectFile, testDirectoryRoot); + Assert.IsTrue(resolvedSDKReferences1.Length == 1); + + Assert.IsTrue(resolvedSDKReferences1[0].GetMetadata("MoreInfo").Equals("http://msdn.microsoft.com/MySDK", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(resolvedSDKReferences1[0].GetMetadata("MaxPlatformVersion").Equals("9.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(resolvedSDKReferences1[0].GetMetadata("MinOSVersion").Equals("6.2.0", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(resolvedSDKReferences1[0].GetMetadata("MaxOSVersionTested").Equals("6.2.3", StringComparison.OrdinalIgnoreCase)); + + File.WriteAllText(sdkManifestFile, sdkManifestContents2); + ITaskItem[] resolvedSDKReferences2 = RunBuildAndReturnResolvedSDKReferences(logger, testProjectFile, testDirectoryRoot); + Assert.IsTrue(resolvedSDKReferences2.Length == 1); + + Assert.IsTrue(resolvedSDKReferences2[0].GetMetadata("MoreInfo").Equals("http://msdn.microsoft.com/MySDK", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(resolvedSDKReferences2[0].GetMetadata("MaxPlatformVersion").Equals(String.Empty)); + Assert.IsTrue(resolvedSDKReferences2[0].GetMetadata("MinOSVersion").Equals(String.Empty)); + Assert.IsTrue(resolvedSDKReferences2[0].GetMetadata("MaxOSVersionTested").Equals(String.Empty)); + } + finally + { + Environment.SetEnvironmentVariable("MSBUILDSDKREFERENCEDIRECTORY", null); + Environment.SetEnvironmentVariable("MSBUILDDISABLEREGISTRYFORSDKLOOKUP", null); + if (Directory.Exists(testDirectoryRoot)) + { + FileUtilities.DeleteDirectoryNoThrow(testDirectoryRoot, true); + } + } + } + + private ITaskItem[] RunBuildAndReturnResolvedSDKReferences(ILogger logger, string testProjectFile, string testDirectoryRoot) + { + ProjectCollection pc = new ProjectCollection(); + ProjectInstance project = pc.LoadProject(testProjectFile).CreateProjectInstance(); + project.SetProperty("SDKReferenceDirectoryRoot", testDirectoryRoot); + project.SetProperty("SDKReferenceRegistryRoot", ""); + + IDictionary targetResults = new Dictionary(); + bool success = project.Build(new string[] { "GetResolvedSDKReferences" }, new ILogger[] { logger }, out targetResults); + Assert.IsTrue(success); + + Assert.IsTrue(targetResults.ContainsKey("GetResolvedSDKReferences")); + TargetResult result = targetResults["GetResolvedSDKReferences"]; + return result.Items; + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/ResolveVCProjectOutput_Tests.cs b/src/XMakeTasks/UnitTests/ResolveVCProjectOutput_Tests.cs new file mode 100644 index 00000000000..2743a6551ff --- /dev/null +++ b/src/XMakeTasks/UnitTests/ResolveVCProjectOutput_Tests.cs @@ -0,0 +1,2129 @@ +/* +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using NUnit.Framework; + +namespace Microsoft.Build.UnitTests +{ + [TestFixture] + sealed public class ResolveVCProjectOutput_Tests + { + internal void VerifyVCProjectOutputs(string vcProjectContents, bool useOverride, string configuration, + string expectedAssemblyOutput, string expectedImportLibOutput) + { + ObjectModelHelpers.DeleteTempProjectDirectory(); + + ResolveVCProjectOutput resolveVCProjectOutput = new ResolveVCProjectOutput(); + resolveVCProjectOutput.BuildEngine = new MockEngine(); + + ObjectModelHelpers.CreateFileInTempProjectDirectory("cppproj.vcproj", vcProjectContents); + + if (useOverride) + { + string overrideContents = @" + + "; + + ObjectModelHelpers.CreateFileInTempProjectDirectory("VCOverride.vsprops", overrideContents); + + resolveVCProjectOutput.Override = new TaskItem(Path.Combine(ObjectModelHelpers.TempProjectDir, "VCOverride.vsprops")); + } + + resolveVCProjectOutput.ProjectReferences = new TaskItem[] { new TaskItem(Path.Combine(ObjectModelHelpers.TempProjectDir, "cppproj.vcproj")) }; + resolveVCProjectOutput.Configuration = configuration; + + Assertion.AssertEquals(true, resolveVCProjectOutput.Execute()); + + if (expectedAssemblyOutput != null) + { + Assertion.AssertEquals(1, resolveVCProjectOutput.ResolvedOutputPaths.GetLength(0)); + + Assertion.Assert(resolveVCProjectOutput.ResolvedOutputPaths[0].ItemSpec, + string.Compare(Path.Combine(ObjectModelHelpers.TempProjectDir, expectedAssemblyOutput), + resolveVCProjectOutput.ResolvedOutputPaths[0].ItemSpec, StringComparison.OrdinalIgnoreCase) == 0); + } + else + { + Assertion.AssertEquals(0, resolveVCProjectOutput.ResolvedOutputPaths.GetLength(0)); + } + + if (expectedImportLibOutput != null) + { + Assertion.AssertEquals(1, resolveVCProjectOutput.ResolvedImportLibraryPaths.GetLength(0)); + + Assertion.Assert(resolveVCProjectOutput.ResolvedImportLibraryPaths[0].ItemSpec, + string.Compare(Path.Combine(ObjectModelHelpers.TempProjectDir, expectedImportLibOutput), + resolveVCProjectOutput.ResolvedImportLibraryPaths[0].ItemSpec, StringComparison.OrdinalIgnoreCase) == 0); + } + else + { + Assertion.AssertEquals(0, resolveVCProjectOutput.ResolvedImportLibraryPaths.GetLength(0)); + } + } + + [Test] + public void OverrideOutputDirectory() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + #endregion + + VerifyVCProjectOutputs(vcProjectContents, true, "Debug|Win32", + @"Outputs\Debug\CppProj.dll", null); + } + + [Test] + public void ResolveNativeDllOutputs() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + #endregion + + // Standard import lib for VC native dll + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + null, @"Debug\NativeDll.lib"); + + // No import lib because IgnoreImportLibrary is set to Yes for the Release configuration + VerifyVCProjectOutputs(vcProjectContents, false, "Release|Win32", + null, null); + } + + [Test] + public void ResolveStaticLibOutputs() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + #endregion + + // Primary output for VC native static lib is an import library + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + null, @"Debug\StaticLib.lib"); + + VerifyVCProjectOutputs(vcProjectContents, true, "Debug|Win32", + null, @"Outputs\Debug\StaticLib.lib"); + } + + /// + /// Regress DDB #81422 + /// + [Test] + public void ResolveStaticLibOutputsWherePathIsQuoted() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + #endregion + + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + null, @"Debug\w32staticlib.lib"); + } + + [Test] + public void ResolveNativeExeOutputs() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + #endregion + + // No consumable outputs from an Exe project + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + null, null); + + // Release has no values for ConfigurationType and ManagedExtensions, which should default to a native .exe + VerifyVCProjectOutputs(vcProjectContents, false, "Release|Win32", + null, null); + } + + [Test] + public void ResolveManagedDllOutputs() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + #endregion + + // For normal managed dll, only primary output is consumable as an assembly ref + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + @"Debug\ManagedDll.dll", null); + + // The Release configuration is changed to generate an import lib (yes, it's possible even for a managed project) + // It's enough to set IgnoreImportLibrary to No to achieve this + VerifyVCProjectOutputs(vcProjectContents, false, "Release|Win32", + @"Release\ManagedDll.dll", @"Release\ManagedDll.lib"); + } + + [Test] + public void ResolveManagedExeOutputs() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + #endregion + + // For normal managed exe, only primary output is consumable as an assembly ref + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + @"Debug\ManagedExe.exe", null); + + // The Release configuration is changed to generate an import lib (yes, it's possible even for a managed project) + // It's enough to set IgnoreImportLibrary to No to achieve this + // However, we should always ignore import libs for .exe projects + VerifyVCProjectOutputs(vcProjectContents, false, "Release|Win32", + @"Release\ManagedExe.exe", null); + + // For normal managed exe, only primary output is consumable as an assembly ref + VerifyVCProjectOutputs(vcProjectContents, true, "Debug|Win32", + @"Outputs\Debug\ManagedExe.exe", null); + } + + [Test] + public void ResolveManagedDllOutputsManagedExtensionsOverridenInFiles() + { + string vcProjectContents = + #region VC project contents + @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + +#endregion + + // The project has its main ManagedExtensions property set to No, but the Debug configuration overrides it + // for individual files. + VerifyVCProjectOutputs(vcProjectContents, false, "Debug|Win32", + @"Debug\ClassLib.dll", @"Debug\ClassLib.lib"); + + // The Release configuration does not override the ManagedExtensions property. + VerifyVCProjectOutputs(vcProjectContents, false, "Release|Win32", + null, @"Release\ClassLib.lib"); + } + } +} +*/ \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/SGen_Tests.cs b/src/XMakeTasks/UnitTests/SGen_Tests.cs new file mode 100644 index 00000000000..3424f40a12e --- /dev/null +++ b/src/XMakeTasks/UnitTests/SGen_Tests.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using System.IO; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class SGen_Tests + { + internal class SGenExtension : SGen + { + internal string CommandLine() + { + return base.GenerateCommandLineCommands(); + } + } + + [TestMethod] + public void KeyFileQuotedOnCommandLineIfNecessary() + { + SGenExtension sgen = new SGenExtension(); + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + + // This should result in a nested, quoted parameter on + // the command line, which ultimately looks like this: + // + // /compiler:"/keyfile:\"c:\Some Folder\MyKeyFile.snk\"" + // + sgen.KeyFile = "c:\\Some Folder\\MyKeyFile.snk"; + + string commandLine = sgen.CommandLine(); + + Assert.IsTrue(commandLine.IndexOf("/compiler:\"/keyfile:\\\"" + sgen.KeyFile + "\\\"\"", StringComparison.OrdinalIgnoreCase) >= 0); + } + + [TestMethod] + public void TestKeepFlagTrue() + { + SGenExtension sgen = new SGenExtension(); + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + sgen.UseKeep = true; + + string commandLine = sgen.CommandLine(); + + Assert.IsTrue(commandLine.IndexOf("/keep", StringComparison.OrdinalIgnoreCase) >= 0); + } + [TestMethod] + public void TestKeepFlagFalse() + { + SGenExtension sgen = new SGenExtension(); + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + sgen.UseKeep = false; + + string commandLine = sgen.CommandLine(); + + Assert.IsTrue(commandLine.IndexOf("/keep", StringComparison.OrdinalIgnoreCase) < 0); + } + + + [TestMethod] + public void TestInputChecks1() + { + MockEngine engine = new MockEngine(); + SGenExtension sgen = new SGenExtension(); + sgen.BuildEngine = engine; + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + Path.GetInvalidPathChars()[0]; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + // This should result in a quoted parameter... + sgen.KeyFile = "c:\\Some Folder\\MyKeyFile.snk"; + string commandLine = sgen.CommandLine(); + Assert.IsTrue(engine.Errors == 1); + } + + [TestMethod] + public void TestInputChecks2() + { + MockEngine engine = new MockEngine(); + SGenExtension sgen = new SGenExtension(); + sgen.BuildEngine = engine; + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll" + Path.GetInvalidPathChars()[0]; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + // This should result in a quoted parameter... + sgen.KeyFile = "c:\\Some Folder\\MyKeyFile.snk"; + string commandLine = sgen.CommandLine(); + Assert.IsTrue(engine.Errors == 1); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestInputChecks3() + { + MockEngine engine = new MockEngine(); + SGenExtension sgen = new SGenExtension(); + sgen.BuildEngine = engine; + sgen.BuildAssemblyName = null; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + // This should result in a quoted parameter... + sgen.KeyFile = "c:\\Some Folder\\MyKeyFile.snk"; + string commandLine = sgen.CommandLine(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void TestInputChecks4() + { + MockEngine engine = new MockEngine(); + SGenExtension sgen = new SGenExtension(); + sgen.BuildEngine = engine; + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = null; + sgen.ShouldGenerateSerializer = true; + sgen.UseProxyTypes = false; + // This should result in a quoted parameter... + sgen.KeyFile = "c:\\Some Folder\\MyKeyFile.snk"; + + string commandLine = sgen.CommandLine(); + } + + [TestMethod] + public void TestInputPlatform() + { + SGenExtension sgen = new SGenExtension(); + sgen.Platform = "x86"; + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + + string commandLine = sgen.CommandLine(); + + Assert.IsTrue(String.Equals(commandLine, "/assembly:\"C:\\SomeFolder\\MyAsm.dll\\MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" /compiler:/platform:x86", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void TestInputTypes() + { + SGenExtension sgen = new SGenExtension(); + sgen.Types = new string[] { "System.String", "System.Boolean" }; + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + + string commandLine = sgen.CommandLine(); + + Assert.IsTrue(String.Equals(commandLine, "/assembly:\"C:\\SomeFolder\\MyAsm.dll\\MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\" /type:System.String /type:System.Boolean", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void TestInputEmptyTypesAndPlatform() + { + SGenExtension sgen = new SGenExtension(); + sgen.BuildAssemblyName = "MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + sgen.BuildAssemblyPath = "C:\\SomeFolder\\MyAsm.dll"; + sgen.ShouldGenerateSerializer = true; + + string commandLine = sgen.CommandLine(); + + Assert.IsTrue(String.Equals(commandLine, "/assembly:\"C:\\SomeFolder\\MyAsm.dll\\MyAsm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\"", StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/XMakeTasks/UnitTests/SampleResx b/src/XMakeTasks/UnitTests/SampleResx new file mode 100644 index 00000000000..ecd58f32ce5 --- /dev/null +++ b/src/XMakeTasks/UnitTests/SampleResx @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Value + + + diff --git a/src/XMakeTasks/UnitTests/SdkToolsPathUtility_Tests.cs b/src/XMakeTasks/UnitTests/SdkToolsPathUtility_Tests.cs new file mode 100644 index 00000000000..df0cc8d25e0 --- /dev/null +++ b/src/XMakeTasks/UnitTests/SdkToolsPathUtility_Tests.cs @@ -0,0 +1,364 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.Build.UnitTests; +using System.IO; +using Microsoft.Build.Tasks; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class SdkToolsPathUtility_Tests + { + private string _defaultSdkToolsPath = "C:\\ProgramFiles\\WIndowsSDK\\bin"; + private TaskLoggingHelper _log = null; + private string _toolName = "MyTool.exe"; + private MockEngine _mockEngine = null; + private MockFileExists _mockExists = null; + + [TestInitialize] + public void Setup() + { + // Create a delegate helper to make the testing of a method which uses a lot of fileExists a bit easier + _mockExists = new MockFileExists(_defaultSdkToolsPath); + + // We need an engine to see any logging messages the method may log + _mockEngine = new MockEngine(); + + // Dummy task to get a TaskLoggingHelper + TaskToLogFrom loggingTask = new TaskToLogFrom(); + loggingTask.BuildEngine = _mockEngine; + _log = loggingTask.Log; + _log.TaskResources = AssemblyResources.PrimaryResources; + } + + + #region Misc + /// + /// Test the case where the sdkToolsPath is null or empty + /// + [TestMethod] + public void GeneratePathToToolNullOrEmptySdkToolPath() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInX86, ProcessorArchitecture.X86, null, _toolName, _log, true); + Assert.IsNull(toolPath); + + string comment = ResourceUtilities.FormatResourceString("General.SdkToolsPathNotSpecifiedOrToolDoesNotExist", _toolName, null); + _mockEngine.AssertLogContains(comment); + Assert.AreEqual(0, _mockEngine.Warnings); + + comment = ResourceUtilities.FormatResourceString("General.SdkToolsPathToolDoesNotExist", _toolName, null, ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45)); + _mockEngine.AssertLogContains(comment); + Assert.AreEqual(1, _mockEngine.Errors); + } + + /// + /// Test the case where the sdkToolsPath is null or empty and we do not want to log errors or warnings + /// + [TestMethod] + public void GeneratePathToToolNullOrEmptySdkToolPathNoLogging() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInX86, ProcessorArchitecture.X86, null, _toolName, _log, false); + Assert.IsNull(toolPath); + + string comment = ResourceUtilities.FormatResourceString("General.SdkToolsPathNotSpecifiedOrToolDoesNotExist", _toolName, null); + _mockEngine.AssertLogDoesntContain(comment); + Assert.AreEqual(0, _mockEngine.Warnings); + + comment = ResourceUtilities.FormatResourceString("General.SdkToolsPathToolDoesNotExist", _toolName, null, ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45)); + _mockEngine.AssertLogDoesntContain(comment); + Assert.AreEqual(0, _mockEngine.Errors); + } + + #endregion + + #region Test x86 + /// + /// Test the case where the processor architecture is x86 and the tool exists in the x86 sdk path + /// + [TestMethod] + public void GeneratePathToToolX86ExistsOnx86() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInX86, ProcessorArchitecture.X86, _defaultSdkToolsPath, _toolName, _log, true); + + // Path we expect to get out of the method + string expectedPath = Path.Combine(_defaultSdkToolsPath, _toolName); + + // Message to show when the test fails. + string message = "Expected to find the tool in the defaultSdkToolsPath but the method returned:" + toolPath; + Assert.IsTrue(string.Equals(expectedPath, toolPath, StringComparison.OrdinalIgnoreCase), message); + Assert.IsTrue(String.IsNullOrEmpty(_mockEngine.Log)); + } + + + #endregion + + #region Test x64 + /// + /// Test the case where the processor architecture is x64 and the tool exists in the x64 sdk path + /// + [TestMethod] + public void GeneratePathToToolX64ExistsOnx64() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInX64, ProcessorArchitecture.AMD64, _defaultSdkToolsPath, _toolName, _log, true); + + // Path we expect to get out of the method + string expectedPath = Path.Combine(_defaultSdkToolsPath, "x64"); + expectedPath = Path.Combine(expectedPath, _toolName); + + // Message to show when the test fails. + string message = "Expected to find the tool in " + expectedPath + " but the method returned:" + toolPath; + Assert.IsTrue(string.Equals(expectedPath, toolPath, StringComparison.OrdinalIgnoreCase), message); + Assert.IsTrue(String.IsNullOrEmpty(_mockEngine.Log)); + } + + /// + /// Test the case where the processor architecture is x64 and the tool does not exists in the x64 sdk path but does exist in the x86 path + /// + [TestMethod] + public void GeneratePathToToolX64ExistsOnx86() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInX86, ProcessorArchitecture.AMD64, _defaultSdkToolsPath, _toolName, _log, true); + + // Path we expect to get out of the method + string expectedPath = Path.Combine(_defaultSdkToolsPath, _toolName); + + // Message to show when the test fails. + string message = "Expected to find the tool in " + expectedPath + " but the method returned:" + toolPath; + Assert.IsTrue(string.Equals(expectedPath, toolPath, StringComparison.OrdinalIgnoreCase), message); + Assert.IsTrue(String.IsNullOrEmpty(_mockEngine.Log)); + } + #endregion + + #region Test Ia64 + /// + /// Test the case where the processor architecture is ia64 and the tool exists in the ia64 sdk path + /// + [TestMethod] + public void GeneratePathToToolIa64ExistsOnIa64() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInIa64, ProcessorArchitecture.IA64, _defaultSdkToolsPath, _toolName, _log, true); + + // Path we expect to get out of the method + string expectedPath = Path.Combine(_defaultSdkToolsPath, "ia64"); + expectedPath = Path.Combine(expectedPath, _toolName); + + // Message to show when the test fails. + string message = "Expected to find the tool in " + expectedPath + " but the method returned:" + toolPath; + Assert.IsTrue(string.Equals(expectedPath, toolPath, StringComparison.OrdinalIgnoreCase), message); + Assert.IsTrue(String.IsNullOrEmpty(_mockEngine.Log)); + } + + /// + /// Test the case where the processor architecture is ia64 and the tool does not exists in the ia64 sdk path but does exist in the x86 path + /// + [TestMethod] + public void GeneratePathToToolIa64ExistsOnx86() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileExistsOnlyInX86, ProcessorArchitecture.IA64, _defaultSdkToolsPath, _toolName, _log, true); + + // Path we expect to get out of the method + string expectedPath = Path.Combine(_defaultSdkToolsPath, _toolName); + + // Message to show when the test fails. + string message = "Expected to find the tool in " + expectedPath + " but the method returned:" + toolPath; + Assert.IsTrue(string.Equals(expectedPath, toolPath, StringComparison.OrdinalIgnoreCase), message); + Assert.IsTrue(String.IsNullOrEmpty(_mockEngine.Log)); + } + #endregion + + + /// + /// Test the case where the processor architecture is x86 and the tool does not exist in the x86 sdk path (or anywhere for that matter) + /// + [TestMethod] + public void GeneratePathToToolX86DoesNotExistAnywhere() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileDoesNotExist, ProcessorArchitecture.X86, _defaultSdkToolsPath, _toolName, _log, true); + Assert.IsNull(toolPath); + + string comment = ResourceUtilities.FormatResourceString("General.PlatformSDKFileNotFoundSdkToolsPath", _toolName, _defaultSdkToolsPath, _defaultSdkToolsPath); + _mockEngine.AssertLogContains(comment); + + comment = ResourceUtilities.FormatResourceString("General.SdkToolsPathToolDoesNotExist", _toolName, _defaultSdkToolsPath, ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45)); + _mockEngine.AssertLogContains(comment); + Assert.AreEqual(1, _mockEngine.Errors); + } + + /// + /// Test the case where there are illegal chars in the sdktoolspath and Path.combine has a problem. + /// + [TestMethod] + public void VerifyErrorWithIllegalChars() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileDoesNotExist, ProcessorArchitecture.X86, "./?><;)(*&^%$#@!", _toolName, _log, true); + Assert.IsNull(toolPath); + _mockEngine.AssertLogContains("MSB3666"); + Assert.AreEqual(1, _mockEngine.Errors); + } + + /// + /// Test the case where the processor architecture is x86 and the tool does not exist in the x86 sdk path (or anywhere for that matter)and we do not want to log + /// + [TestMethod] + public void GeneratePathToToolX86DoesNotExistAnywhereNoLogging() + { + string toolPath = SdkToolsPathUtility.GeneratePathToTool(_mockExists.MockFileDoesNotExist, ProcessorArchitecture.X86, _defaultSdkToolsPath, _toolName, _log, false); + Assert.IsNull(toolPath); + + string comment = ResourceUtilities.FormatResourceString("General.PlatformSDKFileNotFoundSdkToolsPath", _toolName, _defaultSdkToolsPath, _defaultSdkToolsPath); + _mockEngine.AssertLogDoesntContain(comment); + + comment = ResourceUtilities.FormatResourceString("General.SdkToolsPathToolDoesNotExist", _toolName, _defaultSdkToolsPath, ToolLocationHelper.GetDotNetFrameworkSdkRootRegistryKey(TargetDotNetFrameworkVersion.Version45)); + _mockEngine.AssertLogDoesntContain(comment); + Assert.AreEqual(0, _mockEngine.Errors); + } + + #region Helper Classes + // Task just so we can access to a real taskLogging helper and inspect the log. + internal class TaskToLogFrom : Task + { + /// + /// Empty execute, this task will never be executed + /// + /// + public override bool Execute() + { + throw new NotImplementedException(); + } + } + + /// + /// This class is used for testing the ability of the SdkToolsPathUtility class to handel situations when + /// the toolname exists or does not exist. + /// + internal class MockFileExists + { + #region Data + /// + /// Path to the x86 sdk tools location + /// + private string _sdkToolsPath = null; + #endregion + + #region Constructor + + /// + /// This class gives the ability to create a fileexists delegate which helps in testing the sdktoolspath utility class + /// which makes extensive use of fileexists. + /// The sdkToolsPath is the expected location of the x86 sdk directory. + /// + public MockFileExists(string sdkToolsPath) + { + _sdkToolsPath = sdkToolsPath; + } + #endregion + + #region Properties + /// + /// A file exists object that will only return true if path passed in is the sdkToolsPath + /// + public FileExists MockFileExistsOnlyInX86 + { + get + { + return new FileExists(ExistsOnlyInX86); + } + } + + /// + /// A file exists object that will only return true if path passed in is the sdkToolsPath\X64 + /// + public FileExists MockFileExistsOnlyInX64 + { + get + { + return new FileExists(ExistsOnlyInX64); + } + } + + /// + /// A file exists object that will only return true if path passed in is the sdkToolsPath\Ia64 + /// + public FileExists MockFileExistsOnlyInIa64 + { + get + { + return new FileExists(ExistsOnlyInIa64); + } + } + + /// + /// File exists delegate which will always return true + /// + public FileExists MockFileExistsInAll + { + get + { + return new FileExists(ExistsInAll); + } + } + + /// + /// File Exists delegate which will always return false + /// + public FileExists MockFileDoesNotExist + { + get + { + return new FileExists(DoesNotExist); + } + } + #endregion + + #region FileExists Methods + /// + /// A file exists object that will only return true if path passed in is the sdkToolsPath + /// + private bool ExistsOnlyInX86(string filePath) + { + return string.Equals(Path.GetDirectoryName(filePath), _sdkToolsPath, StringComparison.OrdinalIgnoreCase); + } + + /// + /// A file exists object that will only return true if path passed in is the sdkToolsPath\x64 + /// + private bool ExistsOnlyInX64(string filePath) + { + return string.Equals(Path.GetDirectoryName(filePath), _sdkToolsPath + "\\x64", StringComparison.OrdinalIgnoreCase); + } + + /// + /// A file exists object that will only return true if path passed in is the sdkToolsPath + /// + private bool ExistsOnlyInIa64(string filePath) + { + return string.Equals(Path.GetDirectoryName(filePath), _sdkToolsPath + "\\ia64", StringComparison.OrdinalIgnoreCase); + } + + /// + /// File Exists delegate which will always return true + /// + private bool ExistsInAll(string filePath) + { + return true; + } + + /// + /// File Exists delegate which will always return false + /// + private bool DoesNotExist(string filePath) + { + return false; + } + #endregion + } + #endregion + } +} diff --git a/src/XMakeTasks/UnitTests/StreamHelpers.cs b/src/XMakeTasks/UnitTests/StreamHelpers.cs new file mode 100644 index 00000000000..1b773cd63ad --- /dev/null +++ b/src/XMakeTasks/UnitTests/StreamHelpers.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests +{ + sealed internal class StreamHelpers + { + /* + * Method: StringToStream (overload) + * + * Take a string and convert it into a Stream. + * Use the default encoding which means this machine's ANSI codepage. + */ + static internal Stream StringToStream(string value) + { + return StringToStream(value, System.Text.Encoding.Default); // We want this to be Default which is ANSI + } + + /* + * Method: StringToStream (overload) + * + * Take a string and convert it into a Stream. + * Takes an alternate encoding type + */ + static internal Stream StringToStream(string value, System.Text.Encoding encoding) + { + MemoryStream m = new MemoryStream(); + TextWriter w = new StreamWriter(m, encoding); // HIGHCHAR: StringToStream helper accepts encoding from caller. + + w.Write(value); + w.Flush(); + m.Seek(0, SeekOrigin.Begin); + return m; + } + } +} diff --git a/src/XMakeTasks/UnitTests/StreamMappedString_Tests.cs b/src/XMakeTasks/UnitTests/StreamMappedString_Tests.cs new file mode 100644 index 00000000000..34f929b1df7 --- /dev/null +++ b/src/XMakeTasks/UnitTests/StreamMappedString_Tests.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using Microsoft.Build.Shared.LanguageParser; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class StreamMappedString_Tests + { + /// + /// Test for a string that has ANSI but non-ascii characters. + /// + [TestMethod] + public void Regress_Mutation_ForceANSIWorks_RelatedTo172107() + { + // Can't embed the 'Ã' directly because the string is Unicode already and the Unicode<-->ANSI transform + // isn't bidirectional. + MemoryStream sourcesStream = (MemoryStream)StreamHelpers.StringToStream("namespace d?a { class Class {} }"); + + // Instead, directly write the ANSI character into the memory buffer. + sourcesStream.Seek(11, SeekOrigin.Begin); + sourcesStream.WriteByte(0xc3); // Plug the 'Ã' in + sourcesStream.Seek(0, SeekOrigin.Begin); + + // Should not throw an exception because we force ANSI. + StreamMappedString s = new StreamMappedString(sourcesStream, /* forceANSI */ true); + s.GetAt(11); + } + + [TestMethod] + public void Regress_Mutation_BackingUpMoreThanOnePageWorks() + { + Stream stream = StreamHelpers.StringToStream("A" + new String('b', StreamMappedString.DefaultPageSize * 4)); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get the last character... + s.GetAt(StreamMappedString.DefaultPageSize * 4); + + // ...now get the first character. + Assert.AreEqual('A', s.GetAt(0)); + } + + [TestMethod] + public void Regress_Mutation_RetrievingFromLastPageWorks() + { + Stream stream = StreamHelpers.StringToStream("A" + new String('b', StreamMappedString.DefaultPageSize)); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get the last character... + s.GetAt(StreamMappedString.DefaultPageSize); + + // ...now get the first character (which should be saved on lastPage). + Assert.AreEqual('A', s.GetAt(0)); + } + + [TestMethod] + public void Regress_Mutation_LastCharacterShouldBeNewLine() + { + Stream stream = StreamHelpers.StringToStream("A"); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get the last character (which should be the appended newLine). + Assert.AreEqual('\xd', s.GetAt(1)); + } + + [TestMethod] + public void Regress_Mutation_1AShouldBeStripped() + { + Stream stream = StreamHelpers.StringToStream("x\x1Ay"); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get the last character (which should be 'y' and not 0x1A). + Assert.AreEqual('y', s.GetAt(1)); + } + + [TestMethod] + public void Regress_Mutation_MultiplePagesOf1AShouldBeStripped() + { + Stream stream = StreamHelpers.StringToStream(new String('\x1a', StreamMappedString.DefaultPageSize * 2) + "x"); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get the last character (which should be 'x' and not 0x1A). + Assert.AreEqual('x', s.GetAt(0)); + } + + [TestMethod] + public void Regress_Mutation_NewLineGetsAppendedAcrossPageBoundaries() + { + Stream stream = StreamHelpers.StringToStream(new String('x', StreamMappedString.DefaultPageSize)); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get the last character (which should be '\xd'). + Assert.AreEqual('\xd', s.GetAt(StreamMappedString.DefaultPageSize)); + } + + [TestMethod] + public void Regress_Mutation_SubstringWorks() + { + Stream stream = StreamHelpers.StringToStream("abcdefg"); + StreamMappedString s = new StreamMappedString(stream, false); + + Assert.AreEqual("bcd", s.Substring(1, 3)); + } + + [TestMethod] + public void Regress_Mutation_SubstringWorksWithPageSizeOne() + { + Stream stream = StreamHelpers.StringToStream("abcdefg"); + StreamMappedString s = new StreamMappedString(stream, false, /* pageSize */ 1); + + Assert.AreEqual("bcd", s.Substring(1, 3)); + } + + [TestMethod] + public void Regress_Mutation_SubstringWorksFromPriorPage() + { + Stream stream = StreamHelpers.StringToStream("abcxdef"); + StreamMappedString s = new StreamMappedString(stream, false, 7); + + // Move to the last page + s.GetAt(7); + + // And then extract a string from the beginning page. + Assert.AreEqual("abcxdef", s.Substring(0, 7)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void Regress_Mutation_SubstringReadPastEndThrowsException() + { + Stream stream = StreamHelpers.StringToStream("abcdefg"); + StreamMappedString s = new StreamMappedString(stream, false); + + Assert.AreEqual(String.Empty, s.Substring(1, 30)); + } + + [TestMethod] + public void Regress_Mutation_SubstringOnLastPageWorks() + { + Stream stream = StreamHelpers.StringToStream("abcdefg" + new String('x', StreamMappedString.DefaultPageSize)); + StreamMappedString s = new StreamMappedString(stream, false); + + // Move to the second page + s.GetAt(StreamMappedString.DefaultPageSize); + + // Get a string from the firstPage + Assert.AreEqual("abc", s.Substring(0, 3)); + } + + [TestMethod] + public void Regress_Mutation_UnicodeIsDetected() + { + Stream stream = StreamHelpers.StringToStream("\u00C3ngelo's Steak House", System.Text.Encoding.UTF32); + StreamMappedString s = new StreamMappedString(stream, false); + + // This won't read correctly with ANSI encoding. + Assert.AreEqual('\u00C3', s.GetAt(0)); + } + + [TestMethod] + public void Regress_Mutation_ReadingCharactersForwardOnlyShouldCauseNoAdditionalResets() + { + RestartCountingStream stream = new RestartCountingStream(StreamHelpers.StringToStream("abcdefg")); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get a few characters. + s.GetAt(0); + s.GetAt(1); + s.GetAt(2); + s.GetAt(3); + s.GetAt(4); + + // There should be exactly one reset for this. + Assert.AreEqual(0, stream.ResetCount); + } + + [TestMethod] + public void Regress_Mutation_IsPastEndWorks() + { + RestartCountingStream stream = new RestartCountingStream(StreamHelpers.StringToStream("a")); + StreamMappedString s = new StreamMappedString(stream, false); + + // There's only one character, so IsPastEnd(2) should be true. + Assert.IsTrue(s.IsPastEnd(2)); // <-- 2 required because of extra \xd added. + } + + [TestMethod] + public void Regress_Mutation_MinimizePagesAllocated() + { + Stream stream = StreamHelpers.StringToStream("a" + new String('x', StreamMappedString.DefaultPageSize * 2)); + StreamMappedString s = new StreamMappedString(stream, false); + + // Get a few characters. + s.GetAt(0); + s.GetAt(StreamMappedString.DefaultPageSize); + s.GetAt(StreamMappedString.DefaultPageSize * 2); + + // Even though three pages were read, only two allocations should have occurred. + Assert.AreEqual(2, s.PagesAllocated); + } + + [TestMethod] + public void Regress_Mutation_1DNotAppendedIfAlreadyThere() + { + RestartCountingStream stream = new RestartCountingStream(StreamHelpers.StringToStream("\xd")); + StreamMappedString s = new StreamMappedString(stream, false); + + // There's only one \x1d so IsPastEnd(1) should be true. + Assert.IsTrue(s.IsPastEnd(1)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void Regress_Codereview_RequestPageWellPastEnd() + { + Stream stream = StreamHelpers.StringToStream("x"); + StreamMappedString s = new StreamMappedString(stream, false); + + // Read something way past the end. This should result in a range exception. + s.GetAt(1000000); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void Regress_Mutation_FirstCharacterOnPagePastEndDoesntExist() + { + Stream stream = StreamHelpers.StringToStream("abc"); + StreamMappedString s = new StreamMappedString(stream, false, 256); + + s.GetAt(256); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void Regress_Mutation_RequestPageWellPastEnd() + { + Stream stream = StreamHelpers.StringToStream(new String('x', StreamMappedString.DefaultPageSize * 2)); + StreamMappedString s = new StreamMappedString(stream, false); + + // Read something way past the end. This should result in a range exception. + s.GetAt(1000000); + } + + + + /// + /// A stream class that counts the number of times it was reset. + /// + private class RestartCountingStream : Stream + { + private int _resetCount; + private Stream _stream; + + public RestartCountingStream(Stream stream) + { + _stream = stream; + } + + /// + /// Returns the number of times this stream was reset. + /// + public int ResetCount + { + get { return _resetCount; } + } + + public override bool CanRead + { + get { return _stream.CanRead; } + } + + public override bool CanSeek + { + get { throw new Exception("The method or operation is not implemented."); } + } + + public override bool CanWrite + { + get { throw new Exception("The method or operation is not implemented."); } + } + + public override void Flush() + { + throw new Exception("The method or operation is not implemented."); + } + + public override long Length + { + get { throw new Exception("The method or operation is not implemented."); } + } + + public override long Position + { + get + { + return _stream.Position; + } + set + { + throw new Exception("The method or operation is not implemented."); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + ++_resetCount; + return this.Seek(offset, origin); + } + + public override void SetLength(long value) + { + throw new Exception("The method or operation is not implemented."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new Exception("The method or operation is not implemented."); + } + } + } +} + + + + + diff --git a/src/XMakeTasks/UnitTests/TlbImp_Tests.cs b/src/XMakeTasks/UnitTests/TlbImp_Tests.cs new file mode 100644 index 00000000000..7e8e4df3dc4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/TlbImp_Tests.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +namespace Microsoft.Build.UnitTests.AxTlbImp_Tests +{ + [TestClass] + sealed public class TlbImp_Tests + { + /// + /// Tests that /machine flag will be set. + /// + [TestMethod] + public void Machine() + { + var t = new ResolveComReference.TlbImp(); + Assert.IsNull(t.Machine, "Machine should be null by default"); + + t.Machine = "Agnostic"; + Assert.AreEqual("Agnostic", t.Machine, "New TypeLibName value should be set"); + CommandLine.ValidateHasParameter(t, "/machine:Agnostic", false /* no response file */); + } + + /// + /// Check ReferenceFiles + /// + [TestMethod] + public void ReferenceFiles() + { + var t = new ResolveComReference.TlbImp(); + Assert.IsNull(t.ReferenceFiles, "ReferenceFiles should be null by default"); + + t.ReferenceFiles = new string[] { "File1.dll", "File2.dll" }; + CommandLine.ValidateHasParameter(t, "/reference:File1.dll", false /* no response file */); + CommandLine.ValidateHasParameter(t, "/reference:File2.dll", false /* no response file */); + } + /// + /// Tests that the assembly being imported is passed to the command line + /// + [TestMethod] + public void TypeLibName() + { + var t = new ResolveComReference.TlbImp(); + string testParameterValue = "Interop.Foo.dll"; + + Assert.IsNull(t.TypeLibName, "TypeLibName should be null by default"); + + t.TypeLibName = testParameterValue; + Assert.AreEqual(testParameterValue, t.TypeLibName, "New TypeLibName value should be set"); + CommandLine.ValidateHasParameter(t, testParameterValue, false /* no response file */); + } + + /// + /// Tests that the assembly being imported is passed to the command line + /// + [TestMethod] + public void TypeLibNameWithSpaces() + { + var t = new ResolveComReference.TlbImp(); + string testParameterValue = @"c:\Program Files\Interop.Foo.dll"; + + Assert.IsNull(t.TypeLibName, "TypeLibName should be null by default"); + + t.TypeLibName = testParameterValue; + Assert.AreEqual(testParameterValue, t.TypeLibName, "New TypeLibName value should be set"); + CommandLine.ValidateHasParameter(t, testParameterValue, false /* no response file */); + } + + /// + /// Tests the /namespace: command line option + /// + [TestMethod] + public void AssemblyNamespace() + { + var t = new ResolveComReference.TlbImp(); + string testParameterValue = "Microsoft.Build.Foo"; + + Assert.IsNull(t.AssemblyNamespace, "AssemblyNamespace should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/namespace:", false /* no response file */); + + t.AssemblyNamespace = testParameterValue; + Assert.AreEqual(testParameterValue, t.AssemblyNamespace, "New AssemblyNamespace value should be set"); + CommandLine.ValidateHasParameter(t, @"/namespace:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /asmversion: command line option + /// + [TestMethod] + public void AssemblyVersion() + { + var t = new ResolveComReference.TlbImp(); + Version testParameterValue = new Version(2, 12); + + Assert.IsNull(t.AssemblyVersion, "AssemblyVersion should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/asmversion:", false /* no response file */); + + t.AssemblyVersion = testParameterValue; + Assert.AreEqual(testParameterValue, t.AssemblyVersion, "New AssemblyNamespace value should be set"); + CommandLine.ValidateHasParameter(t, @"/asmversion:" + testParameterValue.ToString(), false /* no response file */); + } + + /// + /// Tests the /nologo switch + /// + [TestMethod] + public void NoLogo() + { + var t = new ResolveComReference.TlbImp(); + + Assert.IsFalse(t.NoLogo, "NoLogo should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/nologo", false /* no response file */); + + t.NoLogo = true; + Assert.IsTrue(t.NoLogo, "NoLogo should be true"); + CommandLine.ValidateHasParameter(t, @"/nologo", false /* no response file */); + } + + /// + /// Tests the /out: switch + /// + [TestMethod] + public void OutputAssembly() + { + var t = new ResolveComReference.TlbImp(); + string testParameterValue = "AxInterop.Foo.dll"; + + Assert.IsNull(t.OutputAssembly, "OutputAssembly should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/out:", false /* no response file */); + + t.OutputAssembly = testParameterValue; + Assert.AreEqual(testParameterValue, t.OutputAssembly, "New OutputAssembly value should be set"); + CommandLine.ValidateHasParameter(t, @"/out:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /out: switch, with a space in the output file + /// + [TestMethod] + public void OutputAssemblyWithSpaces() + { + var t = new ResolveComReference.TlbImp(); + string testParameterValue = @"c:\Program Files\AxInterop.Foo.dll"; + + Assert.IsNull(t.OutputAssembly, "OutputAssembly should be null by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/out:", false /* no response file */); + + t.OutputAssembly = testParameterValue; + Assert.AreEqual(testParameterValue, t.OutputAssembly, "New OutputAssembly value should be set"); + CommandLine.ValidateHasParameter(t, @"/out:" + testParameterValue, false /* no response file */); + } + + /// + /// Tests the /noclassmembers switch + /// + [TestMethod] + public void PreventClassMembers() + { + var t = new ResolveComReference.TlbImp(); + + Assert.IsFalse(t.PreventClassMembers, "PreventClassMembers should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/noclassmembers", false /* no response file */); + + t.PreventClassMembers = true; + Assert.IsTrue(t.PreventClassMembers, "PreventClassMembers should be true"); + CommandLine.ValidateHasParameter(t, @"/noclassmembers", false /* no response file */); + } + + /// + /// Tests the /sysarray switch + /// + [TestMethod] + public void SafeArrayAsSystemArray() + { + var t = new ResolveComReference.TlbImp(); + + Assert.IsFalse(t.SafeArrayAsSystemArray, "SafeArrayAsSystemArray should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/sysarray", false /* no response file */); + + t.SafeArrayAsSystemArray = true; + Assert.IsTrue(t.SafeArrayAsSystemArray, "SafeArrayAsSystemArray should be true"); + CommandLine.ValidateHasParameter(t, @"/sysarray", false /* no response file */); + } + + /// + /// Tests the /silent switch + /// + [TestMethod] + public void Silent() + { + var t = new ResolveComReference.TlbImp(); + + Assert.IsFalse(t.Silent, "Silent should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/silent", false /* no response file */); + + t.Silent = true; + Assert.IsTrue(t.Silent, "Silent should be true"); + CommandLine.ValidateHasParameter(t, @"/silent", false /* no response file */); + } + + /// + /// Tests the /transform: switch + /// + [TestMethod] + public void Transform() + { + var t = new ResolveComReference.TlbImp(); + + var dispRet = ResolveComReference.TlbImpTransformFlags.TransformDispRetVals; + var serialize = ResolveComReference.TlbImpTransformFlags.SerializableValueClasses; + var both = ResolveComReference.TlbImpTransformFlags.TransformDispRetVals | ResolveComReference.TlbImpTransformFlags.SerializableValueClasses; + + t.TypeLibName = "SomeRandomControl.tlb"; + t.ToolPath = Path.GetTempPath(); + + Assert.AreEqual(ResolveComReference.TlbImpTransformFlags.None, t.Transform, "Transform should be TlbImpTransformFlags.None by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/transform:", false /* no response file */); + + t.Transform = dispRet; + Assert.AreEqual(dispRet, t.Transform, "New Transform value should be set"); + CommandLine.ValidateHasParameter(t, @"/transform:DispRet", false /* no response file */); + + t.Transform = serialize; + Assert.AreEqual(serialize, t.Transform, "New Transform value should be set"); + CommandLine.ValidateHasParameter(t, @"/transform:SerializableValueClasses", false /* no response file */); + + t.Transform = both; + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "TlbImp.InvalidTransformParameter", t.Transform); + } + + /// + /// Tests the /verbose switch + /// + [TestMethod] + public void Verbose() + { + var t = new ResolveComReference.TlbImp(); + + Assert.IsFalse(t.Verbose, "Verbose should be false by default"); + CommandLine.ValidateNoParameterStartsWith(t, @"/verbose", false /* no response file */); + + t.Verbose = true; + Assert.IsTrue(t.Verbose, "Verbose should be true"); + CommandLine.ValidateHasParameter(t, @"/verbose", false /* no response file */); + } + + /// + /// Tests that task does the right thing (fails) when no .ocx file is passed to it + /// + [TestMethod] + public void TaskFailsWithNoInputs() + { + var t = new ResolveComReference.TlbImp(); + + Utilities.ExecuteTaskAndVerifyLogContainsErrorFromResource(t, "TlbImp.NoInputFileSpecified"); + } + } +} diff --git a/src/XMakeTasks/UnitTests/ToolTaskExtension_Tests.cs b/src/XMakeTasks/UnitTests/ToolTaskExtension_Tests.cs new file mode 100644 index 00000000000..c1ada4f4ee1 --- /dev/null +++ b/src/XMakeTasks/UnitTests/ToolTaskExtension_Tests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Resources; +using System.Reflection; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + public class ToolTaskExtensionTests + { + /// + /// Verify that the resources in ToolTask/derived classes work correctly (are accessible with correct resource managers) + /// With moving ToolTask into Utilities, tasks inheriting from it now have to deal with 3 (THREE!) resource streams, + /// which has a lot of potential for breaking. Make sure that tasks can access all of them using the correct logger helpers. + /// + [TestMethod] + public void TestResourceAccess() + { + Csc t = new Csc(); + MockEngine engine = new MockEngine(); + + t.BuildEngine = engine; + + // No need to actually check the outputted strings. We only care that this doesn't throw, which means that + // the resource strings were reachable. + + // Normal CSC messages first, from private XMakeTasks resources. They should be accessible with t.Log + t.Log.LogErrorWithCodeFromResources("Csc.AssemblyAliasContainsIllegalCharacters", "PlanetSide", "Knights of the Old Republic"); + t.Log.LogWarningWithCodeFromResources("Csc.InvalidParameter"); + t.Log.LogMessageFromResources("Vbc.ParameterHasInvalidValue", "Rome Total War", "Need for Speed Underground"); + + // Now shared messages. Should be accessible with the private LogShared property + PropertyInfo logShared = typeof(ToolTask).GetProperty("LogShared", BindingFlags.Instance | BindingFlags.NonPublic); + TaskLoggingHelper log = (TaskLoggingHelper)logShared.GetValue(t, null); + log.LogWarningWithCodeFromResources("Shared.FailedCreatingTempFile", "Gothic II"); + log.LogMessageFromResources("Shared.CannotConvertStringToBool", "foo"); + + // Now private Utilities messages. Should be accessible with the private LogPrivate property + PropertyInfo logPrivate = typeof(ToolTask).GetProperty("LogPrivate", BindingFlags.Instance | BindingFlags.NonPublic); + log = (TaskLoggingHelper)logPrivate.GetValue(t, null); + log.LogErrorWithCodeFromResources("ToolTask.CommandTooLong", "Painkiller"); + log.LogWarningWithCodeFromResources("ToolTask.CouldNotStartToolExecutable", "Fallout Tactics", "Fallout 2"); + log.LogMessageFromResources("ToolsLocationHelper.InvalidRedistFile", "Deus Ex", "Fallout"); + } + + /// + /// Verify that the above method actually tests something, that is make sure that non-existent resources throw + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ResourceAccessSanityCheck() + { + Csc t = new Csc(); + MockEngine engine = new MockEngine(); + + t.BuildEngine = engine; + t.Log.LogErrorFromResources("Beyond Good and Evil"); + } + + /// + /// Retrieve a non-existent value but ask for a default. + /// + [TestMethod] + public void GetNonExistentBoolWithDefault() + { + Csc t = new Csc(); + Assert.AreEqual(5, t.GetIntParameterWithDefault("Key", 5)); + } + + /// + /// Retrieve a value that exists, but ask for a default. We expect the + /// real value to win. + /// + [TestMethod] + public void GetBoolWithDefault() + { + Csc t = new Csc(); + t.Bag["Key"] = true; + + Assert.AreEqual(true, t.GetBoolParameterWithDefault("Key", false)); + } + + /// + /// Retrieve a value that exists, but ask for a default. We expect the + /// real value to win. + /// + [TestMethod] + public void GetIntWithDefault() + { + Csc t = new Csc(); + t.Bag["Key"] = 5; + + Assert.AreEqual(5, t.GetIntParameterWithDefault("Key", 9)); + } + } +} diff --git a/src/XMakeTasks/UnitTests/Touch_Tests.cs b/src/XMakeTasks/UnitTests/Touch_Tests.cs new file mode 100644 index 00000000000..af9abb2935d --- /dev/null +++ b/src/XMakeTasks/UnitTests/Touch_Tests.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class Touch_Tests + { + internal static Microsoft.Build.Shared.FileExists fileExists = new Microsoft.Build.Shared.FileExists(FileExists); + internal static Microsoft.Build.Shared.FileCreate fileCreate = new Microsoft.Build.Shared.FileCreate(FileCreate); + internal static Microsoft.Build.Tasks.GetAttributes fileGetAttributes = new Microsoft.Build.Tasks.GetAttributes(GetAttributes); + internal static Microsoft.Build.Tasks.SetAttributes fileSetAttributes = new Microsoft.Build.Tasks.SetAttributes(SetAttributes); + internal static Microsoft.Build.Tasks.SetLastAccessTime setLastAccessTime = new Microsoft.Build.Tasks.SetLastAccessTime(SetLastAccessTime); + internal static Microsoft.Build.Tasks.SetLastWriteTime setLastWriteTime = new Microsoft.Build.Tasks.SetLastWriteTime(SetLastWriteTime); + + private bool Execute(Touch t) + { + return t.ExecuteImpl + ( + fileExists, + fileCreate, + fileGetAttributes, + fileSetAttributes, + setLastAccessTime, + setLastWriteTime + ); + } + + /// + /// Mock file exists. + /// + /// + /// + private static bool FileExists(string path) + { + if (path == @"c:\touch\myexisting.txt") + { + return true; + } + + if (path == @"c:\touch\mynonexisting.txt") + { + return false; + } + + if (path == @"c:\touch-nonexisting\file.txt") + { + return false; + } + + if (path == @"c:\touch\myreadonly.txt") + { + return true; + } + Assert.Fail("Unexpected file exists: " + path); + + return true; + } + + /// + /// Mock file create. + /// + /// + private static FileStream FileCreate(string path) + { + if (path == @"c:\touch\mynonexisting.txt") + { + return null; + } + + if (path == @"c:\touch-nonexisting\file.txt") + { + throw new DirectoryNotFoundException(); + } + + + Assert.Fail("Unexpected file create: " + path); + return null; + } + + /// + /// Mock get attributes. + /// + /// + private static FileAttributes GetAttributes(string path) + { + FileAttributes a = new FileAttributes(); + if (path == @"c:\touch\myexisting.txt") + { + return a; + } + + if (path == @"c:\touch\mynonexisting.txt") + { + // Has attributes because Touch created it. + return a; + } + + if (path == @"c:\touch\myreadonly.txt") + { + a = System.IO.FileAttributes.ReadOnly; + return a; + } + + Assert.Fail("Unexpected file attributes: " + path); + return a; + } + + /// + /// Mock get attributes. + /// + /// + private static void SetAttributes(string path, FileAttributes attributes) + { + if (path == @"c:\touch\myreadonly.txt") + { + return; + } + Assert.Fail("Unexpected set file attributes: " + path); + } + + /// + /// Mock SetLastAccessTime. + /// + /// + private static void SetLastAccessTime(string path, DateTime timestamp) + { + if (path == @"c:\touch\myexisting.txt") + { + return; + } + + if (path == @"c:\touch\mynonexisting.txt") + { + return; + } + + if (path == @"c:\touch\myreadonly.txt") + { + // Read-only so throw an exception + throw new IOException(); + } + + Assert.Fail("Unexpected set last access time: " + path); + } + + /// + /// Mock SetLastWriteTime. + /// + /// + private static void SetLastWriteTime(string path, DateTime timestamp) + { + if (path == @"c:\touch\myexisting.txt") + { + return; + } + + if (path == @"c:\touch\mynonexisting.txt") + { + return; + } + + if (path == @"c:\touch\myreadonly.txt") + { + return; + } + + + Assert.Fail("Unexpected set last write time: " + path); + } + + [TestMethod] + public void TouchExisting() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch\myexisting.txt") + }; + + bool success = Execute(t); + + Assert.IsTrue(success); + + Assert.AreEqual(1, t.TouchedFiles.Length); + + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("Touch.Touching"), "c:\\touch\\myexisting.txt") + ) + ); + } + + [TestMethod] + public void TouchNonExisting() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch\mynonexisting.txt") + }; + + bool success = Execute(t); + + // Not success because the file doesn't exist + Assert.IsTrue(!success); + + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("Touch.FileDoesNotExist"), "c:\\touch\\mynonexisting.txt") + ) + ); + } + + [TestMethod] + public void TouchNonExistingAlwaysCreate() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.AlwaysCreate = true; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch\mynonexisting.txt") + }; + + bool success = Execute(t); + + // Success because the file was created. + Assert.IsTrue(success); + + Assert.IsTrue + ( + engine.Log.Contains + ( + String.Format(AssemblyResources.GetString("Touch.CreatingFile"), "c:\\touch\\mynonexisting.txt", "AlwaysCreate") + ) + ); + } + + [TestMethod] + public void TouchNonExistingAlwaysCreateAndBadlyFormedTimestamp() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.AlwaysCreate = true; + t.ForceTouch = false; + t.Time = "Badly formed time String."; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch\mynonexisting.txt") + }; + + bool success = Execute(t); + + // Failed because of badly formed time string. + Assert.IsTrue(!success); + + Assert.IsTrue(engine.Log.Contains("MSB3376")); + } + + [TestMethod] + public void TouchReadonly() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.AlwaysCreate = true; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch\myreadonly.txt") + }; + + bool success = Execute(t); + + // Failed because file is readonly. + Assert.IsTrue(!success); + + Assert.IsTrue(engine.Log.Contains("MSB3374")); + Assert.IsTrue(engine.Log.Contains(@"c:\touch\myreadonly.txt")); + } + + [TestMethod] + public void TouchReadonlyForce() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.ForceTouch = true; + t.AlwaysCreate = true; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch\myreadonly.txt") + }; + + bool success = Execute(t); + } + + [TestMethod] + public void TouchNonExistingDirectoryDoesntExist() + { + Touch t = new Touch(); + MockEngine engine = new MockEngine(); + t.BuildEngine = engine; + t.AlwaysCreate = true; + + t.Files = new ITaskItem[] + { + new TaskItem(@"c:\touch-nonexisting\file.txt") + }; + + bool success = Execute(t); + + // Failed because the target directory didn't exist. + Assert.IsTrue(!success); + + Assert.IsTrue(engine.Log.Contains("MSB3371")); + Assert.IsTrue(engine.Log.Contains(@"c:\touch-nonexisting\file.txt")); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/TrustInfo_Tests.cs b/src/XMakeTasks/UnitTests/TrustInfo_Tests.cs new file mode 100644 index 00000000000..59f577677d4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/TrustInfo_Tests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// These functions are now guarded with a linkDemand and requires the caller to be signed with a +// ms pkt. The test harness does not appear to be signed. +#if never +using System; +using System.IO; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; + + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class TrustInfo_Tests + { + /// + /// Write a trust info object to a file. + /// + [TestMethod] + public void Basic() + { + TrustInfo t = new TrustInfo(); + string file = FileUtilities.GetTemporaryFile(); + File.Delete(file); + t.WriteManifest(file); + Assert.IsTrue(File.Exists(file)); + // Writing a second time is an in-place modification. + t.WriteManifest(file); + Assert.IsTrue(File.Exists(file)); + File.Delete(file); + } + } +} +#endif + + diff --git a/src/XMakeTasks/UnitTests/Vbc_Tests.cs b/src/XMakeTasks/UnitTests/Vbc_Tests.cs new file mode 100644 index 00000000000..41e6d4229c7 --- /dev/null +++ b/src/XMakeTasks/UnitTests/Vbc_Tests.cs @@ -0,0 +1,1270 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests +{ + /* + * Class: VbcTests + * + * Test the Vbc task in various ways. + * + */ + [TestClass] + sealed public class VbcTests + { + /// + /// Tests the "References" parameter on the Vbc task, and confirms that it sets + /// the /reference switch on the command-line correctly. The Vbc task does + /// not support assembly aliases, so we want to make sure that we don't try + /// to pass an assembly alias into vbc.exe. + /// + [TestMethod] + public void References() + { + Vbc t = new Vbc(); + + TaskItem reference = new TaskItem("System.Xml.dll"); + reference.SetMetadata("Alias", "Foo"); + + t.References = new TaskItem[] { reference }; + CommandLine.ValidateHasParameter(t, "/reference:System.Xml.dll"); + } + + // Tests the "BaseAddress" parameter on the Vbc task, and confirms that it sets + // the /baseaddress switch on the command-line correctly. The catch here is that + // "vbc.exe" only supports passing in hex values, but the task may be passed + // in hex or decimal. + [TestMethod] + public void BaseAddressHex1() + { + Vbc t = new Vbc(); + t.BaseAddress = "&H00001000"; + CommandLine.ValidateHasParameter(t, "/baseaddress:00001000"); + } + + // Tests the "BaseAddress" parameter on the Vbc task, and confirms that it sets + // the /baseaddress switch on the command-line correctly. The catch here is that + // "vbc.exe" only supports passing in hex values, but the task may be passed + // in hex or decimal. + [TestMethod] + public void BaseAddressHex2() + { + Vbc t = new Vbc(); + t.BaseAddress = "&h00001000"; + CommandLine.ValidateHasParameter(t, "/baseaddress:00001000"); + } + + // Tests the "BaseAddress" parameter on the Vbc task, and confirms that it sets + // the /baseaddress switch on the command-line correctly. The catch here is that + // "vbc.exe" only supports passing in hex values, but the task may be passed + // in hex or decimal. + [TestMethod] + public void BaseAddressHex3() + { + Vbc t = new Vbc(); + t.BaseAddress = "0x0000FFFF"; + CommandLine.ValidateHasParameter(t, "/baseaddress:0000FFFF"); + } + + // Tests the "BaseAddress" parameter on the Vbc task, and confirms that it sets + // the /baseaddress switch on the command-line correctly. The catch here is that + // "vbc.exe" only supports passing in hex values, but the task may be passed + // in hex or decimal. + [TestMethod] + public void BaseAddressHex4() + { + Vbc t = new Vbc(); + t.BaseAddress = "0X00001000"; + CommandLine.ValidateHasParameter(t, "/baseaddress:00001000"); + } + + // Tests the "BaseAddress" parameter on the Vbc task, and confirms that it sets + // the /baseaddress switch on the command-line correctly. The catch here is that + // "vbc.exe" only supports passing in hex values, but the task may be passed + // in hex or decimal. + [TestMethod] + public void BaseAddressDecimal() + { + Vbc t = new Vbc(); + t.BaseAddress = "285212672"; + CommandLine.ValidateHasParameter(t, "/baseaddress:11000000"); + } + + /// + /// Test the hex parsing code with a large integer value (unsigned int in size) + /// + [TestMethod] + public void BaseAddressLargeDecimal() + { + Vbc t = new Vbc(); + t.BaseAddress = "3555454580"; + CommandLine.ValidateHasParameter(t, "/baseaddress:D3EBEE74"); + } + + // Tests the "BaseAddress" parameter on the Vbc task, and confirms that it throws + // an exception when an invalid string is passed in. + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void BaseAddressInvalid() + { + Vbc t = new Vbc(); + t.BaseAddress = "deadbeef"; + CommandLine.ValidateHasParameter(t, "WeShouldNeverGetThisFar"); + } + + // Tests the "GenerateDocumentation" and "DocumentationFile" parameters on the Vbc task, + // and confirms that it sets the /doc switch on the command-line correctly. + [TestMethod] + public void DocumentationFile() + { + Vbc t = new Vbc(); + + t.DocumentationFile = "foo.xml"; + t.GenerateDocumentation = true; + + int firstParamLocation = CommandLine.ValidateHasParameter(t, "/doc+"); + int secondParamLocation = CommandLine.ValidateHasParameter(t, "/doc:foo.xml"); + + Assert.IsTrue(secondParamLocation > firstParamLocation, "The order of the /doc switches is incorrect."); + } + + // Tests the "AdditionalLibPaths" parameter on the Vbc task, and confirms that it sets + // the /lib switch on the command-line correctly. + [TestMethod] + public void AdditionaLibPaths() + { + Vbc t = new Vbc(); + + t.AdditionalLibPaths = new string[] { @"c:\xmake\", @"c:\msbuild" }; + CommandLine.ValidateHasParameter(t, @"/libpath:c:\xmake\,c:\msbuild"); + } + + // Tests the "NoVBRuntimeReference" parameter on the Vbc task, and confirms that it sets + // the /novbruntimeref switch on the command-line correctly. + [TestMethod] + public void NoVBRuntimeReference() + { + Vbc t = new Vbc(); + + t.NoVBRuntimeReference = true; + CommandLine.ValidateHasParameter(t, @"/novbruntimeref"); + + Vbc t2 = new Vbc(); + + t2.NoVBRuntimeReference = false; + CommandLine.ValidateNoParameterStartsWith(t2, @"/novbruntimeref"); + } + + // Tests the "Verbosity" parameter on the Vbc task, and confirms that it sets + // the /quiet switch on the command-line correctly. + [TestMethod] + public void VerbosityQuiet() + { + Vbc t = new Vbc(); + + t.Verbosity = "QUIET"; + CommandLine.ValidateHasParameter(t, @"/QUIET"); + } + + // Tests the "Verbosity" parameter on the Vbc task, and confirms that it sets + // the /verbose switch on the command-line correctly. + [TestMethod] + public void VerbosityVerbose() + { + Vbc t = new Vbc(); + + t.Verbosity = "verbose"; + CommandLine.ValidateHasParameter(t, @"/verbose"); + } + + // Tests the "Platform" parameter on the Vbc task, and confirms that it sets + // the /platform switch on the command-line correctly. + [TestMethod] + public void Platform() + { + Vbc t = new Vbc(); + + t.Platform = "x86"; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + } + + // Tests the "Platform" and "Prefer32Bit" parameter combinations on the Vbc task, + // and confirms that it sets the /platform switch on the command-line correctly. + [TestMethod] + public void PlatformAndPrefer32Bit() + { + // Implicit "anycpu" + Vbc t = new Vbc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/platform:"); + t = new Vbc(); + t.Prefer32Bit = false; + CommandLine.ValidateNoParameterStartsWith(t, @"/platform:"); + t = new Vbc(); + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu32bitpreferred"); + + // Explicit "anycpu" + t = new Vbc(); + t.Platform = "anycpu"; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu"); + t = new Vbc(); + t.Platform = "anycpu"; + t.Prefer32Bit = false; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu"); + t = new Vbc(); + t.Platform = "anycpu"; + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:anycpu32bitpreferred"); + + // Explicit "x86" + t = new Vbc(); + t.Platform = "x86"; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + t = new Vbc(); + t.Platform = "x86"; + t.Prefer32Bit = false; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + t = new Vbc(); + t.Platform = "x86"; + t.Prefer32Bit = true; + CommandLine.ValidateHasParameter(t, @"/platform:x86"); + } + + // Tests the "HighEntropyVA" parameter on the Vbc task, and confirms that it + // sets the /highentropyva switch on the command-line correctly. + [TestMethod] + public void HighEntropyVA() + { + // Implicit /highentropyva- + Vbc t = new Vbc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/highentropyva"); + + // Explicit /highentropyva- + t = new Vbc(); + t.HighEntropyVA = false; + CommandLine.ValidateHasParameter(t, @"/highentropyva-"); + + // Explicit /highentropyva+ + t = new Vbc(); + t.HighEntropyVA = true; + CommandLine.ValidateHasParameter(t, @"/highentropyva+"); + } + + // Tests the "SubsystemVersion" parameter on the Vbc task, and confirms that it sets + // the /subsystemversion switch on the command-line correctly. + [TestMethod] + public void SubsystemVersion() + { + Vbc t = new Vbc(); + CommandLine.ValidateNoParameterStartsWith(t, @"/subsystemversion"); + + t = new Vbc(); + t.SubsystemVersion = "4.0"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:4.0"); + + t = new Vbc(); + t.SubsystemVersion = "5"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:5"); + + t = new Vbc(); + t.SubsystemVersion = "6.02"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:6.02"); + + t = new Vbc(); + t.SubsystemVersion = "garbage"; + CommandLine.ValidateHasParameter(t, @"/subsystemversion:garbage"); + } + + // Tests the "Verbosity" parameter on the Vbc task, and confirms that it + // does not add any command-line switches when verbosity is set to normal. + [TestMethod] + public void VerbosityNormal() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc t = new Vbc(); + t.BuildEngine = mockEngine; + + t.Verbosity = "Normal"; + t.Sources = new TaskItem[] { new TaskItem("a.vb") }; + Assert.IsTrue(CommandLine.CallValidateParameters(t), "Vbc task didn't accept 'normal' for the Verbosity parameter"); + CommandLine.ValidateNoParameterStartsWith(t, @"/quiet"); + CommandLine.ValidateNoParameterStartsWith(t, @"/verbose"); + CommandLine.ValidateNoParameterStartsWith(t, @"/normal"); + } + + // Tests the "Verbosity" parameter on the Vbc task, and confirms that it + // throws an error when an invalid value is passed in. + [TestMethod] + public void VerbosityBogus() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc t = new Vbc(); + t.BuildEngine = mockEngine; + + t.Verbosity = "bogus"; + Assert.IsFalse(CommandLine.CallValidateParameters(t), "Bogus verbosity setting not caught as error."); + } + + // Check all parameters that are based on ints, bools and other value types. + // This is because parameters with these types go through a not-so-typesafe check + // for existence in the property bag. + [TestMethod] + public void FlagsAndInts() + { + Vbc t = new Vbc(); + + // From managed compiler + t.CodePage = 5; + t.EmitDebugInformation = true; + t.DelaySign = true; + t.FileAlignment = 9; + t.NoLogo = true; + t.Optimize = true; + t.TreatWarningsAsErrors = true; + t.Utf8Output = true; + + // From Vbc. + t.GenerateDocumentation = true; + t.NoWarnings = true; + t.OptionExplicit = true; + t.OptionStrict = true; + t.RemoveIntegerChecks = true; + t.TargetCompactFramework = true; + t.OptionInfer = true; + + + // Check the parameters. + CommandLine.ValidateHasParameter(t, "/codepage:5"); + CommandLine.ValidateHasParameter(t, "/debug+"); + CommandLine.ValidateHasParameter(t, "/delaysign+"); + CommandLine.ValidateHasParameter(t, "/filealign:9"); + CommandLine.ValidateHasParameter(t, "/nologo"); + CommandLine.ValidateHasParameter(t, "/optimize+"); + CommandLine.ValidateHasParameter(t, "/warnaserror+"); + CommandLine.ValidateHasParameter(t, "/utf8output"); + + CommandLine.ValidateHasParameter(t, "/doc+"); + CommandLine.ValidateHasParameter(t, "/nowarn"); + CommandLine.ValidateHasParameter(t, "/optionexplicit+"); + CommandLine.ValidateHasParameter(t, "/optionstrict+"); + CommandLine.ValidateHasParameter(t, "/removeintchecks+"); + CommandLine.ValidateHasParameter(t, "/netcf"); + CommandLine.ValidateHasParameter(t, "/optioninfer+"); + } + + /*********************************************************************** + * Test: VbcHostObject + * + * Instantiates the Vbc task, sets a host object on it, and tries executing + * the task to make sure the host object is called appropriately. + * + **********************************************************************/ + [TestMethod] + [Ignore] + public void VbcHostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + MockVbcHostObject vbcHostObject = new MockVbcHostObject5(); + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsTrue(!vbcHostObject.CompileMethodWasCalled); + + vbc.Sources = new TaskItem[] { new TaskItem("a.vb") }; + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.IsTrue(vbcHostObject.CompileMethodWasCalled); + } + + [TestMethod] + public void VbcHostObjectNotUsedIfToolNameSpecified() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + MockVbcHostObject vbcHostObject = new MockVbcHostObject(); + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + vbc.ToolExe = "vbc_custom.exe"; + + Assert.IsTrue(vbc.UseAlternateCommandLineToolToExecute()); + } + + [TestMethod] + public void VbcHostObjectNotUsedIfToolPathSpecified() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + MockVbcHostObject vbcHostObject = new MockVbcHostObject(); + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + vbc.ToolPath = "c:\\some\\custom\\path"; + + Assert.IsTrue(vbc.UseAlternateCommandLineToolToExecute()); + } + + + /* + * Class: VbcCrossParameterInjection + * + * Test the Vbc task for cases where the parameters are passed in + * with with whitespace and other special characters to try to fool + * us into spawning Vbc.exe while bypassing security + */ + [TestClass] + sealed public class VbcCrossParameterInjection + { + [TestMethod] + public void Win32Icon() + { + Vbc t = new Vbc(); + t.Win32Icon = @"MyFile.ico /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void AdditionalLibPaths() + { + Vbc t = new Vbc(); + t.AdditionalLibPaths = new string[] { @"parm /out:c:\windows\system32\notepad.exe" }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void AddModules() + { + Vbc t = new Vbc(); + t.AddModules = new string[] { @"parm /out:c:\windows\system32\notepad.exe" }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void BaseAddress() + { + Vbc t = new Vbc(); + t.BaseAddress = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void DebugType() + { + Vbc t = new Vbc(); + t.DebugType = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + [Ignore] // Because constants may legitimately contains quotes _and_ we've cut security, we decided to let DefineConstants be passed through literally. + public void DefineConstants() + { + Vbc t = new Vbc(); + t.DefineConstants = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void DocumentationFile() + { + Vbc t = new Vbc(); + t.DocumentationFile = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void KeyContainer() + { + Vbc t = new Vbc(); + t.KeyContainer = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void KeyFile() + { + Vbc t = new Vbc(); + t.KeyFile = @"parm /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void LinkResources() + { + Vbc t = new Vbc(); + t.KeyFile = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void MainEntryPoint() + { + Vbc t = new Vbc(); + t.MainEntryPoint = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void OutputAssembly() + { + Vbc t = new Vbc(); + t.OutputAssembly = new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe"); + CommandLine.ValidateHasParameter(t, @"/out:parm1 /out:c:\windows\system32\notepad.exe"); + } + + [TestMethod] + public void References() + { + Vbc t = new Vbc(); + t.References = new TaskItem[] + { + new TaskItem("parm0"), + new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void Resources() + { + Vbc t = new Vbc(); + t.References = new TaskItem[] + { + new TaskItem("parm0"), + new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void ResponseFiles() + { + Vbc t = new Vbc(); + t.ResponseFiles = new TaskItem[] + { + new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") + }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void TargetType() + { + Vbc t = new Vbc(); + t.TargetType = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void ToolPath() + { + Vbc t = new Vbc(); + t.ToolPath = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void Win32Resource() + { + Vbc t = new Vbc(); + t.Win32Resource = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void Imports() + { + Vbc t = new Vbc(); + t.Imports = new TaskItem[] { new TaskItem(@"parm1 /out:c:\windows\system32\notepad.exe") }; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void OptionCompare() + { + Vbc t = new Vbc(); + t.OptionCompare = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void RootNamespace() + { + Vbc t = new Vbc(); + t.RootNamespace = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + + [TestMethod] + public void SdkPath() + { + Vbc t = new Vbc(); + t.SdkPath = @"parm1 /out:c:\windows\system32\notepad.exe"; + CommandLine.ValidateNoParameterStartsWith(t, "/out"); + } + } + + [TestMethod] + public void OptionStrictOffNowarnsUndefined() + { + Vbc t = new Vbc(); + t.OptionStrict = false; + t.DisabledWarnings = null; + CommandLine.ValidateHasParameter(t, "/optionstrict:custom"); // we should be custom if no warnings are disabled + } + + [TestMethod] + public void OptionStrictOffNowarnsEmpty() + { + Vbc t = new Vbc(); + t.OptionStrict = false; + t.DisabledWarnings = ""; + CommandLine.ValidateHasParameter(t, "/optionstrict:custom"); // we shuold be custom if no warnings are disabled + } + + [TestMethod] + public void OptionStrictOffNoWarnsPresent() + { + // When the below warnings are set to NONE, we are effectively Option Strict-. But because we don't want the msbuild task + // to have to know the current set of disabled warnings that implies option strict-, we just set option strict:custom + // with the understanding that we get the same behavior as option strict- since we are passing the /nowarn line on that + // contains all the warnings OptionStrict- would disable anyway. + Vbc t = new Vbc(); + t.OptionStrict = false; + t.DisabledWarnings = "41999,42016,42017,42018,42019,42020,42021,42022,42032,42036"; + CommandLine.ValidateHasParameter(t, "/optionstrict:custom"); + t.DisabledWarnings = "/nowarn:41999,42016,42017,42018,42019,42020,42021,42022,42032,42036"; + } + + [TestMethod] + public void OptionStrictOnNoWarnsUndefined() + { + Vbc t = new Vbc(); + t.OptionStrict = true; + t.DisabledWarnings = ""; + CommandLine.ValidateHasParameter(t, "/optionstrict+"); + } + + [TestMethod] + public void OptionStrictOnNoWarnsPresent() + { + Vbc t = new Vbc(); + t.OptionStrict = true; + t.DisabledWarnings = "41999"; + CommandLine.ValidateHasParameter(t, "/optionstrict+"); + t.DisabledWarnings = "/nowarn:41999"; + } + + [TestMethod] + public void OptionStrictType1() + { + Vbc t = new Vbc(); + t.OptionStrict = true; + t.OptionStrictType = "custom"; + CommandLine.ValidateContains(t, "/optionstrict+ /optionstrict:custom", true); + } + + [TestMethod] + public void OptionStrictType2() + { + Vbc t = new Vbc(); + t.OptionStrictType = "custom"; + CommandLine.ValidateContains(t, "/optionstrict:custom", true); + CommandLine.ValidateDoesNotContain(t, "/optionstrict-", true); + } + + [TestMethod] + public void MultipleResponseFiles() + { + Vbc t = new Vbc(); + t.ResponseFiles = new TaskItem[] + { + new TaskItem(@"1.rsp"), + new TaskItem(@"2.rsp"), + new TaskItem(@"3.rsp"), + new TaskItem(@"4.rsp") + }; + CommandLine.ValidateContains(t, "@1.rsp @2.rsp @3.rsp @4.rsp", true); + } + + [TestMethod] + public void SingleResponseFile() + { + Vbc t = new Vbc(); + t.ResponseFiles = new TaskItem[] + { + new TaskItem(@"1.rsp") + }; + CommandLine.ValidateHasParameter(t, "@1.rsp"); + } + + [TestMethod] + public void ParseError_StandardVbcError() + { + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + + string error1 = "d:\\scratch\\607654\\Module1.vb(5) : error BC30451: Name 'Ed' is not declared."; + string error2 = ""; + string error3 = " Ed Sub"; + string error4 = " ~~ "; + + t.ParseVBErrorOrWarning(error1, MessageImportance.High); + t.ParseVBErrorOrWarning(error2, MessageImportance.High); + t.ParseVBErrorOrWarning(error3, MessageImportance.High); + t.ParseVBErrorOrWarning(error4, MessageImportance.High); + + Assert.IsTrue((t.BuildEngine as MockEngine).Errors >= 1, "Should be at least one error"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30451"); + (t.BuildEngine as MockEngine).AssertLogContains("(5,5)"); + } + + [TestMethod] + public void ParseError_StandardVbcErrorWithColon() + { + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + + string error1 = "d:\\scratch\\607654\\Module1.vb(5) : error BC30800: Method arguments must be enclosed in parentheses : Don't you think?"; + string error2 = ""; + string error3 = " Ed Sub"; + string error4 = " ~~~"; + + t.ParseVBErrorOrWarning(error1, MessageImportance.High); + t.ParseVBErrorOrWarning(error2, MessageImportance.High); + t.ParseVBErrorOrWarning(error3, MessageImportance.High); + t.ParseVBErrorOrWarning(error4, MessageImportance.High); + + Assert.IsTrue((t.BuildEngine as MockEngine).Errors >= 1, "Should be at least one error"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30800"); + (t.BuildEngine as MockEngine).AssertLogContains("(5,8)"); + } + + [TestMethod] + public void ParseError_ProjectLevelVbcError() + { + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + + string error = "vbc : error BC30573: Error in project-level import '' at '' : XML namespace prefix '' is already declared"; + + t.ParseVBErrorOrWarning(error, MessageImportance.High); + + Assert.IsTrue((t.BuildEngine as MockEngine).Errors >= 1, "Should be at least one error"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30573"); + } + + [TestMethod] + public void ParseError_ProjectLevelVbcErrorFollowedByStandard() + { + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + + string error1 = "vbc : error BC30573: Error in project-level import '' at '' : XML namespace prefix '' is already declared"; + + string error2 = "d:\\scratch\\607654\\Module1.vb(5) : error BC30800: Method arguments must be enclosed in parentheses : Don't you think?"; + string error3 = ""; + string error4 = " Ed Sub"; + string error5 = " ~~~"; + + t.ParseVBErrorOrWarning(error1, MessageImportance.High); + t.ParseVBErrorOrWarning(error2, MessageImportance.High); + t.ParseVBErrorOrWarning(error3, MessageImportance.High); + t.ParseVBErrorOrWarning(error4, MessageImportance.High); + t.ParseVBErrorOrWarning(error5, MessageImportance.High); + + Assert.IsTrue((t.BuildEngine as MockEngine).Errors >= 2, "Should be at least two errors"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30573"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30800"); + } + + [TestMethod] + public void ParseError_StandardVbcErrorFollowedByProjectLevel() + { + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + + string error1 = "d:\\scratch\\607654\\Module1.vb(5) : error BC30800: Method arguments must be enclosed in parentheses : Don't you think?"; + string error2 = ""; + string error3 = " Ed Sub"; + string error4 = " ~~~"; + + string error5 = "vbc : error BC30573: Error in project-level import '' at '' : XML namespace prefix '' is already declared"; + + t.ParseVBErrorOrWarning(error1, MessageImportance.High); + t.ParseVBErrorOrWarning(error2, MessageImportance.High); + t.ParseVBErrorOrWarning(error3, MessageImportance.High); + t.ParseVBErrorOrWarning(error4, MessageImportance.High); + t.ParseVBErrorOrWarning(error5, MessageImportance.High); + + Assert.IsTrue((t.BuildEngine as MockEngine).Errors >= 2, "Should be at least two errors"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30573"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30800"); + } + + [TestMethod] + public void ParseError_TwoProjectLevelErrors() + { + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + + string error1 = "vbc : error BC30205: Error in project-level import ', ' at ', ' : End of statement expected."; + string error2 = "vbc : error BC30573: Error in project-level import '' at '' : XML namespace prefix '' is already declared"; + + t.ParseVBErrorOrWarning(error1, MessageImportance.High); + t.ParseVBErrorOrWarning(error2, MessageImportance.High); + + Assert.IsTrue((t.BuildEngine as MockEngine).Errors >= 2, "Should be at least two errors"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30573"); + (t.BuildEngine as MockEngine).AssertLogContains("BC30205"); + } + + + /// + /// Make sure that if we pass a name of a pdb file to the task that it corrrectly moves the file. + /// + [TestMethod] + public void MovePDBFile_GoodName() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "MovePDBFile_GoodName"); + try + { + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + + Directory.CreateDirectory(tempDirectory); + + string outputAssemblyPath = Path.Combine(tempDirectory, "Out.dll"); + string newoutputAssemblyPath = Path.Combine(tempDirectory, "MyNewPDBFile.pdb"); + string outputPDBPath = Path.Combine(tempDirectory, "Out.pdb"); + File.WriteAllText(outputPDBPath, "Hello"); + File.WriteAllText(outputAssemblyPath, "Hello"); + + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + t.PdbFile = newoutputAssemblyPath; + t.MovePdbFileIfNecessary(outputAssemblyPath); + + FileInfo newPDBInfo = new FileInfo(newoutputAssemblyPath); + FileInfo oldPDBInfo = new FileInfo(outputPDBPath); + + Assert.IsTrue(newPDBInfo.Exists); + Assert.IsFalse(oldPDBInfo.Exists); + ((MockEngine)t.BuildEngine).MockLogger.AssertNoErrors(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + } + } + + + /// + /// Make sure if the file already exists that the move still happens. + /// + [TestMethod] + public void MovePDBFile_SameNameandFileAlreadyExists() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "MovePDBFile_SmeNameandFileAlreadyExists"); + try + { + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + + Directory.CreateDirectory(tempDirectory); + + string outputAssemblyPath = Path.Combine(tempDirectory, "Out.dll"); + string newoutputAssemblyPath = Path.Combine(tempDirectory, "Out.pdb"); + + File.WriteAllText(outputAssemblyPath, "Hello"); + File.WriteAllText(newoutputAssemblyPath, "Hello"); + + Vbc t = new Vbc(); + t.BuildEngine = new MockEngine(); + t.PdbFile = newoutputAssemblyPath; + t.MovePdbFileIfNecessary(outputAssemblyPath); + + FileInfo newPDBInfo = new FileInfo(newoutputAssemblyPath); + + Assert.IsTrue(newPDBInfo.Exists); + ((MockEngine)t.BuildEngine).MockLogger.AssertNoErrors(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + } + } + + /// + /// Make sure that if we pass a name of a pdb file to the task that it corrrectly moves the file. + /// + [TestMethod] + public void MovePDBFile_BadFileName() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), "MovePDBFile_BadFileName"); + + try + { + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + + Directory.CreateDirectory(tempDirectory); + + string outputAssemblyPath = Path.Combine(tempDirectory, "Out.dll"); + string outputPDBPath = Path.Combine(tempDirectory, "Out.pdb"); + File.WriteAllText(outputPDBPath, "Hello"); + File.WriteAllText(outputAssemblyPath, "Hello"); + + MockEngine engine = new MockEngine(); + Vbc t = new Vbc(); + t.BuildEngine = engine; + t.PdbFile = "||{}}{<>?$$%^&*()!@#$%`~.pdb"; + t.MovePdbFileIfNecessary(outputAssemblyPath); + + FileInfo oldPDBInfo = new FileInfo(outputAssemblyPath); + Assert.IsTrue(oldPDBInfo.Exists); + + Assert.IsTrue(engine.Errors >= 1, "Should be one error"); + (t.BuildEngine as MockEngine).AssertLogContains("MSB3402"); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + FileUtilities.DeleteDirectoryNoThrow(tempDirectory, true); + } + } + } + + [TestMethod] + public void NoAnalyzers_CommandLine() + { + Vbc vbc = new Vbc(); + + CommandLine.ValidateNoParameterStartsWith(vbc, "/analyzer"); + } + + [TestMethod] + public void Analyzer_CommandLine() + { + Vbc vbc = new Vbc(); + vbc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll") + }; + + CommandLine.ValidateHasParameter(vbc, "/analyzer:Foo.dll"); + } + + [TestMethod] + public void MultipleAnalyzers_CommandLine() + { + Vbc vbc = new Vbc(); + vbc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll"), + new TaskItem("Bar.dll") + }; + + CommandLine.ValidateHasParameter(vbc, "/analyzer:Foo.dll"); + CommandLine.ValidateHasParameter(vbc, "/analyzer:Bar.dll"); + } + + [TestMethod] + public void NoAnalyzer_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.Analyzers); + + vbc.Sources = new TaskItem[] { new TaskItem("a.vb") }; + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.IsNull(vbcHostObject.Analyzers); + } + + [TestMethod] + public void Analyzer_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.Analyzers); + + vbc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll") + }; + + vbc.Sources = new TaskItem[] { new TaskItem("a.vb") }; + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.AreEqual(1, vbcHostObject.Analyzers.Length); + Assert.AreEqual("Foo.dll", vbcHostObject.Analyzers[0].ItemSpec); + } + + [TestMethod] + public void MultipleAnalyzers_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.Analyzers); + + vbc.Analyzers = new TaskItem[] + { + new TaskItem("Foo.dll"), + new TaskItem("Bar.dll") + }; + + vbc.Sources = new TaskItem[] { new TaskItem("a.vb") }; + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.AreEqual(2, vbcHostObject.Analyzers.Length); + Assert.AreEqual("Foo.dll", vbcHostObject.Analyzers[0].ItemSpec); + Assert.AreEqual("Bar.dll", vbcHostObject.Analyzers[1].ItemSpec); + } + + [TestMethod] + public void NoRuleSet_CommandLine() + { + Vbc vbc = new Vbc(); + + CommandLine.ValidateNoParameterStartsWith(vbc, "/ruleset"); + } + + [TestMethod] + public void RuleSet_CommandLine() + { + Vbc vbc = new Vbc(); + vbc.CodeAnalysisRuleSet = "Bar.ruleset"; + + CommandLine.ValidateHasParameter(vbc, "/ruleset:Bar.ruleset"); + } + + [TestMethod] + public void NoRuleSet_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.RuleSet); + + vbc.Sources = new TaskItem[] { new TaskItem("a.vb") }; + + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.IsNull(vbcHostObject.RuleSet); + } + + [TestMethod] + public void RuleSet_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.RuleSet); + + vbc.CodeAnalysisRuleSet = "Bar.ruleset"; + + vbc.Sources = new TaskItem[] { new TaskItem("a.vb") }; + + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.AreEqual("Bar.ruleset", vbcHostObject.RuleSet); + } + + [TestMethod] + public void NoAdditionalFiles_CommandLine() + { + Vbc vbc = new Vbc(); + + CommandLine.ValidateNoParameterStartsWith(vbc, "/additionalfile"); + } + + [TestMethod] + public void AdditionalFiles_CommandLine() + { + Vbc vbc = new Vbc(); + vbc.AdditionalFiles = new TaskItem[] + { + new TaskItem("web.config") + }; + + CommandLine.ValidateHasParameter(vbc, "/additionalfile:web.config"); + } + + [TestMethod] + public void MultipleAdditionalFiles_CommandLine() + { + Vbc vbc = new Vbc(); + vbc.AdditionalFiles = new TaskItem[] + { + new TaskItem("app.config"), + new TaskItem("web.config") + }; + + CommandLine.ValidateHasParameter(vbc, "/additionalfile:app.config"); + CommandLine.ValidateHasParameter(vbc, "/additionalfile:web.config"); + } + + [TestMethod] + public void NoAdditionalFile_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.AdditionalFiles); + + vbc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.IsNull(vbcHostObject.AdditionalFiles); + } + + [TestMethod] + public void AdditionalFile_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.AdditionalFiles); + + vbc.AdditionalFiles = new TaskItem[] + { + new TaskItem("web.config") + }; + + vbc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + Assert.AreEqual(1, vbcHostObject.AdditionalFiles.Length); + Assert.AreEqual("web.config", vbcHostObject.AdditionalFiles[0].ItemSpec); + } + + [TestMethod] + public void MultipleAdditionalFiles_HostObject() + { + IBuildEngine2 mockEngine = new MockEngine(); + Vbc vbc = new Vbc(); + vbc.BuildEngine = mockEngine; + + MockVbcAnalyzerHostObject vbcHostObject = new MockVbcAnalyzerHostObject(); + vbcHostObject.SetDesignTime(true); + + vbc.HostObject = vbcHostObject; + vbc.UseHostCompilerIfAvailable = true; + + Assert.IsNull(vbcHostObject.AdditionalFiles); + + vbc.AdditionalFiles = new TaskItem[] + { + new TaskItem("web.config"), + new TaskItem("app.config") + }; + + vbc.Sources = new TaskItem[] { new TaskItem("a.cs") }; + + bool vbcSuccess = vbc.Execute(); + + Assert.IsTrue(vbcSuccess, "Vbc task failed."); + + Assert.AreEqual(2, vbcHostObject.AdditionalFiles.Length); + Assert.AreEqual("web.config", vbcHostObject.AdditionalFiles[0].ItemSpec); + Assert.AreEqual("app.config", vbcHostObject.AdditionalFiles[1].ItemSpec); + } + } +} + + + + + diff --git a/src/XMakeTasks/UnitTests/VisualBasicParserUtilitites_Tests.cs b/src/XMakeTasks/UnitTests/VisualBasicParserUtilitites_Tests.cs new file mode 100644 index 00000000000..3f3ace3228b --- /dev/null +++ b/src/XMakeTasks/UnitTests/VisualBasicParserUtilitites_Tests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class VisualBasicParserUtilititesTests + { + // Try just and empty file + [TestMethod] + public void EmptyFile() + { + AssertParse("", null); + } + + // A simple form + [TestMethod] + public void SimpleForm() + { + AssertParse + ( + @" + rem This is my class + Public ClAsS Form1 + End Class + ", + "Form1"); + } + + // A simple form with a namespace + [TestMethod] + public void Namespace() + { + AssertParse + ( + @" + ' This is my Namespace + NamEspacE Goofy.Mickey + rem This is my class + Public ClAsS Form1 + End Class + End Namespace + ", + "Goofy.Mickey.Form1"); + } + + // A simple form with a namespace + [TestMethod] + public void NestedNamespace() + { + AssertParse + ( + @" + Namespace Goofy + Namespace Mickey + Public Class Form1 + ", + "Goofy.Mickey.Form1"); + } + + // A namespace the is ended before the class + [TestMethod] + public void NestedAndEndedNamespace() + { + AssertParse + ( + @" + Namespace Goofy + Namespace Mickey + End Namespace ' Just finished with the namespace, about to make a class + PuBlic Class Form1 + ", + "Goofy.Form1"); + } + + /// + /// Our Visual Basic parser would sink any string that begins with "rem" as + /// the beginning of a comment, even if the "rem" wasn't immediately followed + /// by whitespace. This resulted in broken resource names when the namespace + /// name was something like "BugResources.RemoveStuff.XYZ", because we would + /// only match the "BugResources" bit. + /// + [TestMethod] + public void NamespaceElementBeginsWithRem() + { + AssertParse +( + @" + ' This is my Namespace + NamEspacE Artist.Painter.Rembrandt + rem This is my class + Public ClAsS SelfPortrait + End Class + End Namespace + ", + "Artist.Painter.Rembrandt.SelfPortrait"); + } + + /* + * Method: AssertParse + * + * Parse 'source' as VB source code and get the first class name fully-qualified + * with namespace information. That classname must match the expected class name. + */ + private static void AssertParse(string source, string expectedClassName) + { + source = source.Replace("&qt", "\""); + + ExtractedClassName className = VisualBasicParserUtilities.GetFirstClassNameFullyQualified + ( + StreamHelpers.StringToStream(source) + ); + + Assert.AreEqual(expectedClassName, className.Name); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/VisualBasicTokenizer_Tests.cs b/src/XMakeTasks/UnitTests/VisualBasicTokenizer_Tests.cs new file mode 100644 index 00000000000..92ec457cb03 --- /dev/null +++ b/src/XMakeTasks/UnitTests/VisualBasicTokenizer_Tests.cs @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; + +using Microsoft.Build.Shared.LanguageParser; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class VisualBasicTokenizer_Tests + { + [TestMethod] + public void Empty() { AssertTokenize("", "", "", 0); } + [TestMethod] + public void OneSpace() { AssertTokenize(" ", " \x0d", ".eol", 1); } + [TestMethod] + public void TwoSpace() { AssertTokenize(" ", " \x0d", ".eol", 1); } + [TestMethod] + public void Tab() { AssertTokenize("\t", "\t\x0d", ".eol", 1); } + [TestMethod] + public void TwoTab() { AssertTokenize("\t\t", "\t\t\x0d", ".eol", 1); } + [TestMethod] + public void SpaceTab() { AssertTokenize(" \t", " \t\x0d", ".eol", 1); } + + // Test line continuation character + [TestMethod] + public void SimpleLineContinuation() { AssertTokenize(" _\xd\xa", "."); } + [TestMethod] + public void LineContinuationWithspacesAfter() { AssertTokenize(" _ \xd\xa\xd\xa", "."); } + + // Comments + [TestMethod] + public void SimpleComment() { AssertTokenize("' This is a comment\xd", "Comment(' This is a comment)eol"); } + [TestMethod] + public void RemComment() { AssertTokenize("rEm This is a comment\xd", "Comment(rEm This is a comment)eol"); } + + // Identifiers + [TestMethod] + public void SimpleIdentifier() { AssertTokenize("_MyIdentifier3\xd", "Identifier(_MyIdentifier3)eol"); } + [TestMethod] + public void IdentifierWithEmbeddedUnderscore() { AssertTokenize("_M_\xd", "Identifier(_M_)eol"); } + [TestMethod] + public void IdentifierWithStringTypeCharacter() { AssertTokenize("MyString$\xd", "Identifier(MyString$)eol"); } + [TestMethod] + public void IdentifierWithLongTypeCharacter() { AssertTokenize("MyString&\xd", "Identifier(MyString&)eol"); } + [TestMethod] + public void IdentifierWithDecimalTypeCharacter() { AssertTokenize("MyString@\xd", "Identifier(MyString@)eol"); } + [TestMethod] + public void IdentifierWithSingleTypeCharacter() { AssertTokenize("MyString!\xd", "Identifier(MyString!)eol"); } + [TestMethod] + public void IdentifierWithDoubleTypeCharacter() { AssertTokenize("MyString#\xd", "Identifier(MyString#)eol"); } + [TestMethod] + public void IdentifierWithIntegerTypeCharacter() { AssertTokenize("MyString%\xd", "Identifier(MyString%)eol"); } + [TestMethod] + public void EscapedIdentifier() { AssertTokenize("[Namespace]\xd", "Namespace\xd", "Identifier(Namespace)eol", 1); } + [TestMethod] + public void UnfinishedEscapedIdentifier() { AssertTokenize("[Namespace\xd", "ExpectedIdentifier([Namespace)"); } + [TestMethod] + public void EscapedIdentifierWithoutGoodStart() { AssertTokenize("[3]\xd", "ExpectedIdentifier([)"); } + [TestMethod] + public void EscapedLineContinuation() { AssertTokenize("[_]\xd", "ExpectedIdentifier([_])"); } + [TestMethod] + public void EscapedButEmptyIdentifier() { AssertTokenize("[]\xd", "ExpectedIdentifier([)"); } + [TestMethod] + public void EscapedIdentifierHasType() { AssertTokenize("[MyString$]\xd", "ExpectedIdentifier([MyString)"); } + [TestMethod] + public void EscapedIdentifierHasTypeOnTheOutside() { AssertTokenize("[MyString]$\xd", "MyString$\xd", "Identifier(MyString)Unrecognized($)", 1); } + + // A lone underscore is an invalid identifier. + [TestMethod] + public void LoneUnderscore() + { + AssertTokenize + ( + "Sub Foo(ByVal _ As Int16)\xd", + "Keyword(Sub).Identifier(Foo)Separator(()Keyword(ByVal).ExpectedIdentifier(_)" + ); + } + + // Boolean literals + [TestMethod] + public void BooleanTrue() { AssertTokenize("tRuE\xd", "BooleanLiteral(tRuE)eol"); } + [TestMethod] + public void BooleanFalse() { AssertTokenize("falsE\xd", "BooleanLiteral(falsE)eol"); } + + // Integer literals + [TestMethod] + public void HexInteger() { AssertTokenize("&H0123456789aBcDeF\xd", "HexIntegerLiteral(&H0123456789aBcDeF)eol"); } + [TestMethod] + public void Octalnteger() { AssertTokenize("&O01234567\xd", "OctalIntegerLiteral(&O01234567)eol"); } + [TestMethod] + public void HexIntegerLowerCase() { AssertTokenize("&h001\xd", "HexIntegerLiteral(&h001)eol"); } + [TestMethod] + public void OctalntegerUpperCase() { AssertTokenize("&o001\xd", "OctalIntegerLiteral(&o001)eol"); } + [TestMethod] + public void Decimallnteger() { AssertTokenize("001\xd", "DecimalIntegerLiteral(001)eol"); } + [TestMethod] + public void InvalidHexInteger() { AssertTokenize("&H00FG\xd", "HexIntegerLiteral(&H00F)Identifier(G)eol"); } + [TestMethod] + public void InvalidOctalnteger() { AssertTokenize("&O0089\xd", "OctalIntegerLiteral(&O00)DecimalIntegerLiteral(89)eol"); } + [TestMethod] + public void InvalidHexIntegerWithNoneValid() { AssertTokenize("&HG\xd", "ExpectedValidHexDigit(&H)"); } + [TestMethod] + public void InvalidOctalntegerWithNoneValid() { AssertTokenize("&O9\xd", "ExpectedValidOctalDigit(&O)"); } + [TestMethod] + public void HexIntegerShort() { AssertTokenize("&HaBcDeFS\xd", "HexIntegerLiteral(&HaBcDeFS)eol"); } + [TestMethod] + public void HexIntegerShortLower() { AssertTokenize("&HaBcDeFs\xd", "HexIntegerLiteral(&HaBcDeFs)eol"); } + [TestMethod] + public void DecimalIntegerShort() { AssertTokenize("123S\xd", "DecimalIntegerLiteral(123S)eol"); } + [TestMethod] + public void DecimalIntegerShortLower() { AssertTokenize("123s\xd", "DecimalIntegerLiteral(123s)eol"); } + [TestMethod] + public void OctalntegerShort() { AssertTokenize("&O01234567S\xd", "OctalIntegerLiteral(&O01234567S)eol"); } + [TestMethod] + public void OctalntegerShortLower() { AssertTokenize("&O01234567s\xd", "OctalIntegerLiteral(&O01234567s)eol"); } + [TestMethod] + public void HexIntegerInteger() { AssertTokenize("&HaBcDeFI\xd", "HexIntegerLiteral(&HaBcDeFI)eol"); } + [TestMethod] + public void HexIntegerIntegerLower() { AssertTokenize("&HaBcDeFi\xd", "HexIntegerLiteral(&HaBcDeFi)eol"); } + [TestMethod] + public void OctalntegerInteger() { AssertTokenize("&O01234567I\xd", "OctalIntegerLiteral(&O01234567I)eol"); } + [TestMethod] + public void OctalntegerIntegerLower() { AssertTokenize("&O01234567i\xd", "OctalIntegerLiteral(&O01234567i)eol"); } + [TestMethod] + public void DecimalIntegerInteger() { AssertTokenize("123I\xd", "DecimalIntegerLiteral(123I)eol"); } + [TestMethod] + public void DecimalIntegerIntegerLower() { AssertTokenize("123i\xd", "DecimalIntegerLiteral(123i)eol"); } + [TestMethod] + public void HexIntegerLong() { AssertTokenize("&HaBcDeFL\xd", "HexIntegerLiteral(&HaBcDeFL)eol"); } + [TestMethod] + public void HexIntegerLongLower() { AssertTokenize("&HaBcDeFl\xd", "HexIntegerLiteral(&HaBcDeFl)eol"); } + [TestMethod] + public void OctalntegerLong() { AssertTokenize("&O01234567L\xd", "OctalIntegerLiteral(&O01234567L)eol"); } + [TestMethod] + public void OctalntegerLongLower() { AssertTokenize("&O01234567l\xd", "OctalIntegerLiteral(&O01234567l)eol"); } + [TestMethod] + public void DecimalIntegerLong() { AssertTokenize("123L\xd", "DecimalIntegerLiteral(123L)eol"); } + [TestMethod] + public void DecimalIntegerIntegerLong() { AssertTokenize("123l\xd", "DecimalIntegerLiteral(123l)eol"); } + [TestMethod] + public void DecimalIntegerWithIntegerTypeChar() { AssertTokenize("1234%\xd", "DecimalIntegerLiteral(1234%)eol"); } + [TestMethod] + public void DecimalIntegerWithLongTypeChar() { AssertTokenize("1234&\xd", "DecimalIntegerLiteral(1234&)eol"); } + [TestMethod] + public void DecimalIntegerWithDecimalTypeChar() { AssertTokenize("1234@\xd", "DecimalIntegerLiteral(1234@)eol"); } + [TestMethod] + public void DecimalIntegerWithSingleTypeChar() { AssertTokenize("1234!\xd", "DecimalIntegerLiteral(1234!)eol"); } + [TestMethod] + public void DecimalIntegerWithDoubleTypeChar() { AssertTokenize("1234#\xd", "DecimalIntegerLiteral(1234#)eol"); } + [TestMethod] + public void DecimalIntegerWithStringTypeChar() { AssertTokenize("1234$\xd", "DecimalIntegerLiteral(1234)Unrecognized($)"); } + + // String literal + [TestMethod] + public void BasicString() { AssertTokenize("\"A string\"\xd", "StringLiteral(\"A string\")eol"); } + [TestMethod] + public void StringWithDoubledQuotesAsEscape() { AssertTokenize("\"\"\"\"\x0d", "\"\"\"\"\x0d", "StringLiteral(\"\"\"\")eol", 1); } + [TestMethod] + public void StringUnclosed() { AssertTokenize("\"string\x0d", "EndOfFileInsideString(\"string\x0d)"); } + + // Operators + [TestMethod] + public void CheckAllOperators() + { + AssertTokenize + ( + "a=1 & 2*3+4-5/6\\7^8<9=10>11\xd", + @"Identifier(a)Operator(=)DecimalIntegerLiteral(1).Operator(&).DecimalIntegerLiteral(2)Operator(*)DecimalIntegerLiteral(3)Operator(+)DecimalIntegerLiteral(4)Operator(-)DecimalIntegerLiteral(5)Operator(/)DecimalIntegerLiteral(6)Operator(\)DecimalIntegerLiteral(7)Operator(^)DecimalIntegerLiteral(8)Operator(<)DecimalIntegerLiteral(9)Operator(=)DecimalIntegerLiteral(10)Operator(>)DecimalIntegerLiteral(11)eol" + ); + } + + // Inplace arrays + [TestMethod] + public void InplaceArray() + { + AssertTokenize + ( + "Me.Controls.AddRange(New Control() {Me.lblCodebase, Me.lblCopyright})\xd", + "Keyword(Me)Separator(.)Identifier(Controls)Separator(.)Identifier(AddRange)Separator(()Keyword(New).Identifier(Control)Separator(()Separator()).Separator({)Keyword(Me)Separator(.)Identifier(lblCodebase)Separator(,).Keyword(Me)Separator(.)Identifier(lblCopyright)Separator(})Separator())eol" + ); + } + + // Keywords + [TestMethod] + public void SimpleKeyword() { AssertTokenize("Namespace\xd", "Keyword(Namespace)eol"); } + + // From the real world + [TestMethod] + public void WackyBrackettedClassName() + { + AssertTokenize + ( + "Public Class [!output SAFE_ITEM_NAME]\xd", + "Keyword(Public).Keyword(Class).ExpectedIdentifier([)" + ); + } + [TestMethod] + public void MyClassIsAKeyword() + { + AssertTokenize + ( + "Class MyClass\xd", + "Keyword(Class).Keyword(MyClass)eol" + ); + } + + + [TestMethod] + public void Regress_Mutation_x0dx0aIsASingleLine() + { + AssertTokenize("\x0d\x0a", "\x0d\x0a", "eol", 1); + } + + /* + * Method: AssertTokenize + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also, the source must be regenerated exactly when the tokens are concatenated + * back together, + */ + static private void AssertTokenize(string source, string expectedTokenKey) + { + // Most of the time, we expect the rebuilt source to be the same as the input source. + AssertTokenize(source, source, expectedTokenKey, 1); + } + + /* + * Method: AssertTokenize + * + * Tokenize a string ('source') and compare it to the expected set of tokens. + * Also compare the source that is regenerated by concatenating all of the tokens + * to 'expectedSource'. + */ + static private void AssertTokenize + ( + string source, + string expectedSource, + string expectedTokenKey, + int expectedLastLineNumber + ) + { + VisualBasicTokenizer tokens = new VisualBasicTokenizer + ( + StreamHelpers.StringToStream(source), + false + ); + string results = ""; + string tokenKey = ""; + int lastLine = 0; + bool syntaxError = false; + foreach (Token t in tokens) + { + results += t.InnerText; + lastLine = t.Line; + + if (!syntaxError) + { + // Its not really a file name, but GetExtension serves the purpose of getting the class name without + // the namespace prepended. + string tokenClass = t.ToString(); + int pos = tokenClass.LastIndexOfAny(new char[] { '+', '.' }); + + if (t is VisualBasicTokenizer.LineTerminatorToken) + { + tokenKey += "eol"; + } + else if (t is WhitespaceToken) + { + tokenKey += "."; + } + else + { + tokenKey += tokenClass.Substring(pos + 1); + tokenKey += "("; + tokenKey += t.InnerText; + tokenKey += ")"; + } + } + + if (t is SyntaxErrorToken) + { + // Stop processing after the first syntax error because + // the order of tokens after this is an implementation detail and + // shouldn't be encoded into the unit tests. + syntaxError = true; + } + } + tokenKey = tokenKey.Replace("Token", ""); + + if (expectedSource != results || expectedTokenKey != tokenKey) + { + Console.WriteLine(tokenKey); + } + + Assert.AreEqual(expectedSource, results); + Assert.AreEqual(expectedTokenKey, tokenKey); + Assert.AreEqual(expectedLastLineNumber, lastLine); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/WinMDExp_Tests.cs b/src/XMakeTasks/UnitTests/WinMDExp_Tests.cs new file mode 100644 index 00000000000..c1aa2cb63ab --- /dev/null +++ b/src/XMakeTasks/UnitTests/WinMDExp_Tests.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using System.Collections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using System.Text.RegularExpressions; +using System.Globalization; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class WinMDExpTests + { + /// + /// Tests the "References" parameter on the winmdexp task, and confirms that it sets + /// the /reference switch on the command-line correctly. + /// + [TestMethod] + public void References() + { + WinMDExp t = new WinMDExp(); + t.WinMDModule = "Foo.dll"; + + TaskItem mscorlibReference = new TaskItem("mscorlib.dll"); + TaskItem windowsFoundationReference = new TaskItem("Windows.Foundation.winmd"); + + t.References = new TaskItem[] { mscorlibReference, windowsFoundationReference }; + CommandLine.ValidateHasParameter(t, "/reference:mscorlib.dll", false); + CommandLine.ValidateHasParameter(t, "/reference:Windows.Foundation.winmd", false); + } + + [TestMethod] + public void TestNoWarnSwitchWithWarnings() + { + WinMDExp t = new WinMDExp(); + t.WinMDModule = "Foo.dll"; + t.DisabledWarnings = "41999,42016"; + CommandLine.ValidateHasParameter(t, "/nowarn:41999,42016", false); + } + + + // Tests the "GenerateDocumentation" and "DocumentationFile" parameters on the Vbc task, + // and confirms that it sets the /doc switch on the command-line correctly. + [TestMethod] + public void DocumentationFile() + { + WinMDExp t = new WinMDExp(); + + t.WinMDModule = "Foo.dll"; + t.OutputDocumentationFile = "output.xml"; + t.InputDocumentationFile = "input.xml"; + + CommandLine.ValidateHasParameter(t, "/d:output.xml", false); + CommandLine.ValidateHasParameter(t, "/md:input.xml", false); + } + + [TestMethod] + public void PDBFileTesting() + { + WinMDExp t = new WinMDExp(); + t.WinMDModule = "Foo.dll"; + t.OutputWindowsMetadataFile = "Foo.dll"; + t.OutputPDBFile = "output.pdb"; + t.InputPDBFile = "input.pdb"; + + CommandLine.ValidateHasParameter(t, "/pdb:output.pdb", false); + CommandLine.ValidateHasParameter(t, "/mp:input.pdb", false); + } + + [TestMethod] + public void WinMDModule() + { + WinMDExp t = new WinMDExp(); + + t.WinMDModule = "Foo.dll"; + CommandLine.ValidateContains(t, "Foo.dll", false); + } + + [TestMethod] + public void UsesrDefinedOutputFile() + { + WinMDExp t = new WinMDExp(); + t.WinMDModule = "Foo.dll"; + t.OutputWindowsMetadataFile = "Bob.winmd"; + CommandLine.ValidateHasParameter(t, "/out:Bob.winmd", false); + } + + [TestMethod] + public void NoOutputFileDefined() + { + WinMDExp t = new WinMDExp(); + + t.WinMDModule = "Foo.dll"; + t.OutputWindowsMetadataFile = "Foo.winmd"; + CommandLine.ValidateHasParameter(t, "/out:Foo.winmd", false); + } + } +} + + + + + diff --git a/src/XMakeTasks/UnitTests/WriteCodeFragment_Tests.cs b/src/XMakeTasks/UnitTests/WriteCodeFragment_Tests.cs new file mode 100644 index 00000000000..8e0e2617fd4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/WriteCodeFragment_Tests.cs @@ -0,0 +1,532 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tests for write code fragment task. +//----------------------------------------------------------------------- + +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for write code fragment task + /// + [TestClass] + public class WriteCodeFragment_Tests + { + /// + /// Need an available language + /// + [TestMethod] + public void InvalidLanguage() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "xx"; + task.OutputFile = new TaskItem("foo"); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3712"); + } + + /// + /// Need a language + /// + [TestMethod] + public void NoLanguage() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.OutputFile = new TaskItem("foo"); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3098"); + } + + /// + /// Need a location + /// + [TestMethod] + public void NoFileOrDirectory() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + bool result = task.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3711"); + } + + /// + /// Combine file and directory + /// + [TestMethod] + public void CombineFileDirectory() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + task.AssemblyAttributes = new TaskItem[] { new TaskItem("aa") }; + task.OutputFile = new TaskItem("CombineFileDirectory.tmp"); + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + string file = Path.Combine(Path.GetTempPath(), "CombineFileDirectory.tmp"); + Assert.AreEqual(file, task.OutputFile.ItemSpec); + Assert.AreEqual(true, File.Exists(file)); + } + + /// + /// Ignore directory if file is rooted + /// + [TestMethod] + public void DirectoryAndRootedFile() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + task.AssemblyAttributes = new TaskItem[] { new TaskItem("aa") }; + + string folder = Path.Combine(Path.GetTempPath(), "foo\\"); + string file = Path.Combine(folder, "CombineFileDirectory.tmp"); + Directory.CreateDirectory(folder); + task.OutputFile = new TaskItem(file); + task.OutputDirectory = new TaskItem("c:\\"); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + Assert.AreEqual(file, task.OutputFile.ItemSpec); + Assert.AreEqual(true, File.Exists(file)); + + Directory.Delete(folder, true); + } + + /// + /// Given nothing to write, should succeed but + /// produce no output file + /// + [TestMethod] + public void NoAttributesShouldEmitNoFile() + { + string file = Path.Combine(Path.GetTempPath(), "NoAttributesShouldEmitNoFile.tmp"); + + if (File.Exists(file)) + { + File.Delete(file); + } + + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + task.AssemblyAttributes = new TaskItem[] { }; // MSBuild sets an empty array + task.OutputFile = new TaskItem(file); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(false, File.Exists(file)); + Assert.AreEqual(null, task.OutputFile); + } + + /// + /// Given nothing to write, should succeed but + /// produce no output file + /// + [TestMethod] + public void NoAttributesShouldEmitNoFile2() + { + string file = Path.Combine(Path.GetTempPath(), "NoAttributesShouldEmitNoFile.tmp"); + + if (File.Exists(file)) + { + File.Delete(file); + } + + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + task.AssemblyAttributes = null; // null this time + task.OutputFile = new TaskItem(file); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(false, File.Exists(file)); + Assert.AreEqual(null, task.OutputFile); + } + + /// + /// Bad file path + /// + [TestMethod] + public void InvalidFilePath() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + task.AssemblyAttributes = new TaskItem[] { new TaskItem("aa") }; + task.OutputFile = new TaskItem("||invalid||"); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3713"); + } + + /// + /// Bad directory path + /// + [TestMethod] + public void InvalidDirectoryPath() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + task.Language = "c#"; + task.AssemblyAttributes = new TaskItem[] { new TaskItem("aa") }; + task.OutputDirectory = new TaskItem("||invalid||"); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + engine.AssertLogContains("MSB3713"); + } + + /// + /// Parameterless attribute + /// + [TestMethod] + public void OneAttributeNoParams() + { + string file = Path.Combine(Path.GetTempPath(), "OneAttribute.tmp"); + + try + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputFile = new TaskItem(file); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, File.Exists(file)); + + string content = File.ReadAllText(file); + Console.WriteLine(content); + + Assert.AreEqual(true, content.Contains("using System;")); + Assert.AreEqual(true, content.Contains("[assembly: System.AssemblyTrademarkAttribute()]")); + } + finally + { + File.Delete(file); + } + } + + /// + /// Test with the VB language + /// + [TestMethod] + public void OneAttributeNoParamsVb() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "visualbasic"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + string content = File.ReadAllText(task.OutputFile.ItemSpec); + Console.WriteLine(content); + + Assert.AreEqual(true, content.Contains("Imports System")); + Assert.AreEqual(true, content.Contains("")); + } + + /// + /// More than one attribute + /// + [TestMethod] + public void TwoAttributes() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute1 = new TaskItem("AssemblyTrademarkAttribute"); + attribute1.SetMetadata("Name", "Microsoft"); + TaskItem attribute2 = new TaskItem("System.AssemblyCultureAttribute"); + attribute2.SetMetadata("Culture", "en-US"); + task.AssemblyAttributes = new TaskItem[] { attribute1, attribute2 }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + string content = File.ReadAllText(task.OutputFile.ItemSpec); + Console.WriteLine(content); + + Assert.AreEqual(true, content.Contains(@"[assembly: AssemblyTrademarkAttribute(Name=""Microsoft"")]")); + Assert.AreEqual(true, content.Contains(@"[assembly: System.AssemblyCultureAttribute(Culture=""en-US"")]")); + } + + /// + /// Specify directory instead + /// + [TestMethod] + public void ToDirectory() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, File.Exists(task.OutputFile.ItemSpec)); + Assert.AreEqual(true, String.Equals(task.OutputFile.ItemSpec.Substring(0, Path.GetTempPath().Length), Path.GetTempPath(), StringComparison.OrdinalIgnoreCase)); + Assert.AreEqual(".cs", task.OutputFile.ItemSpec.Substring(task.OutputFile.ItemSpec.Length - 3)); + + File.Delete(task.OutputFile.ItemSpec); + } + + /// + /// Regular case + /// + [TestMethod] + public void OneAttributeTwoParams() + { + string file = Path.Combine(Path.GetTempPath(), "OneAttribute.tmp"); + + try + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("Company", "Microsoft"); + attribute.SetMetadata("Year", "2009"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputFile = new TaskItem(file); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + Assert.AreEqual(true, File.Exists(file)); + + string content = File.ReadAllText(file); + Console.WriteLine(content); + + Assert.AreEqual(true, content.Contains("using System;")); + Assert.AreEqual(true, content.Contains("using System.Reflection;")); + Assert.AreEqual(true, content.Contains(@"[assembly: AssemblyTrademarkAttribute(Company=""Microsoft"", Year=""2009"")]")); + } + finally + { + File.Delete(file); + } + } + + /// + /// This produces invalid code, but the task works + /// + [TestMethod] + public void OneAttributeTwoParamsSameName() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("Company", "Microsoft"); + attribute.SetMetadata("Company", "2009"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + File.Delete(task.OutputFile.ItemSpec); + } + + /// + /// Some attributes only allow positional constructor arguments. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// + [TestMethod] + public void OneAttributePositionalParamInvalidSuffix() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("_ParameterXXXXXXXXXX", "Microsoft"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + + engine.AssertLogContains("MSB3098"); + } + + + /// + /// Some attributes only allow positional constructor arguments. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// + [TestMethod] + public void OneAttributeTwoPositionalParams() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("_Parameter1", "Microsoft"); + attribute.SetMetadata("_Parameter2", "2009"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + string content = File.ReadAllText(task.OutputFile.ItemSpec); + Console.WriteLine(content); + + Assert.AreEqual(true, content.Contains(@"[assembly: AssemblyTrademarkAttribute(""Microsoft"", ""2009"")]")); + + File.Delete(task.OutputFile.ItemSpec); + } + + /// + /// Some attributes only allow positional constructor arguments. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// If a parameter is skipped, it's an error. + /// + [TestMethod] + public void OneAttributeSkippedPositionalParams() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("_Parameter2", "2009"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + + engine.AssertLogContains("MSB3714"); + } + + /// + /// Some attributes only allow positional constructor arguments. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// This test is for "_ParameterX" + /// + [TestMethod] + public void InvalidNumber() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("_ParameterX", "2009"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + + engine.AssertLogContains("MSB3098"); + } + + /// + /// Some attributes only allow positional constructor arguments. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// This test is for "_Parameter" + /// + [TestMethod] + public void NoNumber() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("_Parameter", "2009"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(false, result); + + engine.AssertLogContains("MSB3098"); + } + + /// + /// Some attributes only allow positional constructor arguments. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// These can also be combined with named params. + /// + [TestMethod] + public void OneAttributePositionalAndNamedParams() + { + WriteCodeFragment task = new WriteCodeFragment(); + MockEngine engine = new MockEngine(true); + task.BuildEngine = engine; + TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute"); + attribute.SetMetadata("_Parameter1", "Microsoft"); + attribute.SetMetadata("Date", "2009"); + attribute.SetMetadata("Copyright", "(C)"); + task.AssemblyAttributes = new TaskItem[] { attribute }; + task.Language = "c#"; + task.OutputDirectory = new TaskItem(Path.GetTempPath()); + bool result = task.Execute(); + + Assert.AreEqual(true, result); + + string content = File.ReadAllText(task.OutputFile.ItemSpec); + Console.WriteLine(content); + + Assert.AreEqual(true, content.Contains(@"[assembly: AssemblyTrademarkAttribute(""Microsoft"", Date=""2009"", Copyright=""(C)"")]")); + + File.Delete(task.OutputFile.ItemSpec); + } + } +} + + + diff --git a/src/XMakeTasks/UnitTests/XamlDataDrivenToolTask_Tests.cs b/src/XMakeTasks/UnitTests/XamlDataDrivenToolTask_Tests.cs new file mode 100644 index 00000000000..d8fc886930a --- /dev/null +++ b/src/XMakeTasks/UnitTests/XamlDataDrivenToolTask_Tests.cs @@ -0,0 +1,422 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.IO; +using System.Runtime.Remoting; +using System.Globalization; +using System.Reflection; + +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Xaml; + +using Microsoft.CSharp; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks.Xaml; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Build.UnitTests.XamlDataDrivenToolTask_Tests +{ + /// + /// Test fixture for testing the DataDrivenToolTask class with a fake task generated by XamlTestHelpers.cs + /// Tests to see if certain switches are appended. + /// + [TestClass] + public class GeneratedTask + { + private Assembly _fakeTaskDll; + + [TestInitialize] + public void SetupGeneratedCode() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + } + + /// + /// Test to see whether all of the correct boolean switches are appended. + /// + [TestMethod] + public void TestDefaultFlags() + { + object fakeTaskInstance = CreateFakeTask(); + CheckCommandLine("/always /Cr:CT", XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// A test to see if all of the reversible flags are generated correctly + /// This test case leaves the default flags the way they are + /// + [TestMethod] + public void TestReversibleFlagsWithDefaults() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicReversible", true); + string expectedResult = "/always /Br /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// A test to see if all of the reversible flags are generated correctly + /// This test case explicitly sets the ComplexReversible to be false + /// + [TestMethod] + public void TestReversibleFlagsWithoutDefaults() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicReversible", true); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexReversible", false); + string expectedResult = "/always /Br /Cr:CF"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// Tests to make sure enums are working well. + /// + [TestMethod] + public void TestBasicString() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicString", "Enum1"); + string expectedResult = "/always /Bs1 /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + [TestMethod] + public void TestDynamicEnum() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicDynamicEnum", "MyBeforeTarget"); + string expectedResult = "/always MyBeforeTarget /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// Tests the basic string array type + /// + [TestMethod] + public void TestBasicStringArray() + { + object fakeTaskInstance = CreateFakeTask(); + string[] fakeArray = new string[1]; + fakeArray[0] = "FakeStringArray"; + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicStringArray", new object[] { fakeArray }); + string expectedResult = "/always /BsaFakeStringArray /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// Tests the basic string array type, with an array that contains multiple values. + /// + [TestMethod] + public void TestBasicStringArray_MultipleValues() + { + object fakeTaskInstance = CreateFakeTask(); + string[] fakeArray = new string[3]; + fakeArray[0] = "Fake"; + fakeArray[1] = "String"; + fakeArray[2] = "Array"; + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicStringArray", new object[] { fakeArray }); + string expectedResult = "/always /BsaFake /BsaString /BsaArray /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// Tests to see whether the integer appears correctly on the command line + /// + [TestMethod] + public void TestInteger() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "BasicInteger", 2); + string expectedResult = "/always /Bi2 /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + // complex tests + /// + /// Tests the (full) functionality of a reversible property + /// + [TestMethod] + public void TestComplexReversible() + { + // When flag is set to false + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexReversible", false); + string expectedResult = "/always /Cr:CF"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + + // When flag is set to true + fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexReversible", true); + expectedResult = "/always /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// Tests the functionality of a nonreversible property + /// When set to true, should find that the file exists because of the default property + /// + [TestMethod] + [Ignore] // Arguments are not supported in Dev10 + public void TestComplexNonreversible() + { + // When flag is set to false + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexNonreversibleWArgument", false); + string expectedResult = "/always /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + + // When flag is set to true + fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexNonreversibleWArgument", true); + expectedResult = "/always /Cr:CT /CnrwaDefaultFile"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + [TestMethod] + public void TestComplexString() + { + // check to see that the resulting value is good + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexString", "LegalValue1"); + string expectedResult = "/always /Cr:CT /Lv1"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// Tests the functionality of a string type property + /// + [TestMethod] + public void TestComplexStringArray() + { + object fakeTaskInstance = CreateFakeTask(); + string[] fakeArray = new string[] { "FakeFile1", "FakeFile2", "FakeFile3" }; + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexStringArray", new object[] { fakeArray }); + string expectedResult = "/always /Cr:CT /CsaFakeFile1;FakeFile2;FakeFile3"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void TestComplexIntegerLessThanMin() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexInteger", 2); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void TestComplexIntegerGreaterThanMax() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexInteger", 256); + string expectedResult = "/always /Ci256 /Cr:CT"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + [TestMethod] + public void TestComplexIntegerWithinRange() + { + object fakeTaskInstance = CreateFakeTask(); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexInteger", 128); + string expectedResult = "/always /Cr:CT /Ci128"; + CheckCommandLine(expectedResult, XamlTestHelpers.GenerateCommandLine(fakeTaskInstance)); + } + + /// + /// This method checks the generated command line against the expected command line + /// + /// + /// + /// true if the two are the same, false if they are different + public void CheckCommandLine(string expected, string actual) + { + Assert.IsTrue(expected == actual, "Expected " + expected + " but got " + actual); + } + + /// + /// XamlTaskFactory does not, in and of itself, support the idea of "always" switches or default values. At least + /// for Dev10, the workaround is to create a property as usual, and then specify the required values in the .props + /// file. Since these unit tests are just testing the task itself, this method serves as our ".props file". + /// + public object CreateFakeTask() + { + object fakeTaskInstance = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + + XamlTestHelpers.SetProperty(fakeTaskInstance, "Always", true); + XamlTestHelpers.SetProperty(fakeTaskInstance, "ComplexReversible", true); + + return fakeTaskInstance; + } + } + + /// + /// Tests for XamlDataDrivenToolTask / XamlTaskFactory in the context of a project file. + /// + [TestClass] + public class ProjectFileTests + { + /// + /// Tests that when a call to a XamlDataDrivenTask fails, the commandline is reported in the error message. + /// + [TestMethod] + [Ignore] + // Ignore: Test requires installed toolset. + public void CommandLineErrorsReportFullCommandlineAmpersandTemp() + { + string projectFile = @" + + + + + + + + + ]]> + + + + + + "; + + + string directoryWithAmpersand = "foo&bar"; + string newTmp = Path.Combine(Path.GetTempPath(), directoryWithAmpersand); + string oldTmp = Environment.GetEnvironmentVariable("TMP"); + + try + { + Directory.CreateDirectory(newTmp); + Environment.SetEnvironmentVariable("TMP", newTmp); + Project p = ObjectModelHelpers.CreateInMemoryProject(projectFile); + MockLogger logger = new MockLogger(); + + bool success = p.Build(logger); + Assert.IsFalse(success); + logger.AssertLogContains("FINDSTR"); + + // Should not be logging ToolTask.ToolCommandFailed, should be logging Xaml.CommandFailed + logger.AssertLogDoesntContain("MSB6006"); + logger.AssertLogContains("MSB3721"); + } + finally + { + Environment.SetEnvironmentVariable("TMP", oldTmp); + ObjectModelHelpers.DeleteDirectory(newTmp); + if (Directory.Exists(newTmp)) Directory.Delete(newTmp); + } + } + + + /// + /// Tests that when a call to a XamlDataDrivenTask fails, the commandline is reported in the error message. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void CommandLineErrorsReportFullCommandline() + { + string projectFile = @" + + + + + + + + + ]]> + + + + + + "; + + Project p = ObjectModelHelpers.CreateInMemoryProject(projectFile); + MockLogger logger = new MockLogger(); + + bool success = p.Build(logger); + + Assert.IsFalse(success, "Build should have failed"); + + // Should not be logging ToolTask.ToolCommandFailed, should be logging Xaml.CommandFailed + logger.AssertLogDoesntContain("MSB6006"); + logger.AssertLogContains("MSB3721"); + } + + /// + /// Tests that when a call to a XamlDataDrivenTask fails, the commandline is reported in the error message. + /// + [TestMethod] + [Ignore] + // Ignore: Changes to the current directory interfere with the toolset reader. + public void SquareBracketEscaping() + { + string projectFile = @" + + + + + + + + + ]]> + + + + + + + + + + + + + + + + + + + "; + + Project p = ObjectModelHelpers.CreateInMemoryProject(projectFile); + MockLogger logger = new MockLogger(); + + bool success = p.Build(logger); + + Assert.IsTrue(success, "Build should have succeeded"); + + logger.AssertLogContains("echo 1) value end"); + logger.AssertLogContains("echo 2) [value end"); + logger.AssertLogContains("echo 3) [ value end"); + logger.AssertLogContains("echo 4) [ value value end"); + logger.AssertLogContains("echo 5) value] end"); + logger.AssertLogContains("echo 6) [test]] end"); + logger.AssertLogContains("echo 7) [value] end"); + logger.AssertLogContains("echo 8) [notaproperty] end"); + logger.AssertLogContains("echo 9) [[notaproperty] end"); + logger.AssertLogContains("echo 10) [ [notaproperty] end"); + logger.AssertLogContains("echo 11) [ [nap] [nap] end"); + logger.AssertLogContains("echo 12) [notaproperty]] end"); + logger.AssertLogContains("echo 13) [notaproperty]] end"); + logger.AssertLogContains("echo 14) [[notaproperty]] end"); + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/XamlTaskFactory_Tests.cs b/src/XMakeTasks/UnitTests/XamlTaskFactory_Tests.cs new file mode 100644 index 00000000000..0ab7fa64cff --- /dev/null +++ b/src/XMakeTasks/UnitTests/XamlTaskFactory_Tests.cs @@ -0,0 +1,792 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Build.Framework; +using System.Xml; +using System.IO; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Linq; +using Microsoft.CSharp; +using System.Reflection; +using Microsoft.Build.Shared; +using System.Globalization; +using Microsoft.Build.Tasks.Xaml; +using System.Xaml; + +namespace Microsoft.Build.UnitTests.XamlTaskFactory_Tests +{ + #region Tests for Load and Parse methods + /// + /// The text fixture to unit test the task generator. + /// Creates a new TaskGenerator object and tests the various methods + /// + [TestClass] + public sealed class LoadAndParseTests + { + /// + /// Tests the load method. Expects true to be returned. + /// + [TestMethod] + public void TestLoadXml() + { + TaskParser tp = new TaskParser(); + string s = @" + + + + "; + Assert.IsTrue(tp.Parse(s.Replace("`", "\""), "TaskGeneratorLoadTest"), "File failed to load correctly."); + } + + /// + /// Tests the TaskName property. + /// Should get "CL" back for this specific case. + /// + [TestMethod] + public void TestGetTaskName() + { + string xmlContents = @" + + + + "; + + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + Assert.IsTrue(tp.GeneratedTaskName.Equals("CL"), "Was expecting task name to be CL, but was " + tp.GeneratedTaskName); + } + + /// + /// Tests the BaseClass property. XamlTaskFactory does not currently support setting the BaseClass. + /// + [TestMethod] + [Ignore] // "Should probably translate XamlObjectWriterException for this case into XamlParseException, but I want to minimize the code changes in this initial unit test checkin" + public void TestGetBaseClass() + { + string xmlContents = @" + + + + "; + + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + Assert.AreEqual("DataDrivenToolTask", tp.BaseClass); + } + + /// + /// Tests the ResourceNamespace property. XamlTaskFactory does not currently support setting the ResourceNamespace. + /// + [TestMethod] + [Ignore] // "Should probably translate XamlObjectWriterException for this case into XamlParseException, but I want to minimize the code changes in this initial unit test checkin" + public void TestGetResourceNamespace() + { + string xmlContents = @" + + + + "; + + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + Assert.AreEqual(null, tp.ResourceNamespace); + } + + /// + /// Tests the Namespace property. XamlTaskFactory does not currently support setting the Namespace. + /// + [TestMethod] + [Ignore] // "Should probably translate XamlObjectWriterException for this case into XamlParseException, but I want to minimize the code changes in this initial unit test checkin" + public void TestGetNamespace() + { + string xmlContents = @" + + + + "; + + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + Assert.AreEqual("XamlTaskNamespace", tp.Namespace); + } + + /// + /// See what happens when the name is missing from the task element + /// + [TestMethod] + public void TestParseIncorrect_NoName() + { + bool exceptionCaught = false; + + try + { + string incorrectXmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(incorrectXmlContents, "CL"); + } + catch (XamlParseException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught, "Should have caught a XamlParseException"); + } + + /// + /// See what happens when the task element is valid, but we're searching for a different rule that's not in the file. + /// + [TestMethod] + public void TestParseIncorrect_NoMatchingRule() + { + bool exceptionCaught = false; + + try + { + string incorrectXmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(incorrectXmlContents, "CL"); + } + catch (XamlParseException) + { + exceptionCaught = true; + } + + Assert.IsTrue(exceptionCaught, "Should have caught a XamlParseException"); + } + + /// + /// Basic test of several reversible boolean switches, to verify that everything gets passed through correctly. + /// + [TestMethod] + public void TestBasicReversibleBooleanSwitches() + { + string xmlContents = @" + + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(2, properties.Count, "Expected two properties but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "GlobalOptimizations switch should exist"); + Assert.AreEqual("GlobalOptimizations", properties.First.Value.Name); + Assert.AreEqual("Og", properties.First.Value.SwitchName); + Assert.AreEqual("Og-", properties.First.Value.ReverseSwitchName); + Assert.AreEqual("true", properties.First.Value.Reversible, "Switch should be marked as reversible"); + + properties.RemoveFirst(); + + Assert.IsNotNull(properties.First.Value, "IntrinsicFunctions switch should exist"); + Assert.AreEqual("IntrinsicFunctions", properties.First.Value.Name); + Assert.AreEqual("Oi", properties.First.Value.SwitchName); + Assert.AreEqual("Oi:NO", properties.First.Value.ReverseSwitchName); + Assert.AreEqual("true", properties.First.Value.Reversible, "Switch should be marked as reversible"); + Assert.AreEqual(PropertyType.Boolean, properties.First.Value.Type); + } + + /// + /// Tests a basic non-reversible booleans switch + /// + [TestMethod] + public void TestBasicNonReversibleBooleanSwitch() + { + string xmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "KeepComments switch should exist"); + Assert.AreEqual("KeepComments", properties.First.Value.Name); + Assert.AreEqual("C", properties.First.Value.SwitchName); + Assert.IsNull(properties.First.Value.ReverseSwitchName, "KeepComments shouldn't have a reverse switch value"); + Assert.AreEqual(String.Empty, properties.First.Value.Reversible, "Switch should NOT marked as reversible"); + Assert.AreEqual(String.Empty, properties.First.Value.DefaultValue, "Switch should NOT have a default value"); + Assert.AreEqual(PropertyType.Boolean, properties.First.Value.Type); + } + + /// + /// Tests a basic non-reversible booleans switch that has a default value set. + /// + [TestMethod] + public void TestBasicNonReversibleBooleanSwitch_WithDefault() + { + string xmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "SuppressStartupBanner switch should exist"); + Assert.AreEqual("SuppressStartupBanner", properties.First.Value.Name); + Assert.AreEqual("nologo", properties.First.Value.SwitchName); + Assert.IsNull(properties.First.Value.ReverseSwitchName, "SuppressStartupBanner shouldn't have a reverse switch value"); + Assert.AreEqual(String.Empty, properties.First.Value.Reversible, "Switch should NOT be marked as reversible"); + Assert.AreEqual("true", properties.First.Value.DefaultValue, "Switch should default to true"); + Assert.AreEqual(PropertyType.Boolean, properties.First.Value.Type); + } + + /// + /// Test for a basic string property switch + /// + [TestMethod] + public void TestBasicEnumProperty() + { + string xmlContents = @" + + + + + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "GeneratePreprocessedFile switch should exist"); + Assert.AreEqual("GeneratePreprocessedFile", properties.First.Value.Name); + Assert.AreEqual(PropertyType.String, properties.First.Value.Type); // Enum properties are represented as string types + Assert.AreEqual(3, properties.First.Value.Values.Count, "GeneratePreprocessedFile should have three values"); + } + + /// + /// Tests XamlTaskFactory support for DynamicEnumProperties. These are primarily of use as a visualization in the property pages; as far as the + /// XamlTaskFactory and XamlDataDrivenToolTask are concerned, they are treated as StringProperties. + /// + [TestMethod] + public void TestDynamicEnumProperty() + { + string xmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "CLBeforeTargets switch should exist"); + Assert.AreEqual("CLBeforeTargets", properties.First.Value.Name); + Assert.AreEqual(PropertyType.String, properties.First.Value.Type); // Enum properties are represented as string types + } + + /// + /// Tests a simple string property. + /// + [TestMethod] + public void TestBasicStringProperty() + { + string xmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "TargetAssembly switch should exist"); + Assert.AreEqual("TargetAssembly", properties.First.Value.Name); + Assert.AreEqual(PropertyType.String, properties.First.Value.Type); + Assert.AreEqual("/target:\"[value]\"", properties.First.Value.SwitchName); + } + + /// + /// Tests a simple string array property. + /// + [TestMethod] + public void TestBasicStringArrayProperty() + { + string xmlContents = @" + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "TargetAssembly switch should exist"); + Assert.AreEqual("TargetAssembly", properties.First.Value.Name); + Assert.AreEqual(PropertyType.StringArray, properties.First.Value.Type); + Assert.AreEqual("/target:\"[value]\"", properties.First.Value.SwitchName); + Assert.AreEqual(";", properties.First.Value.Separator); + } + + /// + /// Tests a simple string array property. + /// + [TestMethod] + public void TestStringArrayPropertyWithDataSource() + { + string xmlContents = @" + + + + + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "TargetAssembly switch should exist"); + Assert.AreEqual("TargetAssembly", properties.First.Value.Name); + Assert.AreEqual(PropertyType.StringArray, properties.First.Value.Type); + Assert.AreEqual("/target:\"[value]\"", properties.First.Value.SwitchName); + Assert.AreEqual(";", properties.First.Value.Separator); + } + + /// + /// Tests a simple string array property. + /// + [TestMethod] + public void TestStringArrayPropertyWithDataSource_DataSourceIsItem() + { + string xmlContents = @" + + + + + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + + LinkedList properties = tp.Properties; + + Assert.AreEqual(1, properties.Count, "Expected one property but there were " + properties.Count); + Assert.IsNotNull(properties.First.Value, "TargetAssembly switch should exist"); + Assert.AreEqual("TargetAssembly", properties.First.Value.Name); + Assert.AreEqual(PropertyType.ItemArray, properties.First.Value.Type); // Although it's a String array property, DataSource.SourceType overrides that + Assert.AreEqual("/target:\"[value]\"", properties.First.Value.SwitchName); + Assert.AreEqual(";", properties.First.Value.Separator); + } + } + + #endregion + + #region Tests for compilation + + [TestClass] + public class CompilationTests + { + /// + /// Tests to see if the generated stream compiles + /// Code must be compilable on its own. + /// + [TestMethod] + public void TestGenerateCodeToStream() + { + string xmlContents = @" + + + + + + + + "; + TaskParser tp = XamlTestHelpers.LoadAndParse(xmlContents, "CL"); + TaskGenerator tg = new TaskGenerator(tp); + CodeCompileUnit compileUnit = tg.GenerateCode(); + CodeDomProvider codeGenerator = CodeDomProvider.CreateProvider("CSharp"); + + using (StringWriter sw = new StringWriter(CultureInfo.CurrentCulture)) + { + CodeGeneratorOptions options = new CodeGeneratorOptions(); + options.BlankLinesBetweenMembers = true; + options.BracingStyle = "C"; + + codeGenerator.GenerateCodeFromCompileUnit(compileUnit, sw, options); + + CSharpCodeProvider provider = new CSharpCodeProvider(); + // Build the parameters for source compilation. + CompilerParameters cp = new CompilerParameters(); + + // Add an assembly reference. + cp.ReferencedAssemblies.Add("System.dll"); + cp.ReferencedAssemblies.Add("System.XML.dll"); + cp.ReferencedAssemblies.Add(Path.Combine(XamlTestHelpers.PathToMSBuildBinaries, "microsoft.build.utilities.core.dll")); + cp.ReferencedAssemblies.Add(Path.Combine(XamlTestHelpers.PathToMSBuildBinaries, "microsoft.build.tasks.core.dll")); + cp.ReferencedAssemblies.Add(Path.Combine(XamlTestHelpers.PathToMSBuildBinaries, "microsoft.build.framework.dll")); + cp.ReferencedAssemblies.Add("System.Data.dll"); + + // Generate an executable instead of + // a class library. + cp.GenerateExecutable = false; + // Set the assembly file name to generate. + cp.GenerateInMemory = true; + // Invoke compilation + CompilerResults cr = provider.CompileAssemblyFromSource(cp, sw.ToString()); + // put in finally block + Assert.IsTrue(cr.Errors.Count == 0, "Compilation Failed"); + } + } + + /// + /// Tests to make sure the file generated compiles + /// + [TestMethod] + public void TestGenerateToFile() + { + string xml = @" + + + + + + + + "; + + TaskParser tp = XamlTestHelpers.LoadAndParse(xml, "CL"); + TaskGenerator tg = new TaskGenerator(tp); + CodeCompileUnit compileUnit = tg.GenerateCode(); + CodeDomProvider codeGenerator = CodeDomProvider.CreateProvider("CSharp"); + + try + { + using (StreamWriter sw = new StreamWriter("XamlTaskFactory_Tests_TestGenerateToFile.cs")) + { + CodeGeneratorOptions options = new CodeGeneratorOptions(); + options.BlankLinesBetweenMembers = true; + options.BracingStyle = "C"; + + codeGenerator.GenerateCodeFromCompileUnit(compileUnit, sw, options); + } + + CSharpCodeProvider provider = new CSharpCodeProvider(); + // Build the parameters for source compilation. + CompilerParameters cp = new CompilerParameters(); + + // Add an assembly reference. + cp.ReferencedAssemblies.Add("System.dll"); + cp.ReferencedAssemblies.Add("System.XML.dll"); + cp.ReferencedAssemblies.Add(Path.Combine(XamlTestHelpers.PathToMSBuildBinaries, "microsoft.build.utilities.core.dll")); + cp.ReferencedAssemblies.Add(Path.Combine(XamlTestHelpers.PathToMSBuildBinaries, "microsoft.build.tasks.core.dll")); + cp.ReferencedAssemblies.Add(Path.Combine(XamlTestHelpers.PathToMSBuildBinaries, "microsoft.build.framework.dll")); + cp.ReferencedAssemblies.Add("System.Data.dll"); + + // Generate an executable instead of + // a class library. + cp.GenerateExecutable = false; + // Set the assembly file name to generate. + cp.GenerateInMemory = true; + // Invoke compilation + CompilerResults cr = provider.CompileAssemblyFromFile(cp, "XamlTaskFactory_Tests_TestGenerateToFile.cs"); + Assert.IsTrue(cr.Errors.Count == 0, "Compilation Failed"); + } + finally + { + if (File.Exists("XamlTaskFactory_Tests_TestGenerateToFile.cs")) + { + File.Delete("XamlTaskFactory_Tests_TestGenerateToFile.cs"); + } + } + } + } + #endregion + + #region Tests Generated code based on one xml file + [TestClass] + public sealed class GeneratedTaskTests + { + private Assembly _fakeTaskDll; + + /// + /// Tests that quotes are correctly escaped + /// + [TestMethod] + public void TestQuotingQuotes() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(XamlTestHelpers.QuotingQuotesXml); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + } + + /// + /// Tests that backslashes are correctly escaped + /// + [TestMethod] + public void TestQuotingBackslashes() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(XamlTestHelpers.QuotingBackslashXml); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + } + + /// + /// Tests the GenerateReversible method + /// + [TestMethod] + public void TestGenerateReversible() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + PropertyInfo pi = obj.GetType().GetProperty("BasicReversible"); + Assert.IsNotNull(pi, "Shouldn't be null"); + Assert.IsTrue(pi.PropertyType == typeof(bool), "PropertyType should be a boolean"); + object[] attributes = pi.GetCustomAttributes(true); + foreach (object attribute in attributes) + { + Assert.IsTrue((attribute.GetType().GetProperty("SwitchName").GetValue(attribute, null).ToString()) == "/Br"); + } + } + + /// + /// Tests the GenerateNonreversible method + /// + [TestMethod] + public void TestGenerateNonreversible() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + PropertyInfo pi = obj.GetType().GetProperty("BasicNonreversible"); + Assert.IsNotNull(pi, "Shouldn't be null"); + Assert.IsTrue(pi.PropertyType == typeof(bool), "PropertyType should be a boolean"); + object[] attributes = pi.GetCustomAttributes(true); + foreach (object attribute in attributes) + { + Assert.IsTrue((attribute.GetType().GetProperty("SwitchName").GetValue(attribute, null).ToString()) == "/Bn"); + } + } + + /// + /// Tests the GenerateStrings method + /// + [TestMethod] + public void TestGenerateStrings() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + PropertyInfo pi = obj.GetType().GetProperty("BasicString"); + Assert.IsNotNull(pi, "Shouldn't be null"); + Assert.IsTrue(pi.PropertyType == typeof(string), "PropertyType should be a string"); + object[] attributes = pi.GetCustomAttributes(true); + foreach (object attribute in attributes) + { + Assert.IsTrue((attribute.GetType().GetProperty("SwitchName").GetValue(attribute, null).ToString()) == "/Bs"); + } + } + + /// + /// Tests the GenerateIntegers method + /// + [TestMethod] + public void TestGenerateIntegers() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + PropertyInfo pi = obj.GetType().GetProperty("BasicInteger"); + Assert.IsNotNull(pi, "Shouldn't be null"); + Assert.IsTrue(pi.PropertyType == typeof(int), "PropertyType should be an int"); + object[] attributes = pi.GetCustomAttributes(true); + foreach (object attribute in attributes) + { + Assert.IsTrue((attribute.GetType().GetProperty("SwitchName").GetValue(attribute, null).ToString()) == "/Bi"); + } + } + + /// + /// Tests the GenerateStringArrays method + /// + [TestMethod] + public void TestGenerateStringArrays() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + PropertyInfo pi = obj.GetType().GetProperty("BasicStringArray"); + Assert.IsNotNull(pi, "Shouldn't be null"); + Assert.IsTrue(pi.PropertyType == typeof(string[]), "PropertyType should be a stringarray"); + object[] attributes = pi.GetCustomAttributes(true); + foreach (object attribute in attributes) + { + PropertyInfo documentationAttribute = attribute.GetType().GetProperty("SwitchName"); + if (documentationAttribute != null) + { + Assert.IsTrue((attribute.GetType().GetProperty("SwitchName").GetValue(attribute, null).ToString()) == "/Bsa"); + } + else + { + // required attribute + Assert.IsTrue(attribute is RequiredAttribute); + } + } + } + + [TestMethod] + public void TestBasicReversibleTrue() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicReversible", true); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + bool booleanValue = switchList["BasicReversible"].BooleanValue; + string toolSwitchValue; + if (booleanValue) + { + toolSwitchValue = switchList["BasicReversible"].SwitchValue; + } + else + { + toolSwitchValue = switchList["BasicReversible"].SwitchValue + switchList["BasicReversible"].FalseSuffix; + } + Assert.IsTrue(toolSwitchValue == "/Br", "Expected /Br, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicReversibleFalse() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicReversible", false); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + bool booleanValue = switchList["BasicReversible"].BooleanValue; + string toolSwitchValue; + if (booleanValue) + { + toolSwitchValue = switchList["BasicReversible"].SwitchValue; + } + else + { + toolSwitchValue = switchList["BasicReversible"].ReverseSwitchValue; + } + Assert.IsTrue(toolSwitchValue == "/BrF", "Expected /BrF, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicNonreversible() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicNonreversible", true); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + bool booleanValue = switchList["BasicNonreversible"].BooleanValue; + Assert.IsTrue(booleanValue, "Actual BooleanValue is " + booleanValue.ToString()); + string toolSwitchValue = switchList["BasicNonreversible"].SwitchValue; + Assert.IsTrue(toolSwitchValue == "/Bn", "Expected /Bn, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicString() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicString", "Enum1"); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string CommandLineToolSwitchOutput = switchList["BasicString"].SwitchValue; + Assert.IsTrue(CommandLineToolSwitchOutput == "/Bs1", "Expected /Bs1, got " + CommandLineToolSwitchOutput); + obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicString", "Enum2"); + switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsTrue(switchList != null); + CommandLineToolSwitchOutput = switchList["BasicString"].SwitchValue; + Assert.IsTrue(CommandLineToolSwitchOutput == "/Bs2", "Expected /Bs2, got " + CommandLineToolSwitchOutput); + } + + [TestMethod] + public void TestBasicStringArray() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicStringArray", new string[1]); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string toolSwitchValue = switchList["BasicStringArray"].SwitchValue; + Assert.IsTrue(toolSwitchValue == "/Bsa", "Expected /Bsa, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicFileWSwitch() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicFileWSwitch", "File"); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string toolSwitchValue = switchList["BasicFileWSwitch"].SwitchValue; + Assert.IsTrue(toolSwitchValue == "/Bfws", "Expected /Bfws, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicFileWOSwitch() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicFileWOSwitch", "File"); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string toolSwitchValue = switchList["BasicFileWOSwitch"].SwitchValue; + Assert.IsTrue(String.IsNullOrEmpty(toolSwitchValue), "Expected nothing, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicDynamicEnum() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicDynamicEnum", "MySpecialBeforeTarget"); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string toolSwitchValue = switchList["BasicDynamicEnum"].SwitchValue; + Assert.IsTrue(String.IsNullOrEmpty(toolSwitchValue), "Expected nothing, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicDirectory() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicDirectory", "FakeDirectory"); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string toolSwitchValue = switchList["BasicDirectory"].SwitchValue; + Assert.IsTrue(String.IsNullOrEmpty(toolSwitchValue), "Expected nothing, got " + toolSwitchValue); + } + + [TestMethod] + public void TestBasicInteger() + { + _fakeTaskDll = XamlTestHelpers.SetupGeneratedCode(); + object obj = _fakeTaskDll.CreateInstance("XamlTaskNamespace.FakeTask"); + XamlTestHelpers.SetProperty(obj, "BasicInteger", 1); + Dictionary switchList = (Dictionary)XamlTestHelpers.GetProperty(obj, "ActiveToolSwitches"); + Assert.IsNotNull(switchList); + string CommandLineToolSwitchOutput = switchList["BasicInteger"].SwitchValue + switchList["BasicInteger"].Separator + switchList["BasicInteger"].Number; + Assert.IsTrue(CommandLineToolSwitchOutput == "/Bi1", "Expected /Bi1, got " + CommandLineToolSwitchOutput); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/XamlTestHelpers.cs b/src/XMakeTasks/UnitTests/XamlTestHelpers.cs new file mode 100644 index 00000000000..61036ee0c11 --- /dev/null +++ b/src/XMakeTasks/UnitTests/XamlTestHelpers.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CSharp; +using Microsoft.Build.Tasks.Xaml; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Xaml; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.UnitTests +{ + internal static class XamlTestHelpers + { + // the following are used for the tests in XamlTaskFactory_Tests.cs and XamlDataDrivenToolTask_Tests.cs + // make as robust as possible + private const string fakeXml = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + + public const string QuotingQuotesXml = @" + + + + + + + + + "; + + public const string QuotingBackslashXml = @" + + + + + + + + + "; + + private static string s_pathToMSBuildBinaries = null; + + /// + /// Returns the path to the MSBuild binaries + /// + public static string PathToMSBuildBinaries + { + get + { + if (s_pathToMSBuildBinaries == null) + { + Toolset currentToolset = ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion); + + Assert.IsNotNull(currentToolset, String.Format("For some reason, we couldn't get the current ({0}) toolset!", ObjectModelHelpers.MSBuildDefaultToolsVersion)); + s_pathToMSBuildBinaries = currentToolset.ToolsPath; + } + + return s_pathToMSBuildBinaries; + } + } + + public static Assembly SetupGeneratedCode() + { + return SetupGeneratedCode(fakeXml); + } + + public static Assembly SetupGeneratedCode(string xml) + { + TaskParser tp = null; + try + { + tp = LoadAndParse(xml, "FakeTask"); + } + catch (XamlParseException) + { + Assert.Fail("Parse of FakeTask XML failed"); + } + + TaskGenerator tg = new TaskGenerator(tp); + CodeCompileUnit compileUnit = tg.GenerateCode(); + CodeDomProvider codeGenerator = CodeDomProvider.CreateProvider("CSharp"); + + using (StringWriter sw = new StringWriter(CultureInfo.CurrentCulture)) + { + CodeGeneratorOptions options = new CodeGeneratorOptions(); + options.BlankLinesBetweenMembers = true; + options.BracingStyle = "C"; + + codeGenerator.GenerateCodeFromCompileUnit(compileUnit, sw, options); + CSharpCodeProvider provider = new CSharpCodeProvider(); + // Build the parameters for source compilation. + CompilerParameters cp = new CompilerParameters(); + + // Add an assembly reference. + cp.ReferencedAssemblies.Add("System.dll"); + cp.ReferencedAssemblies.Add("System.Data.dll"); + cp.ReferencedAssemblies.Add("System.XML.dll"); + cp.ReferencedAssemblies.Add(Path.Combine(PathToMSBuildBinaries, "Microsoft.Build.Framework.dll")); + cp.ReferencedAssemblies.Add(Path.Combine(PathToMSBuildBinaries, "Microsoft.Build.Utilities.Core.dll")); + cp.ReferencedAssemblies.Add(Path.Combine(PathToMSBuildBinaries, "Microsoft.Build.Tasks.Core.dll")); + + // Generate an executable instead of + // a class library. + cp.GenerateExecutable = false; + // Set the assembly file name to generate. + cp.GenerateInMemory = true; + // Invoke compilation + CompilerResults cr = provider.CompileAssemblyFromSource(cp, sw.ToString()); + + foreach (CompilerError error in cr.Errors) + { + Console.WriteLine(error.ToString()); + } + if (cr.Errors.Count > 0) + { + Console.WriteLine(sw.ToString()); + } + Assert.IsTrue(cr.Errors.Count == 0, "Compilation Failed with errors: " + cr.Errors.Count.ToString()); + if (cr.Errors.Count > 0) + { + foreach (CompilerError error in cr.Errors) + { + Console.WriteLine(error.ErrorText); + } + } + return cr.CompiledAssembly; + } + } + + /// + /// used for testing. Will load snippets of xml into the task generator + /// + /// + /// + public static TaskParser LoadAndParse(string s, string desiredRule) + { + TaskParser tp = new TaskParser(); + tp.Parse(s.Replace("`", "\""), desiredRule); + return tp; + } + + /// + /// This method is a method to set any property in a task to a certain value + /// + /// + /// + public static void SetProperty(object instance, string propertyName, params object[] parameters) + { + try + { + instance.GetType().InvokeMember(propertyName, BindingFlags.SetProperty, null, instance, parameters, CultureInfo.CurrentCulture); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + } + + /// + /// This method returns the certain attribute for a property (the value it is set to) + /// + /// + /// + public static object GetProperty(object instance, string propertyName, params object[] parameters) + { + try + { + return instance.GetType().InvokeMember(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetProperty, null, instance, parameters, CultureInfo.CurrentCulture); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + } + + /// + /// This method gets called to call the GenerateResponseFileCommands method + /// + /// + /// + public static string GenerateCommandLine(object task) + { + try + { + return (string)task.GetType().InvokeMember("GetCommandLine_ForUnitTestsOnly", BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.InvokeMethod, null, task, new object[] { }); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + } + } +} \ No newline at end of file diff --git a/src/XMakeTasks/UnitTests/XmlPeek_Tests.cs b/src/XMakeTasks/UnitTests/XmlPeek_Tests.cs new file mode 100644 index 00000000000..2180095c9b4 --- /dev/null +++ b/src/XMakeTasks/UnitTests/XmlPeek_Tests.cs @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Copyright (c) 2015 All Right Reserved +// +// 2008-12-28 +// The unit tests for XslTransformation buildtask. + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.IO; +using System.Text.RegularExpressions; +using System.Text; +using System.Xml.Xsl; +using System.Xml; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class XmlPeek_Tests + { + private string _xmlFileWithNs = @" + + + + + + + +"; + + private string _xmlFileWithNsWithText = @" + + + This + is + Sparta! + + +"; + + private string _xmlFileNoNs = @" + + + + + + + +"; + + [TestMethod] + public void PeekWithNamespaceAttribute() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Namespaces = ""; + + Assert.IsTrue(p.Execute(), "Test should've passed"); + Assert.IsTrue(p.Result.Length == 3, "result Length should be 3"); + string[] results = new string[] { "a", "b", "c" }; + for (int i = 0; i < p.Result.Length; i++) + { + Assert.IsTrue(p.Result[i].ItemSpec.Equals(results[i]), "Results don't match: " + p.Result[i].ItemSpec); + } + } + + [TestMethod] + public void PeekWithNamespaceNode() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/."; + p.Namespaces = ""; + + Assert.IsTrue(p.Execute(), "Test should've passed"); + Assert.IsTrue(p.Result.Length == 3, "result Length should be 3"); + + string[] results = new string[] { + "", + "", + "" + }; + + for (int i = 0; i < p.Result.Length; i++) + { + Assert.IsTrue(p.Result[i].ItemSpec.Equals(results[i]), "Results don't match: " + p.Result[i].ItemSpec); + } + } + + [TestMethod] + public void PeekWithNamespaceText() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNsWithText, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/text()"; + p.Namespaces = ""; + Assert.IsTrue(p.Namespaces.Equals("")); + Assert.IsTrue(p.Execute(), "Test should've passed"); + Assert.IsTrue(p.Result.Length == 3, "result Length should be 3"); + + string[] results = new string[] { + "This", + "is", + "Sparta!" + }; + + for (int i = 0; i < p.Result.Length; i++) + { + Assert.IsTrue(p.Result[i].ItemSpec.Equals(results[i]), "Results don't match: " + p.Result[i].ItemSpec); + } + } + + [TestMethod] + public void PeekNoNamespace() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//variable/@Name"; + + Assert.IsTrue(p.Execute(), "Test should've passed"); + Assert.IsTrue(p.Result.Length == 3, "result Length should be 3"); + string[] results = new string[] { "a", "b", "c" }; + for (int i = 0; i < p.Result.Length; i++) + { + Assert.IsTrue(p.Result[i].ItemSpec.Equals(results[i]), "Results don't match: " + p.Result[i].ItemSpec); + } + } + + [TestMethod] + public void PeekNoNSXmlContent() + { + MockEngine engine = new MockEngine(true); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlContent = _xmlFileNoNs; + p.Query = "//variable/@Name"; + + Assert.IsTrue(p.Execute(), "Test should've passed"); + Assert.IsTrue(p.Result.Length == 3, "result Length should be 3"); + string[] results = new string[] { "a", "b", "c" }; + for (int i = 0; i < p.Result.Length; i++) + { + Assert.IsTrue(p.Result[i].ItemSpec.Equals(results[i]), "Results don't match: " + p.Result[i].ItemSpec); + } + } + + [TestMethod] + public void PeekNoNSXmlContentAndXmlInputError1() + { + MockEngine engine = new MockEngine(true); + + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.XmlContent = _xmlFileNoNs; + Assert.IsTrue(p.XmlInputPath.ItemSpec.Equals(xmlInputPath)); + Assert.IsTrue(p.XmlContent.Equals(_xmlFileNoNs)); + + p.Query = "//variable/@Name"; + Assert.IsTrue(p.Query.Equals("//variable/@Name")); + + Assert.IsFalse(p.Execute(), "Test should've failed"); + Assert.IsTrue(engine.Log.Contains("MSB3741"), "Error message MSB3741 should fire"); + } + + [TestMethod] + public void PeekNoNSXmlContentAndXmlInputError2() + { + MockEngine engine = new MockEngine(true); + + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.Query = "//variable/@Name"; + + Assert.IsFalse(p.Execute(), "Test should've failed"); + Assert.IsTrue(engine.Log.Contains("MSB3741"), "Error message MSB3741 should fire"); + } + + [TestMethod] + public void PeekNoNSWPrefixedQueryError() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + + Assert.IsFalse(p.Execute(), "Test should've failed"); + Assert.IsTrue(engine.Log.Contains("MSB3743"), "Engine log should contain error code MSB3743"); + } + + [TestMethod] + public void ErrorInNamespaceDecl() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Namespaces = ""; + + bool executeResult = p.Execute(); + Assert.IsTrue(engine.Log.Contains("MSB3742"), "Engine Log: " + engine.Log); + Assert.IsFalse(executeResult, "Execution should've failed"); + } + + [TestMethod] + public void MissingNamespaceParameters() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + string[] attrs = new string[] { "Prefix=\"s\"", "Uri=\"http://nsurl\"" }; + for (int i = 0; i < Math.Pow(2, attrs.Length); i++) + { + string res = ""; + for (int k = 0; k < attrs.Length; k++) + { + if ((i & (int)Math.Pow(2, k)) != 0) + { + res += attrs[k] + " "; + } + } + XmlPeek p = new XmlPeek(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Namespaces = ""; + + bool result = p.Execute(); + Console.WriteLine(engine.Log); + + if (i == 3) + { + Assert.IsTrue(result, "Only 3rd value should pass."); + } + else + { + Assert.IsFalse(result, "Only 3rd value should pass."); + } + } + } + + [TestMethod] + public void PeekWithoutUsingTask() + { + string projectContents = @" + + + + +"; + + // The task won't complete properly, but ContinueOnError converts the errors to warnings, so the build should succeed + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents); + + // Verify that the task was indeed found. + logger.AssertLogDoesntContain("MSB4036"); + } + + private void Prepare(string xmlFile, out string xmlInputPath) + { + string dir = Path.Combine(Path.GetTempPath(), DateTime.Now.Ticks.ToString()); + Directory.CreateDirectory(dir); + xmlInputPath = dir + "\\doc.xml"; + using (StreamWriter sw = new StreamWriter(xmlInputPath, false)) + { + sw.Write(xmlFile); + sw.Close(); + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/XmlPoke_Tests.cs b/src/XMakeTasks/UnitTests/XmlPoke_Tests.cs new file mode 100644 index 00000000000..ad53313f6a1 --- /dev/null +++ b/src/XMakeTasks/UnitTests/XmlPoke_Tests.cs @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Copyright (c) 2015 All Right Reserved +// +// 2008-12-28 +// The unit tests for XslTransformation buildtask. + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.IO; +using System.Text.RegularExpressions; +using System.Text; +using System.Xml.Xsl; +using System.Xml; + +namespace Microsoft.Build.UnitTests +{ + [TestClass] + sealed public class XmlPoke_Tests + { + private string _xmlFileWithNs = @" + + + + + + +"; + + private string _xmlFileNoNs = @" + + + + + + +"; + + [TestMethod] + public void PokeWithNamespace() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Namespaces = ""; + p.Value = new TaskItem("Mert"); + p.Execute(); + + List positions = new List(); + positions.AddRange(new int[] { 141, 200, 259 }); + + string result; + using (StreamReader sr = new StreamReader(xmlInputPath)) + { + result = sr.ReadToEnd(); + Regex r = new Regex("Mert"); + MatchCollection mc = r.Matches(result); + + foreach (Match m in mc) + { + Assert.IsTrue(positions.Contains(m.Index), "This test should effect 3 positions. There should be 3 occurances of 'Mert'\n" + result); + } + } + } + + [TestMethod] + public void PokeNoNamespace() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//variable/@Name"; + p.Value = new TaskItem("Mert"); + p.Execute(); + + List positions = new List(); + positions.AddRange(new int[] { 117, 172, 227 }); + + string result; + using (StreamReader sr = new StreamReader(xmlInputPath)) + { + result = sr.ReadToEnd(); + Regex r = new Regex("Mert"); + MatchCollection mc = r.Matches(result); + + foreach (Match m in mc) + { + Assert.IsTrue(positions.Contains(m.Index), "This test should effect 3 positions. There should be 3 occurances of 'Mert'\n" + result); + } + } + } + + [TestMethod] + public void PokeAttribute() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//class[1]/@AccessModifier"; + p.Value = new TaskItem("Testing"); + p.Execute(); + string result; + using (StreamReader sr = new StreamReader(xmlInputPath)) + { + result = sr.ReadToEnd(); + Regex r = new Regex("AccessModifier=\"<Test>Testing</Test>\""); + MatchCollection mc = r.Matches(result); + + Assert.IsTrue(mc.Count == 1, "Should match once"); + } + } + + [TestMethod] + public void PokeChildren() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//class/."; + p.Value = new TaskItem("Testing"); + Assert.IsTrue(p.Execute(), engine.Log); + + string result; + using (StreamReader sr = new StreamReader(xmlInputPath)) + { + result = sr.ReadToEnd(); + + Regex r = new Regex("Testing"); + MatchCollection mc = r.Matches(result); + + Assert.IsTrue(mc.Count == 1, "Should match once"); + } + } + + [TestMethod] + public void PokeMissingParams() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + for (int i = 0; i < 8; i++) + { + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + + if ((i & 1) == 1) + { + p.XmlInputPath = new TaskItem(xmlInputPath); + } + + if ((i & 2) == 2) + { + p.Query = "//variable/@Name"; + } + + if ((i & 4) == 4) + { + p.Value = new TaskItem("Mert"); + } + + bool exceptionThrown = false; + try + { + p.Execute(); + } + catch (ArgumentNullException) + { + exceptionThrown = true; + } + + if (i < 7) + { + Assert.IsTrue(exceptionThrown, "Expecting argumentnullexception for the first 7 tests"); + } + else + { + Assert.IsFalse(exceptionThrown, "Expecting argumentnullexception for the first 7 tests"); + } + } + } + + [TestMethod] + public void ErrorInNamespaceDecl() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Namespaces = ""; + Assert.IsTrue(p.Namespaces.Equals("")); + p.Value = new TaskItem("Nur"); + + bool executeResult = p.Execute(); + Assert.IsTrue(engine.Log.Contains("MSB3731")); + Assert.IsFalse(executeResult, "Execution should've failed"); + } + + [TestMethod] + public void PokeNoNSWPrefixedQueryError() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Value = new TaskItem("Nur"); + Assert.IsFalse(p.Execute(), "Test should've failed"); + Assert.IsTrue(engine.Log.Contains("MSB3732"), "Engine log should contain error code MSB3732 " + engine.Log); + } + + [TestMethod] + public void MissingNamespaceParameters() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileWithNs, out xmlInputPath); + + string[] attrs = new string[] { "Prefix=\"s\"", "Uri=\"http://nsurl\"" }; + for (int i = 0; i < Math.Pow(2, attrs.Length); i++) + { + string res = ""; + for (int k = 0; k < attrs.Length; k++) + { + if ((i & (int)Math.Pow(2, k)) != 0) + { + res += attrs[k] + " "; + } + } + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + p.Query = "//s:variable/@Name"; + p.Namespaces = ""; + p.Value = new TaskItem("Nur"); + + bool result = p.Execute(); + + if (i == 3) + { + Assert.IsTrue(result, "Only 3rd value should pass."); + } + else + { + Assert.IsFalse(result, "Only 3rd value should pass."); + } + } + } + + [TestMethod] + public void PokeElement() + { + MockEngine engine = new MockEngine(true); + string xmlInputPath; + Prepare(_xmlFileNoNs, out xmlInputPath); + + XmlPoke p = new XmlPoke(); + p.BuildEngine = engine; + p.XmlInputPath = new TaskItem(xmlInputPath); + Assert.IsTrue(p.XmlInputPath.ItemSpec.Equals(xmlInputPath)); + p.Query = "//variable/."; + Assert.IsTrue(p.Query.Equals("//variable/.")); + string valueString = "With"; + p.Value = new TaskItem(valueString); + Assert.IsTrue(p.Value.ItemSpec.Equals(valueString)); + + Assert.IsTrue(p.Execute()); + + List positions = new List(); + positions.AddRange(new int[] { 126, 249, 372 }); + + string result; + using (StreamReader sr = new StreamReader(xmlInputPath)) + { + result = sr.ReadToEnd(); + + Regex r = new Regex("With"); + MatchCollection mc = r.Matches(result); + + foreach (Match m in mc) + { + Assert.IsTrue(positions.Contains(m.Index), "This test should effect 3 positions. There should be 3 occurances of 'Mert'\n" + result); + } + } + } + + [TestMethod] + public void PokeWithoutUsingTask() + { + string projectContents = @" + + + + +"; + + // The task will error, but ContinueOnError means that it will just be a warning. + MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents); + + // Verify that the task was indeed found. + logger.AssertLogDoesntContain("MSB4036"); + } + + private void Prepare(string xmlFile, out string xmlInputPath) + { + string dir = Path.Combine(Path.GetTempPath(), DateTime.Now.Ticks.ToString()); + Directory.CreateDirectory(dir); + xmlInputPath = dir + "\\doc.xml"; + using (StreamWriter sw = new StreamWriter(xmlInputPath, false)) + { + sw.Write(xmlFile); + sw.Close(); + } + } + } +} diff --git a/src/XMakeTasks/UnitTests/XslTransformation_Tests.cs b/src/XMakeTasks/UnitTests/XslTransformation_Tests.cs new file mode 100644 index 00000000000..a82c305c81c --- /dev/null +++ b/src/XMakeTasks/UnitTests/XslTransformation_Tests.cs @@ -0,0 +1,1235 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Copyright (c) 2015 All Right Reserved +// +// 2008-12-28 +// The unit tests for XslTransformation buildtask. + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.IO; +using System.Text.RegularExpressions; +using System.Text; +using System.Xml.Xsl; +using System.Xml; + +namespace Microsoft.Build.UnitTests +{ + /// + /// These tests run. The temporary output folder for this test is Path.Combine(Path.GetTempPath(), DateTime.Now.Ticks.ToString()) + /// 1. When combination of (xml, xmlfile) x (xsl, xslfile). + /// 2. When Xsl parameters are missing. + /// 3. When Xml parameters are missing. + /// 4. Both missing. + /// 5. Too many Xml parameters. + /// 6. Too many Xsl paramters. + /// 7. Setting Out parameter to file. + /// 8. Setting Out parameter to screen. + /// 9. Setting correct "Parameter" parameters for Xsl. + /// 10. Setting the combination of "Parameter" parameters (Name, Namespace, Value) and testing the cases when they should run ok. + /// 11. Setting "Parameter" parameter as empty string (should run OK). + /// 12. Compiled Dll with type information. + /// 13. Compiled Dll without type information. + /// 14. Load Xslt with incorrect character as CNAME (load exception). + /// 15. Missing XmlFile file. + /// 16. Missing XslFile file. + /// 17. Missing XsltCompiledDll file. + /// 18. Bad XML on "Parameter" parameter. + /// 19. Out parameter pointing to nonexistent location (K:\folder\file.xml) + /// 20. XslDocument that throws runtime exception. + /// 21. Passing a dll that has two types to XsltCompiledDll parameter without specifying a type. + /// + [TestClass] + sealed public class XslTransformation_Tests + { + /// + /// The "surround" regex. + /// + private readonly Regex _surroundMatch = new Regex("surround", RegexOptions.Multiline | RegexOptions.Compiled); + + /// + /// The contents of xmldocument for tests. + /// + private readonly string _xmlDocument = ""; + + /// + /// The contents of another xmldocument for tests. + /// + private readonly string _xmlDocument2 = ""; + + /// + /// The contents of xsl document for tests. + /// + private readonly string _xslDocument = ""; + + /// + /// The contents of another xsl document for tests + /// + private readonly string _xslDocument2 = ""; + + /// + /// The contents of xslparameters for tests. + /// + private readonly string _xslParameters = ""; + + /// + /// The contents of xslt file for testing parameters. + /// + private readonly string _xslParameterDocument = "param 1: param 2: "; + + /// + /// The errorious xsl documents + /// + private readonly string _errorXslDocument = ""; + + /// + /// The errorious xsl document 2. + /// + private readonly string _errorXslDocument2 = "error?"; + + /// + /// When combination of (xml, xmlfile) x (xsl, xslfile). + /// + [TestMethod] + public void XmlXslParameters() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test when Xml and Xsl parameters are correct + for (int xmi = 0; xmi < xmlInputs.Count; xmi++) + { + for (int xsi = 0; xsi < xslInputs.Count; xsi++) + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + XslTransformation.XmlInput.XmlModes xmlKey = xmlInputs[xmi].Key; + object xmlValue = xmlInputs[xmi].Value; + XslTransformation.XsltInput.XslModes xslKey = xslInputs[xsi].Key; + object xslValue = xslInputs[xsi].Value; + + switch (xmlKey) + { + case XslTransformation.XmlInput.XmlModes.Xml: + t.XmlContent = (string)xmlValue; + break; + case XslTransformation.XmlInput.XmlModes.XmlFile: + t.XmlInputPaths = (TaskItem[])xmlValue; + break; + default: + Assert.Fail("Test error"); + break; + } + + switch (xslKey) + { + case XslTransformation.XsltInput.XslModes.Xslt: + t.XslContent = (string)xslValue; + break; + case XslTransformation.XsltInput.XslModes.XsltFile: + t.XslInputPath = (TaskItem)xslValue; + break; + case XslTransformation.XsltInput.XslModes.XsltCompiledDll: + t.XslCompiledDllPath = (TaskItem)xslValue; + break; + default: + Assert.Fail("Test error"); + break; + } + + Assert.IsTrue(t.Execute(), "The test should have passed at the both params correct test"); + } + } + + CleanUp(dir); + } + + /// + /// When Xsl parameters are missing. + /// + [TestMethod] + public void MissingXslParameter() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // test Xsl missing. + for (int xmi = 0; xmi < xmlInputs.Count; xmi++) + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + + XslTransformation.XmlInput.XmlModes xmlKey = xmlInputs[xmi].Key; + object xmlValue = xmlInputs[xmi].Value; + switch (xmlKey) + { + case XslTransformation.XmlInput.XmlModes.Xml: + t.XmlContent = (string)xmlValue; + break; + case XslTransformation.XmlInput.XmlModes.XmlFile: + t.XmlInputPaths = (TaskItem[])xmlValue; + break; + default: + Assert.Fail("Test error"); + break; + } + + Assert.IsFalse(t.Execute(), "The test should fail when there is missing Xsl params"); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3701"), "The output should contain MSB3701 error message at missing Xsl params test"); + } + + CleanUp(dir); + } + + /// + /// When Xml parameters are missing. + /// + [TestMethod] + public void MissingXmlParameter() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test Xml missing. + for (int xsi = 0; xsi < xslInputs.Count; xsi++) + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + + XslTransformation.XsltInput.XslModes xslKey = xslInputs[xsi].Key; + object xslValue = xslInputs[xsi].Value; + switch (xslKey) + { + case XslTransformation.XsltInput.XslModes.Xslt: + t.XslContent = (string)xslValue; + break; + case XslTransformation.XsltInput.XslModes.XsltFile: + t.XslInputPath = (TaskItem)xslValue; + break; + case XslTransformation.XsltInput.XslModes.XsltCompiledDll: + t.XslCompiledDllPath = (TaskItem)xslValue; + break; + default: + Assert.Fail("Test error"); + break; + } + + Assert.IsFalse(t.Execute(), "The test should fail when there is missing Xml params"); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3701"), "The output should contain MSB3701 error message at missing Xml params test"); + engine.Log = ""; + } + + CleanUp(dir); + } + + /// + /// Both missing. + /// + [TestMethod] + public void MissingXmlXslParameter() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test both missing. + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + + Assert.IsFalse(t.Execute(), "The test should fail when there is no params"); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3701"), "The output should contain MSB3701 error message"); + } + + CleanUp(dir); + } + + /// + /// Too many Xml parameters. + /// + [TestMethod] + public void ManyXmlParameters() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test too many Xml. + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XmlInputPaths = xmlPaths; + t.XslContent = _xslDocument; + Assert.IsTrue(t.XmlContent.Equals(_xmlDocument)); + Assert.IsTrue(t.XmlInputPaths.Equals(xmlPaths)); + Assert.IsFalse(t.Execute(), "The test should fail when there are too many files"); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3701"), "The output should contain MSB3701 error message" + engine.Log); + } + + CleanUp(dir); + } + + /// + /// Too many Xsl paramters. + /// + [TestMethod] + public void ManyXslParameters() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test too many Xsl. + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _xslDocument; + t.XslInputPath = xslPath; + Assert.IsTrue(t.XslContent.Equals(_xslDocument)); + Assert.IsTrue(t.XslInputPath.Equals(xslPath)); + Assert.IsFalse(t.Execute(), "The test should fail when there are too many files"); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3701"), "The output should contain MSB3701 error message at no params test"); + } + + CleanUp(dir); + } + + /// + /// Test out parameter. + /// + [TestMethod] + public void OutputTest() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test Out + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.XmlContent = _xmlDocument; + t.XslContent = _xslDocument; + t.OutputPaths = outputPaths; + Assert.IsTrue(t.Execute(), "Test out should have given true when executed"); + Assert.IsTrue(engine.Log.Equals(String.Empty), "The log should be empty"); + Console.WriteLine(engine.Log); + using (StreamReader sr = new StreamReader(t.OutputPaths[0].ItemSpec)) + { + string fileContents = sr.ReadToEnd(); + MatchCollection mc = _surroundMatch.Matches(fileContents); + Assert.IsTrue(mc.Count == 8, "The file test doesn't match"); + } + } + + CleanUp(dir); + } + + /// + /// Setting correct "Parameter" parameters for Xsl. + /// + [TestMethod] + public void XsltParamatersCorrect() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test Correct Xslt Parameters + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _xslParameterDocument; + t.Parameters = _xslParameters; + t.Execute(); + Console.WriteLine(engine.Log); + using (StreamReader sr = new StreamReader(t.OutputPaths[0].ItemSpec)) + { + string fileContents = sr.ReadToEnd(); + Assert.IsTrue(fileContents.Contains("param 1: 1param 2: 2")); + } + } + + CleanUp(dir); + } + + /// + /// Setting the combination of "Parameter" parameters (Name, Namespace, Value) and testing the cases when they should run ok. + /// + [TestMethod] + public void XsltParametersIncorrect() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test Xslt Parameters + { + string[] attrs = new string[] { "Name=\"param2\"", "Namespace=\"http://eksiduyuru.com\"", "Value=\"2\"" }; + for (int i = 0; i < Math.Pow(2, attrs.Length); i++) + { + string res = ""; + for (int k = 0; k < attrs.Length; k++) + { + if ((i & (int)Math.Pow(2, k)) != 0) + { + res += attrs[k] + " "; + } + } + + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _xslParameterDocument; + t.Parameters = ""; + Assert.IsTrue(t.Parameters.Equals("")); + bool result = t.Execute(); + Console.WriteLine(engine.Log); + + if (i == 5 || i == 7) + { + Assert.IsTrue(result, "Only 5th and 7th values should pass."); + } + else + { + Assert.IsFalse(result, "Only 5th and 7th values should pass."); + } + } + } + + CleanUp(dir); + } + + /// + /// Setting "Parameter" parameter as empty string (should run OK). + /// + [TestMethod] + public void EmptyParameters() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load empty parameters + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlInputPaths = xmlPaths; + t.XslInputPath = xslPath; + t.Parameters = " "; + Assert.IsTrue(t.Execute(), "This test should've passed (empty parameters)."); + Console.WriteLine(engine.Log); + } + + CleanUp(dir); + } + + /// + /// Compiled Dll with type information. + /// + [TestMethod] + public void CompiledDllWithType() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // Test Compiled DLLs + + // with type specified. + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + xslCompiledPath.ItemSpec = xslCompiledPath.ItemSpec + ";xslt"; + t.XslCompiledDllPath = xslCompiledPath; + Assert.IsTrue(t.XslCompiledDllPath.ItemSpec.Equals(xslCompiledPath.ItemSpec)); + Assert.IsTrue(t.Execute(), "XsltComiledDll1 execution should've passed"); + Console.WriteLine(engine.Log); + Assert.IsFalse(engine.Log.Contains("MSB"), "The log should not contain any errors. (XsltComiledDll1)"); + } + + CleanUp(dir); + } + + /// + /// Compiled Dll without type information. + /// + [TestMethod] + public void CompiledDllWithoutType() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // without type specified. + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslCompiledDllPath = xslCompiledPath; + Assert.IsTrue(t.Execute(), "XsltComiledDll2 execution should've passed" + engine.Log); + Console.WriteLine(engine.Log); + Assert.IsFalse(engine.MockLogger.ErrorCount > 0, "The log should not contain any errors. (XsltComiledDll2)"); + } + + CleanUp(dir); + } + + /// + /// Load Xslt with incorrect character as CNAME (load exception). + /// + [TestMethod] + public void BadXsltFile() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load bad xslt + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _errorXslDocument; + try + { + t.Execute(); + Console.WriteLine(engine.Log); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("The '$' character")); + } + } + + CleanUp(dir); + } + + /// + /// Load Xslt with incorrect character as CNAME (load exception). + /// + [TestMethod] + [ExpectedException(typeof(System.ArgumentNullException))] + public void MissingOutputFile() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load missing xml + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.XmlInputPaths = xmlPaths; + t.XslInputPath = xslPath; + Assert.IsFalse(t.Execute(), "This test should've failed (no output)."); + Console.WriteLine(engine.Log); + } + + CleanUp(dir); + } + + /// + /// Missing XmlFile file. + /// + [TestMethod] + public void MissingXmlFile() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load missing xml + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + xmlPaths[0].ItemSpec = xmlPaths[0].ItemSpec + "bad"; + t.XmlInputPaths = xmlPaths; + t.XslInputPath = xslPath; + Console.WriteLine(engine.Log); + Assert.IsFalse(t.Execute(), "This test should've failed (bad xml)."); + Assert.IsTrue(engine.Log.Contains("MSB3703")); + } + + CleanUp(dir); + } + + /// + /// Missing XslFile file. + /// + [TestMethod] + public void MissingXsltFile() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load missing xsl + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlInputPaths = xmlPaths; + xslPath.ItemSpec = xslPath.ItemSpec + "bad"; + t.XslInputPath = xslPath; + Assert.IsFalse(t.Execute(), "This test should've failed (bad xslt)."); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3704")); + } + + CleanUp(dir); + } + + /// + /// Missing XsltCompiledDll file. + /// + [TestMethod] + public void MissingCompiledDllFile() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // missing xsltCompiledDll + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + xslCompiledPath.ItemSpec = xslCompiledPath.ItemSpec + "bad;xslt"; + t.XslCompiledDllPath = xslCompiledPath; + Assert.IsFalse(t.Execute(), "XsltComiledDllBad execution should've failed"); + Console.WriteLine(engine.Log); + Assert.IsTrue(engine.Log.Contains("MSB3704")); + System.Diagnostics.Debug.WriteLine(engine.Log); + } + + CleanUp(dir); + } + + /// + /// Bad XML on "Parameter" parameter. + /// + [TestMethod] + public void BadXmlAsParameter() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load bad xml on parameters + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _xslParameterDocument; + t.Parameters = "<<>>"; + try + { + Assert.IsFalse(t.Execute(), "This test should've failed (bad params1)."); + Console.WriteLine(engine.Log); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("'<'")); + } + } + + CleanUp(dir); + } + + /// + /// Out parameter pointing to nonexistent location (K:\folder\file.xml) + /// + [TestMethod] + public void OutputFileCannotBeWritten() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load bad output + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _xslDocument; + t.OutputPaths = new TaskItem[] { new TaskItem("k:\\folder\\file.xml") }; + try + { + Assert.IsFalse(t.Execute(), "This test should've failed (bad output)."); + Console.WriteLine(engine.Log); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("MSB3701")); + } + } + + CleanUp(dir); + } + + /// + /// XslDocument that throws runtime exception. + /// + [TestMethod] + public void XsltDocumentThrowsError() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // load error xslDocument + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslContent = _errorXslDocument2; + try + { + Assert.IsFalse(t.Execute(), "This test should've failed (xsl with error)."); + Console.WriteLine(engine.Log); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("error?")); + } + } + + CleanUp(dir); + } + + /// + /// Passing a dll that has two types to XsltCompiledDll parameter without specifying a type. + /// + [TestMethod] + public void CompiledDllWithTwoTypes() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // doubletype + string doubleTypePath = Path.Combine(dir, "double.dll"); + CompileDoubleType(doubleTypePath); + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlContent = _xmlDocument; + t.XslCompiledDllPath = new TaskItem(doubleTypePath); + try + { + t.Execute(); + Console.WriteLine(engine.Log); + } + catch (Exception e) + { + Assert.IsTrue(e.Message.Contains("error?")); + } + + System.Diagnostics.Debug.WriteLine(engine.Log); + } + + CleanUp(dir); + } + + /// + /// Matching XmlInputPaths and OutputPaths + /// + [TestMethod] + public void MultipleXmlInputs_Matching() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + var otherXmlPath = new TaskItem(Path.Combine(dir, Guid.NewGuid().ToString())); + using (StreamWriter sw = new StreamWriter(otherXmlPath.ItemSpec, false)) + { + sw.Write(_xmlDocument2); + } + + // xmlPaths have one XmlPath, lets duplicate it + TaskItem[] xmlMultiPaths = new TaskItem[] { xmlPaths[0], otherXmlPath, xmlPaths[0], xmlPaths[0] }; + + // outputPaths have one output path, lets duplicate it + TaskItem[] outputMultiPaths = new TaskItem[] { new TaskItem(outputPaths[0].ItemSpec + ".1.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".2.xml"), new TaskItem(outputPaths[0].ItemSpec + ".3.xml"), new TaskItem(outputPaths[0].ItemSpec + ".4.xml") }; + + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.XslInputPath = xslPath; + t.XmlInputPaths = xmlMultiPaths; + t.OutputPaths = outputMultiPaths; + Assert.IsTrue(t.Execute(), "CompiledDllWithTwoTypes execution should've passed" + engine.Log); + Console.WriteLine(engine.Log); + foreach (TaskItem tsk in t.OutputPaths) + { + Assert.IsTrue(File.Exists(tsk.ItemSpec), tsk.ItemSpec + " should exist on output dir"); + } + + // The first and second input XML files are not equivalent, so their output files + // should be different + Assert.AreNotEqual(new FileInfo(xmlMultiPaths[0].ItemSpec).Length, new FileInfo(xmlMultiPaths[1].ItemSpec).Length); + Assert.AreNotEqual(new FileInfo(outputMultiPaths[0].ItemSpec).Length, new FileInfo(outputMultiPaths[1].ItemSpec).Length); + + System.Diagnostics.Debug.WriteLine(engine.Log); + } + + CleanUp(dir); + } + + /// + /// Not Matching XmlInputPaths and OutputPaths + /// + [TestMethod] + public void MultipleXmlInputs_NotMatching() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + // xmlPaths have one XmlPath, lets duplicate it **4 times ** + TaskItem[] xmlMultiPaths = new TaskItem[] { xmlPaths[0], xmlPaths[0], xmlPaths[0], xmlPaths[0] }; + + // outputPaths have one output path, lets duplicate it **3 times ** + TaskItem[] outputMultiPathsShort = new TaskItem[] { new TaskItem(outputPaths[0].ItemSpec + ".1.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".2.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".3.xml") }; + + TaskItem[] outputMultiPathsLong = new TaskItem[] { new TaskItem(outputPaths[0].ItemSpec + ".1.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".2.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".3.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".4.xml"), + new TaskItem(outputPaths[0].ItemSpec + ".5.xml") }; + // Short version. + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.XslInputPath = xslPath; + t.XmlInputPaths = xmlMultiPaths; + t.OutputPaths = outputMultiPathsShort; + Assert.IsFalse(t.Execute(), "CompiledDllWithTwoTypes execution should've failed" + engine.Log); + + System.Diagnostics.Debug.WriteLine(engine.Log); + } + + // Long version + { + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.XslInputPath = xslPath; + t.XmlInputPaths = xmlMultiPaths; + t.OutputPaths = outputMultiPathsLong; + Assert.IsFalse(t.Execute(), "CompiledDllWithTwoTypes execution should've failed" + engine.Log); + Console.WriteLine(engine.Log); + + System.Diagnostics.Debug.WriteLine(engine.Log); + } + + CleanUp(dir); + } + + /// + /// Validate that the XslTransformation task allows use of the document function + /// + [TestMethod] + public void XslDocumentFunctionWorks() + { + string dir; + TaskItem[] xmlPaths; + TaskItem xslPath; + TaskItem xslCompiledPath; + TaskItem[] outputPaths; + List> xmlInputs; + List> xslInputs; + MockEngine engine; + Prepare(out dir, out xmlPaths, out xslPath, out xslCompiledPath, out outputPaths, out xmlInputs, out xslInputs, out engine); + + var otherXslPath = new TaskItem(Path.Combine(dir, Guid.NewGuid().ToString() + ".xslt")); + using (StreamWriter sw = new StreamWriter(otherXslPath.ItemSpec, false)) + { + sw.Write(_xslDocument2); + } + + // Initialize first xml file for the XslTransformation task to consume + var myXmlPath1 = new TaskItem(Path.Combine(dir, "a.xml")); + using (StreamWriter sw = new StreamWriter(myXmlPath1.ItemSpec, false)) + { + sw.Write(""); + } + + // Initialize second xml file for the first one to consume + var myXmlPath2 = new TaskItem(Path.Combine(dir, "b.xml")); + using (StreamWriter sw = new StreamWriter(myXmlPath2.ItemSpec, false)) + { + sw.Write(""); + } + + // Validate that execution passes when UseTrustedSettings is true + XslTransformation t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlInputPaths = new TaskItem[] { myXmlPath1 }; + t.XslInputPath = otherXslPath; + t.UseTrustedSettings = true; + + Assert.IsTrue(t.Execute(), "Test should have passed and allowed the use of the document() function within the xslt file"); + + // Validate that execution fails when UseTrustedSettings is false + t = new XslTransformation(); + t.BuildEngine = engine; + t.OutputPaths = outputPaths; + t.XmlInputPaths = new TaskItem[] { myXmlPath1 }; + t.XslInputPath = otherXslPath; + t.UseTrustedSettings = false; + + Assert.IsFalse(t.Execute(), "Test should have failed and not allowed the use of the document() function within the xslt file"); + + CleanUp(dir); + } + + /// + /// Prepares the test environment, creates necessary files. + /// + /// The temp dir + /// The xml file's path + /// The xsl file's path + /// The xsl dll's path + /// The output file's path + /// The xml input ways + /// The xsl input ways + /// The Mock engine + private void Prepare(out string dir, out TaskItem[] xmlPaths, out TaskItem xslPath, out TaskItem xslCompiledPath, out TaskItem[] outputPaths, out List> xmlInputs, out List> xslInputs, out MockEngine engine) + { + dir = Path.Combine(Path.GetTempPath(), DateTime.Now.Ticks.ToString()); + Directory.CreateDirectory(dir); + + // save XML and XSLT documents. + xmlPaths = new TaskItem[] { new TaskItem(Path.Combine(dir, "doc.xml")) }; + xslPath = new TaskItem(Path.Combine(dir, "doc.xslt")); + xslCompiledPath = new TaskItem(Path.Combine(dir, "doc.dll")); + outputPaths = new TaskItem[] { new TaskItem(Path.Combine(dir, "testout.xml")) }; + using (StreamWriter sw = new StreamWriter(xmlPaths[0].ItemSpec, false)) + { + sw.Write(_xmlDocument); + sw.Close(); + } + + using (StreamWriter sw = new StreamWriter(xslPath.ItemSpec, false)) + { + sw.Write(_xslDocument); + sw.Close(); + } + + xmlInputs = new List>(); + xslInputs = new List>(); + + xmlInputs.Add(new KeyValuePair(XslTransformation.XmlInput.XmlModes.Xml, _xmlDocument)); + xmlInputs.Add(new KeyValuePair(XslTransformation.XmlInput.XmlModes.XmlFile, xmlPaths)); + + xslInputs.Add(new KeyValuePair(XslTransformation.XsltInput.XslModes.Xslt, _xslDocument)); + xslInputs.Add(new KeyValuePair(XslTransformation.XsltInput.XslModes.XsltFile, xslPath)); + + Compile(xslPath.ItemSpec, xslCompiledPath.ItemSpec); + + engine = new MockEngine(); + List results = new List(); + } + + /// + /// Clean ups the test files + /// + /// The directory for temp files. + private void CleanUp(string dir) + { + try + { + Directory.Delete(dir, true); + } + catch + { + } + } + + #region Compiler + +#pragma warning disable 0618 // XmlReaderSettings.ProhibitDtd is obsolete + + /// + /// Compiles given stylesheets into an assembly. + /// + private void Compile(string inputFile, string outputFile) + { + const string CompiledQueryName = "xslt"; + string outputDir = Path.GetDirectoryName(outputFile) + Path.DirectorySeparatorChar; + XsltSettings xsltSettings = new XsltSettings(true, true); + + XmlUrlResolver xmlResolver = new XmlUrlResolver(); + XmlReaderSettings readerSettings = new XmlReaderSettings(); + + AssemblyBuilder asmBldr; + + readerSettings.ProhibitDtd = false; + readerSettings.XmlResolver = xmlResolver; + + string scriptAsmPathPrefix = outputDir + Path.GetFileNameWithoutExtension(outputFile) + ".script"; + + // Create assembly and module builders + AssemblyName asmName = new AssemblyName(); + asmName.Name = CompiledQueryName; + + asmBldr = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Save, outputDir); + + // Add custom attribute to assembly marking it as security transparent so that Assert will not be allowed + // and link demands will be converted to full demands. + asmBldr.SetCustomAttribute(new CustomAttributeBuilder(typeof(System.Security.SecurityTransparentAttribute).GetConstructor(Type.EmptyTypes), new object[] { })); + + // Mark the assembly with GeneratedCodeAttribute to improve profiling experience + asmBldr.SetCustomAttribute(new CustomAttributeBuilder(typeof(GeneratedCodeAttribute).GetConstructor(new Type[] { typeof(string), typeof(string) }), new object[] { "XsltCompiler", "2.0.0.0" })); + + ModuleBuilder modBldr = asmBldr.DefineDynamicModule(Path.GetFileName(outputFile), Path.GetFileName(outputFile), true); + + string sourceUri = inputFile; + string className = Path.GetFileNameWithoutExtension(inputFile); + string scriptAsmId = ""; + + // Always use the .dll extension; otherwise Fusion won't be able to locate this dependency + string scriptAsmPath = scriptAsmPathPrefix + scriptAsmId + ".dll"; + + // Create TypeBuilder and compile the stylesheet into it + TypeBuilder typeBldr = modBldr.DefineType(CompiledQueryName, TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit); + + CompilerErrorCollection errors = null; + try + { + using (XmlReader reader = XmlReader.Create(sourceUri, readerSettings)) + { + errors = XslCompiledTransform.CompileToType( + reader, xsltSettings, xmlResolver, false, typeBldr, scriptAsmPath + ); + } + } + catch (Exception e) + { + Assert.Fail("Compiler didn't work" + e.ToString()); + } + + asmBldr.Save(Path.GetFileName(outputFile), PortableExecutableKinds.ILOnly, ImageFileMachine.I386); + } + +#pragma warning restore 0618 + + /// + /// Creates a dll that has 2 types in it. + /// + /// The dll name. + private void CompileDoubleType(string outputFile) + { + string outputDir = Path.GetDirectoryName(outputFile) + Path.DirectorySeparatorChar; + const string CompiledQueryName = "xslt"; + + AssemblyBuilder asmBldr; + + // Create assembly and module builders + AssemblyName asmName = new AssemblyName(); + asmName.Name = "assmname"; + + asmBldr = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Save, outputDir); + + ModuleBuilder modBldr = asmBldr.DefineDynamicModule(Path.GetFileName(outputFile), Path.GetFileName(outputFile), true); + + // Create TypeBuilder and compile the stylesheet into it + TypeBuilder typeBldr = modBldr.DefineType(CompiledQueryName, TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit); + + typeBldr.DefineField("x", typeof(int), FieldAttributes.Private); + + TypeBuilder typeBldr2 = modBldr.DefineType(CompiledQueryName + "2", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit); + + typeBldr2.DefineField("x", typeof(int), FieldAttributes.Private); + + typeBldr.CreateType(); + typeBldr2.CreateType(); + + asmBldr.Save(Path.GetFileName(outputFile), PortableExecutableKinds.ILOnly, ImageFileMachine.I386); + } + #endregion + } +} diff --git a/src/XMakeTasks/UnregisterAssembly.cs b/src/XMakeTasks/UnregisterAssembly.cs new file mode 100644 index 00000000000..b82442cbdf5 --- /dev/null +++ b/src/XMakeTasks/UnregisterAssembly.cs @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Resources; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Threading; +using System.Runtime.InteropServices.ComTypes; + +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Registers a managed assembly for COM interop (equivalent of regasm.exe functionality, but this code + /// doesn't actually call the exe). + /// + public class UnregisterAssembly : AppDomainIsolatedTaskExtension + { + #region Constructors + + /// + /// Default constructor + /// + public UnregisterAssembly() + { + // do nothing + } + + #endregion + + #region Properties + + public ITaskItem[] Assemblies + { + get { return _assemblies; } + set { _assemblies = value; } + } + + private ITaskItem[] _assemblies = null; + + public ITaskItem[] TypeLibFiles + { + get { return _typeLibFiles; } + set { _typeLibFiles = value; } + } + + private ITaskItem[] _typeLibFiles = null; + + /// + /// The cache file for Register/UnregisterAssembly. Necessary for UnregisterAssembly to do the proper clean up. + /// + public ITaskItem AssemblyListFile + { + get { return _assemblyListFile; } + set { _assemblyListFile = value; } + } + + private ITaskItem _assemblyListFile = null; + + #endregion + + #region ITask members + + /// + /// Task entry point + /// + /// + override public bool Execute() + { + AssemblyRegistrationCache cacheFile = null; + + if (AssemblyListFile != null) + { + cacheFile = (AssemblyRegistrationCache)StateFileBase.DeserializeCache(AssemblyListFile.ItemSpec, Log, typeof(AssemblyRegistrationCache)); + + // no cache file, nothing to do. In case there was a problem reading the cache file, we can't do anything anyway. + if (cacheFile == null) + { + StateFileBase.DeleteFile(AssemblyListFile.ItemSpec, Log); + return true; + } + } + else + { + if (Assemblies == null) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.AssemblyPathOrStateFileIsRequired", GetType().Name); + return false; + } + + // TypeLibFiles isn't [Required], but if it is specified, it must have the same length as Assemblies + if (TypeLibFiles != null && TypeLibFiles.Length != Assemblies.Length) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", Assemblies.Length, TypeLibFiles.Length, "Assemblies", "TypeLibFiles"); + return false; + } + + cacheFile = new AssemblyRegistrationCache(); + + for (int i = 0; i < Assemblies.Length; i++) + { + // if the type lib path is not supplied, generate default one + if (TypeLibFiles != null && TypeLibFiles[i] != null && TypeLibFiles[i].ItemSpec.Length > 0) + { + cacheFile.AddEntry(Assemblies[i].ItemSpec, TypeLibFiles[i].ItemSpec); + } + else + { + cacheFile.AddEntry(Assemblies[i].ItemSpec, Path.ChangeExtension(Assemblies[i].ItemSpec, ".tlb")); + } + } + } + + bool taskReturnValue = true; + + try + { + for (int i = 0; i < cacheFile.Count; i++) + { + string assemblyPath = null, typeLibraryPath = null; + + cacheFile.GetEntry(i, out assemblyPath, out typeLibraryPath); + + try + { + // If one of assemblies failed to unregister, the whole task failed. + // We still process the rest of assemblies though. + if (!Unregister(assemblyPath, typeLibraryPath)) + { + taskReturnValue = false; + } + } + catch (ArgumentException ex) // assembly path has invalid chars in it + { + Log.LogErrorWithCodeFromResources("General.InvalidAssemblyName", assemblyPath, ex.Message); + taskReturnValue = false; + } +#if _DEBUG + catch (Exception e) + { + Debug.Assert(false, "Unexpected exception in AssemblyRegistration.Execute. " + + "Please log a MSBuild bug specifying the steps to reproduce the problem. " + + e.Message); + throw; + } +#endif + } + } + finally + { + if (AssemblyListFile != null) + { + StateFileBase.DeleteFile(AssemblyListFile.ItemSpec, Log); + } + } + + return taskReturnValue; + } + + #endregion + + #region Methods + + /// + /// Helper unregistration method + /// + /// + /// + /// + private bool Unregister(string assemblyPath, string typeLibPath) + { + ErrorUtilities.VerifyThrowArgumentNull(typeLibPath, "typeLibPath"); + + Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisteringAssembly", assemblyPath); + + if (File.Exists(assemblyPath)) + { + try + { + // Load the specified assembly. + Assembly asm = Assembly.UnsafeLoadFrom(assemblyPath); + + RegistrationServices comRegistrar = new RegistrationServices(); + + try + { + s_unregisteringLock.WaitOne(); + + // Unregister the assembly + if (!comRegistrar.UnregisterAssembly(asm)) + { + // If the assembly doesn't contain any types that could be registered for COM interop, + // warn the user about it + Log.LogWarningWithCodeFromResources("UnregisterAssembly.NoValidTypes", assemblyPath); + } + } + finally + { + s_unregisteringLock.ReleaseMutex(); + } + } + catch (ArgumentNullException e) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.CantUnregisterAssembly", assemblyPath, e.Message); + return false; + } + catch (InvalidOperationException e) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.CantUnregisterAssembly", assemblyPath, e.Message); + return false; + } + catch (TargetInvocationException e) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.CantUnregisterAssembly", assemblyPath, e.Message); + return false; + } + catch (IOException e) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.CantUnregisterAssembly", assemblyPath, e.Message); + return false; + } + catch (TypeLoadException e) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.CantUnregisterAssembly", assemblyPath, e.Message); + return false; + } + catch (UnauthorizedAccessException e) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.UnauthorizedAccess", assemblyPath, e.Message); + return false; + } + catch (BadImageFormatException) + { + Log.LogErrorWithCodeFromResources("General.InvalidAssembly", assemblyPath); + return false; + } + catch (SecurityException e) // running as normal user + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.UnauthorizedAccess", assemblyPath, e.Message); + return false; + } + } + else + Log.LogWarningWithCodeFromResources("UnregisterAssembly.UnregisterAsmFileDoesNotExist", assemblyPath); + + Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisteringTypeLib", typeLibPath); + + if (File.Exists(typeLibPath)) + { + try + { + ITypeLib typeLibrary = (ITypeLib)NativeMethods.LoadTypeLibEx(typeLibPath, (int)NativeMethods.REGKIND.REGKIND_NONE); + + // Get the library attributes so we can unregister it + IntPtr pTlibAttr = IntPtr.Zero; + + try + { + typeLibrary.GetLibAttr(out pTlibAttr); + if (pTlibAttr != IntPtr.Zero) + { + // Unregister the type library + System.Runtime.InteropServices.ComTypes.TYPELIBATTR tlibattr = (System.Runtime.InteropServices.ComTypes.TYPELIBATTR)Marshal.PtrToStructure(pTlibAttr, typeof(System.Runtime.InteropServices.ComTypes.TYPELIBATTR)); + NativeMethods.UnregisterTypeLib(ref tlibattr.guid, tlibattr.wMajorVerNum, tlibattr.wMinorVerNum, tlibattr.lcid, tlibattr.syskind); + } + } + finally + { + typeLibrary.ReleaseTLibAttr(pTlibAttr); + Marshal.ReleaseComObject(typeLibrary); + } + } + catch (COMException ex) + { + // if the typelib to be unregistered is not registered, then we don't have anything left to do + if (ex.ErrorCode == NativeMethods.TYPE_E_REGISTRYACCESS) + { + Log.LogWarningWithCodeFromResources("UnregisterAssembly.UnregisterTlbFileNotRegistered", typeLibPath); + } + // if the typelib can't be loaded (say because it's not a valid typelib file) we should report an error + else if (ex.ErrorCode == NativeMethods.TYPE_E_CANTLOADLIBRARY) + { + Log.LogErrorWithCodeFromResources("UnregisterAssembly.UnregisterTlbCantLoadFile", typeLibPath); + return false; + } + + // rethrow other exceptions + else + { +#if _DEBUG + Debug.Assert(false, "Unexpected exception in UnregisterAssembly.DoExecute. " + + "Please log a MSBuild bug specifying the steps to reproduce the problem."); +#endif + throw; + } + } + } + else + Log.LogMessageFromResources(MessageImportance.Low, "UnregisterAssembly.UnregisterTlbFileDoesNotExist", typeLibPath); + + return true; + } + + #endregion + + #region Data + private static Mutex s_unregisteringLock = new Mutex(false, unregisteringLockName); + private const string unregisteringLockName = "MSBUILD_V_3_5_UNREGISTER_LOCK"; + #endregion + } +} diff --git a/src/XMakeTasks/UpdateManifest.cs b/src/XMakeTasks/UpdateManifest.cs new file mode 100644 index 00000000000..13ad0a6e716 --- /dev/null +++ b/src/XMakeTasks/UpdateManifest.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Resources; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// Updates selected properties in a manifest and resigns. + /// + public class UpdateManifest : Task + { + private string _applicationPath; + private string _targetFrameworkVersion; + private ITaskItem _applicationManifest; + private ITaskItem _inputManifest; + private ITaskItem _outputManifest; + + [Required] + public string ApplicationPath + { + get { return _applicationPath; } + set { _applicationPath = value; } + } + + public string TargetFrameworkVersion + { + get { return _targetFrameworkVersion; } + set { _targetFrameworkVersion = value; } + } + + [Required] + public ITaskItem ApplicationManifest + { + get { return _applicationManifest; } + set { _applicationManifest = value; } + } + + [Required] + public ITaskItem InputManifest + { + get { return _inputManifest; } + set { _inputManifest = value; } + } + + [Output] + public ITaskItem OutputManifest + { + get { return _outputManifest; } + set { _outputManifest = value; } + } + + public override bool Execute() + { + Manifest.UpdateEntryPoint(InputManifest.ItemSpec, OutputManifest.ItemSpec, ApplicationPath, ApplicationManifest.ItemSpec, _targetFrameworkVersion); + + return true; + } + } +} + diff --git a/src/XMakeTasks/Vbc.cs b/src/XMakeTasks/Vbc.cs new file mode 100644 index 00000000000..5f1e1bc6fee --- /dev/null +++ b/src/XMakeTasks/Vbc.cs @@ -0,0 +1,1139 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Collections.Specialized; +using System.Collections.Generic; +using System.Resources; +using System.Reflection; +using System.Globalization; +using System.Text.RegularExpressions; + +#if (!STANDALONEBUILD) +using Microsoft.Internal.Performance; +#endif + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks.Hosting; +using Microsoft.Build.Tasks.InteropUtilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// This class defines the "Vbc" XMake task, which enables building assemblies from VB + /// source files by invoking the VB compiler. This is the new Roslyn XMake task, + /// meaning that the code is compiled by using the Roslyn compiler server, rather + /// than vbc.exe. The two should be functionally identical, but the compiler server + /// should be significantly faster with larger projects and have a smaller memory + /// footprint. + /// + public class Vbc : ManagedCompiler + { + private bool _useHostCompilerIfAvailable = false; + + // The following 1 fields are used, set and re-set in LogEventsFromTextOutput() + /// + /// This stores the origional lines and error priority together in the order in which they were recieved. + /// + private Queue _vbErrorLines = new Queue(); + + // Used when parsing vbc output to determine the column number of an error + private bool _isDoneOutputtingErrorMessage = false; + private int _numberOfLinesInErrorMessage = 0; + + #region Properties + + // Please keep these alphabetized. These are the parameters specific to Vbc. The + // ones shared between Vbc and Csc are defined in ManagedCompiler.cs, which is + // the base class. + + public string BaseAddress + { + set { Bag["BaseAddress"] = value; } + get { return (string)Bag["BaseAddress"]; } + } + + public string DisabledWarnings + { + set { Bag["DisabledWarnings"] = value; } + get { return (string)Bag["DisabledWarnings"]; } + } + + public string DocumentationFile + { + set { Bag["DocumentationFile"] = value; } + get { return (string)Bag["DocumentationFile"]; } + } + + public string ErrorReport + { + set { Bag["ErrorReport"] = value; } + get { return (string)Bag["ErrorReport"]; } + } + + public bool GenerateDocumentation + { + set { Bag["GenerateDocumentation"] = value; } + get { return GetBoolParameterWithDefault("GenerateDocumentation", false); } + } + + public ITaskItem[] Imports + { + set { Bag["Imports"] = value; } + get { return (ITaskItem[])Bag["Imports"]; } + } + + public string LangVersion + { + set { Bag["LangVersion"] = value; } + get { return (string)Bag["LangVersion"]; } + } + + public string ModuleAssemblyName + { + set { Bag["ModuleAssemblyName"] = value; } + get { return (string)Bag["ModuleAssemblyName"]; } + } + + public bool NoStandardLib + { + set { Bag["NoStandardLib"] = value; } + get { return GetBoolParameterWithDefault("NoStandardLib", false); } + } + + // This is not a documented switch. It prevents the automatic reference to Microsoft.VisualBasic.dll. + // The VB team believes the only scenario for this is when you are building that assembly itself. + // We have to support the switch here so that we can build the SDE and VB trees, which need to build this assembly. + // Although undocumented, it cannot be wrapped with #if BUILDING_DF_LKG because this would prevent dogfood builds + // within VS, which must use non-LKG msbuild bits. + public bool NoVBRuntimeReference + { + set { Bag["NoVBRuntimeReference"] = value; } + get { return GetBoolParameterWithDefault("NoVBRuntimeReference", false); } + } + + public bool NoWarnings + { + set { Bag["NoWarnings"] = value; } + get { return GetBoolParameterWithDefault("NoWarnings", false); } + } + + public string OptionCompare + { + set { Bag["OptionCompare"] = value; } + get { return (string)Bag["OptionCompare"]; } + } + + public bool OptionExplicit + { + set { Bag["OptionExplicit"] = value; } + get { return GetBoolParameterWithDefault("OptionExplicit", true); } + } + + public bool OptionStrict + { + set { Bag["OptionStrict"] = value; } + get { return GetBoolParameterWithDefault("OptionStrict", false); } + } + + public bool OptionInfer + { + set { Bag["OptionInfer"] = value; } + get { return GetBoolParameterWithDefault("OptionInfer", false); } + } + + // Currently only /optionstrict:custom + public string OptionStrictType + { + set { Bag["OptionStrictType"] = value; } + get { return (string)Bag["OptionStrictType"]; } + } + + public bool RemoveIntegerChecks + { + set { Bag["RemoveIntegerChecks"] = value; } + get { return GetBoolParameterWithDefault("RemoveIntegerChecks", false); } + } + + public string RootNamespace + { + set { Bag["RootNamespace"] = value; } + get { return (string)Bag["RootNamespace"]; } + } + + public string SdkPath + { + set { Bag["SdkPath"] = value; } + get { return (string)Bag["SdkPath"]; } + } + + /// + /// Name of the language passed to "/preferreduilang" compiler option. + /// + /// + /// If set to null, "/preferreduilang" option is omitted, and vbc.exe uses its default setting. + /// Otherwise, the value is passed to "/preferreduilang" as is. + /// + public string PreferredUILang + { + set { Bag["PreferredUILang"] = value; } + get { return (string)Bag["PreferredUILang"]; } + } + + public string VsSessionGuid + { + set { Bag["VsSessionGuid"] = value; } + get { return (string)Bag["VsSessionGuid"]; } + } + + public bool TargetCompactFramework + { + set { Bag["TargetCompactFramework"] = value; } + get { return GetBoolParameterWithDefault("TargetCompactFramework", false); } + } + + public bool UseHostCompilerIfAvailable + { + set { _useHostCompilerIfAvailable = value; } + get { return _useHostCompilerIfAvailable; } + } + + public string VBRuntimePath + { + set { Bag["VBRuntimePath"] = value; } + get { return (string)Bag["VBRuntimePath"]; } + } + + public string Verbosity + { + set { Bag["Verbosity"] = value; } + get { return (string)Bag["Verbosity"]; } + } + + public string WarningsAsErrors + { + set { Bag["WarningsAsErrors"] = value; } + get { return (string)Bag["WarningsAsErrors"]; } + } + + public string WarningsNotAsErrors + { + set { Bag["WarningsNotAsErrors"] = value; } + get { return (string)Bag["WarningsNotAsErrors"]; } + } + + public string VBRuntime + { + set { Bag["VBRuntime"] = value; } + get { return (string)Bag["VBRuntime"]; } + } + + public string PdbFile + { + set { Bag["PdbFile"] = value; } + get { return (string)Bag["PdbFile"]; } + } + #endregion + + #region Tool Members + + /// + /// Return the name of the tool to execute. + /// + override protected string ToolName + { + get + { + return "vbc2.exe"; + } + } + + /// + /// Override Execute so that we can moved the PDB file if we need to, + /// after the compiler is done. + /// + public override bool Execute() + { + if (!base.Execute()) + { + return false; + } + + MovePdbFileIfNecessary(OutputAssembly.ItemSpec); + + return !Log.HasLoggedErrors; + } + + /// + /// Move the PDB file if the PDB file that was generated by the compiler + /// is not at the specified path, or if it is newer than the one there. + /// VBC does not have a switch to specify the PDB path, so we are essentially implementing that for it here. + /// We need make this possible to avoid colliding with the PDB generated by WinMDExp. + /// + /// If at some future point VBC.exe offers a /pdbfile switch, this function can be removed. + /// + internal void MovePdbFileIfNecessary(string outputAssembly) + { + // Get the name of the output assembly because the pdb will be written beside it and will have the same name + if (String.IsNullOrEmpty(PdbFile) || String.IsNullOrEmpty(outputAssembly)) + { + return; + } + + try + { + string actualPdb = Path.ChangeExtension(outputAssembly, ".pdb"); // This is the pdb that the compiler generated + + FileInfo actualPdbInfo = new FileInfo(actualPdb); + + string desiredLocation = PdbFile; + if (!desiredLocation.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)) + { + desiredLocation += ".pdb"; + } + + FileInfo desiredPdbInfo = new FileInfo(desiredLocation); + + // If the compiler generated a pdb.. + if (actualPdbInfo.Exists) + { + // .. and the desired one does not exist or it's older... + if (!desiredPdbInfo.Exists || (desiredPdbInfo.Exists && actualPdbInfo.LastWriteTime > desiredPdbInfo.LastWriteTime)) + { + // Delete the existing one if it's already there, as Move would otherwise fail + if (desiredPdbInfo.Exists) + { + FileUtilities.DeleteNoThrow(desiredPdbInfo.FullName); + } + + // Move the file to where we actually wanted VBC to put it + File.Move(actualPdbInfo.FullName, desiredLocation); + } + } + } + catch (Exception e) // Catching Exception, but rethrowing unless it's an IO related exception + { + if (!ExceptionHandling.IsIoRelatedException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("VBC.RenamePDB", PdbFile, e.Message); + } + } + + /// + /// Generate the path to the tool + /// + override protected string GenerateFullPathToTool() + { + string pathToTool = ToolLocationHelper.GetPathToBuildToolsFile(ToolName, ToolLocationHelper.CurrentToolsVersion); + + if (null == pathToTool) + { + pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolName, TargetDotNetFrameworkVersion.VersionLatest); + + if (null == pathToTool) + { + Log.LogErrorWithCodeFromResources("General.FrameworksFileNotFound", ToolName, ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.VersionLatest)); + } + } + + return pathToTool; + } + + /// + /// vbc.exe only takes the BaseAddress in hexadecimal format. But we allow the caller + /// of the task to pass in the BaseAddress in either decimal or hexadecimal format. + /// Examples of supported hex formats include "0x10000000" or "&H10000000". + /// + internal string GetBaseAddressInHex() + { + string originalBaseAddress = this.BaseAddress; + + if (originalBaseAddress != null) + { + if (originalBaseAddress.Length > 2) + { + string twoLetterPrefix = originalBaseAddress.Substring(0, 2); + + if ( + (0 == String.Compare(twoLetterPrefix, "0x", StringComparison.OrdinalIgnoreCase)) || + (0 == String.Compare(twoLetterPrefix, "&h", StringComparison.OrdinalIgnoreCase)) + ) + { + // The incoming string is already in hex format ... we just need to + // remove the 0x or &H from the beginning. + return originalBaseAddress.Substring(2); + } + } + + // The incoming BaseAddress is not in hexadecimal format, so we need to + // convert it to hex. + try + { + uint baseAddressDecimal = UInt32.Parse(originalBaseAddress, CultureInfo.InvariantCulture); + return baseAddressDecimal.ToString("X", CultureInfo.InvariantCulture); + } + catch (FormatException e) + { + ErrorUtilities.VerifyThrowArgument(false, e, "Vbc.ParameterHasInvalidValue", "BaseAddress", originalBaseAddress); + } + } + + return null; + } + + /// + /// Looks at all the parameters that have been set, and builds up the string + /// containing all the command-line switches. + /// + /// + protected internal override void AddResponseFileCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendSwitchIfNotNull("/baseaddress:", this.GetBaseAddressInHex()); + commandLine.AppendSwitchIfNotNull("/libpath:", this.AdditionalLibPaths, ","); + commandLine.AppendSwitchIfNotNull("/imports:", this.Imports, ","); + // Make sure this /doc+ switch comes *before* the /doc: switch (which is handled in the + // ManagedCompiler.cs base class). /doc+ is really just an alias for /doc:.xml, + // and the last /doc switch on the command-line wins. If the user provided a specific doc filename, + // we want that one to win. + commandLine.AppendPlusOrMinusSwitch("/doc", this.Bag, "GenerateDocumentation"); + commandLine.AppendSwitchIfNotNull("/optioncompare:", this.OptionCompare); + commandLine.AppendPlusOrMinusSwitch("/optionexplicit", this.Bag, "OptionExplicit"); + // Make sure this /optionstrict+ switch appears *before* the /optionstrict:xxxx switch below + + /* In Orcas a change was made that set Option Strict-, whenever this.DisabledWarnings was + * empty. That was clearly the wrong thing to do and we found it when we had a project with all the warning configuration + * entries set to WARNING. Because this.DisabledWarnings was empty in that case we would end up sending /OptionStrict- + * effectively silencing all the warnings that had been selected. + * + * Now what we do is: + * If option strict+ is specified, that trumps everything and we just set option strict+ + * Otherwise, just set option strict:custom. + * You may wonder why we don't try to set Option Strict- The reason is that Option Strict- just implies a certain + * set of warnings that should be disabled (there's ten of them today) You get the same effect by sending + * option strict:custom on along with the correct list of disabled warnings. + * Rather than make this code know the current set of disabled warnings that comprise Option strict-, we just send + * option strict:custom on with the understanding that we'll get the same behavior as option strict- since we are passing + * the /nowarn line on that contains all the warnings OptionStrict- would disable anyway. The IDE knows what they are + * and puts them in the project file so we are good. And by not making this code aware of which warnings comprise + * Option Strict-, we have one less place we have to keep up to date in terms of what comprises option strict- + */ + + // Decide whether we are Option Strict+ or Option Strict:custom + object optionStrictSetting = this.Bag["OptionStrict"]; + bool optionStrict = optionStrictSetting != null ? (bool)optionStrictSetting : false; + if (optionStrict) + { + commandLine.AppendSwitch("/optionstrict+"); + } + else // OptionStrict+ wasn't specified so use :custom. + { + commandLine.AppendSwitch("/optionstrict:custom"); + } + + commandLine.AppendSwitchIfNotNull("/optionstrict:", this.OptionStrictType); + commandLine.AppendWhenTrue("/nowarn", this.Bag, "NoWarnings"); + commandLine.AppendSwitchWithSplitting("/nowarn:", this.DisabledWarnings, ",", ';', ','); + commandLine.AppendPlusOrMinusSwitch("/optioninfer", this.Bag, "OptionInfer"); + commandLine.AppendWhenTrue("/nostdlib", this.Bag, "NoStandardLib"); + commandLine.AppendWhenTrue("/novbruntimeref", this.Bag, "NoVBRuntimeReference"); + commandLine.AppendSwitchIfNotNull("/errorreport:", this.ErrorReport); + commandLine.AppendSwitchIfNotNull("/platform:", this.PlatformWith32BitPreference); + commandLine.AppendPlusOrMinusSwitch("/removeintchecks", this.Bag, "RemoveIntegerChecks"); + commandLine.AppendSwitchIfNotNull("/rootnamespace:", this.RootNamespace); + commandLine.AppendSwitchIfNotNull("/sdkpath:", this.SdkPath); + commandLine.AppendSwitchIfNotNull("/langversion:", this.LangVersion); + commandLine.AppendSwitchIfNotNull("/moduleassemblyname:", this.ModuleAssemblyName); + commandLine.AppendWhenTrue("/netcf", this.Bag, "TargetCompactFramework"); + commandLine.AppendSwitchIfNotNull("/preferreduilang:", this.PreferredUILang); + commandLine.AppendPlusOrMinusSwitch("/highentropyva", this.Bag, "HighEntropyVA"); + + if (0 == String.Compare(this.VBRuntimePath, this.VBRuntime, StringComparison.OrdinalIgnoreCase)) + { + commandLine.AppendSwitchIfNotNull("/vbruntime:", this.VBRuntimePath); + } + else if (this.VBRuntime != null) + { + string vbRuntimeSwitch = this.VBRuntime; + if (0 == String.Compare(vbRuntimeSwitch, "EMBED", StringComparison.OrdinalIgnoreCase)) + { + commandLine.AppendSwitch("/vbruntime*"); + } + else if (0 == String.Compare(vbRuntimeSwitch, "NONE", StringComparison.OrdinalIgnoreCase)) + { + commandLine.AppendSwitch("/vbruntime-"); + } + else if (0 == String.Compare(vbRuntimeSwitch, "DEFAULT", StringComparison.OrdinalIgnoreCase)) + { + commandLine.AppendSwitch("/vbruntime+"); + } + else + { + commandLine.AppendSwitchIfNotNull("/vbruntime:", vbRuntimeSwitch); + } + } + + + // Verbosity + if ( + (this.Verbosity != null) && + + ( + (0 == String.Compare(this.Verbosity, "quiet", StringComparison.OrdinalIgnoreCase)) || + (0 == String.Compare(this.Verbosity, "verbose", StringComparison.OrdinalIgnoreCase)) + ) + ) + { + commandLine.AppendSwitchIfNotNull("/", this.Verbosity); + } + + commandLine.AppendSwitchIfNotNull("/doc:", this.DocumentationFile); + commandLine.AppendSwitchUnquotedIfNotNull("/define:", Vbc.GetDefineConstantsSwitch(this.DefineConstants)); + AddReferencesToCommandLine(commandLine); + commandLine.AppendSwitchIfNotNull("/win32resource:", this.Win32Resource); + + // Special case for "Sub Main" + if (0 != String.Compare("Sub Main", this.MainEntryPoint, StringComparison.OrdinalIgnoreCase)) + { + commandLine.AppendSwitchIfNotNull("/main:", this.MainEntryPoint); + } + + base.AddResponseFileCommands(commandLine); + + // This should come after the "TreatWarningsAsErrors" flag is processed (in managedcompiler.cs). + // Because if TreatWarningsAsErrors=false, then we'll have a /warnaserror- on the command-line, + // and then any specific warnings that should be treated as errors should be specified with + // /warnaserror+: after the /warnaserror- switch. The order of the switches on the command-line + // does matter. + // + // Note that + // /warnaserror+ + // is just shorthand for: + // /warnaserror+: + // + // Similarly, + // /warnaserror- + // is just shorthand for: + // /warnaserror-: + commandLine.AppendSwitchWithSplitting("/warnaserror+:", this.WarningsAsErrors, ",", ';', ','); + commandLine.AppendSwitchWithSplitting("/warnaserror-:", this.WarningsNotAsErrors, ",", ';', ','); + + // If not design time build and the globalSessionGuid property was set then add a -globalsessionguid: + bool designTime = false; + if (this.HostObject != null) + { + var vbHost = this.HostObject as IVbcHostObject; + designTime = vbHost.IsDesignTime(); + } + if (!designTime) + { + if (!string.IsNullOrWhiteSpace(this.VsSessionGuid)) + { + commandLine.AppendSwitchIfNotNull("/sqmsessionguid:", this.VsSessionGuid); + } + } + + // It's a good idea for the response file to be the very last switch passed, just + // from a predictability perspective. + if (this.ResponseFiles != null) + { + foreach (ITaskItem response in this.ResponseFiles) + { + commandLine.AppendSwitchIfNotNull("@", response.ItemSpec); + } + } + } + + private void AddReferencesToCommandLine + ( + CommandLineBuilderExtension commandLine + ) + { + if ((this.References == null) || (this.References.Length == 0)) + { + return; + } + + List references = new List(this.References.Length); + List links = new List(this.References.Length); + + foreach (ITaskItem reference in this.References) + { + bool embed = MetadataConversionUtilities.TryConvertItemMetadataToBool + ( + reference, + ItemMetadataNames.embedInteropTypes + ); + + if (embed) + { + links.Add(reference); + } + else + { + references.Add(reference); + } + } + + if (links.Count > 0) + { + commandLine.AppendSwitchIfNotNull("/link:", links.ToArray(), ","); + } + + if (references.Count > 0) + { + commandLine.AppendSwitchIfNotNull("/reference:", references.ToArray(), ","); + } + } + + /// + /// Validate parameters, log errors and warnings and return true if + /// Execute should proceed. + /// + override protected bool ValidateParameters() + { + if (!base.ValidateParameters()) + { + return false; + } + + // Validate that the "Verbosity" parameter is one of "quiet", "normal", or "verbose". + if (this.Verbosity != null) + { + if ((0 != String.Compare(Verbosity, "normal", StringComparison.OrdinalIgnoreCase)) && + (0 != String.Compare(Verbosity, "quiet", StringComparison.OrdinalIgnoreCase)) && + (0 != String.Compare(Verbosity, "verbose", StringComparison.OrdinalIgnoreCase))) + { + Log.LogErrorWithCodeFromResources("Vbc.EnumParameterHasInvalidValue", "Verbosity", this.Verbosity, "Quiet, Normal, Verbose"); + return false; + } + } + + return true; + } + + /// + /// This method intercepts the lines to be logged coming from STDOUT from VBC. + /// Once we see a standard vb warning or error, then we capture it and grab the next 3 + /// lines so we can transform the string form the form of FileName.vb(line) to FileName.vb(line,column) + /// which will allow us to report the line and column to the IDE, and thus filter the error + /// in the duplicate case for multi-targeting, or just squiggle the appropriate token + /// instead of the entire line. + /// + /// A single line from the STDOUT of the vbc compiler + /// High,Low,Normal + protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance) + { + // We can return immediately if this was not called by the out of proc compiler + if (!this.UsedCommandLineTool) + { + base.LogEventsFromTextOutput(singleLine, messageImportance); + return; + } + + // We can also return immediately if the current string is not a warning or error + // and we have not seen a warning or error yet. 'Error' and 'Warning' are not localized. + if (_vbErrorLines.Count == 0 && + singleLine.IndexOf("warning", StringComparison.OrdinalIgnoreCase) == -1 && + singleLine.IndexOf("error", StringComparison.OrdinalIgnoreCase) == -1) + { + base.LogEventsFromTextOutput(singleLine, messageImportance); + return; + } + + ParseVBErrorOrWarning(singleLine, messageImportance); + } + + /// + /// Given a string, parses it to find out whether it's an error or warning and, if so, + /// make sure it's validated properly. + /// + /// + /// INTERNAL FOR UNITTESTING ONLY + /// + /// The line to parse + /// The MessageImportance to use when reporting the error. + internal void ParseVBErrorOrWarning(string singleLine, MessageImportance messageImportance) + { + // if this string is empty then we haven't seen the first line of an error yet + if (_vbErrorLines.Count > 0) + { + // vbc separates the error message from the source text with an empty line, so + // we can check for an empty line to see if vbc finished outputting the error message + if (!_isDoneOutputtingErrorMessage && singleLine.Length == 0) + { + _isDoneOutputtingErrorMessage = true; + _numberOfLinesInErrorMessage = _vbErrorLines.Count; + } + + _vbErrorLines.Enqueue(new VBError(singleLine, messageImportance)); + + // We are looking for the line that indicates the column (contains the '~'), + // which vbc outputs 3 lines below the error message: + // + // + // + // + // + if (_isDoneOutputtingErrorMessage && + _vbErrorLines.Count == _numberOfLinesInErrorMessage + 3) + { + // Once we have the 4th line (error line + 3), then parse it for the first ~ + // which will correspond to the column of the token with the error because + // VBC respects the users's indentation settings in the file it is compiling + // and only outputs SPACE chars to STDOUT. + + // The +1 is to translate the index into user columns which are 1 based. + + VBError originalVBError = _vbErrorLines.Dequeue(); + string originalVBErrorString = originalVBError.Message; + + int column = singleLine.IndexOf('~') + 1; + int endParenthesisLocation = originalVBErrorString.IndexOf(')'); + + // If for some reason the line does not contain any ~ then something went wrong + // so abort and return the origional string. + if (column < 0 || endParenthesisLocation < 0) + { + // we need to output all of the original lines we ate. + Log.LogMessageFromText(originalVBErrorString, originalVBError.MessageImportance); + foreach (VBError vberror in _vbErrorLines) + { + base.LogEventsFromTextOutput(vberror.Message, vberror.MessageImportance); + } + + _vbErrorLines.Clear(); + return; + } + + string newLine = null; + newLine = originalVBErrorString.Substring(0, endParenthesisLocation) + "," + column + originalVBErrorString.Substring(endParenthesisLocation); + + // Output all of the lines of the error, but with the modified first line as well. + Log.LogMessageFromText(newLine, originalVBError.MessageImportance); + foreach (VBError vberror in _vbErrorLines) + { + base.LogEventsFromTextOutput(vberror.Message, vberror.MessageImportance); + } + + _vbErrorLines.Clear(); + } + } + else + { + CanonicalError.Parts parts = CanonicalError.Parse(singleLine); + if (parts == null) + { + base.LogEventsFromTextOutput(singleLine, messageImportance); + } + else if ((parts.category == CanonicalError.Parts.Category.Error || + parts.category == CanonicalError.Parts.Category.Warning) && + parts.column == CanonicalError.Parts.numberNotSpecified) + { + if (parts.line != CanonicalError.Parts.numberNotSpecified) + { + // If we got here, then this is a standard VBC error or warning. + _vbErrorLines.Enqueue(new VBError(singleLine, messageImportance)); + _isDoneOutputtingErrorMessage = false; + _numberOfLinesInErrorMessage = 0; + } + else + { + // Project-level errors don't have line numbers -- just output now. + base.LogEventsFromTextOutput(singleLine, messageImportance); + } + } + } + } + + #endregion + + /// + /// Many VisualStudio VB projects have values for the DefineConstants property that + /// contain quotes and spaces. Normally we don't allow parameters passed into the + /// task to contain quotes, because if we weren't careful, we might accidently + /// allow a parameter injection attach. But for "DefineConstants", we have to allow + /// it. + /// So this method prepares the string to be passed in on the /define: command-line + /// switch. It does that by quoting the entire string, and escaping the embedded + /// quotes. + /// + internal static string GetDefineConstantsSwitch + ( + string originalDefineConstants + ) + { + if ((originalDefineConstants == null) || (originalDefineConstants.Length == 0)) + { + return null; + } + + StringBuilder finalDefineConstants = new StringBuilder(originalDefineConstants); + + // Replace slash-quote with slash-slash-quote. + finalDefineConstants.Replace("\\\"", "\\\\\""); + + // Replace quote with slash-quote. + finalDefineConstants.Replace("\"", "\\\""); + + // Surround the whole thing with a pair of double-quotes. + finalDefineConstants.Insert(0, '"'); + finalDefineConstants.Append('"'); + + // Now it's ready to be passed in to the /define: switch. + return finalDefineConstants.ToString(); + } + + /// + /// This method will initialize the host compiler object with all the switches, + /// parameters, resources, references, sources, etc. + /// + /// It returns true if everything went according to plan. It returns false if the + /// host compiler had a problem with one of the parameters that was passed in. + /// + /// This method also sets the "this.HostCompilerSupportsAllParameters" property + /// accordingly. + /// + /// Example: + /// If we attempted to pass in Platform="foo", then this method would + /// set HostCompilerSupportsAllParameters=true, but it would throw an + /// exception because the host compiler fully supports + /// the Platform parameter, but "foo" is an illegal value. + /// + /// Example: + /// If we attempted to pass in NoConfig=false, then this method would set + /// HostCompilerSupportsAllParameters=false, because while this is a legal + /// thing for csc.exe, the IDE compiler cannot support it. In this situation + /// the return value will also be false. + /// + private bool InitializeHostCompiler + ( + // NOTE: For compat reasons this must remain IVbcHostObject + // we can dynamically test for smarter interfaces later.. + IVbcHostObject vbcHostObject + ) + { + this.HostCompilerSupportsAllParameters = this.UseHostCompilerIfAvailable; + string param = "Unknown"; + + try + { + param = "BeginInitialization"; + vbcHostObject.BeginInitialization(); + + param = "AdditionalLibPaths"; this.CheckHostObjectSupport(param, vbcHostObject.SetAdditionalLibPaths(this.AdditionalLibPaths)); + param = "AddModules"; this.CheckHostObjectSupport(param, vbcHostObject.SetAddModules(this.AddModules)); + + // For host objects which support them, set the analyzers, ruleset and additional files. + IAnalyzerHostObject analyzerHostObject = vbcHostObject as IAnalyzerHostObject; + if (analyzerHostObject != null) + { + param = "Analyzers"; this.CheckHostObjectSupport(param, analyzerHostObject.SetAnalyzers(this.Analyzers)); + param = "CodeAnalysisRuleSet"; this.CheckHostObjectSupport(param, analyzerHostObject.SetRuleSet(this.CodeAnalysisRuleSet)); + param = "AdditionalFiles"; this.CheckHostObjectSupport(param, analyzerHostObject.SetAdditionalFiles(this.AdditionalFiles)); + } + + param = "BaseAddress"; this.CheckHostObjectSupport(param, vbcHostObject.SetBaseAddress(this.TargetType, this.GetBaseAddressInHex())); + param = "CodePage"; this.CheckHostObjectSupport(param, vbcHostObject.SetCodePage(this.CodePage)); + param = "DebugType"; this.CheckHostObjectSupport(param, vbcHostObject.SetDebugType(this.EmitDebugInformation, this.DebugType)); + param = "DefineConstants"; this.CheckHostObjectSupport(param, vbcHostObject.SetDefineConstants(this.DefineConstants)); + param = "DelaySign"; this.CheckHostObjectSupport(param, vbcHostObject.SetDelaySign(this.DelaySign)); + param = "DocumentationFile"; this.CheckHostObjectSupport(param, vbcHostObject.SetDocumentationFile(this.DocumentationFile)); + param = "FileAlignment"; this.CheckHostObjectSupport(param, vbcHostObject.SetFileAlignment(this.FileAlignment)); + param = "GenerateDocumentation"; this.CheckHostObjectSupport(param, vbcHostObject.SetGenerateDocumentation(this.GenerateDocumentation)); + param = "Imports"; this.CheckHostObjectSupport(param, vbcHostObject.SetImports(this.Imports)); + param = "KeyContainer"; this.CheckHostObjectSupport(param, vbcHostObject.SetKeyContainer(this.KeyContainer)); + param = "KeyFile"; this.CheckHostObjectSupport(param, vbcHostObject.SetKeyFile(this.KeyFile)); + param = "LinkResources"; this.CheckHostObjectSupport(param, vbcHostObject.SetLinkResources(this.LinkResources)); + param = "MainEntryPoint"; this.CheckHostObjectSupport(param, vbcHostObject.SetMainEntryPoint(this.MainEntryPoint)); + param = "NoConfig"; this.CheckHostObjectSupport(param, vbcHostObject.SetNoConfig(this.NoConfig)); + param = "NoStandardLib"; this.CheckHostObjectSupport(param, vbcHostObject.SetNoStandardLib(this.NoStandardLib)); + param = "NoWarnings"; this.CheckHostObjectSupport(param, vbcHostObject.SetNoWarnings(this.NoWarnings)); + param = "Optimize"; this.CheckHostObjectSupport(param, vbcHostObject.SetOptimize(this.Optimize)); + param = "OptionCompare"; this.CheckHostObjectSupport(param, vbcHostObject.SetOptionCompare(this.OptionCompare)); + param = "OptionExplicit"; this.CheckHostObjectSupport(param, vbcHostObject.SetOptionExplicit(this.OptionExplicit)); + param = "OptionStrict"; this.CheckHostObjectSupport(param, vbcHostObject.SetOptionStrict(this.OptionStrict)); + param = "OptionStrictType"; this.CheckHostObjectSupport(param, vbcHostObject.SetOptionStrictType(this.OptionStrictType)); + param = "OutputAssembly"; this.CheckHostObjectSupport(param, vbcHostObject.SetOutputAssembly(this.OutputAssembly.ItemSpec)); + + // For host objects which support them, set platform with 32BitPreference, HighEntropyVA, and SubsystemVersion + IVbcHostObject5 vbcHostObject5 = vbcHostObject as IVbcHostObject5; + if (vbcHostObject5 != null) + { + param = "PlatformWith32BitPreference"; this.CheckHostObjectSupport(param, vbcHostObject5.SetPlatformWith32BitPreference(this.PlatformWith32BitPreference)); + param = "HighEntropyVA"; this.CheckHostObjectSupport(param, vbcHostObject5.SetHighEntropyVA(this.HighEntropyVA)); + param = "SubsystemVersion"; this.CheckHostObjectSupport(param, vbcHostObject5.SetSubsystemVersion(this.SubsystemVersion)); + } + else + { + param = "Platform"; this.CheckHostObjectSupport(param, vbcHostObject.SetPlatform(this.Platform)); + } + + param = "References"; this.CheckHostObjectSupport(param, vbcHostObject.SetReferences(this.References)); + param = "RemoveIntegerChecks"; this.CheckHostObjectSupport(param, vbcHostObject.SetRemoveIntegerChecks(this.RemoveIntegerChecks)); + param = "Resources"; this.CheckHostObjectSupport(param, vbcHostObject.SetResources(this.Resources)); + param = "ResponseFiles"; this.CheckHostObjectSupport(param, vbcHostObject.SetResponseFiles(this.ResponseFiles)); + param = "RootNamespace"; this.CheckHostObjectSupport(param, vbcHostObject.SetRootNamespace(this.RootNamespace)); + param = "SdkPath"; this.CheckHostObjectSupport(param, vbcHostObject.SetSdkPath(this.SdkPath)); + param = "Sources"; this.CheckHostObjectSupport(param, vbcHostObject.SetSources(this.Sources)); + param = "TargetCompactFramework"; this.CheckHostObjectSupport(param, vbcHostObject.SetTargetCompactFramework(this.TargetCompactFramework)); + param = "TargetType"; this.CheckHostObjectSupport(param, vbcHostObject.SetTargetType(this.TargetType)); + param = "TreatWarningsAsErrors"; this.CheckHostObjectSupport(param, vbcHostObject.SetTreatWarningsAsErrors(this.TreatWarningsAsErrors)); + param = "WarningsAsErrors"; this.CheckHostObjectSupport(param, vbcHostObject.SetWarningsAsErrors(this.WarningsAsErrors)); + param = "WarningsNotAsErrors"; this.CheckHostObjectSupport(param, vbcHostObject.SetWarningsNotAsErrors(this.WarningsNotAsErrors)); + // DisabledWarnings needs to come after WarningsAsErrors and WarningsNotAsErrors, because + // of the way the host object works, and the fact that DisabledWarnings trump Warnings[Not]AsErrors. + param = "DisabledWarnings"; this.CheckHostObjectSupport(param, vbcHostObject.SetDisabledWarnings(this.DisabledWarnings)); + param = "Win32Icon"; this.CheckHostObjectSupport(param, vbcHostObject.SetWin32Icon(this.Win32Icon)); + param = "Win32Resource"; this.CheckHostObjectSupport(param, vbcHostObject.SetWin32Resource(this.Win32Resource)); + + // In order to maintain compatibility with previous host compilers, we must + // light-up for IVbcHostObject2 + if (vbcHostObject is IVbcHostObject2) + { + IVbcHostObject2 vbcHostObject2 = (IVbcHostObject2)vbcHostObject; + param = "ModuleAssemblyName"; this.CheckHostObjectSupport(param, vbcHostObject2.SetModuleAssemblyName(this.ModuleAssemblyName)); + param = "OptionInfer"; this.CheckHostObjectSupport(param, vbcHostObject2.SetOptionInfer(this.OptionInfer)); + param = "Win32Manifest"; this.CheckHostObjectSupport(param, vbcHostObject2.SetWin32Manifest(this.GetWin32ManifestSwitch(this.NoWin32Manifest, this.Win32Manifest))); + // initialize option Infer + CheckHostObjectSupport("OptionInfer", vbcHostObject2.SetOptionInfer(this.OptionInfer)); + } + else + { + // If we have been given a property that the host compiler doesn't support + // then we need to state that we are falling back to the command line compiler + if (!String.IsNullOrEmpty(ModuleAssemblyName)) + { + CheckHostObjectSupport("ModuleAssemblyName", false); + } + + if (Bag.ContainsKey("OptionInfer")) + { + CheckHostObjectSupport("OptionInfer", false); + } + + if (!String.IsNullOrEmpty(Win32Manifest)) + { + CheckHostObjectSupport("Win32Manifest", false); + } + } + + // Check for support of the LangVersion property + if (vbcHostObject is IVbcHostObject3) + { + IVbcHostObject3 vbcHostObject3 = (IVbcHostObject3)vbcHostObject; + param = "LangVersion"; this.CheckHostObjectSupport(param, vbcHostObject3.SetLanguageVersion(this.LangVersion)); + } + else if (!String.IsNullOrEmpty(this.LangVersion) && !this.UsedCommandLineTool) + { + CheckHostObjectSupport("LangVersion", false); + } + + if (vbcHostObject is IVbcHostObject4) + { + IVbcHostObject4 vbcHostObject4 = (IVbcHostObject4)vbcHostObject; + param = "VBRuntime"; this.CheckHostObjectSupport(param, vbcHostObject4.SetVBRuntime(this.VBRuntime)); + } + // Support for NoVBRuntimeReference was added to this task after IVbcHostObject was frozen. That doesn't matter much because the host + // compiler doesn't support it, and almost nobody uses it anyway. But if someone has set it, we need to hard code falling back to + // the command line compiler here. + if (NoVBRuntimeReference) + { + CheckHostObjectSupport("NoVBRuntimeReference", false); + } + + // In general, we don't support preferreduilang with the in-proc compiler. It will always use the same locale as the + // host process, so in general, we have to fall back to the command line compiler if this option is specified. + // However, we explicitly allow two values (mostly for parity with C#): + // Null is supported because it means that option should be omitted, and compiler default used - obviously always valid. + // Explicitly specified name of current locale is also supported, since it is effectively a no-op. + if (!String.IsNullOrEmpty(PreferredUILang) && !String.Equals(PreferredUILang, System.Globalization.CultureInfo.CurrentUICulture.Name, StringComparison.OrdinalIgnoreCase)) + { + CheckHostObjectSupport("PreferredUILang", false); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + if (this.HostCompilerSupportsAllParameters) + { + // If the host compiler doesn't support everything we need, we're going to end up + // shelling out to the command-line compiler anyway. That means the command-line + // compiler will log the error. So here, we only log the error if we would've + // tried to use the host compiler. + Log.LogErrorWithCodeFromResources("General.CouldNotSetHostObjectParameter", param, e.Message); + } + + return false; + } + finally + { + // In the case of the VB host compiler, the EndInitialization method will + // throw (due to FAILED HRESULT) if there was a bad value for one of the + // parameters. + vbcHostObject.EndInitialization(); + } + + return true; + } + + /// + /// This method will get called during Execute() if a host object has been passed into the Vbc + /// task. Returns one of the following values to indicate what the next action should be: + /// UseHostObjectToExecute Host compiler exists and was initialized. + /// UseAlternateToolToExecute Host compiler doesn't exist or was not appropriate. + /// NoActionReturnSuccess Host compiler was already up-to-date, and we're done. + /// NoActionReturnFailure Bad parameters were passed into the task. + /// + override protected HostObjectInitializationStatus InitializeHostObject() + { + if (this.HostObject != null) + { + // When the host object was passed into the task, it was passed in as a generic + // "Object" (because ITask interface obviously can't have any Vbc-specific stuff + // in it, and each task is going to want to communicate with its host in a unique + // way). Now we cast it to the specific type that the Vbc task expects. If the + // host object does not match this type, the host passed in an invalid host object + // to Vbc, and we error out. + + // NOTE: For compat reasons this must remain IVbcHostObject + // we can dynamically test for smarter interfaces later.. + using (RCWForCurrentContext hostObject = new RCWForCurrentContext(this.HostObject as IVbcHostObject)) + { + IVbcHostObject vbcHostObject = hostObject.RCW; + + if (vbcHostObject != null) + { + bool hostObjectSuccessfullyInitialized = InitializeHostCompiler(vbcHostObject); + + // If we're currently only in design-time (as opposed to build-time), + // then we're done. We've initialized the host compiler as best we + // can, and we certainly don't want to actually do the final compile. + // So return true, saying we're done and successful. + if (vbcHostObject.IsDesignTime()) + { + // If we are design-time then we do not want to continue the build at + // this time. + return hostObjectSuccessfullyInitialized ? + HostObjectInitializationStatus.NoActionReturnSuccess : + HostObjectInitializationStatus.NoActionReturnFailure; + } + + if (!this.HostCompilerSupportsAllParameters || UseAlternateCommandLineToolToExecute()) + { + // Since the host compiler has refused to take on the responsibility for this compilation, + // we're about to shell out to the command-line compiler to handle it. If some of the + // references don't exist on disk, we know the command-line compiler will fail, so save + // the trouble, and just throw a consistent error ourselves. This allows us to give + // more information than the compiler would, and also make things consistent across + // Vbc / Csc / etc. + // This suite behaves differently in localized builds than on English builds because + // VBC.EXE doesn't localize the word "error" when they emit errors and so we can't scan for it. + if (!CheckAllReferencesExistOnDisk()) + { + return HostObjectInitializationStatus.NoActionReturnFailure; + } + + // The host compiler doesn't support some of the switches/parameters + // being passed to it. Therefore, we resort to using the command-line compiler + // in this case. + UsedCommandLineTool = true; + return HostObjectInitializationStatus.UseAlternateToolToExecute; + } + + // Ok, by now we validated that the host object supports the necessary switches + // and parameters. Last thing to check is whether the host object is up to date, + // and in that case, we will inform the caller that no further action is necessary. + if (hostObjectSuccessfullyInitialized) + { + return vbcHostObject.IsUpToDate() ? + HostObjectInitializationStatus.NoActionReturnSuccess : + HostObjectInitializationStatus.UseHostObjectToExecute; + } + else + { + return HostObjectInitializationStatus.NoActionReturnFailure; + } + } + else + { + Log.LogErrorWithCodeFromResources("General.IncorrectHostObject", "Vbc", "IVbcHostObject"); + } + } + } + + + // No appropriate host object was found. + UsedCommandLineTool = true; + return HostObjectInitializationStatus.UseAlternateToolToExecute; + } + + /// + /// This method will get called during Execute() if a host object has been passed into the Vbc + /// task. Returns true if an appropriate host object was found, it was called to do the compile, + /// and the compile succeeded. Otherwise, we return false. + /// + override protected bool CallHostObjectToExecute() + { + Debug.Assert(this.HostObject != null, "We should not be here if the host object has not been set."); + + IVbcHostObject vbcHostObject = this.HostObject as IVbcHostObject; + Debug.Assert(vbcHostObject != null, "Wrong kind of host object passed in!"); + try + { +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildHostCompileBegin); +#endif + IVbcHostObject5 vbcHostObject5 = vbcHostObject as IVbcHostObject5; + Debug.Assert(vbcHostObject5 != null, "Wrong kind of host object passed in!"); + + // IVbcHostObjectFreeThreaded::Compile is the preferred way to compile the host object + // because while it is still synchronous it does its waiting on our BG thread + // (as opposed to the UI thread for IVbcHostObject::Compile) + if (vbcHostObject5 != null) + { + IVbcHostObjectFreeThreaded freeThreadedHostObject = vbcHostObject5.GetFreeThreadedHostObject(); + return freeThreadedHostObject.Compile(); + } + else + { + // If for some reason we can't get to IVbcHostObject5 we just fall back to the old + // Compile method. This method unfortunately allows for reentrancy on the UI thread. + return vbcHostObject.Compile(); + } + } + finally + { +#if (!STANDALONEBUILD) + CodeMarkers.Instance.CodeMarker(CodeMarkerEvent.perfMSBuildHostCompileEnd); +#endif + } + } + + /// + /// private class that just holds together name, value pair for the vbErrorLines Queue + /// + private class VBError + { + public string Message { get; set; } + public MessageImportance MessageImportance { get; set; } + + public VBError(string message, MessageImportance importance) + { + this.Message = message; + this.MessageImportance = importance; + } + } + } +} diff --git a/src/XMakeTasks/VisualBasicParserUtilities.cs b/src/XMakeTasks/VisualBasicParserUtilities.cs new file mode 100644 index 00000000000..44d9c2f8257 --- /dev/null +++ b/src/XMakeTasks/VisualBasicParserUtilities.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Text; +using System.Resources; +using System.Reflection; + +using System.Collections; +using Microsoft.Build.Shared.LanguageParser; + +namespace Microsoft.Build.Tasks +{ + /// + /// Specific-purpose utility functions for parsing VB. + /// + internal static class VisualBasicParserUtilities + { + /// + /// Parse a VB file and get the first class name, fully qualified with namespace. + /// + /// + /// + static internal ExtractedClassName GetFirstClassNameFullyQualified(Stream binaryStream) + { + try + { + VisualBasicTokenizer tokens = new VisualBasicTokenizer(binaryStream, /* forceANSI */ false); + return Extract(tokens); + } + catch (DecoderFallbackException) + { + // There was no BOM and there are non UTF8 sequences. Fall back to ANSI. + VisualBasicTokenizer tokens = new VisualBasicTokenizer(binaryStream, /* forceANSI */ true); + return Extract(tokens); + } + } + + /// + /// Extract the class name. + /// + /// + /// + private static ExtractedClassName Extract(VisualBasicTokenizer tokens) + { + ParseState state = new ParseState(); + ExtractedClassName result = new ExtractedClassName(); + + foreach (Token t in tokens) + { + // Search first for keywords that we care about. + if (t is KeywordToken) + { + state.Reset(); + + if (t.EqualsIgnoreCase("namespace")) + { + state.ResolvingNamespace = true; + if (state.InsideConditionalDirective) + { + result.IsInsideConditionalBlock = true; + } + } + else if (t.EqualsIgnoreCase("class")) + { + state.ResolvingClass = true; + if (state.InsideConditionalDirective) + { + result.IsInsideConditionalBlock = true; + } + } + else if (t.EqualsIgnoreCase("end")) + { + state.PopNamespacePart(); + } + } + else if (t is VisualBasicTokenizer.LineTerminatorToken) + { + if (state.ResolvingNamespace) + { + state.PushNamespacePart(state.Namespace); + } + state.Reset(); + } + else if (t is VisualBasicTokenizer.SeparatorToken) + { + if (state.ResolvingNamespace) + { + if (t.InnerText == ".") + { + state.Namespace += "."; + } + } + } + else if (t is IdentifierToken) + { + // If we're resolving a namespace, then this is part of the namespace. + if (state.ResolvingNamespace) + { + state.Namespace += t.InnerText; + } + // If we're resolving a class, then we're done. We found the class name. + else if (state.ResolvingClass) + { + // We're done. + result.Name = state.ComposeQualifiedClassName(t.InnerText); + return result; + } + } + else if (t is OpenConditionalDirectiveToken) + { + state.OpenConditionalDirective(); + } + else if (t is CloseConditionalDirectiveToken) + { + state.CloseConditionalDirective(); + } + } + + return result; + } + } +} diff --git a/src/XMakeTasks/Warning.cs b/src/XMakeTasks/Warning.cs new file mode 100644 index 00000000000..3adce9216e5 --- /dev/null +++ b/src/XMakeTasks/Warning.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Diagnostics; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Globalization; +using System.Resources; + +namespace Microsoft.Build.Tasks +{ + /// + /// Task that simply emits a warning. Engine will add the project path because + /// we do not specify a filename. + /// + public sealed class Warning : TaskExtension + { + private string _text; + + /// + /// Error message + /// + public string Text + { + get + { + return _text; + } + + set + { + _text = value; + } + } + + private string _code; + + /// + /// Warning code + /// + public string Code + { + get + { + return _code; + } + set + { + _code = value; + } + } + + /// + /// Relevant file if any. + /// If none is provided, the file containing the Warning + /// task will be used. + /// + public string File + { + get; + set; + } + + private string _helpKeyword; + + /// + /// Warning help keyword + /// + public string HelpKeyword + { + get + { + return _helpKeyword; + } + set + { + _helpKeyword = value; + } + } + + /// + /// Main task method + /// + /// + public override bool Execute() + { + if (Text != null || Code != null) + { + Log.LogWarning(null, Code, HelpKeyword, File, 0, 0, 0, 0, (Text == null) ? String.Empty : Text); + } + + return true; + } + } +} diff --git a/src/XMakeTasks/WinMDExp.cs b/src/XMakeTasks/WinMDExp.cs new file mode 100644 index 00000000000..296c9973f8f --- /dev/null +++ b/src/XMakeTasks/WinMDExp.cs @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Export a windows metadata file from a dll +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using System.Text; + +namespace Microsoft.Build.Tasks +{ + /// + /// Exports a managed assembly to a windows runtime metadata. + /// + public class WinMDExp : ToolTaskExtension + { + #region Properties + + /// + /// Set of references to pass to the winmdexp tool. + /// + [Required] + public ITaskItem[] References + { + get + { + return (ITaskItem[])Bag["References"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "References"); + Bag["References"] = value; + } + } + + /// + /// Warning codes to disable + /// + public string DisabledWarnings + { + get + { + return (string)Bag["DisabledWarnings"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "DisabledWarnings"); + Bag["DisabledWarnings"] = value; + } + } + + /// + /// Input documentation file + /// + public string InputDocumentationFile + { + get + { + return (string)Bag["InputDocumentationFile"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "InputDocumentationFile"); + Bag["InputDocumentationFile"] = value; + } + } + + /// + /// Output documentation file + /// + public string OutputDocumentationFile + { + get + { + return (string)Bag["OutputDocumentationFile"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "OutputDocumentationFile"); + Bag["OutputDocumentationFile"] = value; + } + } + + /// + /// Input PDB file + /// + public string InputPDBFile + { + get + { + return (string)Bag["InputPDBFile"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "InputPDBFile"); + Bag["InputPDBFile"] = value; + } + } + + /// + /// Output PDB file + /// + public string OutputPDBFile + { + get + { + return (string)Bag["OutputPDBFile"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "OutputPDBFile"); + Bag["OutputPDBFile"] = value; + } + } + + /// + /// WinMDModule to generate the WinMDFile for. + /// + [Required] + public string WinMDModule + { + get + { + return (string)Bag["WinMDModule"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "WinMDModule"); + Bag["WinMDModule"] = value; + } + } + + /// + /// Output windows metadata file .winmd + /// + [Output] + public string OutputWindowsMetadataFile + { + get + { + return (string)Bag["OutputWindowsMetadataFile"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "OutputWindowsMetadataFile"); + Bag["OutputWindowsMetadataFile"] = value; + } + } + + /// + /// Path to the SDK directory which contains this tool + /// + public string SdkToolsPath + { + get { return (string)Bag["SdkToolsPath"]; } + set { Bag["SdkToolsPath"] = value; } + } + + /// + /// Use output stream encoding as UTF-8. + /// + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "UTF", Justification = "Not worth breaking customers because of case correction")] + public bool UTF8Output + { + get { return (bool)Bag["UTF8Output"]; } + set { Bag["UTF8Output"] = value; } + } + + /// + /// Path to the SDK directory which contains this tool + /// + public bool TreatWarningsAsErrors + { + get { return (bool)Bag["TreatWarningsAsErrors"]; } + set { Bag["TreatWarningsAsErrors"] = value; } + } + + /// + /// The policy used for assembly unification. + /// + public string AssemblyUnificationPolicy + { + get + { + return (string)Bag["AssemblyUnificationPolicy"]; + } + + set + { + ErrorUtilities.VerifyThrowArgumentNull(value, "AssemblyUnificationPolicy"); + Bag["AssemblyUnificationPolicy"] = value; + } + } + + /// + /// The name of the tool to execute. + /// + override protected string ToolName + { + get + { + return "winmdexp.exe"; + } + } + + /// + /// Overridable property specifying the encoding of the captured task standard output stream + /// + protected override Encoding StandardOutputEncoding + { + get + { + return (UTF8Output) ? Encoding.UTF8 : base.StandardOutputEncoding; + } + } + + /// + /// Overridable property specifying the encoding of the captured task standard error stream + /// + protected override Encoding StandardErrorEncoding + { + get + { + return (UTF8Output) ? Encoding.UTF8 : base.StandardErrorEncoding; + } + } + + #endregion + + #region Tool Members + + /// + /// Fills the provided CommandLineBuilderExtension with all the command line options used when + /// executing this tool + /// + /// Gets filled with command line commands + protected internal override void AddCommandLineCommands(CommandLineBuilderExtension commandLine) + { + commandLine.AppendSwitchIfNotNull("/d:", OutputDocumentationFile); + commandLine.AppendSwitchIfNotNull("/md:", InputDocumentationFile); + commandLine.AppendSwitchIfNotNull("/mp:", InputPDBFile); + commandLine.AppendSwitchIfNotNull("/pdb:", OutputPDBFile); + commandLine.AppendSwitchIfNotNull("/assemblyunificationpolicy:", AssemblyUnificationPolicy); + + if (String.IsNullOrEmpty(OutputWindowsMetadataFile)) + { + OutputWindowsMetadataFile = Path.ChangeExtension(WinMDModule, ".winmd"); + } + + commandLine.AppendSwitchIfNotNull("/out:", OutputWindowsMetadataFile); + commandLine.AppendSwitchWithSplitting("/nowarn:", DisabledWarnings, ",", ';', ','); + commandLine.AppendWhenTrue("/warnaserror+", this.Bag, "TreatWarningsAsErrors"); + commandLine.AppendWhenTrue("/utf8output", this.Bag, "UTF8Output"); + + if (References != null) + { + // Loop through all the references passed in. We'll be adding separate + foreach (ITaskItem reference in this.References) + { + commandLine.AppendSwitchIfNotNull("/reference:", reference.ItemSpec); + } + } + + commandLine.AppendFileNameIfNotNull(WinMDModule); + + base.AddCommandLineCommands(commandLine); + } + + /// + /// The full path of the tool to execute. + /// + override protected string GenerateFullPathToTool() + { + string pathToTool = null; + + if (String.IsNullOrEmpty(pathToTool) || !File.Exists(pathToTool)) + { + pathToTool = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, Microsoft.Build.Utilities.ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolName, Log, true); + } + + return pathToTool; + } + + /// + /// Validate parameters, log errors and warnings and return true if Execute should proceed. + /// + override protected bool ValidateParameters() + { + if (References == null) + { + Log.LogErrorWithCodeFromResources("WinMDExp.MustPassReferences"); + return false; + } + + return true; + } + + /// + /// Returns true if task execution is not necessary. Executed after ValidateParameters + /// + override protected bool SkipTaskExecution() + { + if (!String.IsNullOrEmpty(OutputWindowsMetadataFile)) + { + if (File.Exists(OutputWindowsMetadataFile) && File.Exists(WinMDModule)) + { + FileInfo outputFileInfo = FileUtilities.GetFileInfoNoThrow(OutputWindowsMetadataFile); + FileInfo winMDModuleFileInfo = FileUtilities.GetFileInfoNoThrow(WinMDModule); + + // If the last write time of the input file is less than the last write time of the output file + // then the output is newer then the input so we do not need to re-run the tool. + if (outputFileInfo.LastWriteTimeUtc > winMDModuleFileInfo.LastWriteTimeUtc) + { + return true; + } + } + } + + return false; + } + #endregion + } +} diff --git a/src/XMakeTasks/Workflow.Shim.Targets b/src/XMakeTasks/Workflow.Shim.Targets new file mode 100644 index 00000000000..03a86174d6d --- /dev/null +++ b/src/XMakeTasks/Workflow.Shim.Targets @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/Workflow.VisualBasic.Shim.Targets b/src/XMakeTasks/Workflow.VisualBasic.Shim.Targets new file mode 100644 index 00000000000..9e13597f2c3 --- /dev/null +++ b/src/XMakeTasks/Workflow.VisualBasic.Shim.Targets @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/WriteCodeFragment.cs b/src/XMakeTasks/WriteCodeFragment.cs new file mode 100644 index 00000000000..a22daf79201 --- /dev/null +++ b/src/XMakeTasks/WriteCodeFragment.cs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Generates a temporary code file with the specified generated code fragment. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using System.Runtime.InteropServices; +using System.ComponentModel; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Configuration; +using System.Security; +using System.Collections.Generic; + +namespace Microsoft.Build.Tasks +{ + /// + /// Generates a temporary code file with the specified generated code fragment. + /// Does not delete the file. + /// + /// + /// Currently only supports writing .NET attributes. + /// + public class WriteCodeFragment : TaskExtension + { + /// + /// Language of code to generate. + /// Language name can be any language for which a CodeDom provider is + /// available. For example, "C#", "VisualBasic". + /// Emitted file will have the default extension for that language. + /// + [Required] + public string Language + { + get; + set; + } + + /// + /// Description of attributes to write. + /// Item include is the full type name of the attribute. + /// For example, "System.AssemblyVersionAttribute". + /// Each piece of metadata is the name-value pair of a parameter, which must be of type System.String. + /// Some attributes only allow positional constructor arguments, or the user may just prefer them. + /// To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + /// If a parameter index is skipped, it's an error. + /// + public ITaskItem[] AssemblyAttributes + { + get; + set; + } + + /// + /// Destination folder for the generated code. + /// Typically the intermediate folder. + /// + public ITaskItem OutputDirectory + { + get; + set; + } + + /// + /// The path to the file that was generated. + /// If this is set, and a file name, the destination folder will be prepended. + /// If this is set, and is rooted, the destination folder will be ignored. + /// If this is not set, the destination folder will be used, an arbitrary file name will be used, and + /// the default extension for the language selected. + /// + [Output] + public ITaskItem OutputFile + { + get; + set; + } + + /// + /// Main entry point. + /// + public override bool Execute() + { + if (String.IsNullOrEmpty(Language)) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", "Language", "WriteCodeFragment"); + return false; + } + + if (OutputFile == null && OutputDirectory == null) + { + Log.LogErrorWithCodeFromResources("WriteCodeFragment.MustSpecifyLocation"); + return false; + } + + string extension; + string code = GenerateCode(out extension); + + if (Log.HasLoggedErrors) + { + return false; + } + + if (code.Length == 0) + { + Log.LogMessageFromResources(MessageImportance.Low, "WriteCodeFragment.NoWorkToDo"); + OutputFile = null; + return true; + } + + try + { + if (OutputFile != null && OutputDirectory != null && !Path.IsPathRooted(OutputFile.ItemSpec)) + { + OutputFile = new TaskItem(Path.Combine(OutputDirectory.ItemSpec, OutputFile.ItemSpec)); + } + + OutputFile = OutputFile ?? new TaskItem(FileUtilities.GetTemporaryFile(OutputDirectory.ItemSpec, extension)); + + File.WriteAllText(OutputFile.ItemSpec, code); // Overwrites file if it already exists (and can be overwritten) + } + catch (Exception ex) + { + if (ExceptionHandling.NotExpectedException(ex)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotWriteOutput", (OutputFile == null) ? String.Empty : OutputFile.ItemSpec, ex.Message); + return false; + } + + Log.LogMessageFromResources(MessageImportance.Low, "WriteCodeFragment.GeneratedFile", OutputFile.ItemSpec); + + return !Log.HasLoggedErrors; + } + + /// + /// Generates the code into a string. + /// If it fails, logs an error and returns null. + /// If no meaningful code is generated, returns empty string. + /// Returns the default language extension as an out parameter. + /// + [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.IO.StringWriter.#ctor(System.Text.StringBuilder)", Justification = "Reads fine to me")] + private string GenerateCode(out string extension) + { + extension = null; + bool haveGeneratedContent = false; + + CodeDomProvider provider; + + try + { + provider = CodeDomProvider.CreateProvider(Language); + } + catch (ConfigurationException ex) + { + Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotCreateProvider", Language, ex.Message); + return null; + } + catch (SecurityException ex) + { + Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotCreateProvider", Language, ex.Message); + return null; + } + + extension = provider.FileExtension; + + CodeCompileUnit unit = new CodeCompileUnit(); + + CodeNamespace globalNamespace = new CodeNamespace(); + unit.Namespaces.Add(globalNamespace); + + // Declare authorship. Unfortunately CodeDOM puts this comment after the attributes. + string comment = ResourceUtilities.FormatResourceString("WriteCodeFragment.Comment", DateTime.Now.ToString()); + globalNamespace.Comments.Add(new CodeCommentStatement(comment)); + + if (AssemblyAttributes == null) + { + return String.Empty; + } + + // For convenience, bring in the namespaces, where many assembly attributes lie + globalNamespace.Imports.Add(new CodeNamespaceImport("System")); + globalNamespace.Imports.Add(new CodeNamespaceImport("System.Reflection")); + + foreach (ITaskItem attributeItem in AssemblyAttributes) + { + CodeAttributeDeclaration attribute = new CodeAttributeDeclaration(new CodeTypeReference(attributeItem.ItemSpec)); + + // Some attributes only allow positional constructor arguments, or the user may just prefer them. + // To set those, use metadata names like "_Parameter1", "_Parameter2" etc. + // If a parameter index is skipped, it's an error. + IDictionary customMetadata = attributeItem.CloneCustomMetadata(); + + List orderedParameters = new List(new CodeAttributeArgument[customMetadata.Count + 1] /* max possible slots needed */); + List namedParameters = new List(); + + foreach (DictionaryEntry entry in customMetadata) + { + string name = (string)entry.Key; + string value = (string)entry.Value; + + if (name.StartsWith("_Parameter", StringComparison.OrdinalIgnoreCase)) + { + int index; + + if (!Int32.TryParse(name.Substring("_Parameter".Length), out index)) + { + Log.LogErrorWithCodeFromResources("General.InvalidValue", name, "WriteCodeFragment"); + return null; + } + + if (index > orderedParameters.Count || index < 1) + { + Log.LogErrorWithCodeFromResources("WriteCodeFragment.SkippedNumberedParameter", index); + return null; + } + + // "_Parameter01" and "_Parameter1" would overwrite each other + orderedParameters[index - 1] = new CodeAttributeArgument(String.Empty, new CodePrimitiveExpression(value)); + } + else + { + namedParameters.Add(new CodeAttributeArgument(name, new CodePrimitiveExpression(value))); + } + } + + bool encounteredNull = false; + for (int i = 0; i < orderedParameters.Count; i++) + { + if (orderedParameters[i] == null) + { + // All subsequent args should be null, else a slot was missed + encounteredNull = true; + continue; + } + + if (encounteredNull) + { + Log.LogErrorWithCodeFromResources("WriteCodeFragment.SkippedNumberedParameter", i + 1 /* back to 1 based */); + return null; + } + + attribute.Arguments.Add(orderedParameters[i]); + } + + foreach (CodeAttributeArgument namedParameter in namedParameters) + { + attribute.Arguments.Add(namedParameter); + } + + unit.AssemblyCustomAttributes.Add(attribute); + haveGeneratedContent = true; + } + + StringBuilder generatedCode = new StringBuilder(); + + using (StringWriter writer = new StringWriter(generatedCode, CultureInfo.CurrentCulture)) + { + provider.GenerateCodeFromCompileUnit(unit, writer, new CodeGeneratorOptions()); + } + + string code = generatedCode.ToString(); + + // If we just generated infrastructure, don't bother returning anything + // as there's no point writing the file + return haveGeneratedContent ? code : String.Empty; + } + } +} diff --git a/src/XMakeTasks/XMakeTasksUnitTests/AssemblyIdentityTest.cs b/src/XMakeTasks/XMakeTasksUnitTests/AssemblyIdentityTest.cs new file mode 100644 index 00000000000..5425479a9a6 --- /dev/null +++ b/src/XMakeTasks/XMakeTasksUnitTests/AssemblyIdentityTest.cs @@ -0,0 +1,125 @@ +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +namespace XMakeTasksUnitTests +{ + + + /// + ///This is a test class for AssemblyIdentityTest and is intended + ///to contain all AssemblyIdentityTest Unit Tests + /// + [TestClass()] + public class AssemblyIdentityTest + { + + + private TestContext testContextInstance; + + /// + ///Gets or sets the test context which provides + ///information about and functionality for the current test run. + /// + public TestContext TestContext + { + get + { + return testContextInstance; + } + set + { + testContextInstance = value; + } + } + + #region Additional test attributes + // + //You can use the following additional attributes as you write your tests: + // + //Use ClassInitialize to run code before running the first test in the class + //[ClassInitialize()] + //public static void MyClassInitialize(TestContext testContext) + //{ + //} + // + //Use ClassCleanup to run code after all tests in a class have run + //[ClassCleanup()] + //public static void MyClassCleanup() + //{ + //} + // + //Use TestInitialize to run code before running each test + //[TestInitialize()] + //public void MyTestInitialize() + //{ + //} + // + //Use TestCleanup to run code after each test has run + //[TestCleanup()] + //public void MyTestCleanup() + //{ + //} + // + #endregion + + + /// + ///A test for IsFrameworkAssembly + /// + [TestMethod()] + public void IsFrameworkAssemblyTest() + { + bool actual; + IList listOfInstalledFrameworks = FrameworkMultiTargeting.GetSupportedTargetFrameworks(); + + // if 2.0 is installed on this computer, we will test IsFrameworkAssembly for 2.0 assemblies. + if (hasVersion(listOfInstalledFrameworks, "Version=v2.0")) + { + //if (hasVersion(listOfInstalledFrameworks + // Test 2.0 CLR binary + // "Microsoft.Build.Engine" Version="2.0.0.0" PublicKeyToken="b03f5f7f11d50a3a" Culture="neutral" ProcessorArchitecture="MSIL" FileVersion="2.0.50727.3026" InGAC="true" /> + AssemblyIdentity clr2Binary = new AssemblyIdentity("Microsoft.Build.Engine", "2.0.0.0", "b03f5f7f11d50a3a", "neutral", "MSIL"); + actual = clr2Binary.IsFrameworkAssembly; + Assert.IsTrue(actual); + } + + if (hasVersion(listOfInstalledFrameworks, "Version=v3.0")) + { + // Test 3.0 CLR binary + // AssemblyName="System.ServiceModel" Version="3.0.0.0" PublicKeyToken="b77a5c561934e089" Culture="neutral" ProcessorArchitecture="MSIL" InGAC="false" IsRedistRoot="true" /> + AssemblyIdentity clr3Binary = new AssemblyIdentity("System.ServiceModel", "3.0.0.0", "b77a5c561934e089", "neutral", "MSIL"); + actual = clr3Binary.IsFrameworkAssembly; + Assert.IsTrue(actual); + } + + if (hasVersion(listOfInstalledFrameworks, "Version=v3.5")) + { + // Test 3.5 CLR binary + // AssemblyName="Microsoft.Build.Tasks.v3.5" Version="3.5.0.0" PublicKeyToken="b03f5f7f11d50a3a" Culture="neutral" ProcessorArchitecture="MSIL" InGAC="false" /> + AssemblyIdentity clr35Binary = new AssemblyIdentity("Microsoft.Build.Tasks.v3.5", "3.5.0.0", "b03f5f7f11d50a3a", "neutral", "MSIL"); + actual = clr35Binary.IsFrameworkAssembly; + Assert.IsTrue(actual); + } + + if (hasVersion(listOfInstalledFrameworks, "Version=v4.0")) + { + // Test 4.0 CLR binary + // AssemblyName="Microsoft.VisualBasic" Version="10.0.0.0" PublicKeyToken="b03f5f7f11d50a3a" Culture="neutral" ProcessorArchitecture="MSIL" FileVersion="4.0.41117.0" InGAC="true" /> + AssemblyIdentity clr4Binary = new AssemblyIdentity("Microsoft.VisualBasic", "10.0.0.0", "b03f5f7f11d50a3a", "neutral", "MSIL"); + actual = clr4Binary.IsFrameworkAssembly; + Assert.IsTrue(actual); + } + } + + private bool hasVersion(IList listOfInstalledFrameworks, string p) + { + foreach (string fx in listOfInstalledFrameworks) + { + if (fx.Contains(p)) + return true; + } + + return false; + } + } +} diff --git a/src/XMakeTasks/XMakeTasksUnitTests/AuthoringTests.txt b/src/XMakeTasks/XMakeTasksUnitTests/AuthoringTests.txt new file mode 100644 index 00000000000..3eb1fca8202 --- /dev/null +++ b/src/XMakeTasks/XMakeTasksUnitTests/AuthoringTests.txt @@ -0,0 +1,136 @@ +========================================================================== + Visual Studio Team System: Overview of Authoring and Running Tests +========================================================================== + +This overview describes the features for authoring and running tests in +Visual Studio Team System and Visual Studio Team Edition for Software Testers. + +Opening Tests +------------- +To open a test, open a test project or a test metadata file (a file with +extension .vsmdi) that contains the definition of the test. You can find +test projects and metadata files in Solution Explorer. + +Viewing Tests +------------- +To see which tests are available to you, open the Test View window. Or, +if you have installed Team Edition for Software Testers, you can also open +the Test List Editor window to view tests. + +To open the Test View window, click the Test menu, point to Windows, and +then click Test View. To open the Test List Editor window (if you have +installed Team Edition for Software Testers), click Test, point to Windows, +and then click Test List Editor. + +Running Tests +------------- +You can run tests from the Test View window and the Test List Editor window. +See Viewing Tests to learn how to open these windows. To run one or more +tests displayed in the Test View window, first select the tests in that +window; to select multiple tests, hold either the Shift or CTRL key while +clicking tests. Then click the Run Tests button in the Test View window +toolbar. + +If you have installed Visual Studio Team Edition for Software Testers, you can +also use the Test List Editor window to run tests. To run tests in Test List Editor, +select the check box next to each test that you want to run. Then click the +Run Tests button in the Test List Editor window toolbar. + +Viewing Test Results +-------------------- +When you run a test or a series of tests, the results of the test run will be +shown in the Test Results window. Each individual test in the run is shown on +a separate line so that you can see its status. The window contains an +embedded status bar in the top half of the window that provides you with +summary details of the complete test run. + +To see more detailed results for a particular test result, double-click it in +the Test Results window. This opens a window that provides more information +about the particular test result, such as any specific error messages returned +by the test. + +Changing the way that tests are run +----------------------------------- +Each time you run one or more tests, a collection of settings is used to +determine how those tests are run. These settings are contained in a “test +settings” file. + +Here is a partial list of the changes you can make with a test settings file: + + - Change the naming scheme for each test run. + - Change the test controller that the tests are run on so that you can run + tests remotely. + - Gather code coverage data for the code being tested so that you can see + which lines of code are covered by your tests. + - Enable and disable test deployment. + - Specify additional files to deploy before tests are run. + - Select a different host, ASP.NET, for running ASP.NET unit tests. + - Select a different host, the smart device test host, for running smart device unit tests. + - Set various properties for the test agents that run your tests. + - Specify to use data collectors that can collect various information as + the tests are running. + - Run custom scripts at the start and end of each test run so that you can + set up the test environment exactly as required each time tests are run. + - Set time limits for tests and test runs. + - Set the browser mix and the number of times to repeat Web tests in the + test run. + +By default, a test settings file is created whenever you create a new test +project. You make changes to this file by double-clicking it in Solution +Explorer and then changing the test settings. (Test settings files have the +extension .testsettings.) + +A solution can contain multiple test settings files. Only one of those +files, known as the “Active” test settings file, is used to determine the +settings that are currently used for test runs. You select the active test +settings by clicking Select Active Test Settings on the Test menu. + +------------------------------------------------------------------------------- + +Test Types +---------- +Using Visual Studio Team Edition for Software Testers, you can create a number +of different test types: + +Unit test: Use a unit test to create a programmatic test in C++, Visual C# or +Visual Basic that exercises source code. A unit test calls the methods of a +class, passing suitable parameters, and verifies that the returned value is +what you expect. +There are three specialized variants of unit tests: + - Data-driven unit tests are created when you configure a unit test to be + called repeatedly for each row of a data source. The data from each row + is used by the unit test as input data. + - ASP.NET unit tests are unit tests that exercise code in an ASP.NET Web + application. + - Smart device unit tests are unit tests that are deployed to a smart device + or emulator and then executed by the smart device test host. + +Web Test: Web tests consist of an ordered series of HTTP requests that you +record in a browser session using Microsoft Internet Explorer. You can have +the test report specific details about the pages or sites it requests, such +as whether a particular page contains a specified string. + +Load Test: You use a load test to encapsulate non-manual tests, such as +unit, Web, and generic tests, and then run them simultaneously by using +virtual users. Running these tests under load generates test results, +including performance and other counters, in tables and in graphs. + +Generic test: A generic test is an existing program wrapped to function as a +test in Visual Studio. The following are examples of tests or programs that +you can turn into generic tests: + - An existing test that uses process exit codes to communicate whether the + test passed or failed. 0 indicates passing and any other value indicates + a failure. + - A general program to obtain specific functionality during a test scenario. + - A test or program that uses a special XML file (called a “summary results + file”), to communicate detailed results. + +Manual test: The manual test type is used when the test tasks are to be +completed by a test engineer as opposed to an automated script. + +Ordered test: Use an ordered test to execute a set of tests in an order you +specify. + +------------------------------------------------------------------------------- + + diff --git a/src/XMakeTasks/XMakeTasksUnitTests/Properties/AssemblyInfo.cs b/src/XMakeTasks/XMakeTasksUnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..71137e1b7a8 --- /dev/null +++ b/src/XMakeTasks/XMakeTasksUnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("XMakeTasksUnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("XMakeTasksUnitTests")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM componenets. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("de6f3aea-4f78-444d-9f24-6955d929c1f9")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/XMakeTasks/XMakeTasksUnitTests/UtilTest.cs b/src/XMakeTasks/XMakeTasksUnitTests/UtilTest.cs new file mode 100644 index 00000000000..ff01de06a76 --- /dev/null +++ b/src/XMakeTasks/XMakeTasksUnitTests/UtilTest.cs @@ -0,0 +1,96 @@ +using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace XMakeTasksUnitTests +{ + + + /// + ///This is a test class for UtilTest and is intended + ///to contain all UtilTest Unit Tests + /// + [TestClass()] + public class UtilTest + { + + + private TestContext testContextInstance; + + /// + ///Gets or sets the test context which provides + ///information about and functionality for the current test run. + /// + public TestContext TestContext + { + get + { + return testContextInstance; + } + set + { + testContextInstance = value; + } + } + + #region Additional test attributes + // + //You can use the following additional attributes as you write your tests: + // + //Use ClassInitialize to run code before running the first test in the class + //[ClassInitialize()] + //public static void MyClassInitialize(TestContext testContext) + //{ + //} + // + //Use ClassCleanup to run code after all tests in a class have run + //[ClassCleanup()] + //public static void MyClassCleanup() + //{ + //} + // + //Use TestInitialize to run code before running each test + //[TestInitialize()] + //public void MyTestInitialize() + //{ + //} + // + //Use TestCleanup to run code after each test has run + //[TestCleanup()] + //public void MyTestCleanup() + //{ + //} + // + #endregion + + + /// + ///A test for GetClrVersion + /// + [TestMethod()] + [DeploymentItem("Microsoft.Build.Tasks.v4.0.dll")] + public void GetClrVersionTest() + { + string targetFrameworkVersion = "v3.5"; + string expected = "2.0.50727.0"; + string actual; + actual = Util_Accessor.GetClrVersion(targetFrameworkVersion); + Assert.AreEqual(expected, actual); + + targetFrameworkVersion = "3.5"; + actual = Util_Accessor.GetClrVersion(targetFrameworkVersion); + Assert.AreEqual(expected, actual); + + System.Version currentVersion = System.Environment.Version; + System.Version clr4Version = new System.Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build, 0); + + targetFrameworkVersion = "v4.0"; + actual = Util_Accessor.GetClrVersion(targetFrameworkVersion); + expected = clr4Version.ToString(); + Assert.AreEqual(expected, actual); + + targetFrameworkVersion = "v4.2"; + actual = Util_Accessor.GetClrVersion(targetFrameworkVersion); + expected = clr4Version.ToString(); + Assert.AreEqual(expected, actual); + } + } +} diff --git a/src/XMakeTasks/XMakeTasksUnitTests/XMakeTasksUnitTests.csproj b/src/XMakeTasks/XMakeTasksUnitTests/XMakeTasksUnitTests.csproj new file mode 100644 index 00000000000..653ef033271 --- /dev/null +++ b/src/XMakeTasks/XMakeTasksUnitTests/XMakeTasksUnitTests.csproj @@ -0,0 +1,102 @@ + + + Debug + AnyCPU + 10.0.10911 + 2.0 + {9EA71CF9-9A62-4ED8-AFE8-DD5753EE377B} + Library + Properties + XMakeTasksUnitTests + XMakeTasksUnitTests + v4.0 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\Framework\objr\i386\Microsoft.Build.Framework.dll + + + False + ..\objr\i386\Microsoft.Build.Tasks.v4.0.dll + + + False + ..\..\Utilities\objr\i386\Microsoft.Build.Utilities.v4.0.dll + + + + $(SdkRefPath)\System.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Configuration.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Core.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Data.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Design.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Drawing.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Security.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.Windows.Forms.dll + + + False + ..\..\..\..\public\sdk\ref\v4.0\System.XML.dll + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/XMakeTasks/XamlRules/CSharp.BrowseObject.xaml b/src/XMakeTasks/XamlRules/CSharp.BrowseObject.xaml new file mode 100644 index 00000000000..b5151a4ebf8 --- /dev/null +++ b/src/XMakeTasks/XamlRules/CSharp.BrowseObject.xaml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/CSharp.ProjectItemsSchema.xaml b/src/XMakeTasks/XamlRules/CSharp.ProjectItemsSchema.xaml new file mode 100644 index 00000000000..a8b8e008446 --- /dev/null +++ b/src/XMakeTasks/XamlRules/CSharp.ProjectItemsSchema.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/CSharp.xaml b/src/XMakeTasks/XamlRules/CSharp.xaml new file mode 100644 index 00000000000..899420f7674 --- /dev/null +++ b/src/XMakeTasks/XamlRules/CSharp.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/Content.xaml b/src/XMakeTasks/XamlRules/Content.xaml new file mode 100644 index 00000000000..cfa3cf78836 --- /dev/null +++ b/src/XMakeTasks/XamlRules/Content.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/Debugger_General.xaml b/src/XMakeTasks/XamlRules/Debugger_General.xaml new file mode 100644 index 00000000000..49089861e8d --- /dev/null +++ b/src/XMakeTasks/XamlRules/Debugger_General.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/EmbeddedResource.xaml b/src/XMakeTasks/XamlRules/EmbeddedResource.xaml new file mode 100644 index 00000000000..32924484fc0 --- /dev/null +++ b/src/XMakeTasks/XamlRules/EmbeddedResource.xaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/Folder.xaml b/src/XMakeTasks/XamlRules/Folder.xaml new file mode 100644 index 00000000000..98fec3edb19 --- /dev/null +++ b/src/XMakeTasks/XamlRules/Folder.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/General.BrowseObject.xaml b/src/XMakeTasks/XamlRules/General.BrowseObject.xaml new file mode 100644 index 00000000000..61eeb693f0d --- /dev/null +++ b/src/XMakeTasks/XamlRules/General.BrowseObject.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/General.xaml b/src/XMakeTasks/XamlRules/General.xaml new file mode 100644 index 00000000000..1080a5e4d4c --- /dev/null +++ b/src/XMakeTasks/XamlRules/General.xaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/General_File.xaml b/src/XMakeTasks/XamlRules/General_File.xaml new file mode 100644 index 00000000000..68e5ae5d1b2 --- /dev/null +++ b/src/XMakeTasks/XamlRules/General_File.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/None.xaml b/src/XMakeTasks/XamlRules/None.xaml new file mode 100644 index 00000000000..2b82f066ef1 --- /dev/null +++ b/src/XMakeTasks/XamlRules/None.xaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/ProjectItemsSchema.xaml b/src/XMakeTasks/XamlRules/ProjectItemsSchema.xaml new file mode 100644 index 00000000000..28f54055052 --- /dev/null +++ b/src/XMakeTasks/XamlRules/ProjectItemsSchema.xaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/ResolvedAssemblyReference.xaml b/src/XMakeTasks/XamlRules/ResolvedAssemblyReference.xaml new file mode 100644 index 00000000000..86be5aa81db --- /dev/null +++ b/src/XMakeTasks/XamlRules/ResolvedAssemblyReference.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/ResolvedCOMReference.xaml b/src/XMakeTasks/XamlRules/ResolvedCOMReference.xaml new file mode 100644 index 00000000000..1aff28d9d8d --- /dev/null +++ b/src/XMakeTasks/XamlRules/ResolvedCOMReference.xaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/ResolvedProjectReference.xaml b/src/XMakeTasks/XamlRules/ResolvedProjectReference.xaml new file mode 100644 index 00000000000..8f1ceda99f4 --- /dev/null +++ b/src/XMakeTasks/XamlRules/ResolvedProjectReference.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/Scc.xaml b/src/XMakeTasks/XamlRules/Scc.xaml new file mode 100644 index 00000000000..155f08b65da --- /dev/null +++ b/src/XMakeTasks/XamlRules/Scc.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/SpecialFolder.xaml b/src/XMakeTasks/XamlRules/SpecialFolder.xaml new file mode 100644 index 00000000000..9cbbebef564 --- /dev/null +++ b/src/XMakeTasks/XamlRules/SpecialFolder.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/SubProject.xaml b/src/XMakeTasks/XamlRules/SubProject.xaml new file mode 100644 index 00000000000..bc479db7729 --- /dev/null +++ b/src/XMakeTasks/XamlRules/SubProject.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/VisualBasic.BrowseObject.xaml b/src/XMakeTasks/XamlRules/VisualBasic.BrowseObject.xaml new file mode 100644 index 00000000000..6da44dd8555 --- /dev/null +++ b/src/XMakeTasks/XamlRules/VisualBasic.BrowseObject.xaml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/VisualBasic.ProjectItemsSchema.xaml b/src/XMakeTasks/XamlRules/VisualBasic.ProjectItemsSchema.xaml new file mode 100644 index 00000000000..4010206fbc5 --- /dev/null +++ b/src/XMakeTasks/XamlRules/VisualBasic.ProjectItemsSchema.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/VisualBasic.xaml b/src/XMakeTasks/XamlRules/VisualBasic.xaml new file mode 100644 index 00000000000..b2ed3dfa33c --- /dev/null +++ b/src/XMakeTasks/XamlRules/VisualBasic.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/assemblyreference.xaml b/src/XMakeTasks/XamlRules/assemblyreference.xaml new file mode 100644 index 00000000000..531e007af32 --- /dev/null +++ b/src/XMakeTasks/XamlRules/assemblyreference.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/comreference.xaml b/src/XMakeTasks/XamlRules/comreference.xaml new file mode 100644 index 00000000000..164f0e736be --- /dev/null +++ b/src/XMakeTasks/XamlRules/comreference.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlRules/projectreference.xaml b/src/XMakeTasks/XamlRules/projectreference.xaml new file mode 100644 index 00000000000..1526db5784b --- /dev/null +++ b/src/XMakeTasks/XamlRules/projectreference.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XMakeTasks/XamlTaskFactory/CommandLineGenerator.cs b/src/XMakeTasks/XamlTaskFactory/CommandLineGenerator.cs new file mode 100644 index 00000000000..fa47c584135 --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/CommandLineGenerator.cs @@ -0,0 +1,774 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// A helper class that generates a command line based on the +// specified switch descriptions and values. +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Framework.XamlTypes; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// The list of active switches in the order they should be emitted. + /// + public class CommandLineGenerator + { + /// + /// The list of active switches in the order they should be emitted. + /// + private IEnumerable _switchOrderList; + + /// + /// The dictionary that holds all set switches + /// The string is the name of the property, and the CommandLineToolSwitch holds all of the relevant information + /// i.e., switch, boolean value, type, etc. + /// + private Dictionary _activeCommandLineToolSwitches = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Any additional options (as a literal string) that may have been specified in the project file + /// We eventually want to get rid of this + /// + private string _additionalOptions = String.Empty; + + /// + /// Creates a generator that generates a command-line based on the specified Xaml file and parameters. + /// + public CommandLineGenerator(Rule rule, Dictionary parameterValues) + { + ErrorUtilities.VerifyThrowArgumentNull(rule, "rule"); + ErrorUtilities.VerifyThrowArgumentNull(parameterValues, "parameterValues"); + + // Parse the Xaml file + TaskParser parser = new TaskParser(); + bool success = parser.ParseXamlDocument(rule); + ErrorUtilities.VerifyThrow(success, "Unable to parse specified file or contents."); + + // Generate the switch order list + _switchOrderList = parser.SwitchOrderList; + + _activeCommandLineToolSwitches = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (Property property in parser.Properties) + { + Object value = null; + if (parameterValues.TryGetValue(property.Name, out value)) + { + CommandLineToolSwitch switchToAdd = new CommandLineToolSwitch(); + if (!String.IsNullOrEmpty(property.Reversible) && String.Equals(property.Reversible, "true", StringComparison.OrdinalIgnoreCase)) + { + switchToAdd.Reversible = true; + } + + switchToAdd.IncludeInCommandLine = property.IncludeInCommandLine; + switchToAdd.Separator = property.Separator; + switchToAdd.DisplayName = property.DisplayName; + switchToAdd.Description = property.Description; + if (!String.IsNullOrEmpty(property.Required) && String.Equals(property.Required, "true", StringComparison.OrdinalIgnoreCase)) + { + switchToAdd.Required = true; + } + + switchToAdd.FallbackArgumentParameter = property.Fallback; + switchToAdd.FalseSuffix = property.FalseSuffix; + switchToAdd.TrueSuffix = property.TrueSuffix; + if (!String.IsNullOrEmpty(property.SwitchName)) + { + switchToAdd.SwitchValue = property.Prefix + property.SwitchName; + } + + switchToAdd.IsValid = true; + + // Based on the switch type, cast the value and set as appropriate + switch (property.Type) + { + case PropertyType.Boolean: + switchToAdd.Type = CommandLineToolSwitchType.Boolean; + switchToAdd.BooleanValue = (bool)value; + if (!String.IsNullOrEmpty(property.ReverseSwitchName)) + { + switchToAdd.ReverseSwitchValue = property.Prefix + property.ReverseSwitchName; + } + + break; + + case PropertyType.Integer: + switchToAdd.Type = CommandLineToolSwitchType.Integer; + switchToAdd.Number = (int)value; + if (!String.IsNullOrEmpty(property.Min)) + { + if (switchToAdd.Number < Convert.ToInt32(property.Min, System.Threading.Thread.CurrentThread.CurrentCulture)) + { + switchToAdd.IsValid = false; + } + } + + if (!String.IsNullOrEmpty(property.Max)) + { + if (switchToAdd.Number > Convert.ToInt32(property.Max, System.Threading.Thread.CurrentThread.CurrentCulture)) + { + switchToAdd.IsValid = false; + } + } + + break; + + case PropertyType.ItemArray: + switchToAdd.Type = CommandLineToolSwitchType.ITaskItemArray; + switchToAdd.TaskItemArray = (ITaskItem[])value; + break; + + case PropertyType.None: + break; + + case PropertyType.String: + switchToAdd.Type = CommandLineToolSwitchType.String; + switchToAdd.ReverseSwitchValue = property.Prefix + property.ReverseSwitchName; + if (property.Values.Count > 0) + { + string enumValueToSelect = (string)value; + + switchToAdd.Value = (string)value; + switchToAdd.AllowMultipleValues = true; + + // Find the matching value in the enum + foreach (Value enumValue in property.Values) + { + if (String.Equals(enumValue.Name, enumValueToSelect, StringComparison.OrdinalIgnoreCase)) + { + if (!String.IsNullOrEmpty(enumValue.SwitchName)) + { + switchToAdd.SwitchValue = enumValue.Prefix + enumValue.SwitchName; + } + else + { + switchToAdd = null; + break; + } + } + } + } + else + { + switchToAdd.Value = (string)value; + } + + break; + + case PropertyType.StringArray: + switchToAdd.Type = CommandLineToolSwitchType.StringArray; + switchToAdd.StringList = (string[])value; + break; + } + + if (switchToAdd != null) + { + _activeCommandLineToolSwitches[property.Name] = switchToAdd; + } + } + } + } + + /// + /// Creates a generator that generates a command-line based on the specified Xaml file and parameters. + /// + internal CommandLineGenerator(Dictionary activeCommandLineToolSwitches, IEnumerable switchOrderList) + { + _activeCommandLineToolSwitches = activeCommandLineToolSwitches; + _switchOrderList = switchOrderList; + } + + /// + /// Accessor for the additional options string. + /// + public string AdditionalOptions + { + get + { + return _additionalOptions; + } + + set + { + _additionalOptions = value; + } + } + + /// + /// The template which, if set, will be used to govern formatting of the command line(s) + /// + public string CommandLineTemplate + { + get; + set; + } + + /// + /// The string to append to the end of a non-templated commandline. + /// + public string AlwaysAppend + { + get; + set; + } + + /// + /// Generate the command-line + /// + public string GenerateCommandLine() + { + CommandLineBuilder commandLineBuilder = new CommandLineBuilder(true /* quote hyphens */); + + if (!String.IsNullOrEmpty(CommandLineTemplate)) + { + GenerateTemplatedCommandLine(commandLineBuilder); + } + else + { + GenerateStandardCommandLine(commandLineBuilder, false); + } + + return commandLineBuilder.ToString(); + } + + /// + /// Appends a literal string containing the verbatim contents of any + /// "AdditionalOptions" parameter. This goes last on the command + /// line in case it needs to cancel any earlier switch. + /// Ideally this should never be needed because the MSBuild task model + /// is to set properties, not raw switches + /// + internal void BuildAdditionalArgs(CommandLineBuilder cmdLine) + { + // We want additional options to be last so that this can always override other flags. + if ((cmdLine != null) && !String.IsNullOrEmpty(_additionalOptions)) + { + cmdLine.AppendSwitch(_additionalOptions); + } + } + + /// + /// Generates a part of the command line depending on the type + /// + /// Depending on the type of the switch, the switch is emitted with the proper values appended. + /// e.g., File switches will append file names, directory switches will append filenames with "\" on the end + internal void GenerateCommandsAccordingToType(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch, bool recursive) + { + // if this property has a parent skip printing it as it was printed as part of the parent prop printing + if (commandLineToolSwitch.Parents.Count > 0 && !recursive) + { + return; + } + + switch (commandLineToolSwitch.Type) + { + case CommandLineToolSwitchType.Boolean: + EmitBooleanSwitch(clb, commandLineToolSwitch); + break; + case CommandLineToolSwitchType.String: + EmitStringSwitch(clb, commandLineToolSwitch); + break; + case CommandLineToolSwitchType.StringArray: + EmitStringArraySwitch(clb, commandLineToolSwitch); + break; + case CommandLineToolSwitchType.Integer: + EmitIntegerSwitch(clb, commandLineToolSwitch); + break; + case CommandLineToolSwitchType.ITaskItemArray: + EmitTaskItemArraySwitch(clb, commandLineToolSwitch); + break; + default: + // should never reach this point - if it does, there's a bug somewhere. + ErrorUtilities.VerifyThrow(false, "InternalError"); + break; + } + } + + /// + /// Verifies that the required args are present. This function throws if we have missing required args + /// + internal bool VerifyRequiredArgumentsArePresent(CommandLineToolSwitch property, bool throwOnError) + { + return true; + } + + /// + /// Verifies that the dependencies are present, and if the dependencies are present, or if the property + /// doesn't have any dependencies, the switch gets emitted + /// + internal bool VerifyDependenciesArePresent(CommandLineToolSwitch property) + { + // check the dependency + if (property.Parents.Count > 0) + { + // has a dependency, now check to see whether at least one parent is set + // if it is set, add to the command line + // otherwise, ignore it + bool isSet = false; + foreach (string parentName in property.Parents) + { + isSet = isSet || HasSwitch(parentName); + } + + return isSet; + } + else + { + // no dependencies to account for + return true; + } + } + + /// + /// Returns true if the property has a value in the list of active tool switches + /// + internal bool IsPropertySet(string propertyName) + { + if (!String.IsNullOrEmpty(propertyName)) + { + return _activeCommandLineToolSwitches.ContainsKey(propertyName); + } + else + { + return false; + } + } + + /// + /// Checks to see if the switch name is empty + /// + internal bool HasSwitch(string propertyName) + { + if (IsPropertySet(propertyName)) + { + return !String.IsNullOrEmpty(_activeCommandLineToolSwitches[propertyName].Name); + } + else + { + return false; + } + } + + /// + /// Returns true if the property exists (regardless of whether it is + /// set or not) and false otherwise. + /// + internal bool PropertyExists(string propertyName) + { + return _switchOrderList.Contains(propertyName, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Emit a switch that's an array of task items + /// + private static void EmitTaskItemArraySwitch(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch) + { + if (String.IsNullOrEmpty(commandLineToolSwitch.Separator)) + { + foreach (ITaskItem itemName in commandLineToolSwitch.TaskItemArray) + { + clb.AppendSwitchIfNotNull(commandLineToolSwitch.SwitchValue, itemName.ItemSpec); + } + } + else + { + clb.AppendSwitchIfNotNull(commandLineToolSwitch.SwitchValue, commandLineToolSwitch.TaskItemArray, commandLineToolSwitch.Separator); + } + } + + /// + /// Generates the commands for the switches that may have an array of arguments + /// The switch may be empty. + /// + /// For stringarray switches (e.g., Sources), the CommandLineToolSwitchName (if it exists) is emitted + /// along with each and every one of the file names separately (if no separator is included), or with all of the + /// file names separated by the separator. + /// e.g., AdditionalIncludeDirectores = "@(Files)" where Files has File1, File2, and File3, the switch + /// /IFile1 /IFile2 /IFile3 or the switch /IFile1;File2;File3 is emitted (the latter case has a separator + /// ";" specified) + private void EmitStringArraySwitch(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch) + { + List stringList = new List(commandLineToolSwitch.StringList.Length); + for (int i = 0; i < commandLineToolSwitch.StringList.Length; ++i) + { + // Make sure the file doesn't contain escaped " (\") + string value; + if (commandLineToolSwitch.StringList[i].StartsWith("\"", StringComparison.OrdinalIgnoreCase) && commandLineToolSwitch.StringList[i].EndsWith("\"", StringComparison.OrdinalIgnoreCase)) + { + value = commandLineToolSwitch.StringList[i].Substring(1, commandLineToolSwitch.StringList[i].Length - 2).Trim(); + } + else + { + value = commandLineToolSwitch.StringList[i].Trim(); + } + + if (!String.IsNullOrEmpty(value)) + { + stringList.Add(value); + } + } + + string[] arrTrimStringList = stringList.ToArray(); + + if (String.IsNullOrEmpty(commandLineToolSwitch.Separator)) + { + foreach (string fileName in arrTrimStringList) + { + if (!PerformSwitchValueSubstition(clb, commandLineToolSwitch, fileName)) + { + clb.AppendSwitchIfNotNull(commandLineToolSwitch.SwitchValue, fileName); + } + } + } + else + { + if (!PerformSwitchValueSubstition(clb, commandLineToolSwitch, String.Join(commandLineToolSwitch.Separator, arrTrimStringList))) + { + clb.AppendSwitchIfNotNull(commandLineToolSwitch.SwitchValue, arrTrimStringList, commandLineToolSwitch.Separator); + } + } + } + + /// + /// Substitute the value for the switch into the switch value where the [value] string is found, if it exists. + /// + private bool PerformSwitchValueSubstition(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch, string switchValue) + { + Regex regex = new Regex(@"\[value]", RegexOptions.IgnoreCase); + Match match = regex.Match(commandLineToolSwitch.SwitchValue); + if (match.Success) + { + string prefixToAppend = commandLineToolSwitch.SwitchValue.Substring(match.Index + match.Length, commandLineToolSwitch.SwitchValue.Length - (match.Index + match.Length)); + string valueToAppend; + if (!switchValue.EndsWith("\\\\", StringComparison.OrdinalIgnoreCase) && switchValue.EndsWith("\\", StringComparison.OrdinalIgnoreCase) && prefixToAppend.Length > 0 && prefixToAppend[0] == '\"') + { + // If the combined string would create \" then we need to escape it + // if the combined string would create \\" then we ignore it as as assume it is already escaped. + valueToAppend = commandLineToolSwitch.SwitchValue.Substring(0, match.Index) + switchValue + "\\" + prefixToAppend; + } + else + { + valueToAppend = commandLineToolSwitch.SwitchValue.Substring(0, match.Index) + switchValue + prefixToAppend; + } + + clb.AppendSwitch(valueToAppend); + return true; + } + + return false; + } + + /// + /// Generates the commands for switches that have integers appended. + /// + /// For integer switches (e.g., WarningLevel), the CommandLineToolSwitchName is emitted + /// with the appropriate integer appended, as well as any arguments + /// e.g., WarningLevel = "4" will emit /W4 + private void EmitIntegerSwitch(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch) + { + if (commandLineToolSwitch.IsValid) + { + string numberAsString = commandLineToolSwitch.Number.ToString(System.Threading.Thread.CurrentThread.CurrentCulture); + if (PerformSwitchValueSubstition(clb, commandLineToolSwitch, numberAsString)) + { + return; + } + else if (!String.IsNullOrEmpty(commandLineToolSwitch.Separator)) + { + clb.AppendSwitch(commandLineToolSwitch.SwitchValue + commandLineToolSwitch.Separator + numberAsString); + } + else + { + clb.AppendSwitch(commandLineToolSwitch.SwitchValue + numberAsString); + } + } + } + + /// + /// Generates the switches for switches that either have literal strings appended, or have + /// different switches based on what the property is set to. + /// + /// The string switch emits a switch that depends on what the parameter is set to, with and + /// arguments + /// e.g., Optimization = "Full" will emit /Ox, whereas Optimization = "Disabled" will emit /Od + private void EmitStringSwitch(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch) + { + if (PerformSwitchValueSubstition(clb, commandLineToolSwitch, commandLineToolSwitch.Value)) + { + return; + } + + String strSwitch = String.Empty; + strSwitch += commandLineToolSwitch.SwitchValue + commandLineToolSwitch.Separator; + + String str = commandLineToolSwitch.Value; + if (!commandLineToolSwitch.AllowMultipleValues) + { + str = str.Trim(); + if (str.Contains(' ')) + { + if (!str.StartsWith("\"", StringComparison.OrdinalIgnoreCase)) + { + str = "\"" + str; + if (str.EndsWith(@"\", StringComparison.OrdinalIgnoreCase) && !str.EndsWith(@"\\", StringComparison.OrdinalIgnoreCase)) + { + str += "\\\""; + } + else + { + str += "\""; + } + } + } + } + else + { + strSwitch = String.Empty; + str = commandLineToolSwitch.SwitchValue; + string arguments = GatherArguments(commandLineToolSwitch.Name, commandLineToolSwitch.Arguments, commandLineToolSwitch.Separator); + if (!String.IsNullOrEmpty(arguments)) + { + str = str + commandLineToolSwitch.Separator + arguments; + } + } + + clb.AppendSwitchUnquotedIfNotNull(strSwitch, str); + } + + /// + /// Gets the arguments required by the specified switch and collects them into a string. + /// + private string GatherArguments(string parentSwitch, ICollection> arguments, string separator) + { + string retVal = String.Empty; + if (arguments != null) + { + foreach (Tuple arg in arguments) + { + CommandLineToolSwitch argSwitch; + if (_activeCommandLineToolSwitches.TryGetValue(arg.Item1, out argSwitch)) + { + if (!String.IsNullOrEmpty(retVal)) + { + retVal = retVal + separator; + } + + retVal = retVal + argSwitch.Value; + } + else + { + if (arg.Item2) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("Xaml.MissingRequiredArgument", parentSwitch, arg.Item1)); + } + } + } + } + + return retVal; + } + + /// + /// Generates the switches that are nonreversible + /// + /// A boolean switch is emitted if it is set to true. If it set to false, nothing is emitted. + /// e.g. nologo = "true" will emit /Og, but nologo = "false" will emit nothing. + private void EmitBooleanSwitch(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch) + { + if (commandLineToolSwitch.BooleanValue) + { + if (!String.IsNullOrEmpty(commandLineToolSwitch.SwitchValue)) + { + StringBuilder val = new StringBuilder(); + val.Insert(0, commandLineToolSwitch.Separator); + val.Insert(0, commandLineToolSwitch.TrueSuffix); + val.Insert(0, commandLineToolSwitch.SwitchValue); + clb.AppendSwitch(val.ToString()); + } + } + else + { + EmitReversibleBooleanSwitch(clb, commandLineToolSwitch); + } + } + + /// + /// Generates the command line for switches that are reversible + /// + /// A reversible boolean switch will emit a certain switch if set to true, but emit that + /// exact same switch with a flag appended on the end if set to false. + /// e.g., GlobalOptimizations = "true" will emit /Og, and GlobalOptimizations = "false" will emit /Og- + private void EmitReversibleBooleanSwitch(CommandLineBuilder clb, CommandLineToolSwitch commandLineToolSwitch) + { + // if the value is set to true, append whatever the TrueSuffix is set to. + // Otherwise, append whatever the FalseSuffix is set to. + if (!String.IsNullOrEmpty(commandLineToolSwitch.ReverseSwitchValue)) + { + string suffix = (commandLineToolSwitch.BooleanValue) ? commandLineToolSwitch.TrueSuffix : commandLineToolSwitch.FalseSuffix; + StringBuilder val = new StringBuilder(); + val.Insert(0, suffix); + val.Insert(0, commandLineToolSwitch.Separator); + val.Insert(0, commandLineToolSwitch.TrueSuffix); + val.Insert(0, commandLineToolSwitch.ReverseSwitchValue); + clb.AppendSwitch(val.ToString()); + } + } + + /// + /// Generates the command line using the standard algorithm. + /// + private void GenerateStandardCommandLine(CommandLineBuilder builder, bool allOptionsMode) + { + // iterates through the list of set CommandLineToolSwitches + foreach (string propertyName in _switchOrderList) + { + if (IsPropertySet(propertyName)) + { + CommandLineToolSwitch property = _activeCommandLineToolSwitches[propertyName]; + + if (allOptionsMode) + { + if (property.Type == CommandLineToolSwitchType.ITaskItemArray) + { + // If we are in all-options mode, we will ignore any "switches" which are item arrays. + continue; + } + else if (String.Equals(propertyName, "AdditionalOptions", StringComparison.OrdinalIgnoreCase)) + { + // If we are handling the [AllOptions], then skip the AdditionalOptions, which is handled later. + continue; + } + } + + // verify the dependencies + if (property.IncludeInCommandLine && VerifyDependenciesArePresent(property) && VerifyRequiredArgumentsArePresent(property, false)) + { + GenerateCommandsAccordingToType(builder, property, false); + } + } + else if (String.Equals(propertyName, "AlwaysAppend", StringComparison.OrdinalIgnoreCase)) + { + builder.AppendSwitch(AlwaysAppend); + } + } + + if (!allOptionsMode) + { + // additional args should go on the end + BuildAdditionalArgs(builder); + } + } + + /// + /// Generates the command-line using the template specified. + /// + private void GenerateTemplatedCommandLine(CommandLineBuilder builder) + { + // Match all instances of [asdf], where "asdf" can be any combination of any + // characters *except* a [ or an ]. i.e., if "[ [ sdf ]" is passed, then we will + // match "[ sdf ]" + string matchString = @"\[[^\[\]]+\]"; + Regex regex = new Regex(matchString, RegexOptions.ECMAScript); + MatchCollection matches = regex.Matches(CommandLineTemplate); + + int indexOfEndOfLastSubstitution = 0; + foreach (Match match in matches) + { + if (match.Length == 0) + { + continue; + } + + // Because we match non-greedily, in the case where we have input such as "[[[[[foo]", the match will + // be "[foo]". However, if there are multiple '[' in a row, we need to do some escaping logic, so we + // want to know what the first *consecutive* square bracket was. + int indexOfFirstBracketInMatch = match.Index; + + // Indexing using "indexOfFirstBracketInMatch - 1" is safe here because it will always be + // greater than indexOfEndOfLastSubstitution, which will always be 0 or greater. + while (indexOfFirstBracketInMatch > indexOfEndOfLastSubstitution && CommandLineTemplate[indexOfFirstBracketInMatch - 1].Equals('[')) + { + indexOfFirstBracketInMatch--; + } + + // Append everything we know we want to add -- everything between where the last substitution ended and + // this match (including previous '[' that were not initially technically part of the match) begins. + if (indexOfFirstBracketInMatch != indexOfEndOfLastSubstitution) + { + builder.AppendTextUnquoted(CommandLineTemplate.Substring(indexOfEndOfLastSubstitution, indexOfFirstBracketInMatch - indexOfEndOfLastSubstitution)); + } + + // Now replace every "[[" with a literal '['. We can do this by simply counting the number of '[' between + // the first one and the start of the match, since by definition everything in between is an '['. + // + 1 because match.Index is also a bracket. + int openBracketsInARow = match.Index - indexOfFirstBracketInMatch + 1; + + if (openBracketsInARow % 2 == 0) + { + // even number -- they all go away and the rest of the match is appended literally. + for (int i = 0; i < openBracketsInARow / 2; i++) + { + builder.AppendTextUnquoted("["); + } + + builder.AppendTextUnquoted(match.Value.Substring(1, match.Value.Length - 1)); + } + else + { + // odd number -- all but one get merged two at a time, and the rest of the match is substituted. + for (int i = 0; i < (openBracketsInARow - 1) / 2; i++) + { + builder.AppendTextUnquoted("["); + } + + // Determine which property the user has specified in the template. + string propertyName = match.Value.Substring(1, match.Value.Length - 2); + if (String.Equals(propertyName, "AllOptions", StringComparison.OrdinalIgnoreCase)) + { + // When [AllOptions] is specified, we append all switch-type options. + CommandLineBuilder tempBuilder = new CommandLineBuilder(true); + GenerateStandardCommandLine(tempBuilder, true); + builder.AppendTextUnquoted(tempBuilder.ToString()); + } + else if (String.Equals(propertyName, "AdditionalOptions", StringComparison.OrdinalIgnoreCase)) + { + BuildAdditionalArgs(builder); + } + else if (IsPropertySet(propertyName)) + { + CommandLineToolSwitch property = _activeCommandLineToolSwitches[propertyName]; + + // verify the dependencies + if (VerifyDependenciesArePresent(property) && VerifyRequiredArgumentsArePresent(property, false)) + { + CommandLineBuilder tempBuilder = new CommandLineBuilder(true); + GenerateCommandsAccordingToType(tempBuilder, property, false); + builder.AppendTextUnquoted(tempBuilder.ToString()); + } + } + else if (!PropertyExists(propertyName)) + { + // If the thing enclosed in square brackets is not in fact a property, we + // don't want to replace it. + builder.AppendTextUnquoted('[' + propertyName + ']'); + } + } + + indexOfEndOfLastSubstitution = match.Index + match.Length; + } + + builder.AppendTextUnquoted(CommandLineTemplate.Substring(indexOfEndOfLastSubstitution, CommandLineTemplate.Length - indexOfEndOfLastSubstitution)); + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/CommandLineToolSwitch.cs b/src/XMakeTasks/XamlTaskFactory/CommandLineToolSwitch.cs new file mode 100644 index 00000000000..09ff60e571e --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/CommandLineToolSwitch.cs @@ -0,0 +1,692 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Tool switch description class for DataDriven tasks. +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// This enumeration specifies the different types for each switch in a tool + /// The types are used in the documentation + /// + public enum CommandLineToolSwitchType + { + /// + /// The boolean type has a boolean value, and there are types: one that can have a flag appended on the end + /// and one that can't + /// e.g. GlobalOptimizations = "true" would be /Og, and GlobalOptimizations="false" would be /Og-, but + /// WarnAsError = "true" would be /WX, while WarnAsError = "false" would be nothing. + /// + Boolean = 0, + + /// + /// The integer switch is used for properties that have several different integer values, + /// and depending on the value the property is set to, appends an integer to the end + /// of a certain switch + /// e.g. WarningLevel = "0" is /W0, WarningLevel = "2" is /W2 + /// + Integer = 1, + + /// + /// The string switch is used for two kinds of properties. + /// The first is the kind that has multiple values, and has a different switch for each value + /// e.g. Optimization="disabled" is /Od, "Full" is /Ox + /// The second is the kind that has a literal string appended to the end of a switch. + /// This type is similar to the File type, but in this case, will never get quoted. + /// + String = 2, + + /// + /// The stringarray switch is used for properties that may have more + /// than one string appended to the end of the switch + /// e.g. InjectPrecompiledHeaderReference = myfile is /Ylmyfile + /// + StringArray = 3, + + /// + /// The ITaskItemArray type is used for properties that pass multiple files, but + /// want to keep the metadata. Otherwise, it is used in the same way as a StringArray type. + /// + ITaskItemArray = 4, + } + + /// + /// The class CommandLineToolSwitch holds information about the properties + /// for each task + /// + public class CommandLineToolSwitch + { + #region Constant strings + + /// + /// Boolean switch type + /// + private const string TypeBoolean = "CommandLineToolSwitchType.Boolean"; + + /// + /// Integer switch type + /// + private const string TypeInteger = "CommandLineToolSwitchType.Integer"; + + /// + /// ITaskItem switch type + /// + private const string TypeITaskItem = "CommandLineToolSwitchType.ITaskItem"; + + /// + /// ITaskItemArray switch type. + /// + private const string TypeITaskItemArray = "CommandLineToolSwitchType.ITaskItemArray"; + + /// + /// String array switch type. + /// + private const string TypeStringArray = "CommandLineToolSwitchType.StringArray"; + + #endregion + + /// + /// The name of the switch. + /// + private string _name = String.Empty; + + /// + /// The switch type + /// + private CommandLineToolSwitchType _type; + + /// + /// "true" if this should be emitted on the command line. + /// + private bool _includeInCommandLine; + + /// + /// The suffix to use when the switch is false/negated. + /// + private string _falseSuffix = String.Empty; + + /// + /// The suffix to use when the switch is true. + /// + private string _trueSuffix = String.Empty; + + /// + /// The separator to use between the switch name and its parameters. + /// + private string _separator = String.Empty; + + /// + /// The fallback parameter. + /// + private string _fallback = String.Empty; + + /// + /// Flag indicating if the switch is required. + /// + private bool _required; + + /// + /// Parents for the switch. + /// + private LinkedList _parents = new LinkedList(); + + /// + /// Overrides for the switch. + /// + private LinkedList> _overrides = new LinkedList>(); + + /// + /// Flag indicating if the switch is valid. + /// + private bool _isValid; + + /// + /// Flag for boolean switches, indicating the switch is reversible. + /// + private bool _reversible; + + /// + /// Flag indicating if the switch has multiple values. + /// + private bool _allowMultipleValues; + + /// + /// The value for a boolean switch. + /// + private bool _booleanValue = true; + + /// + /// The value for the integer type. + /// + private int _number; + + /// + /// The list of strings for a string array. + /// + private string[] _stringList; + + /// + /// The list of task items for ITaskItemArray types. + /// + private ITaskItem[] _taskItemArray; + + /// + /// The value for a string type. + /// + private string _value = String.Empty; + + /// + /// The switch text. + /// + private string _switchValue = String.Empty; + + /// + /// The reverse switch text, for reversible switches. + /// + private string _reverseSwitchValue = String.Empty; + + /// + /// The arguments from which the value should be derived. + /// + private ICollection> _arguments; + + /// + /// Thw switch description. + /// + private string _description = String.Empty; + + /// + /// The display name for the switch. + /// + private string _displayName = String.Empty; + + /// + /// The default constructor creates a new CommandLineToolSwitch to hold the name of + /// the tool, the attributes, the dependent switches, and the values (if they exist) + /// + public CommandLineToolSwitch() + { + // does nothing + } + + /// + /// Overloaded constructor. Takes a CommandLineToolSwitchType and sets the type. + /// + public CommandLineToolSwitch(CommandLineToolSwitchType toolType) + { + _type = toolType; + } + + #region Properties + + /// + /// The name of the parameter + /// + public string Name + { + get + { + return _name; + } + + set + { + _name = value; + } + } + + /// + /// Specifies if this switch should be included on the command-line. + /// + public bool IncludeInCommandLine + { + get + { + return _includeInCommandLine; + } + + set + { + _includeInCommandLine = value; + } + } + + /// + /// The Value of the parameter + /// + public string Value + { + get + { + return _value; + } + + set + { + _value = value; + } + } + + /// + /// Flag indicating if the switch is valid. + /// + public bool IsValid + { + get + { + return _isValid; + } + + set + { + _isValid = value; + } + } + + /// + /// The SwitchValue of the parameter + /// + public string SwitchValue + { + get + { + return _switchValue; + } + + set + { + _switchValue = value; + } + } + + /// + /// The SwitchValue of the parameter + /// + public string ReverseSwitchValue + { + get + { + return _reverseSwitchValue; + } + + set + { + _reverseSwitchValue = value; + } + } + + /// + /// The arguments. + /// + public ICollection> Arguments + { + get + { + return _arguments; + } + + set + { + _arguments = value; + } + } + + /// + /// The DisplayName of the parameter + /// + public string DisplayName + { + get + { + return _displayName; + } + + set + { + _displayName = value; + } + } + + /// + /// The Description of the parameter + /// + public string Description + { + get + { + return _description; + } + + set + { + _description = value; + } + } + + /// + /// The type of the switch, i.e., boolean, string, stringarray, etc. + /// + public CommandLineToolSwitchType Type + { + get + { + return _type; + } + + set + { + _type = value; + } + } + + /// + /// Indicates whether or not the switch is emitted with a flag when false + /// + public bool Reversible + { + get + { + return _reversible; + } + + set + { + _reversible = value; + } + } + + /// + /// True if multiple values are allowed. + /// + public bool AllowMultipleValues + { + get + { + return _allowMultipleValues; + } + + set + { + _allowMultipleValues = value; + } + } + + /// + /// The flag to append at the end of a switch when the switch is set to false + /// i.e., for all CL switches that are reversible, the FalseSuffix is "-" + /// + public string FalseSuffix + { + get + { + return _falseSuffix; + } + + set + { + _falseSuffix = value; + } + } + + /// + /// The flag to append to the end of the switch when that switch is true + /// i.e., In the OptimizeForWindows98, the switch is OPT, the FalseSuffix is + /// :NOWIN98, and the TrueSuffix is :WIN98 + /// + public string TrueSuffix + { + get + { + return _trueSuffix; + } + + set + { + _trueSuffix = value; + } + } + + /// + /// The separator indicates the characters that go between the switch and the string + /// in the string typed case, the characters that go between each name for the + /// string array case, or the characters that go between the switch and the + /// appendage for the boolean case. + /// + public string Separator + { + get + { + return _separator; + } + + set + { + _separator = value; + } + } + + /// + /// The Fallback attribute is used to specify which property to look at in the + /// case that the argument property is not set, or if the file that the + /// argument property indicates is nonexistent. + /// + public string FallbackArgumentParameter + { + get + { + return _fallback; + } + + set + { + _fallback = value; + } + } + + /// + /// This attribute specifies whether or not an argument attribute is required. + /// + public bool ArgumentRequired + { + get; + set; + } + + /// + /// This property indicates whether or not the property is required in the project file + /// + public bool Required + { + get + { + return _required; + } + + set + { + _required = value; + } + } + + /// + /// This property indicates the parent of the dependency + /// + public LinkedList Parents + { + get + { + return _parents; + } + } + + /// + /// This property indicates the parent of the dependency + /// + public LinkedList> Overrides + { + get + { + return _overrides; + } + } + + /// + /// The BooleanValue is used for the boolean switches, and are set to true + /// or false, depending on what you set it to. + /// + public bool BooleanValue + { + get + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.Boolean, "InvalidType", TypeBoolean); + return _booleanValue; + } + + set + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.Boolean, "InvalidType", TypeBoolean); + _booleanValue = value; + } + } + + /// + /// The number is the number you wish to append to the end of integer switches + /// + public int Number + { + get + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.Integer, "InvalidType", TypeInteger); + return _number; + } + + set + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.Integer, "InvalidType", TypeInteger); + _number = value; + } + } + + /// + /// Returns the set of inputs to a switch + /// + /// + public string[] StringList + { + get + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.StringArray, "InvalidType", TypeStringArray); + return _stringList; + } + + set + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.StringArray, "InvalidType", TypeStringArray); + _stringList = value; + } + } + + /// + /// Returns the set of inputs to a switch that is a set of ITaskItems + /// + /// + public ITaskItem[] TaskItemArray + { + get + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.ITaskItemArray, "InvalidType", TypeITaskItemArray); + return _taskItemArray; + } + + set + { + ErrorUtilities.VerifyThrow(_type == CommandLineToolSwitchType.ITaskItemArray, "InvalidType", TypeITaskItemArray); + _taskItemArray = value; + } + } + #endregion + } + + /// + /// Expresses a relationship between an argument and a property. + /// + public class PropertyRelation + { + /// + /// Constructor + /// + public PropertyRelation() + { + } + + /// + /// Constructor. + /// + public PropertyRelation(string argument, string value, bool required) + { + this.Argument = argument; + this.Value = value; + this.Required = required; + } + + /// + /// The name of the argument + /// + public string Argument + { + get; + set; + } + + /// + /// The value. + /// + public string Value + { + get; + set; + } + + /// + /// Flag indicating if the argument is required or not. + /// + public bool Required + { + get; + set; + } + } + + /// + /// Derived class indicating how to separate values from the specified argument. + /// + public class CommandLineArgumentRelation : PropertyRelation + { + /// + /// Constructor. + /// + public CommandLineArgumentRelation(string argument, string value, bool required, string separator) + : base(argument, value, required) + { + this.Separator = separator; + } + + /// + /// The separator. + /// + public string Separator + { + get; + set; + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/Property.cs b/src/XMakeTasks/XamlTaskFactory/Property.cs new file mode 100644 index 00000000000..4f167f97b55 --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/Property.cs @@ -0,0 +1,825 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Property description class for the XamlTaskFactory parser. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// The type of value this property takes. + /// + internal enum PropertyType + { + /// + /// The property has no value type specified + /// + None, + + /// + /// The property takes values of type Boolean + /// + Boolean, + + /// + /// The property takes values of type String + /// + String, + + /// + /// The property takes values of type Integer + /// + Integer, + + /// + /// The property takes values of type String[] + /// + StringArray, + + /// + /// The property takes values of type ITaskItem[] + /// + ItemArray + } + + /// + /// The class Property holds information about the properties + /// for each task + /// + internal class Property + { + /// + /// The property name + /// + private string _name = String.Empty; + + /// + /// The property type + /// + private PropertyType _type = PropertyType.None; + + /// + /// "true" if the property should itself be emitted on the command line. + /// + private bool _includeInCommandLine; + + /// + /// "true" if the property is a reversible switch + /// + private string _reversible = String.Empty; + + /// + /// The switch name + /// + private string _switchName = String.Empty; + + /// + /// The reverse switch name. + /// + private string _reverseSwitchName = String.Empty; + + /// + /// The false suffix. + /// + private string _falseSuffix = String.Empty; + + /// + /// The true suffix + /// + private string _trueSuffix = String.Empty; + + /// + /// The min value for an integer property. + /// + private string _max = String.Empty; + + /// + /// The max value for an integer property. + /// + private string _min = String.Empty; + + /// + /// The separator between the switch and its value. + /// + private string _separator = String.Empty; + + /// + /// The default value for the property. + /// + private string _defaultValue = String.Empty; + + /// + /// The argument from which the switch should derive its value. + /// + private string _argument = String.Empty; + + /// + /// The fallback argument if the preferred argument is not specified. + /// + private string _fallback = String.Empty; + + /// + /// "true" if the property is required. + /// + private string _required = String.Empty; + + /// + /// True if this property is an output property. + /// + private bool _output = false; + + /// + /// The prefix for thw property. + /// + private string _prefix = null; + + /// + /// The property category + /// + private string _category = String.Empty; + + /// + /// The display name. + /// + private string _displayName = String.Empty; + + /// + /// The description. + /// + private string _description = String.Empty; + + /// + /// The parents of this property. + /// + private LinkedList _parents = new LinkedList(); + + /// + /// The dependencies. + /// + private LinkedList _dependencies = new LinkedList(); + + /// + /// The values allowed for this property, if it is an enum. + /// + private List _values = new List(); + + /// + /// The arguments which can provide values to this property. + /// + private List _arguments = new List(); + + /// + /// Default constructor + /// + public Property() + { + // does nothing + } + + #region Properties + + /// + /// The type of the switch, i.e., boolean, stringarray, etc. + /// + public PropertyType Type + { + get + { + return _type; + } + + set + { + _type = value; + } + } + + /// + /// Specifies if the property should be included on the command line. + /// + public bool IncludeInCommandLine + { + get + { + return _includeInCommandLine; + } + + set + { + _includeInCommandLine = value; + } + } + + /// + /// Specifies whether the switch is reversible (has a false suffix) or not + /// + public string Reversible + { + get + { + return _reversible; + } + + set + { + _reversible = value; + } + } + + /// + /// The name of the switch, without the / in front of it + /// i.e., Od for the Optimization property + /// + public string SwitchName + { + get + { + return _switchName; + } + + set + { + _switchName = value; + } + } + + /// + /// The name of the reverse switch, without the / in front of it + /// + public string ReverseSwitchName + { + get + { + return _reverseSwitchName; + } + + set + { + _reverseSwitchName = value; + } + } + + /// + /// The flag to append at the end of a switch when the switch is set to false + /// i.e., for all CL switches that are reversible, the FalseSuffix is "-" + /// + public string FalseSuffix + { + get + { + return _falseSuffix; + } + + set + { + _falseSuffix = value; + } + } + + /// + /// The flag to append to the end of the switch when that switch is true + /// i.e., In the OptimizeForWindows98, the switch is OPT, the FalseSuffix is + /// :NOWIN98, and the TrueSuffix is :WIN98 + /// + public string TrueSuffix + { + get + { + return _trueSuffix; + } + + set + { + _trueSuffix = value; + } + } + + /// + /// The max integer value an integer typed switch can have + /// An exception should be thrown in the number the user specifies is + /// larger than the max + /// + public string Max + { + get + { + return _max; + } + + set + { + _max = value; + } + } + + /// + /// The minimum integer value an integer typed switch can have + /// An exception should be thrown in the number the user specifies is + /// less than the minimum + /// + public string Min + { + get + { + return _min; + } + + set + { + _min = value; + } + } + + /// + /// The separator indicates the characters that go between the switch and the string + /// in the string typed case, the characters that go between each name for the + /// string array case, or the characters that go between the switch and the + /// appendage for the boolean case. + /// + public string Separator + { + get + { + return _separator; + } + + set + { + _separator = value; + } + } + + /// + /// The default value for the switch to have (in the case of reversibles, true + /// or false, in the case of files, a default file name) + /// + public string DefaultValue + { + get + { + return _defaultValue; + } + + set + { + _defaultValue = value; + } + } + + /// + /// The argument specifies which property to look for when appending a + /// file name, and that property contains the actual file name. + /// i.e., UsePrecompiledHeader has the argument "PrecompiledHeaderThrough" + /// and the values "CreateUsingSpecific", "GenerateAuto", and "UseUsingSpecific" + /// that have the switches /Yc, /YX, and /Yu. + /// If PrecompiledHeaderThrough has the value "myfile", then the emitted switch + /// would be /Ycmyfile, /YXmyfile, or /Yumyfile + /// + public string Argument + { + get + { + return _argument; + } + + set + { + _argument = value; + } + } + + /// + /// The Fallback attribute is used to specify which property to look at in the + /// case that the argument property is not set, or if the file that the + /// argument property indicates is nonexistent. + /// + public string Fallback + { + get + { + return _fallback; + } + + set + { + _fallback = value; + } + } + + /// + /// This property whether or not the property is required in the project file + /// + public string Required + { + get + { + return _required; + } + + set + { + _required = value; + } + } + + /// + /// This property indicates whether the property is an output, i.e., object files + /// + public bool Output + { + get + { + return _output; + } + + set + { + _output = value; + } + } + + /// + /// The name of the property this one is dependent on. + /// + public LinkedList Parents + { + get + { + return _parents; + } + } + + /// + /// The name of the property + /// + public string Name + { + get + { + return _name; + } + + set + { + _name = value; + } + } + + /// + /// The list of switches that are dependent with this one. + /// + public LinkedList DependentArgumentProperties + { + get + { + return _dependencies; + } + } + + /// + /// The different choices for each property, and the corresponding switch + /// + public List Values + { + get + { + return _values; + } + } + + /// + /// The prefix for each switch. + /// + public string Prefix + { + get + { + return _prefix; + } + + set + { + _prefix = value; + } + } + + /// + /// The Category for each switch. + /// + public string Category + { + get + { + return _category; + } + + set + { + _category = value; + } + } + + /// + /// The Display Name for each switch. + /// + public string DisplayName + { + get + { + return _displayName; + } + + set + { + _displayName = value; + } + } + + /// + /// The Description for each switch. + /// + public string Description + { + get + { + return _description; + } + + set + { + _description = value; + } + } + + /// + /// The arguments which apply to this property. + /// + public List Arguments + { + get + { + return _arguments; + } + + set + { + _arguments = value; + } + } + + #endregion + + /// + /// creates a new Property with the exact same information as this one + /// + public Property Clone() + { + Property cloned = new Property(); + cloned.Type = _type; + cloned.SwitchName = _switchName; + cloned.ReverseSwitchName = _reverseSwitchName; + cloned.FalseSuffix = _falseSuffix; + cloned.TrueSuffix = _trueSuffix; + cloned.Max = _max; + cloned.Min = _min; + cloned.Separator = _separator; + cloned.DefaultValue = _defaultValue; + cloned.Argument = _argument; + cloned.Fallback = _fallback; + cloned.Required = _required; + cloned.Output = _output; + cloned.Reversible = _reversible; + cloned.Name = _name; + cloned.Prefix = _prefix; + return cloned; + } + } + + /// + /// An enum value. + /// + internal class Value + { + /// + /// The name of the value. + /// + private string _name = String.Empty; + + /// + /// The switch name when this value is specified. + /// + private string _switchName = String.Empty; + + /// + /// The reverse switch name. + /// + private string _reverseSwitchName = String.Empty; + + /// + /// The description of the value. + /// + private string _description = String.Empty; + + /// + /// The display name of the value. + /// + private string _displayName = String.Empty; + + /// + /// The prefix. + /// + private string _prefix = null; + + /// + /// The arguments for this value. + /// + private List _arguments = new List(); + + /// + /// The name of the property + /// + public string Name + { + get + { + return _name; + } + + set + { + _name = value; + } + } + + /// + /// The switch Name of the property + /// + public string SwitchName + { + get + { + return _switchName; + } + + set + { + _switchName = value; + } + } + + /// + /// The switch Name of the property + /// + public string ReverseSwitchName + { + get + { + return _reverseSwitchName; + } + + set + { + _reverseSwitchName = value; + } + } + + /// + /// The switch Name of the property + /// + public string Description + { + get + { + return _description; + } + + set + { + _description = value; + } + } + + /// + /// The switch Name of the property + /// + public string DisplayName + { + get + { + return _displayName; + } + + set + { + _displayName = value; + } + } + + /// + /// The prefix for each switch. + /// + public string Prefix + { + get + { + return _prefix; + } + + set + { + _prefix = value; + } + } + + /// + /// The switch Name of the property + /// + public List Arguments + { + get + { + return _arguments; + } + + set + { + _arguments = value; + } + } + } + + /// + /// An argument for the property. + /// + internal class Argument + { + /// + /// The parameter to which the argument refers. + /// + private string _parameter = String.Empty; + + /// + /// The argument value separator. + /// + private string _separator = String.Empty; + + /// + /// True if the argument is required. + /// + private bool _required = false; + + /// + /// The switch Name of the property + /// + public string Parameter + { + get + { + return _parameter; + } + + set + { + _parameter = value; + } + } + + /// + /// The switch Name of the property + /// + public string Separator + { + get + { + return _separator; + } + + set + { + _separator = value; + } + } + + /// + /// The switch Name of the property + /// + public bool Required + { + get + { + return _required; + } + + set + { + _required = value; + } + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/RelationsParser.cs b/src/XMakeTasks/XamlTaskFactory/RelationsParser.cs new file mode 100644 index 00000000000..78db36c8689 --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/RelationsParser.cs @@ -0,0 +1,900 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +// Property description class for the XamlTaskFactory parser. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.IO; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// Class describing the relationship between switches. + /// + internal class SwitchRelations + { + private string _switchValue; + private string _status; + private List _includedPlatforms; + private List _excludedPlatforms; + private List _conflicts; + private List _overrides; + private List _requires; + private Dictionary> _externalOverrides; + private Dictionary> _externalConflicts; + private Dictionary> _externalRequires; + + public SwitchRelations() + { + _switchValue = String.Empty; + _status = String.Empty; + _conflicts = new List(); + _overrides = new List(); + _requires = new List(); + _includedPlatforms = new List(); + _excludedPlatforms = new List(); + _externalOverrides = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _externalConflicts = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _externalRequires = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + public SwitchRelations Clone() + { + SwitchRelations cloned = new SwitchRelations(); + cloned._switchValue = _switchValue; + cloned._status = _status; + cloned._conflicts = new List(_conflicts); + cloned._overrides = new List(_overrides); + cloned._requires = new List(_requires); + cloned._excludedPlatforms = new List(_excludedPlatforms); + cloned._includedPlatforms = new List(_includedPlatforms); + cloned._externalConflicts = new Dictionary>(_externalConflicts, StringComparer.OrdinalIgnoreCase); + cloned._externalOverrides = new Dictionary>(_externalOverrides, StringComparer.OrdinalIgnoreCase); + cloned._externalRequires = new Dictionary>(_externalRequires, StringComparer.OrdinalIgnoreCase); + + return cloned; + } + + public string SwitchValue + { + get + { + return _switchValue; + } + + set + { + _switchValue = value; + } + } + + public string Status + { + get + { + return _status; + } + + set + { + _status = value; + } + } + + public List Conflicts + { + get + { + return _conflicts; + } + + set + { + _conflicts = value; + } + } + + public List IncludedPlatforms + { + get + { + return _includedPlatforms; + } + + set + { + _includedPlatforms = value; + } + } + + public List ExcludedPlatforms + { + get + { + return _excludedPlatforms; + } + + set + { + _excludedPlatforms = value; + } + } + + public List Overrides + { + get + { + return _overrides; + } + + set + { + _overrides = value; + } + } + + public List Requires + { + get + { + return _requires; + } + + set + { + _requires = value; + } + } + + public Dictionary> ExternalOverrides + { + get + { + return _externalOverrides; + } + set + { + _externalOverrides = value; + } + } + + public Dictionary> ExternalConflicts + { + get + { + return _externalConflicts; + } + + set + { + _externalConflicts = value; + } + } + + public Dictionary> ExternalRequires + { + get + { + return _externalRequires; + } + + set + { + _externalRequires = value; + } + } + } + + /// + /// The RelationsParser class takes an xml file and parses the parameters for a task. + /// + internal class RelationsParser + { + /// + /// The name of the task e.g., CL + /// + private string _name; + + /// + /// The name of the executable e.g., cl.exe + /// + private string _toolName; + + /// + /// The base class + /// + private string _baseClass = "DataDrivenToolTask"; + + /// + /// The namespace to generate the class into + /// + private string _namespaceValue = "MyDataDrivenTasks"; + + /// + /// The resource namespace to pass to the base class, if any + /// + private string _resourceNamespaceValue = null; + + /// + /// The prefix to append before a switch is emitted. + /// Is typically a "/", but can also be a "-" + /// + private string _defaultPrefix = "/"; + + /// + /// The list that contains all of the properties that can be set on a task + /// + private LinkedList _properties = new LinkedList(); + + /// + /// The list that contains all of the properties that can be set on a task + /// + private Dictionary _switchRelationsList = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The list that contains all of the properties that have a default value + /// + private LinkedList _defaultSet = new LinkedList(); + + /// + /// The list of properties that serve as fallbacks for other properties. + /// That is, if a certain property is not set, but has a fallback, we need to check + /// to see if that fallback is set. + /// + private Dictionary _fallbackSet = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// A boolean to see if the current file parsed is an import file. + /// + private bool _isImport; + + /// + /// The number of errors that occurred while parsing the xml file or generating the code + /// + private int _errorCount; + + /// + /// The errors that occurred while parsing the xml file or generating the code + /// + private LinkedList _errorLog = new LinkedList(); + + #region Private const strings + private const string xmlNamespace = "http://schemas.microsoft.com/developer/msbuild/tasks/2005"; + private const string toolNameString = "TOOLNAME"; + private const string prefixString = "PREFIX"; + private const string baseClassAttribute = "BASECLASS"; + private const string namespaceAttribute = "NAMESPACE"; + private const string resourceNamespaceAttribute = "RESOURCENAMESPACE"; + private const string importType = "IMPORT"; + private const string tasksAttribute = "TASKS"; + private const string parameterType = "PARAMETER"; + private const string parameterGroupType = "PARAMETERGROUP"; + private const string enumType = "VALUE"; + private const string task = "TASK"; + private const string nameProperty = "NAME"; + private const string status = "STATUS"; + private const string switchName = "SWITCH"; + private const string reverseSwitchName = "REVERSESWITCH"; + private const string oldName = "OLDNAME"; + private const string argumentType = "ARGUMENT"; + private const string argumentValueName = "ARGUMENTVALUE"; + private const string relations = "RELATIONS"; + private const string switchGroupType = "SWITCHGROUP"; + private const string switchType = "SWITCH"; + private const string includedPlatformType = "INCLUDEDPLATFORM"; + private const string excludedPlatformType = "EXCLUDEDPLATFORM"; + private const string overridesType = "OVERRIDES"; + private const string conflictsType = "CONFLICTS"; + private const string requiresType = "REQUIRES"; + private const string externalOverridesType = "EXTERNALOVERRIDES"; + private const string externalConflictsType = "EXTERNALCONFLICTS"; + private const string externalRequiresType = "EXTERNALREQUIRES"; + private const string toolAttribute = "TOOL"; + private const string switchAttribute = "SWITCH"; + + // properties + private const string typeProperty = "TYPE"; + private const string typeAlways = "ALWAYS"; + private const string trueProperty = "TRUE"; + private const string falseProperty = "FALSE"; + private const string minProperty = "MIN"; + private const string maxProperty = "MAX"; + private const string separatorProperty = "SEPARATOR"; + private const string defaultProperty = "DEFAULT"; + private const string fallbackProperty = "FALLBACKARGUMENTPARAMETER"; + private const string outputProperty = "OUTPUT"; + private const string argumentProperty = "ARGUMENTPARAMETER"; + private const string argumentRequiredProperty = "REQUIRED"; + private const string propertyRequiredProperty = "REQUIRED"; + private const string reversibleProperty = "REVERSIBLE"; + private const string categoryProperty = "CATEGORY"; + private const string displayNameProperty = "DISPLAYNAME"; + private const string descriptionProperty = "DESCRIPTION"; + #endregion + + /// + /// The constructor. + /// + public RelationsParser() + { + // do nothing + } + + #region Properties + + /// + /// The name of the task + /// + public string GeneratedTaskName + { + get + { + return _name; + } + set + { + _name = value; + } + } + + /// + /// The base type of the class + /// + public string BaseClass + { + get + { + return _baseClass; + } + } + + /// + /// The namespace of the class + /// + public string Namespace + { + get + { + return _namespaceValue; + } + } + + /// + /// Namespace for the resources + /// + public string ResourceNamespace + { + get + { + return _resourceNamespaceValue; + } + } + + /// + /// The name of the executable + /// + public string ToolName + { + get + { + return _toolName; + } + } + + /// + /// The default prefix for each switch + /// + public string DefaultPrefix + { + get + { + return _defaultPrefix; + } + } + + /// + /// All of the parameters that were parsed + /// + public LinkedList Properties + { + get + { + return _properties; + } + } + + /// + /// All of the parameters that have a default value + /// + public LinkedList DefaultSet + { + get + { + return _defaultSet; + } + } + + /// + /// All of the properties that serve as fallbacks for unset properties + /// + public Dictionary FallbackSet + { + get + { + return _fallbackSet; + } + } + + /// + /// Returns the number of errors encountered + /// + public int ErrorCount + { + get + { + return _errorCount; + } + } + + /// + /// Returns the log of errors + /// + public LinkedList ErrorLog + { + get + { + return _errorLog; + } + } + + public Dictionary SwitchRelationsList + { + get + { + return _switchRelationsList; + } + } + + + #endregion + + /// + /// The method that loads in an XML file + /// + /// the xml file containing switches and properties + private XmlDocument LoadFile(string fileName) + { + try + { + XmlDocument xmlDocument = new XmlDocument(); + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Ignore; + XmlReader reader = XmlReader.Create(fileName, settings); + xmlDocument.Load(reader); + return xmlDocument; + } + catch (FileNotFoundException e) + { + LogError("LoadFailed", e.ToString()); + return null; + } + catch (XmlException e) + { + LogError("XmlError", e.ToString()); + return null; + } + } + + /// + /// Overloaded method that reads from a stream to load. + /// + /// the xml file containing switches and properties + internal XmlDocument LoadXml(string xml) + { + try + { + XmlDocument xmlDocument = new XmlDocument(); + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Ignore; + XmlReader reader = XmlReader.Create(new StringReader(xml), settings); + xmlDocument.Load(reader); + return xmlDocument; + } + catch (XmlException e) + { + LogError("XmlError", e.ToString()); + return null; + } + } + + /// + /// Parses the xml file + /// + /// + /// + public bool ParseXmlDocument(string fileName) + { + XmlDocument xmlDocument = LoadFile(fileName); + if (xmlDocument != null) + { + return ParseXmlDocument(xmlDocument); + } + else + { + return false; + } + } + + /// + /// Parses the loaded xml file, creates toolSwitches and adds them to the properties list + /// + internal bool ParseXmlDocument(XmlDocument xmlDocument) + { + ErrorUtilities.VerifyThrow(xmlDocument != null, "NoXml"); + + // find the root element + XmlNode node = xmlDocument.FirstChild; + while (!IsXmlRootElement(node)) + { + node = node.NextSibling; + } + + // now we know that we've found the root; verify it is the task element + // verify the namespace + if (String.IsNullOrEmpty(node.NamespaceURI) || !String.Equals(node.NamespaceURI, xmlNamespace, StringComparison.OrdinalIgnoreCase)) + { + LogError("InvalidNamespace", xmlNamespace); + return false; + } + + // verify that the element name is "task" + if (!VerifyNodeName(node)) + { + LogError("MissingRootElement", relations); + return false; + } + else if (!VerifyAttributeExists(node, nameProperty) && !_isImport) + { + // we must have the name attribute if it not an import + LogError("MissingAttribute", task, nameProperty); + return false; + } + // TODO verify resource namespace exists + + // we now know that that there is indeed a name attribute + // assign prefix, toolname if they exist + foreach (XmlAttribute attribute in node.Attributes) + { + if (String.Equals(attribute.Name, prefixString, StringComparison.OrdinalIgnoreCase)) + { + _defaultPrefix = attribute.InnerText; + } + else if (String.Equals(attribute.Name, toolNameString, StringComparison.OrdinalIgnoreCase)) + { + _toolName = attribute.InnerText; + } + else if (String.Equals(attribute.Name, nameProperty, StringComparison.OrdinalIgnoreCase)) + { + _name = attribute.InnerText; + } + else if (String.Equals(attribute.Name, baseClassAttribute, StringComparison.OrdinalIgnoreCase)) + { + _baseClass = attribute.InnerText; + } + else if (String.Equals(attribute.Name, namespaceAttribute, StringComparison.OrdinalIgnoreCase)) + { + _namespaceValue = attribute.InnerText; + } + else if (String.Equals(attribute.Name, resourceNamespaceAttribute, StringComparison.OrdinalIgnoreCase)) + { + _resourceNamespaceValue = attribute.InnerText; + } + } + // parse the child nodes if it has any + if (node.HasChildNodes) + { + return ParseSwitchGroupOrSwitch(node.FirstChild, _switchRelationsList, null); + } + else + { + LogError("NoChildren"); + return false; + } + } + + /// + /// Checks to see if the "name" attribute exists + /// + /// + /// + /// + private static bool VerifyAttributeExists(XmlNode node, string attributeName) + { + if (node.Attributes != null) + { + foreach (XmlAttribute attribute in node.Attributes) + { + if (attribute.Name.Equals(attributeName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + return false; + } + + /// + /// Checks to see if the element's name is "task" + /// + /// + /// + private static bool VerifyNodeName(XmlNode node) + { + return String.Equals(node.Name, relations, StringComparison.OrdinalIgnoreCase); + } + + private bool ParseSwitchGroupOrSwitch(XmlNode node, Dictionary switchRelationsList, SwitchRelations switchRelations) + { + while (node != null) + { + if (node.NodeType == XmlNodeType.Element) + { + // if the node's name is get all the attributes + if (String.Equals(node.Name, switchGroupType, StringComparison.OrdinalIgnoreCase)) + { + SwitchRelations newSwitchRelations = ObtainAttributes(node, switchRelations); + if (!ParseSwitchGroupOrSwitch(node.FirstChild, switchRelationsList, newSwitchRelations)) + { + return false; + } + } + else if (String.Equals(node.Name, switchType, StringComparison.OrdinalIgnoreCase)) + { + // node is a switchRelations + if (!ParseSwitch(node, switchRelationsList, switchRelations)) + { + return false; + } + } + else if (String.Equals(node.Name, importType, StringComparison.OrdinalIgnoreCase)) + { + // node is an import option + if (!ParseImportOption(node)) + { + return false; + } + } + } + node = node.NextSibling; + } + return true; + } + + private bool ParseImportOption(XmlNode node) + { + if (!VerifyAttributeExists(node, tasksAttribute)) + { + LogError("MissingAttribute", importType, tasksAttribute); + return false; + } + else + { + // we now know there is a tasks attribute + string[] importTasks = null; + foreach (XmlAttribute attribute in node.Attributes) + { + if (String.Equals(attribute.Name, tasksAttribute, StringComparison.OrdinalIgnoreCase)) + { + importTasks = attribute.InnerText.Split(';'); + } + } + _isImport = true; + foreach (string task in importTasks) + { + if (!ParseXmlDocument(task)) + { + return false; + } + } + _isImport = false; + } + return true; + } + + private bool ParseSwitch(XmlNode node, Dictionary switchRelationsList, SwitchRelations switchRelations) + { + SwitchRelations switchRelationsToAdd = ObtainAttributes(node, switchRelations); + + // make sure that the switchRelationsList has a name, unless it is type always + if (switchRelationsToAdd.SwitchValue == null || switchRelationsToAdd.SwitchValue == String.Empty) + { + return false; + } + + // generate the list of parameters in order + if (!switchRelationsList.ContainsKey(switchRelationsToAdd.SwitchValue)) + { + switchRelationsList.Remove(switchRelationsToAdd.SwitchValue); + } + + + // build the dependencies and the values for a parameter + XmlNode child = node.FirstChild; + while (child != null) + { + if (child.NodeType == XmlNodeType.Element) + { + if (String.Equals(child.Name, requiresType, StringComparison.OrdinalIgnoreCase)) + { + string Name = String.Empty; + string Tool = String.Empty; + string Switch = String.Empty; + bool isExternal = false; + foreach (XmlAttribute attrib in child.Attributes) + { + switch (attrib.Name.ToUpperInvariant()) + { + case nameProperty: + Name = attrib.InnerText; + break; + case toolAttribute: + isExternal = true; + Tool = attrib.InnerText; + break; + case switchAttribute: + Switch = attrib.InnerText; + break; + default: + return false; + } + } + + if (!isExternal) + if (Switch != String.Empty) + switchRelationsToAdd.Requires.Add(Switch); + else + return false; + else + { + if (!switchRelationsToAdd.ExternalRequires.ContainsKey(Tool)) + { + List switches = new List(); + switches.Add(Switch); + switchRelationsToAdd.ExternalRequires.Add(Tool, switches); + } + else + { + switchRelationsToAdd.ExternalRequires[Tool].Add(Switch); + } + } + } + + else if (String.Equals(child.Name, includedPlatformType, StringComparison.OrdinalIgnoreCase)) + { + foreach (XmlAttribute attrib in child.Attributes) + { + switch (attrib.Name.ToUpperInvariant()) + { + case nameProperty: + switchRelationsToAdd.IncludedPlatforms.Add(attrib.InnerText); + break; + default: + return false; + } + } + } + else if (String.Equals(child.Name, excludedPlatformType, StringComparison.OrdinalIgnoreCase)) + { + foreach (XmlAttribute attrib in child.Attributes) + { + switch (attrib.Name.ToUpperInvariant()) + { + case nameProperty: + switchRelationsToAdd.ExcludedPlatforms.Add(attrib.InnerText); + break; + default: + return false; + } + } + } + else if (String.Equals(child.Name, overridesType, StringComparison.OrdinalIgnoreCase)) + { + foreach (XmlAttribute attrib in child.Attributes) + { + switch (attrib.Name.ToUpperInvariant()) + { + case switchName: + switchRelationsToAdd.Overrides.Add(attrib.InnerText); + break; + case argumentValueName: + break; + default: + return false; + } + } + } + } + child = child.NextSibling; + } + + // We've read any enumerated values and any dependencies, so we just + // have to add the switchRelations + switchRelationsList.Add(switchRelationsToAdd.SwitchValue, switchRelationsToAdd); + return true; + } + + /// + /// Gets all the attributes assigned in the xml file for this parameter or all of the nested switches for + /// this parameter group + /// + /// + /// + /// + private SwitchRelations ObtainAttributes(XmlNode node, SwitchRelations switchGroup) + { + SwitchRelations switchRelations; + if (switchGroup != null) + { + switchRelations = switchGroup.Clone(); + } + else + { + switchRelations = new SwitchRelations(); + } + foreach (XmlAttribute attribute in node.Attributes) + { + // do case-insensitive comparison + switch (attribute.Name.ToUpperInvariant()) + { + case nameProperty: + switchRelations.SwitchValue = attribute.InnerText; + break; + case status: + switchRelations.Status = attribute.InnerText; + break; + default: + //LogError("InvalidAttribute", attribute.Name); + break; + } + } + return switchRelations; + } + + /// + /// Increases the error count by 1, and logs the error message + /// + /// + /// + private void LogError(string messageResourceName, params object[] messageArgs) + { + _errorLog.AddLast(ResourceUtilities.FormatResourceString(messageResourceName, messageArgs)); + _errorCount++; + } + + /// + /// An XML document can have many root nodes, but usually we want the single root + /// element. Callers can test each root node in turn with this method, until it returns + /// true. + /// + /// Candidate root node + /// true if node is the root element + private static bool IsXmlRootElement(XmlNode node) + { + // "A Document node can have the following child node types: XmlDeclaration, + // Element (maximum of one), ProcessingInstruction, Comment, and DocumentType." + return ( + (node.NodeType != XmlNodeType.Comment) && + (node.NodeType != XmlNodeType.Whitespace) && + (node.NodeType != XmlNodeType.XmlDeclaration) && + (node.NodeType != XmlNodeType.ProcessingInstruction) && + (node.NodeType != XmlNodeType.DocumentType) + ); + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/TaskGenerator.cs b/src/XMakeTasks/XamlTaskFactory/TaskGenerator.cs new file mode 100644 index 00000000000..b77c56add99 --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/TaskGenerator.cs @@ -0,0 +1,1374 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.IO; +using System.Globalization; +using System.CodeDom; +using System.CodeDom.Compiler; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using Microsoft.Build.Framework; +using System.Configuration; +using System.Resources; +using System.Reflection; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// The TaskGenerator class creates code for the specified file + /// + internal class TaskGenerator + { + #region Private const strings + + // --------------------------------------------- + // private static strings used in code for types + // --------------------------------------------- + + /// + /// The base class for the task + /// + private const string BaseClass = "DataDrivenToolTask"; + + /// + /// The namespace for the task. + /// + private const string NamespaceOfGeneratedTask = "MyDataDrivenTasks"; + + /// + /// The property for the tool name. + /// + private const string ToolNamePropertyName = "ToolName"; + + /// + /// The property for the tool exe. + /// + private const string ToolExePropertyName = "ToolExe"; + + /// + /// The field for the tool exe. + /// + private const string ToolExeFieldName = "toolExe"; + + /// + /// IsOn + /// + private const string IsOn = "true"; + + /// + /// IsOff + /// + private const string IsOff = "false"; + + /// + /// AlwaysType + /// + private const string AlwaysType = "always"; + + /// + /// The value attribute. + /// + private const string ValueAttribute = "value"; + + // -------------------- + // ToolSwitchType types + // -------------------- + + /// + /// The always type + /// + private const string TypeAlways = "always"; + + /// + /// The boolean type + /// + private const string TypeBoolean = "Boolean"; + + /// + /// The integer type + /// + private const string TypeInteger = "Integer"; + + /// + /// The string type + /// + private const string TypeString = "String"; + + /// + /// The string array type + /// + private const string TypeStringArray = "StringArray"; + + /// + /// The file type + /// + private const string TypeFile = "File"; + + /// + /// The directory type + /// + private const string TypeDirectory = "Directory"; + + /// + /// The ITaskItem type + /// + private const string TypeITaskItem = "ITaskItem"; + + /// + /// The ITaskItemArray type + /// + private const string TypeITaskItemArray = "ITaskItemArray"; + + /// + /// The KeyValue pair type. + /// + private const string TypeKeyValuePairStrings = "KeyValuePair"; + + // ----------- + // Other types + // ----------- + + /// + /// The import type. + /// + private const string ImportType = "import"; + + /// + /// The ToolSwitch. + /// + private const string TypeToolSwitch = "CommandLineToolSwitch"; + + /// + /// The ToolSwitch type. + /// + private const string TypeToolSwitchType = "CommandLineToolSwitchType"; + + /// + /// The AlwaysAppend type. + /// + private const string TypeAlwaysAppend = "AlwaysAppend"; + + /// + /// The ArgumentRelation type. + /// + private const string TypeArgumentRelation = "CommandLineArgumentRelation"; + + // ---------------- + // Common variables + // ---------------- + + /// + /// The switchToAdd field. + /// + private const string SwitchToAdd = "switchToAdd"; + + /// + /// The ActiveToolSwitches property. + /// + private const string DictionaryOfSwitches = "ActiveToolSwitches"; + + /// + /// The ActiveToolSwitchesValues property. + /// + private const string DictionaryOfSwitchesValues = "ActiveToolSwitchesValues"; + + /// + /// The switchMap field. + /// + private const string SwitchMap = "switchMap"; + + /// + /// The MultiValues property. + /// + private const string MultiValues = "AllowMultipleValues"; + + /// + /// The relation field. + /// + private const string Relation = "relation"; + + // -------------- + // Common methods + // -------------- + + /// + /// The Add method. + /// + private const string AddMethod = "Add"; + + /// + /// The AddLast method. + /// + private const string AddLastMethod = "AddLast"; + + /// + /// The AlwaysAppend method. + /// + private const string AppendAlwaysMethod = "AlwaysAppend"; + + /// + /// The ValidateInteger method. + /// + private const string ValidateIntegerMethod = "ValidateInteger"; + + /// + /// The ReadSwitchMap method. + /// + private const string ReadSwitchMapMethod = "ReadSwitchMap2"; + + /// + /// The Remove method. + /// + private const string RemoveMethod = "Remove"; + + /// + /// The IsPropertySet method. + /// + private const string IsPropertySetMethod = "IsPropertySet"; + + /// + /// The IsSwitchValueSet method. + /// + private const string IsSwitchValueSetMethod = "IsSwitchValueSet"; + + /// + /// The EnsureTrailingSlash method. + /// + private const string EnsureTrailingSlashMethod = "EnsureTrailingSlash"; + + /// + /// The AddDefaultsToActiveSwitchList method. + /// + private const string AddDefaultsToActiveSwitchList = "AddDefaultsToActiveSwitchList"; + + /// + /// The AddFallbacksToActiveSwitchList method. + /// + private const string AddFallbacksToActiveSwitchList = "AddFallbacksToActiveSwitchList"; + + /// + /// The ValidateRelations method. + /// + private const string ValidateRelationsMethod = "ValidateRelations"; + + /// + /// The ReplaceToolSwitch method. + /// + private const string ReplaceToolSwitchMethod = "ReplaceToolSwitch"; + + /// + /// The AddActiveSwitchToolValue method. + /// + private const string AddActiveSwitchToolValueMethod = "AddActiveSwitchToolValue"; + + /// + /// The Overrides method. + /// + private const string Overrides = "Overrides"; + + // ------------------------ + // properties of ToolSwitch + // ------------------------ + + /// + /// The Name property + /// + private const string NameProperty = "Name"; + + /// + /// The BooleanValue property + /// + private const string BooleanValueProperty = "BooleanValue"; + + /// + /// The FileName property + /// + private const string FileNameProperty = "Value"; + + /// + /// The TaskItem property + /// + private const string TaskItemProperty = "TaskItem"; + + /// + /// The TaskItemArray property + /// + private const string TaskItemArrayProperty = "TaskItemArray"; + + /// + /// The StringList property + /// + private const string StringListProperty = "StringList"; + + /// + /// The Number property + /// + private const string NumberProperty = "Number"; + + /// + /// The FalseSuffix property + /// + private const string FalseSuffixProperty = "FalseSuffix"; + + /// + /// The TrueSuffix property + /// + private const string TrueSuffixProperty = "TrueSuffix"; + + /// + /// The Separator property + /// + private const string SeparatorProperty = "Separator"; + + /// + /// The FallbackArgumentParameter property + /// + private const string FallbackProperty = "FallbackArgumentParameter"; + + /// + /// The Output property + /// + private const string OutputProperty = "Output"; + + /// + /// The ArgumentParameter property + /// + private const string ArgumentProperty = "ArgumentParameter"; + + /// + /// The ArgumentRequired property + /// + private const string ArgumentRequiredProperty = "ArgumentRequired"; + + /// + /// The Required property + /// + private const string PropertyRequiredProperty = "Required"; + + /// + /// The Parents property + /// + private const string ParentProperty = "Parents"; + + /// + /// The Reversible property + /// + private const string ReversibleProperty = "Reversible"; + + /// + /// The SwitchValue property + /// + private const string SwitchValueProperty = "SwitchValue"; + + /// + /// The Value property + /// + private const string ValueProperty = "Value"; + + /// + /// The Required property + /// + private const string RequiredProperty = "Required"; + + /// + /// The ArgumentRelationList property + /// + private const string ArgumentRelationList = "ArgumentRelationList"; + + /// + /// The DisplayName property + /// + private const string DisplayNameProperty = "DisplayName"; + + /// + /// The Description property + /// + private const string DescriptionProperty = "Description"; + + /// + /// The ReverseSwitchValue property + /// + private const string ReverseSwitchValueProperty = "ReverseSwitchValue"; + + /// + /// The IsValid property + /// + private const string IsValidProperty = "IsValid"; + + /// + /// The Type property + /// + private const string TypeProperty = "Type"; + + /// + /// Types to ignore. + /// + private string[] _propertiesTypesToIgnore = { "AdditionalOptions", "CommandLineTemplate" }; + + #endregion + + /// + /// The current platform. + /// + private string _platform = String.Empty; + + /// + /// The number of errors that occurred while parsing the xml file or generating the code + /// + private int _errorCount; + + /// + /// The errors that occurred while parsing the xml file or generating the code + /// + private LinkedList _errorLog = new LinkedList(); + + /// + /// The xml parsers + /// + private TaskParser _taskParser = new TaskParser(); + + /// + /// The relations parser + /// + private RelationsParser _relationsParser = new RelationsParser(); + + #region Constructor + + /// + /// The default constructor + /// + public TaskGenerator() + { + // do nothing + } + + /// + /// When set to true, the generated code will include comments. + /// + public bool GenerateComments + { + get; + set; + } + + /// + /// Constructor that takes a parser + /// + internal TaskGenerator(TaskParser parser) + { + _taskParser = parser; + } + + #endregion + + /// + /// The platform + /// + private string Platform + { + get + { + return _platform; + } + } + + #region Generate code methods + + /// + /// Removes properties that have types we are ignoring. + /// + internal void RemovePropertiesWithIgnoredTypes(LinkedList propertyList) + { + LinkedList propertyToIgnoreList = new LinkedList(); + foreach (Property property in propertyList) + { + foreach (string propertyToIgnore in _propertiesTypesToIgnore) + { + if (String.Equals(property.Name, propertyToIgnore, StringComparison.OrdinalIgnoreCase)) + { + propertyToIgnoreList.AddFirst(property); + } + } + } + + foreach (Property property in propertyToIgnoreList) + { + propertyList.Remove(property); + } + } + + /// + /// Generates the source code for the task in the specified file + /// + internal CodeCompileUnit GenerateCode() + { + try + { + // set up the class namespace + CodeCompileUnit compileUnit = new CodeCompileUnit(); + CodeNamespace dataDrivenToolTaskNamespace = new CodeNamespace(_taskParser.Namespace); + CodeTypeDeclaration taskClass = new CodeTypeDeclaration(_taskParser.GeneratedTaskName); + + if (GenerateComments) + { + // add comments to the class + taskClass.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + string commentContent = ResourceUtilities.FormatResourceString("ClassDescription", _taskParser.GeneratedTaskName); + taskClass.Comments.Add(new CodeCommentStatement(commentContent, true)); + taskClass.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + + // set up the class attributes + taskClass.IsClass = true; + taskClass.IsPartial = true; + taskClass.BaseTypes.Add(new CodeTypeReference("XamlDataDrivenToolTask")); + dataDrivenToolTaskNamespace.Types.Add(taskClass); + compileUnit.Namespaces.Add(dataDrivenToolTaskNamespace); + + RemovePropertiesWithIgnoredTypes(_taskParser.Properties); + + // generate the using statements + GenerateImports(dataDrivenToolTaskNamespace); + + // generate the constructor for this class + GenerateConstructor(taskClass); + + // generate the property for ToolName + GenerateToolNameProperty(taskClass); + + // generate all of the properties + GenerateProperties(taskClass, _taskParser.Properties); + + // generate the method to set all of the properties that have default values + GenerateDefaultSetProperties(taskClass); + + // generate the method to set all of the fallback properties in case the main property is not set + GenerateFallbacks(taskClass); + + GenerateRelations(taskClass); + + return compileUnit; + } + catch (ConfigurationException e) + { + LogError("InvalidLanguage", e.Message); + } + + return null; + } + + /// + /// Generates a method called "AddDefaultsToActiveSwitchList" that takes all of the properties that have + /// default values and adds them to the active switch list + /// + private void GenerateDefaultSetProperties(CodeTypeDeclaration taskClass) + { + if (_taskParser.DefaultSet.Count > 0) + { + CodeMemberMethod addToActiveSwitchList = new CodeMemberMethod(); + addToActiveSwitchList.Name = AddDefaultsToActiveSwitchList; + addToActiveSwitchList.Attributes = MemberAttributes.Family | MemberAttributes.Override; + foreach (Property Property in _taskParser.DefaultSet) + { + CodeConditionStatement removeExisting = new CodeConditionStatement(); + removeExisting.Condition = new CodeBinaryOperatorExpression(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), IsPropertySetMethod), new CodeSnippetExpression(SurroundWithQuotes(Property.Name))), CodeBinaryOperatorType.IdentityEquality, new CodeSnippetExpression("false")); + if (Property.Type == PropertyType.Boolean) + { + removeExisting.TrueStatements.Add(new CodeAssignStatement(new CodeVariableReferenceExpression(Property.Name), new CodeSnippetExpression(Property.DefaultValue))); + } + else + { + removeExisting.TrueStatements.Add(new CodeAssignStatement(new CodeVariableReferenceExpression(Property.Name), new CodeSnippetExpression(SurroundWithQuotes(Property.DefaultValue)))); + } + addToActiveSwitchList.Statements.Add(removeExisting); + } + taskClass.Members.Add(addToActiveSwitchList); + + if (GenerateComments) + { + // comments + addToActiveSwitchList.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + string commentContent = ResourceUtilities.FormatResourceString("AddDefaultsToActiveSwitchListDescription"); + addToActiveSwitchList.Comments.Add(new CodeCommentStatement(commentContent, true)); + addToActiveSwitchList.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + } + } + + /// + /// Generates a method called "AddFallbacksToActiveSwitchList" that takes all of the properties that + /// are not set but have fallbacks and adds the fallbacks to the active list if they are set. + /// + private void GenerateFallbacks(CodeTypeDeclaration taskClass) + { + if (_taskParser.FallbackSet.Count > 0) + { + CodeMemberMethod addToActiveSwitchList = new CodeMemberMethod(); + addToActiveSwitchList.Name = AddFallbacksToActiveSwitchList; + addToActiveSwitchList.Attributes = MemberAttributes.Family | MemberAttributes.Override; + foreach (KeyValuePair fallbackParameter in _taskParser.FallbackSet) + { + CodeConditionStatement removeExisting = new CodeConditionStatement(); + CodeMethodInvokeExpression isPropertySet = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), IsPropertySetMethod, new CodeSnippetExpression(SurroundWithQuotes(fallbackParameter.Value))); + CodeBinaryOperatorExpression propertyNotSet = new CodeBinaryOperatorExpression(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), IsPropertySetMethod, new CodeSnippetExpression(SurroundWithQuotes(fallbackParameter.Key))), CodeBinaryOperatorType.ValueEquality, new CodeSnippetExpression(IsOff)); + removeExisting.Condition = new CodeBinaryOperatorExpression(propertyNotSet, CodeBinaryOperatorType.BooleanAnd, isPropertySet); + removeExisting.TrueStatements.Add(new CodeAssignStatement(new CodeVariableReferenceExpression(fallbackParameter.Key), new CodeVariableReferenceExpression(fallbackParameter.Value))); + addToActiveSwitchList.Statements.Add(removeExisting); + } + taskClass.Members.Add(addToActiveSwitchList); + + if (GenerateComments) + { + // comments + addToActiveSwitchList.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + string commentContent = ResourceUtilities.FormatResourceString("AddFallbacksToActiveSwitchListDescription"); + addToActiveSwitchList.Comments.Add(new CodeCommentStatement(commentContent, true)); + addToActiveSwitchList.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + } + } + + /// + /// Generates code for the different properties in a task + /// + private void GenerateProperties(CodeTypeDeclaration taskClass, LinkedList propertyList) + { + foreach (Property property in propertyList) + { + if (!String.Equals(property.Name, ImportType, StringComparison.OrdinalIgnoreCase)) + { + if (!ContainsCurrentPlatform(property)) + continue; + + CodeAttributeDeclarationCollection collection = new CodeAttributeDeclarationCollection(); + CodeMemberProperty propertyName = new CodeMemberProperty(); + propertyName.Name = property.Name; + propertyName.HasGet = true; + propertyName.HasSet = true; + propertyName.Attributes = MemberAttributes.Public; + + // check to see if the property has a default value set + if (!String.IsNullOrEmpty(property.DefaultValue)) + { + _taskParser.DefaultSet.AddLast(property); + } + + // check to see whether it is required, whether it is an output, etc. + if (!String.IsNullOrEmpty(property.Required) && property.Required == IsOn) + { + collection.Add(new CodeAttributeDeclaration(PropertyRequiredProperty)); + } + if (property.Output) + { + collection.Add(new CodeAttributeDeclaration(OutputProperty)); + } + if (String.IsNullOrEmpty(property.Argument) && !String.IsNullOrEmpty(property.Fallback)) + { + _taskParser.FallbackSet.Add(property.Name, property.Fallback); + } + if (property.Type == PropertyType.StringArray) + { + GenerateStringArrays(property, propertyName); + } + else if (property.Type == PropertyType.String) + { + GenerateStrings(property, propertyName); + } + else if (property.Type == PropertyType.Boolean) + { + GenerateBooleans(property, propertyName); + } + else if (property.Type == PropertyType.Integer) + { + GenerateIntegers(property, propertyName); + } + else if (property.Type == PropertyType.ItemArray) + { + GenerateITaskItemArray(property, propertyName); + } + else + { + LogError("ImproperType", property.Name, property.Type); + } + + // also assign a parent for each one + foreach (Property dependentProperty in property.DependentArgumentProperties) + { + // Does not exist already add it to the list of parents + if (!dependentProperty.Parents.Contains(property.Name)) + { + dependentProperty.Parents.AddLast(property.Name); + } + } + + GenerateOverrides(property, propertyName); + + propertyName.CustomAttributes = collection; + taskClass.Members.Add(propertyName); + } + } + } + + /// + /// Generates an assignment statment for the setters of properties, where the rhs is a string + /// e.g., switchToAdd.Name = "Optimizations"; + /// + private void GenerateAssignPropertyToString(CodeMemberProperty propertyName, string property, string value) + { + if (!String.IsNullOrEmpty(value)) + { + CodeAssignStatement setStatement = new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), property), new CodeSnippetExpression(SurroundWithQuotes(value))); + propertyName.SetStatements.Add(setStatement); + } + } + + /// + /// Generates an assignment statment for the setters of properties, where the rhs is an expression + /// e.g., switchToAdd.ArgumentRequired = true; + /// + private void GenerateAssignPropertyToValue(CodeMemberProperty propertyName, string property, CodeExpression value) + { + ErrorUtilities.VerifyThrow(value != null, "NullValue", property); + CodeAssignStatement setStatement = new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), property), value); + propertyName.SetStatements.Add(setStatement); + } + + /// + /// Generates an assignment for the toolswitch, with a prefix included + /// i.e., switchToAdd.ToolSwitchName = "/Ox"; + /// + private void GenerateAssignToolSwitch(CodeMemberProperty propertyName, string property, string prefix, string toolSwitchName) + { + if (!String.IsNullOrEmpty(toolSwitchName)) + { + GenerateAssignPropertyToString(propertyName, property, prefix + toolSwitchName); + } + } + + /// + /// This method generates all of the common cases between different property types. + /// The common cases are: + /// 1) A new ToolSwitch object has to be created for each property + /// 2) The newly created ToolSwitch has to be added to the ActiveToolSwitches list + /// 4) For all non-empty common attributes that don't need customization, set the property + /// These would be: + /// name, type, separator, argument, argumentRequired, fallback, dependencies + /// + /// The property + /// The CodeDom property + /// The type of the property + /// The return type of the property + /// The lhs of the assignment statement lhs = value + private void GenerateCommon(Property property, CodeMemberProperty propertyName, string type, Type returnType, string valueName) + { + // get statements + propertyName.Type = new CodeTypeReference(returnType); + CodeConditionStatement isSet = new CodeConditionStatement(); + isSet.Condition = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "IsPropertySet", new CodeExpression[] { new CodeSnippetExpression(SurroundWithQuotes(property.Name)) }); + isSet.TrueStatements.Add(new CodeMethodReturnStatement(new CodePropertyReferenceExpression(new CodeArrayIndexerExpression(new CodeVariableReferenceExpression(DictionaryOfSwitches), new CodeVariableReferenceExpression(SurroundWithQuotes(property.Name))), valueName))); + if (property.Type == PropertyType.Boolean) + { + isSet.FalseStatements.Add(new CodeMethodReturnStatement(new CodeSnippetExpression(IsOff))); + } + else if (property.Type == PropertyType.Integer) + { + isSet.FalseStatements.Add(new CodeMethodReturnStatement(new CodePrimitiveExpression(0))); + } + else + { + isSet.FalseStatements.Add(new CodeMethodReturnStatement(new CodeSnippetExpression("null"))); + } + propertyName.GetStatements.Add(isSet); + + // set statements + CodeVariableDeclarationStatement createNewToolSwitch = new CodeVariableDeclarationStatement(new CodeTypeReference(TypeToolSwitch), SwitchToAdd, new CodeObjectCreateExpression(TypeToolSwitch, new CodeExpression[] { new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(TypeToolSwitchType), type) })); + propertyName.SetStatements.Add(createNewToolSwitch); + if (!String.IsNullOrEmpty(property.Reversible) && String.Equals(property.Reversible, IsOn, StringComparison.OrdinalIgnoreCase)) + { + GenerateAssignPropertyToValue(propertyName, ReversibleProperty, new CodeSnippetExpression(property.Reversible)); + } + + GenerateAssignPropertyToString(propertyName, ArgumentProperty, property.Argument); + GenerateAssignPropertyToString(propertyName, SeparatorProperty, property.Separator); + GenerateAssignPropertyToString(propertyName, DisplayNameProperty, property.DisplayName); + GenerateAssignPropertyToString(propertyName, DescriptionProperty, property.Description); + if (!String.IsNullOrEmpty(property.Required) && String.Equals(property.Required, IsOn, StringComparison.OrdinalIgnoreCase)) + GenerateAssignPropertyToValue(propertyName, RequiredProperty, new CodeSnippetExpression(property.Required)); + GenerateAssignPropertyToString(propertyName, FallbackProperty, property.Fallback); + GenerateAssignPropertyToString(propertyName, FalseSuffixProperty, property.FalseSuffix); + GenerateAssignPropertyToString(propertyName, TrueSuffixProperty, property.TrueSuffix); + if (property.Parents.Count > 0) + { + foreach (string parentName in property.Parents) + { + CodeMethodInvokeExpression setParent = new CodeMethodInvokeExpression(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), ParentProperty), AddLastMethod, new CodeSnippetExpression(SurroundWithQuotes(parentName))); + propertyName.SetStatements.Add(setParent); + } + } + + if (property.IncludeInCommandLine) + { + CodeAssignStatement setInclude = new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), "IncludeInCommandLine"), new CodePrimitiveExpression(true)); + propertyName.SetStatements.Add(setInclude); + } + + if (GenerateComments) + { + // comments + propertyName.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + string commentContent = ResourceUtilities.FormatResourceString("PropertyNameDescription", property.Name); + propertyName.Comments.Add(new CodeCommentStatement(commentContent, true)); + commentContent = ResourceUtilities.FormatResourceString("PropertyTypeDescription", type); + propertyName.Comments.Add(new CodeCommentStatement(commentContent, true)); + commentContent = ResourceUtilities.FormatResourceString("PropertySwitchDescription", property.SwitchName); + propertyName.Comments.Add(new CodeCommentStatement(commentContent, true)); + propertyName.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + } + + /// + /// Generates standart set statements for properties. + /// + private void GenerateCommonSetStatements(Property property, CodeMemberProperty propertyName, string referencedProperty) + { + if (referencedProperty != null) + { + CodeAssignStatement setValue = new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), referencedProperty), new CodePropertySetValueReferenceExpression()); + propertyName.SetStatements.Add(setValue); + } + + propertyName.SetStatements.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), ReplaceToolSwitchMethod, new CodeExpression[] { new CodeSnippetExpression(SwitchToAdd) })); + } + + /// + /// Generates an ITaskItem array property type. + /// + private void GenerateITaskItemArray(Property property, CodeMemberProperty propertyName) + { + CodeTypeReference ctr = new CodeTypeReference(); + ctr.BaseType = "ITaskItem"; + ctr.ArrayRank = 1; + GenerateCommon(property, propertyName, TypeITaskItemArray, typeof(Array), TaskItemArrayProperty); + propertyName.Type = ctr; + CodeAssignStatement setToolName = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), NameProperty), + new CodeSnippetExpression(SurroundWithQuotes(property.Name))); + propertyName.SetStatements.Add(setToolName); + + GenerateAssignToolSwitch(propertyName, SwitchValueProperty, property.Prefix, property.SwitchName); + GenerateCommonSetStatements(property, propertyName, TaskItemArrayProperty); + } + + /// + /// This method generates all of the switches for integer typed properties. + /// + private void GenerateIntegers(Property property, CodeMemberProperty propertyName) + { + // set statments + GenerateCommon(property, propertyName, TypeInteger, typeof(Int32), NumberProperty); + + // if a min or max exists, check those boundaries + CodeExpression[] parameters; + string name = property.SwitchName != String.Empty ? property.Prefix + property.SwitchName : property.Name; + if (!String.IsNullOrEmpty(property.Min) && !String.IsNullOrEmpty(property.Max)) + { + parameters = new CodeExpression[] { new CodeSnippetExpression(SurroundWithQuotes(name)), new CodePrimitiveExpression(Int32.Parse(property.Min, CultureInfo.CurrentCulture)), new CodePrimitiveExpression(Int32.Parse(property.Max, CultureInfo.CurrentCulture)), new CodePropertySetValueReferenceExpression() }; + } + else if (!String.IsNullOrEmpty(property.Min)) + { + parameters = new CodeExpression[] { new CodeSnippetExpression(SurroundWithQuotes(name)), new CodePrimitiveExpression(Int32.Parse(property.Min, CultureInfo.CurrentCulture)), new CodeSnippetExpression("Int32.MaxValue"), new CodePropertySetValueReferenceExpression() }; + } + else if (!String.IsNullOrEmpty(property.Max)) + { + parameters = new CodeExpression[] { new CodeSnippetExpression(SurroundWithQuotes(name)), new CodeSnippetExpression("Int32.MinValue"), new CodePrimitiveExpression(Int32.Parse(property.Max, CultureInfo.CurrentCulture)), new CodePropertySetValueReferenceExpression() }; + } + else + { + parameters = new CodeExpression[] { new CodeSnippetExpression(SurroundWithQuotes(name)), new CodeSnippetExpression("Int32.MinValue"), new CodeSnippetExpression("Int32.MaxValue"), new CodePropertySetValueReferenceExpression() }; + } + + CodeMethodReferenceExpression validateInt = new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), ValidateIntegerMethod); + + CodeConditionStatement isValid = new CodeConditionStatement(); + isValid.Condition = new CodeMethodInvokeExpression(validateInt, parameters); + isValid.TrueStatements.Add(new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), IsValidProperty), new CodeSnippetExpression(IsOn))); + isValid.FalseStatements.Add(new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), IsValidProperty), new CodeSnippetExpression(IsOff))); + propertyName.SetStatements.Add(isValid); + + CodeAssignStatement setToolName = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), NameProperty), + new CodeSnippetExpression(SurroundWithQuotes(property.Name))); + propertyName.SetStatements.Add(setToolName); + + GenerateAssignToolSwitch(propertyName, SwitchValueProperty, property.Prefix, property.SwitchName); + GenerateCommonSetStatements(property, propertyName, NumberProperty); + } + + /// + /// This method generates the switches for all of the nonreversible properties. + /// + private void GenerateBooleans(Property property, CodeMemberProperty propertyName) + { + // set statments + GenerateCommon(property, propertyName, TypeBoolean, typeof(Boolean), BooleanValueProperty); + GenerateAssignToolSwitch(propertyName, SwitchValueProperty, property.Prefix, property.SwitchName); + GenerateAssignToolSwitch(propertyName, ReverseSwitchValueProperty, property.Prefix, property.ReverseSwitchName); + + CodeAssignStatement setToolName = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), NameProperty), + new CodeSnippetExpression(SurroundWithQuotes(property.Name))); + propertyName.SetStatements.Add(setToolName); + + GenerateCommonSetStatements(property, propertyName, BooleanValueProperty); + } + + /// + /// This method generates all of the switches for the string type property. + /// + private void GenerateStrings(Property property, CodeMemberProperty propertyName) + { + GenerateCommon(property, propertyName, TypeString, typeof(string), FileNameProperty); + string propertyToReceiveValue = null; + + // if there are no enums, the value is the fileName, otherwise, the value is the enum's name + if (property.Values.Count > 0) + { + CodeVariableDeclarationStatement createArray = new CodeVariableDeclarationStatement("Tuple[]>[]", SwitchMap); + List codeExpressions = new List(); + + CodeTypeReference temporaryArrayType = new CodeTypeReference(typeof(string)); + foreach (Value val in property.Values) + { + if (ContainsCurrentPlatform(val.SwitchName)) + { + // Create the array of argument expressions. + List argumentInitializers = new List(val.Arguments.Count); + foreach (Argument arg in val.Arguments) + { + argumentInitializers.Add(new CodeObjectCreateExpression(new CodeTypeReference("Tuple"), + new CodeSnippetExpression(SurroundWithQuotes(arg.Parameter)), + new CodePrimitiveExpression(arg.Required))); + } + + // Now create the entry for the switch itself. + CodeObjectCreateExpression valueExpression = new CodeObjectCreateExpression(new CodeTypeReference("Tuple[]>"), + new CodeSnippetExpression(SurroundWithQuotes(val.Name)), + val.SwitchName != String.Empty ? new CodeSnippetExpression(SurroundWithQuotes(val.Prefix + val.SwitchName)) : new CodeSnippetExpression(SurroundWithQuotes("")), + new CodeArrayCreateExpression(new CodeTypeReference("Tuple"), argumentInitializers.ToArray())); + + codeExpressions.Add(valueExpression); + } + } + + // Initialize the switch array + CodeArrayCreateExpression initializeArray = new CodeArrayCreateExpression("Tuple[]>[]", codeExpressions.ToArray()); + createArray.InitExpression = initializeArray; + propertyName.SetStatements.Add(createArray); + + // Create an index variable to hold the entry in the array we matched + CodeVariableDeclarationStatement indexDecl = new CodeVariableDeclarationStatement(typeof(int), "i", new CodeMethodInvokeExpression( + new CodeThisReferenceExpression(), ReadSwitchMapMethod, + new CodeExpression[] { new CodeSnippetExpression(SurroundWithQuotes(property.Name)), + new CodeVariableReferenceExpression(SwitchMap), + new CodeVariableReferenceExpression(ValueAttribute) })); + propertyName.SetStatements.Add(indexDecl); + + + // Set the switch value from the index into the array + CodeAssignStatement setToolSwitchNameGoodIndex = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), SwitchValueProperty), + new CodePropertyReferenceExpression(new CodeArrayIndexerExpression(new CodeVariableReferenceExpression("switchMap"), new CodeVariableReferenceExpression("i")), "Item2")); + + // Set the arguments + CodeAssignStatement setArgumentsGoodIndex = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), "Arguments"), + new CodePropertyReferenceExpression(new CodeArrayIndexerExpression(new CodeVariableReferenceExpression("switchMap"), new CodeVariableReferenceExpression("i")), "Item3")); + + // Set the switch value from the index into the array + CodeAssignStatement setToolSwitchNameBadIndex = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), SwitchValueProperty), + new CodePrimitiveExpression(String.Empty)); + + // Set the arguments + CodeAssignStatement setArgumentsBadIndex = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), "Arguments"), + new CodePrimitiveExpression(null)); + + // Create a CodeConditionStatement that tests a boolean value named boolean. + CodeConditionStatement conditionalStatement = new CodeConditionStatement( + // The condition to test. + new CodeVariableReferenceExpression("i >= 0"), + // The statements to execute if the condition evaluates to true. + new CodeStatement[] { setToolSwitchNameGoodIndex, setArgumentsGoodIndex }, + // The statements to execute if the condition evalues to false. + new CodeStatement[] { setToolSwitchNameBadIndex, setArgumentsBadIndex }); + + propertyName.SetStatements.Add(conditionalStatement); + // Set the separator + CodeAssignStatement setSeparator = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), "Separator"), + new CodeSnippetExpression(SurroundWithQuotes(property.Separator))); + propertyName.SetStatements.Add(setSeparator); + + // Set the tool name + CodeAssignStatement setToolName = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), NameProperty), + new CodeSnippetExpression(SurroundWithQuotes(property.Name))); + propertyName.SetStatements.Add(setToolName); + + propertyToReceiveValue = ValueProperty; + GenerateAssignPropertyToValue(propertyName, MultiValues, new CodeSnippetExpression(IsOn)); + } + else + { + CodeAssignStatement setToolName = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), NameProperty), + new CodeSnippetExpression(SurroundWithQuotes(property.Name))); + propertyName.SetStatements.Add(setToolName); + + propertyToReceiveValue = FileNameProperty; + CodeAssignStatement setToolSwitchName = new CodeAssignStatement(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(SwitchToAdd), SwitchValueProperty), property.SwitchName != String.Empty ? new CodeSnippetExpression(SurroundWithQuotes(property.Prefix + property.SwitchName)) : new CodeSnippetExpression(SurroundWithQuotes(""))); + propertyName.SetStatements.Add(setToolSwitchName); + GenerateAssignToolSwitch(propertyName, ReverseSwitchValueProperty, property.Prefix, property.ReverseSwitchName); + } + + GenerateCommonSetStatements(property, propertyName, propertyToReceiveValue); + } + + /// + /// Returns true if the property refers to the current platform. + /// + /// + /// + private bool ContainsCurrentPlatform(Property property) + { + if (Platform == null) + return true; + + if (property.Values.Count > 0) + { + bool containsCurrentPlatform = false; + foreach (Value val in property.Values) + { + containsCurrentPlatform = ContainsCurrentPlatform(val.SwitchName) ? true : containsCurrentPlatform; + } + return containsCurrentPlatform; + } + else + { + return ContainsCurrentPlatform(property.SwitchName); + } + } + + /// + /// Returns true if the switch value refers to the current platform. + /// + private bool ContainsCurrentPlatform(string SwitchValue) + { + // If we don't have a platform defined it meens all + if (Platform == null) + return true; + + if (_relationsParser.SwitchRelationsList.ContainsKey(SwitchValue)) + { + SwitchRelations rel = _relationsParser.SwitchRelationsList[SwitchValue]; + if (rel.ExcludedPlatforms.Count > 0) + { + foreach (string excludedPlatform in rel.ExcludedPlatforms) + { + if (Platform == excludedPlatform) + return false; + } + } + if (rel.IncludedPlatforms.Count > 0) + { + bool isIncluded = false; + foreach (string includedPlatform in rel.IncludedPlatforms) + { + if (Platform == includedPlatform) + isIncluded = true; + } + return isIncluded; + } + } + return true; + } + + /// + /// This method generates overrides array + /// + private void GenerateOverrides(Property property, CodeMemberProperty propertyName) + { + if (_relationsParser.SwitchRelationsList.ContainsKey(property.SwitchName)) + { + SwitchRelations rel = _relationsParser.SwitchRelationsList[property.SwitchName]; + if (rel.Overrides.Count > 0) + { + foreach (string overrided in rel.Overrides) + { + propertyName.SetStatements.Add(new CodeMethodInvokeExpression( + new CodeFieldReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), Overrides), AddLastMethod, + new CodeExpression[] { new CodeObjectCreateExpression( + new CodeTypeReference(TypeKeyValuePairStrings), new CodeExpression[] { + new CodeSnippetExpression(SurroundWithQuotes(rel.SwitchValue)), + new CodeSnippetExpression(SurroundWithQuotes(overrided))})})); + } + } + + if (property.ReverseSwitchName != "") + { + rel = _relationsParser.SwitchRelationsList[property.ReverseSwitchName]; + if (rel.Overrides.Count > 0) + { + foreach (string overrided in rel.Overrides) + { + propertyName.SetStatements.Add(new CodeMethodInvokeExpression( + new CodeFieldReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), Overrides), AddLastMethod, + new CodeExpression[] { new CodeObjectCreateExpression( + new CodeTypeReference(TypeKeyValuePairStrings), new CodeExpression[] { + new CodeSnippetExpression(SurroundWithQuotes(rel.SwitchValue)) , + new CodeSnippetExpression(SurroundWithQuotes(overrided))})})); + } + } + } + } + } + + /// + /// This method generates switches for all the properties that are of type + /// string array + /// + private void GenerateStringArrays(Property property, CodeMemberProperty propertyName) + { + CodeTypeReference ctr = new CodeTypeReference(); + ctr.BaseType = "System.String"; + ctr.ArrayRank = 1; + GenerateCommon(property, propertyName, TypeStringArray, typeof(Array), StringListProperty); + propertyName.Type = ctr; + GenerateAssignToolSwitch(propertyName, SwitchValueProperty, property.Prefix, property.SwitchName); + CodeAssignStatement setToolName = new CodeAssignStatement( + new CodePropertyReferenceExpression( + new CodeVariableReferenceExpression(SwitchToAdd), NameProperty), + new CodeSnippetExpression(SurroundWithQuotes(property.Name))); + propertyName.SetStatements.Add(setToolName); + GenerateCommonSetStatements(property, propertyName, StringListProperty); + } + + /// + /// This method generates the property that returns the tool exe value set by the ToolExe property + /// + private void GenerateToolNameProperty(CodeTypeDeclaration taskClass) + { + CodeMemberProperty toolNameAccessor = new CodeMemberProperty(); + toolNameAccessor.Name = ToolNamePropertyName; + toolNameAccessor.HasGet = true; + toolNameAccessor.HasSet = false; + toolNameAccessor.Attributes = MemberAttributes.Override | MemberAttributes.Family; + toolNameAccessor.Type = new CodeTypeReference(typeof(string)); + + string commentContent = null; + + if (GenerateComments) + { + // Comment on this property assignment + commentContent = ResourceUtilities.FormatResourceString("ToolExeFieldDescription"); + toolNameAccessor.GetStatements.Add(new CodeCommentStatement(commentContent, false)); + } + + toolNameAccessor.GetStatements.Add(new CodeMethodReturnStatement(new CodeSnippetExpression(SurroundWithQuotes(_taskParser.ToolName)))); + taskClass.Members.Add(toolNameAccessor); + + if (GenerateComments) + { + // comments + toolNameAccessor.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + commentContent = ResourceUtilities.FormatResourceString("ToolNameDescription"); + toolNameAccessor.Comments.Add(new CodeCommentStatement(commentContent, true)); + toolNameAccessor.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + } + + /// + /// This method generates the code that appears at the top of each class (that imports other libraries) + /// + private void GenerateImports(CodeNamespace codeNamespace) + { + string[] imports = new string[] + { + "System", + "System.Globalization", + "System.Collections", + "System.Collections.Generic", + "System.Diagnostics", + "System.IO", + "Microsoft.Build.Utilities", + "Microsoft.Build.Framework", + "Microsoft.Build.Tasks.Xaml", + }; + + foreach (string reference in imports) + { + codeNamespace.Imports.Add(new CodeNamespaceImport(reference)); + } + } + + /// + /// This method generates the default constructor for the generated task + /// + private void GenerateConstructor(CodeTypeDeclaration taskClass) + { + CodeConstructor defaultConstructor = new CodeConstructor(); + defaultConstructor.Attributes = MemberAttributes.Public; + + // new System.Resources.ResourceManager("Microsoft.Build.NativeTasks.Strings", System.Reflection.Assembly.GetExecutingAssembly())) + CodeTypeReference resourceManagerType = new CodeTypeReference("System.Resources.ResourceManager"); + CodeSnippetExpression resourceNamespaceString = new CodeSnippetExpression(SurroundWithQuotes(_taskParser.ResourceNamespace)); + CodeTypeReferenceExpression systemReflectionAssembly = new CodeTypeReferenceExpression("System.Reflection.Assembly"); + CodeMethodReferenceExpression getExecutingAssemblyReference = new CodeMethodReferenceExpression(systemReflectionAssembly, "GetExecutingAssembly"); + CodeMethodInvokeExpression getExecutingAssembly = new CodeMethodInvokeExpression(getExecutingAssemblyReference); + CodeObjectCreateExpression resourceManager = new CodeObjectCreateExpression(resourceManagerType, new CodeExpression[] { resourceNamespaceString, getExecutingAssembly }); + + CodeTypeReference switchOrderArrayType = new CodeTypeReference(new CodeTypeReference("System.String"), 1); + List valueExpressions = new List(); + foreach (string switchName in _taskParser.SwitchOrderList) + { + valueExpressions.Add(new CodeSnippetExpression(SurroundWithQuotes(switchName))); + } + + CodeArrayCreateExpression arrayExpression = new CodeArrayCreateExpression(switchOrderArrayType, valueExpressions.ToArray()); + defaultConstructor.BaseConstructorArgs.Add(arrayExpression); + defaultConstructor.BaseConstructorArgs.Add(resourceManager); + + taskClass.Members.Add(defaultConstructor); + + if (GenerateComments) + { + // comments + defaultConstructor.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + string commentContent = ResourceUtilities.FormatResourceString("ConstructorDescription"); + defaultConstructor.Comments.Add(new CodeCommentStatement(commentContent, true)); + defaultConstructor.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + } + + /// + /// This method generates the relations which will be used at runtime to validate the command line + /// + private void GenerateRelations(CodeTypeDeclaration taskClass) + { + if (_relationsParser.SwitchRelationsList.Count > 0) + { + CodeMemberMethod addValidateRelationsMethod = new CodeMemberMethod(); + addValidateRelationsMethod.Name = ValidateRelationsMethod; + addValidateRelationsMethod.Attributes = MemberAttributes.Family | MemberAttributes.Override; + + foreach (KeyValuePair switchRelations in _relationsParser.SwitchRelationsList) + { + if (switchRelations.Value.Requires.Count > 0) + { + CodeConditionStatement checkRequired = new CodeConditionStatement(); + + checkRequired.Condition = null; + + foreach (string required in switchRelations.Value.Requires) + { + if (checkRequired.Condition != null) + checkRequired.Condition = new CodeBinaryOperatorExpression( + checkRequired.Condition, CodeBinaryOperatorType.BooleanAnd, new CodeBinaryOperatorExpression(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), IsSwitchValueSetMethod), new CodeSnippetExpression(SurroundWithQuotes(required))), CodeBinaryOperatorType.IdentityEquality, new CodeSnippetExpression("false"))); + else + checkRequired.Condition = new CodeBinaryOperatorExpression(new CodeMethodInvokeExpression(new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), IsSwitchValueSetMethod), new CodeSnippetExpression(SurroundWithQuotes(required))), CodeBinaryOperatorType.IdentityEquality, new CodeSnippetExpression("false")); + } + + checkRequired.TrueStatements.Add(new CodeMethodInvokeExpression + (new CodeThisReferenceExpression(), "RemoveSwitchToolBasedOnValue", + new CodeExpression[]{new CodeSnippetExpression(SurroundWithQuotes(switchRelations.Key)), + })); + + addValidateRelationsMethod.Statements.Add(checkRequired); + } + } + + taskClass.Members.Add(addValidateRelationsMethod); + + if (GenerateComments) + { + // comments + addValidateRelationsMethod.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("StartSummary"), true)); + string commentContent = ResourceUtilities.FormatResourceString("AddValidateRelationsMethod"); + addValidateRelationsMethod.Comments.Add(new CodeCommentStatement(commentContent, true)); + addValidateRelationsMethod.Comments.Add(new CodeCommentStatement(ResourceUtilities.FormatResourceString("EndSummary"), true)); + } + } + } + + #endregion + + #region Miscellaneous methods + + /// + /// Increases the error count by 1, and logs the error message + /// + private void LogError(string messageResourceName, params object[] messageArgs) + { + _errorLog.AddLast(ResourceUtilities.FormatResourceString(messageResourceName, messageArgs)); + _errorCount++; + } + + /// + /// Puts a string inside two quotes + /// + private string SurroundWithQuotes(string unformattedText) + { + if (String.IsNullOrEmpty(unformattedText)) + { + return "@\"\""; + } + else + { + return "@\"" + unformattedText.Replace("\"", "\"\"") + "\""; + } + } + + #endregion + + /// + /// Returns the number of errors encountered + /// + internal int ErrorCount + { + get + { + return _errorCount; + } + } + + /// + /// Returns the log of errors + /// + internal LinkedList ErrorLog + { + get + { + return _errorLog; + } + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/TaskParser.cs b/src/XMakeTasks/XamlTaskFactory/TaskParser.cs new file mode 100644 index 00000000000..fa52da25a6c --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/TaskParser.cs @@ -0,0 +1,630 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// +// Helper class which converts Xaml rules into data structures +// suitable for command-line processing +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Xaml; +using System.Xml; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +using XamlTypes = Microsoft.Build.Framework.XamlTypes; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// The TaskParser class takes an xml file and parses the parameters for a task. + /// + internal class TaskParser + { + /// + /// The name of the task e.g., CL + /// + private string _name; + + /// + /// The name of the executable e.g., cl.exe + /// + private string _toolName; + + /// + /// The base class + /// + private string _baseClass = "DataDrivenToolTask"; + + /// + /// The namespace to generate the class into + /// + private string _namespaceValue = "XamlTaskNamespace"; + + /// + /// The resource namespace to pass to the base class, if any + /// + private string _resourceNamespaceValue = null; + + /// + /// The prefix to append before a switch is emitted. + /// Is typically a "/", but can also be a "-" + /// + private string _defaultPrefix = String.Empty; + + /// + /// The list that contains all of the properties that can be set on a task + /// + private LinkedList _properties = new LinkedList(); + + /// + /// The list that contains all of the properties that have a default value + /// + private LinkedList _defaultSet = new LinkedList(); + + /// + /// The list of properties that serve as fallbacks for other properties. + /// That is, if a certain property is not set, but has a fallback, we need to check + /// to see if that fallback is set. + /// + private Dictionary _fallbackSet = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The set of switches added so far. + /// + private HashSet _switchesAdded = new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// The ordered list of how the switches get emitted. + /// + private List _switchOrderList = new List(); + + /// + /// The errors that occurred while parsing the xml file or generating the code + /// + private LinkedList _errorLog = new LinkedList(); + + /// + /// The constructor. + /// + public TaskParser() + { + // do nothing + } + + #region Properties + + /// + /// The name of the task + /// + public string GeneratedTaskName + { + get + { + return _name; + } + + set + { + _name = value; + } + } + + /// + /// The base type of the class + /// + public string BaseClass + { + get + { + return _baseClass; + } + } + + /// + /// The namespace of the class + /// + public string Namespace + { + get + { + return _namespaceValue; + } + } + + /// + /// Namespace for the resources + /// + public string ResourceNamespace + { + get + { + return _resourceNamespaceValue; + } + } + + /// + /// The name of the executable + /// + public string ToolName + { + get + { + return _toolName; + } + } + + /// + /// The default prefix for each switch + /// + public string DefaultPrefix + { + get + { + return _defaultPrefix; + } + } + + /// + /// All of the parameters that were parsed + /// + public LinkedList Properties + { + get + { + return _properties; + } + } + + /// + /// All of the parameters that have a default value + /// + public LinkedList DefaultSet + { + get + { + return _defaultSet; + } + } + + /// + /// All of the properties that serve as fallbacks for unset properties + /// + public Dictionary FallbackSet + { + get + { + return _fallbackSet; + } + } + + /// + /// The ordered list of properties + /// + public IEnumerable SwitchOrderList + { + get + { + return _switchOrderList; + } + } + + /// + /// Returns the log of errors + /// + public LinkedList ErrorLog + { + get + { + return _errorLog; + } + } + #endregion + + /// + /// Parse the specified string, either as a file path or actual XML content. + /// + public bool Parse(string contentOrFile, string desiredRule) + { + ErrorUtilities.VerifyThrowArgumentLength(contentOrFile, "contentOrFile"); + ErrorUtilities.VerifyThrowArgumentLength(desiredRule, "desiredRule"); + + string fullPath = null; + bool parseSuccessful = false; + try + { + fullPath = Path.GetFullPath(contentOrFile); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + // We will get an exception if the contents are not a path (for instance, they are actual XML.) + } + + if (fullPath != null) + { + if (!File.Exists(fullPath)) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("Xaml.RuleFileNotFound", fullPath)); + } + + parseSuccessful = ParseXamlDocument(new StreamReader(fullPath), desiredRule); + } + else + { + parseSuccessful = ParseXamlDocument(new StringReader(contentOrFile), desiredRule); + } + + if (!parseSuccessful) + { + StringBuilder parseErrors = new StringBuilder(); + parseErrors.AppendLine(); + foreach (string error in ErrorLog) + { + parseErrors.AppendLine(error); + } + + throw new ArgumentException(ResourceUtilities.FormatResourceString("Xaml.RuleParseFailed", parseErrors.ToString())); + } + + return parseSuccessful; + } + + /// + /// Parse a Xaml document from a TextReader + /// + internal bool ParseXamlDocument(TextReader reader, string desiredRule) + { + ErrorUtilities.VerifyThrowArgumentNull(reader, "reader"); + ErrorUtilities.VerifyThrowArgumentLength(desiredRule, "desiredRule"); + + object rootObject = XamlServices.Load(reader); + if (null != rootObject) + { + XamlTypes.ProjectSchemaDefinitions schemas = rootObject as XamlTypes.ProjectSchemaDefinitions; + if (schemas != null) + { + foreach (XamlTypes.IProjectSchemaNode node in schemas.Nodes) + { + XamlTypes.Rule rule = node as XamlTypes.Rule; + if (rule != null) + { + if (String.Equals(rule.Name, desiredRule, StringComparison.OrdinalIgnoreCase)) + { + return ParseXamlDocument(rule); + } + } + } + + throw new XamlParseException(ResourceUtilities.FormatResourceString("Xaml.RuleNotFound", desiredRule)); + } + else + { + throw new XamlParseException(ResourceUtilities.FormatResourceString("Xaml.InvalidRootObject")); + } + } + + return false; + } + + /// + /// Parse a Xaml document from a rule + /// + internal bool ParseXamlDocument(XamlTypes.Rule rule) + { + if (rule == null) + { + return false; + } + + _defaultPrefix = rule.SwitchPrefix; + + _toolName = rule.ToolName; + _name = rule.Name; + + // Dictionary of property name strings to property objects. If a property is in the argument list of the current property then we want to make sure + // that the argument property is a dependency of the current property. + + // As properties are parsed they are added to this dictionary so that after we can find the property instances from the names quickly. + Dictionary argumentDependencyLookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // baseClass = attribute.InnerText; + // namespaceValue = attribute.InnerText; + // resourceNamespaceValue = attribute.InnerText; + foreach (XamlTypes.BaseProperty property in rule.Properties) + { + if (!ParseParameterGroupOrParameter(property, _properties, null, argumentDependencyLookup /*Add to the dictionary properties as they are parsed*/)) + { + return false; + } + } + + // Go through each property and their arguments to set up the correct dependency mappings. + foreach (Property property in Properties) + { + // Get the arguments on the property itself + List arguments = property.Arguments; + + // Find all of the properties in arguments list. + foreach (Argument argument in arguments) + { + Property argumentProperty = null; + if (argumentDependencyLookup.TryGetValue(argument.Parameter, out argumentProperty)) + { + property.DependentArgumentProperties.AddLast(argumentProperty); + } + } + + // Properties may be enumeration types, this would mean they have sub property values which themselves can have arguments. + List values = property.Values; + + // Find all of the properties for the aruments in sub property. + foreach (Value value in values) + { + List valueArguments = value.Arguments; + foreach (Argument argument in valueArguments) + { + Property argumentProperty = null; + + if (argumentDependencyLookup.TryGetValue(argument.Parameter, out argumentProperty)) + { + // If the property contains a value sub property that has a argument then we will declare that the original property has the same dependenecy. + property.DependentArgumentProperties.AddLast(argumentProperty); + } + } + } + } + + return true; + } + + /// + /// Reads in the nodes of the xml file one by one and builds the data structure of all existing properties + /// + private bool ParseParameterGroupOrParameter(XamlTypes.BaseProperty baseProperty, LinkedList propertyList, Property property, Dictionary argumentDependencyLookup) + { + // node is a property + if (!ParseParameter(baseProperty, propertyList, property, argumentDependencyLookup)) + { + return false; + } + + return true; + } + + /// + /// Fills in the property data structure + /// + private bool ParseParameter(XamlTypes.BaseProperty baseProperty, LinkedList propertyList, Property property, Dictionary argumentDependencyLookup) + { + Property propertyToAdd = ObtainAttributes(baseProperty, property); + + if (String.IsNullOrEmpty(propertyToAdd.Name)) + { + propertyToAdd.Name = "AlwaysAppend"; + } + + // generate the list of parameters in order + if (!_switchesAdded.Contains(propertyToAdd.Name)) + { + _switchOrderList.Add(propertyToAdd.Name); + } + + // Inherit the Prefix from the Tool + if (String.IsNullOrEmpty(propertyToAdd.Prefix)) + { + propertyToAdd.Prefix = DefaultPrefix; + } + + // If the property is an enum type, parse that. + XamlTypes.EnumProperty enumProperty = baseProperty as XamlTypes.EnumProperty; + if (enumProperty != null) + { + foreach (XamlTypes.EnumValue enumValue in enumProperty.AdmissibleValues) + { + Value value = new Value(); + + value.Name = enumValue.Name; + value.SwitchName = enumValue.Switch; + if (value.SwitchName == null) + { + value.SwitchName = String.Empty; + } + + value.DisplayName = enumValue.DisplayName; + value.Description = enumValue.Description; + value.Prefix = enumValue.SwitchPrefix; + if (String.IsNullOrEmpty(value.Prefix)) + { + value.Prefix = enumProperty.SwitchPrefix; + } + + if (String.IsNullOrEmpty(value.Prefix)) + { + value.Prefix = DefaultPrefix; + } + + if (enumValue.Arguments.Count > 0) + { + value.Arguments = new List(); + foreach (XamlTypes.Argument argument in enumValue.Arguments) + { + Argument arg = new Argument(); + arg.Parameter = argument.Property; + arg.Separator = argument.Separator; + arg.Required = argument.IsRequired; + value.Arguments.Add(arg); + } + } + + if (value.Prefix == null) + { + value.Prefix = propertyToAdd.Prefix; + } + + propertyToAdd.Values.Add(value); + } + } + + // build the dependencies and the values for a parameter + foreach (XamlTypes.Argument argument in baseProperty.Arguments) + { + // To refactor into a separate func + if (propertyToAdd.Arguments == null) + { + propertyToAdd.Arguments = new List(); + } + + Argument arg = new Argument(); + arg.Parameter = argument.Property; + arg.Separator = argument.Separator; + arg.Required = argument.IsRequired; + propertyToAdd.Arguments.Add(arg); + } + + if (argumentDependencyLookup != null && !argumentDependencyLookup.ContainsKey(propertyToAdd.Name)) + { + argumentDependencyLookup.Add(propertyToAdd.Name, propertyToAdd); + } + + // We've read any enumerated values and any dependencies, so we just + // have to add the property + propertyList.AddLast(propertyToAdd); + return true; + } + + /// + /// Gets all the attributes assigned in the xml file for this parameter or all of the nested switches for + /// this parameter group + /// + private Property ObtainAttributes(XamlTypes.BaseProperty baseProperty, Property parameterGroup) + { + Property parameter; + if (parameterGroup != null) + { + parameter = parameterGroup.Clone(); + } + else + { + parameter = new Property(); + } + + XamlTypes.BoolProperty boolProperty = baseProperty as XamlTypes.BoolProperty; + XamlTypes.DynamicEnumProperty dynamicEnumProperty = baseProperty as XamlTypes.DynamicEnumProperty; + XamlTypes.EnumProperty enumProperty = baseProperty as XamlTypes.EnumProperty; + XamlTypes.IntProperty intProperty = baseProperty as XamlTypes.IntProperty; + XamlTypes.StringProperty stringProperty = baseProperty as XamlTypes.StringProperty; + XamlTypes.StringListProperty stringListProperty = baseProperty as XamlTypes.StringListProperty; + + parameter.IncludeInCommandLine = baseProperty.IncludeInCommandLine; + + if (baseProperty.Name != null) + { + parameter.Name = baseProperty.Name; + } + + if (boolProperty != null && !String.IsNullOrEmpty(boolProperty.ReverseSwitch)) + { + parameter.Reversible = "true"; + } + + // Determine the type for this property. + if (boolProperty != null) + { + parameter.Type = PropertyType.Boolean; + } + else if (enumProperty != null) + { + parameter.Type = PropertyType.String; + } + else if (dynamicEnumProperty != null) + { + parameter.Type = PropertyType.String; + } + else if (intProperty != null) + { + parameter.Type = PropertyType.Integer; + } + else if (stringProperty != null) + { + parameter.Type = PropertyType.String; + } + else if (stringListProperty != null) + { + parameter.Type = PropertyType.StringArray; + } + + // We might need to override this type based on the data source, if it specifies a source type of 'Item'. + if (baseProperty.DataSource != null) + { + if (!String.IsNullOrEmpty(baseProperty.DataSource.SourceType)) + { + if (baseProperty.DataSource.SourceType.Equals("Item", StringComparison.OrdinalIgnoreCase)) + { + parameter.Type = PropertyType.ItemArray; + } + } + } + + if (intProperty != null) + { + parameter.Max = intProperty.MaxValue != null ? intProperty.MaxValue.ToString() : null; + parameter.Min = intProperty.MinValue != null ? intProperty.MinValue.ToString() : null; + } + + if (boolProperty != null) + { + parameter.ReverseSwitchName = boolProperty.ReverseSwitch; + } + + if (baseProperty.Switch != null) + { + parameter.SwitchName = baseProperty.Switch; + } + + if (stringListProperty != null) + { + parameter.Separator = stringListProperty.Separator; + } + + if (baseProperty.Default != null) + { + parameter.DefaultValue = baseProperty.Default; + } + + parameter.Required = baseProperty.IsRequired.ToString().ToLower(CultureInfo.InvariantCulture); + + if (baseProperty.Category != null) + { + parameter.Category = baseProperty.Category; + } + + if (baseProperty.DisplayName != null) + { + parameter.DisplayName = baseProperty.DisplayName; + } + + if (baseProperty.Description != null) + { + parameter.Description = baseProperty.Description; + } + + if (baseProperty.SwitchPrefix != null) + { + parameter.Prefix = baseProperty.SwitchPrefix; + } + + return parameter; + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/XamlDataDrivenToolTask.cs b/src/XMakeTasks/XamlTaskFactory/XamlDataDrivenToolTask.cs new file mode 100644 index 00000000000..0a5f5bf33c0 --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/XamlDataDrivenToolTask.cs @@ -0,0 +1,579 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Threading; +using System.IO; +using System.Runtime.InteropServices; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections.Specialized; +using System.Resources; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks.Xaml +{ + /// + /// Part of the base class for tasks generated by the Xaml task factory. + /// + public abstract class XamlDataDrivenToolTask : ToolTask + { + /// + /// The list of switches in the order they should appear, if set. + /// + private IEnumerable _switchOrderList; + + /// + /// The dictionary that holds all set switches + /// The string is the name of the property, and the CommandLineToolSwitch holds all of the relevant information + /// i.e., switch, boolean value, type, etc. + /// + private Dictionary _activeToolSwitches = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The dictionary holds all of the legal values that are associated with a certain switch. + /// For example, the key Optimization would hold another dictionary as the value, that had the string pairs + /// "Disabled", "/Od"; "MaxSpeed", "/O1"; "MinSpace", "/O2"; "Full", "/Ox" in it. + /// + private Dictionary> _values = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// Any additional options (as a literal string) that may have been specified in the project file + /// We eventually want to get rid of this + /// + private string _additionalOptions = String.Empty; + + /// + /// True if we returned our commands directly from the command line generation and do not need to use the + /// response file (because the command-line is short enough) + /// + private bool _skipResponseFileCommandGeneration; + + /// + /// The task logging helper + /// + private TaskLoggingHelper _logPrivate; + + /// + /// The set of active tool switch values. + /// + private Dictionary _activeToolSwitchesValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// An array of non-zero codes which don't cause an error. + /// + private string[] _acceptableNonZeroExitCodes = null; + + /// + /// The command line for this task. + /// + private string _commandLine = null; + + /// + /// Constructor called by the generated task. + /// + protected XamlDataDrivenToolTask(string[] switchOrderList, ResourceManager taskResources) + : base(taskResources) + { + this.InitializeLogger(taskResources); + _switchOrderList = switchOrderList; + + _logPrivate = new TaskLoggingHelper(this); + _logPrivate.TaskResources = AssemblyResources.PrimaryResources; + _logPrivate.HelpKeywordPrefix = "MSBuild."; + } + + /// + /// The command-line template to use, if any. + /// + public string CommandLineTemplate + { + get; + set; + } + + /// + /// The additional options that have been set. These are raw switches that + /// go last on the command line. + /// + public string AdditionalOptions + { + get + { + return _additionalOptions; + } + + set + { + _additionalOptions = value; + } + } + + /// + /// Retrieves the list of acceptable non-zero exit codes. + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "NonZero", Justification = "Already shipped as public API")] + public virtual string[] AcceptableNonZeroExitCodes + { + get + { + return _acceptableNonZeroExitCodes; + } + set + { + _acceptableNonZeroExitCodes = value; + } + } + + /// + /// Gets or set the dictionary of active tool switch values. + /// + public Dictionary ActiveToolSwitchesValues + { + get + { + return _activeToolSwitchesValues; + } + + set + { + _activeToolSwitchesValues = value; + } + } + + /// + /// Ordered list of switches + /// + /// ArrayList of switches in declaration order + internal virtual IEnumerable SwitchOrderList + { + get + { + return _switchOrderList; + } + } + + /// + /// The list of all the switches that have been set + /// + protected internal Dictionary ActiveToolSwitches + { + get + { + return _activeToolSwitches; + } + } + + /// + /// Overridden to use UTF16, which works better than UTF8 for older versions of CL, LIB, etc. + /// + protected override Encoding ResponseFileEncoding + { + get + { + return Encoding.Unicode; + } + } + + /// + /// Made a property to abstract out the "if null, call GenerateCommands()" logic. + /// + private string CommandLine + { + get + { + if (_commandLine == null) + { + _commandLine = GenerateCommands(); + } + + return _commandLine; + } + set + { + _commandLine = value; + } + } + + /// + /// Returns true if the property has a value in the list of active tool switches + /// + public bool IsPropertySet(string propertyName) + { + if (!String.IsNullOrEmpty(propertyName)) + { + return _activeToolSwitches.ContainsKey(propertyName); + } + else + { + return false; + } + } + + /// + /// Replace an existing switch with the specifed one of the same name. + /// + public void ReplaceToolSwitch(CommandLineToolSwitch switchToAdd) + { + _activeToolSwitches[switchToAdd.Name] = switchToAdd; + } + + /// + /// Add the value for a switch to the list of active values + /// + public void AddActiveSwitchToolValue(CommandLineToolSwitch switchToAdd) + { + if (((switchToAdd.Type != CommandLineToolSwitchType.Boolean) + || (switchToAdd.BooleanValue == true))) + { + if ((switchToAdd.SwitchValue != String.Empty)) + { + ActiveToolSwitchesValues.Add(switchToAdd.SwitchValue, switchToAdd); + } + } + else + { + if ((switchToAdd.ReverseSwitchValue != String.Empty)) + { + ActiveToolSwitchesValues.Add(switchToAdd.ReverseSwitchValue, switchToAdd); + } + } + } + + /// + /// Override Execute so that we can close the event handle we've created + /// + public override bool Execute() + { + if (!String.IsNullOrEmpty(this.CommandLineTemplate)) + { + UseCommandProcessor = true; + } + else + { + if (String.IsNullOrEmpty(ToolExe)) + { + Log.LogError(ResourceUtilities.FormatResourceString("Xaml.RuleMissingToolName")); + return false; + } + } + + + bool success = base.Execute(); + return success; + } + + /// + /// For testing purposes only + /// Returns the generated command line + /// + internal string GetCommandLine_ForUnitTestsOnly() + { + return GenerateResponseFileCommands(); + } + + /// + /// Checks to see if the switch name is empty + /// + internal bool HasSwitch(string propertyName) + { + if (IsPropertySet(propertyName)) + { + return !String.IsNullOrEmpty(_activeToolSwitches[propertyName].Name); + } + else + { + return false; + } + } + + /// + /// Determine if the return value is in the list of acceptable exit codes. + /// + internal bool IsAcceptableReturnValue() + { + if (AcceptableNonZeroExitCodes != null) + { + foreach (string acceptableExitCode in AcceptableNonZeroExitCodes) + { + if (ExitCode == Convert.ToInt32(acceptableExitCode, CultureInfo.InvariantCulture)) + { + return true; + } + } + } + + return false; + } + + /// + /// Validate the data + /// + internal void PostProcessSwitchList() + { + ValidateRelations(); + ValidateOverrides(); + } + + /// + /// Validate relationships. + /// + internal void ValidateRelations() + { + // do nothing by default. + } + + /// + /// Validate the overrides. + /// + internal void ValidateOverrides() + { + List overriddenSwitches = new List(); + + //Collect the overrided switches + foreach (KeyValuePair overriddenSwitch in ActiveToolSwitches) + { + foreach (KeyValuePair overridePair in overriddenSwitch.Value.Overrides) + { + if (String.Equals(overridePair.Key, (overriddenSwitch.Value.Type == CommandLineToolSwitchType.Boolean && overriddenSwitch.Value.BooleanValue == false) ? overriddenSwitch.Value.ReverseSwitchValue.TrimStart('/') : overriddenSwitch.Value.SwitchValue.TrimStart('/'), StringComparison.OrdinalIgnoreCase)) + { + foreach (KeyValuePair overrideTarget in ActiveToolSwitches) + { + if (!String.Equals(overrideTarget.Key, overriddenSwitch.Key, StringComparison.OrdinalIgnoreCase)) + { + if (String.Equals(overrideTarget.Value.SwitchValue.TrimStart('/'), overridePair.Value, StringComparison.OrdinalIgnoreCase)) + { + overriddenSwitches.Add(overrideTarget.Key); + break; + } + else if ((overrideTarget.Value.Type == CommandLineToolSwitchType.Boolean) && (overrideTarget.Value.BooleanValue == false) && String.Equals(overrideTarget.Value.ReverseSwitchValue.TrimStart('/'), overridePair.Value, StringComparison.OrdinalIgnoreCase)) + { + overriddenSwitches.Add(overrideTarget.Key); + break; + } + } + } + } + } + } + + //Remove the overrided switches + foreach (string overridenSwitch in overriddenSwitches) + { + ActiveToolSwitches.Remove(overridenSwitch); + } + } + + /// + /// Creates the command line and returns it as a string by: + /// 1. Adding all switches with the default set to the active switch list + /// 2. Customizing the active switch list (overridden in derived classes) + /// 3. Iterating through the list and appending switches + /// + protected override string GenerateResponseFileCommands() + { + if (_skipResponseFileCommandGeneration) + { + _skipResponseFileCommandGeneration = false; + return null; + } + else + { + return CommandLine; + } + } + + /// + /// Allows tool to handle the return code. + /// This method will only be called with non-zero exitCode. If the non zero code is an acceptable one then we return true + /// + /// The return value of this method will be used as the task return value + protected override bool HandleTaskExecutionErrors() + { + if (IsAcceptableReturnValue()) + { + return true; + } + + // We don't want to use ToolTask's implementation because it doesn't report the command line that failed. + if (ExitCode == NativeMethods.SE_ERR_ACCESSDENIED) + { + _logPrivate.LogErrorWithCodeFromResources("Xaml.CommandFailedAccessDenied", this.CommandLine, ExitCode); + } + else + { + _logPrivate.LogErrorWithCodeFromResources("Xaml.CommandFailed", this.CommandLine, ExitCode); + } + return false; + } + + /// + /// Generates the command line for the tool. + /// + private string GenerateCommands() + { + PostProcessSwitchList(); + + CommandLineGenerator generator = new CommandLineGenerator(_activeToolSwitches, SwitchOrderList); + generator.CommandLineTemplate = this.CommandLineTemplate; + generator.AdditionalOptions = _additionalOptions; + + CommandLine = generator.GenerateCommandLine(); + return CommandLine; + } + + /// + /// A method that will validate the integer type arguments + /// If the min or max is set, and the value a property is set to is not within + /// the range, the build fails + /// + public bool ValidateInteger(string switchName, int min, int max, int value) + { + if (value < min || value > max) + { + _logPrivate.LogErrorWithCodeFromResources("Xaml.ArgumentOutOfRange", switchName, value); + return false; + } + + return true; + } + + /// + /// A method for the enumerated values a property can have + /// This method checks the value a property is set to, and finds the corresponding switch + /// + /// The switch that a certain value is mapped to + public string ReadSwitchMap(string propertyName, string[][] switchMap, string value) + { + if (switchMap != null) + { + for (int i = 0; i < switchMap.Length; ++i) + { + if (String.Equals(switchMap[i][0], value, StringComparison.OrdinalIgnoreCase)) + { + return switchMap[i][1]; + } + } + + _logPrivate.LogErrorWithCodeFromResources("Xaml.ArgumentOutOfRange", propertyName, value); + } + + return String.Empty; + } + + + /// + /// A method for the enumerated values a property can have + /// This method checks the value a property is set to, and finds the corresponding switch + /// + /// The switch that a certain value is mapped to + public int ReadSwitchMap2(string propertyName, Tuple[]>[] switchMap, string value) + { + if (switchMap != null) + { + for (int i = 0; i < switchMap.Length; ++i) + { + if (String.Equals(switchMap[i].Item1, value, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + _logPrivate.LogErrorWithCodeFromResources("Xaml.ArgumentOutOfRange", propertyName, value); + } + + return -1; + } + + /// + /// Gets a switch value by concatenating the switch's base value (usually the switch itself) with its argument, if any. + /// + public string CreateSwitchValue(string propertyName, string baseSwitch, string separator, Tuple[] arguments) + { + StringBuilder switchValue = new StringBuilder(baseSwitch); + foreach (Tuple argument in arguments) + { + string argName = argument.Item1; + bool isRequired = argument.Item2; + + if (!String.IsNullOrEmpty(argName)) + { + if (!IsPropertySet(argName)) + { + if (isRequired) + { + _logPrivate.LogErrorWithCodeFromResources("Xaml.MissingRequiredArgument", propertyName, argName); + throw new ArgumentException(ResourceUtilities.FormatResourceString("Xaml.MissingRequiredArgument", propertyName, argName)); + } + } + else + { + switchValue.Append(separator).Append(ActiveToolSwitches[argName]); + } + } + } + + return baseSwitch; + } + + /// + /// Default constructor + /// + internal void InitializeLogger(ResourceManager taskResources) + { + _logPrivate = new TaskLoggingHelper(this); + _logPrivate.TaskResources = AssemblyResources.PrimaryResources; + _logPrivate.HelpKeywordPrefix = "MSBuild."; + } + + #region ToolTask Members + + /// + /// This method is called to find the tool if ToolPath wasn't specified. + /// We just return the name of the tool so it can be found on the path. + /// Deriving classes can choose to do something else. + /// + protected override string GenerateFullPathToTool() + { + return ToolName; + } + + /// + /// Validates all of the set properties that have either a string type or an integer type + /// + protected override bool ValidateParameters() + { + return !_logPrivate.HasLoggedErrors && !Log.HasLoggedErrors; + } + + #endregion + + /// + /// Generate the command line if it is less than 32k. + /// + protected override string GenerateCommandLineCommands() + { + // If the command is too long, it will most likely fail. The command line + // arguments passed into any process cannot exceed 32768 characters, but + // depending on the structure of the command (e.g. if it contains embedded + // environment variables that will be expanded), longer commands might work, + // or shorter commands might fail -- to play it safe, we warn at 32000. + // NOTE: cmd.exe has a buffer limit of 8K, but we're not using cmd.exe here, + // so we can go past 8K easily. + if (CommandLine.Length < 32000) + { + _skipResponseFileCommandGeneration = true; + return CommandLine; + } + + _skipResponseFileCommandGeneration = false; + return null; + } + } +} diff --git a/src/XMakeTasks/XamlTaskFactory/XamlTaskFactory.cs b/src/XMakeTasks/XamlTaskFactory/XamlTaskFactory.cs new file mode 100644 index 00000000000..275682ef735 --- /dev/null +++ b/src/XMakeTasks/XamlTaskFactory/XamlTaskFactory.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// The task factory for Xaml data driven tasks. +//----------------------------------------------------------------------- + +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Text; +using System.CodeDom.Compiler; +using System.Reflection; +using System.Xml; +using System.IO; +using System.Threading; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks.Xaml; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Tasks +{ + /// + /// The task factory provider for XAML tasks. + /// + public class XamlTaskFactory : ITaskFactory + { + /// + /// The namespace we put the task in. + /// + private const string XamlTaskNamespace = "XamlTaskNamespace"; + + /// + /// The compiled task assembly. + /// + private Assembly _taskAssembly; + + /// + /// The task type. + /// + private Type _taskType; + + /// + /// The name of the task pulled from the XAML. + /// + public string TaskName + { + get; + private set; + } + + /// + /// The namespace of the task pulled from the XAML. + /// + public string TaskNamespace + { + get; + private set; + } + + /// + /// The contents of the UsingTask body. + /// + public string TaskElementContents + { + get; + private set; + } + + /// + /// The name of this factory. This factory name will be used in error messages. For example + /// Task "Mytask" failed to load from "FactoryName". + /// + public string FactoryName + { + get + { + return "XamlTaskFactory"; + } + } + + /// + /// The task type object. + /// + public Type TaskType + { + get + { + if (_taskType == null) + { + _taskType = _taskAssembly.GetType(String.Concat(XamlTaskNamespace, ".", TaskName), true); + } + + return _taskType; + } + } + + /// + /// MSBuild engine will call this to initialize the factory. This should initialize the factory enough so that the factory can be asked + /// whether or not task names can be created by the factory. + /// + public bool Initialize(string taskName, IDictionary taskParameters, string taskElementContents, IBuildEngine taskFactoryLoggingHost) + { + ErrorUtilities.VerifyThrowArgumentNull(taskName, "taskName"); + ErrorUtilities.VerifyThrowArgumentNull(taskParameters, "taskParameters"); + + TaskLoggingHelper log = new TaskLoggingHelper(taskFactoryLoggingHost, taskName); + log.TaskResources = AssemblyResources.PrimaryResources; + log.HelpKeywordPrefix = "MSBuild."; + + if (taskElementContents == null) + { + log.LogErrorWithCodeFromResources("Xaml.MissingTaskBody"); + return false; + } + + TaskElementContents = taskElementContents.Trim(); + + // Attempt to load the task + TaskParser parser = new TaskParser(); + + bool parseSuccessful = parser.Parse(TaskElementContents, taskName); + + TaskName = parser.GeneratedTaskName; + TaskNamespace = parser.Namespace; + TaskGenerator generator = new TaskGenerator(parser); + + CodeCompileUnit dom = generator.GenerateCode(); + + string pathToMSBuildBinaries = ToolLocationHelper.GetPathToBuildTools(ToolLocationHelper.CurrentToolsVersion); + + // create the code generator options + // Since we are running msbuild 12.0 these had better load. + CompilerParameters compilerParameters = new CompilerParameters + ( + new string[] + { + "System.dll", + Path.Combine(pathToMSBuildBinaries, "Microsoft.Build.Framework.dll"), + Path.Combine(pathToMSBuildBinaries, "Microsoft.Build.Utilities.Core.dll"), + Path.Combine(pathToMSBuildBinaries, "Microsoft.Build.Tasks.Core.dll") + } + + ); + + compilerParameters.GenerateInMemory = true; + compilerParameters.TreatWarningsAsErrors = false; + + // create the code provider + CodeDomProvider codegenerator = CodeDomProvider.CreateProvider("cs"); + CompilerResults results; + bool debugXamlTask = Environment.GetEnvironmentVariable("MSBUILDWRITEXAMLTASK") == "1"; + if (debugXamlTask) + { + using (StreamWriter outputWriter = new StreamWriter(taskName + "_XamlTask.cs")) + { + CodeGeneratorOptions options = new CodeGeneratorOptions(); + options.BlankLinesBetweenMembers = true; + options.BracingStyle = "C"; + + codegenerator.GenerateCodeFromCompileUnit(dom, outputWriter, options); + } + + results = codegenerator.CompileAssemblyFromFile(compilerParameters, taskName + "_XamlTask.cs"); + } + else + { + results = codegenerator.CompileAssemblyFromDom(compilerParameters, new[] { dom }); + } + + try + { + _taskAssembly = results.CompiledAssembly; + } + catch (FileNotFoundException) + { + // This occurs if there is a failure to compile the assembly. We just pass through because we will take care of the failure below. + } + + if (_taskAssembly == null) + { + StringBuilder errorList = new StringBuilder(); + errorList.AppendLine(); + foreach (CompilerError error in results.Errors) + { + if (error.IsWarning) + { + continue; + } + + if (debugXamlTask) + { + errorList.AppendLine(String.Format(Thread.CurrentThread.CurrentUICulture, "({0},{1}) {2}", error.Line, error.Column, error.ErrorText)); + } + else + { + errorList.AppendLine(error.ErrorText); + } + } + + log.LogErrorWithCodeFromResources("Xaml.TaskCreationFailed", errorList.ToString()); + } + + return !log.HasLoggedErrors; + } + + /// + /// Create an instance of the task to be used. + /// + /// The task factory logging host will log messages in the context of the task. + public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) + { + string fullTaskName = String.Concat(TaskNamespace, ".", TaskName); + return (ITask)_taskAssembly.CreateInstance(fullTaskName); + } + + /// + /// Cleans up any context or state that may have been built up for a given task. + /// + /// The task to clean up. + /// + /// For many factories, this method is a no-op. But some factories may have built up + /// an AppDomain as part of an individual task instance, and this is their opportunity + /// to shutdown the AppDomain. + /// + public void CleanupTask(ITask task) + { + ErrorUtilities.VerifyThrowArgumentNull(task, "task"); + } + + /// + /// Get a list of parameters for the task. + /// + public TaskPropertyInfo[] GetTaskParameters() + { + PropertyInfo[] infos = TaskType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var propertyInfos = new TaskPropertyInfo[infos.Length]; + for (int i = 0; i < infos.Length; i++) + { + propertyInfos[i] = new TaskPropertyInfo( + infos[i].Name, + infos[i].PropertyType, + infos[i].GetCustomAttributes(typeof(OutputAttribute), false).Length > 0, + infos[i].GetCustomAttributes(typeof(RequiredAttribute), false).Length > 0); + } + + return propertyInfos; + } + } +} diff --git a/src/XMakeTasks/XmlPeek.cs b/src/XMakeTasks/XmlPeek.cs new file mode 100644 index 00000000000..b3459e3a75a --- /dev/null +++ b/src/XMakeTasks/XmlPeek.cs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Returns the value specified by XPath. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Xml; +using System.Xml.Xsl; +using System.Xml.XPath; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task that returns values as specified by XPath Query + /// from an XML file. + /// + public class XmlPeek : TaskExtension + { + #region Members + /// + /// The XML input as a file path. + /// + private ITaskItem _xmlInputPath; + + /// + /// The XML input as a string. + /// + private string _xmlContent; + + /// + /// The XPath Query. + /// + private string _query; + + /// + /// The results that this task will return. + /// + private ITaskItem[] _result; + + /// + /// The namespaces for XPath query's prefixes. + /// + private string _namespaces; + #endregion + + #region Properties + /// + /// The XML input as a file path. + /// + public ITaskItem XmlInputPath + { + get + { + return _xmlInputPath; + } + + set + { + _xmlInputPath = value; + } + } + + /// + /// The XML input as a string. + /// + public string XmlContent + { + get + { + return _xmlContent; + } + + set + { + _xmlContent = value; + } + } + + /// + /// The XPath Query. + /// + public string Query + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_query, "Query"); + return _query; + } + + set + { + _query = value; + } + } + + /// + /// The results returned by this task. + /// + [Output] + public ITaskItem[] Result + { + get + { + return _result; + } + } + + /// + /// The namespaces for XPath query's prefixes. + /// + public string Namespaces + { + get + { + return _namespaces; + } + + set + { + _namespaces = value; + } + } + #endregion + + /// + /// Executes the XMLPeek task. + /// + /// true if transformation succeeds. + public override bool Execute() + { + XmlInput xmlinput; + ErrorUtilities.VerifyThrowArgumentNull(_query, "Query"); + + try + { + xmlinput = new XmlInput(_xmlInputPath, _xmlContent); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPeek.ArgumentError", e.Message); + return false; + } + + XPathDocument xpathdoc; + try + { + // Load the XPath Document + using (XmlReader xr = xmlinput.CreateReader()) + { + xpathdoc = new XPathDocument(xr); + xr.Close(); + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPeekPoke.InputFileError", _xmlInputPath.ItemSpec, e.Message); + return false; + } + finally + { + xmlinput.CloseReader(); + } + + XPathNavigator nav = xpathdoc.CreateNavigator(); + XPathExpression expr = null; + try + { + // Create the expression from query + expr = nav.Compile(_query); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPeekPoke.XPathError", _query, e.Message); + return false; + } + + // Create the namespace manager and parse the input. + XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(nav.NameTable); + + try + { + LoadNamespaces(ref xmlNamespaceManager, _namespaces); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPeek.NamespacesError", e.Message); + return false; + } + + try + { + expr.SetContext(xmlNamespaceManager); + } + catch (XPathException e) + { + Log.LogErrorWithCodeFromResources("XmlPeek.XPathContextError", e.Message); + return false; + } + + XPathNodeIterator iter = nav.Select(expr); + + List peekValues = new List(); + while (iter.MoveNext()) + { + if (iter.Current.NodeType == XPathNodeType.Attribute + || iter.Current.NodeType == XPathNodeType.Text) + { + peekValues.Add(iter.Current.Value); + } + else + { + peekValues.Add(iter.Current.OuterXml); + } + } + + _result = new ITaskItem[peekValues.Count]; + int i = 0; + foreach (string item in peekValues) + { + _result[i++] = new TaskItem(item); + + // This can be logged a lot, so low importance + Log.LogMessageFromResources(MessageImportance.Low, "XmlPeek.Found", item); + } + + if (_result.Length == 0) + { + // Logged no more than once per execute of this task + Log.LogMessageFromResources("XmlPeek.NotFound"); + } + + return true; + } + + /// + /// Loads the namespaces specified at Namespaces parameter to XmlNSManager. + /// + /// The namespace manager to load namespaces to. + /// The namespaces as XML snippet. + private void LoadNamespaces(ref XmlNamespaceManager namespaceManager, string namepaces) + { + XmlDocument doc = new XmlDocument(); + try + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader reader = XmlReader.Create(new StringReader("" + namepaces + ""), settings)) + { + doc.Load(reader); + } + } + catch (XmlException xe) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPeek.NamespacesParameterNotWellFormed"), xe); + } + + XmlNodeList xnl = doc.SelectNodes("/Namespaces/*[local-name() = 'Namespace']"); + for (int i = 0; i < xnl.Count; i++) + { + XmlNode xn = xnl[i]; + + if (xn.Attributes["Prefix"] == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPeek.NamespacesParameterNoAttribute", "Name")); + } + + if (xn.Attributes["Uri"] == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPeek.NamespacesParameterNoAttribute", "Uri")); + } + + namespaceManager.AddNamespace(xn.Attributes["Prefix"].Value, xn.Attributes["Uri"].Value); + } + } + + /// + /// This class prepares XML input from XMLInputPath and XMLContent parameters + /// + internal class XmlInput + { + /// + /// What XML input type are we at. + /// + private XmlModes _xmlMode; + + /// + /// This either contains the raw Xml or the path to Xml file. + /// + private string _data; + + /// + /// Filestream used to read XML. + /// + private FileStream _fs; + + /// + /// Constructor. + /// Only one parameter should be non null or will throw ArgumentException. + /// + /// The path to XML file or null. + /// The raw XML. + public XmlInput(ITaskItem xmlInputPath, string xmlContent) + { + if (xmlInputPath != null && xmlContent != null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPeek.XmlInput.TooMany")); + } + else if (xmlInputPath == null && xmlContent == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPeek.XmlInput.TooFew")); + } + + if (xmlInputPath != null) + { + _xmlMode = XmlModes.XmlFile; + _data = xmlInputPath.ItemSpec; + } + else + { + _xmlMode = XmlModes.Xml; + _data = xmlContent; + } + } + + /// + /// Possible accepted types of XML input. + /// + public enum XmlModes + { + /// + /// If the mode is a XML file. + /// + XmlFile, + + /// + /// If the mode is a raw XML. + /// + Xml + } + + /// + /// Returns the current mode of the XmlInput + /// + public XmlModes XmlMode + { + get + { + return _xmlMode; + } + } + + /// + /// Creates correct reader based on the input type. + /// + /// The XmlReader object + public XmlReader CreateReader() + { + if (_xmlMode == XmlModes.XmlFile) + { + _fs = new FileStream(_data, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + return XmlReader.Create(_fs); + } + else // xmlModes.Xml + { + return XmlReader.Create(new StringReader(_data)); + } + } + + /// + /// Closes the reader. + /// + public void CloseReader() + { + if (_fs != null) + { + _fs.Close(); + _fs = null; + } + } + } + } +} diff --git a/src/XMakeTasks/XmlPoke.cs b/src/XMakeTasks/XmlPoke.cs new file mode 100644 index 00000000000..33d70cecc0c --- /dev/null +++ b/src/XMakeTasks/XmlPoke.cs @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Returns the value specified by XPath. +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Xml; +using System.Xml.Xsl; +using System.Xml.XPath; + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task that sets values as specified by XPath Query + /// into a XML file. + /// + public class XmlPoke : TaskExtension + { + #region Members + /// + /// The XML input as file paths. + /// + private ITaskItem _xmlInputPath; + + /// + /// The XPath Query. + /// + private string _query; + + /// + /// The property that this task will set. + /// + private ITaskItem _value; + + /// + /// The namespaces for XPath query's prefixes. + /// + private string _namespaces; + #endregion + + #region Properties + /// + /// The XML input as file path. + /// + public ITaskItem XmlInputPath + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_xmlInputPath, "XmlInputPath"); + return _xmlInputPath; + } + + set + { + _xmlInputPath = value; + } + } + + /// + /// The XPath Query. + /// + public string Query + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_query, "Query"); + return _query; + } + + set + { + _query = value; + } + } + + /// + /// The output file. + /// + [Required] + public ITaskItem Value + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_value, "Value"); + return _value; + } + + set + { + _value = value; + } + } + + /// + /// The namespaces for XPath query's prefixes. + /// + public string Namespaces + { + get + { + return _namespaces; + } + + set + { + _namespaces = value; + } + } + #endregion + + /// + /// Executes the XMLPoke task. + /// + /// true if transformation succeeds. + public override bool Execute() + { + ErrorUtilities.VerifyThrowArgumentNull(_query, "Query"); + ErrorUtilities.VerifyThrowArgumentNull(_value, "Value"); + ErrorUtilities.VerifyThrowArgumentNull(_xmlInputPath, "XmlInputPath"); + + // Load the XPath Document + XmlDocument xmlDoc = new XmlDocument(); + + try + { + using (FileStream fs = new FileStream(_xmlInputPath.ItemSpec, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + XmlReaderSettings xrs = new XmlReaderSettings(); + xrs.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader sr = XmlReader.Create(fs, xrs)) + { + xmlDoc.Load(sr); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPeekPoke.InputFileError", _xmlInputPath.ItemSpec, e.Message); + return false; + } + + XPathNavigator nav = xmlDoc.CreateNavigator(); + XPathExpression expr = null; + + try + { + // Create the expression from query + expr = nav.Compile(_query); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPeekPoke.XPathError", _query, e.Message); + return false; + } + + // Create the namespace manager and parse the input. + XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(nav.NameTable); + + // Arguments parameters + try + { + LoadNamespaces(ref xmlNamespaceManager, _namespaces); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPoke.NamespacesError", e.Message); + return false; + } + + try + { + expr.SetContext(xmlNamespaceManager); + } + catch (XPathException e) + { + Log.LogErrorWithCodeFromResources("XmlPoke.XPathContextError", e.Message); + return false; + } + + XPathNodeIterator iter = nav.Select(expr); + + while (iter.MoveNext()) + { + try + { + iter.Current.InnerXml = _value.ItemSpec; + Log.LogMessageFromResources(MessageImportance.Low, "XmlPoke.Replaced", iter.Current.Name, _value.ItemSpec); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XmlPoke.PokeError", _value.ItemSpec, e.Message); + return false; + } + } + + Log.LogMessageFromResources(MessageImportance.Normal, "XmlPoke.Count", iter.Count); + + if (iter.Count > 0) + { + xmlDoc.Save(_xmlInputPath.ItemSpec); + } + + return true; + } + + /// + /// Loads the namespaces specified at Namespaces parameter to XmlNSManager. + /// + /// The namespace manager to load namespaces to. + /// The namespaces as XML snippet. + private void LoadNamespaces(ref XmlNamespaceManager namespaceManager, string namepaces) + { + XmlDocument doc = new XmlDocument(); + try + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Ignore; + + using (XmlReader reader = XmlReader.Create(new StringReader("" + namepaces + ""), settings)) + { + doc.Load(reader); + } + } + catch (XmlException xe) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPoke.NamespacesParameterNotWellFormed"), xe); + } + + XmlNodeList xnl = doc.SelectNodes("/Namespaces/*[local-name() = 'Namespace']"); + + for (int i = 0; i < xnl.Count; i++) + { + XmlNode xn = xnl[i]; + + if (xn.Attributes["Prefix"] == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPoke.NamespacesParameterNoAttribute", "Name")); + } + + if (xn.Attributes["Uri"] == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XmlPoke.NamespacesParameterNoAttribute", "Uri")); + } + + namespaceManager.AddNamespace(xn.Attributes["Prefix"].Value, xn.Attributes["Uri"].Value); + } + } + } +} diff --git a/src/XMakeTasks/XslTransformation.cs b/src/XMakeTasks/XslTransformation.cs new file mode 100644 index 00000000000..4e1e3a87cfb --- /dev/null +++ b/src/XMakeTasks/XslTransformation.cs @@ -0,0 +1,651 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +//----------------------------------------------------------------------- +// +// Transforms Xml with Xsl. +//----------------------------------------------------------------------- + +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security; +using System.Security.Permissions; +using System.Text; +using System.Xml; +using System.Xml.Xsl; +using System.Xml.XPath; + +namespace Microsoft.Build.Tasks +{ + /// + /// A task that transforms a XML input with an XSLT or Compiled XSLT + /// and outputs to screen or specified file. + /// + public class XslTransformation : TaskExtension + { + #region Members + /// + /// The XML input as file paths. + /// + private ITaskItem[] _xmlInputPaths; + + /// + /// The XML input as string. + /// + private string _xmlString; + + /// + /// The XSLT input as file path. + /// + private ITaskItem _xsltFile; + + /// + /// The XSLT input as string. + /// + private string _xsltString; + + /// + /// The XSLT input as compiled dll. + /// + private ITaskItem _xsltCompiledDll; + + /// + /// The output files. + /// + private ITaskItem[] _outputPaths; + + /// + /// The parameters to XSLT Input document. + /// + private string _parameters; + + /// + /// Determines whether or not to use trusted settings when loading the Xslt file + /// + private bool _useTrustedSettings = false; + #endregion + + #region Properties + /// + /// The XML input as file path. + /// + public ITaskItem[] XmlInputPaths + { + get + { + return _xmlInputPaths; + } + + set + { + _xmlInputPaths = value; + } + } + + /// + /// The XML input as string. + /// + public string XmlContent + { + get + { + return _xmlString; + } + + set + { + _xmlString = value; + } + } + + /// + /// The XSLT input as file path. + /// + public ITaskItem XslInputPath + { + get + { + return _xsltFile; + } + + set + { + _xsltFile = value; + } + } + + /// + /// The XSLT input as string. + /// + public string XslContent + { + get + { + return _xsltString; + } + + set + { + _xsltString = value; + } + } + + /// + /// The XSLT input as compiled dll. + /// + public ITaskItem XslCompiledDllPath + { + get + { + return _xsltCompiledDll; + } + + set + { + _xsltCompiledDll = value; + } + } + + /// + /// The output file. + /// + [Required] + public ITaskItem[] OutputPaths + { + get + { + ErrorUtilities.VerifyThrowArgumentNull(_outputPaths, "OutputPath"); + return _outputPaths; + } + + set + { + _outputPaths = value; + } + } + + /// + /// The parameters to XSLT Input document. + /// + public string Parameters + { + get + { + return _parameters; + } + + set + { + _parameters = value; + } + } + + /// + /// Determines whether or not to use trusted settings. Default is false. + /// + public bool UseTrustedSettings + { + get + { + return _useTrustedSettings; + } + + set + { + _useTrustedSettings = value; + } + } + #endregion + + /// + /// Executes the XslTransform task. + /// + /// true if transformation succeeds. + public override bool Execute() + { + XmlInput xmlinput; + XsltInput xsltinput; + ErrorUtilities.VerifyThrowArgumentNull(_outputPaths, "OutputPath"); + + // Load XmlInput, XsltInput parameters + try + { + xmlinput = new XmlInput(_xmlInputPaths, _xmlString); + xsltinput = new XsltInput(_xsltFile, _xsltString, _xsltCompiledDll, Log); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XslTransform.ArgumentError", e.Message); + return false; + } + + // Check if OutputPath has same number of parameters as xmlInputPaths. + if (_xmlInputPaths != null && _xmlInputPaths.Length != _outputPaths.Length) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", _outputPaths.Length, _xmlInputPaths.Length, "XmlContent", "XmlInputPaths"); + return false; + } + + // Check if OutputPath has 1 parameter if xmlString is specified. + if (_xmlString != null && _outputPaths.Length != 1) + { + Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", _outputPaths.Length, 1, "XmlContent", "OutputPaths"); + return false; + } + + XsltArgumentList arguments; + + // Arguments parameters + try + { + arguments = ProcessXsltArguments(_parameters); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XslTransform.XsltArgumentsError", e.Message); + return false; + } + + XslCompiledTransform xslct; + + // Load the XSLT + try + { + xslct = xsltinput.LoadXslt(UseTrustedSettings); + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XslTransform.XsltLoadError", e.Message); + return false; + } + + // Do the transformation. + try + { + for (int i = 0; i < xmlinput.Count; i++) + { + using (XmlWriter xmlWriter = XmlWriter.Create(_outputPaths[i].ItemSpec, xslct.OutputSettings)) + { + using (XmlReader xr = xmlinput.CreateReader(i)) + { + xslct.Transform(xr, arguments, xmlWriter); + } + + xmlWriter.Close(); + } + } + } + catch (Exception e) + { + if (ExceptionHandling.IsCriticalException(e)) + { + throw; + } + + Log.LogErrorWithCodeFromResources("XslTransform.TransformError", e.Message); + return false; + } + + // Copy Metadata + if (xmlinput.XmlMode == XmlInput.XmlModes.XmlFile) + { + for (int i = 0; i < _xmlInputPaths.Length; i++) + { + _xmlInputPaths[i].CopyMetadataTo(_outputPaths[i]); + } + } + + return true; + } + + /// + /// Takes the raw XML and loads XsltArgumentList + /// + /// The raw XML that holds each parameter as + /// XsltArgumentList + private static XsltArgumentList ProcessXsltArguments(string xsltParametersXml) + { + XsltArgumentList arguments = new XsltArgumentList(); + if (xsltParametersXml == null) + { + return arguments; + } + + XmlDocument doc = new XmlDocument(); + try + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Ignore; + XmlReader reader = XmlReader.Create(new StringReader("" + xsltParametersXml + ""), settings); + doc.Load(reader); + } + catch (XmlException xe) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XsltParameterNotWellFormed"), xe); + } + + XmlNodeList xnl = doc.SelectNodes("/XsltParameters/*[local-name() = 'Parameter']"); + + for (int i = 0; i < xnl.Count; i++) + { + XmlNode xn = xnl[i]; + + if (xn.Attributes["Name"] == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XsltParameterNoAttribute", "Name")); + } + + if (xn.Attributes["Value"] == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XsltParameterNoAttribute", "Value")); + } + + string ns = String.Empty; + if (xn.Attributes["Namespace"] != null) + { + ns = xn.Attributes["Namespace"].Value; + } + + arguments.AddParam(xn.Attributes["Name"].Value, ns, xn.Attributes["Value"].Value); + } + + return arguments; + } + + #region Supporting classes for input + /// + /// This class prepares XML input from XmlFile and Xml parameters + /// + internal class XmlInput + { + /// + /// What XML input type are we at. + /// + private XmlModes _xmlMode; + + /// + /// This either contains the raw Xml or the path to Xml file. + /// + private string[] _data; + + /// + /// Constructor. + /// Only one parameter should be non null or will throw ArgumentException. + /// + /// The path to XML file or null. + /// The raw XML. + public XmlInput(ITaskItem[] xmlFile, string xml) + { + if (xmlFile != null && xml != null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XmlInput.TooMany")); + } + else if (xmlFile == null && xml == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XmlInput.TooFew")); + } + + if (xmlFile != null) + { + _xmlMode = XmlModes.XmlFile; + _data = new string[xmlFile.Length]; + for (int i = 0; i < xmlFile.Length; i++) + { + _data[i] = xmlFile[i].ItemSpec; + } + } + else + { + _xmlMode = XmlModes.Xml; + _data = new string[] { xml }; + } + } + + /// + /// Possible accepted types of XML input. + /// + public enum XmlModes + { + /// + /// If the mode is a XML file. + /// + XmlFile, + + /// + /// If the mode is a raw XML. + /// + Xml + } + + /// + /// Returns the count of Xml Inputs + /// + public int Count + { + get + { + return _data.Length; + } + } + + /// + /// Returns the current mode of the XmlInput + /// + public XmlModes XmlMode + { + get + { + return _xmlMode; + } + } + + /// + /// Creates correct reader based on the input type. + /// + /// The XmlReader object + public XmlReader CreateReader(int itemPos) + { + if (_xmlMode == XmlModes.XmlFile) + { + return XmlReader.Create(_data[itemPos]); + } + else // xmlModes.Xml + { + return XmlReader.Create(new StringReader(_data[itemPos])); + } + } + } + + /// + /// This class prepares XSLT input from XsltFile, Xslt and XsltCompiledDll parameters + /// + internal class XsltInput + { + /// + /// What XSLT input type are we at. + /// + private XslModes _xslMode; + + /// + /// Contains the raw XSLT + /// or the path to XSLT file + /// or the path to compiled XSLT dll. + /// + private string _data; + + /// + /// Tool for logging build messages, warnings, and errors + /// + private TaskLoggingHelper _log; + + /// + /// Constructer. + /// Only one parameter should be non null or will throw ArgumentException. + /// + /// The path to XSLT file or null. + /// The raw to XSLT or null. + /// The path to compiled XSLT file or null. + public XsltInput(ITaskItem xsltFile, string xslt, ITaskItem xsltCompiledDll, TaskLoggingHelper logTool) + { + _log = logTool; + if ((xsltFile != null && xslt != null) || + (xsltFile != null && xsltCompiledDll != null) || + (xslt != null && xsltCompiledDll != null)) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XsltInput.TooMany")); + } + else if (xsltFile == null && xslt == null && xsltCompiledDll == null) + { + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.XsltInput.TooFew")); + } + + if (xsltFile != null) + { + _xslMode = XslModes.XsltFile; + _data = xsltFile.ItemSpec; + } + else if (xslt != null) + { + _xslMode = XslModes.Xslt; + _data = xslt; + } + else + { + _xslMode = XslModes.XsltCompiledDll; + _data = xsltCompiledDll.ItemSpec; + } + } + + /// + /// Possible accepted types of XSLT input. + /// + public enum XslModes + { + /// + /// If the mode is a XSLT file. + /// + XsltFile, + + /// + /// If the mode is a raw XSLT. + /// + Xslt, + + /// + /// If the mode is a compiled Xslt dll. + /// + XsltCompiledDll + } + + /// + /// Loads the XSLT to XslCompiledTransform. By default uses Default settings instead of trusted settings. + /// + /// A XslCompiledTransform object. + public XslCompiledTransform LoadXslt() + { + return LoadXslt(false); + } + + /// + /// Loads the XSLT to XslCompiledTransform. By default uses Default settings instead of trusted settings. + /// + /// Determines whether or not to use trusted settings. + /// A XslCompiledTransform object. + public XslCompiledTransform LoadXslt(bool useTrustedSettings) + { + XslCompiledTransform xslct = new XslCompiledTransform(); + XsltSettings settings = XsltSettings.Default; + + switch (_xslMode) + { + case XslModes.Xslt: + xslct.Load(XmlReader.Create(new StringReader(_data)), settings, new XmlUrlResolver()); + break; + case XslModes.XsltFile: + if (useTrustedSettings) + { + settings = XsltSettings.TrustedXslt; + } + else + { + _log.LogMessageFromResources(MessageImportance.Low, "XslTransform.UseTrustedSettings", _data); + } + + xslct.Load(new XPathDocument(XmlReader.Create(_data)), settings, new XmlUrlResolver()); + break; + case XslModes.XsltCompiledDll: + // We accept type in format: assembly_name[;type_name]. type_name may be omitted if assembly has just one type defined + string dll = _data; + string[] pair = dll.Split(';'); + string assemblyPath = pair[0]; + string typeName = (pair.Length == 2) ? pair[1] : null; + + Type t = FindType(assemblyPath, typeName); + xslct.Load(t); + break; + default: + ErrorUtilities.ThrowInternalErrorUnreachable(); + break; + } + + return xslct; + } + + /// + /// Find the type from an assembly and loads it. + /// + /// The path to assembly. + /// The type name. + /// Found type. + private static Type FindType(string assemblyPath, string typeName) + { + AssemblyName assemblyName = new AssemblyName(); + assemblyName.CodeBase = assemblyPath; + Assembly loadedAssembly = Assembly.Load(assemblyName); + if (typeName != null) + { + return loadedAssembly.GetType(typeName); + } + else + { + List types = new List(); + foreach (Type type in loadedAssembly.GetTypes()) + { + if (!type.Name.StartsWith("$", StringComparison.Ordinal)) + { + types.Add(type); + } + } + + if (types.Count == 1) + { + return types[0]; + } + + throw new ArgumentException(ResourceUtilities.FormatResourceString("XslTransform.MustSpecifyType", assemblyPath)); + } + } + } + #endregion + } +} diff --git a/src/XMakeTasks/native.rc b/src/XMakeTasks/native.rc new file mode 100644 index 00000000000..1445200005f --- /dev/null +++ b/src/XMakeTasks/native.rc @@ -0,0 +1,4 @@ +#include +#include + +#include diff --git a/src/XMakeTasks/system.design/stronglytypedresourcebuilder.cs b/src/XMakeTasks/system.design/stronglytypedresourcebuilder.cs new file mode 100644 index 00000000000..744e8d825b6 --- /dev/null +++ b/src/XMakeTasks/system.design/stronglytypedresourcebuilder.cs @@ -0,0 +1,702 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// ************************************************************************************************ +// ************************************************************************************************ +// Extracted in order to remove System.Designer dependency from Microsoft.Build.Tasks. +// When they add typeforwarders for StronglyTypedResourceBuilder this can be removed. +// Almost completely unchanged, except for visibility and namespace. +// +// When making changes to this file, consider whether the changes also need to be made in +// venus\project\webapp\package\GlobalResourceProxyGenerator.cs +// ************************************************************************************************ +// ************************************************************************************************ + + +//------------------------------------------------------------------------------ +// +//------------------------------------------------------------------------------ +/*============================================================ +** +** Purpose: Uses CodeDOM to produce a resource class, with a +** strongly-typed property for every resource. +** For usability & eventually some perf work. +** +** +===========================================================*/ + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Resources; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Reflection; +using System.Globalization; +using System.Xml; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Build.Shared; + +/* + Plan for the future: + Ideally we will be able to change the property getters here to use a + resource index calculated at build time, being the x'th resource in the + .resources file. We would then call something like + ResourceManager.LookupResourceByIndex(). This would avoid some string + comparisons during resource lookup. + + This would require work from ResourceReader and/or ResourceWriter (or + a standalone, separate utility with duplicated code) to calculate the + id's. It would also require that all satellite assemblies use the same + resource ID's as the main assembly. This would require dummy entries + for resources in all satellites. + + I'm not sure how much time this will save, but it does sound like an + interesting idea. + -- Brian Grunkemeyer, 1/16/2003 +*/ + + +namespace Microsoft.Build.Tasks +{ + internal static class StronglyTypedResourceBuilder + { + // Note - if you add a new property to the class, add logic to reject + // keys of that name in VerifyResourceNames. + private const String ResMgrFieldName = "resourceMan"; + private const String ResMgrPropertyName = "ResourceManager"; + private const String CultureInfoFieldName = "resourceCulture"; + private const String CultureInfoPropertyName = "Culture"; + + // When fixing up identifiers, we will replace all these chars with + // a single char that is valid in identifiers, such as '_'. + private static readonly char[] s_charsToReplace = new char[] { ' ', + '\u00A0' /* non-breaking space */, '.', ',', ';', '|', '~', '@', + '#', '%', '^', '&', '*', '+', '-', '/', '\\', '<', '>', '?', '[', + ']', '(', ')', '{', '}', '\"', '\'', ':', '!' }; + private const char ReplacementChar = '_'; + + private const String DocCommentSummaryStart = ""; + private const String DocCommentSummaryEnd = ""; + + // Maximum size of a String resource to show in the doc comment for its property + private const int DocCommentLengthThreshold = 512; + + // Save the strings for better doc comments. + internal sealed class ResourceData + { + private Type _type; + private String _valueAsString; + + internal ResourceData(Type type, String valueAsString) + { + _type = type; + _valueAsString = valueAsString; + } + + internal Type Type + { + get { return _type; } + } + + internal String ValueAsString + { + get { return _valueAsString; } + } + } + + + internal static CodeCompileUnit Create(IDictionary resourceList, String baseName, String generatedCodeNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + return Create(resourceList, baseName, generatedCodeNamespace, null, codeProvider, internalClass, out unmatchable); + } + + internal static CodeCompileUnit Create(IDictionary resourceList, String baseName, String generatedCodeNamespace, String resourcesNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + if (resourceList == null) + throw new ArgumentNullException("resourceList"); + + Dictionary resourceTypes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (DictionaryEntry de in resourceList) + { + ResXDataNode node = de.Value as ResXDataNode; + ResourceData data; + if (node != null) + { + string keyname = (string)de.Key; + if (keyname != node.Name) + throw new ArgumentException(SR.GetString(SR.MismatchedResourceName, keyname, node.Name)); + + String typeName = node.GetValueTypeName((AssemblyName[])null); + Type type = Type.GetType(typeName); + String valueAsString = node.GetValue((AssemblyName[])null).ToString(); + data = new ResourceData(type, valueAsString); + } + else + { + // If the object is null, we don't have a good way of guessing the + // type. Use Object. This will be rare after WinForms gets away + // from their resource pull model in Whidbey M3. + Type type = (de.Value == null) ? typeof(Object) : de.Value.GetType(); + data = new ResourceData(type, de.Value == null ? null : de.Value.ToString()); + } + resourceTypes.Add((String)de.Key, data); + } + + // Note we still need to verify the resource names are valid language + // keywords, etc. So there's no point to duplicating the code above. + + return InternalCreate(resourceTypes, baseName, generatedCodeNamespace, resourcesNamespace, codeProvider, internalClass, out unmatchable); + } + + private static CodeCompileUnit InternalCreate(Dictionary resourceList, String baseName, String generatedCodeNamespace, String resourcesNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + if (baseName == null) + throw new ArgumentNullException("baseName"); + if (codeProvider == null) + throw new ArgumentNullException("codeProvider"); + + // Keep a list of errors describing known strings that couldn't be + // fixed up (like "4"), as well as listing all duplicate resources that + // were fixed up to the same name (like "A B" and "A-B" both going to + // "A_B"). + ArrayList errors = new ArrayList(0); + + // Verify the resource names are valid property names, and they don't + // conflict. This includes checking for language-specific keywords, + // translating spaces to underscores, etc. + SortedList cleanedResourceList; + Hashtable reverseFixupTable; + cleanedResourceList = VerifyResourceNames(resourceList, codeProvider, errors, out reverseFixupTable); + + // Verify the class name is legal. + String className = baseName; + // Attempt to fix up class name, and throw an exception if it fails. + if (!codeProvider.IsValidIdentifier(className)) + { + String fixedClassName = VerifyResourceName(className, codeProvider); + if (fixedClassName != null) + className = fixedClassName; + } + if (!codeProvider.IsValidIdentifier(className)) + throw new ArgumentException(SR.GetString(SR.InvalidIdentifier, className)); + + // If we have a namespace, verify the namespace is legal, + // attempting to fix it up if needed. + if (!String.IsNullOrEmpty(generatedCodeNamespace)) + { + if (!codeProvider.IsValidIdentifier(generatedCodeNamespace)) + { + String fixedNamespace = VerifyResourceName(generatedCodeNamespace, codeProvider, true); + if (fixedNamespace != null) + generatedCodeNamespace = fixedNamespace; + } + // Note we cannot really ensure that the generated code namespace + // is a valid identifier, as namespaces can have '.' and '::', but + // identifiers cannot. + } + + CodeCompileUnit ccu = new CodeCompileUnit(); + ccu.ReferencedAssemblies.Add("System.dll"); + + ccu.UserData.Add("AllowLateBound", false); + ccu.UserData.Add("RequireVariableDeclaration", true); + + CodeNamespace ns = new CodeNamespace(generatedCodeNamespace); + ns.Imports.Add(new CodeNamespaceImport("System")); + ccu.Namespaces.Add(ns); + + // Generate class + CodeTypeDeclaration srClass = new CodeTypeDeclaration(className); + ns.Types.Add(srClass); + AddGeneratedCodeAttributeforMember(srClass); + + TypeAttributes ta = internalClass ? TypeAttributes.NotPublic : TypeAttributes.Public; + //ta |= TypeAttributes.Sealed; + srClass.TypeAttributes = ta; + srClass.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + srClass.Comments.Add(new CodeCommentStatement(SR.GetString(SR.ClassDocComment), true)); + + CodeCommentStatement comment = new CodeCommentStatement(SR.GetString(SR.ClassComments1), true); + srClass.Comments.Add(comment); + comment = new CodeCommentStatement(SR.GetString(SR.ClassComments3), true); + srClass.Comments.Add(comment); + + srClass.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + CodeTypeReference debuggerAttrib = new CodeTypeReference(typeof(System.Diagnostics.DebuggerNonUserCodeAttribute)); + debuggerAttrib.Options = CodeTypeReferenceOptions.GlobalReference; + srClass.CustomAttributes.Add(new CodeAttributeDeclaration(debuggerAttrib)); + + CodeTypeReference compilerGenedAttrib = new CodeTypeReference(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute)); + compilerGenedAttrib.Options = CodeTypeReferenceOptions.GlobalReference; + srClass.CustomAttributes.Add(new CodeAttributeDeclaration(compilerGenedAttrib)); + + // Figure out some basic restrictions to the code generation + bool useStatic = internalClass || codeProvider.Supports(GeneratorSupport.PublicStaticMembers); + bool supportsTryCatch = codeProvider.Supports(GeneratorSupport.TryCatchStatements); + EmitBasicClassMembers(srClass, generatedCodeNamespace, baseName, resourcesNamespace, internalClass, useStatic, supportsTryCatch); + + // Now for each resource, add a property + foreach (DictionaryEntry entry in cleanedResourceList) + { + String propertyName = (String)entry.Key; + // The resourceName will be the original value, before fixups, + // if any. + String resourceName = (String)reverseFixupTable[propertyName]; + if (resourceName == null) + resourceName = propertyName; + bool r = DefineResourceFetchingProperty(propertyName, resourceName, (ResourceData)entry.Value, srClass, internalClass, useStatic); + if (!r) + { + errors.Add(entry.Key); + } + } + + unmatchable = (String[])errors.ToArray(typeof(String)); + + // Validate the generated class now + CodeGenerator.ValidateIdentifiers(ccu); + + return ccu; + } + + internal static CodeCompileUnit Create(String resxFile, String baseName, String generatedCodeNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + return Create(resxFile, baseName, generatedCodeNamespace, null, codeProvider, internalClass, out unmatchable); + } + + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] + internal static CodeCompileUnit Create(String resxFile, String baseName, String generatedCodeNamespace, String resourcesNamespace, CodeDomProvider codeProvider, bool internalClass, out String[] unmatchable) + { + if (resxFile == null) + throw new ArgumentNullException("resxFile"); + + // Read the resources from a ResX file into a dictionary - name & type name + Dictionary resourceList = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + using (ResXResourceReader rr = new ResXResourceReader(resxFile)) + { + rr.UseResXDataNodes = true; + foreach (DictionaryEntry de in rr) + { + ResXDataNode node = (ResXDataNode)de.Value; + String typeName = node.GetValueTypeName((AssemblyName[])null); + Type type = Type.GetType(typeName); + String valueAsString = node.GetValue((AssemblyName[])null).ToString(); + ResourceData data = new ResourceData(type, valueAsString); + resourceList.Add((String)de.Key, data); + } + } + + // Note we still need to verify the resource names are valid language + // keywords, etc. So there's no point to duplicating the code above. + + return InternalCreate(resourceList, baseName, generatedCodeNamespace, resourcesNamespace, codeProvider, internalClass, out unmatchable); + } + + private static void AddGeneratedCodeAttributeforMember(CodeTypeMember typeMember) + { + CodeAttributeDeclaration generatedCodeAttrib = new CodeAttributeDeclaration(new CodeTypeReference(typeof(System.CodeDom.Compiler.GeneratedCodeAttribute))); + generatedCodeAttrib.AttributeType.Options = CodeTypeReferenceOptions.GlobalReference; + CodeAttributeArgument toolArg = new CodeAttributeArgument(new CodePrimitiveExpression(typeof(StronglyTypedResourceBuilder).FullName)); + CodeAttributeArgument versionArg = new CodeAttributeArgument(new CodePrimitiveExpression(MSBuildConstants.CurrentAssemblyVersion)); + + generatedCodeAttrib.Arguments.Add(toolArg); + generatedCodeAttrib.Arguments.Add(versionArg); + + typeMember.CustomAttributes.Add(generatedCodeAttrib); + } + + [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] + private static void EmitBasicClassMembers(CodeTypeDeclaration srClass, String nameSpace, String baseName, String resourcesNamespace, bool internalClass, bool useStatic, bool supportsTryCatch) + { + const String tmpVarName = "temp"; + String resMgrCtorParam; + + if (resourcesNamespace != null) + { + if (resourcesNamespace.Length > 0) + resMgrCtorParam = resourcesNamespace + '.' + baseName; + else + resMgrCtorParam = baseName; + } + else if ((nameSpace != null) && (nameSpace.Length > 0)) + { + resMgrCtorParam = nameSpace + '.' + baseName; + } + else + { + resMgrCtorParam = baseName; + } + + CodeAttributeDeclaration suppressMessageAttrib = new CodeAttributeDeclaration(new CodeTypeReference(typeof(System.Diagnostics.CodeAnalysis.SuppressMessageAttribute))); + suppressMessageAttrib.AttributeType.Options = CodeTypeReferenceOptions.GlobalReference; + suppressMessageAttrib.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression("Microsoft.Performance"))); + suppressMessageAttrib.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression("CA1811:AvoidUncalledPrivateCode"))); + + // Emit a constructor - make it protected even if it is a "static" class to allow subclassing + CodeConstructor ctor = new CodeConstructor(); + ctor.CustomAttributes.Add(suppressMessageAttrib); + if (useStatic || internalClass) + ctor.Attributes = MemberAttributes.FamilyAndAssembly; + else + ctor.Attributes = MemberAttributes.Public; + srClass.Members.Add(ctor); + + // Emit _resMgr field. + CodeTypeReference ResMgrCodeTypeReference = new CodeTypeReference(typeof(ResourceManager), CodeTypeReferenceOptions.GlobalReference); + CodeMemberField field = new CodeMemberField(ResMgrCodeTypeReference, ResMgrFieldName); + field.Attributes = MemberAttributes.Private; + if (useStatic) + field.Attributes |= MemberAttributes.Static; + srClass.Members.Add(field); + + // Emit _resCulture field, and leave it set to null. + CodeTypeReference CultureTypeReference = new CodeTypeReference(typeof(CultureInfo), CodeTypeReferenceOptions.GlobalReference); + field = new CodeMemberField(CultureTypeReference, CultureInfoFieldName); + field.Attributes = MemberAttributes.Private; + if (useStatic) + field.Attributes |= MemberAttributes.Static; + srClass.Members.Add(field); + + // Emit ResMgr property + CodeMemberProperty resMgr = new CodeMemberProperty(); + srClass.Members.Add(resMgr); + resMgr.Name = ResMgrPropertyName; + resMgr.HasGet = true; + resMgr.HasSet = false; + resMgr.Type = ResMgrCodeTypeReference; + if (internalClass) + resMgr.Attributes = MemberAttributes.Assembly; + else + resMgr.Attributes = MemberAttributes.Public; + if (useStatic) + resMgr.Attributes |= MemberAttributes.Static; + + // Mark the ResMgr property as advanced + CodeTypeReference editorBrowsableStateTypeRef = new CodeTypeReference(typeof(System.ComponentModel.EditorBrowsableState)); + editorBrowsableStateTypeRef.Options = CodeTypeReferenceOptions.GlobalReference; + + CodeAttributeArgument editorBrowsableStateAdvanced = new CodeAttributeArgument(new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(editorBrowsableStateTypeRef), "Advanced")); + CodeAttributeDeclaration editorBrowsableAdvancedAttribute = new CodeAttributeDeclaration("System.ComponentModel.EditorBrowsableAttribute", + new CodeAttributeArgument[] { editorBrowsableStateAdvanced }); + editorBrowsableAdvancedAttribute.AttributeType.Options = CodeTypeReferenceOptions.GlobalReference; + resMgr.CustomAttributes.Add(editorBrowsableAdvancedAttribute); + + // Emit the Culture property (read/write) + CodeMemberProperty culture = new CodeMemberProperty(); + srClass.Members.Add(culture); + culture.Name = CultureInfoPropertyName; + culture.HasGet = true; + culture.HasSet = true; + culture.Type = CultureTypeReference; + if (internalClass) + culture.Attributes = MemberAttributes.Assembly; + else + culture.Attributes = MemberAttributes.Public; + + if (useStatic) + culture.Attributes |= MemberAttributes.Static; + + // Mark the Culture property as advanced + culture.CustomAttributes.Add(editorBrowsableAdvancedAttribute); + + + /* + // Here's what I'm trying to emit. Since not all languages support + // try/finally, we'll avoid our double lock pattern here. + // This will only hurt perf when we get two threads racing through + // this method the first time. Unfortunate, but not a big deal. + // Also, the .NET Compact Framework doesn't support + // Thread.MemoryBarrier (they only run on processors w/ a strong + // memory model, and who knows about IA64...) + // Once we have Interlocked.CompareExchange, we should use it here. + if (_resMgr == null) { + ResourceManager tmp = new ResourceManager("", typeof("").Assembly); + _resMgr = tmp; + } + return _resMgr; + */ + CodeFieldReferenceExpression field_resMgr = new CodeFieldReferenceExpression(null, ResMgrFieldName); + CodeMethodReferenceExpression object_equalsMethod = new CodeMethodReferenceExpression(new CodeTypeReferenceExpression(typeof(Object)), "ReferenceEquals"); + + CodeMethodInvokeExpression isResMgrNull = new CodeMethodInvokeExpression(object_equalsMethod, new CodeExpression[] { field_resMgr, new CodePrimitiveExpression(null) }); + + // typeof().Assembly + CodePropertyReferenceExpression getAssembly = new CodePropertyReferenceExpression(new CodeTypeOfExpression(new CodeTypeReference(srClass.Name)), "Assembly"); + + // new ResourceManager(resMgrCtorParam, typeof().Assembly); + CodeObjectCreateExpression newResMgr = new CodeObjectCreateExpression(ResMgrCodeTypeReference, new CodePrimitiveExpression(resMgrCtorParam), getAssembly); + + CodeStatement[] init = new CodeStatement[2]; + init[0] = new CodeVariableDeclarationStatement(ResMgrCodeTypeReference, tmpVarName, newResMgr); + init[1] = new CodeAssignStatement(field_resMgr, new CodeVariableReferenceExpression(tmpVarName)); + + resMgr.GetStatements.Add(new CodeConditionStatement(isResMgrNull, init)); + resMgr.GetStatements.Add(new CodeMethodReturnStatement(field_resMgr)); + + // Add a doc comment to the ResourceManager property + resMgr.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + resMgr.Comments.Add(new CodeCommentStatement(SR.GetString(SR.ResMgrPropertyComment), true)); + resMgr.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + + + // Emit code for Culture property + CodeFieldReferenceExpression field_resCulture = new CodeFieldReferenceExpression(null, CultureInfoFieldName); + culture.GetStatements.Add(new CodeMethodReturnStatement(field_resCulture)); + + CodePropertySetValueReferenceExpression newCulture = new CodePropertySetValueReferenceExpression(); + culture.SetStatements.Add(new CodeAssignStatement(field_resCulture, newCulture)); + + // Add a doc comment to Culture property + culture.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + culture.Comments.Add(new CodeCommentStatement(SR.GetString(SR.CulturePropertyComment1), true)); + culture.Comments.Add(new CodeCommentStatement(SR.GetString(SR.CulturePropertyComment2), true)); + culture.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + } + + // Helper method for DefineResourceFetchingProperty + // Truncates a comment string if it is too long and ensures it is safely encoded for XML. + private static string TruncateAndFormatCommentStringForOutput(string commentString) + { + if (commentString != null) + { + // Stop at some length + if (commentString.Length > DocCommentLengthThreshold) + commentString = SR.GetString(SR.StringPropertyTruncatedComment, commentString.Substring(0, DocCommentLengthThreshold)); + + // Encode the comment so it is safe for xml. SecurityElement.Escape is the only method I've found to do this. + commentString = System.Security.SecurityElement.Escape(commentString); + } + + return commentString; + } + + // Defines a property like this: + // {internal|internal} {static} Point MyPoint { + // get { + // Object obj = ResourceManager.GetObject("MyPoint", _resCulture); + // return (Point) obj; } + // } + // Special cases static vs. non-static, as well as internal vs. internal. + // Also note the resource name could contain spaces, etc, while the + // property name has to be a valid language identifier. + [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] + private static bool DefineResourceFetchingProperty(String propertyName, String resourceName, ResourceData data, CodeTypeDeclaration srClass, bool internalClass, bool useStatic) + { + CodeMemberProperty prop = new CodeMemberProperty(); + prop.Name = propertyName; + prop.HasGet = true; + prop.HasSet = false; + + Type type = data.Type; + + if (type == null) + { + return false; + } + + if (type == typeof(MemoryStream)) + type = typeof(UnmanagedMemoryStream); + + // Ensure type is internalally visible. This is necessary to ensure + // users can access classes via a base type. Imagine a class like + // Image or Stream as a internalally available base class, then an + // internal type like MyBitmap or __UnmanagedMemoryStream as an + // internal implementation for that base class. For internalally + // available strongly typed resource classes, we must return the + // internal type. For simplicity, we'll do that for internal strongly + // typed resource classes as well. Ideally we'd also like to check + // for interfaces like IList, but I don't know how to do that without + // special casing collection interfaces & ignoring serialization + // interfaces or IDisposable. + while (!type.IsPublic) + type = type.BaseType; + + CodeTypeReference valueType = new CodeTypeReference(type); + prop.Type = valueType; + if (internalClass) + prop.Attributes = MemberAttributes.Assembly; + else + prop.Attributes = MemberAttributes.Public; + + if (useStatic) + prop.Attributes |= MemberAttributes.Static; + + // For Strings, emit this: + // return ResourceManager.GetString("name", _resCulture); + // For Streams, emit this: + // return ResourceManager.GetStream("name", _resCulture); + // For Objects, emit this: + // Object obj = ResourceManager.GetObject("name", _resCulture); + // return (MyValueType) obj; + CodePropertyReferenceExpression resMgr = new CodePropertyReferenceExpression(null, "ResourceManager"); + CodeFieldReferenceExpression resCultureField = new CodeFieldReferenceExpression((useStatic) ? null : new CodeThisReferenceExpression(), CultureInfoFieldName); + + bool isString = type == typeof(String); + bool isStream = type == typeof(UnmanagedMemoryStream) || type == typeof(MemoryStream); + String getMethodName = String.Empty; + String text = String.Empty; + String valueAsString = TruncateAndFormatCommentStringForOutput(data.ValueAsString); + String typeName = String.Empty; + + if (!isString) // Stream or Object + typeName = TruncateAndFormatCommentStringForOutput(type.ToString()); + + if (isString) + getMethodName = "GetString"; + else if (isStream) + getMethodName = "GetStream"; + else + getMethodName = "GetObject"; + + if (isString) + text = SR.GetString(SR.StringPropertyComment, valueAsString); + else + { // Stream or Object + if (valueAsString == null || + String.Equals(typeName, valueAsString)) // If the type did not override ToString, ToString just returns the type name. + text = SR.GetString(SR.NonStringPropertyComment, typeName); + else + text = SR.GetString(SR.NonStringPropertyDetailedComment, typeName, valueAsString); + } + + prop.Comments.Add(new CodeCommentStatement(DocCommentSummaryStart, true)); + prop.Comments.Add(new CodeCommentStatement(text, true)); + prop.Comments.Add(new CodeCommentStatement(DocCommentSummaryEnd, true)); + + CodeExpression getValue = new CodeMethodInvokeExpression(resMgr, getMethodName, new CodePrimitiveExpression(resourceName), resCultureField); + CodeMethodReturnStatement ret; + if (isString || isStream) + { + ret = new CodeMethodReturnStatement(getValue); + } + else + { + CodeVariableDeclarationStatement returnObj = new CodeVariableDeclarationStatement(typeof(Object), "obj", getValue); + prop.GetStatements.Add(returnObj); + + ret = new CodeMethodReturnStatement(new CodeCastExpression(valueType, new CodeVariableReferenceExpression("obj"))); + } + prop.GetStatements.Add(ret); + + srClass.Members.Add(prop); + return true; + } + + // Returns a valid identifier made from key, or null if it can't. + internal static String VerifyResourceName(String key, CodeDomProvider provider) + { + return VerifyResourceName(key, provider, false); + } + + // Once CodeDom provides a way to verify a namespace name, revisit this method. + private static String VerifyResourceName(String key, CodeDomProvider provider, bool isNameSpace) + { + if (key == null) + throw new ArgumentNullException("key"); + if (provider == null) + throw new ArgumentNullException("provider"); + + foreach (char c in s_charsToReplace) + { + // For namespaces, allow . and :: + if (!(isNameSpace && (c == '.' || c == ':'))) + key = key.Replace(c, ReplacementChar); + } + + if (provider.IsValidIdentifier(key)) + return key; + + // Now try fixing up keywords like "for". + key = provider.CreateValidIdentifier(key); + if (provider.IsValidIdentifier(key)) + return key; + + // make one last ditch effort by prepending _. This fixes keys that start with a number + key = "_" + key; + if (provider.IsValidIdentifier(key)) + return key; + + return null; + } + + private static SortedList VerifyResourceNames(Dictionary resourceList, CodeDomProvider codeProvider, ArrayList errors, out Hashtable reverseFixupTable) + { + reverseFixupTable = new Hashtable(0, StringComparer.InvariantCultureIgnoreCase); + SortedList cleanedResourceList = new SortedList(StringComparer.InvariantCultureIgnoreCase, resourceList.Count); + + foreach (KeyValuePair entry in resourceList) + { + String key = entry.Key; + + // Disallow a property named ResourceManager or Culture - we add + // those. (Any other properties we add also must be listed here) + // Also disallow resource values of type Void. + if (String.Equals(key, ResMgrPropertyName) || + String.Equals(key, CultureInfoPropertyName) || + typeof(void) == entry.Value.Type) + { + errors.Add(key); + continue; + } + + // Ignore WinForms design time and hierarchy information. + // Skip resources starting with $ or >>, like "$this.Text", + // ">>$this.Name" or ">>treeView1.Parent". + if ((key.Length > 0 && key[0] == '$') || + (key.Length > 1 && key[0] == '>' && key[1] == '>')) + { + continue; + } + + + if (!codeProvider.IsValidIdentifier(key)) + { + String newKey = VerifyResourceName(key, codeProvider, false); + if (newKey == null) + { + errors.Add(key); + continue; + } + + // Now see if we've already mapped another key to the + // same name. + String oldDuplicateKey = (String)reverseFixupTable[newKey]; + if (oldDuplicateKey != null) + { + // We can't handle this key nor the previous one. + // Remove the old one. + if (!errors.Contains(oldDuplicateKey)) + errors.Add(oldDuplicateKey); + if (cleanedResourceList.Contains(newKey)) + cleanedResourceList.Remove(newKey); + errors.Add(key); + continue; + } + reverseFixupTable[newKey] = key; + key = newKey; + } + ResourceData value = entry.Value; + if (!cleanedResourceList.Contains(key)) + cleanedResourceList.Add(key, value); + else + { + // There was a case-insensitive conflict between two keys. + // Or possibly one key was fixed up in a way that conflicts + // with another key (ie, "A B" and "A_B"). + String fixedUp = (String)reverseFixupTable[key]; + if (fixedUp != null) + { + if (!errors.Contains(fixedUp)) + errors.Add(fixedUp); + reverseFixupTable.Remove(key); + } + errors.Add(entry.Key); + cleanedResourceList.Remove(key); + } + } + return cleanedResourceList; + } + } +} + diff --git a/src/XMakeTasks/system.design/system.design.txt b/src/XMakeTasks/system.design/system.design.txt new file mode 100644 index 00000000000..3baa32dc3b4 --- /dev/null +++ b/src/XMakeTasks/system.design/system.design.txt @@ -0,0 +1,27 @@ + +# Originated in ndp\fx\src\Designer\System\system.txt +# Extracted in order to remove System.Designer dependency from Microsoft.Build.Tasks. +# When they add typeforwarders for StronglyTypedResourceBuilder this can be removed. +# All irrelevant strings removed from this file. + +#=------------------------------------------------------------------= +# StronglyTypedResourceBuilder resources +# +ClassDocComment= A strongly-typed resource class, for looking up localized strings, etc. +ClassComments1= This class was generated by MSBuild using the GenerateResource task. +#ClassComments2= (not used) +ClassComments3= To add or remove a member, edit your .resx file then rerun MSBuild. +#ClassComments4= (not used) +StringPropertyComment= Looks up a localized string similar to {0}. +StringPropertyTruncatedComment= {0} [rest of string was truncated]"; +NonStringPropertyComment= Looks up a localized resource of type {0}. +NonStringPropertyDetailedComment= Looks up a localized resource of type {0} similar to {1}. +CulturePropertyComment1= Overrides the current thread's CurrentUICulture property for all +CulturePropertyComment2= resource lookups using this strongly typed resource class. +ResMgrPropertyComment= Returns the cached ResourceManager instance used by this class. +MismatchedResourceName=When passing ResXDataNodes as the IDictionary values to StronglyTypedResourceBuilder.Create, the keys must match the corresponding values for ResXDataNode.Name. Key name: '{0}', ResXDataNode.Name: '{1}'. +InvalidIdentifier=Identifier '{0}' is not valid. + + + + diff --git a/src/dir.props b/src/dir.props new file mode 100644 index 00000000000..4c4916a8b1c --- /dev/null +++ b/src/dir.props @@ -0,0 +1,3 @@ + + + diff --git a/src/dir.targets b/src/dir.targets new file mode 100644 index 00000000000..d6662a9ac70 --- /dev/null +++ b/src/dir.targets @@ -0,0 +1,19 @@ + + + + + + $(MSBuildProjectDirectory)\packages.config + true + $(PackagesDir)$(MSBuildProjectFile).package.restored + + + + + + + diff --git a/src/dirs.proj b/src/dirs.proj new file mode 100644 index 00000000000..d79da41495a --- /dev/null +++ b/src/dirs.proj @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + $(BaseOutputPathWithConfig) + + + \ No newline at end of file